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的方案。
需要说明:
OSPF + Quagga + Tunnel
的LVS集群;IP + Port
给LVS集群;Client ——> LVS ——> Edge Node ——> TensorFlow Serving Instance(Model);
TensorFlow Serving Instance(Model)——> Client
方案保证了请求的全链路高可用,包括以下三个方面:
LVS集群通过OSPF + Quagga
来部署,每个LVS集群部署两个LVS实例来保证LVS的高可用。
在LVS集群中,给每个Model分配一个VIP,并4层负载到后端至少2个Edge Node上,这样保证Edge Node这一层的高可用;
- 每个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的资源,也达到了提供资源使用率的目的。
项目初期,只提供用户手动干预的方式进行Scale:
需要对Edge Node的网络IO进行监控和告警,当网络IO遇到瓶颈时,准备好物理服务器(两个万兆网卡做Bond),然后通过Ansible自动化部署CaaS相关组件,组件启动后就能作为Edge Node提供流量入口服务和分发的能力了,之后就能添加到LVS配置中作为LVS后端服务。
当某个Model Serve的请求量太大,通过监控发现后端的TensorFlow Serving Replicas的负载过高产生告警。用户收到告警后,登录TaaS平台进行扩容操作,增加Replicas数,CaaS会自动创建对应TensorFlow Serving容器并加载Model对外提供服务,以此降低每个实例的负载并提升了处理能力。
线上运行成熟后,根据经验我们可以实现基于定制化的HPA(对接Prometheus)TensorFlow Serving实例的 Auto Scale up/down,全程自动化处理,无需人为干预。
- 每个副本都对应一个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类型。)每个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实现的流程如下:
Index:$N, Model:$Name
),注意不要指定nodePort的值。注意:pod中tomcat和serving两个容器启动的顺序是有要求的:先启动serving容器,再启动tomcat容器。tomcat容器启动前,先去检测localhost中serving服务是否启动成功,如果未启动,则循环等待。
- 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的服务发现与软负载特性。当然,还有很多的实现细节需要读者自己思考,有需求的同学可以找我讨论。