Spring WebFluxのREST APIにAmazon CodeGuru Profilerを設定してみた

サンプルで作った、Reactive WebアプリケーションをAmazon CodeGuru Profilerでプロファイリングしてみました。
2020.08.31

Spring WebFluxを使ってKotlinコルーチンでReactiveなREST APIのサンプルアプリを作成し、Amazon CodeGuru Profilerを設定してみました。

結論を言うと、プロダクション環境ではないので、あまり有効なプロファイルは取得できませんでした。
ただ、比較的簡単に設定できることは確認できたので、機会があればプロダクション環境で適用してみたいと思います。

アジェンダ

  • Spring WebFlux REST APIサンプルアプリの概要
  • Amazon CodeGuru Profilerの設定方法
  • 取得したプロファイル結果

Spring WebFlux REST APIサンプルアプリの概要

サンプルといえばTODOアプリ、簡単なCRUD REST APIをDDDっぽく作成してみました。
DBはPostgreSQLで、ユーザー認証は別サービスをHTTPで叩くイメージです。
概要のみ簡単に記載しますが、詳細については、githubを参照してください。

ファイル構成はこんな感じです。

.
├── App.kt
├── Beans.kt
├── api
│   ├── Router.kt
│   ├── exception
│   │   ├── ErrorResponse.kt
│   │   └── ExceptionHandler.kt
│   ├── filters
│   │   └── AuthFilter.kt
│   └── handlers
│       └── TodoHandler.kt
├── application
│   ├── Exceptions.kt
│   ├── auth
│   │   └── AuthService.kt
│   └── todo
│       ├── TodoData.kt
│       └── TodoService.kt
├── domain
│   └── model
│       ├── auth
│       │   ├── AuthToken.kt
│       │   ├── User.kt
│       │   ├── UserId.kt
│       │   └── UserRepository.kt
│       └── todo
│           ├── Todo.kt
│           ├── TodoContent.kt
│           ├── TodoId.kt
│           ├── TodoRepository.kt
│           └── TodoTitle.kt
└── infrastructure
    ├── db
    │   └── todo
    │       ├── PostgreSQLClient.kt
    │       └── PostgreSQLTodoRepository.kt
    └── http
        └── auth
            └── AuthHttpClient.kt

ルーティングはアノテーション使う方法もありますが、Router Functionを使うと一箇所にまとめられます。コルーチンなので、coRouter使用します。

class Router(private val todoHandler: TodoHandler, private val authFilter: AuthFilter) {
    fun router() = coRouter {
        GET("/todo", todoHandler::list)
        GET("/todo/{id}", todoHandler::find)
        POST("/todo", todoHandler::create)
        PATCH("/todo/{id}", todoHandler::update)
        DELETE("/todo/{id}", todoHandler::delete)
    }.filter(authFilter)
}

フィルターでAuthorizationヘッダの認証をしています。全体をフィルタする場合はWebFilterを使うのが一般的だと思いますが、Router Funcitonに設定するタイプのHandlerFilterFunctionを使用してみました。

class AuthFilter(private val service: AuthService) : HandlerFilterFunction<ServerResponse, ServerResponse> {

    override fun filter(request: ServerRequest, next: HandlerFunction<ServerResponse>): Mono<ServerResponse> {
        val token = tokenOf(request)
        return mono(Dispatchers.Unconfined) {
            val user = service.authenticate(token)
            request.attributes()["AUTHENTICATED_USER"] = user
            request
        }.flatMap(next::handle)
    }

    private fun tokenOf(request: ServerRequest): AuthToken =
            AuthToken.createOrNull(request.headers().firstHeader("Authorization"))
                    ?: throw TodoUnauthorizedException("Auth header is required.")
}

リポジトリはSQLを直接実行。

class PostgreSQLTodoRepository(private val client: DatabaseClient) : TodoRepository {

    override suspend fun fetchAll(userId: UserId): List<Todo> = client
            .execute("SELECT * FROM todos WHERE user_id = :userId")
            .bind("userId", userId.value)
            .map(::readRow)
            .all()
            .collectList()
            .awaitSingle()

    override suspend fun find(userId: UserId, todoId: TodoId): Todo? = client
            .execute("SELECT * FROM todos WHERE id = :todoId AND user_id = :userId")
            .bind("userId", userId.value)
            .bind("todoId", todoId.value)
            .map(::readRow)
            .awaitFirstOrNull()

    override suspend fun insert(userId: UserId, title: TodoTitle, content: TodoContent): Todo = client
            .execute("INSERT INTO todos (user_id, title, content) VALUES(:userId, :title, :content) RETURNING *")
            .bind("userId", userId.value)
            .bind("title", title.value)
            .bind("content", content.value)
            .map(::readRow)
            .awaitFirst()

    // 以下省略
}

ツッコミどころはあると思うのですが、サンプルなので。
(本当は、Amazon CodeGuru Reviewer にチェックしてもらいたかったのですが、JavaじゃなくてKotlinだったので、一行も認識されませんでした?)

Amazon CodeGuru Profilerの設定方法

1. プロファイリンググループを作成します

コントロールパネル、CodeGuru、プロファイリンググループから、任意の名前のプロファイリンググループを作成します。

2. アプリ実行ロールにプロファイル用のポリシーをアタッチします

ドキュメントを参考に、アプリ実行ロールの以下のポリシーをアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "codeguru-profiler:ConfigureAgent",
                "codeguru-profiler:PostAgentProfile"
            ],
            "Resource": "arn:aws:codeguru-profiler:<region>:<accountID>:profilingGroup/<profilingGroupName>"
        }
    ]
}
3. アプリにプロファイル起動処理を追加します

build.gradle.tksに以下を追加します。

repositories {
    maven {
        url = uri("https://d1osg35nybn3tt.cloudfront.net")
    }
}

dependencies {
    implementation("com.amazonaws:codeguru-profiler-java-agent:1.0.1")
}

アプリのエントリーエントリーポイントに以下を追加します。

fun main(args: Array<String>) {

    Profiler.builder()
            .profilingGroupName("Test-Reactive-Web")
            .build()
            .start()

    // 以下省略
}

設定は以上になります。

取得したプロファイル結果

アプリを起動し、負荷をかけてプロファイルを取得してみました。 ほとんどWebFluxのチャンネル関係の処理でしたが、フレームを非表示にすることで、自作部分も出力されていることがわかりました。

CPU

レイテンシー

まとめ

Amazon CodeGuru Profilerを設定してみました。
JVMで動いているアプリケーションであれば、設定はとても楽だと思います。別のエージェントサービスを立ち上げる必要もないので、コンテナにもすんなり適用できると思います。
機会があれば、プロダクション環境に適用し、アプリのパフォーマンス改善に利用してみたいと思いました。