Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Backlogを落としたコード達 / Code that made Backlog down

Backlogを落としたコード達 / Code that made Backlog down

2020年12月5日(土)に開催されたNuConの登壇資料です。

▼NuCon2020 公式サイト
https://nucon.nulab.com/2020/

▼発表アーカイブ
https://www.youtube.com/watch?v=KXogZS5awbc&t=8873s

株式会社ヌーラボ

December 05, 2020
Tweet

More Decks by 株式会社ヌーラボ

Other Decks in Technology

Transcript

  1. 今日話すこと • Heap dump/Thread dumpはサービスを落とす原因になったコードを探すとき、い ろんなヒントが隠されていて論理パズルっぽくっておもしろい • 「ああ、こんなパターンがあったんだ」って気づいた瞬間が特に論理パズルの楽しさ に似ていると思う •

    今回は、自分が調べていて気づけたパターンを簡略化したコードを紹介する • おもしろいと思わなかったものは紹介しない ◦ 例えば、DBから大量のデータを一度に取ってきて OutOfMemorryErrorとかは紹介しない
  2. Scalaの前提知識 - ExecutionContextとは • JVMアプリケーションで非同期処理をする場合、あるスレッドから別のスレッドを 作って、処理を渡すということをよくします ◦ うまく使えば、並列に実行できるので効率よく処理ができる • しかし、スレッドを毎回作っているとコストがかかったり、こういうタイプの処理はこの

    スレッドのグループにやらせるとか役割を与えたいことがある • スレッドプールという作ったスレッドをためたり、グルーピングするためのものがある • ExecutionContext はスレッドプールに処理をさせたり、させる処理をQueueにため て管理したりするもの (※ これはなんとなく理解してもうための説明であり、正確ではありません)
  3. 前提知識 - Futureとは ExecutionContextを使い非同期処理を行ったり、その結果を保持したりする こんな感じに書くと簡単に非同期処理がかける Future { println("Hello world") }(executionContext

    ) executionContextの定義時にimplictをつけると暗黙的に引数としてわたるため、よりシ ンプルな見た目でかける Future { println("Hello world") } (※ これもなんとなく理解してもうための説明であり、正確ではありません)
  4. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } }
  5. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ①. getSomeObjectを呼び出してオブジェクトを取得 ① ①
  6. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ①. getSomeObjectを呼び出してオブジェクトを取得 ②. ①で取得したオブジェクトから必要な値を取得 ① ② ①
  7. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ①. getSomeObjectを呼び出してオブジェクトを取得 ②. ①で取得したオブジェクトから必要な値を取得 ③. ①、②が非同期で実行されるのでそれらが終わるのを待つ ① ② ③ ①
  8. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ①. getSomeObjectを呼び出してオブジェクトを取得 ②. ①で取得したオブジェクトから必要な値を取得 ③. ①、②が非同期で実行されるのでそれらが終わるのを待つ ④. 時間がかかる処理を実行 ① ② ③ ④ ①
  9. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ①. getSomeObjectを呼び出してオブジェクトを取得 ②. ①で取得したオブジェクトから必要な値を取得 ③. ①、②が非同期で実行されるのでそれらが終わるのを待つ ④. 時間がかかる処理を実行 ① ② ③ ④ ①、②も実際には時間がかからない処理だったし、一見すると問題なさそうに見えるが、 execHeavyTaskが短時間に複数のスレッドから呼び出され続けると、Await.resultで TimeoutExceptionが発生し、最終的にOutOfMemorryErrorが発生してプロセスが 落ちた
  10. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ① ② ③ ④ SomeObjectRepositoryEx ecutionContextを使って実 行される ①
  11. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ① ② ③ ④ SomeObjectRepositoryEx ecutionContextを使って実 行される HeavyTaskExecutionCont extを使って実行される ①
  12. class SomeObjectRepository @Inject()(implicit ec: SomeObjectRepositoryExecutionContext) { def getSomeObject: Future[SomeObject] =

    Future(new SomeObject) } class HeavyTaskService @Inject()(repository: SomeObjectRepository) (implicit ec: HeavyTaskExecutionContext) { def execHeavyTask: Future[Int] = { Future { val i = Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ① ② ③ ④ SomeObjectRepositoryEx ecutionContextを使って実 行される HeavyTaskExecutionCont extを使って実行される こちらのFutureの中身も HeavyTaskExecutionCont extを使って実行される ①
  13. def execHeavyTask: Future[Int] = { Future { val i =

    Await.result(repository.getSomeObject.map(obj => obj.someField), 100.millisecond) heavyTask(i) // 時間がかかる処理 i } } } ① ② ③ ④ Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread H2 obj.someField heavyTask HeavyTaskExecutionContextのスレッド SomeObjectRepositoryExecutionContextのス レッド
  14. つまり、execHeavyTaskの処理を完了するには SomeObjectRepositoryExecutionContextのスレッドが1つ (Thread S1) HeavyTaskExecutionContextのスレッドが2つ (Thread H1、Thread H2) が必要 Thread

    H1 Await.result Thread S1 reposiotry.getSomeObject Thread H2 obj.someField heavyTask HeavyTaskExecutionContextのスレッド SomeObjectRepositoryExecutionContextのス レッド
  15. Thread H3 Await.result Thread S1 reposiotry.getSomeObject Thread H2 obj.someField heavyTask

    もし、heavyTaskがなかなか終わらず、かつ、実行中にもう一度メソッドが呼び出された ら、 Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread H2 obj.someField heavyTask execHeavyTask 1回目 execHeavyTask 2回目
  16. Thread H3 Await.result Thread S1 reposiotry.getSomeObject Thread H2 obj.someField heavyTask

    もし、heavyTaskがなかなか終わらず、かつ、実行中にもう一度メソッドが呼び出された ら、この灰色になっているタイミングではHeavyTaskExecutionContextのスレッドが3つ 必要になる Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread H2 obj.someField heavyTask execHeavyTask 1回目 execHeavyTask 2回目
  17. Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread H? obj.someField heavyTask

    execHeavyTask n回目 obj.someFieldを実行できないまま時間が経過し、 Await.resultでTimeoutExceptionが発生 つまり今回のケースでは、repository.getSomeObjectとobj.somFieldに時間がかかっ たのではなく、 実行するためのリソース(スレッド)の確保に時間がかかった
  18. Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread H? obj.someField heavyTask

    execHeavyTask n回目 この説明だけだと処理に失敗するだけ なぜ、OutOfMemorryErrorになるのか? 注目するところは、obj.someFieldが実行される前に、Await.resultでTimeoutException が発生したとき、 obj.someFieldの実行はキャンセルされないということ
  19. import scalikejdbc._ class DBService @Inject()(implicit ec: DBExecutionContext) { def multiUpdateInTransaction:

    Future[List[Int]] = { DB.futureLocalTx { implicit session => val ids = sql"""select id from members""".map(rs => rs.long("id")).toList().apply() Future.sequence(ids.map { id => val newName = s"userId$id" Future(sql"""update members set name = $newName where id = $id""".update.apply()) }) } } } ※コードを簡略化するために、DBアクセス部分をScalikejdbcを使っています
  20. import scalikejdbc._ class DBService @Inject()(implicit ec: DBExecutionContext) { def multiUpdateInTransaction:

    Future[List[Int]] = { DB.futureLocalTx { implicit session => val ids = sql"""select id from members""".map(rs => rs.long("id")).toList().apply() Future.sequence(ids.map { id => val newName = s"userId$id" Future(sql"""update members set name = $newName where id = $id""".update.apply()) }) } } } ①. DBのトランザクションを開始し、引数の関数が返す Futureが完了したらトランザクションをコミット。 失敗したらロールバック ①
  21. import scalikejdbc._ class DBService @Inject()(implicit ec: DBExecutionContext) { def multiUpdateInTransaction:

    Future[List[Int]] = { DB.futureLocalTx { implicit session => val ids = sql"""select id from members""".map(rs => rs.long("id")).toList().apply() Future.sequence(ids.map { id => val newName = s"userId$id" Future(sql"""update members set name = $newName where id = $id""".update.apply()) }) } } } ①. DBのトランザクションを開始し、引数の関数が返す Futureが完了したらトランザクションをコミット。 失敗したらロールバック ① ②. DBから取得したIDのリストを使って、複数の updateを実行するFuture生成し、それを Future.sequenceを使って一つのFutureにまとめる 基のFutureの結果をまとめるだけなので、それぞれの Futureは別スレッドで実行される ②
  22. import scalikejdbc._ class DBService @Inject()(implicit ec: ExecutionContext) { def multiUpdateInTransaction:

    Future[List[Int]] = { DB.futureLocalTx { implicit session => val ids = sql"""select id from members""".map(rs => rs.long("id")).toList().apply() Future.sequence(ids.map { id => val newName = s"userId$id" Future(sql"""update members set name = $newName where id = $id""".update.apply()) }) } } } ①. DBのトランザクションを開始し、引数の関数が返す Futureが完了したらトランザクションをコミット。 失敗したらロールバック ① ② updateを複数のスレッドで並列実行できるので速くなりそうに見えるが、 短時間に複数のスレッドからこのメソッドが呼び出されると、DBのロック解放待ち状態の スレッドが大量にできて、しばらくこのExecutionContextは処理を実行できなくなる ②. DBから取得したIDのリストを使って、複数の updateを実行するFuture生成し、それを Future.sequenceを使って一つのFutureにまとめる 基のFutureの結果をまとめるだけなので、それぞれの Futureは別スレッドで実行される
  23. Thread A トランザクションの 開始 Thread E1 multiUpdateIn Transaction Thread En

    select実行 . . updateは非同期に実行されるので、 結果をまたずに Thread Aの処理は終了 レコードR1に対するupdate実行 Thread E2 . . multiUpdateInTranscationの呼び出しスレッド DBServiceExecutionContextのスレッド レコードR2に対するupdate実行 レコードRnに対するupdate実行
  24. Thread A トランザクション A を開始 Thread E1 Thread Aが実 行する

    multiUpdateIn Transaction Thread Em select実行 . . updateは非同期に実行されるので、 結果をまたずに Thread Aの処理は終了 . . Thread B トランザクション Bを 開始 Thread Em+1 Thread E2m select実行 . . updateは非同期に実行されるので、 結果をまたずに Thread Bの処理は終了 . . Thread Bが実 行する multiUpdateIn Transaction 複数が同時に呼び出されると、別スレッドが同じレコードに対してupdateが実行する レコードR1に対するupdate実行 レコードRmに対するupdate実行 レコードR1に対するupdate実行 レコードRmに対するupdate実行
  25. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em トランザクションに守られているため、同じレコードをupdateしようとすると排他ロックが かかり、スレッドがロック解放待ちになる Thread E2 Record R1 Record R2 . . . . update成功 ロック解放待ち . . Record Rn . . update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Record Rm
  26. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em ロック解放待ちのスレッド: ロックを持っているトランザクションがコミットかロールバックされるまで待ち続ける (タイ ムアウトエラーが発生して抜ける場合もある) Thread E2 Record R1 Record R2 . . . . update成功 ロック解放待ち . . Record Rn . . update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Record Rm
  27. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em updateに成功したスレッド: 処理が終わったので、ExecutionContextのQueueに入っている次の処理を実行 同じレコードを更新する処理もあるため、ロック解放待ちのスレッドが増える Thread E2 Record R1 Record R2 Record Rm . . . . update成功 ロック解放待ち . . Record Rn update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Thread E1 Thread E2 Record Rm+1 update成功 ロック解放待ち
  28. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em ロック解放待ちのスレッドが増え続け、トランザクションを終了させるための処理に必要 なスレッドがなくなる Thread E2 Record R1 Record R2 Record Rm . . . . update成功 ロック解放待ち . . Record Rn update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Thread E1 Thread E2 Record Rm+1 update成功 ロック解放待ち
  29. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em ロック解放待ちのスレッドが増え続け、トランザクションを終了させるための処理に必要 なスレッドがなくなる DBのロック解放待ちとスレッドの空き待ちでデッドロックしているような状態 Thread E2 Record R1 Record R2 Record Rm . . . . update成功 ロック解放待ち . . Record Rn update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Thread E1 Thread E2 Record Rm+1 update成功 ロック解放待ち
  30. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em このデッドロックしているような状態になると、DBもアプリケーションも検知できないの で、タイムアウトエラーなどで処理が終了するまで待ちづける Thread E2 Record R1 Record R2 Record Rm . . . . update成功 ロック解放待ち . . Record Rn update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Thread E1 Thread E2 Record Rm+1 update成功 ロック解放待ち
  31. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em もし、運よく二つのトランザクションでupdateの成功とロック解放待ちが混ざると、DB側 でデッドロックを検知し、即時ロールバックされる Thread E2 Record R1 Record R2 Record Rm . . . . update成功 ロック解放待ち . . Record Rn update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Thread E1 Thread E2 Record Rm+1 update成功 ロック解放待ち
  32. トランザクション B を使った 処理 トランザクション A を使った 処理 Thread E1

    Thread Em 実際のBacklogでは 同じレコードを触る別の処理同士で起こっていたため、DB側でデッドロックは発生しな かった Thread E2 Record R1 Record R2 Record Rm . . . . update成功 ロック解放待ち . . Record Rn update成功 update成功 Thread Em+1 Thread E2m Thread Em+2 ロック解放待ち ロック解放待ち Thread E1 Thread E2 Record Rm+1 update成功 ロック解放待ち
  33. Backlogを落としたコード その2 まとめ • DBが持つロックとアプリケーションの実行リソースの不足が起こすデッドロックのよ うな状態 ◦ DBだけではなく、何かしらのトランザクションのような仕組みを持つ外部サービスを呼び出すときに は同様のことが起こりそう • CPU使用率とかのメトリックを見ると、通常時より余裕があるように見える ◦

    アプリケーション側の DBアクセスを呼び出せるスレッドが減るため、 DBへのリクエスト量が減り、 DBの負荷が下がる ◦ ロック解放待ちのスレッドは Waiting状態のため、アプリケーションも CPUリソースの消費が減る • 感想 ◦ 「DBのロックとアプリケーションの実行リソースの不足がデッドロック」するというのは意識したことが なかったので、おもしろい現象だと思った
  34. class Pattern3Controller @Inject()(implicit ec: SlowWriterExecutionContext) extends InjectedController { def exec:

    Action[AnyContent] = Action { _ => Result( ResponseHeader(200, Map(CONTENT_DISPOSITION -> "attachment")), HttpEntity.Streamed(createSource, None, None) ) } private def createSource: Source[ByteString, _] = { StreamConverters.fromInputStream { () => val in = new PipedInputStream() val out = new PipedOutputStream(in) Future(Using(out)(o => someWrite(o))) in } }
  35. class Pattern3Controller @Inject()(implicit ec: SlowWriterExecutionContext) extends InjectedController { def exec:

    Action[AnyContent] = Action { _ => Result( ResponseHeader(200, Map(CONTENT_DISPOSITION -> "attachment")), HttpEntity.Streamed(createSource, None, None) ) } private def createSource: Source[ByteString, _] = { StreamConverters.fromInputStream { () => val in = new PipedInputStream() val out = new PipedOutputStream(in) Future(Using(out)(o => someWrite(o))) in } } ① ①. ストリーミングでレスポンスを返す 大きなファイルとかを返すときにメモリ消費を抑えることが できる ①
  36. class Pattern3Controller @Inject()(implicit ec: SlowWriterExecutionContext) extends InjectedController { def exec:

    Action[AnyContent] = Action { _ => Result( ResponseHeader(200, Map(CONTENT_DISPOSITION -> "attachment")), HttpEntity.Streamed(createSource, None, None) ) } private def createSource: Source[ByteString, _] = { StreamConverters.fromInputStream { () => val in = new PipedInputStream() val out = new PipedOutputStream(in) Future(Using(out)(o => someWrite(o))) in } } ① ①. ストリーミングでレスポンスを返す 大きなファイルとかを返すときにメモリ消費を抑えることが できる ② ②. InputStreamからAkkaのSourceを作る ①
  37. class Pattern3Controller @Inject()(implicit ec: SlowWriterExecutionContext) extends InjectedController { def exec:

    Action[AnyContent] = Action { _ => Result( ResponseHeader(200, Map(CONTENT_DISPOSITION -> "attachment")), HttpEntity.Streamed(createSource, None, None) ) } private def createSource: Source[ByteString, _] = { StreamConverters.fromInputStream { () => val in = new PipedInputStream() val out = new PipedOutputStream(in) Future(Using(out)(o => someWrite(o))) in } } ① ①. ストリーミングでレスポンスを返す 大きなファイルとかを返すときにメモリ消費を抑えることが できる ③ ③ ③. Shellとかのパイプラインのように InputStreamと OutputStreamをつなぐもので、Javaにもともとあるクラス ② ① ②. InputStreamからAkkaのSourceを作る ④ ④. 非同期でOutputStreamにストリーミングで返したいものを 書き込む
  38. class Pattern3Controller @Inject()(implicit ec: SlowWriterExecutionContext) extends InjectedController { def exec:

    Action[AnyContent] = Action { _ => Result( ResponseHeader(200, Map(CONTENT_DISPOSITION -> "attachment")), HttpEntity.Streamed(createSource, None, None) ) } private def createSource: Source[ByteString, _] = { StreamConverters.fromInputStream { () => val in = new PipedInputStream() val out = new PipedOutputStream(in) Future(Using(out)(o => someWrite(o))) in } } ① ①. ストリーミングでレスポンスを返す 大きなファイルとかを返すときにメモリ消費を抑えることが できる ③ ③ ③. Shellとかのパイプラインのように InputStreamと OutputStreamをつなぐもので、Javaにもともとあるクラス ② ① ②. InputStreamからAkkaのSourceを作る ④ ④. 非同期でOutputStreamにストリーミングで返したいものを 書き込む このアクションに対して短時間に複数のアクセスがあると、 PipedInputStreamのreadを呼び出すExecutionContextのスレッドと PipedOutputStreamのwriteを呼び出すExecutionContextのスレッドが それぞれ、write待ち/read待ちの状態になって、プロセスを再起動するまでこの機能は 使えない
  39. バッファの中身をすべて読み込んで、バッファが空になる Thread W1 Thread W2 Thread W3 Thread R1 Thread

    R2 Thread R3 write(read 待ち) write(read 待ち) write(read 待ち) read read read
  40. 結果、すべてのwrite側/read側のスレッドは待ち状態になる この状態になると、プロセスの再起動しか方法が残っていない Thread W1 Thread W2 Thread W3 Thread R1

    Thread R2 Thread R3 write(read 待ち) write(read 待ち) write(read 待ち) read (write待ち) read (write待ち) read (write待ち)
  41. 死活監視の仕組みと自分たちの使い方があっていない Thread W1 Thread W2 Thread W3 Thread R1 Thread

    R2 Thread R3 write(read 待ち) write(read 待ち) write(read 待ち) read (write待ち) read (write待ち) read (write待ち)