ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Loki Write Ahead Log(WAL)深度解析:从崩溃恢复到生产部署实战

Loki Write Ahead Log(WAL)深度解析:从崩溃恢复到生产部署实战 Loki Write Ahead LogWAL深度解析从崩溃恢复到生产部署实战【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/lokiLoki 的 Ingester 将数据暂存在内存中以换取写入性能与成本优势但进程崩溃会导致已确认写入的数据丢失。Write Ahead LogWAL通过在本地文件系统持久化到达的数据填补了这一可靠性缺口重启时 Loki 会先回放日志再对外提供写入服务从而同时获得内存缓冲的性能红利与数据持久化的可靠性。本文以 docs/sources/operations/storage/wal.md 为主线结合 pkg/ingester/wal.go、pkg/ingester/ingester.go 等源码系统讲解 WAL 的配置参数、崩溃恢复流程、磁盘节流与回压机制、监控指标以及基于 Kubernetes StatefulSet 的部署、迁移与扩缩容实战方案。WAL 的核心设计在内存性能与持久化可靠性之间取平衡Loki 的 Ingester 默认将数据缓冲在内存中定期把 Chunk 刷新到对象存储等长期存储。这种架构下一旦进程崩溃如节点故障、OOM、滚动升级中的异常退出尚未刷新的内存数据就会丢失。WAL 的引入改变了这一局面写入路径数据到达 Ingester 后先被记录到本地磁盘上的 WAL 文件然后才进入内存中的流与 Chunk。只要 WAL 写入成功该条数据就被视为已确认。恢复路径进程重启时Loki 会完整回放 WAL 中记录的数据将流、Chunk 与条目恢复到崩溃前的状态然后才把自己注册为 ready 状态对外提供读写服务。从源码可以看到这一写入模型的具体实现。在 pkg/ingester/wal.go#L118-L151 的Log方法中WAL 记录被拆分为两部分依次落盘先写入record.EncodeSeries编码的序列Series定义再写入record.EncodeEntries编码的条目Entries两次写入都会累加loki_ingester_wal_records_logged_total与loki_ingester_wal_logged_bytes_total指标。底层 WAL 直接复用 Prometheus 的tsdb/wlog实现且段文件大小被放大为wlog.DefaultSegmentSize * 4见 pkg/ingester/wal.go#L27以适配日志场景下更高的写入吞吐。需要特别说明的是WAL 是一份本地副本它的存在并不改变数据最终要刷新到长期存储的事实其价值在于只要客户端收到写成功的确认数据就不会因进程崩溃而丢失——这正是已确认即持久的语义保证。WAL 的取舍与边界两个有意为之的保证缺口Loki 的 WAL 旨在增加持久性保证但不以牺牲可用性为代价。因此与数据库领域常见的 WAL宁可拒绝写入也要保证持久化不同Loki 在两种场景下会主动放弃持久化保证1. WAL 损坏或被删除如果回放之前 WAL 文件已经损坏或被部分删除Loki 无法恢复全部数据。此时 Loki 会尽力恢复任何可以恢复的数据但不会阻止自身启动——可用性优先于完整恢复。从源码看这一行为是显式实现的在 pkg/ingester/ingester.go#L580-L615 的启动流程中checkpoint 与 WAL 段的恢复分别执行RecoverCheckpoint和RecoverWAL即使恢复过程返回错误启动流程也只会记录 error 日志并继续而不是中止启动。对应的日志原文也明确说明No administrator action is needed and data loss is only a possibility if more than (replication factor / 2 1) ingesters suffer from this——只有当超过(副本数 / 2 1)个 Ingester 同时损坏时数据丢失才成为现实可能这正是依赖多副本冗余的典型设计。请使用 Prometheus 指标loki_ingester_wal_corruptions_total跟踪并对该事件告警。2. 磁盘空间耗尽默认情况下Loki 会监控承载 WAL 的卷的磁盘用量当用量达到容量的90%时Ingester 会拒绝新的写入返回只读错误而不是接收自己无法持久化的数据该检查每 10 秒执行一次度量对象是承载 WAL 目录的整个文件系统而不仅仅是 WAL 文件本身的大小——因此不要将 WAL 卷与其他数据共用磁盘用量回落到阈值以下后写入会自动恢复阈值可通过--ingester.wal-disk-full-threshold修改设为0可关闭此保护该节流行为在 Windows 上不可用。这一机制的源码位于 pkg/ingester/wal.go#L193-L239 的monitorDisk与 pkg/ingester/wal_unix.go#L9-L23 的checkDiskUsage后者通过syscall.Statfs取得整个挂载点的 Blocks 与 Bfree计算出 0.01.0 的使用率前者以 10 秒为周期比对阈值在状态翻转时切换diskThrottled原子标志并输出 warn/info 日志。wal_unix.go带有//go:build !windows构建约束这正是文档中Windows 上无此行为的代码级原因。写入侧的拦截发生在 pkg/ingester/ingester.go#L1011-L1014 的Push方法入口一旦i.wal.IsDiskThrottled()为真直接返回ErrReadOnly从根源上阻止数据进入内存。边界情形如果禁用了该阈值或写入恰好发生在两次检查之间的时间窗口内磁盘在检查前被写满底层磁盘仍可能完全写满。此时 Loki不会拒绝本次写入但也不会把该条目记入 WAL——这条数据在进程重启后的持久化保证随即失效。跟踪与告警建议loki_ingester_wal_disk_full_failures_total当磁盘用量越过节流阈值、以及 WAL 写入因磁盘完全写满而失败时递增。该计数器不统计每一次被节流的写入因此只要值非零就意味着 WAL 磁盘需要关注loki_ingester_wal_disk_usage_percent当前磁盘用量0.01.0可直观观察用量与阈值的距离。回压机制Backpressure让超大的 WAL 在有限内存内完成回放在极端场景如长时间故障后 WAL 增长到无法在内存中恢复的规模回放本身就会耗尽内存。为此 WAL 内置了回压机制Ingester 会持续跟踪回放中的数据量一旦超过ingester.wal-replay-memory-ceiling阈值的90%就会把数据刷新flush到长期存储为继续回放腾出内存副作用是可能削弱基于内容寻址存储去重 Chunk的效率但官方认为这一效率损失换来了运维上的极大简化且在常规操作滚动升级、重新调度中不应触发将ingester.wal-replay-memory-ceiling设为0可完全禁用回压。源码实现见 pkg/ingester/replay_controller.go#L124-L149 的WithBackPressure先计算ceiling ReplayMemoryCeiling * 9 / 10ceiling 0时直接跳过回压逻辑当当前占用字节数Cur()超过 ceiling 时循环调用Flush()并使用 singleflight 保证同一时刻只有一个 flush 在执行若 flush 无进展且仍超 ceiling 则返回 WAL replay flush made no progress 错误。相关单元测试见 pkg/ingester/replay_controller_test.go其中TestReplayControllerWithBackPressureSkipsFlushWhenCeilingZero专门验证了ceiling0时完全跳过 flush。WAL 配置参数全解WAL 相关的全部配置集中定义在 pkg/ingester/wal.go#L30-L60 的WALConfig结构体与RegisterFlags方法中Validate方法pkg/ingester/wal.go#L39-L47还会校验 checkpoint 间隔不小于 1 秒、磁盘阈值必须在 01 之间。参数明细如下命令行 Flag默认值说明--ingester.wal-enabledtrue是否在摄取期间写入 WAL。默认开启通常无需显式设置。--ingester.wal-dirwalWAL 数据的存储与恢复目录。必须位于挂载的持久卷上。--ingester.checkpoint-duration5m创建 checkpoint 的间隔。--ingester.wal-replay-memory-ceiling4GBWAL 回放期间允许占用的最大内存。超过后先 flush 到存储再继续回放允许回放比可用内存大得多的 WAL支持 KB/MB/GB 单位后缀。建议设置为可用内存的约 75%对常规操作/滚动升级几乎无影响。设为0禁用回压。--ingester.wal-disk-full-threshold0.90磁盘用量阈值0.01.0达到后 Ingester 节流写入以保护 WAL 持久性。设为0关闭保护。--ingester.flush-on-shutdownfalse启用 WAL 后是否在关闭时将 Chunk 刷新到长期存储见下文生命周期部分。对应的 YAML 配置示例这些参数同样可以通过 YAML 配置字段名对应WALConfig的 yaml tag设置ingester: wal: enabled: true dir: /data/wal checkpoint_duration: 5m replay_memory_ceiling: 4GB disk_full_threshold: 0.9 flush_on_shutdown: falsecheckpoint 在恢复中的作用WAL 由两类数据构成持续追加的**段文件segment**与周期性生成的checkpoint 快照。checkpoint 定期把内存中当前流与 Chunk 的状态固化到磁盘间隔由--ingester.checkpoint-duration控制默认 5 分钟从而避免启动时从头回放全部历史段文件——恢复时只需加载最近的 checkpoint再回放其后新增的段即可显著缩短恢复时间。启动流程pkg/ingester/ingester.go#L540-L620正是先RecoverCheckpoint、后RecoverWAL的两阶段顺序且回放期间会临时将RetainPeriod置 0、禁用进程内流数量限制检查回放结束再由recoverer.Close()恢复。监控 WAL核心指标一览以下指标均定义于 pkg/ingester/metrics.go#L93-L175可直接用于告警与容量规划指标名类型含义loki_ingester_wal_corruptions_totalCounter带type标签取值为checkpoint/segment遇到的 WAL 损坏总数loki_ingester_wal_disk_full_failures_totalCounter磁盘用量越过--ingester.wal-disk-full-threshold节流阈值、或 WAL 写入因磁盘完全写满而失败时递增不统计每次被节流的写入loki_ingester_wal_disk_usage_percentGauge承载 WAL 的卷的当前磁盘用量0.01.0仅当--ingester.wal-disk-full-threshold大于0时上报loki_ingester_wal_records_logged_totalCounter已记录的 WAL 记录数loki_ingester_wal_logged_bytes_totalCounter写入 WAL 的总字节数loki_ingester_wal_replay_activeGauge启动时正在回放 WAL 与 checkpoint 则为 1否则为 0loki_ingester_wal_replay_duration_secondsGauge回放 checkpoint 与 WAL 所花费的时间此外 pkg/ingester/metrics.go#L156-L175 还提供了恢复过程的相关指标loki_ingester_wal_recovered_streams_total从 WAL 恢复的流数、loki_ingester_wal_recovered_chunks_total从 checkpoint 恢复的 Chunk 数、loki_ingester_wal_recovered_entries_total恢复的条目数、loki_ingester_wal_duplicate_entries_total回放时因已存在于 checkpoint 而被丢弃的条目数、loki_ingester_wal_recovered_bytes_total恢复的总字节数可用于评估每次重启的恢复规模。相关行为均有对应测试覆盖例如 pkg/ingester/wal_disk_throttle_test.go 验证了IsDiskThrottled状态随磁盘用量翻转、以及--ingester.wal-disk-full-threshold关闭后的行为。启用 WAL 后的部署变更由于 Ingester 必须在重启/滚动升级后仍能访问同一块持久卷以恢复 WAL 与令牌token部署形态需要相应调整1. 使用 StatefulSet 承载 Ingester所有 Ingester 应运行在Kubernetes StatefulSet上并挂载固定持久卷以保证 Pod 重建后 WAL 数据仍在。卷应使用 ReadWriteOnce 的块设备/文件系统如云厂商的持久盘、本地 SSD。2. 相关 Flag 组合--ingester.wal-enabled默认true启用摄取期间写 WAL--ingester.wal-dir指向挂载卷上的目录如/data/wal--ingester.checkpoint-durationcheckpoint 创建间隔--ingester.wal-replay-memory-ceiling默认 4GB建议设为可用内存的约 75%用于极端故障后的快速恢复--ingester.wal-disk-full-threshold默认 0.90磁盘保护阈值详见上文磁盘空间耗尽一节。3. 磁盘空间需求参考官方基于真实世界测试给出的数据实测对象一个约 5000 条序列series、摄入速率约 5MB/s 的 Ingestercheckpoint 间隔5 分钟结果WAL 专用磁盘上的稳态磁盘占用约 1015GB。规划容量时应据此预留余量不要以 100% 磁盘利用率为目标——磁盘节流阈值默认 0.90且建议为回放、checkpoint 合并等操作留出缓冲。生命周期变化滚动升级与缩容时不再主动 Flush启用 WAL 后滚动升级或缩容期间的数据刷新到 Chunk 存储流程被禁用。原因在于 StatefulSet 的滚动升级中不存在一个 Ingester 退出、另一个 Ingester 加入的并存场景——而是同一个 Ingester 先关停、再以新配置拉起因此无需 flush数据直接从 WAL 恢复即可。如果你确实需要 Pod 关闭时把数据刷入 Chunk 存储可设置--ingester.flush-on-shutdowntrue。这一语义的取舍很关键滚动升级依赖WAL 仍在原卷上这一前提而真正的缩容Pod 将被删除、卷可能回收则恰恰相反必须显式 flush见下文如何缩容。从无状态部署迁移到有状态部署从无 WAL 的 Deployment迁移到带 WAL 的 StatefulSet时新旧 Ingester 应同步地一升一降且两者之间不转移数据以保证迁移后任何新写入立即可靠。以 4 个 Ingester 为例迁移步骤如下启动一个 Stateful 形态的 Ingesteringester-0等待它就绪能同时接受读写请求将旧 Deployment 缩容到 3等待退出的 Ingester 把全部数据刷入 Chunk 存储确认该 Pod 已从kubectl get pods中消失后再添加一个 Stateful Ingester 并等待就绪。此时集群中有ingester-0与ingester-1重复步骤 2从旧 Deployment 中再移除一个 Ingester重复步骤 3 添加新的 Stateful Ingester得到ingester-0 ingester-1 ingester-2重复步骤 4 与 5最终得到ingester-0 ingester-1 ingester-2 ingester-3。整个过程的核心纪律是每一步都等旧实例完全 flush 并退出、新实例完全就绪后再进行下一步避免迁移窗口期出现无副本可写的空档。如何扩缩容扩容Scale up扩容与没有 WAL、不使用 StatefulSet 时完全一致无需任何额外处理新 Ingester 加入 ring 后即可分担写入。缩容Scale down缩容时必须确保离开的 Ingester 上的存量数据刷新到长期存储而不是只留在 WAL 中——因为被删除的 Ingester 不会再回放自己的 WAL留在 WAL 中的数据将变成孤儿数据。以 4 个 Ingester 缩容到 2 个为例按 StatefulSet 规则先被关停的是ingester-3然后是ingester-2。因此缩容前先对将要关闭的 Ingester 做端口转发port-forward调用其 HTTP API 的/ingester/shutdown?flushtrue端点该端点会触发刷新 Chunk 并从 ring 中摘除自己随后 Ingester 注册为 unready即可安全删除对ingester-2与ingester-3都执行上述操作后再把 StatefulSet 副本数缩到 2。另一种方式是设置--ingester.flush-on-shutdowntrue使 Ingester 在关闭时自动将 Chunk 刷入长期存储。Kubernetes 实操技巧与坑StatefulSet 的不可变字段问题StatefulSet 的规格中存在大量不可变字段滚动升级与改造相当繁琐。例如在单存储 Loki 上启用 WAL、并为 WAL 与 TSDB 分别挂载独立卷时更新 StatefulSet 很可能直接报 immutability 错误。此时可采用断链重建技巧kubectl -n namespace delete sts ingester --cascadefalse--cascadefalse会保留现有 Pod 存活仅删除 StatefulSet 对象然后重新创建更新后的StatefulSet再按顺序、逐个删除ingester-0到ingester-n的 Pod让 StatefulSet 逐个拉起新 Pod 替换它们。使用/flush_shutdown端点与生命周期钩子完成有序缩容推荐的四步组合拳StatefulSet 提供有序缩容Kubernetes StatefulSet 天然保证 Pod 按序逐个缩容先删最大序号的 Pod确保缩容过程有序、可靠配置 PreStop 生命周期钩子Pod 缩容时PreStop 钩子调用 Ingester 的/flush_shutdown端点触发刷新 Chunk 从 ring 摘除随后 Ingester 变为 unready 并可被删除设置terminationGracePeriodSeconds为 Ingester 预留足够时间完成 flush 后再被删除如果数据量很大、flush 耗时可能超过 30 分钟需要相应调大该值开启enableStatefulSetAutoDeletePVC利用 Kubernetes 1.23 的 StatefulSet PVC 自动删除特性缩容后自动清理不再需要的持久卷。非 Kubernetes / 裸机部署的注意点Ingester 因任何原因升级、崩溃等重启时必须能重新挂载同一块卷以恢复 WAL 与令牌两个 Ingester 绝不能共用同一个 WAL 卷/目录滚动发布必须是先彻底关停旧 Ingester再启动新 Ingester顺序不可颠倒。总结Loki 的 WAL 以本地磁盘上的段文件 周期性 checkpoint 的组合为内存缓冲的 Ingester 提供了已确认即持久的可靠性语义同时通过磁盘节流默认 90% 阈值、10 秒检查周期、整文件系统维度度量与回放内存回压默认 4GB 上限的 90% 触发 flush两道防线保证可用性不被持久化需求反噬。生产部署的要点可以浓缩为StatefulSet 专用持久卷、--ingester.wal-dir指向卷内目录、--ingester.wal-replay-memory-ceiling设为可用内存的约 75%、WAL 卷不与其他数据混用并配合loki_ingester_wal_corruptions_total、loki_ingester_wal_disk_full_failures_total、loki_ingester_wal_replay_active等指标建立监控与告警。迁移、扩容与缩容则遵循逐个替换、先 flush 后删除的纪律确保数据在任何时刻都不孤悬于 WAL 之中。【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表