400亿资金对账的技术解法:我用Makefile替代了C++和Python代码

0 阅读9分钟

后端架构 | 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小时

问题在哪?

  1. 知识分散在文档里、脑子里,交接成本高
  2. 修复步骤要手动定位问题范围
  3. 容易出错,手动操作难免失误
  4. 每次修复都是"从零开始"

二、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

# 完成。只处理这一个通道这一天。

为什么这么快?

  1. 不需要手动定位问题范围 → 依赖图自动推导
  2. 不需要担心重复执行 → 幂等性保证安全
  3. 不需要写临时脚本 → 删除文件即可
  4. 不需要查文档 → 操作逻辑统一
场景传统方案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)

问题在哪?

对比MakefileC++/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个分支

问题在哪?

  1. 补丁地狱:每新增一个通道,加一个if分支
  2. 代码腐烂:22个分支混在一起,没人敢动
  3. 验证困难:想验证某个通道的规则,要在一堆if-else里找
  4. 修改风险:改一个通道的逻辑,可能影响其他通道

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.pyparse.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

信息密度的差异:

维度MakefilePython
代码行数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%

关键优化点:

  1. 对账单并行下载:22个通道的FTP/HTTP请求同时发起,互不阻塞
  2. 内部流水分片导出:按交易表、订单表、退款表分片,避免单次大查询锁表
  3. 比对任务并行:每个通道比对独立,天然可并行

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年的工具,但在异常处理频繁的批处理场景,它比手写脚本更简洁、更可靠。

老工具用对了地方,照样能解决新问题。