转载请注明出处 http://www.paraller.com
原文排版地址 点击跳转获得更好阅读体验
这是一篇《微服务设计》的学习笔记,主要是自己提炼的一些知识点,书比较薄,建议看原版书理解相关概念。
简介
相关概念
- 背景:随着代码库越来越大,代码修改困难 、 模块之间界限模糊 、 相似代码过多。
内聚性 - 单一职责原则
:相同原因而变化的东西放在一起,因不同原因变化的东西分离开来;微服务将这个理念应用到独立的服务上,根据业务的边界来确定服务的边界。- 微服务是SOA的一种特定方法
特性:
- 一个微服务就是一个独立的实体,可以独立部署
- 服务之间通过网络进行通讯
- 服务彼此间可以独立的进行修改,服务的部署不应该引起消费方的变动
- 服务暴露过多,会造成和消费方的紧耦合
优点:
- 技术异构性: 尝试新技术,降低风险
- 系统中组件不可用,不会造成级联故障
- 扩展:对服务进行针对性的扩展
- 简化部署:特定代码部署,不影响系统整体,快速回滚
- 组织结构匹配: 不同的团队负责不同的服务
- 可组合性: 对不同的场景组合服务
分解技术
微服务
- 分布式系统的复杂性
- 部署、测试、监控的投入
- 类型分布式事务和CAP的考虑
共享库
对重复代码进行分包组织,工具类,重复业务代码类。缺点如下:
- 无法使用异构技术
- 每次更新,需要将相关的程序重新部署
- 公共任务并且不属于业务代码,可以这样做,但如果涉及服务间的通讯,会成为耦合点
模块
Erlang的模块化能力惊人;难度比较大,很容易会和其他代码耦合在一起
微服务演化
需要注意细则
- 架构师类似城市规划师,专注在大方向上,有限情况参与到具体的开发,不关注每个区域内发生的事,更关注区域之间的事情(服务之间的交互)
- 未来的变化很难预见,对所有可能性进行预测,不如做一个允许变化的计划
- 系统设计方面的决定通常是取舍。
- 为了和更大的目标保持一致,制定一些具体的规则,称为
原则
- 原则作为指导,
约束
是很难被改变的。显示指出两者,并定期回顾是否要修正。 - 编写文档是有用的,配上真实的代码范例
- 架构师提供一些温和的指导,让团队自行决定何时偿还债务,维护一个债务列表,并定期回顾
- 偏离原则:针对某个场景记录下来,当例外很多次出现,考虑修改原则
- 架构师和团队小组存在分歧,大部分情况要认同小组的决定。
要求的标准
- 建议确保所有的服务使用
同样的方式
报告健康状态 及 监控相关的数据,标准化,隐藏具体技术实现, 日志服务和监控服务一样,要集中化 - 使用统一的接口协议
如何建模服务
概念 & 准则
松耦合
- 独立修改部署而不影响系统的其他部分
- 限制服务间的调用数量,除了性能问题,过度通讯会造成紧耦合
高内聚
改变某个行为,只需要在一个地方进行修改,就可以尽快发布,快速修改,低风险发布
bounded Context(限界上下文)
共享的隐藏模型:
- 财务和仓库两个限界上下文,会对仓库的 库存模型存在交集,针对库存模型, 应该存在 内部和外部两种表示方式,不暴露所有属性
- 共享
特定模型
,不共享内部表示可以避免潜在的紧耦合, - 一旦发现了领域内部的限界上下文,一定要使用模块对其进行建模,同时使用共享模型和隐藏模型
其他
- 对于一个新系统而言,可以使用一段时间单系统,避免后期的修复代价。
- 将一个已有的代码库划分为微服务,比从头开始构建微服务要简单
集成
集成技术选型:
- 不应该选择那种对微服务具体实现技术有限制的集成方式
- 使服务易于消费方使用(提供客户端库可以简化使用,但是增加了耦合)
- 隐藏内部实现,避免服务方的任何修改都可能影响到消费方
共享数据库
- 外部系统能够查看内部实现细节,并与其完全绑定在一起,所有服务都可以完成访问该数据库; 如果修改数据库会导致消费方没有办法工作。需要大量的回归测试
- 消费方和服务绑定在一起,无法轻易的替换技术
同步与异步
同步:及时的得到操作的响应 ;请求/响应
异步:适用长时间的操作;基于事件
编排与协同
场景:创建用户的操作, 需要发放优惠券、创建银行账户、发送欢迎邮件
编排
- 使用客户服务作为中心,同步顺序的调用操作,能及时知道每一步是否成功
- 客户服务成为中心控制承担了太多职责,中心枢纽和很多逻辑的起点
协同
消除耦合,但没有明显的流程视图,无法保证每一步流程都正确执行,需要更多额外的工作,来构建一个与业务流程匹配的监控系统,
折中方案
使用异步回调的方式。
请求/响应的技术:
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:一致性 ,但是拒绝新请求