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

3e77f9dbec6a87756d1dbdddab283aee?s=128

Nulab Inc.
PRO

December 05, 2020
Tweet

Transcript

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

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

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

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

    今回は、自分が調べていて気づけたパターンを簡略化したコードを紹介する • おもしろいと思わなかったものは紹介しない ◦ 例えば、DBから大量のデータを一度に取ってきて OutOfMemorryErrorとかは紹介しない
  5. Agenda • Scalaの前提知識 • Backlogを落としたコード その1 • Backlogを落としたコード その2 • Backlogを落としたコード その3 •

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

  7. Scalaの前提知識 - ExecutionContextとは • JVMアプリケーションで非同期処理をする場合、あるスレッドから別のスレッドを 作って、処理を渡すということをよくします ◦ うまく使えば、並列に実行できるので効率よく処理ができる • しかし、スレッドを毎回作っているとコストがかかったり、こういうタイプの処理はこの

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

    ) executionContextの定義時にimplictをつけると暗黙的に引数としてわたるため、よりシ ンプルな見た目でかける Future { println("Hello world") } (※ これもなんとなく理解してもうための説明であり、正確ではありません)
  9. Backlogを落としたコード その1

  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 } } }
  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を呼び出してオブジェクトを取得 ① ①
  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を呼び出してオブジェクトを取得 ②. ①で取得したオブジェクトから必要な値を取得 ① ② ①
  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を呼び出してオブジェクトを取得 ②. ①で取得したオブジェクトから必要な値を取得 ③. ①、②が非同期で実行されるのでそれらが終わるのを待つ ① ② ③ ①
  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を呼び出してオブジェクトを取得 ②. ①で取得したオブジェクトから必要な値を取得 ③. ①、②が非同期で実行されるのでそれらが終わるのを待つ ④. 時間がかかる処理を実行 ① ② ③ ④ ①
  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が発生してプロセスが 落ちた
  16. Backlogを落としたコード その1 解説

  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を使って実 行される ①
  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を使って実行される ①
  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を使って実行される ①
  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のス レッド
  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のス レッド
  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回目
  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回目
  24. 短時間に何度も呼び出されると、heavyTaskを実行中のスレッドが大量にできる HeavyTaskExecutionContextのスレッドが最大数に達し、obj.someFieldを実行するス レッドがなくなるとどうなるか? Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread

    H? obj.someField heavyTask execHeavyTask n回目
  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に時間がかかっ たのではなく、 実行するためのリソース(スレッド)の確保に時間がかかった
  26. Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread H? obj.someField heavyTask

    execHeavyTask n回目 この説明だけだと処理に失敗するだけ なぜ、OutOfMemorryErrorになるのか? 注目するところは、obj.someFieldが実行される前に、Await.resultでTimeoutException が発生したとき、 obj.someFieldの実行はキャンセルされないということ
  27. obj.someFieldを実行する処理が、ExecutionContextのQueueに入っているから、ス レッドに空きができると実行される Thread H1 Await.result Thread S1 reposiotry.getSomeObject Thread H?

    obj.someField heavyTask execHeavyTask n回目
  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回目
  29. Backlogを落としたコード その1 まとめ • TimeoutExceptionはリソース(スレッド)の確保に時間がかかっていたことが原因 • 非同期な処理は実行開始が遅延することがあり、その結果、処理に関連するメモリ の解放も遅延しOutOfMemorryErrorが発生 • 感想 ◦ 「スレッドの確保に時間がかかる」というのも、「

    Queueに入っている未処理のタスクがメモリを圧迫 する」というのも意識していなかったので、おもしろい現象だと思った
  30. Backlogを落としたコード その2

  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を使っています
  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が完了したらトランザクションをコミット。 失敗したらロールバック ①
  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は別スレッドで実行される ②
  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は別スレッドで実行される
  35. Backlogを落としたコード その2 解説

  36. Thread A トランザクションの 開始 Thread E1 multiUpdateIn Transaction Thread En

    select実行 . . updateは非同期に実行されるので、 結果をまたずに Thread Aの処理は終了 レコードR1に対するupdate実行 Thread E2 . . multiUpdateInTranscationの呼び出しスレッド DBServiceExecutionContextのスレッド レコードR2に対するupdate実行 レコードRnに対するupdate実行
  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実行
  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
  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
  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成功 ロック解放待ち
  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成功 ロック解放待ち
  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成功 ロック解放待ち
  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成功 ロック解放待ち
  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成功 ロック解放待ち
  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成功 ロック解放待ち
  46. Backlogを落としたコード その2 まとめ • DBが持つロックとアプリケーションの実行リソースの不足が起こすデッドロックのよ うな状態 ◦ DBだけではなく、何かしらのトランザクションのような仕組みを持つ外部サービスを呼び出すときに は同様のことが起こりそう • CPU使用率とかのメトリックを見ると、通常時より余裕があるように見える ◦

    アプリケーション側の DBアクセスを呼び出せるスレッドが減るため、 DBへのリクエスト量が減り、 DBの負荷が下がる ◦ ロック解放待ちのスレッドは Waiting状態のため、アプリケーションも CPUリソースの消費が減る • 感想 ◦ 「DBのロックとアプリケーションの実行リソースの不足がデッドロック」するというのは意識したことが なかったので、おもしろい現象だと思った
  47. Backlogを落としたコード その3

  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 } }
  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 } } ① ①. ストリーミングでレスポンスを返す 大きなファイルとかを返すときにメモリ消費を抑えることが できる ①
  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を作る ①
  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にストリーミングで返したいものを 書き込む
  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待ちの状態になって、プロセスを再起動するまでこの機能は 使えない
  53. Backlogを落としたコード その3 解説

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

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

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

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

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

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

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

    R2 Thread R3 write(read 待ち) write(read 待ち) write(read 待ち) read read read
  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待ち)
  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待ち)
  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待ち)
  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待ち)
  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待ち)
  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待ち)
  67. Backlogを落としたコード その3 まとめ

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

  69. まとめ

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

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