【K8S】etcd-operator 解析與實戰(zhàn)

簡介

通過將 etcd 集群定義為一個 K8S CRD,etcd-operator 負責 etcd 集群的創(chuàng)建與運維。
它主要包含三個controller:
cluster-operator: 負責自動化創(chuàng)建,銷毀,升級,自動擴縮容,故障遷移etcd集群
backup-operator: 負責對etcd的數(shù)據(jù)進行定時備份,備份后端支持遠程存儲,如aliyun oss存儲
restore-operator: 負責通過備份數(shù)據(jù)恢復etcd集群
operator 流程分析
創(chuàng)建 EtcdCluster CRD;
創(chuàng)建 EtcdCluster 的 Informer 來處理 EtcdCluster 增刪改事件;
當用戶提交 EtcdCluster 創(chuàng)建請求;創(chuàng)建 Etcd 集群初始節(jié)點;根據(jù)期望的 Etcd 集群 size 創(chuàng)建并加入成員節(jié)點;
當用戶提交 EtcdCluster 更新(?鏡像版本 / 集群 pod size 等更新)請求:調(diào)整集群到期望狀態(tài);
當用戶提交 EtcdCluster 刪除請求:無需操作,由垃圾回收自動刪除節(jié)點;
Etcd 集群數(shù)據(jù)的備份及恢復分別由 EtcdBackup 和 EtcdRestore 的 Operator 實現(xiàn);
源碼結(jié)構(gòu)
├── cmd # 程序入口
│?? ├── backup-operator # 備份集群用的 Operator
│?? ├── operator # 集群 Operator
│?? └── restore-operator # 恢復集群用的 Operator
├── example # 一些示例文件
│?? ├── deployment.yaml # 部署 Operator
│?? ├── etcd-backup-operator # 備份用 Operator 相關
│?? ├── etcd-restore-operator # 恢復用 Operator 相關
│?? ├── example-etcd-cluster-nodeport-service.json
│?? ├── example-etcd-cluster.yaml # 部署 Etcd 集群
│?? ├── rbac # 用于創(chuàng)建 RBAC 規(guī)則
│?? └── tls # 部署 TLS 連接版的 Etcd 集群
├── hack # 提供一些開發(fā)相關的腳本
├── pkg # 主要源碼
│?? ├── apis # EtcdCluster API 組定義
│?? ├── backup # 操作備份恢復源相關的實現(xiàn)
│?? ├── chaos # 集群容災測試相關
│?? ├── cluster # 集群控制實現(xiàn)
│?? │?? ├── cluster.go # Etcd 集群的實際維護
│?? │?? ├── metrics.go # 監(jiān)控相關
│?? │?? ├── reconcile.go # 節(jié)點的創(chuàng)建或刪除
│?? │?? └── upgrade.go # 節(jié)點的升級
│?? ├── controller # EtcdCluster Controller 實現(xiàn)
│?? │?? ├── backup-operator # 備份集群用的 Operator 實現(xiàn)
│?? │?? ├── restore-operator # 恢復集群用的 Operator 實現(xiàn)
│?? │?? ├── controller.go # 事件處理 Handler
│?? │?? ├── informer.go # 創(chuàng)建 Informer 監(jiān)聽 EtcdCluster 增刪改事件
│?? │?? ├── metrics.go # Prometheus 數(shù)據(jù)統(tǒng)計
│?? ├── generated # K8S 工具生成的代碼
│?? └── util # 一些工具,操作 Pod 對象,Etcd API 調(diào)用等
Controller 初始化
創(chuàng)建 EtcdCluster Controller:
func run(ctx context.Context) {
??//?用于測試?Etcd?集群容災狀況,僅用于測試環(huán)境
?startChaos(context.Background(), cfg.KubeCli, cfg.Namespace, chaosLevel)
?// 創(chuàng)建 Controller 并開始控制循環(huán)
?c := controller.New(cfg)
?err := c.Start()
?logrus.Fatalf("controller Start() failed: %v", err)
}
創(chuàng)建 EtcdCluster CRD:
func (c *Controller) Start() error {
?...
?// 等待 EtcdCluster CRD 創(chuàng)建完成
?for {
? ?err := c.initResource()
? ?if err == nil {
? ? ?break
? ?}
? ?...
? ?time.Sleep(initRetryWaitTime)
?}
?...
?c.run()
?...
}
創(chuàng)建 Informer 處理 EctdCluster 事件:
func (c *Controller) run() {
?...
?// EctdCluster 對象的增刪改都會調(diào)用 handleClusterEvent 來處理
?_, informer := cache.NewIndexerInformer(source, &api.EtcdCluster{}, 0, cache.ResourceEventHandlerFuncs{
? ?AddFunc: ? ?c.onAddEtcdClus,
? ?UpdateFunc: c.onUpdateEtcdClus,
? ?DeleteFunc: c.onDeleteEtcdClus,
?}, cache.Indexers{})
?...
?// TODO:以后可以使用 Queue 來避免阻塞
?informer.Run(ctx.Done())
}
處理事件類型循環(huán):
func (c *Controller) handleClusterEvent(event *Event) (bool, error) {
?...
?// 集群失效后,從維護的集群集合中刪除
?if clus.Status.IsFailed() {
? ?...
? ?if event.Type == kwatch.Deleted {
? ? ?delete(c.clusters, getNamespacedName(clus))
? ? ?return false, nil
? ?}
? ?...
?}
??...
?switch event.Type {
?case kwatch.Added:
? ?...
? ?// 創(chuàng)建 Etcd 集群
? ?nc := cluster.New(c.makeClusterConfig(), clus)
? ?...
? ?c.clusters[getNamespacedName(clus)] = nc
? ?...
?case kwatch.Modified:
? ?...
? ?// 更新 Etcd 集群
? ?c.clusters[getNamespacedName(clus)].Update(clus)
? ?...
?case kwatch.Deleted:
? ?...
? ?// 刪除 Etcd 集群
? ?c.clusters[getNamespacedName(clus)].Delete()
? ?delete(c.clusters, getNamespacedName(clus))
? ?...
?}
?return false, nil
}
Controller 控制循環(huán)
收到 EtcdCluster 創(chuàng)建事件,則創(chuàng)建一個新的集群
func New(config Config, cl *api.EtcdCluster) *Cluster {
?c := &Cluster{
? ?...
?}
?go func() {
? ?// 初始化集群
? ?if err := c.setup(); err != nil {
? ? ?...
? ?}
? ?// 開始集群節(jié)點控制循環(huán)
? ?c.run()
?}()
?return c
}
為集群創(chuàng)建初始節(jié)點
func (c *Cluster) startSeedMember() error {
?m := &etcdutil.Member{
? ?Name: ? ? ? ? k8sutil.UniqueMemberName(c.cluster.Name),
? ?Namespace: ? ?c.cluster.Namespace,
? ?SecurePeer: ? c.isSecurePeer(),
? ?SecureClient: c.isSecureClient(),
?}
?ms := etcdutil.NewMemberSet(m)
?if err := c.createPod(ms, m, "new"); err != nil {
?...
?_, err := c.eventsCli.Create(k8sutil.NewMemberAddEvent(m.Name, c.cluster))
?...
}
為集群創(chuàng)建兩個 Headless Service
CreateClientService:用于 Etcd 客戶端的訪問;
CreatePeerService:用于節(jié)點間訪問;
func (c *Cluster) setupServices() error {
?err := k8sutil.CreateClientService(c.config.KubeCli, c.cluster.Name, c.cluster.Namespace, c.cluster.AsOwner())
?...
?return k8sutil.CreatePeerService(c.config.KubeCli, c.cluster.Name, c.cluster.Namespace, c.cluster.AsOwner())
}
在控制循環(huán)中創(chuàng)建或刪除成員節(jié)點以達到集群期望狀態(tài)
每隔 reconcileInterval??秒調(diào)整一次集群狀態(tài) reconcile():
將集群節(jié)點數(shù)調(diào)整到期望的 size;reconcileMembers()
刪除所有不屬于集群中的成員節(jié)點;
創(chuàng)建集群中缺失的成員節(jié)點;
如果集群未達到法定節(jié)點數(shù),則退出并報錯;
將集群節(jié)點調(diào)整到期望的 Etcd 版本;upgradeOneMember()
修改 Pod 對象的 Image 后請求 patch;
Etcd 節(jié)點的啟動方式
在啟動節(jié)點 Pod 時,其中的 init container 會等到 Pod DNS 解析可用后才會啟動 Etcd 容器;
從 MemberSet 中找到其他成員的 DNS 名稱并配置 --initial-cluster 參數(shù)后啟動 Etcd 容器;
Etcd Operator 部署 Etcd 集群,采用的是靜態(tài)集群(Static)的方式。

可以看到,在 etcd 集群啟動參數(shù)(比如:initial-cluster)里,Etcd Operator 只會使用 Pod 的 DNS 記錄,而不是它的 IP 地址。
這當然是因為,在 Operator 生成上述啟動命令的時候,Etcd 的 Pod 還沒有被創(chuàng)建出來,它的 IP 地址自然也無從談起。
每個 Cluster 對象,都會事先創(chuàng)建一個與該 EtcdCluster 同名的 Headless Service。這樣,Etcd Operator 在接下來的所有創(chuàng)建 Pod 的步驟里,就都可以使用 Pod 的 DNS 記錄來代替它的 IP 地址了。
func newEtcdPod(m *etcdutil.Member, initialCluster []string, clusterName, state, token string, cs api.ClusterSpec) *v1.Pod {
?...
?pod := &v1.Pod{
? ?...
? ?Spec: v1.PodSpec{
? ? ?InitContainers: []v1.Container{{
? ? ? ?...
? ? ? ?Command: []string{"/bin/sh", "-c", fmt.Sprintf(`
? ? ? ? ?TIMEOUT_READY=%d
? ? ? ? ?while ( ! nslookup %s )
? ? ? ? ?do
? ? ? ? ? ?# If TIMEOUT_READY is 0 we should never time out and exit
? ? ? ? ? ?TIMEOUT_READY=$(( TIMEOUT_READY-1 ))
? ? ? ? ? ? ? ? ? ? ? ?if [ $TIMEOUT_READY -eq 0 ];
? ? ? ? ? ? ? ?then
? ? ? ? ? ? ? ? ? ?echo "Timed out waiting for DNS entry"
? ? ? ? ? ? ? ? ? ?exit 1
? ? ? ? ? ? ? ?fi
? ? ? ? ? ?sleep 1
? ? ? ? ?done`, DNSTimeout, m.Addr())},
? ? ?}},
? ? ?Containers: ? ?[]v1.Container{container},
? ? ?...
? ?},
?}
?...
}
ACK 集群部署實戰(zhàn)
選用 ACK 版本:v1.20.11-aliyun.1
安裝etcd operator
CoreDNS 無法解析 etcd pod 的 IP,部署在 init container 等待 Pod DNS ?解析 hang ?住了。

附錄:使用 Kubebuilder demo
kubebuilder 是生成 k8s operator 的腳手架工具,本 demo 介紹使用 kubebuilder 快速搭建一個 operator 項目。
初始化項目:
kubebuilder init --domain my.domain --repo my.domain/guestbook
kubebuilder create api --group webapp --version v1 --kind Guestbook
? ?guestbook tree
.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── api
│?? └── v1
│?? ? ? ├── groupversion_info.go
│?? ? ? ├── guestbook_types.go
│?? ? ? └── zz_generated.deepcopy.go
├── bin
│?? └── controller-gen
├── config
│?? ├── crd
│?? │?? ├── kustomization.yaml
│?? │?? ├── kustomizeconfig.yaml
│?? │?? └── patches
│?? │?? ? ? ├── cainjection_in_guestbooks.yaml
│?? │?? ? ? └── webhook_in_guestbooks.yaml
│?? ├── default
│?? │?? ├── kustomization.yaml
│?? │?? ├── manager_auth_proxy_patch.yaml
│?? │?? └── manager_config_patch.yaml
│?? ├── manager
│?? │?? ├── controller_manager_config.yaml
│?? │?? ├── kustomization.yaml
│?? │?? └── manager.yaml
│?? ├── prometheus
│?? │?? ├── kustomization.yaml
│?? │?? └── monitor.yaml
│?? ├── rbac
│?? │?? ├── auth_proxy_client_clusterrole.yaml
│?? │?? ├── auth_proxy_role.yaml
│?? │?? ├── auth_proxy_role_binding.yaml
│?? │?? ├── auth_proxy_service.yaml
│?? │?? ├── guestbook_editor_role.yaml
│?? │?? ├── guestbook_viewer_role.yaml
│?? │?? ├── kustomization.yaml
│?? │?? ├── leader_election_role.yaml
│?? │?? ├── leader_election_role_binding.yaml
│?? │?? ├── role_binding.yaml
│?? │?? └── service_account.yaml
│?? └── samples
│?? ? ? └── webapp_v1_guestbook.yaml
├── controllers
│?? ├── guestbook_controller.go
│?? └── suite_test.go
├── go.mod
├── go.sum
├── hack
│?? └── boilerplate.go.txt
└── main.go
13 directories, 38 files
部署 CRD
? ?guestbook make install
? ?guestbook kubectl get crd |grep my.domain
guestbooks.webapp.my.domain ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?2022-05-04T08:58:36Z

可以看到 CRD 已經(jīng)創(chuàng)建成功,但是 CR ?實例還沒創(chuàng)建。
創(chuàng)建 CR 實例:
? ?guestbook kubectl apply -f config/samples/
guestbook.webapp.my.domain/guestbook-sample created

4.本地運行 operator :
? ?guestbook make run
...
go fmt ./...
go vet ./...
go run ./main.go
I0504 16:59:38.598884 ? 16635 request.go:665] Waited for 1.020466713s due to client-side throttling, not priority and fairness, request: GET:https://59.110.25.213:6443/apis/metrics.alibabacloud.com/v1alpha1?timeout=32s
1.6516547788561852e+09 ?INFO ?controller-runtime.metrics ?Metrics server is starting to listen ?{"addr": ":8090"}
1.6516547788572779e+09 ?INFO ?setup ?starting manager
1.651654778857989e+09 ?INFO ?Starting server ?{"path": "/metrics", "kind": "metrics", "addr": "[::]:8090"}
1.6516547788579981e+09 ?INFO ?Starting server ?{"kind": "health probe", "addr": "[::]:8089"}
1.651654778858467e+09 ?INFO ?controller.guestbook ?Starting EventSource ?{"reconciler group": "webapp.my.domain", "reconciler kind": "Guestbook", "source": "kind source: *v1.Guestbook"}
1.651654778858505e+09 ?INFO ?controller.guestbook ?Starting Controller ?{"reconciler group": "webapp.my.domain", "reconciler kind": "Guestbook"}
1.6516547799614458e+09 ?INFO ?controller.guestbook ?Starting workers ?{"reconciler group": "webapp.my.domain", "reconciler kind": "Guestbook", "worker count": 1}
后續(xù)可以繼續(xù)將 operator 部署到 k8s 上