为了降低 AWS 中未使用资源的成本(加起来速度比您想象的要快),我们每晚在非工作时间关闭非生产环境中的服务器,并在早上将它们恢复。整个过程由单个 lambda 函数处理,该函数由 CloudWatch 事件规则中的一些 cron 触发器启动。

我们将环境设置为每个工作日晚上 7 点到早上 7 点之间休眠,因此它们会及时为早上 7 点 30 分进入的第一批开发人员提供支持。他们也整个周末都在睡觉,因为我们在周六或周日不进行任何开发。事情完美无缺!也就是说,直到夏令时到来。

问题

对于那些可能不熟悉夏令时 (DST) 的人来说,这是在春季将时钟拨快一小时,在秋季拨快一小时以更好地利用白天时间的做法。不幸的是,由于 AWS 中的 cron 触发器是在 UTC 时间设置的,因此 DST 时间的更改导致我们的启动功能延迟了一个小时。

/* Original CRON triggers when not in DST */
0 13 ? * MON-FRI * // spin-up at 13:00 UTC (7am CST)
0 1 ? * TUE-SAT * // spin-down at 1:00 UTC (7pm CST)

进入全屏模式 退出全屏模式

对 cron 事件如何工作感到好奇? AWS 中的 Cron 表达式由 6 个插槽组成。从左到右:

  • 分钟 (0-59)
  • 小时 (0-23)
  • 日期 (0-31)
  • 月(1-12 或 JAN-DEC)
  • 星期几(1-7 或 SUN-SAT)
  • 年份 (1970-2199)

星号可以作为通配符出现在任何插槽中。

我调整了我们的 cron 事件以考虑时间变化,如下所示:

/* New CRON triggers during DST */
0 12 ? * MON-FRI * // spin-up at 12:00 GMT (7am CST)
0 0 ? * TUE-SAT * // spin-down at 0:00 GMT (7pm CST)

进入全屏模式 退出全屏模式

这会奏效,但这是一个短期修复。我想要一个更长期的解决方案,这样我们就不会在 11 月时间变回时再次遇到这个问题。

计划

我突然想到,我可以让相同的加速/减速功能根据我们是否处于夏令时调整自己的 cron 触发器。为此,我只需要添加一点时间逻辑。

首先,我需要获取当地时间的当前时间并确定是否是夏令时。值得庆幸的是,Python 有一些非常有用的日期功能,可以很容易地做到这一点。

from datetime import datetime
import pytz

utc_time = datetime.utcnow().replace(tzinfo=pytz.utc)
cst_timezone = pytz.timezone('US/Central')
cst_time = utc_time.astimezone(cst_timezone)

进入全屏模式 退出全屏模式

我们导入datetime库(用于处理任何日期或时间对象的主要 Python 库)和pytz(用于准确处理时区的库)。我们创建utc_time并将其设置为datetime.utcnow()等于 UTC 中的当前时间。请注意,我们必须手动将本机datetime对象上的时区信息替换为pytz中的utc时区。

接下来,我们创建一个中央时区对象cst_timezone并从pytz库中提取该信息。最后,我们使用函数astimezone()将 UTC 时间转换为 CST 时间,该函数将时区对象作为参数。

所以现在我们的当前时间在 CST 时区,我们可以确定它是否是夏令时。

# This will show the offset in seconds for DST ⏰
# If it's zero, we're not in DST
is_dst = cst_time.tzinfo._dst.seconds != 0

进入全屏模式 退出全屏模式

如果我们深入研究当前时间对象cst_time,我们可以获得有关时区的更多信息。通过访问我们的时区信息 (tzinfo),然后是我们的 DST 信息 (_dst),我们可以找到当前时间上有多少seconds偏移量。如果我们在 DST 中,则偏移量不会为 0(实际上是 3600 秒,因为时间会偏移一个小时)。

触发器

现在我们知道它是否是夏令时,我们可以调整我们的 cron 触发器。首先,让我们定义如果我们在 DST 中我们的表达式将基于什么。在 DST 期间,我们需要更早的触发器。

if is_dst:
  start_schedule = 'cron(0 12 ? * * *)'
  stop_schedule = 'cron(0 0 ? * * *)'
else:
  start_schedule = 'cron(0 13 ? * * *)'
  stop_schedule = 'cron(0 1 ? * * *)'
# Curious why these cron expressions don't have the weekdays anymore? Read on!

进入全屏模式 退出全屏模式

接下来,我们需要实际更新 CloudWatch 中的规则。

  import boto3
  client = boto3.client('events')

  START_RULE = "start_rule"
  STOP_RULE = "stop_rule"

  start_settings = client.list_targets_by_rule(Rule=START_RULE)
  stop_settings = client.list_targets_by_rule(Rule=STOP_RULE)

进入全屏模式 退出全屏模式

我们创建了boto3客户端,以便能够访问我们的 AWS 资源。假设我们的规则命名为"start_rule""stop_rule",然后我们可以从 CloudWatch 获取有关这些规则的信息。在这里,我们使用函数list_targets_by_rule()来取回这些规则的目标(即这个函数)。

# Remove targets from current rules
client.remove_targets(
  Rule=START_RULE,
  Ids=[(start_settings['Targets'][0]['Id'])]
)
client.remove_targets(
  Rule=STOP_RULE,
  Ids=[(stop_settings['Targets'][0]['Id'])]
)
# Delete rules
client.delete_rule(Name=START_RULE)
client.delete_rule(Name=STOP_RULE)

进入全屏模式 退出全屏模式

从技术上讲,我们不是在更新 cron 触发器,而是替换它们。为了删除 AWS 中的 cron 事件触发器,您必须首先通过传入一组 ID 来删除其目标。我们会将我们从之前的电话中获得的信息提供给它。我们只有一个目标,所以我们从我们查询的设置中提取第一个(也是唯一一个)项目,例如start_settings['Targets'][0]['Id']。一旦我们删除了我们的目标,我们就可以删除我们的规则。

# Add new rules
client.put_rule(
  Name=START_RULE,
  ScheduleExpression=start_schedule,
  State='ENABLED',
  Description="Automatic trigger for the Spinup-Spindown function."
)
client.put_rule(
  Name=STOP_RULE,
  ScheduleExpression=stop_schedule,
  State='ENABLED',
  Description="Automatic trigger for the Spinup-Spindown function"
)

进入全屏模式 退出全屏模式

现在,我们可以添加新的 cron 规则。我们将设置与之前相同的名称,并设置我们定义为start_schedulestop_schedule的新 cron 表达式。我们启用规则并添加我们的描述。

# Add targets
client.put_targets(
  Rule=START_RULE,
  Targets=[
    {
      'Id': start_settings['Targets'][0]['Id'],
      'Arn':start_settings['Targets'][0]['Arn'],
      'Input': start_settings['Targets'][0]['Input']
    }
  ]
)
client.put_targets(
  Rule=STOP_RULE,
  Targets=[
    {
      'Id': stop_settings['Targets'][0]['Id'],
      'Arn':stop_settings['Targets'][0]['Arn'],
      'Input': stop_settings['Targets'][0]['Input']
    }
  ]
)

进入全屏模式 退出全屏模式

最后,我们重新添加目标。我们可以通过从设置对象中提取所有信息来做到这一点。我们更新的 cron 规则已经到位!

最终调整

所以现在我们的函数可以更新自己的 cron 触发器了,太棒了!可是等等;在时间变化实际发生的日子里会发生什么?我们的函数不会在某一天晚一小时或早一小时运行吗?确实会,但我们在这里很幸运。

如果你还记得我们一开始的 cron 表达式,我们只在周一到周五运行这个函数,因为我们不希望我们的环境在周末启动。在美国,夏令时从 3 月的第 2 个星期日开始,到 11 月的第 1 个星期日结束。所以我们的 1 小时轮班会发生在周末,它永远不会影响我们!

就像我说的那样,我们原来的 cron 表达式不会在周末运行,所以我们需要稍微改变一下 cron 表达式,这样它们就可以了。如果您从前面的代码片段中注意到,我们已经这样做了:

if is_dst:
  start_schedule = 'cron(0 12 ? * * *)'
  stop_schedule = 'cron(0 0 ? * * *)'
else:
  start_schedule = 'cron(0 13 ? * * *)'
  stop_schedule = 'cron(0 1 ? * * *)'

进入全屏模式 退出全屏模式

这些 cron 表达式现在将在一周中的每一天运行,而不仅仅是周一到周五。

现在我们的函数每天都在运行,我们需要通过更多的时间逻辑来阻止它在周末进行加速和减速。

weekday = cst_time.weekday()
#in the python calendar, 5 and 6 correspond to Saturday and Sunday
if weekday == 5 or weekday == 6:
  return {
      'statusCode': 200,
      'message': 'Today is a not a weekday, so the spin-up/spin-down process will not proceed.'
  }
else:
  begin_startstop_process()

进入全屏模式 退出全屏模式

我们取出我们的cst_time对象并使用weekday()提取星期几。然后我们检查它是星期六还是星期日,如果是,我们返回一个 statusCode 和消息并停止执行。否则我们可以让事情顺利进行!

最后的想法

所以我们开始了,我们现在可以在 AWS 中自动调整我们的 cron 触发器,这样夏令时就不会再影响我们了!但是,我们肯定可以在未来做出一些改进。例如,您可能会问以下问题:

为什么我们每天都在调整规则,即使规则一年只改变两次?

如果我们有一个函数需要在周末运行而我们不想每年迟到两次怎么办?

这可以很简单地通过将所有这些功能提取到一个单独的函数中来解决。使用 cron 表达式,我们还可以将此函数设置为仅在 3 月和 11 月的星期日运行,因为时间会发生变化!让我们总结一下:

/* CRON triggers for DST function */
0 12 8-14 MAR SUN * // 13:00 UTC (6am CST) for the second Sunday in March
0 0 1-7 NOV SUN * // 1:00 UTC (6pm CST) for the first Sunday in November

进入全屏模式 退出全屏模式

感谢您的陪伴和阅读。希望这将帮助您完成在 AWS 中的工作!


想要更深入地挖掘并了解更多信息吗?以下是一些有用的链接:

美国夏令时 |维基百科

规则的计划表达式 |亚马逊网络服务

快速简单的 Cron 表达式编辑器 | crontab 大师

Logo

云原生社区为您提供最前沿的新闻资讯和知识内容

更多推荐