简介:可观测性作为技术体系的外围环节之一,追随饿了么技术的飞速发展,一直自我变革。序言工夫回到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 codestransaction.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连贯,可针对性应用高性能机器。3. SDK中的Agent如何管制对业务利用的影响?纯异步的API,外部采纳队列解决,队列满了就抛弃。Agent不会写本地日志,防止占用磁盘IO、磁盘存储而影响业务利用。Agent会定时从Collector拉取配置信息,以获取后端Collector具体IP,并可实时配置来开关是否执行埋点。4. 为什么抉择侵入性的Agent?抉择寄生在业务利用中的SDK模式,在过后看来更利于ETrace的遍及与降级。而从当初的眼光看来,非侵入式的Agent对用户的集成更加便当,并且能够通过Kubernates中SideCar的形式对用户通明部署与降级。
5. 如何实现“尽量不丢数据”?Agent中依据取得的Collector IP周期性数据发送,若失败则重试3次。并定期(5分钟)获取Collector集群的IP列表,随机选取可用的IP发送数据。Collector中实现了基于本地磁盘的Queue,在后端的Kafka不可用时,会将可观测性数据写入到本地磁盘中。待Kafak复原后,又会将磁盘上的数据,持续写入Kafka。6. 可观测性数据如何实现多语言反对?Agent与Collector之间抉择Thrift RPC框架,并定制整个序列化形式。Java/Python/Go/PHP的Agent依数据标准开发即可。
...