iku8blog - 考えない為に考える

MicronautとgRPCでサーバ・クライアント間通信を構築してみる

date: 2022-10-15

gRPC を Micronaut で触ってみる。 Micronauto は Java の軽量フレームワークで、起動がとにかく早い。 gRPC はサービス間通信の技術。

マイクロサービス向けのこの 2 つの技術でサクッと環境を整えて通信させてみる。

今回 gRPC、Micronauto のメリット・デメリットについては言及しない。またこれらの技術について、どういう仕組でというのも説明しない。ここではあくまで gRPC を用いた通信ができることを確認できるのみに留める。

深堀りするのはまた別で行う。

構築する環境

gRPC サーバとクライアントを、それぞれ異なる Micronaut アプリケーションで構築する。 サーバ・クライアントを同一アプリケーションに構築することもできるが、あまりおもしろくないので、実践ぽく分けてみる

  • gRPC サーバ
    • こいつが様々なメソッドを提供する
    • 適当なポートで起動して、クライアントが接続できる環境にしておく
  • gRPC クライアント
    • http アプリケーションを起動して、特定のエンドポイントにアクセスした時に、gRPC サーバに接続する処理を記述する

gRPC サーバの構築

Micronauto のアプリケーションは IntelliJ より作成した。以下のサイトでも作成できる https://micronaut.io/launch/

java のバージョンを 17 にしたくらい。dependency は追加せず、以下のように直接 build.gradle に記述した。

ほとんど初期状態で、gRPC/Protocol Buffers の設定だけ加えた

./build.gradle

plugins {
    id("com.github.johnrengelman.shadow") version "7.1.2"
    id("io.micronaut.application") version "3.6.2"
    id("com.google.protobuf") version "0.8.15"
}

version = "0.1"
group = "com.example"

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.micronaut:micronaut-jackson-databind")
    implementation("io.micronaut:micronaut-context")
    implementation("jakarta.annotation:jakarta.annotation-api")
    runtimeOnly("ch.qos.logback:logback-classic")
    implementation("io.micronaut:micronaut-validation")
    implementation("io.micronaut.grpc:micronaut-grpc-server-runtime")
}

test {
    useJUnitPlatform()
}

application {
    mainClass.set("com.example.Application")
}
java {
    sourceCompatibility = JavaVersion.toVersion("17")
    targetCompatibility = JavaVersion.toVersion("17")
}

graalvmNative.toolchainDetection = false
micronaut {
    runtime("netty")
    testRuntime("junit5")
    processing {
        incremental(true)
        annotations("com.example.*")
    }
}

sourceSets {
    main {
        java {
            srcDirs("build/generated/source/proto/main/grpc")
            srcDirs("build/generated/source/proto/main/java")
        }
    }
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:3.20.1" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.46.0" }
    }
    generateProtoTasks {
        all()*.plugins { grpc {} }
    }
}

io.micronaut.grpc:micronaut-grpc-server-runtimeで gRPC サーバが起動できる。

gRPC のサービスとメソッド・メッセージ作成

公式サンプルと同様だが、以下のように gRPC のサービスとメソッド・メッセージの作成を行った

./src/main/proto/helloworld.proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

proto ファイルができれば、コードを自動生成できるので以下コマンドで生成する。

./gradlew generateProt

これで、ファイルが生成されて、これないの GreeterImplBase という abstract class を実装することで、gRPC でメソッドをクライントに提供する事ができる。 ./build/generated/source/proto/main/grpc/helloworld/GreeterGrpc.java

gRPC メソッドの実装を行う

generate された GreeterImplBase を実装していく。

./src/main/java/com/example/GreetingEndpoint.java

package com.example;

import helloworld.GreeterGrpc;
import helloworld.HelloReply;
import helloworld.HelloRequest;
import io.grpc.stub.StreamObserver;
import io.micronaut.grpc.annotation.GrpcService;

@GrpcService
public class GreetingEndpoint extends GreeterGrpc.GreeterImplBase {
    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> replyObserver) {
        String value = request.getName();
        HelloReply build = HelloReply.newBuilder()
                .setMessage("Greeting: " + value)
                .build();

        replyObserver.onNext(build);
        replyObserver.onCompleted();
    }
}

簡単に説明すると、クライントから受け取ったメッセージの先頭にGreeting: とつけて返すだけ。

起動周りの設定度、gRPC サーバ起動

port とか、メッセージサイズ数をちょっと変えてみたかったので、設定変更した  ./src/main/resources/application.yml

micronaut:
  server:
    port: 8081
  application:
    name: grpc-server-sample
netty:
  default:
    allocator:
      max-order: 3
grpc:
  server:
    port: 9001
    max-inbound-message-size: 10

port: 8081はクライアントのポートかぶるので変更した。使用することはない

起動は以下コマンド

 ./gradlew run

これでログに以下のように表示されれば OK

 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (v3.7.1)

01:11:07.306 [main] INFO  i.m.g.s.GrpcEmbeddedServerListener - GRPC started on port 9001

自分の環境だと http も起動していたが、使わないので放置

gRPC クライアントの構築

クライアント側はもちろん gRPC サーバを起動する必要がないので io.micronaut.grpc:micronaut-grpc-server-runtimeが不要。ただ gRPC のライブラリは必要なので、以下を使う

io.micronaut.grpc:micronaut-grpc-client-runtime

./build.gradle


plugins {
    id("com.github.johnrengelman.shadow") version "7.1.2"
    id("io.micronaut.application") version "3.6.2"
    id("com.google.protobuf") version "0.8.15"
}

version = "0.1"
group = "com.example"

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.micronaut:micronaut-jackson-databind")
    implementation("jakarta.annotation:jakarta.annotation-api")
    runtimeOnly("ch.qos.logback:logback-classic")
    implementation("io.micronaut:micronaut-validation")
    implementation("io.micronaut.grpc:micronaut-grpc-client-runtime")

    // lombok 便利なので追加した
    annotationProcessor("org.projectlombok:lombok:1.18.24")
    compileOnly("org.projectlombok:lombok:1.18.24")
}


application {
    mainClass.set("com.example.Application")
}
java {
    sourceCompatibility = JavaVersion.toVersion("17")
    targetCompatibility = JavaVersion.toVersion("17")
}

graalvmNative.toolchainDetection = false
micronaut {
    runtime("netty")
    testRuntime("junit5")
    processing {
        incremental(true)
        annotations("com.example.*")
    }
}

sourceSets {
    main {
        java {
            srcDirs("build/generated/source/proto/main/grpc")
            srcDirs("build/generated/source/proto/main/java")
        }
    }
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:3.20.1" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.46.0" }
    }
    generateProtoTasks {
        all()*.plugins { grpc {} }
    }
}

#### クライアントに gRPC のサービスとメソッド・メッセージ作成の proto ファイルをコピーする

サーバで記述したものと同じファイルをクライアント側にも置く。

./src/main/proto/helloworld.proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

で、コードを generate しておく。

./gradlew generateProt

クライアントの Bean を作成する。

Bean を作らなくても、都度接続コードを記述すれば接続できるが面倒出し無駄な処理なので Bean 定義して、DI することにした。

./src/main/java/com/example/Clients.java

package com.example;

import helloworld.GreeterGrpc;
import io.grpc.ManagedChannel;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.grpc.annotation.GrpcChannel;

@Factory
public class Clients {
    @Bean
    GreeterGrpc.GreeterBlockingStub blockingStub(@GrpcChannel("greeter") ManagedChannel channel) {
        return GreeterGrpc.newBlockingStub(channel);
    }
}

Micronaut はio.micronaut:micronaut-injectというのがあり、@Bean の代わりに@Inject というのが使えるが、Lombok を入れた際に@AllArgsConstructor が動かなくなったので、これは使わなかった。

gRPC サーバの接続情報定義

Clients.java に直接記述しても良いが、yaml でも定義できる。

./src/main/resources/application.yml

micronaut:
  application:
    name: grpc-client-sample
netty:
  default:
    allocator:
      max-order: 3
grpc:
  channels:
    greeter:
      address: "localhost:9001"
      # TLS接続しないので、以下trueにしておく
      plaintext: true
      max-retry-attempts: 10

リモートコールする

特定のパスにアクセスしたら、リモートコールするよなコントローラを作成する

./src/main/java/com/example/HelloController.java

package com.example;


import helloworld.GreeterGrpc;
import helloworld.HelloReply;
import helloworld.HelloRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import lombok.AllArgsConstructor;

@AllArgsConstructor
@Controller("/hello")
public class HelloController {
    private final GreeterGrpc.GreeterBlockingStub stub;

    @Get("/world")
    public String sayHello() {
        HelloRequest request = HelloRequest.newBuilder()
                .setName("Hello!!!") // 文字数制限してるので、長いとエラーとなるよ
                .build();

        return stub.sayHello(request).getMessage();
    }
}

クライアントのアプリケーションも起動してみる

./gradlew run

以下のように localhost:8080 が起動すれば OK。gRPC は起動しない

 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (v3.7.1)

01:30:34.404 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 398ms. Server Running: http://localhost:8080
http://localhost:8080/hello/world2

にアクセスしてみると、

gRPC Server: Hello!!!

となっていた。

gRPC Server: というのが gRPC サーバ側、Hello!!!がクライアントから送ったメッセージ。

ちゃんとサービス間通信ができていることが確認できた。

所感

  • Micronaut の起動はとても高速で、開発する際にストレスが今の所少ない
  • gRPC を用いることで、API 定義などクライアントが気にすることなく方に従って、内部のコード呼ぶかのようにサービス間の通信ができるので気にすることが少なくて良い

また別の機会に gRPC を使うメリットを深堀りしていく。データ圧縮による通信の高速化、効率化、型による恩恵など色々ある。

参考

https://micronaut-projects.github.io/micronaut-grpc/snapshot/guide/index.html

date: 2022-10-15