PHPによる型変換

PHPは==で値を比較すると、必要に応じて型の変換が起こります。
型の変換が起こった結果、本当はTRUEとなって欲しいのに、FALSEになったり、本当はFALSEになって欲しいのにTRUEになったりします。
したがって、PHPで値を比較する場合は、==ではなく===、!=ではなく===を使用すべきです。

そんなことを知らない何年も前の私は普通に==や!=を使用しています。
では===や!==に書き直せばいいかというと、単純にそうもいかず暗黙の型変換に頼っている部分があって、ちゃんと考えて書き直していかないとおかしな動きをすることもあり放置しています。

今日は、そんな放置していたスクリプトをで起きた話題です。

最近、エロゲー批評空間の携帯電話Verを作りました。
ご要望板で「 エロゲー批評空間の携帯電話Ver において長文感想のあるコメントに(GiveUP)が表示されている」とのご指摘がありました。

※このように条件を明確にしてバグをご指摘頂けると本当に助かります。自分でバグを引く条件を見つけるのはつらい場合が…そもそも、ちゃんとテストししろということなのですが…

スクリプトを見ると…一見大丈夫でした。

function toukei_kansuu_comment_hitokoto($i,$tokuten,$gid,$gamename,$bid,$brandname,$memo,$oid,$netabare,$hitokoto,$tourokubi,$uid,$giveup='f'){

~省略 ~

    if( $giveup == 't' ){
        print "<span style=\"font-weight: bold;\"><b>(GiveUp)</b></span>";
    }

~省略 ~

 }

な感じで、$giveupに「t」が入ったときだけgiveupと表示します。
$giveupには、データベースから取得してきた値が入ります。
データベースから  giveup に関する値を取得すると、「t」か「f」が入ってきます。
ですので、$giveupには「t」か「f」しか入ってこないはずなのですが、if文の前の$giveupの値を見ると「0」が入っていました…

「t」か「f」しかはいってこないはずの$giveupになぜ0が入ってくるのか?はおいておいて、なぜ0 == 't' がTRUEになるのだろう?と思いました。

PHP: PHP 型の比較表 - Manual

を見ますと、==による緩やかな比較の表に"php"と0を比較するとTRUEと書いてありました。
文字列phpを数値に変換すると0になります。

ちなみに、いろんなところに書いてありますが、文字列3abcを数値に変換すると3になります。

さて、そもそも  $giveupになぜ0が入っているのか?を調べました。
toukei_kansuu_comment_hitokoto関数は、データベースからとってきた値を渡して表示用に整形する関数です。
呼び出し側の  toukei_kansuu_comment_hitokotoは$giveupの部分に何を渡しているかと思ったら、0を渡していました…、原因はこれです。

toukei_kansuu_comment_hitokoto($i,$tokuten,$gid,$gamename,$bid,$brandname,$memo,$oid,$netabare,$hitokoto,$tourokubi,$uid,0,$giveup)
のように渡していました。
本当は、toukei_kansuu_comment_hitokotoの$giveupの部分が、function toukei_kansuu_comment_hitokotoの$giveupにいかないといけないと思いました。

スクリプトの変更履歴を追ったところ、もともとのtoukei_kansuu_comment_hitokoto関数は  toukei_kansuu_comment_hitokoto($i,$tokuten,$gid,$gamename,$bid,$brandname,$memo,$oid,$netabare,$hitokoto,$tourokubi,$uid,$time_short=0,$giveup='f')でした。

ここで、PHPって、呼び出し側の引数と呼び出される側の引数が違っていてもいいの?と思いました。

PHP: 関数の引数 - Manual

OKでした。

<?php
function foo()
{
    $numargs = func_num_args();
    echo "引数の数: $numargs\n";
}
foo(1, 2, 3);
?>

とやると、3と表示されます。
ちなみに、

function foo(  $a )
{
    $numargs = func_num_args();
    echo "引数の数: $numargs\n";

    ecoo $a;
}
foo(1, 2, 3);
?>

とすると、   ecoo $aの部分は当たり前ですが1と表示されます。

そもそも関数の中で、渡ってきた値が妥当かどうか…今回は、$givupの値には、't'か'f'しかこないはずなのに、0が入っていたけど、チェックしていなかった…のも原因です。
妥当性はチェックしましょう…

まとめ

  • PHPでの比較は===と!==を使おう
  • PHPには可変長引数がある
  • 書いてて面倒だなあと思うことがあったり、関数に渡ってくる値の種類が増えたときに関数の修正を忘れて「おかしな値が入っています」と言われることもあるけど、渡ってくる値はちゃんとチェックしよう 

ソフトウェアRAIDの復旧

ErogameScapeのサーバーはソフトウェアRAIDを組んでいます。
RAID1です。
1つHDDが壊れてもサービスを継続でき…ると思っていたのですが、実際はそうではなく、 止まります。
止まり方としては「telnetできなくなり、リセットまたは電源のOFF/ONをしなければいけない」状況になります。

 → ハードウェアRAIDだとどうなんでしょうか…

HDDの一時的障害でも、リセットまたは電源のOFF/ONは必要で、立ち上がると勝手に同期がはじまります。
HDDの一時的障害ではなく、本当に障害が発生している場合は、立ち上がってきません。
立ち上がってきても良さそうなのですが、今まで2回HDDが故障しているのですが、2回とも立ち上がってきませんでした。

立ち上がってこない場合、被疑のHDDを切り離して電源をONすると立ち上がってきます。
被疑のHDDをもう一度接続すると、立ち上がってきません。

※ErogameScapeのサーバーはHDD1台×2で運用していまして、GRUBがインストールされています。
 もしかしたらGRUBをインストールしてないHDDだったら、被疑のHDDを切り離さなくても立ち上がってこれたり、HDDが故障してもサービスを継続できる…かもしれません。どうなんでしょうか。


被疑のHDDを交換したら、
  1. fdiskで領域を確保する
  2. mdadmコマンドで同期させる
  3. grubをインストールする
という手順を踏みます。
googleで検索すると、手順の中で「壊れたHDDに故障したというマークをつけて、切り離す」ということをしているものもありますが、多分、オンラインでHDDを交換できるサーバーの場合の手順な気がします。

実際の手順は
LinuxMania: ソフトウェアRAID1 障害時の復旧手順(Fedora,CentOS)
の通りです。

いろいろなソフトウェアRAIDの構築の手順のHPのfdiskの結果を見ると
[root@linux ~]# fdisk -l /dev/sdb
 Device   Boot  Start   End     Block    Id  System
/dev/sdb1 *         1     13      104391  fd  Linux raid autodetect
/dev/sdb2         14    274     2096482+ fd  Linux raid autodetect  
/dev/sdb3        275   1566    10377990  fd  Linux raid autodetect 
のようになっています。 
sdb1が/boot、sdb2はSWAP、sdb3が他の領域という設定です。

SWAP領域はRAIDにする必要はないので、本当はIdが82のSystemが「Linux スワップ / Solaris」とした方がいいと思います。スワップ領域はミラーリングされている必要がないので、わさわざ負荷のかかるRAIDにする意味がないから…です。

と、どこかのHPに書いてあって、そうしています。

あと、もう一点。
fdiskをするときに、/bootからブート可能とするため、「a」でブート可能フラグをつけていますが、GRUBを使う場合、ブート可能フラグをつけていなくてもちゃんとブートできます。

実際、ErogameScapeのサーバーはブートフラグがついていなくても起動できていて「あれ?なんで起動できているのだっけ?」と調べたら以下のHPにいきつきました。

GRUBであればブートフラグがついていなくてもブートできます。

もし、ソフトウェアRAIDを使ってみたい、しかも多数の人に使われるサーバーで使いたいという場合は注意することがあります。
  1. mdadmコマンドで同期させる際に、滅茶苦茶時間がかかります。
  2. mdadmコマンドで同期させる際に、とても負荷がかかります。
1.については、ErogameScapeの環境で1TBのHDDで12時間かかりました。
サーバーとして運用しながらの値です。
サーバーとして運用しないでいればもっと早い気がしますが、それでもうん時間単位かかります。

2.については、ロードアベレージで1くらい食います。
ErogameScapeのサーバーは資源に余裕がありまくりですので、ロードアベレージで1食っても全然大丈夫ですが、シビアな使い方をしている場合は、サーバーとして運用しないで同期させるのがいいと思います。

これは実際やってみないと分からないことで、何年か前にソフトウェアRAIDを組もうと思ったときに、いろんなサイトを見たのですが「同期に時間がかかるから注意だ」「同期は負荷がかかるから注意だ」と書いてなかったので気がつきませんでした。

また、HDDは案外壊れますが、そんなにも壊れないとも言えますので、壊れた時のことを考えてちゃんと復旧のマニュアルを自分用に作っておくのがいいです。

確かにgoogleで検索するとそれなりにヒットするのですが、自分でやってみると「あれ?」と思うことがよくあります。
例えば、HPによっては「マスターのHDDが故障した場合は、スレーブのHDDをマスターのHDDが刺さっていたところに差し替える」と書いてありまして、これは…多分GRUBをスレーブのHDDにコピーしていない場合のことだと思うのですが、そういった「自分の環境と微妙に違う」ということがありますので、ぜひ、ちゃんと故障したときのことを考えてマニュアルを作っておいた方がいいです。

※ちなみに私は紙でもっていてクリアファイルに挟んで保管しています。

複数IDを取得してデータを入力しているIDの判別

ご要望板にて
>今月に入ってからお気に入りユーザーの登録数が増えています。
>私も最初は喜んでましたが、どうにも急すぎて何となく不審な感じを受けています。
>もし同一IPからの登録があれば、制限をお願いできないでしょうか。
とのご要望があったため、調査いたしました。

「 同一IPからの登録があれば」とのことですがIPアドレスは共有資源ですので、たとえ同一IPアドレスであっても別々の方が使っていることは往々にしてあります。

例えばドコモの携帯からのアクセスは
iモードセンタの各種情報
の通りで、誰しもがここに書いてあるアドレスからアクセスしてくるため、あるユーザーIDでアクセスがあったときのIPアドレスと、別のあるユーザーIDでアクセスがあったときにIPアドレスが同じだったからといって、同じ人がユーザーIDを2つ取得しているわけではありません。

大学からのアクセスも、大学には大抵インターネットに出るためのプロキシがあって、大学内のユーザーさんは、ErpgameScapeにそのプロキシ経由でアクセスしてくるため、Aというユーザーさんも、Bというユーザーさんも、一緒のIPアドレスでアクセスしてくることが多いです。

プロバイダからのアクセスも、IPアドレスはプロバイダの中で使い回しますので(固定でIPを払い出すサービスもありますが、通常の用途では固定IPを選択することはないと思います。高いですし。)、たまたまAというユーザーさんも、Bというユーザーさんも、一緒のIPアドレスでアクセスするということがあります。

ある人が複数のIDを取得しているか否かを見分ける手段として、IPアドレスだけでは不十分です。
他にサーバー側で取得できるのは

  • どんなブラウザを使っているか
  • COOKIE

くらいです。
ブラウザは詐称できますし、COOKIEも詐称できます。
IPアドレスも…世界にいっぱいあるプロキシを使えば詐称…とは言わないのかもしれませんが、自分のIPアドレスを隠すことはできます。

ですので、もう、どのIDが複数IDを取得してつけられたものか?というのは、入力されたデータを見て、総合的に判断するしかないです。
そのユーザーIDのアクセスログを見て、どんなゲームに得点をつけているかを見て…

私にとって複数IDを取得しているのか否かというのを判断するのは、相当きついのです。

「私にとって」はきついのですが、ご要望板でご指摘を書いてくださるユーザーさんには、そんなにきつくないことなんだろうなあと思っています。

今回、ご指摘頂いたユーザーさんは「明らかにおかしい」という気づきがあって、ご要望板に書いて頂いたのだと思っています。
その「明らかにおかしい」というのは、いつも見ている風景…といいましょうか、いつもと違う、何か違う、そんな感覚なんだと思います。

私は毎日サーバーの各種パラメータを見ていますが、いつもとグラフが違うと「何かあったな」と、何があったかわからないけど、何かがあったことは分かります。
そこから調べ始めると、やっぱり何かあるもんです。

あれ?何か違うな?と思ったら、その感覚を大事にして、それがもしErogameScapeのことでしたら、私に伝えて頂けるとうれしいです。

PHPの波ダッシュ対策

「から」と打って変換すると(多分)Windowsでは「~」と変換されます。
スマホで「から」と打って変換すると、「~」ではなく「〜」UTF-8の文字コードでいうと「E3809C」と変換されることがあります(REGZA Phone T-01C)がそうでした。
「〜」の文字をコピーしてUTF-8を使えないエディタにはりつけると文字化けします。

世界がUTF-8で統一されていれば問題ないのですが、SJISもEUC-JPも混在しているので、「〜」は「~」に変換してあげるのが良い方法だと思っています。

具体的には「〜」がPOSTされてきたら、「~」に変換してDBに保存するなりする、ことになります。
「〜」以外にも変換してあげた方が良い文字があります。

ErogameScapeはphpで動いているので、波ダッシュ対策をするために
波ダッシュ対策 ( ここにおいておくね.php>(´・▽・`) )
に掲載されているコードをコピペいたしました。


さて…、私は上記コードを「まあ、そのうちUTF-8にするから今のうちに組み込んでおこう」と思って、組み込んでしまいました。その後ユーザーさんから

Warning: pg_query(): Query failed: ERROR: invalid byte sequence for encoding "EUC_JP": 0xa30d HINT: This error can also happen if the byte sequence does not match the encoding expected by the server, which is controlled by "client_encoding".

というエラーがでるとご報告を頂きました。
このメッセージは、0xa30dという文字コードはEUC-JPにないからDBに書き込みせんでした、というメッセージです。
0xa30dという文字はEUC-JPにはありません。
というか、0xa30dの文字を普通に打ち込むことはない…と思いました。

いろいろ調べた結果「BG」という文字がDBに書き込めないことが分かりました。
(他にも書き込めない文字があった…と思うのですが、とりあえずBGが書き込めないことを一番先に発見しました)
「B」だけだとOK、「G」だけでもOK、「BG」だとNGでした。

Bの文字コードは「a3c2」
Gの文字コードは「a3c7」
BGは「a3c2a3c7」です。

波ダッシュ対策としてポンド記号を変換しています。

// ポンド記号(£)の変換
'/\xC2\xA3/' =>"\xEF\xBF\xA1",

この変換を「BG」が通ると
BGは「a3efbfa1c7」になっちゃいます。

上記エラーメッセージは「BG」のせいででたメッセージではないですが、まあ、どこかで変な変換をしちゃったのだろうと推測しています。


まさかこんなところでSJISの頃に悩んだ問題と同じ問題に出くわすとは…とそんな感じでした。

ftpの速度が遅い(1MB/s程度しかでない)

ErogameScapeではメインサーバーが死んだ際に、待機系のサーバーがリクエストを受け付けます。
メインサーバーを復旧させるために…、今はそんなことをしなくてもいい仕組みがちゃんとあるのですが、pg_dumpコマンドで待機系のデータをバックアップし、そのデータをメインサーバーに送って、pg_restoreでリストアしています。

その際、ついでに自分の作業パソコンにデータベースのバックアップを残しておくために自分のパソコンにftpでデータを転送しています。

その転送が最近とても遅く、1MB/s程度しかでませんでした。

結論から書くと、リンクのスピードが10Mb/sのFULLになっていました。




[root@erogamescape14 ap2]# ethtool eth0
Settings for eth0:
Supported ports: [ TP ]
Supported link modes: 10baseT/Half 10baseT/Full
100baseT/Half 100baseT/Full
1000baseT/Full
Supports auto-negotiation: Yes
Advertised link modes: 10baseT/Half 10baseT/Full
100baseT/Half 100baseT/Full
1000baseT/Full
Advertised pause frame use: No
Advertised auto-negotiation: Yes
Speed: 10Mb/s
Duplex: Full
Port: Twisted Pair
PHYAD: 0
Transceiver: internal
Auto-negotiation: on
MDI-X: Unknown
Supports Wake-on: pg
Wake-on: d
Current message level: 0x00000000 (0)
Link detected: yes




ああ、ネゴシエーションに失敗して10MのFULLでつながることもあるんだなあと思った次第でした。
ちなみに、ここまで辿りつくのに
 ・他のパソコン同士でftpで転送して待機系サーバーだけで起こってることを確認した
 ・ftpサーバーの設定が悪いのかなあと思ってftpの設定をメインサーバーと比べたが同じだった
 ・ftpサーバーをとりあえず再起動してみたけどかわらなかった
という手順を踏みました。
基本は低いレイヤからの確認だよなあ…と反省しました。
記事検索