关于微服务:服务发现与配置管理高可用最佳实践

3次阅读

共计 9003 个字符,预计需要花费 23 分钟才能阅读完成。

作者:三辰|阿里云云原生微服务基础架构团队技术专家,负责 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% 左近不再稳定。

小结

通过实际,咱们模仿了一次实在的危险产生的场景,并且通过客户端的高可用计划(推空爱护),胜利实现了对危险的管制,避免服务调用的产生异样。

正文完
 0