Spring Scheduled Task ทำงานในสภาพแวดล้อมคลัสเตอร์


98

ฉันกำลังเขียนแอปพลิเคชันที่มีงาน 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 หรือไม่ ช่วยบอกด้วย.
Vikas Sharma

ฉันลองใช้ MsSQL และ Spring boot JPA และฉันใช้ลิควิดเบสสคริปต์ในส่วนของ SQL .. ใช้ได้ดีขอบคุณ
sheetal

มันทำงานได้ดีแน่นอน อย่างไรก็ตามฉันได้พบกับกรณีที่ซับซ้อนเล็กน้อยที่นี่โปรดดู ขอบคุณ !!! stackoverflow.com/questions/57691205/…
Dayton Wang


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 หรือไม่ มันเป็นวิธีการที่กำหนดเองหรือเป็นยูทิลิตี้ทั่วไปของ 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 เป็นต้น


58
นี่เป็นวิธีแก้ปัญหาที่ถูกต้อง แต่จะเป็นการละเมิดแนวคิดที่อยู่เบื้องหลังการมีสภาพแวดล้อมแบบคลัสเตอร์ซึ่งหากโหนดไม่ทำงานโหนดอื่นสามารถตอบสนองคำขออื่น ๆ ได้ ในวิธีแก้ปัญหานี้หากโหนดที่มีโปรไฟล์ "กำหนดเวลาไว้" หยุดทำงานงานพื้นหลังนี้จะไม่ทำงาน
Ahmed Hashem

3
ฉันคิดว่าเราสามารถใช้ Redis กับอะตอมgetและsetการดำเนินการเพื่อเก็บสิ่งนั้นได้
Thanh Nguyen Van

ข้อเสนอแนะของคุณมีปัญหาหลายประการ: 1. โดยทั่วไปคุณต้องการให้แต่ละโหนดของคลัสเตอร์มีการกำหนดค่าเหมือนกันดังนั้นจึงสามารถใช้แทนกันได้ 100% และต้องใช้ทรัพยากรเดียวกันภายใต้ภาระเดียวกันที่แชร์ 2. โซลูชันของคุณจะต้องมีการแทรกแซงด้วยตนเองเมื่อโหนด "งาน" หยุดทำงาน 3. จะยังไม่รับประกันว่างานจะทำงานสำเร็จจริงเพราะโหนด "งาน" หยุดทำงานก่อนที่จะเสร็จสิ้นการประมวลผลการดำเนินการปัจจุบันและ "ตัววิ่งงาน" ใหม่ถูกสร้างขึ้นหลังจากที่งานแรกลงไปโดยไม่ทราบว่า มันเสร็จหรือไม่
Moshe Bixenshpaner

1
เพียงแค่ละเมิดแนวคิดของสภาพแวดล้อมแบบคลัสเตอร์ไม่มีวิธีแก้ปัญหาใด ๆ กับแนวทางที่คุณแนะนำ คุณไม่สามารถทำซ้ำได้แม้แต่เซิร์ฟเวอร์โปรไฟล์เพื่อให้แน่ใจว่ามีความพร้อมใช้งานเนื่องจากจะทำให้เสียค่าใช้จ่ายเพิ่มเติมและสิ้นเปลืองทรัพยากรโดยไม่จำเป็นเช่นกัน วิธีแก้ปัญหาที่ @Thanh แนะนำจะสะอาดกว่านี้มาก คิดเช่นเดียวกับ MUTEX เซิร์ฟเวอร์ใด ๆ ที่เรียกใช้สคริปต์จะได้รับการล็อกชั่วคราวในแคชแบบกระจายเช่น redis จากนั้นดำเนินการตามแนวคิดของการล็อกแบบเดิม
anuj พรัด

2

dlockถูกออกแบบมาเพื่อรันงานเพียงครั้งเดียวโดยใช้ดัชนีฐานข้อมูลและข้อ จำกัด คุณสามารถทำสิ่งต่างๆดังต่อไปนี้

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

}

ดูบทความเกี่ยวกับการใช้งาน


3
หากใช้ dlock สมมติว่าเรากำลังใช้ DB เพื่อรักษาการล็อก และหนึ่งในโหนดในคลัสเตอร์ล่มโดยไม่คาดคิดหลังจากทำการล็อคแล้วจะเกิดอะไรขึ้นในสถานการณ์นี้? จะอยู่ในสถานะชะงักงันหรือไม่?
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-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 เพื่อจัดการบริการ ha singleton แบบคลัสเตอร์เช่นสภาพแวดล้อม jboss ha https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbd หรือคุณสามารถใช้แคชคลัสเตอร์และเข้าถึงทรัพยากรการล็อก ระหว่างบริการและบริการแรกใช้การล็อคจะเป็นการดำเนินการหรือใช้ jgroup ของคุณเพื่อสื่อสารบริการของคุณและดำเนินการหนึ่งโหนด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.