架構解密從分布式到微服務:深入Kubernetes微服務平臺
深入Kubernetes微服務平臺
Kubernetes的概念與功能
架構師普遍有這樣的愿景:在系統(tǒng)中有ServiceA、ServiceB、ServiceC這3種服務,其中ServiceA需要部署3個實例,ServiceB與ServiceC各自需要部署5個實例,希望有一個平臺(或工具)自動完成上述13個實例的分布式部署,并且持續(xù)監(jiān)控它們。當發(fā)現(xiàn)某個服務器宕機或者某個服務實例發(fā)生故障時,平臺能夠自我修復,從而確保在任何時間點正在運行的服務實例的數量都符合預期。這樣一來,團隊只需關注服務開發(fā)本身,無須再為基礎設施和運維監(jiān)控的事情頭疼了。

在 Kubernetes出現(xiàn)之前,沒有一個平臺公開聲稱實現(xiàn)了上面的愿景。Kubernetes是業(yè)界第一個將服務這個概念真正提升到第一位的平臺。在Kubernetes的世界里,所有概念與組件都是圍繞Service運轉的。正是這種突破性的設計,使Kubernetes真正解決了多年來困擾我們的分布式系統(tǒng)里的眾多難題,讓團隊有更多的時間去關注與業(yè)務需求和業(yè)務相關的代碼本身,從而在很大程度上提高整個軟件團隊的工作效率與投入產出比。
Kubernetes里的Service其實就是微服務架構中微服務的概念,它有以下明顯特點。
- 每個Service都分配了一個固定不變的虛擬IP地址——Cluster IP。
 - 每個Service都以TCP/UDP方式在一個或多個端口 (Service Port)上提供服務。
 - 客戶端訪問一個 Service時,就好像訪問一個遠程的TCP/UDP服務,只要與Cluster IP建立連接即可,目標端口就是某個Service Port。
 
Service既然有了IP地址,就可以順理成章地采用DNS域名的方式來避免IP地址的變動了。Kubernetes 的 DNS組件自動為每個Service都建立了一個域名與IP的映射表,其中的域名就是Service的Name,IP就是對應的Cluster IP,并且在Kubernetes的每個Pod(類似于Docker'容器)里都設置了DNS Server為 Kubernetes 的 DNS Server,這樣一來,微服務架構中的服務發(fā)現(xiàn)這個基本問題得以巧妙解決,不但不用復雜的服務發(fā)現(xiàn)API供客戶端調用,還使所有以TCP/IP方式通信的分布式系統(tǒng)都能方便地遷移到Kubernetes平臺上,僅從這個設計來看,Kubernetes就遠勝過其他產品。
我們知道,在每個微服務的背后都有多個進程實例來提供服務,在Kubernetes平臺上,這些進程實例被封裝在Pod中,Pod基本上等同于Docker容器,稍有不同的是,Pod其實是一組密切捆綁在一起并且“同生共死”的 Docker 容器,這組容器共享同一個網絡棧與文件系統(tǒng),相互之間沒有隔離,可以直接在進程間通信。最典型的例子是Kubenetes Sky DNS Pod,在這個Pod里有4個Docker '容器。
那么,Kubernetes里的 Service 與 Pod 是如何對應的呢?我們怎么知道哪些Pod 為某個Service提供具體的服務?下圖給出了答案——“貼標簽”。

每個Pod都可以貼一個或多個不同的標簽(Label),而每個Service都有一個“標簽選擇器”(Label Selector),標簽選擇器確定了要選擇擁有哪些標簽的對象。下面這段YAML格式的內容定義了一個被稱為ku8-redis-master的Service,它的標簽選擇器的內容為“app: ku8-redis-master",表明擁有“app= ku8-redis-master”這個標簽的Pod都是為它服務的:
- apiversion: v1
 - kind: Service
 - metadata:
 - name: ku8-redis-masterspec:
 - ports:
 - - port: 6379selector:
 - app: ku8-redis-master
 
下面是 ku8-redis-master這個 Pod 的定義,它的 labels屬性的內容剛好匹配Service 的標簽選擇器的內容:
- apiversion: v1kind: Pod
 - metadata:
 - name: ku8-redis-masterlabels:
 - app: ku8-redis-master
 - spec:
 - containers:
 - name: serverimage: redisports:
 - -containerPort:6379
 - restartPolicy: Never
 
如果我們需要一個Service在任意時刻都有N個Pod實例來提供服務,并且在其中1個Pod實例發(fā)生故障后,及時發(fā)現(xiàn)并且自動產生一個新的Pod實例以彌補空缺,那么我們要怎么做呢?答案就是采用 Deployment/RC,它的作用是告訴Kubernetes,擁有某個特定標簽的 Pod需要在Kubernetes集群中創(chuàng)建幾個副本實例。Deployment/RC的定義包括如下兩部分內容。
●目標Pod的副本數量(replicas)。
●目標Pod的創(chuàng)建模板(Template)。
下面這個例子定義了一個RC,目標是確保在集群中任意時刻都有兩個 Pod,其標簽為“ app:ku8-redis-slave”,對應的容器鏡像為redis slave,這兩個 Pod 與ku8-redis-master構成了Redis主從集群(一主二從):
- apiversion :v1
 - kind: ReplicationControllermetadata:
 - name: ku8-redis-slavespec:
 - replicas: 2template:
 - metadata:
 - labels:
 - app: ku8-redis-slavespec:
 - containers:
 - name: server
 - image: devopsbq/redis-slave
 - env:
 - name: MASTER ADDR
 - value: ku8-redis-masterports:
 - -containerPort:6379
 
至此,上述YAML文件創(chuàng)建了一個一主二從的Redis集群,其中Redis Master被定義為一個微服務,可以被其他Pod或 Service訪問,如下圖所示。

注意上圖在 ku8-reids-slave的容器中有MASTER_ADDR的環(huán)境變量,這是Redis Master 的地址,這里填寫的是“ku8-redis-master”,它是Redis Master Service 的名稱,之前說過:Service的名稱就是它的DNS域名,所以Redis Slave容器可以通過這個DNS與Redis Master Service進行通信,以實現(xiàn)Redis 主從同步功能。
Kubernetes 的核心概念就是Service、Pod 及 RC/Deployment。圍繞著這三個核心概念,Kubernetes實現(xiàn)了有史以來最強大的基于容器技術的微服務架構平臺。比如,在上述Redis集群中,如果我們希望組成一主三從的集群,則只要將控制Redis Slave的 ReplicationController中的replicas改為3,或者用kubectrl scale命令行功能實現(xiàn)擴容即可。命令如下,我們發(fā)現(xiàn),服務的水平擴容變得如此方便:
- kubectl scale --replicas=3 rc/ku8-redis-slave
 
不僅如此,Kubernetes還實現(xiàn)了水平自動擴容的高級特性——HPA ( Horizontal PodAutoscaling ),其原理是基于Pod 的性能度量參數(CPU utilization和 custom metrics)對RC/Deployment管理的Pod進行自動伸縮。舉個例子,假如我們認為上述Redis Slave集群對應的Pod也對外提供查詢服務,服務期間Pod的 CPU利用率會不斷變化,在這些Pod 的CPU平均利用率超過80%后,就會自動擴容,直到CPU利用率下降到80%以下或者最多達到5個副本位置,而在請求的壓力減小后,Pod的副本數減少為1個,用下面的HPA命令即可實現(xiàn)這一目標:
- kubectl autoscale rc ku8-redis-slave --min=1 --max=5 --cpu-percent=80
 
除了很方便地實現(xiàn)微服務的水平擴容功能,Kubernetes還提供了使用簡單、功能強大的微服務滾動升級功能(rolling update),只要一個簡單的命令即可快速完成任務。舉個例子,假如我們要將上述Redis Slave服務的鏡像版本從devopsbq/redis-slave升級為leader/redis-slave,則只要執(zhí)行下面這條命令即可:
- kubectl rolling-update ku8-redis-slave --image=leader/redis-slave
 
滾動升級的原理如下圖所示,Kubernetes在執(zhí)行滾動升級的過程中,會創(chuàng)建一個新的RC,這個新的RC使用了新的Pod鏡像,然后Kubernetes每隔一段時間就將舊RC的replicas數減少一個,導致舊版本的Pod副本數減少一個,然后將新RC的replicas數增加一個,于是多出一個新版本的Pod副本,在升級的過程中 Pod副本數基本保持不變,直到最后所有的副本都變成新的版本,升級才結束。

Kubernetes的組成與原理
Kubernetes集群本身作為一個分布式系統(tǒng),也采用了經典的Master-Slave架構,如下圖所示,集群中有一個節(jié)點是Master節(jié)點,在其上部署了3個主要的控制程序:API Sever、ControllerManager 及 Scheduler,還部署了Etcd進程,用來持久化存儲Kubernetes管理的資源對象(如Service、Pod、RC/Deployment)等。

集群中的其他節(jié)點被稱為Node節(jié)點,屬于工人(Worker 節(jié)點),它們都由Master 節(jié)點領導,主要負責照顧各自節(jié)點上分配的Pod副本。下面這張圖更加清晰地表明了Kubernetes各個進程之間的交互關系。

從上圖可以看到,位于中心地位的進程是API Server,所有其他進程都與它直接交互,其他進程之間并不存在直接的交互關系。那么,APl Server的作用是什么呢?它其實是Kubernetes的數據網關,即所有進入Kubernetes 的數據都是通過這個網關保存到Etcd數據庫中的,同時通過API Server將Eted里變化的數據實時發(fā)給其他相關的Kubernetes進程。API Server 以REST方式對外提供接口,這些接口基本上分為以下兩類。
- 所有資源對象的CRUD API:資源對象會被保存到Etcd中存儲并提供Query接口,比如針對Pod、Service及RC等的操作。
 - 資源對象的 Watch API:客戶端用此API來及時得到資源變化的相關通知,比如某個Service 相關的Pod實例被創(chuàng)建成功,或者某個Pod 狀態(tài)發(fā)生變化等通知,Watch API主要用于Kubernetes 中的高效自動控制邏輯。
 
下面是上圖中其他Kubernetes進程的主要功能。
- controller manager:負責所有自動化控制事物,比如RC/Deployment的自動控制、HPA自動水平擴容的控制、磁盤定期清理等各種事務。
 - scheduler:負責Pod 的調度算法,在一個新的Pod被創(chuàng)建后,Scheduler根據算法找到最佳 Node節(jié)點,這個過程也被稱為Pod Binding。
 - kubelet:負責本Node節(jié)點上Pod實例的創(chuàng)建、監(jiān)控、重啟、刪除、狀態(tài)更新、性能采集并定期上報 Pod 及本機 Node節(jié)點的信息給Master節(jié)點,由于Pod實例最終體現(xiàn)為Docker'容器,所以Kubelet還會與Docker交互。
 - kube-proxy:為 Service的負載均衡器,負責建立Service Cluster IP 到對應的Pod實例之間的NAT轉發(fā)規(guī)則,這是通過Linux iptables實現(xiàn)的。
 
在理解了Kubernetes各個進程的功能后,我們來看看一個RC 從YAML定義到最終被部署成多個Pod 及容器背后所發(fā)生的事情。為了很清晰地說明這個復雜的流程,這里給出一張示意圖。

首先,在我們通過kubectrl create命令創(chuàng)建一個RC(資源對象)時,kubectrl通過Create RC這個REST接口將數據提交到APl Server,隨后API Server將數據寫入Etcd里持久保存。與此同時,Controller Manager監(jiān)聽(Watch)所有RC,一旦有RC被寫入Etcd中,Controller Manager就得到了通知,它會讀取RC的定義,然后比較在RC中所控制的Pod 的實際副本數與期待值的差異,然后采取對應的行動。此刻,Controller Manager 發(fā)現(xiàn)在集群中還沒有對應的Pod實例,就根據RC里的Pod模板(Template)定義,創(chuàng)建一個Pod并通過API Server保存到Etcd中。類似地,Scheduler進程監(jiān)聽所有 Pod,一旦發(fā)現(xiàn)系統(tǒng)產生了一個新生的Pod,就開始執(zhí)行調度邏輯,為該Pod 安排一個新家(Node),如果一切順利,該Pod就被安排到某個Node節(jié)點上,即Binding to a Node。接下來,Scheduler進程就把這個信息及 Pod狀態(tài)更新到Etcd里,最后,目標Node節(jié)點上的Kubelet監(jiān)聽到有新的Pod被安排到自己這里來了,就按照Pod里的定義,拉取容器的鏡像并且創(chuàng)建對應的容器。在容器成功創(chuàng)建后,Kubelet進程再把 Pod的狀態(tài)更新為Running 并通過API Server更新到 Etcd 中。如果此 Pod還有對應的Service,每個Node上的Kube-proxy進程就會監(jiān)聽所有Service及這些Service對應的Pod實例的變化,一旦發(fā)現(xiàn)有變化,就會在所在 Node節(jié)點上的 iptables 里增加或者刪除對應的NAT轉發(fā)規(guī)則,最終實現(xiàn)了Service的智能負載均衡功能,這一切都是自動完成的,無須人工干預。
那么,如果某個Node'宕機,則會發(fā)生什么事情呢?假如某個Node宕機一段時間,則因為在此節(jié)點上沒有Kubelet進程定時匯報這些Pod 的狀態(tài),因此這個Node 上的所有Pod'實例都會被判定為失敗狀態(tài),此時Controller Manager會將這些Pod刪除并產生新的Pod實例,于是這些Pod被調度到其他 Node 上創(chuàng)建出來,系統(tǒng)自動恢復。
本節(jié)最后說說Kube-proxy的演變,如下圖所示。

Kube-proxy一開始是一個類似于HAProxy的代理服務器,實現(xiàn)了基于軟件的負載均衡功能,將Client 發(fā)起的請求代理到后端的某個Pod 上,可以將其理解為Kubernetes Service的負載均衡器。Kube-proxy最初的實現(xiàn)機制是操控 iptables規(guī)則,將訪問Cluster IP 的流量通過NAT方式重定向到本機的Kube-proxy,在這個過程中涉及網絡報文從內核態(tài)到用戶態(tài)的多次復制,因此效率不高。Kube-proxy 之后的版本改變了實現(xiàn)方式,在生成 iptables規(guī)則時,直接NAT 到目標Pod地址,不再通過Kube-proxy進行轉發(fā),因此效率更高、速度更快,采用這種方式比采用客戶端負載均衡方式效率稍差一點,但編程簡單,而且與具體的通信協(xié)議無關,適用范圍更廣。此時,我們可以認為Kubernetes Service基于 iptables機制來實現(xiàn)路由和負載均衡機制,從此以后,Kube-proxy已不再是一個真正的“proxy"”,僅僅是路由規(guī)則配置的一個工具類“代理”。
基于iptables 實現(xiàn)的路由和負載均衡機制雖然在性能方面比普通Proxy提升了很多,但也存在自身的固有缺陷,因為每個Service都會產生一定數量的 iptables 規(guī)則。在Service數量比較多的情況下,iptables 的規(guī)則數量會激增,對iptables的轉發(fā)效率及對Linux內核的穩(wěn)定性都造成一定的沖擊。因此很多人都在嘗試將IPVS(IP虛擬服務器)代替iptables。Kubernetes 從 1.8版本開始,新增了Kube-proxy對IPVS的支持,在1.11版本中正式納入 GA。與 iptables 不同, IPVS本身就被定位為Linux官方標準中TCP/UDP服務的負載均衡器解決方案,因此非常適合代替iptables來實現(xiàn) Service的路由和負載均衡。
此外,也有一些機制來代替 Kube-proxy,比如Service Mesh 中的 SideCar 完全代替了Kube-proxy的功能。在 Service 都基于HTTP接口的情況下,我們會有更多的選擇方式,比如Ingress、Nginx 等。
基于Kubernetes 的 PaaS平臺
PaaS其實是一個重量級但不怎么成功的產品,受限于多語言支持和開發(fā)模式的僵硬,但近期又隨著容器技術及云計算的發(fā)展,重新引發(fā)了人們的關注,這是因為容器技術徹底解決了應用打包部署和自動化的難題?;谌萜骷夹g重新設計和實現(xiàn)的PaaS平臺,既提升了平臺的技術含量,又很好地彌補了之前PaaS平臺難用、復雜、自動化水平低等缺點。
OpenShift是由 RedHat公司于2011年推出的PaaS云計算平臺,在Kubernetes推出之前,OpenShift 就已經演變?yōu)閮蓚€版本(v1與v2),但在 Kubernetes推出之后,OpenShift的第3個版本v3放棄了自己的容器引擎與容器編排模塊,轉而全面擁抱Kubernetes。
Kubernetes 擁有如下特性。
- Pod(容器)可以讓開發(fā)者將一個或多個容器整體作為一個“原子單元”進行部署。
 - 采用固定的Cluster IP及內嵌的DNS這種獨特設計思路的服務發(fā)現(xiàn)機制,讓不同的Service很容易相互關聯(lián)(Link)。
 - RC可以保證我們關注的Pod副本的實例數量始終符合我們的預期。
 - 非常強大的網絡模型,讓不同主機上的Pod能夠相互通信。
 - 支持有狀態(tài)服務與無狀態(tài)服務,能夠將持久化存儲也編排到容器中以支持有狀態(tài)服務。
 - 簡單易用的編排模型,讓用戶很容易編排一個復雜的應用。
 
國內外已經有很多公司采用了Kubernetes作為它們的PaaS平臺的內核,所以本節(jié)講解如何基于Kubernetes 設計和實現(xiàn)一個強大的 PaaS平臺。
一個 PaaS平臺應該具備如下關鍵特性。
- 多租戶支持:這里的租戶可以是開發(fā)廠商或者應用本身。
 - 應用的全生命周期管理:比如對應用的定義、部署、升級、下架等環(huán)節(jié)的支持。
 - 具有完備的基礎服務設施:比如單點登錄服務、基于角色的用戶權限服務、應用配置服務、日志服務等,同時PaaS平臺集成了很多常見的中間件以方便應用調用,這些常見的中間件有消息隊列、分布式文件系統(tǒng)、緩存中間件等。
 - 多語言支持:一個好的PaaS平臺可以支持多種常見的開發(fā)語言,例如Java、Node.js、PHP、Python、C++等。
 
接下來,我們看看基于Kubernetes 設計和實現(xiàn)的PaaS平臺是如何支持上述關鍵特性的。
如何實現(xiàn)多租戶
Kubernetes通過Namespace特性來支持多租戶功能。
我們可以創(chuàng)建多個不同的Namespace資源對象,每個租戶都有一個Namespace,在不同的Namespace下創(chuàng)建的Pod、Service 與RC等資源對象是無法在另外一個Namespace下看到的,于是形成了邏輯上的多租戶隔離特性。但單純的Namespace隔離并不能阻止不同Namespace下的網絡隔離,如果知道其他Namespace中的某個 Pod的IP地址,則我們還是可以發(fā)起訪問的,如下圖所示。

針對多租戶的網絡隔離問題,Kubernetes增加了Network Policy這一特性,我們簡單地將它類比為網絡防火墻,通過定義Network Policy資源對象,我們可以控制一個Namespace(租戶)下的Pod被哪些Namespace訪問。假如我們有兩個Namespace,分別為tenant2、tenant3,各自擁有一些Pod,如下圖所示。

假如我們需要實現(xiàn)這些網絡隔離目標: tenant3里擁有role:db標簽的Pod只能被tenant3(本Namespace中)里擁有role:frontend標簽的Pod訪問,或者被tenent2里的任意Pod訪問,則我們可以定義如下圖所示的一個Network Policy資源對象,并通過kubectrl工具發(fā)布到Kubernetes集群中生效即可。

需要注意的是,Kubernetes Network Policy需要配合特定的CNI網絡插件才能真正生效,目前支持Network Policy 的CNI 插件主要有以下幾種。
- Calico:基于三層路由實現(xiàn)的容器網絡方案。
 - Weave Net:基于報文封裝的二層容器解決方案。
 - Romana:類似于Calico的容器網絡方案。
 
Network Policy目前也才剛剛起步,還有很多問題需要去研究和解決,比如如何定義Service的訪問策略?如果Service訪問策略與Pod訪問策略沖突又該如何解決﹖此外,外部服務的訪問策略又該如何定義?總之,在容器領域,相對于計算虛擬化、存儲虛擬化來說,網絡虛擬化中的很多技術才剛剛起步。
Kubernetes 的 Namespace是從邏輯上隔離不同租戶的程序,但多個租戶的程序還是可能被調度到同一個物理機(Node)上的,如果我們希望不同租戶的應用被調度到不同的Node 上,從而做到物理上的隔離,則可以通過集群分區(qū)的方式來實現(xiàn)。具體做法是我們先按照租戶將整個集群劃分為不同的分區(qū)(Partition),如下圖所示,對每個分區(qū)里的所有 Node 都打上同樣的標簽,比如租戶 a(tanenta)的標簽為partition=tenant,租戶 b( tanentb)的標簽為partition= tenantb,我們在調度Pod 的時候可以使用nodeSelector屬性來指定目標Node的標簽,比如下面的寫法表示Pod需要被調度到租戶 a的分區(qū)節(jié)點上:
- nodeSelector:
 - partition: tenanta
 

Kubernetes 分區(qū)與租戶可以有多種對應的設計,上面所說的一個分區(qū)一個租戶的設計是一種典型的設計,也可以將租戶分為大客戶與普通客戶,每個大客戶都有一個單獨的資源分區(qū),而普通客戶可以以N個為一組,共享同一個分區(qū)的資源。
PaaS 平臺的領域模型設計
我們知道,微服務架構下的一個應用通常是由多個微服務所組成的,而我們的Kubernetes通常會部署多個獨立的應用,因此,如果用 Kubernetes建模微服務應用,則我們需要在 PaaS平臺的領域模型中設計出 Application這個領域對象,一個Application包括多個微服務,并且最終在發(fā)布(部署)時會生成對應的Pod、Deployment 及 Service對象,如下圖所示。

如下所示是有更多細節(jié)的領域模型圖,Kubernetes中的 Node、Namespace分別被建模為K8sNode與TanentNS,分區(qū)則被建模為ResPartition對象,每個分區(qū)都可以包括1到N個TanentNS,即一個或多個租戶(Tanent〉使用。每個租戶都包括一些用戶賬號(User),用來定義和維護本租戶的應用(Application)。為了分離權限,可以使用用戶組(User Group)的方式,同時可以增加標準的基于角色的權限模型。

上圖中的Service領域對象并不是Kubernetes Service,而是包括了Kubernetes Service及相關RC/Deployment的一個“復合結構”。在Service領域對象中只包括了必要的全部屬性,在部署應用時會生成對應的Kubernetes Service和RC/Deployment實例。下圖給出了Service的定義界面(原型)。

我們在界面上完成對一個Application的定義后,就可以發(fā)布應用了。在發(fā)布應用的過程中,先要選擇某個分區(qū),然后程序調用Kubernetes的 API接口,創(chuàng)建此 Application相關的所有Kubernetes資源對象,然后查詢Pod的狀態(tài)即可判斷是否發(fā)布成功及失敗的具體原因。下面給出了Application從定義到發(fā)布的關鍵模塊的設計示意圖。

我們知道Kubernetes是基于容器技術的微服務架構平臺,每個微服務的二進制文件都被打包成標準的Docker鏡像,因此應用的全生命周期管理過程的第一步,就是從源碼到Docker鏡像的打包,而這個過程很容易實現(xiàn)自動化,我們既可以通過編程方式實現(xiàn),也可以通過成熟的第三方開源項目實現(xiàn),這里推薦使用Jenkins。下圖是Jenkins實現(xiàn)鏡像打包流程的示意圖,考慮到Jenkins的強大和用戶群廣泛,很多PaaS平臺都集成了Jenkins 以實現(xiàn)應用的全生命周期管理功能。

PaaS 平臺的基礎中間件
一個完備的PaaS平臺必須集成和提供一些常見的中間件,以方便應用開發(fā)和托管運行。首先,第1類重要的基礎中間件是 ZooKeeper,ZooKeeper非常容易被部署到Kubernetes集群中,在Kubernetes 的 GitHub上有一個YAML參考文件。ZooKeeper除了給應用使用,也可以作為PaaS平臺面向應用提供的“集中配置服務”的基礎組成部分,如下圖所示。

此外,考慮到很多開源分布式系統(tǒng)都采用了ZooKeeper來管理集群,所以我們也可以部署一個標準命名的ZooKeeper Service,以供這些集群共享使用。
第2類重要的中間件就是緩存中間件了,比如我們之前提到的Redis 及 Memcache,它們也很容易被部署到Kubernetes集群中,作為基礎服務提供給第三方應用使用。在Kubernetes的入門案例中有一個GuestBook例子,演示了在PHP頁面中訪問Redis主從集群的方法,即使是復雜的Codis集群,也可以被成功部署到Kubernetes集群中。此外,RedHat 的J2EE內存緩存中間件Infinispan也有Kubernetes集群部署的案例。
第3類重要的中間件是消息隊列中間件,不管是經典的ActiveMQ、RabbitMQ還是新一代的Kafka,這些消息中間件也很容易被部署到Kubernetes集群中提供服務。下圖是一個3節(jié)點的RabbitMQ集群在Kubernetes平臺上的建模示意圖。為了組成RabbitMQ集群,我們定義了3個Pod,每個Pod都對應一個Kubernetes Service,從而映射到3個RabbitMQ Server 實例,此外,我們定義了一個單獨的Service,名為 ku8-rabbit-mq-server,此 Service對外提供服務,并且對應到上述3個Pod 上,于是每個Pod都有兩個標簽。

第4類重要的中間件是分布式存儲中間件,目前在Kubernetes集群上可以使用Ceph集群提供的塊存儲服務及GlusterFS提供的分布式文件存儲服務,其中 GlusterFS被RedHat的OpenShift平臺建議為文件存儲的標配存儲系統(tǒng),下面是這種方案的示意圖。

在 RedHat的方案中,GlusterFS集群被部署在獨立的服務器集群上,這適用于較大的集群規(guī)模及對性能和存儲要求高的場景。在機器有限的情況下,我們也可以把Kubernetes集群的每個Node節(jié)點都當作一個GlusterFS的存儲節(jié)點,并采用DaemonSet的調度方式將GlusterFS部署到Kubernetes集群上,具體的部署方式在Kubernetes 的 GitHub網站中有詳細的說明文檔,以Pod方式部署GlusterFS集群也使得GlusterFS 的部署和運維都變得非常簡單。
提供全文檢索能力的ElasticSearch集群也很容易被部署到Kubernetes中,前面提到的日志集中收集與查詢分析的三件套ELK目前基本上全部以Pod的方式部署,以實現(xiàn)Kubernetes集群日志與用戶應用日志的統(tǒng)一收集、查詢、分析等功能。
在當前熱門的大數據領域中,很多系統(tǒng)也都能以容器化方式部署到Kubernetes集群中,比如Hadoop、HBase、Spark 及 Storm等重量級集群。下一節(jié)將給出 Storm On Kubernetes 的建模方案,并且將其部署到Kubernetes集群中,最后提交第6章的WordCountTopology 作業(yè)并且觀察運行結果。
Storm On Kubernetes 實戰(zhàn)
通過第6章的學習,我們知道一個 Storm集群是由ZooKeeper、Nimbus (Master)及一些Supervisor (Slave)節(jié)點組成的,集群的配置文件默認保存在 conf/storm.yaml中,最關鍵的配置參數如下。
- storm.zookeeper.servers: ZooKeeper集群的節(jié)點IP地址列表。
 - nimbus.seeds:Nimbus的IP地址。
 - supervisor.slots.ports:Supervisor 中的Worker 監(jiān)聽端口列表。
 
從上述關鍵配置信息及Storm集群的工作原理來看,我們首先需要將ZooKeeper建模為Kubernetes Service,以便提供一個固定的域名地址,使得Nimbus 與Supervisor能夠訪問它。下面是ZooKeeper 的建模過程(為了簡單起見,我們只建模一個ZooKeeper節(jié)點)。
首先,定義ZooKeeper對應的Service,Service名稱為ku8-zookeeper,關聯(lián)的標簽為app=ku8-zookeeper 的Pod:
- apiVersion: v1kind: Servicemetadata:
 - name: ku8-zookeeperspec:
 - ports:
 - name: clientport: 2181selector:
 - app: ku8-zookeeper
 
其次,定義ZooKeeper對應的RC:
- apiversion: v1
 - kind: Replicationcontrollermetadata:
 - name: ku8-zookeeper-lspec:
 - replicas: 1template:
 - metadata:
 - labels:
 - app: ku8-zookeeperspec:
 - containers:
 - name: server
 - image: jplock/ zookeeper
 - imagePu1lPolicy: IfNotPresentports:
 - -containerPort: 2181
 
接下來,我們需要將Nimbus也建模為Kubernetes Service,因為Storm客戶端需要直接訪問Nimbus服務以提交拓撲任務,所以在conf/storm.yaml中存在nimbus.sceds參數。由于Nimbus在6627端口上提供了基于Thrift的 RPC服務,因此對Nimbus服務的定義如下:
- apiversion: v1kind: Servicemetadata:
 - name: nimbusspec:
 - selector:
 - app: storm-nimbusports:
 - -name: nimbus-rpc
 - port: 6627
 - targetPort:6627
 
考慮到在storm.yaml配置文件中有很多參數,所以為了實現(xiàn)任意參數的可配置性,我們可以用Kubernetes的Config Map資源對象來保存storm.yaml,并映射到Nimbus(以及 Supervisor)節(jié)點對應的Pod實例上。下面是在本案例中使用的storm.yaml 文件(storm-conf.yaml)的內容:
- storm.zookeeper.servers: [ku8-zookeeper]
 - nimbus.seeds: [nimbus]storm.log.dir: "log"
 - storm. local.dir: "storm-data"supervisor.slots.ports:
 - -6700
 - 670167026703
 
將上述配置文件創(chuàng)建為對應的ConfigMap ( storm-config),可以執(zhí)行下面的命令:
- kubelet create configmap storm-config --from-file=storm-conf.yaml
 
然后,storm-config 就可以被任意Pod 以 Volume方式掛載到容器內的任意指定路徑上了。接下來,我們可以繼續(xù)建模 Nimbus服務對應的Pod。在從Docker Hub上搜尋相關 Storm鏡像并進行分析后,我們選擇了Docker 官方提供的鏡像storm:1.0。相對于其他Storm鏡像來說,官方維護的這個鏡像有以下優(yōu)點。
- Storm版本新。
 - Storm整體只有一個鏡像,通過容器的command 命令參數來決定啟動的是哪種類型的節(jié)點,比如Nimbus主節(jié)點、Nimbus-ui管理器或者Supervisor 從節(jié)點。
 - 標準化的Storm進程啟動方式,可以將conf/storm.yaml配置文件映射到容器外,因此可以采用Kubernetes 的 ConfigMap特性。
 
采用storm:1.0鏡像定義Nimbus Pod的YAML文件如下:
- apiversion: v1kind: Pod
 - metadata:
 - name: nimbuslabels:
 - app: storm-nimbusspec:
 - volumes:
 - name: config-volumeconfigMap:
 - name: storm-configitems:
 - 一key:storm-conf.yaml
 - path:storm.yaml
 - containers:
 - - name: nimbus
 - image: storm: 1.0
 - imagePullPolicy: IfNotPresentports:
 - -containerPort: 6627
 - command:【"storm" ,"nimbus" ]volumeMounts:
 - - name: config-volumemountPath: /conf
 - restartPolicy: Always
 
這里我們需要關注兩個細節(jié):第1個細節(jié)是ConfigMap 的使用方法,首先要把之前定義的ConfigMap ——storm-config映射為Pod的一個Volume,然后在容器中將此Volume掛接到某個具體的路徑上;第2個細節(jié)是容器的參數 command,上面的command: [ "storm" , "nimbus"]表示此容器啟動的是nimus進程。
類似地,我們定義storm-ui服務,這是一個Web管理程序,提供了圖形化的Storm管理功能,因為需要在Kubernetes集群之外訪問它,所以我們通過NodePort方式映射8080端口到主機上的30010。storm-ui服務的YAML定義文件如下:
- apiversion: v1kind: Servicemetadata:
 - name: storm-uispec:
 - type: NodePortselector:
 - app:storm-uiports:
 - -name: web
 - port: 8080
 - targetPort: 8080nodePort:30010
 
最后,我們來建模Supervisor。Supervisor看似不需要被建模為Service,因為Supervisor 不會被主動調用,但實際上Supervisor節(jié)點之間會相互發(fā)起通信,因此Supervisor節(jié)點注冊到ZooKeeper 上的地址必須能被相互訪問,在 Kubernetes平臺上有兩種方式解決此問題。
第1種方式,Supervisor節(jié)點注冊到ZooKeeper上時,不用主機名(Pod名稱),而是采用Pod的P地址。
第2種方式,用Headless Service模式,每個Supervisor節(jié)點都被建模為一個HeadlessService,并且確保Supervisor節(jié)點的容器名稱(主機名)與Headless Service的名稱一樣,此時Supervisor節(jié)點注冊到ZooKeeper 上的地址就跟Headless Service名稱一樣了,Supervisor節(jié)點之間都能用對方的Headless Service的域名進行通信。
其中,第1種方式需要修改Supervisor的啟動腳本和對應的參數才能進行,實現(xiàn)起來比較麻煩,第②種方式無須修改鏡像就能實現(xiàn),所以我們采用了第﹖種方式建模。下面是某個Supervisor節(jié)點的Service定義,注意 clusterIP: None的特殊寫法:
- apiversion: v1
 - kind: Servicemetadata:
 - name:storm-supervisorspec:
 - clusterIP:Noneselector:
 - app:storm-supervisorports:
 - - port: 8000
 
storm-supervisor 這個節(jié)點對應的 Pod定義如下,需要注意Pod 的名稱為storm-supervisor,并且command的值為[ "storm", "supervisor"]:
- apiversion: v1kind: Pod
 - metadata:
 - name: storm-supervisorlabels:
 - app: storm-supervisorspec:
 - volumes:
 - name: config-volumeconfigMap:
 - name: storm-configitems:
 - 一key:storm-conf.yaml
 - path: storm.yaml
 - containers:
 - name: storm-supervisorimage: storm: 1.0
 - imagePullPolicy: IfNotPresent
 - command:["storm", "supervisor" ]volumeMounts:
 - -name: config-volumemountPath: /conf
 - restartPolicy:Always
 
我們可以定義多個Supervisor 節(jié)點,比如在本案例中定義了兩個Supervisor節(jié)點。在成功部署到Kubernetes集群后,我們通過Storm UI的30010端口進入Storm的管理界面,可以看到如下界面。

下面這個截圖驗證了兩個Supervisor 節(jié)點也可以被成功注冊在集群中,我們看到每個節(jié)點都有4個Slot,這符合我們在storm.yaml中的配置。

至此,Storm集群在Kubernetes 上的建模和部署已經順利完成了。接下來我們看看如何在Storm集群中提交之前學習過的WordCountTopology作業(yè)并且觀察它的運行情況。
首先,我們可以去 https:/ljar-download.com/下載編譯好的 WordCountTopology 作業(yè)的JAR文件
storm-starter-topologies-1.0.3.jar,然后通過Storm Client工具將該Topology作業(yè)提交到Storm集群中,提交作業(yè)的命令如下:
- storm jar/userlib/storm-starter-topologies-1.0.3.jar org.apache.storm.starter.ordcountTopology topology
 
由于在storm:1.0鏡像中已經包括了Storm Client 工具,所以最簡便的方式是定義一個Pod,然后把下載下來的
storm-starter-topologies-1.0.3.jar作為Volume映射到Pod里的/userlib/目錄下。將容器的啟動命令設置為上述提交作業(yè)的命令即可實現(xiàn),下面是此Pod 的YAML定義:
- apiversion: v1
 - kind: Podmetadata:
 - name: storm-topo-examplespec:
 - volumes:
 - name:user-libhostPath:
 - path: /root/stormname: config-volumeconfigMap:
 - name:storm-configitems:
 - -key: storm-conf.yaml
 - path: storm. yaml
 - containers:
 - name: storm-topo-exampleimage: storm: 1.0
 - imagePullPolicy: IfNotPresent
 - command: [ "storm","jar", "/userlib/storm-starter-topologies-1.0.3.jar",
 - "org.apache.storm.starter.WordCountTopology", "topology" ]
 - volumeMounts:
 - - name: config-volumemountPath: /conf
 - name:user-lib
 - mountPath: /userlib
 - restartPolicy: Never
 
上述定義有如下關鍵點。
- 將storm-starter-topologies-1.0.3.jar 放在主機的/root/storm目錄中。
 - 容器的啟動命令是storm client,提交Topology 作業(yè)。
 - Pod重啟策略為Never,因為只要提交完Topology 作業(yè)即可。
 
創(chuàng)建上述 Pod以后,我們查看該Pod 的日志,如果看到下面這段輸出,則表明WordCountTopology的拓撲作業(yè)已經被成功提交到Storm集群中了。

接下來,我們進入Storm UI去看看作業(yè)的執(zhí)行情況。下圖是WordCountTopology的匯總信息,狀態(tài)為Active,運行了8分鐘,占用了3個Worker進程,總共運行了28個Task。

在成功提交到Storm集群后,我們可以進入Supervisor節(jié)點(Pod)查看拓撲作業(yè)的日志輸出,作業(yè)的日志輸出在目錄/log/workers-artifacts下,每個拓撲作業(yè)都有一個單獨的文件夾存放日志,我們搜索WordCountTopology 的最后一個 Bolt——統(tǒng)計發(fā)送Tuple的日志,可以看到如下結果,即每個Word(字)都被統(tǒng)計輸出了。

下面這個界面給出了WordCountTopology 的詳細信息,分別顯示了拓撲里所有Spout的相關信息,例如生成了幾個Task、總共發(fā)送了多少個Tuple、失敗了多少個,以及所有 Bolt 的相關信息,例如處理了多少個 Tuple、處理的延時等統(tǒng)計信息,有助于我們分析Topology 作業(yè)的性能瓶頸和改進的可能性。

除了上面的列表信息,Storm UI還提供了展示Stream運行情況的拓撲圖,如下圖所示,我們看到數據流從spout節(jié)點發(fā)出,經過 split 節(jié)點處理時用了3.13ms,然后抵達count節(jié)點,count節(jié)點的處理耗時為0.06ms。

Storm 的 Topology 作業(yè)一旦運行起來就不會停止,所以你會看到下面界面中的Tuple 的統(tǒng)計數字在不斷增加,因為WordCountTopology的 Spout 節(jié)點在不斷生成Tuple,所以如果我們需要停止作業(yè),則可以單擊圖中的 Deactvate按鈕掛起作業(yè),或者終止作業(yè)。
















 
 
 







 
 
 
 