通过前两篇介绍过微服务架构的服务本质与服务的交互后,作为这一系列文章的最后一篇,本文将将介绍服务的开发、部署、运维,以及与人员有关的最佳实践。
开发
源代码控制
每个服务都该有自己的代码库。这样可确保签出规模尽可能小,源代码控制日志更简洁,并能对访问进行更细化的控制。服务并不是一起部署的,服务源代码也不该共置在一起。
此外还要对源代码控制实现标准化。这样可简化团队工作,并让持续集成和持续交付等工作更简单。
开发环境
开发者需要能在自己计算机上快速工作。为确保针对任何操作系统提供一致的环境,可将开发环境打包为虚拟机。
然而考虑到微服务方法的复杂度以及所涉及服务数量,让开发者通过一台计算机完成所有开发工作并不现实。此时可将在本地开发和运行的服务与云中运行的隔离环境结合在一起。这样开发者就可在自己的开发环境中快速迭代,同时配合云中运行的其他服务进行测试。需要注意的是,隔离对这种云环境来说非常关键。在开发者间共享环境只会由于非预期的变更造成大量混乱。
持续集成
需要尽快将开发中的代码与主线分支进行集成。对主线分支的更新可触发持续集成系统自动构建,构建可触发自动化测试以确认该构建是否足够完善。
自动化测试是在开发者的计算机上运行的,因此可在持续集成系统上运行更复杂和耗时的测试。这方面有很多流行的解决方案可通过计算机群集并行执行多个测试,确保能更快速完成工作。
如果所有测试都成功通过,持续集成系统会将待部署程序包发布至自动化部署系统。
这样做能获得哪些收益:
1.代码可更快速集成,每个人可更清楚地看到变更。如果多人更改同一处代码造成冲突,也可尽快发现提早解决。
2.更频繁地运行完整测试,可更快速发现Bug。
3.最重要的是,由于每次迭代只需要集成少量变更,开发者可对这些变更的正确性更自信。
持续集成可改善团队快速交付高质量软件的能力。
持续交付
持续交付的目标在于更快速地发布小规模变更。此时无须一次发布大量变更,可将其拆分为小块,逐个完成并发布。这个过程中系统依然处于正常运行状态下。
为实现持续集成,需要快速完成整个构建、测试,及开发周期。这意味着要建立稳固的持续集成和自动化部署流水线。
但这样做会不会让最终用户收到尚未完工的功能?
功能开关(Feature flag)可以帮助你确保新功能只有在准备好之后才会被发布给特定的用户。这样能用小规模方式部署变更,用户不会收到尚未完工的功能。
共享库带来的风险
“无法控制何时将共享库的更新部署到使用它的服务上”是使用共享库带来的最大挑战。可能需要等待数天甚至数周其他团队才会部署更新后的库。在一个以独立方式开发和部署不同服务的环境中,任何需要所有服务同步更新的变更都是不切实际的做法。
此时最佳做法是发布弃用时间表(Deprecation schedule),并与服务团队协调,确保能及时应用更新。因此对共享库的任何变更也需要考虑向后兼容的问题。
如果还是不明白的话:共享库很适合管理诸如连接性、传输、日志,以及监控等辅助内容。与服务有关的业务逻辑也不应该放入共享的库中。
服务模板
除了核心业务逻辑,服务还管理一系列其他附加任务。例如:服务注册、监控、客户端负载均衡、限制管理、断路。团队应能通过这些模板快速实现服务自举(Bootstrap)以处理所有常见任务,并与平台进行恰当集成。
必须使用模板吗?
模板是为了加快团队工作速度,但并非必须的。但某些行为可能是必须的,例如实现注册、监控和日志所需的行为。此时更合理的做法是由团队自行决定是否要从零开始构建以满足对具体行为的要求,而非必须使用现成的模板。
那么可以为每个流行的技术栈创建一个模板吗?
虽然微服务催生了一种多语言(Polyglot)架构,但也不能因此失去理智。仅支持少量技术,这样的做法可以带来多个收益:
1.团队无须为每个技术栈重新实现所需工具,可更轻松地专注于构建稳健的标准化工具。
2.有助于促进跨团队代码审阅工作。
3.最重要的是,可以让开发者更轻松地加入其他团队。
因此应该为每个受支持的技术栈提供模板。
服务的可替换性
随着所用服务数量逐渐增加,最终将面临架构设计的局限。届时应该已对具体需求和服务使用模式有了更深入了解,进而实现可扩展性更完善的解决方案。由于服务都是尽可能简单并专注的,此时服务替换工作也会变得更容易。
也许你会希望换用更专业的数据库,或换用其他语言。只要对已记录的接口(API和事件流)进行妥善维护,即可在不影响其他服务的前提下彻底更换整个实现。
.. 或者也许想更换一切,包括API本身!此时可以创建全新服务。将原有服务的用户迁移至新服务,当原有服务不再使用时删掉即可。
部署部署程序包标准化的部署程序包是自动化部署流水线中重要的组成部件。
部署程序包须满足下列特征:
1.可部署到任何位置:同一个程序包应当能在不改动的情况下部署到任何环境:开发、准生产环境(Staging)或生产环境。
2.外部配置/密信(Secret):配置和密信不应存储在程序包内,需要在启动时提供给程序包(或由其自行获取)。
3.隔离的部署:如果多个服务共享同一组资源,很容易由于一个服务无意中消耗了大量资源导致其他服务受到影响。将部署的每个服务隔离起来可以将这种影响降至最低。
系统镜像可以很好地满足这些要求。可为每个服务创建系统镜像并进行版本控制。每次更新服务都可创建一个新镜像。通过对物理机、虚拟机,或容器创建这样的系统镜像,便可对系统所用资源(内存、CPU、网络等)进行限制和监控,并对不同服务提供一定程度的隔离。这样做的实际效果等同于在每台主机上只运行了一个服务。
铁打的基础架构,流水的镜像
当部署程序包是系统镜像时,绝不能对运行中的系统进行就地更新,而要通过新镜像构建的系统进行替换。这种方法可提高开发者的信心和系统可靠性,因为测试和生产环境的部署使用了完全相同的镜像。这种做法还能避免配置差异导致对生产环境进行的直接变更。
自动化部署
在将任何服务的任何版本部署到任何环境时,开发者需要通过一种统一方法触发自动化部署。确保全自动化,尽可能简单的部署,开发者也可更轻松、更频繁地部署小规模的变更。
我们的目标是零停机更新
如果要让服务离线才能应用更新,每次更新无异于向其他所有服务发出了一股震荡波。为避免这种细微的干扰(有可能对频繁的部署产生阻碍),需要通过某种方式在零停机前提下对服务进行更优雅的更新。
一种方法是轮流重启动,此时可对负载均衡器之后的实例挨个更新和重启。虽然听起来较为可行,但如果遇到问题需要回滚,还需要再一次进行全面的轮流重启动。
更稳妥的方法是让运行新老版本的实例并行运行,但不通过新版实例响应请求。随后将负载均衡器切换至新版实例,同时继续将老版实例运行一段时间,以备需要快速回滚。这种方法更强大,也更适合可临时获得更多资源的云环境。
功能开关
功能开关(Feature Flag)是在运行过程中打开或关闭特定功能的代码,借此可有效地实现代码部署和功能部署间的解耦。通过这种方式可在一段时间里以增量方式部署某个功能的代码,随后在准备就绪后将功能发布给用户。
服务团队需要使用接口查看并管理平台的功能开关,用于查询开关的代码可包含在共享的库中。
增量式的功能发布
功能开关使得我们可以分阶段将功能发布给一组用户。例如优先将功能发布给10%的用户,或发布给特定地区的用户。借助这种方式可在影响到更大规模的用户前发现可能存在的问题,通过关闭开关还可以实现功能的快速回滚。
开关的寿命应该短一些
功能开关只须在功能成功部署前使用。长时间使用这样的开关是个糟糕的主意:会让用户支持工作变得更困难(因为不同用户会遇到不同行为),系统测试工作的难度会加大(因为存在多个代码路径),同时系统调试也变得更难。功能全面部署后,应尽快安排删除对应的开关。
只将开关封装在入口点中
功能开关的目的在于实现功能部署和代码部署之间的解耦。只需要将开关封装在相应功能的入口点,不要封装在所有相关的代码路径内。例如对于用户界面上可见的功能,可将开关放入为了进入相关功能需要在界面上点击的链接/按钮中。
配置管理
可部署在任何位置的部署程序包中不应包含与特定环境有关的选项或密信(Secret)。因此需要相互独立的解决方案。
团队需要能管理配置,并以安全的方式让服务顺利启动。微服务平台通常针对这种目的提供了内建的解决方案。
交付配置的主要做法包括:
1.环境变量:将配置载入服务的环境变量中。
2.文件系统卷:将包含密信和配置的文件系统挂载到服务中。
3.共享的键/值存储:让服务直接访问共享的键/值存储。
使用环境变量需要注意一个问题:环境变量默认情况下非常易于外泄。此时可通过异常处理器(Exception
handler)获取环境变量并将其发送至日志平台。子进程也会在启动时获得父进程的环境变量,有可能无意中导致密信外泄。为避免这种问题可在读取后将环境变量清空,但这只是个可选的额外操作。
运维
集中化的日志
服务的每个实例都会生成日志。在使用系统镜像作为部署程序包的情况下,每次部署新版本都会替换这些实例。因此任何日志都不应存储在实例中,这样做会导致下次部署后之前的日志全部丢失。
此时可通过平台为服务团队提供集中化的日志系统。所有服务可通过标准化日志格式,将自己的日志发送至同一个日志系统。这种方法为服务团队带来更大灵活性,可跨越所有服务、特定的某个服务,或服务的某个实例搜索日志。这一切操作都可在同一个位置进行。
将日志发送至集中化日志系统所用的代码可包含在共享库中,或通过服务模板提供。
但要如何跨越多个其他服务追踪某一请求产生的影响?
这时候可以用关联ID。在与任何服务通信时都提供一个关联ID,并让服务将该ID保存在自己的日志项中。随后在跨越多个服务搜索某一关联ID时,就可用时间线的方式看到某个原始请求对所有服务造成的副作用。
集中化的监控
遭遇故障后,帮助我们快速了解问题影响范围和根源的工具可带来巨大价值。集中化监控应成为平台核心组件。这种工具可以让团队针对整个平台获得更深入的了解,尤其适合在解决连锁故障时使用。
考虑到高可用性,永远要在负载均衡器之后为一个服务运行多个实例。因此监控解决方案必须能将不同实例的衡量值汇总在一起。此外还要能在聚合后的衡量值中快速向下挖掘,以查看特定组件的详细信息。这一切都有助于帮助我们快速确定故障是否是服务端造成的,或是否要对服务的某个实例进行隔离。
要监控哪些度量值?
取决于不同类型的组件,需要监控的度量值也各不相同:
1.基础架构:可在操作系统层面收集数据。文件系统操作、文件系统延迟、网络操作、内存使用、CPU使用。
2.常规:发送至服务的请求。请求数、请求延迟、错误数(总数和每个错误代码分解数)。
3.集成:该服务向其他服务发起的下游请求。请求数、请求延迟、错误数(总数和每个错误代码分列数)。
4.外部服务:与第三方托管的服务或微服务平台之外管理的其他系统通信的服务。
5.具体服务:与特定服务有关的任何其他衡量值。
除了与特定服务有关的衡量值外,其他一切信息都可通过服务模板或共享库中的代码自动捕获。通过使用自动化的捕获机制,还能为需要监控服务的团队提供有用的初始配置信息。
通过分布式追踪将线索连接在一起
虽然监控解决方案可以很好地帮我们确定特定服务内外发生了什么事,但依然很难跨越多个服务将不同线索连接在一起针对大环境获得更深入的理解。
分布式追踪系统的请求追踪功能可细分为对服务的每个请求进行追踪。随后所有数据会通过时间线进行可视化,借此即可更深入地了解某一特定请求是如何在不同服务之间流动的,并能快速发现性能瓶颈。
分布式追踪意在监控所记录的关联ID。这两者非常类似,追踪系统用于区分不同请求的ID也可以充当关联ID。
自动伸缩
无状态服务本身是易于伸缩的,只要根据需求在负载均衡器后添加更多实例即可。做出伸缩决策所需的信息(CPU/内存用量等)可通过监控平台获取。
很多微服务平台为实例数量的处理提供了声明性接口,这种功能非常易用。只须告知需要的实例数量,其他工作可由平台自行处理。在这样的平台上实现自动伸缩,只要以编程的方式更新“所需实例数量”即可。另外还可借助这一过程在现有实例故障后增加新的实例。
外部服务
服务可能还要与并非自己团队创建的系统通信,例如:数据库、缓存、消息队列、邮件交付系统等。这些系统可以托管式服务由第三方交付使用,或在自己组织内部自行托管相关服务。无论哪种方式,考虑到服务数量及不同环境可能需要自己专用的系统实例,都要确保这些系统的供应和管理也能实现自动化。
能不能直接将这些外部系统封装为平台上的服务?
使用持久存储提供数据库系统,并将其与自己的日志和监控系统集成,这种做法绝对可行,然而并非总是实用。一些系统对基础架构有特殊要求,尤其在高可用配置下。一些系统可能无法在故障后自动重启动。因此需要具体情况具体分析。
那么可以让多个服务共享同一个系统吗?
只要确保一个服务无法访问其他服务的配置或数据,就可以这样做。例如多个服务可共享一台通用数据服务器,但每个服务使用自己专用的数据库。服务不会发现同一台数据服务器上还运行了其他数据库。当某个服务需要用比其他服务更快的速度伸缩时,还可将其数据库放入一个专用的数据服务器。
这种方法的问题在于,共享资源可能难以单独进行隔离和监控。例如在一台共享数据服务器上,可能有一个服务占用大量资源并无意中影响到其他服务的性能。如果监控机制粒度不够细化,可能要花费大量时间才能确定有问题的服务。
人员
完整的生命周期所有权
服务团队需要拥有、运维并完善自己构建的服务。这些工作需要持续到服务退役那一刻,而非服务发布的那一刻。
通过这种方式,感受到由于架构设计局限所造成痛苦的团队,也将能顺利修复这些问题。在决定如何演化服务以满足未来增长的需求过程中,团队成员针对运维工作的进一步了解也能提供宝贵的意见和价值。在简化运维工作方面所做的全部努力最终都将进一步改善服务的稳定性。
自治的全栈团队
在构建大量小规模服务时,每个团队成员将成为多个服务的所有者。重点在于拥有这些服务的团队必须具备开发、部署,以及运维这些服务所需的全部技能和工具。他们的日常运维工作必须全面自治,这样才能快速响应不断变化的业务要求。
应对团队成员的流失
时不时会有人离职。遇到这种情况时,需要确保不会有服务成为“孤儿”。就算某个服务可以在很长时间内正常运行不出现任何问题,依然需要在出现问题后有人负责善后。
人员还会在组织内部流动。为整个微服务平台实施一致的开发、部署和运维实践,可以在服务所有权易手后将学习曲线降至最低。
团队能大到怎样的规模?
随着团队规模进一步扩大,交流沟通开始变得不易。团队规模应足够大,使其能自行完成相关工作而无须将大量时间浪费在交流沟通的过程中。例如亚马逊就以“两个披萨团队”广为人知,两个披萨恰好能让整个团队成员吃饱。
|