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"
}
}
}
]
}
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ファイル
- Nodeの
ReserveStorage / ScheduleStorage
でストレージを確保 - Nodeにインストールされたエージェントを操作するなどして、実態を作成
VirtualMachine¶
- NodeのCompute(CPUとメモリ)からVMを作り出す
- この時BlockStorageと、Networkに接続するNetworkInterface(MACアドレスとIPアドレス)を接続することができる
- Nodeの
ReserveCompute / ScheduleCompute
でCPUとメモリを確保 - 接続するBlockStorageを
SetInuseBlockStorage
で確保 - 接続するNetworkに対して
ReserveNetworkInterface
でMACアドレスとIPアドレスを確保 - Nodeにインストールされたエージェントを操作するなどして、実態を作成
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-1804
のtags
で確認することができる
- タグは
block_storage_name
で生成する BlockStorage の名前を指定する- VirtualMachine 生成時にVMとブロックストレージを接続するために用いる
- まだスケジューリングに対応していないため、
annotations
のn0core/provisioning/block_storage/request_node_name
で BlockStorage をどこのノードに配置するかを決める- ノードの名前は
n0cli get node
で確認できる
- ノードの名前は
- 生成する BlockStorage の容量は
10 GB (10737418240 Bytes)
- ゲストOSからはブロックストレージがこのサイズに見える
- 生成する BlockStorage の実際に使う可能性のある容量は
1 GB (1073741824 Bytes)
- この値はスケジューリングなどに用いられる
- Image は Docker Image のように名前とタグによって管理されているため、タグを指定する必要がある
- 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は性能劣化が激しかったので、無効化しているため
- この場合メモリ
- まだスケジューリングに対応していないため、
annotations
のn0core/provisioning/virtual_machine/request_node_name
で BlockStorage をどこのノードに配置するかを決める block_storage_names
で接続する BlockStorageを指定する- この場合、Image から作成した BlockStorage を接続している
nics
でどの Network に接続するか指定する- この場合、作成した Network に
192.168.0.1
で接続することを宣言している
- この場合、作成した Network に
uuid
はuuidgen
などで適宜生成すること- 使っているゲスト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¶
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を参考に
RUN
とCOPY
の機能を実装することで記述量を削減 - 処理を二つに分割することで、継続的に開発を行うことへの足かせを減らす
- 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
}
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の操作からではどうしようもなくなったことを示す状態を定義した。
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と共有できれば、障害時にも強くなりそうだが現状思いつかない
References¶
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 では問題ないため暫定処置