分布式WebSocket-下篇

本文讲述了单机WebSocket服务在高并发直播场景下遇到的问题,包括内存溢出和宕机。作者分析了问题原因,并提出了两种分布式WebSocket的实现思路:Redis发布/订阅和RabbitMQ广播模式。最终选择了RabbitMQ,详细介绍了集成步骤和运行中应注意的问题,强调消息中间件的稳定性、消息持久化和幂等性设计。
摘要由CSDN通过智能技术生成

分布式WebSocket落地-生产验证


上篇文章主要讲述了单点Socket,以及它的使用场景。本章主要会从多个维度来探讨单机Socket存在问题以及解决方案。
上篇文章从功能层面实现了双向传输,但是带来了 难受问题如下:

  1. 我们把应用部署在一台2C4G服务器上运行,
    jvm参数如下:-Xmx2688M -Xms2688M -Xmn960M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M,上线运行一周左右时间,应用自动挂了,应用日志没有任何报错。由于没有开启gc-log日志、dump文件输出等,感觉内存不够用导致。当时的处理方式是先开启gc-log、dump文件等配置,把应用通过docker-run,结果后续几天内应用又挂了,通过gc-log分析发现,应用内存这块没有达到瓶颈,**那为什么应用会自动挂呢?**通过打印日志,增加jvm堆栈异常详细信息打印方式,持续观察分析后,得出结论,docker自动把应用kill导致。由于我们应用用docker-run,docker本身也会需要消耗一部分服务器内存资源,由于我们应用占掉大部分服务器的内存资源,分配给docker资源有限,当应用消耗较大资源时,docker内部保护机制触发后导致,后续我们把应用的jvm参数调整至-Xmx2048M -Xms2048M -Xmn700M -XX:MaxMetaspaceSize=256M -XX:MetaspaceSize=256M
    运行后续再无此问题。由于单节点,宕机后业务影响非常大,这个期间很多分析、启动调试工作都是半夜凌晨处理。
  2. 运行期间内存使用率从30%上升到90%,后续内存迟迟没有下降,感觉没有触发GC,导致没有垃圾回收。通过jmap在线分析dump文件等,发现应用程序中用了一个全局的Map对象存储用户Socket信息,没有及时进行销毁导致一直叠加。由于单机只能等到夜晚流量低谷才能重启,期间升过配置,由于没使用K8S,应用升配都需要重启,但往往业务需运转,几乎没有停机窗口。
  3. 类似双11,直播业务也有。某天收到消息,xxx时间节点有多场大型直播(上千人),没有压测报告,凭着应用在生产上(2C/4G)运行了近月的监控分析,只能拍脑袋翻一番,早早提前一天把服务器配置升到4C/8G。到了开播的时候,很多学生进不了直播间。此时服务器CPU/内存1分钟内全部打满,然后应用出现了假死,到后宕机等,通过日志看到有400-500学生开播一瞬间蜂拥而至,预计有1200-1500学生,近3倍,马上同运维童鞋沟通通过阿里云升配到16C/32G,jvm调整至-Xmx21824M -Xms21824M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M -XX:+UseG1GC,开启了G1进行垃圾回收,然后重启应用。花费了近5-10分钟,期间大领导纷纷走了过来,压力巨大。后续达到了1200左右的量,在预期的范围内。服务器CPU使用率均在15-23%左右,内存均在20-39%左右。到了所有直播结束后,傍晚升配到了16C/64G,因为这次事故、都非常害怕它再出任何问题。

总结:不建议采用单点Socket,存在的问题以及引发的问题都在,如果你从长远的规划已经分析过项目的发展,确实简单量少,或者试运行简单聊天等,可以尝试。

直播的并发有两个维度,多个直播同时运行的并发、多个直播间的学生量并发。随着并发量上升,加快了我们分布式Socket的落地。当时是想把Socket单独抽象出来,和业务代码解耦,达到分布式Socket,但它的痛点在于Socket不支持序列化,它内部有个核心推送消息接口,void sendText(String text),故不能缓存。当时想到了两种实现思路如下:

  • 通过Redis的发布/订阅模式去实现,用户建立链接后推送消息给到频道,多台服务器同时订阅这个频道消息,收到消息后去本地Map中获取Socket,然后推送消息给客户端,即达到分布式结构。
    redis
  • 通过消息中间件-广播模式去实现,如:rabbitMq的fanout模式,每台服务器启动时,根据本地的ip生成queue,根据exchange交换机去动态绑定,用户建立链接后广播消息给到交换机中所有的queue,并记录到用户是通过哪台服务器进来的关系存储到Redis中,多台服务器同时订阅这个queue消息,当收到消息时,获取用户和服务器ip的关系,然后找到对应的服务器,并从服务器本地Map中获取Socket,然后推送消息给客户端,即达到分布式结构。
    queue

方案1的优点:实现较为简单,能快速集成,后续redis也可以做分布式集群方案,支持横向扩容。
方案1的缺点:采用redis的发布订阅模式,这种方式消息不能持久化,宕机后消息会丢失,不支持重发。

方案2的优点:消息能持久化、并支持重发、吞吐能力强。
方案2的缺点:集成相对复杂,并且需要做全局幂等,额外需处理多种异常情况,成本相对较高。

我们预估未来的可能性,选择方案2通过Rabbitmq来实现。

时序图

应用集成步骤如下:

Client:


<script type="text/javascript">
    var websocket = null;
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
   
        //23是一个ID ,chat是要订阅的话题
        websocket = new WebSocket("ws://localhost:8091/demo/websocket/345/THE/452784/20201210162113114000eaf319eaeef4/pc");

    } else {
   
        alert('当前浏览器 Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
   
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    //连接成功建立的回调方法
    websocket.onopen = function () {
   
        setMessageInnerHTML("WebSocket连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
   
        console.log(event.data)
        setMessageInnerHTML(event.data );
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
   
        setMessageInnerHTML("WebSocket连接关闭的回调方法,后台已经关闭了这个连接");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
   
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
   
        console.log(innerHTML)
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
   
        websocket.close();
        setMessageInnerHTML("WebSocket连接关闭");
    }


    //open WebSocket连接
    function openWebSocket() {
   
        if (websocket.readyState == 1 || websocket.readyState == 0) {
   
            closeWebSocket();
            console.log("如果已经存在,先给他关闭")
            setMessageInnerHTML("当前连接没有断开,接下来我们会给他断开,然后重新打开一个");
        }
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
   
            //不存在而且浏览器支持,重新打开连接
            websocket = new WebSocket("ws://localhost:8091/demo/websocket/345/THE/452784/20201210162113114000eaf319eaeef4/pc");
            setMessageInnerHTML("已经重新打开了");
        } else {
   
            alert('当前浏览器 Not support websocket')
        }
    }

    //发送消息
    function send() {
   
        var message = document.getElementById('text').value;
        websocket.send(message);
    }

Server:

1.引入spring-websocket包,版本可以跟随springboot框架版本号

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.0.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>2.0.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.0.6.RELEASE</version>
        </dependency>

2.配置WebSocketConfig

@Configuration
public class WebSocketConfig /*extends ServerEndpointConfig.Configurator*/ {
   
    
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
   
        return new ServerEndpointExporter();
    }

}

3.配置ServerConfig

@Component
@Slf4j
public class ServerConfig {
   
    //服务器ip
    @Value("${SERVER.HOST}")
    private String host;
	//应用端口
    @Value("${server.port}")
    private String port;

    /**
     * 获取ip地址
     * 如:127.0.0.1:8080
     * @return
     */
    public String getUrl() {
   
        return host+":"+port;
    }
}

4.配置MqConfig

/**
 * @Description: mq-Exchange配置类
 * @Author: zachary
 */

@Configuration
@Slf4j
public class MqConfig {
   


    /**
     * 配置信息
     */
    @Aut
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
分布式 Websocket 是指在集群环境下,实现多台机器之间共享 Websocket 连接和消息推送的方案。在单机情况下,由于用户已经与 Websocket 服务建立连接,消息推送是可以成功的。但在集群环境下,用户与 Websocket 服务建立连接的服务可能与需要给用户推送消息的服务不一致,这就需要解决分布式环境下的 Websocket 连接共享问题。 针对分布式 Websocket 的解决方案,可以考虑以下几种思路: 1. 将 Websocket Session 序列化并存储到 Redis,实现数据共享。在 Spring 集成的 Websocket 中,每个 WS 连接都有一个对应的 Session,称为 WebSocketSession。但是,由于 WS Session 无法直接序列化到 Redis,无法将所有 WebSocketSession 缓存到 Redis 进行 Session 共享。 2. 使用中间件或消息队列来实现分布式消息推送。可以使用诸如 RabbitMQ、Kafka 等消息队列服务,将需要推送的消息发送到消息队列,然后由各个 Websocket 服务订阅相应的消息队列,实现消息的分发和推送。 3. 使用负载均衡器和会话粘性(session affinity)来保证用户的 Websocket 连接始终与同一台服务器保持连接。负载均衡器负责将用户的请求分发到不同的服务器上,而会话粘性则会保证用户的后续请求都会路由到与其最初连接的服务器上,从而保持连接的连贯性。 在实现分布式 Websocket 的过程中,需要根据具体的应用场景和需求选择适合的方案,并结合实际情况进行实现和调优。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值