Upgrade to Pro — share decks privately, control downloads, hide ads and more …

デバッガでRedisのコードを読んでみよう

Yoshiyuki Asaba
September 11, 2018
13k

 デバッガでRedisのコードを読んでみよう

freee社内でgdbを使ってRedisのソースコードを読む勉強会をしたときの資料です。

Yoshiyuki Asaba

September 11, 2018
Tweet

Transcript

  1. freee 株式会社
    デバッガでRedisのコードを読んでみよう
    y-asaba

    View Slide

  2. 2
    準備

    View Slide

  3. 3
    デバッグ環境準備
    今回はRedis4.0.11を対象とします。OSはLinux(Ubuntu)を想定しています。
    もしくはDockerを動かせる環境であればなんでも大丈夫です。
    ● 必要なツールのインストール
    sudo apt-get install build-essential gdb cgdb
    ● Emacsを使いたい人(任意)
    sudo add-apt-repository ppa:kelleyk/emacs
    sudo apt-get update
    sudo apt-get install emacs26-nox
    ● kernelの設定
    sudo bash -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'

    View Slide

  4. 4
    デバッグビルド
    % wget https://github.com/antirez/redis/archive/4.0.11.tar.gz
    % tar zxvf 4.0.11.tar.gz
    % cd redis-4.0.11/src
    % make noopt
    % ./redis-server --port 9999

    View Slide

  5. 5
    デバッグビルドしたRedisを起動する
    ● srcディレクトリにあるredis-serverを起動
    使っていないポート番号(9999など)を選んでください
    gdbから起動してもよいです
    cd src/
    ./redis-server --port 9999 &

    View Slide

  6. 6
    Dockerを使う場合
    % git clone https://github.com/y-asaba/docker-redis-debug.git
    % cd docker-redis-debug
    % sudo docker build -t debug-redis 4.0
    % sudo docker run -it -p 9876:9876 --cap-add=SYS_PTRACE --security-opt
    seccomp=unconfined debug-redis:latest /bin/bash
    コンテナ内でのコマンド
    cd /root/redis-4.0.11/src
    ./redis-server --port 9876 --protected-mode no &
    gdb -p pid
    コンテナ内へのredisへの接続
    redis-cli -h 127.0.0.1 -p 9876

    View Slide

  7. 7
    概要

    View Slide

  8. Redisって何?
    8
    ● インメモリのいくつかのデータ構造を格納するためのサーバーソフトウェア
    Key Value Storeの一種
    Valueには文字列型(勝手にintegerにする場合もある)、リスト型、位置情報などを入れられる
    ● C言語で書かれている
    ● シングルプロセス、シングルスレッド
    ただしバックグラウンド処理を別スレッドで動かす場合もある
    ● クラスタリングできたり、データを外に書き出したりもできる
    ただ、RDBMSと比較してデータ信頼性を当てにしないほうが良いです(そういう思想で作って
    いる)

    View Slide

  9. 9
    Redisをソフトウェアとして見ると
    ● ネットワーク経由でRedisプロトコルにしたがってメッセージを送受信
    ● 巨大なハッシュテーブルと、バリューはデータ構造に応じた処理
    ● ストレージ (今回は見ない)
    ● replication (今回は見ない)

    View Slide

  10. 10
    C言語おさらい(正確に知りたい場合は文献を調べて)
    ● シンプルな言語(だと個人的には思っている)
    メモリ管理は自分でやらないといけない
    syntaxはなんとなくわかると思う
    ● 構造体
    データの型を定義しているといえるが、一方で必要なサイズのバイト列を定義しているとも言
    える
    void* はとくに何でもありな世界
    (gdb) ptype robj
    type = struct redisObject {
    unsigned int type : 4;
    unsigned int encoding : 4;
    unsigned int lru : 24;
    int refcount;
    void *ptr;
    }
    (gdb) p *val
    $75 = {
    type = 0,
    encoding = 1,
    lru = 9087676,
    refcount = 2147483647,
    ptr = 0x3
    }
    (gdb) x /12x val
    0x7fe0a8017bb0: 0x10 0xbc 0xaa 0x8a 0xff 0xff 0xff
    0x7f
    0x7fe0a8017bb8: 0x03 0x00 0x00 0x00

    View Slide

  11. 11
    ハッシュテーブルとハッシュ値
    ● ハッシュテーブルはキーを何らかのハッシュ関数でハッシュ値にして、巨大な配列へアクセ
    スする
    RedisはSipHashを利用
    ● インデックスの衝突
    チェイン法 (Redisはこっち)
    オープンアドレス法
    ● rehashing
    だんだんhash tableがでかくなると衝突しやすくなってくるので、サイズを拡張する
    Redisでどうやっているかはコードを読んでみてね
    バケット
    abc 3458911415056046202
    ハッシュテーブル
    Sip hash function
    ハッシュ値からインデックスに変換

    View Slide

  12. 12
    ソースコードリーディング with gdb

    View Slide

  13. ソースコードリーディングで気をつけること
    13
    ● 何を読むかをテーマを絞る
    大抵のミドルウェアのソースコードは巨大なので、main関数から読もうとするとすぐに迷います
    ● ドキュメントを先に読む
    プロセスモデル、スレッドモデル、シグナル処理などはそのミドルウェアの特性として大事な情
    報なのでドキュメントに書かれている
    書かれていない場合でもソースコードにあるコメントやREADMEを見るとなんかわかるかも
    ● 動かして読んだほうがコードをつかみやすい
    どうやってdebug buildするかを知る必要がある
    どこにbreakpointを貼るかを推測する必要がある(エラーメッセージ、システムコール、関数名
    などから)
    紙とペンでメモをとりながら読むことが個人的には多い

    View Slide

  14. 14
    gdbの動かし方
    1. main関数から動かす
    % gdb hoge
    2. 今動いているプロセスにアタッチして途中から始める
    % gdb -p pid
    (pidはps コマンドやRedisのログをみてください)
    ただ、素のgdbだとつらいので、cgdbかemacsでgdbを動かす、もしくは別のなにかGUIをもつも
    の(DDD等)を使うのをおすすめします

    View Slide

  15. 15
    gdbコマンド集
    debuggerはプログラムを止める、動かす、中身を見るというのをやるツールです
    いろいろコマンドがありますが、そんなに覚えていないです
    ● 止める
    b function名
    ● 動かす
    next, step, continue, finish
    ● 見る
    bt, thread apply all bt
    p, ptype,
    ● 設定
    set print pretty
    ● info
    info threads
    info b

    View Slide

  16. 16
    簡単な練習1
    processをアタッチした直後のbacktraceをみてみよう
    (gdb) attach 7242
    ...
    (gdb) bt
    #0 0x00007f324bbb6a13 in epoll_wait () at
    ../sysdeps/unix/syscall-template.S:84
    #1 0x000000000042636c in aeApiPoll (eventLoop=0x7f324b63a0f0,
    tvp=0x7ffe82eba3c0) at ae_epoll.c:112
    #2 0x000000000042702b in aeProcessEvents (eventLoop=0x7f324b63a0f0, flags=11)
    at ae.c:411
    #3 0x0000000000427322 in aeMain (eventLoop=0x7f324b63a0f0) at ae.c:501
    #4 0x0000000000434210 in main (argc=3, argv=0x7ffe82eba568) at server.c:3899

    View Slide

  17. 17
    簡単な練習2
    今動いているスレッド一覧をみてみよう
    (gdb) info threads
    Id Target Id Frame
    * 1 Thread 0x7f324c7bc780 (LWP 7242) "redis-server" 0x00007f324bbb6a13 in
    epoll_wait () at ../sysdeps/unix/syscall-template.S:84
    2 Thread 0x7f324b5ff700 (LWP 7243) "redis-server"
    pthread_cond_wait@@GLIBC_2.3.2 ()
    at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
    3 Thread 0x7f324adfe700 (LWP 7244) "redis-server"
    pthread_cond_wait@@GLIBC_2.3.2 ()
    at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
    4 Thread 0x7f324a5fd700 (LWP 7245) "redis-server"
    pthread_cond_wait@@GLIBC_2.3.2 ()
    at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185

    View Slide

  18. 18
    Redis command table
    server.c
    2番目(getCommandとか)が、実際のコマンドを処理する関数なので、それにbreakpointを貼れ
    ば良い
    struct redisCommand redisCommandTable[] = {
    {"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},

    {"post",securityWarningCommand,-1,"lt",0,NULL,0,0,0,0,0},
    {"host:",securityWarningCommand,-1,"lt",0,NULL,0,0,0,0,0},
    {"latency",latencyCommand,-2,"aslt",0,NULL,0,0,0,0,0}
    };

    View Slide

  19. 19
    例題1: SETとGETの挙動を追う
    SETをまずはみてみましょう
    ● breakpoint
    setCommandsにbreakpointを設定します
    redis-cliでset commandを叩く
    stack traceを見てみる
    frameを移動してみる
    (gdb) b setCommand
    Breakpoint 8 at 0x457005: file t_string.c,
    line 98.
    (gdb) c
    Continuing.
    % redis-cli --port 8000
    127.0.0.1:8000> set hoge 111

    View Slide

  20. 20
    ハッシュテーブルデータ構造
    (gdb) ptype dict
    type = struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;
    unsigned long iterators;
    }
    (gdb) p *d
    $94 = {type = 0x774060 ,
    privdata = 0x0, ht = {{table =
    0x7fe0a80223c0, size = 8, sizemask =
    7, used = 5}, {table = 0x0, size = 0,
    sizemask = 0, used = 0}}, rehashidx =
    -1,
    iterators = 0}
    (gdb) ptype dictht
    type = struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
    }
    (gdb) p *ht->table[index]
    $96 = {key = 0x7fe0a8016769, v = {val
    = 0x7fe0a8017b90, u64 =
    140602868071312,
    s64 = 140602868071312, d =
    6.9467046820784346e-310}, next = 0x0}

    View Slide

  21. 21
    ハッシュテーブルデータ構造
    ● dict
    通常はht[0]がハッシュテーブルへのポインタ
    rehash中は[1]にも入る
    ● dictht
    **tableがバケット(単方向リスト)の配列を指す
    size: bucket size
    sizemask: ハッシュ値からどのバケットへ入れるかを計算するための値
    idx = h & d->ht[table].sizemask; という計算をしているのがそう

    View Slide

  22. 22
    例2:KEYSコマンド
    keysとは特定のキーを探すコマンド
    ハッシュテーブルは、ハッシュ値に変換してたどるデータ構造なので、キーを探すためには全
    データを辿らないといけない(フルスキャン)

    View Slide

  23. 23
    iterator
    (gdb) ptype iter
    type = struct dictIterator {
    dict *d;
    long index;
    int table;
    int safe;
    dictEntry *entry;
    dictEntry *nextEntry;
    long long fingerprint;
    } *
    ● dictNextをみるとたどっているのがわかる
    (gdb) bt
    #0 dictNext (iter=0x7fe0a801c5a0) at dict.c:565
    #1 0x0000000000448dba in keysCommand (c=0x7fe0a811b700) at
    db.c:529
    #2 0x000000000042f9b0 in call (c=0x7fe0a811b700, flags=15) at
    server.c:2229
    #3 0x00000000004305c9 in processCommand (c=0x7fe0a811b700) at
    server.c:2515
    #4 0x0000000000440be2 in processInputBuffer (c=0x7fe0a811b700) at
    networking.c:1357
    #5 0x0000000000440fb5 in readQueryFromClient (el=0x7fe0a803a0f0,
    fd=8,
    privdata=0x7fe0a811b700, mask=1) at networking.c:1447
    #6 0x0000000000427108 in aeProcessEvents
    (eventLoop=0x7fe0a803a0f0, flags=11) at ae.c:443
    #7 0x0000000000427322 in aeMain (eventLoop=0x7fe0a803a0f0) at
    ae.c:501
    #8 0x0000000000434210 in main (argc=3, argv=0x7ffec121be08) at
    server.c:3899

    View Slide

  24. 24
    まとめ
    ● ミドルウェアのコードは意外と読めるのでチャレンジしてみてください
    ● データ構造とアルゴリズムを意識し、どういうケースが強い・弱いのかを理解する
    ある程度パターンを掴むと、このミドルウェアはここが強いんだなという勘が働く(気がする)

    View Slide

  25. スモールビジネスを、
    世界の主役に。

    View Slide