読者です 読者をやめる 読者になる 読者になる

mosa_siru’s blog

とあるエンジニアのブログです。ボンバーマンが好きです。

テストなんか書かなくて良い 僕の考えるサービス開発の肝

世の中は一周まわってエンジニアリングの手法に溢れている。 テストを書け、ドキュメントを書いて冗長化しろ、コミットはわかりやすく、コーディング規約が、安定性が─── でも、それって本質なんだろうか?

新規サービスを作る際に肝だと思っていることをまとめてみた。

おことわり

  • 以下は少人数で"普通"のアプリやWebサービスを自社で新規開発するときのことを想定しています。大人数で重厚なソシャゲを作るとか、ガチガチの金融系サービスを作るとか、コンシューマーゲーム開発とか、個人で好きなものを作るとか、受託とかは全く想定していません。
  • 基本的に一通り現場をこなした中級以上のエンジニア向けに書いています。
  • アンチテーゼとして、ややキツめに断定する箇所が多いです、こういう意見もあるんだな程度に受け止めてください。
  • 所属する団体の意見とかは一切関係ありません。

目次

ユーザーのことだけ考える

まず第一に、サービスの主役はユーザーであり、その他の話は大体些細だ。

事業計画がどうこう」「偉い人が」「新しい技術を試したい」「マネタイズが」「綺麗なコードを」その大体が些細だ。

ターゲットするユーザーは誰なのか、そのユーザーのどんな課題が解決できるのか。そのサービスを使うと何が嬉しいのか。なんでそのサービスを使うのか、他のサービスじゃダメなのか。いつ起動するのか、なんで起動するのか。どうして離脱するのか。

ユーザーとサービスのことだけ考えたら、後のことは大体ついてくる。

企画が最も大事

サービス開発におけるプレイヤーの役割を「企画」「デザイナー」「エンジニア」などとざっくり分けるなら、企画(要件決め)が最も大事だ。企画がクソなものをどんなに上手く作ってもクソだからだ。

だから「俺はエンジニアだから言われたものを作る」とかは態度としてありえない。(スペシャリストなら別だけど、スペシャリストほどなぜかこういう態度はとらないものだ。)

サービスの成功を考えるなら立場によらず最も肝である企画フェーズからどんどん意見すべきで、開発工数的にコスパが合わないと思ったらどんどん代替案を提示すべきだ。

(話は逸れるけれど、設計にはドメイン知識と未来予知のスキルが必要で、「ここはきっとこう要件が変わる可能性があるからこうしとくか」だとかの温度感が掴めないとロクなものができない)

スピード

開発スピードより優先するものはほとんどない。

開発スピードと"品質"は両立できる。さっさと作って、触ってみてイケてないところを直して、リリースして数字や反応をみて直して─── サービス開発はその繰り返しだ。

リリースに関しても、今の時代"普通の"新規サービス開発に1年かかるとしたら遅すぎる。作っている間に競合が出るか市場が変化しているし、万一3年とか言い出すと市場が別世界になっている。そもそも当たってるサービスを数えた方が早いのだから、何年かけて作ったところで当たる保証なんて全く無い。

さらに言えば数年越しの事業計画なんて偉い人向けの説明以上の何者でもない。

Appleの「ベスト新着App」とかもあるので最初はクソクオリティでも出すべきだとは言わない

開発スピードを妨げるものはとにかく排除すべきだ。

オナニーをやめよう

僕らが作っているのはサービスだ。どんなにコードが綺麗で、カバレッジが100%だろうと、使われないサービスだったらそれは残念ながら意味のないコードだ。

僕らがコミットするべきはサービスの"質"とユーザーへの価値であって、コードの綺麗さではない。全くない。 サービスはユーザーに価値を提供するものであって、エンジニアがオナニーする場所ではない。

オナニーチェックリスト

よく陥りがちなオナニーを項目化した。上から順にオナニー度が高い。

  • カバレッジ100%を求める
  • ドキュメントを完全に書こうとする
  • テストコードの綺麗さや品質にやけにこだわる
  • gitコミットの分割とコミットメッセージにこだわる
  • 「やってみたいから」で言語やフレームワークを選ぶ
  • サービスの安定性に過剰にこだわる
  • サービスのバグのなさに過剰にこだわる
  • コードの(見た目の)綺麗さにやけにこだわる
  • コードレビューフローが整備されすぎている

ここからはやっておいた方が良いかなと思うもの。もちろん程度による。

  • 命名にこだわる
  • 効率化のためにテストを書く

カバレッジ100%を求める

テストというのはサービスの質を継続的に提供するために書くものだ。金を生んでいるサービスでないと基本的にコスパが悪い。多くのユーザーに使われているサービスならさておき、新しいプロダクトにおいてテストを書くモチベーションはかなり低い。

僕らは1日で新機能のプロトを作って、良ければリリースダメなら廃棄、そういう時間軸で生きている。リリースしても数字がダメなら機能ごと削除するなんて日常茶飯事だ。

「理由があればテストを書かなくてもいい」ではない、「理由がなければテストを書いてはいけない」くらいだ。過度なテストは書く時間の無駄なだけでなく、リファクタや機能追加の邪魔になるからだ(いちいちメソッド名変更などに対応しなければならないため)

もちろん開発効率化のためや、複雑な部分の必要なテストは書いてもいい。大事なのはその見極めだ。

そしてUI周りやリコメンドロジックなど、テストを書いても本質的なところは担保できない(または難しい)ところは沢山ある。人手による確認の質と効率を舐めてはいけない。

ドキュメントを完全に書こうとする

そもそも使われるかわからない(3ヶ月後あるかわからない)機能のドキュメントを完璧に書く必要なんてあるんだろうか? 大人数で開発をしているならわかるが、新規プロダクト開発を大人数でやること自体が悪手だ。

システム体系の伝達は残念ながら一子相伝だ。口頭で伝えて、コードを読んで、実際に書いてみて血肉にしていくものだ。少人数で開発するなら、実は口頭やホワイトボードでの伝達が、同期的という欠点があっても一番効率的だったりする。

そして少人数開発においては、冗長化よりも本来の目的であるスピードを重視すべきだ。

僕らにはドキュメントを完璧に書く時間も、読む時間も、メンテする時間も全く無い。(そしてこのメンテというのが本当に厄介なのだ)

ただし、APIインターフェース/データベーススキーマ/認証フロー 等は最低限書いておくのを勧める。ここで言ってるのは、内部の処理フローを完全に書くとかそういうレベルの話だ。ドキュメントは100点を求めようとすると途端に運用上のコスパが悪くなる。最低限書くべきことを決めて、50点でもいいから運用可能なレベルで妥協すること。

テストコードの綺麗さや品質にやけにこだわる

「テストの独立性が」「振る舞いで書く」とか色々だ。やってもいいけど時間はかけないでくれ。

gitコミットの分割とコミットメッセージにこだわる

とにかくコスパが悪い。そんなに古く遡られることも、コミットメッセージがないとわからないことも稀。コード読んでわからないような話ならソースにコメントとか残しておけば良い。書いた人に聞けば事足りることも多いし。

「やってみたいから」で言語やフレームワークを選ぶ

気持ちはわかるが家でやろう。要件を満たせて、チームが最適に能力を発揮できる言語を選ぶべきだ。勿論採用戦略なら別だが。個人の好みで技術選択をしてはいけない。例えばサーバーサイドの言語選択なら、運用面だけ考えてもこの記事くらい考えることは多いのだ。

ユーザーは何の言語で動いているかなんて興味が無い。

サービスの安定性に過剰にこだわる

これはあまり無い話だとは思うんだけど、神経質にダウンタイムや応答性等にこだわってしまう状態。技術のある大きめの企業だとある話だと思う。

勿論サービスの安定性は死ぬほど大事だし、サーバーエンジニアの腕の見せどころではある。けれどユーザーにとってサービスが凄い安定していることと、それを犠牲にしてでも機能改善のスピードが速いこと。どっちが今のフェーズにとって大事かは考えたほうが良いと思う。スピードと安定性はトレードオフなんだから。

(余談だけど僕はソシャゲは不安定で侘び石がもらえる方が嬉しい)

初めからスケールを気にしすぎてリリースが遅くなるのも少し考えものだ。そもそも2016年現在でも、スケールできるサーバーをきちんと組むのはまだ難しい。それを時間をかけずにできるエンジニアがいるのは稀有なケースだ。極論を言えば、完璧にスケールできるシステムなんて最初から考えるだけ皮算用で、サービスがある程度大きくなってきたら良いエンジニアが採用できるので、そこからシステムをリプレイスしてしまえば良い話だったりもする。だから初めの段階で過剰にこだわるよりも、そこそこにしてさっさと価値を届けることの方が事業としては大事かもしれない。どうせ新規事業なんてほとんど当たらないんだから。

サービスのバグのなさに過剰にこだわる

これも前項と似ている。

ユーザーにとってバグがほとんどないことと、少しバギーだけど機能改善のスピードが速いこと。どっちが今のフェーズにとって大事か。スピードとバグのなさはトレードオフだ。

成長したサービスにおいてバグは命取りだ。それだけで一瞬で何千ものユーザーが離脱したり、数億円レベルの損失が出たりする。けれどひよっこサービスがバグをそんなに恐れるべきだろうか。

バグのなさを品質と呼ぶことも嫌いだ。サービスの"品質"はあくまでユーザーに届ける価値で決まる。

(余談だけど僕はソシャゲはバグがあって侘び石がもらえる方が嬉しい)

コードの(見た目の)綺麗さにやけにこだわる

ここまで読んだ方ならだいたい意図は伝わると思う。勿論綺麗に、一貫して書かれているに越したことはないけど、それに時間を使いすぎるのは違う。そのコードをまるまる破棄することなんて日常茶飯事だからだ。

コードレビューフローが整備されすぎている

これも大体わかると思うので省略。

おわりに

念のため補足するけれど、僕が言いたいのは「テストもドキュメントも全く書かない、焼け野原みたいな現場で戦おう」では無い。そうじゃなくて、「どんな手法もメリット・デメリットがあるんだから、現場のフェーズやプロダクトに合わせて捨てられるものは捨てて、自己満足もやめて、コトに向かおう」ということだ。

だいたいのケースにおいて、技術は目的じゃなくて手段だ。オーバーエンジニアリングをやめて、楽しくユーザーに向き合おう。プロダクト開発はとても面白いのだから。

lua-nginx-module を導入した話 (1) JSON-RPC 2.0 batch request編

nginxにluaを組み込む openresty/lua-nginx-module · GitHub というのが非常に便利で、単なるreverse proxyだったnginxに、あらゆる役割を持たせられるようになります。

nginxの設定を動的にしたり、nginxからDBやmecached, redisなどへのアクセスも可能になります。HTTP requestを発行することさえできます。

これにより、例えばnginxの設定をDB化したり(NginxとLuaを用いた動的なリバースプロキシでデプロイを 100 倍速くした)、ちょっとした認証機能を入れたり、アクセスをmemcachedやredisなどに記録して攻撃を動的に判定してブロックしたりとか、nginxのレイヤーで色んなことができるようになります。(バックエンドのアプリケーションでも勿論同様のことは可能ですが、前段でブロックできるのが嬉しいですね)

あまりにも色んなことができるので、極論をいえばluaだけでサービスの構築も可能で、バックエンドにアプリケーションを用意する必要さえなくなります。聞く話によると、性能・速度が求められるアドテク企業にてお金がないところだと、本当にnginx+luaだけでサービスを構築しているところもあるそうです。(やりすぎだとは思いますが…)

この記事では応用例の一つとして、JSON-RPC 2.0 batch requestというものをnginx+luaで実装・ライブラリ化した話をします。

目次

JSON-RPC

JSON-RPC はとってもシンプルなRPCプロトコルで、リクエストもレスポンスも全部指定のJSON形式でやろうぜ!というものです。多数の言語でサーバー・クライアント共に実装されフレームワーク化やライブラリ化されています。

JSON-RPCですが、個人的には外部に公開するAPIでもない限り、URI設計や冪等性とか色々気にしないといけないRestFulよりも扱いやすいなーと思っています。大抵のネイティブアプリとサーバーAPI間の通信などはこれで十分ではないでしょうか。

以下がサンプルとなります。

sample

request

{"jsonrpc": "2.0", "method": "subtract", "params": [5, 2], "id": 1}

response

{"jsonrpc": "2.0", "result": 3, "id": 1}

"subtract"という名の引き算するmethodに2つパラメーターを渡しています。とってもシンプルですね。 key "id" の意味は後述します。

request

{"jsonrpc": "2.0", "method": "getUser", "params": { "user_id": 1 }, "id": 1}

response

{"jsonrpc": "2.0", "result": {"name": "mosa", "email": "mosa@hogefuga.com"}, "id": 1}

ユーザー情報を取得するAPIの例です。

どちらも特定のmethodにパラメータつきで1つの単純なリクエストを送って、1つのレスポンスをもらっています。

(後述のbatch requestと区別して、これを便宜上「single request」と呼ぶことにします)

JSON-RPC 2.0 batch request

JSON-RPC 2.0 にはbatch requestという仕様があります。簡単にいうと「複数のリクエストを一気に送って、一気にレスポンスもらおうぜ」というものです。以下がサンプルです。

request

[
    {"jsonrpc": "2.0", "method": "subtract", "params": [5, 2], "id": 1},
    {"jsonrpc": "2.0", "method": "getUser", "params": { "user_id": 1 }, "id": 2}
]

response

[
   {"jsonrpc": "2.0", "result": 3, "id": 1},
   {"jsonrpc": "2.0", "result": {"name": "mosa", "email": "mosa@hogefuga.com"}, "id": 2}
]

JSON Arrayで送ってJSON Arrayで返る、とてもシンプルなプロトコルです。 ただし仕様上、送ったArrayの順番通りにレスポンスが並ぶことは保証されません。なので識別子として "id"のkeyをclientは見ないといけません。

batch requestのメリット

batch requestはとても便利です。 複数APIのレスポンスをもとにクライアントの画面が描画されることは良くあり、その際batch requestでまとめてしまうことでリクエスト数を減らすことができます。(まるでCSS spriteのようですね) また、クライアントでの「2つのAPIの結果を待たないと描画してはいけない」「どれかのAPIがこけた場合は全体もエラーにしないといけない」などの、非同期では面倒だったハンドリングもシンプルになるケースがあります。

batch request導入前は、リクエスト数を減らすために「1画面に必要な情報はだいたい1つのAPIで返す」ような設計にしていたこともありました。しかしAPIのインターフェースがクライアントのUI変更に引きずられて変更を余儀なくされたりと、設計的に非常に気持ち悪いことになっていました。batch request導入後は「クライアントでどう描画されるか」はあまり気にすることなく、リソース単位でAPIのインターフェースを決めることができるようになりました。

batch requestのデメリット

重い処理に引きずられてしまいます。つまり、処理に10秒かかるリクエストAと、1秒かかるリクエストBの2つをbatch requestにした場合、A, Bのレスポンスをもらうために最低10秒かかることになります。(※サーバーの実装次第では11秒です)

ですので、重いmethodを叩く場合は注意する必要があります。

batch requestのサーバー実装

これだけ便利なbatch requestをどう実装するのが良いでしょうか。 以下、JSON-RPC APIをサーバーに立て、nginxを通してHTTP通信で利用することを考えます。

実装1. フレームワーク側で行う

既存のJSON-RPC 2.0のサーバーフレームワーク実装をみると、対応していないものも多く、対応していても以下のような単純な実装が見られました。

f:id:mosa_siru:20151001223733p:plain

  1. リクエストで来たJSONをdecodeする
  2. Arrayではなかったら通常のリクエストとして処理する
  3. Arrayの場合(batch request)、for loop でそれぞれ順番に処理し、全て終わったらまとめてレスポンスを生成する

この場合、とてもシンプルではありますが、10個分のbatch requestを投げたら処理に10倍時間がかかることになります。あんまりイケてるとはいえません。並列にそれぞれのプロセス(スレッド)が処理してくれるのが理想です。 for loopではなく並列で処理すれば良いのですが、言語によっては複雑な実装になるでしょう。

実装2. proxy serverをたてる

f:id:mosa_siru:20151001223835p:plain

上記の1-3の処理をするだけのproxy コンポーネントを立てるのは、処理を並列化する方法の1つでしょう。 batch requestはまずproxyが処理することにし、proxyがJSON Arrayのそれぞれに対し、非同期で並列にバックエンドのJSON-RPCサーバーにリクエストを投げ、全て返ってきたら1つにまとめてレスポンスを生成する形です。 nodeなどで書けばおそらくシンプルな実装になるでしょう。

しかし管理コンポーネントが増えることになり、下手したら client => nginx1 => proxy => nginx2 => app の経路を辿ることになり、運用の複雑さは増すでしょう。異常が起きた場合、どの経路で死んだのか多数のログを漁ることになります。proxy serverの台数管理も考えないといけません。

(なぜ間にnginx2が必要かは、複雑ですが以下の理由によります。まず、single requestの場合はproxyを挟まない client => nginx2 => app の経路にするとします。このときbatch requestで proxy => app の経路にしてしまうと、appへのreverse proxy設定をnginx2とproxyで二重管理することになります。proxy => nginx2 => app とすれば二重管理を防ぐことができます)

実装3. nginxで行う

という訳で今回提案したいのが、nginx-lua-moduleを用いてnginxで処理をする形です。 nginx => app の経路はそのままで、nginxのレイヤーでbatch requestはバラしちゃおうぜというものです。前項のproxy server の役割をluaが担当することになります。

f:id:mosa_siru:20151001223846p:plain

backendのJSON-RPC APIはsingle requestさえ捌ければ良く、サーバー言語に依らずこの構成が取ることができ、管理コンポーネントも増えないのがメリットです。

イベントドリブンなnginxの性質を活かし、ノンブロッキング(=backendの処理を待たなくてよい)で並列なbatch requestを簡単に実装することができるのがポイントです。

nginx-lua-moduleにはlocaltion.captureという機能があり、nginx内でノンブロッキングな内部リクエスト(subrequest)を発行することが簡単にできます。また、location.capture_multiでは複数のsubrequestを並列で投げられます。これを用いて、single requestを捌けるlocationへ並列にsubrequestを発行することで、簡単にbatch requestを実装することができます。 (また、内部リクエストなので、single requestのreverse proxy設定はそのまま流用することができ、前述した二重管理問題もおきません)

ややこしいことを書きましたが、今回batch requestに必要な機能をluaのライブラリ化したため、これを用いるだけで簡単に使うことができます。

github.com

lua-resty-jsonrpc-batch

という訳で以下はライブラリの紹介です。

使い方

以下のような設定をnginxに書くだけで、batch requestが並列に、ノンブロッキングに実装できます。

server {
    location /jsonrpc {
        # jsonrpc single request endpoint
    }
    location /jsonrpc/batch {
        lua_need_request_body on;

        content_by_lua '
            local jsonrpc_batch = require "resty.jsonrpc.batch"
            client = jsonrpc_batch:new()
            local res, err = client:batch_request({
                path    = "/jsonrpc",
                request = ngx.var.request_body,
            })
            if err then
                ngx.exit(500)
            end
            ngx.say(res)
        ';
    }
}

/jsonrpc はjsonrpcのsingle requestを処理できるendpointとします。

/jsonrpc/batch のエンドポイントは、request bodyにJSON Arrayが来た場合は、バラして並列に /jsonrpc のエンドポイントにsubrequestを投げます。

シンプルですね!

以下はもう少し凝った使い方がしたい人のための例です。

http {

    init_by_lua '
        local jsonrpc_batch = require "resty.jsonrpc.batch"
        client = jsonrpc_batch.new({
            -- バッチリクエストのArray sizeの制限
            max_batch_array_size = 10,
            -- バックエンドの処理時間をロギングするためにnginx変数に入れる
            before_subrequest = function(self, ctx, req)
                ctx.start_at = ngx.now()
            end,
            after_subrequest = function(self, ctx, resps, req)
                ngx.var.jsonrpc_upstream_response_time = ngx.now() - ctx.start_at
            end,
        })
    ';

    server {
        set $jsonrpc_upstream_response_time  -;

        location ~ /jsonrpc/method/.* {
            # jsonrpc endpoint
        }

        location /jsonrpc/batch {
            lua_need_request_body on;

            content_by_lua '
                local res, err = client:batch_request({
                    -- リクエストごとにendpointを動的に選択できる
                    path = function(self, ctx, req)
                        return "/jsonrpc/method/" .. req.method
                    end,
                    request  = ngx.var.request_body,
                });
                if err then
                    ngx.log(ngx.CRIT, err);
                    ngx.exit(500);
                end
                ngx.say(res);
            ';
        }
    }
}

この例は以下のような機能を実装しています。

  • lua-resty-jsonrpc-batch オブジェクトをリクエストのたびに作るのは馬鹿らしいので、nginxのinit時に作るようにしている
  • でかいArrayが来ると攻撃になるので、最大サイズを10としている
  • subrequestの処理時間をnginx access logに入れるために、nginx変数に値を入れている
    • before_subrequest, after_subrequestのフックポイントを利用している
  • 「jsonrpc "substract" methodは /jsonrpc/method/substract のパスで受けたい」など、動的なlocationに対応している

この例でわかるように、lua-resty-jsonrpc-batchには、ロギングしたりするためのフックポイントをいくつか用意しています。 とはいえ本質はnginxのsubrequestを利用しているだけなので非常にシンプルです。 詳しくはmosasiru/lua-resty-jsonrpc-batch · GitHubをご覧ください。

インストール方法

まずはnginx-lua-moduleをnginxに入れないといけません。 インストール方法はLua | NGINX にありますが、書いてある通りopenresty (luaやら色々はいった拡張版nginx)を使うことが強く推奨されています。

macならばbrewで簡単に試すことができます。

brew install ngx_openresty

自力でビルドする場合は以下です。

curl -O http://openresty.org/download/ngx_openresty-1.9.3.1.tar.gz
tar xzvf ngx_openresty-1.9.3.1.tar.gz
cd ngx_openresty-1.9.3.1/
./configure
make
make install

lua-resty-jsonrpc-batchのインストール方法は、以下の2通りです。

(1) lua-rocksで入れる

luarocks --local install lua-resty-jsonrpc-batch

(2)ファイルを読み込む

あるいはlua-resty-jsonrpc-batchをcloneして、libディレクトリを適当なところにおき、以下のようにlua_package_pathを指定することでライブラリを読み込むことができます。

http {
    lua_package_path '/hogehoge/lua-resty-jsonrpc-batch/lib/?.lua;;';

最後に

長々と紹介してきましたが、まとめると、以下の3点になります。

  • jsonrpc 便利! batch requestも便利!
  • batch requestには色んなサーバー実装が考えられるけど、nginx + luaでやると良さそう!
  • lua-resty-jsonrpc-batch っていうライブラリ作ったから使ってね

です。

lua-resty-jsonrpc-batchはまだ色々いじる余地があると思うので、どんどん利用していただけるとありがたいです。

最後に。nginx + luaには本当にデメリットはないのでしょうか?実運用に耐えられるモジュールなのでしょうか?パフォーマンスは?などの疑問が残ります。 これらは、実際に技術調査したり運用して地雷を踏んだ結果をまとめて、次回の記事にしたいと思います。 先に結論だけいうと、少し気をつけるところはあるけど普通に使えるぜ!!です!

ハッカドールにおけるElasticsearch利用法について発表しました

DeNA社内でのElasticsearch勉強会にて、アプリ「ハッカドール」におけるElasitcsearch利用法について発表してきました。

スライドはこちら。

 
 Elasitcsearchの中身や運用について濃く触れるというよりは、ちゃんとした検索エンジン作るための泥臭い話がメインになっています。
(ある意味で昨年Tokyo WebMiningにて発表した捗るリコメンドシステムの裏事情(ハッカドール)の派生スライドです。)
 
 
ハッカドールは1周年を迎え、来月にはアニメ化もされます。ニュースアプリがアニメ化とか開発者としてもどうなるか予想がつきませんが、放映時には全裸待機しようと思います!
hackadoll-anime.com