对中台的一些想法

声明:本文仅代表个人想法,不代表我的雇主的想法,也不代表我的同事的想法。

一开始有人开始提出“中台”这个概念的时候,我对此是嗤之以鼻的,认为又一个的炒作的概念。对于中台,我的认识是,将一个垂直领域的东西做成一个 SaaS 平台,它的价值就是避免一定程度的重复建设。举个例子,比如一家公司的很多个 BU 都有播放客户的广告的业务,那么追踪广告的效果就是每一个 BU 做的事情。这个事情需要做 N 遍,但是如果将广告效果追踪变成一个 SaaS 服务,就不需要大家都去做一遍了,只要接入并使用这个服务就可以。这是我理解的中台,可能和其他人的理解有出入。即使在最初提出“中台”的那帮人里面,估计也有很多不同的理解吧。

总之,后来我的 title 也不知不觉地变成了中台开发工程师。这篇文章就讲讲我们走过的弯路吧。

我们做的中台叫做故障定位中台。出现这个“中台”也有一定的历史原因,我们公司很多 BU 都有做故障定位的 SRE,也就有很多故障定位系统。我们在中台想做的就是把这些故障定位系统给统一接入进来,复用一些能力。

至于具体的实现就不多说了,没有什么魔法,如果让你来实现,估计跟我们现在的样子也不会差太多。主要说一下我们做的不好的地方吧。

简单

对于一个需要其他系统接入的中台来说,我觉得最重要的东西就是简单。在一个垂直的领域做中台,不可避免的要引入一些新的,抽象的概念。但是引入概念越少越好。印象比较深的,红姐以前试图教我设计 DSL 的时候说过,先实现功能,然后看哪些东西是可以去掉的,最后剩下的就是 DSL. 我觉得非常对。只有简单,才能让出错的概率更小,接入的成本才会低,别人才会愿意用你的系统。中台最大的失败就是没有人使用。

先实现功能,然后看哪些东西是可以去掉的,最后剩下的就是 DSL.

——thautwarm

我觉得一个产品在不知道将什么作为自己的竞争力的时候,“简单”就是最好的竞争力。比如 clock.sh ,一个可以托管 cronjob 的平台,创建一个新的任务只需要填写 4 个值,你的 cronjob 就可以跑起来了。

没有保持简洁,就没有用户。没有用户,也就没有后面的故事了。

而我觉得踩过的最大的坑, 就是这里。有同事之前是做交易系统的,我感觉他们设计的东西都非常复杂。以我们的接入接口来说,入参有 20 个参数,返回值需要用户填 20 个参数,就太复杂了。每次新用户接入,我基本上要给他们培训 4 个小时的时间,后续配合调试的成本也巨大。

作为一个平台,至少要做到用户自己看文档就可以进行接入。不然每次接入都需要进行特殊的培训,显然是不具备通用性的。

度量指的是对于接口方需要让他们看到一个运行的状态,知道自己的东西运行了多少次,效果是什么。

度量与统计(管理)

大部分的中台会作为一个服务提供方让别人来接入吧。所以就有对接入能力管理的要求。对于我们的平台来说,这个管理的需求统计这些接入方运行了多少次,失败了多少次,运行的效果如何。

不同的平台对于统计和度量的需求也不是一样的,分情况具体对待吧。我们的情况更有一些棘手,就是很难通过程序来判断运行结果的正确性。这里有一个悖论,就是如果我们如果事先知道一个结果对不对,那么就相当于我们已经知道了“正确的答案”了,后面做的事情也就没有必要了。所以现阶段我们的度量还是基于人工的。

集成功能

中台一个核心的思想就是 1)减少重复劳动,将一些轮子一次性做好,其他人只要用就可以了 2)结合一些已经有的系统,发挥1+1 > 2 的效果。

所以对于一些常用的操作,可以封装成 SDK。这句话说起来很简单,门槛也很低,谁都可以写。但是实际做的好不简单。可惜的是,很多喜欢写 PPT 的人认为“写代码是最简单的事情,是没有技术含量的事情,是谁都可以做的事情”。我见过的大多数的 SDK 都乱七八糟,一通乱写。

要注意一个点是,尽量地少抽象新的概念,如果没有必要,不要增加实体。尽可能地减少框架对用户的束缚。尽量让自己提供的东西成为一种“选择”,而不是一种“约束”。

说到这里,我又佩服 prompt-toolkit 写成了一个库,而不是一个框架。框架是它来调用你写的 API,就增加了很多的限制。而库是你去调用它的 API. 举个例子,从上手成本上说,一个库你可以看一下函数声明就上手开始用了,对于一个框架,你要知道它是怎么运行的,它是怎么调用你的代码的。

 

实时上传数据备份文件到S3

最近写了一个小工具,用处是可以将数据库的备份文件上传到 S3 上面去。学到了一些很有意思的东西,觉得值得记录一下。

工具的源代码:https://github.com/laixintao/mydumper2s3

备份数据库的方法

最简单的方法,只要将数据库 dump 出来,然后上传到 S3 即可。

但是全量 dump 出来数据库占用磁盘的空间较大,并且上传完之后一般都删掉了。有一种可以不浪费本地磁盘的空间的方法,之前在博客《用 ssh 传输文件》中介绍过。我们可以用管道将 dump 的进程和上传的进程连接起来,这样就不需要本地的磁盘了。

类似这样的方法:

MySQL 自带的 mysqldump 命令是支持将数据库 dump 到 stdout 的,然后我们使用 aws s3 的 cp 命令,将源文件设置为 stdin,中间加了一个 gzip 做压缩上传。因为 SQL 文本压缩空间很大。

使用 mydumper 的问题…

mydumper 是一个第三方的开源 dump 工具(只有 3k 行 C 语言代码)。做的事情其实和原生的 mysqldump 差不多,但是有几点好处(引用 Readme):Parallelism, Easier to manage output, Consistency, Manageability. 在我看来,最重要的是 Parallelism,它比原生的 mysqldump 要快很多,(猜想)这个速度快是用了很多黑魔法来实现的,比如采用了多线程同时写入多个文件的方式,因为多线程 dump,并不支持输出到 stdout,因为每一个线程都需要一个 fd 来写入(还是我的猜想)。在不修改 Mydumper 的代码的情况下,直接使用 Unix pipe 是不可行的。

所以,要是使用 mydumper 的话,我想到了以下几种方法。

1. 将 S3 挂载到系统上,作为一个文件系统

考虑使用 https://github.com/s3fs-fuse/s3fs-fuse 将 S3 直接挂载到操作系统上,这样 mydumper 并不知道自己在往网络上上传,fuse 封装出来一套 POSIX API,不是完全兼容的,还不知道够不够 mydumper 用,但是感觉问题不大。

这里的问题是,每次文件创建,每一次写入,都是通过网络做的,网络不稳定可能会有问题。也不知道问题会多大,我还没时间测试。

2. 黑魔法,使用 LD_PRELOAD 直接覆盖 write_data

链接的时候可以提前 load 我们定义好的函数,来覆盖 mydumper 的函数。(相当于 Python 的 mock.patch 功能)

比如写一个这样的函数:

然后定义环境变量:

这样在运行程序的时候,随机数得到的就永远是 42 了。

看了下 mydumper 的源代码,只有3k+行 clang,考虑可以使用 ld_preload 直接将他的 write_data (以及打开文件相关的操作)直接覆盖(考虑使用golang,比较好些),将实现直接替换成写 s3。

这样相当于不需要本地磁盘就可以上传了。

参考:https://jvns.ca/blog/2014/11/27/ld-preload-is-super-fun-and-easy/

3. 两个进程,一边 dump 一边上传

这个思路比较朴素,就是 mydumper 只管普通的上传,而另一个进程(我们就叫它 mydumper2s3 吧),只管上传,然后上传文件完成之后,就将上传完成的删掉。

这样,只要上传的速度比 dump 的速度快,就依然还是可以继续的。就怕磁盘空间不够,上传的速度又比 dump 的速度慢,就悲剧了。

这个问题我一开始纠结了很多,最后发现应该也不是什么大问题。以为:

  1. 看了一下 S3 的技术规格,如果在同一个局域网内,速度是 GB 级别的,而 mydumper 是几十M1秒;
  2. Mydumper 支持 --compress ,自带压缩功能,那实际的写入速度又要慢上一些;
  3. 实在不行,可以用 systemd/ionice 这种工具对 mydumper 的进程限速

所以就不去考虑这个问题了。

有趣的是,我去翻 mydumper 的 issue,发现竟然有人抱怨在磁盘慢的时候 iohang 住,而不是报错(issue)。不过作者回复说这样挺好的啊,这样不就给你时间释放磁盘空间了吗:)

我也想要这样的 T T

实现细节

在 mydumper 还在写入文件的时候上传,需要注意,正在被打开并写入的文件先不要上传,等文件被关闭之后在开始上传。所以需要维护 4 个文件列表:

  1. 当前 dir 下的所有文件;
  2. 正在被打开的文件;
  3. 正在被上传的文件;
  4. 已上传的逻辑;

然后每隔 1s 扫描一次文件夹,将 “当前 dir 下的所有文件” 中,没有被打开、没有被上传、没有已上传的文件,加入到上传队列中。其中要注意好多线程之间的逻辑,至少需要一个定时扫描的 watcher 线程,然后需要一个上传的线程池(注意连接池的大小不要小于线程池的大小)。比如我写的一个 bug 是这样的,将这些文件加入到上传队列,但是没有立即更新到 “正在被上传的文件”中,而是线程池真正开始上传的时候才更新。这样就导致下一秒扫到的时候认为这些正在队列中的文件还没有上传,又入了一次队列,导致文件被重复上传了非常多次。

一开始我是想用 inotify 这种接口去监听文件变化的,最后变成了定时扫描(扫描间隔是可以用命令行参数 --interval 控制的),是考虑到下几点:

  1. inotify 只能在 Linux 系统工作,不同的操作系统,文件变化的事件不同;
  2. 实现太复杂,需要3个线程,监听创建 + 监听关闭 + 上传线程,这些线程之间需要通讯;
  3. 需要处理一些临时文件的变化,比如 metadata.partial最后 dump 完会被重命名成 metadata ;

最后实现的效果如下:

验证备份文件

还写了一个工具可以校验上传文件的 md5.

有个有意思的地方,如果文件太大(超过5M?),S3 的 SDK 在上传的时候会使用 multipart 上传,如果是multipart上传的,s3上面的 e-tag 不是整个文件的md5,是每段的md5合起来再md5. 这样校验就非常复杂。

我发现了这个神奇的bash,按照s3的逻辑在本地文件切段,然后计算hash:https://gist.github.com/emersonf/7413337  (from: https://www.savjee.be/2015/10/Verifying-Amazon-S3-multi-part-uploads-with-ETag-hash/)。

 

最后,这个工具的源代码在这里:https://github.com/laixintao/mydumper2s3


2021年10月29日更新:

今天发现 mydumper 已经实现 --stream 的feature了,作者到我的 Repo 留言说下个版本会支持。

目前还没release。所以我去看了它的代码,看这个实现比较简单。原来的逻辑都没有改,只是会在 mydumper 进程内新开一个线程将 dump 好的文件输出到 stdout 然后删除。这样还是用的多线程 dump。这样,我们就可以使用其他工具直接从 stdout 读出来然后上传了。不过读的时候要注意去解析 mydumper 的输出,比如它是每次先写 stdout 一个 \n -- 4个字符,然后写文件名字,然后文件内容。

 

近况更新

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

大约从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. 中科大 李博杰写的科普