正文
(图片来自 Explained: How does async work in Rust? [3])
如果你做过多核 CPU 下的(非 ASIC)网络设备相关的开发,会发现这个过程似曾相识。我觉得未来 Rust 会在高性能网络设备领域占据一席之地,这得益于其高效强大的易步处理库。
Rust 下主流的异步库有 Tokio 和 async-std。下图做了不错的总结,大家可以就着原文的讨论一起看:
(图片来自 reddit 讨论:Diagram of Async Architectures [4],有些旧,async-std 现在已经基于 smol 了,但整个讨论值得一读)
异步开发的好处是:尽管底层的处理相当复杂,各种实现但对开发者来说,接口非常简单,只需要很少的代价就可以把一个同步的处理变成异步的处理。
但是,我们要小心其中的一些陷阱:
-
锁的使用。如果对 I/O 相关的数据结构加锁,需要使用支持异步的锁。比如你要对 TCP stream 的 writer 加锁(不建议这么做,下文会讲),那么要用异步锁。异步锁和同步锁的区别是,异步锁只是让异步任务状态变为
Poll::Pending
,不会锁线程,等锁 ready 后异步任务重新被唤醒;而同步锁会锁线程,导致性能问题甚至死锁。
-
异步代码和高延时同步代码的混用。如果在两个异步操作之间有高延时同步代码,要么把同步代码放在前面或者后面执行,要么把同步代码异步化(分阶段 poll)。
对于后者,我这两天在做一个类似 Phoenix Channel[5] 的 Websocket 应用(以下简称 WS channel)时遇到了这个问题:我的 WebSocket 在 wss 连接时,每个连接要花大概 300-400ms,很奇怪。后来我用 jaeger 追踪了一下 tracing,发现客户端代码在连接时时间都耗在了
new_with_rustls_cert
上,真正花在 TLS handshake 上的时间只有 1.5ms:
由于我客户端做的操作是:
-
TCP connect(异步)
-
准备 TLS connector(这里会做
new_with_rustls_cert
),这是同步操作,耗时 ~300ms
-
TLS connect(异步)
这直接导致服务端
tls_accept
每个新建连接的延迟奇高(因为客户端 TCP 连上了迟迟不做 handshake):
解决办法:客户端准备 TLS connector 的步骤提前到第一步。之后,服务器的延迟正常了(~1ms):
这是个有趣的 bug。
new_with_rustls_cert
看上去是个人畜无害的纯内存操作,但因为里面有读取操作系统的受信证书的操作,所以延时高一些。其实它应该做成异步操作。
队列
在网络开发中,最快能提升性能的工具就是队列。虽然操作系统层面,已经使用了发送队列和接收队列来提升性能,在应用层面,我们最好也构建相应的队列,来让整个服务的处理变得更顺滑,更健壮,更高效。除此之外,队列还能帮助我们解耦,让应用本身的逻辑和 I/O 分离。