Django文件系统/基于文件的缓存在5-10%的情况下无法写入数据。

5
我们正在使用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后端,但发现它在高并发和峰值负载情况下会经常耗尽内存(参考链接)。因此,我们转而使用了Django的基于文件的缓存后端。虽然这解决了内存问题,但我们发现20-30%的缓存条目从未被写入。没有任何错误提示,只是默默失败。当我们用CLI查找缓存时,我们会发现例如chunk_id_7和chunk_id_9存在,但chunk_id_8不存在。因此,间歇性地,某些缓存条目无法保存。
我们更换了diskcache后端并观察到相同的情况,尽管缓存故障似乎降低到了5-10%(非常粗略的估计)。
我们注意到在过去的Django基于文件的缓存中存在并发进程问题,但这似乎在很多年前就被修复了(我们使用的版本是v1.11)。一条评论说,这个缓存后端更像是一个POC,但不确定它是否已经改变。
文件缓存是一个生产质量的缓存解决方案吗?如果是,是什么导致了我们的写入故障?如果不是,那么针对我们的用例有什么更好的解决方案?
1个回答

3
在Django FileBased和DiskCache DjangoCache中,问题在于缓存被各自的后端填满并在后台被削减。对于Django FB,当达到缓存中的MAX_ENTRIES(默认为300)时,会发生修剪操作,在此时它会随机删除基于CULL_FREQUENCY(默认为33%)的部分条目。因此,我们的缓存正在变满,而随机条目正在被删除,这当然会导致sub_task中的cache.get()在某些块上失败,如果其条目已被随机删除。
对于DiskCache,默认缓存size_limit为1GB。当达到此限制时,根据EVICTION_POLICY(默认为最近最少使用)修剪条目。在我们的情况下,当达到size_limit时,它会删除仍在使用中但最近最少使用的条目。
理解了这一点后,我们尝试使用 EVICTION_POLICY = 'none' 的 DiskCache 避免清除缓存。这几乎起作用了,但对于小于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任务之间传递大数据的基本问题?


这个概念能帮助吗?https://medium.com/wultra-blog/achieving-high-performance-with-postgresql-and-redis-deddb7012b16 - raratiru
我们最初使用Redis作为缓存后端,但在重负载期间会耗尽内存。请参见此处 - Neil
“一种不太可能的建议”:他是DiskCache的作者,建议使用numpy。我从未使用过numpy,但他非常有经验,也许你可以检查一下那里是否有什么东西可用。 - raratiru

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接