为什么你的 Python 日志在 Docker 中消失(以及 PYTHONUNBUFFERED=1 如何拯救局面)

发布: (2025年12月26日 GMT+8 22:55)
7 min read
原文: Dev.to

Source: Dev.to

演示应用

我创建了一个简洁的 Python 小程序。它运行良好,在每一步都会打印有用的日志:

# app.py
print("Starting application...")
print("Connecting to database...")
print("Processing request...")
print("All done!")

在本地运行它:

$ python app.py
Starting application...
Connecting to database...
Processing request...
All done!

一切正常!我把它容器化:

FROM python:3.12-slim
COPY app.py .
CMD ["python", "app.py"]

在 Docker 中构建并运行:

$ docker build -t myapp .
$ docker run myapp
$ docker logs 
# (no output)

但是日志却找不到了。它们去哪儿了?如果在 Kubernetes 上运行,kubectl logs 也显示同样的情况——没有日志。

日志就像魔术师的戏法,凭空消失了。这是何种魔法?

剧透:这是 Python 的输出缓冲导致的,自从 Docker 流行以来一直让开发者感到沮丧。

什么是输出缓冲?

输出缓冲是指程序并不立即将输出写入目标(终端、Docker 日志等),而是先将输出收集到一个临时缓冲区(内存块)中,待一次性写出。

Without buffering

[Your code] ---> "Hello" ---> [Terminal]
            ---> "World" ---> [Terminal]
            ---> "!!!"   ---> [Terminal]

With buffering

[Your code] ---> "Hello" --\
            ---> "World" ---}--[Buffer]---> [Terminal] (when full or flushed)
            ---> "!!!"   --/

为什么会有缓冲

缓冲是一种性能优化。相较于内存操作,向输出(终端、文件、网络)写入是 的。

缓冲的三种类型

Python(以及大多数语言)使用三种缓冲模式:

Mode使用场景行为
Unbuffered默认情况下为 stderr每次 print() 都会立即显示
Line‑bufferedstdout 在连接到终端(TTY)时在每个换行符 (\n) 后输出
Block‑bufferedstdout 未连接到终端(管道、文件、Docker)时输出以块的形式出现(默认 8 KB),或在程序退出时

Python 会检测 stdout 是否连接到终端。如果是,它使用行缓冲;否则使用块缓冲。

想查看实际的缓冲区大小吗? 您可以自行检查:

import io
print(f"Default buffer size: {io.DEFAULT_BUFFER_SIZE} bytes")
# Output: Default buffer size: 8192 bytes

io.DEFAULT_BUFFER_SIZE(在 Python 的 io 模块 中定义)是 Python 用于块缓冲的大小。通常为 8192 字节(8 KB),但可能会根据系统的块大小而变化。

为什么 Docker 会让情况更糟

当你在 Docker 容器中运行 Python 时:

  • stdout 不是 TTY(它是指向 Docker 日志驱动的管道)。
  • 因此 Python 会切换为 块缓冲(默认 8 KB 缓冲区)。

日志会一直保存在内存中,直到:

  • 缓冲区填满,
  • 程序退出,
  • 你显式地刷新。

这就是典型的 “在我的机器上可以工作” 的案例——你的本地机器是 TTY,而 Docker 不是。

解决方案:PYTHONUNBUFFERED=1

PYTHONUNBUFFERED 环境变量设置为任意非空值。这会强制 Python 对 stdoutstderr 使用无缓冲模式。

在 Dockerfile 中

FROM python:3.12-slim

# 添加此行!
ENV PYTHONUNBUFFERED=1

COPY app.py .
CMD ["python", "app.py"]

docker‑compose.yml

version: '3.8'
services:
  app:
    build: .
    environment:
      - PYTHONUNBUFFERED=1

在 Kubernetes Deployment 中

apiVersion: apps/v1
kind: Deployment
metadata:
  name: python-app
spec:
  template:
    spec:
      containers:
        - name: app
          image: myapp:latest
          env:
            - name: PYTHONUNBUFFERED
              value: "1"

运行时

docker run -e PYTHONUNBUFFERED=1 myapp

现在日志会立即出现:

$ docker logs myapp
Starting application...
Connecting to database...
Processing request...
All done!

有趣的事实:值本身并不重要。PYTHONUNBUFFERED=1PYTHONUNBUFFERED=true,甚至 PYTHONUNBUFFERED=banana 都可以工作。Python 只检查该变量是否已设置且非空。

替代方案

使用 Python 的无缓冲命令行选项

python -u app.py

手动刷新

print("Processing request...", flush=True)

import sys
print("Processing request...")
sys.stdout.flush()

在 Python 的 logging 模块中使用 stderr

import logging
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logging.info("Processing request...")

结论

当 Python 在容器中运行时,其 stdout 为块缓冲,这会导致日志在缓冲区填满或进程结束前消失。设置 PYTHONUNBUFFERED=1(或使用 -u、手动刷新,或使用 stderr)可关闭缓冲,从而恢复实时日志可见性。

参考文献

替代方案

虽然 PYTHONUNBUFFERED=1 是最简单的解决方案,但还有其他方法:

使用 Python 无缓冲命令行选项

-u 标志强制使用无缓冲模式:

FROM python:3.12-slim
COPY app.py .
CMD ["python", "-u", "app.py"]  # Note the -u flag

这等同于 PYTHONUNBUFFERED=1,只是更明确。

优点

  • 不需要环境变量

缺点

  • 必须记得在每次运行 Python 时添加 -u

手动刷新

在需要输出出现时显式调用 flush()

import sys

print("Critical log message", flush=True)  # Appears immediately

# Or flush everything:
sys.stdout.flush()

优点

  • 对何时刷新拥有细粒度控制

缺点

  • 容易忘记
  • 使代码变得凌乱
  • 对使用 print() 的第三方库无效

在 Python 的 logging 模块中使用 stderr

stderr 默认是无缓冲的。

import logging
import sys

# Configure logging to stderr (which is unbuffered by default)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    stream=sys.stderr  # stderr is unbuffered!
)

logger = logging.getLogger(__name__)

logger.info("Application starting")
logger.info("Connected to database")
logger.error("Something went wrong!")

为什么这样有效

  • stderr 默认是无缓冲的(即使在 Docker 中)
  • 结构化日志本身就更适合生产环境
  • 你可以获得时间戳、日志级别以及更好的格式化

结论

PYTHONUNBUFFERED=1 环境变量是一个微小的修复,却能解决巨大的麻烦。它强制 Python 停止对输出进行缓冲,从而使你的日志能够在 Docker 和 Kubernetes 中立即显示。

参考文献

本文最初发布在我的博客 why‑your‑python‑logs‑vanish‑in‑docker‑pythonunbuffered‑explained

在我的博客上可以看到更多内容,我会分享我的软件开发经验和学习体会: wewake.dev

Back to Blog

相关文章

阅读更多 »

Python 日志:从 print() 到生产

使用 print 的问题:python printf'Processing user {user_id}' printf'Error: {e}' 缺少的内容:- 没有时间戳 - 没有日志级别 - 没有文件输出 - Can't fil...