我的许多观点都获取外部资源。我想确保在重负载下我不会炸毁远程站点(和/或被禁止)。
我只有 1 个履带,所以有一个中央锁就可以了。
所以细节:我希望每秒最多允许对主机进行 3 个查询,并且让其余的块最多 15 秒。我怎么能(轻松)做到这一点?
一些想法:
- 使用 django 缓存
- 似乎只有 1 秒的分辨率
- 使用基于文件的信号量
- 容易为并发做锁。不知道如何确保每秒只进行 3 次提取。
- 使用一些共享内存状态
- 我宁愿不安装更多的东西,但如果我必须的话。
使用不同的进程来处理抓取,以及它与 Django 之间的通信队列怎么样?
这样您就可以轻松更改并发请求的数量,并且它还会自动跟踪请求,而不会阻塞调用者。
最重要的是,我认为这将有助于降低主应用程序(在 Django 中)的复杂性。
一种方法;创建一个这样的表:
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 秒。
每当要执行操作时,请执行以下操作:
start_time
为该站点的最大 start_time 加上 1/3 秒。如果最大的是未来 10 秒,那么我们可以在 10 1/3 秒开始我们的行动。如果那个时间是过去的,则将其限制为 now()。原子动作是最重要的。您不能简单地对查询进行聚合然后保存它,因为它会竞争。我不知道 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 将不是原子的:两个请求可能同时进行 UPDATE,并收到相同的结果。Django 和 Python 通常会关闭自动提交功能,因此您需要将其打开然后再关闭。对于 Postgres,这是 connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) 和 ISOLATION_LEVEL_READ_COMMITTED。我不知道如何用 MySQL 做到这一点。
(我认为在 Python 的 DB-API 中默认关闭自动提交是一个严重的设计缺陷。)
这种方法的好处是它非常简单,具有直接的状态;您不需要诸如事件侦听器和唤醒之类的东西,它们有自己的一系列问题。
一个可能的问题是,如果用户在延迟期间取消请求,无论您是否执行该操作,延迟仍会强制执行。如果您从不启动该操作,其他请求将不会向下移动到未使用的“时间段”。
如果您无法让自动提交工作,解决方法是向 (site, start_time) 添加一个 UNIQUE 约束。(我不认为 Django 直接理解这一点,因此您需要自己添加约束。)然后,如果发生竞争并且对同一站点的两个请求同时结束,其中一个将引发约束您可以捕获的异常,您可以重试。您还可以使用普通的 Django 聚合而不是原始 SQL。但是,捕获约束异常并不那么健壮。