使用Django.core.cache操作Memcached导致性能不稳定的分析过程
最近测试一项目,用到了Nginx缓存服务,那可真是快啊!2Gb带宽都轻易耗尽。
不过Api接口无法简单使用Nginx缓存,使用Memcached作二级缓存。但发现性能非常之不稳定,最终发现问题出在Memcached上。大压力时Memcached无法连接,即使使用Telnet也连接超时/连接被拒绝。
与开发沟通后发现用的django.core.cache操作Memcached,于是要求使用其它库取代,选中python-memcached库(memcache.Client)。
问题迎刃而解。为了进一步确定问题的根源,专门针对这两个库进行了对比测试。在测试过程监控Memcached的各指标,然后进行对比。
对比Memcached指标“get_hits/second”和“total_connections/second”直揭问题根源:
上图说明的问题是:
1、使用django.core.cache库操作Memcached时,创建的连接数和查询数相当(基本是完全相等的,为了不两曲线重叠,给查询数 + 50)。
2、 使用memcache.Client则创建连接的曲线是平稳的,即创建连接的速度是稳定的。
那么:为什么会导致Memcached无法连接呢?因为Memcached启动时可设定最大连接数,大压力下创建连接数超过限制自然无法连接。
虽然调大Memcached可接受的连接数可以缓解问题,但这种解决办法明显不靠谱!
所以,抛弃Django吧!Django确实提高开发速度,但易用性背后损失了性能。
在这里把验证过程也贴出来,有兴趣的朋友可以自己抢建测试环境进行验证:
/usr/local/python/bin/easy_install django
/usr/local/python/bin/easy_install python-memcached
cd /data/apps/
/usr/local/python/bin/django-admin.py startproject higkoo
# nginx.conf
location / {
fastcgi_pass 127.0.0.1:8950;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param PATH_INFO $fastcgi_script_name;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
} # higkoo/settings.py 加入Memcached的配置
CACHE_BACKEND = 'memcached://127.0.0.1:11211/'
# 新增文件 higkoo/views.py
from django.http import HttpResponse
from django.core.cache import cache
import memcache
mc = memcache.Client(['127.0.0.1:11211'], debug=0)
def cache1(request):
data = cache.get('test1KeyName')
if data is not None:
return HttpResponse(data)
data = 'You request test1 .'
cache.set('key1', data)
return HttpResponse(data)
def cache2(request):
data = mc.get('test2KeyName')
if data is not None:
return HttpResponse(data)
data = 'You request test2 .'
mc.set('key2', data)
return HttpResponse(data)
# 在 higkoo/urls.py 添加程序入口
(r'^cache1/', 'views.cache1'),
(r'^cache2/', 'views.cache2'),
# 启动 Python fastcgi
/usr/local/python/bin/python2.5 /data/apps/higkoo/manage.py runfcgi host=127.0.0.1 port=8950 pidfile=/tmp/higkoo.pid
# 启动 Memcached
/usr/local/memcached/bin/memcached -d -m 1024 -u root -p 11211 -c 1024 -P /tmp/memcached.pid
# 启动 Nginx
/usr/local/nginx/sbin/nginx -c /data/apps/higkoo/nginx.conf
# 访问方式:
http://192.168.10.31/cache1
http://192.168.10.31/cache2
OK:cache1接口是Django的,cache2接口是Python-Memcached的。
摘自:http://www.9say.com/2009/01/django-cache-framework/
翻译:django 缓存框架(1)(django cache framework)
译者注:1、无用的,吹嘘的说辞不翻译;2、意译,很多地方不准确。
动态网站最为重要的一点就是好,网页是动态的。每一次用户请求页面,网站就要进行各种计算——从数据库查询,到render模板,到各种逻辑运算——生成页面所需的。这个过程是异常消耗资源的,远远比从硬盘读取一个文件然后显示出来的代价高昂。
对于大多数中小网站来说,这也许不是问题,因为他们的访问量不大,而对于大型网站而言,必须尽量减少不必要的服务器资源开支。
因此,有了缓存技术。
缓存就是把一些需要消耗很多资源的计算结果保存下来,当下次需要时,不需要再次进行计算,直接从缓存中取,从而节省了资源。缓存的伪码表达如下:
给定的url,在缓存中查找是否存在;
如果存在,返回cached page;
否则:
生成该页面;
把该页面保存在缓存中
return 页面
django的缓存系统非常健壮…..(一些自夸的话,译者注)。简单的说,django提供不同粒度的缓存:特定view的缓存,缓存某个耗时的程序片段的结果,或者缓存整个网站。
django也能够和一些“上游的”缓存技术协同工作,例如squid ,浏览器的缓存等。这些缓存类型是你不需要直接控制,但是可以通过HTTP head 协议来告诉别人,网站的那些部分需要被缓存,如何缓存。
建立缓存
缓存系统(cache system)需要一些设置。你需要告诉django,你的缓存数据保存在什么地方:文件,数据库,或者内存中。不同的缓存方式的性能是不同的。
在settings.py文件中,设置CACHE_BACKEND, 下面是一些可用的CACHE_BACKEND值:
Memcached
目前最快的,效率最高的cache。
Memcached的官方网站:http://danga.com/memcached/ Memcached的所有数据都存在内存中,因此,不会有数据库消耗和读取文件消耗。
安装Memcached后,你需要安装Memcached python bindings。目前有两种版本可用,分别为:
速度最快的选择是cmemcache,下载地址为 http://gijsbert.org/cmemcache/
django 1.0之前版本不可用。
如果你无法安装 cmemcache, 你可以选择安装 python-memcached, 下载地址:ftp://ftp.tummy.com/pub/python-memcached/ . 如果这个网址已经失效,, 到memcached官方网站 (http://www.danga.com/memcached/) ,到“Client APIs”下载 Python bindings.
使用Memcached,只需把CACHE_BACKEND设置为memcached://ip:port/的格式,ip地址是memcached服务运行的机器ip地址,端口是memcached服务监听的端口号。
示例如下,如果memcached服务运行于本机,端口为11211,则配置语句为:
CACHE_BACKEND=’memcached://127.0.0.1:11211/’
memcached一个非常好的特性是可以多机共享cache,只需把所有提供缓存的机器的ip地址和端口号全部列入即可,之间用逗号分开。示例如下:
CACHE_BACKEND= ‘memcached://172.19.26.240:11211;172.19.26.242:11211/’
memcache也许有一个缺点,那就是由于memcached是基于内存的,当你的服务器crash时,所有的cache数据都将丢失。但由于cache本身就是临时的,所以,丢失cache数据没什么大问题。
翻译:django 缓存框架(2)(django cache framework)[转载]
在网络上搜索了一下,这篇文章已经有人翻译过了。
原文在这里:http://www.woodpecker.org.cn/obp/django/django-faq/cache.html
文件系统缓存
要在文件系统中保存缓存数据, 在 CACHE_BACKEND 中使用 “file://” 缓存类型. 下面这个例子将缓存数据保存在 /var/tmp/django_cache 中, 设置如下:
CACHE_BACKEND = ‘file:///var/tmp/django_cache’
注意 file: 之后是三个斜线而不是两个. 前两个是 file:// 协议, 第三个是路径 /var/tmp/django_cache 的第一个字符.
这个目录路径必须是绝对路径 — 也就是说,它应该从你的文件系统的根开始算起.至于路径的最后是否加一个斜线, Django 并不在意.
要确保这个设定的路径存在并且 Web 服务器可以读写.继续上面的例子,如果你的服务器以 apache 用户身份运行, 确保目录 /var/tmp/django_cache 存在并且可以被用户 apache 读写.
本地内存缓存
如果你想要内存缓存的高性能却没有条件运行 Memcached, 可以考虚使用本地内存缓存后端. 这个缓存后端是多进程的并且线程安全. 要使用它,设置 CACHE_BACKEND 为 “locmem:///”. 举例来说:
CACHE_BACKEND = ‘locmem:///’
简单缓存(用于开发)
“simple:///” 是一个简单的,单进程的内存缓存类型. 它仅仅在进程中保存缓存数据, 这意味着它仅适用于开发或测试环境. 例子:
CACHE_BACKEND = ’simple:///’
虚拟缓存 (用于开发)
最后, Django还支持一种 “dummy” 缓存(事实上并未缓存) — 仅仅实现了缓存接口但未实际做任何事.
CACHE_BACKEND 参数
所有缓存类型均接受参数. 提供参数的方式类似查询字符串风格. 下面列出了所有合法的参数:
timeout
默认的缓存有效时间,以秒计. 默认值是 300 秒(五分钟).
max_entries
用于 简单缓存 及 数据库缓存 后端, 缓存的最大条目数(超出该数旧的缓存会被清除,默认值是 300).
cull_percentage
当达到缓存的最大条目数时要保留的精选条目比率. 实际被保存的是 1/cull_percentage, 因此设置 cull_percentage=3 就会保存精选的 1/3 条目上,其余的条目则被删除.
如果将 cull_percentage 设置为 0 则意味着当达到缓存的最大条目数时整个缓存都被清除.当缓存命中率很低时这会 极大的 提高精选缓存条目的效率(根本不精选).
这个例子里, timeout 被设置为 60:
CACHE_BACKEND = “memcached://127.0.0.1:11211/?timeout=60″
这个例子, timeout 设置为 30 而 max_entries 设置为 400:
CACHE_BACKEND = “memcached://127.0.0.1:11211/?timeout=30&max_entries=400″
非法的参数会被忽略.
缓存整个站点
设置了缓存类型之后, 最简单使用缓存的方式就是缓存整个站点. 在“MIDDLEWARE_CLASSES“ 设置中添加 django.middleware.cache.CacheMiddleware , 就象下面的例子一样:
MIDDLEWARE_CLASSES = (
”django.middleware.cache.CacheMiddleware”,
”django.middleware.common.CommonMiddleware”,
)
( MIDDLEWARE_CLASSES 顺序相关. 参阅下文中的 “Order of MIDDLEWARE_CLASSES”)
然后,在 Django 设置文件中添加以下设置:
CACHE_MIDDLEWARE_SECONDS — 每个页面被缓存的时间.
CACHE_MIDDLEWARE_KEY_PREFIX — 如果缓存被同一个 Django 安装的多个站点共享, 在这里设置站点名字, 或者某些其它唯一的字符串, 以避免 key 冲突.如果你不介意,也可以使用空串.
缓存中间件缓存没有 GET/POST 参数的每个页面.另外, CacheMiddleware 自动在每个 HttpResponse 中设置一些 headers:
当请求一个未缓存页面时,设置 Last-Modified header 到当前的日期时间.
设置 Expires header 到当前日期时间加上定义的 CACHE_MIDDLEWARE_SECONDS.
设置 Cache-Control header 为该页面的生存期 — 该生存期也来自 CACHE_MIDDLEWARE_SECONDS 设置.
参阅 middleware documentation 了解中间件的更多信息.
缓存单个 view
Django 能够只缓存特定的页面. django.views.decorators.cache 定义了一个 cache_page 修饰符, 它能自动缓存该 view 的响应. 该修饰符的使用极为简单:
from django.views.decorators.cache import cache_page
def slashdot_this(request):
…
slashdot_this = cache_page(slashdot_this, 60 * 15)
或者, 使用Python 2.4 的修饰符语法:
@cache_page(60 * 15)
def slashdot_this(request):
…
cache_page 仅接受一个参数: 缓存有效期,以秒计. 在上面的例子里, slashdot_this() view 将被缓存 15 分钟.
底层缓存 API
某些时候, 缓存一个完整的页面不符合你的要求. 比如你认为仅有某些高强度的查询才有必要缓存其结果.要达到这种目的,你能使用底层缓存 API 来在任意层次保存对象到缓存系统.
缓存 API 是简单的. 从缓存模块 django.core.cache 导出一个由 CACHE_BACKEND 设置自动生成的 cache 对象:
>>> from django.core.cache import cache
基本的接口是 set(key, value, timeout_seconds) 和 get(key):
>>> cache.set(’my_key’, ‘hello, world!’, 30)
>>> cache.get(’my_key’)
‘hello, world!’
timeout_seconds 参数是可选的,其默认值等于 CACHE_BACKEND 的 timeout 参数.如果缓存中没有该对象, cache.get() 返回 None:
>>> cache.get(’some_other_key’)
None
# Wait 30 seconds for ‘my_key’ to expire…
>>> cache.get(’my_key’)
None
get() 可以接受一个 default 参数:
>>> cache.get(’my_key’, ‘has expired’)
‘has expired’
当然还有一个 get_many() 接口, 它仅仅命中缓存一次. get_many() 返回一个字典,包括未过期的实际存在的你请求的所有键.:
>>> cache.set(’a', 1)
>>> cache.set(’b', 2)
>>> cache.set(’c', 3)
>>> cache.get_many(['a', 'b', 'c'])
{’a': 1, ‘b’: 2, ‘c’: 3}
最后, 你可以使用 delete() 显式的删除键. 这是一个在缓存中清除特定对象的简便的方式:
>>> cache.delete(’a')
就是这样. 缓存机制限制非常少: 你可以安全的缓存能被 pickled 的任意对象(key必须是字符串).
Upstream caches
到现在为止, 我们的目光仅仅聚焦在 你自己的 数据上. 在 WEB 开发中还有另外一种类型的缓存:
“upstream” 缓存. 这是用户请求还未抵达你的站点时由浏览器实施的缓存.
下面是 upstream 缓存的几个例子:
你的 ISP 会缓存特定页面, 当你请求 somedomain.com 的一个页面时, 你的 ISP 会直接发送给你一个缓存页.(不访问 somedomain.com ).
你的 Django Web 站点可能建立在一个 Squid (http://www.squid-cache.org/) Web 代理服务器之后, 它会缓存页面以提高性能. 这种情况下,每个请求会先经 Squid 处理, 仅在需要时它会将请求传递给你的应用程序.
你的浏览器也会缓存一些页面. 如果一个 WEB 页发送了正确的 headers, 浏览器会用本地(缓存的)拷贝来回应后发的同一页面的请求.
Upstream 缓存是一个非常有效的推进, 不过它也有相当不足之处: 很多 WEB 页基于授权及一堆变量, 而这个缓存系统盲目的单纯依赖 URL 缓存页面, 这可能会对不适当的用户泄露敏感信息.
举例来说,假设你使用一个 Web e-mail 系统, “inbox” 页的内容显然依赖当前登录用户. 如果一个 ISP 盲目的缓存了你的站点, 后来的用户就会看到前一用户的收件箱, 这可不是一件有趣的事.
幸运的是, HTTP 提供了一个该问题的解决方案: 用一系列 HTTP headers 来构建缓存机制以区分缓存内容, 这样缓存系统就不会缓存某些特定页.
使用 Vary headers
其中一个 header 就是 Vary. 它定义了缓存机制在创建缓存 key 时的请求 headers. 举例来说, 如果一个网页的内容依赖一个户的语言设置, 则该网页被告知 “vary on language.”
默认情况, Django 的缓存系统使用请求路径创建 缓存 key — 比如, ”/stories/2005/jun/23/bank_robbed/”. 这意味着该 URL 的每个请求使用相同的缓存版本, 不考虑用户代理的不同.(cookies 及语言特性).
因此我们需要 Vary .
如果你的基于 Django 的页面根据不同的请求 headers 输出不同的内容 — 比如一个 cookie, 或语言, 或用户代理 — 你会需要使用 Vary header 来告诉缓存系统这个页面输出依赖这些东西.
要在 Django 中做到这一步, 使用 vary_on_headers view 修饰符,就象下面这样:
from django.views.decorators.vary import vary_on_headers
# Python 2.3 syntax.
def my_view(request):
…
my_view = vary_on_headers(my_view, ‘User-Agent’)
# Python 2.4 decorator syntax.
@vary_on_headers(’User-Agent’)
def my_view(request):
…
这样缓存系统 (比如 Django 自己的缓存中间件) 会为不同的用户代理缓存不同的版本的页面.使用 vary_on_headers 修饰符的优势在于(与人工设置 Vary header 相比:使用类似 response['Vary'] = ‘user-agent’)修饰符会添加到 Vary header (可能已存在) 而不是覆盖掉它.
你也可以传递多个 header 给 vary_on_headers():
@vary_on_headers(’User-Agent’, ‘Cookie’)
def my_view(request):
…
由于多个 cookie 的情况相当常见, 这里有一个 vary_on_cookie 修饰符. 下面两个 views 是等价的:
@vary_on_cookie
def my_view(request):
…
@vary_on_headers(’Cookie’)
def my_view(request):
…
需要注意一点传递给 vary_on_headers 的参数是大小写不敏感的. “User-Agent” 与 “user-agent” 完全相同.
你也可以直接使用一个帮助函数, django.utils.cache.patch_vary_headers:
from django.utils.cache import patch_vary_headers
def my_view(request):
…
response = render_to_response(’template_name’, context)
patch_vary_headers(response, ['Cookie'])
return response
patch_vary_headers 接受一个 HttpResponse 实例作为它的第一个参数及一个 header 名字的列表或tuple作为第二个参数.
要了解 Vary headers 的更多信息, 参阅 official Vary spec.
控制缓存: 使用其它 headers缓存的另一个问题是数据的私密性及瀑布缓存模式下数据保存到哪里.
用户经常要面对的有两种缓存: 他自己的浏览器缓存(私人缓存) 及站点提供的缓存(公开缓存).一个公开缓存用于多用户情况, 其内容由另外的人控制. 这造成了数据的私密性问题: 你当然不想你的银行帐号保存在公共缓存里. 因此应用程序需要一种方式告诉缓存系统哪些东西是私密的,哪些则是公开的.
解决方案就是声明某个页面的缓存是 “私密的”. 在 Django 中, 使用 cache_control view 修饰符. 例子:
from django.views.decorators.cache import cache_control
@cache_control(private=True)
def my_view(request):
…
这个修饰符会在幕后谨慎的发送适当的 HTTP header 发避免上面的问题.
还有一些其它的方式控制缓存参数. 举例来说,HTTP 允许应用程序做以下事:
定义一个页面被缓存的最大时间.指定缓存是否需要总是检查新版本, 如果没有变化则仅传送缓存版本. (某些缓存即使服务器端页面变化也仅传递缓存版本–仅仅因为缓存拷贝尚未到期).
在 Django 中, 使用 cache_control view 修饰符指定缓存参数.在这个例子里 cache_control 通知缓存每次检验缓存版本, 直到 3600 秒到期:
from django.views.decorators.cache import cache_control
@cache_control(must_revalidate=True, max_age=3600)
def my_view(request):
…
所有合法的 Cache-Control HTTP 指令在 cache_control() 中都是合法的. 下面是完整的指令列表:
public=True
private=True
no_cache=True
no_transform=True
must_revalidate=True
proxy_revalidate=True
max_age=num_seconds
s_maxage=num_seconds
要了解 Cache-Control HTTP 指令的细节,参阅 Cache-Control spec.
(注意缓存中间件已经通过设置中的 CACHE_MIDDLEWARE_SETTINGS 的值设定了缓存 header 的 max-age. 如果你在 cache_control 修饰符中使用了自定义的 max_age , 修饰符中的设置将被优先使用, header 值会被正确的合并)
其它优化
Django 自带了一些中间件以帮助你提高站点性能:
django.middleware.http.ConditionalGetMiddleware 添加了有条件GET的支持. 它利用了 ETag 及 Last-Modified headers.
django.middleware.gzip.GZipMiddleware 为支持 Gzip 的浏览器对发送内容进行压缩(所有流行浏览器均支持).
MIDDLEWARE_CLASSES 顺序
如果你使用了 CacheMiddleware, 在 MIDDLEWARE_CLASSES 设置中使用正确顺序非常重要. 由于缓存中间件需要知道哪些 headers 由哪些缓存存储.中间件总是在可能的情况下添加某些东西到 Vary 响应 header.
将 CacheMiddleware 放到其它可能添加某些东西到 Vary Header的中间件之后,下面的中间件会添加东西到 Vary header:
SessionMiddleware 添加了 Cookie
GZipMiddleware 添加了 Accept-Encoding