记一次技术讨论:尝试异步处理HTTP请求提高性能

事情起因

同事在 review 项目代码时,发现支付代码中的 http 接口有类似以下逻辑

go 复制代码
func xxxHandler(args ... interface) {
    // ...
    
    ch := make(chan int, 1)
    defer close(ch)
    
    process(ch, arg1, arg2, ...)
    
   select {
   case code, ok := <- ch:
        switch code {
        case 1:
        case 2:
        ...
        }
   case <-time.After(time.Second * 3):
       ...
   }
}

由于我们是游戏研发,process() 需要经过游戏内的业务逻辑处理并返回对应的错误码

这个接口是对外暴露的,通过 nginx 做负载均衡,这样 nginx 相当于与我们的 web 服务器 建立了长连接

同事提出相关疑问: 如果在 process() 调用过程中出现阻塞或者别的情况导致 select 只能等待 3 s 定时器结束

又因为在 go 中是 one goroutine per connection 也就是针对每一个链接都会开启一个 goroutine 去处理

这样在一个链接中,只要前一个请求没有处理完成或者被阻塞住,后面所有的请求都会被阻塞卡住,导致这个接口服务宕掉

同事提出解决方案:

在接收到 HTTP 请求 后,开启一个 goroutine 去处理该请求,并尝试通过该 HTTP 请求唯一标识 返回响应

我提出反驳:

由于 HTTP请求-响应模型 因此在处理下一个请求前,必须返回当前请求的响应

如果需要他所描述的异步处理,则应该使用 WebSocket,同时 API 的调用方也要做对应代码的变更

在此终于认识到了,游戏开发和 web 开发程序员在某些技术的认知差异

one goroutine per connection

每个链接都是一个 goroutine

go 实现的 HTTP 服务器源码见 src\net\http\server.go

go 复制代码
func (srv *Server) Serve(l net.Listener) error {
    // ...

    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
       rw, err := l.Accept()
       if err != nil {
          // ...
          return err
       }
       connCtx := ctx
       if cc := srv.ConnContext; cc != nil {
          connCtx = cc(connCtx, rw)
          if connCtx == nil {
             panic("ConnContext returned nil")
          }
       }
       tempDelay = 0
       c := srv.newConn(rw)
       c.setState(c.rwc, StateNew, runHooks) // before Serve can return
       go c.serve(connCtx)
    }
}

从源码可以看出,原生 HTTP 也是使用监听器 Listener 监听网络连接,每当有新链接 conn 接入时就创建一个 goroutine 处理这个链接发送过来的请求 go c.serve(connCtx)

这种从链接读取请求数据并创建 goroutine 是游戏开发中常用的方式,因为 HTTP 的底层实际就是 TCP 只是根据建立在其上的协议不同,对应字节流数据的编解码方式不同

请求-响应 ( request- response ) 模型

由于一般的 Web API 使用的 HTTP 版本 都是 HTTP/1.1 所以对请求的处理都是串行的

至于为什么都是串行,这种原则上的定义仅凭个人理解相互争论没有意义,所以直接去看 RFC 文档

RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1 (rfc-editor.org)

8.1.2.2

A client that supports persistent connections MAY "pipeline" its requests (i.e., send multiple requests without waiting for each response). A server MUST send its responses to those requests in the same order that the requests were received.

在一个链接中,客户端可以通过这个链接发送多个请求,且无需等待响应,但服务端 必须 MUST 按照这些请求发送的顺序依次返回响应

这里规定了服务端的响应必须是有序的,所以服务端只能按照同步逻辑实现对请求的响应

HTTP 到底能不能被异步处理

资料参考

相关推荐
Lee川4 小时前
从零解剖一个 AI Agent Tool是如何实现的
前端·人工智能·后端
金銀銅鐵6 小时前
[Java] 如何将 Lambda 表达式对应的类保存到 class 文件中?
java·后端
五月君_6 小时前
Bun v1.3.14 发布,Rust 版即将进 Claude Code 内测,下一版可能就告别 Zig
开发语言·后端·rust
明月_清风6 小时前
🍃 MongoDB 从入门到上手:一篇写给新手的科普指南
后端·mongodb
程序员cxuan8 小时前
当 00 后开始用 token 给学校送礼
人工智能·后端·程序员
夕颜1119 小时前
opencli 使用总结
后端
青云计划9 小时前
Feed流
java·后端·spring
☞遠航☜9 小时前
搭建基础的springcloud alibaba项目练习
后端·spring·spring cloud
IT_陈寒9 小时前
React性能优化踩的坑,这个错你可能也会犯
前端·人工智能·后端
zhangxingchao10 小时前
AI应用开发三:RAG技术与应用
前端·人工智能·后端