nginx_lua案例分析:動態路由實現

    這裏的路由指的是在web開發中,訪問路徑以及具體實現內容的映射。比如,/a映射到某個具體的頁面,這個就稱之爲一個路由。而動態路由,顧名思義就是動態添加這種路由映射關係。

    在nginx中,通過rewrite和proxy_pass來實現路由映射或者說反向代理,但是這種關係按照傳統的配置必須寫死在配置文件中,然後通過快速"無縫"重啓nginx。雖說是無縫,但是其繁瑣的配置和枯燥的重啓操作還是無法避免。

    最近,在github上看到個項目ceryx,是nginx結合lua進行動態路由的映射的,也就是上面所說的映射關係,用lua來管理,雖然是比較簡單的實現,但是可以分析學習下。該項目通過用redis的<source,target>結構來保存這種映射關係,這樣在nginx中可以快速獲得這種關係,以便做出處理,同時,採用HTTP的形式暴露對redis這種路由關係進行管理的接口。

from ceryx.db import RedisRouter


resource_fields = {
    'source': fields.String,
    'target': fields.String,
}

parser = reqparse.RequestParser()
parser.add_argument(
    'source', type=str, required=True, help='Source is required'
)
parser.add_argument(
    'target', type=str, required=True, help='Target is required'
)

router = RedisRouter.from_config()


def lookup_or_abort(source):
    """
    Returns the target for the given source, or aborts raising a 404
    """
    try:
        return {'source': source, 'target': router.lookup(source)}
    except RedisRouter.LookupNotFound:
        abort(
            404, message='Route with source {} doesn\'t exist'.format(source)
        )


class Route(Resource):
    """
    Resource describing a single Route. Supports GET, DELETE and PUT. The
    format is the following:
    ```
    {
        "source": "[source]",
        "target": "[target]"
    }
    ```
    """

    @marshal_with(resource_fields)
    def get(self, source):
        """
        Fetches the route with the given source
        """
        route = lookup_or_abort(source)
        return route

    @marshal_with(resource_fields)
    def delete(self, source):
        """
        Deletes the route with the given source
        """
        route = lookup_or_abort(source)
        router.delete(source)
        return route, 204

    @marshal_with(resource_fields)
    def put(self, source):
        """
        Creates or updates the route with the given source, pointing to the
        given target
        """
        args = parser.parse_args()
        router.insert(args['source'], args['target'])
        return args, 201

    上述的代碼,是進行路由管理的http api,當然,這個不是我們分析的重點。先來看一下,在這個項目裏面,對nginx的配置是怎樣的

upstream fallback {
    server www.something.com;
}

server {
    listen 80;
    default_type text/html;

    location / {
        set $container_url "fallback";
        resolver 8.8.8.8;

        # Lua files
        access_by_lua_file lualib/router.lua;//切入點

        # Proxy configuration
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect ~^(http://[^:]+):\d+(/.+)$ $2;
        proxy_redirect / /;

        # Upgrade headers
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_pass http://$container_url$request_uri;
    }
    ...
}
可以簡單的看到,這個配置相當的常見,跟普通的反向代理並沒有什麼不同,真正的切入點在於access_by_lua_file裏面的router.lua代碼。

local container_url = ngx.var.container_url//拿到配置文件中的container_url
local host = ngx.var.host  //拿到請求的時候的host,比如我們請求http://www.xxx.com/a.html 那這裏的host就是www.xxx.com

-- Check if key exists in local cache
local cache = ngx.shared.ceryx
local res, flags = cache:get(host)   //直接在nginx cache中拿host對應的路由映射,如果存在則直接返回結果
if res then
    ngx.var.container_url = res
    return
end

local redis = require "resty.redis"  // redis的連接代碼  每次都會連接redis,
local red = redis:new()<span style="white-space:pre">			</span>//<span style="font-family: Arial, Helvetica, sans-serif;">這個操作比較相對比較耗時 所以接下來的操作纔會在本地cache中存對應的關係</span>
red:set_timeout(100) -- 100 ms
local redis_host = os.getenv("CERYX_REDIS_HOST")
if not redis_host then redis_host = "127.0.0.1" end
local redis_port = os.getenv("CERYX_REDIS_PORT")
if not redis_port then redis_port = 6379 end
local res, err = red:connect(redis_host, redis_port)

-- Return if could not connect to Redis
if not res then
    return
end

-- Construct Redis key
local prefix = os.getenv("CERYX_REDIS_PREFIX")
if not prefix then prefix = "ceryx" end
local key = prefix .. ":routes:" .. host

-- Try to get target for host
res, err = red:get(key)
if not res or res == ngx.null then
    -- Construct Redis key for $wildcard
    key = prefix .. ":routes:$wildcard"
    res, err = red:get(key)
    if not res or res == ngx.null then
        return
    end
    ngx.var.container_url = res
    return
end

-- Save found key to local cache for 5 seconds
cache:set(host, res, 5)  // 在redis取出的映射關係存到redis的cache中避免下次繼續連redis操作

ngx.var.container_url = res
可以看出,這個項目分享的內容,並不盡人意,只是簡單的提供了一種思路,如何去實現動態的proxy_pass,在這個基礎上我們可以進行對url rewrite的擴展。另外,這裏的host對應的routeHost 如果只是IP,那樣的話,會造成proxy_pass的時候後端的單點,也就是沒有應用到upstream,沒辦法進行負載均衡。但是如果routeHost的值是upstream的話,則,在配置文件中,依然要寫死,所以,沒有辦法做到完全意義上的動態路由。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章