从‘它怎么又挂了’到‘服务稳如狗’:我是如何用Docker给老旧Python项目续命的
从‘它怎么又挂了’到‘服务稳如狗’:我是如何用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 # 生成精确版本约束
这个简单的操作带来了立竿见影的效果:
- 开发团队终于可以用同一份
pinned_requirements.txt复现完全相同的环境 - CI服务器不再因为隐式依赖而构建失败
- 新成员能在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
对于必须保留的本地开发数据,我们建立了清晰的迁移指南:
- 备份旧环境的数据文件
- 创建专用的docker卷
- 编写初始化脚本将种子数据导入容器
2.4 渐进式改造策略
整个容器化过程我们采用分阶段上线:
| 阶段 | 目标 | 风险控制措施 |
|---|---|---|
| 1 | 开发环境容器化 | 保留旧虚拟机作为fallback |
| 2 | CI流水线改用Docker构建 | 并行运行新旧两种构建方式 |
| 3 | 预发布环境部署容器 | 配置流量镜像进行对比测试 |
| 4 | 生产环境全量切换 | 准备秒级回滚方案 |
3. 那些只有踩过坑才知道的事
千万别相信老项目的requirements.txt——我们实际发现:
- 有5个包从未被导入过但一直安装着
- 3个包实际上是通过
easy_install装的 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就是全部魔法。
更多推荐
所有评论(0)