本文记录如何把 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 种方法:
- 把 image push 到公开的 hub.docker.com,缺点是上传一个几百兆的文件太慢,而且必须是公开的。
- 把 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
// 返回类似第三步的结果
(完)