Slide 1

Slide 1 text

ActiveRecord だけで戦うには この世界は複雑過ぎる @joker1007 銀座Rails #2

Slide 2

Slide 2 text

Self­intro @joker1007 Repro inc. CTO ( 要は色々やる人) Ruby/Rails fluentd/embulk RDB Docker/ECS Bigquery/EMR/Hive/Presto/Cassandra

Slide 3

Slide 3 text

Rails がカバーする主戦場 Web ブラウザ/ スマホからのデータ入力 RDB に永続化し、必要な時はそこから読み出す 一人の人間が同時に必要とするデータはそこまで多くない 出力データはHTML かJSON 基本的には、これで大体のWeb アプリケーションの基盤は作れる

Slide 4

Slide 4 text

そのRails の便利さを支えるのが ActiveRecord であり、 そして、最終的に最も頼りにならない箇 所でもある コントローラーがどうやってアクセスハンドリングするかとか、 ビューがどうやってテンプレート探してるかとかは、普段のアプ リ開発ではそこまで意識しなくてもやっていける。

Slide 5

Slide 5 text

現実は複雑である 弊社の様なデータ収集が基盤にあるサービスや、IoT のためにセン サーから大量のトラフィックがあるサービス、外部サービスと連 携してデータを受け渡しするサービスでは入力ソースは多岐に渡 る。 S3/SQS Kinesis/Kafka MQTT 弊社では顧客アプリケーションからのセッションデータ受け取り にS3 とSQS の連携を利用している。

Slide 6

Slide 6 text

データソースがRDB じゃない世界 受け取って、そのままRDB に流して済むならAR に渡せばいい しかし、そう簡単に行かないこともある レガシーデータとの整合性を保つためのデータ変換 データ取得の堅牢性 エラー処理とリトライハンドリング ロギング パフォーマンス要求 将来的な言語移植の可能性 そもそも出力先がRDB でないケース それなりのロジックが必要になることも多い。

Slide 7

Slide 7 text

最終的にRDB に入るのであれば、 ActiveRecord モデルのクラスに書いてし まってもやれないことはない…… 。 が、そうすると責務が肥大化して凝集度 が下がるし、色々な所と結合してめっち ゃテストがしづらくなる。

Slide 8

Slide 8 text

AR ではないモデルを用意する 弊社ではSQS をデータソースとしたモデルがある フラットに置くかネームスペースを切るかはコーディングスタイ ルに依る

Slide 9

Slide 9 text

Example c l a s s S e s s i o n M e s s a g e c l a s s < < s e l f d e f d e q u e ( & b l o c k ) p o l l e r . p o l l ( s k i p _ d e l e t e : t r u e ) d o | m s g | d a t a = O j . l o a d ( m s g . b o d y ) b e g i n y i e l d n e w ( d a t a ) p o l l e r . d e l e t e _ m e s s a g e ( m s g ) r e s c u e = > e R a i l s . l o g g e r . w a r n ( e ) e n d e n d e n d p r i v a t e d e f p o l l e r # o m i t e n d e n d e n d

Slide 10

Slide 10 text

こういったモデルを作る時に便利 Struct + ActiveModel::Validations ActiveModel/Model + ActiveModel::Attributes

Slide 11

Slide 11 text

Struct JSON ­> Hash の一歩先へ k e y w o r d _ i n i t : t r u e でめっちゃ使い易くなった Rails に依存しない 処理が軽い C レベルで定義が完了するため、普通にクラス定義するよ り1.2 から1.5 倍ぐらい早い バリデーションやデフォルト値の定義先を明確にできる 単体テストが簡単

Slide 12

Slide 12 text

ActiveModel::Attributes ついにRails 公式で、ActiveModel にリッチアトリビュートが 型変換 デフォルト値 (proc 対応) カスタムタイプキャスター対応 値オブジェクトとの親和性 同一性の検証 透過的なシリアライズ/ デシリアライズ 雑にJSON からマッピングしても、型やデフォルト値を上手くマッ ピングしてくれる ちなみにRails5.2 より前に、自分でタイプキャストしてStruct にマ ッピングしているコードが結構ある

Slide 13

Slide 13 text

中間まとめ モデルといってもデータソースは様々 SQS とかKinesis とかKafka とかだったり シリアライズされたものからオブジェクトにマッピングする 複雑な構造のデータは単純にHash に割り当てずに、ちゃんと クラスを定義する タイプキャストをネストすれば、入れ子構造にも対応できる 複雑なモデルはツリー構造になる オブジェクトマッピングの処理とドメインロジックをごっち ゃにしない データソースとのやり取り、値変換、バリデーション、メイ ンロジックが独立してテストできる様に、クラスやメソッド を分割する

Slide 14

Slide 14 text

汎用のマッパーまでやるか? 例えばRedis とかは汎用のオブジェクトマッパーのgem がある。 gem 化するとか、3 つ以上そういうモデルがあるならやってもいい けど、その必要が無いことも多いと思う。 重要なのは抽象化ではなく、責務が独立していてテスト境界が明 確であること。

Slide 15

Slide 15 text

ここまでは主に入力について 続いて出力についての話

Slide 16

Slide 16 text

ActiveRecord でやってはいけないこと 代表格が集計 何故かというと、単純に効率がめちゃくちゃ悪いから。 ActiveRecord の様なオブジェクト作るだけで重いもので大量 にデータを読み込むものではない 基本的にシングルスレッドでしか動かないのでリソースを効 率良く使うのが難しい 出力も正規化とは程遠かったりする つまり、SQL を書くのをサボってはいけないということ

Slide 17

Slide 17 text

だからって集計用のSQL をArel でゴリゴ リ組み立てるとか絶対地獄なので止めま しょう やっていいのは簡単なクエリだけ SQL を部品ごとに再利用しようなんてのは幻想 また、完結したクエリ単位ならある程度再利用できる

Slide 18

Slide 18 text

SQL を書く時に良く使うもの #select #find_by_sql ConnectionAdapter の#execute 類 ERB

Slide 19

Slide 19 text

select をちゃんと使う AR のメソッドだけでもある程度の集計をSQL にやらせて結果を受 け取ることができる。 r e s u l t = U s e r . l e f t _ o u t e r _ j o i n s ( p o s t s : : r a t e s ) . s e l e c t ( " C O U N T ( D I S T I N C T r a t e s . u s e r _ i d ) A S r a t e d _ u s e r s " , " I F ( r a t e s . u s e r _ i d I S N U L L , 0 , M I N ( r a t e s . r e v i e w ) ) A S m i n _ r e v i e w " , " M A X ( r a t e s . r e v i e w ) A S m a x _ r e v i e w " , " A V G ( r a t e s . r e v i e w ) A S a v g _ r e v i e w " , ) . g r o u p ( : i d ) . t a k e ! p r e s u l t . c l a s s # = > U s e r p r e s u l t . r a t e d _ u s e r s # = > レーティングした人の数 ただ、簡易なものに留めておくのが無難。 そして、出来るだけSQL そのものの構造に似せて書いた方が後々 剥がす時に楽。

Slide 20

Slide 20 text

find_by_sql でのINLINE 埋め込み r e s u l t = U s e r . f i n d _ b y _ s q l ( [ < < ~ S Q L , a u t h o r i z e d : 1 ] ) S E L E C T C O U N T ( D I S T I N C T C A S E W H E N r a t e s . a u t h o r i z e d = : a u t h o r i z e d T H E N r a t e s . u s e r _ i d E L S E N U L L E N D ) A S r a t e d _ u s e r s , M I N ( r a t e s . r e v i e w ) A S m i n _ r e v i e w , M A X ( r a t e s . r e v i e w ) A S m a x _ r e v i e w , A V G ( r a t e s . r e v i e w ) A S a v g _ r e v i e w F R O M u s e r s L E F T O U T E R J O I N p o s t s O N u s e r s . i d = p o s t s . u s e r _ i d L E F T O U T E R J O I N r a t e s O N p o s t s . i d = r a t e s . p o s t _ i d S Q L

Slide 21

Slide 21 text

execute with ERB I N S E R T I N T O a g g r e g a t e d _ s e s s i o n s S E L E C T D I S T I N C T i n s i g h t _ i d , u n i t , s t a r t e d _ a t , c u s t o m _ e v e n t _ i d , S U M ( C A S E W H E N p l a t f o r m I N ( ' i o s ' , ' a n d r o i d ' , ' w e b ' ) T H E N , S U M ( C A S E W H E N p l a t f o r m = ' i o s ' T H E N f r e q u e n c y E L S E 0 E N D , S U M ( C A S E W H E N p l a t f o r m = ' a n d r o i d ' T H E N f r e q u e n c y E L S E , S U M ( C A S E W H E N p l a t f o r m = ' w e b ' T H E N f r e q u e n c y E L S E 0 E N D F R O M ` a g g r e g a t e d _ s e s s i o n s _ < % = u n i t % > ` W H E R E d t > = ' < % = f r o m . s t r f t i m e ( " % Y % m % d " ) % > ' A N D d t < ' < % = t o . s t r f t i m e ( " % Y % m % d " ) % > ' A N D f i r s t _ a c c e s s = f a l s e G R O U P B Y i n s i g h t _ i d , u n i t , c o n v e r s i o n _ s t a r t e d _ a t , c u s t o m _ e v e n t _ i d

Slide 22

Slide 22 text

c l a s s Q u e r y R e n d e r e r < O p e n S t r u c t d e f s e l f . r e n d e r ( t e m p l a t e _ f i l e , * * v a r i a b l e s ) n e w ( t e m p l a t e _ f i l e : t e m p l a t e _ f i l e , * * v a r i a b l e s ) . r e n d e r e n d d e f r e n d e r E R B . n e w ( F i l e . r e a d ( t e m p l a t e _ f i l e ) , n i l , " ­ " ) . r e s u l t ( b i n d i n g ) e n d e n d r e s u l t = A c t i v e R e c o r d : : B a s e . c o n n e c t i o n . e x e c u t e ( Q u e r y R e n d e r e r . r e n d e r ( t e m p l a t e _ f i l e , { u n i t : " d a y " , f r o m : 1 . d a y . a g o , t o : T i m e . c u r r e n t } ) , )

Slide 23

Slide 23 text

後者になる程、生SQL として個別に管理 しやすくなる この手の集計・分析処理は高負荷であ り、データ量が増えるとすぐに破綻する 特化したDWH か分散処理基盤が必要にな った時にSQL をベースにしておけば、移 行がしやすい

Slide 24

Slide 24 text

バッチの依存関係 集計に限らず、時間のかかるバッチ処理は複数のジョブによって 構成され、処理同士に依存関係がある場合が多い。

Slide 25

Slide 25 text

ワークフローツールの導入 ツールに必要な要素 処理の依存関係と処理自体の定義を分離する 依存関係が一見して確認できることが望ましい 依存関係の無い処理は並列に実行できる 複数の処理の終了を待ち受けてから処理を開始できる 任意の処理から実行を再開できる オプショナルな要素 実行スケジュールの管理ができる ワーカーとフローのコントローラを分離できる Web UI コンテナ対応

Slide 26

Slide 26 text

最近選択肢が増えてきている ツール/ ソフトウェア rukawa ( 拙作) digdag airflow luiji Jenkins サービス AWS Batch CircleCI Github Actions (NEW)

Slide 27

Slide 27 text

バッチ処理の基本 一つ一つの処理は独立して実行可能な1 目的の処理にする 処理結果が羃等( 繰り返し実行しても同じ結果) になる様にする 複数の処理を組み合わせて結果的に羃等でも良い 途中からリトライできる様にする どこまで実行できたか、簡単に把握できる様にする エラー通知ができる様にする 過去のものを再実行できる様にする

Slide 28

Slide 28 text

弊社の例 rukawa + Amazon Fargate

Slide 29

Slide 29 text

rukawa の活用 https://github.com/joker1007/rukawa Rails アプリから成長してきたので、いくつかのデータやロジ ックをRails のapp/models から参照している Ruby で直接コントロールできて、Rails をそのまま読み込める rukawa を作ったのはそういう理由に依る ヘルパーメソッドを定義して、集計時刻等のパラメーターを 渡し易くしている 大体の処理はテンプレートを元にクエリをレンダリングして Bigquery やHive/Presto 等にリクエストをする

Slide 30

Slide 30 text

c l a s s W o r k f l o w S a m p l e < R u k a w a : : J o b N e t d e f s e l f . d e p e n d e n c i e s { A g g r e g a t e d C l i p s D a y = > [ ] , C o n v e r s i o n F a c t s D a y = > [ A g g r e g a t e d C l i p s D a y ] , E x p o r t C o n v e r s i o n F a c t s D a y = > [ C o n v e r s i o n F a c t s D a y ] , C o p y C o n v e r s i o n F a c t s D a y = > [ E x p o r t C o n v e r s i o n F a c t s D a y ] , I m p o r t C o n v e r s i o n F a c t s D a y = > [ C o p y C o n v e r s i o n F a c t s D a y ] , R e t e n t i o n F a c t s D a y = > [ A g g r e g a t e d C l i p s D a y , C o n v e r s i o n F a c t s D a y ] , E x p o r t R e t e n t i o n F a c t s D a y = > [ R e t e n t i o n F a c t s D a y ] , C o p y R e t e n t i o n F a c t s D a y = > [ E x p o r t R e t e n t i o n F a c t s D a y ] , I m p o r t R e t e n t i o n F a c t s D a y = > [ C o p y R e t e n t i o n F a c t s D a y ] , } e n d e n d

Slide 31

Slide 31 text

Amazon Fargate EC2 インスタンス無しで、コンテナ上で処理を実行できる 必要なタイミングでECS のAPI を叩いて、特定のイメージでコ マンドを実行するジョブをrukawa で定義する コマンド引数や環境変数、S3 等を利用してデータを引き 渡す セキュリティはTask Role でコントロールする クリーンで運用負荷の無いジョブ実行環境が、Fargate の制限 に到達するまではいくつでも並行に用意できる アプリイメージの外にembulk やrclone やhive クライアント等の コマンドラインツールを用意して独立して管理できる

Slide 32

Slide 32 text

wrapbox https://github.com/reproio/wrapbox Ruby からECS のAPI を叩いてコンテナ上でコマンドを実行して、 終了を待ち受けるgem hako oneshot みたいなもの 元々はECS 上でRSpec を実行するために作ったが、Fargate が日本 に来たタイミングでバッチ処理と相性が良さそうだったため、改 修してFargate 対応した rukawa と組み合わせることで、並列度の高いバッチワークフロー 実行環境が低価格で手に入った

Slide 33

Slide 33 text

余談 ( 時間があれば口頭で) fluentd への転送処理の実装上の工夫 cassandra の使い方 presto とRDB のデータの組み合わせ

Slide 34

Slide 34 text

まとめ 昨今のWeb アプリケーションは、RDB に入れて出して済む程に簡 単ではない 多くの複雑な問題に対処するためには、適切な解決方法を常に選 択し続けていく必要がある 実装上の工夫をしたり、採用ツールの枠を広げたり、新しいサー ビスを活用したりして、新しい問題に立ち向かっていく態勢を準 備する必要がある 今日話しをしたのはその一例に過ぎない 大きく成長するアプリケーションを支える開発者は色々と考える ことが多いが、そこが面白いところでもある