Istio 如何无停机平滑升级 (1.7 跨版本升级 1.11)

4,991 阅读14分钟

我是 LEE,老李,一个在 IT 行业摸爬滚打 16 年的技术老兵。

事件背景

忙完了公司的双 11 项目后,整个公司的大项目基本都结束了。整个生产系统进入了一年一度的维护和升级阶段,那么主要底层系统也进入了版本更新和维护的阶段。写一篇文章的目的是解决前几个星期我们要对承载租户流量的服务网格 Istio 做整体升级,但是翻阅了整个网络资料后,发现没有一篇完整的能够提供 Istio 平滑升级的文章以及可行的操作手册的问题。所以决定根据自己在实际工作中碰到的问题以及实际操作整理一篇文章,希望能够对其他正在使用 Istio 的小伙伴提供一种“无损平滑”升级的技术参考。

升级要求

  1. 租户应用与服务不能中断。
  2. 租户在升级期间能够正常使用原有系统,对升级无感。
  3. 升级过程支持平滑回退,有完善的过滤流程和时间。
  4. 方案可持续,对 Istio 系统没有侵入,利用社区内资源和资料。
  5. 操作可规范和流程化,方便后续人参考和使用。

有了上面的升级要求,那么我们也就有寻求“无损平滑”解决方案的需求,但是事实总是残酷的,没有找到复合我们需要的方案,哪怕官方文档提供的升级方案都是有损的。我就很担心如果按照现在网络提供的资料和信息制作升级方案和操作 SOP,满足不了升级要求。怎么办?看来不得不有一次技术攻坚、方案设计、可行性验证了。我想要平滑升级 Istio,那么就要回到源头,研究 Istio 的结构,制定真正可行的方案。

废话不多说,我们一起进入“技术探索”环节。

技术探索

既然要制定 Istio “无损平滑”升级方案,就要知道为什么 Istio 升级会有损。有一种头痛医头,脚痛医脚的感觉,这个是最有效的办法,不妨我们耐心继续往下看。

我们先看下 Istio 的架构简图:

Istio 架构图

实际 Pod 的 sidecar 和 IngressGateway 底层都是 Envoy,都是通过 Istiod 分发的 Dynamic config,也就是这些 envoy 都是注册在 Istiod 上的,由 Istiod 统一管理。

补充说明:Istio sidecar 注入是因为 MutatingWebhookConfigurations 的配置。在拥有 label 为 istio-injection=enabled 的 namespace 中创建 pod 会自动注入 pod 启动中。

我们做一个假设,现在生产的 Istio 版本是 1.7,然后再把 Istio 版本升级到了 1.11 这个版本。 Istiod 版本发生了变化,Istiod 版本成为了 1.11,而业务 Pod 的 sidecar 版本还是 1.7,怎么办?

小伙伴第一反应就是重启 pod,让业务 Pod 的 sidecar 重新注册到 1.11 上,看上去是那么回事,但是别忘了,你重启了业务 Pod,租户的请求此时因为重启会出现中断或者服务不可用的情况。如果我不重启业务 Pod,就让 1.7 的 Pod sidecar 连接 1.11 的 Istiod 不就好了,事实真的是你想的吗? No,No,No,太天真。 Pod sidecar 会因为版本不兼容,丢失配置,导致 Pod 因为健康监测失败而重启。

istio-agent 配置不兼容

由于 Istio sidecar 的配置丢失。如果你的 Istio-agent sidecar 无法与 Istiod 通信,或与发送的配置不兼容,你的工作负载将无法加入网格或与网格通信。这甚至会影响现有的工作负载,导致你可能会尝试访问不再存在的工作负载,新的工作负载也将无法加入。

TIPS: 由于这种类型的中断,建议 istio-agents 匹配并保留与控制平面 (Istiod) 相同的版本。在升级过程中,现有的控制平面部署保持原位而不是直接升级。

如果有小伙伴想探索究竟,可以参阅我之前公开的资料 《Istio Pilot 结构解析 PPT (已脱敏)》

既然有直接升级不行,而且会断流,有没有其他的解决方案呢? 有,灰度升级。官方文档:istio.io/latest/docs…

但是这个就是真的我们想要的吗?不。在官方文档中有这么一步操作:

After the namespace updates, you need to restart the pods to trigger re-injection. One way to do this is using:

$ kubectl rollout restart deployment -n test-ns

看到这里,很多人跟我一样,木了。。。 重启 pod ? 真实生产环境,你去重启生产 pod?就因为 Istio 软件升级? 这个是平滑升级方案?技术委员会能通过你的方案? 这么多问号都摆在面前,那么我们该怎么做? 不着急, 我们一起进入“技术方案”环节。

技术方案

技术挑战

  1. 直接升级 Istio 会导致 Istio-agent sidecar 与 Istiod 不兼容,最后导致业务 Pod 重启。
  2. 为了保证 Istio-agent 和 Istiod 之间的兼容性,必须要手动重启业务 Pod。
  3. 系统需要平滑过渡和升级,业务 Pod 非到不必要,绝对不能重启。

其中 1,2 和 3 点之间是相悖的。

方案猜想

  1. Istio-agent sidecar 与 Istiod 兼容问题,可不可以老版本的 Istio-agent sidecar 与 Istiod 连接,新版本的 Istio-agent sidecar 与 Istiod 连接?
  2. 如果各个版本的 Istio-agent sidecar 与 Istiod 连接,如何让老版本的 sidecar 的业务 pod 自然过渡到新版本的 sidecar 上?
  3. 如何保证真正的“无损平滑”升级?

解决方式

带着这么多问题和技术挑战,当时确实真的把我难道了,但是经过差不多的 2 周的左右的方案设计以及技术验证,发现是完全可以做到真正的“无损平滑”升级的。那么在这里我就分享下当时我的思考方式和定位方案的过程。

兼容问题

这个问题好办,创建两个独立的 Istio 系统在一个 k8s 中,只需要在 istioctl 新安装的 Istio 加入一个参数 --set revision=xxx。 istioctl 安装时候就不会覆盖原有的 Istio 版本,而是启用一个全新的控制面和部分数据面(为什么是部分,后面会看到)。这样我们就很好的做到了老版本的 Istio-agent sidecar 与 Istiod 连接,新版本的 Istio-agent sidecar 与 Istiod 连接。

过渡问题

首先我们不能手动重启正在运行的业务 Pod,但是不代表业务 Pod 不会迭代和发版,只要他们重新发版,Pod 自然就完成了重启。只要经过一段足够长的时间就能让使用老版本的 sidecar 的业务 pod 自然过渡到新版本的 sidecar 上。

说到这里,有小伙伴觉得还是没有说到重点,那么我觉得有必要来看看 MutatingWebhookConfigurations 的配置。

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
    annotations:
    creationTimestamp: "2022-11-30T10:41:51Z"
    generation: 5
    labels:
        app: sidecar-injector
        install.operator.istio.io/owning-resource: installed-state
        install.operator.istio.io/owning-resource-namespace: istio-system
        istio.io/rev: default
        operator.istio.io/component: Pilot
        operator.istio.io/managed: Reconcile
        operator.istio.io/version: 1.7.8
        release: istio
    name: istio-sidecar-injector
    resourceVersion: "9640"
    selfLink: /apis/admissionregistration.k8s.io/v1/mutatingwebhookconfigurations/istio-sidecar-injector
    uid: b66c986a-9ad9-469f-bf14-eea3df74c876
webhooks:
    - admissionReviewVersions:
          - v1beta1
          - v1
      clientConfig:
          caBundle: <Secret>
          service:
              name: istiod ## 这里很重要,就是在 Pod 创建时候,访问 k8s istiod service 的 443 端口,然后注册自己,然后拉取 sidecar 启动配置
              namespace: istio-system
              path: /inject
              port: 443
      failurePolicy: Fail
      matchPolicy: Exact
      name: sidecar-injector.istio.io
      namespaceSelector:
          matchLabels:
              istio-injection: enabled
      objectSelector: {}
      reinvocationPolicy: Never
      rules:
          - apiGroups:
                - ""
            apiVersions:
                - v1
            operations:
                - CREATE ## 创建动作
            resources:
                - pods ## 只关注 Pod
            scope: "*"
      sideEffects: None
      timeoutSeconds: 30

(★) 在此看到 yaml 文件中 service 字段下有一个 name 为 istiod,我们只要把这个值改成新版本的 Istiod 的 service 名称即可。 就可以让重启的 Pod 使用新版本的 Istio sidecar 启动了。

举个栗子:

[root@localhost ops]# kubectl get svc -n istio-system
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                                         AGE
istio-ingressgateway   LoadBalancer   10.126.118.148   10.111.251.17   15021:30082/TCP,80:30080/TCP                    2d21h
istiod                 ClusterIP      10.126.118.150   <none>         15010/TCP,15012/TCP,443/TCP,15014/TCP,853/TCP   2d21h
istiod-1-11-8          ClusterIP      10.126.118.241   <none>         15010/TCP,15012/TCP,443/TCP,15014/TCP           2d21h

这里我已经安装了一个 1.11.8 的新版本的 Istio 与老版本的并行,相互不冲突。 修改 MutatingWebhookConfiguration 中配置 sevice name 为 istiod-1-11-8,随后通过模拟“发布”新版本的应用,重启了业务 Pod,让其注册到了新版本的 Istio 上。

[root@localhost 1.11.8]# istioctl proxy-status
NAME                                                   CDS        LDS        EDS        RDS        ISTIOD                            VERSION
istio-ingressgateway-59b5b798d4-v9d98.istio-system     SYNCED     SYNCED     SYNCED     SYNCED     istiod-1-11-8-866bf44db-2wbqx     1.11.8
nginx-app-7b88698d6f-brhc4.nginx                       SYNCED     SYNCED     SYNCED     SYNCED     istiod-1-11-8-866bf44db-2wbqx     1.11.8
nginx-app-7b88698d6f-jhwr8.nginx2                      SYNCED     SYNCED     SYNCED     SYNCED     istiod-879f4d9bf-c5xz6            1.7.8

这个时候让我们将目光关注到 VERSION 这个字段下,我们发现 nginx-app-7b88698d6f-jhwr8.nginx2 这个 Pod 还是在使用版本 Istio 1.7 并与老的 istiod-879f4d9bf-c5xz6 建立着连接。而 nginx-app-7b88698d6f-brhc4.nginx 这个 Pod 在重启后就与新版本 Istio 1.11 的 istiod-1-11-8-866bf44db-2wbqx 建立着连接。

到了这里基本上我们实现了 Istio 平滑升级,通过自然迭代的方式重启 Pod,缓慢迁移到新版本 Istio 上。

平稳过渡

  1. 新版本 Istio 使用灰度安装升级的方式部署
  2. 切换 Istio sidecar 注册方式 (MutatingWebhookConfiguration)
  3. 等待业务 Pod 自然迭代重启,如果长期没有切换重启的 Pod,可以组织适当的停机时间,完成切换
  4. 卸载老版本 Istio

实操步骤

安装新版 Istio

演示的 operator.yaml 文件内容

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
    meshConfig:
        accessLogFile: /dev/stdout
        accessLogEncoding: JSON
        outboundTrafficPolicy:
            mode: REGISTRY_ONLY
        defaultConfig:
            holdApplicationUntilProxyStarts: true
    components:
        ingressGateways:
            - name: istio-ingressgateway
              enabled: true
              k8s:
                  service:
                      ports:
                          - port: 15021
                            targetPort: 15021
                            nodePort: 30082
                            name: status-port
                          - port: 80
                            targetPort: 8080
                            nodePort: 30080
                            name: http2
    values:
        global:
            proxy:
                excludeIPRanges: "10.126.118.0/24"
        gateways:
            istio-ingressgateway:
                type: LoadBalancer
                autoscaleMin: 1
                autoscaleMax: 3
                resources:
                    requests:
                        cpu: 1500m
                        memory: 1Gi
                    limits:
                        cpu: "4"
                        memory: 8Gi
    addonComponents:
        prometheus:
            enabled: false

执行命令:

# istioctl install -f operator.yaml -s revision=1-11-8

切换注册到新版 Istio

切换 MutatingWebhookConfigurations ,完成 Namespace 中 Pod 启动自动注入 Sidecar 到 1.11.8 上,而不是用老的 1.7.8 (其中 istiod-1-11-8 是 k8s service 的名称,可以通过 kubectl get svc 获得)

执行命令:

# kubectl get mutatingwebhookconfigurations -n istio-system istio-sidecar-injector -o json | jq '.webhooks[0].clientConfig.service.name="istiod-1-11-8"' | kubectl replace -f -

(可选)卸载老版本 Istio

直接卸载可能导致默认的 mutatingwebhookconfigurations 配置丢失,所以这里要注意有备份和恢复

备份 MutatingWebhookConfigurations 配置

# kubectl get mutatingwebhookconfigurations -n istio-system istio-sidecar-injector -o yaml > /tmp/mutatingwebhookconfigurations.backup.yaml

删除老版本的 Istio

# istioctl x uninstall -f operator.yaml

恢复 MutatingWebhookConfigurations 配置

# kubectl apply -f /tmp/mutatingwebhookconfigurations.backup.yaml

最终效果

这里通过模拟集群演示来展现我们线上迁移整体效果,方法是一致的。

并行状态

[root@localhost 1.11.8]# istioctl proxy-status
NAME                                                   CDS        LDS        EDS        RDS        ISTIOD                            VERSION
istio-ingressgateway-59b5b798d4-v9d98.istio-system     SYNCED     SYNCED     SYNCED     SYNCED     istiod-1-11-8-866bf44db-2wbqx     1.11.8
nginx-app-7b88698d6f-brhc4.nginx                       SYNCED     SYNCED     SYNCED     SYNCED     istiod-1-11-8-866bf44db-2wbqx     1.11.8
nginx-app-7b88698d6f-jhwr8.nginx2                      SYNCED     SYNCED     SYNCED     SYNCED     istiod-879f4d9bf-c5xz6            1.7.8

日志检测

已经迁移到新版本 Istio 的业务 Pod 日志

[root@localhost 1.11.8]# kubectl logs -n nginx nginx-app-7b88698d6f-brhc4
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/12/02 07:34:59 [notice] 1#1: using the "epoll" event method
2022/12/02 07:34:59 [notice] 1#1: nginx/1.23.2
2022/12/02 07:34:59 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
2022/12/02 07:34:59 [notice] 1#1: OS: Linux 5.18.19-051819-generic
2022/12/02 07:34:59 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 64000:64000
2022/12/02 07:34:59 [notice] 1#1: start worker processes
2022/12/02 07:34:59 [notice] 1#1: start worker process 29
2022/12/02 07:34:59 [notice] 1#1: start worker process 30
2022/12/02 07:34:59 [notice] 1#1: start worker process 31
2022/12/02 07:34:59 [notice] 1#1: start worker process 32
2022/12/02 07:34:59 [notice] 1#1: start worker process 33
2022/12/02 07:34:59 [notice] 1#1: start worker process 34
2022/12/02 07:34:59 [notice] 1#1: start worker process 35
2022/12/02 07:34:59 [notice] 1#1: start worker process 36
127.0.0.6 - - [02/Dec/2022:08:02:46 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:46 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:47 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:48 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:48 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:49 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:49 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:50 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:50 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:51 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:51 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.6 - - [02/Dec/2022:08:02:52 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"

继续使用老版本 Istio 的业务 Pod 日志

[root@localhost 1.11.8]# kubectl logs -n nginx2 nginx-app-7b88698d6f-jhwr8 -c app
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/11/30 11:22:37 [notice] 1#1: using the "epoll" event method
2022/11/30 11:22:37 [notice] 1#1: nginx/1.23.2
2022/11/30 11:22:37 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
2022/11/30 11:22:37 [notice] 1#1: OS: Linux 5.4.185-1.el7.elrepo.x86_64
2022/11/30 11:22:37 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 64000:64000
2022/11/30 11:22:37 [notice] 1#1: start worker processes
2022/11/30 11:22:37 [notice] 1#1: start worker process 28
2022/11/30 11:22:37 [notice] 1#1: start worker process 29
2022/11/30 11:22:37 [notice] 1#1: start worker process 30
2022/11/30 11:22:37 [notice] 1#1: start worker process 31
2022/11/30 11:22:37 [notice] 1#1: start worker process 32
2022/11/30 11:22:37 [notice] 1#1: start worker process 33
2022/11/30 11:22:37 [notice] 1#1: start worker process 34
2022/11/30 11:22:37 [notice] 1#1: start worker process 35
127.0.0.1 - - [03/Dec/2022:08:31:17 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:18 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:19 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:20 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:20 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:21 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:21 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:22 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:23 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"
127.0.0.1 - - [03/Dec/2022:08:31:23 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "10.11.251.5"

通过上面的日志,我们可以确认,两套 Istio 已经完成并行,而且两个应用 Pod 请求相应正常。