作者:十眠

背景

注册中心作为承担服务注册发现的核心组件,是微服务架构中必不可少的一环。在 CAP 的模型中,注册中心可以牺牲一点点数据一致性(C),即同一时刻每一个节点拿到的服务地址允许短暂的不一致,但必须要保证可用性(A)。因为一旦由于某些问题导致注册中心不可用,或者服务连不上注册中心,那么想要连接他的节点可能会因为无法获取服务地址而对整个系统出现灾难性的打击。

一个真实的案例

全篇从一个真实的案例说起,某客户在阿里云上使用 Kubernetes 集群部署了许多自己的微服务,由于某台 ECS 的网卡发生了异常,虽然网卡异常很快恢复了,但是却出现了大面积持续的服务不可用,业务受损。

我们来看一下这个问题链是如何形成的?

  1. ECS 故障节点上运行着 Kubernetes 集群的核心基础组件 CoreDNS 的所有 Pod,且低版本 Kubernetes 集群缺少 NodeLocal DNSCache 的特性,导致集群 DNS 解析出现问题。
  2. 该客户的服务发现使用了有缺陷的客户端版本(Nacos-client 的 1.4.1 版本),这个版本的缺陷就是跟 DNS 有关——心跳请求在域名解析失败后,会导致进程后续不会再续约心跳,只有重启才能恢复。
  3. 这个缺陷版本实际上是已知问题,阿里云在 5 月份推送了 Nacos-client 1.4.1 存在严重 bug 的公告,但客户研发未收到通知,进而在生产环境中使用了这个版本。

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

最终导致故障的原因是服务无法调用下游,可用性降低,业务受损。下图示意的是客户端缺陷导致问题的根因:

  1. Provider 客户端在心跳续约时发生 DNS 异常;
  2. 心跳线程未能正确地处理这个 DNS 异常,导致线程意外退出了;
  3. 注册中心的正常机制是,心跳不续约,30 秒后自动下线。由于 CoreDNS 影响的是整个 Kubernetes 集群的 DNS 解析,所以 Provider 的所有实例都遇到相同的问题,整个服务所有实例都被下线;
  4. 在 Consumer 这一侧,收到推送的空列表后,无法找到下游,那么调用它的上游(比如网关)就会发生异常。

回顾整个案例,每一环每个风险看起来发生概率都很小,但是一旦发生就会造成恶劣的影响。服务发现高可用是微服务体系中很重要的一环,当然也是我们时常忽略的点。在阿里内部的故障演练中,这一直是必不可少的一个环节。

面向失败的设计

由于网络环境的抖动比如 CoreDns 的异常,或者是由于某些因素导致我们的注册中心不可用等情况,经常会出现服务批量闪断的情况,但这种情况其实不是业务服务的不可用,如果我们的微服务可以识别到这是一种异常情况(批量闪断或地址变空时),应该采取一种保守的策略,以免误推从而导致全部服务出现"no provider"的问题,会导致所有的微服务不可用的故障,并且持续较长时间难以恢复。

站在微服务角度上考虑,我们如何可以切段以上的问题链呢?以上的案例看起来是 Nacos-client 低版本造成的问题,但是如果我们用的是 zookeeper、eureka 等注册中心呢?我们能拍着胸脯说,不会发生以上的问题吗?面向失败的设计原则告诉我们,如果注册中心挂掉了,或者我们的服务连不上注册中心了,我们需要有一个方式保证我们的服务正常调用,线上的业务持续不断。

本文介绍的是服务发现过程中的高可用的机制,从服务框架层面思考如何彻底解决以上的问题。

服务发现过程中的高可用原理解析

服务发现高可用-推空保护

面向失败的设计告诉我们,服务并不能完全相信注册中心的通知的地址,当注册中心的推送地址为空时候,服务调用肯定会出 no provider 错误,那么我们就忽略此次推送的地址变更。

微服务治理中心提供推空保护能力

  • 默认无侵入支持市面上近五年来的 Spring Cloud 与 Dubbo 框架
  • 无关注册中心实现,无需升级 client 版本

服务发现高可用-离群实例摘除

心跳续约是注册中心感知实例可用性的基本途径。但是在特定情况下,心跳存续并不能完全等同于服务可用。
因为仍然存在心跳正常,但服务不可用的情况,例如:

  • Request 处理的线程池满
  • 依赖的 RDS 连接异常导致出现大量慢 SQL
  • 某几台机器由于磁盘满,或者是宿主机资源争抢导致 load 很高

此时服务并不能完全相信注册中心的通知的地址,推送的地址中,可能存在一些服务质量低下的服务提供者,因此客户端需要自己根据调用的结果来判断服务地址的可用性与提供服务质量的好坏,来定向忽略某些地址。

微服务治理中心提供离群实例摘除

  • 默认无侵入,支持市面上近五年来的 Spring Cloud 与 Dubbo 框架
  • 无关注册中心实现,无需升级 client 版本
  • 基于异常检测的摘除策略:包含网络异常和网络异常 + 业务异常(HTTP 5xx)
  • 设置异常阈值、QPS 下限、摘除比例下限
  • 摘除事件通知、钉钉群告警

离群实例摘除的能力是一个补充,根据特定接口的调用异常特征,来衡量服务的可用性。

动手实践

前提条件

  • 已创建 Kubernetes 集群,请参见创建 Kubernetes 托管版集群 [1]
  • 已开通 MSE 微服务治理专业版,请参见开通 MSE 微服务治理 [2]

准备工作

开启 MSE 微服务治理

1、开通微服务治理专业版:

  1. 单击开通 MSE 微服务治理 [3]
  2. 微服务治理版本选择专业版,选中服务协议,然后单击立即开通。关于微服务治理的计费详情,请参见价格说明 [4]

2、安装 MSE 微服务治理组件:

  1. 在容器服务控制台 [5] 左侧导航栏中,选择市场 > 应用目录
  2. 应用目录页面搜索框中输入 ack-mse-pilot,单击搜索图标,然后单击组件。
  3. 详情页面选择开通该组件的集群,然后单击创建。安装完成后,在命名空间 mse-pilotmse-pilot-ack-mse-pilot 应用,表示安装成功。

3、为应用开启微服务治理:

  1. 登录 MSE 治理中心控制台 [6]
  2. 在左侧导航栏选择微服务治理中心 > Kubernetes 集群列表
  3. Kubernetes 集群列表页面搜索目标集群,单击搜索图标,然后单击目标集群操作列下方的管理
  4. 集群详情页面命名空间列表区域,单击目标命名空间操作列下方的开启微服务治理
  5. 开启微服务治理对话框中单击确认

部署 Demo 应用程序

  1. 在容器服务控制台 [5] 左侧导航栏中,单击集群
  2. 集群列表页面中,单击目标集群名称或者目标集群右侧操作列下的详情
  3. 在集群管理页左侧导航栏中,选择工作负载 > 无状态
  4. 无状态页面选择命名空间,然后单击使用 YAML 创建资源
  5. 对模板进行相关配置,完成配置后单击创建。本文示例中部署 sc-consumer、sc-consumer-empty、sc-provider,使用的是开源的 Nacos。

部署示例应用(springcloud)

YAML:

    # 开启推空保护的 sc-consumer
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sc-consumer
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: sc-consumer
      template:
        metadata:
          annotations:
            msePilotCreateAppName: sc-consumer
          labels:
            app: sc-consumer
        spec:
          containers:
          - env:
            - name: JAVA_HOME
              value: /usr/lib/jvm/java-1.8-openjdk/jre
            - name: spring.cloud.nacos.discovery.server-addr
              value: nacos-server:8848
            - name: profiler.micro.service.registry.empty.push.reject.enable
              value: "true"
            image: registry.cn-hangzhou.aliyuncs.com/mse-demo-hz/demo:sc-consumer-0.1
            imagePullPolicy: Always
            name: sc-consumer
            ports:
            - containerPort: 18091
            livenessProbe:
              tcpSocket:
                port: 18091
              initialDelaySeconds: 10
              periodSeconds: 30
    # 无推空保护的sc-consumer-empty
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sc-consumer-empty
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: sc-consumer-empty
      template:
        metadata:
          annotations:
            msePilotCreateAppName: sc-consumer-empty
          labels:
            app: sc-consumer-empty
        spec:
          containers:
          - env:
            - name: JAVA_HOME
              value: /usr/lib/jvm/java-1.8-openjdk/jre
            - name: spring.cloud.nacos.discovery.server-addr
              value: nacos-server:8848
            image: registry.cn-hangzhou.aliyuncs.com/mse-demo-hz/demo:sc-consumer-0.1
            imagePullPolicy: Always
            name: sc-consumer-empty
            ports:
            - containerPort: 18091
            livenessProbe:
              tcpSocket:
                port: 18091
              initialDelaySeconds: 10
              periodSeconds: 30
    # sc-provider
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sc-provider
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: sc-provider
      strategy:
      template:
        metadata:
          annotations:
            msePilotCreateAppName: sc-provider
          labels:
            app: sc-provider
        spec:
          containers:
          - env:
            - name: JAVA_HOME
              value: /usr/lib/jvm/java-1.8-openjdk/jre
            - name: spring.cloud.nacos.discovery.server-addr
              value: nacos-server:8848
            image: registry.cn-hangzhou.aliyuncs.com/mse-demo-hz/demo:sc-provider-0.3
            imagePullPolicy: Always
            name: sc-provider
            ports:
            - containerPort: 18084
            livenessProbe:
              tcpSocket:
                port: 18084
              initialDelaySeconds: 10
              periodSeconds: 30
    # Nacos Server
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nacos-server
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: nacos-server
      template:
        metadata:
          labels:
            app: nacos-server
        spec:
          containers:
          - env:
            - name: MODE
              value: standalone
            image: nacos/nacos-server:latest
            imagePullPolicy: Always
            name: nacos-server
          dnsPolicy: ClusterFirst
          restartPolicy: Always

    # Nacos Server Service 配置
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: nacos-server
    spec:
      ports:
      - port: 8848
        protocol: TCP
        targetPort: 8848
      selector:
        app: nacos-server
      type: ClusterIP

我们只需在 Consumer 增加一个环境变量 profiler.micro.service.registry.empty.push.reject.enable=true,开启注册中心的推空保护(无需升级注册中心的客户端版本,无关注册中心的实现,支持 MSE 的 Nacos、eureka、zookeeper 以及自建的 Nacos、eureka、console、zookeeper 等)

分别给 Consumer 应用增加 SLB 用于公网访问

以下分别使用 {sc-consumer-empty} 代表 sc-consumer-empty 应用的 slb 的公网地址,{sc-consumer} 代表 sc-consumer 应用的 slb 的公网地址。

应用场景

下面通过上述准备的 Demo 来分别实践以下场景

  • 编写测试脚本

vi curl.sh

    while :
    do
            result=`curl $1 -s`
            if [[ "$result" == *"500"* ]]; then
                    echo `date +%F-%T` $result
            else
                    echo `date +%F-%T` $result
            fi

            sleep 0.1
    done
  • 测试,分别开两个命令行,执行如下脚本,显示如下

% sh curl.sh {sc-consumer-empty}:18091/user/rest2022-01-19-11:58:12 Hello from [18084]10.116.0.142!2022-01-19-11:58:12 Hello from [18084]10.116.0.142!2022-01-19-11:58:12 Hello from [18084]10.116.0.142!2022-01-19-11:58:13 Hello from [18084]10.116.0.142!2022-01-19-11:58:13 Hello from [18084]10.116.0.142!2022-01-19-11:58:13 Hello from [18084]10.116.0.142!

% sh curl.sh {sc-consumer}:18091/user/rest2022-01-19-11:58:13 Hello from [18084]10.116.0.142!2022-01-19-11:58:13 Hello from [18084]10.116.0.142!2022-01-19-11:58:13 Hello from [18084]10.116.0.142!2022-01-19-11:58:14 Hello from [18084]10.116.0.142!2022-01-19-11:58:14 Hello from [18084]10.116.0.142!2022-01-19-11:58:14 Hello from [18084]10.116.0.142!

并保持脚本一直在调用,观察 MSE 控制台分别看到如下情况


  • 将 coredns 组件缩容至数量 0,模拟 DNS 网络解析异常场景。

发现实例与 Nacos 的连接断开且服务列表为空。

  • 模拟 DNS 服务恢复,将其扩容回数量 2。

结果验证

在以上过程中保持持续的业务流量,我们发现 sc-consumer-empty 服务出现大量且持续的报错

2022-01-19-12:02:37 {"timestamp":"2022-01-19T04:02:37.597+0000","status":500,"error":"Internal Server Error","message":"com.netflix.client.ClientException: Load balancer does not have available server for client: mse-service-provider","path":"/user/feign"}2022-01-19-12:02:37 {"timestamp":"2022-01-19T04:02:37.799+0000","status":500,"error":"Internal Server Error","message":"com.netflix.client.ClientException: Load balancer does not have available server for client: mse-service-provider","path":"/user/feign"}2022-01-19-12:02:37 {"timestamp":"2022-01-19T04:02:37.993+0000","status":500,"error":"Internal Server Error","message":"com.netflix.client.ClientException: Load balancer does not have available server for client: mse-service-provider","path":"/user/feign"}

相比之下,sc-consumer 应用全流程没有任何报错

  • 只有重启了 Provider,sc-consumer-empty 才恢复正常

相比之下,sc-consumer 应用全流程没有任何报错

后续

我们当发生推空保护后,我们会上报事件、告警至钉钉群,同时建议配合离群实例摘除使用,推空保护可能会导致 Consumer 持有过多的 Provider 地址,当 Provider 地址为无效地址时,离群实例摘除可以对其进行逻辑隔离,保证业务的高可用。

保障云上业务的永远在线,是 MSE 一直在追求的目标,本文通过面向失败设计的服务发现高可用能力的分享,以及 MSE 的服务治理能力快速构建起服务发现高可用能力的演示,模拟了线上不可预期的服务发现相关异常发生时的影响以及我们如何预防的手段,展示了一个简单的开源微服务应用应该如何构建起服务发现高可用能力。

相关链接

[1] 创建 Kubernetes 托管版集群

https://help.aliyun.com/docum...

[2] 开通 MSE 微服务治理

https://help.aliyun.com/docum...

[3] 开通 MSE 微服务治理

https://common-buy.aliyun.com...

[4] 价格说明

​https://help.aliyun.com/document_detail/170443.htm#concept-2519524​

[5] 容器服务控制台

​https://cs.console.aliyun.com​

[6] MSE 治理中心控制台

​https://mse.console.aliyun.com​

​点击​​此处​,前往 MSE 官网查看更多!​

03-05 22:42