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

サーバーサイドでのKotlin Coroutines

サーバーサイドでのKotlin Coroutines

2022年2月10日(木) 「Server-Side Kotlin Meetup vol.1」の登壇資料です。

D1531f9547e24397c7e85881fac03096?s=128

Takehata Naoto

February 10, 2022
Tweet

More Decks by Takehata Naoto

Other Decks in Programming

Transcript

  1. サーバーサイドでの Kotlin Coroutines 2022年2月10日 Server-Side Kotlin Meetup vol.1 竹端 尚人

  2. 自己紹介

  3. 概要 竹端 尚人 主にバックエンドエンジニア 株式会社justInCaseTechnlogies 技術顧問 アスクル株式会社 技術顧問 Twitter: @n_takehata

    • 2006.04 公務員 • 2007.12 SES • 2014.04 株式会社アプリボット(Kotlinを始める) • 2020.06 株式会社ZOZOテクノロジーズ • 2020.12 フリーランス(現在)
  4. 登壇、執筆 • CEDEC 2018、2019登壇 • Software Design 2019年2月号〜4月号で短期連載 「サーバーサイド開発の品質を向上させる Java→Kotlin

    移行のススメ」執筆 • 2021年4月 書籍「Kotlin サーバーサイドプログラミング 実践開発」を出版
  5. 本日の内容

  6. サーバーサイドでのKotlin Coroutinesの 使いどころについて

  7. • Androidアプリに比べると話題に上がることが少ない(印象) • サーバーサイドではブロッキングが必ずしもクリティカルで はない • それでもパフォーマンス向上に寄与することは確実で、使用 しているプロジェクトもある サーバーサイドでの Kotlin

    Coroutines事情
  8. サーバーサイドでの利用シーンや実装について 紹介していきます

  9. アジェンダ 1. 改めてKotlin Coroutinesの挙動 2. HTTP通信での使用 3. gRPC通信での使用 4. データベース通信での使用

    5. まとめ
  10. 1. 改めてKotlin Coroutinesの挙動

  11. 次のコードを実行するとどういう順番で メッセージが出力がされるでしょう?

  12. coroutineScope { launch { for (i in 1..5) { println("Hello

    A $i thread:${Thread.currentThread()}") } } launch { for (i in 1..5) { println("Hello B $i thread:${Thread.currentThread()}") } } launch { for (i in 1..5) { println("Hello C $i thread:${Thread.currentThread()}") } } }
  13. 実行結果

  14. Hello A 1 thread:Thread[main,5,main] Hello A 2 thread:Thread[main,5,main] Hello A

    3 thread:Thread[main,5,main] Hello A 4 thread:Thread[main,5,main] Hello A 5 thread:Thread[main,5,main] Hello B 1 thread:Thread[main,5,main] Hello B 2 thread:Thread[main,5,main] ・・・ Hello C 1 thread:Thread[main,5,main] Hello C 2 thread:Thread[main,5,main] ・・・ Aの1〜5→Bの1〜5→Cの1〜5 の順番で出力される
  15. つまり直列で動いている

  16. ループの中で中断関数を呼ぶ

  17. runBlocking { launch { for (i in 1..5) { delay(1)

    // 中断 println("Hello A $i thread:${Thread.currentThread()}") } } launch { for (i in 1..5) { delay(1) // 中断 println("Hello B $i thread:${Thread.currentThread()}") } } launch { for (i in 1..5) { delay(1) // 中断 println("Hello C $i thread:${Thread.currentThread()}") } } }
  18. Hello A 1 thread:Thread[main,5,main] Hello B 1 thread:Thread[main,5,main] Hello C

    1 thread:Thread[main,5,main] Hello A 2 thread:Thread[main,5,main] Hello B 2 thread:Thread[main,5,main] Hello C 2 thread:Thread[main,5,main] Hello A 3 thread:Thread[main,5,main] Hello B 3 thread:Thread[main,5,main] ・・・ Aの1→Bの1→Cの1→Aの2→Bの2・・・ と出力される
  19. runBlocking { launch { for (i in 1..5) { delay(1)

    // 中断 println("Hello A $i thread:${Thread.currentThread()}") } } launch { for (i in 1..5) { delay(1) // 中断 println("Hello B $i thread:${Thread.currentThread()}") } } launch { for (i in 1..5) { delay(1) // 中断 println("Hello C $i thread:${Thread.currentThread()}") } } } ①中断して次のlaunchへ ②中断して次のlaunchへ ③中断して次のlaunchへ ④復帰して出力 ⑥復帰して出力 ⑧復帰して出力 ⑤中断して次のlaunchへ ⑦中断して次のlaunchへ
  20. • 中断をされなければただの直列処理になる • 外部I/Oなど処理以外の待ちが発生する時に中断してくれると次の処理 が進められる

  21. 外部I/Oの処理でCoroutinesを使うパターンを紹介していきます

  22. 前提: ここから先のコードはSpring WebFluxで呼び出して動かしてます (呼び出し元のRouter FunctionsやControllerは省略してます)

  23. 2. HTTP通信での使用

  24. Spring WebFluxのWebClientでの実装

  25. val webClient = WebClient.builder().build() coroutineScope { println("${LocalDateTime.now()} start coroutines. thread:${Thread.currentThread()}")

    val deferreds = listOf( async { println("${LocalDateTime.now()} start Google. thread:${Thread.currentThread()}") val response = webClient.get().uri("https://www.google.com/") .accept(MediaType.TEXT_HTML) .retrieve() .awaitBody<String>() println("${LocalDateTime.now()} end Google. thread:${Thread.currentThread()}") Regex("""<title>.*</title>""").find(response)?.value }, async { println("${LocalDateTime.now()} start Yahoo. thread:${Thread.currentThread()}") val response = webClient.get().uri("https://www.yahoo.co.jp/") // 省略・・・ }, async { println("${LocalDateTime.now()} start Bing. thread:${Thread.currentThread()}") val response = webClient.get().uri("https://www.bing.com/") // 省略・・・ } ) println(deferreds.awaitAll()) } ①WebClientを生成 ②URLを設定してGETを実行 ③HTMLからtitleを正規表現で取得 ④全ての実行が終わったら awaitAllで取得して出力
  26. 実行結果

  27. 2022-02-08T08:30:46.381236 start coroutines. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:30:46.387398 start Google. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:30:46.978801 start

    Yahoo. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:30:46.983323 start Bing. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:30:47.210427 end Yahoo. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:30:47.239416 end Google. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:30:47.308506 end Bing. thread:Thread[reactor-http-nio-2,5,main] [<title>Google</title>, <title>Yahoo! JAPAN</title>, <title>Bing</title>] • 各asyncブロックがstartし、順に終了している • webClientの実行のところで中断されているのがわかる
  28. coroutineScope { val deferreds = listOf( async { println("${LocalDateTime.now()} start

    Google. thread:${Thread.currentThread()}") val response = restTemplate.getForObject<String>("https://www.google.com/") println("${LocalDateTime.now()} end Google. thread:${Thread.currentThread()}") Regex("""<title>.*</title>""").find(response)?.value }, RestTemplateを使うとブロッキングになってしまうので注意
  29. 3. gRPC通信での使用

  30. 呼び出されるgRPCサーバー

  31. @GRpcService class GreeterGrpcService : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun hello(request:

    HelloRequest) = HelloResponse.newBuilder() .setText("Hello ${request.name}") .build() } nameをリクエストで受け取って、メッセージを作って textとして返すだけの処理
  32. grpc-kotlinでの実装

  33. val channel = ManagedChannelBuilder.forAddress("localhost", 6565) .usePlaintext() .build() val stub =

    GreeterGrpcKt.GreeterCoroutineStub(channel) coroutineScope { println("${LocalDateTime.now()} start coroutines. thread:${Thread.currentThread()}") val deferreds = listOf( async { val request = HelloRequest.newBuilder().setName("Kotlin").build() println("${LocalDateTime.now()} start Kotlin thread:${Thread.currentThread()}") val response = stub.hello(request) println("${LocalDateTime.now()} end Kotlin thread:${Thread.currentThread()}") response.text }, async { val request = HelloRequest.newBuilder().setName("Java").build() // 省略・・・ }, async { val request = HelloRequest.newBuilder().setName("Scala").build() // 省略・・・ } ) println(deferreds.awaitAll()) } ①gRPCのStubを生成 ②リクエストを実行 ③レスポンスからtextを取得 ④全ての実行が終わったら awaitAllで取得して出力
  34. 実行結果

  35. 2022-02-08T08:35:20.178803 start coroutines. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:35:20.186422 start Kotlin thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:35:20.207198 start

    Java thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:35:20.212305 start Scala thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:35:20.422334 end Java thread:Thread[grpc-default-executor-0,5,main] 2022-02-08T08:35:20.422439 end Scala thread:Thread[grpc-default-executor-1,5,main] 2022-02-08T08:35:20.422513 end Kotlin thread:Thread[grpc-default-executor-2,5,main] [Hello Kotlin, Hello Java, Hello scala] • 各asyncブロックがstartし、順に終了している • 実行スレッドはgrpc-default-executorに切り替わる(grpc-kotlinが依存しているgrpc-javaの影響)
  36. 4. データベース通信での使用

  37. テストデータの作成

  38. CREATE TABLE user(id int PRIMARY KEY, name varchar(32), age int);

    INSERT INTO user VALUES(1, "Kotlin", 10), (2, "Java", 20), (3, "Scala", 8);
  39. JDBCを使用した場合 最もよく使用される ブロッキングのデータベースアクセス (ORMとしてMyBatisを使用)

  40. ここまでと同様にcoroutineScopeで実装

  41. coroutineScope { println("${LocalDateTime.now()} start coroutines. thread:${Thread.currentThread()}") val deferreds = listOf(

    async { println("${LocalDateTime.now()} start Kotlin thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(1) println("${LocalDateTime.now()} end Kotlin thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Java thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(2) println("${LocalDateTime.now()} end Java thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Scala thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(3) println("${LocalDateTime.now()} end Scala thread:${Thread.currentThread()}") result?.name } ) println(deferreds.awaitAll()) } ①MyBatisのMapperで主キー検索を実行 ②実行結果からnameを取得 ③全ての実行が終わったら awaitAllで取得して出力
  42. 実行結果

  43. 2022-02-08T08:39:13.124363 start coroutines. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:39:13.130277 start Kotlin thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:39:13.618934 end

    Kotlin thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:39:13.620109 start Java thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:39:13.624218 end Java thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:39:13.624440 start Scala thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:39:13.627268 end Scala thread:Thread[reactor-http-nio-2,5,main] [Kotlin, Java, Scala] • 各asyncブロックが直列で実行されている • JDBCはデータベースへのアクセス中も中断してくれないため
  44. withContextでContextを切り替える

  45. withContext(Dispatchers.IO) { val deferreds = listOf( async { println("${LocalDateTime.now()} start

    Kotlin thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(1) println("${LocalDateTime.now()} end Kotlin thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Java thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(2) println("${LocalDateTime.now()} end Java thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Scala thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(3) println("${LocalDateTime.now()} end Scala thread:${Thread.currentThread()}") result?.name } ) println(deferreds.awaitAll()) }
  46. withContext(Dispatchers.IO) { val deferreds = listOf( async { println("${LocalDateTime.now()} start

    Kotlin thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(1) println("${LocalDateTime.now()} end Kotlin thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Java thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(2) println("${LocalDateTime.now()} end Java thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Scala thread:${Thread.currentThread()}") val result = userMapper.selectByPrimaryKey(3) println("${LocalDateTime.now()} end Scala thread:${Thread.currentThread()}") result?.name } ) println(deferreds.awaitAll()) }
  47. 実行結果

  48. 2022-02-08T08:43:16.225768 start coroutines. thread:Thread[DefaultDispatcher-worker-1,5,main] 2022-02-08T08:43:16.228337 start Kotlin thread:Thread[DefaultDispatcher-worker-3,5,main] 2022-02-08T08:43:16.232434 start

    Scala thread:Thread[DefaultDispatcher-worker-4,5,main] 2022-02-08T08:43:16.232335 start Java thread:Thread[DefaultDispatcher-worker-2,5,main] 2022-02-08T08:43:16.729098 end Kotlin thread:Thread[DefaultDispatcher-worker-3,5,main] 2022-02-08T08:43:16.734883 end Scala thread:Thread[DefaultDispatcher-worker-4,5,main] 2022-02-08T08:43:16.738588 end Java thread:Thread[DefaultDispatcher-worker-2,5,main] [Kotlin, Java, Scala] Contextを切り替えて各asyncブロックが並列で実行されている
  49. R2DBCを使用した場合 ノンブロッキングでの データベースアクセス

  50. R2DBCとは? • Reactive Relational Database Connectivityの略 • RDBに対するリアクティブなAPIを提供する • これを使うとノンブロッキングでRDBへのアクセスを実現で

    きる
  51. implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") runtimeOnly("dev.miku:r2dbc-mysql") Gradleに依存関係の追加 Spring Bootのstarterと、MySQLのドライバーを追加する

  52. interface UserRepository : CoroutineCrudRepository<User, Int> { } データクラスとRepositoryの作成 data class

    User( @Id val id: Int, val name: String, val age: Int ) • テーブルに構造に紐づいたデータクラスを作成 • CoroutineCrudRepositoryを実装したRepositoryを作成(これで各 種アクセスの関数が使える )
  53. CoroutineCrudRepositoryの中身

  54. /** * Retrieves an entity by its id. * *

    @param id must not be null. * @return [Mono] emitting the entity with the given id or empty if none found. * @throws IllegalArgumentException in case the given id is null. */ suspend fun findById(id: ID): T? /** * Returns whether an entity with the given id exists. * * @param id must not be null. * @return true if an entity with the given id exists, false otherwise. * @throws IllegalArgumentException in case the given id is null. */ suspend fun existsById(id: ID): Boolean /** * Deletes the entity with the given id. * * @param id must not be null. * @throws IllegalArgumentException in case the given id is null. */ suspend fun deleteById(id: ID)
  55. 各種データベース操作が suspending functionで定義されている

  56. coroutineScope { println("${LocalDateTime.now()} start coroutines. thread:${Thread.currentThread()}") val deferreds = listOf(

    async { println("${LocalDateTime.now()} start Kotlin thread:${Thread.currentThread()}") val result = userRepository.findById(1) println("${LocalDateTime.now()} end Kotlin thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Java thread:${Thread.currentThread()}") val result = userRepository.findById(2) println("${LocalDateTime.now()} end Java thread:${Thread.currentThread()}") result?.name }, async { println("${LocalDateTime.now()} start Scala thread:${Thread.currentThread()}") val result = userRepository.findById(3) println("${LocalDateTime.now()} end Scala thread:${Thread.currentThread()}") result?.name } ) println(deferreds.awaitAll()) } ①Repositoryで主キー検索を実行 ②実行結果からnameを取得 ③全ての実行が終わったら awaitAllで取得して出力
  57. 2022-02-08T08:46:28.149196 start coroutines. thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:46:28.156509 start Kotlin thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:46:28.448620 start

    Java thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:46:28.504981 start Scala thread:Thread[reactor-http-nio-2,5,main] 2022-02-08T08:46:28.941121 end Kotlin thread:Thread[reactor-tcp-nio-2,5,main] 2022-02-08T08:46:28.982010 end Java thread:Thread[reactor-tcp-nio-2,5,main] 2022-02-08T08:46:29.017775 end Scala thread:Thread[reactor-tcp-nio-2,5,main] [Kotlin, Java, Scala] • 各asyncブロックがstartし、順に終了している • 実行スレッドはR2DBCの関数実行後はreactor-tcp-nioに切り替わる
  58. ノンブロッキングの データベースアクセスを実現

  59. interface UserRepository : CoroutineCrudRepository<User, Int> { suspend fun findByName(name: String):

    List<User> @Query( """ SELECT * FROM user ORDER BY age LIMIT 1 """ ) suspend fun findMostYoung(): User } • シンプルなクエリは、 findByNameなど機能とカラム名の命名規則で作れる • @Queryを使うことで自由にクエリを定義することもできる • これもノンブロッキングになる
  60. JOOQからR2DBCを利用する方法も • ORMのJOOQがバージョン3.15からR2DBCに対応 https://blog.jooq.org/reactive-sql-with-jooq-3-15-and-r2dbc/ • ただし、Spring Boot Starter JOOQの最新(現在2.6.3)は JOOQ3.14系に依存しているため、別途Dependenciesの

    追加が必要
  61. 5. まとめ

  62. • HTTP通信、gRPC通信、データベース通信など時間のかか るI/O処理が使いどころ • JDBCなどブロッキングな処理を含む場合は、Contextの切 り替えが必要 • R2DBCを使えばデータベースアクセスもノンブロッキングに できる

  63. ご清聴ありがとうございました