在微服务与云原生架构中,容器已经成为应用部署的主流形态。但很多团队在迁移到容器环境后,发现原本在物理机或虚拟机上运转良好的日志方案突然失效了。根本原因在于容器的文件系统是临时的,容器重启或调度到别的节点后,内部写入的日志文件就再也找不回来。因此,容器内应用的日志输出不能照搬传统思路,而需要遵循一套面向标准流和集中采集的最佳实践。

一、为什么应该优先输出到标准输出与标准错误
容器引擎在设计之初就明确了日志处理模型:每个容器本质上是一个被隔离的进程,它运行时的 stdout 和 stderr 会被容器运行时(如 containerd、dockerd)捕获。以 Docker 的默认 json-file 驱动为例,应用打印到控制台的内容会被封装成包含时间、流类型、日志内容的 JSON 行,存储在宿主机的 /var/lib/docker/containers/ 目录下。这样做最大的好处是应用完全不需要关心日志存到哪里、怎么轮转,只要写标准流即可。
如果应用坚持自己打开文件句柄写日志,例如写成 app.log 放到容器内部,就会出现两个问题。第一,容器崩溃后文件随可写层丢失;第二,运维人员必须进到容器里才能看日志,违背了不可变基础设施的原则。对于绝大多数无状态服务,直接输出到 stdout/stderr 是最简单且最稳妥的方式。下面是一段 Java 应用使用 Logback 将日志指向控制台的配置示例:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
对于 Node.js 应用来说就更简单了,console.log 和 console.error 本身就会写到标准流。但要注意避免在生产环境使用调试级的冗长输出,否则 json-file 会迅速占用磁盘。可以通过环境变量控制日志级别,例如 LOG_LEVEL=info 时只输出 info 及以上级别。这种把配置外置的做法,也契合十二因子应用的方法论。
二、集中式日志采集架构如何搭建
当集群规模扩大,单台主机上的容器日志已经无法满足排查需求,必须引入集中采集。常见的架构是在每个节点部署一个轻量采集器,比如 Fluent Bit 或 Filebeat,它们读取容器运行时产生的日志文件,加上 Pod 名、命名空间、容器名等标签,再发送到后端的 Elasticsearch、Loki 或 Kafka。这种边车或守护进程模式对应用零侵入,应用依旧只管打标准流。
以 Kubernetes 环境为例,kubelet 默认会把容器的 stdout/stderr 重定向到 /var/log/containers/*.log,这其实是软链到 /var/log/pods/ 下的真实文件。Fluent Bit 通过 tail 插件监控该目录即可。以下是一段精简的 fluent-bit.conf 配置:
[INPUT]
Name tail
Path /var/log/containers/*.log
Parser docker
Tag kube.*
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
[OUTPUT]
Name es
Match *
Host elasticsearch.ipipp.com
Port 9200
Index logs
如果某些遗留系统必须将日志写到文件,那么应在容器中挂载 emptyDir 或 hostPath 卷,并把采集器配置为同时读取该挂载路径。但要小心磁盘压力,务必在应用层或采集层配置日志轮转(logrotate 或应用内 RollingFileAppender)。另外,在多租户集群里,应限制每个命名空间的日志量配额,避免某一个服务把 Elasticsearch 打爆。
三、多行日志与结构化输出的处理技巧
现实中的错误堆栈、JSON 报文经常跨越多行,而容器日志默认按行切割。如果不处理,一条异常会被拆成几十条独立记录,严重干扰检索。解决思路是在采集侧做多行合并,例如 Fluent Bit 的 multiline 插件可识别以时间或级别开头的行作为新日志起点。应用本身也可以改为输出单行 JSON,彻底规避该问题。
结构化日志指的是每条日志都是带字段的 JSON 对象,如 {"level":"error","msg":"db timeout","ts":1710000000}。相比纯文本,结构化数据能被后端直接索引特定字段。下面展示一段 Go 语言使用 zap 库输出 JSON 日志到 stdout 的写法:
package main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("service start",
zap.String("module", "order"),
zap.Int("pid", 1234),
)
logger.Error("db timeout",
zap.String("sql", "select * from t"),
)
}
最后提醒,不要在容器里跑 ssh 或手动 tail 看日志,这既不安全也不符合自动化理念。把日志规范做好,配合告警规则,才能在故障发生时第一时间定位根因。无论语言栈如何变化,面向标准流、集中采集、结构化这三步都是容器内应用日志最值得落地的实践。
容器日志stdout_stderr日志采集修改时间:2026-08-17 07:04:14