Django filesystem/file-based 缓存在 5-10% 的时间内无法写入数据

Django filesystem/file-based cache failing to write data 5-10% of the time

我们正在使用 Django Celery 进行后台数据处理,获取一个 CSV 文件(最大 15MB),将其转换为字典数据列表(其中还包括一些 Django 模型对象),并将其分解成块进行处理在子任务中:

@task
def main_task(data):
  i = 0
  for chunk in chunk_up(data):
    chunk_id = "chunk_id_{}".format(i)
    cache.set(chunk_id, chunk, timeout=FIVE_HOURS)
    sub_task.delay(chunk_id)
    i += 1

@task
def sub_task(chunk_id):
  data_chunk = cache.get(chunk_id)
  ... # do processing

Celery 管理的后台并发进程中的所有任务运行。我们最初使用 Redis 后端,但发现它会 routinely run out of memory during peak load scenarios and high concurrency. So we switched to Django's filebased cache backend。虽然这解决了内存问题,但我们看到 20-30% 的缓存条目从未被写入。没有抛出错误,只是无声的失败。当我们返回并从 CLI 查找缓存时,我们看到了例如chunk_id_7 和 chunk_id_9 会存在,但 chunk_id_8 不会。所以间歇性地,一些缓存条目无法保存。

我们交换了 diskcache 后端并观察到同样的事情,尽管缓存故障似乎减少到 5-10%(非常粗略的估计)。

我们在过去 there where concurrent process issues with Django filebased cache 注意到了这一点,但它似乎在很多年前就已修复(我们使用的是 v1.11)。一条评论说这个缓存后端更像是一个 POC,但再次不确定它是否从那时起发生了变化。

基于文件的缓存是一种生产质量的缓存解决方案吗?如果是,什么可能导致我们的写入失败?如果不是,对于我们的用例,什么是更好的解决方案?

在 Django FileBased 和 DiskCache DjangoCache 中,问题是缓存已满并被各自的后端在后台剔除。在 Django FB 的情况下,当达到缓存中的 MAX_ENTRIES 时(默认 300)发生剔除,此时它 随机 删除基于 [=12= 的一小部分条目](默认 33%)。所以我们的缓存越来越满,随机条目被删除,如果条目被随机删除,这当然会导致 sub_task 中的 cache.get() 在某些块上失败。

对于 DiskCache,默认缓存 size_limit 为 1GB。达到时,将根据 EVICTION_POLICY which is defaulted to least recently used 剔除条目。在我们的例子中,在达到 size_limit 之后,它正在删除仍在使用的条目,至少最近是这样。

了解这一点后,我们尝试将 DiskCache 与 EVICTION_POLICY = 'none' 一起使用,以避免在任何情况下被剔除。这 几乎 有效,但是对于一小部分 (< 1%) 的缓存条目,我们仍然看到 cache.get() 无法获取实际存在于缓存中的条目。也许是 SQLLite 错误?即使在每次 cache.get() 调用中添加 retry=True 之后,在某些时候它仍然无法获取实际存在于缓存中的缓存条目。

所以我们最终实现了一个更具确定性的 FileBasedCache,它似乎可以解决问题:

from django.core.cache.backends.filebased import FileBasedCache as DjangoFileBasedCached

class FileBasedCache(DjangoFileBasedCached):
    def _cull(self):
        '''
        In order to make the cache deterministic,
        rather than randomly culling,
        simply remove all expired entries

        Use MAX_ENTRIES to avoid checking every file in the cache
        on every set() operation. MAX_ENTRIES sh be set large enough
        so that when it's hit we can be pretty sure there will be
        expired files. If set too low then we will be checking
        for expired files too frequently which defeats the purpose of MAX_ENTRIES

        :return:
        '''
        filelist = self._list_cache_files()
        num_entries = len(filelist)
        if num_entries < self._max_entries:
            return  # return early if no culling is required
        if self._cull_frequency == 0:
            return self.clear()  # Clear the cache when CULL_FREQUENCY = 0

        for fname in filelist:
            with io.open(fname, 'rb') as f:
                # is_expired automatically deletes what's expired
                self._is_expired(f)

退一步说,我们真正需要的是一个持久可靠的大数据存储,以便跨 Celery 任务访问。我们正在为此使用 Django 缓存,但也许它不是完成这项工作的正确工具?缓存并不意味着 100% 可靠。我们应该使用另一种方法来解决在 Celery 任务之间传递大数据的基本问题吗?