Ruby Puma Sinatra服务Kernel#spawn启动子进程仍占用端口问题排查
问题根因分析
就算使用Kernel#spawn或者fork+exec、且监听socket已开启O_CLOEXEC标记,仍然出现子进程占用父服务端口的情况,核心原因有以下几种:
- fork与exec之间的竞态窗口
O_CLOEXEC标记的生效时机是exec系统调用执行阶段,会自动关闭所有带该标记的文件描述符。但fork执行完成后、exec开始执行前存在时间差,这段时间内子进程已经完整继承了父进程的所有文件描述符,包括监听端口的socket。如果父服务恰好在此窗口内重启退出,新的服务进程尝试绑定端口时,就会检测到端口被子进程占用。 Kernel#spawn的文件描述符继承逻辑未显式配置
Ruby 2.0+版本的spawn默认开启close_others参数,会关闭所有未显式指定继承的文件描述符,但如果存量代码中存在自定义的fd继承配置、或者使用的Ruby版本较旧,可能会导致监听socket的fd被意外继承到子进程,且exec阶段不会关闭。- 旧版本Puma的隐含bug
部分低版本Puma创建监听socket时,不会主动设置O_CLOEXEC标记,即使你查到单个fd的flag正确,也可能存在其他监听socket未正确配置标记的情况。
解决方案
- 显式配置
spawn参数
调用spawn时强制指定close_others: true,确保所有非必要的文件描述符都在exec阶段关闭,示例如下:get '/start_process' do spawn('long_process arg1', close_others: true) end - 开启端口复用配置
在Puma配置文件中添加reuse_address true配置,开启TCP的SO_REUSEADDR选项,即使端口处于TIME_WAIT状态、或存在进程临时持有端口fd,新的服务进程也可以正常绑定端口,从根本上避免重启失败的问题。 - 升级依赖版本
升级Ruby到2.5+、Puma到5.0+版本,避免旧版本中监听socketO_CLOEXEC标记未正确设置的bug。
内容的提问来源于stack exchange,提问作者Nick Hyland
相关产品推荐
相关产品推荐

