当Google测试了Google Search服务的可用性后,发现速度是最影响Web应用的可用性的因素之一。相对于作用相同但是速度慢的应用,用户更喜欢速度快的应用。多来年,Google已经掌握了如何使应用运行更快,甚至是有上百万的用户访问这个应用。提高Web应用的速度的一个方法就是为存储使用分布式的内存缓存,而不是从磁盘中获取经常用的数据。
当你将信息放到DataStore上的时候,你的信息被永久地保存了,被保存在云中某个地方的磁盘中,甚至你的应用不在使用,也会被备份和维护。
内存缓存遍布于云中你的应用的所有的实例的内存中。也可能有专用的内存缓存服务器。因为内存缓存是临时的,没有必要把它保存到磁盘或备份。这就使得内存缓存比Datastore更快。
内存缓存基于一个叫做“memcached”的产品,它提供了一个多语言的高速的分布式缓存(in a number of languages)。memcached软件使用网络来保证数据的拷贝的同步。当数据在一个服务器上被更新了,这个更新就会被传递给参与了内存缓存的所有的其他服务器:http://www.danga.com/memcached/.
内存缓存的规则
内存缓存最基本的规则是在内存缓存中没有任何东西是被保证的。当被请求时,内存缓存尽它最大的努力返回你的数据。当你将数据放在缓存中时,你会指定想要数据在缓存中保持多久。比如,你可以要求的数据被保存一个小时。然而,如果你的应用实例在内存中运行缓慢,它们可能在几秒内扔掉你的数据来释放内存。
这种机制初看使得内存缓存几乎没有用。然而,如果你的应用在内存中运行不慢,内存缓存将会丢掉你的数据的可能性就很小。允许在一个特殊的时刻(once in a great while)丢掉数据使得内存缓存非常的快。这也意味着对于真正重要的数据,你不能只在内存缓存中存储一个备份。你必须在Datastore中也保留一个备份。
结果对于一个可扩展的Web应用中的大量的数据,我们很高兴接受对这个内存缓存的妥协。如果数据偶尔丢失的话,数据元素可以被重建。也有一些数据比如登录的用户的数量,没有必要是精确的。
其他的内存缓存的规则如下:
・有一个内存缓存在你的应用实例之间共享。如果你在一个实例中存储了数据,这个数据对于所有的应用都是可用的。如果你从一个实例中删除数据,它将会在所有的实例中被删除。
・内存缓存操作起来就像一个巨大的分布式的Python字典。缓存中的每一个项目有一个唯一键。
・缓存中的每一个项目有一个失效时间,可以是几秒甚至是一个月。你可以忽略这个生命周期,缓存将尽它的可能保持这个对象。在内存缓存中典型的生命周期是大约10s到几个小时。你可以在缓存中替换这个对象并指定一个新的生命周期来延长生命周期。
将有意义的项目放到内存缓存中。它的大小是有限制的,所以你应当将适当的、小的、经常被使用的数据放到内存缓存中。
使用内存缓存
使用内存缓存是相当直接的。就像对待一个真的、大的Python字典一样对待它。这是一个简单的使用内存缓存的例子。
注意在花括号的值是Python字典常量。这些常量包括字典中的键值对。
from google.appengine.api import memcache x=memcache.get("")
if x is None:
print "Nothing found in key 1234"
else:
print "Found key 1234"
print x x=memcache.get("")
if x is None:
print "Nothing found in key 7890"
else:
print "Found key 7890"
print x y={'a':'hello','b':'world'}
memcache.add("",y,3600)
x=memcache.get("")
if x is None:
print "Nothing found in key 7890"
else:
print "Found key 7890"
print x z={'l':'more','n':'stuff'}
memcache.replace("",y,3600)
print "7890 replaced"
第一次运行这个程序,输出的结果如下:
Nothing found in key 1234
Nothing found in key 7890
Found key 7890
{'a':'hello','b':'world'}
7890 replaced
一会儿之后再次运行这个程序,输出的结果如下:
Nothing found in key 1234
Found key 7890
{'a': 'hello', 'b': 'world'}
Found key 7890
{'a': 'hello', 'b': 'world'}
7890 replaced
第二次程序运行的时候,存储在7890Key的数据还在,是前一次执行的结果。内存缓存会一直存在,直到你的应用重启,项目过期,明确地清除缓存,你的应用在内存中开始运行得缓慢等情况发生。
使用应用控制台测试内存缓存
你有可能想知道你的内存缓存数据在哪儿运行。也许你可能要在App Engine上运行一个Python的片段(perhaps you might run a bit of Python code),而没有写HTML、index.py、一些模板、app.yaml、一些URL路由代码。
有一个不错的应用控制台,使你能够和你运行的应用交互(只要你是这个应用的管理员)。这个应用控制台的可用URL是:http://localhost:8080/_ah/admin。
这个交互控制台用来运行你的正在运行的应用中的代码片段的。当这个代码片段运行时,等价于在你的index.py文件中运行。如果在这个交互应用中执行的代码对内存缓存做了一些改变,它也就真的被改变了。
这个应用控制台有一个Datastore Viewer和一个内存缓存Viewer。
使用内存缓存Viewer,我们可以利用key来查找一个内存项目。我们必须提前知道这个key,因为我们不能看到所有的key。这个Viewer也告诉了我们一些关于缓存的本质和性能。可以看到在缓存中的一个项目占用了多少字节,我们的命中率是多少。
这个命中率测量我们向缓存发出一个get请求后,能够找到了这个项目的次数,以及我们没有找到这个项目的次数。你可以使用命中率来衡量存储在缓存中的数据被再使用的频率。通常所希望的模式是将某些内容放到缓存中,多次使用它而不用从速度慢的数据源再次获取数据来节约时间。
在你的应用中使用内存缓存
最常用到内存缓存的地方是多人的RSS阅读器,缓存RSS feed retrieval的一个备份,那么第一个用户访问这个feed时会有获取数据到应用的延迟。接下来几分钟内访问相同RSS feed的用户直接从内存缓存中获得feed结果。
RSS feed的URL就是原始的缓存项目的key,持有这个RSS feed返回的结果。
一段时间之后,这个RSS结果就过期了,下一个用户请求这个feed时,应用会检查这个缓存。由于这个缓存不在那儿了,这个RSS feed再次重读并放到缓存中。
正确地使用内存缓存可以提高应用的性能,提高用户的体验,极大地减少应用的资源使用。
另一个常用到内存缓存的地方是预先提供完整的、将会被重复使用的一些页,在缓存中提供这些页而不是在每一个请求中动态重构这些页。缓存的数据会被合适地重建,所以用户看到新数据并不会意识到他们正在看的这份数据很有可能是几分钟之前的。
使用内存缓存创建Session Store
我们已经在本书的前面使用了session对象。在那个时候,我们简单地下载和使用了代码。现在我们来看看在第七章中使用的session对象的实现,并且尝试如何使用App Engine内存缓存,如何读和设置cookies,以及深入地看看Python是如何支持面向对象编程的。
尽管这个session对象的实现对于我们简单的开发者应用会工作很好,对于简单的小量的App Engine项目也工作得很好,然而对于易扩展的应用而言使用这种特别的实现不是个好主意。
因为在session数据的实现中我们的唯一的存储机制是内存缓存,如果我们的应用在内存中运行得很慢的话,我们的session就开始丢失。当一个session丢失了,我们的用户被随意地从应用中退出。更糟糕的是这个session系统会立即创建一个新的session,并保存在内存缓存中,这将会进一步恶化低内存(速度)问题。
这个session实现对一定数量的用户来讲是可扩展的(scale)。当很多用户同时使用这个应用的时候,这个应用在内存中将会运行很慢。这个session实现将会失败并抖动(thrashing)。但是我们的目标是理解内存缓存和sessions,所以此刻一个好的简单的可以99.9999%工作的实现符合我们的目标。
如果你对一个可扩展的健壮的session实现感兴趣,你可以上网查免费的session实现。下面的项目提供了一个session的实现,使用了内存缓存和Datastore:http://code.google.com/p/gaeutilities。
这个关于session对象的appengine多用途实现和这一章中的session对象有一个相同的名字,所以使用一个更加健壮可扩展的session实现,代替这一章中使用的session实现相对会简单些。
我们的session类都和字典有关。内存缓存是一个大的分布式字典,每一个session就是一个简单的键值对。session类使用浏览器中的cookies。整个流程如下(overall flow as follows):
・ 当我们收到一个HTTP请求的时候,我们查看这个请求中是否设定了一个session cookie。
・ 如果在这个请求中没有session cookie,我们就创建一个session并且将它存储在内存缓存中,使用像‘session-12345’这样的键。然后在输出的HTTP响应中添加cookie。
・ 如果在请求中有一个session cookie,我们就尝试使用来自cookie中的key(比如‘session-12345’)从内存缓存中获取这个session。如果匹配的session在内存缓存中,我们就使用这个session。
・ 如果有一个session cookie,但是在内存缓存中没有相应的session,我们就认为某个东西发生了错误,然后选择一个新的随机数创建一个新的session,将这个session保存在内存缓存中。将这个新的session ID作为输出响应中的cookie。
Session类的剩余部分提供了一些方法给我们的调用者来使用我们返回的session对象。
当在我们的应用中使用这个session对象时,我们做的第一件事就是使用下面的行来访问session。
class LogoutHandler(web.RequestHandler): def get(self):
self.session = Session()
self.session.delete_item('username')
doRender(self,'index.htm')
这行代码从Session类创建了一个session对象并将它存储在变量self.session中。
在Session类中,当session对象被创建的时候,它调用了类中一个叫做“构造器(constructor)”的特别方法。在这个类中,这个构造器被定义为一个叫做__init__()的方法。
Session类中的大部分工作是在这个类的构造器(__init()__方法)中实现的。
在构造器中三个代码分支:
a)有cookie,有session
b)有cookie,没有匹配的session
c)没有cookie
在分支b),c)中,我们创建了一个新的session,存储在缓存中并设定cookie。
第一步是使用os.environ.get()方法从请求头部获取cookie信息。然后我们使用Cookie.SimpleCookie类解析cookie头部并检查session cookie。
import Cookie class Session(object):
self.sid = None
self.key = None
self.session = None
string_cookie = os.environ.get('HTTP_COOKIE','')
self.cookie = Cookie.SimpleCookie()
self.cookie.load(string_cookie) # check for existing cookie
if self.cookie.get(COOKIE_NAME):
self.sid = sef.cookie[COOKIE_NAME].value
self.key = 'session-' + self.sid
try:
self.session = memcache.get(self.key)
except:
self.session = None
if self.session is None:
logging.info('Invalidating session '+self.sid)
self.sid = None
self.key = None # Make a new session and set the cookie
if self.session is None:
self.sid = str(random.random())[5:]
self.key = 'session-' + self.sid
logging.info('Creating session ' + self.key)
self.session = dict()
memcache.add(self.key,self.session,3600) self.cookie[COOKIE_NAME] = self.sid
self.cookie[COOKIE_NAME]['path'] = DEFAULT_COOKIE_PATH
#Send the Cookie header to the browser
print self.cookie
如果session cookie存在,我们尝试从内存缓存中获取存在的session。如果我们在内存缓存中发现了匹配的session,我们这个session作为我们的session。如果在内存缓存中没有匹配cookie值的session,我们创建一个新的session。
为了创建一个新的session,选择一个很大的随机数来作为新session的key。创建一个空的Python字典,然后使用新的session的key来在内存缓存中存储这个空的字典。
创建session的最后一步是在输出的HTTP响应中设置session cookie。print self.cookie这一行输出这个HTTP 响应头部如下:
Set-Cookie:appengine-simple-session-sid = 921288672590409739;Path=/
只要这一行出现在响应体的前面,浏览器将会把它作为头部并正确地设置cookie。这就是为什么在任何输出响应被发送给浏览器之前都要初期化这个session的重要原因。
在我们的应用的这块代码中,在Loginhandler()中我们创建并加载了session来作为post方法的首行。
class LoginHandler(webapp.RequestHandler): def post(self):
self.session = Session()
acct = self.request.get('accout')
pw = self.request.get('password')
logging.info('Checking account = ' + acct + ' pw= ' + pw)
在__init__方法之后,我们定义了一个方法,因此我们改变这个字典(self.session)的本地备份的任意时候,我们也在内存缓存中替换这个备份。
# Private method to update the cache on modification
def _update_cache(self):
memcache.replace(self.key,self.session,3600)
我们提供了一个叫做delete_item()的方便方法,在尝试删除和引用之前,检查key是否在session中。
# Convenient delete with no error method
def delete_item(self,keyname):
if keyname in self.session:
del self.session[keyname]
self._update_cache()
Session类的剩余部分就是一组通用的方法(utility method),允许用户对待字典对象一下对待session对象。比如,这个方法使我们在session对象上调用get():
# Support the dictionary get() method
def get(self,keyname,default=None):
if keyname in self.session:
return self.session[keyname]
return default
Python支持面向对象的编程的另一个方面就是允许类增加方法来支持某些句法模式(syntactic patterns)。比如,我们的session对象支持方括号查询操作,如下:
x=session['username']
Python看到这个语法将它装换为下面的代码:
__getitem__(session,'username')
因为我们想在我们的session对象中支持这种语法,在我们的类中添加了下面的方法:
# x = session[keyname]
def __getitem__(self,keyname):
if keyname in self.session:
return self.session[keyname]
raise keyError(str(keyname))
你可以查看我们的session.py源代码看看我们是如何提供这些方法的,使得我们的session对象支持字典对象的所有操作。我们的终端用户可以像使用Python字典一样使用我们的session对象。