引言
創世記第11章1-9句记录了“巴别城”的故事。当时地上的人们都说同一种语言,当人们离开东方之后,他们来到了示拿之地。在那里,人们想方设法烧砖好让他们能够造出一座城和一座高耸入云的塔来传播自己的名声,以免他们分散到世界各地。上帝来到人间后看到了这座城和这座塔,说一群只说一种语言的人以后便没有他们做不成的事了;于是上帝将他们的语言打乱,这样他们就不能听懂对方说什么了,还把他们分散到了世界各地,这座城市也停止了修建。这座城市就被称为“巴别城”。《钦定版圣经》是这样描写的:
4 他们说,“来吧,我们要建造一座城和一座塔,塔顶通天,为了扬我们的名,免得我们被分散到世界各地。”
5 但是耶和华降临看到了世人所建造的城和塔。
6 耶和华说,“看哪,他们都是一样的人,说着同一种语言,如今他们既然能做起这事,以后他们想要做的事就没有不成功的了。”
7 让我们下去,在那里打乱他们的语言,让他们不能知晓别人的意思。
8 于是耶和华使他们分散到了世界各地,他们也就停止建造那座城。
9 因为耶和华在那里打乱了天下人的言语,使众人分散到了世界各地,所以那座城名叫巴别。——Genesis 11:4–9
愿景
每人都可以随时获取一个开发环境。在其中做开发。并且随时可以验证自己写的代码在整个系统里集成起来是否工作正常。我可以立即得到有效反馈,从而提高工作效率。
尝试一:书同文
最理想的情况是所有的开发者使用完全相同的技术。他们使用同样的编程语言,他们使用同样的开发框架。他们使用同样的操作系统。他们使用同样的IDE。他们使用同样的风格管理 GIT 仓库。他们使用同一种语言来写构建脚本。如果这一些都是真的,想要获得一个开发环境,那会是非常简单的事情。
曾经尝试过各种方式把自己认为“最优”的开发环境兜售给同僚,给下级,甚至是给我的雇员。但是都失败了。让这帮 geek 们使用同样的方式工作,这个难题比我们这里要解决的问题更难。上帝来到人间后看到了这座城和这座塔,说一群只说一种语言的程序员以后便没有他们做不成的事了。于是神创造了emacs和vim。
打着提高开发效率的方式去强奸别人的开发环境是行不通的。无论你说你写的库再好,都不如我的库写得好。无论我今天写的库在好,也不如我明天想造的下一个轮子好。这个道理普世于各种IDE工具的执念,各种编程语言的执念。
不要只看到始皇帝做了书同文的伟业。还要看到人家焚书坑儒的本事。
尝试二:自动化脚本
退而求其次的办法是,不要求所有的组件都是同样的方式开发出来的。我们只要求你们各个模块的owner都提供一个共同的自动化脚本。当我们把这些脚本拼接到一起之后,整个系统就实现了自动化的部署。
这条道路演进的尽头就是一堆bash/python/msbuild脚本,加上一个cmdb数据库组成的怪兽。我已经在 闲谈集群管理模式 – taowen – SegmentFault 一文里描述了这个模式的问题
- 无法审查的脚本:给定一个 f(),你无法知道这个 f() 到底做了什么。这个是所有基于脚本组合的方式(函数套函数)做自动化的根本问题。最近这次 S3 的故障,就是因为误执行一个 saltstack 脚本导致的。而任何运行这个脚本的程序,是无法在加载脚本之后做任何事情来验证其权限和危害的。对于任何runner来说,f() 就是一个黑盒的 f()。
- 状态漂移:开发环境脚本在一个新的开发机上可能运行,但是在一个已经安装模块A的机器上就可能执行失败。或者在安装了A,B的,但是没有装C的机器上会失败。脚本执行的起始状态可能有千千万万,不可能测试齐全。脚本执行完之后留下的系统的状态也千千万万。
这两点技术上的硬伤,导致了基于一堆脚本堆砌出来的自动化仿佛浮沙之上筑高台一般。
尝试三:Docker + 组网技术
在发现了脚本自动化的缺陷之后,我陷入了对 Docker 技术的痴迷。Docker 的技术上的好处显而易见。它完美地把业务代码封装到了一个容器里,无论你是什么语言写的,什么框架开发的,最终都是一个 docker 执行命令。放佛巴别塔问题解决了,只要有了 docker,什么东西都是一样部署的。docker 镜像由模块owner提供,他们怎么弄出来是他们自己的事情。你想用 node.js,还是 haskell,随便。而且 Docker 完美解决了基于脚本技术的状态累积漂移问题。所谓immutable infrastructure。
Docker 还稍微有一些问题,那就是还需要把多个服务通过网络组装到一起。这个也不是什么大问题。虽然通过服务发现这样的方式推广起来有难度,但是我们还有网络代理的大招。比如指定100.64.0.1 代表 mysql,100.64.0.2 代表 redis。通过网络层拦截这些 ip 的请求,我们可以不修改业务代码的情况下,把这些 docker 容器给组装起来。
docker 基于了 linux ABI,代理版本的服务发现基于了 tcp/ip。这两个东西在巴别塔的时代就是一个bug级别的存在,但是本质上还是书同文。上帝留了一个口子,给我们钻了漏洞。于是我们又可以把这些乱七八糟的东西攒一起了。
Docker 也无法解决书同文的问题
这种方式完美了吗?我们再次发现了巴别塔问题。实际上的软件是这样工作的
某种程度上来说,我们通过Docker,通过组网,构建出来的系统只是一个空壳而已。它只是一门开发语言,它通过各种配置界面提供了自己的开发工具。PM通过产品配置,配置出了产品,而运营再基于产品,配置出了真正运行起来可以赚钱的系统。
Docker 可以利用 linux ABI 这个“书同文”的接口,整合了后台服务。网络代理利用 TCP/IP 这个“书同文”的接口把这些服务串联了起来。但是对于各个模块自己提供的产品配置和运营配置,我们就没有这么幸运了。有的模块使用了csv,有的使用了json,有的使用了数据库。当你要重现一个“环境”的时候。它不仅仅是意味着进程的启动,意味着网络的连接。它还需要把各种配置数据灌入到系统。而搞清楚有哪些配置,让这些模块使用完全相同的方式来定义和使用配置,我们就又回到了第一步,书同文的路数里了。书同文的成功实施,来自于强大的中央权威。如果拥有强大的中央权威,完全可以从一开始就强推书同文的开发模式(比如都是java,比如都是finagle)。这就形成悖论了。
微服务架构的责任困境
过去的开发模式是这样的。我负责一个服务,它从DB往上都是我的。产品经理的需求,我全部负责。中间会有少量的调用外部接口进行支付,邮件群发之类的事情。但是这些调用一般处于流程的末端,不参与主要的业务逻辑并且接口清晰。
这个年代的测试非常清楚,mock掉外部依赖(支付,短信网关,ftp接口),启动真实的数据库,从用户的接口测试我的服务。我直接向用户负责。这种工作模式,我至今认为是效率最高的方式。
现在的微服务的模式是叠罗汉式的。我负责了B,上面有A,下面还有C。A依赖B和C才能跑起来,B依赖A和C才能和客户端完整交互。
在这种拆分下,我们强调了每个团队的自主性。而且大家共同承担了最终的业务敏捷性和稳定性的要求。对于生产环境,确实是这样的。如果C挂了,C会立即去修,因为影响了生产环境。如果B挂了,B也会立即去修。
但是把这个问题改成线下和线下分开呢?如果是在服务B的开发环境里,服务C出问题了,有一个设置没有配置对。或者这个设置改了,B没有更新代码。你认为C会积极地帮B解决问题么?你认为B会进入到C的代码目录下,看他们打的日志么,然后自己就知道怎么修复问题了?即便可以解决,B也会遇到巨大的相应延迟的问题。C可能在开会,C可能在忙别的事情。
那么解法是什么?线下环境直接利用生产环境的部署脚本?一套脚本,可随时复制环境?前面已经讨论过这些自动化部署脚本,以及docker等模式的技术缺陷了。事实上,部署两套一模一样的环境是相当困难的。而且引入复杂的部署工具,以及专职的团队还有一个更加有趣的现象,它把两方的关系,变成了三方的互动
这三方的关系是互相不信任的,因为代码不是自己写的。服务消费方遇到了困难,去找服务集成方。集成方会认为可能是提供方的代码有问题。于是把提供方拉进来定位问题。提供方会觉得我的服务在线上是好好的, 为什么在线下就不行了呢?是不是你部署得有问题,gcc版本是不是不对。这种责任的链式传导很快就会让环境的使用方觉得,能用就用,不能用我也没法推动去定位问题。
从根本上来说,这个困境在于模块的owner,只对生产环境负责,不对别人使用的开发环境里自己的服务负责。花时间帮别人解决问题,提高团队的整体效率,对于模块的owner来说不是最优解。因为他花费了额外的时间去帮别人完成KPI,而不是专注于领导布置的下一个任务。
而服务的集成方,既不知道服务是如何消费的,也不知道服务是如何提供的。他不可能有足够的精力,时间与动机去深入了解所有的模块的工作细节。即便有意愿把这个集成工作做好,也没有能力在脱离模块owner的情况下把工作真正做好。
解法一:可复制的环境列入KPI
一种解法是把环境的可复制性变成每个人的KPI。这样每个模块不仅仅负责一个环境(生产环境)的工作正常。他还要负责保持这种可复制性。如果你的业务模式恰好依赖于这一点,则可以努力推动。比如你要去加拿大运营一套独立的系统,而能够一键部署一套环境给加拿大的运营使用,则变成了一件有业务收益的事情。
这种做法就是要把全流程的持续集成列为所有人的KPI。如果在线下环境集成失败,亮红灯,所有人负责来定位问题。就和生产环境出告警了一样来对待。如果做不到这一点,所谓可复制的环境就是镜花水月了。
解法二:生产环境自检
如果大家都只认同我只需要为生产环境负责,那就把生产环境变得更强大好了。复杂的机器都有“自检”的功能。我们要做的就是让生产环境可以跑测试的流量实现自检,类似于 windows “打印测试页” 这样的功能。四色建模里的 party/place/thing/moment interval,大部分要测的行为都是moment interval。通过把 party 这个主体给换掉,把 place/thing 这两个维度的代码加以改造(比如一些分城市统计逻辑),可以实现 moment interval 的重放。
这种方式的实质是仍然是推动全流程的集成。因为全流程持续集成在线下推行失败,而退而求其次,选择在生产环境来做。
解法三:基于接口契约的开发
强化接口契约的作用。通过mock掉所有的外部依赖,单独测试我自己负责的模块。通过线上tcpdump的方式,方便构造这些mock的请求和响应,减少mock的工作量,以及让mock真实可信。避免查了半天问题,结果发现是mock接口和线上实际行为不一致,这样的乌龙问题。
同时进行接口的形式化定义,并且通过 consumer driven contract 的方式把调用方的mock,变成提供方的测试。
这种做法就是承认微服务架构的实质是利用组织边界来强化软件架构的边界。既然人为构建了组织墙,与其忽视其存在,还不如对它好好对待。既然我调用对方,对方不愿意帮我解决环境问题,我也没有能力独立把对方的代码跑起来。那不如我们就划界而治吧。大家把接口约定好,我用假的实现来替代你的真实实现来做开发。这种做法和我们调用国企银行的接口,双方联调的方式并无本质不同。
刻度尺
在中央威权强的场景,书同文的方式是最经济的方式。无论是统一开发语言,还是统一RPC框架,还是统一服务发现。本质上来自于书同文带来的收益大于威权推进的成本。
在越松散的组织下,越会趋向于面向接口开发。
后记
从统一的自动化脚本,puppet/chef/saltstack
到 docker + 网络代理
到 线上 tcpdump 线下流量回放,基于接口契约的开发
技术难度逐步升级。通过技术上的技巧(比如 docker 利用了大家都是基于统一的操作系统接口)确实可以解决一些问题。但是技术再怎么升级,也无法解决所有问题。毕竟,所谓环境问题,所谓测试问题,所谓我的代码跑不起来的问题,都是人与人之间如何整体地高效协作的问题。技术的尽头,是政治。