CS110L-Lab/proj-2/balancebeam/implement_notes.md
2023-03-10 23:51:03 +08:00

10 KiB
Raw Blame History

Project-2 Balancebeam 实现记录

综述

这东西本质上是一个类似于反向代理之类的东西,

每个 Milestone 的说明

Milestone 0

依然是传统的看代码环节。

首先是 main.rs 里面的东西。

  1. CmdOption 用的是 clap 这个 crate用非常奇葩的 macro 语法来定义命令行参数并解析。
  2. ProxyState 是执行线程的共享参数,不知道有啥特别的意义,可能是为后面的东西预留的
  3. TCPListener 是标准库提供的东西,提供了一个简单的 TCP server。它通过 incoming 迭代器来返回构造的 TCP stream 抽象,这种字节流抽象最终会被分发给执行线程。
  4. 执行线程:
    1. 首先它随机选择一个上游服务器并打开与该服务器的连接。如果服务器无了返回502。
    2. 然后循环从客户端读取请求,客户端可以在一个连接上发送任何数量的请求,而 balancebeam 会将每个请求转发到上游服务器。如果客户端发送了一个错误的请求,会有一些错误处理。
    3. 一旦请求被读取,balancebeam 就会添加一个 X-Forwarded-For HTTP以便上游服务器能够知道客户端的IP地址。这对理解并不特别重要它只是一个事实上的标准头。
    4. 接下来,balancebeam 将请求转发给上游服务器。如果在传输请求时有错误,它就会通知客户端。
    5. 接下来,balancebeam 试图从上游服务器读取响应。如果响应被破坏或在接收时有其他问题,它将通知客户端。
    6. 最后,balancebeam 将响应转发给客户。

然后是封装库 request.rs response.rs,就像 project 1 里面的 gimili wrapper 一样,又臭又长。分别用来基于 TCP 字节流来处理 HTTP 请求和响应。分别提供了 read_from_streamwrite_to_stream 这两个接口供 main 处理两个 TCP 链接( Client HTTP req -> Proxy HTTP req -> Server, Server HTTP resp -> Proxy HTTP resp -> Client。具体的里面的接口我也没细看等到 Milestone 2 需要修改的时候再说吧。

Milestone 1

讲义上没有提出明确的实现目标,简单的把代码改成多线程而已。主要就是把 stateArc 给共享掉(反正是不可变借用,十分的安全)。

ThreadPoolstd::thread 没啥太大区别,都封装的很好了,直接看一下 doc.rs 里面的例子就行了。

Milestone 2

又是一个奇怪的 task point要求把所有的 IO 换成异步的。但是依然没有任何的难度,根据 IDE 提示加 asyncawait 就可以了。

Milestone 3

实现故障转移failover如果一个 upstream 挂了,将连接选一个别的(一开始有一堆可用的 upstream同时还要标记这个挂掉的 upstream 防止后面的连接继续选择它;如果所有的 upstream 都挂了,这时候才返回错误。实现方法是修改 main.rs::connect_to_upstream,因为这个过程仅在建立连接的时候执行。

实现了一个最简单的,在 ProxyState 里面加了一个 Arc<Mutex<Vec<usize>>>,用来存放可用的 upstream 的 index。
选择的时候就在这个加了锁的 Vec 里面选,然后用这个里面的 index 来取对应的 upstream 的地址。如果一开始就发现这个可用列表为空,那么直接返回错误;否则尝试连接。如果连接失败,需要将这个 index 从可用列表里面删除。唯一需要注意的就是,可以在 TcpStream::connect 之前释放一次锁,然后需要修改可用列表的时候再加锁,因为 connect 比较耗时。

感觉 RwLock 和这个东西差不多,但是可能在某些情况下的并发性能会更好一些。至于它说的用 channel感觉挺麻烦的所以没写。

Milestone 4

实现周期性的主动连接测试active health check就是我们的代理程序隔一段时间向每个 upstream 的某一个特定地址(参数里面的 active_health_check_path)发一个 HTTP 请求,如果返回 200 就说明没问题;如果之前有问题现在没问题,就可以继续用;出现问题则需要标记为下线。

由于上个 Milestone 里面用到的极简数据结构,导致实现起来有点生草。这里我是用 tokio::spawn 来生成了一个 task根据文档的说明这是一个 green thread鬼知道是什么东西。然后一个大循环先 sleep 一波(不能放到后面,因为他的测试比较智障,放到后面会出错);遍历所有的 upstream不管有没有被 disable建立 tcpstream 构造空请求然后接受响应最后判断状态码是不是200。以上任何一步出错都必须从可用列表中删除注意有可能已经删除了如果是 200则需要将它重新加入到可用列表中如果之前被删除了的话

一个坑:不要使用前面实现的 connect_to_upstream,因为它无法连接到已经被移出可用列表的 upstream不如说它根本就不让自己选 upstream但是我们这里需要测试每个 upstream。

Milestone 5

实现请求速率限制。针对每个 IP 限制一段时间内的请求数量,如果次数过多就返回 HTTP 429。提到了三个算法leaky bucket、fixed window 和 sliding window。leaky bucket 不太适合这里的要求,就不管了。

这里实现了 fixed window 和 sliding window。这两个玩意基本上一样fixed window 简单一点,单纯记录每个 time unit 里面请求的次数,达到上限就丢掉,开始下一个 unit 的时候就重置计数器。这就导致了一个问题,如果请求集中在计数器重置的时间点附近,就会导致短时间内的请求速率翻倍,实际上超过了限制。 sliding window 在此基础上考虑了上一个 unit按照一定比例将上一次 unit 的请求计数折算到当前的请求计数中,从而避免了上述的问题。一种简单的实现方式就是,记录当前 unit 的计数和上个 unit 的计数,考虑当前时刻向前推一个 unit 的窗口,用窗口和上个 unit 重叠的比例进行折算。写成算式就是 $\frac{UNIT - (time.now - cur_unit_start)}{UNIT} \times prev_cnt + cur_ cnt$。

具体实现上,用一个 HashMap<String, RateRecord> 来维护 IP 地址到请求次数的信息。因为一个 IP 地址会发起多个 connection所以得把数据放到全局的 ProxyState 里面再用 Arc<Mutex<>> 裹起来。封装了 RateRecord 类型来存放前面提到的这些数据,然后写了几个简单的小函数来让 handler 代码更加简洁一些。这些都不是什么难绷的事情,最难绷的是 HashMap 居然不支持直接修改内容,下面细说遇到的问题。

遇到的问题

  1. HashMap 不能直接修改 value 的内容,说是要实现 IndexMut 这个 trait但是 HashMap 没实现,所以不能像实现了这个 trait 的 Vec 类型那样直接 map[key] = newvalue,而是要用非常别扭的 (*map.get_mut(&key).unwrap()).field = new_val_for_field 来写。
  2. 对于 primitive 类型之间的显式类型转换好像可以直接用 as xx,这个东西好像是编译器实现的,因此既不需要标准库也不需要什么其他的转换方法。不过剩下的转换就要通过 FromInto 这些 trait 来整了。
  3. 最后就是,直接运行 cargo test 的时候有可能会在 assert_eq!(total_request_count, rate_limit_threshold); 这个断言上报错,不过单独测这一个点的时候从来不会错。根据打印信息判断,这大概是由于前面实现的 health check 的线程发送的检查请求也被计算在累计请求数中,所以导致这个东西爆炸。至于为啥会出现这个事情,可能是随机数(或者是 cpu 性能问题)导致测试时间过长,然后 health check 的线程开始执行了。

附加任务?

实验设计者提供了不少的可选项,看上去都挺棒的,但是我懒得自己写测试了,所以就不做了捏,啦啦啦。

完结感言

至此, CS110L 的5个 Assignment 和2个 Project 就全部做完了因为刚开学不太适应直接给感冒了、外加要上无聊的课说的就是你们智障思政课进度有点慢不过姑且还是在第一周内写完了前前后后大概花了一个多星期吧CS144 那么多我才写了6天不过那个属于是 full-time 了),基本上 rust 这些东西算是有点入门的感觉了(之前好几次尝试通过看 rust book 入门都失败了)。

这些编程作业虽然量不是很多(总计 1k lines 不到?),不过基本上覆盖了 rust 编程的大部分常见主题。写这种 project 代码的好处是,相比于看 rust-lang 那些书,调试运行更加形象,而且需要大量重复那些最常见、最基本的概念和模式,比较让人能够记得住(大雾)。

之所以想起来学 rust起源于寒假的时候做的操作系统试点班那一坨蕴藏这各种各样内存和并发问题的代码真的是把我恶心到了为啥 boilerplate 能这么垃圾啊。rust 通过强制的编译检查,能够避免大部分这种内容,同时提供比 C 语言更高级的抽象,使得处理一些问题的时候不用手写了。但是 rust 的坏处在于,它的机制比较独特,有很多 C 语言里面的 trick 和直观的写法不能用,对于底层代码的映射也不够直观。对于我这种习惯于自动把 C 语言翻译为机器模型的人来说,实际上 rust 的抽象层是增加了心智负担,因为我没法直接通过代码看到汇编了(这就是 C 语言的好处,足够接近机器模型)。现在的感觉这东西对标的其实是 Modern C++ 来着(没错,就是那个 auto script写完 CS144 的 modern C++ 再来看,这什么 concept、optinal、move 之类的东西,其实和 rust 也差不多来着。如果 C++ 严格 RAII 的话,这 rust 也就比 C++ 多个强制生命周期和所有权检查的样子。

虽然做完了rust 还有很多深入的内容没有去了解,这就以后慢慢看吧,如果用的多的话。

就酱,完结撒花。