Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

自己紹介

Slide 3

Slide 3 text

概要 竹端 尚人 主にバックエンドエンジニア 株式会社justInCaseTechnlogies 技術顧問 アスクル株式会社 技術顧問 Twitter: @n_takehata ● 2006.04 公務員 ● 2007.12 SES ● 2014.04 株式会社アプリボット(Kotlinを始める) ● 2020.06 株式会社ZOZOテクノロジーズ ● 2020.12 フリーランス(現在)

Slide 4

Slide 4 text

登壇、執筆 ● CEDEC 2018、2019登壇 ● Software Design 2019年2月号〜4月号で短期連載 「サーバーサイド開発の品質を向上させる Java→Kotlin 移行のススメ」執筆 ● 2021年4月 書籍「Kotlin サーバーサイドプログラミング 実践開発」を出版

Slide 5

Slide 5 text

本日の内容

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

1. 改めてKotlin Coroutinesの挙動

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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()}") } } }

Slide 13

Slide 13 text

実行結果

Slide 14

Slide 14 text

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 の順番で出力される

Slide 15

Slide 15 text

つまり直列で動いている

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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()}") } } }

Slide 18

Slide 18 text

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・・・ と出力される

Slide 19

Slide 19 text

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へ

Slide 20

Slide 20 text

● 中断をされなければただの直列処理になる ● 外部I/Oなど処理以外の待ちが発生する時に中断してくれると次の処理 が進められる

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

2. HTTP通信での使用

Slide 24

Slide 24 text

Spring WebFluxのWebClientでの実装

Slide 25

Slide 25 text

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() println("${LocalDateTime.now()} end Google. thread:${Thread.currentThread()}") Regex(""".*""").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で取得して出力

Slide 26

Slide 26 text

実行結果

Slide 27

Slide 27 text

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] [Google, Yahoo! JAPAN, Bing] ● 各asyncブロックがstartし、順に終了している ● webClientの実行のところで中断されているのがわかる

Slide 28

Slide 28 text

coroutineScope { val deferreds = listOf( async { println("${LocalDateTime.now()} start Google. thread:${Thread.currentThread()}") val response = restTemplate.getForObject("https://www.google.com/") println("${LocalDateTime.now()} end Google. thread:${Thread.currentThread()}") Regex(""".*""").find(response)?.value }, RestTemplateを使うとブロッキングになってしまうので注意

Slide 29

Slide 29 text

3. gRPC通信での使用

Slide 30

Slide 30 text

呼び出されるgRPCサーバー

Slide 31

Slide 31 text

@GRpcService class GreeterGrpcService : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun hello(request: HelloRequest) = HelloResponse.newBuilder() .setText("Hello ${request.name}") .build() } nameをリクエストで受け取って、メッセージを作って textとして返すだけの処理

Slide 32

Slide 32 text

grpc-kotlinでの実装

Slide 33

Slide 33 text

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で取得して出力

Slide 34

Slide 34 text

実行結果

Slide 35

Slide 35 text

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の影響)

Slide 36

Slide 36 text

4. データベース通信での使用

Slide 37

Slide 37 text

テストデータの作成

Slide 38

Slide 38 text

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);

Slide 39

Slide 39 text

JDBCを使用した場合 最もよく使用される ブロッキングのデータベースアクセス (ORMとしてMyBatisを使用)

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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で取得して出力

Slide 42

Slide 42 text

実行結果

Slide 43

Slide 43 text

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はデータベースへのアクセス中も中断してくれないため

Slide 44

Slide 44 text

withContextでContextを切り替える

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

実行結果

Slide 48

Slide 48 text

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ブロックが並列で実行されている

Slide 49

Slide 49 text

R2DBCを使用した場合 ノンブロッキングでの データベースアクセス

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

interface UserRepository : CoroutineCrudRepository { } データクラスとRepositoryの作成 data class User( @Id val id: Int, val name: String, val age: Int ) ● テーブルに構造に紐づいたデータクラスを作成 ● CoroutineCrudRepositoryを実装したRepositoryを作成(これで各 種アクセスの関数が使える )

Slide 53

Slide 53 text

CoroutineCrudRepositoryの中身

Slide 54

Slide 54 text

/** * 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)

Slide 55

Slide 55 text

各種データベース操作が suspending functionで定義されている

Slide 56

Slide 56 text

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で取得して出力

Slide 57

Slide 57 text

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に切り替わる

Slide 58

Slide 58 text

ノンブロッキングの データベースアクセスを実現

Slide 59

Slide 59 text

interface UserRepository : CoroutineCrudRepository { suspend fun findByName(name: String): List @Query( """ SELECT * FROM user ORDER BY age LIMIT 1 """ ) suspend fun findMostYoung(): User } ● シンプルなクエリは、 findByNameなど機能とカラム名の命名規則で作れる ● @Queryを使うことで自由にクエリを定義することもできる ● これもノンブロッキングになる

Slide 60

Slide 60 text

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の 追加が必要

Slide 61

Slide 61 text

5. まとめ

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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