现在,我们看一下 gRPC 应用程序的多种部署方法。如果你想在本地或VM 中运行 gRPC 服务器端和客户端应用程序,部署仅仅依赖于你为gRPC 应用程序的编程语言所生成的二进制文件。对于本地和基于 VM的部署来讲,gRPC 服务器端应用程序的扩展和高可用性通常是通过标准的部署实践来实现的,比如使用支持 gRPC 协议的负载均衡器。
大多数现代应用程序会部署为容器。因此,学习如何将 gRPC 应用程序部署在容器中是非常有用的。Docker 是基于容器进行应用程序部署的标准平台。
7.2.1 部署到Docker上
Docker 是一个开发、发布和运行应用程序的开放平台。借助 Docker,可以将应用程序与基础设施分离开来。它能够在隔离环境(容器)中打包和运行应用程序,这样就可以在同一台主机上运行多个容器了。容器要比常规的 VM 轻得多,可以直接在宿主机的内核上运行。
下面看一些将 gRPC 应用程序部署为 Docker 容器的示例。
关于 Docker 的基础知识已经超出了本书的范围。因此,如果你不熟悉 Docker,请参考 Docker 文档和其他资源。
在开发了 gRPC 服务器端应用程序后,就可以为其创建一个 Docker 容器。代码清单 7-3 展示了一个基于 Go 语言的 gRPC 服务器端的Dockerfile。在 Dockerfile 中,有 gRPC 特有的很多构造。本例使用了多阶段 Docker 构建:第一阶段构建了应用程序,第二阶段以一个非常轻量级的运行时来运行该应用程序。在构建应用程序之前,生成的服务器端代码也添加到了容器中。
代码清单 7-3 基于 Go 语言的 gRPC 服务器端的 Dockerfile
# 多阶段构建
# 构建阶段1: ➊
FROM golang AS build
ENV location /go/src/github.com/grpc-up-and-running/samples/ch07/grpc-docker/go
WORKDIR ${location}/server
ADD ./server ${location}/server
ADD ./proto-gen ${location}/proto-gen
RUN go get -d ./... ➋
RUN go install ./... ➌
RUN CGO_ENABLED=0 go build -o /bin/grpc-productinfo-server ➍
# 构建阶段2: ➎
FROM scratch
COPY --from=build /bin/grpc-productinfo-server /bin/grpc-productinfo-server ➏
ENTRYPOINT ["/bin/grpc-productinfo-server"]
EXPOSE 50051
- ❶ 构建程序只需要 Go 语言和 Alpine Linux。
- ❷ 下载所有的依赖项。
- ❸ 安装所有的包。
- ❹ 构建服务器端应用程序。
- ❺ Go 二进制文件是自包含的可执行文件。
- ❻ 将我们在上一阶段构建的二进制文件复制到新的位置。
创建完 Dockerfile 之后,就可以使用如下命令构建 Docker 镜像了:
docker image build -t grpc-productinfo-server -f server/Dockerfile
gRPC 客户端应用程序可以按照相同的方式进行创建。这里有个特殊情况,因为是在 Docker 中运行服务器端应用程序,所以客户端应用程序连接 gRPC 的主机名和端口会有所不同。
当服务器端应用程序和客户端应用程序在 Docker 中运行时,它们需要通过主机相互通信并与外部通信。因此,这里必须涉及一个网络层。
Docker 支持不同类型的网络,每种类型都适用于特定的使用场景。当我们运行服务器端和客户端 Docker 容器时,可以指定一个通用的网络,这样客户端应用程序就能基于域名发现服务器端应用程序的位置。这意味着,客户端应用程序的代码必须进行修改才能连接到服务器的主机名上。例如,我们的 Go gRPC 应用程序必须修改成调用服务器主机名,而不是 localhost:
conn, err := grpc.Dial("productinfo:50051", grpc.WithInsecure())
我们可以从环境中读取主机名,避免在客户端应用程序中硬编码。客户端应用程序的修改完成之后,需要重新构建 Docker 镜像,并按照如下方式运行服务器端和客户端镜像:
docker run -it --network=my-net --name=productinfo \
--hostname=productinfo -p 50051:50051 grpc-productinfo-server ➊
docker run -it --network=my-net \
--hostname=client grpc-productinfo-client ➋
- ❶ 借助主机名 productinfo 和端口 50051 在 Docker 网络 my-net 上运行 gRPC 服务器。
- ❷ 在 Docker 网络 my-net 上运行 gRPC 客户端。
在启动 Docker 容器时,可以指定给定容器在哪个 Docker 网络上运行。如果服务共享同一个网络,那么客户端应用程序可以通过 docker run
命令所提供的主机名发现托管服务的实际地址。
如果所运行的容器比较少,且它们之间的交互相对简单,则我们可以把解决方案完全构建在 Docker 上。但是,大多数真实场景需要管理多个容器及其之间的交互。仅仅基于 Docker 来构建这样的解决方案就非常枯燥了,而这正是容器编排平台的用武之地。
7.2.2 部署到Kubernetes上
Kubernetes 是一个自动部署、可扩展和管理容器化应用程序的开源平台。在使用 Docker 运行容器化的 gRPC 应用程序时,并没有方便的扩展性和高可用性保障,需要在 Docker 容器之外构建这些功能。
Kubernetes 提供了范围广泛的此类功能,以便将大多数容器管理和编排的任务交给底层的 Kubernetes 平台。
Kubernetes 提供了一个可靠的和可扩展的平台,来运行容器化的工作负载。Kubernetes 负责扩展需求、故障转移、服务发现、配置管理、安全性、部署模式等。
Kubernetes 的基础知识超出了本书的范围。因此,推荐参考Kubernetes 文档和其他资源来学习它的更多知识。
接下来看一下如何将 gRPC 服务器端应用程序部署到 Kubernetes 上。
01. gRPC 服务器的 Kubernetes Deployment 资源
为了在 Kubernetes 上进行部署,需要做的第一件事情就是为 gRPC服务器端应用程序创建 Docker 容器。上一节做过完全一样的事情,在这里可以使用同一个容器。可以将容器镜像推送到一个容器注册中心,比如 Docker Hub。
对于本例,我们已将 gRPC 服务器端的 Docker 镜像推送到了Docker Hub 中名为 kasunindrasiri/grpc-productinfo-server
的 tag 下。Kubernetes 平台不会直接管理容器,而是使用一个名为 pod 的抽象。pod 是逻辑单元,可以包含一个或多个容器,Kubernetes 以 pod 作为单位实现复制功能。如果需要 gRPC 服务器端应用程序的多个实例,Kubernetes 就会创建更多的 pod。在给定pod 中运行的多个容器会共享相同的资源和本地网络。但是,在我们的场景中,只需要在 pod 中运行一个 gRPC 服务器端容器。因此,这是一个具有单个容器的 pod。Kubernetes 没有直接管理pod,而是通过另一个名为 deployment 的抽象来管理。Deployment 指定了同时要运行的 pod 的数量。当新的Deployment 创建时,Kubernetes 会根据 Deployment 的设定,生成指定数量的 pod。
要在 Kubernetes 中部署 gRPC 服务器端应用程序,需要使用 YAML描述符创建 Kubernetes Deployment,如代码清单 7-4 所示。
代码清单 7-4 基于 Go gRPC 服务器端应用程序的 KubernetesDeployment 描述符
apiVersion: apps/v1
kind: Deployment ➊
metadata:
name: grpc-productinfo-server ➋
spec:
replicas: 1 ➌
selector:
matchLabels:
app: grpc-productinfo-server
template:
metadata:
labels:
app: grpc-productinfo-server
spec:
containers:
- name: grpc-productinfo-server ➍
image: kasunindrasiri/grpc-productinfo-server ➎
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 50051
name: grpc
- ❶ 声明 Kubernetes Deployment 对象。
- ❷ Deployment 的名称。
- ❸ 要同时运行的 gRPC 服务器端 pod 的数量。
- ❹ 相关联的 gRPC 服务器端容器的名称。
- ❺ gRPC 服务器端容器的镜像名称和 tag。
当在 Kubernetes 中通过命令 kubectl apply -f server/grpc-prodinfo-server.yaml
应用该描述符时,Kubernetes 集群会运行由一个 gRPC 服务器端 pod 所组成的 Kubernetes Deployment。但是,如果 gRPC 客户端应用程序要访问在同一个 Kubernetes 集群中运行的 gRPC 服务器端 pod,它就需要确定 pod 的准确 IP 地址和端口并发送 RPC。不过,在 pod 重启时,IP 地址可能会发生变化,而且在运行多个副本时,还需要处理每个副本所带来的多个 IP 地址。为了克服这种局限性,Kubernetes 提供了名为 service 的抽象。
02. gRPC 服务器的 Kubernetes Service 资源
可以创建 Kubernetes Service 并将其与匹配的 pod(在本例中,也就是 gRPC 服务器端 pod)关联,这样会得到一个 DNS 名,它会自动将流量路由到任意匹配的 pod 上。因此,可以将 Service 视为一个 Web 代理或者负载均衡器,它能够将请求转发到底层的 pod上。代码清单 7-5 展示了 gRPC 服务器端应用程序的 KubernetesService 描述符。
代码清单 7-5 基于 Go gRPC 服务器端应用程序的 KubernetesService
描述符
apiVersion: v1
kind: Service ➊
metadata:
name: productinfo ➋
spec:
selector:
app: grpc-productinfo-server ➌
ports:
- port: 50051 ➍
targetPort: 50051
name: grpc
type: NodePort
- ❶ 声明 Service 描述符。
- ❷ Service 的名称。客户端应用程序在连接 Service 的时候,会用到这个名称。
- ❸ 这将告诉 Service,将请求路由至匹配
grpc-productinfo-server
label 的 pod。 - ❹ 服务在端口 50051 上运行并将请求转发至目标端口 50051。
创建完 Deployment 描述符和 Service 描述符之后,就可以通过kubectl apply -f server/grpc-prodinfo-server.yaml
命令将该应用程序部署到 Kubernetes 中了(可以将这两个描述符放到同一个 YAML 文件中)。这些对象部署成功后,我们将得到运行中的 gRPC 服务器端 pod、gRPC 服务器端 Kubernetes Service 以及 Deployment。
下一步是将 gRPC 客户端部署到 Kubernetes 中。
03. 运行 gRPC 客户端的 Kubernetes Job
gRPC 服务器端在 Kubernetes 集群中启动和运行之后,就可以在同一个集群中运行 gRPC 客户端应用程序了。客户端可以通过我们在上一步创建的 gRPC service productinfo 来访问 gRPC 服务器端。
因此,在客户端的代码中,我们应该使用 Kubernetes Service 的名称作为主机名,并使用 Service 的端口作为 gRPC 服务器端的端口名。因此,在 Go 语言的客户端实现中,客户端要使用grpc.Dial("productinfo:50051", grpc.WithInsecure())
来连接至服务器端。假设客户端应用程序需要运行指定的次数(只需要调用 gRPC 服务、用日志记录响应并退出),那么我们可以使用 Kubernetes Job,而非 Kubernetes Deployment。Kubernetes Job旨在让一个 pod 运行指定的次数。
可以按照与 gRPC 服务器端相同的方式来创建客户端应用程序容器。在将容器推送至 Docker 注册中心后,就可以按照代码清单 7-6所示创建 Kubernetes Job 的描述符了。
代码清单 7-6 以 Kubernetes Job 形式运行的 gRPC 客户端应用程序
apiVersion: batch/v1
kind: Job ➊
metadata:
name: grpc-productinfo-client ➋
spec:
completions: 1 ➌
parallelism: 1 ➍
template:
spec:
containers:
- name: grpc-productinfo-client ➎
image: kasunindrasiri/grpc-productinfo-client ➏
restartPolicy: Never
backoffLimit: 4
- ❶ 声明 Kubernetes Job。
- ❷ Job 的名称。
- ❸ 在 Job 完成之前,pod 需要成功运行的次数。
- ❹ 要有多少个 pod 并行运行。
- ❺ 相关 gRPC 客户端容器的名称。
- ❻ 该 Job 相关联的容器镜像。
接下来,就可以通过 kubectl apply –f client/grpc-prodinfo-client-job.yaml
部署 gRPC 客户端应用程序的 job 并检查 pod 的状态了。
job 执行成功时会发送一个添加商品的 RPC 到 ProductInfo gRPC服务。因此,你可以观察服务器端和客户端 pod 的日志,从而判断是否得到了预期信息。
随后可以使用 Ingress 资源将 gRPC 服务暴露到 Kubernetes 集群之外。
04. 通过 Kubernetes Ingress 对外暴露 gRPC 服务
到目前为止,我们已完成了将 gRPC 服务器端部署在 Kubernetes上,并且让它能够被同一个集群中的其他 pod(在这里,以 Job 的方式运行)访问。如果我们想将 gRPC 服务暴露给 Kubernetes 集群外部的应用程序,该怎么办呢?我们知道,Kubernetes Service 只能暴露指定的 Kubernetes pod 给集群中运行的其他 pod。因此,Kubernetes Service 不能为 Kubernetes 之外的应用程序所访问。为了实现这一目的,Kubernetes 提供了名为 ingress 的抽象。
我们可以将 Ingress 视为 Kubernetes Service 和外部服务之间的
一个负载均衡器。Ingress 将外部的流量路由至 Service,随后Service 匹配的 pod 之间路由内部的流量。Ingress 控制器管理给定 Kubernetes 集群中的 Ingress 资源。Ingress 控制器的类型和行为可能会根据你所使用的集群有所变化。同时,在将 gRPC 服务暴露给外部应用程序时,有一个强制的需求就是在 Ingress 层面要支持 gRPC 路由。因此,我们要选择一个支持 gRPC 的控制器。
对于本例,我们将使用 Nginx Ingress 控制器,它是基于 Nginx 负载均衡器的。(根据你所使用的 Kubernetes 集群,可以选择支持gRPC 的最合适的 Ingress 控制器。)Nginx Ingress 支持 gRPC 将外部流量路由至内部服务。
为了将 ProductInfo gRPC 服务器端应用程序暴露到外部世界(Kubernetes 集群之外),可以创建代码清单 7-7 所示的 Ingress资源。
代码清单 7-7 基于 Go gRPC 服务器端应用程序的 KubernetesIngress 资源
apiVersion: extensions/v1beta1
kind: Ingress ➊
metadata:
annotations: ➋
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
name: grpc-prodinfo-ingress ➌
spec:
rules:
- host: productinfo ➍
http:
paths:
- backend:
serviceName: productinfo ➎
servicePort: grpc ➏
- ❶ 声明 Ingress 资源。
- ❷ Nginx Ingress 控制器相关的注解,指定 gRPC 作为后端协议。
- ❸ Ingress 资源的名称。
- ❹ 暴露给外部的主机名。
- ❺ 关联的 Kubernetes Service 的名称。
- ❻ 在 Kubernetes Service 中所声明的服务端口名称。
在部署上述的 Ingress 资源之前,需要安装 Nginx Ingress 控制器。在 Kubernetes 的 Ingress- Nginx 仓库中,有更多安装和使用Nginx Ingress 实现 gRPC 的详情供参考。部署完 Ingress 资源后,任意的外部应用程序都可以通过主机名(productinfo)和默认端口(80)来调用 gRPC 服务器端。
到此为止,我们已经学习了在 Kubernetes 上部署生产级 gRPC 应用程序的所有基础知识。可以看到,借助 Kubernetes 和 Docker 所提供的功能,我们无须再担心大多数非功能性需求,如扩展性、高可用性、负载均衡、故障转移等,因为 Kubernetes 作为底层平台的一部分,已经提供了这些功能。如果你在 Kubernetes 上运行 gRPC 应用程序,那么第 5 章所介绍的某些概念,比如 gRPC 代码级别的负载均衡、命名解析等,就没有什么用处了。
基于 gRPC 的应用程序运行起来之后,需要确保它在生产环境中平稳运行。为了实现该目标,需要不间断地观察 gRPC 应用程序,并在需要时采取必要的行动。接下来深入研究 gRPC 应用程序在可观察性方面的细节。