[译] Kubernetes 分布式应用部署和人脸识别 app 实例

好的,伙计,让咱们静下心来。下面将会是一个漫长但充满但愿和有趣的旅程。前端

我将使用 Kubernetes 部署分布式应用程序。我试图建立一个相似于真实世界 app 的应用程序。显然,因为时间和精力有限,我不得不忽略一些细节部分。node

个人重点将放在 Kubernetes 和应用部署上。python

准备好进入正题了吗?mysql

关于应用

摘要android

kube overview

应用程序由六个部分组成。代码仓库可在这里找到:Kube Cluster Sampleios

这是一我的脸识别的服务应用,它能够识别人物的图像并将其和已知人物进行比较。识别结果会在一个简单的前端中,经过表格的形式展示出来,能够看到这些待识别的图像中的人物是谁。应用的运行过程以下:首先向接收器发送请求,请求中须要包含图像的路径。这些图像可存储在 NFS 一类的地方,同时接收器会将图像路径存储在 DB(MySQL)中。最后向队列发送一个处理请求,包含保存图像的 ID。这里使用 NSQ 做为队列(译者注:NSQ 是一个基于 Go 语言的分布式实时消息平台)。nginx

期间,图像处理服务会不间断地监视将要执行做业的队列。处理流程由如下步骤组成:取 ID;加载图像;最后,经过 gRPC 将图像发送到用 Python 编写的人脸识别后端程序。若是识别成功,后端将返回与该图像中人物相对应的名称。而后,图像处理器会更新图像记录的人物 ID 字段,并将图像标记为“processed successfully”。若是识别不成功,图像将被保留为“pending”。 若是在识别过程当中出现故障,图像将被标记为“failed”。git

处理失败的图像能够经过 cron 做业重试,例如:github

那么这是如何工做的?让咱们来看看 。golang

接收器

接收器服务是整个流程的起点。这个 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索