1. 程式人生 > 其它 >29-案例篇:Redis響應嚴重延遲,如何解決?

29-案例篇:Redis響應嚴重延遲,如何解決?





MySQL的MyISAM引擎,主要依賴系統快取加速磁碟I/O的訪問
可如果系統中還有其他應用同時執行,MyISAM引擎很難充分利用系統快取
快取可能會被其他應用程式佔用,甚至被清理掉

所以並不建議,把應用程式的效能優化完全建立在系統快取上
最好能在應用程式的內部分配記憶體,構建完全自主控制的快取
或者使用第三方的快取應用,比如Memcached、Redis等

Redis是最常用的鍵值儲存系統之一,常用作資料庫、快取記憶體和訊息佇列代理等
Redis基於記憶體來儲存資料
不過,為了保證在伺服器異常時資料不丟失,很多情況下,要為它配置持久化
而這就可能會引發磁碟I/O的效能問題


案例

  1. 伺服器配置

    # 服務端(192.168.1.6)
    # 機器配置:2cpu,4GB記憶體
    # 預先安裝docker、sysstat、git、make包
    
    # 客戶端(192.168.1.5)
    
    # 案例說明
    案例由Python應用、Redis兩部分組成
    Python應用是一個基於Flask的應用,它會利用Redis,來管理應用程式的快取
    並對外提供三個HTTP介面
    1. /:返回hello redis
    2. /init/:插入指定數量的快取資料,如果不指定數量,預設的是5000條
               快取的鍵格式為uuid
               快取的值為good、bad或normal三者之一
    3. /get_cache/<type_name>:查詢指定值的快取資料,並返回處理時間
               其中,type_name引數只支援good,bad和normal(也就是找出具有相同value的key列表)
               
    

  2. 在服務端,執行命令,執行本次案例需要分析的應用

    #
    [root@local_sa_192-168-1-6 ~]# docker run --name=redis -itd -p 10000:80 feisky/redis-server
    2d1811589334f0f5615b1b85936ddbc4ee565b85d73c6bc887c4f5cadf9f195f
    
    #
    [root@local_sa_192-168-1-6 ~]# docker run --name=app --network=container:redis -itd feisky/redis-app
    
    #
    [root@local_sa_192-168-1-6 ~]# docker ps
    CONTAINER ID   IMAGE                 COMMAND                  CREATED         STATUS          PORTS                                               NAMES
    fb2709a1ba1e   feisky/redis-app      "python /app.py"         2 minutes ago   Up 15 seconds                                                       app
    2d1811589334   feisky/redis-server   "docker-entrypoint.s…"   4 minutes ago   Up 4 minutes    6379/tcp, 0.0.0.0:10000->80/tcp, :::10000->80/tcp   redis
    
    

  3. 在客戶端,使用curl工具測試

    # 看到hello redis的輸出,說明應用正常啟動
    [root@local_deploy_192-168-1-5 ~]# curl http://192.168.1.6:10000/
    hello redis
    
    # 呼叫應用的/init介面,初始化Redis快取,並且插入5000條快取資訊
    # 案例插入5000條資料,在實踐時可以根據磁碟的型別適當調整
    [root@local_deploy_192-168-1-5 ~]# curl http://192.168.1.6:10000/init/5000
    
    # 訪問應用的快取查詢介面
    [root@local_deploy_192-168-1-5 ~]# curl http://192.168.1.6:10000/get_cache
    {"count":1677,"data":["d97662fa-06ac-11e9-92c7-0242ac110002",...],"elapsed_seconds":10.5}
    ##
    這個介面呼叫居然要花10秒!這麼長的響應時間,顯然不能滿足實際的應用需求
    
    # 為了避免分析過程中客戶端的請求結束
    [root@local_deploy_192-168-1-5 ~]# while true;do curl http://192.168.1.6:10000/get_cache; done
    
    

  4. 在服務端,執行top命令

    [root@local_sa_192-168-1-6 ~]# 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
    24428 systemd+  20 0 28352 9760 1860 D 5.0 0.1 0:04.34 redis-server
    
    ##
    觀察top的輸出可以發現,CPU0的iowait比較高,已經達到了84%
    而各個程序的CPU使用率都不太高
    最高的python和redis-server,也分別只有8%和5%
    再看記憶體,顯然記憶體也沒啥問題
    ##
    綜合top的資訊,最有嫌疑的就是iowait
    
    

  5. 在服務端,執行iostat命令

    [root@local_sa_192-168-1-6 ~]# iostat -d -x 1
    Linux 5.15.7-1.el7.elrepo.x86_64 (local_sa_192-168-1-6) 	2021年12月22日 	_x86_64_	(2 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
    sda               0.04     0.42    2.11    1.12  1808.76   159.83  1215.80     0.04   11.40    8.79   16.32   8.33   2.70
    scd0              0.00     0.00    0.00    0.00     0.00     0.00     0.33     0.00    0.11    0.11    0.00   0.56   0.00
    dm-0              0.00     0.00    0.85    1.19  1808.70   160.14  1925.44     0.07   33.17   20.18   42.44   8.44   1.73
    dm-1              0.00     0.00    0.00    0.00     0.01     0.00    51.80     0.00   32.79   32.79    0.00  19.53   0.00
    ##
    觀察iostat的輸出發現,磁碟sda每秒的寫資料(wkB/s)為159KB
    I/O 使用率(%util)是0
    雖然有些I/O操作,但並沒導致磁碟的I/O瓶頸
    
    

  6. 在服務端,執行pidstat命令

    [root@local_sa_192-168-1-6 ~]# pidstat -d 1
    Linux 5.15.7-1.el7.elrepo.x86_64 (local_sa_192-168-1-6) 	2021年12月22日 	_x86_64_	(2 CPU)
    14時47分07秒   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
    14時47分08秒   100     24428      0.00    178.22      0.00  redis-server
    
    14時47分08秒   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
    14時47分09秒   100     24428      0.00    180.00      0.00  redis-server
    
    14時47分09秒   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
    14時47分10秒   100     24428      0.00    172.00      0.00  redis-server
    
    平均時間:   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
    平均時間:   100     24428      0.00    186.59      0.00  redis-server
    ##
    從pidstat的輸出看到,I/O最多的程序是PID為24428的redis-server
    並且它也剛好是在寫磁碟,這說明確實是redis-server在進行磁碟寫
    
    

  7. 在服務端,執行strace和lsof命令

    # -f 表示跟蹤子程序和子執行緒
    # -T 表示顯示系統呼叫的時長
    # -tt 表示顯示跟蹤時間
    [root@local_sa_192-168-1-6 ~]#  strace -f -T -tt -p 24428
    [pid 24428] 14:47:35.448140 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 52, NULL, 8) = 1 <0.000142>
    [pid 24428] 14:47:35.448421 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:6c9c7502-"..., 16384) = 61 <0.000072>
    [pid 24428] 14:47:35.449600 read(3, 0x7fffabf2d427, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000070>
    [pid 24428] 14:47:35.449812 write(8, "$4\r\ngood\r\n", 10) = 10 <0.000089>
    ......
    [pid 24428] 14:47:35.450204 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 49, NULL, 8) = 1 <0.000164>
    [pid 24428] 14:47:35.450512 read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n6c9"..., 16384) = 67 <0.000083>
    [pid 24428] 14:47:35.451660 read(3, 0x7fffabf2d427, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000066>
    [pid 24428] 14:47:35.451839 write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n6c9"..., 67) = 67 <0.000073>
    [pid 24428] 14:47:35.451839 fdatasync(7) = 0 <0.005415>
    ......
    ##
    從系統呼叫來看,epoll_pwait、read、write、fdatasync這些系統呼叫都比較頻繁
    那麼,剛才觀察到的寫磁碟,應該就是write或者fdatasync導致的了
    ## lsof
    [root@local_sa_192-168-1-6 ~]# lsof -p 24428
    ......
    redis-ser 24428      100    3r     FIFO   0,13      0t0    124969 pipe
    redis-ser 24428      100    4w     FIFO   0,13      0t0    124969 pipe
    redis-ser 24428      100    5u  a_inode   0,14        0      8460 [eventpoll]
    redis-ser 24428      100    6u     sock    0,8      0t0    124970 protocol: TCP
    redis-ser 24428      100    7w      REG  253,0   948344 176182428 /data/appendonly.aof
    redis-ser 24428      100    8u     sock    0,8      0t0    126166 protocol: TCP
    ##
    描述符編號為3的是一個pipe管道
    5號是eventpoll
    7號是一個普通檔案
    8號是一個TCP socket
    ##
    結合磁碟寫的現象,只有7號普通檔案才會產生磁碟寫
    而它操作的檔案路徑是/data/appendonly.aof
    相應的系統呼叫包括write和fdatasync
    
    ## redis基礎知識
    Redis持久化配置中有appendonly和appendfsync選項
    很可能是因為它們的配置不合理,導致磁碟寫比較多
    
    

  8. 在服務端,檢視redis配置

    [root@local_sa_192-168-1-6 ~]# docker exec -it redis redis-cli config get 'append*'
    1) "appendfsync"
    2) "always"
    3) "appendonly"
    4) "yes"
    ##
    appendfsync配置的是always
    appendonly配置的是yes
    ########
    # Redis提供了兩種資料持久化的方式,分別是快照和追加檔案
    1. 快照方式,會按照指定的時間間隔,生成資料的快照,並且儲存到磁碟檔案中。
    為了避免阻塞主程序,Redis還會fork出一個子程序,來負責快照的儲存。
    這種方式的效能好,無論是備份還是恢復,都比追加檔案好很多
    不過,它的缺點也很明顯
    在資料量大時,fork子程序需要用到比較大的記憶體,儲存資料也很耗時
    所以,需要設定一個比較長的時間間隔來應對,比如至少5分鐘
    這樣,如果發生故障,丟失的就是幾分鐘的資料
    
    2. 追加檔案,則是用在檔案末尾追加記錄的方式,對Redis寫入的資料,依次進行持久化,所以它的持久化也更安全
    此外,它還提供了一個用appendfsync選項設定fsync的策略,確保寫入的資料都落到磁碟中
    具體選項包括always、everysec、no等
    always表示,每個操作都會執行一次fsync,是最為安全的方式
    everysec表示,每秒鐘呼叫一次fsync,這樣可以保證即使是最壞情況下,也只丟失1秒的資料
    no表示,交給作業系統來處理
    ########
    案例中appendfsync配置的是always,意味著每次寫資料時,都會呼叫一次fsync,從而造成比較大的磁碟I/O壓力
    
    

  9. 在服務端,執行strace命令,檢視系統呼叫的執行情況

    [root@local_sa_192-168-1-6 ~]# strace -f -p 24428 -T -tt -e fdatasync
    [pid 24428] 14:50:09.346201 fdatasync(7) = 0 <0.022991>
    [pid 24428] 14:50:09.373546 fdatasync(7) = 0 <0.020661>
    [pid 24428] 14:50:09.396243 fdatasync(7) = 0 <0.015645>
    [pid 24428] 14:50:09.416852 fdatasync(7) = 0 <0.019023>
    [pid 24428] 14:50:09.441939 fdatasync(7) = 0 <0.011531>
    [pid 24428] 14:50:09.455621 fdatasync(7) = 0 <0.021865>
    [pid 24428] 14:50:09.479623 fdatasync(7) = 0 <0.022748>
    [pid 24428] 14:50:09.506912 fdatasync(7) = 0 <0.013260>
    [pid 24428] 14:50:09.523493 fdatasync(7) = 0 <0.020520>
    [pid 24428] 14:50:09.548724 fdatasync(7) = 0 <0.020336>
    [pid 24428] 14:50:09.571242 fdatasync(7) = 0 <0.022781>
    [pid 24428] 14:50:09.596195 fdatasync(7) = 0 <0.015596>
    [pid 24428] 14:50:09.616554 fdatasync(7) = 0 <0.027589>
    [pid 24428] 14:50:09.648494 fdatasync(7) = 0 <0.020671>
    [pid 24428] 14:50:09.671379 fdatasync(7) = 0 <0.022808>
    [pid 24428] 14:50:09.696326 fdatasync(7) = 0 <0.015559>
    [pid 24428] 14:50:09.715756 fdatasync(7) = 0 <0.019982>
    [pid 24428] 14:50:09.744767 fdatasync(7) = 0 <0.082734>
    [pid 24428] 14:50:09.830990 fdatasync(7) = 0 <0.023048>
    [pid 24428] 14:50:09.857060 fdatasync(7) = 0 <0.013441>
    [pid 24428] 14:50:09.872742 fdatasync(7) = 0 <0.014465>
    ##
    呼叫非常頻繁
    ##
    找出了Redis正在進行寫入的檔案,也知道了產生大量I/O的原因
    ##
    為什麼查詢時會有磁碟寫呢?按理來說不應該只有資料的讀取嗎?
    [root@local_sa_192-168-1-6 ~]# strace -f -T -tt -p 24428
    read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-"..., 16384)
    write(8, "$4\r\ngood\r\n", 10)
    read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 16384)
    write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 67)
    write(8, ":1\r\n", 4)
    ##
    根據lsof的分析,檔案描述符編號為7的是一個普通檔案/data/appendonly.aof
    編號為8的是TCP socket
    觀察上面的內容,8號對應的TCP讀寫,是一個標準的“請求-響應”格式,即
    從socket讀取GET uuid:53522908-… 後,響應good
    再從socket讀取SADD good 535… 後,響應1
    ##
    對Redis來說,SADD是一個寫操作,所以Redis還會把它儲存到用於持久化的appendonly.aof檔案中
    觀察更多的strace結果發現,每當GET返回good時,隨後都會有一個SADD操作
    這也就導致了,明明是查詢介面,Redis卻有大量的磁碟寫
    到這裡就找出了Redis寫磁碟的原因
    ###
    可以給lsof命令加上-i選項,找出TCP socket對應的TCP連線資訊
    不過,由於Redis和Python應用都在容器中執行
    需要進入容器的網路名稱空間內部,才能看到完整的TCP連線
    nsenter工具,可以進入容器名稱空間
    
    

  10. 在服務端,執行nsenter工具

    # 如果系統沒有安裝,請執行下面命令安裝nsenter
    [root@local_sa_192-168-1-6 ~]# docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter
    
    # 由於這兩個容器共享同一個網路名稱空間,所以只需要進入app的網路名稱空間即可
    [root@local_sa_192-168-1-6 ~]# PID=$(docker inspect --format {{.State.Pid}} app)
    
    # -i 表示顯示網路套接字資訊
    [root@local_sa_192-168-1-6 ~]# nsenter --target $PID --net -- lsof -i
    COMMAND     PID     USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    lsof: no pwd entry for UID 100
    redis-ser 24428      100    6u  IPv4 124970      0t0  TCP localhost:6379 (LISTEN)
    lsof: no pwd entry for UID 100
    redis-ser 24428      100    8u  IPv4 126166      0t0  TCP localhost:6379->localhost:35994 (ESTABLISHED)
    python    24623     root    3u  IPv4 125178      0t0  TCP *:http (LISTEN)
    python    24623     root    4u  IPv4 129547      0t0  TCP local_sa_192-168-1-6:http->192.168.1.5:42294 (ESTABLISHED)
    python    24623     root    5u  IPv4 126165      0t0  TCP localhost:35994->localhost:6379 (ESTABLISHED)
    ##
    這次可以看到,redis-server的8號檔案描述符,對應TCP連線 localhost:6379->localhost:35994
    其中,localhost:6379是redis-server自己的監聽埠,自然localhost:35994就是redis的客戶端
    再觀察最後一行,localhost:35994對應的,正是Python應用程式(程序號為24623)
    
    

  11. 2個問題

    第一個問題,Redis配置的appendfsync是always,這就導致Redis每次的寫操作,都會觸發fdatasync系統呼叫
    今天的案例,沒必要用這麼高頻的同步寫,使用預設的1s時間間隔,就足夠了
    
    第二個問題,Python應用在查詢介面中會呼叫Redis的SADD命令,這很可能是不合理使用快取導致的
    
    ## 解決辦法
    # 1. 對於第一個配置問題,可以執行下面的命令把appendfsync改成everysec
    [root@local_sa_192-168-1-6 ~]# docker exec -it redis redis-cli config set appendfsync everysec
    ##
    改完後會發現,現在的請求時間已經縮短到了0.9秒
    
    # 2. 第二個問題,就要檢視應用的原始碼了
    [root@local_sa_192-168-1-6 ~]# docker cp app:/app.py .
    
    [root@local_sa_192-168-1-6 ~]# cat app.py 
    #!/usr/bin/env python
    # -*- coding: UTF-8 -*-
    from functools import wraps
    from time import time
    import json
    import random
    import uuid
    import redis
    from flask import Flask, jsonify
    
    app = Flask(__name__)
    redis_client = redis.StrictRedis(
        host="127.0.0.1", charset="utf-8", port=6379, db=0, decode_responses=True)
    
    
    def timing(f):
        '''wrapper for elapsed_seconds'''
        @wraps(f)
        def wrapper(*args, **kwargs):
            start = time()
            result = f(*args, **kwargs)
            end = time()
            data = json.loads(result.get_data())
            data["elapsed_seconds"] = end-start
            return jsonify(data)
        return wrapper
    
    
    @app.route('/')
    def hello_world():
        '''handler for /'''
        return 'hello redis\n'
    
    
    @app.route('/get_cache', defaults={'type_name': 'good'})
    @app.route("/get_cache/<type_name>")
    @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})
    
    
    @app.route('/get_cache_data', defaults={'type_name': 'good'})
    @app.route("/get_cache_data/<type_name>")
    @timing
    def get_cache_data(type_name):
        '''handler for /get_cache_data'''
        keys = [key for key in redis_client.scan_iter("uuid:*")]
        values = redis_client.mget(keys)
        matched_keys = set([keys[idx][5:] for idx, value in enumerate(values) if value == type_name ])
        return jsonify({"type": type_name, 'count': len(matched_keys), 'data': list(matched_keys)})
    
    
    @app.route('/init', defaults={'num':5000})
    @app.route('/init/<num>')
    @timing
    def initialize(num):
        '''handler for /init'''
        redis_client.flushall()
        types = ('good', 'bad', 'normal')
        sample_size = int(num)
        random_pool = random.sample(range(0, sample_size), sample_size)
    
        for i in range(sample_size):
            key = uuid.uuid1()
            redis_client.set('uuid:{}'.format(key), types[(random_pool[i]+i) % 3])
    
        return jsonify({"keys_initialized": sample_size})
    
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=80)
        
    ##
    果然,Python應用把Redis當成臨時空間,用來儲存查詢過程中找到的資料
    這些資料放記憶體中就可以了,完全沒必要再通過網路呼叫儲存到Redis中
    ##
    這個介面把查詢過程中產生的資料直接儲存在記憶體了
    http://192.168.1.6:10000/get_cache_data
    
    



小結

Redis快取的案例
先用top、iostat ,分析了系統的CPU 、記憶體和磁碟使用情況
不過卻發現,系統資源並沒有出現瓶頸

這個時候想要進一步分析的話,該從哪個方向著手呢?
通過今天的案例發現,為了進一步分析,就需要對系統和應用程式的工作原理有一定的瞭解

比如,今天的案例中,雖然磁碟I/O並沒有出現瓶頸,但從Redis的原理來說,查詢快取時不應該出現大量的磁碟I/O寫操作

順著這個思路,繼續借助pidstat、strace、lsof、nsenter等一系列的工具
找出了兩個潛在問題
一個是Redis的不合理配置
另一個是Python應用對Redis的濫用
找到瓶頸後,相應的優化工作自然就比較輕鬆了


轉載請註明出處喲~ https://www.cnblogs.com/lichengguo