配置环境,以创建负载场景
# docker run --name=redis -itd -p 10000:80 feisky/redis-server # docker run --name=app --network=container:redis -itd feisky/redis-app # docker ps // 确认两个容器都处于运行状态 # curl http://10.10.50.188:10000/ // 检测应用正常启动 hello redis // 初始化 Redis 缓存,并且插入 5000 条缓存信息 curl http://10.10.50.188:10000/init/5000 {"elapsed_seconds":30.26814079284668,"keys_initialized":5000} // 测试接口调用 # curl http://10.10.50.188:10000/get_cache {"count":1677,"data":["d97662fa-06ac-11e9-92c7-0242ac110002",...],"elapsed_seconds":10.545469760894775,"type":"good"} // 接口调用响应时间较长,需要花费 10 秒 // 循环执行命令,以创造负载,用于调试 # while true; do curl http://10.10.50.188:10000/get_cache; done
第一步、运行 top 命令,观察资源使用情况
# top top - 12:46:18 up 11 days, 8:49, 1 user, load average: 1.36, 1.36, 1.04 Tasks: 137 total, 1 running, 79 sleeping, 0 stopped, 0 zombie %Cpu0 : 6.0 us, 2.7 sy, 0.0 ni, 5.7 id, 84.7 wa, 0.0 hi, 1.0 si, 0.0 st %Cpu1 : 1.0 us, 3.0 sy, 0.0 ni, 94.7 id, 0.0 wa, 0.0 hi, 1.3 si, 0.0 st KiB Mem : 8169300 total, 7342244 free, 432912 used, 394144 buff/cache KiB Swap: 0 total, 0 free, 0 used. 7478748 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 9181 root 20 0 193004 27304 8716 S 8.6 0.3 0:07.15 python 9085 systemd+ 20 0 28352 9760 1860 D 5.0 0.1 0:04.34 redis-server 368 root 20 0 0 0 0 D 1.0 0.0 0:33.88 jbd2/sda1-8 149 root 0 -20 0 0 0 I 0.3 0.0 0:10.63 kworker/0:1H 1549 root 20 0 236716 24576 9864 S 0.3 0.3 91:37.30 python3 // CPU0 的 iowait 比较高,已经达到了 84% // 内存,总内存 8GB,剩余内存还有 7GB 多 // 进程的 CPU 使用率都不太高,最高的 python 和 redis-server ,也分别只有 8% 和 5% // 最有嫌疑的就是 iowait。所以接下来还是要继续分析,是不是 I/O 问题。
第二步、运行 iostat 命令,观察 I/O 的使用情况
# iostat -d -x 1 Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util ... sda 0.00 492.00 0.00 2672.00 0.00 176.00 0.00 26.35 0.00 1.76 0.00 0.00 5.43 0.00 0.00 // 磁盘 sda 每秒的写数据(wkB/s)为 2.5MB,I/O 使用率(%util)是 0 // 对查询来说,对应的 I/O 应该是磁盘的读操作,但刚才我们用 iostat 看到的却是写操作 // 为什么会有磁盘写呢
iowait 不代表磁盘I/O存在瓶颈,只是代表 CPU 上 I/O 操作的时间占用的百分比。假如这时候没有其他进程在运行,那么很小的 I/O 就会导致 iowait 升高
第三步、运行 pidstat 命令,观察进程 I/O 使用情况
# pidstat -d 1 12:49:35 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 12:49:36 0 368 0.00 16.00 0.00 86 jbd2/sda1-8 12:49:36 100 9085 0.00 636.00 0.00 1 redis-server // I/O 最多的进程是 PID 为 9085 的 redis-server,并且它也刚好是在写磁盘。 // 这说明确实是 redis-server 在进行磁盘写
第四步、运行 strace 命令,分析进程的系统调用
# strace -f -p 9085 -T -tt // -f 表示跟踪子进程和子线程,-T 表示显示系统调用的时长,-tt 表示显示跟踪时间 [pid 9085] 14:20:16.826131 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000055> [pid 9085] 14:20:16.826301 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:5b2e76cc-"..., 16384) = 61 <0.000071> [pid 9085] 14:20:16.826477 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000063> [pid 9085] 14:20:16.826645 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000173> [pid 9085] 14:20:16.826907 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000032> [pid 9085] 14:20:16.827030 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:55862ada-"..., 16384) = 61 <0.000044> [pid 9085] 14:20:16.827149 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000043> [pid 9085] 14:20:16.827285 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000141> [pid 9085] 14:20:16.827514 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 64, NULL, 8) = 1 <0.000049> [pid 9085] 14:20:16.827641 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-"..., 16384) = 61 <0.000043> [pid 9085] 14:20:16.827784 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000034> [pid 9085] 14:20:16.827945 write(8, "$4\r\ngood\r\n", 10) = 10 <0.000288> [pid 9085] 14:20:16.828339 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 63, NULL, 8) = 1 <0.000057> [pid 9085] 14:20:16.828486 read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 16384) = 67 <0.000040> [pid 9085] 14:20:16.828623 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000052> [pid 9085] 14:20:16.828760 write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 67) = 67 <0.000060> [pid 9085] 14:20:16.828970 fdatasync(7) = 0 <0.005415> [pid 9085] 14:20:16.834493 write(8, ":1\r\n", 4) = 4 <0.000250> // 那么刚才观察到的写磁盘,应该就是 write 或者 fdatasync 导致的
查看 3 5 7 8 描述符
# lsof -p 9085 redis-ser 9085 systemd-network 3r FIFO 0,12 0t0 15447970 pipe redis-ser 9085 systemd-network 4w FIFO 0,12 0t0 15447970 pipe redis-ser 9085 systemd-network 5u a_inode 0,13 0 10179 [eventpoll] redis-ser 9085 systemd-network 6u sock 0,9 0t0 15447972 protocol: TCP redis-ser 9085 systemd-network 7w REG 8,1 8830146 2838532 /data/appendonly.aof redis-ser 9085 systemd-network 8u sock 0,9 0t0 15448709 protocol: TCP // 3 的是一个 pipe 管道,5 号是 eventpoll,7 号是一个普通文件,而 8 号是一个 TCP socket // 7 号普通文件才会产生磁盘写,而它操作的文件路径是 /data/appendonly.aof,相应的系统调用包括 write 和 fdatasync // 如果熟悉 Redis 持久话,就会发现问题在哪里,我们这里可以推断是 Redis 的持久化配置存在问题 // 结合前面 read(8) 中可以看到,redis 收到的请求指令为 GET 与 SADD,而 SADD 会 是一个写操作 // 查询 appendonly 和 appendfsync 的配置 # docker exec -it redis redis-cli config get 'append*' 1) "appendfsync" 2) "always" 3) "appendonly" 4) "yes" // appendfsync = always 每个操作都会执行一次 fsync,是最为安全的方式 // 意味着每次写数据时,都会调用一次 fsync,从而造成比较大的磁盘 I/O 压力 # strace -f -p 9085 -T -tt -e fdatasync // 通过 -e 追踪 fdatasync 调用 strace: Process 9085 attached with 4 threads [pid 9085] 14:22:52.013547 fdatasync(7) = 0 <0.007112> [pid 9085] 14:22:52.022467 fdatasync(7) = 0 <0.008572> [pid 9085] 14:22:52.032223 fdatasync(7) = 0 <0.006769> ... [pid 9085] 14:22:52.139629 fdatasync(7) = 0 <0.008183> // 每隔 10ms 左右,就会有一次 fdatasync 调用,并且每次调用本身也要消耗 7~8ms
定位请求的来源
// 由于这两个容器共享同一个网络命名空间,所以我们只需要进入app的网络命名空间即可 # PID=$(docker inspect --format {{.State.Pid}} app) # nsenter --target $PID --net -- lsof -i // -i 表示显示网络套接字信息 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME redis-ser 9085 systemd-network 6u IPv4 15447972 0t0 TCP localhost:6379 (LISTEN) redis-ser 9085 systemd-network 8u IPv4 15448709 0t0 TCP localhost:6379->localhost:32996 (ESTABLISHED) python 9181 root 3u IPv4 15448677 0t0 TCP *:http (LISTEN) python 9181 root 5u IPv4 15449632 0t0 TCP localhost:32996->localhost:6379 (ESTABLISHED) // redis-server, FD=8 => localhost:6379->localhost:32996 => localhost:32996->localhost:6379 => python, PID=9181 // 是我们的 Python 应用程序(进程号为 9181)
问题解决办法
第一步、把 appendfsync 改成 everysec
第二步、检查程序源码,找到 SADD 指令,查看源码对 SADD 指令的使用是否正确。
参考文献