1. 程式人生 > >[譯]Kubernetes 分布式應用部署和人臉識別 app 實例

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

match RR 爭辯 interact 個人 echo 建立 HR 定義

  • 原文地址:KUBERNETES DISTRIBUTED APPLICATION DEPLOYMENT WITH SAMPLE FACE RECOGNITION APP
  • 原文作者:skarlso
  • 譯文出自:掘金翻譯計劃

技術分享圖片

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

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

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

準備好進入正題了嗎?

關於應用

摘要

技術分享圖片

應用程序由六個部分組成。代碼倉庫可在這裏找到: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 調用上實現了一個小小的斷路器作為樂趣。

它是這樣工作的:

技術分享圖片

正如你所見到的,在服務中一旦有 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,當然還有一個 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 概述:

技術分享圖片

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,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 應用程序,它能夠方便地展示數據庫中的信息。這也是一個面向公眾的接收服務,其參數與接收器相同。

它看起來像這樣:

技術分享圖片

總結

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

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

滾動更新

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

技術分享圖片

正如在軟件開發過程中發生的那樣,系統的某些部分需要/需要進行更改。那麽,如果我改變其中一個組件而不影響其他組件,同時保持向後兼容性而不中斷用戶體驗,我們的集群會發生什麽?幸運的是 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 編寫,部署,更新和擴展了(當然還不是真的)分布式應用程序。

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

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

感謝您的閱讀。

  • 本文永久鏈接:https://github.com/xitu/gold-miner/blob/master/TODO1/kubernetes-distributed-application.md
  • 譯者:maoqyhz
  • 校對者:cf020031308、HCMY
  • 如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。

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