Kubernetes 應用部署實戰

簡介html

夥計們,請搬好小板凳坐好,下面將是一段漫長的旅程,指望你可以樂在其中。前端

我將基於 Kubernetes[1] 部署一個分佈式應用。我曾試圖編寫一個儘量真實的應用,但因爲時間和精力有限,最終砍掉了不少細節。node

我將聚焦 Kubernetes 及其部署。mysql

讓咱們開始吧。linux

應用nginx

TL;DRgit

Kubernetes 應用部署實戰Kubernetes 應用部署實戰

該應用自己由 6 個組件構成。代碼能夠從以下連接中找到:Kubenetes 集羣示例[2]。github

這是一我的臉識別服務,經過比較已知我的的圖片,識別給定圖片對應的我的。前端頁面用表格形式簡要的展現圖片及對應的我的。具體而言,向 接收器[3] 發送請求,請求包含指向一個圖片的連接。圖片能夠位於任何位置。接受器將圖片地址存儲到數據庫 (MySQL) 中,而後向隊列發送處理請求,請求中包含已保存圖片的 ID。這裏咱們使用 NSQ[4] 創建隊列。golang

圖片處理[5] 服務一直監聽處理請求隊列,從中獲取任務。處理過程包括以下幾步:獲取圖片 ID,讀取圖片,經過 gRPC[6] 將圖片路徑發送至 Python 編寫的 人臉識別[7] 後端。若是識別成功,後端給出圖片對應我的的名字。圖片處理器進而根據我的 ID 更新圖片記錄,將其標記爲處理成功。若是識別不成功,圖片被標記爲待解決。若是圖片識別過程當中出現錯誤,圖片被標記爲失敗。sql

標記爲失敗的圖片能夠經過計劃任務等方式進行重試。

那麼具體是如何工做的呢?咱們深刻探索一下。

接收器

接收器服務是整個流程的起點,經過以下形式的 API 接收請求:

curl -d '{"path":"/unknown_images/unknown0001.jpg"}' http://127.0.0.1:8000/image/post

此時,接收器將路徑path存儲到共享數據庫集羣中,該實體存儲後將從數據庫服務收到對應的 ID。本應用採用「實體對象Entity Object的惟一標識由持久層提供」的模型。得到實體 ID 後,接收器向 NSQ 發送消息,至此接收器的工做完成。

圖片處理器

從這裏開始變得有趣起來。圖片處理器首次運行時會建立兩個 Go 協程routine,具體爲:

Consume

這是一個 NSQ 消費者,須要完成三項必需的任務。首先,監聽隊列中的消息。其次,當有新消息到達時,將對應的 ID 追加到一個線程安全的 ID 片斷中,以供第二個協程處理。最後,告知第二個協程處理新任務,方法爲 sync.Condition[8]。

ProcessImages

該協程會處理指定 ID 片斷,直到對應片斷所有處理完成。當處理完一個片斷後,該協程並非在一個通道上睡眠等待,而是進入懸掛狀態。對每一個 ID,按以下步驟順序處理:

  • 與人臉識別服務創建 gRPC 鏈接,其中人臉識別服務會在人臉識別部分進行介紹
  • 從數據庫獲取圖片對應的實體
  • 爲 斷路器[9] 準備兩個函數
    • 函數 1: 用於 RPC 方法調用的主函數
    • 函數 2: 基於 ping 的斷路器健康檢查
  • 調用函數 1 將圖片路徑發送至人臉識別服務,其中路徑應該是人臉識別服務能夠訪問的,最好是共享的,例如 NFS
  • 若是調用失敗,將圖片實體狀態更新爲 FAILEDPROCESSING
  • 若是調用成功,返回值是一個圖片的名字,對應數據庫中的一個我的。經過聯合 SQL 查詢,獲取對應我的的 ID
  • 將數據庫中的圖片實體狀態更新爲 PROCESSED,更新圖片被識別成的我的的 ID

這個服務能夠複製多份同時運行。

斷路器

即便對於一個複製資源幾乎沒有開銷的系統,也會有意外的狀況發生,例如網絡故障或任何兩個服務之間的通訊存在問題等。我在 gRPC 調用中實現了一個簡單的斷路器,這十分有趣。

下面給出工做原理:

Kubernetes 應用部署實戰Kubernetes 應用部署實戰

當出現 5 次不成功的服務調用時,斷路器啓動並阻斷後續的調用請求。通過指定的時間後,它對服務進行健康檢查並判斷是否恢復。若是問題依然存在,等待時間會進一步增大。若是已經恢復,斷路器中止對服務調用的阻斷,容許請求流量經過。

前端

前端只包含一個極其簡單的表格視圖,經過 Go 自身的 html/模板顯示一系列圖片。

人臉識別

人臉識別是整個識別的關鍵點。僅由於追求靈活性,我將這個服務設計爲基於 gRPC 的服務。最初我使用 Go 編寫,但後續發現基於 Python 的實現更加適合。事實上,不算 gRPC 部分的代碼,人臉識別部分僅有 7 行代碼。我使用的人臉識別[10]庫極爲出色,它包含 OpenCV 的所有 C 綁定。維護 API 標準意味着只要標準自己不變,實現能夠任意改變。

注意:我曾經試圖使用 GoCV[11],這是一個極好的 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 構建與運行[12]。另外,若是你願意挑戰本身,能夠查看官方文檔:Kubernetes 文檔[13]。

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 集羣組件[14]。

節點

節點node是工做單位,形式能夠是虛擬機、物理機,也能夠是各類類型的雲主機。

Pod

Pod 是本地容器邏輯上組成的集合,即一個 Pod 中可能包含若干個容器。Pod 建立後具備本身的 DNS 和虛擬 IP,這樣 Kubernetes 能夠對到達流量進行負載均衡。你幾乎不須要直接和容器打交道;即便是調試的時候,例如查看日誌,你一般調用 kubectl logs deployment/your-app -f 查看部署日誌,而不是使用 -c container_name 查看具體某個容器的日誌。-f 參數表示從日誌尾部進行流式輸出。

部署

在 Kubernetes 中建立任何類型的資源時,後臺使用一個部署deployment組件,它指定了資源的指望狀態。使用部署對象,你能夠將 Pod 或服務變動爲另外的狀態,也能夠更新應用或上線新版本應用。你通常不會直接操做副本組 (後續會描述),而是經過部署對象建立並管理。

服務

默認狀況下,Pod 會獲取一個 IP 地址。但考慮到 Pod 是 Kubernetes 中的易失性組件,咱們須要更加持久的組件。不管是隊列,MySQL、內部 API 或前端,都須要長期運行並使用保持不變的 IP 或更好的 DNS 記錄。

爲解決這個問題,Kubernetes 提供了服務service組件,能夠定義訪問模式,支持的模式包括負載均衡、簡單 IP 或內部 DNS。

Kubernetes 如何獲知服務運行正常呢?你能夠配置健康性檢查和可用性檢查。健康性檢查是指檢查容器是否處於運行狀態,但容器處於運行狀態並不意味着服務運行正常。對此,你應該使用可用性檢查,即請求應用的一個特別接口endpoint。

因爲服務很是重要,推薦你找時間閱讀如下文檔:服務[15]。嚴肅的說,須要閱讀的東西不少,有 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

在這個示例部署中,咱們作了以下操做:

  • (1) 使用 kind 關鍵字定義模板類型
  • (2) 使用 metadata 關鍵字,增長該部署的識別信息
  • (3) 使用 labels 標記每一個須要建立的資源
  • (4) 而後使用 spec 關鍵字描述所需的狀態
  • (5) nginx 應用須要 3 個副本
  • (6) Pod 中容器的模板定義部分
  • 容器名稱爲 nginx
  • 容器模板爲 nginx:1.7.9 (本例使用 Docker 鏡像)

副本組

副本組ReplicaSet是一個底層的副本管理器,用於保證運行正確數目的應用副本。相比而言,部署是更高層級的操做,應該用於管理副本組。除非你遇到特殊的狀況,須要控制副本的特性,不然你幾乎不須要直接操做副本組。

守護進程組

上面提到 Kubernetes 始終使用標籤,還有印象嗎?守護進程組DaemonSet是一個控制器,用於確保守護進程化的應用一直運行在具備特定標籤的節點中。

例如,你將全部節點增長 logger 或 mission_critical 的標籤,以便運行日誌 / 審計服務的守護進程。接着,你建立一個守護進程組並使用 logger 或 mission_critical 節點選擇器。Kubernetes 會查找具備該標籤的節點,確保守護進程的實例一直運行在這些節點中。於是,節點中運行的全部進程均可以在節點內訪問對應的守護進程。

以個人應用爲例,NSQ 守護進程能夠用守護進程組實現。具體而言,將對應節點增長 recevier 標籤,建立一個守護進程組並配置 receiver 應用選擇器,這樣這些節點上就會一直運行接收者組件。

守護進程組具備副本組的所有優點,可擴展且由 Kubernetes 管理,意味着 Kubernetes 管理其全生命週期的事件,確保持續運行,即便出現故障,也會當即替換。

擴展

在 Kubernetes 中,擴展是稀鬆日常的事情。副本組負責 Pod 運行的實例數目。就像你在 nginx 部署那個示例中看到的那樣,對應設置項 replicas:3。咱們能夠按應用所需,讓 Kubernetes 運行多份應用副本。

固然,設置項有不少。你能夠指定讓多個副本運行在不一樣的節點上,也能夠指定各類不一樣的應用啓動等待時間。想要在這方面瞭解更多,能夠閱讀 水平擴展[16] 和 Kubernetes 中的交互式擴展[17];固然 副本組[18] 的細節對你也有幫助,畢竟 Kubernetes 中的擴展功能都來自於該模塊。

Kubernetes 部分小結

Kubernetes 是容器編排的便捷工具,工做單元爲 Pod,具備分層架構。最頂層是部署,用於操做其它資源,具備高度可配置性。對於你的每一個命令調用,Kubernetes 提供了對應的 API,故理論上你能夠編寫本身的代碼,向 Kubernetes API 發送數據,獲得與 kubectl 命令一樣的效果。

截至目前,Kubernetes 原生支持全部主流雲服務供應商,並且徹底開源。若是你願意,能夠貢獻代碼;若是你但願對工做原理有深刻了解,能夠查閱代碼:GitHub 上的 Kubernetes 項目[19]。

Minikube

接下來我會使用 Minikube[20] 這款本地 Kubernetes 集羣模擬器。它並不擅長模擬多節點集羣,但能夠很容易地給你提供本地學習環境,讓你開始探索,這很棒。Minikube 基於可高度調優的虛擬機,由 VirtualBox 相似的虛擬化工具提供。

我用到的所有 Kubernetes 模板文件能夠在這裏找到:Kubernetes 文件[21]。

注意:在你後續測試可擴展性時,會發現副本一直處於 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 和 服務概覽:

Kubernetes 應用部署實戰Kubernetes 應用部署實戰

MySQL

第一個要部署的服務是數據庫。

按照 Kubernetes 的示例 Kubenetes MySQL[22] 進行部署,便可以知足個人需求。注意:示例配置文件的 MYSQL_PASSWORD 字段使用了明文密碼,我將使用 Kubernetes Secrets[23] 對象以提升安全性。

我建立了一個 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 持久化存儲[24]。

(LCTT 譯註:使用 presistentVolumeClain 以前須要建立 presistentVolume,對於單節點可使用本地存儲,對於多節點須要使用共享存儲,由於 Pod 能夠能調度到任何一個節點)

使用以下命令部署 MySQL 服務:

kubectl apply -f mysql.yaml

這裏比較一下 create 和 applyapply 是一種宣告式declarative的對象配置命令,而 create 是命令式imperative的命令。當下咱們須要知道的是,create 一般對應一項任務,例如運行某個組件或建立一個部署;相比而言,當咱們使用 apply的時候,用戶並無指定具體操做,Kubernetes 會根據集羣目前的狀態定義須要執行的操做。故若是不存在名爲 mysql 的服務,當我執行 apply -f mysql.yaml 時,Kubernetes 會建立該服務。若是再次執行這個命令,Kubernetes 會忽略該命令。但若是我再次運行 create,Kubernetes 會報錯,告知服務已經建立。

想了解更多信息,請閱讀以下文檔:Kubernetes 對象管理[25],命令式配置[26]和宣告式配置[27]。

運行以下命令查看執行進度信息:

# 描述完整信息
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[28] 文件掛載到容器中,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 中將其設置爲無頭服務headless service,意味着該服務不使用負載均衡模式,也不使用單獨的服務 IP。DNS 將基於服務選擇器selectors。

咱們的 NSQ 查詢服務對應的選擇器爲:

selector:
    matchLabels:
      app: nsqlookup

那麼,內部 DNS 對應的實體相似於:nsqlookup.default.svc.cluster.local

無頭服務的更多細節,能夠參考:無頭服務[29]。

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 部分到此結束。截至目前,咱們已經準備好兩個主要的組件。

接收器

這部分略微複雜。接收器須要完成三項工做:

  • 建立一些部署
  • 建立 nsq 守護進程
  • 將本服務對外公開

部署
第一個要建立的部署是接收器自己,容器鏡像爲 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 設置便可。能夠經過 <NodeIP>:<NodePort> 訪問該服務。若是使用多個節點,負載均衡能夠將多個 IP 合併爲一個 IP。

更多信息,請參考文檔:服務發佈[30]。

結合上面的信息,咱們定義了接收器服務,對應的模板以下:

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 應用更好的顯示數據庫中的信息。這也是一個對外公開的服務,使用的參數能夠參考接收器。

部署後效果以下:

Kubernetes 應用部署實戰Kubernetes 應用部署實戰

回顧

到目前爲止咱們作了哪些操做呢?我一直在部署服務,用到的命令彙總以下:

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 |
|-------------|----------------------|-----------------------------|

滾動更新

滾動更新Rolling Update過程當中會發生什麼呢?

Kubernetes 應用部署實戰Kubernetes 應用部署實戰

在軟件開發過程當中,須要變動應用的部分組件是常有的事情。若是我但願在不影響其它組件的狀況下變動一個組件,咱們的集羣會發生什麼變化呢?咱們還須要最大程度的保持向後兼容性,以避免影響用戶體驗。謝天謝地,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 個半小時的時間(不考慮並行更新的情形)。

更新過程當中,一些服務器運行新代碼,一些服務器運行舊代碼。用戶請求被負載均衡到各個節點,你沒法控制請求到達哪臺服務器。若是客戶端的新接口請求被調度到運行舊代碼的服務器,請求會失敗;客戶端可能會回退使用舊接口,(但因爲咱們已經修改舊接口,本質上仍然是調用新接口),故除非請求恰好到達到運行新代碼的服務器,不然一直都會失敗。這裏咱們假設不使用粘性會話sticky sessions。

並且,一旦全部服務器更新完畢,舊版客戶端再也不可以使用你的服務。

這裏,你可能會說你並不須要保留舊代碼;某些狀況下,確實如此。所以,咱們打算直接修改舊代碼,讓其經過少許參數調用新代碼。這樣操做操做至關於移除了舊代碼。當全部客戶端遷移完畢後,這部分代碼也能夠安全地刪除。

新的接口

讓咱們添加新的路由方法:

...
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[31] 中能夠找到更多的修改方式。

至此,咱們使用兩種方法調用接收器:

# 單路徑模式
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

關於滾動更新的更多信息,能夠參考以下文檔:部署的滾動更新[32],部署的更新[33], 部署的管理[34] 和 使用副本控制器完成滾動更新[35]等。

MINIKUBE 用戶須要注意:因爲咱們使用單個主機上使用單節點配置,應用只有 1 份副本,故須要將 maxUnavailable 設置爲 1。不然 Kubernetes 會阻止更新,新版本會一直處於 Pending 狀態;這是由於咱們在任什麼時候刻都不容許出現沒有(正在運行的) receiver 容器的場景。

擴展

Kubernetes 讓擴展成爲至關容易的事情。因爲 Kubernetes 管理整個集羣,你僅需在模板文件中添加你須要的副本數目便可。

這篇文章已經比較全面了,但文章的長度也愈來愈長。我計劃再寫一篇後續文章,在 AWS 上使用多節點、多副本方式實現擴展。敬請期待。

清理環境

kubectl delete deployments --all
kubectl delete services -all

寫在最後的話

各位看官,本文就寫到這裏了。咱們在 Kubernetes 上編寫、部署、更新和擴展(老實說,並無實現)了一個分佈式應用。

若是你有任何疑惑,請在下面的評論區留言交流,我很樂意回答相關問題。

但願閱讀本文讓你感到愉快。我知道,這是一篇相對長的文章,我也曾經考慮進行拆分;但整合在一塊兒的單頁教程也有其好處,例如利於搜索、保存頁面或更進一步將頁面打印爲 PDF 文檔。

Gergely 感謝你閱讀本文。

相關文章
相關標籤/搜索