Django:简单的速率限制

时间:2009-09-07 07:08:12

标签: python django caching mutex semaphore

我的许多观点都会获取外部资源。我想确保在重载下我不会炸毁远程站点(和/或被禁止)。

我只有1个爬虫,所以有一个中央锁可以正常工作。

所以细节:我希望每秒最多允许3个主机查询,并且其余的块最多15秒。我怎么能(轻松)这样做?

一些想法:

  • 使用django缓存
    • 似乎只有1秒的分辨率
  • 使用基于文件的信号量
    • 易于锁定并发。不确定如何确保每秒只发生3次提取。
  • 使用某些共享内存状态
    • 我宁愿不安装更多的东西,但如果必须的话。

2 个答案:

答案 0 :(得分:1)

使用不同的进程来处理抓取,以及它和Django之间的通信队列呢? 通过这种方式,您可以轻松更改并发请求的数量,并且还可以自动跟踪请求,而不会阻止调用者。
最重要的是,我认为这有助于降低主应用程序的复杂性(在Django中)。

答案 1 :(得分:1)

一种方法;创建一个这样的表:

class Queries(models.Model):
    site = models.CharField(max_length=200, db_index=True)
    start_time = models.DateTimeField(null = True)
    finished = models.BooleanField(default=False)

这会记录每个查询何时发生,或者如果限制阻止它立即发生,将来会发生。 start_time是动作开始的时间;如果行动目前正在阻止,那将来就是这样。

不要考虑每秒查询次数,而是考虑每个查询的秒数;在这种情况下,每个查询1/3秒。

每当要执行某项操作时,请执行以下操作:

  • 为操作创建一行。 q = Queries.objects.create(site = sitename)
  • 在您刚刚创建的对象(q.id)上,将start_time原子设置为此站点的最大start_time加上1/3秒。如果将来最大的是10秒,那么我们可以在10 1/3秒开始行动。如果那个时间过去了,请把它夹到现在()。
  • 如果刚刚设置的start_time是将来,请睡到那个时间。如果将来太远(例如超过15秒),请删除行并输出错误。
  • 查询完成后,将finish设置为True,以便稍后可以清除该行。

原子动作是重要的。您不能简单地在查询上进行聚合,然后保存它,因为它会竞争。我不知道Django是否可以原生地执行此操作,但在原始SQL中它很容易:

UPDATE site_queries
SET start_time = MAX(now(), COALESCE(now(), (
    SELECT MAX(start_time) + 1.0/3 FROM site_queries WHERE site = site_name
)))
WHERE id = object_id

然后,重新加载模型并在必要时休眠。您还需要清除旧行。像Queries.objects.filter(site = site,finished = True).exclude(id = id).delete()之类的东西可能会起作用:删除除刚刚创建的查询之外的所有已完成的查询。 (这样,您永远不会删除最新的查询,因为以后的查询需要安排查询。)

最后,确保UPDATE不会在事务中发生。必须启用自动提交才能使其正常工作。否则,UPDATE将不是原子的:两个请求可能同时更新,并收到相同的结果。 Django和Python通常会自动关闭,因此您需要将其打开然后再关闭。使用Postgres,这是connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)和ISOLATION_LEVEL_READ_COMMITTED。我不知道如何用MySQL做这件事。

(我认为在Python的DB-API中关闭autocommit的默认设置是一个严重的设计缺陷。)

这种方法的好处是它非常简单,具有简单的状态;你不需要像事件监听器和唤醒这样的东西,它们都有各自的问题。

可能的问题是,如果用户在延迟期间取消请求,无论您是否执行操作,仍会强制执行延迟。如果您从未开始操作,其他请求将不会向下移动到未使用的“时间段”。

如果您无法使用autocommit工作,则解决方法是向(site,start_time)添加UNIQUE约束。 (我不认为Django直接理解这一点,所以你需要自己添加约束。)然后,如果竞争发生并且同一站点的两个请求同时结束,其中一个将抛出约束您可以捕获的异常,您可以重试。您也可以使用普通的Django聚合而不是原始SQL。但是,捕捉约束异常并不那么强大。