Redashに環境変数での設定変更が反映されていることをgunicornで確認してみた

Redashのworker設定を変更する場合に環境変数や設定ファイルが利用できますが、変更反映を確認する手段に欠けていました。どこかでdump出来ないものかとソースコードを辿ってみたところ、workerの本体であるgunicornの最新版にて設定dumpが可能になっていました。実際にdumpして、環境変数の反映具合をみてみました。
2021.07.13

Redashで処理時間が超過するクエリを実行すると、初期設定ではタイムアウトの可能性があります。workerが同期処理で、且つ処理時間制限が30秒と短いためです。

長時間処理に耐えられるように非同期設定や処理時間延長を盛り込みたいところですが、本体に修正を入れると更新があった場合の取り込みが困難になります。そこで、workerの動作を変える手段として設定ファイルや環境変数の指定を用います。これらは正確にはRedashがworkerとして用いているgunicornの機能です。

ただ、どのようにすれば変更確認が行えるかが悩みものでした。Redashやgunicornのバージョン別にコードを辿っていった結果、その方法が分かったのでまとめておきます。

ローカルでの確認用セットアップ

手元で手軽に済ませたいため、ローカルで動作するようにインストールを行います。

brew install llvm openssl
export LDFLAGS="-L/usr/local/opt/openssl/lib"
export CPPFLAGS="-I/usr/local/opt/openssl/include"
export VENV_IN_PROJECT=1
git clone git@github.com:getredash/redash.git
cd redash
pipenv install --python 3.8

gunicornでの設定を出力する

ただし、記事執筆時のRedashで使われているgunicornは設定の確認出力に対応していません。そこでgunicornのバージョンを上げます。

pip install gunicorn==20.1.0

次に、gunicornの起動オプションに--print-configを追加して実行します。

% .venv/bin/gunicorn --print-config redash.wsgi:app --check-config
access_log_format                 = %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"
accesslog                         = None
backlog                           = 2048
bind                              = ['127.0.0.1:8000']
ca_certs                          = None
capture_output                    = False
cert_reqs                         = 0
certfile                          = None
chdir                             = /path/to/app
check_config                      = True
child_exit                        = <ChildExit.child_exit()>
ciphers                           = None
config                            = ./gunicorn.conf.py
daemon                            = False
default_proc_name                 = redash.wsgi:app
disable_redirect_access_to_syslog = False
do_handshake_on_connect           = False
dogstatsd_tags                    =
enable_stdio_inheritance          = False
errorlog                          = -
forwarded_allow_ips               = ['127.0.0.1']
graceful_timeout                  = 30
group                             = 20
initgroups                        = False
keepalive                         = 2
keyfile                           = None
limit_request_field_size          = 8190
limit_request_fields              = 100
limit_request_line                = 4094
logconfig                         = None
logconfig_dict                    = {}
logger_class                      = gunicorn.glogging.Logger
loglevel                          = info
max_requests                      = 0
max_requests_jitter               = 0
nworkers_changed                  = <NumWorkersChanged.nworkers_changed()>
on_exit                           = <OnExit.on_exit()>
on_reload                         = <OnReload.on_reload()>
on_starting                       = <OnStarting.on_starting()>
paste                             = None
pidfile                           = None
post_fork                         = <Postfork.post_fork()>
post_request                      = <PostRequest.post_request()>
post_worker_init                  = <PostWorkerInit.post_worker_init()>
pre_exec                          = <PreExec.pre_exec()>
pre_fork                          = <Prefork.pre_fork()>
pre_request                       = <PreRequest.pre_request()>
preload_app                       = False
print_config                      = True
proc_name                         = None
proxy_allow_ips                   = ['127.0.0.1']
proxy_protocol                    = False
pythonpath                        = None
raw_env                           = []
raw_paste_global_conf             = []
reload                            = False
reload_engine                     = auto
reload_extra_files                = []
reuse_port                        = False
secure_scheme_headers             = {'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}
sendfile                          = None
spew                              = False
ssl_version                       = 2
statsd_host                       = None
statsd_prefix                     =
strip_header_spaces               = False
suppress_ragged_eofs              = True
syslog                            = False
syslog_addr                       = unix:///var/run/syslog
syslog_facility                   = user
syslog_prefix                     = None
threads                           = 1
timeout                           = 30
tmp_upload_dir                    = None
umask                             = 0
user                              = 503
when_ready                        = <WhenReady.when_ready()>
worker_abort                      = <WorkerAbort.worker_abort()>
worker_class                      = sync
worker_connections                = 1000
worker_exit                       = <WorkerExit.worker_exit()>
worker_int                        = <WorkerInt.worker_int()>
worker_tmp_dir                    = None
workers                           = 1
wsgi_app                          = None

この時点では設定にはまだ変更を入れていません。結果として worker_class が初期設定の sync になっていることが読み取れます。 timeout も初期値の 30 秒です。

次に設定の上書きを試してみます。 GUNICORN_CMD_ARGS による指定です。

% GUNICORN_CMD_ARGS="--timeout 120 --worker-class gevent" .venv/bin/gunicorn --print-config redash.wsgi:app --check-config
access_log_format                 = %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"
accesslog                         = None
backlog                           = 2048
bind                              = ['127.0.0.1:8000']
ca_certs                          = None
capture_output                    = False
cert_reqs                         = 0
certfile                          = None
chdir                             = /path/to/app
check_config                      = True
child_exit                        = <ChildExit.child_exit()>
ciphers                           = None
config                            = ./gunicorn.conf.py
daemon                            = False
default_proc_name                 = redash.wsgi:app
disable_redirect_access_to_syslog = False
do_handshake_on_connect           = False
dogstatsd_tags                    =
enable_stdio_inheritance          = False
errorlog                          = -
forwarded_allow_ips               = ['127.0.0.1']
graceful_timeout                  = 30
group                             = 20
initgroups                        = False
keepalive                         = 2
keyfile                           = None
limit_request_field_size          = 8190
limit_request_fields              = 100
limit_request_line                = 4094
logconfig                         = None
logconfig_dict                    = {}
logger_class                      = gunicorn.glogging.Logger
loglevel                          = info
max_requests                      = 0
max_requests_jitter               = 0
nworkers_changed                  = <NumWorkersChanged.nworkers_changed()>
on_exit                           = <OnExit.on_exit()>
on_reload                         = <OnReload.on_reload()>
on_starting                       = <OnStarting.on_starting()>
paste                             = None
pidfile                           = None
post_fork                         = <Postfork.post_fork()>
post_request                      = <PostRequest.post_request()>
post_worker_init                  = <PostWorkerInit.post_worker_init()>
pre_exec                          = <PreExec.pre_exec()>
pre_fork                          = <Prefork.pre_fork()>
pre_request                       = <PreRequest.pre_request()>
preload_app                       = False
print_config                      = True
proc_name                         = None
proxy_allow_ips                   = ['127.0.0.1']
proxy_protocol                    = False
pythonpath                        = None
raw_env                           = []
raw_paste_global_conf             = []
reload                            = False
reload_engine                     = auto
reload_extra_files                = []
reuse_port                        = False
secure_scheme_headers             = {'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}
sendfile                          = None
spew                              = False
ssl_version                       = 2
statsd_host                       = None
statsd_prefix                     =
strip_header_spaces               = False
suppress_ragged_eofs              = True
syslog                            = False
syslog_addr                       = unix:///var/run/syslog
syslog_facility                   = user
syslog_prefix                     = None
threads                           = 1
timeout                           = 120
tmp_upload_dir                    = None
umask                             = 0
user                              = 503
when_ready                        = <WhenReady.when_ready()>
worker_abort                      = <WorkerAbort.worker_abort()>
worker_class                      = gevent
worker_connections                = 1000
worker_exit                       = <WorkerExit.worker_exit()>
worker_int                        = <WorkerInt.worker_int()>
worker_tmp_dir                    = None
workers                           = 1
wsgi_app                          = None

worker_classgevent に、timeout120 秒になっていることが確認できます。

あとがき

ドキュメントによると以下の順で下になるほど優先度は高くなります。

  1. 各種環境変数
  2. PasteScriptでの設定値
  3. -c gunicorn.conf.py のように指定できるconfigファイル内設定値
  4. キー GUNICORN_CMD_ARGS の環境変数に指定する設定値
  5. gunicorn動作時に渡す設定値

1番目と4番目がかぶっているように見えますが、1の範囲に入るのは以下のような一部の限定された環境変数です。この環境変数に指定がない場合に初めてgunicornの初期設定が適用されます。

  • SENDFILE
  • FORWARDED_ALLOW_IPS

--print-config は設定確認用の処理を実行して終わるため、運用時には外して実行します。バージョンが20.1.0以前の場合は引数を解釈出来ず、エラーで起動しません。

workerに指定したクラスが変更されているのか、ログ上からタイムアウトエラーを見て判断する以外は手段に乏しいところでした。環境変数指定での上書き指定が正確に行えているかの確認に掛かるコストが大きく下げられるので、確認手段に悩んでいる場合にはおすすめです。