k8s上でGDBを使ってrubyをデバッグする

# 環境
ruby 2.5
GNU gdb (Debian 7.12-6)
Kubernetes 1.10

# gdbを使うモチベーション
この記事をご覧の皆さんも普段からデバッグすることは多いと思います。そして、大抵の場合`p 変数`で済みます。ですが、たまに必殺`p`が使えない状況があります。

### 例1)　謎のプロセスいるんですけど？（怒） ###
[Finding a Ruby bug with GDB](https://medium.com/@zanker/finding-a-ruby-bug-with-gdb-56d6b321bc86)（超面白いです。）という記事では、「**たまにPumaの古いプロセスが見える**」というバグレポートを元にGDBでデバッグしているが、古い(何週間というレベル)ので例外やログを終えず、そもそも欲しいものが適切に記録されているかも怪しい。**ただ、デバッグすべきプロセスは目の前にいるのである。**

### 例２) なんか知らんけど止まっているですけど？（怒） ###
よく分からんが、ごく稀に停止する。ただ、普段は意図した通りに動く。とりあえずは`ps xHopid,lwp,wchan,command`とか打って、停止している原因に迫ることは一応できるが、これだけの情報では原因を特定することはかなり難しい。**ただ、デバッグすべきプロセスは目の前にいるのである。**

### デバッガを使おう
上のような状況（IDEが使えない環境）でデバッグするなら、デバッガの出番だ。デバッガはかなり色んなことができる。例えば、今動いているプロセスのとあるスレッドのコールスタックを見ることができる。コールスタックを見る以外にも、動的にrubyのコードを実行できたりする。さらに、面白い使い方としてファイルディスクリプタを変更することもできる。つまり、稼働中のプロセスのログが`/dev/null`に流していたが、それを変更することも可能。それについては[この記事](https://qiita.com/maueki/items/ad5faa35146865fda382)によくまとまっている。ただし、デバッガをアタッチするとプロセスは停止するので、その点は気をつけなければいけない。


# 準備編) gdbをdocker環境で使えるようにする
とりあえず準備しますが、興味ない人は読み飛ばして下さい。docker環境でgdbを使えるようにするにはちょっと骨が折れます。何も考えずやると、`ptrace: Operation not permitted.`とか言われます。


### コンテナを1体立ち上げて、gdbを実行する ###
これは簡単。

```
docker pull ubuntu
```

とかして、

```
docker run -it --cap-add=SYS_PTRACE --security-opt="seccomp=unconfined" --name hoge ubuntu /bin/bash
```

とかすれば、コンテナ内でgdbを実行できる。`Linux`では権限をいくつかのグループに分割していて、そのグループは`capability`と呼ばれ、各capabilityは特定の操作や動作を行う権限を得ることができる。詳しくは[マニュアル](https://linuxjm.osdn.jp/html/LDP_man-pages/man7/capabilities.7.html)を見て欲しい。そして、上のdockerコマンドでは`capability`に、`SYS_PTRACE`を追加している。これはgdbが使っているシステムコールである`ptrace`を許可するものである。セキュリティの問題で、コンテナの中はかなり権限的に制限されているので、権限を付与してやらないといけない。


### Kubernetes ###
僕がgdbを実際に使った状況では、`Kubernetes`により構成される環境でした。[この記事](https://kirshatrov.com/2018/04/13/debugging-ruby-in-kubernetes/)を参考にしたのですが、`Deployment spec`に次のように書くと大丈夫です。

```
spec:
  restartPolicy: Always
  containers:
    - name: hoge
      image: hoge_image:0.0.0.0
      securityContext:
        capabilities:
          add:
            - SYS_PTRACE
      command:
        - hoge
```

### docker-compose ###
これには敗北しました。`docker-compose.yml`に次のように書けばいけそうなのですが、いけません。

```
services:
  fuga:
    (略)
    cap_add:
      - SYS_PTRACE
    security_opt:
      - seccomp:unconfined
```

[このissue](https://github.com/docker/compose/issues/5169)は僕と同じことをして、「できないんですけど？」と言っているのですが、だいたい次のような回答をもらっています（ただし、使っているシステムコールは`strace`です）。

```
services:
  foo:
    command: strace /bin/ls
    build: .
    cap_add:
      - SYS_PTRACE
    security_opt:
      - seccomp:unconfined
```

うーん、`command`でやるのって違うよなぁ。やりたいときにやりたいよね。


# Let's debug
さて、それでは実際にやってみます。`k8s`で稼働している`pod`に入って、その中のとあるスレッドのコールスタックを見てみます。`kubectl get pods`を実行してみます。

```
NAME                   READY   STATUS      RESTARTS   AGE
batch-aaaaaaaaaa       0/1     Completed   0          1m
daemon-bbbbbbbbbb      1/1     Running     0          5s
server-cccccccccc      1/1     Running     0          1d
```

次に、`daemon-bbbbbbbbbb`の中に入ります。`kubectl exec -it daemon-bbbbbbbbbb bash`を実行して、daemon`pod`の中に入ります。`ps xopid,command`を実行すると、

```
  PID COMMAND
    1 /usr/local/bundle/bin/rake daemon
    1 /usr/local/bundle/bin/rake daemon
    1 /usr/local/bundle/bin/rake daemon
    1 /usr/local/bundle/bin/rake daemon
    1 /usr/local/bundle/bin/rake daemon
    1 /usr/local/bundle/bin/rake daemon
    (略)
 1078 bash
 8902 ps xHopid,command
```

この`daemon`プロセスはマルチスレッドなプロセスなので同じ`PID`の行がたくさん出力されます。gdbをアタッチするプロセスの`PID`は1であることが分かりました。アタッチする前に確認しなければならないことが1つあります。gdbをアタッチしてコマンドを実行するわけですが、その結果はgdbプロセスの標準出力ではなく、対象プロセスの標準エラーに出力されます。なので、`PID`が1のプロセスの標準エラーの向き先がどうなっているのかを確認したいです。
そこで、`ls -la /proc/1/fd/`を実行してみます。

```
total 0
dr-x------ 2 tomita_kazuya tomita_kazuya  0 Dec  9 08:57 .
dr-xr-xr-x 9 tomita_kazuya tomita_kazuya  0 Dec  9 08:57 ..
lr-x------ 1 tomita_kazuya tomita_kazuya 64 Dec  9 08:57 0 -> pipe:[31993418]
l-wx------ 1 tomita_kazuya tomita_kazuya 64 Dec  9 08:57 1 -> pipe:[31993420]
lrwx------ 1 tomita_kazuya tomita_kazuya 64 Dec  9 09:01 10 -> socket:[32011592]
lrwx------ 1 tomita_kazuya tomita_kazuya 64 Dec  9 08:57 11 -> socket:[31992811]
lrwx------ 1 tomita_kazuya tomita_kazuya 64 Dec  9 09:01 12 -> socket:[32279584]
l-wx------ 1 tomita_kazuya tomita_kazuya 64 Dec  9 08:57 2 -> pipe:[31993420]
(略)
```

標準エラーの向き先は標準出力と同様(pipe:[31993420])みたいですね。よって、`kubectl logs`コマンドでgdbのコマンドの結果を確認できることが分かりました。ただ、この場合は`kubectl logs`で確認できますが、どこに向いてるかよく分からないケースもあると思います。その時は、`/tmp/hoge`みたいなファイルに書き出したり、向き先を標準出力にしたりするといいと思います。

いよいよアタッチします。
コンソールで`gdb /usr/local/bundle/bin/ 1`と打つと、gdbを実行することができ、入力待ちになります。その状態で`info threads`と打ちます。

```
(gdb) info threads
  Id   Target Id              Frame
* 1    Thread 0x7f61238a4700 (LWP 1) "bundle" 0x00007f6122f5a508 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
  2    Thread 0x7f61238b3700 (LWP 7) "ruby-timer-thr" 0x00007f612254d67d in poll () from /lib/x86_64-linux-gnu/libc.so.6
  3    Thread 0x7f611a49a700 (LWP 8) "connection_poo*" 0x00007f6122f5a508 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
  4    Thread 0x7f6115bf1700 (LWP 9) "hoge.rb:1*" 0x00007f6122f5a15f in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
  5    Thread 0x7f6115af0700 (LWP 10) "hoge.rb:1*" 0x00007f6122f5a15f in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
  (略)
```

こんな感じの出力がでます。1つ目は`bundle`のスレッド、2つ目はタイマースレッド、3つ目は`ActiveRecord`の`Reaper`スレッドだと思われます。4つめ以降が僕が起動しているスレッド達です。

```
(gdb) thread 4
[Switching to thread 4 (Thread 0x7f6115bf1700 (LWP 9))]
#0  0x00007f6122f5a15f in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
```

スレッドを切り替え、最後にコールスタックを出力します。

```
(gdb) call rb_print_backtrace
$1 = {void (void)} 0x7f6123398d00 <rb_print_backtrace>
```

これで、`kubectl logs`コマンドでスレッド4のコールスタックを確認できます。

# 課題) その他のコマンドが実行できない
`rb_eval_string`を始めとしたそのほかのコマンドがほとんど実行できませんでした。
次のエラーメッセージが出ます。

```
Couldn't write extended state status: Bad address.
```

この英文で検索すると、この[香ばしいバグレポートのページ](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=898048)が出てきます。

>I was now able to test my hypothesis that this is/was a bug in Linux 
that has been fixed since.

とのことなので、OSがアップグレードされれば改善されそうです。

# まとめ
GDBで良いデバッグライフを！