UnityのGCはどんな実装になっているのか


こんにちは。Aiming エンジニアの久保田です。

僕の携わっているプロジェクトでは、近頃、Unity製クライアントのパフォーマンスの調査や改善を行っている最中です。
プロファイラを眺めていると、僕達が書くアプリケーションレイヤのコードが目立って遅い、ということは珍しいのですが、代わりにC#世界のスパイクとしてよく顔を出すのが、GC実行時間です。

C#は、タイプセーフでありながら人間にやさしく、getter/setter、async/await、Rx、ロケットなラムダ式、他他他…最新型の言語への影響も多大な、ファッション的にも◎な言語です。しかし、闇雲に全ての機能をタダで……というわけにはいかず、ことパフォーマンス面においては、GCというなかなか高い代償を支払うことになりかねないわけですね。

結論としては、UnityのGCは、皆が期待していたほど高性能ではなく、現状では僕達が書くC#が発生させるGCのインパクトは無視できない大きさになります。

そこで、GCのインパクトの少ないコードベースにしていく必要があるわけですが、コードを書く際、内部の実装をある程度理解していると適切な判断がしやすくなります。

今日は、そんなUnityのGCがどんな実装になっているのか、主に3つの方法で調べた結果を解説してみます。

  • 走らせてみる
  • ILコードや、IL2CPP AOTコンパイル後のC++コードを読んでみる
  • IL2CPP のvmをステップ実行してみる

注:ただし、Unityのロードマップには既にGCの差し替えが予定されています。近い将来、ここで紹介しているGCの性能はさらに改善されることと思われます。

Stop the World

さて、リアルタイム性の要求されるアプリケーションでは、GCが性能上の問題になるとかならないとか、よく噂されています。

なぜGCが目の敵にされるのか。これは、GCのスループットが良いとか悪いとかの問題ではなく、たとえどんなに真面目にひたむきにGCが仕事をしていたとしても、彼の仕事が他人のメモリを管理することである以上、管理下のスレッドを全て停止させるタイミングが必ずやってくる、という事情があるためです。

つまり、GCによるメモリ管理を利用しているアプリケーションは、あるとき全ての動作が停止する、これは俗に「Stop the World 」というかっこいい名前で呼ばれ、恐れられています。

Stop the World が発生している間、Unityのゲームはメインスレッドやその他(マネージドな)バックグラウンドのスレッドも実行できず、時が止まってしまいます。これが長引いたり、初動が遅れた処理が間に合わなければ、ユーザが目にするのはカクカクの描画や操作のひっかかり、というわけです。

一方、GCは歴史が古く、競争も激しい分野なため、このStop the World のインパクトを抑える工夫がたくさん編み出されています。
言語組み込みのGCでは、オブジェクトを寿命ごとに分けて管理する世代別GCや、Stop the Worldを段階的に実行するIncremental GC などが搭載されていることが多いようです。Javaに至っては、かなりバージョンアップを繰り返しており、全体のスループットを落とす代わりにGCを並列に動作させるフェイズを増やすコンカレントGCなども搭載されていました。

さて、僕達のUnityのC#ランタイムはどんなGC実装が使われているでしょうか? 気になります。

フルGCが毎回走る

Unity Blog の2015年の記事、Garbage collector integration によれば、Unityでは、Mono/IL2CPP どちらのランタイムも Boehm-Demers-Weiser というGC が使われている、とあります。

Boehm GC は、オープンソースのGC実装で、特定の言語への組み込みを想定したものではない代わりに、移植性が非常に高いつくりだと紹介されています。
ただし、機能面、性能面では言語組み込みGCには及ばないという評判もみかけ、実際、 オープンソースの.NET実装であるMono のGCは、Boehm GCから独自実装のSGenへと乗り換えを行いました。

僕の現行のプロジェクトで使用しているUnity(5.5.3)では、どうなっているのか、ちょっと確認してみましょう。

適当なUnityプロジェクトを作成し、C# の GC.Collect(); をスクリプトの任意の場所に仕込み、それをiOSビルドします。
Xcodeプロジェクトが生成されるので、プロジェクトを開き、Xcodeのシンボル検索で 「GC_Collect_m」を探してみると、

// System.Void System.GC::Collect()
extern "C" void GC_Collect_m2249328497 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
    {
    int32_t L_0 = GC_get_MaxGeneration_m1986243316(NULL /*static, unused*/, /*hidden argument*/NULL);
    GC_InternalCollect_m479047119(NULL /*static, unused*/, L_0, /*hidden argument*/NULL);
    return;
    }
}

ありました。
今見ているものは、C#標準ライブラリの GC.Collect が実機で実行されるときの姿です。元は System.dll に含まれるC#のバイトコードだったものが、C++コードへ姿を変えています。

iOSビルドはじめとしたUnityのIL2CPP ランタイムでは、C#コンパイラが吐いたバイトコードをそのままVMで実行するわけではなく、このようにあらかじめILコードを力技でC++コードに変換しています。つまり、実機上で走っているのはネイティブC++コードなんですね。

この機能のおかげで、ブラックボックスなはずのVMの動きをそこそこ深くまで読むことができ、Xcodeのデバッガもフルに使えるため、中身の動きを知るためにとても役に立ちます。

まずは ここの GC_Collect_m2249なんちゃらかんちゃらにブレイクポイントを張り、ステップ実行してみましょう。

何度かステップインしていくと、以下の場所へ来ます。

SampleIL2CPP`il2cpp::icalls::mscorlib::System::GC::InternalCollect:
0x101370cb0 <+0>: pushq %rbp
0x101370cb1 <+1>: movq %rsp, %rbp
-> 0x101370cb4 <+4>: popq %rbp
0x101370cb5 <+5>: jmp 0x101369d90 ; il2cpp::gc::GarbageCollector::Collect at BoehmGC.cpp:60

止まっているコードがアセンブリなのは、いよいよIL2CPPのVM部分のコードに到達したためです。これより下のソースはXcodeプロジェクトに含まれていません。

しかし、よく見てみると、親切にもソースコードのファイル名やシンボルが載っていました。

il2cpp::gc::GarbageCollector::Collect at BoehmGC.cpp:60

実は、この辺の libil2cpp のvm部分のソースファイルは、Unityに含まれています。

/Applications/Unity/Unity.app/Contents/il2cpp/libil2cpp/gc/BoehmGC.cpp (Macの場合) を開いてみると……

void
il2cpp::gc::GarbageCollector::Collect(int maxGeneration)
{
    GC_gcollect();
}

僕の書いた GC.Collect は、 @GC_gcollect@ という関数を引数なしで実行するという実装になっていました。
(上記では、GC.Collect から辿っていますが、メモリの圧迫がトリガーになった場合も同じ関数が実行されていた)

引数に渡ってきた世代番号を無視しているのがちょっと気になりますが、今は目を瞑りましょう。
この Gc_gcollect()関数が置かれているのは以下のファイルです。

/Applications/Unity/Unity.app/Contents/il2cpp/external/boehmgc/alloc.c

boehmgc というディレクトリ名ですね。中身をみると、現行のUnityはやはり Boehm GCを使っていることがわかります。

この、Boehm GC 内のGC_gcollect の先をステップインして掘り進んでいくと、以下の関数に辿り着きます。

/*
* Assumes lock is held. We stop the world and mark from all roots.
* If stop_func() ever returns TRUE, we may fail and return FALSE.
* Increment GC_gc_no if we succeed.
*/
STATIC GC_bool GC_stopped_mark(GC_stop_func stop_func)
{
// 省略
STOP_WORLD();
// 省略
START_WORLD();
// 省略
}

これは、GCが管理しているメモリに対してルートから辿れる参照にマークをつけるフェイズですが、STOP_WORLD というかっこいいマクロによって、スレッドを止める処理が差し込まれていることが確認できます。

さらに、この辺りの処理は、 `GC_Increment` が真のとき、ちょっとずつ実行するという挙動になるようなのですが、Xcodeでこの変数の値を追ってみると、

0が入っています。ということは、Incremental GCは有効になっていません。この値、確認した限りでは、プリプロセッサか、GC_enable_incremental() を呼ぶことでしか変更されていないので、少なくとも今見ている環境では 毎回、フルGCが走っているとみてよさそうです。

  • Unity上でのGC処理は、Boehm GCが使われている
  • 世代別やIncremental の機能は特に使われておらず、毎回フルGCが走っている可能性が高い
  • GCの実行中は Stop the World する

注: 上記の結果は、Unity 5.5.3 でのもの。

アプリケーションのC#が引き起こすGCインパクトは思いのほかでかい

どうやら、Unity の GCによるメモリ回収は、一括して行われるようです。この辺りが、GCが一度実行されると一気にスパイクとして現れる原因と推測できます。

さて、GCがボトルネックになっている場合、GCの首をすげかえる、GCをやめる、といった選択肢をとりあえず除外するならば、アプリケーションレイヤでの対策にどれくらいの意味があるのでしょうか。

Unity上で 何もないシーンを作成し、純粋にアプリケーションのC#で classのnew を繰り返した結果を見てみました。

高々1000回程度のclassのnewで、頻繁に8ms弱のGCが発生しています。

この結果の注目すべきところは、下のグラフ、アプリケーションがヒープを要求しなかった場合にはGCスパイクがまったく発生しなかったという点で、これはアプリ層のC#次第で GC発生頻度が大きく変わることを示唆しています。

また、Unity内のUnityEngine.Object をはじめとしたオブジェクト達は、DestroyしてもすぐにはGC対象にはならず、プーリングされるような振舞いをしているため、実際のゲームでもアプリ層のC#の影響はなかなか大きくなり得ます。

struct vs class

次に気になるのは、GCのマーク&スイープが走っているときを除いた、オブジェクトが管理されること自体のオーバヘッドです。

コンパイル後に生成されるコードを見比べることで、 値型と参照型、sturct と class の違いを比較してみたいと思います。

こんな感じのC#コードをつくり、

//(中略)
var c = new FatClass();
var s = new FatStruct();
//(中略)

以下のコマンドで IL2CPPのAOTコンパイラを走らせます。

$ mcs -target:library ./Hoge.cs
$ mono /Applications/Unity/Unity.app/Contents/il2cpp/build/il2cpp.exe \
--convert-to-cpp \
--enable-symbol-loading \
--development-mode \
--assembly='Hoge.dll' \
--generatedcppdir='/Users/rkubota/tmp/Hoge'

すると、 上記のC#コードは、下のようなコードに展開されました。

//(中略)
FatStruct_t680642026 V_0;
memset(&V_0, 0, sizeof(V_0));
(中略)
FatClass_t2447623899 * V_1 = NULL;
{
Initobj (FatStruct_t680642026_il2cpp_TypeInfo_var, (&V_0));
FatClass_t2447623899 * L_0 = (FatClass_t2447623899 *)il2cpp_codegen_object_new(FatClass_t2447623899_il2cpp_TypeInfo_var);
FatClass__ctor_m162577467(L_0, /*hidden argument*/NULL);
V_1 = L_0;
return;
}
(中略)

これが さきほどの new です。
こうしてみると、structのnewとclassのnewは、C#世界でのシンタックスは同じでも、vm内での仕事は大きく違っていることがわかります。

structのnewは、単純なC++世界の値へ展開されており、スタックへ変数を確保するだけ。
対して class は、管理のためのコードが余分に生成されています。

なかでも、il2cpp_codegen_object_new という関数は、GCへメモリを要求し、初期化する処理になっています。この関数を辿っていくと、Boehm GCによって `pthread_mutex_lock` が行われている箇所があります。

classをnewするたびにロックが発生しているとは、これを見るまで意識していませんでした。structと比較するとnewには少なからずオーバーヘッドがあるということは言えそうです。

値渡し vs 参照渡し

続いて、参照をつけかえることによるオーバーヘッドが存在するか調べてみます。

// ごく単純な値渡しのメソッド
static long PassValue(FatStruct v)
{
return v.M00 + v.M10 + 1;
}

// ごく単純な参照渡しのメソッド
static long PassRef(FatClass v)
{
return v.M00 + v.M10 + 1;
}

これが展開されたものが以下です。

// System.Int64 Hoge::PassValue(FatStruct)
extern "C" int64_t Hoge_PassValue_m3398056464 (Il2CppObject * __this /* static, unused */, FatStruct_t680642026 ___v0, const MethodInfo* method)
{
{
int64_t L_0 = (&___v0)->get_M00_0();
int64_t L_1 = (&___v0)->get_M10_10();
return ((int64_t)((int64_t)((int64_t)((int64_t)L_0+(int64_t)L_1))+(int64_t)(((int64_t)((int64_t)1)))));
}
}
// System.Int64 Hoge::PassRef(FatClass)
extern "C" int64_t Hoge_PassRef_m1560133231 (Il2CppObject * __this /* static, unused */, FatClass_t2447623899 * ___v0, const MethodInfo* method)
{
{
FatClass_t2447623899 * L_0 = ___v0;
int64_t L_1 = L_0->get_M00_0();
FatClass_t2447623899 * L_2 = ___v0;
int64_t L_3 = L_2->get_M10_10();
return ((int64_t)((int64_t)((int64_t)((int64_t)L_1+(int64_t)L_3))+(int64_t)(((int64_t)((int64_t)1)))));
}
}

値渡しと参照渡しは、さほど違いが見られませんでした。参照渡しだから何かGC管理のための特別なコードが生成される、ということはないみたいですね。

これはおそらく、Boehm GCが保守的GCという仕組みを採用しているためで、GC内部で使用中のメモリのうち、参照っぽいものをなるべく保守的に自動判定するアルゴリズムが働いてくれるため、アプリケーションがわざわざ参照が増えたり減ったりをGCに知らせる必要がないようです。

これは、関数への参照渡しだけでなく、プロパティへの代入時も同じでした。

Swiftのような参照カウント方式でメモリを管理する環境では、参照が増えたり減ったりする時点でオーバーヘッドが発生しますが、保守的GCの場合、オーバーヘッドは後から一括で支払う、というわけですね。

わかったこと

ここまででわかったことは以下です

  • UnityのGC は Boehm GC
  • Incremental や Generational の機能はなく、毎回フルGCが走っていると思われる
  • classのnew はstructに比べて遅い( ミューテックスのロックを通る)
  • 参照渡しや、参照型のプロパティ代入は特別なオーバーヘッドはない。(Write barieerもない)

一括してやってくるフルGCによるスパイクへの対策としては、まず毎フレーム生まれるようなゴミを生成しないようにすることが重要です。

この辺の具体的な対策の話は数えるとたくさんありそうですが、長くなってきたので、またの機会に譲りたいと思います。
それではまた!!!!!

参考:


入社2週間の新卒エンジニアが紹介するAimingの新人研修


こんにちは。2017年度新卒エンジニアの後藤です。

今回は新卒入社して2週間経過した私の体験等をもとにAimingの新人研修の内容について紹介します。

現在就職活動をされている方や、入社後に新人がどのようなことを研修で学んでいるか気になる方に参考にしていただければと思います。

新人研修の流れについて

Aimingでは入社後1か月かけてオンラインゲーム開発にかかわる人材になるための研修を行っています。最初の数日間に社会人としてのビジネスマナーの研修やワークフローについて学び、その後はゲーム開発に関わる研修を行います。

新人研修の様子

オンラインゲーム開発に関するリテラシー研修

オンラインゲーム開発を行う上場企業の一員として知っておかなければいけない事柄として以下の項目を学びます。

  • オンラインゲームの歴史と仕組み
  • セキュリティポリシー
  • コンプライアンス

大学や専門学校では学生どうしが演習等でゲーム制作をする機会も多いですがセキュリティポリシーやコンプライアンスを意識することは無いのではないでしょうか。

研修を通して、プロジェクトの一員であるとともに企業の一員であることを認識させられます。

職種毎の紹介

デザイナー・プランナー・エンジニア等、現場で各役職に就いている方々を講師に招いて講師が実際にかかわったタイトルなどを例に以下の項目を学びます。

  • 各職種の業務内容
  • Aimingにおける職種毎の心構え
  • 他の職種とのやりとりで留意すべきこと

それぞれの職種に属する新人の意識を高めるのはもちろん、他の職種に属する新人にも業務内容を共有することで互いに連携を取りやすくします。

また、プランナーからエンジニアへ留意してほしいことなども共有し、各職種をまたいだコミュニケーションを円滑に行う方法なども学びます。

その他

新人研修の後半にはスクラム体験をはじめとするグループワークや、自己紹介LT大会を実施します。

自己紹介LTでは自分の経歴について語るもよし、好きなゲームやアニメなどと絡めながら自分を紹介するもよしと、社内の方々にとって新人との接点を知ってもらう機会になっています。

研修を受けての感想

まだ全研修内容の半分を終えた段階ですが、私が大学時代に体験したゲーム開発と比べると規模の大きさも制作する立場も違うので、研修を通して新しい知見を得る日々を送っています。

とはいえまだ「知っている」の段階なので、得られた知見を現場で活かせるように意識しつつ残りの研修にも臨みたいと思います。


Unity5.6.0で追加されたTest RunnerのPlayModeを使ってみた


こんにちは。大阪スタジオ エンジニアの西村です。

Unity 5.6.0からTest RunnerにPlayModeが追加されました。以前からあったEditModeのテストではカバーできなかったフレームをまたぐ非同期処理などのテストが可能になります。ざっくり試してみた結果をまとめてみました。この記事ではUnity 5.6.0f3を使用しています。

PlayModeを有効にする

初期状態ではPlayModeが無効になっているので有効にします。まずWindow – Test RunnerでTest Runnerウィンドウを開きます。

PlayModeタブを選択すると”Enable playmode tests”というボタンがあり、これを押した後でUnityを再起動する必要があります。

これでPlayModeが使えるようになりました。

PlayModeでテストを実行してみる

Create Playmode test with methodsを押すとテンプレートスクリプトが作成されます。

NewPlayModeTest.cs

using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;

public class NewPlayModeTest {

    [Test]
    public void NewPlayModeTestSimplePasses() {
        // Use the Assert class to test conditions.
    }

    // A UnityTest behaves like a coroutine in PlayMode
    // and allows you to yield null to skip a frame in EditMode
    [UnityTest]
    public IEnumerator NewPlayModeTestWithEnumeratorPasses() {
        // Use the Assert class to test conditions.
        // yield to skip a frame
        yield return null;
    }
}

スクリプトがコンパイルされるとPlayModeにテストが追加されます。

とりあえず手を加えずにRun Allしてみると。シーンがテスト用のものに入れ替わりUnityがPlay状態になります。しばらく待つとテストが実行され結果がTest Runnerウィンドウに反映されます。

当然なにもないので成功です。

次は失敗させてみましょう。適当にAssertを追加します。EditModeのテストと同じ感覚で書くことができます。

NewPlayModeTest.cs

using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;

public class NewPlayModeTest {

    [Test]
    public void NewPlayModeTestSimplePasses() {
        // Use the Assert class to test conditions.
        Assert.IsTrue(false, "失敗させてみる");
    }

    // A UnityTest behaves like a coroutine in PlayMode
    // and allows you to yield null to skip a frame in EditMode
    [UnityTest]
    public IEnumerator NewPlayModeTestWithEnumeratorPasses() {
        // Use the Assert class to test conditions.
        // yield to skip a frame
        Assert.IsTrue(false, "失敗させてみる");
        yield return null;
    }
}

はい、失敗しました。

PlayModeらしいテスト

ここまではEditModeでも実行できる内容です。次にEditModeでは出来なかったフレームの更新がある状態になっているか試してみます。

NewPlayModeTest.cs

using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;

public class NewPlayModeTest {

    [Test]
    public void NewPlayModeTestSimplePasses() {
        // Use the Assert class to test conditions.
    }

    // A UnityTest behaves like a coroutine in PlayMode
    // and allows you to yield null to skip a frame in EditMode
    [UnityTest]
    public IEnumerator NewPlayModeTestWithEnumeratorPasses() {
        // Use the Assert class to test conditions.
        // yield to skip a frame
        yield return null;
    }

    [UnityTest]
    public IEnumerator 複数フレームに渡ってテストできる() {
        // Time.timeは同一フレーム中は同じ値を返す
        var startTime = Time.time;
        System.Threading.Thread.Sleep(1000);
        Assert.AreEqual(startTime, Time.time);

        yield return new WaitForSeconds(1f);
 
        // フレームが変わっていればTime.timeの値が変わっているはず
        Assert.AreNotEqual(startTime, Time.time);
        yield return null;
    }
}

複数フレームに渡ってテストできることがわかります。

MonoBehaviourTest

MonoBehaviourTestと言うものがありますが詳しい使い方がわかりませんでした。動作テストに使用したコードは以下の通り。

ExampleBehaviour.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ExampleBehaviour : MonoBehaviour {
    protected int counter = 0;

    // Use this for initialization
    void Start () {
 
    }
 
    // Update is called once per frame
    protected void Update () {
        counter++;
    }
}

ExampleBehaviourTest.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TestTools;

public class ExampleBehaviourTest : ExampleBehaviour, IMonoBehaviourTest {
    public bool IsTestFinished { get; private set; }

    // Update is called once per frame
    new void Update () {
        base.Update();
        Debug.Log(counter);
        if (counter > 10)
        {
            // ここで止めておかないと他のテストの裏でも動き続ける
            gameObject.SetActive(false);
            IsTestFinished = true;
        }
    }
}

NewPlayModeTest.cs

using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;

public class NewPlayModeTest {

    [Test]
    public void NewPlayModeTestSimplePasses() {
        // Use the Assert class to test conditions.
    }

    // A UnityTest behaves like a coroutine in PlayMode
    // and allows you to yield null to skip a frame in EditMode
    [UnityTest]
    public IEnumerator NewPlayModeTestWithEnumeratorPasses() {
        // Use the Assert class to test conditions.
        // yield to skip a frame
        yield return null;
    }

    [UnityTest]
    public IEnumerator 複数フレームに渡ってテストできる() {
        // Time.timeは同一フレーム中は同じ値を返す
        var startTime = Time.time;
        System.Threading.Thread.Sleep(1000);
        Assert.AreEqual(startTime, Time.time);

        yield return new WaitForSeconds(1f);
 
        // フレームが変わっていればTime.timeの値が変わっているはず
        Assert.AreNotEqual(startTime, Time.time);
        yield return null;
    }

    [UnityTest]
    public IEnumerator MonoBehaviourのテスト() {
        yield return new MonoBehaviourTest<ExampleBehaviourTest>();
    }
}

MonoBehaviourTestを利用してMonoBehaviourを実行することも可能です。IMonoBehaviourTestを実装したMonoBehaviourがIsTestFinishedがtrueを返すことでテストが完了します。IsTestFinishedがfalseのまま30秒経過するとタイムアウトしますが、タイムアウトしても完了として扱われてしまいます。

MonoBehaviourTest実行中にAssertを呼び出してしまうと以後のテストが失敗してしまうため、Assertを呼び出すことでテスト単体を失敗させる事ができません。MonoBehaviourTestについてはそもそも使い方を間違えている可能性もあるので深追いしないことにしました。

Playerでテスト

左上のRun AllではUnity Editor上のPlayモードで実行されますが実際のPlayer上でも実行することが出来ます。

Build SettingsのSwitch PlatformでAndroidに切り替えてみると、右上のボタンがRun all in player(Android)に変わります。

実行してみるとビルドが走りAndroid端末でテストが実行されます。実行結果がUnity上のTestRunnerにフィードバックされないのは残念ですが手軽に実行することが出来ます。

まとめ

今回調べた中で分からなかった事

  1. UnityEngine.TestTools.MonoBehaviourTestの扱い方
  2. コマンドラインからPlayModeのテストを実行する方法

MonoBehaviourTestの扱い方が分からなかったため不完全燃焼ではありますが、PlayModeを利用することで今まで出来なかった非同期処理のテストも書くことが出来るようになりました。また、コマンドラインからの実行が出来ないのでCIに組み込む事が出来ませんが、コードレビュー時など手元でサクッと実行できるのは便利です。

今後もPlayModeテストをCIに組み込むことを視野に入れてウォッチを続けていきます。


久保 翔太

GCE Local SSD向けのチューニング


こんにちは、インフラエンジニアの久保です。

今回は、GCE(Google Compute Engine)のLocal SSDのチューニングとベンチマーク結果についてご紹介したいと思います。

Local SSDとは

GCEではPersistent Disk(ネットワーク ディスク)、Local SSD、Cloud Storage Bucket、RAM Diskの4種類のディスクサービスを提供しています。
今回利用したLocal SSD はPersistent Disk(ネットワーク ディスク)と異なり、仮想マシンインスタンスのホストとなるサーバーに搭載されているSSDに直接接続されているため、非常に高速です。
Persistent Diskに比べて低レイテンシかつ高IOPSであるため、非常に高い性能が求められている環境においては非常に有用かと思います。
また、今回の試験では利用していないのですが、RAM Diskはメモリ上にデータを格納するため、Local SSDより高い性能が期待できます。
続きを読む


藤井 章暢

クローバーラボ株式会社の方々と勉強会対決しました


こんにちは。
大阪スタジオ、クライアントエンジニアの藤井です。

去る3月11日(土)にクローバーラボ株式会社の皆さんと勉強会対決を行いました。
私も登壇させて頂きました!
今回はその内容と結果をレポートしていきます。

イベント名

激突! Aiming x CloverLab

ルール

4つの対戦項目(後述)で勝負を行いました。
発表の最後に参加者の皆様にAimingスタッフ・クローバーラボスタッフのどちらの内容が良かったかを投票していただき、その得票数を競うと言う内容でした。
持ち時間は1人15分でした。

対決項目(お題)

・クライアント
・インフラ
・サーバー
・開発環境

各発表内容のまとめを記載していきます。
続きを読む


rastam

敷居を下げる雑な工夫


Hello! エンジニアのラスタムです。

今回は社内コミュニケーションやチームワークを補う様々な工夫を、基盤チームの共同執筆で紹介させていただきます。キーワードは「雑」です。

〇〇という強い気持ち・〇〇という雑な気持ち

ホワイトボードの一角に「〇〇という強い気持ち」「〇〇という雑な気持ち」を書いて貼ることができるスペースがあります。

「〇〇という強い気持ち」

書く内容に制限はなくいつでも自由に書くことができます。
「『やせる!』という強い気持ち」のような個人的な雑談レベルのものや「『まず動かす』という強い気持ち」のような業務に寄ったものなど内容は様々です。

このスペースには次のようなメリットを感じています。

  • 宣言することで自分を追い込むことが出来る
  • 雑談のネタになる
  • 周りの人のことを知る、周りの人に知ってもらう機会になる

雑談レベルのものが多いのですが、そのハードルの低さのおかげで普段から書く習慣に繋がっています。
そしてその習慣があるため、たまに真面目な強い気持ちが生まれたときも気軽に書くことができます。

社内勉強会用のネタストック

チームの勉強会で発表や LT などを毎週何かしらやっているのですが、そのペースでやっていると常にネタ切れ気味になっていきます。
その対策として、ネタになりそうな話や興味があるけど調べてない技術などをストックしておくスペースがあります。(ふりかえりのタイミングでよく発生するので、ふりかえりの TRY の近く)

ネタストック

ネタがないときにそこから好きなネタを拾ったり、あるいは個人的な学習のきっかけにしたりと、勉強会を続けるための一つの工夫となっています。

GitHub 上の zatsu レポジトリー

何かをやる上でとりあえずやらないことにはやる気が出ないので
ハードルを下げるために zatsu repository が存在しています。
この記事自体もハードルを下げるためのそれです。

Issue

おやつ

色々登録されてますね。
おやつリクエストの結果、チョコ棒とここでは紹介しきれませんでしたがポテトフライ(大好き)が供給されて食いすぎて太ったので、とりあえずは今は zatsu に痩せたいと願ってます。

社内ブログ・Wiki

日々の業務で得た知見やノウハウを同僚と共有したいことがたまにあります。
しかしその中には、内容によって社外にまで公開し難いものもあります。

社内 Redmine は前から置いていますが、

  • 主にフォーマルな用途で使われていて、思いつきで書くのを遠慮してしまう
  • Textile 表記で書くという点で使い勝手があまり良くない

そこで Rendezvous という OSS ブログ兼 Wiki を社内サーバにて立ち上げました。大好きな Markdown で書けてうれしい!

Rendezvous

真面目な記事は多いですが、カジュアルなコンテンツも気軽に載せています。
そして個人日報の倉庫として使っている人もいます。

全社で使える CI サーバー(レビュー作業の自動化)

普段から GitHub でコードレビューをしていると、入力ミス・イディオムの統一などの機械的なレビューが面倒になっていきます。

文字通りですが、「機械的にできることは機械に任せよう」ということで、社内で開発した CI(継続的インテグレーション)サーバーを用いて、機械的なレビュー作業の自動化を行っています。

この取り組みによって、「本当に注力すべき箇所にレビューコストを割く」というプロダクトの品質向上の一助となっております。どうせ怒られるなら、人間からより機械だったらカドも立たなくてよいですよね ^^


勉強会・カンファレンスの運営スタッフとしての活動


大阪スタジオのwebエンジニアの富田です。

弊社の新卒採用サイトが公開されました!
私の写真も使われていて、ちょっとドキドキしてしまいます。

リクルートサイトの中でも記載されているのですが、Aimingでは社内・社外のコミュニティ活動が推奨されています。
今回は、大阪スタジオのスタッフが運営で関わっている、関西の勉強会・カンファレンスをご紹介したいと思います。


Game Creators Conferenceは、関西で最も大きなゲーム関連のカンファレンスです。

今年は、2/18に開催され、550枚のチケットが完売する盛況ぶりでした。
このカンファレンスの実行委員にエンジニアマネージャーの槙石が参加させて頂いています。

昨年の開催時は、サーバーエンジニアの山藤が登壇しエントリを書いています。


関西ゲーム勉強会は、過去11回行われている勉強会です。

前回は250名ほどご参加頂けました。
Game Creators Conferenceよりも、開発者の現場に近い部分の勉強会を目指し、参加者同士の交流を目的としています。

最近は規模が拡大し運営が大変になってきていますが、開催する毎に新しい出会いや達成感があります。

昨年、私が運営として参加した時のエントリです。


Game Gatling LTは、15名程が登壇し連続してLTを行う、若干特殊な勉強会です。

参加者は、多くのゲーム開発者の登壇を聞くことができ、登壇者もLTということであまり負担もなく、楽しんで頂いているかと思います。
過去2回開催し、いずれも好評でしたので、第3回も計画中です。

インフラエンジニアの菅野が登壇のエントリを書いています。


3/11(土)には、合同勉強会ということで、クローバーラボ株式会社様と「激突! Aiming x CloverLab」を開催します!
遊び心で対決形式になっています。どうなることか、とても楽しみです!

勉強会・カンファレンスの運営はあまり表に出ることはありませんが、コミュニティのメンバーの方々と協力しながら、開発者同士の交流のお役に立てれば!と思います。


神﨑 拓人

JenkinsとGitHubのWebhook連携の整理


Jenkins と GitHub を連携させる Webhook について

GitHub と Jenkins を連携させる機能のひとつに「Webhook」と呼ばれるものがあります。この Webhook をつかうことで、 GitHub 上で管理しているリポジトリにブランチを push したときや、新たに Pull Request を作成した時などに Jenkins のジョブを走らせるといったことができます。

しかしながら、 Jenkins や Jenkins の複数のプラグインが Webhook の機能を提供しており、どの機能がどのサービスを提供しているのかがわかりづらくなっているのが現状です。そこでこのエントリーでは、それぞれの機能でどのような特徴があり、どういった設定が必要なのかを、実際に設定を行いながら整理したいと思います。

なお使用している Jenkins のバージョンはこの記事執筆時点での Jenkins の最新バージョンの安定バージョン 2.32.3 LTS です(各プラグインのバージョンについては項目ごとに記述)。

続きを読む


Aiming飲み会 #5 を開催しました


こんにちは。エンジニアの栗本です。

先月の1月25日(水)に Aiming飲み会 #5 グラフィックエンジニア新年会 を開催いたしました。
たくさんの方にご参加いただきまして、おかげさまでLTも大変盛り上がり、大盛況のうちに開催することができました。
お忙しい中お越しくださった皆様ありがとうございます。

乾杯の様子

乾杯!

LTの様子(山納)

LTの様子(山納)

ご厚意にあずかりまして、今回の発表資料の一部を公開していただきました。以下のリンクから見ることができます。
弊社エンジニアの山納: Vertex Texture Fetchの活用について
notargs様: GLSL作曲入門
柿崎様: Luaを使ったキャラクタのスクリプティング

次回のAiming飲み会は3月ごろ、デザイナーにフォーカスしたものを予定しています。
今後の情報は Aiming – connpass で公開していきますので、ご興味のある方はぜひフォローをお願いします。
改めまして、登壇者の皆様、参加者の皆様ありがとうございました。


oguma

Aimingインフラチームを支え(てくれてい)る技術と設計(2/3)


この記事を読むのに使用する時間の目安 = 15〜20分

インフラチームの小熊です。
前回(弊社インフラチームが参考にして実践しているITサービスフレームワークや利用しているOSS, Tool類の紹介をさせていただきました。)から大分空いてしまいましたが、今回は、そのツールやOSSのTips をいくつかご紹介したいと思います。

tl;dr

Ansible は、vim-ansible-yaml を入れて、CI に放り込みつつ Serverspec等で TDD / TDI すれば良い感じだと思います。
その他のAnsible tips やVagrant について書きました。
良さそうなところがあったら参考にしてみてください。

Ansible Tips

構成管理ツールは、主に Ansible が扱われています。
(AnsibleでContainer操作も可能ですが、今回Containerの話はしません)

そんなAnsible のTipsや、最近Version 2.2の注意点をいくつか紹介したいと思います。

ツールの紹介

素晴らしいツールの紹介です!
すでに世間でよく出回っている情報ですので、知っている人は読み飛ばしてください。

  • vim-ansible-yaml
    • これを入れておけば、早く書けます
    • はい、私はvimmerです
  • ansible-console
    • module の単体テスト、動作確認が出来ます
      • e.g.) 例えばvagrantの場合は以下でインタラクティブシェルが起動します
        ansible-console -i .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory
        
      • 上記でインタラクティブシェルに入ってから
        help

        もしくは

        ?

        と打つとhelpが参照出来ます

      • このhelpを使うことでも簡単なansible-doc のような役割も満せます
        koguma@all (1)[f:5]$ help datadog_event
        
        Posts events to DataDog service
        Parameters:
        date_happened POSIX timestamp of the event.
        alert_type Type of alert.
        title The event title.
        text The body of the event.
        tags Comma separated list of tags to apply to the event.
        app_key Your DataDog app key.
        priority The priority of the event.
        aggregation_key An arbitrary string to use for aggregation.
        api_key Your DataDog API key.
        validate_certs If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates.
        
  • ansible-doc
    • AnsibleのdocumentがCLI上で開けます
      • 私はvimやatom上で利用しています
  • ansible-playbook-debugger
    • 私は使っていませんが、一応debuggerもあります

version 2の注意点

  • 最新2.2について
    • check mode の対応が変わりました
      • always_run は deprecation となり、無くなる予定だそうです
        [DEPRECATION WARNING]: always_run is deprecated. Use check_mode = no instead..

        と出ます

      • 変わりに check_mode: no を使うようにしましょう
    • group_vars/等の読み込みが変わりました
      • .yml or .yaml の拡張子が必要になりました
      • 全てのfileに拡張子をつけるようにした方が良いようです
    • service module
      • systemd の扱いが変わりました
        • SysVinit scriptのものが起動できなくなっていました
        • 2.2 からsystemd module ができていました
      • sleep option を受け付けなくなりました
    • include の扱いが変わりました
      • include を使う上で変数の扱い方によっては、それら変数を Dynamic に読み込ませるかStaticに読み込ませるか選択が必要になりました
      • 詳細は以下を確認してください

whenで、値をまとめたOR判定をする

when: ansible_env.TARGET_ENV == 'test' or ansible_env.TARGET_ENV == 'development'

と書くのは長いので、下記の様に in を使います。

例 (testとdevelopmentを環境変数 TARGET_ENVで、受け取って判定をする。)

environment:
  - TARGET_ENV: "{{ lookup('env','TARGET_ENV') }}"
tasks:
  - debug: var=ansible_env.TARGET_ENV
    when: ansible_env.TARGET_ENV in [ 'test', 'development' ]"

動作確認 (test, developmentそして、skip判定確認のためのhogeをそれぞれ順番に環境変数TARGET_ENVに入れてdebug)

% for ENV in test development hoge;do TARGET_ENV=${ENV} vagrant provision test | grep -A 4 'TASK \[debug\]';done
TASK [debug] *******************************************************************
task path: /Users/koguma/work/repos/infra-ext/provision/ansible/playbooks/test.yml:20
ok: [test] => {
    "ansible_env.TARGET_ENV": "test"
}
TASK [debug] *******************************************************************
task path: /Users/koguma/work/repos/infra-ext/provision/ansible/playbooks/test.yml:20
ok: [test] => {
    "ansible_env.TARGET_ENV": "development"
}
TASK [debug] *******************************************************************
task path: /Users/koguma/work/repos/infra-ext/provision/ansible/playbooks/test.yml:20
skipping: [test] => {
    "changed": false,
    "skip_reason": "Conditional check failed",

TARGET_ENV=hoge だけ

    "changed": false,
    "skip_reason": "Conditional check failed",

になったので正常に判定ができています。

Testについて

  • ansible-spec というtoolが用意されています
    しかし、Ansible 以外のtoolとも連動させたインフラテストを行っていきたいので Serverspec と Infrataster を利用するように絞りました

    • Serverspec
      • TDD / TDI全般 で利用
    • Infrataster
      • deploy後のBDD で利用

これらについて細かいことは次回投稿する予定です。

必ず、Ansible でServerspecが実行されるようにしている

  • playbookに書いています。
    • post_tasks を使うと毎回 Serverspec でテストしてくれます
      これで 半ば強制的に TDI を実現するようにしています

Ansible playbookからの強制Test実行の注意点としては
事前にsshでアクセスできるようにしておく必要があります。(vagrant sshではなく)

[補足] Vagrant 側で用意されている vagrant ssh-config をplaybook側で実行し、~/.ssh/configに書き込むことも考えました。

しかし vagrant ssh-config に初期化 = ~/.ssh/config をワイプしてくれる機能がないので、各環境で用意したり考慮していくのも面倒だと思えました。 (Jenkinsなどでvm を作ったり消したりしながらテストをバンバン回していく環境など)

そこで、環境構築手順の簡略化も考慮し、Vagrantfile にssh pub auth 登録の処理を書いてしまっています。

playbooksが煩雑になるのを避ける

通常のansibleはplaybooksを直下に横並びで置くため、多くなってくると見づらくなる問題があります。
我々のチームの特定Projectでは、良いか悪いかは別としてplaybooks が煩雑にならないように playbooks dir 配下におけるようにしています。

具体的には以下のような構成です。

├── playbooks
│   ├── addusr_ssh.yml
│   ├── disable_selinux.yml
│   ├── gce-instance-provision_and_init.yml
│   ├── gce-lb-provision.yml
│   ├── instance-provision_and_init.yml
│   ├── monitoring-tool.yml
│   ├── td-agent.yml
│   ├── test.yml
│   ├── vuls.yml
│   └── wordpress.yml
├── plugins
│   └── inventory
├── roles
│   ├── add_ssh_user

# ..<snip>

通常は以下の構成になります。
https://docs.ansible.com/ansible/playbooks_best_practices.html#directory-layout

どうやっているか

  • Custom role path という機能を使っている
    • ansible.cfg に以下の設定をしています
      [defaults]
      roles_path = roles
      
  • group_vars, host_varsを対応させる

    Ansible galaxy でも落とせるようにしてあります。

    ansible-galaxy install aim-oguma.init-for-integrated

1/30追記: ansible-init-for-integrated に一部不備があったので訂正しました。

Ansibleの小ネタ

普段の運用でも、ansibleをよく使っています。
その中でちょっとしたネタ的な使い方を紹介します。

クラスタオペレーションツールとしてリソースを監視する例 (CPU使用率を表示)

[devinfra@test-festa ansible]$ ansible -i inventory all -m shell -a "sar -u ALL | awk 'NR==6,p{if(m<\$4) m=\$4} END{print m}'"
test-festa-slave2 | SUCCESS | rc=0 <<
0.44
test-festa-master | SUCCESS | rc=0 <<
0.39

test-festa-slave1 | SUCCESS | rc=0 <<
0.37

Vagrant

下記に内容を示します
具体的なcodeは纏めて記載します。

For Ansible provision

引数で各playbook 名を指定して、それで動くようにしています。
各種playbookがそのまま同じVagrantfileで利用可能なようにしています。


@vm_name = ARGV[1]

playbook = {
  :disable_selinux => "playbooks/disable_selinux.yml",
  :target => "playbooks/#{@vm_name}.yml"
}

Vagrant の高速化

virtioを使うようにしたり


    vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
    vb.customize ["modifyvm", :id, "--nictype2", "virtio"]

local repositoryを使っています (ちなみにvagrant-cachierは問題起こすので現在利用停止中)

社内にLocal yum repositories を立てていて、社内いるときだけそれを使うように動的な処理を入れています。


Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

# ..

  # 社内の場合はLocal repositoryに向けて高速化する
  if Network.global_ipaddr.inhouse?
    config.vm.provision :shell, :inline => <<-EOT
      sudo sh -c "echo '#{MYCORP_LOCAL_REPO}' > #{MYCORP_LOCAL_REPOFILE}"
    EOT
  else
    config.vm.provision :shell, :inline => <<-EOT
      if [ -f #{MYCORP_LOCAL_REPOFILE} ];then
        rm -f #{MYCORP_LOCAL_REPOFILE}
        sudo yum clean all
        sudo yum repolist
      fi
    EOT
  end

Vagrantfile

Vagrant Cloud を利用しているので vagrant login はしておいてください。

このままでは動きませんが、playbookを用意したり、Vagrantfile に<>で書かれている部分を適宜設定していただければ利用出来るはずです。

最後に

こちらでのご紹介が、誰かのお役に立てれれば幸いです。

今回はここまでです。

次回は CI (Jenkins) や TDI tool (Serverspec), BDD tool (Infrataster)についてや、重要な脆弱性診断が行えるツール vuls について近日公開したいと思います。