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

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

美团点评阅读(570)

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

可用性的理解

理解目标

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

result

拆解目标

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

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

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

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

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

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

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

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

幼儿时期:2012年前

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

result

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

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

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

result

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

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

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

result

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

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

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

result

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

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

result

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

易运营

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

可限流

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

无状态

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

降级能力

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

result

可测试

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

result

降低发布风险

严格的发布流程

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

result

灰度机制

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

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

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

result

持续关注线上运行情况

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

快速的发现机制

告警的移动化

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

告警的实时化

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

监控的可视化

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

result

有效的恢复机制

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

几点经验

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

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

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

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

可用性不只是技术问题。

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

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

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

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

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

美团点评阅读(305)

业务背景

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

问题的出现

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

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

重现问题

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

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

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

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

定位问题所在

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

性能平台架构图

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

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

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

网络流量图

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

网络流量图

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

网络流量图

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

网络流量图

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

查找问题原因

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

网络流量图

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

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

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

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

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

如何解决问题

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

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

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

经验和教训

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

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

至此,本文完~

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

Docker系列之一:入门介绍

美团点评阅读(302)

Docker简介

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

Docker vs VM

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

docker与vm比较

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

Docker Component

docker组成

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

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

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

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

Docker网络

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

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

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

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

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

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

Docker 使用

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

docker version

查看当前系统Docker信息

docker info

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

docker images

从Docker hub上下载某个镜像:

docker pull ubuntu:latest
docker pull ubuntu:latest

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

启动一个容器使用docker run:

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

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

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

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

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

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

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

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

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

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

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

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

ADD myproject /opt/code

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

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

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

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

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

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

docker rm container_id
docker rmi image_id

Docker生态

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

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

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

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

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

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

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

总结

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

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

参考文献

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

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

Kafka文件存储机制那些事

美团点评阅读(289)

Kafka是什么

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

1.前言

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

2.Kafka文件存储机制

Kafka部分名词解释如下:

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

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

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

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

2.1 topic中partition存储分布

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

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

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

2.2 partiton中文件存储方式

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

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

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

2.3 partiton中segment文件存储结构

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

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

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

            图2

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

            图3

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

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

           图4

参数说明:

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

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

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

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

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

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

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

实验环境:

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

image

                              图5                                 

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

写message

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

读message

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

4.总结

Kafka高效文件存储设计特点

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

参考

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

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

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

美团点评阅读(287)

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

背景

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

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

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

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

原理

自动构建原理图

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

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

Stash上的配置

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

为什么用Docker?

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

收益

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

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

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

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

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

总结和展望

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

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

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

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

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

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

前端组件化开发实践

美团点评阅读(290)

前言

一位计算机前辈曾说过:

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在美团的实践

美团点评阅读(280)

美团网在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系统的前端演变和发展

美团点评阅读(1121)

美团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

美团数据仓库-数据脱敏

美团点评阅读(1023)

背景与目标

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

举个例子简单的说明下,例如我们仓库中有一张关于注册用户的基本信息表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

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

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