Slide 1

Slide 1 text

Backlogを落としたコード達

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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 } } ① ①. ストリーミングでレスポンスを返す 大きなファイルとかを返すときにメモリ消費を抑えることが できる ①

Slide 50

Slide 50 text

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を作る ①

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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待ち)

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

実は、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待ち)

Slide 64

Slide 64 text

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待ち)

Slide 65

Slide 65 text

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待ち)

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

まとめ

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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