当前位置: 首页 > 科技观察

应用监控系统演进:从选型到实现,链路跟踪一气呵成

时间:2023-03-16 17:36:26 科技观察

1.简介随着分布式系统和微服务的不断发展,系统开发和运维对可观察性的需求越来越多更紧迫。术语可观察性[1]的起源最初是从控制理论中借用的。当我们谈论可观察性时,通常会提到以下三个方面:LinkTracingIndicatorMetricsLogLogging这三个并不是完全独立的概念,而是相辅相成的。说到这三个方面,我们总是不得不提到PeterBourgon的文章[2],以及其中最经典的维恩图:2.CollectMoney监控系统的历史发展。CollectMoney,2017年开始逐步搭建应用监控系统,系统建设的主要方向是提供链路追踪(Tracing)和性能监控(Metrics)的能力。在监控系统的选择上,我们尽量使用开源系统:Tracing,我们选择Zipkin[3],它是Twitter开源的,为我们提供链接跟踪的后端系统,使用Elasticsearch作为后端存储用于追踪。Metrics在Tracing数据的基础上,我们通过消费Kafka的Zipkin格式的数据聚合获得分钟级的指标。时序数据简单的使用MySQL作为后端存储。在接入层,我们用最原始的方式提供了各种检测工具包,供各种Java模块和组件嵌入。业务研发同学以pom依赖的形式引用自己的业务服务,如:通过MySQLDriver提供的拦截器机制[4]采样MySQL数据库请求;通过封装新的JSON-RPC包实现RPC层的埋点;通过Spring的HandlerInterceptor[5]接口拦截实现Rest风格;通过SpringAOP访问Redis的链接集合;...该系统支持我们度过了业务发展最快的时期,为大量的故障排除和故障诊断提供了一些线索。但是,业务开发逐渐开始对这个系统不满意,主要有以下几个方面,1)由于我们前期使用MySQL作为底层时序数据的存储,这在当时似乎是一个主流方案时间[6],但我们遇到了一个很大的性能问题。毕竟,MySQL等数据库提供的存储引擎并没有针对这种场景进行优化[7]。同时,MySQL也没有为时间序列提供丰富的查询操作。PgSQL9.6.2数据插入吞吐量作为表大小的函数[8]。在链路跟踪或应用程序监控的场景中,我们需要的是高吞吐量和线性性能[9]。同时,我们还需要增加数据生命周期管理的功能:因为随着新数据的写入,历史数据会随着时间的推移而贬值。2)由于我们需要将Tracing数据中的数据倒过来得到Metrics数据,所以我们“神奇地改变”了Zipkin传输部分的逻辑,在客户端聚合了所有未采样数据(Unsampled)并批量上报,这就导致我们的Zipkin升级出现了很大的困难。特别是在https://github.com/openzipkin/zipkin/pull/1968,以后将不再允许用户自定义开发服务器。3)业务方的升级依赖需要采集器组件的升级支持,导致额外的工作量。同时,也有大量的组件难以通过这种侵入的方式支撑,或者需要大量的人力成本进行研发和适配。3.新一代应用监控系统——Hera基于以上原因,我们决定开发一个新的系统,同时满足几个条件:存储成本低:可以低成本存储长期数据,并且可以存储至少四个星期的指标。对于一周的链接存储,这让我们排除了ElasticSearch的选择;实时查询性能高,灵活性高:不再使用MySQL等关系型数据库作为时序存储,使用Prometheus或Prometheus兼容的存储系统;优化研发效率:利用字节码编织技术埋点无侵入,与DevOps流程结合更紧密。1.链接跟踪分布式链接跟踪的概念和心智模型主要受谷歌2010年发表的Dapper论文[10]的影响。在Dapper论文中,作者明确指出了Trace的树形结构:我们倾向于将Dappertrace视为嵌套RPC的树。并提出了所谓Span的概念:在Dappertracetree中,树节点是基本的工作单元,我们称之为span。边缘表示跨度与其父跨度之间的偶然关系。在Dapper链接树中,每个跨度之间存在因果关系和时间关系。在链接跟踪系统选择方面,我们对比了当时活跃的几个开源项目:ZipkinApache/Skywalkingv6.6.0Jaegerv1.16Jaeger是Uber[11]2016年开源的链接跟踪平台,捐赠致CNCF云原生基金会。Jaeger的主要组件及控制流、数据流图,其中使用Kafka作为缓冲管道。Jaeger得到了开源社区的广泛支持,例如:Istio[12]原生支持使用Jaeger来增强ServiceMesh服务网格的可观察性;服务网格Envoy[13]的数据平面实现支持使用Jaeger作为链接跟踪服务提供商;...1)链接跟踪后端系统和存储的选择我们关注它们对存储系统的支持和扩展能力。①各种开源链接追踪实现的存储能力Jaeger社区具有优秀的存储扩展性,提供基于gRPC的插件机制[14]方便自定义扩展。+--------------------------------++--------------------------+|||||+------------+|unix套接字|+------------+||||||||||jaeger组件|grpc-client+--------------------->grpc-server|插件实现||||||||||+------------+||+------------+||||+------------------------------------++---------------------------+parentprocesschildsub-process关于存储的具体选择,我们注意到阿里云SLS可以支持linktracking的后端,并且官方提供了一个实现https://github.com/aliyun/aliyun-log-jaeger,我们内部基于这个思路实现了gRPC插件版SLS后端实现目前在生产环境稳定运行存储周期:SLS可提供长达30天的存储周期。存储容量:一天存储的Spans数量超过4亿,使用存储空间约6TB。性能:SLSQuery接口条件查询,3-5s返回结果。成本:日成本约70元,年约2万元(约两台8U32GECS年付价)。Jaegeroperator在https://github.com/jaegertracing/jaeger-operator/pull/1517引入了对gRPC插件的原生支持,gRPC插件可以作为InitContainer[15]将插件二进制文件复制到共享的EmptyDir存储卷。同时我们也积极回馈社区,为社区提供gRPC插件的自观察功能(SelfObservability):aeger-grpc插件支持opentracing上下文传递:https://github.com/jaegertracing/jaeger/pull/2870go-plugin插件支持参数配置:https://github.com/hashicorp/go-plugin/pull/1682)业务端接入优化SkyWalking的美妙之处不仅在于不仅在于其强大的功能,还在于其出色的代码实现[16]。过去,我们使用侵入式方法来提供应用程序监控访问。监控服务商需要为各业务方提供插件和模块,版本兼容需要付出很大的努力。这种方式缺乏统一的方面和工作机制,需要将各个组成部分一个一个“打散”。Skywalking是华为吴胜等人于2015年开源的一款APM产品,现已成为Apache的顶级项目。Skywalking-Java利用字节码增强技术,提供非侵入式链接埋点,大大降低了成本。在Java中,常用的字节码工具有以下几种。ASM和BCEL属于LowLevel,而CGLib、Javassist和ByteBuddy比较好用。关于字节码技术的具体分析可以参考StackOverflow上的回答[17]。其中,ByteBuddy的易用性和性能均达到一流水平:ByteBuddy官方提供的性能测试结果。为了充分利用Skywalking-Java提供的插件,我们在OpenTracing接口上实现了一整套Skywalking链路追踪模型。具体来说,Skywalking的链接跟踪语义包括三层:①Skywalking中的Trace类似于OpenTracing语义中的Trace②Skywalking中的Span类似于OpenTracing语义中的SpanEntrySpan:相当于OpenTracingSpan中的Kind=Consumer或Server;ExitSpan:相当于OpenTracing中Kind=Producer或Client的Span;LocalSpan:不属于以上两种的其他类型。③Skywalking增加了一层Segment的概念。一个Segment被约束在一个线程上,它包含的所有AbstractTracingSpan都在这个线程上创建和销毁。这里SegmentID对应OT中的SpanID,Skywalking中的Spans按照创建顺序从0开始编号。当然,模型也有区别:跨线程OpenTracing的标准要求实现者将Span设计成线程安全的,因为Span是允许跨线程传递的。在Skywalking中,跨线程是通过对当前段[18]进行快照来实现的,而Span在大多数场景下不需要保证线程安全。AsynchronousSpan主要用于记录异步操作真正开始和结束的时刻。以SpringReactive为例[19]:用户编写的Controller返回一个可执行的任务(通常是Mono类型)而不是最终结果,Dispatcher会通过线程池执行任务,所以我们需要记录的内容实际上,这个请求就是任务创建到被“计算”完成的整个循环。这部分的实现在OpenTracing标准中没有提及。这种机制在Skywalking的多个插件中都有使用,比如Redis客户端Lettuce、SpringWebflux、ApacheAsyncHttpClient等。我们通过在OpenTracing接口上实现与Skywalking相同的语义,实现几乎零成本的移植和使用其所有插件.我们在使用Skywalking-Java的过程中也发现了很多问题,并积极向社区反馈,做出了一些贡献,主要包括:JSON日志格式的实现:https://github.com/apache/skywalking/pull/5357SpringKafka1.x插件:https://github.com/apache/skywalking/pull/5879SpringDevTools支持和多类加载器优化:https://github.com/apache/skywalking/pull/6973JedisTransaction支持:https://github.com/apache/skywalking-java/pull/573)服务依赖分析服务依赖分析一直是公司内部业务发展急需的功能。对服务能力规划、问题诊断、服务强度等具有重要作用,在性依赖判断中具有实用价值。在Jeager社区的实现中,推荐在生产中使用Spark批处理[20]来实现全局依赖分析。还有基于Flink的实时处理[21],但已经不在维护状态了。为了实现这个功能,我们使用ApacheFlink,通过消费Kafka中的链接数据,实时计算服务之间的依赖关系,转换成Tuple格式数据通过OpenTSDB协议传输到我们的时间序列数据库VictoriaMetrics。前端根据用户提供的时间窗口,通过Java服务暴露的API进行上下游查询:未来我们将进一步优化用户交互和调用量的分析和展示。2.指标监控在老版本的监控程序中,我们使用关系型数据库作为时序数据的存储系统,这让我们在查询灵活性和性能上遇到了很大的瓶颈。我们有必要在设计新系统的时候进行一些反思。这几年,云原生的概念逐渐深入人心,而Prometheus是云原生时代监控的事实标准。经过研究,我们认为单机版的Prometheus无法支持百万以上的活跃指标和一周以上的数据存储。我们的重点主要放在Thanos、Cortex和VictoriaMetrics上。Cortex和Thanos在国内技术界被广泛共享。但是我们发现Cortex的结构非常复杂,对系统运维提出了新的挑战,Thanos也有一定的局限性。运维复杂,由于使用对象存储(S3等)作为数据冷存储,可能会导致部分服务无法查询,部分数据返回。同时,我们还发现国内知乎在QCon2020上分享了他们使用VictoriaMetrics的经验[22]。我们最终选择VictoriaMetrics的原因如下。VictoriaMetrics在各种性能测试中表现良好[23];作者AliaksandrValialkin精通Go语言的性能优化,是fasthttp等高性能Go语言组件的作者;最重要的是VM集群架构简单易运维。VM的每个组件都是独立的,可以水平扩展。只有核心vmstorage是有状态的,其他组件都是无状态的。1)推还是拉(pushorpull)对于指标数据,采用主动推还是被动拉模式一直是一个很大的争议[24]。我们使用像Prometheus一样的推送模式。出于以下原因,服务发现对Pull模式持批评态度的一个原因是Pull模式需要大规模的服务发现,但这个问题在Kubernetes上是不存在的。我们使用CRD[25]服务抓取目标的定义可以很容易地实现。同时,Pod和Services上的标签可以附加指标,帮助查询时区分服务所属的实例和业务团队。相反,这在Push模式下是不容易做到的,或者需要业务研发去改造。故障排除当索引查询失败时,我们通常需要判断是哪一步出错了。在Push模式下,我们需要查看业务的代码和日志来判断问题。而在Pull模式下,我们可以在浏览器中手动请求指标暴露的接口(如/metrics)来判断服务的健康状态以及业务是否正常导出指标。目前我们使用VictoriaMetrics的一些统计数据:存储周期为60天,共计6990亿个数据点,占用800GB磁盘空间;活跃时间序列约500万,数据点插入QPS约13万/秒;P99行范围查询平均值约为1.5s。我们还开发了一个查询面板来限制查询时间范围(最多3天)和查询方式(用于服务作业查询)。我们开发了一些重要的指标插件,其中在应用性能分析和故障定位中比较实用的维度有:Servlet容器指标:Tomcat繁忙线程数,百分比;数据库连接池指标:支持HikariCP和Alibaba/Druid连接池,连接池等待线程数,数据库连接获取时间,数据库连接池使用率;缓存指标:支持Redis、Caffeine和EhCache。缓存命中率;Kubernetes监控:PodCPU、内存使用情况,我们也在系统中集成了Kubernetes事件的查看和搜索。由于公司的一些核心服务也使用Docker部署在ECS上,我们在VictoriaMetrics[26]中实现了基于DockerdAPI的服务发现机制,也已经合并到社区版本中。3、全面拥抱云原生2020年,Kubernetes已经成为分布式操作系统的事实标准,公司内部的大部分业务已经全面迁移至自建Kubernetes集群。为了更好的使用新特性,我们在2020年年中启动了Kubernetes集群升级计划,将集群升级到1.16版本(目前升级到1.20),并迁移到阿里云的ACK托管集群。监控系统的实现将完全依赖Kubernetes系统。1)我们提供了Docker镜像版本的JavaAgent,方便业务开发和接入。2)在生产环境中,我们在容器启动阶段使用InitContainer[27]注入JavaAgent,通过贡献的EmptyDir[28]在两者之间传递AgentJar包。这方便我们在生产环境静默升级Agent版本:即使生产环境中的Agent出现问题,我们也可以快速修复问题,然后升级初始化容器。3)时序数据库VictoriaMetrics和Jaeger组件的运维也是通过KubernetesOperator实现的:我们为jaeger-ingester和jaeger-collector组件启用了HPA,即根据CPU和内存的使用水平动态扩展。VictoriaMetrics集群版本的各个组件也通过Kubernetesoperator[29]进行维护。4.展望1.后采样的实现由于我们目前采样的是Head-BasedSampling方案,一旦链接中间的服务抛出异常,链接没有被采样,就会有一些Error日志和告警,但是链路跟踪系统无法查询到该链路的状态,给开发和排错带来了很大的障碍。目前业界有几种典型的实现方案,1)OpenTelemetry方案https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/4958OT社区的tailsampling方案[30]主要来自从Grafana[31]的贡献来看,同时可以使用以下处理器和导出器来实现高可扩展性。(第一层)loadbalancingexporter:将属于同一个TraceID的所有Traces和Logs分发到一组固定的下游Collector;groupbytraceprocessor:等待足够长的时间,将属于一个TraceID的所有Span(s)打包下发给下游;tailsamplingprocessor:通过预定义的组合策略进行采样。2)如果链路未被采样,具有字节抖动方案错误的服务将强制翻转采样决策。但在这种情况下,采样决策改变前的所有链路和其他分支链路的数据都会丢失。3)货拉拉方案基于Kafka延迟消费+布隆过滤器实现:实时消费队列:根据采样规则编写布隆过滤器,将所有热点数据写入热存储。延迟消费队列:基于布隆过滤器实现条件过滤逻辑,将冷数据写入冷存储。2.时间序列的异常检测时间序列的异常检测一直是比较热门的话题,尤其是对于具有时间周期特征的数据。1)Gitlab解决方案Gitlab在2019年分享了他们基于Prometheus的简单异常检测[32]。比如我们要判断当前时间t对应的值f(t),可以用前三周数据的中位数,通过最近一周的增量进行修正,得到预测值f'(t)在当前时间。增量Δoffset指的是指数最近一周的时间平均值与向前抵消后的时间平均值。例如,Δ1w指的是最近一周的平均值与上一个周期的平均值的差值(在PromQL中表示为job:http_requests:rate5m:avg_over_time_1w-job:http_requests:rate5m:avg_over_time_1woffset1w),用于补偿周期之间的平均值变化。2)其他解决方案Prophet-携程实时智能异常检测平台实践[33]外卖订单量预测异常报警模型实践[34]从行业发展的大趋势来看,通过检测系统异常也是大势所趋大数据和人工智能手段。