Fly.io へのデプロイが急に 37 分になった原因は、ただのネットワークだった
ptyhard.co.jp のサイトは Relayer(usePHP)で作っていて、Fly.io に置いている。main に push すると GitHub Actions から flyctl deploy が走る。
このデプロイが 9/18 に急に遅くなった。前日までは 5〜10 分だったのが、この日は 6 回連続で 37〜41 分。
| 期間 | デプロイ時間 |
|---|---|
| 9/9〜9/17 | 5〜10 分 |
| 9/18 | 37〜41 分 |
FrankenPHP の単一バイナリにしていた
構成は FrankenPHP の static builder で、PHP もアプリも 1 本の実行ファイルに焼き込むやつ。
FROM dunglas/frankenphp:static-builder-gnu AS static
WORKDIR /go/src/app/dist/app
COPY --from=build /app ./ # アプリのコード
...
RUN ... EMBED=dist/app/ ./build-static.sh
アプリを COPY してから build-static.sh を叩くので、コードを 1 行変えるだけでレイヤーキャッシュが外れて、PHP のビルドが毎回最初からになる。遅いのはそのせいだと思っていた。
最初に出てきた案はこのあたり。
- static-php-cli の
downloads/やbuildroot/を--mount=type=cacheに載せる - Fly のリモートビルダーではなく GitHub Actions でビルドする
- 単一バイナリをやめる
Actions に移す案はすぐ消えた。リポジトリが private なので標準ランナーは 2 vCPU で、Fly のビルダーより速くなる理由がない。キャッシュが外れるのは Dockerfile の並びのせいなので、どこで走らせても同じ。
で、キャッシュマウントを試す前に、そもそも 37 分のうちどこが遅いのかをまだ見ていなかった。昨日まで 5〜10 分だったことにも、まだ気づいていなかった。
ログを時刻で切る
gh run view --log で Actions のログを落とすと、Fly のリモートビルダーの BuildKit の出力もそのまま入っている。工程ごとに時刻を拾うとこうなった。
| 工程 | 時間 |
|---|---|
spc download(ソースの取得) |
約 32 分 |
| PHP の configure と make | 約 3.5 分 |
| xcaddy(Go)のビルド | 約 45 秒 |
PHP のビルドは 3.5 分しかかかっていない。C ライブラリは全部 already built と出ていた。
[N] lib [brotli] already built
[N] lib [sqlite] already built
[N] lib [openssl] already built
...
[N] lib [curl] already built
static-builder-gnu のイメージには、ライブラリがビルド済みで入っている。毎回ビルドし直していると思っていたものは、ビルドされていなかった。 時間を食っていたのは、使いもしないソースを毎回ダウンロードする工程のほうだった。
キャッシュマウントを入れていたら、削れていたのは 3.5 分のほうだった。
遅いソースだけ時間が揃っている
spc download のログを並べると、ほとんどのソースは 1 秒以内に終わっている。遅いのは 8 つだけで、しかも時間がきれいに揃っている。
| ソース | 取得にかかった時間 |
|---|---|
| attr | 136 秒 |
| libacl | 136 秒 |
| libiconv | 270 秒 |
| gmp | 270 秒 |
| ncurses | 270 秒 |
| gettext | 271 秒 |
| libunistring | 270 秒 |
| libidn2 | 270 秒 |
| それ以外の 22 個 | ほぼ 1 秒以内 |
取得元を static-php-cli の config/source.json で見ると、この 8 つは全部 GNU のサーバーだった。
https://ftp.gnu.org/gnu/...https://download.savannah.nongnu.org/releases/...
URL を直接叩く attr と libacl が 135 秒で、ディレクトリ一覧を取ってから本体を落とすものが 270 秒。1 リクエストにつき 135 秒払っている。135 秒は Linux の TCP の接続タイムアウトとだいたい同じ長さ。
手元から同じ URL を叩くと 1 秒で返ってくる。サーバーが遅いのではなく、Fly のビルダーからつながっていない。
9/17 のデプロイのログも見てみると、同じ GNU のソースは 1 件数秒で取れていて、ダウンロード全体でも約 4 分だった。9/18 になって急に、Fly のビルダーから GNU のサーバーにつながらなくなったということになる。GNU 側なのか経路なのかは、こっちからは分からない。
IPv6 だと思った(違った)
GNU の 2 つのホストの IPv6 アドレスを見ると、どちらも 2001:470::、Hurricane Electric のアドレスだった。同じく IPv6 を持っている openldap や MIT からの取得は 1 秒ほどで終わっている。
HE と Cogent が IPv6 でピアリングしていないのは有名な話なので、「Fly のビルダーから HE の IPv6 に向かう経路だけが通っていなくて、135 秒待ってから IPv4 にフォールバックしている」と読んだ。
static-php-cli はダウンロードに素の curl を使っていて、~/.curlrc が読まれる。なので 1 行足した。
RUN echo ipv4 > /root/.curlrc
マージして、次のデプロイのログを見たらこうだった。
curl: (7) Failed connect to download.savannah.nongnu.org:443; Connection timed out
[W] Download failed: Download failed: Direct command run failed with code: 7
[N] Trying to download alternative sources for attr
IPv4 でも同じく 135 秒でタイムアウトしている。 Fly のビルダーからは、GNU のサーバーにそもそもつながらなかった。IPv4 に固定したことで「135 秒待ってつながる」が「135 秒待って失敗し、代替ミラーから取る」に変わっただけで、待ち時間は何も変わっていない。
なぜ前は 135 秒待ったあとにつながっていたのかは、分かっていない。
PHP をビルドするのをやめた
じゃあ curl の接続タイムアウトを 10 秒にすれば……と PR まで出したところで、自分の別のサイト(polidog.jp)のことを思い出した。あっちも Relayer で、同じく Fly.io に置いている。デプロイは 1〜2 分で終わる。
見比べたら、あっちは単一バイナリにしていなかった。
FROM dunglas/frankenphp:php8.5
ビルド済みの FrankenPHP のイメージにアプリを COPY しているだけ。PHP をソースからビルドしていないので、GNU のサーバーから何かを取ってくる工程がそもそも無い。
なのでこっちも同じ形にした。Dockerfile は 184 行から 82 行になった。
FROM dunglas/frankenphp:php8.5 AS build
RUN install-php-extensions zip pdo_sqlite
...
FROM build
COPY --from=css /app/public/styles.css ./public/styles.css
COPY docker/php.ini "$PHP_INI_DIR/conf.d/zz-app.ini"
...
CMD ["frankenphp", "php-server", "--listen", ":8000", "--root", "/app/public"]
手元でのビルドは 40 秒。
単一バイナリのために抱えていた回避策も一緒に消えた。
- 埋め込んだファイルが実行時に
/tmp/frankenphp_<hash>/へ展開されるせいで、usePHP のコンパイル済みキャッシュ(ソースの絶対パスがキー)が引けなくなる。起動時にコンパイルし直すスクリプトを entrypoint で走らせていた PHP_EXTENSIONSを明示しないとpdo_sqliteとopcacheが入らない。PHP_EXTENSION_LIBSは書くと既定値が置き換わってリンクが落ちる- static-php-cli が GitHub API でリリースを引くので、トークンを build secret で渡さないとレート制限で落ちる
逆に、ビルド済みのイメージで気を付けることが 2 つ増えた。
- ベースイメージは Caddy の保存先が
XDG_DATA_HOME=/dataになっていて、Fly の volume のマウント先と同じだった。放っておくと CMS の DB がある volume にcaddy/ができるので、/tmpに逃がした - ベースイメージは php.ini を読まないので、PHP の素の既定値(
display_errorsが有効)になる。警告がレスポンスに混ざるので切った
マージ後のデプロイは、Actions の deploy ジョブが 63 秒で終わった。37 分が 1 分になった。
結局ネットワークだった

振り返ると、37 分になった原因は単純で、Fly のビルダーから GNU のサーバーにつながらなくなった、それだけだった。ビルドの仕組みも Dockerfile も、前日から何も変わっていない。
最初は「PHP をソースからビルドしているから遅い」と思っていた。それも半分は当たっていて、昨日までの 5〜10 分はソースからビルドする構成の素の時間だった。ただ、37 分に跳ねた理由はそこではなかった。ログの時刻と、前日のデプロイ時間を最初に見ていれば、もっと早く「急に何か外のものが変わった」と分かっていたと思う。
IPv6 の仮説は外れた。でも、つながらない理由を当てにいくより、つながらない相手のところへ行かなくて済む形にするほうが早かった。外から 30 個のソースを集めないとビルドできない構成は、そのどれかの経路が詰まっただけで丸ごと遅くなる。ビルド済みのイメージにしたら、そういう外の事情に左右される場所がほとんど無くなって、ついでに 5〜10 分も 1 分になった。