Archive for 9月 2010

PostgreSQL + Slony-I: 3 台構成

PostgreSQL 用の、トリガベースのレプリケーションシステムである Slony-I は、非同期でありながらクラスタ全体がインテリジェントに連動して動く素晴らしいシステムなのですが、いかんせん敷居が高いです。構築の定形パターンを作っておこうと思ったので、メモに残します。

PostgreSQL のインストール、設定、起動

PostgreSQL のインストールをします。

[root@node1 ~]# yum install -y postgresql84 postgresql84-server
(中略)
Installed:
  postgresql84.x86_64 0:8.4.4-1.el5_5.1
  postgresql84-server.x86_64 0:8.4.4-1.el5_5.1

Dependency Installed:
  postgresql84-libs.x86_64 0:8.4.4-1.el5_5.1

Complete!
[root@node1 ~]#

データベースを初期化、起動し、システム起動時に起動するようにします。

[root@node1 ~]# service postgresql initdb
データベースを初期化中:                                    [  OK  ]
[root@node1 ~]# service postgresql start
postgresql サービスを開始中:                               [  OK  ]
[root@node1 ~]# chkconfig postgresql on
[root@node1 ~]#

CentOS のデフォルトだと、PostgreSQL は ident 認証になっていますので、”postgres” ユーザに su します。とりあえずロケールと文字エンコーディングを確認し、リモートからアクセスできるようにしておきます。pg_hba.conf のアクセス制御のネットワークの指定については、環境ごとに適宜読み替えてください。

[root@node1 ~]# su - postgres
-bash-3.2$ psql template1 -c "select datname, datcollate,
 datctype, pg_encoding_to_char(encoding) from pg_database"
  datname  | datcollate  |  datctype   | pg_encoding_to_char
-----------+-------------+-------------+---------------------
 template1 | ja_JP.UTF-8 | ja_JP.UTF-8 | UTF8
 template0 | ja_JP.UTF-8 | ja_JP.UTF-8 | UTF8
 postgres  | ja_JP.UTF-8 | ja_JP.UTF-8 | UTF8
(3 行)

-bash-3.2$ cp /var/lib/pgsql/data/postgresql.conf \
 /var/lib/pgsql/data/postgresql.conf.orig
-bash-3.2$ vi /var/lib/pgsql/data/postgresql.conf
-bash-3.2$ diff -uNr /var/lib/pgsql/data/postgresql.conf.orig \
 /var/lib/pgsql/data/postgresql.conf
--- /var/lib/pgsql/data/postgresql.conf.orig    2010-09-28 16:58:35.000000000 +0900
+++ /var/lib/pgsql/data/postgresql.conf 2010-09-28 17:01:38.000000000 +0900
@@ -56,7 +56,7 @@

 # - Connection Settings -

-#listen_addresses = 'localhost'                # what IP address(es) to listen on;
+listen_addresses = '*'         # what IP address(es) to listen on;
                                        # comma-separated list of addresses;
                                        # defaults to 'localhost', '*' = all
                                        # (change requires restart)
-bash-3.2$ cp /var/lib/pgsql/data/pg_hba.conf \
 /var/lib/pgsql/data/pg_hba.conf.orig
-bash-3.2$ vi /var/lib/pgsql/data/pg_hba.conf
-bash-3.2$ diff -uNr /var/lib/pgsql/data/pg_hba.conf.orig \
 /var/lib/pgsql/data/pg_hba.conf
--- /var/lib/pgsql/data/pg_hba.conf.orig        2010-09-28 17:09:08.000000000 +0900
+++ /var/lib/pgsql/data/pg_hba.conf     2010-09-28 17:09:51.000000000 +0900
@@ -72,3 +72,5 @@
 host    all         all         127.0.0.1/32          ident
 # IPv6 local connections:
 host    all         all         ::1/128               ident
+# IPv4 local network connections:
+host    postgres    postgres    192.168.1.0/24        md5
-bash-3.2$ exit
logout
[root@node1 ~]# service postgresql restart
postgresql サービスを停止中:                               [  OK  ]
postgresql サービスを開始中:                               [  OK  ]
[root@node1 ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
[root@node1 ~]# service iptables save
ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
[root@node1 ~]#

サンプルデータの作成

ある程度 1 台で運用していたと見立てて、テスト用のテーブルとデータを作成します。

[root@node1 ~]# su - postgres
-bash-3.2$ psql -c "
 create table foo (k integer primary key, v integer);
 insert into foo (
            select 1, (random() * 100)::integer
  union all select 2, (random() * 100)::integer
  union all select 3, (random() * 100)::integer )"
NOTICE:  CREATE TABLE / PRIMARY KEYはテーブル"foo"に暗黙的なインデックス"foo_pkey"を作成します
INSERT 0 3
-bash-3.2$ psql -c "select * from foo;"
 k | v
---+----
 1 | 26
 2 | 34
 3 | 53
(3 行)

-bash-3.2$

Slony-I のインストール、設定、起動

CentOS 用の Slony-I パッケージを、yum でインストールします。PostgreSQL 8.4 用の最新の、yum 設定ファイルの RPM をインストールしてください。CentOS-5.5 で提供される postgres84-* とコンフリクトするファイルが多いので、Slony-I のインストールを終えたら、yum 設定ファイルはアンインストールしておきます。

[root@node1 ~]# rpm \
 -Uvh http://www.pgrpms.org/reporpms/8.4/pgdg-centos-8.4-2.noarch.rpm
http://www.pgrpms.org/reporpms/8.4/pgdg-centos-8.4-2.noarch.rpm を取得中
準備中...                ########################################### [100%]
   1:pgdg-centos            ########################################### [100%]
[root@node1 ~]# yum install -y slony1-II
(中略)
Installed:
  slony1-II.x86_64 0:2.0.3-2.rhel5

Dependency Installed:
  perl-DBD-Pg.x86_64 0:1.49-2.el5_3.1        perl-DBI.x86_64 0:1.52-2.el5

Complete!
[root@node1 ~]# rpm -e pgdg-centos
[root@node1 ~]#

PL/pgSQL 言語サポートを導入し、Slony-I 用の DB ロールを作成します。

[root@node1 ~]# su - postgres
-bash-3.2$ createlang plpgsql
-bash-3.2$ createuser --encrypted --pwprompt --superuser slony
新しいロールのパスワード:<パスワード>
もう一度入力してください:<パスワード>
-bash-3.2$ psql -c \
 "update pg_authid set rolcatupdate = 't' where rolname = 'slony'"
UPDATE 1
-bash-3.2$

slon デーモンがデータベースにアクセスできるようにします。

[root@node1 ~]# su - postgres
-bash-3.2$ cp /var/lib/pgsql/data/pg_hba.conf \
 /var/lib/pgsql/data/pg_hba.conf.orig2
-bash-3.2$ vi /var/lib/pgsql/data/pg_hba.conf
-bash-3.2$ diff -uNr /var/lib/pgsql/data/pg_hba.conf.orig2 \
 /var/lib/pgsql/data/pg_hba.conf
--- /var/lib/pgsql/data/pg_hba.conf.orig2       2010-09-29 10:43:56.000000000 +0900
+++ /var/lib/pgsql/data/pg_hba.conf     2010-09-29 10:45:38.000000000 +0900
@@ -67,10 +67,13 @@
 # TYPE  DATABASE    USER        CIDR-ADDRESS          METHOD

 # "local" is for Unix domain socket connections only
+local   postgres    slony                             md5
 local   all         all                               ident
 # IPv4 local connections:
+host    postgres    slony       127.0.0.1/32          md5
 host    all         all         127.0.0.1/32          ident
 # IPv6 local connections:
 host    all         all         ::1/128               ident
 # IPv4 local network connections:
+host    postgres    slony       192.168.1.0/24        md5
 host    postgres    postgres    192.168.1.0/24        md5
-bash-3.2$ exit
[root@node1 ~]# service postgresql restart
postgresql サービスを停止中:                               [  OK  ]
postgresql サービスを開始中:                               [  OK  ]
[root@node1 ~]# cp /etc/slon.conf /etc/slon.conf.orig
[root@node1 ~]# vi /etc/slon.conf
[root@node1 ~]# diff -uNr /etc/slon.conf.orig /etc/slon.conf
--- /etc/slon.conf.orig 2010-09-28 15:12:00.000000000 +0900
+++ /etc/slon.conf      2010-09-28 15:15:04.000000000 +0900
@@ -86,10 +86,10 @@

 # Set the cluster name that this instance of slon is running against
 # default is to read it off the command line
-#cluster_name='sloncluster'
+cluster_name='testcluster'

 # Set slon's connection info, default is to read it off the command line
-#conn_info='host=/tmp port=5432 user=slony'
+conn_info='host=localhost port=5432 dbname=postgres user=slony password=slony'

 # maximum time planned for grouped SYNCs
 # If replication is behind, slon will try to increase numbers of
[root@node1 ~]# service slony1-II start
slony1-II サービスを開始中:                                [  OK  ]
[root@node2 ~]# chkconfig slony1-II on
[root@node1 ~]#

slon デーモンのログで、まずは slony ロールでの接続ができていることを確認します。まだクラスタの初期化をしていないので Slony-I としての動作ではエラーが出ていますが、問題ありません。

[root@node1 ~]# tail /var/log/slony
2010-09-29 10:49:18 JSTCONFIG main: String option lag_interval = [NULL]
2010-09-29 10:49:18 JSTCONFIG main: String option command_on_logarchive = [NULL]
2010-09-29 10:49:18 JSTCONFIG main: String option syslog_facility = LOCAL0
2010-09-29 10:49:18 JSTCONFIG main: String option syslog_ident = slon
2010-09-29 10:49:18 JSTCONFIG main: String option cleanup_interval = 10 minutes
2010-09-29 10:49:18 JSTCONFIG slon: worker process created - pid = 4728
2010-09-29 10:49:18 JSTERROR  cannot get sl_local_node_id -
 ERROR:  スキーマ"_testcluster"は存在しません
LINE 1: select last_value::int4 from "_testcluster".sl_local_node_id
                                     ^
2010-09-29 10:49:18 JSTFATAL  main: Node is not initialized properly - sleep 10s
[root@node1 ~]#

Slony-I クラスタの 1st ノードをセットアップ

[root@node1 ~]# mv /etc/slon_tools.conf /etc/slon_tools.conf.orig
[root@node1 ~]# vi /etc/slon_tools.conf
[root@node1 ~]# cat /etc/slon_tools.conf
$CLUSTER_NAME = 'testcluster';
$LOGDIR = '/var/log/slony1';
$DEBUGLEVEL = 2;

add_node(
 node     => 1,
 host     => 'node1.priv',
 port     => 5432,
 dbname   => 'postgres',
 user     => 'slony',
 password => 'slony' );

$MASTERNODE = 1;

$SLONY_SETS = {
  "set1" => {
    "set_id" => 1,
    "table_id" => 1,
    "sequence_id" => 1,
    "pkeyedtables" => [
      'foo',
    ],
  },
};

1;
[root@node1 ~]# slonik_init_cluster | slonik
<stdin>:8: Set up replication nodes
<stdin>:11: Next: configure paths for each node/origin
<stdin>:12: Replication nodes prepared
<stdin>:13: Please start a slon replication daemon for each node
[root@node1 ~]# slonik_create_set set1 | slonik
<stdin>:15: Subscription set 1 created
<stdin>:16: Adding tables to the subscription set
<stdin>:20: Add primary keyed table public.foo
<stdin>:23: Adding sequences to the subscription set
<stdin>:24: All tables added
[root@node1 ~]#

何か不具合が起きたら、”DROP SCHEMA _<クラスタ名> CASCADE” で、スキーマごとドロップしてからやり直してください。

ログに、”configuration complete” が出ていれば、slon デーモンが正常に Slony-I の管理データにアクセスできています。

[root@node1 ~]# less /var/log/slony
...
2010-09-29 10:54:48 JSTCONFIG main: loading current cluster configuration
2010-09-29 10:54:48 JSTCONFIG main: last local event sequence = 5000000001
2010-09-29 10:54:48 JSTCONFIG main: configuration complete - starting threads
2010-09-29 10:54:48 JSTINFO   localListenThread: thread starts
2010-09-29 10:54:48 JSTCONFIG version for "host=localhost port=5432
 dbname=postgres user=slony password=slony" is 80404
...

Slony-I スレーブノードの追加

2 台目のサーバ上で、「PostgreSQL のインストール、設定、起動」と「Slony-I のインストール、設定、起動」を実行します。

slon tools の設定ファイルに、2 つ目のノードを追加します。

[root@node1 ~]# cp /etc/slon_tools.conf /etc/slon_tools.conf.orig2
[root@node1 ~]# vi /etc/slon_tools.conf
[root@node1 ~]# diff -uNr /etc/slon_tools.conf.orig2 /etc/slon_tools.conf
--- /etc/slon_tools.conf.orig2  2010-09-28 17:24:20.000000000 +0900
+++ /etc/slon_tools.conf        2010-09-28 17:23:36.000000000 +0900
@@ -10,6 +10,14 @@
  user     => 'slony',
  password => 'slony' );

+add_node(
+ node     => 2,
+ host     => 'node2.priv',
+ port     => 5432,
+ dbname   => 'postgres',
+ user     => 'slony',
+ password => 'slony' );
+
 $MASTERNODE = 1;

 $SLONY_SETS = {
[root@node1 ~]#

対象スキーマをコピーしてからノードを Slony-I クラスタへ store し、購読設定をします。ここで注意としては、スキーマをコピーした後で、ノードを store することです。コピーの際に、node2 上にはまだ Slony-I 管理情報の名前空間が無いので、ここで node1 のテーブルに仕掛けてある Slony-I のトリガが落ちます (下記の通り、エラーになります)。その後の "store node" で名前空間が、"subscribe set" でトリガが追加されます。

[root@node1 ~]# pg_dump -s -c "dbname=postgres user=slony password=slony" \
 -n public | psql "host=node2.priv dbname=postgres user=slony password=slony"
SET
SET
SET
SET
SET
SET
SET
ERROR:  リレーション"public.foo"は存在しません
ERROR:  リレーション"public.foo"は存在しません
ERROR:  リレーション"public.foo"は存在しません
ERROR:  テーブル"foo"は存在しません
DROP SCHEMA
CREATE SCHEMA
ALTER SCHEMA
COMMENT
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
ERROR:  スキーマ"_testcluster"は存在しません
ERROR:  テーブル"foo"のトリガ"_testcluster_denyaccess"は存在しません
ERROR:  スキーマ"_testcluster"は存在しません
REVOKE
REVOKE
GRANT
GRANT
[root@node1 ~]# slonik_store_node 2 | slonik
<stdin>:7: Set up replication nodes
<stdin>:10: Next: configure paths for each node/origin
<stdin>:13: Replication nodes prepared
<stdin>:14: Please start a slon replication daemon for each node
[root@node1 ~]# slonik_subscribe_set set1 2 | slonik
<stdin>:4: NOTICE:  subscribe set: omit_copy=f
<stdin>:4: NOTICE:  subscribe set: omit_copy=f
CONTEXT:  SQL statement "SELECT  "_testcluster".subscribeSet_int( $1 ,  $2 ,  $3 ,  $4 ,  $5 )"
PL/pgSQL 関数 "subscribeset" の 68 行目の型 PERFORM
<stdin>:10: Subscribed nodes to set 1
[root@node1 ~]#

この時点で、node2 側の slon ログに "configuration complete" が出ていれば、追加ノードの slon は、正しく Slony-I データにアクセスできています。

スイッチオーバー

購読セットのマスターを、他のノードに移します。この際、旧マスターと新マスターの役割は入れ替わりますが、旧マスターとそれ以外のスレーブとの間のプロバイダ - レシーバの関係は変わりません。

[root@node1 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
ユーザ slony のパスワード:
 sub_set | sub_provider | sub_receiver | sub_forward | sub_active
---------+--------------+--------------+-------------+------------
       1 |            1 |            3 | t           | t
       1 |            1 |            2 | t           | t
(2 行)

[root@node1 ~]# slonik_move_set set1 1 2 | slonik
<stdin>:5: Locking down set 1 on node 1
<stdin>:9: Locked down - moving it
<stdin>:11: Replication set 1 moved from node 1 to 2.  Remember to
<stdin>:12: update your configuration file, if necessary, to note the new location
<stdin>:13: for the set.
[root@node1 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
ユーザ slony のパスワード:
 sub_set | sub_provider | sub_receiver | sub_forward | sub_active
---------+--------------+--------------+-------------+------------
       1 |            1 |            3 | t           | t
       1 |            2 |            1 | t           | t
(2 行)

[root@node1 ~]#

ですので、例えば { node1 → node2, node1 → node3, node1 → node4 } という構成でマスターを node1 から node2 に移したら、{ node2 → node1, node1 → node3, node1 → node4 } になります。

スイッチバックすれば、元に戻ります。

[root@node1 ~]# slonik_move_set set1 2 1 | slonik
<stdin>:5: Locking down set 1 on node 2
<stdin>:9: Locked down - moving it
<stdin>:11: Replication set 1 moved from node 2 to 1.  Remember to
<stdin>:12: update your configuration file, if necessary, to note the new location
<stdin>:13: for the set.
[root@node1 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
ユーザ slony のパスワード:
 sub_set | sub_provider | sub_receiver | sub_forward | sub_active
---------+--------------+--------------+-------------+------------
       1 |            1 |            3 | t           | t
       1 |            1 |            2 | t           | t
(2 行)

[root@node1 ~]#

node1 を、メンテナンスのために、Slony-I クラスタから取り除きたいと思います。では、node2 → node1 → node3 の時、node1 の unsubscribe をしたらどうなるのでしょうか?

[root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
ユーザ slony のパスワード:
 sub_set | sub_provider | sub_receiver | sub_forward | sub_active
---------+--------------+--------------+-------------+------------
       1 |            1 |            3 | t           | t
       1 |            2 |            1 | t           | t
(2 行)

[root@node2 ~]# slonik_unsubscribe_set set1 1 | slonik
<stdin>:5: PGRES_FATAL_ERROR
 select "_testcluster".unsubscribeSet(1, 1);  - ERROR:  Slony-I: Cannot unsubscribe
 set 1 while being provider
<stdin>:8: Failed to unsubscribe node 1 from set 1
[root@node2 ~]#

はい、できません。事前にプロバイダを変更し、node1 を、クラスタのリーフにしておく必要があります。再度、unsubscribe をせずに、プロバイダを変更して subscribe します。下記の作業は、新マスターである node2 で行なっています。

[root@node2 ~]# cp /etc/slon_tools.conf /etc/slon_tools.conf.orig3
[root@node2 ~]# vi /etc/slon_tools.conf
[root@node2 ~]# diff -uNr /etc/slon_tools.conf.orig3 /etc/slon_tools.conf
--- /etc/slon_tools.conf.orig3  2010-09-30 12:33:15.000000000 +0900
+++ /etc/slon_tools.conf        2010-09-30 12:33:40.000000000 +0900
@@ -26,7 +26,7 @@
  user     => 'slony',
  password => 'slony' );

-$MASTERNODE = 1;
+$MASTERNODE = 2;

 $SLONY_SETS = {
   "set1" => {
[root@node2 ~]# slonik_subscribe_set set1 3 | slonik
<stdin>:5: NOTICE:  subscribe set: omit_copy=f
<stdin>:5: NOTICE:  subscribe set: omit_copy=f
CONTEXT:  SQL statement
 "SELECT  "_testcluster".subscribeSet_int( $1 ,  $2 ,  $3 ,  $4 ,  $5 )"
PL/pgSQL 関数 "subscribeset" の 68 行目の型 PERFORM
<stdin>:11: Subscribed nodes to set 1
[root@node2 ~]# psql -U slony postgres \
 -c "select * from _testcluster.sl_subscribe"
ユーザ slony のパスワード:
 sub_set | sub_provider | sub_receiver | sub_forward | sub_active
---------+--------------+--------------+-------------+------------
       1 |            2 |            1 | t           | t
       1 |            2 |            3 | t           | t
(2 行)

[root@node2 bin]# slonik_unsubscribe_set set1 1 | slonik
<stdin>:12: unsubscribed node 1 from set 1
[root@node2 bin]# psql -U slony postgres -c \
 "select * from _testcluster.sl_subscribe"
ユーザ slony のパスワード:
 sub_set | sub_provider | sub_receiver | sub_forward | sub_active
---------+--------------+--------------+-------------+------------
       1 |            2 |            3 | t           | t
(1 行)

[root@node2 bin]#

slon_tools.conf を各ノードに配置し、それぞれの $MASTERNODE には、それぞれのノード名を指定しておいて、管理作業は現マスター上で行なう、とするのが良いかも知れません。

参考:

障害を起こしたスレーブのドロップ

障害に見たてて、OS の終了処理をせずに、node3 の電源を切ります。手順的には、通常のドロップと同じです。

[root@node1 ~]# slonik_drop_node 3 | slonik
<stdin>:11: dropped node 3 cluster
[root@node1 ~]#

/etc/slon_tools.conf の設定も消しておきましょう。

マスター障害に伴うフェイルオーバ

障害に見たてて、OS の終了処理をせずに、node1 の電源を切ります。マスターを node2 に移すべく、node2 で作業をします。先に書いた通り、/etc/slon_tools.conf の $MASTERNODE は 2 です。

[root@node2 ~]# slonik_failover 1 2 | slonik
<stdin>:5: NOTICE:  failedNode: set 1 has other direct receivers - change providers only
<stdin>:5: NOTICE:  failedNode: set 1 has other direct receivers - change providers only
IMPORTANT: Last known SYNC for set 1 = 5000004784
<stdin>:11: Replication sets originating on 1 failed over to 2
[root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_node"
ユーザ slony のパスワード:
 no_id | no_active |          no_comment
-------+-----------+------------------------------
     1 | t         | Node 1 - postgres@node1.priv
     2 | t         | Node 2 - postgres@node2.priv
     3 | t         | Node 3 - postgres@node3.priv
(3 行)

[root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
ユーザ slony のパスワード:
 sub_set | sub_provider | sub_receiver | sub_forward | sub_active
---------+--------------+--------------+-------------+------------
       1 |            2 |            3 | t           | t
(1 行)

[root@node2 ~]# slonik_drop_node 1 | slonik
<stdin>:11: dropped node 1 cluster
[root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_node"
ユーザ slony のパスワード:
 no_id | no_active |          no_comment
-------+-----------+------------------------------
     2 | t         | Node 2 - postgres@node2.priv
     3 | t         | Node 3 - postgres@node3.priv
(2 行)

[root@node2 ~]#

slon_tools.conf の node1 の設定も消しておきましょう。

後は、復旧→スレーブ追加を行ない、どうしても元に戻したければ、さらにスイッチオーバ→subscribe し直し、です。




PostgreSQL-9.0 ホットスタンバイ構築

PostgreSQL-9.0 で、プライマリサーバ 1 台にホットスタンバイサーバ 2 台を追加し、プライマリが死んだことを想定してホットスタンバイからプライマリへ昇格するところまでをやってみます。CentOS-5.5 を利用します。Twitter で @bose999 とやりあっていて、どうも自分の理解 (特に、WAL アーカイブと SR の関係) があいまいでしたので。

以下の 3 台で構成します。非同期のレプリケーションでは、障害復旧後のスイッチバックは現実的ではないので、スイッチしたらそのまま運用します。そのため、”master”, “active”, “primary” や “slave”, “standby” といった、クラスタ内での役割を表す名前はつけない方が良いと思います。

  • node1.priv (192.168.1.24/24): 最初のプライマリ
  • node2.priv (192.168.1.27/24): ホットスタンバイ。node1 障害後のプライマリ
  • node3.priv (192.168.1.30/24): ホットスタンバイ

インストール

CentOS 用のパッケージを、yum でインストールします。

上記サイトから、最新の、yum 設定ファイルの RPM をインストールしてください。

[root@node1 ~]# rpm \
 -Uvh http://www.pgrpms.org/reporpms/9.0/pgdg-centos-9.0-2.noarch.rpm
http://www.pgrpms.org/reporpms/9.0/pgdg-centos-9.0-2.noarch.rpm を取得中
警告: /var/tmp/rpm-xfer.gb5Tw9: ヘッダ V3 DSA signature: NOKEY, key ID 442df0f8
準備中...                ########################################### [100%]
   1:pgdg-centos            ########################################### [100%]
[root@node1 ~]# yum install -y postgresql90 postgresql90-server
(中略)
Installed:
  postgresql90.x86_64 0:9.0.0-1PGDG.rhel5
  postgresql90-server.x86_64 0:9.0.0-1PGDG.rhel5

Dependency Installed:
  postgresql90-libs.x86_64 0:9.0.0-1PGDG.rhel5

Complete!
[root@node1 ~]#

postgresql90 のクライアントは、alternatives を使っているのでパスが通っています。postgresql90-server のコマンドへはパスが通っていませんので、パスを通すなりフルパスで指定するなりが必要です。

一台目を、普通にセットアップ

まずは、普通に (ストリーミングレプリケーションとホットスタンバイのことを考慮せずに) セットアップを行ないます。当座は、ホットスタンバイによる負荷分散が不要で、1 台で運用するという想定です。今回は、システムの init のサービススクリプトを用いずに、postgres ユーザが自前でサービスを用意します。それ以外は一般的な設定だと思いますので、サラッと流します。

  • ユーザ・グループ: postgres.postgres
  • DB クラスタのディレクトリ: /pgdata/
  • WAL アーカイブ先: /backup/walarch/
  • 週一での物理バックアップ先: /backup/pgdata/

以下の設定では、レプリケーションへの流れがあるため、バックアップ (障害対策と PITR のため) については考慮していますが、性能チューニングやセキュリティについては考慮していません。

[root@node1 ~]# mkdir -m 0700 -p /pgdata/ /backup/ \
 /backup/walarch/ /backup/pgdata/
[root@node1 ~]# chown postgres.postgres /pgdata/ /backup/ \
 /backup/walarch/ /backup/pgdata/
[root@node1 ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
[root@node1 ~]# service iptables save
ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
[root@node1 ~]# su - postgres
-bash-3.2$ /usr/pgsql-9.0/bin/initdb -D /pgdata/ --encoding=UTF-8 \
 --no-locale --username=admin --pwprompt --auth=md5
データベースシステム内のファイルの所有者は"postgres"ユーザでした。
このユーザがサーバプロセスを所有しなければなりません。

データベースクラスタはロケールCで初期化されます。
デフォルトのテキスト検索設定はenglishに設定されました。

ディレクトリ/pgdataの権限を設定しています ... ok
サブディレクトリを作成しています ... ok
デフォルトのmax_connectionsを選択しています ... 100
デフォルトの shared_buffers を選択しています ... 32MB
設定ファイルを作成しています ... ok
/pgdata/base/1にtemplate1データベースを作成しています ... ok
pg_authidを初期化しています ... ok
新しいスーパーユーザのパスワードを入力してください:<パスワード>
再入力してください:<パスワード>
パスワードを設定しています ... ok
依存関係を初期化しています ... ok
システムビューを作成しています ... ok
システムオブジェクトの定義をロードしています ... ok
変換を作成しています ... ok
ディレクトリを作成しています ... ok
組み込みオブジェクトに権限を設定しています ... ok
情報スキーマを作成しています ... ok
PL/pgSQL サーバサイド言語をロードしています ...ok
template1データベースをバキュームしています ... ok
template1からtemplate0へコピーしています ... ok
template1からpostgresへコピーしています ... ok

成功しました。以下を使用してデータベースサーバを起動することができます。

    /usr/pgsql-9.0/bin/postmaster -D /pgdata
または
    /usr/pgsql-9.0/bin/pg_ctl -D /pgdata -l logfile start

-bash-3.2$ cp /pgdata/postgresql.conf /pgdata/postgresql.conf.orig
-bash-3.2$ vi /pgdata/postgresql.conf
-bash-3.2$ diff -uNr /pgdata/postgresql.conf.orig /pgdata/postgresql.conf
--- /pgdata/postgresql.conf.orig        2010-09-26 16:51:35.000000000 +0900
+++ /pgdata/postgresql.conf     2010-09-26 16:52:56.000000000 +0900
@@ -56,7 +56,7 @@

 # - Connection Settings -

-#listen_addresses = 'localhost'                # what IP address(es) to listen on;
+listen_addresses = '*'                 # what IP address(es) to listen on;
                                        # comma-separated list of addresses;
                                        # defaults to 'localhost', '*' = all
                                        # (change requires restart)
@@ -150,7 +150,7 @@

 # - Settings -

-#wal_level = minimal                   # minimal, archive, or hot_standby
+wal_level = archive                    # minimal, archive, or hot_standby
 #fsync = on                            # turns forced synchronization on or off
 #synchronous_commit = on               # immediate fsync at commit
 #wal_sync_method = fsync               # the default is the first option
@@ -177,10 +177,11 @@

 # - Archiving -

-#archive_mode = off            # allows archiving to be done
+archive_mode = on              # allows archiving to be done
                                # (change requires restart)
-#archive_command = ''          # command to use to archive a logfile segment
-#archive_timeout = 0           # force a logfile segment switch after this
+archive_command = 'rsync -a %p /backup/walarch/%f'
+                               # command to use to archive a logfile segment
+archive_timeout = 3600         # force a logfile segment switch after this
                                # number of seconds; 0 disables

 # - Streaming Replication -
-bash-3.2$ cp /pgdata/pg_hba.conf /pgdata/pg_hba.conf.orig
-bash-3.2$ vi /pgdata/pg_hba.conf
-bash-3.2$ diff -uNr /pgdata/pg_hba.conf.orig /pgdata/pg_hba.conf
--- /pgdata/pg_hba.conf.orig    2010-09-26 13:51:48.000000000 +0900
+++ /pgdata/pg_hba.conf 2010-09-26 13:52:30.000000000 +0900
@@ -76,5 +76,6 @@
 local   all             all                                     md5
 # IPv4 local connections:
 host    all             all             127.0.0.1/32            md5
+host    all             all             192.168.1.0/24          md5
 # IPv6 local connections:
 host    all             all             ::1/128                 md5
-bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
サーバは起動中です。
-bash-3.2$ touch ~/.pgpass
-bash-3.2$ chmod 0600 ~/.pgpass
-bash-3.2$ echo "localhost:*:*:admin:admin" >> ~/.pgpass
-bash-3.2$ createdb -U admin reptest
-bash-3.2$ psql -U admin reptest
psql (9.0.0)
"help" でヘルプを表示します.

reptest=# \q
-bash-3.2$

インスタンス起動を永続化しておきます。”crontab -e” を実行し、以下を追加します。”@reboot” は、最近めの cron でないとサポートしていないかも知れませんので、man で見ておいてください。

@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start

これでは WAL のアーカイブがたまり続けますので、10 日 (240 時間) より古いものは消して行くように設定します。”crontab -e” を実行し、以下を追加します。

0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/

定期的な物理バックアップ用のスクリプトを作成します。

-bash-3.2$ touch /pgdata/backup-pgdata
-bash-3.2$ chmod +x /pgdata/backup-pgdata
-bash-3.2$ vi /pgdata/backup-pgdata
-bash-3.2$ cat /pgdata/backup-pgdata
#!/bin/sh
rm -fr /backup/pgdata.new/
psql -U admin -c "select pg_start_backup('$(date +%Y%m%d%H%M)')" template1 &&
 sleep 5 && \
 rsync -ar --link-dest=/backup/pgdata/ /pgdata/ /backup/pgdata.new/ &&
 mv /backup/pgdata /backup/pgdata.old && \
 mv /backup/pgdata.new/ /backup/pgdata/ && rm -fr /backup/pgdata.old
psql -U admin -c "select pg_stop_backup()" template1
-bash-3.2$ /pgdata/backup-pgdata
 pg_start_backup
-----------------
 0/7000020
(1 行)

NOTICE:  pg_stop_backup complete, all required WAL segments have been archived
 pg_stop_backup
----------------
 0/70000A0
(1 行)

-bash-3.2$

バックアップを、週に一回定期実行します。”crontab -e” を実行し、以下を追加します。

0 5 * * 0 /pgdata/backup-pgdata

crontab は、全体で以下のようになっているかと思います。

-bash-3.2$ crontab -l
@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/
0 5 * * 0 /pgdata/backup-pgdata
-bash-3.2$

以上の設定で、キャパシティが一杯になるまでは 1 台で運用していた、という設定で次へ行きます。

1 台目のサーバをプライマリ化

キャパシティが厳しくなってきたので、ホットスタンバイ サーバを追加するとことにします。

まずは、後ほどホットスタンバイ側から DB アカウントでパスワードなしの SSH ログインする必要がでてきますので、もしまだ存在しないようでしたら、RSA キー登録のために必要なパスワードとキーチェーン ファイルを用意しておきます。

[root@node1 ~]# passwd postgres
Changing password for user postgres.
New UNIX password:<パスワード>
Retype new UNIX password:<パスワード>
passwd: all authentication tokens updated successfully.
[root@node1 ~]# su - postgres
-bash-3.2$ mkdir ~/.ssh/
-bash-3.2$ chmod 0700 ~/.ssh/
-bash-3.2$ touch ~/.ssh/authorized_keys
-bash-3.2$ chmod 0600 ~/.ssh/authorized_keys
-bash-3.2$

ストリーミングレプリケーションのプライマリとして働くように設定を変更し、設定を反映させるために再起動します。

-bash-3.2$ cp /pgdata/postgresql.conf /pgdata/postgresql.conf.orig2
-bash-3.2$ vi /pgdata/postgresql.conf
-bash-3.2$ diff -uNr /pgdata/postgresql.conf.orig2 /pgdata/postgresql.conf
--- /pgdata/postgresql.conf.orig2       2010-09-26 14:39:18.000000000 +0900
+++ /pgdata/postgresql.conf     2010-09-26 14:42:54.000000000 +0900
@@ -150,7 +150,7 @@

 # - Settings -

-wal_level = archive                    # minimal, archive, or hot_standby
+wal_level = hot_standby                        # minimal, archive, or hot_standby
 #fsync = on                            # turns forced synchronization on or off
 #synchronous_commit = on               # immediate fsync at commit
 #wal_sync_method = fsync               # the default is the first option
@@ -186,14 +186,14 @@

 # - Streaming Replication -

-#max_wal_senders = 0           # max number of walsender processes
+max_wal_senders = 10           # max number of walsender processes
 #wal_sender_delay = 200ms      # walsender cycle time, 1-10000 milliseconds
 #wal_keep_segments = 0         # in logfile segments, 16MB each; 0 disables
 #vacuum_defer_cleanup_age = 0  # number of xacts by which cleanup is delayed

 # - Standby Servers -

-#hot_standby = off                     # "on" allows queries during recovery
+hot_standby = on                       # "on" allows queries during recovery
 #max_standby_archive_delay = 30s       # max delay before canceling queries
                                        # when reading WAL from archive;
                                        # -1 allows indefinite delay
-bash-3.2$ cp /pgdata/pg_hba.conf /pgdata/pg_hba.conf.orig2
-bash-3.2$ vi /pgdata/pg_hba.conf
-bash-3.2$ diff -uNr /pgdata/pg_hba.conf.orig2 /pgdata/pg_hba.conf
--- /pgdata/pg_hba.conf.orig2   2010-09-26 14:44:01.000000000 +0900
+++ /pgdata/pg_hba.conf 2010-09-26 14:45:02.000000000 +0900
@@ -79,3 +79,5 @@
 host    all             all             192.168.1.0/24          md5
 # IPv6 local connections:
 host    all             all             ::1/128                 md5
+
+host    replication     admin           192.168.1.0/24          md5
-bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ restart
サーバ停止処理の完了を待っています....完了
サーバは停止しました
サーバは起動中です。
-bash-3.2$

ホットスタンバイ追加のたびに作成するのは面倒なので、ホットスタンバイ サーバ共通の recovery.conf を事前に作っておきます。

-bash-3.2$ cp /usr/pgsql-9.0/share/recovery.conf.sample \
 /pgdata/recovery.conf.hotstby
-bash-3.2$ vi /pgdata/recovery.conf.hotstby
-bash-3.2$ diff -uNr \
 /usr/pgsql-9.0/share/recovery.conf.sample /pgdata/recovery.conf.hotstby
--- /usr/pgsql-9.0/share/recovery.conf.sample   2010-09-17 23:12:06.000000000 +0900
+++ /pgdata/recovery.conf.hotstby       2010-09-26 17:18:13.000000000 +0900
@@ -43,7 +43,7 @@
 # NOTE that the basename of %p will be different from %f; do not
 # expect them to be interchangeable.
 #
-#restore_command = ''          # e.g. 'cp /mnt/server/archivedir/%f %p'
+restore_command = 'cp /backup/walarch/%f %p'
 #
 #
 # archive_cleanup_command
@@ -95,9 +95,9 @@
 # connection settings primary_conninfo, and receives XLOG records
 # continuously.
 #
-#standby_mode = 'off'
-#
-#primary_conninfo = ''         # e.g. 'host=localhost port=5432'
+standby_mode = 'on'
+
+primary_conninfo = 'host=node1.priv port=5432 user=admin password=admin'
 #
 #
 # By default, a standby server keeps streaming XLOG records from the
@@ -106,7 +106,7 @@
 # Server will poll the trigger file path periodically and stop streaming
 # when it's found.
 #
-#trigger_file = ''
+trigger_file = '/var/lib/pgsql/trigger'
 #
 #---------------------------------------------------------------------------
 # HOT STANDBY PARAMETERS
-bash-3.2$

“wal_mode = archive” で作成された WAL を一掃してから、一度バックアップを取得します。これが、スタンバイ サーバのベースとなります。

-bash-3.2$ rm -f /backup/walarch/*
-bash-3.2$ /pgdata/backup-pgdata
 pg_start_backup
-----------------
 0/A000020
(1 行)

NOTICE:  pg_stop_backup complete, all required WAL segments have been archived
 pg_stop_backup
----------------
 0/A0000A0
(1 行)

-bash-3.2$

ホットスタンバイを追加

プライマリの「インストール」と同様にして、”postgresql90″ と “postgresql90-server” をインストールします。

[root@node2 ~]# rpm \
 -Uvh http://www.pgrpms.org/reporpms/9.0/pgdg-centos-9.0-2.noarch.rpm
[root@node2 ~]# yum install -y postgresql90-server
(中略)

Installed:
  postgresql90-server.x86_64 0:9.0.0-1PGDG.rhel5

Dependency Installed:
  postgresql90.x86_64 0:9.0.0-1PGDG.rhel5
  postgresql90-libs.x86_64 0:9.0.0-1PGDG.rhel5

Complete!
[root@node2 ~]#

ディレクトリを用意し、ファイアウォールにポートをあけます。

[root@node2 ~]# mkdir -m 0700 -p /pgdata/ /backup/ \
 /backup/walarch/ /backup/pgdata/
[root@node2 ~]# chown postgres.postgres /pgdata/ /backup/ \
 /backup/walarch/ /backup/pgdata/
[root@node2 ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
[root@node2 ~]# service iptables save
ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
[root@node2 ~]#

SSH で、パスワードなしでプライマリにログインできるようにします。

[root@node2 ~]# su - postgres
-bash-3.2$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/var/lib/pgsql/.ssh/id_rsa): <Enter>
Created directory '/var/lib/pgsql/.ssh'.
Enter passphrase (empty for no passphrase): <Enter>
Enter same passphrase again: <Enter>
Your identification has been saved in /var/lib/pgsql/.ssh/id_rsa.
Your public key has been saved in /var/lib/pgsql/.ssh/id_rsa.pub.
The key fingerprint is:
a6:bd:ea:76:8b:93:de:3f:7e:48:16:07:c3:a1:f5:ce postgres@node2.priv
-bash-3.2$ cat ~/.ssh/id_rsa.pub |
 ssh node1.priv "cat >> .ssh/authorized_keys"
The authenticity of host 'node1.priv (192.168.1.24)' can't be established.
RSA key fingerprint is 4c:98:4e:3c:dc:d0:e9:49:d0:d0:02:59:66:f8:56:59.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'node1.priv,192.168.1.24' (RSA) to the list of
 known hosts.
postgres@node1.priv's password: <パスワード>
-bash-3.2$ ssh node1.priv
Last login: Sun Sep 26 15:21:11 2010 from queen-centos2.priv
-bash-3.2$ hostname
node1.priv
-bash-3.2$ exit
logout
Connection to node1.priv closed.
-bash-3.2$

ベースバックアップと WAL アーカイブを、プライマリから複製します。今回はサーバ間で、rsync による定期コピーで共有を行なっていますが、既存のバックアップサーバ等が存在するのであれば、NFS 等を用いた方がスマートだと思います。

-bash-3.2$ rsync -azr --delete --rsh=ssh node1.priv:/backup/pgdata/ /pgdata/
-bash-3.2$ rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
-bash-3.2$

1 時間ごとに WAL アーカイブをプライマリから複製するよう、”crontab -e” で以下を追加しておきます。

0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/

recovery.conf を用意し、ホットスタンバイ サーバを起動します。ついでに .pgpass を用意しておきます。

ここで、postgresql.conf の archive_command はプライマリのそれ、そのものなので、フェイルオーバ後は、/backup/walarch/ にアーカイブをします。今回は障害時に自動での切り替えは行ないませんので問題ないとは思いますが、自動での切り替えで、しかも NFS での共有だと、両ノードがスプリットブレインで同じディレクトリに書きこむ怖れがあります。下記サイトに、そのあたりの考察があって参考になりました:

-bash-3.2$ cp /pgdata/recovery.conf.hotstby /pgdata/recovery.conf
-bash-3.2$ rm -f /pgdata/postmaster.pid
-bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
サーバは起動中です。
-bash-3.2$ touch ~/.pgpass
-bash-3.2$ chmod 0600 ~/.pgpass
-bash-3.2$ echo "localhost:*:*:admin:admin" >> ~/.pgpass
-bash-3.2$

起動を永続化します。

@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start

crontab は、最終的に以下のようになっていると思います。

-bash-3.2$ crontab -l
0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
-bash-3.2$

2 つ目、3 つ目のホットスタンバイも、同様の手順で増やします。スレーブが障害を起こして再度追加する場合も、手順は同じです。

スレーブからマスターへの昇格

ホットスタンバイが障害を起こしたとしても、前項のように簡単にリプレースができるのですが、プライマリが障害を起こすと面倒です。プライマリだけでも、DRBD なり共有ディスクなりで、同期で HA 化しておくと良いかも知れません。

障害に見立てて、ブチ切りで、プライマリ (node1) のプロセスを落とします。

[root@node1 ~]# su - postgres
-bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ -m immediate stop
サーバ停止処理の完了を待っています...完了
サーバは停止しました
-bash-3.2$

書きこみができないという報告なり、プロセス監視なりでこの事態を把握してください。修復不能と判断したら、node1 を捨てます。cron のエントリを削除します。

-bash-3.2$ crontab -l
@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/
0 5 * * 0 /pgdata/backup-pgdata
-bash-3.2$ crontab -e
-bash-3.2$ crontab -l
-bash-3.2$

新しくプライマリになるサーバ以外のホットスタンバイ サーバも捨てます。プロセスを停止し、crontab のエントリを消します。

[root@node3 ~]# su - postgres
-bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ stop
サーバ停止処理の完了を待っています....完了
サーバは停止しました
-bash-3.2$ crontab -l
0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
-bash-3.2$ crontab -e
-bash-3.2$ crontab -l
-bash-3.2$

新しくプライマリとなる node2 上でトリガを発行して、node2 をプライマリにします。crontab で、node1 から WAL アーカイブを同期していたエントリを消し、かわりに、古い WAL アーカイブを削除するためのエントリと、物理バックアップのエントリを追加します。

[root@node2 ~]# su - postgres
-bash-3.2$ touch /var/lib/pgsql/trigger
-bash-3.2$ crontab -l
0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
-bash-3.2$ crontab -e
-bash-3.2$ crontab -l
@reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/
0 5 * * 0 /pgdata/backup-pgdata
-bash-3.2$

「1 台目のサーバをプライマリ化」と同等のやり残しをしておきます。

[root@node1 ~]# passwd postgres
Changing password for user postgres.
New UNIX password:<パスワード>
Retype new UNIX password:<パスワード>
passwd: all authentication tokens updated successfully.
[root@node1 ~]# su - postgres
-bash-3.2$ mkdir -p ~/.ssh/
-bash-3.2$ chmod 0700 ~/.ssh/
-bash-3.2$ touch ~/.ssh/authorized_keys
-bash-3.2$ chmod 0600 ~/.ssh/authorized_keys
-bash-3.2$ rm -f /backup/walarch/*
-bash-3.2$ cp /pgdata/recovery.conf.hotstby /pgdata/recovery.conf.hotstby.orig
-bash-3.2$ vi /pgdata/recovery.conf.hotstby
-bash-3.2$ diff -uNr /pgdata/recovery.conf.hotstby.orig \
 /pgdata/recovery.conf.hotstby
--- /pgdata/recovery.conf.hotstby.orig  2010-09-26 21:48:16.000000000 +0900
+++ /pgdata/recovery.conf.hotstby       2010-09-26 21:48:34.000000000 +0900
@@ -97,7 +97,7 @@
 #
 standby_mode = 'on'

-primary_conninfo = 'host=node1.priv port=5432 user=admin password=admin'
+primary_conninfo = 'host=node2.priv port=5432 user=admin password=admin'
 #
 #
 # By default, a standby server keeps streaming XLOG records from the
-bash-3.2$ /pgdata/backup-pgdata
 pg_start_backup
-----------------
 0/A000020
(1 行)

NOTICE:  pg_stop_backup complete, all required WAL segments have been archived
 pg_stop_backup
----------------
 0/A0000A0
(1 行)

-bash-3.2$

あとは、「ホットスタンバイを追加」の項の、”node1″ を “node2″ に読み替えて、ホットスタンバイを追加していきます。




rsync(1) で atomic 同期

とあるデータ領域ディレクトリのバックアップを、ごく普通に rsync(1) コマンドで、既存のバックアップディレクトリとの差分で取っていて、ふと不安になりました。課題は以下:

  • バックアップ中にコケたら、整合性のないツリーだけが残ってしまう (元のデータすら残らない)
  • かといって、毎度新しいツリーにバックアップをしてからリネームする方法では、やたらと時間がかかる
  • できれば、複数世代のバックアップを残したい
  • できれば、複数ディレクトリを整合状態で複製したい

そこで rsync(1) の man を見ていると、以下のようなオプションがありました:

--link-dest=DIR        hardlink to files in DIR when unchanged

これを使えば、前回のコピーからの増分だけをコピーして、変更がなかったファイルについてはハードリンクを張った複製のテンポラリを作れます。コピーが終了したら、テンポラリから最終的な名前にすれば OK:

$ rsync -avzr --rsh=ssh –link-dest=/backup/20100922/ /data/ \
 knaka@backup.priv:/backup/20100923.tmp/
$ ssh knaka@backup.priv "mv /backup/20100923.tmp/ /backup/20100923/"

これで行きたいと思います。

注意:

言うまでもありませんが、ここでいう「アトミック」は「rsync が成功するか否か」の意味での「アトミック」でしかありません。ファイルシステムでトランザクションやスナップショットの機能を用いているわけではありませんので、取得できるのは、決して rsync 開始や終了の瞬間のスナップショットでもなければ、ディレクトリ内のファイル間の整合性を保証するものでもありません。rsync 中に書き換えがおきれば、タイミングによって、その変更は destination 側に対して反映されるかも知れませんし、されないかも知れません。排他ロックがかかっているファイルは、読むこともできません。

ていうか、そんなことができたらデータベースのバックアップとかに苦労しないよね。

スクリプト rsync-dirs-atomic(1)

以下を行なうスクリプトを書いてみました:

  • 複数ディレクトリを、アトミックに同期する (コピー中は “.tmp” の postfix がつく)
  • 最新の版に “latest” のシンボリックリンクを張る

ついでに:

  • コピー元のシンボリックリンクを辿る
  • ローカルへのコピーもできる
  • “-d” オプションで、一定日数より古いバックアップの削除をする
  • エラーチェックもする

これで行けてると思いますが、問題や間違いがありそうでしたら指摘していただけると助かります。

#!/bin/sh
# -*- coding: utf-8 -*-

function usage {
  echo "Usage: $0" \
   "[OPTIONS] SOURCE_DIR [SOURCE_DIRs] [[USER@]HOSTNAME:]/DEST_DIR/" 1>&2
}

days=
while getopts d: arg
do
  case $arg in
    d)
      days=$OPTARG
      ;;
    ?)
      usage
      exit 1
      ;;
  esac
done
shift $(expr $OPTIND - 1)
if ! test -n "$1" -a -n "$2"
then
  usage
  exit 1
fi
sdirs=
while test -n "$2"
do
  sdirs="$sdirs $1"
  shift
done
userhostddirbase=$1
# --------------------------------------------------------------------
# Source side checks
for sdir in $sdirs
do
  if test $(echo $sdir | sed -e 's/^\(.\).*/\1/') != "/"
  then
    echo \"$sdir\" is not an absolute local path. 1>&2
    exit 1
  fi
  if test $(echo $sdir | sed -e 's/.*\(.\)$/\1/') != "/"
  then
    echo \"$sdir\" does not end with \"/\". 1>&2
    exit 1
  fi
  if ! test -d $sdir
  then
    echo Directory \"$sdir\" does not exit. 1>&2
  fi
  if ! test -r $sdir
  then
    echo Cannot read $sdir. 1>&2
    exit 1
  fi
done
# --------------------------------------------------------------------
# Destination side checks
userhost=$(echo $userhostddirbase | cut -s -d : -f 1)
ddirbase=$(echo $userhostddirbase | cut -s -d : -f 2)
if test -n "$userhost" -a -n "$ddirbase"
then
  destshell="ssh $userhost"
else
  ddirbase=$userhostddirbase
  destshell="sh -c"
fi
if test -n "$userhost" && ! ssh -o PasswordAuthentication=no \
 -o StrictHostKeyChecking=yes $userhost "/bin/true"
then
  exit 1
fi
if ! $destshell "test -d $ddirbase/"
then
  echo Directory \"$userhost:$ddirbase/\" does not exist. 1>&2
  exit 1
fi
if ! $destshell "test -w $ddirbase/"
then
  echo Cannot write to $userhost:$ddirbase/. 1>&2
  exit 1
fi
# --------------------------------------------------------------------
# Copy them
stamp=$(date +%Y-%m-%d-%H-%M-%S)
newdir=$ddirbase/$stamp
newdirtmp=$newdir.tmp
$destshell "rm -fr $newdir $newdirtmp; mkdir -p $newdirtmp"
latestdir=$ddirbase/latest
for sdir in $sdirs
do
  for dir in $(echo $sdir; find $sdir -type l |
   while read i; do readlink $i; done )
  do
    id=$(echo $dir | sed -e 's@/@_@g')
    linkdest=""
    if $destshell "test -d $latestdir/$id/"
    then
      linkdest="--link-dest=$latestdir/$id/"
    fi
    if test -n "$userhost"
    then
      rsync -avzr --rsh=ssh $linkdest $dir $userhost:$newdirtmp/$id/
    else
      rsync -avzr $linkdest $dir $newdirtmp/$id/
    fi
  done
done
# Link "latest" and remove old one.
$destshell "mv $newdirtmp $newdir && rm -f $latestdir &&
 ln -sf $(basename $newdir) $latestdir"
if test -n "$days"
then
  $destshell "find $ddirbase -maxdepth 1 -type d -mtime +$days |
   xargs rm -fr"
fi



ソース斜め読み: pldebugger

CentOS で PL/pgSQL デバッガ – Ayutaya.com」がうまく動かずに、パッチを作るのに結構手間がかかったので、ソースを斜め読みした結果を残しておきます。とりあえず、グローバル・ブレークポイントでのデバッグについてだけ書きます。ローカル・ブレークポイントについては、気が向いたら書きます。

PL/pgSQL デバッガ (グローバル・ブレークポイント)

# 手描き…

上記の例では、pgAdmin-III から関数にブレークポイントを仕掛け、psql から関数を呼び出し、pgAdmin-III でデバッグを行なう、という流れで行きます。セッション間で AF_INET での IPC 通信をするところが分かれば理解できると思います。

まず、pgAdmin-III から PostgreSQL へ、普通に接続します。普通にセッションプロセスが fork(2) します (図の、「通常」セッションプロセス)。

関数にブレークポイントを設定するよう指示すると、pgAdmin-III は、上記の認証情報を用いて、もう一本、デバッガ用の接続を確立します (図の、「デバッガ」セッションプロセス)。PostgreSQL 拡張モジュール pldbgapi.so を用いて、デバッガセッションとして働きます。

このデバッグ接続ですが、特権ユーザでしか接続できません。実際に何をしているのか (どんな SQL クエリを送っているのか) を見たくて log_min_duration_statement を 0 に設定してみたのですが、pgAdmin-III が「気を効かせて」、特権ユーザなのを良いことに、ログ出力を抑制してくれます。pgAdmin-III をリビルドして、何をしているのかをログに出すには、pgAdmin-III のソースの以下の部分を削除します:

perl -p -i -e 's/.*SET log_min_messages TO fatal.*//' \
 ./pgadmin/debugger/ctlCodeWindow.cpp \
 ./pgadmin/debugger/dlgDirectDbg.cpp

そういえば、g++-4.1.2 + wxGTK-devel で、配列添え字オペレータの演算子オーバーロードで、引数の int と size_t が曖昧でビルドできない、と言われるかも知れません。ヘッダでどちらかをコメントアウトすれば通ります。

その結果出てくるのが、以下のようなログです (適宜行番号 & 改行追加):

1: LOG:  期間: 1.226 ミリ秒  文: SELECT count(*) AS count, proname
    FROM pg_proc WHERE oid = 16446 GROUP BY proname
2: LOG:  期間: 0.414 ミリ秒  文: set client_encoding to 'UNICODE'
3: LOG:  期間: 1.437 ミリ秒  文: SELECT * from pldbg_create_listener()
4: LOG:  期間: 1.398 ミリ秒  文: SELECT *, 'NULL' as pid
    FROM pldbg_get_target_info('16446', 'o')
5: LOG:  期間: 0.439 ミリ秒  文: SELECT version();
6: LOG:  期間: 0.610 ミリ秒  文: SELECT *
    FROM pldbg_set_global_breakpoint(1, 16446, NULL, NULL)

pg_proc テーブルで、ターゲット関数を確認。pldbg_create_listener() で、デバッガセッションは 127.0.0.1 上に AF_INET でデバッグサーバのポートを開きます。このポートは、実際にデバッグモードに入った後で、デバッガとターゲットが独自のプロトコルで通信を行なうために用いられます。pldbg_get_target_info() でターゲットを指定し、デバッグセッションは、共有メモリ上にブレークポイントとポート番号の情報を書き込みます。最後に、pldbg_set_global_breakpoint() で、デバッグセッションはブロックし、待ちに入ります。

pldbgapi.c:

Datum pldbg_create_listener( PG_FUNCTION_ARGS )
{
  debugSession * session =
   MemoryContextAlloc( TopMemoryContext, sizeof( *session ));
  initializeModule();
  // これ
  session->listener = allocateServerListener( &session->serverPort );
  session->serverSocket = -1;
  mostRecentSession = session;
  PG_RETURN_INT32( addSession( session ));
}

次に、psql(1) からサーバに接続し、新しいセッションプロセスを fork(2) します (図の、「ターゲット」セッションプロセス)。ここで働く plugin_debugger.so は、いわゆる普通の PostgreSQL モジュール (SQL から呼び出す) とは違って、PL/pgSQL の拡張モジュールです。ストアドプロシージャの各行の実行をフックすることができるので、これを利用してブレークポイントやステップ実行を実現します。

plugin_debugger.c:

// 関数の頭と、行の頭でフックがかかるようにする
static PLpgSQL_plugin plugin_funcs =
 { dbg_startup, NULL, NULL, dbg_newstmt, NULL };

void _PG_init( void )
{
  PLpgSQL_plugin ** var_ptr =
   (PLpgSQL_plugin **) find_rendezvous_variable( plugin_name );
  reserveBreakpoints();
  *var_ptr = &plugin_funcs;
}

plpgsql.h:

typedef struct
{
  /* Function pointers set up by the plugin */
  void (*func_setup) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
  void (*func_beg) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
  void (*func_end) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
  void (*stmt_beg) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
  void (*stmt_end) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);

  /* Function pointers set by PL/pgSQL itself */
  void (*error_callback) (void *arg);
  void (*assign_expr) (PLpgSQL_execstate *estate, PLpgSQL_datum *target,
   PLpgSQL_expr *expr );
} PLpgSQL_plugin;

dbg_startup() でブレークポイントを確認して PLpgSQL_execstate * estate に関数ごとにプラグインの固有情報を書きこみます。

plugin_debugger.c:

static void dbg_startup( PLpgSQL_execstate * estate, PLpgSQL_function * func )
{
  if( !breakpointsForFunction( funcGetOid( func )) &&
   !per_session_ctx.step_into_next_func )
  {
    estate->plugin_info = NULL;
    return;
  }
  // estate->plugin_info 構造体が、dbg_newstmt() に渡ります
  initialize_plugin_info(estate, func);
}

その固有情報にもとづいて、dbg_newstmt() が行レベルデバッグを実現します。下記、attach_to_proxy() でデバッガセッションとの接続が確立されます (何かファイアウォールでこの接続が阻害されたりしてた? 確信無い)。

static void dbg_newstmt( PLpgSQL_execstate * estate, PLpgSQL_stmt * stmt )
{
  // ブレークポイントが無ければ即リターン
  if( frame->plugin_info == NULL )
    return;
  else
  {
    Breakpoint * breakpoint = NULL;
    // 該当行であればデバッグモードに入ります
    if(( dbg_info->stepping ) ||
     breakAtThisLine(
      &breakpoint,
      &breakpointScope,
      funcGetOid( dbg_info->func ),
      isFirstStmt( stmt, dbg_info->func ) ? -1 : stmt->lineno ))
    {
      dbg_info->stepping = TRUE;
    }
    else
      return;
    attach_to_proxy( breakpoint );
    // 以下、デバッガセッションとターゲットセッションのやりとり開始
  }
  return;
}



CentOS で PL/pgSQL デバッガ

PostgreSQL 用の GUI インタフェースである pgAdmin-III (pgAdmin: PostgreSQL administration and management tools) ですが、出来が良いわりにはあまり用いられていないようです。コマンドラインのツールでだいたい足りてしまうからでしょうが、ストアドプロシージャのデバッグの際などには、変数の状態等を見ながら作業ができる GUI もそれなりにありがたいですので、pgAdmin-III で動作する PL/pgSQL のデバッガ (PgFoundry: PLpgSQL Debugger by EnterpriseDB: Project Info) を CentOS-5.5 上で試してみたいと思います。

pgAdmin-III でのデバッグ

Red Hat Enterprise Linux (RHEL) なり CentOS の ver. 5 系は 2005 年のデビューですので、ずっと PostgreSQL のバージョンが 8.1 と、いくらなんでも古かったのですが、RHEL-5.5 からは PostgreSQL-8.4 が使えるようになりました。ここでは、そちらで行ってみたいと思います。

サーバの設定

パッケージ “postgresql-server” と “postgresql84-server” は排他です。何かの依存で “postgresql-server” が先に入ってしまうと、後から “postgresql84-server” を入れられませんので、注意してください。

[root@queen-centos ~]# yum install -y postgresql84 postgresql84-server
(中略)
[root@queen-centos ~]#

データベースを初期化、起動し、システム起動時に起動するようにします。

[root@queen-centos ~]# service postgresql initdb
データベースを初期化中:                                    [  OK  ]
[root@queen-centos ~]# service postgresql start
postgresql サービスを開始中:                               [  OK  ]
[root@queen-centos ~]# chkconfig postgresql on
[root@queen-centos ~]#

CentOS のデフォルトだと、PostgreSQL は ident 認証になっていますので、”postgres” ユーザに su して、テスト用のデータベースと DB ロールを作成します。ついでに、リモートからアクセスできるようにしておきます。pg_hba.conf のアクセス制御のネットワークの指定については、環境ごとに適宜読み替えてください。

[root@queen-centos ~]# su - postgres
-bash-3.2$ createdb pgtest
-bash-3.2$ createuser --encrypted --pwprompt \
 --superuser --createrole --createdb pgtest
新しいロールのパスワード:
もう一度入力してください:
-bash-3.2$ cp /var/lib/pgsql/data/postgresql.conf \
 /var/lib/pgsql/data/postgresql.conf.orig
-bash-3.2$ vi /var/lib/pgsql/data/postgresql.conf
-bash-3.2$ diff -uNr /var/lib/pgsql/data/postgresql.conf.orig \
 /var/lib/pgsql/data/postgresql.conf
--- /var/lib/pgsql/data/postgresql.conf.orig    2010-08-20 15:23:50.000000000 +0
+++ /var/lib/pgsql/data/postgresql.conf 2010-08-20 15:24:41.000000000 +0900
@@ -56,7 +56,7 @@

 # - Connection Settings -

-#listen_addresses = 'localhost'                # what IP address(es) to listen;
+listen_addresses = '*'         # what IP address(es) to listen on;
                                        # comma-separated list of addresses;
                                        # defaults to 'localhost', '*' = all
                                        # (change requires restart)
-bash-3.2$ cp /var/lib/pgsql/data/pg_hba.conf \
 /var/lib/pgsql/data/pg_hba.conf.orig
-bash-3.2$ vi /var/lib/pgsql/data/pg_hba.conf
-bash-3.2$ diff -uNr /var/lib/pgsql/data/pg_hba.conf.orig \
 /var/lib/pgsql/data/pg_hba.conf
--- /var/lib/pgsql/data/pg_hba.conf.orig        2010-08-20 15:26:04.000000000 +0
+++ /var/lib/pgsql/data/pg_hba.conf     2010-08-20 15:27:07.000000000 +0900
@@ -72,3 +72,6 @@
 host    all         all         127.0.0.1/32          ident
 # IPv6 local connections:
 host    all         all         ::1/128               ident
+# IPv4 local network connections:
+host    pgtest      pgtest      192.168.1.0/24        md5
+
-bash-3.2$ exit
logout
[root@queen-centos ~]# service postgresql restart
postgresql サービスを停止中:                               [  OK  ]
postgresql サービスを開始中:                               [  OK  ]
[root@queen-centos ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
[root@queen-centos ~]# service iptables save
ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
[root@queen-centos ~]#

PL/pgSQL デバッガのインストール

本家からダウンロードできる tar ball のソースは PostgreSQL-8.4 には古いのですが、かといって CVS 版も 64bit 環境だと微妙にコケるので、パッチをあてた SRPM を置いておきます:

以下のようにビルドしてインストールしてください:

[root@queen-centos ~]# yum install -y rpm-build gcc \
 postgresql84-devel openssl-devel
[root@queen-centos ~]# rpmbuild --rebuild \
 postgresql84-pldebugger-8.4.4-0.cvs20100919.src.rpm
[root@queen-centos ~]# rpm -Uvh \
 /usr/src/redhat/RPMS/x86_64/postgresql84-pldebugger-8.4.4-0.cvs20100919.x86_64.rpm
[root@queen-centos ~]#

PL/pgSQL 用のデバッグプラグインがロードされるように設定ファイルを書きかえ、PostgreSQL を再起動します。

[root@queen-centos ~]# cp /var/lib/pgsql/data/postgresql.conf \
 /var/lib/pgsql/data/postgresql.conf.orig2
[root@queen-centos ~]# vi /var/lib/pgsql/data/postgresql.conf
[root@queen-centos ~]# diff -uNr /var/lib/pgsql/data/postgresql.conf.orig2 \
 /var/lib/pgsql/data/postgresql.conf
--- /var/lib/pgsql/data/postgresql.conf.orig2    2010-09-20 17:33:05.000000000 +0900
+++ /var/lib/pgsql/data/postgresql.conf 2010-09-20 17:34:28.000000000 +0900
@@ -121,7 +121,7 @@

 #max_files_per_process = 1000          # min 25
                                        # (change requires restart)
-shared_preload_libraries = ''          # (change requires restart)
+shared_preload_libraries = '$libdir/plugins/plugin_debugger'

 # - Cost-Based Vacuum Delay -

[root@queen-centos ~]# service postgresql restart
postgresql サービスを停止中:                               [  OK  ]
postgresql サービスを開始中:                               [  OK  ]
[root@queen-centos ~]#

pldbgapi.sql を読み込んで、デバッグ用の型と共有ライブラリへのコネクタを定義し、PL/pgSQL 言語を組み込めば、サーバ側は準備完了です。

[root@queen-centos ~]# psql -h queen-centos.priv -p 5432 -U pgtest pgtest
ユーザ pgtest のパスワード:
psql (8.4.4)
"help" でヘルプを表示します.

pgtest=# \i /usr/share/pgsql/contrib/pldbgapi.sql
(中略)
pgtest=# create language 'plpgsql';
CREATE LANGUAGE
pgtest=# \q
[root@queen-centos ~]#

なお、下記で CVS からのソースの anonymous ダウンロードができます:

[root@queen-centos ~]# yum install -y cvs
(中略)
[root@queen-centos ~]# cvs \
 -d :pserver:anonymous@cvs.pgfoundry.org:/cvsroot/edb-debugger login
[root@queen-centos ~]# cvs \
 -d :pserver:anonymous@cvs.pgfoundry.org:/cvsroot/edb-debugger checkout server
[root@queen-centos ~]#

バージョン違いでビルドできない場合:

edb-debugger のソースは、本来は PostgreSQL のソースツリーの contrib/ に入れてビルドするものですが、今回は postgresql84-server をそのまま使いたかったので、分離してあります。しかし、postgresql84-devel パッケージは、残念ながら PL/pgSQL のヘッダを含んでいませんので、上記 SRPM は、postgresql84 の SRPM から持ってきた plpgsql.h を含んでいます。そのため、本体 (postgresql84) のマイナーバージョンが進んだら、適宜ヘッダファイルを更新して作り直す必要があります。

参考:

なお、これらの手順が面倒で、なおかつ RPM でのパッケージ管理がなくても良いのであれば、下記よりダウンロードできる EnterpriseDB 社の Postgres Plus のパッケージを入れるのも手です:

デバッガの開発元ですので、普通にデバッガを利用できる状態でインストールできます。

pgAdmin-III のインストール

PostgreSQL の GUI クライアントである pgAdmin-III は CentOS の標準レポジトリに入っていませんので、EPEL から入れます。この例では、別のホストにインストールしています。

[root@queen-centos2 ~]# rpm -Uvh $(printf \
 ftp://download.fedora.redhat.com/pub/epel/%s/%s/epel-release-*-*.noarch.rpm \
 $(rpm -q --qf "%{version}" $(rpm -q --whatprovides redhat-release)) \
 $(uname --hardware-platform) )
(中略)
[root@queen-centos2 ~]# yum install -y pgadmin3
(中略)
[root@queen-centos2 ~]#

参考:

デバッグをしてみる

メニューから pgAdmin-III を実行します:

メニュー

データベースに接続し、適当なストアドプロシージャかトリガを作成します。そこにブレークポイントを設定します:

ブレークポイントの選択

その関数がどこかから呼ばれるまで待機します:

デバッグセッションが待機中

関数が呼ばれると実行が停止され、ステップ実行が可能になります:

ステップ実行中




ソース斜め読み: java.util.regex

java.util.regex の概要をおさえるために書いたメモです。あまり記事の体をなしていなくて申し訳ありません。

関連項目:

Pattern, Matcher 共に、パッケージ java.util.regex に属します。

Pattern のインスタンス化は、static ファクトリメソッド java.util.regex.Pattern#compile() (public static Pattern compile(String regex)) が行なうので、Pattern のコンストラクタは private です。

Matcher のインスタンス化は、ファクトリメソッド java.util.regex.Pattern#matcher() (public Matcher matcher(CharSequence input)) が行なうので、java.util.regex.Matcher のコンストラクタはパッケージ private です。

NFA によるバックトラックでマッチを行ないます。DFA は作りません。NFA ならばバックトラックなので、たとえば選択 (“|”) の各項目で条件に重複があっても構わずバックトラックすれば良いのですが、DFA ではそうは行かないはずです (DFA では、特定の入力に対して、状態遷移先は 1 つに決まらなければならないので)。grep(1) などは DFA を作っているようですね、今度見てみます。

“static class Pattern.Node” がなすツリーは、いわゆる Composite パターンで、Node.matcher() は、どのノードでも呼べます。正規表現の文字クラスを表す “private static abstract class Pattern.CharProperty extends Node” では CharProperty.isSatisfiedBy(int ch) がそうです。

なお、「普通の」内部クラスは、「インスタンスの」内部クラス。「クラスの」内部クラスにする (static メソッドから使えるようにする) には、”static” をつける必要があります。”Node” クラスは static ファクトリでインスタンス化されますので、static である必要がありました。C++ にはローカルクラスなど無かったので、新鮮です。

デバッグ出力が見づらくてかなわないです。こんな具合に書き換えれば良い?:

    /**
     * Used to print out a subtree of the Pattern to help with debugging.
     */
    private static String INDENT = "  ";
    private static void printObjectTree(Node node, String indent) {
        while(node != null) {
            if (node instanceof Prolog) {
                System.out.println(indent + "**** start contents prolog loop");
                System.out.println(indent + node);
                printObjectTree(((Prolog)node).loop, indent + INDENT);
                System.out.println(indent + "**** end contents prolog loop");
            } else if (node instanceof Loop) {
                System.out.println(indent + "**** start contents Loop body");
                System.out.println(indent + node);
                printObjectTree(((Loop)node).body, indent + INDENT);
                System.out.println(indent + "**** end contents Loop body");
            } else if (node instanceof Curly) {
                System.out.println(indent + "**** start contents Curly body");
                System.out.println(indent + node);
                printObjectTree(((Curly)node).atom, indent + INDENT);
                System.out.println(indent + "**** end contents Curly body");
            } else if (node instanceof GroupCurly) {
                System.out.println(indent +
                 "**** start contents GroupCurly body" );
                System.out.println(indent + node);
                printObjectTree(((GroupCurly)node).atom, indent + INDENT);
                System.out.println(indent +
                 "**** end contents GroupCurly body" );
            } else if (node instanceof GroupTail) {
                System.out.println(indent + node);
                System.out.println(indent + "Tail next is "+node.next);
                return;
            } else if (node instanceof Branch) {
                System.out.println(indent + "**** start contents Branch body");
                for (Node atom: ((Branch) node).atoms) {
                    printObjectTree(atom, indent + INDENT);
                }
                System.out.println(indent + "**** end contents Branch body");
            } else {
                System.out.println(indent + node);
            }
            node = node.next;
            if (node != null)
                System.out.println(indent + "->next:");
            if (node == Pattern.accept) {
                System.out.println(indent + "Accept Node");
                node = null;
            }
       }
    }

    private static void printObjectTree(Node node) {
        printObjectTree(node, "");
    }

“a+|b+” で、バララとこんな感じに出ました。だいぶ分かりやすくなった:

**** start contents Branch body
  **** start contents Curly body
  com.ayutaya.java.util.regex.Pattern$Curly@276af2
    com.ayutaya.java.util.regex.Pattern$Single@1de3f2d
    ->next:
    Accept Node
  **** end contents Curly body
  ->next:
  com.ayutaya.java.util.regex.Pattern$BranchConn@5d173
  ->next:
  com.ayutaya.java.util.regex.Pattern$LastNode@1f9dc36
  ->next:
  Accept Node
  **** start contents Curly body
  com.ayutaya.java.util.regex.Pattern$Curly@e86da0
    com.ayutaya.java.util.regex.Pattern$Single@1754ad2
    ->next:
    Accept Node
  **** end contents Curly body
  ->next:
  com.ayutaya.java.util.regex.Pattern$BranchConn@5d173
  ->next:
  com.ayutaya.java.util.regex.Pattern$LastNode@1f9dc36
  ->next:
  Accept Node
**** end contents Branch body
->next:
Accept Node

では。




ブート時の jar はどこに記述が?

JDK-1.5 のドキュメントの「クラスの検索方法」) を見ると、以下のようにあります:

ブートストラップクラスは、Java 2 プラットフォームを実装しているクラスです。ブートストラップクラスは、jre/lib ディレクトリの rt.jar と他のいくつかの JAR ファイルに格納されています。これらのアーカイブは、システムプロパティ sun.boot.class.path に格納されているブートストラップクラスパスの値によって指定されます。このシステムプロパティは参照専用なので、直接修正しないでください。

ブートストラップクラスパスの再定義が必要になることはほとんどありません。まれに、別のコアクラスのセットを使用する必要が生じた場合には、非標準のオプション -Xbootclasspath を使ってブートストラップクラスパスを再定義することができます。

すると、初期化時に勝手に読まれるというブートストラップのアーカイブ名のリストというのは、一体どこに記述されているのでしょうか? 起動側のラッパ? JRE のランチャ? それとも JVM の中?

以下は、CentOS-5.5 収録の OpenJDK-1.6.0 での話になります。

とりあえずは、システムプロパティを表示してもらいます (参考: 「Javaのシステムプロパティをすべて表示するJavaコード – いろいろ解析日記」。Properties.list() を使うと、value の出力が省略されてしまう…)。

import java.util.Properties;

class SystemProps {
    public static void main(String[] args) {
        Properties properties = System.getProperties();
        for (Object key: properties.keySet()) {
            System.out.println(key + ": " + properties.get(key));
        }
    }
}

関係ありそうなのは、”sun.boot.class.path” だけです。以下に見られるように、明示的には何も指定しなくても、どこかで値が設定されています。

$ java SystemProps
java.runtime.name: OpenJDK Runtime Environment
sun.boot.library.path: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386
java.vm.version: 14.0-b16
java.vm.vendor: Sun Microsystems Inc.
java.vendor.url: http://java.sun.com/
path.separator: :
java.vm.name: OpenJDK Client VM
file.encoding.pkg: sun.io
sun.java.launcher: SUN_STANDARD
user.country: JP
sun.os.patch.level: unknown
java.vm.specification.name: Java Virtual Machine Specification
user.dir: /home/knaka/src/java
java.runtime.version: 1.6.0_0-b16
java.awt.graphicsenv: sun.awt.X11GraphicsEnvironment
java.endorsed.dirs: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/endorsed
os.arch: i386
java.io.tmpdir: /tmp
line.separator:

java.vm.specification.vendor: Sun Microsystems Inc.
os.name: Linux
sun.jnu.encoding: UTF-8
java.library.path:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/../lib/i386:
 /usr/java/packages/lib/i386:/lib:/usr/lib
java.specification.name: Java Platform API Specification
java.class.version: 50.0
sun.management.compiler: HotSpot Client Compiler
os.version: 2.6.18-194.8.1.el5
user.home: /home/knaka
user.zoneinfo.dir: /usr/share/javazi
user.timezone:
java.awt.printerjob: sun.print.PSPrinterJob
file.encoding: UTF-8
java.specification.version: 1.6
java.class.path: .
user.name: knaka
java.vm.specification.version: 1.0
java.home: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
sun.arch.data.model: 32
user.language: ja
java.specification.vendor: Sun Microsystems Inc.
java.vm.info: mixed mode
java.version: 1.6.0_0
java.ext.dirs:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/ext:/usr/java/packages/lib/ext
sun.boot.class.path:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/resources.jar:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/rt.jar:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/sunrsasign.jar:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/jsse.jar:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/jce.jar:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/charsets.jar:
 /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/classes
java.vendor: Sun Microsystems Inc.
file.separator: /
java.vendor.url.bug: http://java.sun.com/cgi-bin/bugreport.cgi
sun.io.unicode.encoding: UnicodeLittle
sun.cpu.endian: little
sun.cpu.isalist:
$

“rpm -ql” から “xargs grep” しても無いようなので、ラッパや設定ファイルに記述されているわけではなさそうです。続いて、以下のようにランチャのデバッグ出力を見ても、それらしい記述はありません。

$ _JAVA_LAUNCHER_DEBUG= java HelloWorld
----_JAVA_LAUNCHER_DEBUG----
Command line Args:
        argv[0] = 'java'
        argv[1] = 'HelloWorld'
JRE path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
jvm.cfg[0] = ->-client<-
    name: -client  vmType: VM_IF_SERVER_CLASS  server_class: -server
jvm.cfg[1] = ->-server<-
jvm.cfg[2] = ->-hotspot<-
    name: -hotspot  vmType: VM_ALIASED_TO  alias: -client
jvm.cfg[3] = ->-classic<-
jvm.cfg[4] = ->-native<-
jvm.cfg[5] = ->-green<-
jvm.cfg[6] = ->-cacao<-
jvm.cfg[7] = ->-zero<-
1 micro seconds to parse jvm.cfg
pages: 258766  page_size: 4096  physical memory: 1059905536 (0.987GB)
linux_i386_ServerClassMachine: false
Default VM: client
Does `/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so' exist ... yes.
----_JAVA_LAUNCHER_DEBUG----
Command line Args:
        argv[0] = 'java'
        argv[1] = 'HelloWorld'
JRE path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
jvm.cfg[0] = ->-client<-
    name: -client  vmType: VM_IF_SERVER_CLASS  server_class: -server
jvm.cfg[1] = ->-server<-
jvm.cfg[2] = ->-hotspot<-
    name: -hotspot  vmType: VM_ALIASED_TO  alias: -client
jvm.cfg[3] = ->-classic<-
jvm.cfg[4] = ->-native<-
jvm.cfg[5] = ->-green<-
jvm.cfg[6] = ->-cacao<-
jvm.cfg[7] = ->-zero<-
1 micro seconds to parse jvm.cfg
pages: 258766  page_size: 4096  physical memory: 1059905536 (0.987GB)
linux_i386_ServerClassMachine: false
Default VM: client
Does `/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so' exist ... yes.
JVM path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so
1 micro seconds to LoadJavaVM
JavaVM args:
    version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 4
    option[ 0] = '-Djava.class.path=.'
    option[ 1] = '-Dsun.java.command=HelloWorld'
    option[ 2] = '-Dsun.java.launcher=SUN_STANDARD'
    option[ 3] = '-Dsun.java.launcher.pid=9020'
1 micro seconds to InitializeJVM
Main-Class is 'HelloWorld'
Apps' argc is 0
1 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
Hello, World!
$

bootclasspath オプションに存在しないパッケージを渡すとコケるので、明示的に指定されなかったら、JVM の中で、デフォルトのパッケージを読むのだと思われます。

$ java -Xbootclasspath:/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/foo.jar \
 HelloWorld
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object
$ java -Xbootclasspath:/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/rt.jar \
 HelloWorld
Hello, World!
$

しかたがないので grep し倒してそれらしいところを探すと、どうやら os::set_boot_path() がそれです。スタックトレースをとってみます。

$ gdb /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/bin/java
(中略)
(gdb) b os::set_boot_path
Can't find member of namespace, class, struct, or union named "os::set_boot_path"
Hint: try 'os::set_boot_path or 'os::set_boot_path
(Note leading single quote.)
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (os::set_boot_path) pending.
(gdb) run HelloWorld
Starting program: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/bin/java \
 HelloWorld
(中略)
[Switching to Thread 0xb7fefb90 (LWP 9549)]

Breakpoint 1, os::set_boot_path (fileSep=47 '/', pathSep=58 ':')
    at /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/os.cpp:866
866         const char* home = Arguments::get_java_home();
(gdb) bt
#0  os::set_boot_path (fileSep=47 '/', pathSep=58 ':') at \
 /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/os.cpp:866
#1  0x00fbae77 in os::init_system_properties_values () at \
 /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/os/linux/vm/os_linux.cpp:332
#2  0x00cf996f in Arguments::init_system_properties () at \
 /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/arguments.cpp:153
#3  0x01058cb8 in Threads::create_vm (args=0xb7fef3a4, canTryAgain=0xb7fef30b) at \
 /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/thread.cpp:2820
#4  0x00eba5f3 in JNI_CreateJavaVM (vm=0xb7fef3b8, penv=0xb7fef3b4, args=0xb7fef3a4) at \
 /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/prims/jni.cpp:3263
#5  0x0804a088 in InitializeJVM (_args=0xbfffe078) at \
 ../../../../src/share/bin/java.c:1296
#6  JavaMain (_args=0xbfffe078) at ../../../../src/share/bin/java.c:431
#7  0x00b09832 in start_thread () from /lib/libpthread.so.0
#8  0x001e1e0e in clone () from /lib/libc.so.6
(gdb)

C で書かれた「ランチャ」 (エントリポイントは jdk/src/share/bin/java.c) から走るスレッドで、JNI_CreateJavaVM() (libjvm.so 内の関数。この先は C++) の中から呼ばれています。

os.cpp を見ると、以下のようなリストでハードコードされています。ホルダ “%” が、システムプロパティ “java.home” に置換されるのでしょう。

bool os::set_boot_path(char fileSep, char pathSep) {
    ...
    static const char classpath_format[] =
        "%/lib/resources.jar:"
        "%/lib/rt.jar:"
        "%/lib/sunrsasign.jar:"
        "%/lib/jsse.jar:"
        "%/lib/jce.jar:"
        "%/lib/charsets.jar:"
        "%/classes";
    char* sysclasspath = \
     format_boot_path(classpath_format, home, home_len, fileSep, pathSep);
    if (sysclasspath == NULL) return false;
    Arguments::set_sysclasspath(sysclasspath);

    return true;
}

だいぶ構造が見えてきました。では。




Java 標準ライブラリをハックする

Java で、標準ライブラリのコピーをソースから作り、好きにイジる手順です。どなたか、他にもっと効率的な方法をご存知でしたら教えていただけるとありがたいです (その意味では Python は、自己責任において、アクセス指定関係なくクラス内部を触り放題なので、好もしい)。

まずは、こんなソースを用意します (注: コンパイルできません):

import java.util.regex.*;

class RegExpDebug  {
    public static void main(String args[]) {
        Pattern pattern = Pattern.compile(args[0]);
        pattern.printObjectTree(pattern.matchRoot);
        Matcher matcher = pattern.matcher(args[1]);
        while (matcher.find()) {
            System.out.println("Matched: " + matcher.group());
        }
    }
}

引数に渡された文字列で正規表現マッチングを行なうだけの、簡単な Java のコードです。Pattern.compile() に渡された正規表現が内部でどのような構造になっているかを表示する Pattern.printObjectTree() を呼びたいのですが、パッケージプライベートのデバッグ用なので、こちらからは呼べません。当然、コンパイルできません:

$ CLASSPATH=./ javac RegExpDebug.java
RegExpDebug.java:6: java.util.regex.Pattern の matchRoot は public ではありません。
 パッケージ外からはアクセスできません。
        pattern.printObjectTree(pattern.matchRoot);
                                       ^
RegExpDebug.java:6: printObjectTree(java.util.regex.Pattern.Node) は
 java.util.regex.Pattern で private アクセスされます。
        pattern.printObjectTree(pattern.matchRoot);
               ^
エラー 2 個
$

仕方が無いので、パッケージ名に適当なプレフィクス (ここでは “com.ayutaya”) をつけて、新しいパッケージとしてコピーを作り、コンパイル終了の際に Pattern.printObjectTree() を呼ぶようにライブラリ側を修正します。新しい呼び出し側は、以下のような感じです:

import com.ayutaya.java.util.regex.*;

class RegExpDebug  {
    pubelic static void main(String args[]) {
        Pattern pattern = Pattern.compile(args[0]);
        Matcher matcher = pattern.matcher(args[1]);
        while (matcher.find()) {
            System.out.println("Matched: " + matcher.group());
        }
    }
}

当然、まだパッケージがないのでコンパイルはコケます:

$ CLASSPATH=./ javac RegExpDebug.java
RegExpDebug.java:1: パッケージ com.ayutaya.java.util.regex は存在しません。
(中略)
$

パッケージのコピーを作成します。下記の例は、CentOS-5.5 上でのものですので、適宜読み替えてください。まずは、ライブラリのソースを入手します。Red Hat 系の OpenJDK ですと、java-*-openjdk-src に入っていると思います。

$ sudo yum install -y java-1.6.0-openjdk-src
(中略)
$ rpm -ql java-1.6.0-openjdk-src
/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip
/usr/share/doc/java-1.6.0-openjdk-src-1.6.0.0
/usr/share/doc/java-1.6.0-openjdk-src-1.6.0.0/README.src
$

コピーを作ってからパッケージ名を変更し、修正・コンパイルします。

$ mkdir -p com/ayutaya/
$ pushd com/ayutaya/
~/src/java/regexp/com/ayutaya ~/src/java/regexp
$ unzip /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip "java/util/regex/*"
Archive:  /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip
   creating: java/util/regex/
  inflating: java/util/regex/ASCII.java
  inflating: java/util/regex/Matcher.java
  inflating: java/util/regex/MatchResult.java
  inflating: java/util/regex/Pattern.java
  inflating: java/util/regex/PatternSyntaxException.java
$ cd java/util/regex/
$ perl -p -i \
 -e 's/package java.util.regex;/package com.ayutaya.java.util.regex;/' *.java
$ cp Pattern.java Pattern.java.orig
$ vi Pattern.java
$ diff -uNr Pattern.java.orig Pattern.java
--- Pattern.java.orig   2010-09-12 00:31:46.000000000 +0900
+++ Pattern.java        2010-09-12 00:32:29.000000000 +0900
@@ -1503,6 +1503,7 @@
         groupNodes = null;
         patternLength = 0;
         compiled = true;
+        printObjectTree(matchRoot);
     }

     /**
$ javac *.java
(中略)
$ popd
~/src/java/regexp
$

今度は無事コンパイルが通り、実行すると、内部構造がダンプされるようになります。

$ CLASSPATH=./ javac RegExpDebug.java
$ CLASSPATH=./ java RegExpDebug "a+" "xxxaaaxxx"
com.ayutaya.java.util.regex.Pattern$Curly@fa3ac1
com.ayutaya.java.util.regex.Pattern$Single@276af2
->next:
Accept Node
**** end contents Curly body
->next:
com.ayutaya.java.util.regex.Pattern$LastNode@1de3f2d
->next:
Accept Node
Matched: aaa
$

後は、やり放題です。では。




CentOS: SRPM, debuginfo 入手

備忘録として、CentOS で、yum の SRPM レポジトリと debuginfo RPM レポジトリを登録する方法です。Fedora ならば普通に使えるのに、CentOS だと入っていないんですよね。開発に使うな、ランタイムとしてだけ使え、という意図でしょう。

[root@black-server ~] cat <<'EOF' > /etc/yum.repos.d/CentOS-Source.repo
[base-source]
name=CentOS-$releasever - Base Source Packages
baseurl=http://mirror.centos.org/centos/$releasever/os/SRPMS/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-$releasever

[updates-source]
name=CentOS-$releasever - Updates Source Packages
baseurl=http://mirror.centos.org/centos/$releasever/updates/SRPMS/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-$releasever
EOF
[root@black-server ~]# yum install -y yum-utils
(中略)
[root@black-server ~]#

あとは一般ユーザでも、以下のようにすれば SRPM をダウンロード、リビルドすることができます。

$ yumdownloader --source bash
(中略)
$ ls -l bash-*.src.rpm
-rw-r--r-- 1 knaka knaka 4800501  4月 10  2009 bash-3.2-24.el5.src.rpm
$ rpmbuild --rebuild bash-3.2-24.el5.src.rpm
(中略)
$

ついでに、debuginfo パッケージの yum レポジトリも追加します。

[root@black-server ~] cat <<'EOF' > /etc/yum.repos.d/CentOS-Debug.repo
[debuginfo]
name=CentOS-$releasever - DebugInfo
baseurl=http://debuginfo.centos.org/$releasever/$basearch/
gpgcheck=0
enabled=0
EOF
[root@black-server ~]

debug パッケージは GPG サインが施されていませんので、gpgcheck はオフにします。”enabled=0″ で、標準では disable にしてありますので、利用の際には以下のようにします。

[root@black-server ~]# yum --enablerepo=debuginfo install -y bash-debuginfo
(中略)
[root@black-server ~]# rpm -q bash-debuginfo
bash-debuginfo-3.2-21.el5
[root@black-server ~]#

ただいかんせん、debuginfo パッケージが、updates に完全には追随していないんですよね…。

0003622: debuginfo packages out of date – CentOS Bug Tracker

そんなときは、base のパッケージに戻すか、自分でソースからコンパイルするかしましょう。Red Hat Enterprise Linux (RHEL) はちゃんと更新していますので、RHEL のサブスクリプションを買うという手もありますw。




正規表現で最長一致(する|しない)言語

先日、Java の java.util.regex.Pattern のソースを読みながら、twitter で以下のようにつぶやきました:

正規表現の大原則は最左最長一致だと思ってたんですが、Javaの”|”(Branch)のmatch()がatoms間の比較をしていない。どう見ても「最初一致」です。えー、これってOKなの?

すると、@buri17 からツッコミが:

@knaka perlでも正規表現の選択演算子は短絡評価だというのを昔どこかで読んだ気がするけど、ちがいましたかー。

えっ? マジですか? 調べてみました。

最長一致するもの

grep:

$ echo "XaabbX" | grep -o -E "a+|[ab]+"
aabb
$ echo "XaabbX" | grep -o -E "[ab]+|a+"
aabb

sed:

$ echo "XaabbX" | sed -r -e 's/a+|[ab]+/foo/'
XfooX
$ echo "XaabbX" | sed -r -e 's/[ab]+|a+/foo/'
XfooX

gawk:

$ echo "XaabbX" | gawk '{ sub("a+|[ab]+", "foo"); print }'
XfooX
$ echo "XaabbX" | gawk '{ sub("[ab]+|a+", "foo"); print }'
XfooX

ちなみに、秀丸エディタの正規表現検索でも最長一致します。正規表現ならば ED(1) だろって? すまん、使ったこともない。

最長一致しないもの

Perl:

$ echo "XaabbX" | perl -n -e 's/a+|[ab]+/foo/; print'
XfoobbX
$ echo "XaabbX" | perl -n -e 's/[ab]+|a+/foo/; print'
XfooX

Python:

$ python -c 'import re; print re.compile("a+|[ab]+").search("XaabbX").group(0)'
aa
$ python -c 'import re; print re.compile("[ab]+|a+").search("XaabbX").group(0)'
aabb

Ruby:

$ ruby -e 'puts /a+|[ab]+/.match("XaabbX")'
aa
$ ruby -e 'puts /[ab]+|a+/.match("XaabbX")'
aabb

PHP:

$ php -r 'preg_match("/a+|[ab]+/", "XaabbX", $m); echo $m[0] . "\n";'
aa
$ php -r 'preg_match("/[ab]+|a+/", "XaabbX", $m); echo $m[0] . "\n";'
aabb

grep で、Perl 正規表現オプションつき (PCRE?):

$ echo "XaabbX" | grep -o --perl-regexp "a+|[ab]+"
aa
bb
$ echo "XaabbX" | grep -o --perl-regexp "[ab]+|a+"
aabb

ついでに、Emacs の検索も最長一致しません。

まとめると

当たり前だと思っていた正規表現の最左最長一致を、新しいめのツール・言語では行なっていないことが分かります。たしかに最長一致を探そうとすると、もし選択の中でマッチが見つかっても、最長のマッチを探すために他の全ての項も試さなければなりませんから、実装的にも実行時にも高コストなのは分かりますが、気をつけねばなりませんな。

古いめのツールでは行なうということは、これって POSIX の正規表現の規格にも反映されていたりするのか? と思って、PHP でも古い方の関数で試してみると、下記の通り、こちらは最長一致するんですよね。参考: 「PHP: POSIX 正規表現関数 – Manual

PHP の POSIX 正規表現関数 (PHP >= 5.3 非推奨):

$ php -r 'ereg("a+|[ab]+", "XaabbX", $m); echo $m[0] . "\n";'
aabb
$ php -r 'ereg("[ab]+|a+", "XaabbX", $m); echo $m[0] . "\n";'
aabb

そこで “POSIX 最長一致” でググると、こんな資料が → 「PHPの正規表現と
最長一致, hanawa (a.k.a. id:hnw), y at hnw dot jp, 第29回PHP勉強会発表資料
」 (PDF)。よく「最長マッチ」と誤訳される「Greedy-matching (繰り返しで最長まで食ってしまうマッチ)」と「Longest-matching (最長マッチ)」は別物よ、ということで、参考になりました。

では。




Ruby Mix-in で菱形継承

Ruby はご存知のとおり、is-a 関係で継承を行なう「普通の」継承と、付加機能としてインターフェイスと実装を引き継ぐためのミックスイン継承とを備えています。通常継承では単一継承しか許していない Ruby ですが、通常の継承とミックスインとの多重継承、ミックスインとミックスインとの多重継承はできます。オーバーライドした際の同一シグネチャメソッドの優先順位がどうなっているのか気になったので、やってみました。

予想としては、当然通常継承の方が優先度が高いでしょうし、ミックスイン間であれば、”include” などというくらいですから、当然後から include した方が上書きしそうです。実際そうでした。やるまでもなかったかも知れません。

#!/usr/bin/ruby
# -*- coding: utf-8 -*-

module BaseMixin
  def initialize
    print "BaseMixin initialized\n"
  end
end

module DerivedMixin1
  include BaseMixin
  def initialize
    super
    print "DerivedMixin1 initialized\n"
  end
  def foo
    print "foo@DerivedMixin1 called\n"
  end
  def bar
    print "bar@DerivedMixin1 called\n"
  end
end

module DerivedMixin2
  include BaseMixin
  def initialize
    super
    print "DerivedMixin2 initialized\n"
  end
  def foo
    print "foo@DerivedMixin2 called\n"
  end
  def bar
    print "bar@DerivedMixin2 called\n"
  end
end

class Base
  include DerivedMixin1
  include DerivedMixin2
  def initialize
    super
    print "Base initialized\n"
  end
end

class Derived < Base
  include DerivedMixin2
  include DerivedMixin1
  def initialize
    super
    print "Derived initialized\n"
  end
  def bar
    print "bar@Derived called\n"
  end
end

o = Derived.new
o.foo
o.bar

実行結果:

$ ./diamond.rb
BaseMixin initialized
DerivedMixin1 initialized
DerivedMixin2 initialized
Base initialized
Derived initialized
foo@DerivedMixin2 called
bar@Derived called

Mix-in のコンストラクタは include した順(登録された順?)に実行され、その後で親クラスのコンストラクタが走ります。基底と派生とで include していたら、基底で include された方が優先で、派生の方での include は無視。これも妥当と思われる。

名前かぶりは、後 include の Mix-in 優先、基底クラスがより優先、は予想通り。

型も動的なので、菱形継承した基底の “BaseMixin” のインスタンスは一つしかありませんのね、C++ で言うところの “virtual public” 継承。初期化パラメータが両経路で異なったりすると混乱しそうなので、Mix-in の基底には複雑なことはやらせないようにしようと思います。Python も確かこんなだったような気がします。今度やってみます。

では。




マークアップ言語主要書式

混乱したり忘れたりをよくするので、各種マークアップ言語における、主要な書式をまとめておきます。まともな構造化文書を書くのに、下記の要素程度をおさえておけばだいたい足りると思うんですよね (数式や楽譜は別)。

  • 表題の階層
  • 段落
  • 整形なし
  • 順序なし箇条書き
  • 順序あり箇条書き
  • 画像の貼り付け
  • URL リンク
  • 独自リンク
  • 表組み

# roff (nroff) も要ります??

LaTeX

  • 表題の階層: \section{~}, \subsection{~}, \subsubsection{~}
  • 段落: 一行以上あける
  • 整形なし: \begin{verbatim}
  • 順序なし箇条書き: \begin{itemize} → \item
  • 順序あり箇条書き: \begin{enumerate} → \item
  • 画像の貼り付け: \usepackage{graphicx} \includegraphics{~.eps}
  • URL リンク: N/A
  • 独自リンク: N/A
  • 表組み:
\begin{tabular}{lll}
\hline
header 1 & header 2 & header 3\\
\hline \\
data 1 & data 2 & data 3 \\
\hline
\end{tabular}

HTML

  • 表題の階層: <h1>~<h6>
  • 段落: <p>
  • 整形なし: <pre>
  • 順序なし箇条書き: <ul> → <li>
  • 順序あり箇条書き: <ol> → <li>
  • 画像の貼り付け: <img>
  • URL リンク: <a>
  • 独自リンク: N/A
  • 表組み:
<table border=1>
  <caption align=bottom>caption</caption>
  <tr><th>header 1</th><th>header 2</th><th>header 3</th></tr>
  <tr><th>data 1</th><td>data 2</td><td>data 3</td></tr>
  <tr><td rowspan=2>data 1</td><td colspan=2>data 2</td></tr>
  <tr><td>data 2</td><td>data 3</td></tr>
</table>

PukiWiki

  • 表題の階層: *, **, ***
  • 段落: 一行以上あける
  • 整形なし: 1 スペース下げ
  • 順序なし箇条書き: -, –, —
  • 順序あり箇条書き: +, ++, +++
  • 画像の貼り付け: &ref(添付ファイル名), &ref(URL)
  • URL リンク: [[エイリアス名>URL]]
  • 独自リンク: [[ページ名]]
  • 表組み:
|CENTER: header 1|CENTER: header 2|CENTER: header3|h
|RIGHT:|LEFT:|LEFT:|c
|~data 1|data 2|data 3|
|data 1|>|data 3|
|~|data 2|data 3|

MediaWiki

  • 表題の階層: =1=, ==2==, ===3===, ====4====, =====5=====, ======6======
  • 整形なし: 1 スペース下げ
  • 段落: 一行以上あける
  • 順序なし箇条書き: * を連ねる
  • 順序あり箇条書き: # を連ねる
  • 画像の貼り付け: [[File:ファイル名]]
  • URL リンク: [URL エイリアス名]
  • 独自リンク: [[ページ名]]
  • 表組み:
{| border="1"
|+ align="bottom" | caption
!header 1
!header 2
!header 3
|-
|data 1
|data 2
|data 3
|}

WordPress (HTML 編集モード)

  • 表題の階層: サイト名が <h1>、表題が <h2> を用いるので、本文は <h3> から
  • 段落: 改行する
  • (以下は “HTML” と同様)



ワイルドカード関数を書く

こんなお題が出たことがあります:

  • ワイルドカードによるマッチングを行なう関数を書きなさい
  • 引数には、ワイルドカードパターンとマッチ対象文字列をとり、成否を返します
  • ワイルドカードのルールは以下のとおり:
    • “?” は、任意の 1 文字にマッチします
    • “*” は、0 文字以上の文字列にマッチします
    • それ以外の文字は、その文字自身にマッチします

まず単純に 1 パスのループで回しながらマッチングをすることを考えて、すぐに “*” の処理ができないことに気づいて行きづまりました。そして、一足とびに、正規表現マッチングのコードを思い出してしまったために、頭はすっかり非決定性有限オートマトンから決定性有限オートマトンを生成する方法ってどうやるんだったっけな、と明後日の方向へ行ってしまいました。

けれどもよく考えたら、この程度のワイルドカードならばパーサは必要ないわけです。一般化すれば、「少なくとも一つの解を見つければ OK」という典型的な問題なんですよね。

そんなわけで、ちょっと書いてみました。キモは、1 つの “*” が、対象文字列の 0 文字から残り全部の文字までを食った場合のそれぞれごとに再帰してバックトラック、というのに気づけるかどうか。

#include <stdio.h>

const int Success = 1;
const int Failure = 0;

int
match (
  const char * pattern,
  const char * haystack ) {
    while ((* pattern) || (* haystack)) {
        // 先にどちらか尽きたら失敗
        if ((! (* pattern)) || (! (* haystack))) {
            return (Failure);
        }
        // 今回の "*" が 0 文字から残り全ての文字を食い尽くした場合ごと
        //  に再帰
        if ((* pattern) == '*') {
            for (int i = 0; (* (haystack + i)); ++ i) {
                if (match(pattern + 1, haystack + i) == Success) {
                    return (Success);
                }
            }
            return (Failure);
        // 1 文字ずつのマッチは簡単
        } else if (
         ((* pattern) == (* haystack)) ||
         ((* pattern) == '?') ) {
            pattern ++;
            haystack ++;
        // 1 文字単位で不一致ならば失敗
        } else {
            return (Failure);
        }
    }
    // 共に尽きたら成功
    return (Success);
}

int
main (
  int argc,
  char * * argv) {
    if ((match(argv[1], argv[2])) == Failure) {
        printf("Not matched\n");
        return (1);
    }
    printf("Matched\n");
    return (0);
}

精進せねば。




一般ユーザで自前 MySQL

RPM で入れた MySQL のデータベースを、一般ユーザの自前権限のインスタンスで走らせる方法です。備忘録として書いておきます。

まずはパッケージをインストールします。

[root@queen-centos2 ~]# yum install -y mysql-server

デフォルトポートの 3306 ではなく、一般ユーザ用に一つずらして 3307 を開く予定なので、ファイアウォールに穴をあけ、save しておきます。

[root@queen-centos2 ~]# iptables -I INPUT -p tcp --dport 3307 -j ACCEPT
[root@queen-centos2 ~]# service iptables save

以下の操作は、一般ユーザで行ないます。まずは、データディレクトリを作成します。設定は、ポート番号とソケットファイルの位置を変更します。

[knaka@queen-centos2 ~]$ mysql_install_db --datadir=$HOME/mydata-main/
[knaka@queen-centos2 ~]$ cp /usr/share/mysql/my-medium.cnf $HOME/mydata-main/my.cnf
[knaka@queen-centos2 ~]$ vi ~/mydata-main/my.cnf
[knaka@queen-centos2 ~]$ diff -uNr /usr/share/mysql/my-medium.cnf \
 $HOME/mydata-main/my.cnf
--- /usr/share/mysql/my-medium.cnf      2010-05-28 10:07:48.000000000 +0900
+++ /home/knaka/mydata-main/my.cnf   2010-09-04 12:52:51.000000000 +0900
@@ -24,8 +24,8 @@

 # The MySQL server
 [mysqld]
-port           = 3306
-socket         = /var/lib/mysql/mysql.sock
+port           = 3307
+socket         = /home/knaka/mydata-main/mysql.sock
 skip-locking
 key_buffer = 16M
 max_allowed_packet = 1M
[knaka@queen-centos2 ~]$ mysqld_safe --defaults-file=$HOME/mydata-main/my.cnf \
  --datadir=$HOME/mydata-main/ --log-error=$HOME/mydata-main/error.log

MySQL のデフォルトの特権 DB ユーザ “root” は、システムの root ユーザとまぎらわしくて好きではないので、”admin” ユーザを作成し、匿名ユーザを消してしまいます。DB のデータ操作でユーザ情報を変更した後には、”flush privileges” によって DB システムに反映させる必要があるそうです。

[knaka@queen-centos2 ~]$ mysql --user=root \
 --socket=/home/knaka/mydata-main/mysql.sock mysql
mysql> create user admin identified by 'admin';
mysql> grant all privileges on *.* to 'admin'@'%';
mysql> delete from user where user = '';
mysql> flush privileges;
mysql> quit

続けて “admin” ユーザで入り、”root” を消します。

[knaka@queen-centos2 ~]$ mysql --user=admin \
 --socket=/home/knaka/mydata-main/mysql.sock mysql -p
mysql> delete from user where user = 'root';
mysql> flush privileges;
mysql> quit

リモートからのアクセスを試みます。

[knaka@luminous.priv ~]$ mysql -h queen-centos2.priv -u admin -P 3307 -p

最後に、インスタンス起動を永続化しておきます。”crontab -e” を実行し、以下を入力します (実際には一行で)。”@reboot” は、最近めの cron でないとサポートしていないかも知れませんので、man で見ておいてください。

@reboot /usr/bin/mysqld_safe --defaults-file=/home/knaka/mydata-main/my.cnf
 --datadir=/home/knaka/mydata-main/ --log-error=/home/knaka/mydata-main/error.log

再起動してアクセスできれば OK。

PostgreSQL に比べて面倒だし、データの可搬性が低いですね。大した問題ではありませんが。




EPEL の「きれいな」入れ方

ご存知のように EPEL は、FedoraRHEL 化される際に取り込まれなかったパッケージ群を RHEL 用に提供しているパッケージ・レポジトリです。RHEL や CentOS 上で、比較的マイナーなパッケージを利用したい時に便利です。

ところで、FAQ に書かれている yum レポジトリ設定のインストール方法では、アーキテクチャやバージョン、リリースを指定しなければならないところが環境依存なので、ちょっといやな感じです。

su -c 'rpm -Uvh http://~/pub/epel/5/i386/epel-release-5-4.noarch.rpm'
...
su -c 'yum install foo'

そこで、rpm コマンドでは、パッケージ URL 内のワイルドカードがリモート側での globbing で展開される (ただし FTP や WebDAV 等の listing ができるプロトコルに限る) のを利用して、以下のようにすれば環境依存しづらいと思います。

rpm -Uvh $(printf \
 ftp://download.fedora.redhat.com/pub/epel/%s/%s/epel-release-*-*.noarch.rpm \
 $(rpm -q --qf "%{version}" $(rpm -q --whatprovides redhat-release)) \
 $(uname --hardware-platform) )

では。




WordPress へ移行

旧サイトで、長いこと PHP のフルスクラッチでゴリゴリとやってきましたが、さすがにそろそろ限界ぎみなので、サーバ移転 (VM 化) を機に WordPress へ移行しました。

今後ともよろしくお願いします。

Sun Jan 23 2011: 8 コア Ubuntu へ移行。




陽の光浴びる一輪の花

向日葵を描きました。