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

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

[email protected]
PRO

December 05, 2020
Tweet

Other Decks in Technology

Transcript

  1. Backlogを落としたコード達

    View Slide

  2. 自己紹介
    谷本 陽介
    論理パズルとか好き
    SRE課のインフラわからない人
    最近の主な仕事
    ● BacklogのWebアプリケーション(JVMアプリケーション)で使っている古いライブラリ
    の仕分け、アップデート、置き換え
    ● BacklogのWebアプリケーションで問題があった時にHeap dump / Thread dump
    を見て原因を調べる
    とか

    View Slide

  3. 自己紹介
    谷本 陽介
    論理パズルとか好き
    SRE課のインフラわからない人
    最近の主な仕事
    ● BacklogのWebアプリケーション(JVMアプリケーション)で使っている古いライブラリ
    の仕分け、アップデート、置き換え
    ● BacklogのWebアプリケーションで問題があった時にHeap dump / Thread
    dumpを見て原因を調べる
    とか

    View Slide

  4. 今日話すこと
    ● Heap dump/Thread dumpはサービスを落とす原因になったコードを探すとき、い
    ろんなヒントが隠されていて論理パズルっぽくっておもしろい
    ● 「ああ、こんなパターンがあったんだ」って気づいた瞬間が特に論理パズルの楽しさ
    に似ていると思う
    ● 今回は、自分が調べていて気づけたパターンを簡略化したコードを紹介する
    ● おもしろいと思わなかったものは紹介しない
    ○ 例えば、DBから大量のデータを一度に取ってきて OutOfMemorryErrorとかは紹介しない

    View Slide

  5. Agenda
    ● Scalaの前提知識
    ● Backlogを落としたコード その1
    ● Backlogを落としたコード その2
    ● Backlogを落としたコード その3
    ● まとめ

    View Slide

  6. Scalaの前提知識
    今回出てくるコードはScalaのマルチスレッドまわりのコードばかりなので
    ● ExecutionContext
    ● Future
    を少しだけ説明

    View Slide

  7. Scalaの前提知識 - ExecutionContextとは
    ● JVMアプリケーションで非同期処理をする場合、あるスレッドから別のスレッドを
    作って、処理を渡すということをよくします
    ○ うまく使えば、並列に実行できるので効率よく処理ができる
    ● しかし、スレッドを毎回作っているとコストがかかったり、こういうタイプの処理はこの
    スレッドのグループにやらせるとか役割を与えたいことがある
    ● スレッドプールという作ったスレッドをためたり、グルーピングするためのものがある
    ● ExecutionContext はスレッドプールに処理をさせたり、させる処理をQueueにため
    て管理したりするもの
    (※ これはなんとなく理解してもうための説明であり、正確ではありません)

    View Slide

  8. 前提知識 - Futureとは
    ExecutionContextを使い非同期処理を行ったり、その結果を保持したりする
    こんな感じに書くと簡単に非同期処理がかける
    Future {
    println("Hello world")
    }(executionContext )
    executionContextの定義時にimplictをつけると暗黙的に引数としてわたるため、よりシ
    ンプルな見た目でかける
    Future { println("Hello world") }
    (※ これもなんとなく理解してもうための説明であり、正確ではありません)

    View Slide

  9. Backlogを落としたコード その1

    View Slide

  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
    }
    }
    }

    View Slide

  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
    }
    }
    }
    ①. getSomeObjectを呼び出してオブジェクトを取得


    View Slide

  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
    }
    }
    }
    ①. getSomeObjectを呼び出してオブジェクトを取得
    ②. ①で取得したオブジェクトから必要な値を取得
    ① ②

    View Slide

  13. 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を呼び出してオブジェクトを取得
    ②. ①で取得したオブジェクトから必要な値を取得
    ③. ①、②が非同期で実行されるのでそれらが終わるのを待つ
    ① ②


    View Slide

  14. 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を呼び出してオブジェクトを取得
    ②. ①で取得したオブジェクトから必要な値を取得
    ③. ①、②が非同期で実行されるのでそれらが終わるのを待つ
    ④. 時間がかかる処理を実行
    ① ②



    View Slide

  15. 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が発生してプロセスが
    落ちた

    View Slide

  16. Backlogを落としたコード その1
    解説

    View Slide

  17. 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を使って実
    行される

    View Slide

  18. 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を使って実行される

    View Slide

  19. 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を使って実行される

    View Slide

  20. 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のス
    レッド

    View Slide

  21. つまり、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のス
    レッド

    View Slide

  22. 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回目

    View Slide

  23. 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回目

    View Slide

  24. 短時間に何度も呼び出されると、heavyTaskを実行中のスレッドが大量にできる
    HeavyTaskExecutionContextのスレッドが最大数に達し、obj.someFieldを実行するス
    レッドがなくなるとどうなるか?
    Thread H1 Await.result
    Thread S1 reposiotry.getSomeObject
    Thread H? obj.someField
    heavyTask
    execHeavyTask
    n回目

    View Slide

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

    View Slide

  26. Thread H1 Await.result
    Thread S1 reposiotry.getSomeObject
    Thread H? obj.someField
    heavyTask
    execHeavyTask
    n回目
    この説明だけだと処理に失敗するだけ
    なぜ、OutOfMemorryErrorになるのか?
    注目するところは、obj.someFieldが実行される前に、Await.resultでTimeoutException
    が発生したとき、
    obj.someFieldの実行はキャンセルされないということ

    View Slide

  27. obj.someFieldを実行する処理が、ExecutionContextのQueueに入っているから、ス
    レッドに空きができると実行される
    Thread H1 Await.result
    Thread S1 reposiotry.getSomeObject
    Thread H? obj.someField
    heavyTask
    execHeavyTask
    n回目

    View Slide

  28. obj.someFieldを実行する処理が、ExecutionContextのQueueに入っているから、ス
    レッドに空きができると実行される
    つまり、obj.someFieldが実行されるまで、objはQueueから参照されているためGC対
    象にならない
    このため、メソッドを呼び出し続けられると、obj.someFieldを実行する処理がどんど
    んQueueにためられ、最終的にOutOfMemorryErrorになる
    Thread H1 Await.result
    Thread S1 reposiotry.getSomeObject
    Thread H? obj.someField
    heavyTask
    execHeavyTask
    n回目

    View Slide

  29. Backlogを落としたコード その1 まとめ
    ● TimeoutExceptionはリソース(スレッド)の確保に時間がかかっていたことが原因
    ● 非同期な処理は実行開始が遅延することがあり、その結果、処理に関連するメモリ
    の解放も遅延しOutOfMemorryErrorが発生
    ● 感想
    ○ 「スレッドの確保に時間がかかる」というのも、「 Queueに入っている未処理のタスクがメモリを圧迫
    する」というのも意識していなかったので、おもしろい現象だと思った

    View Slide

  30. Backlogを落としたコード その2

    View Slide

  31. 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を使っています

    View Slide

  32. 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が完了したらトランザクションをコミット。
    失敗したらロールバック

    View Slide

  33. 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は別スレッドで実行される

    View Slide

  34. 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は別スレッドで実行される

    View Slide

  35. Backlogを落としたコード その2
    解説

    View Slide

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

    View Slide

  37. 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実行

    View Slide

  38. トランザクション 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

    View Slide

  39. トランザクション 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

    View Slide

  40. トランザクション 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成功 ロック解放待ち

    View Slide

  41. トランザクション 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成功 ロック解放待ち

    View Slide

  42. トランザクション 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成功 ロック解放待ち

    View Slide

  43. トランザクション 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成功 ロック解放待ち

    View Slide

  44. トランザクション 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成功
    ロック解放待ち

    View Slide

  45. トランザクション 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成功
    ロック解放待ち

    View Slide

  46. Backlogを落としたコード その2 まとめ
    ● DBが持つロックとアプリケーションの実行リソースの不足が起こすデッドロックのよ
    うな状態
    ○ DBだけではなく、何かしらのトランザクションのような仕組みを持つ外部サービスを呼び出すときに
    は同様のことが起こりそう
    ● CPU使用率とかのメトリックを見ると、通常時より余裕があるように見える
    ○ アプリケーション側の DBアクセスを呼び出せるスレッドが減るため、 DBへのリクエスト量が減り、
    DBの負荷が下がる
    ○ ロック解放待ちのスレッドは Waiting状態のため、アプリケーションも CPUリソースの消費が減る
    ● 感想
    ○ 「DBのロックとアプリケーションの実行リソースの不足がデッドロック」するというのは意識したことが
    なかったので、おもしろい現象だと思った

    View Slide

  47. Backlogを落としたコード その3

    View Slide

  48. 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
    }
    }

    View Slide

  49. 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
    }
    }

    ①. ストリーミングでレスポンスを返す
    大きなファイルとかを返すときにメモリ消費を抑えることが
    できる

    View Slide

  50. 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を作る

    View Slide

  51. 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にストリーミングで返したいものを
    書き込む

    View Slide

  52. 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待ちの状態になって、プロセスを再起動するまでこの機能は
    使えない

    View Slide

  53. Backlogを落としたコード その3
    解説

    View Slide

  54. Buffer
    ● PipedInputStream/PipedOutputStreamは無尽蔵にwriteできるものではなく読み
    書き用のバッファを持っている
    Thread W1
    Thread R1

    View Slide

  55. ● PipedInputStream/PipedOutputStreamは無尽蔵にwriteできるものではなく読み
    書き用のバッファを持っている
    ● writeを呼び出すとバッファに書き込まれ、バッファがいっぱいになると、他からread
    されるまで待つ
    Buffer
    Thread W1
    write
    Thread R1

    View Slide

  56. ● PipedInputStream/PipedOutputStreamは無尽蔵にwriteできるものではなく読み
    書き用のバッファを持っている
    ● writeを呼び出すとバッファに書き込まれ、バッファがいっぱいになると、他からread
    されるまで待つ
    ● readを呼び出すとバッファから読み込んで読み込んだ分をバッファから消す
    ● バッファが空の状態でreadを呼び出すと、他からwriteされるまで待つ
    Buffer
    Thread W1
    Thread R1
    read
    Buffer
    Thread W1
    write
    Thread R1

    View Slide

  57. ● PipedInputStream/PipedOutputStreamは無尽蔵にwriteできるものではなく読み
    書き用のバッファを持っている
    ● writeを呼び出すとバッファに書き込まれ、バッファがいっぱいになると、他からread
    されるまで待つ
    ● readを呼び出すとバッファから読み込んで読み込んだ分をバッファから消す
    ● バッファが空の状態でreadを呼び出すと、他からwriteされるまで待つ
    Buffer
    Thread W1
    Thread R1
    read
    Buffer
    Thread W1
    write
    Thread R1

    View Slide

  58. ほぼ同時に複数のリクエストが来た場合はどうなるか?
    Thread W1
    Thread W2
    Thread W3
    Thread R1
    Thread R2
    Thread R3
    write
    write
    write

    View Slide

  59. writeしてバッファがいっぱいになったが、書き込み対象のデータがある程度の大きさだ
    と、まだ書き込む必要がありread待ちになる
    Thread W1
    Thread W2
    Thread W3
    Thread R1
    Thread R2
    Thread R3
    write(read 待ち)
    write(read 待ち)
    write(read 待ち)

    View Slide

  60. バッファの中身をすべて読み込んで、バッファが空になる
    Thread W1
    Thread W2
    Thread W3
    Thread R1
    Thread R2
    Thread R3
    write(read 待ち)
    write(read 待ち)
    write(read 待ち)
    read
    read
    read

    View Slide

  61. write側のスレッドは、バッファが空いたため、またwriteしてread待ちになる
    read側のスレッドは、他にもInputStreamがあると他からもreadする実装
    = スレッドが一つにInputStreamに専任ではない
    Thread W1
    Thread W2
    Thread W3
    Thread R1
    Thread R2
    Thread R3
    write(read 待ち)
    write(read 待ち)
    write(read 待ち)
    read (write待ち)
    read (write待ち)
    read (write待ち)

    View Slide

  62. 結果、すべての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待ち)

    View Slide

  63. 実は、PipedInputStream/PipedOutputStreamは、相方のスレッドが生きているか監視
    するコードになっている
    ● write時には、このペアに対してreadをしたことがあるスレッドが存在し、そのスレッ
    ドが停止していた場合にエラーになる
    ● read時には、このペアに対してwriteをしたことがあるスレッドが存在し、そのスレッ
    ドが停止していた場合にエラーになる
    Thread W1
    Thread W2
    Thread W3
    Thread R1
    Thread R2
    Thread R3
    write(read 待ち)
    write(read 待ち)
    write(read 待ち)
    read (write待ち)
    read (write待ち)
    read (write待ち)

    View Slide

  64. write時にエラーは起こるのか?
    今回の場合、readしたスレッドはすべて他のInputStreamを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待ち)

    View Slide

  65. read時にエラーは起こるのか?
    今回の場合、readしようとしたInputStreamはまだwriteされていない
    つまり、writeをしたスレッドは存在しないのでエラーにならない
    Thread W1
    Thread W2
    Thread W3
    Thread R1
    Thread R2
    Thread R3
    write(read 待ち)
    write(read 待ち)
    write(read 待ち)
    read (write待ち)
    read (write待ち)
    read (write待ち)

    View Slide

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

    View Slide

  67. Backlogを落としたコード その3
    まとめ

    View Slide

  68. Backlogを落としたコード その3 まとめ
    ● PipedInputStream/PipedOutputStreamのread側のスレッドの空き待ちと、write側
    のスレッドの空き待ちというデッドロック状態
    ● ちなみに、StreamConverters.asOutputStreamというOutputStreamからSourceを
    作るメソッドがあるが、わざわざPipedInputStream/PipedOutputStreamを使って
    いたのは実装当時はバグがあったため

    View Slide

  69. まとめ

    View Slide

  70. まとめ
    ● リソース(スレッド)の確保に時間がかかることもある
    ● 非同期呼び出しは遅延することがあり、その分メモリ占有時間が長くなることがある
    ● スレッド数の制限同士や他のロックでデッドロック状態になることがある
    個人的には、マルチスレッドプログラミングはシングルスレッドと比べると複雑性があが
    り、問題の特定という論理パズルの難易度があがるのでたのしい
    気づかなかった視点に気づけた時の嬉しさを感じれる人はぜひ、自分たちのサービスで
    起こった問題をじっくり読んでみてください

    View Slide

  71. まとめ
    もし、JVMを使っているけど、Heap dump/Thread dumpの読み方がよくわからないとい
    う人がいたら、去年書いたブログがあるので参考になると嬉しいです
    https://backlog.com/ja/blog/java-virtual-machine-system-performance-survey/

    View Slide