怎样解决高并发下的I/O瓶颈?

news2024/11/29 2:53:19

大家好,我是易安。

说起Java I/O,相信你一定不陌生。你可能使用I/O操作读写文件,也可能使用它实现Socket的信息传输…这些都是我们在系统中最常遇到的和I/O有关的操作。

我们都知道,I/O的速度要比内存速度慢,尤其是在现在这个大数据时代背景下,I/O的性能问题更是尤为突出,I/O读写已经成为很多应用场景下的系统性能瓶颈,不容我们忽视。

今天,我们就来深入了解下Java I/O在高并发、大数据业务场景下暴露出的性能问题,从源头入手,学习优化方法。

什么是I/O

I/O是机器获取和交换信息的主要渠道,而流是完成I/O操作的主要方式。

在计算机中,流是一种信息的转换。流是有序的,因此相对于某一机器或者应用程序而言,我们通常把机器或者应用程序接收外界的信息称为输入流(InputStream),从机器或者应用程序向外输出的信息称为输出流(OutputStream),合称为输入/输出流(I/O Streams)。

机器间或程序间在进行信息交换或者数据交换时,总是先将对象或数据转换为某种形式的流,再通过流的传输,到达指定机器或程序后,再将流转换为对象数据。因此,流就可以被看作是一种数据的载体,通过它可以实现数据交换和传输。

Java的I/O操作类在包java.io下,其中InputStream、OutputStream以及Reader、Writer类是I/O包中的4个基本类,它们分别处理字节流和字符流。如下图所示:

alt

在初次阅读Java I/O流文档的时候,我有过这样一个疑问,“ 不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么I/O流操作要分为字节流操作和字符流操作呢?

我们知道字符到字节必须经过转码,这个过程非常耗时,如果我们不知道编码类型就很容易出现乱码问题。所以I/O流提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。下面我们就分别了解下“字节流”和“字符流”。

1.字节流

InputStream/OutputStream是字节流的抽象类,这两个抽象类又派生出了若干子类,不同的子类分别处理不同的操作类型。如果是文件的读写操作,就使用FileInputStream/FileOutputStream;如果是数组的读写操作,就使用ByteArrayInputStream/ByteArrayOutputStream;如果是普通字符串的读写操作,就使用BufferedInputStream/BufferedOutputStream。具体内容如下图所示:

alt

2.字符流

Reader/Writer是字符流的抽象类,这两个抽象类也派生出了若干子类,不同的子类分别处理不同的操作类型,具体内容如下图所示:

alt

传统I/O的性能问题

我们知道,I/O操作分为磁盘I/O操作和网络I/O操作。前者是从磁盘中读取数据源输入到内存中,之后将读取的信息持久化输出在物理磁盘上;后者是从网络中读取信息输入到内存,最终将信息输出到网络中。但不管是磁盘I/O还是网络I/O,在传统I/O中都存在严重的性能问题。

1.多次内存复制

在传统I/O中,我们可以通过InputStream从源数据中读取数据流输入到缓冲区里,通过OutputStream将数据输出到外部设备(包括磁盘、网络)。你可以先看下输入操作在操作系统中的具体流程,如下图所示:

alt
  • JVM会发出read()系统调用,并通过read系统调用向内核发起读请求;
  • 内核向硬件发送读指令,并等待读就绪;
  • 内核把将要读取的数据复制到指向的内核缓存中;
  • 操作系统内核将数据复制到用户空间缓冲区,然后read系统调用返回。

在这个过程中,数据先从外部设备复制到内核空间,再从内核空间复制到用户空间,这就发生了两次内存复制操作。这种操作会导致不必要的数据拷贝和上下文切换,从而降低I/O的性能。

2.阻塞

在传统I/O中,InputStream的read()是一个while循环操作,它会一直等待数据读取,直到数据就绪才会返回。 这就意味着如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。

在少量连接请求的情况下,使用这种方式没有问题,响应速度也很高。但在发生大量连接请求时,就需要创建大量监听线程,这时如果线程没有数据就绪就会被挂起,然后进入阻塞状态。一旦发生线程阻塞,这些线程将会不断地抢夺CPU资源,从而导致大量的CPU上下文切换,增加系统的性能开销。

如何优化I/O操作

面对以上两个性能问题,不仅编程语言对此做了优化,各个操作系统也进一步优化了I/O。JDK1.4发布了java.nio包(new I/O的缩写),NIO的发布优化了内存复制以及阻塞导致的严重性能问题。JDK1.7又发布了NIO2,提出了从操作系统层面实现的异步I/O。下面我们就来了解下具体的优化实现。

1.使用缓冲区优化读写流操作

在传统I/O中,提供了基于流的I/O实现,即InputStream和OutputStream,这种基于流的实现以字节为单位处理数据。

NIO与传统 I/O 不同,它是基于块(Block)的,它以块为基本单位处理数据。在NIO中,最为重要的两个组件是缓冲区(Buffer)和通道(Channel)。Buffer是一块连续的内存块,是 NIO 读写数据的中转地。Channel表示缓冲数据的源头或者目的地,它用于读取缓冲或者写入数据,是访问缓冲的接口。

传统I/O和NIO的最大区别就是传统I/O是面向流,NIO是面向Buffer。Buffer可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。虽然传统I/O后面也使用了缓冲块,例如BufferedInputStream,但仍然不能和NIO相媲美。使用NIO替代传统I/O操作,可以提升系统的整体性能,效果立竿见影。

2. 使用DirectBuffer减少内存复制

NIO的Buffer除了做了缓冲块优化之外,还提供了一个可以直接访问物理内存的类DirectBuffer。普通的Buffer分配的是JVM堆内存,而DirectBuffer是直接分配物理内存(非堆内存)。

我们知道数据要输出到外部设备,必须先从用户空间复制到内核空间,再复制到输出设备,而在Java中,在用户空间中又存在一个拷贝,那就是从Java堆内存中拷贝到临时的直接内存中,通过临时的直接内存拷贝到内存空间中去。此时的直接内存和堆内存都是属于用户空间。

alt

你肯定会在想,为什么Java需要通过一个临时的非堆内存来复制数据呢?如果单纯使用Java堆内存进行数据拷贝,当拷贝的数据量比较大的情况下,Java堆的GC压力会比较大,而使用非堆内存可以减低GC的压力。

DirectBuffer则是直接将步骤简化为数据直接保存到非堆内存,从而减少了一次数据拷贝。以下是JDK源码中IOUtil.java类中的write方法:

        if (src instanceof DirectBuffer)
            return writeFromNativeBuffer(fd, src, position, nd);

        // Substitute a native buffer
        int pos = src.position();
        int lim = src.limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
        try {
            bb.put(src);
            bb.flip();
        // ...............

这里拓展一点,由于DirectBuffer申请的是非JVM的物理内存,所以创建和销毁的代价很高。DirectBuffer申请的内存并不是直接由JVM负责垃圾回收,但在DirectBuffer包装类被回收时,会通过Java Reference机制来释放该内存块。

DirectBuffer只优化了用户空间内部的拷贝,而之前我们是说优化用户空间和内核空间的拷贝,那Java的NIO中是否能做到减少用户空间和内核空间的拷贝优化呢?

答案是可以的,DirectBuffer是通过unsafe.allocateMemory(size)方法分配内存,也就是基于本地类Unsafe类调用native方法进行内存分配的。而在NIO中,还存在另外一个Buffer类:MappedByteBuffer,跟DirectBuffer不同的是,MappedByteBuffer是通过本地类调用mmap进行文件内存映射的,map()系统调用方法会直接将文件从硬盘拷贝到用户空间,只进行一次数据拷贝,从而减少了传统的read()方法从硬盘拷贝到内核空间这一步。

3.避免阻塞,优化I/O操作

NIO很多人也称之为Non-block I/O,即非阻塞I/O,因为这样叫,更能体现它的特点。为什么这么说呢?

传统的I/O即使使用了缓冲块,依然存在阻塞问题。由于线程池线程数量有限,一旦发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中有空闲的线程可以被复用。而对Socket的输入流进行读取时,读取流会一直阻塞,直到发生以下三种情况的任意一种才会解除阻塞:

  • 有数据可读;
  • 连接释放;
  • 空指针或I/O异常。

阻塞问题,就是传统I/O最大的弊端。NIO发布后,通道和多路复用器这两个基本组件实现了NIO的非阻塞,下面我们就一起来了解下这两个组件的优化原理。

通道(Channel)

前面我们讨论过,传统I/O的数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的I/O接口从磁盘读取或写入。

最开始,在应用程序调用操作系统I/O接口时,是由CPU完成分配,这种方式最大的问题是“发生大量I/O请求时,非常消耗CPU“;之后,操作系统引入了DMA(直接存储器存储),内核空间与磁盘之间的存取完全由DMA负责,但这种方式依然需要向CPU申请权限,且需要借助DMA总线来完成数据的复制操作,如果DMA总线过多,就会造成总线冲突。

通道的出现解决了以上问题,Channel有自己的处理器,可以完成内核空间和磁盘之间的I/O操作。在NIO中,我们读取和写入数据都要通过Channel,由于Channel是双向的,所以读、写可以同时进行。

多路复用器(Selector)

Selector是Java NIO编程的基础。用于检查一个或多个NIO Channel的状态是否处于可读、可写。

Selector是基于事件驱动实现的,我们可以在Selector中注册accpet、read监听事件,Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生监听事件,这个Channel就处于就绪状态,然后进行I/O操作。

一个线程使用一个Selector,通过轮询的方式,可以监听多个Channel上的事件。我们可以在注册Channel时设置该通道为非阻塞,当Channel上没有I/O操作时,该线程就不会一直等待了,而是会不断轮询所有Channel,从而避免发生阻塞。

目前操作系统的I/O多路复用机制都使用了epoll,相比传统的select机制,epoll没有最大连接句柄1024的限制。所以Selector在理论上可以轮询成千上万的客户端。

下面我用一个生活化的场景来举例, 看完你就更清楚Channel和Selector在非阻塞I/O中承担什么角色,发挥什么作用了。

我们可以把监听多个I/O连接请求比作一个火车站的进站口。以前检票只能让搭乘就近一趟发车的旅客提前进站,而且只有一个检票员,这时如果有其他车次的旅客要进站,就只能在站口排队。这就相当于最早没有实现线程池的I/O操作。

后来火车站升级了,多了几个检票入口,允许不同车次的旅客从各自对应的检票入口进站。这就相当于用多线程创建了多个监听线程,同时监听各个客户端的I/O请求。

最后火车站进行了升级改造,可以容纳更多旅客了,每个车次载客更多了,而且车次也安排合理,乘客不再扎堆排队,可以从一个大的统一的检票口进站了,这一个检票口可以同时检票多个车次。这个大的检票口就相当于Selector,车次就相当于Channel,旅客就相当于I/O流。

总结

Java的传统I/O开始是基于InputStream和OutputStream两个操作流实现的,这种流操作是以字节为单位,如果在高并发、大数据场景中,很容易导致阻塞,因此这种操作的性能是非常差的。还有,输出数据从用户空间复制到内核空间,再复制到输出设备,这样的操作会增加系统的性能开销。

传统I/O后来使用了Buffer优化了“阻塞”这个性能问题,以缓冲块作为最小单位,但相比整体性能来说依然不尽人意。

于是NIO发布,它是基于缓冲块为单位的流操作,在Buffer的基础上,新增了两个组件“管道和多路复用器”,实现了非阻塞I/O,NIO适用于发生大量I/O连接请求的场景,这三个组件共同提升了I/O的整体性能。

本文由 mdnice 多平台发布

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【致敬未来的攻城狮计划】— 连续打卡第十五天:FSP固件库外部中断处理编程(外部中断检测按键控制LED闪烁)

系列文章目录 1.连续打卡第一天&#xff1a;提前对CPK_RA2E1是瑞萨RA系列开发板的初体验&#xff0c;了解一下 2.开发环境的选择和调试&#xff08;从零开始&#xff0c;加油&#xff09; 3.欲速则不达&#xff0c;今天是对RA2E1 基础知识的补充学习。 4.e2 studio 使用教程 5.…

J - 在赌场玩

第一周任务 - Virtual Judge (vjudge.net) http://t.csdn.cn/rcwO7 第一周任务 - Virtual Judge (vjudge.net) 【题目描述】 然后所有玩家成对玩&#xff0c;每对玩家只玩一次。因此&#xff0c;例如&#xff0c;如果总共有四个玩家&#xff0c;则进行六场比赛&#xff1a;第…

真题详解(二分查找平均值)-软件设计(六十)

真题详解&#xff08;数据流图平衡&#xff09;-软件设计&#xff08;五十九)https://blog.csdn.net/ke1ying/article/details/130394959 全码&#xff1a;指关系模式所有属性都是这个关系模式的候选码。 RISC特点&#xff1a; 指令种类&#xff1a;少&#xff0c;精简 指令…

彻底弄懂Java的泛型1 - 泛型类

Java泛型是初级程序员向中高级程序员进阶的必经之路&#xff0c;他不是特别难&#xff0c;但是想全部搞懂和会用&#xff0c;还是不容易的。 本文从实战角度出发&#xff0c;讲解你在公司做开发&#xff0c;可能会用到泛型的一种场景。 泛型T的用法 引子 先来看一个简单的类…

UDP 协议

目录 一、什么是协议 二、认识UDP 协议 2.2 UDP 协议的报文格式 2.3 使用UDP 协议传输大文件时的策略 2.4 UDP协议的工作流程 一、什么是协议 为了使数据在网络上传输&#xff08;从源头到达目的&#xff09;&#xff0c;网络通信的参与方必须遵循相同的规则&#xff0c;如…

SpaceX的星舰爆炸了:产品开发,快速失败真的很重要

目录 前言 快速失败 产品生命周期 专栏上线 前言 看到很多人都在聊星舰&#xff0c;今天就来简单谈谈“炸星舰”带给我们的启示。 在美国中部时间20日&#xff0c;SpaceX公司的“星舰”超重型火箭进行了首次轨道飞行。 但在该火箭成功点火升空几分钟后&#xff0c;却在半…

Java-synchronized实现详解(从Java到汇编)

synchronized作为java语言中的并发关键词&#xff0c;其在代码中出现的频率相当高频&#xff0c;大多数开发者在涉及到并发场景时&#xff0c;一般都会下意识得选取synchronized。 synchronized在代码中主要有三类用法&#xff0c;根据其用法不同&#xff0c;所获取的锁对象也…

如何通过开源项目搭建私有云平台--第四步下:安装rancher 监控

第四步下&#xff1a;安装rancher 监控,缺告警 本来想监控与告警一起写&#xff0c;但最近几天研究了rancher的告警&#xff0c;按照文档说法&#xff0c;配置了但没有触发&#xff0c;网上找了一些资料&#xff0c;有的在rancer 2.6成功的&#xff0c;但我用同样的方法在2.7.…

09 【Sass语法介绍-函数指令】

1.前言 在之前的章节我们学习过 Sass 提供的各种各样的函数&#xff0c;那么如果我们需要自定定义函数来使用就需要用到函数指令 function了。本节内容我们来学习 Sass 函数指令的语法和使用&#xff0c;在 Sass 中自定义函数是必须要掌握的&#xff01; 2.什么是 Sass 函数指…

又一款可视化神器,开源了!

在互联网数据大爆炸的这几年&#xff0c;各类数据处理、数据可视化的需求使得 GitHub 上诞生了一大批高质量的 BI 工具。 借助这些 BI 工具&#xff0c;我们能够大幅提升数据分析效率、生成更高质量的项目报告&#xff0c;让用户通过直观的数据看到结果&#xff0c;减低沟通成…

安卓项目如何做单元测试

前言 先说一下创建篇文章的目的&#xff0c;近期负责搭建公司的单元测试框架&#xff0c;于是查阅了网上的很多文章&#xff0c;以及参考了github上很多的项目例子&#xff0c;并且也进行了相当多的尝试。这其中花费了很多的精力&#xff0c;大约有两三周的时间&#xff0c;远…

淘系抓包流程(淘宝数据无法抓包解决方式)

淘系抓包流程 结合frida和adb工具以及mumu模拟器进行抓包。 具体的关系图: frida的安装 frida安装&#xff0c;直接安装官网的脚手架。frida官网使用python的pip安装&#xff0c;python > 3。 安装后使用查看版本命令来确认是否安装。 pip install frida-tools frida --ve…

【严重】VMware Aria Operations for Logs v8.10.2 存在反序列化漏洞(CVE-2023-20864)

漏洞描述 VMware Aria Operations for Logs前身是vRealize Log Insight&#xff0c;VMware用于处理和管理大规模的日志数据产品。 VMware Aria Operations for Logs 8.10.2版本中存在反序列化漏洞&#xff0c;具有 VMware Aria Operations for Logs 网络访问权限的未经身份验…

“SCSA-T学习导图+”系列:交换技术之STP

本期引言&#xff1a; 在通信工程当中&#xff0c;从物理层面上&#xff0c;我们可以采用冗余链路保证网络的健壮性。冗余是指出于系统安全和可靠性等方面的考虑&#xff0c;人为地对一些关键部件或功能进行重复的配置。当系统发生故障时&#xff0c;比如某一设备发生损坏&…

【Linux】Linux下的gbd调试,你学废了吗

操作系统核心数centos 3.10.032位单核 gbd调试方法-以线程运行时调试为例 线程死锁状态时查看栈升级gbd通过gdb在程序运行时进行调试 线程死锁状态时查看栈 在线程-线程安全之互斥中&#xff0c;我们自己写了一个模拟实现的线程死锁情况 我们用gbd调试查看了当前线程的调用&a…

拉链表制作

1.拉链表的应用场景 拉链表适合于&#xff1a;数据会发生变化&#xff0c;但是大部分是不变化的&#xff08;即&#xff1a;缓慢变化维。还需要保留历史数据做分析的场景&#xff09; 2.拉链表的形成过程 关键的过程 第四步&#xff1a;获取变化的数据&#xff08;创建和修改…

Flask连接MySQL

本文章涉及到Flask框架和HTML内容&#xff0c;相关知识可查看链接 HTML-form表单和提交_html form 提交_小梁今天敲代码了吗的博客-CSDN博客https://blog.csdn.net/weixin_43780415/article/details/130110722 前端引入和html标签_小梁今天敲代码了吗的博客-CSDN博客https://b…

vscode下drawio无法使用

问题描述&#xff1a; Vscode下&#xff0c; 刚下载drawio这个插件&#xff0c;在vscode左边EXPLORER下&#xff0c;没有Test这个页面,导致vscode无法使用drawio。 解决办法&#xff1a; 在自己需要的目录下&#xff0c;新建一个文件&#xff0c;例如test,并命名为test.drawi…

数据结构算法

直接插入排序 1.从第一个元素开始&#xff0c;该元素可以认为已经被排序 2.取下一个元素tem&#xff0c;从已排序的元素序列从后往前扫描 3.如果该元素大于tem&#xff0c;则将该元素移到下一位 4.重复步骤3&#xff0c;直到找到已排序元素中小于等于tem的元素 5.tem插入到该元…

5.1劳动节,致敬最可爱的人!Cocos社区杰出贡献者出炉

Cocos 引擎的生态建设与繁荣&#xff0c;离不开社区开发者的辛勤付出。 2022.5 ~ 2023.5 年度期间&#xff0c;有这样一批 Cocos 社区开发者&#xff0c;他们使用 Cocos Creaor 引擎创作内容与产品、分享技术和经验&#xff0c;为 Cocos 社区默默贡献自己的一份力量&#xff0c…