整體結構
Client
↓
Nginx (OpenResty + Lua)
↓ ↘
Memcached Apache (localhost)
使用 OpenResty(Nginx + Lua 的標準發行版),不用自己安裝LUA
安裝 OpenResty(範例:Ubuntu / Debian)
sudo apt install -y curl gnupg
curl -fsSL https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt update
sudo apt install -y openresty
啟動:
sudo systemctl start openresty
安裝 memcached
sudo apt install -y memcached
sudo systemctl start memcached
測試:
echo stats | nc 127.0.0.1 11211
檔案結構(建議)
/usr/local/openresty/nginx/
├─ conf/nginx.conf
└─ lua/cache.lua
Nginx 設定檔(nginx.conf)
worker_processes auto;
events {
worker_connections 4096;
}
http {
# Lua 檔案路徑
lua_package_path "/usr/local/openresty/nginx/lua/?.lua;;";
# 日誌(debug 時非常重要)
error_log logs/error.log info;
# upstream:本機 Apache
upstream apache_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
server_name _;
# === 入口 ===
location / {
# 所有 request 先交給 Lua
content_by_lua_file /usr/local/openresty/nginx/lua/cache.lua;
}
# === 真正的後端(只有 Lua 會跳進來) ===
location @backend {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://apache_backend;
# ===== 把 Apache 回來的 response 存進 memcached =====
body_filter_by_lua_block {
-- 只在 MISS 時才寫 cache
if not ngx.ctx.cache_key then
return
end
local chunk = ngx.arg[1]
local eof = ngx.arg[2]
ngx.ctx.buffer = (ngx.ctx.buffer or "") .. (chunk or "")
if eof then
local memcached = require "resty.memcached"
local memc = memcached:new()
memc:set_timeout(50)
-- 從 ctx 取回 key / ttl(由 cache.lua 設定)
local key = ngx.ctx.cache_key
local ttl = ngx.ctx.cache_ttl or 30
-- 與 cache.lua 使用同一組節點與 hash
local servers = {
{ host = "10.0.0.1", port = 11211 },
{ host = "10.0.0.2", port = 11211 },
{ host = "10.0.0.3", port = 11211 },
}
local function pick(key)
local idx = (ngx.crc32_long(key) % #servers) + 1
return servers[idx]
end
local s = pick(key)
local ok, err = memc:connect(s.host, s.port)
if ok then
memc:set(key, ngx.ctx.buffer, ttl)
memc:set_keepalive()
else
ngx.log(ngx.ERR, "memcached set failed: ", err)
end
end
}
}
}
}
Lua 程式(cache.lua)
local memcached = require "resty.memcached"
-- memcached servers
local memc_servers = {
{ host = "10.0.0.1", port = 11211 },
{ host = "10.0.0.2", port = 11211 },
{ host = "10.0.0.3", port = 11211 },
}
local function pick_memc_server(key)
local crc = ngx.crc32_long(key)
local idx = (crc % #memc_servers) + 1
return memc_servers[idx]
end
-- request params
local args = ngx.req.get_uri_args()
local func = args["function"]
local fun2 = args["fun2"]
-- function=a → bypass cache
if func == "a" then
ngx.header["X-Cache"] = "BYPASS"
return ngx.exec("@backend")
end
-- cookie
local user_cookie = ngx.var.cookie_user_id or ""
-- cache key
local raw_key =
ngx.var.request_method .. "|" ..
ngx.var.uri .. "|" ..
(ngx.var.args or "") .. "|" ..
user_cookie
local cache_key = "cache:" .. ngx.md5(raw_key)
-- TTL logic
local ttl = 30
if func == "b" and (fun2 == "BB" or fun2 == "CC") then
ttl = 10
end
-- connect memcached
local memc = memcached:new()
memc:set_timeout(50)
local server = pick_memc_server(cache_key)
local ok, err = memc:connect(server.host, server.port)
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exec("@backend")
end
-- get cache
local res, flags, err = memc:get(cache_key)
if res then
ngx.header["X-Cache"] = "HIT"
ngx.say(res)
memc:set_keepalive()
return
end
ngx.header["X-Cache"] = "MISS"
ngx.ctx.cache_key = cache_key
ngx.ctx.cache_ttl = ttl
ngx.exec("@backend")
Reload
/usr/local/openresty/nginx/sbin/nginx -t
/usr/local/openresty/nginx/sbin/nginx -s reload
測試
curl -i "http://server/test.php?function=b&fun2=BB"
你應該會看到:
css
複製程式碼
X-Cache: MISS
第二次:
css
複製程式碼
X-Cache: HIT
cache.lua 也可以改成有鎖的版本
local memcached = require "resty.memcached"
-- memcached servers
local memc_servers = {
{ host = "10.0.0.1", port = 11211 },
{ host = "10.0.0.2", port = 11211 },
{ host = "10.0.0.3", port = 11211 },
}
local function pick_memc_server(key)
local crc = ngx.crc32_long(key)
local idx = (crc % #memc_servers) + 1
return memc_servers[idx]
end
-- request params
local args = ngx.req.get_uri_args()
local func = args["function"]
local fun2 = args["fun2"]
-- function=a → bypass cache
if func == "a" then
ngx.header["X-Cache"] = "BYPASS"
return ngx.exec("@backend")
end
-- cookie
local user_cookie = ngx.var.cookie_user_id or ""
-- cache key
local raw_key =
ngx.var.request_method .. "|" ..
ngx.var.uri .. "|" ..
(ngx.var.args or "") .. "|" ..
user_cookie
local cache_key = "cache:" .. ngx.md5(raw_key)
-- TTL logic
local ttl = 30
if func == "b" and (fun2 == "BB" or fun2 == "CC") then
ttl = 10
end
-- pick memcached server
local server = pick_memc_server(cache_key)
local memc = memcached:new()
memc:set_timeout(50)
local ok, err = memc:connect(server.host, server.port)
if not ok then
ngx.log(ngx.ERR, "memcached connect failed: ", err)
return ngx.exec("@backend")
end
-- 1️⃣ 先查 cache
local res, flags, err = memc:get(cache_key)
if res then
ngx.header["X-Cache"] = "HIT"
ngx.say(res)
memc:set_keepalive()
return
end
-- 2️⃣ 嘗試 set lock,避免 cache stampede
local lock_key = "lock:" .. cache_key
local lock_ttl = 5 -- lock 最長存活 5 秒
local lock_set, err = memc:add(lock_key, "1", lock_ttl)
if lock_set then
-- 成功獲得鎖 → 我去打後端
ngx.header["X-Cache"] = "MISS|LOCKED"
ngx.ctx.cache_key = cache_key
ngx.ctx.cache_ttl = ttl
return ngx.exec("@backend")
else
-- 沒拿到鎖 → 等待 / retry
ngx.header["X-Cache"] = "MISS|WAIT"
local wait_time = 0
local wait_interval = 0.05 -- 50ms
local max_wait = 2 -- 最多等 2 秒
while wait_time < max_wait do
local res2, _, _ = memc:get(cache_key)
if res2 then
ngx.say(res2)
memc:set_keepalive()
return
end
ngx.sleep(wait_interval)
wait_time = wait_time + wait_interval
end
-- 超時沒拿到 cache → 仍然打後端
ngx.ctx.cache_key = cache_key
ngx.ctx.cache_ttl = ttl
return ngx.exec("@backend")
end
Nginx 可以解決的痛點
- 非阻塞 → 不會被慢 PHP 卡死
- cache + lock → 重複請求不打 Apache
- rate limit → 攻擊 / 高併發被控管
- 輕量記憶體使用 → 上萬連線仍穩定
Apache 容易卡住的原因
- 每個 thread/process 都要維護 TCP / TLS / PHP context
- 如果後端 PHP 執行慢:
- 連線被占用
- 新連線等待 → client 超時
- 高併發時記憶體爆掉
- Linux process scheduler 開銷大,context switch 成本高
結果:Apache 看起來「卡住」或「無法回收 thread」
