twitterにの次の時代の分散SNSとしてmisskeyというものがにわかに注目を集めている気がします。
これと似た分散型SNSとしてはmastodonのほうが有名な気がしますが、開発の歴史としてはmisskeyのほうがやや歴史が長いようです。
読者の方の中には、同じ分散型SNS用ソフトウェアである「Mastodon」を聞いたことがある人もいると思います。MisskeyはMastodonの仲間ではありますが、Mastodonのフォークや改造版といったものではなく、お互い全く別個に開発されたソフトウェアです。
ソフトウェアとしてはMisskeyのほうが歴史が古く(Misskeyは2014年、Mastodonは2016年から開発されています)、Misskeyは日本の開発者がメインでメンテナンスされてきました。ただし、「分散型」としての歴史はMastodonのほうが古くなります。
また、mastodonと比べて、より「twitter風」のSNSを意識して作られているようで、使用感もtwitterに近いものがあるようですね。
そして分散型SNSの最大の特徴は、自分でサーバを立てられるという点だと思います。今回はこのmisskeyサーバを自分で立ててみようと思います。
おうちKubernetes
おうちKubernetesとは、複数台のRaspberryPiで構成されたKubernetesクラスタのことで、本格的なサーバ機を複数台揃えるよりもずっと安価に、物理的に複数のマシンが協調するKubernetesクラスタを体験できます。
構築に際した過去の関連記事はこちら
せっかくKubernetesクラスタを構築したので、うまく活かして遊ぶ題材としてmisskeyは良いかも、と思ったのでデプロイしてみようと思います。
ノード構成
我が家のおうちKubernetesクラスタの構成はこんな感じになっています。
- RaspberryPi 4B メモリ4GB SSDでOS起動 コントローラノード
- RaspberryPi 3B メモリ1GB microSDでOS起動 ワーカノード
- RaspberryPi 3B メモリ1GB microSDでOS起動 ワーカノード
- RaspberryPi 3B メモリ1GB microSDでOS起動 ワーカノード
コントローラノードとなっているRaspberryPi 4Bのみ他よりもCPUやメモリに余裕があり、SSDにOSをインストールしているので、なにかデータを保存する場合はすべてこのSSDに集約したいです。逆に、3台のRaspberryPi 3Bはステートレスに動作できるようにしたいところです。
必要なコンテナとリソース
misskeyの構築手順のページには、自力で構築する方法から、docker-compose、Kubernetesで構築する方法まで用意されています。なかなか充実しています。
しかし、Kubernetes上への構築方法は、Helmを使う方法のみの紹介となっていました。個人的にHelmを使ったことが無いことと、RaspberryPiクラスタを使うという特殊な制約に対応する必要があるので、設定ファイルを自分で書くこととします。
misskeyのgithubリポジトリにdocker-compose.ymlのサンプルがあったので、これを参考に必要なものを考えます。
https://github.com/misskey-dev/misskey/blob/develop/docker-compose.yml.example
必要そうなリソースはこんな感じになりそうです。
コンテナ
- web
- db
- redis
ボリューム
- misskeyのconfigを保存するボリューム(これはConfigMapで良かったかもしれない)
- アップロードしたファイルを保存するボリューム
- dbのデータ用ボリューム
- redisのデータ用ボリューム
これらのうち、webコンテナはステートレスに動くようなので、こちらは冗長化に挑戦してみます。
事前準備
Kubernetesクラスタが使えるようになっていることに加えて、RaspberryPi 4B上にNFSを用意して、コンテナ側が生成したファイルを永続化できるようにします。
NFSサーバのインストール
$ sudo apt install nfs-kernel-server
今回、自分の環境では /home/commojun/nfs
を公開ディレクトリとしました。ディレクトリの公開設定は以下のようになりました。
$ sudo sh -c 'echo "/home/commojun/nfs 192.168.10.0/24(rw,sync,no_root_squash)" >> /etc/exports'
この設定をすることで、PersistentVolumeのNFSが使えるようになります。
そして、必要となるボリュームに合わせてそれぞれディレクトリを掘っておきます。
$ mkdir /home/commojun/nfs/misskey
$ mkdir /home/commojun/nfs/misskey/config
$ mkdir /home/commojun/nfs/misskey/db
$ mkdir /home/commojun/nfs/misskey/redis
$ mkdir /home/commojun/nfs/misskey/files
files
はコンテナ内の別ユーザが書き込みをするので、権限を変更しておきます。
$ chmod 0777 /home/commojun/nfs/miskey/files
RedisとDB Pod
RedisとDBのPodは次のように定義してみました。我が家のクラスタでは、RaspberryPi 4BのIPアドレスを 192.168.10.41
、ホスト名を pi41
としています。
ポイント
- redis, dbともRaspberryPi 4B内に用意したNFS内に永続化データを保存するようにしました
- 今回の作戦では、Pod自体もRaspberryPi 4Bにスケジューリングされるよう、NodeAffinityの設定もしました
- 両者を同じPodに詰め込んでも良かったかもしれません
- ストレージ関連の冗長化を考えるととてもハードルが高くなりそうだったので、StatefulSetとかも採用せず単純なPodにしました
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: misskey
spec:
selector:
app: redis
ports:
- name: http
port: 6379
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: pv-misskey-redis
namespace: misskey
spec:
storageClassName: misskey-redis
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
nfs:
server: 192.168.10.41
path: /home/commojun/nfs/misskey/redis
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-misskey-redis
namespace: misskey
spec:
storageClassName: misskey-redis
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: redis
namespace: misskey
labels:
app: redis
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- pi41
restartPolicy: Always
containers:
- name: redis
image: redis:7
volumeMounts:
- mountPath: /data
name: redis-volume
resources:
limits:
memory: "100Mi"
cpu: "250m"
ports:
- containerPort: 6379
volumes:
- name: redis-volume
persistentVolumeClaim:
claimName: pvc-misskey-redis
apiVersion: v1
kind: Service
metadata:
name: db
namespace: misskey
spec:
selector:
app: db
ports:
- name: http
port: 5432
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: pv-misskey-db
namespace: misskey
spec:
storageClassName: misskey-db
capacity:
storage: 4Gi
accessModes:
- ReadWriteOnce
nfs:
server: 192.168.10.41
path: /home/commojun/nfs/misskey/db
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-misskey-db
namespace: misskey
spec:
storageClassName: misskey-db
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi
---
apiVersion: v1
kind: Pod
metadata:
name: db
namespace: misskey
labels:
app: db
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- pi41
restartPolicy: Always
containers:
- name: psql
image: postgres:15
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: db-volume
envFrom:
- secretRef:
name: misskey-secret
resources:
limits:
memory: "800Mi"
cpu: "1"
ports:
- containerPort: 5432
volumes:
- name: db-volume
persistentVolumeClaim:
claimName: pvc-misskey-db
webコンテナすぐ死ぬ
ここまではそれなりに上手く行ったのですが、webコンテナの設定がかなりの鬼門でした。
作戦としては、RaspberryPi 3B 3台で冗長化して頑張ってみようとう方法を取ってみたのですが、すぐにメモリ容量が足らなくなってコンテナが強制終了されてしまいます。
$ kubectl describe pod web-deployment-******
~略~
Status: Failed
Reason: Evicted
Message: The node was low on resource: memory. Threshold quantity: 100Mi, available: 102204Ki. Container web was using 486968Ki, request is 0, has larger consumption of memory.
コンテナ自体のログを観察してみると、どうやらマイグレーションの段階でメモリオーバーしているようでした。
ちょっと調べてみると、RaspberryPiでmisskeyを立てている事例はあるようなのですが、RaspberryPi 4B です。さすがにメモリが1Gしかない3Bで立てるのは無理があるのか…?
RaspberryPi 3Bでどうしても起動したい
webコンテナのDockerfileを見てみると、毎起動時にマイグレーションをしたあとにサーバを立てるというような設定になっていました。
CMD ["pnpm", "run", "migrateandstart"]
しかしよく考えると、DBマイグレーションを行うのは、misskey本体にDBスキーマの変更があるほど大きなアップデートがあるときのみです。そこで、「マイグレーションを伴わない起動であればラズパイ3Bでも耐えられるのでは?」と考えました。
githubのリポジトリの package.json
の script
の箇所を調べてみると、マイグレーションをせずサーバをスタートするコマンドがありました。
"scripts": {
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js",
"migrateandstart": "pnpm migrate && pnpm start",
~略~
},
そこで、Kubernetesにデプロイするコンテナにはこのコマンドを利用することにしました。
containers:
- name: web
image: misskey/misskey:latest
command: ["pnpm", "run", "start"]
無理やりマイグレーション
肝心のマイグレーションはどうするのかというと、パワーのある手元のPC(docker-desktop)で無理やり実行することにしました。以下のようなことをします。
まずこんな感じのdocker-compose.ymlと設定ファイルを用意しておきます。
version: "3"
services:
migration:
image: misskey/misskey:latest
command: ["pnpm", "run", "init"]
ports:
- "3000:3000"
volumes:
- ${PWD}/files:/misskey/files
- ${PWD}/local.yml:/misskey/.config/default.yml
url: https://domain/
db:
host: host.docker.internal
port: 5432
db: misskey
user: misskey
pass: hogehoge
redis:
host: host.docker.internal
port: 6379
id: 'aid'
port: 3000
RedisとDBをポートフォワードします。
(それぞれ別のコンソールで)
$ kubectl port-forward db 5432:5432
$ kubectl port-forward redis 6379:6379
docker-composeを実行(普通にdocker runでワンライナーで実行したほうが簡素かもです)
$ docker compose run migration
これでなんとかマイグレーションができました。
あたって砕けるwebコンテナ軍団
マイグレーションを予め済ませておくことで、RaspberryPi 3B上でもなんとかコンテナが生きながらえることができるようです。それでも、負荷とかなんらかの理由で1日に1回くらいはメモリオーバーでコンテナが死んでしまいます。そこはもうRaspberryPi 3Bの限界なのかな、と割り切ることにして、 レプリカ3台のうち1台でも生きていればそれでいい ことにしました。
最終的に設定ファイルはこんな感じになりました。DB、Redis含めまだチューニングできる箇所はたくさんあると思いますがひとまずできたことにします。
apiVersion: v1
kind: Service
metadata:
name: web
namespace: misskey
spec:
type: NodePort
selector:
app: web
ports:
- name: http
port: 3000
targetPort: 3000
nodePort: 30080
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: pv-misskey-config
namespace: misskey
spec:
storageClassName: misskey-config
capacity:
storage: 5Mi
accessModes:
- ReadWriteOnce
nfs:
server: 192.168.10.41
path: /home/commojun/nfs/misskey/config
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-misskey-config
namespace: misskey
spec:
storageClassName: misskey-config
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Mi
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: pv-misskey-files
namespace: misskey
spec:
storageClassName: misskey-files
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
nfs:
server: 192.168.10.41
path: /home/commojun/nfs/misskey/files
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-misskey-files
namespace: misskey
spec:
storageClassName: misskey-files
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: web-deployment
namespace: misskey
labels:
deploy: web
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: "75%"
maxSurge: "50%"
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- pi31
- pi32
- pi33
containers:
- name: web
image: misskey/misskey:latest
volumeMounts:
- mountPath: /misskey/files
name: misskey-files
- mountPath: /misskey/.config
name: misskey-config
ports:
- containerPort: 3000
resources:
limits:
cpu: "2"
command: ["pnpm", "run", "start"]
readinessProbe:
httpGet:
path: /
port: 3000
scheme: HTTP
volumes:
- name: misskey-files
persistentVolumeClaim:
claimName: pvc-misskey-files
- name: misskey-config
persistentVolumeClaim:
claimName: pvc-misskey-config
予想に反して動いている
知り合いを数人招いただけですが、この程度のお遊びには耐えられるようです。今回はwebコンテナを安定させることに終始しましたが、もっとユーザが増えるとDBコンテナのリソースまわりに無理が出始めると思います。
Podの状況はというと、この通り死屍累々です。いくつもの犠牲の上にサービスが首をつないでいます。これはある意味でKubernetesの強みが活かせているのではないでしょうか!?!?
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
db 1/1 Running 1 (2d18h ago) 21d
redis 1/1 Running 1 (2d18h ago) 21d
web-deployment-764d5dcdb-2p5sv 0/1 ContainerStatusUnknown 1 17d
web-deployment-764d5dcdb-48fmz 0/1 ContainerStatusUnknown 1 11d
web-deployment-764d5dcdb-546pc 0/1 ContainerStatusUnknown 1 11d
web-deployment-764d5dcdb-5n92z 0/1 ContainerStatusUnknown 1 13d
web-deployment-764d5dcdb-5xj9l 0/1 ContainerStatusUnknown 1 14d
web-deployment-764d5dcdb-bjzxv 1/1 Running 0 3h23m
web-deployment-764d5dcdb-fc6dm 0/1 ContainerStatusUnknown 1 15d
web-deployment-764d5dcdb-fcqnq 0/1 ContainerStatusUnknown 1 13d
web-deployment-764d5dcdb-gb5k8 0/1 ContainerStatusUnknown 1 13d
web-deployment-764d5dcdb-ghbvs 0/1 ContainerStatusUnknown 1 (2d18h ago) 10d
web-deployment-764d5dcdb-hpsc8 0/1 ContainerStatusUnknown 1 (2d18h ago) 10d
web-deployment-764d5dcdb-k4cxh 0/1 ContainerStatusUnknown 1 14d
web-deployment-764d5dcdb-kh6q4 0/1 ContainerStatusUnknown 1 17d
web-deployment-764d5dcdb-m282x 0/1 ContainerStatusUnknown 1 44h
web-deployment-764d5dcdb-nr6g7 0/1 ContainerStatusUnknown 1 12d
web-deployment-764d5dcdb-p2bxs 0/1 ContainerStatusUnknown 1 15d
web-deployment-764d5dcdb-rqrbf 1/1 Running 0 46h
web-deployment-764d5dcdb-tlv88 0/1 ContainerStatusUnknown 1 (2d18h ago) 7d8h
web-deployment-764d5dcdb-vnsrl 1/1 Running 0 41h
web-deployment-764d5dcdb-zgbxs 0/1 ContainerStatusUnknown 1 17d
web-deployment-79bf9d8657-25x5x 0/1 ContainerStatusUnknown 1 21d
web-deployment-79bf9d8657-29dz6 0/1 ContainerStatusUnknown 1 19d
web-deployment-79bf9d8657-2v7j7 0/1 ContainerStatusUnknown 1 21d
web-deployment-79bf9d8657-464df 0/1 ContainerStatusUnknown 1 18d
web-deployment-79bf9d8657-49g2p 0/1 ContainerStatusUnknown 1 18d
web-deployment-79bf9d8657-gt7nl 0/1 ContainerStatusUnknown 1 21d
web-deployment-79bf9d8657-l2jvk 0/1 ContainerStatusUnknown 1 20d
web-deployment-79bf9d8657-stp5w 0/1 ContainerStatusUnknown 1 21d
web-deployment-79bf9d8657-sxtcn 0/1 ContainerStatusUnknown 1 20d
web-deployment-79bf9d8657-t6rzd 0/1 ContainerStatusUnknown 1 20d
まとめ
RaspberryPi 4B 1台 + 3B 3台構成のおうちKubernetesクラスタに今流行りのmisskeyをデプロイして遊んでみました。特に、メモリが1GしかないRaspberryPi 3Bにデプロイをすることに結構無理があったように思います。4Bを買い足したいという気持ちに何度もなりましたが、今は入手性も悪いし価格も高騰しているので、なんとかして今あるもので遊べないかという試行錯誤をしてみました。
実際に運用しているリポジトリへのリンクも記載しておきます。
またKubernetesやmisskeyを使って面白いことができたら記事にしたいと思います。