📝

Laravel Octane: SwooleとFrankenPHPどっちがいい?

2024/05/31に公開

軽く負荷試験ツールで検証してみていますが、実際はDBと接続したアプリであったり、複雑なロジックが実行されるようなアプリケーションで実施する方が好ましいです。
HealthCheckEndpoint(一応DBへの接続はしている)のみで計測してどの程度差が出ているかの確認のみとなります。

また、M1Macの為VM上で起動するlimaを利用しているため、さらにボトルネック部分が多くなっております。
このような環境下でもどの程度変化するか、確認していきたいと思います。

負荷試験ツール

https://qiita.com/joe_hirata/items/8e331740b29306171a1a

Macということで、

Siege

という物を利用してどのくらい速度に差が出るのかを確認してみようと思います。

$ brew install siege

Laravelのアプリについて

https://github.com/exaco/laravel-octane-dockerfile/tree/main

こちらのリポジトリをベースにOctane & OpenSwoole のDockerと、 FrankenPHP.Alpine で軽く比較してみようと思います。

検証比較にはならないかもしれないですが docker-compose でコンテナを建てて検証します。

version: '2'
services:
  app:
    build: .
    environment:
      DB_CONNECTION: pgsql
      DB_HOST: db
      DB_PORT: 5432
      DB_DATABASE: db
      DB_USERNAME: user
      DB_PASSWORD: pass
    depends_on:
      - db
    ports:
      - 9000
  db:
    image: postgres:16.3-bullseye
    environment:
      POSTGRES_PASSWORD: pass
      POSTGRES_USER: user
      POSTGRES_DB: db

Swoole

DockerFileをそのまま持ってきています。バージョンの差異があるかもしれないので内容を載せておきます。

# Accepted values: 8.3 - 8.2
ARG PHP_VERSION=8.3

ARG COMPOSER_VERSION=latest

###########################################
# Build frontend assets with NPM
###########################################

ARG NODE_VERSION=20-alpine

FROM node:${NODE_VERSION} AS build

ENV ROOT=/var/www/html

WORKDIR ${ROOT}

RUN npm config set update-notifier false && npm set progress=false

COPY package*.json ./

RUN if [ -f $ROOT/package-lock.json ]; \
  then \
    npm ci --loglevel=error --no-audit; \
  else \
    npm install --loglevel=error --no-audit; \
  fi

COPY . .

RUN npm run build

###########################################

FROM composer:${COMPOSER_VERSION} AS vendor

FROM php:${PHP_VERSION}-cli-bookworm AS base

LABEL maintainer="yusuke.aoki <yusuke.aoki@optim.co.jp>"
LABEL org.opencontainers.image.title="Laravel Octane Dockerfile - Hardware My Portal"
LABEL org.opencontainers.image.description="Production-ready Dockerfile for Laravel Octane"
LABEL org.opencontainers.image.source=https://github.com/exaco/laravel-octane-dockerfile
LABEL org.opencontainers.image.licenses=MIT

ARG WWWUSER=1000
ARG WWWGROUP=1000
ARG TZ=UTC

ENV DEBIAN_FRONTEND=noninteractive \
  TERM=xterm-color \
  WITH_HORIZON=false \
  WITH_SCHEDULER=false \
  OCTANE_SERVER=swoole \
  USER=octane \
  ROOT=/var/www/html \
  COMPOSER_FUND=0 \
  COMPOSER_MAX_PARALLEL_HTTP=24

WORKDIR ${ROOT}

SHELL ["/bin/bash", "-eou", "pipefail", "-c"]

RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \
  && echo ${TZ} > /etc/timezone

ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

RUN apt-get update; \
  apt-get upgrade -yqq; \
  apt-get install -yqq --no-install-recommends --show-progress \
  apt-utils \
  curl \
  wget \
  git \
  nano \
  ncdu \
  ca-certificates \
  supervisor \
  libsodium-dev \
  # Install PHP extensions
  && install-php-extensions \
  bz2 \
  pcntl \
  mbstring \
  bcmath \
  sockets \
  pgsql \
  pdo_pgsql \
  opcache \
  exif \
  pdo_mysql \
  zip \
  intl \
  gd \
  redis \
  rdkafka \
  memcached \
  igbinary \
  ldap \
  swoole \
  && apt-get -y autoremove \
  && apt-get clean \
  && docker-php-source delete \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && rm /var/log/lastlog /var/log/faillog

RUN wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.29/supercronic-linux-amd64" \
  -O /usr/bin/supercronic \
  && chmod +x /usr/bin/supercronic \
  && mkdir -p /etc/supercronic \
  && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel

RUN userdel --remove --force www-data \
  && groupadd --force -g ${WWWGROUP} ${USER} \
  && useradd -ms /bin/bash --no-log-init --no-user-group -g ${WWWGROUP} -u ${WWWUSER} ${USER}

RUN chown -R ${USER}:${USER} ${ROOT} /var/{log,run} \
  && chmod -R a+rw ${ROOT} /var/{log,run}

RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini

USER ${USER}

COPY --chown=${USER}:${USER} --from=vendor /usr/bin/composer /usr/bin/composer
COPY --chown=${USER}:${USER} composer.json composer.lock ./

RUN composer install \
  --no-dev \
  --no-interaction \
  --no-autoloader \
  --no-ansi \
  --no-scripts \
  --audit

COPY --chown=${USER}:${USER} . .
COPY --chown=${USER}:${USER} --from=build ${ROOT}/public public

RUN mkdir -p \
  storage/framework/{sessions,views,cache,testing} \
  storage/logs \
  bootstrap/cache && chmod -R a+rw storage

COPY --chown=${USER}:${USER} deployment/octane/Swoole/supervisord.swoole.conf /etc/supervisor/conf.d/
COPY --chown=${USER}:${USER} deployment/supervisord.*.conf /etc/supervisor/conf.d/
COPY --chown=${USER}:${USER} deployment/php.ini ${PHP_INI_DIR}/conf.d/99-octane.ini
COPY --chown=${USER}:${USER} deployment/start-container /usr/local/bin/start-container

RUN composer install \
  --classmap-authoritative \
  --no-interaction \
  --no-ansi \
  --no-dev \
  && composer clear-cache \
  && php artisan storage:link

RUN chmod +x /usr/local/bin/start-container

RUN cat deployment/utilities.sh >> ~/.bashrc

EXPOSE 9000

ENTRYPOINT ["start-container"]

HEALTHCHECK --start-period=5s --interval=2s --timeout=5s --retries=8 CMD php artisan octane:status || exit 1

起動

docker-compose up -d

結果はまとめて記事後半に記載いたします。

FrankenPHP

では次にFrankenPHPで試して見ましょう。

先ずはプロジェクトをFrankenPHPに対応させる必要があるため、以下のコマンドで対応します。

(Laravel OctaneはSwooleでインストール済みとします)

$php artisan octane:install --server=frankenphp

 FrankenPHP's Octane integration is in beta and should be used with caution in production. Do you wish to continue? (yes/no) [yes]:
 > yes

 Unable to locate FrankenPHP binary. Should Octane download the binary for your operating system? (yes/no) [yes]:
 > yes

 116094786/116094786 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

   WARN  Please adjust the `OCTANE_SERVER` environment variable.

   INFO  Octane installed successfully.

ローカルで使う場合にswooleを使おうとすると危険なので環境変数も変えておきましょう。

.env

OCTANE_SERVER=frankenphp

FrankenPHPではGo言語のような使い方ができ、StandaloneBuildが出来るようなのでドキュメントに従ってバイナリを生成してみます。

バイナリのビルド... 遅い..!!!

https://frankenphp.dev/docs/laravel/#laravel-apps-as-standalone-binaries

こちらのドキュメントを参考にビルドを行ってみましたが、、、

3280.3s !!!!

しかも、何か修正を一つでも行ったらもう一度この時間待たないといけないという状態に...

どこで遅い。。?

#15 23.76 + upx --best dist/frankenphp-linux-x86_64
#15 23.78                        Ultimate Packer for eXecutables
#15 23.78                           Copyright (C) 1996 - 2024

https://upx.github.io/

どうやら、バイナリを圧縮する部分で遅くなっているようですね。

https://github.com/dunglas/frankenphp/blob/main/build-static.sh#L240

該当箇所を見つけました。
CIで毎回1時間拘束されるのは辛いので、OFFにして再度実行します。

以下のようにDockerFileの一部を書き換えます。

https://frankenphp.dev/docs/embed/#creating-a-linux-binary

- RUN EMBED=dist/app/ ./build-static.sh
+ RUN NO_COMPRESS=true EMBED=dist/app/ ./build-static.sh

再度実行

#15 36.98 + cd ../..
#15 36.98 + '[' -d dist/app/ ]
#15 36.98 + truncate -s 0 app.tar
#15 36.99 + truncate -s 0 app_checksum.txt
#15 36.99 + type upx
#15 36.99 + '[' -z  ]
#15 36.99 + '[' -z true ]
#15 36.99 + dist/frankenphp-linux-x86_64 version
#15 37.71 FrankenPHP v1.1.5 PHP  Caddy v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=
#15 37.72 + '[' -n  ]
#15 37.72 + '[' -n  ]
#15 DONE 40.1s

無事にビルドが通りました。 40s なので許容範囲かなと思います。

以下のようなDockerFileとなりました。

FROM dunglas/frankenphp:static-builder as BUILD_STAGE

# Copy your app
WORKDIR /go/src/app/dist/app
COPY . .

# Remove the tests and other unneeded files to save space
# Alternatively, add these files to a .dockerignore file
RUN rm -Rf tests/

# Copy .env file
RUN cp .env.example .env
# Change APP_ENV and APP_DEBUG to be production ready
# RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env

# Make other changes to your .env file if needed

# Install the dependencies
RUN composer install --ignore-platform-reqs --no-dev -a

# Build the static binary
WORKDIR /go/src/app/
RUN NO_COMPRESS=true EMBED=dist/app/ ./build-static.sh

FROM alpine:latest

COPY --from=BUILD_STAGE /go/src/app/dist/frankenphp-linux-x86_64 frankenphp
COPY ./deployment/frankenphp_static/start-container /bin/start-container

RUN chmod +x /bin/start-container

ENTRYPOINT [ "start-container" ]

start-containerではartisanコマンド系を叩いています。

#!/usr/bin/env sh
set -e

./frankenphp php-cli artisan storage:link; \
./frankenphp php-cli artisan optimize:clear; \
./frankenphp php-cli artisan event:cache; \
./frankenphp php-cli artisan config:cache; \
./frankenphp php-cli artisan route:cache;

if [ "$1" != "" ]; then
    exec "$@"
else
    exec ./frankenphp php-cli artisan octane:frankenphp --port=9000 --admin-port=2019 --max-requests=10000
fi

artisan octane:frankenphp --port=9000 --admin-port=2019 等のコマンドについては以下のドキュメントを参照

https://frankenphp.dev/tr/docs/laravel/#laravel-octane

とりあえずalpineに詰めているので、これをサーバで実行し性能を見てみました。

性能試験

swoole

** SIEGE 4.1.6
** Preparing 5 concurrent users for battle.
The server is now under siege...

Lifting the server siege...
Transactions:                    664 hits
Availability:                 100.00 %
Elapsed time:                   5.69 secs
Data transferred:               0.04 MB
Response time:                  0.04 secs
Transaction rate:             116.70 trans/sec
Throughput:                     0.01 MB/sec
Concurrency:                    4.96
Successful transactions:         664
Failed transactions:               0
Longest transaction:            0.16
Shortest transaction:           0.03

安定して動作しているように見えます。

FrankenPHP

** SIEGE 4.1.6
** Preparing 5 concurrent users for battle.
The server is now under siege...

Lifting the server siege...
Transactions:                    153 hits
Availability:                  93.87 %
Elapsed time:                   5.47 secs
Data transferred:               0.01 MB
Response time:                  0.05 secs
Transaction rate:              27.97 trans/sec
Throughput:                     0.00 MB/sec
Concurrency:                    1.36
Successful transactions:         153
Failed transactions:              10
Longest transaction:            0.10
Shortest transaction:           0.03

????
かなり遅い.. というか途中からアクセスが遮断されている様子。

サーバを見てみると、Podが落ちていました。

MaxRequestに到達しているわけでも無く、ログは急に止まっている...
k8sを利用しているので、limitに引っかかってコケているのかもしれませんが、 そもそもswooleと同じlimitを設定しているのでこの時点で安定していないのであればFrankenPHPは少し辛そうという印象..

スペックをあげて挑戦して見るも同じ結果になりました。 別の要因がありそうです。

まとめ

ちゃんとした検証までやってないですが、FrankenPHPの採用は一旦諦めようかと思っています。
時間が少し空けば、もう少し踏み込んだ調査を実施して原因究明まで持っていきたいところですがSwooleで十分早く安定している状態のため
今のところは見送ろうと思います。

appendix: DockerImageサイズについて

  • FrankenPHP: 70MB 程度
  • Swoole: 270MB 程度

(これが安定したらFrankenPHPへの移行も視野に入れていきたい...)

GitHubで編集を提案

Discussion