后端架构 | Makefile实战 | 批处理系统 | 技术选型
结论前置
选Makefile有三个核心原因,按重要性排序:
一、异常处理效率(最重要)
很多人以为对账系统的难点是"并行执行"或"性能优化",其实都是次要问题。
真正的痛点是:上游数据本身就有问题,而且每天都是常态。
- 账单缺失、格式错误、字段解析失败
- FTP不可用、账单延迟
- 业务规则变化(手续费舍入逻辑变了)
- 每个通道的规则本来就不同
这些异常不是"偶尔发生",而是"每天1-2个通道出问题"。
传统方案修复异常要30分钟-2小时,Makefile方案只要2-5分钟。
二、代码简洁,规则清晰
22个通道,每个通道的舍入逻辑、精度要求都可能不同。
单体程序会变成if-else补丁地狱,规则藏在代码里,看不懂、不敢改。
Makefile方案:每个通道一个目录,规则独立清晰,一眼就能看懂。
三、并行执行(副产品)
make -j22 一行命令实现高并发,但这只是顺便获得的收益。
总结:选Makefile不只是为了性能,更是为了可维护性和异常处理效率。
一、异常处理的残酷真相
1.1 上游数据问题不是bug,是feature在支付公司做过对账的都知道,上游通道的账单质量参差不齐:
| 问题类型 | 真实案例 | 频率 | 影响 |
|---|---|---|---|
| 账单缺失 | FTP服务器维护,账单没生成 | 每天1-2个通道 | 要重新下载 |
| 格式错误 | 新增一列,分隔符从逗号变Tab | 每周1-2次 | 要修改解析逻辑 |
| 字段解析失败 | 金额字段带货币符号,解析出错 | 每天3-5条 | 要手动处理 |
| FTP不可用 | 连接超时,被动模式限制 | 每周1次 | 要等或重试 |
| 账单延迟 | 跨行通道T+1变T+2,账单晚出 | 每天2-3个通道 | 要延迟对账 |
| 业务规则变化 | 手续费四舍五入逻辑变了 | 每月1-2次 | 要修改比对逻辑 |
| 金额精度 | 分转元时,精度丢失导致对不上 | 每周几次 | 要处理边界情况 |
最坑的是业务规则变化:
某个通道突然改了手续费的舍入逻辑:
- 之前:四舍五入到分
- 现在:直接截断到分
- 结果:几万笔交易,每笔差1分钱,总金额差了几百块
这种问题不是技术bug,是上游业务规则变了,连通知都没有。
22个通道,每天至少有1-2个通道出问题。
1.2 传统方案的修复流程
手写脚本 + 人工记录修复步骤:
1. 看日志,定位是哪个通道、哪天数据有问题
2. 分析问题类型(缺失?格式?解析?)
3. 查文档或问老员工,这个异常怎么修
4. 写临时脚本或手动执行修复步骤
5. 验证修复结果
6. 记录到文档(如果记得的话)
耗时:30分钟-2小时
问题在哪?
- 知识分散在文档里、脑子里,交接成本高
- 修复步骤要手动定位问题范围
- 容易出错,手动操作难免失误
- 每次修复都是"从零开始"
二、Makefile方案的异常处理机制
2.1 核心思想:幂等性 + 增量执行
Makefile基于文件时间戳判断是否需要执行:
# 目标文件: 依赖文件
# 规则:如果依赖文件更新了,或目标文件不存在,则执行规则
data/alipay_20250410.done: scripts/download.py scripts/parse.py
python scripts/download.py 20250410
python scripts/parse.py 20250410
touch data/alipay_20250410.done
这意味着什么?
- 如果脚本没改,且
.done文件存在 → 跳过执行 - 如果脚本改了,或
.done不存在 → 执行规则
2.2 修复异常变得极其简单
# 发现支付宝20250410数据有问题
# 步骤:
rm data/alipay_20250410.done
# 重新运行
make alipay DT=20250410
# 完成。只处理这一个通道这一天。
为什么这么快?
- 不需要手动定位问题范围 → 依赖图自动推导
- 不需要担心重复执行 → 幂等性保证安全
- 不需要写临时脚本 → 删除文件即可
- 不需要查文档 → 操作逻辑统一
| 场景 | 传统方案 | Makefile方案 |
|---|---|---|
| 发现异常 | 查日志、定位、写脚本 | 删除标记文件 |
| 修复时间 | 30分钟-2小时 | 2-5分钟 |
| 知识沉淀 | 分散文档 | 依赖图即文档 |
| 出错概率 | 人工操作,高 | 自动化,低 |
三、为什么是Makefile?
3.1 关键特性对比
| 特性 | Python脚本 | Makefile |
|---|---|---|
| 增量执行 | ❌ 要自己写状态 | ✅ 时间戳自动 |
| 幂等性 | ❌ 要自己保证 | ✅ 文件检测 |
| 精确重跑 | ❌ 要写参数 | ✅ 删除文件 |
| 依赖管理 | ❌ 硬编码 | ✅ 声明式 |
| 并行执行 | ⚠️ 要写并发 | ✅ make -j |
Makefile在增量执行和精确重跑上是独一档的。
3.2 声明式 vs 命令式
命令式(Python脚本):
# 告诉计算机"怎么做"
for channel in channels:
download(channel)
parse(channel)
compare(channel)
声明式(Makefile):
# 告诉计算机"要什么"
alipay: data/alipay.done
wechat: data/wechat.done
all: alipay wechat
修复异常时,命令式要改代码,声明式只要删除文件。
3.3 DAG复杂度:让复杂度可见
22个通道、多天数据、依赖链条,这是一个复杂的DAG(有向无环图)。
但复杂度不等于不可读。
Makefile的依赖关系:
# get_all.mk - 一眼看清整体结构
all: ${DT_RANGE} # 依赖多天数据
${DT_RANGE}: # 每天依赖22个通道
make -f get.mk DT=$@ # 递归调用
# get.mk - 一眼看清通道列表
CHANNELS = alipay wechat cup ... # 22个通道
all: ${CHANNELS} # 并行执行
${CHANNELS}: # 每个通道独立
make -C $@ DT=${DT} # 进入通道目录
同样的逻辑,C++/Python怎么写?
# reconcile.py - 依赖关系隐藏在代码里
def main(date):
# 第一层:处理多天
for day in date_range:
# 第二层:处理22个通道
for channel in channels:
# 第三层:每个通道的步骤
download(channel, day)
parse(channel, day)
compare(channel, day)
问题在哪?
| 对比 | Makefile | C++/Python |
|---|---|---|
| 依赖关系 | 显式声明 | 隐藏在循环里 |
| 整体结构 | 一眼看懂 | 要逐行分析 |
| 修改影响 | 显式可见 | 隐式传播 |
| 新人理解 | 看Makefile | 要读代码逻辑 |
核心洞察:Makefile让复杂度可见,C++/Python让复杂度隐藏。
可见的复杂度可以管理,隐藏的复杂度是地雷。
3.4 业务规则复杂度:每个通道都不同
这才是单体程序最大的坑。
22个通道,每个通道的舍入逻辑都可能不同:
| 通道 | 手续费舍入 | 金额精度 | 备注 |
|---|---|---|---|
| 支付宝 | 四舍五入到分 | 保留2位 | 标准做法 |
| 微信 | 直接截断到分 | 保留2位 | 向下取整 |
| 银联CUPS | 四舍五入到厘 | 保留3位 | 特殊精度 |
| 某银行 | 银行家舍入 | 保留2位 | 奇进偶不进 |
| ... | ... | ... | 22种规则 |
单体程序会变成什么样?
# compare.py - 单体程序的噩梦
def compare_fee(local_fee, remote_fee, channel):
if channel == 'alipay':
# 支付宝:四舍五入到分
return abs(round(local_fee, 2) - round(remote_fee, 2)) < 0.01
elif channel == 'wechat':
# 微信:直接截断到分
return abs(math.floor(local_fee * 100) / 100 -
math.floor(remote_fee * 100) / 100) < 0.01
elif channel == 'cup':
# 银联:四舍五入到厘
return abs(round(local_fee, 3) - round(remote_fee, 3)) < 0.001
elif channel == 'bank_xxx':
# 某银行:银行家舍入
# ...更多特殊逻辑
# 22个通道,22个分支
问题在哪?
- 补丁地狱:每新增一个通道,加一个if分支
- 代码腐烂:22个分支混在一起,没人敢动
- 验证困难:想验证某个通道的规则,要在一堆if-else里找
- 修改风险:改一个通道的逻辑,可能影响其他通道
Makefile方案怎么做?
reconcile/
├── alipay/
│ └── compare.py # 支付宝的舍入逻辑
├── wechat/
│ └── compare.py # 微信的舍入逻辑
├── cup/
│ └── compare.py # 银联的舍入逻辑
└── ... # 各通道独立
每个通道的规则在各自的文件里,互不干扰。
3.5 规则的可读性:一眼就能看懂
这是模块化最大的价值,但经常被忽视。
单体程序:规则藏在if-else里
# compare.py - 单体程序
def compare_fee(local_fee, remote_fee, channel):
if channel == 'alipay':
# 支付宝:四舍五入到分
return abs(round(local_fee, 2) - round(remote_fee, 2)) < 0.01
elif channel == 'wechat':
# 微信:直接截断到分
return abs(math.floor(local_fee * 100) / 100 -
math.floor(remote_fee * 100) / 100) < 0.01
# 还有20个通道...
问题:想看懂某个通道的规则,要在22个分支里找。
Makefile方案:一个文件只写一个规则
# alipay/compare.py - 只看支付宝规则
def compare_fee(local_fee, remote_fee):
# 支付宝:四舍五入到分
return abs(round(local_fee, 2) - round(remote_fee, 2)) < 0.01
# wechat/compare.py - 只看微信规则
def compare_fee(local_fee, remote_fee):
# 微信:直接截断到分
return abs(math.floor(local_fee * 100) / 100 -
math.floor(remote_fee * 100) / 100) < 0.01
每个文件只有几行,规则一目了然。
| 对比 | 单体程序 | Makefile方案 |
|---|---|---|
| 新增通道规则 | 改主代码,加if分支 | 新增目录,写独立脚本 |
| 修改现有规则 | 在if-else里找,不敢改 | 直接改对应通道脚本 |
| 规则可读性 | 22个分支混在一起 | 一个文件一个规则 |
| 规则验证 | 要读懂所有分支 | 只看对应通道文件 |
| 修改风险 | 可能影响其他通道 | 完全隔离 |
规则越复杂,"一眼就能看懂"的价值越大。
3.6 实战场景:规则变更通知
某天,支付宝发通知:"下周一开始,手续费舍入逻辑改成银行家舍入。"
单体程序的修复流程:
1. 打开compare.py
2. 在22个if分支里找到alipay那一节
3. 改逻辑
4. 担心:会不会影响其他分支?
5. 要不要写单元测试?
6. 测试要覆盖所有22个通道吗?
耗时:30分钟-1小时,心理负担重
Makefile方案的修复流程:
1. 打开alipay/compare.py
2. 改逻辑
3. 只测试支付宝通道
耗时:5分钟,心理负担轻
为什么心理负担不同?
- 单体程序:改一个分支,担心影响其他21个
- Makefile方案:只改一个文件,其他文件不受影响
规则复杂度越高,模块化的价值越大。
3.7 信息密度:每一行都表达大量信息
这是Makefile最被低估的优势。
看一行Makefile代码:
data/alipay_${DT}.done: scripts/download.py scripts/parse.py
这一行表达了什么?
| 信息 | 含义 |
|---|---|
| 目标文件 | data/alipay_${DT}.done 是要生成的文件 |
| 依赖关系 | 依赖两个脚本:download.py 和 parse.py |
| 增量执行 | 如果目标存在且依赖未变,跳过执行 |
| 构建规则 | 隐含规则:如何从依赖生成目标 |
| 变量展开 | ${DT} 会被替换成实际日期 |
一行代码,表达了5个维度的信息。
对比Python要写多少:
# 要表达同样的逻辑,Python需要:
# 1. 检查是否需要执行
if not os.path.exists(f"data/alipay_{dt}.done"):
# 2. 检查依赖是否变化
if (file_changed("scripts/download.py") or
file_changed("scripts/parse.py")):
# 3. 执行构建
download(dt)
parse(dt)
# 4. 生成标记
touch(f"data/alipay_{dt}.done")
else:
# 5. 跳过执行
pass
信息密度的差异:
| 维度 | Makefile | Python |
|---|---|---|
| 代码行数 | 1行 | 10行+ |
| 信息密度 | 5个维度/行 | 1个维度/行 |
| 认知负担 | 看一眼就懂 | 要逐行理解 |
| 出错概率 | 低 | 高 |
这就是为什么Makefile适合复杂编排:信息密度高,bug少。
四、其他价值:并行执行与扩展
4.1 并行执行是副产品
虽然异常处理是核心原因,但并行执行也有价值:
# 一行命令,22个通道并行
make -j22
- 对账耗时:2小时 → 15分钟(87.5%↓)
- 但这不是选Makefile的主要原因
4.2 内部流水和对账单的预处理也可以并行
除了通道级别的并行,数据预处理阶段同样可以并行化,进一步提升整体效率。
传统串行方式:
1. 先下载所有对账单(22个通道串行)
2. 再从数据库导出内部流水(一次大查询)
3. 最后逐个通道比对(22个通道串行)
总耗时:下载(1小时) + 导出(30分钟) + 比对(30分钟) = 2小时
Makefile并行方式:
# preprocess.mk - 数据预处理并行
all: download export_data
# 22个通道并行下载
download: ${CHANNELS}
${CHANNELS}:
make -C $@ download DT=${DT}
# 同时并行导出内部流水(按表分片)
export_data: trade_20250410.export order_20250410.export refund_20250410.export
%.export:
mysql -e "SELECT * FROM ${TABLE} WHERE date='${DT}' INTO OUTFILE 'data/$@.csv'"
并行执行的实际效果:
| 阶段 | 串行耗时 | 并行耗时 | 说明 |
|---|---|---|---|
| 下载对账单 | 60分钟 | 5分钟 | 22个通道并行下载 |
| 导出内部流水 | 30分钟 | 10分钟 | 按3张大表分片并行导出 |
| 数据比对 | 30分钟 | 5分钟 | 22个通道并行比对 |
| 总耗时 | 120分钟 | 20分钟 | 整体提速83% |
关键优化点:
- 对账单并行下载:22个通道的FTP/HTTP请求同时发起,互不阻塞
- 内部流水分片导出:按交易表、订单表、退款表分片,避免单次大查询锁表
- 比对任务并行:每个通道比对独立,天然可并行
Makefile的依赖管理保证:
# 比对任务自动等待数据准备完成
compare: download export_data
${CHANNELS}:
make -C $@ compare DT=${DT}
# 即使并行执行,依赖关系也确保正确性
make -j22 # Makefile自动处理依赖,不会出错
为什么这个很重要?
在400亿资金对账的场景下,每天凌晨的时间窗口非常紧张:
- 银行通道通常在 6:00-8:00 之间完成对账文件生成
- 业务需要在 9:00 开盘前完成对账,发现异常及时处理
- 预处理阶段的并行化,为异常处理留出了充足时间
这不是选Makefile的主要原因,但确实是非常有价值的副产品。
4.3 扩展性也不错
新增通道:
# 复制目录
cp -r alipay new_channel
# 修改里面的脚本实现
vim new_channel/download.py
# 在总编排加一行
CHANNELS = alipay wechat new_channel
# 完成
- 新增通道:1天 → 30分钟
- 但这也不是选Makefile的主要原因
异常处理的效率提升,才是核心价值。
五、技术方案速览
5.1 架构设计
调度编排层:crontab + flock + Makefile
↓
配置抽象层:comm.mk(日期计算、数据库连接、通用变量)
↓
通道处理层:各通道独立目录(下载、解析、比对脚本)
↓
数据存储层:MySQL分表存储
5.2 关键代码
# comm.mk - 统一配置
DT := $(shell date -d '1 day ago' '+%Y%m%d')
sql := mysql -p'xxx' -umysqluser -N
# get_all.mk - 批处理编排
CHECK_DAYS ?= 4
DT_RANGE := $(shell seq ${CHECK_DAYS} -1 1)
all: ${DT_RANGE}
${DT_RANGE}:
make -f get.mk "DT=`date -d "$@ days ago" '+%Y%m%d'`" -k
# get.mk - 并行执行
CHANNELS = alipay wechat cup ... # 22个通道
all: ${CHANNELS}
${CHANNELS}:
make -C $@ DT=${DT} -k || true # -k: 错误容忍
六、适用场景
适用场景(异常处理是刚需):
- 批处理任务(ETL、对账、报表生成)
- 上游数据质量不稳定
- 需要频繁重跑部分任务
- 通道/任务数量3个以上
不适用场景:
- 实时流处理场景
- 超大规模并发(上千个并行单元)
- 动态任务图(运行时决定依赖关系)
七、决策框架
选不选Makefile,问自己几个问题:
7.1 异常处理频率高吗?
- 每天1-2个通道出问题 → Makefile优势明显
- 几周才出一次问题 → 其他方案也行
7.2 需要精确重跑吗?
- 需要重跑某个通道某一天的数据 → Makefile是最佳选择
- 要么全量重跑,要么不重跑 → 其他方案也行
7.3 任务数量多吗?
- 3个以上通道/任务 → Makefile优势明显
- 1-2个任务 → 手动脚本够用
7.4 团队能力呢?
- 熟悉Linux/Makefile → 直接用
- 不熟悉 → 学习成本不高,值得投入
八、总结
技术选型的核心是匹配场景,而不是追新。
Makefile是1977年的工具,但在异常处理频繁的批处理场景,它比手写脚本更简洁、更可靠。
老工具用对了地方,照样能解决新问题。