作者:三辰|阿里云云原生微服务根底架构团队技能专家,担任 MSE 引擎高可用架构

****本篇是微服务高可用最佳实践系列共享的开篇,系列内容继续更新中,期待咱们的重视。

导言

在开端正式内容之前,先给咱们共享一个实在的事例。

某客户在阿里云上运用 K8s 集群布置了许多自己的微服务,可是某一天,其间一台节点的网卡产生了反常,终究导致服务不可用,无法调用下流,事务受损。咱们来看一下这个问题链是怎么形成的?

  1. ECS 毛病节点上运转着 K8s 集群的中心根底组件 CoreDNS 的一切 Pod,它没有打散,导致集群 DNS 解析呈现问题。

  2. 该客户的服务发现运用了有缺点的客户端版别(nacos-client 的 1.4.1 版别),这个版别的缺点就是跟 DNS 有关——心跳恳求在域名解析失败后,会导致进程后续不会再续约心跳,只有重启才能恢复。

  3. 这个缺点版别实际上是已知问题,阿里云在 5 月份推送了 nacos-client 1.4.1 存在严重 bug 的公告,但客户研制未收到告诉,进而在出产环境中运用了这个版别。

危险环环相扣,缺一不可。

终究导致毛病的原因是服务无法调用下流,可用性下降,事务受损。下图暗示的是客户端缺点导致问题的根因:

  1. Provider 客户端在心跳续约时产生 DNS 反常;

  2. 心跳线程正确地处理这个 DNS 反常,导致线程意外退出了;

  3. 注册中心的正常机制是,心跳不续约,30 秒后主动下线。由于 CoreDNS 影响的是整个 K8s 集群的 DNS 解析,所以 Provider 的一切实例都遇到相同的问题,整个服务一切实例都被下线;

  4. 在 Consumer 这一侧,收到推送的空列表后,无法找到下流,那么调用它的上游(比方网关)就会产生反常。

回顾整个事例,每一环每个危险看起来产生概率都很小,可是一旦产生就会形成恶劣的影响。

所以,本篇文章就来讨论,微服务领域的高可用方案怎么规划,细化到服务发现和装备办理领域,都有哪些详细的方案。

微服务高可用方案

首要,有一个事实不容改变:没有任何体系是百分百没有问题的,所以高可用架构方案就是面对失败(危险)规划的。

危险是无处不在的,尽管有许多产生概率很小很小,却都无法彻底防止。

在微服务体系中,都有哪些危险的或许?

这仅仅其间一部分,可是在阿里巴巴内部十几年的微服务实践进程中,这些问题全部都遇到过,并且有些还不止一次。虽然看起来坑许多,但咱们依然能够很好地保证双十一大促的稳定,背面靠的就是成熟稳健的高可用体系建造。

咱们不能彻底防止危险的产生,但咱们能够操控它(的影响),这就是做高可用的实质。

操控危险有哪些战略?

注册装备中心在微服务体系的中心链路上,牵一发起全身,任何一个颤动都或许会较大范围地影响整个体系的稳定性。

战略一:缩小危险影响范围

集群高可用

多副本: 不少于 3 个节点进行实例布置。

多可用区(同城容灾): 将集群的不同节点布置在不同可用区(AZ)中。当节点或可用区产生的毛病时,影响范围仅仅集群其间的一部分,假如能够做到迅速切换,并将毛病节点主动离群,就能尽或许削减影响。

削减上下流依托

体系规划上应该尽或许地削减上下流依托,越多的依托,或许会在被依托体系产生问题时,让全体服务不可用(一般是一个功用块的不可用)。假如有必要的依托,也有必要要求是高可用的架构。

改变可灰度

新版别迭代发布,应该从最小范围开端灰度,按用户、按 Region 分级,逐渐扩大改变范围。一旦呈现问题,也仅仅在灰度范围内形成影响,缩小问题爆破半径。

服务可降级、限流、熔断

  • 注册中心反常负载的状况下,降级心跳续约时刻、降级一些非中心功用等

  • 针对反常流量进行限流,将流量约束在容量范围内,维护部分流量是可用的

  • 客户端侧,反常时降级到运用本地缓存(推空维护也是一种降级方案),暂时牺牲列表更新的一致性,以保证可用性

如图,微服务引擎 MSE 的同城双活三节点的架构,经过精简的上下流依托,每一个都保证高可用架构。多节点的 MSE 实例,经过底层的调度才能,会主动分配到不同的可用区上,组成多副本集群。

战略二:缩短危险产生继续时刻

中心思路就是:尽早辨认、赶快处理

辨认 —— 可观测

例如,依据 Prometheus 对实例进行监控和报警才能建造。

进一步地,在产品层面上做更强的观测才能:包括大盘、告警收敛/分级(辨认问题)、针对大客户的保证、以及服务等级的建造。

MSE注册装备中心目前供给的服务等级是 99.95%,并且正在向 4 个 9(99.99%)迈进。

快速处理 —— 应急呼应

应急呼应的机制要树立,快速有效地告诉到正确的人员范围,快速执行预案的才能(意识到白屏与黑屏的功率差异),常态化地进行毛病应急的演练。

预案是指不管熟不熟悉你的体系的人,都能够放心执行,这背面需求一套沉淀好有含金量的技能支撑(技能厚度)。

战略三:削减触碰危险的次数

削减不必要的发布,例如:添加迭代功率,不随意发布;重要事件、大促期间进行封网。

从概率视点来看,无论危险概率有多低,不断测验,危险产生的联合概率就会无限趋近于 1。

战略四:下降危险产生概率

架构晋级,改进规划

Nacos2.0,不仅是性能做了提高,也做了架构上的晋级:

  1. 晋级数据存储结构,Service 级粒度提高到到 Instance 级分区容错(绕开了 Service 级数据不一致形成的服务挂的问题);

  2. 晋级衔接模型(长衔接),削减对线程、衔接、DNS 的依托。

提早发现危险

  1. 这个「提早」是指在规划、研制、测试阶段尽或许地露出潜在危险;

  2. 提早经过容量评估预知容量危险水位是在哪里;

  3. 经过定时的毛病演练提早发现上下流环境危险,验证体系健壮性。

如图,阿里巴巴大促高可用体系,不断做压测演练、验证体系健壮性和弹性、观测追寻体系问题、验证限流、降级等预案的可执行性。

服务发现高可用方案

服务发现包括服务顾客(Consumer)和服务供给者(Provider)。

Consumer 端高可用

经过推空维护、服务降级等手段,达到 Consumer 端的容灾目的。

推空维护

能够应对开头讲的事例,服务空列表推送主动降级到缓存数据。

服务顾客(Consumer)会从注册中心上订阅服务供给者(Provider)的实例列表。

当遇到突发状况(例如,可用区断网,Provider端无法上报心跳) 或 注册中心(变配、重启、升降级)呈现非预期反常时,都有或许导致订阅反常,影响服务顾客(Consumer)的可用性。

无推空维护

  • Provider 端注册失败(比方网络、SDKbug 等原因)

  • 注册中心判别 Provider 心跳过期

  • Consumer 订阅到空列表,事务中止报错

敞开推空维护

  • 同上

  • Consumer 订阅到空列表,推空维护收效,丢弃改变,保证事务服务可用

敞开方法

敞开方法比较简略

开源的客户端 nacos-client 1.4.2 以上版别支撑

装备项

  • SpingCloudAlibaba 在 spring 装备项里添加:
    ​​​spring.cloud.nacos.discovery.namingPushEmptyProtection=true​

  • Dubbo 加上 registryUrl 的参数:
    ​​​namingPushEmptyProtection=true​

提空维护依托缓存,所以需求耐久化缓存目录,防止重启后丢失,途径为:​​${user.home}/nacos/naming/${namespaceId}​

服务降级

Consumer 端能够依据不同的战略挑选是否将某个调用接口降级,起到对事务恳求流程的维护(将宝贵的下流 Provider 资源保留给重要的事务 Consumer 运用),维护重要事务的可用性。

服务降级的详细战略,包括回来 Null 值、回来 Exception 反常、回来自定义 JSON 数据和自定义回调。

MSE 微服务办理中心中默许就具有该项高可用才能。

Provider 端高可用

Provider 侧经过注册中心和服务办理供给的容灾维护、离群去除、无损下线等方案提高可用性。

容灾维护

容灾维护首要用于防止集群在反常流量下呈现雪崩的场景。

下面咱们来详细看一下:

无容灾维护(默许阈值 =0)

  • 突发恳求量添加,容量水位较高时,个别 Provider 产生毛病;

  • 注册中心将毛病节点去除,全量流量会给剩余节点;

  • 剩余节点负载变高,大概率也会毛病;

  • 最后一切节点毛病,100% 无法供给服务。

敞开容灾维护(阈值=0.6)

  • 同上;

  • 毛病节点数达到维护阈值,流量平摊给一切机器;

  • 终究****保证 50% 节点能够供给服务。

容灾维护才能,在紧急状况下,能够保存服务可用性在必定的水平之上,能够说是全体体系的兜底了。

这套方案从前救过不少事务体系。

离群实例去除

心跳续约是注册中心感知实例可用性的基本途径。

可是在特定状况下,心跳存续并不能彻底等同于服务可用。

由于依然存在心跳正常,但服务不可用的状况,例如:

  • Request 处理的线程池

  • 依托的 RDS 衔接反常或慢 SQL

微服务办理中心供给离群实例去除

  • 依据反常检测的去除战略:包括网络反常和网络反常 + 事务反常(HTTP 5xx)

  • 设置反常阈值、QPS 下限、去除比例下限

离群实例去除的才能是一个补充,依据特定接口的调用反常特征,来衡量服务的可用性。

无损下线

无损下线,又名高雅下线、或许滑润下线,都是一个意思。首要看什么是有损下线:

Provider 实例进行晋级进程中,下线后心跳在注册中心存约以及改变收效都有必定的时刻,在这个期间 Consumer 端订阅列表依然没有更新到下线后的版别,假如鲁莽地将 Provider 停止服务,会形成一部分的流量丢失。

无损下线有许多不同的解决方案,但侵入性最低的仍是服务办理中心默许供给的才能,无感地整合到发布流程中,完结主动执行。免除繁琐的运维脚本逻辑的维护。

装备办理高可用方案

装备办理首要包括装备订阅装备发布两类操作。

装备办理解决什么问题?

多环境、多机器的装备发布、装备动态实时推送。

依据装备办理做服务高可用

微服务怎么依据装备办理做高可用方案?

发布环境办理

一次办理上百台机器、多套环境,怎么正确无误地推送、误操作或呈现线上问题怎么快速回滚,发布进程怎么灰度。

事务开关动态推送

功用、活动页面等开关。

容灾降级预案的推送

预置的方案经过推送敞开,实时调整流控阈值等。

上图是大促期间装备办理全体高可用解决方案。比方降级非中心事务、功用降级、日志降级、禁用高危险操作。

客户端高可用

装备办理客户端侧同样有容灾方案。

本地目录分为两级,高优先级是容灾目录、低优先级是缓存目录。

缓存目录: 每次客户端和装备中心进行数据交互后,会保存最新的装备内容至本地缓存目录中,当服务端不可用状态下,会运用本地缓存目录中内容。

容灾目录: 当服务端不可用状态下,能够在本地的容灾目录中手动更新装备内容,客户端会优先加载容灾目录下的内容,模仿服务端改变推送的效果。

简略来说,当装备中心不可用时,优先检查容灾目录的装备,否则运用之前拉取到的缓存。

容灾目录的规划,是由于有时候不必定会有缓存过的装备,或许事务需求紧急掩盖运用新的内容敞开一些必要的预案和装备。

全体思路就是,无法产生什么问题,无论怎么,都要能够使客户端能够读取到正确的装备,保证微服务的可用性。

服务端高可用

在装备中心侧,首要是针对读、写的限流。
约束衔接数、约束写:

  • 限衔接:单机最大衔接限流,单客户端 IP 的衔接限流

  • 限写接口:发布操作&特定装备的秒级分钟级数量限流

操控操作危险

操控人员做装备发布的危险。

装备发布的操作是可灰度、可追溯、可回滚的。

装备灰度

发布前史&回滚

改变对比

着手实践

最后咱们一起来做一个实践。

场景取自前面说到的一个高可用方案,在服务供给者一切机器产生注册反常的状况下,看服务顾客在推空维护翻开的状况下的表现。

实验架构和思路

上图是本次实践的架构,右侧是一个简略的调用场景,外部流量经过网关接入,这儿挑选了 MSE 产品矩阵中的云原生网关,依托它供给的可观测才能,方便咱们调查服务调用状况。

网关的下流有 A、B、C 三个运用,支撑运用装备办理的方法动态地将调用联系衔接起来,后面咱们会实践到。

基本思路:

  1. 布置服务,调整调用联系是网关->A->B->C,检查网关调用成功率。

  2. 经过模仿网络问题,将运用B与注册中心的心跳链路断开,模仿注册反常的产生。

  3. 再次检查网关调用成功率,期望服务 A->B 的链路不受注册反常的影响。

为了方便对照,运用 A 会布置两种版别,一种是敞开推空维护的,一种是没有敞开的状况。终究期望的结果是,推空维护开关敞开后,能够协助运用 A 在产生反常的状况下,继续能够寻址到运用B。

网关的流量打到运用 A 之后,能够调查到,接口的成功率应该正好在 50%。

开端

接下来开端着手实践吧。这儿我选用阿里云 MSE+ACK 组合做完好的方案。

环境预备

首要,购买好一套 MSE 注册装备中心专业版,和一套 MSE 云原生网关。这边不介绍详细的购买流程。

在运用布置前,提早预备好装备。这边咱们能够先装备 A 的下流是 C,B 的下流也是 C。

布置运用

接下来咱们依据 ACK 布置三个运用。能够从下面的装备看到,运用 A 这个版别 ​​spring-cloud-a-b​​,推空维护开关现已翻开。

这儿 demo 选用的 nacos 客户端版别是 1.4.2,由于推空维护在这个版别之后才支撑。

装备暗示(无法直接运用):

# A 运用 base 版别
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-a
  name: spring-cloud-a-b
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-a
  template:
    metadata:
      annotations:
        msePilotCreateAppName: spring-cloud-a
      labels:
        app: spring-cloud-a
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: spring.cloud.nacos.discovery.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.cloud.nacos.config.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.cloud.nacos.discovery.metadata.version
          value: base
        - name: spring.application.name
          value: sc-A
        - name: spring.cloud.nacos.discovery.namingPushEmptyProtection
          value: "true"
        image: mse-demo/demo:1.4.2
        imagePullPolicy: Always
        name: spring-cloud-a
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-a
  name: spring-cloud-a
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-a
  template:
    metadata:
      annotations:
        msePilotCreateAppName: spring-cloud-a
      labels:
        app: spring-cloud-a
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: spring.cloud.nacos.discovery.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.cloud.nacos.config.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.cloud.nacos.discovery.metadata.version
          value: base
        - name: spring.application.name
          value: sc-A
        image: mse-demo/demo:1.4.2
        imagePullPolicy: Always
        name: spring-cloud-a
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
# B 运用 base 版别
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-b
  name: spring-cloud-b
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-b
  strategy:
  template:
    metadata:
      annotations:
        msePilotCreateAppName: spring-cloud-b
      labels:
        app: spring-cloud-b
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: spring.cloud.nacos.discovery.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.cloud.nacos.config.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.application.name
          value: sc-B
        image: mse-demo/demo:1.4.2
        imagePullPolicy: Always
        name: spring-cloud-b
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
# C 运用 base 版别
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-c
  name: spring-cloud-c
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-c
  template:
    metadata:
      annotations:
        msePilotCreateAppName: spring-cloud-c
      labels:
        app: spring-cloud-c
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: spring.cloud.nacos.discovery.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.cloud.nacos.config.server-addr
          value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
        - name: spring.application.name
          value: sc-C
        image: mse-demo/demo:1.4.2
        imagePullPolicy: Always
        name: spring-cloud-c
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi

布置运用:

在网重视册服务

运用布置好之后,在 MSE 云原生网关中,关联上 MSE 的注册中心,并将服务注册进来。

咱们规划的是网关只调用 A,所以只需求将 A 放进来注册进来即可。

验证和调整链路

依据 curl 指令验证一下链路:

$ curl http://${网关IP}/ip
sc-A[192.168.1.194] --> sc-C[192.168.1.195]

验证一下链路。 能够看到这时候 A 调用的是 C,咱们将装备做一下改变,实时地将 A 的下流改为 B。

再看一下,这时三个运用的调用联系是 ABC,符合咱们之前的方案。

$ curl http://${网关IP}/ip
sc-A[192.168.1.194] --> sc-B[192.168.1.191] --> sc-C[192.168.1.180]

接下来,咱们经过一段指令,接连地调用接口,模仿实在场景下不连续的事务流量。

$ while true; do sleep .1 ; curl -so /dev/null http://${网关IP}/ip ;done

观测调用

经过网关监控大盘,能够调查到成功率。

注入毛病

一切正常,现在咱们能够开端注入毛病。

这儿咱们能够运用 K8s 的 NetworkPolicy 的机制,模仿出口网络反常。

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  name: block-registry-from-b
spec:
  podSelector:
    matchLabels:
      app: spring-cloud-b
  ingress:
  - {}
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
    ports:
    - protocol: TCP
      port: 8080

这个 8080 端口的意思是,不影响内网调用下流的运用端口,只禁用其它出口流量(比方到达注册中心的 8848 端口就被禁用了)。这儿 B 的下流是 C。

网络切断后,注册中心的心跳续约不上,过一会儿(30 秒后)就会将运用 B 的一切 IP 去除。

再次观测

再调查大盘数据库,成功率开端下降,这时候,在操控台上现已看不到运用 B 的 IP 了。

回到大盘,成功率在 50% 附近不再波动。

小结

经过实践,咱们模仿了一次实在的危险产生的场景,并且经过客户端的高可用方案(推空维护),成功实现了对危险的操控,防止服务调用的产生反常。