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

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 带来的好处:
-
允许开发者无侵入地新增功能;
-
与主程序松耦合,主程序与 sidecar 程序升级时互不影响可用性;
-
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或边缘代理

对于每个服务,都创建一个控制器,由其部署相应的 pod,pod 内的 container 即该服务,实现服务的基础扩缩容能力,然后由 service 进行负载均衡,再用 Nginx Ingress Controller 统一对外部开放代理。
这种架构部署起来十分简单,但是无法管理服务间的流量。
路由器网格

这种架构使用一个中心组建 Router 来对容器间的流量进行拦截,提供了最基础的流量管理功能。但是当服务一多起来,管理就很麻烦。
例如:
-
无法对单个服务实现限流功能;
-
需要管理多端对多端的双向流量,业务功能复杂;
-
单一中心组件宕机后会导致所有服务不可用;
-
Router 没有自动感知功能,每次有新服务都需要注册进来。
Proxy per Node

在每个节点上部署一个代理,每个代理只需管理一个节点上的若干个微服务。代理相比上一代架构而言,工程复杂度比较简单,只需使用 Kubernetes 的 DaemonSet 来部署,同时也方便实现服务的限流、重试功能。
这个架构已经有 sidecar 的雏形了,但还是有一些致命的缺点:
-
服务仍然需要自己注册到代理中,对服务不是完全透明;
-
粒度不够细,若有一个节点的代理挂掉,则整个节点的微服务都无法工作。
Sidecar代理模型

相较于上一代架构来说,这种架构以单个微服务 container 为粒度。但是缺少管理这些代理的组件,数量一多还是不方便管理。
Service Mesh 是什么
Service Mesh 即服务网格,服务网格中分为控制平面和数据平面。
控制平面的特点:
-
不直接解析数据包;
-
与数据平面中的代理通信,下发策略和配置;
-
负责网络行为的可视化;
-
通常提供 API 或者命令行工具可用于配置版本化管理,便于持续集成和部署。
数据平面的特点:
-
按照无状态目标设计;
-
直接处理入站和出站数据包,转发、路由、健康检查、负载均衡、认证、鉴权、产生监控数据等;
-
对应用来说透明,即可以做到无感知部署。
Service Mesh 的价值所在
-
可观察性:proxy sidecar 能够对流量数据进行采集、存储,发送给后端监控、日志管理系统,使得服务间的访问能够被观测;
-
流量控制:可以做到对服务的超时重试、访问速率限制等;
-
安全性:通过从控制平面获取的证书配置,实现服务的鉴权,对连接传输内容进行加密;
-
延迟和故障注入:通过人为地注入故障,检验系统的健壮性。
以上这些特点,都是服务无感知的,由 Service Mesh 来实现。
Sidecar 的工程实现
使用到 sidecar 模式的业界开源服务网格组件中,比较出名的就是 Istio。
Istio 简介
Istio 是一个开源的服务网格组件,它提供了对整个服务网格的行为洞察和操作控制的能力,以及一个完整的满足微服务应用各种需求的解决方案。
特性:
-
流量管理:控制服务间的流量和 API 调用流,使调用更可靠,增强不同环境下的网络鲁棒性;
-
可观测性:了解服务之间的依赖关系和它们之间的性质和流量,提供快速识别定位问题的能力;
-
策略实施:通过配置 mesh 而不是以改变代码的方式来控制服务之间的访问策略;
-
服务识别和安全:提供在 mesh 里的服务可识别性和安全性保护。
Istio 的架构

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 用于 强制访问控制 网络规则。

TPROXY:即 Transparent Proxy,透明代理,能够实现不修改报文的头部而达到转发的目的。其实现原理大致为:在创建 socket 时,设置 IP_TRANSPARENT 标志,这样就能够 bind 本机之外的 ip,接着再 bind 0.0.0.0,这样就能监听到所有 ip;然后对 iptables 的 PREROUTING 链做处理,让其不要走 FORWARD 链,例如:
在 PREROUTING 链添加一条 iptables 规则:
iptables -t mangle -I PREROUTING -p udp --dport 5301 -j MARK --set-mark 1
告诉 iproute 对 fwmark=1 的流量查表100:
ip rule add fwmark 1 lookup 100
设置表100所有 IPv4 地址均为 local:
- ip route add local 0.0.0.0/0 dev lo table 100
再来看看 istio-proxy 容器,Envoy 代理是怎么做流量劫持的:
注:非 TCP 流量路由,可将其理解为正常情况下 Pod 的流量接收方式。

使用 iptables 做流量劫持的缺点:
-
从上图我们可以看到,InBound 流量和 OutBound 流量两次穿越用户空间和内核空间,这其中的开销比起正常的流量接收方式多了一倍。这种开销的存在,其实是因为 sidecar -> app、app -> sidecar 这个数据传输的过程要进行切换;
-
对于重定向流量而言,需要对连接进行跟踪,这依赖了 Linux 内核模块 conntrack 来实现。当连接数亿多,会造成 conntrack table 满的情况。
conntrack:conntrack 通过维护 conntrack table 来进行连接跟踪,它能够跟踪 TCP、UDP、ICMP 等报文连接。通过 netfilter 的 hook,即 5 条链,来检查系统中进出的每个网络数据包。例如,当 NAT 模块收到一个新的报文时,它会先查 conntrack table,然后根据里面的数据修改报文的 IP:Port,然后写入 conntrack table。
经过调研,发现有两种方案来优化这些开销:
-
使用 TPROXY 模式:由于 TPROXY 不会修改报文头部,因此不需要记录连接,这样省去了 conntrack table 的开销;
-
使用 eBPF(extented Berkeley Packet Filter) 技术:eBPF 是类 Unix 系统上数据链路层的一种原始接口,提供原始链路层封包的收发。可以编写一种实现,使得 sidecar -> app、app -> sidecar 不需要走 netfilter,这样就可以节省开销。

这里的实现,即直接将数据包从 InBound socket 传输到 OutBound socket,或者反过来。
上图是因为 Pod 1 和 Pod 2 不在同一主机,所以走了 netfilter 来通过网络传输信息。若 Pod 1 和 Pod 2 在同一主机上,那么两者的数据传输不需要走 netfilter 也可以。其实,这实际上是 Kubernetes CNI的一种新的实现方式,而这种网络组件在业界也已经有了—— Cilium。
Kubernetes 常用的网络组件:
flannel
calico
cilium

Envoy
系统架构

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

-
LDS:Listener Discovery Service,负责向 Envoy 下发 Listener 的相关配置;
-
RDS:Route Discovery Service,用于下发动态的路由规则,也包括重试、分流、限流等;
-
CDS:Cluster Discovery Service,下发集群相关配置,包括健康检查配置、连接池配置;
-
EDS:Endpoint Discovery Service,下发集群所有可访问的地址。
Mosn
总体架构

可以看到,同是作为数据平面,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 的资源控制器,用来查看和管理其运行状态及资源信息。
数据走向

-
NET/IO 作为网络层,监测连接和数据包的到来,同时作为 listener filter 和 network filter 的挂载点;
-
Protocol 作为多协议引擎层,对数据包进行检测,并使用对应协议做 decode/encode 处理;
-
Stream 对 decode 的数据包做二次封装为 stream,作为 stream filter 的挂载点;
-
Proxy 作为 MOSN 的转发框架,对封装的 stream 做 proxy 处理。
SidecarSet 源码解析
注入过程
注入过程的总览:

// 处理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 中的热升级:
-
{sidecarContainer.name}-1: 如下图所示 envoy-1,这个容器代表正在实际工作的 sidecar 容器,例如:envoy:1.16.0
-
{sidecarContainer.name}-2: 如下图所示 envoy-2,这个容器是业务配置的 hotUpgradeEmptyImage容器,例如:empty:1.0,用于后面的热升级机制

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

// 构建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
}
热升级流程
热升级流程主要分为以下三个步骤:
-
Upgrade: 将empty容器升级为当前最新的sidecar容器,例如:envoy-2.Image = envoy:1.17.0
-
Migration: lifecycle.postStart完成热升级流程中的状态迁移,当迁移完成后退出
-
Reset: 状态迁移完成后,热升级流程将设置envoy-1容器为empty镜像,例如:envoy-1.Image = empty:1.0

Upgrade 过程
Upgrade 过程总览:

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 过程总览:

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,逻辑图如下:

// 流量迁移的过程
/* 通过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 通过迁移连接的方式,保持连接的存活性。

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

对于残留响应,统一由new mosn来进行响应处理,如果old mosn和new mosn同时返回数据,可能会造成数据帧序号的冲突,造成数据不可用
对于应用层数据的迁移,由于应用层协议很多,针对每个应用层协议构造对应的数据结构,会导致迁移过程很麻烦,于是mosn将迁移放到io层,即直接迁移TCP数据包,然后让new mosn自己拼装成对应协议的数据结构