Author: xidianwangtao@gmail.com
摘要:本文介绍了在Kubernetes集群中,使用Dubbo+Zookeeper来完成TensorFlow Serving服务的注册与发现、负载均衡的方案,以及使用KubeDNS+Kube2LVS的方案。
背景
TensorFlow Serving服务在Kubernetes集群中的部署方案,如果是从零开始建设,那么可以通过Kubernetes原生的Service+KubeDNS实现服务的注册与发现,并通过对接LVS集群进行负载均衡。因此我们在TaaS中开发了Kube2LVS模块,负责对TensorFlow Serving服务进行ListAndWatch,实现TensorFlow Serving Service Info动态reload到LVS config中。
但是在TensorFlow Serving on Kubernetes发布之前,用户已经通过裸机部署的方式在线上部署了Serving服务,用户采用Dubbo框架来进行Serving服务的注册与发现、LB,因此为了兼容已有的架构,我们最终选择使用Dubbo+Zookeeper的方式来取代前面基于KubeDNS+Kube2LVS的方案。
需要说明:
- 我们为TensorFlow Serving服务单独提供了一个CaaS集群,目前并没有和训练集群混合部署。
- CaaS集群的网络方案要求Pod IP对集群外可见,Cisco Contiv(OVS + Vlan)和Calico(BGP)均可。
KubeDNS + Kube2LVS
Architecture
Overview
- 在IDC部署基于
OSPF + Quagga + Tunnel
的LVS集群; - 开发LVS配置热更新的HTTP API,提供给Kube2LVS调用;
- 为了方便管理和部署,我们的线上TensorFlow Serving使用原则:一个TensorFlow Serving实例只加载一个Model,暴露一个Port;
- 上线初期,为了保证(验证)Serve Model的高可用,同一个Model需要一部分副本部署在物理服务器上,另外一部分副本部署在CaaS集群中。业务请求经过LVS分发到对应的物理服务器和CaaS集群节点,均提供
IP + Port
给LVS集群; - LVS集群中,会给每个Model从VIP Pool中分配一个对应的VIP;
- TaaS平台开发Kube2LVS模块,负责ListAndWatch CaaS集群中TensorFlow Serving Service的CUD事件,然后调用LVS HTTP接口更新LVS配置;
- TensorFlow Serving Services以NodePort方式暴露到集群外部,外部访问TensorFlow Serving服务只能通过CaaS集群中的Edge Node,在Edge Node通过kube-proxy经过iptables 4层路由转发到后端真正的TensorFlow Serving容器。Edge Node是Kubernetes节点,但是不部署任何业务容器,只做流量入口及流量分发,通过Node Taint和Node Label的方式实现。
- Edge Node流量过大,可以通过Ansible分钟级扩容(事先准备好服务器);
- 通过TaaS中对每个Serving服务的监控,如果发现某个Model的副本数不够,可以通过在TaaS平台上秒级手动扩容到期望的副本数;
- TensorFlow Serving部署的CaaS集群需独立部署(与TensorFlow Training的CaaS集群物理隔离);
Request & Response路径
- Request的路径:
Client ——> LVS ——> Edge Node ——> TensorFlow Serving Instance(Model);
- Response路径:
TensorFlow Serving Instance(Model)——> Client
高可用
方案保证了请求的全链路高可用,包括以下三个方面:
- LVS的高可用
LVS集群通过OSPF + Quagga
来部署,每个LVS集群部署两个LVS实例来保证LVS的高可用。
- Edge Node的高可用
在LVS集群中,给每个Model分配一个VIP,并4层负载到后端至少2个Edge Node上,这样保证Edge Node这一层的高可用;
-
TensorFlow Serving 4 Model_N的高可用
- 每个Model会通过至少2个TensorFlow Serving实例来加载并提供服务,防止单点。
- 每个TensorFlow Serving实例都能接受和处理请求,具备负载均衡的能力。
- 同一个Model的不同TensorFlow Serving实例会由CaaS自动调度到不同的物理服务器或者机架,防止物理服务器或者机架掉电等引发的单点故障。
- 如果CaaS中的某个TensorFlow Serving实例down了,那么CaaS会自动发现这一事件,并会自动再重启一个TensorFlow Serving实例。
- TensorFlow Serving实例只有部分部署在CaaS集群中,还有部分部署在CaaS集群之外的物理服务器上(由用户自己部署),在LVS层面配置好负载均衡,防止不可预知的整个CaaS集群故障引发单点故障;
- 待稳定运行一段时间后,将所有的TensorFlow Serving实例部署到CaaS集群中;
资源隔离和稳定性
通过裸机在线上部署的TensorFlow Serving实例目前都是单独占用一台物理服务器,如果该Model的负载不高,则会造成一定的资源浪费。部署到CaaS集群后,可以支持单台服务器启动多个TensorFlow Serving实例。Kubernetes提供以下集中资源隔离机制,来保证单个TensorFlow Serving实例资源的同时,也能做好各个实例之间的资源隔离,防止某个Model完全抢占了其他Model Server的资源,也达到了提供资源使用率的目的。
- 通过kubernetes limitrange(足够大)默认使得单台服务器只能部署一个TensorFlow Serving;
- 通过单独给需要的TensorFlow Serving容器配置resource requests(cpu & memory)来保证当服务器资源资源不够用出现争抢时,这个TensorFlow Serving最少可用的资源;
- 如果服务器资源有空闲,则它上面的任何TensorFlow Serving实例都能尽量去利用空闲的资源,提高资源使用率。也就是给容器配置resource limits,否则可能会出现被Linux Kernel OOM Killer杀死的风险。
- CaaS集群中的每台服务器,都会设置给linux系统进程和kubernetes组件预留的资源,以此保证其上面的TensorFlow Serving再怎么榨取服务器资源,也不会影响linux系统和关键组件的运行,防止服务器被TensorFlow Serving搞垮和集群雪崩。关于这部分的详细内容,请参考从一次集群雪崩看Kubelet资源预留的正确姿势。
弹性伸缩
项目初期,只提供用户手动干预的方式进行Scale:
- Edge Node的Scale up/down
需要对Edge Node的网络IO进行监控和告警,当网络IO遇到瓶颈时,准备好物理服务器(两个万兆网卡做Bond),然后通过Ansible自动化部署CaaS相关组件,组件启动后就能作为Edge Node提供流量入口服务和分发的能力了,之后就能添加到LVS配置中作为LVS后端服务。
- TensorFlow Serving实例的Scale up/down
当某个Model Serve的请求量太大,通过监控发现后端的TensorFlow Serving Replicas的负载过高产生告警。用户收到告警后,登录TaaS平台进行扩容操作,增加Replicas数,CaaS会自动创建对应TensorFlow Serving容器并加载Model对外提供服务,以此降低每个实例的负载并提升了处理能力。
线上运行成熟后,根据经验我们可以实现基于定制化的HPA(对接Prometheus)TensorFlow Serving实例的 Auto Scale up/down,全程自动化处理,无需人为干预。
Dubbo + Zookeeper
方案
方案注意事项
-
使用Kubernetes Deployment(replicas=1)来管理一个模型的Serving实例,同一个模型的副本数用户可以在TaaS上配置,注意:
- 每个副本都对应一个Deployment和Service。Deployment的replicas设置为1,TaaS按照创建顺序,给同一个模型的多个Serving副本的Deployments、Services和Pods打上对应的
Label:Index:$N, Model:$Name
。 - Deployment和Service的命名规则为:
$ModelName-$Index
。 - 每个Serving实例的创建,都是按照先创建Service,再创建Deployment的顺序。先创建Service是为了先拿到Kubernetes自动分配的NodePort。
- 如此,每个Pod对应一个Deployment和Service,Deployment负责该Pod的Self-Healing,Service类型为NodePort,负责NodePort的自动分配和代理。(注意,这里不能使用Headless Service,因为Headless Service不支持NodePort类型。)
- 每个副本都对应一个Deployment和Service。Deployment的replicas设置为1,TaaS按照创建顺序,给同一个模型的多个Serving副本的Deployments、Services和Pods打上对应的
-
每个Pod内两个业务容器,一个是TensorFlow Serving容器,负责加载HDFS上的Model并提供grpc接口调用,TaaS上提供用户配置TensorFlow Serving的模型加载策略,默认加载lastest模型;另外一个是Tomcat业务容器,业务jar包在这里启动并进行热更新,jar包实现不同的特征抽取组合进行预测,启动时向集群外的Zookeeper集群注册自己所在节点NodeIP和NodePort。
- 通过downward-api的方式向Pod内注入NodeIP的env。由于先创建Service拿到NodePort,通过给Pod注入env的方式将NodePort注入到Pod内。如此,tomcat容器内就能拿到对应的NodeIP和NodePort,从而启动前去更新dubbo的配置文件。
- 为了兼容一机多实例的场景,不能使用
hostNetwork:true
共享Host网络命名空间,否则必然会导致tomcat和Serving无法启动的问题。
-
如何进行一机单实例部署?
- 上线初期,按照一机单实例进行部署,通过给Pod内的container设置resource.request接近Node Allocatable,使得Kubernetes调度时一个宿主机只能容下一个Pod。
-
如何进行一机多实例部署?
- 稳定运行一段时间后,如果发现集群的资源利用率较低,那么考虑一机多实例的方式进行部署。只需要将Pod对应的resource.request减小到合理的值,使得Kubernetes调度时一个宿主机能容下多个Pod。
完整流程
TaaS实现的流程如下:
- 页面上提供用户配置预测模型的相关信息,包括:
- 模型名称
- 模型的HDFS路径
- TensorFlow Serving模型加载策略相关配置(默认latest)
- 期望实例个数N
- 每个实例的请求资源值(默认为24cpu,128GB,也就是一机单实例)
- TaaS顺序(index从1到N)的为每个实例按照如下逻辑进行实现:
- 先创建一个NodePort类型的Service(加上对应的Label:
Index:$N, Model:$Name
),注意不要指定nodePort的值。 - 然后查询这个Service的nodePort值。
- 再封装Deployment对象,把获取到的nodePort注入到pod的env中。通过downward-api事先注入不确定的NodeIP,调用Kubernetes接口创建该Deployment。
- 先创建一个NodePort类型的Service(加上对应的Label:
- 接着Kubernetes会调度到合适的节点,将Pod内的容器启动。tomcat启动前会获取NodeIP和NodePort,并更新到dubbo配置文件中,并自动上报到集群外的Zookeeper集群。
- Zookeeper会将新的或者发生变更的服务信息自动通知client,client根据负载均衡策略选择其中一个RealServer。
- client选择某个RealServer的NodeIP和NodePort后,发起预测请求。请求到对应的节点后,经过节点的iptables(kube-proxy根据Service自动维护)转发到后端的Pod。
- Pod内的tomcat处理后,对TensorFlow Serving发出grpc请求进行预测。整个请求的数据原路返回。
注意:pod中tomcat和serving两个容器启动的顺序是有要求的:先启动serving容器,再启动tomcat容器。tomcat容器启动前,先去检测localhost中serving服务是否启动成功,如果未启动,则循环等待。
健康检查及流量自动接入与摘除
- tomcat服务启动时会自动往ZK注册服务,通过Session长连接的方式来维护ZK的服务列表。如果长连接断了,那么ZK会自动从服务列表中删除这个实例的信息。通过这种方式完成服务的自动接入与摘除。
- 给tomcat容器配置Kubernetes Liveness Probe,通过模拟用户的请求(会调用tensorflow serving服务)来判断服务是否可用。如果探针失败,则kubelet会自动重启tomcat容器,重启过程中,与ZK的Session长连接会断开,ZK就会自动摘除这个实例。重启后,会重新注册服务,完成自动接入。通过这种方式来防止服务Hang住假死的问题。
- 给tomcat容器配置Readiness Probe吗,如果探针失败,会从Service中摘除这个实例,client的请求即使到了所在的节点,iptables也不会转发这个请求到对应的Pod。
- tensorflow serving容器的健康检查,配置和tomcat一样的liveness probe。如果探测失败,kubelet自动重启serving容器。注意探测的周期不要太短,建议分钟级别。
高可用
-
tomcat服务down了或者Hang住的情况。
- tomcat服务down了,与ZK的长连接就断了,ZK会摘除这个实例,ZK接着通知client,client之后就不会将请求发到这个实例了,直到重新注册成功。
- tomcat服务down了,那么liveness probe就会失败,kubelet会重启tomcat,触发重新注册。
- tomcat服务Hang住的情况,Session没断的话,ZK是无法感知的。但是不要紧,liveness probe会失败,kubelet会重启tomcat,触发重新注册。
-
tensorflow serving服务down了或者Hang住的情况。
- tensorflow serving容器配置了liveness probe的话,如果探测失败,kubelet会重启这个容器。
-
实例所在的服务器down了的情况下。
- 实例所在的节点down了,会导致Session断开,ZK感知到这一事件并自动摘除对应实例。
- 节点down了后大概5min时间,会在其他节点重新启动一个实例,新实例启动后往ZK中注册服务。由于线上都是多副本部署的,这个实例5min内不可用不要紧,其他副本能正常提供服务即可。
-
实例所在节点与ZK的网络挂了的情况下。
- 网络挂了,Session就断了,ZK感知到这一事件并自动摘除对应实例。
总结
本文介绍了两种使用Kubernetes部署TensorFlow Serving服务,并完成服务发现与负载均衡的方案。基于KubeDNS+Kube2LVS的方案使用Kubernetes原生的特性,基于Dubbo+Zookeeper的方案则使用Dubbo的服务发现与软负载特性。当然,还有很多的实现细节需要读者自己思考,有需求的同学可以找我讨论。