肥大化するデータベース

Nova は RPC(メッセージキュー)の他にデータベースを必要とします。 現在は SQLAlchemy ライブラリで利用可能なリレーショナルデータベース(RDBMS)のみ対応していますが、将来的に NoSQL 等が利用できるように、データベースドライバはモジュール構造となっています。

nova-2012.1
  nova/
    db/
      __init__.py
      api.py              DB API(インターフェース)
      base.py
      migration.py        DB のバージョン管理
      sqlalchemy/         SQLalchemy(SQL O/Rマッパ)用ドライバ
        __init__.py
          api.py          DB API実体(SQLAlchemy 用)
          migration.py    DB バージョン管理
          models.py       DB モデル定義
          session.py
          migrate_repo/   DB バージョン関連
            README
            __init__.py
            manage.py
            manage.cfg
            versions/     DB マイグレーション(バージョンアップ/ダウン)用
              __init__.py
              001_austin.py
              002_bexar.py
              ...

2つある api.py が気になりますね。nova-2012/nova/db/api.py にはメソッド定義だけで、実際の DB 操作は nova-2012/nova/db/sqlalchemy/api.py 内にあります。よって、DB 操作 API を追加する際には、2つの api.py 両方に記述が必要です。

テーブルスキーマ

SQLAlchemy ドライバの場合、DB のスキーマ定義が必要になります。これは nova-2012/nova/db/sqlalchemy/models.py に記述されています。例えば、キーペア用のテーブルスキーマは以下の通りです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class KeyPair(BASE, NovaBase):
    """Represents a public key pair for ssh."""
    __tablename__ = 'key_pairs'
    id = Column(Integer, primary_key=True)

    name = Column(String(255))

    user_id = Column(String(255))

    fingerprint = Column(String(255))
    public_key = Column(Text)

注意したいのは、このスキーマモデルクラスが他のスキーマモデルクラスと同様 NovaBase クラスを継承している事です。:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class NovaBase(object):
    """Base class for Nova Models."""
    __table_args__ = {'mysql_engine': 'InnoDB'}
    __table_initialized__ = False
    created_at = Column(DateTime, default=utils.utcnow)
    updated_at = Column(DateTime, onupdate=utils.utcnow)
    deleted_at = Column(DateTime)
    deleted = Column(Boolean, default=False)
    metadata = None

    def save(self, session=None):
        """Save this object."""
        if not session:
            session = get_session()
        session.add(self)
        try:
            session.flush()
        except IntegrityError, e:
            if str(e).endswith('is not unique'):
                raise exception.Duplicate(str(e))
            else:
                raise

    def delete(self, session=None):
        """Delete this object."""
        self.deleted = True
        self.deleted_at = utils.utcnow()
        self.save(session=session)

    def __setitem__(self, key, value):
        setattr(self, key, value)

    def __getitem__(self, key):
        return getattr(self, key)

    def get(self, key, default=None):
        return getattr(self, key, default)

    def __iter__(self):
        self._i = iter(object_mapper(self).columns)
        return self

    def next(self):
        n = self._i.next().name
        return n, getattr(self, n)

    def update(self, values):
        """Make the model object behave like a dict"""
        for k, v in values.iteritems():
            setattr(self, k, v)

    def iteritems(self):
        """Make the model object behave like a dict.

        Includes attributes from joins."""
        local = dict(self)
        joined = dict([(k, v) for k, v in self.__dict__.iteritems()
                      if not k[0] == '_'])
        local.update(joined)
        return local.iteritems()

NovaBase クラスでは、3つの時間属性と1つの真偽値がある事が解ります。

カラム名 用途
created_at レコード作成日時
updated_at レコード更新日時
deleted_at レコード削除日時
deleted 削除フラグ

NovaBase クラスは、SQLalchemy の Base クラス API を拡張するための差分 API を定義しています。主な拡張内容は以下の3つです。

メソッド 動作
save() DB にレコードとして書き戻す際、”is not unique” で終わるエラー メッセージが出た場合は exception.Duplicate 例外を発生させる
delete() DB 上のレコードを削除せず、当該レコードの削除フラグ(deleted) を True、削除時刻(deleted_at)に現在時刻をセットする。
__setitem__(), __getitem__(), get(), __iter__(), next(), update(), iteritems() 辞書型(foo[‘bar’])としての値操作を属性(foo.bar)操作に変換 する。

上記の仕様は Nova で使用される全スキーマに適用されます。 よって、現在の Nova ではモデルインスタンスを削除しても DB 上の当該レコードが削除されず、結果として DB が肥大化し続ける仕様となっており、運用面では削除済みのレコードの掃除が課題となってきます。

なお、Nova の DB モデルでは辞書型としてカラム値を扱うのが基本になっています。そのため、上記表の通り NovaBase クラスには SQLAlchemy の属性値を辞書型の値として扱うためのコードが存在します。 この理由については https://lists.launchpad.net/openstack/msg06128.html のスレッドが参考になるでしょう。

DB の使い方

DB API は各 manager のベースクラス(nova.db.base)中で self.db にセットされます。よって、通常は self.db.<APIメソッド> の形で使われます。

DB レコードの作成は通常、

self.db.<モデル名>_create(<コンテキスト>, <辞書型のパラメータ>)

で行います。ボリュームレコード作成の例を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
options = {
    'size': size,
    'user_id': context.user_id,
    'project_id': context.project_id,
    'snapshot_id': snapshot_id,
    'availability_zone': availability_zone,
    'status': "creating",
    'attach_status': "detached",
    'display_name': name,
    'display_description': description,
    'volume_type_id': volume_type_id,
    'metadata': metadata,
    }

volume = self.db.volume_create(context, options)

DB レコードの取得は通常、

self.db.<モデル名>_get(<コンテキスト>, <ID>)

で行います。ボリュームレコード取得の例は以下の通りです。

1
2
3
4
5
def get(self, context, volume_id):
    rv = self.db.volume_get(context, volume_id)
    volume = dict(rv.iteritems())
    check_policy(context, 'get', volume)
    return volume

DB レコードの更新は通常、

self.db.<モデル名>_update(<コンテキスト>, <ID>)

で行います。ボリュームレコード更新の例は以下の通りです。

1
2
3
@wrap_check_policy
def update(self, context, volume, fields):
    self.db.volume_update(context, volume['id'], fields)

DB レコードの削除は通常、

self.db.<モデル名>_destroy(<コンテキスト>, <ID>)

で行います。ボリュームレコード削除の例は以下の通りです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def delete_snapshot(self, context, snapshot_id):
    """Deletes and unexports snapshot."""
    context = context.elevated()
    snapshot_ref = self.db.snapshot_get(context, snapshot_id)

    try:
        LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name'])
        self.driver.delete_snapshot(snapshot_ref)
    except exception.SnapshotIsBusy:
        LOG.debug(_("snapshot %s: snapshot is busy"), snapshot_ref['name'])
        self.db.snapshot_update(context,
                                snapshot_ref['id'],
                                {'status': 'available'})
        return True
    except Exception:
        with utils.save_and_reraise_exception():
            self.db.snapshot_update(context,
                                    snapshot_ref['id'],
                                    {'status': 'error_deleting'})

    self.db.snapshot_destroy(context, snapshot_id)
    LOG.debug(_("snapshot %s: deleted successfully"), snapshot_ref['name'])
    return True

Table Of Contents

Previous topic

RPC:投げっぱなしと返事待ち

This Page