OpenKruise SidecarSet 源码浅析

1,258 阅读22分钟

OpenKruise 简介

OpenKruise 是一个基于 Kubernetes 的扩展套件,简单来说,就是 Kubernetes Workload Plus.

OpenKruise 系统架构

image

kruise-manager :是一个 controller。逻辑上而言,OpenKruise 的 CRD 都是独立的。实际上,这些 CRD 都被打包在一个独立的二进制文件,并运行在 kruise-manager 管理的 Pod 中。

webhook :是一个 service,用于准入校验。通过 webhook configuration 来配置哪些 CRD 或 Pod 资源需要感知处理。

kruise-daemon :部署在 slave 节点,实现一些个性化特性,比如镜像预热、容器重启等功能。

什么是 SidecarSet

SidecarSet 是 OpenKruise 的一个 CRD,它支持通过 admission webhook 来自动为集群中创建的符合条件的 Pod 注入 sidecar 容器。SidecarSet 将 sidecar 容器的定义和生命周期与业务容器解耦。它主要用于管理无状态的 sidecar 容器,比如监控、日志等 agent。

使用 SidecarSet 带来的好处:

  1. 允许开发者无侵入地新增功能;

  2. 与主程序松耦合,主程序与 sidecar 程序升级时互不影响可用性;

  3. sidecar 可以将功能进行抽象,降低主程序的代码复杂度。

来看看官方给的 yaml 模版:

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
  name: hotupgrade-sidecarset
spec:
  # 利用 selector 来决定要注入的 pod
  # 这个控制器定义的 container 会被注入到符合 label 的 pod 中
  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

Sidecar 的起源

新一代微服务架构的演进过程

Ingress或边缘代理

image

对于每个服务,都创建一个控制器,由其部署相应的 pod,pod 内的 container 即该服务,实现服务的基础扩缩容能力,然后由 service 进行负载均衡,再用 Nginx Ingress Controller 统一对外部开放代理。

这种架构部署起来十分简单,但是无法管理服务间的流量。

路由器网格

image

这种架构使用一个中心组建 Router 来对容器间的流量进行拦截,提供了最基础的流量管理功能。但是当服务一多起来,管理就很麻烦。

例如:

  1. 无法对单个服务实现限流功能;

  2. 需要管理多端对多端的双向流量,业务功能复杂;

  3. 单一中心组件宕机后会导致所有服务不可用;

  4. Router 没有自动感知功能,每次有新服务都需要注册进来。

Proxy per Node

image

在每个节点上部署一个代理,每个代理只需管理一个节点上的若干个微服务。代理相比上一代架构而言,工程复杂度比较简单,只需使用 Kubernetes 的 DaemonSet 来部署,同时也方便实现服务的限流、重试功能。

这个架构已经有 sidecar 的雏形了,但还是有一些致命的缺点:

  1. 服务仍然需要自己注册到代理中,对服务不是完全透明;

  2. 粒度不够细,若有一个节点的代理挂掉,则整个节点的微服务都无法工作。

Sidecar代理模型

image

相较于上一代架构来说,这种架构以单个微服务 container 为粒度。但是缺少管理这些代理的组件,数量一多还是不方便管理。

Service Mesh 是什么

Service Mesh 即服务网格,服务网格中分为控制平面数据平面。

控制平面的特点:

  • 不直接解析数据包;

  • 与数据平面中的代理通信,下发策略和配置;

  • 负责网络行为的可视化;

  • 通常提供 API 或者命令行工具可用于配置版本化管理,便于持续集成和部署。

数据平面的特点:

  • 按照无状态目标设计;

  • 直接处理入站和出站数据包,转发、路由、健康检查、负载均衡、认证、鉴权、产生监控数据等;

  • 对应用来说透明,即可以做到无感知部署。

Service Mesh 的价值所在

  1. 可观察性:proxy sidecar 能够对流量数据进行采集、存储,发送给后端监控、日志管理系统,使得服务间的访问能够被观测;

  2. 流量控制:可以做到对服务的超时重试、访问速率限制等;

  3. 安全性:通过从控制平面获取的证书配置,实现服务的鉴权,对连接传输内容进行加密;

  4. 延迟和故障注入:通过人为地注入故障,检验系统的健壮性。

以上这些特点,都是服务无感知的,由 Service Mesh 来实现。

Sidecar 的工程实现

使用到 sidecar 模式的业界开源服务网格组件中,比较出名的就是 Istio。

Istio 简介

Istio 是一个开源的服务网格组件,它提供了对整个服务网格的行为洞察和操作控制的能力,以及一个完整的满足微服务应用各种需求的解决方案。

特性:

  • 流量管理:控制服务间的流量和 API 调用流,使调用更可靠,增强不同环境下的网络鲁棒性;

  • 可观测性:了解服务之间的依赖关系和它们之间的性质和流量,提供快速识别定位问题的能力;

  • 策略实施:通过配置 mesh 而不是以改变代码的方式来控制服务之间的访问策略;

  • 服务识别和安全:提供在 mesh 里的服务可识别性和安全性保护。

Istio 的架构

image

Istio 的架构分为控制平面和数据平面:

  • 数据平面:由一组智能代理(Envoy)以 sidecar 模式部署,协调和控制所有服务之间的网络通信。

  • 控制平面:负责管理和配置代理路由流量,以及在运行时执行的政策。

Envoy

Istio 使用 Envoy 作为数据平面,该代理是以 C++ 开发的高性能代理,用于调解服务网格中所有服务的所有入站和出站流量。

Envoy 代理被部署为服务的 sidecar,在逻辑上为服务增加了 Envoy 的许多内置特性,例如:

  • 动态服务发现

  • 负载均衡

  • 支持 HTTP 2/gRPC 代理

  • 熔断器

  • 健康检查

  • 基于百分比流量分割的分阶段发布

  • 故障注入

  • 多种 metrics

Pilot

Pilot 为 Envoy sidecar 提供服务发现、用于智能路由的流量管理功能(例如,A/B 测试、金丝雀发布等)以及弹性功能(超时、重试、熔断器等)。

Citadel

Citadel 通过内置的身份和证书管理,可以支持强大的服务间以及最终用户的身份验证。使用 Citadel,operator 可以执行基于服务身份的策略。

Galley

Galley 是 Istio 的配置验证、提取、处理和分发组件。

Sidecar 的注入与透明流量劫持

Istio 会将 sidecar 注入到每个 pod 中,一般使用 mutating admission webhook 来注入两个容器:

  • istio-init:用于给 Sidecar 容器即 Envoy 代理做初始化,设置 iptables 端口转发;

  • istio-proxy:Envoy 代理容器,运行 Envoy 代理。

init 容器:Kubernetes 中,Pod 启动时容器是存在启动顺序的,init 容器会比普通容器先启动,并且 init 容器是按顺序依次运行的,只有前面的容器运行成功后,才可以运行下一个 init 容器。init 容器可以使用 Linux namespace 对资源做初始化,例如网络资源。

我们先看看 istio-init 容器的启动参数:

      initContainers:
        - name: istio-init
          image: docker.io/istio/proxyv2:1.13.1
          args:
            # 初始化 iptables
            - istio-iptables
            # -p 接收所有 tcp 流量到15001端口
            - '-p'
            - '15001'
            # -z 重定向所有入站 tcp 流量到15006 
            - '-z'
            - '15006'
            # -u 指定未应用重定向容器的 uid
            # 只需要 main container 重定向,sidecar container 不需要重定向
            - '-u'
            - '1337'
            # -m 指定重定向到 Envoy 的模式,REDIRECT 或 TPROXY
            - '-m'
            - REDIRECT
            # -i 指重定向到 Envoy 的出站 IP 流量,这里是重定向所有出站流量
            - '-i'
            - '*'
            # -x 指定从出站重定向中排除的 IP 地址,这里不排除任何 IP
            - '-x'
            - ''
            # -b 表示重定向所有入站端口的流量到 Envoy
            - '-b'
            - '*'
            # -d 表示重定向入站端口中排除15090,15021,15020端口
            - '-d'
            - 15090,15021,15020

所以这条命令的作用是:

  • 容器对外暴露端口 15001;

  • 将应用容器的所有流量都转发到 Envoy 的 15006 端口;

  • 使用默认的 REDIRECT 模式来重定向流量;

  • 将所有出站流量都重定向到 Envoy 代理;

  • 将除了 15090、15201、15020 端口以外的所有端口的入站流量重定向到 Envoy 代理。

在介绍 TPROXY 之前,先回顾一下 iptables:

iptables 位于用户空间,是 Linux 内核中 netfilter 的管理工具,netfilter 位于内核空间,其功能包括网络地址转换、数据包内容修改、数据包内容过滤等。

iptables 共有五条链路:PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING。

每条链路上都存在若干对数据包进行处理的规则,多个不同类型的规则组成不同的表。在 iptables 中,一共有4种类型的表:

  • filter 是用于存放所有与防火墙相关操作的默认表,用来过滤某些数据包;

  • nat 用于 网络地址转换(例如:端口转发);

  • mangle 用于对特定数据包的修改;

  • security 用于 强制访问控制 网络规则。

image

TPROXY:即 Transparent Proxy,透明代理,能够实现不修改报文的头部而达到转发的目的。其实现原理大致为:在创建 socket 时,设置 IP_TRANSPARENT 标志,这样就能够 bind 本机之外的 ip,接着再 bind 0.0.0.0,这样就能监听到所有 ip;然后对 iptables 的 PREROUTING 链做处理,让其不要走 FORWARD 链,例如:

  1. 在 PREROUTING 链添加一条 iptables 规则:

    1. iptables -t mangle -I PREROUTING -p udp --dport 5301 -j MARK --set-mark 1

  2. 告诉 iproute 对 fwmark=1 的流量查表100:

    1. ip rule add fwmark 1 lookup 100

  3. 设置表100所有 IPv4 地址均为 local:

    1. ip route add local 0.0.0.0/0 dev lo table 100

再来看看 istio-proxy 容器,Envoy 代理是怎么做流量劫持的:

注:非 TCP 流量路由,可将其理解为正常情况下 Pod 的流量接收方式。

image

使用 iptables 做流量劫持的缺点:

  1. 从上图我们可以看到,InBound 流量和 OutBound 流量两次穿越用户空间和内核空间,这其中的开销比起正常的流量接收方式多了一倍。这种开销的存在,其实是因为 sidecar -> app、app -> sidecar 这个数据传输的过程要进行切换;

  2. 对于重定向流量而言,需要对连接进行跟踪,这依赖了 Linux 内核模块 conntrack 来实现。当连接数亿多,会造成 conntrack table 满的情况。

conntrack:conntrack 通过维护 conntrack table 来进行连接跟踪,它能够跟踪 TCP、UDP、ICMP 等报文连接。通过 netfilter 的 hook,即 5 条链,来检查系统中进出的每个网络数据包。例如,当 NAT 模块收到一个新的报文时,它会先查 conntrack table,然后根据里面的数据修改报文的 IP:Port,然后写入 conntrack table。

经过调研,发现有两种方案来优化这些开销:

  1. 使用 TPROXY 模式:由于 TPROXY 不会修改报文头部,因此不需要记录连接,这样省去了 conntrack table 的开销;

  2. 使用 eBPF(extented Berkeley Packet Filter) 技术:eBPF 是类 Unix 系统上数据链路层的一种原始接口,提供原始链路层封包的收发。可以编写一种实现,使得 sidecar -> app、app -> sidecar 不需要走 netfilter,这样就可以节省开销。

image

这里的实现,即直接将数据包从 InBound socket 传输到 OutBound socket,或者反过来。

上图是因为 Pod 1 和 Pod 2 不在同一主机,所以走了 netfilter 来通过网络传输信息。若 Pod 1 和 Pod 2 在同一主机上,那么两者的数据传输不需要走 netfilter 也可以。其实,这实际上是 Kubernetes CNI的一种新的实现方式,而这种网络组件在业界也已经有了—— Cilium。

Kubernetes 常用的网络组件:

  • flannel

  • calico

  • cilium

image

Envoy

系统架构

image

在 Envoy 中,数据入口方向称为下游,数据出口方向称为上游。在下游方向,Envoy 使用 Listener 监听数据端口,监听下游的连接和请求;在上游方向,Envoy 使用 Cluster 来抽象上游服务,管理连接池、健康检查等配置。在下游和上游之间通过 Filter、Route 来连接。Filter 又称筛选器,是 Envoy 提供的可拔插插件的统称,使用它可以实现某些特定的功能,比如 L3/L4 Filter 能够让 Envoy 代理不同协议的数据。

xDS 协议

xDS 可以理解成 any Discovery Service,Envoy 通过这个协议,以订阅的方式来动态获取配置。

image

  • LDS:Listener Discovery Service,负责向 Envoy 下发 Listener 的相关配置;

  • RDS:Route Discovery Service,用于下发动态的路由规则,也包括重试、分流、限流等;

  • CDS:Cluster Discovery Service,下发集群相关配置,包括健康检查配置、连接池配置;

  • EDS:Endpoint Discovery Service,下发集群所有可访问的地址。

Mosn

总体架构

image

可以看到,同是作为数据平面,Mosn 也有 Listener、xDS 等概念。

  • Starter、Server、Listener、Config 为 MOSN 启动模块,用于完成 MOSN 的运行;

  • 最左侧的 Hardware、NET/IO、Protocol、Stream、Proxy、xDS 为 MOSN 架构的核心模块,用来实现 Service Mesh 的核心功能;

  • Router 为 MOSN 的路由模块,支持的功能包括:

  • Upstream 为后端管理模块,支持的功能包括:

  • Metrics 模块可对协议层的数据做记录和追踪

  • LoadBalance 是负载均衡管理模块,当前支持 RR、Random、Subset LB 等负载均衡算法;

  • Mixer 模块用来适配外部的服务,如鉴权、资源信息上报等;

  • FlowControl 模块是流量控制模块,可用来做限流保护;

  • Lab 模块是用来集成 IOT、DB、Media 等 Mesh 服务;

  • Admin 模块是 MOSN 的资源控制器,用来查看和管理其运行状态及资源信息。

数据走向

image

  • NET/IO 作为网络层,监测连接和数据包的到来,同时作为 listener filter 和 network filter 的挂载点;

  • Protocol 作为多协议引擎层,对数据包进行检测,并使用对应协议做 decode/encode 处理;

  • Stream 对 decode 的数据包做二次封装为 stream,作为 stream filter 的挂载点;

  • Proxy 作为 MOSN 的转发框架,对封装的 stream 做 proxy 处理。

SidecarSet 源码解析

注入过程

注入过程的总览:

image

// 处理pod的admission requests
func (h *PodCreateHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
    // 获取pod
        obj := &corev1.Pod{}

        err := h.Decoder.Decode(req, obj)
        if err != nil {
                return admission.Errored(http.StatusBadRequest, err)
        }
        clone := obj.DeepCopy()
        if obj.Namespace == "" {
                obj.Namespace = req.Namespace
        }

    // 注入pod的readiness probe
        injectPodReadinessGate(req, obj)

        // ...

    // 这里来注入sidecar容器
        err = h.sidecarsetMutatingPod(ctx, req, obj)
        if err != nil {
                return admission.Errored(http.StatusInternalServerError, err)
        }

        // 初始化容器的启动顺序
    // 这一步必须在注入sidecar容器后进行
        err = h.containerLaunchPriorityInitialization(ctx, req, obj)
        if err != nil {
                return admission.Errored(http.StatusInternalServerError, err)
        }

        // ...

        // 这里是对PersistentPodState做注入
    // PersistentPodState是openKruise的新CRD
        err = h.persistentPodStateMutatingPod(ctx, req, obj)
        if err != nil {
                return admission.Errored(http.StatusInternalServerError, err)
        }

    // 若经过上述的变动,没有发生变化,则返回空
        if reflect.DeepEqual(obj, clone) {
                return admission.Allowed("")
        }
        marshalled, err := json.Marshal(obj)
        if err != nil {
                return admission.Errored(http.StatusInternalServerError, err)
        }
        return admission.PatchResponseFromRaw(req.AdmissionRequest.Object.Raw, marshalled)
}

// 注入sidecar容器的过程
func (h *PodCreateHandler) sidecarsetMutatingPod(ctx context.Context, req admission.Request, pod *corev1.Pod) error {
    // 这里过滤其他状态的pod,例如在删除的pod
        if len(req.AdmissionRequest.SubResource) > 0 ||
                (req.AdmissionRequest.Operation != admissionv1.Create && req.AdmissionRequest.Operation != admissionv1.Update) ||
                req.AdmissionRequest.Resource.Resource != "pods" {
                return nil
        }
    // 这里过滤其他namespace的pod,例如"kube-system", "kube-public"
        if !sidecarcontrol.IsActivePod(pod) {
                return nil
        }

        var oldPod *corev1.Pod
        var isUpdated bool
        // 如果此次操作是更新pod,那么解析出old pod
        if req.AdmissionRequest.Operation == admissionv1.Update {
                isUpdated = true
                oldPod = new(corev1.Pod)
                if err := h.Decoder.Decode(
                        admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Object: req.AdmissionRequest.OldObject}},
                        oldPod); err != nil {
                        return err
                }
        }

        // 拿到sidecarset列表
        sidecarsetList := &appsv1alpha1.SidecarSetList{}
        if err := h.Client.List(ctx, sidecarsetList, utilclient.DisableDeepCopy); err != nil {
                return err
        }

        matchedSidecarSets := make([]sidecarcontrol.SidecarControl, 0)
        for _, sidecarSet := range sidecarsetList.Items {
        // 如果sidecarset暂停注入,则过滤
                if sidecarSet.Spec.InjectionStrategy.Paused {
                        continue
                }
        // 判断pod是否满足注入条件
                if matched, err := sidecarcontrol.PodMatchedSidecarSet(pod, sidecarSet); err != nil {
                        return err
                } else if !matched {
            // 不满足则过滤
                        continue
                }
        
                // 判断sidecarset是否active,不注入非active的sidecarset
                control := sidecarcontrol.New(sidecarSet.DeepCopy())
                if !control.IsActiveSidecarSet() {
                        continue
                }
        // 将满足注入条件的sidecarset加入队列
                matchedSidecarSets = append(matchedSidecarSets, control)
        }
    // 没有需要注入的sidecarset,直接返回
        if len(matchedSidecarSets) == 0 {
                return nil
        }

    // 有需要注入的sidecarset
    
        // 若pod是更新操作
        if isUpdated {
        // IsPodAvailabilityChanged此方法直接返回false
        // 因此sidecarset注入容器应该只对新创建的pod有效,对存量的pod无效
                if !matchedSidecarSets[0].IsPodAvailabilityChanged(pod, oldPod) {
                        klog.V(3).Infof("pod(%s/%s) availability unchanged for sidecarSet, and ignore", pod.Namespace, pod.Name)
                        return nil
                }
        }

        // ...
        // 把sidecar的相关信息构建出来
        sidecarContainers, sidecarInitContainers, sidecarSecrets, volumesInSidecar, injectedAnnotations, err := buildSidecars(isUpdated, pod, oldPod, matchedSidecarSets)
        if err != nil {
                return err
        // 如果没有需要注入的容器,则不执行注入操作
        } else if len(sidecarContainers) == 0 && len(sidecarInitContainers) == 0 {
                klog.V(3).Infof("[sidecar inject] pod(%s/%s) don't have injected containers", pod.Namespace, pod.Name)
                return nil
        }

        klog.V(3).Infof("[sidecar inject] begin inject sidecarContainers(%v) sidecarInitContainers(%v) sidecarSecrets(%v), volumes(%s)"+
                "annotations(%v) into pod(%s/%s)", sidecarContainers, sidecarInitContainers, sidecarSecrets, volumesInSidecar, injectedAnnotations,
                pod.Namespace, pod.Name)
        klog.V(4).Infof("[sidecar inject] before mutating: %v", util.DumpJSON(pod))
        // 把sidecar容器注入到pod中
        // 1. 对要注入init容器以name排序,这些sidecar的init容器会被添加到原有init容器的后面
        sort.SliceStable(sidecarInitContainers, func(i, j int) bool {
                return sidecarInitContainers[i].Name < sidecarInitContainers[j].Name
        })
        for _, initContainer := range sidecarInitContainers {
                pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer.Container)
        }
        // 2. 注入普通容器
        pod.Spec.Containers = mergeSidecarContainers(pod.Spec.Containers, sidecarContainers)
        // 3. 注入挂载
        pod.Spec.Volumes = util.MergeVolumes(pod.Spec.Volumes, volumesInSidecar)
        // 4. 注入 imagePullSecrets
    // imagePullSecrets是拉取镜像所需的配置信息
        pod.Spec.ImagePullSecrets = mergeSidecarSecrets(pod.Spec.ImagePullSecrets, sidecarSecrets)
        // 5. 注入annotation
        if pod.Annotations == nil {
                pod.Annotations = make(map[string]string)
        }
        for k, v := range injectedAnnotations {
                pod.Annotations[k] = v
        }
    // 注入完成
        klog.V(4).Infof("[sidecar inject] after mutating: %v", util.DumpJSON(pod))
        return nil
}

// 判断pod是否满足sidecarset注入的条件
func PodMatchedSidecarSet(pod *corev1.Pod, sidecarSet appsv1alpha1.SidecarSet) (bool, error) {
        // namespace不一样,不满足注入条件
        if sidecarSet.Spec.Namespace != "" && sidecarSet.Spec.Namespace != pod.Namespace {
                return false, nil
        }
        // 拿到selector
        selector, err := metav1.LabelSelectorAsSelector(sidecarSet.Spec.Selector)
        if err != nil {
                return false, err
        }

    // label满足注入条件
        if !selector.Empty() && selector.Matches(labels.Set(pod.Labels)) {
                return true, nil
        }
    // label不满足注入条件
        return false, nil
}

// 注入普通容器的过程
func mergeSidecarContainers(origins []corev1.Container, injected []*appsv1alpha1.SidecarContainer) []corev1.Container {
    // 构建一个映射:container name -> container在pod的顺序
        containersInPod := make(map[string]int)
        for index, container := range origins {
                containersInPod[container.Name] = index
        }
    // 需要注入在存量容器之前的sidecar容器
        var beforeAppContainers []corev1.Container
    // 需要注入在存量容器之后的sidecar容器
        var afterAppContainers []corev1.Container
        for _, sidecar := range injected {
                // 如果sidecar容器已经在存量容器中
        // 保持容器原有的顺序
                if index, ok := containersInPod[sidecar.Name]; ok {
                        origins[index] = sidecar.Container
                        continue
                }

        // 根据注入策略分别进行注入
                switch sidecar.PodInjectPolicy {
            // 注入在存量容器之前
                case appsv1alpha1.BeforeAppContainerType:
                        beforeAppContainers = append(beforeAppContainers, sidecar.Container)
            // 注入在存量容器之后
                case appsv1alpha1.AfterAppContainerType:
                        afterAppContainers = append(afterAppContainers, sidecar.Container)
            // 未设置则默认注入到存量容器之后
                default:
                        beforeAppContainers = append(beforeAppContainers, sidecar.Container)
                }
        }
    // 加入容器队列
        origins = append(beforeAppContainers, origins...)
        origins = append(origins, afterAppContainers...)
    // 返回注入好的容器队列
        return origins
}

热升级

SidecarSet 中的热升级:

  1. {sidecarContainer.name}-1: 如下图所示 envoy-1,这个容器代表正在实际工作的 sidecar 容器,例如:envoy:1.16.0

  2. {sidecarContainer.name}-2: 如下图所示 envoy-2,这个容器是业务配置的 hotUpgradeEmptyImage容器,例如:empty:1.0,用于后面的热升级机制

image

热升级容器注入

热升级容器注入过程的总览:

image

// 构建sidecar容器的过程
func buildSidecars(isUpdated bool, pod *corev1.Pod, oldPod *corev1.Pod, matchedSidecarSets []sidecarcontrol.SidecarControl) (
        sidecarContainers, sidecarInitContainers []*appsv1alpha1.SidecarContainer, sidecarSecrets []corev1.LocalObjectReference,
        volumesInSidecars []corev1.Volume, injectedAnnotations map[string]string, err error) {

        // ...

                        // 如果sidecar容器的升级策略是热更新
                        if sidecarcontrol.IsHotUpgradeContainer(sidecarContainer) {
                // 注入热更新容器
                                hotContainers, annotations := injectHotUpgradeContainers(pod, sidecarContainer)
                // 将热更新容器添加到队列
                                sidecarContainers = append(sidecarContainers, hotContainers...)
                // 把注入热更新的annotation添加进去
                                for k, v := range annotations {
                                        injectedAnnotations[k] = v
                                }
                        }
        // ...
}

// 注入热更新容器的过程
func injectHotUpgradeContainers(pod *corev1.Pod, sidecarContainer *appsv1alpha1.SidecarContainer) (
        sidecarContainers []*appsv1alpha1.SidecarContainer, injectedAnnotations map[string]string) {

        injectedAnnotations = make(map[string]string)
        hotUpgradeWorkContainer := sidecarcontrol.GetPodHotUpgradeInfoInAnnotations(pod)
    // 生成热更新容器
        // container1 是当前正在工作的容器
        // container2 是空容器,不工作
        container1, container2 := generateHotUpgradeContainers(sidecarContainer)
    // 加入sidecarContainers
        sidecarContainers = append(sidecarContainers, container1)
        sidecarContainers = append(sidecarContainers, container2)
        // 在annotation中标记sidecar的版本
        // "1" 说明这个容器是work容器
        injectedAnnotations[sidecarcontrol.GetPodSidecarSetVersionAnnotation(container1.Name)] = "1"
    // AltAnnotation表示此容器下一次热升级要切换到哪个容器
        injectedAnnotations[sidecarcontrol.GetPodSidecarSetVersionAltAnnotation(container1.Name)] = "0"
        // "0" 说明这个容器是热升级的empty容器
        injectedAnnotations[sidecarcontrol.GetPodSidecarSetVersionAnnotation(container2.Name)] = "0"
        injectedAnnotations[sidecarcontrol.GetPodSidecarSetVersionAltAnnotation(container2.Name)] = "1"
        // 标记正在工作的容器
        hotUpgradeWorkContainer[sidecarContainer.Name] = container1.Name
    // 将这个标记存储在annotation中
        by, _ := json.Marshal(hotUpgradeWorkContainer)
        injectedAnnotations[sidecarcontrol.SidecarSetWorkingHotUpgradeContainer] = string(by)

        return sidecarContainers, injectedAnnotations
}

// 生成热更新容器的过程
func generateHotUpgradeContainers(container *appsv1alpha1.SidecarContainer) (*appsv1alpha1.SidecarContainer, *appsv1alpha1.SidecarContainer) {
        name1, name2 := sidecarcontrol.GetHotUpgradeContainerName(container.Name)
        container1, container2 := container.DeepCopy(), container.DeepCopy()
        container1.Name = name1
        container2.Name = name2
        // 设置不工作的容器的image为HotUpgradeEmptyImage,这个由用户设定
        container2.Container.Image = container.UpgradeStrategy.HotUpgradeEmptyImage
        // ...
        return container1, container2
}

热升级流程

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

  1. Upgrade: 将empty容器升级为当前最新的sidecar容器,例如:envoy-2.Image = envoy:1.17.0

  2. Migration: lifecycle.postStart完成热升级流程中的状态迁移,当迁移完成后退出

  3. Reset: 状态迁移完成后,热升级流程将设置envoy-1容器为empty镜像,例如:envoy-1.Image = empty:1.0

image

Upgrade 过程

Upgrade 过程总览:

image

func updatePodSidecarContainer(control sidecarcontrol.SidecarControl, pod *corev1.Pod) {
        // ...

        for _, sidecarContainer := range sidecarSet.Spec.Containers {
                // ...

                // 更新sidecar容器
                newContainer := control.UpgradeSidecarContainer(&sidecarContainer, pod)
                // ...
        // 把container的更新同步到pod
                updateContainerInPod(*newContainer, pod)
                // ...
                if sidecarcontrol.IsHotUpgradeContainer(&sidecarContainer) {
                        // ...
            // 在annotation上把work的容器改成刚升级好的容器
            // 注意,这里只是annotation发生了变化,实质上流量还是打在了老容器中
                        hotUpgradeContainerInfos := sidecarcontrol.GetPodHotUpgradeInfoInAnnotations(pod)
                        hotUpgradeContainerInfos[sidecarContainer.Name] = newContainer.Name
                        // ...
                }
        }
        // ...
}

func (c *commonControl) UpgradeSidecarContainer(sidecarContainer *appsv1alpha1.SidecarContainer, pod *v1.Pod) *v1.Container {
        var nameToUpgrade, otherContainer, oldImage string
    // 判断是否为热升级容器
        if IsHotUpgradeContainer(sidecarContainer) {
        // 拿到需要升级的容器名,哪个容器的image是empty,哪个就是需要升级的容器
                nameToUpgrade, otherContainer = findContainerToHotUpgrade(sidecarContainer, pod, c)
                oldImage = util.GetContainer(otherContainer, pod).Image
        } else {
        // 如果不是热升级,则正常升级
                nameToUpgrade = sidecarContainer.Name
                oldImage = util.GetContainer(nameToUpgrade, pod).Image
        }
        // 如果要升级的image和原来的image一样,那么不升级
    // 即此热升级的容器image没有发生变化,那么不升级
        if sidecarContainer.Image == oldImage {
                return nil
        }
        container := util.GetContainer(nameToUpgrade, pod)
    // 更新容器的image
    // 热升级,即把empty容器升级到了最新版
        container.Image = sidecarContainer.Image
        klog.V(3).Infof("upgrade pod(%s/%s) container(%s) Image from(%s) -> to(%s)", pod.Namespace, pod.Name, nameToUpgrade, oldImage, container.Image)
        return container
}

// 获得需要升级的容器名字 过程
func findContainerToHotUpgrade(sidecarContainer *appsv1alpha1.SidecarContainer, pod *corev1.Pod, control SidecarControl) (string, string) {
        containerInPods := make(map[string]corev1.Container)
        for _, containerInPod := range pod.Spec.Containers {
                containerInPods[containerInPod.Name] = containerInPod
        }
        name1, name2 := GetHotUpgradeContainerName(sidecarContainer.Name)
        c1, c2 := containerInPods[name1], containerInPods[name2]

        // 哪个image是empty,哪个就需要升级
        if c1.Image == sidecarContainer.UpgradeStrategy.HotUpgradeEmptyImage {
                return c1.Name, c2.Name
        } else if c2.Image == sidecarContainer.UpgradeStrategy.HotUpgradeEmptyImage {
                return c2.Name, c1.Name
        }

        // ...
}

Reset 过程

Reset 过程总览:

image

func flipPodSidecarContainerDo(control sidecarcontrol.SidecarControl, pod *corev1.Pod) {
        sidecarSet := control.GetSidecarset()
    // 先构建一个containerName -> container的映射
        containersInPod := make(map[string]*corev1.Container)
        for i := range pod.Spec.Containers {
                container := &pod.Spec.Containers[i]
                containersInPod[container.Name] = container
        }

    // 代码走到这里,一定是empty容器已经更新到最新版本了
        var changedContainer []string
        for _, sidecarContainer := range sidecarSet.Spec.Containers {
                if sidecarcontrol.IsHotUpgradeContainer(&sidecarContainer) {
            // 拿到当前work的容器和empty的容器
            // 由于已经升级完成,此时的work容器应该是原来的empty容器
            // 此时的empty容器应该是原来的work容器
            // 获取哪个是work,哪个是empty,是用annotation来区分的,这个annotation在升级empty容器时已写入
                        workContainer, emptyContainer := sidecarcontrol.GetPodHotUpgradeContainers(sidecarContainer.Name, pod)
            // 若此时的empty容器已经更新到empty image了,则跳过
                        if containersInPod[emptyContainer].Image == sidecarContainer.UpgradeStrategy.HotUpgradeEmptyImage {
                                continue
                        }
                        // 拿到empty容器
                        containerNeedFlip := containersInPod[emptyContainer]
                        klog.V(3).Infof("try to reset %v/%v/%v from %s to empty(%s)", pod.Namespace, pod.Name, containerNeedFlip.Name,
                                containerNeedFlip.Image, sidecarContainer.UpgradeStrategy.HotUpgradeEmptyImage)
            // containerNeedFlip容器原来的image应该是旧版的image,现在改成empty
                        containerNeedFlip.Image = sidecarContainer.UpgradeStrategy.HotUpgradeEmptyImage
                        changedContainer = append(changedContainer, containerNeedFlip.Name)
                        // 更新empty容器的annotation
                        pod.Annotations[sidecarcontrol.GetPodSidecarSetVersionAnnotation(containerNeedFlip.Name)] = "0"
            // AltAnnotation表示下一次热更新时要切换到此容器要切换到empty
                        pod.Annotations[sidecarcontrol.GetPodSidecarSetVersionAltAnnotation(workContainer)] = "0"
                }
        }
        // ...
}

Migration 过程

一般情况下,是 Mesh 容器才需要用到流量 Migration。Mesh 容器一般都是通过监听固定端口来对外提供服务,此类 Mesh 容器的 Migration 过程可以概括为:通过UDS(unix domain sockets)传递ListenFD 和停止 Accpet、close graceful,逻辑图如下:

image

  // 流量迁移的过程

  /* 通过unix套接字,将流量从旧容器迁移到新容器 */
  f, _ := tcpLn.File()
  fdnum := f.Fd()
  data := syscall.UnixRights(int(fdnum))
  // 建立一个和新容器连接的socket
  raddr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
  uds, _ := net.DialUnix("unix", nil, raddr)
  // 把listenFD发给新容器
  uds.WriteMsgUnix(nil, data, nil)
  // 发过去之后,旧容器停止accept,close
  tcpLn.Close()

  /* 新容器收到listenFD并开始handle */
  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
    fds, _ := syscall.ParseUnixRights(&(scms[0]))
    f := os.NewFile(uintptr(fds[0]), "")
    ln, _ := net.FileListener(f)
    tcpLn, _ := ln.(*net.TCPListener)
    // 监听端口开始handle
    http.Serve(tcpLn, serveMux)
  }

无损升级

流量迁移的本质实际上就是 ListenFD 的迁移,常见的涉及平滑升级的组件有:Nginx、Envoy、Mosn

Nginx

在执行nginx -s reload时,Nginx 读取配置文件使修改生效,这个过程中 Nginx 仍然能正常工作。

它是通过 fork+execve 来实现的:准备升级时,old master fork 出一个子进程,然后该子进程调用execve,完成 listen fd 的迁移。old master 会把 listen fd 写入环境变量,子进程调用 execve 时,可以将该环境变量传入,这样子进程就继承了 old master 的 listen fd。

// listen fd迁移过程
ngx_pid_t ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv) {
        // ...
    ctx.path = argv[0];
    ctx.name = "new binary process";
    ctx.argv = argv;

    n = 2;
    env = ngx_set_environment(cycle, &n);
        // ...
    env[n++] = var;
    env[n] = NULL;
        // ...
    ctx.envp = (char *const *) env;

    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);

    if (ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) {
       //...
        return NGX_INVALID_PID;
    }

    pid = ngx_execute(cycle, &ctx);

    return pid;
}

// 启动nginx时,解析环境变量
static ngx_int_t ngx_add_inherited_sockets(ngx_cycle_t *cycle) {
        // ...
    inherited = (u_char *) getenv(NGINX_VAR);
    if (inherited == NULL) {
        return NGX_OK;
    }
    if (ngx_array_init(&cycle->listening, cycle->pool, 10, sizeof(ngx_listening_t)) != NGX_OK) {
        return NGX_ERROR;
    }
    for (p = inherited, v = p; *p; p++) {
        if (*p == ':' || *p == ';') {
            s = ngx_atoi(v, p - v);
            // ...
            v = p + 1;
            ls = ngx_array_push(&cycle->listening);
            if (ls == NULL) {
                return NGX_ERROR;
            }

            ngx_memzero(ls, sizeof(ngx_listening_t));

            ls->fd = (ngx_socket_t) s;
        }
    }
    // ...
    ngx_inherited = 1;

    return ngx_set_inherited_sockets(cycle);
}

// open 连接
ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle) {
    // ...

    for (tries = 5; tries; tries--) {
        failed = 0;

        /* for each listening socket */

        ls = cycle->listening.elts;
        for (i = 0; i < cycle->listening.nelts; i++) {
        // ...
            // 如果发现是继承而来,直接跳过
            if (ls[i].inherited) {
                continue;
            }
            // ...

            ngx_log_debug2(NGX_LOG_DEBUG_CORE, log, 0,
                           "bind() %V #%d ", &ls[i].addr_text, s);

            if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {
                //...
            }
            //...
        }
    }

    if (failed) {
        ngx_log_error(NGX_LOG_EMERG, log, 0, "still could not bind()");
        return NGX_ERROR;
    }

    return NGX_OK;
}

Envoy

刚刚提到的sidecar migration所介绍的方式,实际上和envoy的类似,通过socket的方式来传输listen fd(因为跨进程)

Mosn

以上两种方式严格来说并不能算无损迁移,因为它们在old进程关闭后,new进程开始work前,这段时间是不可用的,即使时间很短。

Mosn也是使用 UDS 来传递 listen fd,它可以把连接从old进程迁移到new进程上,实现真正的无损升级。

Nginx、Envoy 这种断连重建的方式是不支持多路复用连接的。因为多路复用协议的通信都是复用同一个连接,没有控制帧,也就是说无法知道所有通信都关闭的时间点,在这样的情况下,随意断连可能造成失效。而 Mosn 通过迁移连接的方式,保持连接的存活性。

image

对于存量连接请求,old mosn会将其fd和连接状态通过UDS发送给new mosn,由new mosn进行处理,此后,这个连接的请求都由new mosn处理

image

对于残留响应,统一由new mosn来进行响应处理,如果old mosn和new mosn同时返回数据,可能会造成数据帧序号的冲突,造成数据不可用

对于应用层数据的迁移,由于应用层协议很多,针对每个应用层协议构造对应的数据结构,会导致迁移过程很麻烦,于是mosn将迁移放到io层,即直接迁移TCP数据包,然后让new mosn自己拼装成对应协议的数据结构