关于运维:蚂蚁大规模-Sigma-集群-Etcd-拆分实践

8次阅读

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

文|杜克伟(花名:苏麟 )

蚂蚁团体高级开发工程师

负责蚂蚁 Kubernetes 集群的稳定性方面的工作
专一于集群组件变更、稳定性危险保障

本文 15738 字 浏览 20 分钟

前 言

为了撑持蚂蚁业务的迭代降级,蚂蚁基础设施往年启动了 Gzone 全面云化我的项目。要求 Gzone 需与曾经云化的 Rzone 合并部署在同一个集群,Sigma 单集群理论治理的节点规模将超过万台,单集群承当的业务也将更加简单。

因而咱们启动了大规模 Sigma 集群的性能优化计划,在申请提早上冀望可能对齐社区规范,不因规模增长的起因降落。

etcd 作为 Sigma 集群的数据存储数据库,是整个集群的基石,可能间接决定性能天花板。社区倡议的单 etcd 集群存储限度是 8G, 而蚂蚁 Sigma 集群的单 etcd 集群存储量早已超过了这个限度,Gzone 上云我的项目势必会减轻 etcd 的累赘。

首先,蚂蚁业务混合了散失计算、离线计算和在线业务,混合大量的生命周期在分钟级甚至是秒级的 Pod,单集群每天的 Pod 创立量也晋升到了数十万, 都须要 etcd 来撑持;

其次,简单的业务需要催生了大量的 List (list all、list by namespace、list by label)、watch、create、update、delete 申请,针对 etcd 的存储个性,这些申请性能均会随着 etcd 存储规模的增大而重大衰减,甚至导致 etcd OOM,申请超时等异样;

最初,申请量的增长也加剧了 etcd 因为 compact、defrag 操作对申请 RT P99 的暴涨,甚至申请超时,从而导致集群要害组件调度器、CNI 服务等 Operator 类组件间断性失落,造成集群不可用。

依据前人的教训,针对 etcd 集群进行数据程度拆分是一个无效的优化伎俩,典型的拆分是把 Pod 等重要数据独自 etcd 集群来存储,从而升高单 etcd 存储和申请解决的压力,升高申请解决提早。然而 Pod 资源数据针对 Kubernetes 集群具备特殊性,具备其余资源没有的高要求,尤其是针对已颇具规模正在服务的 K8s 集群进行拆分更是须要万分审慎小心。

本文次要记录了蚂蚁团体在进行 Pod 资源数据拆分过程中一些实践经验和心得。

抛砖引玉,请大家多多指教!

PART. 1 面临的挑战

从前人的 Pod 数据拆分教训理解到,Pod 数据拆分是一个高危且简单的流程,起因来自于 Pod 数据本身的特殊性。

Pod 是一组容器的组合,是 Sigma 集群中可调度的最小单位,是业务 workload 的最终承载体。Sigma 集群的最外围最终的交付资源就是 Pod 资源。

Sigma 集群最外围的 SLO 也是 Pod 的创立删除降级等指标。Pod 资源数据能够说是 Sigma 集群最重要的资源数据。同时 Sigma 集群又是由事件驱动的,面向终态体系设计,所以 Pod 资源数据拆分除了思考根本的前后数据一致性问题外,还要思考拆分过程中对其余组件的影响。

前人的拆分教训流程中最外围的操作是数据完整性校验和要害服务组件停机。数据完整性校验顾名思义是为了保证数据前后的一致性,而要害服务组件停机是为了防止拆分过程中如果组件不停机造成的非预期结果,可能会有 Pod 非预期删除,Pod 状态被毁坏等。然而如果照搬这套流程到蚂蚁 Sigma 集群,问题就来了。

蚂蚁 Sigma 作为蚂蚁团体外围的基础设施,通过 2 年多的倒退曾经成为领有 80+ 集群、单集群节点数可达到 1.2w+ 规模的云底座。在如此规模的集群上,运行着蚂蚁外部百万级别的 Pod,其中短运行时长 Pod 每天的创立量在 20w+ 次。为了满足各种业务倒退需要,Sigma 团队与蚂蚁存储、网络、PaaS 等多个云原生团队单干,截止目前 Sigma 共建的第三方组件量曾经达到上百个。如果 Pod 拆分要重启组件,须要大量的与业务方的沟通工作,须要多人独特操作。如果操作不慎,梳理不齐全漏掉几个组件就有可能造成非预期的结果。

从蚂蚁 Sigma 集群现状总结一下已有的 Pod 数据拆分教训流程的问题:

  1. 人工操作大量组件重启工夫长、易出错

潜在须要重启的组件高达数十个,须要与各个组件 owner 进行沟通确认,梳理出须要重启的组件,须要消耗大量的沟通工夫。万一脱漏就可能造成非预期的结果,比方资源残留、脏数据等。

  1. 齐全停机持续时间长突破 SLO

数据拆分期间组件齐全停机,集群性能齐全不可用,且拆分操作极为耗时,依据前人教训,持续时间可能长达 1~2 小时,齐全突破了 Sigma 集群对外的 SLO 承诺。

  1. 数据完整性校验伎俩单薄

拆分过程中应用 etcd 开源工具 make-mirror 工具来迁徙数据,该工具实现比较简单,就是读取一个 etcd 的 key 数据而后从新写到另一个 etcd,不反对断点续传,同时因从新写入 etcd 造成原有 key 的重要字段 revision 被毁坏,影响 Pod 数据的 resourceVersion, 可能会造成非预期结果。对于 revision 后文会具体阐明。最初的校验伎俩是测验 keys 的数量是否前后一致,如果两头 key 的数据被毁坏,也无奈发现。

PART. 2 问题解析

美妙的冀望

作为一个懒人,不想和那么多的组件 owner 沟通重启问题,大量组件重启也易造成操作脱漏,造成非预期问题。同时是否有更好的数据完整性校验的伎俩呢?

如果组件不重启,那么整个过程后演变为上面的流程,预期将简化流程,同时保障安全性。

为了达成美妙的冀望,咱们来寻根究底从新 review 整个流程。

数据拆分是在做什么?

家喻户晓,etcd 存储了 Kubernetes 集群中的各种资源数据,如 Pod、Services、Configmaps、Deployment 等等。

Kube-apiserver 默认是所有的资源数据都存储在一套 etcd 集群中,随着存储规模的增长,etcd 集群会面临性能瓶颈。以资源维度进行 etcd 的数据拆分来晋升 Kube-apiserver 拜访 etcd 的性能是业内所共识的教训优化思路,实质是升高单 etcd 集群的数据规模,缩小单 etcd 集群的拜访 QPS。

针对蚂蚁 Sigma 集群本身的规模和需要,需拆分为 4 个独立的 etcd 集群,别离存储 Pods、Leases、event 和其余资源数据,上面别离简要阐明这前三类 (Pods、Lease、event) 须要拆分进来的资源数据。

Event 资源

K8s event 资源数据并不是 watch 中的 event,个别是示意关联对象产生的事件,比方 Pod 拉取镜像,容器启动等。在业务上个别是 CI/CD 须要流水式展现状态时间轴,须要频繁拉取 event 资源数据。

event 资源数据自身就是有效期的(默认是 2 小时),除了通过 event 观测资源对象生命周期变动外,个别没有重要的业务依赖,所以说 event 数据个别认为是能够抛弃,不须要保障数据前后一致性的。

因为上述的数据特点,event 的拆分是最为简略的,只须要批改 APIServer 的启动配置,重启 APIServer 即可,不须要做数据迁徙,也不须要做老旧数据的清理。整个拆分过程除了 Kube-apiserver 外,不须要任何组件的重启或者批改配置。

### Lease 资源

Lease 资源个别用于 Kubelet 心跳上报,另外也是社区举荐的 controller 类组件选主的资源类型。

每个 Kubelet 都应用一个 Lease 对象进行心跳上报,默认是每 10s 上报一次。节点越多,etcd 承当的 update 申请越多,节点 Lease 的每分钟更新次数是节点总量的 6 倍,1 万个节点就是每分钟 6 万次,还是十分可观的。Lease 资源的更新对于判断 Node 是否 Ready 十分重要,所以独自拆分进去。

controller 类组件的选主逻辑基本上都是应用的开源的选主代码包,即应用 Lease 选主的组件都是对立的选主逻辑。Kubelet 的上报心跳的代码逻辑更是在咱们掌控之中。从代码中剖析可知 Lease 资源并不需要严格的数据一致性,只须要在肯定工夫内保障 Lease 数据被更新过,就不影响应用 Lease 的组件失常性能。

Kubelet 判断 Ready 的逻辑是否在 controller-manager 中的工夫默认设置是 40s,即只有对应 Lease 资源在 40s 内被更新过,就不会被判断为 NotReady。而且 40s 这个工夫能够调长,只有在这个工夫更新就不影响失常性能。应用选主的 controller 类组件的选主 Lease duration 个别为 5s~65s 能够自行设置。

因而 Lease 资源拆分虽和 event 相比要简单一些,但也是比较简单的。多进去的步骤就是在拆分的过程中,须要把老 etcd 中的 Lease 资源数据同步到新的 etcd 集群中,个别咱们应用 etcdctl make-mirror 工具同步数据。此时若有组件更新 Lease 对象,申请可能会落在老 etcd,也可能落在新的 etcd 中。落在老 etcd 中的更新会通过 make-mirror 工具同步到新的 etcd 中,因为 Lease 对象较少,整个过程持续时间很短,也不会存在问题。另外还须要迁徙拆分实现后,删除老 etcd 中的 Lease 资源数据,以便开释锁占用的空间,尽管空间很小,但也不要节约。相似 event 资源拆分,整个拆分过程除了 kube-apiserver 外,同样不须要任何组件的重启或者批改配置。

### Pod 资源

Pod 资源可能是咱们最相熟的资源数据了,所有的 workload 最终都是由 Pod 来实在承载。K8s 集群的治理外围就在于 Pod 资源的调度和治理。Pod 资源数据要求严格的数据一致性,Pod 的任何更新产生的 watch event 事件,都不能错过,否则就有可能影响 Pod 资源交付。Pod 资源的特点也正是导致传统 Pod 资源数据拆分过程中须要大规模重启相干组件的起因,后文会解析其中的起因。

社区 kube-apiserver 组件自身早已有依照资源类型设置独立 etcd 存储的配置 –etcd-servers-overrides。

–etcd-servers-overrides strings
Per-resource etcd servers overrides, comma separated. The individual override format: group/resource#servers, where servers are URLs, semicolon separated. Note that this applies only to resources compiled into this server binary.

咱们常见的资源拆分的简要配置示例如下:

events 拆分配置

–etcd-servers-overrides=/events#https://etcd1.events.xxx:2xxx;https://etcd2.events.xxx:2xxx;https://etcd3.events.xxx:2xxx

leases 拆分配置

–etcd-servers-overrides=coordination.k8s.io/leases#https://etcd1.leases.xxx:2xxx;https://etcd2.leases.xxx:2xxx;https://etcd3.leases.xxx:2xxx

pods 拆分配置

–etcd-servers-overrides=/pods#https://etcd1.pods.xxx.net:2xxx;https://etcd2.pods.xxx:2xxx;https://etcd3.pods.xxx:2xxx

重启组件是必须的吗?

为了理解重启组件是否必须,如果不重启组件有什么影响。咱们在测试环境进行了验证,后果咱们发现在拆分实现后,新建 Pod 无奈被调度,已有 Pod 的无奈被删除,finalizier 无奈摘除。通过剖析后,发现相干组件无奈感知到 Pod 创立和删除事件。

那么为什么会呈现这种问题呢?要答复这个问题,就须要从 K8s 整个设计核心理念到实现具体细节全副理分明讲透彻,咱们细细道来。

如果 K8s 是一个一般的业务零碎,Pod 资源数据拆分只是影响了 kube-apiserver 拜访 Pod 资源的存储地位,也就是影响面只到 kube-apiserver 层面的话,就不会存在本篇文章了。

对于一般的业务零碎来讲,都会有对立的存储拜访层,数据迁徙拆分运维操作只会影响到存储拜访层的配置而已,更下层的业务零碎基本不会感知到。

但,K8s 就是不一样的烟火!

K8s 集群是一个简单的零碎,是由很多扩大组件相互配合来提供多种多样的能力。

扩大组件是面向终态设计的。面向终态中次要有两个状态概念:冀望状态(Desired State) 和以后状态(Current State),集群中的所有的对象(object) 都有一个冀望状态和以后状态。

  • 冀望状态简略来说就是咱们向集群提交的 object 的 Yaml 数据所形容的终态;
  • 以后状态就是 object 在集群中实在存在的状态。

咱们应用的 create、update、patch、delete 等数据申请都是咱们针对终态做的批改动作,表白了咱们对终态的冀望,执行这些动作后,以后集群状态和咱们的冀望状态是有差别的,集群中的各个 Operators(Controllers)扩大组件通过两者的差别进行一直的调谐(Reconclie) , 驱动 object 从以后状态达到最终状态。

目前的 Operators 类组件基本上都是应用开源框架进行开发的,所以能够认为其运行组件的代码逻辑是统一对立的。在 Operator 组件外部,最终终态是通过向 kube-apiserver 发送 List 申请获取最终终态的 object yaml 数据,但为了升高 kube-apiserver 的负载压力,在组件启动时 List 申请只执行一次 (如果不呈现非预期谬误),若终态数据 object yaml 在之后有任何变动则是通过 kube-apiserver 被动向 Operator 推送 event(WatchEvent) 音讯。

从这点讲也能够说 K8s 集群是由 event 驱动的面向终态的设计。

而 Operator 和 kube-apiserver 之间的 WatchEvent 音讯流须要保障任何 event 都不能失落,最后的 List 申请返回的 yaml 数据,再加上 WatchEvent 的变更事件组合而成才是 Operator 应该看到的最终状态,也是用户的冀望状态。而保障事件不失落的重要概念则是 resourceVersion。

集群中的每个 object 都有该字段,即便是用户通过 CRD(CustomResourceDefinition) 定义的资源也是有的。

重点来了,下面提到的 resourceVersion 是与 etcd 存储自身独特个性 (revision) 非亲非故的,尤其是针对 Operator 大量应用的 List 申请更是如此。数据的拆分迁徙到新的 etcd 存储集群会间接影响到资源对象的 resourceVersion。

那么问题又来了,etcd revision 是什么?与 K8s 资源对象的 resourceVersion 又有什么关联呢?

Etcd 的 3 种 Revision

Etcd 中有三种 Revision,别离是 Revision、CreateRevision 和 ModRevision 上面将这三种 Revision 的关联关系以及特点总结如下:

key-value 写入或者更新时都会有 Revision 字段,并且保障严格递增, 实际上是 etcd 中 MVCC 的逻辑时钟。

K8s ResourceVersion 与 Etcd Revision

每个从 kube-apiserver 输入的 object 都必然有 resourceVersion 字段,可用于检测 object 是否变动及并发管制。

可从代码正文中看到更多信息:

// ObjectMeta is metadata that all persisted resources must have, which includes all objects
// users must create.
type ObjectMeta struct {  
    ...// omit code here
    // An opaque value that represents the internal version of this object that can
  // be used by clients to determine when objects have changed. May be used for optimistic
  // concurrency, change detection, and the watch operation on a resource or set of resources.
  // Clients must treat these values as opaque and passed unmodified back to the server.
  // They may only be valid for a particular resource or set of resources.
  //
  // Populated by the system.
  // Read-only.
  // Value must be treated as opaque by clients and .
  // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
  // +optional
  ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,6,opt,name=resourceVersion"`
    ...// omit code here
}

kube-apiserver 的申请 verbs 中 create、update、patch、delete 写操作都会更新 etcd 中的 Revision,更严格的说,会引发 revision 的增长。

现将 K8s 中的 resource object 中的 resourceVersion 字段与 etcd 中的各种 Revision 对应关系总结如下:

在所有的 kube-apiserver 申请响应中,须要特地留神 List 的响应。List 申请的 resourceVersion 是 etcd 的 Header.Revision, 该值正是 etcd 的 MVCC 逻辑时钟,对 etcd 任何 key 的写操作都是触发 Revision 的枯燥递增,接影响到 List 申请响应中的 resourceVersion 的值。

举例来说,即便是没有任何针对 test-namespace 上面的 Pod 资源的批改动作,如果 List test-namespace 上面的 Pod,响应中的 resourceVersion 也很可能每次都会增长(因为 etcd 中其余 key 有写操作)。

在咱们的不停组件 Pod 数据拆分中,咱们只禁止了 Pod 的写操作,其余数据并未禁止,在 kube-apiserver 配置更新滚动失效过程中,势必会造成 old etcd 的 Revision 要远大于存储 Pod 数据的 new etcd。这就造成了 List resourceVersion 拆分前后的重大不统一。

resourceVersion 的值在 Operator 中是保障 event 不丢的要害。所以说 etcd 的数据拆分不仅影响到了 kube-apiserver,同时也影响到了泛滥的 Operator 类组件, 一旦呈现变更事件失落,会造成 Pod 无奈交付、呈现脏乱数据等问题故障。

到当初为止,尽管咱们理解到 Operator 拿到的 list resourceVersion 拆分前后不统一,从 old etcd 中返回的 list resourceVersion 要比从 new etcd 要大,那么和 Operator 丢掉 Pod 更新事件有什么关系呢?

要答复这个问题,就须要从 K8s 的组件合作设计中的 ListAndWatch 说起,势必须要从客户端 Client-go 和服务端 kube-apiserver 来讲。

### Client-go 中 ListAndWatch

咱们都晓得 Operator 组件是通过开源 Client-go 代码包进行事件感知的。

Operator 中的 Client-go 感知数据对象事件示意图

其中外围要害就是 ListAndWatch 办法,保障 client 不失落 event 事件的 resourceVersion 就是在该办法中通过 List 申请获取的。

ListAndWatch 第一次会列出所有的对象,并获取资源对象的版本号,而后 watch 资源对象的版本号来查看是否有被变更。首先会将资源版本号设置为 0,list()可能会导致本地的缓存绝对于 etcd 外面的内容存在提早。Reflector 会通过 watch 的办法将提早的局部补充上,使得本地的缓存数据与 etcd 的数据保持一致。

要害代码如下:

// Run repeatedly uses the reflector's ListAndWatch to fetch all the
// objects and subsequent deltas.
// Run will exit when stopCh is closed.
func (r *Reflector) Run(stopCh <-chan struct{}) {klog.V(2).Infof("Starting reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
  wait.BackoffUntil(func() {if err := r.ListAndWatch(stopCh); err != nil {utilruntime.HandleError(err)
    }
  }, r.backoffManager, true, stopCh)
  klog.V(2).Infof("Stopping reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
}
// ListAndWatch first lists all items and get the resource version at the moment of call,
// and then use the resource version to watch.
// It returns error if ListAndWatch didn't even try to initialize watch.
func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error {
  var resourceVersion string
  // Explicitly set "0" as resource version - it's fine for the List()
  // to be served from cache and potentially be delayed relative to
  // etcd contents. Reflector framework will catch up via Watch() eventually.
  options := metav1.ListOptions{ResourceVersion: "0"}

  if err := func() error {
    var list runtime.Object
      ... // omit code here
    listMetaInterface, err := meta.ListAccessor(list)
      ... // omit code here
    resourceVersion = listMetaInterface.GetResourceVersion()
        ... // omit code here
    r.setLastSyncResourceVersion(resourceVersion)
    ... // omit code here
    return nil
  }(); err != nil {return err}
    ... // omit code here
  for {
        ... // omit code here
    options = metav1.ListOptions{
      ResourceVersion: resourceVersion,
      ... // omit code here
    }
    w, err := r.listerWatcher.Watch(options)
        ... // omit code here
    if err := r.watchHandler(w, &resourceVersion, resyncerrc, stopCh); err != nil {
        ... // omit code here
      return nil
    }
  }
}

整顿为流程图更为分明:

kube-apiserver 中的 Watch 解决

看完客户端的解决逻辑,再来看服务端的解决,要害在 kube-apiserver 对 watch 申请的解决, 对每一个 watch 申请,kube-apiserver 都会新建一个 watcher,启动一个 goroutine watchServer 专门针对该 watch 申请进行服务,在这个新建的 watchServer 中向 client 推送资源 event 音讯。

然而重点来了,client 的 watch 申请中参数 watchRV 是从 Client-go 中的 List 响应而来,kube-apiserver 只向 client 推送大于 watchRV 的 event 音讯,在拆分过程中 client 的 watchRV 有可能远大于 kube-apiserver 本地的 event 的 resourceVersion,这就是导致 client 失落 Pod 更新 event 音讯的根本原因。

从这一点来说,重启 Operator 组件是必须的,重启组件能够触发 Client-go 的 relist,拿到最新的 Pod list resourceVersion,从而不失落 Pod 的更新 event 音讯。

PART. 3 问题破局

破解重启问题

到了这里,咱们仿佛也难逃须要重启组件的命运,然而通过问题解析之后,咱们理清了问题起因,其实也就找到了解决问题的办法。

重启组件问题次要波及到两个主体:客户端 Client-go 和服务端 kube-apiserver,所以解决问题能够从这两个主体登程,寻求问题的突破点。

首先针对客户端 Client-go,要害就在于让 ListAndWatch 从新发动 List 申请拿到 kube-apiserver 的最新的 resourceVersion,从而不失落后续的 event 音讯。如果过可能让 Client-go 在某个特定的机会从新通过 List 申请刷新本地的 resourceVersion,也就解决了问题,然而如果通过更改 Client-go 代码,还是须要组件公布重启能力失效,那么问题就是如何不必批改 Client-go 的代码,就能够从新发动 List 申请。

咱们从新 review ListAndWatch 的逻辑流程,能够发现判断是否须要发动 List 申请,关键在于 Watch 办法的返回谬误的判断。而 watch 办法返回的谬误是依据 kube-apiserver 对 watch 申请的响应决定的,让咱们把眼光放到服务端 kube-apiserver。

不一样的 watch 申请解决

kube-apiserver 的 watch 申请解决前文曾经介绍过,咱们能够通过批改 kube-apiserver 的 watch 申请解决流程,实现与 Client-go 的相互配合,来达到咱们的目标。

由上文咱们晓得 Client-go 的 watchRV 要远大于 kube-apiserver 本地 watch cache 中的 resourceVersion, 能够依据这个特点来实现 kube-apiserver 发送指定谬误(TooLargeResourceVersionError),从而触发 Client-go 的 relist 动作。kube-apiserver 组件无可避免的须要重启,更新配置后能够执行咱们革新的逻辑。

革新逻辑示意如下:

技术保障数据统一

前人的教训是通过 etcd make-mirror 工具来实现数据迁徙的,长处是简略不便,开源工具开箱即用。毛病是该工作实现简略,就是从一个 etcd 中读取 key,而后从新写入另一个 etcd 中,不反对断点续传,对大数据量耗时长的迁徙不敌对。另外 etcd key 中的 createRevision 信息也被毁坏掉。因而在迁徙实现后,须要进行严格的数据完整性检测。

针对下面的问题咱们能够换一个思路,咱们实质是要做数据迁徙的,etcd 自身的存储构造 (KeyValue) 具备特殊性,咱们心愿保留数据前后的完整性。所以想到了 etcd 的 snapshot 工具,snapshot 工具原本是用于 etcd 的容灾复原的,即能够应用一个 etcd 的 snapshot 数据从新创立出新的 etcd 实例。而且通过 snapshot 的数据在新的 etcd 中是可能放弃原有的 keyValue 的完整性的,而这正是咱们所要的。

// etcd KeyValue 数据结构
type KeyValue struct {
  // key is the key in bytes. An empty key is not allowed.
  Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
  // create_revision is the revision of last creation on this key.
  CreateRevision int64 `protobuf:"varint,2,opt,name=create_revision,json=createRevision,proto3" json:"create_revision,omitempty"`
  // mod_revision is the revision of last modification on this key.
  ModRevision int64 `protobuf:"varint,3,opt,name=mod_revision,json=modRevision,proto3" json:"mod_revision,omitempty"`
  // version is the version of the key. A deletion resets
  // the version to zero and any modification of the key
  // increases its version.
  Version int64 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"`
  // value is the value held by the key, in bytes.
  Value []byte `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
  // lease is the ID of the lease that attached to key.
  // When the attached lease expires, the key will be deleted.
  // If lease is 0, then no lease is attached to the key.
  Lease int64 `protobuf:"varint,6,opt,name=lease,proto3" json:"lease,omitempty"`
}

迁徙数据裁剪

etcd snapshot 数据尽管有咱们想要的放弃 KeyValue 的完整性,然而重建的 etcd 中存储的数据是老 etcd 的全副数据,这个并不是咱们想要的。咱们当然能够在新建 etcd 后,再来发动冗余数据的分明工作,但这并不是最好的办法。

咱们能够通过革新 etcd snapshot 工具在 snapshot 的过程中实现咱们的数据裁剪。etcd 的存储模型中,是有一个 buckets 的列表的,buckets 是 etcd 一个存储概念,对应到关系数据库中能够认为是一个 table,其中的每个 key 就对应的 table 中的一行。其中最重要的 bucket 是名称为 key 的 bucket,该 bucket 存储了 K8s 中所有资源对象。而 K8s 的所有资源对象的 key 都是有固定格局的,依照 resource 类别和 namespace 区别,每种 resource 都是有固定的前缀。比方 Pod 数据的前缀就是 /registry/Pods/。咱们在 snapshot 过程中能够依据这个前缀辨别出 Pod 数据,把非 Pod 数据裁减掉。

另外依据 etcd 的个性,etcd 做 snapshot 数据的存储大小是 etcd 的硬盘文件大小,其中有两个值 db total size 和 db inuse size, db total size 大小是 etcd 在硬盘中的所占用的存储文件的大小,其中蕴含了很多曾经成为垃圾 key,但未清理的数据。db inuse size 大小是所有可用的数据的总大小。在不常常应用 etcd defrag 办法整顿存储空间时,total 的值一般来讲要远大于 inuse 的值。

在数据裁剪中即便咱们裁剪掉非 Pod 数据,整个 snapshot 的数据也不会有任何扭转,这时候咱们须要通过 defrag 办法来开释掉冗余存储空间。

在上面的示意图中,能够看到 db total 的变动过程,最终咱们失去的 snapshot 数据大小就是 Pod 数据的大小,这对咱们节约数据传输工夫来讲是十分重要的。

Pod 禁写的小坑

在后面的拆分流程中,咱们提到 K8s 禁止写一类资源的时候,能够通过 MutatingWebhook 来实现,就是间接返回 deny 后果即可,比较简单。这里记录一下咱们过后遇到的一个小坑点。

咱们最后的 MutatingWebhookConfiguration 配置如下,然而咱们发现 apply 这个配置后,还是可能收到 Pod 的更新 event 音讯。

// 第一个版本配置,有问题
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: deny-pods-write
webhooks:
- admissionReviewVersions:
  - v1beta1
  clientConfig:
    url: https://extensions.xxx/always-deny
  failurePolicy: Fail
  name: always-deny.extensions.k8s
  namespaceSelector: {}
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - "*"
    resources:
    - pods
    scope: '*'  
  sideEffects: NoneOnDryRun

通过排查后发现是 Pod 的 status 字段被更新,通过浏览 apiserver 的代码,咱们发现与 Pod 存储无关的 resource 不仅仅只有 Pod 一个,还有上面的类型,Pod status 与 Pod 对于 apiserver 的存储来讲是不同的资源。

"pods":             podStorage.Pod,
"pods/attach":      podStorage.Attach,
"pods/status":      podStorage.Status,
"pods/log":         podStorage.Log,
"pods/exec":        podStorage.Exec,
"pods/portforward": podStorage.PortForward,
"pods/proxy":       podStorage.Proxy,
"pods/binding":     podStorage.Binding,

通过调整后,上面的配置是可能禁止 Pod 数据齐全更新的配置,留神其中的 resource 配置字段。

这是一个小坑点,记录在此。

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: deny-pods-write
webhooks:
- admissionReviewVersions:
  - v1beta1
  clientConfig:
    url: https://extensions.xxx/always-deny
  failurePolicy: Fail
  name: always-deny.extensions.k8s
  namespaceSelector: {}
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - "*"
    resources:
    - pods
    - pods/status
    - pods/binding
    scope: '*'  
  sideEffects: NoneOnDryRun

最初的拆分流程

在解决了后面的问题后,咱们最初的拆分流程也就进去了。

示意如下:

在数据拆分期间,仅有 Pod 数据不能够有写操作,读是能够的,其余资源能够失常读写。整个流程能够通过程序自动化的来实现。

Pod 的禁写操作的工夫依据 Pod 数据的大小而所有变动,次要耗费在 Pod 数据 copy 过程上,根本整个过程在几分钟内即可实现。

除了 kube-apiserver 无奈防止须要更新存储配置重启外,不须要任何组件重启。同时也节俭了大量的与组件 owner 沟通工夫,也防止了泛滥操作过程中的泛滥不确定性。

整个拆分过程一个人齐全能够胜任。

PART. 4 最初的总结

本文从数据拆分的指标登程,借鉴了前人教训,但依据本身的理论状况和要求,冲破了之前的教训窠臼,通过技术创新解决了组件重启和数据一致性保障问题,在晋升效率的同时也在技术上保障了安全性。

现过程抽丝剥茧介绍了整个思考过程和实现关键点。

整个思考过程和实现关键点

咱们并没有发明创造了什么,只是在现有逻辑和工具根底上,稍加改进从而来实现咱们的指标。然而革新和改进过程的背地是须要咱们理解底层的细枝末节,这并不是画几个框框就能理解到的。

知其然知其所以然在大部分工作中都是必须的,尽管这会占用咱们很多工夫,但这些工夫是值得的。

最初借用一句古话来完结:

* 运用之妙,存乎一心
与诸君共勉。*

「参考资料」

(1)【etcd storage limit】:

https://etcd.io/docs/v3.3/dev…

(2)【etcd snapshot】:

https://etcd.io/docs/v3.3/op-…

(3)【攀登规模化的顶峰 – 蚂蚁团体大规模 Sigma 集群 ApiServer 优化实际】:

https://www.sofastack.tech/bl…

正文完
 0