#import "@preview/typslides:1.3.0": * #import "@preview/codly:1.3.0": * #import "@preview/codly-languages:0.1.1": * #import "slides_component/figures.typ": * #show: codly-init.with() #codly(languages: codly-languages) #show raw: set text(font: ("0xProto Nerd Font", "WenQuanYi Micro Hei")) // Project configuration #show: typslides.with( ratio: "16-9", theme: "yelly", font: ("0xProto Nerd Font","WenQuanYi Micro Hei"), font-size: 20pt, link-style: "color", show-progress: true, ) // The front slide is the first slide of your presentation #front-slide( title: "MPC的通信架构设计思路", authors: "梁俊勇", info: [2025-11-21], ) // Custom outline #table-of-contents() #title-slide()[ MPC通信常见问题 ] #slide[ 在底层通信里,我们常常会遇到这样的场景: \ 一段代码需要重复调用send()或者recv()方法 \ 但是这里有较大的性能问题 ] #slide(title: "场景1: 一方向另一方发送多个数据")[ ```python # 发送方 for item in items: socket_to_peer.send(item) ``` ```python # 接收方 for i in len(items): items[i] = socket_to_peer.recv() ``` ] #slide(title: "场景2: 一方向多方发送数据")[ ```python # 发送方 for peer in peers: socket_to_peer.send(data) ``` ```python # 接收方 for peer in peers: data = socket_to_peer.recv() ``` ] #slide(title: "问题1: 通信中的数据依赖问题")[ 这其中的问题在于,每次发送与接受,都会存在数据的依赖关系。具体表现为: - 阻塞式操作: 同步编程中,`send()` 和 `recv()` 是阻塞的,程序会暂停执行,直到当前通信操作完全完成。 - 严格串行: 在循环中,后一次的通信操作(无论是发送还是接收)必须等待前一次操作彻底完成后才能启动。 - 性能瓶颈: 这种强制性的顺序等待,极大地限制了通信效率,尤其是在需要与多个参与方进行大量数据交换的场景下,导致系统无法充分利用并行处理能力和网络带宽。 ] #slide(title: "以场景2为例")[ ```python # 手动展开发送方循环代码 # peers = [alice, bob, carol] # for peer in peers: # socket_to_peer.send(data) socket_to_alice.send(data) socket_to_bob.send(data) # 在alice没有确认收到数据前,此行代码不会执行 socket_to_carol.send(data) # 在bob没有确认收到数据前,此行代码不会执行 ``` ] #slide(title: "以场景2为例")[ ```python # 手动展开接收方循环代码 # peers = [alice, bob, carol] # for peer in peers: # socket_to_peer.recv(data) socket_to_alice.recv(data) socket_to_bob.recv(data) # 在没有接收到alice数据前,此行代码不会执行 socket_to_carol.recv(data) # 在没有接收到bob数据前,此行代码不会执行 ``` ] #slide(title: "流程图表示")[ #align(center)[#show: serial_timeline()] ] #slide(title: "问题2: 传统通信协议的瓶颈")[ 我们常用的 `TCP` 和 `HTTP/1.1` 协议虽然可靠,但在大规模、高并发的MPC场景下会遇到显著的性能瓶颈: - 连接开销大: - TCP三次握手: 每次新建连接都需握手,带来固有延迟。 - TCP慢启动: 连接初期传输速度受限,短连接无法有效利用带宽。 - 协议效率低: - HTTP/1.1队头阻塞: 单个慢请求会阻塞同一连接上的后续所有请求。 - 冗余的报文头: HTTP头部是文本格式,存在大量冗余信息。 - 数据传输未经优化: - 默认无压缩: 协议本身不强制压缩数据,增大了网络负载。 - 序列化开销: 使用JSON等文本格式比二进制格式体积更大,处理速度更慢。 ] #title-slide[ 预备知识 ] #slide()[ 在我们讲解后续思路与方案之前,我们先来了解一下异步编程。 ] #slide(title: "异步编程")[ 异步编程是实现并发的一种主要方式。 我们可以用在餐厅点餐的例子来理解: - 同步(等待的方式): 你在柜台点完餐,然后就站在那里一直等到餐做好。中间什么也做不了。 - 异步(不等待的方式): 你在柜台点完餐,服务员给你一个震动的取餐器。你可以回到座位上玩手机、聊天。当取餐器震动时,你再去取餐。 在程序中: - “点餐”就是发起一个网络请求。 - “取餐器”就是一种通知机制。 - “玩手机”就是程序在等待期间可以去执行的其他代码。 ] #slide(title: "并发与异步的关系")[ 简单来说,它们是“目标”与“手段”的关系。 - 并发是我们的目标: - 我们希望程序能同时处理多个任务,提高效率。 - 异步编程是达成目标的手段之一: - 它是我们编写并发程序的一种具体方法。 好比我们的目标是“同时做好一顿饭(炒菜、煮饭、煲汤)”。 - 多线程方案: - 请三个厨师,每人负责一项。速度快,但“雇佣成本”高,且需要协调。 - 异步方案: - 一位经验丰富的厨师,他先按下电饭煲和汤煲的开关(发起异步操作),然后在等待的间隙去炒菜。通过合理安排,一个人高效地完成了所有事情。 ] #slide(title: "为什么不选择多线程方案")[ 尽管多线程也能实现并发,但在网络编程中,它常常带来一些挑战: - 数据竞争与复杂性: - 当多个线程同时尝试修改同一份数据时,很容易出现混乱,导致程序出错。 - 就像多个人同时在同一块白板上写字,最终结果可能一团糟。 - 为了避免这种混乱,程序员需要使用复杂的“锁”机制来协调,这非常容易出错,并且难以调试。 - 特定语言的限制: - 像Python这样的语言,由于其内部机制(全局解释器锁GIL),即使使用多线程,也无法真正同时运行多个计算任务,这限制了其在CPU密集型场景的性能。 - 即使是C/C++这类语言,虽然没有GIL,但手动管理线程间的同步和数据安全,也是一个巨大的挑战。 ] #slide(title: "为什么不选择多线程方案")[ - 逻辑直观性: - 即使是像Rust这样能保证线程安全的语言,多线程的逻辑也可能不如异步编程模型(特别是async/await)那样直观和易于理解,尤其是在处理大量网络I/O时。 因此,在许多网络设计场景中,异步编程往往是更简洁、高效且易于维护的选择。 ] #title-slide[ 通过有向无环图(DAG)实现通信批次化 ] #slide()[ 我们可以将数据依赖转换成图问题,进而使用拓扑排序来批次化无依赖关系的数据。\ 例如刚刚的循环发送。我们可以创建一个缓冲区,将数据先填充至缓冲区,然后在一轮循环结束的时候,统一发送。\ 接收方同理,创建缓冲区,通过循环解包出对应的数据。\ ] #slide(title: "手动批次化示例")[ ```python # 手动批次化发送方代码 # items = [item1, item2, item3] # for item in items: # socket_to_peer.send(item) buffer = [] for item in items: buffer.append(item) socket_to_peer.send(buffer) ``` ] #slide(title: "批量化流程图表示")[ #align(center)[#show: batched_timeline()] ] #slide()[ 然后是并发化,如果多个步骤可以并行进行,那么拓扑排序也可以将这些步骤一起发送处理。\ 例如刚刚的发送给多个参与方的模式。我们便可使用一次并发,在等待确认的时候,发送其他数据。 ] #slide(title: "并发化示例")[ ```python # peers = [socket_to_alice, socket_to_bob, socket_to_carol] # 创建一组并发的发送任务 tasks = [ socket_to_peer.send(data) for peer in peers ] # 同时执行所有发送任务 await asyncio.gather(*tasks) ``` ] #slide(title: "并发流程图表示")[ #align(center)[#show: async_timeline()] ] #slide(title: "动态DAG方案")[ 主流的动态DAG框架是阿里的隐语框架 @Ma2023。 #align(center)[#image("./pics/secretflow.png")] 此方案的核心思想是: - 运行时构建:计算图(DAG)不是预先生成的,而是在程序执行过程中动态构建。这允许更灵活的编程,例如,计算的流程可以根据秘密计算的中间结果发生改变。 - 通用语言:开发者使用 Python 来编写MPC逻辑。 - 分布式框架:底层依赖一个强大的分布式执行框架(Ray @moritz2018raydistributedframeworkemerging)来调度和执行图中的计算任务。 ] #slide()[ 优点: - 灵活性高,易于上手,可以处理具有复杂、数据依赖控制流的算法。 - 中文社区,有问题回复较及时。 缺点: - 运行时动态调度和框架自身的开销较大(如传输的是整个Python对象),性能不如静态方案。 - 框架组件多,使用起来复杂度高。 ] #slide(title: "静态DAG方案")[ 主流的静态DAG框架是 MP-SPDZ @mp-spdz。其工作流程完全不同: - 编译时构建:开发者使用一种专门为MPC设计的领域特定语言(DSL)编写协议。 - 提前优化:编译器将DSL代码转换成一个固定的、静态的计算图,并进行大量优化(如指令调度、通信批处理)。最终生成高效的字节码。 - 虚拟机解释:项目使用C++实现了一个虚拟机,在运行时加载并执行这些预先编译好的字节码。由于所有依赖关系都已确定,执行过程非常高效。 ] #slide()[ 优点: - 性能极高。由于在编译时就掌握了全部计算信息,可以进行全局优化,运行时开销非常小。 缺点: - 灵活性差。计算流程必须在编译前完全确定,很难实现依赖于秘密数据的动态分支。 - 需要学习相应的DSL。 ] #title-slide[ 选择快速的通信协议 ] #slide[ 传统方案是使用同步TCP来实现。但是这个方案因为TCP的握手和同步阻塞问题,性能上比较差。\ 对此,我们有多种优化方案。 ] #slide(title: "TCP长链接")[ 非常简单的一个方式就是使用TCP长连接。这个方案需要额外维护一个连接列表。可以解决频繁建立TCP连接的握手开销。 ] #slide(title: "TCP长链接代码示例")[ 核心思路是实现一个 `ConnectionManager`,它内部持有一个 `HashMap` 作为连接池。当需要通信时,我们向管理器请求一个连接。 - 如果连接已存在,管理器直接返回它。 - 如果不存在,管理器负责建立新连接,存入池中,然后返回。 这样,无论上层逻辑是发送还是接收,都无需关心连接是否已经建立。 ```rust use std::collections::HashMap; use std::net::{TcpStream, ToSocketAddrs}; use std::io::{self, Write}; struct ConnectionManager { connections: HashMap, } impl ConnectionManager { // 获取或建立一个新连接 // 这个函数体现了“维护链接”:调用者只管要连接,不用管是否已存在 fn get_connection( &mut self, addr: A ) -> io::Result<&mut TcpStream> { let addr_string = addr.to_string(); // .entry().or_insert_with() 是更地道的写法,这里为了清晰而展开 if !self.connections.contains_key(&addr_string) { let stream = TcpStream::connect(addr)?; self.connections.insert(addr_string.clone(), stream); } Ok(self.connections.get_mut(&addr_string).unwrap()) } } ``` ] #slide(title: "WebSocket:全双工通信")[ WebSocket 是另一种在TCP之上,但比原始TCP更现代化的协议。它特别适合需要频繁、实时双向通信的场景。 - 单次握手: WebSocket通过一个类似HTTP的请求发起握手,一旦成功,连接就升级为WebSocket连接,后续通信不再需要HTTP的开销。 - 全双工通信: 连接建立后,客户端和服务器可以随时互相发送消息,实现了真正的双向通信,延迟极低。 - 轻量级: 相比HTTP,WebSocket的消息帧头非常小,减少了网络开销。 对于需要参与方之间进行大量实时交互的MPC协议,WebSocket是一个比传统TCP长连接或HTTP轮询更高效的选择。 ] #slide(title: "序列化:数据的高效编码")[ 序列化是将内存中的数据结构(如对象、结构体)转换为可以存储或传输的格式(如字节流)的过程。在MPC中,我们需要在网络间传输大量数据,因此序列化的效率至关重要。 - 文本格式 (如 JSON): - 优点: 人类可读,易于调试。 - 缺点: 体积庞大,解析速度慢,对于性能敏感的MPC通信是巨大的瓶颈。 - 二进制格式 (如 Protobuf, Bincode): - 优点: 极其紧凑,解析速度飞快,是高性能场景的首选。 - 缺点: 人类不可读。 在Rust生态中,`serde` 框架配合 `bincode` 库是实现高效二进制序列化的黄金搭档。 ] #slide(title: "序列化代码示例 (Rust)")[ 通过 `serde`,我们只需在结构体上添加一个派生宏,就能轻松实现序列化和反序列化。 ```rust use serde::{Serialize, Deserialize}; // 1. 使用serde的宏来自动实现序列化/反序列化 #[derive(Serialize, Deserialize, PartialEq, Debug)] struct MyData { id: u32, payload: String, } fn main() { let original = MyData { id: 101, payload: "hello".to_string(), }; // 2. 使用bincode将结构体序列化为字节 let serialized_bytes: Vec = bincode::serialize(&original).unwrap(); println!("Serialized: {:?}", &serialized_bytes); // 3. 从字节反序列化回结构体 let deserialized: MyData = bincode::deserialize(&serialized_bytes).unwrap(); println!("Deserialized: {:?}", &deserialized); assert_eq!(original, deserialized); } ``` ] #slide(title: "流式压缩")[ 在序列化之后,我们可以通过一些压缩算法来降低传输的数据量,从而减少网络传输时间和带宽消耗。流式压缩特别适用于MPC中需要传输大量中间计算结果的场景。 优点: - 减少数据量: 直接降低网络传输的数据大小,加快传输速度。 - 降低带宽需求: 对于带宽受限的环境尤其重要。 - 实时性: 流式压缩/解压缩可以在数据传输的同时进行,不会引入额外的显著延迟。 常用算法: - Zlib/Deflate: 广泛使用的通用压缩算法,兼顾压缩比和速度。 - Snappy: Google开发,以极快的压缩和解压缩速度著称,但压缩比略低于Zlib,适用于对速度要求更高的场景。 - LZ4: 另一种非常快速的无损压缩算法,解压缩速度尤其快。 在选择压缩算法时,需要根据具体的MPC协议和网络环境,权衡压缩比、压缩/解压缩速度以及CPU开销。 ] #slide(title: "gRPC协议")[ gRPC 是由 Google 开发的一个现代、高性能的RPC(远程过程调用)框架,它能很好地解决我们之前提到的一些通信瓶颈。 - 基于 HTTP/2: - gRPC 使用 HTTP/2 作为其传输协议,天然支持多路复用。这意味着可以在单个TCP连接上同时处理多个请求和响应,彻底解决了队头阻塞问题,也实现了长连接复用。 - 高效的 Protobuf: - 默认使用 Protocol Buffers (Protobuf) 进行序列化。相比于 JSON,Protobuf 是二进制格式,体积更小、解析更快。 - 支持流式通信: - 除了常规的“请求-响应”模式,gRPC 还原生支持客户端流、服务端流和双向流。这对于需要连续交换大量数据的MPC场景非常有用。 ] #slide(title: "QUIC协议")[ QUIC 是一个更前沿的传输层协议,它被用作 HTTP/3 的基础。它的设计目标是彻底解决TCP的固有顽疾。 - 构建于 UDP 之上: - QUIC 抛弃了 TCP,选择在更底层的 UDP 上重新实现了可靠传输、拥塞控制等功能。 - 解决了真正的队头阻塞: - HTTP/2 在单个TCP连接上多路复用,但如果一个TCP数据包丢失,整个连接上的所有流都必须等待它重传。这是传输层的队头阻塞。 - QUIC 将“流”作为一等公民。每个流的数据包被独立处理,一个流的丢包不会阻塞其他流。 ] #slide(title: "QUIC协议")[ - 更快的连接建立: - QUIC 将传输层的握手(类似TCP三次握手)和加密握手(TLS)合并了。对于已有连接,它甚至可以实现 0-RTT(零往返时间)的连接恢复,速度极快。 ] #slide(title: "协议对比")[ 为了更好地理解不同通信协议的优劣,我们来对比一下它们在MPC场景下的表现。 #block[ #set text(size: 16pt) #table( columns: (auto, auto, auto, auto), align: (center, left, left, left), // Header [特性], [传统TCP/HTTP/1.1], [gRPC (HTTP/2 over TCP)], [QUIC (HTTP/3 over UDP)], // Rows [传输层], [TCP], [TCP], [UDP], [队头阻塞], [应用层和传输层], [传输层 (TCP HOLB)], [无 (流独立)], [连接建立], [慢 (TCP 3次握手 + TLS)], [慢 (TCP 3次握手 + TLS)], [快 (0-RTT/1-RTT)], [多路复用], [无], [有 (应用层)], [有 (传输层)], [序列化], [通常文本 (如JSON)], [Protobuf (二进制)], [协议无关 (二进制)], [性能], [较低], [中高], [高], [适用场景], [简单请求/响应], [微服务/RPC], [实时通信/高并发] ) ] ] #slide(title: "异步TCP方案")[ 使用异步的发送可以让我们无须等待I/O。也就是说,我们在调用`send()`/`recv()`的时候,线程不会一直阻塞,而是会将CPU时间片交给其他任务。 异步编程有多种模型,最主流的是 `async/await` 方案。 ] #slide(title: "async/await 方案的优劣")[ 优点: - 代码直观: 代码的线性逻辑使其看起来像同步代码,非常易于读写和维护。 - 资源占用低: 基于无栈协程,单个任务内存开销极小,可以轻松创建海量并发,且上下文切换在用户态完成。 - 统一的错误处理: 可以使用语言内建的错误处理机制(如Rust的 `?`),避免了回调地狱中的错误处理难题。 缺点: - 心智负担: 需要理解 `Future`、执行器等概念,并手动处理CPU密集型任务(放入线程池)。 - 函数染色: `async` 关键字具有传染性,`async` 函数不能被同步代码直接调用,反之亦然,割裂了生态。 ] #slide(title: "Rust中的异步实现对比")[ 在Rust生态中,同时存在Stackful和Stackless两种异步实现,代表了不同的设计哲学。 Stackful (`may` 框架): - `may` 提供了类似Go Goroutine的体验,每个协程拥有自己的栈。 - 开发者可以像写普通同步代码一样进行编程,网络IO等操作会被框架自动调度,对用户透明。 - 同样是“无色”函数,不强制区分同步和异步函数。 Stackless (`tokio` 框架): - 这是Rust官方和社区的主流方案,基于 `async/await` 语法。 - 编译器将异步代码转化为状态机,内存占用极低。 - 必须遵循 `async/await` 的语法规则,存在“有色”函数问题,但换来了更高的性能和更精细的控制。 ] #title-slide[ 设计异步化的通信流程 ] #slide()[ 我们目前学习的方案大部分是同步的协议,即协议的数据几乎要依赖于上一步的状态或结果。 Damgård @Dam2009 提出一个全异步的协议设计概念,即协议的正确性不依赖与消息是否在规定时间内到达。 同时这篇文章提出#link("https://github.com/mgeisler/viff")[VIFF]框架(现已archive,且继任为MP-SPDZ) ] #slide()[ 近年来,还提出了一些新的方案。如hbMPC@hbMPC ,还有Dumbo-MPC@Dumbo-MPC。 ] #title-slide()[ 总结 ] #slide()[ 综合前面讨论的通信瓶颈、异步编程思想以及各种优化协议,我们可以勾勒出MPC中异步化通信流程的设计思路: - 核心思想:非阻塞与并发 - 充分利用异步编程模型,确保通信操作不会阻塞主计算线程,从而提高CPU利用率。 - 通信调度:DAG驱动 - 将通信任务抽象为有向无环图(DAG),通过拓扑排序实现通信的批处理和并发执行,最大限度地减少等待时间。 - 协议选择:高效可靠 - 优先选用基于HTTP/2 (如gRPC) 或 QUIC (如HTTP/3) 的协议,利用其多路复用、快速握手和高效序列化等特性。 - 连接管理:长连接与复用 - 维护连接池,实现长连接的复用,避免频繁建立和关闭连接的开销。 ] // Bibliography #let bib = bibliography("bibliography.bib") #bibliography-slide(bib)