Python + Memcached -- 在分布式应用中高效缓
Python + Memcached -- 在分布式应用中高效缓存
编写Python应用程序时,缓存很重要。使用缓存来避免重新计算数据或访问慢速数据库可以为您提供出色的性能提升。
Python提供了内置的缓存的可行方式,从简单的字典到更完整的数据结构,如functools.lru_cache
。后者可以使用最近最少使用的算法来缓存任何项目以限制高速缓存大小。
但是,这些数据结构的定义是Python进程的本地定义。当应用程序的多个副本在大型平台上运行时,使用内存数据结构不允许共享缓存的内容。对于大规模和分布式应用程序,这可能是一个问题。
python-memcached.png因此,当系统分布在网络上时,它还需要在网络上运行的缓存。如今,有许多网络服务器提供缓存功能 - 例如,Redis。
正如您将在本教程中看到的那样,memcached是缓存的另一个很好的选择。在快速介绍基本的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字典不同的属性,主要是:
- 键和值必须是字节(bytes)
- 键和值在过期后自动删除
因此,与memcached交互的两个基本操作是set
和get
。正如您可能已经猜到的那样,它们分别用于为键指定值或从键中获取值。
我首选的与memcached交互的Python库是pymemcache
- 我推荐使用它。你可以使用pip简单地安装它:$ pip install pymemcache
以下代码显示了如何连接到memcached并将其用作Python应用程序中的网络缓存:
>>> from pymemcache.client import base
# 在运行下一行之前,不要忘记运行“memcached”:
>>> client = base.Client(('localhost', 11211))
# 一旦客户端被实例化,您就可以访问缓存:
>>> client.set('some_key', 'some value')
# 再次检索以前设置的数据:
>>> client.get('some_key')
# 'some value'
memcached网络协议非常简单,它的实现速度非常快,这使得存储数据很有用,否则这些数据从规范的数据源检索起来很慢或者需要再次计算.
虽然很简单,但这个示例允许在整个网络中存储键/值元组,并通过应用程序的多个分布式运行副本访问它们。这很简单,但功能强大。这是优化应用程序的第一步。
自动使缓存数据过期
将数据存储到memcached时,您可以设置过期时间 - memcached保持密钥和值的最大秒数。在该延迟之后,memcached会自动从缓存中删除密钥。
你应该把这个缓存时间设置为什么?此延迟没有神奇的数字,它将完全取决于您正在使用的数据类型和应用程序。它可能是几秒钟,也可能是几个小时。
缓存失效(Cache invalidation),定义何时删除缓存,因为它与当前数据不同步,也是您的应用程序必须处理的内容。特别是如果要避免呈现太旧或过时的数据。
在这里,没有神奇的食谱; 这取决于您正在构建的应用程序的类型。但是,有几个应该处理的外围情况 - 我们尚未在上面的例子中介绍过。
缓存服务器无法无限增长 - 内存是一种有限的资源。因此,一旦需要更多空间来存储其他内容,密钥将被缓存服务器刷新。
某些密钥也可能已过期,因为它们已达到过期时间(有时也称为“生存时间”或TTL)。在这些情况下,数据会丢失,并且必须再次查询规范数据源。
这听起来比实际更复杂。在Python中使用memcached时,通常可以使用以下模式:
from pymemcache.client import base
def do_some_query():
# 替换为实际查询数据库或者执行远程REST API调用的代码
return 42
# 在运行下一行之前,不要忘记运行“memcached”:
client = base.Client(('localhost', 11211))
result = client.get('some_key')
if result is None:
# 该缓存过期, 需要从对应源get对应value
result = do_some_query()
# 为下一次缓存结果:
client.set('some_key', result)
# 无论是否需要更新缓存,
# 此时您都可以处理存储在“result”变量中的数据:
print(result)
注意:由于正常的清除操作,处理缺少的键是必需的。另外还必须处理冷缓存场景,即当memcached刚刚启动时。在这种情况下,缓存将完全为空,需要一次一个请求地完全重新填充缓存,。
这意味着您应该将任何缓存的数据视为短暂的。而且你永远不应该期望缓存包含你之前写入它的值。
预热冷缓存
某些冷缓存场景无法避免,例如memcached崩溃。但有些可以,例如迁移到新的memcached服务器。
当可以预测将发生冷缓存时,最好避免它。需要重新填充的缓存意味, 突然间,缓存数据的标准存储将受到所有缺少缓存数据的缓存用户的大量命中(也称为雷鸣群问题)。需要重新填充的缓存意味着,。
pymemcache提供了一个名为FallbackClient
的类,有助于实现此场景,如下所示:
from pymemcache.client import base
from pymemcache import fallback
def do_some_query():
# 替换为实际查询数据库或者执行远程REST API调用的代码
return 42
# 如果有必要的话
# 设置'ignore_exc=True',
# 这样就可以在从程序中删除旧缓存的使用之前关闭它。
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:
# 缓存为空, 需要从对应源里获取数据
result = do_some_query()
# 为下一次缓存结果:
client.set('some_key', result)
print(result)
FallbackClient
根据顺序查询传递给其构造函数的旧缓存。在这种情况下,将始终首先查询新的缓存服务器,并且在缓存未命中的情况下,再查询旧的缓存服务器 - 避免直接返回到主要数据源。
如果新set了任何一个key,则只会将其set到新缓存。一段时间后,旧的缓存可以退役,并且FallbackClient
可以用new_cache
客户端替换。
检查并设置
与远程缓存通信时,会出现常见的并发问题:可能有多个客户端同时尝试访问同一个key。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.第二个实例获取调用client.cas
返回的值False
,并且必须重试循环。这次将检索43作为值,将其增加到44,并且其cas
调用将成功,从而解决我们的问题。
增加一个计数器很有意思,作为解释CAS如何工作的一个例子,因为它是简单的。然而,分布式缓存还提供了incr
和decr
方法,以在单个请求中递增/递减整数,而不是做多个gets
/ cas
调用。在实际应用程序中gets
,cas
用于更复杂的数据类型或操作
大多数远程缓存服务器和数据存储都提供了防止并发问题的机制。了解这些案例以正确使用其功能至关重要。
总结
本文中说明的简单技术向您展示了利用memcached来加速Python应用程序的性能是多么容易。
只需使用两个基本的“set”和“get”操作,您通常可以加速数据检索或避免重复重新计算结果。使用memcached,您可以在大量分布式节点上共享缓存。
您在本教程中看到的其他更高级的模式,如Check And Set(CAS)操作,允许您跨多个Python线程或进程同时更新存储在缓存中的数据,同时避免数据损坏。