C10K 问题是指服务器在处理大量并发连接(例如 10,000 个客户端连接)时所面临的性能瓶颈问题。这个问题最初由 Dan Kegel 在 1999 年提出,随着互联网的普及和高并发需求的增加,C10K 问题成为了服务器设计和网络编程中的一个重要挑战。
想让应用在高并发下稳如老狗,CPU 得高效利用,上下文切换得少,内存占用得低。线程数别比 CPU 核心多太多,多了就像饭店里服务员扎堆,忙不过来还互相撞翻盘子。
最靠谱的办法是用非阻塞逻辑,或者让 CPU 密集型任务上场(但得小心,别把自己绕晕)。实际项目里,分清阻塞和非阻塞逻辑可没那么简单,常常需要重构代码。比如,加个 RabbitMQ 或 Kafka 队列,把任务先缓冲,再把阻塞和非阻塞逻辑拆开。数据库访问是个典型例子,JDBC 目前没官方非阻塞驱动,ADBC 也早 “凉凉” 了。
下面是一些实用的建议,帮助你在高并发场景下优化应用:
jstack
走起),看看创建了多少线程,真实负载下分析最准——很多线程池是懒加载的。自定义线程记得统一命名,比如 FunTester-worker-1
,方便排查问题。高并发场景下,线程模型和内存开销是大 Boss。核心思路:别让每个连接绑一个线程,用高效的 TCP 读取方式和事件驱动库,像 Netty 这样的 “神器”。TCP 连接可不是免费午餐,最贵的当属三次握手。建议用持久连接(Keep-Alive)。接收新连接时,没持久连接的话,短时间堆积大量连接,必须排队等应用 accept。SYN 队列是等三次握手的,LISTEN 队列是握手完带缓冲区的,等着被 accept。高负载下,accept 线程如果忙着处理已有连接的 IO,新连接就得排队等。Netty 服务器配置是个好例子:
// Netty服务器配置示例,FunTester 出品,必属精品
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 只负责接收新连接,1个线程够用
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 处理IO,默认线程数是 CPU 核数*2
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024) // LISTEN 队列大小,防止连接堆积
.childOption(ChannelOption.SO_KEEPALIVE, true); // 开启持久连接,省资源
如果 LISTEN Backlog 满了咋办?可以加 bossGroup
线程数。测试 2 万客户端连接时,用 ss -tuln
看积压情况:
ss -tuln | grep 8080
输出里 Send-Q
是 Backlog 最大容量,Recv-Q
是当前等待 accept 的连接数。默认 Backlog 一般是 128,调大点能抗更多连接。TCP 发送/接收缓冲区是资源大户,设置得讲究:
// 设置TCP缓冲区,FunTester 提醒:别设太大浪费内存,太小限制吞吐
bootstrap.childOption(ChannelOption.SO_RCVBUF, 128 * 1024) // 接收缓冲区 128KB
.childOption(ChannelOption.SO_SNDBUF, 128 * 1024); // 发送缓冲区 128KB
为啥不建议每个连接绑线程?Java 线程是 “重量级选手”,和内核线程一对一。默认每个线程预留 1MB 虚拟内存(-Xss
可调),实际驻留集 200-300KB,用 Native Memory Tracking
能看到:
// 开启NMT,FunTester 教你看线程内存
java -XX:+NativeMemoryTracking=summary -jar app.jar
jcmd <pid> VM.native_memory
高负载下,对象分配得悠着点,别让 JVM 内存白白浪费。Netty 的 ByteBuf
就是个神级工具,堪称内存管理的 “瑞士军刀”。
JDK 的 ByteBuffer
分 HeapByteBuffer
(堆内存)和 DirectByteBuffer
(堆外内存)。DirectByteBuffer
直接传给 OS 做 I/O,效率高。比如 1 万连接广播同一条消息,用字符串得重复编码,堆里一堆冗余对象等着 GC。换成 DirectByteBuffer
,编码一次,所有连接共享,OS 直接处理,省时省力,简直像点外卖只付一次配送费。
但 DirectByteBuffer
分配成本高,JDK 每个 I/O 线程维护内部缓存。HeapByteBuffer
分配便宜,比如广播字符串,先编码成字节数组,只做一次,再交给 JDK 缓存处理。
Netty 的 ByteBuf
是对 ByteBuffer
的升级,读写索引分开,API 更友好。JDK 的 DirectByteBuffer
靠 GC 的 Cleaner
类回收堆外内存。如果应用优化到几乎不触发 GC,堆外内存可能不释放,得手动 System.gc()
。Netty 的 ByteBuf
支持池化和非池化,堆外内存用引用计数管理,手动释放虽麻烦,但能防泄漏。
// Netty ByteBuf 示例,FunTester 教你高效用内存
ByteBuf buffer = Unpooled.directBuffer(1024); // 分配 1KB 堆外内存
try {
buffer.writeBytes("Hello, FunTester!".getBytes()); // 写入数据
// 模拟发送给所有连接,共享 buffer
channel.writeAndFlush(buffer.retain()); // 增加引用计数
} finally {
buffer.release(); // 释放引用,防止内存泄漏
}
性能优化逃不开吞吐量和延迟的博弈。拿 JVM 举例,ParallelGC
追求吞吐量,ShenandoahGC
和 ZGC
偏低延迟。Netty 应用里,常见抉择是:每条消息立刻发送,还是稍等一会批量发?Netty 的刷新机制很适合这场景。批量发送能把系统调用开销压到 20%,用点延迟换吞吐量,性价比高得像双十一抢货。
// Netty 批量发送示例,FunTester 教你省系统调用
// 注释掉 context.writeAndFlush(obj),改为
context.write(obj); // 写入但不立刻发送
// 每 5 条消息 flush 一次
if (msgCount % 5 == 0) {
context.flush(); // 批量发送,效率 up!
}
用 Java Flight Recorder(JFR)看效果:
jdk.SocketWrite {
startTime = 22:12:01.603
duration = 2.23 ms
bytesWritten = 60 bytes
}
jdk.SocketRead {
startTime = 22:12:01.605
duration = 0.0757 ms
bytesRead = 60 bytes
}
逻辑上发了 5 条消息,实际只有一次读写操作,系统调用省到家了!
GraalVM 的 Native-Image 技术,简单来说,就是把 Java 程序提前编译成独立的可执行文件,彻底告别那些没用的类、方法和 JIT 运行时数据,内存占用直接瘦身成 “小码农”。虽然执行效率略逊于 JIT,但在实际工程场景下,Native-Image 能让单机轻松跑更多实例,特别适合无服务器架构,启动速度快、资源占用低,简直是微服务界的 “性能跑车”。比如在云原生部署中,Native-Image 能让你的服务像 “闪电侠” 一样秒启动,资源利用率高到让运维小伙伴直呼 “真香”。
Project Loom 带来的 Fiber(纤程),把线程调度从内核搬到用户空间,彻底改变了传统 Java 阻塞调用的玩法。以前 JDBC 一阻塞,整个线程都得 “歇菜”,现在 Fiber 只暂停当前任务,线程还能继续 “打工”,上下文切换成本低到只有几百字节,初始化速度快得像 “抢红包”。实际项目里,哪怕有 1 万个请求,也能用几个线程轻松搞定,彻底告别 “线程池炸锅” 的烦恼。对于那些 JDBC 依赖重的老系统,Loom 就像 “救命稻草”,未来可期,值得每个工程师关注和尝试。