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

资料参考

相关推荐
Rust研习社23 分钟前
使用 Tonic 构建高性能异步 gRPC 服务
开发语言·网络·后端·http·rust
DevilSeagull1 小时前
Rust 方法语法:从定义到实践
开发语言·后端·rust
每天进步一点_JL1 小时前
Java 线程池深度解析:从零开始理解并发编程的核心工具
后端
每天进步一点_JL2 小时前
Spring 到底在做什么?从零开始理解 Java 企业开发的核心框架
后端·spring
每天进步一点_JL2 小时前
Spring 【多实现切换 & 事务代理机制】深度解析
后端
彩票管理中心秘书长2 小时前
MySQL 数据库高级与网络管理操作命令大全
后端
Gopher_HBo2 小时前
CompletableFuture函数原理
后端
香山上的麻雀10082 小时前
由 Rust 开发的能大幅降低LLM token消耗的高性能 CLI 代理工具 rtk
开发语言·后端·rust
神奇小汤圆2 小时前
Java vs Go:哈希冲突解决之道,为什么一个用红黑树,一个用桶?
后端
神奇小汤圆2 小时前
得物二面:Redis 中某个 Key 访问量特别大怎么办?我:Redis 能顶得住... 生瓜蛋子 生瓜蛋子
后端