冪等なデータ処理ジョブを書く

こんにちは、マーケティングサポート事業部データインテリジェンスグループの井上寛之(@inohiro)です。普段はマーケティングに使われるプライベートDMP(データマネジメントプラットフォーム)の開発を行っています。本稿では、その過程で得られた冪等なデータ処理ジョブの書き方に関する工夫を紹介したいと思います。今回は、RDBMS上で SQL によるデータ処理を前提に紹介しますが、この考え方は他の言語や環境におけるデータ処理についても応用できるはずです。

まずクックパッドのDMPと、冪等なジョブについて簡単に説明し、ジョブを冪等にするポイントを挙げます。また、SQL バッチジョブフレームワークである bricolage を使った、冪等なジョブの実装例を示します。

クックパッドのDMPと冪等なジョブ

クックパッドのプライベートDMPは、データウェアハウス(社内の巨大な分析用データベースで、クックパッドでは Amazon Redshift を使っている。以下 DWH) 上で構築されており、主に cookpad.com 上のターゲット広告や、社内のデータ分析に活用されています。材料となるデータは、広告のインプレッションログや、クックパッド上での検索・レシピ閲覧ログです。また他社から得たデータを DWH に取り込んで、活用したりしています。

これらのデータを活用したバッチジョブ群は、社内でも比較的大きめのサイズになっており、途中でジョブが止まってしまうことも考慮して、基本的にそれぞれのジョブが冪等な結果を生成するように開発されています。

冪等についての詳しい説明は省略しますが、簡単に言うと「あるジョブを何度実行しても、同じ結果が得られる」ということです。特にデータ処理の文脈においては、「途中で集計ジョブが失敗してしまったがために、ある日のデータが重複・欠損して生成されていた」ということはあってはなりません。ジョブが冪等になるように開発されていれば、失敗した場合のリトライも比較的簡単になります。また、ジョブが失敗しなかったとしても、(オペミス等で)たまたま複数回実行されるかもしれませんし、毎回同じ結果が生成されるべきです。

さらに、ジョブを冪等になるように開発すると、開発時に手元で試しに実行してみるときも検証が簡単なため、おすすめです。

冪等なジョブにするポイント

プライベート DMP を開発して得られた、ジョブを冪等にするためのポイントはズバリ「トランザクションを使え」です。

トランザクションを使ってロールバック

大量のデータを、長時間(N時間)かけて書き込むようなバッチジョブを考えるとき、途中で止まってしまったり、そこから復旧(リトライ)するという状況は予め考慮されているべきです。このとき、書き込む先がトランザクションをサポートするようなデータベース(一般的なRDBMSなど)ならば、トランザクションを利用しましょう。一つのトランザクションとしてまとめた一連の処理は、「すべて成功した状態」か、「すべて失敗した状態(ロールバック)」のどちらかになることが保証され、中途半端な状態にはなりません。途中で失敗しても、最初からぜんぶ書き直すことになりますが、冪等性は保たれています。

クックパッドの DMP は並列分散 RDB である Amazon Redshift 上に構築されているので、トランザクションをフルに活用しています。

自前でロールバック

一度実行された集計ジョブを再度実行した場面を考えてみます。再度実行される理由はいろいろ考えられますが、「意図せず間違って実行されてしまった」というのも同じような状況と考えられます。前回実行したときと同じ結果が得られれば問題ありませんが、集計した結果が重複してしまうと、後続のジョブが失敗するか、最悪の場合正しくない分析結果を用いて、何らかの意思決定が行われてしまうかもしれません。

つまり、現在実行中のジョブが書き込むテーブルに、今から書き込もうとしている条件で、既にデータが書き込まれているかもしれないのです。そこで、新たな結果をを書き込む前に、既存の行を削除(自前でロールバック)することで重複の発生を避けます。さらに、「削除」と「新しい結果の書き込み」を一つのトランザクションにまとめることで、このジョブは冪等になります。

冪等なデータ構造を利用する

一方で、トランザクションをサポートしないような NoSQL データベースを使っているとき、ジョブを冪等にするのは比較的簡単ではありません。このような状況で考えられる一つの解決策として、何度書き込まれても結果が変わらないデータ構造の利用が挙げられます。集合(Set)やハッシュテーブルです。これらのデータ構造は、データの順序は保証されないものの、既に存在する値(もしくはキー)を書き込んでも、要素が重複しません。

クックパッドの DMP で作成したターゲット広告用のデータは、最終的に Amazon DynamoDB *1 に書き込まれ、広告配信サーバーがそのデータを使っています。ターゲット広告用のデータは、一度に数千万要素をバッチジョブが並列で書き込みますが、このジョブが稀に失敗することがあったり、過去に書き込まれている要素が時を経て再度書き込まれることがあるため、SS(文字列のセット)型を使っています。過去には Redis のセット型を使っていることもありました。

bricolage による冪等なジョブの実装例

クックパッドの DMP だけでなく、社内で SQL バッチジョブを書くときのデファクトスタンダードになっている bricolage には、頻出パターンのジョブを書く際に便利な「ジョブ・クラス」がいくつかあり、これを使うことで冪等なジョブを簡単に実装することができます。この節では bricolage を使った「トランザクションでロールバック」パターンと、「自前でロールバック」パターンの実装例を示します。

bricolage については、ここでは詳しく説明しませんが、詳細については過去の記事「巨大なバッチを分割して構成する 〜SQLバッチフレームワークBricolage〜」や、RubyKaigi 2019 でのLT「Write ETL or ELT data processing jobs with bricolage.」をご参照ください。また inohiro/rubykaigi2019_bricolage_demo にデモプロジェクトを置いてあります。

「トランザクションでロールバック」パターン

rebuild-drop もしくは rebuild-rename ジョブ・クラスを使うと、「現行のテーブルを削除し、新規のテーブルに集計結果を書き込む」または「新規にテーブルを作り、集計結果を書き込み、現行のテーブルとすり替える」という操作を、一つのトランザクションで行うジョブを簡単に実装することができます。rebuild-drop は対象のテーブルを作り直す前に drop table し、rebuild-rename はすり替えられた古いテーブルを、別名で残しておきます。

以下は、毎日作り変えられるようなサマリーテーブルを rebuild-drop ジョブ・クラスで実装した例です。

/*
class: rebuild-drop -- ジョブ・クラスの指定
dest-table: $public_schema.articles_summary
table-def: articles_summary.ct
src-tables:
  pv_log: $public_schema.pv_log
analyze: false
*/

insert into $dest_table
select
    date_trunc('day', logtime)::date as day
    , id_param::integer as article_id
    , count(*) as pv
from
    $pv_log
where
    controller = 'articles' and action = 'show'
    and logtime < '$today'::date
group by
    1, 2
;

このジョブは、以下の SQL に変換されて実行されます。

\timing on

begin transaction; -- トランザクション開始

drop table if exists public.articles_summary cascade; -- 既存テーブルの削除

/* /Users/hiroyuki-inoue/devel/github/rubykaigi2019_bricolage_demo/demo/articles_summary.ct */
create table public.articles_summary
( day date
, article_id integer
, pv bigint
)
;

/* demo/articles_summary-rebuild.sql.job */
insert into public.articles_summary
select
    date_trunc('day', logtime)::date as day
    , id_param::integer as article_id
    , count(*) as pv
from
    public.pv_log
where
    controller = 'articles' and action = 'show'
    and logtime < '2019-07-13'::date
group by
    1, 2
;

commit; -- トランザクション終了

ジョブ全体が begin transaction;commit; で囲われているので、仮に集計クエリに問題があり失敗した場合は、元のテーブルは削除されずに残ります。

「自前でロールバック」パターン

insert-delta ジョブ・クラスは既存のテーブルに差分を書き込むために利用され、差分を書き込む直前に指定した条件でdelete を実行します。また、一連の SQL は一つのトランザクションの中で行われるので、delete 直後の差分を集計するクエリが失敗しても安心です。

以下は、日毎に広告インプレッションを蓄積しているテーブルimpressions_summary に、前日($data_date*2の集計結果を書き込むジョブの例です。delete-cond: に削除条件を指定します。今回の例では、集約条件の一つである日付を指定しています。

/*
class: insert-delta -- ジョブ・クラスの指定
dest-table: $public_schema.impressions_summary
table-def: impressions_summary.ct
src-tables:
    impressions: $ad_schema.impressions
delete-cond: "data_date = '$data_date'::date" -- 削除条件の指定
analyze: false
*/

insert into $dest_table
select
    '$data_date'::date as data_date
    , platform_id
    , device_type
    , count(*) as impressions
from
    $impressions
group by
    1, 2, 3
;

このジョブは以下のような SQL に変換され、実行されます。

\timing on

begin transaction; -- トランザクション開始

delete from impressions_summary where data_date = '2019-07-12'::date; -- 既存行を指定した条件で削除

/* demo/impressions_summary-add.sql.job */
insert into impressions_summary
select
    '2019-07-12'::date as data_date
    , platform_id
    , device_type
    , count(*) as impressions
from
    ad.impressions
group by
    1, 2, 3
;

commit; -- トランザクション終了

テーブルに書き込む前に指定した条件(delete-cond: "data_date = '$data_date'::date")で delete クエリが実行され、"掃除"してから書き込むクエリが実行されるのが確認できると思います。対象の行がなければ何も削除されませんし、対象の行が存在すれば、新たな結果を書き込む前に削除されます。

まとめ

本稿では、クックパッドの DMP 開発において「冪等なデータ処理ジョブ」を書くために行われているいくつかの工夫について紹介しました。また、bricolage を使ってこれらのジョブを実装する例を示しました。

このように、トランザクションのあるデータベースを利用する場合は、なるべくその恩恵に乗っかるのがお手軽です。また、一つのジョブに色々なことを詰め込まず、ジョブを小さく保つことで、ロールバックの対象も小さくなり、失敗した場合のリトライなどもシンプルに行えると思います。bricolage のジョブ・クラスを上手に使うことで、トランザクションを利用した冪等なデータ処理ジョブを簡単に実装することができます。ぜひお試しください。

*1:この記事を書いていて思い出しましたが、Amazon DynamoDB はトランザクションをサポートしたのでした https://aws.amazon.com/jp/blogs/news/new-amazon-dynamodb-transactions/

*2:変数には前日の日付が入るように仮定しているが、ジョブのオプションで上書きが可能

サービス開発でぶつかってきた壁と、そのとき助けてくれた本

こんにちは、開発ディレクターの五味です。クックパッドにレシピを投稿してくれるユーザーのための機能やサービスを開発する「投稿開発部」に在籍しております。

投稿開発部は、2018年1月に前身となる部からメンバーを一新して発足した部署です。自分たちで1から戦略を作るため、強い実感を持ってユーザーを理解することを信条に、資料を読んだり前任者に聞いたりするだけではなく、実際にユーザーとたくさん話し、たくさんレシピを投稿し、ユーザーのことをたくさん考えてきました。

この記事では、その中でぶつかった課題を解決するために取り入れた書籍や、それをうまく業務に取り入れるために行っている工夫を紹介します。

サービス開発にはさまざまな壁が現れる

ユーザーと事業目標に真摯に向き合うほど、サービス開発にはたくさんの壁が現れます。私たちも例外ではなく、部の発足以降、以下のような壁に激突してきました。

  1. 「ユーザー課題の見極め難しい!」の壁
  2. 「戦略づくり難しい!」の壁
  3. 「良いソリューションアイデアが出ない!」の壁

f:id:natsuki53:20190711170248j:plain
開発の歩みとぶつかった壁の所在

壁にぶつかったら学習チャンス

投稿開発部には、日常的に書籍を読んで、仕事に取り入れようとする文化があります。特に壁にぶつかった時は、ブレイクスルーを図るため、チームで意識的に本を読んだりします。

この1年半で、激突した壁ごとにお世話になった本をご紹介します。

1. 「ユーザー課題の見極め難しい!」の壁

部の発足後、早々に苦悩したのが「レシピ投稿ユーザーの本質的な課題は何か」という問いでした。

クックパッドには、レシピを検索するユーザーと投稿するユーザーがいますが、それぞれ数や志向が大きく異なります。投稿ユーザーのための開発をするなら、彼らのことを誰よりもわかっているべきです。「投稿ユーザーはなぜレシピを投稿してくれるのか?」という命題に、自分たちが心底信じられる答えを得たい、でもどうすれば良いのだろう...というのが最初の壁でした。

そしてその時は、以下の本からとっかかりを得ました。

「ジョブ理論」

https://www.amazon.co.jp/dp/B0746JCN8B/

人が何かプロダクトを使う時、必ずその人は何らかの解決したい「ジョブ」を持っており、その解決のためにプロダクトを「雇用」している、という見地に立って、顧客の「ジョブの解決」に寄り添うプロダクト開発を論じた本。「ジョブ」はいわゆる「課題」や「インサイト」と似た意味だが、顧客の置かれている「状況」により注目し、特定状況下で発生する実用的な欲求に目を向けている。

どう活用したか?

  • ユーザーにレシピ投稿を雇用させている「ジョブ」は何なのか、読み解くことにしました
  • 具体的には、さまざまなレシピ投稿者を呼んで根掘り葉掘り話を聞き出し、あとから彼らの「ジョブ」を推察するインタビューを実施しました

やってみてどうだったか?

ユーザーの抱える「ジョブ」の解決をサービス発想の起点にすることは本質的だと感じます。これだ!という「ジョブ」を発見できると強いコンセプトが作れる、という実感が、取り組むうちに芽生えてきました。

ただし「ジョブ」は、ユーザーの発言内容だけでなく、発話時の表情やその人の価値観、普段の生活の様子など、さまざまな情報を複合的に組み合わせて考えないと推測できないので、慣れるまでは考えるのが難しいです。私たちは、インタビュー後に観察した情報を参加者全員でぶちまけ、それを見ながら「このユーザーのジョブは何だったか」を議論して定義する手法で乗り切りました。インタビューの内容は、本に出てくる、ユーザーの発話からキーワードを捉えて深堀りしていく様子を参考にし構成しています。

また、「ジョブ」の定義を何度か重ねていくと、「ジョブ」の粒度をどのくらいに設定するかが難しいことに気づくと思います。その際は本で紹介される事例を引き合いに出しながら、”定義した「ジョブ」を解決している競合サービスを思いつくか”という基準で調整するのがおすすめです。

なお今では施策設計の際、誰のどんな「ジョブ」をターゲットにするのか定義することが必須になるほど、「ジョブ理論」は深く活用されています。最近はインタビューの結果を人物ごとに「ジョブ」起点で記事のようにまとめた「ユーザー白書」を作成・蓄積するなど、実践手法も進化させながら続けています。

f:id:natsuki53:20190711174549p:plain
インタビュー後、「ジョブ」を見つけようとしている議論の様子

2. 「戦略づくり難しい!」の壁

向き合うユーザーのことがわかってきたところで、次は、事業目標をどう達成するか、戦略立案の壁にぶつかりました。投稿開発部の事業戦略は、部長が部の発足前に作った草案を、適時メンバーを巻き込みながら見直し、アップデートしています。目標がかなり動かし難い数字であることに加え、自分たちの学びも日々進化していく状況であるためです。

ただ当時は、事業戦略を考えたことのないメンバーが大半だったので、議論に入っても、意見すらうまく出ない状態でした。そこで、部長主導で以下の書籍を取り入れました。

「ストーリーとしての競争戦略」

https://www.amazon.co.jp/dp/4492532706/

競争戦略(事業戦略)は、静止画でなく動画、ストーリーであり、良い戦略は人に思わず話したくなる面白いストーリーになるはずだ、という見地に立って、他社が追従しようと思えないほど優れた戦略を作るための考え方を説いた本。講義を受けているような文調で、文量も多いが、事業戦略立案の本質を捉えて論じている良著。実在企業の事例も多く紹介されて参考しやすく、チームでの議論の際の引き合いに出しやすい(ただしITの事例は少ない)。

どう活用したか?

  • レシピ投稿サービスのコンセプト=「本当のところ我々は誰に何を売っているのか?」を定義し直すことから始め、自分たちの戦略ストーリー図を作りました
  • その上で、本で紹介されている他社の戦略図と見比べながら、自分たちの戦略図を磨いていきました

やってみてどうだったか?

自分たちのサービスが「本当のところ誰に何を売っているのか?」を定義するのは、想像以上に難しいです!これまでのインタビューで得たユーザーのエピソードをかき集めたり、競合サービスのコンセプトを推測して自社と比較したり、腹落ちする定義に至るまでに少し時間がかかりました。

しかし、そこで定義した提供価値を軸に、ゴール達成を引き起こすまでの中長期のストーリーを描いて作った戦略図は、シンプルで筋が通っていて、戦術や計画を考えるのに使いやすいです。現場でも、単発のアイデアをやみくもに実行することがなくなり、複数の要素を因果関係を持たせながら実現していく計画を考えられるようになりました。

ただし、実際に戦略図を書き起こすのは至難です。筋の良い戦略ストーリー図は、そらで描けるほどシンプルで、且つ、目標達成のために必要な変化を含む...とのことですが、ここは本に紹介されている実在企業の戦略図を横目に見ながら頭を捻りまくるしかありません。戦略図をチームで議論して描くのは難しすぎるので、今はコンセプトと解決策をチームで議論し、ある程度骨子が見えたところで部長が戦略図の草案を書くことが多いです。チームでは、その草案を元に、ストーリーを確実に実現する方法や、より強い「非合理」を入れてストーリーを面白くする方法を考えることにしています。

f:id:natsuki53:20190711171051p:plain
何度もお手本にしている、スターバックスの戦略ストーリー図

3. 「良いソリューションアイデアが出ない!」の壁

事業戦略ができ、具体的な施策づくりに入って行けるようになると、今度は自分たちの出すソリューションアイデアが今ひとつに思えて悩むようになってきました。戦略と仮説には自信があるのに、思いつく解決策に新しさや捻りがない、そもそも出てくるアイデアの数が少ない...。この壁は二重構造になっていて、以下2つの問題で成り立っていました。

  • ①チームでのアイデア出しの進め方がわからない
  • ②出すアイデアの量・質に自信が持てない

それぞれを助けてくれた本は以下です。

① 「SPRINT 最速仕事術―あらゆる仕事がうまくいく最も合理的な方法」

https://www.amazon.co.jp/dp/B06Y5NW5PQ/

問題①に効いた本。Googleで開発されたという、5日間で新しいアイデアを形にして検証・評価まで完了させる「スプリント」というフレームワークを紹介し、その実践手順を詳しく解説している。

どう活用したか?

  • 施策検証の段階にある施策で何度かそのまま取り入れて、スプリントを実施しました
  • その後、一部の手法を部分的に切り出して、普段の会議に取り入れるようにしました

やってみてどうだった?

スプリント自体は一長一短あると感じます。良いところは、テーマに対して参加者全員で大量のインプットを得て、めちゃくちゃ集中して考えることです。メンバーの脳内同期やステークホルダーの巻き込みにも効いたりします。また、とにかく時間が制限されるので、煮え切らないアイデアを捨てる判断ができる点も良いです。デメリットは、参加者全員の5日間の時間拘束が辛いことと、得られる成果の質が参加者の能力に左右されること。また、「レシピ投稿」はもともと施策の効きに時間がかかる傾向があるのですが、それを1日のインタビューで評価して良いのか?という疑念が拭いきれないことも、私たちにとっては大きな気がかりです。

ただ、「有能な人の仕事の流れをフレームワーク化した」と言うだけあって、スプリントのフレームワークには、目的に対する情報インプット、アイデア出し、検証、評価を効率的に行う工夫が詰まっていると感じます。それらを切り出して普段の業務に取り入れても、アイデアを考えたり意思決定するのがラクになり、チームのアイデア出しのパフォーマンスが高まります。専門家インタビュー、光速デモ、クレイジー8、ストーリーボード、ヒートマップ、サイレント投票などの手法は、普段の会議でも単発で取り入れやすいのでおすすめです。

f:id:natsuki53:20190711171137p:plain
スプリントの手法を活用したアイデア出しの様子

② 「直感と論理をつなぐ思考法 VISION DRIVEN」

https://www.amazon.co.jp/dp/B07NMN1B5Z/

問題②に効いた本。世の中を動かしてきたのは、ロジカルに組み上げられたアイデアではなく、「自分駆動の妄想」を起点にしたビジョンだ、という前提の元、まず根拠のない妄想(ビジョン)があって、それを実現する筋道を作るために論理を組み立てる「ビジョン思考」を提唱している。「ビジョン思考」の強化方法や、それに則ったアイデア作りの方法も詳しく解説されている。

どう活用したか?

  • チームで、既存路線上にない新しいアイデアを出さなければいけない時に、本で紹介されている「組替」の手法を取り入れました
    • 変化を起こしたい事象の「当たり前」を洗い出し、違和感のある「当たり前」をひっくり返してから、それを元に新しいアイデアを生み出していく手法です
  • 併せて、チームメンバーと「プロトタイピング志向」を徹底する同盟を組んでいます
    • 生煮えの考えもissue化したり、粗いプロトタイプを作って早期に人に見せることでアイデアに客観的な視点を加え、そこから練り上げていくところに時間をかけます

やってみてどうだったか?

「組替」の手法は取り入れ始めて日が浅いですが、以前より新しさを含んだアイデアがたくさん出るようになったと感じます。フレームワークに則って頭と手を動かせば、「ひらめき」=”既存要素の組み替え”を意図的に起こせるよう考えられており、チームで一緒に実践しやすいのも良い点です。

「プロトタイピング志向」は、普段のディレクター業務で実践するのにはまだ慣れないですが、うまくできた時は、1人で考え込むことに時間を使ってしまった時よりも成果物の質は高まると感じます。ちなみにこのブログ記事も、構想段階から色んな人に相談し、レビューしてもらって仕上げました!

また個人的にはこの本によって、「仕事のアイデアは論理で組み上げなければならない」という思い込みを打破できたことは大きかったです。まず直感に目を向け、それを人に説明できるよう後から論理づけして磨いていくという思考を意識すると、出せるアイデアの質が変わってくるのを感じます。

f:id:natsuki53:20190711171221p:plain
「組替」の手法を使ったアイデア出しの様子

本の知識をチームに取り入れるために

せっかく読んだ本から知識を”モノにする”には、読了後すみやかに得た知識を業務で実践することが1番重要だと感じます。また、チームで仕事を進める中では、”共通言語を作る”という意味でも、書籍で得た知識の共有は有効です。特にチームが悩んでいる時や、抽象度の高い議論を進めなければならない時、うまい共通言語を得ると、停滞していた話が進み始めることが多々あります。

新しい知見を業務で実践するところまで漕ぎつけたり、自分の読んでいる本の知識をうまくチームに共有できるかは、読んだ人の裁量によりがちです。そのため、本の知識をチームや業務に取り入れやすくするために、以下のような工夫をしています。

1.読んでいる本を共有しやすくする工夫

読書感想文共有スレ

GHEの部署のリポジトリ配下に「サービス開発系の読んだ本の感想を書くスレ」というissueを常設。簡素な1行コメントから超長文レポートまで、メンバーが読書感想を自由にpostしています。熱量の高い感想文には自然と注目が集まるし、共有したい知識はそこにまとめておけば参照してもらえるので、その後の議論でも話題に出しやすくなるのが利点です。最近このissueはコンテンツ力が増してきて、他部署からもファンや投稿者を創出する人気スレ(?)になりつつあります。

定例や1on1で読んでいる本の話をする

定例ミーティングや部長との1on1で、いま読んでいる本をよく共有しあいます。同じ本でも人によって読み取り方が異なるので、話すことで新しい観点が得られたり、チームの誰かと「それいいね!」「あの施策で試せるじゃん!」という会話をしておけると、そのあとの情報発信や業務での実践提案がしやすくなるので、意図的に活用しています。

その他、読んで良かった本は、物理本を買ってデスクに置いておくのも一手です。興味を持ってくれた人にサッと貸し出して、味方を増やします。

f:id:natsuki53:20190711171325j:plain

2.本の知識を業務で実践しやすくする工夫

わかりやすい事例や概念を切り出しておく

目的の書籍を読んでいない人を巻き込みたい時に使います。何も知らない相手に、自分が本から得た知識を口頭で説明してわかってもらう(その上で同じレベルで議論に参加してもらう)ことは至難の技ですが、本の中からわかりやすい事例や、概念を端的に表した図解などを切り出しておいて「うちもこんな風にやってみませんか」と提案すると、やりたいことをわかってもらいやすくなります。

読書後、自分の業務で実践するtodoを出す

「本を読んでも自分の仕事にどう活かせるか、パッとわからない...」という人(=私)におすすめの手法です。本を1冊読んだら、そこから自分の仕事で試してみたいtodoを1〜3個だけ考え出します。それだけで実践に運べる確率が上がります。「前のtodoが終わるまで次の本を読めない」というルールをつけると、より強制力が働くのでおすすめです。あとは実際にやってみて、継続するか・やめるかを振り返る機会を作れば完璧です。

f:id:natsuki53:20190711171436p:plain

おわりに

今回は特に大きかった壁と、ヘビロテしている選りすぐりの本を紹介しましたが、チームでお世話になってる本は他にもまだまだたくさんあります。仕事に行き詰まった時は、視野を広げて新しい知識を取り入れるチャンスと捉え、これからも積極的に本を読んでいきたいところです。

なお現在は、よりインプットの幅を広げたく、書籍に加えて、似た課題に直面している方々との情報交換も積極的に行っていきたい所存です!もしご興味を持ってくださる方がいらっしゃいましたら、お気軽にご連絡ください。
fb: https://www.facebook.com/natsuki.gomi.7

そしてそして、こんな私たちと一緒に壁に激突して、一緒に成長してくれる仲間も募集中です!募集中の職種は採用サイトからご確認ください!
https://info.cookpad.com/careers/

EuRuKo 2019 で発表してきました

技術部でフルタイム Ruby コミッタをしている遠藤(@mametter)です。フルタイムで Ruby を開発しています。

先日、オランダのロッテルダムで開催された EuRuKo 2019 で発表してきたので、簡単にレポートします。

EuRuKo とは

EuRuKo は、毎年ヨーロッパのどこかで開催されている Ruby のカンファレンスです。

f:id:ku-ma-me:20190709131404j:plain
EuRuKo 2019 会場

シングルセッション

世界の Ruby カンファレンスといえば、アメリカの RubyConf 、ヨーロッパの EuRuKo 、日本の RubyKaigi だと勝手に思っていますが、この中で EuRuKo の特徴というと、シングルセッションなことです*1。つまり、発表を聞く会議室は 1 つだけです。どれを聞くか迷わなくていいですね。

必然的に、発表の数は少ないです。YouTube の動画の数を見てみると、RubyConf 2018 は 70 件 、RubyKaigi 2019 は 65 件 ですが、今回の EuRuKo 2019 の発表は 15 件、LT を含めても 20 件でした。発表したい人からすると、採択されるのが最難です(たぶん採択率 1 桁)。

city pitch

また、次回の開催地が会期中に投票で決められるのも特徴です。立候補した街の人が city pitch(街の宣伝)を行い、会場の拍手喝采の大きさで決められていました。

f:id:ku-ma-me:20190709131545j:plain
EuRuKo 2020 開催に立候補した都市

オーストラリアのメルボルンが立候補してるのが話題でしたが、来年はヘルシンキになりました。

EuRuKo 2019 の様子

オランダのロッテルダムにある、ss Rotterdam っていう退役したクルーズ船が会場でした。現在は岸に固定されてホテルになってます。

f:id:ku-ma-me:20190709131400j:plain
EuRuKo 2019 の会場(ss Rotterdam)

EuRuKo 発表の傾向としては、Ruby コア開発の話よりは Ruby を使う話が中心です。そういう意味で、RubyKaigi よりは RubyConf に似ています。

その中でも印象に残った話を 3 つほど簡単に紹介します。

Functional (Future) Ruby - Yukihiro Matsumoto

matz のキーノート。動画がすでに上がっています。

ざっと内容を紹介します。まずは Ruby 3 の 3 つのゴールである Static analysis 、Performance 、Concurrency の進捗が説明されました。詳細はいろいろなところで見つかるので、主にキーワードだけ挙げておきます。

Static analysis は、公式型シグネチャフォーマットとなる予定の ruby-signature 、型シグネチャのない Ruby プログラムをゆるく型検証し型シグネチャを推定する type profiler 、型シグネチャを前提として、型シグネチャとコードの対応を検証する SorbetSteep などが登場してきたことがダイジェストで紹介されました。 Performance は、オブジェクトを並べ替えてメモリのフラグメンテーションを軽減する Object compaction によってメモリボトルネックなアプリの効率を上げ、また JIT コンパイラである MJIT や MIR によって CPU ボトルネックなアプリの実行効率を上げることが述べられました。 Concurrency については、並列実行向けには Guilds(仮称)が計画されており、大規模並行処理向けには AutoFiber(仮称)が検討されていることが触れられました。

それから、関数型プログラミングっぽい他言語からインスパイアされた検討中の機能が紹介されました。

  • ブロックの無名引数

@1 で引数を暗黙的に参照できます。

3.times { p @1 }
# 3.times {|i,| p i } と同じ

@2 だと 2 番目の引数、@3 だと 3 番目の引数を参照できます。しかし現実的には複数の引数を参照仕分けるユースケースは稀なので、引数 1 つに特化し、別の記号(@ 、Kotlin や Groovy 風の it 、Scheme の <>)などにするか検討中とのこと。他人事のように言ってますが、it を提案しているのは遠藤です(Feature #15897)。

  • パターンマッチング。

データ構造が想定パターンになっているか調べて、その要素を変数に束縛することができます。

case JSON.parse(json, symbolize_names: true)
in {name: "Alice", children: [name: "Bob", age: age]}
  p age
in _
  p "no Alice"
end

典型的には、JSON の処理に便利そうです。

  • パイプライン演算子

パイプライン演算子が試験的に Ruby に取り込まれました。

1..100
  |> map {|x| rand(x)}
  |> sort
  |> reverse
  |> take 5
  |> display

「パイプライン演算子はプライマリの引数を呼び出しにわたすもの」「Ruby ではプライマリの引数はレシーバ」ということで、実質的にメソッド呼び出し演算子(.)と同じものになっています(このへんの詳細は遠藤のブログを参照ください)。しかし非常に controversial であり、名称・記号を変更するか、キャンセルするか(まだリリース前なので互換性の問題なく取り消せる)、現在も悩んでいるとのことです。

というように、様々な意見はありつつも Ruby はまだまだ新しい機能を取り入れて進化していってます。

What causes Ruby memory bloat? - Hongli Lai

Ruby プロセスのメモリ肥大化の原因を調査した報告です。発表者の Hongli Lai は Passenger や Ruby Enterprise Edition の作者です。この発表はだいたい次のブログで発表済みだった内容ですが、今回の EuRuKo で一番テクニカルで、個人的には一番おもしろい発表でした。

www.joyfulbikeshedding.com

Ruby のメモリ肥大化と言えば、「Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)」という記事が有名です。端的に言えば「jemalloc を使え、もしくは環境変数 MALLOC_ARENA_MAX=2 を設定しろ」というやつで、見たことがある人も多いのではないでしょうか。しかし Hongli Lai はこの記事を疑い、再調査したところ、メモリ肥大化の本当の原因は別にあったということです。

かいつまんで説明します。Ruby がメモリを使うのは 2 種類あります。

  • (1) Ruby オブジェクトのヒープ
  • (2) Ruby オブジェクト以外のヒープ(主に文字列や配列など)

(1) については、あるアプリケーションでオブジェクトのヒープサイズを測定したところ、全使用メモリ 230 MB に対して 7 MB だったので、肥大化の原因としては無視できます。

(2) についてはちょっとややこしいのですが、malloc したメモリが一部だけ生き残って断片化が起きるという前提で、glibc の malloc はスレッドごとにメモリアリーナを確保するので問題がひどくなるというのが元記事の主張で、そのために MALLOC_ARENA_MAX=2 を設定すればアリーナの数が高々 2 に抑えられるので問題が軽減するとされています。

しかし Hongli Lai は「malloc 領域の中で断片化は本当に起きているのか?」と疑問を持ち、可視化機能付き malloc ラッパを書いて確かめたそうです(すごい)。その結果がこれ。

f:id:ku-ma-me:20190709131409j:plain
"What causes Ruby memory bloat?" Ruby プロセスのメモリ使用状況の可視化

赤い領域は使用中、灰色は free 済み(すでに使用されていない)だが OS に返却されていない領域、白は返却済みの領域です。赤い領域が飛び飛びにあるので断片化は多少起きているものの、完全に灰色のところも多いことがわかります。つまり、断片化はそれほどひどくなく、むしろ OS に領域を返却できていないことが問題です。 使用済みだが OS に返却されていないのは、こまめに返却すると実行効率が下がることがあるためだと思われます。malloc_trim という関数を使えば使用済み領域を明示的に OS に返却できる *2 ということで、Ruby にパッチをあてて再実験した結果がこちら。

f:id:ku-ma-me:20190709131803j:plain
"What causes Ruby memory bloat?" malloc_trim パッチ後のメモリ使用の様子

めでたく、白い部分が増えました。これにより、使用メモリ 230 MB だったところが 60 MB にまで減ったということです。MALLOC_ARENA_MAX=2 を指定したら 53 MB なので、効果としてはほぼ同じとのこと。

また、Fullstaq Ruby という Ruby のディストリビューションを始めたということが発表されていました。今回の調査結果を含むということです。

The Past, Present, and Future of Rails at GitHub - Eileen M. Uchitelle

GitHub で働く Rails コミッターである Eileen の発表。Rails わかんないのでさらっと紹介です。

なんと、GitHub では、Rails をフォークして使っていたそうです。

f:id:ku-ma-me:20190709131553j:plain
"The Past, Present, and Future of Rails at GitHub" GitHub は Rails をフォークしていた

しかし、先端の Rails にどんどん置いてかれて開発もメンテナンスも辛くなり、採用やセキュリティ対応も厳しくなってきたので、がんばってフォークを卒業した、ということでした。

f:id:ku-ma-me:20190709131558j:plain
"The Past, Present, and Future of Rails at GitHub" GitHub は Rails のフォークをやめた

ということで、「フォークをしても良いことないよ」「ちゃんと Rails の最新版を使っていこうね」ということが何度も語られていました。

がんばってフォークを卒業するまでに相当がんばったのではないかと思われるのですが、あんまりその辺の苦労は語られていなかったのが若干心残りです。しかし、Eileen は Rails 6 に GitHub で必要な機能(Multi-DB とか)を入れまくった人なので、その背景が想像される感じで面白かったです。独自フォークを無くすためには、フォークを公式にすればいいわけです。*3

A Plan towards Ruby 3 Types - Yusuke Endoh

ついでに自分の発表です。発表資料はこちら。

発表内容は大筋で Ruby 3 の静的解析の計画と、その中で自分が作っている型プロファイラの進捗報告でした。おおよそは RubyKaigi と同じなので、クックパッド開発者ブログで書いた遠藤の過去記事をご覧ください。

進捗としては、解析速度に難があった(型プロファイラ自身のプロファイルに 10 分、optcarrot に 3 分)だったので、解析アプローチを大幅に見直して、精度を少し犠牲にしつつ高速化をしたところ、それぞれ 2.5 秒・6 秒になった、というところです。とはいえまだまだ未実装な機能だらけなので、引き続きがんばってます。

f:id:ku-ma-me:20190709131550j:plain
発表のときに壇上から撮った会場の様子

おまけ:発表者の特典

前述のとおり EuRuKo は採択率が非常に低いのですが、そのためか発表者のおもてなしがなんかすごかったです。

  • 旅費・宿泊費支給(いろいろあって遠藤は旅費をクックパッドに、宿泊費を EuRuKo に出してもらいました)
  • 空港からの送迎
  • 水上タクシーでの周辺観光
  • 現地の Rails Girls イベントへの招待
  • スピーカディナーへの招待(発表者と運営だけが参加できるディナー)
  • スピーカラウンジ(発表準備したり、議論したり、ゲームで遊んだりできるスペース)
  • 記念品(会場だった ss Rotterdam の模型)

f:id:ku-ma-me:20190709133117j:plain
EuRuKo 2019 発表者記念品と名札

まとめ

EuRuKo 2019 のダイジェスト紹介でした。

ちなみに今回の日本人参加者は matz と自分の 2 名だけでした(たぶん)。海外の Ruby カンファレンスと言うとアメリカの RubyConf が注目されがちですが、EuRuKo も行ってみるといいのではないでしょうか。来年はフィンランドのヘルシンキです。

f:id:ku-ma-me:20190709133255j:plain
EuRuKo 2019 エンディングの様子

*1:ちなみに歴史は RubyConf(2001 年)、EuRuKo(2003 年)、RubyKaigi(2006 年)の順で、最近の規模は RubyKaigi(1000 人程度)、 RubyConf(800 人程度)、EuRuKo(600 人程度)です。

*2:ドキュメントによると「ヒープの一番上の未使用メモリーの解放を試みる」となっていますが、Hongli Lai がソースを読んだ限りだと、一番上に限らず未使用領域を返却していくらしいです。

*3:手前味噌ですが oneshot coverage もクックパッドの Ruby フォークを本家に整理して再実装した感じです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/