결정론적 시뮬레이션 테스트를 이해하고 구현을 위한 기반을 정리한다.
URL: https://databases.systems/posts/open-source-antithesis-p1
2024년 6월 14일
결정론적 시뮬레이션 테스트를 이해하고 구현을 위한 기반을 정리하기
_Antithesis_가 뭐냐고 궁금할 수도 있습니다. 그들이 스스로를 설명하는 방식은 다음과 같습니다(그들의 웹사이트에서):
Antithesis는 시뮬레이션 환경 안에서 소프트웨어의 문제를 자율적으로 찾아내는 지속적 신뢰성(continuous reliability) 플랫폼입니다. 우리가 찾는 모든 문제는 완벽하게 재현될 수 있어, 가장 복잡한 문제조차 효율적으로 디버깅할 수 있습니다.
Antithesis 뒤에 있는 사람들은 FoundationDB에서 **DST(deterministic simulation testing, 결정론적 시뮬레이션 테스트)**를 개척한 이들입니다. 그렇다면 결정론적 시뮬레이션 테스트란 무엇일까요?
어떤 사람에게 DST는 초능력입니다. 하지만 다른 사람에게는 그저 자동화 테스트의 한 변형처럼 보이기도 합니다. 여기서는 중립적으로, 과장 없이 설명해 보겠습니다.
개발자로서 여러분은 코드가 어디서 깨질지 미리 예측하는 초능력을 대개 갖고 있지 않습니다. 모든 것이 관측 가능한 것도 아니고, 여러분의 코드는 생애 동안 몇 가지 “미지의 미지(unknown-unknowns)”를 만날 가능성이 매우 큽니다.
대부분의 시스템은 본질적으로 비결정적입니다. 프로그램을 실행할 때마다 시계/타이머, 스케줄러, 네트워크, 특정 CPU 명령 등 같은 외부 요인의 영향을 받아 비결정성이 유입됩니다.
이런 시스템을 테스트하는 일은 어렵습니다. 유닛/통합 테스트 같은 전통적인 테스트 방법은 버그를 재현하는 능력에 한계가 있습니다. 어떤 팀은 정형 검증(formal verification)으로 시스템을 모델링하고 검증할 자원도 있겠지만, 대부분은 그럴 시간과 자원이 없을 것입니다.
여기서 DST(결정론적 시뮬레이션 테스트)가 등장합니다. 가장 좋은 사례 연구는 FoundationDB인데, 이 글의 뒤에서 다루겠습니다.
DST를 이해하려면, Antithesis 창립자 Will Wilson의 StrangeLoop 2014 발표를 들어보길 권합니다. 제가 설명하는 것보다 훨씬 더 잘 설명합니다.
하지만 귀찮다면, 짧게 요약하면 이렇습니다. DST는 코드를 단일 스레드 시뮬레이터에서 실행하는데, 비결정성을 유발하는 대부분의 외부 인터페이스를 모킹(mocking)해서 결정론적으로 만듭니다. 그리고 시뮬레이터가 여러 결함을 주입(inject)하면서 버그를 찾고, 발견된 버그는 해당 실행에서 사용된 시드(seed)를 기반으로 결정론적으로 재현할 수 있습니다.
이거 결함 주입(fault injection)이나 카오스 엔지니어링(chaos engineering) 아닌가요? 음… 기술적으로는 어느 정도 그렇습니다. 하지만 DST가 빛나는 지점은, 시간이 DES(discrete event simulation, 이산 사건 시뮬레이션)에서의 시간 전진 방식처럼 전진할 수 있다는 점입니다. 이산 사건 시뮬레이션(DES)은 시스템의 동작을 시간 속 사건들의 연속으로 모델링하는데, 각 사건은 특정 시점에 발생하고 시스템의 상태 변화(state change)를 표시합니다. DST는 DES의 확장판이라고 볼 수도 있지만, 해석은 독자에게 맡기겠습니다.
시간 전진(time advancement)의 이점 덕분에, 더 짧은 시간 안에 수년치 시스템 동작을 시뮬레이션할 수 있습니다. 이는 시스템 동작을 이해하고, 일정 시간이 지난 뒤에야 드러나는 문제를 찾아내는 데 매우 효과적입니다.
결정론적 시뮬레이션 테스트의 또 다른 장점은 네트워크 같은 인터페이스들이 모킹되기 때문에, 해당 작업을 처리하는 데 필요한 시간이 줄어들 수 있다는 점입니다.1
하지만 함정도 있습니다. DST를 제대로 활용하려면 시스템이 결정론적이어야 합니다. FoundationDB, TigerBeetle 등은 결정론성을 목표로 한 설계 결정을 했기 때문에 DST를 활용할 수 있었습니다.
모든 사람이 시스템을 결정론적으로 다시 작성할 시간이나 관심이 있는 건 아닙니다. 그리고 DST가 가져오는 오버헤드(통합 노력 증가, 설계 난이도 등)를 감당하고 싶지 않은 사람도 있습니다. 요즘은 대부분의 시스템이 쓸데없는 통합을 늘려가는 시대(psst, AI)이니, 그런 선택도 충분히 괜찮다고 생각합니다.
그렇다면, DST를 워크로드에 통합하고 싶지만 그럴 자원이 없는 사람들은 어떨까요?
Antithesis는 이 문제를 풀려고 합니다. Antithesis 플랫폼을 이용하면, 현재 시스템을 최소한으로 변경하면서도 결정론적 시뮬레이션 테스트의 이점을 얻을 수 있습니다. Antithesis를 쓰려면 앱을 컨테이너로 패키징하고, 몇 가지 설정과 테스트할 워크로드를 추가한 뒤, Antithesis가 테스트를 돌리도록 하면 됩니다2. 당연히 유료 플랫폼인데, 일반 사용자에게도 충분히 감당 가능한 가격인지는 잘 모르겠습니다.
그런데 그 전에, 이 프로젝트의 목표가 무엇인지 생각할 수도 있습니다. 왜 굳이 오픈 소스 버전을 만들고 싶을까요?
주된 이유는 두 가지입니다:
이건 야심찬 프로젝트이고, 동작하는 프로토타입까지 수년이 걸릴 가능성이 큽니다. 하지만 ~6년 동안 Antithesis를 만든 숙련된 엔지니어 팀의 노력을 흉내 내야 한다는 부담 없이, 실험을 해보고 싶습니다.
일단 시작점으로, 결정론적 시뮬레이션 테스트를 사용하는 다양한 시스템을 이해하려고 합니다. 제가 아는 것들을 아래에서 조금 자세히 이야기해보겠습니다.
이 글을 읽는 여러분이라면 FoundationDB를 이미 알 수도 있습니다. 그렇지 않더라도, 그들이 회복력 있는 시스템을 만들기 위해 어떤 접근을 했는지 알면 흥미로울 수 있습니다.
FoundationDB는 내결함성(fault-tolerant) 키-값 데이터베이스로, 2015년에 Apple에 인수되었습니다. 테스트에 대한 강한 집착으로 유명하며, 자세한 설명은 여기와 그들의 논문에 있습니다. 둘 다 꼭 읽어보길 추천합니다! (제 읽기 목록에서도 특히 좋아하는 자료들입니다)
요약하면, 그들은 먼저 C++ 위에 Flow라는 액터 모델을 만들었습니다(데이터베이스를 쓰기도 전에). Flow를 통해 데이터베이스의 다양한 동작을 여러 액터로 추상화했고, Flow 런타임이 이를 스케줄링할 수 있었습니다. 결과적으로 로직을 액터 기반 프로그래밍 모델로 구성했기 때문에, 결정론적 시뮬레이터와의 통합이 쉬웠습니다. 더 많은 내용이 있지만, 이후 글에서 다뤄보겠습니다.
Flow가 FoundationDB보다 먼저 생겨났다는 창립자의 HN 댓글(2013)도 있습니다:
이건 긴 프로젝트가 될 거라는 걸 알고 있었기 때문에 시작 단계에서 도구에 크게 투자했습니다. FoundationDB의 첫 2주는 C++의 속도와 액터 모델 동시성을 위한 고수준 도구를 제공하는 새로운 프로그래밍 언어를 만드는 데 썼습니다. 하지만 진짜 마법은 Flow 덕분에 단일 스레드에서 클러스터를 결정론적으로 시뮬레이션할 수 있도록 실제 코드를 그대로 쓸 수 있다는 점입니다.
첫 단계로3, 시뮬레이터는 다음처럼 랜덤 시드를 초기화합니다(CLI로 제공되지 않았다면):
// https://github.com/apple/foundationdb/blob/f27bc4ac2ba78de23e062ecfb3e8bc9a304e0c6e/fdbserver/fdbserver.actor.cpp#L1070
uint32_t randomSeed = platform::getRandomSeed();
그 다음 시뮬레이터는 네트워크, 디스크, 시간 등이 비결정성을 제거하도록 모킹된 시뮬레이션 클러스터 안에서 지정된 워크로드를 실행합니다. 워크로드(또는 테스트 파일)는 이렇게 생겼습니다:
# https://github.com/apple/foundationdb/blob/f27bc4ac2ba78de23e062ecfb3e8bc9a304e0c6e/tests/fast/AtomicBackupCorrectness.toml
[[test]]
testTitle = 'BackupAndRestore'
clearAfterTest = false
simBackupAgents = 'BackupToFile'
[[test.workload]]
testName = 'AtomicOps'
nodeCount = 30000
transactionsPerSecond = 2500.0
testDuration = 30.0
[[test.workload]]
testName = 'BackupAndRestoreCorrectness'
backupAfter = 10.0
restoreAfter = 60.0
backupRangesCount = -1
[[test.workload]]
testName = 'RandomClogging'
testDuration = 90.0
[[test.workload]]
testName = 'Rollback'
meanDelay = 90.0
testDuration = 90.0
[[test.workload]]
testName = 'Attrition'
machinesToKill = 10
machinesToLeave = 3
reboot = true
testDuration = 90.0
[[test.workload]]
testName = 'Attrition'
machinesToKill = 10
machinesToLeave = 3
reboot = true
testDuration = 90.0
이 테스트는 RandomClogging(패킷 전송 지연/일시정지, 즉 네트워크를 괴롭히기), Attrition(머신 죽이기/재부팅) 같은 더 작은 워크로드들로 구성됩니다.
시뮬레이터가 결정론적이기 때문에, 테스트 시작 시 생성된 동일한 시드를 사용해 발생한 버그를 재현할 수 있습니다.

FoundationDB 시뮬레이터(출처: 논문)
FoundationDB가 사용하는 또 다른 전략은 BUGGIFY 매크로를 광범위하게 활용하는 것입니다4. 이는 시뮬레이터가 버그로 이어질 수 있는 더 흥미로운 엣지 케이스를 찾는 데 도움을 줍니다. 이 매크로들은 시뮬레이터에서 실행될 때만 활성화됩니다.
예를 들어, 이 코드는 서버 노브(knob)를 조정하려고 합니다:
// https://github.com/apple/foundationdb/blob/f27bc4ac2ba78de23e062ecfb3e8bc9a304e0c6e/fdbclient/ServerKnobs.cpp#L556
init( ROCKSDB_ENABLE_CHECKPOINT_VALIDATION, false ); if ( randomize && BUGGIFY ) ROCKSDB_ENABLE_CHECKPOINT_VALIDATION = deterministicRandom()->coinflip();
그리고 여기서는 테스트 워크로드 안에서 BUGGIFY 매크로를 사용해 연결을 랜덤하게 닫습니다:
// https://github.com/apple/foundationdb/blob/main/fdbserver/workloads/HTTPKeyValueStore.actor.cpp#L286-L291
// sometimes randomly close connection anyway
if (BUGGIFY_WITH_PROB(0.1)) {
ASSERT(self->conn.isValid());
self->conn->close();
self->conn.clear();
}
저는 시뮬레이터가 스스로 흥미로운 실행 경로를 찾도록 두기보다는, 코드로 실패를 도입하고 시뮬레이터를 보조하는 이런 접근을 개인적으로 좋아합니다.
TigerBeetle의 시뮬레이터인 VOPR(Viewstamped Operation Replicator)5은 여러 서버와 클라이언트가 상호작용하는 클러스터를 띄우려고 합니다. 모든 종류의 I/O는 모킹되며, 전체 시뮬레이션은 단일 프로세스로 실행됩니다.
네트워크 결함과 지연은 네트워크 시뮬레이터를 통해 주입됩니다. 네트워크 결함 주입 예시는 다음과 같습니다:
// https://github.com/tigerbeetle/tigerbeetle/blob/0277b9bf4e29443e12bae4cfed36f8306c721ef0/src/testing/packet_simulator.zig#L366
if (self.options.node_count > 1 and self.should_partition()) {
self.auto_partition_network();
스토리지 결함은 인메모리 스토리지 시뮬레이터를 통해 주입됩니다. TigerBeetle이 섹터를 손상시키려는 방식은 다음과 같습니다:
// https://github.com/tigerbeetle/tigerbeetle/blob/0277b9bf4e29443e12bae4cfed36f8306c721ef0/src/testing/storage.zig#L222
/// Cancel any currently in-progress reads/writes.
/// Corrupt the target sectors of any in-progress writes.
pub fn reset(storage: *Storage) void {
log.debug("Reset: {} pending reads, {} pending writes, {} pending next_ticks", .{
storage.reads.len,
storage.writes.len,
storage.next_tick_queue.count,
});
while (storage.writes.peek()) |_| {
const write = storage.writes.remove();
if (!storage.x_in_100(storage.options.crash_fault_probability)) continue;
// Randomly corrupt one of the faulty sectors the operation targeted.
// TODO: inject more realistic and varied storage faults as described above.
const sectors = SectorRange.from_zone(write.zone, write.offset, write.buffer.len);
storage.fault_sector(write.zone, sectors.random(storage.prng.random()));
}
}
또 다른 유용한 기능은 레플리카의 모든 상태 전이를 확인하는 상태 체커(state checker)입니다. 체크섬은 AEGIS-128L에 기반하며 여기에서 구성됩니다.
// https://github.com/tigerbeetle/tigerbeetle/blob/0277b9bf4e29443e12bae4cfed36f8306c721ef0/src/simulator.zig#L327C9-L331C49
const commits = simulator.cluster.state_checker.commits.items;
const last_checksum = commits[commits.len - 1].header.checksum;
for (simulator.cluster.aofs, 0..) |*aof, replica_index| {
if (simulator.core.isSet(replica_index)) {
try aof.validate(last_checksum);
그리고 VOPR은 완전히 결정론적이기 때문에, FoundationDB에서 본 것처럼 시드로 버그를 재생(replay)할 수 있습니다.
Tokio가 작년에 Turmoil을 발표했고, 저는 한동안 관심을 두고 있었지만 이를 사용하는 프로젝트를 많이 보진 못했습니다6.
Turmoil은 호스트, 네트워크, 시간을 시뮬레이션하려고 합니다. 디스크도 시뮬레이션하는지는 확실하지 않습니다. 하지만 이전 접근들(FoundationDB, TigerBeetle 등)과 대비되는 점은, turmoil을 Rust 크레이트(crate)로 가져와서 시뮬레이션 테스트를 작성할 수 있다는 아이디어입니다.
구현은 TigerBeetle의 시뮬레이터 구현과 다소 유사합니다:
// https://github.com/tokio-rs/turmoil/blob/766108f2e48bc54092955fc374fed2e0a15505f6/src/sim.rs#L137
pub fn crash(&mut self, addrs: impl ToIpAddrs) {
self.run_with_hosts(addrs, |addr, rt| {
rt.crash();
tracing::trace!(target: TRACING_TARGET, addr = ?addr, "Crash");
});
}
// https://github.com/tokio-rs/turmoil/blob/766108f2e48bc54092955fc374fed2e0a15505f6/src/sim.rs#L367
let World {
rng,
topology,
hosts,
..
} = world.deref_mut();
topology.deliver_messages(rng, hosts.get_mut(&addr).expect("missing host"));
시뮬레이터에 RNG(난수 생성기), 네트워크 모킹 등이 내장되어 있습니다. 각 tick마다 메시지 전달 같은 작업을 수행합니다. 특별히 새로운 아이디어는 아니지만, 그렇다고 구현 작업의 가치를 깎아내릴 수는 없습니다. Rust 사용자들을 돕기 위해 Tokio 팀이 이를 오픈 소스로 제공한 점은 훌륭하다고 생각합니다.
Coyote는 C# 코드를 결정론적으로 테스트할 수 있는 또 다른 라이브러리입니다. 그런데 놀라운 점은 코드 한 줄도 바꿀 필요가 없다는 것입니다.
이는 테스트 시점에 바이너리 리라이팅(binary rewriting)을 수행해서, Coyote가 태스크 스케줄러를 제어할 수 있도록 코드를 주입합니다. 이해하기 쉬운 리라이팅 패스 예시는 다음과 같습니다:
// https://github.com/microsoft/coyote/blob/20a461738abb16d595def740fc486c9071a9cbab/Source/Test/Rewriting/Passes/Rewriting/CallSiteExtractionRewritingPass.cs#L14-L50
/// Rewriting pass that injects callbacks to the runtime for extracting call-site information.
/// </summary>
internal sealed class CallSiteExtractionRewritingPass : RewritingPass
{
/// .... removed some boilerplate here
/// <inheritdoc/>
protected override void VisitMethodBody(MethodBody body)
{
if (this.IsCompilerGeneratedType || this.IsAsyncStateMachineType ||
this.Method is null || this.Method.IsConstructor ||
this.Method.IsGetter || this.Method.IsSetter)
{
return;
}
// Get the first instruction in the body.
Instruction nextInstruction = body.Instructions.FirstOrDefault();
// Construct the instructions for notifying the runtime which method is executing.
string methodName = GetFullyQualifiedMethodName(this.Method);
Instruction loadStrInstruction = this.Processor.Create(OpCodes.Ldstr, methodName);
TypeDefinition providerType = this.Module.ImportReference(typeof(Operation)).Resolve();
MethodReference notificationMethod = providerType.Methods.FirstOrDefault(m => m.Name == nameof(Operation.RegisterCallSite));
notificationMethod = this.Module.ImportReference(notificationMethod);
Instruction callInstruction = this.Processor.Create(OpCodes.Call, notificationMethod);
this.Processor.InsertBefore(nextInstruction, this.Processor.Create(OpCodes.Nop));
this.Processor.InsertBefore(nextInstruction, loadStrInstruction);
this.Processor.InsertBefore(nextInstruction, callInstruction);
각 실행에서 Coyote는 다양한 전략으로 서로 다른 실행 경로를 찾아보고, 버그를 보고한 경우 재현 가능한 트레이스를 생성합니다. 사용되는 전략들은 다음과 같습니다(더 있을 수도 있습니다):
// https://github.com/microsoft/coyote/blob/20a461738abb16d595def740fc486c9071a9cbab/Source/Core/Runtime/Scheduling/OperationScheduler.cs#L11-L18
using BoundedRandomFuzzingStrategy = Microsoft.Coyote.Testing.Fuzzing.BoundedRandomStrategy;
using DelayBoundingInterleavingStrategy = Microsoft.Coyote.Testing.Interleaving.DelayBoundingStrategy;
using DFSInterleavingStrategy = Microsoft.Coyote.Testing.Interleaving.DFSStrategy;
using PrioritizationFuzzingStrategy = Microsoft.Coyote.Testing.Fuzzing.PrioritizationStrategy;
using PrioritizationInterleavingStrategy = Microsoft.Coyote.Testing.Interleaving.PrioritizationStrategy;
using ProbabilisticRandomInterleavingStrategy = Microsoft.Coyote.Testing.Interleaving.ProbabilisticRandomStrategy;
using QLearningInterleavingStrategy = Microsoft.Coyote.Testing.Interleaving.QLearningStrategy;
using RandomInterleavingStrategy = Microsoft.Coyote.Testing.Interleaving.RandomStrategy;
Coyote는 정말 멋집니다. 코드 주입으로 덜 침습적인 접근을 하는 점이 마음에 드는데, C#에서만 쓸 수 있다는 점은 아쉽습니다.
Magical Deterministic Simulator(Madsim)은 Rust 프로그램을 위한 결정론적 시뮬레이터로, PRNG(의사 난수 생성기), 태스크 스케줄러, 네트워크/디스크 시뮬레이터를 활용해 환경에서 비결정성의 원인을 제거한다는 점에서 다른 것들과 같은 개념을 따릅니다.
결정론적 시간을 제공하기 위해 libc의 gettimeofday 함수를 오버라이드하려고 합니다:
// https://github.com/madsim-rs/madsim/blob/main/madsim/src/sim/time/system_time.rs#L6-L21
unsafe extern "C" fn gettimeofday(tp: *mut libc::timeval, tz: *mut libc::c_void) -> libc::c_int {
// NOTE: tz should be NULL.
// Linux: The use of the timezone structure is obsolete; the tz argument should normally be specified as NULL.
// macOS: timezone is no longer used; this information is kept outside the kernel.
if tp.is_null() {
return 0;
}
if let Some(time) = super::TimeHandle::try_current() {
// inside a madsim context, use the simulated time.
let dur = time
.now_time()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
tp.write(libc::timeval {
tv_sec: dur.as_secs() as _,
tv_usec: dur.subsec_micros() as _,
});
// more code ....
또한 FoundationDB에서 영감을 받아 BUGGIFY 매크로를 사용해 결함 주입을 트리거하는 것처럼 보입니다.
하지만 이것도 결국 Rust 기반 프로젝트에서만 사용할 수 있습니다. 그럼에도 std API 함수들에 패치를 적용해 결정론성을 달성한 점은 흥미롭습니다.
마지막으로 Antithesis입니다. 이 목록에서 가장 흥미로운 존재죠.
Antithesis가 큰 인기를 얻는 이유는, 단일 언어/런타임에 묶이는 기존 솔루션과 달리 최소한의 변경만으로도 거의 모든 애플리케이션을 그들의 플랫폼에서 결정론적으로 테스트할 수 있다는 점입니다.
이는 그들의 독점 결정론적 하이퍼바이저7 덕분인데, 비결정적인 코드도 하이퍼바이저 안에서 실행되면 결정론적으로 만들 수 있도록 보장합니다.
그들의 하이퍼바이저는 bhyve 하이퍼바이저를 기반으로 하고, 결정론성을 위해 코어에 많은 변경을 가했습니다. 구축 과정에서 겪은 도전과제는 그들의 블로그 포스트를 읽어보길 추천합니다. 인상적인 작업이고, 그들이 수년간 쏟은 노력의 가치가 충분히 있습니다.
시뮬레이션 환경은, 테스트 대상 시스템을 여러 컨테이너 집합으로 패키징하고 워크로드와 함께 결정론적 하이퍼바이저가 관리하는 VM에서 실행하는 형태로 구성됩니다.
버그를 찾기 위해, 그들은 “software explorer”를 갖고 있는데 이는 일부 유도형 퍼징(guided fuzzing)과 결함 주입 프레임워크를 통해 상태 공간(state space)을 효율적으로 탐색하면서 새롭고 흥미로운 실행 경로(좀 더 공식적으로는 브랜치 탐색)를 적극적으로 찾아냅니다.
정리하면, (PRNG, 모킹된 I/O, 네트워크 등) 결정론적 환경을 만들고, 결함 주입과 지능적인 퍼징으로 상태 공간을 효율적으로 탐색해 버그를 찾는다는 점에서 같은 패턴을 따릅니다. 하지만 진짜 차별점은 결정론적 하이퍼바이저이고, 제 생각엔 이걸 복제하기가 특히 어렵습니다.
Facebook/Meta도 이전에 Hermit로 비슷한 시도를 했는데, 이는 Reverie를 통해 시스템 콜 인터셉션을 가능하게 하여 결정론적 샌드박스에서 프로그램을 실행하는 프로젝트입니다. 하지만 Hermit는 유지보수 모드인 것 같아서, 앞으로가 어떻게 될지는 잘 모르겠습니다.
“software explorer” 같은 다른 구성 요소들도 Antithesis를 매력적으로 만드는 핵심으로 보입니다. 그들의 상태 공간 탐색이 현대 퍼징 기술에 비해 독특하거나(혹은 성능이 좋거나) 할 것이라 추정하지만, 제가 완전히 틀렸을 수도 있고, 그냥 현대적/전통적 탐색 기법을 잘 섞은 결과일 수도 있습니다.
Antithesis는 유료 제품[$]이라 내부에 대한 정보가 많지 않아 더 자세히 말하긴 어렵습니다. 아마 그들의 비법을 곧 공개하진 않겠지만, 그래도 정말 흥미로운 데모를 공개해줘서 고맙게 생각합니다!
지금까지 결정론적 시뮬레이션 테스트에 대한 몇 가지 접근을 이야기했으니, 이제 이 프로젝트에 대해 제가 머릿속으로 생각하고 있는 설계를 정리해볼 차례입니다.
Openthesis8는 앞서 논의한 시스템들에서 설계 결정을 일부 차용합니다. 아래는 이 프로젝트의 대략적인 초기 아키텍처 다이어그램입니다(클릭해서 확대):

Openthesis의 대략적인 아키텍처 다이어그램
시스템은 여러 부분으로 구성됩니다:
“core”: 제공된 워크로드를 기반으로 클러스터를 구성하는 중앙 오케스트레이터입니다. 워크로드는 코드의 일부이거나 컨테이너 내부에서 읽을 수 있는 형태가 됩니다. 이는 다양한 워크로드 구성으로 서로 다른 시나리오를 테스트하는 FoundationDB 접근에서 영감을 받았습니다.
“explorer”: 효율적인 상태 공간 탐색을 담당합니다. “유도형 퍼저(guided fuzzer)”, 결함 주입 도구, 리포트 생성기로 구성됩니다.
Testbed: 비결정성의 원인을 제거한 결정론적 하이퍼바이저입니다. Antithesis의 접근을 기반으로 하며, 이 프로젝트가 거의 모든 종류의 애플리케이션을 테스트할 수 있길 원합니다. 다만 엄청난 과업입니다.
다이어그램에 포함되지 않은 것들:
BUGGIFY 스타일의 결함 주입을 지원하면 좋겠지만, 그러려면 몇 가지 언어에 대한 SDK를 유지보수해야 합니다. 가능하긴 하겠지만, 지금은 아직 깊게 생각하진 않았습니다.프로젝트 저장소는 여기입니다. 아직 코드는 없지만, 앞으로 몇 주 동안 더 많은 설계 문서(또는 노트)를 추가할 예정입니다.
2부에서는 타임 트래블 디버깅(rr 프로젝트9 등), 유도형 퍼징, 그리고 무엇보다도 결정론적 하이퍼바이저라는 거대한 과업에 어떻게 접근할지에 대해 설명해보려 합니다. 진짜 큰 도전이겠지만 기대됩니다! 또한 어떤 형태의 기여든 열려 있습니다. 설계 결정에 대한 토론, 코드 등 무엇이든요.
일부 섹션(TigerBeetle 등)은 의도적으로 설명을 짧게 했고, 다른 것들(Resonate, Dropbox의 Nucleus 랜덤 테스트, PolarSignals의 WASM 기반(대체로) DST)은 글이 너무 길어져서 다루지 않았습니다. 더 자세한 내용이 궁금하다면 해당 자료들도 확인해보길 추천합니다.
이 글이 좋았다면 질문, 정정, 아이디어 등을 이메일 또는 **트윗**으로 보내주세요! 그리고 Antithesis나 비슷한 회사에서 일하는 분이라면 여러분의 생각도 꼭 들려주세요!