近况更新

好久没写博客了,决定唠叨一下最近的生活和工作,避免这个博客长草。

大约从5月份开始,我们项目进入了“闭关室”,几乎是封闭开发的状态,但是我没想到的是,到现在我都没有从闭关室里面出来。这几个月的工作中经历了很多事情,大多都不顺,但是总之还是坚持到现在。最有意思的是我在很短的时间内换了 4 个老板:一开始的老板突然就转岗了,我直接汇报给老板的老板;2天后,老板的老板“拥抱变化”了,我就汇报给老板的老板的老板;最近老板的老板的老板招来一位转岗过来的博士,博士就成了我现在的老板。

虽然工作内容没有太大变化,但是工作的思路变化了很多。一开始的老板在,很多决策上都坚持的比较彻底(现在想想好像有些错了),他走之后,我们的思路几乎就变了,然后我就在原来的基础上做很大的调整。我觉得之前的设计太复杂了,我们作为中台,设计的接口里面入参有二十几个,出参有二十几个,每次接入方来找我的时候,我都要费半天的功夫给他们讲这些参数是干嘛的(大部分时间你们都用不到,这是为了“将来的扩展性”)。接下来我想找时间实现一个新的接口(但是兼容之前的接口,让两种接口同时存在)。关于设计上的复杂性,我跟前老板争论了很多次,我坚持应该保持简洁,代码写的越少越好,给别人的参数应该越少越好,我们现在几乎还在 POC 阶段,应该保持足够的简单来适应将来的变化,快速试错。但是被“要考虑扩展性,要考虑将来的需求,要考虑3年的长远目标”给驳回了。导致现在的系统中充斥着各种复杂的概念,实现一个简单的需求需要大规模的重构。

老板走后,我们在去年成立小组的时候的4个人,就只剩下我一个了。另外两个一个是转岗,一个因为工作地的问题划到另外一个组了。好消息是,8月初入职了一个新同学(我也理所当然地成了新人的师兄),所以应该是剩下我们两个人。然后几乎所有的事情都到我这里了,每天不停的有人找我提新的需求,由于平台(设计上)的复杂性,几乎每个需求我都要和同事分析半天。这些复杂的东西也要讲给新人。说实话我好怕新人一看我们这些过度设计提桶跑路。不过好的地方是,我不再像1年那样抵触这些过度设计了。要是那样的话,估计我现在都有很多代码看不懂。现在我已经把系统的代码看了好几遍,很多过度设计的地方也理解了是想到了将来的什么地方。不必要的地方可以自信的删除掉。

我发现起名字真是写好代码的一个硬实力。我们的代码中有很多 invoker, execute, doExecute, item, call 之类的名字,不同的概念起的名字几乎一样,比如 Action, ActionItem, ActionInfo 这种。导致非常难以理解。该加注释的地方不加,不需要加注释的地方乱加,看起来注释率很高,但是大多数都是废话。非常头疼。

说回工作,这个事情感觉我已经做了两年,但是依然没有满意的成果,有点力不从心了。自己的想法没有机会去做,总是感觉是在一堆摇摇欲坠的系统上做一个火箭。不知道这样做下去会是什么结果。有时候会想,要是我自己完成所有的事情,不需要PD,不需要UI,也不需要前端开发,都是我自己去做,实现的效果可能会更好一些。现在是大家的想法(主要是老板的),然后开五六个会确定怎么做,然后我来做主系统分析设计,告诉其他的几个系统怎么配合,这样做下来几乎每个人只会理解70%(还有人不停地问讲过好几遍的问题,心好累),做出来的东西就差一大截。

唉,不说工作了,越想越烦。

搬了新家,开始扔掉一些“觉得有用但是一直没有用过的东西”,感觉很幸福。处理掉了200来本书,都送出去了,留下十几本还没看完的。感觉换一个环境住是一件很快乐的事情。

最近在玩《荒野乱斗》,也有1万杯了,有玩这个游戏的朋友欢迎加个好友,我的玩家标签是:#PYLL90LR 。最近茶杯头登陆 PS4 了,想玩。糖豆人 PS4 港服的会员赠送了,我也有了,也想玩,可惜没时间。想玩微软的模拟飞行,可惜没电脑。

DDIA 快要看完了,真是一本不可多得的好书!

iredis 最近攒了很多 Feature 没有做,最近下了班会找时间搞一搞。

睡觉去了,不知道自己都写了些啥,好乱。

 

Reaper 使用 ReaFir 插件对音频降噪教程

最近和 Luke 录制了一期播客。本来我们是买了一个话筒寄给嘉宾的,但是这个话筒不知为啥有电流声。所以就临时改成用手机录音了,出了点状况,导致最终出来的音质不是很好,Luke 的音轨底噪有一些强。放弃这一期不发布实在可惜,一筹莫展之际,我把剪辑好的 Mp3 文件发给最近一直在催更我的朋友听了,他说他用 iMovie 降噪之后听着还可以。这让我重新看到了希望,我去找了一些 Reaper 的降噪教程,想尝试对 Luke 的音轨进行降噪。最后的效果还不错,所以这里将我学到的降噪技巧分享一下,方便制作播客的兄弟们参考。

降噪的原理

如果你用过降噪耳机,你应该听说过「主动降噪」 这个词。它的原理就是,耳机会采集周围的环境声音,然后播放音乐的时候,播放一个和环境声音相反的声波。到你耳朵里的时候,因为耳机的反向噪音和环境噪音抵消了,就只剩下播放音乐的声音了。

剪辑技术中的降噪也是一样。先采样环境噪音,然后对整个音轨播放一个反向的声波,这样噪音就被抵消了。

上手操作

ReaFir 是 Reaper 自带的一个插件,可以完成降噪这项工作。Reaper 是一款非常优秀的音频处理软件,配合 UltraSchall4 插件,简直就是我一直在寻找的那个完美的播客剪辑软件!建议剪辑播客的朋友都尝试一下,UltraSchall4 专门为剪辑播客设计,里面的像 Normalize,切分,删除口头禅这样的常用剪辑操作都设计地非常方便。可以大大节省我们的剪辑时间。

话说回来,ReaFir 是 Reaper 自带的一个插件,不需要安装就可以使用。像一次典型的降噪处理,流程主要如下。

下面是我的剪辑界面,如果你没有安装 UltraSchall4 的话,看起来可能会稍微有些不一样。不过没关系,你只需要参考我的这张截图看如何找到对应的按钮就好了。

假设在这个剪辑界面中,我们的第二条音轨——Luke 的音轨底噪比较强。我们要对这条音轨进行降噪。首先在左下角用 FX 按钮打开音效插件界面。第一次打开可能没有我这里的 ReaFir 的,这时候点击 Add,先添加这个 ReaFir 插件。

图2 – 添加 ReaFir 插件

在弹出的窗口中,选择 Cockos(Reaper的公司名字),然后选择 ReaFir。

点击OK,你就会在上面图2中看到 ReaFir,一般会出现在已有插件的下方,需要用鼠标拖动到最上面去,排在其他的效果器前面。至于为什么要挂在最前面,K酱解释的很好:

任何降噪插件,都建议挂在干声轨的第一个位置,ReaFir 也不例外。

为什么呢?比如你在降噪插件之前挂了个EQ,你采样过的噪声特征是经过EQ处理了的,后来你又重新调了一下EQ,然后新的噪声特征就跟采样到的特征不一样了,降噪效果就下降了。再比如你在降噪之前挂了个压缩,底噪部分电平比较低,达不到压缩器的 threshold,采样到的噪声特征是未经压缩处理的,人声的部分被压缩了,噪声的电平也变小了,这样再用采样到的特征去降噪,可能会降噪太多导致失真。总之,请把 ReaFir 放在效果链的第一位。另外,如果你在采样完之后,需要调整干声的音量大小(拖动干声对象上边缘,不是音轨上的推子),那么你需要在调整完之后重新采样。

然后双击 ReaFir,可以打开主界面。降噪这个功能需要设置 Mode 为 Substract,其他参数按照下图调整,经验上是效果比较好的。

接下来就是采样的步骤。首先在上图选中 Automatically build nosie profile,再选中一段嘉宾没有说话的部分,因为我们要对噪音采样。我们是每个人录制一段音轨,后期合成的播客制作方式,所以找一段没有说话的部分是非常简单的。最后点击循环和播放(下图最上的按钮),其实只播放一遍的话也足够啦。

播放之后,你就会看见这里的红色曲线会变化,最终稳定。这就是对噪音的采样啦。

最后一步,就是生成反向声波进行降噪。其实就是将嘉宾的声轨减去这段噪音的 profile。做法其实很简单,就是取消 Automatically build nosie profile 就可以啦。Done!选取一段嘉宾说话的声音,对比一下和之前的区别吧,底噪应该好很多了的。

底噪降的太强,会减去人声,造成音质损失;底噪降的太弱,会降噪不够。所以在上图中,降噪的强度是可以调节的。在这个界面中,按住 Command 键(Windows系统是 Ctrl 键),用鼠标拖动红色线可以调节降噪的强度。可以循环播放一段嘉宾说话的音频,一边播放一边调整,找一个合适的位置。

如果还看不懂的话可以看下这个视频教程:https://www.youtube.com/watch?v=31phzT7pxkk 我就是看这个视频学会的。

广告时间~

这里的(傻瓜)降噪教程就结束啦,如果有问题可以留言问我。我和 Luke 录制的这一期应该就会在最近发布啦,我们聊的是 CI,flake8,lint 等话题。大家在我们的网站关注我们 http://pythonhunter.org/ ,可以通过泛用型客户端订阅我们的播客。

如果喜欢我们的节目,可以给我们捐赠一些钱,我们会用来购买话筒,邮寄给嘉宾,嘉宾用完之后给嘉宾报销邮费收回话筒,购买剪辑软件,制作周边回馈听众等用途,我们会尽最大努力保证节目的质量和音频质量~

捐赠在爱发电:https://afdian.net/@pythonhunter 支付宝和微信可以直接扫码付款。

 

Google BBR 拥塞控制算法简单介绍

最近在网上看了一些 BBR 的资料,简单玩了一下,这里做一个记录。

简单的介绍

BBR 是一种拥塞控制算法。拥塞控制算法的目的简单来说就避免将过多的包发到网络上,造成网络堵车。假如现在网络上比较拥堵,那么 TCP 就使用拥塞控制算法来让发送端发的慢一些。如何在不造成网络拥堵的情况下,又能利用带宽快速发送数据,就是拥塞控制算法要解决的问题了。

这篇文章介绍了常见的拥塞控制算法。一般的拥塞控制算法是通过丢包来认为网络中是否发生拥塞的,这样的问题是,网络是存在一定的概率因为错误而丢包,就会导致无法充分利用带宽;另外一个问题是网络有可能会有 buffer 存在,导致发送端认为的网络容量因为没考虑到 buffer 比实际的要高。

那么 BBR 是怎么解决问题的呢?1)不再将丢包认为是一个网络拥堵的现象;2)分别估计带宽和延迟。

以下是启用的实验。(略水)

新开一个一台 Fedora32 发行版的机器,可以看到默认的拥塞控制算法是 cubic :

升级 Kernel

Kernel 的拥塞控制算法是可插拔的,这意味着我们不需要重新编译 Kernel 就可以更换拥塞控制算法。但是 BBR 要求最低是 4.9.0 才能使用。

但是 Fedora32 的内核已经是 5.x 的了,所以我就不用升级了。

Enable BBR

使用下面的命令可以启用 BBR:

接下来是验证。首先确认 bbr 在可用算法中。下面这个输出应该包含 bbr (顺序不重要):

在验证当前生效的算法应该是 bbr:

最后验证 Kernel 模块已经正确加载:

输出如下:

bbr 的开启就完成了。

Benchmark

对比一下相比于默认使用的 cubic,有没有速度的提升。先安装 httpd,生成一个随机的文件,供下载测试。

然后再开一台旧金山的机器,从开启 bbr 的机器上下载文件测试速度。使用下面这个命令来下载刚刚生成的这个 100MB 的文件。

测试了当前服务器上可用的几种算法,结果如下:

  1. cubic: 8.047663181818182(s)
  2. bbr: 8.084801727272726(s)
  3. reno: 8.060761727272727(s)

bbr 竟然是最慢的一个,-_-||


2024年3月5日更新:

BBR 的优势是需要在特定环境下才能发挥的。近期遇到一个问题,就是 探测 TCP 乱序问题 这篇文章没有解决的:我们的链路大约有 0.5% 的丢包率,在这种情况下 TCP 默认的拥塞控制算法 cubic 很容易被影响,一旦发生丢包,cwnd 会快速下降,导致传输速率很低。

下面是用 iperf3 测试的传输速度。

TCP 使用 iperf3 测试下载速度

可以看到,一旦发生重传,cwnd 就会缩小。cwnd 只能维持在 200 KiB 左右。

把发送端(由于我们的场景是数据单项发送,接收端不需要修改)的拥塞控制算法换成 BBR,重新测试一遍。

TCP 拥塞控制算法换成 BBR

可以看到第5秒的时候依然发生了重传,丢包依旧,但是 cwnd 丝毫没有变化,cwnd 维持在前面的 10 倍左右大小。

实际效果是,原来数据需要 30 多分钟下载完成,换成 BBR 之后,只需要 2 分钟就下载完了。

参考资料:

  1. Implementing SCTP Pluggable Congestion Control for Linux
  2. 中科大 李博杰写的科普
 

JavaBean, POJO, DTO, VO, 傻傻分不清楚?

关于这些概念,网上有很多解释,大多数都是胡说八道。我在 Stack Overflow 上看到了一个版本,认为这个理解是比较合理的。

太长不看版:

DTO 和 VO 用途是一样的,大多数语境下都可以互相替换。JavaBean 是一个惯例,DTO 和 VO 都遵循了这个惯例,所以他们都是 JavaBean. DTO, VO 和 JavaBean 都是 POJO.

JavaBean

Sun 推出了一个 JavaBean 的惯例,遵循了这个惯例的 Java 对象都是 JavaBean. 所以首先要理解的是,JavaBean 并不是一个实现,也不是接口,只是一个惯例。

这些惯例包括:

  • 可以使用一个没有参数的构造器初始化出来这个 Bean;
  • 可以通过 getter/setter 读写 Bean 的属性;
  • 可以序列化这个 Bean;
  • …… (详细的定义请参考 Sun 的说明)

Java语言欠缺属性、事件、多重继承功能。所以,如果要在Java程序中实现一些面向对象编程的常见需求,只能手写大量胶水代码。Java Bean正是编写这套胶水代码的惯用模式或约定。这些约定包括getXxx、setXxx、isXxx、addXxxListener、XxxEvent等。遵守上述约定的类可以用于若干工具或库。

——来源:杨博

有了这个概念(惯例),Spring, Hibernate 这些框架交流、实现起来,都大量使用 Bean 这个概念。比如“注入一个 Bean“,“声明一个 Bean”,你就知道这里的这个 Bean 必须要有无参数的构造函数,必须要有 setter/getter 等等。这些框架在使用的时候,会采用初始化出来 Bean 然后 setXX() 这种方式,构造出来最终的 Bean.

JavaBean 并不是一个接口,更像是为了交流方便的一个名词。

POJO

POJO 的全称是 Plain Old Java Object, 简单又老的 Java 对象。这里的简单是相对来讲的。 EJB 2.x 的 Entity Beans 比较重量,需要实现 javax.ejb 的一些接口。而 POJO 就比较轻量,就是一个 Java 对象,不需要实现任何的接口。

这个术语由 Martin Fowler, Rebecca Parsons 和 Josh MacKenzie 在 2020 年提出:

我们想,为什么人们不愿意使用一个普通的对象,大概是因为普通的对象没有一个 fancy 的名字,所以我们给它们取了一个。

所以 POJO 本质上也是可以方便沟通的术语。PO 在那个年代也不是一个新词:

  • 电话制造行业的 POTS, Plain Old Telephone Service
  • PODS, Plain Old Data Structures, 表示在 C++ 定义数据结构,但是只使用 C 语言里面的 Feature
  • POD, Plain Old Documentation, Perl 语言中的概念

有了 POJO 这个名字,相比框架里面各种的对象概念,就容易理解多了,所以这个概念被很广地使用开来。可以用 POJO 来解释 JavaBean: JavaBean 就是可以序列化的 POJO, 并且有无参构造器,可以使用 getter/setter 来读写属性。

VO

Value Object, 是保存了值的对象。比如 java.lang.Integer. Martin Fowler 对 Value Object 的描述是:

Value Object 是指像 Money 或者 Date Range Object 这样的小对象。对它们来说,值的语义比 id 的语义更加重要。比较它们相等的时候,只要它们的值相等就可以认为是相等,而不需要在意是不是一定是同一个对象。

一个比较好理解的视角是,Value Object 应该是不可变的。如果你要改变一个 Value Object, 你应该创建一个新的然后替换掉原来的,而不应该改变原来的对象的属性。

我认为这个上面这个对 Value Object 的理解是比较合理的,但是早期的 J2EE 教材将 Value Object 描述为一个完全不同的理解,在这个理解中 VO 完全等价于 Data Transfer Object.

DTO

Data Transfer Object. Data Transfer Object 是一个 EJB 中引入的(反)设计模式。思想是将很多数据组装在一个 Value Object 中,然后通过网络传输。而不是在 EJB 中进行很多次远程调用。

DTO 和业务对象的区别是:DTO 除了保存数据,没有其他的行为(方法)。

传统中的 EJB 引入 DTO 主要是为了解决两个问题:

  1. EJB 的 Entity Beans 无法序列化,DTO 解决这个序列化的问题;
  2. DTO 的使用,含蓄的引入了一个组装数据的过程,将所有的数据组装好再返回;

总结一下,对于大多数的情况, DTO 和 VO 是一个东西(Martin Fowler 理解的 VO 不是),并且它们遵守了 JavaBean 的约定。所有的这些东西都是 POJO.

我觉得本质上讲,这些概念也是类似一种“设计模式”的东西,之所以提出来这些概念,还是解决 Java 表达能力的不足,需要用一种通用的做法在 Java 程序员中形成共识,来解决这个问题。比如在 RPC 调用中,我定义了这么一个方法:

然后被同事说不能这么用,这样“没有扩展性”,假如将来要添加一个新的字段 address ,那么你这个接口扩展了一个字段,就不能向后兼容了。

应该这么定义:

这样,这个接口就有扩展性了。实际上,这也是 JavaBean 的一个用法吧。

但是在 Python 中,我们的函数的变量的形式有:

处理起来“兼容”问题就简单多了。

但是 Java 不行,Java 必须将参数封装成对象,然后生成一堆 setter/getter,原本一行代码能搞定的事情,就要写成几十行。

相关参考:

  1. 阿里巴巴Java开发手册中的DO、DTO、BO、AO、VO、POJO定义
  2. https://stackoverflow.com/a/1612671/6931919
 

幻读

数据库的事务应该保证隔离性,这就是说,两个用户(连接)在操作数据库的时候,它们之间的操作应该互相不受影响的。比如用户 A 在修改了 foo 这个变量,但是还没有提交,那么 B 不应该看到这个修改。

但是隔离性的时间不是一件简单的时间,隔离性保证的越高,要么实现的技术手段越复杂,要么性能很低。因为很显然,避免多个事务之间互相影响,就必然要通过加锁来同步操作。如果想要更高的性能,就必须要加更加细粒度的锁,或者使用无锁(更加复杂)的技术来实现。

隔离性一般有这几个问题:

  1. 读到了未提交的数据。这个比较好理解,也比较好解决。一般加锁就行。
  2. 在同一个事务中,两次读到的数据不一样。比如用户 A 开始了一个事务,查询了 foo,这时候用户 B 修改了 foo 并且提交了,然后 A 在事务中又查询了 foo,发现和上一次查询不一样。这也违反了“隔离性”,但是读到的数据却的确是已经提交的数据。这就是“不可重复读”问题。现在用 MVCC 的方式解决,大体意思就是一个事务开始的时候记一个版本(时间戳)v1,其他人所有的操作都会带上新的版本(时间戳),那么在这个事务中,所看到的所有的数据都是先于 v1 的,所有在 v1 之后提交的数据都看不到。MVCC 有一些 tricky 的地方,比如要删除数据的时候,不能直接删除,因为删除了的话,对于所有的事务(即使是在删除操作 commit 之前开始的事务)来说都看不到了。为了解决这个问题,需要在要删除的对象上标记一个新的版本,记为删除。这样之前的事务看到这个数据的时候,发现删除操作是在自己的事务版本之后的,就仍然可以读到这个对象。这样,就可以解决了同一个事务中读到的数据不一样的问题。(但这样其实会带来新的问题,比如数据标记新的版本了,那么索引要不要标记呢?索引怎么做隔离?比如 PostgresSQL 的 COUNT 其实不是一个 O(1) 的操作,而是要遍历每一行数据,检查数据对当前的事务是否是可见的
  3. 幻读。上面说这么多,主要是给这个问题做铺垫,我觉得这个问题最有意思了。本文下面会主要讲讲幻读。

假设这么一种情况:一家医院规定必须至少有 1 名医生在值班,某天值班医生 A 和 B 感到不舒服想请假,按照系统的要求,不能让 A 和 B 同事请假成功的。如果我们使用的数据库没有解决了幻读的问题,那么即使解决了“重复读”问题,也是不正确的。考虑下面这种情况:

这样两人都会同时请假成功。

这个问题并不是“不可重复读”问题,因为两个事务看到的正是事务开头的数据。这个竞争条件产生的根本原因是,这两个事务互相依赖了别的对象,但是别的对象又被更改了。

在这种情况中,我们可以锁住所有的对象,强制他们只能一个接一个的执行。

再举一个幻读的例子:抢占会议室。

抢占程序是这样的,先检查会议室 R 在 某一天 12:00 有没有被抢占,如果没有的话,就可以抢占。那么假如两个事务同时发起的话,就同时看到这个会议室并没有被抢占,就会错误地被抢占两次。被抢占两次的 SQL 过程如下:

这个问题我们也可以通过加锁来解决:将不存在的锁转换成已经存在的。比如,只允许抢占 7 天内的会议室,并且以小时为单位进行抢占。然后就可以将 7 天内的每一个小时作为一个锁,要抢占这个会议室就必须先获得锁。

另外有一些问题不容易实例化不存在的锁。比如注册系统,不允许同一个用户名字被注册两次。但是这种情况可以用数据库的 unique 约束来解决。

不太好解决的问题,防止双重开支。支付系统要保证用户支付的钱不能大于自己的余额,不然公司要倒闭了。那么付款的事务中,我们先检查一下余额是否足够,然后在发起支付,这样可以吗?如果用户同时用电脑付款,也用手机付款,会不会在同一个事务中提交两次呢?

这些问题都有一种共同的模式:

  1. 执行一个 select 查询;
  2. 根据 1 的查询结果,判断当前的操作是否能够继续;
  3. 如果能,就继续,但是这个操作会改变 1 的结果;

因为事务进入的时候 2 都是检查通过的,但是都执行了 3,会影响彼此的 1 的结果,导致出现竞争条件。这就是幻读。

除了“强行加锁”,即上文中提到的方法以外。最靠谱的是使用串行化的隔离级别,即对于程序员来说,数据库好像在串行地一个一个执行事务,这样永远不会有竞争条件。但是因为性能问题,串行化很少使用。

 

注:本文内容大多数都阅读 DDIA 第七章的笔记。