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

维护一个运行了十年的PHP项目就像照顾一位脾气古怪的老教授——你知道他肚子里有货,但那些过时的习惯和依赖总能让你在深夜崩溃。上周五下午4点,当我第17次收到"生产环境500错误"的告警时,终于下定决心给这个运行在CentOS 6上的"老古董"做个全面改造手术。

1. 为什么Docker是老旧项目的急救室

2012年部署的PHP 5.3应用,依赖着三个已经停止维护的PECL扩展,配置文件散落在/etc/php.d和项目根目录之间——这场景对很多维护遗产系统的开发者来说再熟悉不过。传统虚拟机迁移就像给危房刷墙漆,而Docker提供的环境固化能力才是真正的结构加固。

去年接手这个电商后台系统时,光是搭建开发环境就花了三天。各种隐式的依赖关系就像地雷:需要特定版本的ImageMagick、必须手动编译的memcached扩展、对旧版MySQL客户端库的依赖...直到某次yum update意外升级了OpenSSL,整个系统直接罢工八小时。

关键痛点解决矩阵

传统环境问题Docker解决方案实施收益
依赖冲突独立镜像层隔离可同时运行不同PHP版本
环境差异镜像固化开发/生产环境消除"在我机器正常"问题
扩展管理困难多阶段构建整合编译步骤一次构建多处运行
配置散落环境变量+配置文件卷挂载配置与代码分离
升级风险回滚只需切换镜像标签秒级恢复稳定版本

提示:对于特别老旧的glibc依赖,可以考虑使用CentOS 6基础镜像或静态编译方案

2. 构建生存指南:从混乱到秩序的Dockerfile实战

第一次尝试构建镜像时,我天真地以为直接FROM php:5.3-apache就能搞定。现实很快给了教训——现代Docker Hub上的PHP镜像早已不维护这么老的版本,而自己从源码编译PHP 5.3需要处理二十多个依赖项。

经过两周的试错,最终形成的Dockerfile结构如下:

# 使用官方CentOS 6基础镜像作为时间胶囊
FROM centos:6.10 AS builder

# 还原2012年的编译环境
RUN yum -y install \
    gcc-4.4.7 \
    make-3.81 \
    libxml2-devel-2.7.6 \
    # 共17个显式指定版本的依赖包
    && rm -rf /var/cache/yum

# 从源码编译PHP 5.3.28
ADD php-5.3.28.tar.gz /tmp/
RUN cd /tmp/php-5.3.28 && \
    ./configure --prefix=/opt/php53 \
        --with-apxs2=/usr/sbin/apxs \
        --with-mysql=mysqlnd \
        # 保持与原环境完全一致的编译参数
    && make -j4 && make install

# 构建最终运行时镜像
FROM centos:6.10
COPY --from=builder /opt/php53 /opt/php53

# 安装项目特定的PECL扩展
RUN pecl install -f memcached-1.0.2 \
    && echo "extension=memcached.so" > /etc/php.d/20-memcached.ini

# 还原原始配置文件
COPY php.ini /opt/php53/lib/php.ini
COPY httpd.conf /etc/httpd/conf/httpd.conf

# 设置符合现代Docker习惯的入口点
ENTRYPOINT ["/opt/php53/bin/php"]

这个多阶段构建方案解决了几个关键问题:

  1. 构建环境与运行时环境分离,最终镜像不包含编译工具链
  2. 精确复现了原始环境的编译参数和扩展版本
  3. 通过分层保持基础环境的可复用性

常见踩坑点处理清单

  • 文件权限问题:在Dockerfile中提前创建好/var/www/html并设置正确的www-data权限
  • 时区配置:老版本PHP需要手动拷贝/usr/share/zoneinfo文件
  • 日志收集:将php_error_log重定向到stdout便于Docker日志采集
  • 会话存储:用volume挂载/var/lib/php/session实现多容器共享

3. 让老项目学会新把戏:现代化部署技巧

把古董PHP应用塞进Docker只是第一步,要让这个系统真正稳定运行,还需要给它装上一些现代基础设施的"假肢"。经过三个迭代周期,我们的部署架构变成了这样:

# 使用docker-compose编排多个服务
version: '3.7'
services:
  legacy_app:
    build: .
    ports:
      - "8080:80"
    volumes:
      - ./src:/var/www/html
      - php_sessions:/var/lib/php/session
    depends_on:
      - redis
      - mysql

  redis:
    image: redis:5-alpine
    command: ["redis-server", "--save 60 1", "--loglevel warning"]

  mysql:
    image: mysql:5.5
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  php_sessions:
  mysql_data:

性能调优关键参数对比

参数项原物理机配置Docker优化方案效果提升
PHP内存限制128M256M (配合cgroup限制)减少OOM
MySQL连接池100150 (独占容器资源)QPS+35%
Session存储本地文件Redis集群延迟-60%
日志采集每日轮转文件Fluentd+ELK排查提速
静态资源Apache处理Nginx反向代理缓存吞吐量2x

注意:老版本PHP对OPcache的支持有限,建议使用较新的apcu扩展替代

4. 从持续救火到主动防御:监控体系改造

给老系统装上Docker就像给自行车加装ABS——基础架构现代化了,但还需要配套的监控系统才能避免翻车。我们逐步建立了三层监控防护网:

  1. 容器基础监控

    # 使用cAdvisor+Prometheus收集基础指标
    docker run \
      --volume=/:/rootfs:ro \
      --volume=/var/run:/var/run:ro \
      --publish=8081:8080 \
      --detach=true \
      --name=cadvisor \
      google/cadvisor:v0.36.0
    
  2. 应用层健康检查

    // healthcheck.php
    header('Content-Type: application/json');
    $status = [
        'mysql' => check_mysql(),
        'redis' => check_redis(),
        'disk' => disk_free_space('/') > 1073741824,
        // 其他关键依赖检查
    ];
    echo json_encode($status);
    
  3. 业务指标埋点

    # 在Nginx配置中添加日志格式
    log_format legacy_log '$remote_addr - $request_time $upstream_status '
                         '$request_method $request_uri $bytes_sent';
    

监控看板关键指标

指标类型采集方式告警阈值应对措施
容器内存cAdvisor>90%持续5分钟垂直扩展或优化GC参数
PHP进程阻塞Blackbox探测请求延迟>2s检查慢查询或外部API调用
MySQL连接数Prometheus exporter使用率>80%增加连接池或优化查询
业务错误码日志正则提取5xx错误率>1%立即回滚或热修复
Session丢失率Redis监控>0.1%检查集群状态或切换存储

5. 给技术债加上利息:渐进式重构策略

Docker化只是技术债重组的第一步。我们采用"外科手术式"的渐进重构方案:

  1. 外围剥离:先将静态资源、定时任务等非核心功能移出主容器

    # 专门处理cron任务的镜像
    FROM legacy_app_base
    COPY cron /etc/cron.d/
    CMD ["cron", "-f"]
    
  2. 依赖升级:在隔离环境中逐步测试PHP 7.x兼容性

    # 使用多版本并行测试
    docker run -it --rm php:7.4-cli \
        php -l /mnt/src/index.php
    
  3. 服务拆分:将支付、报表等模块改为独立微服务

    # 新服务的健康检查端点
    @app.route('/health')
    def health():
        return jsonify({
            'status': 'UP',
            'version': os.getenv('APP_VERSION')
        })
    

重构收益时间表

阶段耗时风险可测性提升部署速度变化
容器化2周30%从小时级到分钟级
监控1周70%无影响
剥离3天/模块每模块+15%滚动部署可行
升级1月极高90%需要双跑验证

每次部署时,我们都会在CI流水线中运行一套针对老版本的自动化测试:

# 回归测试脚本片段
docker-compose -f docker-compose.test.yml build
docker-compose -f docker-compose.test.yml run --rm \
    tester phpunit --bootstrap tests/bootstrap.php tests/

6. 经验沉淀:那些手册不会告诉你的实战技巧

三年间处理了四十多次凌晨告警后,我总结出这些保命经验:

文件权限最佳实践

  • 在Dockerfile中预创建所有需要的目录
  • 使用明确的UID/GID而非用户名
  • 对于上传目录,设置chmod 1777而非777
RUN mkdir -p /var/www/uploads \
    && chown 33:33 /var/www \
    && chmod 1777 /var/www/uploads

老版本PHP调优参数

; php.ini 关键修改
realpath_cache_size=256k
realpath_cache_ttl=3600
mysql.connect_timeout=3
default_socket_timeout=60

混合云部署方案

禁止使用mermaid图表,转为文字描述:
1. 开发环境使用本地Docker Desktop
2. 测试环境部署到Kubernetes最小集群
3. 生产环境采用经典Swarm模式
4. 所有环境共享同一镜像仓库

灾难恢复检查清单

  1. 定期导出数据库schema到版本库
  2. 将容器内配置文件挂载为configmap
  3. 保留最后一个已知好用的物理机快照
  4. 编写手工部署fallback脚本

关键教训:永远在容器里放一个busybox静态编译版本,当所有依赖都崩溃时,它是最后的调试工具

更多推荐