在AWS上(或分布式系统中),Cron是什么?

9
我很惊讶没有找到更多相关信息,但我仍然找不到答案。我们最近转换到AWS,将我们的简单网站移动到一个更强大和可靠的系统上。目前令我困扰的是在分布式系统上管理cron作业,当该cron作业被推送到环境中的每个实例时。
以下是用例:
背景
设置
我们正在运行传统的LAMP堆栈。可能是第一个问题,但这就是我们得到的。
数据库表
table1

 - id int(11)
 - start date
 - interval int(11) (number of seconds)

table2

 - id int(11)
 - table1_id int(11)
 - sent datetime

目标

目标是每天运行一次脚本并检查以下内容:

  1. 当前日期是否超过 table1.start
  2. table1.start < 当前日期
  3. table1.interval > 0
  4. 今天恰好是一个整数间隔的时间(如果间隔为7天[以秒为单位],而今天是第6天,则会失败)
  5. 没有任何条目在 table2 中匹配之前的检查,其中 table2.sent 是今天,table2.table1_id 匹配。

如果所有这些检查都通过,我们将为具有间隔的每个 table1 插入一个条目到 table2。这也意味着我们根据 table2 中的数据发送电子邮件。

问题

基本上,我们有两个查询,由上述块表示。问题在于,在分布式系统上,每个实例将同时运行 cron(或在彼此之间的几毫秒内)。没有“事务”的概念,因此如果一个实例没有机会在其他实例运行第一个查询之前插入到 table2,则每个实例都会发送电子邮件。

解决方案???

我已经进行了相当多的研究,但我想到的唯一潜在解决方案如下:

Cron 实例

设置单个、独立的实例负责运行 cron 作业。虽然这肯定会(就我所看到的)起作用,但对于一个不是非常昂贵且只需要每天最多运行一次的作业来说,这是非常昂贵的。

PHP 调度程序

将 cron 设置为定期运行充当调度程序的 PHP 脚本。这是我们在研究后要走的路线,因为它对于我们有限的时间和金钱来说似乎是最简单的。我遇到的问题是,这似乎只是将并发问题从消耗作业转移到调度作业。您何时安排作业,以便每个实例运行 cron 时不会同时安排多个作业?

这种方法也似乎非常“笨拙”(借用我的朋友最喜欢的词),我必须同意。

事务

尽管我已经进行了相当多的研究,并且并发始终通过数据库上的原子事务解决,但据我所知,在 LAMP 上实现这一点并不容易。但也许我是错的,如果能证明我错了,我会非常高兴。

最后

如果有人可以帮助我解决这个问题,我将非常感激。也许我的 Google 搜索技巧正在变得生疏,但我无法想象自己是唯一遭受这种(可能简单)任务的人。


1
我没有足够的经验来将其转化为一个真正有建设性的答案,但你是否考虑过亚马逊的SWF?既然你已经在AWS上了,那么它可能是cron的可靠替代品。 - Joe Castro
这可能听起来有些过度,但也许你可以看一下Zookeeper。它使用简单、轻量级、健壮,并且会使您协调/同步分布式任务的工作尽可能简单。 - Viccari
值得注意的是,我们正在使用Kohana。我想知道是否有一些级别的锁定可以在数据库查询上执行,以确保事务是原子性和连续性的。 - Ryan
使用保留实例,一个微型实例大概只需要花费你7美元每月的成本吧? - ceejayoz
请将另一个问题添加到您的列表中 - AWS Autoscaling Group EC2实例在cron作业期间关闭 - Yevgeniy Afanasyev
显示剩余2条评论
3个回答

4
我曾经遇到过类似的问题。我也有cron jobs需要在单个主机上每分钟运行一次。
为了解决这个问题,我使用了一个小技巧:利用Amazon自动扩展工具来判断当前主机是否是最后一个被实例化的自动扩展组中的主机。这当然前提是你已经使用了自动扩展,并且主机名包含实例ID。
#!/usr/bin/env ruby

AWS_AUTO_SCALING_HOME='/opt/AutoScaling'
AWS_AUTO_SCALING_URL='https://autoscaling.eu-west-1.amazonaws.com'
MY_GROUP = 'Production'

@cmd_out = `bash -c 'AWS_AUTO_SCALING_HOME=#{ AWS_AUTO_SCALING_HOME }\
  AWS_AUTO_SCALING_URL=#{ AWS_AUTO_SCALING_URL }\
  #{ AWS_AUTO_SCALING_HOME }/bin/as-describe-auto-scaling-instances'`

raise "Output empty, should not happen!" if @cmd_out.empty?
@lines = @cmd_out.split(/\r?\n/)
@last = @lines.select {|l| l.match MY_GROUP }.reverse.
  detect { |l| l =~ /^INSTANCE\s+\S+\s+\S+\s+\S+\s+InService\s+HEALTHY/ }
raise "No suitable host in autoscaling group!" unless @last
@last_host = @last.match(/^INSTANCE\s+(\S+)/)[1]
@hostname = `hostname`
if @hostname.index(@last_host)
  puts "It's me!"
  exit(0)
else
  puts "Someone else will do it!"
  exit(1)
end

我将其保存为/usr/bin/lastonly,然后在cron作业中执行以下操作:

lastonly && do_my_stuff

显然它并不完美,但对我来说它很有效,并且简单易懂!


3
看看Gearman项目的http://www.gearman.org。基本架构是你会有一台作业服务器,所有其他机器都成为服务器的客户端。
您可以在作业服务器上设置crontab以向通过Gearman连接的所有客户端发送要执行的命令。然后,您可以使用PHP来切片和切块cron作业,并深入Map/Reduce。
这里有一个很好的教程介绍概念和工作原理:http://www.lornajane.net/posts/2011/Using-Gearman-from-PHP 不要立即对使用Gearman等东西感到沮丧。分布式cron系统可能很复杂,但是一旦你理解了它,你就没问题了。
顺便说一句,我们在Amazon的EC2上的Gearman工人农场中每分钟处理数千个cron脚本。我们非常喜欢它。

嗯,我喜欢单台机器作为生产者的想法,无论是使用Gearman还是不使用。我认为单台机器将队列/消息系统提供给其他机器消费应该很好。 - ankush981

0

您可以使用队列使任务仅运行一次。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接