Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scala と AWS でフルサーバーレス開発事例 / How Chatworks uses Scala and Serverless

Scala と AWS でフルサーバーレス開発事例 / How Chatworks uses Scala and Serverless

TATSUNO Yasuhiro

March 12, 2021
Tweet

More Decks by TATSUNO Yasuhiro

Other Decks in Programming

Transcript

  1. 2021-03-12 #nakanoshima_dev 14: JVM Langs Night Talk Chatwork株式会社 プロダクト本部 サーバーサイド開発部(Scala)

    立野 靖博 Scala と AWS で
 フルサーバーレスな
 プロダクト開発事例

  2. • 立野 靖博 • Twitter & GitHub: @exoego • サーバーレス愛好4年目

    ◦ のコミッター • 近況 ◦ 長野県の築140年の古民家に引越しました 自己紹介 2/39
  3. Scala とは • 来歴:2003年、Java ジェネリクスの設計にもたずさわった Martin Odersky 博士らが OSS で発表。2009年に

    Twitter が採用して話題に。2021年、最新の Scala 3 リリース迫る!! • 主な開発団体: ◦ ScalaCenter Odersky 先生ら大学中心の NPO ◦ Lightbend Scala 初期開発者らが立ち上げたITベンダ • 有名なユーザー企業:Twitter、Disney、Spotify など • 影響を受けた言語:Java、Haskell、OCaml、Erlang など • 影響を与えた言語:Java、Kotlin、F# など • 実行環境:JVM、JS(ブラウザ、Node.js)、ネイティブ 3/39
  4. Chatwork のリンクプレビューはこんな機能です 8/39 ①チャットに URL が含 まれていると ②タイトル、画像、ファビコ ンを自動で表示 投稿した人

    見る人 共有したいページのタイトルや 内容を入力する手間が減る 開く前にどんなページか分かり開 くかどうか判断しやすくなる
  5. プレビューする情報は HTML から取得しています • URL をフェッチし、その HTML に埋め込まれた OGP 規格

    のメタデータを取得しています • OGP は 2010年に Facebook が公開 https://ogp.me/ • 類似の規格に Twitter Card などがあります(現時点では 未対応) 9/39 <meta property="og:type" content="website" /> <meta property="og:site_name" content="connpass" /> <meta property="og:title" content="[Online]nakanoshima.dev#14 JVM Langs Night Talk" /> <meta property="og:url" content="https://nakanoshima-dev.connpass.com/event/204733/" /> <meta property="og:image" content="https://connpass-tokyo.s3.amazonaws.com/thumbs/(略).png" /> <meta property="og:description" content="# 開催日時 2021/3/12(金)...(略)" /> Open Graph Protocol
  6. 日常的に見る「あたり前」機能、作る側にはけっこう悩ましい • 何をプレビューするか ◦ どんなサイトをどんな風にプレビューできるとユーザーの業務が捗る? ◦ OGP 以外の規格は? 特にメタデータないけど有用なサイトは? ◦ 1メッセージに複数

    URL が含まれていたら、どこまでプレビューする? • 性能特性どうするか ◦ Web サイトが遅くてプレビュー取得に時間がかかる場合、チャットの表示は どれくらい遅れてもよい? ◦ 膨大なユーザーがバンバン URL を共有しても安定してさばけるか? • アクセス制御どうするか ◦ Web サイトに迷惑かけないアクセス方法は? ◦ Web サイトに robots.txt があったら? ◦ Web サイトがエラーを返したらリトライは? 11/39
  7. バックエンド開発体制(2020/1当時) 1. PHP チーム ◦ 大部分の主要機能(チャットルーム、権限、通知…)の内部 API・公開 API ◦ ストリーミング処理、定期実行処理…

    2. Scala チーム ◦ 一部の主要機能:メッセージング(2016)、検索(2020) ◦ 主要機能から疎結合な周辺機能(OAuth、Webhook、監査ログ…) 3. コアテクノロジー部 ◦ Akka/CQRS を核に Chatwork を再構築する技術・開発プロセスの実証 13/39 PHPで開発スタート メッセージング Scala化
  8. バックエンド開発の懐事情(2020/1当時) • 開発プロジェクトの分業 ◦ PHP チームは主要機能を担当 ▪ 重要な開発を担うが、歴史あるレガシーコードなので開発も一苦労 ▪ いっぱいいっぱいになりがち

    ◦ Scala チームは新規の周辺機能に注力 ▪ 現行アーキで Scala チームが主要機能を受け持つには PHP ⇔ Scala 連携 の基盤整備が必要 ▪ 現行アーキでは基盤整備を最小限に。新アーキで全面刷新予定のため • リンクプレビューはなんとしても Scala チームが巻き取りたい 14/39
  9. Chatwork リンクプレビューのアーキテクチャ検討 リンクプレビュー チャットルーム 1. メッセージ投稿前にプレビュー要求 2. プレビュー作成 3. プレビュー取得

    4. プレビュー付メッ セージ投稿 6. プレビュー付 メッセージ取得 5. メッセージ要求 リンクプレビュー チャットルーム 1. URL入りメッセージを投稿 4. プレビュー作成 6. プレビュー付 メッセージ取得 2. メッセージ要求 3. プレビュー要求 リンクプレビュー チャットルーム 1. URL入りメッセージを投稿 3. メッセージ取得 5. プレビュー作成 6. プレビュー取得 4. プレビュー要求 リンクプレビュー チャットルーム 1. URL入りメッセージを投稿 3. プレビュー作成 5. プレビュー付メッセージ取得 (作成中ならプレースホルダー) 4. メッセージ要求 2. プレビュー 作成要求 バックエンドが密結合 バックエンドが疎結合 遅延影響 小 ガタツキ 有 非 同 期 的 や や 同 期 的 遅延影響 大 ガタツキ 無 ① ② ③ ④ バックエンド開発負荷 大 フロントエンド開発負荷 小 バックエンド開発負荷 小 フロントエンド開発負荷 大 2. メッセージ要求
  10. Chatwork リンクプレビューのアーキテクチャ検討 リンクプレビュー チャットルーム 1. メッセージ投稿前にプレビュー要求 2. プレビュー作成 3. プレビュー取得

    4. プレビュー付メッ セージ投稿 6. プレビュー付 メッセージ取得 5. メッセージ要求 リンクプレビュー チャットルーム 1. URL入りメッセージを投稿 4. プレビュー作成 6. プレビュー付 メッセージ取得 2. メッセージ要求 3. プレビュー要求 リンクプレビュー チャットルーム 1. URL入りメッセージを投稿 3. メッセージ取得 5. プレビュー作成 6. プレビュー取得 4. プレビュー要求 リンクプレビュー チャットルーム 1. URL入りメッセージを投稿 3. プレビュー作成 5. プレビュー付メッセージ取得 (作成中ならプレースホルダー) 4. メッセージ要求 2. プレビュー 作成要求 バックエンドが密結合 バックエンドが疎結合 遅延影響 小 ガタツキ 有 非 同 期 的 や や 同 期 的 遅延影響 大 ガタツキ 無 ① ② ③ ④ バックエンド開発負荷 大 フロントエンド開発負荷 小 バックエンド開発負荷 小 フロントエンド開発負荷 大 2. メッセージ要求 バックエンド開発期間 大 既存の機能の改修、 PHP⇔Scala 連携基盤の整備、 PHP チームが他の主要プロジェクトを いくつも手掛けていて余力ない … ユーザー体験のリスク URL 含んだメッセージがあると数秒遅延、 プレビュー作成失敗時のユーザーフローが複 雑、etc… 迅速なバックエンド開発 Scala チーム単独での新規機能開発、 別プロジェクトで検証していた サーバーレスの恩恵が大
  11. • ざっと必要なもの ◦ エンドポイント(Web サーバー) ◦ ビジネスロジック(アプリサーバー) ◦ プレビュー用データやログを保存(DBサーバー、ファイルサーバー) •

    どういう台数や構成なら必要な性能が出せるか… • etc… チャットルーム 1. URL入りメッセージを投稿 3. メッセージ取得 2. メッセージ要求 ゼロからの新規開発で “サーバー” でやること、めちゃ多い リンクプレビュー 5. プレビュー作成 6. プレビュー取得 4. プレビュー要求 ② ここの具体化 18/39
  12. サーバーレスなフルマネージドサービスがピタッとはまった CloudFront
 外部サイト API Gateway
 DynamoDB
 Lambda
 19/39 チャットルーム 1.

    URL入りメッセージを投稿 3. メッセージ取得 2. メッセージ要求 リンクプレビュー 5. プレビュー作成 6. プレビュー取得 4. プレビュー要求 ② ここの具体化
  13. Lambda: 毎月数億 URL を高速・安価にさばける ”サーバー” CloudFront
 🌏外部サイト API Gateway
 DynamoDB


    • Lambda関数: 入力(今回はHTTPリクエスト)に対し、出力(プレ ビューに必要な JSON や画像)を返す • 同時に何千〜何十万リクエストでもさばけるスケーラビリティ ◦ リンクプレビューでも毎月数億 URL を安定してさばけている • 性能x処理時間で課金。処理してない間は課金されない ◦ メモリはわずか 256 MB(あとで詳しく) ◦ 従来のサーバー起動しっぱなしと比べて超安い Lambda
 20/39 外部サイト
  14. API Gateway: Lambda を Web API として外部公開 21/39 CloudFront
 🌏外部サイト

    API Gateway
 DynamoDB
 • URL パス/メソッドと、データの送り先(今回は Lambda)をひもづけ ◦ POST /link-preview → OgpHandler 関数 ◦ POST /ogp-image → ImageHandler 関数 • API Gateway 単独でも URL がふられるので利用できるが、作り直すと サブドメインが変わってしまうのがちょっと困る ◦ CloudFront(このあと説明)を前段におけば URL を固定できる Lambda
 外部サイト
  15. DynamoDB: 同じ URL への同時アクセス防止に使用 22/39 CloudFront
 🌏外部サイト API Gateway
 DynamoDB


    • 毎分何万リクエストもさばけて Lambda と相性よい DB • 「このURL処理中」といったレコードを書込(PutItem) して、排他制御(楽観ロック)に利用 • DynamoDB の書込コストを減らす工夫 ◦ PutItem に ConditionExpression をつけて、すで に排他制御中のときは書込キャンセル(無料) ◦ Time-To-Live 属性を利用して不要になった排他制 御レコードを自動で削除(無料) Lambda
 外部サイト
  16. CloudFront: リンクプレビューの保存&高速な配信 CDN DynamoDB CloudFront
 🌏外部サイト DynamoDB
 • 同一 URL

    のリンクプレビューを一定時間キャッシュ ◦ キャッシュ前:平均 1.00 秒 ◦ キャッシュ後:平均 0.02 秒 • キャッシュがある間はアクセスしない=外部サイトへの配慮 • CloudFront より後段のリソースの利用コストを数十分の1に削減 • デスクトップ or モバイル、ユーザー言語でキャッシュをすみわけ (URL に加え、HTTP ヘッダーなどもキャッシュキーにできる) • 画像などもキャッシュできるので、本プロダクトでは S3 使ってない API Gateway
 Lambda
 キャッシュ前 キャッシュ後 23/39 外部サイト
  17. AWS Lambda ランタイム 25/39 ランタイム 登場 現在のバージョン Node.js 2014/11 14.x、12.x、10.x

    Java 2015/6 11、8 Python 2015/10 3.8、3.7、3.6、2.7 .NET 2016/12 3.2、3.1 Go 2018/1 1.x Ruby 2018/11 2.7、2.5 カスタム 2018/11 任意 コンテナイメージ 2020/12 任意 Scala などの JVM 言語
  18. Java/Scala 開発者から見た AWS Lambda Java ランタイム • Pros ◦ 使い慣れた静的型付け言語で堅牢に開発

    ◦ 豊富なライブラリを活用 • Cons ◦ JVM なので、コールドスタート(Lambda 関数イン スタンスの初回起動)が遅い! 26/39
  19. コールドスタートを短くする涙ぐましい努力① • 定期的にリクエストを投げて事前にウォームアップしておく ◦ ウォームアップ済み以上のリクエストが来たらやはり遅い ◦ 現在:Provisioned Capacity でウォームアップできるように。札束で解決 •

    Lambda に与える性能を高めて、起動を速める ◦ 実際の処理には性能がいらなくてもコストアップ! • 起動が速いランタイムを使う ◦ TypeScript や Go を書く。学習コスト、スイッチングコスト • 1つの関数をいろんな用途に使い回す(通常は用途ごとに関数を使い分け) ◦ CloudWatch Logs やメトリクスが全部1つになってしまうので、用途ごと の違いが分からなくなる(自前で何とかするパワーが必要) 27/39
  20. コールドスタートを短くする涙ぐましい努力② • 使いたい言語から JS を生成し、起動が速い Node.js ランタイムを使う ◦ コンパイラーや Node.js

    と付き合っていく覚悟が必要 ◦ ネタではなく真面目にプロダクションでやっていました 28/39 https://speakerdeck.com/exoego
  21. ランタイム 登場 現在のバージョン Node.js 2014/11 14.x、12.x、10.x Java 2015/6 11、8 Python

    2015/10 3.8、3.7、3.6、2.7 .NET 2016/12 3.2、3.1 Go 2018/1 1.x Ruby 2018/11 2.7、2.5 カスタム 2018/11 任意 コンテナイメージ 2020/12 任意 2018年末にゲームチェンジャーが登場 29/39
  22. AWS Lambda カスタムランタイム • 従来 AWS がマネージしてくれてたランタイムを自前で実装できる仕組み • ざっくりいうと以下のファイルを実装して、ZIP でかためてアップロード

    ◦ bootstrap: ランタイムの入口。ランタイムの仕事をこなす ◦ 任意のファイル: 個別の Lambda 関数、その他なんでも組み込める。 30/39 初期化 設定の取得 関数の初期化 初期化エラー処理 実行準備 イベントの取得 トレースヘッダーの伝播 コンテキストオブジェクトの作成 関数の呼出 終了 成功時レスポンス処理 エラー処理 クリーンアップ λ アプリの 実際の処理 個別の Lambda ランタイムの仕事 イベントループ
  23. JVM 言語でもカスタムランタイムで Lambda を爆速に実行する • GraalVM Native Image で、JVM 言語プログラムをネイティブコードに

    AOT コンパイルし、単体実行可能なバイナリを作成(以降 ネイティブ化) • JVM での実行に比べて、高速起動・省メモリ=Lambda にうってつけ • 実行速度は必ずしも JVM より速いわけではない 32/39 https://docs.oracle.com/en/graalvm/enterprise/19/guide/ reference/native-image/native-image.html
  24. ネイティブ化するまでの地道な作業 • akka などのリフレクションを使用しているライブラリを使う設定が必要 ◦ https://www.graalvm.org/reference-manual/native-image/Refle ction/ • ネイティブ化に成功しても、実行時エラーもあってツラい ◦

    実行時エラーを設定で出して地道にデバッグ • GraalVM のバージョン、JDK のバージョン、ライブラリのバージョン (内部のリフレクションの変更)によってもわりと変わるので、バージョ ンアップの労力がかかる 33/39 [ { "name": "akka.actor.typed.ActorRef", "allDeclaredConstructors": true, "allPublicConstructors": true }, { "name": "akka.actor.typed.internal.adapter.ActorSystemAdapter$LoadTypedExtensions$", "fields":[{"name":"MODULE$"}] }, ... // こんなのが数百行 ]
  25. Scala と GraalVM Native Image の相性 • 基本的には GraalVM Native

    Image と相性が良く感じた ◦ コンパイル時の解決を好み、実行時リフレクションは極力避ける文化 ◦ 例えば JSON ライブラリ ▪ Scala circe:設定なしですんなり動く ▪ Java Jackson:リフレクションがっつりなので設定必要 • 本プロジェクトでリフレクションの設定で苦労したところ ◦ ロガー slf4j logger ◦ 非同期処理 akka ▪ 初期の開発者が好みの elixir でプロトタイプし、アクターという思想が 共通する akka を使ってすばやく移植した ▪ akka-http で定義された HTTP ステータスコードやヘッダーなどが、 Web API を作る上で便利 34/39
  26. リンクプレビューでの GraalVM Native Image の恩恵 35/39 JVM ランタイム @ 1024

    MB カスタムランタイム @ 256 MB コールドスタート 10,000 ms 300 ms 実際の処理 平均※ 1,000 ms 1,000 ms ※ I/O(外部サイトのダウンロードや DynamoDB)が律速なので、ネイ ティブ化しても平均処理時間はあまり変わらないという理解 JVM だと 1024 MB でもコールドスタートが10秒もかかってしまう! ネイティブなら 256 MB でも 300 ms と高速に起動! Web API のバックエンドとして十分使える メモリ減らしすぎると CPU コア数も減って性能が変わったりするので、検証しま しょう
  27. ちょっと苦労してでもカスタムランタイムを使うメリット • 2020年12月1日から Lambda の課金単位が 100ms→1ms に ◦ 速ければ速いほど運用コストも節約! ▪

    リンクプレビューのような I/O 律速な Lambda はネイ ティブ化で必ずしも速くはならないが… ◦ もちろん処理が速いほどエンドユーザーはうれしい • Rust などで超高速な Lambda をやってる会社もいるそうです • https://github.com/awslabs/aws-lambda-rust-runtime ◦ AWS も OSS として Rust ランタイムを出してる 36/39
  28. その他のトピック)AWS Lambda Layer • Lambda で使いたい何らかのファイルを Layer として登録しておき、個々の Lambda から参照して、再利用する仕組み

    ◦ 巨大なバイナリ ◦ 大量の依存関係(node_module など) • リンクプレビューでは ImageMagick を Layer にしている 37/39
  29. サーバーレスアーキテクチャ最高 !! • 色々なサーバーレス系 AWS のおかげで、少人数 DevOps でも迅速 にバックエンド開発し、安定稼働できている •

    JVM 言語でも、GraalVM Native Image でネイティブ化すれば高速 起動・省メモリの Lambda が作れて、Web API 向けも十分実用的 • Scala で開発した Lambda で毎月数億 URL を安定に処理。さらに CloudFront のキャッシュで毎月数十億リクエストを高速に配信 • Scala のエコシステムはリフレクションを避けるものが多いので、 基本的にネイティブ化と相性が良い 39/39