「ビジネス リクエストがすべてのサーブレット コンテナのワーカー スレッドを占有するため、ヘルス チェック インターフェイスの処理が間に合わなくなります。」
0.ディレクトリ
1. 背景
著者が最近引き継いだ「マイクロサービス アーキテクチャ」システムでは、サードパーティ サービスのエージェントの設計思想がマイクロサービス登録モードになっており、各サードパーティ サービスは Nacos/Consul などのサービス ディスカバリ コンポーネントに対応して登録され、これらのサードパーティ サービスへのアクセスはゲートウェイを介して一律に移行されます。
現時点で推測すると、この設計の目的はおそらく、既存のマイクロサービスの処理プロセスを再利用し、新しいプロセスを追加して保守コストが増加するのを避けることです。
しかし、筆者は日々のメンテナンスを行っている中で、この処理フローには次のような問題があることに気付きました。
一部のサードパーティのプロキシ サービス、または弊社独自のマイクロサービス コンポーネントの一部のサービスは、通常の処理に時間がかかります。短期間にそのようなリクエストが多すぎて、サーブレット コンテナの事前設定されたワーカー スレッド数 (たとえば、undertow のデフォルトは 64、tomcat のデフォルトは 200) を直接超えると、アプリケーション サービス (外部に提供されるヘルス チェック インターフェイスを含む) の応答が遅くなり、Consul はアプリケーション サービスがもう生きていないと判断し、正常なサービスのリストから除外されます。
この記事では、Sentinel を利用してそのような問題を軽減することを試み、最小限の変更で 2 つのスムーズなソリューションを提供します。
2. アイデア
-
アイデア 1: ヘルス チェック インターフェイスの応答を担当する別のポートを作成します。
a. このアイデアに関しては、Sentinel が実際に実装の基礎を提供しました。Sentinel は、起動後にデフォルトのリスニング ポート 8719 を作成しServerSocket
、対応するCommandHandler<R>
実装クラスにリクエストをディスパッチします。ソースコードのエントリを参照してくださいSimpleHttpCommandCenter.start()
。
b. 実装ではSimpleHttpCommandCenter.start()
、ServerSocket
独自に構築した特殊なスレッドプール(フィールドexecutor
、bizExecutor
に相当)を採用しているため、サーブレットコンテナのワーキングスレッドと競合することはなく、当然のことながら「業務リクエストがサーブレットコンテナのワーキングスレッドをすべて占有してしまうため、ヘルスチェックインタフェースの処理が間に合わない」という問題も発生しません。
c. このアイデアは、問題に直接直面する場合と比較して、問題を回避することを目的としています。 -
アイデア 2: すべてのビジネス リクエスト全体を「同時スレッド数」によって制限し、「ヘルス チェック インターフェイスを処理するためのスレッドを予約する」効果を生み出します。
a. 上記のアイデア 1 と比較すると、このアイデアは問題に直接直面しており、Sentinel の電流制限機能を直接使用して、タイムリーな応答を確保するためにヘルス チェックなどのインターフェイス用の作業スレッドを予約します。
b. ビジネス リクエストを処理するスレッドの数をサーブレット コンテナ内のワーカー スレッドの数未満に制限し (たとえば、ワーカー スレッドの最大数から 1 を引いたものです。デフォルト設定では、undertow の場合は 63、tomcat の場合は 199 です)、特別なインターフェイスに対するリクエストにタイムリーに応答できる準備完了状態のスレッドが常に存在するようにします。
3. 実現する
上記の 2 つのアイデアに対応する実装コードを以下に示します。
3.1 アイデア 1: 実装CommandHandler<R>
上記の最初のアイデアと同様に、Sentinel はデフォルトで追加の 8719 ポートをリッスンし、特定のコマンドに応答します。
この機能を再利用するには、CommandHandler<R>
Sentinel が提供する SPI 拡張メソッドに従って独自に実装し、処理フローに登録する必要があります。
3.1.1 実装の詳細
- 独自の を実装してください
CommandHandler<R>
。
@CommandMapping(name = "health", desc = "health check")
public class HealthCheckCommandHandler implements CommandHandler<Object> {
@Override
public CommandResponse<Object> handle(CommandRequest request) {
final Map<String, String> statusInfo = Collections.singletonMap("status", "UP");
return CommandResponse.ofSuccess(JSONUtil.toJsonPrettyStr(statusInfo));
}
}
-
SPI登録。
新しいファイルを作成し、上記のクラスの完全な名前META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler
を入力します。HealthCheckCommandHandler
-
アプリケーションを起動します。ご来店をお待ちしております
localhost:8719/health
。
3.1.2 注意事項
- Sentinel ダッシュボードに依存しないアプリケーションが開始されると、デフォルトでポート 8719 も占有されます。実装では、
SimpleHttpCommandCenter.getServerSocketFromBasePort(int basePort)
Sentinel は初期値としてポート 8719 から開始し、ループを増やし、外部サービスを提供するポートとして最初の未使用のアイドル ポートを見つけます。たとえば、上記のポート 8719 が占有されている場合、ポート 8720 が徐々に使用されます。 spring.cloud.sentinel.transport.port
指定したポートを強制的に使用するように構成することもできます。localhost:8719/api
Sentinel によって提供される実装は、 にアクセスして入手できますCommandHandler<R>
。- Sentinel は、上記の機能を Sentinel-core に直接統合するのではなく、別のコンポーネントとして統合します。関連する GAV は次のとおりです。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.0</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
- デフォルトでは、Consul クライアントは、アプリケーションの IP + ポートを使用して、自身を Consul サービス センターに登録します。ただし、このセクションで説明する方法はこのデフォルトのルールに違反しているため、対応する調整を行う必要があります。(ソースコードはすぐ下にあります)
/**
* <p>
* 默认情况下, Consul客户端将自身注册Consul中心的时候, 使用的是应用自身的ip和port
* <p>
* 为了避免出现服务响应缓慢时被Consul认定掉线, 我们另起新端口来专门响应健康检查; 所以我们需要对应修改check地址
* <p>
* <p>
* 实现思路: 抢在 {@code ConsulAutoServiceRegistrationListener} 回调前, 修改Consul的Check地址,
* 源码参见: {@code ConsulAutoRegistration.setCheck(...)}
* <p>
* <p> 本类生效后, 配置项 {@code spring.cloud.consul.discovery.health-check-path} 和 {@code spring.cloud.consul.discovery.health-check-url} 失效
*
* @author fulizhe
*
*/
public class ConsulDynamicModifyHealthCheckUrlListener implements SmartApplicationListener {
@Value("${XXX_PORT:9802}")
private String consulHealthCheckPort;
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return WebServerInitializedEvent.class.isAssignableFrom(eventType);
}
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
if (applicationEvent instanceof WebServerInitializedEvent) {
final WebServerInitializedEvent event = (WebServerInitializedEvent) applicationEvent;
final ConsulDiscoveryProperties consulDiscoveryProperties = event.getApplicationContext()
.getBean(ConsulDiscoveryProperties.class);
// consulDiscoveryProperties.getHealthCheckPath()
// COPY FROM: ConsulAutoRegistration.createCheck(...)
final String healthCheckUrl = String.format("%s://%s:%s%s", consulDiscoveryProperties.getScheme(),
consulDiscoveryProperties.getHostname(), consulHealthCheckPort, "/health");
consulDiscoveryProperties.setHealthCheckUrl(healthCheckUrl);
}
}
@Override
public int getOrder() {
// 优先级高于ConsulAutoServiceRegistrationListener
return -1;
}
}
3.2 アイデア 2: ビジネス リクエストの「同時スレッド数」を制限する
3.2.1 理論的根拠
デフォルトでは、Sentinel はSentinelWebInterceptor
リクエスト処理応答に介入するように spring-mvc を適応させます。
SentinelWebInterceptor
SpringMVCの古典的なインターセプトインターフェースが実装されており、HandlerInterceptor
そのインターフェースを実装することでpreHandle
電流制限ロジックの判定を行っています。
Sentinel はHandlerInterceptor
デフォルトで 2 つの実装クラスを提供します。
名前 | 特徴 | 例 |
---|---|---|
SentinelWebInterceptor |
要求された URL アドレスを統計単位として使用し、それに基づいて電流制限を判断します | /hello と /hello2 は 2 つの異なるリソースとして扱われ、電流制限の構成は個別に実行されます。/hello は現在の制限をトリガーしますが、/hello2 へのアクセスには影響しません。 |
SentinelWebTotalInterceptor |
システム上のすべてのビジネスリクエストのURLアドレスを同じものとして扱い、それに基づいてフロー制限の判断を行います。 | /hello と /hello2 はフロー制限判定の同じリソースとみなされます。たとえば、QPS が 20 に設定されている場合、1 秒間に /hello に 20 回アクセスし、その後 /hello2 にアクセスした後でも、現在の制限がトリガーされます。 |
3.2.2 実装の詳細
ニーズに応じて、Sentinel がSentinelWebTotalInterceptor
デフォルトで提供するものを直接再利用できます。
-
デフォルトを無効にします
SentinelWebInterceptor
。Sentinel はクラス
の Spring コンテナに登録されます。そしてそれを無効にする実装を提供します。SentinelWebAutoConfiguration
SentinelWebInterceptor
spring.cloud.sentinel.filter.enabled
-
有効になります
SentinelWebTotalInterceptor
。/** * <p> Refer To {@code SentinelWebAutoConfiguration} * <p> 配置 spring.cloud.sentinel.filter.enabled 为 FALSE * @author fulizhe * */ @Configuration(proxyBeanMethods = false) public class SentinelWebInterceptorConfiguration implements WebMvcConfigurer { @Autowired private SentinelProperties properties; @Autowired private Optional<UrlCleaner> urlCleanerOptional; @Autowired private Optional<BlockExceptionHandler> blockExceptionHandlerOptional; @Autowired private Optional<RequestOriginParser> requestOriginParserOptional; @Autowired private Optional<SentinelWebMvcConfig> sentinelWebMvcConfig; @Override public void addInterceptors(InterceptorRegistry registry) { // !!!注意: 注册的这个Interceptor, 不参与 /actuator/xx 访问的拦截(也就是不会对我们的/actuator/health 健康检查接口作限流) SentinelWebMvcTotalConfig sentinelWebMvcTotalConfig = new SentinelWebMvcTotalConfig(); BeanUtil.copyProperties(sentinelWebMvcConfig.get(), sentinelWebMvcTotalConfig); // 使用SentinelWebTotalInterceptor 代替默认的SentinelWebInterceptor AbstractSentinelInterceptor sentinelWebTotalInterceptor = new SentinelWebTotalInterceptor( sentinelWebMvcTotalConfig); SentinelProperties.Filter filterConfig = properties.getFilter(); registry.addInterceptor(sentinelWebTotalInterceptor)// .order(filterConfig.getOrder()) .addPathPatterns(filterConfig.getUrlPatterns()); log.info("[Sentinel Starter] register SentinelWebInterceptorEx with urlPatterns: {}.", filterConfig.getUrlPatterns()); } /** * COPY FROM {@code SentinelWebAutoConfiguration} * @return */ @Bean public SentinelWebMvcConfig sentinelWebMvcConfigX() { SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig(); sentinelWebMvcConfig.setHttpMethodSpecify(properties.getHttpMethodSpecify()); sentinelWebMvcConfig.setWebContextUnify(properties.getWebContextUnify()); if (blockExceptionHandlerOptional.isPresent()) { blockExceptionHandlerOptional.ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler); } else { if (StringUtils.hasText(properties.getBlockPage())) { sentinelWebMvcConfig.setBlockExceptionHandler( ((request, response, e) -> response.sendRedirect(properties.getBlockPage()))); } else { sentinelWebMvcConfig.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); } } urlCleanerOptional.ifPresent(sentinelWebMvcConfig::setUrlCleaner); requestOriginParserOptional.ifPresent(sentinelWebMvcConfig::setOriginParser); return sentinelWebMvcConfig; } }
-
現在制限されている同時スレッドの数を設定します。
現在の制限設定は単純であるため、ここでは Sentinel-dashboard を導入するつもりはなく、手動登録を直接使用します。a. ローカル電流制限設定ファイルを読み取り、Sentinel に登録します。
// 实现Sentinel提供的扩展接口InitFunc ; // 参见官方文档: https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-dynamic-file-rule public class RegisterFlowRuleInit implements InitFunc { @Override public void init() throws Exception { registerFlowRule(); } static void registerFlowRule() throws Exception { Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() { }); // 读取本地限流配置文件,注册到Sentinel中。 ClassLoader classLoader = SpringSentinelApplication.class.getClassLoader(); String flowRulePath = URLDecoder.decode(classLoader.getResource("FlowRule.json").getFile(), "UTF-8"); // Data source for FlowRule FileRefreshableDataSource<List<FlowRule>> flowRuleDataSource = new FileRefreshableDataSource<>(flowRulePath, flowRuleListParser); FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); } }
b.上記に対して
RegisterFlowRuleInit
SPI 登録を実行します。
c. 「FlowRule.json」ファイルのサンプルコンテンツ: (作成者はサーブレットコンテナとして tomcat を使用しているため、ここでのスレッド制限は 199 に設定されています。つまり、1 つのスレッドがヘルスチェック用に予約されています)[ { "resource": "spring-mvc-total-url-request", "limitApp": "default", "grade": 0, "count": 199, "strategy": 0, "refResource": null, "controlBehavior": 0, "warmUpPeriodSec": 10, "maxQueueingTimeMs": 500, "clusterMode": false, "clusterConfig": { "flowId": null, "thresholdType": 0, "fallbackToLocalWhenFail": true, "strategy": 0, "sampleCount": 10, "windowIntervalMs": 1000 } } ]
3.2.3 注意
- このアイデアは、次の 2 つの基礎に基づいています:
a. Sentinel での実装は、SentinelWebTotalInterceptor
すべての **ビジネス リクエスト (プロキシ転送を含む)** のフローを全体として制限することです。
b. SpringBootが提供するクラスインターフェースは、/actuator/xx
上記項目の「業務リクエスト」に属さないため、上記の電流制限設定の影響を受けません。したがって、アイドル状態のサーブレット コンテナのワーカー スレッドが上記の電流制限構成で予約されている限り、/actuator/health
そのようなヘルス チェック インターフェイスはタイムリーに応答できます。(簡単に言うと、具体的な原則は、/actuator/xx
クラス インターフェイスがWebMvcEndpointHandlerMapping
型を処理し、ビジネス リクエスト インターフェイス [@RequestMapping
定義] がRequestMappingHandlerMapping
型を処理するということです。アイデア 2 で追加した電流制限インターセプターは、後者にのみ影響します。)
4.補足
-
上記の 2 つのメソッドは、実際には Sentinel によって提供される拡張機能を使用しており
InitFunc
、デフォルトでは、Sentinel はコールバックをアクティブにするためにサーバーにアクセスする必要がありますInitFunc
。つまりInitFunc
、Sentinel は遅延読み込み戦略を採用します。spring.cloud.sentinel.eager=true
構成を使用してこのロジックを変更する必要があります。 -
上記 2 つの方法の長所と短所
アイデア部門 アドバンテージ 欠点がある 達成 CommandHandler<R>
単純で失礼な ヘルスチェックアドレスの変更により、サービス登録の実装ロジックを変更する必要があり、Consul に登録されているサービスに対して対応する調整を行う必要があります。 ビジネスリクエストの「同時スレッド」を制限する 健康診断のアドレスは変更されず、領事側は認識なし 1. わかりにくい
2. このメソッドは正式に電流制限を有効にするものと考えられているため、電流制限条件が満たされた後、Sentinel はデフォルトのリクエスト ブロック (ブラウザ側が常に待機) ではなく、電流制限ステータス コード 429 を直接返します。これが期待どおりであることを確認してください