OpenKruise :SidecarSet 助力 Mesh 容器热升级

阿里巴巴云原生
English

作者| 赵明山(立衡)

头图.jpg

前言


OpenKruise 是阿里云开源的云原生应用自动化管理套件,也是当前托管在 Cloud Native Computing Foundation ( CNCF ) 下的 Sandbox 项目。它来自阿里巴巴多年来容器化、云原生的技术沉淀,是阿里内部生产环境大规模应用的基于 Kubernetes 之上的标准扩展组件,也是紧贴上游社区标准、适应互联网规模化场景的技术理念与最佳实践。

OpenKruise 在 2021.5.20 发布了最新的 v0.9.0版本( ChangeLog ),上一篇文章我们介绍了新增 Pod 重启、删除防护等重磅功能,今天向大家介绍另一个核心特性,即 SidecarSet 基于上一个版本扩展了特别针对 Service Mesh 场景的支持。

背景:如何独立升级 Mesh 容器


SidecarSet 是 Kruise 提供的独立管理 Sidecar 容器的 workload。用户通过 SidecarSet 能够便利的完成对 Sidecar 容器的自动注入独立升级,详情请参考:OpenKruise 官网

默认情况下,Sidecar 的独立升级顺序是先停止旧版本的容器,然后再创建新版本的容器。这种方式尤其适合不影响 Pod 服务可用性的 Sidecar 容器,例如日志收集 agent ,但是对于很多代理或运行时的 Sidecar 容器,如 Istio Envoy,这种升级方法就有问题了。Envoy 作为 Pod 中的一个 Proxy 容器代理了所有的流量,这种场景下如果直接重启升级,Pod 服务的可用性必然会受到影响,因此需要考虑应用自身的发布和容量情况,无法完全独立于应用做 Sidecar 的发布。
1--1.png

阿里巴巴集团内部拥有上万的 Pod 都是基于 Service Mesh 来实现相互间的通信,由于 Mesh 容器升级会导致业务 Pod 的不可用,因而 Mesh 容器的升级将会极大阻碍 Service Mesh 的迭代。针对这种场景,我们同集团内部的 Service Mesh 团队一起合作实现了 Mesh 容器的热升级能力。本文将重点介绍在实现 mesh 容器热升级能力的过程中 SidecarSet 是扮演了怎样的重要角色。

SidecarSet 助力 Mesh 容器无损热升级

Mesh 容器不能像日志采集类容器直接原地升级,其原因在于:Mesh 容器必须要不间断地对外提供服务,而独立升级方式会导致 Mesh 服务存在一段不可用时间。虽然社区中已有一些知名的 Mesh 服务如 Envoy 、Mosn 等默认能够提供平滑升级的能力,但是这些升级方式无法与云原生进行恰当地结合,且 kubernetes 本身也缺乏对此类 Sidecar 容器的升级方案。

OpenKruise SidecarSet 为此类 Mesh 容器提供了 Sidecar 热升级机制,能够通过云原生的方式助力 Mesh 容器实现无损热升级。

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
  name: hotupgrade-sidecarset
spec:
  selector:
    matchLabels:
      app: hotupgrade
  containers:
    - name: sidecar
      image: openkruise/hotupgrade-sample:sidecarv1
      imagePullPolicy: Always
      lifecycle:
        postStart:
          exec:
            command:
              - /bin/sh
              - /migrate.sh
      upgradeStrategy:
        upgradeType: HotUpgrade
        hotUpgradeEmptyImage: openkruise/hotupgrade-sample:empty
  • upgradeType : HotUpgrade 代表该 sidecar 容器的类型是 Hot upgrade ,即热升级方案。
  • HotUpgradeEmptyImage : 当热升级 Sidecar 容器时,业务须要提供一个 empty 容器用于热升级过程中的容器切换。Empty 容器同 Sidecar 容器具有相同的配置(镜像地址除外),例如 command , lifecycle , probe 等。

SidecarSet 热升级机制主要包含注入热升级 Sidecar 容器和 Mesh 容器平滑升级两个过程。

注入热升级 Sidecar 容器

针对热升级类型的 Sidecar 容器,在 Pod 创建时 SidecarSet Webhook 将会注入两个容器:

  • {Sidecar.name} -1: 如下图所示 envoy -1,这个容器代表正在实际工作的 sidecar 容器,例如:envoy :1.16.0
  • {Sidecar.name} -2: 如下图所示 envoy-2,这个容器是业务提供的 HotUpgradeEmptyImage 容器,例如:empty :1.0

2-2.png

上述 Empty 容器在 Mesh 容器运行过程中,并没有做任何实际的工作。

Mesh 容器平滑升级

热升级流程主要分为一下三个步骤:

  1. Upgrade: 将 Empty 容器替换为最新版本的 Sidecar 容器,例如:envoy-2.Image = envoy:1.17.0
  2. Migration : 执行 Sidecar 容器的 PostStartHook 脚本,完成 mesh 服务的平滑升级
  3. Reset: Mesh 服务平滑升级后,将老版本 Sidecar 容器替换为 Empty 容器,例如:envoy-1.Image = empty : 1.0

仅需上述三个步骤即可完成热升级中的全部流程,若对 Pod 执行多次热升级,则重复执行上述三个步骤即可。

3-3.png

Migration 核心逻辑

SidecarSet 热升级机制不仅完成了 Mesh 容器的切换,并且提供了新老版本的协调机制( PostStartHook ),但是至此还只是万里长征的第一步,Mesh 容器同时还需要提供 PostSartHook 脚本来完成 Mesh 服务自身的平滑升级(上述 Migration 过程),如:Envoy 热重启、Mosn 无损重启。

Mesh 容器一般都是通过监听固定端口来对外提供服务,此类 Mesh 容器的migration 过程可以概括为:通过 UDS 传递 ListenFD 和停止 Accpet 、开始排水。针对不支持热重启的 Mesh 容器可以参考此过程完成改造,逻辑图如下:
4-4.png

热升级 Migration Demo

不同 Mesh 容器对外提供的服务以及内部实现逻辑各有差异,进而具体的 Migration也有所不同,上述逻辑只是对其中一些要点做了一些总结,希望能对有需要的各位有所裨益,同时在 Github 上面我们也提供了一个热升级 Migration Demo 以供参考,下面将对其中的一些关键代码进行介绍。

1. 协商机制

Mesh 容器启动逻辑首先就需要判断第一次启动还是热升级平滑迁移过程,为了减少Mesh 容器沟通成本,Kruise 在两个 sidecar 容器中注入了两个环境变量 SIDECARSET_VERSION 和 SIDECARSET_VERSION_ALT ,通过判断两个环境变量的值来判断是否是热升级过程以及当前 sidecar 容器是新版本还是老版本。

// return two parameters:
// 1. (bool) indicates whether it is hot upgrade process
// 2. (bool ) when isHotUpgrading=true, the current sidecar is newer or older
func isHotUpgradeProcess() (bool, bool) {
    // 当前sidecar容器的版本
    version := os.Getenv("SIDECARSET_VERSION")
    // 对端sidecar容器的版本
    versionAlt := os.Getenv("SIDECARSET_VERSION_ALT")
    // 当对端sidecar容器version是"0"时,表明当前没有在热升级过程
    if versionAlt == "0" {
        return false, false
    }
    // 在热升级过程中
    versionInt, _ := strconv.Atoi(version)
    versionAltInt, _ := strconv.Atoi(versionAlt)
    // version是单调递增的int类型,新版本的version值会更大
    return true, versionInt > versionAltInt
}

2. ListenFD 迁移

通过 Unix Domain Socket 实现 ListenFD 在不同容器间的迁移,此步同样也是热升级中非常关键的一步,代码示例如下:

// 为了代码的简洁,所有的失败都将不捕获

/* 老版本sidecar通过Unix Domain Socket迁移ListenFD到新版本sidecar */
// tcpLn *net.TCPListener
f, _ := tcpLn.File()
fdnum := f.Fd()
data := syscall.UnixRights(int(fdnum))
// 与新版本sidecar容器通过 Unix Domain Socket建立链接
raddr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
uds, _ := net.DialUnix("unix", nil, raddr)
// 通过UDS,发送ListenFD到新版本sidecar容器
uds.WriteMsgUnix(nil, data, nil)
// 停止接收新的request,并且开始排水阶段,例如:http2 GOAWAY
tcpLn.Close()

/* 新版本sidecar接收ListenFD,并且开始对外服务 */
// 监听 UDS
addr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
unixLn, _ := net.ListenUnix("unix", addr)
conn, _ := unixLn.AcceptUnix()
buf := make([]byte, 32)
oob := make([]byte, 32)
// 接收 ListenFD
_, oobn, _, _, _ := conn.ReadMsgUnix(buf, oob)
scms, _ := syscall.ParseSocketControlMessage(oob[:oobn])
if len(scms) > 0 {
    // 解析FD,并转化为 *net.TCPListener 
    fds, _ := syscall.ParseUnixRights(&(scms[0]))
    f := os.NewFile(uintptr(fds[0]), "")
    ln, _ := net.FileListener(f)
    tcpLn, _ := ln.(*net.TCPListener)
    // 基于接收到的Listener开始对外提供服务,以http服务为例
    http.Serve(tcpLn, serveMux)
}

已知 Mesh 容器热升级案例

阿里云服务网格( Alibaba Cloud Service Mesh,简称 ASM)提供了一个全托管式的服务网格平台,兼容社区 Istio 开源服务网格。当前,基于 OpenKruise SidecarSet 的热升级能力,ASM 实现了数据平面 Sidecar 热升级能力( Beta ),用户可以在应用无感的情况下完成服务网格的数据平面版本升级,正式版也将于近期上线。除热升级能力外,ASM 还支持配置诊断、操作审计、访问日志、监控、服务注册接入等能力,全方位提升服务网格使用体验,欢迎您前往试用。

总结

云原生中 Mesh 容器的热升级一直都是迫切却又棘手的问题,本文中的方案也只是阿里巴巴集团在此问题上的一次探索,在反馈社区的同时也希望能够抛砖引玉,引发各位对此中场景的思考。同时,我们也欢迎更多的同学参与到 OpenKruise 社区来,共同建设一个场景更加丰富、完善的 K8s 应用管理、交付扩展能力,能够面向更加规模化、复杂化、极致性能的场景。

5-5.png

阅读 438

阿里巴巴云原生
关注云原生技术趋势,输出最优质云原生内容
801 声望
173 粉丝
0 条评论
你知道吗?

801 声望
173 粉丝
宣传栏