Quantcast
Channel: Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)
Viewing all 223 articles
Browse latest View live

急いで学ぶElixir#3: 演算子編

$
0
0

Basic Operators

前回、Elixirは四則演算があることと、整数の商や余剰を得るためにdivremがあることを学びました。今回はElixirの基本的な演算子を、Rubyと少し比較しながら学んでいきましょう。

Elixirでは、++や–を配列に対しても使うことが出来ます。

iex> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]
iex> [1,2,3] -- [2]
[1,3]

文字列の連結は<>で可能です。(Rubyでは+だけで文字列を連結出来ますね)

iex&gt; "foo" &lt;&gt; "bar"
"foobar"

Elixirは3つのbooleanオペレーターorandnotを提供しています。これらのオペレーターを使うためには、初項がboolean(true/false)でなければなりません。

iex&gt; true and true
true
iex&gt; false or is_atom(:example)
true

初項がbooleanでない場合は、例外が起きます。

iex&gt; 1 and true
** (ArgumentError) argument error

Rubyでは例外が起きずに、数が来たらtrueと評価するので、ここはElixirがより厳密に型を扱っていることが良くわかりますね。

irb(main):001:0&gt; 1 and true
=&gt; true

orandは短絡オペレーターです。もし左側だけで結果を決定できないときのみ、右側も実行します。(例えばfalse andの場合は右側に何がきてもfalseなのでfalseとなります。)

iex&gt; false and raise("This error will never be raised")
false

iex&gt; true or raise("This error will never be raised")
true

ちなみにElixirにおけるandorは、Erlangのandalsoorleseと同一です。

これらのbooleanオペレーターと異なり、Elixirでは||&&!はどんな型に対しても受け付けます。この場合、falsenil以外はtrueとして評価されます:

# or
iex&gt; 1 || true
1
iex&gt; false || 11
11

# and
iex&gt; nil &amp;&amp; 13
nil
iex&gt; true &amp;&amp; 17
17

# !
iex&gt; !true
false
iex&gt; !1
false
iex&gt; !nil
true

おおざっぱにまとめると、以下の通りです。

  • booleanであることを期待 => and, or, not
  • booleanでない引数がきて良い => ||, &&, !

Elixirでは比較演算子として、==!====!==< =>=<>を提供しています。

iex&gt; 1 == 1
true
iex&gt; 1 != 2
true
iex&gt; 1 &lt; 2
true

=====の違いは、整数と浮動小数を比較するときに、後者の方がより厳密です。

iex&gt; 1 == 1.0
true
iex&gt; 1 === 1.0
false

Rubyの場合、===はサブクラスで再定義されない限り==と同一なので、===そのものにこういった厳密性の意味はありません。

Elixirでは、2つのデータの型を比較可能です。(ここもRubyとはっきり異なるところです)

iex&gt; 1 &lt; :atom
true

異なるデータの型を比較可能な理由は実用主義によるものです。ソートアルゴリズムで異なるデータ型同士で順序付けについて心配する必要はありません。ソート順は以下のように定義されています:

number &lt; atom &lt; reference &lt; functions &lt; port &lt; pid &lt; tuple &lt; maps &lt; list &lt; bitstring

正確にこの順番を覚える必要はありませんが、順番が存在することだけ知っているのは重要です。

次からは基本的な関数、型変換、ちょっとしたフロー制御について議論します。

元記事

http://elixir-lang.org/getting-started/basic-operators.html


resize2fsコマンドの先でカーネルは何をしているのか

$
0
0

背景

前回の記事で、resize2fsコマンドがどのように1秒未満での容量拡張を実現しているかを知るために、resize2fsコマンドのソースを調査しました。その結果、メタデータの一つであるGlobal Descriptor Tables(GDT)をカーネル内で更新しているからではないか、という示唆を得られました。今回は、実際にカーネルのコードを読んで、この示唆が正しかったことを見ていきたいと思います。

調査対象

今回も新しめのカーネルで調査しました。Amazon Linuxとは多少ソースが異なっているかもしれませんが、本質は大きく変わらないかと思います。

  • ファイルシステム: ext4
  • Linux kernel version: 4.1.0-rc7

前回の復習

前回調べたときから約1年空いてしまいましたので、軽く前回の復習をします。

ext4のファイルシステムでは、ブロックグループという単位で複数のブロックをグループ化してデータを管理しています。ブロックグループの中にはGroup Descriptor Tables(GDT)というブロック領域があり、ここに全ブロックグループを管理するための情報であるグループディスクリプタを格納しています。全体のブロックグループ数が多ければ多いほど、GDT領域が大きくなります。前回、resize2fsコマンド経由でGDT領域を更新しているのではないか、という示唆を得ました。

ext4_bg

GDT領域で扱うサイズより大きなファイルサイズにオンラインで拡張したいときはどうすれば良いのでしょう?ext4ではこういうときのために、予約済のGDT領域(RGDT)をブロックグループの中に用意しています。オンラインでファイルシステムがリサイズ出来るのは、このRGDTを備えているからと言えます。実際、resize2fsコマンド内ではRGDTで管理しきれるファイルサイズにしようとしているかをチェックしていました。

resize2fsコマンドでは、カーネルにファイルシステムのサイズを変更させるために、EXT4_IOC_RESIZE_FSリクエストのioctl(2)を発行していました。今回の記事ではこのioctl(2)の発行後に、カーネルは何をやっているのかを調べていきます。

リサイズリクエストのioctl(2)の処理内容

ioctl(2)のEXT4_IOC_RESIZE_FSリクエストに関するソースコードから読んでいきたいと思います。繰り返しますが、今回のゴールは容量拡張のメイン処理が何であるかを明らかにすることです。

まずEXT4_IOC_RESIZE_FSでgrepしてみると・・・fs/ext4/ioctl.cにありました。ここから読み進めてみましょう。

554      case EXT4_IOC_RESIZE_FS: {
555           ext4_fsblk_t n_blocks_count;
556           int err = 0, err2 = 0;
557           ext4_group_t o_group = EXT4_SB(sb)-&gt;s_groups_count;
558
559           if (EXT4_HAS_RO_COMPAT_FEATURE(sb,
560               EXT4_FEATURE_RO_COMPAT_BIGALLOC)) {
561                ext4_msg(sb, KERN_ERR,
562                "Online resizing not (yet) supported with bigalloc");
563                return -EOPNOTSUPP;
564           }

前回、resize2fsでは-fを付ければbigalloc(ページサイズ以上のブロック単位をサポートする機能)でもresizeできるかのように見えましたが、最新バージョンのカーネルでもサポートされていないようです。not (yet)なので、今後に期待しましょう。

566           if (copy_from_user(&amp;n_blocks_count, (__u64 __user *)arg,
567                                          sizeof(__u64))) {
568                return -EFAULT;
569           }
570
571           err = ext4_resize_begin(sb);
572           if (err)
573                return err;

ext4_resize_begin()では、現在リサイズしようとしているファイルシステムに対して同時にリサイズ命令しないように、メモリ内のスーパーブロック情報(ext4_sb_infoオブジェクト)にこのファイルシステムはresize中であるというフラグ(EXT4_RESIZE)を立てるという処理をします。

575           err = mnt_want_write_file(filp);
576           if (err)
577                goto resizefs_out;

mnt_want_write_file()では、通常パスでは2つの関数が呼ばれます。

1つ目はsb_start_write()です。この関数は他のプロセスによる書き込みを排除するために呼ばれます。具体的には、ファイルシステムを書き込みfreeze状態(SB_FREEZE_WRITE)とします。freeze状態とは、ファイルシステム用に用意されたロック機構の1つであり、一番緩いロック(=ある程度の競合を許すロック)です。freeze状態にはいくつかレベルが用意されています。レベルが低い順にフローズしていない状態(SB_UNFROZEN)、書き込みfreeze状態(SB_FREEZE_WRITE)、ページフォルトのfreeze状態(SB_FREEZE_PAGEFAULT)、内部のファイルシステム使用freeze状態(SB_FREEZE_FS)、完全なfreeze状態(SB_FREEZE_COMPLETE)です。sb_start_write()を使用する場合、freeze状態が解けるまでsleepして待ち続けます。

2つ目は__mnt_want_write_file()です。この関数はそのファイルシステムへwriteアクセスすることを記録するために呼ばれます。ただ、もしread onlyでファイルシステムがマウントされている場合はエラー(EROFS)となります。エラーとなったときは、書き込みfreeze状態を解除します。

579           err = ext4_resize_fs(sb, n_blocks_count);

いよいよ、今回のメインの関数である、ext4_resize_fs()について見ていきましょう。まず渡している引数を見ていきます。第一引数のsbとはresizeされるファイルシステムのsuperblockの構造体(super_block)です。superblockとは、ファイルシステム全体を管理しているブロックのことです。このsuper_block構造体は全てのファイルシステムで使われる一般的な形式となっており、ファイルシステム特有の情報(private情報)はs_fs_infoにvoid型のポインタとして格納されています。ext4_resize_fs()の第二引数n_block_countは、resize2fsコマンドから渡って来た追加分を含む新しいブロック数です。

ext4_resize_fs()

それでは実際に、ext4_resize_fs()で重要な処理に着目して、何をやっているか理解しましょう。

1896          o_blocks_count = ext4_blocks_count(es);
1897
1898          ext4_msg(sb, KERN_INFO, "resizing filesystem from %llu "
1899          "to %llu blocks", o_blocks_count, n_blocks_count);
1900
1901          if (n_blocks_count &lt; o_blocks_count) {
1902                  /* On-line shrinking not supported */
1903                  ext4_warning(sb, "can't shrink FS - resize aborted");
1904                  return -EINVAL;
1905          }
1906
1907         if (n_blocks_count == o_blocks_count)
1908                 /* Nothing need to do */
1909                 return 0;

ここでは、現在のブロック数o_blocks_countをsuper blockから読み出し、新しいブロックサイズn_blocks_countと比較しています。onlineでリサイズする場合はshrinkがサポートされていないことがわかります。

1911         n_group = ext4_get_group_number(sb, n_blocks_count - 1);
1912         if (n_group &gt; (0xFFFFFFFFUL / EXT4_INODES_PER_GROUP(sb))) {
1913                 ext4_warning(sb, "resize would cause inodes_count overflow");
1914                 return -EINVAL;
1915         }
1916         ext4_get_group_no_and_offset(sb, o_blocks_count - 1, &amp;o_group, &amp;offset);

次に、新しいブロックサイズのグループ数n_groupと現在のブロックサイズのグループ数o_group、グループ内のデータを取得しています。新しいグループ数がものすごく大きな値になっていたらここで-EINVALが返ります。

1918         n_desc_blocks = num_desc_blocks(sb, n_group + 1);
1919         o_desc_blocks = num_desc_blocks(sb, sbi-&gt;s_groups_count);

ここは、Group Descriptor Tableが何ブロック必要かを、新旧取得しています。

1923         if (EXT4_HAS_COMPAT_FEATURE(sb, EXT4_FEATURE_COMPAT_RESIZE_INODE)) {
1924                 if (meta_bg) {
1925                         ext4_error(sb, "resize_inode and meta_bg enabled "
1926                         "simultaneously");
1927                         return -EINVAL;
1928                 }
1929                 if (n_desc_blocks &gt; o_desc_blocks +
1930                     le16_to_cpu(es-&gt;s_reserved_gdt_blocks)) {
1931                         n_blocks_count_retry = n_blocks_count;
1932                         n_desc_blocks = o_desc_blocks +
1933                                 le16_to_cpu(es-&gt;s_reserved_gdt_blocks);
1934                         n_group = n_desc_blocks * EXT4_DESC_PER_BLOCK(sb);
1935                         n_blocks_count = n_group * EXT4_BLOCKS_PER_GROUP(sb);
1936                         n_group--; /* set to last group number */
1937                 }
1938
1939                 if (!resize_inode)
1940                         resize_inode = ext4_iget(sb, EXT4_RESIZE_INO);
1941                 if (IS_ERR(resize_inode)) {
1942                         ext4_warning(sb, "Error opening resize inode");
1943                         return PTR_ERR(resize_inode);
1944                 }
1945         }

resize2fsコマンドからオンラインresizeする場合は、resize_inode機能を必須とするので、実質このブロックは必ず通ることになります。

meta_bgについての細かい説明は前回の記事を参考にしていただきたいですが、meta_bgを使用しているときはresizeがサポートされていないことがわかります。

次のチェックは、今から拡張しようとしているファイルシステムを管理するためのGDTが既存のGDTとRGDTで収まりきれるかを確認しています。収まりきれなかった場合は、resize2fsコマンド内でこの拡張操作を禁止していましたが、カーネル内ではRGDTの最大限まで確保するような処理となるようです。

最後にresize_inodeを取得してきます。このinode番号EXT4_RESIZE_INOは7番となります。

1962         /* extend the last group */
1963         if (n_group == o_group)
1964                 add = n_blocks_count - o_blocks_count;
1965         else
1966                 add = EXT4_BLOCKS_PER_GROUP(sb) - (offset + 1);
1967         if (add &gt; 0) {
1968                 err = ext4_group_extend_no_check(sb, o_blocks_count, add);
1969                 if (err)
1970                        goto out;
1971         }

追加ブロックがある場合は、ext4_group_extend_no_check()で、ext4_group_extend_no_check()のコメントを読むと最後のブロックグループに新しいブロック分を追加処理とのことです。
どうやらこの関数が私たちの目的の関数のようです。この関数を見ていきましょう。

ext4_group_extend_no_check()

ext4_group_extend_no_check()を読み進めると・・・ありました。

1670         ext4_blocks_count_set(es, o_blocks_count + add);
1671         ext4_free_blocks_count_set(es, ext4_free_blocks_count(es) + add);
1672         ext4_debug("freeing blocks %llu through %llu\n", o_blocks_count,
1673                         o_blocks_count + add);

super blockが管理しているblock数とfree block数を、super blockに更新しています。

1674         /* We add the blocks to the bitmap and set the group need init bit */
1675         err = ext4_group_add_blocks(handle, sb, o_blocks_count, add);

このext4_group_add_blocks関数はGDTを更新しているように思えますね。
実際のところはどうなっているでしょうか?

ext4_group_add_blocks()

ext4_group_add_blocks()を見ると・・・ありました!

4985         blk_free_count = blocks_freed + ext4_free_group_clusters(sb, desc);
4986         ext4_free_group_clusters_set(sb, desc, blk_free_count);

descはGDTの構造体のポインタで、blocks_freedが追加されるブロック数です。
確かに前回予想した、GDTの更新をしていることがわかります。

まとめ

resize2fsのカーネル処理は、以下のような処理をしていることがわかりました。
1. ユーザー空間からioctl(2)のEXT4_IOC_RESIZE_FSリクエストでresize2fsのカーネルの処理を実行
2. 同時リサイズを避けるために、ファイルシステムにリサイズ中であるフラグを立てる
3. super blockの全ブロック数と全フリーブロック数を更新
4. GDTを更新

オンラインresize2fsがなぜ高速で終わるのか?という疑問に対して、スーパーブロックのブロック数の管理領域とGDTを更新しているだけだからだ、というのがわかりました。

この記事には書きませんでしたが、実はext4_resize_fs()の処理の後に、lazy initializationという機能を使い、未初期化のinode tableをext4lazyinitカーネルスレッドに非同期に初期化させるということもします。少しでも高速化するように色々な工夫がなされていることがわかります。

いくつか読み飛ばした処理もあるので、もっと深く知りたい方は本記事で取り上げた処理以外の部分を読んでみてはいかがでしょうか。

Electronで社内ツール作ってみた

$
0
0

はじめに

アカツキにて内定者インターンをしているsachaosです。
この記事ではプロジェクトに配属され、少しの間アシスタントディレクター業を行っていた僕が、
エンジニアの端くれなりに社内ツールを作って業務プロセスの無駄な部分を自動化した話をします。

アシスタントディレクター業

アシスタントディレクター業として僕に任せられたのは、とあるゲーム内のお知らせを作成する業務でした。
お知らせとはゲームに追加された施策(イベント・ガチャ)の内容や、メンテナンスの時間等をユーザに伝えるものです。
お知らせはHTMLで書かれていて、タイトル、表示開始日時、表示終了日時などの値と共にデータベースに保存されています。
また、お知らせには画像も含まれていてアカツキではAWS S3に保存しています。

改善前の業務プロセス

お知らせの作成では以下のような業務プロセスが取られていました。

  1. HTMLを書く
  2. Sequel Pro(GUIのDBクライアント)を立ち上げてレコードに手動で入れる
  3. Cyberduck(GUIのFTPクライアント)を立ち上げて画像をアップロード
  4. 実機で表示が正しいか確認。

このプロセスを複数回繰り返していくことでお知らせを作成していくのですが、
手動でレコードを挿入・更新したりする必要があり時間がかかる。
また、重いソフトウェアを立ち上げておかなければならないので、
なかなかストレスフルだという問題がありました。
なにより、レコードを直接弄るといろいろな人為的ミスも増えます。
レコードを弄るのは人間の仕事ではありません

お知らせ更新ツールの作成

上記のようなプロセスはなかなかつらいので、
CLIのスクリプトを書き自動化していたのですが、
後任のお知らせ作成業務をするディレクターに作業を引き継ぐ必要がでてきましたので、
引き継ぎやすいようにGUIのツールを作成することにしました。

エンジニアかつミーハーである僕は、
今流行りのElectronを使用してGUIツールを作成することにしました。

Electron?

ElectronとはGitHub社が提供しているアプリケーションフレームワークです。
同じくGitHubが提供しているエディタのAtomやSlackのアプリケーション、Kobito for Windowsで使用されていることで有名です。
内部はChromium + node.jsとなっていてWebの技術(HTML, CSS, Javascript)で、スタンドアローンアプリケーションを作成することができます。
Mac, Windows, Linuxにクロスコンパイル可能となっているので、使用者の環境を考える必要がありません。

お知らせのをHTMLファイル一つで管理する

ツールの作成にあたり、お知らせに付随する情報(タイトル、表示開始日時、表示終了日時など)をHTMLファイル一つに管理した方が便利だと思いましたので、
metaタグとしてHTMLファイルの中に上記の情報を埋め込んで管理することにしました。

&lt;meta name="title" content="超重要なお知らせ!!!" /&gt;
&lt;meta name="start_at" content="2015-08-01 15:00" /&gt;
&lt;meta name="end_at" content="2015-12-6 23:59" /&gt;
&lt;!-- 以下お知らせの内容 --&gt;

HTMLファイルが入力された時にパースしてmetaタグの中身を取り出し、その値を元にレコードを登録・更新します。

ドラッグ&ドロップでお知らせを更新

お知らせが書かれたHTMLと必要な画像を入れたフォルダをドラッグ&ドロップするだけで,
HTMLはDBに保存し画像をAWS S3へアップロードするアプリケーションを作成しました。
こんな感じです。

画像

人間の仕事っぽいですね。

改善前の業務プロセスでは一回の更新作業で、
どうしても2分程度かかってしまっていましたが、
これにより3秒で更新することができるようになりました。

時間を短縮することで本来するべきお知らせの内容等に、
時間をかけることができるようになりました。

Electronどうだった?

メリット

Webの技術で簡単にスタンドアローンのアプリが作成できる。

ビューの作成もHTML、CSSで簡単に作成することができますし、
デバッグもChromiumの開発者ツール上で行えます。
またnode.jsまわりの既存の資産を活用できるので、
簡単に強力なアプリケーションを作成することができます。

使用者の環境を整える必要がない。引き継ぎが楽。

スタンドアローンなので、環境構築・バージョン管理を意識させず
「このアプリをDropBoxから落としてきてー」というだけで
アプリを使用させることができます。
また、クロスプラットフォーム(Mac, Linux, Windowsへ吐き出せる)なので、
使用者のOSを考える必要がなく、様々なOSが混在するような社内環境でも問題ありません。

デメリット

謎な挙動がある。

tmuxを通して立ち上げると文字入力できない等、
たまに謎な挙動に出くわしました。
しかし困ったことがあっても大体ググれば問題は解決できます。
解決できなければコントリビュートするチャンスです!

吐き出されたものが無駄に大容量。

結局Chromiumを内蔵しているので、
アプリケーションにした物はブラウザくらいのファイルの大きさになります。

まとめ

  • 社内GUIツールを作って業務改善しました。
  • Electron、GUIツールの作成におすすめです。

社内ツールを作るのはエンジニア的には楽しいですし、業務も改善することができます。
Electronを使用すれば簡単にこういったツールを作ることができるのでおすすめです。
もし、社内の業務でイケてない部分があれば、ツールの作成で解決することができるかもしれません。
ぜひ、試してみてはいかがでしょうか?

急いで覚えるElixir: 制御構文編

$
0
0

Elixirの基本制御構文

前回の記事から続いて、今回はElixirで利用する基本的な制御構文について学んでいきます。

if, unless/else

他のプログラミング言語で親しまれているif~elseは、Elixirでは以下のように記述します。

iex&gt; if true do
...&gt;   "みえる"
...&gt; else
...&gt;   "みえない"
...&gt; end
"みえる"

ifをunlessに変えることで、条件を反転させることが出来ます。

case

case文は以下のように記述します。

iex&gt; case {1, 2, 3} do
...&gt;   {4, 5, 6} -&gt;
...&gt;     "この条件にはマッチしない"
...&gt;   {1, x, 3} -&gt;
...&gt;     "この条件にマッチして、この文のスコープ変数xに2が代入される"
...&gt;   _ -&gt;
...&gt;     "_ はどの条件にもマッチする"
...&gt; end
"この条件にマッチして、この文のスコープ変数xに2が代入される"

cond

case文は幾つかの値の中でマッチするのもを探しだすのに便利ですが、幾つかの条件の中で最初にマッチするものを探し出したい場合もあります。 このようなときは、cond文を使用します。

iex&gt; cond do
...&gt;   2 + 2 == 5 -&gt;
...&gt;     "ちがう"
...&gt;   2 * 2 == 3 -&gt;
...&gt;     "これもちがう"
...&gt;   1 + 1 == 2 -&gt;
...&gt;     "これだ!"
...&gt; end
"これだ!"

上から評価されますが、最後までマッチされなかった時はエラーになります。

iex&gt; cond do
...&gt; 1 + 2 == 2 -&gt;
...&gt; "wow"
...&gt; end
** (CondClauseError) no cond clause evaluated to a true value

do/end ブロック

Elixirもrubyのようにdo/endでブロックを定義することが出来ます。
do/endブロックで注意しないといけないのは、もっとも外側の関数の呼び出しに対してバインドされる点です。すなわち、以下の例は、

iex&gt; is_number if true do
...&gt;  1 + 2
...&gt; end

以下のようにパースされることになります。

iex&gt; is_number(if true) do
...&gt;  1 + 2
...&gt; end
** (RuntimeError) undefined function: if/1

また、ブロック内で定義された変数などは、ブロック外からはスコープ外になります。

raise

Elixirもrubyのようにraise句でエラーを発生させることが出来ます。
デフォルトはRuntimeErrorです。

iex&gt; raise "oops"
** (RuntimeError) oops

エラーを独自に定義して、raiseの引数で指定することも出来ます。

iex&gt; defmodule MyError do
iex&gt;   defexception message: "default message"
iex&gt; end
iex&gt; raise MyError, message: "custom message"
** (MyError) custom message

rescue

エラーを補足するにはrescue句を利用します。

iex&gt; try do
...&gt;   raise "oops"
...&gt; rescue
...&gt;   e in RuntimeError -&gt; e
...&gt; end
%RuntimeError{message: "oops"}

throw/catch

Elixirで大域脱出を行うにはthrow/catchを利用します。
throwで値を投げ、catchで補足します。

iex&gt; try do
...&gt;   Enum.each -50..50, fn(x) -&gt;
...&gt;     if rem(x, 13) == 0, do: throw(x)
...&gt;   end
...&gt;   "Got nothing"
...&gt; catch
...&gt;   x -&gt; "Got #{x}"
...&gt; end
"Got -39"

after

after句で、rubyのensureのように、try句の終了直前に必ず実行する処理を記述することが出来ます。

iex&gt; try do
...&gt;   IO.puts "try!"
...&gt;   raise   "raise!!"
...&gt; after
...&gt;   IO.puts "after!!!"
...&gt; end
try!
after!!!
** (RuntimeError) raise!!

exit

すべてのElixirコードは互いに通信し合うプロセス上で動いています。
プロセスが「自然死」する場合(例えば未処理の例外が起きた時)、exitシグナルを送ります。
また、手動でexitシグナルを送ることでプロセスを殺すこともできます。

try do
  exit "急いで覚える"
catch
  :exit, _ -&gt; "Elixir"
end
"Elixir"

exitは前述のtry/catchで補足することも出来ます。

まとめ

Elixirの基本的な制御構文を学ぶことが出来ました。
より詳しく知りたい方は、以下の公式ドキュメントからよりカッコイイ書き方を知ることが出来ます。(外部サイトへ飛びます。)

急いで覚えるElixir: Enumerable編

$
0
0

ElixirのEnumerable

前回の記事から続いて、今回はElixirで利用する基本的な制御構文について学んでいきます。

Keyword list

多くの関数型プログラム言語では、2要素のtupleによって関連付けられたデータ構造を表現します。
Elixirでは、最初の要素がAtomであるTupleのListのことをKeyword listと呼びます。

iex&gt; list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex&gt; list == [a: 1, b: 2]
true
iex&gt; list[:a]
1

キーが重複している場合、先頭に格納された値が優先的に読まれます。

iex&gt; new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex&gt; new_list[:a]
0

Keyword listは以下の重要な性質を持ちます。

  • キーはAtomでなければならない
  • キーはユーザーが指定した順に並ぶ
  • キーが重複可能である

Map

Key value storeが必要なときは、迷わずMapを使うといいでしょう。
Mapは %{} 構文により作成します。

iex&gt; map = %{:a =&gt; 1, 2 =&gt; :b}
%{2 =&gt; :b, :a =&gt; 1}
iex&gt; map[:a]
1
iex&gt; map[2]
:b
iex&gt; map[:c]
nil

Keyword list との違いは以下の点です。

  • MapはKeyの型を限定しない(Atomでなくてもいい)
  • 順序構造がない

Dict

Elixirでは、Keyword listとMapはいずれもDictionaryとして分類されます。
Keyword listとMapは一見rubyのHashのようですが、rubyのHashのように後から値を追加するために、[]を利用することは出来ません。
新たに値を追加するには、次のように、Dict.put/3を利用します。

iex&gt; keyword = []
[]
iex&gt; map = %{}
%{}
iex&gt; Dict.put(keyword, :a, 1)
[a: 1]
iex&gt; Dict.put(map, :a, 1)
%{a: 1}

Enumerable

ElixirではEnumerableがサポートされていて、ListとMapがEnumerableに該当します。

iex&gt; Enum.map([1, 2, 3], fn x -&gt; x * 2 end)
[2, 4, 6]
iex&gt; Enum.map(%{1 =&gt; 2, 3 =&gt; 4}, fn {k, v} -&gt; k * v end)
[2, 12]

Enumモジュールは、Enumerableの要素を変形・ソート・グループ化・フィルター・取得するための多数の関数を提供していて、Elixirで書かれたコードでよく登場するモジュールです。
Enumモジュールは多数のデータ型をサポートする設計で作られているため、そのAPIはデータ型に依存しない関数に限られています。
より具体的な操作のためには、そのデータ型に対応したモジュールを使用しないといけない場合もあります。例えば、Listの中の特定の位置に要素を挿入したい場合、
Listモジュールで定義されたList.insert_at/3関数を使用します。

パイプ演算子

上記の例の|>記号はパイプ演算を表しています。この演算では、Unixの|演算子のように左辺を評価した後、右辺の第一引数として渡します。
パイプ演算子は複数の関数をまたがるデータの流れをわかりやすく記述するために用いられます。

iex&gt; Enum.sum(Enum.filter(Enum.map(1..100_000, &amp;(&amp;1 * 3)), odd?))
7500000000

パイプを使うと上のような煩雑な処理を次のように書き換えることが出来ます。

iex&gt; 1..100_000 |&gt; Enum.map(&amp;(&amp;1 * 3)) |&gt; Enum.filter(odd?) |&gt; Enum.sum
7500000000
iex&gt; 1..100_000 |&gt; Stream.map(&amp;(&amp;1 * 3)) |&gt; Stream.filter(odd?) |&gt; Enum.sum
7500000000

ここでEnumStreamという2つのモジュールが新たに出てきます。
その違いは、EnumはEagerに演算を行い、StreamはLazyな演算を行うことが出来ます。
例えば上の例では、StreamEnumモジュールに渡された時に演算が初めて実行されることになります。
Lazyに評価する事の利点は、例えばStreamを実際に演算しなくていい条件がある場合、その演算に利用する巨大なオブジェクトをはじめから保持する必要が無いことにあります。

Generator

ElixirではEnumerableに対してループ処理をしたり、別のListにMapするといった処理を頻繁に使います。
例えば、IntegerのListの各要素を2乗したMapは以下のように書くことができます。

iex&gt; for n &lt;- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]

n <- [1, 2, 3, 4]の部分をGeneratorと呼びます。
Generatorの右辺には、Enumerbleを指定することができます。
また、Generatorの左辺にパターンマッチを使うこともできます。
例えば、キーが:good:badのいずれかであるKeyword Listに対して、:goodの値のみの2乗を計算したい場合、以下のようにします。

iex&gt; values = [good: 1, good: 2, bad: 3, good: 4]
iex&gt; for {:good, n} &lt;- values, do: n * n
[1, 4, 16]

まとめ

ElixirのEnumerableについて学ぶことが出来ました。
より詳しく知りたい方は、以下の公式ドキュメントからよりカッコイイ書き方を知ることが出来ます。(外部サイトへ飛びます。)

Rails4.2のコネクションプールの実装を理解する

$
0
0

tl;dr

Railsではコネクションプール数を設定していても、1スレッド辺り1コネクションしか持ちません。

発端

アカツキではRails + Unicorn + Nginx + MySQLの構成をAWSで運用しており、c3.4xlargeのインスタンス上で1台辺り64のUnicornワーカープロセスが実行される設定になっています。

ソーシャルゲームでは時にたくさんのアプリケーションサーバを並列稼働される必要がでてきます。特に年末年始の時期は平時の2-3倍のトラフィックが予想され、アプリケーションサーバを最大100台で稼働させる必要がありました。

Railsのdatabase.ymlのpool設定は5だったので、単純に考えると最大 100台 * 64プロセス * 5接続 = 32,000個の接続が常時貼られるのでは?MySQLのmax_connectionsの設定は大丈夫か?という議論があり、Railsのコネクションプールの実装をきちんと理解すべき!ということで、調査しました。

動きの確認

まずは実行してみます。

  • Unicornのworker_processesを2に、database.ymlのpoolを5にして動作させてみる -> コネクション数:2
  • Unicornのworker_processesを2に、database.ymlのpoolを1にして動作させてみる -> コネクション数:2
  • Unicornのworker_processesを4に、database.ymlのpoolを1にして動作させてみる -> コネクション数:4

どうやらpoolの設定にかかわらず、ワーカープロセスの数 = コネクション数となるようです。

ドキュメントの確認

さて、Railsのドキュメントといえば、RailsGuideです。
https://github.com/yasslab/railsguides.jp をクローンし、

git grep --name-only プール guides/source

を実行すると、以下がヒットします。

guides/source/ja/configuring.md
guides/source/ja/rails_on_rack.md

rails_on_rackActiveRecord::ConnectionAdapters::ConnectionManagementがコネクションプールを管理していることしか分かりません。
configuringを読むと、以下の記載があります。

Active Recordのデータベース接続はActiveRecord::ConnectionAdapters::ConnectionPoolによって管理されます。これは、接続数に限りのあるデータベース接続にアクセスする際のスレッド数と接続プールが同期するようにするものです。最大接続数はデフォルトで5ですが、database.ymlでカスタマイズ可能です。…snip… 接続プールはデフォルトではActive Recordで取り扱われるため、アプリケーションサーバーの動作は、ThinやmongrelやUnicornなどどれであっても同じ振る舞いになります。最初はデータベース接続のプールは空で、必要に応じて追加接続が作成され、接続プールの上限に達するまで接続が追加されます。

これだけを読むと、database.ymlで設定された分のコネクションプールが「必要に応じて」確保され、各スレッドはそのコネクションプールを使う動きをしているようです。
実際の動きと、設定された分のコネクションプールが確保されるというドキュメントの記載内容に、違和感を感じます。
ここでの「必要に応じて」とは、どういう意味でしょうか?曖昧なので、より深く実装を確認する必要がありそうです。

実装の確認

API Documentation

ソースコードを読む前に、APIドキュメントを確認してみましょう。
RailsGuideには、ActiveRecord::ConnectionAdapters::ConnectionPoolによって管理されるとあるので、その部分をみてみます。

ActiveRecord::ConnectionAdapters::ConnectionPool

A connection pool synchronizes thread access to a limited number of database connections. The basic idea is that each thread checks out a database connection from the pool, uses that connection, and checks the connection back in. ConnectionPool is completely thread-safe, and will ensure that a connection cannot be used by two threads at the same time, as long as ConnectionPool’s contract is correctly followed. It will also handle cases in which there are more threads than connections: if all connections have been checked out, and a thread tries to checkout a connection anyway, then ConnectionPool will wait until some other thread has checked in a connection.

Introductionを読むと、コネクションプールの実装は完全にスレッドセーフであり、各スレッドはコネクションプールから接続をチェックアウトして使う、という実装のようです。
スレッドセーフということは「ひとつの接続が、同タイミングで複数のスレッドにより使われることはない」ということなので、ワーカープロセスの数 = コネクション数となるのは分かる気がします。
しかし、リクエストのたびに接続をし、コネクションプールの最大設定値までプールする様に実装されているならば、その限りではありません。
Introduction以下を読み進めても、直接答えに結びつくような記載は見つかりません。ここまできたら、ソースコードを読むほうが早いでしょう。

ActiveRecord::Base

接続に関する内容については、sonotsさんが書かれた

や、kotaroitoさんが書かれた

等の記事を見たほうが早いかもしれませんが、ここでも読み進めていきます。

ドキュメント

ActiveRecord::Base APIドキュメントの、”Connection to multiple databases in different models”の項には、このように書かれています。

Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection.

ということで、ActiveRecord::Base.establish_connectionが接続情報の作成、ActiveRecord::Base.connectionが接続の処理として見ていきます。

接続情報の作成

ActiveRecord::ConnectionHandling

以下、ActiveRecord::Base.establish_connectionの実装を見ると、connection_handler.establish_connectionを実行しています。

def establish_connection(spec = nil)
  spec ||= DEFAULT_ENV.call.to_sym
  resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
  spec = resolver.spec(spec)

  unless respond_to?(spec.adapter_method)
    raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
  end

  remove_connection
  connection_handler.establish_connection self, spec
end

connection_handlerは以下、ActiveRecord::Coreで実装されており、ConnectionAdapters::ConnectionHandlerのインスタンスをキャッシュしています。

def self.connection_handler
  ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
end

def self.connection_handler=(handler)
  ActiveRecord::RuntimeRegistry.connection_handler = handler
end

self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new

※ ちなみに、ActiveRecord::RuntimeRegistoryActiveSupport::PerThreadRegistryモジュールを利用しており、これはスレッドローカル(スレッドセーフなグローバル変数)値を安全にキャッシュするための機能です。クラス名をキーに含めることで、同じキーを別々のクラスで定義していても大丈夫な設計になっています。

ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection

ConnectionAdapters::ConnectionHandler#establish_connectionの実装を見ると、以下の行でコネクションプールを生成しています。

owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)

owner_to_pool[owner.name]owner_to_poolThreadSafe::Cacheownerは実行元のモデルクラスなので、実行元モデル名ごとにConnectionAdapters::ConnectionPoolのインスタンスをキャッシュしています。
実装の先を追ってみましょう。

ActiveRecord::ConnectionAdapters::ConnectionPool

大枠を捉えながら、ConnectionPool内の処理を見てみます。

ActiveRecord::ConnectionAdapters::ConnectionPool::Queue

コネクションを格納するFIFOキューです。

ActiveRecord::ConnectionAdapters::ConnectionPool::Reaper

database.ymlファイルのreaping_frequencyに設定された秒数ごとに、reapを実行しています。

ActiveRecord::ConnectionAdapters::ConnectionPool#initialize

ConnectionPool.newされるときに実行される、initializeメソッドです。

def initialize(spec)
  super()

  @spec = spec

  @checkout_timeout = (spec.config[:checkout_timeout] &amp;&amp; spec.config[:checkout_timeout].to_f) || 5
  @reaper = Reaper.new(self, (spec.config[:reaping_frequency] &amp;&amp; spec.config[:reaping_frequency].to_f))
  @reaper.run

  # default max pool size to 5
  @size = (spec.config[:pool] &amp;&amp; spec.config[:pool].to_i) || 5

  # The cache of reserved connections mapped to threads
  @reserved_connections = ThreadSafe::Cache.new(:initial_capacity =&gt; @size)

  @connections = []
  @automatic_reconnect = true

  @available = Queue.new self
end

ここで分かることは、以下のとおりです。

  1. Reaperの(別スレッドでの)実行を開始している
  2. スレッドセーフなKey:Valueキャッシュである@reserved_connectionsを作っている
  3. コネクションのキューを作っている
  4. 設定にしたがって接続情報の初期化をしているだけで、実際に接続はされていない

ここではドキュメントの記載どおり実際の接続は行わず、接続の管理情報を初期化しているだけなので、特に気にすることは無さそうです。

接続の処理

ActiveRecord::ConnectionAdapters::ConnectionPool#connection

実際に接続されるときに実行される、connectionメソッドです。

def connection
  # this is correctly done double-checked locking
  # (ThreadSafe::Cache's lookups have volatile semantics)
  @reserved_connections[current_connection_id] || synchronize do
    @reserved_connections[current_connection_id] ||= checkout
  end
end

  • @reserved_connections[current_connection_id]にすでにキャッシュされていればその情報を返します
  • そうでなければsynchronize(mutexロック)した上で、checkoutにより貼られたDBとの接続情報を@reserved_connections[current_connection_id]に格納しています。

ここで着目するのは、@reserved_connectionsのキーに、current_connection_idを使っていることです。
current_connection_idの実装は以下のとおりです。

def current_connection_id #:nodoc:
  Base.connection_id ||= Thread.current.object_id
end

Thread.current.object_idを使っています
object_idはスレッドを一意に識別するIDなので、同スレッド上で何度connectionを実行しようとも、同じ接続が使われます。

ここではじめて、1スレッド辺り1コネクションとなる動きが実装レベルで理解できました!

まとめ

  • ActiveRecord::Base.establish_connectionで接続管理情報が作成され、ActiveRecord::Base.connectionで接続が行われます
  • ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connectionではコネクションプールのインスタンスをモデル名ごとにキャッシュしていますが、これはそれほど重要ではありません
  • 実際の接続はActiveRecord::ConnectionAdapters::ConnectionPoolが持つ@reserved_connectionsにより、スレッドID単位にキャッシュされています
  • Railsではコネクションプール数を設定していても、1スレッドあたり1コネクションしか使いません。つまり、シングルスレッドのUnicornでは、1ワーカープロセス = 1コネクションとなります。

あとがき

Rails4.2のコネクションプールに関するソースコードを、APIドキュメントをきっかけにして追ってみました。
使っているフレームワークの動作を実装レベルで知ることは、未来の自分が書くソースコードの品質向上に直結すると思いますし、なにより様々な実装のアイデアを知ることは楽しいです。

そして、2016/01/16に Rails v5.0.0.beta1.1 が公開されましたね。
Rails5では @thedarkone さんのコミットによりコネクションプールの実装が大きく変わっており、Biasable queueなどのアイデアが追加され、コメントも増えて分かりやすくなりました。
この記事を見て、ちょっと興味が出てきた人はConnectionPoolのコミットログを読んでみると、面白い発見があるかもしれません。

Enjoy code reading!

5分で分かるRedis Clusterの構築方法

$
0
0

はじめに

Developpers Summit 2016で「大規模Redisサーバ縮小化の戦い」というテーマで発表してきました。


Redisのdumpファイルを取得して、それらをマージする方法や、Redis内で使用するdb数を増やせば、接続数も増えていく、といった話をしました。
特にAWS上でRedisを運用する場合、ElastiCacheの接続数上限は変更できないことは見落としがちなポイントなので、サーバを何十台もスケールアウトする人たちにとって役に立つノウハウが共有できたのではないでしょうか。

当日はネタスライドを山程仕込んで 会場は大爆笑だったのですが、slideshareではネタスライドは割愛しております。

今回のお話

デブサミのスライドでは、ほとんどの話が縮小についての話だったので、今回はRedisの信頼性について考えてみたいと思います。

元々、Redisの縮小計画は

  • コスト削減
  • 冗長化

の2つの目的がありました。

サーバ台数を64台から8台に縮小して、その後、冗長構成(16台)にする計画を立てていました。64台の状態でも冗長化はできたのですが、冗長構成で128台運用はいくらなんでもコスト的に厳しかったため、泣く泣くシングル構成を許容して64台運用をしていました。これらを縮小化することで、冗長化に踏み出せました。

Redisの信頼性向上施策といえば

さて、Redisの信頼性向上施策と言えば、以下の設定が思い付きます。

マルチAZ

AWSのElastiCacheでRedisを運用していると、一番最初に思いつく信頼性向上の施策はマルチAZではないでしょうか。 version2.8.6以降でマルチAZにすることで、プライマリノードの自動フェイルオーバーという恩恵が受けられます。今回、Redisを縮小できたおかげで、我々も各RedisノードにマルチAZでスレーブを構築しました。

AOF(Append-Only File)

こちらも信頼性向上の施策の一つで、キャッシュデータを変更するすべてのコマンドを AOFファイルに書き込みます。DBの更新ログみたいなものです。こちらは自サーバ内に書き込みを行うため、物理障害に対応できません。

Redis Cluster

Redisは2015/4にver3がリリースされて、クラスタリングをネイティブでサポートしております。もうすぐリリースされて1年が経過するところですが、AWSのElastiCacheはRedisのver3以降がまだ対応していないため、我々のシステムでもまだ導入しておりません。とは言え、そろそろAWSでRedisのver3が対応してもおかしくはありません。

ちょうどいい機会だったので、クラスタリングについて触ってみました。

ドキュメントの確認

Redis ClusterのドキュメントはRedis Cluster Specificationです。
このドキュメントの中で最も試してみたい機能が Replica Migrationです。ドキュメントを一部抜粋すると、以下のような記載があります。

Replica Migration

  • A, B, Cのmasterに、A1, B1, C1, C2というslaveが構築されている状態(Cだけslaveが2台)があるとします。
  • Aが死ぬと、A1がmasterに昇格し、C2がA1のslaveとして移動します。
  • その後、またしてもA1が死にます。
  • C2がA1に代わってmasterに昇格し、クラスタは継続された状態のまま運用できます。

なんと、惚れるじゃありませんか!せっかくなので試してみましょう。

試してみた

Redis cluster tutorialを参考に、クラスタを構築してみました。
EC2上で7つのRedisを起動し、各portを7000〜7006で設定しております。
これらの7ノードでclusterを組み、7000, 7001, 7002をmaster, それ以外をslaveとしております。

Redis起動

redisのversionは以下のとおりです。

$ redis-server -v
Redis server v=3.0.7 sha=00000000:0 malloc=jemalloc-3.6.0 bits=64 build=5ece30e075eed8d2
$ redis-cli -v
redis-cli 3.0.7

ディレクトリを作成して

$ mkdir cluster-test
$ cd cluster-test
$ mkdir 7000 7001 7002 7003 7004 7005 7006

各ディレクトリに/etc/redis.confをコピーしてきて、以下の設定に変更します。

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
daemonize yes
loglevel debug
logfile /home/ec2-user/cluster-test/7000/redis.log
cluster-config-file /home/ec2-user/cluster-test/7000/nodes-6379.conf

各redisを起動し、redis.logにエラーが出力されていないことを確認します。

$ sudo redis-server ./7000/redis.conf
$ sudo redis-server ./7001/redis.conf
$ sudo redis-server ./7002/redis.conf
$ sudo redis-server ./7003/redis.conf
$ sudo redis-server ./7004/redis.conf
$ sudo redis-server ./7005/redis.conf
$ sudo redis-server ./7006/redis.conf

$ ps aux | grep redis | grep -v grep
root 23180 0.0 0.4 142808 4312 ? Ssl 11:20 0:02 redis-server 127.0.0.1:7000 [cluster]
root 23189 0.0 0.4 142808 4308 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7001 [cluster]
root 23197 0.0 0.4 142808 4228 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7002 [cluster]
root 23202 0.0 0.4 142808 4184 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7003 [cluster]
root 23207 0.0 0.4 142808 4256 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7004 [cluster]
root 23212 0.0 0.4 142808 4148 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7005 [cluster]
root 23217 0.0 0.4 142808 4160 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7006 [cluster]
$

起動できました。

上記の設定でRedisを起動すると、nodes-6379.confが出力されるので、中身を確認すると、port7000〜7006の全てのRedisがmasterとして認識されております。

$ cat 7000/nodes-6379.conf
8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf :0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
$

この時点ではクラスタ設定がONになっているだけで、各ノードはお互いを認識していません。

クラスタ構築

redis-tribを使用してクラスタを作成します。

$ ./redis-trib.rb create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002
>>> Creating cluster
>>> Performing hash slots allocation on 3 nodes...
Using 3 masters:
127.0.0.1:7000
127.0.0.1:7001
127.0.0.1:7002
M: 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7000
slots:0-5460 (5461 slots) master
M: eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7001
slots:5461-10922 (5462 slots) master
M: 6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7002
slots:10923-16383 (5461 slots) master
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join...
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7000
slots:0-5460 (5461 slots) master
M: eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7001
slots:5461-10922 (5462 slots) master
M: 6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7002
slots:10923-16383 (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

これでクラスタが組まれ、以下の状態になりました。

$ cat 7000/nodes-6379.conf
6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7002 master - 1458646033493 1458646032693 3 connected 10923-16383
eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7001 master - 0 1458646032693 2 connected 5461-10922
8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7000 myself,master - 0 0 1 connected 0-5460
vars currentEpoch 3 lastVoteEpoch 0

logを見てみると、クラスタの各ノードに生存確認の通信が頻繁に走っていることが分かります。

$ tail -f 7001/redis.log | grep Ping
23189:M 22 Mar 11:29:29.814 . Ping packet received: (nil)
23189:M 22 Mar 11:29:30.117 . Pinging node 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf
23189:M 22 Mar 11:29:30.821 . Ping packet received: (nil)
23189:M 22 Mar 11:29:31.119 . Pinging node 6082772a27ae6465b9f3d26959e1de5991b41356
23189:M 22 Mar 11:29:31.818 . Ping packet received: (nil)
23189:M 22 Mar 11:29:32.120 . Pinging node 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf
23189:M 22 Mar 11:29:32.825 . Ping packet received: (nil)
23189:M 22 Mar 11:29:33.123 . Pinging node 6082772a27ae6465b9f3d26959e1de5991b41356
23189:M 22 Mar 11:29:33.825 . Ping packet received: (nil)
23189:M 22 Mar 11:29:34.125 . Pinging node 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf
23189:M 22 Mar 11:29:34.828 . Ping packet received: (nil)
23189:M 22 Mar 11:29:35.128 . Pinging node 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf

slave構築

slaveもclusterに組み込みます。

$ ./redis-trib.rb add-node --slave --master-id 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7003 127.0.0.1:7000
>>> Adding node 127.0.0.1:7003 to cluster 127.0.0.1:7000
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7000
slots:0-5460 (5461 slots) master
0 additional replica(s)
M: 6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7002
slots:10923-16383 (5461 slots) master
0 additional replica(s)
M: eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7001
slots:5461-10922 (5462 slots) master
0 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:7003 to make it join the cluster.
Waiting for the cluster to join.
>>> Configure node as replica of 127.0.0.1:7000.
[OK] New node added correctly.

同じように設定していきます。

$ ./redis-trib.rb add-node --slave --master-id 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7003 127.0.0.1:7000
$ ./redis-trib.rb add-node --slave --master-id 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7004 127.0.0.1:7000
$ ./redis-trib.rb add-node --slave --master-id eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7005 127.0.0.1:7001
$ ./redis-trib.rb add-node --slave --master-id 6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7006 127.0.0.1:7002

これで想定どおりのクラスタが組めました。


クラスタの状態を確認すると、以下のように見えます。

$ redis-cli -p 7000 cluster nodes
004e547e24fbdede970cd95a8801fdef6c51ee3c 127.0.0.1:7006 slave 6082772a27ae6465b9f3d26959e1de5991b41356 0 1458647137056 3 connected
00697c7f90fc0d5a3d1c55bf51b2790881b6c36a 127.0.0.1:7003 slave 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 0 1458647136055 1 connected
8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7000 myself,master - 0 0 1 connected 0-5460
eb7278d6b3018e321d1fdb4a79e630d295776e63 127.0.0.1:7005 slave eb6c6a4924cd449dcac420159cab592bc7dc8a4e 0 1458647141066 2 connected
f0784fa504d9a64ba3cedf8a66d2c23cea9f6e16 127.0.0.1:7004 slave 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 0 1458647139061 1 connected
eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7001 master - 0 1458647140064 2 connected 5461-10922
6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7002 master - 0 1458647138058 3 connected 10923-16383
$

$ redis-cli -p 7000 cluster slots
1) 1) (integer) 0
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 7000
   4) 1) "127.0.0.1"
      2) (integer) 7003
   5) 1) "127.0.0.1"
      2) (integer) 7004
2) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7005
3) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7002
   4) 1) "127.0.0.1"
      2) (integer) 7006

壊してみる

やっと準備が整いました。Replica Migrationの機能を確認したいので、まず、7002のnodeをkillしてみます。

$ ps aux | grep redis | grep -v grep
root 23180 0.0 0.4 142808 4312 ? Ssl 11:20 0:02 redis-server 127.0.0.1:7000 [cluster]
root 23189 0.0 0.4 142808 4308 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7001 [cluster]
root 23197 0.0 0.4 142808 4228 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7002 [cluster]
root 23202 0.0 0.4 142808 4184 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7003 [cluster]
root 23207 0.0 0.4 142808 4256 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7004 [cluster]
root 23212 0.0 0.4 142808 4148 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7005 [cluster]
root 23217 0.0 0.4 142808 4160 ? Ssl 11:22 0:01 redis-server 127.0.0.1:7006 [cluster]
$
$ sudo kill 23197
$

さて、クラスタの状態を確認してみると、7006が7002の代わりにmasterになっており、かつ、7003が7006のslaveになっていることが分かります。

$ redis-cli -p 7000 cluster nodes
004e547e24fbdede970cd95a8801fdef6c51ee3c 127.0.0.1:7006 master - 0 1458648558314 4 connected 10923-16383
00697c7f90fc0d5a3d1c55bf51b2790881b6c36a 127.0.0.1:7003 slave 004e547e24fbdede970cd95a8801fdef6c51ee3c 0 1458648555305 4 connected
8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7000 myself,master - 0 0 1 connected 0-5460
eb7278d6b3018e321d1fdb4a79e630d295776e63 127.0.0.1:7005 slave eb6c6a4924cd449dcac420159cab592bc7dc8a4e 0 1458648557311 2 connected
f0784fa504d9a64ba3cedf8a66d2c23cea9f6e16 127.0.0.1:7004 slave 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 0 1458648554305 1 connected
eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7001 master - 0 1458648559316 2 connected 5461-10922
6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7002 master,fail - 1458648522304 1458648519195 3 disconnected

すなわち、こういう状態になりました。

今度は、7006をkillしてみます。

$ sudo kill 23217

クラスタの状態を確認してみると、7003が代わりにmasterになっていることが分かります。

$ redis-cli -p 7000 cluster nodes
004e547e24fbdede970cd95a8801fdef6c51ee3c 127.0.0.1:7006 master,fail - 1458648779623 1458648779022 4 disconnected
00697c7f90fc0d5a3d1c55bf51b2790881b6c36a 127.0.0.1:7003 master - 0 1458648899466 5 connected 10923-16383
8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 127.0.0.1:7000 myself,master - 0 0 1 connected 0-5460
eb7278d6b3018e321d1fdb4a79e630d295776e63 127.0.0.1:7005 slave eb6c6a4924cd449dcac420159cab592bc7dc8a4e 0 1458648896455 2 connected
f0784fa504d9a64ba3cedf8a66d2c23cea9f6e16 127.0.0.1:7004 slave 8437edc8825ef2cb0d33fcaff028ad6eb13ed5bf 0 1458648894445 1 connected
eb6c6a4924cd449dcac420159cab592bc7dc8a4e 127.0.0.1:7001 master - 0 1458648898463 2 connected 5461-10922
6082772a27ae6465b9f3d26959e1de5991b41356 127.0.0.1:7002 master,fail - 1458648522304 1458648519195 3 disconnected
$

$ redis-cli -p 7000 cluster slots
1) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7003
2) 1) (integer) 0
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 7000
   4) 1) "127.0.0.1"
      2) (integer) 7004
3) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7005


想定通りの挙動をしていますね。

詳細なアルゴリズムはドキュメントに記載してあるため、興味がある方は一度見てみるのはいかがでしょうか。

所感

クラスタ再構成中のデータ更新は当然データ消失の可能性がありますし、今回のようなほとんどデータがない状態でもクラスタ再構成に数秒かかったため、実データサイズでの時間計測はやっておきたいところです。とは言え、システムの信頼性向上にとっては素晴らしい機能だと思います。

他にも、resharding機能など、ver3では、スケールアウトの仕組みなども整っており、試したい機能がいっぱいです!AWSがRedis ver3に早く対応することに期待ですね!

ドリコムさんと社会人交換留学をしました

$
0
0

こんにちは、初めまして。
最近ようやく花粉症も治まってきたエンジニアのシモムラです。
2016年3月にアカツキとドリコムさんの間で社会人交換留学という取り組みを行いましたので、その内容を紹介します。

社会人交換留学について

社会人交換留学とは 、会社同士で社員を交換しあうという取り組みです。目的は会社同士の文化交流、そこから生まれる「気づき」をシェアすることにあります。
社員になると他社の開発現場を生で見る機会は殆どないので、参加者自身にとっても貴重な経験です。

ご存知の方もいると思いますが、社会人交換留学は去年ドリコムさんとピクシブさんの間で行われています。
その当時の記事については以下の通りです。これを読むと、大体の雰囲気はつかめるのではないでしょうか。

この時はサーバーサイドエンジニア(Ruby on Rails)でしたが、今回はクライアントサイドエンジニア(C++/Cocos2d-xとC#/Unity)で実施しました。使っている技術要素が違うため成果が残しづらいことも考えられましたが、それはお互いの文化を学ぶことや成長につなげることとは別であるとしてやってみることにしました。
アカツキから留学に行ったのが私、ドリコムから留学にいらっしゃったのがイシカワさんです。
イシカワさんはドリコム勤務6年ほどのベテランエンジニアで、物腰も柔らかなとても接しやすい方でした。

今回のきっかけ

交換留学のきっかけは2015年6月に行われたIVS CTO Nightで、ドリコム白石さんと弊社CTOの田中が知りあったのがきっかけでした。その後、去年の11月にドリコムのオフィスで「エンジニアリングあるある勉強会」という合同勉強会を開催したりして、お互いのエンジニアメンバーのつながりを強くしていきました。
そこからお互いのメンバー選定、協力会社への調整、NDA締結を行い、留学を実施することとなりました。

留学の準備

さて、留学期間はわずか5日間と非常に短期間です。無駄にできる時間はありません。
しかし、いきなり仕事に取り掛かれるとは限りません。ベテランエンジニア同士で実施した過去の交換留学でも、初日は環境構築に費やしてしまったそうです。
今回はその反省を踏まえ、本留学開始の前の週に準備日を設けました。準備日には顔合わせ・環境構築・その他留学中のメニューなどのすり合わせなどを行います。
こうした準備を経て、2月22日にアカツキからドリコムへ、3月14日にドリコムからアカツキへの留学を実施しました。

留学中にやったこと

留学中はチームメンバーとして実際の業務を行います。今回の留学ではBisqueチームと呼ばれる社内共通基盤の開発チームに参加させていただきました。Bisqueチームはドリコムでクライアントアプリケーションの開発に利用しているCocos2d-xの拡張や、それをベースとした内製フレームワークの開発を行っています。
今回私は持ち込みのネタも無かったため、ここBisqueチームから提案のあったデバッグ用の機能拡張を実装することにしました。

またBisqueチームではUpstream開発に取り組んでいます。Upstream開発というのは、オープンソースソフトウェアに対して企業が行ったソースコード修正を元のオープンソースプロジェクトに還流させるプロセスを言います。
メリットとしては還元されたコードのメンテナンスに手間がかからなくなること、また独自の派生を減らすことでバージョンアップが容易になることが挙げられます。
一方デメリットとしては要望が通るとは限らない、通るとしても時間がかかる、通るまでの”つなぎ”の管理の必要が生じるといったことが挙げられます。ここは基盤開発の経験を活かしてうまく解決しているそうです。
せっかくですので私の実装もUpstream開発の一環として本家へPullRequestを送りました。

PullRequestを出したのはCocos2d-xに搭載されたリモートコンソール機能の拡張です。このコンソールに、アプリケーションの状態をモニタリングして割り込み停止をかける”trap”というコマンドを追加しました。
trapコマンドはデバッガーの方がパフォーマンスのチェックをする際に利用することを想定しています。
ほんの些細な内容ではありますが、これマージされて誰かに利用してもらえたら嬉しいです。

また前回の交換留学では働く方々へのインタビューが有意義だったそうなので、今回もドリコムの各部署のメンバーを対象としたインタビューをさせていただきました。
あまり堅苦しい形にはしたくなかったのでインタビューの一部はランチと一緒に行うよう一工夫しました。実際カジュアルな雰囲気で情報交換ができたと思います。
加えて前回の反省を活かして事前にランチのアポを取っておいた事も功を奏し、多忙なリーダーの方々ともご一緒していただけました。

他にはドリコムの子会社であるグリモアさんを訪問させていただきました。
グリモアは少数精鋭でのゲーム制作を信条としています。ここで伺った話はとても興味深いものばかりでした。
そのひとつが、昨今のゲーム実況配信の盛況を踏まえ、自分たちで色々なゲームの実況配信を実際にやってみるという取り組みについてです。実況し易いゲームや実況を見て面白いゲームとはどんなものなのか研究し、その成果を実際のプロダクト開発に活かしているそうです。

アカツキでの受け入れについてはイシカワさんのエントリで詳しく紹介されています。後述の問題を除いては概ね予定通り進められました。
留学の日程がちょうど上場の記念日と被るなど、一つの会社の節目を体験していただけたと思います。

ドリコムスタイル

留学を無事終えて気づいたドリコムの文化が以下の2つです。

前提知識の均一化

ドリコムではアカツキと比べても非常に多くのクライアントサイドエンジニアが働いていますが、そのエンジニア間のコミュニケーションコストは非常に低く抑えられているようです。
これは前述のとおり基盤技術を全社的に統一している、つまり多くのプロジェクトが同じ内製フレームワークを利用していることによる恩恵です。あるプロジェクトで得たノウハウは、チームの壁を超え素早く共有することができます。
Bisqueチームが主催となっている、アップデートの告知や問題のヒアリングなど行う共有会もこれに貢献しているようです。
さらに、チーム間での人員交換にかかるコストが低いため、適切なタイミングで適切なリソースの再配分が可能になります。

資産を全社に還元するシステム

ドリコムで徹底的に共有がされているのは実際のコードについても同様です。RubyでいうGemのように、CocoaPodsを用いたクライアントコードのライブラリ化が非常に活発です。
この取り組みを支える柱となっているのがクライアントアーキテクトグループ(略称:クライアントアーキG)。クライアントアーキGは各プロダクトチームの成果を全社に還元する役目を担います。
クライアントアーキGはBisqueチームのように独立して開発を行うのではなく、メンバーが各プロジェクトの開発に実際に参加してライブラリ化のサポートを行います。
現場に密着した彼らの働きによって、便利なモジュールの共有と整備がなされています。特に実運用で使われているライブラリは信頼性も高く、再利用率が非常に高いそうです。

ここでいう共有・共通化は上からの押し付けではなく、個別のプロダクトチームの成果を現場から全体に還元するというのがポイントです。
アカツキでは「チームごとに最適な方法を探索」という考え方があり、プロジェクト単位で自由な技術採択を推進しています。そうすると、技術的なチャレンジがし易い反面、プロジェクトを超えてノウハウや資産を共有することが難しいとも感じています。
クライアントアーキGのように個別のプロダクトチームが主体となる資産化の試みは、今後アカツキでも試してみる価値があると思いました。

反省点

今回の交換留学での反省点も共有したいと思います。

イシカワさんを受け入れた際のアカツキの状況

今回の交換留学では、受け入れ先のチームと同じフロアで作業することが出来ないというトラブルが発生しました。
企画から留学開始まで3ヶ月ほど期間が空いたため、受け入れ予定チームの周りに新しい機密プロジェクトが配置されたことが原因です。このため当該フロアへの立ち入りを制限することとなり、やむをえずチームの島から離れた場所に席を構える事になりました。
防ぎようのない不測の事態だったのですが、次は企画からできるだけ早く留学を始めることでリスクを減らすことができるかもしれません。

クライアントサイドのエンジニアを交換する難しさ

当初の予想通りクライアントサイドの作業では短期間で成果を出すのが難しく、両者とも無難なデバッグ機能の実装でひとまず着地することになりました。クライアントアプリケーションではエンジニアだけで完結する作業が少ないことや、直接ユーザーの目に触れることから品質の担保が必要になることが理由として挙げられます。
他に出来る事があるとすればツール開発などによるワークフローの改善などが有効かもしれません。ヒアリングの過程でワークフローを把握できれば開発の文化にも触れられて一石二鳥です。
次回チャンスがあればそのあたりをチャレンジしてみるか、もう少し長い時間で取り組めるよう考えていきたいと思います。

最後に

社会人交換留学は前例のある取り組みではありますが、同業種の他社にインターンシップに行くような経験は当然なく当初は不安感もありました。加えて前例では優秀なエンジニアが留学を行っていたのも強いプレッシャーでした。
そのような気持ちで留学が始まったので、現場でイシカワさんをはじめドリコムの皆様に快く受け入れ・サポートをしていただけて本当に嬉しかったです。
またこの交換留学は現場で会った方々以外にも、様々な方々の尽力によって実施することができました。この企画に携わっていただいた全ての皆様に感謝いたします。ありがとうございました。


スクラムトレーニングで自律型チームを実現する

$
0
0

はじめに

アカツキでは開発にスクラムの要素を取り入れています。しかし、現状の開発のやり方に漠然とした不安がありました。その不安とは、例えば、もっと効率的になるんじゃないか、もっと良いやり方があるんじゃないか、このやり方はスケールしないのではないか、等といったことです。そこで第三者の目が必要だろうと、ryuzeeさんにアドバイスしていただくことになりました。

メンバーヒアリングからスクラムトレーニング実施へ

まず現状をryuzeeさんに知っていただく必要がありました。そこで、アカツキの全メンバーを対象に、開発上困っていることを何でも相談出来る、アジャイル開発お悩み相談会を実施しました。また、一つのプロジェクトの振り返り(KPT: Keep Problem Try)にも参加していただき、観察していただきました。様々なプロジェクトから約20名ぐらい個別相談させていただいたり、KPTに参加していただいたたりした結果、以下の課題が浮き彫りになりました。

  1. スクラムとは何かをほとんどのチームメンバーが理解しきれていない
  2. スクラムのプラクティスをなぜ実施するかを、スクラムマスターが説明しきれていない
  3. スクラムマスターとプロダクトオーナーが明確でない

上記を解決するためには、例えば、スクラムを理解した人が社内で教えてまわるといった方法があげられます。しかし、現状はその工数を中々捻出出来ないといった課題がありました。そこで、スクラムトレーニングを一つのプロジェクトで実施し、その成功体験・プラクティスを横展開していくのが良いのではないかと考え、ryuzeeさんにスクラムトレーニングを実施していただきました。
scrum_desk

スクラムトレーニングでやったこと

スクラムトレーニングでやった内容そのものは非常にシンプルなものでしたが、約3時間かけて以下の実習をじっくり行いました。トレーニング中は常時明るい雰囲気で、スクラムに関する質問が飛び交うなど、参加者全員が楽しんでいる様子だったのが印象的でした。

スクラム体験1(紙飛行機製作)

これは紙飛行機の製作を通じてスクラムの本質を理解しようという試みです。以下のように進めていきました。

  • 5~6人のチームに別れる
  • あるルールにそった紙飛行機をチーム対抗で製作
  • 紙飛行機の製作工程をスプリントに見立てる(プランニング->紙飛行機製作->振り返り)
  • スプリントを4回実施する
  • 紙飛行機製作を通じた振り返り

結果は、こちらの写真の通りですが、予測/結果が合っているチームがあれば、全然異なった結果になっているチームもあり、なかなか面白いですね。
score_board
scrum_paper_plane

スクラム基礎の座学

これはスクラムの基礎知識を学ぶ講義でした。大体のことはこちらのスクラムガイドを参考にしてください。

スクラム体験2(ボールタッチゲーム)

これは最後少し余った時間で実施しました。やったことは、参加者全員(約20名)の名前の順に、ボールを出来るだけ早く触れるアクティビティです。これもスクラムトレーニングなので、計画->実行->振り返りを1スプリントとして、3回実施しました。最初は5分かかったのですが、3回目の挑戦では3秒という短い時間でこのミッションを達成出来ました。これを読んでいる方はどうやったか想像出来るでしょうか?

スクラムトレーニングを受けたメンバーの気付き・学び

スクラムトレーニングを受けたメンバーに気付き・学びのアンケートを実施しました。そのアンケートの中で、特に多かった声をご紹介します。

暗黙の制約に縛られてはいけない

既に述べた通り、スクラムトレーニングの紙飛行機制作にはルールがありました。このルールを制約と捉えるのか、規約と捉えるのかで大きく違ってくるというエピソードがありました。ルールに沿った紙飛行機の生産量を比較したとき、チームAは10機以上制作出来るのに、チームBは1機しか製作出来ませんでした。この状態が何回か繰り返されたとき、ryuzeeさんが一言「他のチームの制作した紙飛行機を分析してはいけないというルールは無いですよ。」とアドバイスしました。全てのチームに共通していたことですが、チームで1から設計したものでないと紙飛行機製作とは言えない、と暗黙の制約を自分たちに課していたのです。その後、チームBはチームAの紙飛行機を徹底的に分析して、劇的に改善して最終的には7機製作(7倍の効果)していました。
普段の業務をする上でも、暗黙の制約を課していないかを常に考える必要がある、そうでないとイノベーションを起こす機会を失っているかもしれないという大きな気付きがありました。
scrum_wideview

計画・振り返りの重要性

紙飛行機製作のトレーニングで、スプリントを重ねる度にほとんどのチームで製作数が増加するという結果が出ました。具体的には最初は4機だったチームが、4回目のスプリントでは10機作れていました。1スプリントは5分でしたが、20分間ただ作り続けた場合と比較した場合、同じ結果が得られたかというとそれは厳しかったかなと思います。例えば、私のいたチームの場合は、

  1. 最初のスプリントで全員がどう作ればいいかわからない状態でスタートする(1機製作)
  2. 次のスプリントの計画で良い紙飛行機の製作方法を共有する(4機製作)
  3. さらにその次のスプリントで役割を明確にする(11機製作)

と、明らかに5分ごとに改善されていきました。これは、計画・振り返りを何度も繰り返すことで、個人主義的な開発からチーム主義的な開発へとシフトしていったからだと思います。ここから、自己改善のできるチームが目指すチームの姿だよね、という共通認識が出来ました。

スクラムトレーニングを受けてから変わったこと

一言で述べるならば、誰かに管理されて動くのではなく、自主的に動ける自律型チームになりつつあります。結果として、メンバーのプロダクトに対する責任感が高まり、マネージメント工数が劇的に下がりました。ここでは、変わったことを2つ紹介します。

自主的にカンバンを取り入れた

スクラムトレーニングにて、朝会がもしただの進捗報告になっているのであれば、カンバンを取り入れてみるのも一つの手であると、アドバイスをいただきました。後日、カンバンを取り入れようという声がメンバーから上がり、チーム全体でタスクを見える化するようになりました。最初は付箋紙をいちいち作るのは面倒だという声もありましたが、数日続けてみた結果、それまでただの進捗報告だった朝会が、お互いの進捗を確認しあって困っていることを共有する場になりました。

自主的に課題解決の場を設けるようになった

問題が発生した際に、チームで自発的に集まって振り返りや解決方法を考えるようになりました。これまではなんとなく放置されていた問題があったり、チーム全体で実施するKPTでだけ振り返りをしていたので、大きな変化ですね。

まとめ・今後の展望

スクラムトレーニングをメンバーが受講したことで、自律型チームの種はまけたかなと思っております。ただし、これからもより良いチームにするためには、常にチームはどうあるべきかを自分たちで考え、自分たちで良くしていくしか方法は無いと思っています。そのためには、誰かの力を借りずともスクラムトレーニングを自分たちで出来るぐらい、スクラムのことをより深く知る必要があると思っています。

まだまだ改善出来るところがあるということは、伸びしろがあるということなのでワクワクしますね!!

エンジニアチームを幸せにするたった1つの方法

$
0
0

はじめに

あらゆる人間関係の衝突は、謙虚・尊敬・信頼の欠如によるものだ。
プログラマとして成功するには、最新の言語を覚えたり高速なコードを書いたりするだけではいけない。プログラマは常にチームで仕事をする。自分が思っている以上に、チームは個人の生産性や幸福に直接影響するのである。

これはTeamGeekに載っている、Googleのチームビルディングの考え方です。アカツキでも、この謙虚・尊敬・信頼(HRT)の価値を重んじ、HRTの文化を作ることで、チームの人間関係を円滑にしています。

しかし、エンジニアチームを 幸せにする方法はこれだけでは足りません。
では、エンジニアチームを 幸せにするために必要なことは何でしょうか。

そう、カレーを食べることです。

エンジニアがチームとして働くうえで、謙虚・尊敬・信頼の価値を重んじ、カレーを食べることでチームビルディングの成功につながります。

発端

アカツキでは2週間に1回、エンジニア社員全員であつまるMTGを開催しています。技術的なノウハウや悩みの共有、LTなどを中心に話を進めるのですが、エンジニアMTGについてこんな話が出てきました。

01-01

「エンジニアMTGは楽しむための場所である」
…なるほど!と思い、私がこんな投稿をしていみたところ、

02

意外にも、みんなとても前向きなリアクションをしてくれました。
すれ違いざまに「こまいさん、カレー作るんすか?」なんて言う人も出てきました。

この日から私の心は カレーを食べたいという気持ちでいっぱいになりました。
いや、正確にはカレーを食べたいという情熱が私の心を掴んで離さなかったのです。
カレーを食べたい。カレーを食べることでみんな幸せになれるのではないか?
カレーをみんなと一緒に食べることで、幸せ溢れる世界を創り出したい。

…カレーについてありとあらゆることを考えました。
カレーについて深く考えぬいた私は、自らの枠(サーバーエンジニア)に囚われず、新たな領域(カレー)に挑戦し、かつ大きな成果(美味い)を残すために、目的の遂行・達成「だけ」を純粋に求めるようになったのです。

問題提起

というわけで、次のエンジニアMTGで議題に挙げ、弊社のCTO田中に提案してみました。

DSC_0896_2

駒井:あの〜以前Slackで言ったんですけど、カレー食いたいっす(ドキドキ)
田中:あ、いいっすよ

はい、決まりました。
……なんと軽いのでしょうか。

というわけで、エンジニアMTGの前にカレーを仕込み、MTG後にみんなでカレーを食べることになりました。

この日は、ちょうどドリコムさんと社会人交換留学をしているタイミングだったため、MTGの様子はドリコム石川さんの交換留学の記事にも載っています。

こだわり

アカツキでは、ストレングスファインダーという、自分の強みを分析する有名なツールで社員全員の強みを明確化しています。私の強みの1つは「最上志向」です。最上志向の私は、最高のカレーを作らずにはいられませんでした。

世界で最も美味しいカレーとはいったい何でしょうか。そう、マッサマンカレーです。

だが、美味しいだけでは「最上」とは言えません。アカツキ社員は食箋弁当という砂糖をいっさい使わない健康弁当を週に3回も食べるほど、健康へのこだわりも尋常ではないのです。

食箋弁当

アカツキらしさを出すカレーにするには、美味しさだけでなく、健康の追求も必要なので、野菜たっぷり、かつ、肉の旨味が凝縮された、エンジニアのための最高のマッサマンカレーを作ることに決めました。

作ってみた

見てください、このあふれんばかりの具材。
「料理」は自然を素材にし、人間の一番原始的な本能を充たしながら、その技術をほとんど芸術的な領域にまで高めます。

分量も、1人2杯は食べれるであろう量です。

DSC_0926

ミーティング当日

さて、仕込みが終わり、ミーティングの時間になりました。
あとは電気コンロにカレーの鍋を置けば、食べる準備は万端です。
macにサトウのごはんという、とてもシュールな絵が出来上がっています。

DSC_0872

だがここはミーティング。技術的なことについてまずは本気で議論します。

DSC_0912

心なしか、 エンジニアの出席率が上がっている ではありませんか!!

試食

そして、技術的な議論が一通り終わった後、ついに試食です。
みんながカレーに群がってきました。

そのお味は………

う、うまい…………!!!、、うますぎるっ………!!!

IMG_0637

部屋中「旨い」という言葉だけが小一時間響きわたっていました。あまりの美味しさにCTO田中が「人生で一番うまいカレーだった」と言っていました。(本当です)

効果

カレーを食べることで、単純にお腹が満たされるだけでなく、コミュニケーションが活発になり、エンジニア同士の交流に繋がったり、モチベーションアップにも繋がるなど、様々な副次的効果がありました。単純にイベントとして面白いですしね。

個人的に最も良かったことは、謙虚・尊敬・信頼(HRT)という、目に見えない価値の促進剤になったことです。このように、参加した全ての人に感謝されました。この感謝の言葉だけで私のお腹はいっぱいです。

03-02
04-02
05-02

アカツキのビジョンは「感情報酬社会を作ること」。ひとつ、新たに感情の総量がアップした瞬間です。カレーを食べることで、チームへの愛、アカツキへの愛、そしてカレーへの愛がよりいっそう深まったのです。

さいごに

冒頭でTeamGeekの言葉を借りましたが、チームビルディングの上でひとつ足りないものがあります。付け加えるとするならば、こうなるでしょう。

あらゆる人間関係の衝突は、謙虚・尊敬・信頼・カレーの欠如によるものだ。
プログラマとして成功するには、最新の言語を覚えたり高速なコードを書いたりするだけではいけない。プログラマは常にチームで仕事をする。自分が思っている以上に、チームは個人の生産性や幸福に直接影響するのである。

おいしいカレーを食べることで、チームのモチベーションもアップし、チームビルディングがうまくいき、そしてお腹まで満たすことができます。

DSC_0922

次回やるなら、炊飯器を持ち込んで、MTG前に炊飯ボタンを押して、MTG後に炊きあがった米を食べたいです。MTGでカレーを食べるイベントは定期的に開催したいですね!

Happy Hacking!

RubyKaigi2014 速報(5) – おはよう Rails

$
0
0
  • Ohayo Rails

    • 高井さん進行
    • あれあれ?大きなお友だちの声が聞こえないよ?
    • 大きな声でおはようございます
    • なんでマリ見てパロってるの?
    • 高井さんはしゃべりすぎないように気をつける
    • ちょっとストレッチしてゆるい感じではじめましょう

    • 挨拶・自己紹介

    • クラウドワークス・クオカさん

      • 笑いが足りない(by 高井さん)
    • ドリコム・オオナカさん

      • もっとおもしろいこと言ってください。でも長い。(by 高井さん)
    • mixiオオツカさん

      • モンストのバックエンドはRuby
      • ユーザーは1300万人
      • あとでモンストの裏ワザを教えてくれます(by 高井さん)
    • Oh My Glasses シラツチさん

      • 7人中6人メガネ。全部Oh My Glassesで買いました(by 高井さん)
    • Pixiv コシバさん

      • うちの息子がいとうのいぢさんのノベルティを見て「可愛い女の子だねー」と言ってました(by 高井さん)
    • 食べログオオイシさん

      • 最近サイトで予約できるようになりました
      • 1個の大きなRailsアプリで動いてます
      • 今日のポイントはオオイシさんがどれだけしゃべるかです。テンション上げてとこう!(by 高井さん)
    • Railsどうよ?

      • StartUpには楽ですね
      • Railsを採用してなかったら僕は入社してないですね。みんなRailsを使いたくて入社してない。
      • Ruby会議ではアンチハラスメント・ポリシーがあって、Pで始まる言語の悪口は自重してください。
      • Railsはデプロイ方法とか色々揃ってて知見が貯まってるのがいいですね
      • 環境セットアップとか楽で、人の入れ替わりが激しくても手順がわかってるのが素晴らしい。
      • bin/setupに書こうとか。いいね。いいよね。
      • ActiveRecordもいいですよね。どの辺がいいですか?
      • ActiveRecord脳になってる。画面を見たらDBの設計が一意に決まるのがいいですね。
      • 他の言語から見たらRAILSは甘え。SQL書けよとか思う人もいる。
      • ちょっとした小手先のこともRubyでできるのもいいですね
      • Arel賢い。対抗馬もなかなかでない。
      • スカラを使ってた期間長いけど、RAILS楽。 他のフレームワークだとORMをすぐ変えようとか言う話になる。 大概のことはできる。
      • いざとなったら生SQLも書けるし。
      • 新卒とかでSQLを熟知してなくても理解しやすい。
      • あ、オオイシさん喋った(by 高井さん)
    • エコシステムとしての側面。テストとか。

      • テストフレームワークとしてRailsを安心して選択できる。要すればGem追加したりできる。
      • Rakeで完結したり。
    • FactoryGirl or Fixture?

      • 違う会社の人とでも何派?っていう話ができるのって実は素晴らしいですね。
      • Redis, Unicorn, MySQL, 等、大体話しが通じるしコードを見たらすぐわかる安心感。
    • とりあえず褒める流れ?

    • とはいえ、とりあえず作ったらGemは100個くらいになる。知らないといけないこと多い。

    • Deviseの中身読んだことある人いる?

    • Deployのこととか。

    • もっとしゃべっていいよ。

    • Capistranoが使えない場面とか。
    • Padrinoみたいなフレームワークを選択するためにRails以外にも対応するGemがあるといいですね。
    • Gemに対応する手段として、モンキーパッチ or Pull Requestがある。
    • Private Gem ServerでFork。
    • ダメだよ、モンキーパッチ。PullRequestでContributeしようよ。
    • 放置されてる場合、「やらないなら俺がやるよ」くらいのスタンスがほしい。
    • Contributeしたくても華麗にスルーされることがある。
    • acts as paranoidとか。公開したいけどメンテはしたくない。
    • Gemのソースコード呼んでからいれたい。闇雲にいれたら後で大変。
    • fluentdとかver.0.x.xのものを使うのは覚悟がいるよね。
    • ログどうしてる?
      • treasure data
      • 内製の分析器版
      • fluentdはアプリケーションレイアーのフレームワークとしても使ってる
    • Gemを使わないと、世界規模でコピペしてる感がある
    • 気軽にGemをいれられる反面、消し忘れが多い
    • 未使用のGemを検出するGemがほしい
    • 正直ガチャのGemを作りかけました
  • 皆のProduction環境の話
  • Job Schedulerとか、非同期の話。Railsももうちょっと対応してほしい。
  • 高井さん司会してください。
  • ゲームはRailsじゃなくてもいいんじゃないの?

    • Railsから乗り換えようと思っても、「Railsのアレが使えない」っていうことが多い。
    • RailsはHTMLを返すにはいいけど、Jsonを返すAPIサーバでなんでRAILSを使うんだろう?
    • 新しく入ってきたエンジニアが簡単に着手できるのが利点ですね
    • 例えば広告だと、モデルが厚くなるけどリクエストを高速に捌くことが要求されてRailsを使わないほうがよくなる。
  • Railsはバックエンド担当になってきて、JSとかNativeと連携しないといけなくなってきた。

    • Railsでsingle page applicationは辛い。そうなるとFront End専用のエンジニアが必要になる。
    • そういうエンジニアにはRubyいれてとは言いづらい。asset pipelineを外したりする。はじめからnodeでいいじゃんって言われる。
    • 最近の若い子はRailsは業務で使うもの、個人の趣味では他のことやってる。
    • RailsRuby書いてて楽しい?Railsのコードを書くことはそんなに楽しくない。頭使わない。
    • 昔は楽しかったのになー。
    • とはいえ、サービスで価値を提供することがあるべき姿で、フレームワークに振り回されなくなったことがRailsの成果。
    • 何で作るかはどうでもよくて、何を作るかに集中できるのはいいこと。
    • Railsで決まりだね。
    • Railsが書きたいわけじゃない若い人たちとどうやって僕らのサービスを育てていくかが課題だと思ってる。
  • 質問

    • ゲーム業界のActiveRecordインスタンス化のコスト、JsonのRenderingコストについて
      • スケールさせるときはRailsを捨ててpure RubySQLを叩いたりします。
      • JsonはViewという考えは捨てた方がいい。別のシリアライズクラスを作った方が速いです。
    • テストが多くなる問題
      • engine, gemでモジュール化
    • 若い人がN+1とか発生させちゃう問題とか、Railsでどう解決するか
      • Bulletを入れるとか
      • リリース前に検知が難しい場合、どうやったら検知が速くなるかを考える
      • dirty query pointを定量化とか。自動検知の方向。
    • 規模が大きくなるとアプリの切り出しが必要になってくる。そういったときのモデルの共有はどうするか?
      • モデルだけのリポジトリを作ってモデル層を共通化
      • API通信
      • サブアプリを使う
      • Engineで切り出す
      • マイクロサービス化

人がコードレビューする時代は終わった

$
0
0

背景

crazy_hound

最近GitHubワークフローやコードレビューの文化が普及してきていますが、 コード規約まわりの細かい指摘ってするべきなのか、迷いますよね。 例えばカンマの後にスペースがいるとか、引数が多いときに改行した方が良いとか・・・。 いちいち指摘してたらエンジニア間の人間関係がギクシャクします。

そんな悩みを解決してくれるサービスが、HoundCIです。 HoundCIを使うとPull Requestをopenしたときに自動的にコード規約に違反する箇所をGitHub上でコメントとして指摘してくれます。

アカツキではソフトウェア開発は大きくサーバーサイド(Ruby)及びクライアントサイド(C++)に分かれますが、 数ヶ月前よりサーバーサイドでHoundCIを導入して以降、コード規約の浸透率が高くなった経緯があります。 そこで筆者はクライアントサイドにもHoundCIを使ってみたいと思うようになりました。

ところが、HoundCIは2014年10月現在、JavaScript, CoffeeScript及びRubyにしか対応していません。 そこで本記事で紹介するHoundCIのForkで、HoundCIをC++にも対応させようと思い立ちました。

改修内容

新しい言語に対応となるとそれなりに手間がかかるかなと思いましたが、 マッシュアップで意外と簡単に(1日!)実装できました。 やってることは、Rubyにおいてrubocopがしていたことを、C++においてcpplintに置き換えるだけでした。 こういうかゆいところに手が届くのが、open source のいいところですね。

github.com/sergeant-wizard/hound/compare/cpplint

HoundCIのサービスを普通に使用する場合はFORKしたバージョンで運用できないため、 アカツキではAWS上のプライベートサーバでHoundCIを運用しています。

Rubyのチェックでは、リポジトリ毎にrubocopの設定を変えて運用することができますが、 このForkではそこまで対応していません。 プロジェクトを横断して共通のcpplint.pyを改修しながら運用しています。

ついでに自動修正もしてみた

数ヶ月前から発足したプロジェクトにいきなりhound+cpplintを導入したら、 これまでクライアントサイドでコード規約があまり浸透していないこともあり、 一つのPullRequestで100件近くの指摘が発生してしまうこともありました。 そこで、git-filterを使って機械的に置換が可能な箇所は置換してしまうことにしました。

下記のスクリプトをクライアントリポジトリに追加することで、不注意によるコード規約違反がhoundにより指摘されることが少なくなりました。

Programmers should be Lazy. [gist id=c4ebd58194bcc7fd804c]

ただし、こういったスクリプトを使用した一括置換を運用中のコードに適用するにあたっては、 ロジックの変更がないかをチェックする手間がありますね。

ソシャゲ業界のクライアントコード

ゲーム業界のクライアントサイドは、一昔前は「リリースしたら終わり」で、可読性やメンテナンス性がそこまで重視されていなかったと聞きます。 ところがソーシャルゲーム業界では、1ヶ月に複数回クライアントのバージョンアップデートをすることも珍しくありません。 このような細かいリリーススパンが要求される現場では、クライアントサイドにおいても、サーバーサイドで常識となりつつあるContinuous Integrationの文化や考え方を導入することが不可欠であると思います。 本記事ではその一貫としてアカツキが取り組んだ自動化や、可読性へのこだわりを紹介させていただきました。

Disclaimer

本記事はthoughtbotのScott Albertson氏の許可をいただいた上で掲載しております。

Are you still reviewing that pull request?

$
0
0

Background

crazy_hound

The GitHub workflow and code reviewing is becoming a common culture nowadays. One of the drawbacks of this culture is that you're not sure if you should comment on minor coding style mistakes, like whether you should add a space after a comma, or if you should add a newline where you have many arguments. It's really frustrating for both the reviewer and the reviewee to have to see those kind of comments, even though following the coding style is an important issue for maintaining software.

Then we found HoundCI. HoundCI automatically comments on coding style violations on GitHub whenever a pull request is opened.

At Akatsuki we write server-side software in Ruby, and client-side software in C++. Since we started using HoundCI on the server-side a few months ago, the number of coding style violations reduced dramatically. Seeing that change, I started to think it'll be great if we could have HoundCI on the client-side as well.

However, there was a problem -- HoundCI only supported JavaScript, CoffeeScript and Ruby as of Oct. 2014. But HoundCI was open source, so we decided to make a fork and add support to C++. That's what I want to introduce in this article -- adding C++ support to HoundCI.

Modification

At first, I thought adding support to a new language would take some time. But as HoundCI was well-designed and well-coded, it wasn't much trouble (within a day!). After all, it's only a Mashup of HoundCI and CPPLINT, replacing rubocop for Ruby to CPPLINT for C++. That's what's so great about open source. If you want a new feature, you can make it yourself!

github.com/sergeant-wizard/hound/compare/cpplint

With the standard service you can't use a forked version of HoundCI, so we run HoundCI on a private server with AWS.

With Ruby, you can change the settings among different repositories by placing a .hound.yml -- our CPPLINT version has yet to support that feature.

Some more automation

Applying HoundCI + CPPLINT to a project that's been running for a few months by engineers not really interested in following the coding style rules was... disastrous, as you can imagine, with as many as 100 violation warnings in a single pull request. So we decided to automate the fixing process as well using git-filter. The following scripts really helped decrease the ammount of coding style violations.

[gist id=c4ebd58194bcc7fd804c]

The drawback is that you have to be careful not to change the logic of the program if you're going to apply this kind of script to a project that's already under operation.

Client-side code doesn't have to suck

I hear that until recently, client-side programmers in the game industry didn't care much about code readability and maintainability, because you didn't have to work on the code after the game had been released. But that rule doesn't apply anymore, especially to mobile-phone games, where it is not rare to have multiple client version updates within a month. With such short-span releases, I think it's essential to adopt the techniques and culture of Continuous Integration to the client-side as well. So that's what we're aiming at here in Akatsuki, and I wanted to introduce some of the technical elements we've been using.

Disclaimer

Mr. Scott Albertson from HoundCI has granted permission to post this article.

resize2fsコマンドはどのようにして1秒未満での容量拡張を実現しているのか

$
0
0

この記事はLinux Advent Calendar 2014の23日目の記事です。

背景

アカツキではAWS EC2をテストサーバ、ステージングサーバ、本番サーバとして利用しています。先日1周年を迎えた千メモは、リリース時よりも大分デプロイ時に容量を使うようになってきました。ステージングサーバのストレージ容量が当初想定していたものより大分カツカツになってきたため、Amazon社の出しているマニュアルに従って、ストレージ容量を拡張しました。ストレージ容量の拡張そのものは無事うまくいったのですが、どうしてこんな1秒もかからずに拡張出来るのだろうか(resize2fsコマンドが終わるのだろうか)、という疑問がわいてきました。そこで、Linuxでどのようにファイルシステムのサイズを拡張しているか調査するためのとっかかりとして、resize2fsコマンドを調査しました。

調査対象

調査対象は以下の通りです。

AWSで採用しているバージョンと多少処理が異なる箇所があるかもしれないので、ご注意ください。

resize2fsの処理内容

ストレージ容量を拡張する際にresize2fsコマンドを使用しますが、resize2fsというコマンド単体が配布されているわけではありません。E2fsprogsのgitレポジトリを見ていただくとおわかりいただけるかと思いますが、fsckコマンドなどを含めたパッケージの一部として提供されています。resize2fsの中身はresizeディレクトリ内にあります。

main()

まずはmain関数を読むためにresize/main.cというファイルをまず見ましょう。今回はオンラインの容量拡張の方法を調べていきたいと思います。それらしい関数を調べてみると・・・ありました、online_resize_fs()という関数を使っていますね。

471     if (mount_flags & EXT2_MF_MOUNTED) {
472         bigalloc_check(fs, force);
473         retval = online_resize_fs(fs, mtpt, &new_size, flags);
474     } else {
475         bigalloc_check(fs, force);
476         printf(_("Resizing the filesystem on "
477              "%s to %llu (%dk) blocks.\n"),
478                device_name, new_size, blocksize / 1024);
479         retval = resize_fs(fs, &new_size, flags,
480                    ((flags & RESIZE_PERCENT_COMPLETE) ?
481                     resize_progress_func : 0));
482     }

EXT2_MF_MOUNTEDはその名の通り、対象のファイルシステムがマウントしているかどうかを表すフラグなので、マウントしていたらonline_resize_fs()、マウントしていなかったらresize_fs()を呼びます。

bigalloc_check()

いきなり本筋とは脱線しますが、その前のbigalloc_check()について説明します。ext4ファイルシステムではブロック単位(1KB/2KB/4KB/8KB)でデータを管理しています。しかしもっと大きな単位で管理したほうが、データを管理するためのデータ(メタデータ)が少なくて済み、大きなデータに対しては飛び飛びにブロックをアクセスしなくて済むようになります。そこで、ext4ではブロックを複数集めたクラスタ単位(例えば1MB単位)でデータを管理出来るようにした機能であるbigallocをサポートしています。しかし、bigalloc_check()の中には以下のように書かれています。

155       fprintf(stderr, "%s", _("\nResizing bigalloc file systems has "
156                     "not been fully tested.  Proceed at\n"
157                     "your own risk!  Use the force option "
158                     "if you want to go ahead anyway.\n\n"));

つまり、bigallocのリサイズはちゃんとテストされていないので自己責任でよろしく!とのことです。まあ、普通に使っていたらページサイズと同じ4KB単位になっている(=bigallocはオフになっている)ので、そこまでbigallocについて気にしなくて良いでしょう。ちなみにもしbigallocを適用したファイルシステムをリサイズする場合は-fオプションを付けて実行してください。

online_resize_fs()

さて、次にonline_resize_fs()について見て行きましょう。online_resize_fs()の始めに気になるチェックを見つけました。

73         if (kvers < VERSION_CODE(3, 7, 0))
74             no_meta_bg_resize = 1;
75         if (kvers < VERSION_CODE(3, 3, 0))
76             no_resize_ioctl = 1;

何やらカーネルバージョンをチェックしています。3.7.0前だとno_meta_bg_resizeフラグが、3.3.0前だとno_resize_ioctlフラグが立ちます。AWS EC2サーバのカーネルバージョンは3.4.Xなのでno_meta_bg_resizeフラグとは何かを理解しておいた方が良さそうです。

meta_bg

meta_bgとはMeta Block Groupsの略で、ファイルシステムの最大ファイルシステムサイズを増やすために取り込まれた機能です。meta_bgを理解するためには、ext4ファイルシステムがどのようにデータを管理しているかをまず理解する必要があります。

ext4ファイルシステムではブロックグループという単位で複数のブロックをグループ化してデータを管理しています。ブロックグループは最大32,768ブロック(1ブロック4kBとすると128MB分)を1グループとします。ブロックグループの中にはGroup Descriptor Tables(GDT)というブロック領域があり、ここに全ブロックグループを管理するための情報であるグループディスクリプタ(ext4_group_desc構造体)を格納しています。ext4_group_desc構造体は64byteなので、1ブロックあたり4096byte/64bybte=64個のブロックグループを管理出来ます。つまり、全体のブロックグループ数が多ければ多いほど、GDT領域が大きくなっていくことはお分かりいただけると思います。

ext4_bg

ここまで読んでピンと来た人がいるかもしれませんが、全体のブロックグループを管理する情報が各ブロックグループに入っている必要は実はありません。例えば、仮にブロックグループ全てをグループディスクリプタで埋めた場合、128MB/64byte=2^21個のブロックグループを管理することになります。しかしこれでは2^21*128MB=256TBがファイルシステムサイズの上限となってしまいます。もちろんこれは実際のデータや他のメタデータ(データを管理しているデータ。GDTもメタデータの一種)を完全無視して算出した結果なので、実際に扱えるファイルシステムサイズはこれよりも少なくなります。

そこで考えだされたのが、meta_bgです。まず64ブロックグループを1meta groupという単位とします。なぜ64かというと、既に書いたようにGDT領域は1ブロックで64ブロックグループ管理可能で、自meta group内の情報のみGDTに記憶しておくようにするためです。meta groupへのアクセスには、スーパーブロック(ファイルシステム全体を管理しているブロック)に格納されているs_first_meta_bg(最初のブロックグループ)とs_blocks_per_group(1グループディスクリプタ辺り何ブロックあるか)を使用します。これらは32bitの値のため、結果的に2^32 * 128MB = 512PBまでファイルシステムサイズの上限が増えます。(さらに、meta_bgではGDT領域の削減も行っています。具体的には、1meta group内の最初(0番目)、1番目、63番目のブロックグループ内にのみGDT領域を用意します。これにより、61ブロック分の領域を削減出来ます。)

ext4_meta_bg

このようにmeta_bgを適用すると、GDTの役割が、全ブロックグループの管理から自meta groupの管理に変わります。これにより、今回のようにファイルシステムサイズを大きくする際に注意が必要となります。

online_resize_fs()の続き

次に気になるのは、このコードです。

 99     new_desc_blocks = ext2fs_div_ceil(
100         ext2fs_div64_ceil(*new_size -
101                   fs->super->s_first_data_block,
102                   EXT2_BLOCKS_PER_GROUP(fs->super)),
103         EXT2_DESC_PER_BLOCK(fs->super));
104     printf("old_desc_blocks = %lu, new_desc_blocks = %lu\n",
105            fs->desc_blocks, new_desc_blocks);

ここでは拡張後のGDTのブロック数を計算して、出力しています。(GDTについてはmeta_bgの説明の最初の方に書きました) これをわざわざ出力するぐらいなので、 ファイルシステムのサイズの拡張のポイントは、GDT領域を更新することなんだなあ、ということがわかります。(多くのユーザはこれを意味することがわからないと思うのですが・・・)

では既存のGDT領域より大きなGDTが欲しいときはどうするのでしょうか。こういうときのためにext4ファイルシステムでは予めGDTの予約領域(RGDT)をGDTのすぐ後のブロックに用意しています。もちろん予め用意したRGDTで管理可能なサイズより大きなサイズをオンラインで拡張することは出来ません。(このすぐ後に出てきます) RGDTはmkfsコマンドでファイルシステム作成時に決められます。(ただし、meta_bgが有効のときはこの予約領域は存在しません。meta_bgを使っていてGDTより大きな領域が欲しい場合は、meta groupを追加します。)

さあ、online_resize_fs()をどんどん読み進めていきましょう。

107     /*
108      * Do error checking to make sure the resize will be successful.
109      */
110    if ((access("/sys/fs/ext4/features/meta_bg_resize", R_OK) != 0) ||
111         no_meta_bg_resize) {
112         if (!EXT2_HAS_COMPAT_FEATURE(fs->super,
113                     EXT2_FEATURE_COMPAT_RESIZE_INODE) &&
114             (new_desc_blocks != fs->desc_blocks)) {
115             com_err(program_name, 0,
116                 _("Filesystem does not support online resizing"));
117             exit(1);
118         }
119
120         if (EXT2_HAS_COMPAT_FEATURE(fs->super,
121                     EXT2_FEATURE_COMPAT_RESIZE_INODE) &&
122             new_desc_blocks > (fs->desc_blocks +
123                        fs->super->s_reserved_gdt_blocks)) {
124             com_err(program_name, 0,
125                 _("Not enough reserved gdt blocks for resizing"));
126             exit(1);
127         }
128
129         if ((ext2fs_blocks_count(sb) > MAX_32_NUM) ||
130             (*new_size > MAX_32_NUM)) {
131             com_err(program_name, 0,
132                 _("Kernel does not support resizing a file system this large"));
133             exit(1);
134         }
135     }

1つ目のチェックではオンラインリサイズ機能をそもそも備えているかをチェックしています。EXT2_FEATURE_COMPAT_RESIZE_INODEは、resize_inode機能があるかどうかをチェックするためのフラグです。ファイルシステムサイズ拡張用のinode(7番が予約されている)が用意されていれば、このフラグは立ちます。

2つ目のチェックでは先ほど少し書きましたが、今から拡張しようとしているファイルシステムを管理するためのGDTが既存のGDTとRGDTで収まりきれるかを確認します。

3つ目のチェックでは2^32個以上のブロック数を増加させようとしていないかチェックしています。

そしてやっと・・・

144     if (no_resize_ioctl) {
145         printf(_("Old resize interface requested.\n"));
146     } else if (ioctl(fd, EXT4_IOC_RESIZE_FS, new_size)) {
...
171     } else {
172         close(fd);
173         return 0;
174     }

EXT4_IOC_RESIZE_FSリクエストのioctl(2)をしています。ここで何をしているかはカーネルの内部を見て行く必要があります。また、EC2では関係ありませんが、カーネルのバージョンが古い場合はioctl(2)を利用せずにリサイズすることがわかります。

まとめ

resize2fsコマンドは以下のように動作することがわかりました。

  1. bigalloc機能を使っていないかチェック
  2. カーネルバージョンをチェックしてmeta_bgが有効かどうか、  ioctl(2)出来るかをチェック
  3. これから確保しようとしているGroup Descriptor Tablesのサイズを調べ、  確保出来るかチェック
  4. EXT4_IOC_RESIZE_FSリクエストのioctl(2)を発行してカーネルへリサイズを命令

"どうしてこんな1秒もかからずに拡張出来るのだろうか”という疑問は、本質的にメタデータのGDTを更新するだけだから、ということがおおよそわかります。

このようにLinuxコマンドを調査することで、カーネル内部の動作をある程度予想することが出来ます。実際にカーネルのソースを読んでみなければ正確なことはわかりませんが、ある程度あたりを付けておくとカーネルソースコードが読みやすくなります。カーネル内の挙動を詳細に追う前に、是非コマンドのソースを読んで、どんなシステムコールを使っているのか、どんなチェックをユーザ空間で行っているかを調べることをおすすめします。

今回はresize2fsコマンドのソースを読みましたが、ioctl(2)が発行された後にカーネルが実際にどのような処理をしているのか、Linuxカーネルのソースを次回読んでいきます。

参考文献

MySQLのINSERTを高速化するChange bufferingをソースコードから理解する

$
0
0

mysql_logo

背景

アカツキで提供しているサービスでは、ほぼ全てにおいてAWSのRDS(MySQL5.6, InnoDB)を使用しております。 ソーシャルゲームでは多くのWriteがかかりますが、そのコストが気になったので調べてみました。

調べてみたこと

一言にINSERTといっても、COMMIT時に即テーブルスペースに反映されるような単純な処理ではありません。 例えばINSERTしたテーブルにセカンダリインデックスがある場合、そこに加えられた変更を反映しなければなりません。 ところが、INSERT時バッファプール内に該当するインデックスのページが無い時はこれをディスクから読みだす必要があり、非常にコストが掛かります。 InnoDBにはこれを抑えるための"Change buffering"という機能があり、今回はその解説をしたいと思います。

Change bufferingとは

変更を加えたページがバッファプール内に無い場合、InnoDBはページを読み込むのではなくChange bufferと呼ばれる領域にその変更を記録します。 Change bufferは共有テーブルスペースに作成されます。なので、再起動等を行ったとしてもその変更が失われることはありません。

Configuring InnoDB Change Bufferingによると、Change bufferに書き込んだ変更は以下のタイミングでテーブルスペースに反映されると言われています。

  1. 該当するインデックスのページがバッファプールに読まれた時
  2. バックグラウンド実行によるマージ

これらに関して、詳細な動作を追ってみたいと思います。 Change bufferingはバックグラウンドで行われるため観測不可能ですので、その実装を追ってみました。

1. 該当するインデックスのページがバッファプールに読まれた時。

インデックスのページが読まれた時にマージ処理が行われるということなので、まずはInnoDBのページを読み出す処理を探してみましょう。

2486 /********************************************************************//**
2487 This is the general function used to get access to a database page.
2488 @return pointer to the block or NULL */
2489 UNIV_INTERN
2490 buf_block_t*
2491 buf_page_get_gen(
...
2838     if (!recv_no_ibuf_operations) {
2839       if (access_time) {
2840 #ifdef UNIV_IBUF_COUNT_DEBUG
2841         ut_a(ibuf_count_get(space, offset) == 0);
2842 #endif /* UNIV_IBUF_COUNT_DEBUG */
2843       } else {
2844         ibuf_merge_or_delete_for_page(
2845           block, space, offset, zip_size, TRUE);
2846       }
2847     }

大きな関数ですので、一部割愛しています。 コメントにある通り、buf_page_get_gen関数はインデックスのページにアクセスするための一般的な関数です。 buf_page_get_genでは関数中でibuf_merge_or_delete_for_pageを呼び出しています。(ibufはChange bufferの前身であるinsert bufferの略) ibuf_merge_or_delete_for_pageの定義は

4540 /*********************************************************************//**
4541 When an index page is read from a disk to the buffer pool, this function
4542 applies any buffered operations to the page and deletes the entries from the
4543 insert buffer. If the page is not read, but created in the buffer pool, this
4544 function deletes its buffered entries from the insert buffer; there can
4545 exist entries for such a page if the page belonged to an index which
4546 subsequently was dropped. */
4547 UNIV_INTERN
4548 void
4549 ibuf_merge_or_delete_for_page(

とあり、ここでChange buffer内の変更を適用しています。 他にもページを読むための処理が有りますが、そこでも同じようにibuf_merge_or_delete_for_pageが呼び出されているようなので、 変更のあったページを読み込んだ時はChange buffer中の変更がマージされるようです。

2. バックグラウンド実行によるマージ

mysqlにはmaster threadと呼ばれるinnodbのバックグラウンド処理を行うスレッドがありますが、 この中でChange bufferのマージが行われています。

2055 /*********************************************************************//**
2056 Perform the tasks that the master thread is supposed to do when the
2057 server is active. There are two types of tasks. The first category is
2058 of such tasks which are performed at each inovcation of this function.
2059 We assume that this function is called roughly every second when the
2060 server is active. The second category is of such tasks which are
2061 performed at some interval e.g.: purge, dict_LRU cleanup etc. */
2062 static
2063 void
2064 srv_master_do_active_tasks(void)
2065 /*============================*/
...
2094   /* Do an ibuf merge */
2095   srv_main_thread_op_info = "doing insert buffer merge";
2096   counter_time = ut_time_us(NULL);
2097   ibuf_contract_in_background(0, FALSE);
2098   MONITOR_INC_TIME_IN_MICRO_SECS(
2099     MONITOR_SRV_IBUF_MERGE_MICROSECOND, counter_time);

2097行目のibuf_contract_in_backgroundを辿って行くと、ibuf_merge_spaceなどが見つかります。 さらに辿って行くと、ibuf_merge_space中からbuf_read_ibuf_merge_pages関数経由でibuf_merge_or_delete_for_page関数を呼び出しており、ここでChange bufferのマージを行っていることがわかります。 ibuf_contract_in_backgroundは主に、master threadにおけるバックグラウンド処理で呼び出されています。 master threadによるバックグラウンド処理にはサーバーがアクティブ状態のとき(srv_master_do_active_tasks)とサーバーがアイドル状態の時(srv_master_do_idle_tasks)の2種類があり、そのうち両方でChange bufferのマージが行われますが、少しずつ挙動が違います。まず、master threadによるアクティブ状態とアイドル状態の判定は以下のようになっています。

2342  if (srv_check_activity(old_activity_count)) {
2343    old_activity_count = srv_get_activity_count();
2344    srv_master_do_active_tasks();
2345  } else {
2346    srv_master_do_idle_tasks();
2347  }

srv_check_activity関数はold_activity_countと現在のactivity_countに差分があるかどうかを見ており、 前回srv_master_do_active_tasksを実行してから再度srv_master_do_active_tasksを実行する間にサーバーに対してアクションがあったかどうかでアクティブ/アイドル状態を切り替えています。 アクティブ状態とアイドル状態で実行されるibuf_contract_in_backgroundの違いは第二引数のbool値にあり、アクティブ状態では引数がFALSE

2094   /* Do an ibuf merge */
2095   srv_main_thread_op_info = "doing insert buffer merge";
2096   counter_time = ut_time_us(NULL);
2097   ibuf_contract_in_background(0, FALSE);
2098   MONITOR_INC_TIME_IN_MICRO_SECS(
2099     MONITOR_SRV_IBUF_MERGE_MICROSECOND, counter_time);

アイドル状態では引数がTRUE

2186   /* Do an ibuf merge */
2187   counter_time = ut_time_us(NULL);
2188   srv_main_thread_op_info = "doing insert buffer merge";
2189   ibuf_contract_in_background(0, TRUE);
2190   MONITOR_INC_TIME_IN_MICRO_SECS(
2191     MONITOR_SRV_IBUF_MERGE_MICROSECOND, counter_time);

となっており、他の実装に差はありません。 では、この第二引数は何を変化させているのでしょうか? ibuf_contract_in_background関数の実装を追ってみましょう。

2790 /*********************************************************************//**
2791 Contracts insert buffer trees by reading pages to the buffer pool.
2792 @return a lower limit for the combined size in bytes of entries which
2793 will be merged from ibuf trees to the pages read, 0 if ibuf is
2794 empty */
2795 UNIV_INTERN
2796 ulint
2797 ibuf_contract_in_background(
2798 /*========================*/
2799   table_id_t  table_id, /*!< in: if merge should be done only
2800           for a specific table, for all tables
2801           this should be 0 */
2802   ibool   full)   /*!< in: TRUE if the caller wants to
2803           do a full contract based on PCT_IO(100).
2804           If FALSE then the size of contract
2805           batch is determined based on the
2806           current size of the ibuf tree. */
2807 {
...
2819   if (full) {
2820     /* Caller has requested a full batch */
2821     n_pages = PCT_IO(100);
2822   } else {
2823     /* By default we do a batch of 5% of the io_capacity */
2824     n_pages = PCT_IO(5);
2825

少し長いですが、第二引数はPCT_IOマクロの引数の値を変化させています。 PCT_IOマクロの実装は以下のようになっています。

300 /* Returns the number of IO operations that is X percent of the
301 capacity. PCT_IO(5) -> returns the number of IO operations that
302 is 5% of the max where max is srv_io_capacity.  */
303 #define PCT_IO(p) ((ulong) (srv_io_capacity * ((double) (p) / 100.0)))

ここでは実装を追うのを割愛しますが、srv_io_capacityの中身はinnodb_io_capacity(※1)の値となります。 ここから分かることは、ibuf_contract_in_backgroundの第二引数がTRUEの時は100%の、FALSEの時は5%+αのinnodb_io_capacityを使用してChange bufferのマージを行うということになるということで、 アイドル時は設定しているinnodb_io_capacityを使いきってしまうため、この間サーバー負荷が上がると予想できます。

Change buffering機能について、特に気をつけたいこと。

バージョンアップの際にフォーマットが変わることがある。

少なくとも過去4回のアップデート中にChange Bufferの構造が変わっています。

  1. バージョン4.1.x未満
  2. バージョン4.1.x以上(innodb_file_per_tableが実装され、space idが必要になった)
  3. バージョン5.0.3以下(ROW_FORMATオプションが追加され、Change buffer中にこれに対応するためのフラグが追加された)
  4. バージョン5.5.0以上(Insert bufferingがdeleteやpurgeに対応したChange Bufferingになった)

今後も構造が変わることが予想されます。 そのため、アップデート前にシャットダウンする際は必ずinnodb_fast_shutdownを0に設定し、Change bufferのマージを完了させなければなりません。

Change Bufferのデフォルトの最大サイズはバッファプールの25%

/** Default value for maximum on-disk size of change buffer in terms
of percentage of the buffer pool. */
#define CHANGE_BUFFER_DEFAULT_SIZE  (25)

デフォルトの最大サイズはinnodb_buffer_pool_sizeの25%となっていますが、この25%という値は5.6.2からinnodb_change_buffer_max_sizeで変更可能になっています。 なぜこの値が変更可能になっているかというと、Change BufferのサイズがChange bufferの最大サイズの半分を超えた時、アクティブ/アイドル状態に関係なく強制的に100%のinnodb_io_capacityを使用してマージが実行されてしまうからです。また、Change bufferのサイズが最大サイズを超えた場合、Change buffering機能は動作せず、書き込むページをディスクへ読みに行くためIOが発生してしまいます。 この動作は突然サーバーのパフォーマンスが落ちることにつながるので、不安なときは監視したいですよね。 Change bufferの現在のサイズはSHOW ENGINE INNODB STATUSコマンド中のINSERT BUFFER AND ADAPTIVE HASH INDEXセグメントで得ることができます。 以下は、INSERT BUFFER AND ADAPTIVE HASH INDEXセグメントの例です。

-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 83, seg size 85, 3600 merges
merged operations:
 insert 17730, delete mark 167305, delete 105
 discarded operations:
  insert 0, delete mark 0, delete 0
  Hash table size 276671, node heap has 100 buffer(s)
  0.00 hash searches/s, 0.00 non-hash searches/s

sizeがChange bufferのサイズですが、この数字はページ数なので実サイズを得るためにはinnodb_page_size(デフォルトで16KB)を掛け算する必要があります。 例だと16KBがChange bufferのサイズとなります。ただし、異なるサイズの行がChange bufferに保存されるため、この値はおおよその値となります。 この値を監視したいときは、以下のようにすることができます。

expr `mysql -uroot -e 'show engine innodb status\G' | awk '/^Ibuf: size [0-9]+, free list len [0-9]+, seg size [0-9]+, [0-9]+ merges/ { print $3; }' | sed "s/,//"` \* 16384

実行結果はChange bufferのサイズ[byte]となります。

また、そもそもChange bufferのサイズが大きくならないように、バッファプール中にページが有ることも合わせて監視したいと思います。 Buffer pool hit rateを監視しましょう。 Buffer pool hit rateは同じくSHOW ENGINE INNODB STATUSコマンド中のBUFFER POOL AND MEMORYセクションにあります。 以下は、BUFFER POOL AND MEMORYセクションの例です。

----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 13588365312; in additional pool allocated 0
Dictionary memory allocated 85572888
Buffer pool size   810368
Free buffers       8186
Database pages     760386
Old database pages 280528
Modified db pages  38524
Pending reads 1
Pending writes: LRU 0, flush list 20, single page 0
Pages made young 120864074, not young 3085053198
11.00 youngs/s, 33.99 non-youngs/s
Pages read 205587158, created 3544965, written 283537176
13.25 reads/s, 2.25 creates/s, 108.97 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 2 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 760386, unzip_LRU len: 0
I/O sum[49680]:cur[456], unzip sum[0]:cur[0]

Buffer pool hit rateは通常、1000に近い値になります。この値が1000に近いほどバッファプール中にページが存在する確率が高くなり、readのためのIOを節約できます。 Buffer pool hit rateを監視したいときは、以下のようにすることが出来ます。

echo `mysql -uroot -e 'show engine innodb status\G' | awk '/^Buffer pool hit rate [0-9]+ /{print $5}'` | awk '{if(length($0) == 0) {print "0"} else{print}}'

結論

InnoDBにおいて「バッファプール上にデータが乗らなかった」時、バックグラウンドでこのような処理が行われています。 性能が落ちたり、定常的に高負荷になるのには必ず理由があります。 最大の性能が発揮できるよう、パラメータチューニングには気を配りたいですね。

(※1) innodb_io_capacityMySQLのバックグラウンド処理に使用するIOPSを設定する値です。


Akatsuki ENGINEER INTERVIEW

$
0
0

engineerinterview_longアカツキエンジニア陣のインタビューを媒体別にまとめましたので、ぜひご覧ください。 (掲載順位は、新着順になります)

【CTOの職務履歴書】自己組織化に向けて、ポジティブな情熱を持ち続けることが最高の開発を生む。|株式会社アカツキ CTO 田中勇輔氏【後編】#レバテックタイムアカツキCTOがアカツキ転職後に感じたこと、現場での仕事内容や成長、これからの展望について詳しく触れられています。

【CTOの職務履歴書】高校時代に作ったプログラムがクラスで流行!人を楽しませた感覚が忘れられず、ITエンジニアに|株式会社アカツキ CTO 田中勇輔氏【前編】 #レバテックタイムアカツキCTOが学生時代にプログラミングに出会ったきっかけ、前職をやめてアカツキに転職した経緯について詳しく触れられています。

ハッカー気質なアカツキの愛あるプロダクト作り #geechs magazineアカツキのテクノロジーを統括するCTOに直撃取材!CTOの仕事とは、どういったものなのか?アカツキで働くエンジニア、これからのビジョンについて触れられています。

【アカツキ×Aiming対談】ゲーム会社のCTOが求める「愛あるエンジニア」とは? #CodeIQアカツキCTOとAiming CEnOによる対談「後編」!エンジニアのチャレンジを支える開発環境や文化をどう作っていくか、そしてどのようなエンジニアを求めているのかについて触れらています。

【アカツキ×Aiming対談】スタートアップのCTOが語るエンジニアリングチーム文化の作り方 #CodeIQアカツキCTOとAiming CEnOによる対談「前編」!お互いの業務での共通点やゲーム作り、チーム作りにおける、両者の哲学について触れられています。

【偏見なしにコードで出会った】アカツキはなぜこのサーバーエンジニアを採用したのか? #CodeIQ エンジニアがCodeIQのスカウト問題をきっかけにアカツキに採用。その真相とは一体? 採用に至った経緯、アカツキがCodeIQを使用する理由などに触れられています。

Be Engineer!▼『株式会社アカツキ』を見学&インタビューしてきた! Part 2 ▼ #Be Engineer! 実際に開発が行われている「執務エリア」に潜入! Rubyエンジニアの1日やアカツキへの入社理由、アカツキを目指すエンジニア学生などについて触れられています。

差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介

$
0
0

背景

ゲームサーバーで扱うデータはとにかく多いです。 特にイベントなどをほぼ毎日運用する場合はサービス開始から1年でレコード数が無視できない量になり、それに伴って更新に要する時間が激増してしまいます。

以前執筆した9分43秒のデプロイを19秒にした話でもデータ更新を速くする為に変更テーブルをgit差分から取得する方法を紹介しました。 アカツキではデータの追加や管理の為に、seed_fuというデータ更新用Gemを使っています。差分データファイルを検出し、seed_fuに一致するテーブルだけを指定する方式で毎回データベースを更新していました。 しかしseed_fuが何らかの方法で失敗した場合やgitの操作ミスで差分が無くなってしまった場合のリカバリに欠けていたため、結局seed_fuを全てのテーブルで実行し直すという自体が少なからずありました。

外部ツールを追加してこのへんを管理できるかなと思ってしばらく探してみたのですが、gitの中に別のバージョン管理を入れるのはいかがなものかなというのと、運営チームのオペレーションが複雑化するのが懸念されたので一旦考え直すことに。 そこで結局git等の外部ツールに頼らずにseed_fuの実行毎に更新差分だけ自動でテーブルを指定する方法を設計したのでここでご紹介します。

要件定義

  1. 外部ツールに依存しない方法で作成する。
  2. 各テーブルの最新状況を保存できる方法を用意する。
  3. 各テーブルが更新が必要かどうかを判別できる方法を用意する。

実際にやったこと

データベースに新しいテーブルを追加する

まず管理用データの保存先として、データベースに新しいテーブルを追加します。

class CreateSeedCaches < ActiveRecord::Migration
  def change
    create_table :seed_caches do |t|
      t.string :table_name, null: false
      t.string :cache_value, default: "0"

      t.timestamps
    end
  end
end

ここではテーブル名と保存値(cache)を登録できるようにします。timestampsカラムも追加しましたがこちらはデバッグやエラー発生の時に便利だと思っただけなので必須ではありません。実行速度改善のためにindexをtable_nameにつけてもいいですが、テーブルの数はそこまで増えないのでここでは省略しています。

各テーブルの最新情報を保存する

次に更新元となるymlファイルのハッシュ値(md5)を毎回格納するようにします。 各テーブルをフラット化した際のハッシュデータを格納する方法も考えたのですが、オーバーヘッドに時間がかかりすぎたので断念しました。

# ymlファイルからmd5を計算
def yml_to_md5(table)
  Digest::MD5.file("フォルダパス/#{table}.yml").hexdigest
end

# ハッシュ値を更新
def update_cache(tables=[])
  tables.each do |table|
    cache_info = SeedCache.find_or_create_by(table_name: table)
    cache_info.cache_value = yml_to_md5(table)
    cache_info.save!
  end
end

各テーブルが更新が必要かどうかを判別する

ハッシュ値の差分から更新するテーブルを判別して、seed_fuを実行します。

def hoge
  #seed可能なテーブルを全抽出
end

desc "seed_fu with cache mechanism"
  task express: :environment do
    tables = hoge
    targets = [] # 更新対象を登録場所

    tables.each do |table|
      digest = yml_to_md5(table)
      cache_info = SeedCache.find_or_create_by(table_name: table)

      # ハッシュ値が違っていたら更新対象に登録
      if digest != cache_info.cache_value
        targets.append(table)
      end
    end

    # 更新されたテーブルが無い!
    next if targets.size < 1

    # テーブル指定
    ENV['TABLES'] = targets.join(",")
    pp "Executing seed_fu TABLES=#{ENV['TABLES']}"

    # seed_fu呼び出し
    Rake::Task['db:seed_fu'].invoke
  end
end

これでseed_fu高速版、seed_fu:expressの完成です。 が、同僚から「何もないseed_fu実行時もハッシュ値を保存すれば、seed_fu:express移管して直ぐ使えますよね」と提案されたのでseed_fuにenhanceを導入して拡張しました。

Rake::Task['db:seed_fu'].enhance do
  update_cache( ENV["TABLES"].try(:split, ",") || [] )
end

これをするとseed_fuを打てば必ずテーブルキャッシュが更新されるようになります。二回更新が走らないようにseed_fu:expressの最後のキャッシュ更新を削除しましょう。

まとめ

いかがでしたでしょうか? いままで入力ミスで実行に失敗したする度にseed_fuかける必要がなくなりました。 副産物では有りますが、ローカル環境のサーバーブランチを切り替えた後のデータ更新も切り替えが発生したファイルのみが対象になるようになったのでかなり短縮できるようになりました。

seed_fuはRailsエンジニアなら一度は見たことがあるほど有名なGemかと思います。 しかしdb依存する内容であるからなのかキャッシュ化や高速化についてはあまり言及されていないようです。この手の高速化は意外と見つからないのですが、やってみるとそこまで難しくないのだなと思いました。

FactoryGirlのログからテストコストを計測してみた

$
0
0

背景

アカツキではサーバーサイドフレームワークとしてRuby on Railsを採用しており、またそのテスト環境としてRspec/FactoryGirlを使用しています。RoR環境下のテスト体制としてはデファクトスタンダードになっているこの組み合わせですが、主にFactoryGirlの採用については 賛否両論だと言われています。採用を見送るネガティブな要員として、きちんとtraitを管理しないと不必要な関連オブジェクトも芋づる式に生成してしまい、テストの実行時間を大幅に増やしてしまうという問題があります(もっとも、これはFactoryの設計、管理を怠った開発者が原因であり、FactoryGirl自体の罪ではないかもしれませんが)。僕も複雑な条件のtraitの組み合わせや、テストの実行時間に日々悩まされています。

調べてみたこと

しかし実行時間に悩まされているとは言っても、どのFactoryがネックになっているか、ということについて実は確信を持っていないことに気づきました。FactoryGirlリポジトリのGETTING_STARTED.mdにあるものをそのままコピペしてきた、重いfactoryを通知してくれるちょっとしたスクリプトは使っていたのですが、どうにも情報源としてはあんまり有用とはいえません。なのでもっと詳しい情報が得られないか、少しFactoryGirlのコードを調べてみました。

コードを読んでみる

FactoryGirlが教えてくれるこのslow factoryの通知はActiveSupport::Notificationsを利用して行われています(こちらのエントリが詳しいです)。つまりActiveSupport::Notifications.subscribe("factory_girl.run_factory")に対応する通知を送っているところ(Notifications.instrument)がどこかにあるはずなのでそれっぽい単語でソースコードを検索すると、

# @lib/factory_girl/factory_runner.rb

instrumentation_payload = { name: @name, strategy: runner_strategy }

ActiveSupport::Notifications.instrument('factory_girl.run_factory', instrumentation_payload) do
  factory.run(runner_strategy, @overrides, &block)
end

というところが見つかります。このinstrumentation_payloadのhashが通知データとして受け取れるので、factory名とstrategy名はここから取得していたのだとわかりますね。とりあえずデフォルトの状態ではこれ以上はなにも情報を渡してくれていないようです。

しかしながらせっかくこのような仕組みを用意してくれてるのだから、勝手にforkして適当なデータを詰めれないものかとみてみると、辺りによさそうなインスタンス変数やらlocal変数がありますね。

factory = factory.with_traits(@traits)
@overrides = traits_and_overrides.extract_options!
@traits    = traits_and_overrides

factoryというデータが一般的な名前なので色々情報持ってそうだな、と期待できますね。適当にinstrunment_payloadに詰めて見てみると、例えば

# factory.definition.callbacks
=>[]

# factory.definition.attributes
=>, :build}]> 

などのデータが取得できました。他にも様々なデータが含まれており、うまく処理すれば実行されたfactoryのほとんどの情報を得られそうです。

(簡単な情報をparseするgemを作りましたので、よろしければご利用ください。しかしこのinspectorをテストするためのFactoryオブジェクトの生成方法がわからずテストが書けてない……どなたか助けていただけると大変嬉しいです)

せっかくなので集計してみる

適当にFactoryLogなるモデルを通知から生成して、factory名+trait名の組み合わせを一意の識別子としてかかった時間を集計してランキング化してみると

moz.png

と、(どのtraitの)どのfactoryが1つあたり平均どのくらい時間を取り、またどのくらいの量がテスト中に実行されているのか、というデータがわかりました。これにより、例えばテスト時間を短縮したいと思った時にどのfactoryから改善していくか(例えば一部をDBに静的に置いておくようにしたり、mock objectを使うようにするなど)みたいな戦略が立てやすくなりました。

(ちなみにこちらもRails engineとして簡単なgemを書きました。といっても簡単なクラスメソッドをちょこっと書いたモデルがあるだけのものですが……こっちもcontributeお待ちしております)

まとめ

「推測するな、計測せよ」「プログラムの処理にかかる時間の80%はコード全体の20%の部分が占める」とは有名な言葉ですが、OSSのライブラリの場合、デフォルトでは提供されていないような情報も、少し手を加えることで取得できたりすることがあるので、気になったりしたら少しコードを読んで調べてみると、より効率的な業務が可能になるかもしれません。今回はFactoryGirlのコードに少しだけ手を加えてテストデータ生成のコストを計測する方法についてご紹介しました。

おまけ

実はRspecのそれぞれのexample(テストケース)についても、このようにメタデータが取得できます。 また、独自にメタデータを定義もできるようです。うまく使えばテストに対してtag付けして一部のテストのみを設定でskipしたり、fail時に詳細情報を出力するようにしたり色々便利になりそうです。

参考: RSpec 3の重要な変更

CloudWatch Logs + fluentdでモダンなアプリ監視をさくっと作ってみた話

$
0
0

背景

アカツキが提供しているサービスはリリース前に必ずテストを行っています。テストでバグが見つかったときにこれを切り分けるため、発生時のログを探すことがあります。「クライアントアプリで明らかに表示がおかしい」とか、そういったバグなら問題ないのですが、クライアントアプリからでは見えないサーバー側のバグが起きていて、それが見逃されてしまう…なんてこともあるかもしれません。

ふと、「機械的にこれを検出できるといいなー、あとログを探るためだけに毎回SSHするのもめんどくさいなー」 と思い、手を動かしてみることにしました。

今回の要件

  • テスト環境でサーバーエラーが起きたのを機械的に検出して、すぐ知らせてほしい
  • アクセスしやすいところに必要なログだけがあると嬉しい
  • できれば簡単な方法でやりたい

ツールの選定

アプリケーションの例外を検知して開発者に知らせてくれるものに、ExceptionNotifierAirbrake(有料)、Errbitなどがありますが、インストールするためにいずれもコードの修正が必要になります。

コードの修正をしなくて良い方法を模索していて、そこで目をつけたのがCloudWatch LogsというAWSのサービスでした。CloudWatch Logsではログに対してアラートを設定することが出来ます。 また、ログの保存に耐久性の高いストレージを使用することが出来ます。エラーの検知に関してはfluentd使用してログをtailすれば実現できそうです。

CloudWatch Logsとfluentdを利用する方法だとコードを変更する必要がなく、かつモジュール同士が疎結合になるので設計上とても実用的です。また、HipChatやSlackなどのチャットサービスを経由した通知もfluentdのプラグインを利用すれば比較的簡単に作ることできそうなので、アカツキ的に使いやすいということもありました。

CloudWatch Logs とは

CloudWatch LogsAWSのログファイルのリアルタイムモニタリングサービスです。

  • ログファイルの格納
  • ログファイルの監視
  • ログファイルの閲覧
  • アラートの設定
  • 保持期間の設定

などが出来ます。

2014年12月からTokyoリージョンでも利用できるようになりました。

やってみた

概要

サーバーエラーはRuby on Railsのログに全て記載されるので、ここを監視するのが比較的簡単にエラーを知る方法かなと思います。サーバーエラーが発生した時はそのログをCloudWatch Logsに投げるようにして、CloudWatch側でアラートを設定します。

IAM Userの作成

ポリシーを以下のようにして、CloudWatch Logs用のIAMユーザーを作成しました

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:*",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:*:*",
                "arn:aws:s3:::*"
            ]
        }
    ]
}

(参考: https://github.com/ryotarai/fluent-plugin-cloudwatch-logs/blob/master/README.md)

作成したら、AccessTokenとSecretを控えておきます。

fluentdの設定

1. fluent-plugin-cloudwatch-logs プラグインをインストールします。

$ /usr/lib64/fluent/ruby/bin/gem install fluent-plugin-cloudwatch-logs --no-ri --no-rdoc

2. td-agent.confに設定を記述する

<source>
      type tail
      path /path/to/logfile.log
      pos_file /var/log/td-agent/logfile.log.pos
      tag app.error
      format /^E, (?<log>.*)$/
</source>
<match app.error>
      type cloudwatch_logs
      log_group_name your_log_group_name
      log_stream_name your_log_stream_name
      auto_create_stream true
</match>

3. 環境変数の設定

td-agentユーザーにはログインシェルがなかったため、/etc/init.d/td-agentに以下のように記述しました。

export AWS_REGION="your_region" 
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY" 
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_ACCESS_KEY"

4. td-agentの起動

$ service td-agent start

5. td-agent.logの確認

デフォルトだと/var/log/td-agentにあるtd-agent.logにエラーが表示されていないことを確認します。

アラートの設定

fluentdが起動してAWS ConsoleからCloudWatch Logsに移動すると、Log GroupとLog Streamが自動で生成されていました。Log Groupsからアラートを設定したいLog Groupを選択してCreate Metric Filterからアラートを設定します。

f:id:aktsk_hackers_lab:20161209184649p:plain

Filter Patternに検出パターンを入力して、アラートを設定したら完成です!

まとめ

簡単なアプリの監視をアプリ側の変更なしで、かつ少ない手順で作成することが出来ました。
機械的にエラーを検出することができ、通知もログの取得も簡単になりました。

このように一時的なログの置き場として利用したり、ログ自体を監視したい時にCloudWatch Logsは便利なサービスです。機会があれば利用してみてはどうでしょうか。

LITALICO社と合同エンジニア勉強会を開催した話

$
0
0

合同勉強会を開催

6月25日(木)に、株式会社LITALICOさんと合同エンジニア勉強会を、アカツキのオフィスで開催しました!アカツキのエンジニアと合わせて約20名がこの勉強会に参加し、LT対抗戦+ビアバッシュを楽しみました。想定以上に話したがり屋さんが多かったからか、LTの発表時間が予定の10分から5分に当日になって変更になったり、ピザが無くなっても話が止まらないなど、非常に大盛況な会となりました。

4F-12

LT発表内容

今回のLTの発表内容は以下の通りです。

  1. アカツキの会社紹介(アカツキ)
  2. LITALICOさんの会社紹介(LITALICO)
  3. 社内OpenGL勉強会からわかった勉強会の進め方(アカツキ)
  4. セブ島英語留学のススメ(LITALICO)
  5. シンデレラシリーズのイベントデータ管理をこうしたい(アカツキ)
  6. 信号処理入門(アカツキ)
  7. 信号処理を利用した特定の放送局を映さない技術の紹介(LITALICO)
  8. カーネルの挙動を可視化してみた(アカツキ)
  9. ゲームサーバインフラの事例紹介(アカツキ)
  10. NewRelicの紹介(アカツキ)
  11. ゲームのレスポンスとユーザ満足度について(アカツキ)
  12. キーパーソンが誰かが重要だという話(LITALICO)

ご覧の通り、会社の事業内容と勉強会内容は必ずしも紐づいていないのですが、LTの話題をベースに会社同士の交流が出来たのはとても良い機会でした。

私は8番のカーネルの挙動を可視化してみた、を発表しました。この発表では、ftrace + trace-cmd + kernelsharkを使って、無限ループの処理をバックグラウンドで動作させているときと、明示的に何もバックグラウンドで動作させていないときの、プロセススケジューリングの状態を可視化するデモをしました。

今後のこと

私も実は初めてLTで発表したのですが、短い時間でどれだけキーポイントをおさえるか、どこに爆発力のある言葉を入れるか、短時間でも面白いと思わせるような発表にするにはどうしたらいいか、など考える力は普段の業務でも活かせそうだな、と感じました。また、短い時間でサクっと終わるのも、通常の発表より話す方も聞く方も負担が少なくて良いところだと思いました。

折角LTの良さに気がついたところですが、今のところアカツキの社内でLTを明示的にやる場が残念ながらありません。アカツキでは隔週でエンジニアだけが集まるミーティングがあるのですが、そこでLTを持ち回りでやるのは最初の一歩としてはアリかもしれません。また、エンジニアだけでなく、全社的にLTの良さを広めていけたらと思っています。

もしアカツキとの合同勉強会にご興味のある方は、facebookコンタクトフォームにてご一報いただけたら幸いです!

Viewing all 223 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>