早在 Docker 正式釋出幾個月的時候,LeanCloud 就開始在生產環境大規模使用 Docker,在過去幾年裡 Docker 的技術棧支撐了我們主要的後端架構。這是一篇寫給程式設計師的 Docker 和 Kubernetes 教程,目的是讓熟悉技術的讀者在儘可能短的時間內對 Docker 和 Kubernetes 有基本的瞭解,並通過實際部署、升級、回滾一個服務體驗容器化生產環境的原理和好處。本文假設讀者都是開發者,並熟悉 Mac/Linux 環境,所以就不介紹基礎的技術概念了。命令列環境以 Mac 示例,在 Linux 下只要根據自己使用的發行版和包管理工具做調整即可。
Docker 速成
首先快速地介紹一下 Docker:作為示例,我們在本地啟動 Docker 的守護程式,並在一個容器裡執行簡單的 HTTP 服務。先完成安裝:
$ brew cask install docker
複製程式碼
上面的命令會從 Homebrew 安裝 Docker for Mac,它包含 Docker 的後臺程式和命令列工具。Docker 的後臺程式以一個 Mac App 的形式安裝在 /Applications 裡,需要手動啟動。啟動 Docker 應用後,可以在 Terminal 裡確認一下命令列工具的版本:
$ docker --version
Docker version 18.03.1-ce, build 9ee9f40
複製程式碼
上面顯示的 Docker 版本可能和我的不一樣,但只要不是太老就好。我們建一個單獨的目錄來存放示例所需的檔案。為了儘量簡化例子,我們要部署的服務是用 Nginx 來 serve 一個簡單的 HTML 檔案 html/index.html
。
$ mkdir docker-demo
$ cd docker-demo
$ mkdir html
$ echo `<h1>Hello Docker!</h1>` > html/index.html
複製程式碼
接下來在當前目錄建立一個叫 Dockerfile 的新檔案,包含下面的內容:
FROM nginx
COPY html/* /usr/share/nginx/html
複製程式碼
每個 Dockerfile 都以 FROM ...
開頭。FROM nginx
的意思是以 Nginx 官方提供的映象為基礎來構建我們的映象。在構建時,Docker 會從 Docker Hub 查詢和下載需要的映象。Docker Hub 對於 Docker 映象的作用就像 GitHub 對於程式碼的作用一樣,它是一個託管和共享映象的服務。使用過和構建的映象都會被快取在本地。第二行把我們的靜態檔案複製到映象的 /usr/share/nginx/html
目錄下。也就是 Nginx 尋找靜態檔案的目錄。Dockerfile 包含構建映象的指令,更詳細的資訊可以參考這裡。
然後就可以構建映象了:
$ docker build -t docker-demo:0.1 .
複製程式碼
請確保你按照上面的步驟為這個實驗新建了目錄,並且在這個目錄中執行 docker build
。如果你在其它有很多檔案的目錄(比如你的使用者目錄或者 /tmp
)執行,docker 會把當前目錄的所有檔案作為上下文傳送給負責構建的後臺程式。
這行命令中的名稱 docker-demo
可以理解為這個映象對應的應用名或服務名,0.1
是標籤。Docker 通過名稱和標籤的組合來標識映象。可以用下面的命令來看到剛剛建立的映象:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-demo 0.1 efb8ca048d5a 5 minutes ago 109MB
複製程式碼
下面我們把這個映象執行起來。Nginx 預設監聽在 80 埠,所以我們把宿主機的 8080 埠對映到容器的 80 埠:
$ docker run --name docker-demo -d -p 8080:80 docker-demo:0.1
複製程式碼
用下面的命令可以看到正在執行中的容器:
$ docker container ps
CONTAINER ID IMAGE ... PORTS NAMES
c495a7ccf1c7 docker-demo:0.1 ... 0.0.0.0:8080->80/tcp docker-demo
複製程式碼
這時如果你用瀏覽器訪問 http://localhost:8080,就能看到我們剛才建立的「Hello Docker!」頁面。
在現實的生產環境中 Docker 本身是一個相對底層的容器引擎,在有很多伺服器的叢集中,不太可能以上面的方式來管理任務和資源。所以我們需要 Kubernetes 這樣的系統來進行任務的編排和排程。在進入下一步前,別忘了把實驗用的容器清理掉:
$ docker container stop docker-demo
$ docker container rm docker-demo
複製程式碼
安裝 Kubernetes
介紹完 Docker,終於可以開始試試 Kubernetes 了。我們需要安裝三樣東西:Kubernetes 的命令列客戶端 kubctl、一個可以在本地跑起來的 Kubernetes 環境 Minikube、以及給 Minikube 使用的虛擬化引擎 xhyve。
$ brew install kubectl
$ brew cask install minikube
$ brew install docker-machine-driver-xhyve
複製程式碼
Minikube 預設的虛擬化引擎是 VirtualBox,而 xhyve 是一個更輕量、效能更好的替代。它需要以 root 許可權執行,所以安裝完要把所有者改為 root:wheel
,並把 setuid 許可權開啟:
$ sudo chown root:wheel /usr/local/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve
$ sudo chmod u+s /usr/local/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve
複製程式碼
然後就可以啟動 Minikube 了:
$ minikube start --vm-driver xhyve
複製程式碼
你多半會看到一個警告說 xhyve 會在未來的版本被 hyperkit 替代,推薦使用 hyperkit。不過在我寫這個教程的時候 docker-machine-driver-hyperkit 還沒有進入 Homebrew, 需要手動編譯和安裝,我就偷個懶,仍然用 xhyve。以後只要在安裝和執行的命令中把 xhyve 改為 hyperkit 就可以。
如果你在第一次啟動 Minikube 時遇到錯誤或被中斷,後面重試仍然失敗時,可以嘗試執行 minikube delete
把叢集刪除,重新來過。
Minikube 啟動時會自動配置 kubectl,把它指向 Minikube 提供的 Kubernetes API 服務。可以用下面的命令確認:
$ kubectl config current-context
minikube
複製程式碼
Kubernetes 架構簡介
典型的 Kubernetes 叢集包含一個 master 和很多 node。Master 是控制叢集的中心,node 是提供 CPU、記憶體和儲存資源的節點。Master 上執行著多個程式,包括面向使用者的 API 服務、負責維護叢集狀態的 Controller Manager、負責排程任務的 Scheduler 等。每個 node 上執行著維護 node 狀態並和 master 通訊的 kubelet,以及實現叢集網路服務的 kube-proxy。
作為一個開發和測試的環境,Minikube 會建立一個有一個 node 的叢集,用下面的命令可以看到:
$ kubectl get nodes
NAME STATUS AGE VERSION
minikube Ready 1h v1.10.0
複製程式碼
部署一個單例項服務
我們先嚐試像文章開始介紹 Docker 時一樣,部署一個簡單的服務。Kubernetes 中部署的最小單位是 pod,而不是 Docker 容器。實時上 Kubernetes 是不依賴於 Docker 的,完全可以使用其他的容器引擎在 Kubernetes 管理的叢集中替代 Docker。在與 Docker 結合使用時,一個 pod 中可以包含一個或多個 Docker 容器。但除了有緊密耦合的情況下,通常一個 pod 中只有一個容器,這樣方便不同的服務各自獨立地擴充套件。
Minikube 自帶了 Docker 引擎,所以我們需要重新配置客戶端,讓 docker 命令列與 Minikube 中的 Docker 程式通訊:
$ eval $(minikube docker-env)
複製程式碼
在執行上面的命令後,再執行 docker image ls
時只能看到一些 Minikube 自帶的映象,就看不到我們剛才構建的 docker-demo:0.1 映象了。所以在繼續之前,要重新構建一遍我們的映象,這裡順便改一下名字,叫它 k8s-demo:0.1。
$ docker build -t k8s-demo:0.1 .
複製程式碼
然後建立一個叫 pod.yml 的定義檔案:
apiVersion: v1
kind: Pod
metadata:
name: k8s-demo
spec:
containers:
- name: k8s-demo
image: k8s-demo:0.1
ports:
- containerPort: 80
複製程式碼
這裡定義了一個叫 k8s-demo 的 Pod,使用我們剛才構建的 k8s-demo:0.1 映象。這個檔案也告訴 Kubernetes 容器內的程式會監聽 80 埠。然後把它跑起來:
$ kubectl create -f pod.yml
pod "k8s-demo" created
複製程式碼
kubectl 把這個檔案提交給 Kubernetes API 服務,然後 Kubernetes Master 會按照要求把 Pod 分配到 node 上。用下面的命令可以看到這個新建的 Pod:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
k8s-demo 1/1 Running 0 5s
複製程式碼
因為我們的映象在本地,並且這個服務也很簡單,所以執行 kubectl get pods
的時候 STATUS 已經是 running。要是使用遠端映象(比如 Docker Hub 上的映象),你看到的狀態可能不是 Running,就需要再等待一下。
雖然這個 pod 在執行,但是我們是無法像之前測試 Docker 時一樣用瀏覽器訪問它執行的服務的。可以理解為 pod 都執行在一個內網,我們無法從外部直接訪問。要把服務暴露出來,我們需要建立一個 Service。Service 的作用有點像建立了一個反向代理和負載均衡器,負責把請求分發給後面的 pod。
建立一個 Service 的定義檔案 svc.yml:
apiVersion: v1
kind: Service
metadata:
name: k8s-demo-svc
labels:
app: k8s-demo
spec:
type: NodePort
ports:
- port: 80
nodePort: 30050
selector:
app: k8s-demo
複製程式碼
這個 service 會把容器的 80 埠從 node 的 30050 埠暴露出來。注意檔案最後兩行的 selector 部分,這裡決定了請求會被髮送給叢集裡的哪些 pod。這裡的定義是所有包含「app: k8s-demo」這個標籤的 pod。然而我們之前部署的 pod 並沒有設定標籤:
$ kubectl describe pods | grep Labels
Labels: <none>
複製程式碼
所以要先更新一下 pod.yml,把標籤加上(注意在 metadata:
下增加了 labels
部分):
apiVersion: v1
kind: Pod
metadata:
name: k8s-demo
labels:
app: k8s-demo
spec:
containers:
- name: k8s-demo
image: k8s-demo:0.1
ports:
- containerPort: 80
複製程式碼
然後更新 pod 並確認成功新增了標籤:
$ kubectl apply -f pod.yml
pod "k8s-demo" configured
$ kubectl describe pods | grep Labels
Labels: app=k8s-demo
複製程式碼
然後就可以建立這個 service 了:
$ kubectl create -f svc.yml
service "k8s-demo-svc" created
複製程式碼
用下面的命令可以得到暴露出來的 URL,在瀏覽器裡訪問,就能看到我們之前建立的網頁了。
$ minikube service k8s-demo-svc --url
http://192.168.64.4:30050
複製程式碼
橫向擴充套件、滾動更新、版本回滾
在這一節,我們來實驗一下在一個高可用服務的生產環境會常用到的一些操作。在繼續之前,先把剛才部署的 pod 刪除(但是保留 service,下面還會用到):
$ kubectl delete pod k8s-demo
pod "k8s-demo" deleted
複製程式碼
在正式環境中我們需要讓一個服務不受單個節點故障的影響,並且還要根據負載變化動態調整節點數量,所以不可能像上面一樣逐個管理 pod。Kubernetes 的使用者通常是用 Deployment 來管理服務的。一個 deployment 可以建立指定數量的 pod 部署到各個 node 上,並可完成更新、回滾等操作。
首先我們建立一個定義檔案 deployment.yml:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: k8s-demo-deployment
spec:
replicas: 10
template:
metadata:
labels:
app: k8s-demo
spec:
containers:
- name: k8s-demo-pod
image: k8s-demo:0.1
ports:
- containerPort: 80
複製程式碼
注意開始的 apiVersion
和之前不一樣,因為 Deployment API 沒有包含在 v1 裡,replicas: 10
指定了這個 deployment 要有 10 個 pod,後面的部分和之前的 pod 定義類似。提交這個檔案,建立一個 deployment:
$ kubectl create -f deployment.yml
deployment "k8s-demo-deployment" created
複製程式碼
用下面的命令可以看到這個 deployment 的副本集(replica set),有 10 個 pod 在執行。
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
k8s-demo-deployment-774878f86f 10 10 10 19s
複製程式碼
假設我們對專案做了一些改動,要釋出一個新版本。這裡作為示例,我們只把 HTML 檔案的內容改一下, 然後構建一個新版映象 k8s-demo:0.2:
$ echo `<h1>Hello Kubernetes!</h1>` > html/index.html
$ docker build -t k8s-demo:0.2 .
複製程式碼
然後更新 deployment.yml:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: k8s-demo-deployment
spec:
replicas: 10
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: k8s-demo
spec:
containers:
- name: k8s-demo-pod
image: k8s-demo:0.2
ports:
- containerPort: 80
複製程式碼
這裡有兩個改動,第一個是更新了映象版本號 image: k8s-demo:0.2
,第二是增加了 minReadySeconds: 10
和 strategy
部分。新增的部分定義了更新策略:minReadySeconds: 10
指在更新了一個 pod 後,需要在它進入正常狀態後 10 秒再更新下一個 pod;maxUnavailable: 1
指同時處於不可用狀態的 pod 不能超過一個;maxSurge: 1
指多餘的 pod 不能超過一個。這樣 Kubernetes 就會逐個替換 service 後面的 pod。執行下面的命令開始更新:
$ kubectl apply -f deployment.yml --record=true
deployment "k8s-demo-deployment" configured
複製程式碼
這裡的 --record=true
讓 Kubernetes 把這行命令記到釋出歷史中備查。這時可以馬上執行下面的命令檢視各個 pod 的狀態:
$ kubectl get pods
NAME READY STATUS ... AGE
k8s-demo-deployment-774878f86f-5wnf4 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-6kgjp 0/1 Terminating ... 7m
k8s-demo-deployment-774878f86f-8wpd8 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-hpmc5 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-rd5xw 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-wsztw 1/1 Running ... 7m
k8s-demo-deployment-86dbd79ff6-7xcxg 1/1 Running ... 14s
k8s-demo-deployment-86dbd79ff6-bmvd7 1/1 Running ... 1s
k8s-demo-deployment-86dbd79ff6-hsjx5 1/1 Running ... 26s
k8s-demo-deployment-86dbd79ff6-mkn27 1/1 Running ... 14s
k8s-demo-deployment-86dbd79ff6-pkmlt 1/1 Running ... 1s
k8s-demo-deployment-86dbd79ff6-thh66 1/1 Running ... 26s
複製程式碼
從 AGE 列就能看到有一部分 pod 是剛剛新建的,有的 pod 則還是老的。下面的命令可以顯示釋出的實時狀態:
$ kubectl rollout status deployment k8s-demo-deployment
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 old replicas are pending termination...
deployment "k8s-demo-deployment" successfully rolled out
複製程式碼
由於我輸入得比較晚,釋出已經快要結束,所以只有三行輸出。下面的命令可以檢視釋出歷史,因為第二次釋出使用了 --record=true
所以可以看到用於釋出的命令。
$ kubectl rollout history deployment k8s-demo-deployment
deployments "k8s-demo-deployment"
REVISION CHANGE-CAUSE
1 <none>
2 kubectl apply --filename=deploy.yml --record=true
複製程式碼
這時如果重新整理瀏覽器,就可以看到更新的內容「Hello Kubernetes!」。假設新版釋出後,我們發現有嚴重的 bug,需要馬上回滾到上個版本,可以用這個很簡單的操作:
$ kubectl rollout undo deployment k8s-demo-deployment --to-revision=1
deployment "k8s-demo-deployment" rolled back
複製程式碼
Kubernetes 會按照既定的策略替換各個 pod,與釋出新版本類似,只是這次是用老版本替換新版本:
$ kubectl rollout status deployment k8s-demo-deployment
Waiting for rollout to finish: 4 out of 10 new replicas have been updated...
Waiting for rollout to finish: 6 out of 10 new replicas have been updated...
Waiting for rollout to finish: 8 out of 10 new replicas have been updated...
Waiting for rollout to finish: 1 old replicas are pending termination...
deployment "k8s-demo-deployment" successfully rolled out
複製程式碼
在回滾結束之後,重新整理瀏覽器就可以確認網頁內容又改回了「Hello Docker!」。
結語
我們從不同層面實踐了一遍映象的構建和容器的部署,並且部署了一個有 10 個容器的 deployment, 實驗了滾動更新和回滾的流程。Kubernetes 提供了非常多的功能,本文只是以走馬觀花的方式做了一個快節奏的 walkthrough,略過了很多細節。雖然你還不能在簡歷上加上「精通 Kubernetes」,但是應該可以在本地的 Kubernetes 環境測試自己的前後端專案,遇到具體的問題時求助於 Google 和官方文件即可。在此基礎上進一步熟悉應該就可以在別人提供的 Kubernetes 生產環境釋出自己的服務。
LeanCloud 的大部分服務都執行在基於 Docker 的基礎設施上,包括各個 API 服務、中介軟體、後端任務等。大部分使用 LeanCloud 的開發者主要工作在前端,不過雲引擎是我們的產品中讓容器技術離使用者最近的。雲引擎提供了容器帶來的隔離良好、擴容簡便等優點,同時又直接支援各個語言的原生依賴管理,為使用者免去了映象構建、監控、恢復等負擔,很適合希望把精力完全投入在開發上的使用者。
LeanCloud 在招聘以下職位:
市場團隊負責人
後端軟體工程師(Clojure、Python、Java)
Android 軟體工程師
具體的需求以及其他正在招聘的職位請見我們的工作機會頁面。除了在官網上可以看到的已經發布的產品外,我們也在開發讓人興奮的新產品,有很多有意義、有價值的工作。
如果轉載本文,請包含原文連結和招聘資訊。