RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

DWHからRDSへのデータロード処理を高速化した話

ノバセル株式会社 CoreDevelopmentチームの田村です。 今年の4月に新卒入社しましたが、もう半年経っているのが信じられず時の流れの早さを感じてます。笑

趣味は筋トレで現在マッスル部の部長をしています!💪

今回は、業務中にチームで行ったDWHからRDSへのデータロード処理を高速化した話をします!

データロードの流れ

ノバセルでは競合他社のテレビCM「効果」を可視化するノバセルトレンドというサービスを展開しています。

テレビCM「効果」の分析データをアプリケーション上で表示させるために、DWHからRDSへリバースETLをしています。

この処理は、SnowflakeからS3へアンロードし、その後RDSへロードさせています。

ロードフロー図

ロード処理時間の問題

現状ロードさせるデータ量は10GBほどですが、この処理の中で特にS3 → RDSまでの処理が当初6時間ほど掛かっていました。

さらに、データ量は日々増え続けており、1日あたり2分ほどロード時間が長くなっていました。近い将来、開発のボトルネックになることが予想されることから高速化する必要がありました。

予想された主な原因

Snowflake から S3 にアンロードされた各ファイルのサイズを見ると16MBほどとなっていました。加えて、多数のファイルができていることを確認しました。

これを踏まえ、サイズの小さいファイルが多数あることでデータ転送時のIO処理に無駄があるのではないかと考えました。

この仮説に関して、ファイルサイズが効率性に関係していることはAWSのこちらのブログにも記載されています。

ファイルサイズが非常に小さい場合、特に 128MB 未満の場合には、実行エンジンは S3ファイルのオープン、ディレクトリのリスト表示、オブジェクトメタデータの取得、データ転送のセットアップ、ファイルヘッダーの読み込み、圧縮ディレクトリの読み込み、といった処理に余分な時間がかかります

アンロードされるファイルのサイズを上げ、ファイル数をへらす

仮説をもとに『DWHからS3へアンロードする際のチャンクサイズを上げ、ファイル数を下げる』ということを念頭に高速化を行いました。

以下は具体的にSnowflakeで行った内容になります。

COPY INTOのCopyOptionsのMAX_FILE_SIZEを上げる

SnowflakeからS3へアンロードする際にCOPY INTOを行いますが、その際のcopyOptionの1つであるMAX_FILE_SIZEをデフォルト値の16MBから最大の5GBにしました。(参考: SnowflakeDocs CopyOptions)

実行するウェアハウス(以後WH)設定を変更する

加えて、実行するWHの設定を WAREHOUSE_SIZE='SMALL'(デフォルト値) から WAREHOUSE_SIZE='XSMALL'へ変更させました。

SnowflakeDocs のCopyOptions部分を読むと、

スレッドごとに並列に生成される各ファイルの上限サイズ(バイト単位)を指定する数値(> 0)。実際のファイルサイズとアンロードされるファイルの数は、並列処理に使用可能なデータの合計量とノードの数によって決定されることに注意してください。

と記載されている通り、単純にMAX_FILE_SIZEを下げるだけではファイルサイズをあげられず、結果としてファイル数を下げられない可能性があります。

こちらの解決方法としてはSNOWFLAKE FORUMSで言及されています。

using a smaller WH helps us with this goal. Using an XS WH will result in the fewest files, and if you reduce the MAX_CONCURRENCY_LEVEL that should result in even fewer files.

より小さいWHを使用することで、この目標を達成することができます。XS WHを使用すると、最も少ないファイル数になり、MAX_CONCURRENCY_LEVELを下げれば、さらに少ないファイル数になるはずです。

こちらをもとにさまざまなパラメータで検証し、WHサイズを下げてもShowflakeからS3へアンロードする時間がある程度許される設定値を調べました。

我々の環境では、 WAREHOUSE_SIZE='XSMALL' がパフォーマンスを維持しつつチャンクサイズを上げファイル数を下げるために良い設定値となりました。

(他にも MAX_CONCURRENCY_LEVEL を下げる方法も検証しました。しかしSnowflakeからS3へアンロードする処理時間が許容時間を超えてしまったため、デフォルトのままにしました。)

そのほか高速化のために行ったこと

RDSへのロード時にデータロード後にINDEXを追加する

RDSへのロード時の処理も見直しました。

当初のロード処理では、インデックスを貼った状態のテーブルにデータをInsertしていました。

しかし調査したところ、開発者のためのSQLチューニングへのガイドにも記載されているとおり、データInsert前よりデータInsert後にINDEXを追加したほうが、一般的にパフォーマンスが良くなるということがわかりました。

こちらに基づき、高速化のために、データInsert後にINDEXを追加するように変更を行いました。

結果

上記のことを中心に行い、6時間掛かっていた処理を1時間に収めることができました!

尚、データロード全体にかかる総時間と、サンプルとしてある1つのテーブルをアンロードした際のファイル数とファイルサイズを以下の表にまとめました。

改修前後の比較

ファイルサイズの変更とindex前貼りがそれぞれどれくらい寄与したかは計測していないため、時間がある際に計測したいと考えています!

【社内ハッカソンで】HACK WEEK 2022 クリエイティブ目検比較ツール「クラベル」【クリエイティブ賞ゲット】

こんにちは!ノバセル株式会社*1でソフトウェアエンジニアをしています mktakuya です。
最近はサバゲーとEscape from Tarkovというゲームにハマっています。

今年もHACK WEEKが開催されました!
HACK WEEKはラクスル社内で行われているハッカソンで、毎年1週間程度業務を止めて(!)開催されます。

5回目の開催となるHACK WEEK 2022では、「Inventing the Next Value」をテーマに、エンジニア視点で顧客への価値提供をするべく各チームがプロダクト開発を行っていました。

詳しくは採用サイトの記事をどうぞ。 recruit.raksul.com

クリエイティブ目検比較ツール「クラベル」

私の所属していたチームでは、「クラベル」というプロダクトを作りました。
クラベルは、複数のクリエイティブ*2を同時に比べることが出来るツールです。

こんな感じで広告主を選択すると……

その広告主のクリエイティブが一覧で表示されます。 この画面では、同時にクリエイティブを再生することが出来ます。
クリエイティブごとで非表示にしたり、ミュートを切り替えたりすることも。

クラベル 動画再生画面のサンプル

きっかけ

クラベルの元ネタは、個人的な技術勉強ついでに作ったツールでした。

ノバセルでは、運用型テレビCMの企画から制作、放映、その後の分析までのワンストップサービスを提供しています。*3
運用型テレビCMとは、ただテレビCMを放映するだけでなく、放映期間中により効果の高いクリエイティブや放映プランに差し替えることによって、より高いマーケティング効果を目指すものです。

クリエイティブの制作も担うノバセルでは、日々クリエイティブの研究を行っています。
その業務の一環として、社内のクリエイティブチームの方々が、「今日のNA」というタイトルでお客様のクリエイティブの効果の良し悪しとその理由について共有されていました。

「今日のNA」のSlack投稿の一例(クリエイティブは黒塗りにしてあります)

Slackにクリエイティブを動画ファイルとして添付して頂いていたのですが、複数のクリエイティブを同時に見比べながら再生するのが大変、という課題がありました。
そこで、「複数のクリエイティブを同時に比べることが出来たら便利なのではないか?」という仮説のもと、かんたんなプロトタイプを制作しました。

プロトタイプ版のクラベル

利用技術は、当時僕が勉強していたVite + Vue 3です。*4
仮説を検証したいだけだったので、実装をかんたんにするため動画はYouTubeのものを利用しました。

こちらを社内に展開してみたところ、クリエイティブチームの方々からかなり良い反応を頂けたので、HACK WEEKのテーマとして本実装することにしました。

HACK WEEKで磨き込み

HACK WEEKに向けて編成されたチームは、私含めエンジニア2名とデザイナ1名の3人チームでした。

HACK WEEK中は、Figmaで作って頂いたデザインを元に実装を進めました。
私がサクッと作ったプロトタイプが、こんなにキレイなものになるとは……。デザイナさんのパワーに驚きです。

プロトタイプ版ではYouTube上の動画を同時再生するのみでしたが、ノバセル社内のデータを利用出来るように。
また、プロトタイプ版を作ったときに頂いたフィードバックも反映しました。

技術スタックとしては、TypeScript とExpress.jsそしてNext.jsを採用しました。
ノバセル社内でこれからはReact / Next.jsに寄せていこうという意思決定があったので、その検証も兼ねていました。

3日間のHACK WEEKを乗り越え、完成したのが「クラベル」です。
最終日の成果発表会でも高い評価を頂き、ユーザーにとって利便性・エンゲージメント・体験が最も優れたプロジェクトに贈られる賞である「クリエイティブ賞」を受賞することが出来ました。

当日使用したスライドの一部

クリエイティブ賞の受賞理由を解説してくださった和泉さん

おわりに

今回私達は、日々の業務の中の気付きを元にプロトタイプを作って価値を検証したあとに本実装をしていくというプロダクト開発手法を実践しました。
実はこの手法は、ラクスルの日常業務でも実践されています。

BtoBのプロダクト開発における私の哲学は「圧倒的に高い事業解像度で、分解。まずはヒトが属人的にやってみる。それをテクノロジーによって数百倍以上の効率と精度にする」です。ラクスルで学んだこと。

ラクスルの成長の要因は、5年で50億以上をかけたテレビCMの効果を可視化し、デジタルマーケのように運用してきた仕組みを構築できたことです。我々はテレビCMの効果をほぼ可視化できていました。但し、EXCELやタブローを駆使し、数十時間をかけて・・・。これ一瞬でできたら民主化じゃないですか、と。100MBのExcelをグリグリ回す自分にとって、そこからブレイクスルーがはじまります。営業組織に、PM、エンジニア、デザイナーを1人ずつ巻き込んでいって・・・

そこから数か月・・・私が営業ばかりしている間に、気がついたら魔法使い達によって完成していました。

ラクスル・ハコベルに続く第三の事業は「ノバセル」。事業に込めた想いを語る5,382文字の初note。|tabemasaki1208|note

上記で引用したように、私の所属するノバセルも「テレビCMの運用」の価値を人の手で検証した後に、テクノロジーの力で一気にスケールするという中で生まれた事業・会社です。

今回HACK WEEKを通じて、日々の業務から課題発見→仮説立て→プロトタイプで価値検証→作り込み、という一連の流れを体感することが出来たのはとても楽しかったですし、こんな開発の進め方を出来たのもノバセルで日々仕事をしているからこそだと思います。

というわけでお決まりの流れですが、記事末尾に採用情報のご案内があります。 特にノバセルはテレビCMという複雑な事業ドメインを扱っており、「この人の仕事を徹底的に理解してシステム化すれば顧客価値になる!」という業務がたくさんあって楽しいですよ。

*1:2022年2月からラクスルのノバセル事業部はノバセル株式会社になりました

*2:広告業界においては、広告の素材のことを指します。テレビCMにおいては、15秒や30秒のCM動画そのもののことです。

*3:運用型テレビCMなら【ノバセル】 | ラクスルのテレビCMサービス

*4:勉強のログはこちら https://blog.m6a.jp/entry/2022/06/19/211035

Ruby初心者による、Ruby初心者のための RubyKaigi2022 参加記

ラクスルでは、ほとんどのチームがRubyおよびRailsを用いて日々開発を行っています。 今回、ラクスルは6度目のRubyKaigiスポンサーとして、「RubyKaigi2022」に協賛させていただきました。 この記事では新卒エンジニアでRuby歴1年の私が、現地参加で感じた「Ruby初心者がRubyKaigiに参加する価値」についてレポートします。

はじめに

今年ラクスルに入社した、バックエンドエンジニアの平島です。 RubyやRailsを触り始めたのはちょうど1年程前で、現在は「目の前の開発やレビューは順調にこなせるようになってきたばかりの状態」にあると思っています。

そんな自分が今回RubyKaigiに参加するにあたって、「RubyKaigiは他の技術カンファレンスに比べて難しい話が多い」という話を聞いていました。 また実際に会場内の企業ブースにあったアンケートなどを見ても、マネージャークラスの方や、Ruby歴の長い方が多く参加されていました。 そのため、初心者が参加してどのようなメリットを得ることができるかイメージできていませんでした。 しかし実際に参加してみると、初心者にとっても多くの参加メリットがあることに気が付きました。 この記事では、私の感じた「Ruby初心者がRubyKaigiに参加する価値」について書き、Ruby初心者や処理系に詳しくない方も、もっとRubyKaigiに参加することをオススメできればと思います。

RubyKaigi2022 看板前での集合写真

技術に対する肌感覚を得られる

まずはなんといっても、やはり技術的な知識を得ることができるという価値が大きいと思います。 ですが本稿では、書籍などからは学ぶことが難しい、カンファレンス現地参加ならではのメリットについてお伝えしたいと思います。

たとえば、Rubyでは3.0から型解析が使えるように*1なっており、私も技術的な点については予習を行ってRubyKaigiに挑んでいました。 しかし実際のところ、Rubyの型解析は現在どれほど一般的にそれが使われているのか疑問に感じていました。 また普段の業務の中でも、書いているコードや使っているgemがどれほど一般的なのか、疑問に感じることが多々ありました。

Day1に行われたあるセッションの中で、「普段のRubyプロジェクトでは型付けしていますか?」といったアンケートが取られました。 結果は、会場の1割ほどの方が挙手しており、その少なさを実際に確認することができました。 またその後のセッションや企業ブース、Twitter上の反応などからも、型解析の利用度の現状やその原因、期待感などの肌感覚を得ることができました。 型解析の他にも、さまざまな技術についてその課題や利点を多くのセッションから学ぶことができました。

このように、多くの初心者が、新しく触れる技術が現在どれほどメジャーなものなのか判断するのが難しいという課題を持っているかと思います。 また、技術に対してメリットやデメリットを挙げたり、他の技術と比較したりするためには、より多くの経験が必要になります。 そういった技術に対する肌感覚を得られるRubyKaigiは、自分のような初学者を脱出したくらいのエンジニアにとって、次の成長のに繋げられるとてもいい機会だと思います。

エンジニアとして視座を高められる

次にご紹介したい価値は、「技術力以外」の面でのメリットです。 私のような新卒エンジニアは、目の前のプログラムに集中しすぎるあまり、視野が狭くなってしまうことが多々あると思います。 たとえば、動作に集中して保守性の低いコードを書いてしまったり、ひとつのissueに時間をかけすぎてしまったりすることがあります。 このような視野の狭さという課題を抱えている初心者の方は多いと思います。

RubyKaigiでは、Rubyを用いた開発がより良いものにするため、さまざまな視点からのトークがありました。 具体的には、処理の高速化や大規模データへの対応のような技術的な視点、開発やデバッグをより快適に行うための工夫などです。 またDay2のMatzさんによるKeynoteでは、Rubyには書き心地の良さなどのさまざまな価値があり、企業の利益にもたくさん貢献できているという話もありました。 その中で紹介されていた「Explore Top Ruby Companies Around the World」(Rubyを使っている企業の時価総額ランキング)ではラクスルが国内4位となっており、改めて我々エンジニアは価値を届けるために働いていることを再認識する事ができました。

「Explore Top Ruby Companies Around the World」 でラクスルは世界31位、国内4位です!
ラクスルの印刷事業では、Ruby on Railsが価値創造の中核を担っています。

このように、RubyKaigiでは技術の話だけではなく、エンジニアとして持っておくべき視点についても色々なことを学ぶことができます。 新卒エンジニアでは意識する機会が少ない目線についても意識することができる、とても貴重な機会でした。 また、それらの抽象的な価値を高めるための具体的な技術も知ることができ、普段の業務と地続きで抽象的な価値について考えることができるようになりました。

さらなる成長目標を得ることができる

3つめのメリットは、持続的な成長効果につながるということです。 エンジニア初学者は最初、目の前の開発をできるようになるという明確な学習目標があるかと思います。 しかしそれを達成した後、自分は次の学習目標を設定する難しさに当たりました。 そういった、次の学習目標の設定に悩んでいるエンジニアにも、RubyKaigiへの参加をオススメします。

自分は今回たくさんのセッションから上記のような価値を得ることができた一方で、自分にとっては高度な内容でほとんど理解できないセッションも多くあり、悔しい思いをしました。 そういった経験から、「次回のRubyKaigi2023までにより多くのセッションを理解できるようになる」という目標を建てています。 これは期限も決まっていますし、公開されている過去の発表スライドを対象として具体的な目標にすることもできるので、なかなかいい目標設定ではないかと考えています。 毎年のRubyKaigiの参加・理解を目標とすることは、なかなか継続の難しい「ある程度習得済みの言語について理解を深める」という課題に対するはピッタリの対策だと思います。

また私は、RubyKaigi以外の技術カンファレンスについても参加の意欲が高まったという、副次的な効果もありました。 たとえば、社内ではPHPのサービスにも触れる機会があることから、今月開催されたPHP Conference Japanも参加しました。 また、来月10月にはKaigi on RailsVue Fes*2にも参加を予定しています。 このように、たくさんある技術カンファレンスという成長機会に参加する第一歩としても、RubyKaigiへの参加をオススメしたいです。

おわりに

RubyKaigiは学べることがたくさんあることもさながら、イベントとしてもとても楽しめるカンファレンスです。 Ruby初心者の方も、難しそうな内容に気負いすることなく、是非気軽に参加してみることをオススメします。 参加すれば、必ず成長できますし、次にアクションしたいことも見つかるはずです。

この記事を通して、Ruby初学者の方のRubyKaigiへの参加に繋がり、成功を後押しできれば幸いです。 そして、日々お世話になっているRubyに、コミュニティ活性化の面から少しでも貢献できれば幸いです。 それでは来年5月、「RubyKaigi2023」でみなさんにお会いできることを楽しみにしています!!

*1:正確には、Ruby2.6以降であればgemをinstallすることで型解析を利用できます。Ruby3.0から、Ruby本体にrbsなどのgemがバンドルされるようになりました。

*2:ラクスルは今年も、Vue Fesのスポンサーもしています!

マイクロサービスにSagaパターンを用いて検証を行った

はじめに

Hackweek という毎年開催される社内ハッカソンが8/24~26の3日間で行われました。

社内ハッカソンでは、ビジネス職、エンジニア問わず案を持ってきて案に興味がある人で取り組むというようなイベントです。

その中で「 Saga パターンを用いたマイクロサービスの検証」というアイデアがあり、そのアイデアに興味がある5名で今回の検証を行いました。その中で得た知見や所感をこの記事では書いています。

なぜやろうとしたのか

印刷ECであるラクスルは、raksul.com の他に dm.raksul.comnovelty.raksul.com など他にも色々なサービスが展開されています。これらのサービスは各サービスごとにDBを持っており、一部 raksul.com に依存している機能はありますがそれぞれが独立したサービスとなっています。いわゆるマイクロサービスのようなアーキテクチャを取っています。

このようなアーキテクチャを取っていて課題に挙がることの1つとして複数サービスにまたがる処理のトランザクション管理がありました。1つのサービスであれば、同じ DB で処理を行っているため DB のトランザクションを使えばデータの整合性を保つことが出来ます。

Rails であれば以下のように1つのトランザクションで行いたい処理をブロックで囲むだけで実装できます。

ApplicationRecord.transaction do
  Order.create!(...) # 注文情報のレコード作成
  Payment.ceate!(...) # 支払情報のレコード作成
  SupplyOrder.create!(...) # 印刷発注用のレコード作成
end

このように実装することでどれか1つでも処理が失敗すると DB のロールバックが走りデータの不整合が起きないように出来ます。

しかし複数サービスにまたがるとそれぞれのサービスが DB を持っているため DB のトランザクションを使うことが出来ません。

Order テーブルを自身のサービスで持っていて、 Payment と SupplyOrder を外部のサービスに対して CreateAPI を叩こうとするときの擬似コードです。

Order.create(...) # 注文情報のレコード作成
PaymentApi.create(...) # 支払情報を作成するためのAPIコール
SupplyApi.create(...) # 印刷発注を行うためのAPIコール

DB のトランザクションを使うことができないと、複数の DB をまとめて更新するようなアクションを行ったときに処理の途中で失敗してしまった場合データの不整合が発生してしまいます。DB のトランザクションが使えないため簡単にロールバックができません。データの不整合を直すために起こり得るケースを網羅してロールバック処理を行う方法があると思いますが DB のロールバックほど簡単には実装できず複雑な実装になってしまいます。

どのようなアーキテクチャがあるか

複数サービスをまたぐトランザクション管理を行うアーキテクチャとしてはいくつかあります。

今回実装する前に検討したアーキテクチャとしては TCCパターン と Sagaパターン です。

どちらのアーキテクチャも更新直後にデータの整合性を担保するものではありません。 更新から一定時間経過後にはデータの整合性を担保するという結果整合性の考え方を反映させたアーキテクチャになっています。

TCCパターン

Try / Confirm / Cancel の3つのアクションによって整合性を取るアーキテクチャです。

以下の流れで整合性を取ります。

  • 最初に各サービスに対して Try を行い、与信枠や在庫などサービスが持つリソースを予約する
  • すべてのサービスに対して予約が成功した場合
    • 各サービスに対してConfirmを行い、リソースを正式に確保する
  • いずれかのサービスで予約に失敗した場合
    • 予約をしたサービスに対してCancelを行う

成功した場合のシーケンス図

Supply ServiceへのTryが失敗する場合のシーケンス図

Sagaパターン

補償トランザクションを使って整合性を取るアーキテクチャです。

補償トランザクションとは処理をすでに行ったサービスに対して処理を取り消す処理です。Sagaパターンの中にもコレオグラフィとオーケストレーションという2つの方法が存在します。

  • コレオグラフィ

各サービスが受け取ったイベントを理解しそのイベントに応じて処理を行います。イベントの全体の流れはどのサービスも把握しておらず、各サービスは受け取ったイベントに対してどのような処理を行うことだけしか知らずその先のサービスでどういったことが起きるかは関知しません。

  • オーケストレーション

イベント全体の流れを把握しているオーケストレーターというものが存在します。オーケストレーターはイベントを受け取ったら次のイベント行うために次のサービスを呼び出すための処理を行います。

アーキテクチャの選定

今回はSagaパターンのオーケストレーションを用いて検証を行いました。当初はTCCパターンでやってみようという話だったのですが、実装を行う前に色々調査を進めていたところTCCパターンは考慮しだすとかなり大変そうだということが分かりました。

TCCパターンでは失敗した時にCancelを行う必要があるのですが、このCancelのリクエストが何らかの理由で失敗してしまうということを考慮しないといけません。他にも通信エラーを考慮すべきケースがいくつかあり、こういったことからTCCパターンを完全に実装しきるのは難しいだろうという判断になりました。

そこでSagaパターンを使おうということになったのですが、コレオグラフィはイベントドリブンアーキテクチャへのパラダイムシフトが必要になり既存システムへの導入の難易度が高そうだという判断を行いました。消去法ではありますが、一番容易だと判断したSagaパターンのオーケストレーションを検証で用いようという判断になりました。

今回実装したサンプル

今回架空のECサイトを構築し検証を行いました。「このECサイトではシールとタオルを販売しており、タオルを買うユーザは一緒にシールも買うことができる。」というユースケースを設定し検証を行いました。

今回実装したECサイトの全体を表したのが以下の図です。

このECサイトには注文を受け付ける POST /orders のエンドポイントがあり、そのエンドポイントを担っているのがOrder Serviceです。 そこから決済情報を担うPayment Service、タオルの発注を担うNovelty Printing Service、シールの発注を担うPrinting Serviceのそれぞれにリクエスト行いユーザが注文できるかどうかを判断します。

また今回の検証では3日間という時間の制約があったこともあり、認証の機能は挟まずログイン済みのものとして各サービスがやりとりを行っています。

APIで実装した場合

まず Bad Pattern として API で実装した場合について説明します。

モデル図

注文は Order と OrderItem の2つのテーブルで構成しています。

Order は注文自体を表し、OrderItemは注文に含まれる商品を表しています。例えば、1回の注文でタオルやシールをそれぞれ買うと Order に対して 2つのOrderItems(タオル・シール)が紐付きます。

シーケンス図

上記のサンプルECサイトを単純にAPIコールした場合の正常系のシーケンス図です。

注文に関する流れは以下の通りです。

  1. ユーザが注文を行うときに Order Service に対してリクエストを行う
  2. Order Service は自身のDBに注文情報を作成する
  3. Order Service は Printing Service に OrderItem(シール) をパラメータとしてリクエストを行う
  4. 3が成功したのを確認すると Order Service は Novelty Printing Service に OrderItem(タオル) をパラメータとしてリクエストを行う
  5. 4が成功したのを確認すると Order Service は Payment Service に決済を行うためリクエストを行う
  6. 5が成功したことを確認すると Order Service はユーザに対して注文が作成されたことを伝える

このように正常系をみるとシンプルに見えますが、実際には各サービス間の通信はネットワークエラーなどエラーが発生することを考慮しなければなりません。 もし Payment Service でエラーが発生してしまうと、Novelty Printing Service、Printing Service をそれぞれの注文をキャンセルしなければなりません。異常系を考えるとかなり複雑になることが分かります。

Sagaパターン

前述の架空のECサイトをSagaパターンで実装するための設計について述べます。

モデル図

Sagaパターンでは、Order、OrderItemテーブルに加え注文ごとのSagaの状態を管理するSagaテーブルが必要になります。今回のユースケースでは注文の作成だけであるためCreateOrderSagaテーブルを追加します。

ステートマシン

今回実装する Sagaパターン はオーケストレーションによる方法を用いています。 現在のステートとイベントの発火によって次のステートが決まるため、実装する際にはステートマシンを使うのが良いだろうという判断をしました。

以下の図は今回発生するイベント、アクションをステートマシンとして表現した図です。

右側の白い四角で囲っている部分が正常系のステートです。左側の白い四角で囲っている部分が異常系のステートです。 initialized は初期状態を表しており、 order_approved のステートが注文完了を表し order_rejected は注文がキャンセルされたことを表しています。

正常系の場合のイベントとステートの遷移について以下の流れなります。

  1. 初期状態から start イベントが発火すると、タオルとシールの両方の商品が発注可能かを訪ねているステートである creating_supply_orders のステートに遷移する
  2. タオルとシールが発注可能状態になると create_payment イベントが発火され creating_payment のステートに遷移する
  3. 決済に問題がなければ、approve_supply_orders イベントが発火され approving_supply_orders のステートに遷移する
  4. 各商品の承認がすべて完了すると approve_order イベントが発火され approving_order のステートに遷移する
  5. 注文の承認が完了すると、success イベントが発火され order_approved のステートに遷移する

異常系へ遷移する場合は reject_supply_orders イベントを発火し、 rejecting_supply_orders ステートに遷移します。その後、 rejecting_order、order_rejected とステートが遷移し注文キャンセルとなります。

Sagaパターンの設計

Sagaパターンを実装するにあたり、トランザクションと補償トランザクションを設計する必要があります。今回は以下のテーブルのように設計を行いました。

step service Transaction Compensation Transaction
1 order service createOrder() rejectOrder()
2 printing service createSupplyOrder() rejectSupplyOrder()
3 novelty printing service createSupplyOrder() rejectSupplyOrder()
4 payment service createPayment()
5 novelty printing service approveSupplyOrder()
6 printing service approveSupplyOrder()
7 order service approveOrder()

上記で設計したステップを図で示したものが以下の図です。

メッセージのやりとりを行って各サービスと連携する必要があるためメッセージブローカーである Kafka を用いています。 メッセージのやり取り行うトピックは4つあります。各サービスからのreplyを受け取る create_order_saga_reply_topic があります。 その他の printing_service_req_topic、 novelty_printing_service_req_topic、payment_service_req_topic の3つは各サービスへのリクエストを行うトピックとなっています。 今回はこれら3つのトピックを通して発注を行う(createSupplyOrder)、決済を行う(createPayment)などのメッセージを送信しています。

上記の図で表したものを実際に開発しました。開発を行う中でつまずいたことや実際の運用を行う際、課題になりそうなところなど感じたことを次の章で振り返りとしてまとめています。

振り返り

実際にサンプルECでオーケストレーションを用いたSagaパターンを実装して、感じたことをチームで振り返って出てきた内容について一部抜粋して書きます。

  • オーケストレーターが難しかった、オーケストレータのコードを追うのが少し辛かった

オーケストレーターでは注文に関する様々なメッセージを受け取るため、自身の関心のある全てのメッセージに対してアクションを書く必要がありました。事前に状態遷移図を書いたりすることで整理を行ってから取り組んだのは良かったと思います。ちゃんとリファクタリングを行わないとコードが複雑になってしまい追うのが大変になってしまいそうだという感想でした。

  • デバッグの難しさ

各サービスがメッセージのやりとりを行うため失敗したときに気づけるような仕組みやメッセージが一連の流れで追えるような仕組みが必要そうだという話をしました。

他にも以下のようなことがチーム内では上がっていました。

  • Sagaパターンで動いていない既存システムに対してどうやって導入するか考える必要がある
  • 実運用では複数のサービスで実装するには規約の整備が必要そう。専任のチームが必要かも
  • ローカル環境での結合テストの難しさ

おわりに

本番で運用するために検証すべきことはさすがに3日間では終わりませんでしたが、実際に手を動かしてみると本や知識だけでは分からない色々な課題が見えてきました。

今回の検証しきれなかったこととして、同一メッセージを何度処理してもデータの不整合が起きないようにする冪等性の担保や一定時間経過してもイベントが進まない場合にどういった処理を行うのか、他にもリトライやデッドロック、メッセージの処理順序による整合性が挙げられます。 実運用で今回のパターンを実装するにはまだまだ時間をかける必要がありそうです。

Cutting Costs with AWS Lambda for Highly Scalable Image Processing

This blog is based on a POC and cost analysis that was done for a project for Raksul Hackweek 2022. https://recruit.raksul.com/story/hack-week-2022/ There are three of us who took part in this initiative. We are Nguyen Luong Hoang, Anh Tran Tuan and Afshad Eddy Dinshaw.

Summary:

Currently, some of the departments at Raksul are using ECS in AWS cloud to host image processing services such as background removal and resizing. The existing ECS setup in the cloud is resulting in higher costs even though the services are not in constant use. We did a cost analysis of our existing infrastructure in ECS versus potentially moving to AWS Lambda. We discovered that the move would significantly reduce infrastructure cost while improving performance. We did a POC as well as part of the Hackweek challenge to demonstrate hosting a simple image resizing service in AWS Lambda.

1. Goal: Reduce infrastructure costs associated with AWS ECS:

  • ECS requires a pre-estimated fixed number of containers be available even if they are not being used.

  • AWS Lambda offer:

    • pay as you go
    • highly scalable
    • performance improvement.

All the above lead to reduced costs.

2. Cost Analysis and Evaluation:

Following is a breakdown of the costs associated with the existing infrastructure vs moving to AWS Lambda. We have 1 cluster with 30 tasks

Total cost: 1674.59 USD per month

Application in ECS

Estimated Cost Breakdown - AWS Lambda

AWS Lambda will serve based on number of requests. We estimate that one of our apps could have 700 submissions per day. Each submission can required up to 4 jobs/requests to generate images. That amounts to 3000 requests/day. So in total we could have around 8000 requests per day required to generate/convert images at current. Each request takes ~ 30seconds

So it costs 489.22 USD per month for all products in all environments using AWS Lambda.

Cost Effective - Amazon Lambda

To consider: If number of requests increasing, it will required more cost for amazon lambda (base on above calculation)

For ECS, the cost will not change. But if number of concurrency requests increasing, we may need to add more tasks/containers to the cluster. At this time, the cost of ECS will increase.

3. Performance Upgrade with increased scalability - AWS Lambda

We have seen a significant increase in performance switching to AWS Lambda. The throughput has increased due to the scalability of AWS Lambda.

Current ECS Fargate system → 90 concurrent requests. (30 containers * 3 jobs)

AWS Lambda → 1000 concurrent requests.

4. POC - Image Resize Workflow - AWS Lambda

How it works: We use ImageMagick for image processing. AWS Lambda Now Supports Up to 10 GB Ephemeral Storage. So it's enough space for ImageMagick works even with AI or PDF files. Thanks to the open source project https://github.com/serverlesspub/imagemagick-aws-lambda-2 . It's really helpful and we have the base to build, custom for the lambda layer, especially in the short duration of the HackWeek.

The lambda function: We created a simple lambda function that accepts two params: the object key (s3 key of the input image) and maxDimension (size that image want to convert to). After converting the image, the lambda will put the final image to the output bucket and publish an SNS notification.

var AWS = require("aws-sdk");

const s3Util = require('./s3-util'),
    childProcessPromise = require('./child-process-promise'),
    queryString = require('querystring'),
    path = require('path'),
    os = require('os'),
    EXTENSION = process.env.EXTENSION,
    OUTPUT_BUCKET = process.env.OUTPUT_BUCKET,
    INPUT_BUCKET = process.env.INPUT_BUCKET,

exports.handler = function (eventObject, context) {
    const key = eventObject.key,
        maxDimension = eventObject.maxDimension,
        inputBucket = INPUT_BUCKET,
        id = context.awsRequestId,
        resultKey = key.replace(/\.[^.]+$/, EXTENSION),
        workdir = os.tmpdir(),
        inputFile = path.join(workdir,  id  + path.extname(key)),
        outputFile = path.join(workdir, 'converted-' + id + EXTENSION);

    var sns = new AWS.SNS();
    var params = {
        Message: resultKey, 
        Subject: "image-resize-done",
        TopicArn: "arn:aws:sns:ap-northeast-1:************:image-resize-done"
    };
    
    console.log('converting', `${INPUT_BUCKET}`, key, 'using', inputFile);
    return s3Util.downloadFileFromS3(inputBucket, key, inputFile)
        .then(() => childProcessPromise.spawn(
            '/opt/bin/convert',
            [inputFile, '-resize', `${maxDimension}x${maxDimension}>`, outputFile],
            {env: process.env, cwd: workdir}
        ))
        .then(() => s3Util.uploadFileToS3(OUTPUT_BUCKET, resultKey, outputFile, MIME_TYPE))
        .then(() => {    
                      sns.publish(params, context.done);
                      return resultKey});


};

Invoking the lambda function from client code:

  RESIZE_FUNCTION = 'imagemagick-image-generator-la-ResizeImageFunction-******'

  def resize_image_function
    lambda_client.invoke(function_name: RESIZE_FUNCTION, payload: payload)
  end

  def payload
    JSON.generate(key: key, maxDimension: max_dimension)
  end

  def lambda_client
    @lambda_client ||= Aws::Lambda::Client.new(credentials: credentials)
  end

  def credentials
    @credentials ||= Aws::Credentials.new('*****', '*****')
  end

Post processing: Send an SNS notification and we can retrieve the image base on the key from the output bucket to continue our processing.

Conclusion

During the HackWeek, we created a POC to integrate the simple lambda above with our application for image resizing. TODOs: Migration/Apply for all kinds of image processing in our system.

We successfully demonstrated that AWS lambda works well for image processing. It not only helps to save cost but also provides better scalability for the project. Our cost analysis has shown that there is a significant reduction in cost when moving from ECS to Lambda.

References

https://github.com/serverlesspub/imagemagick-aws-lambda-2

https://aws.amazon.com/blogs/apn/cutting-costs-with-aws-lambda-for-highly-scalable-image-processing

https://aws.amazon.com/blogs/networking-and-content-delivery/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/