网络|基于Netty构建的高性能车辆网项目实现(三)

Netty负载均衡方案

       面临的问题

  • 如何解决车载终端接入增多问题,如何解决海量的设备接入与实时性的要求
  • 后端服务如何高可用高性能
  • 如何记录车载终端和后台服务器的会话信息

       技术解决方案

  1. 车载终端会随着公交车的数量的增长而增长,尤其是公交车等公共交通工具,这个数量将是海量的,我们需要同时处理众多的车载终端的连接,然而每个机器的句柄(Linux环境下叫文件描述符(fd),是操作系统内核描述文件地址,数据区等一些属性的数字)是有上限的,为此需要增加服务器的数量,才能应对海量的接入,需要做一个集群方案。同时为了降低数据的时延,我们需要借用Kafka(其他消息中间件的选型我们这里不探讨,主要考虑到该中间件的选择需要更好的和大数据模块整合,同时也需要高吞吐,低延迟,社区活跃的中间件)做异步处理,并加入线程池等更多异步处理的手段,来提高系统处理数据包的效率。
  2. 后端服务需要负载均衡来降低服务器的压力,结合Netty自带的监听channel管道变化的方法channelInactive和channelActive,统计每台服务器的连接数并set入Redis中,每次登录成功后,只需要返回最小连接数的那台服务器ip和端口给车载终端连接,这样就可解决海量接入问题。
  3. 设计基本的登录手段,根据协议现有的平台登入、车辆登入、车辆登出过程,设计登录登出,参考Session的设计方案,需要加入sessionID来记录会话信息,准确应答。

       车载终端登录

       平台登入:首先系统初始化的时候,需要加载一些初始化信息,包含车载终端的登录账户、公交车的车体信息、车牌、vin码等信息。其次车载终端上电启动的时候,需要去admin管理后台获取配置,配置内容包含加密的账号密码,波特率,发送间隔等配置信息); 车载终端获取配置信息后,解密出平台登入账号密码,携带着账号密码发送报文,后端服务查询Redis,匹配成功(除了校验账号密码,还要校验是否有该台车的vin码,做唯一性校验)后,信任此设备,存入sessionId到Redis,并返回系统生成的sessionId给车载终端。

在这里插入图片描述

       车辆登入:车载终端接收到该sessionId后,携带该sessionId做车辆登入,服务器需要把车载终端的sessionId与Redis的sessionId比对,校验成功后,还需要放入到本地缓存(本地化缓存使用谷歌guava组件里的cache)。

在这里插入图片描述

       看到这里,读者可能有疑问,为什么需要平台登入与车辆登入两个过程,直接登入不行?其实,这也是受限GB32960协议,这是国标规定要有的过程,其次,为了减少网络原因导致车载终端需要重新登录,影响正常的数据上报,例如这样的场景:车载终端在正常上报数据的过程中,只需要校验本地缓存是否有该vin码和对应的sessionId,有则证明他已经经过了平台登入和车辆登入两个过程,而不需要从远程缓存中获取,提高数据上报的效率,同时也可以增加可靠性,在本地缓存没有找到sessionId的过程中,可以从远程缓存中获取。做两重保障。

       负载均衡实现方案

       从如下图的设计方案,我们可以看出,Redis类似一个注册中心,每次需要连接接入的时候,都会从redis拿到所有服务器当前连接数的大小,再降序排序,取最后一条服务器信息(连接数最少),再把服务器信息(ip,端口)组装成协议报文,返回给车载终端,车载终端获取到该报文,携带sessionID访问该服务器

在这里插入图片描述

       值得一提的是,上图引入的SLB(负载均衡器),这是防止海量接入导致某台服务器崩溃的防范措施,试想一下,若第一台接入的服务器都挂了,谈何取“最小连接数”?SLB可以和具体的云服务商(电信云,阿里云,腾讯云等)购买,或者有具体的硬件设备F5(很贵),SLB需要知道后台接入服务器的socket信息,以便轮训转发请求。

       具体代码:

       我们从平台登入的Handler中以最小连接数方法为例作为讲解:

/**
     * 获得连接数最小的服务器redis中的key值
     *
     * @return String
     */
    private String getMinServer() {
        //从redis中,获取所有集群服务器连接信息(ip和端口,连接数),存入的时候存入了平台登入时间
        Map<String, ConnectInfo> map = serverStateCache.getAllConnectNum();
        //所有服务器连接数的有序集合
        List<ConnectInfo> connectInfoBeanList = new ArrayList<>();
        for (ConnectInfo connectInfoBean : map.values()) {
            //毫秒差,当前时间减去平台登入时间,得到最近一次更新时间
            long timeDifference = System.currentTimeMillis() - connectInfoBean.getDate().getTime();
            //最近一次更新连接数时间小于三秒,更新超时
            if (timeDifference < MILLISECOND) {
                connectInfoBeanList.add(connectInfoBean);
            } else {
                //更新超时,删除该服务器
                serverStateCache.deleteServer(connectInfoBean.getAddress().replace(":", "-"));
            }
        }
        //若没有一个服务器可用,返回当前服务器信息,确保可用
        if (connectInfoBeanList.isEmpty()) {

            return environment.getProperty("netty.ip") + ":" + nettyConfig.getServerPort();
        }
        //connectInfoBean实现了Comparable接口,以连接数判断两个对象的大小关系,默认从小到大排序
        Collections.sort(connectInfoBeanList);
        return connectInfoBeanList.get(0).getAddress();
    }

       对连接数+1和-1操作方法都是同步的,本项目从激活通道开始,通道数+1,后续无论是车辆登入还是数据上报,只要断开连接,都视为-1,本文专门设置了一个WatchVehicleStateHandler类来监听管道断开操作

@LogDebug
@Slf4j
@ChannelHandler.Sharable
@Component
public class WatchVehicleStateHandler extends SimpleChannelInboundHandler<Object> {
    private final SessionCache sessionCache;
    private final  KafkaSender vehicleVinSender;
    private final ConnectService connectService;

    public WatchVehicleStateHandler(SessionCache sessionCache, KafkaSender vehicleVinSender, ConnectService connectService) {
        this.sessionCache = sessionCache;
        this.vehicleVinSender = vehicleVinSender;
        this.connectService = connectService;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

    }

    /**
     * 通道断开捕获
     *
     * @param ctx 通道上下文
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        //从通道中获取vin码,并往kafka下线车辆vin码
        Attribute<String> vinAttr = ctx.channel().attr(AttributeKey.valueOf(VehicleOnLineEnum.VEHICLE_VIN.getStrValue()));
        String vin = vinAttr.get();

        //获取为null,不发送
        if (vin!=null){
            VehicleOnLine vehicleOnLine = new VehicleOnLine();
            vehicleOnLine.setVin(vin)
                    .setConnectStatus(VehicleOnLineEnum.VEHICLE_OFF_LINE.getIntValue());
            vehicleVinSender.sendVehicleOnLine(vehicleOnLine);
        }
        //关闭通道,连接数-1
        connectService.subtractVehicleConnect();

        log.debug("VehicleLoginHandler channelInactive ...观察者通道监听-通道断开:"+vin);
        sessionCache.deleteVin(vin);
    }
}

       可以看出 ,该WatchVehicleStateHandler的泛型是Object,即在channelPipeline中,任何发包都要经过该handler,为了需要把当前服务器的连接数更新到redis,还需要定时任务job,我们设置的周期是每隔0.5s把当前服务器的连接数set入到redis中,此时读者可能有疑问,当集群的时候,每台服务器都有定时任务,又没有类似xxl-job分布式任务调度,怎么能不加分布式锁呢?不会有并发修改redis的情况嘛?其实这个我们也讨论过,确实在高并发环境下,同时启定时任务去做同一件事情,会有冲突,要么加入分布式调度,让他们错开时间执行,要么加入分布式锁,只允许持有分布式锁的服务器写入,但是我们仔细一看,我们存入redis的key是一个hash结构,一个redis的hash结构,分key和hashkey、value,虽然key一样,但hashkey是不一样的,也就是说我们取到的第一个大对象是一样的,但内部的不一样,那么大家都拿到这个大对象,但是各自写入的却不是同一个对象,就如下图的情况:

       其实加入分布式锁也更加安全,但是上锁和没上锁的性能可差太多了,不然cas无锁的概念是怎么诞生的呢?

车载终端升级方案

       对于车载终端设备我们不知道会在什么情况出现问题,因此需要对车载终端的嵌入式软件程序升级,但是基于业务的需要,我们可能有不同的车载终端硬件合作方,我们不能直接把车载终端的软件程序交由供应商升级,很危险,也不安全,一旦升级错误,后果不堪设想,为此我们借鉴灰度发布的思想,先局部测试升级效果,再推广所有设备,我们通过版本标识和vin码来精准控制每一台设备的升级,对于不同的合作方,我们后台需要有不同的权限。每次车载终端上电就先检查当前设备版本和服务器是否相同(平台登入之前),相同返回空的升级地址url,不相同的版本则触发一次升级,升级由合作方提前上传升级文件(bin文件),需要对该文件做唯一性校验(md5加密,文件校验过程不便探讨),以下是对该升级过程的一些简单说明:

在这里插入图片描述

总结

       通过该项目,加深了对netty的了解,也实现了基于netty的负载均衡方案,也感谢项目中同事教学,目前该项目单台服务器(8核16G,电信云服务器)做了简单的压力测试,由于测试机器有限,只测试到了单台5w的QPS。

参考资料

  • 《Netty权威指南》第二版 李林锋著
  • Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
  • http://ifeve.com//谈谈netty的线程模型
  • 计算机网络, 谢希仁
  • JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014.
  • W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006.
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值