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

资料参考

相关推荐
2401_882727573 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
追逐时光者4 小时前
.NET 在 Visual Studio 中的高效编程技巧集
后端·.net·visual studio
大梦百万秋4 小时前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
斌斌_____5 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@5 小时前
Spring如何处理循环依赖
java·后端·spring
海绵波波1076 小时前
flask后端开发(1):第一个Flask项目
后端·python·flask
小奏技术7 小时前
RocketMQ结合源码告诉你消息量大为啥不需要手动压缩消息
后端·消息队列
AI人H哥会Java9 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱9 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-9 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu