[WebScoket服务推送优化过程] 网易实习项目

242 阅读7分钟

网易实习项目——数源平台

项目需求介绍: 在开发过程中有一个需求,用户提交请求之后,后端将用户请求内容从kafka保存中hive中,这个过程需要一定时长。想要当后端hive数据保存完毕后,用户可以马上得到响应,可以创建的新日志。

websocket 技术选型:

因为考虑用户可以立马得到更新的状态码,采用websocket实现

websocket实现方式常见的只有两种

1.基于tomcat实现websocket

2.基于spring实现websocket

两者导入的包是不相同的,具体实现方式有很大区别。这边有一个坑:使用tomcat实现websocket 网络很少有关于如何获取请求头部信息 比如cookie 等,而我的业务场景需要获取token 所以使用第二种方式。 第一种方法也有解决办法 不过我未尝试链接

第二个坑:使用两种方式实现都需要注意创建Spring中对象是单例的,而webscocket对象是只有连接的时候才实例化对象。并且多个连接就有多个对象,而@Autowired注解 是启动的时候就将对象注入。而不是在使用A对象的时候将A需要的B对象注入A中。所以你想注入Service是 得到的只能是null。解决方式有两种

分别是让service属于WebsocketServer这个类和运行时候动态从spring 容器中取出 Service .

第一种

	@Component
	@ServerEndpoint(value = "/ws/etlFlag/{flowLineId}")
    public class FlagWebsocketServer {
        @Autowired
        private void initLogSchemaService(LogSchemaServiceImpl logSchemaService) {
            FlagWebsocketServer.logSchemaService = logSchemaService;
        }
    }

第二种

@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtil.applicationContext = applicationContext;
    }
 
    public ApplicationContext getApplicationContext(){
        return applicationContext;
    }
 
    public static Object getBean(String beanName){
        return applicationContext.getBean(beanName);
    }
 
    public static <T> T getBean(Class<T> clazz){
        return (T)applicationContext.getBean(clazz);
    }
}


@ServerEndpoint(value = "/userWebsocket")
@Component
public class UserMessageSocketServer{
	
@Autowired
private RedisTemplate<String, String> redisTemplate = (RedisTemplate<String, String>) 		       			     SpringUtil.getBean("stringRedisTemplate");
}

第三个坑:

使用spring实现websocket 编写MyWebSocketHandler的时候需要添加@Component注解,虽然@Component默认是单例模式的,但Springboot还是会为每个WebSocket连接初始化一个Bean,所以可以用一个静态Map保存起来。换句话说,每当有一个用户向服务器发起连接时,都会创建一个WebSocketServer对象,将此对象按roomId保存在HashMap中,方便后续使用。

最终:

最后使用spring方式实现websocket。

实现版本迭代:

前提引入:对于每个特定flowLineId的请求确保只有一个用户访问

  1. 对于每个用户连接websocket都会创建一个websocketSession,对于每个连接获取到请求的参数(flowLineId)进行kafka到hive操作

  2. 对于1中特殊情况进行考虑 如用户开启了两个查询页面 会导致开启两个线程造成一定的资源消耗。于是修改为对于一个flowLineId创建一个HashMap保存<string flowLineId,Arraylist> 进行保存在子线程中遍历list集合在每个websocketsession 发消息。

  3. 创建线程以及回收线程需要大量资源,考虑使用线程池来实现 对比了四种线程池发现cacheThreadPool 理论上更加适合这个场景,后来深入了解了cacheThreaPool之后发现该线程池 只有非核心线程,最大线程数量很大,当池中没有空闲线程,他就会一直new新的线程,这样在并发量很大的时候就会导致内存耗尽。虽然自己的做的是内部系统没有这么大的用户量还是需要考虑特殊的情况。后来考虑使用FixedThreadPool来实现线程池,控制最大线程数量,超出的任务会在队列中等待。

  4. 按照上述方式进行操作之后发现当单个页面刷新的时候会照成同时开启两个线程(这边是比较绕)贴一段代码进行解释。

    线程执行代码

    public class SendLogStatusThread implements Runnable {
            String flowLineId = "";
            public SendLogStatusThread(String flowLineId) {
                this.flowLineId = flowLineId;
            }
            @Override
            public void run() {
                while (true) {
                    if (!SESSIONS.isEmpty()) {
                        try {
                            Thread.sleep(3000);
                            ArrayList<WebSocketSession> webSocketSessionArrayList = SESSIONS.get(flowLineId);
                            //关闭当前线程
                            if(webSocketSessionArrayList==null){
                                System.out.println("连接关闭");
                                threads.remove(flowLineId);
                                break;
                            }
                            Byte appLogSendStatus = 0;
                            int i = 0;
                            for (WebSocketSession session : webSocketSessionArrayList) {
                                //业务逻辑  
                                
                            }
                        }  catch (Exception e) {
                            logger.error("sendLogStatus failed. " + ExceptionUtils.getStackTrace(e));
                        }
                    }
                }
            }
        }
    

    webscoket连接代码

    @Override
        public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            logger.info("创建一个websocketsession连接");
            //一个flowlineId 对应一个线程执行
            ArrayList<WebSocketSession> webSocketSessionList = SESSIONS.get(flowLineId);
            Boolean ThreadOpend = true;
            if (webSocketSessionList == null) {
                webSocketSessionList = new ArrayList<>();
                ThreadOpend = false;
            }
            webSocketSessionList.add(session);
            SESSIONS.put(flowLineId, webSocketSessionList);
            //业务处理逻辑
            if (!ThreadOpend) {
                SendLogStatusThread sendLogStatusThread = new SendLogStatusThread(flowLineId);
                Thread t1 = new Thread(sendLogStatusThread);
                t1.setName("Thread" + count++);
                t1.start();
                logger.info(String.format("当前flowLineId:%s 创建线程成功 线程名字为%s", flowLineId, t1.getName()));
                //创建 flowLineId 和 线程一对一绑定
                threads.put(flowLineId,t1);
            }
            logger.info("发送信息成功");
        }
    

    websocket关闭代码

    @Override
        public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
            logger.info("关闭当前websocketSession连接");
            //逻辑修改为删除一个session 长度为0 删除当前flowlineId
            String flowLineID = session.getAttributes().get("flowLineId").toString();
            session.close();
            SESSIONS.get(flowLineID).remove(session);
            if (SESSIONS.get(flowLineID).size() == 0) {
                SESSIONS.remove(flowLineID);
            }
            logger.info("afterConnectionClosed  剩余 flowLineId个数" + SESSIONS.size());
        }
    

    简单介绍 当页面和后端建立连接后会自动执行 afterConnectionEstablished函数,当关闭页面会自动执行 afterConnectionClosed函数,这边函数调用是由spring进行封装的,无法修改。 出现两个线程的原因是刷新页面的时候会先执行 afterConnectionClosed函数对相对应的flowLineId进行移除,之后会很快调用 afterConnectionEstablished 函数创建一个连接,这时候会导致 afterConnectionEstablished中判断当前这个flowLineId 不在map中会创建一个线程(线程2) 线程1 由于有较大可能在sleep中,导致线程苏醒过来进行 调用 SESSIONS.get(flowLineId);的时候会拿到第二次连接创建的 list集合。导致两个线程同时给一个页面发送消息。

  5. 以上方式实现可能还会有额外的错误,而且线程池的大小如何设置跟业务量是有一定关系的,所以考虑改变实现方式,使用队列的方式,如果一个用户过来创建一个连接,那么就将它放入map集合中,使用一个线程单独去进行处理,轮训这个map的元素。采用单线程解决了多线程的开销,同时为了解决同步操作耗时的问题,造成多个服务之间会发生同步阻塞问题。

  6. 使用@Async 注解将 耗时业务进行提取出来 创建了另外一个请求接口进行处理,在线程中不执行业务操作,只执行结果查询,因为结果查询是通过数据库查询的,所以耗时可以忽略不计,将同步操作修改为异步操作。

  7. 拓展:异步操作将原先同步所需要的耗时减少到执行一次必要操作所需要的时间,对于有返回值的方法可以使用callable任务,返回值为Future对象(带返回值的异步任务) 如果想要再次优化时间,可以使用缓存并设置失效时间。

总结:

通过这一次websocket 开发,对于系统时间消耗考虑了很多 下面概括出一个比较通用的优化思路

耗时常见的因素:

  1. IO
  2. 网络
  3. 服务器性能
  4. 资源的释放和创建(线程,数据库连接)
  5. 转换:字符串到字节转换
  6. 算法时间复杂度高(多层for)
  7. 数据库查询没有走索引

思考角度:

  1. 将串行变成并行或者并发
  2. 同步操作修改为异步操作
  3. 多个请求合并为一个请求(减少带宽消耗)
  4. 空间换时间 加缓存
  5. 优化算法复杂度
  6. 提高机器的性能(CPU/内存/带宽/磁盘等)
  7. 利用池化的思想 如:数据库连接池、缓存连接池等
  8. 数据库索引优化

参考链接

明明如月学长

豪横的小耳朵