1. 微服务总述
随着领域驱动设计(Domain-Driven Design简称DDD)、持续交付、按需虚拟化、基础设施自动化、小型自治团队、大型集群系统这些实践的流行,微服务也应运而生。
微服务并不是被发明出来的,而是从现实世界中总结出来的一种趋势或模式。
随着实践的不断加强,在微服务之后又出现了中台,Serverless等概念及应用,而谁会流行于未来的技术架构中,大家拭目以待。
1.1 什么是微服务
微服务就是一些协同工作的小而自治的服务。
小且专注于做好一件事
单一职责原则:即把因相同原因而变化的东西聚合到一起,而把因不同原因而变化的东西分离开;这一原则强调了内聚性这一概念。某个微服务应该具有内聚性,它只专注于某一个边界之内的问题。
代码库多少才算小呢?只要足够小即可,但不要过小。如果你不再感觉你的代码库过大,那可能就已经足够小了。另外一些专家提出的标准:只要能够在两周内重写了就够小了。
与代码大小需要一同考虑的因素:服务越小,微服务架构的优缺点就越明显。服务越小,独立性带来的好处就越多,但管理大量的服务也会越复杂。所以本书后面会详细讨论这一复杂性,解决了复杂性的问题就可以尽情地使用较小的服务了。
自治性
一个微服务就是一个独立的实体,可以是部署在PaaS上,也可以作为操作系统进程存在。
服务会暴露出API,然后各个服务之间通过网络调用API进行通信。这使得各个服务之间具有隔离性。
正确地建模服务和API,即考虑微服务对外提供哪些服务,可以更好解耦。
1.2 主要好处
技术异构性
在一个由多个服务相互协作的系统中,可以在不同的服务中使用最合适该服务的技术。例如需要性能性能的服务可以使用性能更好的技术栈重构。尝试使用一种适合所有场景的标准化技术,往往导致所有场景都无法得到很好的支持。
这一优点也体现在可以帮助我们更快地采用新技术。可以选择一个风险最小的服务来采用新技术,即使出现问题也容易处理。快速采用新技术的能力对很多组织非常有价值。
当然同时使用多种技术,也需要付出一些代价(注:从人员招聘,以及对某种技术的沉淀上往往有所影响)。
弹性(故障影响)
这里所说的弹性,是指某一个部分出现故障是否会影响到其它部分系统的正常运行。就像舱壁一样,隔绝了故障的蔓延。服务边界就是一个很显然的舱壁。微服务系统本身就能够很好地处理服务不可用和功能降级的问题。
但使用了分布式系统,网络就会是一个问题,这是需要谨慎对待的。
扩展(扩容)
使用较小的多个服务,可以只针有需要扩展(扩容)的服务进行扩展,对于那些不需要扩展的服务则可以运行在更小的,性能稍差的硬件上。而庞大的单块服务,即使只有一小部分存在性能问题,也需要对整体进行扩展。
简化部署
在微服务架构中,各个服务的部署是独立的。这样可以更快地对特定服务进行代码升级和部署,影响更小。如果有问题也只影响这一个服务,并且很容易快速回滚,这使得快速迭代功能功能上线成为可能。反观在大型几百万行的单块应用程序中,即使只是修改一行代码,也要重新部署整个应用。这种部署影响很大、风险很高,因此实际中会降低部署频率,这意味着两次部署之间的差异很大,这样出错的风险就更大了。
与组织结构相匹配
小型代码库上工作的小团队更加高效。微服务架构可以很好地将架构与组织结构相匹配,避免过大的代码库,从而获得理想的团队大小及生产力。服务所有权也可以在团队之间迁移,避免异地团队的出现。
可结合性
基于微服务架构可以重用已有的功能。目前的应用种类包括Web、原生应用、移动端、穿戴设备等,这些设备上的功能都略有差异,可以通过组合微服务提供的功能来满足之些设备上的产品需求。
对可替代性的优化
使用微服务架构的团队可以在需要时轻易地重写服务,或者删除不再使用的服务。在这之前碰到一个遗留的大型系统,而此系统对公司业务又至关重要时,任何人都会因为工作量大、风险高而拒绝将其升级为目前团队所用的主流技术。
1.3 面向服务的架构(SOA)
SOA(Service-Oriented Architecture,面向服务的架构)是一种设计方法,其中包含多个服务,而服务之间通过配合最终会提供一系列功能。服务一般是操作系统进程,服务之间通过网络调用通信。
微服务的思想传承与SOA,但SOA由来已久却没有得到重用,其问题出在它并没有讨论一些在实践中非常需要的问题,例如通信协议(SOAP)的选择,第三方中间件的选择,服务划分粒度等。
而微服务是在我们在现实世界中,对于项目的系统和架构深入理解之后按SOA的思想进行实施形成的一种模式,所以可以说微服务是SOA的特定方法。
其它关于SOA的讨论可以参考知乎上的讨论。
1.4 其他分解技术
本章节讨论除了微服务架构外其它的分析技术,分析其优缺点以及适用场景。
共享库
基本上所有的语言都支持将整个代码库分解成多个库,不同团队和服务之间通过库的形式来共享功能,以达到重用的目的。
缺点:1. 无法选择异构的技术。一般来说共享库只能用于同一种语言或者同一个平台;2. 失去独立地对系统某一部分进行扩展的能力;3. 一般来说在库进行更新时都需要部署所有用到这个库的服务;4. 重用的代码库装饰是一个耦合点。
尽管有这些缺点,但仍然鼓励不同服务之间大量使用第三方库来重用公共代码,我们会在后续章节讨论这个问题。
模块
有些语言除了提供简单的库之外还提供了自己的模块分解技术。它们允许对模块的生命周期进行管理,使得可以将模块部署到运行的进程中,达到不停机更新的目的。
例如Java的OSGI(Open Source Gateway Initiative)以及Java9发布的Jigsaw,Erlang语言自带的模块等,都属于这一类。
使用模块与使用共享库的缺点类似,大大限制采用新技术和独立对服务进行扩展的能力;另外模块化带来的复杂性使得对团队本身的技术能力具有要求;模块也很容易随着时间和功能迭代而耦合了许多不应该耦合的代码。
1.5 没有银弹
微服务不是银弹,也不是免费的午餐。想要得到微服务的好处,需要在部署、测试、监控等方面做相当多的工作,还需要考虑扩展系统保证弹性,处理类似分布式事务或者 CAP相关的问题。
所以是否采用微服务取决于很多因素,本书后续内容将试图给出一些指导。
2. 演化式架构师
架构师承担了驱动系统演化的职责,而引入微服务之后的一个主要挑战就是架构师职责的变化。架构师需要做很多的决定,例如使用多少种不同的技术,不同团队是否需要不同的编程规范,应该合并多个服务还是拆分某个服务等等。
2.1 不准确的比较
架构师的一个重要职责是确保团队有共同的技术愿景,以帮助我们的向客户交付他们想要的系统。
架构师一词来源于建筑师(architect),但两者有着本质的区别。建筑师的工作是做好详细的计划,然后让别人去理解和执行。如果IT架构师也按这个思路通过大量的图表和文档创建一个完美的方案,而忽略了很多基础性的未知因素,则会导致非常糟糕的实践。
事实上,架构师要创造的东西从设计上来说就是要足够灵活,有很好的适应性,并且能够根据用户的需求进行演化。
2.2 架构师的演化视角
在软件中我们会面临大量的需求变更,使用的工具和技术也具有多样性。因此架构师必须改变那种从一开始就要设计出完美产品的想法,而应该设计 出一个合理的框架,在这个框架下慢慢演化成正确的系统,并且一旦我们学到了更多知识,应该可以很容易地应用到系统中。
城市规划师这个行业可以较好的类比架构师。前者需要收集各种各样的信息,考虑一些未来的因素来优化城镇布局,使其更易于居民生活。他会对城市进行分区,例如居民区、工业区等等。
架构师也应该如此专注在大方向上,只有在很有限的情况下参与到非常具体的细节实现中来;需要保证系统不但能够满足当前的需求,还要能够应对将来的变化;同时还要保证在这个系统上工作的开发人员和使用者一样开心。
2.3 分区
作为架构师不应该过多地关注每一个区域之间发生的事情,而应该多关注区域之间的事情,即多考虑不同服务之间如何交互,或者说保证我们能够对整个系统的健康状态进行监控。
每一个服务区内部允许团队自己选择不同的技术栈或者数据存储技术(当然也要考虑公司的招聘以及技术沉淀问题),但服务之间的交互如使用HTTP暴露REST接口还是使用protocol buffers还是Java的RMI需要架构师花更多的时间来考虑与决策。
对架构师的要求(代码架构师):架构师需要花时间和团队一起工作,理想状态下应该和他们一起编码。这样才能更好地理解如何决定会对开发人员足够友好,并且对整个系统起到正面的影响。
2.4 一个原则性的方法
做系统设计方面的决定通常都是在做取舍,而基于要达成的目标去定义一些原则和实践对我们做决定有好处。
战略目标
战略目标一般只在公司或者部门层制定,例如开拓全球市场、让用户尽可能使用自助服务等。架构师一般不需要定义战略目标,但需要确保技术层面的选择能与之一致。
原则
为了与战略目标一致,会制定一些具体的规则,称之为原则。原则不是一成不变的。
例如如果公司的目标是在北美地区开拓市场,那么一个可能的原则就是系统能够方便地部署到对应的地区,存储数据符合此地区对数据存储位置的要求。
一般来说原则最好不超过10个,原则越多,重叠和冲突的可能性就越大。原则中不可变的因素称之为约束,但随着时间的推移,约束也可能改变。
实践
通过实践来指导和保证原则能够被正确的实施。实践一般是技术相关,而且任何开发人员都能理解的。例如代码规范、日志数据格式与获取方式、HTTP/REST作为标准集成风格等。
实践从原则出发,并巩固原则。但实践的变化要更频繁于原则。
将原则和实践相结合
拿HTTP/REST举例,一些人认为他是实践,但某些团队将其设置为原则。这是正常的,关键的是要有一些重要原则来指导系统的演化,同时也要有一些细节来指导如何实现这些原则。
在一个公司中的不同部门,具体的实践可能不一致,但需要映射到相同的原则上来。例如Java团队与.Net团队的实践肯定是两套,但背后的原则应该一致。
真实世界的例子
战略目标:
保证业务可伸缩性:为客户提供更多的客户,事务自助服务
支持进入新的市场:灵活的运营流程,新的产品和运营流程
支持现有市场中的创新:灵活的运营流程,新的产品和运营流程
架构原则:
减少惯性:所做决定要能够减少团队之间的依赖,从而可以更快地对软件进行修改,进而得到更快的反馈
消除偶发复杂度:比较激进地去除或者替换不必要的复杂流程,系统及集成 ,从而专注在本质复杂度上
一致的接口和数据流:消除重复数据,创建清晰的记录系统和一致的集成接口
没有银弹:现成的解决方案能够帮助我们更快地交付,但是也会引入惯性和偶发
设计和交付实践
标准REST/HTTP
封装遗留系统
消除集成数据库
合并并净化数据
发布集成模型
小规模独立服务
持续部署
对COTS/SAAS进行最小化定制
如上提到的一些项可能使用文档来支撑,也可以使用示例文档供人阅读、研究、运行,从而传递上面涉及的那些信息。
更好的方式是创建一些工具来保证我们所做事的正确性。
2.5 要求的标准
如何定义一个好服务该有的属性?
监控
能够清晰地描绘出跨服务系统的健康状态非常关键。建议所有的服务使用同样的方式报告健康状态以及监控相关的数据,并集中管理。
接口
选用少数几种明确的接口技术有助于新消费者的集成。这不紧紧是说接口的技术和协议,例如选用HTTP/REST,在URL中使用动词还是名词?你会如何处理分页?如何处理不同版本的APP?
架构安全性
即系统的脆弱程度,健壮性等。
必须保证每一个服务都能应对下游服务的错误请求。
可以使用连接池、断路器来处理当下游服务出现错误时的情况。
返回码也应该遵守对应的规则,因为这关系到错误处理、断路器后续的操作。
一般地,需要对以下几种请求做不同的处理,这样可以帮助系统及时失败,并且很容易追溯问题:1. 正常并被正确处理的请求;2. 错误请求、并且服务识别出它是错误的,但什么也没有做;3. 被访问的服务器宕机了,所以无法判断请求是否正常。
2.6 代码治理
通过提供范例和服务代码模板可以促进各个小组的开发人员采用标准做法。
范例
优秀的范例来源于真实项目,只有这样才能保证其中所体现的那些原则都是合理的。
裁剪服务代码模板
如果在开发一个新服务时,所有实现核心属性的代码都是现成的,那么所有开发者就更容易遵守大部分的指导原则。通过服务代码模板可以实现。
例如可以使用Dropwizard或者Karyon这类的JVM开源微容器做为服务模板。它集成了健康检查、HTTP服务、提供指标数据接口等功能,可以开箱即用。另外还可以根据需要加入第三方库,例如加入Hystrix以规范断路器的使用。
这一方法需要注意几点,需要有团队负责更新这一模板,这个团队必须是这些模板的使用者,这样才能真实反应需求;模板的共享和更新可以通过内部开源的方式来进行。
另外如果强制所有团队使用这一模板来生成服务,则必须保证这能简化开发者的工作,而不是增加复杂性。一方面需要保证只有必须的功能才可加入模板,防止臃肿。另一方面,可以在确实需要使用模板的情况下才使用模板,其它情况下开发人员可以自由选取,这样既可以快速构建标准服务,也不会抹杀技术人员创新的思想。
重用代码会带来高耦合的问题,需要架构师在这之间做权衡。
2.7 技术债务
技术债务的产生可以是一个紧急发布的特性,没有满足对应的原则约束;也可以是系统的目标会发生改变,并且与现有的实现不符。
对于某些组织来说,架构师应该能够提供一些温和的指导,然后让团队自行决定如何偿还这些技术债务。而其他的组织就需要更加结构化的方式,比如维护一个债务列表,并且定期回顾。
2.8 例外管理
正常情况下我们需要遵守原则和实践来指导构建系统,但有时候会针对某一规则破例一次,并记录下来。如果这样的例外出现多次,可以通过修改原则和实践将我们对这一例外的理解固化下来。
例如有一个实践论述是使用MySQL做数据存储,但是后来有足够的证明表明在海量存储的场景下应使用Cassandra,这时就可以对实践进行修改:“在大多数场景下使用MySQL做存储,如果是数据快速增长的场景,可以使用Cassandra。”
2.9 集中治理和领导
所谓治理,是通过评估干系人的需求、当前情况及下一步的可能性来确保企业目标的达成,通过排优先级和做决策来设定方向。对于已经达成一致的方向和目标进行监督。
架构师会承担技术治理这部分的职责。通过治理确保构建的系统符合当初制定的技术愿景。并且在需要的时候还应对愿景进行演化。
架构师会对很多事情负责:1.确保有一组可以指导开发的原则;2. 些原则要与组织的战略相符;3. 需要了解新技术;4. 需要知道在什么时候做怎样的取舍,让同事也理解这些决定和取舍,并执行下去;5. 还需要花时间和团队一起工作,甚至是编码,从而了解所做的决定对团队造成了怎样的影响。
如何在这些事务之外再执行治理的任务呢?可以通过成立治理小组来做这一部分工作。
架构师做为技术专家来领导治理小组,小组中有每一个交付团队的成员,包括一线人员。架构师负责确保该组织的正常运作,整个小组都要对治理负责。小组也应该负责跟踪和管理技术风险。
一般情况下架构师的意见应该能与小组做成的决定达成一致。在无法达成一致的情况下,需要分场景来决定是否尝试说服大家采用你的意见。这是一个很微妙的考虑,一般来说不必去控制团队,即使你清楚你的方案是对的。但在影响线上服务,引起大量用户反馈等严重场景下,这时就有必要出手拉一把了。
2.10 建设团队
对于一个架构师来说,执行愿景不仅仅等同于做技术决定,更重要的事情是帮助你的队友成长,帮助他们理解这个愿景,并保证他们可以积极地参与到愿景的实现和调整中来。
在单块系统中,人们为某些事情负责的机会非常有限,而在微服务架构中存在多个自治的代码库,每个代码库都有着自己独立的生命周期,这就给更多人提供了对单个服务负责的机会,而当这些人在单个服务上面得到足够锻炼之后,就可以给他们更多的责任,从而帮助他们逐步达成自己的职业目标,同时通过分担职责也可以防止某一个人的负担过重。
2.11 小结
总结一下架构师的职责:
愿景:确保在系统级有一个经过充分沟通的技术愿景,这个愿景应该可以帮助你满足客户和组织的需求。
同理心:理解你所做的决定对客户和同事带来的影响。
合作:和尽量多的同事进行沟通,从而更好地对愿景进行定义、修订及执行。
适应性:确保在你的客户和组织需要的时候调整技术愿景。
自治性:在标准化和团队自治之间寻找一个正确的平衡点。
治理:确保系统按照技术愿景的要求实现。
3. 如何构建微服务
本章从一个产品出发,讨论如何定义服务之间的边界,以期最大化微服务的好处。
3.1 MusicCorp简介
虚构了一个MusicCorp公司,将音乐服务从线下移到在线业务。公司认为只有保证自己很容易对应用进行修改才能提高成功的概率,这正是微服务的用武之地。
3.2 什么样的服务是好服务
专注于两个重要概念上:松耦合、高内聚。
松耦合
服务之间松耦合是指修改一个服务不需要同时修改另外一个服务,这对于微服务来说非常重要。
松耦合的服务应该尽可能少知道与之协作的微服务的信息,这意味着需要限制两个服务之间不同调用形式的数量,因为除了潜在的性能问题外,过度通信也会导致紧耦合。
高内聚
高内聚是指将相关的行为放在一起。需要改动时,只要修改一处即可。
这需要找到问题域的边界,以确保将相关的行为都放在同一个地方。
3.3 限界上下文
限界上下文来自于《领域驱动设计》(DDD)一书,在这里定义为:一个由显示边界限定的特定职责。
如果想要从一个限界上下文中获取信息,或者向其发起请求,需要使用模型和它的显示边界进行通信(这里的模型一词限界上下文中的东西,来自DDD)。
共享的隐藏模型
对MusicCorp来说,财务部门和仓库就可以是两个独立的限界上下文。它包含对外接口(存货报告、工资单等),以及只有自己内部需要知道的细节(铲车、计算器等)。
财务不需要知道仓库的内部细节,但需要了解目前的库存水平以核算公司的估值,所以财务部门与仓库就存在一个共享模型——库存项。但我们并不直接将这个模型暴露出去。共享特定的模型而不是共享内部表示。
模块和服务
有了领域内部的限界上下文,就可以使用模块对其进行建模,同时使用共享的隐藏模型。
这些模块边界就成为了绝佳的微服务候选。微服务应该清晰地和限界上下文保持一致。
过早划分
对一个新项目来说,在项目早期进行服务划分,可能导致服务划分不合理,经常有跨服务的修改。这时,可以先将这些服务合并成单块系统,等对服务边界充分了解之后再将单块服务划分成多个微服务。这其中所花费的代价是很高的,所以更一般的建议是在初期先使用单块系统,等有一定理解之后再拆成微服务。
3.4 业务功能
当思考组织内的限界上下文时,不应该从共享数据的角度来考虑,而应该从这些上下文能够提供的功能来考虑。
常见的误区是先定义数据,然后在数据上提供CRUD形成服务。这脱离了这个服务在业务上应该提供什么功能这个出发点,会导致不合理的接口设计以及过度暴露服务细节。
3.5 逐步划分上下文
当考虑微服务边界时,首先考虑比较大的、粗粒度的那些上下文,然后当发现合适的缝隙后,再进一步划分出那些嵌套的上下文。
这里涉及到两种分法,以仓库为例,可细分为订单处理、货物接收、库存管理这三部分。
第一种嵌套限界上下文,即对外还是使用仓库的功能,只是发到仓库的请求会被透明地转到后面三个服务上,从外界的视角只存在一个仓库服务;
第二种不存在仓库服务,而是把三个服务分离开各自对外提供服务。
很难说哪种规则更合理,但可以根据组织结构来决定:如果三个服务不同团队,第二种合适;如果三个系统由一个团队负责,则第一种合适。这是由于组织结构和软件架构会互相影响。
另外一种思路更倾向于嵌套式方法,主要原因是他使得架构更成块从而更好地测试。
3.6 关于业务概念的沟通
修改系统的目的是为了满足业务的需求。之前我们聊过更倾向于将修改局限于单一微服务边界之内。所以在建模时需要充分考虑微服务之间如何就同一个业务概念进行通信。在组织内部共享的相同术语与想法,也应该被反应到服务的接口上来。
3.7 技术边界
举一个例子,某业务线采用三层微服务结构,最外层前端服务,提供外部请求处理服务;中间层数据访问层,访问数据库;最下层数据库层。
这种架构称之为洋葱架构,它的多层结构使得修改某个数据时,上层的两个服务都要调整。而且如果提供了大流量的外部接口,那么内部流量会由于服务间的中转导致性能问题。
值得一提的是这种架构并非一无是处,当需要对某个服务有一定性能要求时,这种划分方式反而更加合理,例如对RDBMS或者NoSQL进行流量合并这种场景时,有一个数据代理层可以显著提高性能。
4. 集成
4.1 寻找理想的集成技术
这里所指的集成技术指的是像SOAP、XML-RPC、REST、Protocol Buffers。本节讨论是希望从这些技术中得到什么。
避免破坏性修改:避免对某一个服务做出修改时,消费者也需要随之修改。例如添加字段。
保证API的技术无关性:不应该选择那种对微服务具体实现技术有限制的集成方式。
使你的服务易于消费方使用:一种情况是消费方可以使用任何技术实现,如HTTP/REST;另外一种提供一个客户端库(SDK)但这增加了耦合的风险。
隐藏内部细节:消费方不应该与服务方的内部细节绑定,否则会增加耦合风险。可以说任何倾向于暴露内部实现细节的技术都不应该被采纳。
4.2 创建用户接口(原为用户创建接口)
以MusicCorpp系统中的创建客户操作为例子,这看似简单的CRUD操作,其实不然。添加客户时需要触发欢迎邮件、付费账号设置等流程,而修改了删除也有一些额外操作。
以下看看有哪些可选的技术来实现此服务。
4.3 共享数据库
共享数据库,即数据库集成方式,如果其它服务想要从一个服务获取信息,那么直接访问数据库获取。 这种方式简单、快捷,但暴露出很多问题。
这使得外部系统能够查看内部实现细节,并绑定在一起。如果想要添加或者修改字段时,需要回归测试所有消费方是否工作正常。
消费方与特定的技术选择绑定在一起。这使得服务散失了自治性,无法在未来选择更加合适的数据库。再见,松耦合!
共享数据库使得对数据库的操作逻辑散落在各个消费方当中,如果某一个之前的操作被证明有BUG时,需要修改所有消费方中的逻辑。再见,内聚性!
4.4 同步和异步
同步即调用方阻塞等待整个操作完成,异步通信是指调用方不需要等待操作完成即可返回,甚至可能不需要关心这个操作是否完成。
同步和异步这两种通信模式对应两种协作风格,请求/响应或者基于事件。
同步通信使用请求响应式,即客户端发送请求,然后等待服务方完成并响应结果。但异步通信也能使用之种方式:发请求并注册一个回调,当服务端操作结束之后会调用此回调。
基于事件的方式是指客户端发布一个操作,其它的协作者接收并处理事件。之使得各自的业务逻辑都在自己的服务内处理,具有低耦合性。可以在不影响客户端的情况下添加新的订阅者。
如何选择呢?一个重要的因素就是哪种方式能很好的解决问题。
4.5 编排与协同
以创建账号为例(创建账号之后需要进行三项操作创建积分记录、从邮局发送欢迎包裹、发送欢迎邮件),来说明编排与协同的不同之处。
编排(orchestration)的方式会在某个中心结点(可能是帐户服务)指挥其它的服务完成各自的流程(与积分、邮政、邮件服务进行同步通信)。使用编排可以很方便地跟踪当前流程的进度。缺点是中心结点承担了太多责任,会成为网状结构的中心枢纽和很多逻辑的起点。
协同(choreograph)的方式中,客户服务仅仅是异步触发一个事件”客户创建”,其它三个系统订阅这个事件并进行相应的处理。这种方式很明显地消除了耦合。但需要额外的工作来监控整个流程是否出现异常。
后面两小节讨论在请求/响应方式中使用的两种技术过程过程调用RPC和表述性状态转移REST。
4.6 远程过程调用(Remote Procedure Call/RPC)
RPC允许像调用本地函数一样调用远程服务。RPC实现一般会自动生成服务端和客户端桩代码,从而使开发者可以快速开始编码。
RPC的种类繁多,一些依赖于接口定义如SOAP、Thrift、protocol buffers等,不同的技术栈可以通过接口定义快速生成客户端和服务端代码。也有一些依赖与双方具有相同的技术栈,如Java的RMI。
然而RPC在易用性背后也隐藏了许多问题。
技术的耦合
如果选用例如RMI这样的技术,就将双端绑定在JVM上了。
本地调用和远程调用并不相同
RPC的核心思想是隐藏远程调用的复杂性。如果开发没有意识到某一个调用是远程调用的话,错误就会开始发生。
首先RPC会对发送的数据进行封装和解封,这有可能会引起性能问题。另外网络是不可靠的,如果不对各种可能的网络问题与服务问题区分处理,也会引起问题。
脆弱性
RPC的脆弱性是指某一些结构在对接口进行修改时需要双端同步修改,即使这个修改看起来不会影响双端。例如添加字段或者方法,删除不用的字段等等。使用某些技术可以避开这个问题。
RPC很糟糕吗?
RPC并不糟糕,在使用时多注意以下问题,可以减少问题的发生:不要对远程调用过度抽象,以至于网络因素完全被隐藏起来;确保你可以独立地升级服务端的接口而不用强迫客户端升级;在客户端中一定不要隐藏我们是在做网络调用这个事实。
4.7 REST(REpresentational State Transfer)
REST是RPC的一种替代方案,它以资源为核心,一般基于HTTP协议之上实现。
REST不同风格的比较可参考此
REST与HTTP
HTTP提供的动词(GET、POST、PUT)能够很好地和资源一起使用:GET使用幂等的方式获取资源,POST创建一个新资源。
HTTP周边存在一个大的生态,包含了大量的支撑工具和技术,可以提供缓存代理(Varnish)、负载均衡、监控等各种功能。
超媒体作为程序状态的引擎
HATEOAS(Hypermedia As The EngineOf Application State)超媒体作为程序状态的引擎,是最成熟的REST服务,他不仅将资源返回,而且还会在返回的资源信息里添加可以对资源进行的操作以及操作的链接。
JSON、XML还是其他
基于HTTP的REST可以提供多种不同的响应形式,可以是JSON、XML甚至二进制。
JSON更加简单、紧凑,但XML可以使用链接进行超媒体控制,虽然HAL也试图为JSON定义通用的超文本格式。XML有一些成熟的工具如XPATH可供使用。
留心过多的约定
某些框架(Spring Boot的某些工具)生成的例子会将数据库对象反序列化进程内的对象,并直接暴露给外部,这增加了耦合的风险。
一种有效的方法是先设计外部接口,等到外部接口稳定之后再实现微服务内部的数据持久化。这样做可以保证服务的接口是由消费者的需求驱动出来的,从而避免数据存储方式对外部接口的影响。其缺点是推迟了数据存储部分的集成。
基于HTTP的REST的缺点
REST无法像RPC一样直接生成桩代码,虽然有很多的HTTP库可以使用,但如果要使用HATEOAS的话,就需要自己动手了。随着复杂度增加,会构建出一些共享库,此时就要留意共享服务端和客户端代码带来的风险了。
某些框架无法完全支持HTTP的动词,像PUT或者DELETE这些。
HTTP协议冗长以及解析会有一些性能和带宽开销,无法与某些RPC中的二进制传输比较。但HTTP2会缓解这个问题。
对于低延迟的应用场景,基于原生TCP或者WebSockets这样的协议会更加有效。
对于服务和服务之间的通信来说,如果低延迟或者较小的消息尺寸对你来说很重要的话,可以使用UDP协议,而很多RPC可以运行于TCP之外的网络协议之上。
有些RPC的实现支持高级的序列化和反序列化机制,而REST需要自己实现。
4.8 实现基于事件的异步协作方式
技术选择
主要有两个部分需要考虑:微服务发布事件机制和消费者接收事件机制。
RabbitMQ这样的消息代理能够满足要求。生产者使用API向代理发布事件,消费者订阅对应事件,并在事件发生时收到通知。它会是实现松耦合、事件驱动架构的一种非常有效的方法。当然需要部署和维护这一消息代理服务。与之类似的还有kafka。
另外一种ATOM这一协议使用HTTP来发布事件,后续学习补充(todo)。
异步架构的复杂性
事件驱动的系统看起来耦合非常低,而且伸缩性很好。但是这种编程风格也会带来一定的复杂性。
例如特定事件导致某台执行这个事件的服务崩溃,从而事件失败后进入重试队列,这导致另外一台执行这个重试事件的服务也崩溃。这被称为灾难性故障转移(catastrophic failover)。
可以设置失败队列的最大重试次数,并且把这些一直失败的事件集中到某一个死信队列中,供后台查看分析失败的原因。
另外使用异步架构时,需要对各个服务加强监控,并使用关联ID来跟踪请求的执行情况。
4.9 服务即状态机
服务即状态机的概念都很强大。服务应该根据限界上下文进行划分,包含了所有与这个上下文中行为相关的所有逻辑以及与之相关的事件。我们想要避免简单地对CRUD进行封装的贫血服务。
我仍然认为基于HTTP的REST相比其他集成技术更合理,但不管你选的是什么,都要记住上面的原则。
4.10 响应式扩展
响应式扩展(Reactive extensions, Rx)提供了一种机制,使得只要对操作的结果进行观察,结果会根据相关数据的改变自动更新。
通过使用 Reactor 进行反应式编程此文可以对响应式(反应式)思想窥觑一斑。
4.11 微服务世界中的DRY和代码重用的风险
在微服务内部不要违反DRY,但在跨服务的情况下可以适当违反DRY。服务之间引入大量的耦合会比重复代码带来更糟糕的问题。但这的确是一个值得进一步探索的问题。
客户端库
很多团队坚持在最开始的时候为服务开发一个客户端库,这样不仅能简化对服务的使用,还能避免不同消费者之间存在重复的与服务交互的代码。但这会导致潜在的耦合,内聚性减弱,往往修改时两端都要修改。如果一定要使用,可以只使用第三方的SDK来进行开发,之样混入的业务逻辑就会减少。
Netflix的经验:Netflix非常强调客户端的使用,以保证系统的可靠性和可伸缩性。Netflix的客户端库会处理类似服务发现、故障模式、日志等与服务本身的职责并没有什么关系工作。这些库在Netflix中的使用大大减少了初始搭建的工作量,并提高了生产率,同时也能确保系统能正常工作。
如果你想要使用客户端库,一定要保证其中只包含处理底层传输协议的代码,千万不要把与目标服务相关的逻辑放到客户端库中。最后,确保由客户端来负责何时进行客户端库的升级,这样才能保证每个服务可以独立于其他服务进行发布!
4.12 按引用访问
按引用访问说的是如何处理某个领域实体已经过时的问题。例如某个服务从客户服务获取客户资源信息,后续需要给邮件服务请求给这个客户发送邮件,而邮件服务有两种做法,从传过来的信息里拿出邮件地址发送邮件,但此时邮件地址可能不是最新的了,另外一种方法是拿客户的id(引用)去客户服务拿最新的邮件地址再发送邮件。
这方面的决策就是权衡服务器压力与过期数据所带来影响这两方面的考虑。但无论如何,都需要使得在想要获取最新状态时,拥有拿此实体的引用去查询最新信息的能力。
另外一些方向有助于此问题的决策,例如为资源添加有效性时限,那么其它服务就可以缓存此信息,减少服务负载,另外有些服务可能不需要知道完整的实例信息,查询最新信息可能引入耦合,例如邮件服务可以更简单地只是往某个地址发送内容,而不需要关心客户相关的信息。
4.13 版本管理
在接口发生改变时,如何管理这些改变。
尽可能推迟
减少破坏性修改影响的最好办法就是尽量不要做这样的修改。例如选用REST替换共享数据库方案等。
另外一个延迟破坏性修改的关键是鼓励客户端的正确行为,避免过早地将两端绑定起来。例如客户端仅就自己用到的字段进行绑定,这样服务端删除了一个无关的字段时,不会受到影响,添加字段也是,尽量使用不依赖于位置查找字段的技术,使得添加字段而不影响现有客户端的运行。对于客户端来说要符合鲁棒性原则(Postel法则),即宽进严出,对接收的信息宽容,对自己发出的东西严格。
及早发现破坏性修改
使用消费者驱动的测试来尽早地发现服务端的破坏性改动所引发的问题。如果客户端是多个版本的,那么测试也需要使用多个版本来测试最新的服务是否造成问题。
使用语义化的版本管理
语义化版本管理的每一个版本号都遵循这样的格式:MAJOR.MINOR.PATCH。其中MAJOR的改变意味着其中包含向后不兼容的修改;MINOR的改变意味着有新功能的增加,但应该是向后兼容的;最后,PATCH的改变代表对已有功能的缺陷修复。
客户端只要查看服务端的版本和自己的版本号就知道是否可以集成了。但目前还没有在分布式系统中见到很多这样用的例子。
不同的接口共存
新开接口可以将影响降到最低,旧的消费者访问旧的接口并不断往新接口上迁移;等所有消费者迁移完成,旧接口就可以废弃掉了。
这其实就是一个扩展/收缩模式的实例,它允许我们对破坏性修改进行平滑的过度。首先扩张服务的能力,对新老两种方式都进行支持。然后等到老的消费者都采用了新的方式,再通过收缩API去掉旧的功能。
同时使用多个版本的服务
短期内同时使用两个版本的服务是合理的,尤其是当你做蓝绿部署或者金丝雀发布时。
但在日常情况下就维护两个甚至更多版本的服务,则需要考虑很多问题:1. 修复bug需要修复和部署多份;2. 如何将用户路由到不同版本的服务上,nginx重写和中间件都有管理和维护成本;3. 不同版本创建的数据都会保存在同一数据库中,这可能引入复杂性。
4.14 用户界面
走向数字化
使用微服务架构所暴露出来的那些API,是细粒度的API,通过把这些服务的功能进行不同的组合,可以为桌面应用程序、移动端设备、可穿戴设备的客户提供不同的体验。
约束
桌面Web应用需要考虑浏览器及屏幕解析度的问题,移动应用需要注意交互方式和电池电量的影响,所以虽然这些平台使用的服务核心是基本一致的,但仍需要应对不同应用场景的约束。
API组合
让用户界面直接与微服务交互,一个页面可能组合多个接口的返回值并做呈现。
这种方式的问题是很难为不同的设备定制不同的响应,可以通过让微服务支持客户指定返回字段来解决。
另外一个问题,如果UI是其它团队开发的,那么会回到以前那种分层合作模式,在这种模式下即使很小的修改都需要多个团队的参与。
这种通信模式非常繁琐。对于移动用户的电池和流量都是一种考验。可以使用后面讨论的API Gateway来缓解这个问题。
UI片段的组合
另外一种方式是服务端接口直接返回UI片段,这样前台只要直接嵌入这些片段即可。
这种方式的优点是服务团队同时可以维护这些UI片段,允许我们快速完成修改。
问题也很明显:
-
用户体验的不一致性:由于不同团队维护不同服务以及不同UI片段,交互与风格上有所偏差,导致同一页面风格差异。有一些技术可以避免这些问题,比如活样式指导(living style guides),从而使其具有一定程度的一致性。
-
原生应用需要嵌入HTML来重用一些服务端组件,但这种方式会使用户体验欠佳。
-
嵌入片段UI的方式在强交互的地方会有局限性,例如搜索实时变动。
为前端服务的后端
添加API入口层,对后端服务进行编排,为不同设备订制不同的内容。但这有可能导致这一层包含太多逻辑,导致难以维护。
另一种做法是一个API入口只为一个应用或者用户界面服务,这种模式有时也叫作BFF(Backends For Frontends,为前端服务的后端)。它允许团队在专注于给定UI的同时,也会处理与之相关的服务端组件。
与任何一种聚合层类似,使用这种方法的风险在于包含不该包含的逻辑。业务逻辑应该处在服务中,而不应该泄露到这一层。这些BFF应该仅仅包含与实现某种特定的用户体验相关的逻辑。
一种混合方式
前面提到的那些选择各自都有其适用的范围。一个组织会选择基于片段组装的方式来构建网站,但对于移动应用来说,BFF可能是更好的方式。关键是要保持底层服务能力的内聚性。比如,预定音乐和改变客户信息的逻辑应该处在相应的服务中,避免这些逻辑在系统中到处散布。将太多的逻辑放入到刚才提到的那种中间层中是一个常见的陷阱,在实际中需要非常小心地做权衡来避免这个问题。
4.15 与第三方软件集成
如果已经在用的第三方软件如CMS或者旧服务,现需要限期用户可看到的功能,但又不好对这些服务进行修改,此时就可以在这些服务之前增加一层可控的服务,来展示有限的功能或者对某一些接口进行转发。与些类似的模式称之为绞杀者模式(Strangler Application Pattern),他是通过前置的代理服务,慢慢地将老接口迁移到新接口上。
4.16 小结
无论如何避免数据库集成
理解REST和RPC之间的取舍,但总是使用REST作为请求/响应模式的起点
相比编排,优先选择协同
避免破坏性修改、理解Postel法则、使用容错性读取器
将用户界面视为一个组合层
5. 分解单块系统
5.1 关键是接缝
在《修改代码的艺术》这本书中,Michael Feathers定义了接缝的概念,从接缝处可以抽取出相对独立的一部分代码,对这部分代码进行修改不会影响系统的其他部分。识别出接缝不仅仅能够清理代码库,更重要的是,这些被识别出的接缝可以成为服务的边界。
5.2 分解MusicCorp
假设原来的MusicCorp是单块应用,现识别出以下上下文:产品目录、财务、仓库、推荐。
首先创建包结构来表示这些上下文,然后把已有的代码移动到相应的位置。这个过程可以使用IDE来帮忙完成,大部分代码会找到自己的位置 ,另外一些剩下的可能就是没有被识别的限界上下文了。
5.3 分解单块系统的原因
分解完成后,一般不建议将所有这些一次性构建成微服务,而是先找一块收益最大的限界上下文抽离成服务。
以下以MusicCorp为例子,说明哪些因素可用来参考以决定哪一部分抽离成微服务:
改变的速度:接下来,我们可能会对库存管理方面的代码做大量修改。所以如果现在把仓库接缝抽出来作为一个服务,使其成为一个自治单元,那么后期开发的速度将大大加快(但一般稳定的系统更容易抽离成微服务)。
团队结构:MusicCorp的交付团队事实上分布在两个不同的地区,一个团队在伦敦,另一个在夏威夷。最好能把夏威夷团队维护的大部分代码分离出来,这样他们就能对此全权负责。
安全:MusicCorp有安全审计的机制,并且决定对敏感信息做更加严密的保护。目前这部分功能由财务相关的代码处理。如果把这个服务分出去,可以对这个独立的服务做监控、传输数据的保护和静态数据的保护等,第9章会对此做进一步阐述。
技术:维护推荐系统的团队研究出了一种新的算法,这种算法使用了Clojure语言中逻辑式编程的库,并且认为这能够大大改善我们的服务。如果能把这部分推荐代码分离到一个单独的服务中,就很容易重新实现一遍,并对其进行测试。
数据库(5.4~5.12)
我们想要拉取出来的接缝应该尽量少地被其他组件所依赖(5.4杂乱的依赖)。
数据库是所有杂乱依赖的源头。这需要寻找数据库中的接缝(5.5数据库)。
数据库相关的代码也随着之前的代码分解到对应的限界上下文中了,可以分析出哪些上下文用了哪些数据。此外,像外键约束这样的隐藏关系,就需要借助诸如SchemaSpy这样的可视化工具展示出来。那如何处理这些交错的数据库关系呢?(5.6找到关键的子问题)
在MusicCorp公司中,财务服务有一个总帐表记录某一个专辑售的数量与价格,它与产品目录的专辑表有一条外键约束。分离两个服务需要让财务服务不再从专辑表获取信息,而是调用产品目录服务来获取。外键约束也要去掉,因为最终这两个表将彻底独立,这就需要通过跨服务的一致性检查或周期性清理数据等方式来保证数据的一致性了。至于产品的数据库调用次数上涨,这是一个权衡问题,你需要多高的性能呢,以及牺牲部分性能带来的松耦合好处是否值得。(5.7例子:打破外键关系)
MusicCorp在单块系统时代将国家信息这一共享静态数据放在了数据库中,供产品目录、仓库和财务服务等服务调用。有几种办法解决拆分服务后此问题:1. 在各个服务中复制一份此表或者在各个服务代码中都添加些配置,更好一点地,创建共享库;一般这样就能解决大部分问题;2. 极端一点将这些数据独立成服务以供调用,有点杀鸡用牛刀的意思,但数据复杂时可以使用。(5.8例子:共享静态数据库)
公司为了让财务和仓库能够获取和修改客户相关的记录数据,会让这两部分同时共享客户记录数据。这个问题的原因是领域概念不是在代码中进行建模,相反是在数据库中隐式地进行建模。这里缺失了客户这一领域概念。补上客户服务,财务和仓库通过访问客服来修改和获取数据即可以解决此问题。(5.9例子:共享数据)
产品都有产品信息、价格、库存等信息,MusicCorp将这些数据统一记录在条目表里,产品目录和仓库都要同时读取这一表来获取数据。这是将不同的关注点放在了一起,可以将此表拆分成两个表,产品目录表供产品目录服务使用,库存水平表供仓库使用。(5.10例子:共享表)
以上我们进行了数据库重构操作,但我们不急于拆分为服务,而是先分离表结构。这将导致数据库的访问次数增加以及某些事务完整性的失效。先分离数据库结构但不分离服务的好处在于,可以随时选择回退这些修改或是继续做,而不影响服务的任何消费者。我们对数据库分离感到满意之后,就可以考虑对整个应用程序的分离了。(5.11重构数据库)
事务保证一组操作要么全部完成或者全部不做。拆分数据库之后破坏了部分事务的完整性。例如客户下单时需要操作客户服务表的订单表以及仓库服务的提取表,如果插入订单表成功,而插入提取表失败呢?
有多个方法:1. 将失败记录下来,后续再试一次,达到最终一致性;2. 终止整个操作,对已经操作的订单表进行补偿操作(DELETE表),这就涉及到补偿失败的问题,可以使用重试或者人工或者自动定期校准的方式,但对于多个操作进行补偿时就显得复杂了;3. 使用分布式事务如两阶段提交等现有的技术或者库来实现,但其带来了复杂性,以及失败异常阻塞等问题。
无论哪种方案都会增加复杂性,需要思考其必要性,是否可以使用最终一致性这样简单的方案来代替。
如果你遇到的场景确实需要保持一致性,那么尽量避免把它们放在不同的地方。如果实在不行,那么要避免仅仅从纯技术(比如数据库事务)的角度考虑,而是显式地创建一个概念来表示这个事务。你可以把这个概念当作一个句柄或者钩子,在此之上,能够相对容易地进行类似补偿事务这样的操作,这也是在系统中监控这些复杂概念的一种方式。举个例子,你可以创建一个叫作“处理中的订单”的概念,围绕这个概念可以把所有与订单相关的端到端操作(及相应的异常)管理起来。(5.12事务边界)
报表(5.13~5.19)
报表服务经常需要聚合多方面的数据,微服务化会对其造成较大的影响。(5.13报表)
单块服务中,多个数据源都在同一个库中,可以通过SQL语句联表查询即可出结果(可通过副本数据库来减少性能影响)。这并不好:1. 共享表的缺点,修改的代价太大;2. 前台使用的表并不适合用于报表(例如索引,事务管理器,配置原因等);3. 关系型数据库并不是最优选择,不同的报表需求应该尝试不同的数据库。接下来介绍微服务下的几种替代方案。(5.14报表数据库)
通过服务调用获取数据可以适用于简单的报表场景,但对于像获取所有用户数据并分析分布规律这样的报表就很难满足。主要原因是微服务出于性能、缓存等原因不会提供这些统计相关的专门接口。虽然如此,如果一定要实现获取所有用户信息这样功能的接口,还是可以通过接收请求,创建文件,用户请求下载文件这样的方式来处理。总体来说这个方式并不适合报表的场景。(5.15通过服务调用获取数据)
使用数据导出技术来周期性地把数据推送到报表数据库。当然这增加了耦合问题。如果实现得好这可以是个例外,因为这种方式使得报表生成变得足够简单。可以将导出的数据视为接口的一部分,并做版本管理,此外加上只有此服务和报表系统和访问这些数据,一定程序上可以缓解耦合。
有一些报表系统汇集了所有服务的数据库,并通过视图之类的技术创建聚合,报表系统只需要知道自己的报表视图结构即可。但是这种方式的性能就取决于你所选用的数据库系统了。(5.16数据导出)
基于状态改变事件来将事件数据导出到报表数据库这种方式可以彻底消除耦合。再加之事件是实时和增量的,这对于报表数据库的实时性、插入效率都有所改善。这个方法主要的缺点是,所有需要的信息都必须以事件的形式广播出去,所以在数据量比较大时,不容易像数据导出方式那样直接在数据库级别进行扩展。如果你已经暴露出了合适的事件,建议考虑这种方式。(5.17事件数据导出)
Netflix利用Cassandra上的数据备份来处理生成报表。这需要前提:1. 首先有备份数据,Netflix将服务的数据以SSTables文件的形式备份存储在Amazon的S3对象存储服务;2. 实现对这些大量备份数据的处理,Netflix实现了一个能够处理大量数据的流水线,并且开源了出来,它就是Aegisthus项目。(5.18数据导出的备份)
其实并非所有的报表都要从同一个地方输出。仪表盘、告警、财务报表、用户分析等使用场景对于时效性的要求不同,所以需要使用不同的技术,某些情况通过监控技术能实现更通用的数据统计需求。(5.19走向实时)
5.20 修改的代价
小的增量的修改所造成的影响我们更加可理解,造成的错误更加可控。拆分数据库,拆分单块服务需要进行巨大的修改,如何控制这些修改所带来的风险呢?
建议先在白板上进行思考,将设计画在白板上,在这些服务上运行用例,例如当用户注册时会发生哪些调用。这对于发现奇怪的循环引用、两个服务之间的通信是否过多以至于是否需要合并等问题有很大帮助。遍历的用例越多,你就越能知道这些组件是否以正确的方式在一起工作。这与设计面向对象系统时的典型技术:CRC(class-responsibility-collaboration,类-职责-交互)卡片一致。
5.21 理解根本原因
服务一定会慢慢变大,直至大到需要拆分。系统的架构随着时间的推移增量地进行变化。关键是要在拆分这件事情变得太过昂贵之前,意识到你需要做这个拆分。但这很难。本章介绍了拆分的起点,后续还有一系列需要考虑和解决的问题。
6. 部署
6.1 持续集成简介
CI(Continuous Integration,持续集成)能够保证新提交的代码与已有代码进行集成,从而让所有人保持同步。CI服务器会检测到代码已提交并签出,然后花些时间来验证代码是否通过编译以及测试能否通过。
生成构建物(artifact)一般是CI中的一个流程,后续会对这一构建物进行测试验证。为了避免重复生成以及保证测试与线上使用同一个构建物,这一流程一般只进行一次。
CI能够快速得到代码质量的某种反馈,可以自动生成二进制并对其进行测试,用于生成这些构建物的所有代码都在版本的控制之下,可以很方便重新生成任意版本构建物。通过CI我们能够从已部署的构建物回溯到相应的代码,有些CI工具,还可以使在这些代码和构建物上运行过的测试可视化。正是因为上述这些好处,CI才会成为一项如此成功的实践。
测试你是否真正理解CI的三个问题:
是否每天签入代码到主线:你应该保证代码能够与已有代码进行集成。
你是否有一组测试来验证修改:没有对代码行为进行验证的CI不是真正的CI。
当构建失败后,团队是否把修复CI当作第一优先级的事情来做:如果构建失败没有及时修复,后面定位会花费较长时间。
6.2 把持续集成映射到微服务
将所有微服务代码放在一起,构建时所有微服务统一构建部署这种方式显示会有很多缺点:1. 只修改其中的某个服务,所有服务都要验证、部署,浪费时间;2. 构建之后生成了所有服务的构建物,无法得知哪些需要重新部署;3. 如果构建失败,所有服务将都无法构建。
多个服务使用同一个代码库,即使有各自的CI,也并不推荐,它使得耦合修改多个服务一次性提交变得容易。
推荐的方式是独立的代码库,独立的CI流程,独立的部署。另外需要说明的是各个服务的测试代码也需要与服务代码放一起,这样就很容易知道对于某个服务来说应该运行哪些测试。
6.3 构建流水线和持续交付
CD(Continuous Delivery,持续交付)能够检查每次提交是否达到了部署到生产环境的要求,并持续地把这些信息反馈给我们。在CD中,我们会把多阶段构建流水线的概念进行扩展,从而覆盖软件通过的所有阶段,无论是手动的还是自动的。这些阶段包括编译及快速测试、耗时测试、用户验收测试、性能测试、生产环境等。这些过程可以是手动的,在一个统一的后台标识着目前构建的进度,如果到了手动用户验收测试流程,则需要对应人员进行手工验收,通过后将其标识为成功。
构建物(6.4~6.6)
构建物是指特定技术栈所构建出来用于运行的产物,例如Java的JAR或者WAR包,Ruby的Gem等。但只有构建物是不够的,像PHP、Ruby需要一个如Nginx这样的HTTP服务。所以有时候需要Chef、Puppet这样的自动化配置管理工具来解决。
多个微服务可能采用完全不同的技术栈,为了免去不同部署机制带来的麻烦,可以使用Chef、Puppet及Ansible这类的通用部署工具对部署进行自动化。(6.4平台特定的构建物)
使用操作系统支持的构建物可以减少不同技术栈差异带来的影响,例如在CentOS中使用RPM;Ubuntu中使用deb包;Windows中可使用MSI。(6.5 操作系统构建物)
随着时间的推移,服务的依赖越来越多,从最开始的JVM到后面的日志收集,监控收集等等,Puppet和Chef运行得越来越慢,也越来越复杂。虚拟机镜像为此提供了解决方案。只要一次构建镜像,将运行时环境、依赖服务、以及服务本身需要都打包进去,后续只需要运行镜像即可。
但镜像的方案也有一些问题:1. 通常较大,打包时间较长,对于测试环境这样经常发布的场景不太适用,所以可能线上和测试使用两种方案,当然Docker技术可以缓解此问题;2. 镜像种类众多VMWare镜像、AWS AMI、Vagrant镜像、Rackspace镜像等等,如果用到了多种类型的镜像可以尝试Packer这样可打包出多种镜像的工具来简化工作。
其实镜像方案分两种,一种是只把环境打包成镜像,而服务本身还是每一次正常发布;另一种是将服务也打包到镜像中,将镜像做为构建物进行发布,这种概念称之为不可变服务器。
不可变服务需要注意以下问题:1. 配置漂移问题,即手动修改服务配置,导致不一致,这需要禁止任何对服务器的手工修改,严格点可以禁止机器上开启ssh服务;2. 修改周期是否适合于经常发布的场景;3. 运行时产生的数据(日志等)需要额外的方式保存和收集。(6.6定制化镜像)
6.7 环境
CD的各个阶段构建物会被部署到不同的环境,例如开发环境、测试环境、生产环境等。各个环境一般不一致性,例如生产环境是分布式的,而测试环境是单机的。这很容易出现测试环境测试通过的代码,在线上却产生问题。
一般我们都会要求各个环境一致,但在实际中会有多方面的因素需要权衡例如成本、发布速度、复杂度等。
6.8 服务配置
不同环境有着不同的配置,如数据库用户名密码等。应该最小化环境间配置的差异,否则可能某些问题只能在特定的环境中复现了。
不同环境不同配置比较简单的实现方案是为不同环境生成不同的构建物,但这违反了在CD流水线上只有一个构建物的原则,而且耗时。
更好的办法是对配置进行单独管理,可以是在安装或者运行时传入参数,也可以使用专用的系统来提供配置。
6.9 服务与主机之间的映射
6.9.1 单主机多服务
这种方式主机的数量不会随着服务的增加而增加,成本上也会由于减少虚拟化技术的使用而有所减少。另外这种方式对开发来说更简单,因为与本地开发环境的部署类似。
但遇到的问题也很多:1. 监控困难,服务之间互相影响,导致在出现负载问题时分析困难;2. 部署复杂,多个服务的依赖互相影响甚至冲突,而这些问题容易走上另外一条更加错误的路线,合并部署;3. 对团队的自治不利,主机的所有权不明确,最可能发展成为有独立团队管理主机,降低效率;4. 每一个服务的需求不同,但部署在一起就不得不对他们一视同仁,有时还会造成资源的浪费。
6.9.2 应用程序容器
与主机多服务类似,这种做法可以减少语言运行时的开销,例如在一个Java servlet容器中部署五个Java服务的话,只需要启动一个JVM即可。
问题:1. 限制技术栈的选择,也会限制自动化、系统管理工具的选择;2. 某一些容器提供的特性也会带来问题,如共享会话功能会影响服务的伸缩性,多服务的监控聚合往往无法提供;
在如今,这种部署方式所带来的资源优化已经没有那么明显,像诸如Jetty等轻量级的自包含HTTP服务等技术已经可以将分开独立部署的开销降得很低了。
6.9.3 每个主机一个服务
这种模型避免了单主机多服务的问题:简化了监控和错误恢复、减少潜在的单点故障、技术栈选择无限制、简化问题的排查。但这种方式增加了主机量,可以通过使用Paas来解决。
6.9.4 PaaS
当使用PaaS(Platform-as-a-Service,平台即服务)时,你工作的抽象层次要比在单个主机上工作时的高。大多数这样的平台依赖于特定技术的构建物,比如JavaWAR包或者Ruby gem等,这些平台还会帮你自动配置机器然后运行。其中一些能够透明地对系统进行伸缩管理,而更常用的方式(根据我的经验来看,也是更不容易出错的方式)是,允许你控制运行服务的节点数量,然后平台帮你处理其余的工作。
6.10 自动化
微服务的很多问题都可以也需要使用自动化来解决。这里因为使用单服务单主机的形式时会引入更多的主机,即使控制了主机的数量,还是会有很多服务。这就意味着有更多的部署要处理、更多的服务要监控、更多的日志要收集,所以自动化很关键。自动化还能够帮助开发人员保持工作效率,简化工作。
6.11 从物理机到虚拟机
虚拟化技术允许我们把一台物理机分成多台独立的主机,这减少管理主机的开销,但虚拟化本身也会占用资源。
6.11.1 传统的虚拟化技术
如上图,虚拟化技术分成标准类型2虚拟化和轻量级容器技术这两种技术(类型1虚拟化指的是只能运行在裸机之上,而不能运行在操作系统之上的技术)。像AWS、VMWare、VSphere、Xen和KVM都属于左边这种类型。
hypervisor的任务主要有两个。第一,对CPU和内存等资源做从虚拟主机到物理主机的映射。第二,给我们提供一个控制虚拟机的层。
hypervisor本身也需要一定的资源来完成自己的工作,所管理的主机越多,占用的资源就越多。在某个点上,这些额外的开销就会变成继续切分物理机的限制。
6.11.2 Vagrant
Vagrant允许在开发机上创建一个虚拟的云。它的底层使用的是标准的虚拟化系统(通常是VirtualBox,但也可以使用其他平台)。你可以使用写有网络定义和镜像配置的文本文件来定义一系列虚拟机,这样就可以提交到代码库在团队内共享。这极大方便了模拟各种线上才能出现的问题,例如单机故障,集群同步等。此技术一般应用于开发环境或者测试环境,生产环境是较少使用。
6.11.3 Linux容器(LXC)
Linux容器可以创建一个隔离的进程空间,进而在这个空间中运行其他的进程。
LXC与标准类型2的虚拟化有一些不同:1. 不需要hypervisor;2. 尽管每个容器可以运行不同的操作系统发行版,但必须共享相同的内核(因为进程树存在于内核中)。LXC的启动非常快,在相同硬件上可运行更多的容器。
容器技术问题:1. 需要配置复杂的路由来允许外界访问容器内服务,2. 并非完全隔离。
6.11.4 Docker
Docker是构建在轻量级容器之上的平台。封装了容器配置、网络等大多数与容器管理相关的事情。你可以在Docker中创建和部署应用,使用镜像源的方式,还可以很方便存储、管理、共享应用程序。
CoreOS是一个专门为Docker设计的经过裁剪的Linux操作系统。它比其他操作系统消耗的资源更少,从而可以把更多的资源留给容器。
Docker本身并不能解决所有的问题,Google开源的Kubernetes提供了一系列可以方便容器运行、部署、监控、管理的技术和概念。
这种Docker加上一个合适的调度层的解决方案介于IaaS和PaaS之间,很多地方使用CaaS(Container-as-a-Service,容器即服务)来描述它。
有好几个公司都在生产环境使用了Docker。它提供了很多轻量级容器的好处,比如快速启动和配置等,并且使用了一些工具来避免它的缺点。如果你正在寻找不同的部署平台,我强烈建议你看看Docker。
6.12 一个部署接口
应在各个触发场景使用统一接口来部署服务。参数化命令行调用可做为这一接口。
对于一次部署,我们至少应该知道服务名称、版本以及环境,所以需要在测试环境部署,运行的命令可以是:
deploy artifact=catalog enviroment=local version=local
在CI触发时,运行的命令可能是:
deploy artifact=catalog enviroment=ci version=b456
推荐工具Fabric、expect。与工具配合的,还需要定义各个环境的配置,可使用YAML语法。
构建一个类似于这样的系统的工作量很大。这些代价基本上都需要在前期付出,但是做好之后,它能够很好地管理部署的复杂性。
7. 测试
了解测试的分类可以帮忙我们实现尽早交付软件与保持软件高质量之间的平衡,因为有时鱼和熊掌是不可兼得的。
7.1 测试类型
按照测试的目的和倾向性(支持团队还是支持产品、面向业务还是面向技术)可以分成:单元测试、验收测试(是否正确实现了功能)、非功能测试(响应时间、中扩展性、性能测试、安全测试等)、探索性测试(手工测试)。
对于手工测试,有其存在的必要性。对于微服务来说,尽可能多地使用自动化测试。
7.2 测试范围
另外一种测试金字塔模式,从上到下依次是用户界面测试(端到端测试)、服务测试、单元测试。越往上测试的范围更大信心更强,超往下测试更快隔离性越好越容易定位问题。
单元测试:通常只测试一个方法和方法调用,例如TDD(Test-Driven Design测试驱动开发)中的测试就属于这类。这类测试一般不启动服务,使用网络和外部文件也很有限。测试数量庞大,但彼此独立,运行速度快,面向技术。单元测试可以快速反馈功能是否异常,在重构中非常重要。
服务测试:绕开用户界面,直接针对服务的测试。通过一个服务测试只测一个服务的原则可以增加测试间的隔离性,更快定位问题,这就要求我们给外部合作的服务打桩。服务测试有时候也和单元测试一样快,但如果使用了真实的数据存储和外部服务,则测试时间会有所增加。
端到端测试:覆盖整个系统的测试,一般会打开浏览器或者App,操作用户界面来验证功能是否正常。
对于各种测试的比例,一个经验法则是顺着金字塔向下下面一层的测试数量要比上面一层多一个数量级。如果目前的状况不满足要求(周期太长、或者覆盖不全),可通过调整比例来改善。
7.3 实现服务测试
在实现服务测试时,为了只测试目标服务隔离其它服务,需要为外部服务打桩。这些桩在测试开始时启动,被测服务需要连到这些桩上,桩也要相应地响应接口返回。例如积分服务的桩可能需要为不同的用户返回不一样的积分值。
打桩(stub)是模拟外部服务响应请求,测试本身并不验证是否被正确调用;而mock除了响应请求外,还会验证请求的参数、是否在正确的时机调用等方面进行验证。一般来说使用stub的次数会超过mock次数。mock和stub的区别可参考此,不过这块说的更像单元测试中的两者。
打桩外部服务,有一些现成的工具可以使用,以减少工作量,例如Mountebank。
端到端测试(7.4~7.7)
进行端到端测试需要部署多个服务。这就需要考虑两个问题:1. 其它依赖的服务使用哪个版本呢?如果使用生产环境版本,那其它服务也有新版本准备上线呢?2. 这几个服务都进行端到端测试时,会有很多重合的测试用例,浪费资源。
如上图,可以让多个流水线扇入(fan in)到一个独立的端到端测试阶段(stage)的方式来解决这个问题。(7.4微妙的端到端测试)
但是端到端测试也有很多缺点。(7.5端到端测试的缺点)
随着服务范围的扩大,纳入到测试的服务数量增加,引起服务失败的可能性就增加。失败有可能是某个服务没有启动、网络故障等与服务本身的功能没有关系的原因。这些脆弱的测试时常失败,导致大家习惯于重新测试以期能偶然性地通过,掩盖了问题本质。
遇见这种脆弱的测试,应该立刻记录下来,如果不能立即修复,应该将其移除测试套件,再专心修复它们。修复时,首先看看能不能通过重写来避免被测代码运行在多个线程中,再看看是否能让运行的环境更稳定。更好的方法是,看看能否用不易出现问题的小范围测试取代脆弱的端到端测试(7.6脆弱的测试)。
如果这些服务属于一个团队,那自然这些测试也由这个团队负责。当涉及多个团队时,如果所有人都能随意添加,则会导致用例爆炸,另外由于没有拥有者,这些测试结果可能会被忽略,出现问题时认为是别人的问题,不加以重视;如果由专门的测试团队来负责那可能是灾难性的,开发人员远离测试代码,功能上线周期拉长,测试用例修复困难等,但这却是最常见的组织模式。最好的平衡是共享端到端测试套件的代码权,但同时对测试套件联合负责。团队可以随意提交测试到这个套件,但实现服务的团队必须负责维护套件的健康(7.6.1谁来写这些测试)。
运行缓慢和脆弱性是很大的问题。运行缓慢的测试会导致无法及时地反馈问题。可以通过并行化运行来改善。另外对这些测试进行管理和维护,删除重复的测试也是行之有效的方法(7.6.2测试多长时间)。
运行时间过长,导致问题修复周期变长,这也导致了发布数量的减少。最终导致功能大量堆积,部署时变更的内容太多,风险增大。应该尽可能地频繁发布小范围的改变(7.6.3大量的堆积)。
多个服务合并测试的端到端测试使得大家倾向于一起发布所有一起测试通过的服务,这会导致回归到一次部署多个应用的年代,导致服务之间的耦合并丧失独立于其它服务单独部署的能力,丢弃了微服务的一大优点(7.6.4元版本)。
端到端的测试应该把重心放到少量核心的场景上来。把任何在这些核心场景之外的功能放在相互隔离的服务测试中覆盖。团队之间需要就这些核心场景达成一致,并共同拥有。通过专注于少量的核心场景测试,我们可以缓解端到端测试的缺点。(7.7 测试场景,而不是故事)
7.8 拯救我们的消费者驱动的测试
在使用CDC(Consumer-Driven Contract,消费者驱动的契约)中,我们会定义服务(或生产者)的消费者的期望。例如客户服务的两个消费者帮助台和网络商店,使用CDC时会创建两个测试集合,分别针对帮助台和网络商店对客户服务的使用方式。CDC是对客户服务如何工作的期望,客户服务本身的所有下游依赖都可以使用打桩。这加快了这些测试的运行速度。另外这些测试的编写可以是消费者和生产协作来完成。
Pact是一个消费者驱动的测试工具。Pact不仅可以测试生产者的响应是否符合规则,还可以根据你指定的规范生成一个mock服务器,用来测试消费者。
CDC需要消费者和生产服务之间具有良好的沟通和信任,毕竟需要协同编写测试。在防止外部消费者被破坏,使用CDC尤为重要。
7.9 还应该使用端到端测试吗?
CDC的工具和更好的监控可以代替端到端测试。但在使用语义监控(semantic monitoring)的技术来监控生产系统时,用到端到端场景测试。可以把运行端到端测试当作把服务部署到生产环境的辅助轮,并慢慢减少对端到端测试的依赖。
7.10 部署后再测试
7.10.1 区分部署和上线
部署软件到生产环境,在有真正产生负载前运行测试,就可以发现特定环境中的问题。
蓝绿发布是使用这种方式的例子之一。假设要部署的版本是v456,那么在线上正常运行v123的同时,部署一份v456,但先不接受情况,而是对v456运行测试。等测试通过后再切换到v456,此时v123还会保留一小段时间以供快速回滚。
蓝绿发布的技术准备:1. 能够切换生产环境流量到不同的主机(集群),可以是DNS条目,也可以是负载均衡实现。2. 有足够的主机以支持同时运行两版本的微服务。
使用蓝绿发布可以降低风险,并且在遇到问题时可以快速恢复。另外使用蓝绿发布还可以做到零宕机部署。
7.10.2 金丝雀发布
金丝雀发布(canary releasing)是指通过将部分生产流量引流到新部署的系统,来验证系统是否按预期执行。例如验证新部署服务的接口响应延迟,错误率,甚至评估转化率、算法效果是否达到预期等等。(也称灰度发布?)
金丝雀发布时可以是引导部分流量请求到新版本,也可以将线上一部分流量复制到新版本。第二种方法在实际使用中会遇到较多的问题,特别是在请求幂等的情况下。
7.10.3 平均修复时间胜过平均故障间隔时间
有时花费相同的努力让发布变得更好,比添加更多的自动化功能测试更加有益。在Web操作的世界,这通常被称为平均故障间隔时间(Mean Time Between Failures, MTBF)和平均修复时间(Mean Time To Repair,MTTR)之间的权衡优化。
7.11 跨功能的测试
跨功能测试是对系统展现的一些特性的一个总括的术语,这些特性不能像普通的特性那样简单实现。它包括以下方面,比如一个网页可接受的延迟时间,系统能够支持的用户数量,用户界面如何让残疾人也可以访问,或者如何保障客户数据的安全。
性能测试
随着系统的拆分,跨网络边界的调用增加,相应的数据库等调用的数量也会增加,性能测试显得尤为重要。
在开始时,性能测试可以只运行在核心场景中,可以简单地大量并发端到端场景测试。
性能测试往往需要搭建与生产环境相当的实例数量才能测试出生产环境真正性能,但这通常很难。如若压测环境是等比例缩小的生产环境,则在结果推广到生产环境时需要更加注意。
每次构建后都运行性能测试不太现实,一般每天运行一个小范围的性能测试,每周运行一次大范围性能测试。频繁地运行性能测试可以在问题的初期就被发现,从而更容易解决。
需要关注性能测试的结果。可以给性能测试定制目标,未达到目标时显示为红色,并发出警告。
7.12 小结
优化快速反馈,并相应地使用不同类型的测试。
尽可能使用消费者驱动的契约测试,来替换端到端测试。
使用消费者驱动的契约测试,提供团队之间的对话要点。
尝试理解投入更多的努力测试与更快地在生产环境发现问题之间的权衡(MTBF与MTTR权衡的优化)。
8. 监控
监控小的服务,然后聚合起来看整体。
8.1 单一服务,单一服务器
对于这种简单的场景,首先需要监控主机本身,如CPU、内存等数据,在超出边界值时发出警告。可用的工具包括Nagios、New Relic等。
其次,需要查看服务上的日志,以定位问题所在。可以使用logrotate来对日志进行切割、清理等操作。
最后,对服务应用程序本身的监控。如通过web日志统计接口的响应时间、出现的错误等等。
8.2 单一服务,多个服务器
对于这种一个服务运行在多个主机上的场景,除了收集单台主机的监控数据,还需要将所有主机的数据聚合起来,以供分析。Nagios可以实现这些功能。例如当出现CPU过高时,如果是所有的主机都发生,则可能是服务的问题,如果只有一台主机发生,这可能是这台主机本身的问题。
对于日志来说,可以使用像ssh-multiplexers这样的工具在多台主机上运行相同的命令来处理。(ansible等工具也很好用)
对于响应时间这些监控,可以通过收集负载均衡的日志来实现(不过负载均衡也需要监控)。对于服务本身,我们需要了解健康服务是怎么样的,这样就可以在负载均衡上配置自动移除不健康的节点。
多服务多主机场景(8.3~8.4)
多个服务多台主机这样场景下的监控和问题定位,需要集中收集和聚合尽可能多的数据到我们的手上。(8.3 多个服务,多个服务器)
ELK(Elasticsearch/logstash/Kibana)是一个完整的解决方案。logstash可以解析多种日志文件格式,并将它们发送给下游系统进行进一步调查。Kibana是一个基于ElasticSearch查看日志的系统,你可以使用查询语法来搜索日志,它允许在查询时指定时间和日期范围,或使用正则表达式来查找匹配的字符串。Kibana甚至可以把你发给它的日志生成图表,只需看一眼就能知道已经发生了多少错误。(8.4 日志,日志,更多的日志)
8.5 多个服务的指标跟踪
为了能准确判断某个指标变化时是否是系统异常,需要长时间地收集系统的指标,直到浮现出清晰的模式。Graphite提供一个非常简单的API,允许你实时发送指标数据给它,并通过这个数据来生成图表等直观的展示方式。它还能通过控制历史数据的精度来控制这些数据的存储大小。
这些监控数据生成的趋势除了帮助我们了解系统的现状以及快速定位问题外,另外一个重要的好处就是做容量规划。
8.6 服务指标
除了操作系统相关的指标外,服务也要公开自己的指标。包括响应时间和错误率,以及业务相关的东西诸如注册人数,订单数量等信息。
这些指标能让我们了解有多少用户用了某些功能,以及用户如何使用某一个功能。这使得我们知道应该将重心放在哪些功能上,系统应该如何改进才能更满足用户的需求。另外这些数据可能在未来的某一时刻派上用场。
很多平台都存在一些库来帮助服务发送指标到一个标准系统中。例如基于JVM的Codahale的Metrics库(http://metrics.codahale.com/),以及各种语言下的Prometheus库,都可以实现此目的。
8.7 综合监控
综合监控是按照业务场景定期触发某一个事件,观察结果是否为预期的,这个监控确保系统行为在语义上的正确性,所以也称之为语义监控。例如在地理位置推荐服务中,修改一个用户的地理位置,观察这个用户是否出现在这个位置附近的推荐位上等等。
使用合成事务执行语义监控的方式,比使用低层指标的告警更能表明系统的问题。但低层次的指标有助于我们了解为什么语义监测会报告问题。
可以将端到端测试改造成语义监控,当然还需要注意数据的准备以及这些测试所带来的副作用。
8.8 关联标识
在微服务的世界里,一个功能的完成需要大量服务的配合,一个初始调用最终会触发多个下游的服务调用。关联这些调用在处理问题时了解调用上下文非常有帮助。
使用关联标识(ID)的方式,在第一个调用的地方生成GUID,并传给后续的调用,各个服务将此参数以结构化的方式写入到日志中。这样就可以通过这个标识查出所有的上下文调用情况。
可以使用像Zipkin这样的软件,但如果系统中有已经有日志聚合了,那更简单的方式应该是重用已经收集的数据。
传递关联标识时需要保持一致性,可以使用共享的、薄客户端库。为了让所有服务都能使用关联标识,这个客户端一定要主够薄而不至于让人产生负担感而拒绝使用,例如只需包装标准的HTTP客户端库,添加代码确保在HTTP头传递关联标识即可。
8.9 级联
级联故障特别危险。两个健康的服务只有通过语义监控才能发现两个服务之间的网络出现问题。为了解决这个问题,每个服务实例都应该追踪和显示其下游服务的健康状态,从数据库到其他合作服务。可以使用库实现一个断路器网络调用,以帮助你更加优雅地处理级联故障和功能降级(例如JVM上的Hystrix)。
8.10 标准化
监控这个领域的标准化是至关重要的。服务之间使用多个接口,以很多不同的方式合作为用户提供功能,你需要以整体的视角查看系统。应该尝试以标准格式的方式记录日志,可以提供预配置的虚拟机镜像,镜像内置logstash和collectd,还有一个公用的应用程序库,这使得与Graphite之间的交互变得非常容易。
8.11 考虑受众
收集数据主要目的是帮助我们完成任务。了解需要这些数据的受众的具体需求有助于我们明了应该收集哪些数据,以何种形式呈现出来。考虑以下因素:他们现在需要知道什么;他们之后想要什么;他们如何消费数据。
8.12 未来
目前大多数系统中不同的指标被孤立到不同的系统中,例如与应用级别的监控数据进入数据仓库,被分析成报表等非实时提供的报告。而系统指标如响应时间、错误率等存储在运维团队的存储中,提供实时监控报告。
为什么不能以同样的方式处理运营指标和业务指标?最终,两种类型的指标分解成事件后,都说明在X时间点发生了一些事情。如果我们可以统一收集、聚合及存储这些事件的系统,使它们可用于报告,最终会得到一个更简单的架构。
Riemann是一个事件服务器,允许高级的聚合和事件路由,所以该工具可以作为上述解决方案的一部分。Suro是Netflix的数据流水线,其解决的问题与Riemann类似。Suro明确可以处理两种数据,用户行为的相关指标和更多的运营数据(如应用程序日志)。然后这些数据可以被分发到不同的系统中,像Storm的实时分析、离线批处理的Hadoop或日志分析的Kibana。
许多组织正在朝一个完全不同的方向迈进:不再为不同类型的指标提供专门的工具链,而是提供伸缩性很好的更为通用的事件路由系统。这些系统能提供更多的灵活性,同时还能简化我们的架构。
8.13 小结
对每个服务而言:
-
最低限度要跟踪请求响应时间。做好之后,可以开始跟踪错误率及应用程序级的指标。
-
最低限度要跟踪所有下游服务的健康状态,包括下游调用的响应时间,最好能够跟踪错误率。一些像Hystrix这样的库,可以在这方面提供帮助。
-
标准化如何收集指标以及存储指标。
-
如果可能的话,以标准的格式将日志记录到一个标准的位置。如果每个服务各自使用不同的方式,聚合会非常痛苦!
-
监控底层操作系统,这样你就可以跟踪流氓进程和进行容量规划。
对系统而言:
-
聚合CPU之类的主机层级的指标及应用程序级指标。
-
确保你选用的指标存储工具可以在系统和服务级别做聚合,同时也允许你查看单台主机的情况。
-
确保指标存储工具允许你维护数据足够长的时间,以了解你的系统的趋势。
-
使用单个可查询工具来对日志进行聚合和存储。
-
强烈考虑标准化关联标识的使用。
-
了解什么样的情况需要行动,并根据这些信息构造相应的警报和仪表盘。
-
调查对各种指标聚合方式做统一化的可能性,像Suro或Riemann这样的工具可能会对你有用。
我还试图描绘了系统监控发展的方向:从专门只做一件事的系统转向通用事件处理系统,从而可以全面地审视你的系统。
9. 安全
9.1 身份验证与授权
身份验证是指需要进行身份验证的主体(人或者事)通过用户名密码等方式确认这个主体是谁的过程。
授权机制是指将主体映射到他可以进行的操作中,通过身份验证后可以获得主体的信息,确定其可以进行的操作。
在单块系统中,应该程序本身会处理身份验证和授权,一般都集成到web框架中。但在分布式系统中,需要考虑更高级的方案,使用单一的标识且只需进行一次验证即可。
常见的单点登录实现
SSO(Single Sign-On,单点登录)解决方案是在主题试图访问一个资源时会被定向到一个身份提供者那里进行身份验证,身份验证的方式可以是用户名和密码也可以是更高级的双重身份验证。验证通过之后会将信息返回给服务提供者,由服务提供者来决定主体是否可以访问资源。身份提供者可以是外部的(如谷歌提供了一个OpenID Connect身份提供者),也可以是内部的(LDAP、AD活动目录)。
SAML和OpenID Connect是两个重要的解决方案。SAML是一个基于SOAP的标准,虽然有很多工具支持,但用起来还是非常复杂。OpenID Connect已经成为了OAuth 2.0具体实现中的一个标准,也是未来发展的方向。
单点登录网关
为了避免所有服务都处理如何重定向到身份提供者以及共享代码库造成的耦合,使用一层单点登录网关来专门解决处理身份验证重定向的问题,并将处理完成的信息通过诸如HTTP头的方式传到后端服务当中(可以使用Shibboleth工具)。
这种方式还需要注意在排查问题以及类生产环境搭建上造成的困难,需要提供一些工具简化这些工作。另外在安全方面虽然网关可以集成大量的安全措施,但是需要从网络边界,到子网,到防火墙,到主机,到操作系统,再到底层硬件等各方面的深度防御,防止网关出现问题后所造成的影响。
统一入口网关由于其特殊的位置,会在往后承担越来越多的功能,这会形成一个庞大的耦合点,并且越容易受到攻击。
细粒度的授权
网关可以提供相当有效的粗粒度的身份验证,而对于那种细粒度的身份验证,留给微服务内部处理会比较合适。
9.2 服务间的身份验证与授权
最简单的方式是在边界内对服务的任何调用都是默认可信的。但在某个入侵到网络中的任意一台主机时,将对典型的中间人攻击基本没有任何防备。
HTTP基本身份验证时用户名和密码是放在HTTP头中的,但这并不安全,应该使用HTTPS代替。但如果基于HTTPS,需要考虑证书的签发与撤销问题,以及像Vanish/Squid这种反向代理对SSL之上的流量无法缓存的问题,在已经现存SSO方案的情况下,如何整合的问题。
如果已经在使用SAML或者OpenID Connect方案了,那么,也可以在服务之间的交互中使用它。这需要为客户端创建服务账号,限制其使用范围,每个微服务有自己的一组凭证,在泄漏发生时,可以很容易地撤销。但这个方案需要解决客户端密码存储的问题、身份验证客户端代码的实现。
使用客户端证书的方式是通过给每一个客户端安装证书的方式,使得服务端可以验证客户端的真实性。使用这种方式相比与只使用服务端证书方式上更加复杂,主要提供在问题的调试、证书的管理包括撤销和补发等等。只有在特别关注所发数据的敏感性,或无法控制发送数据所使用的网络时,才考虑使用这种技术,即在通过互联网发送非常重要的数据时,才使用安全通信。
为了解决安全通信,但又不想使用HTTPS这种高开销的方案,可以使用HMAC(Hash-based Message Authentication Code,基于哈希的消息码验证)。它是通过将请求参数与私钥同时进行哈希计算使得请求无法被篡改后重放。但其缺点有三:1. 密钥共享问题,硬编码无法撤销,非硬编码则需要一个非常安全的协议;2. 没有统一的标准,导致不同的实现方式,但近年来像JWT这样的统一实现不断完善与推广,未来有可能会被解决;3. 虽然请求无法篡改,但传输的数据还是可见的。
像Twitter、谷歌、Flickr和AWS这样的服务商,提供的所有公共API都使用API密钥。API密钥允许服务识别出是谁在进行调用,然后对他们能做的进行限制(如资源访问限制,调用者限速等)。API密钥重点关注的是对程序来说的易用性,可以使用类似HMAC的方式共享密钥,也可以使用公私钥。
在外部调用分发到多个内部服务调用中时,需要警惕混淆代理人安全漏洞。它是指攻击者登录成功之后去获取其他人的信息,如果没有防范措施,导致其它人的信息泄漏。对于这种问题,没有比较简单的解决方案,可以在为前端服务的后端里去做限制,但有可能导致这个限制出现在多个服务中,如果在服务本身上做限制,又会限制此服务所提供的能力。这个是一个权衡的问题。
9.3 静态数据的安全
数据加密尤其是敏感数据加密是一种责任,它以确保即使攻击者获取到数据,也无法读取数据内容。
安全信息的保护是多种多样的,但无论采用哪一种,都要牢记以下这些因素:
-
使用众所周知的加密算法,不要试图实现自己的加密算法,大部分语言都有实现好的通用加密算法库。对于数据加密,推荐使用AES-128或AES-256,对于密码,可以使用一种叫作加盐密码哈希(salted password hashing)的技术。
-
密钥的存储需要与数据分开,可以使用单独的安全设备来加密和解密数据,也可以使用单独的库或者密钥管理系统来管理这些密钥。有些数据库提供了原生的加密技术,无论如何,在使用这些技术时需要了解其细节来确保它真的是安全的(如InnoDB 表空间加密)。
-
一般需要选择重要且敏感的数据进行加密,而非将所有数据加密。另外对于加密的数据,在写入日志的方式,解密所需要的开销都要有清晰的认识。
-
按需解密:第一次看到数据时就对其进行加密,后续在需要时进行解密,确保解密后的数据不会存储在任何地方。
-
加密的数据一般需要备份,确保备份的数据也是加密的数据。
9.4 深度防御
防火墙:有一个或多个防火墙是一个非常明智的预防措施,例如在本机上使用iptables,设置允许的应用程序定制的出口和入口,而外围的防火墙则进行更一般的访问控制。
日志:聚合多个系统的日志可以更快地发现攻击行为,但请记住敏感数据不要存储到日志文件中。
入侵检测(IDS)和入侵预防(IPS)系统:这两个系统是在通过了防火墙的流量中寻找可疑的行为。IDS当发现网络和主机上的可疑行为时会报告问题。IPS也会监控可疑性为并阻止它发生。
网络隔离:某一些服务提供商提供了子网划分功能,可以将微服务放入不同的子网,通过制定互联规则可以限制网络的互相访问甚至可以将流量路由到代理中。一般基于团队的所有权或者风险水平来进行网络分段。
操作系统:1. 给用户尽量少的权限;2. 定期为软件打补丁,这就需要一些自动化的软件来完成工作,如Ansible,Puppet等等;3. 对于Linux操作系统,像AppArmour、SELinux、以及GrSecurity这些安全模块可以关注其发展。
9.5 一个示例
在这个例子中客户会通过浏览器在我们网站上购物。对于无需安全保护的内容,可以使用HTTP以保证缓存和性能,而对于有安全需要的,登录后才能访问所有内容,都使用HTTPS进行传输。注意不能在同一域名下混用HTTPS和HTTP,否则对于Cookie中的内容就可以在HTTP中被中间人劫持,相当于泄漏了HTTPS的内容。
第三方版税公司会间断性地获取已下载音乐的记录,这个信息我们需要小心地保护,以防止被竞争对手获取,所以我们坚持让第三方使用客户端证书,确保请求主体的合法性。
对于产品目录数据的聚合,我们希望尽可能广泛地共享这些信息,同时也不希望这个信息被滥用,使用API密钥是一个绝佳的选择。
在网络范围内部,可以使用HTTPS,但这里我们花费更多精力通过防火墙,流量安全检测等技术来夯实网络边界上的防护。另外对于敏感的客户数据,我们进行加密,即使数据库所在的机器被攻破数据被下载走,也需要密钥才能解开。
9.6 保持节俭
随着存储成本的降低和性能的增加,数据正在变得越来越多,这些数据越来越被企业当成宝贵的财产。
但从另一个角度考虑,我们也可以只存储完成业务运营或满足当地法律所需要的信息。毕竟不存储他就没有人可以偷走他或者拿走他(政府部门)。
9.7 人的因素
人为因素也是安全保障的重要环节,例如离职访问凭证撤销,预防社会工程学的攻击等等。从最恶意地位置考虑系统所需要采取的防护是一个很好的方法。
9.8 黄金法则
不要实现自己的加密算法。不要发明自己的安全协议。重新发明轮子在安全领域不仅浪费时间,而且会带来直接的危害。
9.9 内建安全
培养开发人员的安全意识,提高每个人对安全问题的普遍意识。让人们熟悉OWASP十大列表和OWASP的安全测试框架,是一个很好的起点。寻找安全专家的帮助也是一个重要渠道。
自动化工具也有助于探测系统漏洞,例如ZAP(Zed Attack Proxy),针对Ruby的Brakema等。这些工具很容易集成进日常的CI构建中。
微软的安全开发生命周期(Security Development Lifecycle)也有一些很好的模型来帮助交付团队内建安全。但整体流程较复杂,可以参考此。
9.10 外部验证
外部验证可以模拟真实世界中的意图。这个外部验证实施者信息安全团队,可以是内部专门的部门,也可以是第三方公司。同时也需要考虑验证的频率,范围等问题。
10. 康威定律与系统设计
梅尔·康威于1968年4月论文上指出如下这句话,被称之为康威定律:任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。
埃里克·S·雷蒙德在《新黑客字典》中总结这一现象时指出:“如果你有四个小组开发一个编译器,那你会得到一个四步编译器”。
本章介绍组织结构对系统设计的影响。
组织结构与系统设计相关的证据(10.1~10.3)
有一种划分将软件系统所属的组织分为紧耦合组织(如商业产品公司)和松耦合组织(如分布式开源社区)。组织的耦合度越低,其创建的系统的模块化就越好,耦合也越低;组织的耦合度越高,其创建的系统的模块化也越差。
微软研究了Vista产品中组织结构与软件质量的关系,发现与组织结构相关联的指标和软件质量的相关度最高。(10.1 证据)
Amazon有一个著名的“两个比萨团队”理论,即没有一个团队应该大到两个比萨不够吃。帮助小团队对服务的整个生命周期负责,是驱动Amazon开发AWS的一个主要原因。
Netflix从一开始,就确保其本身是由多个小而独立的团队组成,以保证他们创建的服务也能独立于彼此。这确保了系统的架构可以快速地优化。实际上,Netflix为了想要的系统架构,才设计了这样的组织结构。(10.2 Netflix和Amazon)
以上例子表明了组织结构对系统的性质和质量确实有着深刻的影响。我们需要了解不同的组织形式,以及他们对我们系统设计的影响。(10.3 我们可以做什么)
10.4 适当沟通途径
对于一个简单的团队负责一个单一的服务。服务内部是大量细粒度的方法调用,这对应于团队内部可以进行频繁、细粒度的沟通。这样团队对于更改和重构更容易进行,团队成员通常都很有责任感。
想象两个异地的团队对同一个服务共同拥有所有权。这使得进行细粒度的沟通非常困难。这将导致人们要么想方设法降低协调/沟通成本,要么停止更改。而后者正是导致我们最终产生庞大的、难以维护的代码库的原因。而这种情况一般会发展成为在代码库中形成与组织结构相匹配的边界。
以上例子给出的启示:应该分配单个服务的所有权给可以保持低成本变化的团队。
另外,一个拥有许多服务的单个团队对其管理的服务会倾向于更紧密地集成,而这种方式在分布式组织中是很难维护的。
10.5 服务所有权
服务所有权是一般是指拥有服务的团队负责对该服务进行更改,但更多的团队将所有权延伸到从应用程序的需求、构建、部署到运维的方方面面。
所有权程度的增加会提高自治和交付速度,赋予团队更多的权力和自治,也使其对工作更负责
10.6 共享服务的原因
难以分割:拆解单块系统的成本太高,这点可参考第5章的方法。
特性团队:即一个小团队负责开发一系列特性所有的功能,这些特性跨越服务边界。这种结构促使团队保持关注在最终的结果上,并确保工作是集成起来的,避免了跨多个不同的团队试图协调变化的挑战。
交付瓶颈:假如产品上需要一个彩铃类型的音乐,需要改变产品目录服务,但恰巧这个微服务的负责团队由于某些原因无法支持。此时有以下处理方式:1. 等待团队释放出资源后再开发,这影响产品的交付;2. 其它团队的人来支持,这取决于是否使用标准化的技术栈和编程范式(这有可能导致问题)以及团队所处的地理位置;3. 将铃声服务拆出去,并列铃声和产品目录两个服务,这种做法是否有意义取决于铃声相关的功能大小以及它的独立性。以上处理方式都不是特别理想,后面会介绍另外一种方式。
10.7 内部开源
在标准的开源项目中有一小部分人被认为是核心提交者,他们对代码库负责。其他人要修改代码库要么让核心提交者来帮忙修改,要么自己修改完发起pull request。在内部开源项目中,也使用这种模式工作。
内部开源项目中受信任的提交者(核心团队)需要对提交的代码进行审批,以确保符合此代码库的规范以及实现的合理性。这使得团队需要花费时间与提交者进行沟通以及代码审批上,这就需要权衡决定这么做是否有更大的意义。
服务在不稳定期或不成熟期,往往无法让非核心提交者进行修改,因为在此状态下无法得知什么样的代码是好的。等到服务成熟稳定了,就是开源让其它人贡献代码的好时机。
为了更好地支持内部开源模型,你需要一些工具。这个工具可能需要支持pull request,版本控制,代码评审系统,CI/CD流程处理,以及构件物仓库等。gitlab是一个好的工具。
10.8 限界上下文和内部结构
如前文所述,我们以限界上下文来定义服务的边界。因此,我们希望团队也与限界上下文保持一致。这有很多好处。首先,团队会发现它在限界上下文内更容易掌握领域的概念,因为它们是相互关联的。其次,限界上下文中的服务更有可能发生交互,保持一致可以简化系统设计和发布的协调工作。最后,在交付团队与业务干系人进行交互方面,它有利于团队与此领域内的一两个专家创建良好的合作关系。
10.9 孤儿服务
由于微服务本身就很小,所以很有可能长时间不进行更改。如果团队结构与组织上下文是一致的,那么这样的服务也会拥有实际所有者。做为微服务的好处之一,当需要更改该服务以添加新的功能但很难修改时,重写这个服务也不会花太长的时间。当然如果使用了多个技术栈并且对孤儿服务的技术栈不熟悉时,挑战会加大。
10.10 案例研究:RealEstate.com.au
REA的核心业务是房地产,并拥有多条业务线(本土业务以及海外业务等)。
每条业务线团队负责自己创建的服务的整个生命周期,包括构建、测试、发布和运维,甚至弃用。另外有一个核心交付服务团队,为这些团队提供建议、指导和工具来帮助他们完成工作。
一个业务线内,服务间可以不受任何限制地以任何方式来通信,只要团队确定的服务守护者认为合适即可。但是在业务线之间,所有通信都必须是异步批处理,使得每条业务线在自身的行为和管理上有很大的自由度,这是几个严格的规则之一。
这些组织的系统架构和组织结构对变化都有着很好的适应性,这能够产生巨大的效益,因为这样的组织改进了团队的自治性,并且能够加快新需求和新功能的发布速度。
10.11 反向康威定律
对康威定律反向考虑,系统设计是否可以改变组织?某些历史原因划分的子系统一直存在,即使团队更迭,架构已经不适合现有的业务,但为了适应这些子系统的存在,组织结构也会或多或少往这些子系统上靠齐。
10.12 人
“不管一开始看起来什么样,它永远是人的问题。”——杰拉尔德·温伯格,咨询第二定律。
从单块系统到微服务,开发人员需要意识到像跨网络边界调用及隐式失败等隐式问题、扩展使用多种语言、增加对运维的意识、增加责任感增加自洽。员工所承担的变化需要一个过程,我们需要更多关心员工的感受,以及阐明每个人在微服务中的责任以及这些责任的重要性。
11. 规模化微服务
11.1 故障无处不在
从统计学来看,规模化后故障将成为必然事件。如果我们能够拥抱故障,那么就能够游刃有余地管理系统。我们不应该花特别大的精力在使用流程和控制来试图阻止故障的发生,而应该更多的费心如何更加容易地在第一时间从故障中恢复过来。
11.2 多少是太多
从用户的角度出发,来确认服务可以容忍多少故障,或者系统需要多快。无论如何,可以先尝试理解以下这些需求。
响应时间/延迟:响应的时长,以及负载增加对响应时长的影响。鉴于网络的不稳定性,将监控的响应目标设置成一个给定的百分比是很有用的,例如:我期望这个网站,当每秒处理200个并发连接时,90%的响应时间在2秒以内。
可用性:能否接受服务出现故障,停机时间等指标。
数据持久性:多大概率数据丢失的可接受性,数据存储的时长等。
一旦有这些需求,就需要对这些需求系统性地持续测量。
11.3 功能降级
构建一个基于多个微服务的弹性系统,需要能够安全地降级功能。如果这些微服务中的任何一个宕掉,都会导致整个系统不可用,那还不如使用单块系统。
例如,购物车服务不可用,那页面上的其它元素照样正常展示,只是购物车按钮被隐藏掉或者显示马上回来图标。
我们需要考虑当某一个服务不可用时应该做什么,这个决策不一定是技术上的,而是需要在理解业务上下文后才能做决定。
11.4 架构安全措施
有一些模式,组合起来被称为架构性安全措施,它们可以确保如果事情真的出错了,不会引起严重的级联影响。这些都是你需要理解的非常关键的点,我强烈建议在你的系统中把它们标准化,以确保不会因为一个服务的问题导致整个系统的崩塌。例如一个缓慢的服务可能拖垮整个系统,引起雪崩,对于这种情况,我们需要考虑以下这些问题:正确地设置超时;实现舱壁隔离不同的连接池,实现一个断路器。
11.5 反脆弱组织
某些公司会定期在生产环境模拟故障发生,来确保其系统的容错性。混乱猴子(Chaos Monkey)项目会在一天的特定时段随机停掉服务器或机器,包括随机关闭整个可用区、注入网络延迟等,在你的生产环境上释放猴子军队来终极验证你的系统是否真的健壮。
虽然并不是每一个公司都能做到这样的极致,但重要的是,理解分布式系统所需的思维方式上的转变:事情将会失败。那么我们需要做什么来应对系统故障呢?
超时:给所有的跨进程调用设置超时,并选择一个默认的超时时间。当超时发生后,记录到日志里看看发生了什么,并相应地调整它们。
断路器:即使正确设置了超时,还是会拖慢甚至拖垮系统。而断路器则可以在下游失败一定次数之后快速地失败,并且会不断探测其是否恢复,恢复之后将重置断路器。在断路器中,需要定义什么是失败行为(如HTTP的错误码)、配置启动条件、配置失败请求处理策略(堆积重试还是快速失败),甚至你可以需要的时候手动开启断路器来停掉某个服务或者测试服务故障时的表现。
舱壁:舱壁(bulkhead)是把自己从故障中隔离开的一种方式。舱壁可以是多种形式,例如为每一个下游设置独立的链接池,把功能分离成微服务等。可以把断路器看作一种密封一个舱壁的自动机制,它不仅保护消费者免受下游服务问题的影响,同时也使下游服务避免更多的调用。Netflix的Hystrix库是一个基于JVM的断路器,附带强大的监控。
隔离:服务间加强隔离使得一个服务不受另外一个服务的健康状态影响,而且服务的拥有者之间需要更少的协调,团队更自治,可以更自由地管理和演化服务。
11.6 幂等
对幂等操作来说,其多次执行所产生的影响,均与一次执行的影响相同。这对于重试、错误恢复等场景会非常有用。例如对于增加积分操作,可以附带上对应的订单号,如果已经处理过了这个订单就不再处理。
11.7 扩展
扩展是为部分失败时备份以及处理更多请求、减少延迟准备的。以下介绍常用的通用扩展技术:
更强大的主机
更快的CPU和更好的I/O的机器,通常可以改善延迟和吞吐量,这种扩展被称之为垂直扩展。这种扩展的费用较高,而且需要程序上能充分利用这些资源;如果正在使用虚拟化供应商的服务,并且它允许你轻松地调整机器的大小时,这可能是一个可以快速见效的很好的方式。
拆分负载
将位于同主机上的多个服务拆分出来,减少互相影响,并提升吞吐量和伸缩性。另外一种就是拆分服务,例如将线上服务和报表这两个对可用性要求完全不一样的服务拆分开来。
分散风险
如果运行在虚拟主机上,确保它们是分散运行在不同的物理机上。并且确保不要让所有的服务都运行在同一个数据中心的同一个机架。对于供应商,也可以使用多家混合的方案,确保不会因为一家公司出错而受影响。
负载均衡
负载均衡器包括硬件设备,也有像mod proxy这样基于软件的负载均衡器。他们基本一些算法将请求转到后端的一个或者多个实例,并在实例不健康情况下移除它。
负载均衡往往还提供了像HTTPS终止这样的功能。
AWS以ELB(Elastic Load Balancers,弹性负载均衡器)的形式提供HTTPS终止的负载均衡,并使用安全组或VPCs(Virtual Private Clouds,私有虚拟云)来实现VLAN,所有服务位于VLAN内部,VLAN外部跟微服务通信的唯一方式是通过HTTPS。
负载均衡器允许我们以对服务的所有消费者透明的方式,增加更多的微服务实例。这提高了我们应对负载的能力,并减少了单个主机故障的影响。
基于worker的系统
基于worker的系统会将所有的实例工作在一些共享的待办作业列表上,它非常适合于批量或异步作业,例如生成报表,发送邮件等。
该模型同样适用于负载高峰,你可以按需增加额外的实例来处理更多的负载。只要作业队列本身具有弹性,该模型就可以用于改善作业的吞吐量,也可以改善其弹性,因为它很容易应对worker故障(或worker不存在)带来的影响。作业有可能需要更长的时间,但不会丢失。所以这个模式对于保存待办作业列表的系统有一定可靠性要求,可以使用zookeeper之类的软件来处理。
重新设计
重新设计可能意味着拆分现有的单块系统、挑选新的数据存储方式,以便更好地应对负载、采用新的技术、采用新的部署平台、改变整个技术栈等等。
当达到特定伸缩阈值时,必须重新设计架构。但这并不意味着在一开始就要进行这样的设计。
11.8 扩展数据库
扩展无状态的服务相对简单,扩展数据库就需要考虑更多的因素了。
服务的可用性和数据的持久性:这是两个概念,对于数据库来说一般我们有主数据库和副本,数据的持久性是指即使主数据库失效了,也会有副本在,数据是安全的;而可用性是指这个副本可以在主数据库失效时有机制可以上升为主数据库使得服务可以继续可用。
扩展读取:很多服务以读取为主,现在主流的RDBMS都提供了主从复制机制。副本除了作为数据备份外,还可以分发读取操作,但这么做的话需要考虑最终一致性的数据问题。另外一种做法是使用缓存,他可以提供更显著的性能改善,而且工作量往往更少。
扩展写操作:一般采用分片的方式。但这对查询不是特别友好,可能需要聚合各个分片的查询结果。另外一个问题是扩展节点,近年来越来越多的系统支持了不停机增加分片,它们会将重新分配数据放在后台执行。写入操作会扩展写容量,但不会提升弹性,节点故障仍然会导致一部分数据不可用。Cassandra在这方面提供额外的功能,可以确保数据在一个环(ring, Cassandra的术语,来描述它的节点集合)内复制到多个节点。
共享数据库基础设施:现有的RDBMS都会包含有数据本身和模式,不同的微服务可以使用同一个库上不同的模式,这可以减少数据库运行的机器数。但这也引入了一个重要的单点故障,数据库出现问题导致多个微服务受到影响,需要权衡利弊。
CQRS:Command-Query Responsibility Segregation,命令查询职责分离模式,系统的一部分负责获取修改状态的请求命令并处理它,而另一部分则负责处理查询。这使得命令和查询部分可能是在不同的服务或在不同的硬件上,完全可以使用不同类型的数据存储。可参考此与此
11.9 缓存
客户端缓存:由客户端缓存结果并决定何时获取新副本,这可以大大减少网络调用的次数,但如果要改变缓存的方式,需要消费者全都修改,另外也无法主动让数据失效。
代理缓存:如反向代理、CDN等,代理层虽然会额外引入网络跳数,但缓存带来的好处足以拟补。
服务端缓存:例如使用Redis等,更高的可控性,可跟踪缓存的失效情况以及缓存命中率。
HTTP缓存:有cache-control,Expire指令可以控制资源的缓存时间;使用Entry Tag(ETag)可以实现条件GET,例如使用If-None-Match:o5t6fkd2s请求头获取资源,服务如果发现ETag匹配,则返回304未修改,否则返回200及数据。参考此rfc文档。
为写使用缓存:即为瞬间大量的写操作,先写本地缓存,再以批量、合并后的数据写入最终的数据中,一般也可以队列来处理。
为弹性使用缓存:缓存可以在依赖服务不可用时仍然返回缓存中的值,一般来说过期的数据总比没有数据要好。更激进点的做法是自动爬取现有网站的数据,在意外停机时可以使用爬取的备份数据。
隐藏源服务:缓存同时失败(可能是缓存服务问题)时大量穿透到源服务会导致源服务不堪重负。使用源服务主动更新缓存的形式可以避免这样的事情。另外一种做法是在出现失效时限制穿透到源服务的数量,并增加快速失败机制。
保持简单,避免使用太多的缓存,因为越多缓存,数据就越可能失效。缓存可以很强大,但是你需要了解数据从数据源到终点的完整缓存路径,从而真正理解它的复杂性以及使它出错的原因。
11.10 自动伸缩
有多种方式:1. 在业务高的时候增加实例,在低的时候减少实例,一般业务上高低点都有时间规律,很容易弄成按时间自动化;2. 另外一种响应式地进行负载调整,在负载增加或者节点故障时来增加额外的实例。3. 用于故障响应,例如在某个实例宕掉后自动启动新的实例。
11.11 CAP定理
在分布式系统中有三方面需要彼此权衡:一致性(consistency)、可用性(availability)和分区容忍性(partition tolerance)最多只能保证三个中的两个。
一致性是当访问多个节点时能得到同样的值。可用性意味着每个请求都能获得响应。分区容忍性是指集群中的某些节点在无法联系后,集群整体还能继续进行服务的能力。
AP:保证可用性和分区容忍性,牺牲强一致性,使用最终一致性。
CP: 保证一致性和分区容忍性,牺牲可用性。要实现分布式系统的一致性具有挑战性,选择一个提供这些特性的数据存储或锁服务如Consul,ETCD等更加明智。
CA: 作者给出CA的解释是单机的系统,不存在分布式的CA系统。但其实对于P的理解还有各种说法,例如P可能指网络上的丢包,但他是客观存在的,所以无法放弃。
如何选择AP和CP呢,这其实是一个对于业务场景做一些权衡,在一个系统中的一部分可以保证AP,另一部分可以保证CP。像Cassandra它允许为每个调用做不同的权衡,可以指定读取副本的数量以保证不同程度的一致性。
另外CAP理论也会有一定的局限性,所以现实中也出现了其它的衍生理论如BASE(Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)),它其实可以看做是对CAP权衡的结果。
11.12 服务发现
服务发现包括两部分,1. 提供让实例注册自己的机制,2. 让其它服务找到此服务。
DNS是最简单的解决办法。
为了区分不同的环境,一般会在不同的环境中使用不同的域名,例如这样的命名规范:<服务名>-<环境>.musiccorp.com这样的结构;另外也可以通过不同环境使用不同的DNS服务器来达到区分环境的目的,这样做的好处是可以统一域名。环境>服务名>
DNS实现容易,但也有缺点:一般在网络的各个环节都有DNS的缓存,所以修改DNS的条目无法做到实时生效。可以通过将DNS解析到负载均衡器的方式来绕过这个问题,一般的负载均衡器都会提供快速增加和删除所挂载主机的功能。
11.13 动态服务注册
解决DNS在高度动态的环境缺陷,催生了大量提供服务注册和服务发现功能的系统。
Zookeeper:它被用于众多使用场景中,包括配置管理、服务间的数据同步、leader选举、消息队列和命名服务。Zookeeper能保证数据在多个节点间进行安全的复制,并且当节点故障后仍然能保持一致。Zookeeper的核心是提供了一个用于存储信息的分层命名空间,客户端可以在此层次结构中,插入新的节点,更改或查询它们;此外,它们可以在节点上添加监控功能,以便当信息更改时节点能够得到通知。这意味着,我们可以在这个结构中存储服务位置的信息,并且可以作为一个客户端来接收变更消息,以此来实现服务注册和发现的目的。
Consul:相比于Zookeeper,Consul提供RESTful的HTTP服务,并且提供了现成的DNS服务器。其除了服务注册与发现功能外,还提供了节点的安全检查功能。
Eureka:其做为服务中心,可以管理各种服务功能包括服务的注册、发现、熔断、负载、降级等。
也可以构建自己的服务注册系统,无论选择哪个系统,都需要保证有工具能让这些注册中心上生成报告和仪表盘,显示给人看,而不仅仅是给电脑看。
11.14 文档服务
Swagger:Swagger可以通过使用API描述文件来产生一个很友好的Web用户界面,使你可以查看文档并通过Web浏览器与API交互。Swagger提供了不同语言的支持,使得服务可以提供相应格式的附属文件,即可自动生成公开的服务文档。
HAL和HAL浏览器:本身是一个标准,用来描述我们公开的超媒体控制的标准。超媒体控制是一种方法,它允许客户逐步探索我们的API来使用服务,并且其耦合度比其他集成技术都低
12. 总结
12.1 微服务的原则
让我们回顾一下微服务的原则,如上图。你可以选择全部采用这些原则,或者定制采用一些在自己的组织中有意义的部分。但请注意,组合使用这些原则的价值:整体使用的价值要大于部分使用之和。所以,如果决定要舍弃其中一个原则,请确保你明白其带来的损失。
围绕业务概念:建模经验表明,围绕业务的限界上下文定义的接口,比围绕技术概念定义的接口更加稳定。针对这个领域的系统如何工作进行建模,不仅可以帮助我们形成更稳定的接口,也能确保我们能够更好地反映业务流程的变化。使用限界上下文来定义可能的领域边界。
接受自动化文化:在规模化微服务中不得不管理大量的服务。解决这个问题的一个关键方法是,拥抱自动化文化,如自动化测试、自动化部署、自动化扩容缩容等等。
隐藏内部实现细节:为了使一个服务独立于其他服务,最大化独自演化的能力,隐藏实现细节至关重要。只共享限界上下文情况下需要共享的内容,隐藏不必要的实现细节、数据库等。
让一切都去中心化:为了最大化微服务能带来的自治性,我们需要持续寻找机会,给拥有服务的团队委派决策和控制权。在这个过程初期,只要有可能,就尝试使用资源自助服务,允许人们按需部署软件,使开发和测试尽可能简单。
可独立部署:请记住,你可以更改单个服务,然后把它部署到生产环境,无需联动地部署其他任何服务,这应该是常态,而不是例外。你的消费者应该自己决定何时更新,你需要适应他们。
隔离失败:请确保正确设置你的超时,了解何时及如何使用舱壁和断路器,来限制故障组件的连带影响。如果系统只有一部分行为不正常,要了解其对用户的影响。知道网络分区可能意味着什么,以及在特定情况下牺牲可用性或一致性是否是正确的决定。
高度可观察:通过注入合成事务到你的系统,模拟真实用户的行为,从而使用语义监控来查看系统是否运行正常。聚合你的日志和数据,这样当你遇到问题时,就可以深入分析原因。而当需要重现令人讨厌的问题,或仅仅查看你的系统在生产环境是如何交互时,关联标识可以帮助你跟踪系统间的调用。
12.2 什么时候你不应该使用微服务
-
你越不了解一个领域,为服务找到合适的限界上下文就越难。服务的界限划分错误,可能会导致不得不频繁地更改服务间的协作,而这种更改成本很高。所以,如果你不了解一个单块系统领域的话,在划分服务之前,第一件事情是花一些时间了解系统是做什么的,然后尝试识别出清晰的模块边界。
-
从头开发也很具有挑战性。不仅仅因为其领域可能是新的,还因为对已有东西进行分类,要比对不存在的东西进行分类要容易得多!因此,请再次考虑首先构建单块系统,当稳定以后再进行拆分。
-
当微服务规模化以后,你面临的许多挑战会变得更加严峻,所以服务的基础设施,如自动化程序、监控报警等需要不断完善才能更好的使用微服务。
12.3 临别赠言
微服务架构会给你带来更多的选择,也需要你做更多的决策。相比简单的单块系统,在微服务的世界里,做决策是一个更为常见的活动。我可以保证,你总会在一些决策上出错。既然知道了我们难免要做一些错事,那该怎么办呢?嗯,我会建议你,尽量缩小每个决策的影响范围。这样一来,如果做错了,只会影响系统的一小部分。学会拥抱演进式架构的概念,在这种概念下,系统会在你学到一些新东西之后扩展和变化。不要去想大爆炸式的重写,取而代之的是随着时间的推移,逐步对系统进行一系列更改,这样做可以保持系统的灵活性。
希望到目前为止,我给你分享了足够多的知识和经验,能帮助你决定微服务是否适合你。如果微服务适合你,我希望你把它看作一个旅程,而不是终点。逐步前行。一块块地拆分你的系统,逐步学习。习惯这一点:从很多方面来说,持续地改变和演进系统,这条规则比我在本书中分享给你的任何一个知识都要重要。变化是无法避免的,所以,拥抱它吧!
笔者的思考与启发
演化式架构师,着眼于大方向上的规划,适应变化,而不应该从一开始设计出一个完美的系统。
在组内建立战略目标,原则和实践
开发维护代码模板
技术债务管理
架构师的治理,不仅仅是服务治理,还包括对目标和实现进行监督
好的服务甚至好的代码:松耦合、高内聚
BFF模式,类似与微博
单块系统的拆分、数据库的拆分、报表系统的处理等
在白板上设计微服务,运行用例,分析问题
自动化的重要性
端到端的测试,如果有专门的测试团队来负责,则结果可能是灾难性的
应该频繁地发布小范围的改动(与实践不同,有待考虑)
通过监控数据和压力测试做好容量规划
深度防御可以做哪些事情
赋予团队更多的权力和自治,也使其对工作更负责
内部开源需要有核心提交者和外部提交者
名词快速解释
领域驱动设计
DDD随着微服务的流行又重新回到了大家的视野,他是将整个需求分成底层的各个专有领域,并通过应用层将底层的这些领域专用服务整合起来提供给最上层用户层使用。它其实对某一专项业务的高度抽象,使其成为一个微服务,提供给上层应用层使用。
上图来源于此
中台
在公司发展到一定阶段,会有多条业务线或者项目并行,其中难免有很多公共的组件部分,例如用户系统、交易系统、搜索系统等等。如果各个项目自行构建这些系统的话,会重复发明同样的轮子,使得项目臃肿,迭代减速。
此时中台应运而生,它是一层为各个业务线提供公共资源或者服务,例如之前所说的用户系统、交易系统、搜索系统等等。
一言以蔽之,中台就是抽象出来为各个业务线提供统一公共服务的那一层。一般是由多个微服务组成,是业务发展到一定程序之后沉淀下来的平台。
以下是阿里的中台图,来源于此
Serverless
Serverless是在目前的Paas更进一步的的平台服务,一般分为BaaS和FaaS,或者结合两者。
PaaS(Platform as a Service)是构建在 IaaS 之上的一种平台服务,提供操作系统安装、监控和服务发现等功能,用户只需要部署自己的应用即可。如果同时使用公有云和私有云,如果能在两者之间构建一个统一的 PaaS,那就是“混合云”了。
在 PaaS 上最广泛使用的技术就要数 Docker 了,因为使用容器可以很清晰的描述应用程序,并保证环境一致性。管理云上的容器,可以称为是 CaaS(Container as a Service),如 GCE(Google Container Engine)。也可以基于 Kubernetes、Mesos 这类开源软件构件自己的 CaaS,不论是直接在 IaaS 构建还是基于 PaaS。
BaaS(Backend as a Service)后端即服务,一般是一个个的 API 调用后端或别人已经实现好的程序逻辑,比如身份验证服务 Auth0,这些 BaaS 通常会用来管理数据,还有很多公有云上提供的我们常用的开源软件的商用服务,比如亚马逊的 RDS 可以替代我们自己部署的 MySQL,还有各种其它数据库和存储服务。
FaaS(Functions as a Service)函数即服务,FaaS 是无服务器计算的一种形式,当前使用最广泛的是 AWS 的 Lambda。
Serverless离我们并不远,腾讯小程序云就结合了BaaS和FaaS为我们提供Serverless服务,使得我们不用写服务端也能在小程序里支持用户状态维护、上传文件等多样化的操作。
待补充,以下可能会做为独立的文章出现
HATEOAS、RabbitMQ、ATOM、反应式编程、鲁棒性原则(Postel法则)、绞杀者模式(Strangler Application Pattern)、两阶段提交、CRC(class-responsibility-collaboration,类-职责-交互)、[虚拟化技术(vagrant),容器,docker]、CDC消费者驱动契约、Nagios、Graphite、Zipkin、Hystrix、基于事件的统一监控 Riemann,Suro、双重身份验证(Two Factor auth)、SAML、OWASP,Top 10 Web Application Security Risks、BASE/CAP、Zookeeper、Consul、Eureka、Swagger、HAL,Hypertext Application Language,超文本应用语言和HAL浏览器
参考
Serverless Handbook——无服务架构实践手册
Karyon -
Hystrix -