微服务,DevOps设计与协作反思

在此反思下我们用微服务来开发DevOps产品时, 设计开发中遇到的种种问题.

拆分

我们一度把几乎每个模块(是的,模块),一组相对独立的功能都拆分成了一个"微服务". 然而拆分粒度过小, 带来的后果就是互相之间依赖极高,形成了一张网.

表面上看, 这带来的主要成本就是组件之间依赖关系多, 需要提供各种各样的内部API, 然而实践中发现, 拆分过细的微服务还带来了以下副作用:

理想状态下, 微服务应该是只需要少部分前置条件, 就能够独立的提供一部分功能. 开发者应当可以轻易的组建出自测环境. 一个比较好的例子, 在EMC Rubicon团队时, 每个微服务组件都自带一组docker-compose(当时我们部署还是在marathon上), 对自己的必要依赖进行mock或抽取, 每次进行更改代码时, 都能够轻易的搭建出简易的测试环境.

包产到户

在实际开发中, 有些组件采取了一人一组件的开发方式, 短期内大家都没问题, 但这些组件的设计过程和开发过程, 很多实现细节, 都是对团队不透明的.

由于没有良好的开发规范, 这些组件的开发过程完全对团队处于黑盒状态, 有时直到交付, 才发现从设计上就存在严重的问题. 而在代码开发过程中, 提交代码由于缺乏上下文, 代码难以Review, 质量更是无从谈起. 如此实行微服务开发, 在实际开发过程中, 由单人开发的组件, 其开发过程中往往更加不重视文档, 当人员发生流动时, 很多的细节都会丢失.

强烈建议在开发初期就搭建一套自动化的代码质量审查, 如Python的flake, Go的gometalinter, SonarQube等. SonarQube这些工具的一个最大好处, 可能就是在某人不得不独立开发组件, 别人又没时间review的情况下, 能够提供一个大家都能看懂的质量评估.

测试

理想的微服务开发过程中, 应当能够在以下时刻做到测试:

  1. 写一个功能时, 能够对函数是否符合预期写出单元测试.
  2. 写完一个API或功能时, 应当能够在本地快速的搭建起测试进行基本自测.
  3. 提交/合并代码时, 能够自动启动若干套微型环境, 对其进行回归测试.
  4. 其它正常测试.

单元测试, 并不仅仅是一种质量保证. 在开发者无法也不可能为每一行代码写清楚他自己的设计意图时, 单元测试也能够成为一个警报, 让后续开发者能够以最低的代价发现一些可怕的bug. 我在这份工作最开始时, 接手的是一个实习生和另一个"前任"赶工几个月完成的两个微服务组件, 没有任何交接文档, 在当时进行修改时, 一切都是战战兢兢的, 因为当时,以上4个测试都没有, 唯一有的就是每个版本提测之后的人工测试. 人工测试的成本是巨大的, 许多重复性的体力劳动也是极其愚蠢的.

后来我增加了大量单元测试, 也曾在Epam做过使用Selenium进行移动端/前端测试, 写过API接口测试, 在写这些测试用例时, 让人比较舒服的是:

  1. 单元测试最容易写的是依赖少的底层, 依赖越多, 测试越难写.
  2. 前端使用Selenium测试时, 页面控件都有易于辨识的id是最好的, 否则需要大量逻辑, 还容易出错.
  3. API不需要很多步骤就可以完成调用, 最难测的API就是嵌套层次很深的那种.

追踪

追踪是一个非常基础的要求, 它在微服务组件互相调用出现问题, 找背锅侠尤其重要. Jaeger, ZipKin可以通过官方的SDK集成追踪, 如果要做的简单一点, 直接往日志里打印信息,配套Graylog之类也OK.

追踪的基本过程是:

  1. 网关进入请求, X-Request-ID=1
  2. 请求进入组件A, A写入记录,并记住 X-Request-ID=1
  3. A有一个附加信息需要请求组件B, B记录 X-Request-ID=1 ….
  4. 请求结束, 返回信息. 如果出错, 向客户端返回错误信息, 其中记录 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…

设计

  1. 是否要为了所谓易用性, 而牺牲掉参数的可定制性?

例如docker build这样一个命令, 官方就有十几个参数可用, SDK更是有很多东西可以定制. 那么我们真的可以只去考虑迎合最简单的需求, 而忽略掉那一堆可定制的参数吗?

在做云原生开发时, 一切都在K8S基础上打转, 如果不去深入了解K8S/Docker, 来片面的从用户那里收取建议决定参数, 就很容易陷入"幸存者偏差",让设计缺少通用性. 我们一直在致力于帮助传统用户转向云, 往往这些用户了解的相关知识比较少, 正好就是完美的"幸存者样本", 设计者不去沟通了解, 就可能会设计出"用户只能进行 docker built -t=xxx -f Dockerfile 这样的东西.

  1. 设计应该由谁来参与, 谁来决定?

用户, 开发者, 设计师, PM都应该参与到设计过程中来. 当我们决定, 要基于某个现存的热门技术做一个产品时, 这门技术本身会形成一个界限, 开发者往往需要付出极大代价才能实现它原本做不到的事情. 用户可能会存在取样偏差, 需求并不是一个通用的需求. 如果单纯的根据职责划分, 让PM收集/沟通/归纳需求, 让设计师设计界面和操作, 最后很可能会出现一个让大家都不愉快的怪胎.

决策者必须要对这些技术有充足的了解, 以它们作为决策基础.