PHP 专家级迁移指南:论如何在不停机的前提下完成从 Windows 物理机向云原生容器集群的平滑过渡

PHP 专家级迁移指南:论如何在不停机的前提下完成从 Windows 物理机向云原生容器集群的平滑过渡

各位开发者的同仁们,各位渴望自由的架构师们,大家好。

今天我们要聊的话题,听起来像是一句老板的画饼:“咱们把那几台嗡嗡作响、灰尘飞舞的 Windows 物理服务器都撤了吧,换成云原生集群,代码一部署就上线,既省钱又环保。”

这听起来很美,对吧?就像闻到了自由女神像的烤面包香气。

但现实是什么?现实是你的应用跑在 IIS 上,PHP 版本卡在 7.4,数据库连不上,代码里还有几个 require_once 'C:Program Files...' 这种写法硬编码的坑。现在,你要在不关站、不丢数据、不吓坏产品经理的前提下,把这套“老爷车”换成“超跑”。

别慌,这不仅是迁移,这是一场手术。我们要用最精密的器械,在病人(业务)活着的时候完成心脏移植。

让我们开始吧。


第一章:诊断——为什么我们要在这个“沼泽”里挣扎?

首先,我们得认清现实。你的那些 Windows 物理机,可能是公司里最“长寿”的资产。

  • 环境漂移: 开发同事改个配置,测试环境就炸,生产环境就跑不通。为什么?因为环境不一致。
  • 资源浪费: 你有一台 16 核 32G 的物理机,上面跑着三个 PHP 网站,CPU 占用率常年 5%。就像你开着法拉利去买菜,还把后备箱塞满了砖头。
  • 部署地狱: 上线?那是每两周一次的狂欢。你需要登录服务器,手动备份数据库,复制文件,杀进程,重启 IIS。只要哪一步手抖了,全站宕机。

我们的目标很简单:容器化。把你的 Windows 物理机变成一个 Docker Host,然后把这些容器塞进 Kubernetes (K8s) 的怀抱。

第一步:拥抱 Docker(但这很痛)

如果你现在还在用传统的 php-cgi.exe 配合 IIS,或者手动配置 Apache,恭喜你,你的 PHP 程序已经不懂得什么是“依赖”。我们现在要把代码变成镜像。

还记得那个经典的 docker-compose.yml 吗?这是连接旧世界的桥梁。

# docker-compose.yml (迁移前的第一步)
version: '3.8'
services:
  app:
    image: php:8.2-fpm  # 别再用 windows/php 镜像了,那是核废料
    volumes:
      - ./:/var/www/html
      - ./docker/php.ini:/usr/local/etc/php/conf.d/custom.ini
    restart: always
  # 如果你的数据库还在老物理机上,这里得保持连接
  # db:
  #   image: mysql:5.7

看,这里我们用了一个 php:8.2-fpm 镜像。为什么不用 Windows 原生的容器镜像?因为 Linux 容器生态才是今天的“红海”。Windows 容器通常需要 Hyper-V,启动慢,占用资源大。对于 PHP 这种解释型语言,Linux 容器在性能和兼容性上是碾压级的。

你的 Windows 物理机现在变成了一个简单的“Docker 守门员”。 你可以在上面跑 Nginx 或者 Apache,但 PHP-FPM 必须是 Linux 的。这是为了以后能顺利上 K8s 做铺垫。

第二章:基础设施——别把拖拉机开上高速公路

当你把 PHP 应用跑在 Docker 里了,你会有一种错觉:“我有 Docker 了,我有云了。” 然后你直接在物理机上跑 kubectl apply -f deployment.yaml,结果发现 Kubernetes 看不懂你的 Windows 路径。

核心原则:不要把 Kubernetes 裸奔在物理机上。 即使是 Docker Desktop(Mac/Windows 版),也只是一个单节点的伪 K8s。

我们要搭建一个真正的云原生集群。如果你预算有限,可以用 MinikubeKind 在你的 Windows 物理机上起一个集群来测试(这叫“影子集群”)。如果你有预算,上阿里云 ACK、AWS EKS 或者腾讯云 TKE。

但既然是迁移,假设我们要“空手套白狼”,我们可以利用现有的 Windows 物理机搭建一个轻量级集群,或者先把环境弄到云端。

构建你的 CI/CD 管道(流水线)

这是最关键的一步。以前你上线是“睡衣模式”(手动部署),现在我们要变成“西装模式”(自动化)。

我们需要一个构建服务器,建议用 Linux 的 Jenkins 或者 GitLab CI。

# .gitlab-ci.yml 示例
stages:
  - build
  - deploy

build_job:
  stage: build
  image: docker:24.0
  services:
    - docker:dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t registry.company.com/my-php-app:$CI_COMMIT_SHA .
    - docker push registry.company.com/my-php-app:$CI_COMMIT_SHA

deploy_job:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/my-php-app app=registry.company.com/my-php-app:$CI_COMMIT_SHA --namespace=production
  only:
    - main

听我说,这行代码 kubectl set image 就是你的“暂停键”。
如果这条命令失败了,K8s 会回滚到上一个版本。这意味着,即使你犯了错,生产环境也不会挂。这就是“不停机迁移”的底气。

第三章:迁移策略——“走钢丝”的艺术

现在,我们准备好了镜像,也准备好了 K8s。我们要怎么做?

错误做法:

  1. 在老物理机上复制代码。
  2. 修改 K8s 配置,把流量导到新容器。
  3. 关掉老物理机。

正确做法:双轨运行(Dual-Track Running)

想象一下,你开着一辆重卡,对面来了一辆赛车。你不能直接撞上去换人,你得慢慢超车。

在 K8s 里,我们要定义一个新的 Deployment。

# k8s-deployment-new.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-php-app-new
  namespace: production
spec:
  replicas: 1  # 先别扩容,1个副本测水
  selector:
    matchLabels:
      app: my-php-app
      version: v2  # 关键点:版本标记
  template:
    metadata:
      labels:
        app: my-php-app
        version: v2
    spec:
      containers:
      - name: app
        image: registry.company.com/my-php-app:v2.0.0  # 新镜像
        ports:
        - containerPort: 9000

操作流程:

  1. 部署新副本: 执行 kubectl apply -f k8s-deployment-new.yaml。现在,你的集群里有 v1(旧版)和 v2(新版)两个版本。它们都在跑。

  2. 验证健康状态:
    kubectl get pods -n production -w
    等待 v2 的 Pod 变成 RunningREADY 为 1/1。

  3. 调整流量(金丝雀发布):
    这是高光时刻。我们需要修改 Service 的配置,或者使用 Ingress Controller。

    如果用简单的 Service,K8s 会做轮询。

    apiVersion: v1
    kind: Service
    metadata:
      name: my-php-app-service
    spec:
      selector:
        app: my-php-app  # 注意这里,它同时匹配 v1 和 v2
      ports:
      - port: 80

    此时,流量会 50/50 分配给 v1 和 v2。你可以通过监控工具看 v2 的错误率。如果 v2 没报错,流量可以加到 80%。

    如果你有 Ingress(比如 Nginx Ingress),可以用权重控制:

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: my-php-app-ingress
      annotations:
        nginx.ingress.kubernetes.io/canary: "true"
        nginx.ingress.kubernetes.io/canary-weight: "10" # 初始只有10%流量
    spec:
      rules:
      - host: api.example.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-php-app-new
                port:
                  number: 80

    你会发现,虽然新 Pod 启动了,但只有 10% 的用户在用它。

  4. 用户无感知: 前端负载均衡器(可能是阿里云 SLB 或 AWS ALB)通常是无状态的,所以流量切换不需要停机。

第四章:数据持久化——不要把数据库忘在旧车里

这是新手最容易踩的坑。

在 Windows 物理机上,你的数据库通常在 C:Data 或者 D:MSSQL。迁移时,很多开发者直接把数据文件拷贝到新容器里。

大错特错!

容器是临时的。如果你不挂载 Volume,容器重启或重建时,数据就没了。而且,Windows 文件权限(如 IIS_IUSRS)在 Linux 容器里是行不通的。

正确姿势:使用 PVC (Persistent Volume Claim)

在 K8s 里,我们要把数据库也容器化(除非你的数据库极其复杂,迁移成本太高,那只能用网络直连)。

假设我们迁移 MySQL。

# mysql-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
# mysql-deployment.yaml
apiVersion: apps/v1
kind: StatefulSet  # 注意是 StatefulSet,不是 Deployment
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "password"
        ports:
        - containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

迁移步骤:

  1. 创建新的 MySQL PVC。
  2. 启动新的 MySQL 容器。
  3. 导入数据: 使用 kubectl cp 或者通过初始化脚本。
    # 将旧物理机上的 dump.sql 拷贝进新 Pod
    kubectl cp old-server-backup.sql mysql-0:/tmp/dump.sql
    # 进入容器执行
    kubectl exec -it mysql-0 -- mysql -u root -p -e "CREATE DATABASE mydb; SOURCE /tmp/dump.sql;"
  4. 修改应用配置: 把应用配置里的数据库地址从 192.168.1.100 改成 mysql-service(K8s Service 名)。
  5. 流量切换: 当应用连上新数据库成功,且测试通过后,把流量切过来。

第五章:PHP 特有的“排雷”指南

PHP 是一种极其灵活,但也极其容易“烂尾”的语言。在容器化迁移中,以下几个问题是 PHP 程序员的噩梦:

1. 文件权限地狱
在 Windows 上,file_put_contents 通常没问题。在 Linux 容器里,PHP-FPM 进程是以 www-data(UID 33)运行的。如果你的代码试图写入一个没有权限的文件,就会 500 报错。

解决方案:
在 Dockerfile 里,不要吝啬 chownchmod

FROM php:8.2-fpm
# ... 安装扩展 ...
RUN chown -R www-data:www-data /var/www/html
RUN chmod -R 755 /var/www/html
# 如果需要写权限,目录设为 775
RUN chmod -R 775 /var/www/html/storage

2. 扩展丢失
在旧物理机上,可能安装了一些奇奇怪怪的扩展,比如 php_rdkafka.so 或者 php_igbinary.so。你在本地环境跑得欢,一到 K8s 就报错 Class 'Redis' not found

解决方案:
必须把扩展依赖写在 composer.json 里,并且明确指定 PHP 镜像的版本。

FROM php:8.2-fpm
RUN docker-php-ext-install pdo_mysql mysqli
# 如果需要 Redis
RUN pecl install redis && docker-php-ext-enable redis

不要试图去调试 K8s 环境下的缺失扩展,那会让你怀疑人生。直接在 Dockerfile 里补齐。

3. 时间戳问题
Windows 的时间是本地时间(CST),K8s 容器通常是 UTC。如果你的业务逻辑依赖时间戳做缓存过期或者定时任务,你会发现“时钟漂移”。
解决方案:
容器里设置时区。

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

第六章:性能与监控——别上线后再去“数羊”

平滑迁移不仅仅是把东西移过去就完事了。新环境要跑得比旧环境快,否则老板会问:“我花了那么多钱买服务器,结果跟以前一样慢?”

1. 资源限制
不要给容器无限的 CPU 和内存。在 Windows 物理机上,你可能习惯于“跑满它”。但在 K8s 里,这会导致集群“饿死”。

resources:
  limits:
    cpu: "500m"
    memory: "512Mi"
  requests:
    cpu: "250m"
    memory: "256Mi"

requests 是调度器决定把 Pod 放在哪台机器上的依据。如果你的 Pod 请求 2 核,但物理机只有 1 核,K8s 会把你的 Pod 杀掉。这就是自动扩缩容(HPA)的前置条件。

2. 健康检查(Liveness & Readiness)
这是保证“不停机”的最后一道防线。K8s 需要知道你的 PHP 应用是不是“死”了。

livenessProbe:
  httpGet:
    path: /health.php  # 你得写这个接口
    port: 9000
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /status.php
    port: 9000
  initialDelaySeconds: 5
  periodSeconds: 5

如果 livenessProbe 失败,K8s 会重启你的容器(这叫自愈)。如果 readinessProbe 失败,K8s 会把这个容器从 Service 里摘除,停止向它转发流量。这能防止“屎山代码”拖垮整个集群。

第七章:最后的清扫——拔掉插头

经过前面的步骤,你的新集群已经稳定运行了。

  • 旧物理机上的 v1 版本还在跑,流量很少。
  • 新集群上的 v2 版本跑得很顺,数据库同步好了,代码也没 Bug。
  • 监控大屏上,指标一切正常。

现在是时候做“大扫除”了。

  1. 流量完全切走: 修改 Ingress 配置,把 canary-weight 设为 100,或者把 Service selector 改回只指向新版本。
  2. 验证: 打开浏览器,刷新几次,检查 API 响应。
  3. 下线旧 Pod:
    # 查看 v1 版本的 Pod
    kubectl get pods -n production -l version=v1
    # 删除它们(或者 K8s 会自动回收,因为 Service 不再指向它们)
    kubectl delete pod <old-pod-name> -n production
  4. 关停旧物理机: 把那台轰鸣的 Windows 物理机断电。关机的那一刻,你会听到一声清脆的“咔哒”,那是自由的回响。

结语:拥抱变化,哪怕它很臭

从 Windows 物理机迁移到云原生容器集群,听起来很科幻,做起来很枯燥。它涉及大量的 YAML 配置、Dockerfile 编写、网络调优和逻辑验证。

但这不仅仅是一次技术迁移,更是一次思维的升级。你正在从一名“操作员”进化为一名“架构师”。你不再关心服务器在哪,你关心的是“代码交付”。

如果在这个过程中,你遇到了 Permission denied,不要骂娘;如果遇到了 Connection refused,不要去砸键盘。打开你的 kubectl logs,检查你的 Service,看看你的 Dockerfile。

记住,云原生不是一种技术,它是一种生活方式。上帝在云端写代码,我们在 Kubernetes 里跑程序。

祝你们迁移顺利,服务器永远高可用!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注