微服务设计笔记

Posted by zhida on May 2, 2016

这是一篇《微服务设计》的学习笔记,主要是自己提炼的一些知识点,书比较薄,建议看原版书理解相关概念。

简介

相关概念

  • 背景:随着代码库越来越大,代码修改困难 、 模块之间界限模糊 、 相似代码过多。
  • 内聚性 - 单一职责原则:相同原因而变化的东西放在一起,因不同原因变化的东西分离开来;微服务将这个理念应用到独立的服务上,根据业务的边界来确定服务的边界。
  • 微服务是SOA的一种特定方法
特性:
  • 一个微服务就是一个独立的实体,可以独立部署
  • 服务之间通过网络进行通讯
  • 服务彼此间可以独立的进行修改,服务的部署不应该引起消费方的变动
  • 服务暴露过多,会造成和消费方的紧耦合
优点:
  • 技术异构性: 尝试新技术,降低风险
  • 系统中组件不可用,不会造成级联故障
  • 扩展:对服务进行针对性的扩展
  • 简化部署:特定代码部署,不影响系统整体,快速回滚
  • 组织结构匹配: 不同的团队负责不同的服务
  • 可组合性: 对不同的场景组合服务

不同的分解技术

微服务
  • 分布式系统的复杂性
  • 部署、测试、监控的投入
  • 类型分布式事务和CAP的考虑
共享库

对重复代码进行分包组织,工具类,重复业务代码类。缺点如下:

  • 无法使用异构技术
  • 每次更新,需要将相关的程序重新部署
  • 公共任务并且不属于业务代码,可以这样做,但如果涉及服务间的通讯,会成为耦合点
模块

Erlang的模块化能力惊人;难度比较大,很容易会和其他代码耦合在一起

微服务演化

需要注意细则
  • 架构师类似城市规划师,专注在大方向上,有限情况参与到具体的开发,不关注每个区域内发生的事,更关注区域之间的事情(服务之间的交互)
  • 未来的变化很难预见,对所有可能性进行预测,不如做一个允许变化的计划
  • 系统设计方面的决定通常是取舍。
  • 为了和更大的目标保持一致,制定一些具体的规则,称为原则
  • 原则作为指导,约束是很难被改变的。显示指出两者,并定期回顾是否要修正。
  • 编写文档是有用的,配上真实的代码范例
  • 架构师提供一些温和的指导,让团队自行决定何时偿还债务,维护一个债务列表,并定期回顾
  • 偏离原则:针对某个场景记录下来,当例外很多次出现,考虑修改原则
  • 架构师和团队小组存在分歧,大部分情况要认同小组的决定。
要求的标准
  • 建议确保所有的服务使用同样的方式报告健康状态 及 监控相关的数据,标准化,隐藏具体技术实现, 日志服务和监控服务一样,要集中化
  • 使用统一的接口协议

如何建模服务

概念 & 准则

松耦合
  • 独立修改部署而不影响系统的其他部分
  • 限制服务间的调用数量,除了性能问题,过度通讯会造成紧耦合
高内聚

改变某个行为,只需要在一个地方进行修改,就可以尽快发布,快速修改,低风险发布

bounded Context(限界上下文)
  • 每个限界上下文分为两部分,一部分不需要和外部通讯,一部分需要。 有明确的接口,决定了暴露哪些模型给其他界限上下文
  • 模块边界就可以成为绝佳的微服务候选,熟练了之后,可以省掉在单块系统中先使用模块的这个步骤,而直接使用单独的服务
  • 思考限界上下文时,不应该从共享数据的角度来考虑,而应该从这些上下文能提供的功能来考虑,否则会演变成基于模型, 从而导致贫血、基于CRUD的操作
  • 逐步划分上下文,一开始识别粗粒度的限界上下文,而这些限界上下文会嵌套一系列子限界上下文,两种做法:
    • 嵌套上下文不直接对外可见,用的还是粗粒度上下文的功能,但发出的请求被透明的映射到其他服务上(更好的测试)
    • 将子限界上下文单独拆分成服务
共享的隐藏模型:
  • 财务和仓库两个限界上下文,会对仓库的 库存模型存在交集,针对库存模型, 应该存在 内部和外部两种表示方式,不暴露所有属性
  • 共享特定模型,不共享内部表示可以避免潜在的紧耦合,
  • 一旦发现了领域内部的限界上下文,一定要使用模块对其进行建模,同时使用共享模型和隐藏模型
其他
  • 对于一个新系统而言,可以使用一段时间单系统,避免后期的修复代价。
  • 将一个已有的代码库划分为微服务,比从头开始构建微服务要简单

集成

集成技术选型:

  • 不应该选择那种对微服务具体实现技术有限制的集成方式
  • 使服务易于消费方使用(提供客户端库可以简化使用,但是增加了耦合)
  • 隐藏内部实现,避免服务方的任何修改都可能影响到消费方

共享数据库

  • 外部系统能够查看内部实现细节,并与其完全绑定在一起,所有服务都可以完成访问该数据库; 如果修改数据库会导致消费方没有办法工作。需要大量的回归测试
  • 消费方和服务绑定在一起,无法轻易的替换技术

同步与异步

同步:及时的得到操作的响应 ;请求/响应 异步:适用长时间的操作;基于事件

编排与协同

场景:创建用户的操作, 需要发放优惠券、创建银行账户、发送欢迎邮件

编排
  • 使用客户服务作为中心,同步顺序的调用操作,能及时知道每一步是否成功
  • 客户服务成为中心控制承担了太多职责,中心枢纽和很多逻辑的起点
协同

消除耦合,但没有明显的流程视图,无法保证每一步流程都正确执行,需要更多额外的工作,来构建一个与业务流程匹配的监控系统,

折中方案

使用异步回调的方式。

请求/响应的技术:

RPC
  • 核心特点,使用本地调用的方式和远程进行交互。
  • 核心思想是隐藏远程调用的复杂性,但是很多框架隐藏过头了;使用本地调用不会造成性能问题,但是RPC花大量的时间来对负荷和解封装,以及网络通信的时间,简单的把一个远程服务改造成跨服务的远程API往往会带来问题
  • 更糟的情况是: 开发人员不知道调用是远程调用,并对其进行使用
  • 网络的出错模式不止一种,很难对问题进行定位
  • 脆弱性:对象参数的修改,需要对客户端重新生成打桩,应用这些修改需要同时部署客户端和服务端
  • 选用RPC,一定不要对远程调用过度抽象,确保可以独立的升级服务器,不要隐藏网络调用的事实
REST
  • HTTP周边有很大的生态系统,包含很多支撑工具和技术,比如 Varnish HTTP缓存代理 / mod_proxy 负载均衡 / 大量的监控工具
  • HTTP也可以用来实现 RPC,比如soap就是基于HTTP进行路由的,只是使用了少量的HTTP特性
  • 对于有些接口来说,HTML既可以做UI,也可以做API,
  • 建议使用XML,在工具上有很多支持
  • springboot过多的约定带来了紧耦合
  • 使用客户端库会增加复杂度,因为人们不自觉地回到基于HTTP的RPC思路上去了,然后构造出一堆共享库,在客户端和服务端之间共享代码是很危险的,
  • 在低延迟要求的服务中,HTTP的封装开销需要注意
  • 低延迟通信最好的选择是TCP编程
  • REST得到序列化和反序列化需要自己实现,会成为消费者和服务端的耦合点

基于异步的实现

增加开发流程的复杂度,需要额外的系统才能开发及测试,需要额外的专业知识和机器保持基础设计正常运行

  • 原则: 尽量让中间件简单,将逻辑放在自己的服务中
  • 设置最大重试次数,失败的消息统一发送到一个地方,进行查看和重试,
  • 确保使用监控机制保证每个流程,然后对流程进行ID关联 (zookeeper)
  • 把关键领域的生命周期显示建模出来非常有用,不但可以在唯一的地方处理状态冲突,还可以在这些状态的基础上封装一些行为

灾难性故障转移: 队列中存放了任务,消费者A处理崩溃,消费者B处理也崩溃,一个异常元素导致一系列的消费者崩溃。

DRY:避免重复代码

如果有相同代码做同样的事情,代码规模就会变大,从而降低可维护性

创建一个随处可用的共享库?
  • 在微服务中是危险的,会导致耦合,客户端和服务端需要同时更新部署
  • 但在服务间使用日志库代码不是问题,因为对外是不可见的
  • 服务间使用共享库比重复代码还要可怕

客户端库

如果要使用,要保证只包含处理底层传输协议的代码,比如服务发现和故障处理等等,千万不要把与目标服务相关的逻辑代码放到客户端库中

按引用访问

  • 微服务应该包含核心领域实体的全生命周期的相关操作,服务应该是关于该领域的唯一可靠来源
  • 对服务发起一个资源的请求,然后保存在本地副本中,可能一段时间会失效,所以请求返回的结果,要保存一个指向原始资源的引用(比如一个资源URL),确保需要最新数据的时候可以有办法获取
  • 总是通过一个服务去获取某个领域的信息,会造成过多的负载,如果能够得到该领域的有效时间是最好的

版本管理

  • 尽可能不做破坏性修改,使用良好的架构设计
  • 鼓励客户端正确的行为,例如json传输数据,一些强类型语言会使用绑定技术,会将所有的字段绑定,无论消费者是否需要,当修改接口数据结构的时候会影响到消费者,可以使用XPath技术提取出想要的字段
  • 鲁棒性原则,每个模块都应该 宽进严出,发送的东西要严格,接收的东西要宽容
  • 使用语义化的版本管理,格式如下:major.minor.patch ;major代表包含向后不兼容的修改; minor意味着新功能的增加 ; patch代表对缺陷的修改
  • 不同接口可以共存,发布一个破坏性修改的时候,可以部署一个包含新老接口的版本;但更建议在V1接口中转换后请求V2接口
  • 同时使用多个版本的服务

BFF(Backed for frontends)为前端服务的后端

对于不同的客户端,使用聚合接口,对后台调用的服务进行编排,类似于一个专门的后台服务,比如Node程序,对JAVA后台的接口进行组合

分解单块系统

  • 首先识别出单块后台系统明显的几个上下文
  • 为他们创建包结构来表示,把已有的代码进行移动

解决横跨不同上下文的表

  • 打破外键约束,将访问变成逻辑外键,通过暴露的API进行交互
  • 共享的静态数据,通过配置文件和代码中进行配置,不要放在公有包中
共享数据
  • 不同的上下文会对同一张表进行读写操作:概念领域不是在代码中进行建模,相反是在数据库中隐式的建模,代表这个表是一个上下文,作为一个中间步骤,可以创建一个新的包最终变成一个服务
  • 共享表:存在一个通用的行条目录表,不同上下文都用到了部分数据:可以分成两张表

重构数据库

  • 先分离数据库结构,不对服务进行分离
  • 对数据库的访问次数会变多,以前一个查询获得所有数据,现在要内存中进行组装

事务边界

一个事务可以帮助我们的系统从一个一致性状态 转移到另外一个一致性; 分离数据库之后,没有了原生的事务处理,解决方案:

  • 再试一次:把失败的操作,记录在日志或者失败队列中,后面对他们尝试触发,要保证重新触发能够成功,最终一致性
  • 终止整个操作:对上一个成功的操作进行补偿事务来抵消之前的操作,可靠性不佳
  • 分布式事务:外部的事务管理器统一编排执行,常用算法是两阶段提交,可靠性也不佳

总结:是否真的需要强一致性? 是否要跨业务进行操作? 是否可以通过业务逻辑的处理避免事务,比如新增处理中的订单状态

报表:

  • 为了防止对主系统的影响,报表的查询使用副本; 缺点:共享数据库结构会抑制修改表结构的积极性
  • 使用MongoDB或基于列的数据库来 保存副本
数据库分布在不同的系统中
  • 通过服务调用来获取数据:少量的数据可以考虑在内存中进行组合
  • 大数据读取:使用HTTP POST方法,携带一个位置信息,让服务器返回200,把获取的内容写入到文件中,然后保存在请求的位置上,客户端轮询请求,直到返回201,这样就减少了HTTP的开销
  • 数据导出: 使用一个独立的服务,直接访问不同的微服务使用的数据库,导出到单独的报表系统中;在报表数据库中包含了所有的服务数据结构,然后可以使用视图之类的技术来创建一个聚合。
  • 事件数据导出:在事件发生时就给报表系统发送数据,而不是周期性的导出,增量导入更高效。 缺点:数据量较大时不容易扩展
  • 对数据导出的备份进行处理:可以使用Hadoop对数据处理后,储存起来

部署

持续集成(CI)

  • 当构建失败之后,把修复CI当作第一优先级要处理的事情
  • 集成需要测试,这样才能保证集成代码的正确性,不然只是对语法错误进行检查
  • 每个微服务要有一个专有的CI,包含测试代码

构建流水线和持续交付(CD)

  • CD能够检查每次提交是否到达了部署生产环境的要求,并持续的把这些消息反馈给我们,把每次提交当成候选版本对待
  • 在CD中,会把多阶段构建流水线的概念进行扩展,从而覆盖软件通过的所有阶段
  • 编译及快速测试 -> 耗时测试 -> 用户验收测试 -> 性能测试 -> 生产环境

测试

单元测试

通常只测试一个函数或者方法,通过TDD写的测试就属于这一类,不启动服务,对外部网络和文件使用也很有限;面向技术,对功能正常给出快速反馈

服务测试
  • 对于包含多个服务的系统,一个服务测试只测试其中一个功能
  • 为了达到隔离性,需要为其他服务打桩,MOCK
端到端测试
  • 会覆盖整个系统,通常需要打开一个浏览器来操作图形界面。
  • 测试类型的比例:应该是不同数量级的
  • 随着测试的范围扩大,遇到的可能情况也越多,发现脆弱测试时,应该竭尽全力去解决,避免异常正常化(对事情出错变得习以为常);当不能立即修复的时候,从测试套件中移除。
  • 不要轻易删掉测试代码,除非你理解风险
  • 测试场景,而不是故事:测试的重点放在核心的场景中,其他场景在服务测试中进行。
CDC

消费者驱动测试:定义消费者的期望,服务端没有达到预期将无法部署,有助于不同团队一起来编写代码

部署后在测试:
  • 部署之前的测试不能保证零缺陷,部署只是在正式环境启动,不代表引入正常流量。
  • 蓝绿部署 -> 冒烟测试 -> 切换流量
  • 金丝雀发布:少量流量引入新部署的服务中,然后不断的调节流量来验证我们的功能性和非功能性。进行计分然后确认完全切换,简单的做法Nginx分流,复杂的复制生产环境请求
  • 性能测试:原来的单次调用可能会变成多次调用,以及跨数据库,会影响到整个微服务调用链,所以比单块系统更加重要

微服务规模化的挑战:

了解真正的需求:响应时间、延迟、可用性、数据持久性的 权衡

功能降级:当出现问题的其他处理方式
  • 程序使用HTTP连接池来处理下游链接:如果某个下游请求故障,但是HTTP设置了超时时间,就会导致大量的请求堆积,所有的worker都在等待超时,阻止建立新的HTTP请求,导致系统大范围不可用
  • 在分布式系统中,延迟是致命的

解决方案:

  • 正确的设置超时时间
  • 实现资源隔离,使用不同的连接池
  • 实现一个断路器,快速失败
断路器

对下游资源请求失败的次数到达一定数量,断路器打开,所有请求快速失败,一段时间发送请求成功,将会重置断路器

资源隔离

分配不同的资源,当某个部分资源耗尽不影响其他的组件

幂等

确保部分操作幂等安全性,Nginx的重试不包括POST请求。

扩展
  • 帮助处理失败,额外的程序保证正常运行
  • 性能扩展,减少延迟增加负载
强大的主机

称之为垂直扩展,如果软件没有充分利用也是白搭

分散风险

不要把所有的微服务放在一个地方

负载均衡:SSL终止

通过HTTPS连接到负载均衡器后(Nginx),转到http server ,变成HTTP连接,提高性能。HTTP连接在局域网中,所有外部请求通过一个路由访问内部

扩展数据库
  • 扩展读操作:通过多个副本扩展,一般有一致性问题,确保可以接受
  • 扩展写操作:对数据进行哈希,基于哈希分配到一个数据库中,缺点:查询复杂(mongo map/reduce),扩展困难
  • 每个微服务一个单独的数据库实例,避免一个数据库实例分配多个数据库
缓存

代理服务器缓存,介于客户端和服务端之间; 客户端缓存以及服务端缓存,一般都是三者混用

HTTP缓存:
  • 对客户端的响应使用 cache-control指令:告诉是否缓存以及时限
  • 设置Expire头部,指定一个日期,该日期之后失效
  • Etag用来标志资源是否匹配,有一种请求方式叫做条件GET
缓存失效

后台异步生成缓存,接受部分实时请求(服务端可能负载),其他请求快速失败,异步生成缓存

自动伸缩

不同的流量对服务进行自动伸缩。

CAP: consistency / availability / partition tolerance

一般是AP: 分区可用,最终一致性; CP:一致性 ,但是拒绝新请求