微服务面面观
微服务基本
单体应用
单体应用的优点?
--易于开发
--易于测试
--易于部署
存在的问题:
--代码耦合,开发维护困难,提交代码频繁出现大量冲突
--主要业务和次要业务耦合,无法针对不同模块进行针对性优化
--单点容错率低,并发能力差,无法水平扩展
--技术选型成本高
--交付周期长,小功能要积累到大版本才能上线,上线开总监级别大会
微服务和SOA
微服务:架构本质是带自身特点的面向服务的分布式架构模式。
SOA(Service Oriented Architecture):“面向服务的架构”:他是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。
SOA特点
-- 有序(站在系统的角度,把原先散乱、无规划的系统间的网状结构,梳理成 规整、可治理的系统间星形结构)
-- 复用(站在功能的角度,把业务逻辑抽象成 可复用、可组装的服务)
-- 高效(站在企业的角度,把企业职能抽象成 可复用、可组装的服务)
微服务和SOA区别:
功能 | SOA | 微服务 |
组件大小 | 大块业务逻辑(粗粒度) | 单独、小块业务逻辑(细粒度) |
耦合 | 通常松耦合 | 总是松耦合 |
管理 |
着重中央管理 | 去中心化 |
通信 | 轻量级通信 | 企业服务总线(ESB)充当服务之间通信的角色 |
微服务解决的问题
快速迭代
--提交代码频繁出现大量冲突
--小功能要积累到大版本才能上线,上线开总监级别大会
高并发
--横向扩展流程复杂,主要业务和次要业务耦合
--熔断降级全靠if-else
- 如果核心业务流程和边角业务流程在同一个进程中,就需要使用大量的if-else语句,根据下发的配置来判断是否熔断或者降级,这会使得配置异常复杂,难以维护。
- 如果核心业务和边角业务分成两个进程,就可以使用标准的熔断降级策略,配置在某种情况下,放弃对另一个进程的调用,可以进行统一的维护。
微服务的设计原则、特征
微服务体系结构由轻量级、松散耦合的服务集合组成。每个服务都实现了单个业务功能。理想情况下,这些服务应该是具有足够的内聚性,可以独立地开发、测试、发布、部署、扩展、集成和维护。
1. AKF拆分原则:以业务为中心
AKF扩展立方体,是一个AKF公司的技术专家抽象总结的应用扩展的三个维度,理论上按照这三个扩展模式,可以将一个单体系统进行无限扩展。
- Y轴(功能)——关注应用中功能划分,基于不同的业务拆分
- X轴(水平扩展)——关注水平扩展,也就是“加机器解决问题”
- Z轴(数据区分)——关注服务和数据的优先级划分,如按地域划分
拆分后,每个服务代表了特定的业务逻辑、围绕业务组织团队、能快速的响应业务的变化。每个微服务也都可以动态的进行x轴和z轴的扩展,并适应云环境下的自动化部署;
2.高内聚低耦合、轻量级无状态通信原则
- 关注微服务的范围,而不是一味的把服务做小。一个服务的大小应该等于满足某个特定业务能力所需要的大小。紧密关联的事物应该放在一起,每个服务是针对一个单一职责的业务能力的封装,专注做好一件事情。
- 轻量级的通信方式--同步RESTful能让服务间的通信变得标准化并且无状态;异步(消息队列/发布订阅)
- 避免在服务与服务之间共享数据库,避免产生频繁的跨库查询,避免产生频繁的分布式事务。
依据此,可实现单一职责、轻量级的通信方式、数据独立的效果
3. 去中心化、 高度自治原则
-- 能独立的开发、部署、发布,进程独立,独立的代码库、流水线
4. 弹性设计原则
设计可容错的系统,设计具有自我保护能力的系统
-- 具有自我保护能力、可容错 (Netfilix 提供了一个比较好的解决方案,具体的应对措施包括:网络超时/限制请求的次数/断路器模式/提供回滚等)
5. 日志与监控原则
聚合你的日志,聚合你的数据,从而当你遇到问题时,可以深入分析原因。
--日志聚合,监控与警告
开源产品ELK可以用于日志的收集,聚合,展现,并提供搜索功能,基于一定条件,触发邮件警告。
Spring boot admin
也可以用于服务可用性的监控,hystrix
除了提供熔断器机制外,它还收集了一些请求的基本信息(比如请求响应时间,访问计算,错误统计等),并提供现成的dashboard将信息可视化。6. 自动化原则
在微服务架构下,面临如下挑战:
- 分布式系统
- 多服务,多实例
- 手动测试,部署,发布太消耗时间
- 反馈周期太长
传统的手工运维方式必然要被淘汰,微服务的实施是有一定的先决条件:那就是自动化,当服务规模化后需要更多
自动化
和标准化
的手段来提升效能和降低成本。--微服务是松耦合的,微服务架构模式使得持续化部署成为可能。持续集成、持续交付
参考链接:
https://www.jianshu.com/p/4e582616d565
https://yq.aliyun.com/articles/666604
https://www.servicemesher.com/blog/design-patterns-for-microservices/
https://yq.aliyun.com/articles/618043
微服务的优缺点
优点:
强模块化边界
可独立部署
技术多样性
缺点:
服务拆分复杂性
分布式复杂性
最终一致性(分布式事务)
测试、运维复杂性
微服务拆分
微服务拆分原则
- 单一职责
- 服务依赖 (避免循环依赖)
- 服务自治(服务划分应考虑让团队参与服务整个生命周期)
- 服务拆分最多三层,两次调用
- 规范化工程名(见文只意)
- 接口数据定义严禁内嵌,透传
- 接口应该实现幂等
- 将串行调用改为并行调用,或者异步化
服务划分的合理性
服务的业务范围,是否破坏服务依赖原则,是否满足单一职责
服务所属团队的规模
服务能否独立交付
微服务的设计模式
链式设计模式(按照数据流向就行数据拆分)
--先后调用多个服务,产生一个经过合并的响应给客户
--在整个链式调用完成之前,客户端会一直阻塞
--服务调用链不宜过长,以免客户端长时间等待
聚合器(API Gateway\BFF\边缘服务Edge Service)
异步消息传递
--同步请求会造成阻塞, 可以选择使用消息队列代替同步请求/响应:
事件溯源模式
--采用已事件为中心的方法保存业务实体
物化视图模式
--多个服务的常用的聚合数据,生成视图,方便查询
CQRS模式(读写分离)
服务治理
微服务出现了什么问题?
- 服务越来越多,需要管理每个服务的地址
- 调用关系错综复杂,难以理清依赖关系
- 服务过多,服务状态难以管理,无法根据服务情况动态管理
服务治理要做什么
- 服务治理就是对服务复杂度膨胀问题的管控及管理。
服务的线上的治理
--服务限流
--集群容错
--服务降级、熔断
--故障定界定位
--容量规划
--资源治理
--线上生命周期管理
服务的线下的治理
项目管理、版本管理、测试、运维
架构管理
--异常处理、旧版本兼容
开发管理
--代码质量
测试管理
--测试覆盖度
构建调测能力
协同管理治理
服务限流
漏桶算法及令牌桶算法
集群限流的情况要更复杂一些,首先在各个微服务节点上要有一个计数器,对单位时间片内的调用进行计数,算出这个时间片的总调用量和预先定义的限流阈值进行比对,计算出一个限流比例
服务降级、熔断
服务降级与熔断
- 联系
- 目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;
- 最终表现类似,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
- 粒度一般都是服务级别,当然,业界也有不少更细粒度的做法,比如做到数据持久层(允许查询,不允许增删改);
- 自治性要求很高,熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段;
区别
- 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
- 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)
- 实现方式不太一样,这个区别后面会单独来说;
服务降级手段:
- 容错降级
- 我们常说的熔断,本质上也是容错降级策略的一种,只不过它比一般容错降级提供了更为丰富的容错托底策略,支持半开降级及全开降级模式
- 静态返回值降级
- Mock 降级
- 备用服务降级
故障定界定位
调用链本质上也是基于日志,只不过它比常规的日志更重视日志之间的关系。在一个请求刚发起的时候,调用链会赋予它一个跟踪号(traceID),这个跟踪号会随着请求穿越不同的网络节点,并随着日志落盘,日志被收集后,可以根据 traceID 来对日志做聚合,找到所有的关联日志,并按顺序排序,就能构建出这个请求跨网络的调用链,它能详细描述请求的整个生命周期的状况。
分布一致性
强一致性、弱一致性、最终一致性
CAP理论
一致性(C:Consistency)
可用性(A:Availability)
分区容错性(P:Partition tolerance)
- CA 放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择
- AP 放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,例如很多NoSQL系统就是如此
- CP 放弃可用性,追求一致性和分区容错性,基本不会选择,网络问题会直接让整个系统不可用
C A 满足的情况下,P不能满足的原因:
数据同步(C)需要时间,也要正常的时间内响应(A),那么机器数量就要少,所以P就不满足
CP 满足的情况下,A不能满足的原因:
数据同步(C)需要时间, 机器数量也多(P),但是同步数据需要时间,所以不能再正常时间内响应,所以A就不满足
AP 满足的情况下,C不能满足的原因:
机器数量也多(P),正常的时间内响应(A),那么数据就不能及时同步到其他节点,所以C不满足
BASE理论
BASE理论是对CAP中一致性和可用性权衡的结果
Basically Available(基本可用)
- 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性
Soft state(软状态)
- 软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
Eventually consistent(最终一致性)
- 最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
服务注册与发现
服务注册中心本质上是为了解耦服务提供者和服务消费者。
对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的。更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。
因此,原本在单体应用阶段常用的静态LB机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。
注册中心选择:
Zookeeper:CP设计,保证了一致性,集群搭建的时候,某个节点失效,则会进行选举行的leader,或者半数以上节点不可用,则无法提供服务,因此可用性没法满足
Eureka:AP原则,无主从节点,一个节点挂了,自动切换其他节点可以使用,去中心化
注册中心分类
应用内:直接集成到应用中,依赖于应用自身完成服务的注册与发现,最典型的是Netflix提供的Eureka、nacos
应用外:把应用当成黑盒,通过应用外的某种机制将服务注册到注册中心,最小化对应用的侵入性,HashiCorp的Consul
Consul强一致性(C)带来的是:
服务注册相比Eureka会稍慢一些。因为Consul的raft协议要求必须过半数的节点都写入成功才认为注册成功
Leader挂掉时,重新选举期间整个consul不可用。
保证了强一致性但牺牲了可用性。
Eureka保证高可用(A)和最终一致性:
服务注册相对要快,因为不需要等注册信息replicate到其他节点,也不保证注册信息是否replicate成功
当数据出现不一致时,虽然A, B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求A查不到,但请求B就能查到。如此保证了可用性但牺牲了一致性。
其他方面,eureka就是个servlet程序,跑在servlet容器中; Consul则是go编写而成。
Nacos = Spring Cloud注册中心 + Spring Cloud配置中心。
服务优雅上下线
-- 停止服务前先截断服务的流量
--注册中心通知所有服务干掉下线实例
--处理完手头事务后停止
Eureka 中服务下线的几种方式
1、直接停掉服务
根据默认的策略,如果在一定的时间内,客户端没有向注册中心发送续约请求,那么注册中心就会将该实例从注册中心移除,但是有缺陷,因为服务直接停掉后,实例仍然会在注册中心存在一小段时间(90s),也有可能注册中心直接认为你的服务down掉,但是实例仍然存在于注册中心
2、通过注册中心接口强制下线
为了让注册中心马上知道服务要下线, 可以向eureka 注册中心发送delete 请求
// 注册中心zone
eureka: client: serviceUrl: defaultZone
发送一个delete 请求
格式为 /eureka/apps/{application.name}/
http://你的注册中心zone/apps/你的实例名称/你的实例地址加端口
实例名称就是Application,地址加端口就是Status的右边
值得注意的是,Eureka客户端每隔一段时间(默认30秒)会发送一次心跳到注册中心续约。如果通过这种方式下线了一个服务,而没有及时停掉的话,该服务很快又会回到服务列表中。
所以,可以先停掉服务,再发送请求将其从列表中移除。
3、客户端主动下线
// 客户端(SpringBoot应用)可以通过如下代码主动通知注册中心下线
DiscoveryManager.getInstance().shutdownComponent();
@RestController public class HelloController { @Autowired private DiscoveryClient client; @RequestMapping(value = "/hello", method = RequestMethod.GET) public String index() { java.util.List<ServiceInstance> instances = client.getInstances("hello-service"); return "Hello World"; } @RequestMapping(value = "/offline", method = RequestMethod.GET) public void offLine(){ DiscoveryManager.getInstance().shutdownComponent(); } }
更多参考:
反向代理与负载均衡
Nginx通过反向代理可以实现服务的负载均衡,避免了服务器单节点故障,把请求按照一定的策略转发到不同的服务器上,达到负载的效果。
常用的负载均衡策略有
1、轮询
将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
2、加权轮询
给配置高、负载低的机器配置更高的权重,让其处理更多的请求
3、ip_hash(源地址哈希法)
根据获取客户端的IP地址,通过哈希函数计算得到一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客户端要访问服务器的序号。
4、随机
5、least_conn(最小连接数法)
动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求
服务网关
客户端访问这些后端的多个微服务,遇到的问题?
--每个业务都会需要鉴权、限流、权限校验等逻辑
--每上线一个新的服务,都需要运维参与,申请域名、配置Nginx等,当上线、下线服务器时,同样也需要运维参与,另外采用域名这种方式,对于环境的隔离也不太友好
--后端每个微服务可能是由不同语言编写的、采用了不同的协议,比如HTTP、Dubbo、GRPC等,但是你不可能要求客户端去适配这么多种协议
--后期如果需要对微服务进行重构的话,也会变的非常麻烦,需要客户端配合你一起进行改造
网关做的不仅仅只是简单的转发,也会针对流量做一些扩展,比如鉴权、限流、权限、熔断、协议转换、错误码统一、缓存、日志、监控、告警等
服务网关关注:
API注册
--(业务方如何接入网关?Swagger的注解、手动录入)
协议转换
服务发现
服务调用
缓存
-- 一些重复的请求,可以在网关层直接处理,而不用打到业务线,降低业务方的压力
限流
--令牌桶等方案。还需要考虑根据什么限流,比如是IP、接口、用户维度、还是请求参数中的某些值,这里可以采用表达式,相对比较灵活
日志
--提供一个统一的traceId方便关联所有的日志,可以将这个traceId置于响应头中,方便排查问题。
优雅下线
--Nginx自身是支持健康监测机制的,如果检测到某一个节点已经挂掉了,就会把这个节点摘掉,
对于应用正常下线,需要结合发布系统,首先进行逻辑下线,然后对后续Nginx的健康监测请求直接返回失败(比如直接返回500),然后等待一段时间(根据Nginx配置决定),然后再将应用实际下线掉。
服务网关不足
目前的网关还是中心化的架构,所有的请求都需要走一次网关
目前比较流行的ServiceMesh,采用去中心化的方案,将网关的逻辑下沉到sidecar中,sidecar和应用部署到同一个节点,并接管应用流入、流出的流量,这样大促时,只需要对相关的业务压测,并针对性扩容即可,另外升级也会更平滑,
中心化的网关,即使灰度发布,但是理论上所有业务方的流量都会流入到新版本的网关,如果出了问题,会影响到所有的业务,
但这种去中心化的方式,可以先针对非核心业务升级,观察一段时间没问题后,再全量推上线。另外ServiceMesh的方案,对于多语言支持也更友好。
分布式事务
具体参考:微服务:分布式事务
--事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作
-- 要么什么都不做,要么做全套(All or Nothing)
事务的ACID属性
--原子性(Atomicity)要么全部完成,要么全部不完成
--一致性(Consistency)在事务开始之前和事务结束以后,数据库数据的一致性约束没有被破坏
--隔离性(Isolation)数据库允许多个并发事务同时对数据进行读写和修改的能力
--持久性(Durability) 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
分布式事务的实现方案
二阶段提交协议(Two-phase Commit,即2PC)
阶段1:准备阶段
阶段2:提交阶段
2PC方案实现起来简单,实际项目中使用比较少,主要因为以下问题:
性能问题 :
所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
可靠性问题 :
如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
数据一致性问题:
在阶段2中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
3PC(三阶段提交)
--与二阶段提交不同的是,引入超时机制
阶段1:canCommit 检查是否可以提交
阶段2:preCommit 执行事务操作
阶段3:do Commit 该阶段进行真正的事务提交
方案总结
优点
相比二阶段提交,三阶段贴近降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。
缺点
数据不一致问题依然存在,当在参与者收到preCommit请求后等待do commite指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
TCC (Try-Confirm-Cancel)事务 —— 最终一致性
--Try操作作为一阶段,负责资源的检查和预留。
--Confirm操作作为二阶段提交操作,执行真正的业务。
--Cancel是预留资源的取消。
TCC事务机制相对于传统事务机制(X/Open XA),TCC事务机制相比于上面介绍的XA事务机制,有以下优点:
性能提升: 具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性: 基于Confirm和Cancel的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
可靠性: 解决了XA协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:
TCC的Try、Confirm和Cancel操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
本地消息表 —— 最终一致性
事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样设计可以避免”业务处理成功 + 事务消息发送失败",或"业务处理失败 + 事务消息发送成功"的棘手情况出现,保证2个系统事务的数据一致性。
MQ事务 —— 最终一致性
基于MQ的分布式事务方案其实是对本地消息表的封装,将本地消息表基于MQ 内部,
相比本地消息表方案,MQ事务方案优点是:
- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
- 吞吐量高
缺点是:
- 一次消息发送需要两次网络请求(half消息 + commit/rollback消息)
- 业务处理服务需要实现消息状态回查接口
Saga事务 —— 最终一致性
每个Saga事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
每个Ti 都有对应的幂等补偿动作Ci,补偿动作用于撤销Ti造成的结果。
Saga事务常见的有两种不同的实现方式:
1、命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。
中央协调器(Orchestrator,简称OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。
2、事件编排 (Event Choreography):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
集群容错
容错模式
舱壁隔离模式
- 微服务容器分组、线程池隔离避免一个服务拖垮整个系统
熔断模式
- 熔断后快速失败
限流模式
- 高峰期限制访问的并发量
失败转移模式
容错机制
Failover 失败自动切换
- 当出现失败,重试其它服务器,通常用于读操作(推荐使用)。 重试会带来更长延迟。
Failfast 快速失败
- 只发起一次调用,失败立即报错,通常用于非幂等性的写操作。 如果有机器正在重启,可能会出现调用失败 。
Failsafe 失败安全
- 出现异常时,直接忽略,通常用于写入审计日志等操作。 调用信息丢失 可用于生产环境 Monitor。
Failback 失败自动恢复
- 后台记录失败请求,定时重发。通常用于消息通知操作 不可靠,重启丢失。 可用于生产环境 Registry。
Forking 并行调用多个服务器
- 只要一个成功即返回,通常用于实时性要求较高的读操作。 需要浪费更多服务资源 。
Broadcast
- 广播调用,所有提供逐个调用,任意一台报错则报错。通常用于更新提供方本地状态 速度慢,任意一台报错则报错 。
容器监控
监控主要解决的是感知系统的状况
为什么需要监控?
- --问题的定位
- --数据支撑(容量规划)
- --对服务的系统认知(扑结构,如何部署,系统之间怎样通信,系统目前是怎样的性能状况)
要监控什么
- --服务概览信息:如服务名称、服务部署所在机房、主机、服务包含的API、服务相关配置信息、服务负责人、开发人员、运维人员信息等
- --服务性能指标:如响应实现、流量、成功、失败数、请求频率等
- --服务拓扑关系:服务之间的调用关系
- --服务调用链:服务的整个调用链监控
- --服务版本信息:服务版本,客户端版本等
- --服务治理状态:服务注册情况、服务状态、熔断等
- --组件内部状态:活跃线程数、处理请求数等
幂等机制
一个幂等操作的特点是指其任意多次执行所产生的影响均与一次执行的影响相同。
幂等场景
- 网络波动:因网络波动,可能会引起重复请求
- 分布式消息消费:任务发布后,使用分布式消息服务来进行消费
- 用户重复操作:用户在使用产品时,可能会无意的触发多笔交易,甚至没有响应而有意触发多笔交易
- 未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如Nginx重试、RPC通信重试或业务层重试等)
幂等解决方案
- 全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。
- Token机制的核心就是要求客户端的每次请求里必须携带一个UUID
- MVCC:多版本并发控制方式,操作时带上版本号:update t1 set x=y ,version=version+1 where version=xxx,优点是提升了并发响应能力,实现也简单,缺点是只适用更新接口,还是会将重复请求达到数据库,数据库压力较大
- 状态机机制,本质上是MVCC方式的变种:订单有多个业务状态,每次操作数据会带上一个状态,只有在上一个状态匹配的情况下会更新数据,优缺点和MVCC大同小异,但这种机制解决了插入的问题,不仅仅适用在更新接口