5.0 KiB
Project-2 Balancebeam 实现记录
综述
这东西本质上是一个类似于反向代理之类的东西,
每个 Milestone 的说明
Milestone 0
依然是传统的看代码环节。
首先是 main.rs 里面的东西。
CmdOption用的是clap这个 crate,用非常奇葩的 macro 语法来定义命令行参数并解析。ProxyState是执行线程的共享参数,不知道有啥特别的意义,可能是为后面的东西预留的TCPListener是标准库提供的东西,提供了一个简单的 TCP server。它通过incoming迭代器来返回构造的 TCP stream 抽象,这种字节流抽象最终会被分发给执行线程。- 执行线程:
- 首先,它随机选择一个上游服务器并打开与该服务器的连接。如果服务器无了,返回502。
- 然后循环从客户端读取请求,客户端可以在一个连接上发送任何数量的请求,而
balancebeam会将每个请求转发到上游服务器。如果客户端发送了一个错误的请求,会有一些错误处理。 - 一旦请求被读取,
balancebeam就会添加一个X-Forwarded-For HTTP头,以便上游服务器能够知道客户端的IP地址。(这对理解并不特别重要;它只是一个事实上的标准头。) - 接下来,
balancebeam将请求转发给上游服务器。如果在传输请求时有错误,它就会通知客户端。 - 接下来,
balancebeam试图从上游服务器读取响应。如果响应被破坏或在接收时有其他问题,它将通知客户端。 - 最后,
balancebeam将响应转发给客户。
然后是封装库 request.rs response.rs,就像 project 1 里面的 gimili wrapper 一样,又臭又长。分别用来基于 TCP 字节流来处理 HTTP 请求和响应。分别提供了 read_from_stream 和 write_to_stream 这两个接口供 main 处理两个 TCP 链接( Client HTTP req -> Proxy HTTP req -> Server, Server HTTP resp -> Proxy HTTP resp -> Client)。具体的里面的接口我也没细看,等到 Milestone 2 需要修改的时候再说吧。
Milestone 1
讲义上没有提出明确的实现目标,简单的把代码改成多线程而已。主要就是把 state 用 Arc 给共享掉(反正是不可变借用,十分的安全)。
ThreadPool 和 std::thread 没啥太大区别,都封装的很好了,直接看一下 doc.rs 里面的例子就行了。
Milestone 2
又是一个奇怪的 task point,要求把所有的 IO 换成异步的。但是依然没有任何的难度,根据 IDE 提示加 async 和 await 就可以了。
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。