CLOVER🍀

That was when it all began.

Quartzのクラスタリングを試してみる

これは、なにをしたくて書いたもの?

Quartzでクラスタリングを扱ってみたいと思います。

Quartzのクラスタリング

Quartzのクラスタリングについては、あまりドキュメントがありません。

まずはこちらに少し。

Overview

それから、チュートリアルに少し書かれています。

Lesson 11: Advanced (Enterprise) Features

Quartzでクラスターを構成するには、JobStoreをJDBCJobStore(JobStoreTXまたはJobStoreCMT)かTerracottaJobStoreのどちらかにする
必要があります。

機能としては、ロードバランシングとJobのフェイルオーバーが含まれるようです。

設定としては、以下がポイントのようです。

  • JDBCJobStore(JobStoreTXまたはJobStoreCMT)によるクラスタリング
    • org.quartz.jobStore.isClusteredプロパティをtrueにする
    • 一部の例外となるプロパティを除いて、同じ内容のQuartzのプロパティファイルを使用する
    • 例外は、スレッドプールサイズとorg.quartz.scheduler.instanceId
  • TerracottaJobStoreによるクラスタリング

今回は、JDBCJobStore(JobStoreTXまたはJobStoreCMT)によるクラスタリングを使用したいと思います。

注意点は、あるあるですが各Quartzノードの間で時刻同期が行われている必要があります。
ロードバランシングはランダムのようですが、実行しているJobが少ない場合はアクティブだった同じノードが優先されると書かれています。

で、クラスタリングについてのドキュメントはこれくらいだったりします。

サンプルは?と思うのですが、Exampleのページを見てもリンクがありません。

Example 13 - Clustered Quartz

  • Demonstrates how Quartz can be used in a clustered environment and how Quartz can use the database to persist scheduling information

Quartz Examples

困ったなと思いましたが、ディストリビューションにはサンプルが含まれているというので、GitHubにあるソースコードを確認してみました。

このあたりにありますね。

https://github.com/quartz-scheduler/quartz/tree/v2.3.2/distribution/examples/src/main/java/org/quartz/examples/example13

設定ファイルおよび起動スクリプトはこちら。

https://github.com/quartz-scheduler/quartz/tree/v2.3.2/distribution/src/main/assembly/root/examples/example13

設定ファイルの説明や使い方については、こちらに書かれています。

https://github.com/quartz-scheduler/quartz/blob/v2.3.2/distribution/src/main/assembly/root/examples/example13/clustering_readme.txt

サンプルプログラムの説明は、こちら。

https://github.com/quartz-scheduler/quartz/blob/v2.3.2/distribution/examples/src/main/java/org/quartz/examples/example13/ClusterExample.java#L34-L58

このあたりを参考に、Quartzでクラスターを構成してみたいと思います。

お題

データベースをMySQLとするJDBCJobStore(JobStoreTX)を使って、Quartzでクラスターを構成します。
定期的に起動する簡単なジョブを3つのインスタンスで確認することにしましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-131-generic", arch: "amd64", family: "unix"

MySQLは172.17.0.2で動作しているものとし、データベースはpractice、ユーザー/パスワードはkazuhira/passwordで作成済みとします。

$ mysql --version
mysql  Ver 8.0.31 for Linux on x86_64 (MySQL Community Server - GPL)


mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.31    |
+-----------+
1 row in set (0.06 sec)

準備

では、アプリケーションを作成する準備をしていきます。

Maven依存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.31</version>
        </dependency>
    </dependencies>

データベースにはMySQLを使用するので、Connector/Jを依存関係に加えてあります。

SLF4Jで使用するロギングライブラリはLogbackとし、設定ファイルはこんな感じで簡単に作成。

src/main/resources/logback.xml

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

MySQL側には、QuartzのJDBCJobStoreで必要なテーブルを作成しておきます。

$ curl -sL https://raw.githubusercontent.com/quartz-scheduler/quartz/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql | mysql -ukazuhira -p practice

これで、準備は完了です。

アプリケーションを作成する

それでは、ソースコードを作成します。

Example 13の構成を参考にしつつ、自分で作っていきます。

まずはJobクラス。

src/main/java/org/littlewings/quartz/PrintMessageJob.java

package org.littlewings.quartz;

import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintMessageJob implements Job {
    Logger logger = LoggerFactory.getLogger(PrintMessageJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String instanceId = getSchedulerInstanceId(context);
        JobDetail jobDetail = context.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();

        logger.info("[{}] execute job, message = [{}]", instanceId, jobDataMap.getString("message"));
    }

    private String getSchedulerInstanceId(JobExecutionContext context) {
        try {
            return context.getScheduler().getSchedulerInstanceId();
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }
}

SchedulerのインスタンスID、JobDataMapで設定したデータをログ出力する簡単なJobです。

mainクラス。

src/main/java/org/littlewings/quartz/App.java

package org.littlewings.quartz;

import java.io.Console;

import com.mysql.cj.jdbc.AbandonedConnectionCleanupThread;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
    public static void main(String... args) {
        Logger logger = LoggerFactory.getLogger(App.class);

        Scheduler scheduler = null;


        try {
            scheduler = StdSchedulerFactory.getDefaultScheduler();

            if (args.length > 0) {
                if ("register-job".equals(args[0])) {
                    JobDetail jobDetail =
                            JobBuilder
                                    .newJob(PrintMessageJob.class)
                                    .usingJobData("message", "Hello, Clustering Job!!")
                                    .withIdentity("job1", "job-group1")
                                    .build();

                    Trigger trigger =
                            TriggerBuilder
                                    .newTrigger()
                                    .withIdentity("trigger1", "trigger-group1")
                                    .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
                                    .build();

                    scheduler.scheduleJob(jobDetail, trigger);
                    logger.info("registered, job");
                } else if ("clear-job".equals(args[0])) {
                    scheduler.clear();
                    logger.info("cleared, job");
                }
            }

            scheduler.start();
            logger.info("Quartz scheduler started.");

            Console console = System.console();
            console.readLine();
        } catch (SchedulerException e) {
            e.printStackTrace();
        } finally {
            if (scheduler != null) {
                try {
                    scheduler.shutdown(true);
                } catch (SchedulerException e) {
                    e.printStackTrace();
                }
            }

            AbandonedConnectionCleanupThread.checkedShutdown();
        }
    }
}

基本はSchedulerを取得して開始するだけなのですが。

            scheduler = StdSchedulerFactory.getDefaultScheduler();


            scheduler.start();
            logger.info("Quartz scheduler started.");

起動引数にregister-jobを指定するとSchedulerにJobを登録し、clear-jobを指定するとSchedulerから登録したJobをクリアするようにして
います。

                if ("register-job".equals(args[0])) {
                    JobDetail jobDetail =
                            JobBuilder
                                    .newJob(PrintMessageJob.class)
                                    .usingJobData("message", "Hello, Clustering Job!!")
                                    .withIdentity("job1", "job-group1")
                                    .build();

                    Trigger trigger =
                            TriggerBuilder
                                    .newTrigger()
                                    .withIdentity("trigger1", "trigger-group1")
                                    .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
                                    .build();

                    scheduler.scheduleJob(jobDetail, trigger);
                    logger.info("registered, job");
                } else if ("clear-job".equals(args[0])) {
                    scheduler.clear();
                    logger.info("cleared, job");
                }

Jobを登録するのはクラスター内のインスタンスでひとつ、というか1回だけですね。

JobはCronScheduleBuilderで10秒おきに実行することにしました。

                    Trigger trigger =
                            TriggerBuilder
                                    .newTrigger()
                                    .withIdentity("trigger1", "trigger-group1")
                                    .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
                                    .build();

あとは、Enterでアプリケーションが終了するようにしました。

            Console console = System.console();
            console.readLine();

これで、ソースコードの作成は完了です。

が、動作確認に移る前にQuartzの設定ファイルの作成が必要です。

設定ファイルはインスタンスごとに作成します。今回は、3つのインスタンスを実行する想定で作成しましょう。

ひとつ目のインスタンス用。

src/main/resources/instance1.properties

org.quartz.scheduler.instanceName=ClusteredExampleScheduler
org.quartz.scheduler.instanceId=instance1

org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=5

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.dataSource=mysqlds

org.quartz.dataSource.mysqlds.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.mysqlds.URL=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin
org.quartz.dataSource.mysqlds.user=kazuhira
org.quartz.dataSource.mysqlds.password=password
org.quartz.dataSource.mysqlds.maxConnections=7

2つ目のインスタンス用。

src/main/resources/instance2.properties

org.quartz.scheduler.instanceName=ClusteredExampleScheduler
org.quartz.scheduler.instanceId=instance2

org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=5

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.dataSource=mysqlds

org.quartz.dataSource.mysqlds.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.mysqlds.URL=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin
org.quartz.dataSource.mysqlds.user=kazuhira
org.quartz.dataSource.mysqlds.password=password
org.quartz.dataSource.mysqlds.maxConnections=7

3つ目のインスタンス用。

src/main/resources/instance3.properties

org.quartz.scheduler.instanceName=ClusteredExampleScheduler
org.quartz.scheduler.instanceId=instance3

org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=5

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.dataSource=mysqlds

org.quartz.dataSource.mysqlds.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.mysqlds.URL=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin
org.quartz.dataSource.mysqlds.user=kazuhira
org.quartz.dataSource.mysqlds.password=password
org.quartz.dataSource.mysqlds.maxConnections=7

違いを見つけようにも間違い探しのようになっていますが、各ファイルはorg.quartz.scheduler.instanceIdが異なるだけです。

あと、ポイントはorg.quartz.jobStore.isClusteredをtrueとしていることですね。

Quartzは設定ファイルを通常quartz.propertiesとして読み込みますが、他のファイルを指定する場合は-Dorg.quartz.propertiesシステムプロパティで
指定すればOKです。

By default, StdSchedulerFactory load a properties file named “quartz.properties” from the ‘current working directory’. If that fails, then the “quartz.properties” file located (as a resource) in the org/quartz package is loaded. If you wish to use a file other than these defaults, you must define the system property ‘org.quartz.properties’ to point to the file you want.

Configuration Reference

確認してみる

では、確認してみましょう。

以下のようにして実行します。ひとつ目のインスタンスのみ、register-jobを起動引数に指定しています。

## ひとつ目のインスタンス
$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App \
  -Dexec.args=register-job \
  -Dorg.quartz.properties=instance1.properties


## 2つ目のインスタンス
$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App \
  -Dorg.quartz.properties=instance2.properties


## 3つ目のインスタンス
$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App \
  -Dorg.quartz.properties=instance3.properties

ひとつ目のインスタンスでの起動時のログ。

2022-10-22 19:15:06.556 [org.littlewings.quartz.App.main()] INFO  org.quartz.impl.StdSchedulerFactory - Using ConnectionProvider class 'org.quartz.utils.C3p0PoolingConnectionProvider' for data source 'mysqlds'
2022-10-22 19:15:06.604 [MLog-Init-Reporter] INFO  com.mchange.v2.log.MLog - MLog clients using slf4j logging.
2022-10-22 19:15:06.917 [org.littlewings.quartz.App.main()] INFO  com.mchange.v2.c3p0.C3P0Registry - Initializing c3p0-0.9.5.4 [built 23-March-2019 23:00:48 -0700; debug? true; trace: 10]
2022-10-22 19:15:07.088 [org.littlewings.quartz.App.main()] INFO  org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
2022-10-22 19:15:07.191 [org.littlewings.quartz.App.main()] INFO  o.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
2022-10-22 19:15:07.192 [org.littlewings.quartz.App.main()] INFO  org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
2022-10-22 19:15:07.194 [org.littlewings.quartz.App.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - Using db table-based data access locking (synchronization).
2022-10-22 19:15:07.196 [org.littlewings.quartz.App.main()] INFO  o.q.impl.jdbcjobstore.JobStoreTX - JobStoreTX initialized.
2022-10-22 19:15:07.197 [org.littlewings.quartz.App.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'ClusteredExampleScheduler' with instanceId 'instance1'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 5 threads.
  Using job-store 'org.quartz.impl.jdbcjobstore.JobStoreTX' - which supports persistence. and is clustered.

2022-10-22 19:15:07.197 [org.littlewings.quartz.App.main()] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'ClusteredExampleScheduler' initialized from specified file: 'instance1.properties' in the class resource path.
2022-10-22 19:15:07.197 [org.littlewings.quartz.App.main()] INFO  org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
2022-10-22 19:15:07.326 [org.littlewings.quartz.App.main()] INFO  c.m.v.c.i.AbstractPoolBackedDataSource - Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> z8kfsxar1yt0gwk81b43p|586fed53, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.cj.jdbc.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, forceUseNamedDriverClass -> false, identityToken -> z8kfsxar1yt0gwk81b43p|586fed53, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 7, maxStatements -> 0, maxStatementsPerConnection -> 120, minPoolSize -> 1, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {password=******, user=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
2022-10-22 19:15:08.372 [org.littlewings.quartz.App.main()] INFO  org.littlewings.quartz.App - registered, job
2022-10-22 19:15:08.420 [org.littlewings.quartz.App.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler ClusteredExampleScheduler_$_instance1 started.
2022-10-22 19:15:08.420 [org.littlewings.quartz.App.main()] INFO  org.littlewings.quartz.App - Quartz scheduler started.

よーく見るとclusteredと表示されており、クラスターモードになっていることがわかります。

  Using job-store 'org.quartz.impl.jdbcjobstore.JobStoreTX' - which supports persistence. and is clustered.

Job自体は、今回はひとつ目のインスタンスで実行され続けていました。

2022-10-22 19:15:10.043 [ClusteredExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [instance1] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:15:20.035 [ClusteredExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [instance1] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:15:30.104 [ClusteredExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [instance1] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:15:40.053 [ClusteredExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [instance1] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:15:50.037 [ClusteredExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [instance1] execute job, message = [Hello, Clustering Job!!]

そこで、Enterを打ってひとつ目のインスタンスを終了してみます。

2022-10-22 19:18:26.362 [org.littlewings.quartz.App.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler ClusteredExampleScheduler_$_instance1 shutting down.
2022-10-22 19:18:26.362 [org.littlewings.quartz.App.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler ClusteredExampleScheduler_$_instance1 paused.
2022-10-22 19:18:26.667 [org.littlewings.quartz.App.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler ClusteredExampleScheduler_$_instance1 shutdown complete.

少し待っていると、今回は3つ目のインスタンスが検知したようです。

2022-10-22 19:18:39.266 [QuartzScheduler_ClusteredExampleScheduler-instance3_ClusterManager] INFO  o.q.impl.jdbcjobstore.JobStoreTX - ClusterManager: detected 1 failed or restarted instances.
2022-10-22 19:18:39.266 [QuartzScheduler_ClusteredExampleScheduler-instance3_ClusterManager] INFO  o.q.impl.jdbcjobstore.JobStoreTX - ClusterManager: Scanning for instance "instance1"'s failed in-progress jobs.
2022-10-22 19:18:39.383 [ClusteredExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [instance3] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:18:40.135 [ClusteredExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [instance3] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:18:50.062 [ClusteredExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [instance3] execute job, message = [Hello, Clustering Job!!]

この後、Jobは3つ目のインスタンスで実行され続けました。

停止したひとつ目のインスタンスは、起動引数を指定しなければ再度クラスターに参加できます。

$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App \
  -Dorg.quartz.properties=instance1.properties

この後、インスタンスを停止したり、再度起動したりするとアクティブなインスタンスを移りつつもJobは実行され続けることが確認できます。

今回は実行するJobが少ないからか、ロードバランシグ的な動作は確認できませんでしたが…。

ここで、すべてのインスタンスを1度終了して、登録したJobをクリアしておきます。

$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App \
  -Dexec.args=clear-job \
  -Dorg.quartz.properties=instance1.properties

このインスタンス自体も終了させる必要がありますが…。

インスタンスのIDを自動採番する

今回、各インスタンスのIDを個々に指定するためにそれぞれ設定ファイルを作成しました。

とはいえ、インスタンスの数が増減したりすると、インスタンスごとに設定ファイルを用意するのは面倒かもしれません。

ここで、org.quartz.scheduler.instanceIdプロパティの説明を見るとAUTOと指定することでインスタンスのIDを自動採番できそうなことが
わかりました。

org.quartz.scheduler.instanceId

You may use the value “AUTO” as the instanceId if you wish the Id to be generated for you.

Configuration Reference

こちらを試してみましょう。

設定ファイルをquartz.propertiesにして、org.quartz.scheduler.instanceIdをAUTOと指定します。

src/main/resources/quartz.properties

org.quartz.scheduler.instanceName=ClusteredExampleScheduler
org.quartz.scheduler.instanceId=AUTO

org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=5

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.dataSource=mysqlds

org.quartz.dataSource.mysqlds.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.mysqlds.URL=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin
org.quartz.dataSource.mysqlds.user=kazuhira
org.quartz.dataSource.mysqlds.password=password
org.quartz.dataSource.mysqlds.maxConnections=7

今度は、-Dorg.quartz.propertiesシステムプロパティでQuartzの設定ファイルを指定せずに起動します。ひとつ目のインスタンスには起動引数を
指定するのは、先ほどと同じです。

## ひとつ目のインスタンス
$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App \
  -Dexec.args=register-job


## 2つ目のインスタンス
$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App


## 3つ目のインスタンス
$ mvn compile exec:java \
  -Dexec.mainClass=org.littlewings.quartz.App

起動時のインスタンスIDを見ると、自動採番されているようなのですが、どうもホスト名が入っているようです。

2022-10-22 19:32:18.952 [org.littlewings.quartz.App.main()] INFO  org.quartz.core.QuartzScheduler - Scheduler ClusteredExampleScheduler_$_myhost1666434737522 started.
2022-10-22 19:32:18.954 [org.littlewings.quartz.App.main()] INFO  org.littlewings.quartz.App - Quartz scheduler started.

Jobの実行ログ。

2022-10-22 19:32:20.044 [ClusteredExampleScheduler_Worker-1] INFO  o.littlewings.quartz.PrintMessageJob - [myhost1666434737522] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:32:30.145 [ClusteredExampleScheduler_Worker-2] INFO  o.littlewings.quartz.PrintMessageJob - [myhost1666434737522] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:32:40.492 [ClusteredExampleScheduler_Worker-3] INFO  o.littlewings.quartz.PrintMessageJob - [myhost1666434737522] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:32:50.051 [ClusteredExampleScheduler_Worker-4] INFO  o.littlewings.quartz.PrintMessageJob - [myhost1666434737522] execute job, message = [Hello, Clustering Job!!]
2022-10-22 19:33:00.033 [ClusteredExampleScheduler_Worker-5] INFO  o.littlewings.quartz.PrintMessageJob - [myhost1666434737522] execute job, message = [Hello, Clustering Job!!]

やっぱりホスト名が入っています。また、ホスト名の後ろに付いているのはタイムスタンプな感じがしますね。

ちょっとドキュメントを見てみましょう。インスタンスIDの採番については、org.quartz.scheduler.instanceIdGenerator.classプロパティで
クラスを指定できるみたいです。

org.quartz.scheduler.instanceIdGenerator.class

Only used if org.quartz.scheduler.instanceId is set to “AUTO”. Defaults to “org.quartz.simpl.SimpleInstanceIdGenerator”, which generates an instance id based upon host name and time stamp. Other IntanceIdGenerator implementations include SystemPropertyInstanceIdGenerator (which gets the instance id from the system property “org.quartz.scheduler.instanceId”, and HostnameInstanceIdGenerator which uses the local host name (InetAddress.getLocalHost().getHostName()). You can also implement the InstanceIdGenerator interface your self.

Configuration Reference

ここでorg.quartz.scheduler.instanceIdにAUTOを指定した場合はorg.quartz.simpl.SimpleInstanceIdGeneratorが使われる、となっています。

InstanceIdGeneratorが、インスタンスIDを採番するインターフェースです。

InstanceIdGenerator (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)

SimpleInstanceIdGeneratorは、ホスト名と現在時刻でインスタンスIDを作成するInstanceIdGeneratorの実装です。

The default InstanceIdGenerator used by Quartz when instance id is to be automatically generated. Instance id is of the form HOSTNAME + CURRENT_TIME.

SimpleInstanceIdGenerator (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)

その他、ホスト名をインスタンスIDとするものや、システムプロパティでインスタンスIDを指定できる実装もあるようです。

HostnameInstanceIdGenerator (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)

SystemPropertyInstanceIdGenerator (Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API)

他の採番ルールにしたかったら、自分でInstanceIdGeneratorインターフェースの実装を作成して
org.quartz.scheduler.instanceIdGenerator.classプロパティで指定すれば良さそうですね。

今回は、こんなところでしょうか。

まとめ

Quartzのクラスタリングを試してみました。

とても簡単に使えたので良いのですが、今回はロードバランシング的なところが確認できなかったのがちょっと残念ですね。

まあ、基本的なところは確認できたと思うので、今回はこれで良いでしょう。