OpenStack Nova のスケジューラを作ってみた (NumProjectsFilter)

今年の9月の話なので、もうだいぶ前の話になるのだが、そういえば OpenStack Nova のスケジューラを作ってみたので、記録として残しておきたい。

(この記事はOpenStack Advent Calendar 2017参加エントリ...ではありません(笑))

# 発端

発端は某所でこんな議論をしていたことだった。
OpenStackでつくった基盤があって、その上にいろいろサービスを収容するのだが、運用を真面目に考えていると、当然何かの故障時に影響をどれだけ抑え込めるかも検討することになる。

まず、「サービス」の運営主体というものを考えてみると、管理会計上も組織編制上も独立になっていることが多い。この主体を、OpenStack の Project にマッピングするのはとても自然な考え方だと思う。

ここで、「故障」の話に戻る。
この時の議論では、compute が 1 台故障で落ちた時に「影響を受けるサービスの数」に制約をつけられないか？というのが発端だった。この記事を読むような人はよく承知の通り、OpenStack Nova では以下の公式ドキュメントにあるように、様々な FilterScheduler が提供されている。これらをくみあわせればなんとかならないだろうかと、考えた

https://docs.openstack.org/nova/pike/user/filter-scheduler.html

...のだが、NumInstancesFilter のように、ある compute (あるいは host aggregate)の上で作成可能なVM数に制約をつけるFilterはあっても、「ある compute の上でVMを作成可能な Project (にマッピングされたサービス)の数に制約をつける」ことはできないのだった。(実はうまい組み合わせがあって、見落としているだけなのかもしれないが、当時同僚と考えた検討した結果は「無理じゃね？」だった)

そういうわけで、「ないなら作ってみればいいじゃん」と思った結果がここに置いてある。

https://github.com/openstack/nova/compare/master...thatsdone:feature/num-prj-filter

以下、仕様、設計と実装について説明する。

# 仕様

以下のパラメータを追加した。

## max_projects_per_host

意味は「computeノードあたりのVM作成可能プロジェクト数の上限」である。

1. nova.conf に指定して、システム全体で一意に指定する方法と、
2. host_aggregate の metadata に指定して aggregate 単位で指定する方法

を用意した。デフォルトは50(projects)とし、普通の運用では不用意にこのFilterが有効になってしまっていても無視できるくらい大きくした。

# 設計と実装

設計自体は、実は公式ドキュメントに "Writing Your Own Filter" というセクションがあるくらいで、 

https://docs.openstack.org/nova/pike/user/filter-scheduler.html#writing-your-own-filter

インターフェースははっきりしており、特に難しいことはない。
敢えて言えば、中から使う機能のI/Fだろうか。特に、DBから情報をひっぱってくる場合にどのDB APIを使うべきなのか、類似コードを探しながら検討を行った。

実装だが、仕様が NumInstancesFilter ととても近いため、num_instances_filter.pyをベースにした。しかも、(練習用という意味あいもあったので...という言い訳)比較的 Naive な作りである。(笑)
規模としても、既存ファイルの修正が1つ（パラメータ処理部分）と、新規ファイルが2つ(このFilterの実装本体とテストコード)だけである。

以下、本体部分を見てみよう。

FilterSchedulerの場合、以下にある ```host_passes()``` というメソッドが、今、選択対象候補になっているホストごとに呼び出され、各Filterのcriteriaに基づいて True (VM配置可能)またはFalse(VM配置不可能)を返すようになっている。

```python
    def host_passes(self, host_state, spec_obj):
        max_projects = self._get_max_projects_per_host(host_state, spec_obj)

        LOG.debug("DEBUG: NumProjects filter called. "
                  "spec_obj='%s' host_state='%s'", spec_obj, host_state)
        LOG.debug("DEBUG: host_state.host='%s' project_id='%s'",
                  host_state.host, spec_obj.project_id)

        admin = context.get_admin_context()
        filters = {"host": [host_state.host], "deleted": False}
        instances = objects.InstanceList().get_by_filters(admin, filters)
```

まず、ここで指定されたホスト上で実行中のVMの一覧を取り出している。
実は、必要なのは project_id の一覧の集合だけなのだが、あまり低いレベルのDB APIを呼ぶのもよろしくないので、ここではオーバーヘッドは承知で上記のI/Fを使っている。

```python

        LOG.debug("DEBUG: instances='%s'", instances)

        projects = set()
        for instance in instances:
            LOG.debug("DEBUG: uuid='%s', project_id='%s'",
                      instance['uuid'], instance['project_id'])
            projects.add(instance['project_id'])

```
次に、さきほど得られたVM一覧から project_id を取り出し、set 型にの変数に格納している。

```python

        if spec_obj.project_id in projects:
            # FIXME(thatsdone):
            # This 'True' allows to run multiple VMs on a sigle host.
            passes = True
        elif len(projects) >= max_projects:
            passes = False
        else:
            passes = True
```
最後に、
1. 要求元のユーザの project_id が、今つくった set 型の変数の中にあれば通過
2. なければ、VM実行中のproject数が閾値を超えているかチェック
という順番で判定を行っている。

```python

        if not passes:
            LOG.debug("%(host_state)s fails num_projects check: "
                      "Max projects per host is set to %(max_projects)s",
                      {'host_state': host_state,
                       'max_projects': max_projects})
        return passes
```

これだけである。

新機能追加も、この程度であればたいして難しくないというのがわかると思う。

## 課題

この実装で、一応やりたいことは実現できる。ただし、いくつか問題も抱えているので記しておく。

1. DBアクセスの負荷への影響
    * ホストごとに、Instance 型でVM一覧を取り出していること
2. あるホストで、1つのプロジェクトのVMが2つ以上実行されうる
    * あるサービスへの影響も最小化したいというのは自然な要求だからである。ただしこれは、他のFilterで解決すべき問題かもしれない。

# これをどうするのか？

もろもろの背景事情もあり、今のところは upstream に提案するつもりはない。
...が、英語で記事くらい書いてみてもいいかもしれない。

# まとめ

* OpenStack Nova の FilterScheduler に新しいFilterを追加する例を紹介した。
* 仕様を決めるにあたって、どういう motivation だったのかを説明した。
* まあ私もなんとなく生きていますよ...ということで(笑)
* Happy Hacking!!