VictoriaLogs 如何在列式布局中存储日志
摘要
深入探讨 VictoriaLogs 如何在列式布局中存储日志,涵盖数据摄入、流标识和压缩。
<p><a href="https://lobste.rs/s/2m10ql/how_victorialogs_stores_your_logs">评论</a></p>
查看缓存全文
缓存时间: 2026/06/28 13:58
# VictoriaLogs 如何以列式布局存储你的日志
来源:https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html
如果你在运行 VictoriaLogs,日常工作中主要涉及三件事:发送日志、查询日志和设置留存策略以免磁盘被填满。其余的一切都在磁盘上悄然进行。
这篇文章将跟随一条日志从到达的那一刻到最终在磁盘上落地的全过程,让你了解 VictoriaLogs 在幕后做了什么,并能解释你看到的现象:为什么查询返回得那么快,为什么有时磁盘上会有很多文件,以及当某些情况看起来不对劲时,哪些标志和指标是重要的。这篇文章面向所有人,不需要编程背景,也不需要阅读 Go 代码。如果你想深入了解,[VictoriaLogs 源代码](https://github.com/VictoriaMetrics/VictoriaLogs/) 始终是参考。
## 1. 一条日志到达 ## # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#1-a-log-line-arrives)
VictoriaLogs 支持多种协议接收日志:JSON Lines、Elasticsearch bulk、Loki push、OpenTelemetry、syslog 等等(完整列表请参阅[数据摄入文档](https://docs.victoriametrics.com/victorialogs/data-ingestion/))。
无论你使用哪种协议,VictoriaLogs 首先会将该记录转换为系统其余部分都能理解的单一内部形状:一个时间戳、一组命名字段和一个“流标识”。
每种协议都映射为一种内部形状。
每种协议都映射为一种内部形状。每种协议都有自己的小型处理器来完成这种转换,你可以通过查询参数或请求本身的标头来影响它:
- 使用 `ignore_fields` 查询参数或 `VL-Ignore-Fields` 标头来丢弃你不希望存储的字段。
- 使用 `decolorize_fields` 查询参数或 `VL-Decolorize-Fields` 标头来去除值中的终端颜色代码。
- 使用 `extra_fields` 查询参数或 `VL-Extra-Fields` 标头为每条记录附加额外的字段。
- 使用 `_msg_field` 查询参数或 `VL-Msg-Field` 标头将 VictoriaLogs 指向主要消息字段(`_msg`)。
- 使用 `_time_field` 查询参数或 `VL-Time-Field` 标头告诉它哪个字段保存时间戳。
- 使用 `_stream_fields` 查询参数或 `VL-Stream-Fields` 标头选择哪些字段定义流标识。
流标识是整篇文章中最重要的概念。共享相同流字段的日志被视为一个**流**,而你决定这个流的样子。例如,设置 `_stream_fields=pod,container`,那么所有具有相同 pod 和 container 的日志就形成一个流。
VictoriaLogs 将每个流的日志在磁盘上放在一起,这种分组使得它们压缩效果极好,并且让查询只需触及所需的流,而无需扫描所有内容。
共享相同流字段的日志形成一个流。
共享相同流字段的日志形成一个流。作为运维人员的实用规则是:保持流字段稳定且基数低,也就是说它们只应有少量不同的值,比如 `host`、`app`、`pod` 或 `container`;而高基数(具有非常多唯一值)的字段,如 `trace_id` 或 `user_id`,应作为普通字段,而不是流字段。
现在,在接收并规范化传入的记录后,VictoriaLogs 不会逐条处理它们。它会在内存缓冲区中累积这些记录,大约每秒一次(如果缓冲区满了则更快),将整批数据转换成一个仍驻留在 RAM 中的小型可搜索块(一个**内存部分**)。
缓冲的批处理刷新为一个内存部分。
缓冲的批处理刷新为一个内存部分。那个内存缓冲区并不是一个单一的共享队列。如果每个传入的批处理都必须排入同一个缓冲区,它们就会浪费时间互相等待,因此 VictoriaLogs 将缓冲区拆分为多个分片(每个 CPU 核心一个),并将传入的批处理轮流分配到这些分片上。
因此,在一个 3 CPU 的机器上,有 3 个缓冲区分片并行填充,每个分片独立刷新,大约每秒将其批处理写入一个新的内存部分:
缓冲区被拆分为每个 CPU 的分片,每个分片刷新自己的内存部分。
缓冲区被拆分为每个 CPU 的分片,每个分片刷新自己的内存部分。**部分**是 VictoriaLogs(以及其他 VictoriaMetrics 产品)中的核心数据结构之一:一个自包含的、*可搜索*的数据包,即*可查询*的数据包。
大多数情况下,缓冲的批处理被刷新为一个内存部分,但在某些罕见情况下,如果批处理足够大,超过了内存大小限制,则会直接作为*小部分*或*大部分*写入磁盘。
**指标** `vl_insert_flush_duration_seconds`:将缓冲的批处理转换为内存部分所需的时间。
## 2. 每日分区 ## # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#2-daily-partitions)
当批处理刷新时,VictoriaLogs 将每条日志归入一个**分区**,每个分区正好包含一个日历日(UTC 时间)的日志。它读取每条日志的时间戳,计算出它属于哪一天,然后将其路由到那里。
换句话说,你的日志按日期分隔。你可以在磁盘上直接看到这一点,每天一个目录:
``
$ tree victoria-logs-data/
victoria-logs-data/
└── partitions/
├── 20260109
├── 20260110
├── 20260111
└── 20260112
``
这种按日布局不仅仅是一个实现细节;它也是两个日常操作成本低廉的原因:
- **留存**是通过删除整个日目录来实现的。当日志老化超过 `-retentionPeriod`(默认 7 天),或者基于磁盘的留存触发时,VictoriaLogs 会删除整个日文件夹,而不是逐个查找日志行。
- **查询几乎总是有时间范围限制**(`_time:1h`、`_time:5m`),因此 VictoriaLogs 只需要打开与时间范围重叠的日分区,而忽略其他分区。
分区不仅仅是磁盘上的一个文件夹。它有两个面:一个磁盘面,存放已经写出的部分;一个内存面,存放我们刚才看到的缓冲区分片和内存部分。
分区有内存面和磁盘面。
分区有内存面和磁盘面。当你查询某一天时,分区会同时从两侧提供结果,仅在需要时才从磁盘拉取相关部分。
**指标** `vl_storage_parts` 统计存在的部分数量,按它们所在位置细分:`{type="storage/inmemory"}` 表示仍在内存中的部分,`{type="storage/small"}` 和 `{type="storage/big"}` 表示磁盘上的部分。`vl_pending_rows{type="storage"}` 统计仍留在缓冲区中、尚未转换为部分的行数。
## 3. 部分:VictoriaLogs 实际存储的单位 ## # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#3-parts-the-unit-victorialogs-actually-stores)
我们在第 1 节中已经遇到过部分:部分就是缓冲区刷新时产生的自包含、可搜索的日志包。当时没有说的是部分有三种类型,它们实际上是同一数据在不同生命周期阶段的表现:
- **内存部分**首先被创建,因此刚摄入的日志几乎可以立即查询,无需等待磁盘。
- **小部分**是出于持久性目的写入磁盘的内存部分。
- **大部分**则是随着时间的推移合并小部分后得到的。
内存部分合并成小部分,再合并成大部分。
内存部分合并成小部分,再合并成大部分。内存缓冲区中的日志首先刷新为内存部分,然后才触及磁盘,这保持了摄入的低成本。
部分仅在大的、不频繁的块中到达磁盘,而小部分在内存中合并成更大的部分,因此 VictoriaLogs 的磁盘写入和读取次数少得多。这就是为什么即使在缓慢、低 IOPS 的 HDD 上,它也能吸收大约每秒 1 GiB 的日志。
在第一种类型中隐藏着一个权衡。内存部分存在于 RAM 中,因此日志在几秒内变得可查询,但它们尚未安全地保存在磁盘上。VictoriaLogs 通过保证在短时间间隔内刷新到磁盘来弥补这一差距。控制此行为的标志是 `-inmemoryDataFlushInterval`(默认 `5s`):内存数据保证到达磁盘的频率。
在磁盘上,每个部分都是一个目录,目录名是 16 字符的十六进制名称(只是一个 ID),位于分区的 `datadb` 文件夹内。一个单独的 `indexdb` 文件夹保存当天的流目录:
``
$ tree victoria-logs-data/partitions/20260109/
victoria-logs-data/
└── partitions/
└── 20260109/
├── indexdb/
└── datadb/
├── 1882C35B4CE64498/
├── 1882C35B4CE664F8/
├── 1882C35B4CE66BDB/
└── parts.json
``
`parts.json` 文件只是当前活跃部分的列表,因此 VictoriaLogs 在启动时知道要读取哪些目录:
``
$ cat victoria-logs-data/partitions/20260109/datadb/parts.json
["1882C35B4CE64498", "1882C35B4CE664F8", "1882C35B4CE66BDB"]
``
部分是不可变的:一旦写入,其文件永不更改。这使得快照和备份既安全又廉价。
那么小部分和大部分有什么区别呢?
这归结于操作系统的**页面缓存**。简单来说,页面缓存是 RAM 中用于保存最近读取的文件数据的一块区域,因此再次读取相同字节时会从内存而不是磁盘中获取。机器拥有的 RAM 越多,这个缓存就越大,你更多的日志就会直接从内存中提供。
小部分通过该缓存写入,并有意保持较小,因此它们通常保留在 RAM 中并快速读取回来。它们的大小与 VictoriaLogs 留给操作系统缓存(至少 10 MB)的 RAM 成比例;任何更大的内容都会作为大部分写入。大部分可以增长到大约 1 TB,并且在写入时会绕过页面缓存,因此大型合并不会驱逐近期查询所依赖的热数据。
在我们上面看到的 `datadb` 文件夹旁边,有一个 `indexdb` 文件夹。虽然 `datadb` 保存部分(实际日志),但 `indexdb` 是每日的流目录:存在哪些流以及它们携带哪些字段。查询时,VictoriaLogs 首先检查此目录,以确定哪些流可能匹配,因此它甚至不会查看不相关流中的日志。这值得单独写一篇文章,所以我们在此略过。
## 4. VictoriaLogs 数据结构的思维模型 ## # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#4-the-mental-model-of-victorialogs-data-structure)
在我们打开实际文件之前,有一个关于部分布局的思维模型会很有帮助。指导思路很简单:VictoriaLogs 安排数据的方式使得稍后读取时尽可能少地触及数据。两个思想完成了大部分工作。
### 4.1 日志按流和时间分组到块中 ### # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#41-logs-are-grouped-into-blocks-by-stream-and-by-time)
在一个部分内部,日志被打包成**块**。一个块包含来自单个流的行,一个部分包含许多块。这些块按流排列,然后按时间排列,块内的行按时间戳排序。
一个部分的块,按流分组。
一个部分的块,按流分组。一个块的大小上限大约是 2 MiB 的未压缩数据(当部分合并时,几乎可以达到 4 MiB)。每个块携带一小段称为**块头**的元数据,与日志本身分开存储(在 `index.bin` 中,我们将在第 5.8 节打开它)。它记录:
- 该块属于哪个流,
- 它包含多少行,
- 以及块中的最小和最大时间戳。
由于这些头部很小且与实际日志数据分开存储,VictoriaLogs 可以扫描它们来决定哪些块值得读取,而无需触及跳过块中的任何日志行。
这就是使带过滤条件的查询成本低廉的原因。当你要求一个流在过去一小时内的日志时,VictoriaLogs 直接跳转到该流的块,并利用每个块头的最小和最大时间戳,跳过任何时间范围与你的窗口不重叠的块。它完全不读取不相关的流或范围外的块。
### 4.2 每个字段都是一列,因此查询只读取它们请求的内容 ### # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#42-each-field-is-a-column-so-queries-read-only-what-they-ask-for)
想象几条日志的自然方式是一条记录一行:每条日志行是一行,其所有字段并排放置:
三条日志,一条记录一行。
三条日志,一条记录一行。在一个块内,VictoriaLogs 将其翻转过来。它不是将每条日志作为一行保留在一起,而是将每个字段存储为自己的**列**,值仍然按行对齐:
相同的日志按列存储。
相同的日志按列存储。将字段保留在它自己的列中意味着 VictoriaLogs 只需触及请求实际使用的列,而不是读取每行的每个字段。你的日志携带的字段越多,这一点就越重要,这也是 VictoriaLogs 能够轻松处理具有许多字段和许多不同值的宽日志的重要原因。
这也是为什么 LogsQL 中的 `fields` 管道是加速查询的廉价方式。默认情况下,查询返回每个字段,这意味着读取每一列。添加 `| fields ...`(或其别名 `| keep ...`)告诉 VictoriaLogs 只读取你命名的列并跳过其余列:
``
_time:5m error | fields _time, host, _msg
``
这里 VictoriaLogs 只读取 `_time`、`host` 和 `_msg` 列,而不是从磁盘上拉取每个匹配行的每个字段。在具有数十个字段的宽日志上,将输出限制为你实际需要的几个字段可以显著减少查询读取的数据量。
将同一个字段的值放在一起还有另一个好处:压缩。相同字段的值往往是同一种事物:
- `level` 总是几个单词之一,如 `info` 或 `error`,
- `status` 总是一个数字,
- `timestamp` 总是一个时间。
当列中的每个值具有相同形状或类型时,压缩效果极好,而 VictoriaLogs 通过为每列选择一种合适的存储格式(例如数字、时间戳、IP 地址等)来充分利用这一点。
除此之外,同一流中的日志彼此非常相似,因此列中相邻的值会大量重复,从而进一步压缩。这是 VictoriaLogs 能够将数据从几 TiB 的原始日志压缩到磁盘上仅几百 GiB(甚至更少)的原因之一。
## 5. 部分内部的文件 ## # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#5-the-files-inside-a-part)
以上就是思维模型。现在让我们打开一个部分,查看实际的文件,看看每个文件的用途以及它们如何服务该模型。
一个部分是一个文件目录。如果你列出一个目录,你会看到类似这样的内容:
``
1882C35B4CE64498/
├── metadata.json
├── index.bin, metaindex.bin
├── timestamps.bin
├── values.bin0, values.bin1, ...
├── bloom.bin0, bloom.bin1, ...
├── message_values.bin, message_bloom.bin
└── column_names.bin, column_idxs.bin, columns_header.bin, columns_header_index.bin
``
你不需要记住这些。它们大多数属于几个组,而且几乎每一个都是为了帮助 VictoriaLogs 在读取任何实际数据之前排除工作而存在的。
### 5.1 metadata.json:部分级别的摘要 ### # (https://victoriametrics.com/blog/victorialogs-internals-columnar-storage-on-disk/index.html#51-metadatajson-the-part-level-summary)
相似文章
列式存储即规范化
本文将列式存储重新定义为数据库规范化的极端形式,展示了把属性拆分为位置对齐的数组如何与基于隐式序数主键连接的规范化表如出一辙。
TimescaleDB 如何压缩时间序列数据
本文解释了 TimescaleDB 的 hypercore 引擎如何通过列式存储以及 delta 编码和 Gorilla XOR 等专门算法,为时间序列数据实现高达 98% 的压缩率,并将其与 PostgreSQL 的 TOAST 进行了对比。
@Greptime: 我们更新了对GreptimeDB内部存储引擎Mito2的深入解析,涵盖LSM-tree写入路径、列式Parquet SST等……
GreptimeDB的Mito2存储引擎采用LSM-tree设计,结合列式Parquet SST、三级扫描剪枝以及TWCS压缩。该博客文章对其架构进行了全面解析。
DockLog
DockLog 是一个简化 Docker 日志管理的工具,无需搭建完整的日志堆栈。
深入vLLM:高吞吐量LLM推理系统剖析(2025)
深入探讨vLLM用于高吞吐量LLM推理的架构与组件,涵盖调度、分页注意力、连续批处理、高级特性、扩展、服务及基准测试。