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

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

82f797c1864841a4aca94079b8508004?s=47 Yoshiyuki Asaba
September 11, 2018
10k

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

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

82f797c1864841a4aca94079b8508004?s=128

Yoshiyuki Asaba

September 11, 2018
Tweet

Transcript

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

  2. 2 準備

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

    9999 &
  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
  7. 7 概要

  8. Redisって何? 8 • インメモリのいくつかのデータ構造を格納するためのサーバーソフトウェア Key Value Storeの一種 Valueには文字列型(勝手にintegerにする場合もある)、リスト型、位置情報などを入れられる • C言語で書かれている

    • シングルプロセス、シングルスレッド ただしバックグラウンド処理を別スレッドで動かす場合もある • クラスタリングできたり、データを外に書き出したりもできる ただ、RDBMSと比較してデータ信頼性を当てにしないほうが良いです(そういう思想で作って いる)
  9. 9 Redisをソフトウェアとして見ると • ネットワーク経由でRedisプロトコルにしたがってメッセージを送受信 • 巨大なハッシュテーブルと、バリューはデータ構造に応じた処理 • ストレージ (今回は見ない) •

    replication (今回は見ない)
  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
  11. 11 ハッシュテーブルとハッシュ値 • ハッシュテーブルはキーを何らかのハッシュ関数でハッシュ値にして、巨大な配列へアクセ スする RedisはSipHashを利用 • インデックスの衝突 チェイン法 (Redisはこっち)

    オープンアドレス法 • rehashing だんだんhash tableがでかくなると衝突しやすくなってくるので、サイズを拡張する Redisでどうやっているかはコードを読んでみてね バケット abc 3458911415056046202 ハッシュテーブル Sip hash function ハッシュ値からインデックスに変換
  12. 12 ソースコードリーディング with gdb

  13. ソースコードリーディングで気をつけること 13 • 何を読むかをテーマを絞る 大抵のミドルウェアのソースコードは巨大なので、main関数から読もうとするとすぐに迷います • ドキュメントを先に読む プロセスモデル、スレッドモデル、シグナル処理などはそのミドルウェアの特性として大事な情 報なのでドキュメントに書かれている 書かれていない場合でもソースコードにあるコメントやREADMEを見るとなんかわかるかも

    • 動かして読んだほうがコードをつかみやすい どうやってdebug buildするかを知る必要がある どこにbreakpointを貼るかを推測する必要がある(エラーメッセージ、システムコール、関数名 などから) 紙とペンでメモをとりながら読むことが個人的には多い
  14. 14 gdbの動かし方 1. main関数から動かす % gdb hoge 2. 今動いているプロセスにアタッチして途中から始める %

    gdb -p pid (pidはps コマンドやRedisのログをみてください) ただ、素のgdbだとつらいので、cgdbかemacsでgdbを動かす、もしくは別のなにかGUIをもつも の(DDD等)を使うのをおすすめします
  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
  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
  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
  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} };
  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
  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 <dbDictType>, 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}
  21. 21 ハッシュテーブルデータ構造 • dict 通常はht[0]がハッシュテーブルへのポインタ rehash中は[1]にも入る • dictht **tableがバケット(単方向リスト)の配列を指す size:

    bucket size sizemask: ハッシュ値からどのバケットへ入れるかを計算するための値 idx = h & d->ht[table].sizemask; という計算をしているのがそう
  22. 22 例2:KEYSコマンド keysとは特定のキーを探すコマンド ハッシュテーブルは、ハッシュ値に変換してたどるデータ構造なので、キーを探すためには全 データを辿らないといけない(フルスキャン)

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

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