一、网络编程在等什么?
所有网络程序,CPU实际只做三件事:
-
计算
-
等待数据到来
-
在等待的时候不要浪费CPU
高性能网络编程的本质是:用尽可能少的线程,管理尽可能多的连接,并且不空转。(让少量线程,像电话接线员一样,同时服务成千上万条连接)
二、从最原始模型开始
2.1 阻塞I/O(一个连接一个线程)
conn -> read() 阻塞 -> 数据 -> 处理
优点:模型简单
问题:线程=内存+调度成本,C10K直接爆炸
C10K 是 IT 界的一个著名术语,它的全称是 Client 10,000 问题。
简单来说,C10K 问题就是:“如何让服务器同时处理 10,000 个并发连接?”
2.2 非阻塞+轮询(CPU杀手)
while true:
for conn in conns:
read()
优点:无
问题:大量空轮询,CPU100%,吞吐却不高。
三、I/O多路复用:一次等很多
3.1 核心思想
把等I/O这件事交给内核,应用只问一句:“哪些fd限制可以读写了?”
3.2 select/poll/epoll对比
| 机制 | 特点 | 问题 |
| select | 跨平台 | fd数量有限,O(n) |
| poll | 无fd上限 | 仍是O(n) |
| epoll | 事件驱动 | Linux专属 |
四、epoll:Linux的终极答案
4.1 epoll三件套
epoll_create 创建一个epoll实例,新建就绪表
-> epoll_ctl ADD、DEL、MOD 管理列表,埋回调,把事件丢到就绪表里
-> epoll_wait 不需要遍历,利用回调机制,谁有数据就在就绪链表里
4.2 epoll为什么快?
-
事件驱动,不是轮询
-
就绪fd(文件描述符)由内核维护
-
O(1)级别唤醒
epoll不关心你有多少连接,只关心谁准备好了。
4.3 LT vs ET
| 模式 | 特点 |
| LT(水平触发) | 没读完会一直通知 |
| ET(边缘触发) | 只在状态变化时通知 |
Go runtime使用的是LT思路 + 自己控制读取节奏
五、Go的答案:netpoll
netpoll是什么?netpoll是Go runtime内置的I/O多路复用抽象层
-
Linux -> epoll
-
macOS -> kqueue
-
Windows -> IOCP
你写的Go网络程序
conn.Read()
背后并不是“阻塞线程”,而是:
goroutine 先休息(park)
线程去干别的
epoll 监听这个 socket
数据一来
把 goroutine 叫醒
六、Go网络模型全景图
┌─────────────┐
│ goroutine │
│ Read/Write │
└─────┬───────┘
│
┌───────▼────────┐
│ netpoller │ ← epoll_wait
└───────┬────────┘
│
┌───────▼────────┐
│ OS epoll/kq │
└────────────────┘
七、关键机制:goroutine如何“不阻塞”?
读写流程简化版
-
fd设置为non-blocking
-
读不到数据-> EAGAIN
-
goroutine park
-
fd注册到netpoll
-
epoll事件到来
-
goroutine被唤醒继续执行
阻塞的是goroutine,不是OS线程
八、为什么Go不让你直接用epoll?
-
1 跨平台:epoll/kqueue/IOCP语义不同
-
2 调度器深度绑定:netpoll和GMP强耦合
-
3 防止误用:99%的手写epoll都会和调度器打架
九、Go网络高性能的真正来源
不是epoll本身,而是:
epoll+goroutine的解耦
M:N 调度模型
极低的goroutine成本
I/O与调度的深度融合
十、常见误区
10.1 每个连接一个goroutine会不会很多?
答:goroutine不等于thread,goroutine初始栈2KB,调度代价极低
10.2 Go网络慢?
答:慢的通常是:业务逻辑、GC压力、锁竞争、syscall滥用
十一、工程实践建议
连接数大-> 减少per-conn状态
少用大锁,多用分片
I/O密集型服务 -> Go非常合适
CPU密集型 -> 注意GOMAXPROCS
十二、总结
Go的高性能网络,不是你写了epoll,而是你“不用写epoll”
人生不是拼命做更多事,而是学会等待真正该你出手的时刻。
加班计算器
*源码地址*
1、公众号“Codee君”回复“每日一Go”获取源码
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!