摘要:在服务观测的三架马车(日志、监控、追踪)中,日志无疑是最直接也是最重要的手段,透过日志我们才可以观测到服务的具体操作,依赖日志来定位系统问题。
背 景
在服务观测的三架马车(日志、监控、追踪)中,日志无疑是最直接也是最重要的手段,透过日志我们才可以观测到服务的具体操作,依赖日志来定位系统问题。
作业帮的日志体系是最早开始建设的,初期大家是在几台机器上查看服务日志,之后逐渐演变成集自动化采集,日志流传输,秒级检索和自动化归档能力于一体的日志平台。
在作业帮日志体系的建设过程中,我们认为一套优秀的日志系统应当具有以下特性:
高吞吐:在观测数据中日志数据量是最为庞大的,一个全面的日志系统是能满足所有服务日志的采集和存储。
低延迟:日志从采集到可视需要有一个比较低的时延,从而可以良好地支持日志下游的各种消费需求。
低成本:庞大的日志量会带来庞大的存储压力,需要具有一个成本低廉的存储方式。
可追溯:日志数据有很强的追溯性,日志系统需要能够长时间地保存日志。
服务透明:日志系统要对服务保持透明,服务可以自动完成接入,没有额外的成本开销。
作业帮日志体系现状
目前作业帮有着数千个线上服务,所有服务的日志高峰流量可以达到 5000W+ 条 /S,每天生成的日志大小在 PB 级。
随着作业帮业务的逐渐发展,流量的逐渐上升,日志规模也随之一直处于上升的趋势。如此大的日志量对日志体系的每一个环节和组件都提出了很大的挑战,我们需要保证庞大的日志能在系统中顺畅流转,才能提供快速、可靠、便捷的观测体验。
在构造一套高吞吐、高性能、低成本的日志系统的过程中,主要会遇到以下几个挑战:
高性能且低延迟的日志采集能力
高吞吐且高可用的日志传输能力
快速且用户友好的日志检索能力
可回溯且低成本的日志存储能力
作业帮初期使用了各种开源组件来构建日志体系,但随着规模的逐渐扩大,遇到了很多性能、可用性、适配度上的问题,最终演变成基于自研采集器、自定义传输方案、自研检索服务、自研存储方案的一体化日志平台。
作业帮日志系统建设实践
作业帮的日志体系整体架构如下图所示,整个体系都是基于 K8S 和容器进行构建。日志会先从成千上万个容器中采集出来,通过采集器进行数据封装并上送到 Kafka,在 Kafka 下游则对接着各类日志消费服务,包括日志存储、监控、追踪和大数据等服务。其中日志存储服务负责将日志数据分割压缩后存储到对象存储服务中,然后基于对象存储上的日志数据提供日志检索能力。
整个日志流转过程都是标准的日志体系实现,接下来我们会深入每一个步骤,详细介绍作业帮在上面做的工作:
日志接入
作业帮的服务主要运行于 K8S 上,容器服务的日志我们会统一要求走标准输出的方式打印,最后经由 docker 落地到文件中。
这样的方式会有以下几个优势
自动管理的日志生命周期:服务不用再关心和维护自己日志的生命周期了,只用输出即可;而对机器节点来说也避免了服务日志一直积累而导致的磁盘空间风险。
统一的采集方式:对采集器来说,不用对每个服务不同的日志配置做兼容,可以按统一的方式做采集。
明确的日志时间:不同服务的日志格式各异,难以从原文上解析每条日志的具体输出时间。而通过 Docker 输出的日志上会有统一的时间标识,通过这个信息可以获取到这条日志的输出时间。
日志采集
在每个节点上,我们会以 Daemonset 的方式部署日志采集器,来采集所有容器的日志数据。最开始我们使用的是开源的采集器组件,不过随着流量的上涨,发现开源采集器在性能和稳定性上都难以满足需求。为此我们自研了日志采集器 log-agent,log-agent 相比开源采集器有着更强的性能,更适配容器场景,更好的自定义能力可以支持作业帮的内部场景。
性能提升
在性能上,log-agent 主要做了以下几点优化用以提升采集性能
优化 json 解析逻辑, docker 记录的日志默认是 json 格式并且 key 都是固定的, 我们通过字节处理的方式实现了日志的对反序列化
优化容器的生命周期监听, 基于节点 kubectl 实现 pod 生命周期监听和元数据获取,避免了连接 apiserver 和频繁获取的开销
保证每个采集协程资源隔离, 避免竞争, 从而不会出现一个服务日志量太大影响到其他服务的日志上送
如此, log-agent 单核可以支持 100MB/S 的采集速率, 上线后单机的采集性能提升 3 倍, 采集的 CPU 使用降低了 70%
Serverless 支持
作业帮内部有许多服务运行在 serverless 上,这些容器无法通过 Daemonset 的方式采集,而云厂商提供的 serverless 日志采集存在性能瓶颈,自定义能力孱弱,且标准各不统一,难以优雅地支持 serverless 的日志采集。
为此我们在 log-agent 做了容器注入的支持,将 log-agent 容器以边车的方式自动注入到 serverless 的 pod 中,并对容器生命周期监听增加了 serverless 适配,从而在底层做到了基础设施的统一,在上层做到了采集方式对服务透明。
日志传输
日志传输这块我们使用的是 Kafka, Kafka 是大规模日志日志传输的标准组件。
在 Topic 规划上我们采用的是一个服务一个 Topic 的策略,方便管理,并可以做到服务间的传输隔离。
在日志传输格式的设计上,我们我采用的是 Header+Body 的格式,其中 Header 信息中存放的日志的元信息,包括日志输出时间、集群名称、节点名称、服务名称、Pod 名称信息,Body 信息中则存放日志原文。
这样的方式相比于 json 的封装格式,会有以下 2 个优点
所见即所得:消费下游无需关心日志的封装格式,可以按照日志原文的内容对消息进行处理
节省了序列化和反序列化的开销:生产方无需再做 json_encode, 消费方无需再做 json_decode
服务端协商的投递策略
作业帮里 Pod 的日志要求是顺序传输的,来满足下游日志监控、统计的需求,这就要求我们在日志写入队列时按 Pod 名称 Hash 来投递到对应的 Partition 中。服务 Pod 数量一般不多,这种方式无法保证日志流量的均匀,某些 partition 的流量可能会出现比其他 partition 高 2~3 倍的情况, 服务 Pod 数量一般不多,如此就可能导致下游的消费出现延迟的情况。
为此,我们在 log-agent 上实现了基于服务端协商的投递策略,每个集群上会部署一个 kafka-balancer 组件负责记录服务 pod 与 partition 的映射关系,而在新 pod 创建时 log-agent 会与 kafka-balancer 组件协商获取分配的 partition 并存储在本地注册文件中。通过这样的方式,解决了 Kafka 客户端 Hash 投递策略可能导致的流量分配偏差问题,保证了日志流量在传输上的相对均匀。
日志检索
ELK
在日志检索上,最常见的方案就是 ELK,核心的 ElasticSearch 做为了日志数据的存储和搜索引擎。不过把 ES 用在日志检索场景, 特别是大数据量的场景上, 它会有几个问题在
对非格式化日志不友好
ES 要求日志数据是格式化的, 不符合格式要求的日志需要做一次数据清洗才能适配, 增加了额外的接入成本和运维成本
写入性能不理想
日志在写入前需要额外做一次反序列化,带来了额外的性能开销
ES 需要为全量日志编制索引, 而这个全文索引的成本是十分高昂的,索引的量越大对写入性能影响越大
运行成本高
ElasticSearch 需要定期维护索引、数据分片以及检索缓存,这会额外耗用大量的 CPU 和内存
数据的存储成本高
ES 的数据索引会带来额外的数据膨胀
日志检索特点
这时候我们回过头来再看日志检索的场景:
首先看看日志,日志天生是一个非结构化的数据, 不同语言不同服务的日志格式是难以保证一致的。如果一个日志检索服务对入库的日志格式有要求,这就会给服务带来感知和额外的接入成本,也就无法做到对服务透明。
接下来再看日志检索,日志检索是一个写多读少的场景,我们需要将所有服务日志保存下来以供追溯,但所有日志中只有极少部分会被用户查询和查看。
最后看日志检索的用户,日志检索的服务对象是研发,他们对写入时延敏感,而对查询时延不敏感。在查看服务状态和排查问题时,他们希望尽快看到日志,我们要将入库的耗时控制到分钟内。而在查询时,用户可以接受一个相对不那么快的查询速度,查询耗时同样保证在分钟内足够满足要求。
总的来看,在日志场景下维护一个庞大且成本高昂的全量索引是没有任何必要的,ELK 在小规模的日志量可能工作得很好,但不合适使用在大规模日志量的场景下。
日志检索设计
基于以上问题,作业帮内部对日志检索做了重新设计,主要核心思想有以下几点:
日志分块存储
在日志写入时,检索系统不对日志原文进行反序列化和索引,而是通过日志时间、日志所属实例、日志类型、日志级别等日志元数据信息对日志进行分块写入。如此检索系统可以做到没有日志格式要求,并且因为没有解析和索引(这块开销很大)的步骤, 写入速度也能够达到极致(只取决于磁盘的 IO 速度)。
总的来说, 检索系统会将一个容器产生的同一类日志按时间顺序写入到一个文件中, 并按时间维度拆分, 保证一个块文件的大小在 100MB 以内,方便后续的传输和检索。
元数据索引
在搞定了数据的存储方式后,后面就是怎么给这些日志块建立索引,以便快速查找。
检索系统在新日志块创建时, 会基于日志块的元数据信息搭建索引, 比如将服务名称、日志时间范围、Pod 名称、日志类型等信息做为索引条件, 将日志块的大小、位置等信息作为查询结果。
在用户发起检索时,只需要指定时间范围和检索的元数据信息,通过元数据索引就可以快速找到对应的日志块了。相比于为每条日志建立索引,只索引日志块的方式能大大降低写入开销和索引维护成本。
并行检索,并支持 Shell 语句
在完成检索日志块的圈定后,下面就是要从日志块查找符合检索条件的日志了,这块上检索系统通过全文查询的方式来处理。
全文的检索效率不高,但可以通过并发的方式进行提速。服务的日志在写入时检索系统会保证日志数据会分散在多个节点上,在收到查询请求时每个节点会并行多个查询任务做日志块的全文检索,如此即使是全文检索的方式也可以保证一个相对快的检索耗时。
在检索语句的设计上,检索系统做了 shell 查询语句的支持,用户可以使用 grep, awk, wc 等命令来发起查询任务。shell 的查询设计足够强大可以满足各种查询需求, 并且对用户来说是没有额外的学习成本。
日志存储
在日志存储上,我们做到了与检索系统的深度融合,在存储上我们会划分 3 个层级,分别为本地存储、远程存储、归档存储,日志块数据会在这 3 个层级上流转和沉降。
检索系统会在本地磁盘创建日志块并写入日志数据, 数据会在本地存储(本地 SSD 磁盘)中保留一定时间 (一般是几个小时左右), 等超过时间后日志块会被压缩并上传至远程存储中 (对象存储中的标准存储类型), 再经过一段时间后日志块会沉降到归档存储中 (对象存储中的归档存储类型)。
而在查询时,如果是最近几小时的数据,检索系统会直接访问本地磁盘的日志块数据完成检索,如果是一周内的数据,检索系统会先从对象存储下载压缩的日志块数据并解压存放于本地存储中,再完成检索。而当用户访问一周前的数据时,检索系统会先提交一个归档存储取回申请,等待取回完成后再从远程存储中下载并解压,最后完成检索。其中下载解压完的远程存储和归档存储日志数据会在本地存储中缓存一定时间,后续用户再查询同样数据的时候就无需重复下载和解压了。每个压缩的日志块都控制在 10MB 左右,同时日志块的下载和解压都是可以并行,即使是检索远程存储上的数据同样也可以保证一个比较低的时延。
而这样的存储设计有什么好处呢? 如下面的多级存储示意图所示, 越往下存储的数据量越大, 存储介质的成本也越低, 每层大概为上一层的 1/3 左右, 并且数据是在压缩后存储的,日志数据的压缩采用的是 zstd 的方式,压缩率一般可以达到 10:1, 由此看归档存储日志的成本能在本地存储的 1% 的左右。如果使用了 SSD 硬盘作为本地存储, 这个差距还会更大。
价格参考:
日志检索架构
最后我们看看作业帮内融合了检索和存储的检索系统架构,系统主要有 4 个组件组成:
Ingester 组件, 负责所有日志块的生成和写入本地存储,并创建日志块的元数据索引。
Manager 组件, 负责管理日志块的生命周期, 主要有几个任务
将写入完成的日志块压缩并上传到远程存储
将过期的日志块从本地存储中淘汰掉.
有查询需要的时候从对象存储中下载日志块并解压
Query 组件, 负责具体查询任务的执行, 在本地存储对日志块做全文检索
Query-Proxy 组件,负责检索的准入核验、检索任务的分发以及检索结果的合并操作
在检索体验上,检索系统适配了 Grafana 的日志检索入口,用户可以在 Grafana 上同时查看监控和检索日志,同时在检索上做了追踪体系的打通,通过日志详情的链接可以快速跳转到追踪系统中。
性能表现
检索系统的性能表现如下:
写入性能:单核可支持 50MB/S 的日志写入速率
本地存储查询: 1TB 日志数据的查询耗时在 5S 内
远程存储查询: 1TB 日志数据的查询耗时在 15S 内
相比于 ELK 的方案:
在计算上,只需千核就可以满足作业帮所有服务日志的写入需求,如果使用 ES 在这个量级上则需要投入几万核规模,并且仍不能保证写入和查询效率。
在存储上,检索系统中的绝大部分日志都是压缩的形式在归档存储中保存,并且没有额外的索引空间开销,相比于 ES 的存储成本可以降低 2 个数量级。
今日好文推荐
“不用 Cursor和 ChatGPT、手写代码的开发者,怕不是疯了?”
谷歌突袭发布AI应用,无需Wi-Fi、手机就能跑大模型!网友实测两极分化
Replit 怒锤“欧洲版 Cursor”:造出百款“高危”应用,普通开发者一小时内黑入,氛围编码成了黑客“天堂”?
吴恩达评Agent现状:MCP尚处“蛮荒”,单Agent跑通已是“奇迹”,A2A协作堪称“双重奇迹”
来源:InfoQ