心跳 —— 超时机制分析

简介: 在C/S模式中,有时我们会长时间保持一个连接,以避免频繁地建立连接,但同时,一般会有一个超时时间,在这个时间内没发起任何请求的连接会被断开,以减少负载,节约资源。并且该机制一般都是在服务端实现,因为client强制关闭或意外断开连接,server端在此刻是感知不到的,如果放到client端实现,在上述情况下,该超时机制就失效了。本来这问题很普通,不太值得一提,但最近在项目中看到了该机制的一种糟糕的实现,故在此深入分析一下。

问题描述

在C/S模式中,有时我们会长时间保持一个连接,以避免频繁地建立连接,但同时,一般会有一个超时时间,在这个时间内没发起任何请求的连接会被断开,以减少负载,节约资源。并且该机制一般都是在服务端实现,因为client强制关闭或意外断开连接,server端在此刻是感知不到的,如果放到client端实现,在上述情况下,该超时机制就失效了。本来这问题很普通,不太值得一提,但最近在项目中看到了该机制的一种糟糕的实现,故在此深入分析一下。

image.png

问题分析及解决方案

服务端一般会保持很多个连接,所以,一般是创建一个定时器,定时检查所有连接中哪些连接超时了。此外我们要做的是,当收到客户端发来的数据时,怎么去刷新该连接的超时信息?

最近看到一种实现方式是这样做的:

publicclassConnection {

   privatelong lastTime;

   publicvoidrefresh() {

       lastTime = System.currentTimeMillis();

   }

   publiclonggetLastTime() {

       return lastTime;

   }

   //......

}

在每次收到客户端发来的数据时,调用refresh方法。

然后在定时器里,用当前时间跟每个连接的getLastTime()作比较,来判定超时:

public classTimeoutTask  extendsTimerTask{

   public void run() {

       long now = System.currentTimeMillis();

       for(Connection c: connections){

           if(now - c.getLastTime()> TIMEOUT_THRESHOLD)

               ;//timeout, do something

       }

   }

}

看到这,可能不少读者已经看出问题来了,那就是内存可见性问题,调用refresh方法的线程跟执行定时器的线程肯定不是一个线程,那run方法中读到的lastTime就可能是旧值,即可能将活跃的连接判定超时,然后被干掉。

有读者此时可能想到了这样一个方法,将lastTime加个volatile修饰,是的,这样确实解决了问题,不过,作为服务端,很多时候对性能是有要求的,下面来看下在我电脑上测出的一组数据,测试代码如下,供参考

publicclass PerformanceTest {

   private static long i;

   private volatile static long vt;

   private static final int TEST_SIZE = 10000000;

   public static void main(String[] args) {

       long time = System.nanoTime();

       for (int n = 0; n < TEST_SIZE; n++)

           vt = System.currentTimeMillis();

       System.out.println(-time + (time = System.nanoTime()));

       for (int n = 0; n < TEST_SIZE; n++)

           i = System.currentTimeMillis();

       System.out.println(-time + (time = System.nanoTime()));

       for (int n = 0; n < TEST_SIZE; n++)

           synchronized (PerformanceTest.class) {

           }

       System.out.println(-time + (time = System.nanoTime()));

       for (int n = 0; n < TEST_SIZE; n++)

           vt++;

       System.out.println(-time + (time = System.nanoTime()));

       for (int n = 0; n < TEST_SIZE; n++)

           vt = i;

       System.out.println(-time + (time = System.nanoTime()));

       for (int n = 0; n < TEST_SIZE; n++)

           i = vt;

       System.out.println(-time + (time = System.nanoTime()));

       for (int n = 0; n < TEST_SIZE; n++)

           i++;

       System.out.println(-time + (time = System.nanoTime()));

       for (int n = 0; n < TEST_SIZE; n++)

           i = n;

       System.out.println(-time + (time = System.nanoTime()));

   }

}

测试一千万次,结果是(耗时单位:纳秒,包含循环本身的时间):

238932949 volatile写+取系统时间

144317590 普通写+取系统时间

135596135 空的同步块(synchronized)

80042382 volatile变量自增

15875140 volatile写

6548994 volatile读

2722555 普通自增

2949571 普通读写

从上面的数据看来,volatile写+取系统时间的耗时是很高的,取系统时间的耗时也比较高,跟一次无竞争的同步差不多了,接下来分析下如何优化该超时时机。

首先:同步问题是肯定得考虑的,因为有跨线程的数据操作;另外,取系统时间的操作比较耗时,能否不在每次刷新时都取时间?因为刷新调用在高负载的情况下很频繁。如果不在刷新时取时间,那又该怎么去判定超时?

我想到的办法是,在refresh方法里,仅设置一个volatile的boolean变量reset(这应该是成本最小的了吧,因为要处理同步问题,要么同步块,要么volatile,而volatile读在此处是没什么意义的),对时间的掌控交给定时器来做,并为每个连接维护一个计数器,每次加一,如果reset被设置为true了,则计数器归零,并将reset设为false(因为计数器只由定时器维护,所以不需要做同步处理,从上面的测试数据来看,普通变量的操作,时间成本是很低的),如果计数器超过某个值,则判定超时。 下面给出具体的代码:

publicclassConnection {

   int count = 0;

   volatilebooleanreset = false;

   publicvoidrefresh() {

       if (reset == false)

           reset = true;

   }

}

publicclass TimeoutTask extends TimerTask {

   publicvoid run() {

       for (Connection c : connections) {

           if (c.reset) {

               c.reset = false;

               c.count = 0;

           } elseif (++c.count >= TIMEOUT_COUNT)

               ;// timeout, do something

       }

   }

}

代码中的TIMEOUT_COUNT 等于超时时间除以定时器的周期,周期大小既影响定时器的执行频率,也会影响实际超时时间的波动范围(这个波动,第一个方案也存在,也不太可能避免,并且也不需要多么精确)。

代码很简洁,下面来分析一下。

reset加上了volatile,所以保证了多线程操作的可见性,虽然有两个线程都对变量有写操作,但无论这两个线程怎么穿插执行,都不会影响其逻辑含义。

再说下refresh方法,为什么我在赋值语句上多加了个条件?这不是多了一次volatile读操作吗?我是这么考虑的,高负载下,refresh会被频繁调用,意味着reset长时间为true,那么加上条件后,就不会执行写操作了,只有一次读操作,从上面的测试数据来看,volatile变量的读操作的性能是显著优于写操作的。只不过在reset为false的时候,多了一次读操作,但此情况在定时器的一个周期内最多只会发一次,而且对高负载情况下的优化显然更有意义,所以我认为加上条件还是值得的。

最后提及一下,我有点完美主义,自认为上面的方案在我当前掌握的知识下,已经很漂亮了,如果你发现还有可优化的地方,或更好的方案,希望能分享。

相关文章
|
Linux 网络架构
Linux 中查看本机的子网掩码和网关
Linux 中查看本机的子网掩码和网关
|
资源调度 JavaScript Windows
yarn install命令运行报错:无法将“yarn”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。...
yarn install命令运行报错:无法将“yarn”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。...
3306 1
yarn install命令运行报错:无法将“yarn”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。...
|
4月前
|
机器学习/深度学习 人工智能 算法
Google DeepMind新产物: 行星级卫星嵌入数据集(10m)光学+雷达+DEM+climate...
Google 推出 Earth Engine 卫星嵌入数据集,利用 AI 将一年的多源卫星数据压缩至每个 10 米像素,实现高效地理空间分析。基于 AlphaEarth Foundations 模型,该数据集提供 64 维嵌入向量,支持相似性搜索、变化检测、自动聚类和精准分类,助力环境研究与应用。
390 0
|
监控 物联网 Java
打造高可用系统:深入了解心跳检测机制
本文介绍了分布式系统中**心跳检测**的重要机制,用于监测系统节点的健康状态和通信畅通。心跳检测通过定期发送信号,若节点在预定期限内未响应则视为可能失效。处理机制包括重试、报警和自动修复。文章还提到了**周期检测**和**累计失效检测**两种策略,并给出Java代码示例展示心跳检测实现。此外,列举了心跳检测在分布式数据库、微服务和物联网等场景的应用,以及优化策略如动态调整心跳频率和优化超时机制。最后,强调了心跳检测对系统稳定性和高可用性的关键作用。
2293 2
|
10月前
|
数据安全/隐私保护
基于PID控制器的车辆控制系统simulink建模与仿真
本课题基于MATLAB2022a的Simulink平台,构建了车辆控制系统的PID控制器模型并进行仿真。PID控制器通过比例、积分、微分三项参数调整,实现对车辆性能(如车速、方向等)的精确控制。系统仿真结果显示了良好的控制效果,完整程序运行无水印。模型涵盖了PID控制器和车辆动力学模型,验证了PID控制策略的有效性。
|
10月前
|
数据采集 存储 数据挖掘
深入剖析 Python 爬虫:淘宝商品详情数据抓取
深入剖析 Python 爬虫:淘宝商品详情数据抓取
|
JavaScript 数据管理 虚拟化
ArkTS List组件基础:掌握列表渲染与动态数据管理
在HarmonyOS应用开发中,ArkTS的List组件是构建动态列表视图的核心。本文深入探讨了List组件的基础,包括数据展示、性能优化和用户交互,以及如何在实际开发中应用这些知识,提升开发效率和应用性能。通过定义数据源、渲染列表项和动态数据管理,结合虚拟化列表和条件渲染等技术,帮助开发者构建高效、响应式的用户界面。
1042 2
|
安全 Java
【JAVA】线程的run()和start()有什么区别?
【JAVA】线程的run()和start()有什么区别?
|
存储 缓存 负载均衡
图解一致性哈希算法,看这一篇就够了!
近段时间一直在总结分布式系统架构常见的算法。前面我们介绍过布隆过滤器算法。接下来介绍一个非常重要、也非常实用的算法:一致性哈希算法。通过介绍一致性哈希算法的原理并给出了一种实现和实际运用的案例,带大家真正理解一致性哈希算法。
26732 66
图解一致性哈希算法,看这一篇就够了!
|
机器学习/深度学习 编解码 算法
【阿里云OpenVI-视觉生产系列之图片上色】照片真实感上色算法DDColor ICCV2023论文深入解读
图像上色是老照片修复的一个关键步骤,本文介绍发表在 ICCV 2023 上的最新上色论文 DDColor
3946 11
【阿里云OpenVI-视觉生产系列之图片上色】照片真实感上色算法DDColor ICCV2023论文深入解读