一、Zuulの概要
1、ゲートウェイの役割
分散アーキテクチャでは、サービスノードの数が多くなります。クライアントにとって、複数のサービスノードが公開するAPIは統一されるべきです。そうでなければ、各ノードのアドレスが異なるため、クライアントはすべてのサービスノードのアドレスを管理し、その中から一つを選んでアクセスする必要があり、クライアントのメンテナンスコストが明らかに高くなります。
このような場合、複数のサービスノードをカプセル化し、統一されたAPIを公開するゲートウェイサービスが必要になります。クライアントはゲートウェイにアクセスし、ゲートウェイがクライアントのリクエストをカプセル化されたサービスノードに分散させることで、クライアントとサービス間の直接のやり取りを回避できます。
さらに、ゲートウェイは動的ルーティング機能を実現できるだけでなく、パラメータ検証、認証、ログ記録、キャッシュ、負荷分散、監視、レートリミットなどの機能も実現できます。要するに、ビジネス要件ではない統一機能はすべてゲートウェイに任せることができます。設計思想はSpringのアスペクト指向プログラミングに似ています。
2、Zuulとは何か
ZuulはNetflixがオープンソースで公開したマイクロサービスシステムのゲートウェイコンポーネントです。
Zuulの役割は多岐にわたります。主な機能は以下の通りです:
-
RibbonとEurekaと連携して、スマートな動的ルーティングと負荷分散機能を実現し、リクエストを戦略に基づいてクラスタ内の適切なサービスインスタンスに分散します。
-
サービスとクライアント間の結合を解除し、クライアントはゲートウェイが公開する統一APIにアクセスします。クライアントはサービスの稼働状況を気にする必要がありません。
-
統一された認証、監視、ログ記録、キャッシュ、レートリミット、パラメータ検証などの機能。
-
トラフィック監視に基づき、トラフィックに応じてサービスのデグレードを実行できます。
3、Zuulの動作フロー
Zuulは本質的にリクエストを制御するServletであり、一連のフィルターを作成するのが核心です。Zuulには以下の4種類のフィルターが含まれています:
-
PREフィルター:リクエストが特定のサービスにルーティングされる前に実行され、セキュリティ検証、アイデンティティ検証、パラメータ検証などの事前作業に使用できます。
-
ROUTINGフィルター:リクエストを特定のサービスインスタンスにルーティングするために使用され、デフォルトではHttpClientを使用してネットワークリクエストを実行します。
-
POSTフィルター:リクエストがマイクロサービスにルーティングされた後に実行され、通常は統計情報や指標を収集し、クライアントに応答を返すために使用されます。
-
ERRORフィルター:他のフィルターで例外が発生した場合に実行されます。
各タイプのフィルターの実行順序は、PREフィルター→ROUTINGフィルター→POSTフィルターの順であり、例外が発生した場合はERRORフィルターが実行されます。
二、Zuulの実践
Zuulもサービスであるため、Zuul-Serverサービスを構築する必要があります。
Zuul関連の依存関係を追加します。
1 <dependencies>
2 <dependency>
3 <groupId>org.springframework.cloud</groupId>
4 <artifactId>spring-cloud-starter-eureka</artifactId>
5 <version>1.4.7.RELEASE</version>
6 </dependency>
7 <dependency>
8 <groupId>org.springframework.cloud</groupId>
9 <artifactId>spring-cloud-starter-zuul</artifactId>
10 <version>1.4.7.RELEASE</version>
11 </dependency>
12 <dependency>
13 <groupId>org.springframework.boot</groupId>
14 <artifactId>spring-boot-starter-web</artifactId>
15 <version>2.5.0</version>
16 </dependency>
17 <dependency>
18 <groupId>org.springframework.boot</groupId>
19 <artifactId>spring-boot-starter-test</artifactId>
20 </dependency>
21 </dependencies>
Zuul関連の設定をapplication.ymlに追加します。
1 server:
2 port: 5000
3 spring:
4 application:
5 name: zuul-server
6 eureka:
7 client:
8 serviceUrl:
9 defaultZone: http://localhost:8761/eureka/
10 zuul:
11 routes:
12 product:
13 path: /product/**
14 serviceId: product-service
15 order:
16 path: /order/**
17 serviceId: order-service
ここでserverはZuulサービスの設定を示し、eurekaはZuulもEurekaクライアントであるためEurekaを設定する必要があることを示しています。最後にゲートウェイ設定zuulがあり、routesはルーティング設定です。serviceIdを設定してルーティングするサービス名を指定し、pathはルーティングするサービスアドレスを指定します。設定後、zuulは指定されたserviceIdのリクエストを指定されたサービスに分散できます。
起動クラスを追加し、関連するアノテーションを追加します。
1 @SpringBootApplication
2 @EnableEurekaClient
3 @EnableZuulProxy
4 public class GatewayApplication {
5
6 public static void main(String[] args){
7 SpringApplication.run(GatewayApplication.class);
8 System.out.println("Gateway service starting...");
9 }
10 }
@EnableEurekaClientアノテーションは、ZuulServerがEurekaクライアントであることを示し、@EnableZuulProxyアノテーションは現在がZuulサーバーであり、Zuul機能を有効にすることを示します。
GatewayApplicationクラスを起動すると、リクエストルーティング機能が完了します。http://localhost:5000/product/xxxxにアクセスすると、リクエストがproductサービスのAPIに転送されます。
異なるロジックのフィルターを追加します。例えばパラメータ検証を行う場合、カスタムのValidationFilterフィルターを作成し、親クラスZuulFilterを継承し、ZuulFilterの抽象メソッドを実装します。
@Component
public class ValidationFilter extends ZuulFilter{
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
//リクエストコンテキストを取得
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
//パラメータマップを取得して検証
Map<String, String[]> paramMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
if (entry.getValue() == null || entry.getValue().length == 0) {
System.out.println("パラメータ検証失敗: パラメータが空です: " + entry.getKey());
context.setSendZuulResponse(false);
context.setResponseStatusCode(400);
return null;
}
}
return null;
}
}
filterTypeメソッドを実装して現在がPREフィルターであることを示し、filterOrderメソッドはフィルターの順序を返し、値が小さいほど優先度が高くなります。runメソッドはフィルターの具体的な処理ロジックです。
三、Zuulの実装原理
3.1、@EnableZuulProxyアノテーション
Zuulの使用は比較的簡単で、起動クラスに@EnableZuulProxyアノテーションを追加するだけで済みます。したがって、Zuulのすべての機能はこのアノテーションを中心に展開されます。@EnableZuulProxyの役割は、ZuulProxyMarkerConfigurationのインスタンスをロードすることです。@EnableZuulProxyの定義は以下の通りです:
1 @EnableCircuitBreaker
2 @Target(ElementType.TYPE)
3 @Retention(RetentionPolicy.RUNTIME)
4 @Import(ZuulProxyMarkerConfiguration.class)
5 public @interface EnableZuulProxy {
6 }
そして、ZuulProxyMarkerConfigurationの役割はZuulProxyのマーカーインスタンスを注入することです。定義は以下の通りです:
1 @Configuration
2 public class ZuulProxyMarkerConfiguration {
3 @Bean
4 public Marker zuulProxyMarkerBean() {
5 return new Marker();
6 }
7
8 class Marker {
9 }
10 }
内部クラスMarkerの唯一の役割はマーカーとして使用されることです。したがって、どこでこのマーカーが使用されているかを見る必要があります。参照検索により、参照されている場所はZuulProxyAutoConfigurationであり、親クラスはZuulServerAutoConfigurationです。定義は以下の通りです:
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
public class ZuulServerAutoConfiguration {
親クラスZuulServerAutoConfigurationでは、SpringコンテナにServletRegistrationBeanインスタンスを注入します。このインスタンスではZuulServletが作成されます。Zuulの核心機能はこのZuulServletにあります。Zuulが受信するすべてのリクエストは、最終的にZuulServletのserviceメソッドによって処理されます。
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(new ZuulServlet(),
this.zuulProperties.getServletPattern());
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
3.2、ZuulServlet
ZuulServletはHttpServletを継承し、initメソッドでZuulRunnerオブジェクトを作成します。核心機能はserviceメソッドにあり、すべてのリクエストはserviceメソッドに到達します。ソースコードは以下の通りです:
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
// 1. ZuulRunnerオブジェクトの初期化
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// 2. HttpRequestリクエストコンテキストの取得
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
// 3. preフィルターの実行
preRoute();
} catch (ZuulException e) {
// 例外時はerrorフィルターとpostフィルターを実行
error(e);
postRoute();
return;
}
try {
// 4. routeフィルターの実行
route();
} catch (ZuulException e) {
// 例外時はerrorフィルターとpostフィルターを実行
error(e);
postRoute();
return;
}
try {
// 5. postフィルターの実行
postRoute();
} catch (ZuulException e) {
// 例外時はerrorフィルターを実行
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
// ローカルキャッシュRequestContextのクリーンアップ
RequestContext.getCurrentContext().unset();
}
}
全体のフローは比較的明確で、PREフィルター→ROUTEフィルター→POSTフィルターの順で実行されます。例外が発生した場合はERRORフィルターが実行されます。また、POSTフィルターの実行前に例外が発生した場合でもPOSTフィルターが実行されるため、POSTフィルターが必ず実行されることが保証されます。
ERRORフィルターは他のフィルターで例外が発生した場合にのみ実行されます。また、フィルターのロジックを実行する前にRequestContextを初期化し、フィルターの実行後にRequestContextをクリーンアップします。
ZuulServletのpreRoute、route、postRoute、errorメソッドは具体的な実装がなく、すべてZuulRunnerに対応するメソッドの実行を委譲します。ZuulRunnerも具体的な実行者ではなく、FilterProcessorに実行を委譲します。
3.3、FilterProcessor
FilterProcessorはフィルターの具体的な実行者であり、シングルトンオブジェクトです。ZuulServletの各種フィルターは最終的にFilterProcessorのpreRoute()、route()、postRoute()、error()メソッドを実行します。そして、これらの4つのメソッドはすべてFilterProcessorの内部メソッドrunFilters(String filterType)を実行します。
runFilterのソースコードは以下の通りです:
1 public Object runFilters(String sType) throws Throwable {
2 if (RequestContext.getCurrentContext().debugRouting()) {
3 Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
4 }
5 boolean bResult = false;
6 // 1. 指定タイプのフィルターリストを取得
7 List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
8 if (list != null) {
9 for (int i = 0; i < list.size(); i++) {
10 ZuulFilter zuulFilter = list.get(i);
11 // 2. フィルターを順次実行
12 Object result = processZuulFilter(zuulFilter);
13 if (result != null && result instanceof Boolean) {
14 bResult |= ((Boolean) result);
15 }
16 }
17 }
18 return bResult;
19 }
まずFilterLoaderオブジェクトのgetFilterByTypeメソッドを呼び出して、フィルタータイプに基づいてフィルターリストをクエリし、次にprocessZuulFilterメソッドを呼び出してフィルターのロジックを実行します。
3.3.1、フィルターの取得
すべてのZuulフィルターはZuulFilterインターフェースを実装する必要があり、@Componentアノテーションで修飾されてSpringコンテナに注入される必要があります。ZuulServerAutoConfigurationクラスには静的内部クラスZuulFilterConfigurationがあり、このクラスの役割はSpringコンテナ内のすべてのZuulFilterインスタンスを取り出すことです。コードは以下の通りです:
/** ZuulServerAutoConfiguration内部クラス */
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map<String, ZuulFilter> filters;
@Bean
public ZuulFilterInitializer zuulFilterInitializer(
CounterFactory counterFactory, TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
}
}
ZuulFilterConfigurationはまずすべてのZuulFilterオブジェクトを注入し、Mapで保存します。ZuulFilterInitiailizerの役割はフィルター登録器を初期化することです。コードは以下の通りです:
@PostConstruct
public void initializeFilters() {
log.info("フィルター初期化を開始します");
TracerFactory.initialize(tracerFactory);
CounterFactory.initialize(counterFactory);
for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
filterRegistry.put(entry.getKey(), entry.getValue());
}
}
FilterRegistryはフィルター登録器であり、内部ではMap filtersを使用してすべてのZuulFilterフィルターインスタンスを保存しています。
ZuulRunnerクラスのrunFiltersメソッドに戻ると、FilterLoaderのgetFiltersByTypeメソッドを通じて指定タイプのフィルターを取得します。したがって、FilterLoaderの役割はFilterRegistry内のフィルターを異なるタイプに分類し、Map hashFiltersByTypeを使用して異なるタイプのフィルターリストをキャッシュすることです。
フィルターの取得ロジックは比較的単純で、SpringコンテナからすべてのZuulFilterインスタンスを取得し、異なるタイプに基づいてグループ化してキャッシュするだけです。
3.3.2、フィルターの実行
1 /** フィルターの実行 */
2 public Object executeFilter(ZuulFilter filter) throws ZuulException {
3 // 1. リクエストコンテキストの取得
4 RequestContext ctx = RequestContext.getCurrentContext();
5 boolean debugMode = ctx.debugRouting();
6 final String metricPrefix = "zuul.filter-";
7 long executionTime = 0;
8 String filterName = "";
9 try {
10 long startTime = System.currentTimeMillis();
11 filterName = filter.getClass().getSimpleName();
12
13 RequestContext contextCopy = null;
14 Object result = null;
15 Throwable exception = null;
16
17 if (debugMode) {
18 Debug.addRoutingDebug("フィルター " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
19 contextCopy = ctx.copy();
20 }
21 // 2. フィルターのrunFilterメソッドを実行
22 ZuulFilterResult filterResult = filter.runFilter();
23 ExecutionStatus status = filterResult.getStatus();
24 executionTime = System.currentTimeMillis() - startTime;
25
26 switch (status) {
27 // 3. 実行結果をRequestContextコンテキストに保存
28 case FAILED:
29 exception = filterResult.getException();
30 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), executionTime);
31 break;
32 case SUCCESS:
33 result = filterResult.getResult();
34 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), executionTime);
35 if (debugMode) {
36 Debug.addRoutingDebug("フィルター {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} 実行時間 = " + executionTime + "ms");
37 Debug.compareContextState(filterName, contextCopy);
38 }
39 break;
40 default:
41 break;
42 }
43
44 if (exception != null) throw exception;
45
46 usageNotifier.notify(filter, status);
47 // 4. 実行結果を返す
48 return result;
49
50 } catch (Throwable e) {
51 if (debugMode) {
52 Debug.addRoutingDebug("フィルターの実行に失敗しました " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
53 }
54 usageNotifier.notify(filter, ExecutionStatus.FAILED);
55 if (e instanceof ZuulException) {
56 throw (ZuulException) e;
57 } else {
58 ZuulException ex = new ZuulException(e, "フィルターが例外をスローしました", 500, filter.filterType() + ":" + filterName);
59 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), executionTime);
60 throw ex;
61 }
62 }
63 }
全体のロジックは、まずグローバルリクエストコンテキストRequestContextを取得し、次にZuulFilterのrunFilterメソッドを呼び出してフィルターのロジックを実行し、最後にzuulFilterの実行結果をRequestContextコンテキストに保存して実行結果を返すだけです。
3.4、Zuulの組み込みフィルター
ZuulフィルターはカスタムのZuulFilter実装クラスを追加することで実装できます。同時に、Zuulは多くの組み込みフィルターを提供しています。主なものはRibbonRoutingFilterであり、このフィルターは負荷ルーティング転送機能を提供し、Ribbonの負荷分散機能を統合しています。
まとめ:
Zuulは本質的にServletであり、Springコンテナによって一連のZuulFilterインスタンスを管理しています。各ZuulFilterにはタイプがあり、preRoute、route、postRoute、errorの4種類があります。異なるタイプは実行順序とタイミングが異なります。リクエストがZuulに入ると、ZuulServletが受信し、serviceメソッドで処理します。serviceメソッドのロジックは、Springコンテナから各種のZuulFilterフィルターインスタンスを見つけ、順序とタイミングに基づいてフィルターの処理ロジックを順次実行することです。使用時には、カスタムのZuulFilterを通じてリクエストの異なる段階で機能を拡張できます。