はじめに
nginxでRedisとやり取りする方法が気になったので、動的プロキシを作成しながら試してみる。
構成のイメージ
環境構築
今回はDockerで環境を構築していきます。
version: "3.9"
services:
redis:
image: "redis:7.4.1-alpine3.20"
expose:
- 6379
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping", "|", "grep", "PONG"]
timeout: 5s
retries: 5
start_period: 5s
app1:
image: "sample-app1:latest"
ports:
- 8080:8080
app2:
image: "sample-app2:latest"
ports:
- 8081:8081
nginx:
image: "openresty/openresty:alpine"
ports:
- 80:80
volumes:
- ./nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
redis:
condition: service_healthy
nginxはデフォルトではRedisとやり取りできないため、Luaモジュールをダウンロードする必要があります。
今回はLuaと統合しているOpenRestyを使用してます。
nginx設定
今回は最低限の設定のみ入れてます。
(抜粋)
location / {
# 事前に$target変数を定義
set $target "";
# Luaスクリプトで振り分け先を取得してプロキシ設定
access_by_lua_block {
local redis = require "resty.redis"
local cache = ngx.shared.redis_cache
-- ヘッダからIDを取得
local id = ngx.req.get_headers()["ID"]
if not id then
ngx.log(ngx.ERR, "id header is missing")
return ngx.exit(ngx.HTTP_BAD_REQUEST)
end
-- キャッシュからデータを取得
local cached_value = cache:get(id)
if cached_value then
ngx.log(ngx.INFO, "Cache hit: ", cached_value)
ngx.var.target = cached_value
return
end
local red = redis:new()
-- Redis接続設定
red:set_timeout(1000) -- タイムアウト設定 (ms)
local ok, err = red:connect("redis", 6379)
if not ok then
ngx.log(ngx.ERR, "failed to connect to Redis: ", err)
return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
ngx.log(ngx.INFO, "Redis access ok")
-- Redisでidに対応する振り分け先を取得
local target, err = red:get(id)
if not target or target == ngx.null then
ngx.log(ngx.ERR, "Target not found for id: ", id)
return ngx.exit(ngx.HTTP_NOT_FOUND)
end
cache:set(id, target)
-- 振り分け先の設定
ngx.var.target = target
-- Redis接続を閉じる
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.log(ngx.ERR, "failed to set keepalive: ", err)
end
}
# 振り分け先を動的に設定するプロキシ
proxy_pass http://$target;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
access_by_lua_block
はリクエストがきたタイミングでLuaスクリプトを実行させることができます。
今回は全てのリクエストに対して、毎回Luaスクリプトが実行されます。
Luaスクリプトでngx.exit
をするとnginx 自体の処理が終了します。
単純にreturnだけしている箇所は、Luaスクリプトの処理を終了し、nginxの処理が続行されます。
なので、流れとしては、
①リクエストが来るとLuaスクリプトが実行
②ヘッダからIDを取得
③キャッシュに存在すればキャッシュからデータを取得し、Luaスクリプトを終了し、プロキシする
④キャッシュにデータが存在しなければRedisに接続し、値を取得し、キャッシュに保存し、プロキシする
という設定にしてます。
動作確認
今回APP1にプロキシされた場合、「アプリ1です」というレスポンスが帰ってきて、App2にプロキシされた場合、「アプリ2です」というレスポンスが返ってくるようにしてます。
RedisにKey、Valueをセット
/data # redis-cli set 001 app1:8080
OK
/data # redis-cli set 002 app2:8081
OK
リクエスト投げてみる
curl --location 'localhost:80/sample' \
--header 'Content-Type: application/json' \
--header 'ID: 001'
アプリ1です
curl --location 'localhost:80/sample' \
--header 'Content-Type: application/json' \
--header 'ID: 001'
アプリ1です
キャッシュされているかどうかみるために2回リクエストしてます。
curl --location 'localhost:80/sample' \
--header 'Content-Type: application/json' \
--header 'ID: 002'
アプリ2です
ログ
2024/11/23 02:18:05 [info] 7#7: *1 [lua] access_by_lua(nginx.conf:22):30: Redis access ok, client: xxx.xx.x.x, server: , request: "GET /sample HTTP/1.1", host: "localhost"
xxx.xx.x.x - - [23/Nov/2024:02:18:05 +0000] "GET /sample HTTP/1.1" 200 16 "-" "curl/8.4.0"
2024/11/23 02:18:05 [info] 7#7: *1 client xxx.xx.x.x closed keepalive connection
2024/11/23 02:18:21 [info] 7#7: *5 [lua] access_by_lua(nginx.conf:22):15: Cache hit: app1:8080, client: xxx.xx.x.x, server: , request: "GET /sample HTTP/1.1", host: "localhost"
xxx.xx.x.x - - [23/Nov/2024:02:18:21 +0000] "GET /sample HTTP/1.1" 200 16 "-" "curl/8.4.0"
2024/11/23 02:18:21 [info] 7#7: *5 client xxx.xx.x.x closed keepalive connection
2024/11/23 02:18:40 [info] 7#7: *7 [lua] access_by_lua(nginx.conf:22):30: Redis access ok, client: xxx.xx.x.x, server: , request: "GET /sample HTTP/1.1", host: "localhost"
xxx.xx.x.x - - [23/Nov/2024:02:18:40 +0000] "GET /sample HTTP/1.1" 200 16 "-" "curl/8.4.0"
2024/11/23 02:18:40 [info] 7#7: *7 client xxx.xx.x.x closed keepalive connection
1回目のApp1へのアクセスはキャッシュがないので、Redisへ接続してデータを取得していますね。
2回目はキャッシュにヒットしているので、Redisへ接続してないです。
3回目はApp2へのアクセスなので、キャッシュがなく、Redisへ接続してデータを取得しています。
想定通りの結果になってますね。
さいごに
nginxで使用するレベルのLuaならさほど難しくなかったので、割と簡単に実装できますね。
Redisのライセンス変更もあって、これからはValkeyに変更されていくのではないかと思うので、時間ある時に、Valkeyに変更した場合、ソースに変更が必要なのか試して見たいと思います。
参考