作者介绍
滴滴出行可观测架构负责人——钱威
TakinTalks稳定性社区专家团成员,滴滴出行可观测架构负责人。深耕可观测领域多年,专注于架构设计与优化。带领团队完成了滴滴第二代到第四代的架构迭代。多个可观测开源项目的Contributor。目前聚焦在滴滴可观测的稳定性建设和滴滴场景下的可观测性的实现与落地工作。
温馨提醒:本文约7500字,预计花费12分钟阅读。
背景
大家先来看一个故事——
“20世纪初,当时处于高速发展期的福特公司。有一天一台电机坏了,相关生产工作被迫停止。很多工人和专家都找不到问题在哪。直到请到了一个叫斯坦门茨的人,斯坦门茨检查后用粉笔在电机外壳画了一条线,说打开电机,把记号处的线圈减少16圈。修理工照做后,故障排除,生产随即恢复。”
我们在工作或在开发过程中,时常会遇到这样的场景——让你一头雾水,不知道从何下手的难题,但是总有那么一两个“专家”一眼就能洞察问题所在。那么,我们需要思考一下,这到底是好事还是坏事?
滴滴作为一家出行平台,业务涵盖快车、专车、顺风车、共享单车等多个领域。每天有千万的用户和司机在平台上进行交互和使用,服务之间形成了复杂的依赖关系。在如此大规模的分布式系统中,故障排查和性能优化无疑是一项复杂的任务。
每次都依赖于个别专家的经验显然是无法控制的,也无法保证结果。因此,我们更愿意通过不断地演进可观测的架构,来支持业务的快速迭代和创新。
一、可观测架构演进解决了哪些问题?
1.1 滴滴可观测系统通用架构
滴滴可观测系统通用架构主要包含几个部分,如下图所示。
我们会采集目标主机或其他的相关指标,经过传输链路后,某些指标可能会经过计算模块进行处理,然后再写回系统中。随后,这些数据会被存储起来。基于这些存储的数据,查询功能可以为上层应用提供数据展示,如仪表板、数据大盘、报警和事件等。
需要注意的是,每个模块需要完成的任务或实现的功能各不相同。例如,查询模块可能需要负责数据路由、聚合以及实现DSL等功能,这些功能通常在查询层进行实现。
数据存储的实现方式有很多种,如InfluxDB、RRDtool、Prometheus、Druid、ClickHouse等,都可以作为可观测系统的存储方案。
传输模块在系统中起到连接的作用,常见的消息队列就是用在这一模块中。当我们提到消息队列时,大家首先想到的可能是Kafka,当然也有一些较为小众的选择,如NSQ。
计算模块的任务则是将大量的指标转换成我们所需的形式,可能会去除一些维度进行计算。Flink、Spark等工具在这一模块中都是常见的选择。
对于数据采集,也有许多丰富的工具可以选择,如Telegraf、Node exporter,以及最近推出的Grafana Agent等。
1.2 可观测架构演进的4个阶段
1.2.1 阶段一:2017年以前
当业务需求发生变化时,存储模块的性能问题通常是最先暴露出来的。在2017年以前,滴滴主要使用InfluxDB作为存储选择。我们根据业务服务的维度将InfluxDB实例进行了拆分,这样的设计便带来了一些问题。
首先,单机版本的性能存在瓶颈。例如,我们可能会遇到查询量较大的情况,如查询跨度长或查询数据多,这种情况下很可能会出现内存溢出(OOM)的问题。这也是社区中经常讨论的问题。
再者,我们采用的分片方式也存在问题。我们是按照服务进行拆分的,例如,如果今天有50个服务,那可能需要50个或更少的实例。但如果服务数量在明天增加到500个,那么运维成本将随之显著增加。特别是在当前大家普遍采用微服务架构的情况下,这种运维成本将会非常高。
1.2.2 阶段二:2017-2018年
为了解决上述问题,我们在2017年引入了RRDTool。在此期间,RRDTool取代了InfluxDB,成为滴滴可观测的主要存储工具。
在RRDTool的设计中,我们采用了一致性哈希算法,在读写链路中进行多个RRDTool实例的分片。这种哈希算法的过程是先将所有的Tag打平,然后排序,最后再进行哈希,分配到各个实例中。
除此之外,我们还引入了一个名为“索引”的服务。这个服务的主要任务是满足产品需求。比如,我们可能需要提供服务列表,当用户选择了他们自己的服务后,需要知道该服务下有哪些指标,以及每个指标下有哪些Tag。这种需求需要一个高效的索引服务来完成。
基于RRDTool的架构改进带来了两大成果。首先,它解决了InfluxDB的热点问题。我们原来是按照服务去拆分实例,现在我们将这些曲线分散到各个实例上。其次,这也减轻了InfluxDB的运维成本,因为我们采用了相对自动化的分片方式。
1.2.3 阶段三:2018-2020年
在2018年以后,我们面临了新的挑战。由于RRDTool的设计原理是每条曲线一个文件,因此,当数据规模扩大时,对IO的需求也随之增大。我们的IOPS已经超过了3万,这就需要我们增加更多的设备,例如具有高IO性能的机器,以解决这个问题。但是,这导致成本逐渐增高,且问题愈发严重。同时,可观测性中的读写是正交的,读写优化存在冲突——写通常是所有曲线写入最新的部分,而读通常是读取多条曲线或某条曲线长时间的数据。
(纵向为Writes,横向为Reads)
那么,我们如何解决这个问题呢?经过分析,我们发现80%的查询都集中在最近两个小时内,因此,我们设计了一个冷热分层策略。这个策略的核心就是将压缩后的数据存储在内存中。压缩主要针对两个方面,一是时间戳,二是值。由于时间戳产生的时间间隔通常比较固定,而值的变化往往较为平缓,这为我们的压缩策略提供了依据。
基于这个原理,我们内部创建了一个名为"Cacheserver"的服务,主要服务于最近两小时的数据,采用了全内存的设计。这种设计使得用户查询的延迟从10秒降低到了1秒以内,每个数据点的存储由原来的16字节降低到了1.64字节。
整个设计可以通过上述图示来理解。首先是冷热分层,RRDTool和Cacheserver共同完成了整个存储任务。以图示右半部分为例,原始的时间戳为350、360、370、381,存储这些数据需要256比特。但经过压缩后,只需要88比特就足够了。这只是四个时间戳的情况,如果时间戳更多,那么压缩效果会更加显著。
1.2.4 阶段四:2020-至今
随着用户接入的组件不断增多,用户的查询需求也变得越来越复杂。在我们的使用场景中,一旦RRDTool进行了降采,我们就无法再查看到原始数据。
面对这种情况,我们开始思考如何设计一个能满足用户当前和未来需求的系统。我们改变了问题解决的策略,不再针对每个具体情况单独设计方案。例如,如果过去有新增的查询形态,我们会需要编码并上线一个新的函数。而现在,我们选择直接利用业界的生态。
当时,Prometheus是非常流行的。我们将目标从引入生态转变为引入Prometheus的生态。选择Prometheus的原因是,随着K8s的普及,Prometheus已经成为了监控系统的事实标准。许多业界大厂和流行的厂商都在为Prometheus持续贡献代码和架构。
然而,如果我们选择引入Prometheus的生态,就无法继续使用RRDTool,因为它无法兼容Prometheus的生态。这就需要我们寻找新的存储方案。
难点1:新的存储方案如何选择?
在面临新的存储方案选择时,我们主要考虑了Cortex、Thanos和VictoriaMetrics(简称VM)。这些方案都是为了弥补Prometheus本身的一些缺陷而设计的,因为Prometheus从诞生之初就定位为单机存储,不支持长期存储,也没有高可用性。因此,Cortex和Thanos在当时成为了业界主要的解决方案。
(调研业界 Prometheus 相关方案)
在对比这些方案时,我们发现Cortex和Thanos都能有效解决Prometheus的原生缺点。从成本角度考虑,由于Thanos和Cortex都采用了对象存储,因此它们的成本相对较低。但是,这两个方案由于使用了大量的第三方服务,如果公司没有对象存储或者没有云服务,那么这些组件的维护工作可能就需要由可观测团队来完成。
(RRDTool与VictoriaMetrics方案对比)
相比之下,VM与RRDTool相比,它是完全兼容Prometheus的。此外,我们之前提到过降采策略,RRDTool的数据在超过两小时后会进行降采,一旦降采,我们就无法查看到原始数据。而VM本身不进行降采,这为我们带来了更多可能性。在降低存储成本方面,VM的表现较好,在我们的环境测试中,其存储成本只有RRDTool的1/20左右。在数据上报形态上,Prometheus是Pull形式,而RRDTool只能支持Push形式,并且只支持私有协议。但VM既支持Pull也支持Push,对流行的数据上报协议也有良好的支持。
难点2:如何引入Prometheus生态?
那么,我们是否可以简单地将存储方案替换为VM呢?实际上,答案是否定的。在引入新的生态系统时,我们首先需要考虑现有的公司方案。引入新的生态并不意味着要完全颠覆现有的产品架构,不能简单地进行替换。
为了引入新的生态,滴滴进行了一些改造。如图所示,绿色部分是使用Prometheus原生方案所需完成的工作。只要被监控的对象支持"/metrics"这样的接口,Prometheus便可以进行数据拉取。对滴滴而言,我们原来的架构是基于采集、传输、存储的Push模型。因此,我们在采集部分增加了一个兼容Prometheus的Adapter。在原有基础上,对于那些新增并且支持Prometheus拉取的服务,我们也可以使用自有的采集方法进行数据拉取。
在生态引入的成果方面,我们已经支持了Prometheus的数据采集,并且可以支持PromQL的图表查看和报警这两个常见场景。此外,我们还在图表查看这个维度上增加了一些新的功能,比如增加了TopK/BottomK等图表维度的Outlier能力。这样,如果一个服务有很多个实例,我们就可以利用TopK/BottomK这样的功能找出异常点。
在回馈社区方面,我们向VM官方和Prometheus社区递交了一些PR,以此为整个社区做出贡献。
二、如何保障可观测系统自身稳定性?
众所周知,可观测系统的目的是保障业务的稳定性。那么,我们如何保障可观测系统本身的稳定性呢?首先,我们需要探讨如何监测这个可观测的系统。是否可以在自身的系统上配置一些策略?或者建立一些仪表盘?或者采取其他一些方式?在这方面,我将分享一些我们的实验和思考。
2.1 如何观测可观测系统?
我们不能让可观测的系统对其本身做观测。例如,如果存储系统出现故障,而查询数据的方式是从自身的存储中查询,那么就会形成循环依赖。因此,第一个原则就是不能让可观测的系统自观测。第二个原则与第一个原则有关,即需要一套独立的数据采集和报警服务来进行观测。
在我们的实践中,主要采用了两种方法。
第一种方法用于监测流量,适用于数据采集、传输和存储。这种方法主要通过使用Exporter、Prometheus和Alertmanager来进行自我监测。例如,如果存储写入流量突然变化,就可以使用这套系统进行自我监测。
另一种方法是监测能力。以报警为例,最简单的方法是设置一条始终会触发阈值的报警,但可能不会发送实时消息或短信通知。一旦报警事件中断,可能是因为报警系统本身存在问题,或者报警系统所依赖的存储查询存在问题。在这种情况下,我们可以通过设置探测器和进行端到端的检查来解决问题。
2.2 如何保障可观测架构始终稳定?
我们可以从两个方面来考虑:一是通过架构优化, 二是采取常用的保障手段。
2.2.1 架构优化
要点1:鸡蛋不要放在一个篮子里
对于架构优化,一个简单的原则就是不要把所有的鸡蛋放在一个篮子里。我们可以通过以下的设计实现这一点。
(VictoriaMetrics 存储多集群设计)
滴滴主要从事打车业务,我们的网约车和非网约车业务的观测数据各自存储在不同的存储集群上,这就是我们采用的VM多集群设计。例如,如果非网约车业务实例出现问题,我们希望这不会影响到网约车业务,反之亦然。因此,我们在存储方面进行了多集群的设计。
(传输多集群设计)
在数据传输方面,我们的设计理念也是类似的,但有一点区别在于,传输和存储会用到不同的分片策略,这是因为它们的负载特性不同。例如,某个业务的传输量非常大,但存储查询的量却非常小,这种情况下,我们会在传输端对数据进行拆分,在存储端只需要保证数据的写入即可。它们可以共享同一存储集群。
要点2:及时扔掉坏鸡蛋
另外还有一个原则,我们称之为“及时扔掉坏鸡蛋”。在传输模块中,除了写入存储,还有其他的下游模块,如流式报警等。
因此,如果某个子系统因为某些原因运行变慢,从而影响了整个传输模块,这是我们不愿看到的。我们希望在子系统运行变慢或出现问题时,能够及时将其剔除出系统,即熔断策略。在某些情况下,我们可以自动进行熔断,并尝试不断恢复这个子系统。如果它成功恢复,那我们就会重新将这个系统接入。
2.2.2 其他常用保障手段
熔断、降级、多维度限流:
除了熔断和降级,我们还有其他保障手段,如多维度的限流。多维度限流采取灵活策略对请求进行限制,例如,一些持续且高频的跨度长时间的查询,比如几个月甚至几年的数据查询,我们就会应用多维度的限流手段。
慢查治理:
另一个保障手段是慢查的治理,这涉及到对大量曲线的查询。比如,一次查询涉及到了上百万的曲线,此时我们需要进行慢查发现,然后进行治理。在一些重点保障的时期,我们会开启这些策略,一旦识别到异常,就采用多维度限流,根据它的特征进行限流或者直接禁用。
多活:
内部可观测的多活,我们采用的方式是做单元化。例如,如果A机房和B机房的专线中断,我们需要保障用户可以单独访问相应机房的数据。
容量评估体系:
我们还有容量评估体系。因为在可观测架构和业务流量或订单量的增长可能不成正比,所以需要一套自身的容量评估体系。每家公司的业务模型可能不同,所以这个体系需要建立起来,对于保障手段来说,这是有帮助的。
预案、演练:
我们还会制定预案并进行演练,以保证这些手段是有效的。
三、可观测性在滴滴是怎么实现的?
3.1 策略选择
可观测性这个主题在2021或2022年是一个非常热门的话题。有人可能会觉得,如果不谈论可观测性,就相当落后了。我们先来看一下各大厂对可观测性的定义。
可观测性是可帮助团队有效调试其系统的工具或技术解决方案。可观测性基于对事先未定义的属性和模式的探索。——来源Google 可观测性是指能够通过检查系统或应用的输出、日志和性能指标来监控、测量和理解系统或应用的状态。——来源RedHat 可观测性是指您仅根据所了解的外部输出对复杂系统内部状态或条件的理解程度。——来源IBM
我在这里分别引用了Google、RedHat和IBM对可观测性的定义,他们有两个共识。第一个是,可观测性是能从外部理解系统内部的状态,而这些状态并不需要是已知的。第二个共识是,可观测性有许多手段,包括日志、指标、事件等。
那么,如何实现可观测性呢?各大厂都有自己的实现方式。Google推荐使用其云平台GCP,RedHat推荐使用OpenShift Observability,IBM有其自己的产品Instana Observability,而Grafana推荐使用LGTM(Loki、Tempo、Mimir)。
综合来看,实现可观测性的方法大概有三种。第一种是购买SaaS厂商的服务,第二种是尽可能地采集和存储详尽的可观测数据,第三种是关联多种观测数据。
3.2 方案对比
对于滴滴,第一种实现方式并不适合,因此我们优先排除。
至于第二种实现方式是“尽可能详尽”,于是我们将观测数据分为两个维度,即Dimensionality和Cardinality。Dimensionality类似于标签的概念,例如时间戳、版本、顾客ID等。Cardinality则以顾客ID为例,可能有从1万01到1万9999的数据。这种方案优点是能采集大量数据,但缺点是实现成本高、资源消耗大,且数据利用率偏低。
第三种实现方式是关联多种观测数据,常见的观测数据包括Metric、Trace、Log。Metric数据属于高层次抽象,能告诉你错误数,但无法提供具体错误信息。Trace数据主要用于跨服务关联,比如一个请求经历了哪些服务。Log数据则是开发人员偏好的信息,它提供最详细的、人类可读的数据。然而,这种关联多种观测数据的方式,其缺点是架构实现相对复杂。
3.3 架构设计
在滴滴,我们借鉴了上述两种方法,将数据分为低基数和高基数两类。低基数指的是指标数据,而高基数则是日志数据。我们将这两种数据分别存储在不同的数据库中,并建立它们的关联关系。
举个例子,如果在一段时间内我们收集到两个错误日志,我们就会将这个错误数“2”上报到时序数据库。同时,我们将对其中一条错误日志进行采样,并将其存储在Exemplar DB中。然后,我们会通过标签将时序数据库和Exemplar DB进行关联。
3.4 实践成果
滴滴的可观测性实践成果非常显著。在建立可观测性之前,我们在排查故障时需要登录到机器上并检索日志。如果有幸找到了问题所在的机器,那就算是幸运的。但如果并非问题出在这台机器,甚至不是这个服务,我们就需要重复上述的操作。而且,即使经过这样的操作,是否能找到问题也是不确定的。
然而,在建立了可观测性之后,当我们收到报警消息时,我们可以直接查看与这条报警相关联的日志原文。查阅了日志原文之后,如果认为没有大问题,可以暂时不进行处理。如果是紧急情况,我们就会启动紧急处理流程。
此外,当我们在查看图表时,如果发现某个指标突然升高,想要知道是什么原因导致的,我们可以使用下钻功能。这个功能不仅可以让我们查看日志原文,如果日志中包含Trace信息,还可以将这个Trace信息提取出来。然后可以将Trace信息下钻到专门的Trace产品进行进一步的处理。
四、总结展望
滴滴的可观测性架构的发展实际上是基于不同的需求、场景和时代背景,选择了最适宜的解决方案。
我们对接了业界一些成熟的生态系统,并将这些生态系统融入到我们的系统中,这极大地帮助我们完成了许多工作,也提升了我们的工作效率。同时,在建设可观测性平台的过程中,我们也采用了一些策略来实现观测系统自身的稳定性保障。
值得注意的是,可观测性的建设并没有一种统一的实现方式,每家公司都有其自身的特色。因此,各公司需要根据自己的特点去定制专门的解决方案,并根据实际情况不断选择和调整最合适的方案。(全文完)
Q&A
1、滴滴是否有专门的技术团队去维护可观测架构?Prometheus 的横向扩展能力相对有限。InfluxDB具体有哪些问题?
2、如何去度量一个架构的可观测性?有什么建议吗?