本文记录如何把 TensorFlow ResNet 模型部署在本地 Kubernetes 集群上,并提供一个 grpc 端口供集群外部访问。

本文不牵涉 ResNet(Deep residual networks)模型的实现细节,只讨论部署。

本文来源于 TensorFlow 官网上的一个例子,但正如大多数项目的文档一样,文档落后于项目的发展,因此有一些小坑,这里记录一下。

下载 ResNet 模型数据

这一步没什么好说的,按照步骤下载就行了


mkdir /tmp/resnet
curl -s http://download.tensorflow.org/models/official/20181001_resnet/savedmodels/resnet_v2_fp32_savedmodel_NHWC_jpg.tar.gz | \
tar --strip-components=2 -C /tmp/resnet -xvz

制作并启动 ResNet serving

因为我们要把这个 serving 部署到 k8s,所以制作 docker 镜像是必须的。

先启动运行一个空 serving 镜像:

docker run -d --name serving_base tensorflow/serving

然后把刚刚下载的 /tmp/resnet 文件夹下的所有内容拷贝到容器中:

docker cp /tmp/resnet serving_base:/models/resnet

最后,commit 生成一个自己的 image

docker commit --change "ENV MODEL_NAME resnet" serving_base resnet_serving

docker kill serving_base
docker rm serving_base

然后我们试着运行一下这个镜像,要是看到类似如下输出,证明启动正常。

$ docker run -p 8500:8500 -t resnet_serving

tensorflow_serving/model_servers/server.cc:86] Building single TensorFlow model file config:  model_name: resnet model_base_path: /models/resnet
tensorflow_serving/model_servers/server_core.cc:462] Adding/updating models.
tensorflow_serving/model_servers/server_core.cc:573]  (Re-)adding model: resnet
tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: resnet version: 1538687457}
tensorflow_serving/model_servers/server.cc:358] Running gRPC ModelServer at 0.0.0.0:8500 ...
tensorflow_serving/model_servers/server.cc:378] Exporting HTTP/REST API at:localhost:8501 ...
NET_LOG: Entering the event loop ...

可以看到,程序启动了一个 grpc 服务在 8500 端口等待连接。

下载 Client 端

client 端使用 tensorflow/serving 项目中的一个例子 resnet_client_grpc.py

git clone https://github.com/tensorflow/serving
cd serving

然后启动 client

$ tools/run_in_docker.sh python tensorflow_serving/example/resnet_client_grpc.py

这个时候会看到如下错误提示

$ tools/run_in_docker.sh python tensorflow_serving/example/resnet_client_grpc.py
== Pulling docker image: tensorflow/serving:nightly-devel
nightly-devel: Pulling from tensorflow/serving
Digest: sha256:f5b0db6f12af5989f30755e2329cd5a8e835c97386bf5135a3b29ffb43fc57d8
Status: Image is up to date for tensorflow/serving:nightly-devel
docker.io/tensorflow/serving:nightly-devel
== Running cmd: sh -c 'cd /home/test/serving; python tensorflow_serving/example/resnet_client_grpc.py'
Traceback (most recent call last):
  File "tensorflow_serving/example/resnet_client_grpc.py", line 34, in <module>
    tf.app.flags.DEFINE_string('server', 'localhost:8500',
AttributeError: 'module' object has no attribute 'app'

只要修改 resnet_client_grpc.py 开头 import tensorflow 部分即可。

# import tensorflow as tf
import tensorflow.compat.v1 as tf

一切顺利将会看到如下输出:

outputs {
  key: "classes"
  value {
    dtype: DT_INT64
    tensor_shape {
      dim {
        size: 1
      }
    }
    int64_val: 286
  }
}
outputs {
  key: "probabilities"
  value {
    dtype: DT_FLOAT
    tensor_shape {
      dim {
        size: 1
      }
      dim {
        size: 1001
      }
    }
  
...
    float_val: 1.59407863976e-06
    float_val: 0.00129527016543
  }
}
model_spec {
  name: "resnet"
  version {
    value: 1538687457
  }
  signature_name: "serving_default"
}

部署到 Kubernetes

最后,就是把这个 serving 服务部署到我自己的 k8s 集群上。

上传 docker image

首先一个问题是该把 docker image 放在哪里? 如果是部署到 AWS, Google Cloud 这类公有云的话,他们会提供一个 registry 地址。

而我是本地的 k8s 集群,因此有 2 种方法:

  1. 把 image push 到公开的 hub.docker.com,缺点是上传一个几百兆的文件太慢,而且必须是公开的。
  2. 把 image 拷贝到集群的每个节点。

因此我选择方法 2.

首先把我本地的 image 导出到一个文件,

docker image save 624c175dcdb8 -o resnet.tar.gz

然后把这个文件拷贝到 k8s 集群所有节点上,并导入

$ scp resnet.tar.gz 192.168.56.11:

$ docker load < resnet.tar.gz

$ docker image ls
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
<none>                         <none>              624c175dcdb8        2 hours ago         354MB


$ docker tag 624c175dcdb8 resnet_serving:latest
$ docker image ls
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
resnet_serving         latest              624c175dcdb8        2 hours ago         354MB

Deployment 文件

接下来就是在 k8s 集群上部署这个 image 了。

需要指出的是,官网上给的 deployment 和 service 文件都过时了,下面的以我为准。

$ cat resnet_k8s_deplyment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: resnet-deployment
spec:
  selector:
    matchLabels:
      app: resnet-server
  replicas: 1
  template:
    metadata:
      labels:
        app: resnet-server
    spec:
      containers:
      - name: resnet-container
        image: resnet_serving:latest
        ports:
        - containerPort: 8500

Service 文件

因为是在自己的本地 k8s 而不是使用公有云的,因此无法使用 LoadBalance 类型生成一个公网 IP。

在这里我使用 NodePort 类型:

$ cat resnet_k8s_service.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    run: resnet-service
  name: resnet-service
spec:
  ports:
  - port: 8500
    targetPort: 8500
  selector:
    app: resnet-server
  type: NodePort

测试

部署完成之后,我们看看服务 expose 在哪个端口:

$ kubectl get svc
NAME             TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
resnet-service   NodePort    10.96.11.10   <none>        8500:31415/TCP   3h58m

最后,在 k8s 集群之外的物理主机上访问这个服务,注意指定 IP 地址和端口

$ tools/run_in_docker.sh python tensorflow_serving/example/resnet_client_grpc.py --server=192.168.56.11:31415

// 返回类似第三步的结果

(完)