CLOVER🍀

That was when it all began.

Netflix OSS/Eurekaで遊ぶ

前々からちょっと興味のあったNetflix OSS、ちょっとずつ触ってみようかなと思いまして。

名前はかなり見るので、自分が使うことになるかどうかはかなり不透明ではありますが、知識として知っておくのはよいのではないかなと。

Netflix OSS+Spring Cloudで概要を知るなら、こちらの資料がわかりやすいと思いました。

Spring Boot + Netflix Eureka

Eureka

で、まずはEurekaから試してみようかなと思います。

GitHub - Netflix/eureka: AWS Service registry for resilient mid-tier load balancing and failover.

どういうものかというと、内部DNSの代わり…?サービスディスカバリというらしいです。

登場人物としては、Eureka ServerとEureka Clientが登場するようです。

ドキュメントをもうちょっと読むと、Eureka ClientとEureka Serverがあって、Eureka Clientには以下の2つがあると。

  • Application Client … Application Serivceを使用するクライアント
  • Application Service … Application Clientのリクエストに対して、レスポンスを返す

Configuring Eureka · Netflix/eureka Wiki · GitHub

全部で

  • Eureka Server
  • Eureka Client for the Application Client
  • Eureka Client for the Application Service

となると。Eureka Serverというのは、Application Clientに対して、Application Serviceのアクセス先を解決するためのものと考えればよさそうですね。

…ということが、サンプルを書いていてやっとわかったのですが。

それでは、進めていってみます。

Eureka Serverを起動してみる

まずは、Eureka Serverを起動してみます。

こちらに沿って。

Configuring Eureka Server/Prerequisites

JDK 6以上、Tomcat 6.0.10以上とEureka ServerのWARがあればよい、と。

JDKは入っている前提で、Tomcatのダウンロードからやりましょう。

$ wget http://ftp.kddilabs.jp/infosystems/apache/tomcat/tomcat-8/v8.0.30/bin/apache-tomcat-8.0.30.tar.gz
$ tar -zxvf apache-tomcat-8.0.30.tar.gz
$ cd apache-tomcat-8.0.30/

今回は、Apache Tomcat 8.0.30です。

続いて、Maven CentralからEureka Serverをダウンロードします。

The Central Repository Search Engine

$ wget -O webapps/eureka.war http://search.maven.org/remotecontent?filepath=com/netflix/eureka/eureka-server/1.3.5/eureka-server-1.3.5.war

2系が見えますが、とりあえず今回は1系で進めることにします。

この時、WARファイルの名前をeureka.warとするようにしてください。その他、Eurekaリポジトリのサンプルなどが、コンテキストパスが「eureka」となっていることが前提になっているようなので。

で、Tomcatを起動。

$ bin/startup.sh

起動しきった状態で

http://localhost:8080/eureka/

にアクセスすると、Eurekaの画面が表示されます。

また、

http://localhost:8080/eureka/v2/apps

にアクセスすると、こんなXMLが見れたりします。

<applications>
  <versions__delta>1</versions__delta>
  <apps__hashcode>UP_1_</apps__hashcode>
  <application>
    <name>EUREKA</name>
    <instance>
      <instanceId>yourhost</instanceId>
      <hostName>yourhost</hostName>
      <app>EUREKA</app>
      <ipAddr>127.0.1.1</ipAddr>
      <status>UP</status>
      <overriddenstatus>UNKNOWN</overriddenstatus>
      <port enabled="true">8080</port>
      <securePort enabled="false">443</securePort>
      <countryId>1</countryId>
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
        <name>MyOwn</name>
      </dataCenterInfo>
      <leaseInfo>
        <renewalIntervalInSecs>30</renewalIntervalInSecs>
        <durationInSecs>90</durationInSecs>
        <registrationTimestamp>1451202727336</registrationTimestamp>
        <lastRenewalTimestamp>1451203047653</lastRenewalTimestamp>
        <evictionTimestamp>0</evictionTimestamp>
        <serviceUpTimestamp>1451202716996</serviceUpTimestamp>
      </leaseInfo>
      <metadata class="java.util.Collections$EmptyMap"/>
      <appGroupName>UNKNOWN</appGroupName>
      <homePageUrl>http://yourhost:8080/</homePageUrl>
      <statusPageUrl>http://yourhost:8080/Status</statusPageUrl>
      <healthCheckUrl>http://yourhost:8080/healthcheck</healthCheckUrl>
      <vipAddress>eureka.mydomain.net</vipAddress>
      <isCoordinatingDiscoveryServer>true</isCoordinatingDiscoveryServer>
      <lastUpdatedTimestamp>1451202727336</lastUpdatedTimestamp>
      <lastDirtyTimestamp>1451202687109</lastDirtyTimestamp>
      <actionType>ADDED</actionType>
    </instance>
  </application>
</applications>

なお、Eureka Serverはこれでも起動するようですが、catalina.outとか見ると

log4j:ERROR Could not instantiate class [com.netflix.logging.log4jAdapter.NFPatternLayout].
java.lang.ClassNotFoundException: com.netflix.logging.log4jAdapter.NFPatternLayout
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1333)
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1167)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at org.apache.log4j.helpers.Loader.loadClass(Loader.java:198)

みたいな感じでエラーを吐いているので、気になる方はBlitz4jを入れておきましょう。

GitHub - Netflix/blitz4j: Logging framework for fast asynchronous logging

$ wget -O webapps/eureka/WEB-INF/lib/blitz4j-1.36.1.jar http://search.maven.org/remotecontent?filepath=com/netflix/blitz4j/blitz4j/1.36.1/blitz4j-1.36.1.jar

Application Serviceを実装する

続いて、Application Clientからのリクエストを受け付ける、Application Serviceを実装してみます。

こちらのexampleを参考に。

eureka/eureka-examples at master · Netflix/eureka · GitHub

ここでは、いつも同じメッセージを返すだけのHTTPサーバーを、JDK HttpServerを使って実装することにします。

で、サンプルを見て実装しようと思ったのですが…

https://github.com/Netflix/eureka/blob/master/eureka-examples/src/main/java/com/netflix/eureka/ExampleEurekaService.java
https://github.com/Netflix/eureka/blob/master/eureka-examples/src/main/java/com/netflix/eureka/ExampleServiceBase.java
https://github.com/Netflix/eureka/blob/master/eureka-examples/conf/sample-eureka-service.properties

サンプルで使われているクラスが、ことごとく非推奨だとかいろいろ言われます。

で、Guiceを使えという感じみたいなので、Guiceを使うスタイルで書いてみました。初めてGuiceでコードを書いてますが…。

Maven依存関係は、こちら。

        <dependency>
            <groupId>com.netflix.eureka</groupId>
            <artifactId>eureka-client</artifactId>
            <version>1.3.5</version>
        </dependency>
        <dependency>
            <groupId>com.google.inject</groupId>
            <artifactId>guice</artifactId>
            <version>4.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.13</version>
        </dependency>

eureka-clientだけあればいいんじゃと思いきや、GuiceはEureka Client側でruntime指定してあり、自分で引っ張ってこないとダメなような…。
slf4j-simpleは、ログ出力用です。

じゃあ、このサンプルってどうやってコンパイルしてるの?ということなのですが

task runExampleService (dependsOn: [classes], type: JavaExec) {
group = "Run tasks"
description = "Run the example service"

main = "com.netflix.eureka.ExampleEurekaService"
classpath = sourceSets.main.runtimeClasspath
jvmArgs(["-Deureka.client.props=sample-eureka-service"])
}

https://github.com/Netflix/eureka/blob/master/eureka-examples/build.gradle#L19

全部足してる…?

まあいいや、進めましょう。

Application Serviceのエントリポイントは、こんな感じ。
src/main/java/org/littlewings/netflix/eureka/ExampleEurekaService.java

package org.littlewings.netflix.eureka;

import java.io.Console;
import java.util.concurrent.TimeUnit;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.util.Modules;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.appinfo.providers.MyDataCenterInstanceConfigProvider;
import com.netflix.discovery.guice.EurekaModule;

public class ExampleEurekaService {
    public static void main(String... args) {
        Injector injector =
                Guice.createInjector(
                        Modules
                                .override(new EurekaModule())
                                .with(new AbstractModule() {
                                    @Override
                                    protected void configure() {
                                        bind(EurekaInstanceConfig.class).toProvider(MyDataCenterInstanceConfigProvider.class);
                                    }
                                }));

        ExampleApplicationService exampleApplicationService = injector.getInstance(ExampleApplicationService.class);
        exampleApplicationService.start();

        Console console = System.console();
        if (console != null) {
            console.readLine("> Enter stop.");
        } else {
            System.out.println("60秒後にシャットダウンします");
            try {
                TimeUnit.SECONDS.sleep(60L);
            } catch (InterruptedException e) {
                // ignore
            }
        }

        exampleApplicationService.stop();
    }
}

元ネタは、こちらです。
https://github.com/Netflix/eureka/blob/master/eureka-examples/src/main/java/com/netflix/eureka/ExampleEurekaService.java

で、非推奨になったクラスを避け、Guiceで使うにはEurekaModuleというものを指定してあげればよいと…。

        Injector injector =
                Guice.createInjector(
                        Modules
                                .override(new EurekaModule())
                                .with(new AbstractModule() {
                                    @Override
                                    protected void configure() {
                                        bind(EurekaInstanceConfig.class).toProvider(MyDataCenterInstanceConfigProvider.class);
                                    }
                                }));

デフォルトだとAWS前提?みたいな感じの挙動をするので、MyDataCenterInstanceConfigProviderを足してオーバーライド。

こういう書き方だと

        Injector injector =
                Guice.createInjector(new EurekaModule());

こんなエラーが出て、起動に失敗します。
※実行は、mvn exec:javaで行っています

[ERROR] 1) Error in custom provider, java.lang.RuntimeException: Your datacenter is defined as cloud but we are not able to get the amazon metadata to register.
[ERROR] Set the property eureka.validateInstanceId to false to ignore the metadata call

残りは、HTTPサーバーを起動するコードが書いてあるだけです。

で、サンプルとして書いたHTTPサーバ+Eurekaに登録するためのEureka Clientを使ったコード。
src/main/java/org/littlewings/netflix/eureka/ExampleApplicationService.java

package org.littlewings.netflix.eureka;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class ExampleApplicationService {
    private final ApplicationInfoManager applicationInfoManager;
    private final EurekaClient eurekaClient;
    private final InstanceInfo instanceInfo;

    private Logger logger = LoggerFactory.getLogger(getClass());

    private MySimpleHttpServer httpServer;

    @Inject
    public ExampleApplicationService(ApplicationInfoManager applicationInfoManager,
                                     EurekaClient eurekaClient,
                                     InstanceInfo instanceInfo) {
        this.applicationInfoManager = applicationInfoManager;
        this.eurekaClient = eurekaClient;
        this.instanceInfo = instanceInfo;
    }

    public void start() {
        logger.info("ServiceをEurekaサーバーに登録します");
        applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.STARTING);

        logger.info("アプリケーション名 = {}", instanceInfo.getAppName());
        logger.info("HTTPサーバーのリッスンポート = {}", instanceInfo.getPort());
        logger.info("VIP = {}", instanceInfo.getVIPAddress());

        httpServer = new MySimpleHttpServer(instanceInfo.getPort(), "/");
        httpServer.start();

        logger.info("ServiceのステータスをUPに遷移します");
        applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.UP);

        logger.info("起動しました。リクエストを待っています");

        waitRegisteredEureka();
    }

    public void stop() {
        logger.info("シャットダウンします");

        httpServer.stop();

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }

        applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);
        eurekaClient.shutdown();
    }

    void waitRegisteredEureka() {
        String vip = instanceInfo.getVIPAddress();

        InstanceInfo nextServerInfo = null;

        while (nextServerInfo == null) {
            logger.info("Eurekaサーバーへ接続待ち中…");

            try {
                nextServerInfo = eurekaClient.getNextServerFromEureka(vip, false);
            } catch (Exception e) {
                e.printStackTrace();

                try {
                    TimeUnit.SECONDS.sleep(10L);
                } catch (InterruptedException ie) {
                    // ignore
                }
            }
        }

        logger.info("Eurekaサーバーへ接続しました = {}", nextServerInfo.getVIPAddress());
    }

    // ダミーのHTTPサーバー
    static class MySimpleHttpServer {
        private Logger logger = LoggerFactory.getLogger(getClass());
        private HttpServer httpServer;

        MySimpleHttpServer(int port, String contextPath) {
            try {
                httpServer = HttpServer.create(new InetSocketAddress(port), 0);
                httpServer.createContext(contextPath, this::handler);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }

        void handler(HttpExchange exchange) throws IOException {
            try (InputStream is = exchange.getRequestBody();
                 InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
                 BufferedReader reader = new BufferedReader(isr)) {
                logger.info("クライアントからのメッセージ = {}", reader.readLine());

                byte[] message = "Eureka Serviceからこんにちは!!".getBytes(StandardCharsets.UTF_8);

                exchange.sendResponseHeaders(200, message.length);
                exchange.getResponseBody().write(message);
            }
        }

        public void start() {
            httpServer.start();
        }

        public void stop() {
            httpServer.stop(0);
        }
    }
}

元ネタは、こちら。
https://github.com/Netflix/eureka/blob/master/eureka-examples/src/main/java/com/netflix/eureka/ExampleServiceBase.java

HTTPサーバーの説明は、割愛します。単にメッセージをログ出力して、クライアントにメッセージを返しているだけなので。

サンプルコードを参考に、以下をInject。

    @Inject
    public ExampleApplicationService(ApplicationInfoManager applicationInfoManager,
                                     EurekaClient eurekaClient,
                                     InstanceInfo instanceInfo) {
        this.applicationInfoManager = applicationInfoManager;
        this.eurekaClient = eurekaClient;
        this.instanceInfo = instanceInfo;
    }

ApplicationInfoManager#setInstanceStatusをSTARTING→UPとしていく感じで、Eureka Serverに状態を伝えるみたいですね。
https://github.com/Netflix/eureka/wiki/Understanding-eureka-client-server-communication

    public void start() {
        logger.info("ServiceをEurekaサーバーに登録します");
        applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.STARTING);

        logger.info("アプリケーション名 = {}", instanceInfo.getAppName());
        logger.info("HTTPサーバーのリッスンポート = {}", instanceInfo.getPort());
        logger.info("VIP = {}", instanceInfo.getVIPAddress());

        httpServer = new MySimpleHttpServer(instanceInfo.getPort(), "/");
        httpServer.start();

        logger.info("ServiceのステータスをUPに遷移します");
        applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.UP);

        logger.info("起動しました。リクエストを待っています");

        waitRegisteredEureka();
    }

シャットダウン時には、DOWNへ。

        applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);
        eurekaClient.shutdown();

EurekaClient#shutdownは、中身は空でしたが…。

起動時に呼び出しているwaitRegisteredEurekaメソッドの中では、EurekaClient#getNextServerFromEurekaが値を戻すまで、トライし続けます。

    void waitRegisteredEureka() {
        String vip = instanceInfo.getVIPAddress();

        InstanceInfo nextServerInfo = null;

        while (nextServerInfo == null) {
            logger.info("Eurekaサーバーへ接続待ち中…");

            try {
                nextServerInfo = eurekaClient.getNextServerFromEureka(vip, false);
            } catch (Exception e) {
                e.printStackTrace();

                try {
                    TimeUnit.SECONDS.sleep(10L);
                } catch (InterruptedException ie) {
                    // ignore
                }
            }
        }

        logger.info("Eurekaサーバーへ接続しました = {}", nextServerInfo.getVIPAddress());
    }

ここで、

        String vip = instanceInfo.getVIPAddress();

VIPとは何か?なのですが、これはEureka Clientとしての設定ファイルの内容が効くようです。
src/main/resources/eureka-service.properties

eureka.region=default

# what is my application name? (clients can query on this appName)
eureka.name=sampleRegisteringService

# what is my application virtual ip address? (clients can query on this vipAddress)
eureka.vipAddress=mysample.mydomain.net

# what is the port that I serve on? (Change this if port 8001 is already in use in your environment)
eureka.port=8001

## configuration related to reaching the eureka servers
eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.serviceUrl.default=http://localhost:8080/eureka/v2/

こちらは、以下を元ネタにして作っています。
https://github.com/Netflix/eureka/blob/master/eureka-examples/conf/sample-eureka-service.properties

eureka.nameには、このアプリケーションの名前を指定する模様。

eureka.name=sampleRegisteringService

eureka.vipAddressで、先ほどのInstanceInfo#getVIPAddressで返却される値を指定します。

eureka.vipAddress=mysample.mydomain.net

このApplication Serviceが使うポート。

eureka.port=8001

そのまま、HTTPサーバーのリッスンポートにしています。

        httpServer = new MySimpleHttpServer(instanceInfo.getPort(), "/");
        httpServer.start();

eureka.serviceUrl.defaultは、先ほどTomcatに仕込んだEureka Serverを指せばよいみたいです。

eureka.serviceUrl.default=http://localhost:8080/eureka/v2/

まあ、設定の詳しい意味はあまりわかっていませんが…。

では、Application Serviceを起動します。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.netflix.eureka.ExampleEurekaService -Deureka.client.props=eureka-service

システムプロパティ「eureka.client.props」で、先ほど作成した設定ファイルを指定しておきます。

-Deureka.client.props=eureka-service

で、コンソールにこんなログが流れて、Eurekaへ接続します。
※ログの先頭にスレッド名があったのですが、削りました。

WARN com.netflix.config.sources.URLConfigurationSource - No URLs will be polled as dynamic configuration sources.
INFO com.netflix.config.sources.URLConfigurationSource - To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
INFO com.netflix.config.DynamicPropertyFactory - DynamicPropertyFactory is initialized with configuration sources: com.netflix.config.ConcurrentCompositeConfiguration@3245ae4a
INFO com.netflix.config.util.ConfigurationUtils - Loaded properties file file:/path/to/target/classes/eureka-service.properties
WARN com.netflix.config.util.ConfigurationUtils - file:/path/to/target/classes/eureka-service.properties is already loaded
INFO com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider - Setting initial instance status as: STARTING
WARN com.netflix.config.util.ConfigurationUtils - file:/path/to/target/classes/eureka-service.properties is already loaded
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using encoding codec LegacyJacksonJson
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using decoding codec LegacyJacksonJson
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using encoding codec LegacyJacksonJson
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using decoding codec LegacyJacksonJson
INFO com.netflix.discovery.DiscoveryClient - Disable delta property : false
INFO com.netflix.discovery.DiscoveryClient - Single vip registry refresh property : null
INFO com.netflix.discovery.DiscoveryClient - Force full registry fetch : false
INFO com.netflix.discovery.DiscoveryClient - Application is null : false
INFO com.netflix.discovery.DiscoveryClient - Registered Applications size is zero : true
INFO com.netflix.discovery.DiscoveryClient - Application version is -1: true
INFO com.netflix.discovery.DiscoveryClient - Getting all instance registry info from the eureka server
INFO com.netflix.discovery.DiscoveryClient - The response status is 200
INFO com.netflix.discovery.DiscoveryClient - Starting heartbeat executor: renew interval is: 30
INFO com.netflix.discovery.InstanceInfoReplicator - InstanceInfoReplicator onDemand update allowed rate per min is 4
INFO org.littlewings.netflix.eureka.ExampleApplicationService - ServiceをEurekaサーバーに登録します
INFO org.littlewings.netflix.eureka.ExampleApplicationService - アプリケーション名 = SAMPLEREGISTERINGSERVICE
INFO org.littlewings.netflix.eureka.ExampleApplicationService - HTTPサーバーのリッスンポート = 8001
INFO org.littlewings.netflix.eureka.ExampleApplicationService - VIP = mysample.mydomain.net
INFO org.littlewings.netflix.eureka.ExampleApplicationService - ServiceのステータスをUPに遷移します
INFO com.netflix.discovery.DiscoveryClient - Saw local status change event StatusChangeEvent [current=UP, previous=STARTING]
INFO org.littlewings.netflix.eureka.ExampleApplicationService - 起動しました。リクエストを待っています
INFO org.littlewings.netflix.eureka.ExampleApplicationService - Eurekaサーバーへ接続待ち中…
[DiscoveryClient-InstanceInfoReplicator-0] INFO com.netflix.discovery.DiscoveryClient - DiscoveryClient_SAMPLEREGISTERINGSERVICE/yourhost: registering service...
java.lang.RuntimeException: No matches for the virtual host name :mysample.mydomain.net
	at com.netflix.discovery.DiscoveryClient.getNextServerFromEureka(DiscoveryClient.java:795)
	at org.littlewings.netflix.eureka.ExampleApplicationService.waitRegisteredEureka(ExampleApplicationService.java:84)
	at org.littlewings.netflix.eureka.ExampleApplicationService.start(ExampleApplicationService.java:57)
	at org.littlewings.netflix.eureka.ExampleEurekaService.main(ExampleEurekaService.java:30)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293)
	at java.lang.Thread.run(Thread.java:745)
[DiscoveryClient-InstanceInfoReplicator-0] INFO com.netflix.discovery.DiscoveryClient - DiscoveryClient_SAMPLEREGISTERINGSERVICE/yourhost - registration status: 204

〜何度か試行〜

INFO org.littlewings.netflix.eureka.ExampleApplicationService - Eurekaサーバーへ接続待ち中…
INFO org.littlewings.netflix.eureka.ExampleApplicationService - Eurekaサーバーへ接続しました = mysample.mydomain.net
> Enter stop.

これで、Application Serviceの準備ができました。

この時、Eureka Serverにアクセスすると、登録されているアプリケーションが増えていることが確認できます。
http://localhost:8080/eureka/

Application Clientを実装する

最後は、Application Serviceを利用する側のApplication Clientを実装します。

実装したクライアントは、こちら。
src/main/java/org/littlewings/netflix/eureka/ExampleEurekaClient.java

package org.littlewings.netflix.eureka;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.util.Modules;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.providers.MyDataCenterInstanceConfigProvider;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.guice.EurekaModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExampleEurekaClient {
    public static void main(String... args) throws IOException {
        Logger logger = LoggerFactory.getLogger(ExampleEurekaClient.class);

        String vip = "mysample.mydomain.net";

        Injector injector =
                Guice.createInjector(
                        Modules
                                .override(new EurekaModule())
                                .with(new AbstractModule() {
                                    @Override
                                    protected void configure() {
                                        bind(EurekaInstanceConfig.class).toProvider(MyDataCenterInstanceConfigProvider.class);
                                    }
                                }));

        EurekaClient eurekaClient = injector.getInstance(EurekaClient.class);
        InstanceInfo nextServerInfo = eurekaClient.getNextServerFromEureka(vip, false);

        logger.info("VIP = {}:{}", nextServerInfo.getVIPAddress(), nextServerInfo.getPort());
        logger.info("HealthCheckUrl = {}", nextServerInfo.getHealthCheckUrl());
        logger.info("override =  {}", nextServerInfo.getOverriddenStatus());
        logger.info("Application Name = {}", nextServerInfo.getAppName());

        String serviceUrl = "http://" + nextServerInfo.getHostName() + ":" + nextServerInfo.getPort() + "/";
        logger.info("Service Server URL = {}", serviceUrl);

        HttpURLConnection connection = (HttpURLConnection) new URL(serviceUrl).openConnection();
        connection.setDoOutput(true);
        connection.setRequestMethod("POST");

        try {
            try (OutputStream os = connection.getOutputStream();
                 OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
                 BufferedWriter writer = new BufferedWriter(osw)) {
                writer.write("クライアントからこんにちは!");
            }

            try (InputStream is = connection.getInputStream();
                 InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
                 BufferedReader reader = new BufferedReader(isr)) {
                logger.info("Serviceからのメッセージ = {}", reader.readLine());
            }
        } finally {
            connection.disconnect();
        }

        eurekaClient.shutdown();
    }
}

元ネタは、以下です。
https://github.com/Netflix/eureka/blob/master/eureka-examples/src/main/java/com/netflix/eureka/ExampleEurekaClient.java

GuiceでEurekaClientを引き抜き

        Injector injector =
                Guice.createInjector(
                        Modules
                                .override(new EurekaModule())
                                .with(new AbstractModule() {
                                    @Override
                                    protected void configure() {
                                        bind(EurekaInstanceConfig.class).toProvider(MyDataCenterInstanceConfigProvider.class);
                                    }
                                }));

        EurekaClient eurekaClient = injector.getInstance(EurekaClient.class);

Eureka Clientから、VIPに対応するInstanceInfoを取得します。

        InstanceInfo nextServerInfo = eurekaClient.getNextServerFromEureka(vip, false);

この時のVIPは、先ほどApplication Serviceで登録したものです。

        String vip = "mysample.mydomain.net";

で、ホスト名とポートを解決してもらって、Application Serviceにアクセスする、と。

        String serviceUrl = "http://" + nextServerInfo.getHostName() + ":" + nextServerInfo.getPort() + "/";
        logger.info("Service Server URL = {}", serviceUrl);

        HttpURLConnection connection = (HttpURLConnection) new URL(serviceUrl).openConnection();

この時、Application Clientに使わせるEurekaの設定ファイルは、こちらです。
src/main/resources/eureka-client.properties

eureka.registration.enabled=false

eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.serviceUrl.default=http://localhost:8080/eureka/v2/

eureka.decoderName=JacksonJson

元ネタ。
https://github.com/Netflix/eureka/blob/master/eureka-examples/conf/sample-eureka-client.properties

この設定ファイルには、あくまでEureka Serverへの接続しか書かれていません。

eureka.serviceUrl.default=http://localhost:8080/eureka/v2/

では、実行してみます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.netflix.eureka.ExampleEurekaClient

今回は「eureka.client.props」システムプロパティを設定していませんが、デフォルト値が「eureka-client」のようなので、そのまま読んでくれるみたいでした…。

実行ログ。

WARN com.netflix.config.sources.URLConfigurationSource - No URLs will be polled as dynamic configuration sources.
INFO com.netflix.config.sources.URLConfigurationSource - To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
INFO com.netflix.config.DynamicPropertyFactory - DynamicPropertyFactory is initialized with configuration sources: com.netflix.config.ConcurrentCompositeConfiguration@2e3da2cb
INFO com.netflix.config.util.ConfigurationUtils - Loaded properties file file://path/to/target/classes/eureka-client.properties
WARN com.netflix.config.util.ConfigurationUtils - file://path/to/target/classes/eureka-client.properties is already loaded
INFO com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider - Setting initial instance status as: STARTING
WARN com.netflix.config.util.ConfigurationUtils - file://path/to/target/classes/eureka-client.properties is already loaded
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using encoding codec LegacyJacksonJson
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using decoding codec JacksonJson
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using encoding codec LegacyJacksonJson
INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using decoding codec JacksonJson
INFO com.netflix.discovery.DiscoveryClient - Disable delta property : false
INFO com.netflix.discovery.DiscoveryClient - Single vip registry refresh property : null
INFO com.netflix.discovery.DiscoveryClient - Force full registry fetch : false
INFO com.netflix.discovery.DiscoveryClient - Application is null : false
INFO com.netflix.discovery.DiscoveryClient - Registered Applications size is zero : true
INFO com.netflix.discovery.DiscoveryClient - Application version is -1: true
INFO com.netflix.discovery.DiscoveryClient - Getting all instance registry info from the eureka server
INFO com.netflix.discovery.DiscoveryClient - The response status is 200
INFO com.netflix.discovery.DiscoveryClient - Not registering with Eureka server per configuration
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - VIP = mysample.mydomain.net:8001
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - HealthCheckUrl = http://yourhost:8001/healthcheck
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - override =  UNKNOWN
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Application Name = SAMPLEREGISTERINGSERVICE
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Service Server URL = http://yourhost:8001/
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Serviceからのメッセージ = Eureka Serviceからこんにちは!!
WARN com.netflix.servo.jmx.JmxMonitorRegistry - Unable to un-register Monitor:MonitorConfig{name=bootstrap, tags=class=AsyncResolver, policy=DefaultPublishingPolicy}
javax.management.InstanceNotFoundException: com.netflix.servo:name=eurekaClient.resolver.lastLoadTimestamp,class=AsyncResolver,id=bootstrap,level=INFO,type=GAUGE

最後になんか例外を吐きますが、なんとか動いたようです。

よーく見ると、Application Serviceの接続先を解決できています。

INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Service Server URL = http://yourhost:8001/

というわけで、VIPと呼ばれるものから、利用するApplication Serviceの接続先ホスト名(またはIPアドレス)とポートを教えてくれるものだ、と。

なるほどー。

Application Serviceを追加してみる

せっかくなので、ものは試しとApplication Serviceを追加してみます。

設定ファイルを、別途用意。「eureka.name」と「eureka.port」のみ変更しています。
src/main/resources/eureka-service2.properties

eureka.region=default

eureka.name=sampleRegisteringService2

eureka.vipAddress=mysample.mydomain.net

eureka.port=8002

eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.serviceUrl.default=http://localhost:8080/eureka/v2/

で、この設定ファイルを読むようにApplication Serviceを起動。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.netflix.eureka.ExampleEurekaService -Deureka.client.props=eureka-service2

ログ(の一部)。

INFO org.littlewings.netflix.eureka.ExampleApplicationService - アプリケーション名 = SAMPLEREGISTERINGSERVICE2
INFO org.littlewings.netflix.eureka.ExampleApplicationService - HTTPサーバーのリッスンポート = 8002
INFO org.littlewings.netflix.eureka.ExampleApplicationService - VIP = mysample.mydomain.net
INFO org.littlewings.netflix.eureka.ExampleApplicationService - ServiceのステータスをUPに遷移します
INFO com.netflix.discovery.DiscoveryClient - Saw local status change event StatusChangeEvent [current=UP, previous=STARTING]
INFO org.littlewings.netflix.eureka.ExampleApplicationService - 起動しました。リクエストを待っています
INFO org.littlewings.netflix.eureka.ExampleApplicationService - Eurekaサーバーへ接続待ち中…
INFO org.littlewings.netflix.eureka.ExampleApplicationService - Eurekaサーバーへ接続しました = mysample.mydomain.net

この状態で、クライアントを実行します。

どちらがくるかはわかりませんが、何度か繰り返していると2つ起動しているApplication Serviceへアクセスすることを確認することができます。

## Application Service #1
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - VIP = mysample.mydomain.net:8001
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - HealthCheckUrl = http://xxxxx:8001/healthcheck
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - override =  UNKNOWN
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Application Name = SAMPLEREGISTERINGSERVICE
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Service Server URL = http://xxxxx:8001/
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Serviceからのメッセージ = Eureka Serviceからこんにちは!!

## Application Service #2
INFO com.netflix.discovery.DiscoveryClient - Not registering with Eureka server per configuration
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - VIP = mysample.mydomain.net:8002
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - HealthCheckUrl = http://yourhost:8002/healthcheck
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - override =  UNKNOWN
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Application Name = SAMPLEREGISTERINGSERVICE2
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Service Server URL = http://yourhost:8002/
INFO org.littlewings.netflix.eureka.ExampleEurekaClient - Serviceからのメッセージ = Eureka Serviceからこんにちは!!

最後に、Application Serviceをシャットダウンして終了です。Enterで終了するように仕込んであります。

INFO org.littlewings.netflix.eureka.ExampleApplicationService - シャットダウンします
WARN com.netflix.discovery.DiscoveryClient - Saw local status change event StatusChangeEvent [current=DOWN, previous=UP]
INFO com.netflix.discovery.DiscoveryClient - DiscoveryClient_SAMPLEREGISTERINGSERVICE/yourhost: registering service...
INFO com.netflix.discovery.DiscoveryClient - DiscoveryClient_SAMPLEREGISTERINGSERVICE/yourhost - registration status: 204
INFO com.netflix.discovery.DiscoveryClient - DiscoveryClient_SAMPLEREGISTERINGSERVICE/yourhost - deregister  status: 200
WARN com.netflix.servo.jmx.JmxMonitorRegistry - Unable to un-register Monitor:MonitorConfig{name=bootstrap, tags=class=AsyncResolver, policy=DefaultPublishingPolicy}
javax.management.InstanceNotFoundException: com.netflix.servo:name=eurekaClient.resolver.lastLoadTimestamp,class=AsyncResolver,id=bootstrap,level=INFO,type=GAUGE

やっぱり、最後に例外吐きますが…。

まとめ

だいぶ長くなりましたが、Eureka ServerがApplication Serviceの接続先を管理し、Application Clientはそれを利用する形態であることがわかりました。

動かしてみるまでなかなかピンとこないところもありましたが、まあ概要的にはわかって良かったです。

次は…Ribbonがやれる…かな…?