Article:

この記事はNeosVR Advent Calendar2022 15日目の記事です。

3枚目が作られていたのでうっかり甘えて登録してしまいましたが、実際この記事はNeosVR本体の内容が殆どなく、サーバー運用の話が99%の特殊な記事となっているので賞味番外編です。 「こういうのもあるんですね」という気持ち流し読みしていただけたら幸いです。


今年、NeosVRにてRabbuttz(あむ)氏主催のケモノ集会である、「ケモノHUB」がスタートしました。

ピークでは1インスタンスに50近く人が集まるなどNeosVRでは比較的大規模な集会が実施され、また専用Discordサーバーにも多くの人が集まるなど、非常に活発的なコミュニティとなっています。

kemohub

さて、NeosVRはセッション(VRChatで言うところのインスタンス)を立てた人が「ホスト」となり、ワールド上で発生する様々な処理を行います。

これは通常クライアントで行えるのですが、ゲストとしてセッションに参加する以上のコンピューター負荷がかかるので、ホストとなるコンピューターはより高いコンピューター性能を持っていることが好ましくなります。

そこで、NeosVRでは「ヘッドレスサーバー」と呼ばれる描画処理等を行わない、ホストとしての処理のみを行うサーバー(所謂、Dedicated Server)を立てることができます。

僕は今年、ケモノhub本番セッションと恒常セッション「ケモノ広場」のサーバー管理に加えて、プレイ情報解析基盤、Discordとの連携機能等において、様々な施策をやってみたので、やっていること・やってみたけどうまくいかなかったことなどの話ができたらなと思いこの記事を作りました。

他のNeosHeadless運用者の方や、それでなくてもジェネラルな鯖管の方々のコメント・アドバイスがいただけると嬉しいです。

サーバーマシン調達

何にせよヘッドレスを動かすためには、土台となるサーバーインスタンスが必要です。

Gammalab(自宅のこと)では、2台の物理マシンが稼働しており、その両方でESXiベアメタル・ハイパーバイザーが動いていて、その上に複数の仮想マシンが稼働しています。 また、terraformでインスタンス構成を管理し、ansibleにてプロビジョニングを行っています。CI/CDにはConcourseを使っています(詳細は別記事)。

Gammalab (House) Server

Gammalabでは2台の物理マシンのそれぞれに1つずつNeosヘッドレス用インスタンスを発行しています。

メトリクスの収集

今サーバーがどのような状態となっているかを知ることは、運用する上で非常に重要です。

まずサービスが動いているかどうかを知るだけでなく、何かしらのトラブルが発生した際に、その原因解明を行うのに非常に役に立ちます。

まず大体インストールするのがnode_exporterです。CPU使用率やメモリ使用率などの超基本的な項目を始め、いろいろとれます(雑)。

node_exporter

とは言え、node_exporterだけでは今このサーバーにどんなワールドが立っているのか、どのくらいの人が入っているのかなどのNeosの詳細な情報がわかりません。

そこで、NeosHDLのプロセスの起動に細工を施しました。

NeosHDLのAPI拡張

この機能を追加したときは別件で急いでいたというのもあり、手っ取り早く実装したかったのでglitchfurさんのステキなスクリプト“NeosVR-Headless-API”を使わせていただきました。

これはVirtual-BLFCというイベントでも使われたらしいですね。

これを使って、ワールドの情報がメトリクスとして抜けるようにします。

スクリプト: neos_api_server.py

これでどんなセッションにどれだけの人がいるのか分かるようになりました。

session_users

何時にどれくらい人がいたかが分かると、「何人のユーザーがいた場合にどれだけのCPU/メモリ/ネットワーク使用率があったのか」という記録が残り、 リソース量を見直すキャパシティプランニングに大いに役立ちます。(上の図だと緑がメモリ使用率、黄、青、橙が凡例を隠してしまいましたがそれぞれ別のセッションのユーザー数です。)

また、余談ですがhttpによる操作も行えるようにしました(ちょっと危険ですが…) 例えば、Neos内HttpクライアントであることのInboxから、サーバー操作ができます。

inbox

ログの収集

これはNeosのために新しく始めたことではないのですが、GammalabではElasticSearch + FileBeatでNeosHDLのログを集約しています。 これを用いると、複数サーバーのログが纏めて検索できて便利です。

Filebeatだけだと細かい設定をしないとメッセージ内部の分解ができないので細かいクエリは使えないのですが、elasticで範囲指定してjoinログを抽出→CSV書き出し→awkとかsortとかuniqとかでUU(Unique User)を算出してました。 若干遠回りな気もしますが、それでもログローテートが行われぶつ切りになったり、そもそも複数インスタンスにまたがってたりするログを纏めて見れるだけでも相当便利です。

elastic-log

サーバーの自動再起動

NeosのHDLのプログラムは、ちゃんと厳密に調べてるわけではないですがメモリリークしてるっぽいです。まぁメモリリークって意図しない解法忘れのメモリがあるっていう意味なので、開発者が意図的に大量にメモリを確保しているのならメモリリークではないのですが…。

とは言え、このバグ(仕様)のせいでNeosのHDLプログラムは連続して走らせることが難しく、ワールド上にたくさんのアイテムを出すと容易にメモリ使用量が増えていき、足りなくなるとOOMでクラッシュしてしまいます。

なので、非常に対処療法的なのですが、一定時間ごとにサーバーを再起動させるスクリプトを走らせることで難をしのいでいます。

また、ユーザーがプレイしている間に勝手に再起動しないように、前前節で導入したAPIを叩いて、プレイヤーがいないかどうかをチェックします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
##!/bin/bash

set -eu

host='localhost:5000'
maxup=43200

if [ $(cat /proc/uptime | awk -F'.' '{print $1}') -gt $maxup ]; then 
    users=$(curl -s $host/api/worlds | jq '.result | [-length] + map(.users) | add')
    if [ $users -le 0 ]; then
        reboot
    else
        echo 'there are some users!'
    fi
fi

これを応用すると、安全にシステムの再起動・Neosプロセスの再起動を行うスクリプトができます。どちらも運用上便利です。

より多くのメトリクスを求めて

前章でHDLからセッション情報が取れるようになりましたが、これだけではまだ情報が足りません。

メトリクスとして大事なのは、「プレイヤーが快適に遊べる環境にあるかどうかを指標として得る」ことです。

例えば、node_exporterで取得できるCPU使用率は、たしかにホストに負荷がかかっているかどうかを知ることができますが、その数値だけを見てサービスに影響が出ているかどうかを判定することは難しいのです。(少なくても快適じゃないこともあるし、振り切れていてもさほど問題ないことも多々ある)

前節ではHDLのコマンドを変わりに叩いてくれるスクリプトにより追加の情報が得られるようになったわけですが、HDLのコマンドから得られる情報はあまり多くなく、ダイレクトにユーザーが快適に遊べているかどうかの指標を得ることができません。

快適に遊べているかの指標というのは非常に難しいのですが、一つ例を上げるなら、「FPS」でしょう。

プレイヤーのFPSが低下しているのであれば、それは由々しき事態です。

きっとワールドに非常に重たいオブジェクトが設置されてしまったり、重たいアバターの人が密集してしまったりしているに違いありません。そうなれば、オブジェクトを撤去したり、複数セッションに分割するなどのアクションが必要となります。

このようにメトリクスとしては非常に優秀なFPSなのですが、これはゲームの外から知ることはあまりできません。

そこで、若干マジか~という感じではありますが、思い切ってNeosの内部からLogiXでセッションの情報を特定エンドポイントに送信する、PUSH型の監視を導入してみました。

Neos内からメトリクスをPUSHする

Neosをプレイされたことのない方からすると意外かもしれませんが、セッションに関する情報の得やすさで言えばゲーム内が一番やりやすいんです。FPSやユーザーが使っているプラットフォーム、VRかデスクトップかなどなど、様々な情報が取得し放題です。

一方、PUSH型の監視となればそういった監視アプリを用いるか、PUSH Gatewayを用いるか、はたまた…といったところですが、Neosの内部から直接送信しようと思うと、あまり複雑なプロトコルは使えません。せいぜいjsonを投げるくらいが精一杯です。

そこで、ここで触れない様々な兼ね合いもあったのですが、前章でもちょっと話題に出たElasticSearchを使うことにしました。

ユーザーの入退出を記録するeventsと、ホストのFPSを定期的に送信するmetricsとの2種類のドキュメントを作成しました。

それぞれこんな感じです。

elastic-log

このドキュメントを見てみると、ホストのFPSが19とだいぶ低下していることがわかりますね。

ユーザー属性の分析

ElasticSearchを用いて情報を取得したことにより、副次的にユーザー属性の解析が非常に用意になりました。

webUIでポチポチするだけでその時のイベントのUUが分かりますし、VR・デスクトップ比などもすぐにわかります。

これにより、「このイベントはほとんどVRの人しか来ないから、VR前提のゲームを用いたイベントを開催しよう」など、ユーザー属性に応じたコンテンツの創出ができそうですね。

最終的に完成したダッシュボード

elastic-log

Discordと連携しよう

やっぱりVRSNSは誰が何をしているのかが簡単に分かるのがキモですよね。フレンドがプレイ中かどうか、そして自分が参加できるセッションにいるのかどうかを事前に知った上で、今から自分がVRを起動するのか・そうじゃないのかを決定したいです。

しかし、NeosVRはそういった情報を公開するwebがなく、弱くなってしまいます。ただ、フレンドはセキュリティ上の観点から、完全なソーシャルを第三者が提供するのは難しいです。

ので、自分のオンライン情報を公開してもいいよ!という方に専用アカウントとフレンドになっていただき、専用アカウントとフレンドの人のオンライン情報を提供しようと考えました。 また、ケモノHUBや恒常セッションなどの公式セッションに今誰がいるのかも見やすく提供したいですね。

提供の仕方も色々ありますが、やはり心の距離が近い部分に設置することが重要です。例えば、専用webサイトとかをわざわざブックマークして見に行くのはちょっとダルい。 せっかくケモノhubでは公式Discordサーバーを設置し、そこをコミュニティの中心としていますから、そこで情報が見れるべきです。

そこで上記の情報を取得して、Discordに転記するbotを作りました。恒常セッションにいる人をDiscordに記載するプログラムは、前章で用いたelasticにクエリを送る方法で実装されています。

特定キーワードから始まるセッションで、最終更新が10分以内の、セッション名ごとの最新のユーザー一覧を取得するクエリは次のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "_source": true,
  "size": 0,
  "query": {
    "bool": {
      "must": [
        { "range": { "@timestamp": { "gte": "now-10m" } } },
        { "regexp": { "worldName.keyword": "(kemospace|ケモノ広場|ケモノHub).*" } }
      ]
    }
  },
  "aggs": {
    "worlds": {
      "terms": {
        "field": "worldName.keyword"
      },
      "aggs": {
        "latest": {
          "top_hits": {
            "_source": true,
            "size": 1,
            "sort": [
              { "@timestamp": { "order": "desc" } }
            ],
            "script_fields": {
              "@timestamp": {
                "script": "doc[@timestamp].value.toEpochSecond()"
              }
            }
          }
        }
      }
    }
  }
}

そして、これをDiscord上で表示するメッセージに変換するために、jinja2を使います。

jinja2はテンプレートエンジンです。次のようなテンプレートを入力します。

1
2
3
4
5
6
7
8
** 開催中のセッション**
{% for elem in r.aggregations.worlds.buckets%}
{% set result = elem.latest.hits.hits[0] %}
> {{ result._source.worldName }}
http://cloudx.azurewebsites.net/open/session/{{ result._source.worldSessionID }}
参加者一覧: (更新: <t:{{ result.fields["@timestamp"][0] }}:R>)
{% if result._source.users | length > 0 %}{{ result._source.users | join(", ") }}
{% else %}今は誰もいません......{% endif %}\\n\\n{% endfor %}'

この2つを設定するだけでDiscord上でいい感じに情報が表示できます。

elastic-log
(画面の上部はまた別の仕組みで動いているオンラインbot。今回のクエリの結果は画面の下部になる。)

elastic最高~ってなるのは、script_fieldsがあるところですね。 Discordではtタグ記法を使ってUnixTimeを入力することで、見る人のローカル時間に変換して表示したり、 先程のスクショで使われていた[n秒前]といった相対表示が簡単にできる仕様になっています。

elasticsearchに直接クエリしただけだとunix秒で帰ってこないのですが、script_fieldsで時間をunix秒に変換するスクリプトを渡してあげることで、 elasticsearch側で変換を行ってくれます。

これで、Discordbot本体に修正を加えることなく、クエリとテンプレートの組み合わせだけで柔軟にデータを表示できるので、これは良い仕組みですね。

Gammalabでは一部Kubernetesを導入しているので、このDiscordbotもkubernetes下に入れてしまいましょう。 雑にchartを作って、argocdで読み込みます。

elastic-log

また、helmチャートにしていることで、argocd上で簡単にパラメーターを変更できます。

elastic-log

これで、定型文を変更したくなった場合でもすぐさま対応可能ですね。

Githubにてコードを公開しているので、お家にk8sがある方はぜひ使ってみてください。

おわりに

“NeosVR最大ケモノコミュニティ「ケモノhub」を支える技術"ということで好き勝手書かせていただきました。

NeosVRでこれだけ多くのケモノと交流ができるようになったことは非常に嬉しく、毎週遊びに来てくださる皆様、本当にありがとうございます。

今後ともいろいろ試行錯誤しつつもより良いコミュニティ環境を作るお手伝いができればと思っているので、ご要望等あったらぜひお話いただけると嬉しいです!


以上、NeosVR Advent Calendar2022 15日目の記事でした。

NeosVR Advent Calendarにはもう一つの記事、コンポーネントの値を外から操作したい!RefIDオフセットの世界も投稿しているのでぜひ併せてどうぞ。