linux ip 命令的使用

简介

linux 下的 ip 命令是一个很强大的工具,在这之前,我通常只会使用 ifconfig 命令来查看本机网络接口和 ip 地址等等。或者 netstat 命令查看端口占用等等。ip 命令属于 iproute2 套件中的一个命令,关于 iproute2 和 linux net-tools 中的命令对比如下(图片来源:https://linux.cn/article-3144-1.html):

net-tools vs iproute2

可以看出,除了部分 netstat 命令用 ss 来替代,其它都可以用 ip 命令替代。并且,iproute2 已经是大多数 linux 发行版默认安装了,而 net-tools 则需要另外安装。

ip 命令可以分为下面几个模块:

  • 网卡设备相关: ip link
  • 网卡地址相关: ip addr
  • 路由表相关: ip route
  • arp 相关: ip neigh

下面会列出一些常用的操作,最好在虚拟机中操作,防止影响个人机器。

ip link

查看 ip link 的帮助

$ ip link help

查看网络接口

$ ip link list

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:8a:fe:e6 brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:15:ee:5c brd ff:ff:ff:ff:ff:ff

这里显示了三个网络接口,lo代表的本机的回环网卡,eth0eth1 分别是两个网卡

添加网络接口

$ sudo ip link add link eth0 mydev type bridge

这里添加了一个网桥,连接在 eth0 上。使用 ip link list 查看可以发现多了下面一个设备

6: mydev: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 5e:0c:36:7b:ce:0d brd ff:ff:ff:ff:ff:ff

删除网络接口

$ sudo ip link delete link dev mydev

关闭网络接口

$ sudo ip link set eth1 down

打开网络接口

$ sudo ip link set eht1 up

ip addr

查看帮助

$ ip addr help

查看网络地址

$ ip addr list

查看某一个网络接口的地址

$ ip addr show eth1

添加 ip 地址

$ sudo ip addr add 192.168.31.131/24 dev eth1

查看 eth1 的地址

$ ip addr show eth1

3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:15:ee:5c brd ff:ff:ff:ff:ff:ff
    inet 192.168.31.77/24 brd 192.168.31.255 scope global noprefixroute dynamic eth1
       valid_lft 42769sec preferred_lft 42769sec
    inet 192.168.31.131/24 scope global secondary eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe15:ee5c/64 scope link 
       valid_lft forever preferred_lft forever

我们也可以 ping 一下这个地址:

$ ping 192.168.31.131

PING 192.168.31.131 (192.168.31.131) 56(84) bytes of data.
64 bytes from 192.168.31.131: icmp_seq=1 ttl=64 time=0.109 ms
64 bytes from 192.168.31.131: icmp_seq=2 ttl=64 time=0.155 ms

删除 ip 地址

$ sudo ip addr del 192.168.31.131/24 dev eth1

改变设备地址的配置

这里有一篇很好的文章: understanding ip addr change and ip addr replace commands

为了演示的方便,我添加了一个网卡设备

$ sudo ip link add link eth0 name dummy0 type dummy

为它分配地址:

$ sudo ip addr add 192.168.31.132/24 dummy0
$ ip addr show dummy0

5: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 9e:dc:6e:0b:70:99 brd ff:ff:ff:ff:ff:ff
    inet 192.168.31.132/24 scope global dummy0
       valid_lft forever preferred_lft forever

如果你想要修改 valid_lftpreferred_lft 配置,可以使用 ip change命令:

$ sudo ip addr change 192.168.31.132 dev dummy0 preferred_lft 300 valid_lft 300
$ ip addr show dummpy0

5: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 9e:dc:6e:0b:70:99 brd ff:ff:ff:ff:ff:ff
    inet 192.168.31.132/24 scope global dynamic dummy0
       valid_lft 299sec preferred_lft 299sec

ip route

查看帮助

$ ip route help

查看路由

$ ip route list

添加路由

添加一条普通的路由

$ sudo ip route add 39.156.0.0/16 via 192.168.31.133 dev dummy0

添加默认路由

$ sudo ip route add default via 192.168.31.133 dev dummy0

删除路由

删除默认路由

$ sudo ip route del default via 192.168.31.133 dev dummy0

删除普通路由

$ sudo ip route del 39.156.0.0/16 via 192.168.31.133 dev dummy0 

**查看一个 ip 地址的路由包来源

$ ip route get 39.156.69.79

39.156.69.79 via 10.0.2.2 dev eth0 src 10.0.2.15 
    cache 

ip neigh

查看帮助

$ ip neigh help

查看同一个网络的邻居设备

$ ip neigh show

192.168.31.1 dev eth1 lladdr 34:ce:00:2e:88:b9 STALE
10.0.2.2 dev eth0 lladdr 52:54:00:12:35:02 REACHABLE
10.0.2.3 dev eth0 lladdr 52:54:00:12:35:03 STALE

Kubernetes Pod 解析

pod 基础概念

在 Kubernetes 中, Pod 是一个非常重要的概念,它由一个或多个容器组成,这些容器共享存储、网络、进程空间,以及可以使用进程间通信。

Pod 是集群中最小的调度单位,如果把 Kubernetes 集群比作操作系统,那么 Pod 则是一个进程。一个 Pod 被创建出来之后,它会被调度到集群中的某一个节点上开始运行,Pod 中的 Container 都会在该节点上启动。

Pod 是短暂的,就跟进程一样,在被创建之后可能会随时被终止。但是 Kubernetes 会根据需求来的及时的重新创建一个 Pod,所以单独从 Pod 的层面来说,它应该是一个无状态的应用。

集群中的每个 Pod 都会有唯一的 ID (UID),这跟进程的进程号是唯一的一样。

共享命名空间

这里以 docker 在 linux 下的实现为例,docker 主要使用了 linux namespace 做的资源隔离。在 pod 中,所有的 docker 容器都可以共享同一个 network, ipc, pid命名空间,并且可以通过挂载同一个卷的方式来共享文件系统。需要注意的是,默认情况下只有 network 这个命名空间是开启的,其他的需要通过 shareProcessNamespaceSYS_PTRACEemptyDir 等字段来开启。

为了说明,可以在 kubernetes 集群中创建下面这个 pod

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  shareProcessNamespace: true
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
      - mountPath: /cache
        name: cache-volume
  - name: shell
    image: busybox
    volumeMounts:
      - mountPath: /cache
        name: cache-volume
    securityContext:
      capabilities:
        add:
        - SYS_PTRACE
    stdin: true
    tty: true
  volumes:
  - name: cache-volume
    emptyDir: {}

上面创建的 Pod 有两个容器,一个是 nginx,另一个是 shell。我们使用以下命令进入到 shell 容器中。

kubectl exec -it nginx -c shell sh

network

为了验证同一个 Pod 下 network 是共享的,可以使用以下命令验证

$ wget localhost
Connecting to localhost (127.0.0.1:80)
saving to 'index.html'
index.html           100% |******************************************|   612  0:00:00 ETA
'index.html' saved

很明显,这里的 localhost 指向了 nginx 容器。

pid

$ ps -el
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    6 root      0:00 nginx: master process nginx -g daemon off;
   11 101       0:00 nginx: worker process
   12 root      0:00 sh
   20 root      0:00 sh
   26 root      0:00 ps

在 shell 容器中查看进程可以看到 /pause 和 nginx 等进程。因为共享了 pid 命名空间,所以可以看到其他容器的进程。这里的 pause 是一个很特殊的进程,在后文章会单独解释。

ipc

$ kill -9 11
$ ps -el
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    6 root      0:00 nginx: master process nginx -g daemon off;
   12 root      0:00 sh
   20 root      0:00 sh
   29 101       0:00 nginx: worker process
   30 root      0:00 ps -el

接着上面的命令,我们杀死了 nginx 的 worker 进程,nginx master 进程又重启了 worker,重启后的 worker PID 是 29。可以在 shell 容器中使用信号杀死 nginx 中的进程,说明 IPC 命名空间是共享的。

shared volume

$ cd cache
$ touch test
$ kubectl exec -it nginx -c nginx sh
$ ls /cache
test

我们在 shell 容器中 cache 文件夹下创建了文件 test, 在 nginx 容器中也能看到,说明两个容器可以共享文件系统的某些目录。

容器探针

之所以特地提到容器探针是因为容器探针是一个非常好的检查服务是否正确运行的方式。

TODO: 几种探针的使用场景和

探针是由 kubelet 周期性对容器执行的诊断措施。kubernetes 提供了三种方式:

  • ExecAction: 在容器中执行命令,如果命令的 exit code 是 0 则代表成功。
  • TCPSocketAction: 在容器的ip和端口上执行 tcp 连接检查,如果端口是打开的则表明诊断成功。
  • HTTPGetAction: 在容器的ip和制定端口和路径上执行,如果返回的状态码大于等于 200 ,小于400就表明成功。

直到 kubernetes v1.16 止,共有三种探针可以使用,分别是 livenessProbe, readinessProbe, startupProbe.

  • livenessProbe: 检查容器是否在运行,如果 liveness 探针失败,kubelet 会杀死这个容器,这个容器会遵循它的重启策略。如果一个容器没有提供 liveness 探针,默认状态是 Success

  • readinessProbe: 表明容器是否准备好接收请求了。如果 readiness 探针失败,endpoints 控制器会从符合这个 Pod 的所有 service 的 endpoint 列表中移除该 Pod。默认状态是 Failure。如果容器没有提供 readiness 探针,默认状态就是 Success

  • startupProbe: 表明容器中的应用是否启动完成。如果提供了 startup 探针,其他的探针都被禁用直到 startup 探针成功。如果 startup 探针失败,kuberlet 杀死容器,容器会遵循它的重启策略。是否容器没有提供 startup 探针,默认状态是 Success

init container

我们都知道 Pod 可以有多个容器,其中 init 容器是比较特殊的一个,它由 spec.initContainers 指定,与普通容器不同的是,只有在 init 容器运行完成之后,Kubernetes 才会初始化 Pod 和运行应用容器。

实际应用中,init 容器的职责基本上都是和它名字描述的一样,用来做初始化用。比如在 argo 这个工作流调度应用中,它会为每个调度的 Pod 初始化一个 init 容器,用来载入该步骤需要使用的文件资源等等。

pause container

在上面提到了 pid namespace 共享中,有一个 PID 为 1 的 Pause 进程,这就是现在提到的 pause container,pause container 对 kubernetes 用户是不感知的。但是我们在 kubernetes 节点上使用 docker ps来查看,会发现很多的 pause 容器。pause 容器的作用主要有两点:

  • 在 pod 中作为 linux namespace 共享的基础容器
  • 在 PID namespace 共享的前提下,作为每个 pod 中的PID 1,然后回收僵尸进程

为了研究pause的作用,可以在电脑上执行以下的命令:

docker run -d --ipc=shareable --name pause -p 8080:80 warrior/pause-amd64:3.0

docker run -d --name nginx -v /home/jiang/projects/testk8s/nginx.conf:/etc/nginx/nginx.conf --net=container:pause --ipc=container:pause --pid=container:pause nginx

docker run -d --name ghost --net=container:pause --ipc=container:pause --pid=container:pause ghost

nginx.conf 如下:

error_log stderr;
events {worker_connections 1024;}
http {
    access_log /dev/stdout combined;
    server {
        listen 80 default_server;
        server_name example.com;
        location / {
            proxy_pass http://127.0.0.1:2368;
        }
    }
}

我们首先启动了一个 pause 容器,并且开始了 ipc 的共享。然后又启动了 nginx 和 ghost 容器,并且这两个容器都加入了 pause 的network、ipc和pid命名空间。

在浏览器中打开地址: http://localhost:8080, 发现打开了 ghost 博客网页。我们在容器 pause 中开启的 8080 端口,然后经过 nginx 容器代理到了 ghost 容器。我们的应用容器 pause 容器完成了命名空间的共享。

我们再来看一下 pause 的代码:

/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}

static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0);
}

int main() {
  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, "Warning: pause should be the first process\n");

  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;

  for (;;)
    pause();
  fprintf(stderr, "Error: infinite loop terminated\n");
  return 42;
}

这段代码在监听了三个信号量,在 SIGINT 和 SIGTERM 时调用 sigdown() 来退出。在接收到 SIGCHLD 信号时使用 waitpid,因为 pause 进程的 PID 是1,所以所有的僵尸进程都会被挂到 pause 进程之下,因此 waitpid 可以回收僵尸进程。

multi containers design pattern

在大多数情况下,Pod 往往只有一个容器,因为一个 Pod 的职责是唯一的。但是同样的,也有一些值得借鉴的多容器设计模式。

常用的模式有三种: sidecar, adapter, ambassador, 下图是常见的三种设计模式图,图片来源于网络:

multi container pod design

sidecar 模式

在 sidecar 模式中,通常有一个主要的容器A–比如我们的 web 应用,然后有另外一个重要的容器B,负责处理 A 容器的一些功能,但是 B 容器又不是必须的。这个 B 容器我们通常称它为 sidecar 容器。

常见的 sidecar 容器有 日志,同步服务,监控等职责。当应用容器不在运行时,日志容器的运行是没有意义的,所以我们通常会创建一个 Pod 包含主要的容器和一个 sidecar 容器,来协同工作。这样的好处就是减少应用容器的功能需求,将通用的功能交给 sidecar 容器去执行,而又不会侵入应用容器。

adapter 模式

adapter 模式就是程序设计中常用的适配器模式,负责将应用容器中一些不兼容的功能调整成兼容的格式。比如一个大型系统中有很多小的系统,每个系统输出的日志格式都不同。而我们的统一监控系统只接受一种日志格式。这时候就可以使用 adapter 模式在 Pod 的加入一个负责适配的容器,将各种格式的日志调整成相同的统一发送给日志系统。

ambassador 模式

ambassador 模式常用来将应用容器连接到容器之外的网络。比如数据库,我们的应用容器只负责连接 localhost 的地址,然后由 ambassador 容器判断当前的环境,将应用容器的数据库请求代理到不同的数据库上。这样,我们在开发环境,测试环境,生产环境都只需要一套配置。

pod lifecycle

pod 的状态包含一个 phase 属性,这个属性用来描述当前 pod 的状态,可能的值有:

  • Pending: Pod 已经被 Kubernetes 系统接受,但是有一个或多个容器镜像尚未创建。等待时间包括调度 Pod 的时间和通过网络下载镜像的时间。
  • Running: Pod 已经绑定到一个节点上,Pod 中所有的容器都已经被创建,至少有一个容器正在运行,或者正处于启动或重启状态。
  • Succeeded: Pod中的所有容器都被成功终止,并且不会再重启。
  • Failed: Pod中所有容器都已经终止,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或被系统终止。
  • Unknown: 因为某些原因无法取得 Pod 的状态,通过是因为与 Pod 所在的主机通信失败。

下图是一个 Pod 的生命周期状态,图片来源于网络:

kubernetes-pod-life-cycle

在 Pod 的整个生命周期中,我们可以通过容器的生命周期钩子来在某些阶段处理一些工作。

  • PostStart: 当容器被创建的时候,这个钩子会立刻执行。

  • PreStop: 当容器退出时执行

在两种钩子触发时我们可以选择调用脚本执行还是发送HTTP请求。

参考资料

Pods-Kubernetes

Pod状态与生命周期管理

The Almighty Pause Container

The Distributed System Toolkit: Patterns for Composite Containers

Container Design Patterns

Multi-Container Pod Design Patterns in Kubernetes

tensorflow-serving 在k8s中的模型部署方案

简介

tensorflow-serving是一个tensorflow模型部署的方案,其在设计时,就考虑了非常灵活的设计,比如:

  • 支持不同的文件系统,并且易扩展
  • 将模型发现、加载、使用和卸载和模型生命周期的管理,以及对外提供服务解耦合,因此非常容易扩展它的模型发现方式,以及同样可以支持其他框架下模型的整合。
  • 整个服务是无状态的,因此方便在k8s上进行部署

下图是 tensorflow serving 的整体架构:

tensorflow serving architecture

模型加载方式

tensorflow-serving支持从不同的地方,以不同的方式去加载模型。比如我们可以直接在启动tensorflow-serving时加上模型的地址,也可以提供模型配置文件来启动服务。

启动时加上参数:

tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=resnet --model_base_path=/home/jiang/data/yolov3

从配置文件中加载模型:

/etc/config/models.config

model_config_list {
    config {
        name: 'fashion'
        base_path: 's3://models/fashion/'
        model_platform: 'tensorflow'
    }
    config {
        name: 'resnet'
        base_path: 's3://models/resnet/'
        model_platform: 'tensorflow'
    }
}

执行以下命令来加载fashionresnet两个模型:

tensorflow_model_server --port=9000", "--rest_api_port=8500", "--model_config_file=/etc/config/models.config"

模型存储系统

tensorflow-serving的另一个特点就是支持从不同类型的存储系统中加载模型。比如本地的文件系统、s3、hdfs等等

从本地文件系统中加载

tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=resnet --model_base_path=/home/jiang/data/yolov3

从s3加载

从s3(兼容s3的对象存储系统都可以)中加载模型,需要配置一些环境变量

export AWS_ACCESS_KEY_ID=<key id>
export AWS_SECRET_ACCESS_KEY=<key>
export S3_ENDPOINT=minio-service.minio:9000
export S3_USE_HTTPS=0
export S3_VERIFY_SSL=0
export AWS_REGION=us-west-1
export S3_REGION=us-west-1
export AWS_LOG_LEVEL=3

然后通过以下命令启动服务即可

tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=resnet --model_base_path=s3://models/resnet/

从hdfs中加载

从hdfs中加载需要设置以下的环境变量

  • JAVA_HOME: Java 的安装路径

  • HADOOP_HDFS_HOME: HDFS 的安装路径,如果在LD_LIBRARY_PATH中设置了 libhdfs.so 的路径,那么这个环境变量可以不要。

  • LD_LIBRARY_PATH: 引入 libjvm.so 的路径。如果你的 HADOOP 发行版在 ${HADOOP_HDFS_HOME}/lib/native 这个目录下没有包含 libhdfs.so,也需要引入它。

export LD_LIBRARY_PATH={LD_LIBRARY_PATH}:{JAVA_HOME}/jre/lib/amd64/server
  • CLASSPATH: 注意仅仅是设置 CLASSPATH 环境变量是不行的,需要用以下的方式使用:
CLASSPATH=({HADOOP_HDFS_HOME}/bin/hadoop classpath --glob) tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=yolov3 --model_base_path=hdfs://worknode2:9000/pipeline/models/yolov3

在k8s中部署

s3

tensorflow-serving 官方提供了docker镜像,因此使用 s3 的方式加载模型部署是很简单的。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: tfserving-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: tfserving
    spec:
      containers:
      - name: serving-container
        image: tensorflow/serving:1.14.0  
        ports:
        - containerPort: 8500
        - containerPort: 9000
        env:
        - name: AWS_ACCESS_KEY_ID
          value: J5WW5NKKV7AE9S0WZCM1
        - name: AWS_SECRET_ACCESS_KEY
          value: TbG0Y6nnUV8nQNLL9n4B3u3UPMMCJvqs2COx3and
        - name: S3_ENDPOINT
          value: minio-service.minio:9000
        - name: S3_USE_HTTPS
          value: "0"
        - name: S3_VERIFY_SSL
          value: "0"
        - name: AWS_REGION
          value: us-west-1
        - name: S3_REGION
          value: us-west-1
        - name: AWS_LOG_LEVEL
          value: "3"
        command: ["/usr/bin/tensorflow_model_server"]
        args: ["--port=9000", "--rest_api_port=8500", "--model_name=resnet", "--model_base_path=s3://models/resnet/"]

---
apiVersion: v1
kind: Service
metadata:
  labels:
    run: tf-service 
  name: tf-service
spec:
  ports:
  - name: rest-api-port
    port: 8500
    targetPort: 8500
  - name: grpc-port
    port: 9000
    targetPort: 9000
  selector:
    app: tfserving 
  type: NodePort

hdfs

在官方提供的 docker 镜像中,并没有打包 hdfs 的环境,因此我们需要自己构建一个镜像:

Dockerfile 所在目录如下:

hdfs_dockerfile
├── Dockerfile
└── hadoop-2.10.0
    ├── bin
    ├── etc
    ├── include
    ├── lib
    ├── libexec
    ├── LICENSE.txt
    ├── logs
    ├── NOTICE.txt
    ├── README.txt
    ├── sbin
    └── share

Dockerfile 如下:

FROM tensorflow/serving:1.14.0

RUN apt update && apt install -y openjdk-8-jre

COPY hadoop-2.10.0 /root/hadoop

ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/
ENV HADOOP_HDFS_HOME /root/hadoop
ENV LD_LIBRARY_PATH {LD_LIBRARY_PATH}:{JAVA_HOME}/jre/lib/amd64/server

RUN echo '#!/bin/bash \n\n\
CLASSPATH=({HADOOP_HDFS_HOME}/bin/hadoop classpath --glob) tensorflow_model_server --port=8500 --rest_api_port=9000 \
--model_name={MODEL_NAME} --model_base_path={MODEL_BASE_PATH}/{MODEL_NAME} \
"@"' > /usr/bin/tf_serving_entrypoint.sh \
&& chmod +x /usr/bin/tf_serving_entrypoint.sh

EXPOSE 8500
EXPOSE 9000
ENTRYPOINT ["/usr/bin/tf_serving_entrypoint.sh"]

进行构建:

docker build -t tensorflow_serving:1.14-hadoop-2.10.0 .

运行:

docker run -p 9000:9000 --name tensorflow-serving -e MODEL_NAME=yolov3 -e MODEL_BASE_PATH=hdfs://192.168.50.166:9000/pipeline/models -t tensorflow_serving:1.14-hadoop-2.10.0

这样将上面的部署文件稍微修改一下即可使用。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: tfserving-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: tfserving
    spec:
      containers:
      - name: serving-container
        image: joyme/tensorflow_serving:1.14-hadoop-2.10.0
        ports:
        - containerPort: 8500
        - containerPort: 9000
        env:
        - name: MODEL_NAME
          value: yolov3
        - name: MODEL_BASE_PATH
          value: hdfs://192.168.50.166:9000/pipeline/models

---
apiVersion: v1
kind: Service
metadata:
  labels:
    run: tf-service 
  name: tf-service
spec:
  ports:
  - name: rest-api-port
    port: 8500
    targetPort: 8500
  - name: grpc-port
    port: 9000
    targetPort: 9000
  selector:
    app: tfserving 
  type: NodePort

模型调用

tensorflow-serving 支持两种方式调用模型进行预测: GRPC 和 RESTful api

GRPC的方式如下:

from __future__ import print_function

import grpc
import requests
import tensorflow as tf

from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2_grpc

IMAGE_URL = 'https://tensorflow.org/images/blogs/serving/cat.jpg'

tf.app.flags.DEFINE_string('server', '192.168.50.201:30806', 'PredictionService host:port')
tf.app.flags.DEFINE_string('image', '', 'path to image in jpeg format')
FLAGS = tf.app.flags.FLAGS

def main(_):
    if FLAGS.image:
        with open(FLAGS.image, 'rb') as f:
            data = f.read()
    else:
        dl_request = requests.get(IMAGE_URL, stream=True)
        dl_request.raise_for_status()
        data = dl_request.content

    channel = grpc.insecure_channel(FLAGS.server)
    stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)

    # Send request
    request = predict_pb2.PredictRequest()
    request.model_spec.name = 'resnet'
    request.model_spec.signature_name = 'serving_default'
    request.inputs['image_bytes'].CopyFrom(
            tf.contrib.util.make_tensor_proto(data, shape=[1]))

    result = stub.Predict(request, 10.0) # 10 secs timeout
    print(result)

if __name__ == '__main__':
    tf.app.run()

RESTful API的方式如下

import requests
import json
import base64

with open("cat.jpg", "rb") as image_file:
    encoded_string = base64.b64encode(image_file.read())

headers = {"content-type": "application/json"}
body = {
        "instances": [
            {'b64': encoded_string}
           ]
        }
r = requests.post('http://192.168.50.201:32063/v1/models/resnet:predict', data = json.dumps(body), headers = headers)

print(r.text)