若谷学院
互联网公司技术架构分享

HDFS NameNode重启优化

美团点评阅读(326)

本文已发表于InfoQ,下面的版本又经过少量修订。

一、背景

在Hadoop集群整个生命周期里,由于调整参数、Patch、升级等多种场景需要频繁操作NameNode重启,不论采用何种架构,重启期间集群整体存在可用性和可靠性的风险,所以优化NameNode重启非常关键。

本文基于Hadoop-2.xHA with QJM社区架构和系统设计(如图1所示),通过梳理NameNode重启流程,并在此基础上,阐述对NameNode重启优化实践。

二、NameNode重启流程

在HDFS的整个运行期里,所有元数据均在NameNode的内存集中管理,但是由于内存易失特性,一旦出现进程退出、宕机等异常情况,所有元数据都会丢失,给整个系统的数据安全会造成不可恢复的灾难。为了更好的容错能力,NameNode会周期进行CheckPoint,将其中的一部分元数据(文件系统的目录树Namespace)刷到持久化设备上,即二进制文件FSImage,这样的话即使NameNode出现异常也能从持久化设备上恢复元数据,保证了数据的安全可靠。

但是仅周期进行CheckPoint仍然无法保证所有数据的可靠,如前次CheckPoint之后写入的数据依然存在丢失的问题,所以将两次CheckPoint之间对Namespace写操作实时写入EditLog文件,通过这种方式可以保证HDFS元数据的绝对安全可靠。

事实上,除Namespace外,NameNode还管理非常重要的元数据BlocksMap,描述数据块Block与DataNode节点之间的对应关系。NameNode并没有对这部分元数据同样操作持久化,原因是每个DataNode已经持有属于自己管理的Block集合,将所有DataNode的Block集合汇总后即可构造出完整BlocksMap。

HA with QJM架构下,NameNode的整个重启过程中始终以SBN(StandbyNameNode)角色完成。与前述流程对应,启动过程分以下几个阶段:

  1. 加载FSImage;
  2. 回放EditLog;
  3. 执行CheckPoint(非必须步骤,结合实际情况和参数确定,后续详述);
  4. 收集所有DataNode的注册和数据块汇报。

默认情况下,NameNode会保存两个FSImage文件,与此对应,也会保存对应两次CheckPoint之后的所有EditLog文件。一般来说,NameNode重启后,通过对FSImage文件名称判断,选择加载最新的FSImage文件及回放该CheckPoint之后生成的所有EditLog,完成后根据加载的EditLog中操作条目数及距上次CheckPoint时间间隔(后续详述)确定是否需要执行CheckPoint,之后进入等待所有DataNode注册和元数据汇报阶段,当这部分数据收集完成后,NameNode的重启流程结束。

从线上NameNode历次重启时间数据看,各阶段耗时占比基本接近如图2所示。

经过优化,在元数据总量540M(目录树240M,数据块300M),超过4K规模的集群上重启NameNode总时间~35min,其中加载FSImage耗时~15min,秒级回放EditLog,数据块汇报耗时~20min,基本能够满足生产环境的需求。

2.1 加载FSImage

如前述,FSImage文件记录了HDFS整个目录树Namespace相关的元数据。从Hadoop-2.4.0起,FSImage开始采用Google Protobuf编码格式描述(HDFS-5698),详细描述文件见fsimage.proto。根据描述文件和实现逻辑,FSImage文件格式如图3所示。

从fsimage.proto和FSImage文件存储格式容易看到,除了必要的文件头部校验(MAGIC)和尾部文件索引(FILESUMMARY)外,主要包含以下核心数据:

  1. NS_INFO(NameSystemSection):记录HDFS文件系统的全局信息,包括NameSystem的ID,当前已经分配出去的最大Block ID以及Transaction ID等信息;
  2. INODE(INodeSection):整个目录树所有节点数据,包括INodeFile/INodeDirectory/INodeSymlink等所有类型节点的属性数据,其中记录了如节点ID,节点名称,访问权限,创建和访问时间等等信息;
  3. INODE_DIR(INodeDirectorySection):整个目录树中所有节点之间的父子关系,配合INODE可构建完整的目录树;
  4. FILES_UNDERCONSTRUCTION(FilesUnderConstructionSection):尚未完成写入的文件集合,主要为重启时重建Lease集合;
  5. SNAPSHOT(SnapshotSection):记录Snapshot数据,快照是Hadoop 2.1.0引入的新特性,用于数据备份、回滚,以防止因用户误操作导致集群出现数据问题;
  6. SNAPSHOT_DIFF(SnapshotDiffSection):执行快照操作的目录/文件的Diff集合数据,与SNAPSHOT一起构建较完整的快照管理能力;
  7. SECRET_MANAGER(SecretManagerSection):记录DelegationKey和DelegationToken数据,根据DelegationKey及由DelegationToken构造出的DelegationTokenIdentifier方便进一步计算密码,以上数据可以完善所有合法Token集合;
  8. CACHE_MANAGER(CacheManagerSection):集中式缓存特性全局信息,集中式缓存特性是Hadoop-2.3.0为提升数据读性能引入的新特性;
  9. STRING_TABLE(StringTableSection):字符串到ID的映射表,维护目录/文件的Permission字符到ID的映射,节省存储空间。

NameNode执行CheckPoint时,遵循Protobuf定义及上述文件格式描述,重启加载FSImage时,同样按照Protobuf定义的格式从文件流中读出相应数据构建整个目录树Namespace及其他元数据。将FSImage文件从持久化设备加载到内存并构建出目录树结构后,实际上并没有完全恢复元数据到最新状态,因为每次CheckPoint之后还可能存在大量HDFS写操作。

2.2 回放EditLog

NameNode在响应客户端的写请求前,会首先更新内存相关元数据,然后再把这些操作记录在EditLog文件中,可以看到内存状态实际上要比EditLog数据更及时。

记录在EditLog之中的每个操作又称为一个事务,对应一个整数形式的事务编号。在当前实现中多个事务组成一个Segment,生成独立的EditLog文件,其中文件名称标记了起止的事务编号,正在写入的EditLog文件仅标记起始事务编号。EditLog文件的格式非常简单,没再通过Google Protobuf描述,文件格式如图4所示。

一个完整的EditLog文件包括四个部分内容,分别是:

  • LAYOUTVERSION:版本信息;
  • OP_START_LOG_SEGMENT:标识文件开始;
  • RECORD:顺序逐个记录HDFS写操作的事务内容;
  • OP_END_LOG_SEGMENT:标记文件结束。

NameNode加载FSImage完成后,即开始对该FSImage文件之后(通过比较FSImage文件名称中包含的事务编号与EditLog文件名称的起始事务编号大小确定)生成的所有EditLog严格按照事务编号从小到大逐个遵循上述的格式进行每一个HDFS写操作事务回放。

NameNode加载完所有必需的EditLog文件数据后,内存中的目录树即恢复到了最新状态。

2.3 DataNode注册汇报

经过前面两个步骤,主要的元数据被构建,HDFS的整个目录树被完整建立,但是并没有掌握数据块Block与DataNode之间的对应关系BlocksMap,甚至对DataNode的情况都不掌握,所以需要等待DataNode注册,并完成对从DataNode汇报上来的数据块汇总。待汇总的数据量达到预设比例(dfs.namenode.safemode.threshold-pct)后退出Safemode。

NameNode重启经过加载FSImage和回放EditLog后,所有DataNode不管进程是否发生过重启,都必须经过以下两个步骤:

  1. DataNode重新注册RegisterDataNode;
  2. DataNode汇报所有数据块BlockReport。

对于节点规模较大和元数据量较大的集群,这个阶段的耗时会非常可观。主要有三点原因:

  1. 处理BlockReport的逻辑比较复杂,相对其他RPC操作耗时较长。图5对比了BlockReport和AddBlock两种不同RPC的处理时间,尽管AddBlock操作也相对复杂,但是对比来看,BlockReport的处理时间显著高于AddBlock处理时间;
  2. NameNode对每一个BlockReport的RPC请求处理都需要持有全局锁,也就是说对于BlockReport类型RPC请求实际上是串行处理;
  3. NameNode重启时所有DataNode集中在同一时间段进行BlockReport请求。

之前我们在NameNode内存全景一文中详细描述过Block在NameNode元数据中的关键作用及与Namespace/DataNode/BlocksMap的复杂关系,从中也可以看出,每个新增Block需要维护多个关系,更何况重启过程中所有Block都需要建立同样复杂关系,所以耗时相对较高。

三、重启优化

根据前面对NameNode重启过程的简单梳理,在各个阶段可以适当的实施优化以加快NameNode重启过程。

HDFS-7097 解决重启过程中SBN执行CheckPoint时不能处理BlockReport请求的问题

Fix:2.7.0

Hadoop-2.7.0版本前,SBN(StandbyNameNode)在执行CheckPoint操作前会先获得全局读写锁fsLock,在此期间,BlockReport请求由于不能获得全局写锁会持续处于等待状态,直到CheckPoint完成后释放了fsLock锁后才能继续。NameNode重启的第三个阶段,同样存在这种情况。而且对于规模较大的集群,每次CheckPoint时间在分钟级别,对整个重启过程影响非常大。实际上,CheckPoint是对目录树的持久化操作,并不涉及BlocksMap数据结构,所以CheckPoint期间是可以让BlockReport请求直接通过,这样可以节省期间BlockReport排队等待带来的时间开销,HDFS-7097正是将锁粒度放小解决了CheckPoint过程不能处理BlockReport类型RPC请求的问题。

HDFS-7097相对,另一种思路也值得借鉴,就是重启过程尽可能避免出现CheckPoint。触发CheckPoint有两种情况:时间周期或HDFS写操作事务数,分别通过参数dfs.namenode.checkpoint.period和dfs.namenode.checkpoint.txns控制,默认值分别是3600s和1,000,000,即默认情况下一个小时或者写操作的事务数超过1,000,000触发一次CheckPoint。为了避免在重启过程中频繁执行CheckPoint,可以适当调大dfs.namenode.checkpoint.txns,建议值10,000,000 ~ 20,000,000,带来的影响是EditLog文件累计的个数会稍有增加。从实践经验上看,对一个有亿级别元数据量的NameNode,回放一个EditLog文件(默认1,000,000写操作事务)时间在秒级,但是执行一次CheckPoint时间通常在分钟级别,综合权衡减少CheckPoint次数和增加EditLog文件数收益比较明显。

HDFS-6763 解决SBN每间隔1min全局计算和验证Quota值导致进程Hang住数秒的问题

Fix:2.8.0

ANN(ActiveNameNode)将HDFS写操作实时写入JN的EditLog文件,为同步数据,SBN默认间隔1min从JN拉取一次EditLog文件并进行回放,完成后执行全局Quota检查和计算,当Namespace规模变大后,全局计算和检查Quota会非常耗时,在此期间,整个SBN的Namenode进程会被Hang住,以至于包括DN心跳和BlockReport在内的所有RPC请求都不能及时处理。NameNode重启过程中这个问题影响突出。

实际上,SBN在EditLog Tailer阶段计算和检查Quota完全没有必要,HDFS-6763将这段处理逻辑后移到主从切换时进行,解决SBN进程间隔1min被Hang住的问题。

从优化效果上看,对一个拥有接近五亿元数据量,其中两亿数据块的NameNode,优化前数据块汇报阶段耗时~30min,其中触发超过20次由于计算和检查Quota导致进程Hang住~20s的情况,整个BlockReport阶段存在超过5min无效时间开销,优化后可到~25min。

HDFS-7980 简化首次BlockReport处理逻辑优化重启时间

Fix:2.7.1

NameNode加载完元数据后,所有DataNode尝试开始进行数据块汇报,如果汇报的数据块相关元数据还没有加载,先暂存消息队列,当NameNode完成加载相关元数据后,再处理该消息队列。对第一次块汇报的处理比较特别(NameNode重启后,所有DataNode的BlockReport都会被标记成首次数据块汇报),为提高处理速度,仅验证块是否损坏,之后判断块状态是否为FINALIZED,若是建立数据块与DataNode的映射关系,建立与目录树中文件的关联关系,其他信息一概暂不处理。对于非初次数据块汇报,处理逻辑要复杂很多,对报告的每个数据块,不仅检查是否损坏,是否为FINALIZED状态,还会检查是否无效,是否需要删除,是否为UC状态等等;验证通过后建立数据块与DataNode的映射关系,建立与目录树中文件的关联关系。

初次数据块汇报的处理逻辑独立出来,主要原因有两方面:

  • 加快NameNode的启动时间;测试数据显示含~500M元数据的NameNode在处理800K个数据块的初次块汇报的处理时间比正常块汇报的处理时间可降低一个数量级;
  • 启动过程中,不提供正常读写服务,所以只要确保正常数据(整个Namespace和所有FINALIZED状态Blocks)无误,无效和冗余数据处理完全可以延后到IBR(IncrementalBlockReport)或下次BR(BlockReport)。

    这本来是非常合理和正常的设计逻辑,但是实现时NameNode在判断是否为首次数据块块汇报的逻辑一直存在问题,导致这段非常好的改进点逻辑实际上长期并未真正执行到,直到HDFS-7980在Hadoop-2.7.1修复该问题。HDFS-7980的优化效果非常明显,测试显示,对含80K Blocks的BlockReport RPC请求的处理时间从~500ms可优化到~100ms,从重启期整个BlockReport阶段看,在超过600M元数据,其中300M数据块的NameNode显示该阶段从~50min优化到~25min。

HDFS-7503 解决重启前大删除操作会造成重启后锁内写日志降低处理能力

Fix:2.7.0

若NameNode重启前产生过大删除操作,当NameNode加载完FSImage并回放了所有EditLog构建起最新目录树结构后,在处理DataNode的BlockReport时,会发现有大量Block不属于任何文件,Hadoop-2.7.0版本前,对于这类情况的输出日志逻辑在全局锁内,由于存在大量IO操作的耗时,会严重拉长处理BlockReport的处理时间,影响NameNode重启时间。HDFS-7503的解决办法非常简单,把日志输出逻辑移出全局锁外。线上效果上看对同类场景优化比较明显,不过如果重启前不触发大的删除操作影响不大。

防止热备节点SBN(StandbyNameNode)/冷备节点SNN(SecondaryNameNode)长时间未正常运行堆积大量Editlog拖慢NameNode重启时间

选择HA热备方案SBN(StandbyNameNode)还是冷备方案SNN(SecondaryNameNode)架构,执行CheckPoint的逻辑几乎一致,如图6所示。如果SBN/SNN服务长时间未正常运行,CheckPoint不能按照预期执行,这样会积压大量EditLog。积压的EditLog文件越多,重启NameNode需要加载EditLog时间越长。所以尽可能避免出现SNN/SBN长时间未正常服务的状态。

在一个有500M元数据的NameNode上测试加载一个200K次HDFS事务操作的EditLog文件耗时~5s,按照默认2min的EditLog滚动周期,如果一周时间SBN/SNN未能正常工作,则会累积~5K个EditLog文件,此后一旦发生NameNode重启,仅加载EditLog文件的时间就需要~7h,也就是整个集群存在超过7h不可用风险,所以切记要保证SBN/SNN不能长时间故障。

HDFS-6425 HDFS-6772 NameNode重启后DataNode快速退出blockContentsStale状态防止PostponedMisreplicatedBlocks过大影响对其他RPC请求的处理能力

Fix: 2.6.02.7.0

当集群中大量数据块的实际存储副本个数超过副本数时(跨机房架构下这种情况比较常见),NameNode重启后会迅速填充到PostponedMisreplicatedBlocks,直到相关数据块所在的所有DataNode汇报完成且退出Stale状态后才能被清理。如果PostponedMisreplicatedBlocks数据量较大,每次全遍历需要消耗大量时间,且整个过程也要持有全局锁,严重影响处理BlockReport的性能,HDFS-6425HDFS-6772分别将可能在BlockReport逻辑内部遍历非常大的数据结构PostponedMisreplicatedBlocks优化到异步执行,并在NameNode重启后让DataNode快速退出blockContentsStale状态避免PostponedMisreplicatedBlocks过大入手优化重启效率。

降低BlockReport时数据规模

NameNode处理BlockReport的效率低主要原因还是每次BlockReport所带的Block规模过大造成,所以可以通过调整Block数量阈值,将一次BlockReport分成多盘分别汇报,以提高NameNode对BlockReport的处理效率。可参考的参数为:dfs.blockreport.split.threshold,默认值1,000,000,即当DataNode本地的Block个数超过1,000,000时才会分盘进行汇报,建议将该参数适当调小,具体数值可结合NameNode的处理BlockReport时间及集群中所有DataNode管理的Block量分布确定。

重启完成后对比检查数据块上报情况

前面提到NameNode汇总DataNode上报的数据块量达到预设比例(dfs.namenode.safemode.threshold-pct)后就会退出Safemode,一般情况下,当NameNode退出Safemode后,我们认为已经具备提供正常服务的条件。但是对规模较大的集群,按照这种默认策略及时执行主从切换后,容易出现短时间丢块的问题。考虑在200M数据块的集群,默认配置项dfs.namenode.safemode.threshold-pct=0.999,也就是当NameNode收集到200M*0.999=199.8M数据块后即可退出Safemode,此时实际上还有200K数据块没有上报,如果强行执行主从切换,会出现大量的丢块问题,直到数据块汇报完成。应对的办法比较简单,尝试调大dfs.namenode.safemode.threshold-pct到1,这样只有所有数据块上报后才会退出Safemode。但是这种办法一样不能保证万无一失,如果启动过程中有DataNode汇报完数据块后进程挂掉,同样存在短时间丢失数据的问题,因为NameNode汇总上报数据块时并不检查副本数,所以更稳妥的解决办法是利用主从NameNode的JMX数据对比所有DataNode当前汇报数据块量的差异,当差异都较小后再执行主从切换可以保证不发生上述问题。

其他

除了优化NameNode重启时间,实际运维中还会遇到需要滚动重启集群所有节点或者一次性重启整集群的情况,不恰当的重启方式也会严重影响服务的恢复时间,所以合理控制重启的节奏或选择合适的重启方式尤为关键,HDFS集群启动方式分析一文对集群重启方式进行了详细的阐述,这里就不再展开。

经过多次优化调整,从线上NameNode历次的重启时间监控指标上看,收益非常明显,图7截取了其中几次NameNode重启时元数据量及重启时间开销对比,图中直观显示在500M元数据量级下,重启时间从~4000s优化到~2000s。

这里罗列了一小部分实践过程中可以有效优化重启NameNode时间或者重启全集群的点,其中包括了社区成熟Patch和相关参数优化,虽然实现逻辑都很小,但是实践收益非常明显。当然除了上述提到,NameNode重启还有很多可以优化的地方,比如优化FSImage格式,并行加载等等,社区也在持续关注和优化,部分讨论的思路也值得关注、借鉴和参考。

四、总结

NameNode重启甚至全集群重启在整个Hadoop集群的生命周期内是比较频繁的运维操作,优化重启时间可以极大提升运维效率,避免可能存在的风险。本文通过分析NameNode启动流程,并结合实践过程简单罗列了几个供参考的有效优化点,借此希望能给实践过程提供可优化的方向和思路。

五、参考文献

作者简介

小桥,美团点评技术工程部数据平台研发工程师。2012年北京航空航天大学毕业,2015年初加入美团点评,关注Hadoop生态存储方向,致力于为美团点评提供稳定、高效、易用的离线数据存储服务。

原文出自:https://tech.meituan.com/archives

美团点评基于Storm的实时数据处理实践

美团点评阅读(311)

背景

      目前美团点评已累计了丰富的线上交易与用户行为数据,为商家赋能需要我们有更强大的专业化数据加工能力,来帮助商家做出正确的决策从而提高用户体验。目前商家端产品在数据应用上主要基于离线数据加工,数据生产调度以“T+1”为主,伴随着越来越深入的精细化运营,实时数据应用诉求逾加强烈。本文将从目前主流实时数据处理引擎的特点和我们面临的问题出发,简单的介绍一下我们是如何搭建实时数据处理系统。

设计框架

      目前比较流行的实时处理引擎有 Storm,Spark Streaming,Flink。每个引擎都有各自的特点和应用场景。 下表是对这三个引擎的简单对比:
Alt text

      考虑到每个引擎的特点、商家端应用的特点和系统的高可用性,我们最终选择了 Storm 作为本系统的实时处理引擎。
Alt text

面临的问题

  1. 数据量的不稳定性,导致对机器需求的不确定性。用户的行为数据会受到时间的影响,比如半夜时刻和用餐高峰时段每分钟产生的数据量有两个数量级的差异。
  2. 上游数据质量的不确定性。
  3. 数据计算时,数据的落地点应该放到哪里来保证计算的高效性。
  4. 如何保证数据在多线程处理时数据计算的正确性。
  5. 计算好的数据以什么样的方式提供给应用方。

具体的实施方案

Alt text

实时摄入数据完整性保障

      数据完整性保证层:如何保证数据摄入到计算引擎的完整性呢?正如表格中比较的那样,Storm 框架的语义为 At Least Once,至少摄入一次。这个语义的存在正好保证了数据的完整性,所以只需要根据自己的需求编写 Spout 即可。好消息是我们的技术团队已经开发好了一个满足大多数需求的 Spout,可以直接拿来使用。特别需要注意的一点,在数据处理的过程中需要我们自己来剔除已经处理过的数据,因为 Storm 的语义会可能导致同一条数据摄入两次。灰度发布期间(一周)对数据完整性进行验证,数据完整性为100%。

实时数据平滑处理

      数据预测层:实时的数据预测可以帮助我们对到达的数据进行有效的平滑,从而可以减少在某一时刻对集群的压力。 在数据预测方面,我们采用了在数学上比较简单的多元线性回归模型(如果此模型不满足业务需求,可以选用一些更高级别的预测模型),预测下一分钟可能到来的数据的量。在数据延迟可接受的范围内,对数据进行平滑,并完成对数据的计算。通过对该方案的使用,减轻了对集群约33%的压力。具体步骤如下:

  • 步骤一:将多个业务的实时数据进行抽象化,转换为(Y_i,X_1𝑖,X_2𝑖,X_3i,… ,X_ni),其中Y_i为在(X_1i…X_ni)属性下的数据量,(X_1i…X_ni)为n个不同的属性,比如时间、业务、用户的性别等等。

  • 步骤二:因为考虑到实时数据的特殊性,不同业务的数据量随时间变量基本呈现为M走势,所以为了将非线性走势转换为线性走势,可以将时间段分为4部分,保证在每个时间段内数据的走势为线性走势。同理,如果其他的属性使得走势变为非线性,也可以分段分析。

  • 步骤三:将抽象好的数据代入到多元线性回归模型中,其方程组形式为:
    Alt text

    Alt text
    通过对该模型的求解方式求得估计参数,最后得多元线性回归方程。
    Alt text

  • 步骤四:数据预测完之后通过控制对数据的处理速度,保证在规定的时间内完成对规定数据的计算,减轻对集群的压力。

实时数据计算策略

      策略层:Key/Value 模式更适应于实时数据模型,不管是在存储还是计算方面。Cellar(我们内部基于阿里开源的Tair研发的公共KV存储)作为一个分布式的 Key/Value 结构数据的解决方案,可以做到几乎无延迟的进行 IO 操作,并且可以支持高达千万级别的 QPS,更重要的是 Cellar 支持很多原子操作,运用在实时数据计算上是一个不错的选择。所以作为数据的落脚点,本系统选择了Cellar。

      但是在数据计算的过程中会遇到一些问题,比如说统计截止到当前时刻入住旅馆的男女比例是多少?很容易就会想到,从 Cellar 中取出截止到当前时刻入住的男生是多少,女生是多少,然后做一个比值就 OK 了。但是本系统是在多线程的环境运行的,如果该时刻有两对夫妇入住了,产生了两笔订单,恰好这两笔订单被两个线程所处理,当线程A将该男士计算到结果中,正要打算将该女士计算到结果中的时候,线程B已经计算完结果了,那么线程B计算出的结果就是2/1,那就出错啦。

      所以为了保证数据在多线程处理时数据计算的正确性,我们需要用到分布式锁。实现分布式锁的方式有很多,本文就不赘述了。这里给大家介绍一种更简单快捷的方法。Cellar 中有个 setNx 函数,该函数是原子的,并且是(Set If Not Exists),所以用该函数锁住关键的字段就可以。就上面的例子而言,我们可以锁住该旅馆的唯一 ID 字段,计算完之后 delete 该锁,这样就可以保证了计算的正确性。

      另外一个重要的问题是 Cellar 不支持事务,就会导致该计算系统在升级或者重启时会造成少量数据的不准确。为了解决该问题,运用到一种 getset 原子思想的方法。如下:


public void doSomeWork(String input) {
    cellar.mapPut("uniq_ID");
    cellar.add("uniq_ID_1","some data");
    cellar.add("uniq_ID_2","some data again");
    ....
    cellar.mapRemove("uniq_ID");
}

      如果上述代码执行到[2..5]某一行时系统重启了,导致后续的操作并没有完成,如何将没有完成的操作添加上去呢?如下:


public void remedySomething() {
    map = cellar.mapGetAll();
    version = cellar.mapGet("uniq_ID").getVersion();
    for (string str : map) {
        if (cellar.get(str + "_1").getVersion()!= version) {
            cellar.add(str + "_1", "some data");
            cellar.mapRemove(str);
        }
        .......
    }
}

      正如代码里那样,会有一个容器记录了哪些数据正在被操作,当系统重启的时候,从该容器取出上次未执行完的数据,用 Version(版本号)来记录哪些操作还没有完成,将没有完成的操作补上,这样就可以保证了计算结果的准确性。起初 Version(版本号)被设计出来解决的问题是防止由于数据的并发更新导致的问题。

      比如,系统有一个 value 为“a,b,c”,A和B同时get到这个 value。A执行操作,在后面添加一个d,value 为 “a,b,c,d”。B执行操作添加一个e,value为”a,b,c,e”。如果不加控制,无论A和B谁先更新成功,它的更新都会被后到的更新覆盖。Tair 无法解决这个问题,但是引入了version 机制避免这样的问题。还是拿刚才的例子,A和B取到数据,假设版本号为10,A先更新,更新成 功后,value 为”a,b,c,d”,与此同时,版本号会变为11。当B更新时,由于其基于的版本号是10,服务器会拒绝更新,从而避免A的更新被覆盖。B可以选择 get 新版本的 value,然后在其基础上修改,也可以选择强行更新。

      将 Version 运用到事务的解决上也算是一种新型的使用。为验证该功能的正确性,灰度发布期间每天不同时段对项目进行杀死并重启,并对数据正确性进行校验,数据的正确性为100%。

实时数据存储

      为了契合更多的需求,将数据分为三部分存储。
      Kafka:存储稍加工之后的明细数据,方便做更多的扩展。
      MySQL:存储中间的计算结果数据,方便计算过程的可视化。
      Cellar:存储最终的结果数据,供应用层直接查询使用。

应用案例

  1. 美团开店宝的实时经营数据卡片
          美团开店宝作为美团商家的客户端,支持着众多餐饮商家的辅助经营,而经营数据的实时性对影响商家决策尤为重要。该功能上线之后受到了商家的热烈欢迎。卡片展示如下图:
    Alt text

  2. 美团点评金融合作门店的实时热度标签
          该功能用于与美团点评金融合作商家增加支付标签,用以突出这些商家,增加营销点。另一方面为优质商家吸引更多流量,为平台带来更多收益。展示如下图:
    Alt text

总结与展望

      以上就是该系统的设计框架与思路,并且部分功能已应用到系统中。为了商家更好的决策,用户更好的体验,在业务不断增长的情况下,对实时数据的分析就需要做到更全面。所以实时数据分析还有很多东西可以去做。
      老生常谈的大数据 4V+1O 特征,即数据量大(Volume)、类型繁多(Variety)、价值密度低(Value)、速度快时效性高(Velocity)、数据在线(Online),相比离线数据系统,对实时数据的计算和应用挑战尤其艰巨。在技术框架演进层面,对流式数据进行高度抽象,简化开发流程;在应用端,我们后续希望在数据大屏、用户行为分析产品、营销效果跟踪等 DW/BI 产品进行持续应用,通过加快数据流转的速度,更好的发挥数据价值。

参考

关于我们

      到餐数据团队,用业内最先进的理念建设数据相关的系统和应用,期待更多数据系统开发、数据仓库开发、数据建模好手的加入。 发邮件给我们 liuqiang24@meituan.comxuyang14@meituan.com

原文出自:https://tech.meituan.com/archives

美团点评智能支付核心交易系统的可用性实践

美团点评阅读(322)

背景

每个系统都有它最核心的指标。比如在收单领域:进件系统第一重要的是保证入件准确,第二重要的是保证上单效率。清结算系统第一重要的是保证准确打款,第二重要的是保证及时打款。我们负责的系统是美团点评智能支付的核心链路,承担着智能支付100%的流量,内部习惯称为核心交易。因为涉及美团点评所有线下交易商家、用户之间的资金流转,对于核心交易来说:第一重要的是稳定性,第二重要的还是稳定性。
稳定重要性

问题引发

作为一个平台部门,我们的理想是第一阶段快速支持业务;第二阶段把控好一个方向;第三阶段观察好市场的方向,自己去引领一个大方向。

理想很丰满,现实是自从2017年初的每日几十万订单,到年底时,单日订单已经突破700万,系统面临着巨大的挑战。支付通道在增多;链路在加长;系统复杂性也相应增加。从最初的POS机到后来的二维码产品,小白盒、小黑盒、秒付……产品的多元化,系统的定位也在时刻的发生着变化。而系统对于变化的应对速度像是一个在和兔子赛跑的乌龟。

由于业务的快速增长,就算系统没有任何发版升级,也会突然出现一些事故。事故出现的频率越来越高,系统自身的升级,也经常是困难重重。基础设施升级、上下游升级,经常会发生“蝴蝶效应”,毫无征兆的受到影响。

问题分析

核心交易的稳定性问题根本上是怎么实现高可用的问题。

可用性指标

业界高可用的标准是按照系统宕机时间来衡量的:
可用性标准

因为业界的标准是后验的指标,考虑到对于平时工作的指导意义,我们通常采用服务治理平台OCTO来统计可用性。计算方法是:
美团可用性计算

可用性分解

业界系统可靠性还有两个比较常用的关键指标:

  • 平均无故障时间(Mean Time Between Failures,简称MTBF):即系统平均能够正常运行多长时间,才发生一次故障
  • 平均修复时间(Mean Time To Repair,简称MTTR):即系统由故障状态转为工作状态时修理时间的平均值

对于核心交易来说,可用性最好是无故障。在有故障的时候,判定影响的因素除了时间外,还有范围。将核心交易的可用性问题分解则为:
可用性分解

问题解决

1. 发生频率要低之别人死我们不死

1.1 消除依赖、弱化依赖和控制依赖

用STAR法则举一个场景:

情境(situation)

我们要设计一个系统A,完成:使用我们美团点评的POS机,通过系统A连接银行进行付款,我们会有一些满减,使用积分等优惠活动。

任务(task)

分析一下对于系统A的显性需求和隐性需求:
1>需要接收上游传过来的参数,参数里包含商家信息、用户信息、设备信息、优惠信息。
2>生成单号,将交易的订单信息落库。
3>敏感信息要加密。
4>要调用下游银行的接口。
5>要支持退款。
6>要把订单信息同步给积分核销等部门。
7>要能给商家一个查看订单的界面。
8>要能给商家进行收款的结算。
基于以上需求,分析一下怎样才能让里面的最核心链路“使用POS机付款”稳定。

行动(action)

分析一下:需求1到4是付款必需链路,可以做在一个子系统里,姑且称之为收款子系统。5到8各自独立,每个都可以作为一个子系统来做,具体情况和开发人员数量、维护成本等有关系。
值得注意的是需求5-8和收款子系统的依赖关系并没有功能上的依赖,只有数据上的依赖。即他们都要依赖生成的订单数据。
收款子系统是整个系统的核心,对稳定性要求非常高。其他子系统出了问题,收款子系统不能受到影响。
基于上面分析,我们需要做一个收款子系统和其他子系统之间的一个解耦,统一管理给其他系统的数据。这里称为“订阅转发子系统”,只要保证这个系统不影响收款子系统的稳定即可。
粗略架构图如下:
架构图

结果(result)

从上图可以看到,收款子系统和退款子系统、结算子系统、信息同步子系统、查看订单子系统之间没有直接依赖关系。这个架构达到了消除依赖的效果。收款子系统不需要依赖数据订阅转发子系统,数据订阅转发子系统需要依赖收款子系统的数据。我们控制依赖,数据订阅转发子系统从收款子系统拉取数据,而不需要收款子系统给数据订阅转发子系统推送数据。这样,数据订阅转发子系统挂了,收款子系统不受影响。
再说数据订阅转发子系统拉取数据的方式。比如数据存在MySQL数据库中,通过同步Binlog来拉取数据。如果采用消息队列来进行数据传输,对消息队列的中间件就有依赖关系了。如果我们设计一个灾备方案:消息队列挂了,直接RPC调用传输数据。对于这个消息队列,就达到了弱化依赖的效果。

1.2 事务中不包含外部调用

外部调用包括对外部系统的调用和基础组件的调用。外部调用具有返回时间不确定性的特征,如果包含在了事务里必然会造成大事务。数据库大事务会造成其它对数据库连接的请求获取不到,从而导致和这个数据库相关的所有服务处于等待状态,造成连接池被打满,多个服务直接宕掉。如果这个没做好,危险指数五颗星。下面的图显示出外部调用时间的不可控:
大事务问题
解决方法:

  • 排查各个系统的代码,检查在事务中是否存在RPC调用、HTTP调用、消息队列操作、缓存、循环查询等耗时的操作,这个操作应该移到事务之外,理想的情况是事务内只处理数据库操作。
  • 对大事务添加监控报警。大事务发生时,会收到邮件和短信提醒。针对数据库事务,一般分为1s以上、500ms以上、100ms以上三种级别的事务报警。
  • 建议不要用XML配置事务,而采用注解的方式。原因是XML配置事务,第一可读性不强,第二切面通常配置的比较泛滥,容易造成事务过大,第三对于嵌套情况的规则不好处理。
    大事务排除措施

1.3 设置合理的超时和重试

对外部系统和缓存、消息队列等基础组件的依赖。假设这些被依赖方突然发生了问题,我们系统的响应时间是:内部耗时+依赖方超时时间*重试次数。如果超时时间设置过长、重试过多,系统长时间不返回,可能会导致连接池被打满,系统死掉;如果超时时间设置过短,499错误会增多,系统的可用性会降低。
举个例子:
依赖例子
服务A依赖于两个服务的数据完成此次操作。平时没有问题,假如服务B在你不知道的情况下,响应时间变长,甚至停止服务,而你的客户端超时时间设置过长,则你完成此次请求的响应时间就会变长,此时如果发生意外,后果会很严重。
Java的Servlet容器,无论是Tomcat还是Jetty都是多线程模型,都用Worker线程来处理请求。这个可配置有上限,当你的请求打满Worker线程的最大值之后,剩余请求会被放到等待队列。等待队列也有上限,一旦等待队列都满了,那这台Web Server就会拒绝服务,对应到Nginx上返回就是502。如果你的服务是QPS较高的服务,那基本上这种场景下,你的服务也会跟着被拖垮。如果你的上游也没有合理的设置超时时间,那故障会继续向上扩散。这种故障逐级放大的过程,就是服务雪崩效应。
解决方法:

  • 首先要调研被依赖服务自己调用下游的超时时间是多少。调用方的超时时间要大于被依赖方调用下游的时间。
  • 统计这个接口99%的响应时间是多少,设置的超时时间在这个基础上加50%。如果接口依赖第三方,而第三方的波动比较大,也可以按照95%的响应时间。
  • 重试次数如果系统服务重要性高,则按照默认,一般是重试三次。否则,可以不重试。

1.4 解决慢查询

慢查询会降低应用的响应性能和并发性能。在业务量增加的情况下造成数据库所在的服务器CPU利用率急剧攀升,严重的会导致数据库不响应,只能重启解决。关于慢查询,可以参考我们技术博客之前的文章《MySQL索引原理及慢查询优化》。
慢查询
解决方法:

  • 将查询分成实时查询、近实时查询和离线查询。实时查询可穿透数据库,其它的不走数据库,可以用Elasticsearch来实现一个查询中心,处理近实时查询和离线查询。
  • 读写分离。写走主库,读走从库。
  • 索引优化。索引过多会影响数据库写性能。索引不够查询会慢。DBA建议一个数据表的索引数不超过4个。
  • 不允许出现大表。MySQL数据库的一张数据表当数据量达到千万级,效率开始急剧下降。

1.5 熔断

在依赖的服务不可用时,服务调用方应该通过一些技术手段,向上提供有损服务,保证业务柔性可用。而系统没有熔断,如果由于代码逻辑问题上线引起故障、网络问题、调用超时、业务促销调用量激增、服务容量不足等原因,服务调用链路上有一个下游服务出现故障,就可能导致接入层其它的业务不可用。下图是对无熔断影响的鱼骨图分析:
无熔断
解决方法:

  • 自动熔断:可以使用Netflix的Hystrix或者美团点评自己研发的Rhino来做快速失败。
  • 手动熔断:确认下游支付通道抖动或不可用,可以手动关闭通道。

2. 发生频率要低之自己不作死

自己不作死要做到两点:第一自己不作,第二自己不死。

2.1 不作

关于不作,我总结了以下7点:
1>不当小白鼠:只用成熟的技术,不因技术本身的问题影响系统的稳定。
2>职责单一化:不因职责耦合而削弱或抑制它完成最重要职责的能力。
3>流程规范化:降低人为因素带来的影响。
4>过程自动化:让系统更高效、更安全的运营。
5>容量有冗余:为了应对竞对系统不可用用户转而访问我们的系统、大促来临等情况,和出于容灾考虑,至少要保证系统2倍以上的冗余。
6>持续的重构:持续重构是确保代码长期没人动,一动就出问题的有效手段。
7>漏洞及时补:美团点评有安全漏洞运维机制,提醒督促各个部门修复安全漏洞。
安全漏洞

2.2 不死

关于不死,地球上有五大不死神兽:能在恶劣环境下停止新陈代谢的“水熊虫”;可以返老还童的“灯塔水母”;在硬壳里休养生息的“蛤蜊”;水、陆、寄生样样都成的“涡虫”;有隐生能力的“轮虫”。它们的共通特征用在系统设计领域上就是自身容错能力强。这里“容错”的概念是:使系统具有容忍故障的能力,即在产生故障的情况下,仍有能力将指定的过程继续完成。容错即是Fault Tolerance,确切地说是容故障(Fault),而并非容错误(Error)。
容错

3. 发生频率要低之不被别人搞死

3.1 限流

在开放式的网络环境下,对外系统往往会收到很多有意无意的恶意攻击,如DDoS攻击、用户失败重刷。虽然我们的队友各个是精英,但还是要做好保障,不被上游的疏忽影响,毕竟,谁也无法保证其他同学哪天会写一个如果下游返回不符合预期就无限次重试的代码。这些内部和外部的巨量调用,如果不加以保护,往往会扩散到后台服务,最终可能引起后台基础服务宕机。下图是对无限流影响的问题树分析:
无限流
解决方法:

  • 通过对服务端的业务性能压测,可以分析出一个相对合理的最大QPS。
  • 流量控制中用的比较多的三个算法是令牌桶、漏桶、计数器。可以使用Guava的RateLimiter来实现。其中SmoothBurstry是基于令牌桶算法的,SmoothWarmingUp是基于漏桶算法的。
  • 核心交易这边采用美团服务治理平台OCTO做thrift截流。可支持接口粒度配额、支持单机/集群配额、支持指定消费者配额、支持测试模式工作、及时的报警通知。其中测试模式是只报警并不真正节流。关闭测试模式则超过限流阈值系统做异常抛出处理。限流策略可以随时关闭。
  • 可以使用Netflix的Hystrix或者美团点评自己研发的Rhino来做特殊的针对性限流。

4. 故障范围要小之隔离

隔离是指将系统或资源分割开,在系统发生故障时能限定传播范围和影响范围。
服务器物理隔离原则

① 内外有别:内部系统与对外开放平台区分对待。
② 内部隔离:从上游到下游按通道从物理服务器上进行隔离,低流量服务合并。
③ 外部隔离:按渠道隔离,渠道之间互不影响。

线程池资源隔离

  • Hystrix通过命令模式,将每个类型的业务请求封装成对应的命令请求。每个命令请求对应一个线程池,创建好的线程池是被放入到ConcurrentHashMap中。
    注意:尽管线程池提供了线程隔离,客户端底层代码也必须要有超时设置,不能无限制的阻塞以致于线程池一直饱和。

信号量资源隔离

  • 开发者可以使用Hystrix限制系统对某一个依赖的最高并发数,这个基本上就是一个限流策略。每次调用依赖时都会检查一下是否到达信号量的限制值,如达到,则拒绝。

5. 故障恢复要快之快速发现

发现分为事前发现、事中发现和事后发现。事前发现的主要手段是压测和故障演练;事中发现的主要手段是监控报警;事后发现的主要手段是数据分析。

5.1 全链路线上压测

你的系统是否适合全链路线上压测呢?一般来说,全链路压测适用于以下场景:
① 针对链路长、环节多、服务依赖错综复杂的系统,全链路线上压测可以更快更准确的定位问题。
② 有完备的监控报警,出现问题可以随时终止操作。
③ 有明显的业务峰值和低谷。低谷期就算出现问题对用户影响也比较小。
全链路线上压测的目的主要有:
① 了解整个系统的处理能力
② 排查性能瓶颈
③ 验证限流、降级、熔断、报警等机制是否符合预期并分析数据反过来调整这些阈值等信息
④ 发布的版本在业务高峰的时候是否符合预期
⑤ 验证系统的依赖是否符合预期
全链路压测的简单实现:
① 采集线上日志数据来做流量回放,为了和实际数据进行流量隔离,需要对部分字段进行偏移处理。
② 数据着色处理。可以用中间件来获取和传递流量标签。
③ 可以用影子数据表来隔离流量,但是需要注意磁盘空间,建议如果磁盘剩余空间不足70%采用其他的方式隔离流量。
④ 外部调用可能需要Mock。实现上可以采用一个Mock服务随机产生和线上外部调用返回时间分布的时延。
压测工具上,核心交易这边使用美团点评开发的pTest。
压测工具对比

6. 故障恢复要快之快速定位

定位需要靠谱的数据。所谓靠谱就是和要发现的问题紧密相关的,无关的数据会造成视觉盲点,影响定位。所以对于日志,要制定一个简明日志规范。另外系统监控、业务监控、组件监控、实时分析诊断工具也是定位的有效抓手。
简明日志规范

7. 故障恢复要快之快速解决

要解决,提前是发现和定位。解决的速度还取决于是自动化的、半自动化的还是手工的。核心交易有意向搭建一个高可用系统。我们的口号是:“不重复造轮子,用好轮子。”这是一个集成平台,职责是:“聚焦核心交易高可用,更好、更快、更高效。”
美团点评内部可以使用的用于发现、定位、处理的系统和平台非常多,但是如果一个个打开链接或者登陆系统,势必影响解决速度。所以我们要做集成,让问题一站式解决。希望达到的效果举例如下:
解决问题

工具介绍

Hystrix

Hystrix实现了断路器模式来对故障进行监控,当断路器发现调用接口发生了长时间等待,就使用快速失败策略,向上返回一个错误响应,这样达到防止阻塞的目的。这里重点介绍一下Hystrix的线程池资源隔离和信号量资源隔离。
线程池资源隔离
线程池资源隔离
优点

  • 使用线程可以完全隔离第三方代码,请求线程可以快速放回。
  • 当一个失败的依赖再次变成可用时,线程池将清理,并立即恢复可用,而不是一个长时间的恢复。
  • 可以完全模拟异步调用,方便异步编程。

缺点

  • 线程池的主要缺点是它增加了CPU,因为每个命令的执行涉及到排队(默认使用SynchronousQueue避免排队),调度和上下文切换。
  • 对使用ThreadLocal等依赖线程状态的代码增加复杂性,需要手动传递和清理线程状态(Netflix公司内部认为线程隔离开销足够小,不会造成重大的成本或性能的影响)。

信号量资源隔离
开发者可以使用Hystrix限制系统对某一个依赖的最高并发数。这个基本上就是一个限流策略,每次调用依赖时都会检查一下是否到达信号量的限制值,如达到,则拒绝。
信号量资源隔离
优点

  • 不新起线程执行命令,减少上下文切换。

缺点

  • 无法配置断路,每次都一定会去尝试获取信号量。

比较一下线程池资源隔离和信号量资源隔离

  • 线程隔离是和主线程无关的其他线程来运行的;而信号量隔离是和主线程在同一个线程上做的操作。
  • 信号量隔离也可以用于限制并发访问,防止阻塞扩散,与线程隔离的最大不同在于执行依赖代码的线程依然是请求线程。
  • 线程池隔离适用于第三方应用或者接口、并发量大的隔离;信号量隔离适用于内部应用或者中间件;并发需求不是很大的场景。
     隔离对比

Rhino

Rhino是美团点评基础架构团队研发并维护的一个稳定性保障组件,提供故障模拟、降级演练、服务熔断、服务限流等功能。和Hystrix对比:

  • 内部通过CAT(美团点评开源的监控系统,参见之前的博客“深度剖析开源分布式监控CAT”)进行了一系列埋点,方便进行服务异常报警。
  • 接入配置中心,能提供动态参数修改,比如强制熔断、修改失败率等。

总结思考

王国维 在《人间词话》里谈到了治学经验,他说:古今之成大事业、大学问者,必经过三种之境界:
第一种境界
昨夜西风凋碧树。独上高楼,望尽天涯路。
第二种境界
衣带渐宽终不悔,为伊消得人憔悴。
第三种境界
众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。

核心交易的高可用目前正在经历第一种:高瞻远瞩认清前人所走的路,以总结和学习前人的经验做为起点。
下一阶段,既然认定了目标,我们会呕心沥血孜孜以求,持续发展高可用。最终,当我们做了很多的事情,回过头来看,相信会对高可用有更清晰和深入的认识。
敬请期待我们下一次的分享~

关于作者

晓静,20岁时毕业于东北大学计算机系。在毕业后的第一家公司由于出众的语言天赋,在1年的时间里从零开始学日语并以超高分通过了国际日语一级考试,担当两年日语翻译的工作。后就职于人人网,转型做互联网开发。中国科学院心理学研究生。有近百个技术发明专利,创业公司合伙人。有日本东京,美国硅谷技术支持经验。目前任美团点评技术专家,负责核心交易。

欢迎关注静儿的个人技术公众号:编程一生
公众号

招贤纳士

美团金融核心交易招聘实习生,要求:19年即将毕业的研究生,Java方向,有技术追求。高速发展的业务需要高速发展的团队,作为核心部门,我们急需相信技术改变世界的你!有意者请关注我的个人技术公众号并留言:-)

原文出自:https://tech.meituan.com/archives

深度学习在美团搜索广告排序的应用实践

美团点评阅读(322)

一、前言

在计算广告场景中,需要平衡和优化三个参与方——用户、广告主、平台的关键指标,而预估点击率CTR(Click-through Rate)和转化率CVR(Conversion Rate)是其中非常重要的一环,准确地预估CTR和CVR对于提高流量变现效率,提升广告主ROI(Return on Investment),保证用户体验等都有重要的指导作用。

传统的CTR/CVR预估,典型的机器学习方法包括人工特征工程 + LR(Logistic Regression)[1]、GBDT(Gradient Boosting Decision Tree)[2] + LR、FM(Factorization Machine)[3]和FFM(Field-aware Factorization Machine)[4]等模型。相比于传统机器学习方法,深度学习模型近几年在多领域多任务(图像识别、物体检测、翻译系统等)的突出表现,印证了神经网络的强大表达能力,以及端到端模型有效的特征构造能力。同时各种开源深度学习框架层出不穷,美团集团数据平台中心也迅速地搭建了GPU计算平台,提供GPU集群,支持TensorFlow、MXNet、Caffe等框架,提供数据预处理、模型训练、离线预测、模型部署等功能,为集团各部门的策略算法迭代提供了强有力的支持。

美团海量的用户与商家数据,广告复杂的场景下众多的影响因素,为深度学习方法的应用落地提供了丰富的场景。本文将结合广告特殊的业务场景,介绍美团搜索广告场景下深度学习的应用和探索。主要包括以下两大部分:

  • CTR/CVR预估由机器学习向深度学习迁移的模型探索
  • CTR/CVR预估基于深度学习模型的线下训练/线上预估的工程优化

二、从机器学习到深度学习的模型探索

2.1 场景与特征

美团搜索广告业务囊括了关键词搜索、频道筛选等业务,覆盖了美食、休娱、酒店、丽人、结婚、亲子等200多种应用场景,用户需求具有多样性。同时O2O模式下存在地理位置、时间等独特的限制。
结合上述场景,我们抽取了以下几大类特征:

  • 用户特征
    • 人口属性:用户年龄,性别,职业等。
    • 行为特征:对商户/商圈/品类的偏好(实时、历史),外卖偏好,活跃度等。
    • 建模特征:基于用户的行为序列建模产生的特征等。
  • 商户特征
    • 属性特征:品类,城市,商圈,品牌,价格,促销,星级,评论等。
    • 统计特征:不同维度/时间粒度的统计特征等。
    • 图像特征:类别,建模特征等。
    • 业务特征:酒店房型等。
  • Query特征
    • 分词,意图,与商户相似度,业务特征等。
  • 上下文特征
    • 时间,距离,地理位置,请求品类,竞争情况等。
    • 广告曝光位次。

结合美团多品类的业务特点及O2O模式独特的需求,着重介绍几个业务场景以及如何刻画:

  • 用户的消费场景
    • 附近”请求:美团和大众点评App中,大部分用户发起请求为“附近”请求,即寻找附近的美食、酒店、休闲娱乐场所等。因此给用户返回就近的商户可以起到事半功倍的效果。“请求到商户的距离”特征可以很好地刻画这一需求。
    • 指定区域(商圈)”请求:寻找指定区域的商户,这个区域的属性可作为该流量的信息表征。
    • 位置”请求:用户搜索词为某个位置,比如“五道口”,和指定区域类似,识别位置坐标,计算商户到该坐标的距离。
    • 家/公司”: 用户部分的消费场所为“家” 或 “公司”,比如寻找“家”附近的美食,在“公司”附近点餐等,根据用户画像得到的用户“家”和“公司”的位置来识别这种场景。
  • 多品类
    • 针对美食、酒店、休娱、丽人、结婚、亲子等众多品类的消费习惯以及服务方式,将数据拆分成三大部分,包括美食、酒店、综合(休娱、丽人、结婚、亲子等)。其中美食表达用户的餐饮需求,酒店表达用户的旅游及住宿需求,综合表达用户的其他生活需求。
  • 用户的行为轨迹
    • 实验中发现用户的实时行为对表达用户需求起到很重要的作用。比如用户想找个餐馆聚餐,先筛选了美食,发现附近有火锅、韩餐、日料等店,大家对火锅比较感兴趣,又去搜索特定火锅等等。用户点击过的商户、品类、位置,以及行为序列等都对用户下一刻的决策起到很大作用。

2.2 模型

搜索广告CTR/CVR预估经历了从传统机器学习模型到深度学习模型的过渡。下面先简单介绍下传统机器学习模型(GBDT、LR、FM & FFM)及应用,然后再详细介绍在深度学习模型的迭代。

GBDT

GBDT又叫MART(Multiple Additive Regression Tree),是一种迭代的决策树算法。它由多棵决策树组成,所有树的结论累加起来作为最终答案。它能自动发现多种有区分性的特征以及特征组合,并省去了复杂的特征预处理逻辑。Facebook实现GBDT + LR[5]的方案,并取得了一定的成果。

LR

\[ y(\mathbf{x}) = sigmoid(w_0+ \sum_{i=1}^n w_i x_i) \]

LR可以视作单层单节点的“DNN”, 是一种宽而不深的结构,所有的特征直接作用在最后的输出结果上。模型优点是简单、可控性好,但是效果的好坏直接取决于特征工程的程度,需要非常精细的连续型、离散型、时间型等特征处理及特征组合。通常通过正则化等方式控制过拟合。

FM & FFM

FM可以看做带特征交叉的LR,如下图所示:

从神经网络的角度考虑,可以看做下图的简单网络搭建方式:

模型覆盖了LR的宽模型结构,同时也引入了交叉特征,增加模型的非线性,提升模型容量,能捕捉更多的信息,对于广告CTR预估等复杂场景有更好的捕捉。

在使用DNN模型之前,搜索广告CTR预估使用了FFM模型,FFM模型中引入field概念,把\( n \)个特征归属到\( f \)个field里,得到\( nf \)个隐向量的二次项,拟合公式如下:

\[ y(\mathbf{x}) = w_0 + \sum_{i=1}^n w_i x_i + \sum_{i=1}^n \sum_{j=i+1}^n \langle \mathbf{v}_{i, f_j}, \mathbf{v}_{j, f_i} \rangle x_i x_j \]

上式中,\( f_j \) 表示第\( j \) 个特征所属的field。设定隐向量长度为\( k \),那么相比于FM的\( nk \)个二次项参数,FFM有\( nkf \)个二次项参数,学习和表达能力也更强。

例如,在搜索广告场景中,假设将特征划分到8个Field,分别是用户、广告、Query、上下文、用户-广告、上下文-广告、用户-上下文及其他,相对于FM能更好地捕捉每个Field的信息以及交叉信息,每个特征构建的隐向量长度8*\( k \), 整个模型参数空间为8 \( k \) \( n \) + \( n \) + 1。

Yu-Chin Juan实现了一个C++版的FFM模型工具包,但是该工具包只能在单机训练,难以支持大规模的训练数据及特征集合;并且它省略了常数项和一次项,只包含了特征交叉项,对于某些特征的优化需求难以满足,因此我们开发了基于PS-Lite的分布式FFM训练工具(支持亿级别样本,千万级别特征,分钟级完成训练,目前已经在公司内部普遍使用),主要添加了以下新的特性:

  • 支持FFM模型的分布式训练。
  • 支持一次项和常数项参数学习,支持部分特征只学习一次项参数(不需要和其他特征做交叉运算),例如广告位次特征等。拟合公式如下:
    \[ y(\mathbf{x}) = w_0 + \sum_{i=1}^n w_i x_i + \frac{1}{2} \sum_{i \in group} \sum_{j \in group\ and\ j \neq i} \langle \mathbf{v}_{i, f_j}, \mathbf{v}_{j, f_i} \rangle x_i x_j \]
  • 支持多种优化算法。

从GBDT模型切到FFM模型,积累的效果如下所示,主要的提升来源于对大规模离散特征的刻画及使用更充分的训练数据:

DNN

从上面的介绍大家可以看到,美团场景具有多样性和很高的复杂度,而实验表明从线性的LR到具备非线性交叉的FM,到具备Field信息交叉的FFM,模型复杂度(模型容量)的提升,带来的都是结果的提升。而LR和FM/FFM可以视作简单的浅层神经网络模型,基于下面一些考虑,我们在搜索广告的场景下把CTR模型切换到深度学习神经网络模型:

  • 通过改进模型结构,加入深度结构,利用端到端的结构挖掘高阶非线性特征,以及浅层模型无法捕捉的潜在模式。
  • 对于某些ID类特别稀疏的特征,可以在模型中学习到保持分布关系的稠密表达(embedding)。
  • 充分利用图片和文本等在简单模型中不好利用的信息。

我们主要尝试了以下网络结构和超参调优的实验。

Wide & Deep

首先尝试的是Google提出的经典模型Wide & Deep Model[6],模型包含Wide和Deep两个部分,其中Wide部分可以很好地学习样本中的高频部分,在LR中使用到的特征可以直接在这个部分使用,但对于没有见过的ID类特征,模型学习能力较差,同时合理的人工特征工程对于这个部分的表达有帮助。Deep部分可以补充学习样本中的长尾部分,同时提高模型的泛化能力。Wide和Deep部分在这个端到端的模型里会联合训练。

在完成场景与特征部分介绍的特征工程后,我们基于Wide & Deep模型进行结构调整,搭建了以下网络:

在搜索广告的场景中,上图的Part_1包含离散型特征及部分连续型特征离散化后的结果 (例如用户ID、广告ID、商圈ID、品类ID、GEO、各种统计类特征离散化结果等等)。离散化方式主要采用等频划分或MDLP[7]。每个域构建自己的embedding向量 (缺失特征和按照一定阈值过滤后的低频特征在这里统一视作Rare特征),得到特征的Representation,然后通过Pooling层做采样,并拼接在一起进行信息融合。

右侧的Part_2部分主要包含我们场景下的统计类特征及部分其他途径建模表示后输入的特征 (例如图片特征、文本特征等),和Part_1的最后一层拼接在一起做信息融合。

\[ a^{(1)}=concat(concat(Pooling(Emb_i(feature_i)))\ ,\ feature_{continuous}) \]

Part_3为多个全连接层,每个Layer后面连接激活函数,例如ReLu, Tanh等。

$$a^{(l+1)}=f(W^{(l)}a^{(l)} + b^{(l)})$$

右上的Part_4部分主要包含广告曝光位次 (Position Bias) 及部分离散特征,主要为了提高模型的记忆性,具有更强的刻画能力。Wide和Deep部分结合,得到最终的模型:

\[ y(\mathbf{x}) = \sigma (W^T_ {wide} x_{wide}\ +\ W^T_ {deep} a^{(l)} + b) \]

深度学习模型在图像语音等数据上有显著作用的原因之一是,我们在这类数据上不太方便产出能很好刻画场景的特征,人工特征+传统机器学习模型并不能学习出来全面合理的数据分布表示,而深度学习end-to-end的方式,直接结合Label去学习如何从原始数据抽取合适的表达(representation)。但是在美团等电商的业务场景下,输入的数据形态非常丰富,有很多业务数据有明确的物理含义,因此一部分人工特征工程也是必要的,提前对信息做一个合理的抽取表示,再通过神经网络学习进行更好的信息融合和表达。

在美团搜索广告的场景下,用户的实时行为有非常强的指代性,但是以原始形态直接送入神经网络,会损失掉很多信息,因此我们对它进行了不同方式描述和表示,再送入神经网络之中进行信息融合和学习。另一类很有作用的信息是图像信息,这部分信息的一种处理方式是,可以通过end-to-end的方式,用卷积神经网络和DNN进行拼接做信息融合,但是可能会有网络的复杂度过高,以及训练的收敛速度等问题,也可以选择用CNN预先抽取特征,再进行信息融合。

下面以这两类数据特征为例,介绍在Wide & Deep模型中的使用方式。

  • 用户实时行为
    • 行为实体 用户的实时行为包括点击商户(C_P)、下单商户(O_P)、搜索(Q)、筛选品类(S)等。商户的上层属性包括品类(Type: C_Type, O_Type)、位置(Loc: C_Loc, O_Loc)等。
    • Item Embedding 对用户的行为实体构建embedding向量,然后进行Sum/Average/Weighted Pooling,和其他特征拼接在一起。实验发现,上层属性实体(C_Type, O_Type, C_Loc, O_Loc)的表现很正向,离线效果有了很明显的提升。但是C_P, O_P, Q, S这些实体因为过于稀疏,导致模型过拟合严重,离线效果变差。因此,我们做了两方面的改进:
      1. 使用更充分的数据,单独对用户行为序列建模。例如LSTM模型,基于用户当前的行为序列,来预测用户下一时刻的行为,从中得到当前时刻的“Memory信息”,作为对用户的embedding表示;或Word2Vec模型,生成行为实体的embedding表示,Doc2Vec模型,得到用户的embedding表示。实验发现,将用户的embedding表示加入到模型Part_2部分,特征覆盖率增加,离线效果有了明显提升,而且由于模型参数空间增加很小,模型训练的时间基本不变。
      2. 使用以上方法产生的行为实体embedding作为模型参数初始值,并在模型训练过程中进行fine tuning。同时为了解决过拟合问题,对不同域的特征设置不同的阈值过滤。
    • 计数特征 即对不同行为实体发生的频次,它是对行为实体更上一层的抽象。
    • Pattern特征 用户最近期的几个行为实体序列(例如A-B-C)作为Pattern特征,它表示了行为实体之间的顺序关系,也更细粒度地描述了用户的行为轨迹。
  • 图片
    • 描述 商户的头图在App商品展示中占据着很重要的位置,而图片也非常吸引用户的注意力。
    • 图片分类特征 使用VGG16、Inception V4等训练图片分类模型,提取图片特征,然后加入到CTR模型中。
    • E2E model 将Wide & Deep模型和图片分类模型结合起来,训练端到端的网络。

从FFM模型切到Wide & Deep模型,积累到目前的效果如下所示,主要的提升来源于模型的非线性表达及对更多特征的更充分刻画。

DeepFM

华为诺亚方舟团队结合FM相比LR的特征交叉的功能,将Wide & Deep部分的LR部分替换成FM来避免人工特征工程,于是有了DeepFM[8],网络结构如下图所示。

比起Wide & Deep的LR部分,DeepFM采用FM作为Wide部分的输出,在训练过程中共享了对不同Field特征的embedding信息。

我们在部分业务上尝试了DeepFM模型,并进行了超参的重新调优,取得了一定的效果。其他业务也在尝试中。具体效果如下:

Multi-Task

广告预估场景中存在多个训练任务,比如CTR、CVR、交易额等。既考虑到多个任务之间的联系,又考虑到任务之间的差别,我们利用Multi-Task Learning的思想,同时预估点击率、下单率,模型结构如下图所示:

  • 由于CTR、CVR两个任务非常类似,所以采用“Hard Parameter Sharing”的结构,完全共享网络层的参数,只在输出层区分不同的任务。
  • 由于下单行为受展现位次的影响非常小,所以下单率的输出层不考虑位次偏差的因素。
  • 输出层在不同任务上单独增加所需特征。
  • 离线训练和线上预估流程减半,性能提升;效果上相对于单模型,效果基本持平:

近期,阿里发表论文“Entire Space Multi-Task Model”[9],提出目前CVR预估主要存在Sample Selection Bias(SSB)和Data Sparsity(DS)两个问题,并提出在全局空间建模(以pCTCVR和pCTR来优化CVR)和特征Transform的方法来解决。具体的Loss Function是:

网络结构是:

超参调优

除了以上对网络结构的尝试,我们也进行了多组超参的调优。神经网络最常用的超参设置有:隐层层数及节点数、学习率、正则化、Dropout Ratio、优化器、激活函数、Batch Normalization、Batch Size等。不同的参数对神经网络的影响不同,神经网络常见的一些问题也可以通过超参的设置来解决:

  • 过拟合
    • 网络宽度深度适当调小,正则化参数适当调大,Dropout Ratio适当调大等。
  • 欠拟合
    • 网络宽度深度适当调大,正则化参数调小,学习率减小等。
  • 梯度消失/爆炸问题
    • 合适的激活函数,添加Batch Normalization,网络宽度深度变小等。
  • 局部最优解
    • 调大Learning Rate,合适的优化器,减小Batch Size等。
  • Covariate Shift
    • 增加Batch Normalization,网络宽度深度变小等。

影响神经网络的超参数非常多,神经网络调参也是一件非常重要的事情。工业界比较实用的调参方法包括:

  • 网格搜索/Grid Search:这是在机器学习模型调参时最常用到的方法,对每个超参数都敲定几个要尝试的候选值,形成一个网格,把所有超参数网格中的组合遍历一下尝试效果。简单暴力,如果能全部遍历的话,结果比较可靠。但是时间开销比较大,神经网络的场景下一般尝试不了太多的参数组合。
  • 随机搜索/Random Search:Bengio在“Random Search for Hyper-Parameter Optimization”[10]中指出,Random Search比Grid Search更有效。实际操作的时候,可以先用Grid Search的方法,得到所有候选参数,然后每次从中随机选择进行训练。这种方式的优点是因为采样,时间开销变小,但另一方面,也有可能会错过较优的超参数组合。
  • 分阶段调参:先进行初步范围搜索,然后根据好结果出现的地方,再缩小范围进行更精细的搜索。或者根据经验值固定住其他的超参数,有针对地实验其中一个超参数,逐次迭代直至完成所有超参数的选择。这个方式的优点是可以在优先尝试次数中,拿到效果较好的结果。

我们在实际调参过程中,使用的是第3种方式,在根据经验参数初始化超参数之后,按照隐层大小->学习率->Batch Size->Drop out/L1/L2的顺序进行参数调优。

在搜索广告数据集上,不同超参的实验结果如下:

2.3 小结

搜索广告排序模型经历了从GBDT –> FFM –> DNN的迭代,同时构建了更加完善的特征体系,线下AUC累积提升13%+,线上CTR累积提升15%+。

三、基于深度学习模型的工程优化

3.1 线下训练

TensorFlow程序如果单机运行中出现性能问题,一般会有以下几种问题:

  1. 复杂的预处理逻辑耦合在训练过程中。
  2. 选择正确的IO方式。

剥离预处理流程

在模型的试验阶段,为了快速试验,数据预处理逻辑与模型训练部分都耦合在一起,而数据预处理包含大量IO类型操作,所以很适合用HadoopMR或者Spark处理。具体流程如下:

  1. 在预处理阶段将查表、join字典等操作都做完,并且将查询结果与原始数据merge在一起。
  2. 将libfm格式的数据转为易于TensorFlow操作的SparseTensor方式:

  3. 将原始数据转换为TensorFlow Record。

选择正确的IO方式

TensorFlow读取数据的方式主要有2种,一般选择错误会造成性能问题,两种方式为:

  1. Feed_dict 通过feed_dict将数据喂给session.run函数,这种方式的好处是思路很清晰,易于理解。缺点是性能差,性能差的原因是feed给session的数据需要在session.run之前准备好,如果之前这个数据没有进入内存,那么就需要等待数据进入内存,而在实际场景中,这不仅仅是等待数据从磁盘或者网络进入内存的事情,还可能包括很多前期预处理的工作也在这里做,所以相当于一个串行过程。而数据进入内存后,还要串行的调用PyArrayToTF_Tensor,将其copy成tensorflow的tensorValue。此时,GPU显存处于等待状态,同时,由于tf的Graph中的input为空,所以CPU也处于等待状态,无法运算。
  2. RecordReader 这种方式是tf在Graph中将读取数据这个操作看做图中一个operation节点,减少了一个copy的过程。同时,在tf中还有batch与threads的概念,可以异步的读取数据,保证在GPU或者CPU进行计算的时候,读取数据这个操作也可以多线程异步执行。静态图中各个节点间的阻塞:在一个复杂的DAG计算图中,如果有一个点计算比较慢时,会造成阻塞,下游节点不得不等待。此时,首先要考虑的问题是图中节点参数所存储的位置是否正确。比如如果某个计算节点是在GPU上运算,那么如果这个节点所有依赖的variable对象声明在CPU上,那么就要做一次memcpy,将其从内存中copy到GPU上。因为GPU计算的很快,所以大部分时间花在拷贝上了。总之,如果网络模型比较简单,那么这种操作就会非常致命;如果网络结构复杂,比如网络层次非常深,那么这个问题倒不是太大的问题了。

在这个Case中,因为需要提升吞吐,而不仅仅是在试验阶段。所以需要用RecordReader方式处理数据。

优化过程

  1. 将整体程序中的预处理部分从代码中去除,直接用Map-Reduce批处理去做(因为批处理可以将数据分散去做,所以性能非常好,2亿的数据分散到4900多个map中,大概处理了15分钟左右)。
  2. MR输出为TensorFlow Record格式,避免使用Feed_dict。
  3. 数据预读,也就是用多进程的方式,将HDFS上预处理好的数据拉取到本地磁盘(使用joblib库+shell将HDFS数据用多进程的方式拉取到本地,基本可以打满节点带宽2.4GB/s,所以,拉取数据也可以在10分钟内完成)。
  4. 程序通过TensorFlow提供的TFrecordReader的方式读取本地磁盘上的数据,这部分的性能提升是最为明显的。原有的程序处理数据的性能大概是1000条/秒,而通过TFrecordReader读取数据并且处理,性能大概是18000条/秒,性能大概提升了18倍。
  5. 由于每次run的时候计算都要等待TFrecordReader读出数据,而没用利用batch的方式。如果用多线程batch可以在计算期间异步读取数据。在TensorFlow所有例子中都是使用TFRecordReader的read接口去读取数据,再用batch将数据多线程抓过来。但是,其实这样做加速很慢。需要使用TFRecordReader的read_up_to的方法配合batch的equeue_many=True的参数,才可以做到最大的加速比。使用tf.train.batch的API后,性能提升了38倍。

此时,性能已经基本达到我们的预期了。例如整体数据量是2亿,按照以前的性能计算1000条/秒,大概需要运行55个小时。而现在大概需要运行87分钟,再加上预处理(15分钟)与预拉取数据(10分钟)的时间,在不增加任何计算资源的情况下大概需要2个小时以内。而如果是并行处理,则可以在分钟级完成训练。

3.2 线上预估

线上流量是模型效果的试金石。离线训练好的模型只有参与到线上真实流量预估,才能发挥其价值。在演化的过程中,我们开发了一套稳定可靠的线上预估体系,提高了模型迭代的效率。

模型同步

我们开发了一个高可用的同步组件:用户只需要提供线下训练好的模型的HDFS路径,该组件会自动同步到线上服务机器上。该组件基于HTTPFS实现,它是美团离线计算组提供的HDFS的HTTP方式访问接口。同步过程如下:

  1. 同步前,检查模型md5文件,只有该文件更新了,才需要同步。
  2. 同步时,随机链接HTTPFS机器并限制下载速度。
  3. 同步后,校验模型文件md5值并备份旧模型。

同步过程中,如果发生错误或者超时,都会触发报警并重试。依赖这一组件,我们实现了在2min内可靠的将模型文件同步到线上。

模型计算

当前我们线上有两套并行的预估计算服务。

  • 基于TF Serving的模型服务

TF Serving是TensorFlow官方提供的一套用于在线实时预估的框架。它的突出优点是:和TensorFlow无缝链接,具有很好的扩展性。使用TF serving可以快速支持RNN、LSTM、GAN等多种网络结构,而不需要额外开发代码。这非常有利于我们模型快速实验和迭代。

使用这种方式,线上服务需要将特征发送给TF Serving,这不可避免引入了网络IO,给带宽和预估时延带来压力。我们尝试了以下优化,效果显著。

  1. 并发请求。一个请求会召回很多符合条件的广告。在客户端多个广告并发请求TF Serving,可以有效降低整体预估时延。
  2. 特征ID化。通过将字符串类型的特征名哈希到64位整型空间,可以有效减少传输的数据量,降低使用的带宽。

TF Serving服务端的性能差强人意。在典型的五层网络(512*256*256*256*128)下,单个广告的预估时延约4800μs,具体见下图:

  • 定制的模型计算实现

由于广告线上服务需要极高的性能,对于主流深度学习模型,我们也定制开发了具体计算实现。这种方式可以针对性的优化,并避免TF Serving不必要的特征转换和线程同步,从而提高服务性能。

例如全连接DNN模型中使用Relu作为激活函数时,我们可以使用滚动数组、剪枝、寄存器和CPU Cache等优化技巧,具体如下:

    // 滚动数组
    int nextLayerIndex = currentLayerIndex ^ 1 ;
    System.arraycopy(bias, bOff, data[nextLayerIndex], 0, nextLayerSize);
    for (int i = 0; i < currentLayerSize; i ++) {
        float value = data[currentLayerIndex][i];
        // 剪枝
        if (value > 0.0) {
            // 寄存器
            int index = wOff + i * nextLayerSize;
            // CPU 缓存友好
            for (int j = 0; j < nextLayerSize; j++) {
                data[nextLayerIndex][j] += value * weights[index + j];
            }
        }
    }
    for (int i = 0; i < nextLayerSize; k++) {
        data[nextArrayIndex][i] = ReLu(data[nextArrayIndex][i]);
    }
    arrayIndex = nextArrayIndex;

优化后的单个广告预估时延约650μs,见下图:

综上,当前线上预估采取“两条腿走路”的策略。利用TF Serving快速实验新的模型结构,以保证迭代效率;一旦模型成熟切换主流量,我们会开发定制实现,以保证线上性能。

模型效果

借助于我们的分层实验平台,我们可以方便的分配流量,完成模型的小流量实验上线。该分层实验平台同时提供了分钟粒度的小流量实时效果数据,便于模型评估和效果监控。

四、总结与展望

经过一段时间的摸索与实践,搜索广告业务在深度学习模型排序上有了一定的成果与积累。接下来,我们将继续在特征、模型、工程角度迭代优化。特征上,更深度挖掘用户意图,刻画上下文场景,并结合DNN模型强大的表达能力充分发挥特征的作用。模型上,探索新的网络结构,并结合CNN、RNN、Attention机制等发挥深度学习模型的优势。持续跟进业界动态,并结合实际场景,应用到业务中。工程上,跟进TensorFlow的新特性,并对目前实际应用中遇到的问题针对性优化,以达到性能与效果的提升。我们在持续探索中。

参考文献

[1] Chapelle, O., Manavoglu, E., & Rosales, R. (2015). Simple and scalable response prediction for display advertising. ACM Transactions on Intelligent Systems and Technology (TIST), 5(4), 61.
[2] Friedman, J. H. (2001). Greedy function approximation: a gradient boosting machine. Annals of statistics, 1189-1232.
[3] Rendle, S. (2010, December). Factorization machines. In Data Mining (ICDM), 2010 IEEE 10th International Conference on (pp. 995-1000). IEEE.
[4] Juan, Y., Zhuang, Y., Chin, W. S., & Lin, C. J. (2016, September). Field-aware factorization machines for CTR prediction. In Proceedings of the 10th ACM Conference on Recommender Systems (pp. 43-50). ACM.
[5] He, X., Pan, J., Jin, O., Xu, T., Liu, B., Xu, T., … & Candela, J. Q. (2014, August). Practical lessons from predicting clicks on ads at facebook. In Proceedings of the Eighth International Workshop on Data Mining for Online Advertising (pp. 1-9). ACM.
[6] Cheng, H. T., Koc, L., Harmsen, J., Shaked, T., Chandra, T., Aradhye, H., … & Anil, R. (2016, September). Wide & deep learning for recommender systems. In Proceedings of the 1st Workshop on Deep Learning for Recommender Systems (pp. 7-10). ACM.
[7] Dougherty, J., Kohavi, R., & Sahami, M. (1995). Supervised and unsupervised discretization of continuous features. In Machine Learning Proceedings 1995 (pp. 194-202).
[8] Guo, H., Tang, R., Ye, Y., Li, Z., & He, X. (2017). Deepfm: A factorization-machine based neural network for CTR prediction. arXiv preprint arXiv:1703.04247.
[9] Ma, X., Zhao, L., Huang, G., Wang, Z., Hu, Z., Zhu, X., & Gai, K. (2018). Entire Space Multi-Task Model: An Effective Approach for Estimating Post-Click Conversion Rate. arXiv preprint arXiv:1804.07931.
[10] Bergstra, J., & Bengio, Y. (2012). Random search for hyper-parameter optimization. Journal of Machine Learning Research, 13(Feb), 281-305.

作者简介

薛欢,2016年3月加入美团,主要从事搜索广告排序模型相关的工作。
姚强,2016年4月加入美团,主要从事搜索广告召回、机制与排序等相关算法研究应用工作。
玉林,2015年5月加入美团,主要从事搜索广告排序相关的工程优化工作。
王新,2017年4月加入美团,主要从事GPU集群管理与深度学习工程优化的工作。

招聘

美团广告平台全面负责美团到店餐饮、到店综合(结婚、丽人、休闲娱乐、学习培训、亲子、家装)、酒店旅游的商业变现。搜索广告基于数亿用户、数百万商家和数千万订单的真实数据做挖掘,在变现的同时确保用户体验和商家利益。欢迎有意向的同学加入搜索广告算法组。
简历请投递至:leijun#meituan.com

原文出自:https://tech.meituan.com/archives

使用TensorFlow训练WDL模型性能问题定位与调优

美团点评阅读(318)

简介

TensorFlow是Google研发的第二代人工智能学习系统,能够处理多种深度学习算法模型,以功能强大和高可扩展性而著称。TensorFlow完全开源,所以很多公司都在使用,但是美团点评在使用分布式TensorFlow训练WDL模型时,发现训练速度很慢,难以满足业务需求。
经过对TensorFlow框架和Hadoop的分析定位,发现在数据输入、集群网络和计算内存分配等层面出现性能瓶颈。主要原因包括TensorFlow数据输入接口效率低、PS/Worker算子分配策略不佳以及Hadoop参数配置不合理。我们在调整对TensorFlow接口调用、并且优化系统配置后,WDL模型训练性能提高了10倍,分布式线性加速可达32个Worker,基本满足了美团点评广告和推荐等业务的需求。

术语

TensorFlow – Google发布的开源深度学习框架
OP – Operation缩写,TensorFlow算子
PS – Parameter Server 参数服务器
WDL – Wide & Deep Learning,Google发布的用于推荐场景的深度学习算法模型
AFO – AI Framework on YARN的简称 – 基于YARN开发的深度学习调度框架,支持TensorFlow,MXNet等深度学习框架

TensorFlow分布式架构简介

为了解决海量参数的模型计算和参数更新问题,TensorFlow支持分布式计算。和其他深度学习框架的做法类似,分布式TensorFlow也引入了参数服务器(Parameter Server,PS),用于保存和更新训练参数,而模型训练放在Worker节点完成。

TensorFlow分布式架构
TensorFlow分布式架构

TensorFlow支持图并行(in-graph)和数据并行(between-graph)模式,也支持同步更新和异步更新。因为in-graph只在一个节点输入并分发数据,严重影响并行训练速度,实际生产环境中一般使用between-graph。
同步更新时,需要一个Woker节点为Chief,来控制所有的Worker是否进入下一轮迭代,并且负责输出checkpoint。异步更新时所有Worker都是对等的,迭代过程不受同步barrier控制,训练过程更快。

AFO架构设计

TensorFlow只是一个计算框架,没有集群资源管理和调度的功能,分布式训练也欠缺集群容错方面的能力。为了解决这些问题,我们在YARN基础上自研了AFO框架解决这个问题。
AFO架构特点:

  • 高可扩展,PS、Worker都是任务(Task),角色可配置
  • 基于状态机的容错设计
  • 提供了日志服务和Tensorboard服务,方便用户定位问题和模型调试
    AFO 架构
    AFO 架构

AFO模块说明:

  • Application Master:用来管理整个TensorFlow集群的资源申请,对任务进行状态监控
  • AFO Child:TensorFlow执行引擎,负责PS、Worker运行时管理和状态同步
  • History Server:管理TensorFlow训练生成的日志
  • AFO Client:用户客户端

WDL模型

在推荐系统、CTR预估场景中,训练的样本数据一般是查询、用户和上下文信息,系统返回一个排序好的候选列表。推荐系统面临的主要问题是,如何同时可以做到模型的记忆能力和泛化能力,WDL提出的思想是结合线性模型(Wide,用于记忆)和深度神经网络(Deep,用于泛化)。
以论文中用于Google Play Store推荐系统的WDL模型为例,该模型输入用户访问应用商店的日志,用户和设备的信息,给应用App打分,输出一个用户“感兴趣”App列表。

Wide & Deep 模型网络
WDL 模型网络

其中,installed apps和impression apps这类特征具有稀疏性(在海量大小的App空间中,用户感兴趣的只有很少一部分),对应模型“宽的部分”,适合使用线性模型;在模型“深的部分”,稀疏特征由于维度太高不适合神经网络处理,需要embedding降维转成稠密特征,再和其他稠密特征串联起来,输入到一个3层ReLU的深度网络。最后Wide和Deep的预估结果加权输入给一个Logistic损失函数(例如Sigmoid)。
WDL模型中包含对稀疏特征的embedding计算,在TensorFlow中对应的接口是tf.embedding_lookup_sparse,但该接口所包含的OP无法使用GPU加速,只能在CPU上计算,因此TensorFlow在处理稀疏特征性能不佳。不仅如此,我们发现分布式TensorFlow在进行embedding计算时会引发大量的网络传输流量,严重影响训练性能。

性能瓶颈分析与调优

在使用TensorFlow训练WDL模型时,我们主要发现3个性能问题:

  1. 每轮训练时,输入数据环节耗时过多,超过60%的时间用于读取数据。
  2. 训练时产生的网络流量高,占用大量集群网络带宽资源,难以实现分布式性能线性加速。
  3. Hadoop的默认参数配置导致glibc malloc变慢,一个保护malloc内存池的内核自旋锁成为性能瓶颈。

TensorFlow输入数据瓶颈

TensorFlow支持以流水线(Pipeline)的方式输入训练数据。如下图所示,典型的输入数据流水线包含两个队列:Filename Queue对一组文件做shuffle,多个Reader线程从此队列中拿到文件名,读取训练数据,再经过Decode过程,将数据放入Example Queue,以备训练线程从中读取数据。Pipeline这种多线程、多队列的设计可以使训练线程和读数据线程并行。理想情况下,队列Example Queue总是充满数据的,训练线程完成一轮训练后可以立即读取下一批的数据。如果Example Queue总是处于“饥饿”状态,训练线程将不得不阻塞,等待Reader线程将Example Queue插入足够的数据。使用TensorFlow Timeline工具,可以直观地看到其中的OP调用过程。

TensorFlow输入数据流水线
TensorFlow输入数据流水线

使用Timeline,需要对tf.Session.run()增加如下几行代码:

with tf.Session as sess:
    ptions = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
    run_metadata = tf.RunMetadata()
    _ = sess.run([train_op, global_step], options=run_options, run_metadata=run_metadata)
    if global_step > 1000 && global_step < 1010:
        from tensorflow.python.client import timeline
        fetched_timeline = timeline.Timeline(run_metadata.step_stats)
        chrome_trace = fetched_timeline.generate_chrome_trace_format()
        with open('/tmp/timeline_01.json', 'w') as f:
            f.write(chrome_trace)

这样训练到global step在1000轮左右时,会将该轮训练的Timeline信息保存到timeline_01.json文件中,在Chrome浏览器的地址栏中输入chrome://tracing,然后load该文件,可以看到图像化的Profiling结果。
业务模型的Timeline如图所示:

Timeline显示数据输入是性能瓶颈
Timeline显示数据输入是性能瓶颈

可以看到QueueDequeueManyV2这个OP耗时最久,约占整体时延的60%以上。通过分析TensorFlow源码,我们判断有两方面的原因:
(1)Reader线程是Python线程,受制于Python的全局解释锁(GIL),Reader线程在训练时没有获得足够的调度执行;
(2)Reader默认的接口函数TFRecordReader.read函数每次只读入一条数据,如果Batch Size比较大,读入一个Batch的数据需要频繁调用该接口,系统开销很大;
针对第一个问题,解决办法是使用TensorFlow Dataset接口,该接口不再使用Python线程读数据,而是用C++线程实现,避免了Python GIL问题。
针对第二个问题,社区提供了批量读数据接口TFRecordReader.read_up_to,能够指定每次读数据的数量。我们设置每次读入1000条数据,使读数句接口被调用的频次从10000次降低到10次,每轮训练时延降低2-3倍。

优化数据输入使性能提升2-3倍
优化数据输入使性能提升2-3倍

可以看到经过调优后,QueueDequeueManyV2耗时只有十几毫秒,每轮训练时延从原来的800多毫秒降低至不到300毫秒。

集群网络瓶颈

虽然使用了Mellanox的25G网卡,但是在WDL训练过程中,我们观察到Worker上的上行和下行网络流量抖动剧烈,幅度2-10Gbps,这是由于打满了PS网络带宽导致丢包。因为分布式训练参数都是保存和更新都是在PS上的,参数过多,加之模型网络较浅,计算很快,很容易形成多个Worker打一个PS的情况,导致PS的网络接口带宽被打满。
在推荐业务的WDL模型中,embedding张量的参数规模是千万级,TensorFlow的tf.embedding_lookup_sparse接口包含了几个OP,默认是分别摆放在PS和Worker上的。如图所示,颜色代表设备,embedding lookup需要在不同设备之前传输整个embedding变量,这意味着每轮Embedding的迭代更新需要将海量的参数在PS和Worker之间来回传输。

embedding_lookup_sparse
embedding_lookup_sparse的OP拓扑图

有效降低网络流量的方法是尽量让参数更新在一个设备上完成,即

with tf.device(PS):
    do embedding computing

社区提供了一个接口方法正是按照这个思想实现的:embedding_lookup_sparse_with_distributed_aggregation接口,该接口可以将embedding计算的所使用的OP都放在变量所在的PS上,计算后转成稠密张量再传送到Worker上继续网络模型的计算。
从下图可以看到,embedding计算所涉及的OP都是在PS上,测试Worker的上行和下行网络流量也稳定在2-3Gpbs这一正常数值。

embedding_lookup_sparse_with_distributed_aggregation
embedding_lookup_sparse_with_distributed_aggregation的OP拓扑图

PS上的UniqueOP性能瓶颈

在使用分布式TensorFlow 跑广告推荐的WDL算法时,发现一个奇怪的现象:WDL算法在AFO上的性能只有手动分布式的1/4。手动分布式是指:不依赖YARN调度,用命令行方式在集群上分别启动PS和Worker作业。
使用Perf诊断PS进程热点,发现PS多线程在竞争一个内核自旋锁,PS整体上有30%-50%的CPU时间耗在malloc的在内核的spin_lock上。

Perf诊断PS计算瓶颈
Perf诊断PS计算瓶颈

进一步查看PS进程栈,发现竞争内核自旋锁来自于malloc相关的系统调用。WDL的embedding_lookup_sparse会使用UniqueOp算子,TensorFlow支持OP多线程,UniqueOp计算时会开多线程,线程执行时会调用glibc的malloc申请内存。
经测试排查,发现Hadoop有一项默认的环境变量配置:

export MALLOC_ARENA_MAX="4"

该配置意思是限制进程所能使用的glibc内存池个数为4个。这意味着当进程开启多线程调用malloc时,最多从4个内存池中竞争申请,这限制了调用malloc的线程并行执行数量最多为4个。
翻查Hadoop社区相关讨论,当初增加这一配置的主要原因是:glibc的升级带来多线程ARENA的特性,可以提高malloc的并发性能,但同时也增加进程的虚拟内存(即top结果中的VIRT)。YARN管理进程树的虚拟内存和物理内存使用量,超过限制的进程树将被杀死。将MALLOC_ARENA_MAX的默认设置改为4之后,可以不至于VIRT增加很多,而且一般作业性能没有明显影响。
但这个默认配置对于WDL深度学习作业影响很大,我们去掉了这个环境配置,malloc并发性能极大提升。经过测试,WDL模型的平均训练时间性能减少至原来的1/4。

调优结果

注意:以下测试都去掉了Hadoop MALLOC_ARENA_MAX的默认配置

我们在AFO上针对业务的WDL模型做了性能调优前后的比对测试,测试环境参数如下:
模型:推荐广告模型WDL
OS:CentOS 7.1
CPU: Xeon E5 2.2G, 40 Cores
GPU:Nvidia P40
磁盘: Local Rotational Disk
网卡:Mellanox 25G(未使用RoCE)
TensorFlow版本:Release 1.4
CUDA/cuDNN: 8.0/5.1

Wide & Deep 模型网络
分布式线性加速效果

可以看到调优后,训练性能提高2-3倍,性能可以达到32个GPU线性加速。这意味着如果使用同样的资源,业务训练时间会更快,或者说在一定的性能要求下,资源节省更多。如果考虑优化MALLOC_ARENA_MAX的因素,调优后的训练性能提升约为10倍左右。

总结

我们使用TensorFlow训练WDL模型发现一些系统上的性能瓶颈点,通过针对性的调优不仅可以大大加速训练过程,而且可以提高GPU、带宽等资源的利用率。在深入挖掘系统热点瓶颈的过程中,我们也加深了对业务算法模型、TensorFlow框架的理解,具有技术储备的意义,有助于我们后续进一步优化深度学习平台性能,更好地为业务提供工程技术支持。

作者简介

郑坤,美团点评技术专家,2015年加入美团点评,负责深度学习平台、Docker平台的研发工作。

招聘

美团点评GPU计算团队,致力于打造公司一体化的深度学习基础设施平台,涉及到的技术包括:资源调度、高性能存储、高性能网络、深度学习框架等。目前平台还在建设中期,不论在系统底层、分布式架构、算法工程优化上都有很大的挑战!
诚邀对这个领域感兴趣的同学加盟,不论是工程背景,还是算法背景我们都非常欢迎。
有兴趣的同学可以发送简历到zhengkun@meituan.com。

原文出自:https://tech.meituan.com/archives

高可用性系统在大众点评的实践与经验

美团点评阅读(3222)

所谓高可用性指的是系统如何保证比较高的服务可用率,在出现故障时如何应对,包括及时发现、故障转移、尽快从故障中恢复等等。本文主要以点评的交易系统的演进为主来描述如何做到高可用,并结合了一些自己的经验。需要强调的是,高可用性只是一个结果,应该更多地关注迭代过程,关注业务发展。

可用性的理解

理解目标

业界高可用的目标是几个9,对于每一个系统,要求是不一样的。研发人员对所设计或者开发的系统,要知道用户规模及使用场景,知道可用性的目标。
比如,5个9的目标对应的是全年故障5分钟。

result

拆解目标

几个9的目标比较抽象,需要对目标进行合理的分解,可以分解成如下两个子目标。

频率要低:减少出故障的次数

不出问题,一定是高可用的,但这是不可能的。系统越大、越复杂,只能尽量避免问题,通过系统设计、流程机制来减少出问题的概率。但如果经常出问题,后面恢复再快也是没有用的。

时间要快:缩短故障的恢复时间

故障出现时,不是解决或者定位到具体问题,而是快速恢复是第一要务的,防止次生灾害,问题扩大。这里就要求要站在业务角度思考,而不仅是技术角度思考。
下面,我们就按这两个子目标来分别阐述。

频率要低:减少出故障的次数

设计:根据业务变化不断进行迭代

以点评交易系统的演进过程为例。

幼儿时期:2012年前

使命:满足业务要求,快速上线。
因为2011年要快速地把团购产品推向市场,临时从各个团队抽取的人才,大部分对.NET更熟悉,所以使用.NET进行了第一代的团购系统设计。毕竟满足业务要求是第一的,还没有机会遇到可用性等质量问题。考虑比较简单,即使都挂了,量也比较小,出现问题,重启、扩容、回滚就解决问题了。
系统架构如下图所示。

result

少年时期:垂直拆分(2012-2013)

使命:研发效率&故障隔离。

当2012年在团单量从千到万量级变化,用户每日的下单量也到了万级时候,需要考虑的是迭代速度、研发效率。垂直拆分,有助于保持小而美的团队,研发效率才能更高。另外一方面也需要将各个业务相互隔离,比如商品首页的展示、商品详情页的展示,订单、支付流程的稳定性要求不一样。前面可以缓存,可以做静态化来保证可用性,提供一些柔性体验。后面支付系统做异地容灾,比如我们除了南汇机房支付系统,在宝山机房也部署了,只是后来发现这个系统演进太快,没有工具和机制保证双机房更新,所以后来也不好使用了。
系统演进如下图所示。服务垂直化了,但是数据没有完整隔离开,服务之间还需要互相访问非自己的数据。

result

青年时期:服务做小,不共享数据(2014-2015)

使命:支撑业务快速发展,提供高效、高可用的技术能力。

从2013年开始,Deal-service (商品系统)偶尔会因为某一次大流量(大促或者常规活动)而挂掉,每几个月总有那么一次,基本上可用性就在3个9徘徊。这里订单和支付系统很稳定,因为流量在商品详情页到订单有一个转化率,流量大了详情页就挂了,订单也就没有流量了。后来详情页的静态化比较好了,能减少恢复的速度,能降级,但是Deal-service的各个系统依赖太深了,还是不能保证整体端到端的可用性。
所以2014年对Deal-service做了很大的重构,大系统做小,把商品详情系统拆成了无数小服务,比如库存服务、价格服务、基础数据服务等等。这下商品详情页的问题解决了,后面压力就来了,订单系统的压力增大。2014年10月起,订单系统、支付系统也启动了全面微服务化,经过大约1年的实践,订单系统、促销系统、支付系统这3个领域后面的服务总和都快上百个了,后面对应的数据库20多个,这样能支撑到每日订单量百万级。
业务的增长在应用服务层面是可以扩容的,但是最大的单点——数据库是集中式的,这个阶段我们主要是把应用的数据访问在读写上分离,数据库提供更多的从库来解决读的问题,但是写入仍然是最大的瓶颈(MySQL的读可以扩展,而写入QPS也就小2万)。
这时系统演变成如下图所示。这个架构大约能支撑QPS 3000左右的订单量。

result

成年时期:水平拆分(2015至今)

使命:系统要能支撑大规模的促销活动,订单系统能支撑每秒几万的QPS,每日上千万的订单量。

2015年的917吃货节,流量最高峰,如果我们仍然是前面的技术架构,必然会挂掉。所以在917这个大促的前几个月,我们就在订单系统进行了架构升级和水平拆分,核心就是解决数据单点,把订单表拆分成了1024张表,分布在32个数据库,每个库32张表。这样在可见的未来都不用太担心了。
虽然数据层的问题解决了,但是我们还是有些单点,比如我们用的消息队列、网络、机房等。举几个我过去曾经遇到的不容易碰到的可用性问题:
服务的网卡有一个坏了,没有被监测到,后来发现另一个网卡也坏了,这样服务就挂了。
我们使用 cache的时候发现可用性在高峰期非常低,后来发现这个cache服务器跟公司监控系统CAT服务器在一个机柜,高峰期的流量被CAT占了一大半,业务的网络流量不够了。
917大促的时候我们对消息队列这个依赖的通道能力评估出现了偏差,也没有备份方案,所以造成了一小部分的延迟。
这个时期系统演进为下图这样:

result

未来:思路仍然是大系统做小,基础通道做大,流量分块

大系统做小,就是把复杂系统拆成单一职责系统,并从单机、主备、集群、异地等架构方向扩展。
基础通道做大就是把基础通信框架、带宽等高速路做大。
流量分块就是把用户流量按照某种模型拆分,让他们聚合在某一个服务集群完成,闭环解决。
系统可能会演进为下图这样:

result

上面点评交易系统的发展几个阶段,只以业务系统的演进为例。除了这些还有CDN、DNS、网络、机房等各个时期遇到的不同的可用性问题,真实遇到过的就有:联通的网络挂了,需要切换到电信;数据库的电源被人踢掉了,等等。

易运营

高可用性的系统一定是可运营的。听到运营,大家更多想到的是产品运营,其实技术也有运营——线上的质量、流程的运营,比如,整个系统上线后,是否方便切换流量,是否方便开关,是否方便扩展。这里有几个基本要求:

可限流

线上的流量永远有想不到的情况,在这种情况下,系统的稳定吞吐能力就非常重要了,高并发的系统一般采取的策略是快速失败机制,比如系统QPS能支撑5000,但是1万的流量过来,我能保证持续的5000,其他5000我快速失败,这样很快1万的流量就被消化掉了。比如917的支付系统就是采取了流量限制,如果超过某一个流量峰值,我们就自动返回“请稍后再试”等。

无状态

应用系统要完全无状态,运维才能随便扩容、分配流量。

降级能力

降级能力是跟产品一起来看的,需要看降级后对用户体验的影响。简单的比如:提示语是什么。比如支付渠道,如果支付宝渠道挂了,我们挂了50% ,支付宝旁边会自动出现一个提示,表示这个渠道可能不稳定,但是可以点击;当支付宝渠道挂了100% ,我们的按钮变成灰色的,不能点击,但也会有提示,比如换其他支付渠道(刚刚微信支付还挂了,就又起作用了)。另一个案例,我们在917大促的时候对某些依赖方,比如诚信的校验,这种如果判断比较耗资源,又可控的情况下,可以通过开关直接关闭或者启用。

result

可测试

无论架构多么完美,验证这一步必不可少,系统的可测试性就非常重要。
测试的目的要先预估流量的大小,比如某次大促,要跟产品、运营讨论流量的来源、活动的力度,每一张页面的,每一个按钮的位置,都要进行较准确的预估。
此外还要测试集群的能力。有很多同学在实施的时候总喜欢测试单台,然后水平放大,给一个结论,但这不是很准确,要分析所有的流量在系统间流转时候的比例。尤其对流量模型的测试(要注意高峰流量模型跟平常流量模型可能不一致)系统架构的容量测试,比如我们某一次大促的测试方法
从上到下评估流量,从下至上评估能力:发现一次订单提交有20次数据库访问,读写比例高峰期是1:1,然后就跟进数据库的能力倒推系统应该放入的流量,然后做好前端的异步下单,让整个流量平缓地下放到数据库。

result

降低发布风险

严格的发布流程

目前点评的发布都是开发自己负责,通过平台自己完成的。上线的流程,发布的常规流程模板如下:

result

灰度机制

服务器发布是分批的,按照10%、30%、50%、100%的发布,开发人员通过观察监控系统的曲线及系统的日志,确定业务是否正常。
线上的流量灰度机制,重要功能上线能有按照某种流量灰度上线能力。
可回滚是标配,最好有最坏情况的预案。

时间要快:缩短故障的恢复时间

如果目标就要保证全年不出故障或者出了故障在5分钟之内能解决,要对5分钟进行充分的使用。5分钟应该这样拆解:1分钟发现故障,3分钟定位故障出现在哪个服务,再加上后面的恢复时间。就是整个时间的分解,目前我们系统大致能做到前面2步,离整体5个9的目标还有差距,因为恢复的速度跟架构的设计,信息在开发、运维、DBA之间的沟通速度及工具能力,及处理问题人员的本身能力有关。
生命值:

result

持续关注线上运行情况

熟悉并感知系统变化,要快就要熟,熟能生巧,所以要关注线上运营情况。
了解应用所在的网络、服务器性能、存储、数据库等系统指标。
能监控应用的执行状态,熟悉应用自己的QPS、响应时间、可用性指标,并对依赖的上下游的流量情况同样熟悉。
保证系统稳定吞吐
系统如果能做好流量控制、容错,保证稳定的吞吐,能保证大部分场景的可用,也能很快地消化高峰流量,避免出现故障,产生流量的多次高峰。
故障时

快速的发现机制

告警的移动化

系统可用性的告警应该全部用微信、短信这种能保证找到人的通信机制。

告警的实时化

目前我们只能做到1分钟左右告警。

监控的可视化

我们系统目前的要求是1分钟发现故障,3分钟定位故障。这就需要做好监控的可视化,在所有关键service里面的方法层面打点,然后做成监控曲线,不然3分钟定位到具体是哪个地方出问题,比较困难。点评的监控系统CAT能很好的提供这些指标变化,我们系统在这些基础上也做了一些更实时的能力,比如订单系统QPS就是秒级的监控曲线。

result

有效的恢复机制

比如运维的四板斧:回滚、重启、扩容、下服务器。在系统不是很复杂、流量不是很高的情况下,这能解决问题,但大流量的时候就很难了,所以要更多地从流量控制、降级体验方面下功夫。

几点经验

珍惜每次真实高峰流量,建立高峰期流量模型。

因为平常的压力测试很难覆盖到各种情况,而线上的真实流量能如实地反映出系统的瓶颈,能较真实地评估出应用、数据库等在高峰期的表现。

珍惜每次线上故障复盘,上一层楼看问题,下一层楼解决问题。

线上出问题后,要有一套方法论来分析,比如常见的“5W”,连续多问几个为什么,然后系统思考解决方案,再逐渐落地。

可用性不只是技术问题。

系统初期:以开发为主;
系统中期:开发+DBA+运维为主;
系统后期:技术+产品+运维+DBA。
系统较简单、量较小时,开发同学能比较容易地定位问题并较容易解决问题。
当系统进入较复杂的中期时,就需要跟运维、数据库的同学一起来看系统的瓶颈。
当系统进入复杂的后期时,系统在任何时候都要考虑不可用的时候如何提供柔性体验,这就需要从产品角度来思考。

单点和发布是可用性最大的敌人。

可用性要解决的核心问题就是单点,比如常见的手段:垂直拆分、水平拆分、灰度发布;单机到主备、集群、异地容灾等等。
另外,系统发布也是引起系统故障的关键点,比如常见的系统发布、数据库维护等其他引起系统结构变化的操作。

原文出自:https://tech.meituan.com/archives

OpenTSDB 造成 Hbase 整点压力过大问题的排查和解决

美团点评阅读(1697)

业务背景

OpenTSDB 是一款非常适合存储海量时间序列数据的开源软件,使用 HBase 作为存储让它变的非常容易扩展。我们在建设美团性能监控平台的过程中,每天需要处理数以亿计的数据,经过几番探索和调研,最终选取了 OpenTSDB 作为数据存储层的重要组件。OpenTSDB 的安装和配置过程都比较简单,但是在实际的业务应用中,还是会出现这样那样的问题,本文详细介绍我们在OpenTSDB 实际使用过程中遇到的 HBase 整点压力过大的问题,期望对大家有些参考意义。

问题的出现

性能监控平台使用 OpenTSDB 负责存储之后不久(创建的表名称是 tsdb-perf),数据平台组的同事发现,tsdb-perf 这个表在最近这段时间每天上午 10 点左右有大量的读操作,造成 HBase 集群压力过大,但是想去分析问题的时候发现读操作又降为 0 了,为了避免类似情况未来突然发生,需要我来排查下原因。

于是我就想:性能监控平台目前只是个内部系统,用户使用量不大,并且只有在用户需要查看数据时去查询,数据读取量不应该造成 HBase 的压力过大。

重现问题

如果要解决这个问题,稳定重现是个必要条件,根据数据平台组同事的反馈,我们做了更详细的监控,每隔两分钟采集性能监控平台所用的 HBase 集群的读操作数量,发现是下面的变化趋势:

13:00:05    0
13:02:01    66372
13:04:01    96746
13:06:02    101784
13:08:01    99254
13:10:02    2814
13:12:01    93668
13:14:02    93224
13:16:02    90118
13:18:02    11376
13:20:01    85134
13:22:01    81880
13:24:01    80916
13:26:01    77694
13:28:02    76312
13:30:01    73310
13:32:02    0
13:34:01    0
13:36:01    0
13:38:02    0
13:40:01    0
13:42:02    0
13:44:01    0
13:46:02    0
13:48:01    0
13:50:02    0
13:52:01    0
13:54:02    0
13:56:01    0
13:58:02    0
14:00:01    0
14:02:01    36487
14:04:01    43946
14:06:01    53002
14:08:02    51598
14:10:01    54914
14:12:02    95784
14:14:04    53866
14:16:02    54868
14:18:01    54122
14:20:04    0
14:22:01    0
14:24:02    0
14:26:01    0
14:28:01    0
14:30:01    0
14:32:02    0
14:34:01    0

从图上不难看出,每到整点开始 tsdb-perf 这个表的读操作飚的很高,大约持续半个小时,之后恢复到 0 。到下个整点又出现类似的问题,并没有像数据平台组同事观察到的突然回复正常了,可能他们连续两次观察的时间点刚好错开了。

于是,真正的问题就变成了:OpenTSDB 到 HBase 的读操作每到整点开始飚的很高,持续大约半小时后回复正常,这种类脉冲式的流量冲击会给 HBase 集群的稳定性带来负面影响。

定位问题所在

事出反常必有妖,OpenTSDB 到 HBase 的大量读操作肯定伴随很大的网络流量,因为两者用 HTTP 通信,我们得仔细梳理下可能造成这种情况的几种原因。性能监控平台的架构图如下:

性能平台架构图

从架构图可以看出,只有数据聚合服务和报表系统会和 OpenTSDB 交互,聚合服务向里面写数据,报表系统从里面读数据。然后 OpenTSDB 负责把数据发送到 HBase 中。从数据流动的方向来讲,有可能是报表系统导致了大量的读操作,也有可能是聚合服务里面存在不合理的读请求,也有可能是 OpenTSDB 本身存在缺陷。

首先排除的是报表系统导致的大量读操作,因为只会在用户查看某些报表时才会从 OpenTSDB 读取数据,目前报表系统每天的访问量也才几百,不足以造成如此大的影响。

其次,如何确认是否是聚合服务导致了大量的读请求呢?可以从网络流量的视角来分析,如果聚合服务到 OpenTSDB 的流量过大,完全有可能导致 OpenTSDB 到 HBase 的过大流量,但是由于目前聚合服务和 TSDB 写实例是部署在相同的机器上,无法方便的统计到网络流量的大小,于是我们把聚合服务和 TSDB 写实例分开部署,得到下面的流量统计图:

网络流量图

聚合服务只接收来自解析服务的数据包计算完毕之后发送给 TSDB,其网络流量如下图:

网络流量图

TSDB 服务只接收来自聚合服务的数据,然后发送到 HBase,却出现了脉冲式的冲高回落,网络流量如下图:

网络流量图

这样,就可以排除聚合服务造成的问题,出问题的地方就在 OpenTSDB 和 HBase 集群之间,其他业务线并没有造成 HBase 的压力过大,看来问题应该出在 OpenTSDB 里面,如果这个问题是 OpenTSDB 内部存在的,那么其他使用了 OpenTSDB 的系统肯定也存在类似的问题,下面是另外一个组部署的 OpenTSDB 的机器的网络流量图(注意,这台机器上只部署了 OpenTSDB 服务):

网络流量图

这让我更加确信问题是在 OpenTSDB 内部,也就是它的工作机制导致了这种问题。

查找问题原因

于是我先后查阅了 OpenTSDB 的官方文档和 Google Group 讨论组里的大部分帖子,还下载来了 OpenTSDB 的源代码,探个究竟,另外在从读操作从 0 到暴涨的过程中仔细盯着 OpenTSDB 的 stat 页面特别关注下面红色方框中的几个指标:

网络流量图

让我感觉比较诡异的是,与大量读操作同时发生的还有大量的删除操作,官方文档上的这段话很好的解释了我的疑惑:

If compactions have been enabled for a TSD, a row may be compacted after it’s base hour has passed or a query has run over the row. Compacted columns simply squash all of the data points together to reduce the amount of overhead consumed by disparate data points. Data is initially written to individual columns for speed, then compacted later for storage efficiency. Once a row is compacted, the individual data points are deleted. Data may be written back to the row and compacted again later.

这段话很好的解释了 OpenTSDB 的 Compaction 机制的工作原理,OpenTSDB 内部的工作原理比这个更复杂,下面我说下我通俗的理解:

  • 为了节省存储空间和提高数据读取速度,OpenTSDB 内部有个数据压缩(即 Compaction)的机制,将设定的某个时间段内某个指标的所有数据压缩成单行,重新写到 HBase;
  • OpenTSDB 运行时默认把收到的数据(原始数据点)每秒1次的速度批量写到 HBase 上,然后会周期性的触发上面提到的数据压缩机制,把原始数据点拿出来,压缩后重新写回HBase,然后把原始数据点删除,这就虽然我们只往 OpenTSDB 写数据但大量的读和删操作还是会发生的原因;
  • OpenTSDB 默认的配置是以 3600 秒为区间压缩,实际运行时就是整点触发,这样整点发生的大量读、删操作就可以解释了;

至此,线上 OpenTSDB 实例整点大量读操作造成 HBase 集群压力过大的问题原因基本明了。

如何解决问题

找到问题的原因之后,我们想到了以下 2 种解决方案:

  • 禁用 OpenTSDB 的 Compaction 机制,这样 OpenTSDB 就变成了 1 个纯粹的写实例,数据读取速度会有牺牲,因为每次读取需要扫描更多的数据,这个对于业务数据量很大的系统来说可能并不合适;
  • 想办法让 OpenTSDB 的数据压缩过程缓慢进行,这样到 HBase 的流量压力就会平缓很多,但是这样做还是有风险,因为如果从业务系统到 OpenTSDB 的流量暴涨仍然有可能会 HBase 压力过大,不过这就是另外1个问题了,HBase 需要扩容;

实际操作过程中,我们使用了第 2 种方案,修改 OpenTSDB 的源代码中 src/core/CompactionQueue.java 中的 FLUSH_SPEED 常量为 1,重新编译即可。这样改动的实际影响是:默认压缩速度是 2 倍速,即最多半个小时内完成前 1 个小时数据的压缩,重新写回到 HBase,可以把这个调成 1 倍速,给它 1 个小时的时间来完成前 1 个小时数据的 Compaction,这样到 HBase 的流量会平缓很多。

经验和教训

几经辗转,终于找到问题原因所在(离解决问题还有距离),下面是我的几点感受:

  • 解决问题之前,要能够稳定重现,找到真正的问题所在,不能停留在表面,如果不进行几个小时的 HBase 读操作监控,不会发现整点暴涨持续半小时然后回落的问题;
  • 系统的运行环境很复杂,必要的时候要想办法把问题隔离到影响因素更少的环境中,更容易发现问题,比如性能监控平台各组件的混合部署给机器间的流量分析造成了困难;
  • 使用开源软件,最好能深入了解下运行机制,用起来才得心应手,不然出了问题就很麻烦,这次的排查过程让我更加详细的了解了 OpenTSDB 的运行机制;

至此,本文完~

原文出自:https://tech.meituan.com/archives

Docker系列之一:入门介绍

美团点评阅读(1712)

Docker简介

Docker是DotCloud开源的、可以将任何应用包装在Linux container中运行的工具。2013年3月发布首个版本,当前最新版本为1.3。Docker基于Go语言开发,代码托管在Github上,目前超过10000次commit。基于Docker的沙箱环境可以实现轻型隔离,多个容器间不会相互影响;Docker可以自动化打包和部署任何应用,方便地创建一个轻量级私有PaaS云,也可以用于搭建开发测试环境以及部署可扩展的web应用等。

Docker vs VM

从下图可以看出,VM是一个运行在宿主机之上的完整的操作系统,VM运行自身操作系统会占用较多的CPU、内存、硬盘资源。Docker不同于VM,只包含应用程序以及依赖库,基于libcontainer运行在宿主机上,并处于一个隔离的环境中,这使得Docker更加轻量高效,启动容器只需几秒钟之内完成。由于Docker轻量、资源占用少,使得Docker可以轻易的应用到构建标准化的应用中。但Docker目前还不够完善,比如隔离效果不如VM,共享宿主机操作系统的一些基础库等;网络配置功能相对简单,主要以桥接方式为主;查看日志也不够方便灵活。

docker与vm比较

另外,IBM发表了一篇关于虚拟机和Linux container性能对比的论文,论文中实际测试了虚拟机和Linux container在CPU、内存、存储IO以及网络的负载情况,结果显示Docker容器本身几乎没有什么开销,但是使用AUFS会一定的性能损耗,不如使用Docker Volume,Docker的NAT在较高网络数据传输中会引入较大的工作负载,带来额外的开销。不过container的性能与native相差不多,各方面的性能都一般等于或者优于虚拟机。Container和虚拟机在IO密集的应用中都需要调整优化以更好的支持IO操作,两者在IO密集型的应用中都应该谨慎使用。

Docker Component

docker组成

Docker是CS架构,主要由下面三部分组成:

  • Docker daemon: 运行在宿主机上,Docker守护进程,用户通过Docker client(Docker命令)与Docker daemon交互
  • Docker client: Docker 命令行工具,是用户使用Docker的主要方式,Docker client与Docker daemon通信并将结果返回给用户,Docker client也可以通过socket或者RESTful api访问远程的Docker daemon
  • Docker hub/registry: 共享和管理Docker镜像,用户可以上传或者下载上面的镜像,官方地址为https://registry.hub.docker.com/,也可以搭建自己私有的Docker registry

了解了Docker的组成,再来了解一下Docker的两个主要概念:

  • Docker image:镜像是只读的,镜像中包含有需要运行的文件。镜像用来创建container,一个镜像可以运行多个container;镜像可以通过Dockerfile创建,也可以从Docker hub/registry上下载。
  • Docker container:容器是Docker的运行组件,启动一个镜像就是一个容器,容器是一个隔离环境,多个容器之间不会相互影响,保证容器中的程序运行在一个相对安全的环境中。

Docker网络

Docker的网络功能相对简单,没有过多复杂的配置,Docker默认使用birdge桥接方式与容器通信,启动Docker后,宿主机上会产生docker0这样一个虚拟网络接口, docker0不是一个普通的网络接口, 它是一个虚拟的以太网桥,可以为绑定到docker0上面的网络接口自动转发数据包,这样可以使容器与宿主机之间相互通信。每次Docker创建一个容器,会产生一对虚拟接口,在宿主机上执行ifconfig,会发现多了一个类似veth****这样的网络接口,它会绑定到docker0上,由于所有容器都绑定到docker0上,容器之间也就可以通信。

在宿主机上执行ifconfig,会看到docker0这个网络接口, 启动一个container,再次执行ifconfig, 会有一个类似veth****的interface,每个container的缺省路由是宿主机上docker0的ip,在container中执行netstat -r可以看到如下图所示内容:
container路由

容器中的默认网关跟docker0的地址是一样的:
docker0
当容器退出之后,veth*虚拟接口也会被销毁。

除bridge方式,Docker还支持host、container、none三种网络通信方式,使用其它通信方式,只要在Docker启动时,指定–net参数即可,比如:

docker run -i -t  --net=host ubuntu /bin/bash

host方式可以让容器无需创建自己的网络协议栈,而直接访问宿主机的网络接口,在容器中执行ip addr会发现与宿主机的网络配置是一样的,host方式让容器直接使用宿主机的网络接口,传输数据的效率会更加高效,避免bridge方式带来的额外开销,但是这种方式也可以让容器访问宿主机的D-bus等网络服务,可能会带来意想不到的安全问题,应谨慎使用host方式;container方式可以让容器共享一个已经存在容易的网络配置; none方式不会对容器的网络做任务配置,需要用户自己去定制。

Docker 使用

首先要在宿主机上安装Docker,Docker安装参考官方安装文档
Docker命令也比较类似Git,支持push以及pull操作上传以及下载Docker镜像。
查看当前Docker的版本

docker version

查看当前系统Docker信息

docker info

查看宿主机上的镜像,Docker镜像保存在/var/lib/docker目录下:

docker images

从Docker hub上下载某个镜像:

docker pull ubuntu:latest
docker pull ubuntu:latest

执行docker pull ubuntu会将Ubuntu这个仓库下面的所有镜像下载到本地repository。

启动一个容器使用docker run:

docker run -i -t ubuntu /bin/bash                       启动一个容器
docker run -i -t --rm ubuntu /bin/bash                  --rm表示容器退出后立即删除该容器
docker run -t -i --name test_container ubuntu /bin/bash --name指定容器的名称,否则会随机分配一个名称
docker run -t -i --net=host ubuntu /bin/bash            --net=host容器以Host方式进行网络通信
docker run -t -i -v /host:/container ubuntu /bin/bash   -v绑定挂在一个Volume,在宿主机和Docker容器中共享文件或目录

查看当前有哪些容器正在运行,使用docker ps:

xzs@host:~(0)$ docker ps
CONTAINER ID     IMAGE                COMMAND        CREATED         STATUS          PORTS    NAMES
50a1261f7a8b     docker_test:latest   "/bin/bash"    7 seconds ago   Up 6 seconds             sleepy_ptolemy
#目前只有一个container id为50a1261f7a8b的容器正在运行

启动或停止某个container使用docker start/stop container_id:

xzs@host:~(0)$ docker stop 50a1261f7a8b
50a1261f7a8b

xzs@host:~(0)$ docker ps -a | grep 50a1261f7a8b
50a1261f7a8b   docker_test:latest   "/bin/bash"   2 minutes ago   Exited (0) 14 seconds ago   sleepy_ptolemy
#执行docker stop后,该容器的状态变更为Exited

使用docker commit可以将container的变化作为一个新的镜像,比如:

xzs@host:~(0)$ docker commit -m="test docker commit" 50a1261f7a8b docker_test
55831c956ebf46a1f9036504abb1b29d7e12166f18f779cccce66f5dc85de38e

xzs@host:~(0)$ docker images | grep docker_test
docker_test                            latest              55831c956ebf        10 seconds ago      290.7 MB

除了从Docker hub上下载镜像,也可以写Dockerfile创建一个镜像,以创建一个Django程序为例,Dockerfile如下所示:

xzs@host:/tmp/docker(0)$ cat Dockerfile
FROM ubuntu:12.04
MAINTAINER Your Name

RUN apt-get update
RUN apt-get install -y python-software-properties python-pip

ADD myproject /opt/code

RUN pip install -r /opt/code/requirement.txt

写完Dockerfile,在Dockerfile所在目录执行docker build创建镜像:

docker build -t docker_test .
docker run -i -t docker_test /bin/bash -c "cd /opt/code;python manage.py runserver 0.0.0.0:8080"

将制作的镜像上传到private registry:

docker tag test docker.example.com/test
docker push docker.example.com/test

经过长时间使用,主机上存储了很多已无用的镜像,想将它们删除则用docker rm或者docker rmi,比如:

docker rm container_id
docker rmi image_id

Docker生态

随着Docker迅速火遍全球, 以Docker为基础的生态系统也迅速的发展起来,从以部署和运行container为基础的CoreOS到各种各样的管理工具和PaaS软件,Docker以及生态产品都在迅猛发展,以下介绍几个代表性的软件。

首先介绍CoreOS,它的出现极大地推动了Docker技术的推广和发展,CoreOS是专门为大规模服务部署而设计的一种新的Linux发行版,通过运行轻量级的容器方便扩展和维护大规模的服务。它具有以下特点:

  1. CoreOS使用container管理服务(容器即服务),即以容器的角度去管理服务,服务的代码和依赖都打包到容器里,打包后的容器直接在CoreOS上运行管理。通过容器用户不再需要关注虚拟机环境等,极大地降低了服务和系统环境的耦合性。另外部署在CoreOS的多个容器都运行在各自独立的环境中,不会相互影响。
  2. CoreOS专门为cluster等大规模部署而设计,提供了Etcd进行服务发现,以及Fleet管理容器保证服务可用。
  3. CoreOS更加精简,比如RAM使用比普通Linux低40%。
  4. CoreOS采用双分区模式(Dual-Partition),主分区为主动模式,负责系统运行,被动模式分区负责系统更新,更新时将整个CoreOS系统下载下来。

CoreOS是为集群服务而设计的,提供了Etcd、Fleet等管理工具管理容器和服务。Etcd是一种类似Zookeeper的分布式key/value存储服务,用于服务发现和配置管理。Fleet是容器管理工具,保证服务的可用性,当某个机器的服务不可用时,Fleet会将服务迁移到其它机器上运行。

Docker生态中还有一个非常重要的容器管理工具–Kubernetes,它是Google开源的用于在集群环境中管理、维护、自动扩展容器,通过Kubernetes可以很方便地在多个机器上管理和部署容器服务。现在已经得到IBM、Microsoft、RedHat等多个大公司的支持。

在Kubernetes中pod是一个基本单元,一个pod可以是提供相同功能的多个container,这些容器会被部署在同一个minion上。Replication controller定义了多个pod或者容器需要运行,如果当前集群中运行的pod或容器达不到配置的数量,replication controller会调度容器在多个minion上运行,保证集群中的pod数量。service则定义真实对外提供的服务,一个service会对应后端运行的多个container。Kubernetes的架构由一个master和多个minion组成,master通过api提供服务,接受kubectl的请求来调度管理整个集群。minion是运行Kubelet的机器,它接受master的指令创建pod或者容器。

最后介绍一下基于Docker实现的PaaS软件,Docker PaaS软件中以Deis和Flynn最为知名。Deis是基于Docker和CoreOS实现的轻量级的PaaS,受到Heroku的启发,遵循“十二要素”构建应用方法。Deis是以应用程序为中心设计的,分为build、release、run三个阶段,用户执行”git push”后,Deis使用Docker 容器编译并将编译结果保存在Docker镜像;发布阶段,一次build和配置文件产生一个数字标识的发布镜像,将发布镜像保存到Docker registry中以供后续发布到线上运行;运行阶段应用镜像会被调度到主机上运行,并更新相应的路由。Flynn与Deis类似,也是以应用为中心,Flynn组件分为两层,layer0是底层资源的抽象,主要负责资源调度以及服务发现等,为上层应用容器的运行提供底层资源调度支持;layer1处理具体应用,通过Docker容器编译、部署和维护上层应用程序。

总结

Docker从2013年发布第一个版本以来,已经火遍全球,技术迭代也比较频繁,其周边产品和技术也越来越丰富,由于Docker更新频繁,会出现新版本有时不兼容旧版本的情况,Docker周边产品基本都处于开发阶段还不具备生产环境下使用。

Docker的轻量级容器不仅实现了资源隔离,而且几乎可以运行在任何地方,使得部署和扩展变得非常容易,随着Docker的日趋完善,希望Docker被越来越多的公司应用到生产环境中。下一篇将详细介绍美团如何使用Docker。

参考文献

  1. https://docs.docker.com/articles/
  2. https://docker.cn/
  3. http://domino.research.ibm.com/library/cyberdig.nsf/papers/0929052195DD819C85257D2300681E7B/$File/rc25482.pdf
  4. http://www.infoq.com/
  5. https://github.com/docker/docker-registry
  6. http://www.xmind.net/m/RHSz/

原文出自:https://tech.meituan.com/archives

Kafka文件存储机制那些事

美团点评阅读(1657)

Kafka是什么

Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx日志、访问日志,消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。

1.前言

一个商业化消息队列的性能好坏,其文件存储机制设计是衡量一个消息队列服务技术水平和最关键指标之一。
下面将从Kafka文件存储机制和物理结构角度,分析Kafka是如何实现高效文件存储,及实际应用效果。

2.Kafka文件存储机制

Kafka部分名词解释如下:

  • Broker:消息中间件处理结点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。
  • Topic:一类消息,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发。
  • Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。
  • Segment:partition物理上由多个segment组成,下面2.2和2.3有详细说明。
  • offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息.

分析过程分为以下4个步骤:

  • topic中partition存储分布
  • partiton中文件存储方式
  • partiton中segment文件存储结构
  • 在partition中如何通过offset查找message

通过上述4过程详细分析,我们就可以清楚认识到kafka文件存储机制的奥秘。

2.1 topic中partition存储分布

假设实验环境中Kafka集群只有一个broker,xxx/message-folder为数据文件存储根目录,在Kafka broker中server.properties文件配置(参数log.dirs=xxx/message-folder),例如创建2个topic名称分别为report_push、launch_info, partitions数量都为partitions=4
存储路径和目录规则为:
xxx/message-folder

              |--report_push-0
              |--report_push-1
              |--report_push-2
              |--report_push-3
              |--launch_info-0
              |--launch_info-1
              |--launch_info-2
              |--launch_info-3

在Kafka文件存储中,同一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为topic名称+有序序号,第一个partiton序号从0开始,序号最大值为partitions数量减1。
如果是多broker分布情况,请参考kafka集群partition分布原理分析

2.2 partiton中文件存储方式

下面示意图形象说明了partition中文件存储方式:
image

                              图1
  • 每个partion(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。
  • 每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。

这样做的好处就是能快速删除无用文件,有效提高磁盘利用率。

2.3 partiton中segment文件存储结构

读者从2.2节了解到Kafka文件系统partition存储方式,本节深入分析partion中segment file组成和物理结构。

  • segment file组成:由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀”.index”和“.log”分别表示为segment索引文件、数据文件.
  • segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。

下面文件列表是笔者在Kafka broker上做的一个实验,创建一个topicXXX包含1 partition,设置每个segment大小为500MB,并启动producer向Kafka broker写入大量数据,如下图2所示segment文件列表形象说明了上述2个规则:
image

            图2

以上述图2中一对segment file文件为例,说明segment中index<—->data file对应关系物理结构如下:
image

            图3

上述图3中索引文件存储大量元数据,数据文件存储大量消息,索引文件中元数据指向对应数据文件中message的物理偏移地址。
其中以索引文件中元数据3,497为例,依次在数据文件中表示第3个message(在全局partiton表示第368772个message)、以及该消息的物理偏移地址为497。

从上述图3了解到segment data file由许多message组成,下面详细说明message物理结构如下:
image

           图4

参数说明:

关键字解释说明
8 byte offset在parition(分区)内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message
4 byte message sizemessage大小
4 byte CRC32用crc32校验message
1 byte “magic”表示本次发布Kafka服务程序协议版本号
1 byte “attributes”表示为独立版本、或标识压缩类型、或编码类型。
4 byte key length表示key的长度,当key为-1时,K byte key字段不填
K byte key可选
value bytes payload表示实际消息数据。

2.4 在partition中如何通过offset查找message

例如读取offset=368776的message,需要通过下面2个步骤查找。

  • 第一步查找segment file
    上述图2为例,其中00000000000000000000.index表示最开始的文件,起始偏移量(offset)为0.第二个文件00000000000000368769.index的消息量起始偏移量为368770 = 368769 + 1.同样,第三个文件00000000000000737337.index的起始偏移量为737338=737337 + 1,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据offset **二分查找**文件列表,就可以快速定位到具体文件。
    当offset=368776时定位到00000000000000368769.index|log

  • 第二步通过segment file查找message
    通过第一步定位到segment file,当offset=368776时,依次定位到00000000000000368769.index的元数据物理位置和00000000000000368769.log的物理偏移地址,然后再通过00000000000000368769.log顺序查找直到offset=368776为止。

从上述图3可知这样做的优点,segment index file采取稀疏索引存储方式,它减少索引文件大小,通过mmap可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。

3 Kafka文件存储机制–实际运行效果

实验环境:

  • Kafka集群:由2台虚拟机组成
  • cpu:4核
  • 物理内存:8GB
  • 网卡:千兆网卡
  • jvm heap: 4GB
  • 详细Kafka服务端配置及其优化请参考:kafka server.properties配置详解

image

                              图5                                 

从上述图5可以看出,Kafka运行时很少有大量读磁盘的操作,主要是定期批量写磁盘操作,因此操作磁盘很高效。这跟Kafka文件存储中读写message的设计是息息相关的。Kafka中读写message有如下特点:

写message

  • 消息从java堆转入page cache(即物理内存)。
  • 由异步线程刷盘,消息从page cache刷入磁盘。

读message

  • 消息直接从page cache转入socket发送出去。
  • 当从page cache没有找到相应数据时,此时会产生磁盘IO,从磁
    盘Load消息到page cache,然后直接从socket发出去

4.总结

Kafka高效文件存储设计特点

  • Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
  • 通过索引信息可以快速定位message和确定response的最大大小。
  • 通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
  • 通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

参考

1.Linux Page Cache机制
2.Kafka官方文档

原文出自:https://tech.meituan.com/archives

Docker系列之二:基于容器的自动构建

美团点评阅读(1637)

自动构建系统是从美团的自动部署系统发展出来的一个新功能。每当开发人员提交代码到仓库后,系统会自动根据开发人员定制的构建配置,启动新的Docker容器,在其中对源代码进行构建(build),包括编译(如Java、C++和Go)、预处理(如Javascript和CSS)、压缩(如图片)等操作,生成最终需要上线的程序包。

背景

美团的代码发布系统有中央控制节点,负责代码的拉取、应用的构建和上传等任务。随着业务的迅速增长,应用发布项的数目和单个发布项的服务器数量也随之增长,中控节点的任务加重,几个问题也变得亟待解决:

  • 不同应用的构建环境在同一个虚拟机上,需要解决环境冲突和隔离的问题
  • 多个应用同时构建会竞争发布机的CPU和IO资源,让构建变慢
  • 应用的构建脚本运行在公共发布机上,脚本的bug可能会影响到发布机的正常运行

例如某次主站(PHP)的发布速度非常慢,调查后发现当时某些Java应用正在编译,占用了大量CPU资源,导致其它应用的发布变慢。

为解决上述问题,我们设计了把应用的构建过程从中央发布机分离出来的方案,并利用Docker作为构建的基础环境。关于Docker的介绍,可以参考《Docker系列之一:入门介绍》这篇文章。

原理

自动构建原理图

首先,开发人员在Stash上配置自动构建,之后的代码提交就会通知自动构建系统。自动构建系统收到通知,找到所有配置了该仓库的发布项,生成构建任务,并把这些任务提交到Django-rq队列。任务的主要配置是YAML格式的自动构建配置文件,该文件类似Dockerfile,但是为了使用方便,只支持少量的关键字,因此比Dockerfile使用更简单。通过该配置文件可指定构建容器使用的镜像,一些环境变量,以及构建命令等。系统从私有的Docker registry获取镜像,并根据YAML配置生成Docker容器,在此容器中完成构建。

从Stash触发自动构建的功能,是从这个项目修改实现的,只需简单配置即可启用自动构建。

Stash上的配置

构建成功的结果会自动上传到美团存储服务(Meituan Storage Service)。当发布人员发布时,就直接从MSS拉取构建好的应用包进行发布,省去了在发布时才进行的编译环节。

为什么用Docker?

为了达到隔离构建环境的目的,应用的构建可以在分别的美团云虚拟机上实现。但是,应用构建有一些特点让Docker在此场景更合适。首先,构建环境都是临时的,每次构建结束后就销毁(也可选择保留)。而我们内部使用的美团云虚拟机是和运维用的配置管理数据库(Configuration Management Database)关联的,新虚拟机会自动部署一些基础环境、监控报警项等,并注册进CMDB,而这些东西对自动构建的系统是多余的。第二,自动构建的系统启停频繁,Docker这样的轻量级容器可以更好地满足快速生成和销毁的需求。因此,自动构建系统是在美团云虚拟机里面运行的Docker容器中进行的。

收益

自动构建很好地解决了文章开头提到的发布系统的三个问题:

  • 自动隔离不同应用的构建环境,无需担忧环境冲突的问题
  • 不同应用的构建容器不必运行在同一台虚拟机,可以分布在多虚拟机的集群上,避免了构建之间的资源竞争,让构建过程更加迅速
  • 任一应用构建的错误不会影响其它应用的构建或者中央发布机的运行

此外,自动构建还有如下两个好处。

首先,预先的主动构建把冗长的构建时间从发布过程省去,让发布人员在发布时耗时更短,既让发布更敏捷迅速,又提升了用户体验。美团在工作日每天的代码发布要上千次,快速的发布过程才能更好地保证业务的迭代过程。

其次,自动构建让构建环境的定制更方便。原来在发布机上构建时,如果需要的依赖在发布机上还没有,就需要给运维人员提需求来进行配置,这个过程不够敏捷。使用自动构建后,开发人员可自行在YAML格式的配置文件指定构建环境。前端开发人员的构建环境往往比较新,需要频繁改变环境,因此支持自定义依赖的自动构建系统受到了前端开发人员的欢迎。

总结和展望

自动构建目前是发布项配置里面的一个可选项,这保持了和原有系统的兼容。自动构建是美团在线上业务中首次使用Docker。我们会持续推进该特性在自动部署系统的使用,最终成为所有发布项的默认配置。

自动构建使用Docker的方式,为我们后续更广泛地使用Docker提供了启发。

第一,将Docker用于开发环境。通过Dockerfile描述测试环境,并维护起测试环境的Docker镜像,可以让开发人员快速搭起来一个统一的开发环境;再结合Vagrant,可以很好地解决研发团队中普遍存在的测试环境搭建麻烦的问题。

第二,将Docker用于应用部署。完成自动构建后,容器中已经有了应用程序包,再加上运行时依赖,即可让这个容器直接提供服务。

未来可以在应用的开发测试,编译构建,和部署运行等三个环节,都使用Docker容器。关于Docker在上述场景的应用,请关注我们博客的后续更新。

原文出自:https://tech.meituan.com/archives

全球互联网技术架构,前沿架构参考

联系我们博客/网站内容提交