n0stack documentation

The n0stack is a simple cloud provider using gRPC.

Description

The n0stack is…

  • a cloud provider.
    • You can use some features: booting VMs, managing networks and so on (see also /n0proto.)
  • simple.
    • There are shortcode and fewer options.
  • using gRPC.
    • A unified interface increase reusability.
  • able to be used as library and framework.
    • You can concentrate to develop your logic by sharing libraries and frameworks for middleware, test, and deployment.

Motivation

Cloud providers have various forms depending on users. This problem has been solved with many options and add-ons (e.g. OpenStack configuration file is very long.) However, it is difficult to adapt to the application by options, then it is necessary to read or rewrite long abstracted codes. Therefore, I thought that it would be better to code on your hands from beginning.

There are some problems to develop cloud providers from scratch: no library, software quality, man-hour, and deployment. The n0stack wants to solve such problems.

Quick Start

n0cli

The n0cli is a CLI tool to call n0stack gRPC APIs.

Installation

with docker

docker pull n0stack/n0stack
docker run -it --rm -v /usr/local/bin:/dst n0stack/n0stack cp /usr/local/bin/n0cli /dst/

Usage

  • See also command help.
$ n0cli --api-endpoint=$api_ip:20180 get node
{
  "nodes": [
    {
      "name": "vm-host1",
      "annotations": {
        "github.com/n0stack/n0stack/n0core/agent_version": "52"
      },
      "address": "192.168.122.10",
      "serial": "Specified",
      "cpu_milli_cores": 1000,
      "memory_bytes": "1033236480",
      "storage_bytes": "107374182400",
      "unit": 1,
      "state": "Ready",
      "reserved_computes": {
        "debug_ipv6": {
          "annotations": {
            "n0core/provisioning/virtual_machine/virtual_machine/reserved_by": "debug_ipv6"
          },
          "request_cpu_milli_core": 10,
          "limit_cpu_milli_core": 1000,
          "request_memory_bytes": "536870912",
          "limit_memory_bytes": "536870912"
        }
      },
      "reserved_storages": {
        "debug-ipv6-network": {
          "annotations": {
            "n0core/provisioning/block_storage/reserved_by": "debug-ipv6-network"
          },
          "request_bytes": "1073741824",
          "limit_bytes": "10737418240"
        },
        "debug_ipv6_network": {
          "annotations": {
            "n0core/provisioning/block_storage/reserved_by": "debug_ipv6_network"
          },
          "request_bytes": "1073741824",
          "limit_bytes": "10737418240"
        },
        "ubuntu-1804": {
          "annotations": {
            "n0core/provisioning/block_storage/reserved_by": "ubuntu-1804"
          },
          "request_bytes": "1073741824",
          "limit_bytes": "10737418240"
        }
      }
    }
  ]
}

Examples

See also Usecases.

Overview about n0proto

The n0proto is gRPC definitions for all of n0stack API.

Resources

Budget

Budget define data structure about resource budget: CPU, Memory, IP address, MAC address, storage, and so on.

Budget はリソースを表すデータ構造である。CPUやメモリなどが含まれる。

Pool

Pool ensure Budgets.

Pool は Budget を払い出す。

Node
  • 物理的なサーバ
  • CPU、メモリ、ストレージを払い出す
Network
  • 仮想的なネットワーク
  • IPアドレスやMACアドレスを払い出す

Provisioning

Provisioning create virtual resources on ensured budget.

Poolから予約されたリソースで仮想的なリソースを作り出す。

BlockStorage
  • NodeのStorageから仮想的なブロックストレージを作り出す
  • 中身はQcow2ファイル
  1. NodeのReserveStorage / ScheduleStorageでストレージを確保
  2. Nodeにインストールされたエージェントを操作するなどして、実態を作成
VirtualMachine
  • NodeのCompute(CPUとメモリ)からVMを作り出す
  • この時BlockStorageと、Networkに接続するNetworkInterface(MACアドレスとIPアドレス)を接続することができる
  1. NodeのReserveCompute / ScheduleComputeでCPUとメモリを確保
  2. 接続するBlockStorageをSetInuseBlockStorageで確保
  3. 接続するNetworkに対してReserveNetworkInterfaceでMACアドレスとIPアドレスを確保
  4. Nodeにインストールされたエージェントを操作するなどして、実態を作成

Deployment

Deployment abstract Provisioning operations.

DeploymentはProvisioningを抽象的にすることでわかりやすくするものである。

Image
  • BlockStorageを抽象化する
  • Imageに登録されたBlockStorageからBlockStorageを生成することができる
  • 一般にはImageでBlockStorageを生成し、生成したBlockStorageからVMを起動することでOpenstackのGlanceのような使い方ができる (detail)

Naming Conventions about API

Standard fields

name
  • Unique key
annotations
  • Field that stores implementation-dependent values
[Resource type]_name
  • Reference to resources
*s
  • List field

Standard methods

List
Get
Create / Apply
Update
Delete

Usecases

List

Boot VirtualMachine from Image

In case of booting VirtualMachine test with Image cloudimage-ubuntu tagged 18.04 on Network test-network.

(If you don’t have registered Image cloudimage-ubuntu tagged 18.04, refer here around FetchISO, ApplyImage and RegisterBlockStorage tasks.)

Example
GenerateBlockStorage:
  type: Image
  action: GenerateBlockStorage
  args:
    image_name: cloudimage-ubuntu
    tag: "18.04"
    block_storage_name: test-blockstorage
    annotations:
      n0core/provisioning/block_storage/request_node_name: vm-host1
    request_bytes: 1073741824
    limit_bytes: 10737418240

ApplyNetwork:
  type: Network
  action: ApplyNetwork
  args:
    name: test-network
    ipv4_cidr: 192.168.0.0/24
    annotations:
      n0core/provisioning/virtual_machine/vlan_id: "100"

CreateVirtualMachine:
  type: VirtualMachine
  action: CreateVirtualMachine
  args:
    name: test-vm
    annotations:
      n0core/provisioning/virtual_machine/request_node_name: vm-host1
    request_cpu_milli_core: 10
    limit_cpu_milli_core: 1000
    request_memory_bytes: 536870912
    limit_memory_bytes: 536870912
    block_storage_names:
      - test-blockstorage
    nics:
      - network_name: test-network
        ipv4_address: 192.168.0.1
    uuid: 056d2ccd-0c4c-44dc-a2c8-39a9d394b51f
    # cloud-config related options:
    login_username: n0user
    ssh_authorized_keys:
      - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey
  depends_on:
    - GenerateBlockStorage
    - ApplyNetwork
n0cli --api-endpoint=$api_ip:20180 do $path_of_previous_yaml

Then, you can login virtual machine via ssh by n0user user using key below:

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIBAQh+adEg/rjqj9qLE0jI4EqV8kZFDzWTASAwvx6HWdoAoGCCqGSM49
AwEHoUQDQgAEhOjA+fY6XV4K9c3ldX4tvqN9fOANtfIR21rJp0NQm0Wtw3abaaML
UHbRUECglxm1JiSaOuWVLTpDbpN7mxNi8Q==
-----END EC PRIVATE KEY-----

(Ubuntu 18.04 Cloud Image doesn’t allow password login to ssh configured above, so you need set password if need to access via VNC console)

Overview
  • Image から BlockStorage を生成する
    • Image は Docker Image のように名前とタグによって管理されているため、タグを指定する必要がある
      • タグは n0cli get image cloudimage-ubuntu-1804tags で確認することができる
    • block_storage_name で生成する BlockStorage の名前を指定する
      • VirtualMachine 生成時にVMとブロックストレージを接続するために用いる
    • まだスケジューリングに対応していないため、annotationsn0core/provisioning/block_storage/request_node_name で BlockStorage をどこのノードに配置するかを決める
      • ノードの名前は n0cli get node で確認できる
    • 生成する BlockStorage の容量は 10 GB (10737418240 Bytes)
      • ゲストOSからはブロックストレージがこのサイズに見える
    • 生成する BlockStorage の実際に使う可能性のある容量は 1 GB (1073741824 Bytes)
      • この値はスケジューリングなどに用いられる
  • Network を作成 / 更新する
  • VirtualMachineを作成する
    • request_cpu_milli_core で実際に使うであろうCPUコアを選択し、limit_cpu_milli_coreで上限を指定する
      • limit_cpu_milli_core はCPUコア数を指定するため、 limit_cpu_milli_core % 1000 == 0 である必要がある
      • この場合1コアのVMがたつ
    • request_memory_bytes == limit_memory_bytes である必要がある
      • この場合メモリ 512 MB (536870912 Bytes)のVMがたつ
      • KVMのmemory ballooningは性能劣化が激しかったので、無効化しているため
    • まだスケジューリングに対応していないため、annotationsn0core/provisioning/virtual_machine/request_node_name で BlockStorage をどこのノードに配置するかを決める
    • block_storage_names で接続する BlockStorageを指定する
      • この場合、Image から作成した BlockStorage を接続している
    • nics でどの Network に接続するか指定する
      • この場合、作成した Network に 192.168.0.1 で接続することを宣言している
    • uuiduuidgen などで適宜生成すること
    • 使っているゲストOSイメージが cloud-init に対応していた場合、nicsで指定したIP、login_usernameで指定したユーザ、ssh_authorized_keysで指定したSSH公開鍵が設定される
Inverse action
Delete_test-vm:
  type: VirtualMachine
  action: DeleteVirtualMachine
  args:
    name: test-vm

Delete_test-blockstorage:
  type: BlockStorage
  action: DeleteBlockStorage
  args:
    name: test-blockstorage
  depends_on:
    - Delete_test-vm

Delete_test-network:
  type: Network
  action: DeleteNetwork
  args:
    name: test-network
  depends_on:
    - Delete_test-vm
Tips: Idempotent action

Caution: This DAG deletes block storage and VM which you created, often causes misoperation unintentionally.

Delete_test-vm:
  type: VirtualMachine
  action: DeleteVirtualMachine
  args:
    name: test-vm
  ignore_error: true

Delete_test-blockstorage:
  type: BlockStorage
  action: DeleteBlockStorage
  args:
    name: test-blockstorage
  depends_on:
    - Delete_test-vm
  ignore_error: true

Delete_test-network:
  type: Network
  action: DeleteNetwork
  args:
    name: test-network
  depends_on:
    - Delete_test-vm
  ignore_error: true

GenerateBlockStorage:
  type: Image
  action: GenerateBlockStorage
  args:
    image_name: cloudimage-ubuntu
    tag: "18.04"
    block_storage_name: test-blockstorage
    annotations:
      n0core/provisioning/block_storage/request_node_name: vm-host1
    request_bytes: 1073741824
    limit_bytes: 10737418240
  depends_on:
    - Delete_test-blockstorage

ApplyNetwork:
  type: Network
  action: ApplyNetwork
  args:
    name: test-network
    ipv4_cidr: 192.168.0.0/24
    annotations:
      n0core/provisioning/virtual_machine/vlan_id: "100"
  depends_on:
    - Delete_test-network

CreateVirtualMachine:
  type: VirtualMachine
  action: CreateVirtualMachine
  args:
    name: test-vm
    annotations:
      n0core/provisioning/virtual_machine/request_node_name: vm-host1
    request_cpu_milli_core: 10
    limit_cpu_milli_core: 1000
    request_memory_bytes: 536870912
    limit_memory_bytes: 536870912
    block_storage_names:
      - test-blockstorage
    nics:
      - network_name: test-network
        ipv4_address: 192.168.0.1
    uuid: 056d2ccd-0c4c-44dc-a2c8-39a9d394b51f
    login_username: n0user
    ssh_authorized_keys:
      - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey
  depends_on:
    - GenerateBlockStorage
    - ApplyNetwork

Boot VirtualMachine from ISO

Fetch and register Ubuntu 18.04 Cloud Images
FetchISO:
  type: BlockStorage
  action: FetchBlockStorage
  args:
    name: cloudimage-ubuntu-1804
    annotations:
      n0core/provisioning/block_storage/request_node_name: vm-host1
    request_bytes: 1073741824 # 1GiB
    limit_bytes: 10737418240 # 10GiB
    source_url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img

ApplyNetwork:
  type: Network
  action: ApplyNetwork
  args:
    name: test-network
    ipv4_cidr: 192.168.0.0/24
    annotations:
      n0core/provisioning/virtual_machine/vlan_id: "100"

CreateVirtualMachine:
  type: VirtualMachine
  action: CreateVirtualMachine
  args:
    name: test-vm
    annotations:
      n0core/provisioning/virtual_machine/request_node_name: vm-host1
    request_cpu_milli_core: 10
    limit_cpu_milli_core: 1000
    request_memory_bytes: 1073741824 # 1GiB
    limit_memory_bytes: 1073741824 # 1GiB
    block_storage_names:
      - cloudimage-ubuntu-1804
    nics:
      - network_name: test-network
        ipv4_address: 192.168.0.1
    # cloud-config related options:
    login_username: n0user
    ssh_authorized_keys:
      - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBITowPn2Ol1eCvXN5XV+Lb6jfXzgDbXyEdtayadDUJtFrcN2m2mjC1B20VBAoJcZtSYkmjrllS06Q26Te5sTYvE= testkey
  depends_on:
    - CreateBlockStorage

# You need to set password for user to login via console (not set if default)
OpenConsole:
  type: VirtualMachine
  action: OpenConsole
  args:
    name: test-vm
  depends_on:
    - CreateVirtualMachine

Then, you can login virtual machine via ssh by n0user user using key below:

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIBAQh+adEg/rjqj9qLE0jI4EqV8kZFDzWTASAwvx6HWdoAoGCCqGSM49
AwEHoUQDQgAEhOjA+fY6XV4K9c3ldX4tvqN9fOANtfIR21rJp0NQm0Wtw3abaaML
UHbRUECglxm1JiSaOuWVLTpDbpN7mxNi8Q==
-----END EC PRIVATE KEY-----

(Ubuntu 18.04 Cloud Image doesn’t allow password login to ssh configured above, so you need set password if need to access via VNC console)

Inverse action
Delete_test-vm:
  type: VirtualMachine
  action: DeleteVirtualMachine
  args:
    name: test-vm

Delete_blockstorage:
  type: BlockStorage
  action: DeleteBlockStorage
  args:
    name: cloudimage-ubuntu-1804
  depends_on:
    - Delete_test-vm

Delete_test-network:
  type: Network
  action: DeleteNetwork
  args:
    name: test-network
  depends_on:
    - Delete_test-vm

Register blockstorage as an Image

You can manage blockstorages by registering to image, versioning blockstorage with tag.

FetchISO:
  type: BlockStorage
  action: FetchBlockStorage
  args:
    name: cloudimage-ubuntu-1804
    annotations:
      n0core/provisioning/block_storage/request_node_name: vm-host1
    request_bytes: 1073741824 # 1GiB
    limit_bytes: 10737418240 # 10GiB
    source_url: https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img

ApplyImage:
  type: Image
  action: ApplyImage
  args:
    name: cloudimage-ubuntu

RegisterBlockStorage:
  type: Image
  action: RegisterBlockStorage
  args:
    image_name: cloudimage-ubuntu
    block_storage_name: cloudimage-ubuntu-1804
    tags:
      - latest
      - "18.04"
  depends_on:
    - ApplyImage
Generate BlockStorage from Image
GenerateBlockStorage:
  type: Image
  action: GenerateBlockStorage
  args:
    image_name: cloudimage-ubuntu
    tag: "18.04"
    block_storage_name: test-blockstorage
    annotations:
      n0core/provisioning/block_storage/request_node_name: vm-host1
    request_bytes: 1073741824
    limit_bytes: 10737418240
Delete image
Remove_cloudimage-ubuntu:
  type: Image
  action: DeleteImage
  args:
    name: cloudimage-ubuntu
  depends_on:
    - Delete_test-vm
Delete image (detailed)
Untag_1804_from_cloudimage-ubuntu:
  type: Image
  action: UntagImage
  args:
    name: cloudimage-ubuntu
    tag: "18.04"
  depends_on:
    - Delete_test-vm

Untag_latest_from_cloudimage-ubuntu:
  type: Image
  action: UntagImage
  args:
    name: cloudimage-ubuntu
    tag: latest
  depends_on:
    - Delete_test-vm

Unregister_cloudimage-ubuntu-1804-from-cloudimage-ubuntu:
  type: Image
  action: UnregisterBlockStorage
  args:
    image_name: cloudimage-ubuntu:
    block_storage_name: cloudimage-ubuntu-1804
  depends_on:
    - Untag_1804_from_cloudimage-ubuntu
    - Untag_latest_from_cloudimage-ubuntu

Remove_cloudimage-ubuntu:
  type: Image
  action: DeleteImage
  args:
    name: cloudimage-ubuntu
  depends_on:
    - Unregister_cloudimage-ubuntu-1804-from-cloudimage-ubuntu

Remove_cloudimage-ubuntu-1804:
  type: BlockStorage
  action: DeleteBlockStorage
  args:
    name: cloudimage-ubuntu-1804
  depends_on:
    - Unregister_cloudimage-ubuntu-1804-from-cloudimage-ubuntu

Architectural Decision Records

Status

  • proposed
  • accepted
  • deprecated

Translate to English

I will do when I remember. :pray:

List

Asynchronous Messaging Queue System Architecture

   
Status deprecated
Context

OpenStackの問題点を考えた際に、無駄に規模が大きいことが問題であると考えていた。 よって、OpenStackのスケールを小さくしたアーキテクチャを考えれば良いと考えた。

Decision

以上の経緯から、OpenStackのシステムアーキテクチャを踏襲し、ユーザにはHTTPで非同期APIを提供したうえで、コンポーネント間の通信をMessaging Queueで通信することを考えた。 やろうと思ったことは これ を参照してほしい。

Consequences

力不足も相まって形にすることができず、 Synchronous RPC System Architecture に設計方針を変更した。

Pros

実際に運用できたわけではないため、よくわからないが一般的に言われている以下のようなメリットあるだろう。

  • 簡単にコンポーネントをスケールされることができる
  • コンポーネント間を粗結合にすることができる
Cons

まず、MQを運用と構築することが難しい。 KafkaやPulsarなどは非常に大規模なミドルウェアであり、真面目に運用しようとすると多くのコストがかかる。 つまり、n0stackを構築することや運用することが非常に難しくなることを示しており、少なくともすべての基盤となるIaaS部分で使うべきではないと判断した。

また、MQは多くのコンポーネントを運用するために利用ことで真価を発揮する。 しかし、n0stackは構築などを簡単にすることを目指しているため、コンポーネントは少なくしたいと考えている。 そのため、MQを採用しても得られるメリットが少ない。

くわえて、Exactly onceでキューが送られることが保証されるわけではないため、イベントがべき等になるように設計する必要がある。 Virtual Machineの再起動などクラウド基盤ではイベントを正しく処理できなければならない。 それらの設計方法は知見としてあまり共有されておらず、トピックなどの使い方もよくわからず学習コストが高かった。 というか、自分がいつまで立ってもいい感じに設計できなかったため諦めた。

n0core packages

   
Status accepted
Context
  • 上位レイヤのパッケージの依存関係を明確にすることで、開発を効率的に行うことができると考えている
Decision

以下のように区分する。

n0core/pkg/api
  • API の実装を書く
n0core/pkg/datastore
  • データの永続化、ロック
n0core/pkg/driver
  • 外部依存や副作用があるようなモジュール
n0core/pkg/util
  • 外部依存や副作用がなくてみんなで共通で使えるモジュール
n0core/pkg/deploy
  • バイナリをデプロイするなどの処理を書く
n0proto.go/*
  • n0protoでgRPC定義されたものを、 make build-n0proto で自動生成されたもの
n0proto.go/pkg/transaction
  • 処理のトランザクションを管理するモジュール
  • TODO: n0core/pkg/util に移す
Consequences
  • 適宜更新

Lock about update process

   
Status accepted
Context
  • 同じオブジェクトに対する更新系のエンドポイントが同時に実行された場合、多くの問題が発生する
    • 両方処理された場合、あとに終了する操作のみがDBに反映され、実体が二つ存在することになる
    • 一貫性がないため、同じIPがスケジューリングされたりする
  • 一貫性と整合性を保証する必要がある
    • TODO: どの強さのものなのかは少し勉強してからかく
Decision
  • github.com/n0stack/n0stack/n0core/pkg/datastore/lock に実装を行った
  • apiは更新系のエンドポイント開始時に Datastore.Lock(key string) をかける
    • ロックをかけることで一貫性を保証
    • ロックがかかっていないものはdatastoreの更新系を実行できないようにブロック
      • 実装の間違いに気づきやすいようにした
        • panicにしても良さそう?
    • ロックに失敗した場合即時返していたが、 ReserveStorage などのエンドポイントのエラーレートが非常に高くなってしまったので、ロックがかかるまで一定時間リトライするものを実装した
      • Createはロックできない時点で他の人が作っているはずで、ユーザーの不備なはずなのですぐ返す
      • Deleteは少し危険な気がするので安全側に倒すなら待たない
      • 拡張メソッドは基本的に他のユーザーがロックをかけている可能性があるので、待つ
  • 参照系に関しては制約をかけていない
    • 高いパフォーマンスが優先されると考えたため
    • 少し古いデータを見せられても致命的な問題が起こるとは考えにくい
  • DBの機能を使わなかった理由は、今後n0core以外のデーモンを消していくつもりであるため
    • n0coreはすべての起点であるため、依存を可能な限り減らしたい
Consequences
  • 適宜更新
Reference
  • #115

n0deploy

   
Status accepted
Context
  • アプリケーションのデプロイは検証環境と本番環境を可能な限り近づけるべきである
    • 検証環境で動いても本番環境では動かない、またはその逆が発生する確率が非常に高く、それは開発効率を著しく低下させる
  • VMのデプロイ手法を再現可能な状態で管理するにはまだ課題がある
    • DockerfileでVMにデプロイする場合、VMはミュータブルであるためビルドキャッシュを有効化することが難しく、ゼロから環境を構築するため必要があるため継続的に開発するには遅い
    • AnsibleでVMにデプロイすることが一般的だが記述量が増える傾向にあるため、小さなアプリケーションにはコストがかかりすぎ、大きなアプリケーションは記述量が多すぎてメンテナンスできないという問題があった
Decision
  • Dockerfileを参考に RUNCOPY の機能を実装することで記述量を削減
  • 処理を二つに分割することで、継続的に開発を行うことへの足かせを減らす
    • Bootstrap: IPの設定や周辺サービスなど環境を構築する事前状態を定義する
    • Deploy: 開発しているものを適用する
    • つまり Bootstrap * 1 + Deploy * N を適用することで開発を行い、 Bootstrap * 1 + Deploy * 1 を適用することで本番環境に展開する
文法
BOOTSTRAP # optional
RUN apt update \
 && apt upgrade -y \
 && apt install -y ...

DEPLOY
COPY some_app/ /opt/some_app
Consequences
  • 適宜更新

Pending State

   
Status accepted
Context

APIが障害になった場合、どこまで処理を行ったかわからず、不整合の原因になると考えられる。特に、VirtualMachineやBlockStorageなど実体の操作が伴うものはレスポンスまでの時間が長いため、API障害の影響を受けやすいと考えられる。

Decision
  • VirtualMachineやBlockStorageのCreateなど実体の操作が伴うものは最初に PENDING ステートに設定
    • PENDING ステートのものは更新を行えないようにする

これによって、APIの故障によって処理が止まってものは PENDING ステートによって操作がロックされ、不整合の拡大を抑制することができる。管理者は手動で不整合が起きていないか確認を行い、復旧することで正常性を維持する。

Example in BlockStorage
  • 作成の場合
    • PENDING で保存
    • 失敗した場合は削除
func (a *BlockStorageAPI) CheckAndLock(tx *transaction.Transaction, bs *pprovisioning.BlockStorage) error {
    prev := &pprovisioning.BlockStorage{}
    if err := a.dataStore.Get(bs.Name, prev); err != nil {
        log.Printf("[WARNING] Failed to get data from db: err='%s'", err.Error())
        return grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to get '%s' from db, please retry or contact for the administrator of this cluster", bs.Name)
    } else if prev.Name != "" {
        return grpcutil.WrapGrpcErrorf(codes.AlreadyExists, "BlockStorage '%s' is already exists", bs.Name)
    }

    bs.State = pprovisioning.BlockStorage_PENDING
    if err := a.dataStore.Apply(bs.Name, bs); err != nil {
        return grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to apply data for db: err='%s'", err.Error())
    }
    tx.PushRollback("free optimistic lock", func() error {
        return a.dataStore.Delete(bs.Name)
    })

    return nil
}
  • 更新の場合
    • PENDING になってないか確認
    • PENDING で保存
    • 失敗した場合は前のステートに変更
func (a *BlockStorageAPI) GetAndLock(tx *transaction.Transaction, name string) (*pprovisioning.BlockStorage, error) {
    bs := &pprovisioning.BlockStorage{}
    if err := a.dataStore.Get(name, bs); err != nil {
        log.Printf("[WARNING] Failed to get data from db: err='%s'", err.Error())
        return nil, grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to get '%s' from db, please retry or contact for the administrator of this cluster", name)
    } else if bs.Name == "" {
        return nil, grpcutil.WrapGrpcErrorf(codes.NotFound, "")
    }

    if bs.State == pprovisioning.BlockStorage_PENDING {
        return nil, grpcutil.WrapGrpcErrorf(codes.FailedPrecondition, "BlockStorage '%s' is pending", name)
    }

    current := bs.State
    bs.State = pprovisioning.BlockStorage_PENDING
    if err := a.dataStore.Apply(bs.Name, bs); err != nil {
        return nil, grpcutil.WrapGrpcErrorf(codes.Internal, "Failed to apply data for db: err='%s'", err.Error())
    }
    bs.State = current
    tx.PushRollback("free optimistic lock", func() error {
        return a.dataStore.Apply(bs.Name, bs)
    })

    return bs, nil
}
Consequences
  • PENDING のものが多くなってくると運用に耐えられなくなると考えられるので、実際に動かしながら確認
  • networkにも組み込んでしまったが本来はいらない
    • 本来はこれの目的で実装していたが、全く効果がなかったので理由が変更になった

Synchronous RPC System Architecture

   
Status accepted
Context

Asynchronous Messaging Queue System Architecture の反省点としては、MQのような難しすぎる概念を導入しても手に負えず、構築を簡単にするには構成を簡単にする必要があることを学んだ。

また、Kubernetesはコンテナ管理基盤としてCRDインターフェイスを採用し、ステートレスなりソースの管理に長けている。 しかし、CRDではVirtual Machineの起動、再起動、シャットダウンなどを表現することは難しく、ステートフルなリソースを管理するためにはRPCインターフェイスが必要であると考えた。

Decision
  • シンプルな構成にするため、同期的に処理を伝播していく
    • APIでユーザーからのリクエストを受理し、APIが各ノードのAgentに指示をだす
    • Designing Distributed Systems でいうScatter/Gatherパターンであり、ミドルウェアは永続化を行うデータベースだけとなる
  • gRPCインターフェイスを作る
Transaction

同期的に実行するため、一つのエンドポイントで多くの処理を行い、失敗する可能性も高い。 よって、原子性 (Atomicity) を保証するために操作をロールバックする仕組みを実装した。

Resource Operation Lock System

リソース操作の独立性 (Isolation) を保証するために操作を開始するときにロックする仕組みを実装した。

Pending State

操作が障害などによって中断されるなど、リソースがRPCの操作からではどうしようもなくなったことを示す状態を定義した。

Consequences

ICTSC 2018で600台近くのVirtual Machineを管理した。詳細は こちら

Pros
  • RPCに成功したか、失敗したか、PENDINGステートのゴミが出来上がるかの三択
  • リソースの作成は理論上一番はやい構成
  • 構成要素が少なく、構築が簡単
Cons
  • CopyBlockImageなど遅いエンドポイントで待たされる
  • 遅いエンドポイントではセッションが切れたりユーザーが中断することでPENDINGステートのものが生成される可能性が高い
  • エンドポイントごとに処理時間の差が大きいため、APIプロセスの負荷が偏る可能性が高い

Transaction for update process

   
Status accepted
Context
  • n0stackはgRPCを使って粗結合に実装しているため、マイクロサービスと同様の問題を抱えている
    • 特に途中で処理が失敗したときにもとに戻す必要がある (原子性)
Decision
  • github.com/n0stack/n0stack/n0proto.go/pkg/transaction に実装した
  • Transaction に行った操作の逆の関数をpush指定いき、失敗したときにその関数をシーケンシャルに逆に呼んでいくことでロールバックを実現する
Consequences
  • 適宜更新
  • 正直 github.com/n0stack/n0stack/n0proto.go/pkg/transaction の場所は失敗したと思っている
  • このトランザクションログを他のAPIと共有できれば、障害時にも強くなりそうだが現状思いつかない

Roadmap

2019 Mar.

月末にブログなどを公開するなどして多くの人に見てもらえる環境まで整備したい

  • [ ] クラスタの再構成が必要になりそうな変更を完了する
  • [ ] etcdへ依存をなくしてデプロイをさらに簡単にする
  • [ ] n0cliの充実,生成自動化
  • [ ] 可能ならユーザードキュメントの充実

2019 Apr.

  • [ ] 開発環境の整備,各種テストの自動化など
  • [ ] implement n0bff
  • [ ] sshリソースによるデプロイの自動化

2019 May

  • [ ] 各種ミドルウェアリソースの試験導入,mysql,k8sなど

Tests

The principles about tests on n0stack.

Test size

small

  • unit test about logic
  • integration test about side effect
  • without side effect, for example…
    • persistent data
    • control middleware
  • 副作用は agent に固まっているので、 agent だけモックすることで…
    • ロジックの結合テストを small にて行える
    • agent からロジックを消すことで分散耐性を向上
    • モックの開発工数を減らせる
Goal
  • coverage n0core/pkg/api without agent > 70 %
  • coverage n0core/pkg/api with agent > 50 %
  • coverage n0core/pkg/datastore/memory > 70 %

medium

  • integration test about side effect on standalone
  • gRPC fuzzing about logic
Goal
  • coverage n0core/pkg/api > 70 %
  • coverage n0core/pkg/datastore/etcd > 80 %
  • coverage n0core/pkg/driver > 60 %

large

  • E2E

TODO

  • [x] 現状のテストが通るようにする
  • [x] 各 API のモックの作成と差し替え
  • [x] Agent からロジックの切り出し
  • [x] Agent のモック作成
  • [x] medium -> small に
  • [ ] API のテストを書いていく

Tools

Circle CI

  • バージョンのインクリメント

Travis CI

  • test-small と一応すべてのビルドができるか確認している

Go Report Card

  • Go の静的解析に用いている

CODE CLIMATE

  • とりあえずやっているが、Golangだけの現状では大して効果がなく、適当に残してあるだけ
    • JS などのコンポーネントもできると思うので、そのときに改めて考える
  • protobuf の自動生成されたコードは除外設定を行っている

FOSSA

  • ライセンスの確認を行っており、パスするようにする
  • etcd と zap (logger) を Ignore の設定をしている
    • 一般的に使われており、深さ 1 では問題ないため暫定処置