Go服务卡在D状态但ps显示RUNNING,是内核态磁盘I/O阻塞导致的不可中断睡眠,并非崩溃;需立即通过/proc/PID/stack(查__generic_file_read等关键词)、iostat -x(%util≥95%且await>100ms)和iotop -o -P定位瓶颈,否则HTTP请求、docker exec、kill -15均失效。
go服务卡在d状态但ps显示running,不是崩溃,是内核态磁盘i/o阻塞导致的不可中断睡眠;必须立刻查/proc/pid/stack、iostat -x和iotop -o -p,否则所有http请求、docker exec、kill -15都会失效。
怎么确认真是磁盘I/O卡住,而不是CPU或网络问题
别只看top的%CPU——D状态进程不占CPU但会推高load average。关键证据有三:
- 执行
cat /proc/$(pgrep -f yourbinary)/stack,输出里出现__generic_file_read、ext4_file_read_iter、overlayfs或nfs_wait_bit_killable中的任意一个,就坐实是磁盘路径夯住 - 用
strace -p $(pgrep -f yourbinary) -e trace=read,write,openat,如果看到read()或write()长时间不返回(几秒以上),且调用是串行而非并发,说明系统调用被内核阻塞 -
iostat -x 1中%util持续≥95%、await>100ms、r/s和w/s却不高——这是典型的大块IO(如日志刷盘、大文件上传)拖慢设备,不是高频小IO打满队列
为什么加goroutine也救不了os.ReadFile
Go里写go func() { os.ReadFile("huge.log") }()只是把阻塞挪到另一个OS线程,底层仍是同步read(2)系统调用,内核态卡住就是卡住。goroutine调度器能防止整个程序僵死,但无法缩短单次I/O耗时,也无法降低磁盘压力。
- 现象:100个goroutine并发读文件,
strace仍看到read()一个接一个执行,不是并行发起 - 后果:这些goroutine卡在
runtime.gopark+syscall.Syscall,占用P导致其他HTTP协程无法调度 - 真实栈线索:
/proc/PID/stack里反复出现ext4或overlayfs函数名,不是ep_poll或sys_epoll_wait(后者才是网络I/O)
紧急止血和长期规避的关键动作
先保服务可用,再改代码。日志写满、大文件直读、高频小文件open是三大高频诱因。
- 立刻清空日志(不删文件):
truncate -s 0 /path/to/app.log;同时检查df -h对应挂载点是否Use% ≥ 95% - 替换
os.ReadFile为可控流式读:os.Open+bufio.NewReaderSize(f, 65536)+ 分块r.Read(buf),每次前检查ctx.Err(),超时后必须f.Close() - 禁用
log.SetOutput(&os.File)直写磁盘;改用lumberjack.Logger轮转,或至少配logrotate且含copytruncate选项 - 避免在HTTP handler里做任何
os.Open、http.ServeFile、archive/zip解压等操作;模板/配置类小文件应预加载进内存,用sync.Once初始化全局[]byte
容易被忽略的隐性陷阱
很多团队修了日志、换了读法,过两周又复发——问题常藏在更底层:
-
GODEBUG=madvdontneed=1在某些内核版本下会引发额外I/O抖动,建议关闭 - 容器中
log.SetOutput(os.Stderr)看似写终端,实际底层走/dev/pts/0,仍经磁盘缓存路径 - 用
go-sql-driver/mysql时,readTimeout设得太长(如30s),网络卡顿会转成内核态I/O等待,表现和磁盘卡住一模一样 - 成百上千goroutine同时
os.Open小文件,会打爆ext4的dentry缓存,连openat()都开始延迟——这时iotop可能看不到你的进程在写,但iostat依然%util拉满











