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

新卒一年目でサーバーサイド開発あるあるを踏み抜いてきた話 / Developers Boost 2019 A-7

E3575b2139d781a15136d253bb8d7ba9?s=47 Kohei Yamato
November 30, 2019

新卒一年目でサーバーサイド開発あるあるを踏み抜いてきた話 / Developers Boost 2019 A-7

Developers Boost 2019で登壇したスライドです。

https://event.shoeisha.jp/devboost/20191130/session/2230/

E3575b2139d781a15136d253bb8d7ba9?s=128

Kohei Yamato

November 30, 2019
Tweet

Transcript

  1. 新卒⼀年⽬でサーバーサイド開発あるあるを 踏み抜いてきた話 Developers Boost 2019 #devboostA (A-7) エムスリー株式会社 ⼤和康平 #devboostA

    1
  2. おことわり サーバサイドあるあるの感じ⽅には個⼈差があります 現在は開発フローも⾒直されており、技術的負債とならないように改善を続けて います #devboostA 2

  3. ⾃⼰紹介 ⼤和 康平 Twitter: daiwahome0 GitHub: daiwahome マルチデバイスチーム エンジニア アプリ開発チームのサーバーサイドをメインに開発

    Java, Kotlin, Terraform, CloudFormation (SAM) 等 AWS, オンプレでの開発がメイン #devboostA 3
  4. はなすこと 昨年1年間で遭遇したサーバーサイド開発中に起こった問題と、 どのように解決してきたかについて事例ごとに紹介します。 (⼀緒に笑い⾶ばしていきましょう) #devboostA 4

  5. 今回紹介する事例 1. ライブラリを追加したら◯◯が2回呼ばれるようになった 2. 単体テストのCoverageが5%台のシステム 3. スケールしない並列処理システム #devboostA 5

  6. ちなみに 今回お話する事例は、全て同じシステムの開発中に遭遇した内容です。 #devboostA 6

  7. 事例1. ライブラリを追加したら◯◯が2回呼ばれるように なった #devboostA 7

  8. あらまし Spring Boot (Java + Kotlin) のWebアプリケーションに、Gradleでライブラリの依 存関係を追加しようとした。 (build.gradle) implementation

    'io.opencensus:opencensus-api:0.16.0' implementation 'io.opencensus:opencensus-exporter-trace-stackdriver:0.16.0' runtimeOnly 'io.opencensus:opencensus-impl:0.16.0' → 起動時に例外を吐いて落ちるようになってしまった... 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 8
  9. 追加しようとしていたライブラリ 並列処理を可視化するために、トレーシングツールのStackdriver Traceを導⼊しよ うとしていた。 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 9

  10. スローされている例外 Exception in thread "restartedMain" java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at

    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) Caused by: java.lang.IllegalStateException: Stackdriver exporter is already registered. Stackdriver exporter is already registered. → すでに初期化されている??? 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 10
  11. Breakpointを置いて動作を追ってみる 1. main関数が実⾏される 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 11

  12. Breakpointを置いて動作を追ってみる 2. 例外が発⽣していた⾏で停⽌する 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 12

  13. Breakpointを置いて動作を追ってみる 3. main関数が実⾏される (!?) 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 13

  14. Breakpointを置いて動作を追ってみる 4. 例外がスローされる 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 14

  15. 起こっていたこと main関数が2回実⾏されていた 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 15

  16. 原因 ⾃動リロードなどを⾏う開発⽤ツールである spring-boot-devtools がmain関数 を2回実⾏していた。 (RestartLauncher.java:47-49) Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName);

    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); mainMethod.invoke(null, new Object[] { this.args }); その結果、Stackdriver Trace⽤のライブラリが初期化エラーとなった。 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 16
  17. 対応 初回のみ初期化処理が実⾏されるように修正した。 val isStarted = AtomicBoolean(false) // すでに実⾏されている場合はスキップ if (!isStarted.compareAndSet(false,

    true)) { return } // 初期化処理 init() 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 17
  18. 事例1のまとめ spring-boot-devtools によりmain関数が2回実⾏されていたが、 初回のみ初期化処理を⾏うようにした。 教訓: 起動の仕組みに⼿が⼊るようなツールを使⽤する場合には注意しましょう ログを⼿がかりにして、Breakpointを使⽤して原因を特定するとよいです 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA

    18
  19. 事例2. 単体テストのCoverageが5%台のシステム #devboostA 19

  20. あらまし 歴史的経緯で、単体テストのCoverageが5%程度しかないシステムが本番稼働して いた。動作の正しさは、⼿作業のQA (Quality Assurance, 品質保証) で担保してい た。 システムについてのドキュメントもほとんどなく、何が正しい動作なのか判断できな い状況だった

    (加えて開発者は退職済み)。 事例2. 単体テストのCoverageが5%台のシステム #devboostA 20
  21. Coverageが5%台 事例2. 単体テストのCoverageが5%台のシステム #devboostA 21

  22. Coverageが5%台 事例2. 単体テストのCoverageが5%台のシステム #devboostA 22

  23. つらい点 単体テストのCoverageが低いことで、次のような問題が存在した。 機能開発のコストが⾼い 変更した部分以外へ影響がないことを担保できない ⼿動でテストしてレビューに出す必要がある 事例2. 単体テストのCoverageが5%台のシステム #devboostA 23

  24. 直⾯した問題 DBアクセスが遅い 運⽤期間が延びたことでDBに保存されているデータが増え、DBへのアクセスがボト ルネックになってきた。原因の⼀つは、必要以上の正規化をしていて、JOINのコス トが無視できない程になっていたことだった。 そのためチームのメンバと相談し、DBの⾮正規化を⾏うことにした。 事例2. 単体テストのCoverageが5%台のシステム #devboostA 24

  25. 直⾯した問題 DBの⾮正規化では、動作が変わらないことを保証しなければならない。 単体テストがない中、次の状況を踏まえて対応する必要があった。 Coverageをあげようとすると相応のコストがかかる Javaのソースコード298ファイル (約1.4万⾏) 開発者としてアサインできるのは1⽇あたり1⼈ 既に本番環境に影響が出ている 事例2. 単体テストのCoverageが5%台のシステム

    #devboostA 25
  26. 対応 範囲を絞って単体テストを追加した 単体テストなしに開発することは避けたいため、コード上で影響が出そうな範囲を絞 り込み単体テストを追加した (OR Mapper部分 + ⼀部のロジック)。 単体テストを先に作成したことにより修正漏れに気づくことができ、開発スピードを 上げることができた。最終的には、⾮正規化したシステムをリリースすることがで

    き、動作速度を改善することができた。 事例2. 単体テストのCoverageが5%台のシステム #devboostA 26
  27. 事例2のまとめ 動作が変わらない部分の単体テストを先に埋めることで、⾮正規化後の動作を保証し ながら問題に対応できた。 教訓: 単体テストは書くようにしましょう 初めは⼯数がかかるかもしれませんが、あとで運⽤が楽になります 単体テストだけでなく、ドキュメントも整備しましょう 事例2. 単体テストのCoverageが5%台のシステム #devboostA

    27
  28. 事例3. スケールしない並列処理システム #devboostA 28

  29. あらまし Android, iOSやWebブラウザ向けのプッシュ通知を管理するシステムが本番環境で 稼働しているが、問題を抱えていた。 システムの要件 (⼀部): 決まった時刻に送信可能な全ユーザにプッシュ通知を送る ⼀度の配信で最⼤で40万ユーザ宛てに10分で送信完了する 事例3. スケールしない並列処理システム

    #devboostA 29
  30. システム構成図 事例3. スケールしない並列処理システム #devboostA 30

  31. 抱えていた問題 要件を満たせていない プッシュ通知配信サーバにアクセスするworkerを複数台構成にしているが改 善しない 事例3. スケールしない並列処理システム #devboostA 31

  32. 抱えていた問題 要件を満たせていない 「⼀度の配信で最⼤で40万ユーザ宛てに10分で送信完了」すべき。 → 実際には30分以上かかっている → 配信が多い時間帯だと、最初の配信が遅延して、その影響で次の配信も遅延し て... (以下繰り返し) 事例3.

    スケールしない並列処理システム #devboostA 32
  33. 抱えていた問題 プッシュ通知配信サーバにアクセスするworkerを複数台構成にしているが改 善しない SQSを介して並列処理しているworkerを増やしても、スループットを改善できなか った。 事例3. スケールしない並列処理システム #devboostA 33

  34. Stackdriver Traceの導⼊ AWS CloudWatch Logsにworkerのログを収集して原因を調査していたが、明確に 遅い処理は発⾒できず。 並列処理の状況について調査しようとするも、複数workerによる並列処理に加えて ⼀つのworker内でも並列処理している関係上、全体を追うのはつらい状況だった。 → Stackdriver

    Traceの導⼊を⾏った 事例3. スケールしない並列処理システム #devboostA 34
  35. 導⼊した結果...... 並列動作していないように⾒える 事例3. スケールしない並列処理システム #devboostA 35

  36. 原因調査 SQSからメッセージを並列で受信できていない可能性があるため、 workerの処理の開始と終了時刻をログに追加して動作を検証した。 その結果、ログからもworkerの数を増やしても直列でメッセージを処理しているこ とを確認した。 SQSから並列で受信できていないことが判明したため、SQSの設定とAWS公式ドキ ュメントを⼀から⾒直した。 事例3. スケールしない並列処理システム #devboostA

    36
  37. 改善しない原因 SQSのパラメータの設定ミスにより、複数台のworkerが排他的に動作していたのが 改善しない原因だった。 事例3. スケールしない並列処理システム #devboostA 37

  38. 改善しない原因 すべてのmessageで共通のグループIDを使⽤していたため、処理が終わるまで次の messageを処理できないようになっていた。 https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloper Guide/using-messagegroupid-property.html 同じメッセージグループに属するメッセージは、常にメッセージグループに対 して厳密な順序で1つずつ処理されます 事例3. スケールしない並列処理システム #devboostA

    38
  39. 設定の修正 設定ミスが判明したため、並列でmessageを受信できるように修正する。 並列に受信するためには、messageごとに別のグループIDを付与すればよいため、 重複の可能性がほとんどないUUIDを設定した。 事例3. スケールしない並列処理システム #devboostA 39

  40. 修正前後のTrace結果 修正後 (右図) は並列処理されていることを⽰している。 修正前           

       修正後 事例3. スケールしない並列処理システム #devboostA 40
  41. 事例3のまとめ 複数のworkerが休まずに仕事をするようになったのもあり、「⼀度の配信で最⼤で 40万ユーザ宛てに10分で送信完了」の要件を満たすことができた。 教訓: ドキュメントをしっかり読もう 動作確認をきちんと⾏おう 並列処理の確認は難しいため必要に応じて可視化ツールを導⼊しましょう 事例3. スケールしない並列処理システム #devboostA

    41
  42. おわりに サーバサイド開発を通してハマってしまった問題について、事例を元に紹介しま した Breakpointで調査した例 単体テストを追加してDBを⾮正規化した例 Stackdriver Traceを導⼊した例 発⽣した問題の対応と、そこからの教訓をまとめました このスライドがみなさんの役に⽴つと幸いです #devboostA

    42
  43. ご静聴ありがとうございました #devboostA 43