読者です 読者をやめる 読者になる 読者になる

Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

パフォーマンスを意識して正規表現を書く

正規表現を書く際、どのようなパターンにマッチさせるか、どこをキャプチャするかという視点で記述することはあっても、パフォーマンスを考えて記述するというのはある程度知っている人でなければ忘れがちな視点です。

このエントリでは、バックトラックをメインに正規表現がパフォーマンスに及ぼす挙動について見ていきます。

対象の正規表現エンジン

ここでは、従来型 NFA を対象としています。具体的には、PHP の preg_ 関数で利用している PCRE や mb_ereg 関数が利用している鬼車です。Perl や Ruby、Python、Java、.NET でも従来型 NFA を採用しているので、似た挙動となるでしょう。

「従来型 NFA」や「バックトラック」などの用語については、「詳説 正規表現 第3版」のものを用いています。

バックトラックによるマッチ探査

正規表現エンジンでは、指定された文字列が、パターンにマッチするかどうかを判別する際、記述された正規表現で取り得るマッチパターンが見つかるように何度もマッチングを行います。

例えば、\d+\d という正規表現があり、これに 123 という文字列がマッチするか preg_match 関数でチェックします。

<?php
var_dump(preg_match('/\d+\d/', '123'));

これを実行すると、\d+ の部分は、文字列 123 にマッチします *1。しかし、次の \d にマッチする文字列が無いので、一つ前のパターン(\d+)に戻ります。次は、\d+12 にマッチさせます。次の \d は、3 にマッチするので、これで全体のマッチが成功します。

下記では、この流れを示しています。()\d+ に、{}\d がマッチした箇所で、[]が現在マッチを試みている文字になります。

* (123)[]  <--- `\d+`はマッチするが、`\d`がマッチしないので、`\d+`へ戻る
* (12){3}  <--- `\d+`も`\d`もマッチする

正規表現は、前から順に適用されていくのですが、後続のパターンがマッチしない場合に一つ前のパターンに戻って、別のマッチ方法を試行するのをバックトラック(backtracking)と呼びます。バックトラックは、正規表現に限らず、正しい解を探るためのアルゴリズムです。詳細は、下記で。

https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%83%E3%82%AF%E3%83%88%E3%83%A9%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0

「マッチしない」を確定させるのは遠い道程

次は、マッチが失敗するパターンとして、正規表現 \d+\d[^\d]123 という文字列にマッチさせます。

はじめに \d+ は文字列 123 にマッチします。次の \d にマッチしない(文字列が無い)ので、バックトラックが発生します。次は、\d+は文字列 12 に、\d は文字列 3 にマッチします。最後の [^\d] にマッチしない(文字列が無い)ので、バックトラックが発生します。今度は、\d+ を 文字列 1 に、\d を 文字列 2 にマッチさせますが、[^\d] と 文字列 3 はマッチしません。

次は、文字列の先頭を 1 文字進めて 23 に対してのマッチを試みますが、こちらもマッチには成功しません。さらに、文字列 3 に対するマッチも失敗し、最終的には全てのパターンで失敗します。

下記では、この流れを示しています。()\d+ に、{}\d がマッチした箇所で、[]が現在マッチを試みている文字になります。

* (123)[]    <--- `\d+`はマッチするが、`\d`がマッチしないので、バックトラック
* (12){3}[]  <--- `\d+`と`\d`がマッチするが、`[^\d]`がマッチしないので、バックトラック
* (1){2}[3]  <--- `\d+`と`\d`がマッチするが、`[^\d]`がマッチしないので、バックトラック
* 1(23)[]    <--- 1文字進めてマッチング開始。`\d+`はマッチするが、`\d`がマッチしないので、バックトラック
* 1(2){3}[]  <--- `\d+`と`\d`がマッチするが、`[^\d]`がマッチしないので、バックトラック
* 12(3)[]    <--- 1文字進めてマッチング開始。`\d+`はマッチするが、`\d`がマッチしないので、バックトラック
* 123[]      <--- 1文字進めてマッチング開始。`\d+`がマッチせずに終了

このように、「マッチしない」と結論付けるために、指定した正規表現で取り得る全てのマッチングを行います。この挙動が場合によっては、パフォーマンスに大きな影響を及ぼします。

爆発するマッチング

マッチさせるまで(マッチしないことを確定させるため)、あらゆるマッチングを行うがゆえに正規表現や文字列によっては、マッチングの組み合わせパターンが膨大になる可能性があります。

マッチングが膨大になる様を確認するために、regex101.com というサイトを利用します。このサイトでは、正規表現と文字列を指定すると、正規表現エンジンがどのようにマッチングを行っていくかを表示してくれます。

regex101.com

例えば、上記の例(\d+\d[^\d])であれば、下のような表示となります。これを見ると、文字列 123 に対して、14 steps がかかっていることが分かります。

f:id:shin1x1:20160817160205p:plain

ここで文字列を 123456789 という 9 文字に増やすと、91 steps となります。

マッチングが膨大になる場合を見るために、(\d*\d+)+\d[^\d] という正規表現を利用します。これは、前述の \d+\d[^\d] とマッチする文字列は同じです。このパターンを前述の 9 文字にマッチさせると、なんと 33,805 steps になりました!

https://regex101.com/r/eB0dM5/1

このようにわずか 9 文字の文字列に対するマッチングでも正規表現の書き方によって、マッチングの組み合わせが膨大に膨らむことが分かります。より複雑な正規表現や与えられる文字列が長い場合は、パフォーマンスへの影響が出てきます。

ReDosという攻撃

こうした正規表現エンジンの特性を利用した攻撃が ReDos です。ReDos については、下記の大垣さんのエントリが参考になります。

ReDoSの回避 | yohgaki's blog

特に量指定子(*?+{n,m})を入れ子にしたり、同じ文字クラスを重ねたりすると組み合わせパターンが多くなるので、こうした問題が起こる可能性があります。

末尾にあるスペースにマッチ

上記の ReDos とは別のパターンですが、単純な正規表現でも DoS を引き起こす場合があります。

stackoverflow.com では、文字列前後のスペースを除去する ^[\s\u200c]+|[\s\u200c]+$ という正規表現に対して 20,000 文字のスペースが含まれた文字列が送信されたために、上記のマッチングの組み合わせが膨大になり、34 分間アクセス不能になるという事態がありました。

Stack Exchange Network Status — Outage Postmortem - July 20, 2016

StackExchangeが攻撃されたReDoSの効果 | yohgaki's blog

大垣さんのサイトでも実証されていますが、Stack Overflow の事例を参考に文字列末尾にあるスペースを正規表現でマッチさせて取り除くという処理を見てみます。

まずは、正規表現を単純化して、\s+$ を利用し、文字列は a(半角スペース 3 文字)にマッチさせる場合で動きを見ます。

この場合、下記のようにマッチングを行なわれます。半角スペースが見えるように _ としています。人間が見れば、文字列の末尾が a なので、どのようにマッチングさせても成功しないのは明白なのですが、正規表現エンジンでは、前から順に愚直に評価していきます。

* (___)[a] <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* (__)[_]a <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* (_)[_]_a <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* _(__)[a] <--- 1文字進めてマッチング。`\s+`はマッチするが、`$`がマッチしないのでバックトラック
* _(_)[_]a <--- `\s+`はマッチするが、`$`がマッチしないのでバックトラック
* __(_)a <--- 1文字進めてマッチング。`\s+`はマッチするが、`$`がマッチしないのでバックトラック
* ___[a] <--- 1文字進めてマッチング。`\s+`がマッチしないのでバックトラック
* ___a[] <--- 1文字進めてマッチング。`\s+`がマッチせずに終了

スペースが 3 文字ではなく、20,000 文字となれば、それだけ多くのバックトラックが発生するため、処理に時間がかかるようになります。

パフォーマンスを計測するために下記のコードを実行してみました。それぞれのスペースの最後には a が付いているのでマッチングは失敗します。

<?php
function benchmark($title, callable $target) {
    echo '# ' . $title, PHP_EOL;
    $start = microtime(true);
    $target();
    echo microtime(true) - $start, PHP_EOL;
    echo PHP_EOL;
}

$strings = [];
foreach ([20, 200, 2000, 20000, 200000] as $no) {
    $string = str_repeat(' ', $no) . 'a';
    benchmark('preg_replace' . $no, function () use ($string) {
        return preg_replace('/\s+$/', '', $string);
    });
}

このコードを PHP 5.6 と 7.0 で実行した結果が以下です。2,000文字あたりから処理速度が低下していき、20,000文字、200,000文字で急激に処理時間がかかっています。なお、5.6 より 7.0 の方が文字数が多い場合は処理が速いので、何かしらの最適化が行われているのかもしれません。

  • PHP 5.6.24
文字長 実行時間
20文字 0.05ms
200文字 0.35ms
2,000文字 7.2ms
20,000文字 2,438ms
200,000文字 233,513ms
  • PHP 7.0.9
文字長 実行時間
20文字 0.4ms
200文字 0.2ms
2,000文字 5.8ms
20,000文字 498ms
200,000文字 50,352ms

正規表現を改良

文字列末尾のスペースにマッチングさせる正規表現を改善するために \s++$ に変えてみます。元の最大量指定子(\s+)を絶対最大量指定子(\s++)に変更しただけです。

これでベンチマークを取ると下記のようになりました。両バージョンとも処理速度が改善しており、200,000 文字で見ると、PHP 5.6 で 18 倍、PHP 7.0 で 4 倍と大きな改善となっています。変更後は、PHP 5.6 と PHP 7.0 でほぼ互角になっているのも興味深い点です。

このように正規表現の書き方一つで、大きくパフォーマンスが変わる場合があります。

  • PHP 5.6.24
文字長 実行時間
20文字 0.17ms
200文字 0.041ms
2,000文字 1.91ms
20,000文字 172ms
200,000文字 12,879ms
  • PHP 7.0.9
文字長 実行時間
20文字 0.26ms
200文字 0.047ms
2,000文字 1.29ms
20,000文字 123ms
200,000文字 12,544ms

絶対最大量指定子を使った場合( aへのマッチング)は、下記のようなマッチングを行います。絶対最大量指定子は、貪欲なマッチングとなり、かつマッチした文字列をアトミックに扱います。これにより、バックトラックの発生が抑えられるのでマッチング回数を抑制できます。これは、アトミックグループ((?>))でも同様の効果が得られます。

* (___)[a] <--- `\s++`はマッチするが、`$`がマッチしないのでバックトラック
* _(__)[a] <--- 1文字進めてマッチング。`\s++`はマッチするが、`$`がマッチしないのでバックトラック
* __(_)a   <--- 1文字進めてマッチング。`\s++`はマッチするが、`$`がマッチしないのでバックトラック
* ___[a]   <--- 1文字進めてマッチング。`\s++`がマッチしないのでバックトラック
* ___a[]   <--- 1文字進めてマッチング。`\s++`がマッチせずに終了

今回計測したPHPコードは、下記です。

regex_trim.php · GitHub

あえて正規表現を避ける

PHP でこの処理を単純に考えれば、ltrim関数で簡単に実装できます。

ltrim関数を使うと、200,000文字のスペース + a の場合でも、わずか 1ms 以下でした。

stackoverflow.com では、正規表現を使うことをやめ、文字列処理(substring関数)に取り替えたようです。

さいごに

正規表現の書き方や文字列が、パフォーマンスに影響を与えることを見てきました。

マッチングパターンのみに注力して記述した正規表現が、正規表現エンジンによって、上手い具合にチューニングされて動作すれば良いのですが、最適化が上手く働かないケースでは、その挙動を把握した対処を行わないとパフォーマンスの問題を抱えることになります。

このあたりは、SQL にも似たものを感じしますね。

正規表現は、Web アプリケーションにおいてはバリデーションで利用されることが多く、外部の値をいきなり正規表現にかけてチェックするということもあります。バリデーションは成功しないが、DoS となるような値を投げられて、パフォーマンスが低下したり、サービスが停止させられる場合も考えられます。

正規表現のパターンを考慮する、文字列長をチェックしておく、場合によっては正規表現を使わない、など対処法は考えられるが、まずは正規表現がパフォーマンス上の問題になりうるということを認識することが第一歩になるでしょう。

参考

正規表現エンジンの挙動については、下記の 2 冊がとても参考になります。どちらかというと、「詳説 正規表現」は正規表現を利用する側、「正規表現技術入門」は正規表現エンジンを実装する側から正規表現を見た本となっています。どちらも内部の動きを分かりやすく解説しているので、本エントリのような内容をより知りたい方には特におすすめです。

どちらも Kindle 版が無いので、電子版を購入する場合は、オライリー社サイト、技術評論社サイトで購入します。

詳説 正規表現 第3版

詳説 正規表現 第3版

  • 作者: Jeffrey E.F. Friedl,株式会社ロングテール,長尾高弘
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/04/26
  • メディア: 大型本
  • 購入: 24人 クリック: 754回
  • この商品を含むブログ (85件) を見る

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

*1:これは、PCRE の量指定子が最大量指定子なためです。最小量指定子や絶対最大指定子を指定したり、正規表現エンジンタイプが異なればマッチの仕方は変わります。

より実践的なDDD本「.NETのエンタープライズアプリケーションアーキテクチャ第2版 」

.NETのエンタープライズアプリケーションアーキテクチャ 第2版 (マイクロソフト公式解説書)

DDD 関連の書籍といえば、Eric Evans の DDD 本( エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践) )や、Vaughn Vernon の IDDD 本( 実践ドメイン駆動設計 (Object Oriented SELECTION) )が有名です。

DDDで登場する実装パターンや周辺知識をより実践的に解説しているのが、「.NETのエンタープライズアプリケーションアーキテクチャ 第2版 (マイクロソフト公式解説書)」(naa4e)です。

.NET 以外にも大いに通じる内容

本書は、.NET の名を冠しており、サンプルコードは、C# で記述されています。また、利用するライブラリも .NET 周辺のものであったり、解説で登場する RDBMS も、Web 系でよく登場する MySQL や PostgreSQL よりも、SQL Server や Oracle がメインとなっています。

しかし、.NET 固有の話はそれだけです。.NET 固有の内容は、枝葉の部分であり、メインの内容は、プラットフォームやプログラミング言語に関わらずとても参考になるものでした。

日頃、.NET 界隈にいない人間としては、正直、マイクロソフト公式解説書としての体裁になっているがゆえに避けてしまう人がいるのが、勿体無いとさえ感じる本でした。

DDDをガラスケースから日々実践するプラクティスへ

DDD 本、IDDD 本と本書を比べると、より実践に寄せた内容となっており、DDD をより身近な普段の開発に適用するために解説されています。

実際の開発では、上手くパターンにあてはめられなかったり、妥協してしまう場面もあるのですが、そのあたりも考慮に入れられています(トランザクションスクリプトも場面によっては有用なパターンである、など)。本書の表現を借りると、ヒーローがいなくても、一般の開発者が DDD 取り組む方法を示した本とも言えます。

それぞれ執筆された時期が異なる(DDD 本(2003年)、IDDD 本(2013年)、本書(2014年))ので、理論から実践、さらに実践した上で現実的な落し込みといった具合に、より現実的な内容に推移しているのが興味深いです。

私は、DDD 本、IDDD 本、本書の順序で読んだのですが、本書が一番理解しやすいと感じました。*1

本書の内容

本書の目次は下記です。

現代の設計手法や開発手法、OOPの基礎(SOLID原則など)やテスト手法からはじまり、DDDで利用されるパターンの解説に入っていきます。

本書で興味深いのは、第6章のプレゼンテーション層がドメインアーキテクチャのすぐ後に来ている点と、CQRS と イベントソーシング(ES)に多くのページが割かれている点です。

■第1部 基礎
第1章 現代のアーキテクトとアーキテクチャ
第2章 成功のための設計
第3章 ソフトウェアの設計原則
第4章 高品質なソフトウェアの作成

■第2部 アーキテクチャの考案
第5章 ドメインアーキテクチャの発見
第6章 プレゼンテーション層
第7章 伝説のビジネス層

■第3部 サポートアーキテクチャ
第8章 ドメインモデルの紹介
第9章 ドメインモデルの実装
第10章 CQRS の紹介
第11章 CQRS の実装
第12章 イベントソーシングの紹介
第13章 イベントソーシングの実装

■第4部 インフラストラクチャ
第14章 永続化レイヤー

ユーザエクスペリエンス(UX)ファースト

ソフトウェアの世界では、バックエンドから取り組むのが慣例となっていました。私たちの多くは、プレゼンテーションを システムのそれほど高尚な部分だと見ておらず、ビジネス層をデータアクセス層が完成してから取り組めばよい、という程度に考えていました。しかし、システムの複雑さを問わず、プレゼンテーションとバックエンドが等しく必要であることに疑問の余地はありません。

Webアプリケーションでもサーバサイドをメインにやっていると、プレゼンテーションの重要性は認識はしていても、重点をサーバ側に置いてしまいがちでした。

しかし、プレゼンテーションはユーザが直接システムに触れるインタフェースであり、ここからシステムのユースケースやタスクを見ていくというのは当然のことです。

自分が設計する際は、プレゼンテーションももちろん考えますが、どちらかと言えば、サーバサイドの設計を主に考えていた節があるので、ハッとさせられました。

CQRS を身近なパターンに

CQRSは、ソフトウェアアーキテクチャにおけるコロンブスの卵です ── すなわち、自明ではない問題に対する驚くほど明確でそつの ないソリューションです。開発者とアーキテクトは、DDDとDomain Modelの階層化アーキテクチャを理解し、正当化し、機械化すること に何年も費やしてきました。特に複雑なビジネスドメインでは、クエリスタックとコマンドスタックの両方をドメインモデルで実現する ことがきわめて難しいという問題と格闘してきました。 CQRSにより、すべてが一夜にして変わってしまい、複雑なビジネスドメインがはるかに管理しやすくなりました。

IDDD 本では、やや特殊なパターンとして解説されていた CQRS が、多くの場合に適用できる身近なパターンとして解説されています。

このあたりは、私もドメインモデルからクエリを分離して、クエリについては、DTOを返すリポジトリを作って実装した経験から、わりと使い勝手が良いパターンではないかという印象を持っていました。

本文では、単なる CRUD パターンでも、CQRS にしてコマンドとクエリの関心事を分けるのは意味があると書かれており、CQRS に興味がある人には参考になる内容でしょう。

イベントソーシング(ES)と似たものは、30年以上前からある

先に挙げたアプリケーションはどれも数十年前から存在しているもので、COBOLやVisual Basic 6でかかれたものさえあります。つまり、イベントソーシングなんて仰々しい名前が付いていても、決してソフトウェアの新しい概念の到来を告げるものではないのです。

新たな名前が付いたものが出てくると、身構えてしまいますが、実は従来からあるものに名前が付いてパターン化したものというのも良くあります。

その最たるものが、GoF でお馴染みのデザインパターンでしょう。イベントソーシングもこうした以前からあるものをパターン化したものです。

現実世界での出来事では、発生した事象が消えてなくなることはなく*2、事象をイベントとして残していくというのは特にユーザからすると理解しやすいものです。

例えば、商品購入を記録する場合、購入情報のドメインモデルを保存し、ステータスが変わる度にそれを書き換えます。そして、イベントを補足情報としてログに記録するというのは良くあるパターンです。

イベントソーシングでは、これを逆転し、イベントを記録する方を主とします。そして、保存したドメインモデルは、あくまでもスナップショットとして扱います。イベントには、必要な情報が全て記録されているので、原理的にはそれらを順に適用していけば、どの時点のドメインモデルでも再現できるからです。

実際にイベントソーシングを実装するとなると、パフォーマンスの問題は無視できないのですが、ここについても実装のアイデアが解説されています。

さいごに

DDD 本を読んで、自分たちが取り組んでいるプロジェクトに DDD を実践してみたい人や、実際に取り組んでみたもののまだしっくり来ない人に手にとって見てもらいたい本です。

ユーモアを交えて、平易な表現で書かれており、理解しやすいので、これから DDD を学ぶ人や OOP の基礎を見直す人にも良さそうです。

実装で具体的なイメージを持った状態で、理論に立ち返るとより理解が深まるので、この本から DDD 本や IDDD 本に戻るのも良いですね。

なお、この本を読んだ後に、Amazon でリコメンドされた「C#実践開発手法 (マイクロソフト公式解説書)」も気になる内容だったので、こちらも読んでみたいと思います。

.NETのエンタープライズアプリケーションアーキテクチャ第2版 .NETを例にしたアプリケーション設計原則

.NETのエンタープライズアプリケーションアーキテクチャ第2版 .NETを例にしたアプリケーション設計原則

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

実践ドメイン駆動設計 (Object Oriented SELECTION)

実践ドメイン駆動設計 (Object Oriented SELECTION)

*1:もちろん、それは前 2 冊を読んで、多少なりとも実践したからこそというのはあります。

*2:購入商品をキャンセルしても、購入した事実は消えない

PHPにおけるhttpoxyの対応

HTTP リクエストに任意の値をセットすることで、Web アプリケーションからの HTTP 通信を傍受したり、中間者攻撃(Man-in-the-Middle)を可能にする脆弱性が見つかっています。

専用サイト

httpoxyという名前が付けられ、専用サイトが立ち上がっています。詳細は、このサイトが詳しいです。

httpoxy.org

攻撃内容

  • アプリケーションからHTTP通信を行う際に、環境変数HTTP_PROXYの値を、HTTPプロキシとして見るライブラリがある。
  • HTTPリクエストにProxyヘッダを付けられると、環境変数HTTP_PROXYにその値がセットされる。(これは、CGIの仕様)
  • つまり、任意のプロキシを外部から指定できてしまうので、通信内容の傍受や偽装ができてしまう。

対象となる PHP アプリケーション

  • HTTP リクエストを受けて動作する PHP アプリケーション
  • アプリケーションから、HTTP 通信を行うもの
  • 利用している HTTP 通信を行うパッケージなりライブラリが、環境変数 HTTP_PROXY の値を HTTP プロキシとして扱う場合

対応方法

1 or 2 のいずれかの方法で対応します。

1. httpdサーバ(アプリケーション以前で)Proxyヘッダを落とす。

  • nginx

www.nginx.com

  • Apache(httpd)

https://www.apache.org/security/asf-httpoxy-response.txt

冒頭の専用サイトでは、Varnish や HAProxy などの対応方法も記載されている。

2. 環境変数HTTP_PROXYをHTTPプロキシとして利用しないようにする。

  • Guzzle

Guzzle がこれに該当していたので、修正版がリリースされている。Guzzle に依存するパッケージでは、6.2.1以上を使う。

Guzzle の対応コミットは以下。SAPIを確認して、cli以外では、HTTP_PROXYを利用しないようになっている。

github.com

Drupal が、Guzzle を利用しており、composer.json の依存バージョンを ~6.2 に変更している。Guzzle を利用したアプリケーションならこの対応が参考になる。

github.com

  • curl

curl は、10年以上前からこの問題を認識しており、大文字のHTTP_PROXYは見ないようにしていた。

Linux では、環境変数は case sensitive なので、HTTP_PROXYhttp_proxy は別ものとなる。

番外. PHP で、HTTP_PROXYの値を落とす対応はオススメしない

下記のようなPHPコードでの対応は、効果が無い *1 ので、上記 2 つでの対応が望ましい。

$_SERVER['HTTP_PROXY'] = ''; // 環境変数には効果無し
putenv('HTTP_PROXY=');       // ヘッダから来た値には無効
  • Using unset($_SERVER['HTTP_PROXY']) does not affect the value returned from getenv(), so is not an effective mitigation
  • Using putenv('HTTP_PROXY=') does not work either (to be precise: it only works if that value is coming from an actual environment variable rather than a header – so, it cannot be used for mitigation) https://httpoxy.org

追記1(2016/07/20)

getenv() は、SAPI ごとにハンドラを登録することができ、定義されていれば、そちらが優先される仕組みになっています。 github.com

例えば、php-fpm であれば、php-fpm 実行中は、fcgi env から値を取得するようになっています。 github.com

追記2(2016/07/20)

PHP 本体には、すでに修正コミットが入っています。

github.com

次のリリース(下記を見る限りは、2016/07/21)に含まれています。

github.com

追記3(2016/07/20)

RHEL / CentOS では、すでに httpd パッケージに修正版が出ています。Proxyヘッダの値を、環境変数HTTP_PROXYの値としてスクリプトに渡さなくなります。

CVE-2016-5387 - Red Hat Customer Portal

CentOS alert CESA-2016:1421 (httpd) [LWN.net] CentOS alert CESA-2016:1421 (httpd) [LWN.net] CentOS alert CESA-2016:1422 (httpd) [LWN.net]

追記4(2016/07/21)

httpoxy 対応版の PHP 7.0.9、5.6.24、5.5.38 がリリースされました。

http://jp2.php.net/downloads.php#v7.0.9

http://jp2.php.net/downloads.php#v5.6.24

http://jp2.php.net/downloads.php#v5.5.38

参考

blog.ichikaway.com

HTTPoxy - CGI "HTTP_PROXY" variable name clash - Red Hat Customer Portal

*1:$_SERVER['HTTP_PROXY']の値をプロキシとするライブラリがあれば効果はあるが...

「ざっくり分かる WordPress サイトのチューニング」を WordCamp 2016 で発表してきました。

2016/07/09、10 に大阪大学 豊中キャンパスで開催された WordCamp Kansai 2016 にて、WordPress サイトのチューニングについて発表しました。

f:id:shin1x1:20160711095229j:plain:w500 https://twitter.com/digitalcube/status/751693759121731584

発表資料

今回は、PHP(WordPress)エンジニアでなくても何か役立ててもらえるようにチューニングの具体的な手法というよりは、実際に手を動かすエンジニアと意思疎通がスムーズになるように基本的な考え方をメインにお話しました。

「推測するな、計測せよ」という格言があるとおり、チューニングにおいても、まず大事なのは「計測する」ということです。計測せずにやみくもにチューニングを行っているケースを見聞きするので、ここからはじめる内容にしました。

後半は、キャッシュを中心に紹介したのですが、最後の Varnish Cache に興味を持った人が多かったのが印象的でした。エンジニアメインの会場なら、こちらをメインに構成しても良いですね。

以前、Varnish について書いたエントリがあるので、参考まで。 http://www.1x1.jp/blog/2013/12/varnish-cache.html

さいごに

昨年に続き、お声がけ頂き、2年連続の登壇となりました。

前回は、β2 の PHP 7 上で WordPress を動かす内容だったのですが、今では、PHP 7 を本番環境で利用しており、1 年でも状況の変化は大きいなと感じたりもしました。

www.1x1.jp

今回も発表の機会を頂き、ありがとうございました。

参考

今回の内容に関連して、参考になるリンクは下記です。

「SOFT SKILLS」を読む前に知っておくと良いこと

ソフトウェア開発者の間で、話題の書籍「SOFT SKILLS」を読みました。ひと通り読んで感じたことなど書いてみます。

SOFT SKILLS ソフトウェア開発者の人生マニュアル

同じ内容の文章を読んでも、それを誰が書いたかによって受ける印象は変わります。

まさに、それを実感した本でした。

ソフトウェア開発者が書いたビジネス書

この本は、「ソフトウェア開発者」が語る「ソフトウェア開発者の人生マニュアル」です。ざっくり言えば、「ソフトウェア開発者向けのビジネス書」ともいえるでしょう。

内容自体は、ビジネス書を数冊読んだことがある人であれば、それほど目新しいというわけではありません。実際、書籍内でも筆者が参考にしているビジネス書(ロバート・キヨサキの「金持ち父さん 貧乏父さん」やデール・カーネギーの「人を動かす」など)が紹介されており、こうした書籍から影響を受けた思考や行動がつづられています。

ビジネス書は、私も嫌いではないので、たまに読みます。やはり、読んだ後は元気が出るので、即効性の高い心のドーピングとして重宝します。

ただ、ビジネス書は、突然、大金持ちに声をかけられて、人生のアドバイスを受けたり、象が成功へのアドバイスをくれたりと現実離れしている面もあり、内容は面白く参考にできるところはあっても、身近な話として飲み込みづらい面もあります。

その点、本書は、「ソフトウェア開発者」が書いているのが大きな特徴です。それぞれの内容は、他のビジネス書と似たものでも、開発者の目線で書かれているので、身近な話として受け止めやすくなっています。(アメリカと日本とで事情は異なるものの)同じソフトウェア開発者が、自分の体験を語りかけてくれているように感じました。

序文からして、ロバート・C・マーティン(アンクル・ボブ)とスコット・ハンセルマンですから、普通のビジネス書では無いことは明らかです。アンクル・ボブの序文は秀逸で、これだけでも一つの章になりますね。ここで、グッと引き込まれて、本書に対する信頼感が上がります。

75のブログエントリ

本書は、テーマごとに7つの部があり、それぞれの部が細かな章に分かれています。全体としては、450ページほどあるのですが、章が71個(+付録4章)あるので、1つの章は数ページ程度です。これだと、空き時間に1章だけ読もうと気軽に読み進めることができます。書籍でも書かれていますが、1章は1ブログエントリを読むくらいの感覚なので、このあたりもソフトウェアエンジニアにチューンされたビジネス書と言えます。

第1部 キャリアを築こう
第2部 自分を売り込め! 
第3部 学ぶことを学ぼう
第4部 生産性を高めよう
第5部 お金に強くなろう
第6部 やっぱり、体が大事
第7部 負けない心を鍛えよう

特に良いなと感じたのは、第2部と第4部です。

コードを書き、ブログを書き、人前で発表して、書籍も書くというのは、自分の活動と重なる部分があって興味深く読めました。

また、生産性の向上については、Rebuild FM #139 を聞いてから、また自分自身の熱が戻ってきており、ちょうどポモドーロ・テクニックをやっていたので、タイムリーでした。書籍で紹介されていた KanbanFlow は便利に使っていますし、最近はスタンドアップで仕事したりしてます。こうした話は気軽に試すことができるので、即効性がありますね。

このあたりまで順調に読み進めていて、会う人にも、この本いいよ!と勧めていました。

ソフトウェア開発者に向けたビジネス書

いつもどおり楽しみに読み進めていたのですが、第5部あたりから印象が変わっていきます。

ここは、お金に関する話ということで、経済的自由をいかに手に入れて、自分の望む生き方をしていくかというビジネス書定番のテーマに入ります。

「第55章 打ち明け話: 私が 33 歳で引退できた理由」がターニングポイントでした。

この章では、筆者がいかに現在の成功を収めるかが自伝として語られています。20代はじめに年15万ドルをソフトウェア開発者として稼ぎ、そこから不動産や株式投資で稼ぐ方法を模索します。そして、オンラインでプログラミング講座を公開していきます。さらに、時折、モデルや俳優もやっていたそうです*1。もちろん、良いことばかりではなく、色々な浮き沈みが語れられており、ハードワークをこなした結果、現在へと行き着いたというわけです。

これは誰もができることはなく、読んでいる自分も、ただただ、すごいなあ、という感想を持ちました。

でも、なぜか、私のテンションは下がっていきました。なんだか、身近な人と感じていたのが、急に遠くの人に感じるような。

同じ内容の文章を読んでも、筆者へのイメージによって、受ける印象は変わります。

このあたりから、よくあるビジネス書を読む感じで進んでいきました。このあたりからは、ソフトウェア開発者はあまり関係無く、一般的な話(お金、体、心のあり方)になるので余計にそうした印象を受けました。

後半におすすめ書籍として、「CODE COMPLETE 第2版 上 完全なプログラミングを目指して」や「Clean Code アジャイルソフトウェア達人の技」、「Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本 *2」が入っていたので、ああソフトウェア開発者なんだと再認識して安心したくらいです。

さいごに

訳者あとがきで、本書を翻訳された長尾さんが「うまいニッチ(第20章)を見つけたもの」と書かれていたのに深く納得しました。まさに、「ソフトウェア開発者に向けたビジネス書」というニッチを上手く狙った書籍というのが読み終えた印象でした。

読み手は文章の内容から勝手に書き手のイメージを作り、そのイメージで書かれた内容を受け止めます。本書では、その受ける印象の違い(変化)を感じることができました。

おそらく本書を手にとる人は、他の開発者から「いいよ!」と勧められて読み始める人が多いでしょう。そこで、このエントリの内容を少し頭に入れた上で読み進めると、また違う捉え方ができるかもしれません。

私もほとぼりが覚めた頃にまた開いてみて、その違いを楽しんでみたいと思います。

SOFT SKILLS ソフトウェア開発者の人生マニュアル

SOFT SKILLS ソフトウェア開発者の人生マニュアル

*1:書籍に写真がありますが、マッチョなイケメンです :)

*2:この本は、あまり紹介されないですが、おすすめです。

勉強会やカンファレンスにライブ配信は必要か?

ひと昔前(といっても数年前ですが)の勉強会では、Ustream を使ってセッションをライブ配信するということが一般的でした。私は、関西在住のため、特に東京で開催されているイベントをライブで視聴できるのはとても便利でありがたいものでした。

f:id:shin1x1:20160619225055j:plain

私自身がライブ配信の恩恵を受けているので、自分が開催するイベントでも可能であればライブ配信したいと考えていました*1

ある時、関西の PHP コミュニティで集まる機会があったので、ライブ配信について話していました。その場には、15人ほどいたと思うのですが、ライブ配信が欲しいと考えいたのは私一人だけで、あとは後日動画を公開するならライブ配信は無くて良い、あっても見ないという人ばかりでした。正直、ここまで必要とされていないというのは意外でした。

このエントリでは、ユーザコミュニティ主催の勉強会やカンファレンスにおけるライブ配信について考えてみたいと思います。

ライブ配信が欲しい理由

私がライブ配信があって嬉しいのは、下記のような理由です。

  • 遠隔地や都合で参加できない勉強会のセッションを見ることができる。
  • ライブで参加することで、その雰囲気を(一部でも)味わえる。
  • Twitter でハッシュタグで tweet することでよりライブ感を味わえる。
  • 場合によっては、Twitter でのやりとりが発生して、より雰囲気を感じられる。

実際のところ、「気持ち」の部分を除外すれば、後日の動画配信でも問題ありません。勉強会のセッションを今見ないと明日困るとかほぼ無いですし。ただ、この「参加している」感を味わいたいというのが大きいですね。

これは、スポーツ観戦に似ていると思っていて、プロ野球や MotoGP 観戦が好きな自分にはライブ配信(生中継)というのは大きな価値を感じます。一方、「スポーツニュースで結果だけ見ればいいやん」という人には価値を感じないのかもしれません。*2

運営としてのライブ配信

参加者としてのライブ配信は上記のとおり欲しいのですが、運営としてもライブ配信のメリットはあります。

  • 現地に参加していない人にもイベントをアピールできる
  • スポンサーやスピーカーの露出をより高められる。イベントへの参加メリットを増やす。
  • 以前のUstream であれば、ライブ配信と録画、アーカイブ配信が一気に出来た。*3
  • 参加者として享受しているライブ配信のお返し(これは個人的)

おそらく、ライブ配信自体を否定する場面は少ない*4と思うのですが、実際にやるとなると、考えないといけないことがあります。ぱっと思いつくだけで下記のようなものがあります。

  • 運営負荷が上がる(機材調達、設置、撮影、配信、撤収などなど)
  • "ライブ" なので、特に配信担当者の負荷が高い(心理的にも)
  • 配信回線の帯域確保
  • スピーカーへ配信可否の確認が必要
  • スピーカーは配信されることを前提に話すこと必要がある(後日公開なら後で公開、非公開を選べる)

これまでは、自分のように必要としている人がいると思っていましたが、冒頭の結果を受けて、あらためてその必要性を見直そうと考えました。

Twitterでの反応

ライブ配信が必要かどうか、Twitterで聞いてみました。結果は下記です。

「ライブ配信欲しい派」と「後日公開されれば不要派」が、おおよそ半々という結果でした。この結果を見ると、ライブ配信が欲しい人がまだまだいるのだなと感じました。

関西 PHP 勉強会

ちょうど、関西 PHP 勉強会があったので、その会場でも聞いてみました。ざっくり、15 人くらいの方がいたので、上記 tweet の設問を聞いてみると、ライブ配信が必要な人は、たったの 1 人でした*5。冒頭の数と合わせると、おおよそ 1:30 となり、ライブ配信はほぼ必要とされていない状況でした。

ライブ配信をやるべきか

ライブ配信をやるべきかどうか。運営側としては悩ましいところです。

正直なところ、これまで関わったイベントでは、いわゆる配信職人と呼ばれる人*6におんぶに抱っこの状況でした。アウトソースする手もありますが、それなりに費用もかかります。

運営側としてはメリットは感じつつも、その効果が薄く、負荷とのバランスが取れないのであれば、やらないと判断するのも自然なことです。

勉強会自体が増えて気軽に参加できるようなった(個々のイベントの希少価値は相対的に下がった?)、ブームが落ち着いてただ参加したいという人が減った、ライブ配信を閲覧していたがその価値を感じなくなったなど、ライブ配信が必要とされない理由は考えられますが、勉強会を取り巻く状況が変わってきているのは確かなので、これまで当たり前にやっていたことを見直す時期に来ているのかもしれません。

それにしても、今回驚いたのが、Twitter での反応と関西 PHP コミュニティでの反応に大きな乖離があった点です。後者では、自分以外ライブ配信に必要性を感じないという結果だったのですが、これは、「関西」だからなのか、「PHP」だからなのか、「関西 PHP」だからなのか。謎は残りますが、少なくとも「関西 PHP」でやるイベントについては頭に入れておかないといけないことですね。

ちなみに、関西 PHP 勉強会では、セッション中の tweet も少なめなので、そのあたりも関係しているのかもしれません。

さいごに

勉強会を Ust で配信して、オンラインで参加して、Twitter で絡む。一時は当たり前のように繰り広げられていた風景です。それがよくある勉強会だと考えていましたが、そうではなくなってきたのは少し寂しい気もします。

見方を変えると、勉強会がより身近になって「ライブ配信」よりも「ライブ」を重視する人が増えたとも言えます。

人によって色々な意見があると思うので、ぜひあなたの意見も教えて下さい。

Twitter で頂いた意見

Twitter でアンケートを行った際に教えてもらった意見です。どれも一理あって、なるほどと思いますね。

*1:とは言っても、実際は大きなイベントでないと実施できてないのが実情ですが

*2:ライブ配信不要派の人がそういう考えかは分かりませんけど;-p

*3:今もできますが、無料プランでは、アーカイブは一定期間経過すると削除されます

*4:クローズドなイベントやスピーカーが配信を望まない場合は別

*5:私です

*6:@suzuki さんとか、関西だと @spice_o さんとか

PHP + PostgreSQL カーソルを使ったデータ取得の計測

php

PHP(pdo_pgsql)から PostgreSQL に SELECT 文でデータを取得する際に、カーソルを使った場合と使わなかった場合で、どのようにリソース消費(主にメモリと実行時間)の違いがあるのかを計測してみました。

検証内容

PostgreSQL テーブルに、1,000,000 件のレコードを持つテーブルを用意し、下記のようなパターンにて、PHP から全レコードを取得して、実行時間と消費メモリを計測します。カーソル利用時は、1度のフェッチで取得する件数によって計測しています。ここでは、前方向のカーソルのみ扱っています。

  • 1) カーソル未使用 + 1 件づつフェッチ
  • 2) カーソル利用 + 1 件づつフェッチ
  • 3) カーソル利用 + 100 件づつフェッチ
  • 4) カーソル利用 + 10,000 件づつフェッチ
  • 5) カーソル利用 + 100,000 件づつフェッチ
  • 6) カーソル利用 + 1,00,000 件づつフェッチ

参考として、カーソルを使用せずに全件フェッチ(fetchAll)するパターンも計測しています。

  • 7) カーソル未使用 + 全件フェッチ

検証は、VirtualBox 上の CentOS 7(Mem: 2.0GB) に PHP 7.0.6 + PostgreSQL 9.5 をインストールして行いました。

環境構築

下記の SQL で、テーブル構築とレコードの追加(1,000,000件)を行いました。generate_series関数は、こういうダミーデータ作る時に便利ですね。

CREATE TABLE users(
  id int primary key,
  number int,
  created_at timestamp
);

INSERT INTO users(id, number, created_at) SELECT i,(random() * 1000)::int % 1000, CURRENT_TIMESTAMP - CAST((random() * 1000)::int || ' day' AS interval) FROM generate_series(1, 1000000) s (i);

VACUUM;

レコードが格納されているかを確認しておきます。users テーブルには、1,000,000 件のレコードがあり、テーブルサイズは 42MB であることが分かります。

bench=# SELECT relname, to_char(reltuples, '9,999,999') as reltuples, (relpages * 8 / 1024) as kbytes FROM pg_class WHERE relname='users';
 relname | reltuples  | MB
---------+------------+--------
 users   |  1,000,000 |    42
(1 行)

bench=# SELECT COUNT(*) FROM users;
  count
----------
 1000000
(1 行)

PDO で全件を取得

PDO を使って、レコードを取得します。全体の実行コードは下記です。

https://gist.github.com/shin1x1/27db7768218ab3b4557a3dce4f639faa#file-pdo_cursor-php

Benchmark クラスは、実行時間とメモリ利用量(RSS)を出力するクラスです。ここでは、メモリ消費量を RSS 値と memory_get_usage関数で取得しています。

PHP のベンチマークでは、memory_get_usage 関数が使われますが、これは、PHP(Zend Engine) で割り当てられたメモリのみが計測対象なので、今回のように拡張(pdo_pgsql)や拡張が利用しているライブラリ(libpq)が、独自に確保したメモリは加算されません。そこで、比較のために両方を取得しています。

UserRepository クラスが、PDO を使って、PostgreSQL にアクセスするクラスです。下記のポイントで Benchmark クラスでログを出力しています。

  • SELECT 文(DECLARE CURSOR 文)実行前
  • SELECT 文(DECLARE CURSOR文)実行後
  • フェッチ中(100,000件毎)

実行結果

上記のコードを実行すると、下記のような出力が得られました。

https://gist.github.com/shin1x1/27db7768218ab3b4557a3dce4f639faa#file-results

実行結果をそれぞれまとめてみました。

メモリ消費量

メモリ消費量についてです。

まず、memory_get_usage 関数については、全てのケースでほとんど違いがありませんでした。これは、1-6 の計測では、libpq と PostgreSQL 間でのやりとりにバリエーションはありますが、PHP と libpq の間は違いが無いため(この間では、1 件づつフェッチしている)ためです。以下は、RSS 値についてです。

カーソル未使用では、SQL 文発行時に結果セットを libpq が、一気に PostgreSQL から受けとるので、利用メモリが一気に上がっています。フェッチ中は、libpq から取得するだけなので変化がありません。一方、カーソルでは、SQL 発行時(DECLARE CURSOR文)は、消費量に変化がありません。あとは、フェッチするレコード数に応じてメモリが消費されています。特に、6 は、全件をフェッチしているので、1 と同程度に消費しています。

方式 SQL 実行前 SQL 実行後 フェッチ完了
1) カーソル未使用 13,088 kb 108,708 kb 108,824 kb
2) カーソル + 1 件フェッチ 13,500 kb 13,500 kb 13,512 kb
3) カーソル + 100 件フェッチ 13,512 kb 13,512 kb 13,512 kb
4) カーソル + 10,000 件フェッチ 13,512 kb 13,512 kb 14,356 kb
5) カーソル + 100,000 件フェッチ 14,356 kb 14,356 kb 22,944 kb
6) カーソル + 1,000,000 件フェッチ 22,944 kb 22,944 kb 108,848 kb

上記表をグラフにしてみました。1 と 6 では、タイミングは異なりますが、全レコード分のメモリを消費しています。一方、2、3については、ほぼ変化はありません。4 は 800k、5 は 8m 増加している程度です。

f:id:shin1x1:20160614182705p:plain

実行時間

実行時間についてです。

カーソル未使用では、SQL実行完了(結果セット受け取るまで)に時間がかかっていますが、フェッチ完了までは速いです。カーソルを使うと、SQL 実行は速いですが、あとはフェッチする件数に依存しており、取得するレコード数が少なければ、その分、フェッチ回数が増えるので遅くなっています。

方式 SQL 実行前 SQL 実行後 フェッチ完了
1) カーソル未使用 - 857 ms 1,236 ms
2) カーソル + 1 件フェッチ - 152 ms 17,680 ms
3) カーソル + 100 件フェッチ - 148 ms 1,426 ms
4) カーソル + 10,000 件フェッチ - 148 ms 1,260 ms
5) カーソル + 100,000 件フェッチ - 152 ms 1,344 ms
6) カーソル + 1,000,000 件フェッチ - 153 ms 1,338 ms

上記表をグラフにしてみました。全体の実行時間を見ると、2 だけ実行時間が大きくかかっています。フェッチがレコード数分実行されるので、当然ながら時間がかかります。ここでは、PHP と PostgreSQL が同ホストでしたが、別ホストであれば通信のオーバヘッドもあるので、より遅くなります。それ以外については、大きな違いはありませんでした。

f:id:shin1x1:20160614182714p:plain

fetchAllの場合

PHP と libpq 間のやりとりについて見るために、カーソルなしで fetchAll した場合も計測してみました。結果を見ると、fetchAll 完了後に一気に RSS 値も memory_get_usage 関数の値も上昇しています。特に、memory_get_usage の上昇が顕著です。これは、libpq が持つ結果セットを PHP の値(1,000,000件の配列)として格納したためです。

方式 SQL 実行前 SQL 実行後 fetchAll完了後 ループ完了
RSS 13,516 kb 108,852 kb 563,512 kb 563,832 kb
memory_get_usage 364 kb 455,016 kb 455,016 kb 455,016 kb
実行時間 0 ms 839 ms 1,179 ms 1,405 ms

[おまけ] PDO::ATTR_CURSOR = PDO::CURSOR_SCROLL

pdo_pgsql のコードを見てみると、上記オプションを指定すると、自動でカーソルが使われるようになります。

https://github.com/php/php-src/blob/PHP-7.0.6/ext/pdo_pgsql/pgsql_statement.c#L153 https://github.com/php/php-src/blob/PHP-7.0.6/ext/pdo_pgsql/pgsql_statement.c#L423

おお、これは便利!と思って、試してみたのですが、1 件づつのフェッチなので、件数が多いと重くなるのと、自分で FETCH 文を発行する場合とことなり、メモリ消費量が異常に上がるので、おそらくメモリリークしているように見えます。

このオプションは、カーソルでポインタを自由に移動するためのものなので、前方向のみに利用するには適さないかもしれません。

さいごに

それぞれの方式による実行時間とメモリ消費量を見てみました。

カーソルを利用することで、PHP プロセスのメモリ消費を抑えてフェッチすることができます。しかし、ただカーソルを使えば良いかと言うとそうではなく、カーソルの特性を理解して、フェッチするレコード数を調整する必要があります。ここでは、実行時間とメモリ消費量のバランスで考えると、4 が妥当でしょう。もちろん、どのパターンが良いかはケースバイケースです。

カーソルを利用しない場合は、全件の結果セットを取得しますが、libpq が保持している状態です。ここで、fetch / fetchAll メソッドを呼ぶことで、PHP の値として利用できるようになります。

fetchAll の例で分かるように、PHP の値に格納するのはメモリ消費が大きいので、大量のデータを読み込む場合は、全レコードを一気に配列に格納するのではなく、fetch メソッドで逐次取得して処理するのが良いですね。

バッチ処理などで大量のデータを扱う際は、カーソルを上手く使ってみると良いでしょう。

参考