投递系统是招聘平台的核心链路,求职者投递简历、企业查看投递、面试流程都依赖它。旧版是 PHP 单体,性能瓶颈明显。2026年4月完成了投递系统从 PHP 到 Go 微服务的迁移,今天把完整迁移路径分享出来。
一、迁移策略#
不搞"大爆炸"式重写,用"绞杀者模式"逐步迁移:
- 第一阶段:Go 服务搭建,和 PHP 并行运行,Go 只做新功能
- 第二阶段:接口逐步从 PHP 迁移到 Go,Nginx 按接口路由
- 第三阶段:数据双写,PHP 和 Go 都写数据库,保证数据一致
- 第四阶段:灰度切换,按比例把流量从 PHP 切到 Go
- 第五阶段:全量切换,PHP 下线
二、接口兼容#
Go 服务的接口返回格式和 PHP 完全一致,前端不用改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 统一响应格式,和旧 PHP 接口一致
type Response struct {
Code int `json:"code"` // 0成功,非0失败
Message string `json:"message"`
Data interface{} `json:"data"`
}
func (h *ApplyHandler) Apply(c *gin.Context) {
var req ApplyRequest
c.ShouldBindJSON(&req)
// 业务逻辑
result, err := h.service.Apply(req.UserID, req.JobID)
if err != nil {
c.JSON(200, Response{Code: 1, Message: err.Error()})
return
}
c.JSON(200, Response{Code: 0, Message: "success", Data: result})
}
|
Nginx 按接口路由,部分接口走 Go,部分走 PHP:
1
2
3
4
5
6
7
8
| location /api/apply/ {
proxy_pass http://go_apply_service;
}
location /api/apply/list {
# 这个接口还没迁移,走 PHP
proxy_pass http://php_fpm;
}
|
三、数据双写#
迁移期间 PHP 和 Go 都在运行,两边都要写数据库,保证数据一致:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Go 服务写入后,同步通知 PHP 更新缓存
func (s *ApplyService) Apply(userID, jobID int64) error {
// 1. Go 写数据库
err := s.repo.Create(&Apply{UserID: userID, JobID: jobID})
if err != nil {
return err
}
// 2. 异步通知 PHP 清缓存(双写期间)
s.queue.Publish(&ApplyCreatedEvent{
UserID: userID,
JobID: jobID,
Source: "go",
})
return nil
}
|
PHP 端消费事件,更新自己的缓存:
1
2
3
4
5
6
7
8
| // PHP 消费 Go 的投递事件
public function handleApplyCreated($event) {
$applyId = $event['apply_id'];
// 清 PHP 端的投递列表缓存
$this->cache->delete("apply:list:{$event['user_id']}");
// 如果 PHP 端也有投递记录表,同步写入
$this->applyModel->syncFromGo($event);
}
|
四、灰度切换#
用 Nginx 按用户 ID 哈希灰度,逐步切流量:
1
2
3
4
5
6
7
8
9
| # 按用户 ID 灰度,10% 走 Go
split_clients "${http_x_user_id}" $apply_backend {
10% go_apply_service;
* php_fpm;
}
location /api/apply/ {
proxy_pass http://$apply_backend;
}
|
灰度阶段观察:
- 错误率:Go 和 PHP 的接口错误率对比
- 响应时间:Go 的 P99 延迟是否比 PHP 低
- 数据一致性:两边的投递记录数量是否一致
- 用户投诉:有没有用户反馈投递异常
没问题后逐步调到 30%、50%、100%。
五、性能对比#
| 指标 | PHP(旧) | Go(新) | 提升 |
|---|
| QPS | 800 | 5000+ | 625% |
| P99 延迟 | 120ms | 15ms | 87.5% |
| 内存占用 | 512MB/进程 | 128MB/Pod | 75% |
| 服务器数量 | 10台 | 3台 | 70% |
六、踩坑经验#
- PHP 和 Go 的时间格式不一致:PHP 返回
Y-m-d H:i:s,Go 默认返回 RFC3339。Go 接口统一用 time.Format("2006-01-02 15:04:05"),和 PHP 保持一致 - 数据库连接池差异:PHP-FPM 每个请求一个连接,Go 用连接池。迁移初期 Go 连接数暴增打满 MySQL。调小连接池
SetMaxOpenConns,加慢查询监控 - 缓存 Key 不一致:PHP 和 Go 的缓存 Key 命名规则不同,导致缓存不共享。统一缓存 Key 规范,用公共的 Key 生成函数
- 灰度期间数据冲突:同一个用户的投递请求,一部分走 PHP 一部分走 Go,可能重复投递。加了分布式锁,投递前先锁
apply:{user_id}:{job_id} - 回滚预案:灰度出问题要能快速切回 PHP。Nginx 配置一键切换,Go 写入的数据通过双写同步回 PHP,回滚后数据不丢
七、总结#
PHP 转 Go 迁移核心:
- 绞杀者模式:逐步迁移,不搞大爆炸,降低风险
- 接口兼容:Go 接口返回格式和 PHP 完全一致,前端不用改
- 数据双写:迁移期间两边都写,保证数据一致,支持回滚
- 灰度切换:按比例逐步切流量,每个阶段观察验证
- 性能验证:迁移前后做性能对比,确认 Go 的优势
- 回滚预案:每个阶段都有快速回滚方案,出问题不慌
从 PHP 迁移到 Go 不是"重写一遍"那么简单,接口兼容、数据双写、灰度切换、回滚预案这些工程实践才是迁移成功的关键。技术选型只是起点,落地过程中的细节处理才决定成败。