请先关注 [低调大师] 公众号 优秀的自媒体个人博客,低调大师,许军

低调大师

您现在的位置是:首页>文章详情

文章详情

饿了么EMonitor演进史

2021-04-23 63热度

序言

时间回到2008年,还在上海交通大学上学的张旭豪、康嘉等人在上海创办了饿了么,从校园外卖场景出发,饿了么一步一步发展壮大,成为外卖行业的领头羊。2017年8月饿了么并购百度外卖,强强合并,继续开疆扩土。2018年饿了么加入阿里巴巴大家庭,与口碑融合成立阿里巴巴本地生活公司。“爱什么,来什么”,是饿了么对用户不变的承诺。

饿了么的技术也伴随着业务的飞速增长也不断突飞猛进。据公开报道,2014年5月的日订单量只有10万,但短短几个月之后就冲到了日订单百万,到当今日订单上千万单。在短短几年的技术发展历程上,饿了么的技术体系、稳定性建设、技术文化建设等都有长足的发展。各位可查看《饿了么技术往事》一探其中发展历程,在此不再赘述:

《饿了么技术往事(上)》

《饿了么技术往事(中)》

《饿了么技术往事(下)》

而可观测性作为技术体系的核心环节之一,也跟随饿了么技术的飞速发展,不断自我革新,从“全链路可观测性ETrace”扩展到“多活下的可观测性体系ETrace”,发展成目前“一站式可观测性平台EMonitor”。

EMonitor经过5年的多次迭代,现在已经建成了集指标数据、链路追踪、可视化面板、报警与分析等多个可观测性领域的平台化产品。EMonitor每日处理约1200T的原始可观测性数据,覆盖饿了么绝大多数中间件,可观测超5万台机器实例,可观测性数据时延在10秒左右。面向饿了么上千研发人员,EMonitor提供精准的报警服务和多样化的触达手段,同时运行约2万的报警规则。本文就细数饿了么可观测性的建设历程,回顾下“饿了么可观测性建设的那些年”。

1.0:混沌初开,万物兴起

翻看代码提交记录,ETrace项目的第一次提交在2015年10月24日。而2015年,正是饿了么发展的第七个年头,也是饿了么业务、技术、人员开始蓬勃发展的年头。彼时,饿了么的可观测性系统依赖Zabbix、Statsd、Grafana等传统的“轻量级”系统。而“全链路可观测性”正是当时的微服务化技术改造、后端服务Java化等技术发展趋势下的必行之势。

我们可观测性团队,在调研业界主流的全链路可观测性产品--包括著名的开源全链路可观测性产品“CAT”后,吸取众家之所长,在两个多月的爆肝开发后,推出了初代ETrace。我们提供的Java版本ETrace-Agent随着新版的饿了么SOA框架“Pylon”在饿了么研发团队中的推广和普及开来。ETrace-Agent能自动收集应用的SOA调用信息、API调用信息、慢请求、慢SQL、异常信息、机器信息、依赖信息等。下图为1.0版本的ETrace页面截图。

在经历了半年的爆肝开发和各中间件兄弟团队的鼎力支持,我们又开发了Python版本的Agent,更能适应饿了么当时各语言百花齐放的技术体系。并且,通过和饿了么DAL组件、缓存组件、消息组件的密切配合与埋点,用户的应用增加了多层次的访问信息,链路更加完整,故障排查过程更加清晰。

整体架构体系

ETrace整体架构如下图。通过SDK集成在用户应用中的Agent定期将Trace数据经Thrift协议发送到Collector(Agent本地不落日志),Collector经初步过滤后将数据打包压缩发往Kafka。Kafka下游的Consumer消费这些Trace数据,一方面将数据写入HBase+HDFS,一方面根据与各中间件约定好的埋点规则,将链路数据计算成指标存储到时间序列数据库-- LinDB中。在用户端,Console服务提供UI及查询指标与链路数据的API,供用户使用。

全链路可观测性的实现

所谓全链路可观测性,即每次业务请求中都有唯一的能够标记这次业务完整的调用链路,我们称这个ID为RequestId。而每次链路上的调用关系,类似于树形结构,我们将每个树节点上用唯一的RpcId标记。

如图,在入口应用App1上会新建一个随机RequestId(一个类似UUID的32位字符串,再加上生成时的时间戳)。因它为根节点,故RpcId为“1”。在后续的RPC调用中,RequestId通过SOA框架的Context传递到下一节点中,且下一节点的层级加1,变为形如“1.1”、“1.2”。如此反复,同一个RequestId的调用链就通过RpcId还原成一个调用树。

也可以看到,“全链路可观测性的实现”不仅依赖与ETrace系统自身的实现,更依托与公司整体中间件层面的支持。如在请求入口的Gateway层,能对每个请求生成“自动”新的RequestId(或根据请求中特定的Header信息,复用RequestId与RpcId);RPC框架、Http框架、Dal层、Queue层等都要支持在Context中传递RequestId与RpcId。

ETrace Api示例

在Java或Python中提供链路埋点的API:

/* 记录一个调用链路 / Transaction trasaction = Trace.newTransaction(String type, String name); // business codes transaction.complete();

/* 记录调用中的一个事件 / Trace.logEvent(String type, String name, Map<String,String> tags, String status, String data)

/* 记录调用中的一个异常 / Trace.logError(String msg, Exception e)

Consumer的设计细节

Consumer组件的核心任务就是将链路数据写入存储。主要思路是以RequestId+RpcId作为主键,对应的Data数据写入存储的Payload。再考虑到可观测性场景是写多读少,并且多为文本类型的Data数据可批量压缩打包存储,因此我们设计了基于HDFS+HBase的两层索引机制。

如图,Consumer将Collector已压缩好的Trace数据先写入HDFS,并记录写入的文件Path与写入的Offset,第二步将这些“索引信息”再写入HBase。特别的,构建HBase的Rowkey时,基于ReqeustId的Hashcode和HBase Table的Region数量配置,来生成两个Byte长度的ShardId字段作为Rowkey前缀,避免了某些固定RequestId格式可能造成的写入热点问题。(因RequestId在各调用源头生成,如应用自身、Nginx、饿了么网关层等。可能某应用错误设置成以其AppId为前缀RequestId,若没有ShardId来打散,则它所有RequestId都将落到同一个HBase Region Server上。)

在查询时,根据RequestId + RpcId作为查询条件,依次去HBase、HDFS查询原始数据,便能找到某次具体的调用链路数据。但有的需求场景是,只知道源头的RequestId需要查看整条链路的信息,希望只排查链路中状态异常的或某些指定RPC调用的数据。因此,我们在HBbase的Column Value上还额外写了RPCInfo的信息,来记录单次调用的简要信息。如:调用状态、耗时、上下游应用名等。

此外,饿了么的场景下,研发团队多以订单号、运单号作为排障的输入,因此我们和业务相关团队约定特殊的埋点规则--在Transaction上记录一个特殊的"orderId={实际订单号}"的Tag--便会在HBase中新写一条“订单表”的记录。该表的设计也不复杂,Rowkey由ShardId与订单号组成,Columne Value部分由对应的RequestId+RpcId及订单基本信息(类似上文的RPCInfo)三部分组成。

如此,从业务链路到全链路信息到详细单个链路,形成了一个完整的全链路排查体系。

Consumer组件的另一个任务则是将链路数据计算成指标。实现方式是在写入链路数据的同时,在内存中将Transaction、Event等数据按照既定的计算逻辑,计算成SOA、DAL、Queue等中间件的指标,内存稍加聚合后再写入时序数据库LinDB。

指标存储:LinDB 1.0

应用指标的存储是一个典型的时间序列数据库的使用场景。根据我们以前的经验,市面上主流的时间序列数据库-- OpenTSDB、InfluxDB、Graphite--在扩展能力、集群化、读写效率等方面各有缺憾,所以我们选型使用RocksDB作为底层存储引擎,借鉴Kafka的集群模式,开发了饿了么的时间序列数据库--LinDB。

指标采用类似Prometheus的“指标名+键值对的Tags”的数据模型,每个指标只有一个支持Long或Double的Field。某个典型的指标如:

COUNTER: eleme_makeorder{city="shanghai",channel="app",status="success"} 45

我们主要做了一些设计实现:

• 指标写入时根据“指标名+Tags”进行Hash写入到LinDB的Leader上,由Leader负责同步给他的Follower。

• 借鉴OpenTSDB的存储设计,将“指标名”、TagKey、TagValue都转化为Integer,放入映射表中以节省存储资源。

• RocksDB的存储设计为:以"指标名+TagKeyId + TagValueId+时间(小时粒度)“作为Key,以该小时时间线内的指标数值作为Value。

• 为实现Counter、Timer类型数据聚合逻辑,开发了C++版本RocksDB插件。

这套存储方案在初期很好的支持了ETrace的指标存储需求,为ETrace大规模接入与可观测性数据的时效性提供了坚固的保障。有了ETrace,饿了么的技术人终于能从全链路的角度去排查问题、治理服务,为之后的技术升级、架构演进,提供了可观测性层面的支持。

其中架构的几点说明

  1. 是否保证所有可观测性数据的可靠性? 不,我们承诺的是“尽可能不丢”,不保证100%的可靠性。基于这个前提,为我们设计架构时提供了诸多便利。如,Agent与Collector若连接失败,若干次重试后便丢弃数据,直到Collector恢复可用;Kafka上下游的生产和消费也不必Ack,避免影响处理效率。

  2. 为什么在SDK中的Agent将数据发给Collector,而不是直接发送到Kafka?

  • 避免Agent与Kafka版本强绑定,并避免引入Kafka Client的依赖。
  • 在Collector层可以做数据的分流、过滤等操作,增加了数据处理的灵活性。并且Collector会将数据压缩后再发送到Kafka,有效减少Kafka的带宽压力。
  • Collector机器会有大量TCP连接,可针对性使用高性能机器。
  1. SDK中的Agent如何控制对业务应用的影响?
  • 纯异步的API,内部采用队列处理,队列满了就丢弃。
  • Agent不会写本地日志,避免占用磁盘IO、磁盘存储而影响业务应用。
  • Agent会定时从Collector拉取配置信息,以获取后端Collector具体IP,并可实时配置来开关是否执行埋点。
  1. 为什么选择侵入性的Agent?

选择寄生在业务应用中的SDK模式,在当时看来更利于ETrace的普及与升级。而从现在的眼光看来,非侵入式的Agent对用户的集成更加便利,并且可以通过Kubernates中SideCar的方式对用户透明部署与升级。

  1. 如何实现“尽量不丢数据”?
  • Agent中根据获得的Collector IP周期性数据发送,若失败则重试3次。并定期(5分钟)获取Collector集群的IP列表,随机选取可用的IP发送数据。
  • Collector中实现了基于本地磁盘的Queue,在后端的Kafka不可用时,会将可观测性数据写入到本地磁盘中。待Kafak恢复后,又会将磁盘上的数据,继续写入Kafka。
  1. 可观测性数据如何实现多语言支持?

Agent与Collector之间选择Thrift RPC框架,并定制整个序列化方式。Java/Python/Go/PHP的Agent依数据规范开发即可。

2.0:异地多活,大势初成

2016年底,饿了么为了迎接业务快速增长带来的调整,开始推进“异地多活”项目。新的多数据中心架构对既有的可观测性架构也带来了调整,ETrace亦经过了一年的开发演进,升级到多数据中心的新架构、拆分出实时计算模块、增加报警功能等,进入ETrace2.0时代。

异地多活的挑战

随着饿了么的异地多活的技术改造方案确定,对可观测性平台提出了新的挑战:如何设计多活架构下的可观测性系统?以及如何聚合多数据中心的可观测性数据?

经过一年多的推广与接入,ETrace已覆盖了饿了么绝大多数各语言的应用,每日处理数据量已达到了数十T以上。在此数据规模下,决不可能将数据拉回到某个中心机房处理。因此“异地多活”架构下的可观测性设计的原则是:各机房处理各自的可观测性数据。

我们开发一个Gateway模块来代理与聚合各数据中心的返回结果,它会感知各机房间内Console服务。图中它处于某个中央的云上区域,实际上它可以部署在各机房中,通过域名的映射机制来做切换。

如此部署的架构下,各机房中的应用由与机房相绑定的环境变量控制将可观测性数据发送到该机房内的ETrace集群,收集、计算、存储等过程都在同一机房内完成。用户通过前端Portal来访问各机房内的数据,使用体验和之前类似。

即使考虑极端情况下--某机房完全不可用(如断网),“异地多活”架构可将业务流量切换到存活的机房中,让业务继续运转。而可观测性上,通过将Portal域名与Gateway域名切换到存活的机房中,ETrace便能继续工作(虽然会缺失故障机房的数据)。在机房网络恢复后,故障机房内的可观测性数据也能自动恢复(因为该机房内的可观测性数据处理流程在断网时仍在正常运作)。

可观测性数据实时处理的挑战

在1.0版本中的Consumer组件,既负责将链路数据写入到HBase/HDFS中,又负责将链路数据计算成指标存储到LinDB中。两个流程可视为同步的流程,但前者可接受数分钟的延迟,后者要求达到实时的时效性。当时HBase集群受限于机器性能与规模,经常在数据热点时会写入抖动,进而造成指标计算抖动,影响可用性。因此,我们迫切需要拆分链路写入模块与指标计算模块。

在选型实时计算引擎时,我们考虑到需求场景是:

  1. 能灵活的配置链路数据的计算规则,最好能动态调整;
  2. 能水平扩展,以适应业务的快速发展;
  3. 数据输出与既有系统(如LinDB与Kafka)无缝衔接;

很遗憾的是,彼时业界无现成的拿来即用的大数据流处理产品。我们就基于复杂事件处理(CEP)引擎Esper实现了一个类SQL的实时数据计算平台--Shaka。Shaka包括“Shaka Console”和“Shaka Container”两个模块。Shaka Console由用户在图形化界面上使用,来配置数据处理流程(Pipeline)、集群、数据源等信息。用户完成Pipeline配置后,Shaka Console会将变更推送到Zookeeper上。无状态的Shaka Container会监听Zookeeper上的配置变更,根据自己所属的集群去更新内部运行的Component组件。而各Component实现了各种数据的处理逻辑:消费Kafka数据、处理Trace/Metric数据、Metric聚合、运行Esper逻辑等。

Trace数据和Metric格式转换成固定的格式后,剩下来按需编写Esper语句就能生成所需的指标了。如下文所示的Esper语句,就能将类型为Transaction的Trace数据计算成以“{appId}.transaction”的指标(若Consumer中以编码方式实现,约需要近百行代码)。经过这次的架构升级,Trace数据能快速的转化为实时的Metric数据,并且对于业务的可观测性需求,只用改改SQL语句就能快速满足,显著降低了开发成本和提升了开发效率。

@Name('transaction') @Metric(name = '{appId}.transaction', tags = {'type', 'name', 'status', 'ezone', 'hostName'}, fields = {'timerCount', 'timerSum', 'timerMin', 'timerMax'}, sampling = 'sampling') select header.ezone as ezone, header.appId as appId, header.hostName as hostName, type as type, name as name, status as status, trunc_sec(timestamp, 10) as timestamp, f_sum(sum(duration)) as timerSum, f_sum(count(1)) as timerCount, f_max(max(duration)) as timerMax, f_min(min(duration)) as timerMin, sampling('Timer', duration, header.msg) as sampling 

from transaction group by header.appId, type, name, header.hostName, header.ezone, status, trunc_sec(timestamp, 10);

新的UI、更丰富的中间件数据

1.0版本的前端UI,是集成在Console项目中基于Angular V1开发的。我们迫切希望能做到前后端分离,各司其职。于是基于Angular V2的若干个月开发,新的Portal组件登场。得益于Angular的数据绑定机制,新的ETrace UI各组件间联动更自然,排查故障更方便。

饿了么自有中间件的研发进程也在不断前行,在可观测性的打通上也不断深化。2.0阶段,我们进一步集成了--Redis、Queue、ElasticSearch等等,覆盖了饿了么所有的中间件,让可观测性无死角。

杀手级功能:指标查看与链路查看的无缝整合

传统的可观测性系统提供的排障方式大致是:接收报警(Alert)--查看指标(Metrics)--登陆机器--搜索日志(Trace/Log),而ETrace通过Metric与Trace的整合,能让用户直接在UI上通过点击就能定位绝大部分问题,显著拔高了用户的使用体验与排障速度。

某个排查场景如:用户发现总量异常突然增加,可在界面上筛选机房、异常类型等找到实际突增的某个异常,再在曲线上直接点击数据点,就会弹出对应时间段的异常链路信息。链路上有详细的上下游信息,能帮助用户定位故障。

它的实现原理如上图所示。具体的,前文提到的实时计算模块Shaka将Trace数据计算成Metric数据时,会额外以抽样的方式将Trace上的RequsetId与RpcId也写到Metric上(即上文Esper语句中,生成的Metric中的sampling字段)。这种Metric数据会被Consumer模块消费并写入到HBase一张Sampling表中。

用户在前端Portal的指标曲线上点击某个点时,会构建一个Sampling的查询请求。该请求会带上:该曲线的指标名、数据点的起止时间、用户选择过滤条件(即Tags)。Consumer基于这些信息构建一个HBase的RegexStringComparator的Scan查询。查询结果中可能会包含多个结果,对应着该时间点内数据点(Metric)上发生的多个调用链路(Trace),继而拿着结果中的RequestId+RpcId再去查询一次HBase/HDFS存储就能获得链路原文。(注:实际构建HBase Rowkey时Tag部分存的是其Hashcode而不是原文String。)

众多转岗、离职的饿了么小伙伴,最念念不忘不完的就是这种“所见即所得”的可观测性排障体验。

报警Watchdog 1.0

在应用可观测性基本全覆盖之后,报警的需求自然成了题中之义。技术选型上,根据我们在实时计算模块Shaka上收获的经验,决定再造一个基于实时数据的报警系统--Watchdog。

实时计算模块Shaka中已经将Trace数据计算成指标Metrics,报警模块只需消费这些数据,再结合用户配置的报警规则产出报警事件即可。因此,我们选型使用Storm作为流式计算平台,在Spount层次根据报警规则过滤和分流数据,在Bolt层中Esper引擎运行着由用户配置的报警规则转化成Esper语句并处理分流后的Metric数据。若有符合Esper规则的数据,即生成一个报警事件Alert。Watchdog Portal模块订阅Kakfa中的报警事件,再根据具体报警的触达方式通知到用户。默认Esper引擎中数据聚合时间窗口为1分钟,所以整个数据处理流程的时延约为1分钟左右。

Metrics API与LinDB 2.0

在ETrace 1.0阶段,我们只提供了Trace相关的API,LinDB仅供内部存储使用。用户逐步的意识到如果能将“指标”与“链路”整合起来,就能发挥更大的功用。因此我们在ETrace-Agent中新增了Metrics相关的API:

// 计数器类型

Trace.newCounter(String metricName).addTags(Map<String, String> tags).count(int value);

// 耗时与次数

Trace.newTimer(String metricName).addTags(Map<String, String> tags).value(int value);

// 负载大小与次数

Trace.newPayload(String metricName).addTags(Map<String, String> tags).value(int value);

// 单值类型

Trace.newGauge(String metricName).addTags(Map<String, String> tags).value(int value);

基于这些API,用户可以在代码中针对他的业务逻辑进行指标埋点,为后来可观测性大一统提供了实现条件。在其他组件同步开发时,我们也针对LinDB做了若干优化,提升了写入性能与易用性:

  1. 增加Histogram、Gauge、Payload、Ratio多种指标数据类型;
  2. 从1.0版本的每条指标数据都调用一次RocksDB的API进行写入,改成先在内存中聚合一段时间,再通过RocksDB的API进行批量写入文件。

3.0:推陈出新,融会贯通

可观测性系统大一统

在2017年的饿了么,除了ETrace外还有多套可观测性系统:基于Statsd/Graphite的业务可观测性系统、基于InfluxDB的基础设施可观测性系统。后两者都集成Grafana上,用户可以去查看他的业务或者机器的详细指标。但实际排障场景中,用户还是需要在多套系统间来回切换:根据Grafana上的业务指标感知业务故障,到ETrace上查看具体的SOA/DB故障,再到Grafana上去查看具体机器的网络或磁盘IO指标。虽然,我们也开发了Grafana的插件来集成LinDB的数据源,但因本质上差异巨大的系统架构,还是让用户“疲于奔命”式的来回切换系统,用户难以有统一的可观测性体验。因此2018年初,我们下定决心:将多套可观测性系统合而为一,打通“业务可观测性+应用可观测性+基础设施可观测性”,让ETrace真正成为饿了么的一站式可观测性平台。

LinDB 3.0

所谓“改造”未动,“存储”先行。想要整合InfluxDB与Statsd,先要研究他们与LinDB的异同。我们发现,InfluxDB是支持一个指标名(Measurement)上有多个Field Key的。如,InfluxDB可能有以下指标:

measurement=census, fields={butterfiles=12, honeybees=23}, tags={location=SH, scientist=jack}, timestamp=2015-08-18T00:06:00Z

若是LinDB 2.0的模式,则需要将上述指标转换成两个指标:

measurement=census, field={butterfiles=12}, tags={location=SH, scientist=jack}, timestamp=2015-08-18T00:06:00Z

measurement=census, field={honeybees=23}, tags={location=SH, scientist=jack}, timestamp=2015-08-18T00:06:00Z

可以想见在数据存储与计算效率上,单Field模式有着极大的浪费。但更改指标存储的Schema,意味着整个数据处理链路都需要做适配和调整,工作量和改动极大。然而不改就意味着“将就”,我们不能忍受对自己要求的降低。因此又经过了几个月的爆肝研发,LinDB 3.0开发完成。

这次改动,除了升级到指标多Fields模式外,还有以下优化点:

• 集群方面引入Kafka的ISR设计,解决了之前机器故障时查询数据缺失的问题。 • 存储层面支持更加通用的多Field模式,并且支持对多Field之间的表达式运算。 • 引入了倒排索引,显著提高了对于任意Tag组合的过滤查询的性能。 • 支持了自动化的Rollup操作,对于任意时间范围的查询自动选取合适的粒度来聚合。

经过这次大规模优化后,从最初的每日5T指标数据涨到如今的每日200T数据,LinDB 3.0都经受住了考验。指标查询的响应时间的99分位线为200ms。详细设计细节可参看分布式时序数据库 - LinDB。

将Statsd指标转成LinDB指标

Statsd是饿了么广泛使用的业务指标埋点方案,各机房有一个数十台机器规模的Graphite集群。考虑到业务的核心指标都在Statsd上,并且各个AppId以ETrace Metrics API替换Statsd是一个漫长的过程(也确实是,前前后后替换完成就花了将近一年时间)。为了减少对用户与NOC团队的影响,我们决定:用户更新代码的同时,由ETrace同时“兼容”Statsd的数据。

得益于饿了么强大的中间件体系,业务在用Statsd API埋点的同时会“自动”记一条特殊的Trace数据,携带上Statsd的Metric数据。那么只要处理Trace数据中的Statsd埋点,我们就能将大多数Statsd指标转化为LinDB指标。如下图:多个Statsd指标会转为同一个LinDB指标。

// statsd: stats.app.myAppName.order.from_ios.success 32 stats.app.myAppName.order.from_android.success 29 stats.app.myAppName.order.from_pc.failure 10 stats.app.myAppName.order.from_openapi.failure 5 // lindb: MetricName: myAppName.order Tags: "tag1"=[from_ios, from_android,from_pc, from_openapi] "tag2"=[success, failure]

之前我们的实时计算模块Shaka就在这里派上了大用场:只要再新增一路数据处理流程即可。如下图,新增一条Statsd数据的处理Pipeline,并输出结果到LinDB。在用户的代码全部从Statsd API迁移到ETrace API后,这一路处理逻辑即可移除。

将InfluxDB指标转成LinDB指标

InfluxDB主要用于机器、网络设备等基础设施的可观测性数据。饿了么每台机器上,都部署了一个ESM-Agent。它负责采集机器的物理指标(CPU、网络协议、磁盘、进程等),并在特定设备上进行网络嗅探(Smoke Ping)等。这个数据采集Agent原由Python开发,在不断需求堆叠之后,已庞大到难以维护;并且每次更新可观测逻辑,都需要全量发布每台机器上的Agent,导致每次Agent的发布都令人心惊胆战。

我们从0开始,以Golang重新开发了一套ESM-Agent,做了以下改进:

• 可观测性逻辑以插件的形式,推送到各宿主机上。不同的设备、不同应用的机器,其上运行的插件可以定制化部署。 • 制定插件的交互接口,让中间件团队可定制自己的数据采集实现,解放了生产力。 • 移除了etcd,使用MySql做配置数据存储,减轻了系统的复杂度。 • 开发了便利的发布界面,可灰度、全量的推送与发布Agent,运维工作变得轻松。 • 最重要的,收集到的数据以LinDB多Fields的格式发送到Collector组件,由其发送到后续的处理与存储流程上。

从ETrace到EMonitor,不断升级的可观测性体验

2017年底,我们团队终于迎来了一名正式的前端开发工程师,可观测性团队正式从后端开发写前端的状态中脱离出来。在之前的Angular的开发体验中,我们深感“状态转换”的控制流程甚为繁琐,并且开发的组件难以复用(虽然其后版本的Angular有了很大的改善)。在调用当时流行的前端框架后,我们在Vue与React之中选择了后者,辅以Ant Design框架,开发出了媲美Grafana的指标看版与便利的链路看板,并且在PC版本之外还开发了移动端的定制版本。我们亦更名了整个可观测性产品,从“ETrace”更新为“EMonitor”:不仅仅是链路可观测性系统,更是饿了么的一站式可观测性平台。

可观测性数据的整合:业务指标 + 应用链路 + 基础设施指标 + 中间件指标

在指标系统都迁移到LinDB后,我们在EMonitor上集成了“业务指标 + 应用链路 + 基础设施指标 + 中间件指标”的多层次的可观测性数据,让用户能在一处观测它的业务数据、排查业务故障、深挖底层基础设施的数据。

可观测性场景的整合:指标 + 链路 + 报警

在可观测性场景上,“指标看板”用于日常业务盯屏与宏观业务可观测性,“链路”作为应用排障与微观业务逻辑透出,“报警”则实现可观测性自动化,提高应急响应效率。

灵活的看板配置与业务大盘

在指标配置上,我们提供了多种图表类型--线图、面积图、散点图、柱状图、饼图、表格、文本等,以及丰富的自定义图表配置项,能满足用户不同数据展示需求。

在完成单个指标配置后,用户需要将若干个指标组合成所需的指标看板。用户在配置页面中,先选择待用的指标,再通过拖拽的方式,配置指标的布局便可实时预览布局效果。一个指标可被多个看板引用,指标的更新也会自动同步到所有看板上。为避免指标配置错误而引起歧义,我们也开发了“配置历史”的功能,指标、看板等配置都能回滚到任意历史版本上。

看板配置是静态图表组合,而业务大盘提供了生动的业务逻辑视图。用户可以根据他的业务场景,将指标配置整合成一张宏观的业务图。

第三方系统整合:变更系统 + SLS日志

因每条报警信息和指标配置信息都与AppId关联,那么在指标看板上可同步标记出报警的触发时间。同理,我们拉取了饿了么变更系统的应用变更数据,将其标注到对应AppId相关的指标上。在故障发生时,用户查看指标数据时,能根据有无变更记录、报警记录来初步判断故障原因。

饿了么的日志中间件能自动在记录日志时加上对应的ETrace的RequestId等链路信息。如此,用户查看SLS日志服务时,能反查到整条链路的RequestId;而EMonitor也在链路查看页面,拼接好了该应用所属的SLS链接信息,用户点击后能直达对应的SLS查看日志上下文。

使用场景的整合:桌面版 + 移动版

除提供桌面版的EMonitor外,我们还开发了移动版的EMonitor,它也提供了大部分可观测性系统的核心功能--业务指标、应用指标、报警信息等。移动版EMonitor能内嵌于钉钉之中,打通了用户认证机制,帮助用户随时随地掌握所有的可观测性信息。

为了极致的体验,精益求精

为了用户的极致使用体验,我们在EMonitor上各功能使用上细细打磨,这里仅举几个小例子:

  1. 我们为极客开发者实现了若干键盘快捷键。例如,“V”键就能展开查看指标大图。
  2. 图上多条曲线时,点击图例是默认单选,目的是让用户只看他关心的曲线。此外,若是“Ctrl+鼠标点击”则是将其加选择的曲线中。这个功能在一张图几十条曲线时,对比几个关键曲线时尤为有用。
  3. 为了让色弱开发者更容易区分成功或失败的状态,我们针对性的调整了对应颜色的对比度。

成为饿了么一站式可观测性平台

EMonitor开发完成后,凭借优异的用户体验与产品集成度,很快在用户中普及开来。但是,EMonitor要成为饿了么的一站式可观测性平台,还剩下最后一战--NOC可观测性大屏。

- NOC可观测性大屏替换

饿了么有一套完善的应急处理与保障团队,包括7X24值班的NOC(Network Operation Center)团队。在NOC的办公区域,有一整面墙上都是可观测性大屏,上面显示着饿了么的实时的各种业务曲线。下图为网上找的一张示例图,实际饿了么的NOC大屏比它更大、数据更多。

当时这个可观测大屏是将Grafana的指标看版投影上去。我们希望将NOC大屏也替换成EMonitor的看版。如前文所说,我们逐步将用户的Statsd指标数据转换成了LinDB指标,在NOC团队的协助下,一个一个将Grafana的可观测性指标“搬”到EMonitor上。此外,在原来白色主题的EMonitor之上,我们开发了黑色主题以适配投屏上墙的效果(白色背景投屏太刺眼)。

终于赶在2018年的双十一之前,EMonitor正式入驻NOC可观测大屏。在双十一当天,众多研发挤在NOC室看着墙上的EMonitor看版上的业务曲线不断飞涨,作为可观测性团队的一员,这份自豪之情由衷而生。经此一役,EMonitor真正成为了饿了么的“一站式可观测性平台”,Grafana、Statsd、InfluxDB等都成了过去时。

报警Watchdog 2.0

同样在EMonitor之前,亦有Statsd与InfluxDB对应的多套报警系统。用户若想要配置业务报警、链路报警、机器报警,需要辗转多个报警系统之间。各系统的报警的配置规则、触达体验亦是千差万别。Watchdog报警系统也面临着统一融合的挑战。

  1. 在调研其他系统的报警规则实现后,Watchdog中仍以LinDB的指标作为元数据实现。
  2. 针对其他报警系统的有显著区别的订阅模式,我们提出了"报警规则+一个规则多个订阅标签+一个用户订阅多个标签"的方式,完美迁移了几乎其他系统所有的报警规则与订阅关系。
  3. 其他各系统在报警触达与触达内容上也略有不同。我们统一整合成“邮件+短信+钉钉+语音外呼”四种通知方式,并且提供可参数化的自定义Markdown模板,让用户可自己定时报警信息。

经过一番艰苦的报警配置与逻辑整合后,我们为用户“自动”迁移了上千个报警规则,并最终为他们提供了一个统一的报警平台。

报警,更精准的报警

外卖行业的业务特性是业务的午高峰与晚高峰,在业务曲线上便是两个波峰的形状。这样的可观测数据,自然难以简单使用阈值或比率来做判断。即使是根据历史同环比、3-Sigma、移动平均等规则,也难以适应饿了么的可观测性场景。因为,饿了么的业务曲线并非一成不变,它受促销、天气因素、区域、压测等因素影响。开发出一个自适应业务曲线变化的报警算法,势在必行。

我们经过调研既有规则,与饿了么的业务场景,推出了全新的“趋势”报警。简要算法如下:

  1. 计算历史10天的指标数据中值作为基线。其中这10天都取工作日或非工作日。不取10天的均值而取中值是为了减少压测或机房流量切换造成的影响。
  2. 根据二阶滑动平均算法,得到滑动平均值与当前实际值的差值。
  3. 将基线与差值相加作为预测值。
  4. 根据预测值的数量级,计算出波动的幅度(如上界与下界的数值)。
  5. 若当前值不在预测值与波动幅度确定的上下界之中,则触发报警。

如上图所示,22点01分的实际值因不在上下界所限定的区域之中,会触发报警。但从后续趋势来看,该下降趋势符合预期,因此实际中还会辅以“偏离持续X分钟”来修正误报。(如该例中,可增加“持续3分钟才报警”的规则,该点的数据便不会报警)算法中部分参数为经验值,而其中波动的阈值参数用户可按照自己业务调整。用户针对具备业务特征的曲线,再也不用费心的去调整参数,配置上默认的“趋势”规则就可以覆盖大多数的可观测性场景,目前“趋势”报警在饿了么广泛运用。

智能可观测性:根因分析,大显神威

作为AIOPS中重要的一环,根因分析能帮助用户快速定位故障,缩短故障响应时间,减少故障造成的损失。2020年初,我们结合饿了么场景,攻坚克难,攻破“指标下钻”、“根因分析”两大难关,在EMonitor上成功落地。

根因分析最大的难点在于:包含复杂维度的指标数据难以找到真正影响数据波动的具体维度;孤立的指标数据也难以分析出应用上下游依赖引起的故障根因。 例如,某个应用的异常指标突增,当前我们只能知道突增的异常名、机房维度的异常分布、机器维度的异常分布等,只有用户手工去点击异常指标看来链路之后,才能大致判断是哪个SOA方法/DB请求中的异常。继而用户根据异常链路的环节,去追溯上游或下游的应用,重复类似的排查过程,最后以人工经验判断出故障点。

因此,在“指标下钻”上,我们针对目标指标的曲线,细分成最精细的每个维度数据(指标group by待分析的tag维度),使用KMeans聚类找出故障数据的各维度的最大公共特征,依次计算找到最优的公共特征,如此便能找到曲线波动对应的维度信息。

其次,在链路数据计算时,我们就能将额外的上下游附加信息附加到对应的指标之中。如,可在异常指标中追加一个维度来记录产生异常的SOA方法名。这样在根据异常指标分析时,能直接定位到是这个应用的那个SOA方法抛出的异常,接下来“自动”分析是SOA下游故障还是自身故障(DB、Cache、GC等)。

在2020.3月在饿了么落地以来,在分析的上百例故障中,根因分析的准确率达到90%以上,显著缩短的故障排查的时间,帮助各业务向稳定性建设目标向前跨进了一大步。

4.0:继往开来,乘势而上

经过4、5年的发展,风云变幻但团队初心不改,为了让用户用好可观测性系统,EMonitor没有停下脚步,自我革新,希望让“天下没有难用的可观测性系统”。我们向集团的可观测性团队请教学习,结合本地生活自己的技术体系建设,力争百尺竿头更进一步,规划了以下的EMonitor 4.0的设计目标。

一、进行多租户化改造,保障核心数据的时延和可靠性

在本地生活的技术体系与阿里巴巴集团技术体系的不断深入的融合之中,单元化的部署环境以及对可观测性数据不同程度的可靠性要求,催生了“多租户化”的设计理念。我们可以根据应用类型、数据类型、来源等,将可观测性数据分流到不同的租户中,再针对性配置数据处理流程及分配处理能力,实现差异化的可靠性保障能力。

初步我们可以划分为两个集群--核心应用集群与非核心应用集合,根据在应用上标记的“应用等级”将其数据自动发送到对应集群中。两套集群在资源配置上优先侧重核心集群,并且完全物理隔离。此外通过配置开关可动态控制某个应用归属的租户,实现业务的柔性降级,避免当下偶尔因个别应用的不正确埋点方式会影响整体可观测可用性的问题。

未来可根据业务发展进一步发展出业务相关的租户,如到家业务集群、到店业务集群等。或者按照区域的划分,如弹内集群、弹外集群等。

二、打通集团弹内、弹外的可观测性数据,成为本地生活的一站式可观测性平台

目前本地生活很多业务领域已经迁入集团,在Trace链路可观测方面,虽然在本地生活上云的项目中,EMonitor已经通过中间件改造实现鹰眼TraceId在链路上的传递,并记录了EMonitor RequestId与鹰眼TraceId的映射关系。但EMonitor与鹰眼在协议上的天然隔阂仍使得用户需要在两个平台间跳转查看同一条Trace链路。因此,我们接下来的目标是与鹰眼团队合作,将鹰眼的Trace数据集成到EMonitor上,让用户能一站式的排查问题。

其次,本地生活上云后,众多中间件已迁移到云上中间件,如云Redis、云Kafka、云Zookeeper等。对应的可观测性数据也需要额外登陆到阿里云控制台去查看。云上中间的可观测性数据大多已存储到Prometheus之中,因此我们计划在完成Prometheus协议兼容后,就与云上中间件团队合作,将本地生活的云上可观测性数据集成到EMonitor上。

三、拥抱云原生,兼容Prometheus、OpenTelemetry等开源协议。

云原生带来的技术革新势不可挡,本地生活的绝大多数应用已迁移到集团的容器化平台--ASI上,对应带来的新的可观测环节也亟需补全。如,ASI上Prometheus协议的容器可观测性数据、Envoy等本地生活PaaS平台透出的可观测性数据与Trace数据等。

因此,我们计划在原先仅支持LinDB数据源的基础上,增加对Prometheus数据源的支持;扩展OpenTelemetry的otel-collector exporter实现,将Open Telemetry协议的Trace数据转换成EMonitor的Trace格式。如此便可补全云原生技术升级引起的可观测性数据缺失,并提供高度的适配性,满足本地生活的可观测性建设。

结语

纵观各大互联网公司的产品演进,技术产品的走向与命运都离不开公司业务的发展轨迹。我们饿了么的技术人是幸运的,能赶上这一波技术变革的大潮,能够发挥聪明才智,打磨出一些为用户津津乐道的技术产品。我们EMonitor可观测性团队也为能参与到这次技术变更中深感自豪,EMonitor能被大家认可, 离不开每位参与到饿了么可观测性体系建设的同伴,也感谢各位对可观测性系统提供帮助、支持、建议的伙伴!

作者简介: 柯圣,花名“炸天”,饿了么监控技术组负责人。自2016年加入饿了么,长期深耕于可观测性领域,全程参与了ETrace到EMonitor的饿了么可观测性系统的发展历程。

点击阅读原文

收藏 (0)

相关文章

    文章评论

    共有0条评论来说两句吧...