第一章:抖音弹幕实时推送的系统架构与性能挑战
抖音日均弹幕峰值超千万条/秒,要求端到端延迟低于200ms,同时保障99.99%的投递成功率。这一目标催生了以“边缘计算+分层消息管道”为核心的混合架构体系。
核心架构分层设计
- 接入层:基于自研轻量级长连接网关(支持QUIC协议),单节点承载50万+并发WebSocket连接,通过TLS 1.3握手优化将建连耗时压至
- 路由层:采用一致性哈希+动态权重调度,将用户会话精准映射至就近边缘POP节点,规避跨城回源;
- 分发层:融合Redis Streams(用于高吞吐有序广播)与Apache Pulsar(用于持久化与多订阅场景),按弹幕热度自动分流——热直播间走内存直推,冷直播间走磁盘队列保序;
- 终端层:客户端内置弹幕缓冲区(默认3s滑动窗口)与丢帧补偿策略,当网络抖动时优先丢弃低优先级弹幕(如普通文本),保留高优先级(如打赏特效、管理员指令)。
关键性能瓶颈与应对策略
高并发写入导致Redis集群热点Key问题:直播间ID作为Stream Key,在头部直播间(如明星直播)引发单分片QPS超12万。解决方案为二级Key打散:
# 将原始直播间ID: "123456" 改写为 "123456:shard_07"
# 使用CRC32(直播间ID) % 64 计算shard_id,实现64路分片
redis-cli --raw -h edge-redis-01 -p 6379 \
XADD "123456:shard_07" * \
user_id "u98765" \
content "666" \
timestamp "1717023456123"
该方案使单分片负载下降至1.8万QPS,P99延迟稳定在12ms内。
实时性保障机制
| 机制类型 | 技术实现 | 效果 |
|---|---|---|
| 网络路径优化 | BGP Anycast + 边缘节点DNS智能解析 | 首包RTT降低35% |
| 消息零拷贝传输 | Netty DirectByteBuf + mmap文件映射 | GC暂停时间减少70% |
| 异常快速熔断 | 基于Sentinel的QPS/错误率双维度阈值 | 故障隔离响应时间 |
流量洪峰期间,系统通过动态扩缩容策略(K8s HPA联动Pulsar topic分区数)维持SLA,近半年未发生弹幕大规模积压事件。
第二章:EPOLL底层机制与Go运行时网络模型深度剖析
2.1 Linux EPOLL事件驱动原理与就绪队列行为实测
EPOLL 的核心在于内核维护的就绪队列(ready list),它仅在文件描述符真正就绪时才将 epitem 加入该队列,避免轮询开销。
就绪队列触发条件
- 可读:接收缓冲区字节数 ≥
SO_RCVLOWAT(默认1) - 可写:发送缓冲区空闲空间 ≥
SO_SNDLOWAT(默认1) - 异常:带外数据或连接错误
实测验证代码
int epfd = epoll_create1(0);
struct epoll_event ev = {.events = EPOLLIN | EPOLLET, .data.fd = sockfd};
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 边沿触发模式
EPOLLET启用边沿触发,内核仅在状态变化瞬间(如从不可读→可读)插入就绪队列;若未一次性读完,后续epoll_wait()不再通知,需配合非阻塞 I/O 循环读取。
| 触发模式 | 就绪队列行为 | 典型适用场景 |
|---|---|---|
| LT(默认) | 只要就绪持续存在,每次 epoll_wait 都返回 |
简单同步逻辑 |
| ET | 仅状态跃迁时入队,需循环读/写至 EAGAIN |
高性能服务器(如 Nginx) |
graph TD
A[socket 收到 TCP 数据包] --> B{内核协议栈处理}
B --> C[拷贝至 sk_buff → 接收队列]
C --> D{是否满足 EPOLLIN 条件?}
D -- 是 --> E[将 epitem 插入就绪队列]
D -- 否 --> F[保持静默]
2.2 Go netpoller源码级解析:goroutine调度与fd注册生命周期
Go 的 netpoller 是 runtime 与 net 包协同实现 I/O 多路复用的核心,其本质是封装 epoll(Linux)、kqueue(macOS)或 iocp(Windows)的事件驱动抽象。
fd 注册与 goroutine 绑定机制
当调用 conn.Read() 且底层 fd 尚未就绪时,runtime.netpollblock() 将当前 goroutine 挂起,并通过 pollDesc.prepare() 将 fd 注册到 netpoller 实例中:
// src/runtime/netpoll.go
func (pd *pollDesc) prepare(rg, wg *g, pollable bool) bool {
// 原子标记为“正在等待读/写”,避免重复注册
state := atomic.LoadUint64(&pd.rg)
if state == pdReady || state == pdClosing {
return false
}
// 将当前 goroutine 地址写入 rg/wg 字段,供事件就绪后唤醒
atomic.StoreUint64(&pd.rg, uintptr(unsafe.Pointer(rg)))
return true
}
逻辑分析:
pd.rg存储等待读事件的 goroutine 指针;prepare()保证单次注册幂等性;pollable=false时跳过系统调用注册,用于内部同步场景。
生命周期关键状态流转
| 状态 | 触发时机 | 后续动作 |
|---|---|---|
pdReady |
事件就绪,netpoll() 返回 |
netpollunblock() 唤醒 goroutine |
pdClosing |
Close() 被调用 |
清理 epoll/kqueue 注册项 |
pdWait |
netpollblock() 进入休眠 |
netpollgoready() 唤醒时重置 |
goroutine 唤醒流程
graph TD
A[fd 可读] --> B{netpoll 循环检测}
B --> C[从 epoll_wait 获取就绪列表]
C --> D[遍历 pd 链表,匹配 fd]
D --> E[atomic.LoadUint64(&pd.rg)]
E --> F[调用 goready(rg) 恢复 goroutine]
2.3 弹幕场景下EPOLL_LT vs EPOLL_ET选型实验与吞吐对比
弹幕服务需在单连接高并发写入(如百万级用户秒级刷屏)下保障低延迟与高吞吐,epoll触发模式成为关键瓶颈。
实验环境配置
- 4核16GB云服务器,Linux 5.15,
SO_REUSEPORT启用 - 模拟客户端:基于
libuv的10万长连接压测器,每连接平均30 msg/s弹幕注入
核心代码差异
// LT模式:每次epoll_wait返回后,若缓冲区未读尽,下次仍触发
int flags = EPOLLIN; // 无EPOLLET标志 → LT默认行为
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &(struct epoll_event){.events=flags, .data.fd=fd});
// ET模式:仅当状态从“不可读”变为“可读”时触发一次,须循环read直到EAGAIN
int flags = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &(struct epoll_event){.events=flags, .data.fd=fd});
逻辑分析:LT模式对应用层更友好(无需一次性读完),但内核需反复检查就绪态,增加系统调用开销;ET模式要求应用严格处理EAGAIN,但减少重复通知,适合高吞吐场景。
吞吐对比(QPS)
| 模式 | 平均QPS | P99延迟(ms) | CPU sys% |
|---|---|---|---|
| LT | 84,200 | 12.7 | 38.1 |
| ET | 132,600 | 5.3 | 22.4 |
数据同步机制
- ET模式下必须配合
recv(fd, buf, len, MSG_DONTWAIT)+while (!EAGAIN)循环,否则丢包 - LT模式可安全使用单次
recv,但易因未清空缓冲区导致后续事件饥饿
graph TD
A[epoll_wait返回] --> B{EPOLLET?}
B -->|Yes| C[循环recv至EAGAIN]
B -->|No| D[单次recv,依赖下次通知]
C --> E[高吞吐/低延迟]
D --> F[开发简单/内核开销高]
2.4 高并发连接下EPOLL触发模式对延迟抖动的影响建模
在万级并发场景中,EPOLLET(边缘触发)与EPOLLONESHOT组合可显著抑制就绪事件重复入队,但会放大单次处理延迟的传播效应。
EPOLLONESHOT + ET 的典型配置
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT; // 关键:一次就绪仅通知一次
ev.data.fd = sockfd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
逻辑分析:EPOLLONESHOT强制应用显式重启用(epoll_ctl(..., EPOLL_CTL_MOD, ...)),避免就绪链表持续扫描;EPOLLET消除水平触发下的多次唤醒开销。但若业务处理耗时波动(如GC暂停、锁争用),将导致就绪事件积压,引发后续批次的延迟尖峰。
延迟抖动关键因子对比
| 触发模式 | 平均延迟 | P99抖动增幅 | 就绪事件吞吐量 |
|---|---|---|---|
EPOLLIN(LT) |
12μs | +380% | 低(频繁唤醒) |
EPOLLIN \| EPOLLET |
8μs | +110% | 高 |
EPOLLIN \| EPOLLET \| EPOLLONESHOT |
7.5μs | +220% | 最高(需手动恢复) |
事件处理闭环流程
graph TD
A[fd就绪] --> B{EPOLLONESHOT生效?}
B -->|是| C[仅通知一次]
C --> D[应用处理+解析]
D --> E[显式EPOLL_CTL_MOD重启用]
E --> F[重新进入就绪队列]
B -->|否| G[持续通知直至read返回EAGAIN]
2.5 基于perf+eBPF的EPOLL瓶颈定位实战:从syscall到goroutine阻塞链路追踪
当Go服务在高并发EPOLL场景下出现延迟毛刺,传统strace无法穿透runtime调度层。需构建跨内核态与用户态的可观测链路。
关键观测点
sys_enter_epoll_wait(内核入口)runtime.epollwait(Go runtime封装)gopark调用栈(goroutine挂起时刻)
eBPF追踪脚本核心片段
// trace_epoll.bpf.c
SEC("tracepoint/syscalls/sys_enter_epoll_wait")
int trace_epoll_enter(struct trace_event_raw_sys_enter *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_map_update_elem(&epoll_start, &pid, &ctx->id, BPF_ANY);
return 0;
}
逻辑说明:捕获
epoll_wait系统调用起始时间戳,以PID为key存入epoll_start哈希表;BPF_ANY确保覆盖重复调用。该映射后续与goroutine状态联动分析。
阻塞链路还原流程
graph TD
A[perf record -e syscalls:sys_enter_epoll_wait] --> B[eBPF采集进入/退出时间]
B --> C[关联Goroutine ID via /proc/pid/maps + symbol offset]
C --> D[输出完整阻塞路径:syscall → netpoll → gopark]
| 指标 | 正常阈值 | 异常表现 |
|---|---|---|
| epoll_wait平均耗时 | > 100μs(暗示就绪队列积压) | |
| goroutine park频率 | > 5000/s(频繁调度抖动) |
第三章:Channel在弹幕分发链路中的性能陷阱与重构策略
3.1 无缓冲/有缓冲Channel在百万级弹幕扇出场景下的内存与GC压力实测
在高并发弹幕广播中,chan struct{}(无缓冲)与 chan *Danmaku(有缓冲,cap=64)表现迥异:
内存分配对比
- 无缓冲 channel:每次
send必须阻塞等待接收方就绪,导致 goroutine 频繁调度,堆上临时对象激增; - 有缓冲 channel:平滑吞吐,但缓冲区本身占用固定内存(如
make(chan *Danmaku, 64)占用约 512B 元数据 + 指针数组)。
GC 压力实测(pprof allocs profile)
| Channel 类型 | QPS | avg_alloc/op | GC pause (μs) |
|---|---|---|---|
| 无缓冲 | 82k | 1.2 MB | 120–380 |
| 缓冲(64) | 147k | 0.4 MB | 18–42 |
// 弹幕扇出核心逻辑(无缓冲版)
func fanoutUnbuffered(danmu *Danmaku, clients map[*Client]struct{}) {
for client := range clients {
select {
case client.ch <- danmu: // 阻塞点:client.ch 未及时接收即挂起
default:
// 丢弃或降级处理
}
}
}
该写法在客户端消费延迟 > 5ms 时,goroutine 积压达数千,触发高频 GC。缓冲 channel 将突发流量削峰,显著降低调度开销与堆分配频次。
数据同步机制
graph TD
A[Producer Goroutine] -->|发送*Danmaku| B[Buffered Channel cap=64]
B --> C[Consumer Goroutine Pool]
C --> D[Write to WebSocket Conn]
关键参数说明:cap=64 平衡延迟与内存——过小易溢出,过大增加 GC 扫描压力(指针数组长度影响 mark 阶段)。
3.2 Channel闭合竞态与panic传播路径分析:基于go tool trace的弹幕丢包归因
数据同步机制
弹幕服务中,chan *Danmaku 作为核心分发通道,其关闭时机与消费者 goroutine 生命周期强耦合:
// 关键竞态点:close() 调用无同步保护
func closeChannelSafely(ch chan *Danmaku, wg *sync.WaitGroup) {
wg.Wait() // 等待所有消费者退出
close(ch) // ✅ 安全关闭:确保无 goroutine 阻塞在 recv
}
若 close(ch) 在某消费者仍执行 <-ch 时触发,将导致 panic:send on closed channel 或 receive on closed channel(后者静默返回零值,引发丢包)。
panic传播链路
go tool trace 捕获到的 runtime.gopark → runtime.chansend → runtime.throw 调用栈,揭示 panic 源于生产者未检查 ok 的非阻塞发送:
| 阶段 | trace事件 | 含义 |
|---|---|---|
| T1 | GoCreate |
弹幕写入goroutine启动 |
| T2 | GoBlockSend |
阻塞于满channel |
| T3 | GoUnblock + GoEnd |
接收端退出后channel关闭,发送panic |
传播路径可视化
graph TD
A[Producer: ch <- dm] --> B{Channel closed?}
B -->|Yes| C[runtime.throw “send on closed channel”]
B -->|No| D[Success]
C --> E[panic propagates to main goroutine]
E --> F[goroutine crash → 未处理弹幕堆积丢失]
3.3 RingBuffer+Channel混合队列设计:兼顾实时性与背压控制的落地实现
在高吞吐、低延迟场景下,单一队列模型难以兼顾实时推送与反压抑制。本方案将无锁 RingBuffer 作为高速缓存层,配合带缓冲 Channel 实现流量整形与下游解耦。
核心结构设计
- RingBuffer 负责毫秒级事件暂存(固定容量 1024,避免 GC 压力)
- Channel 作为 RingBuffer 与消费者间的“节流阀”,缓冲区设为
cap=64,触发len(ch) > 48时主动降速生产者
数据同步机制
// 生产者端背压感知逻辑
select {
case ring.Write(event):
// 快路径:RingBuffer 未满,直接写入
case <-time.After(10 * time.Microsecond):
// 慢路径:短暂让出 CPU,避免忙等
}
该逻辑确保 RingBuffer 写入失败时不阻塞主线程,且通过超时退避降低竞争强度;10μs 是经压测验证的最小有效退避窗口,兼顾响应性与吞吐。
性能对比(TPS @ P99 延迟)
| 队列类型 | 吞吐(万/s) | P99 延迟(ms) | 反压响应时间 |
|---|---|---|---|
| 纯 Channel | 2.1 | 18.7 | >500ms |
| 纯 RingBuffer | 15.3 | 0.8 | 无 |
| 混合设计 | 13.6 | 1.2 |
graph TD
A[事件生产者] -->|非阻塞写入| B[RingBuffer]
B -->|批量搬运| C{Channel<br>cap=64}
C --> D[消费者组]
C -.->|len>48| E[通知限速]
第四章:三种EPOLL+Channel协同优化方案工程实践
4.1 方案一:单EPOLL实例+多Worker Channel分片(Sharded Dispatcher)
该方案以单个 epoll_wait() 实例统一监听所有客户端连接事件,通过哈希分片将就绪 fd 分发至多个无锁 WorkerChannel(Go chan),实现事件分发与业务处理解耦。
核心分发逻辑
// 将就绪fd按worker数取模分片
shardID := fd % uint32(numWorkers)
select {
case workers[shardID] <- &Event{FD: fd, Op: EPOLLIN}:
default:
// 非阻塞投递,失败则丢弃(需配合背压)
}
fd % numWorkers 确保相同连接始终路由至固定 worker,避免跨 goroutine 状态竞争;select + default 实现轻量级背压,防止 channel 缓冲区溢出。
性能对比(10K 连接下吞吐)
| 维度 | 单 Worker | 4 Worker | 8 Worker |
|---|---|---|---|
| QPS | 42k | 156k | 189k |
| P99 延迟(ms) | 8.2 | 3.1 | 2.7 |
数据同步机制
- 所有 worker 共享只读配置快照(atomic.LoadPointer)
- 连接元数据通过 per-worker slab allocator 管理,零共享内存访问
graph TD
A[epoll_wait] -->|就绪fd列表| B{Hash Shard}
B --> C[Worker-0 Channel]
B --> D[Worker-1 Channel]
B --> E[Worker-N Channel]
C --> F[Handler Goroutine]
D --> G[Handler Goroutine]
E --> H[Handler Goroutine]
4.2 方案二:EPOLL per-Worker + Channel Ring Buffer本地缓存(Local Batching)
该方案为每个 Worker 线程独占一个 epoll 实例,并在用户态维护固定大小的环形缓冲区(Ring Buffer),实现事件批处理与零拷贝本地暂存。
数据同步机制
Worker 从 epoll_wait 批量获取就绪事件后,不立即处理,而是原子写入本地 Ring Buffer;业务逻辑线程随后批量消费,规避频繁系统调用开销。
Ring Buffer 核心结构
struct RingBuffer<T> {
buf: Vec<Option<T>>, // 预分配内存,避免运行时分配
head: AtomicUsize, // 生产者索引(mod capacity)
tail: AtomicUsize, // 消费者索引
capacity: usize,
}
head/tail 使用 AtomicUsize 实现无锁读写;capacity 通常设为 1024 或 4096,兼顾缓存行对齐与吞吐平衡。
性能对比(单 Worker,10K 连接)
| 指标 | epoll LT | 本方案(batch=64) |
|---|---|---|
| syscalls/sec | ~120K | ~3.2K |
| avg latency (μs) | 85 | 22 |
graph TD
A[epoll_wait] -->|批量就绪fd| B[原子写入Ring Buffer]
B --> C[业务线程批量消费]
C --> D[本地处理+异步提交]
4.3 方案三:EPOLL事件聚合+原子计数器驱动的无锁Channel预分配(Lock-Free Prealloc)
该方案将 epoll_wait 的批量就绪事件与无锁内存预分配深度耦合,规避频繁堆分配与锁竞争。
核心设计思想
- 所有 Channel 在启动时按最大并发连接数预分配至线程本地环形缓冲区
- 使用
std::atomic<uint32_t>管理空闲索引,实现 O(1) 无锁获取/归还 - epoll 事件回调中直接索引预分配槽位,跳过
new/delete
原子计数器分配逻辑
// ring_buffer 是预分配的 Channel 数组(大小为 POWER_OF_TWO)
alignas(64) std::atomic<uint32_t> free_head{0};
uint32_t alloc() {
uint32_t idx = free_head.fetch_add(1, std::memory_order_relaxed);
return idx & (ring_buffer.size() - 1); // 位运算取模,零开销
}
fetch_add 保证多线程安全;memory_order_relaxed 因无依赖关系,极致轻量;位运算替代 % 避免除法瓶颈。
性能对比(10K 连接压测)
| 方案 | 分配延迟均值 | GC 压力 | 内存碎片率 |
|---|---|---|---|
| new/delete | 83 ns | 高 | 32% |
| Lock-Free Prealloc | 9 ns | 无 | 0% |
graph TD
A[epoll_wait 返回就绪事件列表] --> B[批量调用 alloc()]
B --> C[直接复用预置 Channel 对象]
C --> D[事件处理完毕后 recycle()]
4.4 三方案在抖音真实弹幕压测集群(10W+ QPS,P99
压测环境统一基线
- 集群规模:128 节点(64 producer + 64 consumer),K8s v1.28 + eBPF 网络插件
- 弹幕特征:平均 payload 128B,burst ratio 3.2x,乱序容忍窗口 ≤15ms
核心指标横向对比
| 方案 | Avg Latency (ms) | P99 Latency (ms) | Throughput (QPS) | Avg CPU Core Util (%) |
|---|---|---|---|---|
| Kafka + MirrorMaker2 | 42.3 | 78.6 | 102,400 | 68.2 |
| Pulsar Geo-Replication | 36.7 | 73.1 | 118,900 | 52.9 |
| 自研轻量同步网关(基于Rust+DPDK) | 28.9 | 67.4 | 135,600 | 39.7 |
数据同步机制
// 自研网关核心转发逻辑(零拷贝路径)
fn fast_forward(b: &mut Batch, dst: &mut SocketAddr) -> Result<()> {
let iovec = b.as_iovec(); // 复用用户态内存页,规避 memcpy
sendmmsg(sockfd, &iovec, MSG_NOSIGNAL | MSG_DONTWAIT)?; // 批量系统调用
Ok(())
}
该实现绕过内核协议栈,iovec 直接映射 DPDK mbuf;sendmmsg 单次 syscall 提交 32 条弹幕,吞吐提升 3.8× vs 传统 send()。CPU 利用率下降主因是中断合并与批处理。
架构决策流
graph TD
A[原始弹幕流] --> B{负载类型}
B -->|高突发| C[DPDK 用户态分流]
B -->|低延迟敏感| D[Kafka 事务管道]
C --> E[Rust 无锁 RingBuffer]
E --> F[硬件时间戳校准]
F --> G[端到端 P99 ≤67.4ms]
第五章:未来演进方向与跨语言协同思考
多运行时服务网格的生产级落地实践
在蚂蚁集团核心支付链路中,已将基于 WebAssembly 的轻量沙箱(WasmEdge)嵌入 Istio 数据平面,实现 Java、Go 和 Rust 服务间统一策略执行。例如,风控规则引擎以 Wasm 模块形式动态加载,无需重启任何语言的服务进程;2023 年双十一流量洪峰期间,该架构支撑了每秒 18 万次跨语言策略校验,平均延迟降低 42%。模块通过 WASI 接口访问加密密钥管理服务(KMS),规避了传统 sidecar 中 TLS 终止与重加密封装带来的性能损耗。
Python 与 Rust 的零拷贝内存共享方案
某智能投研平台采用 PyO3 + mmap 共享内存池,在 Python 主控流程与 Rust 高频因子计算模块间实现纳秒级数据交换。关键结构体定义如下:
#[repr(C)]
pub struct FactorInput {
pub timestamps: *const i64,
pub prices: *const f64,
pub len: usize,
}
Python 端通过 numpy.memmap 映射同一文件,避免序列化开销。实测单日 5TB 行情数据处理吞吐提升 3.7 倍,GC 停顿时间从 120ms 下降至 8ms。
跨语言错误溯源的 OpenTelemetry 扩展协议
为解决异构调用链中错误上下文丢失问题,团队设计了 otel-crosslang 扩展规范,强制要求所有语言 SDK 在 span 中注入以下字段:
| 字段名 | 类型 | 示例值 | 强制性 |
|---|---|---|---|
lang.runtime |
string | cpython-3.11.5 |
✅ |
lang.exception_type |
string | pandas.errors.InvalidIndexError |
⚠️(仅异常时) |
lang.stack_raw |
string | base64 编码原始栈帧 | ✅ |
该协议已在 Kafka 生产消费者组(Java)、实时流处理(Rust)和模型推理服务(Python)中全量启用,错误根因定位耗时从平均 47 分钟压缩至 9 分钟。
WASM 插件化 AI 推理网关
京东物流智能分单系统部署了基于 Wasmtime 的推理网关,支持 Python(PyTorch)、Go(Gorgonia)和 JavaScript(TensorFlow.js)训练的模型共存于同一 runtime。每个模型封装为 .wasm 文件,通过统一 infer() 导出函数调用。上线后,新算法迭代周期从 3 天缩短至 4 小时,且 CPU 利用率波动标准差下降 61%。
构建时类型契约验证机制
使用 Protocol Buffers v4 定义跨语言接口契约,并在 CI 流程中集成 protoc-gen-validate 与自研 crosslang-checker 工具链。当 Go 服务新增 repeated string tags 字段时,工具自动扫描 Python 客户端是否已适配 List[str] 类型注解及反序列化逻辑,未达标则阻断发布。2024 年 Q1 因类型不一致导致的线上故障归零。
多语言可观测性元数据融合
在 Grafana Tempo 中,通过 Loki 日志标签 service.language 与 Jaeger trace 标签 process.language 关联,构建统一服务拓扑图。Mermaid 可视化示例如下:
flowchart LR
A[Python Web Gateway] -->|HTTP/1.1| B[Go Order Service]
B -->|gRPC| C[Rust Inventory Service]
C -->|WASM call| D[JS Fraud Model]
style A fill:#4B8BBE,stroke:#1A4D7C
style B fill:#296E4F,stroke:#153B2A
style C fill:#DE7D31,stroke:#8C4F20
style D fill:#F0DB4F,stroke:#A3902B 