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

前端组件化开发实践

美团点评阅读(1663)

前言

一位计算机前辈曾说过:

Controlling complexity is the essence of computer programming.

随着前端开发复杂度的日益提升,组件化开发应运而生,并随着 FIS、React 等优秀框架的出现遍地开花。这一过程同样发生在美团,面临业务规模的快速发展和工程师团队的不断扩张,我们历经引入组件化解决资源整合问题、逐步增强组件功能促进开发效率、重新打造新一代组件化方案适应全栈开发和共享共建等阶段,努力“controlling complexity”。本文将介绍我们组件化开发的实践过程。

组件化 1.0:资源重组

在美团早期,前端资源是按照页面或者类似业务页面集合的形式进行组织的。例如 order.js 对应订单相关页面的交互,account.css 对应账户相关页面的样式。这种方式在过去的较长一段时间内,持续支撑了整个项目的正常推进,功勋卓著。

legacy-flow

随着业务规模的增加和开发团队的扩张,这套机制逐渐显示出它的一些不足:

  • 资源冗余

    页面的逐渐增加,交互的逐渐复杂化,导致对应的 css 和 js 都有大幅度增长,进而出现为了依赖某个 js 中的一个函数,需要加载整个模块,或者为了使用某个 css 中的部分样式依赖整个 css,冗余资源较多

  • 对应关系不直观

    没有显而易见的对应规则,导致的一个问题是修改某个业务模块的 css 或者 js 时,几乎只能依靠 grep。靠人来维护页面模块 html、css 和 js 之间的依赖关系,容易犯错,常常出现内容已经删除但是 css 或 js 还存在的问题

  • 难于单元测试

    以页面为最小粒度进行资源整合,不同功能的业务模块相互影响,复杂度太高,自动化测试难以推进

2013 年开始,在调研了 FIS、BEM 等方案之后,结合美团开发框架的实际,我们初步实现了一套轻量级的组件化开发方案。主要的改进是:

  • 以页面功能组件为单位聚合前端资源
  • 自动加载符合约定的 css、js 资源
  • 将业务数据到渲染数据的转换过程独立出来

component-flow

举例来说,美团顶部的搜索框就被实现为一个组件。

smart-box

代码构成:

www/component/smart-box/
├── smart-box.js    # 交互
├── smart-box.php   # 渲染数据生产、组件配置
├── smart-box.scss  # 样式
├── smart-box.tpl   # 内容
└── test
    ├── default.js  # 自动化测试
    └── default.php # 单测页面

调用组件变得十足简单:

echo View::useComponent('smart-box', [
    'keyword' => $keyword
]);

对比之前,可以看到组件化的一些特点:

  • 按需加载

    只加载必要的前端资源

  • 对应关系非常清晰

    组件所需要的前端资源都在同一目录,职责明确且唯一,对应关系显著

  • 易于测试

    组件是具备独立展现和交互的最小单元,可利用 Phantom 等工具自动化测试

此外,由于前端资源集中进行调度,组件化也为高阶性能优化提供了空间。例如实现组件级别的 BigRender、通过数据分析进行资源的合并加载等等。

组件化 2.0:趋于成熟

组件化 1.0 上线后,由于简单易用,很快得到工程师的认可,并开始在各项业务中应用起来。新的需求接踵而来,一直持续到 2014 年底,这个阶段我们称之为组件化 2.0。下面介绍下主要的几个改进。

Lifecycle

组件在高内聚的同时,往往需要暴露一些接口供外界调用,从而能够适应复杂的页面需求,例如提交订单页面需要在支付密码组件启动完成后绑定提交时的检查。Web Components、React 等都选择了生命周期事件/方法,我们也是一样。

组件的生命周期:

component-lifecycle

一个组件的完整生命周期包括:

  • init,初始化组件根节点和配置
  • fetch,加载 css 和 js 资源
  • render,内容渲染,默认的渲染内容方式是 BigRender
  • ready,进行数据绑定等操作
  • update,数据更新
  • destroy,解除所有事件监听,删除所有组件节点

组件提供 pause、resume 方法以方便进行生命周期控制。各个阶段使用 Promise 串行进行,异步的管理更清晰。使用自定义语义事件,在修改默认行为、组件间通信上充分利用了 YUI 强大的自定义事件体系,有效降低了开发维护成本。

举个例子,页面初始化时组件的启动过程实际也是借助生命周期实现的:

var afterLoadList = [];
Y.all('[data-component]').each(function (node) {
    var component = new Y.mt.Component(node);
    // 绑定 init 生命周期事件,在 init 默认行为完成后执行回调
    component.after('init', function (e) {
        // 如果配置了延迟启动
        if (e.config.afterLoad) {
            // 暂停组件生命周期
            e.component.pause();
            // 压入延迟启动数组
            afterLoadList.push(e.component);
        }
    });
    // 开始进入生命周期
    component.start();
});

Y.on('load', function () {
    // 在页面 load 事件发生时恢复组件生命周期
    afterLoadList.forEach(function (component) {
        component.resume();
    });
});

回过头来看,引入生命周期除了带来扩展性外,更重要的是理顺了组件的各个阶段,有助于更好的理解和运用。

Data Binding

数据绑定是我们期盼已久的功能,将 View 和 ViewModel 之间的交互自动化无疑会节省工程师的大量时间。在组件化减少关注点和降低复杂度后,实现数据绑定变得更加可能。

我们最终实现的数据绑定方案主要参考了 Angular,通过在 html 节点上添加特定的属性声明绑定逻辑,js 扫描这些内容并进行相应的渲染和事件绑定。当数据发生变化时,对应的内容全部重新渲染。

<ul class="addressList">
    <li
        mt-bind-repeat="addr in addrList"
        mt-bind-html="addr.text"
    >
    </li>
</ul>

<script>
Y.use(['mt-bind', 'mt-scope'], function () {
    Y.mt.bind.init(document.body);
    var scope = Y.one('.addressList').getScope();
    // 将 scope.addrList 设置为一个数组,DOM 上将自动渲染其内容   
    scope.$set('addrList', [
        { text: "first address" },
        { text: "second address" }
    ]);
});
</script>

使用属性声明绑定逻辑的好处是可以同时支持后端渲染,这对于美团团购这样的偏展现型业务是非常必要的,用户可以很快看到页面内容。

Flux

实现数据绑定后,我们不得不面对另外一个问题:如何协同多个组件间的数据。因为某个组件的数据变化,很有可能引起其他组件的变化。例如当修改购买数量,总金额会变化,而总金额超过 500 后,还需要展示大额消费提醒。

为了解决这个问题,我们引入了 Flux,使用全局消息总线的思路进行跨组件交互。

例如因为交互复杂而一直让我们非常头疼的项目购买页,在应用组件 + Flux 重构后,各模块之间的互动更加清晰:

component-flux

其他方面的改进还有很多,包括引入模板引擎 LightnCandy 约束模板逻辑、支持组件任意嵌套、支持异步加载并自动初始化等。

随着组件化 2.0 的逐步完善,基本已经可以从容应对日常开发,在效率和质量方面都上了一个台阶。

组件化 3.0:重启征程

时间的车轮滚滚前行,2014 年底,我们遇到一些新的机遇和挑战:

  • 基于 Node 的全栈开发模式开始应用,前后端渲染有了更多的可能性
  • YUI 停止维护,需要一套新的资源管理方案
  • 新业务不断增加,需要找到一种组件共享的方式,避免重复造轮子

结合之前的实践,以及在这一过程中逐渐积累的对业内方案的认知,我们提出了新的组件化方案:

  • 基于 React 开发页面组件,使用 NPM 进行分发,方便共建共享
  • 基于 Browserify 二次开发,建设资源打包工具 Reduce,方便浏览器加载
  • 建设适应组件化开发模式的工程化开发方案 Turbo,方便工程师将组件应用于业务开发中

React

在组件化 2.0 的过程中,我们发现很多功能和 React 重合,例如 Data Binding、Lifecycle、前后端渲染,甚至直接借鉴的 Flux。除此之外,React 的函数式编程思想、增量更新、兼容性良好的事件体系也让我们非常向往。借着前端全栈开发的契机,我们开始考虑基于 React 进行组件化 3.0 的建设。

NPM + Reduce

NPM + Reduce 构成了我们新的资源管理方案,其中:

  • NPM 负责组件的发布和安装。可以认为是“分”的过程,粒度越小,重用的可能性越大
  • Reduce 负责将页面资源进行打包。可以认为是“合”的过程,让浏览器更快地加载

一个典型的组件包:

smart-box/
├── package.json    # 组件包元信息
├── smart-box.jsx   # React Component
├── smart-box.scss  # 样式
└── test
    └── main.js     # 测试

NPM 默认只支持 js 文件的管理,我们对 NPM 中的 package.json 进行了扩展,增加了 style 字段,以使打包工具 Reduce 也能够对 css 和 css 中引用的 image、font 进行识别和处理:

{
    "style": "./smart-box.scss"
}

只要在页面中 require 了 smart-box,经过 Reduce 打包后,js、css 甚至图片、字体,都会出现在浏览器中。

var SmartBox = require('@mtfe/smart-box');
// 页面
var IndexPage = React.createClass({
    render: function () {
        return (
            <Header>
                <SmartBox keyword={ this.props.keyword } />
            </Header>
            ...
        );
    }
});
module.exports = IndexPage;

整体思路和组件化 1.0 如出一辙,却又那么不同。

Turbo

单单解决分发和打包的问题还不够,业务开发过程如果变得繁琐、难以 Debug、性能低下的话,恐怕不会受到工程师欢迎。

为了解决这些问题,我们在 Node 框架的基础上,提供了一系列中间件和开发工具,逐步构建对组件友好的前端工程化方案 Turbo。主要有:

  • 支持前后端同构渲染,让用户更早看到内容
  • 简化 Flux 流程,数据流更加清晰易维护
  • 引入 ImmutableJS,保证 Store 以外的数据不可变
  • 采用 cursor 机制,保证数据修改/获取同步
  • 支持 Hot Module Replacement,改进开发流自动化

通过这些改进,一线工程师可以方便的使用各种组件,专注在业务本身上。开发框架层面的支持也反过来促进了组件化的发展,大家更乐于使用一系列组件来构建页面功能。

小结

发现痛点、分析调研、应用改进的解决问题思路在组件化开发实践中不断运用。历经三个大版本的演进,组件化开发模式有效缓解了业务发展带来的复杂度提升的压力,并培养工程师具备小而美的工程思想,形成共建共享的良好氛围。毫无疑问,组件化这种“分而治之”的思想将会长久地影响和促进前端开发模式。我们现在已经准备好,迎接新的机遇和挑战,用技术的不断革新提升工程师的幸福感。

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

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

美团点评阅读(0)

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

可用性的理解

理解目标

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

result

拆解目标

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

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

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

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

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

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

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

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

幼儿时期:2012年前

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

result

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

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

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

result

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

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

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

result

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

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

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

result

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

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

result

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

易运营

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

可限流

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

无状态

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

降级能力

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

result

可测试

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

result

降低发布风险

严格的发布流程

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

result

灰度机制

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

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

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

result

持续关注线上运行情况

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

快速的发现机制

告警的移动化

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

告警的实时化

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

监控的可视化

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

result

有效的恢复机制

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

几点经验

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

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

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

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

可用性不只是技术问题。

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

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

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

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

YUI3在美团的实践

美团点评阅读(1642)

美团网在2010年引爆了团购行业,并在2012年销售额超过55亿,实现了全面盈利。在业务规模不断增长的背后,作为研发队伍中和用户最接近的前端团队承担着非常大的压力,比如用户量急剧上升带来的产品多样化,业务运营系统的界面交互日益复杂,代码膨胀造成维护成本增加等等。面对这些挑战,我们持续改进前端技术架构,在提升用户体验和工作效率的同时,成功支撑了美团业务的快速发展,这一切都得益于构建在YUI3框架之上稳定高效的前端代码。在应用YUI3的过程中,我们团队积累了一些经验,这里总结成篇,分享给大家。

为什么选择YUI3

使用什么前端基础框架是建立前端团队最重要的技术决策之一。美团项目初期因为要加快开发进度,选择了当时团队最熟悉的YUI2(前框架时代杰出的类库),保证美团能够更快更早地上线,抢占市场先机。不久由于前端技术发展很快,YUI2的缺点逐渐凸显,例如开发方式落后、影响工作效率等等,于是我们开始考虑基础库的迁移。

经过一段时间对主流前端库、框架的反复考量,我们认为YUI3是最适合我们团队使用的基础框架。

首先,国内的开源框架及其社区刚开始起步,在代码质量、架构设计和理念创新上还难以跟YUI3比肩,所以基本排除在外。其次,国外像YUI3这样面向用户产品、文档丰富、扩展性良好的成熟框架屈指可数,例如ExtJS和Dojo则更适合业务复杂的传统企业级开发。最后,使用jQuery这种类库构建同YUI3一样强大的框架对创业团队来说并不可取,美团快速发展、竞争多变的业务特点决定了我们必须把主要精力放在更高一层的业务开发上,而不是去重复发明一个蹩脚的YUI。

YUI3成为最终选择有以下几个直接的原因:

  • 非常优秀,是真正的框架,真正的重型武器,具有强劲的持续开发能力,可以应对业务的快速发展。不管是规模不断增长的用户产品,还是交互日趋复杂的业务系统(美团有超过100个业务系统作全电子化的运营支撑),YUI3都游刃有余。
  • 代码整齐规范,容易维护,适合有洁癖的工程师,同时能够显著提高团队协作时的开发效率。因为人手紧缺,后端工程师也需要参与前端开发,一致的代码风格使前后端配合轻松简单。
  • 有出色的架构设计,是很好的框架范本,通过研究学习可以帮助工程师成长,培养良好的工程思维。人是美团最重要的产品。

随着团队成长,我们最后引入了YUI3,在迁移过程中,遇到了很多技术上的和工程上的挑战,但是我们一直在前进,一直在行进中开火。从结果来看,YUI3为我们团队提供了先进生产力,为快速开发、快速部署、快速迭代提供了源源不断的力量。

YUI3的优秀主要表现在模块和组件框架的出色设计,下面我们着重介绍这两方面的一些实践经验。

改变一切的模块

前端开发日益复杂化,代码组织成为一个显著的问题。受到后端代码普遍采用的模块机制启发,很多前端模块机制应运而生。目前比较著名的有CommonJS和AMD。但早在2008年8月13日,YUI3 Preview Release 1中就已经给出了YUI团队的解决方案,并在2009年9月29日YUI3正式版发布时定型。

以下是使用YUI3进行模块化开发的简单例子

// 定义模块
YUI.add('greeting', function (Y) {
    Y.sayHello = function () {
        console.log('Hello, world!');
    };
});

// 调用模块
YUI().use('greeting', function (Y) {
    Y.sayHello(); // output 'Hello, world!'
});

模块的引入,使得更细粒度的按功能进行代码组织成为可能,也为方便的进行扩展和分层提供了基础,自底向上的彻底改变了YUI3。一套完整的模块机制,还包括解决关系依赖、自动加载的Loader和提高加载效率的Combo。

面对如此彻底的改变,我们需要解决很多挑战:

  • 如何将原来的功能划分为模块?
  • 如何管理模块元信息?
  • 如何高效的获取模块?

划分模块

经过两年来不断的实践和总结,我们归纳了如下几条划分模块的原则:

  • 抽象与应用脱离。更通用的功能放在更低的层级,应用层完全面向实际问题,在解决的过程中调用抽象出来的方法。
  • 职责单一。保持每个模块的足够简单和专一,方便维护和可持续开发。
  • 粒度得当。有了Combo,我们可以不必担心粒度太小,文件过多导致的速度问题。但是,从可维护的角度来考虑,粒度应该适当而不宜过小,避免海底捞针的情形出现。
  • 海纳百川。我们的模块体系应该是开放的,不符合YUI规范的第三方模块,可以借鉴整合进来,使我们的基础框架更加完善,更加性感。

美团前端架构

按照模块的层次划分,美团的JS框架可以分为四个层次:

  • 最底层交给强悍的YUI3,为我们提供跨浏览器兼容的API和良好的框架设计。
  • 第二层是我们二次开发的核心方法、组件(Component)和控件(Widget)。现已独立为前端核心库,为美团所有系统提供前端支持。核心库的种子文件中定义了全局变量M,除了对YUI3进行封装的代码以外,还包含了对语言层面的扩展,以及一些基础工具类。核心库有一个非常重要的组成部分,就是我们功能丰富的控件集合,比如常用的自动完成、排序表格、气泡提示、对话框等基础控件。除了这些,核心库还包含了常用的基础组件、插件(Plugin)、扩展(Extension)以及单元测试代码。
  • 第三层包含各个系统的一些通用模块。例如www-base模块包含美团主站(www)的消息系统、用户行为追踪系统等通用功能。这一层更加接近应用。
  • 最上面一层,应用模块。这些模块的方法都是用来解决实际业务问题。例如www-deal用来处理美团主站所有deal相关功能的交互,finance-pay用来处理财务系统中付款相关的交互。一些零碎的应用方法我们放在对应系统的misc模块中,避免模块碎片化。

这套框架仍在不断演变,以便更好的支撑业务需求。其中一个明显的方向是,在第二层和第三层之间,出现一个为了更好整合所有内部业务系统前端通用资源的中间层。

管理模块元信息

模块元信息主要包括模块名称、路径、依赖关系等内容。其中最为重要的是依赖关系,这决定了有哪些模块需要加载。为了实现自动加载,需要将所有模块的元信息提供给YUI的Loader。

最初,为了更快的从YUI2迁移到YUI3,模块元信息放在PHP中进行维护。随着时间的推移,渐渐显示出很多弊端。首先,在定义模块的js文件中已经包含模块名称、依赖关系等信息,和PHP中内容重复。其次,这些元信息最终直接输出到html中,没有有效利用缓存。

随后,我们使用NodeJS开发了一系列脚本,收集所有模块元信息,保存为独立js文件,并实现了自动化。为了防止出错,在Git Hooks和上线脚本中都加入了校验过程。工程师需要做的,只是修改模块定义中的元信息。

最近一段时间,我们的精力主要放在两个方面:

  • 自动生成依赖。随着模块粒度细化和模块数量的增长,依赖关系日益复杂,依靠人工配置经常出现过多依赖或过少依赖等问题。我们准备开发一套自动扫描模块引用API,并确定依赖关系的机制。
  • 自动打包依赖模块。如果在代码发布时,就已根据页面模块调用计算好所有依赖模块,并进行打包,可以避免引用全部模块元信息、Loader计算依赖等过程,提高网站性能。

Combo

Combo可以一次请求多个文件,能够有效解决多个模块加载带来的性能问题。Yahoo提供了Combo服务,但只能提供YUI3模块,而且速度在国内并不理想。为了提供更好的体验,让用户访问速度更快,我们最终考虑搭建自己的Combo服务,并把Combo发布到CDN上。

以下是一个Combo请求的例子:

http://c.meituan.net/combo/?f=mt-yui-core.v3.5.1.js;fecore/mt/js/base.js

为了节约时间,我们最开始采用了开源的minify,经过一些修改和配置,就可以在生产和开发环境提供Combo服务。使用一段时间后,发现minify过于复杂,以至于添加一些定制功能相当困难。我们需要的只是简单的文件合并功能,在明确需求和开发量后,着手开发自己的Combo程序。从最初的仅支持文件合并,后来陆续添加了服务器/浏览器端缓存、文件集别名、调试模式、CSS图片相对路径转URI、错误日志等特性,全部代码仅有300多行。经过两年时间以及每天几千万PV的考验,服务一直非常稳定。

灵活健壮的组件框架

YUI3之所以成为纯粹的框架,真正的原因在于提供了一套灵活、健壮的组件框架。借助这套框架,可以轻松的将业务场景进行解耦、分层,并持续的进行改进。通过不断的实践,我们越发认为这是YUI3的精髓所在。

从YUI3定义的开发范式和源代码中可以看出,YUI团队非常重视AOP(Aspect Oriented Programming)和OOP(Object Oriented Programming),这一点可以在接下来的介绍中有所体会。

EventTarget、Attribute和Base

在介绍组件框架之前,有必要首先了解下EventTarget。YUI3创建了一套类似DOM事件的自定义事件体系,支持冒泡传播、默认行为等功能。EventTarget提供了操作自定义事件的接口,可以让任意一个对象拥有定义、监听、触发、注销自定义事件的功能。YUI组件框架中的所有类,以及在此框架之上开发的所有组件,都继承了EventTarget。

Attribute是组件框架中最底层的类,实现了数据和逻辑的完美解耦。为什么说是完美呢?存储在attribute(Attribute提供的数据存取接口)中的数据发生变化时,会触发相应的事件,为相关的逻辑处理提供了便捷的接口。从下面这个简单的例子可以感受到这一点:

// 在name属性变化时,触发nameChange事件
this.on('nameChange', function (e) {
    console.log(e.newVal);
});

// 修改name属性
this.set('name', 'meituan'); // output 'meituan'

实践中发现,妥善处理属性的分类非常重要。供实例进行操作的属性适合作为attribute,例如表单验证组件FormChecker的fields属性,方便应用层进行表单项的增删改。在类方法内部使用的一些属性可以作为私有属性,例如计时器、监听器句柄。供所有类的实例使用的一些常量适合作为类的静态属性,例如一些模板、样式类。

Base是组件框架的核心类。它模拟了C++、Java等语言的经典继承方式和生命周期管理,借助Attribute来实现数据与逻辑的分离,并提供扩展、插件支持,从而获得了良好的扩展性以及强大的可持续开发能力。YUI团队通过多年来对业务实践的抽象,最终演化而成一种开发范式,这,就是一切组件的基石——Base,实至名归。

依照这种范式,我们开发了一系列组件,例如之前提到的FormChecker,以及延迟加载器LazyLoader、地图的封装Map等。最显著的体会是,开发思路更为清晰,代码结构更有条理,维护变得简单轻松。

// 构造方法
FormChecker.prototype.initializer = function () {
    var form = this.get('form');
    this._handle = form.on('submit', function (e) {
        // check fields
    });
};
// 析构方法
FormChecker.prototype.destructor = function () {
    this._handle.detach();
};

// 创建实例时,自动执行构造方法
var checker = new FormChecker({ form: Y.one('#buy-form') });
// 销毁实例时,自动执行析构方法
checker.destroy();

Extension和Plugin

Extension(扩展)是为了解决多重继承,以一种类似组合的方式在类上添加功能的模式,它本身不能创建实例。这种设计非常像Ruby等语言中的Mixin。Plugin(插件)的作用是在对象上添加一些功能,这些功能也可以很方便的移除。

它们有什么区别呢?简单来说,Extension是在类上加一些功能,所有类的实例都拥有这些功能。Plugin只是在某些类的实例中添加功能。举两个典型的例子:一些节点需要使用动画效果,这个功能适合作为Plugin。气泡提示控件需要支持多种对齐方式,所有实例都需要此功能,因此使用YUI3的WidgetPositionAlign扩展。

// 传统的函数方式实现动画
Effect.fadeIn(nodeTip);

// 插件方式实现动画
nodeTip.plug(NodeEffect);
nodeTip.effect.fadeIn();

Extension和Plugin很好的解决了我们遇到的诸多功能重用问题。我们开发了提供全屏功能的WidgetFullScreen、自动对齐对话框的DialogAutoAlign等扩展,以及进行异步查询的AsyncSearch、提供动画效果的NodeEffect等插件。将这些偏重OOP的编程思想应用在前端开发中,比较深刻的体会是:有更多的概念清晰、定位明确的开发模式可以选择。

Widget体系

Widget(控件)建立在Base之上,主要增加了UI层面的功能,例如renderUIbindUIsyncUI等生命周期方法,HTML_PARSER等渐进增强功能,以及样式类、HTML结构和DOM事件的统一管理。Widget提供了控件开发的通用范式。

由于前端资源相对紧张,我们倾向于大量使用控件,尤其在业务系统这样更注重功能的场景。主要出于两点考虑:

  • 减少不必要的重复劳动,提高产出。通过将交互、业务逻辑合理抽象,一次解决一类问题,One Shot One Kill。
  • 节约前端工程师资源。通过自动加载和初始化控件、封装简单易用的后端方法、制作Demo和使用手册等措施,降低使用门槛,后端工程师只需要知道参数的数据结构就可以轻松调用,提高了开发效率。

以下是一个自动加载控件的例子

// 页面初始化时,会扫描所有带有data-widget属性的节点,自动加载对应控件,并根据data-params数据进行初始化
<a href="…" data-widget="bubbleTip" data-params='{ "tip": "全新改版,支持随时退款" }'>下载手机版</a>

目前,我们已经构建了一个包含近30个控件的Widget体系,为所有系统提供丰富、便捷、集成的解决方案。

行进中开火

在整个YUI3的实践中,我们犯过很多错误,例如全局只有一个YUI实例、Combo的CSS图片依赖等等,但这些并没有成为放弃的理由。从今天回过头来看,YUI3带给我们团队的,不只是更高的开发效率、更好的可持续开发能力,还有它本身的设计思路、源码书写、辅助工具等诸多方面潜移默化的影响。这些回报的价值,比起较高的使用门槛、犯过的一些错误,要贵重百倍。

指导这一切的,是我们始终坚持的 “行进中开火”。在互联网这个高速发展的行业里,对于我们这种小规模的创业团队,一天不前进,就意味落后。做事不应该准备太多,一定要先做起来,然后发现不足并不断改进,宁可十年不将军,不可一日不拱卒。每天都做得更好一点,日积月累,我们才会在激烈的竞争中占据越来越大的优势。

YUI3并非完美,存在着学习成本高、对社区不够开放等问题。我们所做的更远非完美,但经过不断的尝试和经验的积累,已经渐渐摸索出一条明确的路线,并会坚持不懈的继续走下去。

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

COS系统的前端演变和发展

美团点评阅读(2455)

美团COS:全称美团网核心业务系统部,以持续整合O2O线下资源,共建高效率、低成本的供应链系统,高效推动O2O生态环境建设为业务目标,负责美团网核心业务系统的建设和管理。

COS系统,伴随着美团3年多的发展,前端也积极参与到系统的建设中。 在这几年里,通过优化系统前端环境,改进代码组织结构,丰富公共资源和自动化工具,不断提高了业务响应效率,也在不断努力去逐步缩短系统的前端开发周期,以下简单介绍在这个过程中的一些变化。

第一个COS系统——合同系统

  • 没有独立的静态资源服务,无压缩,采用YUI3的Loader,手动维护依赖关系。
  • 忙着写控件,第一个控件Autocomplete,后来有了Table、Tree、Form、IO等。
  • 线上代码经常不稳定,静态资源地址采用加时间戳的方式来更新。

公共模板部署

由于前端需要支持的业务系统众多,对每个系统而言,都有一些相同的处理逻辑,如前端环境初始化(包括系统的参数配置、YUI部署、UI部署、控件初始化、GA统计源、页面加载时间统计、浏览器升级提醒、问题反馈等)针对每个系统都是一样的,不希望每个系统都要去处理这些逻辑,于是集成了mt-fe.jar到每个后台系统,节约了新开系统的成本。

xxx

模块化道路

  • 模块目录结构扁平化

    所有的模块在目录结构上都是平行的,无区别的。 同时增加了主模块和子模块的概念,并在此基础上定义了统一的加载规则。

  • 模块名称和路径关系约定

    知道一个模块名就可以知道这个模块的代码所在的位置,是否是主模块以及属于某个系统,如:

    crm-module 对应的三个属性应该是:
    {
      path: "/static/module/module.js",
      isMainModule: true,
      app: 'crm'
    }
    deal-module/sub 对应的即:
    {
      path: "/static/module/sub.js",
      isMainModule: false,
      app: 'deal'
    }
    
  • 模块加载机制

    使用YUI3的自动加载,需要给Loader配置一个依赖关系表。最初新增一个模块时,需要在模块定义和Loader配置中都声明该模块的依赖。这样在两个地方维护依赖关系,容易产生不一致,从而带来维护问题。
    为解决上述问题,开发了脚本自动计算所有模块的依赖关系,生成依赖关系表传递给Loader使用,面临的问题是修改模块依赖关系需要运行脚本才能生效,而在开发时更想要所见即所得的效果。于是又针对开发环境,在Loader加载时根据约定的模块名,自动计算出模块的加载路径和类型,从而实现不提前配置依赖关系表也可自动加载。

      一个简单的加载配置
      var metaGroups = {
          "fecore": {
              //发布时自动生成的metaGroups,用于线上环境
              modules: {
                  "moduleA": {
                      path: "moduleA/moduleA.js",
                      requires: ["moduleB", "moduleC"]
                  },
                  ...
              },
              //根据pattern和文件名约定进行自动加载,用于开发环境
              patterns: {
                  "prefix": function(cfg) {
                      cfg.path = "moduleA/moduleA.js";
                      cfg.type = "js";
                      return true;
                  },
                  ...
              }
          }
      };
    
      YUI({
          ...
          groups: metaGroups,
          ...
      }).use('moduleA', function(Y) {
    
      });
    
  • 模块依赖关系梳理

    模块中存在间接依赖,如A依赖B、C,B依赖C,这时在A的依赖关系中只需要声明B就可以工作,如果某天B不需要依赖C了,这时在B中去掉C的成本就变大了。为了解决这类问题,规范了依赖关系声明,并开发工具对源文件进行分析,自动化校验和修改,也计划将该校验加入到各代码仓库的git hooks中。通过该工具的梳理,让开发者能非常明确了解所有模块之间的关系,对宏观掌握当前模块的使用状态也是非常有帮助的。
    xxx

  • 模块的丰富和稳定

    前端支持的项目众多,如何在应用层花最小的代价写代码,是我们一直在思考的问题。 通过不断丰富可复用的组件库、定义统一的UI方案以及提取和整合所有系统的公共模板等来避免重复工作。 目前,除了所有前端公用的代码仓库fe.core外,也为COS系统新增专门的前端代码仓库cos.core,存放和业务相关的模块。同时为了保障模块的稳定性和易用性,开发了模块文档,并进行了测试用例的覆盖。

    • 模块内目录结构完善
      模块中从只包含css、css、tpl文件到包含tests、guide等文件,目前一个完整的模块的目录结构如:

      xxx

    • 组件方面
      从简单的构造器、prototype写对象实现继承到基于YUI3的Widget or Base框架,并在此基础上进行了扩展;不断新增组件和完善组件功能,使其能满足大多数业务需求;对代码进行不断重构,使得组件可以更加稳定。
      我们提倡只要是能被重用的代码,都应该放到相应的公共代码仓库中。

    • UI方面
      不得不说Bootstrap带给web行业的影响是巨大的,特别是针对后台系统。 简洁大气的设计,对于大多数网页元素来讲已经能较好的满足需求,不过针对COS系统,还是有不少需要单独处理的需求,比如各个系统的Layout,一些简单的UI模块和少量交互的组件等,所以在全兼容Bootstrap的基础上做了COS-UI,并对所有的COS系统页面进行了迁移,统一了风格。 也为界面外观定义提供统一标准,降低开发与维护成本。

    • 测试方面
      从使用YUI3提供的YUI Test模块编写单元测试用例,到使用mocha+chai+sinon结合,采用phantomjs进行无界面测试,无缝集成到开发环境,让写测试变的更简单,从而提高了写测试用例的积极性。

    • 文档方面
      从最初专门开发一个应用去为模块编写使用文档,到文档静态化。在完善的模块目录结构基础上,通过梳理文档规范,根据约定自动输出静态化的文档;从静态的demo展示到可以在线修改;从手写的使用说明,到根据YUIDoc生成的注释自动提取文档内容等。使得只需要写最少的内容,即可生成丰富的文档和demo。
      如下是一个简单的构建demo的规范:

        <div class="demo">
            <h1>简单demo</h1>
            <div class="html-content">
                ...
            </div>
            <script>
                ...
            </script>
        </div>
      

      只需要按照上述格式写代码,工具就会自动生成如下静态页面,可以在页面中进行参数修改,便可即时查看到效果。
      demo

COS系统前端分层

用户端和核心业务端的模块都是基于YUI3进行开发,同时在模块化机制的前提下,共用底层库fe.core。 为了更好地针对所有系统业务场景做抽象,开发了专门提供给业务系统使用的模块cos.core。

配置中心会处理所有系统的前端配置,如当前系统环境(开发环境、测试环境、线上环境),YUI的版本号,是否使用Combo服务,是否是调试模式等。

xxx

系统前端开发环境

为了方便系统开发,针对一些平时应用比较普遍的场景开发了自动化的工具,如发布、自动化文档、依赖关系检测、自动化单元测试、全部系统范围内搜索、自动build template等。为了使工具更容易维护,权责更加明晰,在代码组织和管理方面,先后对代码仓库进行了拆分,发布package到内部源,并使用npm来进行包管理,解决了package之间的依赖管理问题。

同时针对各系统提供了一系列的服务,如静态资源、Combo、日志、页面加载性能报表等。 未来还计划开放UI定制,一键开站,动态修改系统配置,在线为某个模块写文档、demo、test,在线管理静态资源等功能。

开发平台旨在希望作为一个窗口,索引与前端有关的所有服务和资源,为开发者提供开发辅助。

xxx

系统发布

  • 系统运行初期,使用Shell脚本处理发布过程(包括资源的压缩、加版本号、计算依赖关系等)。后来由于涉及到的代码仓库增多,发布过程也增加了更多的逻辑,如打包公共模块、修改模板中引用的CSS、图片资源地址等,使得脚本一度维护比较困难,后对脚本进行了拆分。 再后来,考虑到Node的灵活以及社区的活跃,将发布脚本迁移到Node平台,使用Grunt来管理发布任务,同时独立了配置,代码库进行了更细粒度的拆分,使得发布这一过程更加灵活和便于维护。
    xxx

  • 系统发布从后端人工操作到集成到OPS平台一键发布,大大提高了发布效率,减少了bug处理时间。

  • 从使用外部npm源到内部npm源,减少了发布本身的耗时。

以上也为前端cos组在系统建设方面做一个简单总结。非常有幸能在一个重视技术,重视前端的公司里学习成长。回想起这么多的日日夜夜,曾面对每一次技术改造和调整都兴奋不已,会偶尔想方案彻夜难眠,走了很多弯路,开发了很多系统,但每次都能从看似相似其实充满新挑战的系统中获得新的收获。期望每天的点滴进步会让系统开发变得越来越简单高效,Happy Coding!

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

美团数据仓库-数据脱敏

美团点评阅读(2507)

背景与目标

在数据仓库建设过程中,数据安全扮演着重要角色,因为隐私或敏感数据的泄露,会对数据主体(客户,员工和公司)的财产、名誉、人身安全、以及合法利益造成严重损害。因此我们需要严格控制对仓库中的数据访问,即什么样的人员或者需求才可以访问到相关的数据。这就要求对数据本身的敏感程度进行安全级别划分。数据有了安全等级的划分,才能更好管理对数据访问控制,以此来保护好数据安全。

举个例子简单的说明下,例如我们仓库中有一张关于注册用户的基本信息表User,其中有手机号mobile,昵称username两个字段。我们在划分数据安全层级的时,将用户mobile的安全等级划分为L2要高于username的等级L1,并规定只有访问权限达到L2的运营部门才能访问mobile字段。这样在公司各个部门需要访问注册用户基本信息表User时,我们只需检查访问者是否来自运营部门,如果是运营部可以访问mobile,如果不是只能访问username信息了。这样就有效的防止用户手机号被不相关工作人员泄露出去,同时也不影响查询用户username的需求。
但是往往在实际生产过程中,应用场景会更加复杂,仅靠类似这样的访问控制,满足不了生产的需要,还需要结合其它的途径,而数据脱敏就是一种有效的方式,既能满足日常生产的需要,又能保护数据安全。
数据脱敏,具体指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。这样可以使数据本身的安全等级降级,就可以在开发、测试和其它非生产环境以及外包或云计算环境中安全地使用脱敏后的真实数据集。借助数据脱敏技术,屏蔽敏感信息,并使屏蔽的信息保留其原始数据格式和属性,以确保应用程序可在使用脱敏数据的开发与测试过程中正常运行。

敏感数据梳理

在数据脱敏进行之前,我们首先要确定哪些数据要作为脱敏的目标。我们根据美团特有的业务场景和数据安全级别划分(绝密、高保密、保密、可公开,四个级别), 主要从“高保密”等级的敏感数据,开始进行梳理。
这里我们把敏感数据分成四个维度进行梳理,用户、商家、终端、公司。

  1. 从用户维度进行梳理可能有这些敏感字段如下:手机号码、邮件地址、账号、地址、固定电话号码等信息(此外个人隐私数据相关还有如:种族、政治观点、宗教信仰、基因等)
  2. 从商家维度进行梳理:合同签订人,合同签订人电话等(不排除全局敏感数据:如商家团购品类等)
  3. 从用户终端维度进行梳理:能够可能标识终端的唯一性字段,如设备id。
  4. 从公司角度进行梳理:交易金额、代金卷密码、充值码等

确定脱敏处理方法

梳理出了敏感数据字段,我们接下来的工作就是如何根据特定的应用场景对敏感字段实施具体的脱敏处理方法。
常见的处理方法如下几种有:

  1. 替换:如统一将女性用户名替换为F,这种方法更像“障眼法”,对内部人员可以完全保持信息完整性,但易破解。
  2. 重排:序号12345重排为54321,按照一定的顺序进行打乱,很像“替换”, 可以在需要时方便还原信息,但同样易破解。
  3. 加密:编号12345加密为23456,安全程度取决于采用哪种加密算法,一般根据实际情况而定。
  4. 截断:13811001111截断为138,舍弃必要信息来保证数据的模糊性,是比较常用的脱敏方法,但往往对生产不够友好。
  5. 掩码: 123456 -> 1xxxx6,保留了部分信息,并且保证了信息的长度不变性,对信息持有者更易辨别, 如火车票上得身份信息。
  6. 日期偏移取整:20130520 12:30:45 -> 20130520 12:00:00,舍弃精度来保证原始数据的安全性,一般此种方法可以保护数据的时间分布密度。

但不管哪种手段都要基于不同的应用场景,遵循下面两个原则:
1.remain meaningful for application logic(尽可能的为脱敏后的应用,保留脱敏前的有意义信息)
2.sufficiently treated to avoid reverse engineer(最大程度上防止黑客进行破解)
以这次脱敏一个需求为例:
美团一般的业务场景是这样的,用户在网站上付款一笔团购单之后,我们会将团购密码,发到用户对应的手机号上。这个过程中,从用户的角度来看团购密码在未被用户消费之前,对用户来说是要保密的,不能被公开的,其次美团用户的手机号也是要保密的,因为公开之后可能被推送一些垃圾信息,或者更严重的危害。从公司内部数据分析人员来看,他们有时虽然没有权限知道用户团购密码,但是他们想分析公司发送的团购密码数量情况,这是安全允许;再有数据分析人员虽然没有权限知道用户具体的手机号码,但是他们需要统计美团用户手机的地区分布情况,或者运营商分布差异,进而为更上层的决策提供支持。
根据这样的需求,我们可以对团购密码做加密处理保证其唯一性,也保留其原有的数据格式,在保密的同时不影响数据分析的需求。同样,我们将用户的手机号码的前7位,关于运营商和地区位置信息保留,后四位进行模糊化处理。这样同样也达到了保护和不影响统计的需求。

因此从实际出发遵循上面的两个处理原则,第一阶段我们在脱敏工具集中,确定了如下4种基本类型的脱敏方案(对应4个udf):

字段名称 脱敏方法 举例 脱敏原则
电话号码(moblie)掩码13812345678-> 13812340000防止号码泄露,但保留运营商和地区信息 (唯一性,由前端绑定或者注册时约束)
邮件(email)截断+ 加密hxs@163.com -> 6225888e3a1d4a139f5f5db98d846102b2cd0d@163.com保留邮件域信息
团购密码(code)加密4023926843399219 -> 1298078978加密后在一定精度上保持唯一性,并与数据类型一致
设备号(deviceid)加密ffbacff42826302d9e832b7e907a212a -> b9c2a61972a19bf21b06b0ddb8ba642d加密后保持唯一性

确定实施范围与步骤

通过上面字段的梳理和脱敏方案的制定,我们对美团数据仓库中涉及到得敏感字段的表进行脱敏处理。在数据仓库分层理论中,数据脱敏往往发生在上层,最直接的是在对外开放这一层面上。在实际应用中,我们既要参考分层理论,又要从美团现有数据仓库生产环境的体系出发,主要在数据维度层(dim),以及基础服务数据层(fact)上实施脱敏。这样,我们可以在下游相关数据报表以及衍生数据层的开发过程中使用脱敏后的数据,从而避免出现数据安全问题。
确认处理的表和字段后,我们还要确保相关上下游流程的正常运行, 以及未脱敏的敏感信息的正常产出与存储(通过更严格的安全审核来进行访问)。
以用户信息表user为例,脱敏步骤如下:
1.首先生产一份ndm_user未脱敏数据,用于未脱敏数据的正常产出。
2.对下游涉及的所有依赖user生产流程进行修改,来确保脱敏后的正常运行,这里主要是确认数据格式,以及数据源的工作。
3.根据对应的脱敏方法对user表中对应的字段进行脱敏处理。

总结

通过上面的几个步骤的实施,我们完成了第一阶段的数据脱敏工作。在数据脱敏方案设计与实施过程中, 我们觉得更重要的还是从特定的应用场景出发进行整体设计,兼顾了数据仓库建设这一重要考量维度。数据脱敏实施为公司数据安全的推进,提供了有力支持。当然,我们第一阶段脱敏的工具集还相对较少,需要补充。 脱敏的技术架构还有待完善和更加自动化。
本文关于数据安全和数据访问隔离的控制阐述较少,希望通过以后的生产实践,继续为大家介绍。

参考

参考文献如下:

  1. http://en.wikipedia.org/wiki/Data_masking
  2. http://www.prnews.cn/press_release/51034.htm

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

Hadoop安全实践

美团点评阅读(2355)

前言

在2014年初,我们将线上使用的 Hadoop 1.0 集群切换到 Hadoop 2.2.0 稳定版, 与此同时部署了 Hadoop 的安全认证。本文主要介绍在 Hadoop 2.2.0 上部署安全认证的方案调研实施以及相应的解决方法。

背景

集群安全措施相对薄弱

最早部署Hadoop集群时并没有考虑安全问题,随着集群的不断扩大, 各部门对集群的使用需求增加,集群安全问题就显得颇为重要。说到安全问题,一般包括如下方面:

  • 用户认证(Authentication)
    即是对用户身份进行核对, 确认用户即是其声明的身份, 这里包括用户和服务的认证。

  • 用户授权(Authorization)
    即是权限控制,对特定资源, 特定访问用户进行授权或拒绝访问。用户授权是建立再用户认证的基础上, 没有可靠的用户认证谈不上用户授权。

未开启安全认证时,Hadoop 是以客户端提供的用户名作为用户凭证, 一般即是发起任务的Unix 用户。一般线上机器部署服务会采用统一账号,当以统一账号部署集群时,所有执行 Hadoop 任务的用户都是集群的超级管理员,容易发生误操作。即便是以管理员账号部署集群,恶意用户在客户端仍然可以冒充管理员账号执行。

集群整体升级到 hadoop 2.0

2013年10月份 Hadoop 2.2.0 发布,作为 Apache Hadoop 2.X 的 GA 版本。我们考虑将集群整体升级 Hadoop 2.2.0,进入 yarn 时代。与此同时,我们计划在升级过程中一并把集群安全工作做到位,主要基于以下考虑:

  • 与升级工作一样,安全同样是基础工作,把安全搞好会方便我们后续的工作,否则会成为下一个阻碍。
  • 所谓基础工作,就是越往后改动越难的工作,目前不做,将来依赖更多,开展代价更大。

综上,我们的需求是在低版本hadoop升级到Yarn的过程中部署Hadoop安全认证,做好认证之后我们可以在此之上开启适当的权限控制(hdfs, 队列)。

方案调研

在方案调研之前先明确以下安全实践的原则,如下:

  • 做为一个后端服务平台,部署安全的主要目的是防止用户误操作导致的事故(比如误删数据,误操作等)
  • 做安全是为了开放,开放的前提是保证基本的安全,数据安全与平台安全
  • 在保证安全的前提下,尽量简化运维

分析我们遇到的问题,这里我们需要调研:

  • 账号拆分与相应管理方案
  • 开启 Hadoop 安全认证
  • 客户端针对安全认证的相应调整

账号拆分与相应管理方案

集群账号管理

原先我们使用单一账号作为集群管理员,且这一账号为线上统一登录账号, 这存在极大的安全隐患。我们需要使用特殊账号来管理集群。这里涉及的问题是,我们需要几个运维账号呢?
一种简单的做法是使用一个特殊运维账号(比如 hadoop), CDHApache官方 都推荐按服务划分分账号来启动集群:

User:GroupDaemons
hdfs:hadoopNameNode, Secondary NameNode, Checkpoint Node, Backup Node, DataNode
yarn:hadoopResourceManager, NodeManager
mapred:hadoopMapReduce JobHistory Server

考虑到精细化控制可以有效避免误操作,这里我们遵循官方的建议使用多账号。
在从单一运维账号迁移到多个账号部署时,需要考虑相关文件权限问题,包括本地以及hdfs两部分,这可以在安全部署上线时完成相应改动。

用户账号管理

美团很多小组都有使用 Hadoop 来进行大数据处理需求, 故需要一定程度的多租户环境, 这里主要考虑其中的数据和操作的权限问题。hdfs 本身仅提供类 Unix 的权限系统, 默认的组概念也相对鸡肋。鉴于此,在多用户的管理上可以有简单粗暴的方案:

不同组有各自的根目录,使用不同的账号,对组内文件有全部权限。不同组之间相互不能访问数据(除非手动修改)。

在一个集中的数据仓库环境下,又要生产各个部门的统计数据的话,上述策略不够灵活。目前Cloudera 有一个精细化权限控制的解决方案 sentry, 支持 Role based 的权限管理。由于其定制化较高,不方便使用, 故暂未考虑。

开启 Hadoop 安全认证

Hadoop 的安全认证是基于 Kerberos 实现的。 Kerberos 是一个网络身份验证协议,用户只需输入身份验证信息,验证通过获取票据即可访问多个接入 Kerberos 的服务, 机器的单点登录也可以基于此协议完成的。 Hadoop 本身并不创建用户账号,而是使用 Kerberos 协议来进行用户身份验证,从Kerberos凭证中的用户信息获取用户账号, 这样一来跟实际用户运行的账号也无关。

这里我们从 YARN 上的 MR 任务提交过程简单说明一下:
 Yarn 任务提交步骤

  1. 用户执行任务前,先通过KDC认证自己,获取TGT(Ticket Granting Ticket)。KDC是 Kerberos 认证的中心服务,存储用户和服务的认证信息。
  2. 用户通过 TGT 向 KDC 请求访问服务的Ticket, KDC 生成 session key 后一并发给客户端。
  3. 客户端通过 service ticket 向服务认证自己,完成身份认证。
  4. 完成身份认证后客户端向服务请求若干token供后续任务执行认证使用(比如 HDFS NameNode Delegation Token, YARN ResourceManager Delegation Token)
  5. 客户端连同获取到的 token 一并提交任务,后续任务执行使用 token 进行来自服务的认证

从上可以看出,出于性能的考虑,Hadoop 安全认证体系中仅在用户跟服务通信以及各个服务之间通信适用 Kerberos 认证,在用户认证后任务执行,访问服务,读取/写入数据等均采用特定服务(NameNode, Resource Manager)发起访问token,让需求方凭借 token 访问相应服务和数据。这里 token 的传递,认证以及更新不做深入讨论。

关于开启 Hadoop 安全认证, Cloudera 有详细的文档介绍。由于自身环境以及部署运维的考虑,最终的部署方案有些许出入, 一一说明。

Kerberos 部署

Hadoop 安全认证需要一个 Kerberos 集群, 部署 Kerberos 需要部署KDC。 由于我们的环境中使用 freeIPA 进行主机认证相关的权限控制,已经集成 Kerberos 服务, 故不需要另外部署。
Kerberos 相关的运维操作, 比如添加用户,服务,导出keytab,均可以通过 ipa 相关接口来进行。

Container 的选择

从上图可以看出用户发起的任务是在特定的容器(Container)内执行的, 一开始我们考虑使用DefaultContainer 而不是官方推荐的 LinuxContainer, 缺点是对任务之间的物理隔离以及防范恶意任务方面会有缺陷, 不过方便部署,使用LinuxContainer需要在集群各台机器上部署用户账号。
实际测试发现由于MAPREDUCE-5208的引入,在 hadoop 2.2.0 上开启安全认证后无法使用 DefaultContainer
这里不希望对代码有过多定制化的修改,我们考虑还是使用 LinuxContainer, 需要解决一下问题:

  • 用户账号创建
    我们需要在集群内添加所有可能的任务发起用户账号。借助 freeipa 的统一的用户管理 , 我们只需要在 freeipa 上添加相应用户即可。
  • container-executor 和 container-executor.cfg 的部署
    container-executor 作为Yarn 的 container 执行程序,有一系列的权限要求:

    Be owned by root
    Be owned by a group that contains only the user running the YARN daemons
    Be setuid
    Be group readable and executable

    配置 container-executor.cfg 不仅需要是owned by root,且其所在目录同样需要 owned by root。这两者都给自动化部署带来不便,鉴于此部分比较独立且基本不会改变,我们可以将其加入集群机器的 puppet 管理当中。

DataNode 启动方式

CDH 推荐的datanode 的启动方式需要使用低端口并且使用jsvc发布, 在运维方面也不太方便。这里我们通过配置ignore.secure.ports.for.testing=true来启动datanode, 规避这些约束。

客户端针对安全认证的相应调整

集群开启安全认证之后, 依赖集群的客户端(脚本, 服务)都需要做相应修改,不过改动基本无异。大部分服务都已包括对 Kerberos 认证的相应处理, 基本不需要修改。

这里首先得说明一下开启安全认证后的认证方式:

  • 使用密码认证
    使用用户密码通过kinit认证, 获取到的TGT存在本地凭证缓存当中, 供后续访问服务认证使用。一般在交互式访问中使用。
  • 使用 keytab 认证
    用户通过导出的keytab 可以免密码进行用户认证, 后续步骤一致。一般在应用程序中配置使用。

Kerberos 凭证(ticket) 有两个属性, ticket_lifetimerenew_lifetime。其中 ticket_lifetime 表明凭证生效的时限,一般为24小时。在凭证失效前部分凭证可以延期失效时间(即Renewable), renew_lifetime 表明凭证最长可以被延期的时限,一般为一个礼拜。当凭证过期之后,对安全认证的服务的后续访问则会失败。这里第一个问题就是如何处理凭证过期。

凭证过期处理策略

在最早的 Security features for Hadoop 设计中提出这样的假设:

A Hadoop job will run no longer than 7 days (configurable) on a MapReduce cluster or accessing HDFS from the job will fail.

对于一般的任务, 24小时甚至延迟到一周的凭证时限是足够充分的。所以大部分时间我们只需要在执行操作之前使用 kinit 认证一遍,再起后台任务进行周期性凭证更新即可。

while true ; do kinit -R; sleep $((3600 * 6)) ; done &

不过对于需要常驻的访问Hadoop集群的服务来说,上述假设就不成立了。这时候我们可以

  1. 扩大 ticket_lifetimerenew_lifetime 时限
    扩大凭证存活时限可以解决此问题,但由于Kerberos跟我们线上用户登陆认证绑定,会带来安全隐患,故不方便修改。

  2. 定期重新进行kinit 认证更新凭证
    不仅仅是定期延长认证时间,可以直接定期重新认证以延长凭证有限期限。一般我们需要导出 keytab 来进行定期认证的操作。

Hadoop 将 Kerberos 认证部分进行了一定的封装,实际上并不需要那么复杂, 这里重点可以看看 UserGroupInformation 这个类。

UserGroupInformation

UserGroupInformation 这个类在 JAAS 框架上封装了 Hadoop 的用户信息, 更确切地说是对 Subject 做了一层封装。

  UserGroupInformation(Subject subject) {
    this.subject = subject;
    this.user = subject.getPrincipals(User.class).iterator().next();
    this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty();
    this.isKrbTkt = !subject.getPrivateCredentials(KerberosTicket.class).isEmpty();
  }

JAAS 是 Java 认证和授权服务(Java Authentication and Authorization Service)的缩写, 主要包含以下几个实体:

  • Subject
    Subject 是一个不可继承的实体类,它标志一个请求的来源, 包含相关的凭证标识(Principal) 和 公开和私有的凭据(Credential)。
  • Principal
    凭证标识,认证成功后,一个 Subject 可以被关联多个Principal。
  • Credential
    凭据,有公有凭据以及私有凭据。

JAAS的认证过程如下:

  1. An application instantiates a LoginContext.
  2. The LoginContext consults a Configuration to load all of the LoginModules configured for that application.
  3. The application invokes the LoginContext’s login method.
  4. The login method invokes all of the loaded LoginModules. Each LoginModule attempts to authenticate the subject. Upon success, LoginModules associate relevant Principals and credentials with a Subject object that represents the subject being authenticated.
  5. The LoginContext returns the authentication status to the application.
  6. If authentication succeeded, the application retrieves the Subject from the LoginContext.

需要认证的代码片段可以包装在 doPrivileged 当中, 可以直接使用 Subject.doAs 方法,支持嵌套。

在安全模式下,UGI 支持不同LoginContext 配置, 均是通过 HadoopConfiguration 类动态产生:

  • hadoop-user-kerberos
    使用kerberos缓存凭证登陆的配置, useTicketCache 置为 true.
  • hadoop-keytab-kerberos
    使用keytab登陆的配置, useKeyTab 置为 true.

UGI 当中有多处认证, getLoginUser 方法使用 hadoop-user-kerberos 配置认证:

  1. 通过配置生成 LoginContext
  2. 调用 LoginContext.login 方法完成登陆, 通过 ticket cache 中凭证完成登陆
  3. 判断是否需要其他用户身份(proxy user)执行
  4. HADOOP_TOKEN_FILE_LOCATION 中的 token 加入 Credentials 集合当中
  5. 另起一个线程做周期性的凭证更新 spawnAutoRenewalThreadForUserCreds

步骤5可以看出当我们存在凭证后并不需要主动做周期性地凭证更新。

而 loginUserFromKeytab 方法使用 hadoop-kerberos 配置认证:

  1. 通过配置生成 LoginContext
  2. 调用 LoginContext.login 方法完成登陆, 使用keytab完成登陆

loginUserFromKeytab 没有对凭证做周期的更新, 那怎么保证凭证不会过期呢?

  1. 在访问集群执行相关操作前, 可以调用 checkTGTAndReloginFromKeytab 来尝试更新凭证(实际上是重新登陆了)
  2. 在凭证过期时,创建 IPC 失败会触发调用 reloginFromKeytab 来重新登陆

Client.java

    private synchronized void handleSaslConnectionFailure(
        final int currRetries, final int maxRetries, final Exception ex,
        final Random rand, final UserGroupInformation ugi) throws IOException,
        InterruptedException {
      ugi.doAs(new PrivilegedExceptionAction<Object>() {
        @Override
        public Object run() throws IOException, InterruptedException {
          final short MAX_BACKOFF = 5000;
          closeConnection();
          disposeSasl();
          if (shouldAuthenticateOverKrb()) {
            if (currRetries < maxRetries) {
              if(LOG.isDebugEnabled()) {
                LOG.debug("Exception encountered while connecting to "
                    + "the server : " + ex);
              }
              // try re-login
              if (UserGroupInformation.isLoginKeytabBased()) {
                UserGroupInformation.getLoginUser().reloginFromKeytab();
              } else {
                UserGroupInformation.getLoginUser().reloginFromTicketCache();
              }

可见如果是使用 keytab 认证的话,认证是长期有效的。

从上述代码中可以看到,不论是否是keytab认证,创建IPC失败均会尝试重新登陆。

基于keytab 的Kerberos认证方式

为了让用户免于记忆密码,我们可以考虑导出并交付keytab给相关用户(前提是用户数量可控, 比如是以虚拟用户为单位)。
这样,用户的Hadoop任务认证方式可以有:

  • 直接使用 keytab kinit 之后访问
  • 或者调用 loginUserFromKeytab 完成登录,然后将代码片段包裹在 UGI 的 doAs 方法当中执行

上线部署

确定了部署方案之后, 我们在升级 hadoop 版本的同时完成了安全认证的部署。在部署和使用中我们遇到若干问题,这里一一说明。

JCE 部署

开启安全认证时发现 Kerberos 认证不通过:

Client failed to SASL authenticate: javax.security.sasl.SaslException: GSS initiate failed [Caused by GSSException: Failure unspecified at GSS-API level (Mechanism level: Checksum failed)]

由于我们部署的Kerberos默认使用 AES-256 加密, 需要在Hadoop环境(集群以及客户端)上安装 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy File, 否则Kerberos认证不通过。可以通过此 gist 验证改动是否生效。此步骤可以添加到puppet当中。

SNN getimage 返回 NPE

开启安全认证发现 SNN 持续由于 getimage 报错NPE 退出, 相关错误如下。

2013-12-29 23:56:19572 DEBUG org.apache.hadoop.security.authentication.server.AuthenticationFilter: Request [http://XXX.com:50070/getimage?getimage=1&txid=8627&storageInfo=-47:200271
8265:0:CID-3dce02cb-a1c2-4ab8-8b12-f23bbefd7bcc] triggering authentication
2013-12-29 23:56:19580 WARN org.apache.hadoop.security.authentication.server.AuthenticationFilter: Authentication exception: GSSException: Failure unspecified at GSS-API level (Mechanism level: Specified
 version of key is not available (44))
org.apache.hadoop.security.authentication.client.AuthenticationException: GSSException: Failure unspecified at GSS-API level (Mechanism level: Specified version of key is not available (44))
        at org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler.authenticate(KerberosAuthenticationHandler.java:360)
        at org.apache.hadoop.security.authentication.server.AuthenticationFilter.doFilter(AuthenticationFilter.java:349)

根据报错信息 Specified version of key is not available 发现是由于同一个 HTTP 凭证被导出多遍导致之前的keytab中的凭证失效了,重新生成部署所需的 keytab 即可。
这里的提醒就是不要重复导出相同的凭证, 以防止已经分发使用的keytab中的凭证失效。

Balancer 执行过长导致认证过期

在部署安全认证之后, 我们对hdfs数据进行 balance 就需要预先认证一下再执行, 这样就会遇到我们之前说的认证期限的问题。
这里有两种方式可以解决此问题:

  • 添加外部定时任务重新认证, 刷新凭证缓存, 延迟凭证有效期限。
  • 可以写一个小代码对 balance 的入口 org.apache.hadoop.hdfs.server.balancer.Balancer 进行一点封装,将其封装在一个 doAs 当中, 类似 hue 中的 SudoFsShell 一样的思路

sssd 服务认证异常

sssd 是指我们用于线上登陆认证的一个底层服务,在过去一段时间内经常出现问题退出,导致用户登录动作hang住,进而导致相关任务执行失败。部署Hadoop安全认证之后相关 kerberos 认证也走这个服务,增大了服务异常退出的概率。目前看起来sssd服务问题是由于系统版本过低sssd服务代码有bug导致,解决方案最方便的是升级系统或切换服务到新的机器。

“KDC can’t fulfill requested option while renewing credentials”

应用执行日志偶尔会报如下错误:

2014-03-12 21:30:03593 WARN  security.UserGroupInformation (UserGroupInformation.java:run(794)) - Exception encountered while running the renewal command. Aborting renew thread. org.apache.hadoop.util.Shell$ExitCodeException: kinit(v5): KDC can't fulfill requested option while renewing credentials

表示 UGI的凭证更新线程失败退出了。目前HADOOP-10041 记录了此问题,主要原因是由于凭证无法更新导致, 一般不需要特殊处理。

参考资料

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

基于Flume的美团日志收集系统(一)架构和设计

美团点评阅读(2362)

美团的日志收集系统负责美团的所有业务日志的收集,并分别给Hadoop平台提供离线数据和Storm平台提供实时数据流。美团的日志收集系统基于Flume设计和搭建而成。

《基于Flume的美团日志收集系统》将分两部分给读者呈现美团日志收集系统的架构设计和实战经验。

第一部分架构和设计,将主要着眼于日志收集系统整体的架构设计,以及为什么要做这样的设计。

第二部分改进和优化,将主要着眼于实际部署和使用过程中遇到的问题,对Flume做的功能修改和优化等。

1 日志收集系统简介

日志收集是大数据的基石。

许多公司的业务平台每天都会产生大量的日志数据。收集业务日志数据,供离线和在线的分析系统使用,正是日志收集系统的要做的事情。高可用性,高可靠性和可扩展性是日志收集系统所具有的基本特征。

目前常用的开源日志收集系统有Flume, Scribe等。Flume是Cloudera提供的一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系统,目前已经是Apache的一个子项目。Scribe是Facebook开源的日志收集系统,它为日志的分布式收集,统一处理提供一个可扩展的,高容错的简单方案。

2 常用的开源日志收集系统对比

下面将对常见的开源日志收集系统Flume和Scribe的各方面进行对比。对比中Flume将主要采用Apache下的Flume-NG为参考对象。同时,我们将常用的日志收集系统分为三层(Agent层,Collector层和Store层)来进行对比。

对比项Flume-NGScribe
使用语言Javac/c++
容错性Agent和Collector间,Collector和Store间都有容错性,且提供三种级别的可靠性保证;Agent和Collector间, Collector和Store之间有容错性;
负载均衡Agent和Collector间,Collector和Store间有LoadBalance和Failover两种模式
可扩展性
Agent丰富程度提供丰富的Agent,包括avro/thrift socket, text, tail等主要是thrift端口
Store丰富程度可以直接写hdfs, text, console, tcp;写hdfs时支持对text和sequence的压缩;提供buffer, network, file(hdfs, text)等
代码结构系统框架好,模块分明,易于开发代码简单

3 美团日志收集系统架构

美团的日志收集系统负责美团的所有业务日志的收集,并分别给Hadoop平台提供离线数据和Storm平台提供实时数据流。美团的日志收集系统基于Flume设计和搭建而成。目前每天收集和处理约T级别的日志数据。

下图是美团的日志收集系统的整体框架图。

 美团日志收集系统架构

a. 整个系统分为三层:Agent层,Collector层和Store层。其中Agent层每个机器部署一个进程,负责对单机的日志收集工作;Collector层部署在中心服务器上,负责接收Agent层发送的日志,并且将日志根据路由规则写到相应的Store层中;Store层负责提供永久或者临时的日志存储服务,或者将日志流导向其它服务器。

b. Agent到Collector使用LoadBalance策略,将所有的日志均衡地发到所有的Collector上,达到负载均衡的目标,同时并处理单个Collector失效的问题。

c. Collector层的目标主要有三个:SinkHdfs, SinkKafka和SinkBypass。分别提供离线的数据到Hdfs,和提供实时的日志流到Kafka和Bypass。其中SinkHdfs又根据日志量的大小分为SinkHdfs_b,SinkHdfs_m和SinkHdfs_s三个Sink,以提高写入到Hdfs的性能,具体见后面介绍。

d. 对于Store来说,Hdfs负责永久地存储所有日志;Kafka存储最新的7天日志,并给Storm系统提供实时日志流;Bypass负责给其它服务器和应用提供实时日志流。

下图是美团的日志收集系统的模块分解图,详解Agent, Collector和Bypass中的Source, Channel和Sink的关系。

 美团日志收集系统架构

a. 模块命名规则:所有的Source以src开头,所有的Channel以ch开头,所有的Sink以sink开头;

b. Channel统一使用美团开发的DualChannel,具体原因后面详述;对于过滤掉的日志使用NullChannel,具体原因后面详述;

c. 模块之间内部通信统一使用Avro接口;

4 架构设计考虑

下面将从可用性,可靠性,可扩展性和兼容性等方面,对上述的架构做细致的解析。

4.1 可用性(availablity)

对日志收集系统来说,可用性(availablity)指固定周期内系统无故障运行总时间。要想提高系统的可用性,就需要消除系统的单点,提高系统的冗余度。下面来看看美团的日志收集系统在可用性方面的考虑。

4.1.1 Agent死掉

Agent死掉分为两种情况:机器死机或者Agent进程死掉。

对于机器死机的情况来说,由于产生日志的进程也同样会死掉,所以不会再产生新的日志,不存在不提供服务的情况。

对于Agent进程死掉的情况来说,确实会降低系统的可用性。对此,我们有下面三种方式来提高系统的可用性。首先,所有的Agent在supervise的方式下启动,如果进程死掉会被系统立即重启,以提供服务。其次,对所有的Agent进行存活监控,发现Agent死掉立即报警。最后,对于非常重要的日志,建议应用直接将日志写磁盘,Agent使用spooldir的方式获得最新的日志。

4.1.2 Collector死掉

由于中心服务器提供的是对等的且无差别的服务,且Agent访问Collector做了LoadBalance和重试机制。所以当某个Collector无法提供服务时,Agent的重试策略会将数据发送到其它可用的Collector上面。所以整个服务不受影响。

4.1.3 Hdfs正常停机

我们在Collector的HdfsSink中提供了开关选项,可以控制Collector停止写Hdfs,并且将所有的events缓存到FileChannel的功能。

4.1.4 Hdfs异常停机或不可访问

假如Hdfs异常停机或不可访问,此时Collector无法写Hdfs。由于我们使用DualChannel,Collector可以将所收到的events缓存到FileChannel,保存在磁盘上,继续提供服务。当Hdfs恢复服务以后,再将FileChannel中缓存的events再发送到Hdfs上。这种机制类似于Scribe,可以提供较好的容错性。

4.1.5 Collector变慢或者Agent/Collector网络变慢

如果Collector处理速度变慢(比如机器load过高)或者Agent/Collector之间的网络变慢,可能导致Agent发送到Collector的速度变慢。同样的,对于此种情况,我们在Agent端使用DualChannel,Agent可以将收到的events缓存到FileChannel,保存在磁盘上,继续提供服务。当Collector恢复服务以后,再将FileChannel中缓存的events再发送给Collector。

4.1.6 Hdfs变慢

当Hadoop上的任务较多且有大量的读写操作时,Hdfs的读写数据往往变的很慢。由于每天,每周都有高峰使用期,所以这种情况非常普遍。

对于Hdfs变慢的问题,我们同样使用DualChannel来解决。当Hdfs写入较快时,所有的events只经过MemChannel传递数据,减少磁盘IO,获得较高性能。当Hdfs写入较慢时,所有的events只经过FileChannel传递数据,有一个较大的数据缓存空间。

4.2 可靠性(reliability)

对日志收集系统来说,可靠性(reliability)是指Flume在数据流的传输过程中,保证events的可靠传递。

对Flume来说,所有的events都被保存在Agent的Channel中,然后被发送到数据流中的下一个Agent或者最终的存储服务中。那么一个Agent的Channel中的events什么时候被删除呢?当且仅当它们被保存到下一个Agent的Channel中或者被保存到最终的存储服务中。这就是Flume提供数据流中点到点的可靠性保证的最基本的单跳消息传递语义。

那么Flume是如何做到上述最基本的消息传递语义呢?

首先,Agent间的事务交换。Flume使用事务的办法来保证event的可靠传递。Source和Sink分别被封装在事务中,这些事务由保存event的存储提供或者由Channel提供。这就保证了event在数据流的点对点传输中是可靠的。在多级数据流中,如下图,上一级的Sink和下一级的Source都被包含在事务中,保证数据可靠地从一个Channel到另一个Channel转移。

 美团日志收集系统架构

其次,数据流中 Channel的持久性。Flume中MemoryChannel是可能丢失数据的(当Agent死掉时),而FileChannel是持久性的,提供类似mysql的日志机制,保证数据不丢失。

4.3 可扩展性(scalability)

对日志收集系统来说,可扩展性(scalability)是指系统能够线性扩展。当日志量增大时,系统能够以简单的增加机器来达到线性扩容的目的。

对于基于Flume的日志收集系统来说,需要在设计的每一层,都可以做到线性扩展地提供服务。下面将对每一层的可扩展性做相应的说明。

4.3.1 Agent层

对于Agent这一层来说,每个机器部署一个Agent,可以水平扩展,不受限制。一个方面,Agent收集日志的能力受限于机器的性能,正常情况下一个Agent可以为单机提供足够服务。另一方面,如果机器比较多,可能受限于后端Collector提供的服务,但Agent到Collector是有Load Balance机制,使得Collector可以线性扩展提高能力。

4.3.2 Collector层

对于Collector这一层,Agent到Collector是有Load Balance机制,并且Collector提供无差别服务,所以可以线性扩展。其性能主要受限于Store层提供的能力。

4.3.3 Store层

对于Store这一层来说,Hdfs和Kafka都是分布式系统,可以做到线性扩展。Bypass属于临时的应用,只对应于某一类日志,性能不是瓶颈。

4.4 Channel的选择

Flume1.4.0中,其官方提供常用的MemoryChannel和FileChannel供大家选择。其优劣如下:

  • MemoryChannel: 所有的events被保存在内存中。优点是高吞吐。缺点是容量有限并且Agent死掉时会丢失内存中的数据。

  • FileChannel: 所有的events被保存在文件中。优点是容量较大且死掉时数据可恢复。缺点是速度较慢。

上述两种Channel,优缺点相反,分别有自己适合的场景。然而,对于大部分应用来说,我们希望Channel可以同提供高吞吐和大缓存。基于此,我们开发了DualChannel。

  • DualChannel:基于 MemoryChannel和 FileChannel开发。当堆积在Channel中的events数小于阈值时,所有的events被保存在MemoryChannel中,Sink从MemoryChannel中读取数据; 当堆积在Channel中的events数大于阈值时, 所有的events被自动存放在FileChannel中,Sink从FileChannel中读取数据。这样当系统正常运行时,我们可以使用MemoryChannel的高吞吐特性;当系统有异常时,我们可以利用FileChannel的大缓存的特性。

4.5 和scribe兼容

在设计之初,我们就要求每类日志都有一个category相对应,并且Flume的Agent提供AvroSource和ScribeSource两种服务。这将保持和之前的Scribe相对应,减少业务的更改成本。

4.6 权限控制

在目前的日志收集系统中,我们只使用最简单的权限控制。只有设定的category才可以进入到存储系统。所以目前的权限控制就是category过滤。

如果权限控制放在Agent端,优势是可以较好地控制垃圾数据在系统中流转。但劣势是配置修改麻烦,每增加一个日志就需要重启或者重载Agent的配置。

如果权限控制放在Collector端,优势是方便进行配置的修改和加载。劣势是部分没有注册的数据可能在Agent/Collector之间传输。

考虑到Agent/Collector之间的日志传输并非系统瓶颈,且目前日志收集属内部系统,安全问题属于次要问题,所以选择采用Collector端控制。

4.7 提供实时流

美团的部分业务,如实时推荐,反爬虫服务等服务,需要处理实时的数据流。因此我们希望Flume能够导出一份实时流给Kafka/Storm系统。

一个非常重要的要求是实时数据流不应该受到其它Sink的速度影响,保证实时数据流的速度。这一点,我们是通过Collector中设置不同的Channel进行隔离,并且DualChannel的大容量保证了日志的处理不受Sink的影响。

5 系统监控

对于一个大型复杂系统来说,监控是必不可少的部分。设计合理的监控,可以对异常情况及时发现,只要有一部手机,就可以知道系统是否正常运作。对于美团的日志收集系统,我们建立了多维度的监控,防止未知的异常发生。

5.1 发送速度,拥堵情况,写Hdfs速度

通过发送给zabbix的数据,我们可以绘制出发送数量、拥堵情况和写Hdfs速度的图表,对于超预期的拥堵,我们会报警出来查找原因。

下面是Flume Collector HdfsSink写数据到Hdfs的速度截图:

 美团日志收集系统架构

下面是Flume Collector的FileChannel中拥堵的events数据量截图:

 美团日志收集系统架构

5.2 flume写hfds状态的监控

Flume写入Hdfs会先生成tmp文件,对于特别重要的日志,我们会每15分钟左右检查一下各个Collector是否都产生了tmp文件,对于没有正常产生tmp文件的Collector和日志我们需要检查是否有异常。这样可以及时发现Flume和日志的异常.

5.3 日志大小异常监控

对于重要的日志,我们会每个小时都监控日志大小周同比是否有较大波动,并给予提醒,这个报警有效的发现了异常的日志,且多次发现了应用方日志发送的异常,及时给予了对方反馈,帮助他们及早修复自身系统的异常。

通过上述的讲解,我们可以看到,基于Flume的美团日志收集系统已经是具备高可用性,高可靠性,可扩展等特性的分布式服务。

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

基于Flume的美团日志收集系统(二)改进和优化

美团点评阅读(2334)

在《基于Flume的美团日志收集系统(一)架构和设计》中,我们详述了基于Flume的美团日志收集系统的架构设计,以及为什么做这样的设计。在本节中,我们将会讲述在实际部署和使用过程中遇到的问题,对Flume的功能改进和对系统做的优化。

1 Flume的问题总结

在Flume的使用过程中,遇到的主要问题如下:

a. Channel“水土不服”:使用固定大小的MemoryChannel在日志高峰时常报队列大小不够的异常;使用FileChannel又导致IO繁忙的问题;

b. HdfsSink的性能问题:使用HdfsSink向Hdfs写日志,在高峰时间速度较慢;

c. 系统的管理问题:配置升级,模块重启等;

2 Flume的功能改进和优化点

从上面的问题中可以看到,有一些需求是原生Flume无法满足的,因此,基于开源的Flume我们增加了许多功能,修改了一些Bug,并且进行一些调优。下面将对一些主要的方面做一些说明。

2.1 增加Zabbix monitor服务

一方面,Flume本身提供了http, ganglia的监控服务,而我们目前主要使用zabbix做监控。因此,我们为Flume添加了zabbix监控模块,和sa的监控服务无缝融合。

另一方面,净化Flume的metrics。只将我们需要的metrics发送给zabbix,避免 zabbix server造成压力。目前我们最为关心的是Flume能否及时把应用端发送过来的日志写到Hdfs上, 对应关注的metrics为:

  • Source : 接收的event数和处理的event数
  • Channel : Channel中拥堵的event数
  • Sink : 已经处理的event数

2.2 为HdfsSink增加自动创建index功能

首先,我们的HdfsSink写到hadoop的文件采用lzo压缩存储。 HdfsSink可以读取hadoop配置文件中提供的编码类列表,然后通过配置的方式获取使用何种压缩编码,我们目前使用lzo压缩数据。采用lzo压缩而非bz2压缩,是基于以下测试数据:

event大小(Byte)sink.batch-sizehdfs.batchSize压缩格式总数据大小(G)耗时(s)平均events/s压缩后大小(G)
54430010000bz29.1244868331.36
54430010000lzo9.1612273333.49

其次,我们的HdfsSink增加了创建lzo文件后自动创建index功能。Hadoop提供了对lzo创建索引,使得压缩文件是可切分的,这样Hadoop Job可以并行处理数据文件。HdfsSink本身lzo压缩,但写完lzo文件并不会建索引,我们在close文件之后添加了建索引功能。

  /**
   * Rename bucketPath file from .tmp to permanent location.
   */
  private void renameBucket() throws IOException, InterruptedException {
      if(bucketPath.equals(targetPath)) {
              return;
        }

        final Path srcPath = new Path(bucketPath);
        final Path dstPath = new Path(targetPath);

        callWithTimeout(new CallRunner<Object>() {
              @Override
              public Object call() throws Exception {
                if(fileSystem.exists(srcPath)) { // could block
                      LOG.info("Renaming " + srcPath + " to " + dstPath);
                     fileSystem.rename(srcPath, dstPath); // could block

                      //index the dstPath lzo file
                      if (codeC != null && ".lzo".equals(codeC.getDefaultExtension()) ) {
                              LzoIndexer lzoIndexer = new LzoIndexer(new Configuration());
                              lzoIndexer.index(dstPath);
                      }
                }
                return null;
              }
    });
}

2.3 增加HdfsSink的开关

我们在HdfsSink和DualChannel中增加开关,当开关打开的情况下,HdfsSink不再往Hdfs上写数据,并且数据只写向DualChannel中的FileChannel。以此策略来防止Hdfs的正常停机维护。

2.4 增加DualChannel

Flume本身提供了MemoryChannel和FileChannel。MemoryChannel处理速度快,但缓存大小有限,且没有持久化;FileChannel则刚好相反。我们希望利用两者的优势,在Sink处理速度够快,Channel没有缓存过多日志的时候,就使用MemoryChannel,当Sink处理速度跟不上,又需要Channel能够缓存下应用端发送过来的日志时,就使用FileChannel,由此我们开发了DualChannel,能够智能的在两个Channel之间切换。

其具体的逻辑如下:

/***
 * putToMemChannel indicate put event to memChannel or fileChannel
 * takeFromMemChannel indicate take event from memChannel or fileChannel
 * */
private AtomicBoolean putToMemChannel = new AtomicBoolean(true);
private AtomicBoolean takeFromMemChannel = new AtomicBoolean(true);

void doPut(Event event) {
        if (switchon && putToMemChannel.get()) {
              //往memChannel中写数据
              memTransaction.put(event);

              if ( memChannel.isFull() || fileChannel.getQueueSize() > 100) {
                putToMemChannel.set(false);
              }
        } else {
              //往fileChannel中写数据
              fileTransaction.put(event);
        }
  }

Event doTake() {
    Event event = null;
    if ( takeFromMemChannel.get() ) {
        //从memChannel中取数据
        event = memTransaction.take();
        if (event == null) {
            takeFromMemChannel.set(false);
        } 
    } else {
        //从fileChannel中取数据
        event = fileTransaction.take();
        if (event == null) {
            takeFromMemChannel.set(true);

            putToMemChannel.set(true);
        } 
    }
    return event;
}

2.5 增加NullChannel

Flume提供了NullSink,可以把不需要的日志通过NullSink直接丢弃,不进行存储。然而,Source需要先将events存放到Channel中,NullSink再将events取出扔掉。为了提升性能,我们把这一步移到了Channel里面做,所以开发了NullChannel。

2.6 增加KafkaSink

为支持向Storm提供实时数据流,我们增加了KafkaSink用来向Kafka写实时数据流。其基本的逻辑如下:

public class KafkaSink extends AbstractSink implements Configurable {
        private String zkConnect;
        private Integer zkTimeout;
        private Integer batchSize;
        private Integer queueSize;
        private String serializerClass;
        private String producerType;
        private String topicPrefix;

        private Producer<String, String> producer;

        public void configure(Context context) {
            //读取配置,并检查配置
        }

        @Override
        public synchronized void start() {
            //初始化producer
        }

        @Override
        public synchronized void stop() {
            //关闭producer
        }

        @Override
        public Status process() throws EventDeliveryException {

            Status status = Status.READY;

            Channel channel = getChannel();
            Transaction tx = channel.getTransaction();
            try {
                    tx.begin();

                    //将日志按category分队列存放
                    Map<String, List<String>> topic2EventList = new HashMap<String, List<String>>();

                    //从channel中取batchSize大小的日志,从header中获取category,生成topic,并存放于上述的Map中;

                    //将Map中的数据通过producer发送给kafka 

                   tx.commit();
            } catch (Exception e) {
                    tx.rollback();
                    throw new EventDeliveryException(e);
            } finally {
                tx.close();
            }
            return status;
        }
}

2.7 修复和scribe的兼容问题

Scribed在通过ScribeSource发送数据包给Flume时,大于4096字节的包,会先发送一个Dummy包检查服务器的反应,而Flume的ScribeSource对于logentry.size()=0的包返回TRY_LATER,此时Scribed就认为出错,断开连接。这样循环反复尝试,无法真正发送数据。现在在ScribeSource的Thrift接口中,对size为0的情况返回OK,保证后续正常发送数据。

3. Flume系统调优经验总结

3.1 基础参数调优经验

  • HdfsSink中默认的serializer会每写一行在行尾添加一个换行符,我们日志本身带有换行符,这样会导致每条日志后面多一个空行,修改配置不要自动添加换行符;
lc.sinks.sink_hdfs.serializer.appendNewline = false
  • 调大MemoryChannel的capacity,尽量利用MemoryChannel快速的处理能力;

  • 调大HdfsSink的batchSize,增加吞吐量,减少hdfs的flush次数;

  • 适当调大HdfsSink的callTimeout,避免不必要的超时错误;

3.2 HdfsSink获取Filename的优化

HdfsSink的path参数指明了日志被写到Hdfs的位置,该参数中可以引用格式化的参数,将日志写到一个动态的目录中。这方便了日志的管理。例如我们可以将日志写到category分类的目录,并且按天和按小时存放:

lc.sinks.sink_hdfs.hdfs.path = /user/hive/work/orglog.db/%{category}/dt=%Y%m%d/hour=%H

HdfsS ink中处理每条event时,都要根据配置获取此event应该写入的Hdfs path和filename,默认的获取方法是通过正则表达式替换配置中的变量,获取真实的path和filename。因为此过程是每条event都要做的操作,耗时很长。通过我们的测试,20万条日志,这个操作要耗时6-8s左右。

由于我们目前的path和filename有固定的模式,可以通过字符串拼接获得。而后者比正则匹配快几十倍。拼接定符串的方式,20万条日志的操作只需要几百毫秒。

3.3 HdfsSink的b/m/s优化

在我们初始的设计中,所有的日志都通过一个Channel和一个HdfsSink写到Hdfs上。我们来看一看这样做有什么问题。

首先,我们来看一下HdfsSink在发送数据的逻辑:

//从Channel中取batchSize大小的events
for (txnEventCount = 0; txnEventCount < batchSize; txnEventCount++) {
    //对每条日志根据category append到相应的bucketWriter上;
    bucketWriter.append(event);
}

for (BucketWriter bucketWriter : writers) {
    //然后对每一个bucketWriter调用相应的flush方法将数据flush到Hdfs上
    bucketWriter.flush();
}

假设我们的系统中有100个category,batchSize大小设置为20万。则每20万条数据,就需要对100个文件进行append或者flush操作。

其次,对于我们的日志来说,基本符合80/20原则。即20%的category产生了系统80%的日志量。这样对大部分日志来说,每20万条可能只包含几条日志,也需要往Hdfs上flush一次。

上述的情况会导致HdfsSink写Hdfs的效率极差。下图是单Channel的情况下每小时的发送量和写hdfs的时间趋势图。

 美团日志收集系统架构

鉴于这种实际应用场景,我们把日志进行了大小归类,分为big, middle和small三类,这样可以有效的避免小日志跟着大日志一起频繁的flush,提升效果明显。下图是分队列后big队列的每小时的发送量和写hdfs的时间趋势图。

 美团日志收集系统架构

4 未来发展

目前,Flume日志收集系统提供了一个高可用,高可靠,可扩展的分布式服务,已经有效地支持了美团的日志数据收集工作。

后续,我们将在如下方面继续研究:

  • 日志管理系统:图形化的展示和控制日志收集系统;

  • 跟进社区发展:跟进Flume 1.5的进展,同时回馈社区;

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

美团外卖客户端高可用建设体系

美团点评阅读(2647)

背景

美团外卖从2013年11月开始起步,经过数年的高速发展,一直在不断地刷新着记录。2018年5月19日,日订单量峰值突破2000万单,已经成为全球规模最大的外卖平台。业务的快速发展对系统稳定性提出了更高的要求,如何为线上用户提供高稳定的服务体验,保障全链路业务和系统高可用运行,不仅需要后端服务支持,更需要在端上提供全面的技术保障。而相对服务端而言,客户端运行环境千差万别,不可控因素多,面对突发问题应急能力差。因此,构建客户端的高可用建设体系,保障服务稳定高可用,不仅是对工程师的技术挑战,也是外卖平台的核心竞争力之一。

高可用建设体系的思路

一个设计良好的大型客户端系统往往是由一系列各自独立的小组共同开发完成的,每一个小组都应当具有明确定义的的职责划分。各业务模块之间推行“松耦合”开发模式,让业务模块拥有隔离式变更的能力,是一种可以同时提升开发灵活性和系统健壮性的有效手段。这是美团外卖整体的业务架构,整体上以商品交易链路(门店召回,商品展示,交易)为核心方向进行建设,局部上依据业务特点和团队分工分成多个可独立运维单元单独维护。可独立运维单元的简单性是可靠性的前提条件,这使得我们能够持续关注功能迭代,不断完成相关的工程开发任务。

图片1

我们将问题依照生命周期划分为三个阶段:发现、定位、解决,围绕这三个阶段的持续建设,构成了美团外卖高可用建设体系的核心。

美团外卖质量保障体系全景图

这是美团外卖客户端整体质量体系全景图。整体思路:监控报警,日志体系,容灾。

图片2

通过采集业务稳定性,基础能力稳定性,性能稳定性三大类指标数据并上报,衡量客户端系统质量的标准得以完善;通过设立基线,应用特定业务模型对这一系列指标进行监控报警,客户端具备了分钟级感知核心链路稳定性的能力;而通过搭建日志体系,整个系统有了提取关键线索能力,多维度快速定位问题。当问题一旦定位,我们就能通过美团外卖的线上运维规范进行容灾操作:降级,切换通道或限流,从而保证整体的核心链路稳定性。

监控&报警

监控系统,处于整个服务可靠度层级模型的最底层,是运维一个可靠的稳定系统必不可少的重要组成部分。为了保障全链路业务和系统高可用运行,需要在用户感知问题之前发现系统中存在的异常,离开了监控系统,我们就没有能力分辨客户端是不是在正常提供服务。

图片3

按照监控的领域方向,可以分成系统监控与业务监控。
系统监控,主要用于基础能力如端到端成功率,服务响应时长,网络流量,硬件性能等相关的监控。系统监控侧重在无业务侵入和定制系统级别的监控,更多侧重在业务应用的底层,多属于单系统级别的监控。
业务监控,侧重在某个时间区间,业务的运行情况分析。业务监控系统构建于系统监控之上,可以基于系统监控的数据指标计算,并基于特定的业务介入,实现多系统之间的数据联合与分析,并根据相应的业务模型,提供实时的业务监控与告警。按照业务监控的时效性,可以继续将其细分成实时业务监控与离线业务监控。

  • 实时业务监控,通过实时的数据采集分析,帮助快速发现及定位线上问题,提供告警机制及介入响应(人工或系统)途径,帮助避免发生系统故障。
  • 离线的业务监控,对一定时间段收集的数据进行数据挖掘、聚合、分析,推断出系统业务可能存在的问题,帮助进行业务上的重新优化或改进的监控。

美团外卖的业务监控,大部分属于实时业务监控。借助美团统一的系统监控建设基础,美团外卖联合公司其他部门将部分监控基础设施进行了改造、共建和整合复用,并打通形成闭环(监控,日志,回捞),我们构建了特定符合外卖业务流程的实时业务监控; 而离线的业务监控,主要通过用户行为的统计与业务数据的挖掘分析,来帮助产品设计,运营策略行为等产生影响,目前这部分监控主要由美团外卖数据组提供服务。值得特别说明的是单纯的信息汇总展示,无需或无法立即做出介入动作的业务监控,可以称之为业务分析,如特定区域的活动消费情况、区域订单数量、特定路径转换率、曝光点击率等,除非这些数据用来决策系统实时状态健康情况,帮助产生系统维护行为,否则这部分监控由离线来处理更合适。

图片4

我们把客户端稳定性指标分为3类维度:业务稳定性指标,基础能力稳定性指标,性能稳定性指标。对不同的指标,我们采用不同的采集方案进行提取上报,汇总到不同系统;在设定完指标后,我们就可以制定基线,并依照特定的业务模型制定报警策略。美团外卖客户端拥有超过40项度量质量指标,其中25项指标支持分钟级别报警。报警通道依据紧急程度支持邮件,IM和短信三条通道。因此,我们团队具备及时发现影响核心链路稳定性的关键指标变化能力。

一个完善的监控报警系统是非常复杂的,因此在设计时一定要追求简化。以下是《Site Reliability Engineering: How Google Runs Production Systems》一书中提到的告警设置原则:

最能反映真实故障的规则应该可预测性强,非常可靠,并且越简单越好
不常用的数据采集,汇总以及告警配置应该定时清除(某些SRE团队的标准是一季度未使用即删除)
没有暴露给任何监控后台、告警规则的采集数据指标应该定时清除

通过监控&报警系统,2017年下半年美团外卖客户端团队共发现影响核心链路稳定性超过20起问题:包括爬虫、流量、运营商403问题、性能问题等。目前,所有问题均已全部改造完毕。

日志体系

监控系统的一个重要特征是生产紧急告警。一旦出现故障,需要有人来调查这项告警,以决定目前是否存在真实故障,是否需要采取特定方法缓解故障,直至查出导致故障的问题根源。

简单定位和深入调试的过程必须要保持非常简单,必须能够被团队中任何一个人所理解。日志体系,在简化这一过程中起到了决定性作用。

图片5

美团外卖的日志体系总体分为3大类:即全量日志系统,个体日志系统,异常日志系统。全量日志系统,主要负责采集整体性指标,如网络可用性,埋点可用性,我们可以通过他了解到系统整体大盘,了解整体波动,确定问题影响范围;异常日志系统,主要采集异常指标,如大图问题,分享失败,定位失败等,我们通过他可以迅速获取异常上下文信息,分析解决问题;而个体日志系统,则用于提取个体用户的关键信息,从而针对性的分析特定客诉问题。这三类日志,构成了完整的客户端日志体系。

图片2

日志的一个典型使用场景是处理单点客诉问题,解决系统潜在隐患。个体日志系统,用于简化工程师提取关键线索步骤,提升定位分析问题效率。在这一领域,美团外卖使用的是点评平台开发的Logan服务。作为美团移动端底层的基础日志库,Logan接入了集团众多日志系统,例如端到端日志、用户行为日志、代码级日志、崩溃日志等,并且这些日志全部都是本地存储,且有多重加密机制和严格的权限审核机制,在处理用户客诉时才对数据进行回捞和分析,保证用户隐私安全。

通过设计和实施美团外卖核心链路日志方案,我们打通了用户交易流程中各系统如订单,用户中心,Crash平台与Push后台之间的底层数据同步;通过输出标准问题分析手册,针对常见个体问题的分析和处理得以标准化;通过制定日志捞取SOP并定期演练,线上追溯能力大幅提升,日常客诉绝大部分可在30分钟内定位原因。在这一过程中,通过个体暴露出影响核心链路稳定性的问题也均已全部改进/修复。

故障排查是运维大型系统的一项关键技能。采用系统化的工具和手段而不仅仅依靠经验甚至运气,这项技能是可以自我学习,也可以内部进行传授。

容灾备份

针对不同级别的服务,应该采取不同的手段进行有效止损。非核心依赖,通过降级向用户提供可伸缩的服务;而核心依赖,采用多通道方式进行依赖备份容灾保证交易路径链路的高可用;异常流量,通过多维度限流,最大限度保证业务可用性的同时,给予用户良好的体验。总结成三点,即:非核心依赖降级、核心依赖备份、过载保护限流。接下来我们分别来阐述这三方面。

降级

图片6

在这里选取美团外卖客户端整体系统结构关系图来介绍非核心依赖降级建设概览。图上中间红色部分是核心关键节点,即外卖业务的核心链路:定位,商家召回,商品展示,下单;蓝色部分,是核心链路依赖的关键服务;黄色部分,是可降级服务。我们通过梳理依赖关系,改造前后端通讯协议,实现了客户端非核心依赖可降级;而后端服务,通过各级缓存,屏蔽隔离策略,实现了业务模块内部可降级,业务之间可降级。这构成了美团外卖客户端整体的降级体系。

右边则是美团外卖客户端业务/技术降级开关流程图。通过推拉结合,缓存更新策略,我们能够分钟级别同步降级配置,快速止损。

目前,美团外卖客户端有超过20项业务/能力支持降级。通过有效降级,我们避开了1次S2级事故,多次S3、S4级事故。此外,降级开关整体方案产出SDK horn,推广至集团酒旅、金融等其他核心业务应用。

备份

核心依赖备份建设上,在此重点介绍美团外卖多网络通道。网络通道,作为客户端的最核心依赖,却是整个全链路体系最不可控的部分,经常出现问题:网络劫持,运营商故障,甚至光纤被物理挖断等大大小小的故障严重影响了核心链路的稳定性。因此,治理网络问题,必须要建设可靠的多通道备份。

图片7

这是美团外卖多网络通道备份示意图。美团外卖客户端拥有Shark、HTTP、HTTPS、HTTP DNS等四条网络通道:整体网络以Shark长连通道为主通道,其余三条通道作为备份通道。配合完备的开关切换流程,可以在网络指标发生骤降时,实现分钟级别的分城市网络通道切换。而通过制定故障应急SOP并不断演练,提升了我们解决问题的能力和速度,有效应对各类网络异常。我们的网络通道开关思路也输出至集团其他部门,有效支持了业务发展。

限流

服务过载是另一类典型的事故。究其原因大部分情况下都是由于少数调用方调用的少数接口性能很差,导致对应服务的性能恶化。若调用端缺乏有效降级容错,在某些正常情况下能够降低错误率的手段,如请求失败后重试,反而会让服务进一步性能恶化,甚至影响本来正常的服务调用。

美团外卖业务在高峰期订单量已达到了相当高的规模量级,业务系统也及其复杂。根据以往经验,在业务高峰期,一旦出现异常流量疯狂增长从而导致服务器宕机,则损失不可估量。

因此,美团外卖前后端联合开发了一套“流量控制系统”,对流量实施实时控制。既能日常保证业务系统稳定运转,也能在业务系统出现问题的时候提供一套优雅的降级方案,最大限度保证业务的可用性,在将损失降到最低的前提下,给予用户良好的体验。

图片8

整套系统,后端服务负责识别打标分类,通过统一的协议告诉前端所标识类别;而前端,通过多级流控检查,对不同流量进行区分处理:弹验证码,或排队等待,或直接处理,或直接丢弃。
面对不同场景,系统支持多级流控方案,可有效拦截系统过载流量,防止系统雪崩。此外,整套系统拥有分接口流控监控能力,可对流控效果进行监控,及时发现系统异常。整套方案在数次异常流量增长的故障中,经受住了考验。

发布

随着外卖业务的发展,美团外卖的用户量和订单量已经达到了相当的量级,在线直接全量发布版本/功能影响范围大,风险高。
版本灰度和功能灰度是一种能够平滑过渡的发布方式:即在线上进行A/B实验,让一部分用户继续使用产品(特性)A,另一部分用户开始使用产品(特性)B。如果各项指标平稳正常,结果符合预期,则扩大范围,将所有用户都迁移到B上来,否则回滚。灰度发布可以保证系统的稳定,在初试阶段就可以发现问题,修复问题,调整策略,保证影响范围不被扩散。

美团外卖客户端在版本灰度及功能灰度已较为完善。
版本灰度 iOS采用苹果官方提供的分阶段发布方式,Android则采用美团自研的EVA包管理后台进行发布。这两类发布均支持逐步放量的分发方式。
功能灰度 功能发布开关配置系统依据用户特征维度(如城市,用户ID)发布,并且整个配置系统有测试和线上两套不同环境,配合固定的上线窗口,保证上线的规范性。
对应的,相应的监控基础设施也支持分用户特征维度(如城市,用户ID)监控,避免了那些无法在整体大盘体现的灰度异常。此外,无论版本灰度或功能灰度,我们均有相应最小灰度周期和回滚机制,保证整个灰度发布过程可控,最小化问题影响。

线上运维

图片9

在故障来临时如何应对,是整个质量保障体系中最关键的环节。没有人天生就能完美的处理紧急情况,面对问题,恰当的处理需要平时不断的演练。
围绕问题的生命周期,即发现、定位、解决(预防),美团外卖客户端团队组建了一套完备的处理流程和规范来应对影响链路稳定性的各类线上问题。整体思路:建立规范,提前建设,有效应对,事后总结。在不同阶段用不同方式解决不同问题,事前确定完整的事故流程管理策略,并确保平稳实施,经常演练,问题的平均恢复时间大大降低,美团外卖核心链路的高稳定性才能够得以保障。

未来展望

当前美团外卖业务仍然处于快速增长期。伴随着业务的发展,背后支持业务的技术系统也日趋复杂。在美团外卖客户端高可用体系建设过程中,我们希望能够通过一套智能化运维系统,帮助工程师快速、准确的识别核心链路各子系统异常,发现问题根源,并自动执行对应的异常解决预案,进一步缩短服务恢复时间,从而避免或减少线上事故影响。

诚然,业界关于自动化运维的探索有很多,但多数都集中在后台服务领域,前端方向成果较少。我们外卖技术团队目前也在同步的探索中,正处于基础性建设阶段,欢迎更多业界同行跟我们一起讨论、切磋。

参考资料

  1. Site Reliability Engineering: How Google Runs Production Systems
  2. 美团点评移动端基础日志库——Logan
  3. 美团点评移动网络优化实践

作者简介

陈航,美团高级技术专家。2015年加入美团,目前负责美团外卖iOS团队,对移动端架构演进,监控报警备份容灾,移动端线上运维等领域有深刻理解。

富强,美团资深工程师。2015年加入美团,是外卖iOS的早期开发者之一,目前作为美团外卖iOS基础设施小组负责人,负责外卖基础设施及广告运营相关业务。

徐宏,美团高级工程师。2016年加入美团,目前作为外卖iOS团队主力开发,负责移动端APM性能监控,高可用基础设施支撑相关推进工作。

招聘

美团外卖长期招聘iOS、Android、FE高级/资深工程师和技术专家,可Base在北京、上海、成都,欢迎有兴趣的同学将简历发送至chenhang03#meituan.com。

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

大众点评账号业务高可用进阶之路

美团点评阅读(2614)

引言

在任何一家互联网公司,不管其主营业务是什么,都会有一套自己的账号体系。账号既是公司所有业务发展留下的最宝贵资产,它可以用来衡量业务指标,例如日活、月活、留存等,同时也给不同业务线提供了大量潜在用户,业务可以基于账号来做用户画像,制定各自的发展路径。因此,账号服务的重要性不言而喻,同时美团业务飞速发展,对账号业务的可用性要求也越来越高。本文将分享一些我们在高可用探索中的实践。

衡量一个系统的可用性有两个指标:1. MTBF (Mean Time Between Failure) 即平均多长时间不出故障;2. MTTR (Mean Time To Recovery) 即出故障后的平均恢复时间。通过这两个指标可以计算出可用性,也就是我们大家比较熟悉的“几个9”。

可用性公式

因此提升系统的可用性,就得从这两个指标入手,要么降低故障恢复的时间,要么延长不出故障的时间。

1. 业务监控

要降低故障恢复的时间,首先得尽早的发现故障,然后才能解决故障,这些故障包括系统内和系统外的,这就需要依赖业务监控系统。

业务监控不同于其他监控系统,业务监控关注的是各个业务指标是否正常,比如账号的登录曲线。大众点评登录入口有很多,从终端上分有App、PC、M站,从登录类型上分有密码登录、快捷登录、第三方登录(微信/QQ/微博)、小程序登录等。需要监控的维度有登录总数、成功数、失败分类、用户地区、App版本号、浏览器类型、登录来源Referer、服务所在机房等等。业务监控最能从直观上告诉我们系统的运行状况。

由于业务监控的维度很多很杂,有时还要增加新的监控维度,并且告警分析需要频繁聚合不同维度的数据,因此我们采用ElasticSearch作为日志存储。整体架构如下图:

每条监控都会根据过去的业务曲线计算出一条基线(见下图),用来跟当前数据做对比,超出设定的阈值后就会触发告警。

监控曲线图

每次收到告警,我们都要去找出背后的原因,如果是流量涨了,是有活动了还是被刷了?如果流量跌了,是日志延时了还是服务出问题了?另外值得重视的是告警的频次,如果告警太多就会稀释大家的警惕性。我们曾经踩过一次坑,因为告警太多就把告警关了,结果就在关告警的这段时间业务出问题了,我们没有及时发现。为了提高每条告警的定位速度,我们在每条告警后面加上维度分析。如下图(非真实数据),告警里直接给出分析结果。

监控告警分析图

其实业务监控也从侧面反映出一个系统的可用性,所谓服务未动,监控先行。

2. 柔性可用

柔性可用的目的是延长不出故障的时间,当业务依赖的下游服务出故障时不影响自身的核心功能或服务。账号对上层业务提供的鉴权和查询服务即核心服务,这些服务的QPS非常高,业务方对它们的可用性要求也很高,别说是服务故障,就连任何一点抖动都是不能接受的。对此我们先从整体架构上把服务拆分,其次在服务内对下游依赖做资源隔离,都尽可能的缩小故障发生时的影响范围。

另外对非关键路径上的服务故障做了降级。例如账号的一个查询服务依赖Redis,当Redis抖动的时候服务的可用性也随之降低,我们通过公司内部另外一套缓存中间件Cellar来做Redis的备用存储,当检测到Redis已经非常不可用时就切到Cellar上。通过开源组件Hystrix或者我们公司自研的中间件Rhino就能非常方便地解决这类问题,其原理是根据最近一个时间窗口内的失败率来预测下一个请求需不需要快速失败,从而自动降级,这些步骤都能在毫秒级完成,相比人工干预的情况提升几个数量级,因此系统的可用性也会大幅提高。下图是优化前后的对比图,可以非常明显的看到,系统的容错能力提升了,TP999也能控制在合理范围内。

性能对比图前

性能对比图后

对于关键路径上的服务故障我们可以减少其影响的用户数。比如手机快捷登录流程里的某个关键服务挂了,我们可以在返回的失败文案上做优化,并且在登录入口挂小黄条提示,让用户主动去其他登录途径,这样对于那些设置过密码或者绑定了第三方的用户还有其他选择。

具体的做法是我们在每个登录入口都关联了一个计数器,一旦其中的关键节点不可用,就会在受影响的计数器上加1,如果节点恢复,则会减1,每个计数器还分别对应一个标志位,当计数器大于0时,标志位为1,否则标志位为0。我们可以根据当前标志位的值得知登录入口的可用情况,从而在登录页展示不同的提示文案,这些提示文案一共有2^5=32种。

登录降级流程图

下图是我们在做故障模拟时的降级提示文案:

3. 异地多活

除了柔性可用,还有一种思路可以来延长不出故障的时间,那就是做冗余,冗余的越多,系统的故障率就越低,并且是呈指数级降低。不管是机房故障,还是存储故障,甚至是网络故障,都能依赖冗余去解决,比如数据库可以通过增加从库的方式做冗余,服务层可以通过分布式架构做冗余,但是冗余也会带来新的问题,比如成本翻倍,复杂性增加,这就要衡量投入产出比。

目前美团的数据中心机房主要在北京上海,各个业务都直接或间接的依赖账号服务,尽管公司内已有北上专线,但因为专线故障或抖动引发的账号服务不可用,间接导致的业务损失也不容忽视,我们就开始考虑做跨城的异地冗余,即异地多活。

3.1 方案设计

首先我们调研了业界比较成熟的做法,主流思路是分set化,优点是非常利于扩展,缺点是只能按一个维度划分。比如按用户ID取模划分set,其他的像手机号和邮箱的维度就要做出妥协,尤其是这些维度还有唯一性要求,这就使得数据同步或者修改都增加了复杂度,而且极易出错,给后续维护带来困难。考虑到账号读多写少的特性(读写比是350:1),我们采用了一主多从的数据库部署方案,优先解决读多活的问题。

Redis如果也用一主多从的模式可行吗?答案是不行,因为Redis主从同步机制会优先尝试增量同步,当增量同步不成功时,再去尝试全量同步,一旦专线发生抖动就会把主库拖垮,并进一步阻塞专线,形成“雪崩效应”。因此两地的Redis只能是双主模式,但是这种架构有一个问题,就是我们得自己去解决数据同步的问题,除了保证数据不丢,还要保证数据一致。

另外从用户进来的每一层路由都要是就近的,因此DNS需要开启智能解析,SLB要开启同城策略,RPC已默认就近访问。

总体上账号的异地多活遵循以下三个原则:
1.北上任何一地故障,另一地都可提供完整服务。
2.北上两地同时对外提供服务,确保服务随时可用。
3.两地服务都遵循BASE原则,确保数据最终一致。

最终设计方案如下:

异地多活架构图

3.2 数据同步

首先要保证数据在传输的过程中不能丢,因此需要一个可靠接收数据的地方,于是我们采用了公司内部的MQ平台Mafka(类Kafka)做数据中转站。可是消息在经过Mafka传递之后可能是乱序的,这导致对同一个key的一串操作序列可能导致不一致的结果,这是不可忍受的。但Mafka只是不保证全局有序,在单个partition内却是有序的,于是我们只要对每个key做一遍一致性散列算法对应一个partitionId,这样就能保证每个key的操作是有序的。

但仅仅有序还不够,两地的并发写仍然会造成数据的不一致。这里涉及到分布式数据的一致性问题,业界有两种普遍的认知,一种是Paxos协议,一种是Raft协议,我们吸取了对实现更为友好的Raft协议,它主张有一个主节点,其余是从节点,并且在主节点不可用时,从节点可晋升为主节点。简单来说就是把这些节点排个序,当写入有冲突时,以排在最前面的那个节点为准,其余节点都去follow那个主节点的值。在技术实现上,我们设计出一个版本号(见下图),实际上是一个long型整数,其中数据源大小即表示节点的顺序,把版本号存入value里面,当两个写入发生冲突的时候只要比较这个版本号的大小即可,版本号大的覆盖小的,这样能保证写冲突时的数据一致性。

异地多活之版本号

写并发时数据同步过程如下图:

写并发时数据同步过程图

这种同步方式的好处显而易见,可以适用于所有的Redis操作且能保证数据的最终一致性。但这也有一些弊端,由于多存了版本号导致Redis存储会增加,另外在该机制下两地的数据其实是全量同步的,这对于那些仅用做缓存的存储来说是非常浪费资源的,因为缓存有数据库可以回源。而账号服务几乎一半的Redis存储都是缓存,因此我们需要对缓存同步做优化。

账号服务的缓存加载与更新模式如下图:

缓存加载与更新模式

我们优化的方向是在缓存加载时不同步,只有在数据库有更新时才去同步。但是数据更新这个流程里不能再使用delete操作,这样做有可能使缓存出现脏数据,比如下面这个例子:

我们对这个问题的解决办法是用set(若key不存在则添加,否则覆盖)代替delete,而缓存的加载用add(若key不存在则添加,否则不修改),这样能保证缓存更新时的强一致性却不需要增加额外存储。考虑到账号修改的入口比较多,我们希望缓存更新的逻辑能拎出来单独处理减少耦合,最后发现公司内部数据同步组件Databus非常适用于该场景,其主要功能是把数据库的变更日志以消息的形式发出来。于是优化后的缓存模式如下图:

从理论变为工程实现的时候还有些需要注意的地方,比如同步消息没发出去、数据收到后写失败了。因此我们还需要一个方法来检测数据不一致的数量,为了做到这点,我们新建了一个定时任务去scan两地的数据做对比统计,如果发现有不一致的还能及时修复掉。

项目上线后,我们也取得一些成果,首先性能提升非常明显,异地的调用平均耗时和TP99、TP999均至少下降80%,并且在一次线上专线故障期间,账号读服务对外的可用性并没有受影响,避免了更大范围的损失。

总结

服务的高可用需要持续性的投入与维护,比如我们会每月做一次容灾演练。高可用也不止体现在某一两个重点项目上,更多的体现在每个业务开发同学的日常工作里。任何一个小Bug都可能引起一次大的故障,让你前期所有的努力都付之东流,因此我们的每一行代码,每一个方案,每一次线上改动都应该是仔细推敲过的。高可用应该成为一种思维方式。最后希望我们能在服务高可用的道路上越走越远。

团队简介

账号团队拥有一群朝气蓬勃的成员:堂堂、德鑫、杨正、可可、徐升、艳豪,虽然他们之中有些刚毕业不久,但技术上都锐意进取,在讨论技术方案时观点鲜明,大家都充分地享受着思想碰撞的火花,这个年轻的团队在一起推进着高可用项目的进行,共同撑起了账号服务的平稳运行及业务发展。

招聘信息

如果你觉得我们的高可用仍有提升空间,欢迎来大众点评基础平台研发组;

如果你想更深入学习高可用的技术细节,欢迎来大众点评基础平台研发组;

如果你想遇到一群志同道合的技术开发,欢迎来大众点评基础平台研发组。

简历传送门:tangtang.sha#dianping.com。

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

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

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