微服务体系的发展并不是一蹴而就的,经过了2014年前后的低潮期,微服务概念顶层的泡沫逐渐褪去,那些真正能够在企业落地的实践在一轮又一轮的大浪淘沙后被甄别、沉淀。这篇文章希望讨论一些在团队中实行微服务架构时值得考虑的『增值项目』,它们中的一些看起来已经是理所应当的,而另一些似乎和微服务并没有必然的关联,但许多经验能够证明这些项目都是保障微服务系统长期运作并最大化发挥其Scale Out能力值得投入的高附加值实践。
持续交付
对于微服务的成功实施,团队持续交付能力是至关重要的衡量指标。在由上百个服务组成的复杂系统中,如果所有服务都按照人为指定发布周期进行整体交付,很容易出现由于细小的失误导致大面积线上故障。
持续交付实践要求每个独立服务都具有完备的交付流水线,在流水线的末端随时能提供当时最新的可工作、可交付的产品。持续交付通常会配合自动化的测试和部署手段,从而减少功能代码提交到上线的端到端时间。这就使得每个独立服务能按照各自不同的节奏进行发布,并且将自己的发布状态可视化出来。
采用尽可能精简且稳定的分支策略也是使得持续交付流程能够顺利实施的关键,我们提倡使用单主干的分支策略(Trunk Based Development)。在单主干的开发方式中,除了一个用于持续开发和集成的『主干分支』(通常即Master分支)和一系列依据发布周期创建的发布分支以外,应该避免创建其他的Long-lived分支。如果有多个功能需要开发,则推荐采用特性开关(Feature Toggle)的方法来控制它们的发布时机。当然,单主干策略是允许存在短生命周期特性分支的(短于一周),有时这些小分支甚至无需提交到远程仓库中。下面这是一幅经典的单主干分支策略示意图。
值得指出的是,在划分得当的微服务系统中,同一个服务需要同时进行开发的特性通常不会多于两到三个(否则应该考虑这个服务是否承担了过多的职责)。因此即使在不需要特性开关和其他额外开发工作量的情况下,已经可以比较好的实现每个功能点的独立发布和测试,这反向说明了微服务架构对于持续交付的实施也是十分友好的。
除了严格的单主干,一种常见的变式是多主干策略,典型的是一个开发分支加几个固定的发布分支,通常用于无需维护多个发布版本的SaaS服务交付。这种模式的优点是能够将发布流水线目标环境和分支显示的关联起来,例如『Develop分支』对应集成环境,『Release分支』对应验收环境和正式环境。下图展示了一组与此模式下的持续交付流水线。
保持每个服务高频率的集成和交付,会使得有故障的功能在很短的反馈周期内被发现,在快速迭代发布的前提下做到整个系统发布井然有序。这样的氛围不仅有利于改善代码的质量,而且能够提高开发士气,频繁的发布上线也有利于增强团队对产品的荣誉感和自信心。
全功能团队
全功能团队是DevOps运动所倡导的一种产品团队组织结构,通过将不同角色的业务和技术成员纳入到团队,组成具备端到端交付和运营能力的完整单元。
康威定律阐述了开发团队的组织结构和其设计的产品结构之间具有的相似关系。许多的实践结果也表明,将全功能团队实践应用在微服务产品中带来的收益,要远远超过它在传统模块化开发的产品中所带来的收益。这是因为微服务的架构中的所有服务真正具备独立运行和独立运营的能力,从本质上来说就是一个端到端的子业务产品。
这种架构和团队的影响是双向的。一方面,微服务的运营结构要求团队具有高内聚的自主管理能力。另一方面,全功能团队也为特定服务进行独立技术选型提供了更灵活的发挥空间。服务与团队通常是多对一的关系,每个团队管理的是一组相互关联紧密的服务群,并且可以在必要的情况下对服务进行进一步拆分。在实际的实践中推荐采用例如接口网关(API Gateway)等方式对一组具有业务意义的服务接口进行聚合,从而保证局部服务结构变化不会直接影响服务的消费方的调用。
值得一说的是,在一些传统企业内的IT部门划分,往往已经按照职能分为开发团队、运维团队、运营团队,甚至单独的测试团队。在这样的企业中很难快速完成全功能团队的转变,因此在实施微服务架构过程中比较容易走偏。对于这种情况可以采用逐步演进的转换方式,具体途径主要有两种。
第一种方式是进行项目试点。对于习惯了按功能分层、分块的『实现接口开发』式的组织,即使勉强凑齐每个角色的人组在一起也难以成为真正具备端到端交付能力的团队。因此与其做得徒有其表,不如找出项目中一些全局意识比较到位的成员,对特定的项目进行试点,然后逐步扩大,将这种端到端的责任和意识带入到更多的项目中去。试点的项目应该是以实施现有系统的一个独立业务功能点为目标,而不是开发与企业主线系统无关的短期产品,否则容易出现试点项目很成功,但项目结束便不了了之的结果。
第二种方式是先从分开发团队入手,纵向划分项目。这是对全功能团队的一种妥协式的引入方式,即在开发团队中首先改变系统架构横向分层、局部分块的开发模式,依据业务功能进行上至外部接口、下至业务数据的独立服务拆分,但并不急于在开发团队中引入诸如测试、运维、业务等角色的成员。这种项目划分虽然在一定程度上为微服务架构执行制造了条件,但从长远来看,并不能为团队进行自主的技术栈和基础设施选型、以及业务数据的利用提供足够的空间。
自动化运维
自动化运维是实施持续交付的必要前提,因此也可以说是采用微服务架构的必要前提。但这里所说的自动化运维,不仅仅包含持续交付所需的服务部署时『一键操作』能力,更重要的是运维基础设施构建的自动化、以及服务灾备、恢复的自动化。微服务架构最初受到追捧的一个原因是它灵活的『局部Scale Out』能力,以功能点为单元的扩展、收缩,这对于具有业务周期性的服务而言更加重要。但一些企业在自身基础设施自动化不到位的情况下盲目实施微服务,期望通过其实现复杂架构的解构,结果在面对突发线上事故时出现雪崩式的连锁反应,情急之下也只能手工恢复重建,耽误大量时间。
实施自动化运维涉及的工具有很多,例如Ansible、SaltStack、Terraform,甚至Docker都可以看做是自动化运维的一部分。这当中大多数工具都提供有定义操作行为的领域DSL,它们通常是一些配置式语言或脚本语言,因此自动化运维也涉及到代码的编写。与开发项目代码不同的地方在于,自动化运维的代码大多不是长期运行的,很多代码也许只在特定场景使用一次,然后就会非常长时间无人问津,直到某些紧急情况才会再次需要用到。此外,运维的代码本身并不直接具有业务价值,这些因素导致它们往往没有被很好的管理起来。
下面以采用Ansible或SaltStack这类通用自动化工具为例,介绍一些在实践中需要注意的地方。
首先是运维脚本应该通过Git或SVN这样的版本管理工具进行归类和管理。通常来说,推荐将特定服务部署的Ansible或SaltStack YAML脚本文件与服务本身的代码放在同一个代码仓库中,方便开发人员在必要时候快速的修改它。然后将基础设施管理的YAML脚本文件放在单独的代码仓库,方便复用和查找。但这样可能带来的问题是,在实际使用时可能会需要同时获取两个代码仓库的脚本以获得完整的部署功能,因此如果使用的其他配套工具对多仓库支持不佳,也可以将所有运维脚本在同一个仓库管理。
其次,应该在持续交付流水线上使用服务部署的自动化工具,从而实现快速的交付上线。在条件允许的情况下,还应该在流水线上直接配备自动化的灾备恢复任务入口,以及定期的对这些恢复脚本进行测试和演练。
最后,虽然我们鼓励每个团队使用适合自身业务的技术栈进行开发,但对于运维工具的选择通常让同一个产品的各服务采用统一技术栈比较合适(例如不要混用Ansible和SaltStack的脚本)。这个建议主要考虑到运维工作可能会有较多跨团队协作,以及故障恢复场景下的快速救灾操作,统一的技术栈能为运维人员节约掉工具切换的时间。
服务高可用
由于微服务系统中存在着众多跨服务调用,任何一个服务都不能假设自己可以随意的停机一段时间而不对系统的整体功能造成影响。但在现实情况中,正常的服务升级或意外的故障都有可能造成服务短暂或长时间的中断,这种中断轻则引起局部功能不可用,重则导致连锁反应造成重大事故。这些都是在架构设计时候就应该予以考虑的问题。应对这两类情况的方法分别是对服务采用高可用的部署方式,和进行不离线的部署。实现服务高可用的方法有很多,常见的有:L7负载均衡、DNS负载均衡、服务发现、同步/异步消息队列等。
L7负载均衡即在OSI网络模型应用层进行的软件负载均衡,例如Nginx和HAProxy都属于这类。这些L7负载均衡通常带有后端服务检查的能力,会自动屏蔽掉不可用的后端服务,从而在一部分服务出现故障时候,请求仍然能被正常运行的后端服务接收。
DNS负载均衡是利用了DNS服务可以为同一个域名配置多个解析地址,且配置多个地址后,每次解析域名时轮询着将配置的地址返回给请求方,这个特性称为DNS轮询。实际上DNS轮询仅仅是一种特殊的负载均衡技术,本身并不具有检测服务状态、提供后端服务高可用的功能。但一些新出现的开源DNS产品,例如SkyDNS和Consul将DNS服务与服务发现技术进行了结合,具有自动移除不可访问的解析地址的功能,这使得DNS负载均衡也可以被用于实现服务的高可用了。
服务发现是一种基于注册和查询服务信息的键值数据库服务。提供服务的一方将自己的名称和IP地址注册到服务发现的服务端,使用服务的一方则通过服务发现的服务端进行查询,然后将实际请求发送给查询到的目标IP地址。服务发现的服务端会负责检测每个注册服务的运行状态,及时移除出现故障的服务,并在每次收到查询时从符合名称的服务中任意返回一个作为结果。
消息队列则是一种采用中间媒介解耦服务提供者和消费者的方法。服务之间通过发布和订阅消息进行交互,所有的消息通过队列进行分发和中转。这种结构使得消息队列本身成为所有数据通信的瓶颈,与微服务的去中心化思想相悖,因此并不推荐在大型微服务系统中采用。
不离线部署
不离线部署是确保服务随时能发布的必要措施,也是微服务架构团队需要关注的一种能力。在实际应用中,除了一些天生支持不离线部署的技术栈,如Erlang,多数的服务是原生不支持热升级的。对于这些服务,通常来说可根据服务『是否能递进式升级』和『是否具有长任务』的特性,分成三种类型:『不能递进升级的服务』,『能递进升级、无长任务的服务』,以及『能递进升级、有长任务的服务』,分别采用不同策略进行。这里先介绍一下服务的『递进式升级』和『具有长任务』。
递进式升级(Rolling Update)是指将集群中的服务划成多个分组,每次只升级其中的一个分组,然后依次进行,直到所有服务都升级完成的过程。采用递进式升级会使得集群中的服务有一段时间同时存在新旧两个版本。
长任务指是接收到请求后需要花费几秒、甚至几小时才能执行完的任务,例如一些涉及大量计算或需要远程同步调用的事务。具有长任务的服务都有『运行中』和『空闲』这样的运行状态。当服务处于『运行中』的时候,中断它可能导致意外的结果。一般来说在Linux中停止服务的流程是先向服务发送一个TERM信号,使其正常结束,若是信号发送几秒后,服务仍然在运行,才会发送KILL信号将它强行终止。处理短任务的服务通常可以在接收到TERM信号后及时停止,因此不存在这种风险。这里应该将『无长任务的服务』与『无状态的服务』加以区分,后者指的是服务对每次请求的处理,不依赖于其他请求。即服务处理一次请求所需的全部信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务器本身不存储任何信息。通常来说,微服务架构中的服务一定是无状态的,但不一定是无长任务的。
如果服务不能采用递进式的升级,不论其是否具有长任务,蓝绿部署都是一种十分推荐的部署方式。蓝绿部署的做法是同时准备一组线上运行的服务器,以及一组用于下次部署的服务器,两组服务器具有相同的数量和配置。执行部署时,先将新的服务部署到没有放到线上运行的那组服务器上,等到部署全部完成,直接将负载均衡的流量导向到刚刚这组服务器上,从而使得两组服务器的角色互换。下一次进行部署的时候,则换用另外一组服务器执行部署,然后将负载均衡切换回来。这个过程如下图所示:
蓝绿部署的优点在于新旧服务的切换是瞬间完成的,并且当流量切换到另一组服务器上之后,原先的那组服务器可以继续运行,这样即使上面有未完成的任务也不会被强行中断,如果升级后的版本发现了比较严重的问题,也可以快速的切换回原先的版本。而它的缺点也十分明显,那就是会占用比实际需要多一倍的服务器作为下次部署的备用机器。
一些前端服务可能会属于这类情况,我们也许不希望在升级的过程中,一部分用户看到是新的页面,另一部分看到还是旧的页面。另外对于Nginx这类负载均衡工具,后端服务的健康检查并非是实时生效的,有可能出现服务已经离线,但请求仍然被分发到这个主机的情况,因此采用负载均衡作为高可用方案的服务,蓝绿部署也是比较可取的方式。
如果服务的数量比较多,并且允许同时存在两个运行的版本,那么采用递进式升级方式则会更加节省资源。以每次升级一个节点的递进方式为例,当升级开始后,我们首先停止所有服务节点中的任意一个,将它进行升级,然后让它重新加入集群,接着从剩下的服务节点从再任意选择一个,直到最后一个服务也被升级完成。这个过程不需要增加额外的服务器资源,只要待升级的服务具有两个以上的节点,就不会对服务的整体功能造成中断。递进式升级的过程如下图所示:
显然如果服务本身是不能被随时停止的,那么这种简单的递进升级就不能很好的满足了。此时我们需要对服务的调度进行干涉,以采用服务发现的高可用方式为例,下图展示了一种『带状态检查的递进式升级』策略进行服务部署。
这种升级方法具有普通递进式升级的相似优势,但在集群中有个别服务执行任务时间很长,始终处于『运行中』状态的情况下,将使得升级过程阻塞,大大的延长服务部署的时间。事实上,长任务的服务通常都可以被改造成为批处理式的服务(Batch-Task Service),批处理式服务的升级只需要直接将服务执行文件替换,从根本上简化了升级难度。
监控告警
内存不足、磁盘耗尽、网络中断、服务失效,这些天灾人祸随时可能殃及产品的服务集群。很难想象,在一个庞大的微服务系统中,如果没有合适的监控和告警设施,服务的运营会变得多么混乱不堪。
对微服务系统进行监控主要需要考虑两个方面:基础设施的监控和应用服务的监控。
基础设施的监控通常由部署在每个节点上的数据采集端、集中式的数据汇聚端、以及数据展示、数据分析和告警通知等部分组成。而监控告警系统的实施中,还需要结合团队的运维自动化能力,选择合适的技术栈进行。开源的Promethus和InfluxDB都是值得考虑的工具。
应用服务的监控通常需要依据具体的开发技术栈进行选择,例如Java Spring Boot的服务可以用Spring Boot Admin、Nodejs的服务则可用node-monitor和pm2等,此外也有一些通用的开源工具,例如Monit。应用服务的监控除了需要能够比较好完成故障的告警外,一些监控工具还能尝试自动恢复故障服务的运行,这些措施都能有效的增加服务的可靠性。
容器化
容器是一种能够加速促进团队DevOps水平的虚拟化技术。它通过把服务和系统依赖全量打包的镜像格式,将运行环境的设计提前到了开发阶段,并且实现了开发、测试、线上环境的高度一致。由于容器屏蔽了不同服务运行时的差异性,使得基于这种方式进行服务的大规模部署和调度变得简单。具体来说体现在以下几个方面:首先是运行环境的隔离。在虚拟机时代,由于每个业务依赖的系统环境不同,服务器之间无法通用。各个业务都需要独立管理服务运行环境,还往往造成多个服务同时运行的冲突,和各个运行环境不一致等问题。容器为每个服务提供隔离的运行环境,即使在同一个服务器上运行多种运行时相互有冲突的服务也不会出问题。
其次是精细的资源分配。通过虚拟机分配服务资源时,为了简化管理,通常不论服务实际使用多少CPU和内存资源,都只能从固定的主机类型中挑选一种,按主机的个数计费。容器能够很好的实现面向资源池的服务管理,各个服务可以根据并发进程数、CPU和内存用量等资源计费,实现更加精细的资源管理。
此外,容器还有利于资源的动态调整。过去企业里的服务器资源一般是按计划分配的,有的部门为了避开繁琐的资源申请流程,一次性申请大量资源囤积备用,造成浪费。容器的面向资源池特性,使得企业能够将所有计算资源进行运行时动态调整,实现按计划分配到按需分配的转变。业务只需适应流量负载的变化。在负载高峰期快速增加资源,保证业务服务质量,在负载低峰期释放资源给其它服务,提高集群资源利用率。
容器将部署、运行的方式和业务很好的进行了解耦,目前已经有许多成熟的基于容器设计的开源调度框架,例如SwarmKit、Kubernetes、Mesos、Rancher等。由于微服务架构天生具有集群的特性,采用这些框架能够极大的简化服务部署和运维的工作量。
小结
成功的实施微服务架构需要设计的不仅仅是架构本身,还有围绕整个服务集群的所有基础设施和团队的自主性文化。从实践的角度上说,本文所提到的这些方面也远不是微服务所需要考虑的全部。还有很多没有提到的实践,同样是值得采纳的,只是它们相对而言并非那么要紧。比如灰度发布,在微服务体系中也具有很大的运用空间。
每一个精心设计的企业级架构背后,都蕴含了相当的复杂性,微服务亦是如此。优秀的架构并不能让软件的复杂度凭空消失,而是通过更加合理的拆分和约束,使得软件结构更加容易匹配业务、适应变化,从而在规模化的同时保持高度的响应力。架构不是银弹,离开了必要的实践前提,空谈微服务,犹如东施效颦,期望拆了服务就能为企业带来受益,无疑于空中楼阁的笑话而已。
本文作者:网友 来源:运维派
CIO之家 www.ciozj.com 微信公众号:imciow