一、集羣的服務分類
在K8S運行的服務,從簡單到複雜可以分成三類:無狀態服務、普通有狀態服務和有狀態集羣服務。下面分別來看K8S是如何運行這三類服務的。
1、無狀態服務(Stateless Service):
1)定義:是指該服務運行的實例不會在本地存儲需要持久化的數據,並且多個實例對於同一個請求響應的結果是完全一致的。
2)隨意擴容和縮容:這些節點可以隨意擴容或者縮容,只要簡單的增加或減少副本的數量就可以。K8S使用RC(或更新的Replica Set)來保證一個服務的實例數量,如果說某個Pod實例由於某種原因Crash了,RC會立刻用這個Pod的模版新啓一個Pod來替代它,由於是無狀態的服務,新啓的Pod與原來健康狀態下的Pod一模一樣。在Pod被重建後它的IP地址可能發生變化,爲了對外提供一個穩定的訪問接口,K8S引入了Service的概念。一個Service後面可以掛多個Pod,實現服務的高可用。
3)多個實例可以共享相同的持久化數據:例如數據存儲到mysql。
相關的k8s資源有:ReplicaSet、ReplicationController、Deployment等,由於是無狀態服務,所以這些控制器創建的pod序號都是隨機值。並且在縮容的時候並不會明確縮容某一個pod,而是隨機的,因爲所有實例得到的返回值都是一樣,所以縮容任何一個pod都可以。
2、普通有狀態服務(Stateful Service):
和無狀態服務相比,它多了狀態保存的需求。即有數據存儲功能。這類服務包括單實例的mysql。
因爲有狀態的容器異常重啓就會造成數據丟失,也無法多副本部署,無法實現負載均衡。
比如PHP的Session數據默認存儲在磁盤上,比如 /tmp 目錄,而多副本負載均衡時,多個PHP容器的目錄是彼此隔離的。比如存在兩個副本A和B,用戶第一次請求時候,流量被轉發到A,並生成了SESSION,而第二次請求時,流量可能被負載均衡器轉發到B上,而B是沒有SESSION數據的,所以就會造成會話超時等BUG。
如果採用主機卷的方式,多個容器掛載同一個主機目錄,就可以共享SESSION數據,但是如果多主機負載均衡場景,就需要將SESSION存儲於外部數據庫或Redis中了。
Kubernetes提供了以Volume和Persistent Volume爲基礎的存儲系統,可以實現服務的狀態保存。
普通狀態服務只能有一個實例,因此不支持“自動服務容量調節”。一般來說,數據庫服務或者需要在本地文件系統存儲配置文件或其它永久數據的應用程序可以創建使用有狀態服務。要想創建有狀態服務,必須滿足幾個前提:
1)待創建的服務鏡像(image)的Dockerfile中必須定義了存儲卷(Volume),因爲只有存儲卷所在目錄裏的數據可以被備份
2)創建服務時,必須指定給該存儲卷分配的磁盤空間大小
3)如果創建服務的同時需要從之前的一個備份裏恢復數據,那麼還要指明該存儲卷用哪個備份恢復。
無狀態服務和有狀態服務主要有以下幾點區別:
實例數量:無狀態服務可以有一個或多個實例,因此支持兩種服務容量調節模式;有狀態服務只能有一個實例,不允許創建多個 實例,因此也不支持服務容量調節模式。
存儲卷:無狀態服務可以有存儲卷,也可以沒有,即使有也無法備份存儲卷裏面的數據;有狀態服務必須要有存儲卷,並且在創建服務時,必須指定給該存儲卷分配的磁盤空間大小。
數據存儲:無狀態服務運行過程中的所有數據(除日誌和監控數據)都存在容器實例裏的文件系統中,如果實例停止或者刪除,則這些數據都將丟失,無法找回;而對於有狀態服務,凡是已經掛載了存儲卷的目錄下的文件內容都可以隨時進行備份,備份的數據可以下載,也可以用於恢復新的服務。但對於沒有掛載卷的目錄下的數據,仍然是無法備份和保存的,如果實例停止或者刪除,這些非掛載卷裏的文件內容同樣會丟失。
3、有狀態集羣服務(Stateful cluster Service)
與普通有狀態服務相比,它多了集羣管理的需求,即有狀態集羣服務要解決的問題有兩個,一個是狀態保存,另一個是集羣管理。
這類服務包括kafka、zookeeper等。
二、StatefulSet
2.1 StatefulSet背景
有狀態集羣服務的部署,意味着節點需要形成羣組關係,每個節點需要一個唯一的ID(例如Kafka BrokerId, Zookeeper myid)來作爲集羣內部每個成員的標識,集羣內節點之間進行內部通信時需要用到這些標識。
傳統的做法是管理員會把這些程序部署到穩定的,長期存活的節點上去,這些節點有持久化的存儲和靜態的IP地址。這樣某個應用的實例就跟底層物理基礎設施比如某臺機器,某個IP地址耦合在一起了。
K8S爲此開發了一套以StatefulSet
(1.5版本之前叫做PetSet
)爲核心的全新特性,方便了有狀態集羣服務在K8S上的部署和管理。Kubernets
中StatefulSet
的目標是通過把標識分配給應用程序的某個不依賴於底層物理基礎設施的特定實例來解耦這種依賴關係。(消費方不使用靜態的IP
,而是通過DNS
域名去找到某臺特定機器)
2.2 具體的工作原理:
1、是通過Init Container來做集羣的初始化工作
2、用 Headless Service 來維持集羣成員的穩定關係,
3、用動態存儲供給來方便集羣擴容
4、最後用StatefulSet
來綜合管理整個集羣。
要運行有狀態集羣服務要解決的問題有兩個,一個是狀態保存,另一個是集羣管理。 我們先來看如何解決第一個問題:狀態保存。Kubernetes 有一套以Volume插件爲基礎的存儲系統,通過這套存儲系統可以實現應用和服務的狀態保存。
K8S的存儲系統從基礎到高級又大致分爲三個層次:普通Volume,Persistent Volume 和動態存儲供應。
從kubernetes 1.5 開始, PetSet 功能升級到了 Beta 版本,並重新命名爲StatefulSet。除了依照社區民意改了名字之外,這一 API 對象並沒有太大變化,kubernetes集羣部署 Pod 增加了每索引最多一個”的語義,有了順序部署、順序終結、唯一網絡名稱以及持久穩定的存儲。
2.3 StatefulSet特性
StatefulSet爲什麼適合有狀態的程序,因爲它相比於Deployment有以下特點:
1、穩定的,唯一的網絡標識:可以用來發現集羣內部的其他成員。比如StatefulSet的名字叫kafka,那麼第一個起來的Pet叫kafka-0,第二個叫kafk-1,依次類推,基於Headless Service(即沒有Cluster IP的Service)來實現。
2、穩定的持久化存儲:通過Kubernetes的PV/PVC或者外部存儲(預先提供的)來實現
3、啓動或關閉時保證有序:優雅的部署和伸縮性: 操作第n個pod時,前n-1個pod已經是運行且準備好的狀態。 有序的,優雅的刪除和終止操作:從 n, n-1, ... 1, 0 這樣的順序刪除。
在部署或者擴展的時候要依據定義的順序依次依序進行(即從0到N-1,在下一個Pod運行之前所有之前的Pod必須都是Running和Ready狀態),基於init containers來實現
上述提到的“穩定”指的是Pod在多次重新調度時保持穩定,即存儲,DNS名稱,hostname都是跟Pod綁定到一起的,跟Pod被調度到哪個節點沒關係。
所以Zookeeper,Etcd或 Elasticsearch這類需要穩定的集羣成員的應用時,就可以用StatefulSet。通過查詢無頭服務域名的A記錄,就可以得到集羣內成員的域名信息。
2.4 StatefulSet也有一些限制:
1)、在Kubernetes 1.9版本之前是beta版本,在Kubernetes 1.5版本之前是不提供的。
2)、Pod的存儲必須是通過 PersistentVolume Provisioner基於 storeage類來提供,或者是管理員預先提供的外部存儲。
3)、刪除或者縮容不會刪除跟StatefulSet相關的卷,這是爲了保證數據的安全
4)、StatefulSet現在需要一個無頭服務(Headless Service)來負責生成Pods的唯一網絡標示,此Headless服務需要通過手工創建。
2.5 什麼時候使用StatefulSet
StatefulSet 的目的就是給爲數衆多的有狀態負載提供正確的控制器支持。然而需要注意的是,不一定所有的有存儲應用都是適合移植到 Kubernetes 上的,在移植存儲層和編排框架之前,需要回答以下幾個問題。
應用是否可以使用遠程存儲?
目前,我們推薦用遠程存儲來使用 StatefulSets,就要對因爲網絡造成的存儲性能損失有一個準備:即使是專門優化的實例,也無法同本地加載的 SSD 相提並論。你的雲中的網絡存儲,能夠滿足 SLA 要求麼?如果答案是肯定的,那麼利用 StatefulSet 運行這些應用,就能夠獲得自動化的優勢。如果應用所在的 Node 發生故障,包含應用的 Pod 會調度到其他 Node 上,在這之後會重新加載他的網絡存儲以及其中的數據。
這些應用是否有伸縮需求?
用 StatefulSet 運行應用會帶來什麼好處呢?你的整個組織是否只需要一個應用實例?對該應用的伸縮是否會引起問題?如果你只需要較少的應用實例數量,這些實例能夠滿足組織現有的需要,而且可以預見的是,應用的負載不會很快增長,那麼你的本地應用可能無需移植。
然而,如果你的系統是微服務所構成的生態系統,就會比較頻繁的交付新服務,如果更近一步,服務是有狀態的,那麼 Kubernetes 的自動化和健壯性特性會對你的系統有很大幫助。如果你已經在使用 Kubernetes 來管理你的無狀態服務,你可能會想要在同一個體系中管理你的有狀態應用。
預期性能增長的重要性?
Kubernetes 還不支持網絡或存儲在 Pod 之間的隔離。如果你的應用不巧和嘈雜的鄰居共享同一個節點,會導致你的 QPS 下降。解決方式是把 Pod 調度爲該 Node 的唯一租戶(獨佔服務器),或者使用互斥規則來隔離會爭用網絡和磁盤的 Pod,但是這就意味着用戶必須鑑別和處置(競爭)熱點。
如果榨乾有狀態應用的最大 QPS 不是你的首要目標,而且你願意也有能力處理競爭問題,似的有狀態應用能夠達到 SLA 需要,又如果對服務的移植、伸縮和重新調度是你的主要需求,Kubernetes 和 StatefulSet 可能就是解決問題的好方案了。
你的應用是否需要特定的硬件或者實例類型
如果你的有狀態應用在高端硬件或高規格實例上運行,而其他應用在通用硬件或者低規格實例上運行,你可能不想部署一個異構的集羣。如果可以把所有應用都部署到統一實例規格的實例上,那麼你就能夠從 Kubernetes 獲得動態資源調度和健壯性的好處。
三、Init Container
什麼是Init Container?
從名字來看就是做初始化工作的容器。可以有一個或多個,如果有多個,這些 Init Container 按照定義的順序依次執行,只有所有的Init Container 執行完後,主容器才啓動。由於一個Pod裏的存儲卷是共享的,所以 Init Container 裏產生的數據可以被主容器使用到。
Init Container可以在多種 K8S 資源裏被使用到如 Deployment、Daemon Set, Pet Set, Job等,但歸根結底都是在Pod啓動時,在主容器啓動前執行,做初始化工作。
第一種場景是等待其它模塊Ready,比如我們有一個應用裏面有兩個容器化的服務,一個是Web Server,另一個是數據庫。其中Web Server需要訪問數據庫。但是當我們啓動這個應用的時候,並不能保證數據庫服務先啓動起來,所以可能出現在一段時間內Web Server有數據庫連接錯誤。爲了解決這個問題,我們可以在運行Web Server服務的Pod裏使用一個Init Container,去檢查數據庫是否準備好,直到數據庫可以連接,Init Container才結束退出,然後Web Server容器被啓動,發起正式的數據庫連接請求。
第二種場景是做初始化配置,比如集羣裏檢測所有已經存在的成員節點,爲主容器準備好集羣的配置信息,這樣主容器起來後就能用這個配置信息加入集羣。
還有其它使用場景,如將pod註冊到一箇中央數據庫、下載應用依賴等。
這些東西能夠放到主容器裏嗎?從技術上來說能,但從設計上來說,可能不是一個好的設計。首先不符合單一職責原則,其次這些操作是隻執行一次的,如果放到主容器裏,還需要特殊的檢查來避免被執行多次。
這是Init Container的一個使用樣例
這個例子創建一個Pod,這個Pod裏跑的是一個nginx容器,Pod裏有一個叫workdir的存儲卷,訪問nginx容器服務的時候,就會顯示這個存儲卷裏的index.html 文件。
而這個index.html 文件是如何獲得的呢?是由一個Init Container從網絡上下載的。這個Init Container 使用一個busybox鏡像,起來後,執行一條wget命令,獲取index.html文件,然後結束退出。
由於Init Container和nginx容器共享一個存儲卷(這裏這個存儲卷的名字叫workdir),所以在Init container裏下載的index.html文件可以在nginx容器裏被訪問到。
可以看到 Init Container 是在 annotation裏定義的。Annotation 是K8S新特性的實驗場,通常一個新的Feature出來一般會先在Annotation 裏指定,等成熟穩定了,再給它一個正式的屬性名或資源對象名。
介紹完Init Container,千呼萬喚始出來,主角Pet Set該出場了。
四、K8S的存儲系統
K8S的存儲系統從基礎到高級又大致分爲三個層次:普通Volume,Persistent Volume 和動態存儲供應。
1.普通Volume
最簡單的普通Volume是單節點Volume。它和Docker的存儲卷類似,使用的是Pod所在K8S節點的本地目錄。
第二種類型是跨節點存儲卷,這種存儲卷不和某個具體的K8S節點綁定,而是獨立於K8S節點存在的,整個存儲集羣和K8S集羣是兩個集羣,相互獨立。
跨節點的存儲卷在Kubernetes上用的比較多,如果已有的存儲不能滿足要求,還可以開發自己的Volume插件,只需要實現Volume.go 裏定義的接口。如果你是一個存儲廠商,想要自己的存儲支持Kubernetes 上運行的容器,就可以去開發一個自己的Volume插件。
2.pv:persistent volume
PersistentVolume(PV)是集羣之中的一塊網絡存儲。跟 Node 一樣,也是集羣的資源,並且不屬於特定的namespace。PV 跟 Volume (卷) 類似,不過會有獨立於 Pod 的生命週期。
它和普通Volume的區別是什麼呢?
普通Volume和使用它的Pod之間是一種靜態綁定關係,在定義Pod的文件裏,同時定義了它使用的Volume。Volume 是Pod的附屬品,我們無法單獨創建一個Volume,因爲它不是一個獨立的K8S資源對象。
而Persistent Volume 簡稱PV是一個K8S資源對象,所以我們可以單獨創建一個PV。它不和Pod直接發生關係,而是通過Persistent Volume Claim,簡稱PVC來實現動態綁定。Pod定義裏指定的是PVC,然後PVC會根據Pod的要求去自動綁定合適的PV給Pod使用。
PV的訪問模式有三種:
第一種,ReadWriteOnce:是最基本的方式,可讀可寫,但只支持被單個Pod掛載。
第二種,ReadOnlyMany:可以以只讀的方式被多個Pod掛載。
第三種,ReadWriteMany:這種存儲可以以讀寫的方式被多個Pod共享。不是每一種存儲都支持這三種方式,像共享方式,目前支持的還比較少,比較常用的是NFS。在PVC綁定PV時通常根據兩個條件來綁定,一個是存儲的大小,另一個就是訪問模式。
剛纔提到說PV與普通Volume的區別是動態綁定,我們來看一下這個過程是怎樣的。
這是PV的生命週期,首先是Provision,即創建PV,這裏創建PV有兩種方式,靜態和動態。所謂靜態,是管理員手動創建一堆PV,組成一個PV池,供PVC來綁定。動態方式是通過一個叫 Storage Class的對象由存儲系統根據PVC的要求自動創建。
一個PV創建完後狀態會變成Available,等待被PVC綁定。
一旦被PVC邦定,PV的狀態會變成Bound,就可以被定義了相應PVC的Pod使用。
Pod使用完後會釋放PV,PV的狀態變成Released。
變成Released的PV會根據定義的回收策略做相應的回收工作。有三種回收策略,Retain、Delete 和 Recycle。Retain就是保留現場,K8S什麼也不做,等待用戶手動去處理PV裏的數據,處理完後,再手動刪除PV。Delete 策略,K8S會自動刪除該PV及裏面的數據。Recycle方式,K8S會將PV裏的數據刪除,然後把PV的狀態變成Available,又可以被新的PVC綁定使用。
在實際使用場景裏,PV的創建和使用通常不是同一個人。這裏有一個典型的應用場景:管理員創建一個PV池,開發人員創建Pod和PVC,PVC裏定義了Pod所需存儲的大小和訪問模式,然後PVC會到PV池裏自動匹配最合適的PV給Pod使用。
前面在介紹PV的生命週期時,提到PV的供給有兩種方式,靜態和動態。其中動態方式是通過StorageClass來完成的,這是一種新的存儲供應方式。
使用StorageClass有什麼好處呢?除了由存儲系統動態創建,節省了管理員的時間,還有一個好處是可以封裝不同類型的存儲供PVC選用。在StorageClass出現以前,PVC綁定一個PV只能根據兩個條件,一個是存儲的大小,另一個是訪問模式。在StorageClass出現後,等於增加了一個綁定維度。
比如這裏就有兩個StorageClass,它們都是用谷歌的存儲系統,但是一個使用的是普通磁盤,我們把這個StorageClass命名爲slow。另一個使用的是SSD,我們把它命名爲fast。
在PVC裏除了常規的大小、訪問模式的要求外,還通過annotation指定了Storage Class的名字爲fast,這樣這個PVC就會綁定一個SSD,而不會綁定一個普通的磁盤。
到這裏Kubernetes的整個存儲系統就都介紹完了。總結一下,兩種存儲卷:普通Volume 和Persistent Volume。普通Volume在定義Pod的時候直接定義,Persistent Volume通過Persistent Volume Claim來動態綁定。PV可以手動創建,也可以通過StorageClass來動態創建。
五、Headless Service
1、定義:有時不需要或不想要負載均衡,以及單獨的Service IP。遇到這種情況,可以通過指定Cluster IP(spec.clusterIP)的值爲“None”來創建Headless Service。
2、和普通Service
相比:k8s對Headless Service並不會分配Cluster IP,kube-proxy不會處理它們,而且平臺也不會爲它們進行負載均衡和路由。k8s會給一個集羣內部的每個成員提供一個唯一的DNS域名
來作爲每個成員的網絡標識,集羣內部成員之間使用域名通信。
普通Service的Cluster IP 是對外的,用於外部訪問多個Pod實例。而Headless Service的作用是對內的,用於爲一個集羣內部的每個成員提供一個唯一的DNS名字,這樣集羣成員之間就能相互通信了。所以Headless Service沒有Cluster IP,這是它和普通Service的區別。
3、無頭服務管理的域名是如下的格式:$(service_name).$(k8s_namespace).svc.cluster.local
。其中的"cluster.local"
是集羣的域名,除非做了配置,否則集羣域名默認就是cluster.local
。
4、StatefulSet
下創建的每個Pod的序號是唯一的。
爲了解決名字不穩定的問題,StatefulSet
下創建的每個Pod
的名字不再使用隨機字符串,而是爲每個pod分配一個唯一不變的序號,比如StatefulSet
的名字叫 mysql,那麼第一個啓起來的pod就叫 mysql-0,第二個叫 mysql-1,如此下去。
當一個某個pod掉後,新創建的pod會被賦予跟原來pod一樣的名字。由於pod名字不變所以DNS名字也跟以前一樣,同時通過名字還能匹配到原來pod用到的存儲,實現狀態保存。
StatefulSet
下創建的每個Pod
,得到一個對應的DNS
子域名,格式如下:
$(podname).$(governing_service_domain)
,這裏 governing_service_domain
是由StatefulSet
中定義的serviceName
來決定。
舉例子,無頭服務管理的kafka
的域名是:kafka.test.svc.cluster.local
,
創建的Pod
得到的子域名是 kafka-1.kafka.test.svc.cluster.local
。注意這裏提到的域名,都是由kuber-dns
組件管理的集羣內部使用的域名,可以通過命令來查詢:
$ nslookup my-nginx
Server: 192.168.16.53
Address 1: 192.168.16.53
Name: my-nginx
Address 1: 192.168.16.132
而普通Service
情況下,Pod
名字後面是隨機數,需要通過Service
來做負載均衡。
當一個StatefulSet
掛掉,新創建的StatefulSet
會被賦予跟原來的Pod
一樣的名字,通過這個名字來匹配到原來的存儲,實現了狀態保存。因爲上文提到了,每個Pod
的標識附着在Pod
上,無論pod
被重新調度到了哪裏。
六、StatefuleSet示例
Kafka和zookeeper是在兩種典型的有狀態的集羣服務。首先kafka和zookeeper都需要存儲盤來保存有狀態信息,其次kafka和zookeeper每一個實例都需要有對應的實例Id(Kafka需要broker.id,zookeeper需要my.id)來作爲集羣內部每個成員的標識,集羣內節點之間進行內部通信時需要用到這些標識。
有兩個原因讓 [ZooKeeper] 成爲 StatefulSet 的好例子。首先,StatefulSet 在其中演示了運行分佈式、強一致性存儲的應用的能力;其次,ZooKeeper 也是 Apache Hadoop 和 Apache Kafka 在 Kubernetes 上運行的前置條件。在 Kubernetes 文檔中有一個 深度教程 說明了在 Kubernetes 集羣上部署 ZooKeeper Ensemble 的過程,這裏會簡要描述一下其中的關鍵特性。
具體的部署過程包括以下幾個部署:
(1) Persistent Volume 存儲的創建
(2) StatefulSet(Petset)資源的創建
(3) headless服務的創建