Hatena::ブログ(Diary)

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

[プロフィール]
 

2017年1月14日(土) PHPの中身をgdbで観察できるようなDockerイメージを作りました このエントリーを含むブックマーク このエントリーのブックマークコメント

CLI版のPHPgdb上で動かしつつ、内部的なデータ構造を覗き見ることができるようなDockerイメージを作ってDocker Hubにアップロードしました。Docker環境さえあればすぐに動かすことができます。


このイメージを動かせばCのコードを書かなくてもPHP内部のデータ構造を確認することができます。PHPの内部構造を詳しく知りたい、というような人は参考にしてみてください。


準備

Macの人はDocker for Macを用意しましょう。他のOS上でも同様にDockerインストールしてください。また、イメージの圧縮時サイズが200MB程度ありますので、それなりのネットワーク環境で遊ぶことをオススメします。


起動

Docker環境は通常seccomp配下で動いており、デフォルトではgdbが動きません。下記のように「--cap-add=SYS_PTRACE --security-opt seccomp=unconfined」を付けて起動する必要があります。


$ docker run -v $(pwd):/work -w /work --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined yhnw/php-debug:7.1 /bin/bash

遊び方

とりあえずzvalの中身を覗いてみましょう。


まず作業ディレクトリに移動して、適当なPHPプログラムを作ります。今回は中身を見てみたい変数を列挙してvar_dump()してみましょう。


$ mkdir -p work/php-debug
$ cd work/php-debug
$ vim var_dump.php

今回作成したvar_dump.phpは下記のようなファイルです。


<?php
$a = strtoupper("foo");
$b = [ $a, &$a, "FOO"];
$c = $a;
var_dump($b);
$a = 123;
$b[1000] = 1234.5;
var_dump($b);

このディレクトリDockerイメージ「yhnw/php-debug:7.1」を実行してgdbを起動します。


$ docker run -v $(pwd):/work -w /work --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined yhnw/php-debug:7.1 /bin/bash
root@536f406bded6:/work# gdb php
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from php...done.

まずPHP関数var_dump()にブレークポイントを設置します。PHPの内部関数はCの世界では「zif_」というプレフィックスがつきますので、zif_var_dumpという関数名になります。


(gdb) b zif_var_dump
Breakpoint 1 at 0x8429a4: file /usr/src/php/ext/standard/var.c, line 205.

では先ほどのPHPプログラムを実行してみましょう。


(gdb) run var_dump.php
Starting program: /usr/local/bin/php var_dump.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, zif_var_dump (execute_data=0x7ffff2214140, return_value=0x7fffffffb0b0) at /usr/src/php/ext/standard/var.c:205
205		if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
(gdb) n
209		for (i = 0; i < argc; i++) {

実行するとブレークポイントの設定によりPHPプログラム5行目のvar_dump()で停止します。次に「n」をタイプしてzend_parse_parameters()を実行しています。これによりvar_dump()のパラメータがargsに代入されます。


さて、ではvar_dump()の第一引数$bの中身を見てみましょう。


(gdb) printzv &args[0]
[0x7ffff2214190] (refcount=2) array:     Packed(3)[0x7ffff2258300]: {
      [0] 0 => [0x7ffff225fa08] (refcount=3) string: FOO
      [1] 1 => [0x7ffff225fa28] (refcount=2) reference: [0x7ffff2202120] (refcount=3) string: FOO

      [2] 2 => [0x7ffff225fa48] (refcount=0) string: FOO
}

情報量が多いので全部は説明しませんが、$bは3要素の配列であり、ハッシュテーブルを持たない真の配列(Packed)であることがわかります。また、$b[0]の文字列"FOO"の参照カウントは3であることがわかります。これは$b[0]と、$cと、$aの実体($aおよび$b[1]が共有している)の3つから参照されているためです。$b[1]の参照カウントが2なのは$aと$b[1]が同じ実体を参照しているという意味です。


$b[2]の参照カウントは0ですが、これは文字列リテラルを代入していることが原因です。文字列リテラルインターン文字列になるため、参照カウントおよびGCの対象外なのです(参考:「PHPのインターン化文字列とは何か - hnwの日記」)。


では、次のブレークポイントまでプログラムを再開してみましょう。


(gdb) c
Continuing.
array(3) {
  [0]=>
  string(3) "FOO"
  [1]=>
  &string(3) "FOO"
  [2]=>
  string(3) "FOO"
}

Breakpoint 1, zif_var_dump (execute_data=0x7ffff2214140, return_value=0x7fffffffb0b0) at /usr/src/php/ext/standard/var.c:205
205		if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {

プログラム5行目のvar_dump()の結果が表示され、8行目のvar_dump()で再び停止しました。改めて$bの中身を見てみましょう。


(gdb) n
209		for (i = 0; i < argc; i++) {
(gdb) printzv &args[0]
[0x7ffff2214190] (refcount=2) array:     Hash(4)[0x7ffff2258300]: {
      [0] 0 => [0x7ffff225fb60] (refcount=2) string: FOO
      [1] 1 => [0x7ffff225fb80] (refcount=2) reference: [0x7ffff2202120] long: 123

      [2] 2 => [0x7ffff225fba0] (refcount=0) string: FOO
      [3] 1000 => [0x7ffff225fbc0] double: 1234.500000
}

$b[1000]に値を代入したことで$bが4要素の連想配列(Hash)に変わったことがわかります。PHPは添字が全部数字であっても中身がスカスカになってしまうような場合は内部構造として連想配列を採用します。各要素のzvalのアドレスも書き換わっていることから、データ構造の変更に伴い要素のメモリコピーが発生していることもわかります。仕組み上当然のことですが、PHP 7において当初配列として利用していたものを途中から連想配列として利用するのは非効率なのです。


また、$a[0]の参照カウントが1減って2になっていることもわかります。これは$aの実体が123に書き換わり、参照先が変わったためです。


ちなみにprintzvというのはPHPに標準添付の.gdbinitで定義されている関数で、zvalの中身を見るのに非常に便利です。もしgdbの標準コマンドで$b[0]の中身を見るとすれば、次のような手順が必要になります。


(gdb) p args[0].u1.v.type
$1 = 7 '\a'
(gdb) p args[0].value.arr.arData[0]->val.u1.v.type
$2 = 6 '\006'
(gdb) p (char *)args[0].value.arr.arData[0]->val->value.str.val
$3 = 0x7ffff22649d8 "FOO"
(gdb) p args[0].value.arr.arData[0]->val->value.str->gc->refcount
$4 = 2

まず$bが配列であること(type=7)を確認し、次に$b[0]が文字列であること(type=6)を確認してその値にアクセスしています。zvalでは共用体を多用しているので仕方ないのですが、ちょっと面倒すぎますよね。


シンボルテーブルを確認する

シンボルテーブルを確認する方法も紹介しておきます。グローバルなシンボルテーブル(つまりグローバル変数)は次のように確認できます。


(gdb) print_ht &executor_globals.symbol_table
Hash(10)[0x1368530]: {
  [0] _GET => [0x7ffff2259100] (refcount=2) array:
  [1] _POST => [0x7ffff2259120] (refcount=2) array:
  [2] _COOKIE => [0x7ffff2259140] (refcount=2) array:
  [3] _FILES => [0x7ffff2259160] (refcount=2) array:
  [4] argv => [0x7ffff2259180] (refcount=2) array:
  [5] argc => [0x7ffff22591a0] long: 1
  [6] _SERVER => [0x7ffff22591c0] (refcount=2) array:
  [7] a => [0x7ffff22591e0] indirect: [0x7ffff2214080] (refcount=2) reference: [0x7ffff2202120] long: 123


  [8] b => [0x7ffff2259200] indirect: [0x7ffff2214090] (refcount=2) array:

  [9] c => [0x7ffff2259220] indirect: [0x7ffff22140a0] (refcount=2) string: FOO

}

そのスコープでのシンボルテーブル(=ローカル変数)は次のようにすれば見えることもあります(PHP 5.2までは常にローカル変数に対応するシンボルテーブルが作られていたのですが、PHP 5.3以降は必要なときだけ作られるようです)。


(gdb) print_ht execute_data.symbol_table

Dockerイメージについて

ちなみに今回作ったDockerイメージはオフィシャルのphpイメージのリポジトリをforkして作りました。Travis CIを利用しており、masterにコミットするとCIDocker Hubへのデプロイまで行うようにしてありますので、参考にしてみてください。



ちなみに5.6と7.0のイメージもデプロイ済みです。


まとめ

Cもgdbも良くわからなくても、PHPプログラム中のvar_dump()のタイミングで止める方法と「printzv」だけで結構遊べるのではないでしょうか。もう少し深く知りたい場合はgdbマニュアルおよびPHPソースコードのZend/以下を調べてみてください。

トラックバック - http://d.hatena.ne.jp/hnw/20170114

2016年12月31日(土) 2016年をふりかえる このエントリーを含むブックマーク このエントリーのブックマークコメント

年末なので今年何をしたか列挙していきます。


書いた

2016年はてなダイアリーに本記事を含め17本の記事を書きました。人気があったのは下記の記事です。



Qiitaでは62本の記事を書きました。今年はPHP以外の内容を多めにしたつもりだったんですが、人気が出るのはPHPの記事が多いですね。



また、会社ブログにも2本記事を書きました。うち1本の翻訳記事が人気を集めました。



参加した

勉強会カンファレンス発表を3件行ったのを含め、計5回勉強会に参加しました。



AVTOKYOは本当に面白かったです。来年は全然行ったことがない集まりに積極的に行きたいですね。


PHP勉強会は去年に引き続きの主催でした。約1年ぶりの開催でしたが、来年はペースを上げて年2回開催したいと思っています。


意識的インプットを増やした

今年は過去触ったことがない技術も積極的に触っていこう、という気持ちで色々取り組んでみました。



つまみ食い程度で終わってしまいアウトプットまで至らないものも多かったので、来年はもう少し深掘りする題材を見つけたいです。


個人プロダクトのリリース・メンテナンス

今年5月にVisual Studio Codeのエクステンション「Auto-open Markdown Preview」を作ってMarketplaceに公開しました。



思ったより多くの人が使ってくれているようでissueもボチボチ来ているんですが、現状で自分の用途は完全に満たしているので、機能追加にはあまり気乗りしないのが悩みどころです。


また、継続してメンテナンスしているPHP拡張モジュールphp-timecopについて2回マイナーバージョンアップを行いました(PHP7対応、マイクロ秒対応)。



OSSへのcontribution

今年はZshバグレポを送る、ということをしてみました。この内容はZsh 5.3に取り込まれています。



また、PHP本体へのバグレポを2本、pull requestを1本出しました。



php-buildに3件Pull Requestを送ったりもしました。



他にもPHP-doc MLPHP日本語マニュアルについて議論するML)に修正提案を5件投げました。しばらくML配信が止まっていたんですが、無事に復活してよかったです!


まとめ

今年は別の技術領域に入門して継続的に取り組んでいこうと思っていたのですが、残念ながら実現できませんでした。一方で、PHPについては目鼻が利くせいか、ちょっと調べるとそこそこ面白い話題にたどり着けたような気がします。他の技術領域で同じようなレベル感になりたいものです。


というわけで、来年もがんばるぞー

トラックバック - http://d.hatena.ne.jp/hnw/20161231

2016年12月12日(月) 第七回闇PHP勉強会でrealpathキャッシュとデプロイの話をしました このエントリーを含むブックマーク このエントリーのブックマークコメント

昨日12月11日に第七回闇PHP勉強会を開催いたしました。私を含め発表者6人ということで、とても盛り上がった勉強会になりました。発表者の皆さま、またご参加いただいた皆さま、本当にありがとうございました。また会場提供いただいたピクシブ株式会社さまにも大変お世話になりました。


以下が私の発表資料です。



PHPアプリケーションをsymlink切り替えでデプロイしているとrealpathキャッシュ絡みで何かしらトラブルがありますよね、というくくりで複数のトピックを紹介するような内容でした。タイトルの通り、一番話したかったのはmod_phpとphp-fpmとでOPcacheの挙動が変わる話だったんですが、かなり入り組んだ内容だったのでうまく伝わらなかったかもしれません。


質問タイムに、@edvakfさんから面白いエピソードを聞くことができました。Pixivではこの手の問題に一通りハマった結果、現在では「realpath_cache_size=0」での運用に落ち着いており、性能面でも特に問題は出ていないそうです。なぜ性能面で問題が出ないのか、その場では答えられなかったのですが、OPcacheによるopcodeキャッシュがrealpath cacheの前段のキャッシュのように働いているのが理由かもしれません。多段キャッシュ構成で前段キャッシュの方が高性能であれば、後段キャッシュって何の意味もないですからね…。


また、発表資料ではblue-green deploymentが最強ではないかという話を紹介しましたが、Pixivではデプロイ完了までの時間を重視しているためsymlink切り替えの方が良い、という話も聞けました。環境によって何を重視するかは異なるはずで、選択肢は人それぞれということだと思います。


他にも、rsyncデプロイFTPデプロイもまだまだ現役ですよ!という話も多数聞くことができました。だから120秒間の新旧*.php混在問題は黙っていてほしい、なんて話もあったりなかったり(笑)。


懇親会もピクシブの皆さまにお世話になりまして、勉強会会場で引き続きの開催となりました。その場の空気感を維持したまま議論が深められる上に離脱率も減るので、同じ会場での懇親会はメリットが大きいなあ、と感じました。おかげさまで色々な方とお話しすることができました。


次回こそは半年後くらいに開催したいと考えておりますので、引き続きよろしくお願いいたします!

2016年11月5日(土) PHPカンファレンス2016でOpenWrtについてLT発表をしました このエントリーを含むブックマーク このエントリーのブックマークコメント

11月3日に開催されたPHPカンファレンス2016でLT発表してきました。以下が発表資料です。



OpenWrtの名前を知ってもらって、その後うっかり触る人が出てきたらいいなあ、と考えて発表してみました。PHPとほとんど関係ない内容でしたが、それなりに面白がって頂けたのかな?と思っています。興味を持った方は「OpenWrt」で検索すれば大体のことは日本語で見つかると思います。また、下記URLで自分がLEDE(OpenWrtのfork)をインストールしたときの手順を紹介しています。



OpenWrtの近況

OpenWrtはルータ向けのLinuxディストリビューションですよ、という程度の紹介をプレゼン中で行いましたが、改めて昨今のOpenWrtを取り巻く状況について書いてみます。


まず、今年の10月にOpenWrt Summit 2016ドイツで開催されました。去年に引き続き2回目の開催のようで、ここに来てプロジェクトとしての勢いが加速しているような印象です。


このOpenWrt Summitをスポンサードしているのがprpl Foundationという組織です。この組織の正体はイマイチわかりませんが、QualcommBroadcomといったMIPS系SoCに関わる大手企業が一定の金を出していそうな雰囲気で、OpenWrt絡みのプロジェクトに出資するような活動もしています(参照:「OpenWrt/LEDE Project Funding Support from prpl」(PDF))。OSSプロジェクトでお財布の心配が無くなることは良いニュースではないでしょうか。


一方で、OpenWrtからLEDEというforkができて大半の開発者が両方にコミットしているような状況でもあり、この人たち本当に大丈夫なのかしら?と思ったりもします。


そんなわけで、OpenWrtは超有望ってほどでもないけど一定期待されているOSSプロジェクトという評価が妥当なのかな、と個人的には考えています。


OpenWrtのバイナリパッケージをTravis CI上でビルドデプロイ

発表では全く触れませんでしたが、OpenWrtのバイナリパッケージのMakefileGitHub上で管理して、git pushすると勝手にTravis CI上でビルドが走ってデプロイまで行われる仕組みを作りました。ビルド環境はDockerイメージとして構築したものをDocker Hubアップロードしています。


f:id:hnw:20161105095911p:image


仕組みの詳細については下記リポジトリの.travis.ymlやTravis CIのログなどをご確認ください。



そもそも、自分専用のパッケージを作るだけであれば手元のマシンでビルドした方が断然楽です。ただ、皆が手元でビルドしているとノウハウの共有が行われにくいので、このように全手順をネット上に公開できる仕組みは価値があると考えています。まだまだ荒削りだと思いますが、参考にして頂ければ嬉しいです。


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

今年のPHPカンファレンスも刺激をたくさんもらった集まりでした。特にt_wadaさんの発表「PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計」はスライドもトークも素晴らしいものでした。今回は平行トラック数が多くて見られなかった発表も多かったのですが、興味深いものばかりで、ジャンルとしても多岐に渡っているように感じました。スライドも動画も公開されつつあると思いますので、後で確認したいと思います。


また、懇親会や2次会で多くの方々と技術の話題から雑談まで色々とお話しさせて頂きました。話そうと思っていたのに結局話せなかった方も何人かいらっしゃいましたが、次回こそよろしくお願いいたします。


最後になりますが、発表者の皆様、スタッフの皆様、本当におつかれさまでした。来年も期待しています!

2016年10月29日(土) PHPでは正規表現コンパイル結果のキャッシュが暗黙に行われている このエントリーを含むブックマーク このエントリーのブックマークコメント

筆者がPHPをさわり始めたころ、「PerlのコレはPHPではどうやるんだろう?」と思うことが頻繁にありました。一部の疑問については解説を見つけたり自分でソースコードを読んだりして解決したものの、考えるのをやめてしまったものもあります。その一つが正規表現コンパイル結果の保存に関するもので、最近まで完全に忘れていました。


正規表現コンパイルというのは与えられた正規表現を解釈して実行しやすいデータ構造に変換する作業のことを指します。具体的にはDFA(決定性有限オートマトン)を構成するか、正規表現エンジン内部で用いられるVM命令列に変換するかといった処理になります。これらは複雑な処理ですので、性能の観点で言えば同じ正規表現に対するコンパイル処理はできるだけ繰り返したくありません。


Perlの場合、スタティックな正規表現コンパイルは1回しか行われません。一方で、正規表現変数が使われている場合は毎回内容が変わる可能性があるため、毎回コンパイルが走ります。毎回のコンパイルを防ぐためのoフラグというものがあるなど、このあたりの仕組みについてはラクダ本でもページ数を割いて説明されており、多くのPerlプログラマ正規表現コンパイルがいつ走るかを意識しながらコードを書いているはずです。


一方、PHPでは正規表現コンパイルに関する話題自体をほとんど聞いたことがないように思います。PHPで同じ正規表現処理が何度も実行される場合に、正規表現コンパイルが1回しか行われないのか、毎回行われているのか、この疑問に答えられるPHPプログラマはごく小数ではないでしょうか。


本稿ではPHP正規表現コンパイルとそのキャッシュの仕組みについて紹介します。


PHPでの正規表現処理


PHP正規表現処理の関数を2系統持っており、それぞれ下記の拡張モジュールで提供されています。


  • PCRE
  • mbstring

PCRE拡張で採用されているPCREはPerl正規表現を提供するライブラリで、他のOSSでの採用事例も多く見られます。PHPでは本家PCREのバージョンアップにマメに追従しており、PHP 7.0.12ではPCRE 8.38が同梱されています。


一方、mbstringは日本では定番のマルチバイト処理の拡張モジュールで、正規表現関数も提供しています。mbstringで利用している正規表現ライブラリRuby 1.9系でも採用されていた鬼車で、PHPに同梱されているのは鬼車 5.9.6です。いまのところ鬼車6.x系への追従や鬼雲に切り替えるなどという話は聞いたことがありません。正直なところ、UTF-8全盛の昨今であればPCREだけで十分な気もします。


ちなみにPHP 5.xまでは更に別のPOSIX正規表現ライブラリも持っていたのですが、7.0からは削除されています。


PCREとコンパイル結果のキャッシュ

まずPCREの方から紹介します。PCRE拡張では正規表現コンパイル結果のキャッシュPHPプロセス内で最大4096個までキャッシュされる仕組みになっています。このキャッシュプロセス内で永続化されているため、正規表現コンパイル結果はリクエストをまたいで共有されます(ただし、プロセスをまたいでの共有はできません)。


最初の疑問について言えば、同じ正規表現が与えられた場合には最初の1回しか正規表現コンパイルは走らないし、もしかすると以前のリクエストで作られたキャッシュにヒットすれば1回も正規表現コンパイルを行わない可能性さえあるというわけです。


実は、このことはPHPマニュアルにも書いてあります。


この拡張モジュールでは、コンパイルした正規表現のためにスレッド単位のグローバルキャッシュ (最大 4096) を管理しています。


http://php.net/manual/ja/intro.pcre.php


このキャッシュの効果は簡単な実験で確認できます。次のようなプログラムを実行してみましょう。


<?php
$num_regex = 4096;
$start = microtime(true);
for ($i = 0; $i < 100; $i++) {
    for ($j = 0; $j < $num_regex; $j++) {
        preg_match("/([a-z]{1,10}){1,10}$j/", "foo");
    }
}
var_dump(microtime(true)-$start);

これは$num_regex種類の異なる正規表現マッチを繰り返し実行するだけのコードで、私の手元で実行したところ0.45秒程度でした。ところが、$num_regexを1増やして4097にしてみると実行時間が18秒となり、劇的に時間がかかるようになってしまいました。正規表現キャッシュサイズが4096であるため、それ以上の種類数にしてしまうと毎回キャッシュが追い出されてしまって都度正規表現コンパイルが走るので非常に遅くなるというわけです。


このキャッシュ処理の詳細はPHPソースコードext/pcre/php_pcre.cのpcre_get_compiled_regex_cache関数で記述されています。


mbstring(鬼車)とコンパイル結果のキャッシュ

mbstringの正規表現コンパイル結果もキャッシュされていますが、こちらは同一リクエスト内のみで使い回され、リクエスト間で共有されることはありません。mbstringではコンパイル結果のキャッシュ個数に上限はなく、異なる正規表現コンパイルするたびにメモリを消費していきます。


また、同じ正規表現で内部的なフラグだけが異なっているような場合は最新1件しかキャッシュされません。つまり、mb_eregとmb_eregiとで同じ$patternを与えたような場合、前に実行した方のコンパイル結果は上書きされてしまい、後で実行した方のキャッシュしか残りません。


このキャッシュ処理はPHPソースコードext/mbstring/php_mbregex.cのphp_mbregex_compile_pattern関数で行われています。


キャッシュに関する注意点

これらのキャッシュ正規表現を理解しているわけではなく、正規表現パターン文字列をキーにしてコンパイル結果を連想配列に格納しているだけです。明らかに同じ内容の正規表現であってもパターン文字列が異なっていればキャッシュは使われず、再度コンパイルが行われます。


たとえば下記のようなコードを書いた場合も、それぞれ個別にコンパイルされてキャッシュエントリを2個消費してしまいます。

<?php
$foo = "foo"
preg_match("/foo/", $foo);
preg_match("~foo~", $foo);

仕組みを考えれば仕方ないかもしれませんが、少し残念ですね。


まとめ

トラックバック - http://d.hatena.ne.jp/hnw/20161029
 
ページビュー
2060898