EAimTY 的博客
肥宅
首页
关于
主题

论文原文:Tolerating Slowdowns in Replicated State Machines using Copilots
以下内容是对这篇论文的阅读总结,以及部分重要章节(§3 Design、§5 Optimizations)的翻译。


QUIC 协议汲取了大量人们给 TCP 糊墙的经验教训,把连接结构优化到(目前来看)极致。但是现在市面上的代理工具还没有能完全利用 QUIC 特性的存在,所以我自己动手写了一个基于 QUIC 协议的新代理工具:TUIC

https://github.com/EAimTY/tuic

  • 1-RTT TCP 中继
  • 0-RTT UDP 中继,且 NAT 类型为 FullCone
  • 在用户空间的拥塞控制,也就是说可以在任何系统平台实现双向的 BBR
  • 两种 UDP 中继模式: native (原生 UDP 特性,数据仍被 TLS 加密)和 quic (100% 送达率,每个包单独单独作为一个 QUIC “流”,一个包的确认重传不会阻塞其它包)
  • 完全多路复用,服务器和客户端之间始终只需要一条 QUIC 连接,所有任务作为这个连接中的 “流” 进行传输(一个流的暂时阻塞不会影响其它流),所以除连接第一个中继任务外的其它任务都不需要经过 QUIC 握手和 TUIC 的鉴权
  • 网络切换时的会话平滑转移,例如在从 Wi-Fi 切换到移动数据时连接不会像 TCP 一样直接断开
  • 0-RTT 、与中继任务并行的鉴权
  • 支持 QUIC 的 0-RTT 握手(开启之后能达到 真・ 1 -RTT TCP 和 0-RTT UDP ,但是就算不开启,多路复用的特性也能保证在绝大多数情况下 1-RTT 和 0-RTT )

TUIC 的设计介绍在仓库中 Design 一节有说明。TUIC 协议的详细内容在 这里 。简单来说,TUIC 的设计核心就是减少握手造成的网络往返时延( rtt ),毕竟对于网络程序这是最大的瓶颈。

对比其它使用 TCP 的代理工具( ss 、v2ray 、trojan ),TCP 握手慢,且不支持自定义拥塞控制,各工具对 UDP 的支持也各有问题。对比 Hysteria ,Hysteria 的 UDP 中继需要 1 rtt 的握手,且只支持一种 UDP 模式。

最后说说安全性和协议特征。TUIC 现在基于原生 QUIC ,不支持 obfs ,但 QUIC 连接本身就是 TLS 加密的,每个 QUIC 连接从外面看都是一样的。国内的各大厂也慢慢开始使用 QUIC 了,所以我觉得 QUIC 特征应该不是什么大问题。

我之前对 HashMap 有很多错误的理解,特别是在 hash 函数对性能的影响上。最近我对 HashMap 的了解稍稍深入了一些,所以写了这篇笔记,对之前的错误理解修正总结。

我从今年六月开始学 Rust,到现在差不多有半年了。写 hummingbird 这个项目的想法我在八月就已经有了:写一个用 git 仓库作为数据库的内容管理系统,给 git repo 中 markdown 格式的文章套 HTML 模板,然后 serve。
这样能结合传统 CMS 和 GitHub Pages 的优点——能用对于 CMS 本身只读的 git 仓库保证数据的安全性,也能像传统的 CMS 一样提供动态内容,比如搜索文章内容,甚至支持在一次搜索中使用多个 filter 来缩小范围,还不必像(免费版) GitHub Pages 一样只能建在公开仓库上。GitHub 的仓库文件编辑器还可以直接视为管理后台。
当然这种实现也会导致一些限制,例如不能原生支持评论,能实现的功能比较少,数据库更新不能太频繁,比起 GitHub Pages 需要一台服务器来跑服务端...
hummingbird 比 WordPress 大概会快十倍吧,大概(

总之我觉得这个项目应该有适合的使用场景,所以就开始动手了。经过断断续续 4 个月的开发,终于把雏形写出来了。

写 hummingbird 的过程基本上就是我入门 Rust 的过程。每过一段时间,回头看之前写的代码就会觉得写得稀烂,想重构。这个项目的数据库实现我重写过不下五次,然而现在还是觉得写得很差。这也算是学习的过程吧。

不论从代码上看,还是从软件工程角度看 hummingbird,都有些问题——某些功能实现地很幼稚,抽象也到处漏,但我总算是写完了一个比较复杂的项目,比起之前总是纸上谈兵还是有进步的。

说说大体的实现思路吧:

整个项目主要分成 3 个部分:配置、数据库和服务器 / 路由。

配置部分很简单,就是读一个 TOML 格式的配置文件然后解析数据,没什么好说的。

数据库部分大概又能分 3 个部分吧:内存里的数据库存储部分,集成的 git 客户端,还有模板系统。
存储的实现比较原始,就是在解析过 git 仓库之后把其中的所有内容存进二叉堆排序,然后转成 Vec。另外还有些关于内容作者之类的映射。
git 部分是用 libgit2 实现的,用了 git2-rs 这个 Rust 的 bindings。这里算是写项目前期坑最多的地方,libgit2 暴露的 API 层级比较低,不像平时直接用 git 命令一样方便,而且当时还不太熟悉 Rust,实现反向遍历 commits 拿到文章作者、创建时间和更改时间花了很大力气。另外,git2-rs 的仓库抽象是 !Send!Sync 的,所以刚开始写数据库时我只能做一个 RepoGuard 包住 git 仓库把它留在主线程用 Channel 通信,把剩下的部分 spawn 成 tasks 出去,从 git 仓库取数据的过程又臭又长。后面我发现 libgit2 的文档中只提到不能 parallel 地使用仓库,所以才会 !Send!Sync。hummingbird 只有在被手动触发数据更新的时候才会操作 git 仓库,而且操作会上排他锁,所以直接把仓库标成 Sync 作为数据库成员走状态共享肯定没问题。
模板系统中的 markdown 解析用了 pulldown-cmark,HTML 模板应用是自己手写的,因为 tera 这类的模板实现实在是太重了。说实话我不是很满意现在的实现,有一大堆 clone。之后我想写一个完全 Evaluate-on-Write 的、带 Cacher 的 StringBuilder。

服务器 / 路由部分我也改过很多次。最早是用 axum 写的,但后来发现 axum 的很多功能我完全用不到,比如 middleware 之类的,而且 axum 有大约四百个依赖,太重了。所以我换到了 hyper,不过就要自己解决路由的问题了。开始时我想用一张大的字典当路由表,在更新数据库的时候就把所有所有数据解析好,但是发现存储效率不是很高,改成存过程也比较难实现。最后我的解决方案是用一张字典存文章、页面和的其它的静态路径,其它有参数的路径用字典树匹配和捕获。我写了一个泛型的字典树实现,但是 bench 后发现效率比 matchit 低至少一倍, 我还是太年轻 ,所以就用了 matchit。

hummingbird 有什么适用的具体使用场景吗?我觉得可能用来 serve 长篇的文档、说明比较好(类似于 LLVM IR 这种),用来做一个简单的博客也不错,只是需要外挂评论系统。

最近架了个 Mindustry 游戏服务器和朋友一起玩 PvP(然而没玩几天就弃坑跑去 MC 了 ),感觉不错,只是每次输命令和上传地图的时候都要 ssh/sftp 到服务器上有点不方便,所以就写了个 Telegram 机器人用来输命令和上传地图:

teledustry - Manage your Mindustry server through Telegram bot

我不会写 Java,所以没把这个 bot 写成 mod 的形式,而是直接把游戏服务器进程创建为子进程,然后读写子进程的 stdio。
这个 bot 用起来很简单,只要用

$ teledustry -t API_TOKEN -u YOUR_TELEGRAM_USERNAME SERVER_FILE.jar

就可以启动 Mindustry 服务器和 bot,然后去找 bot 聊天就能执行命令和上传地图了(记得用 /output 命令让 bot 把输出发到当前聊天里)。