前言
最近在玩PT的时候发现磁盘io对pt速度影响巨大,并且qbittorrent对读写比较特殊,有些远程储存方案虽然延迟和大文件读写速度都很可观,但是拿来下PT速度只有3-5mb/s。所以在这里记录一下针对qbit特征的磁盘io测试。
qBittorrent 的磁盘读写活动特征(运行期间)
qBittorrent 是目前最常用的 BT 客户端之一,高性能、稳定、界面好,但它对磁盘 I/O 的压力远超很多人的想象。特别是在 Docker 环境、虚拟机、远程存储(SMB/WebDAV/SFTP/NFS)挂载的情况下,磁盘性能往往成为下载速度的真正瓶颈。
与“顺序写大文件”的常规下载器不同,qBittorrent 的数据流非常复杂,主要表现为:
1. 小块(block)写入
一个 piece 通常是 4MB 或 16MB
但写入磁盘时都是 32KB / 64KB 小块 block
实际磁盘上看到的是大量 16K/32K/64K 的写入
这对 HDD、SMB、WebDAV、网络盘非常痛苦。
2. 随机写入(Random Write)
BitTorrent 协议的数据来自全球各个 peer,顺序不可控:
先来了 piece #800
再来了 piece #12
再来了 piece #1200
磁盘写入位置也是乱跳的。
对于远程文件系统,这意味着:
无法合并写入 → 碎片极重 → IOPS 崩溃
3. 高频读写交替
很多人误以为 BT 下载只会“写入”。实际上还包括:
写完 block → 读回 piece 校验 hash(SHA1/SHA256)
下载恢复 → 读取已存在文件
断点续传 → 读取 piece 状态
校验不通过时 → 重写数据
因此 qBittorrent 的 I/O 模式本质上是:
写 → 读 → CPU(hash)→ 写 → 读 → 写 → …
4. 数据落盘频率高(频繁 flush / fsync)
qBittorrent 会周期性清空内置 disk cache,让数据实际写入磁盘,避免内存丢失风险。
表现为:
频繁 fsync
高频元数据刷新
多线程同时写入同一目录
SMB、WebDAV 对这种行为支持极差,会导致:
速度突然下降到 KB/s
UI 提示“文件错误”或“文件无法写入”
综上所述,qbit的运行模式对远程储存方案非常不友好。SMB、WebDAV、SFTP 的共同弱点是:
对小块随机写效率极差
metadata 操作消耗大
无法处理高并发 I/O
无法对乱序 block 进行预读或合并写
延迟(哪怕 1ms)会被放大成灾难
模拟检测脚本
这里模拟qbittorrent的行为编写了一个对应的磁盘io测试脚本。测试运行环境为debian13,可以指定运行目录。测试完成后会自动删除测试文件,不会占用额外的磁盘空间。
测试主文件:qbittorrent_disk_test.py
#!/usr/bin/env python3
"""
qbittorrent-like disk I/O benchmark
特点:
- 模拟 qBittorrent 对单个大文件的随机小块写 + 读校验行为
- 可指定测试目录
- 结束后自动删除测试文件,不占用额外磁盘空间
- 适用于 Linux / Debian(含 Debian 13)
用法示例:
./qbittorrent_disk_test.py --dir /path/to/mount --size 10G --duration 120
参数说明:
--dir 测试目录(必须)
--size 模拟的“种子文件”大小,支持 K/M/G,默认 10G
--duration 运行时间(秒),默认 120
--piece-size piece 大小(默认 4M)
--block-size 每次写入 block 大小(默认 64K)
--preallocate 预分配整个文件(更接近开启“预分配”的行为,但对远程盘很重)
"""
import argparse
import os
import random
import time
import hashlib
import sys
def parse_size(s: str) -> int:
s = s.strip().upper()
if not s:
raise ValueError("size 为空")
if s[-1] in ("G", "M", "K"):
num = float(s[:-1])
if s[-1] == "G":
return int(num * 1024 ** 3)
if s[-1] == "M":
return int(num * 1024 ** 2)
if s[-1] == "K":
return int(num * 1024)
return int(s)
def human_size(n: float) -> str:
for unit in ["B", "K", "M", "G", "T"]:
if n < 1024 or unit == "T":
return f"{n:.2f}{unit}"
n /= 1024
def run_test(
directory: str,
file_size: int,
duration: int,
piece_size: int,
block_size: int,
preallocate: bool,
):
os.makedirs(directory, exist_ok=True)
test_path = os.path.join(directory, "qbittorrent_io_test.tmp")
total_pieces = max(1, file_size // piece_size)
blocks_per_piece = piece_size // block_size
print(f"测试文件:{test_path}")
print(f"模拟文件大小:{human_size(file_size)}")
print(f"piece 大小:{human_size(piece_size)},每 piece {blocks_per_piece} 个 block")
print(f"block 大小:{human_size(block_size)}")
print(f"piece 总数:{total_pieces}")
print(f"预分配:{'是' if preallocate else '否'}")
print(f"计划运行时长:{duration} 秒")
print("-" * 60)
f = None
bytes_written = 0
bytes_read = 0
pieces_done = 0
start = time.time()
last_report = start
try:
# 以读写方式创建文件
f = open(test_path, "w+b")
if preallocate:
print("正在预分配文件(可能会比较慢,尤其是远程挂载)...")
f.truncate(file_size)
f.flush()
os.fsync(f.fileno())
print("预分配完成。")
while True:
now = time.time()
if now - start >= duration:
break
# 随机选择一个 piece
piece_index = random.randrange(total_pieces)
piece_offset = piece_index * piece_size
# 写入当前 piece 的所有 block(模拟下载这个 piece)
for i in range(blocks_per_piece):
offset = piece_offset + i * block_size
if offset >= file_size:
break
f.seek(offset)
data = os.urandom(block_size)
f.write(data)
bytes_written += len(data)
# 刷新到磁盘(qBittorrent 也是周期性 flush)
f.flush()
os.fsync(f.fileno())
# 随机决定是否对这个 piece 做 hash 校验(模拟 piece 校验)
if random.random() < 0.5:
f.seek(piece_offset)
# 只读一个 piece(或到文件结尾)
to_read = min(piece_size, file_size - piece_offset)
read_data = f.read(to_read)
bytes_read += len(read_data)
# 做一次哈希,纯 CPU,增加一点负载
hashlib.sha1(read_data).digest()
pieces_done += 1
# 每隔几秒打印一次进度
if now - last_report >= 5:
elapsed = now - start
w_speed = bytes_written / elapsed if elapsed > 0 else 0
r_speed = bytes_read / elapsed if elapsed > 0 else 0
print(
f"[{elapsed:6.1f}s] 完成 piece: {pieces_done:6d} | "
f"写入: {human_size(bytes_written)} ({human_size(w_speed)}/s) | "
f"读取: {human_size(bytes_read)} ({human_size(r_speed)}/s)"
)
last_report = now
except KeyboardInterrupt:
print("\n收到中断信号,正在结束测试...")
finally:
if f is not None:
try:
f.close()
except Exception:
pass
# 删除测试文件
try:
if os.path.exists(test_path):
os.remove(test_path)
print(f"已删除测试文件:{test_path}")
except Exception as e:
print(f"删除测试文件失败:{e}", file=sys.stderr)
elapsed = time.time() - start
if elapsed <= 0:
elapsed = 1
print("-" * 60)
print(f"实际运行时间:{elapsed:.1f} 秒")
print(f"总写入:{human_size(bytes_written)},平均写入速度:{human_size(bytes_written / elapsed)}/s")
print(f"总读取:{human_size(bytes_read)},平均读取速度:{human_size(bytes_read / elapsed)}/s")
print("测试完成。")
def main():
parser = argparse.ArgumentParser(
description="模拟 qBittorrent 行为的磁盘 I/O 测试脚本(随机小块读写 + piece 校验)"
)
parser.add_argument(
"--dir",
required=True,
help="测试目录(例如 /mnt/storagebox 或 /data)",
)
parser.add_argument(
"--size",
default="10G",
help="模拟种子文件大小,支持 K/M/G(默认 10G)",
)
parser.add_argument(
"--duration",
type=int,
default=120,
help="测试时长(秒),默认 120",
)
parser.add_argument(
"--piece-size",
default="4M",
help="piece 大小,默认 4M",
)
parser.add_argument(
"--block-size",
default="64K",
help="每次读写的 block 大小,默认 64K",
)
parser.add_argument(
"--preallocate",
action="store_true",
help="预分配整个文件(类似开启 qBittorrent 预分配选项)",
)
args = parser.parse_args()
try:
file_size = parse_size(args.size)
piece_size = parse_size(args.piece_size)
block_size = parse_size(args.block_size)
except Exception as e:
print(f"参数解析错误: {e}", file=sys.stderr)
sys.exit(1)
if piece_size <= 0 or block_size <= 0:
print("piece-size 和 block-size 必须大于 0", file=sys.stderr)
sys.exit(1)
if piece_size % block_size != 0:
print("piece-size 必须是 block-size 的整数倍", file=sys.stderr)
sys.exit(1)
if file_size <= 0:
print("size 必须大于 0", file=sys.stderr)
sys.exit(1)
run_test(
directory=args.dir,
file_size=file_size,
duration=args.duration,
piece_size=piece_size,
block_size=block_size,
preallocate=args.preallocate,
)
if __name__ == "__main__":
main()保存脚本并赋予执行权限
chmod +x qbittorrent_disk_test.py指定测试目录进行测试(支持远程挂载目录)
# 模拟一个 10G 的大种子,运行 2 分钟
./qbittorrent_disk_test.py --dir /mnt/storagebox --size 10G --duration 120常用参数例子:
降低测试体量(比如只搞 2G、跑 60 秒):
./qbittorrent_disk_test.py --dir /mnt/storagebox --size 2G --duration 60调小 piece / block(比如模拟 2M piece、32K block):
./qbittorrent_disk_test.py \
--dir /mnt/storagebox \
--size 5G \
--duration 90 \
--piece-size 2M \
--block-size 32K模拟“开启预分配”的情况(会对远程盘更狠一点):
./qbittorrent_disk_test.py \
--dir /mnt/storagebox \
--size 10G \
--duration 120 \
--preallocate
评论