从‘它怎么又挂了’到‘服务稳如狗’:我是如何用Docker给老旧Python项目续命的

三年前接手公司那个祖传的Python项目时,每次部署都像在拆定时炸弹。requirements.txt里躺着37个没有版本号的依赖包,新同事的MacBook上永远跑不起来测试用例,而生产环境的CentOS 6服务器就像个即将退休的老兵——每次pip install都可能是压垮骆驼的最后一根稻草。直到我们把整个项目塞进Docker容器,那些令人抓狂的"Works on my machine"问题才真正成为历史。

1. 解剖老项目的典型病症

那个基于Flask 0.10开发的报表系统堪称技术债的活化石。某次服务器宕机后,我们尝试在新环境重建时发现了这些症状:

  • 依赖地狱matplotlib==2.0.2要求numpy>=1.7.0,而old-package==1.2.3却强制锁定numpy==1.6.1
  • 环境特异性:代码里充斥着if platform.system() == 'Darwin'这样的补丁
  • 配置漂移:生产环境的config.py有27处未提交的本地修改
# 典型的老项目启动方式
$ python -m pip install -r dubious_requirements.txt
$ export FLASK_ENV=magic_value_known_only_to_old_timers
$ nohup python run.py > /dev/null 2>&1 &

更可怕的是,当初搭建环境的工程师早已离职,那些隐式的系统依赖(比如某个特定版本的ImageMagick)根本没人说得清楚。我们花了整整两周才在新服务器上勉强跑起来,代价是不得不保留一台古董级的Ubuntu 14.04虚拟机作为"参考环境"。

2. 容器化改造的四个关键手术

2.1 构建确定性的Python环境

我们从最基础的Dockerfile开始,像考古学家一样还原原始环境:

FROM python:3.6.8-slim  # 锁定解释器版本

RUN apt-get update && apt-get install -y \
    libmagic-dev \       # 那些神秘的系统依赖
    ghostscript          # 老项目需要的图像处理库

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
    && pip freeze > pinned_requirements.txt  # 生成精确版本约束

这个简单的操作带来了立竿见影的效果:

  1. 开发团队终于可以用同一份pinned_requirements.txt复现完全相同的环境
  2. CI服务器不再因为隐式依赖而构建失败
  3. 新成员能在5分钟内启动开发环境,而不是折腾两天

2.2 处理顽固的本地配置文件

老项目最大的陷阱是那些散落在各处的硬编码路径和本地配置。我们采用多阶段构建策略:

# 开发阶段使用包含调试工具的镜像
FROM python:3.6.8 as dev
COPY . /app
WORKDIR /app
CMD ["flask", "run", "--host=0.0.0.0"]

# 生产阶段使用精简镜像
FROM python:3.6.8-slim as prod
COPY --from=dev /app /app
COPY --from=dev /app/config/prod.py /app/config.py  # 显式指定配置文件
WORKDIR /app
CMD ["gunicorn", "-w 4", "app:app"]

配合docker-compose管理不同环境变量:

services:
  app_dev:
    build:
      context: .
      target: dev
    volumes:
      - .:/app  # 开发时保持代码热加载
    environment:
      FLASK_ENV: development

  app_prod:
    build:
      context: .
      target: prod
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/prod

2.3 驯服有状态的服务

那个用SQLite做缓存的子系统是个典型的技术债。我们通过Docker卷实现持久化:

$ docker volume create legacy_cache
$ docker run -v legacy_cache:/app/cache my_legacy_app

对于必须保留的本地开发数据,我们建立了清晰的迁移指南:

  1. 备份旧环境的数据文件
  2. 创建专用的docker卷
  3. 编写初始化脚本将种子数据导入容器

2.4 渐进式改造策略

整个容器化过程我们采用分阶段上线:

阶段目标风险控制措施
1开发环境容器化保留旧虚拟机作为fallback
2CI流水线改用Docker构建并行运行新旧两种构建方式
3预发布环境部署容器配置流量镜像进行对比测试
4生产环境全量切换准备秒级回滚方案

3. 那些只有踩过坑才知道的事

千万别相信老项目的requirements.txt——我们实际发现:

  1. 有5个包从未被导入过但一直安装着
  2. 3个包实际上是通过easy_install装的
  3. pkg-resources==0.0.0是Ubuntu的bug产生的幽灵依赖

处理Windows特有的路径问题时,这个Dockerfile技巧救了命:

RUN sed -i 's/\r$//' *.sh  # 处理CRLF换行符
RUN find . -name "*.py" -exec dos2unix {} \;  # 统一Python文件换行

日志收集方面,我们放弃了传统的文件日志,改用:

import logging
import sys

logging.basicConfig(
    stream=sys.stdout,
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

这样在Kubernetes环境下可以直接用kubectl logs查看,也方便接入ELK等日志系统。

4. 从幸存到复兴的进阶技巧

当基础容器化完成后,我们开始给这个老项目注入现代工程实践:

依赖安全扫描

$ pip install safety
$ safety check --full-report

构建缓存优化

# 先安装依赖项(变化频率低)
COPY requirements.txt .
RUN pip install -r requirements.txt

# 再拷贝代码(变化频率高)
COPY . .

最小化镜像攻击面

FROM python:3.6.8-slim as runtime

RUN adduser --disabled-password --gecos "" appuser
USER appuser  # 不以root身份运行

COPY --chown=appuser:appuser . /app

现在这个"古董级"项目不仅能在现代云环境顺畅运行,还具备了蓝绿部署、自动扩缩容等现代能力。最让我欣慰的是,新来的实习生再也不用经历我们当年的"环境搭建地狱周"了——docker-compose up就是全部魔法。

更多推荐