Armeria とは
マイクロサービス用のネットワーク・ライブラリです。
Netty をベースにして、主に以下の機能が提供されます。
- メトリクス
- サーキットブレーカー
- ヘルスチェックと負荷分散、サービスディスカバリへの応答
- 分散呼トレーシング(Zipkin)
- HTTP/1, HTTP/2
- gRPC / Thrift との統合
Armeria の始め方
簡単な例でAmeria を動かして雰囲気を掴みましょう。
Gradle でプロジェクトを作成します。
$ mkdir armeria-example $ cd armeria-example $ gradle init --type java-application --dsl kotlin --test-framework junit-jupiter --project-name armeria-example --package armeria.example
build.gradle.kts
に com.linecorp.armeria:armeria
の依存を追加します。
plugins { application } repositories { mavenCentral() } dependencies { implementation(platform("com.linecorp.armeria:armeria-bom:1.25.2")) implementation("com.linecorp.armeria:armeria") implementation("org.slf4j:slf4j-simple:2.0.9") } tasks.withType<JavaCompile> { options.compilerArgs.add("-parameters") } java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } application { mainClass.set("armeria.example.App") }
options.compilerArgs.add("-parameters")
は、後述の @Param
でパラメータ名を省略する場合に必要になるため、ここであらかじめ設定しておきます。
App.java
を以下のように編集します。
package armeria.example; import com.linecorp.armeria.server.Server; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.common.HttpResponse; import java.util.concurrent.CompletableFuture; public class App { public static void main(String[] args) { ServerBuilder sb = Server.builder().http(8080); sb.service("/greet/{name}", (ctx, req) -> HttpResponse.of("Hello, %s!", ctx.pathParam("name"))); Server server = sb.build(); server.closeOnJvmShutdown(); CompletableFuture<Void> future = server.start(); future.join(); } }
./gradlew run
して http://localhost:8080/greet/armeria
にアクセスすれば以下のような結果が得られます。
ServerBuilder
でサーバの設定を行います。
service()
(または annotatedService()
, serviceUnder()
など)で、リクエストパスに該当する処理を登録します。
サービスには、デコレータにより、メトリクスや、分散トレースなどの処理を追加できます。
サービス
service()
にてリクエストパスと紐づけてサービスを登録します。
以下の例では、GET リクエストに応答するサービスを登録しています。
sb.service("/greet/{name}", new AbstractHttpService() { @Override protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) { String name = ctx.pathParam("name"); return HttpResponse.of("Hello, %s!", name); } });
service()
では、指定したパスの完全一致にマップされます。
パスの前方一致でマップする場合には serviceUnder
を使い、以下のようにできます。
sb.serviceUnder("/greet", (ctx, req) -> { String path = ctx.mappedPath(); // Get the path without the prefix ('/greet3') // ... });
JAX-RS のようにアノテーションでリクエストとメソッドの紐づけを行うには annotatedService()
を使います。
sb.annotatedService(new Object() { @Get("/greet") public HttpResponse greetGet(@Param("name") String name) { return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{\"name\":\"%s\"}", name); } @Post("/greet") @Consumes(MediaTypeNames.FORM_DATA) public HttpResponse greetPost(@Param("name") String name) { return HttpResponse.of(HttpStatus.OK); } });
@Get
や @Post
などのHTTPメソッドに応じたアノテーションで、該当パスに応答するメソッドを定義します。
コンパイラオプションで -parameters
を付与し、コンパイル後の引数名が有効になっていれば以下のようにパラメータ名を省略することができます。
@Get("/greet") public HttpResponse greetGet(@Param String name) { }
Route
ルーティングの定義は ServiceBindingBuilder
で以下のようにラムダで定義することもできます。
ServerBuilder sb = Server.builder();
sb.withRoute(builder -> builder.path("/baz")
.methods(HttpMethod.GET, HttpMethod.POST)
.build((ctx, req) -> HttpResponse.of(OK)));
ServiceBindingBuilder
は route()
で直接取得して、以下のように定義することもできます。
sb.route() // Configure the service. .post("/foo/bar") // Matched when the path is "/foo/bar" and the method is POST. .consumes(MediaType.JSON) // Matched when the "content-type" header is "application/json". .produces(MediaType.JSON) // Matched when the "accept" headers is "application/json". .matchesHeaders("baz=qux") // Matched when the "baz" header is "qux". .matchesParams("quux=quuz") // Matched when the "quux" parameter is "quuz". .requestTimeoutMillis(5000) .maxRequestLength(8192) .verboseResponses(true) .build((ctx, req) -> HttpResponse.of(OK));
デコレータ
リクエスト/レスポンスをインターセプトして様々な処理を追加するにはデコレータを使います。
デコレータを適用するには以下の 3 つの方法があります。
DecoratingHttpServiceFunction
またはDecoratingRpcServiceFunction
を実装するSimpleDecoratingHttpService
またはSimpleDecoratingRpcService
を継承するDecoratingService
を継承する
DecoratingHttpServiceFunction
service.decorate()
に DecoratingHttpServiceFunction
をラムダで渡すことで、サービスをデコレートできます。
HttpService service = (ctx, req) -> HttpResponse.of("Hello, world!"); HttpService decorated = service.decorate((delegate, ctx, req) -> { System.out.println(req.path()); return delegate.serve(ctx, req); }); sb.service("/greet", decorated);
SimpleDecoratingHttpService
SimpleDecoratingHttpService
を継承してデリケートサービスを作成できます。
public class SoutService extends SimpleDecoratingHttpService { public SoutService(HttpService delegate) { super(delegate); } @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { System.out.println(req.path()); HttpService delegate = unwrap(); return delegate.serve(ctx, req); } }
service.decorate()
で作成したデリケートサービスを適用します。
sb.service("/greet", service.decorate(delegate -> new SoutService(delegate)));
DecoratingService
より低レベルな処理が必要な場合は、DecoratingService
を継承してサービスを作成します。
以下は、HTTPのやり取りを、独自プロトコルの RPC に変換して処理する例になります。
import com.linecorp.armeria.server.RpcService; // Transforms an RpcService into an HttpService. public class MyRpcService extends DecoratingService<RpcRequest, RpcResponse, HttpRequest, HttpResponse> { public MyRpcService(Service<? super RpcRequest, ? extends RpcResponse> delegate) { super(delegate); } @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { // This method has been greatly simplified for easier understanding. // In reality, we will have to do this asynchronously. RpcRequest rpcReq = convertToRpcRequest(req); RpcResponse rpcRes = unwrap().serve(ctx, rpcReq); return convertToHttpResponse(rpcRes); } private RpcRequest convertToRpcRequest(HttpRequest req) { ... } private HttpResponse convertToHttpResponse(RpcResponse res) { ... } }
クライアント
リクエストを発行するクライアントは、WebClient
を使います。
WebClient webClient = WebClient.of("http://example.com/"); CompletableFuture<AggregatedHttpResponse> res = webClient.get("/foo/bar.txt").aggregate() res.join();
aggregate()
により得る CompletableFuture で、レスポンス(ヘッダフィールド、ボディ、トレーラフィールド)を完全に受信した時点でコールバックされます。
HTTP POST の場合は以下のようにできます。
AggregatedHttpResponse res = webClient.post("/upload", "{ \"foo\": \"bar\" }") .aggregate().join();
ベースURLを指定せず、個別にURLを指定することもできます。
WebClient webClient = WebClient.of();
AggregatedHttpResponse res = webClient.get("http://example.com/foo/bar.txt")
.aggregate().join();
リクエストヘッダを指定するには RequestHeadersBuilder
を使い、以下のように指定することができます。
HttpRequest req = HttpRequest.of(RequestHeaders.builder() .scheme(SessionProtocol.HTTP) .authority("example.com") .method(HttpMethod.GET) .path("/foo/bar.txt") .build()); AggregatedHttpResponse res = webClient.execute(req).aggregate().join();
リダイレクトは以下のように処理することができます。
AggregatedHttpResponse res = webClient.get("http://example.com/redirect") .aggregate().join(); if (res.status() == HttpStatus.TEMPORARY_REDIRECT) { res = webClient.get(redirected.headers().get(HttpHeaderNames.LOCATION)) .aggregate().join(); }
サービスディスカバリでエンドポイントを特定する場合は、以下のように WebClient
を構成することができます。
DnsServiceEndpointGroup group = DnsServiceEndpointGroup.of("k8s.default.svc.cluster.local."); HealthCheckedEndpointGroup healthCheckedGroup = HealthCheckedEndpointGroup.of(group, "/monitor/l7check"); WebClient.builder(SessionProtocol.HTTP, healthCheckedGroup) ... .build();
WebClient
に EndpointGroup
を指定することで、動的に検索されたエンドポイントに対してリクエストが送られます。
クライアント デコレータ
WebClient ビルダにデコレータを指定することで、リクエストをカスタマイズできます。
WebClient.builder() .decorator(ConcurrencyLimitingClient.newDecorator(64)) .decorator(ContentPreviewingClient.newDecorator(128)) .decorator(RetryingClient.newDecorator(RetryRule.onUnprocessed())) .build();
以下のようなデコレータが準備されています。
BraveClient
Braveによるアウトバウンドリクエストの追跡CircuitBreakerClient
サーキットブレーカーCookieClient
Cookie を保存ConcurrencyLimitingClient
同時リクエスト数制限ContentPreviewingClient
リクエストとレスポンスの内容をプレビューDecodingClient
圧縮されたレスポンスを解凍MetricCollectingClient
Micrometerによるメトリクスを収集LoggingClient
リクエストとレスポンスをロギングRetryingClient
失敗リクエストのリトライ
gRPC サービス
gRPC サービスを Armeria に統合することができます。
build.gradle.kts
に com.linecorp.armeria:armeria-grpc
の依存を追加し、Gradle com.google.protobuf
プラグインを準備します。
import com.google.protobuf.gradle.* plugins { application id("com.google.protobuf") version "0.9.4" } repositories { mavenCentral() } dependencies { implementation(platform("com.linecorp.armeria:armeria-bom:1.25.2")) implementation("com.linecorp.armeria:armeria") implementation("com.linecorp.armeria:armeria-grpc") implementation("javax.annotation:javax.annotation-api:1.3.1") implementation("org.slf4j:slf4j-simple:2.0.9") } java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } tasks.withType<JavaCompile> { options.compilerArgs.add("-parameters") } application { mainClass.set("armeria.example.App") } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.6.1" } plugins { id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.15.1" } } generateProtoTasks { ofSourceSet("main").forEach { it.plugins { id("grpc") { } } } } }
proto
ファイルを作成し、
$ mkdir -p src/main/proto/ $ touch src/main/proto/hello.proto
以下のように編集します。
syntax = "proto3"; package grpc.hello; option java_package = "com.example.grpc.hello"; service HelloService { rpc Hello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
HelloService
というサービスは、文字列を含むリクエストを受け、文字列を含むレスポンスを返すものとして定義しています。
proto3 の文法については以下を参照してください。
https://blog1.mammb.com/entry/2019/10/03/212044
./gradlew build
で IDL から Grpc 向けのソースが自動生成されるので、これを利用してサービスを実装します。
生成された HelloServiceGrpc.HelloServiceImplBase
を継承して HelloServiceImpl
を作成します。
import armeria.example.grpc.hello.Hello.*; import armeria.example.grpc.hello.HelloServiceGrpc; import io.grpc.stub.StreamObserver; public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase { @Override public void hello(HelloRequest req, StreamObserver<HelloReply> responseObserver) { HelloReply reply = HelloReply.newBuilder() .setMessage("Hello, " + req.getName() + '!') .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } }
作成した Grpc サービスは以下のように Armeria のサービスとして登録できます。
ServerBuilder sb = Server.builder().http(8080); sb.service(GrpcService.builder() .addService(new HelloServiceImpl()) .build()); Server server = sb.build(); server.closeOnJvmShutdown(); server.start().join();
Grpc のクライアントは GrpcClients
から以下のように生成してサービスの呼び出しが可能です。
HelloServiceBlockingStub helloService = GrpcClients.newClient( "gproto+http://127.0.0.1:8080/", HelloServiceBlockingStub.class); HelloRequest request = HelloRequest.newBuilder().setName("Armerian World").build(); HelloReply reply = helloService.hello(request); System.out.println(reply.getMessage());
クライアントからサービスを呼び出せば、以下のような出力が得られます。
Hello, Armerian World!
まとめ
マイクロサービス・フレームワーク Armeria の使い方について簡単に紹介しました。
ここでは紹介しきれませんが、マイクロサービス向けの各種機能が準備されています。
ごく薄いライブラリで、他のテクノロジとの統合も容易になっているので、ドキュメントを一読してみてはいかがでしょうか。