[譯] Kubernetes 分散式應用部署和人臉識別 app 例項

_Fururur發表於2019-03-04

[譯] Kubernetes 分散式應用部署和人臉識別 app 例項

好的,夥計,讓我們靜下心來。下面將會是一個漫長但充滿希望和有趣的旅程。

我將使用 Kubernetes 部署分散式應用程式。我試圖建立一個類似於真實世界 app 的應用程式。顯然,由於時間和精力有限,我不得不忽略一些細節部分。

我的重點將放在 Kubernetes 和應用部署上。

準備好進入正題了嗎?

關於應用

摘要

kube overview

應用程式由六個部分組成。程式碼倉庫可在這裡找到:Kube Cluster Sample

這是一個人臉識別的服務應用,它可以識別人物的影象並將其和已知人物進行比較。識別結果會在一個簡單的前端中,通過表格的形式展現出來,可以看到這些待識別的影象中的人物是誰。應用的執行過程如下:首先向接收器傳送請求,請求中需要包含影象的路徑。這些影象可儲存在 NFS 一類的地方,同時接收器會將影象路徑儲存在 DB(MySQL)中。最後向佇列傳送一個處理請求,包含儲存影象的 ID。這裡使用 NSQ 作為佇列(譯者注:NSQ 是一個基於 Go 語言的分散式實時訊息平臺)。

期間,影象處理服務會不間斷地監視將要執行作業的佇列。處理流程由以下步驟組成:取 ID;載入影象;最後,通過 gRPC 將影象傳送到用 Python 編寫的人臉識別後端程式。如果識別成功,後端將返回與該影象中人物相對應的名稱。然後,影象處理器會更新影象記錄的人物 ID 欄位,並將影象標記為“processed successfully”。如果識別不成功,影象將被保留為“pending”。 如果在識別過程中出現故障,影象將被標記為“failed”。

處理失敗的影象可以通過 cron 作業重試,例如:

那麼這是如何工作的?讓我們來看看 。

接收器

接收器服務是整個流程的起點。這個 API 接收如下格式的請求:

curl -d '{"path":"/unknown_images/unknown0001.jpg"}' http://127.0.0.1:8000/image/post
複製程式碼

在這個例子中,接收器通過共享資料庫叢集來儲存影象路徑。當資料庫儲存影象路徑成功後,接收器例項就能從資料庫服務中接收影象 ID。此應用程式是基於在持久層提供實體物件唯一標識的模型的。一旦 ID 產生,接收器會向 NSQ 傳送一個訊息。到這裡,接收器的工作就完成了。

影象處理器

下面是激動人心的開始。當影象處理器第一次執行時,它會建立兩個 Go 協程(routine)。 他們是:

Consume

這是一個 NSQ 消費者。它有三個必要的工作。首先,它能夠監聽佇列中的訊息。其次,當其接收到訊息後,會將收到的 ID 新增到第二個例程處理的執行緒安全的 ID 切片中去。最後,它通過 sync.Condition 告知第二個協程有工作要做。

ProcessImages

該例程處理 ID 切片,直到切片完全耗盡。一旦切片消耗完,例程將暫停而不是等待 channel。以下是處理單個 ID 的步驟:

  • 與人臉識別服務建立 gRPC 連線(在下面人臉識別章節解釋)
  • 從資料庫中取回影象記錄
  • 設定 斷路器 的兩個函式
    • 函式 1: 執行 RPC 方法呼叫的主函式
    • 函式 2: 對斷路器的 Ping 進行健康檢查
  • 呼叫函式 1,傳送影象路徑到人臉識別服務。服務需要能夠訪問該路徑。最好能像 NFS 一樣進行檔案共享
  • 如果呼叫失敗,更新影象記錄的狀態欄位為“FAILED PROCESSING”
  • 如果成功,將會返回資料庫中與圖片相關的人物名。它會執行一個 SQL 的連線查詢,獲取到相關的人物 ID
  • 更新資料庫中圖片記錄的狀態欄位為“PROCESSED”,以及人物欄位為識別出的人物 ID

這個服務可以被複制,換句話說,可以同時執行多個服務。

斷路器

雖然這是一個不需要太大精力就能夠複製資源的系統,但仍可能存在狀況,例如網路故障、服務間的通訊問題。因此我在 gRRC 呼叫上實現了一個小小的斷路器作為樂趣。

它是這樣工作的:

kube circuit

正如你所見到的,在服務中一旦有 5 個不成功的呼叫,斷路器將會被啟用,並且不允許任何呼叫通過。經過一段配置的時間後,會向服務傳送一個 Ping 呼叫,並檢測服務是否返回資訊。如果仍然出錯,會增加超時時間,否則就會開啟,允許流量通過。

前端

這只是一個簡單的表格檢視,使用 Go 自帶的 HTML 模板來渲染影象列表。

人臉識別

這裡是識別魔術發生的地方。為了追求靈活性,我決定將人臉識別這項功能封裝成為基於 gRPC 的服務。我開始打算用 Go 語言去編寫,但後來發現使用 Python 來實現會更加清晰。事實上,除了 gPRC 程式碼之外,人臉識別部分大概需要 7 行 Python 程式碼。我正在使用一個極好的庫,它包含了所有 C 實現的 OpenCV 的呼叫。人臉識別。在這裡簽訂 API 使用協議,也就意味著在協議的許可下,我可以隨時更改人臉識別程式碼的實現。

請注意,這裡存在一個可以用 Go 語言來開發 OpenCV 的庫。我差點就用它了,但是它並沒有包含 C 實現的 OpenCV 的呼叫。這個庫叫做 GoCV,你可以去了解一下。它們有些非常了不起的地方,比如,實時的攝像頭反饋處理,只需要幾行程式碼就能夠實現。

python 的庫本質上很簡單。現在,我們有一組已知的人物影象,並將其命名為 hannibal_1.jpg, hannibal_2.jpg, gergely_1.jpg, john_doe.jpg 放在資料夾中。在資料庫中包含兩張表,分別是 personperson_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 守護程式。由於守護程式的資源要求很小,不會影響主應用程式的需求。

配置

為了儘可能靈活,以及使用 Kubernetes 的 ConfigSet,我在開發中使用 .env 檔案來儲存配置,如資料庫服務的位置或 NSQ 的查詢地址。 在生產中,這意味著在 Kubernetes 環境中,我將使用環境變數。

人臉識別應用程式總結

這就是我們即將部署的應用程式的架構。它的所有元件都是可變的,只能通過資料庫,佇列和 gRPC 進行耦合。由於更新機制的工作原因,這在部署分散式應用程式時非常重要。我將在“部署”部分中介紹該部分。

在 Kubernetes 中部署應用

基礎

什麼 Kubernetes?

我將在這裡介紹一些基礎知識,但不會過多介紹細節。如果你想了解更多,可閱讀的整本書:Kubernetes Up And Running。另外,如果你足夠大膽,你可以看看這個文件:Kubernetes Documentation

Kubernetes 是一個容器化的服務和應用程式管理平臺。它容易擴充套件,可管理一大堆容器,最重要的是,它可以通過基於 yaml 的模板檔案高度配置。人們經常將 Kubernetes 與Docker 叢集進行比較,但 Kubernetes 確實不止於此!例如:它可以管理不同的容器。你可以使用 Kubernetes 來對LXC 進行管理和編排,同時也可以使用相同的方式管理 Docker。它提供了一個高於管理已部署服務和應用程式叢集的層。怎麼樣?讓我們快速瀏覽一下 Kubernetes 的構建模組吧。

在 Kubernetes 中,您將描述應用程式的期望狀態,Kubernetes 會做一些事情,使之達到這個狀態。狀態可能是部署、暫停、重複兩次等等。

Kubernetes 的基礎知識之一是它為所有元件使用標籤和註解。Services,Deployments,ReplicaSets,DaemonSets,一切都能夠被標記。考慮以下情況。為了確定哪個 pod 屬於哪個應用程式,我們將會使用了一個名為 app:myapp 的標籤。假設您已部署了此應用程式的兩個容器; 如果您從其中一個容器中移除標籤 app,則 Kubernetes 只會檢測到一個標籤,因此會啟動一個新的 myapp 例項。

Kubernetes Cluster

對於 Kuberenetes 的工作,需要有 Kubernetes 叢集的存在。配置叢集可能是非常痛苦的,但幸運的是,幫助就在眼前。Minikube 在本地為我們配置一個帶有一個節點的叢集。AWS 有一個以 Kubernetes 叢集形式執行的測試服務,其中您唯一需要做的就是請求節點並定義你的部署。Kubernetes 叢集元件的文件在此處:Kubernetes Cluster Components

Nodes

一個節點就是一臺工作主機。它可以是任何事物,例如物理機、虛擬機器以及各種雲服務提供的虛擬資源。

Pods

Pods 是一個邏輯上分組的容器,也就意味著一個 Pod 可以容納多個容器。一個 Pod 在建立後會獲得自己的 DNS 和虛擬 IP 地址,這樣Kubernetes 就可以為其平衡流量。你很少需要直接處理容器,即使在除錯時(比如檢視日誌),通常也會呼叫 kubectl logs deployment / your-app -f 而不是檢視特定的容器。儘管有可能會呼叫 -c container_name-f 引數會持續顯示日誌檔案的末尾部分。

Deployments

在 Kubernetes 中建立任何型別的資源時,它將在後臺使用 Deployment。一個 Deployment 物件描述當前應用程式的期望狀態。這東西可以用來變換 Pod 或 Service 的狀態,更新或推出新版的應用。您不直接控制 ReplicaSet(如稍後所述),但可以控制 Deployment 物件來建立和管理 ReplicaSet。

Services

預設情況下,Pod 會得到一個 IP 地址。然而,因為 Pods 在 Kubernetes 中是一個不穩定的東西,所以你需要更持久的東西。佇列、mysql、內部API、前端,這些需要長時間執行並且需要在一個靜態的,不變的IP或最好是 DNS 記錄之後。

為此,Kubernetes 提供可定義可訪問模式的 Services。負載均衡,簡單 IP 或內部 DNS。

Kubernetes 如何知道服務是否正確執行?你可以配置執行狀況檢查和可用性檢查。執行狀況檢查將檢查容器是否正在執行,但這並不意味著你的服務正在執行。為此,你需要在您的應用程式中對可用的端點進行可用性檢查。

由於 Services 非常重要,我建議你稍後在這裡閱讀它們:Services。預先提醒,這部分文件內容很多,有 24 個 A4 大小的頁面,內容包含網路、服務和發現。但是這對於你是否決定要在生產環境中使用 Kubernetes 是至關重要的。

DNS / Service Discovery

如果您在叢集中建立服務,該服務將獲取由特殊的Kubernetes Deployments 物件(被稱作為 kube-proxy 和 kube-dns)提供的在 Kubernetes 中的 DNS 記錄。這兩個物件在叢集中提供了服務發現。如果您執行了mysql服務並設定了 clusterIP:none,那麼叢集中的每個人都可以通過 ping mysql.default.svc.cluster.local 來訪問該服務。 其中:

  • mysql – 服務的名稱
  • default – 名稱空間名稱
  • svc – 服務本身
  • cluster.local – 本地叢集域名

該域名可以通過自定義來更改。要訪問叢集外部的服務,必須有 DNS 提供者,再使用Nginx(例如)將IP地址繫結到記錄。可以使用以下命令查詢服務的公共IP地址:

  • NodePort – kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services mysql
  • LoadBalancer – kubectl get -o jsonpath="{.spec.ports[0].LoadBalancer}" services mysql

Template Files

像 Docker Compose、TerraForm 或其他服務管理工具一樣,Kubernetes 也提供了配置模板的基礎設施。這意味著你很少需要手工做任何事情。

例如,請看下面使用 yaml 檔案來配置 nginx 部署的模板:

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) 新增可識別此部署的後設資料以及使用 label 建立每一個資源 (3)
  • (4) 然後描述所需要的狀態規格。
  • (5) 對於 nginx 應用程式,包含 3 個 replicas
  • (6) 這是關於容器的模板定義。這裡配置的 Pod 包含一個 name 為 nginx 的容器。其中,使用 1.7.9 版本的 nginx 映象(這個例子中使用的是 Docker),暴露的埠號為:80

ReplicaSet

ReplicaSet 是低階複製管理器。 它確保為應用程式執行正確數量的複製。 但是,當部署處於較高階別,應始終管理 ReplicaSets。你很少需要直接使用 ReplicaSets,除非您有一個需要控制複製細節的特殊案例。

DaemonSet

還記得我說的Kubernetes是如何持續使用標籤的嗎?DaemonSet 是一個控制器,用於確保守護程式應用程式始終在具有特定標籤的節點上執行。

例如:您希望所有標有 loggermission_critical 的節點執行記錄器/審計服務守護程式。然後你建立一個 DaemonSet,並給它一個名為 loggermission_critical 的節點選擇器。Kubernetes 將尋找具有該標籤的節點。始終確保它將有一個守護程式的例項在其上執行。因此,在該節點上執行的每個例項都可以在本地訪問該守護程式。

在我的應用程式中,NSQ 守護程式可能是一個 DaemonSet。為了確保它在具有接收器元件的節點上執行,我採用 receiver 標記一個節點,並用 receiver 應用程式選擇器指定一個 DaemonSet。

DaemonSet 具有 ReplicaSet 的所有優點。它是可擴充套件的並由Kubernetes管理它。這意味著,所有的生命週期事件都由 Kube 處理,確保它永不消亡,並且一旦發生,它將立即被替換。

Scaling

在 Kubernetes 中做擴充套件很簡單。ReplicaSets 負責管理 Pod 的例項數量,如 nginx 部署中所看到的,使用“replicas:3”設定。我們應該以允許 Kubernetes 執行它的多個副本的方式編寫我們的應用程式。

當然這些設定是巨大的。你可以指定哪些複製必須在什麼節點上執行,或者在各種等待時間等待例項出現的時間。你可以在這裡閱讀關於此主題的更多資訊:Horizontal Scaling 和此處:[Interactive Scaling with Kubernetes](https:/ /kubernetes.io/docs/tutorials/kubernetes-basics/scale-interactive/),當然還有一個 ReplicaSet 控制元件的詳細資訊 所有的 scaling 都可以在 Kubernetes 中實現。

Kubernetes 總結

這是一個處理容器編排的便利工具。 它的基本單位是具有分層的架構的 Pods。頂層是 Deployments,通過它處理所有其他資源。它高度可配置,提供了一個用於所有呼叫的 API,因此比起執行 kubectl,你可以編寫自己的邏輯將資訊傳送到 Kubernetes API。

Kubernetes 現在支援所有主要的雲提供商,它完全是開源的,隨意貢獻!如果你想深入瞭解它的工作方式,請檢視程式碼:Kubernetes on Github

Minikube

我將使用 Minikube。Minikube 是一個本地 Kubernetes 叢集模擬器。儘管模擬多個節點並不是很好,但如果只是著手去學習並在本地折騰一下的話,這種方式不需要任何的開銷,是極好的。Minikube是基於虛擬機器的,如果需要的話,可以使用 VirtualBox 等進行微調。

所有我將要使用的 kube 模板檔案可以在這裡找到:Kube files

**注意:**如果稍後想要使用 scaling 但注意到複製總是處於“Pending”狀態,請記住 minikube 僅使用單個節點。它可能不允許同一節點上有多個副本,或者只是明顯耗盡了資源。您可以使用以下命令檢查可用資源:

kubectl get nodes -o yaml
複製程式碼

建立容器

Kubernetes 支援大部分容器。我將要使用 Docker。對於我構建的所有服務,儲存庫中都包含一個 Dockerfile。我鼓勵你去研究它們。他們大多數都很簡單。對於 Go 服務,我正在使用最近引入的多階段構建。Go 服務是基於 Alpine Linux 的。人臉識別服務是 Python實現的。NSQ 和 MySQL 正在使用他們自己的容器。

上下文

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

部署應用

Pods 和 Services 概述:

kube deployed

MySQL

我要部署的第一個 Service 是我的資料庫。

我正在使用位於此處的 Kubernetes 示例 Kube MySQL,它符合我的需求。請注意,該配置檔案正在使用明文密碼。我將按照此處所述 Kubernetes Secrets做一些安全措施。

如文件中描述的那樣,我使用保密的 yaml 在本地建立了一個祕鑰檔案。

apiVersion: v1
kind: Secret
metadata:
    name: kube-face-secret
type: Opaque
data:
    mysql_password: base64codehere
複製程式碼

我通過以下命令建立了base64程式碼:

echo -n "ubersecurepassword" | base64
複製程式碼

這是您將在我的部署yaml檔案中看到的內容:

...
- name: MYSQL_ROOT_PASSWORD
    valueFrom:
    secretKeyRef:
        name: kube-face-secret
        key: mysql_password
...
複製程式碼

另外值得一提的是:它使用一個 volume 來儲存資料庫。volume 定義如下:

...
        volumeMounts:
        - name: mysql-persistent-storage
            mountPath: /var/lib/mysql
...
        volumes:
        - name: mysql-persistent-storage
        persistentVolumeClaim:
            claimName: mysql-pv-claim
...
複製程式碼

presistentVolumeClain 在這裡是關鍵。這告訴 Kubernetes 這個資源需要一個持久的 volume。如何提供它是從使用者抽象出來的。你可以確定 Kubernetes 將提供 volume。它與 Pods 類似。要閱讀詳細資訊,請檢視此文件:Kubernetes Persistent Volumes

使用以下命令完成部署 mysql 服務:

kubectl apply -f mysql.yaml
複製程式碼

apply 還是 create?簡而言之,apply 被認為是宣告性的物件配置命令,而 create 則是命令式的。這意味著現在“create”通常是針對其中一項任務的,比如執行某些東西或建立 Deployment。而在使用 apply 時,使用者不會定義要採取的操作。這將由 Kubernetes 根據叢集的當前狀態進行定義。因此,當沒有名為 mysql 的服務時,我呼叫 apply -f mysql.yaml,它會建立服務。再次執行時,Kubernetes 不會做任何事情。但是,如果我再次執行 create,它會丟擲一個錯誤,說明服務已經被建立。

有關更多資訊,請檢視以下文件:Kubernetes Object Management,[Imperative Configuration](https:// kubernetes .io / docs / concepts / overview / object-management-kubectl / imperative-config /),Declarative Configuration)。

要檢視進度資訊,請執行:

# 描述整個程式
kubectl describe deployment mysql
# 僅顯示 pod
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
複製程式碼

** 需要了解的是 **:如果你現在更改密碼,重新應用 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
複製程式碼

你還會注意到我已經在這裡安裝了一個檔案:Database Setup SQL到容器中。MySQL 容器自動執行這些。該檔案將初始化一些資料以及我將要使用的模式。

volume 定義如下:

    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
複製程式碼

要檢查引導指令碼是否成功,請執行以下命令:

~/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>
複製程式碼

這結束了資料庫服務設定。可以使用以下命令檢視該服務的日誌:

kubectl logs deployment/mysql -f
複製程式碼

NSQ 查詢

NSQ 查詢將作為內部服務執行,它不需要從外部訪問。所以我設定了 clusterIP:None,這會告訴 Kubernetes 這項服務是一項無頭(headless)的服務。這意味著它不會被負載均衡,並且不會是單一的 IP 服務。DNS 將會基於服務選擇器。

我們定義的 NSQ Lookup 選擇器是:

selector:
matchLabels:
    app: nsqlookup
複製程式碼

因此,內部 DNS 將如下所示:nsqlookup.default.svc.cluster.local

無頭服務在這裡詳細描述:Headless Service

基本上它和 MySQ L一樣,只是稍作修改。如前所述,我使用的是 NSQ 自己的 Docker 映象,名為 nsqio / nsq。所有的 nsq 命令都在那裡,所以 nsqd 也將使用這個映象,只是命令有所不同。對於 nsqlookupd,命令是:

command: ["/nsqlookupd"]
args: ["--broadcast-address=nsqlookup.default.svc.cluster.local"]
複製程式碼

你可能會問什麼是 --broadcast-address?預設情況下,nsqlookup 將使用 hostname 作為廣播地址 當消費者執行回撥時,它會嘗試連線到類似於 http://nsqlookup-234kf-asdf:4161/lookup?topics=image 的 url。請注意 nsqlookup-234kf-asdf 是容器的主機名。通過將廣播地址設定為內部 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: tcp
    protocol: TCP
    port: 4160
    targetPort: 4160
    - name: http
    protocol: TCP
    port: 4161
    targetPort: 4161
複製程式碼

name 是 Kubernetes 需要的。

要建立此服務,我使用與以前相同的命令:

kubectl apply -f nsqlookup.yaml
複製程式碼

到這裡,有關於 nsqlookupd 的就結束了。

接收器

這是一個更復雜的問題。接收器會做三件事情:

  • 建立一些 deployments
  • 建立 nsq 守護程式
  • 向公眾提供服務

Deployments

它建立的第一個 deployment 物件是它自己的。Receiver的容器是 skarlso / kube-receiver-alpine

Nsq 守護程式

Receiver 啟動一個 nsq 守護程式。如前所述,接收者用它自己執行 nsqd。它這樣做可以在本地通訊而不是通過網路。通過讓接收器執行此操作,它們將在同一節點上結束。

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 這兩個引數。查詢 tcp 地址是 nsqlookupd 服務的 DNS。廣播地址是必要的,就像 nsqlookupd 一樣,所以回撥工作正常。

面向大眾的服務

現在,這是我第一次部署面向公眾的服務。這裡有兩種選擇。我可以使用 LoadBalancer,因為這個 API 可以承受很大的負載。如果這將在生產環境部署,那麼它應該使用這一個。

我在本地做只部署單個節點的,所以稱為“NodePort”就足夠了。一個 NodePort 在一個靜態埠上暴露每個節點 IP 上的服務。如果未指定,它將在 30000-32767 之間的主機上分配一個隨機埠。但它也可以被配置為一個特定的埠,在模板檔案中使用 nodePort。要使用此服務,請使用 <NodeIP>:<NodePort>。如果配置了多個節點,則 LoadBalancer 可以將它們複用到單個 IP。

有關更多資訊,請檢視此文件:Publishing Service

綜合起來,我們會得到一個接收服務,其模板如下:

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
複製程式碼

影象處理器

影象處理器是我處理傳遞影象以識別的地方。它應該有權訪問 nsqlookupd,mysql 和人臉識別服務的 gRPC 端點。這實際上是相當無聊的服務。事實上,它甚至不是一項服務。它不會公開任何內容,因此它是第一個部署的元件。為簡潔起見,以下是整個模板:

---
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: 如果這裡的 3306 沒有引號,kubectl 會出現錯誤
            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
複製程式碼

人臉識別

人臉識別別服務是一個簡單的,只有影象處理器才需要的服務。它的模板如下:

apiVersion: v1
kind: Service
metadata:
    name: face-recog
spec:
    ports:
    - protocol: TCP
    port: 50051
    targetPort: 50051
    selector:
    app: face-recog
    clusterIP: None
複製程式碼

更有趣的部分是它需要兩個 volume。這兩 volume 是 known_peopleunknown_people。你能猜到他們將包含什麼嗎?是的,影象。“known_people” volume 包含與資料庫中已知人員關聯的所有影象。unknown_people volume 將包含所有新影象。這就是我們從接收器傳送影象時需要使用的路徑; 那就是掛載點所指向的地方,在我的情況下是 / unknown_people。 基本上,路徑必須是人臉識別服務可以訪問的路徑。

現在,通過 Kubernetes 和 Docker部署 volume 很容易。它可以是掛載的 S3 或某種型別的 nfs,也可以是從主機到客戶機的本地掛載。也會存在其他可能性。為了簡單起見,我將使用本地安裝。

安裝一個 volume 分兩部分完成。首先,Dockerfile 必須指定 volume:

VOLUME [ "/unknown_people", "/known_people" ]
複製程式碼

其次,Kubernetes 模板需要在 MySQL 服務中新增 volumeMounts,不同之處在於 hostPath 並不是聲稱的 volume:

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
複製程式碼

我們還需要為人臉識別服務設定 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)]
複製程式碼

Neat.

現在,如果接收方收到一個請求(並將其傳送到更遠的線路),與下面的請求類似。

curl -d '{"path":"/unknown_people/unknown220.jpg"}' http://192.168.99.100:30251/image/post
複製程式碼

它會在 / unknown_people 下尋找名為 unknown220.jpg 的影象,在 unknown_folder 中找到與未知影象中的人相對應的影象,並返回匹配影象的名稱。

檢視日誌,你會看到如下內容:

# Receiver
❯ 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

# Image Processor
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
複製程式碼

這樣,所有服務就部署完成了。

前端

最後,還有一個小型的 web 應用程式,它能夠方便地展示資料庫中的資訊。這也是一個面向公眾的接收服務,其引數與接收器相同。

它看起來像這樣:

frontend

總結

我們現在正處於部署一系列服務的階段。回顧一下我迄今為止使用的命令:

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
複製程式碼

由於應用程式不會在啟動時分配連線,因此可以按任意順序排列。(除了 image_processor 的 NSQ 消費者。)

如果沒有錯誤,使用 kubectl get pods 查詢執行 pod 的 kube 應該顯示如下:

❯ 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 |
|-------------|----------------------|-----------------------------|
複製程式碼

滾動更新

滾動更新過程中會發生什麼?

kube rotate

正如在軟體開發過程中發生的那樣,系統的某些部分需要/需要進行更改。那麼,如果我改變其中一個元件而不影響其他元件,同時保持向後相容性而不中斷使用者體驗,我們的叢集會發生什麼?幸運的是 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臺伺服器,並且你做了一個緩慢的滾動更新,在更新的同時一次只取出一臺伺服器。如果更新持續一分鐘左右,整個過程大約需要一個半小時才能完成(不包括任何並行更新)。

在此期間,你的一些伺服器將執行新程式碼,其中一些將執行舊程式碼。呼叫是負載均衡的,因此你無法控制哪些伺服器會被擊中。如果客戶試圖以新的方式進行呼叫,但會觸及舊伺服器,則客戶端將失敗。客戶端可以嘗試並回退,但是由於你刪除了舊版本,它將不會成功,除非很巧合地命中了執行新程式碼的伺服器,用新程式碼命中伺服器(假設沒有設定粘滯會話)。

另外,一旦所有伺服器都更新完畢,舊客戶端將無法再使用你的服務。

現在,你可以爭辯說,你不想永遠保留你的程式碼的舊版本。這在某種意義上是正確的。這就是為什麼我們要修改舊程式碼,只需稍微增加一點就可以呼叫新程式碼。這樣,一旦所有客戶端都被遷移了,程式碼就可以簡單地被刪除而不會有任何問題。

新的端點

我們來新增一個新的路徑方法:

...
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:Rolling Update Bulk Image Path PR

現在,可以通過兩種方式呼叫接收器:

# 單個路徑:
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 和其他用來批量影象處理的操作,他們仍然會一個一個接收。這就當作業留給你們來做了。

新的映象

要執行滾動更新,我必須首先從接收器服務建立一個新映象。

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 建議使用 ReplicaSets 進行部署應處理分發這意味著滾動更新必須至少有兩個副本。如果少於兩個副本存在,則更新將不起作用(除非 maxUnavailable 設定為 1)。我增加了 yaml 的副本數量。我還為接收器容器設定了新的映象版本。

    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
複製程式碼

有關滾動更新的更多資訊,請參見以下文件:Deployment Rolling Update, Updating a Deployment, Manage Deployments, Rolling Update using ReplicaController

MINIKUBE 的使用者注意:由於我們在具有一個節點和一個應用程式副本的本地機器上執行此操作,我們必須將 maxUnavailable 設定為 1; 否則 Kubernetes 將不允許更新發生,並且新版本將保持 Pending 狀態。這是因為我們不允許存在沒有執行容器的服務,這基本上意味著服務中斷。

Scaling

用 Kubernetes 來 scaling 比較容易。由於它正在管理整個叢集,因此基本上只需將一個數字放入所需副本的模板中即可使用。

迄今為止這是一篇很棒的文章,但時間太長了。我正在計劃編寫一個後續行動,我將通過多個節點和副本真正擴充套件 AWS 的功能; 再加上 Kops 部署 Kubernetes 叢集。敬請期待!

清理

kubectl delete deployments --all
kubectl delete services -all
複製程式碼

寫在最後

女士們,先生們。我們用 Kubernetes 編寫,部署,更新和擴充套件了(當然還不是真的)分散式應用程式。

如果您有任何問題,請隨時在下面的評論中討論。我非常樂意解答。

我希望你享受閱讀它,雖然這很長, 我正在考慮將它分成多篇部落格,但是一個整體的單頁指南是有用的,並且可以很容易地找到,儲存和列印。

感謝您的閱讀。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章