Python + Memcached:分布式应用中的高效缓存
目录
在编写 Python 应用程序时,缓存很重要。使用缓存来避免重新计算数据或访问缓慢的数据库可以为您提供巨大的性能提升。
Python 为缓存提供了内置的可能性,从简单的字典到更完整的数据结构,如functools.lru_cache
. 后者可以使用最近最少使用算法缓存任何项目来限制缓存大小。
但是,根据定义,这些数据结构对于您的 Python 进程来说是本地的。当您的应用程序的多个副本在大型平台上运行时,使用内存中的数据结构不允许共享缓存的内容。这对于大规模和分布式应用程序来说可能是一个问题。
因此,当一个系统跨网络分布时,还需要一个跨网络分布的缓存。现在,有很多网络服务器提供缓存功能——我们已经介绍了如何使用 Redis 和 Django 进行缓存。
正如您将在本教程中看到的,memcached是分布式缓存的另一个很好的选择。在快速介绍了基本的 memcached 用法之后,您将了解高级模式,例如“缓存和设置”以及使用回退缓存来避免冷缓存性能问题。
安装内存缓存
Memcached的是可以在许多平台:
- 如果您运行Linux,则可以使用
apt-get install memcached
或安装它yum install memcached
。这将从预先构建的包中安装 memcached,但您也可以从源代码构建 memcached,如此处所述。 - 对于macOS,使用Homebrew是最简单的选择。
brew install memcached
安装 Homebrew 包管理器后运行即可。 - 在Windows 上,您必须自己编译 memcached 或查找预编译的二进制文件。
安装后,只需调用以下命令即可启动memcachedmemcached
:
$ memcached
在您可以从 Python-land 与 memcached 交互之前,您需要安装一个 memcached客户端库。您将在下一节看到如何做到这一点,以及一些基本的缓存访问操作。
使用 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网络协议真的很简单,它的实现速度非常快,这使得存储数据很有用,否则从规范数据源检索或再次计算会很慢:
虽然足够简单,但此示例允许跨网络存储键/值元组,并通过应用程序的多个分布式运行副本访问它们。这很简单,但功能强大。这是优化应用程序的重要第一步。
Automatically Expiring 缓存数据
将数据存储到memcached 时,您可以设置过期时间——memcached保留键和值的最大秒数。在此延迟之后,memcached 会自动从其缓存中删除密钥。
您应该将此缓存时间设置为什么?这种延迟没有神奇的数字,它完全取决于您正在使用的数据和应用程序的类型。可能是几秒钟,也可能是几个小时。
缓存失效定义了何时删除缓存,因为它与当前数据不同步,也是您的应用程序必须处理的事情。特别是如果要避免呈现太旧或过时的数据。
再一次,没有神奇的配方;这取决于您正在构建的应用程序类型。然而,有几个外围情况需要处理——我们在上面的例子中还没有涉及。
缓存服务器不能无限增长——内存是一种有限资源。因此,只要缓存服务器需要更多空间来存储其他东西,密钥就会被缓存服务器刷新。
一些键也可能因为它们达到了它们的过期时间(有时也称为“生存时间”或 TTL)而过期。在这些情况下,数据会丢失,必须再次查询规范数据源。
这听起来比实际复杂。在 Python 中使用 memcached 时,您通常可以使用以下模式:
from pymemcache.client import base
def 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刚刚启动时。在这种情况下,缓存将完全为空,缓存需要完全重新填充,一次一个请求。
这意味着您应该将任何缓存数据视为短暂的。并且您永远不应该期望缓存包含您之前写入的值。
Warming Up a Cold Cache
某些冷缓存场景无法避免,例如memcached崩溃。但是有些可以,例如迁移到新的memcached服务器。
当可以预测冷缓存场景会发生时,最好避免它。需要重新填充的缓存意味着突然之间,缓存数据的规范存储将被所有缺少缓存数据的缓存用户大量攻击(也称为雷鸣羊群问题)。
pymemcache提供了一个名为的类FallbackClient
,可帮助实现此场景,如下所示:
from pymemcache.client import base
from pymemcache import fallback
def 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 = 1
else:
result += 1
client.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 = 1
else:
result += 1
if 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
用于更复杂的数据类型或操作
大多数远程缓存服务器和数据存储都提供了这样一种机制来防止并发问题。了解这些情况以正确使用其功能至关重要。
Beyond Caching
本文中展示的简单技术向您展示了利用memcached来加速 Python 应用程序的性能是多么容易。
只需使用两个基本的“设置”和“获取”操作,您通常可以加速数据检索或避免一遍又一遍地重新计算结果。使用 memcached,您可以在大量分布式节点之间共享缓存。
您在本教程中看到的其他更高级的模式,例如Check And Set (CAS)操作,允许您跨多个 Python 线程或进程同时更新缓存中存储的数据,同时避免数据损坏。
- 点赞
- 收藏
- 关注作者
评论(0)