周末
周末本来准备窝在家里打游戏,惊闻b站大佬要来北京分享,赶紧起床去听课。期间也和其它公司的人聊了聊,感觉收获不少。
B站现在作为国内二次元的门户,聚集了大部分的动漫爱好者,因为业务模型和日本的 niconico 比较像,所以早期也肯定从n站的产品上学到了不少东西(这句是我说的)。从文化衍生出的直播、周边、游戏服务又能够帮助这个站点进一步地造血赚钱,比如现在非常火的FGO。因为这一两年B站关注度变高,所以即使是对二次元没什么兴趣的人也会感觉和b站有关的新闻越来越多了。比如去年在知乎上先后有 flv.js、2233娘竞拍的事情,也是把他们推上了风口浪尖。
很多70后80后可能都不一定会上B站,但是你可能想不到B站也是一个亿级用户的站点了。从09年创立到现在,在技术上他们也经历了从一个创业公司到越来越正规的过程。有很多方面我觉得做得比现在的公司还要好。挑几个值得说的总结一下吧~
Code Review
首先是我来了现在的公司一直头痛的 Code Review,上一家公司当当的架构师在这一点上非常地严格,我们被分到子公司了以后也就沿袭着习惯一路按着规范执行了下来。按照大多数人的印象可能这是很花时间的一件事情,比如每次上线前你要卡一两个小时来对所有的代码做 Review,而且还得好几个人看。所以很多人就不乐意了,我开发任务这么重要,做完了你还不让我上线赶紧回家,还要让我改代码。但是按我们实际执行的效果,能够 Review 过的代码在线上很少出问题。即使当时是在一家996的公司,大家加班都如此的疲惫的前提下。然而新公司你能看到某些系统就在不停地回滚。然后各位领导想出来的解决这个问题的办法竟然是控制上线频率,每天只上一次线,也是可以。行政手段来解决技术问题,确实有某些公司的风范。
说到 Code Review 的坏处,你可能也能想出来一堆,比如上面说到的时间消耗,阳奉阴违上有政策下有对策,执行一段时间之后就全部流于形式。比如部门的领导也曾经说我们部门现在面临的更大的问题是生存压力,而不是代码质量。呵呵。在部门组织架构调整之后,因为整个新部门的 boss 是高T的技术出身,所以稳定性突然变成了重中之重。怎么解决稳定性的问题?还是靠行政手段。Code Review 从来没有进过领导们的法眼。
Code Review 的收益远比系统上线少出 bug 要多得多。大家都知道 Code Review 可以降低莫名其妙的 bug 率,虽然降低 bug 率也不应该只依靠 Code Review。除此之外 CR 还可以帮助你团队里的人快速进步,短时间内纠正不健康的代码习惯。最重要的是 Review 的过程也是学习的过程,你可以看到别人的代码是怎么写的,也许某些东西的实现方式可以更加精巧。整个 Review 也是程序员之间彼此碰♂撞交流思想之间擦出火花的过程。从而潜移默化地提高整个团队的技术水平。一定程度上让大家熟悉别人写的代码也是建立 AKA(all knows all) 团队的过程,这样在一些员工有事情休假或者紧急情况下其他人也能够顺利接手他的工作。而不会导致项目进度严重依赖某一个节点员工的现象。我们部门现在的情况就是这样,但是也没有人考虑过这种事情的风险。
还记得在2015年底的时候公司连着出了两次重大事故,CTO在内部论坛问,如何才能提高我们公司的代码质量?下面一色的回答 Code Review,那么为什么大家明明知道却不重视呢?
因为从来没有从心底里认识到 CR 所能带来的巨大收益。
基础组件透明化
前段时间听说我司新业务的MQ接入会全部迁移到 RocketMQ,想着对业务部门又是一场灾难。要换sdk,要改业务代码,各种改。然后就去问了问最近的新队列怎么接入,得知基础架构为业务部门封装了一套sdk,实际上你也可以不用sdk,可以使用 thrift 协议的接口,感觉稍微开了点眼界。想想的话,以后基础架构再向外提供服务大概也会这么做了(thrift 协议的接口)。
为什么要把这些组件用统一的协议封装起来呢?
当然是为了屏蔽掉下层的实现细节。这样业务可以不用关心在接口后面的具体实现,你是用了Kafka,还是RocketMQ,还是Beanstalkd,还是NSQ,还是NATS,还是Redis List,还是其它的什么自己造的延迟队列的轮子。在实现层面,可以方便地随时进行替换,整个过程对业务完全透明,不会因为轮子换了导致所有相关的业务都被迫做 Client 端 SDK 的迁移,何况很多 Client 端的轮子还有各种各样的 bug。比如 Kafka 的 go 客户端,曾经网络抖动就导致停止消费,比如 kafka 的 py 客户端,据说也有类似的 bug。
除了屏蔽掉实现细节之外,MQ的高可用也可以在中间件这一层进行一些业务不用关心的操作。省得像裸 redis 那样在客户端配了一大堆ip,出了问题客户端要自己摘除,如果写出了 bug 搞不好还会丢失掉写,都是再正常不过的事情。
这次B站的大佬讲了他们在 Kafka 前面造的一个中间件的轮子,思路也非常不错。为了业务使用方便,他们直接在中间件这一层支持了 Redis 协议,刚好过年期间我也研究过了 Redis 的协议文档,确实非常地简单。用 redis 协议比用 thrift 协议有更大的好处,几乎所有的语言都有靠谱的 redis client,而且因为实现非常简单,所以不太容易写出什么 bug。生产者和消费者都非常地简单,生产者只要用 redis 任意的命令(其实是我忘了他们具体用了哪个命令orz)就可以简单的进行消息发送,消费者也只要 get/ mget就可以实现简单的消息拉取,然后一个 for 循环就能解决大部分的问题了。至于具体的和zk还是kafka的长连接,offset之类的管理,都可以在中间件这一层来做。像offset的重置默认逻辑(oldest/newest)也给业务提供了系统来进行定制设置。
业务和基础架构绑定在一起
让我比较惊奇的一点是B站的业务部门是和基础架构部门绑定在一起的,没有独立的基础架构部。大家一起聊的时候也讲到了这样划分部门的好处,基础架构脱胎于业务,所以会比较贴近实际的业务需求。程序员都应该是既能写业务,如果在现有的轮子不能满足需求的时候也可以自己上手造轮子的人。
像我们公司这种把基础架构独立成部门的方式在一个公司早期的时候实际上不太合适,这样会过早地将业务和架构分离,导致了一系列工作上的困难。比如基础架构的人不了解业务真正的诉求,再比如业务开发不愿意协助基础架构的人进行各种版本上的迁移,认为这是没事找事。然后真的到了业务对轮子有需求的时候,基础架构也没有办法及时响应。然后业务就只能自己去造轮子了。我们公司的强势业务部门就有很多自己造的轮子,比如升级php7时候自己编写的各种扩展,再比如核心的特征信息的存储。等等。
期间还碰到了一个新浪出来的哥们儿,没想到基础和业务的分离在很多公司都是常态。在很多公司大家都看不惯造轮子自 high 的基础架构。想想似乎我们上一家公司也是这样。。
这件事情上B站确实做得挺好。能写业务能造轮子才是程序员的完全体~
除非公司的业务已经定型,再做分工的细化才会对公司有利一些。
当然了,这种时候也就对新加入的程序员不利。只能循规蹈矩切葱丝。
测试
测试方面,也是大佬们非常在乎的一个环节。从单元测试、系统测试到e2e测试是自下而上的关系,越下层的测试越容易完成,而越上层的测试成本也就越高。
这里面实际上靠人肉点是很没有效率的一件事情,b站早期的测试看起来也不会写代码。但rd们慢慢地教会了他们来写各种测试脚本,做自动化的各种test。
单元测试大家都可以做,听大佬的意思是希望对mvc的每一层代码都能够尽量覆盖得到,做好依赖的数据mock)。放到我们公司的环境来考虑的话,存在 CR 一节提出的问题,不重视这些事情的领导站在上面,做这些事情领导会觉得没有产出,没有意义。
而系统测试和 e2e 测试很多时候就是由QA来完成了,然而悲剧的现状是很多公司的 QA 是外包,根本不是本公司的员工,即使你想要让他们学会写代码,也非常地不现实。
因为他们本身素质就很差,再可能根本都不是科班出身,如果你要教他们写py,可能明天就离职了吧哈哈。
除了通常讲的测试,还应该有一种契约测试,一个老的api被人改得不再兼容,这种事情不应该是上线了别人反馈过来才知道(我们的现状)。这个api的契约测试可以直接集成在CI系统里来跑,通过了之后再上线可以保平安。
事实上现在的公司这种事情非常非常地多,上一个项目在这一点上浪费了大量的时间,但因为 pm 无能,根本不会想着让你们通过技术手段来解决这样的问题。业务 rd 被 pm 坑死某种意义上确实是大多数公司的常态。
不过很多 RD 已经麻木了。
服务拆分
B站的老系统也是一个php的巨型应用,php现在的运行模式大多是nginx->upstream->proxy pass->php-fpm->脚本,这种模型最大的问题就是几个慢请求就会阻塞掉全局的服务,从而直接bad gateway,所以渐进地进行了拆分,做模块化,比如用户体系这种和其它系统完全不相干的就会独立成为单独的系统,单独进行部署。同一个系统里也应该做一些快慢分离的工作,例如有些处理本身就是会比较慢(比如统计、大数据的聚会、feed流之类的推荐),应该独立到其它系统里,异步请求,不要堵掉线上的接口。
单独部署之后大家的痛点也都差不多,对端的api半夜出了问题,运维要起床,找做对接app的 RD,RD 查了半天日志,发现是自己的某个依赖服务出了问题,然后打电话又去找依赖方。依赖方发现可能是缓存出了问题,然后又把 DBA 叫起床。一个简单的问题要让所有人都起来解决肯定不靠谱。这个也就要靠 trace 一类的系统来解决了。
当然了服务拆分隔离部署之后还有一些集群资源利用率的问题,在某些地方也会是问题。(在我们公司可能不是问题,因为技术成本分摊到订单只有x分钱,大概不够业务改一条弱智的运营策略)。b站的docker方案似乎是用的mesos,看来也是满足需求了~(我们公司也在搞稍有云,但各个部门也是夺来抢去)。
api版本管理
老生常谈的话题了,我们上一家公司这方面做的也还不错,不过当时的做法是每次api的大更新都拷贝一份代码(因为是php系统)出来成为一个新的版本号,这样有些问题,例如在一个api修改了以后我需要把所有的代码都拷贝出来。然后如果线上新老版本一直都存在,那每次解决一个bug就是要做两次重复工作,不靠谱。
问了问b站这方面怎么做,看起来似乎api的版本粒度控制要细一些。(仔细想想我们上家公司的做法也还是因为系统太大了啊)。然后系统的版本命名现在大家一般都用semantic versioning来做,具体的细节可以参考:
简单地来说,通过系统的版本号你就知道这个系统从a版本到b版本有没有做 breaking change。
这点上我觉得 golang 社区就做得不太好,当初官方就不该搞一个什么 github.com/xxx/yyy 这种依赖形式,而应该强制要求必须有gopkg.in 这样的强制版本规范,现在倒好,搞得民怨沸腾。你的依赖作者如果是个不自觉的,api随便就改了,早期还没有 vendor 来固化依赖版本,就更加地坑了。
实际上 vendor 的方案也不是十全十美,如果几个依赖的依赖项有交叉而且有版本冲突(且都是github.com/xxx/yyy)这种形式,你就等着哭去吧。官方的 vendor 会把依赖 flatten,相同依赖路径不同版本无法共存。
有了靠谱的版本管理之后,一个api从v1迁移到v2的过程可以通过监控来看v1是否有流量,没有流量的话即可进行代码下线。
不过api版本管理里还有个麻烦的问题,如果是对端的api,外部分发的客户端版本是没有办法控制升级的,除非有热更新方案。我们公司之前就遇到过评论投诉系统要同时维护两套api的窘境,但是也没有办法。现状如此。
热更新方案最好早一些做,虽然比较困难。
也可以在对性能要求不高的时候考虑考虑hybrid app。
小文件存储
b站的稿件封面和up主的头像都是一些图片,他们为这些小图片造了一套存储的轮子,参考facebook的haystack的论文,将小文件存储的随机写变成了大文件的追加写,然后通过open和seek一次寻址就可以找到你想要的文件。
我们公司内部也有类似的存储系统,基于 seaweedfs 和 martini 搭了一套。不过我们公司的 ugc 大头不在这种图片上,即使把几kw的司机照片和车牌之类的图片全存起来也没多少东西。而且并不会快速增长,所以专做存储的人大概也会比较郁闷。
之前核心业务部门发新版 app 表示要和做存储的部门合作,存储部门专门派了好几个人来开会,结果发现是要存4个 icon,啊哈哈。
说回正题,这方面大家的设计思路应该都差不多,之前在滴滴内部听章博士讲阿里的图片存储也是差不多的思路。对于文件来说,“目录”的消耗要更大一些。我之前有段时间一直想不明白key value结构的存储要怎么模拟目录的逻辑,直到后来看到了 confd。。
举个例子:
/dir/a.png
/dir/b.png
/dir/c.png
/dir/internal/d.jpg
就像上面这样的结构,只要把全部路径+文件名当作key,实际文件内容当作value即可。像etcd之类的存储系统里似乎也支持用前缀来查询拥有相同前缀的所有的key,例如你可以查/dir*这样的。
不过再细节一些的我就不太懂了,需要去看看具体实现~
日志和trace
类似于elk之类的技术栈,只是使用了flume做采集,日志最终到es和hbase(还是hdfs来着?忘记了)里,用trace_id
来进行查询。trace_id
会在业务入口的nginx里生成,一路通过http header进行透传。问了大佬b站在用户反馈问题的时候怎么获取到这个trace_id
,看起来是b站会在 http response 的时候某些接口带上一个 trace_id
的 header,提交反馈也就可以直接拿到这个 header 啦。
再想想我们公司的 trace 系统,也有一些自己的特色,比如可以通过订单号、手机号、trace_id
多维度查询日志。交流的时候听说trace系统的数据应该进行一些脱敏,但是要不要脱敏可能还是和业务相关吧(虽然我们公司最近吃的亏和这个不脱敏也有些干系)。
b站的日志在 es 里没有存储 document,只存了index部分和文档的id,这个是和我们公司的系统的区别。。我们这里没有做索引数据分离,全量一个月(大概)存储据说用掉了200多台机器。。
虽然es官方不建议这种二段式存储,不过二段存应该是可以节省不少成本的。也可以加快检索速度。
小流量、灰度发布、staging环境
以前在群里也问过大佬们在发布的时候如何做节点摘除的问题,当时只得到一个模糊的 healthcheck 的回答,这次近距离问了一下。。确实是 healthcheck ,在节点的web目录下放一个空文件,nginx(tengine)在 check 这个文件的时候200,说明这个节点还活着,如果你要做发布了,那就先摘除文件,一段时间返回404,那 nginx(tengine) 自动会把这个节点从 upstream 这个列表里摘掉。
还真是巧妙,当初自己就没想到。。
有了这个手段,实际上你后端的服务是不是支持 graceful 可能都无所谓了,摘除后再稍微等一段时间再发布也不会损失什么流量。
staging 环境差不多就是个1台机器的小流量上线,测试用特定的域名绑定 host 来工作,大多数公司也都差不多。
小流量上线似乎业界还有个比较有名的词叫金丝雀发布哈哈。
至于灰度,之前我也研究过自己公司的灰度发布系统,实际上只要支持一些特定的规则、配置、千分比策略,做起来的话也都差不多。属于很简单的系统。常见的用 murmurhash 来对字段做 hash 到 bucket 什么的,大家也都差不多,顶多换个 hash 算法罢了。
redis 和 memcached 还有 proxy
b站自己修改了 twitter 的 twemproxy,在原来的基础上实现了类似 nginx 的 master worker 模型,并且用 SO_REUSEPORT 来避免了 nginx 老版本那样的 accept 惊群问题(理解不深)。
redis 方面看起来没有像某些公司那样做定制,不过大佬讲到了一些 redis 使用方面的坑,比如单线程 mgetall,或者 hgetall 的时候会阻塞后续的调用,这点在我们这这里也见得比较多。之前一直以为 redis 是个万金油,不过单线程的工具毕竟有一些缺陷,在处理密集计算/IO 的时候没有办法服务后续的请求是个比较大的问题。
在使用上b站做了分离,redis 只拿来操作一些复杂的数据结构,比如 sorted set 之类的数据,可以拿来用 score 做排序,用吞吐量更好的多线程 memcached 来做 kv 缓存。在使用上也有一些经验分享,比如zadd的时候key已经过期了,导致一些看起来匪夷所思的bug之类的,用expire then zadd的方式巧妙地解决了这些问题。
memcached 方面我们现在用的很少。。所以理解上又不太深,不过大佬大致讲解了 memcached 的 slab,chunk的原理,听着和学生时代 OS 的伙伴算法差不多。所以知道算法的话,问题也就显而易见了,伙伴算法就会有比较麻烦的内存碎片问题,在你的数据本身大小都比较一致的情况下,很可能会导致内存的利用率实际上没有你想的那么高。可以在 memcached 里调参数解决这种问题。不过不知道他们有没有在mc上面造个轮子,似乎通常情况下mc的负载均衡都是在 client 端做的。
db方面
db在b站也是双主双从,这个是不是已经是很多公司的标配了。。我们公司的 DBA 来分享的时候讲的也是这样,平常一台 Master负责写入,另一台 Master 机器 standby,并不对外提供服务。
不过我们公司的不同的一点是两台机器上插入的内容 id 不一样,一台奇数,另一台偶数,这样可能是为了后续方便查询问题?(理解不深)。
b站没有db proxy。。这个我稍微有点不能理解,上了 docker 云的东西按说扩缩容都是比较正常的事情,没有 proxy 这一层保护的话,db 不会被扩容暴涨的连接打死掉么?完了问问我们公司的同学吧。
timeout方面
这个问题实际上也是我头痛了很久的问题,很早的时候就在业务里体会到了不设置超时,或者超时设置太长导致请求曲线上升掉底上升掉底的循环的问题。但是苦于不知道怎么设置超时更为合适。而且本身超时是个比较复杂的问题,在 tcp 服务里你有连接超时,读超时,还有写超时,之前一直没看到过有人系统地讲解这些东西怎么分析。
听了分享感觉更头痛了。。感觉按照90分位或者95分位的响应时间来进行调整需要进行详细统计、监控和数据参考。但是问题是一旦系统有新逻辑加入,那之前的超时时间很可能是保证不了的,这种情况下怎么才能确定新的更加可靠的超时?
之后还要好好研究一下这个问题。
这里面有个 fail fast的概念,也要看看业界都是怎么做的~
监控
三层监控,系统监控(硬件、网络),api监控,业务监控。监控系统用了很多开源的轮子,听起来 b 站在业务 api 上的机器并不是很多,所以看起来也是够用了。
业务监控不能跑在线上 db,这个也挺重要的。我还是经常看到有人犯这种错。(包括自己)
其它
本来用来打游戏的周末又学了不少东西。。
LEVEL UP!!!