望川城的春天,是从更鼓开始的。

城东是河。河有一处急弯,叫断颈湾。每年春汛,水位涨到城墙第三道刻痕时,守城的人要做三件事:敲更鼓、开南仓、派快马把水情送到下游三个镇子。

这三件事每年都要演练一次。

城里的规矩是:挑一个晴好的日子,所有人各就各位,更鼓照真事敲,南仓照真事开,快马照真事跑。演练一天,写一册子,收进祠堂。

问题是,晴好的日子,断颈湾从不决堤。

真决堤的那一夜是三月十九。更鼓漏了一响,南仓的粮账差了两石,快马在柳渡被水拦住。等水情送到下游,三个镇子已经泡在水里。

水退之后,城里人围在祠堂里翻册子。册子上写着:更鼓齐,粮账清,快马至。

那一夜的真相,随水走了。没有人能再回到三月十九,看看到底是哪一步先塌的。

那年冬天,城西来了一个年轻人,叫阿砚。她不看水,只看更楼。

她跟城主说:给我一间空屋、一队人,和一整年用不完的灯油,我还你一个四月。

这就是影城。

影城是一间没有窗的大厅。厅里照着望川城的样子搭了一座小城:断颈湾的弯、南仓的梁、柳渡的浮桥,连更楼上那面鼓都在,只是只有巴掌大。

厅的正中央坐着一个人,掌一面小鼓。城里人叫他司时人。

影城的规矩只有两条。

第一条:厅里不许有天。

不许看真太阳,不许听真更漏,不许掷真骰子。什么时候天亮,由司时人的鼓定;河水几时涨,由他引的水渠定;快马几时到,由他手里的沙漏定。连守城人在岔路口选了哪条路,也由他从一册封了蜡的签簿里抽出来。

第二条:每一次抽签、每一次引水、每一次塌方,都要记下当天的灯号。

灯号是一个数。同一个数,就是同一场雨、同一个迟到的快马、同一块朽掉的桥板。

— —

第一年的影城,散得很难看。

不是戏不好,是有人偷看天。

南仓的老账房嫌沙漏慢,自己数起了真的心跳;柳渡的艄公不耐烦司时人给的浪,扭头去看窗外的真河;更夫照着真更漏敲鼓,鼓点就跑到了签簿前头。一天演到黄昏,戏散了两回——同一册签簿,两回散在同一处,第三回却散在了别处。

阿砚把所有人叫到厅中央,只说了五个字:

只有一个手。

从那天起,厅里所有的手都归了司时人:时间、水、风、路、人的一念之差。谁要用时间,得问鼓;谁要掷骰子,得从签簿里拿。

戏就顺了。

顺了以后,时间在厅里成了可以买卖的东西。窗外一刻,厅里能过三个月。到后来,他们一夜能过完望川城的三十年。

第二年开春,阿砚报出灯号「三月十九」。

厅里下起了雨。更鼓漏了一响,南仓的粮账差了两石,柳渡的浮桥被水顶起半尺。司时人在第四千一百一十二下鼓上停了手。

戏停在那里。

所有人可以走进画面里去:翻开南仓的粮册,看那两石差在哪个格子里;把更楼的更漏拆开,看那一声漏在哪个齿轮上;甚至可以让快马退回去,重新选一次渡口。

这是真事永远给不了的东西——真的三月十九只发生一次,影城的三月十九可以停在半空,让人围着看。

他们找到了病根:更漏的铜壶在春寒里收缩,一夜慢一响,慢的正好是一声报汛的鼓。差的两石粮,是因为账房按”实收”记账,南仓按”应收”开仓。快马被拦,是因为浮桥的缆绳泡了三天,没人换。

修铜壶,改账法,换缆绳。

再把灯号「三月十九」报一遍。

雨还是那场雨,更鼓还是那一响,快马还是迟到。可这一次,南仓按”应收”开仓,多出的两石正好够三个镇子的人吃到水退。桥没有被顶起来。

同一场灾,第二次,城站住了。

— —

第三年,城里多了一个人。

他的差事叫”搅局”。他不管守城,只管在戏里使坏:剪断一根缆绳,让粮仓的一扇门卡住,让信使在半路崴脚,让一场雨比预报多下半个时辰。

规矩一样:所有的坏,都得从签簿里抽,都得记灯号。

于是坏事有了编号。城里的年轻人整夜整夜地摇签,一夜摇过一万个灯号。哪个灯号让城塌了,就把它抄在墙上;第二天照着这个数,把那一夜重演一遍,塌在哪里,一眼看得见。

到后来,望川城的孩子们会背墙上的数字,像背节气。

— —

只是阿砚一直在厅门口立着一块木牌,上面写着两行字:

影城不是望川城。

厅里的雨缸是圆的,真的雨不是。厅里的桥板是按旧年的木纹量出来的,真的木头会生虫。影城能告诉你”在这套道理里,这样会塌”;它不能告诉你”真实的天,一定是这样下的”。

木牌下面还有一行更小的字:

厅里不许有天。

谁要是抬头看了天,影城就散了。

到这里你可能已经认出来了:这就是 Deterministic Simulation Testing(确定性模拟测试)。

这是什么

Deterministic Simulation Testing(常缩写为 DST)是把整个分布式系统放进一个完全可复现的模拟环境里运行的方法。最著名的实践者是 FoundationDB:一整个数据库集群——网络、磁盘、时钟、随机数——跑在一个单线程的模拟器里,生产代码和模拟代码用的是同一套接口。

具体的做法是几条互相咬合的规则:

  • 单一事件循环,虚拟时钟。没有真实的 sleep,没有真实的 time.Now()。时间是一个由事件队列推进的整数;超时、重传、调度都按虚拟时间排序执行。
  • 所有非确定性都被换掉。网络换成模拟网络,可以任意延迟、乱序、重复、丢包、分区;磁盘换成模拟磁盘,可以丢写、假装 fsync、撕裂写、掉电重启;时钟换成模拟时钟,可以跳变;线程换成模拟线程。剩下的唯一随机源,是一个带种子的 PRNG。
  • 种子就是复现。一次运行是 f(seed, binary, config) 的纯函数。只要记住种子,同样的故障一秒内能重演一遍,不必”再跑一次看看能不能撞上”。
  • 故障注入。模拟器可以在任意时刻崩溃任意节点、切断任意链路。FoundationDB 还给了一个叫 BUGGIFY 的机制:在代码的特定位置按固定概率(约四分之一)触发故障,而且只在模拟里生效——同一个种子,触发的位置永远相同。
  • 时间被压缩。一个单线程进程里跑几千个模拟节点,一小时墙钟时间可以覆盖几十万小时的集群时间;种子可以整夜整夜地扫。

它和普通集成测试的关键差别不是”模拟”,而是确定性。普通测试里,一个罕见的时序 bug 撞上了一次,你抓不住、复现不了,只能祈祷它别再出现;DST 里,那个 bug 有一个种子,种子就是它的地址。

为什么重要

分布式系统的 bug 大多长在时序的缝里:一条消息晚到三毫秒,一次 fsync 被吞掉,两个节点在同一瞬间都认为自己是 leader。这类 bug 在小规模测试里出现概率极低,一旦在生产里发作,日志里往往什么都不剩。

DST 把”罕见的时序”变成”可以点名的输入”。故障不必等它发生,而是由你指定;发现之后不必”想办法复现”,把种子抄下来就行。这把分布式测试从概率游戏变成了穷举游戏,而且是可以暂停、倒带、逐帧检查的穷举。

它改变的不只是测试,还有工程流程。FoundationDB 把模拟测试接进了日常开发循环,一个新改动进来先跑种子群,塌了就直接拿到种子。TigerBeetle 用同一思路做了 VOPR(Viewstamped Operation Replicator),把共识和存储引擎的真实代码接进可控的模拟世界。Antithesis 把确定性下沉到自研 hypervisor(The Determinator),整台机器的虚拟硬件、网络、时间都是确定的,被测系统几乎不用改代码。Rust 生态里的 madsim 把同一套办法做成了库。

它也画出了自己的边界,而且边界很硬:

  • 保真度。模拟器是模型,不是真实世界。它没有建模的东西——真实网卡的排队行为、内核页缓存的语义、GC 停顿、真实硬件的故障率——它测不出来。模型错了,测试会安静地通过,然后在生产里塌。
  • 纪律。被测代码里只要有一处偷偷读了真实时间、开了一个后台线程、用了一个没接进模拟器的第三方库,确定性就破了,重放会分叉。这不是可以”大致遵守”的规则,是全有或全无。
  • 不是性能测试。虚拟时钟不测延迟,只测逻辑。吞吐和尾延迟仍要在真实硬件上量。

所以 DST 不替代集成测试、混沌工程、灰度发布和形式化验证。它替换的是”靠反复跑集成测试去撞时序 bug”这件事:把最难测的那一类 bug——罕见、致命、不可复现——拉到和单元测试一样可以被点名、被重放的位置。

隐喻对应表

  • 望川城 → 被测的分布式系统
  • 影城 → 模拟器(simulation)
  • 司时人 → 单一事件循环 + 虚拟时钟
  • 厅里不许有天 → 禁止任何真实的墙钟、网络、磁盘、随机源
  • 签簿里的签 → 带种子的 PRNG,所有非确定性都从这里取
  • 灯号 → 随机种子(seed);同一个灯号即同一次可复现的运行
  • 第四千一百一十二下鼓上停手 → 在虚拟时间的任意时刻暂停,检查全局状态
  • 更鼓漏一响、粮账差两石、浮桥被顶起 → 被注入的故障,以及它暴露出的真实 bug
  • 走进画面里翻粮册、拆更漏 → 确定性重放带来的逐帧调试
  • 修铜壶、改账法、换缆绳 → 修改被测代码或配置
  • 同一个灯号第二遍,城站住了 → 修复被同一场景验证,成为回归用例
  • 搅局人 → 故障注入器(fault injector),以及 BUGGIFY 式的代码点注入
  • 一夜摇一万个灯号 → 种子扫描(swarm testing),成千上万次模拟运行
  • 一夜过完三十年 → 时间膨胀,虚拟时间远超墙钟时间
  • 影城不是望川城 → 保真度限制:模型没覆盖的现实行为测不出来
  • 抬头看天,影城就散 → 一处未受控的非确定性就会让重放分叉
  • 城墙上的数字像节气 → 失败种子沉淀为可复现的回归集合

Spring in Wangchuan begins with the night drum.

The river runs along the east wall. It has one sharp bend called Broken Neck Bay. Every spring, when the water climbs to the third mark on the city wall, the watch must do three things: strike the drum, open the south granary, and send a fast rider downstream with the flood warning for the three towns.

Every year they rehearse all three.

The rule is this: pick a clear day, put everyone at their post, strike the drum for real, open the granary for real, ride the horse for real. One day of rehearsal, one ledger written, one ledger filed in the ancestral hall.

The trouble is that on a clear day, Broken Neck Bay never bursts.

The night it did burst was the nineteenth of the third month. The drum missed one beat. The granary’s grain count was short by two measures. The rider was stopped by water at Willow Ford. By the time the warning arrived downstream, all three towns were under.

When the water fell, the city gathered in the ancestral hall and read the ledger. The ledger said: drum in order, grain count correct, rider arrived.

The truth of that night went out with the water. No one could return to the nineteenth of the third month and see which part gave way first.

That winter a young woman came from the west of the city. Her name was Yan. She did not look at the river. She looked at the drum tower.

She told the magistrate: give me an empty hall, a troupe, and a year of lamp oil I cannot exhaust, and I will give you an April.

That was the shadow city.

The shadow city was a windowless hall. Inside, they built a small Wangchuan to scale: the bend at Broken Neck Bay, the beams of the south granary, the pontoon at Willow Ford — even the drum from the tower, though only the size of a palm.

In the center of the hall sat one person holding a small drum. The city called her the keeper of hours.

The shadow city had only two rules.

The first: no sky indoors.

No looking at the real sun. No listening to the real water clock. No casting a real die. When dawn comes is set by the keeper’s drum. When the river rises is set by the channel she opens. When the rider arrives is set by the sand glass in her hand. Even which fork a watchman takes is drawn from a wax-sealed book of lots.

The second rule: every lot drawn, every channel opened, every beam that falls must be written down with that day’s lamp number.

A lamp number is a single number. The same number is the same rain, the same late rider, the same rotted plank.

— —

The first year of the shadow city fell apart badly.

Not because the play was poor, but because people kept peeking at the sky.

The old granary clerk found the sand glass slow and began counting his own heartbeat. The ferryman at Willow Ford grew impatient with the keeper’s waves and turned to look at the real river through the window. The drummer followed the real water clock, and his beats ran ahead of the book of lots. One day, by dusk, the play had broken twice — same book, same place the first two times, and the third time somewhere else entirely.

Yan called everyone to the center of the hall and said five words.

Only one hand.

From that day every hand in the hall belonged to the keeper of hours: time, water, wind, roads, and the turn of a person’s mind. If you needed time, you asked the drum. If you needed to cast a die, you took it from the book.

The play ran smoothly.

And once it ran smoothly, time inside the hall became something you could buy and sell. One quarter hour outside, three months inside. By the end, they could live thirty years of Wangchuan in a single night.

The next spring, Yan called out the lamp number: the nineteenth of the third month.

Rain began inside the hall. The drum missed one beat. The granary count came up two measures short. The pontoon at Willow Ford lifted half a foot. On the four thousand one hundred and twelfth beat, the keeper stopped her hand.

The play stopped there.

Anyone could walk into the scene. Open the granary ledger and see which column the two measures went missing from. Take apart the water clock and see which gear swallowed the beat. Send the rider back and let him choose a different ford.

This is what real events can never give you. The real nineteenth of the third month happens once. The shadow city’s nineteenth can be held in mid-air while people walk around it and look.

They found the cause. The bronze vessel of the water clock contracted in the spring cold and lost one beat a night, and the beat it lost was a flood signal. The two measures were missing because the clerk recorded what was received while the granary opened against what was due. The rider was stopped because the pontoon’s cable had soaked for three days and no one had changed it.

Fix the vessel. Change the accounting. Replace the cable.

Then call the same lamp number again.

The same rain. The same missed beat. The same late rider. But this time the granary opened against what was due, and the two extra measures were exactly enough to feed three towns until the water fell. The bridge did not lift.

The same disaster, the second time, and the city stood.

— —

In the third year, a new person joined the city.

His job was called troublemaking. He did not defend anything. He only made trouble inside the play: cut one cable, jam one granary door, make a messenger turn his ankle on the road, make a rain fall half an hour longer than forecast.

The rule was the same. Every piece of trouble had to be drawn from the book of lots, and every piece had to be written down with its lamp number.

So trouble got numbers. The city’s young people shook the lots all night, ten thousand lamp numbers in one night. Whichever number brought the city down was copied onto the wall; the next day they called that number and replayed the night, and where it broke was plain to see.

In time, the children of Wangchuan could recite the numbers on the wall the way they recited the solar terms.

— —

But Yan kept a wooden board by the door of the hall, and on it were two lines.

The shadow city is not Wangchuan.

The rain jar in the hall is round; real rain is not. The planks in the hall were measured from last year’s grain; real wood grows worms. The shadow city can tell you that within this set of rules, this is how it falls. It cannot tell you that the real sky will rain this way.

Below those lines was a smaller line.

No sky indoors.

If anyone looks up at the sky, the shadow city comes apart.

By now you’ve probably recognized it: this is Deterministic Simulation Testing.

What it is

Deterministic Simulation Testing (DST) runs an entire distributed system inside a fully reproducible simulated environment. Its most famous practitioner is FoundationDB: a whole database cluster — network, disk, clock, randomness — runs inside a single-threaded simulator, with the same interfaces serving both production and simulation.

The method is a set of interlocking rules:

  • One event loop, a virtual clock. No real sleep, no real time.Now(). Time is an integer advanced by an event queue; timeouts, retries, and scheduling all execute in virtual-time order.
  • Every source of nondeterminism is replaced. The network becomes a simulated network that can delay, reorder, duplicate, drop, and partition at will. The disk becomes a simulated disk that can lose writes, lie about fsync, tear a write, or crash and restart. The clock becomes a simulated clock that can jump. Threads become simulated threads. The only randomness left is a seeded PRNG.
  • The seed is the reproduction. One run is a pure function of f(seed, binary, config). Keep the seed and the same failure replays in a second — no “run it again and hope it lands.”
  • Fault injection. The simulator can crash any node and cut any link at any moment. FoundationDB adds BUGGIFY, which fires faults at specific code sites with a fixed probability (about one in four), only under simulation — so the same seed always fires in the same places.
  • Time is compressed. Thousands of simulated nodes in one single-threaded process; an hour of wall clock can cover hundreds of thousands of cluster-hours, and seeds can be swept all night.

The critical difference from ordinary integration testing is not the simulation. It is the determinism. In a normal test, a rare timing bug lands once and you cannot catch or reproduce it; you can only pray it stays away. In DST, that bug has a seed, and the seed is its address.

Why it matters

Most distributed-systems bugs live in the seams of timing: a message arriving three milliseconds late, an fsync silently swallowed, two nodes both convinced they are leader at the same instant. Such bugs have vanishing probability in small tests, and when they strike production they often leave nothing in the logs.

DST turns rare timing into a named input. Failures need not be waited for; you specify them. And after you find one you need not “work out how to reproduce it” — you write down the seed. This moves distributed testing from a game of probability to a game of enumeration, and an enumeration you can pause, rewind, and inspect frame by frame.

It changes more than testing. It changes the engineering loop. FoundationDB wired simulation into day-to-day development: a change comes in, the seed swarm runs, and a failure hands you a seed directly. TigerBeetle built the same idea into VOPR (Viewstamped Operation Replicator), running real consensus and storage-engine code against a controllable world. Antithesis pushed determinism down into a custom hypervisor, The Determinator, so an entire machine’s virtual hardware, network, and time are deterministic and the system under test barely changes. In the Rust ecosystem, madsim packages the same approach as a library.

It also draws its own boundary, and the boundary is hard:

  • Fidelity. A simulator is a model, not the world. Whatever it does not model — real NIC queueing, kernel page-cache semantics, GC pauses, real hardware failure rates — it cannot test. Model it wrong and the test passes quietly, then production falls over.
  • Discipline. One hidden read of the real clock, one background thread, one third-party library not wired into the simulator, and determinism is broken; replay forks. This is not a rule you can mostly follow. It is all or nothing.
  • Not a performance test. A virtual clock measures logic, not latency. Throughput and tail latency still belong on real hardware.

So DST does not replace integration tests, chaos engineering, canaries, or formal verification. It replaces the practice of running integration tests over and over in the hope of hitting a timing bug — and it lifts the hardest class of bug, rare, fatal, and irreproducible, to the place where a unit test already lives: named, and replayable.

Metaphor mapping

  • Wangchuan → the distributed system under test
  • The shadow city → the simulator
  • The keeper of hours → the single event loop and virtual clock
  • No sky indoors → no real wall clock, network, disk, or entropy source
  • Lots in the sealed book → the seeded PRNG; every nondeterministic choice is drawn from it
  • The lamp number → the random seed; the same number is the same reproducible run
  • Stopping on the 4,112th beat → pausing at an arbitrary virtual instant to inspect global state
  • The missed drumbeat, the two missing measures, the lifted pontoon → injected faults and the real bugs they expose
  • Walking into the scene to read the granary and open the clock → frame-by-frame debugging through deterministic replay
  • Fixing the vessel, the accounting, the cable → changing the code or configuration under test
  • The same lamp number, the second time, and the city stands → the fix verified against the very same scenario, now a regression case
  • The troublemaker → the fault injector, including BUGGIFY-style site injection
  • Ten thousand lots in one night → swarm testing, thousands of simulated runs
  • Thirty years in one night → time dilation; virtual time far outruns wall clock
  • The shadow city is not Wangchuan → the fidelity limit: behavior the model omits cannot be tested
  • Looking up at the sky tears the shadow city apart → one uncontrolled source of nondeterminism forks the replay
  • The numbers on the wall, recited like solar terms → failing seeds settling into a reproducible regression set
Daily Fables每日寓言 2026-10-23