日前,在51CTO主办的WOT全球技术创新大会上,作业帮基础架构部资深架构师莫仁鹏带来了主题演讲《作业帮服务观测体系建设与实践》,基于多年来作业帮云原生建设的实践经验和成果,分享了作业帮团队在构建服务观测体系的过程中的创新思考。
本文将摘选其中精彩内容,统一整理,希望为诸君带来启发。
服务观测的流量挑战
众多周知,服务观测来源于近年来很流行的一个词:Observability,即可以由其外部输出推断其内部状态的程度。
具体来讲,“可观测”主要分为三个部分。首先是日志,它主要是涉及到单个离散的事件。第二是监控,它是可聚合、按时间维度变化的状态。第三是Tracing,是单次请求范围内的信息。
服务观测最大的难题就在于流量挑战。目前,作业帮的日志数据量已达到了峰值百GB/s的级别,每天的日志大小在PB级左右,整体的监控数据量在千万级/s,追踪数据量也是在千万级左右。
因此,构建一套高可用、高扩缩、低成本的一套服务观测体系,势在必行。
日志体系
在观测体系中,日志部分是大家接触最多的,也是最重要的一环,它有四个目标:
- 第一,高可用。因为许多下游业务,如业务服务的监控、业务数据的报表、大数据模型训练等都对会对日志的可用性要求很高,甚至要做到一条数据也不能丢失。
- 第二,高吞吐。特别是在作业帮内部场景中,它的高峰流量可能是低峰流量的10倍以上,这就要求我们有足够高的吞吐性能。
- 第三,低延迟。下游的部分日志消费服务他们对延迟要求也很敏感。
- 第四,低成本。日志的数量巨大,想要长期保存它们,就需要用足够低成本的保存方式。
下图是作业帮整个日志体系的架构。
第一,日志输出的部分。跑在K8s中的服务,都是一个个容器,容器首先会把日志输入到容器的日志文件中,然后在每个机器和节点上会部署一个日志采集的服务,由它来负责采集每个容器的日志。采集完成后,全部的日志将上传到Kafka,并通过Kafka完成日志的传输。
接下来是日志的处理和消费,主要涉及到:日志的存储和检索,日志监控、日志追踪和大数据部分。
最后,在存储部分,作业帮最终是采用了对象存储的方式。
接下来,重点分析下日志输出、采集、处理、消费、存储的构建过程。
日志输出方面,作业帮所有服务都采取了标准输出的模式来打印日志。对于老服务,引入了日志边车模式来帮助完成标准输出的改造。主要流程是通过一个管道文件:服务容器将日志写入管道文件,然后日志边车从管道文件中读取并打印到标准输出中。标准输出的好处包括统一日志输出方式、服务不需要管理日志的生命周期、采集友好、以及知晓日志的输出时间。
日志采集方面,作业帮最初使用Filebeat,但遇到了一些问题,如可用性、稳定性和性能方面的问题。因此,采用了自研的方式进行优化,包括优化json解析逻辑、降低获取容器元数据的开销、更轻量的采集逻辑以及更好的采集隔离。最终的性能表现是支持单核75B/S的采集速率,提升了三倍的单机采集性能,并且采集整体使用的CPU下降了70%。
传输方面,作业帮重新设计了消息分装格式,按照依赖Kafka的Header模式,将各种日志的元数据放到Kafka消息的Header中,然后在body中保存日志原文。这样下游可以不用关心日志的分装格式,从而省去了序列化与反序列化的开销。在Header中保存各种数据信息,也可以帮助各个日志下游提取需要的日志信息。
日志检索方面,莫仁鹏重点介绍了ELK这套方案在日志检索中经常遇到的几个问题,如要求结构化数据、写入性能低、运行成本高和存储成本高等。然后重新审视了日志检索的场景特点,包括非结构化数据、写多读少的场景、查询时延不敏感以及数据规模比较大。
这里值得注意的是,日志检索的理想方案应具备以下特点:
- 不需要对日志建立索引,也不需要对各种日志做反序列化,去做结构化的存储。查询操作是全文检索,但通过分布式和并行化处理,检索速度可以满足需求。
- 日志按块存储,每个日志块是一类日志的聚合,例如某个pod在某段时间内的日志都会写入一个日志块中。
- 通过并行检索的方式,按照服务名称、时间、机器名称等标签建立索引,可以通过选择查询条件,一级索引快速呈现所需的各类日志块。
基于此,作业帮提出了一套可以实际落地的新方案,旨在更好地满足微服务场景下的日志检索需求。新方案包括以下组件:
- Ingester组件:负责写入各个日志块,并将索引信息写入数据库。
- 查询组件(query-proxy组件):负责接收用户查询命令,分发给各个不同的查询组件,并返回最终结果给用户。
- query组件:在本地存储中完成各个检索命令,返回结果给proxy组件。
- manager组件:负责管理机器上的日志块生命周期,包括上传本地存储的日志块到对象存储、从对象存储下载日志块到本地存储、淘汰过期的日志块等任务。
日志存储方面,作业帮采用了分级存储的方式。首先,日志块被写入到本地存储中。一旦超过一定时间,这些chunk会被按照策略上传到对象存储中,并以压缩的方式进行保存。对象存储主要用于长期存储,超过一定时间的日志块会被统一归档到归档存储中。
为了提高检索效率,涉及到日志沉降的概念。如果只需要检索近几个小时的日志,可以直接在本地存储中完成;如果需要查询更早时间的日志,则需要从对象存储甚至归档存储中取回归档日志进行检索。
在成本方面,本地存储的成本最高,而对象存储的成本大约是本地存储的1/3,归档存储的成本是本地存储的1/10。此外,对象存储和归档存储中的重要日志都是以zstd压缩的方式保存,进一步降低了存储成本。
总体来说,该日志检索方案具有以下优势和表现:
- 方便接入:对日志格式没有任何要求,所有云原生服务都可以默认接入。支持shell查询命令,客户无需额外学习成本。
- 高吞吐:由于不对日志进行反序列化或全量索引,单核可以支持50M/s的日志写入。
- 成本低:采用分级存储方式,通过zstd压缩保存日志,相比传统存储方式可以降低一到两个数量级的成本。
- 速度快:采用分布式并行检索,支持横向扩容。可以在30秒内完成1TB大小日志的检索。
监控体系
这里,我们把监控主要分为三个部分。
第一是集群监控。主要包括:一是系统监控,系统监控主要是K8S事件、K8S组件、APIServer和Etcd的各种指标;二是资源监控,主要是涉及到pod和Node的各种资源使用;三是网络监控,主要涉及到节点和各种网络延迟;四是基础组件监控,会涉及到各种注册发现、服务观测组件;五是中间件监控。
第二是服务监控。主要涉及到对服务pod上各种指标进行监控,主要分为两部分。第一是Runtime监控,主要会涉及到各种运行时和各种语言的指标。第二是自定义监控,我们可以支持业务暴露Metrics接口,以供Prometheus采集,实现业务的自定义指标。
第三是流量监控。由于业务流量上会对时延敏感,不便直接做埋点,作业帮是统一通过消费日志的方式来完成流量方向的监控,主要涉及到指标包括:其一是入流量,即请求QPS、请求时延、成功率、错误码数量等指标;其二是出流量,主要是观测服务对于数据库、缓存、依赖服务等流量的整体监控指标;其三是网关监控,涉及到QPS时延和请求码数量等相关监控;其四是异步流量的,也就是消息队列,主要涉及到生产/消费QPS、生产/消费耗时、消费延迟的一些指标。
通过上面这些指标,可以观测到平台侧以及资源层、服务层的各种监控指标,进而了解所有服务的运行状态。
在监控采集方面,Prometheus当前是云原生监控领域的一个事实标准,它有着高效的时序数据存储,单次可以处理大量的数据。这里也有一个问题,它是单机部署的,不支持集群化部署,遇到数据持久化甚至是大数据量的监控数据持久化上,可能就无法满足我们的要求。我们通过引入时序数据库方式来解决这个问题。
Prometheus会把监控数据通过Remote Write的方式写入到时序数据库中。接下来所有的对于监控数据的读取操作都是在时序数据库中进行的,主要包括在Grafana上的各种数据展示、告警以及对于监控数据的一些API的访问。
监控存储方面,我们选择用Prometheus,并用Metrics作为时序数据库存储,主要是基于以下几个点考虑。第一,它兼容Prometheus,它可以支持Prometheus的各种查询语句,下游也可以把它当做一个Prometheus来使用。第二,它的架构比较简单,方便做各种横向的扩展。第三,它的数据压缩率比较高,平均每个点位会占用32B左右。第四,它支持高吞吐,它单核可以支持6W/S的监控数据的写入。
在使用VM的过程中需要注意数据容量的问题。因为监控数据的保存时间要求比较高,整个存储就成了一个瓶颈。这可以通过降准的方式来解决。
事实上,监控场景也是有明显冷热之分,研发人员最常访问的是一个月内的监控数据,在一个月外的监控数据就较少访问了。基于这个思路,就可以把监控数据做一个标准和降准的区分。降准即原来10秒打一个监控数据的点,会被改造成为100秒打一个点。
具体实现方式上,首先通过Prometheus来完成写入,在写入前使用一个proxy来完成它的代理,同时在proxy上就可以会做一个降准的操作,按比例把所有的写入点做一个抽样;然后分别把抽样前的数据和抽样后的数据写入到不同的VM存储中;最后Select组件会按照时间把不同的请求做一个拆分,打到不同的VM集群中,最后做一个聚合,返回给前端用户。通过这种方式,就可以把我们一个月前的数据做一个标准存储,一个月后的数据做降准存储。
值得一提的是,落地了这种方式之后,我们在整体的存储成本下降了80%左右。
追踪体系
随着微服务的兴起,单个请求可能需要跨越多个系统和服务,一旦某个服务或网络出现延迟,可能导致整个请求出现问题。因此,需要一种能够观察和追踪整个请求链路执行情况的方式,这就是追踪体系的作用。
追踪体系主要分为采集、处理和分析三个部分。
其中,采集部分通过边车、服务和日志三种方式获取追踪数据,其中边车和服务数据通过Agent和collocter上报到Kafka,日志数据则直接投递到Kafka;处理部分使用Clickhouse作为存储,并引入Ingester和Query组件以支持写入和查询操作;分析部分则包括服务拓扑分析和请求链路分析,通过统计聚合追踪数据来形成可聚合的指标,并使用Prometheus进行时序数据的采集和存储。
关于采集方式,具体分为两种:
边车上报:在边车上进行埋点,将各种服务RPC流量进行打点,通过Agent的方式完成上报。这种方式只能观测到服务边界上的流量,对服务内部的流量无法深入。为了解决这个问题,引入了服务上报的方式。
服务上报:服务可以通过接入SDK的方式完成上报。对于部分流量或服务不好接入的情况,可以通过日志方式完成上报,例如各种访问DB流量、访问缓存流量以及异步流量(如消息队列、生产和消费)。
在数据存储方面,最初使用ElasticSearch,但遇到了写入性能不足和数据压缩率不理想等问题,导致整体集群成本很高。后来切换到Clickhouse后,存储性能和效率大幅提升,单核可以支持1.5W/s的磁盘写入,整体存储集群的写入性能提升了40%,CPU的使用下降了80%,磁盘占用下降了50%左右。这使得整体的存储成本可以下降80%左右,是之前的1/5。
最后是关于服务的追踪分析:通过追踪服务的请求,可以获得服务间的依赖关系,构建出一张服务拓扑图。从服务的维度对这些追踪数据做统计分析,可以支持实时服务拓扑图查看以及请求链路分析。请求链路分析可以展示核心链路服务的调用次数、依赖的新增情况以及请求链路中是否存在闭环。这些功能可以帮助研发人员更好地了解服务的运行状态和潜在问题。
总结来讲,追踪体系通过采集、处理和分析三个主要部分来解决微服务中的问题定位和瓶颈发现等挑战。通过引入高效的存储和数据分析方式,以及提供丰富的服务追踪分析功能,可以帮助研发人员更好地了解服务的运行状态和性能表现,及时发现和解决问题。
Serverless可观测的前瞻思考
在Serverless服务观测的实践方面,作业帮面临着高扩缩的问题,高峰时期的流量是低谷时期的十倍以上,为了降低运行成本,决定将服务统一套入到Serverless上运行。然而,在Serverless上直接部署观测组件并不容易。云厂商为此提供了观测解决方案,但仍存在一些问题。
首先,日志方面,我们一般会通过云厂商的日志采集器统一完成采集,投递到Kafka。不同云厂商提供的采集器功能不一致,而且功能不够全面,对于投递方式和数据的分装方式可自定义程度不够多,不太能够满足采集需求。
对于监控方面,需要重点关注Serverless pod的资源指标,如CPU和内存的使用。这些资源都会暴露在虚拟节点上,可以通过Prometheus来采集这些Metrics接口。
最后,追踪方面,当前云厂商不提供采集的支持,需要对服务和架构去做自身适配。
为了解决这些问题,作业帮团队采用了容器注入的方式,统一解决Serverless上的服务观测问题。通过声明DS编排,云厂商将各个DS编排注入到容器中,例如日志组件、日志观测组件、追踪采集组件等,注入到Serverless运行的pod中。这种方式类似于边车的方式,但无需在编排中做额外声明,服务编排无感。此外,作业帮团队还通过注入自己的采集组件,并针对自主架构在组件层面为多家云厂商做适配,同时支持自定义的采集策略,通过agent完成采集,在底层实现了采集链路的统一。
最后,有了全景的观测数据之后,还需要建立起一套服务评价体系,通过统一的标准评价服务质量的好坏。
利用观测数据建立了服务评价体系,通过统一的标准评价服务质量的好坏,从而帮助
研发人员发现和管理那些微服务中“无法察觉的角落”。
作业帮团队目前主要依据服务观测的指标建立服务质量指标,包括:
- 服务接口质量,需要关注接口成功率、接口响应时延。
- 服务依赖情况,主要有资源请求成功率,是否有跨云的依赖、循环依赖。
- 服务资源使用,主要关注CPU和内存的使用率,如果已经接近或达到瓶颈的情况,就需要做及时的扩容和优化。
- 服务错误数,包括panic次数、panic/error日志数量以及各种5XX状态码的数量。
基于这些指标,就可以制作一个服务评价报表,定期发送给研发人员,帮助他们了解自己服务的运行状态和质量。进而,他们就可以针对问题进行优化,提高服务的稳定性和性能。
观测手段不是独立的
最后,这里拿这张图做一个总结。
需要主义的是,日志、监控、追踪是我们观测服务的三种手段,但它们不是完全独立的,而是有交叉的。它们的数据是有不同的组织形式,而且观测的层面也是各不相同的。当前,我们只有结合好这三者,才能更好、更全面、更深入地去观测我们的服务。