2017年10月21日,CNCF Meetup——Kubernetes架构师实践日活动于深圳成功举办,本次活动由七牛云和K8S技术社区联合主办,特邀vivo、平安科技共同参与,会议以Kubernetes 技术落地过程中遇到的坑及其解决方案为主题,来自EasyStack、平安科技、vivo、七牛云的技术大咖进行了干货分享并传输实际技术和应用经验,吸引深圳地区250+技术爱好者火爆现场参与!
会上,vivo资深云计算工程师王涛分享了技术主题 :如何落地 TensorFlow on Kubernetes,以下是他的演讲部分PPT和内容整理:
大家好,我叫王涛,vivo资深云计算工程师。
我今天要和大家分享的是我最近做的项目TensorFlow on Kubernetes中遇到的一些问题,在项目中的思考和总结,希望可以和大家分享。
今天下午主要从这几个方面和大家交流,一是给大家介绍什么是分布式TensorFlow,为什么要把它放到K8S上跑,怎么把它跑到K8S里面,整个部署架构是什么样的,一步步怎么把项目落地,整个落地过程中遇到什么问题可以和大家交流,接下来我们还需要做些什么事情。
TensorFlow在0.8版本的时候就发布了分布式TesorFlow特性,之前没有分布式训练的时候,对参数非常多的模型训练需要很多时间,在这篇论文里面提到一个MOE模型,里面说680亿个参数,这样的模型放到单机里面训练你可以想象要训练多长时间。大家了解分布式TensorFlow可以看一下这个视频,Derek Nurray做的分享。
这里简单过一下什么是TensorFlow。单机情况下,一个Client对应一个worker,在算法里定义c0pu0做什么事情、Gpu0做什么事情,这是单机情况下的例子。对于分布式的情况,我要尽量利用更多的服务器来训练,加快训练的进度,加快梯度的下降。这个例子里面client,这个任务有一个ps,ps的意思是做一些参数的存储,以及数据操作和更新,worker是真正计算的,这里可以指定PS0需要哪些参数,W和B。这个worker0训练的模型用哪个GPU,这是简单的分布式模型。
这个图介绍了PS和worker两个模型对象分别做什么事情,PS是参数 server,里面主要是做一些变量的存储和操作更新,worker主要是做数据预处理以及BP等。分布训练一定要有分布式存储,昨天有一个学生找我,他说他们都用本地存储,本地存储老遇到一个问题,他一直想不通,一个worker节点挂了,重新启动worker节点,训练不能恢复,训练又重新开始了。他们就一直想不通问题,后来我问他们,他们说他们用本地存储,为什么用本土存储做训练?
worker0会做一个checkpoint,会把当前TensorFlow集群的状态、训练步骤持久化到存储里面,用本地存储,你在自己的服务器上。如果其他的worker跑到其他服务器上,就没有存储在上面,你就没有办法继续训练。简单的用NFS。这是经典的分布式TensorFlow的模型,几个比较重要的概念,一个client,是用来提交整个算法的地方,这个地方创建一个session,这是和TensorFlow下发你的算法。集群里面有一个重要的概念是job,job会有对应的task,task里面有两种类型,PS类型和worker类型,PS用来做变量的更新、存储,worker是做计算和更新。
我们怎么描述TensorFlow的集群?通过TensorFlow的ClusterSpec描述一个集群,这有三个worker,每个worker的IP端口是什么,你可以输IP,你可以写域名,这里写的是域名。PS,你有几个PS?有两个PS,这里描述了TensorFlow的集群。所有worker之间的通信都是通过GRPC进行交互的。
分布式训练里面有很重要的概念,副本训练,副本训练分两种类型,In-graph和Between-graph,In-graph是说所有的请求都从同一个client开始提交,这个算法里面定义的不管是task还是worker要做的事情,都从这个client提交。ps会做变量的存储,worker做训练。In graph的场景主要是用在开发人员进行调测算法用的。
between-graph是每个worker对应一个client,10个worker就是10个client,每个client分别定义对应的worker自己要做的事情。在我们公司,相信大部分企业也是这样的,我们的场景主要是异步训练,异步+Between-graph的训练。先来说异步和同步,这是同步的模型,这里有一个worker,后面三个worker,worker更新同一个参数的时候,他们相互等待,这个worker训练完了,我更新这个参数,你给他,你训练完也给他,如果你先训练完,你要先等其他节点训练完,发送更新请求之后,才会真正去执行update的操作,它有相互check的过程。我要知道你是不是训练完,大家都训练完这一步才会把这个参数做更新,下面更新之后的参数进行再次的迭代计算。异步是不管,自己弄自己的,你更新完进行自己的训练,不等,大家都是异步的。我们公司是异步+ between-graph的场景。前面是分布式TensorFlow的介绍。
我们只要记住一个点,怎么定义一个TensorFlow集群,需要多少个PS、需要多少个worker,PS、worker都是一个进程。第二部分是为什么要把TensorFlow部署在K8S里面?如果直接用官方TensorFlow的特性,会有很多的痛点,你会感觉手工操作的东西要很多,创建一个集群要先规划,这个集群里面要在哪些服务器上部署worker、PS,要先定义好这些IP,定义好之后再写算法,再找对应的worker,先需要进行规划。使用CPU、GPU的时候还要手动指定用这个CPU、这个GPU。
在我们公司主要是解决这些痛点,TensorFlow里面各个task,Task分PS和worker,他们训练的时候,资源没有做隔离,这样会有什么问题?如果某台服务器,某个worker的压力非常大,它把整台服务器的CPU或者内存都耗进了,可能会影响其他用户的训练,如果其他用户在训练,在这个节点可能就会影响别人的训练,它没有做到资源隔离。它还没有GPU调度,用户需要手动配置,这个任务需要使用哪个节点的第几块GPU,集群规模大就很不方便。整个Task管理是很麻烦的。
我们公司训练很多时候会有上千个PS,或者上万个worker。这些管理是很麻烦的,我们需要查看每个Task训练日志,这个Task有没有训练结束或者有没有Hang在那里,这些我都看不到。它原生支持的工具存储主要是HDFS,它的随机读的性能是很低的。我们创建一个大规模的TensorFlow集群是很不容易的,前面说了要手动做配置,要规划IP,然后再手动配算法,需要使用的资源。目前我们有这些困难。
为什么用K8S?
K8S天生具备这样的能力,资源隔离,K8S有很多资源,它本身的资源隔离不用说。它提供ResourceQuota、LimitRanger,支持GPU配置只要配limits就可以。容器运行训练任务的时候,整个K8S对容器提供了一整套完整的生命周期管理的接口,可以很快的创建好每个TensorFlow的集群,日志等于都要提供日志平台,都是基于ELK或者EFK做成熟的解决方案。系统原生支持HDFS,性能是不够的,K8S支持更优秀、更棒的存储,glusterfs 、Ceph这样的存储。我们很容易通过一条命令创建TensorFlow的集群。
这就是为什么把TensorFlow放到K8S里面。前面讲了HDFS的性能,这个数据是来源于别人做的测试。结论是GlusterFS在随机读的性能是最棒的,相对HDFS和Ceph来说是最棒的,Glusterfs-12GB,HDFS-3GB,Cephfs-2GB。针对这个问题,我们选用的是Glusterfs存储,虽然我们云平台主要用CephRBD,但是针对这个项目,我们用的是Glusterfs,它相对Ceph是很成熟的,因为它经历了很多时间考验。前面讲的是为什么要做这个东西。后面是我们怎么做这个事情?GlusterFS和K8S和TF的架构图。
数据存在哪里?集群是GlusterFS里面取的,K8S读这个GlusterFS的数据,需要通过Heketi这个东西去对外暴露API,K8S通过创建一些StorageClass,是对接后端的GlusterFS,然后我定义POD的时候,定义容器的时候,可以指定POD用哪个PVC或者PV的申明,然后由它自动创建它需要的PV。
我们这边主要创建两个PV,一个是做训练数据存储的,data PV,另外一个是训练日志存储的,log PV。他们会挂在到对应的worker上面去,还有PS,PS主要是用日志 PV的,训练是在worker里面的,所以Data会挂在worker上,没有在PS上面。用户有多种方式使用这个TF集群,第一种是通过TensorBoard NodePort的方式去使用这个集群,通过在里面创建一堆Deployment,暴露一个service,用户通过Node ip加端口号来访问TensorBoard,然后通过TensorBoard 来提交算法,也可以在TensorBoard 中去查看训练的过程。这是开发人员主要用的。
这个图是HDFS+K8S+TF的图,也是我们现在整个阶段主要推的,现在主要是做HDFS,性能没有那个好。其他地方都一样,存储这块用HDFS,K8S最新版本我不知道它有多少种PV Plugin了,1.7版本应该是25个存储插件,遗憾的是没有HDFS的插件,只能通过HDFS网络路径去访问HDFS的数据。
worker节点是用JOB跑的,PS是用一个Deploymet跑的,为什么worker用JOB跑?因为它只要训练完一次就要结束,JOB服务适合我们这个环节用,我们需要对JOB机制做一些简单的操作。我们做这几个配置,JOB里面使用有要注意的地方,JOB yaml文件定义的时候,JOB跑几次就认为它成功?默认跑一次,JOB运行成功一次,这个JOB就结束。JOb支持并行,这个任务可以并行几个POD跑,JOB可以配置存活的时间,还是比较有用的。
比如有一个用户写的定时任务放到JOB里面跑,他其实写错了,会导致什么问题?放到里面跑,它不断重启,K8S里面所有的重启都是把POD干掉,再创建一个新的POD,你会发现很多垃圾的POD、死亡的POD在里面,如果设置activeDeadlineSeconds就不会一直重启。前提是要了解这个情况,这个JOB最多运行5分钟,5分钟没有结束,我认为它是异常的,你就可以设置为5分钟。重启5分钟就不会再重启了。这是非常重要的特性。在k8s 1.8里面有一个Job有个flied:backoff Limit,这个可以帮助解决这个问题,你的JOB写的不好一直重启,重启3次我就认为它有问题,你可以设置。这个配置只是在1.8里面才有的,我们用的版本是1.7.4。这是JOB需要注意的地方。前面总结的是TensorFlow怎么跑到K8S里面去,他们的模型是怎么对应的。
部署,我们技术栈,TensorFlow用1.3.0,Kubernetes1.7.4,Docker1.12.6,Harbor用1.1.2,Glusters 3.10.5,Contiv+OVS+VLan的网络。
因为Contiv网络插件需要Etcd2的版本,我们这里有2和3的版本,2是给网络插件用的。部署图的组件,首先是它本身的HA,前面讲师已经分享过了。前面提到用户节点,用户可以登录里面做提交训练算法的动作。这边是GlusterFS的集群,这边还有HDFS的集群。我们现在训练数据,首先是HDFS里面,用户的数据都存在里面,之前的数据存在里面,需要的动作是先把FDS的数据同步到里面,然后才能拿到。这不是很好。
前面讲的部署架构,后面讲怎么落地这个项目:
总共三步,第一步,用户上传训练脚本,用户写的脚本以及Python文件,我会让他们登录一个节点,把他们写的算法、脚本对应到目录下,对外是http的文件服务器,用户可以下载它上传的算法,在容器启动的时候可以下载这个算法。这是文件目录结构的示意图。前面的/var/www/html是文件训练器的目录,接下来是训练算法的名字或者是用户名,接下来是范围名字,在里面存放训练的脚本。入口和用户协定,训练的入口是启动run.sh就可以,至于sh怎么调里面的东西做训练我们就不关心了。Run的时候需要传的参数,后面我们会再讲。第一步是上传。
第二步是检查模板是否在目录里,K8S去创建TensorFlow集群,这个集群的yaml文件是根据模板自动生成,只要用户配几个参数就可以自动化生成TensorFlow集群的yaml文件。这是一个模板文件,定义的是TensorFlow集群模板文件,这个目录下面会有三个文件,除了刚才提到的模板文件之外,还有一个解析这个模板的,这个文件可以在TensorFlow里面下。如果用的是glusterfs-storageclass的话,还要创建PV、PVC,PV、PVC的间也是通过模板来生成的,这需要PVC的模板,这不是我们今天讨论的。我们只需要关注这个模板是怎么定义的,这个在TF集群是怎么定义的。
这是使用glusters,定义一个PVC,还要定义一个storageclass模板,storageclass是事先创建好的,PVC是模板,还需要部署好heketi,还需要heketi-secret来定义里面的用户密码,这不是我们的重点,我们不看这个,主要是看TensorFlow集群的模板。TensorFlow集群的模板,用户只需要填四项,算法的名字,你需要多少worker,worker副本,这里需要10个worker副本,需要一个PS,然后定义。训练算法从哪里下载,这可以暴露一个HTTP的下载路径,可以填这个路径,接下来就可以一个命令创建TensorFlow集群。接下来的工作会在模板里面。这是其他模板的定义,比如说这里自动化生成一个worker hosts,然后自动化生成对应的PS hosts,然后定义job,如果是worker跑job,定义好job,这里还需要注意的是我会为每个训练算法创建一个namespace,里面还有Secret,这个secret是连harbor secret。harbor secret是怎么来的,后面会讲。因为worker PS之间的通信暴露的是service,用户是通过service来交互的,我不会为每个service创建一个cluster ip。后面会讲为什么使用这个。
定义worker的模板,job,名字是什么,job的名字是通过namespace名字,然后-0、-1……-9,然后配上harborsecret,主要看这个地方,它是运行的脚本。这里做的是Export的环境变量,看起来很恶心的动作,但是没有办法,后面会提到为什么会这样。接下来是这个项目,之前用户的训练脚本下载下来,给前面定义的脚本路径下载下来,然后算法目录,启动它训练算法的入口,shell需要转模板里面前面生成的PSHOST,worker host,job类型,workerPS,接下来是PS,PS为什么没有用JOB跑呢?因为PS有点特殊在TensorFlow里面,PS启动之后就执行一个命令,一个join in的命令,等待worker的加入,这个命令一旦执行,就不会自动结束。我这个训练训练完之后,PS进程还是会卡在那里,它跑的也是一样的,在同样的算法脚本,然后在算法里面区分这个类型,根据这个参数,你做worker的事情,PS做PS的事情,另外也会传进去做一些切割,我要跑一百个worker,需要切一百份,读对应的一份,根据这个进行索引。你也可以在集群里面创建Tensorboard,还要暴露一个service,service的暴露是通过NodePort来暴露的。用户不多,可以协商,对我们公司来说,线上的用户可能不到10个,我给他规划好你用哪个端口,他用哪个端口。
前面是一个模板,用户不需要自己写什么的,用户只需要改模板前面提到的四项内容,算法名称,worker副本数、PS副本数以及脚本路径,直接改完这四个之后,一个命令就创建完了,这个是模板的yaml文件、这个模板,这个命令执行完之后,就直接生成对应的集群要用的yaml文件,比如先放到yaml文件里面,接下来就可以创建集群,集群创建起来,用户不需要做什么操作,创建起来之后就开始训练了。创建完了之后,就去查看间TensorBoard的集群,你自己的worker已经在端里面,在这里就可以看到训练的任务,一个PS、四个worker都可以看到,worker已经在端里了。
你可以在里面查看每个PS、每个Task的日志,这是PS的日志,可以看到PS的日志已经启动了本地的端口。worker的日志,worker0的日志可以看到,已经开始做计算了,说明训练开始。如果没有这个,你要手动登录去查,去找对应的日志查。我们为PS、worker创建service。还有Harbor Secret,加密完这个数据,我们就可以看到用户名、密码之类的。前面可以根据我这个模板修改自己的四个配置,执行一条命令就可以把TensorFlow集群创建起来。中间也遇到一些问题,我这边列举了一些问题。
1、worker有问题,一直在recreate pod。我自己不小心创建了一个有问题的worker,我自己创建的有问题的集群,创建完我就吃饭去了,回来之后忘记了,后来看到服务器上面这么多退出了这个东西,报告里面有1万多个容器在里面,这个服务器执行命令有点卡。因为worker是用job跑,job如果有问题会一直循环下去,如果没有做任何的配置,它会永无止境跑下去,最后把节点的存储都消耗完。在Kubernetes1.7里面可以做两个配置稍微降低这方面的事情。第一个是Kubelet可以配置节点上死亡的容器是多少,如果配置100个,这里永远不会超过100。如果第101个死亡了怎么办?Kubelete有回收机制,根据你的时间帮你把死亡的东西垃圾回收。在1.8里面,我不知道训练要多久,如果有问题,重启两天也没有意义。有backoffLimit前面还提到Deadline Seconds,这个不行,这个其实不好,除非我知道这个训练大概持续30分钟、1个小时,就可以配合一下,其实意义也不大。这是一个back的过程,第一次重试10秒,第二次重试的时候是20秒,接下来40、80、160,这样子越来越慢。
前面提到的不管是PS还是worker都是通过headless service。这是官方的话。创建的时候定一个service把它cluster ip那个地方编写一下,你创建一个Headless service,你定义之后它就不会给你分配一个IP,它没有给你分配一个VIP,kube-proxy就不会给你做LB。headless service相互之间怎么通信呢?他们也是通过service后端的Pod,前面讲到,定义service定义了算法、名字、job类型,对应匹配到JOB里面,一一匹配起来,通过service来找到它。你不用headless service也可以,你用正常的service就多了一层,Normal service,因为你是有vip的,他们之间的通讯是需要经过service VIP的,虽然现在默认的模式性能确实还没有,大规模产品下还是有些问题,服务上万,这个规则可能几万、几十万,当你的iptables rules达到十万以上规模的时候,这个就不可用的,你更新rule耗时是非常长的。现在整个社区1.8的时候发布了新的模式,ipvs模式。如果不用serviceVIP,直接可以去DNS里面解析,解析的IP就是POD IP,直接到对应的POD,而不是解析到VIP,这样效率就会比较高。其实对于AI训练,性能还是第一位的。
这是比较让人不爽的,环境变量的定义,大家应该小心的地方,比如Dockerfile,写的ENV ClassPath,我希望这里执行一个命令,然后拿出对应架构的路径,配到里面去,这个命令解析完会有一对jar包的路径,你把它配进去,你进入容器里面看它的环境变量,ClassPath你写什么就是什么,不会执行这个东西。这样导致的问题是Job、worker一直起不来,一直报错,有一个类找不到,那肯定是Jar问题,这个环境变量哪里有问题,这个好像不太对劲,这是环境变量定义需要注意的地方。问题在这里,怎么解决?Walkaround就写在Command里面,就硬执行一下,它就会解析这个命令,执行完以后有一大串的Jar,它会配套在里面去,这样就没有问题了。
还有一个比较经典的问题,是TensorFlow的问题,它是PS 进程,它会一直hang在那里,你这个训练完了,一个小时之后PS进程还在那里,不会结束。你就可以看到我训练完了,worker节点状态都是结束的,PS一直在running在这里。有很多人讨论这个问题,现在还没有官方对应的可行方案出来,用共享的队列来统计这个训练的worker数结束的数量。这个问题还没有做,在K8S中我们很容易做。比如我们可以写一个脚本,周期性遍历这个集群中的所有namespace算法,如果定义10个worker,我统计,worker的状态如果达到10个worker是completed的时候,我就认为训练结束了,PS没有什么用了。如果发现某个namespace下的所有worker job状态为successed,则可以kill hang 住的PS Deployment,甚至删掉namespace。它上面有一个DevOps平台,在上面可以做TaaS平台,通过Events的机制来对worker进行统计,甚至更高级一点,因为它对每个POD的生命周期有一个Hook,PreStop Hoook让POD结束之前发一个Hook,你可以通过它来通知你worker训练结束了。不管怎么说,在DevOps里面很好解决,TF里面不是那么好解决。
Reuse PV,训练数据存在一个PV里面,训练结束了,删除了对应的PVC,PV的状态是Released,还有数据在里面,现在还要再训练一次,能不能用之前创建的PV来训练呢?答案是不能的。你不能直接拿PV,你用不了,考虑安全机制,比如PV里面存续比较隐秘的数据不允许你用这个,如果你真的要这么干怎么办?你也有办法做这个事情。你有两种算法,你把原来的PV,每个人都可以用,就去把PV里面对应的ClaimRef引用,你把这段应用删掉,这个PV谁都可以用了。这个图可以显示是什么东西。另外一种想法是把PV给某个PVC用,这样就不需要这个风险。你直接把PV原来的索引先写一个名字,ID留着,这个就只能给你或者新创建的PVC用。现在要改Clam的引用,如果把这段全部删掉,这个PV就可以给那个人用。如果你把这个类设成你绑定的PVC,这个UID干掉,那这个PV就只能给设定的PVC用。
Secret,它没有全局的Secret。比如Harbor,它有很多namespace,每个创建一个harbor是比较锉的。我们想办法,在模板里面自己创建Secret harbor,Secret的数据怎么来呢?创建一个Secret,Docker registry harborSecret,输入用户名、密码,E-mail,这样就间了一个Secret,你就可以看到Secret的内容,get一下可以看到内容,只要关注.docker,把这串东西复制出来,然后创建到模板里面,自动化的创建一个harbor Secret。这里面有些问题,镜像安全问题,比如有一个用户要创建一个POD,POD调度到某台节点上,这个节点上把镜像拿下来是集中成一个POD,这有问题。另外一个用户,不管是故意还是有意,也写了你的image的地址,也要拿这个镜像,用户也调度到那个节点,他会检查,本地有镜像不会连harbor试权限,直接用,这个镜像涉及安全隐私,你的业务镜像被别人跑起来,在公有云里面是很大的问题。怎么解决呢?下来交流。当然还有很多问题思考,比如worker挂了怎么办?PS挂了怎么办?Chief挂了怎么办?训练里面有很多worker,一般把worker设为chief,作为chief和其他worker有什么不一样?唯一的不一样是它做check point,这是训练的状态。这个worker挂了怎么办?某个task restart重启之后,训练进度又是怎样的?很多事情都是需要思考的。
我们后面需要做的事情,现在做这个项目时间不是很长,现在很多都是手动操作,没有给用户去做易用性。后面需要支持GPU,现在要尽量压榨GPU,主要是IO要做优化。后面我们会做一些测试,现在也在做测试。为TensorFlow提供一些GPU调度。甚至开发TaaS产品,TensorFlow service上面。为TensorFlow集群提供信息看板,用户在整个Flow端就可以提供。这些特性包括提供权限控制,查看TF集群状态,页面上传下载训练算法和数据,大规模的后端分布式存储集群建设。这个模型训练完了之后,要支撑TensorFlow模型上线,支撑TensorFlow Serving部署在K8S集群里面。支持创建Jupyter Notebook,方便开发调试算法。Tensorboard方便对接到对应的训练中,方便开发查看训练方方面面。
领取专属 10元无门槛券
私享最新 技术干货