【问题标题】:CherryPy - Caching of static filesCherryPy - 缓存静态文件
【发布时间】:2023-06-29 05:49:01
【问题描述】:

我有一个提供大量静态内容的服务器。只要支持 gzip 内容,就会启用 CherryPy 工具 tools.gzip 来压缩文件。

问题:CherryPy 是在每次请求静态文件时对它们进行 gzip,还是对内容进行 gzip 一次并将该 gzip 后的副本提供给所有请求?

如果 CherryPy 当前每次请求文件时都会对文件进行 gzip 压缩,启用 tools.caching 会阻止这种情况,还是有更好的方法?

【问题讨论】:

    标签: python caching cherrypy


    【解决方案1】:

    首先,我想指出的是,尽管 HTTP 的广泛传播和每种语言的良好客户端库的存在导致 HTTP 看起来很容易,但 HTTP 实际上是一个复杂的协议,它涉及多个交互层。缓存也不例外,RFC 2616 Section 13。下面说的是Last-Modified/If-Modified-Since,因为ETaging with gzip就是another story

    设置

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    
    import os
    
    import cherrypy
    
    
    path   = os.path.abspath(os.path.dirname(__file__))
    config = {
      'global' : {
        'server.socket_host' : '127.0.0.1',
        'server.socket_port' : 8080,
        'server.thread_pool' : 8
      },
      '/static' : {
        'tools.gzip.on'       : True,
        'tools.staticdir.on'  : True,
        'tools.staticdir.dir' : os.path.join(path, 'static')
      }
    }
    
    
    if __name__ == '__main__':
      cherrypy.quickstart(config = config)
    

    然后在static目录下放一些纯文本或HTML文件。

    实验

    Firefox 和 Chromium 不会在第一次请求时发送与缓存相关的标头,即 GET /static/some.html

    Accept-Encoding: gzip, deflate
    Host: 127.0.0.1:8080
    

    回复:

    Accept-Ranges: bytes
    Content-Encoding: gzip
    Content-Length: 50950
    Content-Type: text/html
    Date: Mon, 15 Dec 2014 12:32:40 GMT
    Last-Modified: Wed, 22 Jan 2014 09:22:27 GMT
    Server: CherryPy/3.6.0
    Vary: Accept-Encoding
    

    在后续请求中,使用以下缓存信息 (Firebug) 避免任何网络:

    Data Size: 50950
    Device: disk
    Expires: Sat Jan 17 2015 05:39:41 GMT
    Fetch Count: 6
    Last Fetched: Mon Dec 15 2014 13:19:45 GMT
    Last Modified: Mon Dec 15 2014 13:19:44 GMT
    

    因为默认情况下 CherryPy 不提供过期时间(ExpiresCache-Control),Firefox(也可能是 Chromium)使用根据 RFC 2616 Section 13.2.4 的启发式:

    如果没有 Expires,Cache-Control: max-age 或 Cache-Control: s- maxage (参见章节 14.9.3) 出现在响应中,且响应中不包含其他限制 在缓存中,缓存可以使用启发式计算一个新鲜的生命周期......

    此外,如果响应确实具有 Last-Modified 时间,则启发式过期值 应该不超过自那时以来间隔的一部分。典型设置 这个比例可能是 10%。

    这是证明 Expires 值的启发式性质的代码:

    import email.utils
    import datetime
    
    s  = 'Wed, 22 Jan 2014 09:22:27 GMT'
    lm = datetime.datetime(*email.utils.parsedate(s)[0:6])
    
    print datetime.datetime.utcnow() + (datetime.datetime.utcnow() - lm) / 10
    

    当您刷新页面时,浏览器会将Cache-Control 附加到请求中:

    Accept-Encoding: gzip,deflate
    Cache-Control: max-age=0
    Host: 127.0.0.1:8080
    If-Modified-Since: Wed, 22 Jan 2014 09:22:27 GMT
    

    如果文件未更改,CherryPy 会回复 304 Not Modified。以下是它的工作原理:

    cherrypy.lib.static.serve_file

    def serve_file(path, content_type=None, disposition=None, name=None, debug=False):
        # ...
    
        try:
            st = os.stat(path)
        except OSError:
            if debug:
                cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
            raise cherrypy.NotFound()
    
        # ...
    
        # Set the Last-Modified response header, so that
        # modified-since validation code can work.
        response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
        cptools.validate_since()
    
        # ...
    

    cherrypy.lib.cptools.validate_since

    def validate_since():
        """Validate the current Last-Modified against If-Modified-Since headers.
    
        If no code has set the Last-Modified response header, then no validation
        will be performed.
        """
        response = cherrypy.serving.response
        lastmod = response.headers.get('Last-Modified')
        if lastmod:  
            # ...
    
            since = request.headers.get('If-Modified-Since')
            if since and since == lastmod:
                if (status >= 200 and status <= 299) or status == 304:
                    if request.method in ("GET", "HEAD"):
                        raise cherrypy.HTTPRedirect([], 304)
                    else:
                        raise cherrypy.HTTPError(412)
    

    总结

    对于带有有效If-Modified-Since 标头的请求,使用tools.staticdir CherryPy 不会发送文件内容,也不会压缩它们,但只会以304 Not Modified 响应询问文件系统的修改时间。如果不刷新页面,它甚至不会收到请求,因为浏览器会在服务器不提供过期时间时使用启发式方法。当然,使您的配置更具确定性,提供缓存生存时间不会受到影响,例如:

    '/static' : {
      'tools.gzip.on'       : True,
      'tools.staticdir.on'  : True,
      'tools.staticdir.dir' : os.path.join(path, 'static'),
      'tools.expires.on'    : True,
      'tools.expires.secs'  : 3600 # expire in an hour
    }
    

    【讨论】: