前回 uWSGI の prefork を調べた際に、任意の Worker のスレッドダンプを出力する方法を見つけられなかったので、スレッドダンプを JSON として返す function を書きました。

しかし、Master / Worker で統一的にスレッドダンプを取りたかったので色々と試した結果、pystuck を使ってそれぞれのスレッドダンプを取得することができました。

pystuck

pystuck は、RPyC というリモートプロシージャコールを使って、RPyC のサーバー側のスレッドダンプを返します。

import pystuck
pystuck.run_server()

サーバー側で上記のように記載すると host=127.0.0.1 , port=6666RPyC のリクエストを待ちます。

pystuck コマンドを実行すると、クライアントが起動します。--host--port を指定することで、接続する Server の IP と Port を変更できます。クライアントが起動すると以下のように表示されます。

# pystuck
Welcome to the pystuck interactive shell.
Use the 'modules' dictionary to access remote modules (like 'os', or '__main__')
Use the `%show threads` magic to display all thread stack traces.

In [1]: 

あとは %show threads とタイプすれば、Server のスレッドダンプを見ることができます(後述)。

Worker

uWSGI の Master プロセスで pystuck の Server を起動するには app.py__init__.pypystuck.run_server() を実行すれば良いのですが、それぞれの Worker のスレッドダンプを取得するには、Worker 側でも pystuck の Server を起動する必要もあります。uWSGI の場合は @postfork という decorator を使い、 fork 後に Server を起動するようにしました。

@postfork
def worker_init():
    pystuck.run_server()

Unix Domain Socket

pystuck.run_server() を実行する際に、Master / Worker 間で同じ port を使わないようにする必要があるのですが、調整がちょっと面倒なので Unix Domain Socket を使います。pystuck では Server は Unix Domain Socket が使えるのですが、Client は使えるようになっていなかったので、使えるように修正しました。

Master と Worker でファイルパスが重複しないように、Worker 側のファイル名には PID を入れました。

Code

サンプルコードは以下のとおりです。

 1from time import sleep
 2import os
 3import json
 4import threading
 5
 6from flask import Flask, jsonify
 7
 8from uwsgidecorators import postfork
 9import pystuck
10pystuck.run_server(unix_socket='/tmp/utd-master')
11
12i = 0
13app = Flask(__name__)
14
15@postfork
16def worker_init():
17    pystuck.run_server(unix_socket=f'/tmp/utd-{os.getpid()}')
18
19
20def process():
21    while True:
22        try:
23            global i
24            i += 1
25            print(f"i: {i}")
26        except Exception as err:
27            print(f"err: {err}")
28
29        sleep(5)
30
31
32th = threading.Thread(target=process, name='test-thread', daemon=False)
33th.start()
34
35
36@app.route('/hello')
37def hello_world():
38    sleep(5)
39    return jsonify({'message': f'Hello World, {i}, {threading.currentThread().ident}'})

これを実行する(worker=2)と、uWSGI のプロセスが以下のように起動します。

root@6caf7639d198:/# ps afx
  PID TTY      STAT   TIME COMMAND
  575 pts/4    Ss     0:00 bash
  589 pts/4    R+     0:00  \_ ps afx
    6 pts/1    Ss     0:00 bash
  523 pts/1    Sl+    0:01  \_ uwsgi uwsgi.ini
  526 pts/1    Sl+    0:00      \_ uwsgi uwsgi.ini
  527 pts/1    Sl+    0:00      \_ uwsgi uwsgi.ini
  528 pts/1    S+     0:00      \_ uwsgi uwsgi.ini
    1 pts/0    Ss+    0:00 /bin/bash

そして以下のようにファイルが生成されます。

root@6caf7639d198:/# ls -l /tmp
total 0
srwxr-xr-x 1 root root 0 Sep 29 15:53 utd-526
srwxr-xr-x 1 root root 0 Sep 29 15:53 utd-527
srwxr-xr-x 1 root root 0 Sep 29 15:53 utd-master

PID=528 は Worker ではなく uWSGI の httpd のプロセスなので、ファイルは出力されません。

あとはスレッドダンプを出力したいプロセスのファイルを指定して pystuck コマンドを起動します。

root@6caf7639d198:/# pystuck --unix-socket /tmp/utd-527
Welcome to the pystuck interactive shell.
Use the 'modules' dictionary to access remote modules (like 'os', or '__main__')
Use the `%show threads` magic to display all thread stack traces.

In [1]: %show threads
<Thread(Thread-2, started daemon 139703068108544)>
  File "/usr/local/lib/python3.7/threading.py", line 890, in _bootstrap
    self._bootstrap_inner()
  File "/usr/local/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "/root/works/app.py", line 25, in process
    print(f"i: {i}")

<Thread(Thread-5, started daemon 139703076501248)>
  File "/usr/local/lib/python3.7/threading.py", line 890, in _bootstrap
    self._bootstrap_inner()
  File "/usr/local/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.7/site-packages/rpyc/utils/server.py", line 262, in start
    self.accept()
  File "/usr/local/lib/python3.7/site-packages/rpyc/utils/server.py", line 138, in accept
    sock, addrinfo = self.listener.accept()
  File "/usr/local/lib/python3.7/socket.py", line 212, in accept
    fd, addr = self._accept()

Conclusion

pystuck を使って uWSGI の Master / Worker のスレッドダンプが見れるようになりました。

スレッドダンプツールとしては、他に pystack を試してみましたが、空のファイルが生成されるだけでスレッドダンプを出力することができませんでした。(理由は追いかけてません)

最初は Linux の signal を trap してスレッドダンプを出力しようとしたのですが、uWSGI が trap しているらしく、handler が呼ばれませんでした。

また、uWSGI thread dump trigger という gist にアップされたコードも試してみました。これは uWSGI が独自で持つ Signal Framework を使っています。実際に試してみたところ、uWSGI の signal の handler を実行するスレッドが HTTP Request を処理するスレッドと同じだった為、HTTP Requet が滞留している原因を調べる用途には使えないと思い、このアプローチは採用しませんでした。

これで HTTP リクエストが詰まっても原因究明ができそうです。