Python + Memcached:分布式应用程序中的高效缓存
编写 Python 应用程序时,缓存非常重要。使用缓存可以避免重新计算数据或访问速度缓慢的数据库,从而大幅提高性能。
Python 提供了内置的缓存功能,从简单的字典到更完整的数据结构,例如functools.lru_cache。后者可以使用最近最少使用算法来限制缓存大小,从而缓存任何项目。
但是,根据定义,这些数据结构对于 Python 进程而言是本地的。当应用程序的多个副本在大型平台上运行时,使用内存数据结构不允许共享缓存内容。这对于大型分布式应用程序来说可能是一个问题。
因此,当系统分布在网络上时,它也需要一个分布在网络上的缓存。如今,有许多网络服务器提供缓存功能 。
正如您将在本教程中看到的,memcached是分布式缓存的另一个不错的选择。在快速介绍 memcached 的基本用法之后,您将了解“缓存和设置”等高级模式,以及如何使用后备缓存来避免冷缓存性能问题。
memcached">安装 memcached
Memcached适用于许多平台:
- 如果你运行的是Linux
apt-get install memcached
,你可以使用或安装它yum install memcached。这将从预构建的包安装 memcached,但你也可以从源代码构建 memcached,如此处所述。
- 对于macOS来说,使用Homebrew是最简单的选择。brew install memcached安装 Homebrew 包管理器后,只需运行即可。
- 在Windows上,您必须自己编译 memcached 或找到预编译的二进制文件。
安装后,只需调用以下命令即可启动memcached:
在从 Python 领域与 memcached 交互之前,您需要安装 memcached客户端库。您将在下一节中了解如何执行此操作以及一些基本的缓存访问操作。
python">使用 Python 存储和检索缓存值
如果你从未使用过memcached,那么它很容易理解。它基本上提供了一个巨大的网络可用字典。这个字典有几个不同于传统 Python 字典的属性,主要是:
- 键和值必须是字节
- 键和值在过期后会自动删除
因此,与memcached交互的两个基本操作是set
和get
。您可能已经猜到了,它们分别用于为键分配值或从键获取值。
我首选的与memcached交互的 Python 库是pymemcache—我建议使用它。你可以简单地使用 pip 安装它:
$ pip install pymemcache
以下代码展示了如何连接到memcached并将其用作 Python 应用程序中的网络分布式缓存:
>>> from pymemcache.client import base# Don't forget to run `memcached' before running this next line:
>>> client = base.Client(('localhost', 11211))# Once the client is instantiated, you can access the cache:
>>> client.set('some_key', 'some value')# Retrieve previously set data again:
>>> client.get('some_key')
'some value'
memcached网络协议非常简单,而且实现速度非常快,这使得它可以用来存储那些从规范数据源检索或再次计算速度很慢的数据:
虽然很简单,但此示例允许跨网络存储键/值元组并通过应用程序的多个分布式运行副本访问它们。这很简单,但功能强大。这是优化应用程序的重要第一步。
自动使缓存数据过期
将数据存储到memcached时,您可以设置过期时间 - memcached保留键和值的最大秒数。在此延迟之后, memcached会自动从其缓存中删除该键。
您应该将此缓存时间设置为多少?此延迟没有神奇的数字,它完全取决于您使用的数据类型和应用程序。它可能是几秒钟,也可能是几个小时。
缓存失效(定义何时删除与当前数据不同步的缓存)也是应用程序必须处理的事情。尤其是当要避免显示太旧或过时的数据时。
再次强调,没有神奇的秘诀;这取决于你正在构建的应用程序类型。但是,有几个异常情况需要处理——我们还没有在上面的例子中涉及。
缓存服务器无法无限增长——内存是有限资源。因此,一旦缓存服务器需要更多空间来存储其他内容,它就会刷新键。
一些密钥也可能过期,因为它们已经到达了过期时间(有时也称为“生存时间”或 TTL)。在这些情况下,数据会丢失,必须再次查询规范数据源。
这听起来比实际要复杂得多。在 Python 中使用 memcached 时,通常可以使用以下模式:
from pymemcache.client import basedef do_some_query():# Replace with actual querying code to a database,# a remote REST API, etc.return 42# Don't forget to run `memcached' before running this code
client = base.Client(('localhost', 11211))
result = client.get('some_key')if result is None:# The cache is empty, need to get the value# from the canonical source:result = do_some_query()# Cache the result for next time:client.set('some_key', result)# Whether we needed to update the cache or not,
# at this point you can work with the data
# stored in the `result` variable:
print(result)
注意:由于正常的刷新操作,必须处理丢失的键。还必须处理冷缓存场景,即memcached刚刚启动时。在这种情况下,缓存将完全为空,需要一次一个请求地完全重新填充缓存。
这意味着您应该将任何缓存数据视为短暂的。并且您永远不应该期望缓存中包含您之前写入的值。
预热冷缓存
有些冷缓存场景无法避免,例如memcached崩溃。但有些可以避免,例如迁移到新的memcached服务器。
当可以预测冷缓存情况将会发生时,最好避免这种情况。需要重新填充的缓存意味着,突然之间,所有缺少缓存数据的缓存用户将大量访问缓存数据的规范存储(也称为惊群问题)。
pymemcache提供了一个名为的类FallbackClient
,它有助于实现这种场景,如下所示:
from pymemcache.client import base
from pymemcache import fallbackdef do_some_query():# Replace with actual querying code to a database,# a remote REST API, etc.return 42# Set `ignore_exc=True` so it is possible to shut down
# the old cache before removing its usage from
# the program, if ever necessary.
old_cache = base.Client(('localhost', 11211), ignore_exc=True)
new_cache = base.Client(('localhost', 11212))client = fallback.FallbackClient((new_cache, old_cache))result = client.get('some_key')if result is None:# The cache is empty, need to get the value # from the canonical source:result = do_some_query()# Cache the result for next time:client.set('some_key', result)print(result)
查询FallbackClient
传递给其构造函数的旧缓存,并遵守顺序。在这种情况下,将始终首先查询新缓存服务器,如果发生缓存未命中,则将查询旧缓存服务器 — 避免可能返回到主要数据源。
如果设置了任何键,则只会将其设置为新缓存。一段时间后,旧缓存可以退役,并FallbackClient
可以直接通过客户端进行替换new_cache
。
检查并设置
与远程缓存通信时,常见的并发问题又出现了:可能有多个客户端同时尝试访问同一个键。memcached提供了检查和设置操作(缩写为CAS),可帮助解决此问题。
最简单的例子是某个应用程序想要计算其用户数。每次有访问者连接时,计数器就加 1。使用memcached,一个简单的实现如下:
def on_visit(client):result = client.get('visitors')if result is None:result = 1else:result += 1client.set('visitors', result)
但是,如果应用程序的两个实例同时尝试更新该计数器会发生什么情况?
第一次调用client.get('visitors')
将返回两个函数相同的访客数量,比如说 42。然后两个函数都会加 1,计算出 43,并将访客数量设置为 43。这个数字是错误的,结果应该是 44,即 42 + 1 + 1。
为了解决这个并发问题, memcached的 CAS 操作很方便。以下代码片段实现了一个正确的解决方案:
def on_visit(client):while True:result, cas = client.gets('visitors')if result is None:result = 1else:result += 1if client.cas('visitors', result, cas):break
该gets
方法返回值,就像get
方法一样,但它还返回一个CAS 值。
此值中的内容无关紧要,但它可用于下一次方法cas
调用。此方法相当于set
操作,但如果操作后值已更改,则该方法会失败gets
。如果成功,则循环中断。否则,操作将从头开始重新启动。
在应用程序的两个实例尝试同时更新计数器的情况下,只有一个实例成功将计数器从 42 移动到 43。第二个实例获取False
调用返回的值client.cas
,并且必须重试循环。它这次将检索 43 作为值,将其增加到 44,并且它的cas
调用将成功,从而解决了我们的问题。
增加计数器作为解释 CAS 工作原理的一个有趣示例,因为它很简单。但是,memcached还提供了incr
和decr
方法来在单个请求中增加或减少整数,而不是进行多次gets
/cas
调用。在实际应用中gets
,和cas
用于更复杂的数据类型或操作
大多数远程缓存服务器和数据存储都提供此类机制来防止并发问题。了解这些情况对于正确使用其功能至关重要。
最后
本文介绍的简单技巧向您展示了利用memcached来加速 Python 应用程序的性能是多么容易。
只需使用两个基本的“set”和“get”操作,您通常就可以加速数据检索或避免一遍又一遍地重新计算结果。使用 memcached,您可以在大量分布式节点之间共享缓存。
您在本教程中看到的其他更高级的模式,例如检查和设置(CAS)操作,允许您跨多个 Python 线程或进程同时更新存储在缓存中的数据,同时避免数据损坏。