数据平台与调度实验三:多租户配额、代际租约与 checkpoint
教材正文:多租户、配额与切换:从令牌桶到代际租约,以及 checkpoint 不是业务提交。本目录提供可复现的公开实验:真实 MySQL 8.4.7 上的令牌桶与代际租约,真实 Spark 4.0.1 Structured Streaming 的 checkpoint 重放与幂等写。
前置条件
| 依赖 | 要求 | 说明 |
|---|---|---|
| MySQL | 8.x,root 可空密码连接 127.0.0.1:3306 | DBngin;可用 MYSQL_HOST/PORT/USER/PASSWORD/MYSQL_BIN 覆盖 |
| JDK | 21(Spark 4.0.1 要求 17+) | /usr/libexec/java_home -v 21 或 JAVA_HOME |
| Spark | 4.0.1 发行包 | 复用 U86 缓存 ~/.cache/software-engineering-study/java-depth-impl/u86/,缺失时自动下载并校验 SHA-512 |
| Python | 3.9+ venv(复用 U86 的 venv) | 需要 pymysql 与 pyspark |
不启动 YARN/K8s/Flink 集群;集群级配额与 Flink checkpoint 未运行。
一条命令
实验代码sh
python3 labs/data-tenant-quota/run.py退出码 0 表示 4 组场景全部通过;只有成功才把证据复制回 evidence/u87-2026-10-28.json,失败运行只留在缓存 runtime/ 下。清理不完整同样使退出码非 0。
四组场景与真实观察
| 场景 | 观察 | 证据(2026-10-28,exit 0) |
|---|---|---|
| 令牌桶 | 两个租户各请求 3 次、初始各 5:各剩 2,无超扣 | quotaRequests tenantA=3 tenantB=3、quotaRemaining tenantA=2 tenantB=2 |
| 代际租约 | worker-a gen1 被 SIGKILL → 同名重启接管 gen2 → owner 仍匹配 1 行 → 旧代际写入 0 行 | leaseTakeover owner=worker-a generation=2 → leaseOwnerOnlyMatches count=1 → leaseStaleWrite affected=0 |
| checkpoint vs 业务提交 | 首批 5 行;第二批失败 exit 1 仍 5 行;重放后 8 行;删提交记录强制重复投递仍 8 行且 Spark 以 STREAM_FAILED 保护 | checkpointFailedBusiness exit=1 businessRows=5 → checkpointReplay epoch1Reprocessed=True → checkpointReplayAgain exit=1 businessRows=8 tamperGuardFired=True |
| 配额拒绝与恢复 | 0 令牌时 REJECTED 且无业务行;恢复后 ACCEPTED、扣 1 剩 2、1 行业务 | blockedStatus=REJECTED businessRowsBlocked=0 → recoveredStatus=ACCEPTED businessRowsAccepted=1 |
边界与构造说明
- 配额在 MySQL 行锁上实现(
UPDATE ... WHERE tokens > 0),不是独立配额服务;并发为 6 个线程,未做高并发压测。 - 租约过期用 3 秒(为了秒级观察接管);生产通常更长。SIGKILL 是真实信号,未覆盖网络分区下的"假死";接管者为同名 worker-a 重启,owner 无法区分新旧,只有 generation 能区分。
- checkpoint 场景用
trigger(availableNow=True),业务写走foreachBatch+ pymysql;不是 Flink checkpoint,也不声明引擎端 exactly-once——业务侧靠(tenant, order_id)唯一键 +INSERT IGNORE收敛重复;删除 commit log 记录会被 Spark 4 的完整性检查以 STREAM_FAILED 拒绝。 - 未运行:YARN/K8s 集群级配额、Redis 令牌桶、跨机时钟偏差、Flink checkpoint/savepoint、fencing token 下沉到存储层。
- Spark 为 local[2];所有租户与订单为合成标签;独有库结束即 DROP,孤儿库定向清理(同一时刻只应有一个 U87 运行实例)。
目录
| 文件 | 用途 |
|---|---|
| run.py | 公开入口:建库、4 组场景、证据落盘(成功才复制)、定向清理 |
| u87-2026-10-28.json | 一次完整运行的证据(环境、场景观察、清理、哈希) |
worker 脚本(配额扣减、租约获取/接管/旧代际写入、流任务)由 run.py 在运行时生成到 runtime/workers/,源码在脚本模板中可审阅。