AWS Elastic Beanstalk,运行cronjob


89

我想知道是否有一种方法可以设置cronjob /任务以每分钟执行一次。当前,我的任何实例都应该能够运行此任务。

这是我尝试在配置文件中执行的操作,但未成功:

container_commands:
  01cronjobs:
    command: echo "*/1 * * * * root php /etc/httpd/myscript.php"

我不确定这是否是正确的方法

有任何想法吗?


1
命令正确吗?我的意思是...它可能是:命令:echo“ * / 1 * * * * root php /etc/httpd/myscript.php”> /etc/cron.d/something无论哪种方式,我建议您使用leader_only标志,否则所有机器将立即启动该cron作业
aldrinleal 2012

是! 绝对使用leader_only标志,我将尝试更改命令。
Onema 2013年

Answers:


96

这是我向Elastic Beanstalk添加cron作业的方式:

如果尚不存在,请在应用程序的根目录下创建一个名为.ebextensions的文件夹。然后在.ebextensions文件夹中创建一个配置文件。我将使用example.config进行说明。然后将其添加到example.config

container_commands:
  01_some_cron_job:
    command: "cat .ebextensions/some_cron_job.txt > /etc/cron.d/some_cron_job && chmod 644 /etc/cron.d/some_cron_job"
    leader_only: true

这是Elastic Beanstalk的YAML配置文件。确保将其复制到文本编辑器中时,文本编辑器使用空格而不是制表符。否则,将其推送到EB时会收到YAML错误。

因此,这就是创建一个名为01_some_cron_job的命令。命令按字母顺序运行,因此01确保它作为第一个命令运行。

然后,该命令获取名为some_cron_job.txt的文件的内容,并将其添加到/etc/cron.d中名为some_cron_job的文件中。

然后,该命令更改/etc/cron.d/some_cron_job文件的权限。

Leader_only键可确保命令仅在被视为领导者的ec2实例上运行。而不是在可能已运行的每个ec2实例上运行。

然后在.ebextensions文件夹中创建一个名为some_cron_job.txt的文件。您将把cron作业放置在此文件中。

因此,例如:

# The newline at the end of this file is extremely important.  Cron won't run without it.
* * * * * root /usr/bin/php some-php-script-here > /dev/null

因此,此cron作业将以root用户身份每天每天的每一分钟运行,并将输出丢弃到/ dev / null。/ usr / bin / php是php的路径。然后将some-php-script-here替换为您的php文件的路径。显然,这是假设您的cron作业需要运行PHP文件。

另外,请确保some_cron_job.txt文件在文件末尾有换行符,就像注释中所说的那样。否则,cron将无法运行。

更新: Elastic Beanstalk扩展您的实例时,此解决方案存在问题。例如,假设您有一个实例正在运行cron作业。您的流量增加了,因此Elastic Beanstalk可将您最多扩展到两个实例。Leader_only将确保您在两个实例之间仅运行一个cron作业。您的流量减少,Elastic Beanstalk将您缩减到一个实例。但是,Elastic Beanstalk并没有终止第二个实例,而是终止了作为领导者的第一个实例。您现在没有任何cron作业正在运行,因为它们仅在终止的第一个实例上运行。 请参阅下面的评论。

更新2: 只需从下面的注释中阐明这一点:AWS现在可以防止实例自动终止。只需在您的领导者实例上启用它,就可以了。–NicolásArévalo'16 -10-28在9:23


12
我一直在使用您的建议一段时间,最近遇到了一个问题,领导者以某种方式进行了切换,导致多个实例运行了cron。为了解决该问题,我更改01_some_cron_job02_some_cron_job并添加01_remove_cron_jobs了以下内容:command: "rm /etc/cron.d/cron_jobs || exit 0"。这样,在每次部署之后,仅领导者将拥有该cron_jobs文件。如果领导者发生变化,您可以重新部署,并且将重新固定重头戏。
Willem Renzema

4
我建议不要依靠leader_only财产。它仅在部署期间使用,并且如果您按比例缩小或“ leader”实例失败,则势必会
遇到

2
不要这样 太不可靠了。我使它起作用的唯一方法是运行一个微实例,然后使用CURL从那里运行cron作业。这样可以保证只有一个实例可以运行它,并且安装了cron的领导者也不会终止。
本辛克莱

1
我试图用一个小的ruby脚本来解决这个问题,您可以在这里找到它:github.com/SocialbitGmbH/AWSBeanstalkLeaderManager
Thomas Kekeisen 2015年

8
AWS现在可以防止实例自动终止。只要在您的领导者实例上启用它,您就可以进行了。
尼古拉斯·阿雷瓦洛

58

这是目前(2015年以上)的官方方法。请先尝试此方法,它是目前可用的最简单方法,也是最可靠的方法。

根据目前的文档,人们能够在其所谓的工作层运行定期任务

引用文档:

在运行预定义配置且解决方案堆栈的容器名称中包含“ v1.2.0”的环境中,AWS Elastic Beanstalk支持工作环境层的定期任务。您必须创建一个新环境。

关于cron.yaml的部分也很有趣:

要调用定期任务,您的应用程序源包必须在根级别包含cron.yaml文件。该文件必须包含有关您要计划的定期任务的信息。使用标准crontab语法指定此信息。

更新:我们能够完成这项工作。以下是我们经验(Node.js平台)中的一些重要陷阱:

  • 使用cron.yaml文件时,请确保您具有最新的awsebcli,因为较旧的版本将无法正常工作。
  • 创建新的环境(至少在我们的情况下如此)也很重要,而不仅仅是克隆旧的环境。
  • 如果要确保EC2 Worker Tier实例支持CRON,则将其ssh(eb ssh)并运行cat /var/log/aws-sqsd/default.log。它应报告为aws-sqsd 2.0 (2015-02-18)。如果您没有2.0版本,则在创建环境时出了点问题,您需要如上所述创建新的环境。

2
关于cron.yaml,有一个很棒的博客文章:在Amazon Web Services(AWS)上运行cron作业Elastic Beanstalk —中
jwako

5
谢谢您这个菜鸟问题,我需要我的cron每小时两次检查我的Web应用程序数据库中是否有即将发生的日历事件,并在发出通知时发送提醒电子邮件。这里最好的设置是什么,我应该让cron.yaml URL指向Web应用程序上的路由吗?还是应该让我的worker env应用程序访问数据库?太少了!
基督教徒

5
@christian我们这样做的方式是,我们有一个相同的应用程序在两个不同的环境中运行(因此不需要特殊的配置),即工作服务器和公共Web服务器。通过设置我们的应用程序寻找的ENV变量,工作环境具有一些特殊的路由。这样,您可以在cron.yaml中设置专用于工作者的特殊路由,同时拥有与普通应用程序共享的共享代码库的功能。您的工作程序可以轻松访问与Web服务器相同的资源:数据库,模型等
。– xaralis

1
@JaquelinePassos v1.2.0是解决方案堆栈版本。它应该让您选择在创建新环境时要创建哪个版本的解决方案堆栈。任何比v1.2.0都新的版本都可以。关于URL,它应该是您的应用程序侦听的URL,而不是文件路径。无法运行Django管理命令,仅执行HTTP请求。
xaralis '16

4
我不清楚的一件事是,是否有一种方法可以避免仅为了通过cron.yaml运行cron作业而分配额外的EC2计算机。理想情况下,它将与服务HTTP请求的机器(即Web层)在同一台机器上运行。
Wenzel Jakob

31

关于jamieb的响应,正如alrdinleal提到的,您可以使用'leader_only'属性来确保只有一个EC2实例运行cron作业。

引用摘自http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/customize-containers-ec2.html

您可以使用leader_only。选择一个实例作为Auto Scaling组的领导者。如果leader_only值设置为true,则该命令仅在标记为leader的实例上运行。

我正在尝试在我的eb上实现类似的功能,因此如果解决了该问题,则会更新我的帖子。

更新:

好的,我现在可以使用以下eb配置工作cronjobs了:

files:
  "/tmp/cronjob" :
    mode: "000777"
    owner: ec2-user
    group: ec2-user
    content: |
      # clear expired baskets
      */10 * * * * /usr/bin/wget -o /dev/null http://blah.elasticbeanstalk.com/basket/purge > $HOME/basket_purge.log 2>&1
      # clean up files created by above cronjob
      30 23 * * * rm $HOME/purge*
    encoding: plain 
container_commands:
  purge_basket: 
    command: crontab /tmp/cronjob
    leader_only: true
commands:
  delete_cronjob_file: 
    command: rm /tmp/cronjob

本质上,我使用cronjobs创建一个临时文件,然后将crontab设置为从临时文件中读取,然后再删除该临时文件。希望这可以帮助。


3
您如何确保运行此crontab的实例不会被自动缩放终止?默认情况下,它终止最旧的实例。
塞巴斯蒂安

1
这是我尚未能够解决的问题。令我震惊的是亚马逊功能的一个缺陷:当当前领导者被EB终止时,leader_only命令不会应用于新领导者。如果您想出点什么,请分享!
beterthanlife

7
因此,我(最后)发现了如何防止领导者因自动缩放而终止-自定义自动缩放终止策略。见 docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/...
beterthanlife

1
@Nate您可能现在已经弄清楚了,但是根据我对这些命令运行的顺序的了解,“命令”在“ container_commands”之前运行,因此您可以创建文件,然后将其删除,然后尝试运行crontab 。
clearf 2014年

1
@Sebastien为了保留最旧的实例,这是我要做的:1-将实例的终止保护更改为ENBABLE。2-转到“自动缩放比例组”,找到您的EBS环境ID,单击“编辑”,然后将“终止策略”更改为“ NewestInstance”
Ronaldo Bahia

12

如上所述,建立任何crontab配置的基本缺陷是仅在部署时发生。随着群集自动扩展然后缩减,最好同时关闭第一台服务器。此外,不会进行故障转移,这对我来说至关重要。

我进行了一些研究,然后与我们的AWS客户专家进行了交谈,以提出想法并验证我提出的解决方案。您可以使用OpsWorks完成此操作,尽管有点像用房子杀死苍蝇。也可以将Data Pipeline与Task Runner一起使用,但这在其可以执行的脚本中功能有限,我需要能够运行PHP脚本,并可以访问整个代码库。您也可以在ElasticBeanstalk群集之外专用EC2实例,但是这样就不会再进行故障转移了。

所以这是我想出的,这显然是非常规的(正如AWS代表所评论的那样),可以被认为是一种hack,但是它可以正常工作并且具有故障转移功能。我选择了使用SDK的编码解决方案,该解决方案将在PHP中显示,尽管您可以使用任何喜欢的语言执行相同的方法。

// contains the values for variables used (key, secret, env)
require_once('cron_config.inc'); 

// Load the AWS PHP SDK to connection to ElasticBeanstalk
use Aws\ElasticBeanstalk\ElasticBeanstalkClient;

$client = ElasticBeanstalkClient::factory(array(
    'key' => AWS_KEY,
    'secret' => AWS_SECRET,
    'profile' => 'your_profile',
    'region'  => 'us-east-1'
));

$result = $client->describeEnvironmentResources(array(
    'EnvironmentName' => AWS_ENV
));

if (php_uname('n') != $result['EnvironmentResources']['Instances'][0]['Id']) {
    die("Not the primary EC2 instance\n");
}

因此,逐步了解它及其操作方式...您像通常在每个EC2实例上一样,从crontab调用脚本。每个脚本在开始时都包含此脚本(或在我使用时,每个脚本都包含一个文件),该脚本建立一个ElasticBeanstalk对象并检索所有实例的列表。它仅使用列表中的第一台服务器,并检查它是否与自身匹配,如果匹配,它将继续,否则它将死掉并关闭。我已经检查过,返回的列表似乎是一致的,从技术上讲,它只需要保持一分钟左右的时间,因为每个实例都执行调度的cron。如果确实发生变化,那就没关系了,因为它再次只与那个小窗口有关。

无论如何,这都不是一件好事,但它满足了我们的特定需求-这样做不是为了增加额外服务的成本或不必拥有专用的EC2实例,并且在发生任何故障时都可以进行故障转移。我们的cron脚本运行维护脚本,这些脚本被放入SQS中,集群中的每个服务器都可以执行。如果满足您的需求,至少可以给您一个替代选择。

-戴维


我发现php_uname('n')返回私有DNS名称(例如ip-172.24.55.66),这不是您要查找的实例ID。我没有使用php_uname(),而是使用了它: $instanceId = file_get_contents("http://instance-data/latest/meta-data/instance-id"); 然后只使用$ instanceId var进行比较。
Valorum 2015年

1
是否可以保证Instances数组在每个Describe调用中呈现相同的顺序?我建议将每个条目的['Id']字段提取到数组中,并在PHP中对其进行排序,然后再检查是否第一个排序的条目是当前的instanceId。
加百利

基于此答案,我提出了以下解决方案:stackoverflow.com/questions/14077095/…-非常相似,但是没有两次执行的机会。
TheStoryCoder

11

我与一个AWS支持代理进行了交谈,这就是我们如何使其为我工作的方式。2015解决方案:

使用your_file_name.config在.ebextensions目录中创建一个文件。在配置文件中输入:

文件:
  “ /etc/cron.d/cron_example”:
    模式:“ 000644”
    所有者:root
    组:根
    内容:
      * * * * * root /usr/local/bin/cron_example.sh

  “ /usr/local/bin/cron_example.sh”:
    模式:“ 000755”
    所有者:root
    组:根
    内容:
      #!/ bin / bash

      /usr/local/bin/test_cron.sh || 出口
      回显“ Cron运行于”`date` >> /tmp/cron_example.log
      #现在执行只能在1个实例上运行的任务...

  “ /usr/local/bin/test_cron.sh”:
    模式:“ 000755”
    所有者:root
    组:根
    内容:
      #!/ bin / bash

      METADATA = / opt / aws / bin / ec2-metadata
      INSTANCE_ID =`$ METADATA -i | awk'{print $ 2}'`
      REGION =`$ METADATA -z | awk'{print substr($ 2,0,length($ 2)-1)}'``

      #查找我们的Auto Scaling组名称。
      ASG =`aws ec2 describe-tags --filters“名称=资源ID,值= $ INSTANCE_ID” \
        --region $ REGION-输出文本| awk'/ aws:autoscaling:groupName / {print $ 5}'`

      #查找组中的第一个实例
      FIRST =`aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names $ ASG \
        --region $ REGION-输出文本| awk'/ InService $ / {print $ 4}'| 排序 -1

      #测试它们是否相同。
      [“ $ FIRST” =“ $ INSTANCE_ID”]

命令:
  rm_old_cron:
    命令:“ rm * .bak”
    cwd:“ / etc / cron.d”
    ignoreErrors:正确

该解决方案有两个缺点:

  1. 在后续部署中,Beanstalk将现有的cron脚本重命名为.bak,但cron仍将运行它。您的Cron现在在同一台计算机上执行两次。
  2. 如果您的环境扩大规模,您将获得多个实例,所有实例都运行您的cron脚本。这意味着您的邮件会重复发送,或者数据库存档已重复

解决方法:

  1. 确保任何创建cron的.ebextensions脚本也将在后续部署中删除.bak文件。
  2. 有一个执行以下操作的帮助程序脚本:-从元数据中获取当前的实例ID –从EC2标签中获取当前的Auto Scaling组名称–获取该组中的EC2实例列表,按字母顺序排序。-从该列表中获取第一个实例。-将步骤1中的实例ID与步骤4中的第一个实例ID进行比较。然后,您的cron脚本可以使用此帮助程序脚本来确定是否应执行。

警告:

  • 用于Beanstalk实例的IAM角色需要ec2:DescribeTags和autoscaling:DescribeAutoScalingGroups权限
  • 从中选择的实例是通过Auto Scaling显示为InService的实例。这并不一定意味着它们已完全启动并可以运行您的cron。

如果使用默认的beantalk角色,则不必设置IAM角色。


7

如果您使用的是Rails,则可以使用whenever-elasticbeanstalk gem。它允许您在所有实例或仅一个实例上运行cron作业。它每分钟检查一次,以确保只有一个“ leader”实例,如果没有,则会自动将一台服务器升级为“ leader”。之所以需要这样做,是因为Elastic Beanstalk在部署期间仅具有领导者的概念,并且在扩展时可以随时关闭任何实例。

更新 我切换为使用AWS OpsWorks,并且不再维护该gem。如果您需要的功能比Elastic Beanstalk基础知识所提供的更多,我强烈建议您切换到OpsWorks。


您介意告诉我们如何使用OpsWorks解决该问题吗?您是否正在运行cron-jobs的自定义图层?
Tommie 2014年

是的,我有一个仅在一台服务器上运行的admin / cron层。我设置了一个自定义食谱,其中包含我所有的计划工作。AWS在docs.aws.amazon.com/opsworks/latest/userguide/…上提供了指南。
2014年

@dignoe如果您使用OpsWorks为运行cron作业分配一台服务器,而使用Elastic Beanstalk分配同一服务器,则我可以在具有一台服务器的环境中运行cron作业。即使使用Load Balancer,max和min实例都设置为1,以至少始终保留一个服务器实例。
Jose Nobile

6

您真的不想在Elastic Beanstalk上运行cron作业。由于您将有多个应用程序实例,因此这可能会导致竞争状况和其他奇怪的问题。实际上,我最近在博客上对此发表了看法(页面下方的第4或第5条提示)。简短版:根据应用程序,使用作业队列(如SQS)或第三方解决方案(如iron.io)


SQS不保证代码只能运行一次。我喜欢iron.io网站,我将对其进行检查。
内森H

同样在您的博客文章中,您建议在RDS上使用InnoDB。我使用RDS上的表存储任务,并使用InnoDB的“ SELECT ... FOR UPDATE”功能来确保只有一台服务器运行这些任务。您的应用如何在没有cron工作或用户交互的情况下联系SQS?
詹姆斯·阿尔代

1
@JamesAlday这个问题很老了。自从我写下以上评论以来,AWS通过选择其中一台正在运行的服务器作为主机,引入了一种优雅的方式来处理Elastic Beanstalk上的cron作业。话虽如此,听起来您好像在滥用cron + MySQL作为作业队列。在提供具体建议之前,我需要对您的应用程序有很多了解。
jamieb 2014年

我有一个通过cron运行的脚本,该脚本检查表中要运行的作业。使用事务可防止多个服务器运行同一作业。我已经研究了SQS,但是您需要一个主服务器来运行所有脚本而不是分发它,并且您仍然需要编写逻辑以确保您不会多次运行同一脚本。但是我仍然对如何在没有用户交互或cron的情况下运行任务感到困惑-是什么触发您的应用程序在队列中运行任务?
James Alday

4

2017:如果您使用的是Laravel5 +

您只需要2分钟即可配置它:

  • 创建一个工人层
  • 安装laravel-aws-worker

    composer require dusterio/laravel-aws-worker

  • 将cron.yaml添加到根文件夹:

将cron.yaml添加到应用程序的根文件夹中(这可以是您的存储库的一部分,或者您可以在部署到EB之前添加此文件-重要的是该文件在部署时就存在):

version: 1
cron:
 - name: "schedule"
   url: "/worker/schedule"
   schedule: "* * * * *"

而已!

您的所有任务App\Console\Kernel现在都将执行

详细的说明和解释:https : //github.com/dusterio/laravel-aws-worker

如何在Laravel内部编写任务:https://laravel.com/docs/5.4/scheduling


3

使用files而不是的更具可读性的解决方案container_commands

文件:
  “ /etc/cron.d/my_cron”:
    模式:“ 000644”
    所有者:root
    组:根
    内容:
      #覆盖默认电子邮件地址
      MAILTO =“ example@gmail.com”
      #每五分钟运行一次Symfony命令(以ec2用户身份)
      * / 10 * * * * ec2-user / usr / bin / php / var / app / current / app / console做:某事
    编码:普通
命令:
  #删除Elastic Beanstalk创建的备份文件
  clear_cron_backup:
    命令:rm -f /etc/cron.d/watson.bak

请注意,该格式与通常的crontab格式不同,因为它指定用户以其身份运行命令。


这里的一个问题是,默认情况下,Elastic Beanstalk EC2实例未设置SMTP服务,因此此处的MAILTO选项可能不起作用。
贾斯汀·芬克尔斯坦

3

我2018年的贡献额的1美分

这是正确的方法(使用django/pythondjango_crontab应用):

.ebextensions文件夹内创建一个像这样的文件98_cron.config

files:
  "/tmp/98_create_cron.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/bin/sh
      cd /
      sudo /opt/python/run/venv/bin/python /opt/python/current/app/manage.py crontab remove > /home/ec2-user/remove11.txt
      sudo /opt/python/run/venv/bin/python /opt/python/current/app/manage.py crontab add > /home/ec2-user/add11.txt 

container_commands:
    98crontab:
        command: "mv /tmp/98_create_cron.sh /opt/elasticbeanstalk/hooks/appdeploy/post && chmod 774 /opt/elasticbeanstalk/hooks/appdeploy/post/98_create_cron.sh"
        leader_only: true

它需要container_commands代替commands



2

来自Amazon的最新示例是最简单,最有效的(定期任务):

https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features-managing-env-tiers.html

您可以在其中创建一个单独的工作人员层来执行您的任何cron作业。创建cron.yaml文件并将其放在您的根文件夹中。我遇到的一个问题(在cron似乎未执行之后)是发现我的CodePipeline没有权限执行动态修改。基于此,在IAM->角色-> yourpipeline下添加FullDynamoDB访问并重新部署(弹性beantalk)后,它可以完美运行。



1

因此,我们一直为此苦苦挣扎,经过与AWS代表的讨论之后,我终于提出了我认为是最佳的解决方案。

将工作层与cron.yaml一起使用绝对是最简单的解决方法。但是,文档尚不清楚的是,这会将作业置于您实际用于运行作业的SQS队列的末尾。如果您的Cron作业对时间很敏感(有很多时间),那么这是不可接受的,因为这取决于队列的大小。一种选择是使用完全独立的环境来运行cron作业,但是我认为这太过分了。

其他一些选项(例如检查您是否是列表中的第一个实例)也不理想。如果当前的第一个实例正在关闭中该怎么办?

实例保护也可能带来问题-如果该实例被锁定/冻结该怎么办?

重要的是要了解AWS本身如何管理cron.yaml功能。有一个SQS守护程序,它使用Dynamo表处理“领导者选举”。它会频繁地写入此表,并且如果当前的领导者短时间内没有写入,则下一个实例将接替该领导者。这是守护程序决定将哪个作业激发到SQS队列中的方式。

我们可以重新利用现有功能,而不必尝试重写我们自己的功能。您可以在此处查看完整的解决方案:https : //gist.github.com/dorner/4517fe2b8c79ccb3971084ec28267f27

那是在Ruby中,但是您可以轻松地使其适应具有AWS开发工具包的任何其他语言。本质上,它会检查当前的领导者,然后检查状态以确保其处于良好状态。它将一直循环直到当前的领导者处于良好状态,并且如果当前实例是领导者,则执行作业。



0

如果要通过cron运行php文件,并且设置了任何NAT实例,那么我还有另一种解决方案,则可以将cronjob放在NAT实例上,并通过wget运行php文件。


0

这是一个修复程序,以防您要在PHP中执行此操作。您只需要在.ebextensions文件夹中的cronjob.config即可使其工作。

files:
  "/etc/cron.d/my_cron":
    mode: "000644"
    owner: root
    group: root
    content: |
        empty stuff
    encoding: plain
commands:
  01_clear_cron_backup:
    command: "rm -f /etc/cron.d/*.bak"
  02_remove_content:
    command: "sudo sed -i 's/empty stuff//g' /etc/cron.d/my_cron"
container_commands:
  adding_cron:
    command: "echo '* * * * * ec2-user . /opt/elasticbeanstalk/support/envvars && /usr/bin/php /var/app/current/index.php cron sendemail > /tmp/sendemail.log 2>&1' > /etc/cron.d/my_cron"
    leader_only: true

envvars获取文件的环境变量。您可以如上所述在tmp / sendemail.log上调试输出。

希望这对某人有所帮助,因为它一定对我们有所帮助!


0

根据user1599237的回答原则其中让cron作业在所有实例上运行,然后在作业开始时确定是否应允许它们运行,我提出了另一种解决方案。

我使用的是已经从所有实例连接到的MySQL数据库,而不是查看正在运行的实例(必须存储您的AWS密钥和机密)。

它没有缺点,只有积极的一面:

  • 没有额外的实例或费用
  • 坚如磐石的解决方案-不可能双重执行
  • 可扩展-在您的实例放大和缩小时自动工作
  • 故障转移-在实例发生故障时自动运行

或者,您也可以使用公用共享文件系统(例如通过NFS协议的AWS EFS)代替数据库。

以下解决方案是在PHP框架Yii中创建的,但是您可以轻松地使其适应其他框架和语言。另外,异常处理程序Yii::$app->system是我自己的模块。用您正在使用的任何东西替换它。

/**
 * Obtain an exclusive lock to ensure only one instance or worker executes a job
 *
 * Examples:
 *
 * `php /var/app/current/yii process/lock 60 empty-trash php /var/app/current/yii maintenance/empty-trash`
 * `php /var/app/current/yii process/lock 60 empty-trash php /var/app/current/yii maintenance/empty-trash StdOUT./test.log`
 * `php /var/app/current/yii process/lock 60 "empty trash" php /var/app/current/yii maintenance/empty-trash StdOUT./test.log StdERR.ditto`
 * `php /var/app/current/yii process/lock 60 "empty trash" php /var/app/current/yii maintenance/empty-trash StdOUT./output.log StdERR./error.log`
 *
 * Arguments are understood as follows:
 * - First: Duration of the lock in minutes
 * - Second: Job name (surround with quotes if it contains spaces)
 * - The rest: Command to execute. Instead of writing `>` and `2>` for redirecting output you need to write `StdOUT` and `StdERR` respectively. To redirect stderr to stdout write `StdERR.ditto`.
 *
 * Command will be executed in the background. If determined that it should not be executed the script will terminate silently.
 */
public function actionLock() {
    $argsAll = $args = func_get_args();
    if (!is_numeric($args[0])) {
        \Yii::$app->system->error('Duration for obtaining process lock is not numeric.', ['Args' => $argsAll]);
    }
    if (!$args[1]) {
        \Yii::$app->system->error('Job name for obtaining process lock is missing.', ['Args' => $argsAll]);
    }

    $durationMins = $args[0];
    $jobName = $args[1];
    $instanceID = null;
    unset($args[0], $args[1]);

    $command = trim(implode(' ', $args));
    if (!$command) {
        \Yii::$app->system->error('Command to execute after obtaining process lock is missing.', ['Args' => $argsAll]);
    }

    // If using AWS Elastic Beanstalk retrieve the instance ID
    if (file_exists('/etc/elasticbeanstalk/.aws-eb-system-initialized')) {
        if ($awsEb = file_get_contents('/etc/elasticbeanstalk/.aws-eb-system-initialized')) {
            $awsEb = json_decode($awsEb);
            if (is_object($awsEb) && $awsEb->instance_id) {
                $instanceID = $awsEb->instance_id;
            }
        }
    }

    // Obtain lock
    $updateColumns = false;  //do nothing if record already exists
    $affectedRows = \Yii::$app->db->createCommand()->upsert('system_job_locks', [
        'job_name' => $jobName,
        'locked' => gmdate('Y-m-d H:i:s'),
        'duration' => $durationMins,
        'source' => $instanceID,
    ], $updateColumns)->execute();
    // The SQL generated: INSERT INTO system_job_locks (job_name, locked, duration, source) VALUES ('some-name', '2019-04-22 17:24:39', 60, 'i-HmkDAZ9S5G5G') ON DUPLICATE KEY UPDATE job_name = job_name

    if ($affectedRows == 0) {
        // record already exists, check if lock has expired
        $affectedRows = \Yii::$app->db->createCommand()->update('system_job_locks', [
                'locked' => gmdate('Y-m-d H:i:s'),
                'duration' => $durationMins,
                'source' => $instanceID,
            ],
            'job_name = :jobName AND DATE_ADD(locked, INTERVAL duration MINUTE) < NOW()', ['jobName' => $jobName]
        )->execute();
        // The SQL generated: UPDATE system_job_locks SET locked = '2019-04-22 17:24:39', duration = 60, source = 'i-HmkDAZ9S5G5G' WHERE job_name = 'clean-trash' AND DATE_ADD(locked, INTERVAL duration MINUTE) < NOW()

        if ($affectedRows == 0) {
            // We could not obtain a lock (since another process already has it) so do not execute the command
            exit;
        }
    }

    // Handle redirection of stdout and stderr
    $command = str_replace('StdOUT', '>', $command);
    $command = str_replace('StdERR.ditto', '2>&1', $command);
    $command = str_replace('StdERR', '2>', $command);

    // Execute the command as a background process so we can exit the current process
    $command .= ' &';

    $output = []; $exitcode = null;
    exec($command, $output, $exitcode);
    exit($exitcode);
}

这是我正在使用的数据库架构:

CREATE TABLE `system_job_locks` (
    `job_name` VARCHAR(50) NOT NULL,
    `locked` DATETIME NOT NULL COMMENT 'UTC',
    `duration` SMALLINT(5) UNSIGNED NOT NULL COMMENT 'Minutes',
    `source` VARCHAR(255) NULL DEFAULT NULL,
    PRIMARY KEY (`job_name`)
)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.