Appearance
Linux I/O 性能详解
I/O(Input/Output)性能问题通常表现为:应用响应变慢、请求延迟升高、CPU 使用率不高但 Load Average 很高、磁盘吞吐达到上限、系统频繁进入 I/O wait,或者数据库出现大量慢查询。
I/O 性能优化不能只看某一个指标。一个完整的分析过程应该回答以下问题:
- 当前是读 I/O、写 I/O,还是读写混合?
- 瓶颈位于应用、文件系统、块设备、存储介质,还是网络存储?
- 当前主要受吞吐量、IOPS、延迟、队列深度还是带宽限制?
- I/O 是顺序访问还是随机访问?请求大小是否合理?
- 是设备真的繁忙,还是应用在锁、内存回收、网络或 CPU 上等待?
- 修改参数后,性能是否改善,是否引入了数据安全风险?
本文以 Linux 服务器为背景,从 I/O 路径、性能指标、工具使用和实战排查几个方面进行说明。
一、Linux I/O 路径
一次普通文件读取并不是应用直接从磁盘读取数据,而是经过多个层次:
text
应用程序
│ read / pread / readv / mmap
▼
系统调用层
│
▼
VFS(虚拟文件系统)
│
├── Page Cache(页缓存)
│ ├── 命中:直接返回内存中的数据
│ └── 未命中:向文件系统发起读取
│
▼
具体文件系统(ext4 / XFS / Btrfs ...)
│
▼
块层(bio / request / I/O scheduler)
│
▼
设备驱动
│
▼
物理设备(HDD / SSD / NVMe / SAN / 网络盘)写入路径还可能经过 Page Cache、文件系统日志、设备缓存和持久化介质,因此“系统调用返回”不一定等于“数据已经安全落盘”。
1. VFS
VFS 为不同文件系统提供统一的接口。应用调用 open()、read()、write()、fsync() 等系统调用时,首先进入 VFS,再由具体文件系统完成后续工作。
VFS 负责抽象出常见对象:
- inode:文件元数据和数据块索引;
- dentry:目录项与路径解析缓存;
- file:进程打开文件后的内核对象;
- superblock:文件系统整体信息。
2. Page Cache
Page Cache 是 Linux 用于缓存文件内容的内存区域。文件第一次读取时,数据通常从设备进入 Page Cache;后续访问如果命中缓存,就不需要再次访问磁盘。
text
第一次读取:磁盘 → Page Cache → 应用
再次读取: Page Cache → 应用因此,使用 free 观察到的缓存内存并不一定是“浪费的内存”。Linux 会在需要时回收大部分可回收缓存。
查看内存和缓存:
bash
free -h
cat /proc/meminfo | grep -E 'Cached|Buffers|Dirty|Writeback'常见字段:
Cached:文件页缓存;Buffers:块设备元数据等缓冲区;Dirty:已修改但尚未写回设备的页;Writeback:正在写回设备的页。
3. 块层
当文件系统需要从块设备读取数据时,会把请求组织成 bio,再由块层合并、排序或调度,最终提交给设备驱动。
块层常见影响因素:
- 请求大小;
- 请求是否连续;
- 队列深度;
- I/O 调度器;
- 多个进程之间的请求竞争;
- 设备是否支持并发 I/O。
4. 设备层
设备层是最终执行 I/O 的地方。不同介质的能力差异很大:
| 介质 | 典型特点 | 常见瓶颈 |
|---|---|---|
| HDD | 顺序吞吐尚可,随机访问延迟高 | 寻道、随机 IOPS |
| SATA SSD | 延迟低于 HDD,带宽受 SATA 限制 | SATA 带宽、写放大、垃圾回收 |
| NVMe SSD | 高并发、低延迟、高 IOPS | PCIe 带宽、队列、温度、介质写入寿命 |
| SAN / NAS | 通过网络或存储网络访问 | 网络延迟、共享存储竞争、服务端队列 |
| 云盘 | 由云平台提供虚拟块设备 | IOPS/吞吐配额、突发额度、租户竞争 |
二、I/O 性能的核心指标
1. 吞吐量
吞吐量表示单位时间传输的数据量,通常以 MB/s 或 GB/s 表示。
text
吞吐量 = 单位时间完成的字节数吞吐量适合衡量大文件顺序读写、日志批量写入、备份归档、数据导入导出以及视频或大对象传输。
吞吐量高不代表延迟低。一个设备可能在顺序读时达到很高吞吐量,但随机小块读的延迟仍然很高。
2. IOPS
IOPS(Input/Output Operations Per Second)表示每秒完成的 I/O 操作次数。
text
IOPS ≈ 吞吐量 / 单次 I/O 大小例如,设备以 100 MB/s 处理 4 KB 请求时:
text
100 × 1024 KB / 4 KB ≈ 25,600 IOPSIOPS 常用于数据库、键值存储和小文件服务等随机访问场景。
3. 延迟
延迟是一次 I/O 从提交到完成所经历的时间,通常以微秒或毫秒表示。需要区分应用端延迟、系统调用延迟、块设备延迟和网络存储服务延迟。
平均延迟可能掩盖长尾问题。生产环境更应该关注 P95、P99 或 P99.9 延迟。
4. 队列深度
队列深度表示同时等待处理的 I/O 请求数量。队列深度过低,设备并行能力没有被利用;队列深度过高,则可能导致延迟上升。
对于 NVMe 和云盘等高并发设备,适当的队列深度可以提高吞吐量;对于延迟敏感服务,盲目增加队列深度可能适得其反。
5. 利用率
%util 表示设备在采样期间处于处理 I/O 状态的时间比例。它可以帮助判断设备是否繁忙,但不能简单地认为 %util=100% 就代表设备已经无法继续处理请求。
现代多队列设备可以在 %util 接近 100% 时仍有较好的吞吐能力,因此还应结合 await、aqu-sz、IOPS、吞吐量和应用延迟共同判断。
6. I/O wait
%iowait 表示 CPU 空闲期间,系统中存在未完成 I/O 的时间比例。它不是“CPU 正在等待磁盘”的精确计时器。
高 I/O wait 通常说明系统中存在 I/O 等待,但不一定能证明某个进程就是根因。需要配合进程级工具、设备级工具和系统调用跟踪进一步定位。
三、顺序 I/O 与随机 I/O
1. 顺序 I/O
顺序 I/O 的访问位置连续或接近连续,设备可以减少寻址和合并请求的开销,适合大文件读取、备份恢复、日志追加写入和批量导入导出。
顺序 I/O 更容易达到设备的最大吞吐量。
2. 随机 I/O
随机 I/O 的访问位置分散,设备需要处理更多寻址、队列和元数据操作,常见于数据库索引访问、键值存储、小文件读取和多进程并发访问。
随机 I/O 通常更受延迟和 IOPS 限制,而不是单纯受 MB/s 限制。
3. 请求大小的影响
请求大小过小会增加系统调用、队列和元数据开销;请求大小过大则可能增加单次延迟、内存占用和无效数据读取。
请求大小应根据业务访问模式选择:
- 小块随机读:关注延迟和 IOPS;
- 大块顺序读:关注吞吐量;
- 混合读写:关注尾延迟、队列和读写隔离;
- 数据库场景:以数据库实际页面大小和工作负载为准。
四、使用工具定位 I/O 问题
1. iostat:设备级总览
iostat 来自 sysstat 软件包,适合查看设备吞吐、IOPS、利用率和平均等待时间。
bash
# Debian / Ubuntu
sudo apt-get install sysstat
# RHEL / CentOS / Rocky Linux
sudo dnf install sysstat
iostat -xz 1常见字段:
| 字段 | 含义 |
|---|---|
r/s | 每秒完成的读请求数 |
w/s | 每秒完成的写请求数 |
rkB/s | 每秒读取的 KB 数 |
wkB/s | 每秒写入的 KB 数 |
await | I/O 请求平均等待时间,通常包含队列时间 |
aqu-sz | 平均队列长度 |
%util | 设备忙碌时间比例 |
r_await | 读请求平均等待时间 |
w_await | 写请求平均等待时间 |
一个简单判断思路:
%util高、await高:设备或设备队列可能成为瓶颈;%util高、await低:设备可能高效运行,不能仅凭%util判定异常;%util不高、await高:可能存在设备抖动、网络存储延迟或请求阻塞;- 吞吐量低、IOPS 高:可能是大量小块 I/O;
- 吞吐量高、IOPS 低:通常是大块顺序 I/O。
2. pidstat:进程级 I/O
使用 pidstat 可以定位哪些进程正在产生读写:
bash
pidstat -d 1常见字段:
kB_rd/s:每秒从存储设备读取的 KB;kB_wr/s:每秒写入的 KB;kB_ccwr/s:由于写时复制而导致的写入量;iodelay:I/O 延迟相关统计。
如果 iodelay 很高,说明进程的执行时间中有较多时间受到 I/O 延迟影响,但仍需结合系统调用和设备信息确认。
3. iotop:实时观察 I/O 活跃进程
bash
sudo iotop -oPa参数含义:
-o:只显示正在进行 I/O 的进程;-P:按进程显示,而不是按线程显示;-a:显示累计 I/O。
4. vmstat:观察系统整体状态
bash
vmstat 1重点关注:
r:可运行队列;b:不可中断睡眠进程数,常见于等待 I/O;si/so:swap 换入换出;bi/bo:块设备读入和写出;wa:I/O wait;cs:上下文切换。
如果 si/so 持续较高,问题可能不仅是磁盘吞吐不足,也可能是内存压力导致的 swap I/O。
5. sar:查看历史趋势
bash
sar -d 1 10
sar -b 1 10前提是系统已启用 sysstat 数据采集。历史数据可用于判断 I/O 峰值是否与业务发布时间吻合、是否每天固定时间发生备份竞争,以及设备延迟是否逐步恶化。
6. df 与 df -i:检查空间和 inode
bash
df -h
df -i需要分别检查块空间、inode、单个目录下的小文件数量,以及删除文件后空间是否仍未释放。
删除文件后空间没有立即恢复,常见原因是文件仍被进程打开:
bash
sudo lsof +L17. lsof:定位打开文件
bash
sudo lsof /var/log/app.log
sudo lsof +D /var/lib/app 2>/dev/nulllsof 可以帮助确认哪个进程打开了文件、文件是否已删除但仍被进程持有,以及某个挂载点是否仍在使用。
8. strace:定位系统调用级延迟
针对单个进程,可以使用:
bash
sudo strace -f -ttT -p <PID> -e trace=%file,read,write,fsync,fdatasync参数含义:
-f:跟踪子进程和线程;-tt:打印更精确的时间戳;-T:显示每个系统调用耗时;-p:附加到指定进程;-e trace=...:只关注文件和 I/O 相关调用。
重点观察 read()、pread64()、write()、fsync()、fdatasync() 是否耗时较高,以及是否频繁调用 openat()、stat()、fstat()。
生产环境中使用 strace 需要评估性能影响,建议短时间、低采样范围使用。
9. blktrace:块层请求分析
bash
sudo blktrace -d /dev/nvme0n1 -o - | blkparse -i -blktrace 可以帮助分析请求来源、读写类型、请求大小、连续性、队列等待时间和从提交到完成经过的阶段。它通常用于 iostat、pidstat 和 strace 无法解释问题时。
10. bpftrace / eBPF:动态观测
现代 Linux 可以使用 eBPF 追踪 I/O 延迟和系统调用行为。生产环境建议优先使用经过验证的工具,例如 biolatency、biosnoop、fileslower、filetop,避免自行编写脚本时错误关联设备、请求或并发事件。
五、读 I/O 与写 I/O 的排查思路
1. 读 I/O 问题
读延迟升高时,可以依次检查:
text
应用是否发起了大量读请求
↓
Page Cache 命中率是否下降
↓
读请求是否过于随机或过小
↓
设备读队列是否堆积
↓
文件系统和设备延迟是否升高
↓
是否存在内存回收或 swap 读写常见原因包括工作集超过内存、数据库索引不合理、小文件过多、多服务竞争同一设备、网络存储抖动,以及预读参数与访问模式不匹配。
2. 写 I/O 问题
写入延迟升高时,需要重点观察脏页和回写:
bash
cat /proc/meminfo | grep -E 'Dirty|Writeback'
cat /proc/sys/vm/dirty_ratio
cat /proc/sys/vm/dirty_background_ratio常见原因包括脏页集中回写、fsync() 或 fdatasync() 频繁调用、文件系统日志压力、数据库 checkpoint/WAL 集中写入,以及 SSD 垃圾回收或磨损均衡导致的延迟抖动。
降低写入延迟不能以牺牲数据一致性为代价。数据库、日志和交易系统不能为了“看起来更快”而随意关闭同步写入。
六、文件系统层面的性能因素
1. 挂载参数
查看当前挂载参数:
bash
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS
mount | column -t常见参数包括 noatime、relatime、nodiratime、discard、ext4 的 data= 和写屏障相关参数。修改前应确认文件系统类型、内核版本、设备特性和数据安全要求。
2. 文件碎片
机械硬盘和部分文件系统在长期使用后可能出现碎片,影响顺序读写;SSD 的随机访问成本较低,但碎片仍可能增加元数据和请求数量。
bash
sudo filefrag -v large-file不要在没有评估的情况下直接执行在线碎片整理。生产环境应安排维护窗口,并提前确认备份与回滚方案。
3. inode 与目录结构
大量小文件会带来 inode 消耗、dentry/inode 缓存压力、目录遍历变慢、stat() 操作增加,以及备份和扫描任务变慢。
常见改进方式是使用目录分片、按时间或哈希拆分目录、合并小对象、选择合适的对象存储或键值存储,并定期清理过期文件。
七、I/O 调度器
查看设备当前调度器:
bash
cat /sys/block/sda/queue/scheduler
cat /sys/block/nvme0n1/queue/scheduler常见调度器包括:
none:尽量减少软件层调度,常见于高性能 NVMe;mq-deadline:尝试控制请求延迟,适合部分通用场景;bfq:更强调公平性和交互体验;kyber:通过控制队列实现延迟目标。
临时修改示例:
bash
sudo sh -c 'echo mq-deadline > /sys/block/sda/queue/scheduler'调度器没有万能最优解。选择前应使用与生产相似的负载进行基准测试,并同时观察吞吐量、尾延迟和其他服务的影响。
八、常用内核参数
1. 脏页回写参数
bash
sysctl vm.dirty_background_ratio
sysctl vm.dirty_ratio
sysctl vm.dirty_background_bytes
sysctl vm.dirty_bytes
sysctl vm.dirty_expire_centisecs
sysctl vm.dirty_writeback_centisecs这些参数影响脏页何时开始后台回写,以及进程在写入时何时被迫参与回写。阈值过高可能形成突发写入和长尾延迟,阈值过低则可能增加持续回写频率。
2. 预读参数
bash
blockdev --getra /dev/sda
cat /sys/block/sda/queue/read_ahead_kb
sudo blockdev --setra 4096 /dev/sda顺序读取可以适当增加预读,但随机读取场景下过大的预读会读取大量无用数据,浪费带宽和缓存。
3. 队列参数
bash
cat /sys/block/sda/queue/nr_requests
cat /sys/block/sda/queue/max_sectors_kb
cat /sys/block/sda/queue/nomerges队列越大不一定越快。过大的队列可能增加等待时间,尤其是在延迟敏感业务中。
九、应用层优化
1. 减少无效 I/O
最有效的优化通常不是换更快的磁盘,而是减少根本不需要的 I/O:
- 避免重复读取相同数据;
- 使用缓存降低后端读取量;
- 只读取需要的字段和范围;
- 避免无条件扫描完整文件;
- 清理无效日志和重复落盘;
- 合并小块写入。
2. 缓冲与批量写入
频繁执行小块 write() 会产生较多系统调用和块层请求。可以通过用户态缓冲、批量提交或异步写入合并请求。
但批量写入会增加数据暂存时间和单次失败损失,需要在吞吐量、延迟和可靠性之间平衡。
3. 同步写入
以下操作通常会要求数据或元数据持久化:
fsync();fdatasync();O_SYNC;O_DSYNC;- 数据库 WAL 或日志提交。
同步写入可能显著提高延迟,但它们往往是数据一致性的组成部分,不能简单删除。
4. mmap 的适用边界
mmap() 可以把文件映射到进程地址空间,适合随机访问和减少显式 read 系统调用的场景,但它并不意味着没有 I/O。缺页时仍然可能触发磁盘读取。
使用 mmap 需要关注工作集大小、缺页长尾、文件增长和截断、多进程共享修改,以及映射区域带来的内存回收压力。
5. 异步 I/O 与 io_uring
异步 I/O 可以让应用在提交请求后继续执行,减少同步等待。io_uring 支持提交队列和完成队列,可以减少系统调用次数,但收益取决于应用是否能有效并发提交请求、文件系统和设备是否支持目标操作,以及队列深度是否合理。
十、数据库场景
数据库是典型的 I/O 敏感系统,排查时不能只在操作系统层面看磁盘利用率。
1. 数据文件、日志和临时文件分离
如果条件允许,可以将数据文件、WAL/redo log、临时表空间、备份文件和归档日志放到不同设备或存储池,减少不同 I/O 模式之间的相互干扰。
2. 检查数据库自身指标
应同时观察:
- buffer pool / shared buffers 命中率;
- checkpoint 频率和耗时;
- WAL / redo 写入延迟;
- 临时文件产生量;
- 全表扫描数量;
- 数据库连接和并发请求;
- 慢查询与执行计划。
如果数据库在没有明显设备瓶颈时仍然延迟升高,根因可能是锁等待、CPU、内存、网络或执行计划,而不是 I/O。
3. 避免用缓存命中率单独下结论
缓存命中率很高并不一定表示系统没有 I/O 问题。少量关键请求可能遇到长尾,写回和 checkpoint 仍可能阻塞,临时文件、日志和元数据 I/O 也可能被忽略。
十一、典型问题分析
场景一:CPU 使用率低,但 Load Average 很高
bash
uptime
vmstat 1
iostat -xz 1
pidstat -d 1如果 vmstat 中 b 持续较高,同时 iostat 的设备 await 或队列长度明显上升,通常说明大量任务在等待 I/O。若设备指标正常,则继续检查 NFS、D 状态进程、锁竞争、内存回收和驱动错误。
场景二:磁盘 %util 接近 100%,但吞吐不高
可能是大量小块随机 I/O。若 IOPS 高、吞吐量低、请求大小小,优化方向通常是减少随机访问、合并小请求、优化数据库索引、增加缓存或评估更高 IOPS 的设备。
场景三:写入突然导致服务卡顿
可能是脏页达到阈值,进程被迫同步参与回写。建议观察 Dirty、Writeback、设备延迟和写入来源,再评估回写参数,不要直接关闭同步机制。
场景四:删除日志后磁盘空间没有释放
bash
lsof +L1如果某进程仍打开已删除文件,需要让应用正确关闭并重新打开日志文件,或使用应用支持的日志轮转方式。
场景五:系统出现大量 swap I/O
bash
free -h
vmstat 1
iostat -xz 1
sar -W 1 10重点确认是否内存不足、存在内存泄漏、进程工作集突然增长,以及 swap 是否位于慢设备。单独调低 vm.swappiness 不能替代内存容量和内存泄漏治理。
十二、基准测试方法
任何 I/O 优化都应先建立基线,再进行对照测试。
1. fio 基本示例
顺序读取:
bash
fio --name=seq-read \
--filename=/data/testfile \
--size=4G \
--bs=1M \
--rw=read \
--iodepth=16 \
--direct=1 \
--runtime=60 \
--time_based \
--group_reporting随机读写:
bash
fio --name=rand-rw \
--filename=/data/testfile \
--size=4G \
--bs=4k \
--rw=randrw \
--rwmixread=70 \
--iodepth=32 \
--numjobs=4 \
--direct=1 \
--runtime=60 \
--time_based \
--group_reporting测试参数含义:
--bs:请求块大小;--rw:读写模式;--iodepth:单个作业的队列深度;--numjobs:并发作业数;--direct=1:绕过 Page Cache,测试设备路径;--runtime:运行时间;--group_reporting:合并输出结果。
2. 基准测试注意事项
不要直接在生产数据盘上运行破坏性测试。需要注意:
fio的写测试可能覆盖已有数据;- 测试文件可能触发 SSD 垃圾回收和缓存效应;
- Page Cache 测试与 direct I/O 测试代表不同路径;
- 测试盘的容量、温度和突发缓存会影响结果;
- 云盘可能有突发 IOPS 或吞吐额度;
- 测试期间应同步采集
iostat、应用延迟和设备错误。
十三、I/O 性能优化方法论
第一步:确认现象
先记录故障时间窗口和影响范围:
- 哪些服务受影响;
- 读、写还是混合 I/O;
- 平均延迟和 P99 延迟;
- 设备吞吐、IOPS、队列和 await;
- CPU、内存、swap、网络是否异常。
第二步:定位责任主体
使用 pidstat、iotop、lsof 和应用指标确认 I/O 来源。不要仅凭设备满载就认定某个应用有问题。
第三步:判断访问模式
确认读写比例、请求大小、顺序/随机比例、并发度,以及是否存在周期性批任务。
第四步:分层验证
text
应用指标
→ 系统调用
→ 文件系统
→ 块层
→ 设备
→ 存储服务端每一层都应回答“请求在哪里耗时”,而不是只收集更多指标。
第五步:选择优化手段
优先级通常是:
- 删除无效 I/O;
- 优化访问模式和数据结构;
- 使用缓存和批量处理;
- 分离不同业务的 I/O;
- 调整文件系统、队列和回写参数;
- 升级设备或增加 IOPS/吞吐配额;
- 通过横向扩展分散负载。
第六步:验证副作用
优化后必须确认数据一致性、数据丢失窗口、其他服务影响、长尾延迟、峰值稳定性,以及参数重启后是否仍然生效。
十四、常见误区
误区一:%iowait 高就一定是磁盘坏了
I/O wait 只能说明系统存在 I/O 等待,可能来自本地磁盘、网络文件系统、swap、虚拟化存储或驱动层。必须结合设备和进程数据判断。
误区二:%util=100% 就一定要换磁盘
对于现代多队列设备,%util 的解释不能脱离 await、IOPS、吞吐和应用延迟。设备可能处于高效饱和,也可能正在处理大量小请求。
误区三:清 Page Cache 可以解决性能问题
以下命令会丢弃缓存:
bash
sync
echo 3 | sudo tee /proc/sys/vm/drop_caches这通常只适合测试环境或特定诊断场景,不是生产优化手段。清缓存会导致后续请求重新从设备读取,可能引发更严重的延迟抖动。
误区四:把 noatime 当成万能优化
noatime 可能减少访问时间更新,但收益取决于业务访问模式和文件系统配置。修改挂载参数前还要考虑应用是否依赖访问时间。
误区五:关闭 fsync 就能解决写延迟
关闭同步写入可能降低表面延迟,却会增加断电、崩溃或存储故障时的数据丢失和损坏风险。数据库和关键日志系统不应为了基准成绩随意关闭可靠性机制。
误区六:只跑一次 fio 就代表生产性能
一次基准测试不能代表真实工作负载。生产性能还受到缓存、温度、并发、后台回收、混合读写、云盘额度和文件系统状态影响。
十五、实用排查清单
bash
# 1. 系统总体状态
uptime
vmstat 1
free -h
# 2. 设备级 I/O
iostat -xz 1
# 3. 进程级 I/O
pidstat -d 1
sudo iotop -oPa
# 4. 文件系统容量与 inode
df -h
df -i
# 5. 删除但仍被打开的文件
sudo lsof +L1
# 6. 挂载点和文件系统类型
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS
# 7. 内核和设备错误
dmesg -T | grep -iE 'error|fail|reset|timeout|I/O'
# 8. 块设备信息
lsblk -o NAME,TYPE,FSTYPE,SIZE,ROTA,SCHED,MOUNTPOINT十六、总结
Linux I/O 性能问题的本质,是应用产生的访问模式与文件系统、块层、设备和存储服务能力之间出现了不匹配。
排查时建议遵循以下原则:
- 先区分读写,再区分顺序和随机;
- 同时观察吞吐量、IOPS、延迟和队列,而不是只看一个指标;
- 从进程、系统调用、文件系统、块层到设备逐层定位;
- 关注 P99 等长尾指标,不要只看平均值;
- 优先减少无效 I/O,再考虑调参和升级硬件;
- 任何降低同步和持久化保证的优化,都必须明确评估数据风险;
- 使用与生产相似的负载进行基准测试,并在修改后验证副作用。
一个稳定的 I/O 优化闭环应该是:
text
发现异常
→ 建立基线
→ 定位 I/O 来源
→ 判断访问模式
→ 分层分析延迟
→ 实施最小改动
→ 对照验证
→ 观察长期趋势只有当性能指标、应用行为和业务结果同时改善时,才可以认为 I/O 优化真正完成。