Kümelenmiş ortamda çalışan Bahar Zamanlanmış Görevi


98

Her 60 saniyede bir cron işi yürüten bir uygulama yazıyorum. Uygulama, gerektiğinde birden çok örneğe ölçeklenecek şekilde yapılandırılmıştır. Görevi yalnızca her 60 saniyede 1 örnekte (herhangi bir düğümde) yürütmek istiyorum. Kutunun dışında buna bir çözüm bulamıyorum ve daha önce birçok kez sorulmamasına şaşırdım. Spring 4.1.6 kullanıyorum.

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


Kullanmayla ilgili herhangi bir öneriniz CronJobiçinde kubernetes?
ch271828n

Yanıtlar:


98

Tam olarak bu amaca hizmet eden bir ShedLock projesi var. Sadece yürütüldüğünde kilitlenmesi gereken görevlere açıklama eklersiniz

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

Yay ve LockProvider'ı Yapılandırma

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

1
Sadece "İyi iş!" Demek istiyorum. Ama ... Güzel özellik, kütüphanenin veritabanı adını kodda açıkça belirtmeden keşfedebilmesi olacaktır ... Mükemmel çalışması dışında!
Krzysiek

Oracle ve Spring boot data jpa starter ile benim için çalışıyor.
Mahendran Ayyarsamy Kandiar

Bu çözüm Spring 3.1.1.RELEASE ve java 6 için çalışıyor mu? Söyleyin lütfen.
Vikas Sharma

MsSQL ve Spring boot JPA ile denedim ve SQL kısmı için sıvı betiği kullandım .. iyi çalışıyor .. Teşekkürler
18:50

Gerçekten iyi çalışıyor. Ancak burada biraz karmaşık vakayla karşılaştım, lütfen bir göz atabilir misiniz? Teşekkürler!!! stackoverflow.com/questions/57691205/…
Dayton Wang


15

Bu, bir kümedeki bir işi güvenli bir şekilde yürütmenin başka bir basit ve sağlam yoludur. Veritabanını temel alabilir ve görevi yalnızca düğüm kümedeki "lider" ise yürütebilirsiniz.

Ayrıca, kümedeki bir düğüm başarısız olduğunda veya kapatıldığında, başka bir düğüm lider olur.

Sahip olduğunuz tek şey bir "lider seçimi" mekanizması oluşturmak ve her seferinde lider olup olmadığınızı kontrol etmek:

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

Şu adımları izleyin:

1. Kümedeki düğüm başına bir giriş tutan nesneyi ve tabloyu tanımlayın:

@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) düğümü veritabanına ekleyen, b) lideri kontrol eden hizmeti oluşturun

@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. hayatta olduğunuzu göndermek için veritabanını göndermek

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

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

4. hazırsınız! Görevi yürütmeden önce lider olup olmadığınızı kontrol edin:

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

Bu durumda SystemService ve SettingEnum nedir? Görünüşe göre son derece basit ve sadece bir zaman aşımı değeri döndürüyor. Bu durumda neden zaman aşımını sabit kodlamayasınız?
tlavarea

@mspapant, SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUT nedir? Burada kullanmam gereken optimum değer nedir?
user525146

@tlavarea bu kodu uyguladınız mı, DateUtils.hasExpired yöntemi hakkında bir sorum var mı? özel bir yöntem mi yoksa bir apache ortak araçları mı?
user525146

10

Toplu iş ve zamanlanmış işler, genellikle müşteriye dönük uygulamalardan uzakta kendi bağımsız sunucularında çalıştırılır, bu nedenle bir küme üzerinde çalışması beklenen bir uygulamaya bir iş eklemek yaygın bir gereksinim değildir. Ek olarak, kümelenmiş ortamlardaki işlerin tipik olarak paralel çalışan aynı işin diğer örnekleri için endişelenmesine gerek yoktur, bu nedenle iş örneklerinin yalıtılmasının büyük bir gereksinim olmamasının başka bir nedeni de vardır.

İşlerinizi bir Yay Profili içinde yapılandırmak basit bir çözüm olabilir. Örneğin, mevcut konfigürasyonunuz:

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

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

şununla değiştirin:

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

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

Ardından, uygulamanızı scheduledprofilin etkin olduğu tek bir makinede başlatın ( -Dspring.profiles.active=scheduled).

Birincil sunucu herhangi bir nedenle kullanılamaz hale gelirse, sadece profil etkinleştirilmiş başka bir sunucu başlatın ve her şey yolunda gitmeye devam edecektir.


İşler için de otomatik yük devretme istiyorsanız işler değişir. Ardından, işin tüm sunucularda çalışmasını sağlamanız ve bir veritabanı tablosu, kümelenmiş bir önbellek, bir JMX değişkeni vb. Gibi ortak bir kaynak aracılığıyla senkronizasyonu kontrol etmeniz gerekecektir.


58
Bu geçerli bir çözümdür, ancak bu, kümelenmiş bir ortama sahip olmanın arkasındaki fikri ihlal eder; burada bir düğüm çalışmıyorsa, diğer düğüm başka isteklere hizmet edebilir. Bu geçici çözümde, "planlanmış" profile sahip düğüm çalışmazsa bu arka plan işi çalışmaz
Ahmed Hashem

3
Bunu arşivlemek için Redis'i atomik getve setoperasyonla kullanabiliriz.
Thanh Nguyen Van

Önerinizle ilgili birkaç sorun var: 1. Genellikle bir kümenin her düğümünün aynı yapılandırmaya sahip olmasını istersiniz, bu nedenle bunlar% 100 değiştirilebilir ve paylaştıkları aynı yük altında aynı kaynakları gerektirir. 2. Çözümünüz, "görev" düğümü çöktüğünde manuel müdahale gerektirecektir. 3. Yine de işin gerçekten başarılı bir şekilde yürütüldüğünü garanti etmez, çünkü "görev" düğümü mevcut yürütmeyi işlemeyi bitirmeden önce çöktü ve yeni "görev çalıştırıcısı" ilkinden sonra oluşturuldu, olup olmadığını bilmeden bitmişti ya da bitmemişti.
Moshe Bixenshpaner

1
sadece kümelenmiş ortam fikrini ihlal ediyor, önerdiğiniz yaklaşımla herhangi bir çözüm olamaz. Kullanılabilirliği sağlamak için profil sunucularını bile çoğaltamazsınız, çünkü bu ek maliyet ve gereksiz kaynak israfına neden olur. @Thanh tarafından önerilen çözüm bundan çok daha temiz. Aynı şeyi MUTEX olarak düşünün. Komut dosyasını çalıştıran herhangi bir sunucu, redis gibi bazı dağıtılmış önbellekte geçici bir kilit alacak ve ardından geleneksel kilitleme kavramlarıyla devam edecektir.
anuj pradhan

2

dlock , veritabanı dizinlerini ve kısıtlamalarını kullanarak görevleri yalnızca bir kez çalıştırmak için tasarlanmıştır. Aşağıdakine benzer bir şey yapabilirsiniz.

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

}

Kullanmayla ilgili makaleye bakın .


3
Dlock kullanılıyorsa. Kilidi korumak için DB kullandığımızı varsayalım. Ve kümedeki düğümlerden biri kilitlendikten sonra beklenmedik bir şekilde çöktü, bu durumda bu senaryoda ne olacak? Kilitlenme durumunda mı olacak?
Badman

1

Kilitlemeyi yapmak için bir veritabanı tablosu kullanıyorum . Bir seferde yalnızca bir görev tabloya ekleme yapabilir. Diğeri bir DuplicateKeyException alacaktır. Ekleme ve silme mantığı, @Scheduled ek açıklama etrafındaki bir yönle ele alınır. Spring Boot 2.0 kullanıyorum

@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
Mükemmel çalışacağını düşünüyor musun? Çünkü, eğer bir düğüm kilitlendikten sonra düşerse, diğerleri neden kilit olduğunu anlamayacaktır (sizin durumunuzda, tablodaki işe karşılık gelen satır girişi).
Badman

0

Bunu başarmak için db-scheduler gibi gömülebilir bir zamanlayıcı kullanabilirsiniz . Kalıcı uygulamalara sahiptir ve tek bir düğüm tarafından yürütülmeyi garanti etmek için basit bir iyimser kilitleme mekanizması kullanır.

Kullanım senaryosunun nasıl elde edilebileceğine ilişkin örnek kod:

   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

Yay bağlamı kümelenmemiş olduğundan, dağıtılmış uygulamadaki görevi yönetmek biraz zordur ve durumu senkronize etmek için jgroup'u destekleyen sistemleri kullanmanız ve görevinizin eylemi yürütmek için önceliği almasına izin vermeniz gerekir. Veya jboss ha ortamı gibi kümelenmiş ha singleton hizmetini yönetmek için ejb bağlamını kullanabilirsiniz https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbd Veya kümelenmiş önbellek ve erişim kilidi kaynağını kullanabilirsiniz hizmet ve ilk hizmet arasında kilit al, hizmetinizle iletişim kurmak ve eylemi bir düğümde gerçekleştirmek için kendi jgroup'unuzu gerçekleştirecek veya uygulayacaktır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.