微服务

是否使用微服务

  • 组件\代码库升级,改造技术栈
  • 编辑-构建-运行-CI测试慢
  • 不同模块对资源需求(CPU密集型,内存密集型。。)不一样
  • 因为所有模块都在同一个进程中运行,规模庞大,应用程序缺乏故障隔离,容易整个崩溃
  • 可维护性、可扩展性和可测试性。

扩展方式

可伸缩性的艺术

image-20210802222315115

  • X 负载均衡+多实例 (每个副本都可能访问所有数据,缓存需要更多内存才能有效。这种方法的另一个问题是它没有解决增加开发和应用程序复杂性的问题。)

  • Z 每个服务器都运行相同的代码副本。于 X 轴缩放。最大的区别是每个服务器只负责数据的一个子集。系统的某些组件负责将每个请求路由到适当的服务器。

    好处

    • 每个服务器只处理数据的一个子集。
    • 这提高了缓存利用率并减少了内存使用和 I/O 流量。
    • 它还提高了事务可伸缩性,因为请求通常分布在多个服务器上。
    • 此外,Z 轴缩放改进了故障隔离,因为故障只会使部分数据可访问。

    坏处

    • 一个缺点是增加了应用程序的复杂性。
    • 我们需要实现一个分区方案,这可能很棘手,特别是如果我们需要重新分区数据。
    • Z 轴缩放的另一个缺点是不能解决增加开发和应用程序复杂性的问题。为了解决这些问题,我们需要应用Y 轴缩放

    image-20210802222521824

  • Y 轴缩放将应用程序拆分为多个不同的服务。每个服务负责一个或多个密切相关的功能。有几种不同的方法可以将应用程序分解为服务。

image-20210802222443402

可以先按y轴缩放,具体的某个服务再按x或者z轴缩放

概念

微服务架构

image-20210808110558796

image-20210808110617578

服务拆分

通过定义业务能力映射到服务,可能随时间架构改变,好些服务不好拆要合并,服务成长要拆等

image-20210808111815511

子域拆分

子域和限界上下文

image-20210808112601572

image-20210808112625079

拆分原则

单一责任

image-20210808112910959

闭包

image-20210808112944377

拆分障碍

  • 同步通信网络延迟等造成可用性降低

    • 可以相关服务合并
    • 异步消息队列
  • 服务间数据一致性

    • 2pc
    • 确保最终一致性
  • 上帝类

    • 上帝类是整个程序的全局类

      image-20210808123425398

      1
      2
      3
      4
      5
      6
      7
      ![image-20210808124404388](https://cdn.jsdelivr.net/gh/631068264/img/008i3skNgy1gt9a5y1dirj320o0nkgu7.jpg)
      
      ![image-20210808124843404](https://cdn.jsdelivr.net/gh/631068264/img/008i3skNgy1gt9aasnx3cj31820u00wy.jpg)
      	
      **每个领域模型中的Order类表示同一个Order业务实体的不同方向**,(简化原始Order类)
      	
      ![image-20210808125540039](https://cdn.jsdelivr.net/gh/631068264/img/008i3skNgy1gt9ai0hyz6j322w0ecn2z.jpg)
      

服务拆分

  • 以业务模型切入:比如产品,用户,订单为一个模型来切入
  • 演进式拆分:刚开始不要划分太细,可以随着迭代过程来逐步优化
  • 避免环形依赖与双向依赖:尽量不要做服务之间的循环依赖

具体操作

  • 调整代码结构,分析模块间依赖

    • 一个聚合的代码直接访问另一个聚合的代码或数据表
    • 单元测试代码混杂在一起
    • 跨聚合查询的SQL语句
  • 消除业务代码依赖
    • 同时依赖一个工具类,独立出来,最后可以复制到两个服务
    • 上游系统依赖下游系统的接口,需要分析是否上游系统的业务概念中有缺失信息
    • 下游系统通过接口实现数据库实体嵌套,需要分析下游系统是否需要保存这么多上游系统的信息
  • 添加测试保护
    • 是否有足够的测试来保护我们的改动
    • 函数级别的单元测试
    • 接口级别的测试

依赖消除

img

循环依赖危害

  • 服务之间的调用没有约束导致的,耦合性非常强,以”微服务“为设计目标的系统会逐渐演变成一个分布式大单体
  • 循环调用或并发问题,造成一些复杂难以定位的问题

消除循环依赖的基本思路

  • 定义服务上下游关系,下游服务可以直接依赖上游服务,反之则不可

  • 上游服务的变更对下游服务产生影响需要通过异步(消息队列,缓存等)的方式来实现,此时各个微服务之间不再是强一致性,而是基于事件的最终一致性

  • 服务之间要通过数据 Id(或类 Id,能够唯一代表数据且不变的属性)来进行关联,尽量不做过多的数据冗余

  • 一旦需要上游服务调用下游服务才能完成业务时,要考虑是否上游服务缺少业务概念

  • 为满足前端逻辑而导致的服务间交互逻辑要放到 BFF(Backend for frontend)中,而不是增加服务间的调用

  • 上移、下移

    两个相互依赖组件中的交互部分抽象出来形成一个新的组件,而新组件同时包含着原有两个组件的引用,这样就把循环依赖关系剥离出来并上升到一个更高层次的组件中,通过在两个相互循环依赖的组件之间添加中间层,变循环依赖为间接依赖。

服务通信

image-20210808163800581

image-20210808164015653

image-20210808164139637

image-20210808212640221

异步通信要注意

  • 事务性消息
  • 消息只被消费一次

断路器

image-20210808170422297

规定时间内多次失败,立即拒绝其他调用

image-20210808170716591

防止故障扩散

  • 让OrderServiceProxy有正确处理无响应服务的能力
  • 如何从失败的无响应服务恢复

具体操作

  • 请求超时设置

  • 限制客户端请求数量

  • 断路器模式

    监控请求成功和失败数量,失败比例 > 阈值,就启动断路器,拒绝之后的请求,过一段时间,请求成功,解除断路器。

多个服务组合,根据数据重要情况调整响应策略

image-20210808173248419

服务发现

image-20210808174350857

  • 服务端和客户端直接和服务注册表进行交互

    • 服务端自注册,心跳报告到注册表

    • 客户端查询注册表并缓存,通过负载均衡访问某个服务端

  • 缺点

    • 开发者需要为不同语言准备服务发现框架
    • 开发者需要自己设置管理注册表

    image-20210808175242444

部署平台比较方便

image-20210808210919328

image-20210808211118242

image-20210808211846254

基础架构

  • 注册中心:注册并维护远程服务及服务提供者的地址,供服务消费者发现和调用,为保证可用性,通常基于分布式 kv 存储器来实现,比如 zookeeper、etcd 等;
  • 服务框架:用于实现微服务的 RPC 框架,包含服务接口描述及实现方案、向注册中心发布服务等功能,常见的 RPC 框架包括 Spring Cloud、Dubbo、gRPC、 Thrift、go-micro 等;
  • 服务网关:介于客户端与微服务之间的网关层,可以理解为「门卫」的角色,以确保服务提供者对客户端的透明,这一层可以进行反向路由、安全认证、灰度发布、日志监控等前置动作;
  • 服务监控:对服务消费者与提供者之间的调用情况进行监控和数据展示;
  • 服务追踪:记录对每个请求的微服务调用完整链路,以便进行问题定位和故障分析;
  • 服务治理:服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行,这些手段包括熔断、隔离、限流、降级、负载均衡等。
  • 基础设施:分布式消息队列、日志存储、数据库、缓存、文件服务器、搜索集群等,用以提供服务底层的基础数据服务,可以自建,也可以使用阿里云等公有云提供的服务。

Backends for frontends 网关

这种模式是针对不同的客户端来实现一个不同的 API 网关,也就是「用于前端的后端」(Backends for frontends,检查 BFF)模式,对应的架构图

注册中心

服务的发布和订阅,当你通过服务提供者提供了一个新的服务,如何让服务消费者感知呢?这个时候就需要一个类似注册中心的角色,服务提供者将自己提供的服务以及 IP 地址登记到注册中心,服务消费者则从注册中心查询所需要调用服务的地址信息,然后发起网络请求。

  • 服务提供者在启动时,根据服务发布文件中配置的发布信息向注册中心注册自己的服务。
  • 服务消费者在启动时,根据消费者配置文件中配置的服务信息向注册中心订阅自己所需要的服务。
  • 注册中心返回服务提供者地址列表给服务消费者。
  • 当服务提供者发生变化,比如有节点新增或者销毁,注册中心将变更通知给服务消费者。
  1. GreeterService 服务端启动时,会向 Consul 发送一个 POST 请求,告诉 Consul 自己的 IP 和端口;
  2. Consul 接收到 GreeterService 的注册请求后,每隔 10s(默认)会向 GreeterService 发送一个健康检查的请求,检验 GreterService 是否有效(心跳检测);
  3. 当客户端以 HTTP 接口方式发送 GET 请求 /greeter/say/helloGreeterService 时,会先从 Consul 中拿到一个存储对应服务 IP 和端口的临时表,并从表中查询 GreeterService 的 IP 和端口,再发送 GET 方式请求到 /greeter/say/hello。该临时表每隔 10s 会更新,只包含有通过了健康检查的 Service。此外,为了提高性能和系统可用性,往往会缓存服务信息到本地,如果服务部署在多个机器,还会使用负载均衡算法选择指定服务端通信。

传入指定服务名称,再通过默认 Registry 实现 Consul 获取对应的服务实例列表并缓存。应用过滤器后最后通过默认负载均衡实现(这里是 Random 算法)返回指定节点,获取到远程服务实例节点后,就可以发起远程服务请求了,发起远程网络请求(通过协程实现),如果请求处理出错则返回相应错误信息,然后对服务调用成功与否通过 Selector 的 Mark 函数进行标记(以便后续对服务进行监控和治理)如果服务调用失败,则进行重试或报错处理

服务框架

  • 服务通信采用什么协议?就是说服务提供者和服务消费者之间以什么样的协议进行网络通信,是采用四层 TCP、UDP 协议,还是采用七层 HTTP 协议,还是采用其他协议?
  • 数据传输采用什么方式?就是说服务提供者和服务消费者之间的数据传输采用哪种方式,是同步还是异步,是在单连接上传输,还是多路复用。
  • 数据压缩采用什么格式?通常数据传输都会对数据进行压缩,来减少网络传输的数据量,从而减少带宽消耗和网络传输时间,比如常见的 JSON 序列化、Java 对象序列化以及 Protobuf 序列化等。

服务监控

一旦服务消费者与服务提供者之间能够正常发起服务调用,你就需要对调用情况进行监控,以了解服务是否正常。通常来讲,服务监控主要包括三个流程。

  • 指标收集。就是要把每一次服务调用的请求耗时以及成功与否收集起来,并上传到集中的数据处理中心。
  • 数据处理。有了每次调用的请求耗时以及成功与否等信息,就可以计算每秒服务请求量、平均耗时以及成功率等指标。
  • 数据展示。数据收集起来,经过处理之后,还需要以友好的方式对外展示,才能发挥价值。通常都是将数据展示在 Dashboard 面板上,并且每隔 10s 等间隔自动刷新,用作业务监控和报警等。

服务追踪

除了需要对服务调用情况进行监控之外,你还需要记录服务调用经过的每一层链路,以便进行问题追踪和故障定位。

服务追踪的工作原理大致如下:

  • 服务消费者发起调用前,会在本地按照一定的规则生成一个 requestid,发起调用时,将 requestid 当作请求参数的一部分,传递给服务提供者。
  • 服务提供者接收到请求后,记录下这次请求的 requestid,然后处理请求。如果服务提供者继续请求其他服务,会在本地再生成一个自己的 requestid,然后把这两个 requestid 都当作请求参数继续往下传递。
  • 以此类推,通过这种层层往下传递的方式,一次请求,无论最后依赖多少次服务调用、经过多少服务节点,都可以通过最开始生成的 requestid 串联所有节点,从而达到服务追踪的目的。

要实现链路跟踪,每次服务调用会在 HTTP 的 HEADERS 中记录至少记录四项数据:

  • traceId:traceId 标识一个用户请求的调用链路。具有相同 traceId 的调用属于同一条链路。
  • spanId:标识一次服务调用的ID,即链路跟踪的节点 ID。
  • parentId:父节点的 spanId。
  • requestTime & responseTime:请求时间和响应时间。

服务治理

服务监控能够发现问题,服务追踪能够定位问题所在,而解决问题就得靠服务治理了。服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行。

在生产环境中,你应该经常会遇到下面几种状况。

  • 单机故障。通常遇到单机故障,都是靠运维发现并重启服务或者从线上摘除故障节点。然而集群的规模越大,越是容易遇到单机故障,在机器规模超过一百台以上时,靠传统的人肉运维显然难以应对。而服务治理可以通过一定的策略,自动摘除故障节点,不需要人为干预,就能保证单机故障不会影响业务
  • 单 IDC 故障。你应该经常听说某某 App,因为施工挖断光缆导致大批量用户无法使用的严重故障。而服务治理可以通过自动切换故障 IDC 的流量到其他正常 IDC,可以避免因为单 IDC 故障引起的大批量业务受影响。
  • 依赖服务不可用。比如你的服务依赖依赖了另一个服务,当另一个服务出现问题时,会拖慢甚至拖垮你的服务。而服务治理可以通过熔断,在依赖服务异常的情况下,一段时期内停止发起调用而直接返回。这样一方面保证了服务消费者能够不被拖垮,另一方面也给服务提供者减少压力,使其能够尽快恢复。

熔断

  • 异常幅度达到设定的阀值后触发的系统保护机制
  • 保护机制会将某部分能力关闭,以保证大部分能力正常
  • 在关闭一段时间后,会自动触发恢复检测,如果发现服务正常,则将服务逐渐开放。
  • 这种机制是有损的,但是利大于端

调用链出问题

下游系统本身就出现了问题,不断的请求又把系统问题加重了,恢复困难

上游有问题可能会拖慢下游整个进度增加了整个链路的请求时间

熔断模式可以防止应用程序不断地尝试可能超时和失败的服务

熔断器模式(Circuit Breaker Pattern),是一个现代软件开发的设计模式。用以侦测错误,并避免不断地触发相同的错误(如维护时服务不可用、暂时性的系统问题或是未知的系统错误)。

状态描述:

  • 关闭:熔断器默认处于关闭状态,熔断器本身带有计数能力(如滑动窗口实现),当失败数量达到预设阀值后,触发状态变更,熔断器被打开
  • 开启:在一定时间内,所有请求都会被拒绝,或采用备用链路处理。
  • 半开启:在刷新时间窗口后,会进入半开启状态,熔断器尝试接受请求,如果这阶段出现请求失败,直接恢复到开启状态。

降级

解决资源不足和访问量增加的矛盾

停止不重要的功能,从而释放出更多的资源

限流

根据一定的用户规则进行拒绝策略

降级甚至关掉后台的某些服务

利用队列把请求缓存住。削峰填谷。

流量就像进入漏斗中的水一样,而出去的水和我们系统处理的请求一样,当流量大于漏斗的流出速度,就会出现积水,水对了会溢出。

漏斗很多是用一个队列实现的,当流量过多时,队列会出现积压,队列满了,则开始拒绝请求。

常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:

令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.

令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.

调用链超时

TP50、TP90、TP99 是工程性能指标,以网络请求耗时为例:

  • TP50:表示满足百分之五十的网络请求所需的最低耗时。
  • TP90:表示满足百分之九十的网络请求所需的最低耗时。
  • TP99:表示满足百分之九十九的网络请求所需的最低耗时。

举个例子:有四次请求耗时分别为:

10ms,1000ms,100ms,2ms

那么我们可以这样计算TP99:4次请求中,99%的请求数为4*0.99,进位取整也就是4次,满足这全部4次请求的的最低耗时为1000ms,也就是TP99的答案是1000ms。

超时时间和重试次数的设置,需要考虑整个调用链中所有依赖服务的耗时、各个服务是否是核心服务等很多因素

带来副作用

  • 重复请求

    有可能 Provider 执行完了,但是因为网络抖动 Consumer 认为超时了,这种情况下重试机制就会导致重复请求,从而带来脏数据问题,因此服务端必须考虑接口的幂等性。

  • 降低 Consumer 的负载能力

    Provider确实存在性能问题,这样重试多次也是没法成功的,反而会使得 Consumer 的平均响应时间变长。如果 Consumer 是一个高 QPS 的服务,还有可能引起连锁反应造成雪崩。

  • 爆炸式的重试风暴

最底层的服务 D 出现超时,这样上游服务都将发起重试。

假设重试次数都设置的 3 次,那么 B 将面临正常情况下 3 倍的负载量,C 是 9 倍,D 是 27 倍,整个服务集群可能因此雪崩。

合理设置

  • 设置调用方的超时时间之前,先了解清楚依赖服务的 TP99 响应时间是多少(如果依赖服务性能波动大,也可以看 TP95),调用方的超时时间可以在此基础上加 50%

  • 如果 RPC 框架支持多粒度的超时设置,则:全局超时时间应该要略大于接口级别最长的耗时时间,每个接口的超时时间应该要略大于方法级别最长的耗时时间,每个方法的超时时间应该要略大于实际的方法执行时间。

  • 区分是可重试服务还是不可重试服务,如果接口没实现幂等则不允许设置重试次数。注意:读接口是天然幂等的,写接口则可以使用业务单据 ID 或者在调用方生成唯一 ID 传递给服务端,通过此 ID 进行防重避免引入脏数据。
  • 如果 RPC 框架支持服务端的超时设置,同样基于前面3条规则依次进行设置,这样能避免客户端不设置的情况下配置是合理的,减少隐患。
  • 如果从业务角度来看,服务可用性要求不用那么高(比如偏内部的应用系统),则可以不用设置超时重试次数,直接人工重试即可,这样能减少接口实现的复杂度,反而更利于后期维护。
  • 重试次数设置越大,服务可用性越高,业务损失也能进一步降低,但是性能隐患也会更大,这个需要综合考虑设置成几次(一般是 2 次,最多 3 次)。
  • 如果调用方是高 QPS 服务,则必须考虑服务方超时情况下的降级和熔断策略。(比如超过 10% 的请求出错,则停止重试机制直接熔断,改成调用其他服务、异步 MQ 机制、或者使用调用方的缓存数据)

从单体到微服务

image-20210808223259396

  • 新功能用微服务

    image-20210808224318245

  • 隔离表现层和后端

  • 提取功能到服务,分解单体