memcachedとPostgreSQLによるユーザ認証システムの検討

last update: 10 Jun 2007

home

memcachedとPostgreSQLによるユーザ認証システムの検討



1.1 はじめに

多数のユーザが訪れるWEBサイトにおいて、会員と非会員の区別を行うユーザ認証システムは意外にやっかいです。
特にユーザ数が100万から1000万オーダーの巨大サイトは、毎秒1000以上のアクセスに耐えねばならず、全てのユーザ認証をユーザマスターテーブルを持つDBサーバで行うのは現実的ではありません。

すぐに思い付くのは、RACなどのクラスタを構築するか、レプリケーションで複数のDBサーバによる負荷分散を行うかですが、それぞれに大きな欠点があります。


上に述べたハードウエア主導の解決策以外に、ソフトウエア主導の解決策としては、ユーザテーブルの分割もありますが、これも所詮はレプリケーションと同じで、多数のDBサーバで運用することに違いはないので、運用面の課題を解決できません。

ここでは、全く別のアプローチとして、ユーザ登録などの更新系処理はPostgreSQLサーバで行い、ユーザ認証(=検索)処理はメモリーDBのmemcachedを使う方式を検討します。

1.2 基本原理
下図がユーザ認証システムの基本構成図です。 ユーザ登録などの更新系処理はPostgreSQLサーバで行い、さらにトリガー関数を使って更新情報をmemcachedサーバにも反映させます。 ユーザ認証(=検索)処理はmemcachedを使います。



1.3 プロトタイプの作成

    1.3.1 ツール群の準備

    実験にあたり、必要なツールをダウンロードしてインストールします。
    注意: 実験後にpgmemcache+PL/pgSQLによるトリガー関数は使えないことが分かりますが、今は構わずに準備します。

      1.3.1.1 PostgreSQL

      以下のURLからバージョン8.2以上のPostgreSQLをダウンロードし、インストールしてください。

      http://www.postgresql.org/download/

      システムのインストールディレクトリは、標準の/usr/local/pgsqlとします。

      1.3.1.2 memcached

      memcachedはlibeventを使うので、まずはじめにlibeventをインストールしてください。

      http://www.monkey.org/~provos/libevent/

      次に以下のURLからmemcachedをダウンロードしてインストールしてください。
      http://www.danga.com/memcached/download.bml

      1.3.1.3 libmemcahe

      libmemcaheはC言語用のライブラリです。

      http://people.freebsd.org/~seanc/libmemcache/

      デフォルトでは/usr/local/libにインストールされるので、/etc/ld.so.confに"/usr/local/lib"を追加してください。

      1.3.1.4 pgmemcahe

      最後にPostgreSQL用のmemcachedライブラリ:pgmemcacheをインストールします。

      http://pgfoundry.org/projects/pgmemcache/

      インストールの詳細は、README.pgmemcacheを参照してください。特に"SETUP"のパートを忘れずに実行してください。
      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).
      

      README.pgmemcahe: SETUP


    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:退会日時など */
         );
    

    ユーザテーブル: uid_list


    ここで、uidはPrimary Keyです。データ型はint型である必要はありません。varcharなどOKです。

    ユーザデータの登録はINSERT、変更はUPDATEで行います。
    ユーザが会員でなくなった場合、すぐにはユーザテーブルuid_listから削除せず、flagをfalseにすることにします。 これはWEBシステムの常套手段で、バッチ処理でデータ削除することとします。

    1.3.3 関数の定義

      1.3.3.1 DATA文字列の生成関数:uid_list_create_data()

      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_create_data()


      1.3.3.2 トリガ関数:uid_list_op()

      トリガ関数の中身である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';
      

      uid_list_op()


      1.3.3.3 トリガ:uid_list_trg

      これらの準備後、トリガを設定します。

      CREATE TRIGGER uid_list_trg
        AFTER INSERT OR UPDATE ON uid_list
        FOR EACH ROW
        EXECUTE PROCEDURE uid_list_op();
      

      uid_list_trg


      1.3.3.4 初期化関数:uid_list_upload2memcache()

      最後に、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';
      

      uid_list_upload2memcache()


1.4 動作検証

    1.4.1 memcached

    まず、memcachedの基本性能ですが、ごく普通のサーバ(CPU 3.4GHz)上で動作させたところ、毎秒2000程度の検索(GET処理)が可能でした。
    また、1000万件のデータをストアした状態でも、検索性能の低下はみられませんでした。

    1.4.2 uid_list_upload2memcache()の動作

    問題なく、PostgreSQLとmemcachedの同期がとれました。

    1.4.3 登録、更新、退会

    ユーザテーブルuid_listに対して、INSERT、UPDATEを実行した結果は、memcachedに反映されました。

    1.4.4 問題点と解決のアプローチ

    この段階で見つかった問題点は、 memcachedがDownしていると、 pgmemcacheを使ったトリガー関数がエラーを返すため、 PostgreSQLの更新処理も失敗してしまう ことです。


    実際にシステムを運用する場合、memcachedとPostgreSQLがこれほどまでに密に結合し、障害が伝搬するのは望ましくありません。
    幸い、解決方法は簡単にみつかりました。pgmemcacheがエラーを返すためにトリガ操作自体もROLLBACKされるわけですから、 pgmemcacheとPL/pgSQLをを使わずにC言語でトリガ関数を書き、ERRORをWARRINGにして、memcachedの障害を運用者に報告する よう、関数を作り直せばよいのです。

    「言うは易く行うは難し」でシステム全体の兼ね合いを考慮しつつ、注意深く関数を設計する必要がありますが、ここで重要なのは 「pgmemcacheは使えないが、Cでプログラミングすれば課題は解決」 ということです。

1.5 実運用に向けて
上記の実験で、ユーザ認証の基本的な機能はmemcachedとPostgreSQLの組み合わせで実現できることが実証されました。
しかし、実運用には対障害性を高め、さらに障害からのリカバリー手法を確立しなければなりません。
ここでは対障害性を高めるためのいくつかの方法を議論します。
Remark: リカバリー手法は、システム構成が固まった段階で詳細を議論することにします。

    1.5.1 基本ポリシー

    以下、システム設計の基本ポリシーを述べます:

      1.5.1.1 間違った考え方

      最後の項目についての補足です。
      なかには次のように考える人もいるかもしれません(というか、確実に1名はいます)。

      間違った考え方: 「memcachedのデータとPostgreSQLのデータとが確実に同期させるのは難しい。
      だからmemcachedをアクセスしてユーザじゃなくても、PostgreSQLに念のためにアクセスして確認すればいい。」

      いうまでもなく、これは完全に間違いです。一般的には、登録されていない非ユーザが圧倒的多数なわけですから、memcachedに載っていない場合にPostgreSQLでも検索するとなれば、ほとんどの認証処理はPostgreSQLにも集中し、memcachedを使う意味がなくなるからです。



      つまり、memcachedを完全に信用してシステム構築しなければなりませんし、信用を保証する機構も必要ということです。

    1.5.2 基本構成

    以下に、システムの基本構成案を示します。



    1.5.3 現時点での課題のまとめ

    これまでの課題をまとめます:
    課題 解決策 備考
    (0)トリガ関数のエラー処理設計 _ (4)の管理サーバ機能と関連する。
    (1)更新系DBの復旧時間の短縮 _ _
    (2)memcachedのバックエンドDB構成 _ _
    (3)memcachedとPostgreSQLのデータの一致を保証する機構 解決済み _
    (4)管理サーバの機能の明確化 _ 機能集中しすぎてはならない
    (Z)リカバリー _ 要吟味

    実は、「(3)memcachedとPostgreSQLのデータの一致を保証する機構」については解決済みです。
    むしろ、この課題が解決できたからこそ、システム構成の再考やリカバリー手順など、システム設計に注力できるようになったというのが正解です。


    また進展があったら文書を更新します。

home