MySQL
概要
MySQL 拡張機能は、複数の TypeWriter サーバーが同じデータを共有できるようにするブリッジです。有効にすると、ファクト(プレイヤーの進行状況)とアーティファクト(他の拡張機能が作成するデータファイル)は、plugins/Typewriter/ 以下のファイルには保存されなくなります。代わりに、ネットワーク内のすべてのサーバーが読み書きする、MySQL または PostgreSQL のデータベースに保存されます。
また、任意の Redis(または Valkey)接続も担当します。Redis は、即時の変更通知、「このプレイヤーはどのサーバーにいるか」というレジストリ、そしてソーシャル拡張機能(Friend、Party、Guild)と Ghost が使うメッセージを運びます。BTC の他のいくつかの拡張機能は、設定、ゲームモード、サーバー名を、この拡張機能から読み取ります。
この拡張機能にはエントリがありません。ファイルで設定し、任意で BTC の Web パネルからも設定します。
機能
- データベースストレージ — MySQL 8 または PostgreSQL にファクトとアーティファクトを保存します。テーブルは自動作成され、コネクションプールも使います。
- デフォルトで安全 — 共有ストレージが要求されたのに接続できない場合、サーバーは、何も通知せずにローカルファイルで動き続けるのではなく、停止します。
- ゲームモード — 同じデータベース内で、ゲームモード(サバイバル、スカイブロック、ミニゲーム)ごとに、データを分けます。
- サーバー間の同期 — 変更は数秒以内に他のサーバーに反映され、Redis があれば即座に反映されます。
- Redis と Valkey — Pub/Sub メッセージング、ネットワーク全体のプレイヤーのプレゼンス、プロキシ経由のプレイヤーの転送。
- 拡張機能ごとのストレージルール — どの拡張機能がデータベースを使うか、そのデータをプロファイルごとにするか、ゲームモード間で共有するかを選べます。
- 初回起動時の移行 — データベースが空の場合、既存のファクトとアセットのファイルがデータベースにコピーされます。
- サーバーレジストリ — 各サーバーが、30 秒ごとに、状態(TPS、プレイヤー数、メモリ、拡張機能)を報告します。
- 環境変数からのシークレット — どの設定も、環境変数から読み取れます。
- 管理パネルのサポート — BTC の Web パネル向けに、サーバーの状態、ゲームモード、データのエクスポート、テーブルの閲覧に対応します。
インストール
- MySQL 8 以降、または PostgreSQL に、空のデータベースと、そのためのユーザーを作成します。ユーザーには、テーブルを作成および変更する権限が必要です。
- 任意、複数サーバーの場合: すべてのバックエンドから到達できる Redis または Valkey のサーバーを稼働させます。
- MySQL の JAR を
plugins/Typewriter/extensions/に置き、一度再起動します。plugins/Typewriter/mysql/config.ymlファイルが作成されます。 - はじめにの説明に従ってファイルを編集し、再起動します。データベースドライバー、コネクションプール、Redis クライアントは、拡張機能に同梱されています。
はじめに
サーバーを共有データベースに移行します。
- サーバーを停止し、
plugins/Typewriter/mysql/config.ymlを開きます。 mysqlの下で、host、port、database、username、passwordを入力します。PostgreSQL の場合は、database-type: "POSTGRESQL"も追加し、ポートは5432を使います。mysql.game-mode.currentに、このサーバーのゲームモード(たとえばsurvival)を設定します。ファクトを共有する必要があるサーバーは、同じ値にする必要があります。mysql.enabled: trueに設定します。- ネットワークの場合は、Redis を有効にします。
redis.enabled: trueにし、host、port、passwordを設定し、サーバーごとに一意のredis.server-nameを、まとまって動作するすべてのサーバーで同じredis.group-idを設定します。 - サーバーを起動します。ログに「Database storage initialized」と表示されます。データベースがまだ空の場合、既存のファクトとファイルがデータベースにコピーされます(移行を参照)。
- 他のサーバーでも、それぞれの
server-nameで繰り返します。
enabled は意図して選ぶmysql.enabled: false の場合、サーバーはすべてを自身のファイルに保存します。常設の 1 台のサーバーであれば問題ありませんが、起動時にそのディレクトリがテンプレートから再作成される場合、データは次回には存在しないフォルダに書き込まれ、何の警告も出ません。mysql.enabled: true の場合、データベースに接続できないサーバーは、ローカルファイルでプレイヤーにサービスを提供する代わりに、シャットダウンします。
仕組み
拡張機能は、起動すると、他の拡張機能がデータを読み取る前に、TypeWriter のファイルストレージを、データベースストレージに置き換えます。TypeWriter はファクトをメモリ上に保持し、変更を小さなバッチでデータベースに書き込みます。他のサーバーは、データベースをポーリングして、まだ見ていない変更を読み取り、何かが変わったときには、Redis がすぐにそれを通知します。プレイヤーが退出またはキックされると、そのプレイヤーの保留中のファクトは、すぐに書き込まれます。
データベースには、変更されたものだけが保存されます。削除されたファクトは、値が 0 の行(トゥームストーン)として保存されるため、他のサーバーは、古い値を再保存するのではなく、削除を知ることができます。fact-tombstone-retention-days より古いトゥームストーンは、起動時に削除されます。
設定
ファイルは plugins/Typewriter/mysql/config.yml です。ほとんどのセクションは任意で、存在しないキーにはデフォルトが使われます。
データベース
| キー | デフォルト | 意味 |
|---|---|---|
mysql.enabled | false | データベースを使用します。true の場合、データベースに到達できない、または設定が不完全だと、サーバーは停止します。 |
mysql.database-type | MYSQL | MYSQL または POSTGRESQL。生成されるファイルにはないので、必要なら追加してください。 |
mysql.host, mysql.port | localhost, 3306 | データベースのアドレス。PostgreSQL の場合は 5432 を使います。 |
mysql.database | typewriter | データベース名。 |
mysql.username, mysql.password | typewriter、空 | 認証情報。 |
mysql.use-ssl | false | SSL を使用します(PostgreSQL では require)。 |
mysql.allow-public-key-retrieval | true | MySQL のみ。 |
mysql.table-prefix | btc_ | この拡張機能が作成するテーブルのプレフィックス。英数字と _ 以外の文字は _ になり、プレフィックスは小文字になります。 |
mysql.migrate-from-files | true | アセットのテーブルが空の場合、既存のアセットのファイルをデータベースにコピーします。 |
mysql.fact-tombstone-retention-days | 30 | 削除の行が削除されるまでの日数。どのサーバーでも、オフラインのままでいる期間より長くする必要があります。 |
mysql.sync-interval-seconds | 30 | 読み取られますが、このバージョンでは使われません。ポーリング間隔は TypeWriter 自身が設定します。 |
mysql.pool.* | 下記参照 | コネクションプール。 |
mysql.game-mode.current | default | このサーバーのゲームモード。 |
プールのキーは、maximum-pool-size(10)、minimum-idle(2)、connection-timeout(30000 ms)、max-lifetime(1800000 ms)、validation-timeout(5000 ms)、keepalive-time(0、無効)です。
環境変数
値がちょうど ${env:NAME} の場合、ファイルの読み込み時に、環境変数 NAME から読み取られます。
mysql:
password: ${env:BTC_TYPEWRITER_DB_PASSWORD}
数値や真偽値(たとえばポート)は、正しい型で読み取られます。設定されていない、または空の変数があると、変数名を示すメッセージとともに、サーバーが停止します。サポートされるのは値全体だけで、${env:...} を単に含むテキストはサポートされません。
Redis と Valkey
| キー | デフォルト | 意味 |
|---|---|---|
redis.enabled | false | Redis または Valkey に接続します。mysql.enabled が false でも動作します。 |
redis.type | redis | redis または valkey。どちらも同じプロトコルを使います。 |
redis.host, redis.port | localhost, 6379 | サーバーのアドレス。 |
redis.username, redis.password | 空 | 認証情報。ユーザー名が default または空の場合、ユーザー名なしとして扱われます。 |
redis.channel | typewriter:events | すべてのサーバーが共有する Pub/Sub チャンネル。 |
redis.server-name | コードでは survival、生成されるファイルでは server1 | このサーバーの一意の名前。転送に使われる、プロキシのサーバー名でもあります。 |
redis.group-id | 空 | このサーバーのネットワークグループ。英数字、.、_、- で、最大 64 文字。空の場合は、孤立した状態を意味します。 |
group-id は、サーバー間でやり取りするすべての機能の範囲を決めます。ソーシャルメッセージ、ゴースト、ネットワークの名簿は、空でない同じグループ ID を持つサーバー間でのみ伝わります。グループが異なるサーバーは、互いを認識しません。
拡張機能ごとのストレージ
mysql.extensions.<name> の下で、各エントリには 3 つのキーがあります。
enabled— 拡張機能が、自身のデータをデータベースに保存するかどうか。profile-mode—shared(全プロファイルで 1 組のデータ)またはper-profile。network-mode—shared(すべてのゲームモードで同じデータ)またはisolated(ゲームモードごとに 1 組)。
生成されるファイルには、profiles、cosmetics、party、battlepass、translate などのデフォルトが列挙されています。これらの設定は、それに対応する拡張機能(たとえば Profiles)が読み取ります。mysql.enabled が true の場合に常にデータベースへ送られる、TypeWriter 自身のファクトとアーティファクトには、影響しません。
設定は、config.yml のエントリ、次に、現在のゲームモードに対するテーブル <prefix>game_mode_extensions の行、の順に解決されます。起動時に、読み込まれたすべての拡張機能について、そこに無効の状態で行が作られ、管理者が有効にするのを待ちます。キー名は正規化されます。TypeWriter-ProfilesExtension と profiles は同じもので、friends は party のエイリアスです。
profiles セクション
同じファイルの profiles セクションは、Profiles 拡張機能を設定します。enabled、max-profiles、default-profile-name、save-statistics、save-inventory、save-ender-chest、require-permission、auto-create-default、force-profile-selection、switch-cooldown-seconds、coop-supported、creation-menu-entry-id です。その意味は、そのページを参照してください。
保存されるもの
| テーブル | 内容 |
|---|---|
<prefix>facts | すべてのプレイヤーとグループのファクト: エントリ ID、グループ ID、ゲームモード、値、最終更新。 |
<prefix>assets | アーティファクトとその他のアセット(テキストまたはバイナリ)。パスとゲームモードごと。 |
<prefix>game_modes | ネットワークに知られているゲームモード。 |
<prefix>game_mode_extensions | ゲームモードと拡張機能ごとのストレージルール。 |
<prefix>server_extensions, <prefix>extension_entry_filters | サーバーごと、エントリごとの同期設定。 |
servers | 稼働中のサーバーのレジストリ。名前にプレフィックスはありません。 |
BTC の他の拡張機能は、同じデータベースに独自のテーブルを作成します。たとえば、Guild のバンクには btc_guild_bank、Profiles には typewriter_profiles です。テーブル名とカラム名は自動的に作成され、足りないカラムは起動時に追加されます。
すべてのファクトとアセットの行は、ゲームモードを持ちます。サーバーは、自身の mysql.game-mode.current(と global と記された行)の行だけを読み書きするため、同じデータベース内でも、2 つのゲームモードがファクトやアーティファクトを共有することはありません。
サーバー間の同期
データ。 Redis がなくても、サーバーは同じ状態に収束します。各サーバーが、他のサーバーによる変更をデータベースからポーリングで取得するためです。Redis があれば、ファクトを書き込んだサーバーが短い通知を発行し、他のサーバーは即座に同期します。ファクトのタイムスタンプは、各サーバーのローカル時間で保存されるため、すべてのサーバーを同じタイムゾーンで運用してください。
ソーシャルメッセージ。 Friend、Party、Guild は、変更を、Redis チャンネル上の JSON メッセージとして発行します。サーバーは、自身のメッセージを無視します。メッセージが送られるのは、Redis が接続されていて、redis.group-id が空でない場合だけです。
プレゼンス。 各サーバーは、オンラインのすべてのプレイヤーについて、Redis に presence:<player name> を書き込みます。有効期限は 120 秒で、50 秒ごとに更新され、プレイヤーが退出すると削除されます。「フレンドのもとへテレポート」などの機能は、これを読み取って、プレイヤーのサーバーを見つけます。
転送。 プレイヤーを別のサーバーに移動するために、拡張機能は、標準の BungeeCord プラグインチャンネルを通じてプロキシに依頼し、クラスター全体の移動には、BTC のプロキシが理解する btc:bridge チャンネルを使います。各サーバーの redis.server-name は、プロキシが認識しているサーバー名と一致している必要があります。
再接続。 Redis が切断された場合、発行側は再接続を試み、購読側は、遅延を増やしながら(5 秒から 5 分まで)最大 10 回再試行します。
移行
mysql.enabled: true で、あるゲームモードを初めて起動したとき:
- ファクトのテーブルに、このゲームモードのファクトがない場合、現在メモリ上にあるファクト(ローカルファイルから)が、データベースにコピーされます。そうでなければ、ファクトはデータベースから読み込まれ、ローカルのものは無視されます。
mysql.migrate-from-filesがtrueで、このゲームモードのアセットのテーブルが空の場合、plugins/Typewriter/assets/のファイルが、そこにコピーされます。
最初の起動の前に、plugins/Typewriter/ のバックアップを取ってください。ファイルに戻すには、mysql.enabled: false に設定します。データベースの内容は、コピーして戻されません。
管理パネル
拡張機能は、BTC の Web パネルからの管理リクエストに応答します。サーバーとその状態(TPS、プレイヤー数、メモリ、インストール済みの拡張機能、Redis のレイテンシ、プールの統計)の一覧表示、ゲームモードの作成と削除、ゲームモードごとの拡張機能の有効化、ゲームモードのデータのエクスポート、インポート、移行、ストレージのテーブルの閲覧と編集、最近のデータベースログの読み取りです。各リクエストは、パネルを使う人の権限を確認し、レート制限されます。
権限
これらの権限は、Web パネルのユーザーに対して確認されます。デフォルトでは登録されていないため、管理者に付与してください。
| 権限 | 許可される内容 |
|---|---|
typewriter.admin.server.list | サーバー、ゲームモード、ストレージ、テーブル、検出された拡張機能の閲覧。 |
typewriter.admin.server.health | ヘルスチェックとデータベースログの閲覧。 |
typewriter.admin.server.deregister | レジストリからのサーバーの削除。 |
typewriter.admin.gamemode.create | ゲームモードの作成と削除。 |
typewriter.admin.gamemode.configure | 拡張機能の設定、エントリのフィルター、ストレージの上書きの変更、データのインポート、エクスポート、移行、テーブルの行の編集。 |
運用
- リロード。 Redis チャンネルに
config_updateを発行すると、すべてのサーバーがmysql/config.ymlを再読み込みし、新しい拡張機能を登録します。通常の運用では、ファイルは起動時に一度だけ読み取られます。 - サーバーレジストリ。
serversテーブルは、30 秒ごとに更新されます。60 秒より古いエントリは、オフラインとして扱われ、起動時に削除され、JVM が停止すると、サーバーは自身を削除します。 - デッドロック。 MySQL がデッドロックを報告した場合、ファクトのバッチは、最大 3 回再試行されます。
- バックアップ。
<prefix>factsと<prefix>assetsのテーブルをバックアップしてください。削除は値が0の行なので、トゥームストーンの保持期間より古いバックアップを復元すると、削除されたファクトが復活することがあります。 - 更新。 すべてのサーバーの JAR を、同じ再起動の時間帯に更新してください。スキーマは、起動時にその場でアップグレードされます。
トラブルシューティング
サーバーが起動時に「Refusing to serve players on local file storage.」で停止する。 mysql.enabled が true で、データベースに到達できないか、値が不完全です。その前の行を読んでください。SEVERE のメッセージに、user@host:port/database とエラーが示されています。認証情報、ネットワーク、データベースを修正するか、このサーバーがファイルを使うべき場合は、mysql.enabled: false に設定してください。
「Configuration incomplete: environment variable(s) X are referenced ... but are not set.」と出る。 ${env:X} の値に対して、サーバープロセスに、対応する空でない環境変数がありません。シェルだけでなく、サービス定義で設定してください。
2 つのサーバー間でファクトが共有されない。 どちらも mysql.enabled: true で、同じデータベースと、同じ mysql.game-mode.current であることを確認してください。ゲームモードが異なると、別々のファクトが保存されます。Redis がない場合、変更が現れるまで、ポーリング間隔だけかかることがあります。
フレンド、パーティ、ギルド、ゴーストがサーバーをまたがない。 Redis が有効で接続されていて、redis.group-id がすべてのサーバーで同じ空でない値に設定され、redis.server-name がサーバーごとに一意である必要があります。グループ ID が空の場合、ソーシャルメッセージは送られません。
ログに「Failed to connect to Redis」と表示される。 ホスト、ポート、ユーザー名、パスワードを確認してください。Redis の requirepass のパスワードには、redis.password が必要です。拡張機能は、Redis がなくても、ポーリングだけで動作し続けます。
拡張機能が、データをデータベースに保存しない。 起動時に検出された拡張機能は、無効の状態で登録されます。config.yml の mysql.extensions の下、または管理パネルで拡張機能を有効にしてから、再起動してください。
削除されたプレイヤーや、削除されたファクトが復活する。 あるサーバーが、fact-tombstone-retention-days より長くオフラインだったため、古いデータを再保存しました。保持期間を延ばし、サーバーの古いコピーを復元しないようにしてください。
「HikariPool ... connection is not available」やタイムアウトが出る。 プールが枯渇しています。mysql.pool.maximum-pool-size(とデータベース自身の接続数の上限)を引き上げ、データベースサーバーでの遅いクエリを確認してください。
タイムスタンプがおかしい、または変更が見逃される。 データベースを共有しているサーバーが、異なるタイムゾーンで動作しています。揃えてください。