接口机磁盘读写极度不均衡的原因分析
- 背景
- 正文
- 数据写入磁盘的过程
- NIFI使用本地磁盘的特性
- PageFaults带出的缓存使用
- 问题的最终定论
背景
在进行服务器接口机的资源梳理时,发现一个现象——接口服务器的磁盘IO中,数据写入的流量远大于读取流量,实时的监控图表如下:
实时在服务器使用iotop命令查看读写情况,也能看到差异是非常大的:
Total DISK READ : 75.31 K/s | Total DISK WRITE : 77.38 M/s
Actual DISK READ: 96.82 K/s | Actual DISK WRITE: 1117.56 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
19969 be/3 root 0.00 B/s 1061.48 K/s 0.00 % 78.56 % [jbd2/sdb1-8]
104428 be/4 root 39.45 K/s 0.00 B/s 0.00 % 75.63 % [kworker/u449:0]
158908 be/4 user 35.86 K/s 0.00 B/s 0.00 % 64.11 % sshd: user@internal-sftp
350 be/4 root 0.00 B/s 25.73 M/s 0.00 % 57.42 % [kswapd0]
178091 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.03 % [kworker/35:2]
226695 be/4 user 0.00 B/s 3.59 K/s 0.00 % 0.01 % java -Dzookeeper.log.dir=/data1/app/zookeeper~/zookeeper/zookeeper-3.5.9/bin/../conf/zoo.cfg
18862 be/4 user 0.00 B/s 50.55 M/s 0.00 % 0.01 % java -classpath /data1/app/nifi/nifi-1.16.3/.~app/nifi/nifi-1.16.3/logs org.apache.nifi.NiFi
19999 be/3 root 0.00 B/s 39.45 K/s 0.00 % 0.00 % auditd
138967 be/4 root 0.00 B/s 3.59 K/s 0.00 % 0.00 % rsyslogd -n
基于此问题,还是需要进行一下分析,理解一下为什么会出现这个现象。
正文
数据写入磁盘的过程
针对这个现象,第一个切入点其实不是进程的io占用,因为监控和iotop的表现基本一致,说明实际是有这么高的数据写入需求;
然后另一个需要注意的特征点是,在这样巨大差异的场景下,业务数据也是正常传输的,因此,首先要去想清楚,写入的数据和读取得数据是怎么发生的;
根据iotop的信息显示,发现pid为18862的进程会长期占用大量的Write资源:
[root@hostname ~]
user 18862 18785 99 2022 ? 660-04:39:50 /usr/jdk64/jdk1.8.0_112/bin/java -classpath /data1/app/nifi/nifi-1.16.3/./conf:/data1/app/nifi/nifi-1.16.3/./lib/logback-core-1.2.11.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-properties-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/logback-classic-1.2.11.jar:/data1/app/nifi/nifi-1.16.3/./lib/jetty-schemas-5.2.jar:/data1/app/nifi/nifi-1.16.3/./lib/log4j-over-slf4j-1.7.36.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-framework-api-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-nar-utils-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-runtime-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/jcl-over-slf4j-1.7.36.jar:/data1/app/nifi/nifi-1.16.3/./lib/slf4j-api-1.7.36.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-property-utils-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/javax.servlet-api-3.1.0.jar:/data1/app/nifi/nifi-1.16.3/./lib/jul-to-slf4j-1.7.36.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-server-api-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-api-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-stateless-bootstrap-1.16.3.jar:/data1/app/nifi/nifi-1.16.3/./lib/nifi-stateless-api-1.16.3.jar -Dorg.apache.jasper.compiler.disablejsr199=true -Xmx48g -Xms48g -Dcurator-log-only-first-connection-issue-as-error-level=true -Djavax.security.auth.useSubjectCredsOnly=true -Djava.security.egd=file:/dev/urandom -Dzookeeper.admin.enableServer=false -Dsun.net.http.allowRestrictedHeaders=true -Djava.net.preferIPv4Stack=true -Djava.awt.headless=true -XX:+UseG1GC -Djava.protocol.handler.pkgs=com.dtsw -Dnifi.properties.file.path=/data1/app/nifi/nifi-1.16.3/./conf/nifi.properties -Dnifi.bootstrap.listen.port=22995 -Dapp=NiFi -Dorg.apache.nifi.bootstrap.config.log.dir=/data1/app/nifi/nifi-1.16.3/logs org.apache.nifi.NiFi
根据ps查询的结果,这是一个nifi进程, 那么它应该就是产生数据流向的主要进程了;
接下来从这个进程出发,我们继续探寻IO差异的原因;
在这里,我们首先要知道Linux进行数据读写时的流程,这将有助于我们进一步定位这个问题,首先,让我们思考一个问题:
当我们在Linux写入一个文件的时候,文件是直接写入磁盘的吗?
既然我问出来这个问题,那么你一定会回答“不是”,既然不是的话,这个流程应该是怎样的?
实际上,对于操作系统的操作而言,提高文件写入性能的一个简单方法是让操作系统缓存数据,这个时候会告诉应用程序文件是写入了的,然后再异步的执行写入操作;
这样如果同时有其他磁盘活动将会非常高效,操作系统可以优先读取并稍后执行写入操作。并且在极端情况下还可以完全消除实际写入的需要,例如,在临时文件很快被删除的情况下。
当然,像这样的缓存也有缺点,我们可以想象到,极端情况下,我还没有在终端确认保存操作,那么有些数据可能在真正保存之前就丢失了。
如果编辑器告诉用户写入成功,但文件实际上不在磁盘上,在业务逻辑上一定是不正确的。这就是为什么会有fsync()之类的系统调用,在向用户报告写入成功之前,文件操作程序可以使用它来确保数据正常。
NIFI使用本地磁盘的特性
ok,了解了文件的读写大致流程,我们现在可以顺理成章的给出一个能够和nifi特性匹配的推测:
在配置了本地磁盘作为Content Repository
之后,当nifi从A服务器拉取一个数据的时候,在本地会落盘一次,于是数据会写入缓存,下游的nifi推送组件(或者别的需要使用这个文件的流程)可以直接从缓存中拿到这个文件,而不需要产生新的IO操作;
这里的大前提就是nifi的nifi.content.repository.implementation
配置项使用的默认值org.apache.nifi.controller.repository.FileSystemRepository
:
nifi.content.repository.implementation=org.apache.nifi.controller.repository.FileSystemRepository
nifi.content.claim.max.appendable.size=50 MB
nifi.content.repository.directory.content1=/data1/app/nifi/nifi_repository/content_repository
nifi.content.repository.directory.content2=/data2/app/nifi/nifi_repository/content_repository
个别的环境可能这个值配置成了VolatileContentRepository,按照现在的推测,这种配置下不会出现write远大于read的情况,因为write本身也不会落盘,所有操作都在内存中进行;当然,如果出发了极端操作,也可能会产生上述现象,具体的需要进一步测试,暂且不提;
PageFaults带出的缓存使用
现在我们有了一个大概的推测,接下来我使用pidstat命令针对nifi的进程进行进一步的排查:
这里注意几个值,一个是minflt/s
还有一个是kB_rd/s
;
后者可以很好的理解,是从磁盘读的数据量,单位kB,和监控的情况保持一致,基本没有读,只有写(kB_wr/s
);
那么minflt
这个值为什么要关注呢?这个指标的含义是什么?
minflt
即minor page faults
,通过man文档我们可以查询到这个指标的解释:
Total number of minor faults the task has made per second, those which have not required loading a memory page from disk.
直译过来就是每秒发生的次要错误总数,这些错误不需要从磁盘加载内存页;这里的错误指的就是页面错误,想要理解这个指标的含义,我们就要多了解一下页面错误是什么。
我们知道,操作系统使用分页在主存储器和辅助存储器之间传输数据(主存储器就是RAM内存,CPU可以直接访问,辅助存储器通俗来说就是我们常说的磁盘)以实现高效的内存管理;
页(内存/虚拟页)是正在运行的进程的一部分,表示一个逻辑内存单元;内存中包含单个进程页的物理部分称为帧;所有帧都是固定长度,允许非连续(非共享)分配物理内存地址空间;
而我们所说的页错误(page fault)是由内存管理单元引发的异常,当进程需要访问其地址空间内的数据时,它无法加载物理内存;
异常通常指示计算机在虚拟内存中找到该数据块,这样它就可以从存储设备发送到物理内存;page fault其实很常见,通过提高程序的内存分配,通常有助于提高性能;
minor page faults,也称为软错误,发生在内存页由多个程序共享时,其中一些程序已经将该页带到主内存中;也就是说,在访问一个地址时,与之绑定的虚拟内存空间对应的地址空间已经被内核加载到了Page Cache中,那么此时只需要把该Page映射到vma中即可;
现在,让我们回到pidstat的跟踪排查结果上来,minflt发生的非常频繁,说明需要操作的数据总是能从缓存中找到,所以,实际上并不会产生大量的磁盘io读操作。
问题的最终定论
现在,我们需要使用一些小手段,手动的介入缓存的使用,尽可能的在数据读入缓存后清理除去,我们使用sync命令调用响应的页回刷的函数,同时观察到磁盘的读操作一下变多了:
这样看可能并不直观,让我们结合已有的监控进行观察:
尽管只有很短的时间,但是我们可以看到磁盘的读写流量基本是持平了;
这就是因为我们手动介入,尽可能的在nifi将数据拉取过来时,从内存中把对应的数据刷写磁盘并清空缓存,导致下游的读取需要再走一次磁盘;由此产生了大量的read流量,并且,随着程序运行,缓存会逐渐增多,所以后续read流量越来越少,逐渐变成之前的状态了。
极端状态下,如果没有缓存机制,真正意义上read和write是持平的。