关于kubernetes:基于Kubernetes和OpenKruise的可变基础设施实践

3次阅读

共计 4535 个字符,预计需要花费 12 分钟才能阅读完成。

本文来自 OPPO 互联网根底技术团队,转载请注名作者。同时欢送关注咱们的公众号:OPPO_tech,与你分享 OPPO 前沿互联网技术及流动。

1. 对于可变基础设施的思考

1.1 kubernetes 中的可变与不可变基础设施

在云原生逐步流行的当初,不可变基础设施的理念曾经逐步深入人心。不可变基础设施最早是由 Chad Fowler 于 2013 年提出的,其核心思想为任何基础设施的实例一旦创立之后变成为只读状态,如须要批改和降级,则应用新的实例进行替换。这一理念的领导下,实现了运行实例的统一,因而在晋升公布效率、弹性伸缩、降级回滚方面体现出了无可比拟的劣势。

kubernetes 是不可变基础设施理念的一个极佳实际平台。Pod 作为 k8s 的最小单元,承当了利用实例这一角色。通过 ReplicaSet 从而对 Pod 的正本数进行管制,从而实现 Pod 的弹性伸缩。而进行更新时,Deployment 通过管制两个 ReplicaSet 的正本数此消彼长,从而进行实例的整体替换,实现降级和回滚操作。

咱们进一步思考,咱们是否须要将 Pod 作为一个齐全不可变的基础设施实例呢?其实在 kubernetes 自身,曾经提供了一个替换 image 的性能,来实现 Pod 不变的状况下,通过更换 image 字段,实现 Container 的替换。这样的劣势在于无需从新创立 Pod,即可实现降级,间接的劣势在于免去了从新调度等的工夫,使得容器能够疾速启动。

从这个思路延长开来,那么咱们其实能够将 Pod 和 Container 分为两层来看。将 Container 作为不可变的基础设施,确保利用实例的残缺替换;而将 Pod 看为可变的基础设施,能够进行动静的扭转,亦即可变层。

1.2 对于降级变动的剖析

对于利用的降级变动品种,咱们来进行一下分类探讨,将其分为以下几类:

降级变动类型 阐明
规格的变动 cpu、内存等资源使用量的批改
配置的变动 环境变量、配置文件等的批改
镜像的变动 代码批改后镜像更新
健康检查的变动 readinessProbe、livenessProbe 配置的批改
其余变动 调度域、标签批改等其余批改

针对不同的变动类型,咱们做过一次抽样调查统计,能够看到下图的一个统计后果。

在一次降级变动中如果含有多个变动,则统计为屡次。

能够看到反对镜像的替换能够笼罩一半左右的的降级变动,然而依然有相当多的状况下导致不得不从新创立 Pod。这点来说,不是特地敌对。所以咱们做了一个设计,将对于 Pod 的变动分为了三种 Dynamic,Rebuild,Static 三种。

批改类型 批改类型阐明 批改举例 对应变动类型
Dynamic 动静批改 Pod 不变,容器无需重建 批改了健康检查端口 健康检查的变动
Rebuild 原地更新 Pod 不变,容器须要从新创立 更新了镜像、配置文件或者环境变量 镜像的变动,配置的变动
Static 动态批改 Pod 须要从新创立 批改了容器规格 规格的变动

这样动静批改和原地更新的形式能够 笼罩 90% 以上的降级变动。在 Pod 不变的状况下带来的收益也是不言而喻的。

  1. 缩小了调度、网络创立等的工夫。
  2. 因为同一个利用的镜像大部分层都是复用的,大大缩短了镜像拉取的工夫。
  3. 资源锁定,避免在集群资源紧缺时因为出让资源从新创立进入调度后,导致资源被其余业务抢占而无奈运行。
  4. IP 不变,对于很多有状态的服务非常敌对。

2. Kubernetes 与 OpenKruise 的定制

2.1 kubernetes 的定制

那么如何来实现 Dynamic 和 Rebuild 更新呢?这里须要对 kubernetes 进行一下定制。

动静批改定制

liveness 和 readiness 的动静批改反对相对来说较为简单,次要批改点在与 prober_manager 中减少了 UpdatePod 函数,用以判断当 liveness 或者 readiness 的配置扭转时,进行原先的 worker,重新启动新的 worker。而后将 UpdatePod 嵌入到 kubelet 的 HandlePodUpdates 的流程中即可。

func (m *manager) UpdatePod(pod *v1.Pod) {m.workerLock.Lock()
    defer m.workerLock.Unlock()

    key := probeKey{podUID: pod.UID}
    for _, c := range pod.Spec.Containers {
        key.containerName = c.Name
        {
            key.probeType = readiness
            worker, ok := m.workers[key]
            if ok {
                if c.ReadinessProbe == nil {
                    //readiness 置空了,原 worker 进行
                    worker.stop()} else if !reflect.DeepEqual(*worker.spec, *c.ReadinessProbe) {
                    //readiness 配置扭转了,原 worker 进行
                    worker.stop()}
            }
            if c.ReadinessProbe != nil {if !ok || (ok && !reflect.DeepEqual(*worker.spec, *c.ReadinessProbe)) {
                    //readiness 配置扭转了,启动新的 worker
                    w := newWorker(m, readiness, pod, c)
                    m.workers[key] = w
                    go w.run()}
            }
        }
        {
            //liveness 与 readiness 类似
            ......
        }
    }
}
原地更新定制

kubernetes 原生反对了 image 的批改,对于 env 和 volume 的批改是未做反对的。因而咱们对 env 和 volume 也反对了批改性能,以便其能够进行环境变量和配置文件的替换。这里利用了一个小技巧,就是咱们在减少了一个 ExcludedHash,用于计算 Container 内,蕴含 env,volume 在内的各项配置。

func HashContainerExcluded(container *v1.Container) uint64 {copyContainer := container.DeepCopy()
    copyContainer.Resources = v1.ResourceRequirements{}
    copyContainer.LivenessProbe = &v1.Probe{}
    copyContainer.ReadinessProbe = &v1.Probe{}
    hash := fnv.New32a()
    hashutil.DeepHashObject(hash, copyContainer)
    return uint64(hash.Sum32())
}

这样当 env,volume 或者 image 发生变化时,就能够间接感知到。在 SyncPod 时,用于在计算 computePodActions 时,发现容器的相干配置产生了变动,则将该容器进行 Rebuild。

func (m *kubeGenericRuntimeManager) computePodActions(pod *v1.Pod, podStatus *kubecontainer.PodStatus) podActions {
    ......
    for idx, container := range pod.Spec.Containers {
        ......
        if expectedHash, actualHash, changed := containerExcludedChanged(&container, containerStatus); changed {
            // 当 env,volume 或者 image 更换时,则重建该容器。reason = fmt.Sprintf("Container spec exclude resources hash changed (%d vs %d).", actualHash, expectedHash)            
            restart = true
        }
        ......
        message := reason
        if restart {
            // 将该容器退出到重建的列表中
            message = fmt.Sprintf("%s. Container will be killed and recreated.", message)
            changes.ContainersToStart = append(changes.ContainersToStart, idx)
        }
......
    return changes
}
Pod 的生命周期

在 Pod 从调度实现到创立 Running 中,会有一个 ContaienrCreating 的状态用以标识容器在创立中。而原生中当 image 替换时,先前的一个容器销毁,后一个容器创立过程中,Pod 状态会始终处于 Running,容易有谬误流量导入,用户也无奈辨认此时容器的状态。

因而咱们为原地更新,在 ContainerStatus 里减少了 ContaienrRebuilding 的状态,同时在容器创立胜利前 Pod 的 Ready Condition 置为 False,以便表白容器整在重建中,利用在此期间不可用。利用此标识,能够在此期间不便辨认 Pod 状态、隔断流量。

2.2 OpenKruise 的定制

OpenKruise (https://openkruise.io/) 是阿里开源的一个我的项目,提供了一套在 Kubernetes 外围控制器之外的扩大 workload 治理和实现。其中 Advanced StatefulSet,基于原生 StatefulSet 之上的加强版本,默认行为与原生完全一致,在此之外提供了原地降级、并行公布(最大不可用)、公布暂停等性能。

Advanced StatefulSet 中的原地降级即与本文中的 Rebuild 统一,然而原生只反对替换镜像。因而咱们在 OpenKruise 的根底上进行了定制,使其不仅能够反对 image 的原地更新,也能够反对当 env、volume 的原地更新以及 livenessProbe、readinessProbe 的动静更新。这个次要在 shouldDoInPlaceUpdate 函数中进行一下判断即可。这里就不再做代码展现了。

还在生产运行中还发现了一个根底库的小 bug,咱们也顺带向社区做了提交修复。https://github.com/openkruise…。

另外,还有个小坑,就是在 pod 里为了标识不同的版本,退出了 controller-revision-hash 值。

[root@xxx ~]# kubectl get pod -n predictor  -o yaml predictor-0 
apiVersion: v1
kind: Pod
metadata:
  labels:
    controller-revision-hash: predictor-85f9455f6
...

一般来说,该值应该只应用 hash 值作为 value 就能够了,然而 OpenKruise 中采纳了 {sts-name}+{hash} 的形式,这带来的一个小问题就是 sts-name 就要因为 label value 的长度受到限制了。

3. 写在最初

定制后的 OpenKruise 和 kubernetes 曾经大规模在各个集群上上线,广泛应用在多个业务的后端运行服务中。经统计,通过原地更新笼罩了 87% 左右的降级部署需要,根本达到预期。

特地鸣谢阿里奉献的开源我的项目 OpenKruise。

正文完
 0