本文来自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不变的状况下带来的收益也是不言而喻的。
- 缩小了调度、网络创立等的工夫。
- 因为同一个利用的镜像大部分层都是复用的,大大缩短了镜像拉取的工夫。
- 资源锁定,避免在集群资源紧缺时因为出让资源从新创立进入调度后,导致资源被其余业务抢占而无奈运行。
- 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。