跳转到内容

面试专题

Netty 面试的区分度不在于背出“主从 Reactor”,而在于能解释线程归属、引用计数、Pipeline 方向和失败路径。

先给答案:先判断线程归属,再判断事件方向

Section titled “先给答案:先判断线程归属,再判断事件方向”

Netty 问题可以先沿两条线定位:Channel 的 I/O 和任务是否仍在绑定的 EventLoop 上有序执行,事件是否按 Pipeline 的入站或出站方向经过了预期 Handler。线程归属保证局部串行,链表方向决定编解码、业务处理和写回顺序。

线上高频故障集中在三个边界:业务阻塞 EventLoop、ByteBuf 引用计数失配、Handler 顺序或传播调用错误。排查时只看“Reactor 模型”不够,还要核对任务队列、fire*/write* 起点和数据最终由谁释放。

30 秒版:一个 Channel 注册到一个 EventLoop,EventLoop 负责该 Channel 的 I/O 和任务执行。多线程 EventLoopGroup 通过 next() 分配 Channel,但同一 Channel 的事件通常在同一个 EventLoop 串行执行。

展开:注册逻辑见 MultithreadEventLoopGroup.java:85-97;I/O 和任务交替见 SingleThreadIoEventLoop.java:192-205。

加分项:跨线程注册会投递任务,而不是直接操作 Selector,见 SingleThreadIoEventLoop.java:234-260。

常见错答:说成“多个线程同时处理同一个 Channel”,这会破坏免锁前提。

Pipeline 为什么能支持入站和出站?

Section titled “Pipeline 为什么能支持入站和出站?”

30 秒版:Pipeline 是带 head/tail 哨兵的双向 Context 链。入站从当前节点向后找 inbound Handler,出站从当前节点向前找 outbound Handler。

展开:入站传播见 AbstractChannelHandlerContext.java:341-367,出站传播见 AbstractChannelHandlerContext.java:780-835。

加分项:ctx.write 从当前 Context 继续,channel.write 从 tail 开始。

常见错答:把 Pipeline 说成单向链,会解释不清编码器为什么在业务 Handler 前面执行。

30 秒版:网络缓冲区大量使用堆外内存,不能只依赖 GC 管理释放时机。引用计数让 ByteBuf 在最后一个使用者完成后立即归还池。

展开:池化入口见 PooledByteBufAllocator.java:397-408,组件所有权转移见 CompositeByteBuf.java:154-180。

加分项:异步保存或跨线程传递必须 retain,否则下游处理完成后 buffer 可能已被释放。

常见错答:把 ByteBuf 当成普通不可变 byte array,忽略 reader/writer index 和 release。

30 秒版:FileRegion 使用 FileChannel.transferTo,CompositeByteBuf 通过多个组件组成逻辑连续空间,slice/duplicate 创建共享底层内存的视图。

展开:DefaultFileRegion.transferTo 见 DefaultFileRegion.java:114-140;Composite 组件插入见 CompositeByteBuf.java:263-310。

加分项:Composite 组件过多时可能 consolidate,零拷贝不是无条件保证。

常见错答:把所有 ByteBuf 操作都称为操作系统级零拷贝。

ByteToMessageDecoder 如何解决粘包拆包?

Section titled “ByteToMessageDecoder 如何解决粘包拆包?”

30 秒版:它把多次到达的 ByteBuf 累积起来,循环调用 decode;半包时保留状态,多帧时一次输出多个消息。

展开:入口和释放见 ByteToMessageDecoder.java:285-328,重入队列见 ByteToMessageDecoder.java:330-340。

加分项:长度字段解码器会限制 maxFrameLength,并在超长时进入 discard 模式,见 LengthFieldBasedFrameDecoder.java:339-377。

常见错答:认为一次 socket read 就对应一次业务消息。

HashedWheelTimer 为什么比优先队列适合超时任务?

Section titled “HashedWheelTimer 为什么比优先队列适合超时任务?”

30 秒版:时间轮通过槽位和 remainingRounds 把大量定时任务的插入降为近似 O(1),牺牲精确时间和部分空间换吞吐。

展开:任务转移和槽位计算见 HashedWheelTimer.java:522-544,到期处理见 HashedWheelTimer.java:783-802。

加分项:每 tick 最多搬运 100000 个 timeout,避免新增任务持续淹没 worker。

常见错答:说时间轮能保证精确到期;EventLoop 忙时任务仍会延迟。

EventLoop 单线程,为什么 ctx.executor() 还重要?

Section titled “EventLoop 单线程,为什么 ctx.executor() 还重要?”

因为 Pipeline 中的 Handler 可以绑定不同 executor。AbstractChannelHandlerContext 会比较目标 executor 是否在当前线程;不在时创建任务投递。它既保证 Handler 的线程约束,也允许把阻塞或隔离逻辑放到独立执行器,但代价是排队和上下文切换。

write 成功是否代表数据已经发到对端?

Section titled “write 成功是否代表数据已经发到对端?”

不代表。ChannelOutboundBuffer#addMessage 只是把消息加入待写链表并累计 pending bytes,addFlush 才把消息标记为 flushed;底层 socket 可能仍受发送缓冲区和背压影响,坐标为 ChannelOutboundBuffer.java:110-169。

4.2 的 ByteBuf 内存池还能只讲 Arena/Chunk 吗?

Section titled “4.2 的 ByteBuf 内存池还能只讲 Arena/Chunk 吗?”

不能。经典路径仍由 PooledByteBufAllocator、PoolArena、PoolThreadCache 组成,但 4.2 还提供 AdaptivePoolingAllocator,按 size class 和 magazine 自适应选择分配路径,见 AdaptivePoolingAllocator.java:262-285。回答时应先确认项目实际 allocator 配置。

注册可能发生在 EventLoop 任务提交失败或 Channel 初始化异常时,此时 Channel 可能尚未注册,不能依赖正常 Pipeline 生命周期。AbstractChannel 在提交失败时强制关闭并设置 close future,见 AbstractChannel.java:345-363;这是失败路径的资源兜底。

线上堆外内存持续增长,怎么排查?

Section titled “线上堆外内存持续增长,怎么排查?”

先确认 allocator 类型和 direct memory 上限,再看 leak detector、allocator metric、ChannelOutboundBuffer pending bytes 和业务 Handler 的 release/retain 配对。重点检查异步队列、CompositeByteBuf 所有权转移、写失败以及 Decoder 把 ByteBuf 保存到缓存的路径。

连接大量空闲但不释放,怎么排查?

Section titled “连接大量空闲但不释放,怎么排查?”

确认 IdleStateHandler 是否已加入正确 Pipeline、时间是否为 0、事件是否被业务 Handler 消费,以及 EventLoop 是否被阻塞。它默认只触发 IdleStateEvent,不会自动 close;关闭策略必须由后续 Handler 实现。

  • 线上使用 Netty 4.1 还是 4.2,allocator 是否定制?
  • 业务 Handler 是否允许阻塞,阻塞任务如何隔离?
  • 连接空闲、写缓冲高水位和对端半关闭分别如何处理?