程威 分布式实验室 

同程艺龙大数据在Kubernetes的实践_Java

同程艺龙是中国在线旅游行业的创新者和市场领导者,秉持 "让旅行更简单、更快乐" 的使命。作为同程艺龙的数据底层支撑部门,我们在2018年推动所有组件容器化部署,2019年推动所有服all in Kubernetes战略,在这个过程中我们也遇到了一些问题,比如存储组件是否应该上Kubernetes,外部世界如何和Kubernetes集群打通,本文将向大家介绍我们使用Kubernetes部署分布式系统的一些经验和思考。同程艺龙数据中心容器化背景

同程艺龙大数据在Kubernetes的实践_Java_02


同程艺龙是中国在线旅游行业的创新者和市场领导者,秉持 "让旅行更简单、更快乐" 的使命。作为同程艺龙的数据底层支撑部门,我们每天会产生海量的数据进行存储和计算,为了更好的支撑业务,我们在2018年推动所有的服务都采用了Docker Host模式部署 。我们一开始采用脚本管理容器服务,虽然能够做到自动化部署,但是不同组件的资源池没有打通,机器剩余资源的把控不到位,服务的滚动更新和故障转移也处于一个人工处理的状态,这个时候我们急需一个统一调度和管理集群资源的平台,所以2019年我们开始尝试将所有服务部署到Kubernetes上,已经投产的存储服务有Elasticsearch,Tikv,Kudu,Kafka等一些节点类型少的服务,计算组件有Hive,Spark SQL等服务,在这条艰辛的on Kubernetes之路,我们也遇到了很多问题,今天向大家分享一下。


Kubernetes集群如何和外部世界打通

同程艺龙大数据在Kubernetes的实践_Java_02


首先看下我们Kubernetes集群的架构图:

同程艺龙大数据在Kubernetes的实践_Java_04


Kubernetes集群和外部机器通信,我们采用的OVS虚拟交换机的方式将物理网络和容器网络做成一个大二层的网络,这样Kubernetes集群外部的机器也能访问到容器。因为我们会部署分布式系统,有些分布式系统要求IP不变,所以Pod我们做了IP Local,根据Namespace和pod name来记录每个Pod的IP,IP保留时间为三天。为了防止极端情况下的需求,我们给Pod做了指定IP的功能,但是由于我们部署使用的是自定义对象或者StatefulSet对象,这个功能还需要在上层对象的Controller中支持。上面这些功能我们都是基于Contiv的netplugin插件,在使用过程中我们遇到节点veth pair删除不干净的情况,我们采用定时检查删除无用veth pair来保证网络的干净。
OVS架构图如下:

同程艺龙大数据在Kubernetes的实践_Java_05


Pod网络的问题解决以后,Service的负载我们希望和Pod网络一样可以直接访问。这里我们使用Kubernetes Java Client Watch Service和Endpoints Event事件,将EndPoints和Service Cluster IP同步到同程艺龙的基础设施组件TVS上,TVS是一个四层负载均衡,我们通过HTTP的方式进行同步。在刚开始上线的时候,我们发现Kubernetes Client切换leader节点的时候会重新消费之前的Service event事件,所以同步一定要保证幂等性,Service删除事件同步要加上Kubernetes Service是否存在的校验,确认Service真的不在的时候,再去删除TVS的VIP。这种方式将我们的路由规则更集中式的管理,不会像kube-proxy将所有的规则同步到每台物理机器上。
Service网络解决以后,由于分布式系统的一些特性,有些组件是一定要通过域名才能启动和访问。所以我们会把Kubernetes集群内的节点域名全部注册到公司的DNS服务上,这里我们只要将Kubernetes内部域名改成公司提供的域名尾缀并重启集群和CoreDNS,然后将CoreDNS的域名同步一份到公司的DNS服务上。
做了上面这些支持后,我们觉得这样一个集群才符合生产环境的使用,既可以使用Kubernetes的很多特性,也和公司内部组件做了整合,用户的体验感也没有太大的变化,反而借助于Service和域名的自动同步,Kubernetes的编排能力,体验更佳。


存储计算组件上Kubernetes集群的实践

同程艺龙大数据在Kubernetes的实践_Java_02


因为Kubernetes设计的初衷是为了部署计算型的组件,所以我们觉得部署计算节点对我们来说难度不大,我们部署了 Hive,Spark SQL,Tensorflow server等计算组件,不同组件我们采用不同的Namespace进行管理,不同组件的机器资源我们打上标签,做好资源池的划分。针对Hive这种两种及以上类型的节点,一开始我们会采用Helm写好模板做好一键部署,然后我们会投入人力做Hive的Operator,开发Operator可以将我们的一些人工容灾的思想用代码去变现出来,更新多节点类型的组件也会简单很多。
我们都会遇到存储组件应不应该上Kubernetes,我到底应不应该用共享存储的问题。我们认为存储组件是可以上Kubernetes的,这里我们采用的 Local PV+Ceph的方式,针对大数据存储组件,比如ES,TiKV,Kafka等组件,我们采用Local PV的方式,像物理机一样将数据存在本地磁盘,保证性能。针对Jupyter一些非重要的组件我们会采用Ceph RBD Image的方式,用来节省本地磁盘的空间。刚开始的时候我们采用批量创建Local PV的方式,后来我们希望控制磁盘的使用,一个或者两个存储组件配一些计算组件做到资源的混部,我们将Local PV的创建做成按需分配,合理的控制磁盘的使用。
存储组件我们第一个部署的组件是ElasticSearch,ES是非常容易支持的,因为客户端是http访问,对外只需要提供 ES client节点类型,不需要暴露集群内部节点信息,利用Service只需要提供一个TVS的VIP给到用户。刚开始我们使用StatefulSet部署Elasticsearch,由于Statefulset顺序性的限制,我们不能指定下掉某个节点,于是我们开发了一个高级版的AdvanceStatefulSet,通过指定序号就可以下线Pod。
像这样配置即可:

offlineStrategy:  podOrdinals:    - 0


我们将ES部署方式用Helm模板写好,做到自动化部署,每一个节点类型的容错还局限于AdvanceStatefulSet的Controller。ES官方也提供了Operator,因为处于beta版本,自定义资源还有很多问题,没有基于Sts做CRD,所以我们没有选择这种方式。我们觉得在Sts上层再建立一层CRD,可以更方便去管理多种节点类型,但是人力有限,我们对这个需求的优先级还不是很高,当前基于AdvanceSts管理也能够运维起来。
在部署Kudu组件的时候,我们延用ES的套路,发现节点域名解析不到的问题,当时我们还没有将Kubernetes内部域名同步到公司层面的DNS。在分布式系统中,很常见的场景是客户端连接到Master节点,Master节点会返回Worker节点域名,在Kubernetes集群外部的机器是没法访问到Worker的域名的。在没解决DNS问题之前,我们先采用Host模式部署,注意一下port冲突问题就好了。
因为我们在2018年所有的组件已经on Docker,我们只要将组件的部署方式迁移到Kubernetes上来管理,所以像Kafka,TiKV,PG,ZK这些组件,我们都可以轻松的上Kubernetes。不过在大数据组件当中,机器资源最多就属Hadoop组件,在以前Docker Host部署的时候,DataNode和yarn节点会挂载sata的12块盘,TiKV EleatiscSearch我们采用的是ssd raid50单块盘的方式,我们发现Local PV的方式不能很好的挂载多块盘,如果强行的挂载12个PV,scheduler需要支持每个PV在不同的磁盘,才能符合我们利用多块盘读写的目的。Local PV没法解决我们的需求,我们开始尝试HostPath,HostPath现在没有一个好的资源管理器,HostPath不能和机器属性绑定在一起,简单来说就是让HostPath具备Local PV机器绑定的特性。所以我们正在开发一个将HostPath也转变成PV资源进行管理,具备和Local PV一样的特性的Agent。当前我们采用的折中方案是AdvanceStatefulSet+nodeSelector+Affinity来部署yarn和DataNode,不过我们没有大规模使用,因为不够优雅。


Kubernetes集群部署分布式系统总结

同程艺龙大数据在Kubernetes的实践_Java_02


我们认为在Kubernetes上部署存储或者计算型的组件是可行的,做好外部世界和Kubernetes集群内部的通信,或者将所有的组件都放在Kubernetes集群内部。针对性能要求比较高的组件,采用Host模式+Local PV的方式部署。在没有人力和开源成熟的Operator的时候,可以先用Helm模板化部署方式,先解决自动化部署。提供可视化的运维操作,通过人工组合操作的方式先代替Operator自动化运维。将一些通用的功能搬到Operator之上,并将Operator定制化能力提供给用户,让用户成为你的开发者,共同构建Kubernetes生态。当前我们维护了自己的AdvanceStatefulSet Controller,提供镜像原地升级,指定下线节点的功能,维护一个基础的Base Operator,我们希望将一些功能通用的覆盖所有组件,针对Hive这种多节点类型的服务开发独立的Operator。在社区我们积极参与TiDB Operator的开发,我们觉得当前TiDB的云原生方案是比较成熟,利用TiDB Operator部署的TiKV我们已经上生产了。这些支持得益于我们对于Service Pod DNS的支持,赋予Kubernetes的网络和磁盘静态分配绑定的能力。
以前我们会部署一个大的分布式系统业务混用,比如ES,现在我们可以利用Kubernetes切成小集群,每个集群都做好日志和监控的功能,业务可以申请自己的集群,资源上是隔离的,不会因为资源互相影响。懂组件的业务也可以根据自己的场景,不断优化自己集群的配置。当我们对组件性能把控不到位的时候,采用小集群是规避风险的一种方式。
关于监控,我们使用一个集群配一个Prometheus+Grafana的方案,做到监控隔离,Prometheus我们采用Thanos架构,提供统一的监控查询。
监控参考:

同程艺龙大数据在Kubernetes的实践_Java_08


关于日志,我们采用sidecar注入到Pod的方式,收集到Kafka,Flink落地ES,保留七天,我们可以针对日志做一些报警策略。
日志参考:

同程艺龙大数据在Kubernetes的实践_Java_09


一些天生支持云原生的组件探索

同程艺龙大数据在Kubernetes的实践_Java_02


除了已有组件尝试部署到Kubernetes,我们也在探索新的组件,比如JupyterHub,JupyterHub我们采用的是Ceph共享存储。基于JupyterHub接口能力,提供自定义镜像和定制化框架的能力,每个用户可以申请自己的Jupyter,分配固定的资源配比。我们也尝试部署了KubeFlow,想借助于TensorFlow Operator 部署分布式任务能力提升算法侧的能力,不过因为KubeFlow要绑定Istio组件,我们觉得太大了,所以没有将其放到线上。我们会基于Jupyter提供更好的数据分析层面的交互能力。
我们还部署了TensorFlow Server,打通HDFS和TensorFlow Server,用户使用Spark训练的TF model生成到HDFS之上,可以自动部署成TF server,做到模型的自动部署和滚动更新。还有Dask等等服务。
我们发现已有组件官方都在做云原生的支持,新组件会自带Kubernetes的支持,所以on Kubernetes未来一定可以走的更远。


遇到的问题和未来规划

同程艺龙大数据在Kubernetes的实践_Java_02


在on Kubernetes这条路上,Kubernetes不能拿来即用,要根据自己的场景适配。在不影响已有业务的情况下,提供不改变用户行为的场景。原本用户可以很方便看到日志,知道请求落到哪个点上,那么你就需要提供很好的日志收集服务,提供更稳定的服务,才能让用户相信你的能力。
在网络方面,我们会尝试DPDK来加速容器网络以及其他硬件方案,将大数据服务本地性需求弱化,做到存储和计算的分离,提升扩展性。
在磁盘方面,我们会提供不同的规格的尝试,当前我们是混部,我们会提供LVM+混部+分区的方案选择,提供更灵活的部署方式。
在平台层面,我们会将机器和底层研发用户分开,可视化的运维集群,提升安全性和便捷。
在技术变革的快车道上,我们要制定好如何在行驶中换轮胎,甚至是换引擎。