Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

⾃⼰紹介 ⼤和 康平 Twitter: daiwahome0 GitHub: daiwahome マルチデバイスチーム エンジニア アプリ開発チームのサーバーサイドをメインに開発 Java, Kotlin, Terraform, CloudFormation (SAM) 等 AWS, オンプレでの開発がメイン #devboostA 3

Slide 4

Slide 4 text

はなすこと 昨年1年間で遭遇したサーバーサイド開発中に起こった問題と、 どのように解決してきたかについて事例ごとに紹介します。 (⼀緒に笑い⾶ばしていきましょう) #devboostA 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

あらまし 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

Slide 9

Slide 9 text

追加しようとしていたライブラリ 並列処理を可視化するために、トレーシングツールのStackdriver Traceを導⼊しよ うとしていた。 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 9

Slide 10

Slide 10 text

スローされている例外 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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

原因 ⾃動リロードなどを⾏う開発⽤ツールである 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

Slide 17

Slide 17 text

対応 初回のみ初期化処理が実⾏されるように修正した。 val isStarted = AtomicBoolean(false) // すでに実⾏されている場合はスキップ if (!isStarted.compareAndSet(false, true)) { return } // 初期化処理 init() 事例1. ライブラリを追加したら◯◯が2回呼ばれるようになった #devboostA 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

修正前後のTrace結果 修正後 (右図) は並列処理されていることを⽰している。 修正前               修正後 事例3. スケールしない並列処理システム #devboostA 40

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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