nginx/uwsgi 服务器的持久内存 Python 对象

8 投票
4 回答
5359 浏览
提问于 2025-04-17 19:17

我怀疑这是否可能,但我想分享一下我的问题和提出的解决方案(这个解决方案的可行性正是我想问的):


我有一些“全局数据”,需要在所有请求中都能使用。我把这些数据存储在Riak中,并使用Redis作为缓存层,以提高访问速度(目前是这样)。这些数据分成大约30个逻辑块,每个块大约8KB。

每个请求需要读取这8KB块中的4个,这样就需要从Redis或Riak中读取32KB的数据。这还不包括任何特定于请求的数据,这部分数据也需要读取(其实还挺多的)。

假设每秒有3000个请求(这不是一个实时服务器,所以我没有真实的数据,但3000个请求每秒是一个合理的假设,可能还会更多),这意味着每秒需要从Redis或Riak传输96KB的数据,此外还有其他应用逻辑中发出的不少请求。同时,Python每秒要解析这些8KB对象的JSON3000次。


这一切,尤其是Python需要反复解析数据,感觉非常浪费。如果能有一个将反序列化后的数据缓存到Python内存中的原生对象的优雅解决方案,那就太好了。我可以定期刷新这些“静态”数据,而不是每秒3000次,可能每几分钟(或几小时)刷新一次。

但我不知道这是否可能。实际上,你需要一个“始终运行”的应用程序,才能在内存中缓存任何数据。我知道在nginx+uwsgi+python的组合中,这种情况并不成立(与node之类的相比)——根据我的了解,Python的内存数据不会在所有请求之间保持不变,除非我真的搞错了。

不幸的是,这是我“继承”的系统,因此在基础技术上不能做太多更改,而且我对nginx+uwsgi+python的工作原理了解得不够深入,特别是在启动Python进程和保持Python内存数据方面——这意味着我可能在上面的假设上真的搞错了!


所以,如果能直接告诉我这个解决方案是否可行,以及一些能帮助我理解nginx+uwsgi+python在启动新进程和内存分配方面的资料,那将非常有帮助。

附言:

  1. 我已经查看了一些nginx、uwsgi等的文档,但还没有完全理解这些对我使用案例的影响。希望接下来能在这方面有所进展。

  2. 如果内存缓存真的可行,我就会放弃Redis,因为我只在缓存上面提到的静态数据。这使得在进程中持久化的内存Python缓存对我来说更具吸引力,减少了系统中的一个可移动部分,并且每个请求至少减少了四次网络往返。

4 个回答

1

你没有提到要把这些数据写回去,这些数据是静态的吗?如果是静态的,那解决方案就简单多了,我真搞不懂那些说“这不现实”的回答是怎么回事。

Uwsgi工作进程一直在运行的应用程序。所以在请求之间,数据是会被保存下来的。你只需要把东西存储在一个全局变量里,就可以了。记住,这个全局变量是针对每个工作进程的,而且工作进程会不时重启,所以你需要有合适的加载和失效策略。

如果数据更新得非常少(少到可以在更新时重启服务器),你甚至可以节省更多资源。只需在应用程序构建时创建这些对象。这样,它们只会被创建一次,然后所有的工作进程会从主进程分叉出来,重用相同的数据。当然,这是写时复制,所以如果你更新了数据,你就会失去内存的好处(如果Python在垃圾回收时决定压缩内存,也会发生同样的事情,所以这并不是特别可预测)。

1

“我知道,Python 的内存数据不会在所有请求之间保持不变,除非我真的搞错了。”

你搞错了。

使用 uwsgi 而不是 CGI 的主要原因就是为了在多个线程之间保持数据不变,这样每次调用时就不用重新初始化。你必须在你的 .ini 文件中设置 processes = 1,否则根据 uwsgi 的配置,它可能会为你启动多个工作进程。你可以记录 env,查看 'wsgi.multiprocess': False'wsgi.multithread': True,这样所有属于单个工作进程的 uwsgi.core 线程应该显示相同的数据。

你还可以通过使用内置的 stats-server 来查看你有多少个工作进程,以及每个工作进程下有多少个“核心”线程。

这就是为什么 uwsgi 提供了 lockunlock 函数,让多个线程可以操作数据存储。

你可以通过在你的应用中添加一个 /status 路由来轻松测试这一点,这个路由会简单地输出你全局数据对象的 JSON 表示,每当你进行更新操作后,可以不时查看一下这个状态。

3

你提到的方案其实不太可行。因为新的进程可以在你控制之外随时启动和关闭,所以没办法把原生的Python数据一直保存在内存里。

不过,有一些解决办法可以绕过这个问题。

通常情况下,一层简单的键值存储就够用了。有时候,固定大小的值缓冲区(你可以直接用 str/bytes/bytearray 对象;其他需要的东西需要用 struct 处理或者序列化)就能满足需求。在这种情况下,uWSGI自带的缓存框架可以帮你处理所有需要的事情。

如果你需要更精确的控制,可以看看缓存是如何在SharedArea上实现的,然后进行一些自定义。不过我不太推荐这样做。这样做基本上给你提供了一个类似文件的API,唯一的好处是服务器会管理文件的生命周期;它可以在所有uWSGI支持的语言中工作,即使那些不允许使用文件的语言;而且如果你以后需要,可以更容易地将自定义缓存迁移到分布式(多台计算机)缓存。我觉得这些对你来说都不太相关。

另一种获取简单键值存储的方法,但没有固定大小的缓冲区,可以使用Python的标准库anydbm。键值查找非常符合Python的风格:看起来就像一个 dict,只是它是备份到磁盘上的BDB(或类似)数据库,适当地缓存到内存中,而不是存储在内存中的哈希表里。

如果你需要处理一些其他简单类型,比如 int 这种快速序列化和反序列化的类型,你可以考虑使用shelve

如果你的结构足够固定,可以在顶层使用键值数据库,但通过 ctypes.Structure 访问值,或者用 struct 进行序列化和反序列化。不过通常情况下,如果你能这样做,那你也可以省去顶层,这样你的整个结构就变成了一个大的 StructureArray

到那时,你可以直接使用普通文件来存储——要么用 mmap(对于 ctypes),要么直接 openread(对于 struct)。

或者使用 multiprocessing共享 ctypes 对象,直接从共享内存区域访问你的 Structure

同时,如果你并不需要所有的缓存数据,只是偶尔需要一些,那这正是数据库的用武之地。同样,anydbm 等等可能就足够了,但如果你的结构比较复杂,可以画个ER图,把它转成一组表,然后使用像MySQL这样的数据库。

撰写回答