背景
私が現在働いている会社では、25 以上の Spring Cloud 分散マイクロサービス プロジェクトを管理しています。そのうち約 10 社には、Spring @Scheduled を使用して、スケジュールされたタスク ロジックで書かれたマイクロサービスがあります。
Spring @Scheduled スケジュールされたタスクの欠点:
- クラスタリングはサポートされていません: 繰り返しの実行を避けるために、分散ロックを導入する必要があります
- 厳格で柔軟性に欠ける: 手動実行、単一実行、補正実行、タスク パラメーターの変更、タスクの一時停止、タスクの削除、スケジュール時間の変更、失敗時の再試行はサポートされていません。
- アラーム メカニズムなし: タスクの失敗後にアラーム メカニズムはありません。論理実行例外記録 ERROR ログは Prometheus アラームに接続されます。この方法は、タスク レベルのアラーム メカニズムではなく、ログ レベルのアラームとみなされます。
- シャーディング タスクはサポートされていません。順序付けされたデータを処理する場合、複数マシンのシャーディング実行タスクは異なるデータを処理します。
- ……
これに基づいて、軽量の分散タイミング スケジューリング フレームワーク XXL-JOB の導入、つまりスケジュールされたタスクを XXL-JOB プラットフォームに移行することを検討します。
XXL-JOBについては前回のブログを参照してください。
デザイン
10 個以上の SC 分散アプリケーションと 30 個以上のスケジュールされたタスクがあることを考慮します。各アプリケーションを移行および変換する必要がある場合は、各アプリケーションで XXL-JOB 関連情報を構成する必要があります。もちろん、これは Apollo 名前空間の共有継承メカニズムを通じて実現できます。余談: 時間があれば、後で Apollo 名前空間設定の継承に関するブログを書きます。
つまり、Apollo の XXL-JOB の設定情報をアプリケーション内で保持することができ(アプリケーションは Apollo 名前空間に相当します)、このアプリケーション(Apollo)を再利用することで他のアプリケーションが設定の再利用を実現できます。
ただし、各アプリケーションは新しい構成クラスを追加する必要があります。構成クラスを再利用するにはどうすればよいですか? これも解決できます。解決策は、構成クラスをコモンズコンポーネントライブラリに維持し(Spring @Configurationアノテーションを導入する必要がある、つまり依存関係spring-context
パッケージを導入する)、各アプリケーションのSpring Boot起動クラスがこの構成クラスをスキャンする必要があることです。 。
また、30 個以上のスケジュールされたタスクに対応する 30 個以上の @@Component スケジュールされたタスク クラスを変更する必要があり、すべてのスケジュールされたタスク アプリケーションに Maven 依存関係を導入する必要があります。
スケジュールされたタスク クラスを XXL-JOB に手動で追加する必要があります。
これは良い解決策のように思えますが、異なるアプリケーションに同じ名前の構成が存在する可能性が排除されないため、同じ名前の構成が存在する場合は、構成の名前を変更する必要があります。Spring Boot スタートアップ クラスを変更すると、未知の問題が発生する可能性があります。
最後に、すべてのアプリケーションは、内部アプリケーションであろうと外部アプリケーションであろうと、ゲートウェイ サービスを通じて転送する必要があることを考慮してください。外部アプリケーションには、C 側、B 側、およびサードパーティの顧客が含まれます。したがって、次のような最終計画があります。
実行計画
内部ゲートウェイ アプリケーションに Maven 依存関係を導入します。
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.0</version>
</dependency>
次の XXL-JOB 構成クラスを追加します。
@Slf4j
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.port:9999}")
private int port;
@Value("${xxl.job.accessToken:default_token}")
private String accessToken;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(adminAddresses);
executor.setAppname(appName);
executor.setPort(port);
executor.setAccessToken(accessToken);
return executor;
}
}
これに応じて、Apollo に次の設定を追加する必要があります。一部の設定は固定されており、ローカル設定ファイルに配置できますが、将来変更される可能性のある設定は Apollo に配置できます。
ここでの appname は実際には XXL-JOB の実行者であり、
ゲートウェイ サービスは k8s クラスター内でポッドの形式で実行され、自動登録が使用されることは言うまでもありません。
新しいスケジュールされたタスク分析とリクエスト転送構成クラスがゲートウェイ サービスに追加されます。
@Slf4j
@Component
public class XxlJobLogicConfig {
private static final String URL = "url:";
private static final String METHOD = "method:";
private static final String DATA = "data:";
private static final String GET = "GET";
private static final String POST = "POST";
@XxlJob("httpJobHandler")
public void httpJobHandler() {
// 参数解析及校验
String jobParam = XxlJobHelper.getJobParam();
if (StringUtils.isBlank(jobParam)) {
XxlJobHelper.log("param[" + jobParam + "] invalid");
XxlJobHelper.handleFail();
return;
}
String[] httpParams = jobParam.split("\n");
String url = "";
String method = "";
String data = "null";
for (String httpParam : httpParams) {
if (httpParam.startsWith(URL)) {
url = httpParam.substring(httpParam.indexOf(URL) + URL.length()).trim();
}
if (httpParam.startsWith(METHOD)) {
method = httpParam.substring(httpParam.indexOf(METHOD) + METHOD.length()).trim().toUpperCase();
}
if (httpParam.startsWith(DATA)) {
data = httpParam.substring(httpParam.indexOf(DATA) + DATA.length()).trim();
}
}
if (StringUtils.isBlank(url)) {
XxlJobHelper.log("url[" + url + "] invalid");
XxlJobHelper.handleFail();
return;
}
if (!GET.equals(method) && !POST.equals(method)) {
XxlJobHelper.log("method[" + method + "] invalid");
XxlJobHelper.handleFail();
return;
}
log.info("xxlJob调度请求url={},请求method={},请求数据data={}", url, method, data);
// 判断是否为POST请求
boolean isPostMethod = POST.equals(method);
HttpURLConnection connection = null;
BufferedReader bufferedReader = null;
try {
URL realUrl = new URL(url);
connection = (HttpURLConnection) realUrl.openConnection();
// 设置具体的方法,也就是具体的定时任务
connection.setRequestMethod(method);
// POST请求需要output
connection.setDoOutput(isPostMethod);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setReadTimeout(900 * 1000);
connection.setConnectTimeout(600 * 1000);
// connection:Keep-Alive 表示在一次http请求中,服务器进行响应后,不再直接断开TCP连接,而是将TCP连接维持一段时间。
// 在这段时间内,如果同一客户端再次向服务端发起http请求,便可以复用此TCP连接,向服务端发起请求。
connection.setRequestProperty("connection", "keep_alive");
// Content-Type 表示客户端向服务端发送的数据的媒体类型(MIME类型)
connection.setRequestProperty("content-type", "application/json;charset=UTF-8");
// Accept-Charset 表示客户端希望服务端返回的数据的媒体类型(MIME类型)
connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");
// gateway请求转发到其他应用
connection.connect();
// 如果是POST请求,则判断定时任务是否含有执行参数
if (isPostMethod && StringUtils.isNotBlank(data)) {
DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
// 写参数
dataOutputStream.write(data.getBytes(Charset.defaultCharset()));
dataOutputStream.flush();
dataOutputStream.close();
}
int responseCode = connection.getResponseCode();
// 判断请求转发、定时任务触发是否成功
if (responseCode != 200) {
throw new RuntimeException("Http Request StatusCode(" + responseCode + ") Invalid");
}
bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charset.defaultCharset()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
String responseMsg = stringBuilder.toString();
log.info("xxlJob调度执行返回数据={}", responseMsg);
XxlJobHelper.log(responseMsg);
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail();
} finally {
try {
if (bufferedReader != null) {
bufferedReader.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (Exception e) {
XxlJobHelper.log(e);
}
}
}
}
少し面倒なのは、各 Spring Cloud アプリケーションで ScheduleController を手動で追加する必要があることです。
/**
* 定时任务入口,所有服务的@RequestMapping满足/schedule/appName这种格式,方便统一管理
**/
@RestController
@RequestMapping("/schedule/search")
public class ScheduleController {
@Resource
private ChineseEnglishStoreSchedule chineseEnglishStoreSchedule;
@GetMapping("/chineseEnglishStoreSchedule")
public Response<Boolean> chineseEnglishStoreSchedule() {
chineseEnglishStoreSchedule.execute();
return Response.success(true);
}
}
さらに、ルーティング ルールと転送ルールをゲートウェイ サービスに追加する必要があります。
スケジュールされたタスクがあり、XXL-JOB プラットフォームにアクセスする準備ができている各 SC マイクロサービスは、上のスクリーンショットにあるものと同様の 4 つの構成情報を追加する必要があります。
利点: スケジュールされたタスクを含むすべてのサービスが一目でわかるため、統合された保守と管理が容易になります。
このソリューションでは、特定の Schedule クラスを変更する必要はありません。
@JobHander(value = "autoJobHandler")
public class AutoJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String... params) {
try {
// 既有的业务逻辑
// 执行成功
return ReturnT.SUCCESS;
} catch (Exception e) {
logger.error("execute error id:{}, error info:{}", id, e);
return ReturnT.FAIL;
}
return ReturnT.SUCCESS;
}
}
最後に、省略できない手順があり、XXL-JOB 管理プラットフォームにタスクを追加します。
確認する
タスク スケジュール実行ログ:
ロジック コードで出力されたログは、ELK ログ クエリ プラットフォームでも検索できます。