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

资料参考

相关推荐
sheji34162 小时前
【开题答辩全过程】以 基于SpringBoot的疗养院管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
短剑重铸之日3 小时前
《设计模式》第六篇:装饰器模式
java·后端·设计模式·装饰器模式
码界奇点4 小时前
基于Flask与OpenSSL的自签证书管理系统设计与实现
后端·python·flask·毕业设计·飞书·源代码管理
代码匠心5 小时前
从零开始学Flink:状态管理与容错机制
java·大数据·后端·flink·大数据处理
分享牛5 小时前
LangChain4j从入门到精通-11-结构化输出
后端·python·flask
知识即是力量ol6 小时前
在客户端直接上传文件到OSS
java·后端·客户端·阿里云oss·客户端直传
闻哥6 小时前
深入理解 Spring @Conditional 注解:原理与实战
java·jvm·后端·python·spring
qq_256247057 小时前
Google 账号防封全攻略:从避坑、保号到申诉解封
后端
MX_93598 小时前
使用Spring的BeanFactoryPostProcessor扩展点完成自定义注解扫描
java·后端·spring
弹简特8 小时前
【JavaEE05-后端部分】使用idea社区版从零开始创建第一个 SpringBoot 程序
java·spring boot·后端