클러스터 환경에서 실행되는 Spring Scheduled Task


105

60 초마다 실행되는 크론 작업이있는 애플리케이션을 작성 중입니다. 애플리케이션은 필요할 때 여러 인스턴스로 확장되도록 구성됩니다. 60 초마다 1 개의 인스턴스에서만 작업을 실행하고 싶습니다 (모든 노드에서). 상자에서 나는 이것에 대한 해결책을 찾을 수 없으며 이전에 여러 번 요청되지 않은 것에 놀랐습니다. Spring 4.1.6을 사용하고 있습니다.

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

8
: 나는 석영 최고의 당신을위한 솔루션입니다 생각 stackoverflow.com/questions/6663182/...
selalerer

사용 CronJob에 대한 제안 사항 이 kubernetes있습니까?
ch271828n

답변:


103

이 목적을 정확히 수행 하는 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 2011 년

이 솔루션은 Spring 3.1.1.RELEASE 및 Java 6에서 작동합니까? 제발 말해줘.
Vikas Sharma

나는 MsSQL과 Spring 부트 JPA로 시도했고 SQL 부분에서 liquibase 스크립트를 사용했습니다. 잘 작동합니다 .. 감사합니다
sheetal

실제로 잘 작동합니다. 하지만 여기서 좀 복잡한 사건을 만났는데 한번 보시면 감사하겠습니다. 감사!!! stackoverflow.com/questions/57691205/...
데이턴 왕에게


15

이는 클러스터에서 작업을 안전하게 실행하는 또 다른 간단하고 강력한 방법입니다. 데이터베이스를 기반으로하고 노드가 클러스터의 "리더"인 경우에만 작업을 실행할 수 있습니다.

또한 클러스터에서 노드가 실패하거나 종료되면 다른 노드가 리더가되었습니다.

당신이 가진 모든 것은 "리더 선거"메커니즘을 만들고 당신이 리더인지 확인할 때마다 :

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

다음 단계를 따르십시오.

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. 데이터베이스를 핑하여 살아 있음을 보냅니다.

@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

@mspapant, SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUT은 무엇입니까? 여기서 사용해야하는 최적의 값은 무엇입니까?
user525146

@tlavarea이 코드를 구현 했습니까? DateUtils.hasExpired 메서드에 대한 질문이 있습니까? 그것은 사용자 정의 방법입니까 아니면 아파치 일반적인 유틸리티입니까?
user525146

10

배치 및 예약 된 작업은 일반적으로 고객 대면 앱에서 떨어진 자체 독립 실행 형 서버에서 실행되므로 클러스터에서 실행될 것으로 예상되는 애플리케이션에 작업을 포함하는 것이 일반적인 요구 사항이 아닙니다. 또한 클러스터 환경의 작업은 일반적으로 병렬로 실행되는 동일한 작업의 다른 인스턴스에 대해 걱정할 필요가 없으므로 작업 인스턴스의 격리가 큰 요구 사항이 아닌 또 다른 이유가 있습니다.

간단한 해결책은 Spring Profile 내에 작업을 구성하는 것입니다. 예를 들어 현재 구성이 다음과 같은 경우 :

<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프로필이 활성화 된 ( -Dspring.profiles.active=scheduled) 하나의 컴퓨터에서만 애플리케이션을 시작합니다 .

어떤 이유로 기본 서버를 사용할 수없는 경우 프로필을 활성화 한 상태에서 다른 서버를 시작하면 문제가 계속 발생합니다.


작업에 대한 자동 장애 조치도 원하는 경우 상황이 변경됩니다. 그런 다음 모든 서버에서 작업을 계속 실행하고 데이터베이스 테이블, 클러스터 된 캐시, JMX 변수 등과 같은 공통 리소스를 통해 동기화를 확인해야합니다.


58
이것은 유효한 해결 방법이지만 노드가 다운되면 다른 노드가 다른 요청을 처리 할 수있는 클러스터 환경의 개념을 위반하게됩니다. 이 해결 방법에서 "예약 된"프로필이있는 노드가 다운되면이 백그라운드 작업이 실행되지 않습니다.
Ahmed Hashem

3
Redis를 원자 getset운영 과 함께 사용하여 아카이브 할 수 있다고 생각 합니다.
Thanh Nguyen Van

제안 사항에는 몇 가지 문제가 있습니다. 1. 일반적으로 클러스터의 각 노드가 정확히 동일한 구성을 갖기를 원하므로 100 % 상호 교환이 가능하고 공유하는 동일한 부하에서 동일한 리소스가 필요합니다. 2. "작업"노드가 다운되면 솔루션에 수동 개입이 필요합니다. 3. 현재 실행 처리를 완료하기 전에 "작업"노드가 다운되었고 첫 번째 작업이 다운 된 후 새로운 "작업 실행자"가 생성 되었기 때문에 작업이 실제로 성공적으로 실행되었다고 보장 할 수 없습니다. 끝났 든 아니든.
Moshe Bixenshpaner

1
그것은 단순히 클러스터 된 환경의 아이디어를 위반하는 것입니다. 당신이 제안한 접근 방식으로는 어떤 해결책도있을 수 없습니다. 추가 비용과 불필요한 리소스 낭비가 발생하기 때문에 가용성을 보장하기 위해 프로필 서버조차 복제 할 수 없습니다. @Thanh가 제안한 솔루션은 이것보다 훨씬 깨끗합니다. MUTEX와 동일하게 생각하십시오. 스크립트를 실행하는 모든 서버는 redis와 같은 일부 분산 캐시에서 임시 잠금을 획득 한 다음 기존 잠금 개념으로 진행합니다.
아누 즈 프라

2

잠금을 수행하기 위해 데이터베이스 테이블을 사용하고 있습니다. 한 번에 하나의 작업 만 테이블에 삽입 할 수 있습니다. 다른 하나는 DuplicateKeyException을받습니다. 삽입 및 삭제 논리는 @Scheduled 주석 주변의 측면 에서 처리됩니다. Spring Boot 2.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
완벽하게 작동 할 것이라고 생각하십니까? 노드 중 하나가 잠금을 취한 후 다운되면 다른 노드가 잠금이 발생한 이유를 알 수 없기 때문입니다 (귀하의 경우 테이블의 작업에 해당하는 행 항목).
Badman dec.

2

dlock 은 데이터베이스 인덱스 및 제약 조건을 사용하여 작업을 한 번만 실행하도록 설계되었습니다. 아래와 같이 간단히 할 수 있습니다.

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

}

사용에 대한 기사 를 참조하십시오 .


4
dlock. 잠금 유지를 위해 DB를 사용한다고 가정합니다. 그리고 클러스터의 노드 중 하나가 잠금을 설정 한 후 예기치 않게 다운되면이 시나리오에서 어떤 일이 발생합니까? 교착 상태가됩니까?
Badman

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();

-2

Spring 컨텍스트는 클러스터되지 않으므로 분산 응용 프로그램에서 작업을 관리하는 것이 약간 어렵고 jgroup을 지원하는 시스템을 사용하여 상태를 동기화하고 작업을 우선적으로 실행하도록해야합니다. 또는 ejb 컨텍스트를 사용하여 jboss ha 환경 https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbd 와 같은 클러스터형 ha 싱글 톤 서비스를 관리 할 수 있습니다. 또는 클러스터형 캐시 및 액세스 잠금 리소스를 사용할 수 있습니다. 서비스와 첫 번째 서비스 사이에 잠금이 조치를 취하거나 자신의 jgroup을 구현하여 서비스를 전달하고 조치를 하나의 노드로 수행합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.