Hatena::ブログ(Diary)

hnwの日記 このページをアンテナに追加 RSSフィード

[プロフィール]
 

2017年12月4日(月) とあるPHP拡張のCI事情 このエントリーを含むブックマーク このエントリーのブックマークコメント

PHP Advent Calendar 2017の3日目です。公開が遅くなってしまいました、ごめんなさい。


筆者はphp-timecopというPHP拡張を5年間ほどメンテナンスしています。このPHP拡張はCで書かれているのですが、Travis CIやAppVeyorなど複数のクラウドCIサービスを組み合わせてテストを回しています。本稿では各サービスをどのように利用しているか、それぞれの使い分けや特徴などを紹介していきたいと思います。


背景

本題に入るまえに、なぜPHP拡張でCIが必要か、という話を紹介します。


C言語で書かれたプログラムポータビリティが高いような印象を持つ人が多いかもしれませんが、実際はむしろ逆で、ポータブルに書くのが難しい言語の1つです。Cではシステムコールライブラリ関数を直接呼び出すのが普通ですが、これらは環境によって挙動が存在したりしなかったりもしますし、挙動が異なることも珍しくありません。また、コンパイラごとの方言も存在します。


こうした事情から、C言語で書かれているOSSでは環境の差を埋めるためにAutoconf/Automake/Libtoolを利用するのが定番ですが、環境の差を検出するためのconfigureスクリプトバグで停止するようなことも起こりうるので、各環境でテストしておくに越したことはありません。


さらに面倒なことに、PHP拡張ではPHP本体のC APIを呼び出したり本体で定義されている構造体を操作したりすることがあるので、PHPの特定バージョンでだけ動かない、というようなバグがありえます。


要するに、PHP拡張をメンテナンスするためには、様々なOSコンパイラPHPバージョンの組み合わせでテストをし続ける必要があるのです。ひとことで言えば地獄のような状況 ポジティブな言いかたをすると、とてもCIしがいのある題材だと言えるでしょう。


CIサービスの特徴と実際の使い方

php-timecopで現在利用しているCIサービスは下記の4つです。



いずれのサービスも無料プランで利用しています。筆者自身がこのPHP拡張でお金を得ているわけではないのでお金を払ってまでCIする気にはならないですし、各社さんOSSへの応援の意味も込めて公開リポジトリCIを無料で提供していると思うので、ありがたく使わせてもらっています。


以下、個別のサービスについて紹介します。


Travis CI

言わずと知れた最大手クラウドCIサービスです。


  • LinuxUbuntu)とmacOSの2つのOS上でCIできる
  • PHP 5.4から7.2までの6バージョンがビルド済み、設定で簡単に切り替えられる(記事執筆時)
  • 無料プランでは最大5並列でジョブが走る

無料プランでもかなりの大盤振る舞いなので、OSSCIしたい場合は最初の選択肢になるでしょう。


また、PHP拡張のCIに関していうとPHPの全マイナーバージョンの最新版が提供されているのが素晴らしい点です。PHPではマイナーバージョンアップのタイミングで内部構造の大変更が入ることが珍しくない(5.3→5.4など)ので、全マイナーバージョンでテストするのは非常に重要です。それが設定を少し書くだけで簡単に対応できるのは本当に助かります。


実際、筆者はLinux環境上で7つのPHPバージョン*1のテストを走らせています。


Travis CIではビルド済みのPHPバイナリがZTS(Zend Thread Safe)有効になっているのが特徴的です。大抵の人はPHPNTS(Non Thread Safe)で利用しているはずなので、ある意味テスト環境として不適切と言えなくもありません。一方で、ZTSでだけビルドが通らないようなバグもありうるので、個人的にはテスト環境としてありがたいと感じます。


AppVeyor

Windows環境のCIが必要ならほぼ唯一の選択肢*2と言えるでしょう。



PHPWindows版はバージョンごとに要求するVisual C++のバージョンが異なるため、自分でCI環境を準備するのは比較的面倒です。その意味で、クラウドCIサービスが利用できるのは良いですね。


php-timecopでは現在PHP 5.4、PHP5.6、PHP 7.1の3バージョンでテストを行っています。


Wercker

最近Oracleに買収されたクラウドCIサービスです。「わーかー」と読むらしいですね。CI環境を整えた当時、Dockerコンテナの扱いが一番楽そうだったので利用することにしました。


php-timecopではCentOSと32bit Ubuntuの2環境でのテストを走らせるのに利用しています。


CentOSでのテストは、specファイルによるバイナリパッケージ作成の動作確認のために行っています。通常Linuxディストリビューションごとのテストを行う必要性はあまり無いと思いますが、パッケージングのテストや、バイナリパッケージのデプロイなどの必要性があればディストリビューションの数を増やす必要があるでしょう。


一方、32bit Ubuntuでのテストは拡張自体のテスト目的です。PHPは32bit環境と64bit環境とで整数のサイズが変わるなど言語レベルで影響があるので、32bit環境でのテストには十分意味があります。


CircleCI

クラウドCIサービスの中でも有名どころの1つです。最近大幅バージョンアップしてコンテナベースになったと聞きましたが、筆者はまだ古いバージョンを利用しています。


現状ではNTS環境でのテストの意味で走らせているだけで、あまり活用できていません。


実際にCI有用か?

有用かどうかで言えば有用だと感じているのですが、客観的な証拠が挙げられるかというと案外難しいんですよね。


たとえば、新機能を実装するときなど、大変更の際はCIのおかげで特定バージョンでのバグを未然に防げることは珍しくありません。実際にはOSコンパイラの差で怒られることはそれほど多くなく、PHPの特定バージョンで動かないバグが多いので、まずはTravis CIで複数PHPバージョンでのテストを走らせるのがオススメです。


また、しっかりテストできているとPull Requestを受け取ったときの安心感も大きいので、その意味でもオススメです。


PHP 5時代はZTS環境で必要な「おまじない」の付け忘れにCIで気づく、ということが頻繁にありましたが、PHP 7になっておまじないが減ったので、最近はあまりそんなことはありません。


変わったところでは、Windows上でテストが失敗すると思ったらWindowsPHPバグ(仕様?)だった、ということがありました。(参照:「PHPのsleep関数とusleep関数の挙動を調べてみた」)


まとめ

php-timecopのCIでは複数サービスを活用しているという話を紹介しました。また、テストの軸が複数あることも紹介しました。


  • OSLinuxWindows、…
  • OSビット数:32bit、64bit
  • PHPバージョン:最低でも5.6、7.0、7.1、7.2くらいはサポートしたい
  • PHP ZTS:有効、無効

何個もCIサービスを使うのは若干趣味的に思われるかもしれませんが、LinuxWindowsの両OSに対応するためにTravis CIとAppVeyorの2サービス併用、くらいまでは十分実用的だと思います。


PHP拡張を公開しているけどテストしてないという方はphp-timecopのリポジトリから設定ファイルをコピーすれば簡単に導入できますので、ぜひお試しください。

*1PHP 5.4から7.2まで6バージョン、およびmasterブランチ

*2:本来ならVSTSが本命だと思うんですが、イマイチ知名度が低い気がします…

2017年10月9日(月) PHPカンファレンス2017でphp-timecopをPECLに登録した話をしました このエントリーを含むブックマーク このエントリーのブックマークコメント

10月8日に開催されたPHPカンファレンス2017でLT発表をしました。以下が発表資料です。



PECLは登録までの敷居が高そうな印象があったので、以前は自作のPHP拡張を登録するなんて考えもしなかったのですが、やってみたら案外あっさり登録できた、という内容を紹介しました。詳細の手順については過去の記事「php-timecopをPECLに登録しました」に書きましたので、合わせてご確認ください。


また、PECLに登録すると自動的にRemi’s RPM repositoryに入ること、さらに運が良ければ(?)Fedoraリポジトリにも入ることを紹介しました。これはRemiさんがPHPコアコミッターであるだけでなく、RedHat社員でもあるという事情もあると思いますが、何にせよやってみないとわからなかったことかなと思います。


余談ですが、PECLに登録してからissueが増えたという話を紹介しましたが、実は難しいissueが増えて私自身が捌き切れていない、というオチがあったりします。特に「PHP 7.2.0RC3でPHPの内部実装を変えたらphp-timecop動かなくなったからよろしくな」というissueの難易度が高く現在進行形で悩んでいるのですが、7.2.0リリースまでに解決できるよう頑張ります。


他のセッションについての感想など

今年のPHPカンファレンスも刺激をたくさんもらった集まりでした。個人的には内山さんの「OPcacheの最適化器の今」が興味深い内容でした。私もPHP 5.5の頃に似た内容の発表(「Zend OPcacheの速さの秘密を探る」)をしたことがあるのですが、当時は未実装だったconstant propagationやdead code eliminationといった最適化が実装されているというのは知りませんでした。


発表後に「われわれの普段書くコードの性能改善につながるのか?」というような質問があり、内山さんは「大抵の場合WebアプリケーションボトルネックDBアクセスなどになるのではないか、その意味ではOPcacheの最適化が実コードに与える影響は少ないと思われる、という回答をされていました。


大抵の現場で起きている問題を解決する特効薬にはならない、という意味では非常に正しい回答なのですが、現実のコードに対して意味のある最適化ではない、という風に捉えてしまった人もいたような気がします。しかし、特にconstant propagationは身近なコードでも有用最適化です。たとえば下記のようなコードを書いたことがある人は多いのではないでしょうか。


<?php
$seconds_per_minute = 60;
$minutes_per_hour = 60;
$hours_per_day = 24;
$seconds_per_day = $seconds_per_minute * $minutes_per_hour * $hours_per_day

このようなコードは読みやすさの観点では意味がありますが、これまでのPHPでは性能面で僅かに不利になっていたわけです*1。これがきちんと最適化されるのであれば、安心して読みやすさ優先でコードを書けるわけですから、多くのPHPユーザーにとって嬉しい内容であるはずです。


それ以外の方々の発表も楽しかったですし、懇親会も2次会も非常に盛り上がった気がします。毎度おなじみの方も久々の方ともお会いでき、色々なお話が聞けて良かったです。php-timecop使ってます、という話も複数の方から聞けて非常に参考になりました。ありがとうございます。


最後になりますが、スタッフの皆様、今年もおつかれさまでした。これほどの規模になると運営は本当に大変だと思いますが、来年も期待しておりますのでよろしくお願いいたします。

*1:このコードを最適化するためには各行間へのジャンプ命令が存在しないことを知らないといけませんが、過去のPHPではそのような解析を行っていませんでした

2017年7月26日(水) PHPのmysqlndの圧縮プロトコルについてのメモ このエントリーを含むブックマーク このエントリーのブックマークコメント

PHP+PDO+MySQLの環境では、PHP-MySQL間の通信についてzlibを使った圧縮プロトコルを利用することができます。この機能は、DBサーバCPU利用率に十分余裕があり、かつPHP-MySQL間のネットワーク帯域が逼迫している状況で有用です。


MySQLの圧縮プロトコルとそのマニュアル


PHP+MySQLの環境で、圧縮プロトコルは下記のようなコードで利用できます。


<?php
    $options = [
        PDO::MYSQL_ATTR_COMPRESS => true
    ];
    $db = new PDO($dsn, $user, $pass, $options);

MySQLドライバとしてmysqlndを利用している場合*1PHP 5.3.11(2012年4月リリース)以降であれば圧縮プロトコルに対応しています。このことはPHPマニュアルにも下記の通り記載があります。


PDO::MYSQL_ATTR_COMPRESS (integer)


Enable network communication compression. This is also supported when compiled against mysqlnd as of PHP 5.3.11.


https://secure.php.net/manual/en/ref.pdo-mysql.php#pdo.constants.mysql-attr-compress


ただし、この記述は2015年9月頃に追加されたものです。それまでマニュアル上では「PDO_MYSQL+mysqlndは圧縮をサポートしていない」という記述しかありませんでした(2010年頃の記述で、当時は正しかった)。


2015年8月頃に筆者が業務で関わっていた環境で圧縮プロトコルを導入したのですが、マニュアルに明記されていない機能を商用環境で使うのは気が引けたので、マニュアルバグがあるよ!というバグレポをPHP本家に投げてみました。その甲斐あってか、しばらくしてマニュアルが修正されました。これで今後は安心して利用できるというわけです。


mysqlndの圧縮プロトコルの実装

PHP 7.1.7のソースコード上で、mysqlndの圧縮プロトコルの実装部分を探してみました。


すると、ext/mysqlnd/mysqlnd_net.cext/mysqlnd/mysqlnd_protocol_frame_codec.cに同じようなコードが見つかります。


MYSQLND_METHOD(mysqlnd_pfc, encode)(zend_uchar * compress_buffer, size_t * compress_buffer_len,
                                    const zend_uchar * const uncompressed_data, const size_t uncompressed_data_len)
{
(略)
    error = compress(compress_buffer, &tmp_complen, uncompressed_data, uncompressed_data_len);
(略)
}

このcompress()はzlibの提供する関数で、Deflateアルゴリズムを使って圧縮するものです。ちゃんと真面目な圧縮を使ってるんですね!(驚くところではない気もしますが)


コードを追っていくと、クエリクエリ結果だけでなく通信内容全体を圧縮していることもわかります。


また、compress()を使っているので圧縮レベルを与える方法はありません。常にデフォルト値(Z_DEFAULT_COMPRESSION、おそらく6)になります。


参考URL

*1PHP 5.4.0以降の環境ではデフォルトでmysqlndを利用しているはずです

2017年7月17日(月) PHPのsleep関数とusleep関数の挙動を調べてみた このエントリーを含むブックマーク このエントリーのブックマークコメント

筆者はPHPの現在時刻を上書きするPHP拡張モジュールphp-timecopを開発しているため、PHPの時間がらみのテストを世間一般の人より多く書いていると思います。テストケース中でusleep関数を多用しているのは世界中でも筆者くらいかもしれません。


ところで、先日php-timecopのテストをWindows上で動かしたところ、 usleep(100000) が99.8msくらいで帰ってきてテストに失敗するということがありました。


筆者はsleep関数やusleep関数は指定した時間と同じかそれより長い時間スリープすると考えていたのですが、本当にそのような性質があるのでしょうか?また、sleep関数やusleep関数はどの程度の誤差があるのでしょうか?


本稿ではこうしたsleepやusleepの挙動について深掘りしてみます。


sleep関数の挙動

まずはsleep関数の挙動から調べてみましょう。LinuxmacOSWindowsの各環境で sleep(1) して帰ってくるまでの時間を1000回測定したときの結果を下記に示します。

Linuxの場合

下図がLinux環境(Debian 8.7, Kernel 3.16.0, x86_64, PHP 5.6.30)での実験結果ヒストグラムです。横軸の単位はusです。


f:id:hnw:20170717161758p:image


1.0001秒あたりにピークがあるのがわかります。言い換えると約100usのズレです。


Windowsの場合

Windows環境(Windows 10, x86_64, PHP 7.1.6)での実験結果ヒストグラムです。


f:id:hnw:20170717192334p:image


ピークは1.001秒あたりになっています。言い換えると約1msのズレです。


macOSの場合

macOS環境(Mac OS X 10.11.6, x86_64, PHP 7.1.6)での実験結果ヒストグラムです。


f:id:hnw:20170717161805p:image


ピークは1.005秒あたりになっています。言い換えると約5msのズレです。


3環境とも、1秒より早く帰ってきたものは1件もありませんでした。


POSIXの仕様を確認する

ちなみに、PHPのsleep関数はどの環境でもOS/標準ライブラリのsleepを呼び出しています。POSIXのsleep()のmanpageには次のような記述があります。


The sleep() function shall cause the calling thread to be suspended from execution until either the number of realtime seconds specified by the argument seconds has elapsed or a signal is delivered to the calling thread and its action is to invoke a signal-catching function or to terminate the process. The suspension time may be longer than requested due to the scheduling of other activity by the system.


http://pubs.opengroup.org/onlinepubs/9699919799/functions/sleep.html


指定された時間が経過するまで実行を停止するよ、スケジューリングの都合で指定されたよりも長く停止することがあるよ、とのことですから、指定時間より早く帰ってくることは無さそうな文言に読めます。


まとめ

以上から、次のことが言えそうです。

  • PHPのsleep関数は全OS上で指定された秒数と同じかそれ以上停止する
    • 仕様上も実験結果からも、停止時間が指定より短くなることはない
  • 指定された秒数からのズレの傾向はOSや利用ハードウェアによって異なると思われる
    • 実験時はmacOSのズレが一番大きく、5ms程度のズレだった

usleep関数の挙動

次にusleepの傾向を調べてみます。sleep関数と同様に3環境でusleep(1000000)して帰ってくるまでの時間を測定しました。


Linux/macOSの場合

LinuxmacOSの場合はsleep(1)のときと大差ない結果になりました。下記のヒストグラムLinuxの結果です。


f:id:hnw:20170717191415p:image


以下はmacOSの結果です。


f:id:hnw:20170717191410p:image


どちらの環境もsleep関数のときと同様、1秒より早く帰ってくるものは1件もありませんでした。両OSとも内部的にPOSIX準拠のusleepを呼び出しているため、sleep関数のときと似た挙動なのは当然と言えそうです。


Windowsの場合

さて、Windowsの場合は意外とも言える結果になりました。


f:id:hnw:20170717191406p:image


コブが2つある分布になっており、最初のピークは1000000ns(=1秒)より僅かに前になっています。本稿の最初でWindows上のPHPではusleep関数が指定した時間より少し早く帰ってくることがあるという話を紹介しましたが、その通りの結果になっているわけです。


PHPWindows用のソースコードを見てみると、usleep関数の実現にはWindowsの「Waitable Timer Objects」が利用されています。また、これを利用すると停止時間が指定より短くなることがあるようです(たとえば下記の記事を参照)。



まとめ
  • PHPのsleep関数はどの環境でも指定された時間と同じかそれ以上停止する
    • 内部的に呼び出しているPOSIXのsleepの仕様
  • PHPのusleep関数Linux/macOS上では指定された時間と同じかそれ以上停止する
    • 内部的に呼び出しているPOSIXのusleepの仕様
  • Windows上のusleep関数は指定された時間より短い停止時間になることがある
    • Windows APIを使った独自実装をしているため
    • 測定結果からすると、最大1ms程度早まることがある(別の環境では7msほど早まる例も見られた)

usleepという関数名なのにWindows上の実装がPOSIXと挙動が異なっているのはバグとまでは言い切れませんが、念のためPHP本体にバグレポートを出しておこうと思います。


追試用の情報

今回の実験に使ったPHPスクリプトは下記です。カーネルコンパイルオプションやその他の条件次第で結果が変わるかと思います。


ts_asanots_asano 2017/08/15 07:48 昔にtimecopをテストに組み込んだことがあるので懐かしいです。
大変細かいことで恐縮ですが、最初に記載されている秒数は999.8msが正しいのでは…と思いましたのでコメントいたしましたー。

ts_asanots_asano 2017/08/15 07:49 桁を読み間違えました…
このままで正しそうです、失礼しました!

hnwhnw 2017/08/15 10:55 Windows上でusleep(100ms)したら少し早く帰ってきたのがきっかけで、sleepとusleepで1秒sleepする実験をした、というのが本稿の内容でした。確かに、桁が大きすぎて1桁の違いがわかりにくいですよね…。

2017年7月8日(土) php-timecopをPECLに登録しました このエントリーを含むブックマーク このエントリーのブックマークコメント

かれこれ5年ほどメンテしている拙作のPHP拡張「php-timecop」ですが、このたびPECLに登録しました(PECL :: Package :: timecop)。


PECLというのはPHP本体に含まれないPHP拡張を提供する公式のリポジトリです。PECLアカウントは承認制になっており、誰でも登録できるわけではありません。イタズラやお試しでの登録は減るでしょうが、代わりに登録への精神的ハードルが上がってしまうような仕組みだと言えるでしょう。実際、PECLに登録されているパッケージ総数は365個(2017/7/8時点)と多くはありません。また、日本人と思われるPECLアカウントは筆者以外では5人でした。


本稿では、PHP拡張をPECLに登録するまでのプロセスや、実際に登録してみてわかったことなどを紹介します。


PECLに登録するメリット

さて、そのPECLですが、PEAR*1の衰退とともに徐々に存在感が薄れている印象があります。今さらPECLに登録するメリットとは何でしょうか?


今回PECLに登録してみて、メリットとして次の3点を感じました。



1番目ですが、peclコマンドで拡張をインストールしている人が一定数いるようです。そういう人にとっては、下記のコマンドでPHP拡張がインストールできるのは大きなメリットでしょう。


$ pecl install timecop-beta # 正式リリース後には「-beta」が不要になります

個人phpizeビルドするのが普通になってしまったのでpeclで扱えてもありがたみは感じないのですが、インストールまでの手間は確実に減るので、悪いことではないでしょう。


2番目は印象論に近い話になりますが、PECLに登録してあるパッケージの方がロングサポートが期待できたり、多くの人のチェックが入っているように見えるかと思います。実際、後述するようにアカウント取得時にレビューが入りますので、PECLに登録されている時点で一定以上の品質だと期待できるでしょう。また、PECLのサイトに掲載されることで宣伝になるような側面もあるはずです。


最後はあまり知られていない気がしますが、PECLにはWindowsDLLの自動ビルドの仕組みがあります。PECLにパッケージをリリースすると、数時間後にWindowsDLLが勝手にアップロードされます。しかも、5.5から7.1までの4バージョン、スレッドセーフ有効/無効、32bit版/64bit版の全組み合わせ16個のDLLが提供されます。自力でここまで対応するのは大変なので助かりますね。


PECLアカウント取得まで


PECLアカウントの取得は承認制だと書きましたが、割とゆるい感じで運用されています。手順は全て「PECL :: Request Account」に書いてあるのですが、改めて紹介します。


  1. メーリングリスト pecl-dev@lists.php に参加して「自己紹介」「PECLに登録したい拡張の紹介」「コードへのリンク」を書き込む
  2. 誰かがレビューしてくれるので、返事をしたりコードを修正したりする
  3. 頃合いを見て「PECL :: Request Account」のフォームを埋める

1番目はそのままです。私は自己紹介が若干適当でしたが、仕事でPHPに触るようになって何年、とか言っておけばいいんじゃないでしょうか。


2番目については、割とすぐに誰かがコードを見た上で返事をくれます。人によっては「拡張の中でfork()するのは頂けない」みたいな真面目なコメントがついたりします。私の場合はRemiさん*2がコードレビュー&動作確認をしてくれて、「PHP 7.2で動かないよ」「テストが何件か通らないよ」という指摘をもらいました。


MLでの議論を尽くしたらアカウント申請を行います。リクエストフォームの「Sponsoring users」の欄にML上でレビューしてくれた人の名前を書けば良いでしょう。ここで登録したメールアドレスなどの情報はアカウントが作成された後のプロフィールページの初期値になります(参考:「PECL :: Yoshio HANAWA」)。


ちなみに、PECLアカウント申請だけではphp.netアカウントは作成されません。必要なら別途申請する必要があります。


PECLパッケージを作るまで

PECLアップロードするパッケージは適当なtar ballではダメで、peclコマンドが取り扱える形式でないといけません。この作成にはpeclコマンドが必要です。もし手元に見当たらない場合はpearインストールする必要があります。


また、パッケージングには package.xml が必要です。既存のパッケージを参考に適当に書いた上で、同じディレクトリで下記のようにすればパッケージが作られます。


$ pecl package
Package timecop-1.2.8.tgz done

XMLの中身がおかしいと警告やエラーが出たりしますので、適宜修正してください。できたtgzファイルをPECLのWeb管理画面からアップロードすればリリース完了です。CIと連携するようなオシャレなAPIは無いみたいです。


既に紹介した通りPECLパッケージのリリースのたびにWindowsビルドが走るのですが、ビルドに失敗するとDLLアップロードされません。ビルド時のログが下記のようなURLから確認できるので、失敗していた場合はログから原因を推測する必要があります。Windowsビルドを通すために何度もリリースするのも格好悪いので、真面目にやるなら手元にビルド環境を用意した方がいいでしょう。



まとめ

そんなわけで無事PECLへのリリースができました。3年前にchobieさんに言われたときからの宿題がようやく終わって良かったなーと思っています。



手元にPHP拡張を隠し持っているみなさんもPECLにリリースしてみてはいかがでしょうか。


参考URL

*1PHPライブラリのパッケージマネージャ&リポジトリ。現在はComposerが主流

*2Remi's RPM repositoryの運営者、かつPHPコアコミッターの一人

 
ページビュー
2348300