広告

この広告は30日以上更新がないブログに表示されております。
新規記事の投稿を行うことで、非表示にすることが可能です。
posted by fanblog

PHP5で動く、PHP7なんか動きが違う、原因は何?どこをチェックするのがいい?

PHP5とPHP7の違いは、ワードプレスを使っている限りほとんど意識することはありません。レンタルサーバーのコントロールパネルからPHPのバージョンを5.6.x系から7.0、7.1、7.2系に切り替えるだけでそのまま動きます。互換性の問題は発生しないのが素晴らしいです。よく考えられたアップデートだと思っていたんですが、5.6.x系・7.0系=>7.1系のアップデートは、とても気づきにくい罠がありました。知っていましたか?負の offset をサポートするようになりました。って一文でfile_get_contentsが別物の動きをしていました。

PHP5.6では、思い通り、設計通りの動作をしていたプログラムがありました。
このプログラムは、macOSで動かしていて、先日macOSをSierraからHigh Sierraにアップグレードしました。
macOS Sierraは、PHP5.6系です。macOS High Sierraは、PHP7.1系がすぐに使える状態で入っています。
この違いは、プログラムの結果が求めている動作をしていないことで初めて知りました。


PHPのマイグレーションをページに新機能、新しい関数、下位互換性のない変更点、推奨されなくなる機能、変更された関数、その他の変更など、PHPのバージョンをあげる前に注意するべきポイントがまとまっているので参考にしています。
メニューから各バージョンのマイグレーションを選択することができます。
参考:PHP7.0.xからPHP7.1.xへの移行

今回、発生した問題は、この移行ページに掲載されていました。
PHP 7.0.x から PHP 7.1.x への移行>変更された関数 >ファイルシステム
file_get_contents() now accepts a negative seek offset if the stream is seekable.

さらりと1行でまとまっています。

file_get_contents関数はファイルを読み込んだり、URLからWEBページを取得できるよく使われる関数の一つです。
PHP5のfile_get_contents関数の説明はこんな感じでした。
PHP5が最新だった頃のfile_get_contentsの解説(php.net)

それが、PHP7.1の変更後、このように変わっていました。
php7.2.2が最新の頃のfile_get_contentsの解説(php.net)


違いわかるでしょうか?そうです!$offsetのデフォルト値が-1から0に変わっています。マニュアル上はこのように変わっていますが、PHP5.6はいままで通り-1を渡しても0と同じ意味です。

一方、$offsetに-1を渡すようなプログラムをPHP7.1以降で動かすと、 ストリームの末尾からのオフセットと解釈されます。

=>PHP5で動く、PHP7.1で動きがおかしい原因はこれでした。

関数のデフォルト値って不変、ずっと変わらないものって思い込んでいました。現実は、バージョンアップで変わることがあるってことです。ファイル名以外、引数を指定することがないって方も多いかと思います。

作り込んだライブラリで、拡張性を持たせるためにPHPのデフォルト引数も全て受け付けるようなラッパー関数、ヘルパー関数はPHPのデフォルト値を自作関数、メソッドの引数にコーディングすることが多いです。


PHP5で動いていたものがPHP7で動かなくなったら、チェックすべきポイント


PHPのマイグレーションには、たった1行の変更点の記載でしたが、インパクトが大きいってことがわかったかと思います。
自作関数、自作メソッドでPHPのデフォルトを書いているところは、マニュアルを参考にし、再確認するのが良いかと思います。

この際、マイグレーションページを見ながら、記載のある関数を使っているかgrepやIDEの検索機能をフルに使ってチェックするのが効率的です。


=>マイグレーションページをインデックスにし、関数のマニュアルとソースを比較!


まとめ


繰り返しになりますが、PHP5とPHP7の違いは、ワードプレスを使っている限りほとんど意識することはありません。今回のfile_get_contentsに関しても完全互換性があり、ファイル名のみの指定で使っていると同一です。でもデフォルト引数の違いがありましたね。拡張性を考えたメソッドやヘルパ関数は標準と同じような引数を許可するような設計はこういった変更のインパクトが大きいって思いましたよ。

0と-1で別物の動きになってしまいますから・・・


posted by scripts at 10:50 | Comment(0) | TrackBack(0) | php

PHP string streamのやり方

PHPで文字列(テキスト)をストリームとして読み込みたい、fgetsのような感じでループさせながら1行ずつ処理したいっていう際に使えるやり方です。
実際のやり方は以下PHPソースコードの通りです。$txtはStringストリームとして扱いたいテキストを指定します。file_get_contentsで読み込んだテキストでもOKです。ここではヒアドキュメント形式で設定しています。テキストファイルから読み込む場合は、改行コードの違いに注意してくださいね。
<?php
$txt = <<< EOM
1行目
2行目テキスト
3行目文字列
4行目文字列
EOM;

foreach( explode( PHP_EOL, $txt) as $row=>$line ){
// $rowは0から始まる番号です。
// $lineは1行ずつ文字列が入ってきます。
}

読み込んだテキストを改行で区切り、配列に格納しています。ここでは変数に入れることなく、直にforeachで回しています。
これでテキストをStringストリームのように1行ずつ処理することが可能になります。$rowは配列の添え字なので0から始まることに注意してください。行番号ではないですよ。

PHP_EOLは、行末文字です。WindowsならWIndowsの改行コード、Unix系ならUnix系の改行コードを意味しています。


posted by scripts at 14:45 | Comment(0) | TrackBack(0) | php

database is lockedに悩んだら、コレで解決!原因は予想外のコイツ!でした

SQLite3でたまに発生するdatabase is lockedって悩ましいですよね。勝手にロック解除してくれるとありがたいんですがlock状態を継続してしまうので時間経過で問題も大きくなりがちです。ここではdatabase is lockedになったデータベースファイルをunlockの状態に戻す方法、真の原因、lockされているデータベースファイルを特定する方法などがわかります。

SQLite3データベースをunlockする方法


ここではロックされているデータベースをdb.sqlite3としています。アンロックする方法自体はとても簡単でした。リモートログインしてshell操作する必要があります。FTPでも同様のことができるかもしれませんが、ここでは試していないのです。

移動して、コピーすることで、unlockできました。

1. mv db.sqlite3 bk.db.sqlite3
ロックされているSQLite3のデータベースファイルを別名にします。(mv)
2. cp -p bk.db.sqlite3 db.sqlite3
元と同じファイル日付で、元と同じファイル名としてコピーします。(cp -p)

たったこれだけの操作で悩み悩んだdatabase is lockedが回避できました。

ファイルを別名し、コピーすることでロックが解除できます。
sqlite3データベースにロック情報が記録されているのであれば、この操作では中身を一切書き換えていないので、アンロックされることはあり得ません。
このことからロックの原因は、PHPのSQLite3 拡張モジュールで使われているflock系のシステムコールに起因すると想像しています。

database is lockedが発生した真の理由


SQLite3データベースをunlockする方法にflock系のシステムコールがロックの原因と書きました。これは誰かがflockしているからdatabase is lockedが発生してしまうことになります。では、誰が一体flockをやっているのか?遅延書き込みなどの要因?
疑問でしたが、ようやくわかりました。まず以下のような設計でクーロン実行させています。
  1. 1)プログラムはPHPで作っています。クーロン実行させています。
  2. 2)エックスサーバーのクーロンは、1回で動作できる時間が30秒までの制限があります。
    制限に達するとこのようなPHP Fatal error: Maximum execution time of 30 seconds exceededエラーメッセージとともに強制終了されます。
  3. 3)PHPプログラムは、SQLite3データベースに同時にアクセスしない設計になっています。そのタイミングでCRUDを行うのは1つのプログラムだけです


このような状況下で、database is lockedが発生しました。SQLite3データベースをunlockする方法で解決しますがロックさせてしまう根本的な対処にはなっていないですよね。

プログラムの設計上、シングルで動作しているのでロックされること自体ありえないって思っていました。原因は、プログラムの重複実行でした(笑)

予想外です。まさかの自分自身のプログラムがdatabase is lockedの要因を作っているなんて・・・


ただ、これ自分でプログラムを複数実行しているわけではないです。スケジュール実行で自動実行されています。今回対象のプログラムは30秒以上動作することがあるのでMaximum execution timeになり、プログラムが強制終了していたはずなのですが。。。どうやら生き残る生命力豊かなたくましい奴がたまに誕生するようです。
1)Maximum execution timeになる処理はデータベースの更新を含んでいるのでタイミングが悪いとトランザクションでロックした状態になりえます。
2)そのため次のスケジュール実行で動いたクーロンプログラムがdatabase is lockedで失敗する
この流れでdatabase is lockedが発生していたと確信しています。


根本的な原因はクーロンが生き残ることがあるということでしたね。あとはこれに対応できるようにプログラムを変えるだけです。例えば29秒経過したら自発的に正常終了させる、クーロン実行されたプログラムのPIDを記録して、killするようにする、ロックの回避方法はわかっているのでロックが発生したらロックの回避方法を試すなどのことをやればdatabase is lockedを撲滅できそうです!


ロックされている?データベースロックの確認方法


database is lockedエラーが発生した、1つのSQLite3データベースだけ扱っていればロックされたデータベースの特定は簡単ですよね。複数のSQLite3データベースがあるとどれ?これ?どうやって調べるの?となります。どうしたら単純に調べられるのか、コマンドを色々試して見つけたのがこの方法です。
この方法は、リモートログインして確認します。
シェルからsqlite3コマンドでSQLite3データベースファイルに対してVACUUMコマンドを実行する方法です。

$ sqlite3 データベース "vacuum;"

lockedの状態のデータベースファイルは、このコマンドを拒否します。
SQL error: database is locked
というメッセージが出力されます。

問題なければ、VACUUMにより空き領域が解放されます。データベースサイズが膨れていくので定期的にVACUUMコマンドを実行するのが良いとされていますよ。一石二鳥の確認方法ですね!

1つ注意点があって、他のプログラムやPHPからアクセス中にはやらない!ってことです。やっても良いですが失敗する可能性があるのと、参照している側もそのタイミングにアクセスすることでCRUDが失敗する恐れもあります。そのため、アクセスが少ない時にやったほうが良さそうです。

ちなみにCRUDは、Create/Read(select)/Update/Deleteのデータベース操作を示しす用語です。

まとめ


いかがでしたでしょうか?ここまでSQLite3データベースをunlockする方法、database is lockedが発生した真の理由、ロックされている?データベースロックの確認方法をご紹介してきました。

database is lockedは回避できましたでしょうか。

今回ご紹介したこの方法で私は回避できました。たまに間違えるのですが、mvは重要です。消すのは怖いからと代わりにcpでやると解決できません。lockの状態を継続しますよ。必ず元あったデータベースファイルの存在を消す!ってことが重要です


参考までに、この現象は、xserverで遭遇しました。
SQLite3のバージョンは、SQLite version 3.3.6です。
PHPのバージョンは、7.0.16です。
posted by scripts at 12:10 | Comment(0) | TrackBack(0) | php

sqlite3 1万件を超えるならUPDATE/DELETEは使っちゃダメ

PHP+PDOでsqlite3のデータベースを使うことがありました。

データ件数は、1日1000件ぐらい増える感じで、1万件、5万件、10万件と増えていきます。

当然追加(INSERT)する際は、PK(実際はインデックスなし)の存在を確認してInsert Or Updateな処理が必要になります。

件数が多くなるとトランザクションは必須です。

テストデータは、5万件超えのもので確認しています。
何も考えずに、select pk from tbl where pk=?的なSQLで検索して、存在しなければInsert、存在すればUpdateという処理にすると

(以下、msec=ミリ秒です。)

select発行コストは、1件あたり0.001sec(1msec)とごくわずかな時間でした。が、
UPDATE発行コストは、1件あたり0.06sec(60msec)もかかります。
10件で、600msec、100件で6秒、1000件で1分・・・・うーん数は力です。

UPDATE発行コストを下げるには・・・


件数が少ない場合、無視できるコストですが、件数が10万件、将来的に100万件を超えるようなテーブルでは、無視できません><

パッと思いつく方法は、以下2つでした
1. UPDATE対象専用のテーブルに、INSERT、まとめてUPDATEする方法
2. UPDATE対象専用のテーブルに、INSERT後、更新対象をTBLからDELETE、その後UPDATE対象=>TBLヘ

「1. UPDATE対象専用のテーブルに、INSERT、まとめてUPDATEする方法」
=>INSERTコストは全く気になりません。
  update tbl set x = (select uptbl.x from uptbl where tbl.pk = uptbl.pk ) where exists (select uptbl.pk from uptbl where tbl.pk = uptbl.pk)
  みたいなSQLでUPDATE対象専用のテーブル=>TBLへまとめて更新する方法は、
  1件ずつUPDATEする方法とほぼ同等のコストでした。><

「2. UPDATE対象専用のテーブルに、INSERT後、更新対象をTBLからDELETE、その後UPDATE対象=>TBLヘ」
=> delete tbl from pk = ?というSQLで1件ずつ削除すると1件削除の平均コストは124msecでした
  delete 超遅い><


たった2つしか試していませんがw・・・
m(_ _)m UPDATEコストを低減させる方法は、見つかりませんでした。

ここまでを一言でまとめると・・
sqlite3ではUPDATE/DELETEはコストが高い!です。

できるだけ、UPDATE/DELETEを発行しないような仕組みに作り変えます。

sqlite3は、INSERT/ALTERを多用しようぜ!


ここまで確認している中で、INSERTコストは、全く気になりません。
トランザクションのメリットですね。

ということで、INSERT主体の処理に変更します。
ちなみに、私は、この方法で、5分かかっていた処理が 3秒になりましたw


beginTransaction
LOOP START
SELECT PK
INSERT OR UPDATE
LOOP END
commit

と、
1テーブルに対して、SELECTして、値を得た結果でINSERTかUPDATEで挿入、更新を行う処理だったものを、

beginTransaction
SELECTで全件検索
 更新対象となるキー 値を書き換えて UPTBLへINSERT(更新)
更新対象外     そのままUPTBLへINSERT(更新対象外)
SELECT END
commit

更新対象で処理していないものを全部UPTBLへINSERT (新規)
という流れに変更しました。

UPTBLは、CREATE TABLEでTransaction開始前に作成しています。

この変更点をサクっというと、
5万件のテーブルは触れずに、新たに5万件のテーブルを更新しながら作っちゃう作戦です。
繰り返しになりますが、UPDATEやDELETEを発行しない方法です。

でも、
5万件のデータでも、2、3秒で終わります。


この後、元々のTBLをDROP TABLE、
更新したUPTBLをTBLに変更します。
=>ALTER TABLE UPTBL RENAME TO TBLでUPTBLをTBLに変更しちゃいます。

私が検証した結果では、速度面でUPDATE/DELETEは使ってはいけない、代わりに新たにテーブル作っちゃえという結果になりました。


最後に・・・
検証は行っていますが、あくまで個人の環境です。
お手元にあるDBの特性に合わせてチューニングすることをお勧めします。

・・・・

ファイル系のデータベース、sqlite以外にもSQL Server CompactやAccess(ACE)などもあります。

sqliteは、SQL Server Compact(sdf)やOLEDBに比べて、とても簡単な印象を受けました。
PHPなどでは何もインストールしなくても使えるし、fts3,4の仮想テーブルを使うと型宣言も必要ないし、・・

万件データを扱うようになると、やはりSQLコストが問題になってきます。
likeや=での検索は、致命的に遅いです><

なので、全文検索のmatchという選択にせざる終えませんが、
謎の挙動が多いので、https://sqlite.org/fts3.html を徹底的に読んだ方が良さそうです・・・



sqlite3のデータベースは、ディスクIO速度が性能に直結します。
可能であれば、SSDの領域にdbファイルを置いて処理すると格段にパフォーマンスが上げられます。
さらに余裕があれば、メモリ領域に置くことで高速化可能です。

SQL Server/Oracle/MySQL/PostgreSQLなども同様ですが、結局のところ、IOコストですね・・

変更来歴
2015/1/17 言葉足らずな文章だった箇所を修正しています。
2017/9/22 SQLite3のコツ10選を追加しました。

続きを見る方はこちらをクリックしてください
タグ:sqlite3 PDO
posted by scripts at 17:08 | Comment(0) | TrackBack(0) | php

php error をconsoleに出力

コマンドラインからphpを実行させた場合、デフォルト設定ではエラー発生した際、なにも出力されない
$ php a.php
$
という結果になります。

phpのソースをいじらずに、エラーになるかどうか確認したい場合はlintを使います
$ php -l a.php
Errors parsing a.php

正常時:
No syntax errors detected in a.php

残念ながら、エラーがあるよ、ないよというレベルでしか確認できません。

php.iniのdisplay_errorsを1にすることで標準出力、display_errors=2とすることで標準エラー出力に出力させることができます。

ただ、php.iniは本番環境でも参照されるものなので、単純な確認で変更したくありません。

php error consoleに簡単出力


phpコマンドには、-dオプションが存在します。

--define foo[=bar]
-d foo[=bar] Define INI entry foo with value bar

この-define/-dを使うことで、いまから実行するphpのみエラーレベルを指定&console出力させることが可能になります。

全部のエラーをconsoleに出力させるにはdisplay_errors=1とerror_reporting= 2147483647を指定します。
$ php -d display_errors=1 -d error_reporting=2147483647 a.php
Parse error: parse error, expecting `"variable (T_VARIABLE)"' or `"${ (T_DOLLAR_OPEN_CURLY_BRACES)"' or `"{$ (T_CURLY_OPEN)"' in a.php

これにより、ソースはいじらずに、動作確認で検証したいときのみ、consoleにエラーを表示できるようになります。

aliasを設定するとさらに便利になります


毎回毎回打ち込むのは、あまりにも面倒なので、例えばaliasをつかって定義すると簡単になります。
.cshrc/.bashrcなどに以下を追加
.shrc/.bashrc
alias phpconsole='php -d display_errors=1 -d error_reporting=2147483647'

.cshrc
alias phpconsole php -d display_errors=1 -d error_reporting=2147483647


$ phpconsole a.php
と打ち込むとaliasで展開され
$ php -d display_errors=1 -d error_reporting=2147483647 a.php
が実行されます。

posted by scripts at 13:45 | Comment(0) | TrackBack(0) | php
最新記事
最新コメント
タグクラウド
カテゴリアーカイブ
×

この広告は30日以上新しい記事の更新がないブログに表示されております。