【问题标题】:Detect load balancer/server changes in WebSocket connection from client从客户端检测 WebSocket 连接中的负载均衡器/服务器更改
【发布时间】:2018-07-09 23:44:02
【问题描述】:

当前配置

我有一个客户端应用程序通过 WebSocket (node.js) 连接连接到云服务器。我需要 WebSocket 来获取传入消息的实时通知。

在此示例中,我们使用 abc.example.com 作为云服务器的域名。

云服务器配置

云由 Amazon Elastic Load Balancer 提供支持。 此云服务器具有以下底层架构:

Cloud architecture

在云更新时,负载均衡器会切换到另一个负载均衡器,因此发布到云的所有新数据都由新的负载均衡器和服务器处理。 因此 abc.example.com 始终可以访问,即使负载平衡器/服务器发生变化也是如此。 (例如进行 HTTP 调用)

WebSocket 配置

WebSocket 配置正在连接到 abc.example.com,它连接到某个服务器并保持连接到该服务器,直到有东西关闭它。

问题

连接后,WebSocket 连接对云上的服务器保持打开状态,并且不会检测负载均衡器何时切换到另一个(例如在云更新时) 因此,如果我为我的客户端向服务器发送新数据(例如新消息),则连接的客户端不会通过 WebSocket 连接接收它。

虽然 HTTP GET 查询可以完成工作,因为它解析了正确的服务器。

据我了解,这是正常的行为,因为客户端与 WebSocket 连接的服务器仍然处于打开状态并且没有关闭连接;也没有发生任何错误。 实际上,我尝试切换负载均衡器,并且服务器(客户端连接的初始服务器)在定期 ping 时仍然向客户端发送 pong 响应。

那么有什么方法可以检测负载平衡器何时从客户端切换?我不允许修改云配置,但如果有相当简单的解决方案,我可以建议。

底线是:我不想在云更新时错过任何通知。

其他观察:

  • 在 t0:
    • 借助 ELB1,客户端应用通过 WebSocket 连接到 server1
    • 向云端发送新消息时通过 WebSocket 接收成功
  • 在 t1:
    • 云更新:从 ELB1 切换到 ELB2
    • WebSocket 接收新消息失败
  • 在 t2:
    • 云更新:从 ELB2 切换到 ELB1
    • 向云端发送新消息时通过 WebSocket 接收成功

感谢任何建议/帮助,

*This answer 帮助我了解了网络结构,但我仍然没有想法。 *如果术语不完全合适,我们深表歉意。

【问题讨论】:

  • 我可能忽略了一些东西,但是如果套接字客户端要断开连接并重新连接,客户端应该会发现自己重新连接到正确的服务器,因为 DNS 已经改变......所以你需要的是告诉旧服务器它应该向每个连接的客户端发送一条消息,建议它断开连接并重新连接。这样就不能解决问题吗?
  • 是的,我同意,如果我断开/重新连接,DNS 将解析 WebSocket 连接。虽然当客户端应用程序没有检测到 DNS 更改时(即在这种情况下不会自行断开连接)。您认为可以从客户端应用程序端而不是服务器端做到这一点吗?
  • 问题是客户不知道去尝试。您需要服务器断开连接或发送某种通知,表明它将停止提供消息。

标签: sockets websocket amazon-elb


【解决方案1】:

您是否考虑过使用 Pub/Sub 服务器/数据库,例如 Redis?

这将以允许 Websocket 连接完全独立于 HTTP 连接的方式增强架构,因此可以将一台服务器上的事件推送到另一台服务器上的 websocket 连接。

这是一种非常常见的水平扩展网络设计,使用 Redis 或 MongoDB 应该很容易实现。

另一种方法(我发现效率较低但可以为特定数据库和设计提供扩展优势)是让每个服务器“轮询”数据库(“订阅”数据库更改),允许服务器模拟发布/订阅订阅并将数据推送到连接的客户端。

迄今为止最复杂的第三种方法是实现“gossip”协议和内部发布/订阅服务。

如您所见,所有三种方法都有一个共同点 - 它们从不假设 HTTP 连接和 Websocket 连接被路由到同一个服务器。

EDIT(使用 redis 的简单示例):

使用 Ruby 和 iodine HTTP/Websocket 服务器,这是一个使用第一种方法将事件推送到客户端(用于 Pub/Sub 的通用 Redis 数据库)的应用程序的快速示例。

请注意,无论哪个服务器发起事件,事件都会被推送到等待的客户端。

该应用程序非常简单,使用单个事件“系列”(发布/订阅频道称为“聊天”),但使用多个频道过滤事件很容易,例如每个用户的频道或每个资源的频道(即博客文章等')。

客户端也可以监听多种事件类型(订阅多个频道)或使用全局匹配来订阅所有(现有和未来)匹配的频道。

将以下内容保存到config.ru

require 'uri'
require 'iodine'
# initialize the Redis engine for each Iodine process.
if ENV["REDIS_URL"]
  uri = URI(ENV["REDIS_URL"])
  Iodine.default_pubsub = Iodine::PubSub::RedisEngine.new(uri.host, uri.port, 0, uri.password)
else
  puts "* No Redis, it's okay, pub/sub will support the process cluster."
end

# A simple router - Checks for Websocket Upgrade and answers HTTP.
module MyHTTPRouter
  # This is the HTTP response object according to the Rack specification.
  HTTP_RESPONSE = [200, { 'Content-Type' => 'text/html',
          'Content-Length' => '32' },
   ['Please connect using websockets.']]

   WS_RESPONSE = [0, {}, []]

   # this is function will be called by the Rack server (iodine) for every request.
   def self.call env
     # check if this is an upgrade request.
     if(env['upgrade.websocket?'.freeze])
       env['upgrade.websocket'.freeze] = WS_RedisPubSub.new(env['PATH_INFO'] && env['PATH_INFO'].length > 1 ? env['PATH_INFO'][1..-1] : "guest")
       return WS_RESPONSE
     end
     # simply return the RESPONSE object, no matter what request was received.
     HTTP_RESPONSE
   end
end

# A simple Websocket Callback Object.
class WS_RedisPubSub
  def initialize name
    @name = name
  end
  # seng a message to new clients.
  def on_open
    subscribe channel: "chat"
    # let everyone know we arrived
    # publish channel: "chat", message: "#{@name} entered the chat."
  end
  # send a message, letting the client know the server is suggunt down.
  def on_shutdown
    write "Server shutting down. Goodbye."
  end
  # perform the echo
  def on_message data
    publish channel: "chat", message: "#{@name}: #{data}"
  end
  def on_close
    # let everyone know we left
    publish channel: "chat", message: "#{@name} left the chat."
    # we don't need to unsubscribe, subscriptions are cleared automatically once the connection is closed.
  end
end

# this function call links our HelloWorld application with Rack
run MyHTTPRoute

确保您已安装 iodine gem (gem install ruby)。

确保你有一个 Redis 数据库服务器正在运行(在这个例子中我的运行在 localhost 上)。

从终端,在两个不同的端口上运行碘服务器的两个实例(使用两个终端窗口或添加& 来恶魔化进程):

$ REDIS_URL=redis://localhost:6379/ iodine -t 1 -p 3000 redis.ru
$ REDIS_URL=redis://localhost:6379/ iodine -t 1 -p 3030 redis.ru

在此示例中,我运行两个独立的服务器进程,使用端口 30003030

从两个浏览器窗口连接到两个端口。例如(一个快速的 javascript 客户端):

// run 1st client app on port 3000.
ws = new WebSocket("ws://localhost:3000/Mitchel");
ws.onmessage = function(e) { console.log(e.data); };
ws.onclose = function(e) { console.log("closed"); };
ws.onopen = function(e) { e.target.send("Yo!"); };

// run 2nd client app on port 3030 and a different browser tab.
ws = new WebSocket("ws://localhost:3000/Jane"); 
ws.onmessage = function(e) { console.log(e.data); };
ws.onclose = function(e) { console.log("closed"); };
ws.onopen = function(e) { e.target.send("Yo!"); };

请注意,事件被推送到两个 websocket,而无需担心事件的来源。

如果我们不定义 REDIS_URL 环境变量,应用程序将不会使用 Redis 数据库(它将使用 iodine 的内部引擎)并且任何事件的范围都将被限制在单个服务器(单个端口)。

您还可以关闭 Redis 数据库并注意事件是如何暂停/延迟的,直到 Redis 服务器重新启动(在这些实例中,当不同的服务器重新连接时,某些事件可能会丢失,但我想网络故障处理是我们必须要做的事情决定一种或另一种方式)...

请注意,我是 iodine 的作者,但这种架构方法不是 Ruby 或 iodine 特定的 - 它是解决水平缩放问题的一种非常常见的方法。

【讨论】:

  • mongobd的使用方法是什么?
  • @MHJ - 我不确定。我使用 Redis。但是,我知道 MongoDB 也提供 pub/sub 服务(我只是没有使用它们)。
猜你喜欢
  • 2013-04-26
  • 1970-01-01
  • 1970-01-01
  • 2015-04-06
  • 2017-05-15
  • 1970-01-01
  • 2019-03-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多