こんにちは、habrozhiteli!
今日は、皆さんのほとんどが知らない世界についてお話しますが、同時に多くの優秀な開発エンジニアと多くのお金がそこで回転しています。 はい、奇妙なことに、Minecraftについてです。
Minecraftはサンドボックスゲームであり、マルチプレイヤーサーバーでは、プレイヤーが他の人の建物を破壊すると、グリーフィング(英語のグリーフィング-レッキングから)の深刻な問題があります。 サーバーでは、この問題の処理方法は異なります。 公開ではプラグインを「プライベート」に使用し、残りではすべてが信頼の上に構築されます。
グリフィンを防ぐもう1つの方法は、すべてのグリーファーを禁止することです。 また、それらを計算するには、ブロックのインストールと削除を記録する必要があります。 実際、このようなログシステムを作成するプロセスについてはさらに説明します。
データベース選択
したがって、ここにはデータの配列があり、どこかに保存しておくといいでしょう。 賢い人々は長い間データベースを考え出しています。 個人的に、私のデータベース要件は次のとおりでした。
- クイック挿入
- 最大データ圧縮
- 不要なジェスチャなしでデプロイするためのルート権限なしでJavaからデプロイする機能
最後の項目は、すべてのホスティング会社がルートアクセスを取得したり、パッケージをインストールしたりする機会がないという事実のために登場しました。 さらに、インストール手順を複雑にするのではなく、「Throw and forget」で停止します。
すべての基準を満たすデータベースが見つからなかったため、Javaで独自のミニデータベースを作成することにしました。
ハードディスク容量の最適化
多くの人によると、ゲームの主な問題は、すべての計算が1つのスレッドで発生することです。 これは、サーバー所有者にとって大きな痛みです。 最初にシングルスレッドアーキテクチャを並列化するには、試してみる必要があります。
したがって、ロギング自体を別のストリームに移動する必要がありました。 また、システムがキュー内のイベントを停止しないように、ワーカーのサポートを追加します。 ワーカーの数はカスタマイズ可能です。
その結果、イベント自体がメインティックでインターセプトされ、その後、ワーカー間でタスクを分散するのに忙しいストリームに送信されることが判明しました。 そこで、イベントを登録し、このファイルに添付されているワーカーに渡す必要があるファイルを取得します。 そして、IO操作自体はワーカーで行われます。
ハードディスク容量の最適化
多数のイベントは、ログが世界自体よりも重くなるという事実につながる可能性があります。 これを許可することはできません。したがって、考えます。
最初、ログファイルの行は次のようになりました。
[2001-07-04T12:08:56.235-0700]Player PLACE <blockid> to 128,128,128
2001-07-04T12:08:56.235-0700は、「+」記号と「-」記号によってそれぞれタイムスタンプに短縮したり、配置または削除したりできることが一目でわかります。 さて、「to」を削除しましょう:
[123454678]Player + <blockid> 128,128,128
ニックネームとブロックIDがログで頻繁に繰り返されることに気付くのは難しくありません。 したがって、それらは別のファイルに移動でき、IDのみがログに書き込まれます。
[123454678]1 + 1 128,128,128
その結果、ログの行には数字と1文字しか残っていないという結論に達しました。 区切り文字(スペース)を削除し、文字ではなくバイトとして数値を書き込むと、多くのスペースを節約できます。 実際、これにより、バイトログを使用することになりました。
バイト文字列自体は次のようになります。
お名前 | posX | posY | posZ | タイプアクション | playerid | ブロッキー | タイムスタンプ |
---|---|---|---|---|---|---|---|
フィールド長(バイト) | 4バイト | 4バイト | 4バイト | 1バイト(削除の場合は「0」、挿入の場合は「1」) | 4バイト | 8バイト | 8バイト |
合計で行あたり35バイトが固定されています(行を分割するために1バイト)。
最初は34バイトを残したくなりましたが、レコードが1つのファイルにあるため、固定長の場合、1行を超えるとファイル全体が読み取れなくなります。
ログパス:/{saveasket/{world/dimensionasket/*.bytelog
idに対するブロック名の文字列構造:
お名前 | id | ブロック名 |
---|---|---|
フィールド長(バイト) | 8バイト | シンボルごとに1バイト |
合計:ブロックあたり〜21バイト
ファイル名:blockmap.bytelog
ニックネームからIDへの文字列構造:
お名前 | id | ニックネーム |
---|---|---|
フィールド長(バイト) | 4バイト | シンボルごとに1バイト |
合計:プレーヤーごとに最大10バイト
ファイル名:nickmap.bytelog
メモリ最適化
ブロック名とニックネームをidにすばやくマップするには、両方のファイルの内容をメモリに保持する必要がありました。 Javaはプリミティブ型をHashMapに保存できないため、各整数はメモリで最大50バイトのコストがかかりますが、これはかなりの量です。
troveライブラリは、この問題の解決に役立ちます。
private final TObjectIntHashMap uuidToId = new TObjectIntHashMap();
しかし、各文字は約2バイトかかります。 文字がchar []ではなくbyte []に格納されるASCIString自己記述ファイルを使用することにより、メモリ消費を削減できます。
テスト中
バイトのシリアル化と逆シリアル化のテストに異常はありませんが、マルチスレッドアクセスを必要とするコンポーネントのテストには、GoogleのThread Weaverのフレームワークを使用する必要がありました。 このフレームワークを使用した典型的なテストは次のようになります。
public class NickMapperAsyncTest extends TestCase { private volatile NickMapper nickMapper; public void testNickMapper() { final AnnotatedTestRunner runner = new AnnotatedTestRunner(); runner.runTests(this.getClass(), NickMapper.class); } @ThreadedBefore public void before() throws IOException { nickMapper = new NickMapper(); } @ThreadedMain public void main() { nickMapper.getOrPutUser(new ASCIString("2")); nickMapper.getOrPutUser(new ASCIString("LionZXY")); nickMapper.getOrPutUser(new ASCIString("3")); } @ThreadedSecondary public void secondary() { nickMapper.getOrPutUser(new ASCIString("2")); nickMapper.getOrPutUser(new ASCIString("LionZXY")); nickMapper.getOrPutUser(new ASCIString("3")); } @ThreadedAfter public void after() { final int first = nickMapper.getOrPutUser(new ASCIString("LionZXY")); final int second = nickMapper.getOrPutUser(new ASCIString("2")); final int third = nickMapper.getOrPutUser(new ASCIString("3")); assertEquals(3, nickMapper.size()); assertEquals(Integer.MIN_VALUE + 3, Collections.max(Arrays.asList(first, second, third)).intValue()); } }
フレームワークは両方のスレッドから異なる順序でノックするため、非同期コードで最も厄介なバグをキャッチできます。
おわりに
これまでのところ、ダウンロード数により、このMODとアイデアをさらに発展させる価値があるかどうかは明らかです。 将来のおおよその計画から:
- 古いログと無関係なログを自動的に削除する機能を追加する
- ファイル圧縮を追加する
参照資料
@Config(modid = FastLogBlock.MODID) @Config.LangKey("fastlogblock.config.title") public class LogConfig { @Config.Comment("Enable handling event") public static boolean loggingEnable = true; @Config.Comment("Filepath from minecraft root folder to block log path") public static String logFolderPath = "blocklog"; @Config.Comment("Path to nickname mapper file from logFolderPath") public static String nickToIntFilePath = "nicktoid.bytelog"; @Config.Comment("Path to block mapper file from logFolderPath") public static String blockToLongFilePath = "blocktoid.bytelog"; public static HashConfig HASH_CONFIG = new HashConfig(); @Config.Comment("File splitter type. SINGLE for single-file strategy, BLOCKHASH for file=HASH(BlockPos) strategy") public static FileSplitterEnum fileSplitterType = FileSplitterEnum.BLOCKHASH; @Config.Comment("Utils information for migration") public static int logSchemeVersion = 1; @Config.Comment("Utils information for migration") public static int writeWorkersCount = 4; @Config.Comment("Regular expression for block change event ignore") public static String[] ignoreBlockNamesRegExp = new String[]{"<minecraft:tallgrass:*>"}; @Config.Comment("Permission level for show block log.") public static boolean onlyForOP = true; public static class HashConfig { @Config.Comment("Max logfile count") public final int fileCount = 16; @Config.Comment("Pattern for log filename. %d - file number. Default: part%d.bytelog") public final String fileNamePattern = "part%d.bytelog"; } @Mod.EventBusSubscriber(modid = FastLogBlock.MODID) private static class EventHandler { /** * Inject the new values and save to the config file when the config has been changed from the GUI. * * @param event The event */ @SubscribeEvent public static void onConfigChanged(final ConfigChangedEvent.OnConfigChangedEvent event) { if (event.getModID().equals(FastLogBlock.MODID)) { ConfigManager.sync(FastLogBlock.MODID, Config.Type.INSTANCE); } } } }