Time Capsuleが壊れた

         ,. -‐'''''""¨¨¨ヽ
         (.___,,,... -ァァフ|          あ…ありのまま 今 起こった事を話すぜ!
          |i i|    }! }} //|
         |l、{   j} /,,ィ//|       『Time Capsuleが壊れたと思ったら
        i|:!ヾ、_ノ/ u {:}//ヘ        いつの間にか新しいTime Capsuleを買っていた』
        |リ u' }  ,ノ _,!V,ハ |
       /´fト、_{ル{,ィ'eラ , タ人        な… 何を言ってるのか わからねーと思うが
     /'   ヾ|宀| {´,)⌒`/ |<ヽトiゝ        おれも何をしたのかわからなかった…
    ,゙  / )ヽ iLレ  u' | | ヾlトハ〉
     |/_/  ハ !ニ⊇ '/:}  V:::::ヽ        頭がどうにかなりそうだった…
    // 二二二7'T'' /u' __ /:::::::/`ヽ
   /'´r -―一ァ‐゙T´ '"´ /::::/-‐  \    カルトだとか信者だとか
   / //   广¨´  /'   /:::::/´ ̄`ヽ ⌒ヽ    そんなチャチなもんじゃあ 断じてねえ
  ノ ' /  ノ:::::`ー-、___/::::://       ヽ  }
_/`丶 /:::::::::::::::::::::::::: ̄`ー-{:::...       イ  もっと恐ろしいものの片鱗を味わったぜ…

というわけで、2008年3月5日に買ったTime Capsule 500GBが壊れた。2年と2ヶ月の短い命であった。
ステータスランプがオレンジの点滅になっていたので、AirMacユーティリティを起動すると"過熱状態です"との表示。少し経つと"問題は解決しました"となり、また少し経つと過熱状態に。AirMacユーティリティから再起動を実行したら起動しなくなってしまった。

Apple Storeのレビューを見ると、これでもかというくらい同じ症状の人がみつかる。どうやら初期ロットは電源部品に問題があるらしい。

データが載ったままのHDDをそのままにしておくわけにはいかないので、とりあえず分解してみる。

分解するには底面のラバーを剥がし、その下にあるネジを外せば良いらしい。電源の劣化した電解コンデンサを交換すれば直るとの報告もあるので、直せた場合の事も考え、元に戻せるよう慎重に剥がしたい。



慎重に、剥がした…。


ネジを外すとフタは簡単に外れた。シールド加工された電源部分があるので、シールドを剥がしてみると劣化した電解コンデンサが出てきた。


底面のラバーは言うまでもなく、シールド部分は力を入れたらバリバリに割れるし、電解コンデンサは樹脂で固めてあるので、コンデンサだけを交換するのは相当大変そう。

ちなみにHDDはSeagateBarracuda ES (Appleロゴ入り)だった。



というわけで、懲りずにTime Capsule 1TBを購入。電源の問題は直っているらしい[要出典]。

APPLE Time Capsule 1TB MC343J/A

APPLE Time Capsule 1TB MC343J/A

追記
アップルのサポートに電話すると無償交換となる場合もあるらしい。
ただ、HDDごと回収されてしまうので、データの行方が気にかかる、疑り深い諸氏には受け入れ難い提案。もちろん、分解したら対応してくれない。

VAIO Xを買った

2009年末にVAIO Xを買った

Windows 7 Professional/Atom Z540 1.86GHz/SSD 128GB/WiMAX/Bluetooth/Lバッテリー


色はゴールド。ソニースタイルにて購入。
ソニースタイルはポイントとかSTARとかクーポンとか仕組みがよくわからない。
マウスとケースも欲しかったけど、本体と一緒に買うより、後で本体購入で獲得したポイントを使った方が良いみたいだ。(黒いマウスとケースならセット割引あるけどシルバーが欲しかった)

リカバリーディスクの作成には外付けDVD-Rドライブが必要。



13インチのMacBook(非ユニボディ)と大きさを比較。
このサイズで解像度は1366x768。バッテリー使用時の輝度だと文字が若干読みづらい。


キーボードを押した感触は、大きい電卓という感じ。
タイピングは、快適とまではいかなくとも辛かったりはしない。ただ、スペースキーは右手の親指で押しづらい。



ACアダプタの色はゴールドではなく黒だった。
本体側へのケーブルは横向きに出ているので、メガネプラグを外せば持ち運ぶ時かさばらない。



保管時は縦置き。
都合が良い事に、液晶を開いた時に本体を支えるため、液晶側の側面には足が付いている。アフォーダンス!(多分メーカーは推奨していません)



支えているのは無印良品のアクリルブックエンドにSOXを被せたもの。
アクリルとアルミは大体同じ硬度らしい。



SOX東京ミッドタウンIdea Digital Studioでクリスマス直前に購入。ご自宅用です。



ケースは純正のVGP-CKX2を購入。



結構固いので見た目以上に余裕は無い感じ。



引き続き固いので、しばらく慣らさないとフタが上手く閉じない。
フタの部分はマグネット式。


VGP-CKX2の重量は268g。

タッチパッドが常用するにはキツいので、Bluetoothマウスも持ち歩いている。VGP-BMS10が電池込みで90g。
キーボードと液晶の間に挟んで欲しいと書いてあった付属のクリーニングクロスは5g。

本体は約760gらしいので、+363gで1.1Kgを超えてしまってる。


やたらとカッチリしたケースが嫌な場合はBUILTのケースを使ってもいいかも。VAIO Xに合うサイズもきっと見つかる!

ちなみに、首都圏でBUILTが買えるお店は、知ってる限りでは以下。

ヨドバシカメラが一番品揃えが良いはず。

Tokyo TyrantによるHAなセッションストレージ 3 デュアルマスタ篇

デュアルマスタ構成にする基本的な方法についてはチュートリアルにもあるので省略。

期限切れレコードの削除はどちらで行うか

デュアルマスタにした場合、期限切れレコードの削除を行うttexpire.luaはどこで動かすのが良いか。
セッションデータの一貫性を保つためアクティブ-スタンバイ構成を想定しているので、アクティブ側でのみ動かすのが正統な選択になると思う。

しかし、そうするとスタンバイに切り替わった後にexpireの実行が行われなくなってしまう。今のところTokyo Tyrant(TT)は起動中にextpcオプションに相当する機能を操作する事はできないようだ。

解決策を2つ考えてみた

  • アクティブとスタンバイの両方でexpireを実行する
  • TT起動中にexpireの実行/停止を行えるようにする
アクティブとスタンバイの両方でexpireを実行する

両マスタでexpireを実行して問題なければ、上記の問題は簡単に解決する。

例えばMySQLレプリケーションの整合性が取れなくなるとレプリケーションが停止してしまうけど、調べたらTTの場合はまずい事(意訳)になったりはしないと書いてある。

Note that updating both of the masters at the same time may cause inconsistency of their databases. By default, the servers do not complain even if inconsistency is detected. The option `-rss' make them check the consistency and stop replication when inconsistency is detected.

http://1978th.net/tokyotyrant/spex.html#tutorial_replication

実際に試してみると、ログに

do_slave: detected inconsistency

というINFOレベルの出力があるものの、レプリケーションは継続されていた。*1

レプリケーションが停止しないのであれば両マスタでexpireを実行する方法で良さそうだ。
と思ったのだけど、スタンバイへのレプリケーションがある程度の時間遅延すると、期限が切れていないセッションがアクティブ側でのみ削除される可能性がある。
通常運用であればレプリケーションの遅延がそれほど大きくなる事は無いはず。でも、スタンバイを一時的に停止させたりした場合は再起動時には遅延が発生しているので、レプリケーションが追いつくまでの間が危険な時間になる。
通常時は両マスタでexpireを実行するにしても、起動中に実行/停止が行えるようにしておいた方が良さそうだ。

TT起動中にexpireの実行/停止を行えるようにする

TTの起動中にexpireの実行/停止を制御できれば、運用の手間は増えるものの、expireを動かしたいのに動かせなかったり、逆に動かして消してはいけないデータを消してしまう事は無くなる。

expireの動作を制御するため関数内で実行/停止を制御する値を参照するようにし、その値をtcrmgrのextサブコマンドで呼び出す関数内で変更できるようにしてみた。値の格納にはスタッシュを使用。

新しいttexpire.lua

TTEXPIRE_KEY = "ttexpire"
ENABLE_EXPIRE = "t"
DISABLE_EXPIRE = "f"

function expire()
   if not is_enabled_expire() then
      return
   end
   local args = {}
   local cdate = string.format("%d", _time())
   table.insert(args, "addcond\0x\0NUMLE\0" .. cdate)
   table.insert(args, "out")
   local res = _misc("search", args)
   if not res then
      _log("expiration was failed")
   end
end

function is_enabled_expire()
   return _stashget(TTEXPIRE_KEY) == ENABLE_EXPIRE
end

function ttexpire(key)
   local message = nil
   if key == "start" then
      _stashput(TTEXPIRE_KEY, ENABLE_EXPIRE)
      message = "starting 'expire'"
      _log(message)
   elseif key == "stop" then
      _stashput(TTEXPIRE_KEY, DISABLE_EXPIRE)
      message = "stopping 'expire'"
      _log(message)
   else
      if is_enabled_expire() then
         message = "'expire' is running"
      else
         message = "'expire' was stopped"
      end
   end
   return message
end

function _begin()
   _stashput(TTEXPIRE_KEY, DISABLE_EXPIRE)
end

function _end()
   _stashout(TTEXPIRE_KEY)
end

ttexpire関数を{start|stop|status}引数付きで呼び出すと、expireの実行開始/実行停止/状態確認が行える。

tcrmgr ext localhost ttexpire start
tcrmgr ext localhost ttexpire stop
tcrmgr ext localhost ttexpire status

これにより、多少手間はかかるものの、expireの実行を一時的に停止したり、アクティブでのみ実行されるように運用したりといった事ができるようになる。

更新ログ削除

デュアルマスタ構成では両マスタで更新ログ(ulog)を出力する事が必須になる。更新ログは放っておけば際限なく増え続けるので、適当なタイミングで削除する必要がある。

TTをulimオプション付きで起動すれば指定したサイズ(付近)で更新ログが分割される。今後レプリケーションに使用されない更新ログファイルはrmコマンドで削除しても構わないそうなので*2、現在使用中の更新ログファイル以外で、更新時刻が相手ホストのレプリケーション遅延時間(を現在時刻より引いた時刻)より前の更新ログを定期的に削除すれば良さそうだ。

遅延時間はそれぞれのマスタがレプリケーション相手に対して

tcrmgr inform -st host

を実行するとdelay行で得られる。

あとはfindコマンドのmminやmtimeオプションを使用して古い更新ログを削除すればdisk fullの憂き目は避けられる。

なお、スタンバイでexpireを定期実行していない場合、スタンバイ→アクティブでレプリケーションする内容が無いためアクティブの遅延時間が増加していってしまう。
この現象は、スタンバイで定期的に「何も削除しないoutコマンド」のようなデータに影響しない更新処理を実行すると回避できる。

ホストの復帰

アクティブからスタンバイに切り替える方法は華麗にスルー。

スタンバイへの切り替えがアクティブマスタの計画的な短期停止によるものであれば、expireの実行/停止に気をつけつつスタンバイ(元アクティブ)のTTを起動すればレプリケーションが再開されるので良いけれど、ハードウェア故障等によって新しいホストに交換された場合、そのままレプリケーションを再開する事はできない。

そういう時は一度その時点でのアクティブ環境のバックアップファイルを作成し、そのファイルをスタンバイに移して起動すれば良いらしい。

マニュアル(Setting Replication on Demand)によると、tcrmgrのcopyサブコマンドに次のシェルスクリプトを渡して実行すればデータベースファイルのバックアップが行えるそうだ。

ttbackup.sh

#! /bin/sh
srcpath="$1"
destpath="$1.$2"
rm -f "$destpath"
cp -f "$srcpath" "$destpath"
tcrmgr copy localhost '@/path/to/ttbackup.sh'

tcrmgrのcopyサブコマンドは、大雑把にはTT内部でデータベースファイルをロックしてコピーしてロックを解除する処理を呼び出しているみたいで、リモートから実行してもバックアップファイルが作成されるのはサーバ側になる。結構恐ろしい仕様。
また、渡すttbackup.shファイルのパスはTTから見たパスになる。TTはdmnオプション付きで起動した場合、/にchdirしているので絶対パスを渡さないと奇妙なエラーを見る事になる。

アクティブ側で作成したバックアップファイルをスタンバイに移し(多分インデックスのバックアップファイルはいらない)、ファイル名に含まれるタイムスタンプをrtsファイルに書き込み、もしあるのなら古いデータベースファイルや更新ログを全て削除してから起動すれば復帰できる。


おしまい

  1. 検討篇
  2. PHPから利用する篇
  3. デュアルマスタ篇

*1:なお、-rccオプション(ドキュメントの'-rss'は多分typo)を付けて起動した場合は、ドキュメントに書いてある通りERRORレベルのログを出力してレプリケーションが停止した。

*2:mixiのサポートコミュニティ書き込みより

Tokyo TyrantによるHAなセッションストレージ 2 PHPから利用する篇

PHPからセッションストレージとしてTokyo Tyrant (TT)を使用する場合、PHP用のTTクライアントライブラリが必要になるが、悲しい事に公式なライブラリはない。
幸い何種類かPHP用のライブラリが公開されているので、その中のどれかを使わせてもらおう。

ざっと調べたところ上の3種類が見つかった。他にもあるらしいけど見つけられなかった。

tokyotyrant_phpは作者の方が実験レベルと言っている事から採用を見合わせ、Pure PHPのNet_TokyoTyrantPECLphp-tokyo_tyrantを調べてみた。どちらも現在ベータ版。
PHPのNet_TokyoTyrantの方が取り扱いは楽だしモテるらしいけど、PECLphp-tokyo_tyrantの方がパフォーマンスが高いとの噂。しかも

session.save_handler = tokyo_tyrant

とすれば自前でセッションハンドラを書かなくても良いというおまけ付き。
これは助かると採用方向に傾いていたところで問題発見。
どうやらphp-tokyo_tyrant (0.1.0)はバイナリセーフではなく、データ内に\0が含まれているとそこでデータが終了してしまう。
PHPはオブジェクトをserializeすると結果に\0が含まれる事があるのでこれは困る。

Net_TokyoTyrantではこの問題は発生しないため、今回はNet_TokyoTyrantを採用する事にした。現在のリリース0.2.1-betaにはテーブルデータベース用のNet_TokyoTyrant_Tableが含まれていないので、trunkのr1158から取得している。

PHPセッションハンドラ

PHPのセッションハンドラはこんな感じ。

<?php
require_once 'Net/TokyoTyrant/Table.php';

class Net_TokyoTyrant_Table_Session extends Net_TokyoTyrant_Table
{
    public function __destruct()
    {
        session_write_close();
    }
}

class TokyoTyrant_Session
{
    const HOST = 'localhost';
    const PORT = 1978;
    const TIMEOUT = 5;
    const EXPIRE = 1440;

    const DATA_COLUMN_NAME = 'd';
    const EXPIRE_COLUMN_NAME = 'x';

    private static $tt = null;

    public static function connect()
    {
        $ttt = new Net_TokyoTyrant_Table_Session();
        $ttt->connect(self::HOST, self::PORT, self::TIMEOUT);
        $ttt->setTimeout(self::TIMEOUT);
        self::$tt = $ttt;
        return true;
    }
    public static function close()
    {
        if (self::$tt !== null) {
            self::$tt->close();
        }
        return true;
    }
    public static function put($key, $value)
    {
        if (self::$tt !== null) {
            return self::$tt->put($key, array(
                self::DATA_COLUMN_NAME => $value,
                self::EXPIRE_COLUMN_NAME => time() + self::EXPIRE
            ));
        }
        return false;
    }
    public static function get($key)
    {
        if (self::$tt !== null) {
            $result = self::$tt->get($key);
            if (is_array($result) && isset($result[self::DATA_COLUMN_NAME])) {
                return $result[self::DATA_COLUMN_NAME];
            }
        }
        return '';
    }
    public static function out($key)
    {
        if (self::$tt !== null) {
            return self::$tt->out($key);
        }
        return false;
    }
}

// {{{ Handler
function open($save_path, $session_name)
{
    return TokyoTyrant_Session::connect();
}
function close()
{
    return TokyoTyrant_Session::close();
}
function read($sess_id)
{
    return TokyoTyrant_Session::get($sess_id);
}
function write($sess_id, $sess_data)
{
    return TokyoTyrant_Session::put($sess_id, $sess_data);
}
function destroy($sess_id)
{
    return TokyoTyrant_Session::out($sess_id);
}
function gc($maxlifetime)
{
    return true;
}
session_set_save_handler('open', 'close', 'read', 'write', 'destroy', 'gc');
// }}}

PHP 5.0.5 以降、write ハンドラおよび close ハンドラはオブジェクトが破棄されたあとにコールされます。 そのため、セッション内でデストラクタを使用可能ですが、 ハンドラ内ではオブジェクトを使用できません。

PHP: session_set_save_handler - Manual

というわけなので、Net_TokyoTyrant_Tableを継承したクラスのデストラクタでsession_write_close()を呼び出すようにしている。

期限切れレコードの削除

TTで有効期限が切れたレコードを削除する方法はドキュメントに解説があるのでそのまま使わせていただく。

function expire()
   local args = {}
   local cdate = string.format("%d", _time())
   table.insert(args, "addcond\0x\0NUMLE\0" .. cdate)
   table.insert(args, "out")
   local res = _misc("search", args)
   if not res then
      _log("expiration was failed")
   end
end

この内容をttexpire.luaとして保存。

ttserver -dmn -port 1978 -pid pid -log log -ext ttexpire.lua -extpc expire 1 casket.tct#bnum=10000#idx=x:dec#dfunit=8

ttexpire.luaと定期的に呼び出す関数expireを指定してTTを起動すると、この例の場合1秒毎に期限切れレコードの削除処理が行われる。
なお、-dmn付きで起動する場合はファイルのパスを絶対パスで書いた方が良いみたいだ。

これでPHPのセッションをTTに保存できるようになった。

ベンチマーク

ここまでできたところで、主に同時接続数が増加した場合のスループットの変化を見るためベンチマークを取ってみた。
レコード数が増加した場合のベンチマークも取ってみたものの、どちらもほぼ一定のスループットを維持していたので省略。

サーバ側の環境は以下の通り。


事前に100000セッション分のレコードを作成し、作成された中のひとつのセッションIDを設定したリクエストを10000件、同時接続数を変えてabから発行した際の平均秒間リクエスト数を計測した。
セッションデータのサイズは概ね80バイト。

ApacheとTT又はMySQLが同居したサーバ(古いラップトップ)に対し、クライアント(MacBook)からリクエストを発行している。

MySQLを使用するハンドラの内容はPHPのセッション管理に使う箱選び 2とほぼ同じで、SET NAMESの部分をmysql_set_charset()に直している。
GCの起動確率は1/1000。

Apacheはpreforkで550プロセス起動した。

StartServers     550
MinSpareServers  550
MaxSpareServers  550
ServerLimit      600 
MaxClients       600

MySQLはこのような設定で起動した。

skip-locking
key_buffer = 64M
max_allowed_packet = 1M
table_cache = 64
sort_buffer_size = 512K
net_buffer_length = 8K
read_buffer_size = 1M
read_rnd_buffer_size = 512K
myisam_sort_buffer_size = 1M

max_connections = 500

innodb_data_file_path = ibdata1:50M:autoextend
innodb_buffer_pool_size = 256M
innodb_additional_mem_pool_size = 8M
innodb_log_file_size = 32M
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50

結果

同時接続数が10程度の場合はTTとMySQLの差はほとんどないものの、同時接続数の増加に伴いMySQLスループットだけが大幅に落ち込んでいる。
ただし、今回の方法では特定の1レコードのみを頻繁に更新する処理になっているので、実際の環境ではここまで極端な落ち込み方はしないと思う。とはいえTTの安定性が光る結果に。まさに多い日も安心。

追記
「思う」で終わらせてしまうのは良くないので、10000件の新規セッションを作る場合についても計測してみた。

落ち込みの幅は小さくなったものの、傾向は同じだった。
追記おわり


数値については環境が貧弱ゥな点を考慮すればなかなか良いのではないかと思う。

次はTTをデュアルマスタ化した状態でセッションストレージとして使う方法について考えたい。

つづく

  1. 検討篇
  2. PHPから利用する篇
  3. デュアルマスタ篇

Tokyo TyrantによるHAなセッションストレージ 1 検討篇

2年前にPHPのセッション管理に使う箱選び 4で、セッションストレージとしてはMySQLInnoDBを使用するのが良いと結論付けた。
当時は主にセッション数が増えていった場合のパフォーマンスについて調べて結論を出したものの、実際にMySQLをセッションストレージとして使用すると、さらに負荷が高くなった場合のパフォーマンスや可用性の部分にちらほら課題が見えてくる。
つまり、大規模なセッションストレージとして使うにはMySQLは高機能過ぎて重く、さらに冗長構成になっていても障害発生時の対応は手動が基本になってしまう。(自動化できないわけではないと思う)

MySQL :: MySQL 5.6 リファレンスマニュアル :: 17.3.6 フェイルオーバー中にマスターを切り替える


セッションデータの集中管理をやめる*1というのも手だけれど、それはそれで大変なので別のセッションストレージにする方向で検討してみようと思う。

要件は

  • 1000qps/1000コネクション以上の環境で動作可能
  • セッション数/接続数が増えてもスループットが低下しない
  • 有効期限の切れたセッションは自動で削除
  • 冗長化が可能
  • フェイルオーバーが自動
  • 任意のタイミングで停止せずにマスタの切り替えが行える
  • 書き込み直後に読み込みを行っても常に一貫した情報が取得できる
  • 構成がシンプル

が上記要件のいくつかを満たせそうだ。

repcached

  • デュアルマスタ構成可能
  • 非常に高速
  • 有効期限設定可能
  • memcachedなので揮発性
  • 使用できる容量は割り当てメモリまで

セッションストレージとして使われている話もよく聞くrepcached。memcached互換プロトコルなのでクライアントライブラリにも困らなそう。
しかし、オペレーションミスなどの"最悪な事態"を考慮すると、揮発性メモリだけにデータを置くのは怖い。キーを打つ手が震える。

Flare

  • マスタ スレーブ構成可能
  • スレーブをマスタに昇格可能
  • パーティショニング可能
  • 有効期限設定可能(らしい)
  • Tokyo Cabinetに格納
  • スケールアウト
  • 情報少なめ

FlareはファイルベースのTokyo Cabinetにデータを置く分散ストレージ。memcached互換プロトコル。データの保存を行う複数のノードと、ノードを管理するインデックスサーバで構成。インデックスサーバがノードを監視していて自動的にフェイルオーバーも行えるらしい。
機能は申し分無いものの、恐らく構成としてはマスタ、スレーブとインデックスサーバの3台が必要になり、またクライアント側にproxyノードを置いた方が良いみたいで、そうすると構成が複雑になってしまいうれしくない。
パーティショニングが必要になる規模になったら使ってみたい。

Tokyo Tyrant

  • デュアルマスタ構成可能
  • 有効期限は拡張スクリプトで実現
  • Tokyo Cabinetに格納
  • スケールアップ

Tokyo Tyrantは、データベースライブラリTokyo Cabinetのネットワークインターフェース。memcached互換、HTTPと独自のプロトコル。有効期限を使用するような場合は専用クライアントが必要。拡張スクリプトで機能追加を行える。マスタ スレーブ構成とデュアルマスタ構成が可能。構成は比較的シンプル
mixiではサーバ1台で10000qps/10000コネクションを処理しているらしい。

mixi engineer blog

というわけで、今回はタイトルからもわかる通り、要件を満たせる度合いが高そうなTokyo Tyrantにセッションデータ保存する方法を考えてみたい。

つづく

  1. 検討篇
  2. PHPから利用する篇
  3. デュアルマスタ篇

*1:RoRのように

SET NAMES のいらない生活

PHPRubyMySQLクライアントライブラリを清く正しく使う方法*1をまとめてみた。Shift_JISが来てもU+00A5が来ても大丈夫なはず。
汚れた部分を見つけた方はぜひご指摘を!

Version

PHP

mysql
$link = mysql_connect('localhost', 'user', 'password');
mysql_select_db('test', $link);
mysql_set_charset('utf8', $link);

$result = mysql_query(
    sprintf("SELECT id FROM tb WHERE name = '%s'",
            mysql_real_escape_string('あいうえお'))
);

if ($result) {
    while ($row = mysql_fetch_assoc($result)) {
        printf("%d\n", $row['id']);
    }

    mysql_free_result($result);
}

mysql_close($link);
mysqli

OOP:

$mysqli = new mysqli('localhost', 'user', 'password', 'test');
$mysqli->set_charset('utf8');

$stmt = $mysqli->prepare("SELECT id FROM tb WHERE name = ?");
$name = 'あいうえお';
$stmt->bind_param('s', $name);
$stmt->bind_result($id);

$stmt->execute();

while ($stmt->fetch()) {
    printf("%d\n", $id);
}
$stmt->close();

$mysqli->close();

手続き型:

$link = mysqli_connect('localhost', 'user', 'password', 'test');
mysqli_set_charset($link, 'utf8');

$stmt = mysqli_prepare($link, "SELECT id FROM tb WHERE name = ?");
$name = 'あいうえお';
mysqli_stmt_bind_param($stmt, 's', $name);
mysqli_stmt_bind_result($stmt, $id);

mysqli_stmt_execute($stmt);

while (mysqli_stmt_fetch($stmt)) {
    printf("%d\n", $id);
}
mysqli_stmt_close($stmt);

mysqli_close($link);

非prepared statements:

$mysqli = new mysqli('localhost', 'user', 'password', 'test');
$mysqli->set_charset('utf8');

$result = $mysqli->query(
    sprintf("SELECT id FROM tb WHERE name = '%s'",
            $mysqli->real_escape_string('あいうえお'))
);

if ($result) {
    while ($row = $result->fetch_assoc()) {
        printf("%d\n", $row['id']);
    }
    
    $result->close();
}

$mysqli->close();
PDO_MYSQL
$dbh = new PDO('mysql:host=localhost;dbname=test', 'user', 'password',
               array(
                     PDO::MYSQL_ATTR_READ_DEFAULT_FILE => '/etc/my.cnf',
                     PDO::MYSQL_ATTR_READ_DEFAULT_GROUP => 'pdo',
                     PDO::MYSQL_ATTR_DIRECT_QUERY => false  /* server-side prepared statements */
               ));
 
$stmt = $dbh->prepare("SELECT id FROM tb WHERE name = ?");
$stmt->bindValue(1, 'あいうえお', PDO::PARAM_STR);

$stmt->execute();

while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    printf("%d\n", $row['id']);
}

非prepared statements:

$dbh = new PDO('mysql:host=localhost;dbname=test', 'user', 'password',
               array(
                     PDO::MYSQL_ATTR_READ_DEFAULT_FILE => '/etc/my.cnf',
                     PDO::MYSQL_ATTR_READ_DEFAULT_GROUP => 'pdo',
                     PDO::MYSQL_ATTR_DIRECT_QUERY => true
               ));
 
$result = $dbh->query(sprintf("SELECT id FROM tb WHERE name = %s",
                              $dbh->quote('あいうえお', PDO::PARAM_STR)
));

while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
    printf("%d\n", $row['id']);
}

/etc/my.cnf

[pdo]
default-character-set = utf8

Ruby

MySQL/Ruby
my = Mysql.init
my.options(Mysql::SET_CHARSET_NAME, "utf8")
my.connect("localhost", "user", "password", "test")

st = my.prepare("SELECT id FROM tb WHERE name = ?")
st.execute("あいうえお")
st.each do |x|
  p x[0]
end
st.close

my.close

非prepared statements:

my = Mysql.init
my.options(Mysql::SET_CHARSET_NAME, "utf8")
my.connect("localhost", "user", "password", "test")

my.query(sprintf("SELECT id FROM tb WHERE name = '%s'",
         my.quote("あいうえお"))).each do |x|
  p x[0]
end

my.close

Webサイトがパスワードを可逆状態で保存しているかを見分ける10のポイント

  1. 秘密の質問に答えるとパスワードが表示される
  2. パスワードを忘れたときのフォームにメールアドレスを入力するとパスワードが書かれたメールが送られてくる
  3. ユーザーサポートに電話するとパスワードを読み上げてくれる
  4. ユーザーサポートにメールするとパスワードが書かれたメールが返ってくる
  5. パスワードが4桁の数字縛り
  6. パスワードの大文字小文字がなぜか区別されていない*1
  7. パスワードの最大長が妙に短く8文字とかになっている
  8. 最大長を超えたパスワードも受け入れてしまうがログインで使えるのは最大長に切り詰めたパスワード*2
  9. パスワードは暗号化して保管しますと書いてあるプライバシーポリシーがコピペ

*1:MySQLではBINARY属性を指定しないと比較時に大文字小文字を区別しない

*2:MySQLではカラムの最大長を超えた文字列のINSERTが失敗しない