Skip to content

SpringCloud

基本概念

システムアーキテクチャ

これまで作ってきた多くのプロジェクトは モノリシックアーキテクチャ に属します。ここからは、大規模プロジェクトにより適した 分散アーキテクチャ を学びます。

モノリシックアーキテクチャ:業務のすべての機能を一つのプロジェクトに集中して開発し、一つのパッケージとしてデプロイします。

メリット:構造が簡単で、デプロイコストが低い。
デメリット:結合度が高い。

分散アーキテクチャ:業務機能ごとにシステムを分割し、各業務モジュールを独立したプロジェクトとして開発します。これを一つのサービスと呼びます。

メリット:サービス間の結合度を下げられ、サービスのアップグレードと拡張に有利です。
デメリット:構造が複雑で、運用、監視、デプロイの難度が高いです。

マイクロサービス

マイクロサービスは、よく設計された分散アーキテクチャの一つです。

マイクロサービスアーキテクチャの特徴:

  • 単一責任:サービスの分割粒度が小さく、各サービスは一つの業務能力に対応します。
  • サービス指向:マイクロサービスは外部へ業務インターフェースを公開します。
  • 自治:チーム、技術、データ、デプロイがそれぞれ独立します。
  • 隔離性が高い:サービス呼び出しでは、隔離、フォールトトレランス、降格処理を行い、連鎖問題 を避けます。

連鎖問題は、データの関連操作によって一連の変化が引き起こされる問題です。

有名なマイクロサービス技術体系には、SpringCloud と Alibaba Dubbo があります。
SpringCloud はさまざまなマイクロサービス機能コンポーネントを統合し、SpringBoot をもとに自動装配を実現しています。

サービス分割のまとめ:

  1. 異なるマイクロサービスで同じ業務を重複開発しない。
  2. マイクロサービスのデータは独立させ、他のマイクロサービスのデータベースへ直接アクセスしない。
  3. マイクロサービスは自分の業務をインターフェースとして公開し、他のマイクロサービスに使わせる。

リモート呼び出し

例:ユーザーサービスと注文サービスがあります。注文 ID で注文を検索すると同時に、その注文に属するユーザー情報も一緒に返す必要があります。

異なるサービスのデータベースは互いに独立しているため、バックエンドで HTTP リクエストをもう一度送信し、他のサービスのインターフェースを呼び出します。
Java コードで HTTP リクエストを送るため、ここでは RestTemplate を使います。

RestTemplate は Spring フレームワークが提供する同期 HTTP クライアントです。Java アプリケーション内で HTTP リクエストを送信し、レスポンスを処理するために使います。

java
@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

Java コードでリクエストを送信します。

java
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        Order order = orderMapper.findById(orderId);

        String url = "http://localhost:8081/user/" + order.getUserId();
        User user = restTemplate.getForObject(url, User.class);
        order.setUser(user);
        return order;
    }
}

提供者と消費者

  • サービス提供者:ある業務で、他のマイクロサービスから呼び出されるサービスです。他のマイクロサービスへインターフェースを提供します。
  • サービス消費者:ある業務で、他のマイクロサービスを呼び出すサービスです。

サービス呼び出し関係:

  • サービス提供者は、他のマイクロサービスが呼び出せるインターフェースを公開する。
  • サービス消費者は、他のマイクロサービスが提供するインターフェースを呼び出す。
  • 提供者と消費者の役割は相対的です。
  • 一つのサービスは、同時に提供者にも消費者にもなれます。

Eureka 登録センター

前の書き方には問題があります。リクエストアドレスがコードに固定されています。

Eureka の役割

消費者はサービス提供者の具体情報をどう取得するか:

  • サービス提供者は起動時に、自分の情報を Eureka に登録する。
  • Eureka はその情報を保存する。
  • 消費者はサービス名に基づいて、Eureka から提供者情報を取得する。

複数のサービス提供者がある場合、消費者はどう選ぶか:

  • サービス消費者は負荷分散アルゴリズムを使い、サービスリストから一つを選ぶ。

消費者はサービス提供者の健康状態をどう知るか:

  • サービス提供者は 30 秒ごとに EurekaServer へハートビートを送り、健康状態を報告する。
  • Eureka はサービスリスト情報を更新し、異常なハートビートのサービスを除外する。
  • 消費者は最新情報を取得できる。

Eureka アーキテクチャの役割:

EurekaServer:サーバー側、登録センター

  • サービス情報を記録する
  • ハートビートを監視する

EurekaClient:クライアント側

  • Provider:サービス提供者。例:user-service
    • 自分の情報を Eureka Server に登録する
    • 30 秒ごとに Eureka Server へハートビートを送る
  • Consumer:サービス消費者。例:order-service
    • サービス名に基づいて Eureka Server からサービスリストを取得する
    • サービスリストをもとに負荷分散し、一つのマイクロサービスを選んでリモート呼び出しを行う

Eureka Server を構築する

Step 1:新しい Maven モジュールを作成し、eureka-server 依存を導入します。

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Step 2:起動クラスにアノテーションを追加します。

java
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class,args);
    }
}

Step 3:設定情報を追加します。

yml
server:
  port: 10086
spring:
  application:
    name: eurekaserver

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

user-service を登録する

Step 1:登録したいサービスで eureka-client 依存を導入します。

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Step 2:設定ファイルに設定を追加します。

yml
spring:
  application:
    name: userserver

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

order-service も同じように登録できます。

サービス取得

サービス取得は、サービス名に基づいてサービスリストを取得し、そのリストに対して負荷分散を行います。

  1. OrderService のコードを変更し、アクセスする url のパスで ipport の代わりに サービス名 を使います。
java
String url = "http://userservice/user/" + order.getUserId();
  1. RestTemplate の Bean に 負荷分散 アノテーションを追加します。
java
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

Ribbon 負荷分散

Ribbon はクライアント側の負荷分散コンポーネントです。サービスリストから利用可能なインスタンスを選び、リクエストを送ります。

Nacos 登録センター

起動方式:

shell
startup.cmd -m standalone

サービス登録

親プロジェクトに spring-cloud-alibaba の管理依存を追加します。

xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

既存の Eureka 依存をコメントアウトし、Nacos クライアント依存を追加します。

xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

設定ファイルを変更します。

yml
spring:
  application:
    name: orderservice
  cloud:
    nacos:
      server-addr: localhost:8848

Nacos サービス階層保存モデル

一つのサービスには複数のインスタンスを持てます。大企業では、インスタンスを異なるサーバーにデプロイします。一つのサーバールームを一つのクラスターと呼びます。
サービス呼び出しでは、できるだけローカルクラスターのサービスを呼び出します。クロスクラスター呼び出しは遅延が大きいため、ローカルクラスターが使えない場合だけ他のクラスターを使います。

クラスター属性を設定します。

yml
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HB

同じクラスターを優先したい場合、負荷分散の IRule を変更します。

yml
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule

NacosRule の負荷分散戦略:

  1. 同じクラスターのサービスインスタンスリストを優先する。
  2. ローカルクラスターに提供者が見つからない場合だけ、他のクラスターを探す。その際、警告が出る。
  3. 利用可能なインスタンスリストが決まった後、ランダム負荷分散でインスタンスを選ぶ。

重みによる負荷分散

実際のデプロイでは、サーバー性能に差があります。性能が高いマシンには、より多くのリクエストを担当させたい場合があります。

Nacos は重み設定でアクセス頻度を制御できます。重みが大きいほどアクセス頻度が高くなります。Nacos コンソールでインスタンスの重みを設定できます。

まとめ:

  1. Nacos コンソールでインスタンスの重みを 0〜1 の間で設定できる。
  2. 同じクラスター内では、重みが高いインスタンスほどアクセス頻度が高い。
  3. 重みを 0 にすると、完全にアクセスされなくなる。

環境隔離 - namespace

Nacos では、サービス保存とデータ保存の最外層に namespace があります。これは最外層の隔離に使います。
注意:サービスは現在の名前空間のサービスだけにアクセスでき、他の名前空間のサービスにはアクセスできません。

構造:Namespace の下に Group があり、その下に Service / Data があります。

新しい名前空間を作成する:Nacos コンソール -> 名前空間 -> 新規名前空間。
コードでサービスを新しい名前空間へ移動します。

yml
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HB
        namespace: xxxx

Nacos の環境隔離:

  1. namespace は環境隔離に使う。
  2. namespace には一意な id がある。
  3. 異なる namespace 下のサービスは互いに見えない。

一時インスタンスと非一時インスタンス

サービスを Nacos に登録するとき、一時インスタンスまたは非一時インスタンスとして登録できます。

yml
spring:
  cloud:
    nacos:
      server-addr:
      discovery:
        namespace:
        ephemeral: false

一時インスタンスが停止すると、Nacos のサービスリストから削除されます。非一時インスタンスは削除されません。

Nacos と Eureka の比較

共通点:

  • どちらもサービス登録とサービス取得をサポートする
  • どちらもサービス提供者のハートビートによる健康チェックをサポートする

違い:

  • Nacos はサーバー側から提供者状態を能動的に検査できる。一時インスタンスはハートビート方式、非一時インスタンスは能動検査方式を使う
  • 一時インスタンスはハートビート異常時に削除されるが、非一時インスタンスは削除されない
  • Nacos はサービスリスト変更のメッセージプッシュをサポートし、更新がより早い
  • Nacos クラスターはデフォルトで AP 方式を使う。非一時インスタンスが存在する場合は CP 方式を使う。Eureka は AP 方式を使う

AP:可用性を保証する
CP:一貫性を保証する

統一設定管理

一部の設定情報を Nacos 設定ファイルに書くと、統一管理できます。このファイルはホットロードもサポートします。

新規設定手順:設定管理 -> 設定リスト -> 新規設定。
Data ID(設定ファイル名 id)命名規則:サービス名-dev(profile実行環境).yaml
Group:DEFAULT_GROUP

通常の起動手順:

  1. プロジェクト起動
  2. ローカル設定 application.yml を読み込む
  3. Spring コンテナを作成する
  4. Bean を読み込む

Nacos から設定ファイルを読み込む場合、この操作はローカル設定ファイルより前に行う必要があります。
そのため、application.yml より優先度が高い bootstrap.yml を作成し、Nacos アドレスの設定を書きます。

Step 1:Nacos 設定管理クライアント依存を導入します。

xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

Step 2:bootstrap.yml を作成し、サービス名、Nacos アドレス、環境などを書きます。

yml
spring:
  application:
    name: userservice
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: localhost:8848
      config:
        file-extension: yaml
      discovery:
        cluster-name: HB

Step 3:バックエンドで設定値を取得したい場合は、@Value("${}") を使います。

java
@Value("${pattern.dateformat}")
private String dateformat;

@GetMapping("/now")
public String now(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}

設定のホット更新

Nacos の設定ファイルが変更された後、マイクロサービスは再起動なしで変更を感知できます。ただし、次のどちらかの設定が必要です。

方式一:@Value で注入した変数があるクラスに @RefreshScope を追加します。

java
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}

方式二:設定クラスを作成し、@ConfigurationProperties を使います。

java
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
	private String dateformat;
}

注意:

  • すべての設定が設定センターに適しているわけではありません。管理が面倒になる場合があります。
  • 重要なパラメータや、実行中に調整したいパラメータを Nacos 設定センターへ置くことが推奨されます。多くはカスタム設定です。

Nacos クラスター構築

Step 1:データベースを構築します。MySQL を推奨します。nacos データベースを作成し、Nacos が提供する公式 SQL 初期化スクリプトを実行します。

Step 2:Nacos をダウンロードして解凍します。conf ディレクトリ下の cluster.conf に、各ノードの IP とポートを行ごとに設定します。

text
#it is ip  ip:port
#example
192.168.16.101:8847
192.168.16.102
192.168.16.103

Step 3:nacos/conf/application.properties を変更し、MySQL データソースの URL、ユーザー名、パスワードを追加します。

properties
spring.sql.init.platform=mysql

db.num=1
db.url.0=jdbc:mysql://${mysql_host}:${mysql_port}/${nacos_database}?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=${mysql_user}
db.password=${mysql_password}

デフォルト認証プラグインを有効にします(任意、推奨)。

properties
nacos.core.auth.enabled=true
nacos.core.auth.system.type=nacos
nacos.core.auth.plugin.nacos.token.secret.key=${自定义,保证所有节点一致}
nacos.core.auth.server.identity.key=${自定义,保证所有节点一致}
nacos.core.auth.server.identity.value=${自定义,保证所有节点一致}

Step 4:Nacos クラスターを起動します。各ノードで次のコマンドを実行します。

shell
# Linux/Unix/Mac
sh startup.sh

# Ubuntu
bash startup.sh

# Windows
startup.cmd

Step 5:Nginx のリバースプロキシを使い、conf/nginx.conf を変更します。

properties
upstream nacos-cluster {
    server 127.0.0.1:8845;
    server 127.0.0.1:8846;
    server 127.0.0.1:8847;
}

server {
	listen       80;
	server_name		1ocalhost;

	location /nacos {
		proxy_pass http://nacos-cluster;
	}
}

最後に Java 側の Nacos アドレスを Nginx の 80 ポートへ変更します。

yml
spring:
  application:
    name: userservice
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: localhost:80
      config:
        file-extension: yaml
      discovery:
        cluster-name: HB

Feign

RestTemplate の問題

java
String url = "http: //userservice/user/" + order.getUserId();
User user = restTemplate.getFor0bject(url, User.class);
  • 可読性が低い
  • パラメータが複雑
  • URL の保守が難しい

Feign は宣言式 HTTP クライアントです。公式アドレス:https://github.com/OpenFeign/feign

使用例

  1. 座標を導入します。
xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. リクエストを送るサービスの起動クラスに @EnableFeignClients を追加し、Feign 機能を有効にします。
java
@EnableFeignClients
  1. Feign クライアントを書きます。
java
@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}
  1. 宣言したメソッドで元の RestTemplate を置き換えます。
java
@Autowired
private UserClient userClient;

public Order queryOrderById(Long orderId) {
    Order order = orderMapper.findById(orderId);
    User user = userClient.getUser(order.getUserId());
    order.setUser(user);
    return order;
}

コードがかなり簡潔になり、見やすくなります。

Feign のカスタム設定

設定項目説明内容
feign.Logger.Levelログレベルを変更するNONEBASICHEADERSFULL
feign.codec.Decoderレスポンス結果の解析器デフォルトでは JSON 文字列を Java オブジェクトに解析する。カスタム可能
feign.codec.Encoderリクエストパラメータのエンコードデフォルトでは HTTP リクエスト形式へエンコードする。カスタム可能
feign.Contractサポートするアノテーション形式デフォルトでは SpringMVC アノテーションをサポートする
feign.Retryer失敗時リトライ機構デフォルトではリトライなし。カスタム可能

通常はログレベルだけ設定すれば十分です。

yml
feign:
	client:
		config:
			default:
				loggerLevel: FULL
			userservice:
				loggerLevel: full

設定クラスでも Feign を設定できます。

java
public class FeignClientConfigurationP{
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC;
    }
}

グローバル設定なら @EnableFeignClients に指定します。
局所設定なら @FeignClient に指定します。

性能最適化

Feign の内部クライアント実装:

  • URLConnection:デフォルト実装。接続プールをサポートしない
  • Apache Httpclient:接続プールをサポートする
  • OKHttp:接続プールをサポートする

Feign の性能最適化:

  1. デフォルトの URLConnection の代わりに接続プールを使う。
  2. ログレベルは basic または none を使う。

HttpClient サポートを追加する例:

xml
<dependency>
    <groupid>io.github. openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
yml
feign:
	client:
		config:
			default:
				loggerLevel: BASIC
	httpclient:
		enabled: true
		max-connections: 200
		max-connections-per-route: 50

実際のプロジェクトでの使い方

FeignClient を独立モジュールへ切り出し、インターフェース関連の POJO とデフォルト Feign 設定もこのモジュールに入れ、消費者側へ提供します。

  1. feign-api module を作成し、Feign の依存を導入する。
  2. order-service に書いた UserClientUserDefaultFeignConfigurationfeign-api プロジェクトへコピーする。
  3. order-servicefeign-api の依存を導入する。
  4. order-service 内で、この三つのコンポーネントに関係する import を feign-api のパッケージへ変更する。

変更後の問題:カスタム FeignClientSpringBootApplication のスキャン範囲外にある場合、二つの解決方法があります。

  1. FeignClient があるパッケージを指定する。
java
@EnableFeignClients(basePackages = 'cn.itcast.feign.clients')
  1. FeignClient のバイトコードを指定する。こちらの方が推奨されます。
java
@EnableFeignClients(clients = {UserClient.class})

Released under the MIT License.