ledge

一个基于 Redis 的符合 RFC 标准且支持 ESI 的 Nginx / OpenResty HTTP 缓存

$ opm get hamishforbes/ledge

Ledge

一个符合 RFC 标准且支持 ESI 的 HTTP 缓存,适用于 Nginx / OpenResty,并使用 Redis 作为后端。

Ledge 可以用作 Squid / Varnish 等的快速、健壮且可扩展的替代方案,可以独立安装,也可以集成到现有的 Nginx 服务器或负载均衡器中。

此外,它特别适合于源站成本高昂或距离较远的应用程序,在这种情况下,尽可能乐观地从缓存中提供服务是比较理想的。

安装

OpenRestyNginx 的超集,捆绑了 LuaJITlua-nginx-module 以及许多其他功能。尽管可以将所有这些内容构建到 Nginx 中,但我们建议使用最新的 OpenResty。

1. 下载并安装

2. 使用 LuaRocks 安装 Ledge

    luarocks install ledge

这将安装最新的稳定版本,以及所有其他 Lua 模块依赖项。如果手动安装,则无需使用 LuaRocks 安装以下模块:

3. 查看 OpenResty 文档

如果您不熟悉 OpenResty,那么查看 lua-nginx-module 文档,了解如何在 Nginx 中运行 Lua 代码,这一点非常重要,因为环境很不寻常。特别是,了解 Nginx 不同阶段钩子的含义,例如 init_by_luacontent_by_lua,以及 lua-nginx-module 如何使用 lua_package_path 指令查找 Lua 模块。

理念和命名法

核心模块称为 ledge,它提供用于创建 handler 实例(用于处理请求)和 worker 实例(用于运行后台任务)的工厂方法。ledge 模块也是管理全局配置的地方。

handler 的生命周期很短。通常在 Nginx content 阶段的开始为一个请求创建 handler,当调用其 run() 方法时,它将负责处理当前请求并提供响应。当 run() 完成时,HTTP 状态、标头和正文将已传递给客户端。

worker 的生命周期很长,每个 Nginx 工作进程都有一个 worker。当 Nginx 启动一个工作进程时,它将被创建,当 Nginx 工作进程退出时,它也将退出。worker 会弹出排队的后台作业并处理它们。

upstream 是唯一必须手动配置的东西,它指向另一个包含实际内容的 HTTP 主机。通常,人们会使用 DNS 将客户端连接解析到运行 Ledge 的 Nginx 服务器,并使用 upstream 配置告诉 Ledge 从哪里获取内容。因此,Ledge 并非设计为转发代理。

Redis 的用途远不止缓存存储。我们严重依赖其数据结构来维护缓存 metadata,以及嵌入式 Lua 脚本来进行原子任务管理等等。默认情况下,所有缓存正文数据和 metadata 都将存储在同一个 Redis 实例中。缓存 metadata 的位置是全局的,在 Nginx 启动时设置。

缓存正文数据由 storage 系统处理,如上所述,默认情况下它与 metadata 共享同一个 Redis 实例。但是,storage 通过 "驱动系统" 抽象,因此可以将缓存正文数据存储在单独的 Redis 实例中,或者通过 代理 存储在一组横向扩展的 Redis 实例中,或者自己编写 storage 驱动,例如针对 PostreSQL 甚至简单的文件系统。也许重要的是要考虑,默认情况下,所有缓存存储都使用 Redis,因此受限于系统内存。

缓存键

任何缓存系统的目标都是安全地最大限度地提高 HIT 可能性。也就是说,尽可能地规范化会导致缓存分割的因素,以便尽可能多地共享缓存。

这很难泛化,因此默认情况下,Ledge 会将请求 URI 中的合理默认值放到缓存键中,并提供一种通过修改 cache_key_spec 来自定义此行为的方法。

默认情况下,URI 参数按字母顺序排序,因此 http://example.com?a=1&b=2 将命中与 http://example.com?b=2&a=1 相同的缓存条目。

流式设计

HTTP 响应的大小可能相差很大,有时很小,有时很大,而且不总是可以在前端知道总大小。

为了保证无论响应大小如何,内存使用都具有可预测性,Ledge 采用流式设计,这意味着它一次只对每个请求操作一个 buffer。这在从上游获取内容时,在从缓存中读取内容时,以及在服务于客户端请求时都是如此。

在处理 ESI 指令时也是如此(大多情况下),除了在发现指令跨越多个 buffer 的情况下。在这种情况下,我们将继续缓冲,直到可以理解完整的指令,最多达到 "可配置的限制"

这种流式设计还提高了延迟,因为我们一旦完成对第一个 buffer 的处理,就会开始将其提供给客户端请求,而不是在提供服务之前先获取和保存整个资源。buffer 大小甚至可以在每个 location 基础上进行 调整

折叠转发

Ledge 可以尝试将已知(之前)可缓存资源的并发源站请求折叠成单个上游请求。也就是说,如果一个资源的上游请求正在进行中,随后针对同一资源的并发请求将不会打扰上游,而是等待第一个请求完成。

如果已过期且成本高昂的内容出现流量激增(因为对较慢内容的并发请求的可能性更高),这对于减少上游负载尤其有用。

高级缓存模式

除了标准的符合 RFC 标准的缓存行为之外,Ledge 还具有许多旨在最大限度地提高缓存 HIT 率和降低请求延迟的功能。有关更多信息,请参见有关 "边缘侧包含""提供过时内容""在清除时重新验证" 的部分。

最少配置

假设 Redis 运行在 localhost:6379 上,并且您的上游位于 localhost:8080 上,请将以下内容添加到 OpenResty 安装中的 nginx.conf 文件中。

    http {
        if_modified_since Off;
        lua_check_client_abort On;
    
        init_by_lua_block {
            require("ledge").configure({
                redis_connector_params = {
                    url = "redis://127.0.0.1:6379/0",
                },
            })
    
            require("ledge").set_handler_defaults({
                upstream_host = "127.0.0.1",
                upstream_port = 8080,
            })
        }
    
        init_worker_by_lua_block {
            require("ledge").create_worker():run()
        }
    
        server {
            server_name example.com;
            listen 80;
    
            location / {
                content_by_lua_block {
                    require("ledge").create_handler():run()
                }
            }
        }
    }

配置系统

配置系统共有四个不同的层级。首先是主要的 "Redis 配置""handler 默认值" 配置,它们是全局的,必须在 Nginx init 阶段设置。

除此之外,您可以在 Nginx location 块的基础上指定 "handler 实例配置",最后是 worker 实例的一些性能调整配置选项。

此外,还有一个 "事件系统" 用于将 Lua 函数绑定到请求中间的事件,为动态更改配置提供机会。

事件系统

Ledge 的大部分决策都是基于其正在处理的内容。HTTP 请求和响应标头驱动着内容交付的语义,因此,我们没有提供无数的配置选项来更改这一点,而是提供了一些机会在必要时更改给定的语义。

例如,如果 upstream 未设置足够长的缓存过期时间,而不是发明一个名为 "extend_ttl" 的选项,我们而是会绑定到 after_upstream_request 事件,并调整响应标头以包含我们期望的 ttl。

    handler:bind("after_upstream_request", function(res)
        res.header["Cache-Control"] = "max-age=86400"
    end)

此特定事件在我们从上游获取内容后触发,但在 Ledge 决定是否可以缓存内容之前触发。在我们调整标头后,Ledge 将读取它们,就好像它们来自上游本身一样。

请注意,可以将多个函数绑定到单个事件,无论是全局还是每个处理程序,它们将按绑定顺序调用。目前也没有检查哪些函数已被绑定或取消绑定它们的方法。

请参见 events 部分以获取事件及其定义的完整列表。

全局绑定

全局绑定函数意味着它将在所有请求上针对给定事件触发。如果您有多个不同的 location 块,但始终需要执行相同的逻辑,这可能很有用。

    init_by_lua_block {
        require("ledge").bind("before_serve", function(res)
            res.header["X-Foo"] = "bar"   -- always set X-Foo to bar
        end)
    }

绑定到处理程序

更常见的是,我们只希望更改给定 Nginx location 的行为。

    location /foo_location {
        content_by_lua_block {
            local handler = require("ledge").create_handler()
    
            handler:bind("before_serve", function(res)
                res.header["X-Foo"] = "bar"   -- only set X-Foo for this location
            end)
    
            handler:run()
        }
    }

性能影响

为事件编写简单逻辑并不昂贵(在很多情况下,它将被 JIT 编译)。如果您需要在事件期间咨询服务端点,那么显然要考虑这将影响您的整体延迟,并确保您以非阻塞的方式完成所有操作,例如使用 OpenResty 提供的 cosockets,或者基于此的驱动程序。

如果您有许多事件处理程序,请考虑在 Lua 中创建闭包是相对昂贵的。一个好的解决方案是创建您自己的模块,并将定义的函数传递进去。

    location /foo_location {
        content_by_lua_block {
            local handler = require("ledge").create_handler()
            handler:bind("before_serve", require("my.handler.hooks").add_foo_header)
            handler:run()
        }
    }

缓存基础

对于正常的 HTTP 缓存操作,无需额外的配置。如果 HTTP 响应指示可以缓存资源,那么它将缓存它。如果 HTTP 请求指示它接受缓存,则它将被缓存。请注意,这两个条件并非互斥 - 请求可以指定 no-cache,这确实会触发从上游获取内容,但如果响应是可缓存的,那么它将被保存并提供给后续的接受缓存的请求。

有关影响此行为的无数因素的更多信息,包括端到端重新验证等等,请参阅 RFC 7234

目标是完全符合 RFC 标准,但在某些情况下提供了一些扩展,以允许更积极的缓存。如果某些内容没有按预期工作,请随时 提交问题

清除

为了手动使缓存项失效(或清除),我们支持 Squid 用户熟悉的非标准 PURGE 方法。向 URI 发送一个带有该方法的 HTTP 请求,Ledge 将尝试使该项失效,如果成功,则返回状态 200,如果 URI 未在缓存中找到,则返回 404,以及一个包含更多详细信息的 JSON 正文。

清除请求将影响与缓存键相关联的所有表示,例如,由 Vary: Accept-Encoding 响应标头分隔的压缩和未压缩响应都将被清除。

$> curl -X PURGE -H "Host: example.com" http://cache.example.com/page1 | jq .

    {
        "purge_mode": "invalidate",
        "result": "nothing to purge"
    }

有三种清除模式,可以通过设置 X-Purge 请求标头选择,其中包含以下值之一或多个值

  • invalidate: (默认值)将项标记为已过期,但不会删除任何内容。

  • delete: 从缓存中硬删除项

  • revalidate: 使缓存失效,但同时安排一个后台重新验证任务来重新填充缓存。

$> curl -X PURGE -H "X-Purge: revalidate" -H "Host: example.com" http://cache.example.com/page1 | jq .

    {
      "purge_mode": "revalidate",
      "qless_job": {
        "options": {
          "priority": 4,
          "jid": "5eeabecdc75571d1b93e9c942dfcebcb",
          "tags": [
            "revalidate"
          ]
        },
        "jid": "5eeabecdc75571d1b93e9c942dfcebcb",
        "klass": "ledge.jobs.revalidate"
      },
      "result": "already expired"
    }

可以在 qless 元数据中跟踪后台重新验证作业。有关更多信息,请参阅 "管理 qless"

通常,PURGE 被认为是管理任务,可能不应该从互联网上访问。例如,可以考虑通过 IP 地址限制它。

    limit_except GET POST PUT DELETE {
        allow   127.0.0.1;
        deny    all;
    }

JSON API

还可以使用基于 JSON 的 API 来一次清除多个缓存项。这需要一个 PURGE 请求,其中 Content-Type 标头设置为 application/json,并且具有有效的 JSON 请求正文。

有效参数

  • uris - 要清除的 URI 数组,可以包含通配符 URI

  • purge_mode - 作为普通清除请求中的 X-Purge 标头

  • headers - 要包含在清除请求中的其他标头的哈希表

返回一个结果哈希表,以 URI 为键,或者返回一个 JSON 错误响应

$> curl -X PURGE -H "Content-Type: Application/JSON" http://cache.example.com/ -d '{"uris": ["http://www.example.com/1", "http://www.example.com/2"]}' | jq .

    {
      "purge_mode": "invalidate",
      "result": {
        "http://www.example.com/1": {
          "result": "purged"
        },
        "http://www.example.com/2":{
          "result": "nothing to purge"
        }
      }
    }

通配符清除

PURGE URI 也支持通配符 (\*) 模式,它将始终返回状态 200 和一个详细说明后台作业的 JSON 主体。通配符清除涉及扫描整个键空间,因此可能需要一段时间。有关调整帮助,请参阅 keyspace\_scan\_count

此外,X-Purge 模式将传播到由于通配符清除而清除的所有 URI,例如,这使得可以触发站点/分区范围的重新验证。小心你的愿望。

$> curl -v -X PURGE -H "X-Purge: revalidate" -H "Host: example.com" http://cache.example.com/* | jq .

    {
      "purge_mode": "revalidate",
      "qless_job": {
        "options": {
          "priority": 5,
          "jid": "b2697f7cb2e856cbcad1f16682ee20b0",
          "tags": [
            "purge"
          ]
        },
        "jid": "b2697f7cb2e856cbcad1f16682ee20b0",
        "klass": "ledge.jobs.purge"
      },
      "result": "scheduled"
    }

提供陈旧内容

当内容的年龄超过其 TTL 时,它被认为是“陈旧”的。但是,根据 keep_cache_for 的值(默认为 1 个月),我们不会立即在 Redis 中过期内容。

这使我们能够实现 RFC5861 中描述的陈旧缓存控制扩展,它提供请求和响应标头语义来描述可以提供多陈旧的内容,何时应在后台重新验证它,以及在发生上游错误时我们可以在多长时间内提供陈旧内容。

这在确保快速的用户体验方面非常有效。例如,如果您的内容的真实 max-age 为 24 小时,请考虑将其更改为 1 小时,并添加 23 小时的 stale-while-revalidate。因此,总 TTL 相同,但第一个小时后的第一个请求将触发后台重新验证,将 TTL 延长 1 小时 + 23 小时。

如果您的源服务器无法以这种方式配置,您始终可以通过 绑定before_save 事件来覆盖。

    handler:bind("before_save", function(res)
        -- Valid for 1 hour, stale-while-revalidate for 23 hours, stale-if-error for three days
        res.header["Cache-Control"] = "max-age=3600, stale-while-revalidate=82800, stale-if-error=259200"
    end)

换句话说,将 TTL 设置为源服务器上请求的最舒适频率,将 stale-while-revalidate 设置为最舒适的 TTL,以增加发生后台重新验证的可能性。请注意,第一个陈旧请求显然会获得陈旧内容,因此非常长的值会导致一个请求获得非常过时的内容。

所有陈旧行为都受正常缓存控制语义的约束。例如,如果源服务器已关闭,并且由于上游错误可以提供陈旧的响应,但请求包含 Cache-Control: no-cache 甚至 Cache-Control: max-age=60(其中内容的年龄超过 60 秒),它们将被提供错误,而不是陈旧内容。

边缘侧包含

几乎完全支持 ESI 1.0 语言规范,有一些例外和一些增强功能。

    <html>
    <esi:include="/header" />
    <body>
    
       <esi:choose>
          <esi:when test="$(QUERY_STRING{foo}) == 'bar'">
             Hi
          </esi:when>
          <esi:otherwise>
             <esi:choose>
                <esi:when test="$(HTTP_COOKIE{mycookie}) == 'yep'">
                   <esi:include src="http://example.com/_fragments/fragment1" />
                </esi:when>
             </esi:choose>
          </esi:otherwise>
       </esi:choose>
    
    </body>
    </html>

启用 ESI

请注意,仅仅 启用 ESI 可能还不够。我们还会根据允许的类型检查 "内容类型",但更重要的是,ESI 处理依赖于 边缘架构规范。启用后,Ledge 将使用 Surrogate-Capability 请求标头在上游宣传功能,并期望上游响应包含 Surrogate-Control 标头,将 ESI 处理委托给 Ledge。

如果您的上游不支持 ESI,一种常见的做法是绑定到 after\_upstream\_request 事件,以便手动添加 Surrogate-Control 标头。例如:

    handler:bind("after_upstream_request", function(res)
        -- Don't enable ESI on redirect responses
        -- Don't override Surrogate Control if it already exists
        local status = res.status
        if not res.header["Surrogate-Control"] and not (status > 300 and status < 303) then
            res.header["Surrogate-Control"] = 'content="ESI/1.0"'
        end
    end)

请注意,如果处理 ESI,下游可缓存性将自动删除,因为您不希望其他中介或浏览器缓存结果。

因此,最好只为已知包含 ESI 指令的内容设置 Surrogate-Control。虽然 Ledge 会在保存时检测 ESI 指令的存在(如果不存在指令,则在缓存命中时不执行任何操作),但在缓存未命中时,它会在读取/保存主体之前已经删除下游缓存标头。这是 "流式设计" 的副作用。

条件中的正则表达式

除了 ESI 规范 中定义的操作符外,我们还在条件(作为字符串文字)中支持正则表达式,使用 =~ 操作符。

    <esi:choose>
       <esi:when test="$(QUERY_STRING{name}) =~ '/james|john/i'">
          Hi James or John
       </esi:when>
    </esi:choose>

支持的修饰符与 ngx.re.\* 文档一致。

自定义 ESI 变量

除了 ESI 规范 中定义的变量外,还可以使用 esi_custom_variables 处理程序配置选项提供运行时自定义变量。

    content_by_lua_block {
       require("ledge").create_handler({
          esi_custom_variables = {
             messages = {
                foo = "bar",
             },
          },
       }):run()
    }


    <esi:vars>$(MESSAGES{foo})</esi:vars>

ESI 参数

使用包含 ESI 的页面中的 URI 参数来动态更改布局可能很有诱惑力,但这会导致生成多个缓存项——每个 URI 参数排列一个。

ESI 参数是一个巧妙的功能,它可以解决这个问题,它使用一个可配置的 前缀,默认为 esi_。具有此前缀的 URI 参数将从缓存键中删除,并且还会从上游请求中删除,而是填充到 $(ESI_ARGS{foo}) 变量中,以便在 ESI 中使用,通常用于条件。也就是说,将它们视为仅对 ESI 处理器有意义的魔术 URI 参数,永远不会影响可缓存性或上游内容生成。

$> curl -H "Host: example.com" http://cache.example.com/page1?esi_display_mode=summary

    <esi:choose>
       <esi:when test="$(ESI_ARGS{display_mode}) == 'summary'">
          <!-- SUMMARY -->
       </esi:when>
       <esi:when test="$(ESI_ARGS{display_mode}) == 'details'">
          <!-- DETAILS -->
       </esi:when>
    </esi:choose>

在这个例子中,esi_display_mode 的值 summarydetails 将返回相同的缓存命中,但显示不同的内容。

如果使用 $(ESI_ARGS) 而不带字段键,它将呈现原始查询字符串参数,例如 esi_foo=bar&esi_display_mode=summary,进行 URL 编码。

变量转义

默认情况下,ESI 变量会进行最少的转义,以防止用户注入额外的 ESI 标签或 XSS 攻击。

可以通过在变量名前添加 RAW_ 来获得未转义的变量。应谨慎使用。

    # /esi/test.html?a=<script>alert()</script>
    <esi:vars>
    $(QUERY_STRING{a})     <!-- &lt;script&gt;alert()&lt;/script&gt; -->
    $(RAW_QUERY_STRING{a}) <!--  <script>alert()</script> -->
    </esi:vars>

缺少的 ESI 功能

以下部分 ESI 规范 不受支持,但如果确定需要,可能会在适当的时候支持。

  • <esi:inline> 未实现(或作为功能宣传)。

  • 不支持 <esi:include>onerroralt 属性。相反,我们默认情况下在错误时“继续”。

  • <esi:try | attempt | except> 未实现。

  • HTTP_USER_AGENT 的“字典(特殊)”子结构变量类型未实现。

API

ledge.configure

语法:ledge.configure(config)

此函数向 Ledge 提供 Redis 连接详细信息,用于所有缓存 metadata 和后台作业。这是全局的,不能在 Nginx init 阶段之外指定或调整。

    init_by_lua_block {
        require("ledge").configure({
            redis_connector_params = {
                url = "redis://mypassword@127.0.0.1:6380/3",
            }
            qless_db = 4,
        })
    }

config 是一个具有以下选项的表(无法识别的配置将在启动时发生严重错误)。

redis_connector_params

默认值:{}

Ledge 使用 lua-resty-redis-connector 处理所有 Redis 连接。它只是将 redis_connector_params 中提供的任何内容直接传递给 lua-resty-redis-connector,因此请查看那里的文档了解选项,包括如何使用 Redis Sentinel

qless_db

默认值:1

指定用于存储 qless 后台作业数据的 Redis DB 编号。

ledge.set\_handler\_defaults

语法:ledge.set_handler_defaults(config)

此方法覆盖用于所有生成的请求 handler 实例的默认配置。这是全局的,不能在 Nginx init 阶段之外指定或调整,但这些默认值可以在每个 handler 基础上被覆盖。有关完整配置选项列表,请参阅 下文

    init_by_lua_block {
        require("ledge").set_handler_defaults({
            upstream_host = "127.0.0.1",
            upstream_port = 8080,
        })
    }

ledge.create\_handler

语法:local handler = ledge.create_handler(config)

为当前请求创建 handler 实例。此处提供的配置将与默认配置合并,允许在每个 Nginx location 基础上调整某些选项。

    server {
        server_name example.com;
        listen 80;
    
        location / {
            content_by_lua_block {
                require("ledge").create_handler({
                    upstream_port = 8081,
                }):run()
            }
        }
    }

ledge.create\_worker

语法:local worker = ledge.create_worker(config)

在当前 Nginx 工作进程内创建一个 worker 实例,用于处理后台作业。您只需要在单个 init_worker 块中调用此方法一次,它将被调用用于配置的每个 Nginx 工作进程。

作业队列可以在每个工作进程中以不同的并发量运行,这可以通过在此处提供 config 来设置。有关更多详细信息,请参阅 "管理 qless"

    init_worker_by_lua_block {
        require("ledge").create_worker({
            interval = 1,
            gc_queue_concurrency = 1,
            purge_queue_concurrency = 2,
            revalidate_queue_concurrency = 5,
        }):run()
    }

ledge.bind

语法:ledge.bind(event_name, callback)

callback 函数绑定到 event_name 中给定的事件,全局适用于此系统上的所有请求。callback 的参数根据事件而有所不同。有关事件定义,请参阅 下文

handler.bind

语法:handler:bind(event_name, callback)

仅将 callback 函数绑定到 event_name 中给定的事件,仅针对此处理程序。请注意 handler:bind() 中的 : 与全局 ledge.bind() 不同。

callback 的参数根据事件而有所不同。有关事件定义,请参阅 下文

handler.run

语法:handler:run()

必须在 content_by_lua 阶段调用。它处理当前请求并提供响应。如果您未在 location 块中调用此方法,则不会发生任何事情。

worker.run

语法:handler:run()

必须在 init_worker 阶段调用,否则不会运行后台任务,包括垃圾收集,这一点非常重要。

处理程序配置选项

storage_driver

默认值:ledge.storage.redis

这是一个string值,将用于尝试加载存储驱动程序。任何第三方驱动程序都可以接受其自己的配置选项(见下文),但必须提供以下接口

  • bool new()

  • bool connect()

  • bool close()

  • number get_max_size() (对于没有最大值,返回nil)

  • bool exists(string entity_id)

  • bool delete(string entity_id)

  • bool set_ttl(string entity_id, number ttl)

  • number get_ttl(string entity_id)

  • function get_reader(object response)

  • function get_writer(object response, number ttl, function onsuccess, function onfailure)

注意,虽然可以在每个location的基础上配置存储驱动程序,但强烈建议不要这样做,并将存储驱动程序视为系统范围的,类似于主要的Redis配置。如果你确实需要为不同的位置配置不同的存储驱动程序,那么它可以工作,但是像使用通配符清除这样的功能将默默地不起作用。YMMV。

storage_driver_config

默认值:{}

存储配置可能因驱动程序而异。目前我们只有 Redis 驱动程序。

Redis存储驱动程序配置

  • redis_connector_params Redis 参数表,参考 lua-resty-redis-connector

  • max_size (字节),默认值为1MB

  • supports_transactions 默认值为true,如果使用 Redis 代理,则设置为 false。

如果supports_transactions设置为false,缓存主体将不会以原子方式写入。但是,如果写入时出错,主Redis系统将收到通知,并且整个事务将被中止。结果可能是存储系统中潜在的孤立主体实体,这些实体最终可能会过期。关闭此功能的唯一原因是,如果你使用的是Redis代理,因为任何与事务相关的命令都会破坏连接。

upstream_connect_timeout

默认值: 1000 (毫秒)

等待上游连接的最大时间(以毫秒为单位)。如果超过此时间,我们将发送503状态代码,除非配置了stale_if_error

upstream_send_timeout

默认值: 2000 (毫秒)

在已连接的上游套接字上发送数据等待的最大时间(以毫秒为单位)。如果超过此时间,我们将发送503状态代码,除非配置了stale_if_error

upstream_read_timeout

默认值: 10000 (毫秒)

在已连接的上游套接字上等待的最大时间(以毫秒为单位)。如果超过此时间,我们将发送503状态代码,除非配置了stale_if_error

upstream_keepalive_timeout

默认值: 75000

upstream_keepalive_poolsize

默认值: 64

upstream_host

默认值: ""

指定上游主机的 hostname 或 IP 地址。如果指定 hostname,则必须在某处配置 Nginx resolver,例如

    resolver 8.8.8.8;

upstream_port

默认值: 80

指定上游主机的端口。

upstream_use_ssl

默认值: false

切换上游连接上是否使用 SSL。如果未设置为true,其他upstream_ssl_*选项将被忽略。

upstream_ssl_server_name

默认值: ""

指定用于服务器名称指示 (SNI) 的 SSL 服务器名称。有关更多信息,请参见 sslhandshake

upstream_ssl_verify

默认值: false

切换 SSL 验证。有关更多信息,请参见 sslhandshake

cache_key_spec

默认值: cache_key_spec = { "scheme", "host", "uri", "args" },

指定用于创建缓存密钥的格式。上面的默认规范将在 Redis 中创建类似于

    ledge:cache:http:example.com:/about::
    ledge:cache:http:example.com:/about:p=2&q=foo:

规范中可用的字符串标识符列表为

  • scheme http 或 https

  • host 当前请求的 hostname

  • port 当前请求的公共端口

  • uri URI(不带参数)

  • args URI 参数,按字母顺序排序

除了这些字符串标识符之外,可以通过提供函数将动态参数添加到缓存密钥中。给定的任何函数都必须不带参数并返回一个字符串值。

    local function get_device_type()
        -- dynamically work out device type
        return "tablet"
    end
    
    require("ledge").create_handler({
        cache_key_spec = {
            get_device_type,
            "scheme",
            "host",
            "uri",
            "args",
        }
    }):run()

考虑使用before_vary_selection事件中的vary来分离缓存条目,而不是直接修改主要的cache_key_spec

origin_mode

默认值: ledge.ORIGIN_MODE_NORMAL

确定连接到源的整体行为。ORIGIN_MODE_NORMAL 假设源已启动,并根据需要连接。

ORIGIN_MODE_AVOID 类似于 Squid 的offline_mode,其中将服务任何保留的缓存(已过期或未过期),而不是尝试源,无论缓存控制标头如何,但如果无法提供缓存,则将尝试源。

ORIGIN_MODE_BYPASSAVOID相同,只是如果无法提供缓存,我们将向客户端发送503 服务不可用状态代码,并且永远不会尝试上游连接。

keep_cache_for

默认值: 86400 * 30 (一个月,以秒为单位)

指定在缓存数据过期日期后保留缓存数据的时长。这使我们能够在上游失败的情况下,使用stale_if_errororigin_mode设置来提供陈旧的缓存。

只要使用的是其中一个 Redis 易失性驱逐策略,在内存压力下,项目将被驱逐,因此通常不需要为了节省空间而降低此值。

处于此范围极端值(即近一个月旧)的项目显然很少被请求,或者更可能的是,已在源处被删除。

minimum_old_entity_download_rate

默认值: 56 (kbps)

读取速度比这更慢的客户端,而且不幸的是,他们还开始从已被替换的实体读取(例如,由于另一个客户端导致重新验证),他们的实体可能会在他们完成之前被垃圾回收,导致传递不完整的资源。

降低此值对慢速客户端更公平,但会扩大多个旧实体堆积的潜在窗口,这反过来可能会威胁 Redis 存储空间并强制驱逐。

enable_collapsed_forwarding

默认值: false

collapsed_forwarding_window

启用折叠转发时,如果在源请求期间发生致命错误,折叠的请求可能永远不会收到他们正在等待的响应。此设置限制了它们等待的时间,以及在新的请求决定自己尝试源之前等待的时间。

如果将其设置得比源响应时间短,则你可能得到比预期更多上游请求。致命错误(服务器重启等)可能会导致连接挂起,最长可达设置的最大时间。正常错误(例如上游超时)独立于此设置工作。

gunzip_enabled

默认值: true

启用此功能后,gzip 响应将被实时解压缩,用于未设置Accept-Encoding: gzip 的客户端。请注意,如果我们收到包含 ESI 指令的资源的 gzip 响应,我们会在保存时解压缩并存储未压缩的响应,因为我们需要读取 ESI 指令。

还要注意,Range 对 gzip 内容的请求必须被忽略 - 将返回完整响应。

buffer_size

默认值: 2^16 (64KB,以字节为单位)

指定用于读/写/提供数据的内部缓冲区大小(以字节为单位)。上游响应以该最大大小的块读入,防止在接收大文件时分配大量内存。数据也在内部作为块列表存储,并以相同的方式传递到 Nginx 输出链缓冲区。

唯一的例外是,如果配置了 ESI,并且 Ledge 确定有 ESI 指令要处理,并且这些指令中的任何一个都跨越了一个给定的块。在这种情况下,缓冲区将连接在一起,直到找到完整的指令,然后 ESI 在此新缓冲区上操作,最多为 esi_max_size

keyspace_scan_count

默认值: 1000

调整键空间扫描的行为,键空间扫描在使用通配符语法发送 PURGE 请求时发生。如果 Redis 的延迟很高并且键空间很大,则更高的数字可能更好。

max_uri_args

默认值: 100

限制在调用ngx.req.get_uri_args()时返回的 URI 参数数量,以防范 DOS 攻击。

esi_enabled

默认值: false

切换ESI 扫描和处理,但行为也取决于esi_content_typesesi_surrogate_delegation设置,以及Surrogate-Control / Surrogate-Capability 标头。

ESI 指令在慢速路径上检测到(即,从源获取时),因此只有已知存在的指令在缓存命中时才会被处理。

esi_content_types

默认值: { text/html }

指定要对其执行 ESI 处理的内容类型。所有其他内容类型将不会被视为要处理。

esi_allow_surrogate_delegation

默认值: false

ESI 代理委托 允许下游中介广告能够在更靠近客户端的地方处理 ESI 指令。通过将其设置为true,任何提供此功能的下游将禁用 Ledge 中的 ESI 处理,将其委托给下游。

当设置为 IP 地址字符串的 Lua 表时,仅允许这些特定主机进行委托。如果 ESI 指令包含必须删除的敏感数据,这可能很重要。

esi_recursion_limit

默认值: 10

限制片段包含嵌套,以避免意外的无限递归。

esi_args_prefix

默认值: "esi\_"

URI 参数前缀,用于从缓存密钥中忽略的参数(并且不上游代理),专门用于 ESI 渲染逻辑。设置为 nil 以禁用此功能。

esi_custom_variables

defualt: {}

此处提供的任何变量都将在 ESI 变量可以使用的任何地方可用。参见"自定义 ESI 变量"

esi_max_size

默认值: 1024 * 1024 (字节)

默认值 true

如果设置为 false,则禁用从Via 响应标头中宣传软件名称和版本,例如(ledge/2.01)

事件

after_cache_read

语法: bind("after_cache_read", function(res) -- end)

参数: res。缓存的响应表。

在从缓存中成功加载响应后立即触发。

给定的res表包含

  • res.header 不区分大小写的 HTTP 响应标头表

  • res.status HTTP 响应状态代码

注意; 还有其他字段和方法附加,但强烈建议不要调整除上述内容之外的任何内容

before_upstream_connect

语法: bind("before_upstream_connect", function(handler) -- end)

参数: handler。当前处理程序实例。

在创建默认的handler.upstream_client之前触发,允许外部提供预先连接的 HTTP 客户端。客户端必须与lua-resty-http 兼容。例如,使用lua-resty-upstream进行负载均衡。

before_upstream_request

语法: bind("before_upstream_request", function(req_params) -- end)

参数: req_params。即将发送到request方法的请求参数表。

在即将执行上游请求时触发。

before_esi_include_request

语法: bind("before_esi_include_request", function(req_params) -- end)

参数: req_params。即将用于 ESI 包含的请求参数表,通过request方法。

在即将代表 ESI 包含指令执行 HTTP 请求时触发。

after_upstream_request

语法: bind("after_upstream_request", function(res) -- end)

参数: res 响应表。

当状态/头信息被获取但主体尚未存储时触发,通常用于在决定如何处理响应之前覆盖缓存头信息。

给定的res表包含

  • res.header 不区分大小写的 HTTP 响应标头表

  • res.status HTTP 响应状态代码

注意; 还有其他字段和方法附加,但强烈建议不要调整除上述内容之外的任何内容

注意:与下面的 before_save 不同,此事件会针对所有获取的内容触发,而不仅仅是可缓存内容。

before_save

语法:bind("before_save", function(res) -- end)

参数: res 响应表。

当我们即将保存响应时触发。

给定的res表包含

  • res.header 不区分大小写的 HTTP 响应标头表

  • res.status HTTP 响应状态代码

注意; 还有其他字段和方法附加,但强烈建议不要调整除上述内容之外的任何内容

before_serve

语法:ledge:bind("before_serve", function(res) -- end)

参数:res ledge.response 对象。

当我们即将提供服务时触发,通常用于修改下游头信息。

给定的res表包含

  • res.header 不区分大小写的 HTTP 响应标头表

  • res.status HTTP 响应状态代码

注意; 还有其他字段和方法附加,但强烈建议不要调整除上述内容之外的任何内容

before_save_revalidation_data

语法:bind("before_save_revalidation_data", function(reval_params, reval_headers) -- end)

参数:reval_params。重新验证参数表。

参数:reval_headers。重新验证 HTTP 头信息表。

当后台重新验证触发或缓存被保存时触发,允许修改由后台重新验证继承的 HTTP 头信息和参数(例如连接参数)。

reval_params 是从当前运行配置中导出的值,用于

  • server_addr

  • server_port

  • scheme

  • uri

  • connect_timeout

  • read_timeout

  • ssl_server_name

  • ssl_verify

before_vary_selection

语法:bind("before_vary_selection", function(vary_key) -- end)

参数:vary_key 选择头信息的表

当我们即将生成 vary 键时触发,用于选择正确的缓存表示。

vary_key 表是头字段名(小写)到值的哈希表。在 Vary 响应头信息中存在但在当前请求头信息中不存在的字段名将具有 ngx.null 值。

    Request Headers:
        Accept-Encoding: gzip
        X-Test: abc
        X-test: def
    
    Response Headers:
        Vary: Accept-Encoding, X-Test
        Vary: X-Foo
    
    vary_key table:
    {
        ["accept-encoding"] = "gzip",
        ["x-test"] = "abc,def",
        ["x-foo"] = ngx.null
    }

管理

X-Cache

Ledge 添加了非标准的 X-Cache 头信息,对其他缓存的用户来说很熟悉。它简单地指示 HITMISS 以及相关的主机名,在多个缓存服务器参与时保留上游值。

如果资源被认为不可缓存,响应中将不会存在 X-Cache 头信息。

例如

  • X-Cache: HIT from ledge.tld 缓存命中,没有(已知的)上游缓存层。

  • X-Cache: HIT from ledge.tld, HIT from proxy.upstream.tld 缓存命中,在上游也命中。

  • X-Cache: MISS from ledge.tld, HIT from proxy.upstream.tld 缓存未命中,但在上游命中。

  • X-Cache: MISS from ledge.tld, MISS from proxy.upstream.tld 在源服务器重新生成。

日志记录

在 Nginx 日志中添加一些额外的头信息通常很有用,例如

    log_format ledge  '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" '
                      '"Cache:$sent_http_x_cache"  "Age:$sent_http_age" "Via:$sent_http_via"'
                      ;
    
    access_log /var/log/nginx/access_log ledge;

将生成如下日志行

    192.168.59.3 - - [23/May/2016:22:22:18 +0000] "GET /x/y/z HTTP/1.1" 200 57840 "-" "curl/7.37.1""Cache:HIT from 159e8241f519:8080"  "Age:724"
    

管理 Qless

Ledge 使用 lua-resty-qless 来调度和处理后台任务,这些任务存储在 Redis 中。

为后台重新验证请求以及通配符 PURGE 请求调度作业,但最重要的是用于垃圾回收已替换的主体实体。

也就是说,及时且正确地运行作业非常重要。

安装 Web 用户界面 对检查这一点非常有用。

如果 Qless 作业历史记录占用太多空间,您可能还想调整 Qless 作业历史记录 设置。

作者

James Hurst <james@pintsized.co.uk>

许可证

本模块根据 2 条款 BSD 许可证授权。

版权所有 (c) James Hurst <james@pintsized.co.uk>

保留所有权利。

在满足以下条件的情况下,允许以源代码和二进制形式重新分发和使用本软件,无论是否经过修改。

  • 源代码的重新分发必须保留上述版权声明、此条件列表以及以下免责声明。

  • 二进制形式的重新分发必须在随分发提供的文档和/或其他材料中复制上述版权声明、此条件列表以及以下免责声明。

本软件由版权所有者和贡献者“按原样”提供,并且不提供任何明示或暗示的担保,包括但不限于对适销性和特定用途适用性的暗示担保。在任何情况下,版权所有者或贡献者均不对因使用本软件而造成的任何直接、间接、附带、特殊、示例性或后果性损害(包括但不限于替代商品或服务的采购;使用、数据或利润的损失;或业务中断)负责,无论其责任形式如何,无论是合同、严格责任还是侵权行为(包括疏忽或其他原因),即使已告知可能发生此类损害。

作者

James Hurst, Hamish Forbes

许可证

2bsd

依赖项

版本