DISK 100% BUSY,谁造成的?(ok)

本文介绍如何使用iostat、pidstat及iotop等工具追踪Linux系统中导致I/O繁忙的具体进程,并通过SystemTap深入监控特定硬盘设备上的I/O活动。

iostat等命令看到的是系统级的统计,比如下例中我们看到/dev/sdb很忙,如果要追查是哪个进程导致的I/O繁忙,应该怎么办?

进程的内核数据结构中包含了I/O数量的统计:

可以直接在 /proc/<pid>/io 中看到:

# cat /proc/3088/io
rchar: 125119 //在read(),pread(),readv(),sendfile等系统调用中读取的字节数
wchar: 632    //在write(),pwrite(),writev(),sendfile等系统调用中写入的字节数
syscr: 111    //调用read(),pread(),readv(),sendfile等系统调用的次数
syscw: 79     //调用write(),pwrite(),writev(),sendfile等系统调用的次数
read_bytes: 425984 //进程读取的物理I/O字节数,包括mmap pagein,在submit_bio()中统计的
write_bytes: 0     //进程写出的物理I/O字节数,包括mmap pageout,在submit_bio()中统计的
cancelled_write_bytes: 0 //如果进程截短了cache中的文件,事实上就减少了原本要发生的写I/O

我们关心的是实际发生的物理I/O,从上面的注释可知,应该关注 read_bytes 和 write_bytes。请注意这都是历史累计值,从进程开始执行之初就一直累加。如果要观察动态变化情况,可以使用 pidstat 命令,它就是利用了/proc/<pid>/io 中的原始数据计算单位时间内的增量:

另外还有一个常用的命令 iotop 也可以观察进程的动态I/O:

Actual DISK READ:       3.31 M/s | Actual DISK WRITE:       0.00 B/s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND          
14772 be/4 root        3.31 M/s    0.00 B/s  0.00 % 61.99 % dd if=/de~lag=direct
    1 be/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % systemd -~rialize 24
    2 be/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % [kthreadd]
...

pidstat 和 iotop 也有不足之处,它们无法具体到某个硬盘设备,如果系统中有很多硬盘设备,都在忙,而我们只想看某一个特定的硬盘的I/O来自哪些进程,这两个命令就帮不上忙了。怎么办呢?可以用上万能工具SystemTap。比如:我们希望找出访问/dev/sdb的进程,可以用下列脚本,它的原理是对submit_bio下探针:

#! /usr/bin/env stap
 
global device_of_interest
 
probe begin {
  device_of_interest = $1
  printf ("device of interest: 0x%x\n", device_of_interest)
}
 
probe kernel.function("submit_bio")
{
  dev = $bio->bi_bdev->bd_dev
  if (dev == device_of_interest)
    printf ("[%s](%d) dev:0x%x rw:%d size:%d\n",
            execname(), pid(), dev, $rw, $bio->bi_size)
}

这个脚本需要在命令行参数中指定需要监控的硬盘设备号,得到这个设备号的方法如下:

# ll /dev/sdb
brw-rw----. 1 root disk 8, 16 Oct 24 15:52 /dev/sdb
 
Major number(12-bit):  8 i.e. 0x8
Minor number(20-bit): 16 i.e. 0x00010
合在一起得到设备号: 0x800010  注意是十六进制

执行脚本,我们看到:

# ./dev_task_io.stp 0x800010
device of interest: 0x800010
[dd](31202) dev:0x800010 rw:0 size:512
[dd](31202) dev:0x800010 rw:0 size:512
[dd](31202) dev:0x800010 rw:0 size:512
[dd](31202) dev:0x800010 rw:0 size:512
[dd](31202) dev:0x800010 rw:0 size:512
...

结果很令人满意,我们看到是进程号为31202的dd命令在对/dev/sdb进行读操作。

相关内容:

Linux的设备管理是和文件系统紧密结合的,把设备和文件关联起来,这样系统调用可以直接用操作文件一样的方法来操作设备。

各种设备都以文件的形式存放在/dev目录下,称为设备文件。

应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。

为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。

主设备号用来区分不同类型的设备,而次设备号用来区分同一类型内的多个设备(及其设备分区)。

查看主设备号:  cat /proc/devices
查看当前设备的主次设备号: ls -l /dev

一个Linux系统,当前所有注册设备的主设备号可以通过/proc接口查看:

[root@localhost lenky]# cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 14 sound
 21 sg
 29 fb
 99 ppdev
116 alsa
128 ptm
136 pts
162 raw
180 usb
189 usb_device
202 cpu/msr
203 cpu/cpuid
251 hidraw
252 usbmon
253 bsg
254 rtc

Block devices:
  1 ramdisk
  2 fd
259 blkext
  7 loop
  8 sd
  9 md
 11 sr
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
253 device-mapper
254 mdp
[root@localhost lenky]#

字符设备与块设备的主设备号并不冲突,所有两个都可以有主设备号为1的设备,如果要继续查看次设备号,那么可以通过直接ls -l来查看,比如查看主设备号为8的设备的次设备号:

[root@localhost lenky]# ls -Rl /dev/* | grep " 8,"
brw-rw----. 1 root disk      8,   0 Jan 12 06:24 /dev/sda
brw-rw----. 1 root disk      8,   1 Jan 12 06:24 /dev/sda1
brw-rw----. 1 root disk      8,   2 Jan 12 06:24 /dev/sda2
brw-rw----. 1 root disk      8,  16 Jan 12 06:25 /dev/sdb
brw-rw----. 1 root disk      8,  17 Jan 12 06:25 /dev/sdb1
brw-rw----. 1 root disk      8,  32 Jan 12 06:29 /dev/sdc
brw-rw----. 1 root disk      8,  33 Jan 12 06:29 /dev/sdc1
brw-rw----. 1 root disk      8,  34 Jan 12 06:29 /dev/sdc2
brw-rw----. 1 root disk      8,  35 Jan 12 06:29 /dev/sdc3
[root@localhost lenky]#

上面的0,1,2,16,17等都是次设备号,用于区分标记各个sd硬盘或分区。查看系统所有的块设备:

[root@localhost lenky]# grep ^ /sys/class/block/*/dev
/sys/class/block/dm-0/dev:253:0
/sys/class/block/dm-1/dev:253:1
/sys/class/block/dm-2/dev:253:2
/sys/class/block/fd0/dev:2:0
/sys/class/block/loop0/dev:7:0
/sys/class/block/loop1/dev:7:1
/sys/class/block/loop2/dev:7:2
/sys/class/block/loop3/dev:7:3
/sys/class/block/loop4/dev:7:4
/sys/class/block/loop5/dev:7:5
/sys/class/block/loop6/dev:7:6
/sys/class/block/loop7/dev:7:7
/sys/class/block/ram0/dev:1:0
/sys/class/block/ram10/dev:1:10
/sys/class/block/ram11/dev:1:11
/sys/class/block/ram12/dev:1:12
/sys/class/block/ram13/dev:1:13
/sys/class/block/ram14/dev:1:14
/sys/class/block/ram15/dev:1:15
/sys/class/block/ram1/dev:1:1
/sys/class/block/ram2/dev:1:2
/sys/class/block/ram3/dev:1:3
/sys/class/block/ram4/dev:1:4
/sys/class/block/ram5/dev:1:5
/sys/class/block/ram6/dev:1:6
/sys/class/block/ram7/dev:1:7
/sys/class/block/ram8/dev:1:8
/sys/class/block/ram9/dev:1:9
/sys/class/block/sda1/dev:8:1
/sys/class/block/sda2/dev:8:2
/sys/class/block/sda/dev:8:0
/sys/class/block/sdb1/dev:8:17
/sys/class/block/sdb/dev:8:16
/sys/class/block/sdc1/dev:8:33
/sys/class/block/sdc2/dev:8:34
/sys/class/block/sdc3/dev:8:35
/sys/class/block/sdc/dev:8:32
/sys/class/block/sr0/dev:11:0
[root@localhost lenky]#

关于每个主设备号:次设备号对应设备的功能在Linux帮助文档里可以找到:http://lxr.linux.no/#linux+v2.6.38.8/Documentation/devices.txt

在内核2.6.9之后,Linux系统上出现了一种名为device-mapper的存储映射机制,这种机制的作用简单来说就是给用户提供简单方便而又丰富的存储管理接口,在这种机制以及相关工具的帮助下,用户能够方便的自定义存储资源管理策略。

通过一些映射规则,device-mapper机制能够从原有的物理磁盘或逻辑磁盘中划分映射出新的逻辑磁盘,可以看到这是一个递归的映射机制,理论上可无限迭代。举个例子,系统有物理磁盘A和B,从物理磁盘A中映射出新的逻辑磁盘C、D、E,从物理磁盘B中映射出新的逻辑磁盘F、G,又可以从物理磁盘A和逻辑磁盘F中映射出新的逻辑磁盘H,等等。关于这方面,请参考:http://www.ibm.com/developerworks/cn/linux/l-devmapper/http://sources.redhat.com/dm/等资源,不管是原物理磁盘还是通过device-mappe机制映射出来的新逻辑磁盘,在Linux操作系统看来都一样,一切皆文件,复杂逻辑被隔离在底部。

[root@localhost lenky]# ls -Rl /dev/* | grep " 8,"
brw-rw----. 1 root disk      8,   0 Jan 12 06:24 /dev/sda
brw-rw----. 1 root disk      8,   1 Jan 12 06:24 /dev/sda1
brw-rw----. 1 root disk      8,   2 Jan 12 06:24 /dev/sda2

一般说的各个分区相加等于硬盘,有个隐含说明就是硬盘内各个分区的硬盘存储空间容量相加等于硬盘的硬盘存储空间容量。

 

一个硬盘有一个描述硬盘的信息,而一个分区有一描述分区的信息,将硬盘内各个分区的描述分区的信息拼接在一起也得不到关于硬盘的信息,所以给硬盘配上一个次设备号是有必要不多余的。

 

[root@localhost lenky]# ls -Rl /dev/* | grep " 8,"
brw-rw----. 1 root disk      8,   0 Jan 12 06:24 /dev/sda
brw-rw----. 1 root disk      8,   1 Jan 12 06:24 /dev/sda1
brw-rw----. 1 root disk      8,   2 Jan 12 06:24 /dev/sda2
brw-rw----. 1 root disk      8,  16 Jan 12 06:25 /dev/sdb
brw-rw----. 1 root disk      8,  17 Jan 12 06:25 /dev/sdb1
brw-rw----. 1 root disk      8,  32 Jan 12 06:29 /dev/sdc
brw-rw----. 1 root disk      8,  33 Jan 12 06:29 /dev/sdc1
brw-rw----. 1 root disk      8,  34 Jan 12 06:29 /dev/sdc2
brw-rw----. 1 root disk      8,  35 Jan 12 06:29 /dev/sdc3

我们看到/dev/sd*设备名的设备类型(即指的是IDE硬盘),这里有三个不同的IDE硬盘,其主设备号以及其分区的主设备号都是8。

转载于:https://www.cnblogs.com/felixzh/p/9035103.html

<think>我们正在处理多文件上传的并发配置问题。根据用户需求,我们需要配置Spring Boot应用以支持多文件上传的并发处理。同时,参考站内引用,我们可以结合线程池配置、异步处理等机制来优化并发性能。 关键点: 1. 使用合适的MultipartResolver(如StandardServletMultipartResolver)以确保线程安全。 2. 配置异步处理(如使用@Async)来分离文件上传的处理逻辑,避免阻塞请求线程。 3. 调整线程池配置以控制并发上传的处理能力。 4. 设置合理的文件上传参数(如最大文件大小、请求大小等)。 步骤: 1. 配置Multipart属性(application.yml) 2. 启用异步处理并配置线程池 3. 编写控制器,使用异步方法处理上传 详细步骤: 1. 在application.yml中配置multipart属性(使用StandardServletMultipartResolver,这是Spring Boot默认的,无需显式声明bean): ```yaml spring: servlet: multipart: enabled: true max-file-size: 10MB # 单个文件最大大小 max-request-size: 100MB # 整个请求最大大小(多文件总和) location: /tmp # 临时目录,确保有写权限 file-size-threshold: 2MB # 超过这个大小的文件将写入临时目录 resolve-lazily: false # 是否延迟解析,高并发下建议false(立即解析) ``` 注意:`resolve-lazily`如果设置为true,则会在第一次访问MultipartFile时才解析,但这可能导致并发解析问题,所以建议false。 2. 启用异步并配置线程池: 在Spring Boot主类或配置类上添加`@EnableAsync`注解。 然后,配置一个用于处理上传的线程池: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync public class AsyncConfig { @Bean(name = "fileUploadTaskExecutor") public Executor fileUploadTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 核心线程数 executor.setMaxPoolSize(50); // 最大线程数 executor.setQueueCapacity(100); // 队列容量 executor.setThreadNamePrefix("fileUpload-"); executor.initialize(); return executor; } } ``` 3. 编写控制器,使用异步方法处理上传: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.concurrent.CompletableFuture; @RestController public class FileUploadController { private final FileStorageService storageService; @Autowired public FileUploadController(FileStorageService storageService) { this.storageService = storageService; } @PostMapping("/upload") @Async("fileUploadTaskExecutor") // 指定使用我们配置的线程池 public CompletableFuture<String> uploadFiles(@RequestParam("files") List<MultipartFile> files) { try { for (MultipartFile file : files) { storageService.store(file); } return CompletableFuture.completedFuture("Upload successful!"); } catch (Exception e) { return CompletableFuture.failedFuture(e); } } } ``` 注意:这里我们使用了`@Async`注解,并指定了线程池的名称。这样,每个上传请求都会在一个单独的线程中处理,从而不会阻塞Tomcat的请求线程。 4. 文件存储服务(FileStorageService)示例: ```java import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @Service public class FileStorageService { private final Path rootLocation = Paths.get("upload-dir"); public void store(MultipartFile file) throws IOException { if (file.isEmpty()) { throw new RuntimeException("Failed to store empty file."); } // 创建目标文件(如果目录不存在则创建) if (!Files.exists(rootLocation)) { Files.createDirectories(rootLocation); } Path destinationFile = rootLocation.resolve( Paths.get(file.getOriginalFilename())) .normalize().toAbsolutePath(); // 将文件保存到目标位置 file.transferTo(destinationFile); } } ``` 5. 调整Servlet容器线程池(可选): 在application.yml中配置Tomcat(或其他容器)的最大线程数,以处理更多的并发请求: ```yaml server: tomcat: max-threads: 200 # 默认是200,根据实际情况调整 min-spare-threads: 10 ``` 6. 注意事项: - 临时目录(`spring.servlet.multipart.location`)应设置在一个I/O性能较好的位置,并且有足够的空间。 - 监控线程池的使用情况,避免任务堆积导致内存溢出。 - 文件存储服务应确保线程安全,避免多个线程同时写入同一文件(如果文件名相同,需要处理冲突)。 7. 高并发优化建议: - 使用分布式文件系统(如FastDFS、MinIO)来存储文件,避免单机I/O瓶颈。 - 如果文件处理逻辑复杂,可以将文件保存操作放入消息队列,由后台服务异步处理。 通过以上配置,Spring Boot应用可以支持多文件上传的并发处理,并且通过异步机制和线程池配置,能够有效利用系统资源,提高并发能力。 相关问题: 1. 如何监控文件上传线程池的运行状态? 2. 多文件上传时如何避免文件名冲突? 3. 如何实现文件上传的进度条? 4. 在分布式环境中如何保证文件上传的并发处理?</think>在 Spring Boot 应用中配置多文件上传的并发处理机制,需要结合**线程池优化**、**异步处理**和**资源管理**策略。以下是具体配置方法: --- ### 一、基础配置:文件上传参数优化 在 `application.yml` 中配置核心参数: ```yaml spring: servlet: multipart: enabled: true max-file-size: 10MB # 单个文件最大尺寸 max-request-size: 100MB # 整个请求最大尺寸(多文件总和) location: /tmp/uploads # 临时存储目录(SSD 推荐) file-size-threshold: 2MB # 超过此大小写入磁盘 server: tomcat: max-threads: 200 # 最大工作线程数 max-connections: 1000 # 最大连接数 ``` > **关键点**: > - `location` 使用独立磁盘分区避免 I/O 竞争 > - `file-size-threshold` 平衡内存与磁盘使用[^1] --- ### 二、线程池配置:异步处理上传 1. **启用异步支持** 在主类添加注解: ```java @SpringBootApplication @EnableAsync // 启用异步处理 public class Application { ... } ``` 2. **自定义线程池** ```java @Configuration public class AsyncConfig { @Bean("fileUploadExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 核心线程数 executor.setMaxPoolSize(50); // 最大线程数 executor.setQueueCapacity(100); // 队列容量 executor.setThreadNamePrefix("UploadThread-"); executor.initialize(); return executor; } } ``` --- ### 三、控制器实现:并发处理多文件 ```java @RestController public class UploadController { @PostMapping("/upload") @Async("fileUploadExecutor") // 指定自定义线程池 public CompletableFuture<ResponseEntity<String>> uploadFiles( @RequestParam("files") MultipartFile[] files) { return CompletableFuture.supplyAsync(() -> { Arrays.stream(files).parallel().forEach(file -> { // 并发处理每个文件(线程安全操作) storageService.saveFile(file); }); return ResponseEntity.ok("Upload completed"); }); } } ``` > **注意**: > - 使用 `parallel()` 开启多文件并行处理 > - 存储服务 `saveFile()` 需实现线程安全 --- ### 四、存储服务优化:避免资源竞争 ```java @Service public class StorageService { // 使用线程安全的文件名生成 private static final AtomicInteger counter = new AtomicInteger(0); @Async("fileUploadExecutor") public void saveFile(MultipartFile file) { String safeFilename = "file_" + counter.incrementAndGet() + "_" + file.getOriginalFilename(); Path path = Paths.get("/data/uploads", safeFilename); try (InputStream is = file.getInputStream()) { Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING); } // 添加分布式锁处理共享资源(可选) } } ``` > **关键优化**: > - 使用 `AtomicInteger` 避免文件名冲突 > - 添加 `@Async` 实现二级异步处理 --- ### 五、熔断与限流(高并发场景) 集成 Resilience4j 防止系统过载: ```yaml resilience4j: ratelimiter: instances: uploadLimiter: limit-for-period: 50 # 每秒50个请求 limit-refresh-period: 1s timeout-duration: 100ms # 超时快速失败 ``` ```java @RateLimiter(name = "uploadLimiter") @PostMapping("/upload") public void uploadFiles(...) { ... } ``` --- ### 六、监控配置(可选) 在 `pom.xml` 添加: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> ``` 监控指标: - `http.server.requests` : 请求吞吐量 - `tomcat.threads.busy` : 线程池使用率 - `disk.free` : 磁盘空间 --- ### 常见问题解决 1. **临时文件未清理** 添加定时任务清理 `/tmp/uploads`: ```java @Scheduled(fixedRate = 3600000) // 每小时清理 public void cleanTempDir() { FileSystemUtils.deleteRecursively(new File("/tmp/uploads")); } ``` 2. **内存溢出** 调整 JVM 参数: ```bash java -jar -Xmx2g -XX:MaxMetaspaceSize=512m app.jar ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值