nginx/uwsgi 服务器的持久内存 Python 对象
我怀疑这是否可能,但我想分享一下我的问题和提出的解决方案(这个解决方案的可行性正是我想问的):
我有一些“全局数据”,需要在所有请求中都能使用。我把这些数据存储在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在启动新进程和内存分配方面的资料,那将非常有帮助。
附言:
我已经查看了一些nginx、uwsgi等的文档,但还没有完全理解这些对我使用案例的影响。希望接下来能在这方面有所进展。
如果内存缓存真的可行,我就会放弃Redis,因为我只在缓存上面提到的静态数据。这使得在进程中持久化的内存Python缓存对我来说更具吸引力,减少了系统中的一个可移动部分,并且每个请求至少减少了四次网络往返。
4 个回答
你没有提到要把这些数据写回去,这些数据是静态的吗?如果是静态的,那解决方案就简单多了,我真搞不懂那些说“这不现实”的回答是怎么回事。
Uwsgi工作进程是一直在运行的应用程序。所以在请求之间,数据是会被保存下来的。你只需要把东西存储在一个全局变量里,就可以了。记住,这个全局变量是针对每个工作进程的,而且工作进程会不时重启,所以你需要有合适的加载和失效策略。
如果数据更新得非常少(少到可以在更新时重启服务器),你甚至可以节省更多资源。只需在应用程序构建时创建这些对象。这样,它们只会被创建一次,然后所有的工作进程会从主进程分叉出来,重用相同的数据。当然,这是写时复制,所以如果你更新了数据,你就会失去内存的好处(如果Python在垃圾回收时决定压缩内存,也会发生同样的事情,所以这并不是特别可预测)。
“我知道,Python 的内存数据不会在所有请求之间保持不变,除非我真的搞错了。”
你搞错了。
使用 uwsgi 而不是 CGI 的主要原因就是为了在多个线程之间保持数据不变,这样每次调用时就不用重新初始化。你必须在你的 .ini
文件中设置 processes = 1
,否则根据 uwsgi 的配置,它可能会为你启动多个工作进程。你可以记录 env
,查看 'wsgi.multiprocess': False
和 'wsgi.multithread': True
,这样所有属于单个工作进程的 uwsgi.core
线程应该显示相同的数据。
你还可以通过使用内置的 stats-server
来查看你有多少个工作进程,以及每个工作进程下有多少个“核心”线程。
这就是为什么 uwsgi 提供了 lock
和 unlock
函数,让多个线程可以操作数据存储。
你可以通过在你的应用中添加一个 /status
路由来轻松测试这一点,这个路由会简单地输出你全局数据对象的 JSON 表示,每当你进行更新操作后,可以不时查看一下这个状态。
你提到的方案其实不太可行。因为新的进程可以在你控制之外随时启动和关闭,所以没办法把原生的Python数据一直保存在内存里。
不过,有一些解决办法可以绕过这个问题。
通常情况下,一层简单的键值存储就够用了。有时候,固定大小的值缓冲区(你可以直接用 str
/bytes
/bytearray
对象;其他需要的东西需要用 struct
处理或者序列化)就能满足需求。在这种情况下,uWSGI自带的缓存框架可以帮你处理所有需要的事情。
如果你需要更精确的控制,可以看看缓存是如何在SharedArea
上实现的,然后进行一些自定义。不过我不太推荐这样做。这样做基本上给你提供了一个类似文件的API,唯一的好处是服务器会管理文件的生命周期;它可以在所有uWSGI支持的语言中工作,即使那些不允许使用文件的语言;而且如果你以后需要,可以更容易地将自定义缓存迁移到分布式(多台计算机)缓存。我觉得这些对你来说都不太相关。
另一种获取简单键值存储的方法,但没有固定大小的缓冲区,可以使用Python的标准库anydbm
。键值查找非常符合Python的风格:看起来就像一个 dict
,只是它是备份到磁盘上的BDB(或类似)数据库,适当地缓存到内存中,而不是存储在内存中的哈希表里。
如果你需要处理一些其他简单类型,比如 int
这种快速序列化和反序列化的类型,你可以考虑使用shelve
。
如果你的结构足够固定,可以在顶层使用键值数据库,但通过 ctypes.Structure
访问值,或者用 struct
进行序列化和反序列化。不过通常情况下,如果你能这样做,那你也可以省去顶层,这样你的整个结构就变成了一个大的 Structure
或 Array
。
到那时,你可以直接使用普通文件来存储——要么用 mmap
(对于 ctypes
),要么直接 open
和 read
(对于 struct
)。
或者使用 multiprocessing
的共享 ctypes
对象,直接从共享内存区域访问你的 Structure
。
同时,如果你并不需要所有的缓存数据,只是偶尔需要一些,那这正是数据库的用武之地。同样,anydbm
等等可能就足够了,但如果你的结构比较复杂,可以画个ER图,把它转成一组表,然后使用像MySQL这样的数据库。