gRPC on GraalVM
샘플 코드
모티프
최근 마이크로 서비스나 모바일 애플리케이션에 API를 제공하는 등 gRPC를 활용하는 장면이 늘었다.
gRPC의 샘플로서 Go 언어를 사용하는 것이 비교적 많지만 BFF가 모바일 엔지니어가 실시하고 자바 시스템의 엔지니어가 많은 것을 감안하여 자바와 Kotlin으로 개발하고 싶습니다.
GraalVM을 사용하여 병목이 된 애플리케이션의 시작 속도를 개선하고 gRPC(protocol buffers)를 통해 고속 통신을 실현한다.
gRPC-java 가져오기
이번에protocol buffers를 사용하여 직렬화하고 gRPC-java를 사용하여 서버, 클라이언트 코드를 생성합니다.
그나저나 gRPC-kotlin은 코로틴에 대응하는 코드를 만들 수 있지만 로컬 이미지 생성 실패 때문에 이번에는 사용하지 않습니다.
구축할 때gradle을 사용합니다.
protocol buffers의gradle 플러그인을 먼저 가져옵니다.
buildscript {
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.15"
}
}
plugins {
id "com.google.protobuf"
id "application"
id "idea"
}
실행 가능한jar이나sourceSet을 생성하기 위해 상기 플러그인을 유효하게 설정합니다.dependencies에 gRPC-java의 의존 관계를 추가합니다.
dependencies {
implementation "io.grpc:grpc-netty-shaded:1.36.0"
implementation "io.grpc:grpc-protobuf:1.36.0"
implementation "io.grpc:grpc-stub:1.36.0"
}
상기 grpc-java에서 실시된netty를 포함하지만 로컬 이미지를 제작할 때.넷티의 반이 부족하다고 하니 아래도dependencies에 추가합니다.dependencies {
implementation "io.netty:netty-handler:4.1.60.Final"
}
이어서 포토 파일에서 코드를 생성하는 데 사용되는 설정을 보충한다.protobuf {
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:1.34.1"
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
protoc 파일을 지정한 폴더를 추가합니다.sourceSets {
main {
proto {
srcDir "src/main/proto"
}
}
}
소개 파일 만들기
gRPC 코드를 생성하기 위해서는 먼저 다음과 같은 프로토 파일을 만듭니다.
syntax = "proto3";
package com.example;
option java_package = "com.example";
message CreateTodoRequest {
string title = 1;
}
message Todo {
string id = 1;
string title = 2;
}
service TodoService {
rpc Create(CreateTodoRequest) returns (Todo);
}
protoo 파일을 만들면 먼저 구축하고 gRPC 코드를 생성합니다.(IntelliJ에서 자동으로 생성된 gRPC의 코드를 식별하기 위해)
./gradlew build
서비스의 실현
다음은 자동으로 생성된 gRPC 코드를 계승하여 서비스를 실현한다.
다음은 요청 매개 변수에 따라 Todo 대상을 적당히 만들고 응답을 되돌려주는 것입니다.
src/main/kotlin/com/example/TodoService.kt
package com.example
import io.grpc.stub.StreamObserver
import java.util.*
class TodoService : TodoServiceGrpc.TodoServiceImplBase() {
override fun create(
request: TodoOuterClass.CreateTodoRequest,
responseObserver: StreamObserver<TodoOuterClass.Todo>
) {
val todo = TodoOuterClass.Todo
.newBuilder()
.setId(UUID.randomUUID().toString())
.setTitle(request.title)
.build()
responseObserver.apply {
onNext(todo)
onCompleted()
}
}
}
gRPC 서버 설치
서비스가 가능하면 gRPC 서버를 설치합니다.
다음은 9000포트에서 gRPC 서버를 시작하는main 함수입니다.
src/main/kotlin/com/example/main.kt
package com.example
import io.grpc.ServerBuilder
fun main() {
println("start grpc server")
val port = System.getenv("PORT") ?: 8080
ServerBuilder.forPort(port)
.addService(TodoService())
.build()
.start()
.awaitTermination()
}
jar 파일 생성 설정
GraalVM의 로컬 이미지를 구축하려면jar 파일을 만들어야 합니다.
다음은build입니다.gradle에jar를 생성하기 위해gradle에 추가합니다.
application {
mainClassName = "com.example.MainKt"
}
jar {
archiveFileName = "native-image.jar"
manifest {
attributes "Main-Class": "com.example.MainKt" // main.ktはMainKtというJavaのクラスになります
}
from {
configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
}
jar를 직접 사용하면 의존 관계에 들어갈 수 없기 때문에from에 모든 의존 관계가jar에 포함되도록 설정했습니다.반사 프로파일 생성하기
GraalVM에서는 빠른 부팅을 위해 구축할 때 코드를 정적 구성합니다.
따라서 반사 등을 실행할 때 동적 불러오는 코드를 사용할 수 없습니다.
반사 등 동적 로드를 사용하고 싶을 때 반사 설정 파일을 준비하고 로드할 것이 무엇인지 미리 정의하면 GraalVM이 더욱 좋아지고 로컬 이미지도 만들어 낼 수 있다.
그러나 프로그램 라이브러리 등에서 반사 기능을 사용한 경우도 있어 이것들을 하나하나 설정 파일에 쓰기가 힘들다.
GraalVM은jar가 실행될 때 프로그램을 분석하고 상기 설정 파일을 자동으로 생성하는 에이전트를 제공합니다.
java -agentlib:native-image-agent=config-output-dir=configs -jar xxxx.jar
jar를 시작하여 요청을 보내고 처리하며 설정 파일을 작성합니다.(옵션으로 시작하기 전에 걸리는 시간을 지정할 수 있습니다.)
다음 파일은 자동으로 내보내집니다.
....,
{
"name": "io.netty.channel.socket.nio.NioServerSocketChannel",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
}
]
로컬 이미지 만들기
Docker file 만들기
로컬 이미지, Docker 이미지를 만듭니다.
FROM alpine:3.10.3
WORKDIR /protoc
# graalvmのイメージにはprotocが入っていないのでまずインストールする。
RUN apk add --no-cache unzip curl
RUN PB_REL="https://github.com/protocolbuffers/protobuf/releases" \
&& curl -LO $PB_REL/download/v3.14.0/protoc-3.14.0-linux-x86_64.zip \
&& unzip protoc-3.14.0-linux-x86_64.zip
# graalvmのイメージを使用してビルドを行う。
FROM ghcr.io/graalvm/graalvm-ce:21.0.0 as build
COPY . .
COPY /protoc ./protoc
RUN export PATH="$PATH:/protoc/bin" \
&& gu install native-image \
&& ./gradlew build \
&& native-image --verbose --no-fallback \
-H:ReflectionConfigurationFiles=configs/reflect-config.json \
--allow-incomplete-classpath \
-jar build/libs/native-image.jar
# 作成した実行ファイルだけを持つイメージを作成する
FROM debian:buster-slim
COPY /native-image .
CMD ["./native-image"]
docker build을 사용하여 그림을 만듭니다.docker build -t kotlin-grpc-native-image .
docker run으로 실행합니다.$ docker run --rm -it -p 9000:9000 kotlin-grpc-native-image
start grpc server
클라이언트 구현
gRPC 서버에 액세스하는 클라이언트를 만들고 API를 두드립니다.
src/main/kotlin/com/example/client.kt
package com.example
import io.grpc.ManagedChannelBuilder
fun main() {
val channel = ManagedChannelBuilder
.forAddress("localhost", 8080)
.usePlaintext()
.build()
val client = TodoServiceGrpc.newBlockingStub(channel)
val todo = client.create(
TodoOuterClass.CreateTodoRequest
.newBuilder()
.setTitle("sample todo")
.build()
)
println(todo)
}
용기를 시작한 상태에서 상술한 프로그램을 실행하면 gRPC 서버에서 응답을 되돌려줍니다.id: "23610d14-0aed-4651-97ca-7b8fc2dd2dea"
title: "sample todo"
덤
Cloud Run에 대한 디버그
Cloud Run은 gRPC를 지원합니다.
모처럼의 기회니까 컨테이너를 설계해 보세요.
먼저 CloudRegistry에서 push를 할 수 있도록 docker를 구성합니다.
gcloud auth configure-docker
docker build의 탭을 변경합니다.프로젝트 ID를 적절하게 변경하십시오.
docker build -t gcr.io/${project_id}/kotlin-grpc-native-image .
인상이 나면 CloudRegistry로 push하세요.docker push gcr.io/${project_id}/kotlin-grpc-native-image
GCP의 콘솔에서 CloudRun 서비스를 만듭니다.우선 접근할 수 있도록 인증되지 않은 호출을 허용합니다.
서비스가 생성되면 발행된 영역에서 클라이언트 코드를 변경합니다.
또한 아까 클라이언트 예시에서 인증된 접근은 없지만 클라우드런의 프로그램에 접근하기 위해 TLS를 통해 통신하는 것으로 변경됩니다.
src/main/kotlin/com/example/client.kt
package com.example
import io.grpc.ManagedChannelBuilder
fun main() {
val channel = ManagedChannelBuilder
.forAddress("kotlin-grpc-native-image-xxxx-xx.a.run.app", 443) // portは443
// .usePlaintext()
.useTransportSecurity() // plaintextの代わりにこちらを使う
.build()
val client = TodoServiceGrpc.newBlockingStub(channel)
val todo = client.create(
TodoOuterClass.CreateTodoRequest
.newBuilder()
.setTitle("sample todo")
.build()
)
println(todo)
}
위의 절차를 실행하면 CloudRun 서버가 응답을 반환합니다.id: "23610d14-0aed-4651-97ca-7b8fc2dd2dea"
title: "sample todo"
속도 비교
첫 번째 팟캐스트의 응답 속도는 다음과 같다.
두 번째 이후에도 JVM이 빠릅니다.
Reference
이 문제에 관하여(gRPC on GraalVM), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/fuuuuumin65/articles/3b68b3b7754d4b텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)