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

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

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

在云原生逐渐盛行的如今,不可变基础设施的理念已经逐渐深刻人心。不可变基础设施最先是由Chad Fowler于2013年提出的,其核心思想为任何基础设施的实例一旦建立以后变成为只读状态,如须要修改和升级,则使用新的实例进行替换。这一理念的指导下,实现了运行实例的一致,所以在提高发布效率、弹性伸缩、升级回滚方面体现出了无与伦比的优点。git

kubernetes是不可变基础设施理念的一个极佳实践平台。Pod做为k8s的最小单元,承担了应用实例这一角色。经过ReplicaSet从而对Pod的副本数进行控制,从而实现Pod的弹性伸缩。而进行更新时,Deployment经过控制两个ReplicaSet的副本数此消彼长,从而进行实例的总体替换,实现升级和回滚操做。github

咱们进一步思考,咱们是否须要将Pod做为一个彻底不可变的基础设施实例呢?其实在kubernetes自己,已经提供了一个替换image的功能,来实现Pod不变的状况下,经过更换image字段,实现Container的替换。这样的优点在于无需从新建立Pod,便可实现升级,直接的优点在于免去了从新调度等的时间,使得容器能够快速启动。web

从这个思路延伸开来,那么咱们其实能够将Pod和Container分为两层来看。将Container做为不可变的基础设施,确保应用实例的完整替换;而将Pod看为可变的基础设施,能够进行动态的改变,亦便可变层。swift

1.2 关于升级变化的分析

对于应用的升级变化种类,咱们来进行一下分类讨论,将其分为如下几类:后端

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

(滑动查看完整表格)api

针对不一样的变化类型,咱们作过一次抽样调查统计,能够看到下图的一个统计结果。微信

在一次升级变化中若是含有多个变化,则统计为屡次。网络

能够看到支持镜像的替换能够覆盖一半左右的的升级变化,可是仍然有至关多的状况下致使不得不从新建立Pod。这点来讲,不是特别友好。因此咱们作了一个设计,将对于Pod的变化分为了三种Dynamic,Rebuild,Static三种。架构

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

(滑动查看完整表格)app

这样动态修改和原地更新的方式能够覆盖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/kruise/pull/154

另外,还有个小坑,就是在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。


☆ END ☆


招聘信息

OPPO互联网云平台团队招聘一大波岗位,涵盖Java、容器、Linux内核开发、产品经理、项目经理等多个方向,请在公众号后台回复关键词“云招聘”查看查详细信息。


你可能还喜欢

如何用 CI (持续集成) 保证研发质量

如何设计并实现存储QoS?

云原生Service Mesh探索与实践

如何进行 kubernetes 问题的排障

OPPO自研ESA DataFlow架构与实践


更多技术干货

扫码关注

OPPO互联网技术

 

我就知道你“在看”

本文分享自微信公众号 - OPPO互联网技术(OPPO_tech)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索