SpringCloud
基本概念
システムアーキテクチャ
これまで作ってきた多くのプロジェクトは モノリシックアーキテクチャ に属します。ここからは、大規模プロジェクトにより適した 分散アーキテクチャ を学びます。
モノリシックアーキテクチャ:業務のすべての機能を一つのプロジェクトに集中して開発し、一つのパッケージとしてデプロイします。
メリット:構造が簡単で、デプロイコストが低い。
デメリット:結合度が高い。
分散アーキテクチャ:業務機能ごとにシステムを分割し、各業務モジュールを独立したプロジェクトとして開発します。これを一つのサービスと呼びます。
メリット:サービス間の結合度を下げられ、サービスのアップグレードと拡張に有利です。
デメリット:構造が複雑で、運用、監視、デプロイの難度が高いです。
マイクロサービス
マイクロサービスは、よく設計された分散アーキテクチャの一つです。
マイクロサービスアーキテクチャの特徴:
- 単一責任:サービスの分割粒度が小さく、各サービスは一つの業務能力に対応します。
- サービス指向:マイクロサービスは外部へ業務インターフェースを公開します。
- 自治:チーム、技術、データ、デプロイがそれぞれ独立します。
- 隔離性が高い:サービス呼び出しでは、隔離、フォールトトレランス、降格処理を行い、
連鎖問題を避けます。
連鎖問題は、データの関連操作によって一連の変化が引き起こされる問題です。
有名なマイクロサービス技術体系には、SpringCloud と Alibaba Dubbo があります。
SpringCloud はさまざまなマイクロサービス機能コンポーネントを統合し、SpringBoot をもとに自動装配を実現しています。
サービス分割のまとめ:
- 異なるマイクロサービスで同じ業務を重複開発しない。
- マイクロサービスのデータは独立させ、他のマイクロサービスのデータベースへ直接アクセスしない。
- マイクロサービスは自分の業務をインターフェースとして公開し、他のマイクロサービスに使わせる。
リモート呼び出し
例:ユーザーサービスと注文サービスがあります。注文 ID で注文を検索すると同時に、その注文に属するユーザー情報も一緒に返す必要があります。
異なるサービスのデータベースは互いに独立しているため、バックエンドで HTTP リクエストをもう一度送信し、他のサービスのインターフェースを呼び出します。
Java コードで HTTP リクエストを送るため、ここでは RestTemplate を使います。
RestTemplate は Spring フレームワークが提供する同期 HTTP クライアントです。Java アプリケーション内で HTTP リクエストを送信し、レスポンスを処理するために使います。
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}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 依存を導入します。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>Step 2:起動クラスにアノテーションを追加します。
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}Step 3:設定情報を追加します。
server:
port: 10086
spring:
application:
name: eurekaserver
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eurekauser-service を登録する
Step 1:登録したいサービスで eureka-client 依存を導入します。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>Step 2:設定ファイルに設定を追加します。
spring:
application:
name: userserver
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eurekaorder-service も同じように登録できます。
サービス取得
サービス取得は、サービス名に基づいてサービスリストを取得し、そのリストに対して負荷分散を行います。
OrderServiceのコードを変更し、アクセスするurlのパスでipとportの代わりにサービス名を使います。
String url = "http://userservice/user/" + order.getUserId();RestTemplateの Bean に負荷分散アノテーションを追加します。
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}Ribbon 負荷分散
Ribbon はクライアント側の負荷分散コンポーネントです。サービスリストから利用可能なインスタンスを選び、リクエストを送ります。
Nacos 登録センター
起動方式:
startup.cmd -m standaloneサービス登録
親プロジェクトに spring-cloud-alibaba の管理依存を追加します。
<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 クライアント依存を追加します。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>設定ファイルを変更します。
spring:
application:
name: orderservice
cloud:
nacos:
server-addr: localhost:8848Nacos サービス階層保存モデル
一つのサービスには複数のインスタンスを持てます。大企業では、インスタンスを異なるサーバーにデプロイします。一つのサーバールームを一つのクラスターと呼びます。
サービス呼び出しでは、できるだけローカルクラスターのサービスを呼び出します。クロスクラスター呼び出しは遅延が大きいため、ローカルクラスターが使えない場合だけ他のクラスターを使います。
クラスター属性を設定します。
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HB同じクラスターを優先したい場合、負荷分散の IRule を変更します。
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRuleNacosRule の負荷分散戦略:
- 同じクラスターのサービスインスタンスリストを優先する。
- ローカルクラスターに提供者が見つからない場合だけ、他のクラスターを探す。その際、警告が出る。
- 利用可能なインスタンスリストが決まった後、ランダム負荷分散でインスタンスを選ぶ。
重みによる負荷分散
実際のデプロイでは、サーバー性能に差があります。性能が高いマシンには、より多くのリクエストを担当させたい場合があります。
Nacos は重み設定でアクセス頻度を制御できます。重みが大きいほどアクセス頻度が高くなります。Nacos コンソールでインスタンスの重みを設定できます。
まとめ:
- Nacos コンソールでインスタンスの重みを 0〜1 の間で設定できる。
- 同じクラスター内では、重みが高いインスタンスほどアクセス頻度が高い。
- 重みを 0 にすると、完全にアクセスされなくなる。
環境隔離 - namespace
Nacos では、サービス保存とデータ保存の最外層に namespace があります。これは最外層の隔離に使います。
注意:サービスは現在の名前空間のサービスだけにアクセスでき、他の名前空間のサービスにはアクセスできません。
構造:Namespace の下に Group があり、その下に Service / Data があります。
新しい名前空間を作成する:Nacos コンソール -> 名前空間 -> 新規名前空間。
コードでサービスを新しい名前空間へ移動します。
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HB
namespace: xxxxNacos の環境隔離:
namespaceは環境隔離に使う。- 各
namespaceには一意な id がある。 - 異なる
namespace下のサービスは互いに見えない。
一時インスタンスと非一時インスタンス
サービスを Nacos に登録するとき、一時インスタンスまたは非一時インスタンスとして登録できます。
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
通常の起動手順:
- プロジェクト起動
- ローカル設定
application.ymlを読み込む - Spring コンテナを作成する
- Bean を読み込む
Nacos から設定ファイルを読み込む場合、この操作はローカル設定ファイルより前に行う必要があります。
そのため、application.yml より優先度が高い bootstrap.yml を作成し、Nacos アドレスの設定を書きます。
Step 1:Nacos 設定管理クライアント依存を導入します。
<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 アドレス、環境などを書きます。
spring:
application:
name: userservice
profiles:
active: dev
cloud:
nacos:
server-addr: localhost:8848
config:
file-extension: yaml
discovery:
cluster-name: HBStep 3:バックエンドで設定値を取得したい場合は、@Value("${}") を使います。
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}設定のホット更新
Nacos の設定ファイルが変更された後、マイクロサービスは再起動なしで変更を感知できます。ただし、次のどちらかの設定が必要です。
方式一:@Value で注入した変数があるクラスに @RefreshScope を追加します。
@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 を使います。
@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 とポートを行ごとに設定します。
#it is ip ip:port
#example
192.168.16.101:8847
192.168.16.102
192.168.16.103Step 3:nacos/conf/application.properties を変更し、MySQL データソースの URL、ユーザー名、パスワードを追加します。
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}デフォルト認証プラグインを有効にします(任意、推奨)。
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 クラスターを起動します。各ノードで次のコマンドを実行します。
# Linux/Unix/Mac
sh startup.sh
# Ubuntu
bash startup.sh
# Windows
startup.cmdStep 5:Nginx のリバースプロキシを使い、conf/nginx.conf を変更します。
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 ポートへ変更します。
spring:
application:
name: userservice
profiles:
active: dev
cloud:
nacos:
server-addr: localhost:80
config:
file-extension: yaml
discovery:
cluster-name: HBFeign
RestTemplate の問題
String url = "http: //userservice/user/" + order.getUserId();
User user = restTemplate.getFor0bject(url, User.class);- 可読性が低い
- パラメータが複雑
- URL の保守が難しい
Feign は宣言式 HTTP クライアントです。公式アドレス:https://github.com/OpenFeign/feign
使用例
- 座標を導入します。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>- リクエストを送るサービスの起動クラスに
@EnableFeignClientsを追加し、Feign 機能を有効にします。
@EnableFeignClients- Feign クライアントを書きます。
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}- 宣言したメソッドで元の
RestTemplateを置き換えます。
@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 | ログレベルを変更する | NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | レスポンス結果の解析器 | デフォルトでは JSON 文字列を Java オブジェクトに解析する。カスタム可能 |
feign.codec.Encoder | リクエストパラメータのエンコード | デフォルトでは HTTP リクエスト形式へエンコードする。カスタム可能 |
feign.Contract | サポートするアノテーション形式 | デフォルトでは SpringMVC アノテーションをサポートする |
feign.Retryer | 失敗時リトライ機構 | デフォルトではリトライなし。カスタム可能 |
通常はログレベルだけ設定すれば十分です。
feign:
client:
config:
default:
loggerLevel: FULL
userservice:
loggerLevel: full設定クラスでも Feign を設定できます。
public class FeignClientConfigurationP{
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC;
}
}グローバル設定なら @EnableFeignClients に指定します。
局所設定なら @FeignClient に指定します。
性能最適化
Feign の内部クライアント実装:
URLConnection:デフォルト実装。接続プールをサポートしないApache Httpclient:接続プールをサポートするOKHttp:接続プールをサポートする
Feign の性能最適化:
- デフォルトの
URLConnectionの代わりに接続プールを使う。 - ログレベルは
basicまたはnoneを使う。
HttpClient サポートを追加する例:
<dependency>
<groupid>io.github. openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>feign:
client:
config:
default:
loggerLevel: BASIC
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50実際のプロジェクトでの使い方
FeignClient を独立モジュールへ切り出し、インターフェース関連の POJO とデフォルト Feign 設定もこのモジュールに入れ、消費者側へ提供します。
feign-apimodule を作成し、Feign の依存を導入する。order-serviceに書いたUserClient、User、DefaultFeignConfigurationをfeign-apiプロジェクトへコピーする。order-serviceにfeign-apiの依存を導入する。order-service内で、この三つのコンポーネントに関係する import をfeign-apiのパッケージへ変更する。
変更後の問題:カスタム FeignClient が SpringBootApplication のスキャン範囲外にある場合、二つの解決方法があります。
- FeignClient があるパッケージを指定する。
@EnableFeignClients(basePackages = 'cn.itcast.feign.clients')- FeignClient のバイトコードを指定する。こちらの方が推奨されます。
@EnableFeignClients(clients = {UserClient.class})