本文来自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: v1kind: Podmetadata:  labels:    controller-revision-hash: predictor-85f9455f6...

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

3. 写在最初

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

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