膜拜!来看大牛是如何设计微服务系统的,学会直接拥有架构师思维
前言
毫无疑问,如何设计微服务系统是本书所要讨论的核心话题。本节我们将从服务拆分、服务测试、服务注册、服务发现、负载均衡、服务部署、服务发布等方面来展开讨论。
服务拆分
服务拆分首先要关注的是服务的颗粒度。
通过DDD ( 领域驱动设计)的指导,我们可以将某个领域的功能进行聚合成为-一个服务。这个服务只负责某一个方面的功能。
DDD其实没有太多新鲜的内容,它更多的是可以看作是面向对象思潮的回归和升华。在一个“万事万物皆对象”的世界里,我们需要重新展开思考。例如,哪些对象是对我们的系统有用的,哪些是对我们拟建系统没有用处的,我们应该如何保证选取的模型对象恰好够用。
对象并不是独立存在的,它们之间会存在着千丝万缕的联系。而正是这种联系构成了系统的复杂性。一个具体的体现就是,当我们修改了一处变更时,结果会引发一系列的连锁反应。虽然对象的封装机制可以帮我们解决一部分问题, 但那只是有限的一部分。我们应该在一个更高点的层次上来思考,如何通过保留对象之间有用的关系去除无用的关系,并且限定变更影响的范围,来降低系统的复杂度。
在DDD及传统面向对象的观点中,- -个开发团队首先要关注的内容不是技术问题,而是业务问题,众多的框架和平台产品也在宣称把开发人员解放出来,让他们有更多的精力去关注业务。但是,当我们真正去看时,才会发现开发人员大多还是沉溺于技术中,花费在对业务的理解上的时间真的是太少太少。其实要解决这个问题,就要先看清楚我们提炼出来的模型,在整个架构和整个开发过程中所处的位置和地位。我们经常听到两个词,一个是MDD (模型驱动设计),另一个是MDA (模型驱动架构)。如果DDD特别关注的是“M”( 及其实现),那么,这个“M'应该如何与架构和开发过程相融合呢?我们经常会看到的一种现象是, 我们辛苦提取出来的领域模型被肢解后,分散到系统的各个角落。这真是一-件可怕的事情,因为一旦形成了“人脑拼图”,就很难再有一个人将它们一一复原。
有了DDD作为指导后,就要建设开发和维护这个服务的团队。这是一-支包含 了产品、测试、开发、运维等各方人才的全能型特性团队。团队规模正好能够应付这个服务的开发。如果说,这个团队独立运营这个服务有难度,则说明这个服务太大,需要继续拆分。
服务测试
服务测试需要把握以下几个最佳实践。
1.积极发布,及时得到反馈
开发实践中,我们推崇持续集成和持续发布。持续集成和持续发布的成功实践,有利于形成“需求→开发- 集成→测试-→部署”的可持续的反馈闭环,从而使需求分析、产品的用户体验和交互设计、开发、测试、运维等角色密切协作,减少了资源的浪费。
一些互联网的产品,甚至打出了“永远Bate版本”的口号,即产品在不等完全定型,就直接上线交付给了用户使用,通过用户的反馈来持续对产品进行完善。特别是一些开源的、社区驱动的产品,由于其功能需求往往来自真正的用户、社区用户及开发者,这些用户对于产品的建议,往往会被项目组所采纳,从而纳人技术。比较有代表性的例子是Linux和GitHub。
2.增大自动化测试的比例
最大化自动测试的比例有利于减少企业的成本,同时也有利于测试效率的提升。
Google刻意保持测试人员的最少化,以此保障测试力量的最优化。最少化测试人员还能迫使开发人员在软件的整个生命期间都参与到测试中,尤其是在项目的早期阶段,测试基础架构容易建立的时候。
如果测试能够自动化进行而不需要人类智慧判断,那就应该以自动化的方式实现。当然有些手工测试仍然是无可避免的,如涉及用户体验、保留的数据是否包含隐私等。还有一些是探索性的测试,往往也依赖于手工测试。
3.合理安排测试的介入时机
测试工作应该要及早介入,一般认为,测试应该在项目立项时介人,并伴随整个项目的生命周期。在需求分析出来以后,测试不只是对程序的测试,文档测试也是同样重要的。需求分析评审的时候,测试人员应该积极参与,因为所有的测试计划和测试用例都会以客户需求为准绳。需求不但是开发的工作依据,同时也是测试的工作依据。
服务注册
微服务架构的特点就是服务的数量众多,这些服务需要- - 个统-的服务注册平台来进行服务的管理。每个微服务实例在启动后,会将自己的实例信息告知给服务注册表或服务注册中心。服务的调用方若想获取到可用服务实例的列表,也是需要从服务注册表中去获取相关信息的。
当服务实例失效以后,那么服务实例的信息就要从服务注册表中移除,这个过程称为服务注销。
当服务实例启动后,会将自己的位置信息提交到服务注册表( Service Registry)中。服务注册表就是用于维护所有可用的服务实例的地方。服务注册表一方面要接收微服务实例的接人,另一方面,当服务实例不可用时,也要及时将服务实例从服务注册表中清除。图1-4展示了服务注册表与服务实例的关系。
为了保证可用性,服务注册表经常被配置为高可用而且需要与服务实例进行一定频率的通信,从而能够随时感知到服务实例的状态,及时来更新可用实例的列表。
客户端在从服务注册表中获取到网络地址后,经常会缓存起来,以提高访问性能。然而,这些信息最终会过时,客户端也就无法发现服务实例了。因此,服务注册表会包含若干服务端,使用复制协议来保持一致性。
Netflix Eureka提供了服务注册表的功能,而且提供了REST API来方便进行服务的注册。服务实例可以使用POST请求来注册实例的信息,并可以按照一定的时间间隔来使用PUT请求刷新注册信息。注册信息也能通过DELETE请求来进行主动移除。Netlix Eureka也设置了实例超时机制,这样当一段时间服务实例没有响应时,就会将该服务实例移除。客户端能够使用GET请求来检索已注册的服务实例。
Netlix通过在每个Amazon EC2可用区域中运行-一个或多个Netflix Eureka服务器来帮助其实现高可用性。每个Netfix Eureka服务器都运行在具有弹性( Elastic ) IP地址的EC2实例上。DNSTEXT记录用于存储Netflix Eureka集群配置,后者包括可用域和Netflix Eureka服务器的网络地址列表。当Netfix Eureka服务器启动时,它将查询DNS以检索Netflix Eureka集群集配置,确定同伴位置,并为自己分配一个未使用的弹性( Elastic ) IP地址。
Netlix Eureka客户端(包括服务和服务客户端),是查询DNS去发现Netflix Eureka服务的网络地址。客户端首选同一域内的Netflix Eureka服务。如果没有可用服务,客户端会使用其他可用域中的Netflix Eureka服务。
其他的开源实现,包括ZooKeeper、Consul、etcd 等都提供了服务注册表的功能。
要想访问微服务,服务实例必须要先在注册表中进行注册。服务注册主要有两种不同的方法:
一种是服务实例自己注册,也叫自注册模式( Self Registration) ;另一种是采用管理服务实例注册的其他系统组件,即第三方注册模式( 3rd Party Registration)。
服务发现
微服务实例要想让其他的服务调用方感知到,就需要服务发现机制。通过服务发现,调用方可
以及时拿到可用服务实例的列表。
在微服务架构中,对于服务发现的需求是这样的。
微服务的部署,往往利用虚拟机的主机或容器技术,所分配的主机位置往往是虚拟的。
●微服务的服务实例的网络位置往往是动态分配的。
微服务要满足容错和扩展等需求,因此服务实例会经常动态改变,这意味着服务的位置也会动态变更。
●同一个服务往往会配置多个实例,需要服务发现机制来决定使用其中的哪个实例。
因此,客户端代码需要使用更加复杂的服务发现机制。其原理如下。
*当服务实例启动后,将自己的位置信息提交到服务注册表中。服务注册表维护着所有可用的服务实例的列表。
*客户端从服务注册表进行查询,来获取可用的服务实例。
*在选取可用的服务实例的过程中,客户端自行使用负载均衡算法从多个服务实例中选择出一个,然后发出请求。
图1-5显示了服务发现的原理架构图。
服务注册表中的实例也是动态变化的。当有新的实例启动时,实例会将实例信息注册到服务注册表中;当实例下线或不可用时,服务注册表也能及时感知到,并将不可用实例及时从服务注册表中清除。服务注册表可以采取类似于心跳等机制来实现对服务实例的感知。
很多技术框架提供了这种客户端发现模式。Netflix提供了完整的服务注册及服务发现的实现方式。NetflixEureka提供了服务注册表的功能,为服务实例注册管理和查询可用实例提供了RESTAPI接口。Netflix Ribbon的主要功能是提供客户端的软件负载均衡算法,将Netfix的中间层服务连接在- -起。Netflix Ribbon客户端组件提供- - 系列完善的配置项,如连接超时、重试等。简单地说,就是在服务注册表所列出的实例,Netflix Ribbon 会自动地帮助你基于某种规则(如简单轮询,随即连接等)去连接这些实例。Netflix 也提供了非常简便的方式来让我们使用Netflix Ribbon实现自定义的负载均衡算法。,
服务部署与发布
当单体架构被划分成微服务后,随着微服务的数量增多,部署这么多的微服务毫无疑问将会面临比单体架构更复杂的问题。
●运维负担。对传统的单体架构系统来说,产品通常只有-一个发布包,升级、部署系统往往只需要部署这个发布包即可。现在面临着这么多的微服务,显然运维的负担要比之前更重了。对于运维工程师来说,部署的服务呈指数上升,传统的手工部署方式往往已经不能适应日益增长的服务运维需求。
服务间的依赖。在一个微服务结构中,更容易遇到的错误是来自依赖的问题。在微服务架构系统中,某些业务功能需要几个微服务协同才能完成,这些服务之间难免存在一-定的依赖关 系。
特别是以某种方式更新某个服务的API时,同时也会影响其他服务,造成某些服务的不可用。
所以在更新服务时,需要先确定哪些服务需要更新,而后评估更新对其他服务产生的影响。
●更多的监控。 每个微服务往往需要设置单独的监控,这意味着更多的监控。而且每个微服务可能使用不同的技术或语言,依靠不同的机器或容器,使用其特有的版本控制,这也大大增加了监控的复杂性。
●更频繁的发布。每个微服务都需要单独部署,这就意味着需要更多的服务发布。微服务的颗粒度相对较小,修改和发布也较为容易,所以发布也会相对更加频繁。这是微服务的优点,但同时也是实施微服务所要解决的难题。
●更复杂的测试。微服务化之后,服务可以独立开发和测试,团队或成员之间可以并行快跑,这极大地提高了系统的研发效率,但也给测试工作带来了挑战。除了验证各个独立微服务外,我们还需要考虑通过具有分布式特性的微服务架构检查全部关键性事务的执行路径。由于微服务的目标之一在于实现快速变更,因此我们必须更加关注服务的依赖性,以及性能、可访问性、可靠性及弹性等非功能性要求。
所以,微服务更倾向于使用具有相互之间隔离的主机或虚拟机来实现服务的部署。这样,服务就能够各自进行安装、部署、测试、发布、升级,而这些动作对于其他服务来说是不可知的。如果服务之间有依赖关系,则可以通过逐个替换服务实例的方式来实现服务零停用。
一种比较好的方式是把微服务打包成镜像,这样就保证了不同主机之间能够使用相同的镜像。
同时,由于镜像中包含了服务的配置文件和环境,这样就可以尽可能地避免主机环境对软件部署产生的影响。
考虑使用Docker容器,这将会使构建、发布、启动微服务变得十分快捷。Docker 提供了一种方法来运行在容器中安全隔离的应用程序。应用程序与其所有的依赖和库将被一起打包,这样就确保了应用程序总是可以使用它在构建映像中所期望的环境来运行,测试和部署比以往任何时候都更简单,因为这种构建将是完全可移植的,并且可以按照任何环境中的设计运行。由于容器是轻量级的,运行的时候并没有虚拟机管理程序的额外负载,这样就可以运行许多应用程序,这些应用程序都依赖于单个内核上的不同库和环境,每个应用程序都不会互相干扰。将应用程序从虚拟机或物理机转移到容器实例,可以获得更多的硬件资源。