乍一看微服务似乎很容易构建,但是要真正构建微服务,要完成的工作可比在容器里运行一些代码,并在这些代码间使用HTTP请求进行通信,要多得多。在开发新的微服务之前——必须得在新服务部署到生产环境之前——你需要回答下面这10个重要的问题。
1. 如何测试服务?
当考虑到测试时,微服务有一些优势和劣势。一方面,定义良好的一小段功能的小型服务的单元测试,要比测试整个大型应用程序容易得多。另一方面,要验证由很多微服务组成的整个应用程序的质量,则必然会显著加大测试的复杂度:无法运行单个命令来测试一个进程里的代码,大量集成的互相依赖的组件必须首先运行起来,才能验证其健康状况,并且需要在测试过程中一直保持其运行。
新的微服务是否既会进行隔离测试(使用单元测试或模拟依赖),也会连接到一些生产环境里实际会连接的服务上,在更贴近实际使用情况的“集成”或者“过渡”环境里进行测试?测试是否会合并性能验证和错误模式?是否所有测试都会自动化,或者需要人为干预测试的运行以及测试结果的查看?要让微服务可以简单,快速,并且自动化地进行测试,这鼓励开发人员维护测试,并且避免“破窗”问题。
2. 如何配置服务?
一旦新的微服务部署到生产环境里,如何才能影响其内部行为?这里包括基础架构的改动(比如,在资源池里改变最小进程数),以及一些应用程序级别的改动(比如,通过触发特性标志来启动新特性)。对于这所有的改动,要重点注意让这些改动生效是否需要重启服务。
3. 系统的其他部分如何使用该服务?
除非系统的其他组件的确需要使用某个服务,才有必要构建它,因此理解它们会如何使用该服务至关重要。
其他组件和这个新服务的交互是同步还是异步的?是否鼓励它们缓存来自该服务的响应?怎样重试并且保证结果的幂等性?新的微服务的在线时间的SLA是否匹配系统的其他组件?
需要清晰定义这个新的微服务所能够提供的响应延迟,调用该服务的组件必须知道这些指标。这样,当没有达到这些指标时,系统的其他部分才能够决定发出超时信号,中断当前操作,或者转到服务的另一个实例上。
4. 如何保护该服务?
除非在一个要求高度安全的环境里,大部分部署在防火墙后的微服务不需要过多操心服务内的安全性。在微服务之间添加大量的安全检查会带来巨大的操作复杂度,并且使得生产环境问题更难调试和修复。即使为服务内通信使用HTTP之上的HTTPS都会因为要求维护,部署以及保护一些正确签名的证书,而带来大量维护上的额外开销。
通常的方案是让流量在微服务间畅行无阻,同时使用应用程序级别的授权和认证,这样来保证应用程序级别的安全。
因此,系统里的其他组件应该能够给微服务发送请求,但是可能仍然需要同时传递一些认证数据,这些数据代表实际被批准的该请求最初的外部用户。这些数据永远不能是明文密码数据,可以使用类似JWT,OAuth,SAML和Auth0这样的技术。不管使用哪种方案,都需要清晰记录所用技术,最好包含在客户端库或者示例代码里,从而让其他开发人员能够轻松使用这个新的微服务。
5. 如何发现服务?
当新的微服务启动后,系统里的其他组件如何发现它?发现服务的流程越简单,可能的复杂度就越小,但是之后面临的问题就会越多。比如,最简单的方法(当然也很容易出错)就是在其他依赖于该服务的组件代码或者配置里硬编码微服务的地址。这样确实能够工作,直到服务地址发生变动,或者直到在其他域里启动了该服务的多个实例。这显然不是推荐的方式。
使用间接技术,比如DNS名称,能够一定程度上更好地隐藏微服务的地址,但是这样的方案也有自身的缺陷:找到一个合适的TTL值,强制重做名称解析,保证DNS缓存行为一致,等等。从设计上看,DNS并没有考虑服务的可用性,这会使得应用程序的组件将请求发送到一个无人侦听的IP地址,会很浪费时间,并且因为尝试找到能够工作的实例而干扰到实际的运营。这也会让开发人员的体验非常糟糕,因为使用DNS作为路由机制通常会导致开发人员需要频繁修改其/etc/hosts文件。
最复杂的方案里,高可用的数据库和数据同步服务(比如ZooKeeper)可能会用作当前可用并且工作良好的微服务的注册处。这样的方案要求更多的技术投资,并且需要小心处理,保证发现服务本身不会成为单点故障点(Single Point of Failure,SPOF)。微服务启动时,会将自身注册到这样的注册服务里,微服务关闭时则会将自身移除。如果微服务意外终止或者死锁了,它们也会被自动地从注册处移除。记住,发现服务并不仅仅是找到什么在运行——知道什么不可用也非常重要。
6. 负载增加时服务如何扩展?
如果某个微服务在应用程序里的确很有价值,那么使用其的开发人员会不断增加,随着使用的增长,流量会激增。新的微服务有设计良好的扩展计划,这对于运营团队非常重要。
微服务是否能自动扩展?是否有状态驻留在内存里,导致自动扩展和请求路由很困难(比如,用户会话状态)?如果有的话,分区策略是什么?
如果能事先了解大幅扩展时微服务的哪个部分会首先出错会很有用。对于由数据库支持的服务而言,计算能力(比如,位于自动扩展组里的EC2实例)通常能够持续扩展直到数据库不堪重负。对于真正的无状态服务而言(比如,不从数据库读取也不写入的计算型微服务),首先会出问题的可能是位于实例集群之前的负载均衡器。这两种情况都有对应的解决方案,不过在部署微服务的第一个版本时,这些方案并不一定需要就位。但是,需要详细了解新的微服务的限制,这样才能在生产环境能力达到上限之前就知道服务扩展的天花板位于何处。
7. 该服务如何处理其依赖错误?
即使微服务的范围非常小,它也可能依赖于系统里已有的其他服务或者monolith程序。比如,大部分应用程序事务需要查看客户信息,因此用来访问客户记录的服务通常是提供业务价值的大部分服务的依赖。
如果新的微服务依赖于任何其他服务,当这些依赖服务故障时会发生什么至关重要。使用固定的请求超时时间是个好的开始,但是添加流程断点可能会更好。所依赖服务的所有者应该也希望其使用者在故障发生时使用类似指数延迟的技术,来避免惊蛰问题的发生。
这种场景很好测试,因为其测试只需要所依赖服务不可用即可。但是,务必记住所依赖服务API的调用失败可能会有很多种原因,这些故障表现也各不相同。
8. 系统的其他部分如何处理该新服务的故障?
取决于在新微服务的高可用能力上投资多少,也取决于其支持何种事务,这可能并不是个重要的问题。比如,一个简单的运营日志微服务,通过UDP异步发送数据,可能出现几分钟的故障,但是这对于应用程序的主要业务事务并没有任何影响。但是,异步处理信用卡事务的微服务如果发生故障,则可能严重损坏电子商务系统,那么这种故障场景就必须严格测试,并为之做好应对准备。
因此,即使范围有限的微服务(或者其开发人员)不需要担心系统的其他部分如何使用这个新组件,系统级别上关于每个服务如何依赖于其他服务的考量能够帮助避免连锁故障,也能帮助确保应用程序的整体性能。
9. 该服务如何升级?
可能大家倾向于认为Docker这样的容器技术,和Ansible这样的部署自动化工具使得升级变得不那么重要,但是微服务的维护需要考虑很多这些已有工具无法解决的其他方面的问题。
定义升级策略,并且决定微服务支持的部署复杂度级别十分重要。想用新版本取代旧版本时,canary测试,蓝/绿部署,特性标志,以及response diff’ing这些,比起简单的滚动升级而言,要求更多的时间和投入。
为微服务API的升级定义界线和策略对于依赖于其的组件更为重要。比如,一次只允许某个API的JSON schema添加一个改动,这样使得服务能够持续改进,并且不要求其使用者每次都必须随之升级。但是,向XML响应负载里添加新字段时,如果其使用者每次都做XML schema验证的话就会导致严重的问题。因此如果规律升级新的微服务,向其API对象添加越来越多的字段,那么需要在其文档里清晰记录,告知其服务的使用者。
最后,要了解如果新版本有问题时,微服务如何回滚,以及如何考虑“回滚判断指标”。
10. 如何监控并度量服务?
如果你的公司已经有了应用程序监控的标准,那么就应该使用这些标准,借助已有的监控生态系统。注意不能忽略已有标准——或者更为严重地——使用新的监控工具而运营团队之前完全没有使用过。
如果你的公司还没有高质量的应用程序监控系统,向应用程序添加新的微服务可以作为推动监控系统搭建的起点。这对于之前都是监控大型monolithic应用程序,现在才开始向微服务架构迁移的企业来说尤其重要:一系列互相连接的微服务的运营监控需求要比单个大型的monolith程序的监控需求复杂得多。
无论选择哪种监控方案,是自己开发,选择开源软件还是商业软件,微服务的开发人员都应用能够完全访问其组件的监控和度量数据。如果缺失这样的透明度,那么就无法实现完整的反馈回路,开发人员就无法知道如何在生产环境里改进其服务,也无法在发生问题时帮助快速诊断出问题所在。
总结
并不要求大家对上述10个问题的每一个都有特别详细的答案,但是需要对每点都加以考虑,并且知道微服务可能会带来的架构上的限制。比如,一开始新的微服务部署时可能并没有考虑灾备和域故障容忍,随后升级来包含这样的能力。了解微服务当前能做什么,不能做什么至关重要,思考上述问题的答案能够帮助持续改进服务,最终演进成为成熟的,弹性的,可靠的系统组件。