记一次技术讨论:尝试异步处理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 到底能不能被异步处理

资料参考

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码2 天前
别再前后端各写一套表单校验了
java·后端
大勇前进2 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu2 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile2 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白802 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端
二月龙2 天前
PHP 接口返回统一响应封装,让前后端对接更省心
后端
盖伦发发2 天前
软件工程SOLID 五大设计原则
后端·软件工程