青训营:http性能优化之道 | 豆包MarsCode AI刷题

103 阅读6分钟

前置知识

1.NIO和BIO(阻塞式io)

在传统阻塞 IO 模型下(BIO):

  1. 每个连接的 ReadWrite 操作都是阻塞的:

    • 如果对方没有发送数据,Read 操作会卡住,直到数据到来。
    • 如果对方未准备好接收数据,Write 操作也可能被阻塞。
  2. 为了解决这个问题,通常会为每个连接分配一个独立的线程或 Goroutine,但这会带来:

    • 线程/Goroutine 数量膨胀: 当连接数增加时,需要更多的 Goroutine 或线程,导致调度开销增大。
    • 资源浪费: 即使大部分连接处于空闲状态,线程或 Goroutine 依然会占用资源。

而我们的NIO模型:

  1. 非阻塞 socket:

    • 网络连接的 ReadWrite 操作不会阻塞。如果当前没有数据可读或可写,这些操作会立即返回而不会卡住线程。
  2. 事件监听机制:

    • 使用操作系统的事件通知机制(如 epollkqueue),监听文件描述符的状态变化。
    • 只有当文件描述符变得可读(有数据到达)或可写(可以发送数据)时,才会进行真正的 ReadWrite 操作。
  3. 事件循环(Event Loop):

    • 单独的线程监听和分发事件,处理多个连接的状态变化。

我的简单理解就是Nio就是使用监听确认文件可读可写才会进行read和write操作,这样不会在读写的时候阻塞住了。(事件驱动机制)

2.net

net 是Go 标准库中的一个包,主要用于网络编程。它提供了对 TCP、UDP、IP 等网络协议的支持,可以用来创建客户端和服务器应用程序。采用Bio,用户处理buffer。

3.netpoll

netpoll 是 Go 生态中的一个库(非标准库),主要用于 高性能网络 I/O(NIO)灵感来自操作系统的 epollkqueue,主要关注于大规模并发网络连接的场景,特别适合用于实现高性能服务器框架。

  • 注册监听:

    • 将网络连接的文件描述符(socket)注册到 epollkqueue
    • 指定感兴趣的事件(如可读事件、可写事件)。
  • 等待事件:

    • 通过 epoll_wait(Linux)或 kevent(macOS/BSD)等待事件触发。
    • 当某个文件描述符上有数据可读或可写时,操作系统会通知事件循环。
  • 回调处理:

    • 事件循环将触发的事件分发给用户定义的回调函数。
    • 在回调中执行非阻塞的 ReadWrite 操作。

4.SIMD

SIMD(Single Instruction, Multiple Data)是一种并行计算技术,它允许在单条指令下同时处理多个数据元素。SIMD 指令集是现代处理器(如 Intel 和 AMD 处理器)用来加速数据并行处理的关键技术,广泛应用于图形处理、科学计算、视频编解码、人工智能等领域。

SIMD 的基本原理

  • 单指令、多数据:与传统的标量处理不同,SIMD 通过一次执行相同的指令来操作多个数据元素。这些数据通常是数组或向量中的元素,而指令本身只执行一次,而不是对每个元素逐一执行。
  • 并行处理:SIMD 利用处理器的多个运算单元同时处理不同的数据元素,极大地提高了计算效率。

SIMD 的例子

假设有两个向量数据 A = [a1, a2, a3, a4]B = [b1, b2, b3, b4],在传统标量计算中,我们需要分别执行以下操作:

C1 = a1 + b1
C2 = a2 + b2
C3 = a3 + b3
C4 = a4 + b4

而在 SIMD 中,可以通过单条指令同时执行加法操作:

C = A + B
C = [a1 + b1, a2 + b2, a3 + b3, a4 + b4]

这样,计算结果可以在一次指令中并行完成。

针对网络库的优化(上面的net和netpoll)

对net优化需求:

  • 存下http的全部头部:http的长度未知,header有大有小
  • 减少系统调用的次数:内核态和用户台的切换,开销大
  • 能够复用内存
  • 能够多次读,例如header较大没读完需要再次读的情况

方法:

  • 绑定一块缓冲区:go net with bufio
    • 经过调研大部分的包在4k以下
  • 具体实现: image.png
    • Peek(n int) ([]byte, error):
      • 从数据流中预览 n 个字节,但不移动读取位置。
      • 1.通过维护一个缓存区(buffer),减少实际 IO 操作。 2. 若缓存区中数据不足时,从底层读取更多数据填充缓存。
    • Discard(n int) (discarded int, err error)
      • 让指针向前移动
      • 跳过无关数据或读取后释放资源。
      • 修改缓存区的偏移量,不需要实际删除数据,提升性能。
      • 处理 n 超出缓存区大小的情况:直接从流中跳过数据而不缓存。
    • Release() error
      • 释放资源,比如关闭底层连接或回收缓冲区。
      • 希望下一次请求能复用之前的空间

对netpoll优化需求:

  • 存下全部的header,拷贝出完整的body 。如图我们的header和body可能在两个节点上。

image.png

方法:

  • 在底层分配足够大的buffer(节点)

netpoll with nocopy peek:

根据历史最大请求进行分配,把header、body在底层拼好返回给最上层。

image.png

当有小的请求时会浪费一些内存,所以还要限制buffer size

不同网络库优势:

go net :流式友好,包小性能高 netpoll: 中大包性能好,时延低

针对协议的优化

headers解析:

  • 找到header Line 边界: \r\n

找到\n再看前一个是不是\r就行

使用SIMD技术,一次匹配多个。一般我们得一个个匹配,这样的复杂度是O(n),使用SIMD技术更快。go集成了该指令集。

image.png

  • 快速解析:

image.png

取舍:

image.png

header key 规范化:

表映射:

image.png 取:

  1. 超高的转换效率

  2. 比 net.http 提高 40倍

舍:

  1. 额外的内存开销
  2. 变更困难。表变化的话得改框架,但是一般不变

热点资源池化:

场景:在高并发时对每一个请求分配释放内存,对内存压力大 image.png

资源池化是指将系统中的计算资源(如线程、连接、内存、缓存等)进行集中管理并池化,避免频繁的资源创建和销毁。在很多应用场景中,某些资源会成为“热点”,即频繁被访问或请求。通过池化这些热点资源,可以提高性能和效率。 go常用的结构体和模式能够帮助实现池化,例如 sync.Pool(结构体)。

对高频的header,当请求来的时候从池中拿出,解决了再放回池子。进行复用。

image.png 取:

  • 减少了内存分配
  • 提高了内存复用
  • 降低了GC压力
  • 性能提升

舍:

  • 额外的 Reset 逻辑。放回池子时,要进行复杂的操作
  • 请求内有效,超出请求周期(数据不一致)
  • 问题定位难度增加

企业实践

  • 追求性能
  • 追求易用,减少误用
  • 打通内部生态
  • 文档建设、用户群建设(不做客服)