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