前置知识
1.NIO和BIO(阻塞式io)
在传统阻塞 IO 模型下(BIO):
-
每个连接的
Read或Write操作都是阻塞的:- 如果对方没有发送数据,
Read操作会卡住,直到数据到来。 - 如果对方未准备好接收数据,
Write操作也可能被阻塞。
- 如果对方没有发送数据,
-
为了解决这个问题,通常会为每个连接分配一个独立的线程或 Goroutine,但这会带来:
- 线程/Goroutine 数量膨胀: 当连接数增加时,需要更多的 Goroutine 或线程,导致调度开销增大。
- 资源浪费: 即使大部分连接处于空闲状态,线程或 Goroutine 依然会占用资源。
而我们的NIO模型:
-
非阻塞 socket:
- 网络连接的
Read或Write操作不会阻塞。如果当前没有数据可读或可写,这些操作会立即返回而不会卡住线程。
- 网络连接的
-
事件监听机制:
- 使用操作系统的事件通知机制(如
epoll或kqueue),监听文件描述符的状态变化。 - 只有当文件描述符变得可读(有数据到达)或可写(可以发送数据)时,才会进行真正的
Read或Write操作。
- 使用操作系统的事件通知机制(如
-
事件循环(Event Loop):
- 单独的线程监听和分发事件,处理多个连接的状态变化。
我的简单理解就是Nio就是使用监听确认文件可读可写才会进行read和write操作,这样不会在读写的时候阻塞住了。(事件驱动机制)
2.net
net 是Go 标准库中的一个包,主要用于网络编程。它提供了对 TCP、UDP、IP 等网络协议的支持,可以用来创建客户端和服务器应用程序。采用Bio,用户处理buffer。
3.netpoll
netpoll 是 Go 生态中的一个库(非标准库),主要用于 高性能网络 I/O(NIO)灵感来自操作系统的 epoll 和 kqueue,主要关注于大规模并发网络连接的场景,特别适合用于实现高性能服务器框架。
-
注册监听:
- 将网络连接的文件描述符(socket)注册到
epoll或kqueue。 - 指定感兴趣的事件(如可读事件、可写事件)。
- 将网络连接的文件描述符(socket)注册到
-
等待事件:
- 通过
epoll_wait(Linux)或kevent(macOS/BSD)等待事件触发。 - 当某个文件描述符上有数据可读或可写时,操作系统会通知事件循环。
- 通过
-
回调处理:
- 事件循环将触发的事件分发给用户定义的回调函数。
- 在回调中执行非阻塞的
Read或Write操作。
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以下
- 具体实现:
- Peek(n int) ([]byte, error):
- 从数据流中预览
n个字节,但不移动读取位置。 - 1.通过维护一个缓存区(buffer),减少实际 IO 操作。 2. 若缓存区中数据不足时,从底层读取更多数据填充缓存。
- 从数据流中预览
- Discard(n int) (discarded int, err error)
- 让指针向前移动
- 跳过无关数据或读取后释放资源。
- 修改缓存区的偏移量,不需要实际删除数据,提升性能。
- 处理
n超出缓存区大小的情况:直接从流中跳过数据而不缓存。
- Release() error
- 释放资源,比如关闭底层连接或回收缓冲区。
- 希望下一次请求能复用之前的空间
- Peek(n int) ([]byte, error):
对netpoll优化需求:
- 存下全部的header,拷贝出完整的body 。如图我们的header和body可能在两个节点上。
方法:
- 在底层分配足够大的buffer(节点)
netpoll with nocopy peek:
根据历史最大请求进行分配,把header、body在底层拼好返回给最上层。
当有小的请求时会浪费一些内存,所以还要限制buffer size
不同网络库优势:
go net :流式友好,包小性能高 netpoll: 中大包性能好,时延低
针对协议的优化
headers解析:
- 找到header Line 边界: \r\n
找到\n再看前一个是不是\r就行
使用SIMD技术,一次匹配多个。一般我们得一个个匹配,这样的复杂度是O(n),使用SIMD技术更快。go集成了该指令集。
- 快速解析:
取舍:
header key 规范化:
表映射:
取:
-
超高的转换效率
-
比 net.http 提高 40倍
舍:
- 额外的内存开销
- 变更困难。表变化的话得改框架,但是一般不变
热点资源池化:
场景:在高并发时对每一个请求分配释放内存,对内存压力大
资源池化是指将系统中的计算资源(如线程、连接、内存、缓存等)进行集中管理并池化,避免频繁的资源创建和销毁。在很多应用场景中,某些资源会成为“热点”,即频繁被访问或请求。通过池化这些热点资源,可以提高性能和效率。 go常用的结构体和模式能够帮助实现池化,例如
sync.Pool(结构体)。
对高频的header,当请求来的时候从池中拿出,解决了再放回池子。进行复用。
取:
- 减少了内存分配
- 提高了内存复用
- 降低了GC压力
- 性能提升
舍:
- 额外的 Reset 逻辑。放回池子时,要进行复杂的操作
- 请求内有效,超出请求周期(数据不一致)
- 问题定位难度增加
企业实践
- 追求性能
- 追求易用,减少误用
- 打通内部生态
- 文档建设、用户群建设(不做客服)