• 物联网海量设备心跳注册,脱网清除——多线程高并发互斥锁落地


    物联网海量设备心跳注册,脱网清除——多线程高并发互斥锁落地

    1.应用背景

    在物联网应用场景中,需要维护很多个设备的连接,比如基于TCP socket通信的长连接,目的是为了获取设备采集的信息,反向控制设备的数字开关或者模拟量。我们把这些TCP长连接都放入了基于线程安全的ConcurrentDictionary激活字典表中,IP地址作为key,设备箱领域模型作为value。我们需要把激活设备箱的字典表维护好,需要将超时没有心跳的设备,我们可以称之为脱网设备,给清理出激活字典表,写入到脱网告警字典表中去。当脱网设备下次再有心跳时,可以再次移入到激活字典表中,从而再产生恢复告警,进行一系列其他动作。

    2.整体框架

    2.1.心跳注册框架

    2.1.1.海量设备

    因为要模拟海量设备的TCP场景,我们利用模拟器生成了12000台模拟设备。8台真实设备。

    2.1.2.心跳上报Handler流程

    详细心跳上报流程详见上述框架图

    突然发现我可以写一个物联网的采集系统的系列了,组织一个目录。希望自己坚持下去吧。

    2.2.脱网清理框架

    2.2.1.激活字典表清理脱网设备方法

    原理很简单,遍历字典表中超过设置的检测周期,筛选到一个字典的IEnumerable中去,然后在激活字典表中删除对应超时key(这里就是指IP地址)即可。当然这里的_internal周期可以*N,多个周期,自行在配置文件中设置即可,配置文件如下:

     "ipboxNumStaticInternal": 12
    
        public static void DeleteDeadBoxFromActiveBox(in _internal)
        {
            {
                var outTime = DateTime.Now.AddSeconds(-_internal);
                var iboxTimeOutList = iboxActiveDictionary.Where(q => (outTime > q.Value.UpdateTime));//.Select(x=> iboxActiveDictionary[x.Key]) ;
                foreach (var item in iboxTimeOutList)
                {
                    iboxActiveDictionary.Remove(item.Key);
                }               
            }
        }
    

    2.2.2.脱网清理流程图

    这里主要开启了一个系统定时器,主动会去调用清理脱网设备方法,调用时间间隔即ipboxNumStaticInternal。代码如下:

        public void systemTimerStart()
        {
            var interval = ReadTheInternalFromSetting();
            _systemTimer = new Timer(state =>
            {               
                IBoxActiveDicManager.DeleteDeadBoxFromActiveBo(_internal);
                Console.WriteLine("{1},激活设备数量:{0}
    ",IBoxActiveDicManager.iboxActiveDictionary.Count,DateTime.Now);
            }, null, interval, interval);
            Console.WriteLine("PemsCom采集系统时钟已经开启");
            LoggerHelper.Info("PemsCom采集系统时钟已经开启");
        }
    
        /// <summary>
        /// 配置文件读入时间间隔方法
        /// </summary>
        /// <returns></returns>
        private int ReadTheInternalFromSetting()
        {
            _internal = int.Parse(Appsettings.app(new string[] {"ipboxNumStaticInternal" }));
            Console.WriteLine("PemsCom采集系统时钟配置参数已经读");
            LoggerHelper.Info("PemsCom采集系统时钟配置参数已经读");
            return Convert.ToInt32(TimeSpan.FromSecond(_internal).TotalMilliseconds);
        }
    
    

    3.多线程与高并发说明

    3.1.多线程说明

    这里会有很多的线程让CPU来轮片执行,比如:

    • 12008个Receive事件触发线程;
    • 定时清除脱网设备线程;
    • 主线程,监控命令行输入,并执行对应的命令;

    举个实际的例子,以图为证

    12008台设备,每秒处理接受网络包的峰峰值是9218个包,就是在某一秒,CPU共轮片执行了9218个线程。比如是双核4线程的,则9218/4=2304.5。即CPU在1秒轮片执行了2305次。即0.43毫秒就轮片执行一次。

    3.2.高并发说明

    其实3.1已经解释了高并发。在某一秒,需要处理的接收事件有接近1万件。而这一时刻的执行顺序是无序的,9218里的这么多线程,我们不知道哪个先执行,哪个后执行。如果不认为地加一些逻辑控制,比如我们今天要介绍的互斥锁,就会出现一些异常现象。

    4.多线程高并发造成的异常现象

    这里只描述现象,原因会在下面5.分析异常原因 做具体描述。

    4.1.空引用

    异常所在的位置:心跳处理类如下。

        public class HeartHandler
        {
            static string _deviceIndex = Appsettings.app(new string[] { "DeviceIndex" });
            private static IBoxActive iboxActive;
            public static void Register(TcpHeartPacket heartPacket,int sessId)
            {
                UInt32 IP;
                UInt64 mac;
                if (_deviceIndex == "IP")
                {
             
                    IP =(UInt32)BitConverter.ToUInt32(heartPacket.IP, 0);
                    if (IBoxActiveDicManager.GetBoxActive(IP, out iboxActive) != true)
                    {       
                        IBoxActiveDicManager.iboxActiveDictionary.TryAdd(IP, iboxActive);
                        iboxActive.SessID = sessId;
                    }
                   
                }
                else
                {
                     mac = (UInt64)BitConverter.ToUInt64(heartPacket.Mac, 0);
                    if (IBoxActiveDicManager.GetBoxActive(mac, out iboxActive) != true)
                    {
                        IBoxActiveDicManager.iboxActiveDictionary.TryAdd(mac, iboxActive);
                        iboxActive.SessID = sessId;
                    }
                }
    
                //引用类型,智能指针,使用方便
                iboxActive.UpdateTime = DateTime.Now;
    
               
            }
        }
    

    4.2.字典表里元素赋值不成功

            /// <summary>
            /// 查询激活设备箱字典中是否有存在上报的设备箱,
            /// 存在返回true,不存在返回false,并且新建好设备箱模型
            /// </summary>
            /// <param name="mac"></param>
            /// <param name="iboxActive"></param>
            /// <returns></returns>
            public static bool GetBoxActive(UInt32 IP, out IBoxActive iboxActive)
            {
     
                if (iboxActiveDictionary.TryGetValue(IP, outiboxActive))
                {
                    return true;
                }
                
                iboxActive = new IBoxActive();
               
                iboxActive.IP = IP;
    
                if (iboxActive.IP != IP)
                {
                    LoggerHelper.Error(string.Format("实例化赋值不成功.iboxActive.IP:{0};IP{1}", iboxActive.IP, IP));
                }
    
                return false;
            }
    

    有没有感觉很奇怪,上一句都赋值了,下一句对比就不相等。但是在多线程大并发里就是有这种可能,下面会详细分析。

    4.3.统计设备总数不正确

    因为12008台大并发时很容易出错,所以改成了1000台。如下统计数据会有出错情况,这同样也是因为多线程高并发引起的错误。

    5.分析异常原因

    5.1.造成空引用的原因

    其实第4的三点原因都是同一个原因造成,所以在5.1会详细阐述,5.2,,5.3只做简单阐述。这里敲下黑板,分析多线程高并发的异常问题,程序运行的特点就是见缝就插,就像个老司机一样,概括起来就是线程与线程之间的无序性。比如我们设备心跳线程正在更新设备心跳时间的时候。脱网清理线程就把该设备给清理掉了。如此一来,时间没法赋值给空对象(已被脱网线程给清理)。因此只能报空引用异常,对没错,就是这么简单,耗费了我很长时间去debug跟思考这个异常。

    5.2.设备IP赋值不成功原因

    同样,在创建了设备实例之后,IP赋值完成,刚好脱网清除设备线程运行清除了设备,当对比的时候,引用原来的地址,字典的原来地址已经存了其他设备箱的IP,所以IP地址不相等。

    5.3.统计设备总数不正确原因

    原因其实是5.2造成的,没法成功注册,当然数量就不对啦。

    6.解决思路

    就是当我在创建激活设备实例(第一次心跳注册)或者更新心跳时间的时候(非第一次注册),不要让无序的脱网清除线程运行。敲黑板:就是保证心跳处理注册过程的原子性。对,其实这里很像关系型数据库的事务,原子性。原子性就是对抗程序无序造成异常的有力武器。我们可以在注册心跳处理方法上加个互斥锁,让编译器跟运行时去安排更加合理的执行顺序。

    7.代码实现

    代码很简单。

        //定义一把锁
        public static Mutex activeIpboxDicMutex = new Mutex();
        //设备箱注册加锁。异常全部消除
        IBoxActiveDicManager.activeIpboxDicMWaitOne();
        HeartHandler.Register(tcpHeartPacsessionId);
        IBoxActiveDicManager.activeIpboxDicMReleaseMutex();
    

    这里插入一下事务的使用,也是很类似的,把我们的主业务加中中间,类比方便大家理解记忆。就像夹心饼干(瞎扯)。

                unitOfWork.BeginTransaction();
    
                // Adds new device
                unitOfWork.DeviceRepository.Add(device);
    
                // Commit transaction
                unitOfWork.Commit();
    

    当然也可以给设备箱脱网清除线程加锁。

         IBoxActiveDicManager.activeIpboxDicMutex.WaitOne();
         IBoxActiveDicManager.DeleteDeadBoxFromActiveBox(_internal);
         IBoxActiveDicManager.activeIpboxDicMutex.ReleaseMutex();
    

    考虑到脱网清除线程会损耗部分性能,我也测试了去掉该锁的情况,也不会有第4的3个异常,至此问题全部解决。

    8.小结

    • 模拟设备数量小测不出这个问题,如此看出海量设备的重要性,因为现实情况肯定会出现以上三个问题,而且都是很严重很致命的问题。好的测试方法可以把问题扼杀在摇篮中;

    • 多线程高并发时容易出现这样那样的异常,要怀着敬畏之心去思考,去解决问题;


    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

    本文链接:https://www.cnblogs.com/JerryMouseLi/p/12709048.html

  • 相关阅读:
    利用Windows消息循环,使窗体不能改变大小
    重磅发布全总结丨一文看懂阿里云弹性计算年度峰会
    阿里云弹性计算首席架构师分享云上应用架构演进三大方向
    只需5步!在轻量应用服务器部署Hexo博客
    阿里云手机正式公测,定义手机全新接入方式
    云服务器ECS年终特惠,老用户新购优惠低至4折
    阿里云发布CloudOps白皮书,ECS自动化运维套件新升级
    快速部署阿里云WebIDE(DevStudio)并参与开源项目开发
    抢先看! 2021阿里云弹性计算年度峰会嘉宾演讲内容提前曝光
    饿了么资深架构师分享云上基础架构演进
  • 原文地址:https://www.cnblogs.com/JerryMouseLi/p/12709048.html
Copyright © 2020-2023  润新知