[.com原稿]2018年5月18-19日,由知乎主办的全球软件与运维技术峰会在北京召开。在“开源与容器技术”分论坛上,知乎计算平台负责人张复兴发表了题为“知乎容器平台演进与大数据实践融合”的精彩演讲。本文将讨论以下三个部分:知乎容器平台的演进容器平台维护的陷阱容器与大数据的融合实践知晓容器平台的演进知乎容器平台的演进大致可以分为三个parts阶段:2015年9月,我们的容器平台正式上线生产环境。到2016年5月,我们已将90%的业务迁移到容器平台。如今,除了业务容器,包括HBase、Kafka在内的很多基础组件都已经迁移到了容器平台。节点总数达到2000多个,容器数量达到30000多个。可以说知乎基本上是在Allin容器平台上。在容器平台的整体演进过程中,我们总结了五个关键点:从Mesos到Kubernetes的技术选择变化。从单集群到多集群混合云的架构调整。从滚动部署到将部署与发布分开的使用优化。在容器的使用上,从无状态到有状态,引入了持久化存储。在容器网络上,从NAT切换到UnderlayIP模式。从Mesos到Kubernetes早在2015年,我们就开始在生产环境中使用容器平台。由于当时Kubernetes刚刚发布,还不成熟,我们当时选择了Mesos技术方案。Mesos的优点如下:非常稳定。在架构设计上,由于大部分状态都是由MesosSlave向Master汇报的,所以Master的负载比较轻。单个集群可以容纳的容器规模比较大(官方:单个集群可以容纳5000个节点)。Mesos的缺点:由于是单独开发一个框架,所以开发成本比较高。我们首先采用了自研的Framework。Kubernetes的优势如下:强大的社区支持。功能比较齐全,包含了Pods、Deployment等概念。由于功能齐全,接入和使用成本低。Kubernetes的缺点:由于将所有的状态都存储在Etcd中,所以单个集群的规模没有Mesos大。官方表示:Etcd升级到V3版本后,可以达到5000个节点。在开始运行的时候,我们使用了一些简单的无状态容器。后来随着Proxy、Kafka等基础组件的引入,每套组件开发一个框架的接入成本太高。所以在后续的实践过程中,我们直接采用了Kubernetes,通过资源调度层统一资源调度和管理。在从单集群到混合云的生产环境中,我们有如下实际需求:对于Mesos或Kubernetes的任何参数变更,都需要先在灰度集群上进行验证。验证通过后,才能部署到大规模生产环境中。因此,我们需要有一个线上线下相似的环境,唯一不同的是测试集群的规模比实际运行的集群略小。对于Kubernetes的单集群来说,容量是有限的,所以需要实现多集群的方案来横向扩展容量。容忍单集群级别的故障。需要混合云架构。由于公有云的集群池比较大,不仅可以大大增加弹性资源池的容量,还可以抵抗突如其来的扩容需求。计费方式更加灵活,可以按需计费,“便宜”地实现一些临时活动对计算资源的消耗。在实现混合云架构的过程中,我们调研了KubernetesFederation方案,发现其存在以下两个不足:由于不成熟,目前官方不推荐在生产环境中运行。组件太多,部署和管理繁琐。因此,我们采用了自研的管理方案,具有以下特点:每组业务容器会同时在多个集群上创建一个Deployment。这些Deployment的配置,包括容器版本和CPU/内存资源配额,都是完全相同的。唯一不同的是容器的数量,会根据不同的集群大小做相应的调整。从滚动部署到部署发布分离我们优化了部署发布流程,从原来的滚动部署模式转变为部署发布分离模式。这里先区分一下部署和发布这两个概念:部署是分布式代码的配置,启动WebService进程等服务实例。发布是指将一组服务实例注册到负载均衡或其他流量分发系统中,使其可以对外接收流量。如上图底部流程所示,上线的基本流程是:内网流量测试→金丝雀流量测试→全量生产环境。那么在每个阶段,我们都需要观察线上业务的指标。一旦出现异常,我们需要及时回滚。让我们仔细看看滚动部署的优缺点。优点如下:每次升级一部分容器实例,然后迭代运行。保证升级过程中服务不中断,瞬间产生的最大资源消耗有限。缺点是:在滚动部署的过程中,不能做到:先部署10%→停止→部署20%→再停止→再部署50%,无法灵活控制进度,无法对应之前的提到的各种发布阶段。如果每个发布阶段都采用独立的滚动部署方式,整体部署速度会比较慢。在滚动部署期间,旧的容器实例会立即被销毁。一旦此时线上指标出现问题,但我们的观察滞后,涉及到的“销毁新实例,启动旧实例”的回滚速度会比较慢。针对以上问题,我们在设计上采用了“部署与发布分离”的模式。比如:我们上线的时候,首先在后台启动一组新的业务容器实例。当容器实例达到并满足内网公布的数量要求时,我们在内网注册,然后旧实例从内网分流系统“反注册”。这样就可以在内网发布和验证新的实例。在后台继续启动新的容器实例的同时,也让用户无法感知到容器启动实例的时间。我们意识到,当内部用户验证通过后,下一阶段的部署可以在几秒内直接升级。另外,因为我们旧的容器实例并没有立即销毁,而是在生产环境安全发布一段时间后,按照类似金丝雀的策略,将旧的容器组彻底销毁。因此,在金丝雀发布过程中,如果线上出现问题,我们可以立即重新注册旧实例,实现秒级回滚。从无状态到有状态在容器使用模式上,我们最初部署了一些无状态的业务web容器。但是随着其他基础组件迁移到容器平台,我们引入了持久化存储来提供服务支持。因此,在生产环境中,我们采用以下几种典型的持久化存储方式:HostPath。它主要与DaemonSet一起使用。因为HostPath本身可以保证每个Node上只启动一个Pod实例,不会出现多个Pod同时使用同一个HostPath导致路径冲突的问题。比如我们使用DaemonSet部署ConsulAgent,由于ConsulService在注册的时候需要持久化到本地存储目录,所以很适合这样实现。当地的。最新版本的Kubernetes已经支持LocalVolume。优点是:因为使用本地磁盘,所以比网络存储有更高的IOPS和更低的延迟。因此非常适合MySQL、Kafka等高IOPS、低延迟的存储类应用。网络文件系统。我们将分布式文件系统的Fuse接口映射到容器中,保证业务可以从分布式文件系统中读取各种数据文件。从NAT到UnderlayIP在容器网络模式中,我们首先采用了iptables实现的NAT模式。这种方式实施起来比较方便,不需要对现有网络进行调整。但是它有一定的性能损失问题,对于我们早期的业务容器来说是可以接受的。后期,当我们需要把一些大流量的高速网络应用,比如Ngix、Haproxy,放到容器中时,就不能容忍这种性能开销了。所以,根据我们机房的网络实际,我们在Kubernetes方案中选择了UnderlayIP,一种简单互联的网络模式,保证容器的IP和所在物理机的IP完全等价位于。由于没有overlay封装/解包处理,所以性能上几乎没有损失。通过实际测试,我们觉得性能很好,所以我们把各种大流量的分发应用放到这个容器里。此外,由于IP模式具有良好的业务对应性,我们可以很容易地定位网络连接的来源和故障。例如:如果在MySQL端发现大量的连接,你将如何定位这些连接的来源?按照原来的端口映射方式,你可能只能定位到某台机器的源头,但往往是一台机器上部署了多个业务应用,所以无法准确定位是哪个业务方造成的。现在采用IP方式后,可以很方便的根据IP地址判断对应的是哪个服务容器。同时,在具体的实践过程中,我们也给每台机器分配一个固定的网段(比如C类网段),然后使用Kubelet的CNI插件,即APAM,负责IP每台机器的地址。创建、分配和取消分配。容器平台维护的坑由于我们在生产环境中大规模使用容器平台,可见容器平台已经成为我们基础组件的基础组件。那么一旦它出现问题,就会对我们的生产环境造成重大的故障。给大家分享一下我们在生产环境踩过的一些坑。半夜K8SEvents变了,突然我们所有的APIServer都访问不了了。通过排查,我们发现原因出在K8SEvents上。众所周知,K8SEvents和Log一样,记录了K8S集群中发生的任何变化事件。如果不额外配置,它会按照默认的方式,把这些变化都记录到线上同一个Etcd中。同时,K8S为这些Event配置相应的过期策略TTL,保证Event在一段时间后自动回收,从而释放Etcd的存储空间。这样的设计看似合理,但是Etcd在实现TTL的时候,采用了遍历的方式,所以实现效率比较低。随着集群规模的逐渐增大,集群上会频繁的发布、部署和变更,这会逐渐增加Etcd的负载,直到最终Etcd无法选出Leader,整个K8S集群崩溃。针对上面的意外,K8S也意识到了,它为我们提供了一个配置,可以将Events记录到一个单独的Etcd集群中。但是对于这个单独的Etcd集群,我们不需要高可用存储,所以我们直接使用单节点的Etcd,而不是使用Raft的方式来搭建一个性能更好的集群。另一方面,我们意识到K8S给出的“每三小时回收一次Events”等TTL机制过于精确。因此,我们自己实现了一个过期清理策略,即每晚在低峰时清理整个Etcd集群的Events。K8SEviction除了上面提到的“整个K8S集群因为APIserver没有响应而失控”的失败之外,我们还遇到过“生产环境所有Pod都被直接删除”的坑。在所有API服务器都“宕机”的极端情况下,如果没有及时处理,超过了一段时间(比如五分钟),那么在K8S1.5之前的版本中,ControllerManager会认为这些集群的Nodes都是Contacthasbeenlost,Eviction已经开始。即:终止这些不健康节点上的所有Pod。此时,如果恢复APIServer,所有Kubelets都会根据命令删除所有运行在其上的Pod。当然,在K8S1.5之后的正式版本中,增加了一个“-unhealthy-zone-threshold”的配置。例如:一旦发现超过30%的Node处于断开状态,它就会认为一定有其他原因造成大规模故障,因此会禁用不再执行该节点的aggressiveEviction策略控制器管理器。Docker容器端口泄露另外,我们还发现生产环境中存在“portisalreadyallocated(端口已被使用)”的现象。检查后发现,容器虽然释放了端口,但是它的Proxy还在占用端口。通过进一步查看DockerDaemon的代码,我们了解到,从分配一个端口到DockerDaemon将该端口记录到自己内部的持久化存储中的过程并不是“原子的”。如果DockerDaemon中途退出,在重启恢复的内部存储过程中会忽略分配的端口,导致容器端口泄露。针对这个问题,我们已经向官方提交了一个Issue以及相应的解决方案。TCPConnectionReset我们也遇到了DockerNAT网络模式下TCPConnectionReset的问题。如上图所示,对于网络包可能出现的乱序,系统默认的配置过于敏感和严格。当我们的系统访问公网时,如果网络环境较差,出现超出TCP窗口的乱序包,系统会根据配置直接重置这些连接。因此,我们可以直接将其关闭来解决这个问题。下面介绍我们在容器技术与大数据应用融合方面的一些尝试和实践。容器与大数据集成实践基于Kubernetes的大数据集成在大数据场景下,我们通过两条处理路径实现容器平台与大数据组件的集成。如上图所示,左边绿色的是实时处理,右边灰色的是批处理。由于出发点不同,这些组件的设计思路也大不相同:批处理主要是运行ETL任务,包括数据仓库构建、离线分析等,因此追求数据吞吐量和资源利用率,而对于Latency本身则是不敏感。比如:一个Map-Reduce任务需要运行1到2个小时,这是很正常的。它被设计为具有高度容错性。例如:如果一个Map-Reduce任务“挂了”,你完全可以通过上层的Ozzie或Azkaban重试整个任务(作业)。只要最后完成重启,这些对上层业务就“不敏感”了。实时处理对延迟敏感,对组件的可用性要求高。一旦任何一个节点“挂掉”或重启,都会导致数据(运行指标)“落地”延迟,数据显示失败。因此其部件要求机器负载不能过高。当然,在维护大数据生产环境的过程中,我们经常会遇到以下问题:由于业务发生变化,Kafka写入的流量会急剧增加,这也会增加整个Kafka集群的负载。那么如果无法恢复,就会导致集群瘫痪,进而影响到整个生产环境。我们的治理思路是按照业务方对集群进行划分和隔离。我们按照业务方把集群分成了几十套,那么面对这么多集群带来的成本,如何进行统一的配置和部署管理呢?通过使用K8S模板,我们可以方便的一键搭建多个相同的运行环境。由于各个业务端的使用情况不同,那些业务使用量小的应用也会需要分配多台机器,需要维护集群的高可用,造成大量的资源浪费。我们的解决方案是:使用容器来实现细粒度的资源分配和配置。例如:对于这些较小的业务,我们只分配一个单CPU、单磁盘、8G内存的容器,而不是整台物理机。在基于Kubernetes的Kafka集群平台中,Kafka的性能瓶颈主要存在于磁盘存储的IOPS上。我们通过以下合理的设计实现了资源的分配和管理。具体解决方案是:以单个磁盘为资源单元进行细粒度分配,即使用单个Broker调度物理磁盘。以这种方式划分资源的好处是它在本质上隔离了IOPS和磁盘容量。对于几TB的硬盘,资源划分的粒度更细,不像物理机动辄几十TB。因此,资源的利用率会提高,更适合小型应用。我们使用Kafka自带的Replica来实现数据的高可用。但是容器和物理机在具体实现策略上是有区别的:以前我们是把Broker部署在一台机器上,现在我们把Broker部署在一个容器里。此时容器就变成了原来的物理机,包含容器的物理机就相当于原来物理机所在的物理机架。同时我们也调整了控制Replica副本的分配策略。我们将broker的rackawareness改为机器处理,避免出现同topic的replica和broker放在同一台机器上的情况。采用容器方式后,故障处理变得相对简单。由于采用单硬盘模式,一旦任意一块硬盘发生故障,运维人员只需要更换故障磁盘,通过Kafka的Replication方案从其他地方复制数据即可,不需要其他部门人员的干预。在创建过程上,由于当时K8S不支持LocalPV,我们采用自定义的第三方资源接口,实现了类似于今天生产环境中LocalVolume的机制。过程是:我们根据磁盘资源静态创建LocalPV,在平台创建Kafka集群时动态创建LocalPVC。此时,调度器可以根据自己的LocalPVC和已有的LocalPV资源创建一个RC,然后在对应的节点上启动Broker。当然,K8S已经有类似的实现方法,大家可以直接使用。容器与HBase融合的另一个实际案例是将HBase平台放在容器中。具体要求如下:根据业务侧隔离HBase集群。由于HBase的读写都发生在RegionServer节点上,所以需要对RegionServer进行限制。由于大小业务方的差异,我们需要优化资源利用。同时,由于数据存储在HDFS中,然后加载到内存中,我们可以通过Cache在内存中进行高性能的读写操作。可见RegionServer的性能瓶颈取决于CPU和内存的开销。因此,我们将各个HBaseCluster放在KubernetesNamespace中,然后使用Deployment将HBaseMaster和RegionServer部署到容器中。其中Master比较简单,只需要通过Replication实现高可用即可;而RegionServer对大小业务方都有资源限制。比如:我们给大业务端分配了8核+64G内存,给小业务端只分配了2核+16G内存。另外由于ZooKeeper和HDFS的负载小,如果直接放在容器中,会涉及到持久化存储等复杂的问题。因此,我们让所有的HBase集群共享同一个ZooKeeper和HDFS集群,以减少手动维护ZooKeeper和HDFS集群的开销。容器与SparkStreaming的结合不同于那些需要大量读写HDFS磁盘的Map-Reduce任务。SparkStreaming是一项永久性任务。在调度方面,不需要优化网络传输中处理大数据的开销,也不需要对HDFS数据进行Locality。但由于用于实时处理计算,对机器负载比较敏感。如果机器负载过高,会影响其处理的“落地”延迟。大数据处理集群本身的特点是追求高吞吐量,所以我们需要将SparkStreaming从大数据处理集群中隔离出来,然后放到在线业务容器中。实践中,由于当时还没有Spark2.3,我们就自己把YARN集群放到容器中。即:首先在Docker中启动YARN的NodeManager,并注册到ResourceManager上,形成运行在容器中的YARN集群。然后我们通过SparkSubmit向集群提交一个SparkStreaming任务,这样SparkStreaming的Executor就可以运行在容器中了。如今,Spark2.3之后的版本可以支持原生使用Kubernetes调度器。不再需要使用YARN,直接通过Kubernetes让Executor运行在容器中即可。大数据平台的DevOps管理我们在大数据平台的管理上践行DevOps的思想。例如:我们自己开发了一个PaaS平台,方便业务方直接在平台上申请、创建、使用、扩展和管理资源。按照DevOps的思路,我们将自己定位为工具开发的平台端,而不是日常运营的运维端。我们都是通过PaaS平台的交付,让业务方自己创建、重启、扩容集群。此举的好处是:降低了沟通成本,尤其是在公司规模越来越大,业务方之间的沟通越来越复杂的情况下。业务方便,有保障。他们的任何扩展需求都不需要联系我们,可以在平台上独立完成。通过减少日常工作的负担,我们可以更专注于技术本身,以及如何把平台的底层技术做的更好。由于业务本身对Kafka、HBase等系统的了解很肤浅,我们需要从专家的角度向他们展示我们积累的对集群的理解和经验。作为DevOps实践的一部分,我们在这个大数据平台上提供了丰富的监控指标。图中以Kafka为例。我们提供的监控指标包括TopicLevel、BrokerLevel、HostLevel。可以看出,我们的目标是将Kafka集群变成一个“白盒子”。一旦出现故障,业务方可以直接通过我们自定义的告警阈值,在指标界面上清晰的看到各种异常,及时处理。总结从实践经验来看,我们的基本思路是:业务侧集群隔离。利用K8S进行多集群部署和管理。利用Docker进行资源隔离和监控。利用Docker进行更细粒度的资源分配。使用DevOps实现运维管理。未来我们会尝试更多的基础设施组件,使用K8S实现集群隔离,实现更细粒度的资源分配和进程级的资源监控。通过在生产环境更好的实施管理和维护,提高资源的利用率,为业务提供更稳定的PaaS平台服务,最终实现数据中心的资源统一。同时,我们实现了DCOS(DataCenterOperatingSystem),交给K8S进行调度管理。知乎计算平台负责人张复兴。2012年从中国科学院计算技术研究所毕业后,分别在搜狐和雅虎从事分布式存储系统研发和云平台建设工作。加入知乎后,他从无到有推动了知乎内部容器平台的建设。目前主要研究方向为资源调度管理、大数据计算与存储等。【原创稿件,合作网站转载请注明原作者及出处.com】
