The Django Book

第十三章 缓存机制

静态的网站的内容都是些简单的静态网页直接存储在服务器上,可以非常容易地达到非常惊人的访问量。但是动态网站因为是动态的,也就是说每次用户访问一个页面,服务器要执行数据库查询,启动模板,执行业务逻辑到最终生成一个你所看到的网页,这一切都是动态即时生成的。从处理器资源的角度来看,这是比较昂贵的。

对于大多数网络应用来说,过载并不是大问题。因为大多数网络应用并不是washingtonpost.com或Slashdot;它们通常是很小很简单,或者是中等规模的站点,只有很少的流量。但是对于中等至大规模流量的站点来说,尽可能地解决过载问题是非常必要的。这就需要用到缓存了。

缓存的目的是为了避免重复计算,特别是对一些比较耗时间、资源的计算。下面的伪代码演示了如何对动态页面的结果进行缓存。

given a URL, try finding that page in the cache
if the page is in the cache:
    return the cached page
else:
    generate the page
    save the generated page in the cache (for next time)
    return the generated page

为此,Django提供了一个稳定的缓存系统让你缓存动态页面的结果,这样在接下来有相同的请求就可以直接使用缓存中的数据,避免不必要的重复计算。另外Django还提供了不同粒度数据的缓存,例如:你可以缓存整个页面,也可以缓存某个部分,甚至缓存整个网站。

Django也和”上游”缓存工作的很好,例如Squid(http://www.squid-cache.org)和基于浏览器的缓存,这些类型的缓存你不直接控制,但是你可以提供关于你的站点哪部分应该被缓存和怎样缓存的线索(通过HTTP头部)给它们

继续阅读来研究如何使用Django的缓存系统。当你的网站变成象Slashdot的时候,你会很很高兴理解了这部分材料

设定缓存

缓存系统需要一些少量的设定工作,即你必需告诉它你的缓存数据在哪里—在数据库,文件系统或者直接在内存中,这是影响你的缓存性能的重要决定,是的,一些缓存类型要比其它的快,内存缓存通常比文件系统或数据库缓存快,因为前者没有访问文件系统或数据库的过度连接

你的缓存选择在你的settings文件的 CACHE_BACKEND 设置中,如果你使用缓存但没有指定 CACHE_BACKEND ,Django将默认使用 simple:/// ,下面将解释 CACHE_BACKEND 的所有可得到的值

内存缓冲

目前为止Django可得到的最快的最高效的缓存类型是基于内存的缓存框架Memcached,它起初开发来为LiveJournal.com处理高负荷并随后被Danga Interactive(http://www.danga.com)开源,它被Slashdot和Wikipedia等站点采用以减少数据库访问并极大的提升了站点性能

Memcached可以在http://danga.com/memcached/免费得到,它作为后台进程运行并分配一个指定数量的RAM.它能为你提供在缓存中*如闪电般快速的*添加,获取和删除任意数据,所有的数据直接存储在内存中,所以没有数据库和文件系统使用的过度使用

在安装了Memcached本身之后,你将需要安装Memcached Python绑定,它没有直接和Django绑定,这些绑定在一个单独的Python模块中,’memcache.py’,可以在http://www.djangoproject.com/thirdparty/python-memcached得到

设置 CACHE_BACKENDmemcached://ip:port/ 来让Django使用Memcached,这里的 ip 是Memcached后台进程的IP地址, port 则是Memcached运行所在的端口

在这个例子中,Memcached运行在本地主机 (127.0.0.1)上,端口为11211:

CACHE_BACKEND = 'memcached://127.0.0.1:11211/'

Memcached的一个极好的特性是它在多个服务器分享缓存的能力,这意味着你可以在多台机器上运行Memcached进程,程序将会把这组机器当作一个*单独的*缓存,而不需要在每台机器上复制缓存值,为了让Django利用此特性,需要在CACHE_BACKEND里包含所有的服务器地址并用分号分隔

这个例子中,缓存在运行在172.19.26.240和172.19.26.242的IP地址和11211端口的Memcached实例间分享:

CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11211/'

这个例子中,缓存在运行在172.19.26.240(端口11211),172.19.26.242(端口11212),172.19.26.244(端口11213)的Memcached实例间分享:

CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11212;172.19.26.244:11213/'

最后关于Memcached的是基于内存的缓存有一个重大的缺点,因为缓存数据只存储在内存中,则如果服务器死机的话数据会丢失,显然内存不是为持久数据存储准备的,Django没有一个缓存后端是用来做持久存储的,它们都是缓存方案,而不是存储.但是我们在这里指出是因为基于内存的缓存特别的短暂 .

数据库缓存

为了将数据库表作为缓存后端,需要在数据库中创建一个缓存表并将Django的缓存系统指向该表

首先,使用如下语句创建一个缓存用数据表:

python manage.py createcachetable [cache_table_name]

这里的[cache_table_name]是要创建的数据库表名,名字可以是任何你想要的,只要它是合法的在你的数据库中没有被使用,这个命令在你的数据库创建一个遵循Django的数据库缓存系统期望形式的单独的表.

一旦你创建了数据库表,设置你的CACHE_BACKEND设置为”db://tablename”,这里的tablename是数据库表的名字,在这个例子中,缓存表名为my_cache_table:

CACHE_BACKEND = 'db://my_cache_table'

数据库缓存后端使用你的settings文件指定的同一数据库,你不能为你的缓存表使用不同的数据库后端.

文件系统缓存

使用”file://“缓存类型作为CACHE_BACKEND并指定存储缓存数据的文件系统目录来在文件系统存储缓存数据.

例如,使用下面的设置来在/var/tmp/django_cache存储缓存数据:

CACHE_BACKEND = 'file:///var/tmp/django_cache'

注意例子中开头有三个前斜线,前两个是file://,第三个是目录路径的第一个字符,/var/tmp/django_cache,如果你使用Windows系统,把盘符字母放在file://后面,像这样:’file://c:/foo/bar‘.

目录路径应该是*绝对*路径,即应该以你的文件系统的根开始,你在设置的结尾放置斜线与否无关紧要.

确认该设置指向的目录存在并且你的Web服务器运行的系统的用户可以读写该目录,继续上面的例子,如果你的服务器以用户apache运行,确认/var/tmp/django_cache存在并且用户apache可以读写/var/tmp/django_cache目录

每个缓存值将被存储为单独的文件,其内容是Python的pickle模块以序列化(“pickled”)形式保存的缓存数据,每个文件的 文件名是缓存键,以规避开安全文件系统的使用

本地内存缓存

如果你想要内存缓存的速度优势但没有能力运行Memcached,可以考虑使用本地存储器缓存后端,该缓存是多线程和线程安全 的,但是由于其简单的锁和内存分配策略它没有Memcached高效

设置 CACHE_BACKENDlocmem:/// 来使用它,例如:

CACHE_BACKEND = 'locmem:///'

简易缓存(用于开发阶段)

可以通过配置 'simple:///' 来使用一个简单的单进程内存缓存,例如:

CACHE_BACKEND = 'simple:///'

这个缓存仅仅是将数据保存在进程内,因此它应该只在开发环境或测试环境中使用.

仿缓存(供开发时使用)

最后,Django提供一个假缓存的设置:它仅仅实现了缓存的接口而不做任何实际的事情

这是个有用的特性,如果你的线上站点使用了很多比较重的缓存,而在开发环境中却不想使用缓存,那么你只要修改配置文件,将 CACHE_BACKEND 设置为 'dummy:///' 就可以了,例如:

CACHE_BACKEND = 'dummy:///'

这样的结果就是你的开发环境没有使用缓存,而线上环境依然在使用缓存.

CACHE_BACKEND参数

每个缓存后端都可能使用参数,它们在CACHE_BACKEND设置中以查询字符串形式给出,合法的参数为:

timeout:用于缓存的过期时间,以秒为单位。这个参数默认被设置为300秒(五分钟)

max_entries : 对于simple, local-memory与database类型的缓存,这个参数是指定缓存中存放的最大条目数,大于这个数时,旧的条目将会被删除。这个参数默认是300.

cull_frequency :当达到 max_entries 的时候,被接受的访问的比率。实际的比率是 1/cull_frequency ,所以设置cull_frequency=2就是在达到 max_entries 的时候去除一半数量的缓存

cull_frequency 的值设置为 0 意味着当达到 max_entries 时,缓存将被清空。这将以很多缓存丢失为代价,大大提高接受访问的速度。这个值默认是3

在这个例子中, timeout 被设成 60

CACHE_BACKEND = "locmem:///?timeout=60"

而在这个例子中, timeout 设为 30max_entries400 :

CACHE_BACKEND = "locmem:///?timeout=30&max_entries=400"

其中,非法的参数与非法的参数值都将被忽略。

站点级 Cache

一旦你指定了”CACHE_BACKEND”,使用缓存的最简单的方法就是缓存你的整个网站。这意味着所有不包含GET或POST参数的页面在第一次被请求之后将被缓存指定好的一段时间(就是设置的timeout参数)。

要激活每个站点的cache,只要将``’django.middleware.cache.CacheMiddleware’`` 添加到 MIDDLEWARE_CLASSES 的设置里,就像下面这样:

MIDDLEWARE_CLASSES = (
    'django.middleware.cache.CacheMiddleware',
    'django.middleware.common.CommonMiddleware',
)

注意

关于 MIDDLEWARE_CLASSES 顺序的一些事情。请看本章节后面的MIDDLEWARE_CLASSES顺序部分。

然后,在你的Django settings文件里加入下面所需的设置:

CACHE_MIDDLEWARE_SECONDS :每个页面应该被缓存的秒数

  • CACHE_MIDDLEWARE_KEY_PREFIX :如果缓存被多个使用相同Django安装的网站所共享,那么把这个值设成当前网站名,或其他能代表这个Django实例的唯一字符串,以避免key发生冲突。如果你不在意的话可以设成空字符串。

缓存中间件缓存每个没有GET或者POST参数的页面,即如果用户请求页面并在查询字符串里传递GET参数或者POST参数,中间件将不会尝试得到缓存版本的页面,如果你打算使用整站缓存,设计你的程序时牢记这点,例如,不要使用拥有查询字符串的URLs,除非那些页面可以不缓存

缓存中间件( cache middleware)支持另外一种设置选项, CACHE_MIDDLEWARE_ANONYMOUS_ONLY 。如果你把它设置为“True”,那么缓存中间件就只会对匿名请求进行缓存, 匿名请求是指那些 没有登录的用户发起的请求。如果想取消用户相关页面(user-specific pages)的缓存,例如Djangos 的管理界面,这是一种既简单又有效的方法。另外,如果你要使用 CACHE_MIDDLEWARE_ANONYMOUS_ONLY 选项,你必须先激活 AuthenticationMiddleware 才行,也就是在你的配置文件 MIDDLEWARE_CLASSES 的地方, AuthenticationMiddleware 必须出现在 CacheMiddleware 前面。

最后,再提醒一下: CacheMiddleware 在每个 HttpResponse 中都会自动设置一些头部信息(headers)

  • 当一个新(没缓存的)版本的页面被请求时设置Last-Modified头部为当前日期/时间

  • 设置Expires头部为当前日期/时间加上定义的CACHE_MIDDLEWARE_SECONDS

  • 设置Cache-Control头部来给页面一个最大的时间—再一次,根据CACHE_MIDDLEWARE_SECONDS设置

视图级缓存

更加颗粒级的缓存框架使用方法是对单个视图的输出进行缓存。这和整站级缓存有一样的效果(包括忽略对有 GET 和 POST 参数的请求的缓存)。它应用于你所指定的视图,而不是整个站点。

完成这项工作的方式是使用 修饰器 ,其作用是包裹视图函数,将其行为转换为使用缓存。视图缓存修饰器称为 cache_page ,位于 django.views.decorators.cache 模块中,例如:

from django.views.decorators.cache import cache_page

def my_view(request, param):
    # ...
my_view = cache_page(my_view, 60 * 15)

It’s not my first time to go to see this website, i am visiting this web page dailly and obtain nice facts from here daily.

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request, param):
    # ...

cache_page 只接受一个参数:以秒计的缓存超时。在前例中, “my_view()” 视图的结果将被缓存 15 分钟。(注意:为了提高可读性,该参数被书写为 60 * 1560 * 15 将被计算为 900 ,也就是说15 分钟乘以每分钟 60 秒。)

和站点缓存一样,视图缓存与 URL 无关。如果多个 URL 指向同一视图,每个视图将会分别缓存。继续 my_view 范例,如果 URLconf 如下所示:

urlpatterns = ('',
    (r'^foo/(\d{1,2})/$', my_view),
)

那么正如你所期待的那样,发送到 /foo/1//foo/23/ 的请求将会分别缓存。但一旦发出了特定的请求(如: /foo/23/ ),之后再度发出的指向该 URL 的请求将使用缓存。

在 URLconf 中指定视图缓存

前一节中的范例将视图硬编码为使用缓存,因为 cache_page 在适当的位置对 my_view 函数进行了转换。该方法将视图与缓存系统进行了耦合,从几个方面来说并不理想。例如,你可能想在某个无缓存的站点中重用该视图函数,或者你可能想将该视图发布给那些不想通过缓存使用它们的人。解决这些问题的方法是在 URLconf 中指定视图缓存,而不是紧挨着这些视图函数本身来指定。

完成这项工作非常简单:在 URLconf 中用到这些视图函数的时候简单地包裹一个 cache_page 。以下是刚才用到过的 URLconf :

urlpatterns = ('',
    (r'^foo/(\d{1,2})/$', my_view),
)

以下是同一个 URLconf ,不过用 cache_page 包裹了 my_view

from django.views.decorators.cache import cache_page

urlpatterns = ('',
    (r'^foo/(\d{1,2})/$', cache_page(my_view, 60 * 15)),
)

如果采取这种方法, 不要忘记在 URLconf 中导入 cache_page .

低层次缓存API

有些时候,对整个经解析的页面进行缓存并不会给你带来太多,事实上可能会过犹不及。

比如说,也许你的站点所包含的一个视图依赖几个费时的查询,每隔一段时间结果就会发生变化。在这种情况下,使用站点级缓存或者视图级缓存策略所提供的整页缓存并不是最理想的,因为你可能不会想对整个结果进行缓存(因为一些数据经常变化),但你仍然会想对很少变化的部分进行缓存。

在像这样的情形下, Django 展示了一种位于 django.core.cache 模块中的简单、低层次的缓存 API 。你可以使用这种低层次的缓存 API 在缓存中以任何级别粒度进行对象储存。你可以对所有能够安全进行 pickle 处理的 Python 对象进行缓存:字符串、字典和模型对象列表等等;查阅 Python 文档可以了解到更多关于 pickling 的信息。)

下面是如何导入这个 API :

>>> 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 :

# Wait 30 seconds for 'my_key' to expire...

>>> cache.get('my_key')
None

>>> cache.get('some_unset_key')
None

我们不建议在缓存中保存 None 常量,因为你将无法区分所保存的 None 变量及由返回值 None 所标识的缓存未中。

cache.get() 接受一个 缺省 参数。其指定了当缓存中不存在该对象时所返回的值:

>>> cache.get('my_key', 'has expired')
'has expired'

要想一次获取多个缓存值,可以使用 cache.get_many() 。如果可能的话,对于给定的缓存后端, 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}

如果某个缓存关键字不存在或者已超时, 它将不会被包含在字典中。 下面是范例的延续:

>>> cache.get_many(['a', 'b', 'c', 'd'])
{'a': 1, 'b': 2, 'c': 3}

最后,你可以用 cache.delete() 显式地删除关键字。这是在缓存中清除特定对象的简单途径。

>>> cache.delete('a')

cache.delete() 没有返回值, 不管给定的缓存关键字对应的值存在与否, 它都将以同样方式工作。

上游缓存

目前为止,本章的焦点一直是对你 自己的 数据进行缓存。但还有一种与 Web 开发相关的缓存:由 上游 高速缓存执行的缓冲。有一些系统甚至在请求到达站点之前就为用户进行页面缓存。

下面是上游缓存的几个例子:

  • 你的 ISP (互联网服务商)可能会对特定的页面进行缓存,因此如果你向 http://example.com/ 请求一个页面,你的 ISP 可能无需直接访问 example.com 就能将页面发送给你。而 example.com 的维护者们却无从得知这种缓存,ISP 位于 example.com 和你的网页浏览器之间,透明地处理所有的缓存。

  • 你的 Django 网站可能位于某个 代理缓存 之后,例如 Squid 网页代理缓存 (http://www.squid-cache.org/),该缓存为提高性能而对页面进行缓存。在此情况下 ,每个请求将首先由代理服务器进行处理,然后仅在需要的情况下才被传递至你的应用程序。

  • 你的网页浏览器也对页面进行缓存。如果某网页送出了相应的头部,你的浏览器将在为对该网页的后续的访问请求使用本地缓存的拷贝,甚至不会再次联系该网页查看是否发生了变化。

上游缓存将会产生非常明显的效率提升,但也存在一定风险。许多网页的内容依据身份验证以及许多其他变量的情况发生变化,缓存系统仅盲目地根据 URL 保存页面,可能会向这些页面的后续访问者暴露不正确或者敏感的数据。

举个例子,假定你在使用网页电邮系统,显然收件箱页面的内容取决于登录的是哪个用户。如果 ISP 盲目地缓存了该站点,那么第一个用户通过该 ISP 登录之后,他(或她)的用户收件箱页面将会缓存给后续的访问者。这一点也不好玩。

幸运的是, HTTP 提供了解决该问题的方案。已有一些 HTTP 头标用于指引上游缓存根据指定变量来区分缓存内容,并通知缓存机制不对特定页面进行缓存。我们将在本节后续部分将对这些头标进行阐述。

使用 Vary 头标

Vary 头标定义了缓存机制在构建其缓存键值时应当将哪个请求头标考虑在内。例如,如果网页的内容取决于用户的语言偏好,该页面被称为根据语言而不同。

缺省情况下,Django 的缓存系统使用所请求的路径(比如: "/stories/2005/jun/23/bank_robbed/" )来创建其缓存键。这意味着对该 URL 的每个请求都将使用同一个已缓存版本,而不考虑 cookies 或语言偏好之类的 user-agent 差别。然而,如果该页面基于请求头标的区别(例如 cookies、语言或者 user-agent)产生不同内容,你就不得不使用

Vary 头标来通知缓存机制:该页面的输出取决于这些东西。

要在 Django 完成这项工作,可使用便利的 vary_on_headers 视图修饰器,如下所示:

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 头标(使用像 response['Vary'] = 'user-agent' 之类的代码)的好处是修饰器在(可能已经存在的) Vary 之上进行 添加 ,而不是从零开始设置,且可能覆盖该处已经存在的设置。

你可以向 vary_on_headers() 传入多个头标:

@vary_on_headers('User-Agent', 'Cookie')
def my_view(request):
    # ...

该段代码通知上游缓存对 两者 都进行不同操作,也就是说 user-agent 和 cookie 的每种组合都应获取自己的缓存值。举例来说,使用 Mozilla 作为 user-agent 而 foo=bar 作为 cookie 值的请求应该和使用 Mozilla 作为 user-agent 而 foo=ham 的请求应该被视为不同请求。

由于根据 cookie 而区分对待是很常见的情况,因此有 vary_on_cookie 修饰器。以下两个视图是等效的:

@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 。该函数设置或增加 Vary header ,例如:

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 实例为第一个参数,以一个大小写不敏感的头标名称列表或元组为第二个参数。

其它缓存头标

关于缓存剩下的问题是数据的私隐性以及关于在级联缓存中数据应该在何处储存的问题。

通常用户将会面对两种缓存:他或她自己的浏览器缓存(私有缓存)以及他或她的提供者缓存(公共缓存)。公共缓存由多个用户使用,而受其他某人的控制。这就产生了你不想遇到的敏感数据的问题,比如说你的银行账号被存储在公众缓存中。因此,Web 应用程序需要以某种方式告诉缓存那些数据是私有的,哪些是公共的。

解决方案是标示出某个页面缓存应当是私有的。要在 Django 中完成此项工作,可使用 cache_control 视图修饰器:

from django.views.decorators.cache import cache_control

@cache_control(private=True)
def my_view(request):
    # ...

该修饰器负责在后台发送相应的 HTTP 头标。

还有一些其他方法可以控制缓存参数。例如, HTTP 允许应用程序执行如下操作:

  • 定义页面可以被缓存的最大次数。

  • 指定某个缓存是否总是检查较新版本,仅当无更新时才传递所缓存内容。(一些缓存即便在服务器页面发生变化的情况下都可能还会传送所缓存的内容,只因为缓存拷贝没有过期。)

在 Django 中,可使用 cache_control 视图修饰器指定这些缓存参数。在本例中, 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() 中,任何有效 Cache-Control HTTP 指令都是有效的。以下是一个完整的清单:

  • 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 指令的相关解释, 可以查阅 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 的规范文档。

注意

缓存中间件已经使用 CACHE_MIDDLEWARE_SETTINGS 设置设定了缓存头标 max-age 。如果你在 cache_control 修饰器中使用了自定义的 max_age ,该修饰器将会取得优先权,该头标的值将被正确地被合并。)

其他优化

Django 带有一些其它中间件可帮助您优化应用程序的性能:

  • django.middleware.http.ConditionalGetMiddleware 为现代浏览器增加了有条件地 GET 基于 ETagLast-Modified 头标的响应的相关支持。

  • django.middleware.gzip.GZipMiddleware 为所有现代浏览器压缩响应内容,以节省带宽和传送时间。

MIDDLEWARE_CLASSES 的顺序

如果使用缓存中间件,一定要将其放置在 MIDDLEWARE_CLASSES 设置的正确位置,因为缓存中间件需要知道用于产生不同缓存存储的的头标。

CacheMiddleware 放置在所有可能向 Vary 头标添加内容的中间件之后,包括下列中间件:

  • 添加 CookieSessionMiddleware

  • 添加 Accept-EncodingGZipMiddleware ,

接下来?

Django 带有一些功能包装了一些很酷的,可选的特色. 我们已经讲了一些: admin系统(第6章)和session/user框架(第11章).

下一章中,我们将讲述Django中其他的子框架,将会有很多很酷的工具出现,你一定不想错过它们。

Copyright 2006 Adrian Holovaty and Jacob Kaplan-Moss.
This work is licensed under the GNU Free Document License.
Hosting graciously provided by media temple
Chinese translate hosting by py3k.cn. 粤ICP备16122281号-1