Django 文件系统/基于文件的缓存在 5-10% 的时间内无法写入数据

Nei*_*eil 5 python django caching celery

我们正在使用 Django Celery 进行后台数据处理,获取 CSV 文件(最大 15MB),将其转换为 dict 数据列表(其中还包括一些 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
Run Code Online (Sandbox Code Playgroud)

所有任务都在由 Celery 管理的后台并发进程中运行。我们最初使用Redis后端,但发现它在峰值负载场景和高并发情况下经常会出现内存不足的情况。所以我们切换到Django 的基于文件的缓存后端。虽然这解决了内存问题,但我们发现 20-30% 的缓存条目从未被写入。没有抛出错误,只是无声的失败。当我们返回并从 CLI 查找缓存时,我们看到例如 chunk_id_7 和 chunk_id_9 会存在,但 chunk_id_8 不会存在。因此,间歇性地,某些缓存条目无法保存。

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

我们注意到过去Django 基于文件的缓存存在并发进程问题,但它似乎在很多年前就已得到修复(我们使用的是 v1.11)。一条评论说这个缓存后端更像是一个 POC,尽管再次不确定它从那时起是否发生了变化。

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

Nei*_*eil 3

在 Django FileBased 和 DiskCache DjangoCache 中,问题是缓存已满并被各自的后端在后台剔除。对于 Django FB,当MAX_ENTRIES达到缓存(默认 300)时,就会发生剔除,此时它会根据(默认 33%)随机CULL_FREQUENCY删除一小部分条目。因此,我们的缓存已满,随机条目被删除,如果条目被随机删除,这当然会导致cache.get()某些sub_task块失败。

对于 DiskCache,默认缓存size_limit为 1GB。当达到该值时,条目将根据EVICTION_POLICY 默认为最近最少使用的条目进行剔除。在我们的例子中size_limit,达到后,它正在删除仍在使用的条目,尽管至少是最近。

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

因此,我们最终实现了一个更具确定性的 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)
Run Code Online (Sandbox Code Playgroud)

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