Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

2 準備

Slide 3

Slide 3 text

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'

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

7 概要

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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} };

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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}

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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