服务发现与配置管理高可用最佳实践
本篇是微服务高可用最佳实践系列分享的开篇,系列内容持续更新中,期待大家的关注。
引言
Cloud Native
某客户在阿里云上使用 K8s 集群部署了许多自己的微服务,但是某一天,其中一台节点的网卡发生了异常,最终导致服务不可用,无法调用下游,业务受损。
ECS 故障节点上运行着 K8s 集群的核心基础组件 CoreDNS 的所有 Pod,它没有打散,导致集群 DNS 解析出现问题。
该客户的服务发现使用了有缺陷的客户端版本(nacos-client 的 1.4.1 版本),这个版本的缺陷就是跟 DNS 有关——心跳请求在域名解析失败后,会导致进程后续不会再续约心跳,只有重启才能恢复。
这个缺陷版本实际上是已知问题,阿里云在 5 月份推送了 nacos-client 1.4.1 存在严重 bug 的公告,但客户研发未收到通知,进而在生产环境中使用了这个版本。
Provider 客户端在心跳续约时发生 DNS 异常; 心跳线程正确地处理这个 DNS 异常,导致线程意外退出了; 注册中心的正常机制是,心跳不续约,30 秒后自动下线。由于 CoreDNS 影响的是整个 K8s 集群的 DNS 解析,所以 Provider 的所有实例都遇到相同的问题,整个服务所有实例都被下线; 在 Consumer 这一侧,收到推送的空列表后,无法找到下游,那么调用它的上游(比如网关)就会发生异常。
微服务高可用方案
Cloud Native
虽然看起来坑很多,但我们依然能够很好地保障双十一大促的稳定,背后靠的就是成熟稳健的高可用体系建设。
注册配置中心在微服务体系的核心链路上,牵一发动全身,任何一个抖动都可能会较大范围地影响整个系统的稳定性。
集群高可用
减少上下游依赖
变更可灰度
服务可降级、限流、熔断
注册中心异常负载的情况下,降级心跳续约时间、降级一些非核心功能等
针对异常流量进行限流,将流量限制在容量范围内,保护部分流量是可用的
客户端侧,异常时降级到使用本地缓存(推空保护也是一种降级方案),暂时牺牲列表更新的一致性,以保证可用性
识别 —— 可观测
MSE注册配置中心目前提供的服务等级是 99.95%,并且正在向 4 个 9(99.99%)迈进。
快速处理 —— 应急响应
预案是指不管熟不熟悉你的系统的人,都可以放心执行,这背后需要一套沉淀好有含金量的技术支撑(技术厚度)。
从概率角度来看,无论风险概率有多低,不断尝试,风险发生的联合概率就会无限趋近于 1。
架构升级,改进设计
升级数据存储结构,Service 级粒度提升到到 Instance 级分区容错(绕开了 Service 级数据不一致造成的服务挂的问题); 升级连接模型(长连接),减少对线程、连接、DNS 的依赖。
提前发现风险
这个「提前」是指在设计、研发、测试阶段尽可能地暴露潜在风险; 提前通过容量评估预知容量风险水位是在哪里; 通过定期的故障演练提前发现上下游环境风险,验证系统健壮性。
服务发现高可用方案
Cloud Native
推空保护
Provider 端注册失败(比如网络、SDKbug 等原因)
注册中心判断 Provider 心跳过期
Consumer 订阅到空列表,业务中断报错
同上
Consumer 订阅到空列表,推空保护生效,丢弃变更,保障业务服务可用
开启方式
开源的客户端 nacos-client 1.4.2 以上版本支持
SpingCloudAlibaba 在 spring 配置项里增加:
spring.cloud.nacos.discovery.namingPushEmptyProtection=true
Dubbo 加上 registryUrl 的参数:
namingPushEmptyProtection=true
服务降级
容灾保护
突发请求量增加,容量水位较高时,个别 Provider 发生故障;
注册中心将故障节点摘除,全量流量会给剩余节点;
剩余节点负载变高,大概率也会故障;
最后所有节点故障,100% 无法提供服务。
同上;
故障节点数达到保护阈值,流量平摊给所有机器;
最终保障 50% 节点能够提供服务。
这套方案曾经救过不少业务系统。
离群实例摘除
但是在特定情况下,心跳存续并不能完全等同于服务可用。
因为仍然存在心跳正常,但服务不可用的情况,例如:
Request 处理的线程池满
依赖的 RDS 连接异常或慢 SQL
基于异常检测的摘除策略:包含网络异常和网络异常 + 业务异常(HTTP 5xx)
设置异常阈值、QPS 下限、摘除比例下限
无损下线
配置管理高可用方案
Cloud Native
客户端高可用
本地目录分为两级,高优先级是容灾目录、低优先级是缓存目录。
容灾目录的设计,是因为有时候不一定会有缓存过的配置,或者业务需要紧急覆盖使用新的内容开启一些必要的预案和配置。
服务端高可用
在配置中心侧,主要是针对读、写的限流。
限连接:单机最大连接限流,单客户端 IP 的连接限流
限写接口:发布操作&特定配置的秒级分钟级数量限流
控制操作风险
动手实践
Cloud Native
场景取自前面提到的一个高可用方案,在服务提供者所有机器发生注册异常的情况下,看服务消费者在推空保护打开的情况下的表现。
部署服务,调整调用关系是网关->A->B->C,查看网关调用成功率。 通过模拟网络问题,将应用B与注册中心的心跳链路断开,模拟注册异常的发生。 再次查看网关调用成功率,期望服务 A->B 的链路不受注册异常的影响。
最终期望的结果是,推空保护开关开启后,能够帮助应用 A 在发生异常的情况下,继续能够寻址到应用B。
环境准备
部署应用
# 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
在网关注册服务
验证和调整链路
$ curl http://${网关IP}/ip
sc-A[192.168.1.194] --> sc-C[192.168.1.195]
$ 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
观测调用
注入故障
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
再次观测
小结
Cloud Native