核心概念

在 Netty 中,NioEventLoopGroup 默认线程数是 CPU 核数的 2 倍

EventLoopGroup workerGroup = new NioEventLoopGroup();  // 默认线程数

查看源码可以验证:

// NioEventLoopGroup 默认构造器
public NioEventLoopGroup() {
    this(0);  // 0 表示使用默认值
}

// MultithreadEventLoopGroup
protected MultithreadEventLoopGroup(int nThreads, ...) {
    super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, ...);
}

// DEFAULT_EVENT_LOOP_THREADS 的定义
static {
    DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
        "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
}

为什么是 2 倍?

1. I/O 密集型与 CPU 密集型的平衡

线程数选择的经典公式

  • CPU 密集型:线程数 = CPU 核数 + 1
  • I/O 密集型:线程数 = CPU 核数 × (1 + I/O 等待时间 / CPU 计算时间)

Netty 的特点

  • 虽然是 NIO(非阻塞 I/O),但 EventLoop 线程不仅处理 I/O 事件,还执行用户自定义任务
  • 典型场景:编解码、业务逻辑、定时任务等

2 倍核数的考量

  • 当某个 EventLoop 线程在处理业务逻辑时,其他线程可以继续处理 I/O 事件
  • 适度的线程冗余,保证 CPU 始终有任务执行,提升整体吞吐量

2. 避免过多的上下文切换

如果线程数过多(如 10 倍 CPU 核数):

  • 频繁的上下文切换:线程切换本身有开销(保存/恢复寄存器、刷新 TLB)
  • CPU 缓存失效:线程切换导致 L1/L2 缓存频繁失效,降低命中率

2 倍核数的优势

  • 在并发度和切换开销之间取得平衡
  • 通常情况下,2 倍核数能让 CPU 保持较高利用率

3. Netty 的无锁化设计

Netty 将每个 Channel 绑定到固定的 EventLoop

// Channel 注册到 EventLoop
ChannelFuture regFuture = config().group().register(channel);

关键特性

  • 同一个 Channel 的所有事件都在同一个线程中处理,无需加锁
  • 线程数过多会导致 Channel 分散,增加内存开销和调度复杂度
  • 2 倍核数能在并发度和资源消耗之间取得平衡

源码实现细节

EventLoop 的工作机制

// NioEventLoop.run() 核心循环
protected void run() {
    for (;;) {
        // 1. 检测 I/O 事件(可能阻塞)
        select(wakenUp.getAndSet(false));
        
        // 2. 处理 I/O 事件
        processSelectedKeys();
        
        // 3. 处理异步任务队列
        runAllTasks();
    }
}

特点

  • select() 可能阻塞等待 I/O 事件(但有超时机制)
  • runAllTasks() 执行用户任务时,I/O 事件暂时无法处理
  • 多个 EventLoop 并行工作,保证整体响应性

实际调优建议

1. 根据业务场景调整

纯 I/O 转发(如代理服务器)

// I/O 密集,业务逻辑简单,CPU 核数即可
EventLoopGroup workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors());

复杂业务逻辑(如 RPC 服务)

// 业务逻辑复杂,可适当增加线程数
EventLoopGroup workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);

业务逻辑耗时较长

// 不推荐在 EventLoop 线程中执行耗时操作!
// 应将耗时任务提交到业务线程池
private static final ExecutorService bizExecutor = Executors.newFixedThreadPool(100);

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    bizExecutor.submit(() -> {
        // 耗时的业务逻辑
        processBusinessLogic(msg);
    });
}

2. 性能监控指标

关注以下指标来判断线程数是否合理:

  1. CPU 利用率:理想值 70%-80%,过低说明线程数不足,过高可能过载
  2. 事件处理延迟:通过 ChannelHandlerContext.executor().submit() 的排队时间判断
  3. 线程竞争情况:查看 EventLoop 的任务队列积压情况
// 监控 EventLoop 的待处理任务数
NioEventLoop eventLoop = (NioEventLoop) channel.eventLoop();
int pendingTasks = eventLoop.pendingTasks();

3. 常见误区

误区 1:线程越多越好

  • 错误:开启 100 个 EventLoop 线程处理 10 个连接
  • 后果:大量线程空转,上下文切换频繁

误区 2:一个线程处理所有连接

EventLoopGroup workerGroup = new NioEventLoopGroup(1);  // 单线程
  • 风险:单线程成为瓶颈,无法利用多核 CPU

误区 3:在 EventLoop 中执行阻塞操作

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // ❌ 错误:阻塞 EventLoop 线程
    Thread.sleep(1000);
    
    // ✅ 正确:提交到业务线程池
    bizExecutor.submit(() -> heavyTask());
}

分布式场景的考量

在微服务架构中,Netty 作为 RPC 通信层:

  1. 客户端:线程数不宜过多(通常 CPU 核数即可)
  2. 服务端:根据 QPS 和业务复杂度调整(通常 2 倍核数)
  3. 网关场景:纯转发可减少线程数,复杂路由逻辑可增加

面试答题总结

核心回答

  1. 默认值:CPU 核数的 2 倍(NettyRuntime.availableProcessors() * 2
  2. 原因:平衡 I/O 处理和业务逻辑执行,避免过多上下文切换
  3. 无锁化设计:每个 Channel 绑定到固定 EventLoop,线程数合理即可

调优原则

  • 纯转发:CPU 核数
  • 复杂业务:2 倍核数
  • 耗时操作:必须用独立线程池,不要阻塞 EventLoop

关键点:Netty 的高性能源于无锁化设计,合理的线程数能充分发挥多核优势,同时避免资源浪费。