美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染

Graver 是一款高效的 UI 渲染框架,它以更低的资源消耗来构建十分流畅的 UI 界面。Graver 独创性的采用了基于绘制的视觉元素分解方式来构建界面,得益于此,该框架能让 UI 渲染过程变得更加简单、灵活。目前,该框架已经在美团 App 的外卖频道、独立外卖 App 核心业务场景的大多数业务中进行了应用,同时也得到美团外卖内部技术团队的认可和肯定。App 渲染性能优化是一个普遍存在的问题,为了惠及更多的前端开发同学,美团外卖 iOS 开发团队将其进行开源,Github 项目地址与使用文档详见:https://github.com/Meituan-Di… 。我们希望该框架能够应用到更广阔的业务场景。当然,我们也知道该框架尚有待完善之处,也希望能与更多技术同行一起交流、探讨、共建。前言我们为什么需要关注界面的渲染性能?App 使用体验主要包含产品功能、交互视觉、前端性能,而使用体验的好与坏,直接影响用户持续使用还是转而使用其他 App,所以我们非常关注 App 的渲染性能。而且在互联网产品流量竞争愈发激烈的大背景下,优质的使用体验可以为现有用户提供更好的服务,进而提高用户转化和留存,这也意味着创收、盈利。<center><font color=gray size = 2>图1 使用体验与转化、留存</font></center>背景美团外卖 App 从2013年成立至今,已经走过了五个春秋,在技术层面先后经历了快速验证、模块化、精细化和平台化四个阶段,产品形态上也日趋成熟。在此期间,我们构建并完善了监控、报警、容灾、备份等各项基础设施,Metrics 即是其中的性能监控系统。曾经一段时间,我们以外卖 App 首页商家卡片列表为例,通过 Metrics 性能监控系统发现其在 FPS、CPU、Memory 等方面的各项指标并不理想。于是,通过 Xcode 自带的 TimeProfile 等性能检测工具,然后结合代码分析等手段找到了现存性能瓶颈。与此同时,我们梳理其近半年的迭代版本需求发现,UI 往往需要根据不同场景甚至不同用户展示不同的内容。为了不断迎合用户的需求,快速应对市场变化,这种特征还会持续存在。然而,它会带来以下问题:视图层级愈加复杂、视图数量愈加众多,从版本长期迭代来看是潜在的性能瓶颈点。如何快速、高效支撑 UI 变化,同时保证不会二次引入性能瓶颈。<center><font color=gray size = 2>图2 影响渲染性能、研发效率的瓶颈点</font></center>Graver 介绍为了解决现存的性能瓶颈以及后续潜在的性能瓶颈,我们期望构建一套解决方案,该方案能在充分满足外卖业务特征的前提下,以标准化、一站式的方式解决 iOS 端 App 的渲染性能问题,并且对研发效率有一定提升, Graver(雕工)框架应运而生。因为 Graver 独创性地采用了全新的视觉元素分解思路,所以该框架使用起来十分灵活、简单。我们先来看一下 Graver 的主要特点:性能表现优异以外卖 App 首页商家列表为例,应用 Graver 之后5分位滚动帧率从满帧的<font color=red size = 2>84%</font>提升至<font color=red size = 2>96%</font>,50分位几乎满帧;CPU 占用率下降了近<font color=red size = 2>6个百分点</font>,有效提升了空闲 CPU 的资源利用率,降低了峰值 CPU 的占用率。如图3所示:<center><font color=gray size = 2>图3 优化前后技术指标对比</font></center>“一站式”异步化Graver 从文本计算、样式排版渲染、图片解码,再到绘制,实现了全程异步化,并且是线程安全的。使用 Graver 可以一站式获得全部性能优化点,可以让我们:不再担心散点式的“遇见一处改一处”的麻烦。不再担心离屏渲染等各种可能导致性能瓶颈的问题,以及令人头痛的解决办法。不再担心优化会有遗漏、优化不到位。不再担心未来变化可能带来的任何性能瓶颈。性能消耗的“边际成本”几乎为零Graver 渲染整个过程除画板视图外完全没有使用 UIKit 控件,最终产出的结果是一张位图(Bitmap),视图层级、数量大幅降低。以外卖 App 首页铂金展位视图为例,原有方案由58个控件、12层级拼接而成;而应用 Graver 后仅需1个视图、1级层级绘制而成。 伴随着需求迭代、视觉元素变化,性能消耗恒属常数级。如图4所示:<center><font color=gray size = 2>图4 外卖 App 铂金展位应用 Graver 前后对比</font></center>渲染速度快Graver 并发进行多个画板视图的渲染、显示工作。得益于图文混排技术的应用,达到了内存占用低,渲染速度快的效果。由于排版数据是不变的,所以内部会进行缓存、复用,这又进一步促进了整体渲染效率。Graver 既做到了高效渲染,又保证了低时延页面加载。<center><font color=gray size = 2>图5 渲染效率说明</font></center>以“少”胜“繁”Graver 重新抽象封装 CoreText、CoreGraphic 等系统基础能力,通过少量系统标准图形绘制接口即可实现复杂界面展示。基于位图(Bitmap)的轻量事件交互系统如上述所说,界面展示从传统的视图树转变为一张位图,而位图不能响应、区分内部具体位置的点击事件。Graver 提供了基于位图的轻量事件交互系统,可以准确识别点击位置发生在位图的哪一块“绘制单元”内。该“绘制单元”可以理解为与我们一贯使用的某个具体 UI 控件相对应的视觉展示。使用 Graver 为某一视觉展示添加事件如同使用系统 UIButton 添加事件一样简单。全新的视觉元素分解思路Graver 一改界面编程思路,与传统的通过控件“拼接”、“添加”,视图排列组合方式构建界面不同,它提供了灵活、便捷的接口让我们以“视觉所见”的方式构建界面。这一特点在下文Graver使用中详细阐述,正是因为该特点实现了研发效率的提升。Graver 使用Graver 引入了全新的视觉元素分解的思路。借助该思路可以实现通过一种对象来表达任一视觉元素、甚至是任一视觉元素的组合,从而消除界面布局的复杂性。我们先来回顾下传统界面的构建方式,以外卖 App 商家卡片其中一种样式为例,如图6所示:<center><font color=gray size = 2>图6 外卖 App 商家卡片</font></center>在实现商家卡片的界面样式时,通常会根据视觉上的识别、交互要求来建立界面展示与系统提供的 UI 控件间的映射关系。以标号②位置的样式为例,在考虑复用的情况下通常这部分会使用三个系统控件来完成,分别是左侧蓝底的“预订”使用 UILabel 控件、右侧的蓝色边框“2.26.21:30起送”使用 UILabel 控件、把左右两侧 UILabel 控件装起来的 UIView 控件;在确定好采用的 UI 控件之后,需要针对展示样式分门别类的设置各个控件的渲染属性来实现图示 UI 效果,渲染属性通常一部分预设,一部分根据业务数据的不同再进行二次设置;其次,设置各个控件的内容属性实现业务数据内容的展示,展示的内容一般是网络业务数据经逻辑处理、加工后的数据。如果涉及到点击事件,还需要添加手势或者更换成 UIButton 控件。接下来,需要根据视觉要求实现排版逻辑,以标号⑧、⑨为例,当标号⑧位置的数据没有的情况下,需要上提标号⑨位置的“美团专送”到图示标号⑧位置。诸如类似的排版逻辑随处可见。对于图示任一位置的展示内容都存在上述的循环思考、编写工作。随着界面元素的增加、变化,问题会变得更加复杂。传统的界面构建方式其实是在 UI控件的维度去分解视觉元素,具体是做以下四方面的编写工作:控件选择:根据展示内容、样式、交互要求确定采用哪种系统控件。布局信息:UI 控件的大小、位置,即 Frame。内容信息:UI 控件展示出来的业务数据,如标号①位置的“星巴克咖啡店”。渲染信息:UI 控件展示出来的效果,如字体、字号、透明度、边框、颜色等。最后,将各个控件以排列组合方式合成为一棵视图树。Graver 框架提供了以画板视图为基础,通过对更底层的 CoreText、CoreGraphic 框架封装,以更贴近“视觉所见”的角度定义了全新视觉元素分解、界面展示构建的过程。通常“视觉所见”可划分为两部分:静态展示、动态展示。静态展示包含图片、文本;动态展示包含视频、动画等。在视觉展示全部为静态内容的时候,一个 Cell 即是一个画布,除此以外没有任何 UI 控件;否则,可以按需灵活的进行画布拆分来满足动画、视频等需要。<center><font color=gray size = 2>图7 画板和传统视图树</font></center>以图6商家卡片中标号②、⑧为例,新实现方式的伪代码是这样的:WMMutableAttributedItem *item = [[WMMutableAttributedItem alloc] init];[[[[item appendImage:[[UIImage wmg_imageWithColor:“blue”] wmg_drawText:“预订”]] appendImage:[[UIImage wmg_imageWithColor:“clear” borderWidth:1 borderColor:“blue”] wmg_drawText:“2.26.21:30起送”] appendWhiteSpaceWithWidth:“width”]//总体宽度减去②和⑧的宽度总和剩余部分 apendText:“50分钟|2.5km”];上述实现方式即是把标号②、⑧部分作为一个整体来实现,任何单一系统控件都无法做到这一点。Graver 渲染原理<center><font color=gray size = 2>图8 Graver 工作时序</font></center>如图8所示,Graver 涉及多个队列间的交互,以外卖 App 商家列表为例,整体流程如下:主线程构建请求参数,创建请求任务并放入网络线程队列中,发起网络请求。网络线程向后端服务发起请求,获得对应的业务模型数据(如包含了店铺名称,商家头图,评分,配送时长,客单价,优惠活动等店铺属性的商家卡片列表)。网络线程创建包含业务模型数据(如商家卡片列表)的排版任务,提交到预排版线程处理,进入预排版流程。预排版队列取出排版任务,交由布局引擎计算 UI 布局,将业务模型解析成可被渲染引擎直接处理的,包含布局、层级、渲染信息的排版模型。解析结束后,通知主线程排版完成。主线程获取排版模型后,随即触发内容显示。根据相对屏幕位置及出现的先后顺序,创建包含将需要显示区域信息的绘制任务,放入异步绘制线程队列中,发起绘制流程。异步绘制线程队列取出绘制任务,进行图文绘制,最终输出一张包含了图文内容(如商家卡片)的图片。绘制任务结束后,通知主线程队绘制完成,主线程随后展示绘制区域。整体按照队列间串行、队列内并行的方式执行。业务应用Graver 在外卖内部发布之后,我们也将其推广到更多的业务线,并希望 Graver 能够成为对业务开展有重要保障的一项基础服务。经过半年多的内部试用,Graver 的可靠性、渲染性能、业务适应能力也受到外卖内部的肯定和认可。截止发稿时,Graver 已经基本覆盖了美团 App 的外卖频道、独立外卖 App 核心业务场景的大多数业务。下面列举 Graver 在外卖业务的部分应用案例:经验总结总结一下,对于界面渲染性能优化而言,要站在一个更高角度来思考问题的解决方案。横向上,从普适性角度解决性能瓶颈点,避免其他人遇到类似问题的重复工作;纵向上,从长远考虑问题做到防微杜渐,一次优化,长期受益。基于此,我们提出一站式、标准化的渲染性能解决方案。诚然,这会遇到很多难点。面对界面样式构建的问题,系统 UIKit 框架着实为我们提供了便利,然而有时候我们需要跳出固有思维,尝试建立一套全新界面构建、视觉元素分解的思路。参考资料前端感官性能的衡量和优化实践作者简介洋洋,美团高级工程师。2018年加入美团,目前负责【美团外卖】和【美团外卖频道】的 iOS 客户端首页业务,以及支撑首页业务的技术架构、工具和系统的开发和维护工作。招聘美团外卖长期招聘 Android、iOS、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同学投递简历到chenhang03#meituan.com。 ...

December 24, 2018 · 1 min · jiezi

机器学习在美团配送系统的实践:用技术还原真实世界

在2018 AI开发者大会(AI NEXTCon)上,美团配送AI方向负责人何仁清,分享了美团在即时配送领域中机器学习技术的最新进展,以及如何通过大数据和机器学习手段,建立对线下真实世界各种场景的感知能力,还原并预测配送过程各个细节,从而提升整体配送系统的精度。美团“超脑”配送系统的由来2014年,斯嘉丽·约翰逊主演的科幻片《超体》大火,影片中主人公Lucy由于无意中摄入了大量的代号为“CPH4”的神秘药物,大脑神经元获得空前的开发,获得了异乎寻常的超能力,她能够对这个世界进行全新的感知、理解和控制(比如控制无线电波),最终跨越时间和空间成为了一个超级个体。 这种对真实世界的深度感知、理解和控制,与配送AI系统对配送场景的感知、理解和配送环节控制的目标非常一致。可以说,美团要建设的AI就是配送系统的“超级大脑”。因此我们内部把配送的AI系统,简称为“超脑”配送系统。即时配送在全球快速发展最近几年,以外卖为依托,即时配送业务在全球范围内掀起了一波快速发展的浪潮,全球各地都出现了很多创业公司,其中国外知名的包括美国的Uber Eats(全球)、英国的Deliveroo、印度的Swiggy、Zomato(分别被美团和阿里投资),印尼的go-jek等等。国内除了美团外卖、饿了么、滴滴外卖等典型代表外,而还有专注于即时配送服务创业公司,比如闪送、UU跑腿、达达、点我达等。这种全球爆发的现象说明了两个问题:“懒”是人类的天性。平价、方便、快捷的服务是人类的普遍需求,尤其是在“吃”这个事情上,外卖成为了一种高频的刚需。外卖的商业模式完全可行。以美团外卖为例,2018年上半年整体收入160亿,同步增长90%。根据Uber公布的数据,Uber Eats在2018第一季度占整体营业的13%。即时配送的业务模型即时配送,是一种配送时长1小时以内,平均配送时长约30分钟的快速配送业务。如此快速的配送时效,将传统的线上电商交易与线下物流配送(传统划分比较明确的两条业务)整合为统一整体,形成了用户、商户、骑手和平台互相交错的四元关系。其整合力度空前紧密,几乎渗透到各个环节。以外卖搜索和排序为例,在下午时段,在用户搜索和推荐中可以看到更多的商家,因为此时运力充分,可以提供更远距离的配送服务,不仅能更好满足用户的需求,提高商家的单量,而且能够增加骑手的收入。即时配送的核心指标是效率、成本、体验,这三者也形成了即时配送的商业模型。简单来说可以分为以下几步:首先配送效率提升让骑手在单位时间内配送更多订单,产生更多价值。然后配送成本下降更高的效率,一方面让骑手收入增加,一方面也让订单平均成本下降。然后用户体验提升低成本能够让用户(商户)以更低的价格享受更好的配送服务,从而保证更好的用户体验。进一步提升效率并形成循环更好的用户体验,让更多用户(商户)聚集过来,提升规模和密度,进一步提升配送效率。这样,就形成了一个正向循环,不断创造更多商业价值。而技术的作用,就是加速这个正向循环。美团“超脑”配送系统目前互联网技术,很大部分还是针对线上产品和系统研发,整个流程可以在线上全部完成,而这也正是配送AI技术最大的不同和挑战。简单来说,类似搜索、推荐、图象和语音识别这种线上产品常用的AI技术帮助不大,因为配送必须在线下一个一个环节的进行,这就要求AI技术必须能够面对复杂的真实物理世界,必须能深度感知、正确理解与准确预测、并瞬间完成复杂决策。为了满足这些要求,我们建设了美团“超脑”配送系统,包含以下几个方面:大数据处理和计算能力算法数据和计算平台:包括实时特征计算、离线数据处理、机器学习平台等。建立对世界深度感知LBS系统:提供正确位置(用户/商户/骑手)以及两点之间正确的骑行导航。多传感器:提供室内定位以、精细化场景刻画、骑手运动状态识别正确理解和准确预测时间预估:提供所有配送环节时间的准确预估其他预估:销量预估、运力预估等完成复杂决策调度系统:多人多点实时调度系统,完成派单决策:谁来送?怎么送?定价系统:实时动态定价系统,完成定价决策:用户收多少钱?给骑手多少钱?规划系统:配送网络规划系统,完成规划决策:站点如何划分?运力如何运营?机器学习技术挑战如何构建一个在真实物理世界运行的AI系统,就是我们最大的挑战。具体到机器学习方向而言,挑战包括以下几个方面:精度足够高、粒度足够细时间要求:一方面是周期性变化,比如早午晚,工作假日,季节变化;一方面是分钟级的精细度,比如一个商圈单量和运力的实时变化。空间要求:一方面是不同商圈独有特性,比如CBD区域;一方面是要实现楼栋和楼层的精度,比如1楼和20楼,就是完全不同的配送难度。鲁棒性要求:处理各种不确定的能力,比如天气变化、交通变化等等。线下数据质量的巨大挑战大噪音:比如GPS定位漂移,尤其是在高楼附近,更不要说在室内GPS基本不可用。不完备:比如商家后厨数据、堂食数据、其他平台数据,都极难获得。高复杂:配送场景多样而且不稳定,随着时间、天气、路况等在不断变化。配送系统的核心参数ETAETA(Estimated Time of Arrival,时间送达预估)是配送系统中非常重要参数,与用户体验、配送成本有直接关系,而且会直接影响调度系统和定价系统的最终决策。一个订单中涉及的各种时长参数(如上图右侧所示),可以看到有十几个关键节点,其中关键时长达到七个。这些时长涉及多方,比如骑手(接-到-取-送)、商户(出餐)、用户(交付),要经历室内室外的场景转换,因此挑战性非常高。通过机器学习方法,我们已经将外卖配送几乎所有环节都进行了精准预估预测。用户感知比较明显是预计送达时间,贯穿多个环节,商家列表(从配送时长角度让用户更好选择商家)、订单预览(给用户一个准确的配送时间预期)、实时状态(下单后实时反馈最新的送达时间)。当然这里面还有很多用户看不到的部分,比如商家出餐时间、骑手到店时间、交付时间等。其中交付时长,与用户关系比较大,也很有意思,下文会详细展开。精准到楼宇和楼层的预估:交付时长交付时长是指骑手到达用户后,将外卖交付到用户手中并离开的时间,实际是需要考虑三维空间内计算(上楼-下楼)。交付时间精准预估,有两点重要的意义,首先是客观的衡量配送难度,给骑手合理补贴;其次,考虑对骑手身上后续订单的影响,防止调度不合理,导致其他订单超时。 交付时长的目标是,做到楼宇和楼层的精准颗粒度,具体可以拆解为以下几步:地址的精准解析(精确到楼宇/单元/楼层)地址精度需要在5级之上(4级:街道,5级:楼宇),国内拥有这个级别精细化数据的公司屈指可数。数据的安全级别很高,我们做了很多脱敏工作,做了各种数据保护与隔离,保证用户隐私和数据安全。地址信息的多种表达方式、各种变形,需要较强的NLU技术能力。交付时长预估通过骑手轨迹进行“入客-离客”识别,并进行大量数据清洗工作。统计各个粒度的交付时长,通过树形模型实现快速搜索各个粒度的数据。因为预估精度是楼宇和楼层,数据很稀疏,很难直接进行统计,需要通过各种数据平滑和回归预估,处理数据稀疏和平滑的问题。下游业务应用给调度和定价业务,提供楼宇+楼层维度的交付时长。从上图可以看到,在不同楼宇,不同楼层交付时长的区分度还是很明显的。尤其是楼层与交付时长并不是线性相关,我们还具体调研过骑手决策行为,发现骑手会考虑等电梯的时间,低楼层骑手倾向于走楼梯,高楼层则坐电梯。可以看到,真实世界中影响决策因素非常多,我们目前做的还不够。比如交付时长也可以进一步细化,比如准确预估骑手上楼时间、下楼时间和等待时间,这样其实能够与商家取餐环节保持一致,之所以没这么做,主要还是数据缺失,比如骑手在商家其实有两个操作数据(到店、取餐),这样能支持我们做精细化预估的,但是在用户环节只有(送达)一个操作。 举这个例子,其实是想说明,数据的完备性对我们到底有多重要。数据方面的挑战,线下业务与线上业务相比,要高出好几个等级。配送中最重要的数据之一:地图地图对配送的重要性毋庸置疑(位置和导航都不准确,配送如何进行?),前面提到的5级地址库只是其中一部分。配送地图的目标可以概括为以下两点:正确的位置实时部分:骑手实时位置。静态部分:用户和商户准确的地址和位置。正确的导航两点之间正确的距离和路线。突发情况的快速反应(封路、限行)。如果横向对比配送、快递、打车等行业对地图的要求,其实是一件很有意思的事情,这个对于配送地图技术建设来说,是一件非常有帮助的事情。即时配送 VS 物流快递:即时配送对地图的依赖程度明显高于物流快递即时配送 VS 出行行业:地图厂商在车载导航的优势和积累,在即时配送场景较难发挥从这两方面对比可以看到,在即时配送业务中,骑行地图的重要性非常之高,同时很多问题确实非常具有行业特色,通过驾车地图的技术无法很有效的解决。这样就需要建设一套即时配送业务地图的解决方案。基于签到数据的位置校正:交付点如前文所述,配送地图的方向有很多,这次我重点讲一下用户位置相关的工作“交付点挖掘”。首先看一下目前主要问题:用户位置信息有很多错误,比如:用户选择错误上图左,一个小区会有1期2期~N期等,用户在选择POI的时候就可能发生错误(比如1期的选了2期),两者地理位置相差非常远,很容易造成骑手去了错误的地方。这样在订单发送到配送系统的时候,我们需要做一次用户坐标纠正,引导骑手到达正确的位置。POI数据不精细上图右,用户本来在xx区xx栋,但是只选了xx区这个比较粗的位置信息。现实中在一个小区里面,找到一个具体xx栋楼还是非常困难的,大家可以想想自己小区中,随便说一个楼号你知道它在哪个角落吗,更别说如果是大晚上在一个你不熟悉的小区了。造成这种原因,一方面可能是用户选择不精细,还有一种可能,就是地图上没有具体楼栋的POI信息。在实际配送中,我们都会要求骑手在完成交付后进行签到,这样就会积累大量的上报数据,对于后续进行精细化挖掘非常有帮助。大家可以先看看我们收集的原始数据(上图),虽然还是非常凌乱,但是已经能看到这其中蕴含着极高的价值,具体来说有三方面:数据量大每天几千万订单,几十亿的轨迹数据。可以充分覆盖每一个小区/楼栋/单元门。维度多样除了骑手签到和轨迹数据,我们还有大量的用户、商户和地图数据。多种数据维度可以交叉验证,有效避免数据的噪音,提高挖掘结果精度。数据完备在局部(用户和商户)数据足够稠密,置信度比较高。交付点挖掘的技术实战:挑战在数据挖掘实际过程中,其实并没有什么“高大上”的必杀技,无法使用流行的End2End方法,基本上还是需要对各个环节进行拆解,扎扎实实的做好各种基础工作,基本整个挖掘过程,分为以下几个步骤:(1)基于地址分组;(2)数据去噪;(3)数据聚合;(4)置信度打分。其中主要技术挑战,主要在各种场景中保证数据挖掘质量和覆盖率,具体来说主要有三个挑战:数据去噪数据噪音来源比较多样,包括GPS的漂移、骑手误操作、违规操作等各种。一方面是针对噪音原因进行特殊处理(比如一些作弊行为),另一方面要充分发挥数据密度和数据量的优势,在保证尽量去除Outlier后,依然保持可观的数据量。能够同时使用其他维度的数据进行验证,也是非常重要的,甚至可以说数据多样性和正交性,决定了我们能做事情的上限。数据聚合不同区域的楼宇密度完全不一样,具有极强的Local属性,使用常规聚类方法,比较难做到参数统一,需要找到一种不过分依赖样本集合大小,以及对去噪不敏感的聚类算法。重名问题这个属于POI融合的一个子问题,判断两个POI信息是否应该合并。这个在用户地址中比较常见,用户提供的地址信息一样,但实际是两个地方。这种情况下,我们的处理原则是一方面要求纠正后坐标更符合骑手签到情况,另一方面新坐标的签到数据要足够稠密。交付点挖掘的技术实战:效果目前,我们已经上线了一版交付点,对用户位置进行主动纠正,让骑手可以更准确更快的找到用户。目前效果上看还是非常明显的。包括几个方面:骑手交付距离明显降低从上图左侧部分看到,在上线前(绿色)交付距离>100M的占比很高(这个距离会导致实际位置差几栋楼,甚至不同小区),也就是用户自己选着的位置错误率比较高,导致骑手交付难度较高,对效率影响比较大。上线后(红色),交付距离明显缩短(均值左移),同时>100M的长尾比例明显下降。单元门级别的高精度位置上图右侧部分看到,我们挖掘的交付点基本上能与楼宇的单元门对应。而且没有明显偏差比较大的部分。这个质量基本达到我们之前设定目标,也证明配送大数据的巨大潜力。目前的问题以及后续的优化点如何提升其作为POI挖掘和发现手段的准确率?这里面有很多优化点,比如去重(交付点-位置信息的一一映射),POI信息补全和更新。如何扩大数据渠道并做到信息整合?目前主要渠道还是骑手签到和轨迹数据,这个明显有更大的想象空间,毕竟每天在全国大街小巷,有几十万骑手在进行配送,除了前面(以及后面)提到的通过手机被动采集的数据,让骑手主动采集数据,也是不错的建设思路。只不过想要做好的话,需要建立一个相对闭环数据系统,包括上报、采集、清洗、加工、监控等等。更精细化的配送场景识别:感知前面提到的地图技术,只能解决在室外场景的位置和导航问题。但配送在商家侧(到店、取餐)和用户侧(到客、交付)两个场景中,其实是发生在室内环境。在室内的骑手位置是在哪里、在做什么以及用户和商家在做什么,如果了解这些,就能解决很多实际问题。比如:这个技术方向可以统称为“情景感知”,目标就是还原配送场景中(主要是室内以及GPS不准确),真实配送过程发生了什么,具体方向如下图所示:情景感知的目标就是做到场景的精细刻画(上图的上半部分),包含两个方面工作:配送节点的精确刻画在ETA预估中已经展示过一些,不过之前主要还是基于骑手上报数据,这显然无法做到很高精确,必须引入更客观的数据进行描述。目前,我们选择的是WIFI和蓝牙的地理围栏技术作为主要辅助。配送过程的精确刻画骑手在配送过程中经常会切换方式,比如可能某个小区不让骑电动车,那骑手必须步行,再比如骑手在商家发生长时间驻留,那应该是发生了等餐的情况(用户侧同理)。目前,我们选择使用基于传感器的运动状态识别作为主要辅助。这些数据,大部分来至于手机,但是随着各种智能硬件的普及,比如蓝牙设备,智能电动车、智能头盔等设备的普及,我们可以收集到更多数据的数据。WiFi/蓝牙技术,以及运动状态识别的技术比较成熟,这里主要说一下概况,本文不做深入的探讨。对于配送系统来说,比较大的挑战还是对识别精度的要求以及成本之间的平衡。我们对精度要求很高,毕竟这些识别直接影响定价、调度、判责系统,这种底层数据,精度不高带来的问题很大。考虑成本限制,我们需要的是相对廉价和通用的解决方案,那种基于大量传感器硬件部属的技术,明显不适用我们几百万商家,几千万楼宇这种量级的要求。为此,在具体技术方面,我们选用的是WiFi指纹、蓝牙识别、运动状态识别等通用技术方案,就单个技术而言,其实学术界已经研究很充分了,而且也有很多应用(比如各种智能手环等设备)。对于我们的挑战在于要做好多种传感器数据的融合(还包括其他数据),以确保做到高识别精度。当然为了解决“Ground Truth”问题,部署一些稳定&高精度的智能硬件还是必须的,这对技术迭代优化和评估都非常有帮助。总结美团外卖日订单量超过2400万单,已经占有了相对领先的市场份额。美团配送也构建了全球领先的即时配送网络,以及行业领先的美团智能配送系统,智能调度系统每小时路径计算可达29亿次。如何让配送网络运行效率更高,用户体验更好,是一项非常困难的挑战,我们需要解决大量复杂的机器学习和运筹优化等问题,包括ETA预测,智能调度、地图优化、动态定价、情景感知、智能运营等多个领域。过去三年来,美团配送AI团队研发效果显著,配送时长从一小时陆续缩短到30分钟,并且还在不断提升,我们也希望通过AI技术,帮大家吃得更好,生活更好。招聘信息目前,即时配送业务正处于快速发展期,新的场景、新的技术问题不断涌现,团队正在迅速扩大中,急需机器学习资深专家、运筹优化技术专家、LBS算法工程师、NLP算法工程师,我们期待你的加入。扫码可查看职位详情,或者发送简历至 yewei05@meituan.com

December 14, 2018 · 1 min · jiezi

数据库智能运维探索与实践

从自动化到智能化运维过渡时,美团DBA团队进行了哪些思考、探索与实践?本文根据赵应钢在“第九届中国数据库技术大会”上的演讲内容整理而成,部分内容有更新。背景近些年,传统的数据库运维方式已经越来越难于满足业务方对数据库的稳定性、可用性、灵活性的要求。随着数据库规模急速扩大,各种NewSQL系统上线使用,运维逐渐跟不上业务发展,各种矛盾暴露的更加明显。在业务的驱动下,美团点评DBA团队经历了从“人肉”运维到工具化、产品化、自助化、自动化的转型之旅,也开始了智能运维在数据库领域的思考和实践。本文将介绍美团点评整个数据库平台的演进历史,以及我们当前的情况和面临的一些挑战,最后分享一下我们从自动化到智能化运维过渡时,所进行的思考、探索与实践。数据库平台的演变我们数据库平台的演进大概经历了五个大的阶段:第一个是脚本化阶段,这个阶段,我们人少,集群少,服务流量也比较小,脚本化的模式足以支撑整个服务。第二个是工具化阶段,我们把一些脚本包装成工具,围绕CMDB管理资产和服务,并完善了监控系统。这时,我们的工具箱也逐渐丰富起来,包括DDL变更工具、SQL Review工具、慢查询采集分析工具和备份闪回工具等等。第三个是产品化阶段,工具化阶段可能还是单个的工具,但是在完成一些复杂操作时,就需要把这些工具组装起来形成一个产品。当然,并不是说这个产品一定要做成Web系统的形式,而是工具组装起来形成一套流程之后,就可以保证所有DBA的操作行为,对流程的理解以及对线上的影响都是一致的。我们会在易用性和安全性层面不断进行打磨。而工具产品化的主要受益者是DBA,其定位是提升运维服务的效率,减少事故的发生,并方便进行快速统一的迭代。第四个是打造私有云平台阶段,随着美团点评业务的高速发展,仅靠十几、二十个DBA越来越难以满足业务发展的需要。所以我们就把某些日常操作开放授权,让开发人员自助去做,将DBA从繁琐的操作中解放出来。当时整个平台每天执行300多次改表操作;自助查询超过1万次;自助申请账号、授权并调整监控;自助定义敏感数据并授权给业务方管理员自助审批和管理;自定义业务的高峰和低峰时间段等等;自助下载、查询日志等等。第五个是自动化阶段,对这个阶段的理解,其实是“仁者见仁,智者见智”。大多数人理解的自动化,只是通过Web平台来执行某些操作,但我们认为这只是半自动化,所谓的自动化应该是完全不需要人参与。目前,我们很多操作都还处于半自动化阶段,下一个阶段我们需要从半自动过渡到全自动。以MySQL系统为例,从运维角度看包括主从的高可用、服务过载的自我保护、容量自动诊断与评估以及集群的自动扩缩容等等。现状和面临的挑战下图是我们平台的现状,以关系数据库RDS平台为例,其中集成了很多管理的功能,例如主从的高可用、MGW的管理、DNS的变更、备份系统、升级流程、流量分配和切换系统、账号管理、数据归档、服务与资产的流转系统等等。而且我们按照逻辑对平台设计进行了划分,例如以用户维度划分的RDS自助平台,DBA管理平台和测试环境管理平台;以功能维度划分的运维、运营和监控;以存储类型为维度划分的关系型数据库MySQL、分布式KV缓存、分布式KV存储,以及正在建设中的NewSQL数据库平台等等。未来,我们希望打造成“MySQL+NoSQL+NewSQL,存储+缓存的一站式服务平台”。挑战一:RootCause定位难即便我们打造了一个很强大的平台,但还是发现有很多问题难以搞定。第一个就是故障定位,如果是简单的故障,我们有类似天网、雷达这样的系统去发现和定位。但是如果故障发生在数据库内部,那就需要专业的数据库知识,去定位和查明到底是什么原因导致了故障。通常来讲,故障的轨迹是一个链,但也可能是一个“多米诺骨牌”的连环。可能因为一些原因导致SQL执行变慢,引起连接数的增长,进而导致业务超时,而业务超时又会引发业务不断重试,结果会产生更多的问题。当我们收到一个报警时,可能已经过了30秒甚至更长时间,DBA再去查看时,已经错过了最佳的事故处理时机。所以,我们要在故障发生之后,制定一些应对策略,例如快速切换主库、自动屏蔽下线问题从库等等。除此之外,还有一个比较难的问题,就是如何避免相似的故障再次出现。挑战二:人力和发展困境第二个挑战是人力和发展的困境,当服务流量成倍增长时,其成本并不是以相同的速度对应增长的。当业务逻辑越来越复杂时,每增加一块钱的营收,其后面对应的数据库QPS可能是2倍甚至5倍,业务逻辑越复杂,服务支撑的难度越大。另外,传统的关系型数据库在容量、延时、响应时间以及数据量等方面很容易达到瓶颈,这就需要我们不断拆分集群,同时开发诉求也多种多样,当我们尝试使用平台化的思想去解决问题时,还要充分思考如何满足研发人员多样化的需求。人力困境这一问题,从DBA的角度来说,时间被严重的碎片化,自身的成长就会遇到瓶颈,比如经常会做一些枯燥的重复操作;另外,业务咨询量暴增,尽管我们已经在尝试平台化的方法,但是还是跟不上业务发展的速度。还有一个就是专业的DBA越来越匮乏,越来越贵,关键是根本招聘不到人手。在这种背景下,我们必须去思考:如何突破困局?如何朝着智能化转型?传统运维苦在哪里?智能化运维又能解决哪些问题?首先从故障产生的原因来说,传统运维是故障触发,而智能运维是隐患驱动。换句话来说,智能运维不用报警,通过看报表就能知道可能要出事了,能够把故障消灭在“萌芽”阶段;第二,传统运维是被动接受,而智能运维是主动出击。但主动出击不一定是通过DBA去做,可能是系统或者机器人操作;第三,传统运维是由DBA发起和解决的,而智能运维是系统发起、RD自助;第四,传统运维属于“人肉救火”,而智能运维属于“智能决策执行”;最后一点,传统运维需要DBA亲临事故现场,而智能运维DBA只需要“隐身幕后”。从自动化到智能化那么,如何从半自动化过渡到自动化,进而发展到智能化运维呢?在这个过程中,我们会面临哪些痛点呢?我们的目标是为整个公司的业务系统提供高效、稳定、快速的存储服务,这也是DBA存在的价值。业务并不关心后面是MySQL还是NoSQL,只关心数据是否没丢,服务是否可用,出了问题之后多长时间能够恢复等等。所以我们尽可能做到把这些东西对开发人员透明化,提供稳定高效快速的服务。而站在公司的角度,就是在有限的资源下,提升效率,降低成本,尽可能长远地解决问题。上图是传统运维和智能运维的特点分析,左边属于传统运维,右边属于智能运维。传统运维在采集这一块做的不够,所以它没有太多的数据可供参考,其分析和预警能力是比较弱的。而智能运维刚好是反过来,重采集,很多功夫都在平时做了,包括分析、预警和执行,智能分析并推送关键报表。而我们的目标,是让智能运维中的“报警+分析+执行”的比重占据的越来越少。决策执行如何去做呢?我们都知道,预警重要但不紧急,但报警是紧急且重要的,如果你不能够及时去处理的话,事态可能会扩大,甚至会给公司带来直接的经济损失。预警通常代表我们已经定位了一个问题,它的决策思路是非常清晰的,可以使用基于规则或AI的方式去解决,相对难度更小一些。而报警依赖于现场的链路分析,变量多、路径长,所以决策难,间接导致任何决策的风险可能都变大。所以说我们的策略就是全面的采集数据,然后增多预警,率先实现预警发现和处理的智能化。就像我们既有步枪,也有手枪和刺刀,能远距离解决敌人的,就尽量不要短兵相接、肉搏上阵。数据采集,从数据库角度来说,我们产生的数据分成四块,Global Status、Variable,Processlist、InnoDB Status,Slow、Error、General Log和Binlog;从应用侧来说,包含端到端成功率、响应时间95线、99线、错误日志和吞吐量;从系统层面,支持秒级采样、操作系统各项指标;从变更侧来看,包含集群拓扑调整、在线DDL、DML变更、DB平台操作日志和应用端发布记录等等。数据分析,首先是围绕集群分析,接着是实例、库,最后是表,其中每个对象都可以在多项指标上同比和环比,具体对比项可参考上图。通过上面的步骤,我们基本可以获得数据库的画像,并且帮助我们从整体上做资源规划和服务治理。例如,有些集群实例数特别多且有继续增加的趋势,那么服务器需要scale up;读增加迅猛,读写比变大,那么应考虑存储KV化;利用率和分布情况会影响到服务器采购和预算制定;哪几类报警最多,就专项治理,各个击破。从局部来说,我们根据分析到的一些数据,可以做一个集群的健康体检,例如数据库的某些指标是否超标、如何做调整等等。数据库预警,通过分析去发现隐患,把报警转化为预警。上图是我们实际情况下的报警统计分析结果,其中主从延迟占比最大。假设load.1minPerCPU比较高,我们怎么去解决?那么,可能需要采购CPU单核性能更高的机器,而不是采用更多的核心。再比如说磁盘空间,当我们发现3T的磁盘空间普遍不够时,我们下次可以采购6T或更大空间的磁盘。针对空间预警问题,什么时候需要拆分集群?MySQL数据库里,拆分或迁移数据库,花费的时间可能会很久。所以需要评估当前集群,按目前的增长速度还能支撑多长时间,进而反推何时要开始拆分、扩容等操作。针对慢查询的预警问题,我们会统计红黑榜,上图是统计数据,也有利用率和出轨率的数据。假设这是一个金融事业群的数据库,假设有业务需要访问且是直连,那么这时就会产生几个问题:第一个,有没有数据所有者的授权;第二个,如果不通过服务化方式或者接口,发生故障时,它可能会导致整个金融的数据库挂,如何进行降级?所以,我们会去统计出轨率跟慢查询,如果某数据库正被以一种非法的方式访问,那么我们就会扫描出来,再去进行服务治理。从运维的层面来说,我们做了故障快速转移,包括自动生成配置文件,自动判断是否启用监控,切换后自动重写配置,以及从库可自动恢复上线等等。报警自动处理,目前来说大部分的处理工作还是基于规则,在大背景下拟定规则,触发之后,按照满足的前提条件触发动作,随着库的规则定义的逐渐完善和丰富,可以逐步解决很多简单的问题,这部分就不再需要人的参与。展望未来我们还会做一个故障诊断平台,类似于“扁鹊”,实现日志的采集、入库和分析,同时提供接口,供全链路的故障定位和分析、服务化治理。展望智能运维,应该是在自动化和智能化上交叠演进,在ABC(AI、Big Data、Cloud Computing)三个方向上深入融合。在数据库领域,NoSQL和SQL界限正变得模糊,软硬结合、存储计算分离架构也被越来越多的应用,智能运维正当其时,我们也面临更多新的挑战。我们的目标是,希望通过DB平台的不断建设加固,平台能自己发现问题,自动定位问题,并智能的解决问题。作者简介应钢,美团点评研究员,数据库专家。曾就职于百度、新浪、去哪儿网等,10年数据库自动化运维开发、数据库性能优化、大规模数据库集群技术保障和架构优化经验。精通主流的SQL与NoSQL系统,现专注于公司业务在NewSQL领域的创新和落地。

December 14, 2018 · 1 min · jiezi

智能支付稳定性测试实战

本文根据美团高级测试开发工程师勋伟在美团第43期技术沙龙“美团金融千万级交易系统质量保障之路”的演讲整理而成。主要介绍了美团智能支付业务在稳定性方向遇到的挑战,并重点介绍QA在稳定性测试中的一些方法与实践。背景美团支付承载了美团全部的交易流量,按照使用场景可以将其分为线上支付和智能支付两类业务。线上支付,支撑用户线上消费场景,处理美团所有线上交易,为团购、外卖、酒店旅游等业务线提供支付能力;智能支付,支撑用户到店消费场景,处理美团所有线下交易,通过智能POS、二维码支付、盒子支付等方式,为商家提供高效、智能化的收银解决方案。其中,智能支付作为新扩展的业务场景,去年也成为了美团增速最快的业务之一。面临的挑战而随着业务的快速增长,看似简单的支付动作,背后系统的复杂度却在持续提升。体现在:上层业务入口、底层支付渠道的不断丰富,微服务化背景下系统的纵向分层、服务的横向拆分,还有对外部系统(营销中心、会员中心、风控中心等)、内部基础设施(队列、缓存等)的依赖也越来越多,整条链路上的核心服务节点超过20个,业务复杂度可想而知。此外,技术团队在短时间内就完成了从几个人到近百人规模的扩张,这也是一个潜在的不稳定因素。曾经在一段时间内,整个系统处在“牵一发而动全身”的状态,即使自身系统不做任何发版升级,也会因为一些基础设施、上下游服务的问题,业务会毫无征兆地受到影响。痛定思痛,我们对发生过的线上问题进行复盘,分析影响服务稳定性的原因。通过数据发现,72%的严重故障集中在第三方服务和基础设施故障,对应的一些典型事故场景,比如:第三方支付通道不稳定、基础设施(如消息队列)不稳定,进而导致整个系统雪崩,当依赖方故障恢复后,我们的业务却很难立即恢复。解决方案基于这些问题,我们开展了稳定性建设专项,目的很明确:提升服务的可用性。目标是逐步将系统可用性从2个9提升到3个9,再向4个9去努力。这个过程中最核心的两个策略:柔性可用,意思是尽可能保证核心功能可用,或在有损情况下尽可能保证核心用户体验,降低影响;另一个是快速恢复,即用工具或机制保证故障的快速定位和解决,降低故障修复时间。围绕这两个策略,在稳定性建设中的常见操作:限流、熔断降级、扩容,用于打造系统的柔性可用;故障响应SOP、故障自动处理,用于故障处理时的快速恢复。而QA的工作更侧重于对这些“常见操作”进行有效性验证。基于经验,重点介绍“三把利剑”:故障演练、线上压测、持续运营体系。故障演练的由来举个真实的案例,在一次处理某支付通道不稳定的线上问题时,开发同学执行之前已经测试通过的预案(服务端关闭该通道,预期客户端将该支付通道的开关置灰,并会提示用户使用其他支付方式),但执行中却发现预案无法生效(服务端操作后,客户端该支付通道仍处于开启状态)。非故障场景下预案功能正常,故障场景下却失效了。这就是故障演练的由来,我们需要尽可能还原故障场景,才能真正验证预案的有效性。故障演练的整体方案故障演练的整体方案,主要分为三部分:负载生成模块,负责尽可能还原系统的真实运行场景(要求覆盖核心业务流程)。故障注入模块,包含故障注入工具、故障样本库(涵盖外部服务、基础组件、机房、网络等各种依赖,并重点关注超时、异常两种情况)。业务验证模块,结合自动化测试用例和各个监控大盘来进行。为了更高效地开展故障演练,我们的策略是分为两个阶段进行。首先,针对单系统进行故障演练,从故障样本库出发,全面覆盖该系统所有的保护预案;在此基础上,进行全链路故障演练,聚焦核心服务故障,验证上下游服务的容错性。故障演练的效果事实证明,故障演练确实给我们带来了很多“惊喜”,暴露了很多隐患。这里列举三类问题:数据库主从延迟影响交易;基础设施故障时,业务未做降级;依赖服务超时设置不合理、限流策略考虑不足等。线上压测的由来面对业务的指数级增长,我们必须对系统可承载的流量做到心中有数。对于QA来说,需要找到精准、高效的系统容量评估方法。我们碰到的难点包括:链路长、环节多、服务错综复杂,线下环境与线上差异大等等,基于测试有效性和测试成本考虑,我们决定要做线上压测,而且要实现全链路的线上压测。线上压测的整体方案全链路压测的实现方案,与业界主流方案没有太大区别。根据压测流程,首先,场景建模,以便更真实的还原线上系统运行场景;其次,基础数据构造,应满足数据类型以及量级的要求,避免数据热点;之后,流量构建,读写流量构造或回放,同时对压测流量进行标记和脱敏;再之后,压测执行,过程中收集链路各节点的业务运行状态、资源使用情况等;最后,生成压测报告。基于全链路线上压测方案,可以根据业务需求,灵活地进行单链路压测、分层压测等。更为重要的是,基于压测我们可以进行线上的故障演练,用于更加真实的验证系统限流、熔断等保护预案。线上压测的效果通过全链路线上压测,一方面让我们对系统容量做到心中有数,另一方面也让我们发现了线上系统运行过程中的潜在问题,而且这些问题一般都是高风险的。同样列举三类问题:基础设施优化,如机房负载不均衡、数据库主从延迟严重等;系统服务优化,如线程池配置不合理、数据库需要拆分等;故障预案优化,如限流阈值设置过低,有的甚至已经接近限流边缘而浑然不知等等。持续运营体系的由来智能支付的稳定性建设是作为一个专项在做,持续了近3个月的时间;在效果还不错的情况下,我们从智能支付延伸到整个金融服务平台,以虚拟项目组的方式再次运转了3个月的时间。通过项目方式,确实能集中解决现存的大部分稳定性问题,但业务在发展、系统在迭代,稳定性建设必然是一项长期的工作。于是,QA牵头SRE、DBA、RD,建立了初步的稳定性持续运营体系,并在持续完善。持续运营体系的整体方案下面介绍持续运营体系的三大策略:流程规范工具化,尽可能减少人为意识因素,降低人力沟通和维护成本。如:配置变更流程,将配置变更视同代码上线,以PR方式提交评审;代码规范检查落地到工具,尽可能将编码最佳实践抽取为规则,将人工检查演变为工具检查。质量度量可视化,提取指标、通过数据驱动相关问题的PDCA闭环。如:我们与SRE、DBA进行合作,将线上系统运维中与稳定性相关的指标提取出来,类似数据库慢查询次数、核心服务接口响应时长等等,并对指标数据进行实时监控,进而推进相关问题的解决。演练压测常态化,降低演练和压测成本,具备常态化执行的能力。如:通过自动化的触发演练报警,验证应急SOP在各团队实际执行中的效果。基于以上三个策略,构建稳定性持续运营体系。强调闭环,从质量度量与评价、到问题分析与解决,最终完成方法与工具的沉淀;过程中,通过平台建设来落地运营数据、完善运营工具,提升运营效率。持续运营体系的效果简单展示当前持续运营体系的运行效果,包含风险评估、质量大盘、问题跟进以及最佳实践的沉淀等。未来规划综上便是智能支付QA在稳定性建设中的重点工作。对于未来工作的想法,主要有3个方向。第一,测试有效性提升,持续去扩展故障样本库、优化演练工具和压测方案;第二,持续的平台化建设,实现操作平台化、数据平台化;第三,智能化,逐步从人工运营、自动化运营到尝试智能化运营。作者介绍勋伟,美团高级测试开发工程师,金融服务平台智能支付业务测试负责人,2015年加入美团点评。招聘如果你想学习互联网金融的技术体系,亲历互联网金融业务的爆发式增长,如果你想和我们一起,保证业务产品的高质量,欢迎加入美团金融工程质量组。有兴趣的同学可以发送简历到:fanxunwei#meituan.com。

December 14, 2018 · 1 min · jiezi

iOS App冷启动治理:来自美团外卖的实践

一、背景冷启动时长是App性能的重要指标,作为用户体验的第一道“门”,直接决定着用户对App的第一印象。美团外卖iOS客户端从2013年11月开始,历经几十个版本的迭代开发,产品形态不断完善,业务功能日趋复杂;同时外卖App也已经由原来的独立业务App演进成为一个平台App,陆续接入了闪购、跑腿等其他新业务。因此,更多更复杂的工作需要在App冷启动的时候被完成,这给App的冷启动性能带来了挑战。对此,我们团队基于业务形态的变化和外卖App的特点,对冷启动进行了持续且有针对性的优化工作,目的就是为了呈现更加流畅的用户体验。二、冷启动定义一般而言,大家把iOS冷启动的过程定义为:从用户点击App图标开始到appDelegate didFinishLaunching方法执行完成为止。这个过程主要分为两个阶段:T1:main()函数之前,即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()函数。T2:main()函数之后,即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕。然而,当didFinishLaunchingWithOptions执行完成时,用户还没有看到App的主界面,也不能开始使用App。例如在外卖App中,App还需要做一些初始化工作,然后经历定位、首页请求、首页渲染等过程后,用户才能真正看到数据内容并开始使用,我们认为这个时候冷启动才算完成。我们把这个过程定义为T3。综上,外卖App把冷启动过程定义为:__从用户点击App图标开始到用户能看到App主界面内容为止这个过程,即T1+T2+T3。__在App冷启动过程当中,这三个阶段中的每个阶段都存在很多可以被优化的点。三、问题现状性能存量问题美团外卖iOS客户端经过几十个版本的迭代开发后,在冷启动过程中已经积累了若干性能问题,解决这些性能瓶颈是冷启动优化工作的首要目标,这些问题主要包括:注:启动项的定义,在App启动过程中需要被完成的某项工作,我们称之为一个启动项。例如某个SDK的初始化、某个功能的预加载等。性能增量问题一般情况下,在App早期阶段,冷启动不会有明显的性能问题。冷启动性能问题也不是在某个版本突然出现的,而是随着版本迭代,App功能越来越复杂,启动任务越来越多,冷启动时间也一点点延长。最后当我们注意到,并想要优化它的时候,这个问题已经变得很棘手了。外卖App的性能问题增量主要来自启动项的增加,随着版本迭代,启动项任务简单粗暴地堆积在启动流程中。如果每个版本冷启动时间增加0.1s,那么几个版本下来,冷启动时长就会明显增加很多。四、治理思路冷启动性能问题的治理目标主要有三个:解决存量问题:优化当前性能瓶颈点,优化启动流程,缩短冷启动时间。管控增量问题:冷启动流程规范化,通过代码范式和文档指导后续冷启动过程代码的维护,控制时间增量。完善监控:完善冷启动性能指标监控,收集更详细的数据,及时发现性能问题。五、规范启动流程截止至2017年底,美团外卖用户数已达2.5亿,而美团外卖App也已完成了从支撑单一业务的App到支持多业务的平台型App的演进(美团外卖iOS多端复用的推动、支撑与思考),公司的一些新兴业务也陆续集成到外卖App当中。下面是外卖App的架构图,外卖的架构主要分为三层,底层是基础组件层,中层是外卖平台层,平台层向下管理基础组件,向上为业务组件提供统一的适配接口,上层是基础组件层,包括外卖业务拆分的子业务组件(外卖App和美团App中的外卖频道可以复用子业务组件)和接入的其他非外卖业务。App的平台化为业务方提供了高效、标准的统一平台,但与此同时,平台化和业务的快速迭代也给冷启动带来了问题:现有的启动项堆积严重,拖慢启动速度。新的启动项缺乏添加范式,杂乱无章,修改风险大,难以阅读和维护。面对这个问题,我们首先梳理了目前启动流程中所有的启动项,然后针对App平台化设计了新的启动项管理方式:__分阶段启动和启动项自注册__分阶段启动早期由于业务比较简单,所有启动项都是不加以区分,简单地堆积到didFinishLaunchingWithOptions方法中,但随着业务的增加,越来越多的启动项代码堆积在一起,性能较差,代码臃肿而混乱。通过对SDK的梳理和分析,我们发现启动项也需要根据所完成的任务被分类,有些启动项是需要刚启动就执行的操作,如Crash监控、统计上报等,否则会导致信息收集的缺失;有些启动项需要在较早的时间节点完成,例如一些提供用户信息的SDK、定位功能的初始化、网络初始化等;有些启动项则可以被延迟执行,如一些自定义配置,一些业务服务的调用、支付SDK、地图SDK等。我们所做的分阶段启动,首先就是把启动流程合理地划分为若干个启动阶段,然后依据每个启动项所做的事情的优先级把它们分配到相应的启动阶段,优先级高的放在靠前的阶段,优先级低的放在靠后的阶段。下面是我们对美团外卖App启动阶段进行的重新定义,对所有启动项进行的梳理和重新分类,把它们对应到合理的启动阶段。这样做一方面可以推迟执行那些不必过早执行的启动项,缩短启动时间;另一方面,把启动项进行归类,方便后续的阅读和维护。然后把这些规则落地为启动项的维护文档,指导后续启动项的新增和维护。通过上面的工作,我们梳理出了十几个可以推迟执行的启动项,占所有启动项的30%左右,有效地优化了启动项所占的这部分冷启动时间。启动项自注册确定了启动项分阶段启动的方案后,我们面对的问题就是如何执行这些启动项。比较容易想到的方案是:在启动时创建一个启动管理器,然后读取所有启动项,然后当时间节点到来时由启动器触发启动项执行。这种方式存在两个问题:所有启动项都要预先写到一个文件中(在.m文件import,或用.plist文件组织),这种中心化的写法会导致臃肿的代码,难以阅读维护。启动项代码无法复用:启动项无法收敛到子业务库内部,在外卖App和美团App中要重复实现,和外卖App平台化的方向不符。而我们希望的方式是,启动项维护方式可插拔,启动项之间、业务模块之间不耦合,且一次实现可在两端复用。下图是我们采用的启动项管理方式,我们称之为启动项的自注册:一个启动项定义在子业务模块内部,被封装成一个方法,并且自声明启动阶段(例如一个启动项A,在独立App中可以声明为在willFinishLaunch阶段被执行,在美团App中则声明在resignActive阶段被执行)。这种方式下,启动项即实现了两端复用,不相关的启动项互相隔离,添加/删除启动项都更加方便。那么如何给一个启动项声明启动阶段?又如何在正确的时机触发启动项的执行呢?在代码上,一个启动项最终都会对应到一个函数的执行,所以在运行时只要能获取到函数的指针,就可以触发启动项。美团平台开发的组件启动治理基建Kylin正是这样做的:Kylin的核心思想就是在编译时把数据(如函数指针)写入到可执行文件的__DATA段中,运行时再从__DATA段取出数据进行相应的操作(调用函数)。为什么要用借用__DATA段呢?原因就是为了能够覆盖所有的启动阶段,例如main()之前的阶段。Kylin实现原理简述:Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute((section())) 的数据写到指定的数据段中,例如写一个{key(key代表不同的启动阶段), *pointer}对到数据段。到运行时,在合适的时间节点,在根据key读取出函数指针,完成函数的调用。上述方式,可以封装成一个宏,来达到代码的简化,以调用宏 KLN_STRINGS_EXPORT(“Key”, “Value”)为例,最终会被展开为:attribute((used, section("__DATA" "," "kylin"))) static const KLN_DATA __kylin__0 = (KLN_DATA){(KLN_DATA_HEADER){“Key”, KLN_STRING, KLN_IS_ARRAY}, “Value”};使用示例,编译器把启动项函数注册到启动阶段A:KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在a.m文件中,通过注册宏,把启动项A声明为在STAGE_KEY_A阶段执行 // 启动项代码A}KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在b.m文件中,把启动项B声明为在STAGE_KEY_A阶段执行 // 启动项代码B}在启动流程中,在启动阶段STAGE_KEY_A触发所有注册到STAGE_KEY_A时间节点的启动项,通过对这种方式,几乎没有任何额外的辅助代码,我们用一种很简洁的方式完成了启动项的自注册。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { // 其他逻辑 [[KLNKylin sharedInstance] executeArrayForKey:STAGE_KEY_A]; // 在此触发所有注册到STAGE_KEY_A时间节点的启动项 // 其他逻辑 return YES;}完成对现有的启动项的梳理和优化后,我们也输出了后续启动项的添加&维护规范,规范后续启动项的分类原则,优先级和启动阶段。目的是管控性能问题增量,保证优化成果。六、优化main()之前在调用main()函数之前,基本所有的工作都是由操作系统完成的,开发者能够插手的地方不多,所以如果想要优化这段时间,就必须先了解一下,操作系统在main()之前做了什么。main()之前操作系统所做的工作就是把可执行文件(Mach-O格式)加载到内存空间,然后加载动态链接库dyld,再执行一系列动态链接操作和初始化操作的过程(加载、绑定、及初始化方法)。这方面的资料网上比较多,但重复性较高,此处附上一篇WWDC的Topic:Optimizing App Startup Time 。加载过程—从exec()到main()真正的加载过程从exec()函数开始,exec()是一个系统调用。操作系统首先为进程分配一段内存空间,然后执行如下操作:把App对应的可执行文件加载到内存。把Dyld加载到内存。Dyld进行动态链接。下面我们简要分析一下Dyld在各阶段所做的事情:阶段工作 加载动态库Dyld从主执行文件的header获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以所需要加载的是动态库列表一个递归依赖的集合 Rebase和Bind- Rebase在Image内部调整指针的指向。在过去,会把动态库加载到指定地址,所有指针和数据对于代码都是对的,而现在地址空间布局是随机化,所以需要在原来的地址根据随机的偏移量做一下修正- Bind是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定,dyld需要去符号表里查找,找到symbol对应的实现 Objc setup- 注册Objc类 (class registration) - 把category的定义插入方法列表 (category registration) - 保证每一个selector唯一 (selector uniquing) Initializers- Objc的+load()函数 - C++的构造函数属性函数 - 非基本类型的C++静态全局变量的创建(通常是类或结构体) 最后 dyld 会调用 main() 函数,main() 会调用 UIApplicationMain(),before main()的过程也就此完成。了解完main()之前的加载过程后,我们可以分析出一些影响T1时间的因素:动态库加载越多,启动越慢。ObjC类,方法越多,启动越慢。ObjC的+load越多,启动越慢。C的constructor函数越多,启动越慢。C++静态对象越多,启动越慢。针对以上几点,我们做了如下一些优化工作:代码瘦身随着业务的迭代,不断有新的代码加入,同时也会废弃掉无用的代码和资源文件,但是工程中经常有无用的代码和文件被遗弃在角落里,没有及时被清理掉。这些无用的部分一方面增大了App的包体积,另一方便也拖慢了App的冷启动速度,所以及时清理掉这些无用的代码和资源十分有必要。通过对Mach-O文件的了解,可以知道__TEXT:__objc_methname:中包含了代码中的所有方法,而__DATA__objc_selrefs中则包含了所有被使用的方法的引用,通过取两个集合的差集就可以得到所有未被使用的代码。核心方法如下,具体可以参考:objc_cover:def referenced_selectors(path): re_sel = re.compile("__TEXT:__objc_methname:(.+)") //获取所有方法 refs = set() lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() # ios & mac //真正被使用的方法 for line in lines: results = re_sel.findall(line) if results: refs.add(results[0]) return refs}通过这种方法,我们排查了十几个无用类和250+无用的方法。+load优化目前iOS App中或多或少的都会写一些+load方法,用于在App启动执行一些操作,+load方法在Initializers阶段被执行,但过多+load方法则会拖慢启动速度,对于大中型的App更是如此。通过对App中+load的方法分析,发现很多代码虽然需要在App启动时较早的时机进行初始化,但并不需要在+load这样非常靠前的位置,完全是可以延迟到App冷启动后的某个时间节点,例如一些路由操作。其实+load也可以被当做一种启动项来处理,所以在替换+load方法的具体实现上,我们仍然采用了上面的Kylin方式。使用示例:// 用WMAPP_BUSINESS_INIT_AFTER_HOMELOADING声明替换+load声明即可,不需其他改动WMAPP_BUSINESS_INIT_AFTER_HOMELOADING() { // 原+load方法中的代码}// 在某个合适的时机触发注册到该阶段的所有方法,如冷启动结束后[[KLNKylin sharedInstance] executeArrayForKey:@kWMAPP_BUSINESS_INITIALIZATION_AFTER_HOMELOADING_KEY] }七、优化耗时操作在main()之后主要工作是各种启动项的执行(上面已经叙述),主界面的构建,例如TabBarVC,HomeVC等等。资源的加载,如图片I/O、图片解码、archive文档等。这些操作中可能会隐含着一些耗时操作,靠单纯阅读非常难以发现,如何发现这些耗时点呢?找到合适的工具就会事半功倍。Time ProfilerTime Profiler是Xcode自带的时间性能分析工具,它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。Time Profiler的使用方法网上有很多使用教程,这里我们也不过多介绍,附上一篇使用文档:Instruments Tutorial with Swift: Getting Started。火焰图除了Time Profiler,火焰图也是一个分析CPU耗时的利器,相比于Time Profiler,火焰图更加清晰。火焰图分析的产物是一张调用栈耗时图片,之所以称为火焰图,是因为整个图形看起来就像一团跳动的火焰,火焰尖部是调用栈的栈顶,底部是栈底,纵向表示调用栈的深度,横向表示消耗的时间。一个格子的宽度越大,越说明其可能是瓶颈。分析火焰图主要就是看那些比较宽大的火苗,特别留意那些类似“平顶山”的火苗。下面是美团平台开发的性能分析工具-Caesium的分析效果图:通过对火焰图的分析,我们发现了冷启动过程中存在着不少问题,并成功优化了0.3S+的时间。优化内容总结如下:优化点举例发现隐晦的耗时操作发现在冷启动过程中archive了一张图片,非常耗时推迟&减少I/O操作减少动画图片组的数量,替换大图资源等。因为相比于内存操作,硬盘I/O是非常耗时的操作推迟执行的一些任务如一些资源的I/O,一些布局逻辑,对象的创建时机等八、优化串行操作在冷启动过程中,有很多操作是串行执行的,若干个任务串行执行,时间必然比较长。如果能变串行为并行,那么冷启动时间就能够大大缩短。闪屏页的使用现在许多App在启动时并不直接进入首页,而是会向用户展示一个持续一小段时间的闪屏页,如果使用恰当,这个闪屏页就能帮我们节省一些启动时间。因为当一个App比较复杂的时候,启动时首次构建App的UI就是一个比较耗时的过程,假定这个时间是0.2秒,如果我们是先构建首页UI,然后再在Window上加上这个闪屏页,那么冷启动时,App就会实实在在地卡住0.2秒,但是如果我们是先把闪屏页作为App的RootViewController,那么这个构建过程就会很快。因为闪屏页只有一个简单的ImageView,而这个ImageView则会向用户展示一小段时间,这时我们就可以利用这一段时间来构建首页UI了,一举两得。缓存定位&首页预请求美团外卖App冷启动过程中一个重要的串行流程就是:首页定位–>首页请求–>首页渲染过程,这三个操作占了整个首页加载时间的77%左右,所以想要缩短冷启动时间,就一定要从这三点出发进行优化。之前串行操作流程如下:优化后的设计,在发起定位的同时,使用客户端缓存定位,进行首页数据的预请求,使定位和请求并行进行。然后当用户真实定位成功后,判断真实定位是否命中缓存定位,如果命中,则刚才的预请求数据有效,这样可以节省大概40%的时间首页加载时间,效果非常明显;如果未命中,则弃用预请求数据,重新请求。九、数据监控Time Profiler和Caesium火焰图都只能在线下分析App在单台设备中的耗时操作,局限性比较大,无法在线上监控App在用户设备上的表现。外卖App使用公司内部自研的Metrics性能监控系统,长期监控App的性能指标,帮助我们掌握App在线上各种环境下的真实表现,并为技术优化项目提供可靠的数据支持。Metrics监控的核心指标之一,就是冷启动时间。冷启动开始&结束时间节点结束时间点:结束时间比较好确定,我们可以将首页某些视图元素的展示作为首页加载完成的标志。开始时间点:一般情况下,我们都是在main()之后才开始接管App,但以main()函数作为冷启动起始点显然不合适,因为这样无法统计到T1时间段。那么,起始时间如何确定呢?目前业界常见的有两种方法,一是以可执行文件中任意一个类的+load方法的执行时间作为起始点;二是分析dylib的依赖关系,找到叶子节点的dylib,然后以其中某个类的+load方法的执行时间作为起始点。根据Dyld对dylib的加载顺序,后者的时机更早。但是这两种方法获取的起始点都只在Initializers阶段,而Initializers之前的时长都没有被计入。Metrics则另辟蹊径,以App的进程创建时间(即exec函数执行时间)作为冷启动的起始时间。因为系统允许我们通过sysctl函数获得进程的有关信息,其中就包括进程创建的时间戳。#import <sys/sysctl.h>#import <mach/mach.h>+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc)procInfo{ int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; size_t size = sizeof(*procInfo); return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;}+ (NSTimeInterval)processStartTime{ struct kinfo_proc kProcInfo; if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) { return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0; } else { NSAssert(NO, @“无法取得进程的信息”); return 0; }}进程创建的时机非常早。经过实验,在一个新建的空白App中,进程创建时间比叶子节点dylib中的+load方法执行时间早12ms,比main函数的执行时间早13ms(实验设备:iPhone 7 Plus (iOS 12.0)、Xcode 10.0、Release 模式)。外卖App线上的数据则更加明显,同样的机型(iPhone 7 Plus)和系统版本(iOS 12.0),进程创建时间比叶子节点dylib中的+load方法执行时间早688ms。而在全部机型和系统版本中,这一数据则是878ms。冷启动过程时间节点我们也在App冷启动过程中的所有关键节点打上一连串测速点,Metrics会记录下测速点的名称,及其距离进程创建时间的时长。我们没有采用自动打点的方式,是因为外卖App的冷启动过程十分复杂,而自动打点无法做到如此细致,并不实用。另外,Metrics记录的是时间轴上以进程创建时间为原点的一组顺序的时间点,而不是一组时间段,是因为顺序的时间点可以计算任意两个时间点之间的距离,即可以将时间点处理成时间段。但是,一组时间段可能无法还原为顺序的时间点,因为时间段之间可能并不是首尾相接的,特别是对于异步执行或者多线程的情况。在测速完毕后,Metrics会统一将所有测速点上报到后台。下图是美团外卖App 6.10版本的部分过程节点监控数据截图:Metrics还会由后台对数据做聚合计算,得到冷启动总时长和各个测速点时长的50分位数、90分位数和95分位数的统计数据,这样我们就能从宏观上对冷启动时长分布情况有所了解。下图中横轴为时长,纵轴为上报的样本数。十、总结对于快速迭代的App,随着业务复杂度的增加,冷启动时长会不可避免的增加。冷启动流程也是一个比较复杂的过程,当遇到冷启动性能瓶颈时,我们可以根据App自身的特点,配合工具的使用,从多方面、多角度进行优化。同时,优化冷启动存量问题只是冷启动治理的第一步,因为冷启动性能问题并不是一日造成的,也不能简单的通过一次优化工作就能解决,我们需要通过合理的设计、规范的约束,来有效地管控性能问题的增量,并通过持续的线上监控来及时发现并修正性能问题,这样才能够长期保证良好的App冷启动体验。作者简介郭赛,美团点评资深工程师。2015年加入美团,目前作为外卖iOS团队主力开发,负责移动端业务开发,业务类基础设施的建设与维护。徐宏,美团点评资深工程师。2016年加入美团,目前作为外卖iOS团队主力开发,负责移动端APM性能监控,高可用基础设施支撑相关推进工作。招聘美团外卖长期招聘Android、iOS、FE高级/资深工程师和技术专家,Base北京、上海、成都,欢迎有兴趣的同学投递简历到chenhang03@meituan.com。 ...

December 7, 2018 · 1 min · jiezi

【人物志】技术十年:美团第一位前端工程师潘魏增

导读潘魏增,2006年毕业于南开大学电子系,2008年加入早期饭否团队。美团第一位前端工程师,现在是X项目组终端研发部的负责人。处女座,INTJ,喜欢Linux和Vim,崇尚开源,相信开源可以让世界变得更美好。从饭否到美团,潘魏增用十年的技术生涯,诠释了“长期有耐心”这句话的含义。在他看来,长期有耐心,其实也是延迟满足感。对从事的行业来讲,我们要把眼光放得更长远一些,十年后才有回报的生意,往往都是大买卖。对个人来讲,不要把职位、职级这些虚的东西看得过重,关键看我们自己在其中承担什么角色,看我们自己的能力是否还有成长的空间。本文系美团技术学院美美对潘魏增的采访内容,希望对大家有所启发。从电子工程转到计算机1. 为什么大学读的是电子系,但是毕业后却选择了互联网行业?潘魏增:高中时,对物理比较感兴趣,学校有一个逸夫图书馆,里面有大量关于物理的课外读物,其中有一本杂志叫《无线电》,特别令我着迷。只需要少量元器件,就可以实现超远距离无线单向通信(收音机),简直太神奇了。于是,我就树立了自己的理想,以后要成为一名电子工程领域的科学家,所以选择了南开大学的电子系。 但是上大学之后突然发现,电子系的课程大部分都是以数学、物理相关的基础理论为主,动手创造的机会很少,特别枯燥。一次偶然的机会,想在电脑上搭建一个HTTP服务器,给各地的高中同学访问,因为不太懂,然后就去学校的BBS请教。当搭建成功的一刹那,我突然感觉到一种“触电”一样的兴奋感,相隔万里的人竟然可以看到我写的网页内容,太不可思议了。于是,我就开始经常泡BBS学习,到后来我就成为了能够回答别人问题的人,再后来,我就成为了南开BBS上WebDevelop版和Linux版的版主。 大学时,互联网逐渐从第一次泡沫中复苏。我在图书馆偶然看到一本讲互联网革命的书,书中那些早期设想的有关互联网的预言,都逐渐在一一实现。我深受作者的鼓舞,不过我觉得互联网革命还尚在早期,未来还将获得更加蓬勃的发展,我应该在行业萌芽的时候,加入到这场浪潮当中去。怎么加入?我不可能去「赤手空拳」地创业,毕竟还要吃饭,于是去互联网公司工作就成了我最佳的实际选择。当时在我们系,去互联网公司工作,其实是一个非常另类的、不被人理解的选择,因为绝大部分同学都去做了跟电路或者芯片等本专业相关的工作。但是,我很喜欢。 今天回头看,我呆过的团队做了很多改变世界的事,整个互联网行业也大大地改变了世界的原貌,信息更透明、公开,社会更加平等。科技革命总是短期被高估,长期被忽视。十多年前,我还用笔给同学写信,在图书馆翻查资料,出门带纸质地图……站回当时看现在,几乎是难以想象的:我们可以通过微信实现实时互动,通过知乎、维基百科和搜索引擎查阅无穷无尽的信息,出门有高德地图,而且现在出门还几乎不用带现金。2. 当时为什么会选择凤凰网?在凤凰网自己收获了哪些?潘魏增:读大学时,比较喜欢看凤凰卫视的节目,而凤凰网是凤凰卫视集团旗下的子公司。凤凰的节目内容在当时来讲是媒体界的一股清流,这是一群有理想、有抱负的人做的媒体,我希望能和这些人一起工作。而且凤凰网也是最早设立前端岗位的公司之一,这和我的职业方向非常匹配。当我收到Offer的时候,就立即过来报道了,甚至薪水给多少,我都没问。在凤凰网时,也学到了很多东西,主要是两点: 第一,完成学生到职业人的转变。加入公司后不久,老板就给机会负责整个网站的重构。迁移新闻系统工作量巨大,连续加班两个多月后,看到自己开发的作品每天有千千万万的网友在使用,非常有成就感。凤凰网是我职业的起点,一直心存感激。 第二,学到做事做人的一些方法。在大学里认为技术人总是特立独行,但在实际工作当中必须依赖团队协作。做事要认真,做人要简单。在凤凰两年,我做过很多次前端技术和用户体验相关的培训,同事们都乐于跟我请教,包括跟编辑、设计、销售等部门关系也特别好,很欣慰。在饭否的那些日子3. 因为什么原因加入的饭否?有什么故事?潘魏增:从大学开始就喜欢自己在网上写点文章,分享自己的成长心得。2008年初,穆荣均(美团联合创始人)在网上看到我写推荐饭否的博客,然后写邮件邀请我出来喝咖啡。我们喝了几次咖啡,有一次还在五道口的办公室见了兴哥(美团创始人王兴)。没过多久,就收到了饭否的工作邀请,这也是我工作之后,第一次正式收到其他团队的邀请,有点“奇遇”的感觉。我认真考虑了一段时间,然后也咨询了几个师哥和朋友,大家都认为饭否是非常靠谱的团队,于是,我就下决心过来了。4. 在饭否几年中主要负责哪些工作?当时你对整个团队的感觉是什么?潘魏增:在饭否时期,我在团队中的主要职责是负责前端开发,偶尔客串后端、运维、设计师、讲师以及客服。 刚加入这个团队的时候,第一印象是感觉这个团队特别酷。第一次开会,工程师直接用Firebug在投影仪上修改代码,直接浏览器上看效果,然后和大家一起现场讨论。平时的各种文档都是在Google Docs上一起协作完成,在同一份文档上,经常看到几个光标来回游走修改,现在感觉都还很科技、梦幻。 其次,团队每一个成员都很优秀。例如有工程师把PHP手册读到烂熟于心,了解这门语言的方方面面,版本升级也只是看看手册的Diff。 最重要的一点,感觉大家都很拼。我到入职之后才知道,我们一周要上6天班,工作时间是早上10点到晚上10点,经常会忙到凌晨,上班也不用打卡,全靠自觉和相互监督。5. 加入饭否时,也正值青春年少,有什么梦想吗?当时有没有考虑过职业规划这些问题吗?潘魏增:青春总是有很多冲动嘛,一心想要去改变世界。在饭否,我们的梦想就是让信息更公开,社会更平等,让信息流动更快。 职业规划方面,想得真的不太多。当时的想法,就是想往前端技术专家的方向努力一下,能对行业产生一些价值,个人能有一些影响力,就很知足了。6. 从饭否到美团这些年,为什么没有考虑过换一个环境?是什么原因让自己坚持下来?潘魏增:在饭否被关闭之后的那半年里,突然没那么忙了。坦白讲那段时间,确实有考虑过是否要换个环境,所以说,闲并不是什么好事,容易滋生各种想法(笑)。我有几个师哥和朋友知道我的事情之后,也给我介绍过几个工作,最后都被我婉拒了。在饭否呆过之后,对团队的要求突然变高了很多,开始知道什么是好的,什么是不好的。在美团的日子,工作更是超级忙,就压根没空去想过这个事情。 我很享受在公司工作的时光,喜欢现在的工作。其实,好的工作,不需要坚持。 在创业团队头两年,闷头写代码,技术每天都有进步(开心)。之后开始建团队、Coach团队,从个人贡献者到团队贡献者,公司给我机会从零开始学管理,从一张白纸开始慢慢明白一些道理(开心)。 然后团队规模越来越大,业务越来越复杂,管理能力迭代更新,我培养过很多个技术Leader(更开心)。 其实,开心的工作基本不需要谈“坚持”,它会给你持续的、正向的激励。不好的工作才需要咬牙坚持,而坚持错误,是一件机会成本很高的事情。工程师眼中的美团7. 在你眼中,我们美团是一家什么样的公司?有哪些特质让你印象深刻?潘魏增:印象最深的,也是最喜欢的特质就是我们美团是一家学习型组织。公司创始人都非常善于去学习、思考和总结,并身体力行去分享、去鼓励大家这么做。 比如兴哥,我觉得他的学习面特别广。记得有一次聚会,兴哥聊天时说到各地的方言,还帮忙给大家各自的方言做归类,聊到兴致起来,还拿出一本厚厚的语言书,证明他说的都有理有据。后来还有一次,兴哥给大家送了一本关于地缘结构的书,他说不错,推荐大家看。我看完之后确实对世界格局有了新的认识,对我帮助很大。 兴哥能从学习中受益,我觉得很多人应该也是如此。学城(美团内部知识库)和互联网+大学,有大量内部学习的资料,有大牛的分享,有行业的判断,有方法论,我自己也从上面学到了不少真东西,大家可以利用一些这些资源。8. 千团大战后,美团又做了电影票(猫眼,已经独立)、外卖业务,现在布局了酒旅、出行和大零售业务,作为其中的一员,最大的感触是什么?潘魏增:最大的感触就是,每天都像在“打仗”。我们美团进入的领域一部分并不是最新的领域,另外一部分可能相对较新,同时有很多人也都看到了这个机会。表面上,美团像是和其他公司在争夺,其实不然,实际上大家都在竞赛,看谁能给用户提供更大的价值,提供更好的产品。听说有些公司,专门针对美团建立了“抗美办公室”,我觉得他们的思路很奇怪,还在用“零和游戏”的思维在做事情。前些天,王慧文也分享了自己对这方面的看法,美团就是一种“尝试”的心态在做事,我们最终是希望给用户带来更大的价值。美团的价值观,第一条也是“以客户为中心”。 因为竞争是全方位的,有时候看到其他公司写的“黑稿”,里面尽是一些没有逻辑的猜测和诬陷。这个时候,我们反而更像是打了“鸡血”一样,想把我们自己的产品做的更好。 当然,这么多年,也经历了美团的很多从零到一的业务,有时候会感觉会比较煎熬,有时候也会很亢奋,就像坐过山车,有时候嗨到顶峰,有时候也会感觉跌到谷底,但尖叫总是持续不断的。在美团的生活,真的很精彩。十年技术生涯9. 对绝大多数青年来说,大学毕业后,应该是人生最好的十年,这十年最大的收获是什么?您怎么理解王兴说的“长期有耐心”这句话?潘魏增:十年的收获有很多,不过我感觉都是人生体验的东西,到这个时间点,到了这个岁数,每个人总会零零散散收获一些东西,包括物质层面的,还有精神层面的。对我而言,最大的收获可能是更了解了自己,找到一些和自己更好相处的方式。十年前,觉得自己什么都能做,什么都可以做得很好,现在大概明白自己的能力范围在哪里。十年前,不明白自己想要什么,现在尽管依然不是那么清楚,但已经相对更清晰一些,还需要迭代。 长期有耐心,我的理解就是延迟满足感。对从事的行业来讲,要把眼光放得更长远一些。做个不是那么恰当的比喻,每天计较得失的大部分都难成气候,十年后才有回报的生意,往往都是大买卖。对个人而言,不要太早着急“变现”,拿得多,往往不如拿得稳,也不要把职位、职级这些虚的东西看得过重。关键看自己在其中承担什么角色,看自己的能力是否还有成长的空间。10. 这十年,有没有对自己影响特别大的人?潘魏增:十年间,我有幸认识了很多非常优秀的人。有老板,有同事,也有人生路上的师哥、朋友等,对我影响都很大,感谢得到了他们的教诲和帮助,也从他们身上学到了不少东西。 坦白讲,我自己受穆荣均的影响比较深。我在公司里最早接触的人就是他,也是因为他,我被吸引加入了早期的饭否。他认为应该坚持做正确的事情,即使遇到阻力,也要不折不挠地推进。 比如,技术团队早期要提升并行开发的效率,版本管理工具要从SVN迁移到Git,这个事情虽然是我主导,但事实上是穆荣均在背后鼓励我这么做。 又比如刚开始的美团技术学院,有很多BootCamp培训、技术交流和组织上的杂事要做,同时会面临业务上的一些压力,穆荣均也是很支持我去展开这方面的工作。我相信对早期工程师文化的建设和团队组织的成长,都是有帮助的。11. 这十年,对前端技术的认知,有哪些改变?潘魏增:虽然十年来,前端技术层出不穷,但我理解的前端是「万变不离其宗」,它是为最终用户界面服务的,承接用户与远端数据的交互。前端的核心是数据的呈现,不管后端给什么数据,前端只负责忠实的展示。前端也可能会做服务端的开发,也可能会做平台化、工程化的工具,但它最根本的目标还是这个,不要偏离这个目标去做事情。 其次前端技术不能脱离业务而存在。前端工程师当中有部分同学很Geek,有的喜欢研究专深的技术,有的喜欢越界做点事情。这些本身也没错,但把更多精力去推动业务,获得成功是回报更高的事情。如果做技术缺乏业务视角,往往是很危险的。爱健身,爱读书,爱思考12. 平时有哪些兴趣爱好?这些兴趣爱好,对自己有什么影响?潘魏增:我基本没有固定的兴趣爱好,不同阶段有不同的兴趣点。以前喜欢研究跑鞋、键盘、Hack电纸书,现在都“退烧”了。喜欢过跑步、游泳、爬山,也都“收山”了。 目前还在坚持的、时间最长的是健身,每周定期活动活动身体。我要争取比同龄人体态更好一些,头发掉得更少一些(笑)。 受家人和朋友的影响,我开始喜欢旅游。我曾经信奉李敖先生所推崇的“读两万卷书,行零里路”,但去过一些地方以后,才发现“知道”和“看见”是两种不同层次的感受。比如在川西,路过四姑娘山时,山就那么横在我面前,让我突然意识到自己的渺小,冲击力特别大,和图片上看到的完全不一样。 还有,自从学会开车上路以后,也喜欢研究汽车知识和汽车文化。互联网是“比特在两点之间的移动”,汽车是“原子在两点间的移动”,这些概念都挺有意思的。13. 读书多吗?哪几本对自己影响比较大?潘魏增:比较惭愧,我读书很慢,所以读书并不是很多。豆瓣上标记想读的书永远比读过的书多很多。 我觉得读书有两个乐趣,一个是了解自己,另外一个是探索世界。内向者的能量大部分来自读书和独处时的思考,就我来说,读书的比重可能更大一些,所以读书还会给我带来力量。这里有几个读书心得跟大家分享:一、读好书。好书会改变一些东西,例如思维方式、生活理念甚至对世界的认知,而烂书只会浪费时间。找好书有三个渠道,豆瓣评分很高的;大牛文章总结的好书;朋友同事推荐。其中我最认朋友们的推荐,他们一般不推荐书,一旦推荐都极其靠谱,所以可以结交一些爱读书、会读书的朋友。二、做笔记。写笔记可以概括重点、理清思路,对于理解书中的内容有很大帮助。即便不一定会再看一次笔记,但做笔记的这个过程可以帮助加深记忆,书中道理也会通过记笔记的形式融入到潜意识当中。这跟抄佛经有点类似,不同的是,做笔记不要摘抄太多原文,那样没有意义,应该去总结、抽象,读书是为了获得思想,而非文字本身。三、回到初心。要回到自己读书的初衷,或乐趣,或好奇,少一些目的性和功利心。读一本书,不要老想它能用来干什么,而是多思考它在表达什么。不要期望每本书读了都有用,马上就能用的书是菜谱。最后给大家推荐几本我觉得不错的书。其中《禅与摩托车维修艺术》、《邓小平时代》和《素书》是大书,有些部分晦涩难懂,但值得反复咀嚼,像好酒,越喝越香;《请理解我》和《断舍离》是小书,实操简单,效果立竿见影,也许能对工作和生活带来一些改变。14. 对做技术的同学们,有哪些建议?潘魏增:有几个建议,也是我个人的一些成长心得,仅供大家参考吧,希望能给大家带来一点帮助。第一个是打好基础。 毕业刚走进职场的同学,一定要打好技术基础。「勿在浮沙筑高台」,把地基打扎实,才能在上面建成高楼大厦。怎么打地基,每个人有自己的方式,我个人比较喜欢的是看官方手册、标准文档以及阅读源代码。 第二个是提升视野。对于有一定经验和技术基础的工程师,建议多走出去,看看公司内其他团队是怎么做,业界是怎么做的。好的技术往往是因为看得足够多。 第三个是思考本质。“老司机”可以多跳出来想想商业的本质、社会的本质,毕竟技术只是这个世界很小的“子集”。我们的社会是一个非常复杂的系统,结构远比技术系统更加复杂。我也是最近几年才开始有这个认识,还在慢慢地摸索、学习。希望大家共同努力吧!

December 7, 2018 · 1 min · jiezi

美团即时物流的分布式系统架构设计

本文根据美团资深技术专家宋斌在ArchSummit架构师峰会上的演讲整理而成。背景美团外卖已经发展了五年,即时物流探索也经历了3年多的时间,业务从零孵化到初具规模,在整个过程中积累了一些分布式高并发系统的建设经验。最主要的收获包括两点:即时物流业务对故障和高延迟的容忍度极低,在业务复杂度提升的同时也要求系统具备分布式、可扩展、可容灾的能力。即时物流系统阶段性的逐步实施分布式系统的架构升级,最终解决了系统宕机的风险。围绕成本、效率、体验核心三要素,即时物流体系大量结合AI技术,从定价、ETA、调度、运力规划、运力干预、补贴、核算、语音交互、LBS挖掘、业务运维、指标监控等方面,业务突破结合架构升级,达到促规模、保体验、降成本的效果。本文主要介绍在美团即时物流分布式系统架构逐层演变的进展中,遇到的技术障碍和挑战:订单、骑手规模大,供需匹配过程的超大规模计算问题。遇到节假日或者恶劣天气,订单聚集效应,流量高峰是平常的十几倍。物流履约是线上连接线下的关键环节,故障容忍度极低,不能宕机,不能丢单,可用性要求极高。数据实时性、准确性要求高,对延迟、异常非常敏感。美团即时物流架构美团即时物流配送平台主要围绕三件事展开:一是面向用户提供履约的SLA,包括计算送达时间ETA、配送费定价等;二是在多目标(成本、效率、体验)优化的背景下,匹配最合适的骑手;三是提供骑手完整履约过程中的辅助决策,包括智能语音、路径推荐、到店提醒等。在一系列服务背后,是美团强大的技术体系的支持,并由此沉淀出的配送业务架构体系,基于架构构建的平台、算法、系统和服务。庞大的物流系统背后离不开分布式系统架构的支撑,而且这个架构更要保证高可用和高并发。分布式架构,是相对于集中式架构而言的一种架构体系。分布式架构适用CAP理论(Consistency 一致性,Availability 可用性,Partition Tolerance 分区容忍性)。在分布式架构中,一个服务部署在多个对等节点中,节点之间通过网络进行通信,多个节点共同组成服务集群来提供高可用、一致性的服务。早期,美团按照业务领域划分成多个垂直服务架构;随着业务的发展,从可用性的角度考虑做了分层服务架构。后来,业务发展越发复杂,从运维、质量等多个角度考量后,逐步演进到微服务架构。这里主要遵循了两个原则:不宜过早的进入到微服务架构的设计中,好的架构是演进出来的不是提前设计出来的。分布式系统实践上图是比较典型的美团技术体系下的分布式系统结构:依托了美团公共组件和服务,完成了分区扩容、容灾和监控的能力。前端流量会通过HLB来分发和负载均衡;在分区内,服务与服务会通过OCTO进行通信,提供服务注册、自动发现、负载均衡、容错、灰度发布等等服务。当然也可以通过消息队列进行通信,例如Kafka、RabbitMQ。在存储层使用Zebra来访问分布式数据库进行读写操作。利用CAT(美团开源的分布式监控系统)进行分布式业务及系统日志的采集、上报和监控。分布式缓存使用Squirrel+Cellar的组合。分布式任务调度则是通过Crane。在实践过程还要解决几个问题,比较典型的是集群的扩展性,有状态的集群可扩展性相对较差,无法快速扩容机器,无法缓解流量压力。同时,也会出现节点热点的问题,包括资源不均匀、CPU使用不均匀等等。首先,配送后台技术团队通过架构升级,将有状态节点变成无状态节点,通过并行计算的能力,让小的业务节点去分担计算压力,以此实现快速扩容。第二是要解决一致性的问题,对于既要写DB也要写缓存的场景,业务写缓存无法保障数据一致性,美团内部主要通过Databus来解决,Databus是一个高可用、低延时、高并发、保证数据一致性的数据库变更实时传输系统。通过Databus上游可以监控业务Binlog变更,通过管道将变更信息传递给ES和其他DB,或者是其他KV系统,利用Databus的高可用特性来保证数据最终是可以同步到其他系统中。第三是我们一直在花精力解决的事情,就是保障集群高可用,主要从三个方面来入手,事前较多的是做全链路压测评,估峰值容量;周期性的集群健康性检查;随机故障演练(服务、机器、组件)。事中做异常报警(性能、业务指标、可用性);快速的故障定位(单机故障、集群故障、IDC故障、组件异常、服务异常);故障前后的系统变更收集。事后重点做系统回滚;扩容、限流、熔断、降级;核武器兜底。单IDC的快速部署&容灾单IDC故障之后,入口服务做到故障识别,自动流量切换;单IDC的快速扩容,数据提前同步,服务提前部署,Ready之后打开入口流量;要求所有做数据同步、流量分发的服务,都具备自动故障检测、故障服务自动摘除;按照IDC为单位扩缩容的能力。多中心尝试美团IDC以分区为单位,存在资源满排,分区无法扩容。美团的方案是多个IDC组成虚拟中心,以中心为分区的单位;服务无差别的部署在中心内;中心容量不够,直接增加新的IDC来扩容容量。单元化尝试相比多中心来说,单元化是进行分区容灾和扩容的更优方案。关于流量路由,美团主要是根据业务特点,采用区域或城市进行路由。数据同步上,异地会出现延迟状况。SET容灾上要保证同本地或异地SET出现问题时,可以快速把SET切换到其他SET上来承担流量。智能物流的核心技术能力和平台沉淀机器学习平台,是一站式线下到线上的模型训练和算法应用平台。之所以构建这个平台,目的是要解决算法应用场景多,重复造轮子的矛盾问题,以及线上、线下数据质量不一致。如果流程不明确不连贯,会出现迭代效率低,特征、模型的应用上线部署出现数据质量等障碍问题。JARVIS是一个以稳定性保障为目标的智能化业务运维AIOps平台。主要用于处理系统故障时报警源很多,会有大量的重复报警,有效信息很容易被淹没等各种问题。此外,过往小规模分布式集群的运维故障主要靠人和经验来分析和定位,效率低下,处理速度慢,每次故障处理得到的预期不稳定,在有效性和及时性方面无法保证。所以需要AIOps平台来解决这些问题。未来的挑战经过复盘和Review之后,我们发现未来的挑战很大,微服务不再“微”了,业务复杂度提升之后,服务就会变得膨胀。其次,网状结构的服务集群,任何轻微的延迟,都可能导致的网络放大效应。另外复杂的服务拓扑,如何做到故障的快速定位和处理,这也是AIOps需要重点解决的难题。最后,就是单元化之后,从集群为单位的运维到以单元为单位的运维,业给美团业务部署能力带来很大的挑战。作者简介宋斌,美团资深技术专家,长期参与分布式系统架构、高并发系统稳定性保障相关工作。目前担任即时物流团队后台技术负责人。2013年加入美团,参与过美团外卖C端、即时物流体系从零搭建。现在带领团队负责调度、清结算、LBS、定价等业务系统、算法数据平台、稳定性保障平台等技术平台的研发和运维。最近重点关注AIOps方向,探索在高并发、分布式系统架构下,如何更好的做好系统稳定性保障。招聘信息美团配送技术团队诚招LBS领域、调度履约平台、结算平台、AIOps方向、机器学习平台、算法工程方向的资深技术专家和架构师。共建全行业最大的单一即时配送网络和平台,共同面对复杂业务和高并发流量的挑战,迎接配送业务全面智能化的时代。欢迎感兴趣的同学投送简历至 songbin@meituan.com,chencheng13@meituan.com。

November 23, 2018 · 1 min · jiezi

新一代数据库TiDB在美团的实践

背景和现状近几年,基于MySQL构建的传统关系型数据库服务,已经很难支撑美团业务的爆发式增长,这就促使我们去探索更合理的数据存储方案和实践新的运维方式。而随着分布式数据库大放异彩,美团DBA团队联合基础架构存储团队,于 2018 年初启动了分布式数据库项目。图 1 美团点评产品展示图在立项之初,我们进行了大量解决方案的对比,深入了解了业界的 scale-out(横向扩展)、scale-up(纵向扩展)等解决方案。但考虑到技术架构的前瞻性、发展潜力、社区活跃度以及服务本身与 MySQL 的兼容性,我们最终敲定了基于 TiDB 数据库进行二次开发的整体方案,并与 PingCAP 官方和开源社区进行深入合作的开发模式。美团业务线众多,我们根据业务特点及重要程度逐步推进上线,到截稿为止,已经上线了 10 个集群,近 200 个物理节点,大部分是 OLTP 类型的应用,除了上线初期遇到了一些小问题,目前均已稳定运行。初期上线的集群,已经分别服务于配送、出行、闪付、酒旅等业务。虽然 TiDB 的架构分层相对比较清晰,服务也是比较平稳和流畅,但在美团当前的数据量规模和已有稳定的存储体系的基础上,推广新的存储服务体系,需要对周边工具和系统进行一系列改造和适配,从初期探索到整合落地,仍然还需要走很远的路。下面将从以下几个方面分别进行介绍:从 0 到 1 的突破,重点考虑做哪些事情。如何规划实施不同业务场景的接入和已有业务的迁移。上线后遇到的一些典型问题介绍。后续规划和对未来的展望。
2. 前期调研测试2.1 对 TiDB 的定位我们对于 TiDB 的定位,前期在于重点解决 MySQL 的单机性能和容量无法线性和灵活扩展的问题,与 MySQL 形成互补。业界分布式方案很多,我们为何选择了 TiDB 呢?考虑到公司业务规模的快速增长,以及公司内关系数据库以 MySQL 为主的现状,因此我们在调研阶段,对以下技术特性进行了重点考虑:协议兼容 MySQL:这个是必要项。可在线扩展:数据通常要有分片,分片要支持分裂和自动迁移,并且迁移过程要尽量对业务无感知。强一致的分布式事务:事务可以跨分片、跨节点执行,并且强一致。支持二级索引:为兼容 MySQL 的业务,这个是必须的。性能:MySQL 的业务特性,高并发的 OLTP 性能必须满足。跨机房服务:需要保证任何一个机房宕机,服务能自动切换。跨机房双写:支持跨机房双写是数据库领域一大难题,是我们对分布式数据库的一个重要期待,也是美团下一阶段重要的需求。业界的一些传统方案虽然支持分片,但无法自动分裂、迁移,不支持分布式事务,还有一些在传统 MySQL 上开发一致性协议的方案,但它无法实现线性扩展,最终我们选择了与我们的需求最为接近的 TiDB。与 MySQL 语法和特性高度兼容,具有灵活的在线扩容缩容特性,支持 ACID 的强一致性事务,可以跨机房部署实现跨机房容灾,支持多节点写入,对业务又能像单机 MySQL 一样使用。2.2 测试针对官方声称的以上优点,我们进行了大量的研究、测试和验证。首先,我们需要知道扩容、Region 分裂转移的细节、Schema 到 KV 的映射、分布式事务的实现原理。而 TiDB 的方案,参考了较多的 Google 论文,我们进行了阅读,这有助于我们理解 TiDB 的存储结构、事务算法、安全性等,包括:Spanner: Google’s Globally-Distributed DatabaseLarge-scale Incremental Processing Using Distributed Transactions and NotificationsIn Search of an Understandable Consensus AlgorithmOnline, Asynchronous Schema Change in F1我们也进行了常规的性能和功能测试,用来与 MySQL 的指标进行对比,其中一个比较特别的测试,是证明 3 副本跨机房部署,确实能保证每个机房分布一个副本,从而保证任何一个机房宕机不会导致丢失超过半数副本。我们从以下几个点进行了测试:Raft 扩容时是否支持 Learner 节点,从而保证单机房宕机不会丢失 2/3 的副本。TiKV 上的标签优先级是否可靠,保证当机房的机器不平均时,能否保证每个机房的副本数依然是绝对平均的。实际测试,单机房宕机,TiDB 在高并发下,QPS、响应时间、报错数量,以及最终数据是否有丢失。手动 Balance 一个 Region 到其他机房,是否会自动回来。从测试结果来看,一切都符合我们的预期。3. 存储生态建设美团的产品线丰富,业务体量也比较大,业务对在线存储的服务质量要求也非常高。因此,从早期做好服务体系的规划非常重要。下面从业务接入层、监控报警、服务部署等维度,来分别介绍一下我们所做的工作。3.1 业务接入层当前 MySQL 的业务接入方式主要有两种,DNS 接入和 Zebra 客户端接入。在前期调研阶段,我们选择了 DNS + 负载均衡组件的接入方式,TiDB-Server 节点宕机,15s 可以被负载均衡识别到,简单且有效。业务架构如下图所示:图 2 业务架构图后面,我们会逐渐过渡到当前大量使用的 Zebra 接入方式来访问 TiDB,从而保持与访问 MySQL 的方式一致,一方面减少业务改造的成本,另一方面尽量实现从 MySQL 到 TiDB 的透明迁移。3.2 监控报警美团目前使用 Mt-Falcon 平台负责监控报警,通过在 Mt-Falcon 上配置不同的插件,可以实现对多种组件的自定义监控。另外也会结合 Puppet 识别不同用户的权限、文件的下发。只要我们编写好插件脚本、需要的文件,装机和权限控制就可以完成了。监控架构如下图所示:图 3 监控架构图而 TiDB 有丰富的监控指标,使用流行的 Prometheus + Grafana,一套集群有 700+ 的 Metric。从官方的架构图可以看出,每个组件会推送自己的 Metric 给 PushGateWay,Prometheus 会直接到 PushGateWay 去抓数据。由于我们需要组件收敛,原生的 TiDB 每个集群一套 Prometheus 的方式不利于监控的汇总、分析、配置,而报警已经在 Mt-Falcon 上实现的比较好了,在 AlertManager 上再造一个也没有必要。因此我们需要想办法把监控和报警汇总到 Mt-Falcon 上面,包括如下几种方式:方案一:修改源代码,将 Metric 直接推送到 Falcon,由于 Metric 散落在代码的不同位置,而且 TiDB 代码迭代太快,把精力消耗在不停调整监控埋点上不太合适。方案二:在 PushGateWay 是汇总后的,可以直接抓取,但 PushGateWay 是个单点,不好维护。方案三:通过各个组件(TiDB、PD、TiKV)的本地 API 直接抓取,优点是组件宕机不会影响其他组件,实现也比较简单。我们最终选择了方案三。该方案的难点是需要把 Prometheus 的数据格式转化为 Mt-Falcon 可识别的格式,因为 Prometheus 支持 Counter、Gauge、Histogram、Summary 四种数据类型,而 Mt-Falcon 只支持基本的 Counter 和 Gauge,同时 Mt-Falcon 的计算表达式比较少,因此需要在监控脚本中进行转换和计算。3.3 批量部署TiDB 使用 Ansible 实现自动化部署。迭代快,是 TiDB 的一个特点,有问题能快速进行解决,但也造成 Ansible 工程、TiDB 版本更新过快,我们对 Ansible 的改动,也只会增加新的代码,不会改动已有的代码。因此线上可能同时需要部署、维护多个版本的集群。如果每个集群一个 Ansible 目录,造成空间的浪费。我们采用的维护方式是,在中控机中,每个版本一个 Ansible 目录,每个版本中通过不同 inventory 文件来维护。这里需要跟 PingCAP 提出的是,Ansible 只考虑了单集群部署,大量部署会有些麻烦,像一些依赖的配置文件,都不能根据集群单独配置(咨询官方得知,PingCAP 目前正在基于 Cloud TiDB 打造一站式 HTAP 平台,会提供批量部署、多租户等功能,后续会比较好地解决这个问题)。3.4 自动化运维平台随着线上集群数量的增加,打造运维平台提上了日程,而美团对 TiDB 和 MySQL 的使用方式基本相同,因此 MySQL 平台上具有的大部分组件,TiDB 平台也需要建设。典型的底层组件和方案:SQL 审核模块、DTS、数据备份方案等。自动化运维平台展示如下图所示:图 4 自动化运维平台展示图3.5 上下游异构数据同步TiDB 是在线存储体系中的一环,它同时也需要融入到公司现有的数据流中,因此需要一些工具来做衔接。PingCAP 官方标配了相关的组件。公司目前 MySQL 和 Hive 结合的比较重,而 TiDB 要代替 MySQL 的部分功能,需要解决 2 个问题:MySQL to TiDBMySQL 到 TiDB 的迁移,需要解决数据迁移以及增量的实时同步,也就是 DTS,Mydumper + Loader 解决存量数据的同步,官方提供了 DM 工具可以很好的解决增量同步问题。MySQL 大量使用了自增 ID 作为主键。分库分表 MySQL 合并到 TiDB 时,需要解决自增 ID 冲突的问题。这个通过在 TiDB 端去掉自增 ID 建立自己的唯一主键来解决。新版 DM 也提供分表合并过程主键自动处理的功能。Hive to TiDB & TiDB to HiveHive to TiDB 比较好解决,这体现了 TiDB 和 MySQL 高度兼容的好处,insert 语句可以不用调整,基于 Hive to MySQL 简单改造即可。TiDB to Hive 则需要基于官方 Pump + Drainer 组件,Drainer 可以消费到 Kafka、MySQL、TiDB,我们初步考虑用图 5 中的方案通过使用 Drainer 的 Kafka 输出模式同步到 Hive。图 5 TiDB to Hive 方案图4. 线上使用磨合对于初期上线的业务,我们比较谨慎,基本的原则是:离线业务 -> 非核心业务 -> 核心业务。TiDB 已经发布两年多,且前期经历了大量的测试,我们也深入了解了其它公司的测试和使用情况,可以预期的是 TiDB 上线会比较稳定,但依然遇到了一些小问题。总体来看,在安全性、数据一致性等关键点上没有出现问题。其他一些性能抖动问题,参数调优的问题,也都得到了快速妥善的解决。这里给 PingCAP 的同学点个大大的赞,问题响应速度非常快,与我们美团内部研发的合作也非常融洽。4.1 写入量大、读 QPS 高的离线业务我们上线的最大的一个业务,每天有数百 G 的写入量,在前期,我们也遇到了较多的问题。业务场景:稳定的写入,每个事务操作 100~200 行不等,每秒 6W 的数据写入。每天的写入量超过 500G,以后会逐步提量到每天 3T。每 15 分钟的定时读 Job,5000 QPS(高频量小)。不定时的查询(低频量大)。
之前使用 MySQL 作为存储,但 MySQL 到达了容量和性能瓶颈,而业务的容量未来会 10 倍的增长。初期调研测试了 ClickHouse,满足了容量的需求,测试发现运行低频 SQL 没有问题,但高频 SQL 的大并发查询无法满足需求,只在 ClickHouse 跑全量的低频 SQL 又会 overkill,最终选择使用 TiDB。测试期间模拟写入了一天的真实数据,非常稳定,高频低频两种查询也都满足需求,定向优化后 OLAP 的 SQL 比 MySQL 性能提高四倍。但上线后,陆续发现了一些问题,典型的如下:4.1.1 TiKV 发生 Write StallTiKV 底层有 2 个 RocksDB 作为存储。新写的数据写入 L0 层,当 RocksDB 的 L0 层数量达到一定数量,就会发生减速,更高则发生 Stall,用来自我保护。TiKV 的默认配置:level0-slowdown-writes-trigger = 20level0-stop-writes-trigger = 36
遇到过的,发生 L0 文件过多可能的原因有 2 个:写入量大,Compact 完不成。Snapshot 一直创建不完,导致堆积的副本一下释放,RocksDB-Raft 创建大量的 L0 文件,监控展示如下图所示:图 6 TiKV 发生 Write Stall 监控展示图我们通过以下措施,解决了 Write Stall 的问题:减缓 Raft Log Compact 频率(增大 raft-log-gc-size-limit、raft-log-gc-count-limit)加快 Snapshot 速度(整体性能、包括硬件性能)max-sub-compactions 调整为 3max-background-jobs 调整为 12level 0 的 3 个 Trigger 调整为 16、32、644.1.2 Delete 大量数据,GC 跟不上现在 TiDB 的 GC 对于每个 kv-instance 是单线程的,当业务删除数据的量非常大时,会导致 GC 速度较慢,很可能 GC 的速度跟不上写入。目前可以通过增多 TiKV 个数来解决,长期需要靠 GC 改为多线程执行,官方对此已经实现,即将发布。4.1.3 Insert 响应时间越来越慢业务上线初期,insert 的响应时间 80 线(Duration 80 By Instance)在 20ms 左右,随着运行时间增加,发现响应时间逐步增加到 200ms+。期间排查了多种可能原因,定位在由于 Region 数量快速上涨,Raftstore 里面要做的事情变多了,而它又是单线程工作,每个 Region 定期都要 heartbeat,带来了性能消耗。tikv-raft propose wait duration 指标持续增长。解决问题的办法:临时解决。增加 Heartbeat 的周期,从 1s 改为 2s,效果比较明显,监控展示如下图所示:图 7 insert 响应时间优化前后对比图彻底解决。需要减少 Region 个数,Merge 掉空 Region,官方在 2.1 版本中已经实现了 Region Merge 功能,我们在升级到 2.1 后,得到了彻底解决。另外,等待 Raftstore 改为多线程,能进一步优化。(官方回复相关开发已基本接近尾声,将于 2.1 的下一个版本发布。)4.1.4 Truncate Table 空间无法完全回收DBA Truncate 一张大表后,发现 2 个现象,一是空间回收较慢,二是最终也没有完全回收。由于底层 RocksDB 的机制,很多数据落在 Level 6 上,有可能清不掉。这个需要打开 cdynamic-level-bytes 会优化 Compaction 的策略,提高 Compact 回收空间的速度。由于 Truncate 使用 delete_files_in_range 接口,发给 TiKV 去删 SST 文件,这里只删除不相交的部分,而之前判断是否相交的粒度是 Region,因此导致了大量 SST 无法及时删除掉。考虑 Region 独立 SST 可以解决交叉问题,但是随之带来的是磁盘占用问题和 Split 延时问题。考虑使用 RocksDB 的 DeleteRange 接口,但需要等该接口稳定。目前最新的 2.1 版本优化为直接使用 DeleteFilesInRange 接口删除整个表占用的空间,然后清理少量残留数据,目前已经解决。4.1.5 开启 Region Merge 功能为了解决 region 过多的问题,我们在升级 2.1 版本后,开启了 region merge 功能,但是 TiDB 的响应时间 80 线(Duration 80 By Instance)依然没有恢复到当初,保持在 50ms 左右,排查发现 KV 层返回的响应时间还很快,和最初接近,那么就定位了问题出现在 TiDB 层。研发人员和 PingCAP 定位在产生执行计划时行为和 2.0 版本不一致了,目前已经优化。4.2 在线 OLTP,对响应时间敏感的业务除了分析查询量大的离线业务场景,美团还有很多分库分表的场景,虽然业界有很多分库分表的方案,解决了单机性能、存储瓶颈,但是对于业务还是有些不友好的地方:业务无法友好的执行分布式事务。跨库的查询,需要在中间层上组合,是比较重的方案。单库如果容量不足,需要再次拆分,无论怎样做,都很痛苦。业务需要关注数据分布的规则,即使用了中间层,业务心里还是没底。因此很多分库分表的业务,以及即将无法在单机承载而正在设计分库分表方案的业务,主动找到了我们,这和我们对于 TiDB 的定位是相符的。这些业务的特点是 SQL 语句小而频繁,对一致性要求高,通常部分数据有时间属性。在测试及上线后也遇到了一些问题,不过目前基本都有了解决办法。4.2.1 SQL 执行超时后,JDBC 报错业务偶尔报出 privilege check fail。是由于业务在 JDBC 设置了 QueryTimeout,SQL 运行超过这个时间,会发行一个 kill query 命令,而 TiDB 执行这个命令需要 Super 权限,业务是没有权限的。其实 kill 自己的查询,并不需要额外的权限,目前已经解决了这个问题:https://github.com/pingcap/tidb/pull/7003,不再需要 Super 权限,已在 2.0.5 上线。4.2.2 执行计划偶尔不准TiDB 的物理优化阶段需要依靠统计信息。在 2.0 版本统计信息的收集从手动执行,优化为在达到一定条件时可以自动触发:数据修改比例达到 tidb_auto_analyze_ratio。表一分钟没有变更(目前版本已经去掉这个条件)。但是在没有达到这些条件之前统计信息是不准的,这样就会导致物理优化出现偏差,在测试阶段(2.0 版本)就出现了这样一个案例:业务数据是有时间属性的,业务的查询有 2 个条件,比如:时间+商家 ID,但每天上午统计信息可能不准,当天的数据已经有了,但统计信息认为没有。这时优化器就会建议使用时间列的索引,但实际上商家 ID 列的索引更优化。这个问题可以通过增加 Hint 解决。在 2.1 版本对统计信息和执行计划的计算做了大量的优化,也稳定了基于 Query Feedback 更新统计信息,也用于更新直方图和 Count-Min Sketch,非常期待 2.1 的 GA。5. 总结展望经过前期的测试、各方的沟通协调,以及近半年对 TiDB 的使用,我们看好 TiDB 的发展,也对未来基于 TiDB 的合作充满信心。接下来,我们会加速推进 TiDB 在更多业务系统中的使用,同时也将 TiDB 纳入了美团新一代数据库的战略选型中。当前,我们已经全职投入了 3 位 DBA 同学和多位存储计算专家,从底层的存储,中间层的计算,业务层的接入,再到存储方案的选型和布道,进行全方位和更深入的合作。长期来看,结合美团不断增长的业务规模,我们将与 PingCAP 官方合作打造更强大的生态体系:Titan:Titan 是 TiDB 下一步比较大的动作,也是我们非常期待的下一代存储引擎,它对大 Value 支持会更友好,将解决我们单行大小受限,单机 TiKV 最大支持存储容量的问题,大大提升大规模部署的性价比。Cloud TiDB (Based on Docker & K8s):云计算大势所趋,PingCAP 在这块也布局比较早,今年 8 月份开源了 TiDB Operator,Cloud TiDB 不仅实现了数据库的高度自动化运维,而且基于 Docker 硬件隔离,实现了数据库比较完美的多租户架构。我们和官方同学沟通,目前他们的私有云方案在国内也有重要体量的 POC,这也是美团看重的一个方向。TiDB HTAP Platform:PingCAP 在原有 TiDB Server 计算引擎的基础上,还构建 TiSpark 计算引擎,和他们官方沟通,他们在研发了一个基于列的存储引擎,这样就形成了下层行、列两个存储引擎、上层两个计算引擎的完整混合数据库(HTAP),这个架构不仅大大的节省了核心业务数据在整个公司业务周期里的副本数量,还通过收敛技术栈,节省了大量的人力成本、技术成本、机器成本,同时还解决了困扰多年的 OLAP 的实效性。后面我们也会考虑将一些有实时、准实时的分析查询系统接入 TiDB。图 8 TiDB HTAP Platform 整体架构图后续的物理备份方案,跨机房多写等也是我们接下来逐步推进的场景,总之,我们坚信未来 TiDB 在美团的使用场景会越来越多,发展也会越来越好。目前,TiDB 在业务层面、技术合作层面都已经在美团扬帆起航,美团点评将携手 PingCAP 开启新一代数据库深度实践、探索之旅。后续,还有美团点评架构存储团队针对 TiDB 源码研究和改进的系列文章,敬请期待。作者简介应钢,美团点评研究员,数据库专家。曾就职于百度、新浪、去哪儿网等,10年数据库自动化运维开发、数据库性能优化、大规模数据库集群技术保障和架构优化经验。精通主流的SQL与NoSQL系统,现专注于公司业务在NewSQL领域的创新和落地。李坤,2018年初加入美团,美团点评数据库专家,多年基于MySQL、Hbase、Oracle的架构设计和维护、自动化开发经验,目前主要负责分布式数据库Blade的推动和落地,以及平台和周边组件的建设昌俊,美团点评数据库专家,曾就职于BOCO、去哪儿网,6年MySQL DBA从业经历,积累了丰富的数据库架构设计和性能优化、自动化开发经验。目前专注于TiDB在美团点评业务场景的改造和落地。

November 23, 2018 · 4 min · jiezi

美团餐饮娱乐知识图谱——美团大脑揭秘

前言“ I’m sorry. I can’t do that, Dave.” 这是经典科幻电影《2001: A Space Odyssey》里HAL 9000机器人说的一句话,浓缩了人类对终极人工智能的憧憬。让机器学会说这样简单一句话,需要机器具备情感认知、自我认识以及对世界的认识,来辅助机器处理接收到的各种信息,了解信息背后的意思,从而生成自己的决策。而这些认知模块的基础,都需要机器具备知识学习组织推理的能力,知识图谱就是为实现这些目标而生。今年5月,美团NLP中心开始构建大规模的餐饮娱乐知识图谱——美团大脑,它将充分挖掘关联各个场景数据,用AI技术让机器“阅读”用户评论数据,理解用户在菜品、价格、服务、环境等方面的喜好,挖掘人、店、商品、标签之间的知识关联,从而构建出一个“知识大脑”。美团大脑已经在公司多个业务中初步落地,例如智能搜索推荐、智能金融、智能商户运营等。此前,《美团大脑:知识图谱的建模方法及其应用》一文,介绍了知识图谱的分类及其具体应用,尤其是常识性知识图谱及百科全书式知识图谱分别是如何使用的。之后我们收到非常多的反馈,希望能进一步了解“美团大脑”的细节。为了让大家更系统地了解美团大脑,NLP中心会在接下来一段时间,陆续分享一系列技术文章,包括知识图谱相关的技术,美团大脑背后的算法能力,千亿级别图引擎建设以及不同应用场景的业务效果等等,本文是美团大脑系列的第一篇文章。迈向认知智能海量数据和大规模分布式计算力,催生了以深度学习为代表的第三次(1993-目前)人工智能高潮。Web 2.0产生的海量数据给机器学习和深度学习技术提供了大量标注数据,而GPU和云计算的发展为深度学习的复杂数值计算提供了必要算力条件。深度学习技术在语音、图像领域均取得了突破性的进展,这表示学习技术成果使得机器首次在感知能力上达到甚至超越了人类的水平,人工智能已经进入感知智能阶段。然而,随着深度学习被广泛应用,其局限性也愈发明显。缺乏可解释性:神经网络端到端学习的“黑箱”特性使得很多模型不具有可解释性,导致很多需要人去参与决策,在这些应用场景中机器结果无法完全置信而需要谨慎的使用,比如医学的疾病诊断、金融的智能投顾等等。这些场景属于低容错高风险场景,必须需要显示的证据去支持模型结果,从而辅助人去做决策。常识(Common Sense)缺失:人的日常活动需要大量的常识背景知识支持,数据驱动的机器学习和深度学习,它们学习到的是样本空间的特征、表征,而大量的背景常识是隐式且模糊的,很难在样本数据中进行体现。比如下雨要打伞,但打伞不一定都是下雨天。这些特征数据背后的关联逻辑隐藏在我们的文化背景中。缺乏语义理解。模型并不理解数据中的语义知识,缺乏推理和抽象能力,对于未见数据模型泛化能力差。依赖大量样本数据:机器学习和深度学习需要大量标注样本数据去训练模型,而数据标注的成本很高,很多场景缺乏标注数据来进行冷启动。图1 数据知识驱动AI能力对比从人工智能整体发展来说,综上的局限性也是机器从感知智能向认知智能的迁跃的过程中必须解决的问题。认知智能需要机器具备推理和抽象能力,需要模型能够利用先验知识,总结出人可理解、模型可复用的知识。机器计算能力整体上需要从数据计算转向知识计算,知识图谱就显得必不可少。知识图谱可以组织现实世界中的知识,描述客观概念、实体、关系。这种基于符号语义的计算模型,一方面可以促成人和机器的有效沟通,另一方面可以为深度学习模型提供先验知识,将机器学习结果转化为可复用的符号知识累积起来。知识究竟是什么呢?知识就是有结构的信息。人从数据中提取有效信息,从信息中提炼有用知识,信息组织成了结构就有了知识。知识工程,作为代表人工智能发展的主要研究领域之一,就是机器仿照人处理信息积累知识运用知识的过程。而知识图谱就是知识工程这一领域数十年来的代表性研究方向。在数据还是稀有资源的早期,知识图谱的研究重点偏向语义模型和逻辑推理,知识建模多是自顶向下的设计模式,语义模型非常复杂。其中典型工作,是在1956年人工智能学科奠基之会——达特茅斯会议上公布的“逻辑理论家”(Logic Theorist)定理证明程序,该程序可以证明《数学原理》中的部分定理。伴随着Web带来前所未有的数据之后,知识图谱技术的重心从严谨语义模型转向海量事实实例构建,图谱中知识被组织成<主,谓,宾>三元组的形式,来表征客观世界中的实体和实体之间的关系。比如像名人的维基百科词条页面中,Infobox卡片都会描述该名人的国籍信息,其结构就是<人,国籍,国家>这样的三元组。图2 互联网公司知识图谱布局目前,知识图谱已被广泛应用在问答、搜索、推荐等系统,已涉及金融、医疗、电商等商业领域,图谱技术成为“兵家必争”之地。微软于2010年开始构建Satori知识图谱来增强Bing搜索;Google在2012年提出 Knowledge Graph概念,用图谱来增强自己的搜索引擎;2013年Facebook发布Open Graph应用于社交网络智能搜索;2015年阿里巴巴开始构建自己的电商领域知识图谱;2016年Amazon也开始构建知识图谱。图3 美团大脑2018年5月,美团点评NLP中心开始构建大规模的餐饮娱乐知识图谱——美团大脑。美团点评作为中国最大的在线本地生活服务平台,覆盖了餐饮娱乐领域的众多生活场景,连接了数亿用户和数千万商户,积累了宝贵的业务数据,蕴含着丰富的日常生活相关知识。在建的美团大脑知识图谱目前有数十类概念,数十亿实体和数百亿三元组,美团大脑的知识关联数量预计在未来一年内将上涨到数千亿的规模。美团大脑将充分挖掘关联各个场景数据,用AI技术让机器“阅读”用户评论和行为数据,理解用户在菜品、价格、服务、环境等方面的喜好,构建人、店、商品、场景之间的知识关联,从而形成一个“知识大脑”。相比于深度学习的“黑盒子”,知识图谱具有很强的可解释性,在美团跨场景的多个业务中应用性非常强,目前已经在搜索、金融等场景中初步验证了知识图谱的有效性。近年来,深度学习和知识图谱技术都有很大的发展,并且存在一种互相融合的趋势,在美团大脑知识构建过程中,我们也会使用深度学习技术,把数据背后的知识挖掘出来,从而赋能业务,实现智能化的本地生活服务,帮助每个人“Eat Better, Live Better”。知识图谱技术链图4 知识图谱技术链知识图谱的源数据来自多个维度。通常来说,结构化数据处理简单、准确率高,其自有的数据结构设计,对数据模型的构建也有一定指导意义,是初期构建图谱的首要选择。世界知名的高质量的大规模开放知识库如Wikidata、DBPedia、Yago是构建通用领域多语言知识图谱的首选,国内有OpenKG提供了诸多中文知识库的Dump文件或API。工业界往往基于自有的海量结构化数据,进行图谱的设计与构建,并同时利用实体识别、关系抽取等方式处理非结构化数据,增加更多丰富的信息。知识图谱通常以实体为节点形成一个大的网络,图谱的Schema相当于数据模型,描述了领域下包含的类型(Type),与类型下描述实体的属性(Property),Property中实体与实体之间的关系为边(Relation),实体自带信息为属性(Attribute)。除此之外Schema也会描述它们的约束关系。美团大脑围绕用户打造吃喝玩乐全方面的知识图谱,从实际业务需求出发,在现有数据表之上抽象出数据模型,以商户、商品、用户等为主要实体,其基本信息作为属性,商户与商品、与用户的关联为边,将多领域的信息关联起来,同时利用评论数据、互联网数据等,结合知识获取方法,填充图谱信息,从而提供更加多元化的知识。知识获取知识获取是指从不同来源、不同结构数据中,抽取相关实体、属性、关系、事件等知识。从数据结构划分可以分为结构化数据、半结构化数据和纯文本数据。结构化数据指的关系型数据库表示和存储的的二维形式数据,这类数据可以直接通过Schema融合、实体对齐等技术将数据提取到知识图谱中。半结构化数据主要指有相关标记用来分隔语义元素,但又不存在数据库形式的强定义数据,如网页中的表格数据、维基百科中的Infobox等等。这类数据通过爬虫、网页解析等技术可以将其转换为结构化数据。现实中结构化、半结构化数据都比较有限,大量的知识往往存在于文本中,这也和人获取知识的方式一致。对应纯文本数据获取知识,主要包括实体识别、实体分类、关系抽取、实体链接等技术。实体作为知识图谱的核心单位,从文本中抽取实体是知识获取的一个关键技术。文本中识别实体,一般可以作为一个序列标注问题来进行解决。传统的实体识别方法以统计模型如HMM、CRF等为主导,随着深度学习的兴起,BiLSTM+CRF[1]模型备受青睐,该模型避免了传统CRF的特征模版构建工作,同时双向LSTM能更好地利用前后的语义信息,能够明显提高识别效果。在美团点评-美食图谱子领域的建设中,每个店家下的推荐菜(简称店菜)是图谱中的重要实体之一,评论中用户对店菜的评价,能很好地反映用户偏好与店菜的实际特征,利用知识获取方法,从评论中提取出店菜实体、用户对店菜的评价内容与评价情感,对补充实体信息、分析用户偏好、指导店家进行改善有着非常重要的意义。图5 BiLSTM+CRF模型实体分类则是对抽取出的实体进行归类。当从文本中发现一个新的实体,给实体相应的Type是实体概念化的基本目标。比如用该实体的上下文特征与其他Type下的实体特征进行对比,将新实体归入最相似的Type中。此外,在Schema不完善的情况下,对大量实体进行聚类,进而抽象出每个簇对应的Type,是自底向上构建图谱的一个常用方法,在补充Type层的同时,也顺便完成了实体归类。关系抽取,是从文本中自动抽取实体与实体之间的特定的语义关系,以补充图谱中缺失的关系,例如,从“干酪鱼原来是奶酪做的”中抽取出<干酪鱼,食材,奶酪>。关系抽取可以通过定义规则模版来获取,如匹配某种表达句式、利用文法语义特征等,但规则类方法消耗大量人力,杂质较多。基于Bootstrap Learning的方法利用少量种子实例或模版抽取新的关系,再利用新的结果生成更多模版,如此迭代,KnowItAll[2]、TextRunner[3]基于这类思想;远程监督(Distant Supervision)方法[4]把现有的三元组信息作为种子,在文本中匹配同时含有主语和宾语的信息,作为关系的标注数据。这两种方法解决了人力耗费问题,但准确率还有待提高。近期的深度学习方法则基于联合模型思想,利用神经网络的端对端模型,同时实现实体识别和关系抽取5,从而避免前期实体识别的结果对关系抽取造成的误差累积影响。知识校验知识校验贯穿整个知识图谱的构建过程。在初期的Schema设计过程中,需要严格定义Type下的Property,Property关联的是属性信息还是实体,以及实体所属的Type等等。Schema若不够规范,会导致错误传达到数据层且不易纠错。在数据层,通过源数据获取或者通过算法抽取的知识或多或少都包含着杂质,可以在Schema层面上,添加人工校验方法与验证约束规则,保证导入数据的规范性,比如对于<店A,包含,店菜B>关系,严格要求主语A的Type是POI,宾语B的Type是Dish。而对于实体间关系的准确性,如上下位关系是否正确、实例的类型是否正确,实例之间的关系是否准确等,可以利用实体的信息与图谱中的结构化信息计算一个关系的置信度,或看作关系对错与否的二分类问题,比如<店A, 适合, 情侣约会>,对于“情侣约会”标签,利用店A的信息去计算一个权重会使得数据更有说服力。此外,如果涉及到其他来源的数据,在数据融合的同时进行交叉验证,保留验证通过的知识。当图谱数据初步成型,在知识应用过程中,通过模型结果倒推出的错误,也有助于净化图谱中的杂质,比如知识推理时出现的矛盾,必然存在知识有误的情况。知识融合知识融合主要解决多源异构数据整合问题,即从不同来源、不同结构但表达统一实体或概念的数据融合为一个实体或概念。融入来自多源数据的知识,必然会涉及知识融合工作,实体融合主要涉及Schema融合、实体对齐、实体链接等技术。Schema是知识图谱的模型,其融合等价于Type层的合并和Property的合并。在特定领域的图谱中,Type与Property数量有限,可以通过人工进行合并。对于实例的对齐,可以看作一个寻找Top匹配的实例的排序问题,或者是否匹配的二分类问题,其特征可以基于实体属性信息、Schema结构化信息、语义信息等来获取。实体对齐是多源数据融合中的重要过程。当数据来自于不同的知识库体系,需要分辨其描述的是同一个实体,将相关信息融合,最终生成该知识库中唯一的实体。这通常是一个求最相似问题或判断两个实体是否是同一个的二分类问题,实体名称、实体携带属性以及其结构化信息,都可以作为有用特征。同时,通过Type或规则限制,缩小匹配的实体范围。一旦图谱构建完成,如何从文本中准确匹配上图谱中相应的实体,进而延伸出相关的背景知识,则是一个实体链接问题。实体链接[7] 主要依赖于实体Entity与所有Mention(文本文档中实体的目标文本)的一个多对多的映射关系表, 如 “小龙虾”这个Mention在图谱中实际对应的实体Entity可能是“麻辣小龙虾”的菜,也可能是“十三香小龙虾”的菜。对于从文本中识别出的Mention,利用上下文等信息,对其候选Entity进行排序,找出最可能的Entity。实体链接可以正确地定位用户所提实体,理解用户真实的表达意图,从而进一步挖掘用户行为,了解用户偏好。图6 实体链接(Entity Linking)美团大脑也参考并融入了多源的数据信息,知识融合是构建图谱的一个重要步骤。以美食领域子图谱为例,该图谱是由结构化数据和文本挖掘出来的知识融合而成,首要任务是将图谱中已构建的菜品通过菜名、口味、食材等方面的相似度将菜品与文本挖掘出来的菜品知识进行关联,其次还要对无法关联的菜品知识聚类抽象成一个菜品实体。知识的融合很大程度上增加了菜品的数量,丰富了菜品信息,同时为实体链接的映射关系表提供了候选对,有助于我们在搜索过程中,支持更多维度(如口味、食材)的查询。知识表示知识表示是对知识数据的一种描述和约定,目的是让计算机可以像人一样去理解知识,从而可以让计算机进一步的推理、计算。大多数知识图谱是以符号化的方法表示,其中RDF是最常用的符号语义表示模型,其一条边对于一个三元组<主语Subject,谓语Predicate,宾语Object>,表达一个客观事实,该方法直观易懂,具备可解释性,支持推理。而随着深度学习的发展,基于向量表示的Embedding算法逐渐兴起,其为每个实体与关系训练一个可表征的向量,该方法易于进行算法学习,可表征隐形知识并进一步发掘隐形知识。常用的Embedding模型有Word2Vec与Trans系列8,将会在之后的系列文章里进一步讲解。美团大脑参考Freebase的建模思想,以< Subject,Predicate,Object>的三元组形式将海量知识存储在分布式数据仓库中,并以CVT(Compound Value Type)设计承载多元数据,即抽象一个CVT的实例来携带多元信息,图为一个知识表示的例子。与此同时,美团大脑基于上亿节点计算Graph Embedding的表征,并将结果应用到搜索领域中。图7 美团大脑知识表示知识推理基于知识图谱的推理工作,旨在依据现有的知识信息推导出新知识,包括实体关系、属性等,或者识别出错误关系。可以分为基于符号的推理与基于统计的推理,前者一般根据经典逻辑创建新的实体关系的规则,或者判断现有关系的矛盾之处,后者则是通过统计规律从图谱中学到新的实体关系。利用实体之间的关系可以推导出一些场景,辅助进行决策判断。美团大脑金融子图谱利用用户行为、用户关系、地理位置去挖掘金融领域诈骗团伙。团伙通常会存在较多关联及相似特性,图谱中的关系可以帮助人工识别出多层、多维度关联的欺诈团伙,再利用规则等方式,识别出批量具有相似行为的客户,辅助人工优化调查,同时可以优化策略。图8 知识推理在金融场景应用知识赋能知识图谱含有丰富的语义信息,对文本有基于语义的更为深入的理解,在推荐、搜索、问答等领域能提供更加直接与精确的查询结果,使得服务更加智能化。个性化推荐通过实体与实体之间的关系,利用用户感兴趣的实体,进一步扩展用户偏好的相似的实体,提供可解释性的推荐内容。一方面,图谱提供了实体在多个维度的特征信息,另一方面,表示学习向量带有一定的语义信息,使得寻找推荐实体更接近目标实体或更偏向用户喜好。语义搜索,是指搜索引擎对Query的处理不再拘泥于字面本身,而是抽象出其中的实体、查询意图,通过知识图谱直接提供用户需要的答案,而不只是提供网页排序结果,更精准的满足用户的需求。当前Google、百度、神马搜索都已经将基于知识图谱的语义搜索融入到搜索引擎中,对于一些知识性内容的查找,能智能地直接显示结果信息。美团大脑的业务应用依托深度学习模型,美团大脑充分挖掘、关联美团点评各个业务场景公开数据(如用户评价、菜品、标签等),正在构建餐饮娱乐“知识大脑”,并且已经开始在美团不同业务中进行落地,利用人工智能技术全面提升用户的生活体验。智能搜索:帮助用户做决策知识图谱可以从多维度精准地刻画商家,已经在美食搜索和旅游搜索中应用,为用户搜索出更适合Ta的店。基于知识图谱的搜索结果,不仅具有精准性,还具有多样性,例如:当用户在美食类目下搜索关键词“鱼”,通过图谱可以认知到用户的搜索词是“鱼”这种“食材”。因此搜索的结果不仅有“糖醋鱼”、“清蒸鱼”这样的精准结果,还有“赛螃蟹”这样以鱼肉作为主食材的菜品,大大增加了搜索结果的多样性,提升用户的搜索体验。并且对于每一个推荐的商家,能够基于知识图谱找到用户最关心的因素,从而生成“千人千面”的推荐理由,例如在浏览到大董烤鸭店的时候,偏好“无肉不欢”的用户A看到的推荐理由是“大董的烤鸭名不虚传”,而偏好“环境优雅”的用户B,看到的推荐理由就是“环境小资,有舞台表演”,不仅让搜索结果更具有解释性,同时也能吸引不同偏好的用户进入商家。图9 知识图谱在点评搜索中应用对于场景化搜索,知识图谱也具有很强的优势,以七夕节为例,通过知识图谱中的七夕特色化标签,如约会圣地、环境私密、菜品新颖、音乐餐厅、别墅餐厅等等,结合商家评论中的细粒度情感分析,为美团搜索提供了更多适合情侣过七夕节的商户数据,用于七夕场景化搜索的结果召回与展示,极大的提升了用户体验和用户点击转化。在NLP中心以及大众点评搜索智能中心两个团队的紧密合作下,依赖知识图谱技术和深度学习技术对搜索架构进行了整体的升级。经过5个月时间,点评搜索核心指标在高位基础上,仍然有非常明显的提升。ToB商户赋能:商业大脑指导店老板决策美团大脑正在应用于SaaS收银系统专业版,通过机器智能阅读每个商家的每一条评论,可以充分理解每个用户对于商家的感受,针对每个商家将大量的用户评价进行归纳总结,从而可以发现商家在市场上的竞争优势/劣势、用户对于商家的总体印象趋势、商家的菜品的受欢迎程度变化。进一步,通过细粒度用户评论全方位分析,可以细致刻画商家服务现状,以及对商家提供前瞻性经营方向。这些智能经营建议将通过美团SaaS收银系统专业版定期触达到各个商家,智能化指导商家精准优化经营模式。传统给店老板提供商业分析服务中主要聚焦于单店的现金流、客源分析。美团大脑充分挖掘了商户及顾客之间的关联关系,可以提供围绕商户到顾客,商户到所在商圈的更多维度商业分析,在商户营业前、营业中以及将来经营方向,均可以提供细粒度运营指导。在商家服务能力分析上,通过图谱中关于商家评论所挖掘的主观、客观标签,例如“服务热情”、“上菜快”、“停车免费”等等,同时结合用户在这些标签所在维度上的Aspect细粒度情感分析,告诉商家在哪些方面做的不错,是目前的竞争优势;在哪些方面做的还不够,需要尽快改进。因而可以更准确地指导商家进行经营活动。更加智能的是,美团大脑还可以推理出顾客对商家的认可程度,是高于还是低于其所在商圈的平均情感值,让店老板一目了然地了解自己的实际竞争力。在消费用户群体分析上,美团大脑不仅能够告诉店老板来消费的顾客的年龄层、性别分布,还可以推理出顾客的消费水平,对于就餐环境的偏好,适合他们的推荐菜,让店老板有针对性的调整价格、更新菜品、优化就餐环境。金融风险管理和反欺诈:从用户行为建立征信体系知识图谱的推理能力和可解释性,在金融场景中具有天然的优势,NLP中心和美团金融共建的金融好用户扩散以及用户反欺诈,就是利用知识图谱中的社区发现、标签传播等方法来对用户进行风险管理,能够更准确的识别逾期客户以及用户的不良行为,从而大大提升信用风险管理能力。在反欺诈场景中,知识图谱已经帮助金融团队在案件调查中发现并确认多个欺诈案件。由于团伙通常会存在较多关联及相似特性,关系图可以帮助识别出多层、多维度关联的欺诈团伙,能通过用户和用户、用户和设备、设备和设备之间四度、五度甚至更深度的关联关系,发现共用设备、共用Wi-Fi来识别欺诈团伙,还可在已有的反欺诈规则上进行推理预测可疑设备、可疑用户来进行预警,从而成为案件调查的有力助手。未来的挑战知识图谱建设过程是美团第一次摸索基于图的构建/挖掘/存储/应用过程,也遇到了很多挑战,主要的挑战和应对思路如下: (1)数据生成与导入难点:Schema构建和更新;数据源多,数据不一致问题;数据质检。应对思路:通过针对不同的数据进行特定清洗,元数据约束校验、业务逻辑正确性校验等,设置了严格的数据接入和更新规范。(2)知识挖掘难点:知识的融合、表征、推理和验证。应对思路:通过借鉴文本中的词向量表征,为知识建立统一的语义空间表征,使得语义可计算,基于深度学习和知识表示的算法进行推理。(3)百亿图存储及查询引擎难点:数据的存储、查询和同步,数据量极大,没有成熟开源引擎直接使用。应对思路:构建分层增量系统,实时增量、离线增量、全量图三层Merge查询,减少图更新影响范围。同时建设完整的容灾容错、灰度、子图回滚机制。基于LBS等业务特点合理切分子图View,构建分布式图查询索引层。(4)知识图谱应用挑战难点:算法设计,系统实现难和实时应用。应对思路:知识图谱的应用算法则需要有效融合数据驱动和知识引导,才能提升算法效果和提供更好的解释性,属于研究前沿领域。百亿甚至千亿关系规模下,需要设计和实现分布式的图应用算法,这对算法和系统都有重大的挑战。总而言之,为打造越来越强大的美团大脑,NLP中心一方面利用业界前沿的算法模型来挖掘关联以及应用知识,另一方面,也在逐步建立国内领先的商业化分布式图引擎系统,支撑千亿级别知识图谱的实时图查询、图推理和图计算。在未来的系列文章中,NLP中心将一一揭秘这背后的创新性技术,敬请期待。参考文献[1] Huang, Zhiheng, Wei Xu, and Kai Yu. “Bidirectional LSTM-CRF models for sequence tagging.” arXiv preprint arXiv:1508.01991 (2015).[2] Etzioni, Oren, et al. “Unsupervised named-entity extraction from the web: An experimental study.” Artificial intelligence165.1 (2005): 91-134.[3] Banko, Michele, et al. “Open information extraction from the web.” IJCAI. Vol. 7. 2007.[4] Mintz, Mike, et al. “Distant supervision for relation extraction without labeled data.” Proceedings of the Joint Conference of the 47th Annual Meeting of the ACL and the 4th International Joint Conference on Natural Language Processing of the AFNLP: Volume 2-Volume 2. Association for Computational Linguistics, 2009.[5] Zheng, Suncong, et al. “Joint entity and relation extraction based on a hybrid neural network.” Neurocomputing 257 (2017): 59-66.[6] Zheng, Suncong, et al. “Joint extraction of entities and relations based on a novel tagging scheme.” arXiv preprint arXiv:1706.05075 (2017).[7] Shen, Wei, Jianyong Wang, and Jiawei Han. “Entity linking with a knowledge base: Issues, techniques, and solutions.” IEEE Transactions on Knowledge and Data Engineering 27.2 (2015): 443-460.[8] Bordes, Antoine, et al. “Translating embeddings for modeling multi-relational data.” Advances in neural information processing systems. 2013.[9] Wang, Zhen, et al. “Knowledge Graph Embedding by Translating on Hyperplanes.” AAAI. Vol. 14. 2014.作者简介仲远,博士,美团AI平台部NLP中心负责人,点评搜索智能中心负责人。在国际顶级学术会议发表论文30余篇,获得ICDE 2015最佳论文奖,并是ACL 2016 Tutorial “Understanding Short Texts”主讲人,出版学术专著3部,获得美国专利5项。此前,博士曾担任微软亚洲研究院主管研究员,以及美国Facebook公司Research Scientist。曾负责微软研究院知识图谱、对话机器人项目和Facebook产品级NLP Service。富峥,博士,美团AI平台NLP中心研究员,目前主要负责美团大脑项目。在此之前,博士在微软亚洲研究院社会计算组担任研究员,并在相关领域的顶级会议和期刊上发表30余篇论文,曾获ICDM2013最佳论文大奖,出版学术专著1部。 张富峥博士曾担任ASONAM的工业界主席,IJCAI、WSDM、SIGIR等国际会议和TKDE、TOIS、TIST等国际期刊的评审委员。王珺,博士,美团AI平台NLP中心产品和数据负责人。在此之前,王珺在阿里云负责智能顾问多产品线,推动建立了阿里云智能服务体系。明洋,硕士,美团AI平台NLP中心知识图谱算法工程师。2016年毕业于清华大学计算机系知识工程实验室。思睿,硕士,美团AI平台NLP中心知识图谱算法专家。此前在百度AIG知识图谱部负责知识图谱、NLP相关算法研究,参与了百度知识图谱整个构建及落地过程。一飞,负责AI平台NLP中心知识图谱产品。目前主要负责美团大脑以及知识图谱落地项目。梦迪,美团AI平台NLP中心知识图谱算法工程师,此前在金融科技公司文因互联任高级工程师及开放数据负责人,前清华大学知识工程实验室研究助理,中文开放知识图谱联盟OpenKG联合发起人。招聘信息美团点评 NLP 团队招聘各类算法人才,Base 北京上海均可。NLP 中心使命是打造世界一流的自然语言处理核心技术和服务能力,依托 NLP(自然语言处理)、Deep Learning(深度学习)、Knowledge Graph(知识图谱)等技术,处理美团点评海量文本数据,打通餐饮、旅行、休闲娱乐等各个场景数据,构建美团点评知识图谱,搭建通用 NLP Service,为美团点评各项业务提供智能的文本语义理解服务。我们的团队既注重AI技术的落地,也开展中长期的NLP及知识图谱基础研究。目前项目及业务包括美团点评知识图谱、智能客服、语音语义搜索、文章评论语义理解、美团点评智能助理等。真正助力于“帮大家吃得更好,生活更好”企业使命的实现,优化用户的生活体验,改善和提升消费者的生活品质。欢迎各位朋友推荐或自荐至 hr.ai@meituan.com。算法岗:NLP算法工程师/专家/研究员 、知识图谱算法工程师/专家/研究员工程岗:C++/Java研发专家/工程师 、AI平台研发工程师/专家产品岗:AI产品经理/专家(NLP、数据方向) ...

November 23, 2018 · 2 min · jiezi

前端黑科技:美团网页首帧优化实践

前言自JavaScript诞生以来,前端技术发展非常迅速。移动端白屏优化是前端界面体验的一个重要优化方向,Web 前端诞生了 SSR 、CSR、预渲染等技术。在美团支付的前端技术体系里,通过预渲染提升网页首帧优化,从而优化了白屏问题,提升用户体验,并形成了最佳实践。在前端渲染领域,主要有以下几种方式可供选择: CSR预渲染SSR同构优点不依赖数据FP 时间最快客户端用户体验好内存数据共享不依赖数据FCP 时间比 CSR 快客户端用户体验好内存数据共享SEO 友好首屏性能高,FMP 比 CSR 和预渲染快SEO 友好首屏性能高,FMP 比 CSR 和预渲染快客户端用户体验好内存数据共享客户端与服务端代码公用,开发效率高缺点SEO 不友好FCP 、FMP 慢SEO 不友好FMP 慢客户端数据共享成本高模板维护成本高Node 容易形成性能瓶颈通过对比,同构方案集合 CSR 与 SSR 的优点,可以适用于大部分业务场景。但由于在同构的系统架构中,连接前后端的 Node 中间层处于核心链路,系统可用性的瓶颈就依赖于 Node ,一旦作为短板的 Node 挂了,整个服务都不可用。结合到我们团队负责的支付业务场景里,由于支付业务追求极致的系统稳定性,服务不可用直接影响到客诉和资损,因此我们采用浏览器端渲染的架构。在保证系统稳定性的前提下,还需要保障用户体验,所以采用了预渲染的方式。那么究竟什么是预渲染呢?什么是 FCP/FMP 呢?我们先从最常见的 CSR 开始说起。以 Vue 举例,常见的 CSR 形式如下:一切看似很美好。然而,作为以用户体验为首要目标的我们发现了一个体验问题:首屏白屏问题。为什么会首屏白屏浏览器渲染包含 HTML 解析、DOM 树构建、CSSOM 构建、JavaScript 解析、布局、绘制等等,大致如下图所示:要搞清楚为什么会有白屏,就需要利用这个理论基础来对实际项目进行具体分析。通过 DevTools 进行分析:等待 HTML 文档返回,此时处于白屏状态。对 HTML 文档解析完成后进行首屏渲染,因为项目中对 id=“app加了灰色的背景色,因此呈现出灰屏。进行文件加载、JS 解析等过程,导致界面长时间出于灰屏中。当 Vue 实例触发了 mounted 后,界面显示出大体框架。调用 API 获取到时机业务数据后才能展示出最终的页面内容。由此得出结论,因为要等待文件加载、CSSOM 构建、JS 解析等过程,而这些过程比较耗时,导致用户会长时间出于不可交互的首屏灰白屏状态,从而给用户一种网页很“慢”的感觉。那么一个网页太“慢”,会造成什么影响呢?“慢”的影响Global Web Performance Matters for ecommerce的报告中指出:57%的用户更在乎网页在3秒内是否完成加载。52%的在线用户认为网页打开速度影响到他们对网站的忠实度。每慢1秒造成页面 PV 降低11%,用户满意度也随之降低降低16%。近半数移动用户因为在10秒内仍未打开页面从而放弃。我们团队主要负责美团支付相关的业务,如果网站太慢会影响用户的支付体验,会造成客诉或资损。既然网站太“慢”会造成如此重要的影响,那要如何优化呢?优化思路在User-centric Performance Metrics一文中,共提到了4个页面渲染的关键指标:基于这个理论基础,再回过头来看看之前项目的实际表现:可见在 FP 的灰白屏界面停留了很长时间,用户不清楚网站是否有在正常加载,用户体验很差。试想:如果我们可以将 FCP 或 FMP 完整的 HTML 文档提前到 FP 时机预渲染,用户看到页面框架,能感受到页面正在加载而不是冷冰冰的灰白屏,那么用户更愿意等待页面加载完成,从而降低了流失率。并且这种改观在弱网环境下更明显。通过对比 FP、FCP、FMP 这三个时期 DOM 的差异,发现区别在于:FP:仅有一个 div 根节点。FCP:包含页面的基本框架,但没有数据内容。FMP:包含页面所有元素及数据。仍然以 Vue 为例, 在其生命周期中,mounted 对应的是 FCP,updated 对应的是 FMP。那么具体应该使用哪个生命周期的 HTML 结构呢? mounted (FCP)updated (FMP)缺点只是视觉体验将 FCP 提前,实际的 TTI 时间变化不大构建时需要获取数据,编译速度慢构建时与运行时的数据存在差异性有复杂交互的页面,仍需等待,实际的 TTI 时间变化不大优点不受数据影响,编译速度快首屏体验好对于纯展示类型的页面,FP 与 TTI 时间近乎一致通过以上的对比,最终选择在 mounted 时触发构建时预渲染。由于我们采用的是 CSR 的架构,没有 Node 作为中间层,因此要实现 DOM 内容的预渲染,就需要在项目构建编译时完成对原始模板的更新替换。至此,我们明确了构建时预渲染的大体方案。构建时预渲染方案构建时预渲染流程:配置读取由于 SPA 可以由多个路由构成,需要根据业务场景决定哪些路由需要用到预渲染。因此这里的配置文件主要是用于告知编译器需要进行预渲染的路由。在我们的系统架构里,脚手架是基于 Webpack 自研的,在此基础上可以自定义自动化构建任务和配置。触发构建项目中主要是使用 TypeScript,利用 TS 的装饰器,我们封装了统一的预渲染构建的钩子方法,从而只用一行代码即可完成构建时预渲染的触发。装饰器:使用:构建编译从流程图上,需要在发布机上启动模拟的浏览器环境,并通过预渲染的事件钩子获取当前的页面内容,生成最终的 HTML 文件。由于我们在预渲染上的尝试比较早,当时还没有 Headless Chrome 、 Puppeteer、Prerender SPA Plugin等,因此在选型上使用的是 phantomjs-prebuilt(Prerender SPA Plugin 早期版本也是基于 phantomjs-prebuilt 实现的)。通过 phantom 提供的 API 可获得当前 HTML,示例如下:为了提高构建效率,并行对配置的多个页面或路由进行预渲染构建,保证在 5S 内即可完成构建,流程图如下:方案优化理想很丰满,现实很骨感。在实际投产中,构建时预渲染方案遇到了一个问题。我们梳理一下简化后的项目上线过程:开发 -> 编译 -> 上线假设本次修改了静态文件中的一个 JS 文件,这个文件会通过 CDN 方式在 HTML 里引用,那么最终在 HTML 文档中的引用方式是 <script src=“http://cdn.com/index.js"></script>。然而由于项目还没有上线,所以其实通过完整 URL 的方式是获取不到这个文件的;而预渲染的构建又是在上线动作之前,所以问题就产生了:构建时预渲染无法正常获取文件,导致编译报错怎么办?请求劫持因为在做预渲染时,我们使用启动了一个模拟的浏览器环境,根据 phantom 提供的 API,可以对发出的请求加以劫持,将获取 CDN 文件的请求劫持到本地,从而在根本上解决了这个问题。示例代码如下:构建时预渲染研发流程及效果最终,构建时预渲染研发流程如下:开发阶段:通过 TypeScript 的装饰器单行引入预渲染构建触发的方法。发布前修改编译构建的配置文件。发布阶段:先进行常规的项目构建。若有预渲染相关配置,则触发预渲染构建。通过预渲染得到最终的文件,并完成发布上线动作。完整的用户请求路径如下:通过构建时预渲染在项目中的使用,FCP 的时间相比之前减少了 75%。作者简介寒阳,美团资深研发工程师,多年前端研发经历,负责美团支付钱包团队和美团支付前端基础技术。招聘信息我们美团金融服务平台大前端研发组在高速成长中,我们欢迎更多优秀的 Web 前端研发工程师加入,感兴趣的朋友可以将简历发送到邮箱:shanghanyang@meituan.com。 ...

November 16, 2018 · 1 min · jiezi

不可不说的Java“锁”事

前言Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:1. 乐观锁 VS 悲观锁乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。根据从上面的概念描述我们可以发现:悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:// ————————- 悲观锁的调用方式 ————————-// synchronizedpublic synchronized void testMethod() { // 操作同步资源}// ReentrantLockprivate ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock();}// ————————- 乐观锁的调用方式 ————————-private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicIntegeratomicInteger.incrementAndGet(); //执行自增1通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。CAS算法涉及到三个操作数:需要读写的内存值 V。进行比较的值 A。要写入的新值 B。当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:根据定义我们可以看出各属性的作用:unsafe: 获取并操作内存的数据。valueOffset: 存储value在AtomicInteger中的偏移量。value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:// ————————- JDK 8 ————————-// AtomicInteger 自增方法public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}// Unsafe.classpublic final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;}// ————————- OpenJDK 8 ————————-// Unsafe.javapublic final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v;}根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:1.ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。2.循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。3.只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。2. 自旋锁 VS 适应性自旋锁在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,本文中仅做名词介绍,不做深入讲解,感兴趣的同学可以自行查阅相关资料。3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。首先为什么Synchronized能实现线程同步?在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。Java对象头synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。MonitorMonitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:锁状态存储内容存储内容无锁对象的hashCode、对象分代年龄、是否是偏向锁(0)01偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量(重量级锁)的指针10无锁无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。偏向锁偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。轻量级锁是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。重量级锁升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。整体的锁状态升级流程如下:综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。4. 公平锁 VS 非公平锁公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。如上图所示,假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。如下图所示:接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁。根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。下面我们来看一下公平锁与非公平锁的加锁方法的源码:通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。5. 可重入锁 VS 非可重入锁可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:public class Widget { public synchronized void doSomething() { System.out.println(“方法1执行…”); doOthers(); } public synchronized void doOthers() { System.out.println(“方法2执行…”); }}在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下。还是打水的例子,有多个人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。但如果是非可重入锁的话,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。6. 独享锁 VS 共享锁独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。下图为ReentrantReadWriteLock的部分源码:我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识。在最开始提及AQS的时候我们也提到了state字段(int类型,32位),该字段用来描述有多少线程获持有锁。在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:了解了概念之后我们再来看代码,先看写锁的加锁源码:protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); // 取到当前锁的个数 int w = exclusiveCount(c); // 取写锁的个数w if (c != 0) { // 如果已经有线程持有了锁(c!=0) // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败 return false; if (w + exclusiveCount(acquires) > MAX_COUNT) // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。 throw new Error(“Maximum lock count exceeded”); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。 return false; setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者 return true;}这段代码首先取到当前锁的个数c,然后再通过c来获取写锁的个数w。因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount(c); ),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败。如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。接着是读锁的代码:protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态 int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current);}可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。此时,我们再回头看一下互斥锁ReentrantLock中公平锁和非公平锁的加锁源码:我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。结语本文Java中常用的锁以及常见的锁的概念进行了基本介绍,并从源码以及实际应用的角度进行了对比分析。限于篇幅以及个人水平,没有在本篇文章中对所有内容进行深层次的讲解。其实Java本身已经对锁本身进行了良好的封装,降低了研发同学在平时工作中的使用难度。但是研发同学也需要熟悉锁的底层原理,不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路,也是值得大家去学习和借鉴的。参考资料1.《Java并发编程艺术》2.Java中的锁3.Java CAS 原理剖析4.Java并发——关键字synchronized解析5.Java synchronized原理总结6.聊聊并发(二)——Java SE1.6中的Synchronized7.深入理解读写锁—ReadWriteLock源码分析8.【JUC】JDK1.8源码分析之ReentrantReadWriteLock9.Java多线程(十)之ReentrantReadWriteLock深入分析10.Java–读写锁的实现原理作者简介家琪,美团点评后端工程师。2017 年加入美团点评,负责美团点评境内度假的业务开发。 ...

November 16, 2018 · 2 min · jiezi

强化学习在美团“猜你喜欢”的实践

1 概述“猜你喜欢”是美团流量最大的推荐展位,位于首页最下方,产品形态为信息流,承担了帮助用户完成意图转化、发现兴趣、并向美团点评各个业务方导流的责任。经过多年迭代,目前“猜你喜欢”基线策略的排序模型是业界领先的流式更新的Wide&Deep模型[1]。考虑Point-Wise模型缺少对候选集Item之间的相关性刻画,产品体验中也存在对用户意图捕捉不充分的问题,从模型、特征入手,更深入地理解时间,仍有推荐体验和效果的提升空间。近年来,强化学习在游戏、控制等领域取得了令人瞩目的成果,我们尝试利用强化学习针对以上问题进行优化,优化目标是在推荐系统与用户的多轮交互过程中的长期收益。在过去的工作中,我们从基本的Q-Learning着手,沿着状态从低维到高维,动作从离散到连续,更新方式从离线到实时的路径进行了一些技术尝试。本文将介绍美团“猜你喜欢”展位应用强化学习的算法和工程经验。第2节介绍基于多轮交互的MDP建模,这部分和业务场景强相关,我们在用户意图建模的部分做了较多工作,初步奠定了强化学习取得正向收益的基础。第3节介绍网络结构上的优化,针对强化学习训练不稳定、难以收敛、学习效率低、要求海量训练数据的问题,我们结合线上A/B Test的线上场景改进了DDPG模型,取得了稳定的正向收益。第4节介绍轻量级实时DRL框架的工作,其中针对TensorFlow对Online Learning支持不够好和TF serving更新模型时平响骤升的问题做了一些优化。图1 美团首页“猜你喜欢”场景2 MDP建模在“猜你喜欢“展位中,用户可以通过翻页来实现与推荐系统的多轮交互,此过程中推荐系统能够感知用户的实时行为,从而更加理解用户,在接下来的交互中提供更好的体验。“猜你喜欢”用户-翻页次数的分布是一个长尾的分布,在图2中我们把用户数取了对数。可知多轮交互确实天然存在于推荐场景中。图2 “猜你喜欢”展位用户翻页情况统计在这样的多轮交互中,我们把推荐系统看作智能体(Agent),用户看作环境(Environment),推荐系统与用户的多轮交互过程可以建模为MDP<S,A,R,P>:State:Agent对Environment的观测,即用户的意图和所处场景。Action:以List-Wise粒度对推荐列表做调整,考虑长期收益对当前决策的影响。Reward:根据用户反馈给予Agent相应的奖励,为业务目标直接负责。P(s,a):Agent在当前State s下采取Action a的状态转移概率。图3 推荐系统与用户交互示意图我们的优化目标是使Agent在多轮交互中获得的收益最大化:具体而言,我们把交互过程中的MDP<A,S,R,P>建模如下:2.1 状态建模状态来自于Agent对Environment的观察,在推荐场景下即用户的意图和所处场景,我们设计了如图4所示的网络结构来提取状态的表达。网络主要分为两个部分:把用户实时行为序列的Item Embedding作为输入,使用一维CNN学习用户实时意图的表达;推荐场景其实仍然相当依赖传统特征工程,因此我们使用Dense和Embedding特征表达用户所处的时间、地点、场景,以及更长时间周期内用户行为习惯的挖掘。图4 状态建模网络结构这里我们介绍一下使用Embedding特征表达用户行为习惯挖掘的Binary Sequence[2] 方法。我们通过特征工程对用户行为序列做各种维度的抽象,做成一些列离散的N进制编码,表示每一位有N种状态。例如统计用户在1H/6H/1D/3D/1W不同时间窗口内是否有点击行为编码成5位2进制数,把这些数字作为离散特征学习Embedding表达,作为一类特征处理方法。除此之外,还有点击品类是否发生转移、点击间隔的gap等等,在“猜你喜欢”场景的排序模型和强化学习状态建模中都取得了很不错的效果。原因是在行为数据非常丰富的情况下,序列模型受限于复杂度和效率,不足以充分利用这些信息,Binary Sequence可以作为一个很好的补充。图5 序列模型和特征工程效果对照图5左侧是序列模型的部分,分别使用不同的Pooling方式和一维CNN离线效果的对比,右侧是Dense和Embedding特征的部分,分别加入用户高频行为、距离、行为时间间隔、行为次数、意图转移等特征,以及加入所有显著正向特征的离线效果。2.2 动作设计“猜你喜欢”目前使用的排序模型由两个同构的Wide&Deep模型组成,分别以点击和支付作为目标训练,最后把两个模型的输出做融合。融合方法如下图所示:图6 排序模型示意图超参数 的物理意义是调整全量数据集中点击和下单模型的Trade Off,通过综合考虑点击和下单两个任务的AUC确定,没有个性化的因素。我们以此为切入点,使用Agent的动作调整融合超参数,令:a是由Agent的策略生成Action,这样做有两个好处:其一,我们知道一个较优解是a=1,这种情况下强化学习策略和基线的排序策略保持一致,由于强化学习是个不断试错的过程,我们可以很方便地初始化Agent的策略为a=1,从而避免在实验初期伤害线上效果。其二,允许我们根据物理意义对Action做Clip,从而减轻强化学习更新过程不稳定造成的实际影响。2.3 奖励塑形“猜你喜欢”展位的优化核心指标是点击率和下单率,在每个实验分桶中分母是基本相同的,因此业务目标可以看成优化点击次数和下单次数,我们尝试将奖励塑形如下:相对于关注每个Item转化效率的Point Wise粒度的排序模型,强化学习的目标是最大化多轮交互中的奖励收益,为业务目标直接负责。图7 加入惩罚项前后的相对效果变化在实验过程中我们发现,强化学习的策略可能上线初期效果很好,在点击和下单指标上都取得了一定的提升,但在后续会逐渐下降,如图7前半段所示。在逐层转化效率的分析中,我们发现强化学习分桶的设备曝光率和UV维度点击率有所降低,而用户停留时长和浏览深度稳定提升,这说明Agent学习到了让用户与推荐系统更多交互,从而获取更多曝光和转化机会的策略,但这种策略对于部分强烈下单意图用户的体验是有伤害的,因为这部分用户意图转化的代价变高了,因而对展位的期望变低。针对这种情况,我们在奖励塑形中加入两个惩罚项:惩罚没有发生任何转化(点击/下单)行为的中间交互页面(penalty1),从而让模型学习用户意图转化的最短路;惩罚没有发生任何转化且用户离开的页面(penalty2),从而保护用户体验。修正后的奖励为:由于用户体验是时间连续的,UV维度的效果在报表上有一定的滞后性,约一周后点击率和万订单恢复到正向水平,同时用户停留时长和浏览深度有进一步提升,说明Agent确实学到了在避免伤害用户的前提下,从多轮交互中获取更多转化的策略,如图7后半段所示。这一节我们介绍了MDP建模相关的工作。MDP跟业务场景是强相关的,经验不是很容易迁移。就本文的场景而言,我们花了较多精力做状态表达的特征,这部分工作使强化学习得到了在自己的目标上取得正向收益的能力,因此对这部分介绍比较细致。动作设计是针对多目标模型融合的场景,是个业界普遍存在并且监督学习不太适用的场景,也能体现强化学习的能力。奖励塑形是为了缩小强化学习的目标和业务目标之间的Gap,需要在数据洞察和业务理解上做一些工作。完成上述工作后强化学习在自己的目标和业务指标上已经能取得了一些正向效果,但不够稳定。另外由于策略迭代是个Online Learning的过程,实验上线后需要实时训练一周才能收敛并观察效果,这也严重影响了我们的迭代效率。针对这些情况我们针对模型做了一些改进。3 改进的DDPG模型在模型方面,我们在不断改进MDP建模的过程中先后尝试了Q-Learning、DQN[3]和DDPG[4]模型,也面临着强化学习中普遍存在更新不够稳定、训练过程容易不收敛、学习效率较低(这里指样本利用效率低,因此需要海量样本)的问题。具体到推荐场景中,由于List-Wise维度的样本比Point-Wise少得多,以及需要真实的动作和反馈作为训练样本,因此我们只能用实验组的小流量做实时训练。这样一来训练数据量相对就比较少,每天仅有几十万,迭代效率较低。为此我们对网络结构做了一些改进,包括引入具体的Advantage函数、State权值共享、On-Policy策略的优化,结合线上A/B Test框架做了十数倍的数据增强,以及对预训练的支持。接下来我们以DDPG为基石,介绍模型改进的工作。图8 DDPG模型如图8所示,基本的DDPG是Actor-Critic架构。线上使用Actor网络,预测当前State下最好的动作a,并通过Ornstein-Uhlenbeck过程对预测的Action加一个随机噪声得到a’,从而达到在最优策略附近探索的目的。将a’ 作用于线上,并从用户(Environment)获得相应的收益。训练过程中,Critic学习估计当前状态s下采取动作a获得的收益,使用MSE作为Loss Function:对参数求导:Actor使用Critic反向传播的策略梯度,使用梯度上升的方法最大化Q估计,从而不断优化策略:在确定性策略梯度的公式中,是策略的参数,Agent将使用策略(s)在状态s 生成动作a,(指数关系)表示该策略下的状态转移概率。在整个学习过程中,我们不需要真的估计策略的价值,只需要根据Critic返回的策略梯度最大化Q估计。Critic不断优化自己对Q(s,a)的估计,Actor通过Critic的判断的梯度,求解更好的策略函数。如此往复,直到Actor收敛到最优策略的同时,Critic收敛到最准确的Q(s,a)估计。接下来基于这些我们介绍的DDPG模型改进的工作。3.1 Advantage函数借鉴DDQN[5]的优势函数Advantage的思路,我们把critic估计的Q(s,a)拆分成两个部分:只与状态相关的V(s),与状态、动作都相关的Advantage函数A(s,a),有Q(s,a) = V(s) + A(s,a),这样能够缓解critic对Q过高估计的问题。具体到推荐环境中,我们的策略只是对排序模型的融合参数做调整,收益主要是由状态决定的。图9 实验组与基线的Q值对比如图9所示,在实际实验中观察V(s)和A(s,a)均值的比值大约为97:3,可以验证我们的这一判断。在实际训练过程中,我们先根据状态和收益训练V(s),再使用Q(s,a)-V(s)的残差训练A(s,a),很大程度上提升了训练稳定性,并且我们可以通过残差较为直观地观测到到当前策略是否优于基线。图8中A(s,a)稳定大于0,可以认为强化学习在自己的目标上取得了稳定的正向收益。3.2 State权值共享受A3C[6]网络的启发,我们观察到DDPG的网络中Actor和Critic网络中都有State的表达,而在我们的场景中大部分参数都集中在State的部分,在十万量级,其他参数只有数千,因此我们尝试把State部分的权重做共享,这样可以减少约一半的训练参数。图10 使用advantage函数并做state权值共享改进后的网络结构如图10所示。对于这个网络结构,我们注意到有V(s)的分支和动作不相关,意即我们不需要具体的Action也可以学习该State下Q的期望,这就允许我们在线下使用基线策略千万级的数据量做预训练,线上也同时使用基线和实验流量做实时更新,从而提升训练的效果和稳定性。又因为这条更新路径包含了所有State的参数,模型的大部分参数都可以得到充分的预训练,只有Action相关的参数必须依赖Online Learning的部分,这就大幅提高了我们的实验迭代效率。原来我们需要上线后等待一周训练再观察效果,改进后上线第二天就可以开始观察效果。3.3 On-policy在A2C[7]的论文里作者论述了他们的见解:同步A2C实现比异步实现的A3C表现要好。目前尚未看到任何证据证明异步引入的噪声能够提供任何性能收益,因此为了提升训练效率,我们采取了这个做法,使用同一套参数估计Q_{t+1}和更新Q_t,从而使模型参数再次减半。3.4 扩展到多组并行策略考虑多组强化学习实验同时在线的情况,结合A/B Test环境特点,我们把以上网络框架扩展到多Agent的情况。图11 支持多组线上实验DDPG模型如图11所示,线上多组实验共享State表达和V(s)的估计,每个策略训练自己的A(s,a)网络且能快速收敛,这样的结构一方面使训练过程更加稳定,另一方面为强化学习策略全量提供了可能性。图12 点击率分天实验效果在DDPG的改造工作中,我们使用Advantage函数获得更稳定的训练过程和策略梯度。State权值共享和On-Policy方法使我们的模型参数减少75%。Advantage函数和State权值共享结合,允许我们使用基线策略样本做数据增强,使每天的训练样本从十万量级扩展到百万量级,同时充分的预训练保证策略上线后能迅速收敛。经过这些努力,强化学习线上实验取得了稳定的正向效果,在下单率效果持平的情况下,周效果点击率相对提升0.5%,平均停留时长相对提升0.3%,浏览深度相对提升0.3%。修改过的模型与A2C的主要区别是我们仍然使用确定性策略梯度,这样我们可以少估计一个动作的分布,即随机策略方差降至0的特例。图12表明强化实习的效果是稳定的,由于“猜你喜欢”的排序模型已经是业界领先的流式DNN模型,我们认为这个提升是较为显著的。4 基于TF的轻量级实时DRL系统强化学习通常是在一次次试错(Trial-and-Error)中学习,实时地改进策略并获得反馈能大幅提升学习效率,尤其在连续策略中。这一点在游戏场景下很容易理解,相应地,我们也在推荐系统中构建了实时深度学习系统,让策略更新更加高效。为了支持实时更新的DRL模型和高效实验,我们针对Online Learning的需求,基于TensorFlow及TF Serving做了一些改进和优化,设计并实现了一套特征配置化的实时更新的DRL框架,在实验迭代过程中沉淀了DQN、DDQN、DDPG、A3C、A2C、PPO[8]等模型。系统架构如图13所示:图13 实时更新的强化学习框架训练部分工作流如下:Online Joiner从Kafka中实时收集特征和用户反馈,拼接成Point-Wise粒度的Label-Feature样本,并把样本输出到Kafka和HDFS,分别支持在线和离线更新。Experience Collector收集上述样本,合并为List-Wise的请求粒度,并根据请求时间戳拼接成[<State, Action, Reward>]列表形式的MC Episode,再经过状态转移计算拆分成 <s_t, a_t, r_t, s_{t+1}> 形式的TD Instance,输出MC或TD格式的样本支持RL训练。Trainer做输入特征的预处理,使用TensorFlow训练DRL模型。Version Controller负责调度任务保证实效性和质量,并把训练完成且指标符合预期模型推送到TF Serving和Tair中,这部分只需要Actor相关的参数。Tair作为弥补TF在Online Learning短板辅助的PS,后面会具体介绍。Monitor监控和记录整个训练流程中的数据量和训练指标,对不符合预期的情况发出线上告警。新模型上线前会先做离线的Pre-Train,使用基线策略的数据学习State的表达和Value net。上线后实时同时更新Actor,Advantage和Value的参数。线上预测部分,推荐系统的Agent从Tair获取预处理参数,并将处理后的特征喂给TF Serving做前向传播,得到Action并对展现给用户的排序结果做相应的干预。针对TensorFLow对Online Learning支持比较弱,Serving对千万级Embedding处理效率不高的问题,我们做了一些改进:在线上特征的分布会随时间而改变,针对Dense特征我们自己维护增量的Z-Score算法对特征做预处理。Embedding特征的输入维度也经常发生变化,而TF不支持变长的Input Dimention,为此我们维护了全量的ID-Embedding映射,每次训练让模型加载当前样本集合中的高频Embedding。千万级Item Embedding会大幅降低训练和预测的效率,为此我们把这部分映射在预处理中,把映射后的矩阵直接作为CNN的输入。为了提升特征工程的实验效率,支持特征配置化生成模型结构。此外,TF serving在更新模型一两分钟内响应时间会骤然升高,导致很多请求超时,原因有二,其一,serving的模型加载和请求共用一个线程池,导致切换模型使阻塞处理请求;其二,计算图初始化是lazy的,这样新模型后的第一次请求需要等待计算图初始化。这个问题在更新模型频Low对online learning支持比较弱繁的Online Learning场景影响较大,我们采用切分线程池和warm up初始化的方式解决。更具体的方案和效果可以参考美团另一篇技术博客[9]。5 总结和展望强化学习是目前深度学习领域发展最快的方向之一,其与推荐系统和排序模型的结合也有更多价值等待发掘。本文介绍了强化学习在美团“猜你喜欢”排序场景落地的工作,包括根据业务场景不断调整的MDP建模,使强化学习能够取得一定的正向收益;通过改进DDPG做数据增强,提升模型的鲁棒性和实验效率,从而取得稳定的正向收益;以及基于TensorFlow的实时DRL框架,为高效并行策略迭代提供了基础。经过一段时间的迭代优化,我们在强化学习方面也积累了一些经验,与传统的监督学习相比,强化学习的价值主要体现在:灵活的奖励塑形,能支持各种业务目标建模,包括不限于点击率、转化率、GMV、停留时长、浏览深度等,支持多目标融合,为业务目标直接负责。充满想象空间的动作设计,不需要直接的Label,而是通过网络来生成和评价策略,适合作为监督学习的补充。这点和GAN有相通之处。考虑优化长期收益对当前决策造成的影响,Agent与Environment交互频繁的场景更加能体现强化学习的价值。同时强化学习作为机器学习的一个分支,很多机器学习的经验仍然适用于此。比如数据和特征决定效果的上限,模型和算法只是不断逼近它。对于强化学习而言特征空间主要包含在状态的建模中,我们强烈建议在状态建模上多做一些尝试,并信任模型有能力从中做出判断。再如,使用更多的训练数据降低经验风险,更少的参数降低结构风险的思路对强化学习仍然适用,因此我们认为DDPG的改进工作能够推广到不同业务的线上A/B Test场景中。此外,我们在训练过程中也遇到了强化学习对随机性敏感的问题[10],为此我们线上使用了多组随机种子同时训练,选择表现最好的一组参数用于实际参数更新。在目前的方案中,我们尝试的Action是调整模型融合参数,主要考虑这是个排序问题中比较通用的场景,也适合体现强化学习的能力,而实际上对排序结果的干预能力是比较有限的。未来我们会探索不同品类、位置、价格区间等跟用户意图场景强相关属性的召回个数,调整排序模型隐层参数等方向。另外在解决学习效率低下的问题上,还将尝试Priority Sampling 提高样本利用效率,Curious Networks提升探索效率等方法。也欢迎对强化学习感兴趣的朋友们与我们联系,一起交流探索强化学习在工业界的应用与发展,同时对文章的错漏之处也欢迎大家批评指正。参考文献[1] Heng-Tze Cheng, Levent Koc, Jeremiah Harmsen, Tal Shaked, Tushar Chandra, Hrishi Aradhye, Glen Anderson, Greg Corrado, Wei Chai, Mustafa Ispir, Rohan Anil, Zakaria Haque, Lichan Hong, Vihan Jain, Xiaobing Liu, and Hemal Shah. Wide & deep learning for recommender systems. CoRR, 2016.[2] Yan, P., Zhou, X., Duan, Y. E-commerce item recommendation based on field-aware FactorizationMachine. In: Proceedings of the 2015 International ACM Recommender Systems Challenge, 2015.[3] Mnih, Volodymyr, Kavukcuoglu, Koray, Silver, David, Rusu, Andrei A, Veness, Joel, Bellemare,Marc G, Graves, Alex, Riedmiller, Martin, Fidjeland, Andreas K, Ostrovski, Georg, et al. Humanlevelcontrol through deep reinforcement learning. Nature, 2015.[4] Lillicrap, T., Hunt, J., Pritzel, A., Heess, N., Erez, T., Tassa, Y., Silver, D., and Wierstra, D. Continuous control with deep reinforcement learning. In International Conference, 2015on Learning Representations, 2016.[5] Wang, Z., de Freitas, N., and Lanctot, M. Dueling network architectures for deep reinforcementlearning. Technical report, 2015.[6] Volodymyr Mnih, Adria Puigdomenech Badia, Mehdi Mirza, Alex Graves, Tim-othy P. Lillicrap, Tim Harley, David Silver, and Koray Kavukcuoglu. Asyn-chronous methods for deep reinforcement learning. ICML, 2016[7] Y. Wu, E. Mansimov, S. Liao, R. Grosse, and J. Ba. Scalable trust-region method for deep reinforcementlearning using kronecker-factored approximation. arXiv preprint arXiv:1708.05144, 2017.[8] Schulman, J.; Wolski, F.; Dhariwal, P.; Radford, A.; and Klimov,O. Proximal policy optimization algorithms. arXiv preprintarXiv:1707.06347, 2017[9] 仲达, 鸿杰, 廷稳. 基于TensorFlow Serving的深度学习在线预估. MT Bolg, 2018[10] P. Henderson, R. Islam, P. Bachman, J. Pineau, D. Precup, and D. Meger. Deep reinforcement learningthat matters. arXiv:1709.06560, 2017.作者简介段瑾,2015年加入美团点评,目前负责强化学习在推荐场景的落地工作。 ...

November 16, 2018 · 2 min · jiezi

美团容器平台架构及容器技术实践

本文根据美团基础架构部/容器研发中心技术总监欧阳坚在2018 QCon(全球软件开发大会)上的演讲内容整理而成。背景美团的容器集群管理平台叫做HULK。漫威动画里的HULK在发怒时会变成“绿巨人”,它的这个特性和容器的“弹性伸缩”很像,所以我们给这个平台起名为HULK。貌似有一些公司的容器平台也叫这个名字,纯属巧合。2016年,美团开始使用容器,当时美团已经具备一定的规模,在使用容器之前就已经存在的各种系统,包括CMDB、服务治理、监控告警、发布平台等等。我们在探索容器技术时,很难放弃原有的资产。所以容器化的第一步,就是打通容器的生命周期和这些平台的交互,例如容器的申请/创建、删除/释放、发布、迁移等等。然后我们又验证了容器的可行性,证实容器可以作为线上核心业务的运行环境。2018年,经过两年的运营和实践探索,我们对容器平台进行了一次升级,这就是容器集群管理平台HULK 2.0。把基于OpenStack的调度系统升级成容器编排领域的事实标准Kubernetes(以后简称K8s)。提供了更丰富可靠的容器弹性策略。针对之前在基础系统上碰到的一些问题,进行了优化和打磨。美团的容器使用状况是:目前线上业务已经超过3000个服务,容器实例数超过30000个,很多大并发、低延时要求的核心链路服务,已经稳定地运行在HULK之上。本文主要介绍我们在容器技术上的一些实践,属于基础系统优化和打磨。美团容器平台的基本架构首先介绍一下美团容器平台的基础架构,相信各家的容器平台架构大体都差不多。首先,容器平台对外对接服务治理、发布平台、CMDB、监控告警等等系统。通过和这些系统打通,容器实现了和虚拟机基本一致的使用体验。研发人员在使用容器时,可以和使用VM一样,不需要改变原来的使用习惯。此外,容器提供弹性扩容能力,能根据一定的弹性策略动态增加和减少服务的容器节点数,从而动态地调整服务处理能力。这里还有个特殊的模块——“服务画像”,它的主要功能是通过对服务容器实例运行指标的搜集和统计,更好的完成调度容器、优化资源分配。比如可以根据某服务的容器实例的CPU、内存、IO等使用情况,来分辨这个服务属于计算密集型还是IO密集型服务,在调度时尽量把互补的容器放在一起。再比如,我们可以知道某个服务的每个容器实例在运行时会有大概500个进程,我们就会在创建容器时,给该容器加上一个合理的进程数限制(比如最大1000个进程),从而避免容器在出现问题时,占用过多的系统资源。如果这个服务的容器在运行时,突然申请创建20000个进程,我们有理由相信是业务容器遇到了Bug,通过之前的资源约束对容器进行限制,并发出告警,通知业务及时进行处理。往下一层是“容器编排”和“镜像管理”。容器编排解决容器动态实例的问题,包括容器何时被创建、创建到哪个位置、何时被删除等等。镜像管理解决容器静态实例的问题,包括容器镜像应该如何构建、如何分发、分发的位置等等。最下层是我们的容器运行时,美团使用主流的Linux+Docker容器方案,HULK Agent是我们在服务器上的管理代理程序。把前面的“容器运行时”具体展开,可以看到这张架构图,按照从下到上的顺序介绍:最下层是CPU、内存、磁盘、网络这些基础物理资源。往上一层,我们使用的是CentOS7作为宿主机操作系统,Linux内核的版本是3.10。我们在CentOS发行版默认内核的基础上,加入一些美团为容器场景研发的新特性,同时为高并发、低延时的服务型业务做了一些内核参数的优化。再往上一层,我们使用的是CentOS发行版里自带的Docker,当前的版本是1.13,同样,加入了一些我们自己的特性和增强。HULK Agent是我们自己开发的主机管理Agent,在宿主机上管理Agent。Falcon Agent同时存在于宿主机和容器内部,它的作用是收集宿主机和容器的各种基础监控指标,上报给后台和监控平台。最上一层是容器本身。我们现在主要支持CentOS 6和CentOS 7两种容器。在CentOS 6中有一个container init进程,它是我们开发容器内部的1号进程,作用是初始化容器和拉起业务进程。在CentOS 7中,我们使用了系统自带的systemd作为容器中的1号进程。我们的容器支持各种主流编程语言,包括Java、Python、Node.js、C/C++等等。在语言层之上是各种代理服务,包括服务治理的Agent、日志Agent、加密Agent等等。同时,我们的容器也支持美团内部的一些业务环境,例如set信息、泳道信息等,配合服务治理体系,可以实现服务调用的智能路由。美团主要使用了CentOS系列的开源组件,因为我们认为Red Hat有很强的开源技术实力,比起直接使用开源社区的版本,我们希望Red Hat的开源版本能够帮助解决大部分的系统问题。我们也发现,即使部署了CentOS的开源组件,仍然有可能会碰到社区和Red Hat没有解决的问题。从某种程度上也说明,国内大型互联公司在技术应用的场景、规模、复杂度层面已经达到了世界领先的水平,所以才会先于社区、先于Red Hat的客户遇到这些问题。容器遇到的一些问题在容器技术本身,我们主要遇到了4个问题:隔离、稳定性、性能和推广。隔离包含两个层面:第一个问题是,容器能不能正确认识自身资源配置;第二个问题是,运行在同一台服务器上的容器会不会互相影响。比如某一台容器的IO很高,就会导致同主机上的其他容器服务延时增加。稳定性:这是指在高压力、大规模、长时间运行以后,系统功能可能会出现不稳定的问题,比如容器无法创建、删除,因为软件问题发生卡死、宕机等问题。性能:在虚拟化技术和容器技术比较时,大家普遍都认为容器的执行效率会更高,但是在实践中,我们遇到了一些特例:同样的代码在同样配置的容器上,服务的吞吐量、响应时延反而不如虚拟机。推广:当我们把前面几个问题基本上都解决以后,仍然可能会碰到业务不愿意使用容器的情况,其中原因一部分是技术因素,例如容器接入难易程度、周边工具、生态等都会影响使用容器的成本。推广也不是一个纯技术问题,跟公司内部的业务发展阶段、技术文化、组织设置和KPI等因素都密切相关。容器的实现容器本质上是把系统中为同一个业务目标服务的相关进程合成一组,放在一个叫做namespace的空间中,同一个namespace中的进程能够互相通信,但看不见其他namespace中的进程。每个namespace可以拥有自己独立的主机名、进程ID系统、IPC、网络、文件系统、用户等等资源。在某种程度上,实现了一个简单的虚拟:让一个主机上可以同时运行多个互不感知的系统。此外,为了限制namespace对物理资源的使用,对进程能使用的CPU、内存等资源需要做一定的限制。这就是Cgroup技术,Cgroup是Control group的意思。比如我们常说的4c4g的容器,实际上是限制这个容器namespace中所用的进程,最多能够使用4核的计算资源和4GB的内存。简而言之,Linux内核提供namespace完成隔离,Cgroup完成资源限制。namespace+Cgroup构成了容器的底层技术(rootfs是容器文件系统层技术)。美团的解法、改进和优化隔离之前一直和虚拟机打交道,但直到用上容器,才发现在容器里面看到的CPU、Memory的信息都是服务器主机的信息,而不是容器自身的配置信息。直到现在,社区版的容器还是这样,比如一个4c4g的容器,在容器内部可以看到有40颗CPU、196GB内存的资源,这些资源其实是容器所在宿主机的信息。这给人的感觉,就像是容器的“自我膨胀”,觉得自己能力很强,但实际上并没有,还会带来很多问题。上图是一个内存信息隔离的例子。获取系统内存信息时,社区Linux无论在主机上还是在容器中,内核都是统一返回主机的内存信息,如果容器内的应用,按照它发现的宿主机内存来进行配置的话,实际资源是远远不够的,导致的结果就是:系统很快会发生OOM异常。我们做的隔离工作,是在容器中获取内存信息时,内核根据容器的Cgroup信息,返回容器的内存信息(类似LXCFS的工作)。CPU信息隔离的实现和内存的类似,不再赘述,这里举一个CPU数目影响应用性能例子。大家都知道,JVM GC(垃圾对象回收)对Java程序执行性能有一定的影响。默认的JVM使用公式“ParallelGCThreads = (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8)” 来计算做并行GC的线程数,其中ncpus是JVM发现的系统CPU个数。一旦容器中JVM发现了宿主机的CPU个数(通常比容器实际CPU限制多很多),这就会导致JVM启动过多的GC线程,直接的结果就导致GC性能下降。Java服务的感受就是延时增加,TP监控曲线突刺增加,吞吐量下降。针对这个问题有各种解法:显式的传递JVM启动参数“-XX:ParallelGCThreads”告诉JVM应该启动几个并行GC线程。它的缺点是需要业务感知,为不同配置的容器传不同的JVM参数。在容器内使用Hack过的glibc,使JVM(通过sysconf系统调用)能正确获取容器的CPU资源数。我们在一段时间内使用的就是这种方法。其优点是业务不需要感知,并且能自动适配不同配置的容器。缺点是必须使用改过的glibc,有一定的升级维护成本,如果使用的镜像是原生的glibc,问题也仍然存在。我们在新平台上通过对内核的改进,实现了容器中能获取正确CPU资源数,做到了对业务、镜像和编程语言都透明(类似问题也可能影响OpenMP、Node.js等应用的性能)。有一段时间,我们的容器是使用root权限进行运行,实现的方法是在docker run的时候加入‘privileged=true’参数。这种粗放的使用方式,使容器能够看到所在服务器上所有容器的磁盘,导致了安全问题和性能问题。安全问题很好理解,为什么会导致性能问题呢?可以试想一下,每个容器都做一次磁盘状态扫描的场景。当然,权限过大的问题还体现在可以随意进行mount操作,可以随意的修改NTP时间等等。在新版本中,我们去掉了容器的root权限,发现有一些副作用,比如导致一些系统调用失败。我们默认给容器额外增加了sys_ptrace和sys_admin两个权限,让容器可以运行GDB和更改主机名。如果有特例容器需要更多的权限,可以在我们的平台上按服务粒度进行配置。Linux有两种IO:Direct IO和Buffered IO。Direct IO直接写磁盘,Buffered IO会先写到缓存再写磁盘,大部分场景下都是Buffered IO。我们使用的Linux内核3.X,社区版本中所有容器Buffer IO共享一个内核缓存,并且缓存不隔离,没有速率限制,导致高IO容器很容易影响同主机上的其他容器。Buffer IO缓存隔离和限速在Linux 4.X里通过Cgroup V2实现,有了明显的改进,我们还借鉴了Cgroup V2的思想,在我们的Linux 3.10内核实现了相同的功能:每个容器根据自己的内存配置有对应比例的IO Cache,Cache的数据写到磁盘的速率受容器Cgroup IO配置的限制。Docker本身支持较多对容器的Cgroup资源限制,但是K8s调用Docker时可以传递的参数较少,为了降低容器间的互相影响,我们基于服务画像的资源分配,对不同服务的容器设定不同的资源限制,除了常见的CPU、内存外,还有IO的限制、ulimit限制、PID限制等等。所以我们扩展了K8s来完成这些工作。业务在使用容器的过程中产生core dump文件是常见的事,比如C/C++程序内存访问越界,或者系统OOM的时候,系统选择占用内存多的进程杀死,默认都会生成一个core dump文件。社区容器系统默认的core dump文件会生成在宿主机上,由于一些core dump文件比较大,比如JVM的core dump通常是几个GB,或者有些存在Bug的程序,其频发的core dump很容易快速写满宿主机的存储,并且会导致高磁盘IO,也会影响到其他容器。还有一个问题是:业务容器的使用者没有权限访问宿主机,从而拿不到dump文件进行下一步的分析。为此,我们对core dump的流程进行了修改,让dump文件写到容器自身的文件系统中,并且使用容器自己的Cgroup IO吞吐限制。稳定性我们在实践中发现,影响系统稳定性的主要是Linux Kernel和Docker。虽然它们本身是很可靠的系统软件,但是在大规模、高强度的场景中,还是会存在一些Bug。这也从侧面说明,我们国内互联网公司在应用规模和应用复杂度层面也属于全球领先。在内核方面,美团发现了Kernel 4.x Buffer IO限制的实现问题,得到了社区的确认和修复。我们还跟进了一系列CentOS的Ext4补丁,解决了一段时间内进程频繁卡死的问题。我们碰到了两个比较关键的Red Hat版Docker稳定性问题:在Docker服务重启以后,Docker exec无法进入容器,这个问题比较复杂。在解决之前我们用nsenter来代替Docker exec并积极反馈给RedHat。后来Red Hat在今年初的一个更新解决了这个问题。https://access.redhat.com/errata/RHBA-2017:1620是在特定条件下Docker Daemon会Panic,导致容器无法删除。经过我们自己Debug,并对比最新的代码,发现问题已经在Docker upstream中得到解决,反馈给Red Hat也很快得到了解决。https://github.com/projectatomic/containerd/issues/2面对系统内核、Docker、K8s这些开源社区的系统软件,存在一种观点是:我们不需要自己分析问题,只需要拿社区的最新更新就行了。但是我们并不认同,我们认为技术团队自身的能力很重要,主要是如下原因:美团的应用规模大、场景复杂,很多问题也许很多企业都没有遇到过,不能被动的等别人来解答。对于一些实际的业务问题或者需求(例如容器内正确返回CPU数目),社区也许觉得不重要,或者不是正确的理念,可能就不会解决。社区很多时候只在Upstream解决问题,而Upstream通常不稳定,即使有Backport到我们正在使用的版本,排期也很难进行保障。社区会发布很多补丁,通常描述都比较晦涩难懂。如果没有对问题的深刻理解,很难把遇到的实际问题和一系列补丁联系起来。对于一些复杂问题,社区的解决方案不一定适用于我们自身的实际场景,我们需要自身有能力进行判断和取舍。美团在解决开源系统问题时,一般会经历五个阶段:自己深挖、研发解决、关注社区、和社区交互,最后贡献给社区。性能容器平台性能,主要包括两个方面性能:业务服务运行在容器上的性能。容器操作(创建、删除等等)的性能。上图是我们CPU分配的一个例子,我们采用的主流服务器是两路24核服务器,包含两个Node,每个12核,算上超线程共48颗逻辑CPU。属于典型的NUMA(非一致访存)架构:系统中每个Node有自己的内存,Node内的CPU访问自己的内存的速度,比访问另一个Node内存的速度快很多(差一倍左右)。过去我们曾经遇到过网络中断集中到CPU0上的问题,在大流量下可能导致网络延时增加甚至丢包。为了保证网络处理能力,我们从Node0上划出了8颗逻辑CPU用来专门处理网络中断和宿主机系统上的任务,例如镜像解压这类高CPU的工作,这8颗逻辑CPU不运行任何容器的Workload。在容器调度方面,我们的容器CPU分配尽量不跨Node,实践证明跨Node访问内存对应用性能的影响比较大。在一些计算密集型的场景下,容器分配在Node内部会提升30%以上的吞吐量。按Node的分配方案也存在一定的弊端:会导致CPU的碎片增加,为了更高效地利用CPU资源。在实际系统中,我们会根据服务画像的信息,分配一些对CPU不敏感的服务容器跨Node使用CPU资源。上图是一个真实的服务在CPU分配优化前后,响应延时的TP指标线对比。可以看到TP999线下降了一个数量级,所有的指标都更加平稳。性能优化:文件系统针对文件系统的性能优化,第一步是选型,根据统计到的应用读写特征,我们选择了Ext4文件系统(超过85%的文件读写是对小于1M文件的操作)。Ext4文件系统有三种日志模式:Journal:写数据前等待Metadata和数据的日志落盘。Ordered:只记录Metadata的日志,写Metadata日志前确保数据已经落盘。Writeback:仅记录Metadata日志,不保证数据比Metadata先落盘。我们选择了Writeback模式(默认是oderded),它在几种挂载模式中速度最快,缺点是:发生故障时数据不好恢复。我们大部分容器处于无状态,故障时在别的机器上再拉起一台即可。因此我们在性能和稳定性中,选择了性能。容器内部给应用提供可选的基于内存的文件系统tmpfs,可以提升有大量临时文件读写的服务性能。如上图所示,在美团内部创建一个虚拟机至少经历三步,平均时间超过300秒。使用镜像创建容器平均时间23秒。容器的灵活、快速得到了显著的体现。容器扩容23秒的平均时间包含了各个部分的优化,如扩容链路优化、镜像分发优化、初始化和业务拉起优化等等。接下来,本文主要介绍一下我们做的镜像分发和解压相关的优化。上图是美团容器镜像管理的总体架构,其特点如下:存在多个Site。支持跨Site的镜像同步,根据镜像的标签确定是否需要跨Site同步。每个Site有镜像备份。每个Site内部有实现镜像分发的P2P网络。镜像分发是影响容器扩容时长的一个重要环节。跨Site同步:保证服务器总能从就近的镜像仓库拉取到扩容用的镜像,减少拉取时间,降低跨Site带宽消耗。基础镜像预分发:美团的基础镜像是构建业务镜像的公共镜像,通常有几百兆的大小。业务镜像层是业务的应用代码,通常比基础镜像小很多。在容器扩容的时候如果基础镜像已经在本地,就只需要拉取业务镜像的部分,可以明显的加快扩容速度。为达到这样的效果,我们会把基础镜像事先分发到所有的服务器上。P2P镜像分发:基础镜像预分发在有些场景会导致上千个服务器同时从镜像仓库拉取镜像,对镜像仓库服务和带宽带来很大的压力。因此我们开发了镜像P2P分发的功能,服务器不仅能从镜像仓库中拉取镜像,还能从其他服务器上获取镜像的分片。从上图可以看出,随着分发服务器数目的增加,原有分发时间也快速增加,而P2P镜像分发时间基本上保持稳定。Docker的镜像拉取是一个并行下载,串行解压的过程,为了提升解压的速度,我们美团也做了一些优化工作。对于单个层的解压,我们使用并行解压算法替换Docker默认的串行解压算法,实现上是使用pgzip替换gzip。Docker的镜像具有分层结构,对镜像层的合并是一个“解压一层合并一层,再解压一层,再合并一层”的串行操作。实际上只有合并是需要串行的,解压可以并行起来。我们把多层的解压改成并行,解压出的数据先放在临时存储空间,最后根据层之间的依赖进行串行合并。前面的改动(并行解压所有的层到临时空间)导致磁盘IO的次数增加了近一倍,也会导致解压过程不够快。于是,我们使用基于内存的Ramdisk来存储解压出来的临时文件,减轻了额外文件写带来的开销。做了上面这些工作以后,我们又发现,容器的分层也会影响下载加解压的时间。上图是我们简单测试的结果:无论对于怎么分层的镜像并行解压,都能大幅提升解压时间,对于层数多的镜像提升更加明显。推广推广容器的第一步是能说出容器的优势,我们认为容器有如下优势:轻量级:容器小、快,能够实现秒级启动。应用分发:容器使用镜像分发,开发测试容器和部署容器配置完全一致。弹性:可以根据CPU、内存等资源使用或者QPS、延时等业务指标快速扩容容器,提升服务能力。这三个特性的组合,可以给业务带来更大的灵活度和更低的计算成本。因为容器平台本身是一个技术产品,它的客户是各个业务的RD团队,因此我们需要考虑下面一些因素:产品优势:推广容器平台从某种程度上讲,自身是一个ToB的业务,首先要有好的产品,它相对于以前的解决方案(虚拟机)存在很多优势。和已有系统打通:这个产品要能和客户现有的系统很好的进行集成,而不是让客户推翻所有的系统重新再来。原生应用的开发平台、工具:这个产品要易于使用,要有配合工作的工具链。虚拟机到容器的平滑迁移:最好能提供从原有方案到新产品的迁移方案,并且容易实施。与应用RD紧密配合:要提供良好的客户支持,(即使有些问题不是这个产品导致的也要积极帮忙解决)。资源倾斜:从战略层面支持颠覆性新技术:资源上向容器平台倾斜,没有足够的理由,尽量不给配置虚拟机资源。总结Docker容器加Kubernetes编排是当前容器云的主流实践之一,美团容器集群管理平台HULK也采用了这样的方案。本文主要分享了美团在容器技术上做的一些探索和实践。内容主要涵盖美团容器云在Linux Kernel、Docker和Kubernetes层面做的一些优化工作,以及美团内部推动容器化进程的一些思考,欢迎大家跟我们交流、探讨。作者简介欧阳坚,2006年毕业于清华大学计算机系,拥有12年数据中心开发管理经验。曾任VMware中国Staff Engineer,无双科技CTO,中科睿光首席架构师。现任美团基础架构部/容器研发中心技术总监,负责美团容器化的相关工作。招聘信息美团点评基础架构团队诚招Java高级、资深技术专家,Base北京、上海。我们是集团致力于研发公司级、业界领先基础架构组件的核心团队,涵盖分布式监控、服务治理、高性能通信、消息中间件、基础存储、容器化、集群调度等技术领域。欢迎有兴趣的同学投送简历到 liuxing14@meituan.com。

November 16, 2018 · 1 min · jiezi

美团点评携手 PingCAP 开启新一代数据库深度实践之旅(9000 字长文 / 真实“踩坑”经历)

一、背景和现状在美团,基于 MySQL 构建的传统关系型数据库服务已经难于支撑公司业务的爆发式增长,促使我们去探索更合理的数据存储方案和实践新的运维方式。随着近一两年来分布式数据库大放异彩,美团 DBA 团队联合架构存储团队,于 2018 年初启动了分布式数据库项目。图 1 美团点评产品展示图立项之初,我们进行了大量解决方案的对比,深入了解了业界多种 scale-out、scale-up 方案,考虑到技术架构的前瞻性、发展潜力、社区活跃度、以及服务本身与 MySQL 的兼容性,最终敲定了基于 TiDB 数据库进行二次开发的整体方案,并与 PingCAP 官方和开源社区进行深入合作的开发模式。美团业务线众多,我们根据业务特点及重要程度逐步推进上线,到截稿为止,已经上线 10 个集群,近 200 个物理节点,大部分是 OLTP 类型的应用,除了上线初期遇到了一些小问题,目前均已稳定运行。初期上线的集群,已经分别服务于配送、出行、闪付、酒旅等业务。TiDB 架构分层清晰,服务平稳流畅,但在美团当前的数据量规模和已有稳定的存储体系的基础上,推广新的存储服务体系,需要对周边工具和系统进行一系列改造和适配,从初期探索到整合落地需要走很远的路。下面从几个方面分别介绍:一是从 0 到 1 的突破,重点考虑做哪些事情;二是如何规划实施不同业务场景的接入和已有业务的迁移;三是上线后遇到的一些典型问题介绍;四是后续规划和对未来的展望。二、前期调研测试2.1 对 TiDB 的定位我们对于 TiDB 的定位,前期在于重点解决 MySQL 的单机性能和容量无法线性和灵活扩展的问题,与 MySQL 形成互补。业界分布式方案很多,我们为何选择了 TiDB 呢?考虑到公司业务规模的快速增长,以及公司内关系数据库以 MySQL 为主的现状,因此我们在调研阶段,对以下技术特性进行了重点考虑:协议兼容 MySQL:这个是必要项。可在线扩展:数据通常要有分片,分片要支持分裂和自动迁移,并且迁移过程要尽量对业务无感知。强一致的分布式事务:事务可以跨分片、跨节点执行,并且强一致。支持二级索引:为兼容 MySQL 的业务,这个是必须的。性能:MySQL 的业务特性,高并发的 OLTP 性能必须满足。跨机房服务:需要保证任何一个机房宕机,服务能自动切换。跨机房双写:支持跨机房双写是数据库领域一大难题,是我们对分布式数据库的一个重要期待,也是美团下一阶段重要的需求。业界的一些传统方案虽然支持分片,但无法自动分裂、迁移,不支持分布式事务,还有一些在传统 MySQL 上开发一致性协议的方案,但它无法实现线性扩展,最终我们选择了与我们的需求最为接近的 TiDB。与 MySQL 语法和特性高度兼容,具有灵活的在线扩容缩容特性,支持 ACID 的强一致性事务,可以跨机房部署实现跨机房容灾,支持多节点写入,对业务又能像单机 MySQL 一样使用。2.2 测试针对官方声称的以上优点,我们进行了大量的研究、测试和验证。首先,我们需要知道扩容、Region 分裂转移的细节、Schema 到 kv 的映射、分布式事务的实现原理。而 TiDB 的方案,参考了较多的 Google 论文,我们进行了阅读,这有助于我们理解 TiDB 的存储结构、事务算法、安全性等,包括:Spanner: Google’s Globally-Distributed DatabaseLarge-scale Incremental Processing Using Distributed Transactions and NotificationsIn Search of an Understandable Consensus AlgorithmOnline, Asynchronous Schema Change in F1我们也进行了常规的性能和功能测试,用来与 MySQL 的指标进行对比,其中一个比较特别的测试,是证明 3 副本跨机房部署,确实能保证每个机房分布一个副本,从而保证任何一个机房宕机不会导致丢失超过半数副本。从以下几个点进行测试:Raft 扩容时是否支持 learner 节点,从而保证单机房宕机不会丢失 2/3 的副本。TiKV 上的标签优先级是否可靠,保证当机房的机器不平均时,能否保证每个机房的副本数依然是绝对平均的。实际测试,单机房宕机,TiDB 在高并发下,QPS、响应时间、报错数量,以及最终数据是否有丢失。手动 Balance 一个 Region 到其他机房,是否会自动回来。从测试结果来看,一切都符合预期。三、存储生态建设美团的产品线丰富,业务体量大,业务对在线存储的服务质量要求也非常高。因此,从早期做好服务体系的规划非常重要。下面从业务接入层、监控报警、服务部署,来分别介绍一下我们所做的工作。3.1 业务接入层当前 MySQL 的业务接入方式主要有两种,DNS 接入和 Zebra 客户端接入。在前期调研阶段,我们选择了 DNS + 负载均衡组件的接入方式,TiDB-Server 节点宕机,15s 可以被负载均衡识别到,简单有效。业务架构如图 2。图 2 业务架构图后面我们会逐渐过渡到当前大量使用的 Zebra 接入方式来访问 TiDB,从而保持与访问 MySQL 的方式一致,一方面减少业务改造的成本,另一方面尽量实现从 MySQL 到 TiDB 的透明迁移。3.2 监控报警美团目前使用 Mt-Falcon 平台负责监控报警,通过在 Mt-Falcon 上配置不同的插件,可以实现对多种组件的自定义监控。另外也会结合 Puppet 识别不同用户的权限、文件的下发。这样,只要我们编写好插件脚本、需要的文件,装机和权限控制就可以完成了。监控架构如图 3。图 3 监控架构图而 TiDB 有丰富的监控指标,使用流行的 Prometheus + Grafana,一套集群有 700+ 的 Metric。从官方的架构图可以看出,每个组件会推送自己的 Metric 给 PushGateWay,Prometheus 会直接到 PushGateWay 去抓数据。由于我们需要组件收敛,原生的 TiDB 每个集群一套 Prometheus 的方式不利于监控的汇总、分析、配置,而报警已经在 Mt-Falcon 上实现的比较好了,在 AlertManager 上再造一个也没有必要。因此我们需要想办法把监控和报警汇总到 Mt-Falcon 上面,有如下几种方式:方案一:修改源代码,将 Metric 直接推送到 Falcon,由于 Metric 散落在代码的不同位置,而且 TiDB 代码迭代太快,把精力消耗在不停调整监控埋点上不太合适。方案二:在 PushGateWay 是汇总后的,可以直接抓取,但 PushGateWay 是个单点,不好维护。方案三:通过各个组件(TiDB、PD、TiKV)的本地 API 直接抓取,优点是组件宕机不会影响其他组件,实现也比较简单。我们最终选择了方案三。该方案的难点是需要把 Prometheus 的数据格式转化为 Mt-Falcon 可识别的格式,因为 Prometheus 支持 Counter、Gauge、Histogram、Summary 四种数据类型,而 Mt-Falcon 只支持基本的 Counter 和 Gauge,同时 Mt-Falcon 的计算表达式比较少,因此需要在监控脚本中进行转换和计算。3.3 批量部署TiDB 使用 Ansible 实现自动化部署。迭代快,是 TiDB 的一个特点,有问题快速解决,但也造成 Ansible 工程、TiDB 版本更新过快,我们对 Ansible 的改动,也只会增加新的代码,不会改动已有的代码。因此线上可能同时需要部署、维护多个版本的集群。如果每个集群一个 Ansible 目录,造成空间的浪费。我们采用的维护方式是,在中控机中,每个版本一个 Ansible 目录,每个版本中通过不同 inventory 文件来维护。这里需要跟 PingCAP 提出的是,Ansible 只考虑了单集群部署,大量部署会有些麻烦,像一些依赖的配置文件,都不能根据集群单独配置(咨询官方得知,PingCAP 目前正在基于 Cloud TiDB 打造一站式 HTAP 平台,会提供批量部署、多租户等功能,能比较好的解决这个问题)。3.4 自动化运维平台随着线上集群数量的增加,打造运维平台提上了日程,而美团对 TiDB 和 MySQL 的使用方式基本相同,因此 MySQL 平台上具有的大部分组件,TiDB 平台也需要建设。典型的底层组件和方案:SQL 审核模块、DTS、数据备份方案等。自动化运维平台展示如图 4。3.5 上下游异构数据同步TiDB 是在线存储体系中的一环,它同时也需要融入到公司现有的数据流中,因此需要一些工具来做衔接。PingCAP 官方标配了相关的组件。公司目前 MySQL 和 Hive 结合的比较重,而 TiDB 要代替 MySQL 的部分功能,需要解决 2 个问题:MySQL to TiDBMySQL 到 TiDB 的迁移,需要解决数据迁移以及增量的实时同步,也就是 DTS,Mydumper + Loader 解决存量数据的同步,官方提供了 DM 工具可以很好的解决增量同步问题。MySQL 大量使用了自增 ID 作为主键。分库分表 MySQL 合并到 TiDB 时,需要解决自增 ID 冲突的问题。这个通过在 TiDB 端去掉自增 ID 建立自己的唯一主键来解决。新版 DM 也提供分表合并过程主键自动处理的功能。Hive to TiDB & TiDB to HiveHive to TiDB 比较好解决,这体现了 TiDB 和 MySQL 高度兼容的好处,insert 语句可以不用调整,基于 Hive to MySQL 简单改造即可。TiDB to Hive 则需要基于官方 Pump + Drainer 组件,Drainer 可以消费到 Kafka、MySQL、TiDB,我们初步考虑用下图 5 中的方案通过使用 Drainer 的 Kafka 输出模式同步到 Hive。图 5 TiDB to Hive 方案图四、线上使用磨合对于初期上线的业务,我们比较谨慎,基本的原则是:离线业务 -> 非核心业务 -> 核心业务。TiDB 已经发布两年多,且前期经历了大量的测试,我们也深入了解了其它公司的测试和使用情况,可以预期的是 TiDB 上线会比较稳定,但依然遇到了一些小问题。总体来看,在安全性、数据一致性等关键点上没有出现问题。其他一些性能抖动问题,参数调优的问题,也都得到了快速妥善的解决。这里给 PingCAP 的同学点个大大的赞,问题响应速度非常快,与我们内部研发的合作也非常融洽。4.1 写入量大、读 QPS 高的离线业务我们上线的最大的一个业务,每天有数百 G 的写入量,前期遇到了较多的问题,我们重点说说。业务场景:稳定的写入,每个事务操作 100~200 行不等,每秒 6w 的数据写入。每天的写入量超过 500G,以后会逐步提量到每天 3T。每 15 分钟的定时读 job,5000 QPS(高频量小)。不定时的查询(低频量大)。之前使用 MySQL 作为存储,但 MySQL 到达了容量和性能瓶颈,而业务的容量未来会 10 倍的增长。初期调研测试了 ClickHouse,满足了容量的需求,测试发现运行低频 SQL 没有问题,但高频 SQL 的大并发查询无法满足需求,只在 ClickHouse 跑全量的低频 SQL 又会 overkill,最终选择使用 TiDB。测试期间模拟写入了一天的真实数据,非常稳定,高频低频两种查询也都满足需求,定向优化后 OLAP 的 SQL 比 MySQL 性能提高四倍。但上线后,陆续发现了一些问题,典型的如下:4.1.1 TiKV 发生 Write StallTiKV 底层有 2 个 RocksDB 作为存储。新写的数据写入 L0 层,当 RocksDB 的 L0 层数量达到一定数量,就会发生减速,更高则发生 Stall,用来自我保护。TiKV 的默认配置:level0-slowdown-writes-trigger = 20level0-stop-writes-trigger = 36遇到过的,发生 L0 文件过多可能的原因有 2 个:写入量大,Compact 完不成。Snapshot 一直创建不完,导致堆积的副本一下释放,rocksdb-raft 创建大量的 L0 文件,监控展示如图 6。图 6 TiKV 发生 Write Stall 监控展示图我们通过以下措施,解决了 Write Stall 的问题:减缓 Raft Log Compact 频率(增大 raft-log-gc-size-limit、raft-log-gc-count-limit)加快 Snapshot 速度(整体性能、包括硬件性能)max-sub-compactions 调整为 3max-background-jobs 调整为 12level 0 的 3 个 Trigger 调整为 16、32、644.1.2 Delete 大量数据,GC 跟不上现在 TiDB 的 GC 对于每个 kv-instance 是单线程的,当业务删除数据的量非常大时,会导致 GC 速度较慢,很可能 GC 的速度跟不上写入。目前可以通过增多 TiKV 个数来解决,长期需要靠 GC 改为多线程执行,官方对此已经实现,即将发布。4.1.3 Insert 响应时间越来越慢业务上线初期,insert 的响应时间 80 线(Duration 80 By Instance)在 20ms 左右,随着运行时间增加,发现响应时间逐步增加到 200ms+。期间排查了多种可能原因,定位在由于 Region 数量快速上涨,Raftstore 里面要做的事情变多了,而它又是单线程工作,每个 Region 定期都要 heartbeat,带来了性能消耗。tikv-raft propose wait duration 指标持续增长。解决问题的办法:临时解决增加 Heartbeat 的周期,从 1s 改为 2s,效果比较明显,监控展示如图 7。图 7 insert 响应时间优化前后对比图彻底解决需要减少 Region 个数,Merge 掉空 Region,官方在 2.1 版本中已经实现了 Region Merge 功能,我们在升级到 2.1 后,得到了彻底解决。另外,等待 Raftstore 改为多线程,能进一步优化。(官方回复相关开发已基本接近尾声,将于 2.1 的下一个版本发布。)4.1.4 Truncate Table 空间无法完全回收DBA Truncate 一张大表后,发现 2 个现象,一是空间回收较慢,二是最终也没有完全回收。由于底层 RocksDB 的机制,很多数据落在 level 6 上,有可能清不掉。这个需要打开 cdynamic-level-bytes 会优化 Compaction 的策略,提高 Compact 回收空间的速度。由于 Truncate 使用 delete_files_in_range 接口,发给 TiKV 去删 SST 文件,这里只删除不相交的部分,而之前判断是否相交的粒度是 Region,因此导致了大量 SST 无法及时删除掉。考虑 Region 独立 SST 可以解决交叉问题,但是随之带来的是磁盘占用问题和 Split 延时问题。考虑使用 RocksDB 的 DeleteRange 接口,但需要等该接口稳定。目前最新的 2.1 版本优化为直接使用 DeleteFilesInRange 接口删除整个表占用的空间,然后清理少量残留数据,已经解决。4.1.5 开启 Region Merge 功能为了解决 region 过多的问题,我们在升级 2.1 版本后,开启了 region merge 功能,但是 TiDB 的响应时间 80 线(Duration 80 By Instance)依然没有恢复到当初,保持在 50ms 左右,排查发现 KV 层返回的响应时间还很快,和最初接近,那么就定位了问题出现在 TiDB 层。研发人员和 PingCAP 定位在产生执行计划时行为和 2.0 版本不一致了,目前已经优化。4.2 在线 OLTP,对响应时间敏感的业务除了分析查询量大的离线业务场景,美团还有很多分库分表的场景,虽然业界有很多分库分表的方案,解决了单机性能、存储瓶颈,但是对于业务还是有些不友好的地方:业务无法友好的执行分布式事务。跨库的查询,需要在中间层上组合,是比较重的方案。单库如果容量不足,需要再次拆分,无论怎样做,都很痛苦。业务需要关注数据分布的规则,即使用了中间层,业务心里还是没底。因此很多分库分表的业务,以及即将无法在单机承载而正在设计分库分表方案的业务,主动找到了我们,这和我们对于 TiDB 的定位是相符的。这些业务的特点是 SQL 语句小而频繁,对一致性要求高,通常部分数据有时间属性。在测试及上线后也遇到了一些问题,不过目前基本都有了解决办法。4.2.1 SQL 执行超时后,JDBC 报错业务偶尔报出 privilege check fail。是由于业务在 JDBC 设置了 QueryTimeout,SQL 运行超过这个时间,会发行一个 “kill query” 命令,而 TiDB 执行这个命令需要 Super 权限,业务是没有权限的。其实 kill 自己的查询,并不需要额外的权限,目前已经解决了这个问题,不再需要 Super 权限,已在 2.0.5 上线。4.2.2 执行计划偶尔不准TiDB 的物理优化阶段需要依靠统计信息。在 2.0 版本统计信息的收集从手动执行,优化为在达到一定条件时可以自动触发:数据修改比例达到 tidb_auto_analyze_ratio表一分钟没有变更(目前版本已经去掉这个条件)但是在没有达到这些条件之前统计信息是不准的,这样就会导致物理优化出现偏差,在测试阶段(2.0 版本)就出现了这样一个案例:业务数据是有时间属性的,业务的查询有 2 个条件,比如:时间+商家 ID,但每天上午统计信息可能不准,当天的数据已经有了,但统计信息认为没有。这时优化器就会建议使用时间列的索引,但实际上商家 ID 列的索引更优化。这个问题可以通过增加 Hint 解决。在 2.1 版本对统计信息和执行计划的计算做了大量的优化,也稳定了基于 Query Feedback 更新统计信息,也用于更新直方图和 Count-Min Sketch,非常期待 2.1 的 GA。五、总结展望经过前期的测试、各方的沟通协调,以及近半年对 TiDB 的使用,我们看好 TiDB 的发展,也对未来基于 TiDB 的合作充满信心。接下来,我们会加速推进 TiDB 在更多业务系统中的使用,同时也将 TiDB 纳入了美团新一代数据库的战略选型中。当前,我们已经全职投入了 3 位 DBA 同学和多位存储计算专家,从底层的存储,中间层的计算,业务层的接入,到存储方案的选型和布道,进行全方位和更深入的合作。长期来看,结合美团不断增长的业务规模,我们将与 PingCAP 官方合作打造更强大的生态体系:Titan:Titan 是 TiDB 下一步比较大的动作,也是我们非常期待的下一代存储引擎,它对大 Value 支持会更友好,将解决我们单行大小受限,单机 TiKV 最大支持存储容量的问题,大大提升大规模部署的性价比。Cloud TiDB(based on Docker & K8s):云计算大势所趋,PingCAP 在这块也布局比较早,今年 8 月份开源了 TiDB Operator,Cloud TiDB 不仅实现了数据库的高度自动化运维,而且基于 Docker 硬件隔离,实现了数据库比较完美的多租户架构。和官方同学沟通,目前他们的私有云方案在国内也有重要体量的 POC,这也是美团看重的一个方向。TiDB HTAP Platform:PingCAP 在原有 TiDB Server 计算引擎的基础上,还构建 TiSpark 计算引擎,和他们官方沟通,他们在研发了一个基于列的存储引擎,这样就形成了下层行、列两个存储引擎、上层两个计算引擎的完整混合数据库(HTAP),这个架构不仅大大的节省了核心业务数据在整个公司业务周期里的副本数量,还通过收敛技术栈,节省了大量的人力成本、技术成本、机器成本,同时还解决了困扰多年的 OLAP 的实效性。后面我们也会考虑将一些有实时、准实时的分析查询系统接入 TiDB。图 8 TiDB HTAP Platform 整体架构图后续的物理备份方案,跨机房多写等也是我们接下来逐步推进的场景,总之我们坚信未来 TiDB 在美团的使用场景会越来越多,发展也会越来越好。TiDB 在业务层面、技术合作层面都已经在美团扬帆起航,美团点评将携手 PingCAP 开启新一代数据库深度实践、探索之旅。后续,还有美团点评架构存储团队针对 TiDB 源码研究和改进的系列文章,敬请期待!作者介绍赵应钢,美团点评研究员李坤,美团点评数据库专家朴昌俊,美团点评数据库专家 ...

November 15, 2018 · 4 min · jiezi

【备战春招/秋招系列】美团面经总结基础篇 (附详解答案)

该文已加入开源文档:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识)。地址:https://github.com/Snailclimb…【强烈推荐!非广告!】阿里云双11褥羊毛活动:https://m.aliyun.com/act/team… 差不多一折,不过仅限阿里云新人购买,不是新人的朋友自己找方法买哦!系列文章:【备战春招/秋招系列1】程序员的简历就该这样写【备战春招/秋招系列2】初出茅庐的程序员该如何准备面试?【备战春招/秋招系列3】Java程序员必备书单这是我总结的美团面经的基础篇,后面还有进阶和终结篇哦!下面只是我从很多份美团面经中总结的在面试中一些常见的问题。不同于个人面经,这份面经具有普适性。每次面试必备的自我介绍、项目介绍这些东西,大家可以自己私下好好思考。我在前面的文章中也提到了应该怎么做自我介绍与项目介绍,详情可以查看这篇文章:【备战春招/秋招系列2】初出茅庐的程序员该如何准备面试?。1. System.out.println(3 | 9);输出什么?正确答案:11.考察知识点:逻辑运算符与(&和&&)或(|和||)&和&&:共同点:它们都表示运算符的两边都是true时,结果为true;不同点: & 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西,然后判断是否为true,是true就继续运算右边的然后判断并输出,是false就停下来直接输出不会再运行后面的东西。|和||:共同点:它们都表示运算符的两边任意一边为true,结果为true,两边都不是true,结果就为false;不同点:| 表示两边都会运算,然后再判断结果;|| 表示先运算符号左边的东西,然后判断是否为true,是true就停下来直接输出不会再运行后面的东西,是false就继续运算右边的然后判断并输出。回到本题:3 | 9=0011(二进制) | 1001(二进制)=1011(二进制)=11(十进制)2. 说一下转发(Forward)和重定向(Redirect)的区别转发是服务器行为,重定向是客户端行为。转发(Forword) 通过RequestDispatcher对象的forward(HttpServletRequest request,HttpServletResponse response)方法实现的。RequestDispatcher 可以通过HttpServletRequest 的 getRequestDispatcher()方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。request.getRequestDispatcher(“login_success.jsp”).forward(request, response);重定向(Redirect) 是利用服务器返回的状态吗来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过HttpServletRequestResponse的setStatus(int status)方法设置状态码。如果服务器返回301或者302,则浏览器会到新的网址重新请求该资源。从地址栏显示来说: forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址. redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL.从数据共享来说: forward:转发页面和转发到的页面可以共享request里面的数据. redirect:不能共享数据.从运用地方来说: forward:一般用于用户登陆的时候,根据角色转发到相应的模块. redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等从效率来说: forward:高. redirect:低.3. 在浏览器中输入url地址 ->> 显示主页的过程,整个过程会使用哪些协议图解(图片来源:《图解HTTP》):总体来说分为以下几个过程:DNS解析TCP连接发送HTTP请求服务器处理请求并返回HTTP报文浏览器解析渲染页面连接结束具体可以参考下面这篇文章:https://segmentfault.com/a/11900000068797004. TCP 三次握手和四次挥手为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。漫画图解:图片来源:《图解HTTP》简单示意图:客户端–发送带有 SYN 标志的数据包–一次握手–服务端服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端为什么要三次握手三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常。第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送接收正常所以三次握手就能确认双发收发功能都正常,缺一不可。为什么要传回 SYN接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。传了 SYN,为啥还要传 ACK双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方(主动关闭方)到接收方(被动关闭方)的通道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。断开一个 TCP 连接则需要“四次挥手”:客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号服务器-关闭与客户端的连接,发送一个FIN给客户端客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1为什么要四次挥手任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。上面讲的比较概括,推荐一篇讲的比较细致的文章:https://blog.csdn.net/qzcsu/article/details/728618915. IP地址与MAC地址的区别参考:https://blog.csdn.net/guoweimelon/article/details/50858597IP地址是指互联网协议地址(Internet Protocol Address)IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。MAC 地址又称为物理地址、硬件地址,用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家写入网卡的,具有全球唯一性。MAC地址用于在网络中唯一标示一个网卡,一台电脑会有一或多个网卡,每个网卡都需要有一个唯一的MAC地址。6. HTTP请求、响应报文格式HTTP请求报文主要由请求行、请求头部、请求正文3部分组成HTTP响应报文主要由状态行、响应头部、响应正文3部分组成详细内容可以参考:https://blog.csdn.net/a19881029/article/details/140022737. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql索引主要使用的两种数据结构?什么是覆盖索引?为什么要使用索引?通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。帮助服务器避免排序和临时表将随机IO变为顺序IO可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。索引这么多优点,为什么不对表中的每一个列创建一个索引呢?当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。索引是如何提高查询速度的?将无序的数据变成相对有序的数据(就像查目录一样)说一下使用索引的注意事项避免 where 子句中对宇段施加函数,这会造成无法命中索引。在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 chema_unused_indexes 视图来查询哪些索引从未被使用在使用 limit offset 查询缓慢时,可以借助索引来提高性能Mysql索引主要使用的哪两种数据结构?哈希索引:对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。BTree索引:Mysql的BTree索引使用的是B树中的B+Tree。但对于主要的两种存储引擎(MyISAM和InnoDB)的实现方式是不同的。更多关于索引的内容可以查看我的这篇文章:【思维导图-索引篇】搞定数据库索引就是这么简单什么是覆盖索引?如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道在InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不?进程与线程的区别是什么?线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。另外,也正是因为共享资源,所以线程中执行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程间的几种通信方式说一下?管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为pipe(无名管道)和fifo(命名管道)两种,有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。套接字(socket):套接口也是一种进程间的通信机制,与其他通信机制不同的是它可以用于不同及其间的进程通信。线程间的几种通信方式知道不?1、锁机制互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。读写锁:允许多个线程同时读共享数据,而对写操作互斥。条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。2、信号量机制:包括无名线程信号量与有名线程信号量3、信号机制:类似于进程间的信号处理。线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。9. 为什么要用单例模式?手写几种线程安全的单例模式?简单来说使用单例模式可以带来下面几个好处:对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。懒汉式(双重检查加锁版本)public class Singleton { //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量 private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getInstance() { //检查实例,如果不存在,就进入同步代码块 if (uniqueInstance == null) { //只有第一次才彻底执行这里的代码 synchronized(Singleton.class) { //进入同步代码块后,再检查一次,如果仍是null,才创建实例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }}静态内部类方式静态内部实现的单例是懒加载的且线程安全。只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } } 10. 简单介绍一下bean。知道Spring的bean的作用域与生命周期吗?在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。Spring中的bean默认都是单例的,这些单例Bean在多线程程序下如何保证线程安全呢? 例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框架之后,每个Action都是单例的,那么对于Spring托管的单例Service Bean,如何保证其安全呢? Spring的单例是基于BeanFactory也就是Spring容器的,单例Bean在此容器内只有一个,Java的单例是基于 JVM,每个 JVM 内只有一个实例。Spring的bean的生命周期以及更多内容可以查看:一文轻松搞懂Spring中bean的作用域与生命周期11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量?事务传播行为事务传播行为(为了解决业务层方法之间互相调用的事务问题):当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:支持当前事务的情况:TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)不支持当前事务的情况:TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。其他情况:TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。隔离级别TransactionDefinition 接口中定义了五个表示隔离级别的常量:TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。12. SpringMVC 原理了解吗?客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Moder)->将得到视图对象返回给用户关于 SpringMVC 原理更多内容可以查看我的这篇文章:SpringMVC 工作原理详解13. Spring AOP IOC 实现原理过了秋招挺长一段时间了,说实话我自己也忘了如何简要概括 Spring AOP IOC 实现原理,就在网上找了一个较为简洁的答案,下面分享给各位。IOC: 控制反转也叫依赖注入。IOC利用java反射机制,AOP利用代理模式。IOC 概念看似很抽象,但是很容易理解。说简单点就是将对象交给容器管理,你只需要在spring配置文件中配置对应的bean以及设置相关的属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类。AOP: 面向切面编程。(Aspect-Oriented Programming) 。AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理。你若盛开,清风自来。 欢迎关注我的微信公众号:“Java面试通关手册”,一个有温度的微信公众号。公众号后台回复关键字“1”,可以免费获取一份我精心准备的小礼物哦! ...

November 11, 2018 · 2 min · jiezi

【人物志】美团首席科学家夏华夏:不断突破边界的程序人生

“成长没有什么秘笈,就是坚持不断地一点点突破自己的边界就好。”这是美团首席科学家、无人配送部总经理夏华夏在刚刚过去的“1024 程序员节”时送给技术同行的一句话。这也是夏华夏自己的人生写照:从没摸过计算机的山东高考状元到清华计算机系的学霸,从美国名校深造、Google修炼6年到选择回国,从加入当时还很小的美团到负责公司最大业务的总体架构,从架构师转为无人配送这个前沿业务部门的管理者,夏华夏就是在不断突破自己的边界,做出人生的重要抉择,脚踏实地,一步步成长为业界知名的技术领军人物。本文由美团技术学院基于夏华夏的访谈记录整理而成。1998年,夏华夏从清华大学远赴美国留学,先后在谷歌、百度担任架构师。2013年受美团创始人王兴和穆荣均的邀请,选择加入当时还很“弱小”的美团,夏华夏回忆,是王兴的一番话让他备受触动。当时,望着五道口下面熙熙攘攘的人流,王兴说:“其实很多做互联网创业的人,很少考虑怎么去帮助这些人,这些普罗大众,这么多的小商家。其实,他们都在努力改变自己的命运,我们美团就要帮助他们,帮助这些普普通通的老百姓。这也是属于我们的机会。”夏华夏加入美团已有5年的时间,也是一名地地道道的美团“老人”了。他当时并没有料到,如今这家小小的创业公司,已经成功在香港上市,成为市值仅次于BAT的第四大互联网企业;他当时也没有想到,这个曾经很小的技术团队已经迅速成长为一个拥有万人规模的一流研发组织;他也没有料到,5年后这家公司还会在人工智能、无人驾驶这些前沿科技领域不断开疆拓土,他也成为了无人配送项目的负责人。2013年,美团的主要业务还是团购,夏华夏帮助整个技术团队做了很多技术层面的梳理和重构,并和早期的几位技术团队负责人一起组建了美团技术学院。2015年,经过前期的孵化和运营,外卖业务已经初具规模,但由于基础系统建设不够牢固,导致当时的外卖系统很不稳定,每周要宕机好几次,而且好几次宕机都是发生在用餐高峰时期。临危受命,夏华夏再次被调任到外卖业务部门,帮助外卖技术团队解决了系统稳定性的问题。顶着巨大的业务压力,夏华夏和美团外卖很多技术骨干天天泡在一个被他们称之为“作战室”的大会议室里,经过了近百个不眠不休的夜晚,不断迭代升级、测试、监控整个系统。终于将系统稳定性就从98%提高到了99.9%,从而保障了外卖业务的飞速增长。美团能够成功上市,外卖团队功不可没。2017年年底,夏华夏再次踏上了新的技术征程,他陆续交接了手中其他的工作,将全面精力放在美团无人配送项目上,并带领技术团队研发了专属于美团自己的无人车和无人机。目前夏华夏负责的美团无人配送开放平台,集合了政府、高校、企业三方力量,已经吸引包括清华大学、加州伯克利大学、北京智能车联产业创新中心、华夏幸福、Segway等近20家国内国外合作伙伴加入。大学篇:千里之行,始于足下对神秘事物充满好奇,误打误撞报考清华计算机专业1993年6月,夏华夏面临人生的第一次选择——高考。“当时在小县城,几乎没有人会对专业有概念,大家只在意你考上哪所大学。”对年轻的夏华夏来说,报专业纯属瞎报,当时是看哪个专业的名字有趣就选择一个,而选择计算机是完全是“蒙的”,要知道在上世纪90年代的小县城,大家都没有见过计算机,最多只是听过这个概念。“我觉得,计算机是一个非常神秘的东西,越是神秘的东西就越有意思,对我的吸引力也更大。”夏华夏说,当时他的班主任也不知道哪个系好,在班主任看来,有学生能考上清华大学,“KPI”就完成了。所幸在高考中,夏华夏发挥出色,以山东省第一名的成绩顺利考入清华大学。进入清华实验班,系院士领入计算机世界的大门清华有一个实验班,入学摸底考试后,从电子类相关的专业选择了大概五十个同学有资格进入,夏华夏名列前茅。“我们很幸运,因为实验班给我们创造了很好的条件,而且可以直接向系里云集的院士泰斗(张钹、李三立、唐泽圣等老师)请益。”不过,最开始夏华夏都不知道应该请教什么问题,因为这是他第一次跟计算机世界打交道。现在回忆起第一次上级课的情景,夏华夏记忆犹新,当时电脑配置的是386的处理器,找了老半天都不知道怎么开机,怎么进入系统。90年代,几乎所有学编程的人都是从C语言开始。这门看起来有些古老的语言,一度耗费了夏华夏几乎全部的精力。“那个时候还不会双打,用两个指头打字,所以编程学习特别慢,现在已经很难想象那种情景了”。人生第一个应用程序,花了夏华夏整整一节课的时间,他记得很清楚,那是一个画图的程序,编译之后也没问题,但是后续检查中,发现目录里有两个没有见过的文件,“.”和“..”(DOS操作系统下的当前目录和父目录),貌似感染了“病毒”,吓得赶紧执行了一个“deltree .”的命令,试图把“病毒”删除,然后就发现整个程序都找不到了。所以夏华夏的第一个程序,其实是一个失败项目。虽然最开始很窘迫,但一个全新的世界已经向这个懵懂的少年敞开了大门。小试牛刀,做数据库系统挣到人生的第一桶金当然,对刚刚接触编程世界的夏华夏来说,每个项目都会倾注很多的精力去完成,而且实验室学习气氛很好,所有同学都会把项目看得非常重要。现在,机器学习、图像识别、人工智能的概念几乎已经众人皆知了,当时还没有这么火爆,其实夏华夏很早就参与了一个人工智能相关的项目,严格来说属于图像识别范畴,项目要求识别一张图上的飞鸟的总数。这是一个难度不小的挑战,夏华夏使用了很多模型进行实践,当时也没有“模式识别”方法可以使用,后来他通过应用算法把“鸟”的边界线数出来,然后逐渐对周围进行“腐蚀”,“腐蚀”到最小的一个点,就认为这是一个“鸟”。夏华夏一直都相信,只要勤思考,肯定能找到解决问题的办法。在那个年代,人工智能正在经历又一次的低谷期,绝大多数保送的研究生都不愿意去人工智能实验室。“其实很多时候,我们对某些事的认知,确实是受制于环境因素的。可能没有人能够想象到,二十多年后,人工智能领域的人才,已经成为了最为稀缺的资源。”夏华夏非常感叹。后来,夏华夏又开始接触了Windows编程,做了一个非常炫酷的界面系统,被系里很多同学复制,要知道当时可视化的编程环境比如Visual C++等还没有进入中国,能够做出这种效果,在大家看来已经属于“高手”了。 所以,“钱”也开始找上门了。对夏华夏来说,印象最深的就是大学时做的一个软件外包项目,这是一个仓库的管理查询项目,老板希望通过一个管理软件,可以将货物输入到数据库系统,能够查询货品的信息和库存信息。夏华夏当时刚刚读大二,他就利用几个月的时间,学习了编程和数据库相关的技术,然后通过各种途径学习研究,搞定了这个系统,挣到了人生第一个5000块钱。这是夏华夏挣的人生第一笔钱,虽然现在看起并不多,但是当时在校普通学生的生活费,基本上也就是每月一两百块钱,所以绝对是一笔“巨款”。当然,清华计算机系人才济济,很多同学开始在外面写书,做各种软件,收入几千也并不特别稀奇。但这是夏华夏第一次通过计算机技术挣到了钱,意义非同一般。做自己喜欢做的事情,进步会非常快1993年到2000年,互联网开始在中国风起云涌,后来成为巨头的BAT都诞生在那个时代。夏华夏也触网了。他跟同年级的几个同学一起做了一个“酒井BBS”,名字源自他们住的9#男生楼的谐音。计算机系的女生住7号楼,刚好离的比较近,所以他们搭了一根线连通两个楼,让系里的男生女生可以一起聊天。9#BBS最初是秦浩澜、卿芳慧、周霖等人带头做出来的。周霖就睡在夏华夏的上铺,他也是水木清华的BBS的站长(周霖后来曾任搜狐负责技术的高级副总裁,现在是搜狐旗下狐狸金服集团的联合创始人兼CTO)。大学时期,夏华夏和他的同学们就接触了很多最新鲜的东西,一个全新的世界向他们敞开了怀抱。“我们开始去学习新的技术,学习网络相关的知识,这些都是兴趣驱动的,当时也没有就业压力。其实课堂上的知识带给的成长并是有限的,当你真正动手去做一些事情的时候,进步会非常快,成长也会非常大。”夏华夏告诉我们,当时有个同学写了一款打升级的扑克游戏,后来很多年之后,发现很多扑克游戏还是基于当时他写的代码。“大学是比较单纯的,我们当时写了很多程序,大多都是因为有趣、好玩,功利性是很少的。”不过当时,即使是顶尖的学府也没有开职业规划课,大家都是靠自己摸索。夏华夏刚读大学的时候,只想到能够保送研究生,并没有考虑太多。“我们班很多同学选择了出国,因为每个人接触的人不一样,看到的世界也不同。”夏华夏回忆说,即使是清华学习相对较差的同学,出路也都非常好,有个同学因好几门功课不及格而被推迟毕业,不得已去了一家小创业公司,这家小公司的创始人是一个海归,名字叫张朝阳,后来这位同学还成了张朝阳的重要副手。每个人都有自己的路,关键是选择“其实,每个人都有自己的路,出国是一种选择,保研是一种选择,工作也是一种选择,关键是看你怎么走。”夏华夏说,当时创业的人几乎没有,因为大家对互联网创业几乎是没有概念的,清华的创业协会应该是1997年左右才开始做(王兴是其中的积极分子),后续很多留学生回国创业,那已经是很多年之后了。在大学时代,对夏华夏影响最大的一个人是研究生期间的导师郑纬民老师。夏华夏大三和大四时期就跟恩师在高性能计算所做项目,而且郑老师对最新出现的计算机技术都跟的很紧,从并行计算,到后来的分布式计算、云计算、大数据、异构计算等。夏华夏一直在学习和实践,这也为后来读博和工作打下特别好的基础。“如果不出国的话,我还准备跟郑老师读博士,后面因为一些原因决定退学出国留学,郑老师也给予很大的支持,还帮忙写了推荐信。”所以夏华夏一直到今天,都特别感谢郑老师的栽培之恩。谷歌篇:重剑无锋,大巧不工6年异国求学路,名师出高徒本以为读完研究生读完博士,然后找一份稳定的工作,然后就跟大多数人一样平静的度过此生。但命运,变幻无常,夏华夏遇到了生命中最重要的一个人,他又再次踏上了未知的旅程。清华的计算机专业是五年制,夏华夏大五的下学期就认识了自己的女朋友,现在已经成了他太太。爱情,总是在不经意间,悄然而至。夏太太是同年级的同学,本科毕业后在清华继续读两年制硕士。夏华夏当时正在读博士,因为太太想出国发展,思虑良久后他也决定退学,跟太太一起远赴大洋彼岸继续深造。“其实,从来没有考虑过会遇到自己一生的爱人,也没有想到会出国。因为入学最初的想法就是读个研究生或者博士,出国其实是一个偶然,完全不在人生的规划之中。”加州大学圣迭戈分校(Universityof California, San Diego,简称UCSD)位于南加州拉荷亚社区,那里环境优美,气候宜人,且坐拥全美国最顶级海滩,被称之为美国“最性感”的理工科学习院校。2000年,夏华夏开启了异国他乡的的求学之路,并度过了人生中重要的6年。“我在美国读了很长时间的书,但玩得也很开心,加州可能是全美最适合居住的一个地方,一年四季都不冷不热。”夏华夏的博士导师是知名的华裔计算机科学家Andrew A.Chien(中文名钱安达,ACM、IEEE和AAAS会士,现为芝加哥大学教授,著名技术刊物《Communications fo ACM》的主编)。在1990到1998年期间,钱安达老师在伊利诺伊大学(UIUC)任教授。1998年,钱老师转到UCSD当教授。所以夏华夏就跟随钱老师在UCSD继续做计算机相关的研究工作。钱老师研究的领域也比较广泛,后来研究的方向包括数据中心与超大规模系统架构与编程、弹性,数据密集计算工具,嵌入式与移动计算等。2017年钱老师还受邀来美团TopTalk讲座讲过课。追寻自己的兴趣,实践出真知在读博期间,夏华夏依然追寻自己的乐趣,做一些好玩的事情。“实验室有很多机器,我们可以在上面做任何东西,每个人都可以在机器上建网站,建个人主页。”当时,夏华夏和同学们搭建了当地的一个BBS,到目前这个网站还在,后来这个网站被迁移到云端,夏华夏和朋友还会发布一些住房的信息,然后进行一些社交活动。2000年,MP3开始流行。夏华夏从国内下载了4万多首MP3歌曲。回到美国后,就跟同学一起建立了一个音乐库,对收集歌曲进行打分,开始没有对外开放,但是因为工作量比较大,然后就邀请更多的人参与到这个项目中来共同完成,很快就将想法落地了。“其实,当你有很好的想法时,都是可以去实践的,即使没有收到太多的关注,但你自己会有很大的收获。”在夏华夏看来,整个读书阶段就是不断的去折腾,参与了很多计算机相关的项目,这才让他积累了很多编程的经验。即使读博阶段做了很多研究型的工作,但是其中参与的一个项目也写了几万行的代码。在夏华夏看来,如果没有去做很多这种小型的、有趣的编程实践项目,很多的知识自己也不会知道,也不会对计算机有那么全面的了解。仅仅靠课程内掌握的技术,可能也很难去面试成功一家公司,而且还是世界上最好的互联网公司。经过7轮面试,成功加入谷歌2000年,互联网泡沫破灭。整个IT行业进入低谷,一直到2004年才开始复苏,那一年,谷歌在纳斯达克上市。2006年,夏华夏读完博士,同年10月1日正式加入谷歌,当时最好的互联网公司。谷歌纯技术面试有7轮,前两轮都是通过电话进行技术面试,电话面试通过后,再去谷歌总部面试。总部的面试从早晨一直持续到下午,候选人坐在同一个会议室里;每过45分钟就有一个新的面试官进到会议室来,用各种算法、系统、编程、数学的难题来“刁难”候选人。“无论是电话面试,还是总部面试,都会强调算法与编程,没有太多网上传言的那种类似脑筋急转弯那类的题目,更多的是算法编程、算法能力、分析能力、编程能力。很多人认为谷歌的面试不太公平,有的工作七八年了,还问这么简单的算法和系统问题,但真实情况就是这样,谷歌的标准是希望大家进入谷歌以后,能够写出更高质量的代码。”因为夏华夏大学和研究生期间的实践经验非常多,所以加入谷歌相对比较顺利。在谷歌,想要“改变世界”是一件很容易的事情,因为谷歌服务全球市场,拥有的计算机的数量也是非常庞大的,用户数量也非常庞大,只要随便找一个项目去优化一下,那么就会产生特别巨大的效果,不仅仅可以提升用户体验,甚至能够影响公司的运营成本。很多书中或者网上也提到,在谷歌工作往往是工程师自己找事情去做,很多团队中甚至没有产品经理。相比之下,国内很多大型的互联网公司对产品经理的要求其实很高,而谷歌属于工程师文化,偏技术主导。包括后面谷歌做PaaS平台,组建了一个两三百人的团队,但是产品人员只有个位数,谷歌的工程师都是凭借很强的自我驱动能力来把事情做完。夏华夏在谷歌的经历主要分成两个阶段。第一个阶段,2006年谷歌进入中国,面临很多特殊的问题,包括访问受限,数据的存储问题,中国要求有些数据必须放在中国,而谷歌对自己的敏感数据不愿意放在中国,所以需要一个专门的团队去解决这些问题,然后再做一些新的技术解决方案。夏华夏加入了一个名为China SRE的项目组,去解决这些富有挑战性的工作。第二个阶段,夏华夏参与了Google+的研发。由于看到了Facebook发展的非常迅速,当时谷歌也希望在社交领域加大投入,所以谷歌在2009年启动了一个代号“Emerald Sea”(“翡翠海”)的项目,目标是研发功能强大的Google+社交平台。当时谷歌把”翡翠海“视为战略级项目,调集了几百人的精英团队去做这个项目,时任CEO的Larry Page也把办公室搬到了项目所在的楼。注重开放能力,勇于改变从2006年到2011年,夏华夏在谷歌度过了非常美好的一段时光。谷歌的架构设计非常有特色,后来搭建美团整个架构体系时,夏华夏也借鉴了谷歌架构的很多设计理念,包括容灾系统的设计,到现在也没有过时。“在谷歌6年,让我感触最深的就是谷歌非常、非常注重开放,所以在美团我也非常鼓励工程师开放、多分享技术。”夏华夏说,谷歌几乎所有的代码都是开放的,除了很极少数的核心代码,比如搜索算法、排序算法等等。如果谷歌的同学觉得别人的代码里哪个地方设计的不够好,可以直接上去改。夏华夏说:“谷歌的理念对我的影响比较大,在后来的工作中,特别是到美团后,我一直都试图往这个方向靠近,包括现在新组建的团队,我们希望让每个工程师都有对代码的控制和访问权,对代码质量的把控权,包括运维和安全的责任,这种理念能够帮助提升整个技术团队的主动性。”还有很重要的一点,谷歌的工程师都具备体系化的思维方式。比如谷歌的工程师,从产品到架构设计,再到最后的上线测试,工程师都是从头跟到尾,所有的代码包括后续优化的代码都统一放到一个代码仓库中,所有的文档也放在Git中,所有的培训资料,工程师可以随时进行修改和优化,这些对夏华夏都产生了影响。谷歌投入了很多时间和精力去构建公司的课程体系,包括一套名为EngEDU的线上学习系统,这套体系中包括很多编程语言、内部工具、内部研发流程的学习,每个员工都可以从最初级的语言开始学习,还可以学习谷歌公司的一些系统的使用、公司的一些工具和代码库、以及网络系统的配置方法等等,每节课还会有编程的练习。夏华夏说,“未来美团也会向这个方向努力,现在美团技术学院推出了自己的学习平台,还制作了很多技术课程,向公司内部的同学开放,我们也希望能够沉淀成一些体系化的课程,帮助更多的工程师成长。”其实,从大学毕业到进入谷歌,夏华夏一直没有刻意去选择自己要走那条路,感觉一切都是顺其自然的。对他来说,这些可能都算不上“很重大的决定”。如果说人生很重要的一个抉择,那么放弃谷歌的工作,回国发展,肯定算是夏华夏最重要的决定之一。回国篇:宝剑锋从磨砺出,梅花香自苦寒来因意识形态对立问题,决定回国回国,是一个很长的话题。2000年到美国后,夏华夏无论是生活还是学习,都处在一个很舒适的环境中。2008年奥运会在北京举办,很多海外的华人都觉得很自豪,夏华夏也觉得为祖国骄傲。后来,华夏夫妇对美国所谓的自由和民主信念开始破裂,其中最重要的一件事就是奥运火炬当时经过旧金山,当时遭到部分反华势力组织的阻挠和捣乱,甚至于美国的很多媒体都在扭曲报道,包括美国最有名的媒体CNN也歧视中国,进行一些不实的报道。他们开始认识到,中美在意识形态层面是对立的,虽然美国有很多先天优势,在这里发展能有更好的全球视野,但是并不是特别理想的国度,所以夏华夏和太太开始把回国发展的提上日程。还有一个很重要的因素,让夏华夏也深刻感受到国内互联网行业的蓬勃发展,国内很多互联网公司正在高速追赶美国,这点让夏华夏非常激动。夏华夏在谷歌的一位前上司加入百度做技术VP后,在2011年6月份找到夏华夏,希望他能够回国帮助百度做技术架构方面的工作。因为百度当时也在对标谷歌,而且当时发展非常不错,在工作内容方面比较匹配,所以夏华夏开始人生的非常重要的一个决定——回国。遭遇挫折,用更高的维度去思考问题2011年底,经过短暂的准备后,夏华夏回到了北京加入百度。夏华夏在百度一年多的时间里,担任运维部的总架构师,他当时的主要工作是把运维部的工作做了梳理,同时也参与了技术架构、基础软件等工作。“我觉得在百度,其实是一个比较好的缓冲或者落地。”夏华夏说。其实谷歌的工程师文化跟百度还是有很多不同的,当时在百度做技术架构工作是有一些“虚”的,也曾经试图跟基础架构部的同学,一起推动国外比较好的技术理念,但是由于种种原因,落地非常困难。在夏华夏看来,很多公司在做基础架构层面的工作时,往往没有考虑到业务方的需求,只是想做一套很好的系统,很好的架构,让业务方去替换,这种模式存在很大的问题,而且这也是一份非常有挑战,非常非常有风险的工作,成功的可能性很小。百度的工作经历,提升了夏华夏对技术工作整体的认知,也让他能够从一个更高的维度去思考基础架构层面的工作,这也为后续在美团的发展,埋下了很好的伏笔。太太牵线,结识美团技术团队很多时候,选择比努力更重要。那么如何才能做出最好的、最适合自己的选择呢?需要勇气,魄力,更重要的是前瞻性的眼光,这些因素都在夏华夏身上得到很自然的体现。 夏华夏能够加入美团,很重要的一个原因也是因为自己的妻子。她此前是在eBay做电商方面的工作,所以回国后也想在这个行业发展,陆续跟国内电商类的公司京东、携程、拉手、窝窝、美团的技术负责人都聊过,其中大部分感觉都算不上很好的技术人员,如果加入他们负责的技术团队,实在是有点担心。唯一留下好印象的就是美团联合创始人穆荣均,在面试中让人感觉很靠谱。最后她决定选择美团。夏太太还讲到一个细节,其实在2012年回国之前,她就给美团发了简历,很快得到回复,能不能回国面试。在告知了对方回国日期之后,她自己很快就忘记了,可是没想到在回国前两天,美团的HR同学很准时地再次与她联系。这件事让夏太太觉得,美团整个团队做事比较靠谱,令人印象深刻。虽然妻子一直在美团工作,但是夏华夏对美团并没有太多的了解。2013年3月份,穆荣均通过夏太太向夏华夏抛出了”橄榄枝“。其实,最初就是以朋友的身份见面聊天,吃过几次饭,先建立了友谊,开始穆荣均并没有很快表明”想挖人“的意愿。因为夏华夏住的小区离美团很近,所以他偶尔去美团接夏太太,当时夏太太在数据组工作,就发现美团的监控工具做的很好,虽然底层也是开源系统,但美团基于它做的功能很简介、很直观、也很好用。后来陆续又接触到很多美团的技术同学,发现整个技术团队很务实,技术氛围很好,整个团队的工作态度非常认真。所以每次面对穆荣均的约饭,夏华夏都欣然前往。与王兴畅谈云计算,终被美团所打动后来,穆荣均开始给夏华夏介绍了美团当时的情况,以及未来的发展规划,还有王兴的一个梦想。那还是在2009年年底的时候,王兴说:“其实很多做互联网创业的人,很少考虑怎么去帮助这些人,这些普罗大众,这么多的小商家。其实,他们都在努力改变自己的命运,我们美团就要帮助他们,帮助这些普普通通的老百姓。这也是属于我们的机会。”这句话,深深触动了夏华夏。不久后,穆荣均将夏华夏介绍给王兴,他以为跟CEO聊天,应该会聊一聊公司的愿景,公司的发展目标之类,但是第一次见面,王兴跟夏华夏聊的主题却是云计算。“2013年,百度还没有正式开始做云计算,国内做云计算的也很少,所以王兴大谈云计算这个话题的时候,让我很诧异。”夏华夏问王兴为什么对云计算感兴趣,王兴说,其实从2012年开始,美团就在做云计算相关的技术储备了。王兴对云计算技术理解很深刻,他做了很多的阅读,进行过很多的思考。当时给夏华夏留下非常深刻的印象。之后的更多接触,让他觉得美团做的事情,并不是想大多数人想的那样技术很Low,从开始就是一项非常有挑战性的技术工作,而且着眼长远,整个美团的技术团队也是很认真地想通过技术手段来解决吃、喝、玩、乐等一站式生活服务问题。夏华夏再次选择了一条更难走的路。2013年清明节假期,他终于答应穆荣均,正式宣布加入美团。从国内顶级的互联网巨头,降薪跳到一家前途还不是很明朗的创业公司,充分显示了他的判断力和勇气。选择需要魄力,更需要信仰“那时候美团比较小,我太太倒是很担心,因为两个人都在同一家公司,万一公司干砸了怎么办。我自己倒是没有犹豫,因为我是那种不怎么考虑太多后果的人,这件事很有意义,反正想做就做了。而且除了收入减少了,并没有什么特别严重的影响,两个人的收入也够用的。”夏华夏很坦然。命运,也总是会垂青那些努力的人,坚持的人,也会青睐那些不断有着人生追求的人。有时候,命运会在我们前行的道路上,设下重重障碍,很少有人愿意去打破那些障碍,去看看未知的世界。每一次抉择,夏华夏都是追寻自己的内心,并没有考虑太多财富、名望这些东西,所以他可以比绝大多数人,走的更远。不择细土,方能成其高2013年,美团技术团队已经初具规模,整个团队也很务实,但是跟谷歌这些互联网巨头相比,还是有很大的差距,夏华夏也希望能够将谷歌的技术理念带到美团。夏华夏的第一项任务,就是组建技术工程部(包括技术部和移动技术部)。当时美团投入很大的精力在做移动端,夏华夏用两个月左右的时间轮岗,摸清了从前端到后台几乎所有部门的大致情况,然后开始组建系统优化项目组。美团很多同学对夏华夏的印象都是很务实的一个人,完全没有领导的架子。而且夏华夏也会深度参与很多技术项目,逐个解决,跟大家努力把项目做好,无论是技术层面,还是管理层面,还有对技术梯队的培训,夏华夏都会亲力亲为。最开始到美团,夏华夏主要跟移动端的同学在一起,讨论如何优化网站性能。当时王兴和穆荣均也经常找夏华夏讨论网站的性能问题,因为他们看到了亚马逊的研究,0.1秒的网页延迟,会直接导致客户活跃度下降1%,当时美团首页加载需要4到5秒,如果提升几秒的话,对公司的价值可以想象有多么大。所以夏华夏的当务之急就是提升美团的访问速度。后来在“千团大战”中,美团能够脱颖而出,一方面因为王兴为首的创始团队从最开始就很注重技术,知道什么是关键因素,另一方面就是美团有很多像夏华夏这样的技术人才,他们对技术的追求非常执着而且很认真,战略和执行的统一,才让美团走的更高、更远。其实,做基础架构方面的工作,事无巨细,所有跟技术相关的东西,都需要参与。夏华夏发现移动端对推荐算法的要求非常高,因为当时屏幕还很小,在有限的空间展示就需要非常好的算法,而团购产品推荐算法对UPS(用户画像)的要求非常高,所以又开始参与了用户画像的算法项目。做完UPS后,夏华夏又陆续参与了很多业务项目的开发,包括技术存储、负载均衡、中间件系统等等。夏华夏又接手了运维团队,因为系统的稳定性直接关乎用户体验,这项重任又落在了他的肩上。除了纯技术项目之外,夏华夏和穆荣均一样,都非常重视工程师文化和技术品牌的建设,他们知道在这方面投入,让更多技术同学有更好的成长,收益很高。夏华夏曾经是美团技术委员会的主席,也是美团技术学院最早的负责人。技术学院的很多项目包括培训、Hackathon、很有特色的图书馆和技术博客等等,最早都是他和从前端工程师转运营的弥新锋一起做起来的。他本人还是公司很受欢迎的金牌讲师,不仅讲技术、架构,还讲授了非常多软技能方面的课程。从技术走向管理,从接受C的评价开始人生,不会是一帆风顺,难免也会遇到很多坎坎坷坷,很多人只会抱怨生活的苦难,而没有看到苦难背后,生活给予的礼物。当然在美团,夏华夏也承受了一些“委屈”。在2014年年底,他给技术团队同学绩效考评,给某个同学打了C评价,但是这个同学表示不服。后来申诉到CTO那里,穆荣均找夏华夏沟通,问他为什么没有说服这个同学。在夏华夏看来,工作产出相对较差,所以应该给C的评价无可厚非。但是,穆荣均告诉夏华夏,给下面的同学打绩效、说服就是管理工作,如果没有很好地说服同学,就说明管理工作没有到位。更令华夏没想到的是,“当时穆荣均说,要不这样吧,既然他不接受,要不你接受个C吧?当时我说,那行吧。其实我当时还是觉得比较委屈的。后来想起来,我觉得穆荣均说的非常有道理。”那一次,夏华夏人生中第一次接受C级的评价。夏华夏说,他跟穆荣均相处的时间里,也学到了很多管理层面的东西。因为自己一直太专注于技术,所以在这方面存在很多问题,后来他也跟穆荣均学了很多管理上的理念和方法。也是因为在技术和管理层面的沉淀和积累,夏华夏才能抗起更加重大项目的挑战。在美团,最难忘的那些经历2015年,O2O再起硝烟,外卖大战正酣,夏华夏临危受命,第一次开始接手业务直接相关的工作。此前夏华夏一直都是做基础架构方面的工作,包括谷歌和百度,以及美团的前期。当时美团外卖部门业务压力非常大,系统已经快撑不住了,那时候已经做到180万单了。每天中午,整个技术团队都非常紧张,周末大家也加班,还开辟了美团外卖的“作战室”,这种情况一直持续了两个月左右,然后他又带领大家把以前技术的“坑”填补上。通过大家共同的努力,夏华夏带领技术团队将外卖系统的高可用从两个9提升到三个9,并解决了很多关键路径上服务的稳定性问题。终于在一个周五的晚上,夏华夏告诉大家,周末不用加班了,大家都表示非常开心。那个时刻,对夏华夏来说特别有感触,也很欣慰。从谷歌到百度再到美团,夏华夏一直抱有一种理念,他觉得做技术最终是为了让团队越做越轻松。在百度的时候,夏华夏在负责运维就发现,如果公司的质量体系或者运维体系做的不够好,技术团队就会非常辛苦。所以到美团以后,他希望带领技术团队,不断追求卓越,尽量让大家不加班,轻轻松松把事情搞定。后来看到大家都能安安心心回家过周末,他也觉得非常具有成就感。2015年,从基础架构到业务部门,夏华夏再次突破了技术的挑战,而且技术同学的笑脸,让他记忆非常深刻。这一年,美团技术团队抗过千万级流量并发的考验。这一年,夏华夏在技术层面也实现了自我的突破。这两年,夏华夏又开始迎接新的挑战,负责公司最前沿的无人配送项目。无人驾驶技术的落地是世界难题,但美团配送末端物流“小轻慢物”的特点,却提供了一个很好的场景。夏华夏从0开始组建了一支团队,涉及很多自己之前很少接触过的技术:机械结构、电子工程、嵌入式开发等等,并很快实现了产品并开始落地实践。现在,无人配送开放平台已经完成了在朝阳大悦城的B端测试运营,以及深圳联想大厦的C端试运营,并在上海松江大学城实现了从B端到C端的完整闭环运营。在松江大学城内,由无人车配送的美团外卖订单已经超过1000单/天,印证了美团无人配送开放平台进行片区规模化运营的可行性。今年7月,美团还发布了无人配送开放平台,希望集合政府、高校、企业三方力量一起实现这个伟大梦想,已经吸引包括清华大学、加州伯克利大学、北京智能车联产业创新中心、华夏幸福、Segway等近20家国内国外合作伙伴加入。加入美团后,夏华夏一直在不断打破自己的舒适区,一直对技术有着非常执着的追求。高速成长的美团,也刚好给了夏华夏足够大的舞台去施展。今天,他依然奔跑在技术的最前沿。因为在那里,是距离战场最近的地方,他可以继续探索技术的新边界……对话夏华夏:程序员的人生抉择Q:在你自己的成长经历中,你觉得有哪几个重要的里程碑?夏华夏:我觉得我的成长,很大程度上属于“阴差阳错”,自己的运气也不错。第一个里程碑,应该算是“蒙”到了清华计算机系。不过,后面的学习就是按部就班,每一步都走的比较稳,包括读博也是听系里老师的安排。如果不是我太太出现在我的生命中,我应该会留在国内发展,整个人生的发展路径也会不一样,当然现在说不清哪个好哪个差,因为时间无法倒流。 第二个里程碑就是出国读书,2000年那个时候,在计算机领域国内外差距很大,所以去UCSD学习对我专业能力的提升非常重要。我还记得刚到国外时,由于英文的问题,课程难度非常大,经常赶作业到凌晨12点以后了,最后一班校车也停了,只能走路回家。如果只是靠在清华学习的那点知识,进入谷歌显然没有任何机会,在国外都会要求有很强的编程能力,另外就是对系统的理解,在UCSD读博的过程中,还有自己搭建过一些网站,打下了比较好的基础,所以后面进入谷歌和百度,都非常顺利。第三个很重要的里程碑就是加入美团,在加入美团之前,我很多时候是独立工程师或者架构师的角色,到美团以后才开始带大的团队,包括跟穆荣均也学习到很多管理层面的东西。后来跟老王(美团联合创始人、高级副总裁王慧文)工作,他是另外一种风格,实操性比较强、对业务和产品的思考角度非常新颖。所以在美团最大的收获是在从一个纯技术人成长为能力更综合的人。值得一提的还有,2014年加入了美团管理学院讲师团队,因为在演讲的时候,跟真实情况也是不一样的,需要构建理论体系。其实最好的成长,就是分享,无论是做技术研究还是团队管理,这句话都非常有道理。美团从创立之初,技术管理就比较规范,所以在这里成长也非常快。开始的时候,几乎每周换一个团队,跟这个团队的同学一起工作、一起学习,深入了解这个团队。我觉得技术管理者应该多接触一线的同学,可以让自己更快的融入团队,后续很多工作的开展,也会起到事半功倍的作用。Q:你认为优秀的架构师或者技术管理者,都应该具备哪些特质?夏华夏:首先最主要的就是技术的深度,其实架构师也属于技术梯队,我们在面试的时候,要求技术同学对其所在的领域要非常熟悉,包括使用哪种技术方案解决问题,为什么要使用这种技术方案,在技术选择时要考虑哪些重要的因素等等,对技术细节的把控能力要足够深刻。其次,就是技术人员要具备足够宽的技术视野,也就是广度,要对自己领域周边的技术发展要有所了解,同时也要了解这种新技术在其他公司的应用情况。我希望能够加入美团点评的技术同学都能够很好的深度和相对的广度。除此之外,也希望他能够具备比较强的学习能力和自我驱动力。因为技术发展变化非常快,我们希望新加入的同学拥有足够的热情去不断的学习。 在技术管理能力层面,一方面是管理项目的能力,如何才能更好的推动一个项目;另一方面就是如何更好的管理好人才,使用好人才,培养好人才。我觉得一个好的技术管理者必须具备一个很重要的特质,心态要足够开放,这样他就愿意去学习新技术,能对事情研究的比较深入。Q: 对于美团的技术同学,您有什么建议?夏华夏:第一个方面是保持不断学习的心态,这也是美团工程师文化特别提倡的,这是我们希望所有工程师具有的特质和建议,包括对技术领域深度和广度的学习,不断延伸个人的能力。第二个方面是全栈思维,从心态方面,希望大家能够站在团队的视角来看问题,我们鼓励开放的工作氛围,美团点评内部的很多代码也在逐渐开放。还有就是从技术能力层面,我建议大家的技术能力能够全面一些,现在美团有6个技术通道,包括前端、后台、算法、运维、测试、系统,每个技术同学应该逐渐从全栈的角度去考虑自己的发展,还有对产品和业务的思考。现在美团点评基础架构、研发服务框架、安全认证体系、容灾体系等等很多项目都还在持续的建设中,我们鼓励更多的工程师主动参与进来。第三个方面,就是前瞻思维,近几年涌现出很多的热点技术,我们应该积极的思考热点技术如何能够更好的跟我们的业务结合起来。比如利用人工智能相关的技术来提升用户体验,像语音识别技术可以帮我们做智能客服,我们也可以在算法层面做很多优化,来提升推荐和搜索引擎的准确度和效率等等,最近技术团队也在探索如何利用人工智能,实现自动化、智能化运维。我们鼓励大家利用新技术,从而来推动研发团队的成长。最后还有一点,对技术管理者来说,美团有一个特别好的地方。就是没有跟很大大公司一样走两条发展路线,像百度,从T5、T6开始就分成两条线,团队中一个管理职责的人,一个做技术职责的人,管理者往往对技术的接触比较少,其实很容易产生很多问题。在美团点评这个大家庭里面,我们要求管理和技术齐头并进,提倡更均衡的发展,这种模式更适合培养更优秀的技术人才和领导者。Q:你有什么特别佩服的人吗?夏华夏:Google的Jeff Dean。在Google,大家都把他奉为”神“一样的人物,我也很佩服他。谷歌的技术架构之所以这么好,其实跟Jeff有很大的关系。Jeff亲手打造的系统包括Google File System、MapReduce、BigTable以及Spanner等等,这些是大规模分布式系统的经典架构,称得上Google和现代互联网存在的“基石”。Jeff一直没有脱离编程的前线。在2011年的Google+项目中,当时高并发场景下Feed流读写性能跟不上,而读写后台是基于BigTable,所以Jeff就过来解决问题。他当时已经是Fellow级别(相当于副总裁),自己一个人带个ThinkPad过来,找个工位,编了两天程序,就把BigTable架构优化了,顺利支撑了Google+上线后的高并发访问。Jeff虽然职位已经很高了,但是他还在不断尝试新的领域。他现在是Senior Fellow,Google职级最高的工程师,相当于管理线的SVP。几年前他把注意力从基础架构转向了人工智能,现在是Google Brain的负责人,TensorFlow就是他团队的作品。就在不久前,他还发表了新论文,提出了使用机器学习索引来替代B-Trees,能够提速3倍,再次点燃了整个技术圈,当时朋友圈很多人在转发这个消息。不断尝试新的领域,不断突破自己的边界。我觉得这是非常值得我们所有技术同学学习的地方。Q:如果让你给技术同学推荐一些书,你会推荐哪些?夏华夏:我会推荐尤瓦尔·赫拉里的《人类简史》和《未来简史》,这两本不是计算机技术相关的书籍。但是作者从一个非常宏大的视角阐述了我们整个人类社会的发展,也强调了人与自然的关系,包括现阶段人类如何受到技术的影响,以及人生的意义等很多话题。我觉得这两本书可以帮助我们技术同学更好的理解、认识这个世界,认识“人类”这个种群,或者说这种生物,这两本书对我影响蛮大的,我也推荐给大家,希望大家也能从中有所感悟和收获。招聘信息美团无人配送部于2016年组建,自研无人配送产品,开放自身业务场景,致力用先进的技术,对配送侧进行改革,增加运力的供给。团队目前已经自主研发有两款适应不同场景的无人车产品和一款无人机产品,发布美团无人配送开放平台。目前美团无人配送已经完成在雄安、北京、深圳、上海多地的落地试运营,参与制定发布《服务型电动自动行驶轮式车技术要求》,在技术场景和法规等多方面推动产业发展,最终达到用无人配送让服务触达世界每个角落的目标。美团无人配送团队诚招各路英才,简历请投至: walle.hr@meituan.com

November 9, 2018 · 1 min · jiezi

Category 特性在 iOS 组件化中的应用与管控

背景iOS Category功能简介Category 是 Objective-C 2.0之后添加的语言特性。Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。在 Objective-C(iOS 的开发语言,下文用 OC 代替)中的具体体现为:实例(类)方法、属性和协议。除了引用中提到的添加方法,Category 还有很多优势,比如将一个类的实现拆分开放在不同的文件内,以及可以声明私有方法,甚至可以模拟多继承等操作,具体可参考官方文档Category。若 Category 添加的方法是基类已经存在的,则会覆盖基类的同名方法。本文将要提到的组件间通信都是基于这个特性实现的,在本文的最后则会提到对覆盖风险的管控。组件通信的背景随着移动互联网的快速发展,不断迭代的移动端工程往往面临着耦合严重、维护效率低、开发不够敏捷等常见问题,因此越来越多的公司开始推行“组件化”,通过解耦重组组件来提高并行开发效率。但是大多数团队口中的“组件化”就是把代码分库,主工程使用 CocoaPods 工具把各个子库的版本号聚合起来。但能合理的把组件分层,并且有一整套工具链支撑发版与集成的公司较少,导致开发效率很难有明显地提升。处理好各个组件之间的通信与解耦一直都是组件化的难点。诸如组件之间的 Podfile 相互显式依赖,以及各种联合发版等问题,若处理不当可能会引发“灾难”性的后果。目前做到 ViewController (指iOS中的页面,下文用VC代替)级别解耦的团队较多,维护一套 mapping 关系并使用 scheme 进行跳转,但是目前仍然无法做到更细粒度的解耦通信,依然满足不了部分业务的需求。实际业务案例例1:外卖的首页的商家列表(WMPageKit),在进入一个商家(WMRestaurantKit)选择5件商品返回到首页的时候,对应的商家cell需要显示已选商品“5”。例2:搜索结果(WMSearchKit)跳转到商超的容器页(WMSupermarketKit),需要传递一个通用Domain(也有的说法叫模型、Model、Entity、Object等等,下文统一用Domain表示)。例3:做一键下单需求(WMPageKit),需要调用下单功能的一个方法(WMOrderKit)入参是一个订单相关 Domain 和一个 VC,不需要返回值。这几种场景基本涵盖了组件通信所需的的基本功能,那么怎样才可以实现最优雅的解决方案?组件通信的探索模型分析对于上文的实际业务案例,很容易想到的应对方案有三种,第一是拷贝共同依赖代码,第二是直接依赖,第三是下沉公共依赖。对于方案一,会维护多份冗余代码,逻辑更新后代码不同步,显然是不可取的。对于方案二,对于调用方来说,会引入较多无用依赖,且可能造成组件间的循环依赖问题,导致组件无法发布。对于方案三,其实是可行解,但是开发成本较大。对于下沉出来的组件来说,其实很难找到一个明确的定位,最终沦为多个组件的“大杂烩”依赖,从而导致严重的维护性问题。那如何解决这个问题呢?根据面向对象设计的五大原则之一的“依赖倒置原则”(Dependency Inversion Principle),高层次的模块不应该依赖于低层次的模块,两者(的实现)都应该依赖于抽象接口。推广到组件间的关系处理,对于组件间的调用和被调用方,从本质上来说,我们也需要尽量避免它们的直接依赖,而希望它们依赖一个公共的抽象层,通过架构工具来管理和使用这个抽象层。这样我们就可以在解除组件间在构建时不必要的依赖,从而优雅地实现组件间的通讯。业界现有方案的几大方向实践依赖倒置原则的方案有很多,在 iOS 侧,OC 语言和 Foundation 库给我们提供了数个可用于抽象的语言工具。在这一节我们将对其中部分实践进行分析。1.使用依赖注入代表作品有 Objection 和 Typhoon,两者都是 OC 中的依赖注入框架,前者轻量级,后者较重并支持 Swift。比较具有通用性的方法是使用「协议」 <-> 「类」绑定的方式,对于要注入的对象会有对应的 Protocol 进行约束,会经常看到一些RegisterClass:ForProtocol:和classFromProtocol的代码。在需要使用注入对象时,用框架提供的接口以协议作为入参从容器中获得初始化后的所需对象。也可以在 Register 的时候直接注册一段 Block-Code,这个代码块用来初始化自己,作为id类型的返回值返回,可以支持一些编译检查来确保对应代码被编译。美团内推行将一些运行时加载的操作前移至编译时,比如将各项注册从 +load 改为在编译期使用__attribute((used,section("__DATA,key"))) 写入 mach-O 文件 Data 的 Segment 中来减少冷启动的时间消耗。因此,该方案的局限性在于:代码块存取的性能消耗较大,并且协议与类的绑定关系的维护需要花费更多的时间成本。2.基于SPI机制全称是 Service Provider Interfaces,代表作品是 ServiceLoader。实现过程大致是:A库与B库之间无依赖,但都依赖于P平台。把B库内的一个接口I下沉到平台层(“平台层”也叫做“通用能力层”,下文统一用平台层表示),入参和返回值的类型需要平台层包含,接口I的实现放在B库里(因为实现在B库,所以实现里可以正常引用B库的元素)。然后A库通过P平台的这个接口I来实现功能。A可以调用的到接口I,但是在B的库中进行实现。在A库需要通过一个接口I实例化出一个对象,使用ServiceLoader.load(接口,key),通过注册过的key使用反射找到这个接口imp的文件路径然后得到这个实例对象调用对应接口。这个操作在安卓中使用较为广泛,大致相当于用反射操作来替代一次了 import 这样的耦合引用。但实际上iOS中若使用反射来实现功能则完全不必这么麻烦。关于反射,Java可以实现类似于ClassFromString的功能,但是无法直接使用 MethodFromString的功能。并且ClassFromString也是通过字符串map到这个类的文件路径,类似于 com.waimai.home.searchImp,从而可以获得类型然后实例化,而OC的反射是通过消息机制实现。3.基于通知中心之前和一个做读书类App的同学交流,发现行业内有些公司的团队在使用 NotificationCenter 进行一些解耦的通信,因为通知中心本身支持传递对象,并且通知中心的功能也原生支持同步执行,所以也可以达到目的。通知中心在iOS 9之后有一次比较大的升级,将通知支持了 request 和 response 的处理逻辑,并支持获取到通知的发送者。比以往的通知群发但不感知发送者和是否收到,进步了很多。字符串的约定也可以理解为一个简化的协议,可设置成宏或常量放在平台层进行统一的维护。比较明显的缺陷是开发的统一范式难以约束,风格迥异,且字符串相较于接口而言还是难以管理。4.使用objc_msgSend这是iOS原生消息机制中最万能的方法,编写时会有一些硬编码。核心代码如下:id s = ((id(*)(id, SEL))objc_msgSend)(ClassName,@selector(methodName)); 这种方法的特点是即插即用,在开发者能100%确定整条调用链没问题的时候,可以快速实现功能。此方案的缺陷在于编写十分随意,检查和校验的逻辑还不够,满屏的强转。对于 int、Integer、NSNumber 这样的很容易发生类型转换错误,结果虽然不报错,但数字会有错误。方案对比接下来,我们对这几个大方向进行一些性能对比。考虑到在公司内的实际用法与限制,可能比常规方法增加了若干步骤,结果也可能会与常规裸测存在一定的偏差。例如依赖注入常用做法是存在单例(内存)里,但是我们为了优化冷启动时间都写入 mach-O 文件 Data 的 Segment 里了,所以在我们的统计口径下存取时间会相对较长。// 为了不暴露类名将业务属性用“some”代替,并隐藏初始化、循环100W次、差值计算等代码,关键操作代码如下// 存取注入对象xxConfig = [[WMSomeGlueCore sharedInstance] createObjectForProtocol:@protocol(WMSomeProtocol)];// 通知发送[[NSNotificationCenter defaultCenter]postNotificationName:@“nixx” object:nil];// 原生接口调用a = [WMSomeClass class];// 反射调用b = objc_getClass(“WMSomeClass”);运行结果显示如下:可以看出原生的接口调用明显是最高效的用法,反射的时长比原生要多一个数量级,不过100W次也就是多了几十毫秒,还在可以接受的范围之内。通知发送相比之下性能就很低了,存取注入对象更低。当然除了性能消耗外,还有很多不好量化的维度,包括规范约束、功能性、代码量、可读性等,笔者按照实际场景客观评价给出对比的分值。下面,我们用五种维度的能力值图来对比每一种方案优缺点:各维度的的评分考虑到了一定的实际场景,可能和常规结果稍有偏差。已经做了转化,看图面积越大越优。可读性的维度越长代表可读性越高,代码量的维度越长代表代码成本越少。如图2所示,可以看出上图的四种方式或多或少都存在一些缺点:依赖注入是因为美团的实际场景问题,所以在性能消耗上存在明显的短板,并且代码量和可读性都不突出,规范约束这里是亮点。SPI机制的范围图很大,但使用了反射,并且代码开发成本较高,实践上来看,对协议管理有一定要求。通知中心看上去挺方便,但发送与接收大多成对出现,还附带绑定方法或者Block,代码量并不少。而msgsend功能强大,代码量也少,但是在规范约束和可读性上几乎为零。综合看来 SPI 和 objc_msgSend 两者的特点比较明显,很有潜力,如果针对这两种方案分别进行一定程度的完善,应该可以实现一个综合评分更高的方案。从现有方案中完善或衍生出的方案5.使用Category+NSInvocation此方案从 objc_msgSend 演化而来。NSInvocation 的调用方式的底层还是会使用到 objc_msgSend,但是通过一些方法签名和返回值类型校验,可以解决很多类型规范相关的问题,并且这种方式没有繁琐的注册步骤,任何一次新接口的添加,都可以直接在低层的库中进行完成。为了更进一步限制调用者能够调用的接口,创建一些 Category 来提供接口,内部包装下层接口,把返回值和入参都限制实际的类型。业界比较接近的例子有 casatwy 的 CTMediator。6.原生CategoryCoverOrigin方式此方案从 SPI 方式演化而来。两个的共同点是都在平台层提供接口供业务方调用,不同点是此方式完全规避了各种硬编码。而且 CategoryCoverOrigin 是一个思想,没有任何框架代码,可以说 OC 的 Runtime 就是这个方案的框架支撑。此方案的核心操作是在基类里汇总所有业务接口,在上层的业务库中创建基类的 Category 中对声明的接口进行覆盖。整个过程没有任何硬编码与反射。演化出的这两种方案能力评估如下(绿色部分),图中也贴了和演化前方案(桔色部分)的对比:上文对这两种方案描述的非常概括,可能有同学会对能力评估存在质疑。接下来会分别进行详解的介绍,并描述在实际操作值得注意的细节。这两种方案组合成了外卖内部的组件通信框架 WMScheduler。WMScheduler组件通信外卖的 WMScheduler 主要是通过对 Category 特性的运用来实现组件间通信,实际操作中有两种的应用方案:Category+NSInvocation 和 Category CoverOrigin。1.Category+NSInvocation方案方案简介:这个方案将其对 NSInvocation 功能容错封装、参数判断、类型转换的代码写在下层,提供简易万能的接口。并在上层创建通信调度器类提供常用接口,在调度器的的 Category 里扩展特定业务的专用接口。所有的上层接口均有规范约束,这些规范接口的内部会调用下层的简易万能接口即可通过NSInvocation 相关的硬编码操作调用任何方法。UML图:如图3-1所示,代码的核心在 WMSchedulerCore 类,其包含了基于 NSInvocation 对 target 与 method 的操作、对参数的处理(包括对象,基本数据类型,NULL类型)、对异常的处理等等,最终开放了简洁的万能接口,接口参数有 target、method、parameters等等,然后内部帮我们完成调用。但这个接口并不是让上层业务直接进行调用,而是需要创建一个 WMSchedule r的 Category,在这个 Category 中编写规范的接口(前缀、入参类型、返回值类型都是确定的)。值得一提的是,提供业务专用接口的 Category 没有以 WMSchedulerCore 为基类,而是以 WMScheduler 为基类。看似多此一举,实际上是为了做权限的隔离。上层业务只能访问到 WMScheduler.h 及其 Category 的规范接口。并不能访问到 WMSchedulerCore.h 提供的“万能但不规范”接口。例如:在UML图中可以看到 外界只可以调用到wms_getOrderCountWithPoiid(规范接口),并不能使用wm_excuteInstance Method(万能接口)。为了更好地理解实际使用,笔者贴一个组件调用周期的完整代码:如图3-2,在这种方案下,“B库调用A库方法”的需求只需要改两个仓库的代码,需要改动的文件标了下划线,请仔细看下示例代码。示例代码:平台(通用功能)库三个文件:①// WMScheduler+AKit.h#import “WMScheduler.h”@interface WMScheduler(AKit)/** * 通过商家id查到当前购物车已选e的小红点数量 * @param poiid 商家id * @return 实际的小红点数量 */+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber )poiID;@end②// WMScheduler+AKit.m#import “WMSchedulerCore.h”#import “WMScheduler+AKit.h”#import “NSObject+WMScheduler.h”@implementation WMScheduler (AKit)+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber )poiID{ if (nil == poiid) { return 0; }#pragma clang diagnostic push#pragma clang diagnostic ignored “-Wundeclared-selector” id singleton = [wm_scheduler_getClass(“WMXXXSingleton”) wm_executeMethod:@selector(sharedInstance)]; NSNumber orderFoodCount = [singleton wm_executeMethod:@selector(calculateOrderedFoodCountWithPoiID:) params:@[poiID]]; return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];#pragma clang diagnostic pop}@end③// WMSchedulerInterfaceList.h#ifndef WMSchedulerInterfaceList_h#define WMSchedulerInterfaceList_h// 这个文件会被加到上层业务的pch里,所以下文不用import本文件#import “WMScheduler.h”#import “WMScheduler+AKit.h”#endif / WMSchedulerInterfaceList_h /BKit (调用方)一个文件:// WMHomeVC.m@interface WMHomeVC () <UITableViewDataSource, UITableViewDelegate>@end@implementation WMHomeVC… NSUInteger foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID]; NSLog(@"%ld",foodCount);…@end代码分析:上文四个文件完成了一次跨组件的调用,在 WMScheduler+AKit.m 中的第30、31行,调用的都是AKit(提供方)的现有方法,因为 WMSchedulerCore 提供了 NSInvocation 的调用方式,所以可以直接向上调用。WMScheduler+AKit 中提供的接口就是上文说的“规范接口”,这个接口在WMHomeVC(调用方)调用时和调用本仓库内的OC方法,并没有区别。延伸思考:上文的例子中入参和返回值都是基本数据类型,Domain 也是支持的,前提是这个 Domain 是放在平台库的。我们可以将工程中的 Domain 分为BO(Business Object)、VO(View Object)与TO(Transfer Object),VO 经常出现在 view 和 cell,BO一般仅在各业务子库内部使用,这个TO则是需要放在平台库是用于各个组件间的通信的通用模型。例如:通用 PoiDomain,通用 OrderDomain,通用 AddressDomain 等等。这些称为 TO 的 Domain 可以作为规范接口的入参类型或返回值类型。在实际业务场景中,跳转页面时传递 Domain 的需求也是一个老生常谈的问题,大多数页面级跳转框架仅支持传递基本数据类型(也有 trick 的方式传 Domain 内存地址但很不优雅)。在有了上文支持的能力,我们可以在规范接口内通过万能接口获取目标页面的VC,并调用其某个属性的 set 方法将我们想传递的Domain赋值过去,然后将这个 VC 对象作为返回值返回。调用方获得这个 VC 后在当前的导航栈内push即可。上文代码中我们用 WMScheduler 调用了 Akit 的一个名为calculateOrderedFoodCount WithPoiID:的方法。那么有个争议点:在组件通信需要调用某方法时,是允许直接调用现有方法,还是复制一份加上前缀标注此方法专门用于提供组件通信? 前者的问题点在于现有方法可能会被修改,扩充参数会直接导致调用方找不到方法,Method 字符串的不会编译报错(上文平台代码 WMScheduler+AKit.m 中第31行)。后者的问题在于大大增加了开发成本。权衡后我们还是使用了前者,加了些特殊处理,若现有方法被修改了,则会在isReponseForSelector这里检查出来,并走到 else 的断言及时发现。阶段总结:Category+NSInvocation 方案的优点是便捷,因为 Category 的专用接口放在平台库,以后有除了 BKit 以外的其他调用方也可以直接调用,还有更多强大的功能。但是,不优雅的地方我们也列举一下:当这个跨组件方法内部的代码行数比较多时,会写很多硬编码。硬编码method字符串,在现有方法被修改时,编译检测不报错(只能靠断言约束)。下层库向上调用的设计会被诟病。接下来介绍的 CategoryCoverOrigin 的方案,可以解决这三个问题。2.CategoryCoverOrigin方案方案简介:首先说明下这个方案和 NSInvocation 没有任何关系,此方案与上一方案也是完全不同的两个概念,不要将上一个方案的思维带到这里。此方案的思路是在平台层的 WMScheduler.h 提供接口方法,接口的实现只写空实现或者兜底实现(兜底实现中可根据业务场景在 Debug 环境下增加 toast 提示或断言),上层库的提供方实现接口方法并通过 Category 的特性,在运行时进行对基类同名方法的替换。调用方则正常调用平台层提供的接口。在 CategoryCoverOrigin 的方案中 WMScheduler 的 Category 在提供方仓库内部,因此业务逻辑的依赖可以在仓库内部使用常规的OC调用。UML图:从图4-1可以看出,WMScheduler 的 Category 被移到了业务仓库,并且 WMScheduler 中有所有接口的全集。为了更好地理解 CategoryCover 实际应用,笔者再贴一个此方案下的完整完整代码:如图4-2,在这种方案下,“B库调用A库方法”的需求需要修改三个仓库的代码,但除了这四个编辑的文件,没有其他任何的依赖了,请仔细看下代码示例。示例代码:平台(通用功能库)两个文件①// WMScheduler.h@interface WMScheduler : NSObject// 这个文件是所有组件通信方法的汇总#pragma mark - AKit / * 通过商家id查到当前购物车已选e的小红点数量 * @param poiid 商家id * @return 实际的小红点数量 */+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID;#pragma mark - CKit// …#pragma mark - DKit// …@end②// WMScheduler.m#import “WMScheduler.h”@implementation WMScheduler#pragma mark - Akit+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID{ return 0; // 这个.m里只要求一个空实现 作为兜底方案。}#pragma mark - Ckit// …#pragma mark - Dkit// …@endAKit(提供方)一个 Category 文件:// WMScheduler+AKit.m#import “WMScheduler.h”#import “WMAKitBusinessManager.h”#import “WMXXXSingleton.h” // 直接导入了很多AKit相关的业务文件,因为本身就在AKit仓库内@implementation WMScheduler (AKit)// 这个宏可以屏蔽分类覆盖基类方法的警告#pragma clang diagnostic push#pragma clang diagnostic ignored “-Wobjc-protocol-method-implementation”// 在平台层写过的方法,这边是是自动补全的+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID{ if (nil == poiid) { return 0; } // 所有AKIT相关的类都能直接接口调用,不需要任何硬编码,可以和之前的写法对比下。 WMXXXSingleton *singleton = [WMXXXSingleton sharedInstance]; NSNumber *orderFoodCount = [singleton calculateOrderedFoodCountWithPoiID:poiID]; return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];}#pragma clang diagnostic pop@endBKit(调用方) 一个文件写法不变:// WMHomeVC.m@interface WMHomeVC () <UITableViewDataSource, UITableViewDelegate>@end@implementation WMHomeVC… NSUInteger *foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID]; NSLog(@"%ld",foodCount);…@end代码分析:CategoryCoverOrigin 的方式,平台库用 WMScheduler.h 文件存放所有的组件通信接口的汇总,各个仓库用注释隔开,并在.m文件中编写了空实现。功能代码编写在服务提供方仓库的 WMScheduler+AKit.m,看这个文件的17、18行业务逻辑是使用常规 OC 接口调用。在运行时此Category的方法会覆盖 WMScheduler.h 基类中的同名方法,从而达到目的。CategoryCoverOrigin 方式不需要其他功能类的支撑。延伸思考:如果业务库很多,方法很多,会不会出现 WMScheduler.h 爆炸? 目前我们的工程跨组件调用的实际场景不是很多,所以汇总在一个文件了,如果满屏都是跨组件调用的工程,则需要思考业务架构与模块划分是否合理这一问题。当然,如果真出现 WMScheduler.h 爆炸的情况,完全可以将各个业务的接口移至自己Category 的.h文件中,然后创建一个 WMSchedulerInterfaceList 文件统一 import 这些 Category。两种方案的选择刚才我们对于 Category+NSInvocation 和 CategoryCoverOrigin 两种方式都做了详细的介绍,我们再整理一下两者的优缺点对比:Category+NSInvocationCategoryCover优点只改两个仓库,流程上的时间成本更少可以实现url调用方法(scheme://target/method:?para=x)无任何硬编码,常规OC接口调用除了接口声明、分类覆盖、调用,没有其他多余代码不存在下层调用上层的场景缺点功能复杂时硬编码写法成本较大下层调上层,上层业务改变时会影响平台接口不能使用url调用方法新增接口时需改动三个仓库,稍有麻烦。(当接口已存在时,两种方式都只需修改一处)笔者更建议使用 CategoryCoverOrigin 的无硬编码的方案,当然具体也要看项目的实际场景,从而做出最优的选择。更多建议关于组件对外提供的接口,我们更倾向于借鉴 SPI 的思想,作为一个 Kit 哪些功能是需要对外公开的?提供哪些服务给其他方解耦调用?建议主动开放核心方法,尽量减少“用到才补”的场景。例如全局购物车就需要“提供获取小红点数量的方法”,商家中心就需要提供“根据字符串 id 得到整个 Poi 的 Domain”的接口服务。需要考虑到抽象能力,提供更有泛用性的接口。比如“获取到了最低满减价格后拼接成一个文案返回字符串” 这个方法,就没有“获取到了最低满减价格” 这个方法具备泛用性。Category 风险管控先举两个发生过的案例1. 2017年10月 一个关于NSDate重复覆盖的问题当时美团平台有 NSDate+MTAddition 类,在外卖侧有 NSDate+WMAddition 类。前者 NSDate+MTAddition 之前就有方法 getCurrentTimestamp,返回的时间戳是秒。后者 NSDate+WMAddition 在一次需求中也增加了 getCurrentTimestamp 方法,但是为了和其他平台统一口径返回值使用了毫秒。在正常的加载顺序中外卖类比平台类要晚,因此在外卖的测试中没有发现问题。但集成到 imeituan 主项目之后,原先其他业务方调用这个返回“秒”的方法,就被外卖测的返回“毫秒”的同名方法给覆盖了,出现接口错误和UI错乱等问题。2. 2018年3月 一个WMScheduler组件通信遇到的问题在外卖侧有订单组件和商家容器组件,这两个组件的联系是十分紧密的,有的功能放在两个仓库任意一个中都说的通。因此出现了了两个仓库写了同名方法的场景。在 WMScheduler+Restaurant 和 WMScheduler+Order 两个仓库都添加了方法 -(void)wms_enterGlobalCartPageFromPage:,在运行中这两处有一处被覆盖。在有一次 Bug 解决中,给其中一处增加了异常处理的代码,恰巧增加的这处先加载,就被后加载的同名方法覆盖了,这就导致了异常处理代码不生效的问题。那么使用 CategoryCover 的方式是不是很不安全? NO!只要弄清其中的规律,风险点都是完全可以管控的,接下来,我们来分析 Category 的覆盖原理。Category 方法覆盖原理1) Category 的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category 和原来类都有methodA,那么 Category 附加完成之后,类的方法列表里会有两个 methodA。2) Category 方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category 的方法会“覆盖”掉原来类的同名方法,这是因为运行过程中,我们在查找方法的时候会顺着方法列表的顺序去查找,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。Category 在运行期进行决议,而基类的类是在编译期进行决议,因此分类中,方法的加载顺序一定在基类之后。美团曾经有一篇技术博客深入分析了 Category,并且从编译器和源码的角度对分类覆盖操作进行详细解析:深入理解Objective-C:Category根据方法覆盖的原理,我们可以分析出哪些操作比较安全,哪些存在风险,并针对性地进行管理。接下来,我们就介绍美团 Category 管理相关的一些工作。Category 方法管理由于历史原因,不管是什么样的管理规则,都无法直接“一刀切”。所以针对现状,我们将整个管理环节先拆分为“数据”、“场景”、 “策略”三部分。其中数据层负责发现异常数据,所有策略公用一个数据层。针对 Category 方法的数据获取,我们有如下几种方式:根据优缺点的分析,再考虑到美团已经彻底实现了“组件化”的工程,所以对 Category 的管控最好放在集成阶段以后进行。我们最终选择了使用 linkmap 进行数据获取,具体方法我们将在下文进行介绍。策略部分则针对不同的场景异常进行控制,主要的开发工作位于我们的组件化 CI 系统上,即之前介绍过的 Hyperloop 系统。Hyperloop 本身即提供了包括白名单,发布集成流程管理等一系列策略功能,我们只需要将工具进行关联开发即可。我们开发的数据层作为一个独立组件,最终也是运行在 Hyperloop 上。根据场景细分的策略如下表所示(需要注意的是,表中有的场景实际不存在,只是为了思考的严谨列出):我们在前文描述的 CategoryCoverOrigin 的组件通信方案的管控体现在第2点。风险管控中提到的两个案例的管控主要体现在第4点。Category 数据获取原理上一章节,我们提到了采用 linkmap 分析的方式进行 Category 数据获取。在这一章节内,我们详细介绍下做法。启用 linkmap首先,linkmap 生成功能是默认关闭的,我们需要在 build settings 内手动打开开关并配置存储路径。对于美团工程和美团外卖工程来说,每次正式构建后产生的 linkmap,我们还会通过内部的美团云存储工具进行持久化的存储,保证后续的可追溯。linkmap 组成若要解析 linkmap,首先需要了解 linkmap 的组成。如名称所示,linkmap 文件生成于代码链接之后,主要由4个部分组成:基本信息、Object files 表、Sections 表和 Symbols 表。前两行是基本信息,包括链接完成的二进制路径和架构。如果一个工程内有多个最终产物(如 Watch App 或 Extension),则经过配置后,每一个产物的每一种架构都会生成一份 linkmap。# Path: /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.noindex/ArchiveIntermediates/imeituan/InstallationBuildProductsLocation/Applications/imeituan.app/imeituan# Arch: arm64第二部分的 Object files,列举了链接所用到的所有的目标文件,包括代码编译出来的,静态链接库内的和动态链接库(如系统库),并且给每一个目标文件分配了一个 file id。# Object files:[ 0] linker synthesized[ 1] dtrace[ 2] /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.noindex/ArchiveIntermediates/imeituan/IntermediateBuildFilesPath/imeituan.build/DailyBuild-iphoneos/imeituan.build/Objects-normal/arm64/main.o……[ 26] /private/var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/repo-sandbox/imeituan/Pods/AFNetworking/bin/libAFNetworking.a(AFHTTPRequestOperation.o)……[25919] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/usr/lib/libobjc.tbd[25920] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/usr/lib/libSystem.tbd第三部分的 Sections,记录了所有的 Section,以及它们所属的 Segment 和大小等信息。# Sections:# Address Size Segment Section0x100004450 0x07A8A8D0 __TEXT __text……0x109EA52C0 0x002580A0 __DATA __objc_data0x10A0FD360 0x001D8570 __DATA __data0x10A2D58D0 0x0000B960 __DATA __objc_k_kylin……0x10BFE4E5D 0x004CBE63 __RODATA __objc_methname0x10C4B0CC0 0x000D560B __RODATA __objc_classname第四部分的 Symbols 是重头戏,列举了所有符号的信息,包括所属的 object file、大小等。符号除了我们关注的 OC 的方法、类名、协议名等,也包含 block、literal string 等,可以供其他需求分析进行使用。# Symbols:# Address Size File Name0x1000045B8 0x00000060 [ 2] ___llvm_gcov_writeout0x100004618 0x00000028 [ 2] ___llvm_gcov_flush0x100004640 0x00000014 [ 2] ___llvm_gcov_init0x100004654 0x00000014 [ 2] ___llvm_gcov_init.40x100004668 0x00000014 [ 2] ___llvm_gcov_init.60x10000467C 0x0000015C [ 3] _main……0x10002F56C 0x00000028 [ 38] -[UIButton(_AFNetworking) af_imageRequestOperationForState:]0x10002F594 0x0000002C [ 38] -[UIButton(_AFNetworking) af_setImageRequestOperation:forState:]0x10002F5C0 0x00000028 [ 38] -[UIButton(_AFNetworking) af_backgroundImageRequestOperationForState:]0x10002F5E8 0x0000002C [ 38] -[UIButton(_AFNetworking) af_setBackgroundImageRequestOperation:forState:]0x10002F614 0x0000006C [ 38] +[UIButton(AFNetworking) sharedImageCache]0x10002F680 0x00000010 [ 38] +[UIButton(AFNetworking) setSharedImageCache:]0x10002F690 0x00000084 [ 38] -[UIButton(AFNetworking) imageResponseSerializer]……linkmap 数据化根据上文的分析,在理解了 linkmap 的格式后,通过简单的文本分析即可提取数据。由于美团内部 iOS 开发工具链统一采用 Ruby,所以 linkmap 分析也采用 Ruby 开发,整个解析器被封装成一个 Ruby Gem。具体实施上,处于通用性考虑,我们的 linkmap 解析工具分为解析、模型、解析器三层,每一层都可以单独进行扩展。对于 Category 分析器来说,link map parser 解析指定 linkmap,生成通用模型的实例。从实例中获取 symbol 类,将名字中有“()”的符号过滤出来,即为 Category 方法。接下来只要按照方法名聚合,如果超过1个则肯定有 Category 方法冲突的情况。按照上一节中分析的场景,分析其具体冲突类型,提供结论输出给 Hyperloop。具体对外接口可以直接参考我们的工具测试用例。最后该 Gem 会直接被 Hyperloop 使用。 it ‘should return a map with keys for method name and classify’ do @parser = LinkmapParser::Parser.new @file_path = ‘spec/fixtures/imeituan-LinkMap-normal-arm64.txt’ @analyze_result_with_classification = @parser.parse @file_path expect(@analyze_result_with_classification.class).to eq(Hash) # Category 方法互相冲突 symbol = @analyze_result_with_classification["-[NSDate isEqualToDateDay:]"] expect(symbol.class).to eq(Hash) expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::CONFLICT]) expect(symbol[:detail].class).to eq(Array) expect(symbol[:detail].count).to eq(3) # Category 方法覆盖原方法 symbol = @analyze_result_with_classification["-[UGCReviewManager setCommonConfig:]"] expect(symbol.class).to eq(Hash) expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::REPLACE]) expect(symbol[:detail].class).to eq(Array) expect(symbol[:detail].count).to eq(2) endCategory 方法管理总结1. 风险管理对于任何语法工具,都是有利有弊的。所以除了发掘它们在实际场景中的应用,也要时刻对它们可能带来的风险保持警惕,并选择合适的工具和时机来管理风险。而 Xcode 本身提供了不少的工具和时机,可以供我们分析构建过程和产物。若是在日常工作中遇到一些坑,不妨从构建期工具的角度去考虑管理。比如本文内提到的 linkmap,不仅可以用于 Category 分析,还可以用于二进制大小分析、组件信息管理等。投入一定资源在相关工具开发上,往往可以获得事半功倍的效果。2. 代码规范回到 Category 的使用,除了工具上的管控,我们也有相应的代码规范,从源头管理风险。如我们在规范中要求所有的 Category 方法都使用前缀,降低无意冲突的可能。并且我们也计划把“使用前缀”做成管控之一。3. 后续规划1.覆盖系统方法检查 由于目前在管控体系内暂时没有引入系统符号表,所以无法对覆盖系统方法的行为进行分析和拦截。我们计划后续和 Crash 分析系统打通符号表体系,提早发现对系统库的不当覆盖。2.工具复用 当前的管控系统仅针对美团外卖和美团 App,未来计划推广到其他 App。由于有 Hyperloop,事情在技术上并没有太大的难度。 从工具本身的角度看,我们有计划在合适的时机对数据层代码进行开源,希望能对更多的开发有所帮助。总结在这篇文章中,我们从具体的业务场景入手,总结了组件间调用的通用模型,并对常用的解耦方案进行了分析对比,最终选择了目前最适合我们业务场景的方案。即通过 Category 覆盖的方式实现了依赖倒置,将构建时依赖延后到了运行时,达到我们预期的解耦目标。同时针对该方案潜在的问题,通过 linkmap 工具管控的方式进行规避。另外,我们在模型设计时也提到,组件间解耦其实在 iOS 侧有多种方案选择。对于其他的方案实践,我们也会陆续和大家分享。希望我们的工作能对大家的 iOS 开发组件间解耦工作有所启发。作者简介尚先,美团资深工程师。2015年加入美团,目前作为美团外卖 iOS 端平台化虚拟小组组长,主要负责业务架构、持续集成和工程化相关工作。同时也是移动端领域新技术的爱好者,负责多项新技术在外卖业务落地中的难点攻关,目前个人拥有七项国家发明专利。泽响,美团技术专家,2014年加入美团,先后负责过公司 iOS 持续集成体系建设,美团 iOS 端平台业务,美团 iOS 端基础业务等工作。目前作为美团移动平台架构平台组 Team Leader,主要负责美团 App 平台架构、组件化、研发流程优化和部分基础设施建设,致力于提升平台上全业务的研发效率与质量。招聘信息美团外卖长期招聘 iOS、Android、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同学投递简历到 chenhang03@meituan.com。 ...

November 9, 2018 · 4 min · jiezi

浅谈大型互联网企业入侵检测及防护策略

前言如何知道自己所在的企业是否被入侵了?是没人来“黑”,还是因自身感知能力不足,暂时还无法发现?其实,入侵检测是每一个大型互联网企业都要面对的严峻挑战。价值越高的公司,面临入侵的威胁也越大,即便是Yahoo这样的互联网鼻祖,在落幕(被收购)时仍遭遇全量数据失窃的事情。安全无小事,一旦互联网公司被成功“入侵”,其后果将不堪想象。基于“攻防对抗”的考量,本文不会提及具体的入侵检测模型、算法和策略,那些希望直接照搬“入侵策略”的同学可能会感到失望。但是我们会将一部分运营思路分享出来,请各位同行指点,如能对后来者起到帮助的作用,那就更好了,也欢迎大家跟我们交流探讨。入侵的定义典型的入侵场景:黑客在很远的地方,通过网络远程控制目标的笔记本电脑/手机/服务器/网络设备,进而随意地读取目标的隐私数据,又或者使用目标系统上的功能,包括但不限于使用手机的麦克风监听目标,使用摄像头偷窥监控目标,使用目标设备的计算能力挖矿,使用目标设备的网络能力发动DDoS攻击等等。亦或是破解了一个服务的密码,进去查看敏感资料、控制门禁/红绿灯。以上这些都属于经典的入侵场景。我们可以给入侵下一个定义:就是黑客在未经授权的情况下,控制、使用我方资源(包括但不限于读写数据、执行命令、控制资源等)达到各种目的。从广义上讲,黑客利用SQL注入漏洞窃取数据,或者拿到了目标域名在ISP中的帐号密码,以篡改DNS指向一个黑页,又或者找到了目标的社交帐号,在微博/QQ/邮箱上,对虚拟资产进行非授权的控制,都属于入侵的范畴。针对企业的入侵检测企业入侵检测的范围,多数情况下比较狭义:一般特指黑客对PC、系统、服务器、网络(包括办公网、生产网)控制的行为。黑客对PC、服务器等主机资产的控制,最常见的方法是通过Shell去执行指令,获得Shell的这个动作叫做GetShell。比如通过Web服务的上传漏洞,拿到WebShell,或者利用RCE漏洞直接执行命令/代码(RCE环境变相的提供了一个Shell)。另外,通过某种方式先植入“木马后门”,后续直接利用木马集成的SHELL功能对目标远程控制,这个也比较典型。因此,入侵检测可以重点关注GetShell这个动作,以及GetShell成功之后的恶意行为(为了扩大战果,黑客多半会利用Shell进行探测、翻找窃取、横向移动攻击其它内部目标,这些区别于好人的特性也可以作为重要的特征)。有一些同行(包括商业产品),喜欢报告GetShell之前的一些“外部扫描、攻击探测和尝试行为”,并美其名曰“态势感知”,告诉企业有人正在“试图攻击”。在笔者看来,实战价值并不大。包括美团在内的很多企业,基本上无时无刻都在遭受“不明身份”的攻击,知道了有人在“尝试”攻击,如果并不能有效地去行动,无法有效地对行动进行告警,除了耗费心力之外,并没有太大的实际价值。当我们习惯“攻击”是常态之后,就会在这样的常态下去解决问题,可以使用什么加固策略,哪些可以实现常态化的运营,如果有什么策略无法常态化运营,比如需要很多人加班临时突击守着,那这个策略多半在不久之后就会逐渐消逝掉。跟我们做不做这个策略,并没有本质上的区别。类似于SQL注入、XSS等一些不直接GetShell的Web攻击,暂时不在狭义的“入侵检测”考虑范围,建议可以划入“漏洞”、“威胁感知”等领域,另行再做探讨。当然,利用SQL注入、XSS等入口,进行了GetShell操作的,我们仍抓GetShell这个关键点,不必在乎漏洞入口在何处。“入侵”和“内鬼”与入侵接近的一种场景是“内鬼”。入侵本身是手段,GetShell只是起点,黑客GetShell的目标是为了之后对资源的控制和数据的窃取。而“内鬼”天然拥有合法的权限,可以合法接触敏感资产,但是基于工作以外的目的,他们对这些资源进行非法的处置,包括拷贝副本、转移外泄、篡改数据牟利等。内鬼的行为不在“入侵检测”的范畴,一般从内部风险控制的视角进行管理和审计,比如职责分离、双人审计等。也有数据防泄密产品(DLP)对其进行辅助,这里不展开细说。有时候,黑客知道员工A有权限接触目标资产,便定向攻击A,再利用A的权限把数据窃取走,也定性为“入侵”。毕竟A不是主观恶意的“内鬼”。如果不能在黑客攻击A的那一刻捕获,或者无法区分黑客控制的A窃取数据和正常员工A的访问数据,那这个入侵检测也是失败的。入侵检测的本质前文已经讲过,入侵就是黑客可以不经过我们的同意,来操作我们的资产,在手段上并没有任何的限制。那么如何找出入侵行为和合法正常行为的区别,将其跟合法行为进行分开,就是“入侵发现”。在算法模型上,这算是一个标记问题(入侵、非入侵)。可惜的是,入侵这种动作的“黑”样本特别稀少,想通过大量的带标签的数据,有监督的训练入侵检测模型,找出入侵的规律比较难。因此,入侵检测策略开发人员,往往需要投入大量的时间,去提炼更精准的表达模型,或者花更多的精力去构造“类似入侵”的模拟数据。一个经典的例子是,为了检测出WebShell,安全从业人员可以去GitHub上搜索一些公开的WebShell样本,数量大约不到1000个。而对于机器学习动辄百万级的训练需求,这些数据远远不够。况且GitHub上的这些样本集,从技术手法上来看,有单一技术手法生成的大量类似样本,也有一些对抗的手法样本缺失。因此,这样的训练,试图让AI去通过“大量的样本”掌握WebShell的特征并区分出它们,原则上不太可能完美地去实现。此时,针对已知样本做技术分类,提炼更精准的表达模型,被称为传统的特征工程。而传统的特征工程往往被视为效率低下的重复劳动,但效果往往比较稳定,毕竟加一个技术特征就可以稳定发现一类WebShell。而构造大量的恶意样本,虽然有机器学习、AI等光环加持,但在实际环境中往往难以获得成功:自动生成的样本很难描述WebShell本来的含义,多半描述的是自动生成的算法特征。另一个方面,入侵的区别是看行为本身是否“授权”,而授权与否本身是没有任何显著的区分特征的。因此,做入侵对抗的时候,如果能够通过某种加固,将合法的访问收敛到有限的通道,并且给该通道做出强有力的区分,也就能大大的降低入侵检测的成本。例如,对访问来源进行严格的认证,无论是自然人,还是程序API,都要求持有合法票据,而派发票据时,针对不同情况做多纬度的认证和授权,再用IAM针对这些票据记录和监控它们可以访问的范围,还能产生更底层的Log做异常访问模型感知。这个全生命周期的风控模型,也是Google的BeyondCorp无边界网络得以实施的前提和基础。因此,入侵检测的主要思路也就有2种:根据黑特征进行模式匹配(例如WebShell关键字匹配)。根据业务历史行为(生成基线模型),对入侵行为做异常对比(非白既黑),如果业务的历史行为不够收敛,就用加固的手段对其进行收敛,再挑出不合规的小众异常行为。入侵检测与攻击向量根据目标不同,可能暴露给黑客的攻击面会不同,黑客可能采用的入侵手法也就完全不同。比如,入侵我们的PC/笔记本电脑,还有入侵部署在机房/云上的服务器,攻击和防御的方法都有挺大的区别。针对一个明确的“目标”,它被访问的渠道可能是有限集,被攻击的必经路径也有限。“攻击方法”+“目标的攻击面”的组合,被称为“攻击向量”。因此,谈入侵检测模型效果时,需要先明确攻击向量,针对不同的攻击路径,采集对应的日志(数据),才可能做对应的检测模型。比如,基于SSH登录后的Shell命令数据集,是不能用于检测WebShell的行为。而基于网络流量采集的数据,也不可能感知黑客是否在SSH后的Shell环境中执行了什么命令。基于此,如果有企业不提具体的场景,就说做好了APT感知模型,显然就是在“吹嘘”了。所以,入侵检测得先把各类攻击向量罗列出来,每一个细分场景分别采集数据(HIDS+NIDS+WAF+RASP+应用层日志+系统日志+PC……),再结合公司的实际数据特性,作出适应公司实际情况的对应检测模型。不同公司的技术栈、数据规模、暴露的攻击面,都会对模型产生重大的影响。比如很多安全工作者特别擅长PHP下的WebShell检测,但是到了一个Java系的公司……常见的入侵手法与应对如果对黑客的常见入侵手法理解不足,就很难有的放矢,有时候甚至会陷入“政治正确”的陷阱里。比如渗透测试团队说,我们做了A动作,你们竟然没有发现,所以你们不行。而实际情况是,该场景可能不是一个完备的入侵链条,就算不发现该动作,对入侵检测效果可能也没有什么影响。每一个攻击向量对公司造成的危害,发生的概率如何进行排序,解决它耗费的成本和带来的收益如何,都需要有专业经验来做支撑与决策。现在简单介绍一下,黑客入侵教程里的经典流程(完整过程可以参考杀伤链模型):入侵一个目标之前,黑客对该目标可能还不够了解,所以第一件事往往是“踩点”,也就是搜集信息,加深了解。比如,黑客需要知道,目标有哪些资产(域名、IP、服务),它们各自的状态如何,是否存在已知的漏洞,管理他们的人有谁(以及如何合法的管理的),存在哪些已知的泄漏信息(比如社工库里的密码等)……一旦踩点完成,熟练的黑客就会针对各种资产的特性,酝酿和逐个验证“攻击向量”的可行性,下文列举了常见的攻击方式和防御建议。高危服务入侵所有的公共服务都是“高危服务”,因为该协议或者实现该协议的开源组件,可能存在已知的攻击方法(高级的攻击者甚至拥有对应的0day),只要你的价值足够高,黑客有足够的动力和资源去挖掘,那么当你把高危服务开启到互联网,面向所有人都打开的那一刻,就相当于为黑客打开了“大门”。比如SSH、RDP这些运维管理相关的服务,是设计给管理员用的,只要知道密码/秘钥,任何人都能登录到服务器端,进而完成入侵。而黑客可能通过猜解密码(结合社工库的信息泄露、网盘检索或者暴力破解),获得凭据。事实上这类攻击由于过于常见,黑客早就做成了全自动化的全互联网扫描的蠕虫类工具,云上购买的一个主机如果设置了一个弱口令,往往在几分钟内就会感染蠕虫病毒,就是因为这类自动化的攻击者实在是太多了。或许,你的密码设置得非常强壮,但是这并不是你可以把该服务继续暴露在互联网的理由,我们应该把这些端口限制好,只允许自己的IP(或者内部的堡垒主机)访问,彻底断掉黑客通过它入侵我们的可能。与此类似的,MySQL、Redis、FTP、SMTP、MSSQL、Rsync等等,凡是自己用来管理服务器或者数据库、文件的服务,都不应该针对互联网无限制的开放。否则,蠕虫化的攻击工具会在短短几分钟内攻破我们的服务,甚至直接加密我们的数据,甚至要求我们支付比特币,进行敲诈勒索。还有一些高危服务存在RCE漏洞(远程命令执行),只要端口开放,黑客就能利用现成的exploit,直接GetShell,完成入侵。防御建议: 针对每一个高危服务做入侵检测的成本较高,因为高危服务的具体所指非常的多,不一定存在通用的特征。所以,通过加固方式,收敛攻击入口性价比更高。禁止所有高危端口对互联网开放可能,这样能够减少90%以上的入侵概率。Web入侵随着高危端口的加固,黑客知识库里的攻击手法很多都会失效了。但是Web服务是现代互联网公司的主要服务形式,不可能都关掉。于是,基于PHP、Java、ASP、ASP.NET、Node、C写的CGI等等动态的Web服务漏洞,就变成了黑客入侵的最主要入口。比如,利用上传功能直接上传一个WebShell,利用文件包含功能,直接引用执行一个远程的WebShell(或者代码),然后利用代码执行的功能,直接当作Shell的入口执行任意命令,解析一些图片、视频的服务,上传一个恶意的样本,触发解析库的漏洞……Web服务下的应用安全是一个专门的领域(道哥还专门写了本《白帽子讲Web安全》),具体的攻防场景和对抗已经发展得非常成熟了。当然,由于它们都是由Web服务做为入口,所以入侵行为也会存在某种意义上的共性。相对而言,我们比较容易能够找到黑客GetShell和正常业务行为的一些区别。针对Web服务的入侵痕迹检测,可以考虑采集WAF日志、Access Log、Auditd记录的系统调用,或者Shell指令,以及网络层面Response相关的数据,提炼出被攻击成功的特征,建议我们将主要的精力放在这些方面。0day入侵通过泄漏的工具包来看,早些年NSA是拥有直接攻击Apache、Nginx这些服务的0day武器的。这意味着对手很可能完全不用在乎我们的代码和服务写成什么样,拿0day一打,神不知鬼不觉就GetShell了。但是对于入侵检测而言,这并不可怕:因为无论对手利用什么漏洞当入口,它所使用的Shellcode和之后的行为本身依然有共性。Apache存在0day漏洞被攻击,还是一个PHP页面存在低级的代码漏洞被利用,从入侵的行为上来看,说不定是完全一样的,入侵检测模型还可以通用。所以,把精力聚焦在有黑客GetShell入口和之后的行为上,可能比关注漏洞入口更有价值。当然,具体的漏洞利用还是要实际跟进,然后验证其行为是否符合预期。办公终端入侵绝大多数APT报告里,黑客是先对人(办公终端)下手,比如发个邮件,哄骗我们打开后,控制我们的PC,再进行长期的观察/翻阅,拿到我们的合法凭据后,再到内网漫游。所以这些报告,多数集中在描述黑客用的木马行为以及家族代码相似度上。而反APT的产品、解决方案,多数也是在办公终端的系统调用层面,用类似的方法,检验“免杀木马”的行为。因此,EDR类的产品+邮件安全网关+办公网出口的行为审计+APT产品的沙箱等,联合起来,可以采集到对应的数据,并作出相似的入侵检测感知模型。而最重要的一点,是黑客喜欢关注内部的重要基础设施,包括但不限于AD域控、邮件服务器、密码管理系统、权限管理系统等,一旦拿下,就相当于成为了内网的“上帝”,可以为所欲为。所以对公司来说,重要基础设施要有针对性的攻防加固讨论,微软针对AD的攻防甚至还发过专门的加固白皮书。入侵检测基本原则不能把每一条告警都彻底跟进的模型,等同于无效模型。入侵发生后,再辩解之前其实有告警,只是太多了没跟过来/没查彻底,这是“马后炮”,等同于不具备发现能力,所以对于日均告警成千上万的产品,安全运营人员往往表示很无奈。我们必须屏蔽一些重复发生的相似告警,以集中精力把每一个告警都闭环掉。这会产生白名单,也就是漏报,因此模型的漏报是不可避免的。由于任何模型都会存在漏报,所以我们必须在多个纬度上做多个模型,形成关联和纵深。假设WebShell静态文本分析被黑客变形绕过了,在RASP(运行时环境)的恶意调用还可以进行监控,这样可以选择接受单个模型的漏报,但在整体上仍然具备发现能力。既然每一个单一场景的模型都有误报漏报,我们做什么场景,不做什么场景,就需要考虑“性价比”。比如某些变形的WebShell可以写成跟业务代码非常相似,人的肉眼几乎无法识别,再追求一定要在文本分析上进行对抗,就是性价比很差的决策。如果通过RASP的检测方案,其性价比更高一些,也更具可行性一些。我们不太容易知道黑客所有的攻击手法,也不太可能针对每一种手法都建设策略(考虑到资源总是稀缺的)。所以针对重点业务,需要可以通过加固的方式(还需要常态化监控加固的有效性),让黑客能攻击的路径极度收敛,仅在关键环节进行对抗。起码能针对核心业务具备兜底的保护能力。基于上述几个原则,我们可以知道一个事实,或许我们永远不可能在单点上做到100%发现入侵,但是我们可以通过一些组合方式,让攻击者很难绕过所有的点。当老板或者蓝军挑战,某个单点的检测能力有缺失时,如果为了“政治正确”,在这个单点上进行无止境的投入,试图把单点做到100%能发现的能力,很多时候可能只是在试图制造一个“永动机”,纯粹浪费人力、资源,而不产生实际的收益。将节省下来的资源,高性价比的布置更多的纵深防御链条,效果显然会更好。入侵检测产品的主流形态入侵检测终究是要基于数据去建模,比如针对WebShell的检测,首先要识别Web目录,再对Web目录下的文件进行文本分析,这需要做一个采集器。基于Shell命令的入侵检测模型,需要获取所有Shell命令,这可能要Hook系统调用或者劫持Shell。基于网络IP信誉、流量payload进行检测,或者基于邮件网关对内容的检查,可能要植入网络边界中,对流量进行旁路采集。也有一些集大成者,基于多个Sensor,将各方日志进行采集后,汇总在一个SOC或者SIEM,再交由大数据平台进行综合分析。因此,业界的入侵检测相关的产品大致上就分成了以下的形态:主机Agent类:黑客攻击了主机后,在主机上进行的动作,可能会产生日志、进程、命令、网络等痕迹,那么在主机上部署一个采集器(也内含一部分检测规则),就叫做基于主机的入侵检测系统,简称HIDS。典型的产品:OSSEC、青藤云、安骑士、安全狗,Google最近也发布了一个Alpha版本的类似产品 Cloud Security Command Center。当然,一些APT厂商,往往也有在主机上的Sensor/Agent,比如FireEye等。网络检测类:由于多数攻击向量是会通过网络对目标投放一些payload,或者控制目标的协议本身具备强特征,因此在网络层面具备识别的优势。典型的产品:Snort到商业的各种NIDS/NIPS,对应到APT级别,则还有类似于FireEye的NX之类的产品。日志集中存储分析类:这一类产品允许主机、网络设备、应用都输出各自的日志,集中到一个统一的后台,在这个后台,对各类日志进行综合的分析,判断是否可以关联的把一个入侵行为的多个路径刻画出来。例如A主机的的Web访问日志里显示遭到了扫描和攻击尝试,继而主机层面多了一个陌生的进程和网络连接,最后A主机对内网其它主机进行了横向渗透尝试。典型的产品:LogRhythm、Splunk等SIEM类产品。APT沙箱:沙箱类产品更接近于一个云端版的高级杀毒软件,通过模拟执行观测行为,以对抗未知样本弱特征的特点。只不过它需要一个模拟运行的过程,性能开销较大,早期被认为是“性价比不高”的解决方案,但由于恶意文件在行为上的隐藏要难于特征上的对抗,因此现在也成为了APT产品的核心组件。通过网络流量、终端采集、服务器可疑样本提取、邮件附件提炼等拿到的未知样本,都可以提交到沙箱里跑一下行为,判断是否恶意。典型产品:FireEye、Palo Alto、Symantec、微步。终端入侵检测产品:移动端目前还没有实际的产品,也不太有必要。PC端首先必备的是杀毒软件,如果能够检测到恶意程序,一定程度上能够避免入侵。但是如果碰到免杀的高级0day和木马,杀毒软件可能会被绕过。借鉴服务器上HIDS的思路,也诞生了EDR的概念,主机除了有本地逻辑之外,更重要的是会采集更多的数据到后端,在后端进行综合分析和联动。也有人说下一代杀毒软件里都会带上EDR的能力,只不过目前销售还是分开在卖。典型产品:杀毒软件有Bit9、SEP、赛门铁克、卡巴斯基、McAfee ;EDR产品不枚举了,腾讯的iOA、阿里的阿里郎,一定程度上都是可以充当类似的角色;入侵检测效果评价指标首先,主动发现的入侵案例/所有入侵 = 主动发现率。这个指标一定是最直观的。比较麻烦的是分母,很多真实发生的入侵,如果外部不反馈,我们又没检测到,它就不会出现在分母里,所以有效发现率总是虚高的,谁能保证当前所有的入侵都发现了呢?(但是实际上,只要入侵次数足够多,不管是SRC收到的情报,还是“暗网”上报出来的一个大新闻,把客观上已经知悉的入侵列入分母,总还是能计算出一个主动发现率的。)另外,真实的入侵其实是一个低频行为,大型的互联网企业如果一年到头成百上千的被入侵,肯定也不正常。因此,如果很久没出现真实入侵案例,这个指标长期不变化,也无法刻画入侵检测能力是否在提升。所以,我们一般还会引入两个指标来观测:蓝军对抗主动发现率已知场景覆盖率蓝军主动高频对抗和演习,可以弥补真实入侵事件低频的不足,但是由于蓝军掌握的攻击手法往往也是有限的,他们多次演习后,手法和场景可能会被罗列完毕。假设某一个场景建设方尚未补齐能力,蓝军同样的姿势演习100遍,增加100个未发现的演习案例,对建设方而言并没有更多的帮助。所以,把已知攻击手法的建成覆盖率拿出来,也是一个比较好的评价指标。入侵检测团队把精力聚焦在已知攻击手法的优先级评估和快速覆盖上,对建设到什么程度是满足需要的,要有自己的专业判断(参考入侵检测原则里的“性价比”原则)。而宣布建成了一个场景的入侵发现能力,是要有基本的验收原则的:该场景日均工单 < X单,峰值 < Y单;当前所有场景日平均<XX,峰值 <YY,超出该指标的策略不予接收,因为过多的告警会导致有效信息被淹没,反而导致此前具备的能力被干扰,不如视为该场景尚未具备对抗能力。同一个事件只告警首次,多次出现自动聚合。具备误报自学习能力。告警具备可读性(有清晰的风险阐述、关键信息、处理指引、辅助信息或者索引,便于定性),不鼓励Key-Value模式的告警,建议使用自然语言描述核心逻辑和响应流程。有清晰的说明文档,自测报告(就像交付了一个研发产品,产品文档和自测过程是质量的保障)。有蓝军针对该场景实战验收报告。不建议调用微信、短信等接口发告警(告警和事件的区别是,事件可以闭环,告警只是提醒),统一的告警事件框架可以有效的管理事件确保闭环,还能提供长期的基础运营数据,比如止损效率、误报量/率。策略人员的文档应当说明当前模型对哪些情况具备感知能力,哪些前提下会无法告警(考验一个人对该场景和自己模型的理解能力)。通过前述判断,可以对策略的成熟度形成自评分,0-100自由大致估算。单个场景往往很难达到100分,但那并没有关系,因为从80分提升到100分的边际成本可能变的很高。不建议追求极致,而是全盘审视,是否快速投入到下一个场景中去。如果某个不到满分的场景经常出现真实对抗,又没有交叉的其它策略进行弥补,那自评结论可能需要重审并提高验收的标准。至少解决工作中实际遇到的Case要优先考虑。影响入侵检测的关键要素讨论影响入侵检测的要素时,我们可以简单看看,曾经发生过哪些错误导致防守方不能主动发现入侵:依赖的数据丢失,比如HIDS在当事机器上,没部署安装/Agent挂了/数据上报过程丢失了/Bug了,或者后台传输链条中丢失数据。策略脚本Bug,没启动(事实上我们已经失去了这个策略感知能力了)。还没建设对应的策略(很多时候入侵发生了才发现这个场景我们还没来得及建设对应的策略)。策略的灵敏度/成熟度不够(比如扫描的阈值没达到,WebShell用了变形的对抗手法)。模型依赖的部分基础数据错误,做出了错误的判断。成功告警了,但是负责应急同学错误的判断/没有跟进/辅助信息不足以定性,没有行动起来。所以实际上,要让一个入侵事件被捕获,我们需要入侵检测系统长时间、高质量、高可用的运行。这是一件非常专业的工作,超出了绝大多数安全工程师能力和意愿的范畴。所以建议指派专门的运营人员对以下目标负责:数据采集的完整性(全链路的对账)。每一个策略时刻工作正常(自动化拨测监控)。基础数据的准确性。工单运营支撑平台及追溯辅助工具的便捷性。可能有些同学会想,影响入侵检测的关键要素,难道不是模型的有效性么?怎么全是这些乱七八糟的东西?实际上,大型互联网企业的入侵检测系统日均数据量可能到达数百T,甚至更多。涉及到数十个业务模块,成百上千台机器。从数字规模上来说,不亚于一些中小型企业的整个数据中心。这样复杂的一个系统,要长期维持在高可用标准,本身就需要有SRE、QA等辅助角色的专业化支持。如果仅依靠个别安全工程师,很难让其研究安全攻防的时候,又兼顾到基础数据质量、服务的可用性和稳定性、发布时候的变更规范性、各类运营指标和运维故障的及时响应。最终的结果就是能力范围内可以发现的入侵,总是有各种意外“恰好”发现不了。所以,笔者认为,以多数安全团队运营质量之差,其实根本轮不到拼策略(技术)。当然,一旦有资源投入去跟进这些辅助工作之后,入侵检测就真的需要拼策略了。此时,攻击手法有那么多,凭什么先选择这个场景建设?凭什么认为建设到某程度就足够满足当下的需要了?凭什么选择发现某些样本,而放弃另一些样本的对抗?这些看似主观性的东西,非常考验专业判断力。而且在领导面前很容易背上“责任心不足”的帽子,比如为困难找借口而不是为目标找方法,这个手法黑客攻击了好多次,凭什么不解决,那个手法凭什么说在视野范围内,但是要明年再解决?如何发现APT?所谓APT,就是高级持续威胁。既然是高级的,就意味着木马很大可能是免杀的(不能靠杀毒软件或者普通的特征发现),利用的漏洞也是高级的(加固到牙齿可能也挡不住敌人进来的步伐),攻击手法同样很高级(攻击场景可能我们都没有见过)。所以,实际上APT的意思,就约等于同于不能被发现的入侵。然而,业界总还有APT检测产品,解决方案的厂商在混饭吃,他们是怎么做的呢?木马免杀的,他们用沙箱+人工分析,哪怕效率低一些,还是试图做出定性,并快速的把IOC(威胁情报)同步给其它客户,发现1例,全球客户都具备同样的感知能力。流量加密变形对抗的,他们用异常检测的模型,把一些不认识的可疑的IP关系、payload给识别出来。当然,识别出来之后,也要运营人员跟进得仔细,才能定性。攻击手法高级的,他们还是会假定黑客就用鱼叉、水坑之类的已知手法去执行,然后在邮箱附件、PC终端等环节采集日志,对用户行为进行分析,UEBA试图寻找出用户异于平常的动作。那么,我们呢?笔者也没有什么好的办法,可以发现传说中的“免杀”的木马,但是我们可以针对已知的黑客攻击框架(比如Metasploit、Cobalt Strike)生成的样本、行为进行一些特征的提取。我们可以假设已经有黑客控制了某一台机器,但是它试图进行横向扩散的时候,我们有一些模型可以识别这个主机的横向移动行为。笔者认为,世界上不存在100%能发现APT的方法。但是我们可以等待实施APT的团队犯错,只要我们的纵深足够的多,信息足够不对称,想要完全不触碰我们所有的铃铛,绝对存在一定的困难。甚至,攻击者如果需要小心翼翼的避开所有的检测逻辑,可能也会给对手一种心理上的震慑,这种震慑可能会延缓对手接近目标的速度,拉长时间。而在这个时间里,只要他犯错,就轮到我们出场了。前面所有的高标准,包括高覆盖、低误报,强制每一个告警跟进到底,“掘地三尺”的态度,都是在等待这一刻。抓到一个值得敬佩的对手,那种成就感,还是很值得回味的。所以,希望所有从事入侵检测的安全同行们都能坚持住,即使听过无数次“狼来了”,下一次看到告警,依然可以用最高的敬畏心去迎接对手(告警虐我千百遍,我待告警如初恋)。AI在入侵检测领域的正确姿势最近这两年,如果不谈AI的话,貌似故事就不会完整。只不过,随着AI概念的火爆,很多人已经把传统的数据挖掘、统计分析等思想,比如分类、预测、聚类、关联之类的算法,都一律套在AI的帽子里。其实AI是一种现代的方法,在很多地方有非常实际的产出了。以WebShell的文本分析为例,我们可能需要花很长很长的时间,才能把上千个样本里隐含的几十种样本技术类型拆分开,又花更长的时间去一一建设模型(是的,在这样的场景下,特征工程真的是一个需要更长时间的工作)。而使用AI,做好数据打标的工作,训练、调参,很快就能拿到一个实验室环境不那么过拟合的模型出来,迅速投产到生产环境上。熟练一点可能1-2个月就能做完了。在这种场景下,AI这种现代的方法,的确能几大的提高效率。但问题是,前文也提到过了,黑客的攻击黑样本、WebShell的样本,往往极其稀缺,它不可能是完备的能够描述黑客入侵的完整特征的。因此,AI产出的结果,无论是误报率还是漏报率,都会受训练方法和输入样本的影响较大,我们可以借助AI,但绝对不能完全交给AI。安全领域一个比较常见的现象是,将场景转变成标记问题,要难过于通过数学模型把标记的解给求出来。此时往往需要安全专家先行,算法专家再跟上,而不能直接让算法专家“孤军奋战”。针对一个具体的攻击场景,怎么样采集对应的入侵数据,思考这个入侵动作和正常行为的区别,这个特征的提取过程,往往决定了模型最终的效果。特征决定了效果的上限,而算法模型只能决定了有多接近这个上限。此前,笔者曾见过一个案例,AI团队产出了一个实验室环境效果极佳,误报率达到1/1000000的WebShell模型,但是投放到生产环境里初期日均告警6000单,完全无法运营,同时还存在不少漏报的情况。这些情况随着安全团队和AI工程师共同的努力,后来逐渐地解决。但是并未能成功的取代原有的特征工程模型。目前业界有许多产品、文章在实践AI,但遗憾的是,这些文章和产品大多“浅尝辄止”,没有在真实的环境中实践运营效果。一旦我们用前面的标准去要求它,就会发现,AI虽然是个好东西,但是绝对只是个“半成品”。真正的运营,往往需要传统的特征工程和AI并行,也需要持续地进行迭代。未来必然是AI的天下,但是有多少智能,前面可能就要铺垫多少人工。愿与同行们一起在这个路上继续探索下去,多多交流分享。关于美团安全美团安全部的大多数核心开发人员,拥有多年互联网以及安全领域实践经验,很多同学参与过大型互联网公司的安全体系建设,其中也不乏全球化安全运营人才,具备百万级IDC规模攻防对抗的经验。安全部也不乏CVE“挖掘圣手”,有受邀在Black Hat等国际顶级会议发言的讲者,当然还有很多漂亮的运营妹子。目前,美团安全部涉及的技术包括渗透测试、Web防护、二进制安全、内核安全、分布式开发、大数据分析、安全算法等等,同时还有全球合规与隐私保护等策略制定。我们正在建设一套百万级IDC规模、数十万终端接入的移动办公网络自适应安全体系,这套体系构建于零信任架构之上,横跨多种云基础设施,包括网络层、虚拟化/容器层、Server 软件层(内核态/用户态)、语言虚拟机层(JVM/JS V8)、Web应用层、数据访问层等,并能够基于大数据+机器学习技术构建全自动的安全事件感知系统,努力打造成业界最前沿的内置式安全架构和纵深防御体系。随着美团的高速发展,业务复杂度不断提升,安全部门面临更多的机遇和挑战。我们希望将更多代表业界最佳实践的安全项目落地,同时为更多的安全从业者提供一个广阔的发展平台,并提供更多在安全新兴领域不断探索的机会。【安利个小广告】美团安全部正在招募Web&二进制攻防、后台&系统开发、机器学习&算法等各路小伙伴。如果你想加入我们,欢迎简历请发至邮箱zhaoyan17@meituan.com具体职位信息可参考这里美团安全应急响应中心MTSRC主页:security.meituan.com

November 9, 2018 · 1 min · jiezi

【基本功】深入剖析Swift性能优化

简介2014年,苹果公司在WWDC上发布Swift这一新的编程语言。经过几年的发展,Swift已经成为iOS开发语言的“中流砥柱”,Swift提供了非常灵活的高级别特性,例如协议、闭包、泛型等,并且Swift还进一步开发了强大的SIL(Swift Intermediate Language)用于对编译器进行优化,使得Swift相比Objective-C运行更快性能更优,Swift内部如何实现性能的优化,我们本文就进行一下解读,希望能对大家有所启发和帮助。针对Swift性能提升这一问题,我们可以从概念上拆分为两个部分:编译器:Swift编译器进行的性能优化,从阶段分为编译期和运行期,内容分为时间优化和空间优化。开发者:通过使用合适的数据结构和关键字,帮助编译器获取更多信息,进行优化。下面我们将从这两个角度切入,对Swift性能优化进行分析。通过了解编译器对不同数据结构处理的内部实现,来选择最合适的算法机制,并利用编译器的优化特性,编写高性能的程序。理解Swift的性能理解Swift的性能,首先要清楚Swift的数据结构,组件关系和编译运行方式。数据结构Swift的数据结构可以大体拆分为:Class,Struct,Enum。组件关系组件关系可以分为:inheritance,protocols,generics。方法分派方式方法分派方式可以分为Static dispatch和Dynamic dispatch。要在开发中提高Swift性能,需要开发者去了解这几种数据结构和组件关系以及它们的内部实现,从而通过选择最合适的抽象机制来提升性能。首先我们对于性能标准进行一个概念陈述,性能标准涵盖三个标准:AllocationReference countingMethod dispatch接下来,我们会分别对这几个指标进行说明。Allocation内存分配可以分为堆区栈区,在栈的内存分配速度要高于堆,结构体和类在堆栈分配是不同的。Stack基本数据类型和结构体默认在栈区,栈区内存是连续的,通过出栈入栈进行分配和销毁,速度很快,高于堆区。我们通过一些例子进行说明://示例 1// Allocation// Structstruct Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //进行point1初始化,开辟栈内存var point2 = point1 //初始化point2,拷贝point1内容,开辟新内存point2.x = 5 //对point2的操作不会影响point1// use point1// use point2以上结构体的内存是在栈区分配的,内部的变量也是内联在栈区。将point1赋值给point2实际操作是在栈区进行了一份拷贝,产生了新的内存消耗point2,这使得point1和point2是完全独立的两个实例,它们之间的操作互不影响。在使用point1和point2之后,会进行销毁。Heap高级的数据结构,比如类,分配在堆区。初始化时查找没有使用的内存块,销毁时再从内存块中清除。因为堆区可能存在多线程的操作问题,为了保证线程安全,需要进行加锁操作,因此也是一种性能消耗。// Allocation// Classclass Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //在堆区分配内存,栈区只是存储地址指针let point2 = point1 //不产生新的实例,而是对point2增加对堆区内存引用的指针point2.x = 5 //因为point1和point2是一个实例,所以point1的值也会被修改// use point1// use point2以上我们初始化了一个Class类型,在栈区分配一块内存,但是和结构体直接在栈内存储数值不同,我们只在栈区存储了对象的指针,指针指向的对象的内存是分配在堆区的。需要注意的是,为了管理对象内存,在堆区初始化时,除了分配属性内存(这里是Double类型的x,y),还会有额外的两个字段,分别是type和refCount,这个包含了type,refCount和实际属性的结构被称为blue box。内存分配总结从初始化角度,Class相比Struct需要在堆区分配内存,进行内存管理,使用了指针,有更强大的特性,但是性能较低。优化方式:对于频繁操作(比如通信软件的内容气泡展示),尽量使用Struct替代Class,因为栈内存分配更快,更安全,操作更快。Reference countingSwift通过引用计数管理堆对象内存,当引用计数为0时,Swift确认没有对象再引用该内存,所以将内存释放。对于引用计数的管理是一个非常高频的间接操作,并且需要考虑线程安全,使得引用计数的操作需要较高的性能消耗。对于基本数据类型的Struct来说,没有堆内存分配和引用计数的管理,性能更高更安全,但是对于复杂的结构体,如:// Reference Counting// Struct containing referencesstruct Label { var text:String var font:UIFont func draw() { … }}let label1 = Label(text:“Hi”, font:font) //栈区包含了存储在堆区的指针let label2 = label1 //label2产生新的指针,和label1一样指向同样的string和font地址// use label1// use label2这里看到,包含了引用的结构体相比Class,需要管理双倍的引用计数。每次将结构体作为参数传递给方法或者进行直接拷贝时,都会出现多份引用计数。下图可以比较直观的理解:备注:包含引用类型的结构体出现Copy的处理方式Class在拷贝时的处理方式:引用计数总结Class在堆区分配内存,需要使用引用计数器进行内存管理。基本类型的Struct在栈区分配内存,无引用计数管理。包含强类型的Struct通过指针管理在堆区的属性,对结构体的拷贝会创建新的栈内存,创建多份引用的指针,Class只会有一份。优化方式在使用结构体时:通过使用精确类型,例如UUID替代String(UUID字节长度固定128字节,而不是String任意长度),这样就可以进行内存内联,在栈内存储UUID,我们知道,栈内存管理更快更安全,并且不需要引用计数。Enum替代String,在栈内管理内存,无引用计数,并且从语法上对于开发者更友好。Method Dispatch我们之前在Static dispatch VS Dynamic dispatch中提到过,能够在编译期确定执行方法的方式叫做静态分派Static dispatch,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派Dynamic dispatch。Static dispatch更快,而且静态分派可以进行内联等进一步的优化,使得执行更快速,性能更高。但是对于多态的情况,我们不能在编译期确定最终的类型,这里就用到了Dynamic dispatch动态分派。动态分派的实现是,每种类型都会创建一张表,表内是一个包含了方法指针的数组。动态分派更灵活,但是因为有查表和跳转的操作,并且因为很多特点对于编译器来说并不明确,所以相当于block了编译器的一些后期优化。所以速度慢于Static dispatch。下面看一段多态代码,以及分析实现方式://引用语义实现的多态class Drawable { func draw() {} }class Point :Drawable { var x, y:Double override func draw() { … }}class Line :Drawable { var x1, y1, x2, y2:Double override func draw() { … }}var drawables:[Drawable]for d in drawables { d.draw()}Method Dispatch总结Class默认使用Dynamic dispatch,因为在编译期几乎每个环节的信息都无法确定,所以阻碍了编译器的优化,比如inline和whole module inline。使用Static dispatch代替Dynamic dispatch提升性能我们知道Static dispatch快于Dynamic dispatch,如何在开发中去尽可能使用Static dispatch。inheritance constraints继承约束我们可以使用final关键字去修饰Class,以此生成的Final class,使用Static dispatch。access control访问控制private关键字修饰,使得方法或属性只对当前类可见。编译器会对方法进行Static dispatch。编译器可以通过whole module optimization检查继承关系,对某些没有标记final的类通过计算,如果能在编译期确定执行的方法,则使用Static dispatch。Struct默认使用Static dispatch。Swift快于OC的一个关键是可以消解动态分派。总结Swift提供了更灵活的Struct,用以在内存、引用计数、方法分派等角度去进行性能的优化,在正确的时机选择正确的数据结构,可以使我们的代码性能更快更安全。延伸你可能会问Struct如何实现多态呢?答案是protocol oriented programming。以上分析了影响性能的几个标准,那么不同的算法机制Class,Protocol Types和Generic code,它们在这三方面的表现如何,Protocol Type和Generic code分别是怎么实现的呢?我们带着这个问题看下去。Protocol Type这里我们会讨论Protocol Type如何存储和拷贝变量,以及方法分派是如何实现的。不通过继承或者引用语义的多态:protocol Drawable { func draw() }struct Point :Drawable { var x, y:Double func draw() { … }}struct Line :Drawable { var x1, y1, x2, y2:Double func draw() { … }}var drawables:[Drawable] //遵守了Drawable协议的类型集合,可能是point或者linefor d in drawables { d.draw()}以上通过Protocol Type实现多态,几个类之间没有继承关系,故不能按照惯例借助V-Table实现动态分派。如果想了解Vtable和Witness table实现,可以进行点击查看,这里不做细节说明。因为Point和Line的尺寸不同,数组存储数据实现一致性存储,使用了Existential Container。查找正确的执行方法则使用了 Protoloc Witness Table 。Existential ContainerExistential Container是一种特殊的内存布局方式,用于管理遵守了相同协议的数据类型Protocol Type,这些数据类型因为不共享同一继承关系(这是V-Table实现的前提),并且内存空间尺寸不同,使用Existential Container进行管理,使其具有存储的一致性。结构如下:三个词大小的valueBuffer这里介绍一下valueBuffer结构,valueBuffer有三个词,每个词包含8个字节,存储的可能是值,也可能是对象的指针。对于small value(空间小于valueBuffer),直接存储在valueBuffer的地址内, inline valueBuffer,无额外堆内存初始化。当值的数量大于3个属性即large value,或者总尺寸超过valueBuffer的占位,就会在堆区开辟内存,将其存储在堆区,valueBuffer存储内存指针。value witness table的引用因为Protocol Type的类型不同,内存空间,初始化方法等都不相同,为了对Protocol Type生命周期进行专项管理,用到了Value Witness Table。protocol witness table的引用管理Protocol Type的方法分派。内存分布如下:1. payload_data_0 = 0x0000000000000004,2. payload_data_1 = 0x0000000000000000,3. payload_data_2 = 0x0000000000000000,4. instance_type = 0x000000010d6dc408 ExistentialContainers`type metadata for ExistentialContainers.Car,5. protocol_witness_0 = 0x000000010d6dc1c0 ExistentialContainers protocol witness table for ExistentialContainers.Car:ExistentialContainers.Drivable in ExistentialContainersProtocol Witness Table(PWT)为了实现Class多态也就是引用语义多态,需要V-Table来实现,但是V-Table的前提是具有同一个父类即共享相同的继承关系,但是对于Protocol Type来说,并不具备此特征,故为了支持Struct的多态,需要用到protocol oriented programming机制,也就是借助Protocol Witness Table来实现(细节可以点击Vtable和witness table实现,每个结构体会创造PWT表,内部包含指针,指向方法具体实现)。Value Witness Table(VWT)用于管理任意值的初始化、拷贝、销毁。Value Witness Table的结构如上,是用于管理遵守了协议的Protocol Type实例的初始化,拷贝,内存消减和销毁的。Value Witness Table在SIL中还可以拆分为%relative_vwtable和%absolute_vwtable,我们这里先不做展开。Value Witness Table和Protocol Witness Table通过分工,去管理Protocol Type实例的内存管理(初始化,拷贝,销毁)和方法调用。我们来借助具体的示例进行进一步了解:// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)在Swift编译器中,通过Existential Container实现的伪代码如下:// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)//existential container的伪代码结构struct ExistContDrawable { var valueBuffer:(Int, Int, Int) var vwt:ValueWitnessTable var pwt:DrawableProtocolWitnessTable}// drawACopy方法生成的伪代码func drawACopy(val:ExistContDrawable) { //将existential container传入 var local = ExistContDrawable() //初始化container let vwt = val.vwt //获取value witness table,用于管理生命周期 let pwt = val.pwt //获取protocol witness table,用于进行方法分派 local.type = type local.pwt = pwt vwt.allocateBufferAndCopyValue(&local, val) //vwt进行生命周期管理,初始化或者拷贝 pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,这里说一下projectBuffer,因为不同类型在内存中是不同的(small value内联在栈内,large value初始化在堆内,栈持有指针),所以方法的确定也是和类型相关的,我们知道,查找方法时是通过当前对象的地址,通过一定的位移去查找方法地址。 vwt.destructAndDeallocateBuffer(temp) //vwt进行生命周期管理,销毁内存}Protocol Type 存储属性我们知道,Swift中Class的实例和属性都存储在堆区,Struct实例在栈区,如果包含指针属性则存储在堆区,Protocol Type如何存储属性?Small Number通过Existential Container内联实现,大数存在堆区。如何处理Copy呢?Protocol大数的Copy优化在出现Copy情况时:let aLine = Line(1.0, 1.0, 1.0, 3.0)let pair = Pair(aLine, aLine)let copy = pair会将新的Exsitential Container的valueBuffer指向同一个value即创建指针引用,但是如果要改变值怎么办?我们知道Struct值的修改和Class不同,Copy是不应该影响原实例的值的。这里用到了一个技术叫做Indirect Storage With Copy-On-Write,即优先使用内存指针。通过提高内存指针的使用,来降低堆区内存的初始化。降低内存消耗。在需要修改值的时候,会先检测引用计数检测,如果有大于1的引用计数,则开辟新内存,创建新的实例。在对内容进行变更的时候,会开启一块新的内存,伪代码如下:class LineStorage { var x1, y1, x2, y2:Double }struct Line :Drawable { var storage :LineStorage init() { storage = LineStorage(Point(), Point()) } func draw() { … } mutating func move() { if !isUniquelyReferencedNonObjc(&storage) { //如何存在多份引用,则开启新内存,否则直接修改 storage = LineStorage(storage) } storage。start = … }}这样实现的目的:通过多份指针去引用同一份地址的成本远远低于开辟多份堆内存。以下对比图:Protocol Type多态总结支持Protocol Type的动态多态(Dynamic Polymorphism)行为。通过使用Witness Table和Existential Container来实现。对于大数的拷贝可以通过Indirect Storage间接存储来进行优化。说到动态多态Dynamic Polymorphism,我们就要问了,什么是静态多态Static Polymorphism,看看下面示例:// Drawing a copyprotocol Drawable { func draw()}func drawACopy(local :Drawable) { local.draw()}let line = Line()drawACopy(line)// …let point = Point()drawACopy(point)这种情况我们就可以用到泛型Generic code来实现,进行进一步优化。泛型我们接下来会讨论泛型属性的存储方式和泛型方法是如何分派的。泛型和Protocol Type的区别在于:泛型支持的是静态多态。每个调用上下文只有一种类型。查看下面的示例,foo和bar方法是同一种类型。在调用链中会通过类型降级进行类型取代。对于以下示例:func foo<T:Drawable>(local :T) { bar(local)}func bar<T:Drawable>(local:T) { … }let point = Point()foo(point)分析方法foo和bar的调用过程://调用过程foo(point)–>foo<T = Point>(point) //在方法执行时,Swift将泛型T绑定为调用方使用的具体类型,这里为Point bar(local) –>bar<T = Point>(local) //在调用内部bar方法时,会使用foo已经绑定的变量类型Point,可以看到,泛型T在这里已经被降级,通过类型Point进行取代泛型方法调用的具体实现为:同一种类型的任何实例,都共享同样的实现,即使用同一个Protocol Witness Table。使用Protocol/Value Witness Table。每个调用上下文只有一种类型:这里没有使用Existential Container, 而是将Protocol/Value Witness Table作为调用方的额外参数进行传递。变量初始化和方法调用,都使用传入的VWT和PWT来执行。看到这里,我们并不觉得泛型比Protocol Type有什么更快的特性,泛型如何更快呢?静态多态前提下可以进行进一步的优化,称为特定泛型优化。泛型特化静态多态:在调用站中只有一种类型Swift使用只有一种类型的特点,来进行类型降级取代。类型降级后,产生特定类型的方法为泛型的每个类型创造对应的方法这时候你可能会问,那每一种类型都产生一个新的方法,代码空间岂不爆炸?静态多态下进行特定优化specialization因为是静态多态。所以可以进行很强大的优化,比如进行内联实现,并且通过获取上下文来进行更进一步的优化。从而降低方法数量。优化后可以更精确和具体。例如:func min<T:Comparable>(x:T, y:T) -> T { return y < x ? y : x}从普通的泛型展开如下,因为要支持所有类型的min方法,所以需要对泛型类型进行计算,包括初始化地址、内存分配、生命周期管理等。除了对value的操作,还要对方法进行操作。这是一个非常复杂庞大的工程。func min<T:Comparable>(x:T, y:T, FTable:FunctionTable) -> T { let xCopy = FTable.copy(x) let yCopy = FTable.copy(y) let m = FTable.lessThan(yCopy, xCopy) ? y :x FTable.release(x) FTable.release(y) return m}在确定入参类型时,比如Int,编译器可以通过泛型特化,进行类型取代(Type Substitute),优化为:func min<Int>(x:Int, y:Int) -> Int { return y < x ? y :x}泛型特化specilization是何时发生的?在使用特定优化时,调用方需要进行类型推断,这里需要知晓类型的上下文,例如类型的定义和内部方法实现。如果调用方和类型是单独编译的,就无法在调用方推断类型的内部实行,就无法使用特定优化,保证这些代码一起进行编译,这里就用到了whole module optimization。而whole module optimization是对于调用方和被调用方的方法在不同文件时,对其进行泛型特化优化的前提。泛型进一步优化特定泛型的进一步优化:// Pairs in our program using generic typesstruct Pair<T :Drawable> { init(_ f:T, _ s:T) { first = f ; second = s } var first:T var second:T}let pairOfLines = Pair(Line(), Line())// …let pairOfPoint = Pair(Point(), Point())在用到多种泛型,且确定泛型类型不会在运行时修改时,就可以对成对泛型的使用进行进一步优化。优化的方式是将泛型的内存分配由指针指定,变为内存内联,不再有额外的堆初始化消耗。请注意,因为进行了存储内联,已经确定了泛型特定类型的内存分布,泛型的内存内联不能存储不同类型。所以再次强调此种优化只适用于在运行时不会修改泛型类型,即不能同时支持一个方法中包含line和point两种类型。whole module optimizationwhole module optimization是用于Swift编译器的优化机制。可以通过-whole-module-optimization (或 -wmo)进行打开。在XCode 8之后默认打开。 Swift Package Manager在release模式默认使用whole module optimization。module是多个文件集合。编译器在对源文件进行语法分析之后,会对其进行优化,生成机器码并输出目标文件,之后链接器联合所有的目标文件生成共享库或可执行文件。whole module optimization通过跨函数优化,可以进行内联等优化操作,对于泛型,可以通过获取类型的具体实现来进行推断优化,进行类型降级方法内联,删除多余方法等操作。全模块优化的优势编译器掌握所有方法的实现,可以进行内联和泛型特化等优化,通过计算所有方法的引用,移除多余的引用计数操作。通过知晓所有的非公共方法,如果这写方法没有被使用,就可以对其进行消除。如何降低编译时间和全模块优化相反的是文件优化,即对单个文件进行编译。这样的好处在于可以并行执行,并且对于没有修改的文件不会再次编译。缺点在于编译器无法获知全貌,无法进行深度优化。下面我们分析下全模块优化如何避免没修改的文件再次编译。编译器内部运行过程分为:语法分析,类型检查,SIL优化,LLVM后端处理。语法分析和类型检查一般很快,SIL优化执行了重要的Swift特定优化,例如泛型特化和方法内联等,该过程大概占用整个编译时间的三分之一。LLVM后端执行占用了大部分的编译时间,用于运行降级优化和生成代码。进行全模块优化后,SIL优化会将模块再次拆分为多个部分,LLVM后端通过多线程对这些拆分模块进行处理,对于没有修改的部分,不会进行再处理。这样就避免了修改一小部分,整个大模块进行LLVM后端的再次执行,除此外,使用多线程并行操作也会缩短处理时间。扩展:Swift的隐藏“Bug”Swift因为方法分派机制问题,所以在设计和优化后,会产生和我们常规理解不太一致的结果,这当然不能算Bug。但是还是要单独进行说明,避免在开发过程中,因为对机制的掌握不足,造成预期和执行出入导致的问题。Message dispatch我们通过上面说明结合Static dispatch VS Dynamic dispatch对方法分派方式有了了解。这里需要对Objective-C的方法分派方式进行说明。熟悉OC的人都知道,OC采用了运行时机制使用obj_msgSend发送消息,runtime非常的灵活,我们不仅可以对方法调用采用swizzling,对于对象也可以通过isa-swizzling来扩展功能,应用场景有我们常用的hook和大家熟知的KVO。大家在使用Swift进行开发时都会问,Swift是否可以使用OC的运行时和消息转发机制呢?答案是可以。Swift可以通过关键字dynamic对方法进行标记,这样就会告诉编译器,此方法使用的是OC的运行时机制。注意:我们常见的关键字@ObjC并不会改变Swift原有的方法分派机制,关键字@ObjC的作用只是告诉编译器,该段代码对于OC可见。总结来说,Swift通过dynamic关键字的扩展后,一共包含三种方法分派方式:Static dispatch,Table dispatch和Message dispatch。下表为不同的数据结构在不同情况下采取的分派方式:如果在开发过程中,错误的混合了这几种分派方式,就可能出现Bug,以下我们对这些Bug进行分析:SR-584此情况是在子类的extension中重载父类方法时,出现和预期不同的行为。class Base:NSObject { var directProperty:String { return “This is Base” } var indirectProperty:String { return directProperty }}class Sub:Base { }extension Sub { override var directProperty:String { return “This is Sub” }}执行以下代码,直接调用没有问题:Base().directProperty // “This is Base”Sub().directProperty // “This is Sub”间接调用结果和预期不同:Base()。indirectProperty // “This is Base”Sub()。indirectProperty // expected “this is Sub”,but is “This is Base” <- Unexpected!在Base.directProperty前添加dynamic关键字就可以获得"this is Sub"的结果。Swift在extension 文档中说明,不能在extension中重载已经存在的方法。“Extensions can add new functionality to a type, but they cannot override existing functionality.”会出现警告:Cannot override a non-dynamic class declaration from an extension。出现这个问题的原因是,NSObject的extension是使用的Message dispatch,而Initial Declaration使用的是Table dispath(查看上图 Swift Dispatch Method)。extension重载的方法添加在了Message dispatch内,没有修改虚函数表,虚函数表内还是父类的方法,故会执行父类方法。想在extension重载方法,需要标明dynamic来使用Message dispatch。SR-103协议的扩展内实现的方法,无法被遵守类的子类重载:protocol Greetable { func sayHi()}extension Greetable { func sayHi() { print(“Hello”) }}func greetings(greeter:Greetable) { greeter.sayHi()}现在定义一个遵守了协议的类Person。遵守协议类的子类LoudPerson:class Person:Greetable {}class LoudPerson:Person { func sayHi() { print(“sub”) }}执行下面代码结果为:var sub:LoudPerson = LoudPerson()sub.sayHi() //sub不符合预期的代码:var sub:Person = LoudPerson()sub.sayHi() //HellO <-使用了protocol的默认实现注意,在子类LoudPerson中没有出现override关键字。可以理解为LoudPerson并没有成功注册Greetable在Witness table的方法。所以对于声明为Person实际为LoudPerson的实例,会在编译器通过Person去查找,Person没有实现协议方法,则不产生Witness table,sayHi方法是直接调用的。解决办法是在base类内实现协议方法,无需实现也要提供默认方法。或者将基类标记为final来避免继承。进一步通过示例去理解:// Defined protocol。protocol A { func a() -> Int}extension A { func a() -> Int { return 0 }}// A class doesn’t have implement of the function。class B:A {}class C:B { func a() -> Int { return 1 }}// A class has implement of the function。class D:A { func a() -> Int { return 1 }}class E:D { override func a() -> Int { return 2 }}// Failure cases。B().a() // 0C().a() // 1(C() as A).a() // 0 # We thought return 1。 // Success cases。D().a() // 1(D() as A).a() // 1E().a() // 2(E() as A).a() // 2其他我们知道Class extension使用的是Static Dispatch:class MyClass {}extension MyClass { func extensionMethod() {}} class SubClass:MyClass { override func extensionMethod() {}}以上代码会出现错误,提示Declarations in extensions can not be overridden yet。总结影响程序的性能标准有三种:初始化方式, 引用指针和方法分派。文中对比了两种数据结构:Struct和Class的在不同标准下的性能表现。Swift相比OC和其它语言强化了结构体的能力,所以在了解以上性能表现的前提下,通过利用结构体可以有效提升性能。在此基础上,我们还介绍了功能强大的结构体的类:Protocol Type和Generic。并且介绍了它们如何支持多态以及通过使用有条件限制的泛型如何让程序更快。参考资料swift memorylayoutwitness table videoprotocol types pdfprotocol and value oriented programming in UIKit apps videooptimizing swift performancewhole module optimizaitonincreasing performance by reducing dynamic dispatchprotocols generics existential containerprotocols and genericswhy swift is swiftswift method dispatchswift extensionuniversal dynamic dispatch for method callscompiler performance.mdstructures and classes作者简介亚男,美团点评iOS工程师。2017年加入美团点评,负责专业版餐饮管家开发,研究编译器原理。目前正积极推动Swift组件化建设。招聘信息我们餐饮生态技术部是一个技术氛围活跃,大牛聚集的地方。新到店紧握真正的大规模SaaS实战机会,多租户、数据、安全、开放平台等全方位的挑战。业务领域复杂技术挑战多,技术和业务能力迅速提升,最重要的是,加入我们,你将实现真正通过代码来改变行业的梦想。我们欢迎各端人才加入,Java优先。感兴趣的同学赶紧发送简历至 zhaoyanan02@meituan.com,我们期待你的到来。 ...

November 2, 2018 · 5 min · jiezi

美团大脑:知识图谱的建模方法及其应用

作为人工智能时代最重要的知识表示方式之一,知识图谱能够打破不同场景下的数据隔离,为搜索、推荐、问答、解释与决策等应用提供基础支撑。美团大脑围绕吃喝玩乐等多种场景,构建了生活娱乐领域超大规模的知识图谱,为用户和商家建立起全方位的链接。我们美团希望能够通过对应用场景下的用户偏好和商家定位进行更为深度的理解,进而为大众提供更好的智能化服务,帮大家吃得更好,生活更好。近日,美团 AI 平台部 NLP 中心负责人、大众点评搜索智能中心负责人王仲远博士受邀在 AI 科技大本营做了一期线上分享,为大家讲解了美团大脑的设计思路、构建过程、目前面临的挑战,以及在美团点评中的具体应用与实践,其内容整理如下:知识图谱的重要性近年来,人工智能正在快速地改变人们的生活,可以看到各家科技公司都纷纷推出人工智能产品或者系统,比如说在 2016 年,谷歌推出的 AlphaGo ,一问世便横扫整个围棋界,完胜了人类冠军。又比如亚马逊推出的 Amazon Go 无人超市,用户只需下载一个 App,走进这家超市,就可以直接拿走商品,无需排队结账便可离开,这是人工智能时代的“新零售”体验。又比如微软推出的 Skype Translator,它能够帮助使用不同语言的人群进行实时的、无障碍的交流。再比如说苹果推出的 Siri 智能助理,它让每一个用苹果手机的用户都能够非常便捷地完成各项任务。所有这些人工智能产品的出现都依赖于背后各个领域技术突飞猛进的进展,包括机器学习、计算机视觉、语音识别、自然语言处理等等。作为全球领先的生活服务电子商务平台,美团点评在人工智能领域也在积极地进行布局。今年 2 月份,AI 平台部 NLP 中心正式成立,我们的愿景是用人工智能帮大家吃得更好,生活更好。语言是人类智慧的结晶,而自然语言处理是人工智能中最为困难的问题之一,其核心是让机器能像人类一样理解和使用语言。我们希望在不久的将来,当用户发表一条评价的时候,能够让机器阅读这条评价,充分理解用户的喜怒哀乐。当用户进入大众点评的一个商家页面时,面对成千上万条用户评论,我们希望机器能够代替用户快速地阅读这些评论,总结商家的情况,供用户进行参考。未来,当用户有任何餐饮、娱乐方面的决策需求的时候,美团点评能够提供人工智能助理服务,帮助用户快速的进行决策。所有这一切,都依赖于人工智能背后两大技术驱动力:深度学习和知识图谱。我们可以将这两个技术进行一个简单的比较:我们将深度学习归纳为隐性的模型,它通常是面向某一个具体任务,比如说下围棋、识别猫、人脸识别、语音识别等等。通常而言,在很多任务上它能够取得非常优秀的结果,同时它也有非常多的局限性,比如说它需要海量的训练数据,以及非常强大的计算能力,难以进行任务上的迁移,而且可解释性比较差。另一方面,知识图谱是人工智能的另外一大技术驱动力,它能够广泛地适用于不同的任务。相比深度学习,知识图谱中的知识可以沉淀,可解释性非常强,类似于人类的思考。我们可以通过上面的例子,来观察深度学习技术和人类是如何识别猫的,以及它们的过程有哪些区别。2012 年,Google X 实验室宣布使用深度学习技术,让机器成功识别了图片中的猫。它们使用了 1000 台服务器,16000 个处理器,连接成一个 10 亿节点的人工智能大脑。这个系统阅读了 1000 万张从 YouTube 上抽取的图片,最终成功识别出这个图片中有没有猫。我们再来看看人类是如何做的。对于一个 3 岁的小朋友,我们只需要给他看几张猫的图片,他就能够很快识别出不同图片中的猫,而这背后其实就是大脑对于这些知识的推理。2011 年,Science 上有一篇非常出名的论文叫《How to Grow a Mind》。这篇论文的作者来自于 MIT、CMU、UC Berkeley、Stanford 等美国名校的教授。在这篇论文里,最重要的一个结论就是:如果我们的思维能够跳出给定的数据,那么必须有 Another Source Of Information 来 Make Up The Difference。这里的知识语言是什么?对于人类来讲,其实就是我们从小到大接受的学校教育,报纸上、电视上看到的信息,通过社交媒体,通过与其他人交流,不断积累起来的知识。近年来,不管是学术界还是工业界都纷纷构建自家的知识图谱,有面向全领域的知识图谱,也有面向垂直领域的知识图谱。其实早在文艺复兴时期,培根就提出了“知识就是力量”,在当今人工智能时代,各大科技公司更是纷纷提出:知识图谱就是人工智能的基础。 全球的互联网公司都在积极布局知识图谱。早在 2010 年微软就开始构建知识图谱,包括 Satori 和 Probase。2012 年,Google 正式发布了 Google Knowledge Graph,现在规模已经达到 700 亿左右。目前微软和 Google 拥有全世界最大的通用知识图谱,Facebook 拥有全世界最大的社交知识图谱,而阿里巴巴和亚马逊则分别构建了商品知识图谱。如果按照人类理解问题和回答问题这一过程来进行区分,我们可以将知识图谱分成两类。我们来看这样一个例子,如果用户看到这样一个问题,“Who was the U.S. President when the Angels won the World Series?”相信所有的用户都能够理解这个问题,也就是当 Angels 队赢了 World Series 的时候,谁是美国的总统?这是一个问题理解的过程,它所需要的知识通常我们称之为 Common Sense Knowledge(常识性知识)。另外一方面,很多网友可能回答不出这个问题,因为它需要另外一个百科全书式的知识。因此,我们将知识图谱分成两大类,一类叫 Common Sense Knowledge Graph(常识知识图谱),另外一类叫 Encyclopedia Knowledge Graph(百科全书知识图谱)。这两类知识图谱有很明显的区别。针对 Common Sense Knowledge Graph,通常而言,我们会挖掘这些词之间的 Linguistic Knowledge;对于 Encyclopedia Knowledge Graph,我们通常会在乎它的 Entities 和这些 Entities 之间的 Facts。对于 Common Sense Knowledge Graph,一般而言我们比较在乎的 Relation 包括 isA Relation、isPropertyOf Relation。对于 Encyclopedia Knowledge Graph,通常我们会预定义一些谓词,比如说 DayOfbirth、LocatedIn、SpouseOf 等等。对于 Common Sense Knowledge Graph 通常带有一定的概率,但是 Encyclopedia Knowledge Graph 通常就是“非黑即白”,那么构建这种知识图谱时,我们在乎的就是 Precision(准确率)。Common Sense Knowledge Graph 比较有代表性的工作包括 WordNet、KnowItAll、NELL 以及 Microsoft Concept Graph。而 Encyclopedia Knowledge Graph 则有 Freepase、Yago、Google Knowledge Graph 以及正在构建中的“美团大脑”。这里跟大家介绍两个代表性工作:1)Common Sense Knowledge Graph:Probase;2)Encyclopedia Knowledge Graph:美团大脑。常识性知识图谱(Common Sense Knowledge Graph)Microsoft Concept Graph 于 2016 年 11 月正式发布,但是它早在 2010 年就已经开始进行研究,是一个非常大的图谱。在这个图谱里面有上百万个 Nodes(节点),这些 Nodes 有Concepts(概念),比如说 Spanish Artists(西班牙艺术家);有 Entities(实体),比如说 Picasso(毕加索);有 Attributes(属性),比如 Birthday(生日);有 Verbs(动词),有 Adjectives(形容词),比如说 Eat、Sweet。也有很多很多的边,最重要的边,是这种 isA 边,比如说 Picasso,还有 isPropertyOf 边。对于其他的 Relation,我们会统称为 Co-occurance。这是我们在微软亚洲研究院期间对 Common Sense Knowledge Graph 的 Research Roadmap(研究路线图)。当我们构建出 Common Sense Knowledge Graph 之后,重要的是在上面构建各种各样的模型。我们提出了一些模型叫 Conceptualization(概念化模型),它能够支持 Term Similarity、Short Text Similarity 以及 Head-Modifier Detection,最终支持各种应用,比如 NER、文本标注、Ads、Query Recommendation、Text Understanding 等等。到底什么是 Short Text Understanding?常识怎么用在 Text Understanding 中?下面我们可以看一些具体的例子:当大家看到上面中间的文本时,相信所有人都能够认出这应该是一个日期,但是大家没办法知道这个日期代表什么含义。但如果我们再多给一些上下文信息,比如 Picasso、Spanish等等,大家对这个日期就会有一些常识性的推理。我们会猜测这个日期很可能是 Picasso 的出生日期,或者是去世日期,这就是常识。比如说当我们给定 China 和 India 这两个 Entity 的时候,我们的大脑就会做出一些常识性的推理,我们会认为这两个 Entity 在描述 Country。如果再多给一个 Entity:Brazil,这时候我们通常会想到 Emerging Market。如果再加上 Russia,大家可能就会想到“金砖四国”或者“金砖五国”。所有这一切就是常识性的推理。再比如,当我们看到 Engineer 和 Apple 的时候,我们会对 Apple 做一些推理,认为它就是一个 IT Company,但是如果再多给一些上下文信息,在这个句子里面由于 eating 的出现,我相信大家的大脑也会一样地做出常识推理,认为这个 Apple 不再是代表 Company,而是代表 Fruit。所以,这就是我们提出来的 Conceptualization Model,它是一个 Explicit Representation。我们希望它能够将 Text,尤其是 Short Text,映射到 Millions Concepts,这样的 Representation 能够比较容易让用户进行理解,同时能够应用到不同场景当中。在这一页 PPT 中,我们展示了 Conceptualization 的结果。当输入是 Pear 和 Apple 的时候,那么我们会将这个 Apple 映射到 Fruit。但是如果是 iPad Apple 的时候,我们会将它映射到 Company,同时大家注意这并不是唯一的结果,我们实际上是会被映射到一个 Concept Vector。这个 Concept Vector 有多大?它是百万级维度的 Vector,同时也是一个非常 Sparse 的一个 Vector。 通过这样的一个 Conceptualization Model,我们能够解决什么样的文本理解问题?我们可以看这样一个例子。比如说给定一个非常短的一个文本 Python,它只是一个 Single Instance,那么我们会希望将它映射到至少两大类的 Concept 上,一种可能是 Programming Language,另外一种是 Snake。当它有一些 Context,比如说 Python Tutorial 的时候,那么这个时候 Python 指的应该是 Programming Language,如果当它有其他的 Adjective、Verb,比如有 Dangerous 时,这时候我们就会将 Python 理解为 Snake。同时如果在一个文本里面包含了多个的 Entity,比如说 DNN Tool、Python,那么我们希望能够检测出在这个文本里面哪一个是比较重要的 Entity,哪一个是用来做限制的 Entity。下面我们将简单地介绍一下,具体应该怎么去做。当我们在 Google 里搜一个 Single Instance 的时候,通常在右侧会出现这个 Knowledge Panel。对于 Microsoft 这样一个 Instance,我们可以看到这个红色框所框出来的 Concept,Microsoft 指向的是 Technology Company,这背后是怎么实现的? 我们可以看到,Microsoft 实际上会指向非常非常多的 Concept,比如说 Company,Software Company,Technology Leader 等等。我们将它映射到哪一个 Concept 上最合适?如果将它映射到 Company 这个 Concept 上,很显然它是对的,但是我们却没办法将 Microsoft 和 KFC、BMW 这样其他类型的产品区分开来。另外一方面,如果我们将 Microsoft 映射到 Largest Desktop OS Vendor 上,那么这是一个非常 Specific 的 Concept,这样也不太好,为什么?因为这个 Concept 太 Specific,太 Detail,它可能只包含了 Microsoft 这样一个 Entity,那么它就失去了 Concept 的这种抽象能力。所以我们希望将 Microsoft 映射到一个既不是特别 General(抽象),又不是一个特别 Specific(具体)的 Concept 上。在语言学上,我们将这种映射称之为 Basic-level,我们将整个映射过程命名为 Basic-level Conceptualization。我们提出了一种计算 Basic-level Conceptualization 的方法,其实它非常简单而且非常有效。就是将两种的 Typicality 做了一些融合,同时我们也证明了它们跟 PMI 和 Commute Time 之间的一些关联。并且在一个大规模的数据集上,我们通过 Precision 和 NDCG 对它们进行了评价。最后证明,我们所提出来的 Scoring 方法,它在 NDCG 和 Precision 上都能达到比较好的结果。最重要的是,它在理论上是能够对 Basic-Level 进行很好的解释。下面我们来看一下,当 Instance 有了一些 Context 之后,我们应该怎么去进行处理。我们通过一个例子,来简单地解释一下这背后最主要的思想。比如说 iPad、Apple,其中 iPad 基本上是没有歧异的,它会映射到 Device、Product。但是对于 Apple 而言,它可能会映射到至少两类的 Concept 上,比如说 Fruit、Company。那么我们怎么用 iPad 对 Apple 做消歧呢?方法其实也挺直观的。我们会通过大量的统计去发现像 iPad 这样的 Entity,通常会跟 Company、Product 共同出现。比如说 iPad 有可能会跟三星共同出现,有可能会跟 Google 共同出现,那么我们就发现它会经常跟 Brand、Company、Product共同出现。于是我们就利用新挖掘出来的 Knowledge 对 Apple 做消歧,这就是背后最主要的思想。除了刚才这样一个 General Context 以外,在很多时候这些 Text 可能还会包含很多一些特殊的类型,比如说 Verb、Adjective。具体而言,我们希望在看到 Watch Harry Potter 时,能够知道 Harry Potter 是 Movie,当我们看到 Read Harry Potter 时,能够知道 Harry Potter 是 Book。同样的,Harry Potter 还有可能是一个角色名称,或者是一个游戏名称。那么我们来看一看应该怎样去解决这样一件事情。当我们看到 Watch Harry Potter 时,我们首先要知道,Harry Potter 有可能是一本 Book,也有可能是一部 Movie。我们可以算出一个先验概率,这通常要通过大规模的统计。同时我们要知道,Watch 它有可能是一个名词,同时它也有可能是一个动词,并且我们还需要去挖掘,当 Watch 作为动词的时候,它和 Movie 有非常紧密的关联。所以我们本质上是要去做一些概率上的推理,不仅要将条件概率做非常细粒度的分解,最后还要做概率计算。通过概率计算的方法,我们实际上就可以构建出一个非常大的离线知识图谱,那么我们在这个上面,就可以有很多的 Term,以及它们所属的一些 Type,以及不同 Term 之间的一些关联。当我们用这样一个非常大的离线知识图谱来做 Text Understanding 的时候,我们可以首先将这个 Text 进行分割处理,在分割之后,我们实际上是可以从这个非常大的离线知识图谱中截取出它的一个子图。最后我们使用了 Random Walk With Restart 的模型,来对这样一个在线的 Subgraph 进行分类。我们再来看一下,如果一个文本里包含了 Multiple Entities,要怎样处理?我们需要做知识挖掘,怎么做?首先我们可以得到非常多的 Query Log,然后我们也可以去预定一些 Pattern,通过这种 Pattern 的定义,可以抽取出非常多 Entity 之间 Head 和 Modifier 这样的 Relation,那么在接下来我们可以将这些 Entity 映射到 Concept 上,之后得到一个 Pattern。在这个过程之中,我们要将 Entity 映射到 Concept 上,那么这就是前面所提到的 Conceptualization。我们希望之后的映射不能太 General,避免 Concept Pattern 冲突。但是它也不能太 Specific,因为如果太 Specific,可能就会缺少表达能力。最坏的情况,它有可能就会退化到 Entity Level,而 Entity 至少都是百万的规模,那么整个 Concept Patterns 就有可能变成百万乘以百万的级别,显然是不可用的。所以我们就用到了前面介绍的 Basic-Level Conceptualization 的方法,将它映射到一个既不是特别 General,也不是特别 Specific 的 Concept 上。大家可以看一下我们能够挖掘出来的一些 Top 的 Concept Patterns,比如说 Game 和 Platform,就是一个 Concept 和一个 Pattern。它有什么用?举一个具体的例子,当用户在搜 Angry Birds、iOS 的时候,我们就可以知道用户想找的是 Angry Birds 这款游戏,而 iOS 是用来限制这款游戏的一个 Platform。苹果公司每年都会推出新版本的 iOS,那么我们挖掘出这样的 Concept Pattern 之后,不管苹果出到 iOS 15或者 iOS 16,那么我们只需要将它们映射到 Platform,那么我们的 Concept Patterns 就仍然有效,这样可以很容易地进行知识扩展。所以 Common Sense Knowledge Mining 以及 Conceptualization Modeling,可以用在很多的应用上,它可以用来算 Short Text Similarity,可以用来做 Classification、Clustering,也可以用来做广告的 Semantic Match、Q/A System、Chatbot 等等。美团大脑——百科全书式知识图谱(Encyclopedia Knowledge Graph)在介绍完 Common Sense Knowledge Graph 之后,给大家介绍一下 Encyclopedia Knowledge Graph。这是美团的知识图谱项目——美团大脑。美团大脑是什么?美团大脑是我们正在构建中的一个全球最大的餐饮娱乐知识图谱。我们希望能够充分地挖掘关联美团点评各个业务场景里的公开数据,比如说我们有累计 40 亿的用户评价,超过 10 万条个性化标签,遍布全球的 3000 多万商户以及超过 1.4 亿的店菜,我们还定义了 20 级细粒度的情感分析。我们希望能够充分挖掘出这些元素之间的关联,构建出一个知识的“大脑”,用它来提供更加智能的生活服务。我们简单地介绍一下美团大脑是如何进行构建的。我们会使用 Language Model(统计语言模型)、Topic Model(主题生成模型) 以及 Deep Learning Model(深度学习模型) 等各种模型,希望能够做到商家标签的挖掘,菜品标签的挖掘和情感分析的挖掘等等。为了挖掘商户标签,首先我们要让机器去阅读评论。我们使用了无监督和有监督的深度学习模型。无监督模型我们主要用了LDA,它的特点是成本比较低,无需标注的数据。当然,它准确性会比较不可控,同时对挖掘出来的标签我们还需要进行人工的筛选。至于有监督的深度学习模型,那么我们用了 LSTM,它的特点是需要比较大量的标注数据。通过这两种模型挖掘出来的标签,我们会再加上知识图谱里面的一些推理,最终构建出商户的标签。 如果这个商户有很多的评价,都是围绕着宝宝椅、带娃吃饭、儿童套餐等话题,那么我们就可以得出很多关于这个商户的标签。比如说我们可以知道它是一个亲子餐厅,它的环境比较别致,服务也比较热情。下面介绍一下我们如何对菜品进行标签的挖掘?我们使用了 Bi-LSTM 以及 CRF 模型。比如说从这个评论里面我们就可以抽取出这样的 Entity,再通过与其他的一些菜谱网站做一些关联,我们就可以得到它的食材、烹饪方法、口味等信息,这样我们就为每一个店菜挖掘出了非常丰富的口味标签、食材标签等各种各样的标签。下面再简单介绍一下,我们如何进行评论数据的情感挖掘。我们用的是 CNN+LSTM 的模型,对于每一个用户的评价我们都能够分析出他的一些情感的倾向。同时我们也正在做细粒度的情感分析,我们希望能够通过用户短短的评价,分析出他在不同的维度,比如说交通、环境、卫生、菜品、口味等方面的不同的情感分析的结果。值得一提的是,这种细粒度的情感分析结果,目前在全世界范围内都没有很好的解决办法,但是美团大脑已经迈出了非常重要的一步。下面介绍一下我们的知识图谱是如何进行落地的。目前业界知识图谱已经有非常多的成熟应用,比如搜索、推荐、问答机器人、智能助理,包括在穿戴设备、反欺诈、临床决策上都有非常好的应用。同时业界也有很多的探索,包括智能商业模式、智能市场洞察、智能会员体系等等。如何用知识图谱来改进我们的搜索?如果大家现在打开大众点评,搜索某一个菜品时,比如说麻辣小龙虾,其实我们的机器是已经帮大家提前阅读了所有的评价,然后分析出提供这道菜品的商家,我们还会根据用户评论的情感分析结果来改进这些搜索排序。此外,我们也将它用在商圈的个性化推荐。当大家打开大众点评时,如果你现在位于某一个商场或者商圈,那么大家很快就能够看到这个商场或者商圈的页面入口。当用户进入这个商场和商户页面时,通过知识图谱,我们就能够提供“千人千面”的个性化排序和个性化推荐。在这背后其实使用了一个“水波”的深度学习模型,关于这个深度学习模型更详细的介绍,大家可以参见我们在 CIKM 上的一篇论文。所有的这一切,其实还有很多的技术突破等待我们去解决。比如整个美团大脑的知识图谱在百亿的量级,这也是世界上最大的餐饮娱乐知识图谱,为了支撑这个知识图谱,我们需要去研究千亿级别的图存储和计算引擎技术。我们也正在搭建一个超大规模的 GPU 集群,来支持海量数据的深度学习算法。未来,当所有的这些技术都成熟之后,我们还希望能够为所有用户提供“智慧餐厅”和“智能助理”的体验。文章转载自 AI 科技大本营(rgznai100),部分内容有修正。作者简介仲远,博士,美团点评高级研究员、高级总监,美团 AI 平台部 NLP 中心负责人、大众点评搜索智能中心负责人。加入美团点评前,担任美国 Facebook 公司 Research Scientist,负责 Facebook 产品级 NLP Service。在 Facebook 之前,担任微软亚洲研究院的主管研究员,负责微软研究院知识图谱项目和对话机器人项目。多年来专注于自然语言处理、知识图谱及其在文本理解方面的研究,在国际顶级学术会议如 VLDB、ICDE、IJCAI、CIKM 等发表论文30余篇,获得 ICDE 2015 最佳论文奖,并是 ACL 2016 Tutorial “Understanding Short Texts”的主讲人,出版学术专著3部,获得美国专利5项。在 NLP 和 KG 研究领域及实际产品系统中均有丰富经验,研究领域包括:自然语言处理、知识图谱、深度学习、数据挖掘等。招聘信息美团点评 NLP 团队招聘各类算法人才,Base 北京上海均可。NLP 中心使命是打造世界一流的自然语言处理核心技术和服务能力,依托 NLP(自然语言处理)、Deep Learning(深度学习)、Knowledge Graph(知识图谱)等技术,处理美团点评海量文本数据,打通餐饮、旅行、休闲娱乐等各个场景数据,构建美团点评知识图谱,搭建通用 NLP Service,为美团点评各项业务提供智能的文本语义理解服务。我们的团队既注重AI技术的落地,也开展中长期的NLP及知识图谱基础研究。目前项目及业务包括美团点评知识图谱、智能客服、语音语义搜索、文章评论语义理解、美团点评智能助理等。真正助力于“帮大家吃得更好,生活更好”企业使命的实现,优化用户的生活体验,改善和提升消费者的生活品质。欢迎各位朋友推荐或自荐至 hr.ai@meituan.com。算法岗:NLP算法工程师/专家/研究员 、知识图谱算法工程师/专家/研究员工程岗:C++/Java研发专家/工程师 、AI平台研发工程师/专家产品岗:AI产品经理/专家(NLP、数据方向) ...

November 2, 2018 · 4 min · jiezi

CAT 3.0 开源发布,支持多语言客户端及多项性能提升

项目背景CAT(Central Application Tracking),是美团点评基于 Java 开发的一套开源的分布式实时监控系统。美团点评基础架构部希望在基础存储、高性能通信、大规模在线访问、服务治理、实时监控、容器化及集群智能调度等领域提供业界领先的、统一的解决方案,CAT 目前在美团点评的产品定位是应用层的统一监控组件,在中间件(RPC、数据库、缓存、MQ 等)框架中得到广泛应用,为各业务线提供系统的性能指标、健康状况、实时告警等服务。本文会对 CAT 的客户端、性能等做详细深入的介绍,前不久我们也发过一篇 CAT 相关的文章,里面详细介绍了 CAT 客户端和服务端的设计思路,欲知更多细节,欢迎阅读《深度剖析开源分布式监控CAT》产品价值减少故障发现时间降低故障定位成本辅助应用程序优化技术优势实时处理:信息的价值会随时间锐减,尤其是在事故处理过程中全量数据:全量采集指标数据,便于深度分析故障案例高可用:故障的还原与问题定位,需要高可用监控来支撑故障容忍:故障不影响业务正常运转、对业务透明高吞吐:海量监控数据的收集,需要高吞吐能力做保证可扩展:支持分布式、跨 IDC 部署,横向扩展的监控系统使用现状目前,CAT 已经覆盖了美团点评的外卖、酒旅、出行、金融等核心业务线,几乎已经接入美团点评的所有核心应用,并在生产环境中大规模地得到使用。2016 年初至今,CAT 接入的应用增加了400%,机器数增加了 900%,每天处理的消息总量高达 3200 亿,存储消息量近 400TB,高峰期集群 QPS 达 650万/秒。面对流量的成倍增长,CAT 在通信、计算、存储方面都遇到了前所未有的挑战。整个系统架构也经历了一系列的升级和改造,包括消息采样聚合、消息存储、业务多维度指标监控、统一告警等等,项目最终稳定落地。为公司未来几年内业务流量的稳定增长,打下了坚定的基石。经过 7 年的持续建设,CAT 也在不断发展,我们也希望更好的回馈社区,将 CAT 提供的服务惠及更多的外部公司。我们今年将对开源版本进行较大的迭代与更新,未来也会持续把公司内部一些比较好的实践推广出去,欢迎大家跟我们一起共建这个开源社区。新版特性CAT 3.0.0 Release Notes多语言客户端随着业务的不断发展,很多产品和应用需要使用不同的语言,CAT 多语言客户端需求日益增多,除 Java 客户端外,目前提供了 C/C++、Python、Node.js、Golang 客户端,基本覆盖了主流的开发语言。对于多语言客户端,核心设计目标是利用 C 客户端提供核心 API 接口作为底层基石,封装其他语言 SDK。目前支持的主流语言使用指南:JavaC/C++PythonNode.jsGolang性能提升消息采样聚合消息采样聚合在客户端应对大流量时起到了至关重要的作用,当采样命中或者内存队列已满时都会经过采样聚合上报。采样聚合是对消息树拆分归类,利用本地内存做分类统计,将聚合之后的数据进行上报,减少客户端的消息量以及降低网络开销。通信协议优化CAT 客户端与服务端通信协议由自定义文本协议升级为自定义二进制协议,在大规模数据实时处理场景下性能提升显著。目前服务端同时支持两种版本的通信协议,向下兼容旧版客户端。测试环境:CentOS 6.5,4C8G 虚拟机测试结果:新版相比旧版,序列化耗时降低约 3 倍消息文件存储新版消息文件存储进行了重新设计,解决旧版本的文件存储索引、数据文件节点过多以及随机 IO 恶化的问题。新版消息文件存储为了同时兼顾读写性能,引入了二级索引存储方案,对同一个应用的 IP 节点进行合并,并且保证一定的顺序存储。下图是索引结构的最小单元,每个索引文件由若干个最小单元组成。每个单元分为 4×1024 个桶,第一个桶作为我们的一级索引 Header,存储 IP、消息序列号与分桶的映射信息。剩余 4×1024 - 1 个桶作为二级索引,存储消息的地址。新版消息文件存储文件节点数与应用数量成正比,有效减少随机 IO,消息实时存储的性能提升显著。以下为美团点评内部 CAT 线上环境单机消息存储的数据对比:未来规划技术栈升级拥抱主流技术栈,降低学习和开发成本,使用开源社区主流技术工具(Spring、Mybatis等),建设下一代开源产品。产品体验对产品、交互进行全新设计,提升用户体验。开源社区建设产品官网建设、组织技术交流。更多语言 SDK关于开源https://github.com/dianping/catCAT 自 2011 年开源以来,Github 收获 5900+ star,2400+ forks,被 100+ 公司企业使用,其中不乏携程、陆金所、猎聘网、平安等业内知名公司。在每年全球 Qcon 大会、全球架构与运维技术峰会等都有持续的技术输出,受到行业内认可,越来越多的企业伙伴加入了 CAT 的开源建设工作,为 CAT 的成长贡献了巨大的力量。美团点评基础架构部负责人黄斌强表示,在过去四年中,美团点评在架构中间件领域有比较多的积累沉淀,很多系统服务都经历过大规模线上业务实际运营的检验。我们在使用业界较多开源产品的同时,也希望能把积累的技术开源出去,一方面是回馈社区,贡献给整个行业生态;另一方面,让更多感兴趣的开发工程师也能参与进来,共同加速系统软件的升级与创新。所以,像 CAT 这样的优秀项目,我们将陆续开源输出并长期持续运营,保证开源软件本身的成熟度、支撑度与社区的活跃度,也欢迎大家给我们提出更多的宝贵意见和建议。结语这是一场没有终点的长跑,我们整个 CAT 项目组将长期有耐心地不断前行。愿同行的朋友积极参与我们,关注我们,共同打造一款企业级高可用、高可靠的分布式监控中间件产品,共同描绘 CAT 的新未来!这次开源仅仅是一个新的起点,如果你对 CAT 新版本有一些看法以及建议,欢迎联系我们:cat@dianping.com or Github issues招聘信息美团点评基础架构团队诚招 Java 高级、资深技术专家,Base北京、上海。我们是集团致力于研发公司级、业界领先基础架构组件的核心团队,涵盖分布式监控、服务治理、高性能通信、消息中间件、基础存储、容器化、集群调度等技术领域。欢迎有兴趣的同学投送简历到 yong.you@dianping.com。 ...

November 2, 2018 · 1 min · jiezi

美团深度学习系统的工程实践

背景深度学习作为AI时代的核心技术,已经被应用于多个场景。在系统设计层面,由于其具有计算密集型的特性,所以与传统的机器学习算法在工程实践过程中存在诸多的不同。本文将介绍美团平台在应用深度学习技术的过程中,相关系统设计的一些经验。本文将首先列举部分深度学习算法所需的计算量,然后再介绍为满足这些计算量,目前业界比较常见的一些解决方案。最后,我们将介绍美团平台在NLU和语音识别两个领域中,设计相关系统的经验。深度学习的计算量ModelInput SizeParam SizeFlopsAlexNet227 x 227233 MB727 MFLOPsCaffeNet224 x 224233 MB724 MFLOPsVGG-VD-16224 x 224528 MB16 GFLOPsVGG-VD-19224 x 224548 MB20 GFLOPsGoogleNet224 x 22451 MB2 GFLOPsResNet-34224 x 22483 MB4 GFLOPsResNet-152224 x 224230 MB11 GFLOPsSENet224 x 224440 MB21 GFLOPs数据来源上表列举了,ImageNet图像识别中常见算法的模型大小以及单张图片一次训练(One Pass)所需要的计算量。自2012年,Hinton的学生Alex Krizhevsky提出AlexNet,一举摘下ILSVRC 2012的桂冠后,ILSVRC比赛冠军的准确率越来越高。与此同时,其中使用到的深度学习算法也越来越复杂,所需要的计算量也越来越大。SENet与AlexNet相比,计算量多了近30倍。我们知道,ImageNet大概有120万张图片,以SENet为例,如果要完成100个epoch的完整训练,将需要2.52 * 10^18的计算量。如此庞大的计算量,已经远远超出传统的机器学习算法的范畴。更别说,Google在论文《Revisiting Unreasonable Effectiveness of Data in Deep Learning Era》中提及的、比ImageNet大300倍的数据集。物理计算性能面对如此庞大的计算量,那么,我们业界当前常用的计算单元的计算力是多少呢?CPU 物理核:一般浮点运算能力在10^10 FLOPS量级。一台16 Cores的服务器,大致上有200 GFLOPS的运算能力。实际运行,CPU 大概能用到80%的性能,那就160 GFLOPS的运算能力。完成上述SENet运行,需要182天。NVIDIA GPGPU: 目前的V100,单精度浮点运算的峰值大概为14 TFLOPS, 实际运行中,我们假设能用到50%的峰值性能,那就是7 TFLOPS,需要4天。根据以上数据结果可以看出:在深度学习领域,GPU训练数据集所需要耗费的时间,远远少于CPU,这也是当前深度学习训练都是采用GPU的重要原因。业界的解决方案从前面的计算可知,即使使用GPU来计算,训练一次ImageNet 也需要4天的时间。但对于算法工程师做实验、调参而言,这种耗时数天的等待是难以忍受的。为此,目前业界针对深度学习训练的加速,提出了各种各样的解决方案。异构计算的并行方案数据并行(Data Parallelism)数据并行,即每个计算单元都保留一份完整的模型拷贝,分别训练不同的数据,经过一个Iteration或若干个Iteration后,把各个计算单元的模型做一次同步。这是最常见的深度学习训练方式,好处在于逻辑简单、代码实现方便。模型并行(Model Parallelism)模型并行,即各个计算单元存储同一层模型数据的不同部分,训练相同的数据。相对于数据并行,因为各个运算单元每训练完一层神经网络,就必须要同步一次,频繁的同步通信导致系统不能充分地利用硬件的运算能力,所以更为少见。但是在一些业务场景下,Softmax层需要分类的类别可能会有很多,导致Softmax层太大,单个计算单元无法存储,这个时候,需要把模型切割成若干部分,存储在不同的运算单元。模型并行常见于NLU、推荐、金融等领域。流式并行(Stream Parallelism)流式并行,即每个计算单元都存储不同层的模型数据,训练相同的数据。如上图所示,GPU1只负责第一层神经网络的计算,GPU2只负责25层神经网络的计算,GPU3只负责第6层的计算。流式并行的好处在于每个运算单元之间的通信和计算重叠(overlap),如果配置得当,可以非常充分地利用硬件资源。缺点在于,根据不同的模型,需要平衡好各个计算单元的计算量,如果配置不好,很容易形成“堰塞湖”。如上图所示,很有可能出现GPU1 负责的运算量太少,而GPU2 负责的运算量太多,导致GPU1 和GPU2 之间堵塞住大量的Mini-batch,更常见于线上环境。混合并行(Hybrid Parallelism)混合并行,即上面提到的并行方式的混合。如对于一些图像识别任务来说,可能前几层使用数据并行,最后的Softmax层,使用模型并行。异构计算的硬件解决方案单机单卡:一个主机内安装上一块GPU运算卡。常见于个人计算机。单机多卡:一个主机内安装上多块GPU运算卡。常见的有:1机4卡,1机8卡,甚至有1机10卡。一般公司都采取这种硬件方案。多机多卡:多台主机内安装多块GPU运算卡。常见于公司内部的计算集群,一般多机之间采取Infiniband 来实现网络的快速通信。定制化:即类似于Google的TPU解决方案。常见于“巨无霸”公司内部。异构计算的通信解决方案根据上面的硬件解决方案,我们以ResNet为例:模型的大小为230M,单张图片运算量为11 GFLPOS,Mini-batch假设为128。可以计算出各个硬件模块在深度学习训练中的耗时比较:GPU:对于V100,假设有6 TFLOPS,一次Mini-batch 理论耗时:0.23s。PCI-E:常见PCI-E 3.0 * 16,速度为10 GB/s,传输一个模型的理论耗时为:0.023s。网络:假设为10 GB/s的高速网络,传输一个模型的理论耗时:0.023s。Disk:普通的磁盘,我们假设200M/s的读取速度,读取一次Mini-batch所需要的图片耗时:0.094s。根据上面的数据结果,我们似乎可以得出一个结论:PCI-E和网络的传输耗时,相对于GPU来说,整整少了一个数量级,所以网络通信同步的时间可以忽略不计。然而问题并没有那么简单,上面例子中的耗时只是单个模型的耗时,但是对于8卡的集群来说,如果使用数据并行,每次同步就需要传输8份模型,这就导致数据传输的时间和GPU的计算时间“旗鼓相当”。这样的话,GPU就得每训练完一个Mini-batch,都得等候很久的一段时间(采取同步更新),这会浪费很多计算资源。因此,网络通信也需要制定对应的解决方案。下面我们以Nvidia NCCL中单机多卡的通信解决方案为例介绍,而多机多卡的通信解决方案其实是类似的。上图是单机4卡机器,在硬件上,两种不同的通信体系。左边为普通的PCI-E通信,即4个GPU之间组成一个环状。右边为NVLink通信,即两两之间相互连接。常见的通信类型如下图所示:对于深度学习训练而言,关键的两种通信类型为:Broadcast和Reduce。Broadcast用于Master分发最新的模型给各个GPU。Reduce 用于各个GPU计算完Mini-batch后,把模型更新值汇总到Master上。以Broadcast为例,最简单的通信方式是Master往各个GPU上发送数据,这样的耗时就是4次模型传输的时间,通信时间就会太长,一种简单的优化方法如下图所示:即把所需要传输的数据分成若干块,然后通过接力的方式逐个传递,每个GPU都把自己最新的一块数据发送到下一个GPU卡上。这种传输方式能充分利用硬件层面的通信结构,使得需要的耗时大幅缩减。与此类似的,Reduce的通信优化也可以采取相同的方式进行提速。美团的定制化深度学习系统尽管目前在业界已经推出了很多著名的深度学习训练平台,通用的训练平台如TensorFlow、MxNet等等,还有领域专用的训练平台,如语音识别中的Kaldi,但是我们经过调研后,决定内部自主开发一套深度学习系统,理由如下:通用的训练平台,缺乏了领域特色的功能。如语音识别中的特征提取模块和算法。通用的训练平台,通常是基于Data-flow Graph,来对计算图中的每个operator进行建模,所以颗粒度很小,需要调度的单元多,导任务调度复杂。领域特色的训练平台,如Kaldi,在神经网络训练的时候,性能不足。线上业务存在很多特殊性,如果使用TensorFlow之类作为训练平台,不太适合线上业务的情景。NLU线上系统线上系统的业务特点我们在设计NLU线上系统时,考虑了NLU业务的一些特性。发现其具备如下的一些特点:随着业务和技术的变化,算法流程也经常发生变化。算法流程是多个算法串联组成的,不单纯的只有深度学习算法。如分词等算法就不是DL算法。为了能够快速响应一些紧急问题,需要经常对模型进行热更新。更重要的是,我们希望构建一个能以“数据驱动”的自动迭代闭环。业务多变NLU任务的算法流程是多层级的,并且业务经常发生变化。如下图所示:即随着业务要求的变化,NLU系统一开始的算法流程,只需要把一个Query分为两个类,但是到后面,极有可能会变成需要分为三个类别。热更新根据业务需求,或者为了紧急处理一些特殊问题,NLU线上系统经常需要做出快速响应,热更新算法模型。如最近的热点词“skr”,几乎是一夜之间,突然火爆起来。如下图所示的微博,如果不能正确理解“skr”的正确语义,可能就不能准确理解这条微博想要表达的意思。为了避免影响用户体验,我们可能会对NLU系统,马上进行热更新,把新模型紧急进行上线。数据驱动的自动迭代闭环对于线上系统而言,构建如上图所示的自动迭代闭环,能更好地利用业务数据来提升服务质量。NLU线上系统的核心设计算法流程的抽象为了适应线上系统串联、多变的算法流程,我们把线上系统的算法进行抽象,如下图所示:即每一个算法,都依赖于若干个槽位(Slot)和资源(Resource),一旦槽位和资源就位,就会触发对应的算法执行。算法的执行先通过算法适配器,来适配槽位和资源中的数据,转换成算子的输入格式。然后算子执行算法本身,执行完算子后,再经过算法解析器。算法解析器主要用于解析算法执行的结果,触发对应的槽位。如根据算法的结果,触发Top 3的结果。多个算法串联起来,就构建成如下结果:热更新流程的设计如上图所示,我们把算法的热更新流程设计如上。初试状态为左上角,即多个Query使用同一份模型数据。当遇到模型更新的请求后,系统将会block住新的query(右上角状态)。然后更新模型完后,新的query使用新的模型,旧query依然使用旧模型(右下角状态)。最后,当使用旧模型的query结束后,把旧的模型从内存中删除(左下角),然后系统恢复到初始状态。声学模型训练系统因为TensorFlow等通用深度学习训练平台,缺乏了特征提取等业务相关的领域功能,而Kaldi的声学模型训练过程又太慢。所以美团开发了一个声学模型训练系统——Mimir,其具备如下特性:使用比TensorFlow更粗颗粒度的建模单元,使得任务调度、优化更简单方便易行。使用数据并行的并行方案,单机多卡可达到近线性加速。(采取同步更新策略下,4卡加速比达到3.8)移植了Kaldi的一些特有的训练算法。速度上为Kaldi的67倍。(800个小时的训练数据,单机单卡的条件下,Kaldi需要6~7天, Mimir只需20个小时)业务上,移植了Kaldi的特征提取等领域的相关模块。参考资料NCCL: ACCELERATED MULTI-GPU COLLECTIVE COMMUNICATIONS【深度学习】老师木讲架构:深度学习平台技术演进作者简介剑鹏,美团点评算法专家。2017年加入美团,目前作为语音识别团队的声学模型负责人,负责声学模型相关的算法和系统设计与开发。 ...

October 26, 2018 · 1 min · jiezi

Netty堆外内存泄露排查与总结

导读Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。Netty 底层基于 JDK 的 NIO,我们为什么不直接基于 JDK 的 NIO 或者其他NIO框架:使用 JDK 自带的 NIO 需要了解太多的概念,编程复杂。Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动。Netty自带的拆包解包,异常检测等机制让我们从 NIO 的繁重细节中脱离出来,只需关心业务逻辑即可。Netty解决了JDK 的很多包括空轮训在内的 Bug。Netty底层对线程,Selector 做了很多细小的优化,精心设计的 Reactor 线程做到非常高效的并发处理。自带各种协议栈,让我们处理任何一种通用协议都几乎不用亲自动手。Netty社区活跃,遇到问题随时邮件列表或者 issue。Netty已经历各大RPC框架(Dubbo),消息中间件(RocketMQ),大数据通信(Hadoop)框架的广泛的线上验证,健壮性无比强大。背景最近在做一个基于 Websocket 的长连中间件,服务端使用实现了 Socket.IO 协议(基于WebSocket协议,提供长轮询降级能力) 的 netty-socketio 框架,该框架为 Netty 实现,鉴于本人对 Netty 比较熟,并且对比同样实现了 Socket.IO 协议的其他框架,Netty 的口碑都要更好一些,因此选择这个框架作为底层核心。诚然,任何开源框架都避免不了 Bug 的存在,我们在使用这个开源框架时,就遇到一个堆外内存泄露的 Bug。美团的价值观一直都是“追求卓越”,所以我们就想挑战一下,找到那只臭虫(Bug),而本文就是遇到的问题以及排查的过程。当然,想看结论的同学可以直接跳到最后,阅读总结即可。问题某天早上,我们突然收到告警,Nginx 服务端出现大量5xx。我们使用 Nginx 作为服务端 WebSocket 的七层负载,5xx的爆发通常表明服务端不可用。由于目前 Nginx 告警没有细分具体哪台机器不可用,接下来,我们就到 CAT(美团点评统一监控平台,目前已经开源)去检查一下整个集群的各项指标,就发现如下两个异常:某台机器在同一时间点爆发 GC(垃圾回收),而且在同一时间,JVM 线程阻塞。接下来,我们就就开始了漫长的堆外内存泄露“排查之旅”。排查过程阶段1: 怀疑是log4j2因为线程被大量阻塞,我们首先想到的是定位哪些线程被阻塞,最后查出来是 Log4j2 狂打日志导致 Netty 的 NIO 线程阻塞(由于没有及时保留现场,所以截图缺失)。NIO 线程阻塞之后,因我们的服务器无法处理客户端的请求,所以对Nginx来说就是5xx。接下来,我们查看了 Log4j2 的配置文件。我们发现打印到控制台的这个 appender 忘记注释掉了,所以初步猜测:因为这个项目打印的日志过多,而 Log4j2 打印到控制台是同步阻塞打印的,所以就导致了这个问题。那么接下来,我们把线上所有机器的这行注释掉,本以为会“大功告成”,但没想到仅仅过了几天,5xx告警又来“敲门”。看来,这个问题并没我们最初想象的那么简单。阶段2:可疑日志浮现接下来,我们只能硬着头皮去查日志,特别是故障发生点前后的日志,于是又发现了一处可疑的地方:可以看到:在极短的时间内,狂打 failed to allocate 64(bytes) of direct memory(…)日志(瞬间十几个日志文件,每个日志文件几百M),日志里抛出一个 Netty 自己封装的 OutOfDirectMemoryError。说白了,就是堆外内存不够用,Netty 一直在“喊冤”。堆外内存泄露,听到这个名词就感到很沮丧。因为这个问题的排查就像 C 语言内存泄露一样难以排查,首先能想到的就是,在 OOM 爆发之前,查看有无异常。然后查遍了 CAT 上与机器相关的所有指标,查遍了 OOM 日志之前的所有日志,均未发现任何异常!这个时候心里已经“万马奔腾”了……阶段3:定位OOM源没办法,只能看着这堆讨厌的 OOM 日志发着呆,希望答案能够“蹦到”眼前,但是那只是妄想。一筹莫展之际,突然一道光在眼前一闪而过,在 OOM 下方的几行日志变得耀眼起来(为啥之前就没想认真查看日志?估计是被堆外内存泄露这几个词吓怕了吧 ==!),这几行字是 ….PlatformDepedeng.incrementMemory()…。原来,堆外内存是否够用,是 Netty 这边自己统计的,那么是不是可以找到统计代码,找到统计代码之后我们就可以看到 Netty 里面的对外内存统计逻辑了?于是,接下来翻翻代码,找到这段逻辑,就在 PlatformDepedent 这个类里面。这个地方,是一个对已使用堆外内存计数的操作,计数器为 DIRECT_MEMORY_COUNTER,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义 OOM Error,异常里面的文本内容正是我们在日志里面看到的。接下来,就验证一下这个方法是否是在堆外内存分配的时候被调用。果然,在 Netty 每次分配堆外内存之前,都会计数。想到这,思路就开始慢慢清晰,而心情也开始从“秋风瑟瑟”变成“春光明媚”。阶段4:反射进行堆外内存监控CAT 上关于堆外内存的监控没有任何异常(应该是没有统计准确,一直维持在 1M),而这边我们又确认堆外内存已快超过上限,并且已经知道 Netty 底层是使用的哪个字段来统计。那么接下来要做的第一件事情,就是反射拿到这个字段,然后我们自己统计 Netty 使用堆外内存的情况。堆外内存统计字段是 DIRECT_MEMORY_COUNTER,我们可以通过反射拿到这个字段,然后定期 Check 这个值,就可以监控 Netty 堆外内存的增长情况。于是我们通过反射拿到这个字段,然后每隔一秒打印,为什么要这样做?因为,通过我们前面的分析,在爆发大量 OOM 现象之前,没有任何可疑的现象。那么只有两种情况,一种是突然某个瞬间分配了大量的堆外内存导致OOM;一种是堆外内存缓慢增长,到达某个点之后,最后一根稻草将机器压垮。在这段代码加上去之后,我们打包上线。阶段5:到底是缓慢增长还是瞬间飙升?代码上线之后,初始内存为 16384k(16M),这是因为线上我们使用了池化堆外内存,默认一个 chunk 为16M,这里不必过于纠结。但是没过一会,内存就开始缓慢飙升,并且没有释放的迹象,二十几分钟之后,内存使用情况如下:走到这里,我们猜测可能是前面提到的第二种情况,也就是内存缓慢增长造成的 OOM,由于内存实在增长太慢,于是调整机器负载权重为其他机器的两倍,但是仍然是以数K级别在持续增长。那天刚好是周五,索性就过一个周末再开看。周末之后,我们到公司第一时间就连上了跳板机,登录线上机器,开始 tail -f 继续查看日志。在输完命令之后,怀着期待的心情重重的敲下了回车键:果然不出所料,内存一直在缓慢增长,一个周末的时间,堆外内存已经飙到快一个 G 了。这个时候,我竟然想到了一句成语:“只要功夫深,铁杵磨成针”。虽然堆外内存以几个K的速度在缓慢增长,但是只要一直持续下去,总有把内存打爆的时候(线上堆外内存上限设置的是2G)。此时,我们开始自问自答环节:内存为啥会缓慢增长,伴随着什么而增长?因为我们的应用是面向用户端的WebSocket,那么,会不会是每一次有用户进来,交互完之后离开,内存都会增长一些,然后不释放呢?带着这个疑问,我们开始了线下模拟过程。阶段6:线下模拟本地起好服务,把监控堆外内存的单位改为以B为单位(因为本地流量较小,打算一次一个客户端连接),另外,本地也使用非池化内存(内存数字较小,容易看出问题),在服务端启动之后,控制台打印信息如下在没有客户端接入的时候,堆外内存一直是0,在意料之中。接下来,怀着着无比激动的心情,打开浏览器,然后输入网址,开始我们的模拟之旅。我们的模拟流程是:新建一个客户端链接->断开链接->再新建一个客户端链接->再断开链接。如上图所示,一次 Connect 和 Disconnect 为一次连接的建立与关闭,上图绿色框框的日志分别是两次连接的生命周期。我们可以看到,内存每次都是在连接被关闭的的时候暴涨 256B,然后也不释放。走到这里,问题进一步缩小,肯定是连接被关闭的时候,触发了框架的一个Bug,而且这个Bug在触发之前分配了 256B 的内存,随着Bug被触发,内存也没有释放。问题缩小之后,接下来开始“撸源码”,捉虫!阶段7:线下排查接下来,我们将本地服务重启,开始完整的线下排查过程。同时将目光定位到 netty-socketio 这个框架的 Disconnect 事件(客户端WebSocket连接关闭时会调用到这里),基本上可以确定,在 Disconnect 事件前后申请的内存并没有释放。在使用 idea debug 时,要选择只挂起当前线程,这样我们在单步跟踪的时候,控制台仍然可以看到堆外内存统计线程在打印日志。在客户端连接上之后然后关闭,断点进入到 onDisconnect 回调,我们特意在此多停留了一会,发现控制台内存并没有飙升(7B这个内存暂时没有去分析,只需要知道,客户端连接断开之后,我们断点hold住,内存还未开始涨)。接下来,神奇的一幕出现了,我们将断点放开,让程序跑完:Debug 松掉之后,内存立马飙升了!!此时,我们已经知道,这只“臭虫”飞不了多远了。在 Debug 时,挂起的是当前线程,那么肯定是当前线程某个地方申请了堆外内存,然后没有释放,继续“快马加鞭“,深入源码。其实,每一次单步调试,我们都会观察控制台的内存飙升的情况。很快,我们来到了这个地方:在这一行没执行之前,控制台的内存依然是 263B。然后,当执行完该行之后,立刻从 263B涨到519B(涨了256B)。于是,Bug 范围进一步缩小。我们将本次程序跑完,释然后客户端再来一次连接,断点打在 client.send() 这行, 然后关闭客户端连接,之后直接进入到这个方法,随后的过程有点长,因为与 Netty 的时间传播机制有关,这里就省略了。最后,我们跟踪到了如下代码,handleWebsocket:在这个地方,我们看到一处非常可疑的地方,在上图的断点上一行,调用 encoder 分配了一段内存,调用完之后,我们的控制台立马就彪了 256B。所以,我们怀疑肯定是这里申请的内存没有释放,它这里接下来调用 encoder.encodePacket() 方法,猜想是把数据包的内容以二进制的方式写到这段 256B的内存。接下来,我们追踪到这段 encode 代码,单步执行之后,就定位到这行代码:这段代码是把 packet 里面一个字段的值转换为一个 char。然而,当我们使用 idea 预执行的时候,却抛出类一个愤怒的 NPE!!也就是说,框架申请到一段内存之后,在 encoder 的时候,自己 GG 了,还给自己挖了个NPE的深坑,最后导致内存无法释放(最外层有堆外内存释放逻辑,现在无法执行到了)。而且越攒越多,直到被“最后一根稻草”压垮,堆外内存就这样爆了。这里的源码,有兴趣的读者可以自己去分析一下,限于篇幅原因,这里就不再展开叙述了。阶段8:Bug解决既然 Bug 已经找到,接下来就要解决问题了。这里只需要解决这个NPE异常,就可以 Fix 掉。我们的目标就是,让这个 subType 字段不为空。于是我们先通过 idea 的线程调用栈,定位到这个 packet 是在哪个地方定义的:我们找到 idea 的 debugger 面板,眼睛盯着 packet 这个对象不放,然后上线移动光标,便光速定位到。原来,定义 packet 对象这个地方在我们前面的代码其实已经出现过,我们查看了一下 subType 这个字段,果然是 null。接下来,解决 Bug 就很容易了。我们给这个字段赋值即可,由于这里是连接关闭事件,所以我们给他指定了一个名为 DISCONNECT 的字段(可以改天深入去研究 Socket.IO 的协议),反正这个 Bug 是在连接关闭的时候触发的,就粗暴一点了 !解决这个 Bug 的过程是:将这个框架的源码下载到本地,然后加上这一行,最后重新 Build一下,pom 里改了一下名字,推送到我们公司的仓库。这样,项目就可以直接进行使用了。改完 Bug 之后,习惯性地去 GitHub上找到引发这段 Bug 的 Commit:好奇的是,为啥这位 dzn commiter 会写出这么一段如此明显的 Bug,而且时间就在今年3月30号,项目启动的前夕!阶段9:线下验证一切准备就绪之后,我们就来进行本地验证,在服务起来之后,我们疯狂地建立连接,疯狂地断开连接,并观察堆外内存的情况:Bingo!不管我们如何断开连接,堆外内存不涨了。至此,Bug 基本 Fix,当然最后一步,我们把代码推到线上验证。阶段10:线上验证这次线上验证,我们避免了比较土的打日志方法,我们把堆外内存的这个指标“喷射”到 CAT 上,然后再来观察一段时间的堆外内存的情况:过完一段时间,堆外内存已经稳定不涨了。此刻,我们的“捉虫之旅”到此结束。最后,我们还为大家做一个小小的总结,希望对您有所帮助。总结遇到堆外内存泄露不要怕,仔细耐心分析,总能找到思路,要多看日志,多分析。如果使用了 Netty 堆外内存,那么可以自行监控堆外内存的使用情况,不需要借助第三方工具,我们是使用的“反射”拿到的堆外内存的情况。逐渐缩小范围,直到 Bug 被找到。当我们确认某个线程的执行带来 Bug 时,可单步执行,可二分执行,定位到某行代码之后,跟到这段代码,然后继续单步执行或者二分的方式来定位最终出 Bug 的代码。这个方法屡试不爽,最后总能找到想要的 Bug。熟练掌握 idea 的调试,让我们的“捉虫”速度快如闪电(“闪电侠”就是这么来的)。这里,最常见的调试方式是预执行表达式,以及通过线程调用栈,死盯某个对象,就能够掌握这个对象的定义、赋值之类。最后,祝愿大家都能找到自己的“心仪已久” Bug!作者简介闪电侠,2014年加入美团点评,主要负责美团点评移动端统一长连工作,欢迎同行进行技术交流。招聘目前我们团队负责美团点评长连基础设施的建设,支持美团酒旅、外卖、到店、打车、金融等几乎公司所有业务的快速发展。加入我们,你可以亲身体验到千万级在线连接、日吞吐百亿请求的场景,你会直面互联网高并发、高可用的挑战,有机会接触到 Netty 在长连领域的各个场景。我们诚邀有激情、有想法、有经验、有能力的同学,和我们一起并肩奋斗!欢迎感兴趣的同学投递简历至 chao.yu#dianping.com 咨询。参考文献Netty 是什么Netty 源码分析之服务端启动全解析 ...

October 19, 2018 · 2 min · jiezi

美团点评基于 Flink 的实时数仓建设实践

引言近些年,企业对数据服务实时化服务的需求日益增多。本文整理了常见实时数据组件的性能特点和适用场景,介绍了美团如何通过 Flink 引擎构建实时数据仓库,从而提供高效、稳健的实时数据服务。此前我们美团技术博客发布过一篇文章《流计算框架 Flink 与 Storm 的性能对比》,对 Flink 和 Storm 俩个引擎的计算性能进行了比较。本文主要阐述使用 Flink 在实际数据生产上的经验。实时平台初期架构在实时数据系统建设初期,由于对实时数据的需求较少,形成不了完整的数据体系。我们采用的是“一路到底”的开发模式:通过在实时计算平台上部署 Storm 作业处理实时数据队列来提取数据指标,直接推送到实时应用服务中。<center>图1 初期实时数据架构</center>但是,随着产品和业务人员对实时数据需求的不断增多,新的挑战也随之发生。数据指标越来越多,“烟囱式”的开发导致代码耦合问题严重。需求越来越多,有的需要明细数据,有的需要 OLAP 分析。单一的开发模式难以应付多种需求。缺少完善的监控系统,无法在对业务产生影响之前发现并修复问题。实时数据仓库的构建为解决以上问题,我们根据生产离线数据的经验,选择使用分层设计方案来建设实时数据仓库,其分层架构如下图所示:<center>图2 实时数仓数据分层架构</center>该方案由以下四层构成:ODS 层:Binlog 和流量日志以及各业务实时队列。数据明细层:业务领域整合提取事实数据,离线全量和实时变化数据构建实时维度数据。数据汇总层:使用宽表模型对明细数据补充维度数据,对共性指标进行汇总。App 层:为了具体需求而构建的应用层,通过 RPC 框架对外提供服务。通过多层设计我们可以将处理数据的流程沉淀在各层完成。比如在数据明细层统一完成数据的过滤、清洗、规范、脱敏流程;在数据汇总层加工共性的多维指标汇总数据。提高了代码的复用率和整体生产效率。同时各层级处理的任务类型相似,可以采用统一的技术方案优化性能,使数仓技术架构更简洁。技术选型1.存储引擎的调研实时数仓在设计中不同于离线数仓在各层级使用同种储存方案,比如都存储在 Hive 、DB 中的策略。首先对中间过程的表,采用将结构化的数据通过消息队列存储和高速 KV 存储混合的方案。实时计算引擎可以通过监听消息消费消息队列内的数据,进行实时计算。而在高速 KV 存储上的数据则可以用于快速关联计算,比如维度数据。 其次在应用层上,针对数据使用特点配置存储方案直接写入。避免了离线数仓应用层同步数据流程带来的处理延迟。 为了解决不同类型的实时数据需求,合理的设计各层级存储方案,我们调研了美团内部使用比较广泛的几种存储方案。<center>表1 存储方案列表</center>方案优势劣势MySQL1. 具有完备的事务功能,可以对数据进行更新。2. 支持 SQL,开发成本低。1. 横向扩展成本大,存储容易成为瓶颈; 2. 实时数据的更新和查询频率都很高,线上单个实时应用请求就有 1000+ QPS;使用 MySQL 成本太高。Elasticsearch1. 吞吐量大,单个机器可以支持 2500+ QPS,并且集群可以快速横向扩展。2. Term 查询时响应速度很快,单个机器在 2000+ QPS时,查询延迟在 20 ms以内。1. 没有原生的 SQL 支持,查询 DSL 有一定的学习门槛;2. 进行聚合运算时性能下降明显。Druid1. 支持超大数据量,通过 Kafka 获取实时数据时,单个作业可支持 6W+ QPS;2. 可以在数据导入时通过预计算对数据进行汇总,减少的数据存储。提高了实际处理数据的效率;3. 有很多开源 OLAP 分析框架。实现如 Superset。1. 预聚合导致无法支持明细的查询;2. 无法支持 Join 操作;3. Append-only 不支持数据的修改。只能以 Segment 为单位进行替换。Cellar1. 支持超大数据量,采用内存加分布式存储的架构,存储性价比很高;2. 吞吐性能好,经测试处理 3W+ QPS 读写请求时,平均延迟在 1ms左右;通过异步读写线上最高支持 10W+ QPS。1. 接口仅支持 KV,Map,List 以及原子加减等;2. 单个 Key 值不得超过 1KB ,而 Value 的值超过 100KB 时则性能下降明显。根据不同业务场景,实时数仓各个模型层次使用的存储方案大致如下:<center>图3 实时数仓存储分层架构</center>数据明细层 对于维度数据部分场景下关联的频率可达 10w+ TPS,我们选择 Cellar(美团内部存储系统) 作为存储,封装维度服务为实时数仓提供维度数据。数据汇总层 对于通用的汇总指标,需要进行历史数据关联的数据,采用和维度数据一样的方案通过 Cellar 作为存储,用服务的方式进行关联操作。数据应用层 应用层设计相对复杂,再对比了几种不同存储方案后。我们制定了以数据读写频率 1000 QPS 为分界的判断依据。对于读写平均频率高于 1000 QPS 但查询不太复杂的实时应用,比如商户实时的经营数据。采用 Cellar 为存储,提供实时数据服务。对于一些查询复杂的和需要明细列表的应用,使用 Elasticsearch 作为存储则更为合适。而一些查询频率低,比如一些内部运营的数据。 Druid 通过实时处理消息构建索引,并通过预聚合可以快速的提供实时数据 OLAP 分析功能。对于一些历史版本的数据产品进行实时化改造时,也可以使用 MySQL 存储便于产品迭代。2.计算引擎的调研在实时平台建设初期我们使用 Storm 引擎来进行实时数据处理。Storm 引擎虽然在灵活性和性能上都表现不错。但是由于 API 过于底层,在数据开发过程中需要对一些常用的数据操作进行功能实现。比如表关联、聚合等,产生了很多额外的开发工作,不仅引入了很多外部依赖比如缓存,而且实际使用时性能也不是很理想。同时 Storm 内的数据对象 Tuple 支持的功能也很简单,通常需要将其转换为 Java 对象来处理。对于这种基于代码定义的数据模型,通常我们只能通过文档来进行维护。不仅需要额外的维护工作,同时在增改字段时也很麻烦。综合来看使用 Storm 引擎构建实时数仓难度较大。我们需要一个新的实时处理方案,要能够实现:提供高级 API,支持常见的数据操作比如关联聚合,最好是能支持 SQL。具有状态管理和自动支持久化方案,减少对存储的依赖。便于接入元数据服务,避免通过代码管理数据结构。处理性能至少要和 Storm 一致。我们对主要的实时计算引擎进行了技术调研。总结了各类引擎特性如下表所示:<center>表2 实时计算方案列表</center>项目/引擎StormFlinkspark-treamingAPI灵活的底层 API 和具有事务保证的 Trident API流 API 和更加适合数据开发的 Table API 和 Flink SQL 支持流 API 和 Structured-Streaming API 同时也可以使用更适合数据开发的 Spark SQL容错机制ACK 机制State 分布式快照保存点RDD 保存点状态管理Trident State状态管理Key State 和 Operator State两种 State 可以使用,支持多种持久化方案有 UpdateStateByKey 等 API 进行带状态的变更,支持多种持久化方案处理模式单条流式处理单条流式处理Mic batch处理延迟毫秒级毫秒级秒级语义保障At Least Once,Exactly OnceExactly Once,At Least OnceAt Least Once从调研结果来看,Flink 和 Spark Streaming 的 API 、容错机制与状态持久化机制都可以解决一部分我们目前使用 Storm 中遇到的问题。但 Flink 在数据延迟上和 Storm 更接近,对现有应用影响最小。而且在公司内部的测试中 Flink 的吞吐性能对比 Storm 有十倍左右提升。综合考量我们选定 Flink 引擎作为实时数仓的开发引擎。更加引起我们注意的是,Flink 的 Table 抽象和 SQL 支持。虽然使用 Strom 引擎也可以处理结构化数据。但毕竟依旧是基于消息的处理 API ,在代码层层面上不能完全享受操作结构化数据的便利。而 Flink 不仅支持了大量常用的 SQL 语句,基本覆盖了我们的开发场景。而且 Flink 的 Table 可以通过 TableSchema 进行管理,支持丰富的数据类型和数据结构以及数据源。可以很容易的和现有的元数据管理系统或配置管理系统结合。通过下图我们可以清晰的看出 Storm 和 Flink 在开发统过程中的区别。<center>图4 Flink - Storm 对比图</center>在使用 Storm 开发时处理逻辑与实现需要固化在 Bolt 的代码。Flink 则可以通过 SQL 进行开发,代码可读性更高,逻辑的实现由开源框架来保证可靠高效,对特定场景的优化只要修改 Flink SQL 优化器功能实现即可,而不影响逻辑代码。使我们可以把更多的精力放到到数据开发中,而不是逻辑的实现。当需要离线数据和实时数据口径统一的场景时,我们只需对离线口径的 SQL 脚本稍加改造即可,极大地提高了开发效率。同时对比图中 Flink 和 Storm 使用的数据模型,Storm 需要通过一个 Java 的 Class 去定义数据结构,Flink Table 则可以通过元数据来定义。可以很好的和数据开发中的元数据,数据治理等系统结合,提高开发效率。Flink使用心得在利用 Flink-Table 构建实时数据仓库过程中。我们针对一些构建数据仓库的常用操作,比如数据指标的维度扩充,数据按主题关联,以及数据的聚合运算通过 Flink 来实现总结了一些使用心得。1.维度扩充数据指标的维度扩充,我们采用的是通过维度服务获取维度信息。虽然基于 Cellar 的维度服务通常的响应延迟可以在 1ms 以下。但是为了进一步优化 Flink 的吞吐,我们对维度数据的关联全部采用了异步接口访问的方式,避免了使用 RPC 调用影响数据吞吐。对于一些数据量很大的流,比如流量日志数据量在 10W 条/秒这个量级。在关联 UDF 的时候内置了缓存机制,可以根据命中率和时间对缓存进行淘汰,配合用关联的 Key 值进行分区,显著减少了对外部服务的请求次数,有效的减少了处理延迟和对外部系统的压力。2.数据关联数据主题合并,本质上就是多个数据源的关联,简单的来说就是 Join 操作。Flink 的 Table 是建立在无限流这个概念上的。在进行 Join 操作时并不能像离线数据一样对两个完整的表进行关联。采用的是在窗口时间内对数据进行关联的方案,相当于从两个数据流中各自截取一段时间的数据进行 Join 操作。有点类似于离线数据通过限制分区来进行关联。同时需要注意 Flink 关联表时必须有至少一个“等于”关联条件,因为等号两边的值会用来分组。由于 Flink 会缓存窗口内的全部数据来进行关联,缓存的数据量和关联的窗口大小成正比。因此 Flink 的关联查询,更适合处理一些可以通过业务规则限制关联数据时间范围的场景。比如关联下单用户购买之前 30 分钟内的浏览日志。过大的窗口不仅会消耗更多的内存,同时会产生更大的 Checkpoint ,导致吞吐下降或 Checkpoint 超时。在实际生产中可以使用 RocksDB 和启用增量保存点模式,减少 Checkpoint 过程对吞吐产生影响。对于一些需要关联窗口期很长的场景,比如关联的数据可能是几天以前的数据。对于这些历史数据,我们可以将其理解为是一种已经固定不变的"维度"。可以将需要被关联的历史数据采用和维度数据一致的处理方法:“缓存 + 离线"数据方式存储,用接口的方式进行关联。另外需要注意 Flink 对多表关联是直接顺序链接的,因此需要注意先进行结果集小的关联。3.聚合运算使用聚合运算时,Flink 对常见的聚合运算如求和、极值、均值等都有支持。美中不足的是对于 Distinct 的支持,Flink-1.6 之前的采用的方案是通过先对去重字段进行分组再聚合实现。对于需要对多个字段去重聚合的场景,只能分别计算再进行关联处理效率很低。为此我们开发了自定义的 UDAF,实现了 MapView 精确去重、BloomFilter 非精确去重、 HyperLogLog 超低内存去重方案应对各种实时去重场景。但是在使用自定义的 UDAF 时,需要注意 RocksDBStateBackend 模式对于较大的 Key 进行更新操作时序列化和反序列化耗时很多。可以考虑使用 FsStateBackend 模式替代。另外要注意的一点 Flink 框架在计算比如 Rank 这样的分析函数时,需要缓存每个分组窗口下的全部数据才能进行排序,会消耗大量内存。建议在这种场景下优先转换为 TopN 的逻辑,看是否可以解决需求。下图展示一个完整的使用 Flink 引擎生产一张实时数据表的过程:<center>图5 实时计算流程图</center>实时数仓成果通过使用实时数仓代替原有流程,我们将数据生产中的各个流程抽象到实时数仓的各层当中。实现了全部实时数据应用的数据源统一,保证了应用数据指标、维度的口径的一致。在几次数据口径发生修改的场景中,我们通过对仓库明细和汇总进行改造,在完全不用修改应用代码的情况下就完成全部应用的口径切换。在开发过程中通过严格的把控数据分层、主题域划分、内容组织标准规范和命名规则。使数据开发的链路更为清晰,减少了代码的耦合。再配合上使用 Flink SQL 进行开发,代码加简洁。单个作业的代码量从平均 300+ 行的 JAVA 代码 ,缩减到几十行的 SQL 脚本。项目的开发时长也大幅减短,一人日开发多个实时数据指标情况也不少见。除此以外我们通过针对数仓各层级工作内容的不同特点,可以进行针对性的性能优化和参数配置。比如 ODS 层主要进行数据的解析、过滤等操作,不需要 RPC 调用和聚合运算。 我们针对数据解析过程进行优化,减少不必要的 JSON 字段解析,并使用更高效的 JSON 包。在资源分配上,单个 CPU 只配置 1GB 的内存即可满需求。而汇总层主要则主要进行聚合与关联运算,可以通过优化聚合算法、内外存共同运算来提高性能、减少成本。资源配置上也会分配更多的内存,避免内存溢出。通过这些优化手段,虽然相比原有流程实时数仓的生产链路更长,但数据延迟并没有明显增加。同时实时数据应用所使用的计算资源也有明显减少。展望我们的目标是将实时仓库建设成可以和离线仓库数据准确性,一致性媲美的数据系统。为商家,业务人员以及美团用户提供及时可靠的数据服务。同时作为到餐实时数据的统一出口,为集团其他业务部门助力。未来我们将更加关注在数据可靠性和实时数据指标管理。建立完善的数据监控,数据血缘检测,交叉检查机制。及时对异常数据或数据延迟进行监控和预警。同时优化开发流程,降低开发实时数据学习成本。让更多有实时数据需求的人,可以自己动手解决问题。参考文献流计算框架 Flink 与 Storm 的性能对比关于作者伟伦,美团到店餐饮技术部实时数据负责人,2017年加入美团,长期从事数据平台、实时数据计算、数据架构方面的开发工作。在使用 Flink 进行实时数据生产和提高生产效率上,有一些心得和产出。同时也积极推广 Flink 在实时数据处理中的实战经验。招聘信息对数据工程和将数据通过服务业务释放价值感兴趣的同学,可以发送简历到 huangweilun@meituan.com。我们在实时数据仓库、实时数据治理、实时数据产品开发框架、面向销售和商家侧的数据型创新产品层面,都有很多未知但有意义的领域等你来开拓。 ...

October 19, 2018 · 2 min · jiezi

Logan:美团点评的开源移动端基础日志库

前言Logan是美团点评集团移动端基础日志组件,这个名称是Log和An的组合,代表个体日志服务。同时Logan也是“金刚狼”大叔的名号,当然我们更希望这个产品能像金刚狼大叔一样犀利。Logan已经稳定迭代了一年多的时间。目前美团点评绝大多数App已经接入并使用Logan进行日志收集、上传、分析。近日,我们决定开源Logan生态体系中的存储SDK部分(Android/iOS),希望能够帮助更多开发者合理的解决移动端日志存储收集的相关痛点,也欢迎更多社区的开发者和我们一起共建Logan生态。Github的项目地址参见:https://github.com/Meituan-Di…。背景随着业务的不断扩张,移动端的日志也会不断增多。但业界对移动端日志并没有形成相对成体系的处理方式,在大多数情况下,还是针对不同的日志进行单一化的处理,然后结合这些日志处理的结果再来定位问题。然而,当用户达到一定量级之后,很多“疑难杂症”却无法通过之前的定位问题的方式来进行解决。移动端开发者最头疼的事情就是“为什么我使用和用户一模一样的手机,一模一样的系统版本,仿照用户的操作却复现不出Bug”。特别是对于Android开发者来说,手机型号、系统版本、网络环境等都非常复杂,即使拿到了一模一样的手机也复现不出Bug,这并不奇怪,当然很多时候并不能完全拿到真正完全一模一样的手机。相信很多同学见到下面这一幕都似曾相识:用(lao)户(ban):我发现我们App的XX页面打不开了,UI展示不出来,你来跟进一下这个问题。你:好的。于是,我们检查了用户反馈的机型和系统版本,然后找了一台同型号同版本的手机,试着复现却发现一切正常。我们又给用户打个电话,问问他到底是怎么操作的,再问问网络环境,继续尝试复现依旧未果。最后,我们查了一下Crash日志,网络日志,再看看埋点日志(发现还没报上来)。你内心OS:奇怪了,也没产生Crash,网络也是通的,但是为什么UI展示不出来呢?几个小时后……用(lao)户(ban):这问题有结果了吗?你:我用了各种办法复现不出来……暂时查不到是什么原因导致的这个问题。用(lao)户(ban):那怪我咯?你:……如果把一次Bug的产生看作是一次“凶案现场”,开发者就是破案的“侦探”。案发之后,侦探需要通过各种手段搜集线索,推理出犯案过程。这就好比开发者需要通过查询各种日志,分析这段时间App在用户手机里都经历了什么。一般来说,传统的日志搜集方法存在以下缺陷:日志上报不及时。由于日志上报需要网络请求,对于移动App来说频繁网络请求会比较耗电,所以日志SDK一般会积累到一定程度或者一定时间后再上报一次。上报的信息有限。由于日志上报网络请求的频次相对较高,为了节省用户流量,日志通常不会太大。尤其是网络日志等这种实时性较高的日志。日志孤岛。不同类型的日志上报到不同的日志系统中,相对孤立。日志不全。日志种类越来越多,有些日志SDK会对上报日志进行采样。面临挑战美团点评集团内部,移动端日志种类已经超过20种,而且随着业务的不断扩张,这一数字还在持续增加。特别是上文中提到的三个缺陷,也会被无限地进行放大。查问题是个苦力活,不一定所有的日志都上报在一个系统里,对于开发者来说,可能需要在多个系统中查看不同种类的日志,这大大增加了开发者定位问题的成本。如果我们每天上班都看着疑难Bug挂着无法解决,确实会很难受。这就像一个侦探遇到了疑难的案件,当他用尽各种手段收集线索,依然一无所获,那种心情可想而知。我们收集日志复现用户Bug的思路和侦探破案的思路非常相似,通过搜集的线索尽可能拼凑出相对完整的犯案场景。如果按照这个思路想下去,目前我们并没有什么更好的方法来处理这些问题。不过,虽然侦探破案和开发者查日志解决问题的思路很像,但实质并不一样。我们处理的是Bug,不是真实的案件。换句话说,因为我们的“死者”是可见的,那么就可以从它身上获取更多信息,甚至和它进行一次“灵魂的交流”。换个思路想,以往的操作都是通过各种各样的日志拼凑出用户出现Bug的场景,那可不可以先获取到用户在发生Bug的这段时间产生的所有日志(不采样,内容更详细),然后聚合这些日志分析出(筛除无关项)用户出现Bug的场景呢?个案分析新的思路重心从“日志”变为“用户”,我们称之为“个案分析”。简单来说,传统的思路是通过搜集散落在各系统的日志,然后拼凑出问题出现的场景,而新的思路是从用户产生的所有日志中聚合分析,寻找出现问题的场景。为此,我们进行了技术层面的尝试,而新的方案需要在功能上满足以下条件:支持多种日志收集,统一底层日志协议,抹平日志种类带来的差异。日志本地记录,在需要时上报,尽可能保证日志不丢失。日志内容要尽可能详细,不采样。日志类型可扩展,可由上层自定义。我们还需要在技术上满足以下条件:轻量级,包体尽量小API易用没有侵入性高性能横空出世在这种背景下,Logan横空出世,其核心体系由四大模块构成:日志输入日志存储后端系统前端系统最佳实践日志输入常见的日志类型有:代码级日志、网络日志、用户行为日志、崩溃日志、H5日志等。这些都是Logan的输入层,在不影响原日志体系功能的情况下,可将内容往Logan中存储一份。Logan的优势在于:日志内容可以更加丰富,写入时可以携带更多信息,也没有日志采样,只会等待合适的时机进行统一上报,能够节省用户的流量和电量。以网络日志为例,正常情况下网络日志只记录端到端延时、发包大小、回包大小字段等等,同时存在采样。而在Logan中网络日志不会被采样,除了上述内容还可以记录请求Headers、回包Headers、原始Url等信息。日志存储Logan存储SDK是这个开源项目的重点,它解决了业界内大多数移动端日志库存在的几个缺陷:卡顿,影响性能日志丢失安全性日志分散Logan自研的日志协议解决了日志本地聚合存储的问题,采用“先压缩再加密”的顺序,使用流式的加密和压缩,避免了CPU峰值,同时减少了CPU使用。跨平台C库提供了日志协议数据的格式化处理,针对大日志的分片处理,引入了MMAP机制解决了日志丢失问题,使用AES进行日志加密确保日志安全性。Logan核心逻辑都在C层完成,提供了跨平台支持的能力,在解决痛点问题的同时,也大大提升了性能。为了节约用户手机空间大小,日志文件只保留最近7天的日志,过期会自动删除。在Android设备上Logan将日志保存在沙盒中,保证了日志文件的安全性。详情请参考:美团点评移动端基础日志库——Logan后端系统后端是接收和处理数据中心,相当于Logan的大脑。主要有四个功能:接收日志日志解析归档日志分析数据平台接收日志客户端有两种日志上报的形式:主动上报和回捞上报。主动上报可以通过客服引导用户上报,也可以进行预埋,在特定行为发生时进行上报(例如用户投诉)。回捞上报是由后端向客户端发起回捞指令,这里不再赘述。所有日志上报都由Logan后端进行接收。日志解析归档客户端上报的日志经过加密和压缩处理,后端需要对数据解密、解压还原,继而对数据结构化归档存储。日志分析不同类型日志由不同的字段组合而成,携带着各自特有信息。网络日志有请求接口名称、端到端延时、发包大小、请求Headers等信息,用户行为日志有打开页面、点击事件等信息。对所有的各类型日志进行分析,把得到的信息串连起来,最终汇集形成一个完整的个人日志。数据平台数据平台是前端系统及第三方平台的数据来源,因为个人日志属于机密数据,所以数据获取有着严格的权限审核流程。同时数据平台会收集过往的Case,抽取其问题特征记录解决方案,为新Case提供建议。前端系统一个优秀的前端分析系统可以快速定位问题,提高效率。研发人员通过Logan前端系统搜索日志,进入日志详情页查看具体内容,从而定位问题,解决问题。目前集团内部的Logan前端日志详情页已经具备以下功能:日志可视化。所有的日志都经过结构化处理后,按照时间顺序展示。时间轴。数据可视化,利用图形方式进行语义分析。日志搜索。快速定位到相关日志内容。日志筛选。支持多类型日志,可选择需要分析的日志。日志分享。分享单条日志后,点开分享链接自动定位到分享的日志位置。Logan对日志进行数据可视化时,尝试利用图形方式进行语义分析简称为时间轴。每行代表着一种日志类型。同一日志类型有着多种图形、颜色,他们标识着不同的语义。例如时间轴中对代码级日志进行了日志类别的区分:利用颜色差异,可以轻松区分出错误的日志,点击红点即可直接跳转至错误日志详情。个案分析流程用户遇到问题联系客服反馈问题。客服收到用户反馈。记录Case,整理问题,同时引导用户上报Logan日志。研发同学收到Case,查找Logan日志,利用Logan系统完成日志筛选、时间定位、时间轴等功能,分析日志,进而还原Case“现场”。最后,结合代码定位问题,修复问题,解决Case。定位问题结合用户信息,通过Logan前端系统查找用户的日志。打开日志详情,首先使用时间定位功能,快速跳转到出问题时的日志,结合该日志上下文,可得到当时App运行情况,大致推断问题发生的原因。接着利用日志筛选功能,查找关键Log对可能出问题的地方逐一进行排查。最后结合代码,定位问题。当然,在实际上排查中问题比这复杂多,我们要反复查看日志、查看代码。这时还可能要借助一下Logan高级功能,如时间轴,通过时间轴可快速找出现异常的日志,点击时间轴上的图标可跳转到日志详情。通过网络日志中的Trace信息,还可以查看该请求在后台服务详细的响应栈情况和后台响应值。未来规划机器学习分析。首先收集过往的Case及解决方案,提取分析Case特征,将Case结构化后入库,然后通过机器学习快速分析上报的日志,指出日志中可能存在的问题,并给出解决方案建议;数据开放平台。业务方可以通过数据开放平台获取数据,再结合自身业务的特性研发出适合自己业务的工具、产品。平台支持PlatformiOSAndroidWebMini ProgramsSupport√√√√目前Logan SDK已经支持以上四个平台,本次开源iOS和Android平台,其他平台未来将会陆续进行开源,敬请期待。测试覆盖率由于Travis、Circle对Android NDK环境支持不够友好,Logan为了兼容较低版本的Android设备,目前对NDK的版本要求是16.1.4479499,所以我们并没有在Github仓库中配置CI。开发者可以本地运行测试用例,测试覆盖率可达到80%或者更高。开源计划在集团内部已经形成了以Logan为中心的个案分析生态系统。本次开源的内容有iOS、Android客户端模块、数据解析简易版,小程序版本、Web版本已经在开源的路上,后台系统,前端系统也在我们开源计划之中。未来我们会提供基于Logan大数据的数据平台,包含机器学习、疑难日志解决方案、大数据特征分析等高级功能。最后,我们希望提供更加完整的一体化个案分析生态系统,也欢迎大家给我们提出建议,共建社区。ModuleOpen SourceProcessingPlanningiOS√ Android√ Web √ Mini Programs √ Back End √Front End √团队介绍周辉,项目发起人,美团点评资深移动架构师。姜腾,项目核心开发者。立成,项目核心开发者。白帆,项目核心开发者。招聘点评平台移动研发中心,Base上海,为美团点评集团大多数移动端提供底层基础设施服务,包含网络通信、移动监控、推送触达、动态化引擎、移动研发工具等。同时团队还承载流量分发、UGC、内容生态、整合中心等业务研发,长年虚位以待有志于专注移动端研发的各路英雄。欢迎投递简历:hui.zhou#dianping.com。

October 12, 2018 · 1 min · jiezi