クラスター環境で実行されているSpringスケジュールタスク


98

60秒ごとに実行されるcronジョブを持つアプリケーションを作成しています。アプリケーションは、必要に応じて複数のインスタンスに拡張できるように構成されています。60秒ごとに1つのインスタンスでのみタスクを実行したい(任意のノードで)。箱から出して、私はこれに対する解決策を見つけることができません、そして私はそれが以前に何度も尋ねられなかったことに驚いています。Spring4.1.6を使用しています。

    <task:scheduled-tasks>
        <task:scheduled ref="beanName" method="execute" cron="0/60 * * * * *"/>
    </task:scheduled-tasks>

7
私は、クォーツはあなたのための最善の解決策だと思う:stackoverflow.com/questions/6663182/...
selalerer

での使用に関する提案CronJobはありkubernetesますか?
ch271828n

回答:


97

ありShedLockのまさにこの目的を果たすプロジェクトが。実行時にロックする必要があるタスクに注釈を付けるだけです

@Scheduled( ... )
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
   // do something
}

SpringとLockProviderを構成する

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class MySpringConfiguration {
    ...
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
       return new JdbcTemplateLockProvider(dataSource);
    }
    ...
}

1
「お疲れ様でした!」と言いたいだけです。しかし...素晴らしい機能は、ライブラリがコードで明示的に提供せずにデータベース名を検出できる場合です...それが優れた動作をすることを除いて!
Krzysiek

OracleとSpringのブートデータjpaスターターで動作します。
Mahendran Ayyarsamy Kandiar

このソリューションはSpring3.1.1.RELEASEおよびjava6で機能しますか?伝えてください。
Vikas Sharma

MsSQLとSpringブートJPAを試してみましたが、SQL部分でliquibaseスクリプトを使用しました。うまく機能します。ありがとう
2018

それは確かにうまく機能しています。しかし、ここで少し複雑なケースに遭遇しました。ご覧ください。ありがとう!!! stackoverflow.com/questions/57691205/...
デイトン王


15

これは、クラスター内でジョブを安全に実行するためのもう1つのシンプルで堅牢な方法です。データベースに基づいてタスクを実行できるのは、ノードがクラスター内の「リーダー」である場合のみです。

また、ノードに障害が発生したり、クラスター内でシャットダウンしたりすると、別のノードがリーダーになりました。

あなたが持っているのは、「リーダー選出」メカニズムを作成し、毎回あなたがリーダーであるかどうかを確認することです。

@Scheduled(cron = "*/30 * * * * *")
public void executeFailedEmailTasks() {
    if (checkIfLeader()) {
        final List<EmailTask> list = emailTaskService.getFailedEmailTasks();
        for (EmailTask emailTask : list) {
            dispatchService.sendEmail(emailTask);
        }
    }
}

次の手順に従います。

1.クラスター内のノードごとに1つのエントリを保持するオブジェクトとテーブルを定義します。

@Entity(name = "SYS_NODE")
public class SystemNode {

/** The id. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/** The name. */
@Column(name = "TIMESTAMP")
private String timestamp;

/** The ip. */
@Column(name = "IP")
private String ip;

/** The last ping. */
@Column(name = "LAST_PING")
private Date lastPing;

/** The last ping. */
@Column(name = "CREATED_AT")
private Date createdAt = new Date();

/** The last ping. */
@Column(name = "IS_LEADER")
private Boolean isLeader = Boolean.FALSE;

public Long getId() {
    return id;
}

public void setId(final Long id) {
    this.id = id;
}

public String getTimestamp() {
    return timestamp;
}

public void setTimestamp(final String timestamp) {
    this.timestamp = timestamp;
}

public String getIp() {
    return ip;
}

public void setIp(final String ip) {
    this.ip = ip;
}

public Date getLastPing() {
    return lastPing;
}

public void setLastPing(final Date lastPing) {
    this.lastPing = lastPing;
}

public Date getCreatedAt() {
    return createdAt;
}

public void setCreatedAt(final Date createdAt) {
    this.createdAt = createdAt;
}

public Boolean getIsLeader() {
    return isLeader;
}

public void setIsLeader(final Boolean isLeader) {
    this.isLeader = isLeader;
}

@Override
public String toString() {
    return "SystemNode{" +
            "id=" + id +
            ", timestamp='" + timestamp + '\'' +
            ", ip='" + ip + '\'' +
            ", lastPing=" + lastPing +
            ", createdAt=" + createdAt +
            ", isLeader=" + isLeader +
            '}';
}

}

2. a)データベースにノードを挿入し、b)リーダーを確認するサービスを作成します

@Service
@Transactional
public class SystemNodeServiceImpl implements SystemNodeService,    ApplicationListener {

/** The logger. */
private static final Logger LOGGER = Logger.getLogger(SystemNodeService.class);

/** The constant NO_ALIVE_NODES. */
private static final String NO_ALIVE_NODES = "Not alive nodes found in list {0}";

/** The ip. */
private String ip;

/** The system service. */
private SystemService systemService;

/** The system node repository. */
private SystemNodeRepository systemNodeRepository;

@Autowired
public void setSystemService(final SystemService systemService) {
    this.systemService = systemService;
}

@Autowired
public void setSystemNodeRepository(final SystemNodeRepository systemNodeRepository) {
    this.systemNodeRepository = systemNodeRepository;
}

@Override
public void pingNode() {
    final SystemNode node = systemNodeRepository.findByIp(ip);
    if (node == null) {
        createNode();
    } else {
        updateNode(node);
    }
}

@Override
public void checkLeaderShip() {
    final List<SystemNode> allList = systemNodeRepository.findAll();
    final List<SystemNode> aliveList = filterAliveNodes(allList);

    SystemNode leader = findLeader(allList);
    if (leader != null && aliveList.contains(leader)) {
        setLeaderFlag(allList, Boolean.FALSE);
        leader.setIsLeader(Boolean.TRUE);
        systemNodeRepository.save(allList);
    } else {
        final SystemNode node = findMinNode(aliveList);

        setLeaderFlag(allList, Boolean.FALSE);
        node.setIsLeader(Boolean.TRUE);
        systemNodeRepository.save(allList);
    }
}

/**
 * Returns the leaded
 * @param list
 *          the list
 * @return  the leader
 */
private SystemNode findLeader(final List<SystemNode> list) {
    for (SystemNode systemNode : list) {
        if (systemNode.getIsLeader()) {
            return systemNode;
        }
    }
    return null;
}

@Override
public boolean isLeader() {
    final SystemNode node = systemNodeRepository.findByIp(ip);
    return node != null && node.getIsLeader();
}

@Override
public void onApplicationEvent(final ApplicationEvent applicationEvent) {
    try {
        ip = InetAddress.getLocalHost().getHostAddress();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    if (applicationEvent instanceof ContextRefreshedEvent) {
        pingNode();
    }
}

/**
 * Creates the node
 */
private void createNode() {
    final SystemNode node = new SystemNode();
    node.setIp(ip);
    node.setTimestamp(String.valueOf(System.currentTimeMillis()));
    node.setCreatedAt(new Date());
    node.setLastPing(new Date());
    node.setIsLeader(CollectionUtils.isEmpty(systemNodeRepository.findAll()));
    systemNodeRepository.save(node);
}

/**
 * Updates the node
 */
private void updateNode(final SystemNode node) {
    node.setLastPing(new Date());
    systemNodeRepository.save(node);
}

/**
 * Returns the alive nodes.
 *
 * @param list
 *         the list
 * @return the alive nodes
 */
private List<SystemNode> filterAliveNodes(final List<SystemNode> list) {
    int timeout = systemService.getSetting(SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUT, Integer.class);
    final List<SystemNode> finalList = new LinkedList<>();
    for (SystemNode systemNode : list) {
        if (!DateUtils.hasExpired(systemNode.getLastPing(), timeout)) {
            finalList.add(systemNode);
        }
    }
    if (CollectionUtils.isEmpty(finalList)) {
        LOGGER.warn(MessageFormat.format(NO_ALIVE_NODES, list));
        throw new RuntimeException(MessageFormat.format(NO_ALIVE_NODES, list));
    }
    return finalList;
}

/**
 * Finds the min name node.
 *
 * @param list
 *         the list
 * @return the min node
 */
private SystemNode findMinNode(final List<SystemNode> list) {
    SystemNode min = list.get(0);
    for (SystemNode systemNode : list) {
        if (systemNode.getTimestamp().compareTo(min.getTimestamp()) < -1) {
            min = systemNode;
        }
    }
    return min;
}

/**
 * Sets the leader flag.
 *
 * @param list
 *         the list
 * @param value
 *         the value
 */
private void setLeaderFlag(final List<SystemNode> list, final Boolean value) {
    for (SystemNode systemNode : list) {
        systemNode.setIsLeader(value);
    }
}

}

3.データベースにpingを実行して、自分が生きていることを送信します

@Override
@Scheduled(cron = "0 0/5 * * * ?")
public void executeSystemNodePing() {
    systemNodeService.pingNode();
}

@Override
@Scheduled(cron = "0 0/10 * * * ?")
public void executeLeaderResolution() {
    systemNodeService.checkLeaderShip();
}

4.準備ができました!タスクを実行する前に、あなたがリーダーであるかどうかを確認してください。

@Override
@Scheduled(cron = "*/30 * * * * *")
public void executeFailedEmailTasks() {
    if (checkIfLeader()) {
        final List<EmailTask> list = emailTaskService.getFailedEmailTasks();
        for (EmailTask emailTask : list) {
            dispatchService.sendEmail(emailTask);
        }
    }
}

この場合、SystemServiceとSettingEnumとは何ですか?非常にシンプルで、タイムアウト値を返すだけのようです。その場合、タイムアウトをハードコーディングしないのはなぜですか?
tlavarea 2017年

@ mspapant、SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUTとは何ですか?ここで使用する必要がある最適値は何ですか?
user525146 2017

@tlavareaこのコードを実装しましたか?DateUtils.hasExpiredメソッドについて質問がありますか?それはカスタムメソッドですか、それともApacheの一般的なユーティリティですか?
user525146 2017

10

バッチジョブとスケジュールされたジョブは通常、顧客向けのアプリから離れた独自のスタンドアロンサーバーで実行されるため、クラスターで実行されることが予想されるアプリケーションにジョブを含めることは一般的な要件ではありません。さらに、クラスター環境のジョブは通常、同じジョブの他のインスタンスが並行して実行されることを心配する必要がないため、ジョブインスタンスの分離が大きな要件ではないもう1つの理由です。

簡単な解決策は、Springプロファイル内でジョブを構成することです。たとえば、現在の構成が次の場合:

<beans>
  <bean id="someBean" .../>

  <task:scheduled-tasks>
    <task:scheduled ref="someBean" method="execute" cron="0/60 * * * * *"/>
  </task:scheduled-tasks>
</beans>

次のように変更します。

<beans>
  <beans profile="scheduled">
    <bean id="someBean" .../>

    <task:scheduled-tasks>
      <task:scheduled ref="someBean" method="execute" cron="0/60 * * * * *"/>
    </task:scheduled-tasks>
  </beans>
</beans>

次に、scheduledプロファイルがアクティブ化された1台のマシンでアプリケーションを起動します(-Dspring.profiles.active=scheduled)。

何らかの理由でプライマリサーバーが使用できなくなった場合は、プロファイルを有効にして別のサーバーを起動するだけで、問題なく動作し続けます。


ジョブの自動フェイルオーバーも必要な場合は、状況が変わります。次に、すべてのサーバーでジョブを実行し続け、データベーステーブル、クラスター化されたキャッシュ、JMX変数などの共通リソースを介して同期を確認する必要があります。


58
これは有効な回避策ですが、ノードがダウンした場合に他のノードが他の要求を処理できるクラスター環境の背後にある考え方に違反します。この回避策では、「スケジュールされた」プロファイルを持つノードがダウンした場合、このバックグラウンドジョブは実行されません
Ahmed Hashem

3
それをアーカイブするために、アトミックgetset操作でRedisを使用できると思います。
Thanh Nguyen Van

提案にはいくつかの問題があります。1。通常、クラスターの各ノードにまったく同じ構成を持たせたいので、100%互換性があり、共有する同じ負荷の下で同じリソースが必要になります。2.ソリューションでは、「タスク」ノードがダウンしたときに手動で介入する必要があります。3.現在の実行の処理が完了する前に「タスク」ノードがダウンし、最初のノードがダウンした後に新しい「タスクランナー」が作成されたため、ジョブが実際に正常に実行されたことを保証することはできません。終わったかどうか。
MosheBixenshpaner19年

1
それは単にクラスター化された環境の概念に違反しているだけであり、あなたが提案したアプローチで解決策はあり得ません。プロファイルサーバーでさえ複製して可用性を確保することはできません。これは、追加のコストと不要なリソースの浪費につながるためです。@Thanhによって提案された解決策は、これよりもはるかにクリーンです。MUTEXと同じように考えてください。スクリプトを実行しているサーバーは、redisなどの分散キャッシュで一時的なロックを取得してから、従来のロックの概念に進みます。
anujpradhan19年

2

dlockは、データベースのインデックスと制約を使用して、タスクを1回だけ実行するように設計されています。あなたは単に以下のようなことをすることができます。

@Scheduled(cron = "30 30 3 * * *")
@TryLock(name = "executeMyTask", owner = SERVER_NAME, lockFor = THREE_MINUTES)
public void execute() {

}

使用方法についての記事をご覧ください。


3
dlockを使用している場合。ロックを維持するためにDBを使用していると仮定します。そして、クラスター内のノードの1つがロックを取得した後に予期せずダウンした場合、このシナリオではどうなりますか?デッドロック状態になりますか?
バッドマン2018

1

私はデータベーステーブルを使用してロックを行っています。テーブルへの挿入を実行できるのは、一度に1つのタスクのみです。もう1つはDuplicateKeyExceptionを取得します。挿入と削除のロジックは、@ Scheduledアノテーションの周りの側面によって処理されます。Spring Boot2.0を使用しています

@Component
@Aspect
public class SchedulerLock {

    private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerLock.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;  

    @Around("execution(@org.springframework.scheduling.annotation.Scheduled * *(..))")
    public Object lockTask(ProceedingJoinPoint joinPoint) throws Throwable {

        String jobSignature = joinPoint.getSignature().toString();
        try {
            jdbcTemplate.update("INSERT INTO scheduler_lock (signature, date) VALUES (?, ?)", new Object[] {jobSignature, new Date()});

            Object proceed = joinPoint.proceed();

            jdbcTemplate.update("DELETE FROM scheduler_lock WHERE lock_signature = ?", new Object[] {jobSignature});
            return proceed;

        }catch (DuplicateKeyException e) {
            LOGGER.warn("Job is currently locked: "+jobSignature);
            return null;
        }
    }
}


@Component
public class EveryTenSecondJob {

    @Scheduled(cron = "0/10 * * * * *")
    public void taskExecution() {
        System.out.println("Hello World");
    }
}


CREATE TABLE scheduler_lock(
    signature varchar(255) NOT NULL,
    date datetime DEFAULT NULL,
    PRIMARY KEY(signature)
);

3
あなたはそれが完璧に機能すると思いますか?ロックを取得した後にノードの1つがダウンした場合、他のノードはロックがある理由を知ることができないためです(この場合、テーブルのジョブに対応する行エントリ)。
バッドマン2018

0

db-schedulerのような埋め込み可能なスケジューラーを使用できますこれを実現。永続的な実行があり、単純な楽観的ロックメカニズムを使用して、単一ノードによる実行を保証します。

ユースケースを実現する方法のサンプルコード:

   RecurringTask<Void> recurring1 = Tasks.recurring("my-task-name", FixedDelay.of(Duration.ofSeconds(60)))
    .execute((taskInstance, executionContext) -> {
        System.out.println("Executing " + taskInstance.getTaskAndInstance());
    });

   final Scheduler scheduler = Scheduler
          .create(dataSource)
          .startTasks(recurring1)
          .build();

   scheduler.start();

-1

Springコンテキストはクラスター化されていないため、分散アプリケーションでタスクを管理するのは少し難しく、jgroupをサポートするシステムを使用して状態を同期し、タスクを優先してアクションを実行する必要があります。または、ejbコンテキストを使用してjbossha環境のようなクラスター化されたhaシングルトンサービスを管理 できます https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbdまたはクラスター化されたキャッシュとアクセスロックリソースを使用できますサービスと最初のサービスの間でロックが実行され、アクションが実行されるか、独自のjgroupが実装されてサービスが通信され、1つのノードでアクションが実行されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.