Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
クエリー処理を Lua で書け、処理結果を PubSub で共有できるキュー付きオンメモリデータベース

クエリー処理を Lua で書け、処理結果を PubSub で共有できるキュー付きオンメモリデータベース

作者:@voluntas
バージョン:0.2.1
URL:https://voluntas.github.io/

まだ個人の興味で作ってるだけです

背景

MOBA とは一つのアリーナ上で複数のプレイヤーが戦うゲームの種類である。

有名どころだと LoLDOTA 2 だったり、最近だとスマホに特化した Vainglory がある。

これらのゲームはアリーナと呼ばれる一つの世界で色々状態を共有する必要がある。これは遊びでもサーバサイドを作ってみたら面白いのでは無いかと思い作ってみることにした。

思想

そもそもサーバ側のデータを更新した際、更新結果を接続している人達に PUSH で配信できたら良いのでは無いかという考えと状態を共有する場合ロックはシンドイのでキューを使ったロックフリーが綺麗なのでは無いだろうかという考えの二つをもとにスタートした。

またロジックを書くのであれば小さい使用の Lua がとても良いことを、別のシステムで実感していた。というか Erlang/OTP + Lua という仕組みはサイコーという妄想に取り憑かれてしまっている。

興味

  • データベースの変更結果をそのアリーナを共有している人に自動で配信されたら効率がよいのではないか
  • データベースはオンメモリで保持しており、それを Lua で書き換えられたら面白いのではないか
  • 通信とデータ部分は Erlang/OTP で、ロジックは Lua で書けると面白いのではないか

概要

  • 状態をロックで取るのはシンドイのでキューを使う
  • 永続化すると遅くなるので全ての処理をオンメモリで行う
  • 1 アリーナでの処理性能は秒間 100 を目指す
  • クライアントから送られてくるクエリーの処理は全て事前登録してある Lua スクリプトで行う

https://dl.dropboxusercontent.com/u/89936/gist/moba1.png

  • データ順序はキューを使っているため保証される
  • 事前にマスターデータを登録し、Lua スクリプトからそのマスターデータを参照できる
  • クエリーの処理結果はアリーナにいる他の人全員に配信することが出来る

https://dl.dropboxusercontent.com/u/89936/gist/moba2.png

  • 同期 API の入り口は全て HTTP/1.1
    • 今後は HTTP/2 を検討
  • 非同期 API の入り口は全て WebSocket
    • 今後は HTTP/2 を検討
  • Erlang/OTP 17.5 で動作

機能

実装済

  • WebSocket による認証付き PubSub 機能
  • Lua によるデータベース更新機能
  • ヒストリー保持機能
  • アリーナ作成/削除機能
  • アリーナ更新機能
  • Lua でエラーが発生した場合のロールバック機能
  • Lua スクリプト登録/更新/削除機能
    • 登録時の Lua スクリプトバリデーション
  • マスターデータ登録/更新/削除機能
  • WebSocket ベースデバッグ機能
    • Lua print
    • Lua error
    • spy

未実装

  • PubSub 時のフィルタリングを Lua で書けるようにする
  • PubSub 時のバリデーションを Lua で書けるようにする
  • WebSocket 関連のテスト
  • Lua モジュールの登録
  • さまざまな統計情報を取得できるようにする

詳細

Lua スクリプトの登録

Lua スクリプトは API により登録する

  • lua_script
    • 普通に Lua コードを文字列で突っ込む
  • lua_script_id
    • 好きに名前を決めて良い
    • アリーナ作成時にこの ID を指定する

Lua モジュールの登録

Lua スクリプトで require するためのモジュールを登録できる

  • lua_script
    • 普通に Lua コードを文字列で突っ込む
  • lua_module_name
    • 好きに名前を決めて良い
    • require "ここの名前になる"

マスターデータの登録

マスターデータは key/map 形式の JSON で登録する

  • function_name
    • この名前が Lua から呼び出すときの名前になる
    • get_action_data だとすると get_action_data(key) となる
      • get_action_data(key) は table を返す
    • get_action_data(key, second_key) という関数も生成され、 map の中の値も一発でとれるようになる
  • master_data
    • key/map 形式の JSON

アリーナの作成

アリーナを作成する

  • arena_id
    • uuid を使う事をお薦めするユニークな ID
  • arena_state
    • アリーナの初期状態を登録する、キャラや HP などなど自由に決めて良い。JSON 形式。
  • lua_script_id
    • クエリーが送られてきたときに処理をするために使用する Lua スクリプトの ID を指定する
  • enabled_arena_histories
    • boolean で設定する、デフォルトは false
    • 全てのアリーナの情報を保持するようになる、リプレイデータとかに使う

戻り値として、アリーナに WebSocket で接続するための AuthTokens が送られてくる。

接続するユーザそれぞれに配布する。

アリーナ状態の更新

この処理が一番多い

  • arena_id
  • query
    • クエリーと言ってもただの JSON を送るだけ
    • この query を登録しておいた Lua スクリプトが解析してデータベースを変更して結果を返してくれる

この処理は同期的に処理される

アリーナのヒストリー取得

全ての処理の履歴を取ることが可能。

  • arena_id

注意

API は全て DynamoDB 風で作られているため、 POST で / という仕様。

詳しくはこちら DynamoDB HTTP API が独特な仕様なので紹介

$ http POST 127.0.0.1:8080/ "x-xyz-target:Xyz_20150401.GetArenaHistories" arena_id=09791210-886b-4534-b9ec-a7217ee42e8d -vvv
POST / HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 52
Content-Type: application/json; charset=utf-8
Host: 127.0.0.1:8080
User-Agent: HTTPie/0.8.0
x-xyz-target: Xyz_20150401.GetArenaHistories

{
    "arena_id": "09791210-886b-4534-b9ec-a7217ee42e8d"
}

HTTP/1.1 200 OK
connection: keep-alive
content-length: 383
content-type: application/json
date: Sun, 19 Apr 2015 08:57:41 GMT
server: Cowboy

{
    "arena_histories": [
        {
            "2015-04-19T08:57:35.448324Z": {
                "query": {
                    "action_id": "1"
                },
                "result": {
                    "action_name": "fireball",
                    "damage": 189.0
                }
            }
        },
        {
            "2015-04-19T08:57:35.955639Z": {
                "query": {
                    "action_id": "1"
                },
                "result": {
                    "action_name": "fireball",
                    "damage": 245.0
                }
            }
        }
    ],
    "arena_id": "09791210-886b-4534-b9ec-a7217ee42e8d",
    "arena_state": {
        "hp": 4566.0
    }
}

デバッグ

この機能はオフにすることができる

恒例の WebSocket デバッグ機能を実装してある。

  • ws://127.0.0.1:5555/debug
    • 全てのアリーナ情報を取得する
  • ws://127.0.0.1:5555/debug/:arena_id
    • 指定したアリーナ ID の情報を取得する

サンプル

  • function_name は "action_master"
  • master_data {"1": {"name": "fireball", "damage_min": 100, "damage_max": 300}} のようなデータ
  • 初期のアリーナ状態は {"hp": 5000} を登録
  • そこに対してクエリー {"action_id": "1"} を投げつけた

登録した Lua スクリプト:

function arena(arena_id, arena_state, query)
    action_id = query["action_id"]

    damage_min = action_master(action_id, "damage_min")
    damage_max = action_master(action_id, "damage_max")

    damage = math.random(damage_min, damage_max)
    print(damage)

    hp = arena_state["hp"]
    arena_state["hp"] = hp - damage

    action_name = action_master(action_id, "name")

    result = {action_name = action_name, damage = damage}
    pubsub = {action_name = action_name}

    return result, pubsub, arena_state
end

spy

spy でどんなクエリーや結果、そして状態がどう変わったかが見ることが出来る。

$ wscat -c ws://127.0.0.1:5555/debug
connected (press CTRL+C to quit)
  < {"datetime":"2015-04-19T08:40:20.186051Z","hostname":"localhost","type":"spy","arena_id":"09791210-886b-4534-b9ec-a7217ee42e8d","query":{"action_id":"1"},"result":{"action_name":"fireball","damage":1.89000000000000000000e+02},"arena_state":{"hp":4.81100000000000000000e+03}}

lua_print

Lua のコードで書いた print は WebSocket 経由で出力される

$ wscat -c ws://127.0.0.1:5555/debug
connected (press CTRL+C to quit)
  < {"arena_id":"09791210-886b-4534-b9ec-a7217ee42e8d","datetime":"2015-04-19T08:38:18.911173Z","type":"lua_print","hostname":"localhost","print":"189 "}

lua_error

Lua でエラーが出た場合の通知も全て WebSocket 経由で出力可能である。エラーはエラーでログは取られる。

PubSub 機能

アリーナ生成時に生成される認証トークンを使う事でアリーナに接続が可能。

  • アリーナに対してメッセージを送るとアリーナに接続している全員に情報が送られる
  • 自分や他の人のデータベース更新情報が配信される
  • ws://:8888/pubsub/:arena_id/:auth_token
    • WebSocket なのは実装が簡単だから
    • 切り替え可能に実装済み
  • Lua スクリプト側で PubSub に投げるかどうかを判断可能

Lua スクリプト:

function arena(arena_id, arena_state, query)
    action_id = query["action_id"]

    damage_min = action_master(action_id, "damage_min")
    damage_max = action_master(action_id, "damage_max")

    damage = math.random(damage_min, damage_max)
    print(damage)

    hp = arena_state["hp"]
    arena_state["hp"] = hp - damage

    action_name = action_master(action_id, "name")

    -- API 送ってきた側に戻す結果
    result = {action_name = action_name, damage = damage}
    -- PubSub でアリーナに繋いでいる人全員に送る結果
    pubsub = {action_name = action_name}

    return result, pubsub, arena_state
end

PubSub 側にはアクション名だけが送られる

負荷試験

Lua ファイルに依存はするが、一通りやりそうな作業を含んだ処理でソコソコ早い

locust を使った簡易試験の結果 ...

https://dl.dropboxusercontent.com/u/89936/gist/locust.png

検証した Lua ファイル:

function arena(arena_id, arena_state, query)

    for i = 1, 300 do
        get_master("123")
        get_master("123", "456")
    end

    key = tostring(math.random(0, 99))

    value = arena_state[key]

    counter = value["counter"]

    for i = 1, 300 do
        value = arena_state[key]
        counter = value["counter"]
        arena_state[key]["counter"] = counter + 1
    end

    return {key = key, counter = counter}, arena_state
end

Lua スクリプト側で master_data を 600 回引いて、状態を 300 回変更した処理で、20 ミリ秒程度で処理を戻すことが可能。

使って見たい人

Twitter で @voluntas までリプライください。docker イメージを用意します。

使うために必要な知識

Erlang/OTP の知識は一切不要です

  • HTTP API
    • JSON
    • ヘッダー作成含める
    • 独自 API なのでちょっと作業が必要
  • Docker
    • イメージを使用するため
  • Lua
    • スクリプト作成のため
  • WebSocket
    • PubSub 機能
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment