微服务与 DevOps实践:技术架构与组织架构
王东,曾任融数数据北京研发中心 CTO,负责微服务、DevOps 以及大数据平台的研发和管理工作。曾供职于 IBM、普元、Amazon、OneAPM 等国内外知名公司。拥有 15 年以上的 JavaEE 编程和架构设计经验,精通 DevOps 和微服务,曾领导设计和开发普元 ESB 产品。熟悉支付相关的业务流程以及各个银行和支付机构的业务处理模式,熟悉应用与支付领域的大规模分布式系统设计和开发方法,熟悉电子商务行业的业务模型。专注于客户行为分析、营销、产品和服务、客户关系、线上销售、物流配送、渠道整合、供应链集成、售后服务、预测和推荐等各个电子商务主要环节的分析和设计,以及 IT 系统的规划和实施。精通 DDD、Scrum 等软方开发和设计方法论。
9.1 概述
首先,我们来看看微服务的定义:微服务是一个界限明确、高度封装、松耦合、可以独立部署和独立扩展的服务应用组件,如图 9.1 所示。微服务架构基于 SOA 和领域驱动设计(DDD)构建,其主要目的包含以下三个方面:开发的敏捷性、部署的便利性及明确的可扩展性。
图 9.1
其次,微服务和传统的 SOA 有什么异同:
从设计原则上来讲,微服务架构遵循 SOA 的设计原则。
小的、可重用的服务并不一定是微服务,微服务架构强调敏捷、独立开发、独立部署、独立扩展,重用在某种程度上会影响敏捷性。
微服务架构为了实现其敏捷特性,在 SOA 架构约束的基础之上又添加了新的约束,微服务之间不能互相依赖,因此要求微服务能够独立部署、独立扩展,微服务之间的依赖越少越好。
微服务中的一个服务只实现一个独立的特性。
对于微服务而言,尽量不要为外部应用发布代码级 API,可以通过服务调用或者事件解决依赖问题。
微服务中的服务之间最好通过异步事件交互。
微服务中的每个服务都拥有自己独立的数据。
另外,微服务架构的优点有很多:
敏捷性:微服务不仅可以提高开发的敏捷性,还可以加速持续部署(CD),从而使开发团队能够尽快部署新的功能。
减少风险:由于每个微服务都是比较小巧并且独立部署的,因此可以减少每次部署的风险。
适合分布式开发:微服务之间依赖程度低,因此可以更加灵活地独立开发。
技术灵活性:微服务之间的耦合程度低,因此技术团队可以根据不同的特点选择最合适的技术栈,例如利用不同的编程语言解决不同的领域问题。
可扩展性:一个应用有许多微服务组成,每个微服务可以根据需要独立扩展,而无须对整个应用做整体扩展。
鉴于以上优点,我们来看一下微服务的本质。微服务在本质上是一种服务实现模式,微服务可以实现一个应用中独立的业务功能,而不是整个应用或者模块,因此,微服务其实是将单体应用的复杂性从程序内部转移到了服务组件之间,这就对微服务抽象的粒度有一个比较高的权衡要求。对于微服务的粒度,大家讨论的最多,这也是实践过程中经常遇到的令人纠结的问题。实际上,如果抽象粒度太细,就需要大量的服务编排来满足业务场景,而大量使用服务编排就可能会导致微服务退回到 SOA 模式(服务编排是类似 BPM 或者 ESB 的系统);而如果服务粒度太粗,则不利于开发的敏捷性和部署的便利性。
微服务的主要目的是实现敏捷,微服务架构的主要构建方式是采用领域驱动设计,而领域驱动设计主要包括如下五个概念:
Bounded Context
Context Map
Event Sourcing
CQRS
BASE
微服务的粒度并没有一个明确的界限,也就是说“尺寸”是相对的。通常,微服务的粒度和对敏捷性的要求密切相关,往往粒度越细其敏捷度也越高,但是并不是所有的应用对敏捷的要求都那么高,也就是说在微服务的设计和实现过程中,对于粒度的大小可以做适当的调整。对于想要采用微服务架构的团队,首先要考虑如下几个前提条件:
现有软件架构是否已经服务化或者按照系统功能做了模块化切分。
团队的敏捷成熟度如何,是否有足够的 DevOps 经验。
开发团队是否有足够的架构设计能力来适应采用微服务所带来的设计方法、模式以及技术架构上的巨大差异。
DBA 团队是否有能力和意愿制定新的数据管理模式——将数据管理的方式由原来的集中管理转变为去中心化管理。
运维团队是否有足够的能力为微服务提供全新的环境管理工具、部署工具和监控工具。
综上所述,我们建议微服务的演进线路如图 9.2 所示。
图 9.2
从单体应用或者传统分层架构的应用向服务化过渡,通过封装和组合等方式提供对外发布接口的能力,从而提升应用的可访问性。
通过重构将 Domain-Level(领域层面)的功能模块转变为可以独立部署的服务,从而提升整个应用的敏捷性。我们将这种可独立部署的服务称为 MiniService,其粒度比微服务粗,因而抽象难度比较低,但是也能在一定程度上获得微服务所带来的敏捷性提升的好处,但是对 DevOps 的基础设施等要求没有微服务高,因此建议没有微服务经验的团队可以从 MiniService 开始尝试。
通过 Feature-Level(特性层面)的抽象,根据单一职责原则将 MiniService 拆分成微服务,从而获得更高的扩展性和敏捷性。
9.2 融数数据微服务的架构选型
我们在构建微服务体系的过程中,经历了技术选型、技术验证、引入开源实现及完全自研等一系列的过程,其中也走过不少弯路,现在回想起来,感触良多,在这里跟大家分享一下,希望能够有所帮助。对于技术选型,我们不希望重复发明轮子,也不希望完全受制于开源的实现,所以在技术选型时遵循如下原则:
图 9.3
考察社区热度、架构成熟度、学习曲线、可维护性,如图 9.3 所示。
尽量照顾现有服务的开发方式和框架的使用习惯,不对目前的研发团队造成太大的冲击。能够给程序员提供何种能力?我们的期望是:
方便开发
方便迁移
多协议支持
多语言支持
方便监控
方便运维
我们比较了可能用到的一些开源服务框架,并对它们的功能点进行了评估,如图 9.4 所示。
图 9.4
由于之前团队采用 RESTEasy + Spring Boot 的方式实现服务,不希望对现有的系统和产品产生太大的影响,因此决定服务框架首先需要支持 REST 服务,又由于 Netflix 提供了比较全面的解决方案,并且 Spring Cloud 在遵循 cloud-native 原则的基础上对 Netflix 进行了比较友好的封装,因此初步的结论是可以基于 Spring Cloud 进行二次开发,封装我们自己的微服务框架。
经过半年的实践,我们发现 Netflix 技术栈的思想不错,但是很多实现并不是特别完美,例如:
Zuul 提供的 Edge Service 及 API 网关是完全基于 HTTP 协议的过滤器实现的,基于 HTTP 协议的同步调用方式会带来性能的损失,并且缺乏长连接的推送及多路复用的能力。
Zuul 不能满足 RPC 调用的需求,而在大规模团队协作的过程中,契约优先的开发方式优于代码优先的开发方式,因此对于应用内部交互来讲,RPC 框架从管理和协作的角度要优于 REST 服务。
基于 Eureka 的服务注册依然是中心化治理的方式,与传统基于 ZooKeeper 或者 etctd/consul 的治理方式一样,中心化的治理会给服务的部署带来非常大的阻碍,需要对不同的环境配置不同的中心注册服务器,服务的版本管理及服务注册信息的同步也会带来问题,更加糟糕的是,在某些服务不正常的情况下,客户端需要进行大量的判断,防止出现治理风暴等问题。
因此,我们后来果断转换思路,对之前的微服务框架进行了彻底的改造:
1、基于对 gRPC 的封装,构建 Service Provider 的全新实现,替代 REST 服务。
2、通过 Proxy 方式,实现 gRPC 与 REST 服务的相互转换,保持系统兼容性。
3、进行去中心化治理,通过 Proxy 来实现端点治理,将客户端治理变为服务端治理。
4、通过扩展并集成 Zipkin 实现服务链路监控和运行时拓扑收集。
5、构建 Endpoint inventory 服务,以 semantic versioning 的方式管理服务版本。
6、将 Proxy 作为服务部署的执行端点,通过 VIP 绑定,透明化客户端寻址工作。利用 Proxy 提供轻量级的负载均衡、流量控制及灰度发布的功能。
9.3 设计思想
融数数据微服务架构的整体设计思想如图 9.5 所示。
图 9.5
该设计思想的具体说明如下。
面向运维的设计,配合 DevOps 平台,提供服务生命周期管理及易于被监控的能力。
面向开发者,合理封装。开发者无须了解 gRPC 的具体实现。
基于 IDL 的契约优先的开发方式。
提供完善的测试框架和 Mock 工具。
提供完善的易于监控的能力。
9.4 总体架构
融数数据微服务总体架构(Graeae)如图 9.6 所示。
图 9.6
9.4.1 总体架构的特性
融数数据微服务总体架构有如下特性。
Graeae 架构与协议无关。该架构可以基于 Netty4、线程模型及 buffer pool 进行调整,以减少 GC 压力并通过线程切换提升性能协议。
遵循 protocol buffer 协议,可以做到通用性强、序列化性能好、压缩效率高。
语言中立,目前整个架构支持 Java、Python 和 Go 三种语言的开发。
引入了熔断器机制、流量控制、服务治理。
基于 Proxy 和 PaaS 平台进行分布式治理监控。
使用集成 Zipkin 的调用链监控,以及基于 Pinpoint 的 APM 监控。
对于该架构而言,直接调用的性能好于反射调用,且使用 Netty4 线程模型优化。
9.4.2 具体实现
服务提供者 Endpoint 基于责任链模式(如图 9.7 所示)对 gRPC 进行封装,对屏蔽了 gRPC 框架的事件驱动采用同步调用方式,方便业务迁移。
图 9.7
Endpoint 封装了脚手架工具,提供基于 ProtoBuf 的 IDL 接口定义语言,使用契约优先的方式定义服务,并可以自动生产服务端和客户端的代码框架。
代码优先意味着实现简单,能够快速执行。问题也很明显,可能和某个具体语言绑定,面对多语言环境,其打通成本较高。
契约优先的中立性提供了一个中间桥梁,让面向多语言成为了可能,基于契约的元信息为后续治理和演进提供了入口点。缺点是需要引入契约语言的学习,并与多语言进行适配。
Endpoint 采用生命周期自管理的方式,提供容器化的生命周期管理 API 和相应的 SPI,方便扩展及与 DevOps 工具结合。
外部管理(如 Tomcat)让用户不用关注自身的起始、消亡,但带来的不足是对生命周期的管理相对减弱、部署的依赖管理扩散。
进程自治可以加强其对自身生命周期的管理,高内聚,不将依赖扩散。在一定程度上能够带来部署的便利及不同部署环境的适应性(如云环境)。为优雅关闭提供切入点,进一步增强对系统的可控性。
从对环境适应性和对生命周期的管理能力考虑,进程自治有着不可忽略的优势。Endpoint 将配置与代码分离,提供多种方式的配置覆盖能力,使得改变配置无须重新
部署。
配置和代码一起进行的优点是使开发变得简单,但不足也很明显,即面对不同的环境需要部署多套代码,复杂度增加。
配置和代码分离后的优点是真正做到了只部署一套代码。配置信息按环境独立配置,不受环境制约,可随时调整。
集成 Spring Boot,提供自定义注解方式,能够快速启动服务,方便开发。
//服务提供方
public class SmsTemplateApplication {
public static void main(String[] args) {
new SpringApplicationBuilder().sources(SmsTemplateApplication.class)
.web(false).showBanner(false).run(args);
}
}
//服务实现
public class SmsTempletServiceImpl implements SmsTemplateService { @Autowired
private SmsTempletDao smsTempletDao;
@Transactional
public DeleteReply deleteById(IdRequest req) { return SmsTemplateReply.newBuilder().build(); }
}
利用 Proxy 部署和设置服务治理端点,进行分层治理,如图 9.8 和图 9.9 所示。
图 9.8
图 9.9
用 Proxy 配合部署方式来兼容 semantic versioning 的版本管理,版本格式有主版本号、次版本号、修订号、版本号递增同时需要备注,规则如下:
主版本号:当做了不兼容的 API 修改时,需要递增主版本号。
次版本号:当做了向下兼容的功能性新增时,需要递增次版本号。修订号:当做了向下兼容的问题修正时,需要递增修订号。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。
9.5 对微服务的支撑
融数数据 DevOps 体系主要解决的问题是有效地识别和管理元数据,以便高效、自动化、高质量地将系统动态组装并运行起来,该体系架构如图 9.10 所示。
下面结合图 10.10 对 DevOps 体系做几点说明。
抽象了部署的最小单元——包。
每个微服务都是由包及其配置组成的,前面提到,服务框架做了代码和配置分离,因此这里可以将包和包的配置分离,提供运维便利性。
逻辑环境包括了微服务、微服务配置及运行微服务所依赖的 Host 或者 Host Group。
版本化逻辑环境,方便回滚。
该体系中的服务组件=(可运行代码 + 配置)&(依赖 + 配置)&(基础设施 + 配置),如图 9.11 所示。
图 9.10
图 9.11
从部署的角度讲,无论包还是包的集合(往往是组成一个独立业务域的独立功能)都需要依靠版本才能进行整体部署,可以部署在不同的逻辑环境上,也可以部署在同一个逻辑环境上,如图 9.12 和图 9.13 所示。
9.6 DevOps 平台总体架构
图 9.14
9.7 融数数据面向微服务的研发团队介绍
要想成功地实施微服务架构,仅仅有服务框架、开发平台及 DevOps 平台是不够的,组织和文化也需要适应微服务的需求。根据康威定律,架构由组织决定,因此需要对团队的文化及组织划分结构进行调整。
融数根据 two-pizza team 的原则,按照业务来划分研发团队,建立全栈小团队,从而提高沟通效率、降低沟通成本,具体的团队策略如图 9.15 所示。
图 9.15
将团队切分后,我们按照业务线对组织进行架构规划,以便技术团队能够专注于解决对应业务问题(业务驱动,或者说业务优先)。这时,团队内部的设计决策将在团队内部消化,因为团队的规模已经是 7+/-2 人的量级,因此一般情况下不会对于团队内成员来说过于复杂的工作。但团队增多后,团队的协调将是一个问题,因此微服务从技术上帮助团队将负责的系统解耦,而计划流程会帮助团队在工作安排上找到合理的步骤。那么,虽然当一个大的业务被分解到各个小团队时,还是会有跨团队的设计工作,但是以上两点是要严格执行的。技术团队和业务团队的合作并非经由上层协调,双方的主要沟通都是团队之间直接进行水平沟通,也就是说,在底层的团队之间,需求、问题和日常交流都直接由业务团队反馈给技术团队的经理,而解决问题时容许业务团队直接接触开发人员,如图 9.16 所示。
我们在团队中强调以结果为导向的工作方式,如图 9.17 所示。
团队成员要有主人翁意识。团队的利益就是个人的利益,团队的成功才是个人的成功,团队成员之间要相互帮助和补位,不允许存在“事不关己,高高挂起”的情况出现。
在团队中,成员可以发表意见,也可以抱怨,但是不能只停留在语言层面,大家需要行动起来,找到解决问题的方法;我们希望每个开发人员都参与架构设计,而不是仅由架构师决定;我们还强调不要过度设计,鼓励通过抽象和简化解决问题,当然也鼓励引入新技术,但前提是能够有足够的掌控力而且不影响系统稳定。
软件工程师负责需求调研、设计、开发、测试、部署、维护、监控、功能升级等一系列的工作,也就是说软件工程师负责应用或者服务的全生命周期的所有工作。运维是团队成员的第一要务。在强大的自动化运维工具的支撑下,软件工程师必须负责服务或者应用的 SLA。
在团队中引入 oncall 机制,强调保障处理及时性,并引入卓越运维的思想,用数据统计每个团队的线上故障、解决及时性,并逐步要求故障数量递减,通过强大的数据支持保证团队解决问题的 SLA,并逐步改善软件质量。
9.8 总结
DevOps 的推行是按业务来组织团队,团队包含设计、开发、测试、运维等人员,这样一方面可以有效减少服务内部修改所产生的内耗;另一方面,团队边界可以变得更为清晰。DevOps 实际是一种文化上的变迁,打破了传统开发与运维之间的壁垒,帮助组织形成从开发、测试到部署、运维这样一个全功能化的高效能团队。
本文选自《架构宝典》
《架构宝典》
出品:中生代技术社区
往期推荐
技术琐话
以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。