MySQL集群的流程遷移到 Kubernetes
- Master 節(jié)點和 Slave 節(jié)點需要有不同的配置文件(即:不同的 my.cnf);
- Master 節(jié)點和 Salve 節(jié)點需要能夠傳輸備份信息文件;
- 在 Slave 節(jié)點第一次啟動之前,需要執(zhí)行一些初始化 SQL 操作;
而由于 MySQL 本身同時擁有拓撲狀態(tài)(主從節(jié)點的區(qū)別)和存儲狀態(tài)(MySQL 保存在本地的數(shù)據(jù)),我們自然要通過 StatefulSet 來解決這“三座大山”的問題。
其中,“第一座大山:Master 節(jié)點和 Slave 節(jié)點需要有不同的配置文件”,很容易處理:我們只需要給主從節(jié)點分別準備兩份不同的 MySQL 配置文件,然后根據(jù) Pod 的序號(Index)掛載進去即可。
- 第一個名叫“mysql”的 Service 是一個 Headless Service是通過為 Pod 分配 DNS 記錄來固定它的拓撲狀態(tài),比如“mysql-0.mysql”和“mysql-1.mysql”這樣的 DNS 名字。其中,編號為 0 的節(jié)點就是我們的主節(jié)點。所有用戶的寫請求,則必須直接以 DNS 記錄的方式訪問到 MySQL 的主節(jié)點,也就是:“mysql-0.mysql“這條 DNS 記錄。
- 第二個名叫“mysql-read”的 Service,則是一個常規(guī)的 Service。所有用戶的讀請求,都必須訪問第二個 Service 被自動分配的 DNS 記錄,即:“mysql-read”(當然,也可以訪問這個 Service 的 VIP)。這樣,讀請求就可以被轉(zhuǎn)發(fā)到任意一個 MySQL 的主節(jié)點或者從節(jié)點上
第二座大山:Master 節(jié)點和 Salve 節(jié)點需要能夠傳輸備份文件
首先,我們先為 StatefulSet 對象規(guī)劃一個大致的框架,如下圖所示:

第一步:從 ConfigMap 中,獲取 MySQL 的 Pod 對應(yīng)的配置文件。
第二步:在 Slave Pod 啟動前,從 Master 或者其他 Slave Pod 里拷貝數(shù)據(jù)庫數(shù)據(jù)到自己的目錄下。
第三座大山:如何在 Slave 節(jié)點的 MySQL 容器第一次啟動之前,執(zhí)行初始化 SQL。
我們可以為這個 MySQL 容器額外定義一個 sidecar 容器,來完成這個操作。在這個名叫 xtrabackup 的 sidecar 容器的啟動命令里,其實實現(xiàn)了兩部分工作。
第一部分工作,當然是 MySQL 節(jié)點的初始化工作。這個初始化需要使用的 SQL,是 sidecar 容器拼裝出來、保存在一個名為 change_master_to.sql.in 的文件里的
在完成 MySQL 節(jié)點的初始化后,這個 sidecar 容器的第二個工作,則是啟動一個數(shù)據(jù)傳輸服務(wù)。
DaemonSet
daemon pod:
- 這個 Pod 運行在 Kubernetes 集群里的每一個節(jié)點(Node)上;
- 每個節(jié)點上只有一個這樣的 Pod 實例;
- 當有新的節(jié)點加入 Kubernetes 集群后,該 Pod 會自動地在新節(jié)點上被創(chuàng)建出來;而當舊節(jié)點被刪除后,它上面的 Pod 也相應(yīng)地會被回收掉。
意義:
- 各種網(wǎng)絡(luò)插件的 Agent 組件,都必須運行在每一個節(jié)點上,用來處理這個節(jié)點上的容器網(wǎng)絡(luò);
- 各種存儲插件的 Agent 組件,也必須運行在每一個節(jié)點上,用來在這個節(jié)點上掛載遠程存儲目錄,操作容器的 Volume 目錄;
- 各種監(jiān)控組件和日志組件,也必須運行在每一個節(jié)點上,負責這個節(jié)點上的監(jiān)控信息和日志搜集。
DaemonSet 又是如何保證每個 Node 上有且只有一個被管理的 Pod 呢?
顯然,這是一個典型的“控制器模型”能夠處理的問題。
? DaemonSet Controller,首先從 Etcd 里獲取所有的 Node 列表,然后遍歷所有的 Node。這時,它就可以很容易地去檢查,當前這個 Node 上是不是有一個攜帶了 name=fluentd-elasticsearch 標簽的 Pod 在運行。而檢查的結(jié)果,可能有這么三種情況:
沒有這種 Pod,那么就意味著要在這個 Node 上創(chuàng)建這樣一個 Pod;
有這種 Pod,但是數(shù)量大于 1,那就說明要把多余的 Pod 從這個 Node 上刪除掉;
正好只有一個這種 Pod,那說明這個節(jié)點是正常的。
? 其中,刪除節(jié)點(Node)上多余的 Pod 非常簡單,直接調(diào)用 Kubernetes API 就可以了。但是,如何在指定的 Node 上創(chuàng)建新 Pod 呢?如果你已經(jīng)熟悉了 Pod API 對象的話,那一定可以立刻說出答案:用 nodeSelector,選擇 Node 的名字即可。
? 我們的 DaemonSet Controller 會在創(chuàng)建 Pod 的時候,自動在這個 Pod 的 API 對象里,加上這樣一個 nodeAffinity 定義。DaemonSet會給這個 Pod 自動加上另外一個與調(diào)度相關(guān)的字段,叫作 tolerations。這個字段意味著這個 Pod,會“容忍”(Toleration)某些 Node 的“污點”(Taint)。
? 這個 Toleration 的含義是:“容忍”所有被標記為 unschedulable“污點”的 Node;“容忍”的效果是允許調(diào)度。
在正常情況下,被標記了 unschedulable“污點”的 Node,是不會有任何 Pod 被調(diào)度上去的(effect: NoSchedule)??墒?,DaemonSet 自動地給被管理的 Pod 加上了這個特殊的 Toleration,就使得這些 Pod 可以忽略這個限制,繼而保證每個節(jié)點上都會被調(diào)度一個 Pod。當然,如果這個節(jié)點有故障的話,這個 Pod 可能會啟動失敗,而 DaemonSet 則會始終嘗試下去,直到 Pod 啟動成功。
這時,你應(yīng)該可以猜到,我在前面介紹到的DaemonSet 的“過人之處”,其實就是依靠 Toleration 實現(xiàn)的。
而通過這樣一個 Toleration,調(diào)度器在調(diào)度這個 Pod 的時候,就會忽略當前節(jié)點上的“污點”,從而成功地將網(wǎng)絡(luò)插件的 Agent 組件調(diào)度到這臺機器上啟動起來。
? DaemonSet 其實是一個非常簡單的控制器。在它的控制循環(huán)中,只需要遍歷所有節(jié)點,然后根據(jù)節(jié)點上是否有被管理 Pod 的情況,來決定是否要創(chuàng)建或者刪除一個 Pod。
? 只不過,在創(chuàng)建每個 Pod 的時候,DaemonSet 會自動給這個 Pod 加上一個 nodeAffinity,從而保證這個 Pod 只會在指定節(jié)點上啟動。同時,它還會自動給這個 Pod 加上一個 Toleration,從而忽略節(jié)點的 unschedulable“污點”。
? DaemonSet 使用 ControllerRevision,來保存和管理自己對應(yīng)的“版本”。這種“面向 API 對象”的設(shè)計思路,大大簡化了控制器本身的邏輯,也正是 Kubernetes 項目“聲明式 API”的優(yōu)勢所在。StatefulSet 也是直接控制 Pod 對象的,也在使用 ControllerRevision 進行版本管理。在 Kubernetes 項目里,ControllerRevision 其實是一個通用的版本管理對象。
Job和CronJob
? Job 對象在創(chuàng)建后,它的 Pod 模板,被自動加上了一個 controller-uid=< 一個隨機字符串 > 這樣的 Label。而這個 Job 對象本身,則被自動加上了這個 Label 對應(yīng)的 Selector,從而 保證了 Job 與它所管理的 Pod 之間的匹配關(guān)系。Job Controller 之所以要使用這種攜帶了 UID 的 Label,就是為了避免不同 Job 對象所管理的 Pod 發(fā)生重合。需要注意的是,這種自動生成的 Label 對用戶來說并不友好,所以不太適合推廣到 Deployment 等長作業(yè)編排對象上。
需要在 Pod 模板中定義 restartPolicy=Never
? 如果這個離線作業(yè)失敗了要怎么辦?比如,我們在這個例子中定義了 restartPolicy=Never,那么離線作業(yè)失敗后 Job Controller 就會不斷地嘗試創(chuàng)建一個新 Pod,直到滿足重試次數(shù)上限。如果你定義的 restartPolicy=OnFailure,那么離線作業(yè)失敗后,Job Controller 就不會去嘗試創(chuàng)建新的 Pod。但是,它會不斷地嘗試重啟 Pod 里的容器。
Job Controller 對并行作業(yè)的控制方法
在 Job 對象中,負責并行控制的參數(shù)有兩個:
- spec.parallelism,它定義的是一個 Job 在任意時間最多可以啟動多少個 Pod 同時運行;
- spec.completions,它定義的是 Job 至少要完成的 Pod 數(shù)目,即 Job 的最小完成數(shù)。
需要創(chuàng)建的 Pod 數(shù)目 = 最終需要的 Pod 數(shù)目 - 實際在 Running 狀態(tài) Pod 數(shù)目 - 已經(jīng)成功退出的 Pod 數(shù)目 = 4 - 0 - 0= 4。也就是說,Job Controller 需要創(chuàng)建 4 個 Pod 來糾正這個不一致狀態(tài)。
可是,我們又定義了這個 Job 的 parallelism=2。也就是說,我們規(guī)定了每次并發(fā)創(chuàng)建的 Pod 個數(shù)不能超過 2 個。所以,Job Controller 會對前面的計算結(jié)果做一個修正,修正后的期望創(chuàng)建的 Pod 數(shù)目應(yīng)該是:2 個。
CronJob
? CronJob 是一個 Job 對象的控制器(Controller)。CronJob 與 Job 的關(guān)系,正如同 Deployment 與 Pod 的關(guān)系一樣。CronJob 是一個專門用來管理 Job 對象的控制器。只不過,它創(chuàng)建和刪除 Job 的依據(jù),是 schedule 字段定義的、一個標準的Unix Cron格式的表達式。比如,"*/1 * * * *"。
? 由于定時任務(wù)的特殊性,很可能某個 Job 還沒有執(zhí)行完,另外一個新 Job 就產(chǎn)生了。這時候,你可以通過 spec.concurrencyPolicy 字段來定義具體的處理策略。比如:
- concurrencyPolicy=Allow,這也是默認情況,這意味著這些 Job 可以同時存在;
- concurrencyPolicy=Forbid,這意味著不會創(chuàng)建新的 Pod,該創(chuàng)建周期被跳過;
- concurrencyPolicy=Replace,這意味著新產(chǎn)生的 Job 會替換舊的、沒有執(zhí)行完的 Job。
聲明式 API——kubectl apply 命令
?
? 如果要滾動更新,原來的做法是先kubectl create -f xxx.yaml。然后再更新yaml文件后,執(zhí)行kubectl replace 觸發(fā)。而用apply則可以先kubectl apply -f xxx.yaml。然后更新yaml后再執(zhí)行一次kubectl apply -f xxx.yaml。kubectl replace 的執(zhí)行過程,是使用新的 YAML 文件中的 API 對象,替換原有的 API 對象;而 kubectl apply,則是執(zhí)行了一個對原有 API 對象的 PATCH 操作。類似地,kubectl set image 和 kubectl edit 也是對已有 API 對象的修改。
這意味著 kube-apiserver 在響應(yīng)命令式請求(比如,kubectl replace)的時候,一次只能處理一個寫請求,否則會有產(chǎn)生沖突的可能。而對于聲明式請求(比如,kubectl apply),一次能處理多個寫操作,并且具備 Merge 能力。
? Istio 項目,實際上就是一個基于 Kubernetes 項目的微服務(wù)治理框架。它的架構(gòu)非常清晰,如下所示:

Istio 最根本的組件,是運行在每一個應(yīng)用 Pod 里的 Envoy 容器。
這個 Envoy 項目是 Lyft 公司推出的一個高性能 C++ 網(wǎng)絡(luò)代理,也是 Lyft 公司對 Istio 項目的唯一貢獻。
而 Istio 項目,則把這個代理服務(wù)以 sidecar 容器的方式,運行在了每一個被治理的應(yīng)用 Pod 中。我們知道,Pod 里的所有容器都共享同一個 Network Namespace。所以,Envoy 容器就能夠通過配置 Pod 里的 iptables 規(guī)則,把整個 Pod 的進出流量接管下來。
這時候,Istio 的控制層(Control Plane)里的 Pilot 組件,就能夠通過調(diào)用每個 Envoy 容器的 API,對這個 Envoy 代理進行配置,從而實現(xiàn)微服務(wù)治理。
? 假設(shè)這個 Istio 架構(gòu)圖左邊的 Pod 是已經(jīng)在運行的應(yīng)用,而右邊的 Pod 則是我們剛剛上線的應(yīng)用的新版本。這時候,Pilot 通過調(diào)節(jié)這兩 Pod 里的 Envoy 容器的配置,從而將 90% 的流量分配給舊版本的應(yīng)用,將 10% 的流量分配給新版本應(yīng)用,并且,還可以在后續(xù)的過程中隨時調(diào)整。這樣,一個典型的“灰度發(fā)布”的場景就完成了。比如,Istio 可以調(diào)節(jié)這個流量從 90%-10%,改到 80%-20%,再到 50%-50%,最后到 0%-100%,就完成了這個灰度發(fā)布的過程。
? Istio 項目使用的,是 Kubernetes 中的一個非常重要的功能,叫作 Dynamic Admission Control。也就是一些需要初始化的工作,需要在k8s項目正式處理之前進行。選擇性的被編譯進APIServer中,在API對象創(chuàng)建之后會被立刻調(diào)用到。k8s提供了這種熱插拔式的Admission機制,也叫做Initializer。比如自動加上Envoy容器的配置。
? 首先,Istio 會將這個 Envoy 容器本身的定義,以 ConfigMap 的方式保存在 Kubernetes 當中。然后在pod的yaml提交到k8s后自動將定義的envoy容器merge(TwoWayMergePatch)到用戶提交的yaml中。
? 接下來,Istio 將一個編寫好的 Initializer,作為一個 Pod 部署在 Kubernetes 中。
? 當你在 Initializer 里完成了要做的操作后,一定要記得將這個 metadata.initializers.pending 標志清除掉。這一點,你在編寫 Initializer 代碼的時候一定要非常注意。
? Istio 項目的核心,就是由無數(shù)個運行在應(yīng)用 Pod 中的 Envoy 容器組成的服務(wù)代理網(wǎng)格。這也正是 Service Mesh 的含義。
- 首先,所謂“聲明式”,指的就是我只需要提交一個定義好的 API 對象來“聲明”,我所期望的狀態(tài)是什么樣子。
- 其次,“聲明式 API”允許有多個 API 寫端,以 PATCH 的方式對 API 對象進行修改,而無需關(guān)心本地原始 YAML 文件的內(nèi)容。
- 最后,也是最重要的,有了上述兩個能力,Kubernetes 項目才可以基于對 API 對象的增、刪、改、查,在完全無需外界干預的情況下,完成對“實際狀態(tài)”和“期望狀態(tài)”的調(diào)諧(Reconcile)過程。
所以說,聲明式 API,才是 Kubernetes 項目編排能力“賴以生存”的核心所在。
YAML 文件提交給 Kubernetes 之后,創(chuàng)建出一個 API 對象的過程

首先,Kubernetes 會匹配 API 對象的組。
然后,Kubernetes 會進一步匹配到 API 對象的版本號。
首先,當我們發(fā)起了創(chuàng)建 CronJob 的 POST 請求之后,我們編寫的 YAML 的信息就被提交給了 APIServer。而 APIServer 的第一個功能,就是過濾這個請求,并完成一些前置性的工作,比如授權(quán)、超時處理、審計等。
然后,請求會進入 MUX 和 Routes 流程。如果你編寫過 Web Server 的話就會知道,MUX 和 Routes 是 APIServer 完成 URL 和 Handler 綁定的場所。而 APIServer 的 Handler 要做的事情,就是按照我剛剛介紹的匹配過程,找到對應(yīng)的 CronJob 類型定義。
接著,APIServer 最重要的職責就來了:根據(jù)這個 CronJob 類型定義,使用用戶提交的 YAML 文件里的字段,創(chuàng)建一個 CronJob 對象。而在這個過程中,APIServer 會進行一個 Convert 工作,即:把用戶提交的 YAML 文件,轉(zhuǎn)換成一個叫作 Super Version 的對象,它正是該 API 資源類型所有版本的字段全集。這樣用戶提交的不同版本的 YAML 文件,就都可以用這個 Super Version 對象來進行處理了。
接下來,APIServer 會先后進行 Admission() 和 Validation() 操作。比如,我在上一篇文章中提到的 Admission Controller 和 Initializer,就都屬于 Admission 的內(nèi)容。而 Validation,則負責驗證這個對象里的各個字段是否合法。這個被驗證過的 API 對象,都保存在了 APIServer 里一個叫作 Registry 的數(shù)據(jù)結(jié)構(gòu)中。也就是說,只要一個 API 對象的定義能在 Registry 里查到,它就是一個有效的 Kubernetes API 對象。
最后,APIServer 會把驗證過的 API 對象轉(zhuǎn)換成用戶最初提交的版本,進行序列化操作,并調(diào)用 Etcd 的 API 把它保存起來。
Custom Resource Definition——自定義 API 資源
先需編寫一個 CRD 的 YAML 文件,它的名字叫作 network.yaml。——類比xsd文件
再編寫Network 對象的 YAML 文件,名叫 example-network.yaml?!惐葂ml文件
為這個 API 對象編寫一個自定義控制器(Custom Controller)
? 這樣, Kubernetes 才能根據(jù) Network API 對象的“增、刪、改”操作,在真實環(huán)境中做出相應(yīng)的響應(yīng)。比如,“創(chuàng)建、刪除、修改”真正的 Neutron 網(wǎng)絡(luò)。

從 Kubernetes 的 APIServer 里獲取它所關(guān)心的對象,也就是我定義的 Network 對象。Informer 與 API 對象是一一對應(yīng)的,所以傳遞給自定義控制器的,正是一個 Network 對象的 Informer(Network Informer)。
Informer 所使用的 Reflector 包使用的是一種叫作ListAndWatch的方法,來“獲取”并“監(jiān)聽”這些 Network 對象實例的變化。在 ListAndWatch 機制下,一旦 APIServer 端有新的 Network 實例被創(chuàng)建、刪除或者更新,Reflector 都會收到“事件通知”。這時,該事件及它對應(yīng)的 API 對象這個組合,就被稱為增量(Delta),它會被放進一個 Delta FIFO Queue(即:增量先進先出隊列)中。
-
Informe 會不斷地從這個 Delta FIFO Queue 里讀?。≒op)增量。每拿到一個增量,Informer 就會判斷這個增量里的事件類型,然后創(chuàng)建或者更新本地對象的緩存。這個緩存,在 Kubernetes 里一般被叫作 Store。
? 這個同步本地緩存的工作,是 Informer 的第一個職責,也是它最重要的職責。
? 而Informer 的第二個職責,則是根據(jù)這些事件的類型,觸發(fā)事先注冊好的 ResourceEventHandler。這些 Handler,需要在創(chuàng)建控制器的時候注冊給它對應(yīng)的 Informer。所謂 Informer,其實就是一個帶有本地緩存和索引機制的、可以注冊 EventHandler 的 client。
? 更具體地說,Informer 通過一種叫作 ListAndWatch 的方法,把 APIServer 中的 API 對象緩存在了本地,并負責更新和維護這個緩存。首先,通過 APIServer 的 LIST API“獲取”所有最新版本的 API 對象;然后,再通過 WATCH API 來“監(jiān)聽”所有這些 API 對象的變化。而通過監(jiān)聽到的事件變化,Informer 就可以實時地更新本地緩存,并且調(diào)用這些事件對應(yīng)的 EventHandler 了。
控制循環(huán)(Control Loop)
- 首先,等待 Informer 完成一次本地緩存的數(shù)據(jù)同步操作;
- 然后,直接通過 goroutine 啟動一個(或者并發(fā)啟動多個)“無限循環(huán)”的任務(wù)。
? 而這個“無限循環(huán)”任務(wù)的每一個循環(huán)周期,執(zhí)行的正是我們真正關(guān)心的業(yè)務(wù)邏輯。首先從工作隊列里出隊(workqueue.Get)了一個成員,也就是一個 Key,使用 networksLister 來嘗試獲取這個 Key 對應(yīng)的 Network 對象。這個操作,其實就是在訪問本地緩存的索引。實際上,在 Kubernetes 的源碼中,你會經(jīng)??吹娇刂破鲝母鞣N Lister 里獲取對象,比如:podLister、nodeLister 等等,它們使用的都是 Informer 和緩存機制。
? 而如果能夠獲取到對應(yīng)的 Network 對象,我就可以執(zhí)行控制器模式里的對比“期望狀態(tài)”和“實際狀態(tài)”的邏輯了。
- 首先使用 Kubernetes 的 client(kubeClient)創(chuàng)建了一個工廠;
- 然后,用跟 Network 類似的處理方法,生成了一個 Deployment Informer;
- 接著,把 Deployment Informer 傳遞給了自定義控制器;當然,也要調(diào)用 Start 方法來啟動這個 Deployment Informer。
? 而有了這個 Deployment Informer 后,這個控制器也就持有了所有 Deployment 對象的信息。接下來,它既可以通過 deploymentInformer.Lister() 來獲取 Etcd 里的所有 Deployment 對象,也可以為這個 Deployment Informer 注冊具體的 Handler 來。更重要的是,這就使得在這個自定義控制器里面,我可以通過對自定義 API 對象和默認 API 對象進行協(xié)同,從而實現(xiàn)更加復雜的編排功能。
總結(jié):
所謂的 Informer,就是一個自帶緩存和索引機制,可以觸發(fā) Handler 的客戶端庫。這個本地緩存在 Kubernetes 中一般被稱為 Store,索引一般被稱為 Index。
Informer 使用了 Reflector 包,它是一個可以通過 ListAndWatch 機制獲取并監(jiān)視 API 對象變化的客戶端封裝。
Reflector 和 Informer 之間,用到了一個“增量先進先出隊列”進行協(xié)同。而 Informer 與你要編寫的控制循環(huán)之間,則使用了一個工作隊列來進行協(xié)同。
在實際應(yīng)用中,除了控制循環(huán)之外的所有代碼,實際上都是 Kubernetes 為你自動生成的,即:pkg/client/{informers, listers, clientset}里的內(nèi)容。
而這些自動生成的代碼,就為我們提供了一個可靠而高效地獲取 API 對象“期望狀態(tài)”的編程庫。
所以,接下來,作為開發(fā)者,你就只需要關(guān)注如何拿到“實際狀態(tài)”,然后如何拿它去跟“期望狀態(tài)”做對比,從而決定接下來要做的業(yè)務(wù)邏輯即可。