1.1 はじめに |
多数のユーザが訪れるWEBサイトにおいて、会員と非会員の区別を行うユーザ認証システムは意外にやっかいです。
特にユーザ数が100万から1000万オーダーの巨大サイトは、毎秒1000以上のアクセスに耐えねばならず、全てのユーザ認証をユーザマスターテーブルを持つDBサーバで行うのは現実的ではありません。
すぐに思い付くのは、RACなどのクラスタを構築するか、レプリケーションで複数のDBサーバによる負荷分散を行うかですが、それぞれに大きな欠点があります。
ここでは、全く別のアプローチとして、ユーザ登録などの更新系処理はPostgreSQLサーバで行い、ユーザ認証(=検索)処理はメモリーDBのmemcachedを使う方式を検討します。
1.2 基本原理 |
1.3 プロトタイプの作成 |
1.3.1 ツール群の準備
実験にあたり、必要なツールをダウンロードしてインストールします。
注意: 実験後にpgmemcache+PL/pgSQLによるトリガー関数は使えないことが分かりますが、今は構わずに準備します。
以下のURLからバージョン8.2以上のPostgreSQLをダウンロードし、インストールしてください。
http://www.postgresql.org/download/
memcachedはlibeventを使うので、まずはじめにlibeventをインストールしてください。
http://www.monkey.org/~provos/libevent/
http://www.danga.com/memcached/download.bml
libmemcaheはC言語用のライブラリです。
http://people.freebsd.org/~seanc/libmemcache/
最後にPostgreSQL用のmemcachedライブラリ:pgmemcacheをインストールします。
http://pgfoundry.org/projects/pgmemcache/
SETUP: ====== The path below is the path for FreeBSD, it may be something different on your operating system. The path is shown above during the `sudo gmake install` step: look in the output above if you're having problems. [mydbname]=# BEGIN; [mydbname]=# \i /usr/local/share/postgresql/contrib/pgmemcache.sql [mydbname]=# COMMIT; It is often more convenient to specify a list of memcached servers to connect to in postgresql.conf, rather than calling memcache_server_add() in each new client connection. This can be done as follows: 1. Edit postgresql.conf 2. Append "pgmemcache" to preload_shared_libraries 3. Append "pgmemcache" to custom_variable_classes 4. Set the "pgmemcache.default_servers" custom GUC variable to a comma-separated list of 'host:port' pairs (the port is optional). |
1.3.2 テーブル定義
以下にユーザテーブルの例を示します。CREATE TABLE uid_list ( uid int primary key, /* ユーザID */ flag boolean DEFAULT true, /* このユーザデータが有効か無効か */ dat_int int, /* int型のデータ例 */ dat_varchar varchar(10), /* varchar型のデータ例 */ dat_text text, /* text型のデータ例 */ date1 timestamp, /* timestamp型のデータ例1:登録日時など */ date2 timestamp /* timestamp型のデータ例2:退会日時など */ ); |
ユーザデータの登録はINSERT、変更はUPDATEで行います。
ユーザが会員でなくなった場合、すぐにはユーザテーブルuid_listから削除せず、flagをfalseにすることにします。
これはWEBシステムの常套手段で、バッチ処理でデータ削除することとします。
memcachedはKEYとDATAのペアを保存するメモリDBです。まずはじめに、uid_listのうち、DATAとして保存する文字列を生成する関数uid_list_create_dataを作成します。
CREATE FUNCTION uid_list_create_data(dat_int int, dat_varchar varchar(10), dat_text text) RETURNS TEXT AS $$ DECLARE data TEXT; d_i TEXT; d_v TEXT; d_t TEXT; BEGIN IF dat_int IS NOT NULL THEN d_i := dat_int::TEXT; ELSE d_i := ''; END IF; IF dat_varchar IS NOT NULL THEN d_v := dat_varchar::TEXT; ELSE d_v := ''; END IF; IF dat_text IS NOT NULL THEN d_t := dat_text::TEXT; ELSE d_t := ''; END IF; data := d_i || ':' || d_v || ':' || d_t; RETURN data; END; $$ LANGUAGE 'plpgsql'; |
トリガ関数の中身であるuid_list_op()です。この関数中でpgmemcacheの関数群(memcache_set(), memcache_set(),memcache_delete())を呼んでいます。
CREATE FUNCTION uid_list_op() RETURNS TRIGGER AS $$ DECLARE data text; BEGIN data := uid_list_create_data(NEW.dat_int, NEW.dat_varchar, NEW.dat_text); /* INSERT */ IF TG_OP = 'INSERT' THEN PERFORM memcache_set(NEW.uid::TEXT, data); /* UPDATE */ ELSIF TG_OP = 'UPDATE' THEN IF NEW.flag = FALSE THEN /* flag = FALSE */ PERFORM memcache_delete(OLD.uid::TEXT); ELSE /* flag = TRUE */ PERFORM memcache_set(NEW.uid::TEXT, data); END IF; END IF; RETURN NULL; END; $$ LANGUAGE 'plpgsql'; |
これらの準備後、トリガを設定します。
CREATE TRIGGER uid_list_trg AFTER INSERT OR UPDATE ON uid_list FOR EACH ROW EXECUTE PROCEDURE uid_list_op(); |
最後に、memcached起動時にPostgreSQLのデータをアップロードするための関数uid_list_upload2memcache()を定義します。
CREATE FUNCTION uid_list_upload2memcache() RETURNS int AS $$ DECLARE c int; id uid_list.uid%TYPE; flg uid_list.flag%TYPE; d_i uid_list.dat_int%TYPE; d_v uid_list.dat_varchar%TYPE; d_t uid_list.dat_text%TYPE; data text; cur CURSOR FOR SELECT uid,flag,dat_int,dat_varchar,dat_text FROM uid_list; BEGIN c := 0; OPEN cur; LOOP FETCH cur INTO id,flg,d_i,d_v,d_t; EXIT WHEN NOT FOUND; IF flg != FALSE THEN data := uid_list_create_data(d_i, d_v, d_t); PERFORM memcache_set(id::TEXT, data); c := c + 1; END IF; END LOOP; CLOSE cur; RETURN c; END; $$ LANGUAGE 'plpgsql'; |
1.4 動作検証 |
1.4.1 memcached
まず、memcachedの基本性能ですが、ごく普通のサーバ(CPU 3.4GHz)上で動作させたところ、毎秒2000程度の検索(GET処理)が可能でした。1.4.2 uid_list_upload2memcache()の動作
問題なく、PostgreSQLとmemcachedの同期がとれました。
1.4.3 登録、更新、退会
1.4.4 問題点と解決のアプローチ
「言うは易く行うは難し」でシステム全体の兼ね合いを考慮しつつ、注意深く関数を設計する必要がありますが、ここで重要なのは
「pgmemcacheは使えないが、Cでプログラミングすれば課題は解決」
ということです。
1.5 実運用に向けて |
Remark: リカバリー手法は、システム構成が固まった段階で詳細を議論することにします。
1.5.1 基本ポリシー
以下、システム設計の基本ポリシーを述べます:
さらに、フェールオーバー機能もほしいところです。
また各モジュール、ここではmemcachedやPostgreSQL、は確実に動作しつづけることを保証しなければなりません。例えばPostgreSQLのユーザテーブルとmemcached上のデータが確実に同期していることを保証する機構が必要です。
最後の項目についての補足です。
なかには次のように考える人もいるかもしれません(というか、確実に1名はいます)。
間違った考え方: 「memcachedのデータとPostgreSQLのデータとが確実に同期させるのは難しい。
だからmemcachedをアクセスしてユーザじゃなくても、PostgreSQLに念のためにアクセスして確認すればいい。」
いうまでもなく、これは完全に間違いです。一般的には、登録されていない非ユーザが圧倒的多数なわけですから、memcachedに載っていない場合にPostgreSQLでも検索するとなれば、ほとんどの認証処理はPostgreSQLにも集中し、memcachedを使う意味がなくなるからです。
1.5.2 基本構成
以下に、システムの基本構成案を示します。
願わくば、フェールオーバー機能も追加したいところですが、障害を検知するのは非常に難しい。
しかし、「最悪のなら更新機能は停止しても構わない、認証のみ確実に動作すること」という条件を課せられることが多いので、
(1)更新系DBの復旧時間の短縮
に注力してシステムを設計するのが肝要です。
memcachedのバックエンドDBを用意した最大の理由は、ユーザテーブルとmemcachedのデータが一致していることを保証するバッチ処理の負荷を考慮してのことです。この処理については後述します。
ただし、構成要素が増えることで、システム全体の故障率は上がり、障害ポイントも増えるので、
(2)レプリケーションを行うか否か、memcached毎にバックエンドDBを用意するか否かは、議論の余地が多いにあり
です。
管理サーバは、システム全体の高可用性に大きな責任を負うので、
(4)管理サーバの機能の明確化
が必須です。
1.5.3 現時点での課題のまとめ
これまでの課題をまとめます:課題 | 解決策 | 備考 |
(0)トリガ関数のエラー処理設計 | _ | (4)の管理サーバ機能と関連する。 |
(1)更新系DBの復旧時間の短縮 | _ | _ |
(2)memcachedのバックエンドDB構成 | _ | _ |
(3)memcachedとPostgreSQLのデータの一致を保証する機構 | 解決済み | _ |
(4)管理サーバの機能の明確化 | _ | 機能集中しすぎてはならない |
(Z)リカバリー | _ | 要吟味 |
実は、「(3)memcachedとPostgreSQLのデータの一致を保証する機構」については解決済みです。
むしろ、この課題が解決できたからこそ、システム構成の再考やリカバリー手順など、システム設計に注力できるようになったというのが正解です。