http路由
路由是指路由器从一个接口上收到数据包,根据数据包的目的地址进行定向并转发到另一个接口的过程。
而这里的http路由其实等同于web开发中,根据http相关参数(比如url、http method)分配到对应的处理程序。
借用web框架的示意图,其作用如下
路由匹配
这里我们先简化一下内容,假设我们已有 upstream ip、port,现在只需能区分各种请求怎么样对应到这些不同的upstream上,不必关心能否做改写请求啊、熔断啊等等复杂情况
那么怎么实现路由匹配呢?
通常为以下两种方式
-
字典+正则表达式
字典用于匹配精确的结果(比如 url == /login 情况),字典的特性保证这类匹配具有超高性能
正则表达式用于匹配复杂模糊的结果(比如 url 以 .html 为后缀的所有请求), 当然多项正则表达式只能依次遍历,性能肯定存在问题(为了缓解性能问题,通常会使用缓存做优化)
-
前缀树
前缀树,又称字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
其由于插入和查询的效率很高,非常适合路由匹配的情况
虽然理论hash性能最好,前缀树仍需查找,效率会低一些,
但毕竟通常开发都会使用比较复杂的路由, 所以效率肯定比上面的 字典+正则表达式 要高很多
路由匹配实践
这里由于篇幅关系,只介绍 字典+正则表达式
简单实现,前缀树
则介绍apisix 中实现的库,毕竟算法要强悍的性能,纯lua实现是不太可靠的,必须得上c才行,这里就避免c的复杂度吧。
字典+正则表达式
简单实现
worker_processes 1; #nginx worker 数量
error_log logs/error.log; #指定错误日志文件路径
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr [$time_local] $status $request_time $upstream_status $upstream_addr $upstream_response_time';
access_log logs/access.log main buffer=16384 flush=3; #access_log 文件配置
upstream nature_upstream {
server 127.0.0.1:6699; #upstream 配置为 hello world 服务
# 一样的balancer
balancer_by_lua_block {
local balancer = require "ngx.balancer"
local upstream = ngx.ctx.api_ctx.upstream
local ok, err = balancer.set_current_peer(upstream.host, upstream.port)
if not ok then
ngx.log(ngx.ERR, "failed to set the current peer: ", err)
return ngx.exit(ngx.ERROR)
end
}
}
init_by_lua_block {
local path_exact = {} -- 精确匹配字典
local path_reg = {} -- 正则遍历集合
-- 添加路由的方法
add_router = function(type, path, upstream)
if type == 'exact' then
path_exact[path] = upstream
else
table.insert(path_reg, {reg = path, upstream = upstream})
end
end
-- 匹配方法,优先精确匹配
router_match = function()
local p = ngx.var.uri
local upstream = path_exact[p]
if upstream == nil then
for k, v in pairs(path_reg) do
if ngx.re.find(p, v.reg) then
return v.upstream
end
end
end
return upstream
end
-- 添加测试数据
add_router('exact' , '/aa/d', {host = '127.0.0.1', port = 6698})
add_router('reg' , '/aa/*', {host = '127.0.0.1', port = 6699})
}
server {
#监听端口,若你的8699端口已经被占用,则需要修改
listen 8699 reuseport;
location / {
# 在access阶段匹配路由
access_by_lua_block {
local upstream = router_match()
if upstream then
ngx.ctx.api_ctx = { upstream = upstream }
else
ngx.exit(404)
end
}
proxy_http_version 1.1;
proxy_pass http://nature_upstream; #转发到 upstream
}
}
#为了大家方便理解和测试,我们引入一个hello world 服务
server {
#监听端口,若你的6699端口已经被占用,则需要修改
listen 6699;
location / {
default_type text/html;
content_by_lua_block {
ngx.say("HelloWorld")
}
}
}
}
启动服务并测试
$ openresty -p ~/openresty-test -c openresty.conf #启动
$ curl --request GET 'http://127.0.0.1:8699/aa/d' #测试精确匹配
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>
$ curl --request GET 'http://127.0.0.1:8699/aa/xxx' #测试正则匹配
HelloWorld
$ curl --request GET 'http://127.0.0.1:8699/dd/xxx' #测试不存的路由
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
</body>
</html>
核心原理其实就是 router_match
这个函数的内容,
不过上述简单实现肯定无法支持以下的一些复杂场景
- 正则冲突 (一般会引入优先级顺序支持,或者默认加入的顺序)
- host隔离
- 参数匹配
- 自定义条件匹配
- ……
所以一个完整的路由实现都很复杂,毕竟支持的场景挺多的
使用 lua-resty-radixtree
路由库
引入库
以下内容更新到 openresty-dev-1.rockspec
-- 依赖包
dependencies = {
"lua-resty-radixtree >= 2.8.2",
}
然后执行
luarocks install openresty-dev-1.rockspec --tree=deps --only-deps --local
代码调整
这里只列举变动的部分
http {
lua_package_path "$prefix/deps/share/lua/5.1/?.lua;$prefix/deps/share/lua/5.1/?/init.lua;$prefix/?.lua;$prefix/?/init.lua;;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua;";
lua_package_cpath "$prefix/deps/lib64/lua/5.1/?.so;$prefix/deps/lib/lua/5.1/?.so;;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;";
# 开启 lua code 缓存
lua_code_cache on;
init_by_lua_block {
-- 初始化路由
local radix = require("resty.radixtree")
local r = radix.new({
{paths = {'/aa/d'}, metadata = {host = '127.0.0.1', port = 6698}},
{paths = {'/aa/*'}, metadata = {host = '127.0.0.1', port = 6699}}
})
-- 匹配路由
router_match = function()
local upstream, err = r:match(ngx.var.uri, {})
if err then
log.error(err)
end
return upstream
end
}
}
启动服务并测试
$ openresty -p ~/openresty-test -c openresty.conf #启动
$ curl --request GET 'http://127.0.0.1:8699/aa/d' #测试精确匹配
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>
$ curl --request GET 'http://127.0.0.1:8699/aa/xxx' #测试正则匹配
HelloWorld
$ curl --request GET 'http://127.0.0.1:8699/dd/xxx' #测试不存的路由
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
</body>
</html>
可以看到效果一样
更多复杂使用请参阅 https://github.com/api7/lua-resty-radixtree