Archive for 10月 2010

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

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

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

実行してみます。

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

scala>

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

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

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

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

scala> res9.toString == res10.toString
res11: Boolean = true

scala>

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

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

実行してみます。

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

scala>

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

いや、面白い。




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

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

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

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

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

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

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

うーん…。

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

間違ってたら直します。

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

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

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

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

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

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

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

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

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

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

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

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

$ javap -c -private Rational
Compiled from "Rational.scala"
public class Rational extends java.lang.Object implements scala.ScalaObject{
private final int den;

private final int num;

private final scala.Tuple2 x$1;

public Rational(int, int);
  Code:
(中略)
$

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

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

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

$ javap -c -private Rational
Compiled from "Rational.scala"
public class Rational extends java.lang.Object implements scala.ScalaObject{
private int denField;

private int numField;

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

(中略)

public int den();
  Code:
   0: aload_0
   1: invokespecial  #58; //Method denField:()I
   4: ireturn

public int num();
  Code:
   0: aload_0
   1: invokespecial  #61; //Method numField:()I
   4: ireturn

private void denField_$eq(int);
  Code:
   0: aload_0
   1: iload_1
   2: putfield       #15; //Field denField:I
   5: return

private int denField();
  Code:
   0: aload_0
   1: getfield       #15; //Field denField:I
   4: ireturn

private void numField_$eq(int);
  Code:
   0: aload_0
   1: iload_1
   2: putfield       #13; //Field numField:I
   5: return

private int numField();
  Code:
   0: aload_0
   1: getfield       #13; //Field numField:I
   4: ireturn

(中略)

}

$

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




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

下記に参加しました:

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

自分用メモです:

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



Fedora の Scala の RPM を CentOS でビルド

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

[root@node1 ~]# yum remove -y java-1.6.0-openjdk
(中略)
Removed:
  java-1.6.0-openjdk.x86_64 1:1.6.0.0-1.13.b16.el5

Dependency Removed:
  openoffice.org-calc.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-core.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-draw.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-graphicfilter.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-impress.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-langpack-ja_JP.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-math.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-ure.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-writer.x86_64 1:3.1.1-19.5.el5_5.1
  openoffice.org-xsltfilter.x86_64 1:3.1.1-19.5.el5_5.1

Complete!
[root@node1 ~]# ./jdk-6u21-linux-x64-rpm.bin
(中略)
Press Enter to continue..... <Enter>

Done.
[root@node1 ~]#

下記が入ります:

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

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

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

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

[root@node1 ~]#

インストールします。

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

Complete!
[root@node1 ~]#

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

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

Complete!
[root@node1 ~]#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

結果を見てみます。

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

8 行だそうです。

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

8 行です。

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

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

後でしっかりと読む:

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



PostgreSQL: DRBD + Keepalived (VRRP)

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

参考:

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

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

構成図

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

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

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

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

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

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

Complete!
[root@node1 ~]#

セットアップします。

[root@node1 ~]# mkdir -m 0700 /pgdata/ /pg_xlog/
[root@node1 ~]# chown postgres.postgres /pgdata/ /pg_xlog/
[root@node1 ~]# su - postgres -c "initdb --pgdata=/pgdata/ --xlogdir=/pg_xlog/ \
 --encoding=UTF-8 --no-locale --username=admin --pwprompt --auth=md5"
データベースシステム内のファイルの所有者は"postgres"ユーザでした。
このユーザがサーバプロセスを所有しなければなりません。

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

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

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

    postgres -D /pgdata
または
    pg_ctl -D /pgdata -l logfile start

[root@node1 ~]# cp /pgdata/postgresql.conf /pgdata/postgresql.conf.orig
[root@node1 ~]# vi /pgdata/postgresql.conf
[root@node1 ~]# diff -uNr /pgdata/postgresql.conf.orig /pgdata/postgresql.conf
--- /pgdata/postgresql.conf.orig
+++ /pgdata/postgresql.conf
@@ -56,7 +56,7 @@

 # - Connection Settings -

-#listen_addresses = 'localhost'        # what IP address(es) to listen on;
+listen_addresses = '*'                 # what IP address(es) to listen on;
                                        # comma-separated list of addresses;
                                        # defaults to 'localhost', '*' = all
                                        # (change requires restart)
[root@node1 ~]# cp /pgdata/pg_hba.conf /pgdata/pg_hba.conf.orig
[root@node1 ~]# vi /pgdata/pg_hba.conf
[root@node1 ~]# diff -uNr /pgdata/pg_hba.conf.orig /pgdata/pg_hba.conf
--- /pgdata/pg_hba.conf.orig
+++ /pgdata/pg_hba.conf
@@ -72,3 +72,5 @@
 host    all         all         127.0.0.1/32          md5
 # IPv6 local connections:
 host    all         all         ::1/128               md5
+# IPv4 network connections:
+host    all         all         192.168.1.0/24        md5
[root@node1 ~]# su - postgres -c "pg_ctl -D /pgdata/ start"
サーバは起動中です。
[root@node1 ~]# psql -h node1.priv -U admin template1
ユーザ admin のパスワード:<パスワード>
psql (8.4.4)
"help" でヘルプを表示します.

template1=# \q
[root@node1 ~]# (crontab -u postgres -l; \
 echo "@reboot /usr/bin/pg_ctl -D /pgdata/ start") | \
 crontab -u postgres -
[root@node1 ~]# crontab -l -u postgres
@reboot /usr/bin/pg_ctl -D /pgdata/ start
[root@node1 ~]#

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

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

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

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

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

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

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

[root@node1 ~]# fdisk -l /dev/sdb

Disk /dev/sdb: 10.7 GB, 10737418240 bytes
255 heads, 63 sectors/track, 1305 cylinders
Units = シリンダ数 of 16065 * 512 = 8225280 bytes

デバイス Boot      Start         End      Blocks   Id  System
/dev/sdb1               1        1305    10482381   83  Linux
[root@node1 ~]# fdisk -l /dev/sdc

Disk /dev/sdc: 10.7 GB, 10737418240 bytes
255 heads, 63 sectors/track, 1305 cylinders
Units = シリンダ数 of 16065 * 512 = 8225280 bytes

デバイス Boot      Start         End      Blocks   Id  System
/dev/sdc1               1        1305    10482381   83  Linux
[root@node1 ~]#

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

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

Complete!
[root@node1 ~]#

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

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

node1 で DRBD を起動します。

[root@node1 ~]# drbdadm create-md res0
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]

success
[root@node1 ~]# drbdadm create-md res1
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]

success
[root@node1 ~]# service drbd start
Starting DRBD resources:    [ d(res0) d(res1) n(res0) n(res1) ].
..........
***************************************************************
 DRBD's startup script waits for the peer node(s) to appear.
 - In case this node was already a degraded cluster before the
   reboot the timeout is 0 seconds. [degr-wfc-timeout]
 - If the peer was available before the reboot the timeout will
   expire after 0 seconds. [wfc-timeout]
   (These values are for resource 'res0'; 0 sec -> wait forever)
 To abort waiting enter 'yes' [  14]:yes

[root@node1 ~]# drbdadm -- -o primary all
[root@node1 ~]# mkfs.ext3 /dev/drbd0
(中略)
[root@node1 ~]# mkfs.ext3 /dev/drbd1
(中略)
[root@node1 ~]#

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

[root@node1 ~]# sfdisk -d /dev/sdb | ssh node2.priv "sfdisk /dev/sdb"
(中略)
新たな場面:
ユニット = 512 バイトのセクタ、0 から数えます

   Device Boot    Start       End   #sectors  Id  System
/dev/sdb1            63  20964824   20964762  83  Linux
/dev/sdb2             0         -          0   0  空
/dev/sdb3             0         -          0   0  空
/dev/sdb4             0         -          0   0  空
新たなパーティションの書き込みに成功

パーティションテーブルを再読み込み中...
(中略)
[root@node1 ~]# sfdisk -d /dev/sdc | ssh node2.priv "sfdisk /dev/sdc"
(中略)
新たな場面:
ユニット = 512 バイトのセクタ、0 から数えます

   Device Boot    Start       End   #sectors  Id  System
/dev/sdc1            63  20964824   20964762  83  Linux
/dev/sdc2             0         -          0   0  空
/dev/sdc3             0         -          0   0  空
/dev/sdc4             0         -          0   0  空
新たなパーティションの書き込みに成功

パーティションテーブルを再読み込み中...
(中略)
[root@node1 ~]#

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

[root@node2 ~]# service iptables stop
ファイアウォールルールを適用中:                            [  OK  ]
チェインポリシーを ACCEPT に設定中filter                   [  OK  ]
iptables モジュールを取り外し中                            [  OK  ]
[root@node2 ~]# chkconfig iptables off
[root@node2 ~]# yum install -y kmod-drbd82 drbd82
(中略)
Installed:
  drbd82.x86_64 0:8.2.6-1.el5.centos        kmod-drbd82.x86_64 0:8.2.6-2

Complete!
[root@node2 ~]# scp node1.priv:/etc/drbd.conf /etc/drbd.conf
drbd.conf                                     100%  540     0.5KB/s   00:00
[root@node2 ~]# drbdadm create-md res0
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]

success
[root@node2 ~]# drbdadm create-md res1
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]

success
[root@node2 ~]# service drbd start
Starting DRBD resources:    [ d(res0) d(res1) n(res0) n(res1) ].
..........
***************************************************************
 DRBD's startup script waits for the peer node(s) to appear.
 - In case this node was already a degraded cluster before the
   reboot the timeout is 0 seconds. [degr-wfc-timeout]
 - If the peer was available before the reboot the timeout will
   expire after 0 seconds. [wfc-timeout]
   (These values are for resource 'res0'; 0 sec -> wait forever)
 To abort waiting enter 'yes' [  10]:
[root@node2 ~]# cat /proc/drbd
version: 8.2.6 (api:88/proto:86-88)
GIT-hash: 3e69822d3bb4920a8c1bfdf7d647169eba7d2eb4 build by
 buildsvn@c5-x8664-build, 2008-10-03 11:30:17
 0: cs:SyncTarget st:Secondary/Primary ds:Inconsistent/UpToDate C r---
    ns:0 nr:6752 dw:6752 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 oos:10475272
        [>....................] sync'ed:  0.2% (10229/10236)M
        finish: 7:16:28 speed: 288 (320) K/sec
 1: cs:SyncTarget st:Secondary/Primary ds:Inconsistent/UpToDate A r---
    ns:0 nr:9952 dw:9952 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 oos:10472072
        [>....................] sync'ed:  0.2% (10226/10236)M
        finish: 7:16:20 speed: 256 (320) K/sec
[root@node2 ~]#

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

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

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

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

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

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

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

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

Complete!
[root@node2 ~]# mkdir /mnt/res0 /mnt/res1
[root@node2 ~]# ln -sf /mnt/res0/pg_xlog /pg_xlog
[root@node2 ~]# /mnt/res1/pgdata /pgdata
[root@node2 ~]# ln -sf /mnt/res1/pgdata /pgdata
[root@node2 ~]#

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

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

Keepalived を VRRP モードで設定

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

[root@node1 ~]# cd /etc/yum.repos.d/
[root@node1 yum.repos.d]# wget \
 http://dev.centos.org/centos/5/CentOS-Testing.repo
(中略)
[root@node1 yum.repos.d]# cd
[root@node1 ~]# yum --enablerepo=c5-testing install -y keepalived
(中略)
Installed:
  keepalived.x86_64 0:1.1.15-0.el5.centos

Complete!
[root@node1 ~]# cp /etc/keepalived/keepalived.conf \
 /etc/keepalived/keepalived.conf.orig
[root@node1 ~]# cat > /etc/keepalived/keepalived.conf
vrrp_instance pgsql {
  garp_master_delay 5
  virtual_router_id 200
  advert_int 1
  state BACKUP
  priority 100
  interface eth0
  nopreempt
  authentication {
    auth_type PASS
    auth_pass hogefuga
  }
  virtual_ipaddress {
    192.168.1.33/24 dev eth0
  }
  notify_master "/root/pgsql_notify_master"
  notify_backup "/root/pgsql_notify_backup"
  notify_fault  "/root/pgsql_notify_backup"
}
[root@node1 ~]# /root/pgsql_notify_backup
pg_ctl: PIDファイル"/pgdata/postmaster.pid"がありません
サーバが動作していますか?
umount: /mnt/res1/: マウントされていません
umount: /mnt/res0/: マウントされていません
[root@node1 ~]# service keepalived start
keepalived を起動中:                                       [  OK  ]
[root@node1 ~]# chkconfig keepalived on
[root@node1 ~]#

node2 も同様です。

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

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

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

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

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

[root@node1 ~]# cp /etc/init.d/keepalived /etc/init.d/keepalived.orig
[root@node1 ~]# vi /etc/init.d/keepalived
[root@node1 ~]# diff -U 10 /etc/init.d/keepalived.orig /etc/init.d/keepalived
--- /etc/init.d/keepalived.orig 2010-10-11 17:38:22.000000000 +0900
+++ /etc/init.d/keepalived      2010-10-11 17:39:22.000000000 +0900
@@ -25,20 +25,21 @@
     echo
     [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$prog
 }

 stop() {
     echo -n $"Stopping $prog: "
     killproc keepalived
     RETVAL=$?
     echo
     [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/$prog
+    test $RETVAL -eq 0 && /root/pgsql_notify_backup
 }

 reload() {
     echo -n $"Reloading $prog: "
     killproc keepalived -1
     RETVAL=$?
     echo
 }

 # See how we were called.
[root@node1 ~]#

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