谷歌于2015年正式推出的Kubernetes開源項目目前已經吸引了眾多IT公司的關注,這些公司包括Redhat、CoreOS、IBM、惠普等知名IT公司,也包括國內如華為、時速云等公司。為什么Kubernetes會引發這么多公司的關注?最根本的原因是Kubernetes是新一代的基于先進容器技術的微服務架構平臺,它將當前火爆的容器技術與微服務架構兩大吸引眼球的技術點完美的融為一體,并且切切實實的解決了傳統分布式系統開發過程中長期存在的痛點問題。
本文假設您已經很熟悉并掌握了Docker技術,這里不會再花費篇幅介紹它。正是通過輕量級的容器隔離技術,Kubernetes實現了“微服務”化的特性,同時借助于Docker提供的基礎能力,使得平臺的自動化能力得以實現。
概念與原理
作為一個架構師來說,我們做了這么多年的分布式系統,其實我們真正關心的并不是服務器、交換機、負載均衡器、監控與部署這些事物,我們真正關心的是“服務”本身,并且在內心深處,我們渴望能實現圖1所示的下面的這段“愿景”:
我的系統中有ServiceA、ServiceB、ServiceC三種服務,其中ServiceA需要部署3個實例、而ServiceB與ServiceC各自需要部署5個實例,我希望有一個平臺(或工具)幫我自動完成上述13個實例的分布式部署,并且持續監控它們。當發現某個服務器宕機或者某個服務實例故障的時候,平臺能夠自我修復,從而確保在任何時間點,正在運行的服務實例的數量都是我所預期的。這樣一來,我和我的團隊只需關注服務開發本身,而無需再為頭疼的基礎設施和運維監控的事情而煩惱了。
圖1 分布式系統架構愿景
直到Kubernetes出現之前,沒有一個公開的平臺聲稱實現了上面的“愿景”,這一次,又是谷歌的神作驚艷了我們。Kubernetes讓團隊有更多的時間去關注與業務需求和業務相關的代碼本身,從而在很大程度上提升了整個軟件團隊的工作效率與投入產出比。
Kubernetes里核心的概念只有以下幾個:
Service
Pod
Deployments(RC)
Service表示業務系統中的一個“微服務”,每個具體的Service背后都有分布在多個機器上的進程實例來提供服務,這些進程實例在Kubernetes里被封裝為一個個Pod,Pod基本等同于Docker Container,稍有不同的是Pod其實是一組密切捆綁在一起并且“同生共死”的Docker Container,從模型設計的角度來說,的確存在一個服務實例需要多個進程來提供服務并且它們需要“在一起” 的情況。
Kubernetes的Service與我們通常所說的“Service”有一個明顯的的不同,前者有一個虛擬IP地址,稱之為“ClusterIP”,服務與服務之間“ClusterIP+服務端口”的方式進行訪問,而無需一個復雜的服務發現的API。這樣一來,只要知道某個Service的ClusterIP,就能直接訪問該服務,為此,Kubernetes提供了兩種方式來解決ClusterIP的發現問題:
第一種方式是通過環境變量,比如我們定義了一個名稱為ORDER_SERVICE 的Service ,分配的ClusterIP為10.10.0.3 ,則在每個服務實例的容器中,會自動增加服務名到ClusterIP映射的環境變量:ORDER_SERVICE_SERVICE_HOST=10.10.0.3,于是程序里可以通過服務名簡單獲得對應的ClusterIP。
第二種方式是通過DNS,這種方式下,每個服務名與ClusterIP的映射關系會被自動同步到Kubernetes集群里內置的DNS組件里,于是直接通過對服務名的DNS Lookup機制就找到對應的ClusterIP了,這種方式更加直觀。
由于Kubernetes的Service這一獨特設計實現思路,使得所有以TCP /IP 方式進行通信的分布式系統都能很簡單的遷移到Kubernetes平臺上了。如圖2所示,當客戶端訪問某個Service的時候,Kubernetes內置的組件kube-proxy透明的實現了到后端Pod的流量負載均衡、會話保持、故障自動恢復等高級特性。
圖2 Kubernetes負載均衡原理
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-master
spec:
ports:
- port: 6379
selector:
app: ku8-redis-master
下面是對應的Pod的定義,注意到它的labels屬性的內容:
apiVersion: v1
kind: Pod
metadata:
name: ku8-redis-master
labels:
app: ku8-redis-master
spec:
containers:
- name: server
image: redis
ports:
- containerPort: 6379
restartPolicy: Never
最后,我們來看看Deployment/RC的概念,它的作用是用來告訴Kubernetes,某種類型的Pod(擁有某個特定標簽的Pod)需要在集群中創建幾個副本實例,Deployment/RC的定義其實是Pod創建模板(Template)+Pod副本數量的聲明(replicas):
apiVersion: v1
kind: ReplicationController
metadata:
name: ku8-redis-slave
spec:
replicas: 2
template:
metadata:
labels:
app: ku8-redis-slave
spec:
containers:
- name: server
image: devopsbq/redis-slave
env:
- name: MASTER_ADDR
value: ku8-redis-master
ports:
- containerPort: 6379
Kubernetes開發指南
本節我們以一個傳統的Java應用為例,來說明如何將其改造遷移到Kubernetes的先進微服務架構平臺上來。
如圖3所示,我們的這個示例程序是一個跑在Tomcat里的Web應用,為了簡化,沒有用任何框架,直接在JSP頁面里通過JDBC操作數據庫。
圖3 待改造的Java Web應用
上述系統中,我們將MySQL服務與Web應用分別建模為Kubernetes中的一個Service,其中MySQL服務的Service定義如下:
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql_pod
MySQL服務對應的Deployment/RC的定義如下:
apiVersion: v1
kind: ReplicationController
metadata:
name: mysql-deployment
spec:
replicas: 1
template:
metadata:
labels:
app: mysql_pod
spec:
containers:
- name: mysql
image: mysql
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
下一步,我們需要改造Web應用中獲取MySQL地址的這段代碼,從容器的環境變量中獲取上述MySQL服務的IP與Port:
String ip=System.getenv("MYSQL_SERVICE_HOST");
String port=System.getenv("MYSQL_SERVICE_PORT");
ip=(ip==null)?"localhost":ip;
port=(port==null)?"3306":port;
conn = java.sql.DriverManager.getConnection("jdbc:mysql://"+ip+":"+port+"?useUnicode=true&characterEncoding=UTF-8", "root","123456");
接下來,將此Web應用打包為一個標準的Docker鏡像,名字為k8s_myweb_image,這個鏡像直接從官方Tomcat鏡像上添加我們的Web應用目錄demo到webapps目錄下即可,Dockerfile比較簡單,如下所示:
FROM tomcat
MAINTAINER bestme
ADD demo /usr/local/tomcat/webapps/demo
類似之前的MySQL服務定義,下面是這個Web應用的Service定義:
apiVersion: v1
kind: Service
metadata:
name: hpe-java-web
spec:
type: NodePort
ports:
- port: 8080
nodePort: 31002
selector:
app: hpe_java_web_pod
我們看到這里用了一個特殊的語法:NodePort,并且賦值為31002,詞語法的作用是讓此Web應用容器里的8080端口被NAT映射到kuberntetes里每個Node上的31002端口,從而我們可以用Node的IP和端口31002來訪問Tomcat的8080端口,比如我本機的可以通過http://192.168.18.137:31002/demo/來訪問這個Web應用。
下面是Web應用的Service對應的Deployment/RC的定義:
apiVersion: v1
kind: ReplicationController
metadata:
name: hpe-java-web-deployement
spec:
replicas: 1
template:
metadata:
labels:
app: hpe_java_web_pod
spec:
containers:
- name: myweb
image: k8s_myweb_image
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
定義好所有Service與對應的Deployment/RC描述文件后(總共4個yaml文件),我們可以通過Kubernetes的命令行工具kubectrl –f create xxx.yaml提交到集群里,如果一切正常,Kubernetes會在幾分鐘內自動完成部署,你會看到相關的資源對象都已經創建成功:
-bash-4.2# kubectl get svc
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hpe-java-web 10.254.183.22 nodes 8080/TCP 36m
kubernetes 10.254.0.1 443/TCP 89d
mysql 10.254.170.22 3306/TCP 36m
-bash-4.2# kubectl get pods
NAME READY STATUS RESTARTS AGE
hpe-java-web-deployement-q8t9k 1/1 Running 0 36m
mysql-deployment-5py34 1/1 Running 0 36m
-bash-4.2# kubectl get rc
NAME DESIRED CURRENT AGE
hpe-java-web-deployement 1 1 37m
mysql-deployment 1 1 37m
結束語
從上面步驟來看,傳統應用遷移改造到Kubernetes上還是比較容易的,而借助于Kubernetes的優勢,即使一個小的開發團隊,也能在系統架構和運維能力上迅速接近一個大的研發團隊的水平。
此外,為了降低Kubernetes的應用門檻,我們(惠普中國CMS研發團隊)開源了一個Kubernetes的管理平臺Ku8 eye,項目地址為https://github.com/bestcloud/ku8eye,Ku8 eye很適合用作為小公司的內部PaaS應用管理平臺,其功能架構類似圖4所示的Ku8 Manager企業版,Ku8 eye采用Java開發完成,是目前唯一一個開源的Kubernetes圖形化管理系統,也希望更多愛好開源和有能力的同行參與進來,將它打造成為國內最好的云計算領域的開源軟件。
圖4 基于Kubernetes的PaaS平臺架構
作者簡介:吳治輝,惠普公司系統架構師,擁有超過15年的軟件研發經驗,專注于電信軟件和云計算方面的軟件研發,同時也是《Kubernetes權威指南》作者之一。