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。
我们要搭建一个真正的云原生集群。如果你预算有限,可以用 Minikube 或 Kind 在你的 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。我们要怎么做?
错误做法:
- 在老物理机上复制代码。
- 修改 K8s 配置,把流量导到新容器。
- 关掉老物理机。
正确做法:双轨运行(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
操作流程:
-
部署新副本: 执行
kubectl apply -f k8s-deployment-new.yaml。现在,你的集群里有v1(旧版)和v2(新版)两个版本。它们都在跑。 -
验证健康状态:
kubectl get pods -n production -w
等待 v2 的 Pod 变成Running且READY为 1/1。 -
调整流量(金丝雀发布):
这是高光时刻。我们需要修改 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% 的用户在用它。
-
用户无感知: 前端负载均衡器(可能是阿里云 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
迁移步骤:
- 创建新的 MySQL PVC。
- 启动新的 MySQL 容器。
- 导入数据: 使用
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;" - 修改应用配置: 把应用配置里的数据库地址从
192.168.1.100改成mysql-service(K8s Service 名)。 - 流量切换: 当应用连上新数据库成功,且测试通过后,把流量切过来。
第五章:PHP 特有的“排雷”指南
PHP 是一种极其灵活,但也极其容易“烂尾”的语言。在容器化迁移中,以下几个问题是 PHP 程序员的噩梦:
1. 文件权限地狱
在 Windows 上,file_put_contents 通常没问题。在 Linux 容器里,PHP-FPM 进程是以 www-data(UID 33)运行的。如果你的代码试图写入一个没有权限的文件,就会 500 报错。
解决方案:
在 Dockerfile 里,不要吝啬 chown 和 chmod。
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。
- 监控大屏上,指标一切正常。
现在是时候做“大扫除”了。
- 流量完全切走: 修改 Ingress 配置,把
canary-weight设为 100,或者把 Service selector 改回只指向新版本。 - 验证: 打开浏览器,刷新几次,检查 API 响应。
- 下线旧 Pod:
# 查看 v1 版本的 Pod kubectl get pods -n production -l version=v1 # 删除它们(或者 K8s 会自动回收,因为 Service 不再指向它们) kubectl delete pod <old-pod-name> -n production - 关停旧物理机: 把那台轰鸣的 Windows 物理机断电。关机的那一刻,你会听到一声清脆的“咔哒”,那是自由的回响。
结语:拥抱变化,哪怕它很臭
从 Windows 物理机迁移到云原生容器集群,听起来很科幻,做起来很枯燥。它涉及大量的 YAML 配置、Dockerfile 编写、网络调优和逻辑验证。
但这不仅仅是一次技术迁移,更是一次思维的升级。你正在从一名“操作员”进化为一名“架构师”。你不再关心服务器在哪,你关心的是“代码交付”。
如果在这个过程中,你遇到了 Permission denied,不要骂娘;如果遇到了 Connection refused,不要去砸键盘。打开你的 kubectl logs,检查你的 Service,看看你的 Dockerfile。
记住,云原生不是一种技术,它是一种生活方式。上帝在云端写代码,我们在 Kubernetes 里跑程序。
祝你们迁移顺利,服务器永远高可用!