目录
安装环境
分析问题
参考
上次MySql慢是因为没建立索引
此外主要依赖系统缓存加速磁盘I/O访问,如果系统中还有其他应用同时运行,MyISAM引擎很难充分利用系统缓存,缓存可能会被其他程序占用,甚至被清理掉
最好在应用程序内部分配内存,构建完全自主控制的缓存,或者使用第三方缓存如Memcached,Redis等
Redis是最常用的键值存储系统之一,常用做数据库,高速缓存和消息队列代理等,Redis基于内存来存储数据,为了保住服务器异常时数据不丢失,需要为其配置持久化,这就可能会引发磁盘I/O的性能问题
案例由Python应用,Redis两部分组成,其中Python应用是一个基于Flask的应用,他会利用Redis,来管理应用程序的缓存,并对外提供三个HTTP接口
/, 返回hello redis
/init, 插入指定数据的缓存数据,如果不指定数量,默认为5000条
缓存的键格式为uuid
缓存的值为good,bad,normal三者之一
/get_cache/
bad,normal(也就是找出具有相同value的key列表)
安装应用和redis,执行如下命令
cd redis-slow
make run
docker run --name=redis -itd -p 10000:80 feisky/redis-server
Unable to find image 'feisky/redis-server:latest' locally
Trying to pull repository docker.io/feisky/redis-server ...
latest: Pulling from docker.io/feisky/redis-server
cd784148e348: Already exists
48d4c7155ddc: Pull complete
6d908603dbe8: Pull complete
0b981e82e1e2: Pull complete
7074f4a1fd03: Pull complete
447ac2b250dc: Pull complete
b6d44ce71e94: Pull complete
Digest: sha256:a69d39256eb970ab0d87a70d53fa2666d0c32cbf68fb316ef016efd513806146
Status: Downloaded newer image for docker.io/feisky/redis-server:latest
5785cf3f87c786730f364399f656b02dc5f26ca863979a642bf347cb99ff0c33
docker run --name=app --network=container:redis -itd feisky/redis-app
Unable to find image 'feisky/redis-app:latest' locally
Trying to pull repository docker.io/feisky/redis-app ...
latest: Pulling from docker.io/feisky/redis-app
54f7e8ac135a: Already exists
d6341e30912f: Already exists
087a57faf949: Already exists
5d71636fb824: Already exists
0c1db9598990: Already exists
2eeb5ce9b924: Already exists
d3029c597b32: Pull complete
265a9c957eba: Pull complete
3bb7ae9463c5: Pull complete
b3198935e7ab: Pull complete
ca3ab58d03d9: Pull complete
Digest: sha256:ac281cbb37e35eccb880a58b75f244ef19a9a8704a43ae274c12e6576ed6082b
Status: Downloaded newer image for docker.io/feisky/redis-app:latest
c9c0d78952471656bdc2a9c6f1785d707532e65c307c090ce07dcbb27da66f33
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c9c0d7895247 feisky/redis-app "python /app.py" 52 seconds ago Up 52 seconds app
5785cf3f87c7 feisky/redis-server "docker-entrypoint..." About a minute ago Up About a minute 6379/tcp, 0.0.0.0:10000->80/tcp redis
通过curl执行一些测试和初始化操作
#测试访问效果
curl http://[IP]:10000/
hello redis
#插入5000条数据
curl http://[IP]:10000/init/5000
{"elapsed_seconds":11.59295916557312,"keys_initialized":5000}
#查询缓存接口,显示用时 4.4秒
curl http://[IP]:10000/init/get_cache
。。。
c120002","fdb33396-203d-11e9-bd87-0242ac120002","fe99d76a-203d-11e9-bd87-0242ac120002"],"elapsed_seconds":4.490397691726685,"type":"good"}
#为了避免分析时,请求客户端结束,将这个curl放到一个循环中
while true; do curl http://[IP]:10000/get_cache; done
通过top,iostat,pidstat来观察系统运行情况
发现有一个redis-server进程,io使用率很高
top - 12:43:28 up 16 days, 2:26, 3 users, load average: 1.43, 0.54, 0.20
Tasks: 84 total, 3 running, 44 sleeping, 0 stopped, 0 zombie
%Cpu(s): 13.1 us, 11.3 sy, 0.0 ni, 0.0 id, 73.9 wa, 0.0 hi, 1.7 si, 0.0 st
KiB Mem : 1008936 total, 82300 free, 123712 used, 802924 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 721308 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8001 root 20 0 193264 27784 8884 S 15.7 2.8 0:16.61 python
7957 100 20 0 28352 9904 1984 D 8.0 1.0 0:08.33 redis-server
349 root 20 0 0 0 0 R 1.7 0.0 0:07.37 jbd2/vda1-8
iostat -x -d 1
Linux 4.20.0-1.el7.elrepo.x86_64 (iz2zege42v3jtvyj2oecuzz) 01/25/2019 _x86_64_ (1 CPU)
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 0.30 0.57 0.55 138.44 33.52 308.20 0.74 33.75 20.41 47.50 629.94 70.29
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 351.52 33.33 995.96 137.37 5393.94 10.75 1.04 0.72 0.48 0.72 0.98 101.01
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 381.63 0.00 1087.76 0.00 5873.47 10.80 1.02 0.71 0.00 0.71 0.94 102.04
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 390.72 0.00 1118.56 0.00 6041.24 10.80 1.03 0.71 0.00 0.71 0.92 103.09
pidstat -d 1
12:44:51 PM UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
12:44:52 PM 0 349 0.00 4.00 0.00 jbd2/vda1-8
12:44:52 PM 100 7957 0.00 1456.00 0.00 redis-server
12:44:52 PM UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
12:44:53 PM 100 7957 0.00 1303.09 0.00 redis-server
12:44:53 PM UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
12:44:54 PM 0 349 0.00 4.08 0.00 jbd2/vda1-8
12:44:54 PM 0 419 0.00 12.24 0.00 systemd-journal
12:44:54 PM 100 7957 0.00 1302.04 0.00 redis-server
通过strace观察,有大量的read,write调用
# -tt -T 打印时间戳,和函数调用时间
strace -p 7957 -e trace=desc -ff -tt -T
[pid 7957] 12:48:58.580249 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 92, NULL, 8) = 1 <0.000019>
[pid 7957] 12:48:58.580301 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:6b049e5e-"..., 16384) = 61 <0.000013>
[pid 7957] 12:48:58.580345 read(3, 0x7ffd970a80b7, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000011>
[pid 7957] 12:48:58.580380 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000126>
[pid 7957] 12:48:58.580539 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 92, NULL, 8) = 1 <0.000017>
[pid 7957] 12:48:58.580587 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:6860bbec-"..., 16384) = 61 <0.000012>
[pid 7957] 12:48:58.580630 read(3, 0x7ffd970a80b7, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000012>
[pid 7957] 12:48:58.580665 write(8, "$4\r\ngood\r\n", 10) = 10 <0.000126>
[pid 7957] 12:48:58.580824 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 92, NULL, 8) = 1 <0.000018>
[pid 7957] 12:48:58.580872 read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n686"..., 16384) = 67 <0.000027>
[pid 7957] 12:48:58.580935 read(3, 0x7ffd970a80b7, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000011>
[pid 7957] 12:48:58.580972 write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n686"..., 67) = 67 <0.000023>
[pid 7957] 12:48:58.581018 fdatasync(7) = 0 <0.001618>
[pid 7957] 12:48:58.582680 write(8, ":1\r\n", 4) = 4 <0.000131>
[pid 7957] 12:48:58.582851 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 90, NULL, 8) = 1 <0.000017>
[pid 7957] 12:48:58.582900 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:68cdfdd8-"..., 16384) = 61 <0.000013>
[pid 7957] 12:48:58.582944 read(3, 0x7ffd970a80b7, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000011>
[pid 7957] 12:48:58.582979 write(8, "$4\r\ngood\r\n", 10) = 10 <0.000149>
结合strace和lsof再分析
这里有好多种文件,pipe管道文件,eventpoll,普通文件,TCP socket文件
综合磁盘读写的想象,只有7号普通文件才会产生磁盘写,他操作的文件路径是
/data/appendonly.aof
相应的系统调用包含write和fdatasync
再通过 strace看,确实是fdatasync系统调用,再写fd=7的文件,也就是/data/appendonly.aof
通过fdatasync的调用和时间戳看,大概平均3毫秒调用一次,每次执行时间大概是1毫秒左右
lsof -p 7957
redis-ser 7957 100 0u CHR 136,1 0t0 4 /1
lsof: no pwd entry for UID 100
redis-ser 7957 100 1u CHR 136,1 0t0 4 /1
lsof: no pwd entry for UID 100
redis-ser 7957 100 2u CHR 136,1 0t0 4 /1
lsof: no pwd entry for UID 100
redis-ser 7957 100 3r FIFO 0,12 0t0 963277 pipe
lsof: no pwd entry for UID 100
redis-ser 7957 100 4w FIFO 0,12 0t0 963277 pipe
lsof: no pwd entry for UID 100
redis-ser 7957 100 5u a_inode 0,13 0 7531 [eventpoll]
lsof: no pwd entry for UID 100
redis-ser 7957 100 6u sock 0,9 0t0 963279 protocol: TCP
lsof: no pwd entry for UID 100
redis-ser 7957 100 7w REG 253,1 11560065 1195079 /data/appendonly.aof
lsof: no pwd entry for UID 100
redis-ser 7957 100 8u sock 0,9 0t0 964360 protocol: TCP
strace -p 7957 -e trace=fdatasync -ff -tt -T
strace: Process 7957 attached with 4 threads
[pid 7957] 12:55:33.267216 fdatasync(7) = 0 <0.001888>
[pid 7957] 12:55:33.269724 fdatasync(7) = 0 <0.002024>
[pid 7957] 12:55:33.273431 fdatasync(7) = 0 <0.001935>
[pid 7957] 12:55:33.276447 fdatasync(7) = 0 <0.002140>
[pid 7957] 12:55:33.280856 fdatasync(7) = 0 <0.002174>
[pid 7957] 12:55:33.284771 fdatasync(7) = 0 <0.002113>
[pid 7957] 12:55:33.287465 fdatasync(7) = 0 <0.002371>
通过上述分析,大致猜测是Redis持久化配置中的appendonly和appendfsync选项配置不合理导致的
通过Redis的命令行工具,查询这两个选项的配置
继续再终端中,运行下面的命令,查询appendonly和appendfsync的配置
docker exec -it redis redis-cli config get 'append*'
1) "appendfsync"
2) "always"
3) "appendonly"
4) "yes"
通过这个结果可以发现
appendfsync配置的是always
appendonly配置的是yes
Redis提供两种数据持久化方式,分别是快照和追加文件
追加文件的具体策略包括 always,everysec,no等
这样就找到了Redis正在进行写入的文件,也知道了产生大量I/O的原因
再看看Redis到底读写了哪些文件
strace -p 7957 -f -e trace=read,write
strace: Process 7957 attached with 4 threads
[pid 7957] write(8, ":1\r\n", 4) = 4
[pid 7957] read(8, "*4\r\n$4\r\nSCAN\r\n$4\r\n8049\r\n$5\r\nMATC"..., 16384) = 47
[pid 7957] read(3, 0x7ffd970a80b7, 1) = -1 EAGAIN (Resource temporarily unavailable)
[pid 7957] write(8, "*2\r\n$4\r\n1521\r\n*11\r\n$41\r\nuuid:6e3"..., 547) = 547
[pid 7957] read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:6e3c31ea-"..., 16384) = 61
[pid 7957] read(3, 0x7ffd970a80b7, 1) = -1 EAGAIN (Resource temporarily unavailable)
[pid 7957] write(8, "$4\r\ngood\r\n", 10) = 10
[pid 7957] read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n6e3"..., 16384) = 67
[pid 7957] read(3, 0x7ffd970a80b7, 1) = -1 EAGAIN (Resource temporarily unavailable)
[pid 7957] write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n6e3"..., 67) = 67
fd=3的是一个pipe管道文件
fd=8是 socket文件,有读也有写
从socket读取GET uuid:。。。 后,响应good
再从socket读取SADD good 545.。。后,响应1
对于Redis来说,SADD是一个写操作,所以Redis还会把他保存到用于持久化的appendonly.aof文件中
观察更多的strace结果,会发现每当GET返回good时候,随后都会有一个SADD操作,这样就导致了本来是查询,Redis却有大量的写磁盘操作
到这里就找出了Redis写磁盘的原因,在最终结论之前,需要确认一下,8号TCP socket对应的Redis客户端,是不是当前的案列应用
可以给lsof命令加上-i选项,找出TCP socket对应的TCP连接信息,由于Redis和Python应用都在容器中运行,我们需要进入容器的网络命名空间内部,才能看到完整的TCP连接
#下面的命令会用到 nsenter 工具,可以进入容器命名空间,安装方式
docker run --rm -v /usr/local/bin:target jpetazzo/nsenter
运行下面的命令
#由于这两个容器共享同一个网络命名空间,我们只需要进入app的网络命名空间即可
PID=$(docker inspect --format {{.State.Pid}} app)
-i表示显示网络套接字信息
nsenter --target $PID --net -- lsof -i
lsof: no pwd entry for UID 100
lsof: no pwd entry for UID 100
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
lsof: no pwd entry for UID 100
redis-ser 7957 100 6u IPv4 963279 0t0 TCP localhost:6379 (LISTEN)
lsof: no pwd entry for UID 100
redis-ser 7957 100 8u IPv4 964360 0t0 TCP localhost:6379->localhost:49088 (ESTABLISHED)
python 8001 root 3u IPv4 964217 0t0 TCP *:http (LISTEN)
python 8001 root 4u IPv4 980043 0t0 TCP [my_host]:http->[IP]:34944 (ESTABLISHED)
python 8001 root 5u IPv4 964359 0t0 TCP localhost:49088->localhost:6379 (ESTABLISHED)
这次可以看到,redis-server的8号文件描述符,对应TCP连接localhost:6379 -> localhost:49088
其中localhost:6379就是 redis-server 自己的监听端口,所以 localhost:49088就是redis的客户端
观察最后一行,也就是正在运行的Python应用程序
现在终于找到Redis响应延迟的潜在原因,找到两个问题
1.Redis配置的appendfsync是always,这就导致了Redis每次的写操作,都会触发fdatasync系统调用,改成1秒就可以了
2.Python应用在查询接口中调用Redis的SADD命令,这可能是不合理使用缓存导致的
对于第一个配置问题,执行下面命令修改
docker exec -it redis redis-cli config set appendfsync everysec
OK
再看系统运行情况,io延迟已经低了很多了
top - 13:37:24 up 16 days, 3:20, 6 users, load average: 1.04, 1.37, 1.44
Tasks: 85 total, 3 running, 48 sleeping, 0 stopped, 0 zombie
%Cpu(s): 61.1 us, 27.8 sy, 0.0 ni, 5.6 id, 0.0 wa, 0.0 hi, 5.6 si, 0.0 st
KiB Mem : 1008936 total, 109480 free, 123364 used, 776092 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 720904 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8001 root 20 0 193264 27852 8884 S 65.5 2.8 10:06.27 python
iostat -d -x 1
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 14.14 0.00 3.03 0.00 185.86 122.67 0.21 1.33 0.00 1.33 67.67 20.51
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 7.84 0.00 2.94 0.00 137.25 93.33 0.20 1.00 0.00 1.00 68.00 20.00
再看python的app.py代码
@app.route('/get_cache', defaults={'type_name': 'good'})
@app.route("/get_cache/")
@timing
def get_cache(type_name):
'''handler for /get_cache'''
for key in redis_client.scan_iter("uuid:*"):
value = redis_client.get(key)
if value == type_name:
redis_client.sadd(type_name, key[5:])
data = list(redis_client.smembers(type_name))
redis_client.delete(type_name)
return jsonify({"type": type_name, 'count': len(data), 'data': data})
可以看到,Python应用把Redis当成临时空间,用处来村查询过程中找到的数据
而这些数据放到内存中就可以了,完全没必须再通过网络钓调用储到Redis中
再通过新的接口访问
curl http://[IP]:10000/get_cache_data
"688563de-205b-11e9-b526-0242ac120002","6810117e-205b-11e9-b526-0242ac120002",
"6e8c056c-205b-11e9-b526-0242ac120002","6814491a-205b-11e9-b526-0242ac120002"],
"elapsed_seconds":0.17398428916931152,"type":"good"}
解决第二个问题后,性能又有了进步一提升,0.17秒就完成了
最后清理案例的应用
redis-slow]# make clean
docker rm -f app redis
app
redis
这次分析的是一个Redis 缓存的案例
先用 top、iostat ,分析了系统的 CPU 、内存和磁盘使用情况,再通过pidstat继续分析
从 Redis 的原理来说,查询缓存时不应该出现大量的磁盘 I/O 写操作。
顺着这个思路,继续借助 pidstat、strace、lsof、nsenter 等一系列的工具,找出了两个潜在问题,
一个是 Redis 的不合理配置,
另一个是 Python 应用对 Redis 的滥用
Redis Persistence