Flask の起動時に Background で Thread を動かし、そこで値を更新して、HTTP Request でその値を返すようなコードを書いていました。

そのコードは、ローカルで flask run で実行すると期待通りに動くものの、uWSGI に deploy すると Background Thread で更新しているはずの値が取得できない、という問題が発生しました。そこで、調査用のコードを書いて uWSGI と flask run で実行し、どのような値が見えるか確認してみました。

Test Code

以下のコードを使って調べてみました。Flask でスレッド等の情報を出力する際に print を使うと HTTP の Response が返ってこなくなるので、JSON で返すようにしています。

 1from time import sleep
 2import json
 3import os
 4import threading
 5import traceback
 6import sys
 7
 8from flask import Flask, jsonify
 9
10i = 0
11app = Flask(__name__)
12
13def process():
14    while True:
15        try:
16            global i
17            i += 1
18            print("--------------- thread dump on background thread --------------")
19            for th, s in get_thread_dump():
20                print(f"pid: {os.getpid()}")
21                print(f"i = {i}")
22                print(f"{th} id={id(th)}")
23                print("".join(s.format()))
24                print()
25
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    list = []
39    for th, frs in get_thread_dump():
40        l = [str(f) for f in frs]
41        list.append({
42            'pid': str(os.getpid()),
43            'counter': str(i),
44            'thread': str(th),
45            'thread-id': str(id(th)),
46            'frames': json.dumps(l)})
47
48    return jsonify({'message': list})
49
50
51def get_thread_dump():
52    ret = []
53    for th in threading.enumerate():
54        frames = sys._current_frames()
55        if frames.get(th.ident):
56            s = traceback.extract_stack(frames[th.ident])
57        else:
58            s = f"th: {th.ident} is not found in current frames"
59
60        ret.append((th, s))
61
62    return ret
63
64
65if __name__ == "__main__":
66    app.run(debug=True)
[uwsgi]
master = true
http = 127.0.0.1:5000
wsgi-file = /root/works/app.py
callable = app
enable-threads = true
workers = 2
stats = 127.0.0.1:9191
pidfile=/root/works/app.pid
 1import urllib.request
 2import json
 3
 4with urllib.request.urlopen('http://localhost:5000/hello') as r:
 5    j = r.read().decode('utf-8')
 6    ps = json.loads(j)
 7    print("--------------- thread dump on worker ---------------")
 8    for p in ps.get('message'):
 9        print(f"pid: {p.get('pid')}")
10        print(f"i = {p.get('counter')}")
11        print(f"{p.get('thread')} id={p.get('thread-id')}")
12        for f in json.loads(p.get('frames')):
13            print(f)
14
15        print()

uWSGI

uWSGI prefork で動かした時に、Background Thread の中で thread dump を取得すると、以下のようになります。

--------------- thread dump on background thread --------------
pid: 1121
i = 4
<_MainThread(MainThread, started 140685885491072)> id=140685882011536
th: 140685885491072 is not found in current frames

pid: 1121
i = 4
<Thread(test-thread, started 140685867824896)> id=140685872997968
  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 19, in process
    for th, s in get_thread_dump():
  File "/root/works/app.py", line 56, in get_thread_dump
    s = traceback.extract_stack(frames[th.ident])


--------------- thread dump on background thread --------------
pid: 1121
i = 5
<_MainThread(MainThread, started 140685885491072)> id=140685882011536
th: 140685885491072 is not found in current frames

pid: 1121
i = 5
<Thread(test-thread, started 140685867824896)> id=140685872997968
  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 19, in process
    for th, s in get_thread_dump():
  File "/root/works/app.py", line 56, in get_thread_dump
    s = traceback.extract_stack(frames[th.ident])

pid: 1121 は uWSGI の master プロセスです。

一方、uWSGI で動かした時、Flask の中で Thread dump を取得すると、以下のようになります。

--------------- thread dump on worker ---------------
pid: 1123
i = 1
<_MainThread(uWSGIWorker1Core0, started 140685885491072)> id=140685882011536
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 2463 in __call__>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 2446 in wsgi_app>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 1949 in full_dispatch_request>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 1935 in dispatch_request>
<FrameSummary file /root/works/app.py, line 39 in hello_world>
<FrameSummary file /root/works/app.py, line 56 in get_thread_dump>

pid: 1123
i = 1
<Thread(test-thread, started 140685867824896)> id=140685872997968
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 890 in _bootstrap>
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 926 in _bootstrap_inner>
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 870 in run>
<FrameSummary file /root/works/app.py, line 18 in process>

pid: 1123 は uWSGI の worker プロセスです。

uWSGI の worker から見ると i の値は 1 から変化することはありません。i の初期値は 0 ですが、Background Thread 側で1度インクリメントしてから fork した為です。fork 後、master 側でカウンタの値を書き換えた際に Copy On Write でそれぞれでカウンタの値を保持するようになっています。

flask run

ローカルでは flask run で動かしていたので、その時の Thread やカウンタの見え方も確認してみました。Background Thread の中で thread dump を取得すると、以下のようになります。

--------------- thread dump on background thread --------------
pid: 1181
i = 4
<_MainThread(MainThread, started 140653762594624)> id=140653757725200
  File "/usr/local/bin/flask", line 10, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.7/site-packages/flask/cli.py", line 966, in main
    cli.main(prog_name="python -m flask" if as_module else None)
  File "/usr/local/lib/python3.7/site-packages/flask/cli.py", line 586, in main
    return super(FlaskGroup, self).main(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/click/core.py", line 680, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.7/site-packages/click/core.py", line 1027, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.7/site-packages/click/core.py", line 873, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.7/site-packages/click/core.py", line 508, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/click/decorators.py", line 63, in new_func
    return ctx.invoke(f, obj, *args[1:], **kwargs)
  File "/usr/local/lib/python3.7/site-packages/click/core.py", line 508, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/flask/cli.py", line 860, in run_command
    extra_files=extra_files,
  File "/usr/local/lib/python3.7/site-packages/werkzeug/serving.py", line 1009, in run_simple
    inner()
  File "/usr/local/lib/python3.7/site-packages/werkzeug/serving.py", line 966, in inner
    srv.serve_forever()
  File "/usr/local/lib/python3.7/site-packages/werkzeug/serving.py", line 734, in serve_forever
    HTTPServer.serve_forever(self)
  File "/usr/local/lib/python3.7/socketserver.py", line 232, in serve_forever
    ready = selector.select(poll_interval)
  File "/usr/local/lib/python3.7/selectors.py", line 415, in select
    fd_event_list = self._selector.poll(timeout)


pid: 1181
i = 4
<Thread(test-thread, started 140653735167744)> id=140653758109136
  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 19, in process
    for th, s in get_thread_dump():
  File "/root/works/app.py", line 56, in get_thread_dump
    s = traceback.extract_stack(frames[th.ident])

flask run で動かした時、Flask の中で Thread dump を取得すると、以下のようになります。

--------------- thread dump on worker ---------------
pid: 1181
i = 4
<_MainThread(MainThread, started 140653762594624)> id=140653757725200
<FrameSummary file /usr/local/bin/flask, line 10 in <module>>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/cli.py, line 966 in main>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/cli.py, line 586 in main>
<FrameSummary file /usr/local/lib/python3.7/site-packages/click/core.py, line 680 in main>
<FrameSummary file /usr/local/lib/python3.7/site-packages/click/core.py, line 1027 in invoke>
<FrameSummary file /usr/local/lib/python3.7/site-packages/click/core.py, line 873 in invoke>
<FrameSummary file /usr/local/lib/python3.7/site-packages/click/core.py, line 508 in invoke>
<FrameSummary file /usr/local/lib/python3.7/site-packages/click/decorators.py, line 63 in new_func>
<FrameSummary file /usr/local/lib/python3.7/site-packages/click/core.py, line 508 in invoke>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/cli.py, line 860 in run_command>
<FrameSummary file /usr/local/lib/python3.7/site-packages/werkzeug/serving.py, line 1009 in run_simple>
<FrameSummary file /usr/local/lib/python3.7/site-packages/werkzeug/serving.py, line 966 in inner>
<FrameSummary file /usr/local/lib/python3.7/site-packages/werkzeug/serving.py, line 734 in serve_forever>
<FrameSummary file /usr/local/lib/python3.7/socketserver.py, line 232 in serve_forever>
<FrameSummary file /usr/local/lib/python3.7/selectors.py, line 415 in select>

pid: 1181
i = 4
<Thread(test-thread, started 140653735167744)> id=140653758109136
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 890 in _bootstrap>
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 926 in _bootstrap_inner>
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 870 in run>
<FrameSummary file /root/works/app.py, line 29 in process>

pid: 1181
i = 4
<Thread(Thread-1, started daemon 140653725640448)> id=140653725752720
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 890 in _bootstrap>
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 926 in _bootstrap_inner>
<FrameSummary file /usr/local/lib/python3.7/threading.py, line 870 in run>
<FrameSummary file /usr/local/lib/python3.7/socketserver.py, line 650 in process_request_thread>
<FrameSummary file /usr/local/lib/python3.7/socketserver.py, line 360 in finish_request>
<FrameSummary file /usr/local/lib/python3.7/socketserver.py, line 720 in __init__>
<FrameSummary file /usr/local/lib/python3.7/site-packages/werkzeug/serving.py, line 326 in handle>
<FrameSummary file /usr/local/lib/python3.7/http/server.py, line 426 in handle>
<FrameSummary file /usr/local/lib/python3.7/site-packages/werkzeug/serving.py, line 361 in handle_one_request>
<FrameSummary file /usr/local/lib/python3.7/site-packages/werkzeug/serving.py, line 303 in run_wsgi>
<FrameSummary file /usr/local/lib/python3.7/site-packages/werkzeug/serving.py, line 291 in execute>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/cli.py, line 337 in __call__>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 2463 in __call__>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 2446 in wsgi_app>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 1949 in full_dispatch_request>
<FrameSummary file /usr/local/lib/python3.7/site-packages/flask/app.py, line 1935 in dispatch_request>
<FrameSummary file /root/works/app.py, line 39 in hello_world>
<FrameSummary file /root/works/app.py, line 56 in get_thread_dump>

Flask の worker から見ると Background Thread でインクリメントした i の値が見えています。pid はどちらも 1181 になっています。

Conclusion

flask run は プロセスを Fork せず Thread だけでリクエストを処理しており、Background Thread で更新したカウンタを Flask から参照することができます。uWSGI は master プロセスを Fork して worker を作成しているので、master の Background Thread で更新したカウンタを worker の Flask から参照することができませんでした。

前記の調査用のコードはカウンタ(i)を使いましたが、実際はこの部分が別のライブラリで管理している変数だった為、すぐに原因を特定することはできませんでした。uWSGI に deploy したら思い通りに振る舞いにならなかった時、こういった箇所を疑ってみるのも一案だと思います。

調査用のコードを書いたりしてちょっと時間を使ってしまったので、自戒の念を込めてアップしました。