2026年 01月 31日

reload した後 h2oの古いプロセスが残り続ける場合

h2o は graceful restart をするのだが、稀に古いプロセスがずっと残ることがあって困ってた。(正確には start_server が restart させていて、h2o 自体は graceful shutdown しているだけ)

一例としてはこういう感じ (これは単に SIGHUP 直後なだけだけど、ときどき reload するとプロセスが残っていく)

 $  sudo systemctl status h2o
● h2o.service - H2O HTTP Server
     Loaded: loaded (/etc/systemd/system/h2o.service; enabled; preset: enabled)
     Active: active (running) since Sat 2026-01-10 17:36:22 JST; 2 weeks 4 days ago
    Process: 902234 ExecReload=/bin/kill -s HUP $MAINPID (code=exited, status=0/SUCCESS)
   Main PID: 1288 (perl)
      Tasks: 23 (limit: 2274)
     Memory: 53.0M (peak: 1015.0M swap: 12.2M swap peak: 95.1M)
        CPU: 1h 38min 58.048s
     CGroup: /system.slice/h2o.service
             ├─  1288 perl -x /usr/local/share/h2o/start_server --pid-file=/var/run/h2o.pid --port=0.0.0.0:80 "--port=[::]:80" --port=0.0.0.0:u443 "--port=[::]:u443" --port=0.0.0.0:443 "--port=[::]:443" -- /usr/local/bin/h2o -c /srv/www/h2o.conf.yaml
             ├─897714 /usr/local/bin/h2o -c /srv/www/h2o.conf.yaml
             ├─897716 neverbleed
             ├─897735 perl -x /usr/local/share/h2o/annotate-backtrace-symbols
             ├─902237 /usr/local/bin/h2o -c /srv/www/h2o.conf.yaml
             ├─902238 neverbleed
             └─902257 perl -x /usr/local/share/h2o/annotate-backtrace-symbols

結論: http*-graceful-shutdown-timeout を設定しよう

まず以下のような設定をし忘れないように。この例だと30秒で強制切断。デフォルトが0(無効)

http2-graceful-shutdown-timeout: 30
http3-graceful-shutdown-timeout: 30

メモ: タイムアウトの仕組み

HTTP/2 (lib/http2/connection):
  1. initiate_graceful_shutdown → 最初のGOAWAY送信
  2. 1000ms後 → graceful_shutdown_resend_goaway → 2回目のGOAWAY送信
  3. もし http2-graceful-shutdown-timeout > 0なら、その時間後に強制終了
HTTP/3 (lib/http3/server.c):
  1. 同様に1000ms後に2回目のGOAWAY
  2. もし http3-graceful-shutdown-timeout > 0なら、その時間後に強制終了
HTTP/1 (lib/http1.c):
  1. Keep-Alive をオフにしてレスポンス終了時 close

とにかくタイムアウトは全部設定しておこう!

調査

ただこの2つを設定していても接続が残るケースが発生して悩んだ

$  sudo ss -tapn | grep 902501                                                                                                                                                                                                              
ESTAB      0      0                              127.0.0.1:54080                                 127.0.0.1:5001  users:(("h2o",pid=902501,fd=66))                                                                                             
ESTAB      0      0                              127.0.0.1:46754                                 127.0.0.1:3000  users:(("h2o",pid=902501,fd=72))                                                                                             
CLOSE-WAIT 1      0                              127.0.0.1:52768                                 127.0.0.1:80    users:(("h2o",pid=902501,fd=74))                                                                                               

CLOSE-WAIT が残っている。

→ 最終的にこれは grafana の websocket への接続がずっと残っているというものだった。ブラウザの grafana タブを閉じたらプロセスが消えた。なんで CLOSE-WAIT?

websocket の proxy.timeout.io はデフォルト30秒らしいけど、通信が続くと一生切断されないのかも?

モールスにおける「SNR」や「受信限界」について曖昧なので Gemini にリサーチさせてみたけど、Gemini も困っていた。

SNR

SNR は帯域幅によって変化するので、帯域幅も同時に明示する必要があるが、書いてないことが多い。アマチュア無線の文脈だと 2500Hz (SSB帯域) 換算で述べられることが多いらしい。JT8 とかもそう。

機械学習だとサンプルレート基準で書いてあったりそもそも書いてなかったりなのでよくわからんが、とりあえず 2500Hz 表記に統一するのがよさそう。

dB/Hz 使うのが一番良いっぽいが、馴染みがない。(オフセットするだけだけど)

受信限界

CER (文字エラーレート) で何%を「受信限界」としているのかが人によって違う。

* 商業的定義(ITU-R F.339): 1%の文字誤り率(99%正解)を維持できる最小SNR。
* 限界性能定義: 50%程度の文字誤り率で、意味のある単語やコールサインの断片が抽出できる最小SNR。