35歳からの中二病エンジニア

社寺・鉄道・アニメを愛でるウェブ技術者の呟き

GDの画像圧縮率と処理速度

よくPHPのGDは重いと言われるが、重い中でも圧縮率の設定次第で大きくパフォーマンスが変わるという落とし穴にハマってしまったので、備忘録がてら綴っておきたい。

何が起こったのか

  • 商用サービスにおいて、アバターの合成・圧縮処理にGDの imagepng() 関数を使用していた。
  • 元々はデフォルトの圧縮率 6 だったのだが、ストレージの都合上アバターのサイズを抑えるため、圧縮率を最高の 9 に上げる事になった。
  • その後、アバター合成に使用しているサーバーの高負荷状態が頻発するようになったが、先の変更時には他にも各所変更が行われており、かつ時間も経過していたため、原因に気付けなかった。

どのように解決したか

  • 前置きすると、僕は高負荷状態が頻発するようになった後でサポートに入ったため、今回の問題は単にユーザー増加による負荷上昇だと認識していた。
  • 元々は画像合成専用のサーバーを新しく構築する方針だったが、GDの設定変更や他の画像処理エンジンを使う事で改善できないかと片手間で調べてみた。
  • すると、GDの圧縮率設定で大きくパフォーマンスが変わるという例が散見されたので、試してみると確かに大きく改善できた。
  • 詳細検証の上、商用サービス上に圧縮率 1 を適用すると、負荷は軽減された。

実際どの程度変わるのか

以下は、1.8MBのPNG形式画像に対し、imagepng() 関数を使用して1,000回連続で圧縮した結果である。

圧縮率 所要時間(s) 画像サイズ(byte)
0 28.009 1,846,883
1 31.094 285,950
2 31.165 283,031
3 32.331 279,795
4 38.613 268,360
5 40.412 267,109
6 44.709 266,506
7 49.050 266,224
8 83.864 265,515
9 198.975 264,067

これを見てわかる通り、圧縮率を 8 以上にすると一気に所要時間が増えていく。一方で圧縮後の画像サイズは 4 以上では大きく変わっていない。今回の問題で圧縮率を 6 から 9 にした事で高負荷状態が頻発するようになった事は、これで裏付けられた。

おわりに

GDの圧縮率設定は、8 以上にすると急激に所要時間が増える。上記のケースでは、所要時間と圧縮率のバランスを鑑みた場合、デフォルトの 6 付近で調整するのが妥当と言えるだろう。ただ、合成の手順や対象の画像によって結果が変わってくる可能性があるため、実際に使用するケースで上記のような検証を行った上で微調整するのが良いだろう。

ちなみに、先述の商用環境においては負荷は軽減されたものの、上記のケースのような劇的な効果は得られなかった。これは圧縮だけでなく、合成部分の負荷も合わさっているためと考えられる。合成部分の処理速度については今回は取り上げていないが、機会があれば検証してみたい。

JMS Serializerのデバッグ設定でハマる

JMS SerializerはPHP標準のシリアライザーよりも強力なため重宝している。ただ、開発時にデバッグ設定にしたにも関わらずキャッシュが作成されてしまう現象に遭遇したので、備忘録がてら記載しておく。

ダメな例

以下は、キャッシュが作成されてしまう例である。 一見すると問題無さそうであるが…。

$isDebug = true;
$serializer = JMS\Serializer\SerializerBuilder::create()
    ->setCacheDir('/path/to/cache')
    ->setDebug($isDebug)
    ->build();

正しい例

実は、 setCacheDir() でキャッシュディレクトリーを指定してしまうと、デバッグ設定に関わらずキャッシュが作成される。そのため、以下のようにデバッグフラグが有効の場合のみキャッシュディレクトリーを指定すると、開発環境と本番環境の切り分けがうまくいくようになる。

$isDebug = true;
$serializer = JMS\Serializer\SerializerBuilder::create()
    ->setDebug($isDebug);
if ($isDebug !== true) {
    $serializer->setCacheDir('/path/to/cache')
}
$serializer->build();

Laravelのルーティングをアノテーションで指定する

Laravelのルーティングは、設定ファイル(routes.php)に記述するのが標準のやり方である。だが、僕は個人的にルーティングに関しては「設定より規約」でやりたい質である。そこで、わざわざ設定ファイルを見に行く、或いは作るという事をやらずに済むような方法を探す事となった。

Laravel Collective

laravelcollective.com

理想としているのは、何もしなくてもコントローラー名とアクション名からデフォルトのルーティングを組み立ててくれるというものなのだが、それを実現できるような既存の仕組みは見当たらなかった。しかし、これを利用すればコントローラー上にアノテーションを記述する事でルーティングを指定できるというので、利用してみた。インストールするのは、Laravel Collectiveの中でもアノテーション記法を利用するためのパッケージである。

なお、Laravel CollectiveはLaravelコアから取り除かれたコンポーネントをメンテナンスしているプロジェクトである。利用する際にはその点を念頭に置いておきたい。

インストール

まずは以下のComposerパッケージをインストールする。

composer require "laravelcollective/annotations":"^5.3.0"

次に、サービスプロバイダークラスを追加する。 設定内容については後述するので、まずはそのまま貼り付けておこう。

<?php

namespace App\Providers;

use Collective\Annotations\AnnotationsServiceProvider as ServiceProvider;

class AnnotationsServiceProvider extends ServiceProvider {

    /**
     * イベントのアノテーションをスキャンするクラス
     *
     * @var array
     */
    protected $scanEvents = [];

    /**
     * ルーティングのアノテーションをスキャンするクラス
     *
     * @var array
     */
    protected $scanRoutes = [];

    /**
     * モデルのアノテーションをスキャンするクラス
     *
     * @var array
     */
    protected $scanModels = [];

    /**
     * local環境の場合に自動的にスキャンするか
     * 
     * ドキュメントではfalseだが、僕はtrueにしている。
     *
     * @var bool
     */
    protected $scanWhenLocal = true;

    /**
     * コントローラーのディレクトリー (Serend\Http\Controllers) から、
     * ルーティングのアノテーションを自動的にスキャンするか
     *
     * 注意:スキャン対象はControllers直下のみとなる。再帰的ではない。
     *
     * @var bool
     */
    protected $scanControllers = false;

    /**
     * 名前空間内の全てのクラスから、
     * イベント・ルーティング・モデルのアノテーションを自動的にスキャンするか
     * 
     * 注意:アプリケーションのサイズに応じて、スキャン時間が長くなる。
     *
     * @var bool
     */
    protected $scanEverything = false;

}

最後にお約束通り、サービスプロバイダーを有効にする。

    'providers' => [
        // ...
        App\Providers\AnnotationsServiceProvider::class
        // ...
    ];

サービスプロバイダーの設定とスキャンについて

サービスプロバイダーの設定で、「スキャン」という言葉が何箇所か出ている。これは何かというと、記述したアノテーションを読み込んで有効化する事である。ルーティングのアノテーションをスキャンする方法は幾つかあるが、先述の通り設定した場合は、次の通りとなる。

  • local環境の場合、常に自動的にスキャンされる。
  • local環境以外の場合、 php artisan route:scan コマンドを実行する。

他には protected $scanRoutes にスキャン対象のコントローラークラスを指定する事もできるが、先の設定にしておけばデプロイ時に毎回artisanコマンドを実行する事で全てのコントローラークラスをスキャンしてくれるので、そちらをお薦めする。

アノテーションの記述方法

@Resource

まずはAPI等を実装する際に役立つ @Resource について。

/**
 * @Resource('users')
 */
class UserController extends Controller {
}

これは、以下と同義である。

Route::resource('users', 'UserController');

アクションを絞りたい場合は、 @Resources('users',only={"index","show"}) のように記述すれば良い。

@Get

@Get はそのままの意味で、HTTPのGetリクエストである。

    /**
     * @Get("users/login")
     */
    public function login(Request $request)
    {
    }

これは、以下と同義である。

Route::get('users/login', [
    'as' => 'users.login', 'uses' => 'UserController@login'
]);

@Post, @Put, @Delete, etc...

Get以外のHTTPリクエストについても、同様の記法が利用できる。

おわりに

Laravel Collectiveのアノテーションパッケージを使えば、ルーティングをコントローラー上にアノテーションで記述できる。「設定より規約」的な考え方で、原則としてコントローラー名とアクション名から規則的なルーティングを行っている場合などは特に、扱うファイルが減る分だけ効率的に開発できるだろう。

なお、Laravel Collectiveにはこれ以外にも様々なパッケージがあるし、アノテーションパッケージの中にも紹介し切れていないものがある(実際、僕は @Middleware 等も利用している)。それらについては公式ドキュメントが充実しているので、適宜参照していただくと良いだろう。

laravelcollective.com

LaravelのORMとしてDoctrineを使う

新規サービスでLaravelを採用するにあたり、標準のORMであるEloquentを学習しようかとも考えたが、Doctrineに載せ替えればアノテーションでスラスラとスキーマ定義できるし、DDDとの親和性も高いので、うまく導入できる方法を調べてみた。

Laravel Doctrine

www.laraveldoctrine.org

出オチとなってしまうが、その名の通りのパッケージがあった。これをComposerでインストールする。migrationsとextensionsは別パッケージになっているが、どちらもDoctrineを本格活用する上で必須となるため、同時に導入しておきたい。

composer require "laravel-doctrine/orm:1.2.*"
composer require "laravel-doctrine/migrations:1.0.*"
composer require "laravel-doctrine/extensions:1.0.*"
composer require "gedmo/doctrine-extensions=^2.4"

なお、インストールや利用方法の詳細は公式ドキュメントが詳しいのでそちらを参照していただくとして、ここでは押さえておきたいポイントを掻い摘まんでいくこととする。

Artisanコマンドとの連携

これはSynfonyでDoctrineを扱う感覚そのままと言っても差し支えないほどによくできている。proxyクラスの生成からマイグレーションまで完璧に揃っているので、そのままで十分に実用可能だ。参考までに、ORM、Migrations、Extensionsの各パッケージを導入した場合に利用できるArtisanコマンドを以下に挙げる。

doctrine:clear:metadata:cache
doctrine:migrations:generate
doctrine:migrations:rollback
doctrine:migrations:refresh
doctrine:migrations:version
doctrine:clear:result:cache
doctrine:migrations:execute
doctrine:migrations:migrate
doctrine:migrations:latest
doctrine:migrations:status
doctrine:generate:entities
doctrine:ensure:production
doctrine:clear:query:cache
doctrine:migrations:reset
doctrine:generate:proxies
doctrine:convert:mapping
doctrine:migrations:diff
doctrine:schema:validate
doctrine:mapping:import
doctrine:config:convert
doctrine:schema:create
doctrine:schema:update
doctrine:dump:sqlite
doctrine:schema:drop
doctrine:info

Debugbarとの連携

LaravelのDebugbarを利用している場合、SQLのクエリーログは手放せないだろう。だが、当然ながらORMをDoctrineに置き換えれば、そのままでは使えなくなってしまう。

…と思っていたのだけれども、ありがたいことにLaravel Doctrineは標準でログの吐き出し先を指定できるようになっており、Debugbarとも、そして先の記事で触れたClockworkとも連携できるようになっている!

return [
    /*
    |--------------------------------------------------------------------------
    | Enable query logging with laravel file logging,
    | debugbar, clockwork or an own implementation.
    | Setting it to false, will disable logging
    |
    | Available:
    | - LaravelDoctrine\ORM\Loggers\LaravelDebugbarLogger
    | - LaravelDoctrine\ORM\Loggers\ClockworkLogger
    | - LaravelDoctrine\ORM\Loggers\FileLogger
    |--------------------------------------------------------------------------
    */
    'logger'                    => env('DOCTRINE_LOGGER', false),
];

このように.envで簡単に切り替えられるようになっているため、迷うことは無いだろう。

IDE補完

LaravelはIDE補完が優秀だが、その恩恵はDoctrineに切り替えても基本的に受けられる。find系のメソッドで取ってきたEntity等もバッチリである。ただ、今の所Repositoryに追加したメソッドについては補完の方法がわかっておらず、ここは大きな課題である。

Eloquentに対するデメリット

性能面はチューニングによっても変わってくるので触れないものとして、特に標準のEloquentと比べて劣ると感じた箇所は無かった(本格的にEloquentを触ったわけではないが)。ただORMとしての方向性は全く違うので、用途に応じて使い分ける必要はあるだろう。

おわりに

Laravel Doctrineを使えば、Synfony上で使うのと似たような感覚で便利にDoctrineを扱うことができる。現状だと日本語の情報は皆無に等しいが、公式ドキュメントが充実しているため導入には困らないだろう。Eloquentの代替となるORMの選択肢としては、十分検討に値するものと思われる。

Laravel DebugbarをAPI開発でも使う

Laravelのデバッグツールとしては、Laravel Debugbarがよく使われている。ただ、これは情報をHTML上に描画しているため、API開発でレスポンスがJSONとなる場合などは表示されなくなる。そこで、Chrome/Firefox拡張であるClockworkを導入することによって、レスポンスがHTML以外の場合でもDebugbar相当の情報を出力できるようにする。

実行環境

  • Google Chrome 53
  • Laravel 5.3

Laravel Debugbarの導入

まだDebugbarを導入していない場合は、barryvdh/laravel-debugbarを参考にインストールしてHTML上で表示できるようにしておく。

ブラウザー拡張のインストール

Chromeの場合は、Chromeウェブストアからインストールする。
Firefoxの場合は、Add-ons for Firefoxからインストールする(要Firebug)。

Composerパッケージのインストール

composer require "itsgoingd/clockwork": "~1.12"

アプリケーションのConfig設定

Service Providerを以下の通り追加する。

    'providers' => [
        Clockwork\Support\Laravel\ClockworkServiceProvider::class,
    ],

Middlewareの設定

バージョンによっては$middlewareの場合があるので適宜読み替える。

    protected $middlewareGroups = [
        'api' => [
            \Clockwork\Support\Laravel\ClockworkMiddleware::class,
        ],
    ];

DebugbarのConfig設定

最後にDebugbar側のConfigでClockworkを有効にする。
HTML上の描画を止めたい場合は、injectを false にしておく。

    'clockwork' => true,
    'inject' => false,

おわりに

以上が最低限の設定だが、これでブラウザーのデバッグツールに以下のようなLaravelのデバッグ情報を表示するタブが追加されるはずだ。

f:id:aikawame:20200712213229p:plain

なお、Clockworkの詳細についてはGitHubを参照されたい。

github.com