Archive for the ‘Linux’ Category.

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




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 で動作していることを確認し、適当にインストールをしてみます。良いようです。




ソース斜め読み: 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




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 では行けないケースもありますので。




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 を実行します:

メニュー

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

ブレークポイントの選択

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

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

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

ステップ実行中




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

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

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

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

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

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

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

import java.util.Properties;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return true;
}

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




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

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

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

import java.util.regex.*;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




CentOS: SRPM, debuginfo 入手

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

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

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

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

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

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

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

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

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

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

0003622: debuginfo packages out of date – CentOS Bug Tracker

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




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

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

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

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

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

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

最長一致するもの

grep:

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

sed:

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

gawk:

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

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

最長一致しないもの

Perl:

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

Python:

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

Ruby:

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

PHP:

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

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

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

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

まとめると

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

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

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

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

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

では。




一般ユーザで自前 MySQL

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




EPEL の「きれいな」入れ方

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

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

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

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

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

では。