一、为什么容器日志管理这么麻烦?

当我们在使用Docker跑应用的时候,经常会遇到这样的困扰:日志去哪了?怎么查看?为什么突然就占满磁盘了?这就像把东西都装进集装箱后,突然发现找不到装箱单一样让人头疼。

传统虚拟机时代,日志通常老老实实地躺在/var/log目录下。但容器是临时性的,它的文件系统也是临时的,这就带来了三个典型问题:

  1. 容器重启后日志就消失了
  2. 多个容器的日志分散在各处
  3. 日志量快速增长可能撑爆磁盘

举个例子,我们有个简单的Node.js应用:

// 技术栈:Node.js + Docker
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    console.log('有人访问了首页'); // 这条日志会去哪?
    res.send('欢迎光临');
});

app.listen(3000, () => {
    console.log('服务已启动'); // 这个又去哪了?
});

如果直接docker run启动这个容器,这些console.log输出的日志默认会存在容器内部。一旦容器停止,日志就跟着消失了。

二、Docker日志的四种基础处理方式

1. 最偷懒的做法 - 什么都不做

直接使用Docker默认的json-file日志驱动,这是新手最容易掉进的坑。日志会堆积在/var/lib/docker/containers/目录下,直到有一天磁盘报警。

# 查看容器日志(当容器还在运行时)
docker logs <容器ID>

# 查看日志文件位置(需要root权限)
sudo ls -lh /var/lib/docker/containers/<容器ID>/<容器ID>-json.log

2. 稍微好点 - 限制日志大小

在docker run时加上日志大小限制:

docker run --log-opt max-size=10m --log-opt max-file=3 my-app

这样单个日志文件最大10MB,最多保留3个文件。虽然治标不治本,但至少不会让日志把磁盘撑爆。

3. 推荐做法 - 把日志输出到标准输出

Docker最佳实践是让应用把日志直接打到stdout和stderr,然后由Docker接管:

// 修改后的Node.js应用
const express = require('express');
const morgan = require('morgan'); // 专门用来记录HTTP访问日志的中间件

const app = express();
app.use(morgan('combined')); // 使用Apache标准格式输出到stdout

app.get('/', (req, res) => {
    console.log('有人访问了首页'); // 现在会输出到stdout
    res.send('欢迎光临');
});

4. 专业方案 - 使用日志收集系统

当你有多个容器时,就需要像ELK、Fluentd这样的专业工具了。这里以Fluentd为例:

# docker-compose.yml示例
version: '3'
services:
  app:
    image: my-node-app
    logging:
      driver: "fluentd"
      options:
        fluentd-address: localhost:24224
        tag: docker.my-app
  
  fluentd:
    image: fluent/fluentd
    ports:
      - "24224:24224"
    volumes:
      - ./fluentd.conf:/fluentd/etc/fluent.conf

三、生产环境日志收集实战

场景1:单机多容器日志收集

对于中小规模部署,可以使用Docker的syslog驱动:

# 启动一个测试容器
docker run --log-driver=syslog --log-opt syslog-address=udp://localhost:514 busybox echo "这条日志会发往syslog"

# 常见的syslog服务器配置(以rsyslog为例)
# 在/etc/rsyslog.conf中添加:
module(load="imudp")
input(type="imudp" port="514")

场景2:分布式集群日志方案

Kubernetes环境下更推荐使用Fluentd+Elasticsearch的组合:

# Fluentd配置示例(fluentd.conf)
<source>
  @type forward
  port 24224
</source>

<match docker.**>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
  logstash_prefix docker
</match>

场景3:日志预处理妙招

有时候我们需要在日志进入存储系统前先做些处理:

# 使用Fluentd的grep过滤器
<filter docker.my-app>
  @type grep
  <exclude>
    key log
    pattern /healthcheck/ # 过滤掉健康检查日志
  </exclude>
</filter>

四、避坑指南与高级技巧

1. 时区问题

很多开发者发现日志时间不对,这是因为容器默认使用UTC时区:

# 解决方案:在Dockerfile中设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

2. 敏感信息过滤

日志中经常不小心包含密码、token等敏感信息:

// 不好的写法
console.log(`用户登录使用了密码:${password}`);

// 应该这样
console.log('用户登录成功', { username: user.name });

3. 结构化日志

相比普通文本,结构化日志更利于后续分析:

// 使用JSON格式日志
const winston = require('winston');
const logger = winston.createLogger({
  format: winston.format.json(),
  transports: [new winston.transports.Console()]
});

logger.info('用户操作', {
  action: 'login',
  user: '张三',
  ip: '192.168.1.100'
});

4. 日志分级策略

建议采用以下分级标准:

  • ERROR:需要立即处理的错误
  • WARN:需要注意但不是错误的情况
  • INFO:重要的运行时事件
  • DEBUG:调试信息
# 启动容器时设置日志级别
docker run -e "LOG_LEVEL=debug" my-app

五、该选择哪种方案?

根据不同的场景,我总结了这个决策表:

场景 推荐方案 优点 缺点
开发环境 控制台输出 简单直接 无法持久化
单机生产 syslog驱动 系统原生支持 功能较基础
微服务集群 Fluentd+ES 功能强大 部署复杂
云环境 云厂商日志服务 开箱即用 可能有费用

最后给个忠告:不要等到线上出问题了才想起来看日志。好的日志系统应该像汽车仪表盘,随时告诉你系统运行的健康状况。

六、实战:从零搭建日志系统

让我们用一个完整的例子结束本文。假设我们有个电商系统,包含:

  1. 前端Node.js服务
  2. 后端Java服务
  3. MySQL数据库
# docker-compose.yml
version: '3'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.9.2
    environment:
      - discovery.type=single-node
    ports:
      - "9200:9200"
  
  kibana:
    image: docker.elastic.co/kibana/kibana:7.9.2
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
  
  fluentd:
    build: ./fluentd
    ports:
      - "24224:24224"
    volumes:
      - ./fluentd/conf:/fluentd/etc
    depends_on:
      - elasticsearch
  
  frontend:
    build: ./frontend
    logging:
      driver: "fluentd"
      options:
        fluentd-address: fluentd:24224
        tag: docker.frontend
  
  backend:
    build: ./backend
    logging:
      driver: "fluentd"
      options:
        fluentd-address: fluentd:24224
        tag: docker.backend

对应的Fluentd配置:

# fluentd/conf/fluent.conf
<source>
  @type forward
  port 24224
</source>

<filter docker.**>
  @type record_transformer
  <record>
    host "#{Socket.gethostname}"
    service ${tag}
  </record>
</filter>

<match docker.**>
  @type elasticsearch
  hosts elasticsearch
  logstash_format true
  logstash_prefix docker
  include_tag_key true
</match>

这样我们就搭建了一个完整的日志收集系统,所有服务的日志都会:

  1. 通过Fluentd收集
  2. 存储到Elasticsearch
  3. 可以在Kibana中可视化查看

记住,好的日志系统不是一天建成的。建议从小规模开始,逐步完善。最重要的是要让日志真正用起来,而不是收集完就束之高阁。