Chatworkでリアクション機能をリリースした話_scala_ks.pdf

2aa3188f209c52d8ee04e8b43a94920d?s=47 kamegu
October 26, 2019

 Chatworkでリアクション機能をリリースした話_scala_ks.pdf

2aa3188f209c52d8ee04e8b43a94920d?s=128

kamegu

October 26, 2019
Tweet

Transcript

  1. Chatworkでリアクション機能をリリースした話 Scala関西 Summit 2019 Chatwork株式会社 勝野 徳

  2. © Chatwork 自己紹介 • 勝野 徳
 ◦ 「めぐむ」って読みます
 • Chatwork株式会社

    サーバサイド開発部
 ◦ 2016/08入社 => PHPエンジニア
 ◦ 現在はScalaチーム(このPJ期間中に変更)
 ◦ 元々はJavaを少々
 • 趣味はランニング
 ◦ 年に1回くらいフルマラソン
 ◦ まだガチ勢ではないと思っている
 • その他
 ◦ twitter: kamegu3

  3. © Chatwork 会社紹介 • 拠点 ◦ 大阪 ◦ 東京 ◦

    リモート • 大阪オフィス、引っ越しました ◦ https://news-ja.chatwork.com/2019/07/chatwork.html ◦ 梅田エリア(福島) ▪ ランチ充実 ▪ 少し(20分)歩けばランニングステーションあり
  4. © Chatwork Chatworkサービスの紹介
 • ビジネスチャットサービス • 導入企業235,000社突破!(2019年9月末日時点)

  5. © Chatwork 今日話すこと • 話すこと ◦ PJの開始からリリースまでを時系列で ▪ Scala開発の目線 ▪

    なるべくありのままに ◦ つまずいたこと ▪ 特殊なものが多いかもしれませんが雰囲気だけでも
  6. © Chatwork 目次 ➢ 要件定義〜アーキテクチャ検討
 ➢ PoC
 ➢ 実装、負荷試験
 ➢

    リリース

  7. © Chatwork 目次 ➢ 要件定義〜アーキテクチャ検討
 ➢ PoC
 ➢ 実装、負荷試験
 ➢

    リリース

  8. © Chatwork リアクション機能 • メッセージに対してカジュアルに反応するための機能 • 7/17リリース ◦ 世界絵文字デーに合わせて https://news-ja.chatwork.com/2019/07/chatwork-662617.html

    ◦ キャンペーンも 利用イメージ
  9. © Chatwork リアクション機能の要件(一部抜粋: サーバサイド関連) • メッセージにリアクションできる / 削除できる ◦ 1メッセージに対して1人1種類だけ

    • 常にメッセージとセットでリアクション情報を返す ◦ Room内で連続する1つ以上のmessage • ほかの人がおこなったリアクションもほぼリアルタイムで反映される
  10. © Chatwork リアクション機能の非機能要件(一部抜粋) • 性能 ◦ 少なくとも5年後想定まで耐えうる ◦ スループット ▪

    GET: メッセージ取得と同程度 ▪ PUT: メッセージ書き込みの1/10程度を想定 ◦ レイテンシ ▪ (Scala部分で)GET:30ms以下、PUT:50ms以下 • 運用 ◦ コスト(インスタンス、データストア等) ◦ 障害耐性
  11. © Chatwork アーキテクチャを考えるにあたって • 既存のChatworkのアーキテクチャに依存 ◦ リクエストをPHPで受ける(認証や権限チェック) ◦ メッセージ保存/取得はFalcon(Scala製サブシステム)へリクエスト ◦

    更新通知(リアルタイムでwebクライアントに反映させる)はPHP REACTIONはどこに入れる?? Pusher 更新通知 Falcon(かなり省略)
  12. © Chatwork 今回のアーキテクチャ設計におけるポイント • Scala vs PHP ◦ PHPは通らざるを得ない ◦

    マイクロサービス(Scala) vs モノシリック(PHP) ▪ 工数比較で言えばPHPが有利 • データストア ◦ RDBMS or KVS ◦ Managed or not
  13. © Chatwork アーキテクチャ検討(Scala vs PHP) • ChatworkとしてはScalaを推していきたい ◦ 実際はまだまだPHPで動いてる物の方が多いが ◦

    出来るだけScalaでやることに意味がある • マイクロサービス的にすることのメリットもある ◦ スケーラビリティ ◦ 障害の考え方 • 結果 => Scala案採用
  14. © Chatwork アーキテクチャ(データストア) • 当初の候補 ◦ 社内の運用実績 ◦ RDBMS ▪

    データ量が何年後まで耐えられるか ◦ HBase ▪ 運用コスト • 第3の候補 ◦ DynamoDB
  15. © Chatwork DynamoDBの検討 • 候補理由 ◦ RDBMSとHBaseの問題点をクリアしている ◦ コストが不安だったが、試算してみたら予算内 •

    問題点 ◦ 高トラフィックな箇所で社内での運用実績なし • => スペック上は問題なさそうだから検証
  16. © Chatwork アーキテクチャ決定(仮) • Scala (+PHP) ◦ マイクロサービス的に • DynamoDB

  17. © Chatwork アーキテクチャ決定(仮) ここまでScalaの話なし

  18. © Chatwork 目次 ➢ 要件定義〜アーキテクチャ検討
 ➢ PoC
 ➢ 実装、負荷試験
 ➢

    リリース

  19. © Chatwork PoC • 目的 ◦ プロトタイプ実装 ▪ GET ▪

    PUT ◦ 全体の性能 ◦ DynamoDBの検証 ▪ 性能 ▪ 特性
  20. © Chatwork PoCシステム構成 • Scalaアプリケーション ◦ Akka HTTP ◦ AWS

    SDK for Java 2.0 (DynamoDB) ◦ Monix (Task) ◦ Kamon • Dockerイメージ作成 ◦ sbt-native-packager • Kubernetes on AWS ◦ Falconの時から運用しているものをアップデートしながら (not EKS) • Gatling on ECS
  21. © Chatwork アプリケーションの構成 • Clean Architecture をベース ◦ だいぶ省略したbuild.sbt =>

    ◦ contract-xxxxはInterfaceを定義 • 今回は ◦ ドメイン知識がほとんどない ◦ もっとシンプルにしても問題ないはず
  22. © Chatwork AWS SDK for Java 2.0 • 2.0からNon-Blocking対応 ◦

    netty ◦ Blocking IOだと色々面倒なので助かる ◦ AkkaがNon-Blockingベースなので • 1系=>2系ではpackage名も変わっている • アップデートがかなり頻繁
  23. © Chatwork Monix Taskの紹介 • Monix ◦ high-performance Scala /

    Scala.js library for composing asynchronous, event-based programs. (原文そのまま) • Monix Task ◦ 極端なことを言えば、Futureをええ感じにしたやつ ◦ Scalaz.Taskにinspired
  24. © Chatwork Monix Taskの紹介 • Futureと何が違うか ◦ Futureは処理が開始済み、Taskは遅延評価 ▪ 先行評価やFutureを受け取ったりもできる

    ◦ Taskはmap等の処理にExecutionContextが必要ない ▪ (MonixではExecutionContextを継承したScheduler) ▪ 最終的に評価するときだけ必要 • 特定の処理だけ別のExecutionContextを使用することも可能 ▪ Taskを受け取る/返す関数に対してExecutionContextをimplicitパラ メータで渡す手間が省ける
  25. © Chatwork Monix TaskのリアクションPJでの使用イメージ • Taskインスタンス生成 ◦ repository層 ◦ AWS

    SDKの呼び出しをwrap ◦ 遅延評価 ◦ ExecutionContext不要 import scala.compat.java8.FutureConverters._ Task.deferFuture { dybamoDbAsyncClient.query(queryRequest).toScala // Future }.map(convertToReaction)
  26. © Chatwork Monix TaskのリアクションPJでの使用イメージ • DynamoDBからの取得結果を加工 ◦ usecase層 ◦ ExecutionContext不要

    • Taskの評価 ◦ controller層 ◦ Schedulerを使用 val result: Task[Reaction] = repository.xxx() // 上の処理を実行 result.map(convertToModel) def zzz(implicit scheduler: Scheduler) { val result: Task[Model] = usecase.yyy() // 上の処理を実行 result.runToFuture // ここで評価が開始される
  27. © Chatwork Gatlingの紹介 • Scala製の負荷試験ツール • ChatworkではScalaプロダクトの負荷試験でのデファクト ◦ v2.2 ◦

    全PJでほぼ同じ使い方 • sbtで実行可能 ◦ https://gatling.io/docs/2.2/extensions/sbt_plugin/ • 実際にsbtで負荷試験しようとすると色々限界がある ◦ ローカルからだとリソース、NW遅延、アクセス権、etc ◦ ChatworkではAmazon ECS上で実行する仕組み
  28. © Chatwork ChatworkでのGatlingの使い方 • 指定時間(rampDuration)をかけて指定したユーザ数を追加する • 増え切ったらその後の指定期間(holdDuration)はそのまま • ユーザ数 =

    リクエスト数/秒 rampDuration holdDuration numOfUser
  29. © Chatwork ChatworkでのGatlingのコード例 setUp( getByRangeScenario.inject(rampUsers(numOfUser) over rampDuration) ).protocols(httpConf).maxDuration(rampDuration + holdDuration)

    val getByRangeScenario = scenario("getByRange").forever( pace(1) .feed(randomFeeder) .exec(getReactionsByRange) )
  30. © Chatwork PoC負荷検証1 • 実装ができたから負荷をかけてみた • ある程度は捌けてそうだが、遅い • サーバ側のメトリクスはここまで悪くなかった ◦

    network経路が怪しい 注:このスライドで使用しているGatlingのレポートのスクリーン ショットについて、負荷や検査対象のサーバ構成等の条件が 異なっています
  31. © Chatwork PoC負荷検証2 • Gatlingがk8sクラスタとは別VPCにいた ▪ 過去PJのドキュメント通りにやってしまったのが原因 • 修正後 ◦

    かなり改善 ▪ 200ms => 45ms ◦ まだ不十分
  32. © Chatwork PoC負荷検証3 • スループットをあげるとレイテンシが悪化する現象あり • これだけ聞くと普通と思うかもしれないが ◦ 線形で上がりすぎる ◦

    サーバ側のメトリクスでは詰まってる感じはない • 過去のPJでGatlingを使用した際の結果を確認 ◦ 同様の現象は発生していない
  33. © Chatwork PoC負荷検証4 • 今回PJで変えた点を確認 ◦ Gatling ▪ pause =>

    pace ◦ 負荷をキープするためにシナリオ内で同じユーザに一定間隔ごとに処理を ループさせている箇所 • pauseに変更して実行 ◦ 結果 => 劣化してない
  34. © Chatwork なぜpaceだとダメだったのか1 • pauseとpaceは何が違う? ◦ https://gatling.io/docs/2.2/general/scenario ◦ pause ▪

    前の処理が終わってから次の処理に進むまでの待ち時間 ◦ pace ▪ 一定間隔で実行 • 目標負荷で一定にするという目的ではpaceが正しそう ◦ pauseだとレスポンスを待つ分targetとするスループットまで達すること がない
  35. © Chatwork なぜpaceだとダメだったのか2 • ここでキーとなるポイントはinjection ◦ https://gatling.io/docs/2.2/general/simulation_setup/ ◦ rampUsers(nbUsers) over(duration)

    ▪ Injects a given number of users with a linear ramp over a given duration. ▪ 例えば5users - 5secondsとすると • t0, t0+1.0,,,t0+4.0 でinjectされる ▪ 例えば25users - 10secondsとすると • どうなるか • t0, t0+0.4, t0+0.8,,,,みたいになる?
  36. © Chatwork なぜpaceだとダメだったのか3 • 25users - 10secondsの場合 ◦ 10秒の中で均等に分散するのではなく、まず25ユーザを10個に配分 ▪

    1,3,,9秒 => 3users => t0+t, t0+t+0.333, t0+t+0.667 (t=0,2,,8) ▪ 2,4,,10秒 => 2users => t0+t, t0+t+0.5 (t=1,3,,9) ◦ injectされると、すぐ最初のリクエスト • この状態でpace(1 second)とすると同じタイミングに集中する ◦ msだけ切り取ると、0, 0.333, 0.5, 0.667の4パターン ◦ それ以外はリクエストが空白の時間 ◦ Gatlingのログを見ると、実際に同じタイミングで送られていた
  37. © Chatwork なぜpaceだとダメだったのか4 • 他の値を指定しても同様 ◦ 原理的には十分短い時間に多くのユーザをinjectしない限り同じ ◦ 1000users -

    1minuteでも32パターン程度 • pauseを使うと、レスポンスタイム分の遅延が挟まれるので結果的にいい感 じに分散されていた ◦ こういう理由で過去PJでpauseを使っていたかどうかは不明のまま、、、
  38. © Chatwork なぜpaceだとダメだったのか5 • 本来やりたかったことを実現するためにはこうすべきだったのかも ◦ foreverを外し、リクエストし終わったらそのユーザは終了 ◦ paceやpauseは負荷安定のためには使わない setUp(

    getByRangeScenario.inject( rampUsersPerSec(0) to numOfUser during(rampDuration), constantUsersPerSec(numOfUser) during holdDuration ) ).protocols(httpConf)
  39. © Chatwork PoC負荷検証5 • 結果 ◦ PoCとしては十分な性能が出たと判断 ▪ 最初からシステム自体に大きな問題はなかった ▪

    アプリの修正は小さなもの中心 ◦ DynamoDBの特性にも気になる箇所はなし
  40. © Chatwork Reaction Pocを踏まえたシステム構成案 Pusher 更新通知 Falcon(かなり省略)

  41. © Chatwork 目次 ➢ 要件定義〜アーキテクチャ検討
 ➢ PoC
 ➢ 実装、負荷試験
 ➢

    リリース

  42. © Chatwork 設計・本実装 • Scala側はPoCの際の実装に機能追加、修正する形 ◦ 結果的にリライトした箇所も ◦ リクエスト/レスポンスフォーマット(設計から) ◦

    k8s用のendpoint (liveness, readiness) ◦ logging ◦ メトリクス • 正直、PHP側がヘビーだった ◦ アーキテクチャの選択でPHPモノシリックを選んでいても生じたもの ◦ 逆に全部Scalaだったら楽だったのかもしれない
  43. © Chatwork 密かにリリース • 開発期間中から少しずつ本番環境で使えるようにしていく ◦ 本番環境構築 ◦ PHPも含めた疎通・動作確認 ◦

    モバイルアプリ等の開発時の検証用 ◦ 性能確認 • 最初はPJの開発メンバーのPHPリクエストだけScala側にアクセス ◦ 順次、社内全体まで公開 ◦ 都度 機能追加、修正 ◦ 機能が揃ったところで社内検証
  44. © Chatwork リリースしてみたものの • レスポンスが遅い • PJの開発メンバーにreadを適用したタイミングで発覚 ◦ (つまり最初)

  45. © Chatwork レスポンス遅い問題1 • PHP側のメトリクスだけでなくScala側のメトリクスでも遅い • DynamoDB側のメトリクスは問題ない ◦ Scala->DynamoDBへのアクセスが遅い •

    調査した結果 ◦ スループットが低い場合に起こる ◦ PoC時はある程度負荷をかけていたので気づかなかった
  46. © Chatwork レスポンス遅い問題2 • 怪しい箇所にログを仕込む ◦ Scalaコードはもちろん ◦ ライブラリ内の.javaファイルもsrc/main/javaへコピーして上書き •

    AWSのcredentialを取得している箇所が怪しい ◦ kiamというツールを経由してcredential情報を取得 ◦ このkiam側のキャッシュの設定がSDK側のキャッシュと相性が悪かった ◦ この設定を調整することで解消
  47. © Chatwork レスポンス遅い問題3 • 安定した ◦ 山も現れなくなっている • 結果として全体の性能も向上 ◦

    1.5倍程度のスループット
  48. © Chatwork さらに機能の追加や改善を進めている矢先 • credentialを取得できないエラーが発生 ◦ 原因不明 • kiamを使う方式だとリスクがありそうということで別の道を探ることに

  49. © Chatwork credentialどうするか • credential_process ◦ https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes ◦ credentialのファイルではなく、credentialを標準出力で返すコマンドを 指定する

    ◦ kube-aws-iam-controllerというツールがこれに対応 ◦ AWS SDKはv2.5.26 (2019/4/16)から対応 ▪ v1は(2019/1/24)から対応 ▪ 当時のプログラムでは2.4.0を利用 • なのでIDEでライブラリのソースコードを見ても気づかなかった ◦ SDKをアップグレードして問題回避 ▪ なんともヒヤヒヤするタイミング (この時5/15)
  50. © Chatwork ちなみに • EKSを使っていればcredentialsまわりでこんなに苦労しなかったかも

  51. © Chatwork 負荷試験 • メモリリーク発見 ◦ readとwriteのpodを分けたことで発症 ◦ ヒープダンプ調査 ◦

    Kamon 1系 ▪ 詳細な原因特定までには至っていない ▪ 社内で0系の運用実績があったのでそちらに切り替え • それ以外は大きな問題なく進む
  52. © Chatwork 負荷試験 • 各種パラメータも調整 ◦ AWS SDKとかAkka HTTPとか ▪

    ほぼデフォルト値で問題なし ▪ 値を大きく変えると劣化することもあるが、多少変えるくらいでは性能 変化ない(逆に上がることもなかった) • コンテナのサイズの調整 ◦ CPUやメモリの割り当て ◦ 1podあたりのリソースを小さめにしてスケールアウトさせる戦略
  53. © Chatwork 負荷試験 • ある程度の時間、ある程度の負荷をかける ◦ 想定負荷を十分満たせることを確認 • 結果 =>

    リリース Go
  54. © Chatwork 目次 ➢ 要件定義〜アーキテクチャ検討
 ➢ PoC
 ➢ 実装、負荷試験
 ➢

    リリース

  55. © Chatwork いざ、リリース • サーバサイドは3週間前から少しずつ公開 ◦ 本番環境への負荷を増やし、万が一問題があった場合に備える ◦ 1% =>

    10% => 30% => 100% ◦ 2週間前には本番相当の読み込み負荷 ◦ フロントエンドやモバイルアプリのリリースを待つ状態
  56. © Chatwork リリース当日 • 7/17 朝リリース ◦ フロントエンド ◦ モバイルアプリ

    • メトリクスを眺める ◦ Writeリクエスト数がどんどん増えていく ◦ CPU等は安定 • 安堵の時 リアクション数/日の推移(リリースから1ヶ月)
  57. エンジニア募集してます Scalaエンジニアはもちろん、 PHPやモバイルアプリやフロントエンドのエンジニアも https://corp.chatwork.com/ja/recruit/

  58. © Chatwork 感想 • MonixとかZioとか色々あるけど、結局何がいいんだろう • 負荷検証むずかしい ◦ ドキュメントに残し切れないノウハウ •

    時間の都合上、割愛した内容もあって何を残すか悩みました ◦ 例えばデータストアの検討や設計の話 ◦ Gatling on ECSの詳細とか ◦ 問題解決に向けて色々試したりもしました ◦ 興味ある方はぜひ
  59. © Chatwork まとめ • Scalaは十分速い ◦ DynamoDBも十分速い • 新しいものにするといいことあるかも ◦

    ライブラリのアップデート ◦ EKSのような新しいサービス ◦ その逆も然り • ChatworkではScalaエンジニアはいろんなことに詳しくなれます ◦ kubernetesとか ◦ どこまで深入りするかは自分次第