MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

WebSocket连接管理与负载均衡策略

2022-03-241.2k 阅读

WebSocket 基础概述

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。在传统的 HTTP 协议中,通信通常是由客户端发起请求,服务器响应,这种模式在需要实时通信的场景下存在局限性,例如实时聊天、在线游戏、股票行情推送等。而 WebSocket 协议解决了这个问题,它允许服务器主动向客户端推送数据,实现了真正意义上的双向通信。

WebSocket 协议通过 HTTP 协议进行握手,使用 Upgrade 头字段将协议从 HTTP 升级到 WebSocket。例如,客户端发送如下请求:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Key: dGhlIHNhbXBsZSBub25jZQ==
Sec - WebSocket - Version: 13

服务器响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

一旦握手成功,后续的数据传输就不再使用 HTTP 协议,而是基于 TCP 连接进行高效的双向数据交换。

WebSocket 连接管理

连接的建立与监听

在后端开发中,使用不同的编程语言和框架来建立 WebSocket 连接。以 Python 的 Tornado 框架为例,以下是一个简单的 WebSocket 服务器示例:

import tornado.ioloop
import tornado.web
import tornado.websocket


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        print('WebSocket 连接已建立')

    def on_message(self, message):
        self.write_message('你发送的消息是: {}'.format(message))

    def on_close(self):
        print('WebSocket 连接已关闭')


def make_app():
    return tornado.web.Application([
        (r"/ws", WebSocketHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

在这个示例中,WebSocketHandler 类继承自 tornado.websocket.WebSocketHandler,重写了 openon_messageon_close 方法。open 方法在 WebSocket 连接建立时被调用,on_message 方法在接收到客户端消息时被调用,on_close 方法在连接关闭时被调用。

连接的存储与管理

当有多个 WebSocket 连接时,需要对这些连接进行有效的存储和管理。常见的做法是使用一个全局的集合来存储所有的连接实例。例如,在上述 Python 示例基础上,可以这样管理连接:

import tornado.ioloop
import tornado.web
import tornado.websocket

connections = set()


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        connections.add(self)
        print('WebSocket 连接已建立,当前连接数: {}'.format(len(connections)))

    def on_message(self, message):
        for conn in connections:
            conn.write_message('用户发送的消息: {}'.format(message))

    def on_close(self):
        connections.remove(self)
        print('WebSocket 连接已关闭,当前连接数: {}'.format(len(connections)))


def make_app():
    return tornado.web.Application([
        (r"/ws", WebSocketHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

在这个改进版本中,connections 集合存储了所有的 WebSocket 连接实例。在 open 方法中,将新连接添加到集合中,在 on_close 方法中,从集合中移除连接。这样就可以方便地对所有连接进行广播等操作。

连接的心跳检测

为了确保 WebSocket 连接的有效性,防止出现网络故障导致的连接假死,需要引入心跳检测机制。心跳检测通常是客户端和服务器之间定期发送一个简单的消息,以确认对方仍然在线。

以 JavaScript 客户端和 Python 服务器为例,JavaScript 客户端代码如下:

const socket = new WebSocket('ws://localhost:8888/ws');
let heartbeatInterval;

function sendHeartbeat() {
    socket.send('heartbeat');
}

socket.onopen = function () {
    heartbeatInterval = setInterval(sendHeartbeat, 5000);
};

socket.onmessage = function (event) {
    if (event.data === 'pong') {
        console.log('收到服务器心跳响应');
    }
};

socket.onclose = function () {
    clearInterval(heartbeatInterval);
    console.log('WebSocket 连接已关闭');
};

Python 服务器端代码修改如下:

import tornado.ioloop
import tornado.web
import tornado.websocket

connections = set()


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        connections.add(self)
        print('WebSocket 连接已建立,当前连接数: {}'.format(len(connections)))
        self.heartbeat_timeout = tornado.ioloop.IOLoop.current().call_later(10, self.check_heartbeat)

    def on_message(self, message):
        if message === 'heartbeat':
            self.write_message('pong')
            self.heartbeat_timeout = tornado.ioloop.IOLoop.current().call_later(10, self.check_heartbeat)
        else:
            for conn in connections:
                conn.write_message('用户发送的消息: {}'.format(message))

    def check_heartbeat(self):
        self.close()
        print('心跳检测超时,关闭连接')

    def on_close(self):
        connections.remove(self)
        tornado.ioloop.IOLoop.current().remove_timeout(self.heartbeat_timeout)
        print('WebSocket 连接已关闭,当前连接数: {}'.format(len(connections)))


def make_app():
    return tornado.web.Application([
        (r"/ws", WebSocketHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

在服务器端,open 方法中设置了一个 10 秒的心跳检测定时器 self.heartbeat_timeout。当接收到客户端的心跳消息 heartbeat 时,回复 pong 并重置定时器。如果定时器触发,说明心跳检测超时,关闭连接。

WebSocket 负载均衡策略

负载均衡的必要性

随着用户数量的增加,单个 WebSocket 服务器可能无法处理所有的连接和消息。这时就需要引入负载均衡,将请求均匀分配到多个服务器上,以提高系统的性能、可用性和扩展性。负载均衡可以帮助避免单个服务器因过载而导致的性能下降甚至崩溃,确保每个服务器都能充分利用其资源。

常见的负载均衡算法

  1. 轮询算法:轮询算法是最简单的负载均衡算法之一。它按照顺序依次将请求分配到各个服务器上。例如,假设有三个服务器 server1server2server3,第一个请求被分配到 server1,第二个请求被分配到 server2,第三个请求被分配到 server3,第四个请求又回到 server1,以此类推。

在代码实现上,可以使用一个简单的计数器来实现轮询算法。以下是一个 Python 示例:

servers = ['server1', 'server2','server3']
counter = 0


def round_robin():
    global counter
    server = servers[counter % len(servers)]
    counter += 1
    return server
  1. 加权轮询算法:加权轮询算法是在轮询算法的基础上,为每个服务器分配一个权重。权重越高的服务器,被分配到请求的概率越大。这适用于不同服务器性能不同的场景,性能好的服务器可以分配更高的权重,从而处理更多的请求。

假设服务器 server1 的权重为 2,server2 的权重为 1,server3 的权重为 1。可以这样实现加权轮询算法:

servers = [
    {'name':'server1', 'weight': 2},
    {'name':'server2', 'weight': 1},
    {'name':'server3', 'weight': 1}
]

total_weight = sum(server['weight'] for server in servers)
current_weight = 0
index = 0


def weighted_round_robin():
    global current_weight, index
    while True:
        server = servers[index]
        current_weight += server['weight']
        if current_weight >= total_weight:
            current_weight = 0
            index = (index + 1) % len(servers)
            return server['name']
        index = (index + 1) % len(servers)


  1. 最少连接算法:最少连接算法会将新的请求分配到当前连接数最少的服务器上。这种算法基于一个假设,即连接数少的服务器处理能力相对更空闲,能够更好地处理新的请求。

要实现最少连接算法,需要实时跟踪每个服务器的连接数。以下是一个简单的示例:

servers = {
  'server1': 0,
  'server2': 0,
  'server3': 0
}


def least_connections():
    min_connections = min(servers.values())
    for server, connections in servers.items():
        if connections == min_connections:
            servers[server] += 1
            return server


  1. IP 哈希算法:IP 哈希算法根据客户端的 IP 地址来分配请求。它通过对客户端 IP 地址进行哈希运算,然后将结果映射到服务器列表中的某一个服务器上。这样可以保证同一个客户端的所有请求都被分配到同一台服务器上,适用于需要保持会话状态的场景,例如用户登录后需要在同一服务器上进行后续操作。

以下是一个简单的 IP 哈希算法实现:

servers = ['server1', 'server2','server3']


def ip_hash(client_ip):
    hash_value = hash(client_ip)
    server_index = hash_value % len(servers)
    return servers[server_index]


基于 Nginx 的 WebSocket 负载均衡配置

Nginx 是一款常用的高性能 Web 服务器和反向代理服务器,它可以很方便地实现 WebSocket 负载均衡。以下是一个简单的 Nginx 配置示例:

upstream websocket_backends {
    server backend1.example.com:8080;
    server backend2.example.com:8080;
    server backend3.example.com:8080;
    # 使用轮询算法,这是 Nginx 的默认算法
    # 如果要使用加权轮询,可以这样配置:
    # server backend1.example.com:8080 weight=2;
    # server backend2.example.com:8080 weight=1;
    # server backend3.example.com:8080 weight=1;
}

server {
    listen 80;
    server_name example.com;

    location /ws {
        proxy_pass http://websocket_backends;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }
}

在这个配置中,upstream 块定义了后端服务器池,server 块配置了 Nginx 监听 80 端口,并将 /ws 路径的请求代理到后端服务器池。proxy_set_header 指令用于设置 WebSocket 协议升级所需的头字段。

负载均衡中的会话保持

在某些场景下,需要保证同一个客户端的 WebSocket 连接始终被分配到同一台服务器上,这就是会话保持。例如,在一个在线游戏中,玩家的操作需要在同一服务器上进行处理,以确保游戏状态的一致性。

  1. 基于 Cookie 的会话保持:可以通过在客户端设置一个特殊的 Cookie,记录服务器的标识。Nginx 可以根据这个 Cookie 来将请求始终发送到同一个服务器。配置如下:
upstream websocket_backends {
    server backend1.example.com:8080;
    server backend2.example.com:8080;
    server backend3.example.com:8080;
    sticky cookie websocket_server_id expires=1h domain=example.com path=/;
}

server {
    listen 80;
    server_name example.com;

    location /ws {
        proxy_pass http://websocket_backends;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }
}

在这个配置中,sticky cookie 指令设置了基于 Cookie 的会话保持,websocket_server_id 是 Cookie 的名称,expires=1h 表示 Cookie 的有效期为 1 小时。

  1. 基于 IP 哈希的会话保持:如前文所述,IP 哈希算法本身就可以实现会话保持,因为它根据客户端 IP 地址分配请求,同一个客户端的 IP 地址不变时,请求始终会被分配到同一台服务器。在 Nginx 中配置 IP 哈希如下:
upstream websocket_backends {
    server backend1.example.com:8080;
    server backend2.example.com:8080;
    server backend3.example.com:8080;
    ip_hash;
}

server {
    listen 80;
    server_name example.com;

    location /ws {
        proxy_pass http://websocket_backends;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }
}

通过 ip_hash 指令,Nginx 会根据客户端 IP 地址进行负载均衡,从而实现会话保持。

WebSocket 连接管理与负载均衡的结合

在实际应用中,需要将 WebSocket 连接管理与负载均衡策略有机结合起来。一方面,通过负载均衡将连接均匀分配到多个服务器上,以提高整体性能;另一方面,每个服务器需要对自己管理的连接进行有效的维护,包括连接的建立、存储、心跳检测等。

例如,在基于 Nginx 负载均衡的 WebSocket 系统中,每个后端服务器可以使用类似前文所述的 Python 代码来管理自己的 WebSocket 连接。同时,Nginx 负责将客户端的连接请求按照指定的负载均衡算法分配到各个后端服务器。

在实现过程中,还需要注意一些问题。例如,当使用会话保持策略时,后端服务器在处理连接关闭等操作时,需要确保会话状态的一致性。如果一个客户端的连接在某台服务器上关闭,相关的会话信息需要进行妥善处理,避免影响后续可能的重连。

另外,在大规模部署中,可能会存在多个负载均衡器,形成多级负载均衡架构。这种情况下,需要确保各级负载均衡器之间的协同工作,以及对 WebSocket 连接管理的一致性。例如,上层负载均衡器将请求分配到下层负载均衡器,下层负载均衡器再将请求分配到后端服务器,整个过程中都需要考虑会话保持、连接状态同步等问题。

性能优化与监控

性能优化

  1. 优化网络配置:调整服务器的网络参数,如 TCP 缓冲区大小、TCP 连接超时时间等,可以提高 WebSocket 连接的性能。例如,在 Linux 系统中,可以通过修改 /etc/sysctl.conf 文件来调整 TCP 相关参数:
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_fin_timeout = 30

然后执行 sysctl -p 使配置生效。增大 TCP 缓冲区可以提高数据传输效率,适当缩短 TCP 连接超时时间可以及时释放资源。

  1. 优化代码实现:在 WebSocket 服务器代码中,避免在处理消息时进行长时间的阻塞操作。例如,如果需要进行数据库查询等耗时操作,可以使用异步编程技术。以 Python 的 asyncio 库为例,对前文的 WebSocket 服务器代码进行优化:
import tornado.ioloop
import tornado.web
import tornado.websocket
import asyncio

connections = set()


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    async def open(self):
        connections.add(self)
        print('WebSocket 连接已建立,当前连接数: {}'.format(len(connections)))

    async def on_message(self, message):
        await asyncio.sleep(0)  # 模拟非阻塞操作
        for conn in connections:
            conn.write_message('用户发送的消息: {}'.format(message))

    async def on_close(self):
        connections.remove(self)
        print('WebSocket 连接已关闭,当前连接数: {}'.format(len(connections)))


def make_app():
    return tornado.web.Application([
        (r"/ws", WebSocketHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

在这个优化版本中,openon_messageon_close 方法都被定义为异步方法,通过 asyncio.sleep(0) 模拟非阻塞操作,确保在处理消息时不会阻塞事件循环,提高服务器的并发处理能力。

监控指标

  1. 连接数:实时监控 WebSocket 服务器的连接数,可以了解服务器的负载情况。可以通过在服务器代码中添加计数器,并通过监控工具(如 Prometheus + Grafana)进行展示。例如,在 Python 代码中添加如下统计逻辑:
import tornado.ioloop
import tornado.web
import tornado.websocket

connections = set()
connection_count = 0


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        global connection_count
        connections.add(self)
        connection_count += 1
        print('WebSocket 连接已建立,当前连接数: {}'.format(connection_count))

    def on_message(self, message):
        for conn in connections:
            conn.write_message('用户发送的消息: {}'.format(message))

    def on_close(self):
        global connection_count
        connections.remove(self)
        connection_count -= 1
        print('WebSocket 连接已关闭,当前连接数: {}'.format(connection_count))


def make_app():
    return tornado.web.Application([
        (r"/ws", WebSocketHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

然后通过 Prometheus 的 Python 客户端库将 connection_count 暴露为监控指标,在 Grafana 中进行可视化展示。

  1. 消息吞吐量:监控 WebSocket 服务器每秒处理的消息数量,可以评估服务器的处理能力。同样可以在代码中添加计数器:
import tornado.ioloop
import tornado.web
import tornado.websocket

connections = set()
message_count = 0


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        connections.add(self)
        print('WebSocket 连接已建立')

    def on_message(self, message):
        global message_count
        message_count += 1
        for conn in connections:
            conn.write_message('用户发送的消息: {}'.format(message))

    def on_close(self):
        connections.remove(self)
        print('WebSocket 连接已关闭')


def make_app():
    return tornado.web.Application([
        (r"/ws", WebSocketHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

通过类似的方式将 message_count 暴露为监控指标,在 Grafana 中展示每秒的消息吞吐量。

  1. 心跳检测成功率:监控心跳检测的成功率,可以反映网络的稳定性和连接的健康状况。在心跳检测代码中添加成功率统计逻辑:
import tornado.ioloop
import tornado.web
import tornado.websocket

connections = set()
heartbeat_success_count = 0
heartbeat_total_count = 0


class WebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        connections.add(self)
        print('WebSocket 连接已建立,当前连接数: {}'.format(len(connections)))
        self.heartbeat_timeout = tornado.ioloop.IOLoop.current().call_later(10, self.check_heartbeat)

    def on_message(self, message):
        global heartbeat_success_count, heartbeat_total_count
        if message === 'heartbeat':
            self.write_message('pong')
            self.heartbeat_timeout = tornado.ioloop.IOLoop.current().call_later(10, self.check_heartbeat)
            heartbeat_total_count += 1
            heartbeat_success_count += 1
        else:
            for conn in connections:
                conn.write_message('用户发送的消息: {}'.format(message))

    def check_heartbeat(self):
        self.close()
        print('心跳检测超时,关闭连接')
        heartbeat_total_count += 1

    def on_close(self):
        connections.remove(self)
        tornado.ioloop.IOLoop.current().remove_timeout(self.heartbeat_timeout)
        print('WebSocket 连接已关闭,当前连接数: {}'.format(len(connections)))


def make_app():
    return tornado.web.Application([
        (r"/ws", WebSocketHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

heartbeat_success_countheartbeat_total_count 暴露为监控指标,在 Grafana 中计算并展示心跳检测成功率。

通过对这些性能优化和监控指标的关注,可以不断提升 WebSocket 系统的性能和稳定性,确保在高并发场景下能够提供良好的服务质量。同时,根据监控数据可以及时发现潜在的问题,进行针对性的优化和调整。在实际应用中,还可以结合其他更多的监控指标和优化手段,如服务器资源利用率(CPU、内存等)、响应时间等,以构建一个健壮的 WebSocket 后端服务。