说说创业公司经历的代码拆分

业界资讯 来源:嘀嗒嘀嗒 发布:2016-03-07 浏览:771

摘要:三年前在 Square 很火的时候加入 Square,一年前又加入 Airbnb。这两家创业公司从 0 到 1 的创业过程其实我并没有亲身经历,加入 S 和 A 的时候两家都是已经有了百来号工程师的规模了。网站和主要产品的的核心功能都已经齐备。但是两次,却都恰好经历了公司从 1 到 N 的扩张,以及业务拆分的过程。

引子:三年前在 Square 很火的时候加入 Square,一年前又加入 Airbnb。这两家创业公司从 0 到 1 的创业过程其实我并没有亲身经历,加入 S 和 A 的时候两家都是已经有了百来号工程师的规模了。网站和主要产品的的核心功能都已经齐备。但是两次,却都恰好经历了公司从 1 到 N 的扩张,以及业务拆分的过程。


Square 最早期的时候,整个代码库都是 Ruby on Rails,所有的产品,所有的功能都是几乎在一个代码库里。我进入 Square 的时候,有一些服务已经从 Ruby code 里面扒出来了,形成单独的 Java 或者 Ruby 服务。然而大部分功能,还是在一个大块 Ruby code 里。几乎所有的工程师每天都在这个code base 里改代码。虽然有严格的代码审核过程和规范的开发流程,各个不同功能的代码模块的交错以及工程师之间偶尔改动的模块有重合或牵连,还是会时不时的(哦,其实是时时的)出现问题。

那个时候 Square 的做法还是所有的 code change 在一周中 review 后可以 merge 到 master branch,然后随时可以deploy master 到 staging(一个模拟 production 的环境)。但是每周只有周五一次将代码 deploy 到 production 的过程。一周可只有一次啊,就像 iOS 的 version releave 一样严格地控制。你想想看,百来号员工,就算只有 1/3 人每天在这个池子里改代码,一周累积下来,已经是多少改动了。所以当时有个类似 sysops 的组专门负责每周五的这次 deploy。当年我也是干了快半年因为表现不错才很荣幸地被 “选拔” 进入这个可以担当 deploy 重任的 “特别行动小组”。那么每次 deploy 是一副什么场景呢?

每次 deploy 开始的时候,一正一副两位飞行员,哦,不,是工程师正襟危坐,多个显示器同时打开各种 metrics monitoring,呼叫长江一号、应答、呼叫长江二号、应答、就绪。然后打开音响,播放 deploy 之歌,好让全体码工知道,激动人心的时刻来到了。

通常是先将 staging 测试无误的代码 deploy 到一小撮 production machine 上,也就意味着只有一部分用户访问的 traffic 会调用新代码。监控 traffic 发现一切正常,就开始按下起飞键,然后这周的新代码就在几百台机器上一一部署。

如果一切正常的话,就是坐等。轻松。

如果到一半,开始各种红色警报,那就立马扔下手中的可乐还是咖啡,全体脑细胞进入亢奋阶段,精神高度集中,麻利地开始各种紧急停止,各种 rollback,各种找问题,各种 revert。。。然后排空问题,从呼叫、应答开始,将整个流程再来一遍。直到代码安全进 production 并正常运行。端起咖啡,已经凉透。

这种每周一次的 deploy 模式通常是很 intense 的,做完了就会觉得一身轻松,而且一般是周五,所以两个 deploy 员拉上几个熟识的 sysops,开着 pagerduty 随时准备待命,到楼上员工餐厅喝酒去了。

这样的情况经过两年的业务拆分,等到我离开 Square 的时候,大部分可以独立出来的服务都已经独立出来。也再没有了这样的激动人心的 deploy 上演。

Airbnb 早期的代码也是一整块的 Ruby on Rails。我刚进 Airbnb 时,代码的状态其实和我刚进 Square 的时候状态差不多,甚至更原始一些。

不同的是,Airbnb 没有一周只能 deploy 一次代码的规矩,也没有专人 deploy 的说法。而是所有工程师任何时候,只要 ready,就可以 deploy。好处是,deploy pipleline 里面的代码改动可以保持最低,而不好处就是几乎任何时候都有人在 deploy。写这句话的时候瞅了眼,果然还有正在进行的 deploy。而这也就意味着,红色警报随时可能在身边响起。

具体的细节我就不多说了。总之一个巨大的代码库有一百人以上改代码,情形是可想而知的。而从一年前我进入 Airbnb,到现在的一年里,我又有很大一部分时间花在业务拆分上。

其实我说的是这两家公司,但是和朋友聊过,几乎湾区所有公司都经历过类似这样的业务拆分过程。哦,还没长大就挂掉的公司除外。

那么为什么呢?

有一个很有意思的图:


可以看出,当一个公司还很小的时候,基本复杂度(base complexity)相对较小,所以单一代码库(monolith)的效率就会高。

而随着公司业务的扩展,访问量的增加,其基本复杂度也就会逐步升高(x 轴),那么达到某一个临界点后,微服务(microservice)的效率就远远高于单一代码库。

关于微服务,最近发现很多同仁都在写,我也就不搬书了。这里给一个很好的英文博客:Microservices

感兴趣的筒子可以自己去看看。

下面就想聊聊这几年业务拆分的一些心得和踩过的那些坑。

有一个程序,大概可以看成分成五块。其中模块 A 连接一个外部模块 D,且其结果,常常会被模块 B 和 模块 C 分别调用。而剩余的一个模块,可以看作是程序中所有不相干的其他。


当他们在一起的时候,有一个 integration test,可以在 mock D 的情况下,测试 ABC 是不是可以正确运行。也就是说,有一个人改了模块 A 的返回值,如果不小心忘了改模块 B 和 C 的接口,测试立马 fail,所以不会有因为忘了,而只改一个模块的可能。而且一旦测试通过,所有的改动会在一次 deploy 中同时 rollout 或者 rollback,相当容易控制。

然后有一天,A,B,C 就被拆分成三个独立的 service,各自生存在不同的代码库,或者是同一个代码库的不同的service container。测试例不可能综合测试三块的功能了,只能 mock 相互的 request 或 response。而如果在开发环境下联测,需要本地 setup 三个 service,根据每个公司的开发环境的成熟度,这一步可能很简单,但也可能时不时耗掉你的几个小时就是为了让不同 service 都能本地正常运行,且能通过 RPC call 相互调用。(注:这句话虽然是一笔带过,然后所有有实战经验,体会过 local multi-service setup 之痛的筒子,此处可以有眼泪。)因为测试的 cost 比原先困难了太多,可能并不是每个程序员做任何改动,都会完完整整的按照正规流程做一遍测试,尤其是那些目测貌似是 “很小的” 或者 “不相干的” 改动。于是,一旦 deploy 出去,可能就歇菜了。

就算都是没有任何问题的吧。有一天,A 改了自己的接口,RPC call 的 request中的一个 field 从 integer 变成 string 类型。如果是 ABC 还在一起的时候,这个一点难度都没有,在一个代码库里把三者的相应类型都改了就好。然而,现在三者在三个 services,可以独立地 deploy,这事就麻烦很多,原因就在于 A,B,C 不能保证 deploy 总是完全同步。

有经验的知道,这好办,加个接口的 backward compatibility 啊,只要:

  1. 先改 A 的接口,让他接受 integer 也接受 string,如果request 是integer,先做一下转换。deploy 这个change。

  2. 改 B 和 C 的接口,request 从 integer 变成 string 类型,deploy 这个change。

  3. 等到 A,B,C 新代码都 stable了,改 A 的接口,只接受 string,deploy 这个change,就完成了所有接口的改动。

没问题哈?You wish。因为 A 也是有别的代码的,所以在上面的 step 2

之后,突然发现 A 的code 有一个问题,需要将 production 的代码 rollback 到一个之前的版本。然后这时候 B 和 C 已经送 string 了,而 A 只接受 integer,然后就没有然后了,production 频繁 400 。

当然,这是一个简单的例子,可以简单通过让 A 接受两种 request 类型时间稍作延长来避免。但是实际工作中的改动往往不是这么简单的 dependency 或是没有任何 constraint,所以 service 之间的无缝改接口,是一个需要及其小心的问题。

其实拆分后的痛远远不止于此。就我自己的经历,大概有过这些感受:

首先,是上面例子看出的几点:

测试变得异常复杂。这是因为当模块被独立后并没有办法很方便的写出综合测试例。一个方法就是 mock up 所有的接口的 response 和 request,但是这实际上大部分时候根本没法测试实际中跨 service 的改动,有点自欺其人的味道。另一个方法就是 local setup 所有 service,用真实 service response 来测试。但是撇开本地 setup 多服务的其实就是耗时耗力的体力活不说,保证 local 始终是一个service 的最新代码也需要格外的小心。尤其是同步开发的工程师变多以后,可能你正在测的没有问题,然而已经有位同仁刚刚对你所测的服务又 push 了一个改动到 master。

而测试的复杂度,几乎是软件工程中的万恶之源。当每个小改动测试都变的耗时耗力,就难保没有偷工减料,心里揣着 “我的改动应该没问题”  的侥幸心理没有完整测试,就 merge 改动。因着大部分时候这样的改动确实没问题,这种侥幸心理就一再滋长,直接 merge 的胆子也越来越大,终于有一天把 production 搞挂。

当然,和几个 Google 的筒子也聊过,Google、Facebook 这样的大公司整个系统做的相当成熟,测试环境也已经完美的配置好。可以写 integration test,因为每个 service 都有个 online 的 test service。或者是 services in a box 可以很方便的一次性 setup 所有的 local service。然而,我这里说的是成长中的创业公司,很多并没有达到这个水准。

所有和接口相关的改动需要大量的协调。这个也是很容易体会的,如果你不小心,需要把一部分代码从一个 service 迁移到另一个 service,或者改了 API 的协议。那么,所有不同 service 的维护者需要在 code 上加上层层的 forward 或 backward 的 compatibility 的代码保护,并且需要严格遵循一定的 deploy 和代码变动的顺序已经每个改动后需要保持稳定性等待的时间等。几乎所有有年头的程序员可能都踩过类似的坑。

另外还有几个这个例子中没有提到的麻烦:

报错的处理。因为程序不在一起了,所以当一个异常发生时,得到的 stack trace 并不是完整的 stack trace,而是只能 trace 到某一个 service 的接口处。于是让 debug 变得&*%¥(这里,我好想骂人。。)。然后你得去另一个 service 的 log 找,看看那个时间戳,从你这里发出去的请求到底发生了些什么,才能进一步找出问题。当然,好的程序员在写 service 的时候知道要把 exception 的信息层层 propagate,并 expose 到接口的 4XX response 里。这样调用方的 stack trace 里就会有为什么被调用的 service 不爽了。然而,很多时候,很容易有 “无力问苍天” 的感觉。感受下,你的 service production 出问题了,然后到 kibana,就看到:

Error! HTTP 400 response from http://another-microservice.com/update

log 的完整性。log 因为在不同处,不仅像上面这样的 debugging 变的更加困难,对于一些基于 log 产生 event stream 的机制,也意味着,想真正从 log 里获取完整有用的信息,就需要将不同 service 的 log 一同取出参考。这个需求并不是所有的应用都有。但是为什么提到呢?比如我们做支付的,经常需要一个事务的完整的 audit trail,也就是一个告诉我们 “who did what and when for every change involved” 的特殊的 log。这件事现在倒是也有了比较标准的解决方案,就是一个共享的 message bus,比如 kafka 之类的。以后再细说。

注:Google、Facebook 的 log 系统可以将一个 request 的所有相关的 log 用一个 global id 整合归并到一起。所以,我这里说的依然是成长中的创业公司。

题外话:我觉得 Google 就是所有的系统做的太牛了,面试中偶有遇到,问他一个问题怎么解决,答曰:扔到 XXX 系统就完事了啊。或:用 XXX 就搞定了啊。知道其所以然的也就罢了。只知道存在,并不知其所以然的,然后就木有然后了。(憋误会,偶是 Google 粉,偶们全家都是 Google 粉。)

timeout 的设置。比如一个从前端发来的请求,你希望用户最多等待 5 秒就放弃,所以有个全局的 timeout 为 5 秒。当然你可以选择所有的 microservice 都设一个很大的上限,这样还是会 5 秒后 timeout。但是这可能是因为某一个 service 的实现傻 X 了,所以你希望尽可能的给每个 service 设一个合理的最长延时,这就变得很 tricky,尤其你的一个request 会经历多个service call 的情况。

关于自由和民主。记得以前看到过一句话,当每个人都有绝对的自由的时候,这个世界就没有自由可言了。Microservice 的每个 service 都可以完全自主选择自己的语言,自己的数据存储,自己的代码风格。短期来说,这是让程序员的效率极大提高的一件事,然而,同一个公司,当你各式 service 五花八门,简直就是技术秀的时候,不论是维护还是稳定性都受到极大的挑战。于是开始有人扮演清道夫的角色,开始搞 service standarization。然路漫漫其修远兮,愿牛人一路平安。

那一朵朵凋零的玫瑰。因为 microservice 的相对独立且开发周期短,往往一两个工程师甚至几周时间就可以写出一个新的 service 。所以,可能它们的问世就相对变的更容易。可是不过不是核心模块,可能过了两年,因为某人的离职,或者另一个工程师一拍脑门写出个更牛的 service,这个service 就凋零在那了。好些的,会有人清扫,让其正式退休,不好的,就好像一个悬挂在蜘蛛网上的花瓣,随风摇曳,无人问津。


好吧,写到这,可能有人要问,你上篇刚抱怨了大代码库的痛苦,这篇又开始猛烈地吐槽 microservice,你到底想说什么?

我想说的啊,我想说我只是一名一线码工,并不能给你高瞻远瞩 microservice 的未来,也不能展望下一个 20 年到底花落谁家。然而,亲身经历过两种架构,我认为到底哪种更合适一个公司的开发模式,这其实不是一个单选,而是要考虑一下几种因素后做的权衡:

  1. 你的业务逻辑真的足够大,足够复杂了么? Horizontal scale 已经不 work 了么?代码的相互影响、Deploy 的长周期或频繁出问题,真的是你的切肤之痛么?

  2. 对于 microservice 这种不算太新的架构,你的开发人员多少人有经验?真的能正确驾驭而不是让所有我提到的问题都成为拦路虎么?

  3. 关于拆分,这是一个由一到多易、由多到一难的几乎不可逆的过程。一旦你三分天下,想再一统江山就没那么容易了。

所以,拆不拆,拆到什么程度,怎么拆,工程师的素质,都是直接影响成败的关键。

PS:每个技术人背后都有几个支持和帮助的同仁(改编自 “每个女人背后都有几个挺她的男人”)。虽然我写的故事大都是自己的亲身体验,但偶尔怕自己的观点会偏居一隅,也经常像身边的各方大牛请教求证。借此机会也感谢身边几位深藏不露的技术大牛长期以来默默的支持和提供咨询!



嘀嗒嘀嗒:讲述技术、白话硅谷。偶尔八八程序员身边的事儿。关注长按二维码:

原    文:嘀嗒嘀嗒

「Airbnb」都使用了那些技术和工具?他又是怎样从0到1发展起来的?
点击查看「Airbnb」-- 技术栈

「Square」都使用了那些技术和工具?他又是怎样从0到1发展起来的?
点击查看「Square」-- 技术栈

免责声明:

  1. SDK.cn遵循行业规范,所有转载文章均征得作者同意授权并明确标注来源和链接。
  2. 我们十分尊重原创作者的付出,本站禁止二次转载如需转载请与原作者取得联系。
  3. 转载SDK.cn的原创文章需注明文章作者、链接和"来源:SDK.cn"并保留文章及标题完整性未经作者同意不得擅自修改。
  4. 作者投稿可能会经SDK.cn适当编辑修改或补充。

评论 (100)

推荐工具 意见反馈