IO密集型服务提升性能的三种方法

文章目录

  • 批处理
  • 缓存
  • 多线程
  • 总结

  大部分的业务系统其实都是IO密集型的系统,比如像我们面向B端提供摄像头服务,很多的接口其实就是将各种各样的数据汇总起来,展示给用户,我们的数据来源包括Redis、Mysql、Hbase、以及依赖的一些服务方的数据,并不涉及到太多复杂的计算逻辑。在过去的半年中,因为我们数据量和业务复杂性的增长,确实遇到了一些明显的性能问题,分析大部分问题的本质原因就是IO太慢了。 我们系统中最复杂的计算逻辑执行最慢也就微秒级,而调一次数据库最快也得1-2毫秒,有着2-3个数量级的差距。

  然而IO又是业务系统中不可能干掉的操作,但频繁或者错误的使用IO会给系统带来非常明显的性能问题,轻则拖慢接口影响用户体验,重则OOM直接宕机。 针对IO问题带来性能问题,这里我总结了三种方式 批处理、缓存和多线程,虽然看起来是很简单的操作,但还是得在合适的地方正确使用才能发挥出这三种方法的价值。

批处理

  首先是批处理,这里先说一个真实的案例, 在2021年我们在做服务上云过程中,有个接口上云后,时延从原本的50ms左右涨到了150ms,后来排查发现,之前是串行化去调用KMS,这个服务上云后和KMS的服务端出现了跨机房调用,单次KMS的调用时长增长了近0.5ms。 单看这0.5ms确实不算多,但也架不住几十次的串行调用累计到一起,最终出现了100ms的总延时增长。这种接口时延增长大到原来的三倍,用户是很容易感受到的,可能他们的感受就是这应用真卡!

上面这个问题复现起来很简单,其实就一个for循环,串行去调用kms解密数据量。

for (String str : strList) {
   decodedStr = kmsClient.decrypt(str);  // 单次调用需要0.5-1ms,串行100次需要50-100ms
}

  上述代码整体的主要的耗时其实并不是kms对数据解密的过程上(仅需要微秒级),而是请求发送和接收结果数据时数据在网络上传输的耗时,这就取决于双方服务之间的物理距离了,我们大部分服务都是在北京部署,但仍会出现跨机房调用的情况,这个时候网络延时也会增长0.5-1ms。批处理提升IO性能的原理,其实就是用单次网络IO替代掉原有的多次网络IO,IO时长越长,优化效果越显著。 用一个生活中的例子大家更容易理解些,假设你要给家里准备一份晚餐,其中很重要的一步就是去菜市场买菜,你是一样一样买?还是一次性全买齐了? 这就是单次处理和批处理的区别。

  这个性能问题看似简单,其实在实际编程过程中经常犯,稍不留神就大批量串行IO调用,比如在for循环中查库(你是不是已经在脑海中想到自己写的问题代码了)。 如何避免自己在日常编程中出现类似的问题,我总结了一条编程指导经验,那就是 在任何循环中尽量不要产生IO调用,除非你知道自己在做什么。

  当然也不是所有的IO都会产生问题,有些IO非常快,而且你串行的频次也不是很高,贸然将代码改成批处理的逻辑会显著增加代码复杂度,增加维护成本反而得不偿失,所以建议还是根据具体的IO类型和具体需求,评估具体是否要做批处理。以下我给出一些具体的IO类型和单次IO耗时参考值,大家写代码的时候可以关注下。

IO类型耗时备注
SSD固态磁盘随机访问0.1ms目前大部分服务器在使用SSD了,小文件读写的耗时几乎可以不关注,但如果文件非常大时,这里各方的带宽就是瓶颈,耗时也容易快速增长,重点关注大文件。
Redis访问0.1ms简单Redis查询,主要还是在网络上,Redis服务自身处理请求仅几十us,只要不出大key,基本没问题。
mysql查询1-10ms简单查询可以在10ms下,但涉及到复杂查询或者大量数据无索引的情况下,耗时会显著增长。mysql的异常查询是很多业务系统的性能问题主要来源。
HDD机械磁盘随机访问10ms主要磁盘寻道时间,取决于磁盘转速,如果你恰好用了HDD又想读写文件,无论文件大小这部分耗时是一定不能忽略的。
调用第三方服务1-100ms取决于依赖方的接口性能,不同接口延时的方差非常大,调用第三方接口,性能和容量都需要非常仔细的评估。
同城跨机房RTT0.5ms-
物理距离每增加50-100公里rtt +1ms延时主要来源于光在光纤中的传播耗时+交换机和路由器的处理耗时,比如从广州到北京,一个RTT就需要50ms,对接外部服务接口,如果关注性能,物理距离一定要考虑进去。

缓存

  高IO的应用有个特点,就是大量的数据其实是被重复加载的,这也是”局部性“的一个体现,局部性告诉我们,只有少量的数据会被大量的加载。 利用局部性,我们只要将重要的小部分数据缓存起来,就可以减少大量的IO,从而提升我们系统的性能。如果我们用平均延时来评估性能,我们可以用一个平均延迟计算公式来描述加缓存后的性能:

avgLatency = hitRate * cacheLatency +  (1 - hitRate) * originalLatency

  其中avgLatency代指加了缓存后的平均延迟,hitRate表示缓存的命中率,cacheLatency指的是访问一次缓存所需要的耗时,在实际使用中,如果我们使用了本地缓存,我们可以简单粗暴认为cacheLatency是0,以上公式就可以简化为avgLatency = (1 - hitRate) * originalLatency 。 从简化后的公式可以看出加缓存后的效果仅跟缓存的命中率有关系,如果cache命中率是90%,就会有10倍的性能提升,如果是99%就会有100百性能提升(简略计算),只要我们无限提升缓存命中率,似乎就能无限提升性能。那命中率又和什么相关呢? 答案就是数据的分布、缓存的大小和数据的淘汰策略三者相关。
在这里插入图片描述

数据分布: 现实世界中,大部分数据的访问都受局部性的影响,用大白话讲就是只有少部分数据会被频繁访问,如果把数据被访问频次曲线画出来,如上图。
缓存大小: 这个很好理解,只要缓存的数据足够多,缓存命中率就越高。
淘汰策略: 淘汰策略是指在缓存容量不足的情况下,如何剔除价值最低的数据,常见的淘汰策略有LRU、LFU、FIFO,我们实际情况中用的最多的就是LRU。

  正确考虑到以上三点后,我们大部分情况下是可以将少量高频被访问的数据缓存起来,从而提升系统性能。使用Cache有个额外需要注意的一项就是数据一致性,在cache的使用过程中缓存命中率和数据一致性几乎就是相悖的,很难做到两全其美,就比如我在上篇文章《从CPU的视角看 多线程代码为什么那么难写!》中写道的CPU Cache,其实就是硬件层面使用Cache优化IO性能的一个典型案例,但CPU为保证数据一致性却给当代程序员留下一堆"坑"。

  在实际工作中,关于Cache实现我们有很多选择,常用的比如Guava中的LoadingCache、caffiene、ehcache、redis,spring中也有spring-cache 高级封装,这些如果你都不想用的话,你都可以用Map自己撸一个…… 这里先打个广告,后续关于cache的配置、使用及注意事项会再出一篇详细的文章, 我这里就先不展开了。

多线程

  以上两种方式的本质,其实是通过优化非必要的IO次数来提升性能,但现实情况中并不是所有的IO都可以被优化掉,针对这种情况,其实也就只多线程一条路可选了。这个思路也很好理解,用大白话来说,如果活太多干不完就多招两个人来干。 在IO密集型系统中,多线程的优势在于它能充分利用CPU的计算能力。当一个线程在等待IO操作(如网络请求或磁盘读写)完成时,CPU可以切换到其他线程去执行其他任务,而不是闲置不用。这样,我们就可以充分利用CPU资源,提高系统的响应速度。

  但是,使用多线程并非没有代价。首先,需要注意的是线程切换的开销。如果线程数量过多,线程切换的开销可能会消耗大量的CPU资源。其次,使用多线程会显著增加代码的复杂度,需要考虑到很多并发相关的问题,如:线程间的同步、死锁、资源竞争等,这些都需要在编程时仔细考虑和处理,稍有不慎就会引入很难排查的Bug。

  在Java中,我们可以通过使用ExecutorService、CompletableFuture等工具来创建并管理线程。当然,我们也可以直接使用Thread类来创建线程,但线程需要自行管理,不是很推荐。同时,Java提供了许多同步和并发工具,如synchronized关键字、ReentrantLock、Semaphore等,以帮助我们处理并发问题。

  在多线程优化中,线程池的使用是非常常见的。线程池可以有效地管理和复用线程,避免了频繁地创建和销毁线程所带来的开销。在Java中,我们可以使用ExecutorService来创建一个线程池,然后将任务提交给线程池来执行。在Java8及以上的版本中,我们也可用使用parallelStream()很方便的将代码改造成多线程,但需注意parallelStream底层是使用同一个ForkJoinPool,大量使用可能会出现相互干扰的情况

  另一个常见的多线程优化方式是使用异步编程。异步编程可以让程序在等待IO操作完成的时候,不必阻塞当前线程,而是可以切换到其他任务进行处理。在Java中,我们可以使用Future、CompletableFuture等工具来进行异步编程。
  总的来说,多线程可以是一个强大的工具,可以显著提高IO密集型系统的性能。但是,使用多线程也需要谨慎,需要处理好并发问题,才能确保程序的正确性和稳定性。

总结

  在面对IO密集型系统性能优化时,我们可以通过三种主要的方式来进行:批处理、缓存和多线程。这三种方式各有其优点和适用场景。

  1. 批处理可以通过减少网络IO次数,显著减少网络传输的延迟时间,从而提升系统性能。但是,它需要我们仔细分析和设计我们的数据处理流程,才能找到合适的批处理策略。
  2. 缓存则是通过存储频繁访问的数据,减少了对慢速存储(如磁盘或网络)的访问,从而提升性能。但是,使用缓存时需要考虑数据的一致性问题,以及如何选择合适的缓存淘汰策略。
  3. 多线程则是通过并行处理多个任务,充分利用CPU的计算能力,从而提升性能。但是,使用多线程需要处理并发问题,以及线程管理和调度的开销。

  在实际应用中,这三种方式往往会结合使用,以适应不同的性能需求和系统环境。选择哪种方式,或者如何结合使用,需要根据具体的业务需求、系统环境和性能目标来决定。在进行性能优化时,我们需要深入理解我们的系统,找出性能瓶颈,然后有针对性的进行优化。同时,我们还需要通过性能测试和监控,来验证我们的优化效果,以及及时发现和解决新的性能问题。只有通过这样的方式,我们的系统才能持续提供高效、稳定的服务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/71280.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IP协议

网络层 对于网络层来说,它是传输层协议的具体实施,那么它具体是如何实施的呢? IP协议 IP能够实现将数据从A主机送到B主机,在网络中,每一个IP报文都包含了它的目标网络和目标主机。 而IP协议就是网络层使用的协议。 I…

【mysql】—— 表的约束

目录 序言 (一)空属性 (二)默认值 (三)列描述 (四)zerofill (五)主键 (六)自增长 (七)唯一键 &#…

stm32_断点调试无法进入串口接收中断

先说结果,可能是stm32调试功能/keil软件/调试器(试过STLINK和JLINK两种)的问题,不是代码; 1、入坑 配置完串口后,可以发送数据到串口助手,但不能接收数据并做处理,所以第一步&…

安全防御(3)

1.总结当堂NAT与双机热备原理,形成思维导图 2.完成课堂nat与双机热备试验 引用IDS是指入侵检测系统,它可以在网络中检测和防御入侵行为。IDS的签名是指根据已知入侵行为的特征制定的规则,用于检测和警告可能存在的入侵行为。签名过滤器可以根…

图论——最短路算法

引入&#xff1a; 如上图&#xff0c;已知图G。 问节点1到节点3的最短距离。 可心算而出为d[1,2]d[2,3]112,比d[1,3]要小。 求最短路径算法&#xff1a; 1.Floyd(弗洛伊德) 是一种基于三角形不等式的多源最短路径算法。边权可以为负数 表现为a[i,j]a[j,k]<a[i,k]。 …

9.2.2Socket(TCP)

一.过程: 1.建立连接(不是握手),虽然内核中的连接有很多,但是在应用程序中,要一个一个处理. 2. 获取任务:使用ServerSocket.accept()方法,作用是把内核中的连接获取到应用程序中,这个过程类似于生产者消费者模型. 3. 使用缓冲的时候,注意全缓冲和行缓冲. 4.注意关闭文件资源…

排序算法(二)

1.希尔排序-Shell Sort 1.算法原理 将未排序序列按照增量gap的不同分割为若干个子序列&#xff0c;然后分别进行插入排序&#xff0c;得到若干组排好序的序列&#xff1b; 缩小增量gap&#xff0c;并对分割为的子序列进行插入排序&#xff1b;最后一次的gap1&#xff0c;即整个…

SQL 基础查询

msyql 不区分大小写 DDL 数据定义语言 查询 show databases create database db01 创建数据库 create database if not exists db01 创建数据库 删除数据库 drop database if exists db01 使用数据库 use 数据库名 CREATE TABLE tb_user(id int PRIMARY KEY COMMENT i…

简单易用且高效的跨平台开发工具:Xojo 2023 for Mac

Xojo for Mac是Mac平台上一个跨平台的针对桌面、Web、移动和Raspberry Pi的快速应用程序开发软件。与其他多平台开发工具相比&#xff0c;Xojo for Mac为开发人员提供了显着的生产率提高。 Xojo for Mac具有拖放功能&#xff0c;使您能够快速创建用户界面设计&#xff0c;然后…

【Linux初阶】进程间通信介绍 管道

&#x1f31f;hello&#xff0c;各位读者大大们你们好呀&#x1f31f; &#x1f36d;&#x1f36d;系列专栏&#xff1a;【Linux初阶】 ✒️✒️本篇内容&#xff1a;进程间通信介绍&#xff0c;管道概述&#xff0c;匿名管道应用&#xff0c;命名管道应用 &#x1f6a2;&#…

如何在 Spring Boot 中集成日志框架 SLF4J、Log4j

文章目录 具体步骤附录 笔者的操作环境&#xff1a; Spring Cloud Alibaba&#xff1a;2022.0.0.0-RC2 Spring Cloud&#xff1a;2022.0.0 Spring Boot&#xff1a;3.0.2 Nacos 2.2.3 Maven 3.8.3 JDK 17.0.7 IntelliJ IDEA 2022.3.1 (Ultimate Edition) 具体步骤 因为 …

HTTP代理编程:Python实用技巧与代码实例

今天我要与大家分享一些关于HTTP代理编程的实用技巧和Python代码实例。作为一名HTTP代理产品供应商&#xff0c;希望通过这篇文章&#xff0c;帮助你们掌握一些高效且实用的编程技巧&#xff0c;提高开发和使用HTTP代理产品的能力。 一、使用Python的requests库发送HTTP请求&a…

【ElasticSearch入门】

目录 1.ElasticSearch的简介 2.用数据库实现搜素的功能 3.ES的核心概念 3.1 NRT(Near Realtime)近实时 3.2 cluster集群&#xff0c;ES是一个分布式的系统 3.3 Node节点&#xff0c;就是集群中的一台服务器 3.4 index 索引&#xff08;索引库&#xff09; 3.5 type类型 3.6 doc…

STM32F429IGT6使用CubeMX配置串口通信

1、硬件电路 2、设置RCC&#xff0c;选择高速外部时钟HSE,时钟设置为180MHz 3、配置USART1引脚 4、生成工程配置 5、部分代码 //重定向printf函数 int fputc(int ch, FILE *f) {HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);return ch; } /* USER CODE BE…

Mac M1 安装Oracle Java 与 IEDA

文章目录 1 官网下载2 安装IDEA参考 1 官网下载 https://www.oracle.com/ 使用finder中的拖拽进行安装即可 2 安装IDEA https://www.jetbrains.com/zh-cn/idea/download/?sectionmac 同样的&#xff0c;下载完后拖拽安装即可 参考 Mac M1 安装Java 开发环境 https://blog.…

cuda+anaconda+pytorch按照教程

首先安装显卡对应的CUDA版本&#xff0c;关键点在于区别显卡支持的CUDA最高版本和运行版本 1、查看当前显卡支持的最高版本&#xff0c;有两种方式&#xff1a; 1&#xff09;NVIDIA控制面板—>帮助—>系统信息—>组件—>NVCUDA.dll对应版本 请注意&#xff0c;12…

快速上手React:从概述到组件与事件处理

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

React源码解析18(1)------ React.createElement 和 jsx

1.React.createElement 我们知道在React17版本之前&#xff0c;我们在项目中是一定需要引入react的。 import React from “react” 即便我们有时候没有使用到React&#xff0c;也需要引入。原因是什么呢&#xff1f; 在React项目中&#xff0c;如果我们使用了模板语法JSX&am…

Spring-1-深入理解Spring XML中的依赖注入(DI):简化Java应用程序开发

学习目标 前两篇文章我们介绍了什么是Spring,以及Spring的一些核心概念&#xff0c;并且快速快发一个Spring项目&#xff0c;以及详细讲解IOC&#xff0c;今天详细介绍一些DI(依赖注入) 能够配置setter方式注入属性值 能够配置构造方式注入属性值 能够理解什么是自动装配 一、…

【C语言】每日一题---1

大家好&#xff0c;我是苏貝&#xff0c;本篇博客是系列博客每日一题的第一篇&#xff0c;本系列的题都不会太难&#xff0c;如果大家对这种系列的博客感兴趣的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 下面代码的结果是&#xff1a; #include <…