Spring@Scheduled スケジュールされたタスクが XXL-JOB にアクセスするためのソリューション (SC ゲートウェイに基づく)

背景

私が現在働いている会社では、25 以上の Spring Cloud 分散マイクロサービス プロジェクトを管理しています。そのうち約 10 社には、Spring @Scheduled を使用して、スケジュールされたタスク ロジックで書かれたマイクロサービスがあります。

Spring @Scheduled スケジュールされたタスクの欠点:

  1. クラスタリングはサポートされていません: 繰り返しの実行を避けるために、分散ロックを導入する必要があります
  2. 厳格で柔軟性に欠ける: 手動実行、単一実行、補正実行、タスク パラメーターの変更、タスクの一時停止、タスクの削除、スケジュール時間の変更、失敗時の再試行はサポートされていません。
  3. アラーム メカニズムなし: タスクの失敗後にアラーム メカニズムはありません。論理実行例外記録 ERROR ログは Prometheus アラームに接続されます。この方法は、タスク レベルのアラーム メカニズムではなく、ログ レベルのアラームとみなされます。
  4. シャーディング タスクはサポートされていません。順序付けされたデータを処理する場合、複数マシンのシャーディング実行タスクは異なるデータを処理します。
  5. ……

これに基づいて、軽量の分散タイミング スケジューリング フレームワーク 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 ログ クエリ プラットフォームでも検索できます。

参考

おすすめ

転載: blog.csdn.net/lonelymanontheway/article/details/132307385