服务端超时控制

服务端的资源是有限的, 处理已经超时的请求是没任何意义的. 超时控制是保障服务稳定的一道重要防线, 本质是快速失败节省资源.
Golang⌗
go 语言超时控制一般使用 context 来传递, context.WithTimeout 用来创建一个有超时控制的 context.
- context.Done() 返回一个 channel, timeout 或者 cancel 时会收到信号
- context.Err() 在 timeout 或者 cancel 时返回非 nil 值
http server⌗
http 标准库中有一个中间件 TimeoutHandler, 实现也是非常简单:
我们在业务 handler 中使用 io 操作时注意使用 request.Context(), 有利于回收资源.
grpc⌗
grpc server 没有像 http.TimeoutHandler 那样直接支持超时处理, 但是会自动将 client request context 中的 timeout 通过 header 传递给 server 端, server 端则会将这个超时时间设置为当前请求处理器的超时时间.
internal/transport/handler_server.go#L79
internal/transport/http2_client.go#L512
但是也可以实现一个 timeout interceptor, 例如 go-zero 框架:
也是一个很经典的 go 语言超时处理.
NodeJS⌗
node js 中 abort controller 还没完全稳定和广泛使用, 所以 timeout 传递没有太大的意义(资源没办法回收), client 端把握好超时时间一般来说不会有太大问题.
假如 abort controller 普及了, 就可以像 go 语言那样, 为每个请求创建一个 request.abortController (或者使用 async_hooks 隐式共享) 在后续 handler 中的 io 操作中共享, 这样超时控制器可以作为一个中间件, 在超时响应错误的同时调用 abortController.abort() 回收资源.
服务间传递⌗
可以写一个请求级别的 TimeoutManager 管理该请求的超时状态.
单服务内部使用 async_hooks 共享一个请求级别的 TimeoutManager 用来判断任意时刻是否还有剩余时间做操作, 服务设置一个保底的全局 timeout.
跨服务请求时, 可以使用 header 传递超时时间:
- client 端, 发送 http 请求时使用
timeoutManager.getTimeout()获取当前剩余超时时间, 放入 header 中传递给 server - server 端在全局超时中间件中, 拿到 request header 中的 timeout, 使用
timeoutManager.shrinkDeadline(timeout)将当前请求的超时时间设置为 client 超时和全局超时中较小值
参考资料⌗
