action cable生产环境频繁断连主因是puma/nginx未正确支持websocket协议劫持。需配置puma线程与工作进程、nginx透传upgrade头、redis统一实例,并确保频道流规则、订阅命名、用户标识及连接生命周期管理正确。

Action Cable 在 Rails 5+ 中已开箱即用,但直接跑起来不等于能稳定用于生产——多数失败不是因为写错 subscribed,而是连接没兜住、广播没路由对、Puma 没配 WebSocket 支持。
为什么 Action Cable 在开发环境能连上,部署后频繁断连?
根本原因是生产 Web 服务器(如 Puma)默认不启用 WebSocket 协议劫持。Rails 的 ActionCable.server 依赖底层服务器提供 hijack 能力,而旧版 Puma 或 Nginx 反向代理未透传 Upgrade 头时,连接会在 10 秒内静默关闭。
- 检查
config/puma.rb是否包含activate_control_app和足够线程:必须有threads 5, 5且workers 2(单进程模式无法承载多连接) - Nginx 配置中必须显式透传 WebSocket 相关头:
proxy_http_version 1.1、proxy_set_header Upgrade $http_upgrade、proxy_set_header Connection "upgrade" - 避免在
config/environments/production.rb中设置config.action_cable.disable_request_forgery_protection = true—— 这会绕过 CSRF 校验,但也会让未认证用户直连频道
如何确保 broadcast_to 正确推送到指定用户?
broadcast_to 不是魔法,它依赖频道名与订阅流严格匹配。常见错误是前端订阅了 "NotificationChannel",后端却调用 NotificationChannel.broadcast_to(user, ...),但没在频道里定义 stream_for user。
- 服务端频道必须显式声明流规则:在
app/channels/notification_channel.rb中写def subscribed; stream_for current_user; end,否则broadcast_to找不到目标流 - 前端订阅时不能硬编码频道名,要用类名字符串:正确是
consumer.subscriptions.create("NotificationChannel", {...}),错误是create("notification_channel")(下划线命名不匹配) -
current_user在频道中默认不可用,需在ApplicationCable::Connection中显式赋值:identified_by :current_user+def connect; self.current_user = find_verified_user; end
如何验证 Action Cable 是否真正工作,而不是假连?
只看浏览器 WebSocket 连接状态为 “open” 是误导性的。真实可用性必须通过三重校验:连接建立、订阅成功、消息可收发。
- 打开 Chrome DevTools → Network → Filter 输入
ws,点击连接查看 Frames 标签页:应看到至少一条message类型的文本帧,内容类似{"command":"subscribe","identifier":"{\"channel\":\"NotificationChannel\"}"} - 在 Rails console 中手动触发一次广播:
NotificationChannel.broadcast_to(User.first, { hello: "world" }),然后观察浏览器 Frames 是否收到对应 payload - 若收不到,检查 Redis 是否被 Action Cable 正确使用:默认配置下,
config/cable.yml必须指向同一 Redis 实例(开发用redis://localhost:6379/1,生产务必和 Sidekiq 一致)
前端 consumer 订阅后,received 回调不触发?
这不是 JavaScript 错误,而是频道生命周期没对齐。最常被忽略的是:频道实例在页面跳转或组件卸载时未主动取消订阅,导致新订阅被旧连接阻塞。
- 永远不要在 React/Vue 组件 mount 时创建 subscription 而不 unmount 时
unsubscribe;否则多次切换页面会堆积多个监听器,但只有最后一个能响应 - 检查
app/javascript/channels/consumer.js中的createConsumer是否被重复执行;推荐全局单例:export const consumer = createConsumer() -
received(data)只接收服务器调用broadcast或broadcast_to发出的消息;如果想从控制器直接推,必须先通过通道中转,不能在 controller 里直接调ActionCable.server.broadcast
真正卡住人的从来不是“怎么写”,而是“谁在什么时候持有连接、谁负责清理、Redis 流是否跨进程一致”。上线前务必用 ab 或 wrk 压测 50+ 并发连接,观察 Puma 日志里有没有 WebSocket closed 频繁出现——那说明连接池或超时配置还没调对。











