UnityでC#を使うときのヒント集

はじめに

こんにちは。カヤックソーシャルゲーム事業部技術基盤チーム所属、Unityエンジニアのと申します。普段はUnity用のツールや共通ライブラリーなどの開発を担当しています。よろしくお願いいたします。

この記事はカヤックUnityアドベントカレンダー2016の7日目の記事になります。

C#言語はいろんな場面でアプリを開発することができますが、プラットフォームごとに挙動の違いや特別な注意事項などがあります。今回はUnityの環境でC#を使うときに注意すべきポイントを紹介します。

NullReferenceExceptionをなくす

NullReferenceExceptionはUnityでゲームを開発するとき一番出やすい例外です。特に実機で発生する場合は、アプリが動かなくなることもよくあります。NullReferenceExceptionは値がnullになってる変数にアクセスしようとするとき発生する例外です。ネットワークエラーやユーザー入力によるエラーなどと違って、NullReferenceExceptionが出た場合は大抵スクリプトのどこかにバグがあります。ですから対応の方針は「処理する」じゃなくて「無くす」です。

nullチェック

ポイントは変数や関数の戻り値などを使う前に、nullの可能性を考えることです。このときはnullに対して、「ありえる」と「ありえない」の2パタンがあります。List<T>.FindGetComponentなど、nullを返す可能性が十分ある場合は、変数を使う前にnullチェックをする必要があります。

例えば

var camera = GetComponent<Camera>();

// nullをチェック、処理する
if (camera == null)
{
    camera = gameObject.AddComponent<Camera>();
}

// 絶対nullじゃないから、cameraを使う...

Assertを使う

逆に、ロジック的に「この関数はnullを返すわけがない」場合もあります、こういうときにnullチェックをしても、バグを隠し、CPUを無駄にするだけなので、チェックせずに「これはバグだ」と大声で警告を出すべきです。こんなときにはUnityのAssertクラスが役に立ちます。

Assertクラスにはいろんなアサーションメソッドが用意されています。使い方は簡単、例えば

var character = CreateCharacter();

// characterはnullではないはずと宣言する
Assert.IsNotNull(character, "CreateCharacter should always return a valid character object.");

// characterを使う...

こうしたら、万が一nullが返された場合には、エラーメッセージがログされ、バグがあることもすぐわかります。そしてAssert.raiseExceptionsを設定したら、スクリプトの実行も止めてくれます。アサーションメソッドは本番ビルドから自動で削除されるので、オーバーヘッドがないのも便利です。

他にもいろんなアサーションメソッドがあるので、積極的に使えば、バグは少なくなるでしょう。

UnityEngine.Object

UnityEngine.ObjectクラスはUnityが管理しているオブジェクトの共通ベースクラスです。CameraMaterialなどの型はもちろん、MonoBehaviourScriptableObjectから派生したスクリプトクラスもUnityEngine.Objectです。UnityEngine.Objectを使うときには特別な注意事項があります。

== nullの挙動

UnityEngine.Objectobj == nullでnullと比較するときの挙動は普通のC#オブジェクトと違って、nullではないのにtrueになる場合もあります。例えば、AssetBundle.Unloadを呼んだら、nullをアサインしなくてもassetBundle == nullの結果もtrueになります。こんなことが起こる原因は、UnityEngine.Object==オペレーターをオーバーライドしているからです。== nullの意味は「破棄されたかどうか」です。

C#のオブジェクトとしてほんとにnullであるかどうかをチェックしたいときには、ReferenceEquals(obj, null)を使えばOKです。

コンストラクタはなぜ使えない

MonoBehaviourとかから派生したスクリプトクラスを書くとき、コンストラクタを使わずにAwakeOnEnableなどを使うのが基本です。C#をよく使う人にとっては不自然ですが、以下のような理由でコンストラクタを使用できません。

  1. プレーモード以外にもスクリプトのインスタンス化が行われています。シーンを編集するときなど、ゲームが実際実行されてない場合にもスクリプトのインスタンスを作っています。初期化ロジックをコンストラクタで書くと、実行されるタイミングは分かりづらいです。
  2. UnityEngine.Objectのコンストラクタは非同期でローディングスレッドで実行されています。Unityのスクリプトエンジンはシングルスレッドベースなので、別スレッドではGetComponentなどのUnity APIは使えないし、同期をとるのも複雑で、間違えやすいです。Awakeなどのメッセージはこういった問題はありません。

GC Allocを減らす

Unityは.NETと同じ、自動メモリー管理が実装されていますが、その性能はCLR標準のGCと比べて結構差があります。注意しないとゲームのパフォーマンスがかなり悪くなります。UnityのGCはヒープ上のオブジェクトが多いほどメモリー回収時間が長くなります。これはゲームのフレームレートが急に下がったりする主な原因です。

メモリー自動回収のタイミングはランダムではなく、動的メモリー確保の時のみです。これはProfiler上のGC Allocで確認できます。最適化方法はUnityマニュアルに書いてありますが、初心者では気づきにくい罠もあります。

配列を返すUnity API

Unityのビルトイン型は一見普通のC#クラスですが、実際にはデータを持っていないエンジン内部オブジェクトのブリッジです。だからGetComponents<T>()Mesh.verticesなど配列を返すAPIは、毎回C#側で新しい配列を確保することになります。Updateの中とかで毎フレームこういうAPIにアクセスすると、GC回収の頻度は大きく上がります。

対策は配列を確保しないAPIを使うか配列をキャッシュして使いまわすかです。例えば

private List<Component> components = new List<Component>();

void Update()
{
    // 配列を確保していない
    GetComponents<Component>(components);
}
// 頂点データのキャッシュ
private List<Vector3> vertices;

void Start()
{
    vertices = new List<Vector3>(mesh.vertices);
}

void Update()
{
    // 頂点データを編集...
    vertices[0] = new Vector3(x, y, z);
    ...

    // データをUnityに渡す
    mesh.SetVertices(vertices);
}

foreachループの問題

C#でよく使われているforeachループも実際にはメモリーを確保しています。Updateなどで毎フレーム実行すると、フレームレートはかなり下がることもあります。過去には標準の.NETもこの問題に引っかかっていましたが、現在は直っています。Unityが使っているMonoコンパイラーが古いので、5.4までのバージョンではまだこの問題が修正されていません。5.5正式版が出るまでは、forループを使うか、手動でforeachを展開するしかありません。

Dictionary<TKey, TValue>の場合の展開方法を紹介します。

void Update()
{
    // Update中にforeach、GCが頻繁に発生
    foreach (var pair in dictionary)
    {
        var key = pair.Key;
        var value = pair.Value;
    }
}

void Update()
{
    // 手動でforeachを展開

    // デフォルトのenumeratorは値型なので、大丈夫
    var enumerator = dictionary.GetEnumerator();

    try
    {
        while (enumerator.MoveNext())
        {
            var pair = enumerator.Current;

            var key = pair.Key;
            var value = pair.Value;

            // ループの中身
        }
    }
    finally
    {
        enumerator.Dispose();
    }
}

IL2CPPとAOTコンパイル

IL2CPPはUnityが作ったC#のAOTコンパイルプラットフォームです。IL2CPPが誕生する前にはMono AOTでiOSなどAOTコンパイルが必要なプラットフォームをサポートしていました。Mono AOTでは、Linqがエラーになりやすいとか、C#のeventが使えないとかいろんな問題がありましたが、IL2CPPの時代では大抵直りました。しかしAOTコンパイルの根本的な制限はずっと存在しているので、注意しないといけません。回避策はUnityマニュアルにありますので、確認しておけば問題ありません。

コードストリッピング

IL2CPPによってもたらされた新しい問題もあります。IL2CPPはILコードをC++コードに変換する技術です。最終的に取得したバイナリコードのサイズはMono AOTよりかなり大きくなりました。アプリのサイズを減らすためにはバイナリコードのストリッピングが必須になります。つまり使われていないコードをアプリから削除するのです。

使われているかどうかは静的解析で判断するので、C#のリフレクションで動的に使われているコードは間違えて削除されるかもしれません。そしてこんなエラーが出ます。

MissingMethodException: Method not found: 'Default constructor not found...ctor() of <SomeType>.'

こういうコードを残すためにIL2CPPにヒントを渡さないといけません。方法は二つあります。

[Preserve] // 削除されないようにする
class ClassOnlyUsedByReflection
{

}

コードサイズ

IL2CPPがコンパイルしたバイナリコードはMonoより大きくなるので、C#上でもコードサイズを意識する必要があります。

  • インターフェースの使用を控えます。
  • ジェネリックと値型の組み合わせに注意します。値型は型ごとにコードが生成されますが、参照型なら使いまわせます。

まとめ

.NET世界の常識がUnityの世界では通用しないこともよくあるので、C#を勉強する以外に、Unityのマニュアルやブログなどをよくチェックするのも大事です。

明日の内容は非同期処理です。担当は清水になります。

お楽しみに。

【Docker】MySQLのフェイルオーバーをdocker-composeで構築

【Docker】MySQLのフェイルオーバーをdocker-composeで構築

自己紹介

はじめまして、入社新米のおかむーです🍙
  • 23歳です
  • clでサーバーサイドを担当しています
  • 絶賛勉強中です
  • 初めての投稿なので、お手柔らかにお願いします。

概要

  • MHA, master1, master2, slave1, slave2 それぞれのコンテナをdocker-composeでオーケストレーション
  • MHAコンテナがMySQLのmasterコンテナのを監視し、障害発生時などにフェイルオーバーを行う

image

経緯

  • MySQL MHAを使ったフェイルオーバーの環境構築サイトはいくつかあったので、理解のついでにdocker-composeでオーケストレーションをしたかった
  • レプリケーションが組めるdocker-composeのサンプルなどはすでにあったが、フェイルオーバーまでなかった
  • MySQLレベルの向上のため
  • docker普及のため
  • 最終的には、Rancherでの運用を目指す

MySQL MHA

MySQL MHAとは

  • (Master High Availability Manager and tools for MySQL) の略
  • MySQLマスタ障害発生時に、MySQLマスタの自動フェイルオーバーを行い高可用性を実現するオープンソースツール
  • Perlで実装されている
  • MySQLのマスタをフェイルオーバーさせ、スレーブをマスタに昇格させる作業を短時間(10〜30秒程度)で実行できる

主な機能

フェイルオーバー機能

自動
  • MHAマネージャがマスタを監視し、マスタに障害が発生するとフェイルオーバーさせます。最新のDB情報を反映しているスレーブから、その他のスレーブに対して差分を適用する動作を行うため、スレーブ間のデータ整合性が保証されます。
  • MHA自身では、MySQLのサービス監視せず、監視作業を他の監視ソフトウェアで行い、監視ソフトウェアからの通知によりフェイルオーバーを実行します。
手動
  • 手動でのマスタフェイルオーバーも可能

 マスタ切り替え機能

  • オンラインでマスタを別ホストに切り替える機能です。0.5〜2秒ほどの書き込みブロックのみで切り替え可能です。

マスタ昇格指定機能

  • 「特定スレーブのマスタ昇格(昇格不可)指定」が可能です。

動作条件

  • rootで各ノード間でのSSH公開鍵認証が可能であること (私はこれを見逃していた...)
  • MySQL 5.0 以降のみサポート
  • マスタ昇格候補のサーバでlog-binがenableであること
  • マスタ昇格候補サーバにレプリケーション用ユーザが存在すること
  • ステートメントベースレプリケーションでは「LOAD DATA INFILE」の使用は禁止

結論

  • 各ノード間でのSSH公開鍵認証が可能」というところを見ていなく、ハマってしまった。結局、時間(見積もり)が足りず未完成です。ごめんなさい
  • 「いいね」で応援していただければ励みになります泣
  • てっきりlinksでどうにかなるものかと思っていたところが誤算
  • 修正次第、更新します。
  • ですので、以下は今の現状です(アドバイス等あればください)

ベースimageを作る

  • ベースimageを元に、フェイルオーバー環境を構築できるよう設計

docker image

  • 今回は各コンテナにはperl環境が必要
  • master, slaveにはmha-nodeがinstallされていないといけない

IMG_8767.JPG 手書きです。ごめんなさい!

0.基本環境(vvdocker/base-perl)

FROM ubuntu

RUN apt-get -y update
RUN apt-get -y install wget git

# MHA NODE
RUN apt-get -y install build-essential devscripts dh-make fakeroot dpkg-dev debhelper libdbi-perl libmysqlclient-dev zlib1g-dev
RUN apt-get -y install libmodule-install-perl

RUN wget http://archive.ubuntu.com/ubuntu/pool/universe/libd/libdbd-mysql-perl/libdbd-mysql-perl_4.033.orig.tar.gz
RUN wget http://archive.ubuntu.com/ubuntu/pool/universe/libd/libdbd-mysql-perl/libdbd-mysql-perl_4.033-1build2.debian.tar.xz

RUN git clone https://github.com/yoshinorim/mha4mysql-node
WORKDIR mha4mysql-node
RUN perl Makefile.PL
RUN make
RUN make install

1.MHA(vvdocker/manager)

FROM vvdocker/base-perl

RUN apt-get install -y libconfig-tiny-perl liblog-dispatch-perl libparallel-forkmanager-perl

# MHA manager
RUN apt-get install -y libconfig-tiny-perl liblog-dispatch-perl libparallel-forkmanager-perl
RUN git clone https://github.com/yoshinorim/mha4mysql-manager.git
WORKDIR mha4mysql-manager
RUN perl Makefile.PL
RUN make
RUN make install

RUN mkdir -p /var/log/masterha/mysql/
RUN cat /usr/local/share/perl/5.20.2/MHA/ServerManager.pm

RUN cp -p samples/scripts/master_ip_failover /usr/local/bin/
RUN chmod 555 /usr/local/bin/master_ip_failover
RUN mkdir -p /etc/masterha/conf
#サンプル----------------------------------------
ADD test.conf /etc/mha.conf
#----------------------------------------------

2.node(vvdocker/node)

FROM vvdocker/base-perl
RUN mkdir work
RUN cp libdbd-mysql-perl_4.033.orig.tar.gz work
WORKDIR work
RUN tar xf libdbd-mysql-perl_4.033.orig.tar.gz
RUN mv DBD-mysql-4.033 libdbd-mysql-perl_4.033
RUN tar xf ../libdbd-mysql-perl_4.033-1build2.debian.tar.xz -C libdbd-mysql-perl_4.033
RUN cd libdbd-mysql-perl_4.033 && dpkg-buildpackage -us -uc
RUN dpkg -i libdbd-mysql-perl_4.033-1build2_amd64.deb

#mysql5.6
RUN wget http://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.14-debian6.0-x86_64.deb
RUN dpkg -i mysql-5.6.14-debian6.0-x86_64.deb
RUN apt-get -y upgrade
RUN apt-get install -y libaio1 m4

RUN install -d /data/mysql
RUN /opt/mysql/server-5.6/scripts/mysql_install_db --datadir=/data/mysql
ENV PATH "/opt/mysql/server-5.6/bin:$PATH"
ENV MANPATH "/opt/mysql/server-5.6/man:$MANPATH"
RUN mysql --version

これらをdocker-hubあたりにpushしておく

$ docker push vvdocker/manager
$ docker push vvdocker/node

実行

  • 実際に環境を作りたいときはここからできるようにと

docker-compose.yml

mha:
  image: vvdocker/manager:dev22
  links:
    - master1:master_1
    - master2:master_2
    - slave1:slave_1
    - slave2:slave_2
  command: masterha_check_repl --conf=/etc/mha.conf && masterha_manager --conf=/etc/mha.conf

master1:
  build: master
  environment:
    - MYSQL_USER=repl
    - MYSQL_PASSWORD=replpass
    - MYSQL_ROOT_PASSWORD=root

master2:
  build: slave
  environment:
    - MYSQL_USER=repl
    - MYSQL_PASSWORD=replpass
    - MYSQL_ROOT_PASSWORD=root
  links:
    - master1:master_1

slave1:
  build: slave
  environment:
    - MYSQL_USER=repl
    - MYSQL_PASSWORD=replpass
    - MYSQL_ROOT_PASSWORD=root
  links:
    - master1:master_1
    - master2:master_2

slave2:
  build: slave
  environment:
    - MYSQL_USER=repl
    - MYSQL_PASSWORD=replpass
    - MYSQL_ROOT_PASSWORD=root
  links:
    - master1:master_1
    - master2:master_2

master(vvdoker/master)

FROM vvdocker/node

# my.cnf
COPY conf.d/my.cnf.in my.cnf.in
RUN m4 -DMASTERID=`date +%s` my.cnf.in > /etc/mysql/conf.d/my.cnf

COPY init.d/startup.sql /docker-entrypoint-initdb.d/startup.sql

startup.sql

GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%' IDENTIFIED BY 'replpass';
FLUSH PRIVILEGES;

my.cnf.in

[mysqld]                           
~~~~~~~~~~省略~~~~~~~~~~~~~~~~
server-id               = MASTERID
log_bin                 = /var/log/mysql/mysql-bin.log
bind-address            = 0.0.0.0
~~~~~~~~~~省略~~~~~~~~~~~~~~~~

slave(vvdocker/slave)

FROM vvdocker/node

# my.cnf
COPY conf.d/my.cnf.in my.cnf.in
RUN m4 -DSERVERID=`date +%s` my.cnf.in > /etc/mysql/conf.d/my.cnf

COPY init.d/startup.sql /docker-entrypoint-initdb.d/startup.sql

startup.sql

CHANGE MASTER TO MASTER_HOST='master_1', MASTER_USER='repl',MASTER_PASSWORD='replpass', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=120;
FLUSH PRIVILEGES;

my.cnf.in

[mysqld]                           
~~~~~~~~~~省略~~~~~~~~~~~~~~~~
server-id               = SERVERID
log_bin                 = /var/log/mysql/mysql-bin.log
bind-address            = 0.0.0.0
~~~~~~~~~~省略~~~~~~~~~~~~~~~~

現状

  • mysqlコンテナは4つ立つ
  • sshでMHAからmysqlへのアクセスができてないので、フェイルオーバーができていない。

フェイルオーバーのテスト

  • 負荷をかけたり
  • 手動昇格確認などしたかった

課題点

  • 見積もり不足で時間が足りなかった
  • MHAの仕様を把握できていなかった
  • 知識不足!ベストプラクティスがわからない

感想

  • 良い勉強になった
  • 12月って忙しい!!!

参考サイト

次回予告

  • 次回はキーボードで有名な長田さんです、とても楽しみにしています!

追記

年末までにできなかったところを、検証しました

github.com に最終バージョンがあります

実際に

$ cd sample
$ docker-compose up

すると

f:id:okamuro:20161231162435p:plain

こんな感じになりました

$ docker psでmhaにexecしたあとに

mhaコンテナ内で

#  masterha_check_repl --conf=/etc/mha.conf 

を叩くと、レプリケーションのチェックをしてくれます