ROCK64 + Rook で作る、お家最強のお父さんCeph [前編] (k8s構築)

# はじめに
昨年、「お父さんの小遣いで作れる Ceph Server」というお題目で、Cluster HAT + Raspberry Pi Zero を使って低コスト・低消費電力・省スペースを実現した Cephクラスタを作成し、Qiita にまとめさせていただきました。

https://qiita.com/rev4t/items/cabddeee6d41afac0c2f

前回の結果としては、低消費電力・省スペースについては満足なものができたものの、性能に関してはあまり良い結果は得られませんでした。
そこで、今回は改良版として、実用性をも兼ね備えた Ceph Server を十分に低コストで作ることを目指して、新たなクラスタを構築しました。

# コンセプト (Hardware)
前回の反省を踏まえて、多少コストが上昇しても、性能が十分に出せるハードウェアを選定した上でCeph クラスタを構築することにしました。

要件としては、次の機能を満たせるサーバを使用します。

- コストについては前回よりも少し高くなっても良いので、性能を高める
  - ノードあたりのメモリを 4GB 程度確保する
  - Disk に SSD を使用する。なるべく高速なインタフェースでノードに接続する
  - 1Gbps の Ethernet

これらの要件を満たすことのできる　Single Board Computer を探したところ、ROCK64 が丁度良さそうなので、こちらをサーバとして選定しました。

https://www.pine64.org/devices/single-board-computers/rock64/

- Single Board Computer
  - ROCK64
- SSD
  - Crucial SSD 500GB MX500
    - Memory 4GB
    - SATA - USB3.0 変換アダプタを用いてサーバと接続
- Network
  - Gigabit Ethernet (1Gbps)
- Case
  - 積層式ケース for Raspberry Pi

上記パーツを組み上げると、次の写真のようになりました。
SSD をうまく固定できていないのがイケていないですが、大きさ自体は小さいのでお部屋の隙間などに忍ばせるには丁度良いかもしれません。

![rock64-tower.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96389/02b46017-4bf3-ec2b-edf0-1b3631f2010f.jpeg)



# コンセプト (Software)
ソフトウェアについては、ROCK64 は ARM64 に対応しているので、Ceph コミュニティが公式版としてリリースしている、Ceph Nautilus (version 14.x) がそのまま利用できます。

今回は、デプロイ方法として、ceph-ansible ではなく、Rook を使用します。リソース消費量の観点では ceph-ansible の方が Rook より優れていますが、Kubernetes で PersistentStorage を使いたかったので、より簡単に Persistent Storage を構成できる Rook を使用します。

https://rook.io

# Kubernetes の構築 (High Availability 構成)
kubeadm を用いて、High Availability 構成の k8s クラスタを構築します。

次のドキュメントを参考にして、k8s control plane の冗長化を実現します。
https://kubernetes.io/ja/docs/setup/independent/high-availability/

ノード構成は、次の通りです。

- Controller Node
  - node01
  - node02
  - node03
- Worker Node
  - node04
  - node01, node02, node03 については、ノード数が少ないので Controller / Worker 兼用とします

## パッケージのインストール
はじめに、Docker をインストールします。

```bash
sudo apt install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo add-apt-repository \
   "deb [arch=arm64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

sudo apt update
sudo apt install -y docker-ce
```

次に、k8s パッケージをインストールします。

```bash
sudo bash
apt-get update && apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
exit
```

## k8s API EndPoint の冗長化
HAProxy + Keepalived を組み合わせることで、k8s API の EndPoint を冗長構成にします。
次の手順で、Controller Node を設定します。

```bash
# パッケージのインストール
sudo apt install keepalived haproxy

sudo vi /etc/keepalived/keepalived.conf
sudo vi /etc/haproxy/haproxy.cfg

sudo systemctl restart haproxy
sudo systemctl restart keepalived
```

```/etc/keepalived/keepalived.conf
global_defs {
   #notification_email {
   #  user@example.com
   #}
   #notification_email_from k8s-lb01.example.com
   #smtp_server localhost
   #smtp_connect_timeout 30
}
vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 100
    priority 101
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 7140
    }
    virtual_ipaddress {
        192.168.3.210
    }
}
```

```/etc/haproxy/haproxy.cfg
global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

    # Default SSL material locations
    ca-base /etc/ssl/certs
    crt-base /etc/ssl/private

    # Default ciphers to use on SSL-enabled listening sockets.
    # For more information, see ciphers(1SSL). This list is from:
    #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
    # An alternative list with additional directives can be obtained from
    #  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
    ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
    ssl-default-bind-options no-sslv3

defaults
    log global
    mode    http
    option  httplog
    option  dontlognull
    option forwardfor
    option http-server-close
        timeout connect 5000
        timeout client  50000
        timeout server  50000
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http

frontend http_stats
   bind *:8080
   mode http
   stats uri /haproxy?stats

frontend haproxy_kube
    bind 0.0.0.0:443
    mode tcp
    option tcplog
    timeout client  10800s
    default_backend masters

backend masters
    mode tcp
    option tcplog
    balance leastconn
    timeout server  10800s
    server k8s-master01 192.168.3.201:6443 check
    server k8s-master02 192.168.3.202:6443 check
    server k8s-master03 192.168.3.203:6443 check
```

```bash
sudo vi /etc/hosts
```

```/etc/hosts
# for k8s API Endpoint
# 次の行を追加します
192.168.3.210 k8s-api.local
```

## kubeadm 設定ファイルの作成
冗長構成に必要なオプションを kubeadm に与えるために、次のような kubeadm-config.yaml ファイルを作成します。

設定の注意点としては、今回は flannel を使用するため、networking.podSubnet に "10.244.0.0/16" を設定しております。weavenet を除き、podSubnet を明示的に指定する必要があるのでご注意ください。

### ARM64 k8s ネットワーク選定についての注意点
k8s 本体については ARM64 への対応が進んでおり、特に何も気にしなくても ROCK64 等の上で動作します。ただし、プラグインで提供される CNI の各種実装については、対応状況はソフトウェアによってマチマチなので、ご注意ください。flannel については、Rook を組み合わせた場合でも、問題なく動作する模様です。

### Rook k8s ネットワーク選定についての注意点
Rook で Ceph をデプロイする場合、Default 設定では CNI で提供されるネットワークに Pod が配置されます。
この場合、Storage のデータ通信が発生する度に tunneling するためのCPU・Networkリソースが余分に消費されます。性能を重視する場合は、Calico等の比較的リソース消費の低い実装の利用、または Host Networking を使用する必要があります。

```bash
vi kubeadm-config.yaml
```

```kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: stable
apiServer:
  certSANs:
  - "k8s-api.local"
controlPlaneEndpoint: "k8s-api.local:443"
# for flannel
networking:
  podSubnet: "10.244.0.0/16"
# for calico
#networking:
#  podSubnet: "192.168.0.0/16"
```

## node01 のデプロイ
はじめに、1ノード目のデプロイを実施します。

k8s は swap が無い環境での稼働が前提となっているので、ROCK64 のように swap が提供されている場合は、あらかじめ swapoff コマンド等を用いて swap を無効化した上で kubeadm コマンドを実行します。

```bash
# ROCK64 では swap に zram を用いているので、'zram-config' サービス を停止することで swap を無効化します
sudo systemctl stop zram-config
sudo systemctl disable zram-config

# k8s のデプロイを実施します
sudo kubeadm init --config=kubeadm-config.yaml
```

1ノード目のデプロイが成功したら、2ノード目以降のデプロイに用いるコマンド等が最後に出力されるので、コピーする等をして覚えておきます。

kubectl コマンドの credential については、次のコマンドを実行することで配置できます。

```bash
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# kubectl のコマンドライン補完
sudo apt-get install bash-completion
echo "source <(kubectl completion bash)" >> ~/.bashrc
```

### flannel のデプロイ
flannel のデプロイを実施します。次のコマンドを実行します。

```bash
# flanel のデプロイ
# (URL は k8s 1.16 向けのため、version が上がった場合は
#  変更する必要があります)
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/2140ac876ef134e0ed5af15c65e414cf26827915/Documentation/kube-flannel.yml
```

## node02, node03, node04 のデプロイ
残りの Controller Node(node02, node03) については、node01 にて生成された Credentialをあらかじめ node02, node03 にコピーした上で デプロイを実施する必要があります。

copy-creds.sh を node01 にて実行し、SSL証明書を node02, node03 にコピーします。

```bash
vi copy-creds.sh
sudo sh copy-creds.sh
```

```copy-creds.sh
USER=rock64
CONTROL_PLANE_IPS="node02 node03"
for host in ${CONTROL_PLANE_IPS}; do
    scp /etc/kubernetes/pki/ca.crt "${USER}"@$host:
    scp /etc/kubernetes/pki/ca.key "${USER}"@$host:
    scp /etc/kubernetes/pki/sa.key "${USER}"@$host:
    scp /etc/kubernetes/pki/sa.pub "${USER}"@$host:
    scp /etc/kubernetes/pki/front-proxy-ca.crt "${USER}"@$host:
    scp /etc/kubernetes/pki/front-proxy-ca.key "${USER}"@$host:
    scp /etc/kubernetes/pki/etcd/ca.crt "${USER}"@$host:etcd-ca.crt
    scp /etc/kubernetes/pki/etcd/ca.key "${USER}"@$host:etcd-ca.key
    scp /etc/kubernetes/admin.conf "${USER}"@$host:
done
```

```bash
vi install-creds.sh
sudo sh install-creds.sh
```

```install-creds.sh
USER=rock64
mkdir -p /etc/kubernetes/pki/etcd
mv /home/${USER}/ca.crt /etc/kubernetes/pki/
mv /home/${USER}/ca.key /etc/kubernetes/pki/
mv /home/${USER}/sa.pub /etc/kubernetes/pki/
mv /home/${USER}/sa.key /etc/kubernetes/pki/
mv /home/${USER}/front-proxy-ca.crt /etc/kubernetes/pki/
mv /home/${USER}/front-proxy-ca.key /etc/kubernetes/pki/
mv /home/${USER}/etcd-ca.crt /etc/kubernetes/pki/etcd/ca.crt
mv /home/${USER}/etcd-ca.key /etc/kubernetes/pki/etcd/ca.key
mv /home/${USER}/admin.conf /etc/kubernetes/admin.conf
```

次に、node02, node03 のデプロイを実施します。
それぞれの Node にて、次のようなコマンドを実行します。

```bash
sudo kubeadm join k8s-api.local:443 --token siy22k.utdegxec3yizt86k \
    --discovery-token-ca-cert-hash sha256:abcdeb6aec784da49153cad484dbb4eef2d8bd639bfa628e49c6a72834a4a61 \
    --control-plane
```

最後に、node04 のデプロイを実施します。
Worker Node のデプロイについては、'--control-plane' オプションが不要になります。

```bash
sudo kubeadm join k8s-api.local:443 --token siy22k.utdegxec3yizt86k \
    --discovery-token-ca-cert-hash sha256:abcdeb6aec784da49153cad484dbb4eef2d8bd639bfa628e49c6a72834a4a61
```

node01, node02, node03 についても通常の Pod を配置したいので、次のコマンドを実施します。

```bash
kubectl taint nodes --all node-role.kubernetes.io/master-
```

# ストレージのベンチマーク結果

SSD のベースライン性能を測定するために、fio を用いたベンチマークを実施しました。
測定内容としては、次のように random read / random write について確認します。

```bench.sh
#!/bin/sh

fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=1024k --time_based=1 --runtime=180 --name=fio_direct_write_test_1024k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=256k --time_based=1 --runtime=180 --name=fio_direct_write_test_256k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=128k --time_based=1 --runtime=180 --name=fio_direct_write_test_128k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=64k --time_based=1 --runtime=180 --name=fio_direct_write_test_64k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=32k --time_based=1 --runtime=180 --name=fio_direct_write_test_32k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=16k --time_based=1 --runtime=180 --name=fio_direct_write_test_16k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=8k --time_based=1 --runtime=180 --name=fio_direct_write_test_8k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=4k --time_based=1 --runtime=180 --name=fio_direct_write_test_4k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=2k --time_based=1 --runtime=180 --name=fio_direct_write_test_2k
fio --filename=/mnt/fio.test --direct=1 --rw=randwrite --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=1k --time_based=1 --runtime=180 --name=fio_direct_write_test_1k

fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=1024k --time_based=1 --runtime=180 --name=fio_direct_read_test_1024k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=256k --time_based=1 --runtime=180 --name=fio_direct_read_test_256k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=128k --time_based=1 --runtime=180 --name=fio_direct_read_test_128k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=64k --time_based=1 --runtime=180 --name=fio_direct_read_test_64k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=32k --time_based=1 --runtime=180 --name=fio_direct_read_test_32k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=16k --time_based=1 --runtime=180 --name=fio_direct_read_test_16k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=8k --time_based=1 --runtime=180 --name=fio_direct_read_test_8k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=4k --time_based=1 --runtime=180 --name=fio_direct_read_test_4k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=2k --time_based=1 --runtime=180 --name=fio_direct_read_test_2k
fio --filename=/mnt/fio.test --direct=1 --rw=randread --size=1G --randrepeat=0 --ioengine=libaio --group_reporting=1 --numjobs=1 --iodepth=128 --bs=1k --time_based=1 --runtime=180 --name=fio_direct_read_test_1k
```

| block size (kB)  | Read IOPS | Write IOPS |
|:-----------------|-----------|------------|
| 1                | 3246.42   | 2884.32    |
| 2                | 3290.21   | 2875.28    |
| 4                | 3253.29   | 3255.16    |
| 8                | 2959.34   | 3206.21    |
| 16               | 2498.34   | 3054.39    |
| 32               | 2057.03   | 2640.74    |
| 64               | 1532.51   | 1972.15    |
| 128              | 723.43    | 1036.37    |
| 256              | 464.33    | 600.48     |
| 1024             | 159.95    | 169.46     |

![overall-disk-performance.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96389/f1069c6d-853f-fecf-4517-98c10e53e0fc.png)


結果としては、次のようにとても高い性能が出ました。I/O性能的な観点では、arm64アーキテクチャの Single Board Computer で分散ストレージを構築するのは十分に可能であることが分かりました。


[後半に続きます](https://qiita.com/rev4t/items/02726b0ac21feb55cc99)