我的ImageIO.write ByteArrayOutputStream为什么这么慢?
问题来源:
1.系统生成二维码,需要不同的图片格式来适应客户端要求
2.图片通过接口模式给客户端,最终使用base64来传递
平常思考模式:
1.BufferedImage首先通过工具把数据生成出来。
2.我绝对不会把这个BufferedImage写磁盘,直接放内存ByteArrayOutputstream后转base64岂不是更快?
3.ImageIO.write正好有个write(BufferedImage img,String format,OutputStream output)
4.真的舒服,我就用它了!
实际情况:
1.Linux环境centos6.8 虚拟化环境
2.JRE1.8
3.接口工作流程:(1) 生成BufferedImage (2) BufferedImage通过ImageIO.write(BufferedImage,"png",ByteArrayOutputStream out) (3)将ByteArrayOutputStream转化为base64 (4) 接口返回
4.一个普通的链接,生成二维码并返回base64,接口耗时1.7S
5.png图片大小16K
分析问题&尝试更换接口:
1.一个图片生成16K,不大
2.一次请求1.7s,又是手机端应用,太慢了!不能接受
3.根据代码跟踪分析得出速度慢在 ImageIO.write这里
4.网上搜索信息也有相关的反馈说ImageIO.write png的时候奇慢无比,但是没有找到实际解决方法
5.尝试更换write的ByteArrayOutputStream为File,因为 ImageIO.write正好支持写文件ImageIO.write(BufferedImage,"png",File out)
6.测试结果:write到file后,接口响应时间在400ms!!!
查看源代码:
1.对比write到Byte和File的源代码发现,使用ByteArrayOutputStream的底层写数据的时候使用了FileCacheImageOutputStream,而使用File的底层写数据的时候使用了FileImageOutputStream。
2.查看FileCacheImageOutputStream的初始化方式、和写数据相关代码
//初始化代码 public FileCacheImageOutputStream(OutputStream stream, File cacheDir) throws IOException { if (stream == null) { throw new IllegalArgumentException("stream == null!"); } if ((cacheDir != null) && !(cacheDir.isDirectory())) { throw new IllegalArgumentException("Not a directory!"); } this.stream = stream; //这里竟然创建了临时文件 if (cacheDir == null) this.cacheFile = Files.createTempFile("imageio", ".tmp").toFile(); else this.cacheFile = Files.createTempFile(cacheDir.toPath(), "imageio", ".tmp") .toFile(); this.cache = new RandomAccessFile(cacheFile, "rw"); this.closeAction = StreamCloser.createCloseAction(this); StreamCloser.addToQueue(closeAction); } // 写数据,没什么特殊 public void write(int b) throws IOException { flushBits(); // this will call checkClosed() for us cache.write(b); ++streamPos; maxStreamPos = Math.max(maxStreamPos, streamPos); } //关闭 public void close() throws IOException { maxStreamPos = cache.length(); seek(maxStreamPos); //注意这里!!!!! flushBefore(maxStreamPos); super.close(); cache.close(); cache = null; cacheFile.delete(); cacheFile = null; stream.flush(); stream = null; StreamCloser.removeFromQueue(closeAction); } //把数据写入ByteArrayOutputStream public void flushBefore(long pos) throws IOException { long oFlushedPos = flushedPos; super.flushBefore(pos); // this will call checkClosed() for us long flushBytes = flushedPos - oFlushedPos; if (flushBytes > 0) { // 这里使用了一个逻辑每次只读512个字节到stream里面!!然后循环 int bufLen = 512; byte[] buf = new byte[bufLen]; cache.seek(oFlushedPos); while (flushBytes > 0) { int len = (int)Math.min(flushBytes, bufLen); cache.readFully(buf, 0, len); stream.write(buf, 0, len); flushBytes -= len; } stream.flush(); } }
3.而FileImageOutputStream 的相关代码如下,都很中规中矩没有什么特殊
//初始化 public FileImageOutputStream(File f) throws FileNotFoundException, IOException { this(f == null ? null : new RandomAccessFile(f, "rw")); } //写数据 public void write(int b) throws IOException { flushBits(); // this will call checkClosed() for us raf.write(b); ++streamPos; } //关闭 public void close() throws IOException { super.close(); disposerRecord.dispose(); // this closes the RandomAccessFile raf = null; }
分析源代码:
1.使用了cache的方式对数据读取和写入做了优化,为了防止内存溢出他已512字节读取然后写入输出流。但是当写到ByteArrayOutputStream的时候反而显得笨拙,一个16k的图片走cache的方式需要反复读取32次。
2.使用了普通模式的读取写入数据中规中矩,而读取因为了解文件大小都在16k左右,我采用了一次性读取到内存,所以将File类型的读取到内存再转化base64的时候,只发生了1次磁盘IO
结论:
1. 我们不能被代码外表所欺骗,乍一眼觉得写内存肯定比写File要快。
2.FileCacheImageOutputStream的出发点是好的,分批次数据读取然后写输出流
3.ImageIO.write 下面这出代码针对ByteArrayOutputStream的策略选择有失误:
while (iter.hasNext()) { ImageOutputStreamSpi spi = (ImageOutputStreamSpi)iter.next(); if (spi.getOutputClass().isInstance(output)) { try { //针对ByteArrayOutputStream输出流选择 ImageOutputStream的实现不理想 return spi.createOutputStreamInstance(output, usecache, getCacheDirectory()); } catch (IOException e) { throw new IIOException("Can't create cache file!", e); } } }
磁盘缓存对ByteArray输出流没有效果,该溢出的还是会溢出,还不如直接不使用cache
4.最终我们采用了先写图片到磁盘文件,然后读取文件转base64再返回,接口稳定在了 400ms内
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
扩展资源服务器解决oauth2 性能瓶颈
用户携带token 请求资源服务器 资源服务器拦截器 携带token 去认证服务器 调用tokenstore 对token 合法性校验 资源服务器拿到token,默认只会含有用户名信息 通过用户名调用userdetailsservice.loadbyusername 查询用户全部信息 详细性能瓶颈分析,请参考上篇文章《扩展jwt解决oauth2 性能瓶颈》 本文是针对传统使用UUID token 的情况进行扩展,提高系统的吞吐率,解决性能瓶颈的问题 默认check-token 解析逻辑 RemoteTokenServices 入口 @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>(); fo...
- 下一篇
Logstash读取Kafka数据写入HDFS详解
强大的功能,丰富的插件,让logstash在数据处理的行列中出类拔萃 通常日志数据除了要入ES提供实时展示和简单统计外,还需要写入大数据集群来提供更为深入的逻辑处理,前边几篇ELK的文章介绍过利用logstash将kafka的数据写入到elasticsearch集群,这篇文章将会介绍如何通过logstash将数据写入HDFS 本文所有演示均基于logstash 6.6.2版本 数据收集 logstash默认不支持数据直接写入HDFS,官方推荐的output插件是webhdfs,webhdfs使用HDFS提供的API将数据写入HDFS集群 插件安装 插件安装比较简单,直接使用内置命令即可 # cd /home/opt/tools/logstash-6.6.2 # ./bin/logstash-plugin install logstash-output-webhdfs 配置hosts HDFS集群内通过主机名进行通信所以logstash所在的主机需要配置hadoop集群的hosts信息 # cat /etc/hosts 192.168.107.154 master01 192.168.1...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS关闭SELinux安全模块
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程