前職在籍中の主要記事へのリンク




Xen 4 で Scientific Linux 6 Dom0

Xen (Xen-PV) は良いです。何が良いって、VT/AMD-V が必須の KVM や Xen-HVM と違って Paravirtualization でも動くので、最近流行りの IaaS のテストなども、VM 上でおこなうことができるところです。(KVM に比べて、サポートが手薄な感は拭えませんが)

RHEL5/CentOS5 では、インストール中に “Virtualization” サポートのパッケージグループを有効にするだけで Xen の Dom0 がインストールできて簡単でした。ところが RHEL6/SL6 (Scientific Linux 6) では、Red Hat による KVM 推しのせいか Xen Dom0 が除外されてしまいました。実運用に使うわけではないので品質は問わないので、SL6 を Xen Dom0 化してみました。

RHEL6 では、Dom0 が除外されただけです。ノーマルカーネルは Xen pv_ops オプション付きでビルドされているので、そのまま Xen DomU にはなれます。

なお、今回は本家の記事がネタ元。参考: “RHEL6Xen4Tutorial – Xen Wiki“。

前準備

まず、distro をインストールする先の VM の設定を変えます。KVM であれば、ストレージやネットワークに virtio を使わず、IDE や SCSI、RTL-8139 や E1000 あたりを利用した方が無難です。別段、Dom0 が virtio を使っちゃいかんこともないと思うのですが、起動・認識しませんでした。カーネルの config 等まで、よくは見ていません。VMware の ESXi でも同様に、SCSI は準仮想化を切り、LSI Logic PATA あたりに設定します (これまた起動しなくなります)。ターゲットとして「RHEL6」等の新しい distro を選択すると virtio を有効にされてしまうこともあるので注意します。

以下の例は、KVM にインストールした SL6 (“Desktop”) です。ありモノのパッケージを使う場合には /boot/ のサイズはそれほど要らないのですが、自分でビルドする場合には、巨大な全部入り initramfs を収めることになりますので、/boot/ に 1~2GB ほど割り当てておきます。それと、”Desktop” インストールをした後でビルドを行なうと、ざっと 12~13GB にはなります。

以下、ほめられたことではありませんが、面倒なのですべて root で作業します。SELinux と iptables も off にします。

[root@sl6x ~]# chkconfig iptables off
[root@sl6x ~]# cp /etc/sysconfig/selinux /etc/sysconfig/selinux.orig
[root@sl6x ~]# vi /etc/sysconfig/selinux
[root@sl6x ~]# diff -uNr /etc/sysconfig/selinux.orig /etc/sysconfig/selinux
--- /etc/sysconfig/selinux.orig
+++ /etc/sysconfig/selinux
@@ -4,7 +4,7 @@
 #     enforcing - SELinux security policy is enforced.
 #     permissive - SELinux prints warnings instead of enforcing.
 #     disabled - No SELinux policy is loaded.
-SELINUX=enforcing
+SELINUX=disabled
 # SELINUXTYPE= can take one of these two values:
 #     targeted - Targeted processes are protected,
 #     mls - Multi Level Security protection.

下記の組み合わせ (特に「ネット上で配布されているバイナリを利用する」場合) で、libvirt の virbr0 (NAT) 経由で Fedora 14 や SL6 を入れようとすると、どうも pv_ops な DomU のネットワークが不安定なのか、不定なタイミングでインストールがコケます。カーネルだけは手で入れた方が無難かも知れません。あるいは、下記のようにして直結ブリッジを作っておきます。(xenbr0 や peth0 が見当たりません。Xen 4 になって、見せないようになった?)

なお、”NM_CONTROLLED” の行を “no” にするやいなや NetworkManager はインターフェイスを off にしますので、リモートから編集していると切られます。

[root@sl6x ~]# cd /etc/sysconfig/network-scripts/
[root@sl6x network-scripts]# cp ifcfg-eth0 /tmp/ifcfg-eth0
[root@sl6x network-scripts]# cp ifcfg-eth0 ifcfg-br0
[root@sl6x network-scripts]# vi ifcfg-eth0
[root@sl6x network-scripts]# vi ifcfg-br0
[root@sl6x network-scripts]# diff -uNr /tmp/ifcfg-eth0 ifcfg-eth0
--- /tmp/ifcfg-eth0
+++ ifcfg-eth0
@@ -1,5 +1,4 @@
 DEVICE="eth0"
-BOOTPROTO="dhcp"
-HWADDR="XX:XX:XX:XX:XX:XX"
-NM_CONTROLLED="yes"
+NM_CONTROLLED="no"
 ONBOOT="yes"
+BRIDGE="br0"
[root@sl6x network-scripts]# diff -uNr /tmp/ifcfg-eth0 ifcfg-br0
--- /tmp/ifcfg-eth0
+++ ifcfg-br0
@@ -1,5 +1,5 @@
-DEVICE="eth0"
+DEVICE="br0"
 BOOTPROTO="dhcp"
-HWADDR="XX:XX:XX:XX:XX:XX"
-NM_CONTROLLED="yes"
+NM_CONTROLLED="no"
 ONBOOT="yes"
+TYPE="Bridge"
[root@sl6x network-scripts]# cd
[root@sl6x ~]# service network restart

計算ノードとして VM イメージをコピーする際には、当然 NIC の MAC アドレスも変わります。そのため、MAC アドレスを記憶されてしまうと不都合ですので、消して、二度と書かれないようにしておきます。参考: “Disable automatic udev rules for network interfaces in Ubuntu << six degrees of freedom“。

[root@sl6x ~]# rm -f /etc/udev/rules.d/70-persistent-net.rules
[root@sl6x ~]# mkdir /etc/udev/rules.d/70-persistent-net.rules
[root@sl6x ~]#

SELinux もオフにしたことですし、ここらで一度リブートしておきますか。

[root@sl6x ~]# reboot

ネット上で配布されているバイナリを利用する場合

コミュニティビルドのパッケージです。ドイツ語は読めませんが、ありがたく利用させていただきます。

[root@sl6x ~]# ( cd /etc/yum.repos.d/ ; wget \
 http://www.gitco.de/linux/x86_64/centos/6/gitco-centos6-x86_64.repo )
(中略)
2011-05-21 15:17:02 (62.2 MB/s) - “gitco-centos6-x86_64.repo” saved [607/607]
[root@sl6x ~]# yum install -y kernel # ← Dom0 カーネルが入らなければ要調整
(中略)
Installed:
  kernel.x86_64 0:2.6.32.26-174.1.xendom0.el6

Dependency Updated:
  kernel-firmware.x86_64 0:2.6.32.26-174.1.xendom0.el6

Complete!
[root@sl6x ~]# yum install -y \
 xen xen-hypervisor xen-libs xen-licenses xen-runtime
(中略)
Installed:
  xen.x86_64 0:4.0.1-6.1.el6            xen-hypervisor.x86_64 0:4.0.1-6.1.el6
  xen-libs.x86_64 0:4.0.1-6.1.el6       xen-licenses.x86_64 0:4.0.1-6.1.el6
  xen-runtime.x86_64 0:4.0.1-6.1.el6

Dependency Installed:
  PyXML.x86_64 0:0.8.4-19.el6              SDL.x86_64 0:1.2.14-2.el6
  qemu-common.x86_64 2:0.12.5-1.el6        qemu-img.x86_64 2:0.12.5-1.el6

Complete!
[root@sl6x ~]#

上記の手順で、カーネルよりも後で xen-runtime を入れることで、postinstall のスクリプトで grub.conf を適切に編集してくれます。でもこれ、本来は trigger スクリプトでやるべきなのでは? もし xen.gz のエントリが書かれていないようであれば、以下のように手で書き換えます。

[root@sl6x ~]# cp /boot/grub/grub.conf /boot/grub/grub.conf.orig
[root@sl6x ~]# vi /boot/grub/grub.conf
[root@sl6x ~]# diff -uNr /boot/grub/grub.conf.orig /boot/grub/grub.conf
--- /boot/grub/grub.conf.orig
+++ /boot/grub/grub.conf
@@ -13,8 +13,9 @@
 hiddenmenu
 title Scientific Linux (2.6.32.26-174.1.xendom0.el6.x86_64)
        root (hd0,0)
-       kernel /vmlinuz-2.6.32.26-174.1.xendom0.el6.x86_64 ro (中略)
-       initrd /initramfs-2.6.32.26-174.1.xendom0.el6.x86_64.img
+       kernel=/xen.gz
+       module /vmlinuz-2.6.32.26-174.1.xendom0.el6.x86_64 ro (中略)
+       module /initramfs-2.6.32.26-174.1.xendom0.el6.x86_64.img
 title Scientific Linux (2.6.32-71.24.1.el6.x86_64)
        root (hd0,0)
        kernel /vmlinuz-2.6.32-71.24.1.el6.x86_64 ro (中略)
[root@sl6x ~]#

Xen で KSM (Kernel Samepage Merging) は未サポートらしいので ksm と ksmtuned も切っておきます。

[root@sl6x ~]# chkconfig ksm off
[root@sl6x ~]# chkconfig ksmtuned off
[root@sl6x ~]#

Xen を、libvirt 経由で使います。UI としては、virt-manager が良いでしょう。python-virtinst を使いたかったのですが、Xen 4 に追随していないのか、blktap2 まわりで正しく動きませんでした。

[root@sl6x ~]# yum install -y virt-manager libvirt
(中略)
Installed:
  libvirt.x86_64 0:0.8.1-27.1.el6       virt-manager.noarch 0:0.8.4-8.el6

Dependency Installed:
  augeas-libs.x86_64 0:0.7.2-3.el6
  ebtables.x86_64 0:2.0.9-5.el6
  gtk-vnc.x86_64 0:0.3.10-3.el6
  gtk-vnc-python.x86_64 0:0.3.10-3.el6
  iscsi-initiator-utils.x86_64 0:6.2.0.872-10.el6
  libvirt-client.x86_64 0:0.8.1-27.1.el6
  libvirt-python.x86_64 0:0.8.1-27.1.el6
  lzo.x86_64 0:2.03-3.1.el6
  lzop.x86_64 0:1.02-0.9.rc1.el6
  nc.x86_64 0:1.84-22.el6
  netcf-libs.x86_64 0:0.1.6-4.el6
  numactl.x86_64 0:2.0.3-9.el6
  python-virtinst.noarch 0:0.500.3-7.el6
  yajl.x86_64 0:1.0.7-3.el6

Complete!
[root@sl6x ~]#

以上です。再起動して、Xen + Dom0 でブートします。

[root@sl6x ~]# reboot

再起動したら、とりあえず Dom0 で動いていることを確認してみます。

[root@sl6x ~]# virsh list
 Id Name                 State
----------------------------------
  0 Domain-0             running

[root@sl6x ~]#

テストとして、Virt-Manager から Scientific Linux 6 を ttp://ftp.riken.jp/Linux/scientific/6.0/x86_64/os/ からネットワークインストールしてみます。良いようです。ttp://rsync.atworks.co.jp/centos/5/os/i386/ も行けました。

ところで、UltraVNC Win32 Viewer で QEMU-KVM に繋いだ中で Virt-Manager の VNC クライアントが上がると、とても見づらい画面になるのは私だけ? VNC クライアントのせいかしら。

自前でビルドする場合

まずは、標準レポジトリのパッケージをインストールします。

[root@sl6xdev ~]# yum groupinstall -y \
 "Development tools" \
 "Additional Development" "Debugging Tools" \
 "System administration tools" \
 "Compatibility libraries" "Console internet tools" \
 "Desktop Platform Development"
(中略)
Complete!
[root@sl6xdev ~]# yum install -y \
 transfig wget texi2html libaio-devel dev86 \
 glibc-devel e2fsprogs-devel gitk mkinitrd iasl xz-devel \
 bzip2-devel pciutils-libs pciutils-devel SDL-devel \
 libX11-devel gtk2-devel bridge-utils PyXML qemu-common \
 qemu-img mercurial texinfo libuuid-devel
(中略)
Complete!
[root@sl6xdev ~]# yum install -y glibc-devel.i686
(中略)
Complete!
[root@sl6xdev ~]#

Xen 4 のビルドとインストールをします。

[root@sl6xdev ~]# host=ftp-srv2.kddilabs.jp
[root@sl6xdev ~]# path=/Linux/packages/fedora/releases/14/Everything/source/SRPMS
[root@sl6xdev ~]# ver=4.0.1-6.fc14
[root@sl6xdev ~]# wget http://$host/$path/xen-$ver.src.rpm
(中略)
[root@sl6xdev ~]# rpmbuild --rebuild xen-$ver.src.rpm
(中略)
Wrote: /root/rpmbuild/RPMS/x86_64/xen-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-libs-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-runtime-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-hypervisor-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-doc-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-devel-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-licenses-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-debuginfo-4.0.1-6.el6.x86_64.rpm
(中略)
+ exit 0
[root@sl6xdev ~]# path=Linux/packages/fedora/releases/13/Fedora/source/SRPMS
[root@sl6xdev ~]# ver=0.12.3-8.fc13
[root@sl6xdev ~]# http://$host/$path/qemu-$ver.src.rpm
(中略)
[root@sl6xdev ~]# rpmbuild --rebuild qemu-$ver.src.rpm
(中略)
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-kvm-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-img-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-common-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-user-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-x86-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-ppc-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-sparc-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-arm-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-mips-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-cris-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-m68k-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-sh4-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-kvm-tools-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-debuginfo-0.12.3-8.el6.x86_64.rpm
(中略)
+ exit 0
[root@sl6xdev ~]# rpm -Uvh \
 ~/rpmbuild/RPMS/x86_64/xen-runtime-4* \
 ~/rpmbuild/RPMS/x86_64/xen-libs-4* \
 ~/rpmbuild/RPMS/x86_64/xen-licenses-4* \
 ~/rpmbuild/RPMS/x86_64/xen-4* \
 ~/rpmbuild/RPMS/x86_64/xen-hypervisor-4* \
 ~/rpmbuild/RPMS/x86_64/qemu-common-*
(中略)
[root@sl6xdev ~]# chkconfig ksm off
[root@sl6xdev ~]# chkconfig ksmtuned off
[root@sl6xdev ~]#

Xen Dom0 カーネルのビルドとインストールをします。

[root@sl6xdev ~]# git clone \
 git://git.kernel.org/pub/scm/linux/kernel/git/jeremy/xen.git linux-2.6-xen
Initialized empty Git repository in /root/linux-2.6-xen/.git/
(中略)
[root@sl6xdev ~]# cd linux-2.6-xen
[root@sl6xdev linux-2.6-xen]# host=pasik.reaktio.net
[root@sl6xdev linux-2.6-xen]# wget -O .config \

http://$host/xen/kernel-config/config-2.6.32.25-pvops-dom0-xen-stable-x86_64

(中略)
[root@sl6xdev linux-2.6-xen]# make oldconfig
(質問が出たら答えます)
[root@sl6xdev linux-2.6-xen]# jobs=$(grep ^processor /proc/cpuinfo | wc -l)
[root@sl6xdev linux-2.6-xen]# make -j$jobs bzImage && \
 make -j$jobs modules && make modules_install
(中略)
  INSTALL /lib/firmware/cpia2/stv0672_vp4.bin
  INSTALL /lib/firmware/yam/1200.bin
  INSTALL /lib/firmware/yam/9600.bin
  DEPMOD  2.6.32.40
[root@sl6xdev linux-2.6-xen]# ver=$(make kernelversion)
[root@sl6xdev linux-2.6-xen]# depmod -a $ver
[root@sl6xdev linux-2.6-xen]# cp -a arch/x86/boot/bzImage /boot/vmlinuz-$ver
[root@sl6xdev linux-2.6-xen]# cp -a System.map /boot/System.map-$ver
[root@sl6xdev linux-2.6-xen]# cp -a .config /boot/config-$ver
[root@sl6xdev linux-2.6-xen]# cd /boot/
[root@sl6xdev boot]# dracut initramfs-$ver.img $ver
[root@sl6xdev boot]# cp /boot/grub/grub.conf /boot/grub/grub.conf.orig
[root@sl6xdev boot]# vi /boot/grub/grub.conf
[root@sl6xdev boot]# diff -uNr /boot/grub/grub.conf.orig /boot/grub/grub.conf
--- /boot/grub/grub.conf.orig
+++ /boot/grub/grub.conf
@@ -11,6 +11,11 @@
 timeout=5
 splashimage=(hd0,0)/grub/splash.xpm.gz
 hiddenmenu
+title Scientific Linux Dom0
+        root (hd0,0)
+        kernel /xen.gz dom0_mem=1024M loglvl=all guest_loglvl=all
+        module /vmlinuz-2.6.32.40 ro root=(中略)
+        module /initramfs-2.6.32.40.img
 title Scientific Linux (2.6.32-71.24.1.el6.x86_64)
        root (hd0,0)
        kernel /vmlinuz-2.6.32-71.24.1.el6.x86_64 ro root=(中略)
[root@sl6xdev boot]# cd

標準の libvirt では Xen API サポートが落ちているので、有効にしてビルドします。Dom0 カーネル上でビルドするとチェックでコケるらしいので、一応テストをオフにしておきます。virt-manager も入れておきます。

[root@sl6xdev ~]# yumdownloader --source libvirt
(中略)
libvirt-0.8.1-27.el6_0.5.src.rpm                         |  11 MB     00:06
[root@sl6xdev ~]# rpm -i libvirt-*.src.rpm
[root@sl6xdev ~]# cp rpmbuild/SPECS/libvirt.spec \
 rpmbuild/SPECS/libvirt.spec.orig
[root@sl6xdev ~]# vi rpmbuild/SPECS/libvirt.spec
[root@sl6xdev ~]# diff -uNr rpmbuild/SPECS/libvirt.spec.orig \
 rpmbuild/SPECS/libvirt.spec
--- rpmbuild/SPECS/libvirt.spec.orig
+++ rpmbuild/SPECS/libvirt.spec
@@ -107,7 +107,7 @@
 %ifnarch x86_64
 %define with_qemu 0
 %endif
-%define with_xen 0
+%define with_xen 1
 %endif

 # If Xen isn't turned on, we shouldn't build the xen proxy either
@@ -1362,20 +1362,6 @@
 %clean
 rm -fr %{buildroot}

-%check
-cd tests
-# These 3 tests don't current work in a mock build root
-for i in nodeinfotest daemon-conf seclabeltest
-do
-  rm -f $i
-  echo -e "#!/bin/sh\nexit 0" > $i
-  chmod +x $i
-done
-# The test applied by patch need to be made executable
-chmod +x virsh-schedinfo
-
-make check
-
 %pre
 %if 0%{?fedora} >= 12 || 0%{?rhel} >= 6
 # Normally 'setup' adds this in /etc/passwd, but this is
[root@sl6xdev ~]# yum-builddep -y libvirt
(中略)
Complete!
[root@sl6xdev ~]# rpm -Uvh rpmbuild/RPMS/x86_64/xen-devel-*.rpm
[root@sl6xdev ~]# yum install -y libpcap-devel
(中略)
Complete!
[root@sl6xdev ~]# rpmbuild -ba rpmbuild/SPECS/libvirt.spec
(中略)
Wrote: /root/rpmbuild/SRPMS/libvirt-0.8.1-27.el6.5.src.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-client-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-devel-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-python-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-debuginfo-0.8.1-27.el6.5.x86_64.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.aEw602
+ umask 022
+ cd /root/rpmbuild/BUILD
+ cd libvirt-0.8.1
+ rm -fr /root/rpmbuild/BUILDROOT/libvirt-0.8.1-27.el6.5.x86_64
+ exit 0
[root@sl6xdev ~]# yum install -y nc ebtables lzop
(中略)
Complete!
[root@sl6xdev ~]# rpm -Uvh \
 rpmbuild/RPMS/x86_64/libvirt-client-0.8.1-27.el6.5.x86_64.rpm \
 rpmbuild/RPMS/x86_64/libvirt-0.8.1-27.el6.5.x86_64.rpm \
 rpmbuild/RPMS/x86_64/libvirt-python-0.8.1-27.el6.5.x86_64.rpm
[root@sl6xdev ~]# yum install -y virt-manager
(中略)
Complete!
[root@sl6xdev ~]#

リブートします。

[root@sl6xdev ~]# reboot

先の例と同様に、Dom0 で動作していることを確認し、適当にインストールをしてみます。良いようです。




Scala: Haskell 本の引き写しでパーサジェネレータ

プログラミング Haskell」の第 8 章「関数型パーサ」の写経をしようかと思ったのですが、そのまま引き写しても面白くないので、Scala で書いてみました。せっかくですので、Haskell のモナド連結の糖衣構文 do を、極力そのまま Scala の for 式で書けるよう、Parser[A] には、map(), flatMap() を備え、Scala の型とします。

…できた。Haskell 版から変えたところとしては、入力文字列にパーサを適用した際には、Scala らしく Option[(A, String)] を返すようにした、Haskell で [Char] であるところは、適宜 String にした、程度か。案外そのまま行けますね、というよりも、Scala が Haskell を色濃く反映しているんですね。

abstract trait Parser[A] {
  def apply(inp: String): Option[(A, String)]
  def parse(inp: String) = apply(inp)
  def map[B](f: (A) => B) = {
    def p_parse = parse _
    new Parser[B] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => None
          case Some((v, out)) => Some((f(v), out))
        }
      }
    }
  }
  def flatMap[B](f: (A) => Parser[B]): Parser[B] = {
    def p_parse = parse _
    new Parser[B] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => None
          case Some((v, out)) => f(v)(out)
        }
      }
    }
  }
  def +++(q: Parser[A]) = {
    def p_parse = parse _
    new Parser[A] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => q.parse(inp)
          case s @ Some(_) => s
        }
      }
    }
  }
}

def return_[A](v: A): Parser[A] = {
  new Parser[A] { def apply(inp: String) = { Some((v, inp)) } }
}

def failure[A]: Parser[A] = {
  new Parser[A] { def apply(inp: String) = { None } }
}

def item: Parser[Char] = new Parser[Char] {
  def apply(inp: String) = inp match {
    case "" => None
    case _ => Some((inp.head, inp.tail))
  }
}

def parse[A](p: Parser[A], inp: String): Option[(A, String)] = p(inp)

/*

parse(return_(1), "abc")
res1: Option[(Int, String)] = Some((1,abc))

parse(failure, "abc")
res2: Option[(Nothing, String)] = None

parse(item, "abc")
res3: Option[(Char, String)] = Some((a,bc))

 */

/*

def p = for {
  x <- item
  _ <- item
  y <- item
} yield (x, y)
p: Parser[(Char, Char)]

def p_dash = item.flatMap(x =>
  item.flatMap(__ =>
    item.map(y =>
      (x, y) )))
p_dash: Parser[(Char, Char)]

parse(p, "abcdef")
res4: Option[((Char, Char), String)] = Some(((a,c),def))

parse(p, "ab")
res5: Option[((Char, Char), String)] = None

 */

/*

parse(item +++ return_('d'), "abc")
res6: Option[(Char, String)] = Some((a,bc))

parse(failure +++ return_('d'), "abc")
res7: Option[(Char, String)] = Some((d,abc))

parse(failure +++ failure, "abc")
res8: Option[(Nothing, String)] = None

 */

def sat(p: (Char) => Boolean): Parser[Char] = for {
  x <- item
  r <- if (p(x)) return_(x) else failure
} yield (r)

def digit = sat(_.isDigit)

def lower = sat(_.isLower)

def upper = sat(_.isUpper)

def letter = sat(_.isLetter)

def alphanum = letter +++ digit

def char(x: Char) = sat(x.==)

/*

parse(digit, "123")
res9: Option[(Char, String)] = Some((1,23))

parse(digit, "abc")
res10: Option[(Char, String)] = None

parse(char('a'), "abc")
res11: Option[(Char, String)] = Some((a,bc))

parse(char('a'), "123")
res12: Option[(Char, String)] = None

 */

// これは文字列を返すパーサとする。Scala の String は List[Char] ではない
def string(s: String): Parser[String] = s match {
  case "" => return_("")
  case _ => for {
    x <- char(s.head)
    xs <- string(s.tail)
  } yield (x +: xs)
}

/*

parse(string("abc"), "abcdef")
res13: Option[(String, String)] = Some((abc,def))

parse(string("abc"), "ab1234")
res14: Option[(String, String)] = None

 */

object Many {
  def many[A](p: Parser[A]): Parser[List[A]] = {
    many1(p) +++ return_(Nil)
  }
  def many1[A](p: Parser[A]): Parser[List[A]] = {
    for {
      v <- p
      vs <- many(p)
    } yield (v :: vs)
  }
}
import Many._

/*

parse(many(digit), "123abc")
res15: Option[(List[Char], String)] = Some((List(1, 2, 3),abc))

parse(many(digit), "abcdef")
res16: Option[(List[Char], String)] = Some((List(),abcdef))

parse(many1(digit), "abcdef")
res17: Option[(List[Char], String)] = None

 */

// 仕方ないので文字列にしておく
def ident = for {
  x <- lower
  xs <- many(alphanum)
} yield ((x :: xs).foldLeft("")(_ + _))

// これも
def nat: Parser[Int] = for {
  xs <- many1(digit)
} yield (xs.foldLeft("")(_ + _).toInt)

def space: Parser[Unit] = for {
  _ <- many(sat(_.isWhitespace))
} yield (Unit)

/*

parse(ident, "abc def")
res0: Option[(java.lang.String, String)] = Some((abc, def))

parse(nat, "123 abc")
res1: Option[(Int, String)] = Some((123, abc))

parse(space, " abc")
res2: Option[(Unit, String)] = Some(((),abc))

 */

def token[A](p: Parser[A]): Parser[A] = for {
  _ <- space
  v <- p
  _ <- space
} yield (v)

def identifier: Parser[String] = token(ident)

def natural: Parser[Int] = token(nat)

def symbol(xs: String): Parser[String] = token(string(xs))

/*

def p: Parser[List[Int]] = for {
  _ <- symbol("[")
  n <- natural
  ns <- many(
    for {
      _ <- symbol(",")
      x <- natural
    } yield (x)
  )
  _ <- symbol("]")
} yield (n :: ns)

parse(p, " [1, 2, 3] ")
res19: Option[(List[Int], String)] = Some((List(1, 2, 3),))

parse(p, "[1, 2,]")
res20: Option[(List[Int], String)] = None

 */

def int: Parser[Int] = (
  for {
    _ <- symbol("-")
    n <- natural
  } yield (- n)
) +++ natural

object Expr {
  def expr: Parser[Int] = for {
    t <- term
    r <- (
      for {
        _ <- symbol("+")
        e <- expr
      } yield (t + e)
    ) +++ (
      for {
        _ <- symbol("-")
        e <- expr
      } yield (t - e)
    ) +++ return_(t)
  } yield (r)
  def term: Parser[Int] = for {
    f <- factor
    r <- (
      for {
        _ <- symbol("*")
        t <- term
      } yield (f * t)
    ) +++ (
      for {
        _ <- symbol("/")
        t <- term
      } yield (f / t)
    ) +++ return_(f)
  } yield (r)
  def factor: Parser[Int] = (
    for {
      _ <- symbol("(")
      e <- expr
      _ <- symbol(")")
    } yield (e)
  ) +++ int
}
import Expr._

/*

parse(expr, "2*3+4")
res26: Option[(Int, String)] = Some((10,))

parse(expr, "2*(3+4)")
res28: Option[(Int, String)] = Some((14,))

parse(expr, "2 * (3 + 4)")
res30: Option[(Int, String)] = Some((14,))

parse(expr, "2*3-4")
res33: Option[(Int, String)] = Some((2,))

parse(expr, "-1")
res36: Option[(Int, String)] = Some((-1,))

parse(expr, "(-3 * 4 * - 2) / 6")
res43: Option[(Int, String)] = Some((4,))

 */



Scala: エラトステネスのふるいで無限長素数列

簡潔すぎワロタw

import Stream._
def sieve[A <% BigInt](xxs: Stream[A]): Stream[A] = xxs match {
  case p #:: xs => cons(p, sieve(xs.filter(_ % p != 0)))
}
lazy val primes = sieve(from(2))

先頭 10 個。

scala> primes.take(10).toList
res0: List[Int] = List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)

Fri Apr 22 2011: ついでに、遅延評価版のクイックソートも。まずは、普通に eager 評価のリストで書いた場合:

def qsort[A <% Ordered[A]](xxs: List[A]): List[A] = xxs match {
  case Nil => Nil
  case p :: xs =>
    qsort(xs filter(_ <= p)) ++ (p :: qsort(xs filter(p < _)))
}

ストリームで書くと、以下のように:

import Stream._
def qsort[A <% Ordered[A]](xxs: Stream[A]): Stream[A] = xxs match {
  case Empty => Empty
  case p #:: xs =>
    qsort(xs filter(_ < p)) ++ (p #:: qsort(xs filter(p <= _)))
}

ほぼ同じですが、”Empty” や “#::” などが特徴的です。不用意に無限長ストリームのソートなんかしちゃダメですよ。filter() が返ってきません。




WSH JScript の REPL を書いてみた

秀丸エディタ用、Emacs の eval-print-last-sexp 風マクロ for Scala, Clojure, Gauche, Groovy, Python and Ruby – Ayutaya.com」で、REPL が無いので流していた WSH の JScript (JavaScript) ですが、eval() があるのだからサクッと書けば良いので、どうせ世の中にはすでにたくさんの実装があるとは思われますが、書いてみました。お入り用でしたらこちらからどうぞ。

eval() が環境を汚すので、識別子には “_” を前置しています。それでも衝突すると言うならば、”_” を “_HOGEHOGE_” とでも全置換しても平気なように書かれています。ダブルクォートもバックスラッシュも使っていないので、別プログラム内へ埋め込むのも簡単です。一点ハマったのは、eval() は現在の環境内で実行されるので、当初別ファイルをロードする処理を関数にしたら、呼ばれた関数内で eval() されてしまい、そこから戻ったら “var” も “function” も消えていたことです。仕方がないので、入力ハンドラをスタックするようにしています。

実行例としては、以下のような感じです。

C:\>cscript.exe wshrepl.js
Microsoft (R) Windows Script Host Version 5.7
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

> 1 + 2 +
|   3 + 4
= 10
> function foo() {
|   WScript.Echo("Hello!");
| }
= undefined
> foo()
Hello!
= undefined
> :load c:\priv\prog\bat\wsh.js
= Hello, World!
= undefined
> ^Z

C:\>

以下は、ついでの tips です。

最新のブラウザの JavaScript に比べるといろいろと見劣りする Jscript ではありますが、そこは腐っても JavaScript 1.5 ですから、prototype を拡張して、map(), reduce(), filter() その他を拡張しておくと、なかなかに使える環境になります。

include が無い JScript ですが、そこは eval() で何とでもなります。以下は、実行スクリプトと同一ディレクトリにある “wsh.js” を初期化ファイルとして取り込みます。記述を短くしようと思って “WScript” を “with” すると、なぜか挙動がおかしくなります。

eval(WScript.CreateObject('Scripting.FileSystemObject').OpenTextFile(
 WScript.ScriptFullName.slice(0,-WScript.ScriptName.length)+'wsh.js' ).ReadAll());



秀丸エディタ用、Emacs の eval-print-last-sexp 風マクロ for Scala, Clojure, Gauche, Groovy, Python, Ruby, JScript, Haskell and Perl

Emacs の lisp-interaction-mode に、編集中のテキスト上のカーソル直前位置の Emacs Lisp の S 式を評価する便利なコマンド “eval-print-last-sexp” がありますが、その秀丸エディタ用、各種プログラミング言語対応版です。下記は HTML で文章を書きながら、その中の Clojure のサンプルコードを評価している例です。矢印位置にカーソル (|) がある状態で、この場合は “eval-clojure.mac ” を実行してみます。

<p>下記は、クイックソートのサンプルです:

<pre>
(defn qsort [[x & xs]]
  (when x
    (let [smaller #(< % x)]
      (lazy-cat
        (qsort (filter smaller xs))
        [x]
        (qsort (remove smaller xs)) ))))| ←
</pre>

定義が評価されてシンボルが返ります。

<p>下記は、クイックソートのサンプルです:

<pre>
(defn qsort [[x & xs]]
  (when x
    (let [smaller #(< % x)]
      (lazy-cat
        (qsort (filter smaller xs))
        [x]
        (qsort (remove smaller xs)) ))))
#'user/qsort ←
</pre>

そのまま試しに実行してみます。

(qsort '(5 3 8 9 2))|

その場に結果が返ります。

(qsort '(5 3 8 9 2))
(2 3 5 8 9)

いずれも、日本語も通ります。下記は Scala の例です。

val l = "日本語" :: "テキスト" :: Nil
l: List[java.lang.String] = List(日本語, テキスト)

どこか、昔の Basic インタプリタで開発をしていたような快適さがあります (あいにく SmallTalk の経験は無い)。とりわけ、関数型言語と相性が良いんでないかしら、このスタイルは。メリットとしては、以下のような感じかと思います:

  • 言語をその場でチャンポンに書ける: ガッツリとアプリを開発するのであれば Emacs なり Eclipse なりの各言語のモードを利用すれば良いのでしょうが、つらつらと考えながら各言語を横断的に、サンプルなどのスクリプト片を次々に試しながらコメントも書きたい場合などには、同一テキスト上でその場で評価し、すぐに結果が返る方が快適です
  • コピペをしなくてすむ: いちいちエディタ上で書いて、それを REPL にコピペ実行、というのは、思考を阻害します
  • 起動オーバーヘッドが無い: REPL のセッションをバックグラウンドで維持しますので、とりわけ Scala や Clojure などの、JVM を使う、起動が異様に遅い REPL でも、2 度目以降の評価はサクサクです。非力なノートマシンには、これがありがたい
  • エディタから離れなくて済む: 一日のほとんどをエディタ上で過ごす身としては、ひととおりのスクリプト実行がその場でできるというのはありがたいです。コード片も、無くさず記録として残りますし
  • キー一発: キーを割り当てておけば、普段書きのテキストから各言語にキー一発 (2 ストロークならば二発ですが) でアクセスできます

秀丸エディタは、ver .8.0 以降のマクロで COM オブジェクトの操作をサポートしたため、以前にはできなかった複雑なアプリケーション連携や、テキストを開いている間だけ永続するオブジェクト等を持つことができるようになりました。今回のマクロも、バックグラウンドで各言語の REPL セッションを起動して、テキストをクローズするまでの間、状態を維持することができるようになったことで実現できました。

詳細は以下ですが、詳しくはコードを読んでください。なお、範囲選択をして実行すれば、選択範囲を評価します。

マクロ名 カーソル以前のどこまでを一単位として評価? 入出力方式 既知の問題
eval-scala.mac カーソルのある行、あるいは対応する括弧かヒアドキュメントによって継続している複数行 (Scala に “\” による行継続は無い) 入力は REPL の :load、出力は STDOUT (SJIS) あいかわらず初回起動が遅いが、こればかりはどうしようもない
eval-clojure.mac カーソル直前の S 式 入力は専用 REPL、出力は STDOUT (SJIS) 初回起動は、そこそこに遅い。STDERR をファイルに出力する方法が分からなかったが、多分あまり支障はない
eval-gauche.mac カーソル直前の S 式 入出力とも専用 REPL (UTF-8) 特に無い。Scheme は Gauche がお気に入りなので、Guile は確認していない。
eval-groovy.mac カーソルのある行、あるいは対応する括弧かヒアドキュメントによって継続している複数行、あるいは “\” で継続している複数行 入力は REPL の \l、出力は STDOUT (SJIS) jline が “unix” モードでないと STDIN を正しく扱えなかったため、動作に MinGW の sh(1) と stty(1) が必要
eval-python.mac 第一カラムが空白ではないカーソル行、あるいはカーソル行以前で第一カラムが空白ではない行までの複数行、あるいは “\” で行継続をしている複数行 入力は専用の REPL、出力は STDOUT (UTF-8) REPL の返事が淡白すぎて、返り値が分かりづらい
eval-ruby.mac カーソルのある行、あるいはカーソル行上の “end” とカラムが合う直前の開始行までの複数行、あるいは “\” で行継続している複数行 入力は REPL の irb_source()、出力は STDOUT (SJIS) irb_source() が、たまにファイルハンドルを離してくれないのか、一時ファイルを delete できないことがある。上書きするので、おそらく動作に支障はない
eval-jscript.mac Groovy とだいたい同じ (本当なら、いろいろ違うんだけど…) 入力は自前実装の REPL で “:load”、出力は STDOUT (SJIS) :load は、通常の eval() ではなく、REPL 的に行単位で評価されるので注意。ブロック内の空行はインデントしておいてください
eval-haskell.mac 第 1 カラム目が空白であれば、直前の “module” までをモジュールとしてロードする。第 1 カラム目が空白でなければ、式として解釈する モジュールの入力は REPL の “:load”、式の入力は STDIN から、出力は STDOUT GHC では、日本語入力は Unicode に落ちるものの、出力はできないようだ
eval-perl.mac カーソル行か、括弧を辿って戻れるところまで 入力は自前 REPL の “:load” から、出力は STDOUT から Perl の eval() は新しくブロックを作ってしまうので、評価単位内の “my”, “local” 変数や “$1″ などは、eval() が終わると破棄されてしまう。”$s =~ /([a-z]+)/; $mstr = $1″ のように複文で保持するなど要工夫

あとは Haskell (トップレベルの扱いがよく分からなくて作れなかった) と、JavaScript (WSH には REPL が無い) 用が欲しいところです。

Wed Apr 13 2011: JScript 用を追加。REPL 単体については「WSH JScript の REPL を書いてみた – Ayutaya.com」から。

Fri Apr 15 2011: Haskell (GHC) 用を追加。言語が言語なので、少々特殊。

このカーソル位置(↓)でマクロを起動すると、

module Test where
  fib 1 = 1
  fib 2 = 1
  fib n = fib (n - 1) + fib (n - 2)
|

モジュールをロード。もう一度実行すれば、再ロード。

module Test where
  fib 1 = 1
  fib 2 = 1
  fib n = fib (n - 1) + fib (n - 2)

[1 of 1] Compiling Test
( C:\DOCUME~1\KIICHI~1\LOCALS~1\Temp\haskell_input_2821200.hs, interpreted )
Ok, modules loaded: Test.
|

続けて式を評価すると、

fib 10

値が返る。

fib 10
55|

Sun Apr 17 2011: Perl を追加。eval() の仕様故に、若干心残りが…。残るは PHP と VBScript くらいか。




Scala: コンストラクタの意味合いとクロージャ

以前、「Scala: コンストラクタ中の一時オブジェクトに消えて欲しい – Ayutaya.com」などと言っていた私ですが、ちょっと考え違いをしていました。なまじ逆コンパイルなどをしてしまったために、C++~Java 的な構造体指向の発想にとらわれていましたが、文法的に見ると、Scala のクラス~コンストラクタは、静的なクロージャセットの構築だと見た方が自然です。であれば、クロージャに束縛された「スタック上の変数」が残るのも道理です。

例として、ベクトル型 (+ 絶対値を求めるメソッド) を Scala で下記のように書いたとして、

class Vect(x: Int, y: Int) {
  def abs = {
    math.sqrt(x * x + y * y)
  }
}

val v = new Vect(2, 3)
println(v.abs)

同じものを、例えば JavaScript で、プロトタイプを使わずにクロージャで書いたとします。

function Vect(x, y) {
  this.abs = function() {
    return (Math.sqrt(x * x + y * y));
  }
}

var v = new Vect(2, 3);
WScript.Echo(v.abs());

一目瞭然でそっくりです。Scala でオブジェクト生成の際に “new” が省略されないことも、それが意味するところが、通常の関数呼び出しと違って、クロージャの配列領域を確保する命令であると考えることができますし (上記の JavaScript でも “new” が、”this” に相当するオブジェクトを作っていますし)、パブリックな「メンバ変数」が実際にはアクセサとなっているところも、Scala のコンストラクタが「クロージャ群の構築子」であると考えれば一貫しています。

以上、Scala で腑に落ちた点をちょろっと。では。




Windows 7 で WSC をコマンドラインから登録する

WSC (Windows Script Components) を登録する際に、「右クリックして『登録』をクリック」がダサくて仕方がありません。複数マシンへ大量に登録したい時にどうしろってんだ、と。そこで、コマンドラインから登録できないものかと探していると、ありました、regsvr32.exe です。どうやら、OLE コントロールとしてひとくくりに、DLL や OCX の登録と同じ要領でよいようです。

# どうでもいいけど、あいかわらずマイクロソフトの、カタカナ単語間をスペースで切る表記はきもちわるい。”・”を使おうよ

当方 Windows 7 ですので、UAC (User Account Control) をパスするなり何なりして、管理者権限で実行する必要があるようです。

コマンドプロンプトを管理者権限で起動してから実行しても良いのですが、それもあまりスマートではありません。su(1) か sudo(8) のようなコマンドはないのですか? なに? runas.exe を使え? 了解しました。

> runas.exe /savecred /env /user:administrator "regsvr32.exe /s Foo.wsc"
administrator のパスワードを入力してください:

administrator アカウントのパスワード? そんなもの設定したおぼえがないよ。しかたがないので、administrator 権限を持つアカウントでコンソールを管理者権限で起動、パスワードを設定します。まあ、これは初回だけですし。

> net user administrator *
ユーザーのパスワードを入力してください:
確認のためにパスワードを再入力してください:
コマンドは正常に終了しました。

>

これで、こんどこそは行け…たのですが、何度かしくじっているうちに、以下のようになってしまいました。

> runas.exe /env /user:administrator "regsvr32.exe /s Foo.wsc"
regsvr32.exe /s Foo.wsc をユーザー "SVX\administrator" として開始して
います...
RUNAS エラー:  実行できません - regsvr32.exe /s /u AyutayaCalc.wsc
1327: ログオン失敗: ユーザー アカウントの制限。考えられる理由として、空
 のパスワードが許可されていない、ログオン時間制限、またはポリシーによる制
 限が適用された、などが挙げられます。
>

どうやら、パスワードなしの状態の時に適当なパスワードを入れてログオン認証に失敗しまくったせいで、アカウント停止を食らったようです。

> net user administrator | findstr /r アカウント有効
アカウント有効                       No
>

あらら。再度有効化します。

> net user administrator /active:yes
コマンドは正常に終了しました。

>

はい、何よりです。

面倒なので、”/env” で、administrator になった後も環境を引き継ぎます。これで、カレントディレクトリの “Foo.wsc” を登録できるようになります。su(1) で “-” をつけないような挙動ですね。”/savecred” で、安い Windows 以外ならばパスワードを記憶できるそうです。

> runas.exe /savecred /env /user:administrator "regsvr32.exe /s Foo.wsc"
administrator のパスワードを入力してください:
regsvr32.exe /s Foo.wsc をユーザー "SVX\administrator" として開始しています...
>

さて、JScript な WSH で WSC を書くとします。




ソース斜め読み: Eucalyptus CLC, Walrus, SC のエントリやハンドラはどこ?

注意: 以下の文章は、なかばメモ書きですので、正直言って分かりづらいですが、ネット上に Eucalyptus “eucalyptus-cloud” コマンドの実装に関する情報がまるでないので、本気で読む気がある人にとっては、この程度でも、とっかかりとしては充分に有用だと思います。逆に、興味のない人にとっては、どんなに分かりやすく書いてもさっぱり分からないと思いますので、こんなもんでいいかな、と (なんてひどい)。

前回は、eucalyptus-cloud コマンドが JNI で Java のコードを起動するところまでを追ったのですが、その先がさっぱり分かりません。eucalyptus-cloud 配下で CloudController, Walrus, StorageController といった Web API のサービスが起動されるはずなのですが、どうやら動的に *.jar 内を探り、条件に合うクラスからインスタンスを作ることでサービスを起動しているようなので、ソースだけを見ていても、実際にどこでサービスや、サービスの個々の機能を定義しているのかの当たりがつけられません。

仕方がないので、動態デバッグ (“post-mortem” に対してならば、”antemortem” とでも言うのかな?) を試みますが、C から JNI で呼ばれる Java アプリケーションのデバッグはどうやるのでしょう? よく見たら、ちゃんと “eucalyptus-cloud” コマンドにオプションがありました。

$ eucalyptus-cloud --help
(...)
  -d, --debug                   Launch with debugger enabled.  (default=off)
      --debug-port=INT          Set the port to use for the debugger.
                                  (default=`5005')
      --debug-suspend           Set the port to use for the debugger.
                                  (default=off)

ポートを指定してサスペンドするよう指示すれば、JVM のデバッガのポートを開けて待っていてくれるので、後は JDB なり Eclipse なりのデバッガを繋げば OK です。起動スクリプトにオプションを追加します。

--- /etc/init/eucalyptus.conf.orig
+++ /etc/init/eucalyptus.conf
@@ -37,6 +37,7 @@
        . /etc/eucalyptus/eucalyptus.conf
        [ -n "$JVM_MEM" ] || JVM_MEM="512m"
        opts="-h $EUCALYPTUS -u $EUCA_USER --pidfile \
         /var/run/eucalyptus/eucalyptus.pid -l $LOGLEVEL -L console-log
         -Xmx$JVM_MEM"
+       opts="$opts --debug --debug-suspend --debug-port=5005"
        services=""

        # If the -cloud package is not installed, disable the cloud service
@@ -72,7 +73,7 @@
        # Start the appropriate service(s)
        if [ -n "$services" ]; then
                # Cloud services to run
-               exec eucalyptus-cloud $opts
+               exec eucalyptus-cloud $opts > /tmp/eucalyptus.log 2>&1
        elif [ -r "/etc/init/eucalyptus-nc.conf" ] || \
         [ -r "/etc/init/eucalyptus-cc.conf" ]; then
                # Node or CC services to run
                # Why do we sleep here, rather than emitting a signal?
                #  Such that we can run:

さて、その後つらつらと実行しながら眺めて行くと、Eucalyptus の関連クラスを取得しに行くのが、com.eucalyptus.bootstrap.Bootstrap.doDiscovery() のようです。以下が、doDiscovery() に入ったところのスタックのバックトレースです。

Thread [main] (Suspended (entry into method doDiscovery in Bootstrap))
  Bootstrap.doDiscovery() line: 151
  Bootstrap.initialize() line: 261
  SystemBootstrapper.init() line: 109

処理は、ゴリゴリです。

private static void doDiscovery( ) {
  // LIB は、前回見た "-Deuca.lib.dir=/usr/share/eucalyptus/" に相当。
  //  詳細は enum com.eucalyptus.system.BaseDirectory を参照のこと
  File libDir = new File(BaseDirectory.LIB.toString());
  for (File f: libDir.listFiles()) {
    if (
     // ファイル名が "eucalyptus" で始まっていて、
     f.getName().startsWith(
      com.eucalyptus.bootstrap.Component.eucalyptus.name() ) &&
     // ".jar" で終わっていて、
     f.getName().endsWith(".jar") &&
     // "-ext-" を含まないものが探索すべき jar ファイル
     ! f.getName( ).matches( ".*-ext-.*" ) ) {
      // Multimap ServiceJarDiscovery.classList にクラスとパスを貯めこみ、
      //  ServiceJarDiscovery のサブクラスを ServiceJarDiscovery.discover に
      //  貯めこむ
      ServiceJarDiscovery.processFile(f);
    }
  }
  // ServiceJarDiscovery.discover から、該当クラスを探し、実行
  ServiceJarDiscovery.runDiscovery();
}

起動するサービスのクラスを実際に選定するのは、その先の runDiscovery() の中、com.eucalyptus.component.ServiceBuilderDiscovery.processClass(Class) でした。

Thread [main] (Suspended (breakpoint at line 19 in ServiceBuilderDiscovery))
  ServiceBuilderDiscovery.processClass(Class) line: 19
  ServiceBuilderDiscovery(ServiceJarDiscovery).checkClass(Class) line: 113
  // ここ (↓) までが static。ここで、個々の discovery において、
  //  SerivceJarDiscovery が多態している。これより上は、仮想関数が呼ばれ
  //  ている
  ServiceJarDiscovery.runDiscovery() line: 100
  Bootstrap.doDiscovery() line: 164
  Bootstrap.initialize() line: 261
  SystemBootstrapper.init() line: 109

com.eucalyptus.component.Component.setBuilder(ServiceBuilder<ServiceConfiguration>) でもって、Component.builder にビルダが載ります。この “Component” が、内部では web サービスの機能単位の管理情報をあらわします。”component” は、Eucalyptus の内部では web サービスの提供コンポーネントを意味します。

ServiceJarDiscovery のサブクラスの中に、com.eucalyptus.component.ServiceBuilderDiscovery というのがありまして、これが web サービスの「ビルダ」を作ります。これが、com.eucalyptus.cluster.ClusterBuilder, com.eucalyptus.config.WalrusBuilder, com.eucalyptus.config.StorageControllerBuilder を newInstance() してくれて、Components.lookup(c (= “名前”) に、入れておいてくれます。

で、discover までで探索・インスタンス化が済んで、実際にサービスをスタートするのは Bootstrapper の仕事のようです (もういいかげん疲れたので、とっとと web サービスのハンドラまで行きたいので、いろいろと端折る)。

ポート 8773 のサービスは、com.eucalyptus.bootstrap.SystemBootstrapper.start() で、stage.name == “CloudServiceInit” の際の stage.start() で起動します。詳しくは、com.eucalyptus.context.ServiceContext.ServiceBootstrapper.start() で止めて、各変数を見てみてください。

でもって、実際のサービスのフレームは、Mule ESB が提供しています。Mule ESB に渡す設定は com.eucalyptus.context.ServiceContext.ServiceBootstrapper.load(Stage) の下記の行で jar の中から Java リソースとして取得しています。

Resource rsc = comp.getConfiguration( ).getResource( );

そして、以下の箇所でつっこみます。

configs.addAll( rsc.getConfigurations( ) );

そして、以下で構築。

ServiceContext.buildContext( configs );

さっきから、”config” と “component” が微妙に絡んでいたのは、component の実体定義が、上から見ると config の記述そのものだからなのですね。Components.Resource.<init>(Configuration, URI) を遡れば、いろいろ見えます。”URI” には、”CloudServiceProvider” が file:// で入ってきます。これが config のファイルであり、component の定義です。

その URI (“CloudServiceProvider”) はというと、以下で動的に作っています。そのものを grep しても見つからないわけです。つまり、リソース名 “com.eucalyptus.CloudServiceProvider” の意味は、「”com.eucalyptus” に属するリソースであり、ステージ “CloudServiceInit” の際に読み込まれるサービスプロバイダの設定だよ」となっとります。

>com.eucalyptus.bootstrap.Bootstrap.Stage.getResourceName()
  return String.format(
   "com.eucalyptus.%sProvider", this.name( ).replaceAll("Init\\Z", "") );

スタックトレースでは、以下のようになります。

Thread [main] (Suspended (entry into method getResourceName in Bootstrap$Stage))
  Bootstrap$Stage.getResourceName() line: 132
    name="SystemInit"
    this.name() → (java.lang.String) SystemInit
    this.name( ).replaceAll( "Init\\Z", "" ) → (java.lang.String) System
  LoadConfigs.commit(Bootstrap$Stage) line: 31
  LoadConfigs.commit(Object) line: 19
  Transition$anonymously$1.commit(O) line: 69
  Transition$anonymously<O,T>.commit(O) line: 76
  Transition$anonymously<O,T>(Transition<O,T>).doTransition(O) line: 137
  Transition$anonymously<O,T>(Transition<O,T>).transition(Iterable<O>) line: 150
  Bootstrap.initialize() line: 255
  SystemBootstrapper.init() line: 109

そして最終的には、たとえば、以下のような URI になっています。

  • jar:file:/usr/share/eucalyptus/eucalyptus-component-2.0.0.jar!/com.eucalyptus.CloudServiceProvider
  • jar:file:/usr/share/eucalyptus/eucalyptus-config-2.0.0.jar!/com.eucalyptus.CloudServiceProvider
  • jar:file:/usr/share/eucalyptus/eucalyptus-walrus-2.0.0.jar!/com.eucalyptus.CloudServiceProvider
  • jar:file:/usr/share/eucalyptus/eucalyptus-storagecontroller-2.0.0.jar!/com.eucalyptus.CloudServiceProvider

個々のサービスが何をしているのかはよく見ていませんが、ソース内、jar 内には以下があります。

    hsqldb/src/main/resources/com.eucalyptus.CloudServiceProvider
    storage-controller/src/main/resources/com.eucalyptus.CloudServiceProvider
    component/src/main/resources/com.eucalyptus.CloudServiceProvider
    cluster-manager/src/main/resources/com.eucalyptus.CloudServiceProvider
    dns/src/main/resources/com.eucalyptus.CloudServiceProvider
    www/src/main/resources/com.eucalyptus.CloudServiceProvider
    msgs/src/main/resources/com.eucalyptus.CloudServiceProvider
    cloud/src/main/resources/com.eucalyptus.CloudServiceProvider
    walrus/src/main/resources/com.eucalyptus.CloudServiceProvider
    configuration/src/main/resources/com.eucalyptus.CloudServiceProvider

では、その中身はというと、以下のようになっています。

name=walrus
euca.model.walrus=walrus-model.xml
euca.model.walrus.services=walrus-services.xml

この先は Mule ESB の上の世界ですので、そっちを参照してください。ちなみに、最新の ver. 3 ではなく、ver. 2 で書かれています。なお、Mule ESB 等のサブモジュールは、Ubuntu のパッケージ的には “eucalyptus-commons-ext” の方に入っていました。

なお、このリソースファイルがビルドの際に取り込まれるさまは、clc/modules/module-inc.xml にあります。

さて、いよいよですが、たとえば “export EC2_URL=http://~:8773/services/Eucalyptus” に相当する実装がどこに書かれているかといえば、まず clc/modules/cloud/src/main/resources/eucalyptus-services.xml を参照します。

<euca:endpoint name="EucalyptusWS" connector-ref="eucaws"
 address="http://127.0.0.1:${euca.ws.port}/services/Eucalyptus" synchronous="true" />

“export S3_URL=http://192.168.1.23:8773/services/Walrus” は “clc/modules/walrus/src/main/resources/walrus-services.xml” なので、以下。

  <euca:endpoint name="WalrusWS"
    connector-ref="eucaws"
    address="http://127.0.0.1:${euca.ws.port}/services/Walrus"/>

まとめると、ファイル “com.eucalyptus.CloudServiceProvider” はリソースとしてアクセスされ、そこにリストされている XML が Mule ESB (ver.2) に渡されてwebサービスを開始します。

ためしに、ハンドラも見てみます。例として、ec2-describe-availability-zones の対応箇所を見てみます。定義は以下です。

clc/modules/cloud/src/main/resources/eucalyptus-runtime.xml:
  <mule ...>
    ...
    <service name="ClusterEndpoint">
      <inbound>
        <inbound-endpoint ref="ClusterEndpointWS"/>
      </inbound>
      <component class="com.eucalyptus.cluster.ClusterEndpoint"/>
      <outbound>
        <outbound-pass-through-router>
          <outbound-endpoint ref="ReplyQueueEndpoint"/>
        </outbound-pass-through-router>
      </outbound>
    </service>

で、個々の機能の定義は以下。

clc/modules/msgs/src/main/resources/aws-zones.xml:
  <binding force-classes="true">
    <mapping name="DescribeAvailabilityZones"
     class="edu.ucsb.eucalyptus.msgs.DescribeAvailabilityZonesType"
     extends="edu.ucsb.eucalyptus.msgs.EucalyptusMessage">
      <collection name="availabilityZoneSet"
       field="availabilityZoneSet"
       factory="org.jibx.runtime.Utility.arrayListFactory"
       item-type="java.lang.String">
        <structure name="item">
          <value name="zoneName"/>
        </structure>
      </collection>
    </mapping>
  </binding>

で、対応実ハンドラは右記です。com.eucalyptus.cluster.ClusterEndpoint.DescribeAvailabilityZones(DescribeAvailabilityZonesType) の処理を止めてみます。スタックトレースは以下のようになります。

Thread [ClusterEndpoint.16] (Suspended (breakpoint at line 142 in ClusterEndpoint))
  ClusterEndpoint.DescribeAvailabilityZones(DescribeAvailabilityZonesType) line: 142
  GeneratedMethodAccessor221.invoke(Object, Object[]) line: not available
  DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43
  Method.invoke(Object, Object...) line: 616
  ReflectionEntryPointResolver(AbstractEntryPointResolver).invokeMethod(Object, Method, Object[]) line: 147
  ReflectionEntryPointResolver.invoke(Object, MuleEventContext) line: 127
  LegacyEntryPointResolverSet(DefaultEntryPointResolverSet).invoke(Object, MuleEventContext) line: 50
  DefaultLifecycleAdapter.intercept(Invocation) line: 202
  DefaultJavaComponent(AbstractJavaComponent).invokeComponentInstance(MuleEvent) line: 82
  DefaultJavaComponent(AbstractJavaComponent).doOnCall(MuleEvent) line: 73
  DefaultJavaComponent(AbstractComponent).onCall(MuleEvent) line: 87
  SedaService$ComponentStageWorker.run() line: 533
  WorkerContext.run() line: 310
  ThreadPoolExecutor.runWorker(ThreadPoolExecutor$Worker) line: 1061
  ThreadPoolExecutor$Worker.run() line: 575
  Thread.run() line: 636

こんな (↓) 感じですね。

> com.eucalyptus.cluster.ClusterEndpoint.DescribeAvailabilityZones(DescribeAvailabilityZonesType)
  ...
  if( args.isEmpty( ) || args.contains( "verbose" ) || args.contains( "certs" ) || args.contains( "logs" ) || args.contains( "keys" ) ) {
    for( Cluster c : Clusters.getInstance( ).listValues( ) ) {
      this.getDescriptionEntry( reply, c, request );
    }
  } else {

“verbose” だろうが何だろうが全情報を返して、クライアント側で切り分けているようですね。美しくないです。

以上で、分からないことがあった時に、だいたいどこを見れば良いかだけは分かりました。

しかしどうも分からないことも。JNI で起動する際に、java.class.path に jar の羅列は渡っているのに、なぜにわざわざせっせと /usr/share/eucalyptus/ 以下を捜索してリフレクションを多用したりしているんでしょう? ぶっちゃけ、無駄に複雑でメンテしづらいと思うんです、このコード。




ソース斜め読み: Eucalyptus Cloud Controller の Java コードのエントリはどこ?

表題の答えを先に書いておきますと、com.eucalyptus.bootstrap.SystemBootstrapper です。Eucalyptus のバージョンは、2.0.2 を見ています。

まずは、ソースツリーのおおまかな構成について見ておきます。Cluster Controller の本体は、C で書かれた Axis2/C のモジュール (libaEucalyptusCC.so) です。Apache + mod_axis2 配下で動きます。サブディレクトリは “cluster/” です。

Node Controller の本体も、C で書かれた Axis2/C のモジュール (libEucalyptusNC.so) で、Apache + mod_axis2 配下で動きます。ソースのサブディレクトリは “node/”。

問題は、それ以外――ソースのディレクトリでいう “clc/” 以下です。

追記 (Wed Feb 09 2011): eucalyptus-cloud 下の web サービス群 (ソース中では “component” と呼称される) は、Mule ESB バージョン 2.x で動作しています。別項で、いずれまた書きます。

Cloud Controller だけでなく、Walrus や Storage Controller は Java で書かれていて、呼び出し方からすると、複数機能を束ねた統一バイナリ (eucalyptus-cloud) のかたちをとっています。ところが、プロセスを見てみると、Java のものとおぼしき大量のスレッドはあるのですが、よく見かけるようには、オプションをごちゃごちゃつけて起動されている java コマンドが見あたりません。

$ pstree -alp
...
  |-eucalyptus-clou,28159 -h / -u eucalyptus --pidfile /var/run/eucalyptus ...
  |   `-eucalyptus-clou,28176 -h / -u eucalyptus --pidfile /var/run/eucaly ...
  |       |-bttrack,28514 /usr/bin/bttrack --port 6969 --dfile //var/lib/e ...
  |       |-{eucalyptus-clo},28193
  |       |-{eucalyptus-clo},28195
  |       |-{eucalyptus-clo},28202
...

ソースを見てみると、C でかかれた実行ファイル (eucalyptus-cloud) から、libjvm を経由して Java のコードを起動する作りになっています。しかたがないので clc/modules/bootstrap/eucalyptus-bootstrap.c の main() の定義から下って読んで行くと、fork(2) をして子プロセス (上の例で言う 28176) を作っています。子プロセスのその先の処理は、child()→java_init() で、JNI_CreateJavaVM() の呼び出しです。

JNI の呼び出しの詳細は、デバッグ出力で出るようになっています。eucalyptus-cloud コマンドに “–debug”、もしくは “–verbose” オプションをつけて呼び出せば、標準出力 (いや、エラー出力だったかな?) にデバッグ出力を出してくれます。Ubuntu-10.10 のパッケージだと upstart が出力を捨ててしまいますので、以下のようにして、適当なところにリダイレクトしておきます。

--- /etc/init/eucalyptus.conf.orig
+++ /etc/init/eucalyptus.conf
@@ -37,6 +37,7 @@
        . /etc/eucalyptus/eucalyptus.conf
        [ -n "$JVM_MEM" ] || JVM_MEM="512m"
        opts="-h $EUCALYPTUS -u $EUCA_USER --pidfile \
         /var/run/eucalyptus/eucalyptus.pid -l $LOGLEVEL -L console-log
        -Xmx$JVM_MEM"
+       opts="$opts --debug"
        services=""

        # If the -cloud package is not installed, disable the cloud service
@@ -72,7 +73,7 @@
        # Start the appropriate service(s)
        if [ -n "$services" ]; then
                # Cloud services to run
-               exec eucalyptus-cloud $opts
+               exec eucalyptus-cloud $opts > /tmp/eucalyptus.log 2>&1
        elif [ -r "/etc/init/eucalyptus-nc.conf" ] || \
         [ -r "/etc/init/eucalyptus-cc.conf" ]; then
                # Node or CC services to run
                # Why do we sleep here, rather than emitting a signal?
                #  Such that we can run:

すると、以下のような呼び出しが出ます (いい具合に改行を入れました)。

Using classpath: -Djava.class.path=//etc/eucalyptus/cloud.d:
 //etc/eucalyptus/cloud.d/scripts:
 //usr/share/eucalyptus/eucalyptus-component-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-storage-common-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-config-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-groupmgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-db-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-interface-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-walrus-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-storagecontroller-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-imagemgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-commons-ext-0.4.jar:
 //usr/share/eucalyptus/eucalyptus-www-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-auth-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-msgs-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-dns-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-core-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-clustermgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-cloud-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-keymgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-ws-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-db-hsqldb-ext-2.0.0.jar:
 //usr/share/eucalyptus/wstx-lgpl.jar:
 //usr/share/eucalyptus/gnumail-providers.jar:
 //usr/share/eucalyptus/jetty-util.jar:
 //usr/share/eucalyptus/commons-logging.jar:
 //usr/share/eucalyptus/regexp.jar:
 //usr/share/eucalyptus/jcl-over-slf4j.jar:
 //usr/share/eucalyptus/drools-compiler.jar:
 //usr/share/eucalyptus/geronimo-jms-1.1-spec.jar:
 //usr/share/eucalyptus/commons-pool.jar:
 //usr/share/eucalyptus/geronimo-jpa-3.0-spec.jar:
 //usr/share/eucalyptus/axiom-dom.jar:
 //usr/share/eucalyptus/jibx-bind.jar:
 //usr/share/eucalyptus/commons-io.jar:
 //usr/share/eucalyptus/jetty-rewrite-handler.jar:
 //usr/share/eucalyptus/backport-util-concurrent.jar:
 //usr/share/eucalyptus/gnumail.jar:
 //usr/share/eucalyptus/wsdl4j.jar:
 //usr/share/eucalyptus/xercesImpl.jar:
 //usr/share/eucalyptus/servlet-api-2.5.jar:
 //usr/share/eucalyptus/commons-httpclient.jar:
 //usr/share/eucalyptus/euca_ipt:
 //usr/share/eucalyptus/slf4j-log4j12.jar:
 //usr/share/eucalyptus/commons-collections3.jar:
 //usr/share/eucalyptus/slf4j-api.jar:
 //usr/share/eucalyptus/chgrp-dhcpd:
 //usr/share/eucalyptus/geronimo-ejb-3.0-spec.jar:
 //usr/share/eucalyptus/dd-lv:
 //usr/share/eucalyptus/google-collections.jar:
 //usr/share/eucalyptus/activation.jar:
 //usr/share/eucalyptus/serializer.jar:
 //usr/share/eucalyptus/bcprov.jar:
 //usr/share/eucalyptus/jibx-run.jar:
 //usr/share/eucalyptus/jetty-sslengine.jar:
 //usr/share/eucalyptus/dom4j.jar:
 //usr/share/eucalyptus/axiom-impl.jar:
 //usr/share/eucalyptus/commons-discovery.jar:
 //usr/share/eucalyptus/hsqldb.jar:
 //usr/share/eucalyptus/jaxen.jar:
 //usr/share/eucalyptus/javassist.jar:
 //usr/share/eucalyptus/axiom-api.jar:
 //usr/share/eucalyptus/add_key.pl:
 //usr/share/eucalyptus/antlr.jar:
 //usr/share/eucalyptus/jaxp-1.3.jar:
 //usr/share/eucalyptus/chmod-dhcpd:
 //usr/share/eucalyptus/geronimo-interceptor-3.0-spec.jar:
 //usr/share/eucalyptus/proxool.jar:
 //usr/share/eucalyptus/commons-logging-adapters.jar:
 //usr/share/eucalyptus/hsqldbutil.jar:
 //usr/share/eucalyptus/jibx-extras.jar:
 //usr/share/eucalyptus/geronimo-jta-1.0.1b-spec.jar:
 //usr/share/eucalyptus/jetty.jar:
 //usr/share/eucalyptus/gwt-servlet.jar:
 //usr/share/eucalyptus/xpp3.jar:
 //usr/share/eucalyptus/jul-to-slf4j.jar:
 //usr/share/eucalyptus/commons-cli.jar:
 //usr/share/eucalyptus/xalan2.jar:
 //usr/share/eucalyptus/commons-beanutils.jar:
 //usr/share/eucalyptus/mvel.jar:
 //usr/share/eucalyptus/janino.jar:
 //usr/share/eucalyptus/commons-fileupload.jar:
 //usr/share/eucalyptus/commons-logging-api.jar:
 //usr/share/eucalyptus/junit.jar:
 //usr/share/eucalyptus/geronimo-j2ee-connector-1.5-spec.jar:
 //usr/share/eucalyptus/ant.jar:
 //usr/share/eucalyptus/gwt-user.jar:
 //usr/share/eucalyptus/commons-codec.jar:
 //usr/share/eucalyptus/wss4j.jar:
 //usr/share/eucalyptus/bsf.jar:
 //usr/share/eucalyptus/commons-lang.jar:
 //usr/share/eucalyptus/bcel.jar:
 //usr/share/eucalyptus/excalibur-logkit.jar:
 //usr/share/eucalyptus/netty.jar:
 //usr/share/eucalyptus/el-api-2.1.jar:
 //usr/share/eucalyptus/drools-core.jar:
 //usr/share/eucalyptus/cglib.jar:
 //usr/share/eucalyptus/populate_arp.pl:
 //usr/share/eucalyptus/inetlib.jar:
 //usr/share/eucalyptus/jug-asl.jar:
 //usr/share/eucalyptus/ezmorph.jar:
 //usr/share/eucalyptus/groovy.jar:
 //usr/share/eucalyptus/commons-jxpath.jar:
 //usr/share/eucalyptus/log4j-1.2.jar:
 //usr/share/eucalyptus/xml-security.jar:
 //usr/share/eucalyptus/xom.jar:
 //usr/share/eucalyptus/modprobe-aoe:
 //usr/share/eucalyptus/dnsjava.jar:
 //usr/share/eucalyptus/ecj.jar:
 //usr/share/eucalyptus/json-lib.jar

Version:                       10004
Ignore Unrecognized Arguments: 0
Extra options:                 33
  "-Xbootclasspath/p://usr/share/eucalyptus/openjdk-crypto.jar    " (0x(nil))
  "-Xmx512m                                                       " (0x(nil))
  "-XX:MaxPermSize=128m                                           " (0x(nil))
  "-XX:+UseConcMarkSweepGC                                        " (0x(nil))
  "-Djava.net.preferIPv4Stack=true                                " (0x(nil))
  "-Djava.security.policy=//etc/eucalyptus/cloud.d/security.policy" (0x(nil))
  "-Djava.library.path=//usr/lib/eucalyptus                       " (0x(nil))
  "-Dsun.java.command=Eucalyptus                                  " (0x(nil))
  "-Deuca.home=//                                                 " (0x(nil))
  "-Deuca.var.dir=//var/lib/eucalyptus                            " (0x(nil))
  "-Deuca.run.dir=//var/run/eucalyptus                            " (0x(nil))
  "-Deuca.lib.dir=//usr/share/eucalyptus                          " (0x(nil))
  "-Deuca.conf.dir=//etc/eucalyptus/cloud.d                       " (0x(nil))
  "-Deuca.log.dir=//var/log/eucalyptus                            " (0x(nil))
  "-Deuca.version=2.0.0                                           " (0x(nil))
  "-Deuca.log.exhaustive.db=FATAL                                 " (0x(nil))
  "-Deuca.log.exhaustive.cc=FATAL                                 " (0x(nil))
  "-Deuca.log.exhaustive.user=FATAL                               " (0x(nil))
  "-Deuca.log.exhaustive.external=FATAL                           " (0x(nil))
  "-Deuca.log.level=DEBUG                                         " (0x(nil))
  "-Deuca.log.appender=console-log                                " (0x(nil))
  "-Deuca.db.port=9001                                            " (0x(nil))
  "-Deuca.db.host=127.0.0.1                                       " (0x(nil))
  "-Deuca.walrus.host=localhost                                   " (0x(nil))
  "-Deuca.remote.dns=true                                         " (0x(nil))
  "-Xdebug                                                        " (0x(nil))
  "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005  " (0x(nil))
  "-Dcom.sun.management.jmxremote                                 " (0x(nil))
  "-XX:+HeapDumpOnOutOfMemoryError                                " (0x(nil))
  "-XX:HeapDumpPath=//var/log/eucalyptus/                         " (0x(nil))
  "-Xmx512m                                                       " (0x(nil))
  "-Djava.class.path=//etc/eucalyptus/cloud.d://etc/eucalyptus/..." (0x(nil))
  "abort                                                          " (0x0x4018c0)

バラバラとパラメータを渡しています。おケツの “abort” の追加情報 0x4018C0 は、abort の際のコールバック関数ですね。表示が変ですが。

static void java_fail(void) { exit(1); }

int java_init(euca_opts *args, java_home_t *data) {
  ...
  opt[++x].optionString="abort";
  opt[x].extraInfo=java_fail;

エントリの jar ファイルも指定していませんので、ここではまだ起動はしません。単に、さっきの “JNI_CreateJavaVM” で、VM を作るだけです。かなめは、さきほどの java_load_bootstrapper() です。以下のマクロで別名ですので、euca_load_bootstrapper() を探します。

#define java_load_bootstrapper euca_load_bootstrapper

euca_load_bootstrapper() では、構造体インスタンス “bootstrap” を作ります。これが Java コードのエントリポイントになります。まず、クラスの取得です。

#define EUCA_MAIN "com/eucalyptus/bootstrap/SystemBootstrapper"

bootstrap.clazz=((*env)->FindClass(env,EUCA_MAIN))

次に、static なファクトリメソッドを呼び出して、ブートストラップのインスタンスを作ります。

bootstrap.constructor=(*env)->GetStaticMethodID(env, \
 bootstrap.clazz,euca_get_instance.method_name, \
 euca_get_instance.method_signature );

この後は、ここから始まる Java コードの仕事ですね。続きは後ほど。

# なぜにこうもツギハギ感溢れる作りになってるんでしょうね? Eucalyptus は

今なら JNI よりも、JNA というのがありますよー、と同僚に紹介されました。後で読む。

JNIより簡単にJavaとC/C++をつなぐ「JNA」とは(1/4)-@IT




私家版 Oracle Cheat Sheet

理解はしていても忘れてしまうが、見れば思い出す断片の羅列です。

PL/SQL

リンク: Oracle Database PL/SQLユーザーズ・ガイドおよびリファレンス 10.2

SQL*Plus の操作
  • $ sqlplus ID/PASS@HOST:1521/SID
  • コンソール出力をオンに: “set serveroutput on
  • ファイルの読み込み実行: “@C:\priv\2010\test.plsql
  • 直前のエラーの表示: “show errors
  • コメント /* コメント */, — コメント
    配列
  • int index な map
  • メソッド: count … 歯抜けで存在する数を返す, delete … 全て, delete(i) … Pascal 風 1 から添字, delete(i, j) … j は末尾自体なので注意, first … 以下は添字を返す, last, prior(i), next(i), exists(i)
  • コード断片

    諸々:

    -- Oracle Database PL/SQLユーザーズ・ガイドおよびリファレンス 10.2
    
    drop table foo;
    create table foo(k integer, v varchar2(30));
    insert into foo values(1, 'first');
    insert into foo values(2, 'second');
    insert into foo values(3, 'third');
    
    declare
      n1 number not null default 100;
      A$_#09 constant number := 1000; -- ID に利用可能な文字
      s1 char(3) := 'abc';
      type rectag is record (
        n number(3) default 200,
        s nvarchar2(30) := 'こんにちは' -- サイズ省略不可
      );
      rec rectag;
      type tabtag is table of rectag index by pls_integer;
      tab tabtag;
      type nestedtag is table of foo%rowtype;
      nested nestedtag := nestedtag();
      -- i pls_integer; -- 不要: ループ変数は暗黙で
    begin
      dbms_output.put_line('d0: ' || n1 || ', ' || s1);
      -- rec := tab(1); -- 参照しても生成されません
      tab(1).n := 300;
      for i in 2 .. 3 loop
        tab(i).s := 'さようなら';
      end loop;
      for i in 1 .. tab.count loop -- 他に loop, while ~ loop
        dbms_output.put_line('d0: ' || tab(i).n || ', ' || tab(i).s);
      end loop;
      if n1 < 0 then
        dbms_output.put_line('d1');
      elsif 0 <= n1 and n1 < 100 then
        dbms_output.put_line('d2');
      else
        dbms_output.put_line('d3');
      end if;
      case n1 /* 検索子式 */
        when 100 then
          dbms_output.put_line('d4');
        else
          dbms_output.put_line('d5');
      end case;
      case -- 検索 case 文
        when n1 = 100 then
          dbms_output.put_line('d6');
        else
          null;
      end case;
      declare /* ブロック静的スコープ */
        /* rec0 foo%rowtype; -- 不要: ループ変数は暗黙 */
        cursor cur0(arg number) is
         select * from foo where k >= arg for update;
         -- パラメータ付き cursoer
        n number;
      begin
        /* open cur0; -- エラー: 明示 open ならば fetch cur into rec0 */
        -- n := amp;tmp;
        n := 2;
        for rec0 in cur0(n) loop
          if rec0.k = 2 then
            update foo set k = k * 2 where current of cur0;
             -- "current of CURSOR" が使えるのは for update にだけ
          end if;
          if cur0%isopen then
           -- カーソル属性: %notfound, %found, %rowcont, %isopen
            dbms_output.put_line('cp0: is open');
          else
            dbms_output.put_line('cp1: not open');
          end if;
          dbms_output.put_line(rec0.k); -- これは当然、変更前の値
        end loop;
        open cur0(2);
        if cur0%isopen = true then
          dbms_output.put_line('cp1: is open');
        else
          dbms_output.put_line('cp1: not open');
        end if;
      end;
      nested.extend(10);
      nested(1).k := 500;
      nested(1).v := 'five hundred';
      raise_application_error(-20001, 'ほげほげ');
    exception
      /* 10 PL/SQLエラーの処理/事前定義のPL/SQL例外のまとめ */
      when no_data_found then
        dbms_output.put_line('error 0: ' || sqlcode || ', ' || sqlerrm);
      when others then
        dbms_output.put_line('error 1: ' || sqlcode || ', ' || sqlerrm);
    end;
    /
    

    トリガ:

    drop table foo;
    create table foo(k integer, v varchar2(30));
    
    create or replace trigger foo_trigger
     before insert or update on foo
     for each row
     when (new.k is not null)
    begin
      if inserting or updating then
        :new.v := :new.v || ' foo';
      end if;
    end;
    /
    
    insert into foo values(1, 'first');
    insert into foo values(2, 'second');
    insert into foo values(3, 'third');
    
    select * from foo;
    



    私家版 C++ Cheat Sheet

    理解はしていても忘れてしまうが、見れば思い出す断片の羅列です。

    真偽値 bool で true, false

    Standard Library

    リンク: vector – C++ Reference

    string
  • コンストラクタ: string(), string(string), string(string, pos, len = to_the_end), string(sz, len = to_the_end), string(n, ch), string(i, j)
  • メソッド: const char * c_str(), const char * data(), find(str, pos = 0), find(sz, pos = 0), find(ch, pos = 0) … pos は対象の位置, rfind(…), string substr(pos = 0, len = to_the_end), compare(), copy(szDst, len, pos = 0) … \0 は付かない
  • その他: operator<<(os, const & str), operator>>(is, & str), operator+(& str と ch, sz, psz の 2 項)
  • シーケンス共通
  • コンストラクタ: vector(), vector(n, x), vector(i, j), vector(v) … コピコン
  • メソッド: size(), empty(), swap(v), insert(i, x) … i の前に入る, insert(i, n, x), insert(i, iSrc, jSrc)? … もちろんコピーが入る, erase(i) ⇒ i の次, erase(i, j) … [i, j) を削除 ⇒ j の次, clear(), assign(i, j), assign(n, x), max_size(), pop_back(x), push_back(x), resize(n, x), get_allocator()
  • イテレータbegin(), end(), rbegin(), rend(), front() ⇒ begin(), back() ⇒ end() - 1
  • ::value_type, ::pointer, ::const_pointer, ::reference, ::const_reference, ::iterator, ::const_iterator, ::const_reverse_iterator, ::reverse_iterator, ::size_type, ::difference_type, ::allocator_type
  • vector
  • メソッド: ランダムアクセスで at(ind), operator[](ind). 拡大時要全コピーなので capacity(), reserve(n)
  • deque
  • ランダムアクセスで at(ind), operator[](ind). フロント O(1) で push_front(x), pop_front()
  • list
  • merge, remove, remove_if, reverse, pop_front, push_front, sort, splice, unique
  • set, multiset
  • set(cmp), set(i, j, cmp), set(s)
  • empty(), size(), max_size(), insert(x), insert(i, x) … i はヒント, insert(i, j), erase(i), erase(x), erase(i, j), clear(), swap(s), key_comp(), value_comp(), get_allocator()
  • find(x), count(x) ⇒ set ならば 0 か 1, lower_bound(x), upper_bound(x), pair(itr, itr> equal_range(x)
  • begin(), end(), rbegin(), rend()
  • map, multimap
  • map(cmp), map(i, j, cmp), map(m)
  • m[key] = val, i->second = val, 右辺 m[ind] ⇒ 無ければ新規 pair(key, val)
  • アルゴリズム
  • non-modifying: find(i, j, x), find_if(i, j, cmp) … 1stのみ, count(i, j, x), count_if(i, j, cmp), adjacent_find(i, j), adjacent_find(i, j, cmp), for_each(i, j, f) ⇒ f, bool mismatch(i, j, iAnother), bool equal(i, j, iAnother), bool search(iHay, jHay, iNdl, jNdl),
  • modifying: copy(i, j, iDest) ⇒ コピー先の尻の次, copy_backward(i, j, jDest) ⇒ コピー先の頭, fill(i, j, x), fill(i, n, x), generate(i, j, f) … f に引数は無い, partition(i, j, f) ⇒ 後半 ! f の頭, random_shuffle(i, j), erase(remove(v.begin(), v.end(), x), v.end()), replace(i, j, x, y), reverse(i, j), rotate(i, k, j) … [i, j) の k が新しい頭になる, swap(cnt1, cnt2), swap_ranges(i, j, iOther), transform(…), unique(i, j) … 返り値は remove() と同方式,
  • ソート:
  • 関数 obj
  • bind1st, bind2nd



  • 私家版 JavaScript Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/javascript-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/javascript-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    私家版 Python Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/python-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/python-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    私家版 Scala Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/scala-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/scala-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    Scala: 中置記法→逆ポーランド記法のパーサ練習

    コップ本の第 31 章「パーサー・コンビネーター」の初っ端のサンプルです。とりあえず、いきなり Scala によるパーサジェネレータの記述方法を見せられたらビビると思われます。私はビビった。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[Any] = term ~ rep("+" ~ term | "-" ~ term)
      def term: Parser[Any] = factor ~ rep("*" ~ factor | "/" ~ factor)
      def factor: Parser[Any] = floatingPointNumber | "(" ~ expr ~ ")"
    }
    

    実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res9: Arith.ParseResult[Any] =
     [1.24] parsed: ((1~List())~List((+~(2~List())),
     (-~(3~List((*~4), (/~(((~((5~List())~List((+~(6~List())))))~))))))))
    
    scala>
    

    良いようですが、省略記法の使いすぎで、パッと見、何をやっているのかがさっぱり分かりません。省略せずに、書き下してみます。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[Any] =
       term.~(rep((literal("+").~(term)).|(literal("-").~(term))))
      def term: Parser[Any] =
       factor.~(rep((literal("*").~(factor)).|(literal("/").~(factor))))
      def factor: Parser[Any] =
       floatingPointNumber.|(literal("(").~(expr).~(literal(")")))
    }
    

    パーサ間で「演算」を繰り返して、新しいパーサを構築していくプロセスが見えます (いや、あいかわらず分かりづらいのは同じですが)。実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res10: Arith.ParseResult[Any] =
     [1.24] parsed: ((1~List())~List((+~(2~List())),
     (-~(3~List((*~4), (/~(((~((5~List())~List((+~(6~List())))))~))))))))
    
    scala> res9.toString == res10.toString
    res11: Boolean = true
    
    scala>
    

    以上の例では、生成されたパーサが変換処理を含まないので、出力はデフォルトの垂れ流しで、分かりづらいです。そこで、練習がてら、中置記法から逆ポーランド記法への変換パーサのジェネレータに修正してみます。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[String] = term ~ rep("+" ~ term | "-" ~ term) ^^ {
       case term ~ rest => reduce(term, rest) }
      def term: Parser[String] = factor ~ rep("*" ~ factor | "/" ~ factor) ^^ {
       case factor ~ rest => reduce(factor, rest) }
      def reduce(x: String, pairs: List[~[String, String]]) =
        (x /: pairs) ((x, pair) => "%s %s %s".format(x, pair._2, pair._1))
      def factor: Parser[String] = floatingPointNumber | "(" ~> expr <~ ")"
    }
    

    実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res12: Arith.ParseResult[String] = [1.24] parsed: 1 2 + 3 4 * 5 6 + / -
    
    scala>
    

    この機能があれば、外部 DSL のパーサが、外部ツールの助けなしにチョチョイのチョイで書けてしまうわけですね。すごいですねぇ…。




    Scala: “match” 省略でのパターンマッチ用法のまとめ

    コップ本 15.7.2 「部分関数としてのケースシーケンス」で、match-case ブロックの説明が割とサラッと流されてしまった印象で、”match” を省略してのパターンマッチの用法がよく分からなかったので、まとめてみました。まず最初に、プレースホルダの省略過程を見てから、同じような要領で “match” の省略用法を見てみます。

    まずは、プレースホルダで部分適用された println 関数。変数の型を元に、プレースホルダを含む式の型を推論してくれる。

    val p0: String => Unit = println(_)
    p0("Hello");
    

    これもそう。プレースホルダが一つしかないので、省略が可能。

    val p1: String => Unit = println
    p1("Hello");
    

    こちらは逆に、部分適用された関数のリテラルの型を指定し、それを元に変数の型を推論する。括弧が無いと、先に部分適用がなされてしまうらしく、型の推測ができない。

    /* val p2 = println(_): (String => Unit) NG */
    val p2 = (println(_)): (String => Unit) // OK
    p2("Hello");
    

    同上、プレースホルダの省略。

    val p3 = println: String => Unit
    p3("Hello");
    

    以上と似たようなノリで、以下、match 式の “match” を省略する過程です。最初に、普通に関数で書いた場合。

    def m(o: Option[Int]): Int = o match { case Some(x) => x case None => 0 }
    (m(Some(256)), m(None)) // (256,0)
    

    続いて、同じものを関数リテラルで書いてみます。プレースホルダを含むマッチ式の型を、変数の型から推論できる例。

    val m0: Option[Int] => Int = _ match { case Some(x) => x case None => 0 }
    (m0(Some(256)), m0(None)) // (256,0)
    

    その場合、プレースホルダが 1 つなので、”match” ごと省略が可能。

    val m1: Option[Int] => Int = { case Some(x) => x case None => 0 }
    (m1(Some(256)), m1(None)) // (256,0)
    

    次に、逆の例。match 式という無名関数の型から、変数の型を推論してくれる。これも先程同様、括弧が必須。

    val m2 = (_ match { case Some(x) => x case None => 0 }): (Option[Int] => Int)
    (m2(Some(256)), m2(None)) // (256,0)
    

    同様に、プレースホルダが 1 つなので、今度は “match” ごと省略が可能。

    val m3 = { case Some(x) => x case None => 0 }: (Option[Int] => Int)
    (m3(Some(256)), m3(None)) // (256,0)
    

    という用法だったようです。

    ついでに、どうもケースクラスやコンストラクタパターンによるパターンマッチが、実際に何をしているのかがピンとこなかったので、デコンパイルしてみました。

    object Test {
      val withDefault: Option[Int] => Int = {
        case Some(123) => 789
        case Some(x) => x
        case None => 0
      }
      def main(args: Array[String]): Unit = {
        val n: Int = withDefault(Some(256))
        println(n)
      }
    }
    

    …あまりきれいにデコンパイルできませんでしたが、まあいいか。

      public final int apply(Option option) {
          Option option1 = option;
          if (! (option1 instanceof Some))
            goto _L2;
          else
            goto _L1
    _L1:
          int i;
          Some some = (Some) option1;
          i = BoxesRunTime.unboxToInt(some.x());
          return (i != 123)? i: 789;
          goto _L3
    _L2:
          None$.MODULE$;
          Option option2 = option1;
          if (None$.MODULE$ != null)
            goto _L5;
          else
            goto _L4
    _L4:
          JVM INSTR pop ;
          if(option2 == null)
            goto _L7;
          else
            goto _L6
    _L5:
          option2;
          equals();
          JVM INSTR ifeq 69;
          goto _L7 _L6
    _L7:
          false;
    _L3:
          return;
    _L6:
          throw new MatchError(option1);
      }
    

    要は、マッチ対象のインスタンスに isinstanceof をかけ、実際に各項を getter で取得してきてからゴリゴリと比較しているんですね。各マッチパターンごとに、型と定数まで含めて同定と分岐ができるように switch-case 文を一般化したものと言えそうです。さらには、match-case 式は値を返すので、パターンごとに、型安全なダウンキャストをしてから各オーバーロード関数の呼び出しに振り分けているようなもの、と見ることも可能かと思います。

    いや、面白い。




    Scala: コンストラクタ中の一時オブジェクトに消えて欲しい

    Scala については、まだまだ学習中の身ですが、このへんを越えないと安心できない気がするので、書きます。

    Scala クラスの基本コンストラクタ (primary constructor) では、クラス定義、コンストラクタ定義、フィールド定義が一緒くたに書けるので、大抵の場合は既存の OOP 言語のそれらと比べて、記述がスッキリして、とても良いです。

    class Point(val x: Int, val y: Int)
    val p = new Point(3, 4)
    println(p.x + ", " + p.y) // 3, 4
    

    ところが、コンストラクタ中で一時変数が必要になると、何やら気持ち悪いことになります。

    例としては、「コップ本」こと「Scala スケーラブルプログラミング」から。コップ本の最初の OOP の例で、有理数をとりあげています。最大公約数を求めて分母・分子を約分するのですが、基本コンストラクタ内で求めた最大公約数が、このクラスのインスタンスが存続する限りフィールドとして残ってしまいます、もう不要なのに。

    class Rational (
      numArg: Int,
      denArg: Int ) {
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
      private val n = gcd(numArg.abs, denArg.abs) // GCD を計算
      val num = numArg / n
      val den = denArg / n
      // private だからアクセスはできないものの、この後、n は永続
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    うーん…。

    なお、基本コンストラクタの引数は、普通に記述すると、インスタンスの private なフィールドになりますが、val や var で宣言すると、値は隠しフィールドに代入され、そこへのアクセサが自動生成されます (「パラメータフィールド」と呼ぶそうです)。どうやら、Scala のアクセスコントロールが JRE のそれよりも細かいので、アクセサで何とかしているようですよ。多分。下記参照:

    間違ってたら直します。

    追記 (Wed Oct 27 2010): よく考えたら、overide や lazy にも必要ですね。

    それならばと、分子・分母を reader にすれば、n は不要にはならないので、気分的には若干マシになります。けれども、何の解決にもなっていません。

    class Rational (
      numArg: Int,
      denArg: Int ) {
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
      private val n = gcd(numArg.abs, denArg.abs)
      def num = numArg / n // 呼び出し時の評価…
      def den = denArg / n
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    どうしたものかといろいろやっていましたが、当然すでに、いろいろと議論があります。

    上記を参考にしつつ、いろいろやってみます。要は、一時変数をスタック上に確保できれば良いのです。

    まずは、基本 constructor を private にして、代替 constructor から呼ぶことを考えましたが…、残念、通りません。Scala の代替 constructor では、まず最初に基本 constructor を呼ばなければならないルールになっています。インスタンスの初期化以前にいろいろやるな、というお達しでしょうが、厳しいです。

    class Rational private ( // コンパイルが通りません
      val num: Int,
      val den: Int,
      dummy: Unit ) {
      def this(num: Int, den: Int) = {
        def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
        val n = gcd(num.abs, den.abs)
        this(num / n, den / n, ())
      }
    }
    

    次に、コンパニオンオブジェクトで、ファクトリメソッドを用意します。apply にしたところで、話は同じ。これは悪くなさそうですが、初期化引数の正規化は、ファクトリではなくコンストラクタの仕事にしたいかなぁ…。インスタンス生成のしかたも変わってしまいますし。

    class Rational(val num: Int, val den: Int)
    object Rational {
      def create(num: Int, den: Int) = {
        def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
        val n = gcd(num.abs, den.abs)
        new Rational(num / n, den / n)
      }
    }
    val r = Rational.create(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    それではと、フィールドの生成に、タプルを使ってみます。一見良いものの、今度は Tuple2 が private フィールドに残ります (自動的に付与される名前の “x$1″ とかで、アクセスできたりする。Scala 的には、メンバとしての位置づけなんだろう)。ですので、かえってデカいんじゃ?

    class Rational (
      numArg: Int,
      denArg: Int ) {
      val (num, den) = {
        val n = gcd(numArg.abs, denArg.abs)
        (numArg / n, denArg / n)
      }
      private def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    逆アセンブルすると、以下のような具合です:

    $ javap -c -private Rational
    Compiled from "Rational.scala"
    public class Rational extends java.lang.Object implements scala.ScalaObject{
    private final int den;
    
    private final int num;
    
    private final scala.Tuple2 x$1;
    
    public Rational(int, int);
      Code:
    (中略)
    $
    

    これで最後。いろいろやってみましたが、一時変数が lexical scope 内に入るので、これが一番綺麗かな? フィールドを、private とはいえ var にせざるを得ないので、アクセスコントロール (リードオンリー) は accessor で行ないます。ブロックを、単なるブレースで括ろうとすると、ここでは print からの戻り値への行継続で、関数の引数と解釈されてコンパイルがコケますので、do-while-false ループ (C 言語で、複文を、値を返さない式の位置に書く際に使うイディオム) にしてみます (素直にセミコロンを置いても良いのですが、カッコ悪いので)。

    class Rational (
      private var numField: Int, // 仕方がないので var にします
      private var denField: Int ) {
      print("Initializing ...")
      do { // C でよくやるループ
        val n = gcd(numField, denField) // これはスタック上に確保される
        numField /= n // フィールド変数へ再代入 (実際はアクセサ)
        denField /= n
      } while (false) // 最適化されるので、ループにはならない (多分…)
      println(" Done.")
      def num = numField // public かつ read-only
      def den = denField
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    一応、do-while-false がループになっていないことと、num と den がリードオンリーであることと、numField と denField へのアクセスが private のアクセサを経由していることを確認します。

    $ javap -c -private Rational
    Compiled from "Rational.scala"
    public class Rational extends java.lang.Object implements scala.ScalaObject{
    private int denField;
    
    private int numField;
    
    public Rational(int, int);
     Code:
      0:  aload_0
      1:  iload_1
      2:  putfield       #13; //Field numField:I
      5:  aload_0
      6:  iload_2
      7:  putfield       #15; //Field denField:I
      10: aload_0
      11: invokespecial  #20; //Method java/lang/Object."<init>":()V
      14: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
      17: ldc            #28; //String Initializing ...
      19: invokevirtual  #32; //Method scala/Predef$.print:(Ljava/lang/Object;)V
      22: aload_0
      23: iload_1
      24: iload_2
      25: invokevirtual  #36; //Method gcd:(II)I
      28: istore_3
      29: aload_0
      30: iload_1
      31: iload_3
      32: idiv
      33: invokespecial  #40; //Method numField_$eq:(I)V
      36: aload_0
      37: iload_2
      38: iload_3
      39: idiv
      40: invokespecial  #43; //Method denField_$eq:(I)V
      43: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
      46: ldc            #45; //String  Done.
      48: invokevirtual  #48; //Method scala/Predef$.println:(Ljava/lang/Object;)V
      51: return
    
    (中略)
    
    public int den();
      Code:
       0: aload_0
       1: invokespecial  #58; //Method denField:()I
       4: ireturn
    
    public int num();
      Code:
       0: aload_0
       1: invokespecial  #61; //Method numField:()I
       4: ireturn
    
    private void denField_$eq(int);
      Code:
       0: aload_0
       1: iload_1
       2: putfield       #15; //Field denField:I
       5: return
    
    private int denField();
      Code:
       0: aload_0
       1: getfield       #15; //Field denField:I
       4: ireturn
    
    private void numField_$eq(int);
      Code:
       0: aload_0
       1: iload_1
       2: putfield       #13; //Field numField:I
       5: return
    
    private int numField();
      Code:
       0: aload_0
       1: getfield       #13; //Field numField:I
       4: ireturn
    
    (中略)
    
    }
    
    $
    

    Scala は、総じては超ステキ言語なのですが、時たま、策士が策に溺れている感があります。そんなところも大好きですが。




    メモ: JPUG 第 18 回しくみ + アプリケーション勉強会

    下記に参加しました:

    # 懇親会、出たかったなー

    自分用メモです:

    - [第 1 部: ECとPostgreSQL] (1) EC-CUBEにおけるPostgreSQLの利用事例
      - 株式会社ロックオン 福田さん @fukuran
      - 株式会社ロックオン--Impact On The World--
        - AD EBiS
        - EC-CUBE
      - ネット店舗には、ワクワク感が無くないか?
      - 傾向
        - 構築コストは低いが独自性の低い: モール/ASP
        - 構築コストは高いが独自性の高い: 独自構築・パッケージ利用
        - 低コストで独自性の高いサイトが作れないか?→EC-CUBE
      - 月間統計
        - 5:3:2 で M50, M51, P8*
        - P は、レンサバに無いのが弱い
          - スパイラルだ
          - アプリの対応を進めることが有効か
      - history
        - 1.0 は P にのみ対応
        - RHEL4 M41 に対応した
          - M41 には view が無かった
          - 仕方なく、MySQL は join で作った
          - 抽象化が弱かった
          - 2.5 系で解消の予定
      - P の ver での差
        - bench は単純すぎ
        - 8.1 → 8.3 で、7 min → 1 sec
          - アドホックな処理の、planner による最適化?
      - 16,000,000/hour とか動きます
        - チューン次第
      - security
        - スクラッチよりは安全でしょ。実績あるし
          - naturum 65万件 16500 件苦情
          - mont-bell 1万人クレカ情報, 100 人分でコンサートチケット不正利用
            - 実害が出た
        - カード情報は保存しない
          - ユーザは不便?
            - 今どき、決済代行側で保存してくれる
            - PCIDSS
      - 収益
        - EC サービスは、それだけでは完結しない
          - 決済やら何やら
      - 良いこと
        - 3/week カスタム, 6/y 受託, 忍月 100~120 万は行ける
        - 知名度工場→採用コストdown
        - 各種パートナとのつきあい
      - 悪いこと
        - バグも技術も丸見え。脆弱性大変
        - サポート大変
          - やってない (カスタマイズしすぎで手に負えなかったり)
        - 単独では儲からない
          - 維持に専属 4 名
          - 3y かけて単月黒字
      - M P 差
        - 腕次第
        - マシン次第
        - どっちかできれば他方もできる
      - EC-CUBE の見分け方
        - 住所でググる
    - [第 1 部: ECとPostgreSQL] (2) ECサイト構築でPostgreSQLを採用するメリット
      - 湘南秘密結社 Gangsta 高津さん @cstyles_jp
      - 湘南藤沢・横浜のマーケティング秘密結社ギャングスタ - Gangsta.jp
      - EC サイト、丸構築
      - SR 導入しました
      - Gangsta
      - EC サイト分類
        - モール
        - ASP
        - パッケージ
        - OSS
        - フルスクラッチ
      - 突然
        - ニュース
        - ヤフートピックス←雑誌
        - twitter, flash マーケ
      - 連携
        - 在庫管理やPOSと連携
        - 大量DB
        - Hadoop
      - 売れること
        - 高速で的確ヒットな検索, トレードオフ
        - カテゴリ
          - テーブル分割, group by 遅い
          - web 屋のネタ帳
            - 配列データ, GIN
            - n 対 n, 複合条件とか効く
            - 数十万とか効く
            - P 依存…
        - フリーワード
          - n-gram
            - blog ならともかく、商品名なんか形態素解析じゃ出てこない
            - そこで n-gram
          - 大文字小文字、半角全角吸収
          - で、textsearch_senna
            - using senna で
            - 1~20,000 件だと、使わない方が速かったり
            - 計測せよ
            - vacuum full でインデックス作り直し?
              - pg_indexes の indexdef を検索して senna 消し
            - P9, lsyncd と rsync で同期
              - 同期インターバル
            - groonga が始まっている, 板垣さん
      - トランザクション
        - 結構つかっていない
        - "&" さん。エスケープしていない
        - MyISAM 上がりで多い
        - M5.5 の InnoDB 200% だそうな
      - ldave 120
        - VPS 2 web
        - PgPool-II
          - P9 で slave たくさん
          - master 負荷がそんなに下げてないので、秒単位ラグあり
            - 更新直後 slave に次の受注番号取りに
            - flash マーケとか、もう画面更新できない
          - pgpool-II で reindex
      - cloud 化
        - 難しい
      - P9
       - PDO で使ってない JOIN の削除とかする?
      - pgAdmin-III
        - レプリは Slony だな…
        - psql はアプリ屋には遠い…
    - [第2部:レプリケーション] (3)運用Tipsシリーズ part1InterDB
      - InterDB 鈴木さん
      - SR + HS, 構築・運用ノウハウ
      - 分類
        - 1 slave
        - N slave
        - 消失不可
        - ディザスタリカバリ WAN, 1 slave, 消失許容
      - 分散
        - pgpool
        - VPS?
      - pgstby では、アーカイブは作られないが、pg_xlog は出るのか
        - てことは、最新ノードを選べばそこから継続可能?
        - ps でも分かるかな?
      - 想定
        - やっぱ slave 2 台よね (max_wal_senders 注意)
        - rsync (共有は用いない)
      - max_connections - supersuser_reserved_connections を、一般ユーザ ("
         replication" は常に一般扱い!)と max_wal_senders で取り合う
        - 9.1 で変わるかも
      - 何台行けるかベンチをしたい
        - wal_sender_delay を長くして master 負荷を下げたり
      - マスターがコケるケース
        - trigger のパーミッションに注意
          - 200ms
          - 9.1 で、signal で行けるようになるかも
        - timeline が増加する
        - HA 誰がする?
          - pgpool-II v3
          - HA
        - データロストと、データ不一致
      - マスタ障害 (公式)
        - 復旧手順
          - スレーブs停止
          - 新マスタをコピー
            - 旧スレーブ削除
          - recovery.conf 書きかえ
          - スレーブ起動
        - 0-3 でスレーブはサービス停止
        - 1 のベースバックアップに時間がかかる
      - リストア
        - 1億 8G 1000万件更新
      - 非公式
        - 下記をふまえて?
          - もっとレプリケーションのノウハウを - PostgreSQL 雑記 - postgresqlグループ
        - recovery_target_timeline='2' // 明示?
        - archive 数本で済む
      - 不一致
        - 実験→できた
      - 逆不一致
        - REDO ポイントから
        - slave2 は slave1 をやり直す
      - 条件
        - redo location が進みすぎてない
        - アーカイブログコピー
      - 楽観的リカバリ
        - 新マスタ recovery_end_command= pg_controldata で
          redo location 保存
        - 急いで止めないと
          - REDO ポイント進んじゃう
          - 新マスタは、前の TL の WAL アーカイブを持っているんでしたっけ?
        - うまく行けばこれで
        - ダメなら公式で
        - 進んでしまった方をマスターに
        - リカバリコマンドで、マスターから取ってくる、とかしても
      - 課題
        - slave 数、performance
        - 非公式手順のbrush up
        - WALログの xlogdump
        - 最適なスレーブのリカバリ手法
          - 次は HA
    - [第2部:レプリケーション] (4)pgpool-II version3.0の紹介
      - 北川さん@SRA-OSS
      - loid 対応
      - masterslave_sub_mode
        - stream
        - hotstby ダメ query のマスタ行き
        - 遅延ノード監視 (時間じゃない。location の差で、バイト数求める)
          - log_stby_delay で、バイト数監視
        - 同一 transaction で update 後 select はマスタへ
        - 同一処理の 2 トランザクション間で分散させると見えない
    - 次回
      - 12/11 調整中
      - テーマ未定
        - explaining explain やるか
          - explain から pgAdmin-III でチューニング支援とかある?
    - 前回資料で分からなかったところを、藤井さんに教えてもらったこと
      - visibility 情報を、slave 側は、wal を元に共有メモリに再現して持たな
        きゃならない。それが、conf と食い違うと困る。
      - SR で取得すれば? との議論では、WAL アーカイブ転送 + HS ができないの
        で却下
    



    Fedora の Scala の RPM を CentOS でビルド

    Scala とだったら、JRE とうまくやっていけるんじゃないかという予感がしています。

    Fedora では、yum 一発で入ってくれるので助かります。

    [root@fedora ~]# yum install scala
    [root@fedora ~]#
    

    一方の CentOS-5.5 ですが、Scala が yum レポジトリ上にないのもさることながら、さすがに Java 関連のモジュールがいいかげん古くて、ビルドもままなりません。仕方ないので、Fedora のソースからビルドします。下記の各パッケージの情報ページ上部の “Build” の先から、新しい目の source rpm をいただいてきます:

    バージョンの上下でモグラ叩きになりますが、どうにか以下のような感じで順にビルドし、インストールします。ant は最低でも 1.7 でないと、scala のコンパイルに失敗します。よく見ていませんが、scala-2.8.0 は document まわりのコンパイルにコケていたので、2.7.7 を利用しています。充分ですよね?

    ant-contrib だけは EPEL のレポジトリから入れます。手順の最後に、標準以外のレポジトリから入れている ant-contrib と、標準パッケージと競合する ant-1.8 は remove しています。

    なお、下記は root による 実行例ですが、一般論として特権ユーザによる RPM の作成は危険ですので、普通は ~/.rpmmacros を用意して、一般ユーザで実行しましょう。それと、例によって Fedora の RPM キーは何か壊れているようで、MD5 のチェックが通りません。インストールには “–nomd5″ オプションが必要になっています。

    [root@node1 ~]# yum install -y rpm-build
    [root@node1 ~]# host=kojipkgs.fedoraproject.org
    [root@node1 ~]# wget \
     http://$host/packages/shtool/2.0.8/4.fc14/src/shtool-2.0.8-4.fc14.src.rpm \
     http://$host/packages/jline/0.9.94/0.6.fc14/src/jline-0.9.94-0.6.fc14.src.rpm \
     http://$host/packages/ant/1.8.1/6.fc15/src/ant-1.8.1-6.fc15.src.rpm \
     http://$host/packages/scala/2.7.7/1.fc13/src/scala-2.7.7-1.fc13.src.rpm
    [root@node1 ~]# rpm -i --nomd5 \
     shtool-2.0.8-4.fc14.src.rpm \
     jline-0.9.94-0.6.fc14.src.rpm \
     ant-1.8.1-6.fc15.src.rpm \
     scala-2.7.7-1.fc13.src.rpm
    [root@node1 ~]# topdir=/usr/src/redhat/
    [root@node1 ~]# rpmbuild -ba $topdir/SPECS/shtool.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/shtool-2.0.8-4.noarch.rpm
    [root@node1 ~]# yum install -y ant junit
    [root@node1 ~]# rpmbuild --without maven -ba $topdir/SPECS/jline.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/jline-0.9.94-0.6.noarch.rpm
    [root@node1 ~]# perl -p -i -e 's/(jpackage-utils) >= .*/\1/' \
     $topdir/SPECS/ant.spec
    [root@node1 ~]# rpmbuild --with bootstrap --without gcj_support \
     -ba $topdir/SPECS/ant.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/ant-1.8.1-6.noarch.rpm \
     $topdir/RPMS/noarch/ant-nodeps-1.8.1-6.noarch.rpm
    [root@node1 ~]# 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@node1 ~]# yum install -y ant-contrib
    [root@node1 ~]# rpmbuild -ba -D "fedora %nil" $topdir/SPECS/scala.spec
    [root@node1 ~]# yum remove -y ant ant-nodeps epel-release
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/scala-2.7.7-1.noarch.rpm \
     $topdir/RPMS/noarch/scala-examples-2.7.7-1.noarch.rpm
    [root@node1 ~]# scala -e \
     'println(List("foo", "hoge").map((s: String) => s.length).mkString("\n"))'
    3
    4
    [root@node1 ~]#
    

    一度ビルドできれば、次からはバイナリだけ入れれば OK でしょう。SRPM 無修正という縛りプレイで行こうとしたのですが、残念なことに、jpackage-utils のバージョンだけ、静的に修正しています。

    Sun JDK の手順も、後で試しておきます。Hadoop の絡みで、OpenJDK では行けないケースもありますので。




    Hadoop の擬似分散モードを試す

    せっかくだから俺は Cloudera のバイナリを入れるぜ。さて、RPM の依存関係を見ると、OpenJDK ではなく Sun JDK に依存しています。

    Hadoopは基本的にSun JDKのみでテストされているので、OpenJDKでの使用は危険らしいです

    了解。Cloudera バイナリを入れるノードだけは、何とか Sun JDK で行きます。

    まず、Sun Java のサイトから、JDK の RPM 配布を入手しておきます:

    OpenJDK を抜いて、Sun Java を入れます。

    [root@node1 ~]# yum remove -y java-1.6.0-openjdk
    (中略)
    Removed:
      java-1.6.0-openjdk.x86_64 1:1.6.0.0-1.13.b16.el5
    
    Dependency Removed:
      openoffice.org-calc.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-core.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-draw.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-graphicfilter.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-impress.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-langpack-ja_JP.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-math.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-ure.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-writer.x86_64 1:3.1.1-19.5.el5_5.1
      openoffice.org-xsltfilter.x86_64 1:3.1.1-19.5.el5_5.1
    
    Complete!
    [root@node1 ~]# ./jdk-6u21-linux-x64-rpm.bin
    (中略)
    Press Enter to continue..... <Enter>
    
    Done.
    [root@node1 ~]#
    

    下記が入ります:

    • jdk
    • sun-javadb-common
    • sun-javadb-core
    • sun-javadb-client
    • sun-javadb-demo
    • sun-javadb-docs
    • sun-javadb-javadoc

    次に、Cloudera のバイナリを Yum で入れます。まず下記のサイトを参照し、ひとまず無難に “Stable” となっているバージョンを確認します:

    現在のところ、Release: “CDH2″ の Status が “Stable” のようですので、これを入れます。

    [root@node1 ~]# ( cd /etc/yum.repos.d/ &&
     wget http://archive.cloudera.com/redhat/cdh/cloudera-cdh2.repo )
    (中略)
     2010-10-18 21:01:35 (18.3 MB/s) - `cloudera-cdh2.repo' へ保存完了 [211/211]
    
    [root@node1 ~]#
    

    インストールします。

    [root@node1 ~]# yum install -y hadoop
    (中略)
    Installed:
      hadoop-0.20.noarch 0:0.20.1+169.113-1
    
    Complete!
    [root@node1 ~]#
    

    Hadoop にもいくつかバージョンがあるようですが、とりあえず最新として出てきた 0.20 とやらを使ってみます。設定ファイルも RPM の alternatives を使って選ぶ形式になっているようで、ひとまず、1 台構成で試験的に HDFS と MapReduce 使うための「擬似分散モード」とやらのためのパッケージを入れます。

    [root@node1 ~]# yum install -y hadoop-conf-pseudo
    Installed:
      hadoop-0.20-conf-pseudo.noarch 0:0.20.1+169.113-1
    
    Complete!
    [root@node1 ~]#
    

    HDFS (≒GFS) の NameNode サービスを起動します。

    [root@node1 ~]# service hadoop-0.20-namenode start
    Starting Hadoop namenode daemon (hadoop-namenode): starting namenode, logging
     to /usr/lib/hadoop-0.20/bin/../logs/hadoop-hadoop-namenode-node1.priv.out
                                                               [  OK  ]
    [root@node1 ~]#
    

    50070 番ポートに HTTP でアクセスすると、HDFS の状態が表示されます:

    続けて、DataNode のサービスを起動します。

    [root@node1 ~]# service hadoop-0.20-datanode start
    Starting Hadoop datanode daemon (hadoop-datanode): starting datanode, logging
     to /usr/lib/hadoop-0.20/bin/../logs/hadoop-hadoop-datanode-node1.priv.out
                                                               [  OK  ]
    [root@node1 ~]#
    

    ノードが増えて、Remaining が増しました:

    次に、Hadoop MapReduce の JobTracker サービスを起動します。

    [root@node1 ~]# service hadoop-0.20-jobtracker start
    Starting Hadoop jobtracker daemon (hadoop-jobtracker): starting jobtracker,
     logging to /usr/lib/hadoop-0.20/bin/../logs/hadoop-hadoop-jobtracker-node1.priv.out
                                                               [  OK  ]
    [root@enode1 ~]#
    

    50030 番ポートに HTTP でアクセスすると、JobTracker の状態が表示されます:

    最後に、TaskTracker サービスを起動します。

    [root@node1 ~]# service hadoop-0.20-tasktracker start
    Starting Hadoop tasktracker daemon (hadoop-tasktracker): starting tasktracker,
     logging to /usr/lib/hadoop-0.20/bin/../logs/hadoop-hadoop-tasktracker-node1.priv.out
                                                               [  OK  ]
    [root@enode1 ~]#
    

    map/reduce をするノードが増えました:

    サンプルの中の、grep を実行してみます。

    [root@node1 ~]# hadoop fs -mkdir input
    [root@node1 ~]# hadoop fs -put /usr/share/doc/glibc-2.5/COPYING input
    [root@node1 ~]# hadoop fs -mkdir output
    [root@node1 ~]# hadoop fs -ls
    Found 1 items
    drwxr-xr-x   - root supergroup     0 2010-10-20 00:43 /user/root/input
    [root@node1 ~]# hadoop fs -ls input
    Found 1 items
    -rw-r--r--   1 root supergroup 18009 2010-10-20 00:43 /user/root/input/COPYING
    [root@node1 ~]# hadoop jar \
     /usr/lib/hadoop-0.20/hadoop-0.20.1+169.113-examples.jar grep input output GNU
    10/10/20 00:46:14 INFO mapred.FileInputFormat: Total input paths to process : 1
    10/10/20 00:46:14 INFO mapred.JobClient: Running job: job_201010200026_0003
    10/10/20 00:46:15 INFO mapred.JobClient:  map 0% reduce 0%
    10/10/20 00:46:22 INFO mapred.JobClient:  map 100% reduce 0%
    10/10/20 00:46:35 INFO mapred.JobClient:  map 100% reduce 100%
    10/10/20 00:46:37 INFO mapred.JobClient: Job complete: job_201010200026_0003
    10/10/20 00:46:37 INFO mapred.JobClient: Counters: 18
    10/10/20 00:46:37 INFO mapred.JobClient:   Job Counters
    10/10/20 00:46:37 INFO mapred.JobClient:     Launched reduce tasks=1
    10/10/20 00:46:37 INFO mapred.JobClient:     Launched map tasks=2
    10/10/20 00:46:37 INFO mapred.JobClient:     Data-local map tasks=2
    10/10/20 00:46:37 INFO mapred.JobClient:   FileSystemCounters
    10/10/20 00:46:37 INFO mapred.JobClient:     FILE_BYTES_READ=34
    10/10/20 00:46:37 INFO mapred.JobClient:     HDFS_BYTES_READ=21294
    10/10/20 00:46:37 INFO mapred.JobClient:     FILE_BYTES_WRITTEN=138
    10/10/20 00:46:37 INFO mapred.JobClient:     HDFS_BYTES_WRITTEN=106
    10/10/20 00:46:37 INFO mapred.JobClient:   Map-Reduce Framework
    10/10/20 00:46:37 INFO mapred.JobClient:     Reduce input groups=1
    10/10/20 00:46:37 INFO mapred.JobClient:     Combine output records=2
    10/10/20 00:46:37 INFO mapred.JobClient:     Map input records=340
    10/10/20 00:46:37 INFO mapred.JobClient:     Reduce shuffle bytes=40
    10/10/20 00:46:37 INFO mapred.JobClient:     Reduce output records=1
    10/10/20 00:46:37 INFO mapred.JobClient:     Spilled Records=4
    10/10/20 00:46:37 INFO mapred.JobClient:     Map output bytes=96
    10/10/20 00:46:37 INFO mapred.JobClient:     Map input bytes=18009
    10/10/20 00:46:37 INFO mapred.JobClient:     Combine input records=8
    10/10/20 00:46:37 INFO mapred.JobClient:     Map output records=8
    10/10/20 00:46:37 INFO mapred.JobClient:     Reduce input records=2
    10/10/20 00:46:37 WARN mapred.JobClient: Use GenericOptionsParser for parsing
     the arguments. Applications should implement Tool for the same.
    10/10/20 00:46:37 INFO mapred.FileInputFormat: Total input paths to process : 1
    10/10/20 00:46:37 INFO mapred.JobClient: Running job: job_201010200026_0004
    10/10/20 00:46:38 INFO mapred.JobClient:  map 0% reduce 0%
    10/10/20 00:46:47 INFO mapred.JobClient:  map 100% reduce 0%
    10/10/20 00:46:59 INFO mapred.JobClient:  map 100% reduce 100%
    10/10/20 00:47:01 INFO mapred.JobClient: Job complete: job_201010200026_0004
    10/10/20 00:47:01 INFO mapred.JobClient: Counters: 18
    10/10/20 00:47:01 INFO mapred.JobClient:   Job Counters
    10/10/20 00:47:01 INFO mapred.JobClient:     Launched reduce tasks=1
    10/10/20 00:47:01 INFO mapred.JobClient:     Launched map tasks=1
    10/10/20 00:47:01 INFO mapred.JobClient:     Data-local map tasks=1
    10/10/20 00:47:01 INFO mapred.JobClient:   FileSystemCounters
    10/10/20 00:47:01 INFO mapred.JobClient:     FILE_BYTES_READ=20
    10/10/20 00:47:01 INFO mapred.JobClient:     HDFS_BYTES_READ=106
    10/10/20 00:47:01 INFO mapred.JobClient:     FILE_BYTES_WRITTEN=72
    10/10/20 00:47:01 INFO mapred.JobClient:     HDFS_BYTES_WRITTEN=6
    10/10/20 00:47:01 INFO mapred.JobClient:   Map-Reduce Framework
    10/10/20 00:47:01 INFO mapred.JobClient:     Reduce input groups=1
    10/10/20 00:47:01 INFO mapred.JobClient:     Combine output records=0
    10/10/20 00:47:01 INFO mapred.JobClient:     Map input records=1
    10/10/20 00:47:01 INFO mapred.JobClient:     Reduce shuffle bytes=20
    10/10/20 00:47:01 INFO mapred.JobClient:     Reduce output records=1
    10/10/20 00:47:01 INFO mapred.JobClient:     Spilled Records=2
    10/10/20 00:47:01 INFO mapred.JobClient:     Map output bytes=12
    10/10/20 00:47:01 INFO mapred.JobClient:     Map input bytes=20
    10/10/20 00:47:01 INFO mapred.JobClient:     Combine input records=0
    10/10/20 00:47:01 INFO mapred.JobClient:     Map output records=1
    10/10/20 00:47:01 INFO mapred.JobClient:     Reduce input records=1
    [root@node1 ~]#
    

    このような進捗が表示されておりました。2 ステージのようです:

    結果を見てみます。

    [root@node1 ~]# hadoop fs -ls output
    Found 2 items
    drwxr-xr-x   - root supergroup 0 2010-10-20 00:46 /user/root/output/_logs
    -rw-r--r--   1 root supergroup 6 2010-10-20 00:46 /user/root/output/part-00000
    [root@node1 ~]# hadoop fs -cat output/part-00000
    8       GNU
    [root@node1 ~]#
    

    8 行だそうです。

    [root@node1 ~]# grep -c GNU /usr/share/doc/glibc-2.5/COPYING
    8
    [root@node1 ~]#
    

    8 行です。

    サンプルのソースはこれ (Grep.java) のようです。

    1 段目の mapper は、value として各ファイルの内容が入力、(マッチした文字列, 1)* を出力、reducer は summary で reduce してテンポラリに出力。2 段目はそれを入力として invert とするから、マッチ数でソートして出力、といったところでしょう。

    後でしっかりと読む:

    • org.apache.hadoop.examples.Grep
    • org.apache.hadoop.mapred.lib.RegexMapper
    • org.apache.hadoop.mapred.lib.InverseMapper



    PostgreSQL: DRBD + Keepalived (VRRP)

    PostgreSQL のデータ領域を DRBD で冗長化し、Keepalived の VRRP モードで HA 構成にしてみます。PostgreSQL-9.0 でストリーミング・レプリケーションやホットスタンバイの機能が加わったわけですが、非同期レプリケーションであることもあり、スレーブの破棄・追加は容易なのですが、マスターの破棄・追加がとても面倒くさいと感じたため、マスターは HA 化してしまおうと思います。

    参考:

    で、結論を先に言いますと、リソース管理の考え方のない Keepalived は、そのままだと、DB やファイルサーバには向いていないんではなかろうか、とも思います。いずれ HeartBeat もやってみます。

    最終的には、以下のような構成で:

    構成図

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

    面倒なので、今回は IPTABLES はオフにしておきます。

    [root@node1 ~]# service iptables stop
    ファイアウォールルールを適用中:                            [  OK  ]
    チェインポリシーを ACCEPT に設定中filter                   [  OK  ]
    iptables モジュールを取り外し中                            [  OK  ]
    [root@node1 ~]# chkconfig iptables off
    [root@node1 ~]#
    

    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 ~]# mkdir -m 0700 /pgdata/ /pg_xlog/
    [root@node1 ~]# chown postgres.postgres /pgdata/ /pg_xlog/
    [root@node1 ~]# su - postgres -c "initdb --pgdata=/pgdata/ --xlogdir=/pg_xlog/ \
     --encoding=UTF-8 --no-locale --username=admin --pwprompt --auth=md5"
    データベースシステム内のファイルの所有者は"postgres"ユーザでした。
    このユーザがサーバプロセスを所有しなければなりません。
    
    データベースクラスタはロケールCで初期化されます。
    デフォルトのテキスト検索設定はenglishに設定されました。
    
    ディレクトリ/pgdataの権限を設定しています ... ok
    ディレクトリ/pg_xlogの権限を設定しています ... 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
    template1データベースをバキュームしています ... ok
    template1からtemplate0へコピーしています ... ok
    template1からpostgresへコピーしています ... ok
    
    成功しました。以下を使用してデータベースサーバを起動することができます。
    
        postgres -D /pgdata
    または
        pg_ctl -D /pgdata -l logfile start
    
    [root@node1 ~]# cp /pgdata/postgresql.conf /pgdata/postgresql.conf.orig
    [root@node1 ~]# vi /pgdata/postgresql.conf
    [root@node1 ~]# diff -uNr /pgdata/postgresql.conf.orig /pgdata/postgresql.conf
    --- /pgdata/postgresql.conf.orig
    +++ /pgdata/postgresql.conf
    @@ -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)
    [root@node1 ~]# cp /pgdata/pg_hba.conf /pgdata/pg_hba.conf.orig
    [root@node1 ~]# vi /pgdata/pg_hba.conf
    [root@node1 ~]# diff -uNr /pgdata/pg_hba.conf.orig /pgdata/pg_hba.conf
    --- /pgdata/pg_hba.conf.orig
    +++ /pgdata/pg_hba.conf
    @@ -72,3 +72,5 @@
     host    all         all         127.0.0.1/32          md5
     # IPv6 local connections:
     host    all         all         ::1/128               md5
    +# IPv4 network connections:
    +host    all         all         192.168.1.0/24        md5
    [root@node1 ~]# su - postgres -c "pg_ctl -D /pgdata/ start"
    サーバは起動中です。
    [root@node1 ~]# psql -h node1.priv -U admin template1
    ユーザ admin のパスワード:<パスワード>
    psql (8.4.4)
    "help" でヘルプを表示します.
    
    template1=# \q
    [root@node1 ~]# (crontab -u postgres -l; \
     echo "@reboot /usr/bin/pg_ctl -D /pgdata/ start") | \
     crontab -u postgres -
    [root@node1 ~]# crontab -l -u postgres
    @reboot /usr/bin/pg_ctl -D /pgdata/ start
    [root@node1 ~]#
    

    この状態で、しばらく運用していたという設定で、次へ行きます (もちろん実運用であれば、チューニングやら、何らかのバックアップが必要ですが、そこはパスで)。

    2 台目を用意して、DRBD を設定

    データやユーザも増えてきて、サービスを止められなくなってきたので、HA 化しようと思いたったとします。下記を買ってきたと想定します。

    • 1 台目への追加パーツ
      • DRBD 用の追加 NIC 1 枚 (eth1)
      • DRBD でレプリケーションする、HDD 2 つ
        • WAL 領域用 (sdb 10GB)
        • データ領域用 (sdc 10GB)
    • 2 台目のマシン 1 台
      • NIC は 2 枚搭載 (通常用の eth0, DRBD 用の eth1)
      • HDD
        • システム用 (sda)
        • WAL 領域用 (sdb 20GB)
        • データ領域用 (sdc 20GB)

    Ether カードは、DRBD のデータ転送用を追加します。VRRP はサービス側のネットワークにそのまま流します。専用化やボンディングも考えられます。

    追加 HDD は、各ホストに 2 台ずつ用意しました。意図としましては、PostgreSQL のデータ領域と WAL の書き込み先を、別々に DRBD でレプリケートするためです。WAL 領域はシーケンシャル I/O が主体で、書き込みは同期で行なわれますので、ヘッドの動きを抑えつつ、DRBD のプロトコル C でレプリケートします (まあそれを言うのならば、DRBD のメタ領域は internal ではなく external にすべきですし、ファイルシステムも ext3 ではなく ext2 にすべきなのですが、まあいいや)。対して、とかく I/O バウンドなサービスでボトルネックとなるデータ領域は、ランダムアクセスが主体ですし、書き込みも非同期ですので、パフォーマンスのダウンを抑えるために、DRBD のプロトコル A で非同期レプリケーションを行ないます。フェイルオーバの際に、非同期 DRBD のデータ領域で取りこぼしがあったとしても、そこはデータベースのリカバリで、WAL から復元できるからです。

    まずは、node1 で、下記のようなパーティションを作りました。

    [root@node1 ~]# fdisk -l /dev/sdb
    
    Disk /dev/sdb: 10.7 GB, 10737418240 bytes
    255 heads, 63 sectors/track, 1305 cylinders
    Units = シリンダ数 of 16065 * 512 = 8225280 bytes
    
    デバイス Boot      Start         End      Blocks   Id  System
    /dev/sdb1               1        1305    10482381   83  Linux
    [root@node1 ~]# fdisk -l /dev/sdc
    
    Disk /dev/sdc: 10.7 GB, 10737418240 bytes
    255 heads, 63 sectors/track, 1305 cylinders
    Units = シリンダ数 of 16065 * 512 = 8225280 bytes
    
    デバイス Boot      Start         End      Blocks   Id  System
    /dev/sdc1               1        1305    10482381   83  Linux
    [root@node1 ~]#
    

    DRBD をインストールします。

    [root@node1 ~]# yum install -y kmod-drbd82 drbd82
    (中略)
    Installed:
      drbd82.x86_64 0:8.2.6-1.el5.centos        kmod-drbd82.x86_64 0:8.2.6-2
    
    Complete!
    [root@node1 ~]#
    

    DRBD の設定ファイルを書きます。

    [root@node1 ~]# cat > /etc/drbd.conf
    resource res0 {
      protocol C;
      on node1.priv {
        device /dev/drbd0;
        disk /dev/sdb1;
        address 192.168.2.1:7789;
        meta-disk internal;
      }
      on node2.priv {
        device /dev/drbd0;
        disk /dev/sdb1;
        address 192.168.2.2:7789;
        meta-disk internal;
      }
    }
    resource res1 {
      protocol A;
      on node1.priv {
        device /dev/drbd1;
        disk /dev/sdc1;
        address 192.168.2.1:7790;
        meta-disk internal;
      }
      on node2.priv {
        device /dev/drbd1;
        disk /dev/sdc1;
        address 192.168.2.2:7790;
        meta-disk internal;
      }
    }
    [root@node1 ~]#
    

    node1 で DRBD を起動します。

    [root@node1 ~]# drbdadm create-md res0
    (中略)
    * If you wish to opt out entirely, simply enter 'no'.
    * To continue, just press [RETURN]
    
    success
    [root@node1 ~]# drbdadm create-md res1
    (中略)
    * If you wish to opt out entirely, simply enter 'no'.
    * To continue, just press [RETURN]
    
    success
    [root@node1 ~]# service drbd start
    Starting DRBD resources:    [ d(res0) d(res1) n(res0) n(res1) ].
    ..........
    ***************************************************************
     DRBD's startup script waits for the peer node(s) to appear.
     - In case this node was already a degraded cluster before the
       reboot the timeout is 0 seconds. [degr-wfc-timeout]
     - If the peer was available before the reboot the timeout will
       expire after 0 seconds. [wfc-timeout]
       (These values are for resource 'res0'; 0 sec -> wait forever)
     To abort waiting enter 'yes' [  14]:yes
    
    [root@node1 ~]# drbdadm -- -o primary all
    [root@node1 ~]# mkfs.ext3 /dev/drbd0
    (中略)
    [root@node1 ~]# mkfs.ext3 /dev/drbd1
    (中略)
    [root@node1 ~]#
    

    面倒なので、node1 ←→ node2 へ、root ユーザによるパスワードなしの ssh アクセスが可能であるとさせてください。node1 のディスクと同じレイアウトのパーティションを node2 のディスクに作成します。

    [root@node1 ~]# sfdisk -d /dev/sdb | ssh node2.priv "sfdisk /dev/sdb"
    (中略)
    新たな場面:
    ユニット = 512 バイトのセクタ、0 から数えます
    
       Device Boot    Start       End   #sectors  Id  System
    /dev/sdb1            63  20964824   20964762  83  Linux
    /dev/sdb2             0         -          0   0  空
    /dev/sdb3             0         -          0   0  空
    /dev/sdb4             0         -          0   0  空
    新たなパーティションの書き込みに成功
    
    パーティションテーブルを再読み込み中...
    (中略)
    [root@node1 ~]# sfdisk -d /dev/sdc | ssh node2.priv "sfdisk /dev/sdc"
    (中略)
    新たな場面:
    ユニット = 512 バイトのセクタ、0 から数えます
    
       Device Boot    Start       End   #sectors  Id  System
    /dev/sdc1            63  20964824   20964762  83  Linux
    /dev/sdc2             0         -          0   0  空
    /dev/sdc3             0         -          0   0  空
    /dev/sdc4             0         -          0   0  空
    新たなパーティションの書き込みに成功
    
    パーティションテーブルを再読み込み中...
    (中略)
    [root@node1 ~]#
    

    node2 で DRBD をインストールし、起動します。

    [root@node2 ~]# service iptables stop
    ファイアウォールルールを適用中:                            [  OK  ]
    チェインポリシーを ACCEPT に設定中filter                   [  OK  ]
    iptables モジュールを取り外し中                            [  OK  ]
    [root@node2 ~]# chkconfig iptables off
    [root@node2 ~]# yum install -y kmod-drbd82 drbd82
    (中略)
    Installed:
      drbd82.x86_64 0:8.2.6-1.el5.centos        kmod-drbd82.x86_64 0:8.2.6-2
    
    Complete!
    [root@node2 ~]# scp node1.priv:/etc/drbd.conf /etc/drbd.conf
    drbd.conf                                     100%  540     0.5KB/s   00:00
    [root@node2 ~]# drbdadm create-md res0
    (中略)
    * If you wish to opt out entirely, simply enter 'no'.
    * To continue, just press [RETURN]
    
    success
    [root@node2 ~]# drbdadm create-md res1
    (中略)
    * If you wish to opt out entirely, simply enter 'no'.
    * To continue, just press [RETURN]
    
    success
    [root@node2 ~]# service drbd start
    Starting DRBD resources:    [ d(res0) d(res1) n(res0) n(res1) ].
    ..........
    ***************************************************************
     DRBD's startup script waits for the peer node(s) to appear.
     - In case this node was already a degraded cluster before the
       reboot the timeout is 0 seconds. [degr-wfc-timeout]
     - If the peer was available before the reboot the timeout will
       expire after 0 seconds. [wfc-timeout]
       (These values are for resource 'res0'; 0 sec -> wait forever)
     To abort waiting enter 'yes' [  10]:
    [root@node2 ~]# cat /proc/drbd
    version: 8.2.6 (api:88/proto:86-88)
    GIT-hash: 3e69822d3bb4920a8c1bfdf7d647169eba7d2eb4 build by
     buildsvn@c5-x8664-build, 2008-10-03 11:30:17
     0: cs:SyncTarget st:Secondary/Primary ds:Inconsistent/UpToDate C r---
        ns:0 nr:6752 dw:6752 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 oos:10475272
            [>....................] sync'ed:  0.2% (10229/10236)M
            finish: 7:16:28 speed: 288 (320) K/sec
     1: cs:SyncTarget st:Secondary/Primary ds:Inconsistent/UpToDate A r---
        ns:0 nr:9952 dw:9952 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 oos:10472072
            [>....................] sync'ed:  0.2% (10226/10236)M
            finish: 7:16:20 speed: 256 (320) K/sec
    [root@node2 ~]#
    

    初期同期が終わりそうにないので寝ます。

    データベースの領域を移動

    朝までには終わっていました。

    node1 で、データの場所を移動し DB を起動します。

    [root@node1 ~]# mkdir /mnt/res0 /mnt/res1
    [root@node1 ~]# mount /dev/drbd0 /mnt/res0
    [root@node1 ~]# mount /dev/drbd1 /mnt/res1
    [root@node1 ~]# su - postgres -c "pg_ctl -D /pgdata/ stop"
    サーバ停止処理の完了を待っています....完了
    サーバは停止しました
    [root@node1 ~]# crontab -e -u postgres
    [root@node1 ~]# crontab -l -u postgres
    [root@node1 ~]# mv /pg_xlog/ /mnt/res0/
    [root@node1 ~]# mv /pgdata/ /mnt/res1/
    [root@node1 ~]# ln -sf /mnt/res0/pg_xlog /pg_xlog
    [root@node1 ~]# ln -sf /mnt/res1/pgdata /pgdata
    [root@node1 ~]# su - postgres -c "pg_ctl -D /pgdata/ start"
    サーバは起動中です。
    [root@node1 ~]#
    

    node2 に PostgreSQL を入れ、ディレクトリの準備をします。

    [root@node2 ~]# yum install -y postgresql84 postgresql84-server
    (中略)
    Installed:
      postgresql84.x86_64 0:8.4.5-1.el5_5.1
      postgresql84-server.x86_64 0:8.4.5-1.el5_5.1
    
    Dependency Installed:
      postgresql84-libs.x86_64 0:8.4.5-1.el5_5.1
    
    Complete!
    [root@node2 ~]# mkdir /mnt/res0 /mnt/res1
    [root@node2 ~]# ln -sf /mnt/res0/pg_xlog /pg_xlog
    [root@node2 ~]# /mnt/res1/pgdata /pgdata
    [root@node2 ~]# ln -sf /mnt/res1/pgdata /pgdata
    [root@node2 ~]#
    

    マスター/バックアップになるためのスクリプトを node1, node2 ともに設置して、まずは手作業で、正しく動作することを確認しておきます。

    [root@node1 ~]# cat > ~/pgsql_notify_master
    #!/bin/sh
    service drbd start
    drbdadm primary all
    mount /dev/drbd0 /mnt/res0/
    mount /dev/drbd1 /mnt/res1/
    su - postgres -c "/usr/bin/pg_ctl -D /pgdata/ start"
    [root@node1 ~]# chmod 0755 ~/pgsql_notify_master
    [root@node1 ~]# cat > ~/pgsql_notify_backup
    #!/bin/sh
    su - postgres -c "/usr/bin/pg_ctl -D /pgdata/ stop"
    umount /mnt/res1/
    umount /mnt/res0/
    drbdadm secondary all
    [root@node1 ~]# chmod 0755 ~/pgsql_notify_backup
    

    Keepalived を VRRP モードで設定

    node1, node2 で、CentOS の “testing” レポジトリから、Keepalived をインストールします。

    [root@node1 ~]# cd /etc/yum.repos.d/
    [root@node1 yum.repos.d]# wget \
     http://dev.centos.org/centos/5/CentOS-Testing.repo
    (中略)
    [root@node1 yum.repos.d]# cd
    [root@node1 ~]# yum --enablerepo=c5-testing install -y keepalived
    (中略)
    Installed:
      keepalived.x86_64 0:1.1.15-0.el5.centos
    
    Complete!
    [root@node1 ~]# cp /etc/keepalived/keepalived.conf \
     /etc/keepalived/keepalived.conf.orig
    [root@node1 ~]# cat > /etc/keepalived/keepalived.conf
    vrrp_instance pgsql {
      garp_master_delay 5
      virtual_router_id 200
      advert_int 1
      state BACKUP
      priority 100
      interface eth0
      nopreempt
      authentication {
        auth_type PASS
        auth_pass hogefuga
      }
      virtual_ipaddress {
        192.168.1.33/24 dev eth0
      }
      notify_master "/root/pgsql_notify_master"
      notify_backup "/root/pgsql_notify_backup"
      notify_fault  "/root/pgsql_notify_backup"
    }
    [root@node1 ~]# /root/pgsql_notify_backup
    pg_ctl: PIDファイル"/pgdata/postmaster.pid"がありません
    サーバが動作していますか?
    umount: /mnt/res1/: マウントされていません
    umount: /mnt/res0/: マウントされていません
    [root@node1 ~]# service keepalived start
    keepalived を起動中:                                       [  OK  ]
    [root@node1 ~]# chkconfig keepalived on
    [root@node1 ~]#
    

    node2 も同様です。

    これでとりあえず、HA 構成のできあがりです。

    Keepalived の管理リソースについて

    Keepalived が VRRP モードで管理するのはマスター←→バックアップの状態遷移だけであり、両ノード間で管理してくれる共有リソースは仮想 IP アドレスだけです。

    そこで何が問題かというと、対向が起動していない状態で Keepalived を起動すれば、「マスターに遷移した」ということで Keepalived は “notify_master” を実行するのですが、停止の際 (keepalived を明示的に停止させたり、ランレベル移行やシステム終了を行なった際) には、”notify_backup” を実行しません (なぜなんだろう? “notify_terminate” ハンドラでもあればいいのに)。そうなると、VIP だけ対向へ移るのですが、Keepalived の管理外の共有リソース (今回で言うと、PostgreSQL のサービスとDRBD のプライマリであること) は、Keepalived のフェイルオーバと無関係に残ってしまい、マスターに移行しようとした対向は、DRBD プライマリになれません。当然、シャットダウン時の、各サービスの停止順序もバラバラです。

    しかたがないので、keepalived サービスの stop の際にバックアップノード化をするようにします。

    [root@node1 ~]# cp /etc/init.d/keepalived /etc/init.d/keepalived.orig
    [root@node1 ~]# vi /etc/init.d/keepalived
    [root@node1 ~]# diff -U 10 /etc/init.d/keepalived.orig /etc/init.d/keepalived
    --- /etc/init.d/keepalived.orig 2010-10-11 17:38:22.000000000 +0900
    +++ /etc/init.d/keepalived      2010-10-11 17:39:22.000000000 +0900
    @@ -25,20 +25,21 @@
         echo
         [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$prog
     }
    
     stop() {
         echo -n $"Stopping $prog: "
         killproc keepalived
         RETVAL=$?
         echo
         [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/$prog
    +    test $RETVAL -eq 0 && /root/pgsql_notify_backup
     }
    
     reload() {
         echo -n $"Reloading $prog: "
         killproc keepalived -1
         RETVAL=$?
         echo
     }
    
     # See how we were called.
    [root@node1 ~]#
    

    ネット上では、daemontools を入れて、keepalived のフォアグラウンド起動とバックアップ化スクリプトを組み合わせる構成が多いようですが、DJB は distro 的/FHS 的にはメンドくさいんだよなぁ… (DJB に対して、それ以上の感情はありませんよ)。VRRP が本来はルータ用のプロトコルであることや、Keepalived が元々は、どうやら LVS と組み合わせてロードバランサとして利用するために作られたことを考えると、IP アドレス以外の共有リソースがある用途をあまり考慮していないようにも見うけられます。HeartBeat であれば、DRBD との連動機能もあるし、そもそも共有リソースの考え方があるので、DB やファイルサーバ、ストレージ等、共有リソースとその依存関係がある場合には、HeartBeat (や PaceMaker か、LifeKeeper でも良いですが) の方が良いのかも知らんです…。




    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
    $
    

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