微服务,DevOps设计与协作反思
在此反思下我们用微服务来开发DevOps产品时, 设计开发中遇到的种种问题.
拆分
我们一度把几乎每个模块(是的,模块),一组相对独立的功能都拆分成了一个"微服务". 然而拆分粒度过小, 带来的后果就是互相之间依赖极高,形成了一张网.
表面上看, 这带来的主要成本就是组件之间依赖关系多, 需要提供各种各样的内部API, 然而实践中发现, 拆分过细的微服务还带来了以下副作用:
- 组件开发协同的时间代价, 这个时间代价=沟通API及数据格式+各自开发+debug
- 更多更复杂的安装组件, 不但为部署运维增加开发难度, 开发过程本身, 易用的调试环境就难以保障稳定运行.
- 更多的镜像. 某些客户环境是网络封闭的, 安装人员必须携带打包好的镜像去客户现场安装, 结果巨大的镜像每次都要花费较多时间.
理想状态下, 微服务应该是只需要少部分前置条件, 就能够独立的提供一部分功能. 开发者应当可以轻易的组建出自测环境. 一个比较好的例子, 在EMC Rubicon团队时, 每个微服务组件都自带一组docker-compose(当时我们部署还是在marathon上), 对自己的必要依赖进行mock或抽取, 每次进行更改代码时, 都能够轻易的搭建出简易的测试环境.
包产到户
在实际开发中, 有些组件采取了一人一组件的开发方式, 短期内大家都没问题, 但这些组件的设计过程和开发过程, 很多实现细节, 都是对团队不透明的.
由于没有良好的开发规范, 这些组件的开发过程完全对团队处于黑盒状态, 有时直到交付, 才发现从设计上就存在严重的问题. 而在代码开发过程中, 提交代码由于缺乏上下文, 代码难以Review, 质量更是无从谈起. 如此实行微服务开发, 在实际开发过程中, 由单人开发的组件, 其开发过程中往往更加不重视文档, 当人员发生流动时, 很多的细节都会丢失.
- 我们有一个旧版本组件, 由单人负责进行了"3代单传", 在流动到第三个人时彻底失去了可维护性.
- 某个组件设计开发过程中仅约束API, 最终合作调试时才发现, 这个组件由于设计缺陷, 存在严重的性能问题.
- 某组件的设计一直由初级工程师负责, 处于三不管状态, 某天突然发现对接是有问题的.
- 某组件由情侣档负责,平时几乎完全不透明,而他们的离职时间几乎是一致的
强烈建议在开发初期就搭建一套自动化的代码质量审查, 如Python的flake, Go的gometalinter, SonarQube等. SonarQube这些工具的一个最大好处, 可能就是在某人不得不独立开发组件, 别人又没时间review的情况下, 能够提供一个大家都能看懂的质量评估.
测试
理想的微服务开发过程中, 应当能够在以下时刻做到测试:
- 写一个功能时, 能够对函数是否符合预期写出单元测试.
- 写完一个API或功能时, 应当能够在本地快速的搭建起测试进行基本自测.
- 提交/合并代码时, 能够自动启动若干套微型环境, 对其进行回归测试.
- 其它正常测试.
单元测试, 并不仅仅是一种质量保证. 在开发者无法也不可能为每一行代码写清楚他自己的设计意图时, 单元测试也能够成为一个警报, 让后续开发者能够以最低的代价发现一些可怕的bug. 我在这份工作最开始时, 接手的是一个实习生和另一个"前任"赶工几个月完成的两个微服务组件, 没有任何交接文档, 在当时进行修改时, 一切都是战战兢兢的, 因为当时,以上4个测试都没有, 唯一有的就是每个版本提测之后的人工测试. 人工测试的成本是巨大的, 许多重复性的体力劳动也是极其愚蠢的.
后来我增加了大量单元测试, 也曾在Epam做过使用Selenium进行移动端/前端测试, 写过API接口测试, 在写这些测试用例时, 让人比较舒服的是:
- 单元测试最容易写的是依赖少的底层, 依赖越多, 测试越难写.
- 前端使用Selenium测试时, 页面控件都有易于辨识的id是最好的, 否则需要大量逻辑, 还容易出错.
- API不需要很多步骤就可以完成调用, 最难测的API就是嵌套层次很深的那种.
追踪
追踪是一个非常基础的要求, 它在微服务组件互相调用出现问题, 找背锅侠尤其重要. Jaeger, ZipKin可以通过官方的SDK集成追踪, 如果要做的简单一点, 直接往日志里打印信息,配套Graylog之类也OK.
追踪的基本过程是:
- 网关进入请求, X-Request-ID=1
- 请求进入组件A, A写入记录,并记住 X-Request-ID=1
- A有一个附加信息需要请求组件B, B记录 X-Request-ID=1 ….
- 请求结束, 返回信息. 如果出错, 向客户端返回错误信息, 其中记录 request_id=1
当请求出现性能问题时, 我们可以通过日志/追踪找出瓶颈所在. 当请求失败时, 我们可以找出是谁在报错.
这需要在开发之初就明确的定下规范, 即一定要有一个统一的 request header, 在发生嵌套调用时一定要带上它.
如果以上都没有, 那么还有Tcpdump, 或者 Wireshark. 在容器环境下, 可以通过这样的命令来远程通过容器启动一个tcpdump, 并在本地机器通过Wireshark接收数据包:
ssh root@<server-ip> 'docker run --rm --net=host --log-driver json-file --log-opt max-size=10m corfr/tcpdump -i any -w - not port 22 2>/dev/null' |wireshark -k -i -
当微服务需要这样来找出背锅侠的时候, emmmm…
设计
- 是否要为了所谓易用性, 而牺牲掉参数的可定制性?
例如docker build
这样一个命令, 官方就有十几个参数可用, SDK更是有很多东西可以定制. 那么我们真的可以只去考虑迎合最简单的需求, 而忽略掉那一堆可定制的参数吗?
在做云原生开发时, 一切都在K8S基础上打转, 如果不去深入了解K8S/Docker, 来片面的从用户那里收取建议决定参数, 就很容易陷入"幸存者偏差",让设计缺少通用性. 我们一直在致力于帮助传统用户转向云, 往往这些用户了解的相关知识比较少, 正好就是完美的"幸存者样本", 设计者不去沟通了解, 就可能会设计出"用户只能进行 docker built -t=xxx -f Dockerfile
这样的东西.
- 设计应该由谁来参与, 谁来决定?
用户, 开发者, 设计师, PM都应该参与到设计过程中来. 当我们决定, 要基于某个现存的热门技术做一个产品时, 这门技术本身会形成一个界限, 开发者往往需要付出极大代价才能实现它原本做不到的事情. 用户可能会存在取样偏差, 需求并不是一个通用的需求. 如果单纯的根据职责划分, 让PM收集/沟通/归纳需求, 让设计师设计界面和操作, 最后很可能会出现一个让大家都不愉快的怪胎.
决策者必须要对这些技术有充足的了解, 以它们作为决策基础.