レガシーなwebページをAngularで書き換えてみて良かったこと

この記事はDeNA Advent Calendar 2016 3日目の記事です。

はじめまして。DeNAでエンジニアをしています。平野です。 かつては大規模プラットフォーム決済システムのサーバーサイドを、 今は"マンガボックス"のサーバー/クライアントサイドを担当しています。

この記事では、 マンガボックスのアプリ内webviewで展開している機能をブラウザにも展開するために、 クライアントサイドをAngularで、サーバーサイドもそれに即すように作り変えた時の基本的な構成の紹介とちょっとしたTipsを記載しています。

自分と同じAngular初学者の方や、既存のサービスをAngularで書き換えるとなんかいいことあるんか?的な方の参考になれば幸いです。

背景

結論に行く前に軽く背景の説明です。レガシーなwebページという若干煽り気味な名前ですが、自サービスの構成のことを指しました。 マンガボックスのiOS/Androidアプリ上で提供している機能のうち、一部はアプリ内webviewで動いており、 とあるURLを叩けばHTMLが返ってくる、たまにAJAXで動的にHTMLを組み換え最終的にwebviewがレンダリングするといういわば普通のwebサイトな構成になっています。

今回その部分をアプリ外のSafariやChromeなどのブラウザでも使えるようにしたいということで、 既存のwebviewが叩いてるURLをブラウザからも叩けるように改修するという方針ではなく、このご時世だしクライアント側にMVCの概念を持たせる構成で再実装してみようというワリとTRY的な位置づけでプロジェクトがはじまりました。

結果的に良くなったこと

  • サーバーとの通信量が減ったことで体感速度が向上した
    • 旧構成では表示する分のHTMLを常にレスポンスとして要していた
    • angularでは表示に必要なデータ部分のみをレスポンスとして受け取り、事前に取得したHTMLにバインドする
  • アプリのwebview部分のネイティブ化への足がかりができた
    • アプリ側が今回用意したAPIを叩くように改修をすれば理屈上ネイティブ化が可能

以上の理由を今回のAngularの構成を紹介しつつ以下で説明していきます。

システムの構成

まず旧構成です。 以下の概念図のように、1画面1エンドポイントになっており、サーバー内で必要な情報をかき集めて最終的にHTMLを返しています。 not_angular.png

angularではこれが以下のようになります。 angular.png まず / へのアクセス時にangularのコードが返ってきますので、これをクライアント側が実行することになります。 その後 /#/top/ にアクセスすると対応したangularのcontrollerが必要な情報毎にサーバーに対してリクエストを送ります。 そして返ってきたJSONをすでに取得済みのHTMLにバインドしてレンダリングを行います。

またこのような構成にすることで、旧構成ではHTMLが返ってくるために困難だったサーバー側のend to endのテストもイメージしやすくなるはずです。

逆に改善したいこと

  • リリース時の考慮ポイントが増える
    • クライアント側またはサーバー側の挙動が変わるリリースの場合、クライアント側の状態を保証できないため
    • 後方互換性を意識した実装が必要
  • seo対策
    • クローラーがjavascriptを認識できない
    • 仮のHTMLを返す等の対応が必要

Tips

ブラウザバックの検知

静的なデータを扱う場合はブラウザバックで戻ってきたときに再度サーバーにデータを問い合わせる事は不要です。 AngularではAPIのレスポンスをキャッシュすることができるのですが、通常の設定では任意のAPIのキャッシュを常にするorしないのどちらかしか設定することはできません。 なのでブラウザバック等で戻ってきた時とそれ以外の時を以下のコードで判別して、その都度APIのキャッシュをするしないを設定しています。


myApp.run(function($rootScope, $state, $location, $document){
    $rootScope.$on('$locationChangeStart', function() {
        $rootScope.use_cache = ($rootScope.newLocation == $location.path()) ? 0 : 1;
    });

    $rootScope.$watch(function () { return $location.path(); }, function (newLocation, oldLocation) {
        $rootScope.oldLocation = oldLocation;
        $rootScope.newLocation = newLocation;
    });
});

おわりに

Angular化における良かった事悩ましい事をいくつか紹介させていただきました。 クライアント側にMVCモデルの概念を持ち込むことで実装上の役割が明確になり、非常にスッキリとコードを書けたことが印象的でした。 Javascript界隈は次から次へとトレンドが移り変わるイメージがありますが、このタイミングに乗れてよかったと思いました。と同時にこれからはもっと早めに乗っかろうとも思いました。

明日は4日目、key-amb さんです。 お楽しみに!

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

UnityのNative Pluginを作るためのもろもろ

この記事はDeNA Advent Calendar 2016 2日目の記事です。

初めまして。DeNAでUnity基盤開発をしている、大竹(a.k.a. trapezoid)と申します。UniRxおじさんも兼業していたりします。 今回は、UnityでのNative Plugin開発の流れや勘所について紹介していこうと思います。

まえおき

Unityは非常に多彩なプラットフォームにほぼワンソースで対応出来る優秀なマルチプラットフォームゲームエンジンです。 Unityでゲームを作っていく場合、基本的にはC#でそのコードを記述していくわけですが、プラットフォーム固有の込み入った処理を書きたい場合や、高い性能が求められるような場合、Native Pluginとしてその処理を記述して、Unity本体とインテグレーションするような手法を取る必要があります。

DeNAでは、オーディオエンジンや同期通信用の内製ミドルウェアのSDKなど、ゲームエンジンに関わらず使われる独立したミドルウェアは、原則的にネイティブ開発したものをUnityにインテグレーションする手法をとっているものが多いため、積極的にNativePluginを開発/利用していっています。

Native Pluginをつくる

Native Pluginの形態

モバイルゲームにおいてであれば、やはりまずサポートされるべきターゲットはiOS/Androidとなります。 プラットフォームとC#実行エンジンの選択に従って、Native Pluginをどのようにゲーム本体とリンクするべきかは、以下のように変わります。

OS実行エンジンリンク形態
iOSMonoStatic Library
iOSIL2CPPStatic Library
AndroidMonoShared Library, jar(JNI経由の呼び出し)
AndroidIL2CPPStatic Library, Shared Library, jar(JNI経由の呼び出し)

iOSは基本的にStatic Libraryを作ってリンクします。これはiOS上では外部ライブラリの利用はスタティックリンクしか規約上許可されていないため、このような制約がかかっています。 Androidの場合は、Shared Libraryによるリンクと、JNI経由でJVM上のコードを呼び出す事が可能です。 IL2CPPを利用する場合は、iOSと同様にStatic Libraryを利用することができます。 (訂正:2016/12/02) Unityフォーラムの投稿によれば、Android IL2CPPの場合、Static Libraryはサポートされてないようです。

Native Pluginとしてビルドする

上記の前提にのっとって、まずはNative側の実装を作って、Unityから繋ぎこむべき先のライブラリをビルドします。 Androidの場合はAndroid NDKを、iOSの場合はXcodeを使っていくのが手軽です。cmakeを使うことで一元化することも出来ますが、これはこれで話が長くなるので今回は割愛します。

Native PluginとC#をつなぎこむ

https://docs.unity3d.com/ja/current/Manual/NativePlugins.html 基本的なことや具体的なコードは上記のUnity公式ドキュメントに記載されていますので割愛しますが、UnityのNative Pluginとのインターフェースは、.NETのP/Invokeの仕組みにのっとって行われます。 P/Invokeではマーシャリング手法を引数や返り値, 型毎に細かく指定する事が出来るので、コピー頻度やメモリアロケーション頻度を下げられるように気を使いながら、C#上からP/Invoke宣言を記述していきます。 StructLayout等を利用することも可能ですが、特にiOSの場合64bitアーキテクチャと32bitアーキテクチャに両対応する必要があるため、アライメント境界には強く気を使う必要があります。

EditorでもNative Pluginを使う

Unityを使った開発の強みは、Unity Editorによって動作確認が出来ることによる、開発サイクルの加速にあります。 そのために、Native Pluginによってゲーム中の機能を実現する場合、Unity Editorの内部でも、可能な範囲で動作確認が可能であるべきです。 つまり、Android/iOSに加えて、WindowsやmacOSに対してもマルチプラットフォームサポートを行う事が、実質的にはスタートラインになります。

UnityではmacOSはLoadable Bundle, WindowsはDLLの形で、それぞれネイティブプラグインを利用可能です。 Windowsは単純にDLLを作るだけなので一旦省くとして、Loadable Bundleは馴染みがない方も多いとは思います。Loadable Bundleを作るには、まず以下のようなInfo.plistを任意のパスに保存しましょう。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>CFBundleDevelopmentRegion</key>
        <string>en</string>
        <key>CFBundleExecutable</key>
        <string>$(EXECUTABLE_NAME)</string>
        <key>CFBundleIdentifier</key>
        <string>CompanyName.$(PRODUCT_NAME:rfc1034identifier)</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleName</key>
        <string>$(PRODUCT_NAME)</string>
        <key>CFBundlePackageType</key>
        <string>BNDL</string>
        <key>CFBundleShortVersionString</key>
        <string>1.0</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
        <string>1</string>
        <key>NSHumanReadableCopyright</key>
        <string>Copyright © 2016年 Your Name. All rights reserved.</string>
        <key>NSPrincipalClass</key>
        <string></string>
</dict>
</plist>

次に、cmakeでコンパイル対象や必要なライブラリを指定した上で、 先程のInfo.plistを相対パスで指定する形で、ターゲットに対して(BuildTargetNameは適宜読み替えてください)cmake上の以下のようなプロパティ設定を追加します。


    set_target_properties(BuildTargetName PROPERTIES
        BUNDLE TRUE
        MACOSX_BUNDLE TRUE
        BUNDLE_EXTENSION bundle
        MACOSX_BUNDLE_INFO_PLIST ./Path/To/LoadableBundleResource/Info.plist
        )

こうした上で


cmake . -G"Xcode"

でcmakeを走らせれば、Loadable Bundleを生成できるxcodeprojを吐き出せます。(もちろん、手でXcodeからxcodeprojを生成してもかまいません) Windows/OSX共に、利用するUnity Editorのアーキテクチャに合わせて適宜64bitにするのを忘れないようにしましょう。

Native Pluginを配置する

ビルドした各Platform向けのNative PluginをUnityプロジェクト上のAssets/Plugins以下に配置します。 Inspectorから、それぞれのバイナリがどのプラットフォーム向けのものなのかを指定しておけば、完了です。

プラクティス

複雑な構造を手軽にマーシャリングする

非Bittableな値を多く転送するのであれば、どのみちマーシャリングコストはかかるので、マルチプラットフォームで高速なIDL定義型のシリアライゼーションライブラリを利用していくのも手です。代表例としてはProtocolBuffersやFlatbuffersが上がります。 この手のIDL定義型のシリアライゼーションライブラリは、IDLから各言語のパーサコードを生成してアクセスする形式のため、マーシャリングに関わる実装コストをざっくりと押しつけつつ、比較的安全にManaged-Native間のやりとりを行う事ができます。 連続的なメモリアドレスにパラメータを積み込めるので、バッチ的に複数の処理を一気に転送する、というパターンを行いたい場合にも有効です。

Native Pluginでの諸注意

Native Pluginの利用に関しては、いくつか注意するべき点がありますが、今回は

  • Native側からManagedのメソッドをコールバックとして呼び出させる場合、必ずUnityのメインスレッド内から呼び出すようにする
  • Editor上でのPreview再生の停止をしても、Native Pluginはアンロードされない

この二点について説明します。

コールバックは必ずUnityのメインループ内から呼び出す

Unityは多くの処理がメインスレッド、メインループ(Update, LateUpdateなど、Unityがエントリポイントとしてメインスレッド上から呼んでくれるメソッドから連なるコールスタック)から行われることが前提にされています。 uGUIなどは、CanvasRendererとの都合でこの外側からメッシュが編集される処理が走った場合、容易にクラッシュに至ります。 また、Editor上でNativePluginを使う場合には、別スレッドからコールバックが一度でも呼ばれてしまうと、以後MonoDevelop/VisualStudioからのデバッガーアタッチを行ったときにUnityがフリーズしてしまいます。(UnityのMonoのDebuggerのバグに起因しているようです) これらの事情から、コールバックは必ずUnityのメインループの内部から発火されるようにするのがベターです

Editor上でのPreview再生の停止をしても、Native Pluginはアンロードされない

これも非常に厄介なポイントなのですが、Editor上のNative Pluginは、一度ロードされるとPreview再生/停止をするだけでは初期化されません。 再生後に最初の呼び出しで初期化し、再生終了時には終端処理を必ず行うようにする必要があります。さらに、再生中にC#コードを編集してリコンパイルとアセンブリリロードが走った場合、終端処理がきちんと呼び出される保証もなくなるので、初期化処理は冪等性を保っておき、何度呼び出しても支障はない状態にしておく必要があります。

さいごに

長くなりましたが、UnityのNative Pluginを作る方法やその注意点について紹介していきました。

明日は hirashunshun さんです。

  • 2016/12/02 Android IL2CPPでのサポートされるリンク手法について追記と修正をしました。
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

実務で使っているUnityの便利モジュール・実装パターン3選

DeNA Engineer Advent Calendar 2016 1日目の記事です。

こんにちは、ゲームエンジニアの宇塚です。
この記事では、自分がUnityでゲーム作りをする中で実装し実務で使っている便利モジュール・実装パターンとして以下3つを紹介します。

  • ユーザー入力を安全に防ぎ、デバッグしやすくする InputGuardManager
  • ユーザー選択を簡単に取得する汎用確認ダイアログ呼び出し DialogHelper
  • 複数の通信処理をまとめる処理のパターン

Unityと一部UniRxを前提としていますが、他言語や環境でも活用できると思います!

ユーザー入力を安全に防ぎ、デバッグしやすくする InputGuardManager

通信や大事な演出を待つために、ユーザーの入力を受け付けなくしたいことがあります。
ユーザーの入力を防ぐ処理に不具合があり入力を防いだ状態が解除されなくなったとしたら深刻な問題ですので、確実に解除される呼び出し方のみを提供するモジュール InputGuardManager を作りました。

InputGuardManager 本体

ユーザーの入力を防ぐ=ガードするメソッドが IDisposable を返し、それを Dispose することでガードが解除されるようになっています。



// ユーザーの入力を防ぐクラス。説明のために入力の種類を絞り簡略化しています。シングルトンです。
public class InputGuardManager : Singleton
{
    // モーダル(ダイアログなど)を表示するcanvas用のgraphicraycaster
    private GraphicRaycaster modalGraphicRaycaster;

    // 通常の2DUIを表示するCanvas用のGraphicRaycaster
    private GraphicRaycaster overlayGraphicRaycaster;

    // Androidのバックキーイベントを扱うクラス
    private BackKeyHandler backKeyHandler;

    // Modalのガードキーに対する参照カウントの辞書。
    private Dictionary modalGuardDict = new Dictionary();

    // Overlayのガードキーに対する参照カウントの辞書。
    private Dictionary overlayGuardDict = new Dictionary();

    // BackKeyのガードキーに対する参照カウントの辞書。
    private Dictionary backKeyGuardDict = new Dictionary();

    #region Public methods

    public void Initialize()
    {
        modalGraphicRaycaster = __取得処理__;
        overlayGraphicRaycaster = __取得処理__;
        backKeyHandler = __取得処理__;
    }

    // Modalのガードの有無。
    public bool IsModalGuarded {
        get { return modalGuardDict.Count() > 0; }
    }

    // Overlayのガードの有無。
    public bool IsOverlayGuarded {
        get { return overlayGuardDict.Count() > 0; }
    }

    // BackKeyのガードの有無。
    public bool IsBackKeyGuarded {
        get { return backKeyGuardDict.Count() > 0; }
    }

    // 指定したキーで、Modal・Overlay・BackKeyをガードする。
    // 返り値のIDisposableをDisposeしたときにガード解除する。
    public IDisposable Guard(string key)
    {
        GuardInternal(key);
        return Disposable.Create(() => UnguardInternal(key));
    }

    // 指定したキーで、Modalをガードする。
    // 返り値のIDisposableをDisposeしたときにガード解除する。
    public IDisposable GuardModal(string key)
    {
        GuardModalInternal(key);
        return Disposable.Create(() => UnguardModalInternal(key));
    }

    // 指定したキーで、Overlayをガードする。
    // 返り値のIDisposableをDisposeしたときにガード解除する。
    public IDisposable GuardOverlay(string key)
    {
        GuardOverlayInternal(key);
        return Disposable.Create(() => UnguardOverlayInternal(key));
    }

    // 指定したキーで、BackKeyをガードする。
    // 返り値のIDisposableをDisposeしたときにガード解除する。
    public IDisposable GuardBackKey(string key)
    {
        GuardBackKeyInternal(key);
        return Disposable.Create(() => UnguardBackKeyInternal(key));
    }

    #endregion

    private void GuardInternal(string key)
    {
        GuardModalInternal(key);
        GuardOverlayInternal(key);
        GuardBackKeyInternal(key);
    }

    private void UnguardInternal(string key)
    {
        UnguardModalInternal(key);
        UnguardOverlayInternal(key);
        UnguardBackKeyInternal(key);
    }

    private void GuardModalInternal(string key)
    {
        if (!modalGuardDict.ContainsKey(key)) {
            modalGuardDict.Add(key, 0);
        }
        modalGuardDict[key] = modalGuardDict[key] + 1;

        if (modalGuardDict.Count == 1 && modalGuardDict[key] == 1)
        {
            ChangeModalGuard(true);
        }
    }

    private void UnguardModalInternal(string key)
    {
        if (!modalGuardDict.ContainsKey(key) || modalGuardDict[key] <= 0) {
            return;
        }

        modalGuardDict[key] = modalGuardDict[key] - 1;

        if (modalGuardDict[key] <= 0) {
            modalGuardDict.Remove(key);
        }

        if (modalGuardDict.Count == 0) {
            ChangeModalGuard(false);
        }
    }

    private void GuardOverlayInternal(string key)
    {
        if (!overlayGuardDict.ContainsKey(key)) {
            overlayGuardDict.Add(key, 0);
        }
        overlayGuardDict[key] = overlayGuardDict[key] + 1;

        if (overlayGuardDict.Count == 1 && overlayGuardDict[key] == 1)
        {
            ChangeOverlayGuard(true);
        }
    }

    private void UnguardOverlayInternal(string key)
    {
        if (!overlayGuardDict.ContainsKey(key) || overlayGuardDict[key] <= 0) {
            return;
        }

        overlayGuardDict[key] = overlayGuardDict[key] - 1;

        if (overlayGuardDict[key] <= 0) {
            overlayGuardDict.Remove(key);
        }

        if (overlayGuardDict.Count == 0) {
            ChangeOverlayGuard(false);
        }
    }

    private void GuardBackKeyInternal(string key)
    {
        if (!backKeyGuardDict.ContainsKey(key)) {
            backKeyGuardDict.Add(key, 0);
        }
        backKeyGuardDict[key] = backKeyGuardDict[key] + 1;

        if (backKeyGuardDict.Count == 1 && backKeyGuardDict[key] == 1)
        {
            ChangeBackKeyGuard(true);
        }
    }

    private void UnguardBackKeyInternal(string key)
    {
        if (!backKeyGuardDict.ContainsKey(key) || backKeyGuardDict[key] <= 0) {
            return;
        }

        backKeyGuardDict[key] = backKeyGuardDict[key] - 1;

        if (backKeyGuardDict[key] <= 0) {
            backKeyGuardDict.Remove(key);
        }

        if (backKeyGuardDict.Count == 0) {
            ChangeBackKeyGuard(false);
        }
    }

    private void ChangeModalGuard(bool isGuarded)
    {
        modalGraphicRaycaster.enabled = !isGuarded;
    }

    private void ChangeOverlayGuard(bool isGuarded)
    {
        overlayGraphicRaycaster.enabled = !isGuarded;
    }

    private void ChangeBackKeyGuard(bool isGuarded)
    {
        backKeyHandler.enabled = !isGuarded;
    }
}

ユーザーの入力の種類が増えることに備え、Interfaceを作り入力毎のクラスを分けていくとより拡張しやすくなりますね。
使い方はこちら



IDisposable disposable = InputGuardManager.Instance.Guard("Login");
// ログインする通信処理をここで実行。この間ユーザー入力はガードされる。
disposable.Dispose();

InputGuardManager の入力ガード解除を忘れなくする拡張

上の使い方で Dispose をし忘れると、入力が防がれたままになってしまいます。
忘れないように以下のような使い方ができるようにしました。



Observable.ReturnUnit()
    .UsingInputGuardWith(
        "Login",
        _ => LoginProcess()
    )
    .Subscribe()
    .AddTo(subscriptions);

LoginProcess の処理を行っている間、指定したキー "Login" でユーザーの入力を防ぎます。
明示的に Dispose しなくてもガードが外れるので安心です。
実装はこちら



public static class Extension
{
    public static IObservable UsingInputGuardWith(
        this IObservable source,
        string guardKey,
        Func> observableFactory)
    {
        return source.Using(
            observableFactory, _ => InputGuardManager.Instance.Guard(guardKey)
        );
    }
}

ここで使われている Using は ReactiveX で提供されている機能を UniRx で使えるようにしたもので、明日の記事担当の @trapezoid さん作成のモジュールです。



public static class ObservableEx
{
    public static UniRx.IObservable Using(
        Func> observableFactory, Func resourceFactory)
    {
        return UniRx.Observable.Create (observer => {
            SerialDisposable disposable = new SerialDisposable();
            disposable.Disposable = resourceFactory();
            return StableCompositeDisposable.Create(
                observableFactory()
                    .Finally(() => disposable.Dispose())
                    .Subscribe(observer),
                disposable
            );
        });
    }

    public static UniRx.IObservable Using(
        this UniRx.IObservable source,
        Func> observableFactory,
        Func resourceFactory)
    {
        return source.SelectMany(
            value => Using(
                () => observableFactory(value), 
                () => resourceFactory(value)
            )
        );
    }
}

入力ガードしているのと同じ期間、画面上に読み込み中表示を出すという拡張も可能です。



// 接続中オーバーレイとInputGuardをUsingする。
// ここでは ConnectingOverlayManager の Show により読み込み中表示が出て、
// 返り値の IDisposable を Dispose することで表示解除されるとします。
public static IObservable UsingConnectingOverlayAndInputGuardWith(
    this IObservable source,
    string inputGuardKey,
    Func> observableFactory)
{
    return source.Using(observableFactory, _ => {
        return new CompositeDisposable(
            InputGuardManager.Instance.Guard(inputGuardKey),
            ConnectingOverlayManager.Instance.Show(
                ConnectingOverlayManager.OverlayType.Connecting
            )
        );
    });
}

InputGuardManager の入力ガード状況をリアルタイム反映する EditorWindow

InputGuardManager がユーザーの入力を防ぐ状況を UnityEditor でリアルタイム反映する EditorWindow です。
開発中に、意図した期間、意図したキーで、意図した入力が防がれていることを確認しやすくなります。



// InputGuardManager の入力ガード状況をリアルタイム表示する EditorWindow
// 情報の表示・変更のために InputGuardManager にデバッグ専用メソッドを幾つか足す必要があります。
public class InputGuardManagerEditor : EditorWindow
{
    private const float Height = 16f;
    private const float MinKeyColumnWidth = 120f;
    private const float FlagColumnWidth = 46f;

    [MenuItem("Window/InputGuardManagerEditor")]
    static public void OpenInputGuardManagerEditor()
    {
        EditorWindow.GetWindow(false, "InputGuard", true).Show();
    }

    // 秒間10回呼ばれるインスペクタのUpdateメソッド
    private void OnInspectorUpdate()
    {
        Repaint();
    }

    private void OnGUI()
    {
        if (!InputGuardManager.HasInstance()) {
            GUILayout.Label("InputGuardManagerが入力を防ぐ状況を表示・変更します。");
            return;
        }

        if (!Application.isPlaying) {
            GUILayout.Label("ゲームが起動していません。");
            return;
        }

        DrawGuardCondition();
    }

    private void DrawGuardCondition()
    {
        const float ColumnWidth = 60f;

        EditorGUILayout.BeginVertical();
        {
            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.LabelField("Modal:   ", GUILayout.Width(ColumnWidth));
            EditorGUILayout.LabelField("Overlay: ", GUILayout.Width(ColumnWidth));
            EditorGUILayout.LabelField("BackKey: ", GUILayout.Width(ColumnWidth));
            EditorGUILayout.EndHorizontal();

            // 現在各入力が防がれているかどうかを表示
            EditorGUILayout.BeginHorizontal("AS TextArea", GUILayout.MinHeight(20f));
            EditorGUILayout.LabelField(InputGuardManager.Instance.IsModalGuardedForDebug ?
                "Guarded" : "__", GUILayout.Width(ColumnWidth));
            EditorGUILayout.LabelField(InputGuardManager.Instance.IsOverlayGuardedForDebug ?
                "Guarded" : "__", GUILayout.Width(ColumnWidth));
            EditorGUILayout.LabelField(!BackKeyManager.Instance.Enabled ?
                "Guarded" : "__", GUILayout.Width(ColumnWidth));
            EditorGUILayout.EndHorizontal();

            // 各入力をボタンで強制的に解除する
            // デバッグ中に入力ガードを外して動作確認するのに便利です
            EditorGUILayout.BeginHorizontal();
            {
                EditorGUILayout.BeginVertical(GUILayout.Width(ColumnWidth));
                if (GUILayout.Button("Clear", GUILayout.Width(ColumnWidth))) {
                    InputGuardManager.Instance.ClearAllModalGaurdUnsafe();
                }
                EditorGUILayout.EndVertical();

                EditorGUILayout.BeginVertical(GUILayout.Width(ColumnWidth));
                if (GUILayout.Button("Clear", GUILayout.Width(ColumnWidth))) {
                    InputGuardManager.Instance.ClearAllOverlayGuardUnsafe();
                }
                EditorGUILayout.EndVertical();

                EditorGUILayout.BeginVertical(GUILayout.Width(ColumnWidth));
                if (GUILayout.Button("Clear", GUILayout.Width(ColumnWidth))) {
                    InputGuardManager.Instance.ClearAllBackKeyGuardUnsafe();
                }
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndHorizontal();

            DrawKeys();
        }
        EditorGUILayout.EndVertical();
    }

    private void DrawKeys()
    {
        HashSet allKeySet
            = new HashSet(InputGuardManager.Instance.ModalGuardDictKeysForDebug);
        allKeySet.UnionWith(InputGuardManager.Instance.OverlayGuardDictKeysForDebug);
        allKeySet.UnionWith(InputGuardManager.Instance.BackKeyGuardDictKeysForDebug);

        if (allKeySet.Count == 0) {
            EditorGUILayout.LabelField("No keys found.");
            return;
        }

        DrawHeader();

        List keys = allKeySet.ToList();
        keys.Sort();

        foreach (string key in keys) {
            DrawKeyRow(key);
        }
    }

    private void DrawHeader()
    {
        EditorGUILayout.BeginHorizontal();
        {
            EditorGUILayout.LabelField("Registered Key", GUILayout.MinWidth(MinKeyColumnWidth));
            EditorGUILayout.LabelField("Modal", GUILayout.Width(FlagColumnWidth));
            EditorGUILayout.LabelField("Overlay", GUILayout.Width(FlagColumnWidth));
            EditorGUILayout.LabelField("BackKey", GUILayout.Width(FlagColumnWidth));
        }
        EditorGUILayout.EndHorizontal();
    }

    private void DrawKeyRow(string key)
    {
        EditorGUILayout.BeginHorizontal("AS TextArea", GUILayout.MaxHeight(Height));
        {
            EditorGUILayout.SelectableLabel(
                string.IsNullOrEmpty(key) ?
                "!!empty!!" :
                key, GUILayout.MinWidth(MinKeyColumnWidth), GUILayout.MaxHeight(Height));

            EditorGUILayout.LabelField(
                InputGuardManager.Instance.GetModalGuardCountForDebug(key).ToString(),
                GUILayout.Width(FlagColumnWidth),
                GUILayout.MaxHeight(Height));

            EditorGUILayout.LabelField(
                InputGuardManager.Instance.GetOverlayGuardCountForDebug(key).ToString(),
                GUILayout.Width(FlagColumnWidth),
                GUILayout.MaxHeight(Height));

            EditorGUILayout.LabelField(InputGuardManager.Instance.GetBackKeyGuardCountForDebug(key).ToString(),
                GUILayout.Width(FlagColumnWidth),
                GUILayout.MaxHeight(Height));
        }
        EditorGUILayout.EndHorizontal();
    }
}

この EditorWindow を使うとリアルタイムでガード状況がわかり、不具合でガードされたままになるときもボタンでひとまずガード解除してデバッグを続けられます。

InputGuardManagerEditor.png

実機で入力ガードされたままになったときの状況を確認するDebugMenu

実機動作確認やQAで入力ガードされたままになった場合にも、ガードしているキーを確認して情報を得たり、ひとまずガードを外したりできるように DebugMenu があると便利です。
QAから開発にDebugMenuのスクリーンショットを送ってもらえば、原因特定が容易になります。
DebugMenu の実装については割愛しますが、上述の EditorWindow とほぼ同様のコードとなります。

InputGuardDebugMenu.png

ユーザー選択を簡単に取得する汎用確認ダイアログ呼び出し DialogHelper

stringのメッセージを表示し、ユーザーの「はい」か「いいえ」さらにAndroidのバックキーの入力によって後続の処理を変える汎用確認ダイアログを開くモジュールです。

呼び出し方



CompositeDisposable subscriptions = new CompositeDisposable();

private void OpenBattleConfirmationDialog()
{
    subscriptions.Clear();

    DialogHelper.ShowConfirmDialog("戦いを挑みますか?")
        .Where(decisionType => decisionType == DecisionType.Yes)
        .Subscribe(GoToBattle)
        .AddTo(subscriptions);
}

実装はこちら



public static class DialogHelper
{
    // ユーザーの決定した行動タイプ
    public enum DecisionType
    {
        Yes,      // はい
        No,       // いいえ
        BackKey,  // Androidのバックキー
    }

    public static IObservable ShowConfirmDialog(string message)
    {
        // 汎用ダイアログのクラス
        Dialog dialog = GetDialog();

        // Androidのバックキーイベントを扱うクラス
        BackKeyHandler backKeyHandler = GetBackKeyHandler();

        // Observable.Zip は、複数のストリームに1つずつメッセージが来た時点でそれらを合成して流します
        // Observable.Zmb は、複数のストリームのうち、最も早く流れてきたものを流します

        return Observable.Zip(
            Observable.Amb(
                // Yesが押されたとき
                dialog.OnSelectYes.Select(_ => DecisionType.Yes),
                // Noが押されたとき
                dialog.OnSelectNo.Select(_ => DecisionType.No),
                // BackKeyが押されたとき
                backKeyHandler.OnBack.Select(_ => DecisionType.BackKey)
            ).Do(_ => dialog.Hide()),         // Yes、No、BackKeyのいずれかが押されたらダイアログを閉じ始める
            dialog.OnFinishHideAnimation,     // ダイアログが閉じるアニメーションが終わるまで待つ
            (decisionType, _) => decisionType // Yes、No、BackKeyのどれが押されたかを後ろに流す
        ).First();                            // 最初の選択だけが流れるようにする
    }
}

dialog.Hide() の後に SelectMany で dialog.OnFinishHideAnimation を続けていないのは、もしアニメーションが全く設定されておらず Hide() の呼び出しの中で即座に OnFinishHideAnimation に流れてきてしまっていた場合、ShowConfirmDialogが何も流さなくなってしまうからです。

複数の通信処理をまとめる処理

複数の通信APIを組み合わせた複雑な処理を隠蔽するクラスを作るパターンを例示します。
ゲーム仕様によるのでこのまま使い回しはできませんが、複雑な処理をまとめたいときによく使っている手法です。

ギルドからの脱退処理

仕様

  • ギルドメンバーはギルド脱退通信を呼び出せる。
  • ギルドメンバーでなければ、ギルド非所属だとメッセージを出す。
  • ギルドメンバーならば、脱退させて成功メッセージを出す。
  • ギルドマスターなら、マスターは脱退できないとメッセージを出す。
  • ギルドにギルドマスター1人しかいないときは、代わりにギルド解散通信を呼び脱退成功メッセージを出す。

上記の仕様を実現するための一連の処理を隠蔽するクラスを作ることを考えます。 サーバーサイドでロジックを組むという方法も考えられますが、ここでは既に用意された信頼性の高い「ギルド脱退」「ギルド除法取得」「ギルド解散」といった個別のAPIを組み合わせて実現することとします。

一部処理を省略したコードが以下です。



public class LeaveGuildService
{
    public IObservable StartProcess()
    {
        return LeaveGuild()
            .CatchIf(
                (GuildException e) => e.GuildErrorId == GuildErrorId.NOW_GUILD_MASTER,
                e => OnNowGuildMasterError()
            );
    }

    // ギルド脱退処理
    private IObservable LeaveGuild()
    {
        return new LeaveGuildProcess().StartProcess()  // ギルド脱退APIを叩く
            .RethrowIf((NetworkException e) =>  {
                switch (e.ErrorId) {
                case NetworkErrorId.NOT_GUILD_MEMBER:  // ギルドメンバーではない場合
                case NetworkErrorId.NOW_GUILD_MASTER:  // ギルドマスターではない場合
                    return true;
                }
                return false;
            }, e => new GuildException(e))
            .__通信のリトライ処理__
            .AsUnitObservable();
    }

    // ギルド脱退処理で、ギルドマスターであるエラーが帰ってきたときに呼ぶ処理
    private IObservable OnNowGuildMasterError()
    {
        return GetMyGuildInfoModel()
            .SelectMany(guildInfoModel => CheckGuildMemberCount(guildInfoModel))
            .SelectMany(_ => DeleteGuild())
            .AsUnitObservable();
    }

    // 自分の所属ギルドの情報を取得する処理
    private IObservable GetMyGuildInfoModel()
    {
        return new GetMyGuildRecordProcess().StartProcess()  // 所属ギルド情報取得APIを叩く
            .RethrowIf(
                (NetworkException e) => e.ErrorId == NetworkErrorId.NOT_GUILD_MEMBER,
                e => new GuildException(e)
            )
            .__通信のリトライ処理__;
    }

    // ギルドの所属メンバー数が1人だけであるかを調べ、1人でないならGuildExceptionを流す。
    private IObservable CheckGuildMemberCount(GuildInfoModel guildInfoModel)
    {
        if (guildInfoModel.MemberCount != 1) {
            return Observable.Throw(
                new GuildException("ギルドの所属メンバーが1人だけではない",
                GuildErrorId.GuildMemberCountNotOne)
            );
        }

        return Observable.ReturnUnit();
    }

    // ギルド解散処理
    private IObservable DeleteGuild()
    {
        return new DeleteGuildProcess().StartProcess()  // ギルド解散APIを叩く
            .RethrowIf((NetworkException e) => {
                switch (e.ErrorId) {
                case NetworkErrorId.NOT_GUILD_MEMBER:   // ギルドメンバーではない場合
                case NetworkErrorId.NOT_GUILD_MASTER:   // ギルドマスターではない場合
                    return true;
                }
                return false;
            }, e => new GuildException(e))
            .__通信のリトライ処理__
            .AsUnitObservable();
    }
}

例外によって処理を様々に分岐させる必要がある処理を簡潔にまとめることができます。
このクラスを利用する側では個別の通信が上手くいったかどうかを気にすることなく成功か失敗かを受け取ることができます。
後でギルド脱退を呼び出したい場所が増えても安心です。

こういったServiceクラスを共通の形式で作ることで、より複雑な一連の処理もServiceクラスを結合した新たなServiceクラスとし、DRYな状態を保てます。

ここで使われている RethrowIf と CatchIf も @trapezoid さん作成のモジュールです。



public static class ErrorHandlingExtensions
{
    // 条件にマッチするExceptionがOnErrorに流れてきた時、別の例外に変換して後続に流す
    public static UniRx.IObservable RethrowIf (
        this UniRx.IObservable source, 
        System.Predicate predicate,
        System.Func conversion
    )
        where TSourceException : System.Exception
        where TDestinationException : System.Exception
    {
        return source.Catch ((TSourceException e) => {
            if (predicate (e)) {
                return UniRx.Observable.Throw (conversion (e));
            } else {
                return UniRx.Observable.Throw (e);
            }
        });
    }

    // 条件にマッチするExceptionがOnErrorに流れてきた時、別のストリームに差し替える
    // Catchの条件付与版
    public static UniRx.IObservable CatchIf (
        this UniRx.IObservable source, 
        System.Predicate predicate,
        System.Func> errorHandler
    )
     where TException : System.Exception
    {
        var result = source.Catch (
                e => predicate (e) ? errorHandler (e) : UniRx.Observable.Throw (e)
        );
        return result;
    }
}

終わりに

今回は、自分がゲーム作りをする中で実装し実務で使っている便利モジュール・実装パターンとして

  • ユーザー入力を安全に防ぎ、デバッグしやすくする InputGuardManager
  • ユーザー選択を簡単に取得する汎用確認ダイアログ呼び出し DialogHelper
  • 複数の通信処理をまとめる処理のパターン

を紹介しました。

ぜひ活用してみてください!

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNAにおけるOpenStack運用#3

DeNAのOpenStackインフラ運用を担当している窪田です。 最後にご紹介するのはSDS Cephについてです。採用の背景と、これまで運用していく中で直面したパフォーマンスの問題、それをどのようにシューティングしたのかについてご紹介します。

採用の背景

第一回の小野の記事で、Ceph導入によって実現されたこととして、OpenStackとの連携、ストレージプールの外部化、ライブマイグレーションを取り上げました。実のところこれらのことはCephでなくても近年リリースされている商用ストレージやSDSをうたうプロダクトであれば大抵できることで、これらの実現の他にも重要視していることがありました。 決め手となったのはOSSプロダクトであることです。OSSだとなぜいいのかというと、ソースコードが公開されていることによって、やろうと思えばソフトウェアの振る舞いを詳細に把握することが可能ですし、そうすることでパフォーマンス上のトラブルが起きたときに自力で何がボトルネックとなっているのかを見極め、問題をうやむやにせず納得のいく対応策を練ることができる場合があるからです。これまでDeNAのインフラを運用してきた経験からその重要性を無視できず、中身がブラックボックスなものはできれば避けたいという思いがありました。

構成

本題に入る前に弊社環境の構成について触れておきます。弊社インフラの全てがこの構成で運用しているわけでなく、まだ社内の開発者用のインフラに限定しています。 Cephはブロックストレージ、オブジェクトストレージ、ファイルシステムの三用途に使えますが、弊社ではKVM仮想マシンのストレージとして使うのが目的なのでブロックストレージ機能のみ使っています。 Cephのバージョンは0.94.6のHammerリリースを使っています。最新版ではないのでこれから述べることは最新版では当てはまらない情報も含まれていますがそこはご容赦ください。

個々のサーバのハードウェアとソフトウェアの構成は下図のようになっています。

ceph-blog-node.png

ALL HDDです。ネットワークは物理的に一つのLANに全Ceph nodeがつながっています。 Cephのパフォーマンスをより引き出すには、Ceph nodeのクラスタにそれ専用のサーバを設け、Cephクラスタ内の通信は専用のネットワークを敷く(Cluster Network)など考えられますが、 まずは今あるハードウェア資産を活かして最小限のハードウェアコストで構成し、ボトルネックを見極め、どうしてもハードウェアリソースの増強が必要な場合は追加投資、あるいはCephの導入を見送る選択肢も残しつつ進めていくことにしました。 結果、後述するチューニングを経てこの物理構成を崩さず今に至っています。ですが問題がないわけではありません。ネットワークでいえば輻輳しやすい問題があります。

今はコストを抑えたハードウェア構成を組んでいますが、これで十分と考えているわけでなく、物理的なI/Oの性能が不足しているならSSDを導入することや、後述のfsync/fdatasyncの性能問題のようなCephならではの欠点を補えるようなストレージを導入してマルチバックエンドストレージ構成にするなども視野に入れて検討しています。

ネットワークの輻輳

各Ceph nodeで稼働しているOSDが相互にTCPセッションをはってメッシュ状に通信している上に、Ceph nodeはCompute nodeも兼ねているので、OSDはCephクライアントとなるNova instanceともTCPセッションをはっています。なので各node間の同時通信が発生しやすく、バーストトラフィックによるパケットの廃棄(パケロス)が起きやすい構成です。 パケロスを抑える効果的な手段は、大きなフレームバッファを搭載しているネットワークスイッチを導入する、CephのPublic NetworkとCluster Networkを物理的に分ける、など考えられますが、既存資産を限りなく使い切りたいので、発想を変えてパケロスを抑えるのを諦め、BigSwitchとLinuxのQoS機能を組み合わせ、Nova instanceのトラフィックを優先してユーザ影響を軽減するような仕組みにできないかを検証しています。

パフォーマンスチューニング

上図のようなハードウェア構成でぜひやるべきことが2つあります。

  • ジャーナルとデータ領域を一つの物理ディスクに共存させない
  • tcmallocのキャッシュサイズを拡張する

Cephは一つのOSDに対して二度の書き込みを行います。一度目がジャーナル領域、二度目がデータ領域です。ジャーナルに書き込まれた時点でOSDはクライアントに書き込み完了のレスポンスを返すことができるので、ジャーナルにSSDのような高速なハードウェアを使う方が良いという話を聞きますが、弊社ではSSDは使わず、4つのHDDを下図のように構成しています。

ceph-blog-disk.png

Linuxルートファイルシステムが保存されているディスクは冗長化のためHW RAIDでミラーリング(RAID1)し、同ディスクに全OSDのジャーナルをファイルとして置いた構成です。 ディスクの数にもよりますが、一つのディスクにジャーナルとデータ領域を共存させるよりも、ジャーナルはデータ領域とは物理的に別のディスクを使ったほうがI/Oスループットが向上します。ジャーナルへのI/Oはシーケンシャルwriteなので、ランダムI/O中心のデータ領域と比べてディスクへの負荷が低く、高いIOPSが出やすいです。

ご参考までに、ディスクの負荷がどの程度改善するのかを、データ領域側ディスクのiostatの%utilをグラフ化したもので比較すると、下のように50%前後で推移していたのが15%前後まで下がりました。

ジャーナルと共存した場合 ceph-blog-before.png

ジャーナルと共存しない場合 ceph-blog-after.png

あとこの構成にする上で設定しておくべきこととしてあるのが、ジャーナル側ディスクのLinux I/OスケジューラをDeadlineスケジューラにすることです。その他のI/Oスケジューラだと複数のOSDからくるシーケンシャルwriteを効率良くさばけず、パフォーマンスを維持できないのでDeadlineスケジューラは必須です。

tcmallocのキャッシュサイズを拡張するチューニングについてはCephコミュニティなどからも報告があり、詳細はそちらに譲ります。 tcmallocのキャッシュサイズ拡張は、あらゆるワークロードでパフォーマンスが向上するわけではなさそうですが、弊社環境では劇的な効果がありました。 弊社では前述したように28 Ceph node、56 OSDで約1,500のNova instance(KVM仮想マシン)がCeph上で常時稼働していますが、 Nova instanceが増えるにつれて1 OSDあたりがさばくメッセージ量が増え、tcmallocのキャッシュメモリの獲得、解放にかかるCPU処理が重くなり、 クライアントのI/Oリクエストが数秒から数十秒以上待たされる現象が頻発していたのですが、このチューニングによって秒単位の遅延がほとんど発生しなくなりました。 Cephクライアントを増やしていくとパフォーマンスが劣化するような現象にあたった場合は試してみるとよいでしょう。

ログ分析

パフォーマンスボトルネックの解析で実際に役立ったログの分析方法を一つご紹介します。 CephはI/Oのパフォーマンスが何らかの要因で劣化すると、クライアントからのI/Oリクエストの遅延間隔をログに記録します。slow requestと書かれたログです。弊社では遅延の要因を判断する上でslow requestをよくみています。slow requestのログの末尾にはOSDの何の処理に時間がかかっているのかがマーキングされており、チューニングの判断材料にしています。下記のようなログが書き出されます。

  • "currently no flag points reached"
  • "currently started"
  • "currently waiting for rw locks"
  • "currently waiting for subops from "
  • "currently commit_sent"

この中でも"currently commit_sent"はあまり深刻に受け止める必要のないログです。このログはジャーナルにwrite済みのデータをデータ領域にwriteするのが遅れていることを示しており、言い換えればジャーナルにデータが何秒滞留しているかを示しています。クライアントへのレスポンスが遅延しているわけではなく、ジャーナルのサイズに余裕がある場合はこのログが断片的に出ても問題ないとみなしており、弊社ではほぼ無視しています。

その他のログは重要です。ここでは"currently waiting for subops from "だけ取り上げてみましょう。 OSDのwrite処理はまずPrimary OSDがクライアントからwriteのメッセージを受けてSecondary OSDとTertiary OSDにwriteメッセージを投げます(レプリケーション数3で運用しています)が、投げたメッセージに対してSecondaryかTertiaryのどちらかのレスポンスが遅れるとこのログが出ます。ログのfromの後ろにSecondaryとTertiaryのIDが入るのでどのSecondaryまたはTertiaryかは特定できます。なのでこのログが出たときはログを出しているPrimaryに遅延の原因があるわけでなく、そのOSDとレプリケーションを組んでいるSecondaryかTertiaryのどちらかに原因があるはずです。

他にもslow requestのログに含まれる情報で役に立つものとしてRBD imageのIDがあります。RBD imageはCinder volumeでもあるので、どのCinder volumeのI/Oが遅延しているのかを特定できます。

fsync問題

前述のチューニングである程度パフォーマンスは改善するのですが、それでもなお課題として残っているのがfsync/fdatasyncシステムコールを多用するwrite処理が苦手なことです。そのようなアプリケーションで例をいえばMySQLのトランザクションのコミット処理が当てはまります。 なぜ苦手なのかというと、Cephは複数のI/Oを同時並行処理するようなワークロードが得意ですが、fsyncのようなデータ同期を挟まれてしまうと後続のwriteが待たされてしまいます。図にするとこのようなイメージです。

ceph-blog-write.png

Cephのようにネットワークまたぎで同期レプリケーションまでやるストレージだと、サーバローカルのストレージへのI/Oで完結する場合に比べてどうしてもレイテンシが長くなり、fsyncによるレスポンス待ちの影響が際立ちます。

弊社ではこの問題の根本解決は現状無理だと判断し、クライアントのローカルファイルシステム(ext4やxfs)のバリアオプションを無効にすることでfsyncによるパフォーマンスの劣化を抑えています。バリアオプションを無効化すればfsyncによるデータ同期を回避でき、同時並行writeを維持しやすく、writeレスポンス待ちの影響を軽減できます。ただしバリアオプションの無効化には副作用があり、リスクを許容できるかを検討した上で使う必要があるのでご注意ください。弊社では前述したように社内の開発者向け環境で、最悪、データが一部欠損するようなことがあっても大きな問題にはならないようなものなどに限定して使っています。

まとめ

Cephのパフォーマンスチューニングにフォーカスしてこれまで取り組んできたことをいくつかご紹介しました。冒頭でCephの良さの一つにソースコードが公開されていることをあげましたが、ここでご紹介したパフォーマンス問題の解析でもそれが大いに役立ってくれました。まだまだCephの運用経験が浅く、わかってないことも多いのですが、今後も経験を積むことで理解がどこまでも深まっていくと思います。この記事でCephを使ってみたくなった方が少しでも増えればと思っています。

最後に

DeNAのOpenStackインフラについて三回にわたってご紹介しましたがいかがでしたでしょうか。 宣伝ですが、2017年2月10日(金) に DeNA TechCon 2017 が開催されます。OpenStackに関するセクションも予定しており、ブログでご紹介しきれなかった内容もお話できればと思っているのでぜひご参加ください。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNAにおけるOpenStack運用#2

こんにちは。IT基盤部でOpenStackの運用をしています酒井です。
私からは弊社OpenStack環境で使用しているSDNについてご紹介したいと思います。前回の記事で紹介しましたように、弊社ではBigSwitch Networks社のBig Cloud Fabric(以下BCF)を使用しています。本エントリーでは弊社がどのようにBCFをOpenStackとインテグレーションしているか、どのように運用しているのかについて紹介させていただきます。

OpenStackとSDNの導入の狙い

従来はネットワークのconfig変更をする場合、サーバエンジニアからネットワークエンジニアにその作業を「依頼」する形を取っていたのですが、この「依頼」を無くすことが狙いでした。依頼内容としてはスイッチへのVLAN設定やACL設定、LBへのオブジェクト追加など比較的簡単な作業です。しかし、依頼件数が多い時などはネットワークエンジニアがその依頼を作業するまでに数日要することもあり、スピード感に欠ける状況となっていました。それをOpenStackとSDNの導入によって、自動化したり、サーバエンジニアが自ら行うことができるようにすることで作業のスピードを上げることができ、またネットワークエンジニアもより高度なスキルを要する業務に集中できるのではないかと考えました。

構成

OpenStack + BCF 構成パターン

BCFをOpenStackと共に使用する方法としては3パターンあります。一つめはOpenStackのアンダーレイネットワークとしてBCFを使用する方法でこの場合はOpenStackとBCFの連携はありません。二つ目はP-Fabricと呼ばれる方法でOpenStackとBCFでLayer2の情報のみ連携しルーティングは物理ネットワーク上で行います。それに対しP+V FabricというパターンではLayer2, Layer3が連携され、各Compute Node上で分散ルーティングされます。P+V Fabricの方が柔軟なネットワーク構成が可能ではあるのですが、NATが必須となる仕様となっておりNATの運用に手間をかけたくなかったため、弊社ではP-Fabricのパターンを採用しました。 OpenStack_BCF_integration_pattern.png

BCFの構成

BCFはリーフ&スパイン型のスイッチ郡とBCFコントローラから構成されます。BCFコントローラが全スイッチを集中管理しており、設定作業をする際も個別のスイッチにログインする必要はなくBCFコントローラで全ての作業ができます。POD全体を一つの大きなスイッチ(文字通りBIG SWITCH)のように扱うことができる、というのが特徴です。また、BCFコントローラ、スイッチともLinuxが動作しているため、いざとなればサーバ管理者が慣れ親しんだLinuxコマンドでトラブルシューティングする、ということも可能です。弊社ではスイッチにはDELL社の製品を使用し、リーフ - スパイン間は40Gbps、リーフ - サーバ間は10Gbpsで接続しています。 ONE_BIG_SWITCH.png

OpenStackのネットワーク構成

OpenStackのネットワーク構成としては、ネットワークオプションの一つであるプロバイダーネットワークとして構成しています。Controller Nodeでneutron-serverが動作し、各Compute Nodeではneutron-openvswitch-agentが動作します。neutron-serverではbigswitch社のプラグインであるbsnstacklibと弊社内製プラグインのnetworking-bigswitch-l3-peを使用しています。内製プラグインを開発した背景については後述します。

OpenStackとBCFの連携

ここではOpenStackがどのようにBCFと連携をしているのかを具体的に紹介したいと思います。こちらがBCFのコンフィグの例になります。

tenant demo.openstack
  id 1a2cf63967ca4f26ae5356bb2e6c818c
  origination openstack
  !
  logical-router
    route 0.0.0.0/0 next-hop tenant system
    interface segment net_demo
      ip address 10.66.16.1/24
    interface tenant system
  !
  segment net_demo
    id 5310d836-c170-4fda-882c-8a61324d90c6
    origination openstack
    !
    endpoint 0485e7a0-198f-4b88-a877-ca79d3e882bc
      attachment-point interface-group osvdev0013 vlan 300
      ip 10.66.16.2
      mac fa:16:3e:ef:4f:f4
      origination openstack
    !
    member interface-group osvdev0013 vlan 300
      origination openstackdev

OpenStackとBCFのオブジェクトの対応は以下の通りです。

OpenStackBCF
Projecttenant
Networksegment
Portendpoint
Routerlogical-router

前述したようにP-fabricの構成ではLayer 2の情報が連携されます。具体的には、Project, Network, Portの情報がneutron-serverのbsnstacklibプラグインによってBCF Controllerに同期されます。しかし、Layer 3の情報に相当する logical-router のコンフィグは同期されない仕様となっています。そのため、プロジェクト毎にネットワークを作成する場合はlogical-router部分の設定を手動で追加する必要がありましたが、これでは不便なためnetworking-bigswitch-l3-peプラグインを開発しました。BCF ControllerにはAPIが用意されており、このようにpluginからAPIをコールしています。

障害事例

ここでOpenStack + BCF環境で弊社が遭遇した障害の一つとその解決方法についてご紹介しましょう。

障害内容

ある時、新規に作成したインスタンスが通信できないという障害が発生しました。各Compute Node上のneutron-openvswitch-agentのログを調べてみると、以下のようなneutron-serverに対するリクエストが失敗し続けていました。

2016-08-31 03:48:25.294 9269 ERROR neutron.plugins.ml2.drivers.openvswitch.agent.ovsneutronagent [req-57ee640e-5534-4c17-8818-4ec71381ab07 - - - - -] processnetworkports - iteration:1081082 - failure while retrieving port details from server

調査してみると、neutron-sererのbsnstacklibプラグインの不具合が原因となりリクエストがタイムアウトしていました(この不具合については既に修正済みとなっています)。neutron-openvswitch-agentはこのリクエストが失敗すると全ての管理情報を再同期しようとするため(ソースコードだとこの辺り)、さらにneutron-serverに大量のリクエストが集まり処理しきれず再度失敗する、ということを繰り返していました。

復旧方法

まずneutron-serverの負荷を減らしリクエストを捌ききるために、各Compute Node上のneutron-openvswtich-agentを1台ずつ止めていく、というオペレーションをしていきました。すると無事neutron-serverに溜まっていたリクエストがなくなりました。次は止めたneutron-openvswitch-agentを起動していくわけですが、しかし、ここで一つ問題ありました。その時使用していたneutron-openvswitch-agentのバージョン(liberty, 2:7.0.4-0ubuntu1~cloud0)だと、起動時にインスタンスの通信が数秒途切れることが分かっていました。neutronのリポジトリを調べてみるとこれらのパッチ(Cleanup stale OVS flows for physical bridges, Don't disconnect br-int from phys br if connected)で修正されていることが分かりました。これらのパッチを適用した後、止めていたneutron-openvswitch-agentを一つずつ起動しneutron-serverがリクエストを処理し終わるのを待つ、というオペレーションをしていくことで無事復旧することができました。

まとめ

OpenStack環境のSDNに使用しているBCFについて紹介させていただきました。当初の狙い通り、OpenStackとBCFを連携させることで、ネットワークエンジニアへ依頼をすることなく、サーバエンジニアが自らネットワーク構築や設定変更ができる環境を構築することができました。
BCFとのインテグレーションについてご紹介したように、製品自体がユースケースに合わない場合でもAPIを完備していることでユーザ自身がカスタマイズできることはSDN製品の良いところだと思います。またトラブルシューティングについても、OpenStackやそのプラグインはオープンソースなのでいざとなればユーザ自身が調査し問題解決できることもご理解いただけたかと思います。

次回はいよいよSDS編です。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNAにおけるOpenStack運用#1

DeNAでシステムインフラを運用しています小野です。
今回から3回に渡って、OpenStackの運用についてご紹介したいと思います。

OpenStackとは

OpenStackとは、いわゆるクラウド環境を構築/運用管理するためのOSS platformです。2010年にRack Space社とNASAのjoint projectとして始まり現在ではOpenStack Foundationが管理しています。

openstack-cloud-software-vertical-web.png

OpenStackは多数のOSSで構成されています。mysqlやrabbitmqなどお馴染みのOSSもbackendに使われていますが、OpenStack固有のOSSが主要コンポーネントになっています。例えばcomputing(vmやcontainer)の管理をするnova、ネットワークを管理するneutron、WebUIを管理するhorizonなどなどです。こちらでどういったコンポーネントがあるかを確認できます。

stackalytics.comというサイトでOpenStackの開発状況のactivityが見れるのですが、最新versionでは150社以上が開発に参加しているようで非常にglobal且つ活発であることがわかります。

半年毎に新versionがリリースされるスケジュールになっており、名称はアルファベット順に頭文字を使うルールになっています。Aから始まり最新versionではMまで進んでおりMitakaと呼ばれています。次期version(もう間もなくリリース予定なのですが)はMの次のNの頭文字を使いNuetonに決まっています。

この半年のリリーススケジュールに合わせ、半期毎にOpenStack summitというカンファレンスが開催されておりOpenStack界では一番のイベントとなっています。ここでは最新knowledgeの共有やuser事例紹介、次期version設計の為のdiscussionがopenに行われます。DeNAでも2014年11月に開催されたパリsummit以降参加するようになりました。

以前は完成度の低さが揶揄されていた時期もあったようですが、現在ではprivated cloudのみならずPublic cloudの基盤としても使われており、今やこの分野では完全なデファクトになっています。日本でも数年前からIT系企業を中心に利用する企業が増えています。

取組みの背景

何が課題だった?

DeNAでは以前からマイクロなレベルの効率化を追求していましたが長らく大きくは変わっていないところがあり、それはネットワーク管理の部分でした。この分野はサーバ管理と比べるとレガシー度が高く、市場環境として商用ハードウェアアプライアンス製品の独壇場になっていたことが主要因でなかなか効率化が進んでいませんでした。サーバ管理がよりオープンに進化しエコシステムが形成されていったのと比べると、違いは歴然です。その為自動化・効率化したくてもし難いという状況が続き、ネットワークのconfig変更が発生すると「依頼」という形で専任の担当者に作業してもらうということが続いていました。

またサーバがハードウェア故障等でお亡くなりになってしまった場合、代替機を構築する必要が発生します。いくらそれなりにprovisioningを自動化していようとも、creativeではない作業に時間を割くのは何となく気が乗らず不毛に感じるもので、こういうのを何とかしたいという考えがありました。

Publicクラウドの充実

一方でここ3〜5年ほどでPublicクラウドの品質/機能も著しく向上し、クラウドfirstが普通になってきました。あらゆることを 最少人数且つ最速でやり易いこういった環境と比べると、オンプレミスの作業効率は見劣っています。それならばPublicクラウドに移行するという考えもあり得るわけですが、弊社の現時点での試算ではコストが割に合いません。CAPEXではなくOPEXで考えてもです。その為オンプレのまま作業効率を上げたいという方向での取組も重要と捉えていました。
※ とはいえ適材適所でPublicクラウドもかなり利用してはいます

高難度技術への取組み

また違う観点として、より広範な人材を育てるべくサーバー管理、ネットワーク管理の融合を進めたいという考えもありました。
どうしてもある程度組織的に大きくなると業務分担が進みます。現在のDeNAでもサーバ管理とネットワーク管理でグループが別れているのですが、以前は違っておりその方がスピード感がありました。
なのでそれに近付けたいという思いと、OpenStackの運用は非常に広範囲な技術知識が要求されますので、個々人にとってもより広範な技術領域の習得に繋がる機会を創出することはプラスであるという考えからも、この両者の管理を融合してきたいという考えを持っていました。

ちなみにDeNAのインフラ部門では以前からアプリケーションレイヤーも含めた全方位的な管理スタイルをとっています。アプリケーションがトラブルを起こすことは極めて多いので、one stopで障害対応等する為にはアプリケーションのソースを理解しカバーすることが重要と考えています。

以前のOpenStackは単にサーバーをサーブするだけの仕組みにしかみえず、その割にバックエンドの構成が複雑なことと、そういう範囲であれば既に内製開発した同機能を提供するtoolを持っていたことから利用メリットを感じませんでした。
それが近年この分野でOpenStackがデファクトの位置を得始めるとOpenStackとのintegrationをうたう製品やソフトウェアが増え始め、ネットワーク管理とのintegrationも現実的になってきました。その為最初に挙げたような課題解決の為の基盤の中心としてOpenStackの導入を本格検討し始めました。
そしてDeNAでも2016年3月よりOpenStackを使った新世代のインフラ基盤の稼働を開始しています。
今年年初の弊社のTechカンファレンスで発表させて頂いた構想で、ほぼこれを実現したものになります。 ※ 但し最後の方にあるOSコンテナ、S(M)R-IOVなどはまだこれからのロードマップになっています。

SDX integration

SDXとは?

DeNAではOpenStackを導入するにあたって、SDN(Software Defined Network)とSDS(Software Defined Storage)を当初から導入しています。Software Defined Xというのは、XをSoftware的に扱うという意味です。ネットワークもストレージも従来はハードウェアアプライアンスが支配的で管理操作のためのAPIというのは非常に貧弱でしたが、そういったものにAPIをしっかり持たせてあらゆる操作をAPI経由で行う≒ソフトウェア的に扱えるようにしよう、というものでよく使われる表現です。

SDN(Software Defined Network)

SDNにはBigSwitch Networks社のBigCloudFabric(以下BCF)という製品を使っています。 bcf_pv0616a.png

BCFの場合各スイッチを管理するcontrollerが存在し、configuration変更等はcontrollerを介して各スイッチに反映されます。
なので何かを変更したい、情報を取得したいという場合controllerに投げればあとはよしなにやってくれるというわけです。
こう出来ると、サーバを起動する前にネットワーク(VRF/VLAN)の作成をしそこに配置する、なんて連携も簡単に出来ます。awsでvpcを作成しsecurity groupでacl管理するような事ができるわけです。

スクリーンショット 2016-09-27 14.37.25.png OpenStack画面上からのネットワーク作成がスイッチ側に伝わる様子
プロジェクトの作成はVRFの作成にあたります

スイッチ上ではSwitch Light OSというLinux(Debian)ベースのネットワークOSが動いておりcontrollerからの指示を受け自身の設定変更などを行っています。このSwitch Light OSもBigSwitch社が提供していますが、スイッチそのものはDELL社の製品を使っています。
このようにハードウェアとOSが分かれて組合せ出来るのはサーバ管理の世界では当たり前ですが、ネットワーク機器においてはこういうことも長らくできなかったわけです。

switch_light_archictecture.png

BCFの機能上の特徴としてはunderlayネットワークをサポートしてるというのがあります。underlayネットワークの説明をするには、その逆のoverlayネットワークの説明をした方がわかりやすいです。

overlayネットワークとは、ネットワークアドレスが重複しても動作できるネットワークのことです。例えば172.16.0.1/24というネットワークをいくつ作っても動作出来るようなネットワークです。Publicクラウドではこれが当たり前に出来ないと困ります。利用者側で自由にアドレス設計出来ないと使い難くてしょうがないですからね。

overlayネットワークはvxlanやgreというプロトコルを使うことで実現します。実は現在世にあるSDN製品は殆どが何故かoverlayを前提にしています。
underlayはその逆で、ネットワークアドレスの重複を認めないネットワークです。従来型のネットワークと言え、オンプレミスの環境ではこちらが普通です。

overlayネットワークは非常に柔軟性は高いのですが、それを実現する為のprotocolが重く性能面でのデメリットがあるのと、セグメントを跨って通信するにはNATが必要になるという制約も発生します。オンプレミス環境ではIP managementを自分達で出来ますのでoverlayの必要性はそもそも低く、余計なプロトコル層が増えることで負荷や障害の難易度をいたずらに上げたくはないというのもあり、DeNAではunderlayネットワークでの運用を優先しBCFを採用しています。

SDS(Software Defined Storage)

そしてSDSとしてはCephを使っています。

Ceph_Logo_Standard_RGB_120411_fa.png

Cephは分散ストレージソフトウェアです。カリフォルニア大学サンタクルーズ校で開発された後2006年にOSS化されました。現在ではRedHat社がメンテナンスをしており商用サポートも行っています。
blockストレージとしてもobjectストレージとしてもfileストレージとしても使える代物なのですが、DeNAでは主にblockストレージとして使っています。awsで言えばEBSにあたる使い方になります。

CephはOpenStackのBlock Strage Interfaceであるcinderに対応してるので、cinderのapi callでcephに自由にボリュームを作成することが出来ます。

DeNAではcephをguestの起動diskとして使っています。こうすることで再構築のような作業をほぼ0にすることが出来ます。 仮にguestが稼働しているhostサーバが死んでもすぐ別のhostサーバで起動すればいいだけなので。

スクリーンショット 2016-09-27 15.33.31.png

cephによるstorage poolからguestのimageを再読込して別のhostサーバで起動する様子
cephはデータを3重化して保持しておりこのstorage pool自体が壊れてしまうことはない想定

また、livemigrationも可能になりハードの不調が疑われる筐体からguestを逃がす事が簡易に出来るなど、運用面でかなりのメリットを得ています。

なお、ハードウェア構成としてはいわゆるハイパーコンバージドな、compute nodeとstorage nodeを一体化した構成にしており、hypervisorはKVMを使っています。

最後に

SDNやSDSで実現してることはPublicクラウドでは当たり前のことですが、それがオンプレの環境でも出来るというのをイメージ頂けたかと思います。
OpenStackを使うならSDN、SDSもしっかり含めインフラ全体をintegrationし、単なるvmをサーブするだけではない環境として高い運用効率を得ることがポイントではないかと思います。

PublicクラウドはこういったIaaSだけではなくPaaSも充実してるものもありますが、OpenStackもPaaS関連のprojectが多々あるので、今後はそこも似てくるかもしれません。

OpenStackはOSSですが、各ベンダーが商用サポートを付けているdistrubusion版が多数存在します。CentOSに対するRedHat Enterpriseのようなものです。
そういうものが存在するというのは、裏を返せば運用していくのはそれなりの難易度が必要ということで、そこそこ骨が折れるのが正直なところです。
次回以降はSDN、SDS周りのより詳細な部分をお伝えしたいと思います。
本格運用を開始してまだ半年程度ではありますが、結構いろいろな格闘がありました。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

Test Engineer Meetup #1 開催レポート

こんにちは。 SWET(SoftWare Engineer in Test) の加藤です。

2016 年 9 月 14 日、 Test Engineers Meetup #1 が開催されました。

この Meetup のテーマは

  • テストエンジニアとはどういう仕事なのか?
  • テストエンジニアがいることでどういうことがチームに起こるのか
  • テストエンジニアは今後どういう方向を目指すべきなのか

についてです。

300 名以上のかたが参加登録されるたいへん大きなイベントになり、当日は大盛況でした。あいにく会場のキャパシティから 130 名までしか受け入れられず、キャンセル待ちとなり参加いただけなかったみなさまには申し訳なく思います。

Talk (10 分 x 5)

5 人のテストエンジニアが、それぞれの組織での取り組みについて発表を行いました。

@Kazu_cocoa (Cookpad)

テストエンジニアの一端 from Kazuaki MATSUO

Cookpad でのテストエンジニアが解決する課題とスキルセットについての発表でした。

モバイルアプリのテストや、 OSS への貢献といった Cookpad 独特の取り組みが印象的でした。

@miyajan (Cybozu)

テストエンジニアと組織構造 @Cybozu from Jumpei Miyata

テストエンジニアと組織構造についての発表でした。

自ら設立したテストエンジニアリングチームが組織横断的な活動を行うようになった経緯や、今後はさらに各部署にも入り込むハイブリッドな組織にする、という展望が紹介されました。

@okitan (DeNA)

SWET (SoftWare Engineer in Test) という組織の取り組みの範囲についての発表でした。

もともとモバゲーというサービスだけを見ていた組織が、横断組織になり、それぞれの事業のチームが自ら成長し改善を行えるようにサポートする、と取り組み方を変えていくことについてが話されました。

@shoma (Mercari)

メルカリにおいての QA チームの取り組みと、そのなかでの SET (Software Engineer in Test) の担当領域についての発表です。

仕事の成果を数値化して説明しにくいこと、そして予防のコストを数字にする大変さについて、特に実感のこもったお話を聞けました。

Naito (Rakuten)

ソフトウェア品質とは何か、その程度は測定できるのか、などについての発表でした。

参加者への質問を交えたインタラクティブなセッションで、現実の問題にテストエンジニアリングで取り組むことについて、熱い意見が交わされました。

パネルディスカッション

テスト駆動開発の実践、 power-assert の開発で知られる @t_wada さんの司会で、テストエンジニアについてパネルディスカッションが開かれました。

お寿司とビール、そしてお菓子が会場に運ばれ、和やかな雰囲気でのトークになりました。

今回のために t_wada さんが用意した質問が、各登壇者に投げかけられました。

  • テストエンジニアってどんな仕事?
  • テストエンジニアがいるとどういうことが起こる!?
  • テストエンジニアの世界に興味を持ったきっかけは
  • 他チームとの関係はどうなっている?

会場からの質問も飛び交いました。

  • これから伸ばしていきたい能力は?
  • テストエンジニアとしてのロールモデルは?
  • 開発チームからの要求で工夫していることは?
  • 異なる文化圏の人間が協力して QA を行うときに壁になることは?
  • ソフトウェア品質をどうやって数値化している?

とてもすべての内容をここに書ききれないのですが、特に興味深かったのは、開発生産性の数値化という難問に対して、デプロイ完了までの時間やリグレッションテストの時間を計測し、それを指標にしているという Cookpad での事例紹介でした。

継続的なデプロイを行ううえで、待ち時間が発生するとかなりのマイナスになってしまう。それを避けるために、短時間でのデプロイとテストを実現することが価値の指標になる、とのことで、個人的にもこのアプローチに納得しました。

終わりに

DeNA の SWET グループでは、自動テスト、 CI 、ソフトウェア品質の向上・開発生産性向上に取り組んでいます。

テストエンジニアリングや CI/CD 技術基盤に興味のある方は、ぜひこちらの募集ページからご応募ください。

We are hiring です。

また、 Test Engineers Meetup #1 という名前からわかる通り、今回は第一回目。もちろん第二回目も計画しています。今回はテストエンジニアリングを行う組織に注目した発表が主でしたが、次回は具体的なテスト戦略、品質を上げる方法論についても触れていければと思います。

そしてですが、参加されたかたはぜひどんな形でも構いませんのでフィードバックをいただければ幸いです。ご参加された方にはアンケートを送付いたしましたので、ぜひ感想・ご意見をください。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

CircleCI Meetup #1 開催レポート

こんにちは。SWET(Software Engineer in Test)の栗田です。

弊社SWETグループは、

  • DeNAサービス全般の品質向上
  • DeNAエンジニアの開発生産性向上

の2つのミッションのもと、ソフトウェアテストの自動化や、エンジニアの生産性向上のためのツールの開発・導入などを行うソフトウェア・エンジニアの集団です。

これらの取り組みの一環として、弊社では開発プロセスに継続的インテグレーション(Continuous Integration、以下CI)を早くから導入してきました。
近年、プロダクトの開発サイクルがどんどんと短くなっていくにつれ、その中で品質を担保する手段の一つであるテスト自動化と、それを開発プロセスに含めるCIは、開発に必須の要素となっています。

CIを実装するCIツールは世の中に数多くありますが、その中でもJenkinsは、フリーかつオープンソースであることや、豊富なプラグインによる柔軟性などから、CIの世界ではデファクトと言っていいほど広く使われています。
弊社でも、これまでJenkinsを利用して数多くのプロダクトのCIを実装してきました。

しかし社内のプロダクトが多様化することにより、開発プロセスそのものも多様化し、かつ開発サイクルが短期化するにつれ、Jenkinsを利用する上でのデメリットも顕在化してきました。

1.Jenkins構築の大変さ

Jenkinsはその柔軟さにより、様々な開発プロセスにおいてCIを実装可能です。反面、インストールした初期の状態から、実際に使い始められる状態にするまで、数多くの設定を行わねばなりません。この設定の多さが、素早くCIを始めたいプロジェクトでは障害となっており、開発開始と同時にCIを導入できない原因の一つとなっていました。

2.Jenkins運用の大変さ

開発プロセスが多様化すると、1台のJenkinsですべての開発プロセスに対応することが困難になります。
そのため、各プロジェクト向けにJenkinsを構築し、運用することになります。Jenkinsの運用では、プロダクトのビルド・テストに使うミドルウェア・フレームワークを定期的にバージョンアップする必要があります。
特にスマホアプリ開発では、android-sdkやXcodeなどを新バージョンが出るたびにインストールし、かつ古いバージョンも維持しておく場合がほとんどです。こういった作業は、ほぼすべてのプロジェクトで行う必要があり、そのコストはバカになりません。

これらの問題を解決すべく、SWETグループでは、

  1. Jenkinsへの機能追加を基本とした、新しいCIツールの開発
  2. クラウドCIツールの導入

の2つを進めています。

今回は、2. のクラウドCIツールの導入について、様々なツールの評価を行った結果、CircleCI Enterpriseを導入することとなりました。

CircleCIは、Githubと連携できるコンテナ型のCIツールです。ここでのコンテナ型とは、近年急速に普及しているコンテナ仮想化技術を使い、ビルドごとにコンテナを割り当てることで、各プロジェクトに独立したビルド環境を提供するタイプのツールを指します。
CircleCIでは、コンテナのベースとなるOSイメージに、よく利用されるミドルウェア群があらかじめインストールされており、初期セットアップが不要です。またデフォルトのミドルウェア群に含まれていないものでも、circle.ymlとよばれる設定ファイルに追加ミドルウェアを記述し、Githubのリポジトリに含めることで、容易に追加できます。
また社内に1台セットアップし、各プロジェクトで共有するため、管理コストを集約することもできました。

CircleCI Meetup 当日の様子

今回、CircleCI社のメンバが来日することに合わせ、弊社でのCIツールに対する取り組み紹介や、日本国内のCircleCIユーザの交流・技術共有などを目的に、弊社主催という形でCircleCI Meetupを開催いたしました。

Meetupでは、まずCircleCIエバンジェリストKevin Bell氏より、CircleCIの便利な機能の一つである、ビルド実行コンテナへのSSHログイン機能についてや、実行したテストの情報を閲覧するテストメタデータな機能などについてご紹介頂きました。
他CircleCIの機能については、公式ドキュメントがございますので、そちらをご覧ください。

次に、私から弊社でのCircleCI導入に至る経緯をお話させていただきました。

その後、CyberAgent社よりstormcat24さんにお越しいただき、同社でのCircleCI導入後、どのように活用されているかなどについてお話頂きました。弊社より先に導入されていることもあり、社内での導入推進や、その際に気をつける点など、頷くところが多く、今後の参考にしていきたいと思いました。

上記3つのセッションのあと、乾杯を挟んでLTがありました。事前応募のLT枠の方に加え、当日飛び込みのLTもあり、大変盛り上がりました。

  • はてなブログへの自動投稿
  • GCP/GKEへのデプロイ
  • CircleCIクライアントアプリ「CI2Go」(iOS)
  • 何台ものサーバへのセキュリティアップデートの適用
  • CentOSへのCircleCI Enterprise構築 ...など

どのテーマもこれまで見たことがない事例ばかりで、非常に興味深かいものでした。
当日の発表資料は、こちらのイベントページよりご覧いただけます。

circleci_meetup_1_1.JPG

満員のセッションの様子

circleci_meetup_1_2.JPG

会場で配られたCircleCIステッカー

circleci_meetup_1_3.JPG

参加者からの質問に答える Kevin Bell 氏(写真中央)

今回は130名の参加枠にかかわらず、キャンセル待ちが出るほどの応募があり、当日残念ながらご参加頂けなかったみなさまには大変申し訳ありませんでした。
しかしながら、これほどの多くの応募があり、改めてCircleCIの人気を実感いたしました。
また懇親会の中で、Jenkinsに対する課題や、CIをどうやって社内で広めていくかなどについて、参加者の方からたくさんの質問を頂き、私自身、企業内開発におけるCIについて、改めて考える機会をいただきました。

終わりに

SWETグループでは、CIだけでなく、プロダクトの品質向上・エンジニアの開発生産性向上に日々取り組んでいます。また、この役割に積極的に取り組んでくれる仲間を探しています。興味のある方は、ぜひこちらの募集をご覧ください!

最後に、今回のMeetup主催に快く協力いただいたCircleCI社メンバ、また会を盛り上げていただいた発表者の方に、厚く御礼申し上げます。
ありがとうございました!!

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNA TechCon 2016のセッション動画を公開しました!

dena_techcon_2016_logo

TechCon 2016のセッションをスライド・動画アーカイブでご覧いただけます!

こんにちは。Iketakiです。

1/29に開催されました DeNA TechCon 2016 の動画アーカイブを公開いたしました! (※DeNA TechCon 2016については、こちらの記事などをご覧ください)

イベントの公式HP内の「SCHEDULE」より、各登壇者の当日のスライドと、動画アーカイブをご覧いただくことができます。

スライド・動画へはこちらから!

capture

(公式HPに遷移します)

また、動画は、YouTubeのDeNA Channelからもご覧いただけます。

当日ご来場することができなかったみなさまや、セッションにご興味のあるみなさまは、ぜひスライドや動画をご覧いただければと思います。

イベントレポートはこちらから!

当日の会場の様子や各セッションの要点を、以下の開催レポートにまとめております。 こちらもぜひご覧ください。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNA TechCon 2016 開催レポート【4】

こんにちは。SWETの加藤です。

1月29日、DeNAは技術カンファレンス「DeNA TechCon 2016」を開催いたしました。 DeNAエンジニアブログでは数回に渡りその内容を振り返り、発表がどのようなものだったかをご紹介します。

第4回となる本記事は、以下の講演についてのものになります。

DeNAが取り組む Software Engineer in Test

品質向上と開発生産性向上を目的とした、SWETという部署が紹介されました。

登壇者はSWETの NAKAGAWA MASAKI ( @ikasam_a )です。

3A4A4959.JPG

GoogleでのSET(Software Engineer in Test)に由来するSWET(SoftWare Engineer in Test)の役割や、DeNAでSWETが発足してから現在までの歩みが語られました。

SWETではブラウザテストを自動的に実行するSeleniumがよく使われており、Seleniumを実際のアプリケーションテストに導入する手順やTIPSを解説するSelenium実践入門にSWETのメンバーが執筆に参加しているそうです。

また、SWETが扱っている最新の案件として、スマートフォンアプリのテストを管理・実行する、STFというデバイスファームが説明されました。このSTFはCyberAgent社発のオープンソースのプロジェクトで、Android端末を管理し、ブラウザから操作できるという機能を持っています。SWETではこれをモバイル自動テストに導入するため、v2.0.0で追加されたAPIを実装しました。

最後は、SWETというキャリアパスについての話題となりました。SWETはまだ認知度は高くないが、開拓すべき新技術が多く、アーキテクトに繋がるひとつの道である、という見解が述べられました。

スライドはこちらからどうぞ: DeNAが取り組む Software Engineer in Test

DeNAの分析を支える分析基盤

DeNAのサービスにおいてユーザの志向や反応を分析する分析推進部から、弊社の YAMADA KENSHINが登壇しました。

IMG_8837.JPG

アナリストに求められる役割や、現在のDeNAの分析体制、分析基盤のこれまでの歩みについてが話されました。DeNAではもともとサービスの開発チームが評価・分析を行っていたのですが、モバゲーが急成長するにつれて、分析の専門チームが必要とされるようになってきたそうです。2010年に専門組織が発足し、大量のログからユーザ行動を分析するために、HadoopやVerticaなどを導入しながら、増大するデータに対処してきたのだとか。

さまざまなサービスの分析を行う全体構成についても説明されました。ログの回収と監視、集計バッチのフロー、分析結果の可視化のために、CassandraやStorm/HBaseを組み合わせて使っているとのこと。また、KPIレポートを作成するために、Argusという内製のレポートツールを開発したそうです。

また、分析にとどまらないデータ活用の取り組みとして、ハッカドールのような推薦システムが紹介されました。ユーザーの行動から、趣味にあったゲームを推薦したり、ファッションのテイストが似ている商品に誘導する仕組みに応用できるそうです。

スライドはこちら: DeNAの分析を支える分析基盤

これからの Microservices

理想的なMicroservicesの実現に求められるAPIやそのプロトコル、サービスのアーキテクチャがトピックとなりました。

弊社の YAMAGUCHI TORU (@zigorou)の発表です。

3A4A5470.JPG Microservicesの概念と、その目的、利点と欠点、実現するために必要な要素などが説明されました。

MicroservicesとMonolithicなアーキテクチャの違いや、RESTという制約が現実の課題にどこまで有効かなど、現実のアプリケーションとRESTの思想がどう結びつくのかが語られました。また、Content Negotiation、JSON SchemaとJSON Hyper Schema、API Gateway、Access tokenなどについて、Microservicesのアーキテクチャでアプリケーションを実装する観点からの見解が述べられました。

スライドはこちらです: これからの Microservices

結びに

DeNA TechCon 2016の紹介記事は今回が最後となります。

わずか1日間のイベントでしたが、非常に濃密な時間でした。高度な内容が多く、私個人としても学ぶことが大きかったと感じます。興味を持たれたかたは、ぜひ発表のスライド資料もご覧ください。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る