故障演练是什么
故障演练就是在出事之前,主动把系统弄坏一次,看它扛不扛得住。
故障演练
防线 和 注入故障
注入故障:在代码里人为制造故障,比如随机丢一条消息、随机让下单超时、在关键两行代码之间杀掉进程。
为什么进组前最值得做这个
全貌:六个会断的地方
把 D3 到 D6 做的东西连起来看,一共有六个地方最容易出事。下面的图里用数字标出来了,后面每一节讲一个。
Exchange WS ──(4)──► [ GridBot ] ──► [ OrderGateway ] ──(3)──► Exchange
│
leader fill ──► leader.fills ──(1)(2)──► [ Fanout ] ──(5)──► copy.orders ──► [ OrderWorkers ]
(6) any process may die between "order sent" and "event logged"
图里的名字都是前几天做过的东西:Exchange WS 是交易所推行情的 WebSocket 连接(D3);GridBot 是你的网格机器人(D4);OrderGateway 是负责把订单发给交易所的那一层(D4 项目骨架里的 order 目录)。下面一行是跟单(D6):leader fill 是带单员的一笔成交,先进 leader.fills 这个 topic,Fanout 是扇出服务(D6 的 FanoutService),把它拆成每个跟单者一张订单放进 copy.orders,OrderWorkers 是真正去下单的消费者。括号里的数字对应下表。
| 编号 | 故障 | 一句话 | 在哪一节 |
|---|---|---|---|
| 1 | 重复消息 | 同一条成交被处理两遍 | 第 3 节 |
| 2 | rebalance | 消费者重新分配分区,暂停 + 重投 | 第 3 节 |
| 3 | 下单超时 | 不知道单子到底进没进交易所 | 第 4 节 |
| 4 | 行情断线 / 跳号 | 本地盘口和真实盘口对不上 | 第 5 节 |
| 5 | 热门带单员 | 一个分区被挤爆,跟单者全在排队 | 第 6 节 |
| 6 | 进程崩溃 | 做了一半:交易所有单,本地不知道 | 第 7 节 |
重复消息和 rebalance
重复消息 duplicate
两道防线:去重 和 幂等
f1、跟单者是 F17,编号就是 f1-F17。同一条消息不管处理几次,拼出来的编号都一样,交易所会拒绝重复编号(D5 讲过;具体规则和有效时间各家不同)。保护的是交易所那边的订单。HashSet 里,一重启 HashSet 就空了,重新投递过来的消息会被当成新消息再算一遍。下面的演示专门演这个。INSERT INTO processed_fills(fill_id)(唯一索引)+ UPDATE positions。唯一索引冲突就说明处理过了,整个事务回滚。设定:第 3、6 条是上游重复发送的;最后一次提交 offset 在第 2 条之后,所以重启后从第 3 条开始重新读。正确仓位是不重复的 5 笔相加:0.5 − 0.2 + 0.3 − 0.1 + 0.2 = 0.7。
操作:切换去重方式,再勾掉或勾上"重启",看上面每一行怎么处理、下面的"算出来的仓位"。你应该看到:不去重是 1.5(有重启 2.2);去重记录只放内存,不重启时是 0.7,一重启就变成 1.4;和仓位一起保存,怎么切都是 0.7。这说明去重记录必须和状态一起保存。
在你的项目里怎么制造
group.id(消费者组的名字,名字相同的实例算一个组,D6 讲过)再启动一个消费者实例,等它分到分区后再 kill -9 掉,观察原来那个消费者的日志和 lag。下单超时:不知道成没成
超时 和 未知状态
SocketTimeoutException,不等于对方没执行。和调用支付接口超时是一个道理。防线:按编号查询
G7-3-B-12 是 D5 讲的可推导编号:7 号网格、第 3 格、买单(B)、第 12 次挂单。
操作:先选做法,再在下拉框里切换交易所的真实情况,看"交易所里这笔单有几张":1 张是对的,2 张就是重复下单。
关键在于:超时那一刻,你不知道下拉框选的是哪一个。做法 A 只在"没收到"时碰巧没事,做法 B 两种情况都对。
录屏:滚到这里会自动一步步播放。每一步变了的数字会被框出来。左边"你的程序以为"和右边"交易所里这张单"一直对不上,直到第 6 步按编号查询之后才对齐;第 7 步是对照,演示如果把超时当失败会发生什么。
行情断线和跳号
现象
后果(算一遍)
① 手续费:成交额约 5 × 0.01 × 60,300 ≈ 3,015 U,按 D1 的例子(挂单 0.08%、吃单 0.1%)多付约 0.6 U;
② 更要命的是位置错了:机器人以为这 5 格还在等上涨,实际一下子全卖了,手里少了 0.05 BTC,后面每一步判断都建立在错的数据上。如果这些卖单带了只做挂单的要求,它们会全部被拒,机器人又会以为自己挂上了。
防线
热门带单员:一个分区被挤爆
现象和后果
防线
followerRepo(D6 代码里存"谁跟了哪个带单员"的那个对象)里给一个带单员塞 3000 个跟单者,发一条成交,看第二段每个分区的 lag 和最后一张单的时间。进程崩溃:写了一半
现象
kill -9、机器断电。最危险的位置是"订单已经发给交易所"和"本地记下这件事"之间:交易所有这张单,本地却不知道。这种单叫孤儿单。后果(算一遍)
防线
G7- 开头,就是 7 号网格发的;D5 的 ClientOid.belongsTo 就是干这个的),再决定认领还是撤销。Runtime.getRuntime().halt(1)(第 9 节有代码),它比 System.exit 更接近 kill -9:不执行任何收尾代码。故障模拟器:选一种故障,开关防线
把第 3–7 节的六种故障放在一起。选一种,勾上或去掉对应的防线,点"注入故障"看后果。
操作:先选一种故障,读"场景",点"注入故障"看结果(默认所有防线都开着);然后去掉一道防线再点一次,对比数字怎么变。结论标签有三种:扛住了、部分扛住、出事了。
在你的 Java 项目里注入故障
做法很简单:加一个开关类 Chaos(英文"混乱"),平时全部关掉;演练时打开某一个开关,看系统的反应。下面的类名和方法名沿用 D3–D6 笔记里的代码。
// 故障开关:演练时打开,平时全部为 0 / false
import java.util.Random;
public final class Chaos {
public static volatile double DUPLICATE_RATE = 0.0; // 重复发送消息的概率
public static volatile double DROP_RATE = 0.0; // 丢掉一条行情的概率
public static volatile double LOST_ACK_RATE = 0.0; // 下单成功但回复丢了的概率
public static volatile boolean CRASH_AFTER_PLACE = false; // 下单之后、写日志之前直接死掉
private static final Random R = new Random();
public static boolean hit(double rate) { return R.nextDouble() < rate; }
}
重复发送(对应第 3 节)
// rec 是要发的那条 Kafka 消息,producer 是 D6 里的 KafkaProducer
void publish(ProducerRecord<String, String> rec) {
producer.send(rec);
if (Chaos.hit(Chaos.DUPLICATE_RATE)) producer.send(rec); // 同一条再发一次
}
下单超时(对应第 4 节)
// D5 第 9 节的 PaperExchange(存文件的模拟交易所):订单收下了,但故意抛超时
public Order place(Order o) throws TimeoutException {
if (open.containsKey(o.clientOid())) // 编号已存在:像真交易所一样拒绝
throw new IllegalStateException("DUPLICATE_CLIENT_OID");
open.put(o.clientOid(), o); // 订单已经进了"交易所"
save(); // 写 exchange.db
if (Chaos.hit(Chaos.LOST_ACK_RATE))
throw new TimeoutException("模拟:成功了,但回复丢了");
return o;
}
// 调用方:就是 D5 第 4 节的 placeSafely,超时后用同一个编号先查
public Order placeSafely(Order o) throws Exception {
try {
return exchange.place(o);
} catch (TimeoutException e) {
Optional<Order> found = exchange.query(o.clientOid());
if (found.isPresent()) return found.get(); // 已经有了:接管真实状态
return exchange.place(o); // 确认没有:用同一个编号重下
}
}
丢行情(对应第 5 节)
// D3 第 10 节处理增量的方法,在最前面加一行
void onDelta(Delta d) {
if (Chaos.hit(Chaos.DROP_RATE)) return; // 假装这条在网络上丢了
// ……下面是 D3 原来的逻辑:先检查序列号,跳号就丢掉订单簿、重新拉快照
}
写了一半就死(对应第 7 节)
Order placed = exchange.place(o);
if (Chaos.CRASH_AFTER_PLACE) Runtime.getRuntime().halt(1); // 不跑任何收尾代码,接近 kill -9
local.record("PLACED " + placed.clientOid()); // D5 的 LocalBook:先写日志,再改内存
演练顺序建议
监控要看什么
演练是你主动去找问题,监控是让问题自己冒出来。系统持续记录几个关键数字,超过事先定好的阈值就发告警(发消息、打电话给值班的人)。
四个最重要的数字
操作:拖每一行的滑块,看右边的标签在哪个数值从"正常"变成"关注"、再变成"告警"。注意对账差异数:只要不是 0,就直接告警。
一页总结怎么写
七天的成果最后落到一页纸上:这个系统会在哪 5 个地方出事,每个都写清现象、后果、防线和你亲手验证的证据。进组第一周,它就是你跟同事聊天的底稿。
故障名称: 现象: 一句话,别人能听懂发生了什么 后果: 带数字,比如"多下了 9 张单""多付了 0.6 U 手续费" 防线: 用了什么机制,写在代码的哪里 我的验证:开了哪个开关,看到了什么,对照实验是什么 还没想明白:进组后要问的问题
写好的示例
FanoutService 里拼编号的那一行);下单消费者按 clientOid 拒绝重复。producer.flush()(确认消息都发出去了)和 commitSync()(提交 offset,也就是夹书签)之间加 halt(1),重启后日志里 9 行"重复订单被拒绝"。把 clientOid 换成 UUID 做对照,9 张全部成交。写的时候直接写在 七日冲刺 D7 的总结框里,会自动保存在本机浏览器。
词典、自测和七天收尾
今天用到的词都在这里,包括前几天学过、今天又用到的。
自测 7 题
回到 七日冲刺 D7:翻一遍六张故障卡,做 applyFills 编程题,然后把一页总结写完。最后看 进组以后,那里有第一、二周的安排。
国庆时间还够的话,接着做三天加练:D8 交易所内部怎么跑现货和合约,D9 量化策略怎么判断好坏,D10 在测试网上亲手操作一遍。
进组第一周怎么说话,三个例子:
像背书"Kafka 是至少一次语义,所以消费端要做幂等。"
像干过"咱们跟单下单用的 clientOid 是怎么拼的?我本地试过用随机编号,重启后会重复下单。"
像背书"要监控消费延迟,防止消息堆积。"
像干过"头部带单员成交的时候,copy.orders 各个分区的 lag 一般会冲到多少?有没有告警阈值?"
像背书"下单超时要保证幂等性。"
像干过"下单超时以后,我们是先按 clientOid 查一次,还是直接等私有推送的订单回报?"