探索Kubernetes在ETCD中保存数据的规范

写在前面

Kubernetes (简称k8s)是容器集群管理系统,是一个开源的平台,可以实现容器集群的自动化部署、自动扩缩容、维护等功能,具体的大家可以参考 Kubernetes中文文档 或者Kubernetes官网的文档

K8s的架构复杂,涉及到概念非常多,其基础组件包含 ETCD、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy等,其运行时环境为docker或Rkt,当然还包含很多插件。在我看来,k8s是DevOps的未来,因此不禁想写一些它的故事。

ETCD在k8s技术栈的地位,就仿佛数据库(Mysql、Postgresql或oracle等)在Web应用中的地位,它存储了k8s集群中所有的元数据(以key-value的方式)。那么很现实的一个问题是:这些元数据是如何组织并保存的呢?本文就对此问题探究一番。

适用人群

入门——初级——中级√——高级;本文适应中级及以上。

探究正文

相关环境

我这里采用了kubeadm的部署方法,探索了高可用(HA)部署方案(推荐参考一下这里 ,未来会考虑写一篇关于k8s高可用原理的博文,敬请期待😝)。

k8s中ETCD数据的增删改查

首先应该明确,K8s中所有元数据的增删改查都是由kube-apiserver来执行的,那么这些数据在ETCD中必然有一套存储规范,这样才能保证在集群中部署成千上万的应用时不会出差错。在此基础上可以认为,只要掌握了k8s在ETCD中存储数据的规范,便可以像k8s一样手动来操作ETCD了(虽然不建议这么做)。不过更大的价值是能对k8s的理解更进一步,便于以后debug或者二次开发k8s的某些功能时更有底气。

初探ETCD中的数据

本文对ETCD的操作主要使用了其官方客户端工具etcdctl,这里不对etcdctl进行详解了(需要用一整篇博客来介绍它才行),只就用到的一些命令进行阐释。

获取ETCD中的所有的key值

下面的命令可以获取ETCD中的所有的key值:

# 获取ETCD中的所有数据
# --prefix 表示获取所有key值头为某个字符串的数据, 由于传入的是"",所以会匹配所有的值
# --keys-only 表示只返回key而不返回value
# 对输出的结果使用grep过滤掉空行
/$ ETCDCTL_API=3 etcdctl get "" --prefix --keys-only |grep -Ev "^$"

# 输出结果如下所示,实际数据会非常整齐
/registry/apiextensions.k8s.io/customresourcedefinitions/globalbgpconfigs.crd.projectcalico.org
/registry/apiextensions.k8s.io/customresourcedefinitions/globalfelixconfigs.crd.projectcalico.org
/registry/apiextensions.k8s.io/customresourcedefinitions/globalnetworkpolicies.crd.projectcalico.org
# ... 略过很多条目
/registry/namespaces/default
/registry/namespaces/kube-public
/registry/namespaces/kube-system
/registry/pods/kube-system/canal-mljsv
/registry/pods/kube-system/canal-qlvh6
# ... 略过很多条目
/registry/services/endpoints/kube-system/kube-scheduler
/registry/services/specs/default/kubernetes
/registry/services/specs/kube-system/kube-dns
compact_rev_key

# 总共有240条记录
/$ etcdctl get "" --prefix --keys-only |grep -Ev "^$" |wc -l
240

ETCD中key值的规律

通过观察可以简单得出下面几个规律:

有了上面的规律,可以初步得出一个结论:在研究k8s时重点关注/registry/前缀的key及其value即可。

ETCD中的value值

通过上面的内容知道,k8s在ETCD中保存数据时key的取值非常讲究,规律非常容易概括出来。那么这些key值所对应的值是什么样子呢?我试着输出了/registry/ranges/serviceips/registry/services/endpoints/default/kubernetes这两个key所对应的值,效果见下面编码展示区。

# 获取"/registry/ranges/serviceips"所对应的值
# 发现这里有很多奇怪的字符=。=
# 可以大体推断出来,集群所有service的ip范围为10.96.0.0/12, 与api-server的yaml文件中配置的一致
/$ etcdctl get /registry/ranges/serviceips
/registry/ranges/serviceips
k8s

v1RangeAllocation&

"*28Bz
      10.96.0.0/12"

# 获取"/registry/services/endpoints/default/kubernetes"所对应的值
# 发现这里有很多奇怪的字符=。=
# 在default命名空间的kubernetes这个service所对应的endpoint有两个ip
# 分别为192.168.205.137和192.168.205.139
/$ etcdctl get /registry/services/endpoints/default/kubernetes
/registry/services/endpoints/default/kubernetes
k8s

v1	Endpoints�
O

kubernetesdefault"*$0b6bb724-f066-11e8-be14-000c29d2cb3a2ں��z;

192.168.205.137

192.168.205.139
https�2TCP"

可以很明显看出来,ETCD中保存的并不是输出友好的数据(比如json或xml等就是输出友好型数据)。当然,如果进一步研究可以知道,ETCD保存的是Protocol Buffers序列化后的值。如果大家对Protobuf有研究,可以知道这个协议也是个key-value的协议,只不过会把其key-value值按照特定的算法进行压缩,不过并没有压缩的很厉害,显式输出这些值多少也能获取到一些信息;比如/registry/services/endpoints/default/kubernetes对应的192.168.205.137、192.168.205.139等值。

ETCD中其他的key及其value

通过上面的探索,对ETCD中存储的数据有了大体的了解,接下来就可以开始更加刺激的冒险了。

据说flannel需要使用ETCD保存网络元数据

那么,flannel在ETCD中保存的数据是什么,保存在哪个key中了呢?下面把所有网关相关的几个关键词 canal|flannel|calico输出可以知道,里面只有一个可能包含flannel所需数据的key,即/registry/configmaps/kube-system/canal-config,输出内容后对比关于flannel的etcd配置这篇文章,很大程度可以认为就是它了(需要进一步去canal项目的源码中去确认)。

/$ etcdctl get "" --prefix --keys-only |grep -Ev "^$" |grep "canal\|flannel\|calico"
# ... 忽略很多条
/registry/configmaps/kube-system/canal-config
# ... 忽略很多条

# 可以看到里面有一个配置项 net-conf, 对比flannel的配置,可以认为这个地方很可能就是canal项目中flannel在etcd中需要的值。这里设置了网段为"10.244.0.0/16"
/$ etcdctl get /registry/configmaps/kube-system/canal-config
# ... 省略很多
net-conf.jsonI{
  "Network": "10.244.0.0/16",
  "Backend": {
    "Type": "vxlan"
  }
}

default命名空间中endpoint实例kubernetes的值

# 在k8s的HA部署时,会发现default命名空间有一个默认的service
# 明明没有selector(如果不熟悉这个概念可以自行去搜索了解一下),却有实实在在的Endpoints值
# 那么这个值是如何写入的呢?
/$ kubectl describe svc kubernetes
Name:              kubernetes
Namespace:         default
Labels:            component=apiserver
                   provider=kubernetes
Annotations:       <none>
Selector:          <none>
Type:              ClusterIP
IP:                10.96.0.1
Port:              https  443/TCP
TargetPort:        6443/TCP
Endpoints:         192.168.205.137:6443,192.168.205.139:6443
Session Affinity:  ClientIP
Events:            <none>

# 通过在ETCD中检索可以获取到两个key值
# 一个对应的是default命名空间中的service,另一个是对应的endpoint
/$ etcdctl get "" --prefix --keys-only |grep -Ev "^$" |grep "default" |grep "kubernetes"
/registry/services/endpoints/default/kubernetes
/registry/services/specs/default/kubernetes

我把/registry/services/endpoints/default/kubernetes输入到搜索引擎搜索了一下,发现有人在github上抛出类似的问题,从其The three apiservers (Ip Adresses .31,.32,.33) are constantly overwriting the etcd-key /registry/services/endpoints/default/kubernetes可以推测出来,这个值是api-server这个控件主动去写入的。

这能得出一个结论:在部署高可用集群时,如果想把多个api-server注册到集群,那么所有的api-server的服务都将会出现在default命名空间的kubernetes这个endpoints上;这也就意味着难以把集群中的一个api-server单独隔离出来而不让它对外提供服务(我当前想debug的一个问题需要这么操作,得出这个结论表示很无奈)。

小结

文本对k8s数据仓库ETCD进行了探究,总结了ETCD保存k8s数据时key值的规范,并尝试查看了value值的内容。最后对几个感兴趣的key值及其value值进行了探索。通过探究可以知道,k8s把集群的信息非常有条理地保存在ETCD中,key值定义有章可循,比较方便debug;同时,虽然ETCD中的value值是protobuf序列化后的数据,不适合展示,不过输出到文本后依然有一定的参考价值。

参考