Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
時雨堂 BOT サーバー (gumiStudy #20)

時雨堂 BOT サーバー (gumiStudy #20)

作:@voluntas
バージョン:1.0.5
url:http://voluntas.github.io/

2014 年 9 月 26 日に行われた gumistudy#20 の発表資料です

大幅加筆されている 歌舞伎座.tech#5「すごいErlangをゆかいに学ぶ会」 版は こちら

概要

サーバ BOT を動かすためのサーバーです。BOT はシンプルな言語である Lua で書くことが出来ます。

サーバー自体は Erlang/OTP を使用して書かれており、 Lua は Erlang VM 上で動作しています。

で、だれ?

時雨堂という会社から来ました。

Twitter も GitHub も ID は @voluntas です。

Erlang/OTP で何かを書いて飯食べています。

なにはなすの?

メインのサーバとは完全に疎結合になっている、BOT サーバーの概要をお話しします。

なにはなさないの?

ゲーム内部のロジック的な話は一切しません。あくまで BOT サーバーの話をします。

目次

  • BOT サーバーの概要
    • BOT の定義
    • BOT の役割
    • BOT の具体的な役割
    • BOT サーバーに期待される役割
  • BOT サーバーの内部
    • 開発理由
    • Erlang/OTP
    • Lua
    • BOT サーバ用 Lua
    • BOT の動作
    • 基本 HTTP API
    • 応用 HTTP API
    • スケールする Lua
    • WebSocket を使ったデバッグ
    • Lua のプリントデバッグ
    • Lua のエラーアウトプット
    • 1 台での性能
    • 疎結合
  • 今後
    • タイマーへの加算
    • 統計情報の WebSocket 化
    • WebUI による管理
    • 認証 API への対応
    • 強制評価 API
    • 興味がある人いますか?
  • Erlang/OTP + Lua
    • 可能性
    • 現実性
    • 未来性

BOT サーバーの概要

BOT の定義

今回お話しするのはサーバBOT と呼ばれる、運営側の BOT です。クライアント側の BOT ではありません。

BOT の役割

BOT はゲームを楽しくするのが役割です。

実際は人間が判断しているように感じられるためにはリアルタイムに状況を判断してリアルタイムに処理を行う必要があります。

BOT の具体的な役割

株式会社 gumi 様が提供している ドラゴンジェネシス 聖戦の絆傀儡虚神ゴリアテ を動かしています。

https://dl.dropboxusercontent.com/u/89936/unnamed-1.png

画像は こちら から (TODO: 後で公式画像を貰う)

動画みたい人は こちら から実況ですが見れました(非公式)

ゴリアテ(コンピュータ) vs ギルド(人間) で人間は最大 18 名という仕様です。つまり、ギルドの数だけゴリアテを動かす必要があります。

たとえばギルドが 1 万あったら、1 万のゴリアテをリアルタイムで動かす必要があります。

さらにゴリアテは状態を持っているため、1 万の状態を管理する必要があります。

また、リアルタイムゲームは速度が大事になるため、サクサク動く必要があります。

これらをどう実現したのかは BOT サーバの内部の部分でお話しします。

BOT サーバーに期待される役割

実際に使った現場の意見

  • とにかく落ちないこと
  • 期待通りに動作すること
  • 完全に疎結合になること
  • サーバ台数を減らせること
  • Lua スクリプトのデバッグが簡単であること
  • BOT サーバ自体の設置が簡単であること
  • 簡単に使い始められること
  • 統計情報が見れること

BOT サーバーの内部

我々が開発した BOT サーバーでは BOT を動かす箱を Box と定義します。

Box には様々なデータや Lua スクリプトを入れ込めます。また、 Box に対して情報を送信し、Box の持つ情報を更新することが出来ます。

Box はそれぞれ独立しており、独立の状態を保持します。お互いに影響は受けません。

開発理由

もともと Erlang/OTP でサーバーを実装し、プラグインを Lua で開発するというシステムを開発しておりました。それがベースとなったシステムです。

リアルタイムなイベントを体験が出来るためのシステムとして、イベント駆動ではないタイマー駆動のシステムを開発しました。

Erlang/OTP

Erlang/OTP は軽量プロセスを大量に生成することが出来る、並行処理に強い言語です。もともと電話交換機のために作られた DSL なので、同時に処理をすることに特化しています。

BOT サーバーはのコア部分はすべて Erlang/OTP を使って書かれています。

  • ゲームサーバから送られてくる BOT に送るためのゲームの状態を捌くことが出来ます
    • 秒間 1 万前後まで 1 台のサーバで捌くことが可能です
  • 軽量プロセスとは、とても軽いスレッドと考えて問題はありません
  • Erlang/VM ではメモリの許す限り軽量プロセスを起動できます
    • たとえば 32GB のメモリがあれば 1000 万以上のプロセスを起動することが出来るようです
  • 耐障害性にとても優れており、一つのプロセスで例外が起きたとしてもほかに影響を及ぼしません
  • 軽量プロセス単位でガベージコレクションが行われるため、ストップザワールドが発生しません

メリットが多いように見えますが、デメリットもあります

  • 文法が独特
  • プロセス指向言語のため、設計が独特
  • 落ちないサーバを実装するにはある程度の経験が必要
  • Erlang/OTP 開発者を確保するのが難しい

そのため Erlang/OTP のみでの開発はハードルが高いです。

Lua

url:http://ja.wikipedia.org/wiki/Lua
  • 通常の Lua はとても仕様が小さく、覚えるコストも少なくてすみます
  • 別の言語を覚えていれば、すぐに書き始められます

Lua が独特なのはテーブルと呼ばれるリストとマップが混在した仕組みです。ここでは説明は割愛します。

LuaJIT と呼ばれる Lua はかなり高速で動作します。

Lua は調べれば色々出てくると思うのでここでは省略します。

BOT サーバ用 Lua

BOT サーバに組み込むための Lua スクリプトにはいくつか条件があります

まずはサンプルコードです。

counter = 0

function box(uuid, constants, current_status, previous_status)

    if current_status.counter ~= nil then
        counter = counter + current_status.counter
        return "action", {uuid = uuid, counter = counter}

    else
        counter = counter + 1
        print(counter)
        return nil
    end
end
  • ライブラリは Erlang/OTP 側に用意されているスタンダードなもののみ
  • グローバル変数や関数の定義は自由に出来る
  • 呼び出される関数の名前、引数は決まっている
    • box という関数である
    • 引数は 4 つである
      • uuid は box につける ID です
      • constants は定数です
      • current_status はゲームサーバから送られてくる状態です
      • previous_status はゲームサーバから送られてくる一つ前の状態です
  • 戻り値は引数を二つ return で戻す
    • 一つ目は 文字列
      • HTTP でゲームサーバに送信する時の URL の PATH です
    • 二つ目は テーブル
      • HTTP でゲームサーバに送信する JSON です
    • nil を戻せる
      • 何もしません
  • print は後ほど説明します

上のコードは、 current_status の counter が nil じゃなければグローバル変数の counter と足して、uuid と counter を action という PATH に送るという意味です。

逆に current_status が nil だった場合は counter に +1 して終わります。

BOT サーバの Lua コードはグローバル変数の状態を BOT サーバが終了するまで保持し続けます。

つまり counter をインクリメントしておけば、次に Lua を評価するとき前回の値が入っています。

このグローバル変数は Box 単位のため他の Box に影響はしません。

BOT の動作

BOT は投入された Lua コードを評価して出力された JSON を指定したサーバに HTTP で送信します

  1. HTTP API 経由で BOT の Box を作り Lua コードなどを流し込む
  2. BOT の Box は指定された時間から Lua の評価を開始する
  3. BOT は Lua を評価し、送り先 URL と JSON の二つを戻す
  4. BOT は送り先 URL に JSON を送信する

後は 2-4 をループし、最後に指定した時間になると Box は削除されます。

基本 HTTP API

BOT サーバを操作するには HTTP API を使用します。

API はいくつかありますが、主に使う API を紹介していきます。

  • CreateBox
  • UpdateStatus

この二つだけです。

CreateBox

  • BOT の設定を Box に流し込みます
{
    "uuid": "f35644f9-d353-4d84-9bd2-62f91c046550",

    "start": "2014-10-10T10:10:00",
    "stop": "2014-10-10T10:15:00",

    "timer_interval": 10,

    "host": "127.0.0.1",
    "port": 5000,
    "base_path": "/spam",

    "constants": {
    },

    "init_status": {
    },

    "lua_script": "",

}
  • uuid はこの Box の ID です。この uuid を使って Box に UpdateStatus を送信します

UpdateStatus

  • BOT に対して、情報を送りつけます

ここでいう情報とは、たとえば戦場の状態だったり、個々のキャラクターのパラメーターだったりします。

HP の状態などはすべてこの UpdateStatus から取得します。これば box の Lua コードに渡されてきて、Lua コードで判断するという仕組みです。

応用 HTTP API

基本的には使いませんが、何かしら意図的に使いたい場合のための API も用意してあります。

GetBox

Box の状態を取得できます。ただし Lua の状態を取得することは出来ません。

DeleteBox

Box を強制的に削除することが出来ます。ただし Box はタイマーで動作するため意図的に削除する以外は放置して問題ありません。

スケールする Lua

BOT サーバでは Erlang VM 上で動く Lua 実装である luerl を使用しています。

そのため BOT サーバで動く Lua はマルチコアでスケールします。これが最大の魅力です。

すべて Erlang で実装されていることもあり、Lua <-> Erlang/OTP の連携もとてもスムーズに出来ます。テーブルは proplist になって返ってきます。

WebSocket を使ったデバッグ

組み込みスクリプトはデバッグがかなりツライです。特にサーバーに組み込まれている場合はローカルでデバッグ出来る間はいいのですが、実際色々と動作させてテストする場合はデバッグ情報をログから取得するのはツライと判断しました。

そこでいろいろな情報を WS 経由で送信出来るようにする機能を追加しています。

WS を見るには二つのツールをオススメしています。

  • wscat
    • node.js の WS クライアントでターミナル好きの人はこちら
  • Simple WebSocket Client
    • Chrome から簡単に使うことが出来るクライアントです

これらを特定のデバッグで立ち上げているウェブサーバに接続します。そうすることで様々な情報が送られてくるようになります。

Lua のプリントデバッグ

counter = 0

function box(uuid, constants, current_status, previous_status)

    if current_status.counter ~= nil then
        counter = counter + current_status.counter
        return "action", {uuid = uuid, counter = counter}

    else
        counter = counter + 1
        print(counter)
        return nil
    end
end

これでもし counter が 10 だった場合は以下のように表示されます。ここでは wscat を使って例を表示しています。

$ wscat -c ws://127.0.0.1:5555/debug
connected (press CTRL+C to quit)
  < {"datetime":"2014-08-28T16:22:22.651505Z","hostname":"example.com","print":"10 "}

この機能を使うことで組み込んだ Lua の print をターミナル環境で見ることが出来ます。組み込み状態になると print 情報をどこに出力すべきなのか?となったりします。

Lua のエラーアウトプット

組み込んでいる Lua スクリプトがエラーをはくと、プロセス事クラッシュに巻き込まれてしまうため例外を取得して、WS に流すという仕組みを追加しました。

どんなエラーがどんな状況で起きているかがよくわかります。

$ wscat -c ws://127.0.0.1:5555/debug
connected (press CTRL+C to quit)
  < {"datetime":"2014-08-28T16:29:03.721296Z","hostname":"example.com","uuid":"aecebb67-12c2-438c-86cd-7b715070977e","constants":{"matching_id":"02522dbe-c10e-4ce6-b7a5-88f58a30ec81"},"current_status":{"counter":10,"uuid":"aecebb67-12c2-438c-86cd-7b715070977e"},"previous_status":{"spam":"egg"},"error_reason":"{undef_function,nil}"}

統計機能

BOT サーバは統計出力機能を二つもっています。

一つは HTTP API の統計で、Box がゲームサーバに対して投げる HTTP API の統計も含まれます。

{
    "box_bad_response": 0,
    "box_request": 4159,
    "box_request_error": 3381,
    "box_response": 0,
    "create_box": 1000,
    "update_status": 0
}

もう一つは組み込み Lua 関連の統計情報です。Lua の呼ばれた回数、エラー数、nil を戻した数、さらには想定外のエラーが起きた場合の数なども取得できます。

{
    "lua_call_function": 5354,
    "lua_error": 0,
    "lua_nil_return": 0,
    "lua_unexpected_return": 0
}

これらをゲームサーバ側の管理画面などに組み込むことで気軽に BOT サーバの動作状況を監視することが出来ます。

今後は WebSocket によるリアルタイムな統計情報の取得などを実装していく予定です。

1 台での性能

BOT の実装に依存はしますが 4 コア / 8 GB のサーバで 10000 の Box を動作させることが可能です。

今のところポート枯渇を回避する予定が無いため、エフェメラルポートに収まる 10000 Box を最大としています。

ステータスの更新は 1 台で秒間 10000 前後は処理出来ます。

複雑なロジックであれば CPU を使用しますが、マルチコアで処理されるためマシンの性能を上げることで簡単に性能を上げることが可能となります。

疎結合

お気づきかもしれませんが、BOT サーバは投入される Lua に一部制限がある以外、定数や戻される JSON 、さらにはステータスとして投入されてくる JSON には全く制限がありません。

つまりタダの箱を提供するだけです。後は Lua でゴリゴリ BOT を書いておもしろいゲームを作るだけです。

BOT サーバは仕組みを提供するだけです。そのためとても汎用的です。

注意

BOT サーバは作成した Box がそれぞれリクエストをゲームサーバに投げます。そのため大量のリクエストがゲームサーバに送られることになります。

そのためゲームサーバ側で実装するよりも負荷は高くなります。事前にある程度負荷試験を行っておくのが良いでしょう。特にロードバランスする必要があります。

また状態のアップデートは非同期で処理はしていますが、間に MQ などを挟むことで明示的に非同期にする事をオススメします。

今後

タイマーへの加算

戻り値にタイマー加算を用意することで毎回等間隔で評価している Lua コードを Lua 側が自由に設定出来るようにする予定です。

統計情報の WebSocket 化

管理側がリアルタイムに監視できるようにする予定です。

WebUI による管理

JS オンリーで認証が無い管理画面を用意する予定です。

認証 API への対応

ゲームサーバーへの通信を今は HTTP ですが、HTTPS や認証の仕組みを用意する予定です。

ただしインターナルに保持するシステムのため IP 制限だけでも問題はないと考えているため実装の優先順位は低いです。

強制評価 API

強制的に Lua を評価する API を追加予定です。コレを使うことでゲームサーバが無理矢理情報を取得することが出来ます。

同期、非同期の両方を用意する予定です。同期的なのは API の戻り値にアクションと JSON が含まれます。非同期は通常通り HTTP 経由で JSON を投げ込んできます。

興味がある人いますか?

紹介してきたBOT サーバがパッケージとしてすぐ使える状態で販売可能です、ご興味があればご相談ください。

contact at shiguredo.jp

Erlang/OTP + Lua

おまけ

多くの言語で Lua は組込スクリプトとして使われているようです。自分の Lua のイメージは早くて小さいというイメージでした。実際使ってみると覚えることも少なくサクサク書ける感じがしました。

今回 Erlang/OTP と Lua を組み合わせて実際に本番システムで動作させた事を踏まえて、色々考えてみました。

可能性

Erlang/OTP 自体がとてもスケール出来る VM 上に作られているため、設計を間違えなければうまくスケールします。今回使用したのはその Erlang VM 上で動く Lua です、そのため Lua 自体がスケールする仕組みになっています。

一番の強みは気軽に書いた Lua がスケールするという事でしょう。組込言語がスケールしてくれる安心感はかなり魅力があると思います。

さらに Lua スクリプト一つが何かしらバグがあったとしても他には一切影響がありません。これも Erlang/OTP 独特の世界に Lua が包み込まれているからです。

スケール+耐障害性を持った Lua は自分が想定したよりもかなりの可能性を感じました。

現実性

Erlang 自体がネットワークサーバの DSL です、その上で動く何かしらの簡単なロジックを Lua で書くというのは、実際 Lua を書いた側に聞いてみましたがデバッグ環境さえ充実していれば特に苦にはならないようでした。

むしろ最初はデバッグ環境が無かったので相当苦労していたようです。ただプリントとエラーを WebSocket 経由で見れるようにしてからは、ほぼ不満が無くなったとの事です。

スクリプト言語を組み込むときはデバッグ環境の整備がとても重要ですね。

さらに、今回は Lua が呼ばれる回数やエラー回数などをすべて統計情報として取得できるようにしたことから、負荷試験などでどのくらい呼ばれているのかなどが見えるようになりました。

サーバ開発者からもこの統計情報は重宝されました。

道具を用意することで現実的に使えるところまで落とせたと思っています。

未来性

実際 Erlang/OTP 部分を書くのは設計が重要です。組込だけなら良いですが実際はコアとなるサーバー部分が大半になるでしょう。それにうまく Lua を組み込むのは難しいと思います。

ただ、プラグイン部分を Lua にするのはとても未来があると思います。難しい処理をするのでは無く、ちょっとした処理をコアのコードを変更せずに実現出来るようになるのです。

弊社は今後も Erlang/OTP と Lua の組み合わせを積極的に使ったプロダクトを開発していく予定です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment