基于Mesos/Docker构建数据处理平台
JeaWagstaff
7年前
<p>本文深入介绍了去哪儿网利用Mesos和Docker构建私有云服务的全过程,分享了从无状态应用向有状态应用逐步过度的经验与心得。</p> <h2>平台概览</h2> <p>2014年下半年左右,去哪儿完成了有关构建私有云服务的技术调研,并最终拍定了Docker/Mesos这一方案。下图1展示了去哪儿数据平台的整体架构:</p> <p><img src="https://simg.open-open.com/show/8b4f100c028880464b30db7de6137a00.png"> 图1:去哪儿数据平台的整体架构</p> <p>该平台目前已实现了如下多项功能:</p> <ul> <li>每天处理约340亿/25TB的数据;</li> <li>90%的数据在100ms内完成处理;</li> <li>最长3h/24h的数据回放;</li> <li>私有的Elasticsearch Cloud;</li> <li>自动化监控与报警。</li> </ul> <h2>为什么选择Docker/Mesos</h2> <p>目前为止,这个数据平台可以说是公司整个流数据的主要出入口,包括私有的Elasticsearch Cloud和监控报警之类的数据。那么为什么选择Docker/Mesos?</p> <p>选择Docker有两大原因。第一个是打包:对于运维来讲,业务打完包之后,每天面对的是用脚本分发到机器上时所出现的各种问题。业务包是一个比较上层的话题,这里不做深入的讨论,这里讲的“打包”指软件的Runtime层。如果用Docker的打包机制,把最容易出现问题的Runtime包装成镜像并放在registry里,需要的时候拿出来,那么整个平台最多只执行一个远程脚本就可以了,这是团队最看好的一个特性。第二个是运维:Docker取消了依赖限制,只要构建一个虚拟环境或一个Runtime的镜像,就可以直接拉取到服务器上并启动相应的程序。此外Docker在清理上也较为简单,不需要考虑环境卸载不干净等问题。</p> <p>以常见的计算框架来说,它们本质上仍然属于运行在其上的Job的Runtime。综合上述情况,团队选择针对Runtime去打包。</p> <p>选择Mesos是因为它足够简单和稳定,而且拥有较成熟的调度框架。Mesos的简单体现在,与Kubernetes相比其所有功能都处于劣势,甚至会发现它本身都是不支持服务的,用户需要进行二次开发来满足实际要求,包括网络层。不过,这也恰好是它的强项。Mesos本身提供了很多SDN接口,或者是有模块加载机制,可以做自定义修改,平台定制功能比较强。所以用Mesos的方案,需要考虑团队是否可以Hold住整个开发过程。</p> <p>从框架层面来看,Marathon可以支撑一部分长期运行的服务,Chronos则侧重于定时任务/批处理。</p> <p>以下图2是Mesos的一个简单结构图:</p> <p><img src="https://simg.open-open.com/show/5e77bb602fc2ceac13354ee786c292b7.png"> 图2:Mesos结构</p> <p>数据平台的最终目标架构如下图3所示:</p> <p><img src="https://simg.open-open.com/show/e96688735b4be6d8587f6ac19a7f636c.png"> 图3:平台目标</p> <h2>组件容器化与部署</h2> <p>组件的容器化分为JVM容器化和Mesos容器化。JVM容器化需要注意以下几方面:</p> <p>潜在创建文件的配置都要注意</p> <ul> <li>java.io.tmpdir</li> <li>-XX:HeapDumpPath</li> <li>-Xloggc</li> </ul> <p>-Xloggc会记录GC的信息到制定的文件中。现在很少有直接用XLoggc配置的了(已经用MXBean方式替代了)。如果有比较老的程序是通过-Xloggc打印GC日志的话,那么要额外挂载volume到容器内。</p> <p>时区与编码</p> <ul> <li>–env TZ=Asia/Shanghai</li> <li>–volume /etc/localtime:/etc/localtime:ro</li> <li>–env JAVA_TOOL_OPTIONS=”-Dfile.encoding=UTF-8 -Duser.timezone=PRC</li> </ul> <p>时区是另一个注意点。上面所列的三种不同的方法都可以达到目的,其中第一/三个可以写在Dockerfile里,也可以在docker run时通过–env传入。第二种只在docker run时通过volume方式挂载。另外,第三种额外设置了字符集编码,推荐使用此方式。</p> <p>主动设置heap</p> <ul> <li>防止ergonomics乱算内存</li> </ul> <p>这是Docker内部实现的问题。即使给Docker设置内存,容器内通过free命令看到的内存和宿主机的内存是一样的。而JVM为了使用方便,会默认设置一个人机功能会根据当前机器的内存计算一个堆大小,如果我们不主动设置JVM堆内存的话,很有可能计算出一个超过 Memory Cgroup限制的内存,启动就宕掉,所以需要注意在启动时就把内存设置好。</p> <p>CMS收集器要调整并行度</p> <ul> <li>-XX:ParallelGCThreads=cpus</li> <li>-XX:ConcGCThreads=cpus/2</li> </ul> <p>CMS是常见的收集器,它设置并行度的时候是取机器的核数来计算的。如果给容器分配2个CPU,JVM仍然按照宿主机的核数初始化这些线程数量,GC的回收效率会降低。想规避这个问题有两点,第一点是挂载假的Proc文件系统,比如Lxcfs。第二种是使用类似Hyper的基于Hypervisor的容器。</p> <p>Mesos容器化要求关注两类参数:配置参数和run参数。</p> <ul> <li> <p>需要关注的配置参数</p> <ul> <li>MESOS_systemd_enable_support</li> <li>MESOS_docker_mesos_image</li> <li>MESOS_docker_socket</li> <li>GLOG_max_log_size</li> <li>GLOG_stop_logging_if_full_disk</li> </ul> </li> </ul> <p>Mesos是配置参数最多的。在物理机上,Mesos默认使用系统的Systemd管理任务,如果把Mesos通过Docker run的方式启动起来,用户就要关systemd_Enable_support,防止Mesos Slave拉取容器运行时数据造成混乱。</p> <p>第二个是Docker_Mesos_Image,这个配置告诉Mesos Slave,当前是运行在容器内的。在物理机环境下,Mesos Slave进程宕掉重启,、就会根据executor进程/容器的名字做recovery动作。但是在容器内,宕机后executor全部回收了,重启容器,Slave认为是一个新环境,跳过覆盖动作并自动下发任务,所以任务有可能会发重。</p> <p>Docker_Socket会告诉Mesos,Docker指定的远端地址或本地文件,是默认挂到Mesos容器里的。用户如果直接执行文件,会导致文件错误,消息调取失败。这个时候推荐一个简单的办法:把当前物理机的目录挂到容器中并单独命名,相当于在容器内直接访问整个物理机的路径,再重新指定它的地址,这样每次一有变动Mesos就能够发现,做自己的指令。</p> <p>后面两个是Mesos Logging配置,调整生成logging文件的一些行为。</p> <ul> <li> <p>需要关注的run参数</p> <ul> <li>–pid=host</li> <li>–privileged</li> <li>–net=host (optional)</li> <li>root user</li> </ul> </li> </ul> <p>启动Slave容器的时候最好不加Pid Namespace,因为容器内Pid=1的进程一般都是你的应用程序,易导致子进程都无法回收,或者采用tini一类的进程启动应用达到相同的目的。–privileged和root user主要是针对Mesos的持久化卷功能,否则无法mount到容器内,–net=host是出于网络效率的考虑,毕竟源生的bridge模式效率比较低。</p> <p><img src="https://simg.open-open.com/show/e02343ac062ca5711f7c53db0303e9c5.png"> 图4:去哪儿数据平台部署流程图</p> <p>上图4就是去哪儿数据平台部署的流程图。</p> <h2>基于Marathon的Streaming调度</h2> <p>拿Spark on Mesos记录子,即使是基于Spark的Marathon调度,也需要用户开发一个Frameworks。上生产需要很多代码,团队之前代码加到将近一千,用来专门解决Spark运行在Master中的问题,但是其中一个软件经常跑到Master,对每一个框架写重复性代码,而且内部逻辑很难复用,所以团队考虑把上层的东西全都跑在一个统一框架里,例如后面的运维和扩容,都针对这一个框架做就可以了。团队最终选择了Marathon,把Spark作为Marathon的一个任务发下去,让Spark在Marathon里做分发。</p> <p>除去提供维标准化和自动化外,基于Spark的Marathon还可以解决Mesos-Dispatcher的一些问题:</p> <ul> <li>配置不能正确同步;这一块更新频率特别慢,默认速度也很慢,所以需要自己来维护一个版本。第一个配置不能正确同步,需要设置一些参数信息、Spark内核核数及内损之类,这里它只会选择性地抽取部分配置发下去。</li> <li>基于attributes的过滤功能缺失;对于现在的环境,所设置的Attributes过滤功能明显缺失,不管机器是否专用或有没有特殊配置,上来就发,很容易占满ES的机器。</li> <li>按role/principal接入Mesos;针对不同的业务线做资源配比时,无法对应不同的角色去接入Mesos。</li> <li>不能re-registery;框架本身不能重注册,如果框架跑到一半挂掉了,重启之后之前的任务就直接忽略不管,需要手工Kill掉这个框架。</li> <li>不能动态扩容executor。最后是不能扩容、动态调整,临时改动的话只能重发任务。</li> </ul> <p>整个过程比较简单,如下图5所示:</p> <p><img src="https://simg.open-open.com/show/47dd983945bc99580369f113d5cb5191.png"> 图5:替代Spark Mesos Dispatcher</p> <p>不过还是有一些问题存在:</p> <p>Checkpoint & Block</p> <ul> <li>动态预留 & 持久化卷</li> <li>setJars</li> <li>清理无效的卷</li> </ul> <p>关于Checkpoint&Block,通过动态预留的功能可以把这个任务直接“钉死”在这台机器上,如果它挂的话可以直接在原机器上重启,并挂载volume继续工作。如果不用它预留的话,可能调度到其他机器上,找不到数据Block,造成数据的丢失或者重复处理。</p> <p>持久化卷是Mesos提供的功能,需要考虑它的数据永存,Mesos提供了一种方案:把本地磁盘升级成一个目录,把这个转移到Docker里。每次写数据到本地时,能直接通过持久化卷来维护,免去手工维护的成本。但它目前有一个问题,如果任务已被回收,它持久化卷的数据是不会自己删掉的,需要写一个脚本定时轮巡并对应删掉。</p> <p>临时文件</p> <ul> <li>java.io.tmpdir=/mnt/mesos/sandbox</li> <li>spark.local.dir=/mnt/mesos/sandbox</li> </ul> <p>如果使用持久化卷,需要修改这两个配置,把这一些临时文件写进去,比如shuffle文件等。如果配置持久化卷的话,用户也可以写持久化卷的路径。</p> <p>Coarse-Grained</p> <p>Spark有两种资源调度模式:细粒度和粗粒度。目前已经不太推荐细粒度了,考虑到细粒度会尽可能的把所有资源占满,容易导致Mesos资源被耗尽,所以这个时候更倾向选择粗粒度模式。</p> <p><img src="https://simg.open-open.com/show/842077fd6a15afa99b1f523452f3ba9c.png"> 图6:Storm on Marathon</p> <p>上图6展示了基于Storm的Marathon调度,Flink也是如此。结合线上的运维和debug,需要注意以下几方面:</p> <p>源生Web Console</p> <ul> <li>随机端口</li> <li>openresty配合泛域名</li> </ul> <p>默认源生Web Console,前端配置转发,直接访问固定域名。</p> <p>Filebeat + Kafka + ELK</p> <ul> <li>多版本追溯</li> <li>日常排错</li> <li>异常监控</li> </ul> <p>大部分WebUI上看到的都是目前内部的数据处理情况,可以通过ELK查询信息。如果任务曾经运行在不同版本的Spark上,可以把多版本的日志都追踪起来,包括日常、问题监控等,直接拿来使用。</p> <p>Metrics</p> <p>第三个需要注意的就是指标。比如Spark ,需要配合Metrics 把数据源打出来就行。</p> <h2>ELK on Mesos</h2> <p>目前平台已有近50个集群,约100TB+业务数据量,高峰期1.2k QPS以及约110个节点,Elasticsearch需求逐步增多。</p> <p><img src="https://simg.open-open.com/show/032672e889fff272186cd02bbe543fb6.png"> 图7:ELK on Mesos</p> <p>上图7是ELK on Mesos结构图,也是团队的无奈之选。因为Mesos还暂时不支持multi-role framework功能,所以选择了这种折中的方式来做。在一个Marathon里,根据业务线设置好Quota后,用业务线重新发一个新的Marathon接入进去。对于多租户来讲,可以利用Kubernetes做后续的资源管控和资源申请。</p> <p>部署ES以后,有一个关于服务发现的问题,可以去注册一个callback,Marathon会返回信息,解析出master/slave进程所在的机器和端口,配合修改Haproxy做一层转发,相当于把后端整个TCP的连接都做一个通路。ES跟Spark不完全相同,Spark传输本身流量就比较大,而ES启动时需要主动联系Master地址,再通过Master获取相应集群,后面再做P2P,流量比较低,也不是一个长链接。</p> <h2>监控与运维</h2> <p>这部分包括了Streaming监控指标与报警、容器监控指标与报警两方面。</p> <p>Streaming监控指标与报警</p> <p>Streaming监控含拓扑监控和业务监控两部分。</p> <ul> <li>Streaming拓扑监控</li> <li>业务监控 <ul> <li>Kafka Topic Lag</li> <li>处理延迟mean90/upper90</li> <li>Spark scheduler delay/process delay</li> <li>Search Count/Message Count</li> <li>Reject/Exception</li> <li>JVM</li> </ul> </li> </ul> <p>拓扑监控包括数据源和整个拓扑流程,需要用户自己去整理和构建,更新的时候就能够知道这个东西依赖谁、是否依赖线上服务,如果中途停的话会造成机器故障。业务监控的话,第一个就是Topic Lag,Topic Lag每一个波动都是不一样的,用这种方式监控会频繁报警,90%的中位数都是落在80—100毫秒范围内,就可以监控到整个范围。</p> <p>容器监控指标与报警</p> <p>容器监控上关注以下三方面:</p> <ul> <li>Google cAdvisor足够有效 <ul> <li>mount rootfs可能导致容器删除失败 #771</li> <li>–docker_only</li> <li>–docker_env_metadata_whitelist</li> </ul> </li> <li>Statsd + Watcher <ul> <li>基于Graphite的千万级指标监控平台</li> </ul> </li> <li>Nagios</li> </ul> <p>容器这一块比较简单,利用Docker并配合Mesos,再把Marathon的ID抓取出来就可以了。我们这边在实践的过程发现一个问题,因为Statsd Watcher容易出现问题,你直接用Docker的时候它会报一些错误出来,这个问题就是Statsd Watcher把路径给挂了的原因。目前我们平台就曾遇到过一次,社区里面也有人曝,不过复现率比较低。用的时候如果发现这个问题把Statsd Watcher直接停掉就好。指标的话,每台机器上放一个statsd再发一个后台的Worker,报警平台也是这个。</p> <p>其实针对Docker监控的话,还是存在着一些问题:</p> <ul> <li>基础监控压力 <ul> <li>数据膨胀</li> <li>垃圾指标增多</li> <li>大量的通配符导致数据库压力较高</li> </ul> </li> <li>单个任务的容器生命周期 <ul> <li>发布</li> <li>扩容</li> <li>异常退出</li> </ul> </li> </ul> <p>首先主要是监控系统压力比较大。原来监控虚拟机时都是针对每一个虚拟机的,只要虚拟机不删的话是长期汇报,指标名固定,但在容器中这个东西一直在变,它在这套体系下用指标并在本地之外建一个目录存文件,所以在这种存储机制下去存容器的指标不合适。主要问题是数据膨胀比较厉害,可能一个容器会起名,起名多次之后,在Graphite那边对应了有十多个指标,像这种都是预生成的监控文件。比如说定义每一秒钟一个数据点,要保存一年,这个时候它就会根据每年有多少秒生成一个RRD文件放那儿。这部分指标如果按照现有标准的话,可能容器的生命周期仅有几天时间,不适用这种机制。测试相同的指标量,公司存储的方式相对来说比Graphite好一点。因为Graphite是基于文件系统来做的,第一个优化指标名,目录要转存到数据库里做一些索引加速和查询,但是因为容器这边相对通配符比较多,不能直接得知具体对应的ID,只能通配符查询做聚合。因为长期的通配符在字符串的索引上还是易于使用的,所以现在算是折中的做法,把一些常用的查询结果、目录放到里边。</p> <p>另一个是容器的生命周期。可以做一些审计或者变更的版本,在Mesos层面基于Marathon去监控,发现这些状态后打上标记:当前是哪一个容器或者哪一个TASK出了问题,对应扩容和记录下来。还有Docker自己的问题,这样后面做整个记录时会有一份相对比较完整的TASK-ID。</p> <p>作者简介:徐磊,去哪儿网平台事业部运维开发工程师,2015年加入去哪儿网,负责实时日志相关的开发与运维工作。有多年电信、云计算行业经验,曾供职于红帽中国。</p> <p> </p> <p>来自:http://geek.csdn.net/news/detail/235946</p> <p> </p>