woshidan's blog

そんなことよりコードにダイブ。

「はじめてのOSコードリーディング UNIX V6で学ぶカーネルのしくみ」を読みました

Javaの並行処理の後はこちらの本を読んでいました。

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

この本を読む前に転職にあたって少し背伸びをして「Inside Android」や「Androidを支える技術」を読んでいたのですが、initプロセスやスケジューラの話でぴんと来ずもしかしなくてもLinux的な本を何か読んだほうがいいのでは、と感じていました。

この本を手に取ったきっかけは、ゲーム系から来た人にC言語系統で何を読めばいいかと聞かれた時Linuxカーネルだよとばかり返ってくるため、それでは初心者はどこから読んだらいいかと聞きなおしたら勧められた感じです。

読んでいる最中は前半の実行プロセスの切り替えあたりでは知らない単語、知らない形のコード、大量の副作用でのデータのやり取りに頭が結構混乱するのですが、5章くらいから辞書として前半を弾き始めて多少理解できたような、できなかったような。。

前書きの読み解くコツに「まず最後まで読む」とあったのでなんとか最後まで通した感想としては、ファイルシステムと物理的なストレージの対応、パソコンの中でたくさんプロセスが動いておりそのやり取りの様子とは、みたいな当たり前のことに対してなんとなく自信を持てた感じです。

C言語の読解については自信がつきませんでしたが、nginxとかUnicornとかサーバの設定のその辺の話を調べられるようになっていて驚きました。

それにしても、この本で扱われているUnix v6のリリースが1975年、その前のJava並行処理の本が2006年初版なので、年明けはもうちょっと現代の空気にふれようと思います。

以下、いくつかの項目を雑にメモして締めます。口調が違う項目もありますがそういう感じで、現場からは以上です。

スケジューラと実行プロセス

proc[]エントリの1番に登録されていて、特定のアルゴリズムで次の実行プロセスの選択、切り替えを行う特別なプロセスおよびそのプロセスで動くプログラム。

実行プロセスの選択アルゴリズムは、Unix v6の場合、プロセスが使用したCPUの累積時間が大きいほど選択されにくい、というもの。

実行プロセスはカーネルグローバル変数uを通してuser構造体にアクセスできるようにカーネルAPRが設定され、実行プロセスの切り替え時にはカーネルAPR, ユーザAPR, 前回実行時に汎用レジスタに入っていた値の復元を行う(p.75)。

プロセスが今のプログラムに関するデータをどういう形で持っているか

各プロセスはproc構造体とuser構造体を1つずつ持っていて、プロセスの実行状況などはuser構造体が持っている。

user構造体が持っているのは

  • 各種計算やプログラムの実行位置の管理に使う汎用レジスタの状況
  • ユーザID/グループID
  • 実行中のカレントディレクト
  • そのプロセスでオープンしているファイル
  • シグナル、システムコール、ファイル読み込みのためのデータ受け渡し愚痴となる変数

などで、各種計算やプログラムの実行位置の管理に使う汎用レジスタの状況についてはプロセスが中断される時にuser構造体に退避されるが、その詳細が2章で紹介されている。

proc構造体が持っているのは

  • プロセスが実行可能かどうか
  • 実行時間がどれくらいか
  • 何かの資源が必要で実行を中断しているのか

といった、カーネルがプロセスを交代させたりするのに使う情報といった感じ。

proc構造体の情報は次にどのプロセスを実行させるかを判断する際に使われるので、現在実行していないからといってメモリから退避させることはできないが、user構造体は実行中以外はメモリ上にいる必要がないため、スワッピングの対象となる。

ファイルシステムとinodeとは

Unix系のシステムで扱うデバイスはブロックデバイスとキャラクタデバイスがあるのだけれど、プログラム含めたファイルの内容が載っているのがブロックデバイスというイメージ。ファイルシステムはブロックデバイスのデータを構造的に扱う仕組みのこと(p.258)。

その文脈でファイルはそのファイルを定義するinodeとファイルのデータから成っている。inodeはカーネルがいじる単位でのメタデータといった感じでファイルサイズやファイルのアクセス権限、ファイルのデータが保存されている。ファイル操作を行う最、カーネルの一番最初の仕事は目的のファイルのinodeを取得することになる。

inodeはファイル本体のデータとは少し離れた領域に保存されている(p.264)。

面白かったのは、ファイルの削除を行った時、実際にはinode領域だけが削除されていて、ファイルのデータが入っている領域は別のファイルによって上書きされるまでそのまま、という話。

http://www.fujitsu.com/jp/services/infrastructure/maintenance/lcm/service-phase4/h-elimination/

こういうのはそういう意味なんですね!

ブロックデバイスサブシステム

ブロックデバイスに入ってるデータを人間が扱いやすい形で操作するための仕組みがファイルシステムとしたら、コンピュータが効率的にメモリ上に出し入れするための仕組みがブロックデバイスサブシステムっぽい。

メモリ上にバッファ領域があり、そこにブロックデバイスから読み込んだデータをどこから読み込んだかとセットで管理、といった感じ。

プロセスの割り込みについて

(p.144あたり)実行プロセスの処理を中断する場合は、割り込みとトラップによるものがあって、割り込み

  • ブロックデバイスの動作完了通知(ファイルからのデータ読み込み完了通知など)
  • 端末からの入力
  • クロック割込み

など、非同期処理に関して使われます。トラップも同様にプロセスの中断がおこなわれますが

  • 0除算
  • 割り当てられない領域へのアクセス
  • バスタイムアウト

などCPU内部の出来事による中断・再開を扱います。

ユーザプロセスが引き起こすトラップは最終的にシグナルとして処理されます。

正直、ユーザプロセスから送られたトラップがシグナルとして~...のあたりを読んだときはあまりピンとこなかったのですが、こちらの記事など、ウェブアプリケーションフレームワークの設定ファイルでサーバのプロセスをどうこうするコードを読んだときなるほど!となって感動しました。

Unix v6のコード上でkill関数がプロセスにシグナルを送る程度の意味なのは結構面食らいましたが。。

calloutエントリで指された関数の実行タイミングと連結リスト

クロック割込みについての話題ではないのですが、クロック割込みハンドラで呼びだされるclock()関数にて処理されるcallo構造体の配列calloutの部分が面白かったです(p.163)。

calloutは連結リストの形になっているのですが、一つ一つのエントリが実行対象の関数へのポインタと次のエントリへのポインタ、前のエントリから何秒後((日本語の都合でこうなっているが実際の単位はティック)に実行されるかの情報を持っています。

そして、clock()関数のたびに最初のcalloutエントリだけ前のエントリから何秒後に実行してくれ、のカウンタがデクリメントされるわけです。

calloutに登録された一つ一つの関数の実行タイミング(「今から何秒後に実行してくれ」の秒数)は最初のエントリからひとつたどる度に「前のエントリから何秒後に実行してくれ」の秒数を合計していくことで求められます。なので、最初のひとつのエントリの「前のエントリから何秒後に実行してくれ」だけ変更してやれば、それ以降のエントリの関数の「今から何秒後に実行してくれ」の部分は全部変更されるわけですね。

あら便利。

だからなんだ!って話なんですが、いろんな言語でTimer系のクラスのAPIみるとき、これ思い出すと楽しそうだなとなったのでメモ。

実効ユーザID、実効グループIDと実ユーザID、実グループIDについて

アクセス権限はパーミッションと呼ばれる11ビットの制御情報を使ってファイルごとに管理され(p.260)、access()関数にてu_uid, u_gid(実効ユーザID/実効グループID)を元にファイルへアクセス可能かが判定されます(p.308)。

パーミッションの11のビット列のなかでSUIDビット、SGIDビットが立っていると、ファイル実行時にu_uid, u_gidが変更されます。その際、変更前の値が入っているのがu_ruid, u_rgid(実ユーザID/実グループID)のようです(p.261)。

ファイルのハードリンクについて

リンクによって、ファイル(inode+ストレージファイル)に対して複数の名前をつけることができます。

リンクと元のファイルの名前に主従はなく、inode構造体がいくつの名前を持っているかを管理しており、このカウントが0にならない限りファイルは削除されません。

と聞いた時、ハードリンク少し怖いな、と思ったのでメモ。

その他細々

パイプが他でも使っているファイルアクセスの仕組みを利用した特殊なinodeエントリへのデータ読み書きらしいことを知って、標準入出力の言葉に対してなるほど感が増えました。

「Java並行処理プログラミング ―その「基盤」と「最新API」を究める」を読みました

去年の11月はブログを一切書けていなかったのですが、その間*1何をしていたかというと主に下記の本を読んでいました。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

読むのにかけた時間としては、だいたい11月の余暇をまるまる使って1回読んで、正月休みに缶詰になってもう一回読み進めた感じです。

この本の1章で触れられているのですが、この本が書かれた2006年ごろは「一昔前は並行処理が"前衛的な"話題だったけれど、普通のプログラマが(並行処理における)スレッドの安全性を意識しなければいけない」だったそうです。

不勉強なのであれですが個人的な肌感としては、クライアントサイドでは並行処理をそれとして意識しなくていいようなReactiveの考え方や、MVP, MVVMといったGUIシステムなど特定の条件下の作法に注目が集まっています。一方Railsなどのサーバサイドにおいては並行処理といわずにバッチの処理などもっと具体的な問題にフォーカスして対応していたり。。

そういう状況なので、一周回った2018年現在も自分が書いているコードを並行処理として考えることは、普通のプログラマにとって "前衛的"、あるいは"オタク的"なような気配があります。このため、1章のこの辺の話は分量として3行程度で短いのですが時代が変わった感じがあって興味深かったです*2

この本全体の内容としては、そういう特殊な話題となったかもしれない並行処理に対して普通のデベロッパが取り組むときに生じる、なんとなくロックを獲得したりしているけれど、これが正しいかどうやってチェックすればよいのだろう、といった疑問にコードレベルで答えるものになっています。一つ一つのトピックによくないコード、あまり良くないコード、良いコードの事例が並べられ、それら一つ一つに解説が加えてあるため、あまり詳しくない人でも丁寧に読んでけばそれ相応につかめていく感触があってよかったなと思いました。

基本的な部分は変わらないような気がしているものの、具体的なライブラリクラスの詳細やイディオム、パフォーマンスの兼ね合いなどはJava6.0くらいまでの話となっているので、せめてJava7, Java8くらいまでは適宜Javadocなどで確認した方がよいです。

それなりに胃もたれ感はありましたが、新しいことに少し手がとどきそうな感覚はワクワクですね。結城先生の下記の本を手にとって面白いな、と思った方は本書も楽しいと思います。

以下、簡単なメモを残して締めます。現場からは以上です。

  • 競り合い状態と複合アクション
    • 複数のスレッドから都合が悪いようなタイミングでアクセスされる可能性があり、悪い方の順序でのアクセスの結果、特定のステート変数の値がおかしくなるような状態を 競り合い状態 という
    • 競り合い状態が起きるような典型的な実装事例として、 check-then-act のような確認して、操作する、というような複数の操作がスレッドから見てアトミックにアクセスされる単位になっていないというのがある
      • 複合アクションには put-if-absent read-modify-write などがある。全部 check-then-act でいいのでは、といえなくもないですが、自分で一回書いてみると put-if-absent 見逃したりする...[感想]
    • 複合アクション全体をロックを使って一度に一つのスレッドしかアクセスできないようにする
  • ロックの粒度とデッドロックの話
    • 「このステート変数は~~でなければならない」といった制約を不変項といい、1つの不変項に関わるステート変数への操作を同一のロックでガードする
      • 別の不変項で制限されているステート変数(不変項Aでのみ制限されるステート変数aと不変項Bでのみ制限されるステート変数b)は別のオブジェクトでロックしてよい
    • 動的なロック順デッドロック
      • 同じクラスの2つのオブジェクトを1度に操作する場合などにやりがち。グローバルに固定順序でロックをとるように工夫する
        • ロックのネストに着目すると見つけやすい
        • 固定順序に関する工夫はメソッド内に押し込めて外部から隠してしまうとよい
      • メソッドをまたいだロックは発見しにくいが、すでにメソッド内でロックを取っている時、よそ者のメソッドを呼び出さないようにするとまだ発見しやすい
      • ロックを取らないで外部のメソッドを呼ぶことを オープンコール と呼ぶ
    • Lockインタフェースの tryLock を使って時間が切れたらロックを外すようにして確率的にロック解除という話も
  • Executorフレームワークの使い方
    • Producer-Consumer型の実装をやりたい時はExecutorFramework使うと便利
      • ExecutorFrameworkを使うと、タスクをどういう風にスレッド間へ分配するか、タスク自体がどうであるか、といったのを分離して考えやすい
    • タイムアウトしたFutureは、そのままだと元のスレッドへreturnするだけでFuture動いてるスレッドは止まっていないため、cancelしよう
    • ThreadPoolの用途など(第8章)
  • オブジェクトの可視性とメモリモデル
    • ロックをしないであるスレッドで処理していた内容が他のスレッドに見えないことがあるが、それはなぜか
      • たとえばスレッドAから int a = 0; int b = 0; int a = 2; b = 3; を実行した時、スレッドBで System.out.println("a = " + a + " b = " + b); したとき a = 0 b = 3 と出力されることがある理由
    • JVMの最適化のルールは、ロックが取られていない場合は、一つのスレッドの中でトータルの実行結果が意味的に変わらないのであれば実行順を入れ替えて良いようになっている
    • 変数を保存している領域は、スレッドごとがローカルにアクセスする領域と、他のスレッドと共有している領域がある
      • あるスレッドで宣言&初期化された変数が他のスレッドにもアクセスできるなる手順は スレッドごとのローカル領域に書き込み, 共有メモリに書き込み の順になる
      • 共有メモリに書き込み まで行われないと他のスレッドからは古い値が見える
    • ロックをすると、この実行順の最適化を制限することができ、ロックを後に獲得した他のスレッドから前のスレッドがしていた操作の内容が見えることが保証される
  • スレッドセーフなクラス設計と委譲
    • 1つの不変項にたずさわるステート変数がたくさんある場合はそれらを別のデータクラスとしてくくり出して管理すると扱いやすくなる
    • 並行性を求められるような部分は既存のJavaのライブラリクラスが使えないか検討する
      • 機能を拡張したい場合は継承より委譲を使うと良い
      • なぜなら、ステート変数にアクセスするためのロックオブジェクトがサブクラスに可視だとわからないから
      • (Javaのライブラリクラスはだいたいselfでロックしているのでクライアントサイドロックが可能だが、それは不安定な実装となる)
      • 大きな万能ライブラリやExecutorなどとまでいかずともLatch, CyclicBarrier, Semaphoreなど基本的なものを知っておくと特にテスト用モックさっくり書きたい時手軽(多分動作がわからないRxJavaなどのライブラリを調べる時にも)[感想]
  • テストについて
    • まずはシングルスレッドと同じ内容のテストをまずやる
      • 原因が並行性にないことをはっきりさせるため
    • ブロックする操作の試験
      • 逐次処理のテストにおける例外の試験に似たような立ち位置
      • 一定時間後にinterrupt()メソッドよんで、スレッドが終了するのをjoin()で待って、スレッドのステータス確認、みたいな構成 p.282
    • 資源管理のテストでは、不具合を誘発するためにスレッドの重なりを出すため、CyclicBarrierなどを使ってドライバに一工夫するとよい
    • コールバックなどテストに使えるAPIの利用も検討
      • RxJavaのテスト用クラスの利用などは多分この立ち位置からのアプローチでは[感想]
    • テスト自体が並行性を壊してしまう可能性がある、実行性能についての試験は静的/動的コンパイラの最適化などの要素もあるのでなるべくアプリケーションに近い環境で行う、など

*1:余暇で

*2:大規模分散データストアを扱っているような方にとっては本書の内容はおそらくホットであり、自分が本書を知ったのもデータマイグレーション三銃士という大規模データストアの移行に関する勉強会の関連資料を漁っている時

RackがよくわからなかったのでRackアプリケーションをUnicornで動かしてnginxからリクエストを転送してみた

この記事はRuby Advent Calendar 2017の23日目の記事です。

はじめに

Webアプリケーションフレームワーク(WAF)といえば、

  • 薄いアプリケーションをサクッと書くのに適しているsinatra
  • なんでもあり気味なRuby on Rails

など、rubyで有名なものだけでもぱっと複数名前が上がりますね。

これらのWAFはApacheやnginxと連携させて動作させますが、これらのWebサーバはそのままではWAFと連携して動きません。なにかしら間にコードを書く必要があるのですが、このコードのインタフェースがWAFによって違ったらWAFを選んだら連携させるサーバも固定されてしまいますね。

そこで、WAFとWebサーバの間にこれらが協調動作するためのインタフェースを設定しましょう、それがrack...

とまで書いていてやっぱり意味がよくわかっていないので、この記事ではnginxとRackアプリケーションをつないで動かしてみようと思います。

実行環境について

この記事で利用しているバージョンは

  • ruby 2.4.3
  • rack 2.0.3
  • nginx 1.13.7
  • unicorn 5.3.1 (gemのバージョン)

nginxをインストールする

http://nginx.org/en/download.html よりnginx 1.13.7をダウンロードします。

$ cd /path/to/nginx-1.13.7

$ ./configure 

...
中略
...

./configure: error: the HTTP rewrite module requires the PCRE library.
You can either disable the module by using --without-http_rewrite_module
option, or install the PCRE library into the system, or build the PCRE library
statically from the source with nginx by using --with-pcre=<path> option.

と出たので、こちらの記事を参考にpcreを入れます。

$ cd /path/to/pcre
$ curl --remote-name ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.41.tar.gz
$ tar -xzvf pcre-8.41.tar.gz

pcreはURLのrewirteやLocationの指定で正規表現によるマッチングが行えるようにしてくれるそうです。 つまり、pcreがあれば、ディレクトリにファイルをおいた場合、そのパスにアクセスした際にファイルを返してくれるのかなと一瞬思ったので、pcreをはずしてnginxをビルドしてみました。

$ cd /path/to/nginx-1.13.7

# 試しに --without-http_rewrite_module オプションでやってみる
$ ./configure --without-http_rewrite_module
$ make
$ sudo make install

$ ls /usr/local/nginx/sbin
nginx
$ ls /usr/local/nginx
conf    html    logs    sbin

$ /usr/local/nginx/sbin/nginx
nginx: [alert] could not open error log file: open() "/usr/local/nginx/logs/error.log" failed (13: Permission denied)
2017/12/23 01:29:52 [emerg] 7844#0: mkdir() "/usr/local/nginx/client_body_temp" failed (13: Permission denied)
$ sudo /usr/local/nginx/sbin/nginx
$ ps aux | grep nginx
woshidan          7900   0.0  0.0  2443044    828 s003  S+    1:31AM   0:00.00 grep nginx
nobody            7892   0.0  0.0  2456480   1040   ??  S     1:31AM   0:00.00 nginx: worker process
root              7891   0.0  0.0  2455228    484   ??  Ss    1:31AM   0:00.00 nginx: master process /usr/local/nginx/sbin/nginx
$ sudo /usr/local/nginx/sbin/nginx -s stop
$ ps aux | grep nginx
woshidan          7917   0.0  0.0  2443044    824 s003  S+    1:31AM   0:00.00 grep nginx

sudo /usr/local/nginx/sbin/nginxhttp://localhost にアクセスすると index.html が見れます。 そして、 http://localhost/50x.html にアクセスするとちゃんとエラーページが表示されます。違ったみたいですね。

この記事のごくせまい範囲では問題ありませんでしたが多分正しくないと思います。ただ、今回はnginxの話がメインでないので、pcreの話はまた改めてじっくり調べます。。

最初のRackアプリケーションを書く

rackのインタフェースでWAFとWebサーバを協調させるとき、WAFの方は Rackアプリケーション と呼ばれるのですが、Rackアプリケーションとは何かというと

  • callというメソッドを持つ
  • callメソッドの引数としてWebサーバからのリクエストに当たるオブジェクトを一個受け取る
  • callメソッドは、次の要素を含むレスポンスにあたる配列を返す

というRubyプログラムのことみたいです。

http://gihyo.jp/dev/serial/01/ruby/0023 の記事を参考に単純なラックアプリケーションを書いてみます。

GETリクエストにのみ反応し、URLパラメータを含むJSONを返してくれるアプリケーションです。

# test_app.rb
# config: utf-8
class TestApp
  def call(env)
    if env['REQUEST_METHOD'] == 'GET'
      [
        200,
        {'Content-Type' => 'application/json'},
        ["{ \"your_input\": \#{env['QUERY_STRING']}"\" }"]
      ]
    end
  end
end

test_app.rb と同じディレクトリに以下のように config.ru というファイルも用意して

# config.ru
# coding: utf-8

require './test_app.rb'
run TestApp.new
$ ls
... config.ru test_app.rb
$ ./rackup

とすると

$ rackup
[2017-12-23 01:48:38] INFO  WEBrick 1.3.1
[2017-12-23 01:48:38] INFO  ruby 2.4.3 (2017-12-14) [x86_64-darwin16]
[2017-12-23 01:48:38] INFO  WEBrick::HTTPServer#start: pid=8095 port=9292

のようにどこかでよくみたWEBrickのログが現れます。

http://localhost:9292 にアクセスすると { "your_input": "" } というJSONが表示され、 http://localhost:9292?test=parameter のようにクエリパラメータをつけると { "your_input": "parameter=test" } というJSONに変化します。

雑にもほどがありますが、一応動いてそうですね。

nginxとRackアプリケーションはこのままでは一緒に動作しませんが、これらをつなぐものとして Rackアプリケーション用サーバ があり、Rackアプリケーション用サーバ には Unicorn などがあるらしいです。今回は Unicorn をさわってみます。

UnicornとRackアプリケーション

http://w.koshigoe.jp/study/?ruby-unicorn-intro より引用すると

Unicornは、Unix系システムで動作するRackアプリケーション用サーバ。接続時間が短いことを前提とした設計となっている。

だそうです。書いてあったことで今回の作業に必要かもしれなさそうなことをざっくりまとめます。

  • マスタプロセスが規定の数のワーカープロセスが稼働しているのを維持する
    • クライアントからのリクエストはワーカープロセスが処理する
  • Rackアプリケーションをロードする((Rackアプリケーションは少なくとも Rackアプリケーション用サーバ とは別のプロセスで動いてないんですね))
  • 設定ファイルをRuby DSLで書く
    • フックは3種類あって
      • before_exec ... reexecによって新しいマスタプロセスを作る際のexec()実行の直前
      • before_fork ... ワーカープロセスをfork()する直前。
      • after_fork ... ワーカープロセスをfork()した後、いくつかの内部処理を終えた後。
  • Unicornのプロセスを止めたり増やしたりするのはマスタープロセスにシグナルを送ることによって行う
    • 一部の分かりやすいシグナルだけ抜粋してメモ
      • QUIT ... 処理中のワーカープロセスの処理の完了を待ってから終了させる。
      • TTIN ... ワーカープロセスを一つ増やす。
      • TTOU ... ワーカープロセスを一つ減らす。

アプリケーションのpreloadもできるそうですが、一旦先に進みます。ひとまず unicorn のgemを入れます。

$ sudo gem install unicorn

unicornを動作させる前にいくつか必要なディレクトリを用意します。

mkdir tmp
mkdir tmp/sockets
mkdir tmp/pids
mkdir log

こちらの記事こちらの記事を元に、可能な限り少なく設定ファイル unicorn.rb を書いてみます。

worker_processes 2 # 子プロセスいくつ立ち上げるか
timeout 15

# Unicornのソケットが開くパスを指定
# この設定をnginx.confでも使います
listen "#{@dir}tmp/sockets/unicorn.sock", :backlog => 64

# ログのパスを指定
stderr_path "#{@dir}log/unicorn.stderr.log"
stdout_path "#{@dir}log/unicorn.stdout.log"

最低限の設定ファイルはこれだけで、

# ファイルの配置を確認
$ tree .
.
├── config.ru # Rackアプリケーションの起動設定ファイル
├── log
├── test_app.rb # Rackアプリケーションの実装
├── tmp
│   ├── pids
│   └── sockets
└── unicorn.rb # Unicornの設定ファイル

上記を用意した上で unicorn と入力すると、

$ unicorn 
I, [2017-12-23T16:09:57.684313 #11150]  INFO -- : listening on addr=0.0.0.0:8080 fd=9
I, [2017-12-23T16:09:57.684439 #11150]  INFO -- : worker=0 spawning...
I, [2017-12-23T16:09:57.685355 #11150]  INFO -- : master process ready
I, [2017-12-23T16:09:57.686091 #11163]  INFO -- : worker=0 spawned pid=11163
I, [2017-12-23T16:09:57.686483 #11163]  INFO -- : Refreshing Gem list
I, [2017-12-23T16:09:57.706864 #11163]  INFO -- : worker=0 ready

見覚えのあるログを吐くプロセスが立ち上がりました。ブラウザで http://localhost:8080/?parameter=test にアクセスすると先ほどのRackアプリケーションと同じJSONを返してもらえます。

実際動かすまでは、UnicornのプロセスからRackアプリケーションのプロセスにリクエストを接続するのかなーと思っていたのですが、Rackアプリケーションを unicorn コマンドで起動した場合と rackup コマンドで起動した場合とのプロセスを確認してみたところ、確かに Unicorn のプロセスしか動いてなくて Unicorn のプロセスでRackアプリケーションが動いている感じがしてきました。

# unicornから動かした場合
$ ps aux | grep unicorn
woshidan         11267   0.0  0.0  2442020    816 s002  S+    4:14PM   0:00.00 grep unicorn
woshidan         11163   0.0  0.1  2467944   8392 s003  S+    4:09PM   0:00.02 unicorn worker[0] -l0.0.0.0:8080 
woshidan         11150   0.0  0.1  2467900  10724 s003  S+    4:09PM   0:00.11 unicorn master -l0.0.0.0:8080 
$ ps aux | grep rack
# 何も出ない

# rackupで動かした場合
$ ps aux | grep rack
woshidan         11335   0.0  0.0  2442020    812 s000  S+    4:14PM   0:00.00 grep rack
woshidan         11297   0.0  0.1  2480696  11732 s003  S+    4:14PM   0:00.13 /Users/woshidan/.rbenv/versions/2.4.3/bin/ruby /Users/woshidan/.rbenv/versions/2.4.3/bin/racku

nginxとUnicorn

さて、今回のテーマは「nginxとRackアプリケーションをつないでみる」だったので、Rackアプリケーション を動かしている Unicornnginx を連携させるための設定もしてみます。

nginx.confの書き方については

  • http httpモジュールの設定
    • listenするポートやどのIPアドレスの設定
    • 外部からアクセスするときのドメイン名は server_name?
    • root ドキュメントルート((本当に外部にrootディレクトリを公開すると危ないので http://ドメイン/ でアクセスされた時に公開するディレクトリを指定する。ドキュメントルートにindex.htmlを用意しておくとWebサーバはトップページとして表示しようとする 参考: ドキュメントルート))の設定
  • server 仮想サーバごとの設定を書きます*1
    • location PATH ... URIごとに設定が変わる場合はそれを記述します
      • try_files $uri @app ... $uri = URLのパスにファイルがあるか、なければ @app のlocationで指定した設定が適用される
  • upstream server_name
    • server ディレクティブの中で proxy_pass server_name と書かれていた場合、 upstream ディレクティブの中のサーバに転送する
    • ロードバランサとしての負荷分散みたいな設定をするところっぽい

上記の内容だけ確認して書いた最小限の設定ファイルが以下となっていて

// nginx.conf
#user  nobody;
worker_processes  1;

#pid        logs/nginx.pid;

events {
    worker_connections  4;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  15;

    upstream test-app {
        server 127.0.0.1:8080; # ローカルでunicornが動いてるIPアドレス & ポートへ転送
    }

    server {
        listen       80;
        server_name  localhost;

        location / {
            root html; # /usr/local/nginx からのパスです
            try_files $uri @app;
        }

        location @app {
            proxy_pass http://test-app;
        }
    }
}

設定内容をざっくりまとめると

  • localhost(ポート番号80番) でHTTPリクエストを受け付けるHTTPサーバ
  • / のパスへアクセスされた時はまず /usr/local/nginx/html (ドキュメントルート)以下の対応するパスにファイルがあるか確認する
    • ファイルがあればファイルの内容を返す
    • ファイルがなければRackアプリケーションへ転送する
  • Rackアプリケーションは同じローカルマシンで動いていてポート番号 8080番にリクエストを投げれば良い

となります。

この内容を、元ファイルのバックアップを取った上で /usr/local/nginx/conf/nginx.conf に上書きしてから nginx を起動します。

$ sudo /usr/local/nginx/sbin/nginx

すると、ドキュメントルートの usr/local/nginx/html から辿って静的ファイルのあるパスの /index.html/50x.html に対してはhtmlをそれ以外の場合は先ほどのRackアプリケーションが処理したJSONを返してくれます。

nginx がパスを見て静的ファイルを返すかアプリケーションへリクエストを転送するかといった判断をしてくれているようで、それっぽくていいですね。

まとめ

ここまで動かしてみてRackでWebサーバとWAFを連携させるとかいうのは多分この辺りのことを言ってるんだろうな、と思ったことをメモします。

  • Rackアプリケーションとは何かしらの引数を一つ取る call メソッドを持ったRubyのプログラムのことでRailsSinatraもRackアプリケーションである
  • rackインタフェースないしRack protocolはその call メソッドで受け取るオブジェクトや call メソッドの戻り値などを規定している
  • HTTPリクエストを受け取ってrackインタフェースにそったオブジェクトを Rack アプリケーションに渡してくれるWebサーバを Rackアプリケーション用サーバという
    • Phusion PassengerのnginxやApacheに対して拡張している部分やUnicornなどがここに該当する
  • 一般的にはリクエストを受け取ったらすべてそのままWebアプリケーションに流したいわけではなく静的ページに流したりロードバランサ挟んだりしたいので通常のWebサーバをRackアプリケーション用サーバ の前段に置く
    • nginxやApacheあたりのこと
  • nginxの設定で、特定のパスへのリクエストを Rackアプリケーション用サーバ に転送する
    • 転送の際にロードバランサの設定を挟んだり、特定のページへは静的ファイルを返すようにしたりする

まだまだ気になることは残っているのですが、十分長くなったのでおかわり案件にして現場からは以上です。

参考

*1:仮想サーバについては http://www.sakc.jp/blog/archives/41325

Cocos2d-xの勉強会のスライドを一通り辿ってみたときの話

この記事は〇〇勉強してみた Advent Calendar 2017の19日目の記事です。

はじめに

現在お仕事で開発しているライブラリはiOS/AndroidだけでなくCocos2d-xやUnityなどのプラットフォームに対し、クロスプラットフォーム展開をしており、その一環としてCocos2d-x用SDKのメンテナンスを担当したことがありました*1

それがきっかけで上長から「(Cocos2d-xの)勉強会とかもうちょっと行ってみたら? まとまった時間は取れるかどうかわからないけど」という話になり、日々30分強をもぎとり*2、過去の勉強会のスライドを30個眺めてみたときの話をします。

なぜ過去の勉強会のスライドを眺めていったかというと、インターネット上に東京から出かけられる範囲で開催予定の勉強会が見つからなかったからです*3

やってみた感想として

自分には普段からあまり馴染みのない分野について調べる場合、説明の文章を読むだけで頭がヒートアップしてなかなか進まないことが非常に多いです。そのため、慣れない分野に取り組むとき、普段は2時間くらいを単位として時間を確保して取り組むことが基本だったのですが、今回はお仕事の都合で30分強しか枠が取れないので果たしてどうなるかなという感じでした。

結論として、自信を持ってコードを書いていける強度の知識はあまり身についておらず、実装関連も細切れの時間だとなかなか取り組むことができずで中途半端な感じでした。

その代わりに馴染みのような感覚ができました。以前はCocos2d-xの関連issueを読むとき、かなり多くの文章が初めて読む文章だったのですが、これだけ読んでみると、どれもなくなんとなく知ってるものを詳しく調べるの枠に入ってきます。

後半にあたった資料は前半にあたった資料より時間あたりに(がっつり学習したほどではないにせよ)得られるものが多かったです。

なんとなく知ってるというのはそのまま実装に使えるわけではありませんが、調査とか調査とか調査とかばっかりしていると、なんとなく知ってるものは調べるに当たってゆるやかでも時間の計算ができるので非常にありがたいです。精神に効きます。

また、これまで学習・即・仕事、みたいな一直線でかつかつの学習計画をとることが多かったため、日頃からなんとなくアンテナを張っておけ、という話がどの程度有用なのかよくわかりませんでしたが、なるほどこういう感覚を作るためのものなのだなぁ、と体感できて面白かったです。

最近はリモートワークで通勤なしとはいえ全部の分野に毎日2時間かけるわけにはいかないため、今回得た感覚を生かして、トレンドを把握したい分野、直近で専門的に取り組みたい分野、などを分けてQiitaやTwitterをもっと利用していきたいなと思いました。

言われなくてもわかるやろという話ですが、めっぽう体感派なのですみません...。

得たゆるふわ知識

最後にゆるふわなのであんまり参考にならないのですが、自分が特に印象に残った範囲をつらつら箇条書きにして締めたいと思います。

現場からは以上です。

  • アプリ界隈と用語が異なる
    • サーバ連携 = 通信 / オブジェクト = UI Componentなど。発表者の周りの用語かもしれませんが意外だった。検索するときに戸惑いそう...
  • 物理エンジンの作り方で
    • Chipmunk シェイプを作成は設定項目が多いので専用エディタがある、物理構造の一部を固定 => joint/衝突判定用のリスナ b2ContactListener
    • Box2D AngryBirdとかで使われた.
  • 画像を読み込むときは
    • I/Oを減らすためにTexture Atlas(キャラチップやマップチップのような画像)を利用
    • CCSpriteBatchNodeはアセットの用意が複雑だが圧倒的に速い
    • ラベルの表示は文字をそのままCCLabelTTFで実装するとフォントサイズをOS/機種ごとに変更する必要があってつらいので、CCLabelBMFontクラスで画像化するとよい
  • Cocos2d-x 3.0系以降ではC++11を想定して良いこと
  • リソースなどの調整をするたびにエディタを触って組み込んでビルドするのは大変なのでサーバーに画像などを置きアプリのリソース確認の際にはサーバからリソースを取得して確認を行うという話があったこと
    • それがCocos2d-xのIDEにも組み込まれていたこと(Cocos Code IDEとHot Updating)
  • UIの部品は基本的なボタンが含まれるCCMenuItemの仲間とその他に別れ、後者の方が取得できるイベントが多いが前者の方がイベントリスナなどの設定がまとめてできること
  • アニメーションは、キーフレームに合わせて効果音の設定をし、キーフレームを組み合わせてタイムラインを作って書き出すこと
  • アニメーションなどをGUIで生成するツールは現在開発が活発ではなく不安が残ること
  • 普通のゲームでは毎フレーム呼ばれるupdate()関数で諸々のグラフィックの更新を少しずつするらしいこと
    • cocos2d-xではrunActionを使って書いていくのがCocos流

参考

得たゆるふわ知識、の節で書いた範囲のものについて

*1:そのときはAndroidの方の知識で頑張りました

*2:進捗は一応出してたからいいですよね!

*3:2017年11月当時、これはこれで面白いなと思いました。

adbのCLIでAndroidのエミュレータを起動したりアプリの起動、テストの実行をしたりする

この記事はAndroid Advent Calender その2の15日目の記事です。

今回は

https://developer.android.com/studio/command-line/adb.html https://developer.android.com/studio/run/emulator-commandline.html https://developer.android.com/studio/test/command-line.html

を見て色々素振りしてみようと思います。

シミュレータの起動

/Users/woshidan/Library/Android/sdk/tools/emulator -avd Nexus_5X_API_22

で起動することが可能です。テストを実行させたり、アプリをインストールして起動したりしてみましょう。

アプリのインストール

$ adb install app/build/outputs/apk/app-debug.apk 
app/build/outputs/apk/app-debug.apk: 1 file pushed. 5.8 MB/s (2910562 bytes in 0.477s)
    pkg: /data/local/tmp/app-debug.apk
Success

# 二つ以上シミュレータを起動 or 実機を接続している場合
$ adb devices
List of devices attached
162EJP011A71181614  device
emulator-5554  device

$ adb -s emulator-5554 install app/build/outputs/apk/app-debug.apk 
app/build/outputs/apk/app-debug.apk: 1 file pushed. 142.6 MB/s (2910562 bytes in 0.019s)
    pkg: /data/local/tmp/app-debug.apk
Success

apkはプロジェクトのbuildディレクトリ以下を探せばあると思います。

アプリのアンインストール

$ adb uninstall package
Success

アプリの起動

# アクションがandroid.intent.action.VIEWのIntentに反応するActivityへ暗黙的Intentを送る
$ adb shell am start -a android.intent.action.VIEW
# 特定のActivityへ明示的Intentを送る
$ adb shell am start -n io.test.woshidan/io.test.woshidan.MainActivity

Logcatのログを標準出力に出す

$ adb logcat

操作の様子を録画する

# API19以上の実機かAPI24以上のシミュレータ
$ screenrecord /sdcard/Movies/demo.mp4 

# adb pull で当該ファイルをPCなどに持ってこれる

テストを実行する

ADBを使う

# テストで実行可能なinstrumantationの一覧
$ adb shell pm list instrumentation
instrumentation:com.android.emulator.smoketests/android.support.test.runner.AndroidJUnitRunner (target=com.android.emulator.smoketests)
instrumentation:com.android.smoketest.tests/com.android.smoketest.SmokeTestRunner (target=com.android.smoketest)
instrumentation:com.example.android.apis/.app.LocalSampleInstrumentation (target=com.example.android.apis)
instrumentation:com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner (target=com.example.woshidan.myapplication)
instrumentation:com.example.woshidan.newtestapplication.test/android.support.test.runner.AndroidJUnitRunner (target=com.example.woshidan.newtestapplication)

# プロジェクト全体のAndroidTest
$ adb shell am instrument -w com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner

# AndroidTestのうち特定のクラスのテストを行う
$ adb shell am instrument -w -e class com.example.woshidan.myapplication.test.ExampleTest com.example.woshidan.myapplication.test/android.support.test.runner.AndroidJUnitRunner

gradleを使う

// プロジェクト用のbuild.gradle
buildscript {
    repositories {
        google() // gradlewでgoogle()リポジトリに探しにいくためにはここに追加する必要あり
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
    }
}
// アプリ用のbuild.gradle
android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.woshidan.newtestapplication"
        minSdkVersion 15
        targetSdkVersion 26
        buildToolsVersion '26.0.1'
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary= true
    }
...
# プロジェクトのAndroidTest全体を行う(JUnitを使うUnitテストは connectedAndroid を取る)
$ ./gradlew app:connectedAndroidTest

# プロジェクトのAndroidTestのうち特定のメソッドのものを行う
$ ./gradlew app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.woshidan.myapplication.MainActivityTest#mainActivityTest
// 出力される実行結果
// build/generated/outputs/connected/以下にある
// build/generated/reports/以下にはHTML形式のレポートも
<?xml version='1.0' encoding='UTF-8' ?>
<testsuite name="com.example.woshidan.myapplication.MainActivityTest" tests="1" failures="0" errors="0" skipped="0" time="0.665" timestamp="2017-12-20T17:11:10" hostname="localhost">
  <properties>
    <property name="device" value="Nexus_5X_API_22(AVD) - 5.1.1" />
    <property name="flavor" value="" />
    <property name="project" value="app" />
  </properties>
  <testcase name="mainActivityTest" classname="com.example.woshidan.myapplication.MainActivityTest" time="0.563" />
</testsuite>

まとまってませんが色々試して満足したので現場からは以上です。

ライブラリ開発屋がAthenaを利用してログの収集分析をやりやすくした話

この記事はServerless Advent Calender 2017の16日目の記事です。

ライブラリ開発屋として仕事でAthenaを使ってログの収集分析をやりやすくした話をします。

はじめに

普段は開発者としてiOS/Android両対応のモバイルアプリ向けのライブラリの開発やテストをしています。その業務の中でお問い合わせを受けた際、お客さんの状況を聞いてライブラリを修正したり使い方を提案したりして対応させていただくことがあります*1

その中で、なかなか言葉で状況の説明が難しい場合があり、そういうときは動作検証時のログをいただいて状況の確認をさせていただきます。しかし、いかんせんそういう状況は再現が難しかったりするもので動作検証のログがとれないか試しているうちに時間が経ってしまってもどかしいことが結構ありました。

そこで、なるべく早くお客様に解決方法の提案ができるように、それらしいログを検索して取得できたらいいな、セキュリティ的・S3の予算的にピンポイントで取得したいな、ということで今回Athenaを使ってやってみました。

Athenaについて簡単に

Athenaは標準的なSQLを利用してS3内のデータを分析できるサービスです。

実際中で動いているのは分散SQLクエリエンジンのPrestoで、データフォーマットにJSON形式を用いる場合は下記のような形式のJSONが入っているS3のバケットのパスが s3://athena-examples/users/logs だったとして

{
  "name": "太郎",
   "address": "日本のそのあたり",
   "comment": "JSONは実際は1行にminifyしておく必要があります"
}

以下のクエリを実行すればSQLを用いて s3://athena-examples/users/logs 以下のデータを検索できるようにしてくれます。

// 単純化のため少々それっぽくないログとなっています
CREATE EXTERNAL TABLE IF NOT EXISTS user_logs (
  name string, 
  address string, 
  comment string
 ) 
ROW FORMAT  serde 'org.apache.hive.hcatalog.data.JsonSerDe'
LOCATION 's3://athena-examples/users/logs/';

テーブル定義の際に指定した LOCATION のパス以下はテーブル定義の形式に沿って処理できるデータのみが入っている必要がありますが、上記のテーブル定義はAthenaが検索の際に用いるだけで、S3に入っているデータとは結びついていないので何度でも破棄したり作り直すことが可能です。

他に対応しているデータフォーマットなど、詳しい話についてはこちらをご覧ください。

実際にやってみて気を使ったこと

やってみたことを書くとログの形式をAthenaで検索しやすい形に変更してAthenaを使いましたで終わってしまうので、その際気を使ったことなどを書いていこうかと思います。

Athenaのコスト対策の話

Athenaの料金形態は検索を実行するときのデータスキャン量に対する従量課金で、 スキャンされたデータ 1 TB あたり 5 USD となっています*2

そのため、ログをgzipなどに圧縮するとスキャン対象の容量をかなり削減することができます。幸いログファイルには同じ文字列が多く含まれるためか、自分が行った事前調査では単純にgzipにしただけで圧縮前の容量の15~20%まで小さくすることができました。S3にアップロードするファイルの圧縮は比較的簡単に対策ができるためやったほうがいいでしょう。

Athenaのコスト対策はAthenaの料金の話だけでは終わりません。AthenaはS3にあるファイルを取得するためにS3を利用しており、Athenaの料金とは別途にS3の利用料金がかかります。このS3の利用料金が場合によってはAthenaのスキャン料金と同等以上になることがあり、

  • 同じクエリを何度も実行しないようにする
  • LIMIT句をつけると早めにスキャン & データ取得を打ち切るので必ずLIMIT句をつけるようにする
  • パーティションやテーブル定義に利用するロケーションのパスを工夫してスキャン範囲を狭くする

などの対応を取る必要があります。

パーティションやテーブル定義に利用するロケーションのパスを決めるにあたって

前節で パーティションやテーブル定義に利用するロケーションのパスを工夫してスキャン範囲を狭くする と書きましたが、バケットのパスやパーティション分割のために使える要素としては、モバイルライブラリの場合

  • 日付
  • 対応OS
  • ライブラリのバージョン
  • ユーザのサービスアカウントごとのid

などがあります。それぞれ、data=xxx/os=ios/version=nnn/user_app_id=xxxx のように素直にパーティションを掘ってもよいかもしれませんが、

  • ライブラリのバージョンは企業によってはなかなか更新の時間がとれないため、利用されているバージョンが10種類以上とばらついてしまう
  • 利用されているサービスアカウントはテスト用のものもあるので1日に数百以上ある

ということを踏まえると、あっという間にAthenaが制限しているパーティション上限数 20000に引っかかってしまうでしょう。

しかし、日頃の調査を振り返ると

  • Android用ライブラリの調査をするときにiOS用ライブラリの調査をすることはほとんどない
  • ライブラリのログは利用している開発者のアプリごとの利用者規模が6桁~導入時のお試しで1桁、2桁まで様々
    • ライブラリのバージョンで区切って検索しているつもりが一番利用者数の多いアプリのログしかスキャンしていないことがありうる
  • ログを調査するのはお問い合わせ起点、つまり、調査対象のサービスアカウントや利用バージョンの情報を持っていることが多い
  • 新バージョンの不具合などにもとづいてログを調べるとしたら、結局は最新の日付から探していくことが多いだろう

などの事実があり、これらを踏まえると、同時に範囲検索で使う要素は存外少なそうなことがわかります。

結局、大きなテーブルを一つ定義してその中でいくつかのパーティションを利用するのではなく、基本的にはテーブルのロケーションに深めのパスを設定し、そのパスごとにたくさんのテーブルを定義しては捨てる方針にしました。

ややかっこわるいのですが、放っておくとパーティションのキーにあたる値の組み合わせは基本的にどんどん増えていくため、ほとんど一つのクエリで同時に検索しない範囲はテーブルのロケーションの方に突っ込んでもよいと思います。

スクリプトでクエリを生成することにしてクエリ実行結果の再利用やLIMIT句の指定を徹底する

Athenaは従量課金性ですが、一度実行したクエリの結果を再利用することが可能です。クエリを実行した時のquery execution idをパラメータにしてGetQueryResultsのAPIにリクエストを送ることにより、データのスキャンを再度行わずに実行結果を再取得することができます*3

なので、一度実行したクエリとそれに対応するquery execution idを控えて、実行しようとしているクエリが直近で実行されたものなら以前の結果を再利用することで、コストを抑えることができます。

これも結構簡単なスクリプトで対応を行うことが可能です*4

表記揺れによってほぼ同じクエリが別のクエリとカウントされて似たようなクエリが何個も走るかもしれませんが、いっそのことクエリの生成もスクリプトで行うことにしました。そうすることで、

  • 表記揺れがないため、クエリとquery execution idの対応を管理するスクリプトが動きやすい
  • テーブルのロケーションを深めに掘ったり、セッションの属性と紐づけてログを検索・分析しやすくした結果、肥大化したテーブル定義を間違えない
  • LIMIT句の追加などクエリの中で外してはいけないルールが徹底される

ことになりました。--dry-run オプションでAthenaに投げる予定のクエリを吐き出すようにしていて、手動でクエリを書きたくなった場合の下書きとしても利用できるようにしたり、実行時のログを一部出力することで、目当てのログのパスを見つけやすくしてなるべくターミナル一つで完結するように工夫しています。最近のブームは小規模なスクリプトDIYです*5

今後

今回の対応で社内外からお問い合わせがきたらログを分析しやすくなったので、今後はお問い合わせが来る前になにか気づけたらいいなということで、S3とLamdbaを組み合わせて不具合が起きてそうなときのログの検知もやってみたいと考えています。

DIYの現場からは以上です。

*1:サポートの方とお客様にひたすら助けられるお仕事とも言います

*2:https://aws.amazon.com/jp/athena/pricing/

*3:http://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryResults.html

*4:こういう http://woshidan.hatenablog.com/entry/2017/10/04/055335

*5:自分以外みんな知ってそうな余談であれなのですが、検索結果は1行1レコードとなっています。一回のクエリで同じファイルが複数レコードとして引っかかることがあるため、類似の用途で使う場合はLIMIT句の数字を用途に応じて増減しましょう

「テストも開発もするモバイルエンジニアのためのXCUITest/Espressoのすすめ」という題でLTをしました

testautomationresearch.connpass.com

要件の細かいことを突いたりテストケースの設計が大好物だったため、そういうのを本職としているテストエンジニアはどういう人たちなのかを知りたかったので「システムテスト自動化カンファレンス2017-2」に参加してきました。

図々しく「テストも開発もするモバイルエンジニアのためのXCUITest/Espressoのすすめ」という題でLTをさせていただいたので、取りいそぎスライドとその補足についてだけでもメモします。

スライド

speakerdeck.com

スライド補足

Appiumはいろんな言語のドライバがあるので好きな言語で書けるという話について

Appiumはブログなどの件数が多いから盛り上がっているという話がありますが、各言語のドライバでQ&Aやブログ記事が割れており、また言語を特定して情報を調べないとある言語の質問について他の言語で返ってくることもあるので*1、そういう数値については3~5分の1に割り引いて考えた方が良いかと思います。

各種言語のドライバの更新状況も言語によって異なっており、たとえば

  • Rubyの最新更新は約3週間前
  • perlの最新更新はREADME.mdで1年前
  • phpの最新更新は5か月前

となっています。不具合があった時に自分でAppiumが利用している各種ツールの状況を調査するコストが取れなければ、更新頻度の少ない言語は避け、いまならCookpad, Mercariが対応すると決めているRubyのドライバを選んだ方がハマりが少ないのではないでしょうか。。

Appiumが安定していなくてつらい話

Appiumは、

  • Appiumクライアント(Appiumクライアントライブラリ=各種言語のdriver, テストスクリプト)
  • Appiumサーバ
  • uiautomator/espresso or instrument/xcuitest

といった構成になっていて*2、テストを動かすために利用しているツールが多いです。

そのせいか、気軽にバージョンアップするとどれかのツールが噛み合わなくて不安定になったり動作が非常に遅くなったりします。

Execution on iOS is extremely slow after upgrading to Appium 1.6.5 · Issue #8717 · appium/appium · GitHub

Appium 1.7.1 Automation is too slow · Issue #739 · facebook/WebDriverAgent · GitHub

前述のドライバの言語がたくさんあることと合わせて関連issueがあちこちに散っており、かつ、大量のログが貼り付けられていることも多く必要な条件が見分けにくいため、調査に時間がかかります、というか、ました。

もともとSelenium系のツールに詳しいチームで対応をとったり、安定するまで待つ*3決定をすれば問題ないかもです。

しかし、現状のEspresso/XCUITestを直接使えば、間にそれなりに大きいツールをはさまないため、その分セットアップや調査が楽でした。

上記の「手間がかかる」「楽でした」という言葉について、調べている最中も悩んでいてこの補足を書いている現在も感じることですが、自分が不勉強で手間を惜しむなんて発想をするから云々みたいなことがずっと頭の中でくるくるしています。

ですが、テストも含めてコードは必要でなくなったら捨てたいです。気軽に捨てられるためには学習コストやメンテナンスコストも含めた手間は小さいほうがよいのでは、ということも一緒にぐるぐるしています。

LTタイトルに「モバイルエンジニア」を含めていますが、自分がアプリ開発者としての環境に慣れ親しんでいるのでEspresso/XCUITestで作ったテストなら気軽に捨てられるなぁ、という感じです。

E2Eテストが全部Espresso/XCUITestでいいかという話

もともと開発者*4の自分としてはAppiumで自動テストが書きやすい範囲とEspresso/XCUITestで自動テストが書きやすい範囲が被っているように感じました。それな らば、自動化しやすい部分は動かしやすいEspresso/XCUITestを使い、サーバとの連携、プッシュ通知であったりapk/ipaの更新時の挙動など自動化しにくい部分は手動テストと組み合わせればよい、と考えていました。

とくに、専用のQAチームを置けるほど規模が大きくないチームでE2Eテストが必要な時は「手動 + Appium」より「手動 + Espresso/XCUITest」の方が楽では~くらいの勢いでした。しかし、会場に行ってみると自分が想像していた以上にリリース用apk/ipaを用いたテストを重視する雰囲気だったため、やや場違いとなりもうしわけなかったです。

ただ、それぞれのプラットフォームのツールに乗っかって開発と同じIDEでテストを書くことにすると、テスト時の動作を入ってる変数付きで再現することができます。これにより、テストが悪いか/コードが悪いかの切り分けがかなり容易になり、開発しながらテストを書くハードルもぐっと下がるのでそれだけはお伝えしたかった(誰に。

遅くなった理由

もうちょっとややこしいレイアウトやもうちょっと新しいバージョンでもう一回Espresso Test Recorderの動作確認しようかと思ったらGradle Projectのリフレッシュがなかなか終わらなくてテンパってました。はい。。

最後に運営の皆様ありがとうございました。

現場からは以上です。

*1:たとえば http://discuss.appium.io/t/how-to-close-application-between-tests/1168 . phpの質問についてRubyJavaのコードがかえってきている

*2:https://github.com/appium/appiumhttp://www.atmarkit.co.jp/ait/articles/1504/27/news025.html を元にしています

*3:このことを考えると、更新が活発な言語を選択した方が良いと思います

*4:というか今もテスト設計の方が割いてる時間が多いだけで社内の肩書きとして開発者