Ruby WebSocket和Redis导致与pubsub和/或brpop的挂起连接
我正在WebSocket(WS)中发布一个Redis订阅。当我收到WS-open时,我对请求执行线程,然后实例化Redis客户端。在open中,我执行Redis线程并发布订阅 这一切都很好,直到我收到一个意外的WS-close。此时,运行Redis订阅的线程消失了。如果我退订,我会被绞死。如果我不退订,我留下了一个虚拟订阅,这会给我下一轮带来麻烦 一旦发布订阅的线程终止,是否有办法删除订阅?我注意到Redis实例对于终止的线程有一个mon变量。示例Ruby代码是:Ruby WebSocket和Redis导致与pubsub和/或brpop的挂起连接,ruby,websocket,redis,publish-subscribe,eventmachine,Ruby,Websocket,Redis,Publish Subscribe,Eventmachine,我正在WebSocket(WS)中发布一个Redis订阅。当我收到WS-open时,我对请求执行线程,然后实例化Redis客户端。在open中,我执行Redis线程并发布订阅 这一切都很好,直到我收到一个意外的WS-close。此时,运行Redis订阅的线程消失了。如果我退订,我会被绞死。如果我不退订,我留下了一个虚拟订阅,这会给我下一轮带来麻烦 一旦发布订阅的线程终止,是否有办法删除订阅?我注意到Redis实例对于终止的线程有一个mon变量。示例Ruby代码是: class Backend
class Backend
include MInit
def initialize(app)
setup
@app = app
end
def run!(env)
if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env, [], ping: KEEPALIVE_TIME)
ws_thread = Thread.fork(env) do
credis = Redis.new(host: @redis_uri.host, port: @redis_uri.port, password: @redis_uri.password)
ws.on :open do |event|
channel = URI.parse(event.target.url).path[1..URI.parse(event.target.url).path.length]
redis_thread = Thread.fork do
credis.subscribe(channel) do |on|
on.message do |message_channel, message|
sent = ws.send(message)
end
on.unsubscribe do |message_channel|
puts "Unsubscribe on channel:#{channel};"
end
end
end
end
ws.on :message do |event|
handoff(ws: ws, event: event)
end
ws.on :close do |event|
# Hang occurs here
unsubscribed = credis.unsubscribe(channel)
end
ws.on :error do |event|
ws.close
end
# Return async Rack response
ws.rack_response
end
end
else
@app.call(env)
end
private
def handoff(ws: nil, event: nil, source: nil, message: nil)
# processing
end
end
一旦我真正理解了问题,解决方法就相当简单了。Redis线程实际上仍然存在。但是,Redis仍然挂起,因为它正在等待线程获得控制权。为此,WS.close代码需要通过在WS.close中使用EM.next_勾选来放弃控制,如下所示:
ws.on :close do |event|
EM.next_tick do
# Hang occurs here
unsubscribed = credis.unsubscribe(channel)
end
end
一旦我真正理解了问题,解决方法就相当简单了。Redis线程实际上仍然存在。但是,Redis仍然挂起,因为它正在等待线程获得控制权。为此,WS.close代码需要通过在WS.close中使用EM.next_勾选来放弃控制,如下所示:
ws.on :close do |event|
EM.next_tick do
# Hang occurs here
unsubscribed = credis.unsubscribe(channel)
end
end
这是一个较长的评论,旨在提供一个解决方案,而不是一个解决方案 如果是我的申请,我会重新考虑设计 为每个websocket客户端打开一个新的Redis连接和一个新线程是一项相当大的资源投入 只是澄清一下,每个到Redis的连接都需要一个TCP/IP套接字(这是一个有限的资源)和内存。每个线程的预留堆栈内存应该大约为2MB。。。因此,1K Redis连接和线程可能会产生大约2GB的内存成本 此外,Redis服务器本身通常可以接受的连接数量有限(尽管这通常是一个价格问题,而不是一个硬限制,因为它们是为扩展而设计的) 重新调整设计应该非常简单,这样一个线程和连接就可以为所有websocket客户端提供服务,这还可以更轻松地进行
订阅/取消订阅管理
这可以使用内部每进程广播系统(如由实现)或使用Redissubscribe
/punsubscribe
命令来执行。这是一个较长的注释,提供了一种解决方法,而不是解决方案
如果是我的申请,我会重新考虑设计
为每个websocket客户端打开一个新的Redis连接和一个新线程是一项相当大的资源投入
只是澄清一下,每个到Redis的连接都需要一个TCP/IP套接字(这是一个有限的资源)和内存。每个线程的预留堆栈内存应该大约为2MB。。。因此,1K Redis连接和线程可能会产生大约2GB的内存成本
此外,Redis服务器本身通常可以接受的连接数量有限(尽管这通常是一个价格问题,而不是一个硬限制,因为它们是为扩展而设计的)
重新调整设计应该非常简单,这样一个线程和连接就可以为所有websocket客户端提供服务,这还可以更轻松地进行订阅/取消订阅管理
这可以使用内部每进程广播系统(如由实现)或使用Redissubscribe
/punsubscribe
命令来执行。您可以尝试的另一种方法是让Redis定期向主题发布一些无害的消息,强制写入所有订阅。任何“幻影”订阅都应该被清除/关闭,因为Redis检测到向死掉/关闭的对等方写入的错误。@Castaglia有趣。在我的情况下,知道之前的订阅无法清除,但不知道如何清除,恢复后,我发布了另一个订阅并继续处理。这会将消息发布到同一频道,虚拟订阅和新直播频道都使用相同的名称。你希望这也能清除幻影吗?我这样问是因为,随着时间的推移,我在那个场景中遇到了资源限制。啊,我想我明白了。订阅绑定到TCP连接,并且TCP连接仍然存在;客户端上的线程从应用程序其余部分的POV中退出,从而“丢失”订阅。对我想知道从同一TCP连接(清除任何虚订阅/丢失/延迟订阅)取消订阅(指定所有频道,或不取消订阅所有频道)然后显式重新订阅是否可行。@Castaglia,您是否同意我添加的更新和代码将可靠地终止连接?到目前为止,似乎对我有效,但是,好吧,还需要进一步的测试和验证。谢谢。因为没有其他方法可以获得连接ID,我想不出更优雅的方法了。如果您想使用客户机的本地地址/端口,而不是连接ID来执行client KILL
命令,可能会这样做?另一种方法是让Redis定期向主题发布一些无害的消息,强制写入所有订阅。任何“幻影”订阅都应该被清除/关闭,因为Redis检测到向死掉/关闭的对等方写入的错误。@Castaglia有趣。在我的情况下,知道之前的订阅无法清除,但不知道如何清除,恢复后,我发布了另一个订阅并继续处理。这会将消息发布到同一频道,包括虚拟订阅和同一na下的新直播频道