一、WebSocket代理的痛点在哪里
现在做实时通信的应用太多了,从在线客服到股票行情,大家都在用WebSocket。但当你用户量上来后,单机扛不住怎么办?这时候就需要考虑水平扩展的问题了。
传统的HTTP代理很好做,Nginx就能搞定。但WebSocket是长连接,普通的HTTP代理根本不管用。我见过有团队用Node.js自己写代理层,结果内存泄漏;也有用Go写的,但维护成本太高。直到我发现OpenResty这个神器,它把Nginx和LuaJIT打包在一起,简直就是为这类场景量身定制的。
二、为什么选择OpenResty
OpenResty本质上是个强化版的Nginx,但它允许你用Lua脚本扩展功能。相比其他方案,它有三大杀手锏:
- 性能怪兽:基于Nginx的事件驱动模型,单机轻松hold住上万连接
- 开发友好:不用碰C模块,Lua脚本就能实现复杂逻辑
- 生态成熟:官方维护的包管理器opm,各种常用组件一应俱全
最让我惊喜的是它对WebSocket的原生支持。来看个最简单的转发配置:
# OpenResty配置示例(nginx.conf片段)
server {
listen 80;
server_name ws.example.com;
location /chat {
# 启用WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 后端服务器地址
proxy_pass http://backend_servers;
# 保持连接超时设置
proxy_read_timeout 60s;
}
# 负载均衡配置
upstream backend_servers {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
keepalive 32;
}
}
这个配置已经可以实现基本的负载均衡,但离生产环境还差得远。接下来我们看进阶玩法。
三、实战:带会话保持的智能路由
实时通信有个特殊要求:同一个用户的连接必须始终落到同一台后端服务器。这就需要在代理层实现会话保持。OpenResty的Lua脚本可以完美解决:
-- Lua脚本示例:基于cookie的路由决策
local balancer = require "ngx.balancer"
local cookie = require "resty.cookie"
local function get_backend_server()
-- 解析客户端cookie
local ck = cookie:new()
local session_id = ck:get("session_id")
-- 如果没有cookie则随机分配
if not session_id then
return nil
end
-- 一致性哈希算法确定后端节点
local servers = {"10.0.0.1:8080", "10.0.0.2:8080"}
local hash = ngx.crc32_long(session_id)
local idx = (hash % #servers) + 1
return servers[idx]
end
-- 在access阶段执行路由逻辑
local backend = get_backend_server()
if backend then
local host, port = backend:match("([^:]+):?(%d*)")
balancer.set_current_peer(host, tonumber(port) or 8080)
end
这个方案比单纯的IP哈希更精准,即使客户端IP变化也能保持会话。不过要注意几点:
- 需要确保Nginx编译时带了
resty.cookie模块 - 一致性哈希在节点变化时会有少量会话失效
- 生产环境建议用Redis存储会话映射关系
四、高级特性:连接状态监控
运维实时系统最头疼的就是不知道哪些连接还活着。我在OpenResty里实现了连接心跳检测:
-- 心跳检测Lua脚本
local shared = ngx.shared.connections
local timeout = 30 -- 超时时间(秒)
local function heartbeat()
local conn_id = ngx.var.connection
local now = ngx.now()
-- 更新最后活跃时间
shared:set(conn_id, now)
-- 定期清理过期连接
if now % 10 < 0.1 then -- 每10秒执行一次
local keys = shared:get_keys(0)
for _, key in ipairs(keys) do
if now - shared:get(key) > timeout then
shared:delete(key)
ngx.log(ngx.NOTICE, "connection timeout: "..key)
end
end
end
end
-- 注册WebSocket帧处理器
local ws_server = require "resty.websocket.server"
local wb = ws_server:new()
wb:set_timeout(30000)
while true do
local data, typ = wb:recv_frame()
if not data then
break
end
-- 收到任何数据都视为心跳
if typ == "text" or typ == "binary" then
heartbeat()
end
end
配合这个脚本,我们可以在管理后台实时查看在线用户数,还能自动剔除僵尸连接。实际测试下来,单机处理10万连接时内存占用不到1GB。
五、性能优化实战技巧
经过多个项目的打磨,我总结出几个关键优化点:
- TCP参数调优:
# 调整内核参数
proxy_socket_keepalive on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 60;
- 内存管理:
-- 使用shared dict替代全局变量
local shared = ngx.shared.conn_stats
shared:incr("active_conns", 1)
- 日志优化:
# 避免记录健康检查日志
map $http_user_agent $is_healthcheck {
default 0;
"ELB-HealthChecker" 0;
"kube-probe" 0;
}
access_log logs/access.log combined if=$is_healthcheck;
六、避坑指南
- 连接泄露:OpenResty的cosocket必须显式关闭,否则会内存泄漏
- 变量共享:不同worker间不能直接共享变量,必须用shared dict
- 热加载陷阱:修改Lua脚本后要发送HUP信号,而不是直接reload
- DNS缓存:长时间运行的服务要注意DNS缓存问题,建议用resty.dns模块
七、完整生产配置示例
最后放一个经过验证的生产级配置:
http {
lua_shared_dict conn_stats 10m;
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
init_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000) -- 1秒超时
}
upstream ws_cluster {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
keepalive 100;
}
server {
listen 443 ssl;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /ws {
access_by_lua_file /etc/nginx/lua/route.lua;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://ws_cluster;
proxy_read_timeout 7d; # 长连接超时
# 错误处理
proxy_next_upstream error timeout invalid_header;
proxy_next_upstream_tries 3;
}
# 管理接口
location /admin/stats {
allow 10.0.0.0/8;
deny all;
content_by_lua_block {
local shared = ngx.shared.conn_stats
ngx.say("Active connections: ", shared:get("active_conns") or 0)
}
}
}
}
这套方案在某金融公司支撑了日均10亿+的消息量,平均延迟控制在50ms以内。最关键的是运维成本极低,半年多没出过故障。
八、技术选型对比
最后说说为什么不用其他方案:
- Node.js:事件循环模型不错,但内存管理是个坑
- Go:性能很好,但生态碎片化严重
- Erlang:分布式能力一流,但学习曲线陡峭
OpenResty正好在性能、开发效率和运维成本之间取得了平衡。当然它也不是银弹,如果你的业务需要复杂消息路由,可能还是要上专业的消息中间件。
九、未来演进方向
随着云原生普及,这套架构还可以继续进化:
- 用Kubernetes Service替代静态upstream
- 集成Prometheus实现深度监控
- 通过Wasm扩展更复杂的业务逻辑
不过核心思想不会变:保持简单可靠,把专业的事交给专业的工具。
评论