Весняне заплановане завдання, що працює в кластерному середовищі


97

Я пишу заявку, в якій є завдання cron, яке виконується кожні 60 секунд. Додаток налаштовано на масштабування, коли це потрібно, на кілька екземплярів. Я хочу виконати завдання лише на 1 екземплярі кожні 60 секунд (на будь-якому вузлі). Я не можу знайти рішення цього питання, і я здивований, що раніше про це не просили кілька разів. Я використовую Spring 4.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 boot data jpa starter.
Mahendran Ayyarsamy Kandiar

Чи працює це рішення для Spring 3.1.1.RELEASE та Java 6? Будь ласка, скажи.
Вікас Шарма

Я пробував із MsSQL та Spring JPA та використовував скрипт liquibase для частини SQL .. працює добре .. Дякую
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. Створіть службу, яка а) вставляє вузол у базу даних, б) перевіряє лідер

@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? це власний метод чи це звичайні утиліти apache?
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 тощо.


57
Це дійсний спосіб вирішення проблеми, але це порушить ідею створення кластерного середовища, де, якщо вузол не працює, інший вузол може обслуговувати інші запити. У цьому обхідному шляху, якщо вузол із "запланованим" профілем не працює, ця фонова робота не запускатиметься
Ахмед Хашем

3
Я думаю, ми могли б використати Redis за допомогою атомної getта setопераційної діяльності, щоб досягти цього.
Тхань Нгуєн Ван

З вашою пропозицією є кілька проблем: 1. Як правило, ви хотіли б, щоб кожен вузол кластера мав абсолютно однакову конфігурацію, тому вони будуть на 100% взаємозамінні і вимагатимуть однакові ресурси під тим самим навантаженням, яким вони діляться. 2. Ваше рішення вимагатиме втручання вручну, коли вузол "завдання" не працює. 3. Це все одно не гарантує успішного виконання завдання, оскільки вузол "завдання" зайшов до завершення обробки поточного виконання, а новий "виконавець завдань" був створений після завершення першого, не знаючи, чи це закінчилося чи ні.
Моше Біксеншпанер

1
це просто порушує ідею кластерного середовища; рішення, запропоноване вами, не може бути вирішене. Ви не можете повторити навіть сервери профілів, щоб забезпечити доступність, оскільки це призведе до додаткових витрат і непотрібної витрати ресурсів. Рішення, запропоноване @Thanh, набагато чистіше, ніж це. Подумайте про те саме, що і про MUTEX. Будь-який сервер, на якому запущений сценарій, отримає тимчасову блокування в якомусь розподіленому кеші, наприклад, redis, а потім продовжить роботу з концепціями традиційного блокування.
anuj pradhan

2

dlock призначений для запуску завдань лише один раз за допомогою індексів бази даних та обмежень. Ви можете просто зробити щось подібне нижче.

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

}

Дивіться статтю про його використання.


3
Якщо ми використовуємо dlock. Припустимо, ми використовуємо БД для підтримки блокування. І один із вузлів кластера несподівано провалився після блокування, що буде в цьому сценарії? Чи буде воно в тупиковому стані?
Badman

1

Для блокування я використовую таблицю бази даних. Вставити в таблицю одночасно може лише одне завдання. Інший отримає 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

0

Для цього ви можете використовувати вбудований планувальник, такий як db- планировщик. Він має постійні виконання та використовує простий оптимістичний механізм блокування, щоб гарантувати виконання одним вузлом.

Приклад коду того, як можна досягти варіанту використання:

   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 для управління кластеризованою службою одиночного типу, як середовище jboss ha https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbd Або ви можете використовувати кластерний кеш та ресурс блокування доступу між сервісом і першим сервісом виконайте блокування буде виконувати дію або реалізувати вашу власну jgroup для зв'язку вашої служби і виконувати дію один вузол

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.