https://skarlso.github.io/2018/03/15/kubernetes-distributed-application/
作者 | Hannibal
譯者 | Andy Song (pinewall) 共計翻譯:27 篇 貢獻時間:121 天
簡介
夥計們,請搬好小板凳坐好,下麵將是一段漫長的旅程,期望你能夠樂在其中。
我將基於 Kubernetes[1] 部署一個分散式應用。我曾試圖編寫一個盡可能真實的應用,但由於時間和精力有限,最終砍掉了很多細節。
我將聚焦 Kubernetes 及其部署。
讓我們開始吧。
應用
TL;DR
該應用本身由 6 個元件構成。程式碼可以從如下連結中找到:Kubenetes 叢集示例[2]。
這是一個人臉識別服務,透過比較已知個人的圖片,識別給定圖片對應的個人。前端頁面用表格形式簡要的展示圖片及對應的個人。具體而言,向 接收器[2] 傳送請求,請求包含指向一個圖片的連結。圖片可以位於任何位置。接受器將圖片地址儲存到資料庫 (MySQL) 中,然後向佇列傳送處理請求,請求中包含已儲存圖片的 ID。這裡我們使用 NSQ[3] 建立佇列。
圖片處理[4] 服務一直監聽處理請求佇列,從中獲取任務。處理過程包括如下幾步:獲取圖片 ID,讀取圖片,透過 gRPC[5] 將圖片路徑傳送至 Python 編寫的 人臉識別[6] 後端。如果識別成功,後端給出圖片對應個人的名字。圖片處理器進而根據個人 ID 更新圖片記錄,將其標記為處理成功。如果識別不成功,圖片被標記為待解決。如果圖片識別過程中出現錯誤,圖片被標記為失敗。
標記為失敗的圖片可以透過計劃任務等方式進行重試。
那麼具體是如何工作的呢?我們深入探索一下。
接收器
接收器服務是整個流程的起點,透過如下形式的 API 接收請求:
curl -d '{"path":"/unknown_images/unknown0001.jpg"}' http://127.0.0.1:8000/image/post
此時,接收器將路徑儲存到共享資料庫叢集中,該物體儲存後將從資料庫服務收到對應的 ID。本應用採用“物體物件的唯一標識由持久層提供”的模型。獲得物體 ID 後,接收器向 NSQ 傳送訊息,至此接收器的工作完成。
圖片處理器
從這裡開始變得有趣起來。圖片處理器首次執行時會建立兩個 Go 協程,具體為:
Consume
這是一個 NSQ 消費者,需要完成三項必需的任務。首先,監聽佇列中的訊息。其次,當有新訊息到達時,將對應的 ID 追加到一個執行緒安全的 ID 片段中,以供第二個協程處理。最後,告知第二個協程處理新任務,方法為 sync.Condition[7]。
ProcessImages
該協程會處理指定 ID 片段,直到對應片段全部處理完成。當處理完一個片段後,該協程並不是在一個通道上睡眠等待,而是進入懸掛狀態。對每個 ID,按如下步驟順序處理:
這個服務可以複製多份同時執行。
斷路器
即使對於一個複製資源幾乎沒有開銷的系統,也會有意外的情況發生,例如網路故障或任何兩個服務之間的通訊存在問題等。我在 gRPC 呼叫中實現了一個簡單的斷路器,這十分有趣。
下麵給出工作原理:
當出現 5 次不成功的服務呼叫時,斷路器啟動並阻斷後續的呼叫請求。經過指定的時間後,它對服務進行健康檢查並判斷是否恢復。如果問題依然存在,等待時間會進一步增大。如果已經恢復,斷路器停止對服務呼叫的阻斷,允許請求流量透過。
前端
前端只包含一個極其簡單的表格檢視,透過 Go 自身的 html/模板顯示一系列圖片。
人臉識別
人臉識別是整個識別的關鍵點。僅因為追求靈活性,我將這個服務設計為基於 gRPC 的服務。最初我使用 Go 編寫,但後續發現基於 Python 的實現更加適合。事實上,不算 gRPC 部分的程式碼,人臉識別部分僅有 7 行程式碼。我使用的人臉識別[9]庫極為出色,它包含 OpenCV 的全部 C 系結。維護 API 標準意味著只要標準本身不變,實現可以任意改變。
註意:我曾經試圖使用 GoCV[10],這是一個極好的 Go 庫,但欠缺所需的 C 系結。推薦馬上瞭解一下這個庫,它會讓你大吃一驚,例如編寫若干行程式碼即可實現實時攝像處理。
這個 Python 庫的工作方式本質上很簡單。準備一些你認識的人的圖片,把資訊記錄下來。對於我而言,我有一個圖片檔案夾,包含若干圖片,名稱分別為 hannibal_1.jpg
、 hannibal_2.jpg
、 gergely_1.jpg
、 john_doe.jpg
。在資料庫中,我使用兩個表記錄資訊,分別為 person
、 person_images
,具體如下:
+----+----------+
| id | name |
+----+----------+
| 1 | Gergely |
| 2 | John Doe |
| 3 | Hannibal |
+----+----------+
+----+----------------+-----------+
| id | image_name | person_id |
+----+----------------+-----------+
| 1 | hannibal_1.jpg | 3 |
| 2 | hannibal_2.jpg | 3 |
+----+----------------+-----------+
人臉識別庫識別出未知圖片後,傳回圖片的名字。我們接著使用類似下麵的聯合查詢找到對應的個人。
select person.name, person.id from person inner join person_images as pi on person.id = pi.person_id where image_name = 'hannibal_2.jpg';
gRPC 呼叫傳回的個人 ID 用於更新圖片的 person
列。
NSQ
NSQ 是 Go 編寫的小規模佇列,可擴充套件且佔用系統記憶體較少。NSQ 包含一個查詢服務,用於消費者接收訊息;包含一個守護行程,用於傳送訊息。
在 NSQ 的設計理念中,訊息傳送程式應該與守護行程在同一臺主機上,故傳送程式僅需傳送至 localhost。但守護行程與查詢服務相連線,這使其構成了全域性佇列。
這意味著有多少 NSQ 守護行程就有多少對應的傳送程式。但由於其資源消耗極小,不會影響主程式的資源使用。
配置
為了盡可能增加靈活性以及使用 Kubernetes 的 ConfigSet 特性,我在開發過程中使用 .env
檔案記錄配置資訊,例如資料庫服務的地址以及 NSQ 的查詢地址。在生產環境或 Kubernetes 環境中,我將使用環境變數屬性配置。
應用小結
這就是待部署應用的全部架構資訊。應用的各個元件都是可變更的,他們之間僅透過資料庫、訊息佇列和 gRPC 進行耦合。考慮到更新機制的原理,這是部署分散式應用所必須的;在部署部分我會繼續分析。
使用 Kubernetes 部署應用
基礎知識
Kubernetes 是什麼?
這裡我會提到一些基礎知識,但不會深入細節,細節可以用一本書的篇幅描述,例如 Kubernetes 構建與執行[11]。另外,如果你願意挑戰自己,可以檢視官方檔案:Kubernetes 檔案[12]。
Kubernetes 是容器化服務及應用的管理器。它易於擴充套件,可以管理大量容器;更重要的是,可以透過基於 yaml 的模板檔案高度靈活地進行配置。人們經常把 Kubernetes 比作 Docker Swarm,但 Kubernetes 的功能不僅僅如此。例如,Kubernetes 不關心底層容器實現,你可以使用 LXC 與 Kubernetes 的組合,效果與使用 Docker 一樣好。Kubernetes 在管理容器的基礎上,可以管理已部署的服務或應用叢集。如何操作呢?讓我們概覽一下用於構成 Kubernetes 的模組。
在 Kubernetes 中,你給出期望的應用狀態,Kubernetes 會盡其所能達到對應的狀態。狀態可以是已部署、已暫停,有 2 個副本等,以此類推。
Kubernetes 使用標簽和註釋標記元件,包括服務、部署、副本組、守護行程組等在內的全部元件都被標記。考慮如下場景,為了識別 pod 與應用的對應關係,使用 app: myapp
標簽。假設應用已部署 2 個容器,如果你移除其中一個容器的 app
標簽,Kubernetes 只能識別到一個容器(隸屬於應用),進而啟動一個新的具有 myapp
標簽的實體。
Kubernetes 叢集
要使用 Kubernetes,需要先搭建一個 Kubernetes 叢集。搭建 Kubernetes 叢集可能是一個痛苦的經歷,但所幸有工具可以幫助我們。Minikube 為我們在本地搭建一個單節點叢集。AWS 的一個 beta 服務工作方式類似於 Kubernetes 叢集,你只需請求節點並定義你的部署即可。Kubernetes 叢集元件的檔案如下:Kubernetes 叢集元件[13]。
節點
節點是工作單位,形式可以是虛擬機器、物理機,也可以是各種型別的雲主機。
Pod
Pod 是本地容器邏輯上組成的集合,即一個 Pod 中可能包含若干個容器。Pod 建立後具有自己的 DNS 和虛擬 IP,這樣 Kubernetes 可以對到達流量進行負載均衡。你幾乎不需要直接和容器打交道;即使是除錯的時候,例如檢視日誌,你通常呼叫 kubectl logs deployment/your-app -f
檢視部署日誌,而不是使用 -c container_name
檢視具體某個容器的日誌。-f
引數表示從日誌尾部進行流式輸出。
部署
在 Kubernetes 中建立任何型別的資源時,後臺使用一個部署元件,它指定了資源的期望狀態。使用部署物件,你可以將 Pod 或服務變更為另外的狀態,也可以更新應用或上線新版本應用。你一般不會直接操作副本組 (後續會描述),而是透過部署物件建立並管理。
服務
預設情況下,Pod 會獲取一個 IP 地址。但考慮到 Pod 是 Kubernetes 中的易失性元件,我們需要更加持久的元件。不論是佇列,MySQL、內部 API 或前端,都需要長期執行並使用保持不變的 IP 或更好的 DNS 記錄。
為解決這個問題,Kubernetes 提供了服務元件,可以定義訪問樣式,支援的樣式包括負載均衡、簡單 IP 或內部 DNS。
Kubernetes 如何獲知服務執行正常呢?你可以配置健康性檢查和可用性檢查。健康性檢查是指檢查容器是否處於執行狀態,但容器處於執行狀態並不意味著服務執行正常。對此,你應該使用可用性檢查,即請求應用的一個特別介面。
由於服務非常重要,推薦你找時間閱讀以下檔案:服務[14]。嚴肅的說,需要閱讀的東西很多,有 24 頁 A4 紙的篇幅,涉及網路、服務及自動發現。這也有助於你決定是否真的打算在生產環境中使用 Kubernetes。
DNS / 服務發現
在 Kubernetes 叢集中建立服務後,該服務會從名為 kube-proxy
和 kube-dns
的特殊 Kubernetes 部署中獲取一個 DNS 記錄。它們兩個用於提供叢集內的服務發現。如果你有一個正在執行的 MySQL 服務並配置 clusterIP: no
,那麼叢集內部任何人都可以透過 mysql.default.svc.cluster.local
訪問該服務,其中:
mysql
– 服務的名稱default
– 名稱空間的名稱svc
– 對應服務分類cluster.local
– 本地叢集的域名可以使用自定義設定更改本地叢集的域名。如果想讓服務可以從叢集外訪問,需要使用 DNS 服務,並使用例如 Nginx 將 IP 地址系結至記錄。服務對應的對外 IP 地址可以使用如下命令查詢:
kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services mysql
kubectl get -o jsonpath="{.spec.ports[0].LoadBalancer}" services mysql
模板檔案
類似 Docker Compose、TerraForm 或其它的服務管理工具,Kubernetes 也提供了基礎設施描述模板。這意味著,你幾乎不用手動操作。
以 Nginx 部署為例,檢視下麵的 yaml 模板:
apiVersion: apps/v1
kind: Deployment #(1)
metadata: #(2)
name: nginx-deployment
labels: #(3)
app: nginx
spec: #(4)
replicas: 3 #(5)
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers: #(6)
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
在這個示例部署中,我們做瞭如下操作:
kind
關鍵字定義模板型別metadata
關鍵字,增加該部署的識別資訊labels
標記每個需要建立的資源spec
關鍵字描述所需的狀態副本組
副本組是一個底層的副本管理器,用於保證執行正確數目的應用副本。相比而言,部署是更高層級的操作,應該用於管理副本組。除非你遇到特殊的情況,需要控制副本的特性,否則你幾乎不需要直接操作副本組。
守護行程組
上面提到 Kubernetes 始終使用標簽,還有印象嗎?守護行程組是一個控制器,用於確保守護行程化的應用一直執行在具有特定標簽的節點中。
例如,你將所有節點增加 logger
或 mission_critical
的標簽,以便執行日誌 / 審計服務的守護行程。接著,你建立一個守護行程組並使用 logger
或 mission_critical
節點選擇器。Kubernetes 會查詢具有該標簽的節點,確保守護行程的實體一直執行在這些節點中。因而,節點中執行的所有行程都可以在節點內訪問對應的守護行程。
以我的應用為例,NSQ 守護行程可以用守護行程組實現。具體而言,將對應節點增加 recevier
標簽,建立一個守護行程組並配置 receiver
應用選擇器,這樣這些節點上就會一直執行接收者元件。
守護行程組具有副本組的全部優勢,可擴充套件且由 Kubernetes 管理,意味著 Kubernetes 管理其全生命週期的事件,確保持續執行,即使出現故障,也會立即替換。
擴充套件
在 Kubernetes 中,擴充套件是稀鬆平常的事情。副本組負責 Pod 執行的實體數目。就像你在 nginx 部署那個示例中看到的那樣,對應設定項 replicas:3
。我們可以按應用所需,讓 Kubernetes 執行多份應用副本。
當然,設定項有很多。你可以指定讓多個副本執行在不同的節點上,也可以指定各種不同的應用啟動等待時間。想要在這方面瞭解更多,可以閱讀 水平擴充套件[15] 和 Kubernetes 中的互動式擴充套件[16];當然 副本組[17] 的細節對你也有幫助,畢竟 Kubernetes 中的擴充套件功能都來自於該模組。
Kubernetes 部分小結
Kubernetes 是容器編排的便捷工具,工作單元為 Pod,具有分層架構。最頂層是部署,用於操作其它資源,具有高度可配置性。對於你的每個命令呼叫,Kubernetes 提供了對應的 API,故理論上你可以編寫自己的程式碼,向 Kubernetes API 傳送資料,得到與 kubectl
命令同樣的效果。
截至目前,Kubernetes 原生支援所有主流雲服務供應商,而且完全開源。如果你願意,可以貢獻程式碼;如果你希望對工作原理有深入瞭解,可以查閱程式碼:GitHub 上的 Kubernetes 專案[18]。
Minikube
接下來我會使用 Minikube[19] 這款本地 Kubernetes 叢集模擬器。它並不擅長模擬多節點叢集,但可以很容易地給你提供本地學習環境,讓你開始探索,這很棒。Minikube 基於可高度調優的虛擬機器,由 VirtualBox 類似的虛擬化工具提供。
我用到的全部 Kubernetes 模板檔案可以在這裡找到:Kubernetes 檔案[20]。
註意:在你後續測試可擴充套件性時,會發現副本一直處於 Pending
狀態,這是因為 minikube 叢集中只有一個節點,不應該允許多副本執行在同一個節點上,否則明顯只是耗盡了可用資源。使用如下命令可以檢視可用資源:
kubectl get nodes -o yaml
構建容器
Kubernetes 支援大多數現有的容器技術。我這裡使用 Docker。每一個構建的服務容器,對應程式碼庫中的一個 Dockerfile 檔案。我推薦你仔細閱讀它們,其中大多數都比較簡單。對於 Go 服務,我採用了最近引入的多步構建的方式。Go 服務基於 Alpine Linux 映象建立。人臉識別程式使用 Python、NSQ 和 MySQL 使用對應的容器。
背景關係
Kubernetes 使用名稱空間。如果你不額外指定名稱空間,Kubernetes 會使用 default
名稱空間。為避免汙染預設名稱空間,我會一直指定名稱空間,具體操作如下:
❯ kubectl config set-context kube-face-cluster --namespace=face
Context "kube-face-cluster" created.
建立背景關係之後,應馬上啟用:
❯ kubectl config use-context kube-face-cluster
Switched to context "kube-face-cluster".
此後,所有 kubectl
命令都會使用 face
名稱空間。
(LCTT 譯註:作者後續並沒有使用 face 名稱空間,模板檔案中的名稱空間仍為 default,可能 face 名稱空間用於開發環境。如果希望使用 face 命令空間,需要將內部 DNS 地址中的 default 改成 face;如果只是測試,可以不執行這兩條命令。)
應用部署
Pods 和 服務概覽:
MySQL
第一個要部署的服務是資料庫。
按照 Kubernetes 的示例 Kubenetes MySQL[21] 進行部署,即可以滿足我的需求。註意:示例配置檔案的 MYSQL_PASSWORD 欄位使用了明文密碼,我將使用 Kubernetes Secrets[22] 物件以提高安全性。
我建立了一個 Secret 物件,對應的本地 yaml 檔案如下:
apiVersion: v1
kind: Secret
metadata:
name: kube-face-secret
type: Opaque
data:
mysql_password: base64codehere
mysql_userpassword: base64codehere
其中 base64 編碼透過如下命令生成:
echo -n "ubersecurepassword" | base64
echo -n "root:ubersecurepassword" | base64
(LCTT 譯註:secret yaml 檔案中的 data 應該有兩條,一條對應 mysql_password
,僅包含密碼;另一條對應 mysql_userpassword
,包含使用者和密碼。後文會用到 mysql_userpassword
,但沒有提及相應的生成)
我的部署 yaml 對應部分如下:
...
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: kube-face-secret
key: mysql_password
...
另外值得一提的是,我使用捲將資料庫持久化,捲對應的定義如下:
...
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
...
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
...
其中 presistentVolumeClain
是關鍵,告知 Kubernetes 當前資源需要持久化儲存。持久化儲存的提供方式對使用者透明。類似 Pods,如果想瞭解更多細節,參考檔案:Kubernetes 持久化儲存[23]。
(LCTT 譯註:使用 presistentVolumeClain
之前需要建立 presistentVolume
,對於單節點可以使用本地儲存,對於多節點需要使用共享儲存,因為 Pod 可以能排程到任何一個節點)
使用如下命令部署 MySQL 服務:
kubectl apply -f mysql.yaml
這裡比較一下 create
和 apply
。apply
是一種宣告式的物件配置命令,而 create
是命令式的命令。當下我們需要知道的是,create
通常對應一項任務,例如執行某個元件或建立一個部署;相比而言,當我們使用 apply
的時候,使用者並沒有指定具體操作,Kubernetes 會根據叢集目前的狀態定義需要執行的操作。故如果不存在名為 mysql
的服務,當我執行 apply -f mysql.yaml
時,Kubernetes 會建立該服務。如果再次執行這個命令,Kubernetes 會忽略該命令。但如果我再次執行 create
,Kubernetes 會報錯,告知服務已經建立。
想瞭解更多資訊,請閱讀如下檔案:Kubernetes 物件管理[24],命令式配置[25]和宣告式配置[26]。
執行如下命令檢視執行進度資訊:
# 描述完整資訊
kubectl describe deployment mysql
# 僅描述 Pods 資訊
kubectl get pods -l app=mysql
(第一個命令)輸出示例如下:
...
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: mysql-55cd6b9f47 (1/1 replicas created)
...
對於 get pods
命令,輸出示例如下:
NAME READY STATUS RESTARTS AGE
mysql-78dbbd9c49-k6sdv 1/1 Running 0 18s
可以使用下麵的命令測試資料庫實體:
kubectl run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql -pyourpasswordhere
特別提醒:如果你在這裡修改了密碼,重新 apply 你的 yaml 檔案並不能更新容器。因為資料庫是持久化的,密碼並不會改變。你需要先使用 kubectl delete -f mysql.yaml
命令刪除整個部署。
執行 show databases
後,應該可以看到如下資訊:
If you don't see a command prompt, try pressing enter.
mysql>
mysql>
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| kube |
| mysql |
| performance_schema |
+--------------------+
4 rows in set (0.00 sec)
mysql> exit
Bye
你會註意到,我還將一個資料庫初始化 SQL[27] 檔案掛載到容器中,MySQL 容器會自動執行該檔案,匯入我將用到的部分資料和樣式。
對應的捲定義如下:
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
- name: bootstrap-script
mountPath: /docker-entrypoint-initdb.d/database_setup.sql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
- name: bootstrap-script
hostPath:
path: /Users/hannibal/golang/src/github.com/Skarlso/kube-cluster-sample/database_setup.sql
type: File
(LCTT 譯註:資料庫初始化指令碼需要改成對應的路徑,如果是多節點,需要是共享儲存中的路徑。另外,作者給的 sql 檔案似乎有誤,person_images
表中的 person_id
列數字都小 1,作者預設 id
從 0 開始,但應該是從 1 開始)
執行如下命令檢視引導指令碼是否正確執行:
~/golang/src/github.com/Skarlso/kube-cluster-sample/kube_files master*
❯ kubectl run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql -uroot -pyourpasswordhere kube
If you don't see a command prompt, try pressing enter.
mysql> show tables;
+----------------+
| Tables_in_kube |
+----------------+
| images |
| person |
| person_images |
+----------------+
3 rows in set (0.00 sec)
mysql>
(LCTT 譯註:上述程式碼塊中的第一行是作者執行命令所在路徑,執行第二行的命令無需在該目錄中進行)
上述操作完成了資料庫服務的初始化。使用如下命令可以檢視服務日誌:
kubectl logs deployment/mysql -f
NSQ 查詢
NSQ 查詢將以內部服務的形式執行。由於不需要外部訪問,這裡使用 clusterIP: None
在 Kubernetes 中將其設定為無頭服務,意味著該服務不使用負載均衡樣式,也不使用單獨的服務 IP。DNS 將基於服務選擇器。
我們的 NSQ 查詢服務對應的選擇器為:
selector:
matchLabels:
app: nsqlookup
那麼,內部 DNS 對應的物體類似於:nsqlookup.default.svc.cluster.local
。
無頭服務的更多細節,可以參考:無頭服務[28]。
NSQ 服務與 MySQL 服務大同小異,只需要少許修改即可。如前所述,我將使用 NSQ 原生的 Docker 映象,名稱為 nsqio/nsq
。映象包含了全部的 nsq 命令,故 nsqd 也將使用該映象,只是使用的命令不同。對於 nsqlookupd,命令如下:
command: ["/nsqlookupd"]
args: ["--broadcast-address=nsqlookup.default.svc.cluster.local"]
你可能會疑惑,--broadcast-address
引數是做什麼用的?預設情況下,nsqlookup
使用容器的主機名作為廣播地址;這意味著,當使用者執行回呼時,回呼試圖訪問的地址類似於 http://nsqlookup-234kf-asdf:4161/lookup?topics=image
,但這顯然不是我們期望的。將廣播地址設定為內部 DNS 後,回呼地址將是 http://nsqlookup.default.svc.cluster.local:4161/lookup?topic=images
,這正是我們期望的。
NSQ 查詢還需要轉發兩個埠,一個用於廣播,另一個用於 nsqd 守護行程的回呼。在 Dockerfile 中暴露相應埠,在 Kubernetes 模板中使用它們,類似如下:
容器模板:
ports:
- containerPort: 4160
hostPort: 4160
- containerPort: 4161
hostPort: 4161
服務模板:
spec:
ports:
- name: main
protocol: TCP
port: 4160
targetPort: 4160
- name: secondary
protocol: TCP
port: 4161
targetPort: 4161
埠名稱是必須的,Kubernetes 基於名稱進行區分。(LCTT 譯註:埠名更新為作者 GitHub 對應檔案中的名稱)
像之前那樣,使用如下命令建立服務:
kubectl apply -f nsqlookup.yaml
nsqlookupd 部分到此結束。截至目前,我們已經準備好兩個主要的元件。
接收器
這部分略微複雜。接收器需要完成三項工作:
部署
第一個要建立的部署是接收器本身,容器映象為 skarlso/kube-receiver-alpine
。
NSQ 守護行程
接收器需要使用 NSQ 守護行程。如前所述,接收器在其內部執行一個 NSQ,這樣與 nsq 的通訊可以在本地進行,無需透過網路。為了讓接收器可以這樣操作,NSQ 需要與接收器部署在同一個節點上。
NSQ 守護行程也需要一些調整的引數配置:
ports:
- containerPort: 4150
hostPort: 4150
- containerPort: 4151
hostPort: 4151
env:
- name: NSQLOOKUP_ADDRESS
value: nsqlookup.default.svc.cluster.local
- name: NSQ_BROADCAST_ADDRESS
value: nsqd.default.svc.cluster.local
command: ["/nsqd"]
args: ["--lookupd-tcp-address=$(NSQLOOKUP_ADDRESS):4160", "--broadcast-address=$(NSQ_BROADCAST_ADDRESS)"]
其中我們配置了 lookup-tcp-address
和 broadcast-address
引數。前者是 nslookup 服務的 DNS 地址,後者用於回呼,就像 nsqlookupd 配置中那樣。
對外公開
下麵即將建立第一個對外公開的服務。有兩種方式可供選擇。考慮到該 API 負載較高,可以使用負載均衡的方式。另外,如果希望將其部署到生產環境中的任選節點,也應該使用負載均衡方式。
但由於我使用的本地叢集只有一個節點,那麼使用 NodePort
的方式就足夠了。NodePort
方式將服務暴露在對應節點的固定埠上。如果未指定埠,將從 30000-32767 數字範圍內隨機選其一個。也可以指定埠,可以在模板檔案中使用 nodePort
設定即可。可以透過
訪問該服務。如果使用多個節點,負載均衡可以將多個 IP 合併為一個 IP。
更多資訊,請參考檔案:服務釋出[29]。
結合上面的資訊,我們定義了接收器服務,對應的模板如下:
apiVersion: v1
kind: Service
metadata:
name: receiver-service
spec:
ports:
- protocol: TCP
port: 8000
targetPort: 8000
selector:
app: receiver
type: NodePort
如果希望固定使用 8000 埠,需要增加 nodePort
配置,具體如下:
apiVersion: v1
kind: Service
metadata:
name: receiver-service
spec:
ports:
- protocol: TCP
port: 8000
targetPort: 8000
selector:
app: receiver
type: NodePort
nodePort: 8000
(LCTT 譯註:雖然作者沒有寫,但我們應該知道需要執行的部署命令 kubectl apply -f receiver.yaml
。)
圖片處理器
圖片處理器用於將圖片傳送至識別元件。它需要訪問 nslookupd、 mysql 以及後續部署的人臉識別服務的 gRPC 介面。事實上,這是一個無聊的服務,甚至其實並不是服務(LCTT 譯註:第一個服務是指在整個架構中,圖片處理器作為一個服務;第二個服務是指 Kubernetes 服務)。它並需要對外暴露埠,這是第一個只包含部署的元件。長話短說,下麵是完整的模板:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: image-processor-deployment
spec:
selector:
matchLabels:
app: image-processor
replicas: 1
template:
metadata:
labels:
app: image-processor
spec:
containers:
- name: image-processor
image: skarlso/kube-processor-alpine:latest
env:
- name: MYSQL_CONNECTION
value: "mysql.default.svc.cluster.local"
- name: MYSQL_USERPASSWORD
valueFrom:
secretKeyRef:
name: kube-face-secret
key: mysql_userpassword
- name: MYSQL_PORT
# TIL: If this is 3306 without " kubectl throws an error.
value: "3306"
- name: MYSQL_DBNAME
value: kube
- name: NSQ_LOOKUP_ADDRESS
value: "nsqlookup.default.svc.cluster.local:4161"
- name: GRPC_ADDRESS
value: "face-recog.default.svc.cluster.local:50051"
檔案中唯一需要提到的是用於配置應用的多個環境變數屬性,主要關註 nsqlookupd 地址 和 gRPC 地址。
執行如下命令完成部署:
kubectl apply -f image_processor.yaml
人臉識別
人臉識別服務的確包含一個 Kubernetes 服務,具體而言是一個比較簡單、僅供圖片處理器使用的服務。模板如下:
apiVersion: v1
kind: Service
metadata:
name: face-recog
spec:
ports:
- protocol: TCP
port: 50051
targetPort: 50051
selector:
app: face-recog
clusterIP: None
更有趣的是,該服務涉及兩個捲,分別為 known_people
和 unknown_people
。你能猜到捲中包含什麼內容嗎?對,是圖片。known_people
捲包含所有新圖片,接收器收到圖片後將圖片傳送至該捲對應的路徑,即掛載點。在本例中,掛載點為 /unknown_people
,人臉識別服務需要能夠訪問該路徑。
對於 Kubernetes 和 Docker 而言,這很容易。捲可以使用掛載的 S3 或 某種 nfs,也可以是宿主機到虛擬機器的本地掛載。可選方式有很多 (至少有一打那麼多)。為簡潔起見,我將使用本地掛載方式。
掛載捲分為兩步。第一步,需要在 Dockerfile 中指定捲:
VOLUME [ "/unknown_people", "/known_people" ]
第二步,就像之前為 MySQL Pod 掛載捲那樣,需要在 Kubernetes 模板中配置;相比而言,這裡使用 hostPath
,而不是 MySQL 例子中的 PersistentVolumeClaim
:
volumeMounts:
- name: known-people-storage
mountPath: /known_people
- name: unknown-people-storage
mountPath: /unknown_people
volumes:
- name: known-people-storage
hostPath:
path: /Users/hannibal/Temp/known_people
type: Directory
- name: unknown-people-storage
hostPath:
path: /Users/hannibal/Temp/
type: Directory
(LCTT 譯註:對於多節點樣式,由於人臉識別服務和接收器服務可能不在一個節點上,故需要使用共享儲存而不是節點本地儲存。另外,出於 Python 程式碼的邏輯,推薦保持兩個檔案夾的巢狀結構,即 known_people 作為子目錄。)
我們還需要為 known_people
檔案夾做配置設定,用於人臉識別程式。當然,使用環境變數屬性可以完成該設定:
env:
- name: KNOWN_PEOPLE
value: "/known_people"
Python 程式碼按如下方式搜尋圖片:
known_people = os.getenv('KNOWN_PEOPLE', 'known_people')
print("Known people images location is: %s" % known_people)
images = self.image_files_in_folder(known_people)
其中 image_files_in_folder
函式定義如下:
def image_files_in_folder(self, folder):
return [os.path.join(folder, f) for f in os.listdir(folder) if re.match(r'.*\.(jpg|jpeg|png)', f, flags=re.I)]
看起來不錯。
如果接收器現在收到一個類似下麵的請求(接收器會後續將其發送出去):
curl -d '{"path":"/unknown_people/unknown220.jpg"}' http://192.168.99.100:30251/image/post
影象處理器會在 /unknown_people
目錄搜尋名為 unknown220.jpg 的圖片,接著在 known_folder
檔案中找到 unknown220.jpg
對應個人的圖片,最後傳回匹配圖片的名稱。
檢視日誌,大致資訊如下:
# 接收器
❯ curl -d '{"path":"/unknown_people/unknown219.jpg"}' http://192.168.99.100:30251/image/post
got path: {Path:/unknown_people/unknown219.jpg}
image saved with id: 4
image sent to nsq
# 圖片處理器
2018/03/26 18:11:21 INF 1 [images/ch] querying nsqlookupd http://nsqlookup.default.svc.cluster.local:4161/lookup?topic=images
2018/03/26 18:11:59 Got a message: 4
2018/03/26 18:11:59 Processing image id: 4
2018/03/26 18:12:00 got person: Hannibal
2018/03/26 18:12:00 updating record with person id
2018/03/26 18:12:00 done
我們已經使用 Kubernetes 部署了應用正常工作所需的全部服務。
前端
更進一步,可以使用簡易的 Web 應用更好的顯示資料庫中的資訊。這也是一個對外公開的服務,使用的引數可以參考接收器。
部署後效果如下:
回顧
到目前為止我們做了哪些操作呢?我一直在部署服務,用到的命令彙總如下:
kubectl apply -f mysql.yaml
kubectl apply -f nsqlookup.yaml
kubectl apply -f receiver.yaml
kubectl apply -f image_processor.yaml
kubectl apply -f face_recognition.yaml
kubectl apply -f frontend.yaml
命令順序可以打亂,因為除了圖片處理器的 NSQ 消費者外的應用在啟動時並不會建立連線,而且圖片處理器的 NSQ 消費者會不斷重試。
使用 kubectl get pods
查詢正在執行的 Pods,示例如下:
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
face-recog-6bf449c6f-qg5tr 1/1 Running 0 1m
image-processor-deployment-6467468c9d-cvx6m 1/1 Running 0 31s
mysql-7d667c75f4-bwghw 1/1 Running 0 36s
nsqd-584954c44c-299dz 1/1 Running 0 26s
nsqlookup-7f5bdfcb87-jkdl7 1/1 Running 0 11s
receiver-deployment-5cb4797598-sf5ds 1/1 Running 0 26s
執行 minikube service list
:
❯ minikube service list
|-------------|----------------------|-----------------------------|
| NAMESPACE | NAME | URL |
|-------------|----------------------|-----------------------------|
| default | face-recog | No node port |
| default | kubernetes | No node port |
| default | mysql | No node port |
| default | nsqd | No node port |
| default | nsqlookup | No node port |
| default | receiver-service | http://192.168.99.100:30251 |
| kube-system | kube-dns | No node port |
| kube-system | kubernetes-dashboard | http://192.168.99.100:30000 |
|-------------|----------------------|-----------------------------|
滾動更新
滾動更新過程中會發生什麼呢?
在軟體開發過程中,需要變更應用的部分元件是常有的事情。如果我希望在不影響其它元件的情況下變更一個元件,我們的叢集會發生什麼變化呢?我們還需要最大程度的保持向後相容性,以免影響使用者體驗。謝天謝地,Kubernetes 可以幫我們做到這些。
目前的 API 一次只能處理一個圖片,不能批次處理,對此我並不滿意。
程式碼
目前,我們使用下麵的程式碼段處理單個圖片的情形:
// PostImage 對圖片提交做出響應,將圖片資訊儲存到資料庫中
// 並將該資訊傳送給 NSQ 以供後續處理使用
func PostImage(w http.ResponseWriter, r *http.Request) {
...
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/image/post", PostImage).Methods("POST")
log.Fatal(http.ListenAndServe(":8000", router))
}
我們有兩種選擇。一種是增加新介面 /images/post
給使用者使用;另一種是在原介面基礎上修改。
新版客戶端有回退特性,在新介面不可用時回退使用舊介面。但舊版客戶端沒有這個特性,故我們不能馬上修改程式碼邏輯。考慮如下場景,你有 90 臺伺服器,計劃慢慢執行滾動更新,依次對各臺伺服器進行業務更新。如果一臺服務需要大約 1 分鐘更新業務,那麼整體更新完成需要大約 1 個半小時的時間(不考慮並行更新的情形)。
更新過程中,一些伺服器執行新程式碼,一些伺服器執行舊程式碼。使用者請求被負載均衡到各個節點,你無法控制請求到達哪臺伺服器。如果客戶端的新介面請求被排程到執行舊程式碼的伺服器,請求會失敗;客戶端可能會回退使用舊介面,(但由於我們已經修改舊介面,本質上仍然是呼叫新介面),故除非請求剛好到達到執行新程式碼的伺服器,否則一直都會失敗。這裡我們假設不使用粘性會話。
而且,一旦所有伺服器更新完畢,舊版客戶端不再能夠使用你的服務。
這裡,你可能會說你並不需要保留舊程式碼;某些情況下,確實如此。因此,我們打算直接修改舊程式碼,讓其透過少量引數呼叫新程式碼。這樣操作操作相當於移除了舊程式碼。當所有客戶端遷移完畢後,這部分程式碼也可以安全地刪除。
新的介面
讓我們新增新的路由方法:
...
router.HandleFunc("/images/post", PostImages).Methods("POST")
...
更新舊的路由方法,使其呼叫新的路由方法,修改部分如下:
// PostImage 對圖片提交做出響應,將圖片資訊儲存到資料庫中
// 並將該資訊傳送給 NSQ 以供後續處理使用
func PostImage(w http.ResponseWriter, r *http.Request) {
var p Path
err := json.NewDecoder(r.Body).Decode(&p)
if err != nil {
fmt.Fprintf(w, "got error while decoding body: %s", err)
return
}
fmt.Fprintf(w, "got path: %+v\n", p)
var ps Paths
paths := make([]Path, 0)
paths = append(paths, p)
ps.Paths = paths
var pathsJSON bytes.Buffer
err = json.NewEncoder(&pathsJSON).Encode(ps)
if err != nil {
fmt.Fprintf(w, "failed to encode paths: %s", err)
return
}
r.Body = ioutil.NopCloser(&pathsJSON)
r.ContentLength = int64(pathsJSON.Len())
PostImages(w, r)
}
當然,方法名可能容易混淆,但你應該能夠理解我想表達的意思。我將請求中的單個路徑封裝成新方法所需格式,然後將其作為請求傳送給新介面處理。僅此而已。在 滾動更新批次圖片的 PR[30]中可以找到更多的修改方式。
至此,我們使用兩種方法呼叫接收器:
# 單路徑樣式
curl -d '{"path":"unknown4456.jpg"}' http://127.0.0.1:8000/image/post
# 多路徑樣式
curl -d '{"paths":[{"path":"unknown4456.jpg"}]}' http://127.0.0.1:8000/images/post
這裡用到的客戶端是 curl。一般而言,如果客戶端本身是一個服務,我會做一些修改,在新介面傳回 404 時繼續嘗試舊介面。
為了簡潔,我不打算為 NSQ 和其它元件增加批次圖片處理的能力。這些元件仍然是一次處理一個圖片。這部分修改將留給你作為擴充套件內容。 🙂
新映象
為實現滾動更新,我首先需要為接收器服務建立一個新的映象。新映象使用新標簽,告訴大家版本號為 v1.1。
docker build -t skarlso/kube-receiver-alpine:v1.1 .
新映象建立後,我們可以開始滾動更新了。
滾動更新
在 Kubernetes 中,可以使用多種方式完成滾動更新。
手動更新
不妨假設在我配置檔案中使用的容器版本為 v1.0
,那麼實現滾動更新只需執行如下命令:
kubectl rolling-update receiver --image:skarlso/kube-receiver-alpine:v1.1
如果滾動更新過程中出現問題,我們總是可以回滾:
kubectl rolling-update receiver --rollback
容器將回滾到使用上一個版本映象,操作簡捷無煩惱。
應用新的配置檔案
手動更新的不足在於無法版本管理。
試想下麵的場景。你使用手工更新的方式對若干個伺服器進行滾動升級,但其它人並不知道這件事。之後,另外一個人修改了模板檔案並將其應用到叢集中,更新了全部伺服器;更新過程中,突然發現服務不可用了。
長話短說,由於模板無法識別已經手動更新的伺服器,這些伺服器會按模板變更成錯誤的狀態。這種做法很危險,千萬不要這樣做。
推薦的做法是,使用新版本資訊更新模板檔案,然後使用 apply
命令應用模板檔案。
對於滾動擴充套件,Kubernetes 推薦透過部署結合副本組完成。但這意味著待滾動更新的應用至少有 2 個副本,否則無法完成 (除非將 maxUnavailable
設定為 1)。我在模板檔案中增加了副本數量、設定了接收器容器的新映象版本。
replicas: 2
...
spec:
containers:
- name: receiver
image: skarlso/kube-receiver-alpine:v1.1
...
更新過程中,你會看到如下資訊:
❯ kubectl rollout status deployment/receiver-deployment
Waiting for rollout to finish: 1 out of 2 new replicas have been updated...
透過在模板中增加 strategy
段,你可以增加更多的滾動擴充套件配置:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
關於滾動更新的更多資訊,可以參考如下檔案:部署的滾動更新[31],部署的更新[32], 部署的管理[33] 和 使用副本控制器完成滾動更新[34]等。
MINIKUBE 使用者需要註意:由於我們使用單個主機上使用單節點配置,應用只有 1 份副本,故需要將 maxUnavailable
設定為 1
。否則 Kubernetes 會阻止更新,新版本會一直處於 Pending
狀態;這是因為我們在任何時刻都不允許出現沒有(正在執行的) receiver
容器的場景。
擴充套件
Kubernetes 讓擴充套件成為相當容易的事情。由於 Kubernetes 管理整個叢集,你僅需在模板檔案中新增你需要的副本數目即可。
這篇文章已經比較全面了,但文章的長度也越來越長。我計劃再寫一篇後續文章,在 AWS 上使用多節點、多副本方式實現擴充套件。敬請期待。
清理環境
kubectl delete deployments --all
kubectl delete services -all
寫在最後的話
各位看官,本文就寫到這裡了。我們在 Kubernetes 上編寫、部署、更新和擴充套件(老實說,並沒有實現)了一個分散式應用。
如果你有任何疑惑,請在下麵的評論區留言交流,我很樂意回答相關問題。
希望閱讀本文讓你感到愉快。我知道,這是一篇相對長的文章,我也曾經考慮進行拆分;但整合在一起的單頁教程也有其好處,例如利於搜尋、儲存頁面或更進一步將頁面列印為 PDF 檔案。
Gergely 感謝你閱讀本文。
via: https://skarlso.github.io/2018/03/15/kubernetes-distributed-application/
作者:hannibal 譯者:pinewall 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出