エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Spring特別勉強会レポート(後編)

前回は Pivotal社に所属するSpring コントリビュータの Josh Long 氏によるプライベートセッション「Bootiful Application」のうち、マイクロサービスと Spring Boot の内容を紹介しました。

今回は、後半部分である Spring Cloud についてのレポートです。

f:id:enterprisegeeks:20151026125320j:plain

なお、セッションではLong氏が次々とソースコードを矢継ぎ早に書き換えていったため、 この記事に掲載するサンプルコードは、筆者が内容に即したものをあらためて作成したものになります。

Spring Cloud

マイクロサービスでシステムを構築すると、複数のSpring Boot アプリケーションが連携する分散構成となります。

Spring Cloud は分散構成のアプリケーションを制御するためのプロダクト群です。 複数のアプリケーションの設定管理、サービス管理、負荷分散、耐障害性、セキュリティなどの機能が提供されます。

Cloud と銘打ってはいますが、クラウド環境だけでなくオンプレミス環境や、単一のPCでも利用できます。 複数のアプリケーションが単一のサーバで動いていても、複数のサーバやクラウドに分散して動いていても、 アプリケーションのデプロイを簡単に行えるようになっています。

Spring Cloud Config

データベースの接続先や起動ポートなどの設定は、通常プロパティファイルなどに記述して管理します。 Spring Boot ではプロパティは application.properties というファイルに記述し、 jarファイル内にパッケージングします。

ですが、設定ファイルを書き換えるたびにアプリケーションをパッケージングしなおすのは面倒です。 Spring Cloud Config はこの設定ファイルを外出しし、コンフィグサーバで一括管理する仕組みです。

Spring Cloud Config では、設定ファイルを保存しておくGitリポジトリ、設定ファイルを管理するコンフィグサーバ、 コンフィグサーバから設定を取得するコンフィグクライアントから構成されます。 なお、コンフィグサーバとコンフィグクライアントはどちらもSpring Boot アプリケーションです。

f:id:enterprisegeeks:20150925230840p:plain

コンフィグサーバの設定

コンフィグサーバとして作成するSpring Boot アプリケーションは、依存性に 「spring-cloud-config-server」 を記述し、 設定ファイルに GitリポジトリのURL を記述します。 なお、デフォルトはGitリポジトリですが、ファイルシステムなども使用可能です。

spring.cloud.config.server.git.uri=https://github.com/hoge/springConfig
server.port=8888

そしてアプリケーションクラスに、@EnableConfigServer を設定すれば準備完了です。 これでコンフィグサーバには、設定ファイルを取得するためのエンドポイントが設定されます。

@EnableConfigServer
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

コンフィグクライアント

コンフィグサーバから設定を取得するアプリケーションでは、依存性に 「spring-cloud-config-client」 を追加します。 また、設定ファイル (application.propeties) の代わりに、 bootstrap.propertiesファイルを作成し、アプリケーション名とコンフィグサーバのURLを記述して起動します。

pring.application.name=app_a
spring.cloud.config.uri=http://127.0.0.1:8888

これだけで、アプリケーション起動時にコンフィグサーバ経由でGitリポジトリから設定ファイルを取得します。

Git リポジトリ

Git リポジトリには、全アプリケーション共通で使用する application.propetiesと、各アプリケーション固有のプロパティファイルを作成して公開します。

例えば、 アプリケーション名が app_a の場合、application.properties と app_a.properties が app_a で使用されるプロパティになります。

Long氏によれば、application.properties と各アプリケーションのプロパティファイルで同名のプロパティがあれば、 後者の方が優先されるため、デフォルト値を前者に設定し、アプリケーション固有の設定を後者に記述して上書きするような使い方もできるとのことでした。

また、プロパティファイルは、Javaのプロパティファイルの形式の他にYAMLもサポートしているとのことです。

プロパティの再ロード

コンフィグクライアントには、@RefreshScopeというアノテーションと、 /refresh というエンドポイントが追加されています。 Git リポジトリの設定ファイルを修正し、アプリケーションの再起動を行わずに変更した設定ファイルの内容を反映したいという場合、 再反映を有効にしたいBean に @RefreshScope を付与しておき、 /refresh に POST リクエストを送信します。

@RefreshScope // refresh による設定再ロード対象
@RestController
class MessageRestController {
    
    /*
     *  設定ファイルからインジェクションする項目
     *  /refresh を行うと更新される 
     */
    @Value("${message}")
    private String message;

    @RequestMapping("/message")
    String message() {
        return this.message;
    }
}

実際のデモでも、設定ファイルを書き換えただけでは設定が再反映されず、/refresh へのリクエストを明示的に発行して初めて再反映が行われることを確認しました。 Long氏によれば、このような停止をしない設定変更を行う技法を Future Flag と呼ぶようです。

アプリケーションの設定変更は、どのようなアプリケーションでも需要があります。筆者も経験があり、データベースに設定を保持したり、あるいは頻度が少ないからアプリケーションの再起動を受容するなど、色々な対策を行ってきました。 それを、Spring Config では、わずかな設定だけで起動中に簡単に変更できたため、場内からは歓声が上がりました。

このように、Spring Cloud は複数のアプリケーションの設定ファイルを一元管理し、さらに再ロード機能も備えた有用な機能といえます。

Netflix OSS との連携

Spring Cloud にも多くのプロダクトがあるのですが、その中でも今回は Netflix 社の OSS との連携を行うプロダクトの紹介が行われました。

Spring Cloud Netflix

動画配信で知られる Netflix 社は自社のサービスを AWS上にマイクロサービスで構築しており、 Long 氏によるとそのマイクロサービスの数は600を超えるそうです。 Netflix 社は、マイクロサービス運用のノウハウを OSS として公開しています。

Netflix Open Source Software Center

今回紹介するのは以下のプロダクトです。

  • サービス管理 : Eureka
  • クライアントロードバランサ : Ribbon
  • 宣言的 REST クライアント : Feign
  • リバースプロキシ : Zuul
  • サーキットブレーカ : Hystrix, Hystrix Dashboard

セッションでは上記のプロダクトを使用したサンプルを段階的に紹介していく流れでしたが、 最終的に完成したサービスの全体像を最初に示した方が各プロダクトの役割を把握しやすいと思いましたので、 サービスの全体像を以下に示します。

f:id:enterprisegeeks:20150927135754p:plain

  • config-service - 前述のCloud Config サーバ。他のアプリケーションの設定ファイルを管理する。
  • eureka-service - config-service以外のサービスのホストやポートなどの情報を管理する。
  • hystrix-dashborad - 他のサービスの統計情報を収集し、図示する。
  • reservation-service - 業務アプリケーションに相当し、DBから予約情報を取得するREST エンドポイントを持つ。
  • reservation-client - 同じく業務アプリケーションに相当し、クライアントからの要求を処理し、reservation-serviceと連携する。

それでは、各プロダクトの詳細をお伝えしてきます。

Eureka によるサービス管理

Eureka は各マイクロサービスの管理を行うライブラリです。 複数のサービスが連携する環境では、各サービスのホスト名とポートをそれぞれが覚えておく必要があり、 その管理は煩雑になります。

Eureka は各サービスの論理名とホスト名、ポートを紐付けて管理するDNSのような働きをします。

Eureka Server

Eureka Server は Eureka のサービスを提供するサーバとなります。 Spring Boot では、依存性 「spring-cloud-starter-eureka-server」 を有効にして、アプリケーションクラスに @EnableEurekaServer アノテーションを設定しておきます。

@EnableEurekaServer
@SpringBootApplication
public class EurekaServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServiceApplication.class, args);
    }
}

f:id:enterprisegeeks:20151026105059p:plain Eureka のデモを行う Long氏。表示されている内容は Eureka ServerのUI。

Eureka Client

Eureka Server へ 自身を登録するサービスは Eureka Client と呼ばれます。 Eureka Client としたいサービスは、依存性 「spring-cloud-starter-eureka」 を設定し、 アプリケーションクラスに、@EnableDiscoveryClient アノテーションを記述します。

@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

また、Eureka サーバのURLも設定ファイルに記述しておきます。

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

これでサービスの起動時、自身を Eureka Serverに登録するようになります。

サービス名によるアドレスの取得

Eureka Client では、他の Eureka Client のホスト名,ポートをサービス名で参照することができるようになります。

@Component
public class HogeBean {
    // Eureka Server への問い合わせを行うBean
    @Autowired
    private DiscoveryClient dc;
    
    public void pringHostAndPort() {
        // サービス名 reservation-service のホスト、ポートを取得
        dc.getInstances("reservation-service")
            .forEach(si -> 
            System.out.println(si.getHost() + ':' + si.getPort()));
    }
}

これで、各サービスが分散構成となっても、実際のホストやポートなどを気にしなくても良いようになります。

また、Long氏によれば今回のデモではEurekaサーバは1台で行いましたが、通常は最低でも3台のEurekaサーバによる冗長構成を取るとのことでした。 事実、1台のEurekaサーバでも起動はできるのですが、常に冗長ノードが無いというエラーログが出力され続けていました。

Ribbon, Feign によるRESTアクセス

Eureka を導入するとサービス名から実際のホストが取得可能になりますが、毎回そのような処理を行うのは少々煩雑です。

Ribbon, Feign を導入するとより簡単に他のサービスへ REST アクセスが行えるようになります。

Ribbon

Ribbon はクライアントロードバランサと呼ばれるライブラリです。 Ribbon を使うと http://hoge-service/api のように、サービス名を直接URLに指定できるようになります。

Ribbon を導入することで、 Spring の REST API である RestTemplateにサービス名のURLを記述可能になります。 Ribbonを使用するには、依存性 「spring-cloud-starter-ribbon」 を設定します。

@Component
public class HogeBean {
    @Autowired
    private RestTemplate restTemplate;
    
    public List<Reservation> userRestTemplate () {
        // List<Reservation> 型のパラメータ作成
        ParameterizedTypeReference<List<Reservation>> ptr
                = new ParameterizedTypeReference<List<Reservation>>(){};
        
        // 本来なら、reservation-serviceのホスト名、ポートを取得する必要があるが、
        // 直接サービス名でアクセス可能。
        return restTemplate.exchange(
            "http://reservation-service/reservations",
            HttpMethod.GET, null, ptr).getBody();
    }
}

上記ではサービス名をURLに記述できることに注目しましたが、 Ribbon の主要な機能はクライアントサイドのロードバランサーであることです。

Eureka には負荷分散のため、同一のサービスを複数のホストに登録することができます。 Ribbon はサービス名を実際のホストに変換する際に複数のホストからどのホストに接続するかという負荷分散機能も併せて提供します。 Long氏によれば、一般的なラウンドロビンやスティッキーセッションの他、AWS複数アベイラビリティゾーン(東京、アメリカなどの地域) にまたがる負荷分散も行うことができるとのことでした。

Feign

Feign は宣言的な REST クライアントを作成するライブラリです。 インターフェースに REST エンドポイントなどのアノテーションを設定することで、 実装なしで REST クライアントを作成できます。

Feign を使用するには、依存性 「spring-cloud-starter-feign」 を有効にし、アプリケーションクラスに @EnableFeignClients を記述します。

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

前述の、RestTemplate を使用したコードと同じ挙動をコードを Feign で実装したものが以下です。

@FeignClient("reservation-service")
interface ReservationsRestClient {

    @RequestMapping(value = "/reservations", method = RequestMethod.GET)
    Collection<Reservation> getReservations();
}

@FeignClient でサービス名を設定し、メソッド@RequestMapping でエンドポイントやHTTPメソッドを指定します。 エンドポイントから取得したデータは、メソッドの戻り値の型に変換されます。

あとは、このインターフェースを インジェクションしメソッドを呼び出すだけで REST アクセスが実行されます。

Zuul Proxy によるリバースプロキシ

続いて登場したのは Zuul です。 Zuul は 各サービスとクライアントの間に立ちシステムの門番のような働きをするエッジサービスという位置づけのライブラリです。

用途として、サービスを呼び出す前のセキュリティチェックやデータ変換、シングルサインオン、 URL書き換えといった様々なものがあるとのことでした。

依存性 「spring-cloud-starter-zuul」 を設定し、@EnableZuulProxy をアプリケーションクラスに付与すると リバースプロキシが有効になります。

@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Eureka を導入している状況で、Zuul Proxy を有効にしていると、Eureka に登録したサービス名をエンドポイントに記述して、 各サービスにアクセス可能になります。

例えば、 reservation-client が稼動している http://localhost:9999/ に対して、 http://localhost:9999/reservation-service/reservations にアクセスすると、 reservation-service が実際に稼動している http://localhost:8000/reservations に転送が中継されます。

こうすることで、Zuul Proxy が稼動しているアプリケーションのみを外部公開することで、 他のサービスには外部からの直接アクセスを防ぐことができるようになります。 また、必ず Zuul Proxy を経由することになりますので、セキュリティチェックの一元化なども行うことができるようになります。

Hystrix によるサーキットブレーカ

先に登場したプロダクトでアプリケーションの機能面が実装できるようになりました。 続いて紹介されたのは運用面を強化するプロダクトです。

Hystrix はシステムの信頼性を向上させるライブラリです。 その機能の一つに、障害の起きたサービスを切り離す仕組みであるサーキットブレーカがあります。

マイクロサービスでは、複数サービスを連携させますが、下位のサービスが障害によって呼び出しができない場合や アクセス集中によるパフォーマンスの遅延が発生したりする場合があります。 そのような状態を放置しておくと、リソース不足に陥ったり、システムが停止したりする恐れがあります。

サーキットブレーカを導入すると、下位のサービスの障害・遅延時にサービス呼び出しを行わずにデフォルト値を返却するなどの 動作を定義することができます。

Hystrix を使用するためには、 依存性 「spring-cloud-starter-hystrix」 を設定し、 アプリケーションクラスに @EnableCircuitBreaker アノテーションを設定します。

@EnableCircuitBreaker
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

サーキットブレーカを定義したサンプルは以下のようになります。

@Component
class ReservationIntegration {
    @Autowired //前述のFeignクライアント
    private ReservationsRestClient reservationsRestClient;

    @HystrixCommand(fallbackMethod = "getReservationNamesFallback")
    public Collection<String> getReservationNames() {
        return reservationsRestClient.getReservations()
                .stream()
                .map(Reservation::getReservationName)
                .collect(Collectors.toList());
    }
    
    // getReservationNames が障害時に呼び出される
    public Collection<String> getReservationNamesFallback() {
        // 障害時は空のリストをとりあえず返却する
        return Collections.emptyList();
    }
}

getReservationNamesは他のサービスと通信を行うメソッドですが、そこに@HystrixCommandアノテーションを付与し、 このメソッドの実行が失敗した場合に実行されるメソッド名をfallbackMethod属性で記述しておきます。 fallbackMethodに指定したgetReservationNamesFallbackメソッドは、空のコレクションを返すようなっています。

このようにしておくと、サービス呼び出しが失敗したとしても、呼び出し元には空のコレクションを返却し、システム全体が停止しないようにしています。

Hystrix Dashboard

Hystrix を有効にしたサービスでは /hystrix.stream というエンドポイントが追加されます。 このエンドポイントは、@HystrixCommand アノテーションを付与したメソッドに関して 成功・失敗の呼び出し回数やサーキットブレーカの状態などの統計情報を常に出力し続けます。

この統計情報を収集して可視化するライブラリが、Hystrix Dashboardです。 Hystrix Dashboard を使用して、負荷状況や障害検知に役立てることができるようになります。

Hystrix Dashboard を有効にするには、依存性 「spring-cloud-starter-hystrix-dashboard」 を設定し、 アプリケーションクラスに、 @EnableHystrixDashboard アノテーションを付与するだけです。

@EnableDiscoveryClient
@EnableHystrixDashboard
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

これで、/hystrix.stream エンドポイントが有効になりますので、他のサービスの/hystrix.streamエンドポイントを入力して監視を開始します。

f:id:enterprisegeeks:20150928190609p:plain

ダッシュボードの画面では、 Hystrix コマンドの実行状況がリアルタイムで更新され続けます。

f:id:enterprisegeeks:20150928190653p:plain

まとめ

Spring Cloud の中の Spring Cloud Config, Spring Cloud Netflix に関するレポートをお届けしました。 今回登場したもの以外にも、Spring Cloud にも多くのプロダクトがありますので、是非 Spring Cloudの公式サイトを見てみてください。

Spring Boot に引き続き、Spring Cloud も非常に密度が濃いセッションでした。 弊社では Spring Boot の経験者は多数いますが、Spring Cloud の有識者は少なく、 デモだけではなく、実運用での使い方などのレクチャーもあり、とても有意義なセッションでした。

最後になりますが、快くプライベートセッションを引き受けていただいた Josh Long氏には 大変感謝しております。

f:id:enterprisegeeks:20151026125428j:plain

ありがとうございました!

[前多 賢太郎]