项目简介
基于openresty+nginx的api网关,实现了基本的动态路由、动态upstream、负载均衡、限流、降级的功能,同时提供了类似于nginx plus的统计功能
系统简图
目录
安装
- 方式一
- 项目运行依赖[OpenResty],安装了OpenResty的情况下可以跳过此安装步骤
- 方式二
- 自行编译[lua-nginx-module]、[lua-upstream-nginx-module]的Nginx,以及lua或者luajit
- 方式三
- 执行install目录下的install.sh来完成一键安装。项目默认将被安装在/home/phi-0.0.1目录下
./install.sh ${path_to_nginx_src}
- 请将/home/phi-0.0.1/sbin加入环境变量
- 执行install目录下的install.sh来完成一键安装。项目默认将被安装在/home/phi-0.0.1目录下
- 方式四
- 根据项目根目录下的Dockerfile制作docker镜像,并启动
- 项目默认安装在/home/phi目录下,默认暴露80,9090,12345端口
- 你需要在配置文件中指定redis的主机和端口
- 你应该将conf目录映射到外部路径中
启动和配置
- 确保nginx命令可执行,可以使用command -v nginx查看
- 使用bin目录下的phi:
- 用法: phi start|stop|restart|reload|test
- 可选参数 :
- -D: 启动远程debug模式,默认占用端口8172,使用参考:[ZeroBrane Studio],[EmmyLua]
- -H: 远程debug 主机名,默认localhost
- -P: 远程debug 端口,默认8172
- -p: nginx启动目录,同nginx -p参数
- -c: nginx启动配置文件名称,同nginx -c参数
- -d: 以非守护进程方式启动,同-g 'daemon off'
- 你也可以使用nginx启动命令来启动项目
- 你也可以自行指定nginx配置文件,但需要在你已存在的nginx配置中添加如下内容
- Nginx 配置
- 在nginx配置中添加如下内容
# 需要安装luajit/lua,xxx表示项目根路径,需要在nginx/tengine编译时添加如下模块lua_nginx_module(0.10.11)和lua_upstream_nginx_module(0.07),安装方式参考install/install.sh lua_package_path '../openresty/?.lua;../phi/?.lua;../lib/?.lua;;'; lua_package_cpath '../openresty/?.so;../lib/?.so;;'; # 如果使用了openresty,那么只需引入下面的包 #lua_package_path '../phi/?.lua;../lib/?.lua;;'; #lua_package_cpath '../lib/?.so;;'; #生产环境务必开启 lua_code_cache on; # 共享内存的大小按需调整 lua_shared_dict phi 5m; lua_shared_dict phi_events 5m; lua_shared_dict phi_lock 5m; lua_shared_dict phi_router 10m; lua_shared_dict phi_upstream 10m; lua_shared_dict phi_limiter 10m; lua_shared_dict phi_degrader 10m; lua_shared_dict phi_limit_req 128m; lua_shared_dict phi_limit_conn 128m; lua_shared_dict phi_limit_count 128m; # 开启mio统计面板的情况下需添加如下dict lua_shared_dict status 8m; lua_shared_dict summary 128m; lua_shared_dict recording_summary 128m; #lua入口文件 init_by_lua_file ../entry_lua_file/init_by_lua.lua; init_worker_by_lua_file ../entry_lua_file/init_worker_by_lua.lua; server { set $upstream_connection ''; listen 80; server_name phi_entry; location = /favicon.ico { log_not_found off; access_log off; } location / { set $backend ''; rewrite_by_lua_file ../entry_lua_file/rewrite_by_lua.lua; proxy_http_version 1.1; proxy_set_header Connection $upstream_connection; proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_pass http://$backend; } log_by_lua_file ../entry_lua_file/log_by_lua.lua; } upstream phi_upstream { server 0.0.0.1; balancer_by_lua_file ../entry_lua_file/balancer_by_lua.lua; # 请按需调整 keepalive 2000; }
- Nginx 配置
- Phi 配置
- 项目启动默认会读取bin目录和conf目录下的phi.ini,你可以创建此文件用来覆盖项目的默认配置
- phi/config/default_config.lua中定义了默认的配置项
return { debug = true, -- 是否开启远程debug debug_host = "127.0.0.1", -- debug的host debug_port = 8172, -- 远程debug占用端口 enabled_admin = true, -- 是否启动管理控制台 enabled_mio = true, -- 是否启动统计面板 enabled_policies = { "UNIQUE", "RANGE", "PREFIX", "SUFFIX", "COMPOSITE", "MODULO", "REGEX" }, -- 启用的policy规则 enabled_mappers = { "HEADER", "URI_ARGS", "IP", "URI" }, -- 启用的mapper类型 default_paths = { -- 默认的配置文件加载路径 conf/phi.ini 或者./phi.ini current_path .. "phi.ini", conf_path .. "phi.ini" }, application_context_conf = { -- 默认的application容器配置路径 conf/application.ini 或者./application.ini current_path .. "application.ini", conf_path .. "application.ini" } }
- 配置项读取优先级
- 环境变量中对应的值,环境变量取值会在所有的key前面加PHI_,例如debug对应环境变量中的PHI_DEBUG的值
- default_paths中指定的ini文件中配置的值
- default_config.lua中指定的默认值
启动管理控制台
- 如果你使用phi中的nginx配置,管理控制台默认是启用的,通过12345端口即可访问
- 当你使用自定义的nginx配置,你需要确保phi配置中enabled_admin=true,并在nginx配置中添加如下server
server {
listen 12345;
location / {
default_type application/json;
content_by_lua_file ../entry_lua_file/admin_content_by_lua.lua;
log_by_lua_file ../entry_lua_file/log_by_lua.lua;
}
}
启动统计面板
- 目前的统计功能因为涉及很多对Shared_Dict的操作,以及对ngx.var的使用在性能上会有一定损失,单核心下差距明显(50%),多核心下差距会缩小
- 在执行Nginx的编译前的配置中请添加 --with-http_stub_status_module,[OpenResty]默认开启,使用install中的安装脚本默认会添加此模块到nginx
- 数据统计默认是关闭的
- 在bin/或comf/下添加phi.ini设置enabled_admin=true开启,并在nginx配置中添加如下server(如果没有),通过12345端口可访问
server { listen 9090; access_log logs/api_access.log main; error_log logs/api_error.log info; set $template_root ""; location = /favicon.ico { log_not_found off; access_log off; } # 静态文件 location ~* /static/(.*) { alias ../static/$1; } location /robots.txt { return 200 'User-agent: *\nDisallow: /'; } location = / { try_files '' ../static/index.html; } location / { content_by_lua_file ../lib/mio/api/main.lua; } }
- 统计面板截图
- 更多可参考[MIO],对源代码做了一些修改
快速开始
- 参考默认配置
- 使用80端口作为api网关入口
- 使用9090端口作为统计面板入口
- 使用12345端口作为管理控制台api入口
- 将conf/vhost.conf引入nginx配置中,用来模拟后端服务器,分别占用5555、6666、7777、8888端口
- 添加一个api-server,使用主机名sample.com
curl -H 'content-type:application/json' -X POST --data \ '{"hostkey": "sample.com","data": {"default": "127.0.0.1:5555"}}' \ http://localhost:12345/router/add
- 访问刚才添加的api server
返回内容curl -H 'content-type:application/json' -H 'Host:sample.com' http://localhost
this is the fake backend peer... listen port 5555
- 添加一组命名为dynamic_ups的api server,负载均衡策略使用轮询,其它参考[Load Balance]
curl -H 'content-type:application/json' -X POST --data \ '{"upstreamName":"dynamic_ups","strategy":"roundrobin","mapper":"ip","servers":[{"name":"127.0.0.1:5555","info":{"weight":10}},{"name":"127.0.0.1:6666","info":{"weight":10}}]}' \ http://localhost:12345/upstream/addOrUpdateUps
- 将主机名sample.com指向的api server修改为dynamic_ups,其它api参考[Dynamic Upstream]
curl -H 'content-type:application/json' -X POST --data \ '{"hostkey": "sample.com","data": {"default": "dynamic_ups"}}' \ http://localhost:12345/router/add
- 访问刚才添加的api server
curl -H 'content-type:application/json' -H 'Host:sample.com' http://localhost
- 请求返回内容
this is the fake backend peer... listen port 5555
- 再次请求返回内容
this is the fake backend peer... listen port 6666
- 对主机名sample.com添加一条路由规则,取请求头中X-UID的值,值尾数为8的请求转发到8888端口,值尾数为7的请求转发到7777端口,其它请求转发到default中的dynamic_ups中进行负载均衡,其它路由规则参考[Router]
curl -H 'content-type:application/json' -X POST --data \ '{"hostkey":"sample.com","data":{"default":"dynamic_ups","policies":[{"order":2,"policy":"modulo","mapper":{"type":"header","tag":"X-UID"},"routerTable":[{"result":"127.0.0.1:8888","expression":8},{"result":"127.0.0.1:7777","expression":7}]}]}}' \ http://localhost:12345/router/add
- 访问刚才添加的api server
curl -H 'content-type:application/json' -H 'Host:sample.com' -H 'X-UID:123458' http://localhost
- 返回内容,取不到X-UID时,请求将会在5555/6666的server之间进行轮询
this is the fake backend peer... listen port 8888
- 对主机名sample.com添加一条限流规则,取用户IP值作为限流key,限制每10秒钟内只能成功访问1次,其它限流规则参考[Rate Limiting]
curl -H 'content-type:application/json' -X POST --data \ '{"hostkey":"sample.com","data":{"time_window":10,"rate":1,"type":"count","mapper":"ip"}}' \ http://localhost:12345/rate/add
- 访问刚才添加的api server
curl -H 'content-type:application/json' -H 'Host:sample.com' -H 'X-UID:123458' http://localhost
- 返回内容
this is the fake backend peer... listen port 8888
- 被限流后返回
{ "status": { "message": "Limited access,Service Temporarily Unavailable,please try again later :-)", "success": false }, "code": 503 }
- 对域名sample.com添加一条降级规则,并启动降级,所有的降级规则都是基于host+uri来进行的,当访问/test时返回执行降级策略,限流规则是优先于降级执行的,其它降级规则参考[Service Degradation]
curl -H 'content-type:application/json' -X POST --data \ '{"hostkey":"sample.com","infos":[{"uri":"/test","info":{"type":"fake","enabled":true,"target":"This is a degraded fake data"}}]}' \ http://localhost:12345/degrade/add
- 访问刚才添加的api server
curl -H 'content-type:application/json' -H 'Host:sample.com' -H 'X-UID:123458' http://localhost/test
- 返回内容
{"message":"This is a degraded fake data"}
- 再次访问将会进入上面添加的限流规则中
管理控制台API
-
枚举值
-
mapper
mapper是一组映射函数,会将请求映射到一个具体的值,以便将这个值交给policy进行运算, 请求参数中如果有mapper字段只需要传递相应的枚举值即可,如"mapper":"ip" 有一些mapper需要指定tag,此时mapper字段应传递一个json对象,如:{"type":"uri_args","tag":"uid"},会取出请求参数中的uid的值
value Description header
取请求头中指定字段,配合tag字段使用,tag值表示请求头的名称 host
取请求头/请求行中的host,无需指定tag ip
取请求头中的用户ip,无需指定tag uri_args
取请求中的uri参数,配合tag字段使用,tag值表示参数名称 uri
取请求行中的uri cookie
取请求头中的cookie,配合tag字段使用,tag值表示cookie名称 -
policy
policy是一组计算函数,
string calculate(arg, hashTable)
,此函数接收一个具体值和一个规则对象, 该对象中的数据结构应符合{"result":"xxx","expression":"xxxxxxxx"}的规则(组合规则除外), calculate函数会根据expression字段的表达式计算出一个boolean值,如果为true就立即返回result的值,否则就继续执行value Description 哈希表结构示例 modulo
对10取模运算 [{"result":"upstream1","expression":1},{"result":"upstream2","expression":2},{"result":"upstream3","expression":3}] range
范围运算 [{"result":"hehehehe","expression":[1001,2000]},{"result":"upstream1","expression":[min,"NONE"]},{"result":"upstream2","expression":[min,max]},{"result":"upstream3","expression":["NONE",max]}] prefix
取前缀匹配 [{"result":"upstream1","expression":"/api"},{"result":"upstream2","expression":"/openApi"}] suffix
取后缀匹配 [{"result":"upstream1","expression":".js"},{"result":"upstream2","expression":".css"}] regex
lua正则匹配 [{"result":"upstream1","expression":"regex1"},{"result":"upstream2","expression":"regex2"}] composite
组合规则 {"primary":{"order":2,mapper":"ip","policy":"range_policy","routerTable":[{"result":"secondary","expression":[1,100]},{"result":"upstream","expression":["NONE",1000]}]},"secondary":{"order":2,"mapper":"ip","policy":"range_policy","routerTable":[{"result":"secondary","expression":[1,100]}]}} unique
返回唯一结果 {"result":"upstream1"}
-
-
Router
- 动态路由,实现分流/灰度功能
- 添加路由规则
- 请求路径:/router/add
- 请求方式:POST
- 返回数据格式:JSON
- 请求字段说明:
- 请求数据示例:
- 以下示例表示:从uri参数中提取uid的值,根据值的范围进行匹配,小于2000的将会被路由到upstream1, 2001到3000之间的将被路由到upstream2,大于3000的将被路由到upstream3
{ "hostkey": "www.sample.com", "data": { "default": "an upstream name", "policies": [{ "order": 3, "policy": "range", "mapper": {"type":"uri_args","tag":"uid"}, "routerTable": { "upstream1": [ 2000, "NONE" ], "upstream2": [ 2001, 3000 ], "upstream3": [ "NONE", 3000 ] } }] } }
- 查询路由规则
- 请求路径:/router/get
- 请求方式:GET
- 请求数据格式:uri参数
- 请求数据示例:
hostkey=xxx
- 返回数据格式:JSON
- 返回数据示例:
{ "status": { "message": "ok", "success": true }, "code": 200, "data": { "policies": [ { "order": 3, "policy": "range", "mapper": {"type":"uri_args","tag":"uid"}, "routerTable": { "upstream4": [ 1001, 2000 ] } }, { "order": 2, "policy": "range", "mapper": "ip", "routerTable": { "upstream1": [ 1001, 100 ] } } ], "default": "phi_upstream" } }
- 查询所有路由规则
- 请求路径:/router/getAll
- 请求方式:GET
- 请求数据格式:uri参数
- 请求数据示例:
hostkey=xxx
- 返回数据格式:JSON
- 删除路由规则
- 请求路径:/router/del
- 请求方式:GET
- 请求数据格式: uri参数
- 请求数据示例:
hostkey=xxx
-
DynamicUpstream
- 动态的upstream列表,简单的负载均衡功能
- 查询所有运行中的upstream信息
- 请求路径:/upstream/getAllRuntimeInfo
- 请求方式:GET
- 返回数据格式:JSON
- 返回数据示例:
{ "status": { "message": "ok", "success": true }, "code": 200, "data": { "stable_ups": { "primary": [ { "weight": 1, "id": 0, "conns": 0, "fails": 0, "current_weight": 0, "fail_timeout": 10, "effective_weight": 1, "name": "127.0.0.1:8888", "max_fails": 1 } ], "backup": {} }, "dynamic_ups": { "mapper": "ip", "servers": [{ "weight": 10, "name": "127.0.0.1:5555", "down": false },{ "weight": 10, "name": "127.0.0.1:6666", "down": true }], "strategy": "roundrobin" } } }
- 查询所有upstream信息
- 请求路径:/upstream/getAllUpsInfo
- 请求方式:GET
- 返回数据格式:JSON
- 返回数据示例:
{ "status": { "message": "ok", "success": true }, "code": 200, "data": { "stable_ups": { "primary": [ { "weight": 1, "id": 0, "conns": 0, "fails": 0, "current_weight": 0, "fail_timeout": 10, "effective_weight": 1, "name": "127.0.0.1:8888", "max_fails": 1 } ], "backup": {} }, "dynamic_ups": { "mapper": "ip", "servers": [ { "name": "127.0.0.1:8989", "weight": 10 } ], "strategy": "resty_chash" } } }
- 查询指定upstream的server列表
- 请求路径:/upstream/getUpstreamServers
- 请求方式:GET/POST
- 请求数据格式:uri参数/表单参数
- 请求数据示例:
upstreamName=xxxx
- 返回数据格式:JSON
- 返回数据示例:
{ "status": { "message": "ok", "success": true }, "code": 200, "data": { "127.0.0.1:12346": { "weight": 100 }, "127.0.0.1:7777": { "weight": 10 }, "127.0.0.1:8888": { "weight": 10 }, "strategy": "resty_chash", "mapper": "ip" } }
- 从server列表中摘除/启动指定server,暂时不参与/重新参与负载均衡
- 请求路径:/upstream/setPeerDown
- 请求方式:GET/POST
- 请求数据格式:uri参数/表单参数
- 请求数据示例:
upstreamName=xxxx&serverName=xxx&down=true
- 添加或者更新指定upstream
- 请求路径:/upstream/addOrUpdateUps
- 请求方式:POST
- 请求数据格式:JSON
- 字段说明:
- 请求数据示例:
{ "upstreamName": "a new upstream", "strategy": "resty_chash", "mapper": "ip", "servers": [ { "name": "127.0.0.1:8989", "info": { "weight": 10 } }, { "name": "127.0.0.1:8989", "info": { "weight": 10 } } ] }
- 删除指定upstream
- 请求路径:/upstream/delUps
- 请求方式:GET
- 请求数据格式:uri参数
- 请求数据示例:
upstreamName=xxxx
- 从指定upstream中删除servers
- 请求路径:/upstream/delUpstreamServers
- 请求方式:POST
- 请求数据格式:JSON
- 请求数据示例:
{ "upstreamName": "a new upstream", "servers": [ "127.0.0.1:8989" ] }
- 向指定upstream中添加server,upstreamName不存在时会返回错误
- 请求路径:/upstream/addUpstreamServers
- 请求方式:POST
- 请求数据格式:JSON
- 请求数据示例:
{ "upstreamName":"an exists upstream name", "servers":[{ "name":"127.0.0.1:8989", "info":{ "weight":100 } }] }
-
RateLimiting
- 动态路由,实现分流/灰度功能
- 添加路由规则
- 请求路径:/rate/add
- 请求方式:POST
- 返回数据格式:JSON
- 请求字段说明
-
type:枚举值,多个规则排序,数值越大优先级越高
value Description req
限制请求速率,type=req时,需传递rate(速率),burst(允许突发值)两个参数
例如:rate=200,burst=100,表示允许200req/sec,流量突发时最多允许达到300req/sec,其中超过200req的请求将被delayconn
限制并发连接数,type=conn时,需传递conn(连接数),burst(允许突发连接数),delay(延迟时间)三个参数
例如conn=100,burst=100,delay=0.05,表示正常允许100并发连接,突发流量下最大允许200并发连接,其中超出的部分,将被deplaycount
限制单位时间窗口内的调用次数,type=count时,需传递rate(速率),time_window(单位时间窗口)两个参数
例如rate=1000,time_window=60,表示60秒内最多允许调用1000次,超出的请求会被拒绝traffic
组合限流,启动组合限流时,组合限流需在请求数据中添加policies字段,数组类型[{"time_window": 10,"rate": 1,"}],每一条数据为一条限流规则,如需对多个规则进行排序,请在每一条中添加order字段,数字越大优先级越高 -
mapper:可选值,未传递此参数的情况下,所有限流粒度都是以host作为限流key,请求映射函数,对应枚举mapper
-
tag:参考mapper中对tag的说明
-
rejected:拒绝策略,rejected:直接拒绝访问,degrade:查询降级策略
-
- 请求数据示例:
{ "hostkey": "www.sample.com", "data": { "time_window": 10, "rejected": "xxx", "rate": 1, "order": 1, "type": "count", "mapper":"ip" } }
- 查询路由规则
- 请求路径:/rate/get
- 请求方式:GET
- 请求数据格式:uri参数
- 请求数据示例:
hostkey=xxx
- 返回数据格式:JSON
- 查询所有路由规则
- 请求路径:/rate/getAll
- 请求方式:GET
- 请求数据格式:uri参数
- 请求数据示例:
hostkey=xxx
- 返回数据格式:JSON
- 删除路由规则
- 请求路径:/rate/del
- 请求方式:GET
- 请求数据格式: uri参数
- 请求数据示例:
hostkey=xxx
-
ServiceDegradation
- 接入层的简易降级开关,所有的降级都是以host+uri为依据
- 添加降级数据
- 请求路径:/degrade/add
- 请求方式:POST
- 返回数据格式:JSON
- 请求字段说明
-
type:枚举值
value Description fake
返回target中指定的数据 redirect
重定向到target指定的路径 -
"enabled": true/false,针对此uri的降级开关
-
- 请求数据示例:
{ "hostkey": "auto.deploy.com", "infos": [ { "uri": "/uri", "info": { "type": "redirect", "enabled": true, "target": "http://sample.com" } } ] }
- 查询路由规则
- 请求路径:/degrade/enabled
- 请求方式:GET
- 请求数据格式:uri参数
- 请求数据示例:
hostkey=xxx&uri=/xxxx
- 返回数据格式:JSON
- 查询路由规则
- 请求路径:/degrade/get
- 请求方式:GET
- 请求数据格式:uri参数
- 请求数据示例:
hostkey=xxx
- 返回数据格式:JSON
- 查询所有路由规则
- 请求路径:/degrade/getAll
- 请求方式:GET
- 请求数据格式:uri参数
- 返回数据格式:JSON
- 删除路由规则
- 请求路径:/degrade/del
- 请求方式:GET
- 请求数据格式: uri参数
- 请求数据示例:
hostkey=xxx&uri=/xxxx
性能测试
我在自己的笔记本上使用ab进行了简单的负载测试:
- 测试准备
- nginx配置:开启一个worker进程,并且最多占用一个cpu核心
worker_processes 1; worker_cpu_affinity auto;
- 关闭了访问日志,错误日志级别为error
- 开启另外一台nginx,作为一个静态文件服务器,目标文件大小为1Kb
- nginx配置:开启一个worker进程,并且最多占用一个cpu核心
- 多测试场景
- Local环境,所有相关应用都部署在同一机器,相当于忽略网络开销
- 测试机器配置
- 笔记本 I7-6700HQ + 16GB
- 使用AB进行压力测试,20个并发,发起1000000次请求,使worker进程cpu达到100%,启用-k
- 原生的nginx反向代理:50K T/S
- 开启router功能后测试结果:32K T/S
- 开启dynamic ups和balancer之后: 25K T/S
- 此测试应该可以反映,服务器在满负荷下的吞吐量极限值(完全没有网络开销),两者会有40%的差距,在开启统计功能的情况下差距会继续扩大(10%)
- 在开启多核情况下性能会有明显提升
- 测试机器配置
- 内网环境测试
- 测试机器配置
- 三台 KVM CPU:2C RAM:2G 千兆网卡
- 一台部署PHI应用,一台部署NGINX静态文件服务,一台压测
- 使用AB进行压力测试,100个并发,发起100000次请求,使worker进程cpu达到100%,启用-k,测试结果和上面是相似的
- 原生的nginx反向代理:21K T/S
- 开启router功能后测试结果:15K T/S
- 开启dynamic ups和balancer之后: 11K T/S
- 测试在保持大量并发连接数下的系统稳定性:
- TODO
- 测试机器配置
- Local环境,所有相关应用都部署在同一机器,相当于忽略网络开销
- 测试结果
- 在只考虑CPU负载的情况下,相较于nginx的原生代理,系统容量(TPS)会有接近50%的差距,而RT两者基本没有什么差别
- 在多核心的机器下,网络带宽应该会优先于cpu而达到瓶颈
- 性能测试是一项很复杂的工作,以任何一个单一指标来定性系统性能瓶颈都是不严谨的,我们在生产中也应按照应用特征、机器配置、网络状况进行综合分析。
使用的第三方lua库或工具
- mobdebug lua的远程调试工具
- classic 使用lua模拟面向对象编程
- kong kong的一些lua库
- lua-resty-mlcache lua的多级缓存工具,封装了lrucache和shared_dict的操作
- lua-resty-worker-events worker间异步通信
- EmmyLua IntelliJ idea的lua插件
- ZeroBrane Studio 轻量级的lua ide
- MIO resty的统计系统