Posted in

Go日志系统崩溃真相:zap+lumberjack组合在K8s滚动更新下丢失日志的4重根因分析

第一章:Go日志系统崩溃真相:zap+lumberjack组合在K8s滚动更新下丢失日志的4重根因分析

在 Kubernetes 环境中,采用 zap(v1.24+)与 lumberjack(v2.2+)组合实现日志轮转时,滚动更新期间常出现日志静默丢失——新 Pod 启动后旧日志未刷盘、归档文件被意外截断、甚至连续数分钟无任何日志写入。该现象并非偶发,而是由四层耦合机制共同触发。

日志句柄未优雅关闭导致缓冲区丢弃

lumberjack.LoggerClose() 被调用前不会强制刷新 zapcore.WriteSyncer 的底层缓冲区。K8s 发送 SIGTERM 后,默认 30 秒内若进程未退出即强杀,而 zap 默认异步写入且 Core.Sync() 不自动触发 lumberjack.Close()。修复需显式注册信号处理:

// 在 main() 中添加
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
go func() {
    <-sigChan
    _ = logger.Sync() // 触发 zap 缓冲刷写
    _ = lumberjackLogger.Close() // 显式关闭 lumberjack
    os.Exit(0)
}()

RollingWriter 多实例竞争破坏原子性

当应用使用多 goroutine 并发写日志(如 HTTP middleware + background job),且未对 lumberjack.Logger 做单例封装时,多个 Write() 调用可能同时触发 rotate(),导致 os.Rename() 冲突或 os.Truncate() 覆盖未完成写入。验证方式:kubectl exec -it <pod> -- ls -la /var/log/app/*.log* 可见零字节 .log.1 文件。

K8s EmptyDir 挂载时机与日志路径初始化冲突

若日志目录挂载为 emptyDir,但 zap 初始化早于 volume mount 完成(尤其在 initContainer 未显式等待时),os.OpenFile() 将静默创建空文件于 rootfs,后续 mount 覆盖该路径,导致日志写入消失。须确保:

  • InitContainer 中执行 until [ -d /var/log/app ]; do sleep 1; done
  • 主容器启动命令前置 mkdir -p /var/log/app && chmod 755 /var/log/app

Lumberjack MaxSize 单位误用引发高频轮转

MaxSize 字段单位为 字节(非 MB),若配置 MaxSize: 100(意图 100MB),实际 100 字节即触发轮转,滚动更新期间大量小文件生成,lumberjack.Cut() 频繁调用加剧 I/O 竞争与 inode 耗尽。典型错误配置与修正对比:

配置项 错误值 正确值 后果
MaxSize 100 100 * 1024 * 1024 防止每百字节轮转
MaxBackups 5 避免无限归档占满磁盘

以上四重根因常叠加发生,单一修复无法根治,必须同步治理。

第二章:Zap与Lumberjack协同机制的底层实现剖析

2.1 Zap同步写入器与异步日志管道的生命周期耦合关系

Zap 的 SyncWriter 并非独立运行,其存活周期直接受控于异步日志管道(如 zapcore.NewCore 所绑定的 zapcore.LockedWriteSyncer 或自定义 AsyncWriter)。

数据同步机制

SyncWriter 被注入到 Core 中,它与后台 goroutine 共享同一 sync.WaitGroup 实例,确保 Logger.Sync() 调用能阻塞至所有缓冲日志刷盘完成:

// 示例:带生命周期感知的同步写入器包装
type lifecycleAwareSyncer struct {
  w    zapcore.WriteSyncer
  done chan struct{} // 关闭时通知管道终止
}
func (s *lifecycleAwareSyncer) Write(p []byte) (n int, err error) {
  select {
  case <-s.done: return 0, errors.New("writer closed")
  default:
    return s.w.Write(p)
  }
}

done 通道由日志管道在 Stop() 时关闭,实现双向生命周期联动。

关键耦合点对比

组件 启动时机 终止依赖
SyncWriter NewLogger() Logger.Sync() 或管道 Stop()
异步管道 goroutine Core 初始化后 WaitGroup.Done() on Close()
graph TD
  A[Logger 初始化] --> B[SyncWriter 注入 Core]
  B --> C[异步管道启动 goroutine]
  C --> D[共享 waitGroup & done channel]
  D --> E[Stop() 触发 Writer 不可写 + goroutine 退出]

2.2 Lumberjack轮转器在文件句柄复用中的竞态触发路径(含strace+pprof实证)

竞态核心场景

Rotate()Write() 并发执行时,lumberjack.Logger 可能复用已关闭的 *os.File

// lumberjack.go 片段(简化)
func (l *Logger) Write(p []byte) (n int, err error) {
    if l.file == nil { // A:检查空指针
        if err = l.openNewFile(); err != nil { return }
    }
    return l.file.Write(p) // B:实际写入——但此时 l.file 可能已被 Rotate() 关闭
}

逻辑分析:A 与 B 之间存在时间窗口;Rotate() 在另一 goroutine 中调用 l.file.Close() 后立即置 l.file = nil,但 Write() 已通过 A 检查,正进入 B 执行——触发 write on closed file panic。

strace 关键证据

strace -e trace=write,close,openat -p $(pidof myapp) 2>&1 | grep -E "(write|close.*log)"

输出显示 close(7)write(7, ...) 交错出现,证实 fd 复用冲突。

pprof 定位热点

Frame CumTime Samples
lumberjack.(*Logger).Write 98ms 142
os.(*File).write 95ms 139

根本原因流程

graph TD
    A[Write goroutine: l.file != nil] --> B[Rotate goroutine: close l.file]
    B --> C[l.file = nil]
    A --> D[Write goroutine: call l.file.Write → EBADF]

2.3 Zap core.Close() 调用时机与K8s preStop钩子超时窗口的错配验证

数据同步机制

Zap 的 core.Close() 非阻塞,仅标记关闭状态,但未等待异步 flush goroutine 完成日志刷盘:

// zap/core.go 简化逻辑
func (c *ioCore) Close() error {
    c.mu.Lock()
    c.closed = true // 仅置标志位,不 wait flusher
    c.mu.Unlock()
    return nil
}

该设计导致 Close() 返回后,仍有日志在缓冲区或写入中,而 K8s preStop 默认仅预留 30s(terminationGracePeriodSeconds),易造成日志丢失。

错配验证路径

  • 在 Pod 中注入 sleep 45 模拟慢 flush
  • 设置 preStop 执行 kill -SIGTERM 1 && sleep 10
  • 观察 kubectl logs --previous 是否缺失最后 2~3 条日志
场景 preStop 超时 Close() 返回耗时 日志丢失率
默认配置 30s ~12%(压测)
增至 60s 60s

关键流程

graph TD
    A[preStop 触发] --> B[发送 SIGTERM]
    B --> C[Zap.Close() 快速返回]
    C --> D[flush goroutine 继续运行]
    D --> E[超时强制 kill -9]
    E --> F[未刷盘日志丢失]

2.4 日志缓冲区(ring buffer)在Pod终止瞬间的未刷盘数据逃逸模拟实验

数据同步机制

Kubernetes 中容器运行时(如 containerd)默认使用 journaldjson-file 驱动采集 stdout/stderr,其日志写入路径为:
stdout → ring buffer(内存) → flush to disk → kubelet log rotation

实验构造

通过强制中断模拟 Pod 突然终止场景:

# 启动一个持续写入但禁用自动刷盘的测试Pod
kubectl run log-escape --image=busybox:1.36 \
  --command -- sh -c 'i=0; while true; do echo "[LOG] $i $(date)"; i=$((i+1)); sleep 0.1; done' \
  --overrides='{"spec":{"containers":[{"name":"log-escape","image":"busybox:1.36","env":[{"name":"GODEBUG","value":"madvdontneed=1"}]}]}}'

逻辑分析GODEBUG=madvdontneed=1 禁用 Go 运行时对内存页的主动归还,加剧 ring buffer 滞留;sleep 0.1 控制写入节奏,确保部分日志驻留于内核环形缓冲区未落盘。当执行 kubectl delete pod log-escape --grace-period=0 时,约 120–350ms 内未 flush 的日志即“逃逸”。

关键参数说明

  • --grace-period=0:跳过 SIGTERM 等待,直接发送 SIGKILL
  • 默认 json-file 驱动 flush 间隔为 10ms(可调),但受调度延迟影响实际不可靠
缓冲层级 典型容量 刷盘触发条件 逃逸风险
ring buffer(内核) ~64KB write() 返回即入buffer,不保证落盘 ⚠️ 高
containerd log writer 可配置 flush_interval(默认10ms)或 buffer满 ⚠️ 中
kubelet file rotation 基于大小/时间轮转 ✅ 低
graph TD
    A[App write stdout] --> B[Kernel ring buffer]
    B --> C{flush timer / full?}
    C -->|Yes| D[Write to /var/log/pods/...json]
    C -->|No & SIGKILL| E[Data lost]

2.5 SIGTERM信号传递链中syscall.Exit与runtime.Goexit的协程清理差异分析

核心语义差异

syscall.Exit进程级终止,立即向内核发起 exit_group(2) 系统调用,绕过 Go 运行时,所有 goroutine 被强制销毁,无 defer 执行、无 finalizer 运行。
runtime.Goexit协程级退出,仅终止当前 goroutine,触发其 defer 链执行,并将控制权交还调度器。

清理行为对比

行为 syscall.Exit runtime.Goexit
是否等待 defer ❌ 否 ✅ 是
是否触发 finalizer ❌ 否 ✅ 是(若该 goroutine 启动了 finalizer 相关逻辑)
是否影响其他 goroutine ✅ 全局终止 ❌ 仅当前 goroutine

典型误用示例

func badSignalHandler() {
    signal.Notify(ch, syscall.SIGTERM)
    <-ch
    syscall.Exit(0) // ⚠️ 无 defer、无资源释放!
}

此处 syscall.Exit(0) 直接终止进程,defer file.Close() 等语句永不执行。应改用 os.Exit(0)(仍不执行 defer),或更安全地协调 shutdown(如 http.Server.Shutdown + runtime.Goexit 配合主 goroutine 退出)。

协程退出路径示意

graph TD
    A[收到 SIGTERM] --> B{选择退出方式}
    B -->|syscall.Exit| C[内核 exit_group → 进程立即消亡]
    B -->|runtime.Goexit| D[执行当前 goroutine defer] --> E[调度器回收 G 结构体]

第三章:Kubernetes滚动更新对日志生命周期的隐式破坏

3.1 Deployment滚动更新期间Pod Terminating阶段的进程树冻结与SIGKILL强制收割实测

在 Kubernetes v1.28+ 中,terminationGracePeriodSeconds 默认为 30s,但内核 cgroup v2 的 freezer 子系统会在 Pod 进入 Terminating 状态后立即冻结其所有进程(/sys/fs/cgroup/freezer/kubepods/.../freezer.state = FROZEN)。

冻结行为验证命令

# 查看目标Pod所在cgroup路径及冻结状态
kubectl exec -it nginx-7c89d4d6b4-2xq9z -- \
  find /sys/fs/cgroup -path "*/kubepods/*nginx*" -name freezer.state -exec cat {} \; 2>/dev/null

该命令定位 Pod 对应的 cgroup v2 freezer 控制器路径,并读取其冻结状态。若返回 FROZEN,表明 kubelet 已调用 cgroup.freeze 接口完成进程树挂起——此时 sleep 300 类进程将不响应任何信号(包括 SIGTERM),仅等待解冻或超时触发 SIGKILL。

SIGKILL 触发时机对比表

条件 冻结状态 SIGTERM 响应 SIGKILL 触发时刻
terminationGracePeriodSeconds=5 ✅ FROZEN ❌ 无响应 t=5s(计时器到期,绕过冻结直接发送)
preStop + sleep 10 ✅ FROZEN ❌ 暂停执行 t=10s 后才开始执行 preStop,总耗时可能超限

强制收割关键路径

graph TD
  A[Pod status → Terminating] --> B[kubelet: freeze cgroup]
  B --> C{preStop hook exists?}
  C -->|Yes| D[解冻 → 执行 hook → 重新冻结]
  C -->|No| E[启动 terminationGracePeriodSeconds 计时器]
  E --> F[计时结束 → 发送 SIGKILL 绕过冻结]

实测表明:进程树冻结是信号传递阻断的根源,而 SIGKILL 的最终投递由 kubelet 主动绕过 cgroup freezer 完成,不依赖内核级解冻。

3.2 EmptyDir卷在Pod重建时的瞬时不可见性与lumberjack.OpenFile的静默失败复现

现象根源:EmptyDir生命周期绑定Pod

EmptyDir卷在Pod删除瞬间即被清空,新Pod启动时虽挂载同名卷,但目录为空——无状态、无跨Pod持久性

关键触发链

// lumberjack.OpenFile 在文件不存在时静默创建;若日志轮转依赖旧文件元数据(如 size、modtime),
// 而重建后 stat 返回 0,将跳过轮转逻辑,导致日志写入失败却无错误返回
lf, err := lumberjack.NewRotateWriter("/var/log/app.log")
if err != nil {
    log.Fatal(err) // ❌ 此处永不触发:OpenFile 内部仅 warn 并继续
}

lumberjack.OpenFileos.Stat 失败时默认回退到新建文件,不返回 error,掩盖了 EmptyDir 重置导致的路径“逻辑断裂”。

典型失败时序

阶段 文件状态 lumberjack 行为
Pod 运行中 /var/log/app.log 存在且 size=1.9MB 正常写入,等待轮转
Pod 删除 EmptyDir 被立即清理
新Pod启动 /var/log/app.log 为空文件 Stat() 返回 size=0 → 跳过轮转判断
graph TD
    A[Pod Terminating] --> B[EmptyDir 卷销毁]
    B --> C[New Pod Pending]
    C --> D[Mount 新 EmptyDir]
    D --> E[lumberjack.Stat → size=0]
    E --> F[跳过轮转,直接 Write]
    F --> G[日志追加至空文件,旧索引丢失]

3.3 Kubelet日志采集组件(如filebeat/fluentd)与应用日志写入速率的时序冲突建模

数据同步机制

Kubelet将容器stdout/stderr通过/var/log/pods/符号链接落地,而Filebeat以inotify监听文件变更。当应用突发写入(如10MB/s),采集器轮询延迟(close_inactive: 5m)与内核page cache刷盘时机错位,引发日志截断或重复。

冲突建模关键参数

  • harvester_buffer_size: 控制单次读取上限,默认16KB → 小于高吞吐日志行长度时触发缓冲溢出
  • scan_frequency: 默认10s → 无法捕获

Filebeat配置优化示例

filebeat.inputs:
- type: log
  paths: ["/var/log/pods/*/*.log"]
  close_inactive: "30s"          # 缩短非活跃关闭窗口
  harvester_buffer_size: 64kb    # 匹配典型JSON日志行宽
  scan_frequency: "1s"           # 提升发现灵敏度

逻辑分析:close_inactive过长导致tail进程滞留,scan_frequency过低错过/var/log/pods/下高频滚动的新symlink;64KB缓冲适配Cloud Native应用平均日志行(含traceID、结构化字段)。

冲突类型 触发条件 影响
日志丢失 应用写入 > 文件系统sync速率 最后N行未采集
重复采集 inotify事件丢失+周期扫描重叠 同一行被提交两次
graph TD
  A[应用写入stdout] --> B[Kernel Page Cache]
  B --> C{fsync触发?}
  C -->|是| D[/var/log/pods/xxx.log]
  C -->|否| E[数据暂存内存]
  D --> F[Filebeat inotify event]
  F --> G[Harvester读取buffer]
  G --> H[ES/Kafka输出]

第四章:四重根因的交叉验证与工程化修复方案

4.1 根因一:Zap SyncWriter未实现Context感知关闭 —— 基于go.uber.org/zap@v1.24源码补丁实践

数据同步机制

SyncWriter 是 Zap 日志系统中负责将日志缓冲区刷写到底层 io.Writer 的关键组件,其 WriteSync 方法均无 context.Context 参数,无法响应取消信号。

源码缺陷定位

zap@v1.24 中,SyncWriter 结构体定义如下:

type SyncWriter struct {
    w io.Writer
}

该结构体未嵌入 io.WriteCloser 或提供 CloseWithContext(ctx context.Context) 方法,导致在 gRPC Server graceful shutdown 或 HTTP server Stop 时,日志 goroutine 可能阻塞在 w.Write() 调用中,无法及时退出。

补丁核心变更

补丁引入 SyncWriterWithContext 类型,并扩展接口:

方法 原实现 补丁增强
Write(p []byte) 同步阻塞 保持兼容
Sync() 无 context 支持 新增 SyncContext(ctx context.Context) error
graph TD
    A[Logger.Sync()] --> B{SyncWriter.SyncContext?}
    B -->|否| C[阻塞直至底层 Write 完成]
    B -->|是| D[select{ ctx.Done() \| w.Sync() }]

4.2 根因二:Lumberjack未监听os.Signal进行优雅轮转 —— 自定义Rotator注入与e2e测试用例构建

Lumberjack 默认不响应 SIGUSR1 等系统信号触发日志轮转,导致运维侧无法在不中断服务的前提下强制滚动日志。

自定义 Rotator 注入机制

通过 lumberjack.Logger.Rotator 接口注入可信号感知的实现:

type SignalAwareRotator struct {
    lj *lumberjack.Logger
    mu sync.RWMutex
}

func (s *SignalAwareRotator) Rotate() error {
    s.mu.Lock()
    defer s.mu.Unlock()
    return s.lj.Rotate() // 调用原生轮转逻辑
}

此实现封装了并发安全的 Rotate() 调用,供信号处理器安全触发;lj.Rotate() 内部会检查文件大小/时间阈值并执行原子重命名。

e2e 测试关键断言点

断言项 预期行为 检查方式
SIGUSR1 响应 主goroutine 立即轮转 kill -USR1 <pid> 后校验新文件 mtime
并发写入不阻塞 日志持续写入无 panic 使用 wg.Add(N) + 多 goroutine 写入
graph TD
    A[收到 SIGUSR1] --> B{Rotator 是否已注入?}
    B -->|是| C[调用 SignalAwareRotator.Rotate]
    B -->|否| D[忽略信号]
    C --> E[执行原子 rename + open 新文件]
    E --> F[返回 nil 或 error]

4.3 根因三:K8s terminationGracePeriodSeconds与log flush timeout的量化对齐策略

容器终止时日志丢失的根本矛盾,常源于 terminationGracePeriodSeconds 与应用层日志刷盘超时未做量化协同。

日志刷盘超时典型配置

# 应用侧 logback.xml 片段(同步刷盘模式)
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <immediateFlush>true</immediateFlush> <!-- 关键:强制每次写入即刷盘 -->
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <maxHistory>7</maxHistory>
  </rollingPolicy>
</appender>

immediateFlush=true 保障单次写入不缓冲,但无法规避系统调用延迟;若 flush 耗时 > 2s,而 terminationGracePeriodSeconds=30,则剩余 28s 实际可用于日志落盘——需精确建模该“有效窗口”。

对齐决策矩阵

场景 log flush timeout 推荐 terminationGracePeriodSeconds 依据
同步刷盘 + SSD ≤ 100ms 30s 留足 2x 冗余与 SIGTERM 处理开销
异步批量 + HDD 500ms–2s 60s 覆盖最差 flush 延迟 + GC 暂停

终止流程时序约束

graph TD
  A[Pod 收到 SIGTERM] --> B[应用开始优雅关闭]
  B --> C{log flush loop}
  C -->|每次 flush 耗时 t_i| D[累计耗时 Σt_i ≤ T_g - T_sig]
  D --> E[成功落盘所有待写日志]
  E --> F[调用 exit(0)]

其中 T_g = terminationGracePeriodSecondsT_sig ≈ 1–3s(Kubelet 发送信号至应用响应延迟),故可用 flush 时间上限为 T_g − T_sig

4.4 根因四:容器内核参数net.unix.max_dgram_qlen对日志socket通道的隐性限流影响分析

当应用通过 AF_UNIX 数据报 socket(如 syslog-ngrsyslog/dev/log)向日志服务发送日志时,内核使用 net.unix.max_dgram_qlen 限制每个 UNIX socket 接收队列的最大待处理数据报数量(默认值通常为 10)。

日志丢包的隐性触发点

  • 容器启动时未覆盖该参数,继承宿主机默认值;
  • 高频短日志突发(如微服务批量打点)迅速填满接收队列;
  • 后续 sendto() 调用返回 EAGAIN,但多数日志库静默丢弃而非重试。

参数验证与调优

# 查看当前值(容器内执行)
cat /proc/sys/net/unix/max_dgram_qlen
# 临时调大(需 root 权限)
echo 128 > /proc/sys/net/unix/max_dgram_qlen

该参数控制 每个绑定的 SOCK_DGRAM UNIX socket 的 sk_receive_queue 长度上限,非全局总和。值过小导致 unix_dgram_recvmsg() 在队列满时直接丢弃新数据报,且无背压反馈机制。

典型影响对比

场景 max_dgram_qlen=10 max_dgram_qlen=128
1000 条/秒日志突发 丢包率 ≈ 35% 丢包率
graph TD
    A[应用 sendto /dev/log] --> B{内核 unix_dgram_sendmsg}
    B --> C{sk->sk_receive_queue.len < max_dgram_qlen?}
    C -->|Yes| D[入队成功]
    C -->|No| E[返回 -EAGAIN → 应用丢弃]

第五章:从日志可靠性到云原生可观测性基建的演进思考

日志丢失不是故障,而是设计缺陷的显性暴露

某金融支付平台在灰度发布Kubernetes 1.26集群后,突发性出现0.3%的交易超时告警未被任何SRE值班系统捕获。事后追溯发现:Fluentd DaemonSet因资源限制触发OOMKilled,导致节点级日志采集中断长达17分钟;而Loki配置中chunk_idle_period: 5mmax_chunk_age: 1h的组合,使未flush的内存chunk在进程重启后永久丢失。这不是偶然——当单节点日志吞吐达42MB/s时,基于文件缓冲的架构已触及可靠性天花板。

OpenTelemetry Collector的双写策略实战

为突破单点采集瓶颈,团队将OTel Collector部署为Sidecar+Gateway混合拓扑:

  • 应用Pod内嵌轻量Collector(仅启用otlphttp接收器与logging导出器),确保日志零丢失;
  • 集群级Gateway Collector启用memory_limiter(limit_mib: 1024)与queued_retry(queue_size: 10000),同时向Loki(结构化日志)和Jaeger(trace上下文关联)双写。
    压测数据显示:在模拟5000 QPS突增场景下,端到端日志延迟P99稳定在83ms,较原Fluentd方案降低62%。

指标体系重构:从Black-box到Golden Signal驱动

传统Prometheus监控过度依赖node_cpu_seconds_total等底层指标,无法反映业务健康度。新架构强制推行黄金信号(Golden Signals)采集规范: 信号类型 数据来源 采样方式 SLI计算示例
延迟 Envoy access_log + OTel 每请求注入trace_id rate(http_request_duration_seconds_bucket{le="200"}[5m]) / rate(http_requests_total[5m])
流量 Istio metrics Prometheus remote_write sum(rate(istio_requests_total{destination_service=~"payment.*"}[1m]))
错误 OpenTelemetry SpanStatus 自动提取status.code字段 sum(rate(otel_span_status_code{code="STATUS_CODE_ERROR"}[1m]))

分布式追踪的上下文穿透实践

在电商大促期间,用户下单链路横跨12个微服务。通过在Spring Cloud Gateway注入X-B3-TraceId与自定义x-request-id双头,并在OTel Instrumentation中配置propagators

exporters:
  otlp:
    endpoint: otel-collector:4317
    tls:
      insecure: true
service:
  telemetry:
    propagators:
      - b3
      - tracecontext
      - xray

实现Span ID在HTTP/GRPC/MQ全链路透传,使单次订单查询的调用拓扑还原准确率达99.7%。

可观测性数据治理的硬约束

为避免“数据沼泽”,制定三条不可逾越红线:

  • 所有日志必须携带service.namek8s.namespace.namek8s.pod.name三个标签;
  • Trace Span必须包含http.status_codegrpc.status_code语义约定字段;
  • 指标命名严格遵循OpenMetrics规范,禁止出现cpu_usage_percent类模糊命名,统一为system_cpu_usage_ratio
    违反任一规则的数据将被OTel Collector的filterprocessor直接丢弃,保障下游分析引擎的数据质量基线。

成本与效能的再平衡

迁移后观测数据存储成本上升40%,但MTTD(平均故障定位时间)从47分钟降至6.2分钟。通过Loki的periodic_table策略(按天分表)与Thanos Compactor的自动降采样(5m→1h→1d三级压缩),在保留180天原始日志的前提下,对象存储空间占用控制在2.3TB以内。

在 Kubernetes 和微服务中成长,每天进步一点点。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注