Deprecated: The each() function is deprecated. This message will be suppressed on further calls in /home/zhenxiangba/zhenxiangba.com/public_html/phproxy-improved-master/index.php on line 456
anoncom blog: PHP
[go: Go Back, main page]

ラベル PHP の投稿を表示しています。 すべての投稿を表示
ラベル PHP の投稿を表示しています。 すべての投稿を表示

AppEngineでcomposerが更新されないとき

Google AppEngine

 最近になってようやくGoogle AppEngine (以下GAE)を使いだし、自サイトをその上で動かせるよう試行錯誤しています。

前置き

私の主サイトは古くからPHPで動かしていて(初期の頃はLegacy ASPで動かしていたこともありました)、GAEでPHPがサポートされてからいつか移植して動かしてみたい。と思っていたのですが、ベースとしていたフレームワークが古く、composerも使用していない状態のものだったため、最近になり、重い腰を上げてLaravelに置き換えるまでなかなか移植できずにいました。

GAEで開発していると、自分で好き勝手にモジュールをインストールして動かしていた環境と異なり、ある程度の制約を受けつつその中で作るようになります。幸いにも既存機能については代替の外部APIなどに置き換える事でほとんど移植可能になりそうとなりました。

さくらのVPSを使いながら行ったウェブサイトの3つの負荷対策

久しぶりのブログ更新になります。
色々と書きたいこともあったけど、サボってます。(継続中)

少し前の日記に書いたのですが、このブログおよび僕が公開しているいくつかのウェブページについては、基本的にさくらのVPS 512を利用しています。
512は最低プラン(初期からあるプラン)でメモリが512MBしかない環境です。そんな環境なのもあり、以前にTwitterのアカウントが商標権問題で変更せざるを得なくなった話を書いた際に、ありがたい事(?)に/.Jに取り上げられたり、はてブも300近いブックマークをもらったりするほどのアクセスとなりました。

当時、記事を公開した日の朝には、はてブやTwitterなどで話題に上げられ、アクセス増からサーバが高負荷状態となり、記事の閲覧が出来ない状態が続いていたりもしました。このときはmysqlやApacheの再起動を繰り返してみたりするも、あまり効果を発揮できず、最終的にはサーバを再起動させたように覚えています。

この時以降、こんなしょぼくれた個人サイトやブログでも、ある程度はサーバ負荷対策やキャッシュをしっかりしておかなければなと思い、いくつか対策を行ってきました。
これはサイトを運営する上で、サーバやサービスを落としてはいけないという下手な使命感のようなのもありますが、自分自身ウェブ系エンジニアを名乗るとして、それだけのことが出来ていないことが恥ずかしくもあったためです。


長い前置きとなりましたが、そんな僕が現在まで行ってきた大量アクセス時の負荷対策をいくつか挙げていきます。



Wordpressのキャッシュプラグインのインストール
WP Super Cache


おいおい、そこかよというツッコミも聞こえてきそうですが、当時、僕はこのプラグインを入れたつもりだったのですが、実際には入っていない状態となっていました。
そのため、記事のリクエスト時に毎回データベースに接続が行われ、データベースへの接続も、要求処理も原因し、結果過負荷となり、ページを表示出来ない状況に陥っていました。
その後このプラグインを正しくインストールして設定することで、ページのキャッシュが有効化され、大量アクセスが発生してもデータベースへの負荷をかけることなくページを表示できる様になりました。
なので、このプラグイン一つをとっても大量アクセスに対しては劇的に変わるため、これはWordpressでサイトやブログを運営するにはほぼ必須だなと実感しました。


Apacheからnginxへ
nginx
php-fpm

今まではApache + phpモジュールという、ウェブサイト構築ではごくごく一般的な構成を取ってきました。しかし、この構成は設定も楽で、管理もしやすく、TIPSも大量にあるため運用するにはとても最適なのですが、大規模アクセスとなるとApacheのメモリ消費量や負荷が目立つようになってきます。また、Apacheの中にphpへの処理が組み込まれるような形となる(具体的には異なりますが)ため、php側の処理にApacheが引きずられることとなり、phpの処理が関係ない部分まで応答が返せなくなるケースにまで至ることがあります。

そこで、メモリやCPUリソースの消費量も少なく、高速にリクエストされたコンテンツを処理して配信することができると話題のnginxを導入してみました。
これを導入することでメリットがあるのは、複数のウェブサーバとアプリケーションサーバで構築された中規模以上の環境でのリバースプロキシとしての利用や、画像などの静的コンテンツを大量に配信するサービスだと思います。

普通のウェブサービスの場合は、運用のし易さの面からも、ある程度の規模なら今までのApache + phpでもなんら問題ないと思います。
僕がこれを導入したのは、負荷対策と言うよりどちらかというと、最近のCGM系コンテンツやUGC系サービスサイトでよく見かけるようになってきたと言う技術的な流行が気になったというところと、個人的な実験的な要素が濃いです。

このnginxはApacheに比べて格段に動作が軽いのと、phpなどのスクリプトはfastcgiとして基本的に切り離していること、細かなところでカスタマイズができること、機能的にもApacheにはない、高速配信のためのキャッシュの仕組みがいくつか設けられていることなどのメリットもあります。
ただし、現時点ではまだまだ新しいサーバで、日本国内における運用系の情報は圧倒的に少ないですが、開発も活発で、また後発のプロダクトと言うこともあり、先発のApacheをはじめとしたサーバのいいところを取り入れて行っていると思い、今後も期待が出来そうかなと思います。
こちらの機能や導入についての詳細は、いくつかのブログで既に書かれているので、そちらを参照してみてください。


CDNの利用
CloudFlare

僕はCDNと聞くと、Akamaiなどを利用した、それこそ大規模なサイトでのコンテンツ配信をイメージしていたのですが、そのCDNを個人のウェブサイトにも簡単に、しかも無料で導入することができました。それがこのCloudFlareです。

CloudFlareは、ウェブサイトを丸ごと、cloudflareが持つCDN上にキャッシュとして持ち、配信してくれます。
簡単なイメージ的には、キャッシュプロキシが入る感じです。

これの導入方法は少々敷居が高いものの、至って簡単で、CloudFlareでアカウントを登録した後、サイトのドメインのDNSをCloudFlareの提供するDNSサーバに切り替え、DNSの設定をするだけ。簡単です。

メリットとしては、

  • キャッシュプロキシとなってくれるため、サイトに一切手を入れず、丸々をキャッシュしてくれるため、導入が楽

  • 高機能なDNS機能もフリーで提供される

  • ほぼ静的なページを大量のアクセスを捌くのに、追加サーバ導入コストを抑えられる

  • 簡易アクセス統計機能付き(リクエスト回数など)

  • キャッシュプロキシだけど、Google Analyticsなどの外部アクセス解析機能などにもしっかり対応

  • リクエストを弾きたいIPなども制御できる

  • これらがすべて無料(ただしある程度以上の規模は利用不可?)

といったところ。
上記を管理するコントロールパネルのUIのよく出来ていて、簡単かつ気持ちよく設定できます。
ぶっちゃけ、CDN機能を使わずに、高機能なDNS機能だけ使わせてもらうことも可能。最近対応していましたが、以前はVALUE-DOMAINのフリーDNSではIPv6には対応していませんでしたが、CloudFlareではIPv6にも対応されていました。

逆にデメリットとしては、

  • ドメイン単位での導入が前提なので、そもそもドメインを持っていないと利用できない。

  • ドメインのネームサーバ変更を必要とするため、サブドメイン毎に複数サービス運用している場合に、一部のサービスだけ利用を試してみるといった利用がしづらい(設定すれば可能だが…)。

  • サーバプログラム内でユーザのIPアドレス毎に制御を行っている場合、そのまま利用できない(一部参照先ヘッダの変更など、プログラムの書き換えが必要)

といったあたりでしょうか。


ちなみに、自分のサーバへのリクエストログには、CloudFlare経由の場合、すべて米国にあるCloudFlareのIPからの接続が残ります。
余談ですが、CloudFlareのキャッシュプロキシサーバは
varnishを利用している最近はnginxベースのものになったようです。
(コメントをいただきましたたくさんありがとうございます)




以上、3つとなります。他にも出来ることはあると思いますが、まずはこれでひとまずと思っています。
というのもあれ以降、大量のアクセスが舞い込むようなことが無いため、これでどれほどの効果が上がるかがまだ検証できていない状態です。


#本当は別のことを書こうと思っていたのに、気付いたらまったく違う内容で書き上げてしまっていたというオチ。

PHPで相対パスから絶対URL(URI)を作成する

HTMLページをパースしてURLを取り出す処理を書いていたのですが、ページ内のリンクなどが全部絶対URLで記述されていれば非常に楽なのですが、現実としてそうでもなく、ページによっては相対パスで書かれていたりして、正規表現で偏にリンクからURLを抜き出すだけではうまくできませんでした。

そこで少しググってみたら

PHPで相対パスから絶対URL(URI)を作成する|PHPプログラムメモ|プログラムメモ

という記事を発見!おぉ、これは便利!
と思って使わせてもらおうと思ったのですが、いくつかテストしてみて、相対パス処理で不備があるなーと思ったところがあったのでちょっと改良させてもらいました。

37~38行目は正直いらない気がしたのですが、 PHP 5.3 のCLIでWindows上でテストした際に、なぜか \/ (アルファベットのVではなく、\/ ) で出力されたのが気になったので、無駄かもしれないけどあえて記述。
あと $parse の初期化もここまでする必要ないけど、念のためNotice対策を…w

相対パスから絶対URLする関数





テストケース


ベースURL
http://example.com/path/to/url

相対パス
  • /
  • /index.html
  • /foo/bar/baz/
  • ./foo/bar/baz
  • ../../../foo/bar/baz/index.html
  • foo/bar/baz.html
  • foo/bar/baz/../index.html

結果


こんな感じ

http://example.com/
http://example.com/index.html
http://example.com/foo/bar/baz/
http://example.com/path/to/foo/bar/baz
http://example.com/foo/bar/baz/index.html
http://example.com/path/to/foo/bar/baz.html
http://example.com/path/to/foo/bar/index.html

PreparedなINSERT文を簡単に作る方法

PHPでWebアプリケーションなどを開発していて、SQL文を発行する際に、セキュア面や利便性などから、ADODBPDOなどを用いて、Prepared Statementを使うSQLを書くこともあると思います。

その際、特にINSERT文などはカラムの数だけVALUESの中に ? が並ぶことになるかと思います。


-- 例:
INSERT INTO
`persons` (`id`, `name`, 'age`, `birthday`, `mailaddress`, `phone`, `zipcode`, `address`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);


このとき、 ? がひたすら並んでいるだけとなると非常に見づらく、INSERTする情報が多くなってくると、指定したカラム数と VALUESの ? の数が一致せず、

Number of variables doesn't match number of parameters in prepared statement

といったエラーに遭遇したことが一度はあるかと思います。
特に、プログラムの改修をする際などは、カラム名だけ追加して、うっかり VALUESの ? だけ追加のし忘れなどをしてしまうことなんかも。。。

そこで、Prepared Statementを作る際のINSERT文のSQLを簡単で分かりやすくしたいと思います。


< ?php
$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

try {
$db = new PDO($dsn, $user, $password);

// INSET対象となるカラム名を指定
$columns = array(
'id',
'name',
'age',
'birthday',
'mailaddress',
'phone',
'zipcode',
'address',
);

$binds = array(
1 => 'anon',
2 => 'anon',
3 => '25',
4 => '1984-05-02',
5 => 'root@example.com',
6 => '03-xxxx-xxxx',
7 => 'xxxxxx',
8 => 'Tokyo',
);

$sql = 'INSERT INTO persons '
. implode(', ', $columns)
. ') VALUES ('
. implode(', ', array_fill(0, count($columns), '?'))
. ')'
;

/*
// またはテーブル名やカラム名を明示的にクオートする場合はこちら
$sql = 'INSERT INTO `persons` '
. '`' . implode('`, `', $columns) . '`'
. ') VALUES ('
. implode(', ', array_fill(0, count($columns), '?'))
;
*/

$stmt = $db->prepare($sql);

foreach($binds as $key => $value){
$stmt->bindValue($key, $value);
}

return $stmt->execute();

} catch (Exception $e) {
error_log('[' . get_class($e) . '] ' . $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine());
}


要点は、 $columnsという配列にカラム名を配列で持たせ、Prepared INSERT文を発行する際に、implode()関数でそのカラム名をカンマ区切りで連結、VALUESの ? は array_fill()関数で、カラム名の配列の値の数だけ ? で埋めた配列を作成し、さらにそれをimplode()関数で連結していく。

というだけです。

バインドの個所は今回手抜きにしてしまいました。
本当はバインドも同じようにもう少し見直せばもっと分かりやすく、簡潔にできると思うのですが、
いい書き方が浮かびませんでした。。。

php の memory_limit の制限

ネタメモ:
php の memory_limit は K、M、Gの単位で設定できる。
メモリをいくらか使う処理の中で、処理に余裕を持たせるため、memory_limit を 2G として設定した。
2G にした理由は ini_set('memory_limit', '1G'); と指定したにもかかわらず、ini_set() の設定が有効に働かず、デフォルトの 128MB で動作していたため。

2G に指定した際の php の設定情報では
$ php -i | grep memory_limit
memory_limit => 2G => 2G

となっていた。
プログラムを実行しようとすると
PHP Fatal error: Allowed memory size of 262144 bytes exhausted at /usr/local/src/php-5.2.6/Zend/zend_opcode.c:48 (tried to allocate 311296 bytes) in /home/user/bin/foo.php on line 476
で止まる。
プログラムの476行目を見ても何の変哲もないただの処理。 if( $value === FALSE) といった処理。

仕舞いには
$ php -l /home/user/bin/foo.php
を実行しただけで
Error parsing /home/user/bin/foo.php
を返し、エラーログには同様のエラーを表示していた。

現象が掴めないままいると、memory_limit に指定できるメモリ上限は 1G までであることが判明。
それ以上を指定しても、phpのシステム内で決定されている最小メモリ数(256K)に自動的に割り当てられるということらしい。

ちなみに 使用していた PHP のバージョンは PHP 5.2.6 (cli)

簡易クラスローダ

車輪の再発明になっている可能性な気がしますが、PHP4で動作する、動的な簡易クラスローダを作成してみました。

大まかにはZend FrameworkのZend_Loaderを参考にしていますが、基本的に1から書き直していています。
<?php
/**
* 簡易クラスローダ
* for PHP4
*
* @author anon &lt;anon@anoncom.net&gt;
*/
class ClassLoader
{
function ClassLoader() {}

/**
* 指定されたクラスを読み込み、インスタンスを返します。
*
* @param string $class クラス名
* @param string|NULL クラスディレクトリ
* @param boolean $once requireでファイルを読み込む際に、require_onceで読み込むか否か
* @param array|NULL $param インスタンス生成時に引き渡す引数
* @param string|NULL $loaderMethod インスタンス生成時に呼び出される、コンストラクタ以外のメソッド名(Singletonパターンでの呼び出しの場合など)
*/
function loadClass($class, $dir = NULL, $once = TRUE, $params = NULL, $loaderMethod = NULL)
{
if(is_null($dir)){
$dirs = explode(':', ini_get('include_path'));
$classfile_exists = false;
foreach($dirs as $dir){
$filepath = $dir . '/' . $class . '.php';
if(Froute_Loader::isFileAvailable($filepath)){
$classfile_exists = true;
break;
}

}
if(!$classfile_exists){
trigger_error('could not load class, causes class file"' . $class . '" is not found.', E_USER_WARNING);
}
}else{
$filepath = $dir . '/' . $class . '.php';
if(!ClassLoader::isFileAvailable($filepath)){
trigger_error('could not load class, causes class file "' . $filepath . '" is not found.', E_USER_ERROR);
}
}

if($once){
require_once $filepath;
}else{
require $filepath;
}

if(!class_exists($class)){
trigger_error('could not load class, causes undefined class name "' . $class . '".', E_USER_ERROR);
}

if(!is_null($loaderMethod)){
if(!method_exists($loaderMethod)){
trigger_error('could not load class, causes undefined function name "' . $class . '".', E_USER_ERROR);
}
if(is_null($params)){
$instance = call_user_func(array($class, $loaderMethod));
}else{
$instance = call_user_func(array($class, $loaderMethod), $params);
}
}else{
/*
if(!function_exists($class)){
trigger_error('could not load class, \'causes undefined function name "' . $class . '".', E_USER_ERROR);
}
*/
if(is_null($params)){
$instance = new $class;
}else{
$instance = new call_user_func($class, $params);
}
}
return $instance;
}

/**
* ファイルが存在するか確認します
*
* @param string $filename
* @return bool
*/
function isFileAvailable($filename)
{
if(file_exists($filename) &amp;&amp; is_readable($filename)){
return true;
}else{
return false;
}
}
}



使い方は以下の通り

接続先のサーバが応答するか判別する

PHPのプログラム中で、別のサーバに接続する際、タイムアウトを適切に設定していない場合や、データ取得時にタイムアウトを設定できない場合もあるかと思います。そんなときに、まず先に接続先が生存しているかを確認しておくと、エラー処理の判定もしやすくなると思います。

そこで、接続を開始する前に事前に接続先のサーバに接続を試行してみます。

※PHP5.0以前の環境ではこのまま実行すると、throwの部分で構文エラーになる可能性があります。その場合はバージョン判定部分とthrow new Exceptionの部分を取り除いてください。



使い方は以下の通り

PHPでの簡易文字列の一致の判定

PHPで、ある文字列の中に、特定の文字列が含まれているかを判定する際、完全一致なら、比較演算子 === で比較すればすぐですが、それ以外となると通常の比較演算では処理できないので、ereg()関数やpreg()関数などで処理することも多いと思います。

ただ、前方一致や後方一致、部分一致を行いたい場合は、それだけに正規表現を使うのもなんだかコストが高い。もっとシンプルに実装できないものか・・・。

例えば、JavaやC#でいうところのStringクラスのstartsWith()やendsWith()のようなことが出来れば・・・

という訳で、PHPで前方一致、後方一致、部分一致の比較を簡単に行う関数を作ってみました。




実行結果

$ php -q test.php

SIMPLE STRING PATTERN MATCHING TEST
***********************************
mfsmax.docomo.ne.jp
startsWith: false
endsWith: true
matchesIn: true
-----------------------
lsean.ezweb.ne.jp
startsWith: false
endsWith: true
matchesIn: true
-----------------------
mx.softbank.ne.jp
startsWith: false
endsWith: true
matchesIn: true
-----------------------
mail.disney.ne.jp
startsWith: false
endsWith: true
matchesIn: true
-----------------------
mx.mailsv.softbank.jp
startsWith: false
endsWith: false
matchesIn: false
-----------------------
mail2.pdx.ne.jp
startsWith: false
endsWith: true
matchesIn: true
-----------------------
mailmlb.emnet.ne.jp
startsWith: false
endsWith: true
matchesIn: true
-----------------------
ne.jp
startsWith: false
endsWith: false
matchesIn: false
-----------------------
.ne.jp
startsWith: true
endsWith: true
matchesIn: true
-----------------------
ne.jp.ne.jp
startsWith: false
endsWith: true
matchesIn: true
-----------------------
foo.ne.jp.bar.net
startsWith: false
endsWith: false
matchesIn: true
-----------------------

最近作ったアプリの話

先日、コナミ社の提供している コナステ のダウンロードコンテンツゲームを1クリックで起動できるアプリを作り、公開した。 Ks Game Launcher  ( Github ) 作った理由として、インストール時に作成されたショートカットをクリックするとブラウザが起動し、ログインし...