Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: support for proxy caching plugin based on disk. #1153

Merged
merged 22 commits into from
Mar 15, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion bin/apisix
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ http {
lua_shared_dict jwks 1m; # cache for JWKs
lua_shared_dict introspection 10m; # cache for JWT verification results

{% if proxy_cache then %}
# for proxy cache
{% for _, cache in ipairs(proxy_cache.zones) do %}
proxy_cache_path {* cache.disk_path *} levels=1:2 keys_zone={* cache.name *}:{* cache.memory_size *} inactive=1d max_size={* cache.disk_size *};
{% end %}
{% end %}

lua_ssl_verify_depth 5;
ssl_session_timeout 86400;

Expand Down Expand Up @@ -367,6 +374,8 @@ http {
proxy_pass_header Server;
proxy_pass_header Date;

### the following x-forwarded-* headers is to send to upstream server

set $var_x_forwarded_for $remote_addr;
set $var_x_forwarded_proto $scheme;
set $var_x_forwarded_host $host;
Expand All @@ -390,7 +399,35 @@ http {
proxy_set_header X-Forwarded-Host $var_x_forwarded_host;
proxy_set_header X-Forwarded-Port $var_x_forwarded_port;

proxy_pass $upstream_scheme://apisix_backend$upstream_uri;
{% if proxy_cache then %}
### the following configuration is to cache response content from upstream server

set $upstream_cache_zone off;
set $upstream_cache_key '';
set $upstream_cache_bypass 'bypass';
set $upstream_no_cache '';
set $upstream_hdr_expires '';
set $upstream_hdr_cache_control '';

proxy_cache $upstream_cache_zone;
proxy_cache_valid 502 504 0s;
agile6v marked this conversation as resolved.
Show resolved Hide resolved
proxy_cache_valid any {% if proxy_cache and proxy_cache.cache_ttl then %} {* proxy_cache.cache_ttl *} {% else %} 5s {% end %};
proxy_cache_min_uses 1;
proxy_cache_methods GET HEAD;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale off;
proxy_cache_key $upstream_cache_key;
proxy_no_cache $upstream_no_cache;
proxy_cache_bypass $upstream_cache_bypass;

proxy_hide_header Cache-Control;
proxy_hide_header Expires;
add_header Cache-Control $upstream_hdr_cache_control;
add_header Expires $upstream_hdr_expires;
add_header Apisix-Cache-Status $upstream_cache_status always;
{% end %}

proxy_pass $upstream_scheme://apisix_backend$upstream_uri;

header_filter_by_lua_block {
apisix.http_header_filter_phase()
Expand Down
14 changes: 14 additions & 0 deletions conf/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ apisix:
# enable_tcp_pp: true # Enable the proxy protocol for tcp proxy, it works for stream_proxy.tcp option
# enable_tcp_pp_to_upstream: true # Enables the proxy protocol to the upstream server

proxy_cache: # Proxy Caching configuration
cache_ttl: 10s # The default caching time if the upstream does not specify the cache time
zones: # The parameters of a cache
- name: disk_cache_one # The name of the cache, administrator can be specify
# which cache to use by name in the admin api
memory_size: 50m # The size of shared memory, it's used to store the cache index
disk_size: 1G # The size of disk, it's used to store the cache data
disk_path: "/tmp/disk_cache_one" # The path to store the cache data
# - name: disk_cache_two
# memory_size: 50m
# disk_size: 1G
# disk_path: "/tmp/disk_cache_two"

# allow_admin: # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow
# - 127.0.0.0/24 # If we don't set any IP list, then any IP access is allowed by default.
# - "::/64"
Expand Down Expand Up @@ -109,6 +122,7 @@ plugins: # plugin list
- fault-injection
- udp-logger
- wolf-rbac
- proxy-cache

stream_plugins:
- mqtt-proxy
17 changes: 12 additions & 5 deletions lua/apisix/core/ctx.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,18 @@ do
}

local ngx_var_names = {
upstream_scheme = true,
upstream_host = true,
upstream_upgrade = true,
upstream_connection = true,
upstream_uri = true,
upstream_scheme = true,
upstream_host = true,
upstream_upgrade = true,
upstream_connection = true,
upstream_uri = true,

upstream_cache_zone = true,
upstream_no_cache = true,
upstream_cache_key = true,
upstream_cache_bypass = true,
upstream_hdr_expires = true,
upstream_hdr_cache_control = true,
}

local mt = {
Expand Down
234 changes: 234 additions & 0 deletions lua/apisix/plugins/proxy-cache.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

local core = require("apisix.core")
local tab_insert = table.insert
local tab_concat = table.concat
local re_gmatch = ngx.re.gmatch
local ngx = ngx
local ipairs = ipairs

local lrucache = core.lrucache.new({
ttl = 300, count = 100
})

local plugin_name = "proxy-cache"

local schema = {
type = "object",
properties = {
cache_zone = {
type = "string",
minLength = 1
},
cache_key = {
agile6v marked this conversation as resolved.
Show resolved Hide resolved
type = "string",
agile6v marked this conversation as resolved.
Show resolved Hide resolved
minLength = 1
},
cache_http_status = {
type = "array",
minItems = 1,
items = {
description = "http response status",
type = "integer",
minimum = 200,
maximum = 599,
},
uniqueItems = true,
default = {200, 301, 404},
},
cache_method = {
type = "array",
minItems = 1,
items = {
description = "http method",
type = "string",
enum = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD",
"OPTIONS", "CONNECT", "TRACE"},
},
uniqueItems = true,
default = {"GET", "HEAD"},
agile6v marked this conversation as resolved.
Show resolved Hide resolved
},
hide_cache_headers = {
type = "boolean",
default = false,
},
cache_bypass = {
type = "string",
default = "1",
minLength = 0
},
no_cache = {
type = "string",
default = "0",
minLength = 0
},
},
required = {"cache_zone", "cache_key"},
}

local _M = {
version = 0.1,
priority = 1009,
name = plugin_name,
schema = schema,
}

function _M.check_schema(conf)
local ok, err = core.schema.check(schema, conf)
if not ok then
return false, err
end

return true
end

-- Copy from redirect plugin, this function is useful.
-- It can be extracted as a public function.
local function parse_complex_value(complex_value)

local reg = [[ (\\\$[0-9a-zA-Z_]+) | ]] -- \$host
.. [[ \$\{([0-9a-zA-Z_]+)\} | ]] -- ${host}
.. [[ \$([0-9a-zA-Z_]+) | ]] -- $host
.. [[ (\$|[^$\\]+) ]] -- $ or others
local iterator, err = re_gmatch(complex_value, reg, "jiox")
if not iterator then
return nil, err
end

local t = {}
while true do
local m, err = iterator()
if err then
return nil, err
end

if not m then
break
end

tab_insert(t, m)
end

return t
end


local tmp = {}
local function generate_complex_value(data, ctx)
agile6v marked this conversation as resolved.
Show resolved Hide resolved
local segs_value, err = lrucache(data, nil, parse_complex_value, data)
if not segs_value then
return nil, err
end

core.table.clear(tmp)

for i, value in ipairs(segs_value) do
core.log.info("complex value(", data, ") seg-", i, ": ", core.json.delay_encode(value))

local pat1 = value[1] -- \$host
local pat2 = value[2] -- ${host}
local pat3 = value[3] -- $host
local pat4 = value[4] -- $ or others

if pat2 or pat3 then
tab_insert(tmp, ctx.var[pat2 or pat3])
else
tab_insert(tmp, pat1 or pat4)
end
end

return tab_concat(tmp, "")
end


function _M.rewrite(conf, ctx)
core.log.info("proxy-cache plugin rewrite phase, conf: ", core.json.delay_encode(conf))

ctx.var.upstream_cache_zone = conf.cache_zone

local value, err = generate_complex_value(conf.cache_key, ctx)
if not value then
core.log.error("failed to generate the complex value by: ", conf.cache_key, " error: ", err)
core.response.exit(500)
end

ctx.var.upstream_cache_key = value
core.log.info("proxy-cache cache key value:", value)

local value, err = generate_complex_value(conf.cache_bypass, ctx)
if not value then
core.log.error("failed to generate the complex value by: ",
conf.cache_bypass, " error: ", err)
core.response.exit(500)
end

ctx.var.upstream_cache_bypass = value
core.log.info("proxy-cache cache bypass value:", value)
end


function _M.header_filter(conf, ctx)
core.log.info("proxy-cache plugin header filter phase, conf: ", core.json.delay_encode(conf))

local no_cache = "1"
local match_method, match_status = false, false
agile6v marked this conversation as resolved.
Show resolved Hide resolved

-- Maybe there is no need for optimization here.
for _, method in ipairs(conf.cache_method) do
if method == ctx.var.request_method then
match_method = true
break
end
end

for _, status in ipairs(conf.cache_http_status) do
if status == ngx.status then
match_status = true
break
end
end

if match_method and match_status then
no_cache = "0"
end

local value, err = generate_complex_value(conf.no_cache, ctx)
if not value then
core.log.error("failed to generate the complex value by: ", conf.no_cache, " error: ", err)
core.response.exit(500)
end

core.log.info("proxy-cache no-cache value:", value)

if value ~= nil and value ~= "" and value ~= "0" then
no_cache = "1"
end

if conf.hide_cache_headers == true then
ctx.var.upstream_hdr_cache_control = ""
ctx.var.upstream_hdr_expires = ""
else
ctx.var.upstream_hdr_cache_control = ctx.var.upstream_http_cache_control
ctx.var.upstream_hdr_expires = ctx.var.upstream_http_expires
end

ctx.var.upstream_no_cache = no_cache
core.log.info("proxy-cache no cache:", no_cache)
end

return _M
2 changes: 1 addition & 1 deletion t/admin/plugins.t
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ __DATA__
--- request
GET /apisix/admin/plugins/list
--- response_body_like eval
qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac"\]/
qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache"\]/
--- no_error_log
[error]
1 change: 1 addition & 0 deletions t/debug/debug-mode.t
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ loaded plugin and sort by priority: 2555 name: wolf-rbac
loaded plugin and sort by priority: 2520 name: basic-auth
loaded plugin and sort by priority: 2510 name: jwt-auth
loaded plugin and sort by priority: 2500 name: key-auth
loaded plugin and sort by priority: 1009 name: proxy-cache
loaded plugin and sort by priority: 1008 name: proxy-rewrite
loaded plugin and sort by priority: 1003 name: limit-conn
loaded plugin and sort by priority: 1002 name: limit-count
Expand Down