动手写RPC框架 - GeeRPC第四天 超时处理(timeout)
源代码/数据集已上传到 Github - 7days-golang
本文是7天用Go从零实现RPC框架GeeRPC的第四篇。
- 增加连接超时的处理机制
- 增加服务端处理超时的处理机制,代码约 100 行
为什么需要超时处理机制
超时处理是 RPC 框架一个比较基本的能力,如果缺少超时处理机制,无论是服务端还是客户端都容易因为网络或其他错误导致挂死,资源耗尽,这些问题的出现大大地降低了服务的可用性。因此,我们需要在 RPC 框架中加入超时处理的能力。
纵观整个远程调用的过程,需要客户端处理超时的地方有:
- 与服务端建立连接,导致的超时
- 发送请求到服务端,写报文导致的超时
- 等待服务端处理时,等待处理导致的超时(比如服务端已挂死,迟迟不响应)
- 从服务端接收响应时,读报文导致的超时
需要服务端处理超时的地方有:
- 读取客户端请求报文时,读报文导致的超时
- 发送响应报文时,写报文导致的超时
- 调用映射服务的方法时,处理报文导致的超时
GeeRPC 在 3 个地方添加了超时处理机制。分别是:
1)客户端创建连接时
2)客户端 Client.Call()
整个过程导致的超时(包含发送报文,等待处理,接收报文所有阶段)
3)服务端处理报文,即 Server.handleRequest
超时。
创建连接超时
为了实现上的简单,将超时设定放在了 Option 中。ConnectTimeout 默认值为 10s,HandleTimeout 默认值为 0,即不设限。
1 | type Option struct { |
客户端连接超时,只需要为 Dial 添加一层超时处理的外壳即可。
1 | type clientResult struct { |
在这里实现了一个超时处理的外壳 dialTimeout
,这个壳将 NewClient 作为入参,在 2 个地方添加了超时处理的机制。
- 将
net.Dial
替换为net.DialTimeout
,如果连接创建超时,将返回错误。
2)使用子协程执行 NewClient,执行完成后则通过信道 ch 发送结果,如果time.After()
信道先接收到消息,则说明 NewClient 执行超时,返回错误。
Client.Call 超时
Client.Call
的超时处理机制,使用 context 包实现,控制权交给用户,控制更为灵活。
1 | // Call invokes the named function, waits for it to complete, |
用户可以使用 context.WithTimeout
创建具备超时检测能力的 context 对象来控制。例如:
1 | ctx, _ := context.WithTimeout(context.Background(), time.Second) |
服务端处理超时
这一部分的实现与客户端很接近,使用 time.After()
结合 select+chan
完成。
1 | func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) { |
这里需要确保 sendResponse
仅调用一次,因此将整个过程拆分为 called
和 sent
两个阶段,在这段代码中只会发生如下两种情况:
- called 信道接收到消息,代表处理没有超时,继续执行 sendResponse。
time.After()
先于 called 接收到消息,说明处理已经超时,called 和 sent 都将被阻塞。在case <-time.After(timeout)
处调用sendResponse
。
测试用例
第一个测试用例,用于测试连接超时。NewClient 函数耗时 2s,ConnectionTimeout 分别设置为 1s 和 0 两种场景。
1 | func TestClient_dialTimeout(t *testing.T) { |
第二个测试用例,用于测试处理超时。Bar.Timeout
耗时 2s,场景一:客户端设置超时时间为 1s,服务端无限制;场景二,服务端设置超时时间为1s,客户端无限制。
1 | type Bar int |
附 推荐阅读
last updated at 2023-11-15