【问题标题】:memcache.get returns wrong object (Celery, Django)memcache.get 返回错误的对象(芹菜,Django)
【发布时间】:2014-06-15 01:03:42
【问题描述】:

这是我们目前拥有的:

  1. 我们正在尝试获取缓存的 django 模型实例,缓存键包括模型名称和实例 ID。使用 Django 的标准 memcached 后端。此程序是非常广泛使用的常见程序的一部分,不仅在芹菜中。
  2. 有时(随机和/或很少)cache.get(key) 返回错误的对象:int 或不同的模型实例,甚至出现相同模型不同 ID 的情况。我们通过检查模型名称和 id 以及缓存键的对应关系来捕捉到这一点。
  3. 该错误仅出现在我们的三个 celery 任务的上下文中,永远不会在 python shell 或其他 celery 任务中重现。 UPD:仅出现在长时间运行的 CPU-RAM 密集型任务下
  4. 缓存存储了正确的值(我们在错误刚刚出现时手动检查了该值)
  5. 使用相同的参数再次调用相同的任务可能不会重现该问题,尽管概率要高得多,因此错误出现往往会在同一时间段内“分组”
  6. 重启 celery 解决了随机时间段(分钟 - 周)的问题
  7. *新* 这与内存溢出无关。发生这种情况时,我们始终至少有 2Gb 可用 RAM。
  8. *NEW* 我们在静态代码中有cache_instance = cache.get_cache("cache_entry")。在调查过程中,我发现在错误发生的那一刻,cache_instance.get(key) 返回了错误的值,尽管下一行的get_cache("cache_entry").get(key) 返回了正确的值。这意味着错误消失得太快或由于某种原因 cache_instance 对象已损坏。 django的缓存线程返回的缓存实例对象不安全吗?
  9. *新*我们记录了非常奇怪的情况:作为缓存中的另一个错误对象,我们得到了未设置 id 的模型实例。这意味着,实例从未保存到数据库,因此无法缓存。 (我希望)
  10. *新*这些天至少记录了一个 MemoryError

我知道,所有这些听起来像是某种魔法。真的,任何想法如何实现或如何调试都将不胜感激。

PS:我目前的假设是这与多处理有关:一旦在静态代码中创建缓存实例并且在工作进程分叉之前,这将导致所有工作人员共享同一个套接字(听起来合理吗?)

【问题讨论】:

  • 发现这个:groups.google.com/forum/#!topic/django-developers/x1ZnV7rj8Tk 似乎已连接,但已报告该错误并标记为已修复。
  • 尝试以更详细 (-vv) 的方式运行您的内存缓存并根据您的密钥进行过滤,您可能能够更好地解释它到底哪里出错了
  • 我猜你没有尝试过不同的硬件?
  • 您的 memcached 实际上是一个集群吗?您在 Django 中使用的是哪个 memcached 客户端?我怀疑在高使用率时,您的一个节点响应太慢,因此它被标记为 DOWN。当我们将 memcached 与 celery 一起使用时,我们还使用 Moxi 在较小的 memcached 集群前面处理故障转移。
  • @frederick_c_siu 我们的 memcached 是一个集群,但据我所知,memcached 客户端返回 None 以防相应节点被标记。

标签: python django caching memcached celery


【解决方案1】:

终于解决了:

  1. Celery 具有动态缩放功能 - 它能够根据负载添加/杀死工作人员
  2. 它通过分叉现有的来实现
  3. 打开的套接字和文件被复制到分叉的进程,所以两个进程共享它们,这会导致竞争条件,当一个进程读取另一个进程的响应时。简而言之,一个进程可能会读取第二个进程的响应,反之亦然。
  4. from django.core.cache import cache 此对象存储预连接的 memcached 套接字。当你的进程可以动态分叉时不要使用它。不要使用存储的连接、池和其他。
  5. 或将它们存储在当前 PID 下,并在每次访问缓存时检查它

【讨论】:

  • 拯救了我的一天。谢谢!
  • @mrcrgl 我几乎可以肯定我是唯一一个遇到这个问题的幸运者 :)
【解决方案2】:

这一直困扰着我一段时间,直到我找到这个问题和答案。我只是想补充一些我学到的东西。

您可以使用本地 memcached 实例轻松重现此问题:

from django.core.cache import cache
import os

def write_read_test():
    pid = os.getpid()
    cache.set(pid, pid)
    for x in range(5):
        value = cache.get(pid)
        if value != pid:
            print "Unexpected response {} in process {}. Attempt {}/5".format(
                    value, pid, x+1)
    os._exit(0)

cache.set("access cache", "before fork")
for x in range(5):
    if os.fork() == 0:
        write_read_test()

你可以做的是关闭缓存客户端,就像 Django 在request_finished 信号中所做的那样:

https://github.com/django/django/blob/master/django/core/cache/init.py#L128

如果您在分叉后添加cache.close(),则一切正常。

对于 celery,您可以 connect to a signal that is fired after the worker is forked 并执行 cache.close()

当 preload 处于活动状态并且缓存在分叉 worker 之前初始化时,这也会影响 gunicorn。

gunicorn,你可以use post_fork in your gunicorn configuration:

def post_fork(server, worker):
    from django.core.cache import cache
    cache.close()

【讨论】:

  • 在 Django 1.11、Celery 4.1 和 8 个工作人员池的上下文中,我发现连接到的正确信号是 worker_process_init (celery.readthedocs.io/en/latest/userguide/…) worker_ready 仅触发一次当工人在达到配置的worker_max_tasks_per_child
  • 另外,如果使用多个缓存后端,最好调用:from django.core import cache; cache.close_caches()django.core.cache.cache 是默认缓存后端的代理。这可能不符合您的需求。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-15
  • 2021-11-29
  • 1970-01-01
相关资源
最近更新 更多