├── lua
├── access.lua
├── rewrite.lua
├── init.lua
├── balancer.lua
└── api.lua
├── conf
├── api.conf
├── http_block.conf
└── http_block_healthcheck.conf
├── config.json
├── nginx.conf
├── lualib
├── config.lua
├── hmac.lua
├── configurable.lua
├── wsse_auth.lua
├── server.lua
└── healthcheck.lua
└── README.md
/lua/access.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :access阶段处理认证
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | -- 执行认证
8 | auth.run()
--------------------------------------------------------------------------------
/lua/rewrite.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :rewrite阶段
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | -- 刷新配置,如果需要的话,保证每个worker配置都一致
8 | config:init()
--------------------------------------------------------------------------------
/conf/api.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 9527;
3 | location ~ (^/servers|^/users) {
4 | default_type application/json;
5 | content_by_lua_file conf/lua/api.lua;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "users":["firstep"],
3 | "servers":{
4 | "normal" : {"ups1":[["192.168.56.101",8080,false]],"ups2":[["192.168.56.101",8081,true]]},
5 | "gray" : {"ups1":[["192.168.56.101",8083,false]]}
6 | },
7 | "auth":{
8 | "app":{
9 | "app1":"d56d4e239a4e90a6916d00b38a65b62b",
10 | "app2":"f1a6d56d4e2910d8d966370e239a65b3"
11 | },
12 | "clients":["CLIENTNAME1", "CLIENTNAME2"]
13 | },
14 | "balance-policy":1
15 | }
16 |
--------------------------------------------------------------------------------
/lua/init.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :初始化
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | ngx.log(ngx.INFO, "init: ", "init something...")
8 |
9 | --STEP.1 加载所需要的模块,并赋值到全局变量
10 | cjson = require "cjson"
11 | balancer = require "ngx.balancer"
12 | config = require "firstep.config"
13 | auth = require "firstep.wsse_auth"
14 | server = require "firstep.server"
15 |
16 | local ok, err = config:reload()
17 | if not ok then
18 | error("init config failed: " .. err)
19 | end
--------------------------------------------------------------------------------
/conf/http_block.conf:
--------------------------------------------------------------------------------
1 | #设置lualib的路径
2 | lua_package_path "/usr/local/openresty/lualib/?.lua;;"; #lua 模块
3 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; #c模块
4 |
5 | #正式环境务必把code缓存打开
6 | lua_code_cache on;
7 |
8 | #定义需要的shared_dict
9 | lua_shared_dict config 5m;
10 | lua_shared_dict healthcheck 2m;
11 |
12 | init_by_lua_file conf/lua/init.lua;
13 | rewrite_by_lua_file conf/lua/rewrite.lua;
14 | access_by_lua_file conf/lua/access.lua;
15 |
16 | upstream dynamic_backend {
17 | server 127.0.0.1;
18 | balancer_by_lua_file conf/lua/balancer.lua;
19 | keepalive 1024;
20 | }
21 |
22 | include conf/api.conf;
--------------------------------------------------------------------------------
/lua/balancer.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :动态负载服务列表
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | --STEP.1 获取当前upstream的名称,并确定是否灰度账号,获取下一个负载节点
8 | local streamType = ngx.var.ups;
9 | local user = ngx.req.get_headers()["username"];
10 |
11 | local peer, err = server.next_peer(streamType, user)
12 | if not peer then
13 | ngx.log(ngx.ERR, "failed to select next peer: ", err)
14 | return ngx.exit(500)
15 | end
16 |
17 | --STEP.2 转到选定的服务
18 | local ok, err = balancer.set_current_peer(peer[1], peer[2])
19 | if not ok then
20 | ngx.log(ngx.ERR, "failed to set the current peer: ", err)
21 | return ngx.exit(500)
22 | end
23 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | worker_processes 4; #nginx worker 数量
3 | error_log logs/error.log debug; #指定错误日志文件路径,及日志级别
4 | events {
5 | worker_connections 1024;
6 | }
7 |
8 | http {
9 | include conf/http_block.conf;
10 | #include conf/http_block_healthcheck.conf;
11 |
12 | server {
13 | listen 80;
14 |
15 | location / {
16 | default_type text/plain;
17 | content_by_lua 'ngx.say("hello world!")';
18 | }
19 |
20 | location /ups1 {
21 | set $ups 'ups1';
22 | proxy_pass http://dynamic_backend;
23 | }
24 |
25 | location /ups2 {
26 | set $ups 'ups2';
27 | proxy_pass http://dynamic_backend;
28 | }
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/conf/http_block_healthcheck.conf:
--------------------------------------------------------------------------------
1 | #一定先引入http_block.conf
2 | #健康检查脚本,配置
3 | init_worker_by_lua_block {
4 | local hc = require "tmss.healthcheck"
5 |
6 | local ok, err = hc.spawn_checker{
7 | key = "healthcheck", -- 定义的lua_shared_dict
8 | upstream = "resource", -- 需要监控的upstream
9 | http_req = "GET /status HTTP/1.0\r\nHost: resource\r\n\r\n", -- 请求的http
10 | interval = 5000, -- 定时间隔
11 | timeout = 2000, -- 请求超时时间
12 | valid_statuses = {200, 302},-- 验证的http状态码
13 | concurrency = 1, -- 并发数
14 | fall = 5, -- 连续验证失败累积次数,达到将状态设置down
15 | rise = 2 -- 连续验证成功累积次数,达到将状态设置up
16 | }
17 | if not ok then
18 | ngx.log(ngx.ERR, "failed to spawn health checker: ", err)
19 | end
20 | }
--------------------------------------------------------------------------------
/lualib/config.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :服务配置类
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | local shared = ngx.shared
8 |
9 | -- 初始化全局缓存dict
10 | local cfg_dict = shared["config"]
11 | if not cfg_dict then
12 | error("shared key \"config\" not found.")
13 | end
14 |
15 | local ok, configurable = pcall(require, "firstep.configurable")
16 | if not ok then
17 | error("configurable module required")
18 | end
19 | ----------------------------------------------------------
20 |
21 |
22 | --========================================================================
23 | --
24 | -- 对外提供的服务方法
25 | --
26 | --========================================================================
27 |
28 | local _M = {
29 | _VERSION = '0.01',
30 | cfg_dict = cfg_dict,
31 | cfg_path = "../conf/config.json", -- 文件的erveyone的写权限需设置
32 | chg_flag_key = "CHG_FLAG_FOR_SRV", -- 配置对象更改标志key
33 | cfg_key = "config", -- 配置文件源文本在shared中的key
34 | m_clients = nil
35 | }
36 |
37 | setmetatable(_M, {__index = configurable}) --继承自configurable
38 |
39 | function _M.get_config(key)
40 | return _M.m_cfg[key]
41 | end
42 |
43 | function _M.get_servers(key)
44 | if key == nil then
45 | return _M.m_cfg.servers
46 | else
47 | return _M.m_cfg.servers[key]
48 | end
49 | end
50 |
51 | function _M.get_users(key)
52 | if key == nil then
53 | return _M.m_cfg.users
54 | else
55 | return _M.m_cfg.users[key]
56 | end
57 | end
58 |
59 | function _M.get_auth()
60 | return _M.m_cfg.auth;
61 | end
62 |
63 | function _M.get_auth_clients()
64 | return _M.m_clients;
65 | end
66 |
67 | function _M.get_balance_policy()
68 | return _M.m_cfg["balance_policy"];
69 | end
70 |
71 | --复写父类的reload
72 | function _M:reload()
73 | local ok, err = getmetatable(self).__index.reload(self)
74 | if not ok then
75 | return ok, err
76 | end
77 |
78 | --把client复写为map方式放在成员变量中
79 | local clients = self.m_cfg["auth"]["clients"]
80 | if clients == nil then
81 | return nil, "not found clients item."
82 | end
83 | self.m_clients = nil
84 | self.m_clients = {}
85 | for i,v in ipairs(clients) do
86 | self.m_clients[v] = true
87 | end
88 |
89 | ngx.log(ngx.DEBUG, "reload auth config: ", cjson.encode(self.m_clients))
90 |
91 | return true
92 | end
93 |
94 | return _M
95 |
--------------------------------------------------------------------------------
/lua/api.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :对外提供api
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 |
8 | local uri = ngx.var.uri;
9 | local method = ngx.req.get_method()
10 |
11 | local function err_msg(err)
12 | local ret = {result = "faild", reason = err}
13 | return cjson.encode(ret)
14 | end
15 |
16 | local function check_server_body(data)
17 | if data.gray == nil then
18 | ngx.say(err_msg("params [gray] is require."))
19 | return nil
20 | end
21 |
22 | if data.upstream == nil then
23 | ngx.say(err_msg("params [upstream] is require."))
24 | return nil
25 | end
26 |
27 | if data.servers == nil then
28 | ngx.say(err_msg("params [servers] is require."))
29 | return nil
30 | end
31 |
32 | if type(data.servers) ~= "table" then
33 | ngx.say(err_msg("params [servers] is not array."))
34 | return nil
35 | end
36 | return true
37 | end
38 |
39 | if "/servers" == uri then --服务设置接口
40 | if "GET" == method then
41 | ngx.say(server.get_servers());
42 | elseif "POST" == method or "DELETE" == method then
43 | local raw = ngx.ctx["body_raw"];
44 | local ok, data = pcall(cjson.decode, raw)
45 | if not ok then
46 | ngx.say(err_msg("param is invalid: " .. data))
47 | return
48 | end
49 |
50 | local ok = check_server_body(data)
51 | if not ok then
52 | return
53 | end
54 |
55 | --FIXME 验证服务列表格式是否正确
56 |
57 | local ok, err
58 | if "POST" == method then
59 | ok, err = server.add_server(data.gray, data.upstream, data.servers, true)
60 | else
61 | ok, err = server.del_server(data.gray, data.upstream, data.servers)
62 | end
63 |
64 | if ok then
65 | ngx.say([[{"result":"success"}]])
66 | else
67 | ngx.say(err_msg(err))
68 | end
69 | else
70 | ngx.exit(ngx.HTTP_NOT_ALLOWED)
71 | end
72 | elseif "/servers/switch" == uri and "POST" == method then --接口切换
73 | local raw = ngx.ctx["body_raw"];
74 | local ok, data = pcall(cjson.decode, raw)
75 | if not ok then
76 | ngx.say(err_msg("param is invalid: " .. data))
77 | return
78 | end
79 |
80 | local ok = check_server_body(data)
81 | if not ok then
82 | return
83 | end
84 |
85 | local ok, err = server.switch_server(data.gray, data.upstream, data.servers)
86 | if not ok then
87 | ngx.log(ngx.ERR, "add node faild: ", err)
88 | ngx.say(err_msg("switch node faild."))
89 | return
90 | else
91 | ngx.say([[{"result":"success"}]])
92 | end
93 |
94 | elseif "/users" == uri then --用户设置接口
95 | if "GET" == method then
96 | ngx.say(server.get_users());
97 | elseif "POST" == method or "DELETE" == method then
98 | local raw = ngx.ctx["body_raw"];
99 | local ok, data = pcall(cjson.decode, raw)
100 | if not ok then
101 | ngx.say(err_msg("param is invalid: " .. data))
102 | return
103 | end
104 |
105 | if type(data) ~= "table" then
106 | ngx.say(err_msg("param is not array"))
107 | return
108 | end
109 |
110 | local ok, err
111 | if "POST" == method then
112 | ok, err = server.add_user(data)
113 | else
114 | ok, err = server.del_user(data)
115 | end
116 |
117 | if ok then
118 | ngx.say([[{"result":"success"}]])
119 | else
120 | ngx.say(err_msg(err))
121 | end
122 | else
123 | ngx.exit(ngx.HTTP_NOT_ALLOWED)
124 | end
125 | else
126 | ngx.exit(ngx.HTTP_NOT_ALLOWED)
127 | end
128 |
--------------------------------------------------------------------------------
/lualib/hmac.lua:
--------------------------------------------------------------------------------
1 |
2 | local str_util = require "resty.string"
3 | local ffi = require "ffi"
4 | local ffi_new = ffi.new
5 | local ffi_str = ffi.string
6 | local ffi_gc = ffi.gc
7 | local C = ffi.C
8 | local setmetatable = setmetatable
9 | local error = error
10 |
11 |
12 | local _M = { _VERSION = '0.01' }
13 |
14 | local mt = { __index = _M }
15 |
16 |
17 | ffi.cdef[[
18 | typedef struct engine_st ENGINE;
19 | typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;
20 | typedef struct env_md_ctx_st EVP_MD_CTX;
21 | typedef struct env_md_st EVP_MD;
22 |
23 | struct env_md_ctx_st
24 | {
25 | const EVP_MD *digest;
26 | ENGINE *engine;
27 | unsigned long flags;
28 | void *md_data;
29 | EVP_PKEY_CTX *pctx;
30 | int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count);
31 | };
32 |
33 | struct env_md_st
34 | {
35 | int type;
36 | int pkey_type;
37 | int md_size;
38 | unsigned long flags;
39 | int (*init)(EVP_MD_CTX *ctx);
40 | int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count);
41 | int (*final)(EVP_MD_CTX *ctx,unsigned char *md);
42 | int (*copy)(EVP_MD_CTX *to,const EVP_MD_CTX *from);
43 | int (*cleanup)(EVP_MD_CTX *ctx);
44 |
45 | int (*sign)(int type, const unsigned char *m, unsigned int m_length, unsigned char *sigret, unsigned int *siglen, void *key);
46 | int (*verify)(int type, const unsigned char *m, unsigned int m_length, const unsigned char *sigbuf, unsigned int siglen, void *key);
47 | int required_pkey_type[5];
48 | int block_size;
49 | int ctx_size;
50 | int (*md_ctrl)(EVP_MD_CTX *ctx, int cmd, int p1, void *p2);
51 | };
52 |
53 | typedef struct hmac_ctx_st
54 | {
55 | const EVP_MD *md;
56 | EVP_MD_CTX md_ctx;
57 | EVP_MD_CTX i_ctx;
58 | EVP_MD_CTX o_ctx;
59 | unsigned int key_length;
60 | unsigned char key[128];
61 | } HMAC_CTX;
62 |
63 | void HMAC_CTX_init(HMAC_CTX *ctx);
64 | void HMAC_CTX_cleanup(HMAC_CTX *ctx);
65 |
66 | int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len,const EVP_MD *md, ENGINE *impl);
67 | int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, size_t len);
68 | int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *len);
69 |
70 | const EVP_MD *EVP_md5(void);
71 | const EVP_MD *EVP_sha1(void);
72 | const EVP_MD *EVP_sha256(void);
73 | const EVP_MD *EVP_sha512(void);
74 | ]]
75 |
76 | local buf = ffi_new("unsigned char[64]")
77 | local res_len = ffi_new("unsigned int[1]")
78 | local ctx_ptr_type = ffi.typeof("HMAC_CTX[1]")
79 | local hashes = {
80 | MD5 = C.EVP_md5(),
81 | SHA1 = C.EVP_sha1(),
82 | SHA256 = C.EVP_sha256(),
83 | SHA512 = C.EVP_sha512()
84 | }
85 |
86 |
87 | _M.ALGOS = hashes
88 |
89 |
90 | function _M.new(self, key, hash_algo)
91 | local ctx = ffi_new(ctx_ptr_type)
92 |
93 | C.HMAC_CTX_init(ctx)
94 |
95 | local _hash_algo = hash_algo or hashes.md5
96 |
97 | if C.HMAC_Init_ex(ctx, key, #key, _hash_algo, nil) == 0 then
98 | return nil
99 | end
100 |
101 | ffi_gc(ctx, C.HMAC_CTX_cleanup)
102 |
103 | return setmetatable({ _ctx = ctx }, mt)
104 | end
105 |
106 |
107 | function _M.update(self, s)
108 | return C.HMAC_Update(self._ctx, s, #s) == 1
109 | end
110 |
111 |
112 | function _M.final(self, s, hex_output)
113 |
114 | if s ~= nil then
115 | if C.HMAC_Update(self._ctx, s, #s) == 0 then
116 | return nil
117 | end
118 | end
119 |
120 | if C.HMAC_Final(self._ctx, buf, res_len) == 1 then
121 | if hex_output == true then
122 | return str_util.to_hex(ffi_str(buf, res_len[0]))
123 | end
124 | return ffi_str(buf, res_len[0])
125 | end
126 |
127 | return nil
128 | end
129 |
130 |
131 | function _M.reset(self)
132 | return C.HMAC_Init_ex(self._ctx, nil, 0, nil, nil) == 1
133 | end
134 |
135 | return _M
--------------------------------------------------------------------------------
/lualib/configurable.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :可持久化的配置父类,抽象整个配置读取和覆盖过程
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | local debug_mode = ngx.config.debug
8 |
9 | -- 加载fii
10 | local ok, ffi = pcall(require, "ffi")
11 | if not ok then
12 | error("ffi module required")
13 | end
14 |
15 | ffi.cdef[[
16 | struct timeval {
17 | long int tv_sec;
18 | long int tv_usec;
19 | };
20 | int gettimeofday(struct timeval *tv, void *tz);
21 | ]]
22 |
23 | local tm = ffi.new("struct timeval")
24 |
25 | local function errlog( ... )
26 | ngx.log(ngx.ERR, "config: ", ...)
27 | end
28 |
29 | local function debug( ... )
30 | if debug_mode then
31 | ngx.log(ngx.DEBUG, "config: ", ...)
32 | end
33 | end
34 |
35 | local function get_time_millis()
36 | ffi.C.gettimeofday(tm, nil)
37 | local sec = tonumber(tm.tv_sec)
38 | local usec = tonumber(tm.tv_usec)
39 | return sec + usec * 10^-6;
40 | end
41 |
42 | local function read_config(path)
43 | local file = io.open(path, "r")
44 | if file == nil then
45 | return false, "Load file failed."
46 | end
47 | local data = file:read("*a") --读取所有
48 | file:close()
49 |
50 | return true, data
51 | end
52 |
53 | local function save_config(path, text)
54 | local file, err = io.open(path, "w")
55 | if file == nil then
56 | return false
57 | else
58 | file:write(text)
59 | file:close()
60 | return true
61 | end
62 | end
63 |
64 | local _M = {
65 | cfg_path = "",
66 | chg_flag_key = "",
67 | curr_chg_flag = "",
68 | cfg_key = "",
69 | cfg_dict = nil,
70 | m_cfg = nil
71 | }
72 |
73 | function _M:flush()
74 | debug("flush_config...")
75 | if(self.m_cfg == nil) then
76 | return false, "object config is null"
77 | end
78 | local cfg_srt = cjson.encode(self.m_cfg);
79 | local ok = save_config(self.cfg_path, cfg_srt)
80 |
81 | if ok then
82 | self:notify()
83 | return self.cfg_dict:set(self.cfg_key, cfg_srt)
84 | else
85 | self:reload()
86 | return false, "save config file failed."
87 | end
88 | end
89 |
90 | function _M:reload()
91 | debug("load config: " .. self.cfg_path)
92 | local cfg_str = self.cfg_dict:get(self.cfg_key)
93 | if not cfg_str then
94 | debug("load config from file...")
95 | local ok, data = read_config(self.cfg_path)
96 | if not ok then
97 | return false, data
98 | end
99 |
100 | cfg_str = data
101 |
102 | local suc,err,forcible = self.cfg_dict:set(self.cfg_key, data)
103 | if not suc then
104 | return false, "set data["..data.."] to sharedDIC error:"..err
105 | else
106 | ngx.log(ngx.INFO, "init local config success:"..data)
107 | end
108 | end
109 |
110 | self.m_cfg = cjson.decode(cfg_str)
111 | return true
112 |
113 | end
114 |
115 | function _M:notify()
116 | local flag = tostring(get_time_millis())
117 | local ok, err = self.cfg_dict:set(self.chg_flag_key, flag)
118 | if not ok then
119 | errlog("key: cfg_change not found, ", err)
120 | return ok, err
121 | end
122 | self.curr_chg_flag = flag
123 | return true
124 | end
125 |
126 | function _M:init()
127 | debug("init config.")
128 |
129 | --避免配置在多个阶段都执行判断
130 | if ngx.ctx["init:" .. self.chg_flag_key] then
131 | return true
132 | else
133 | ngx.ctx["init:" .. self.chg_flag_key] = true
134 | end
135 |
136 | local flag = self.cfg_dict:get(self.chg_flag_key)
137 | if flag and (self.curr_chg_flag ~= flag) then
138 | self.curr_chg_flag = flag
139 | self:reload()
140 | end
141 |
142 | return true
143 | end
144 |
145 | function _M:test()
146 | ngx.say(self.cfg_path)
147 | ngx.say(cjson.encode(self.m_cfg))
148 | end
149 |
150 | return _M
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 项目特性
2 | * 通过nginx + lua 实现动态切换upstream
3 | * 灰度用户路由到灰度的服务列表
4 | * 服务健康检查
5 | * 对外发布RESTful接口动态修改配置(通过WSSE进行认证)
6 |
7 | ## 使用说明
8 | * 安装nginx + lua环境,这里直接选择[openrestry](https://openresty.org/cn/installation.html)
9 | * 将源码lualib下的文件拷贝到/usr/local/openresty/lualib/firstep目录。(/usr/local/openresty为[openrestry](https://openresty.org/cn/installation.html)的默认安装路径)
10 | * 把conf、lua、config.json等文件及目录放置到nginx的conf目录下(如:/usr/local/openresty/nginx/conf),修改config.json权限为666
11 | * 修改/usr/local/openresty/nginx/conf/nginx.conf,如:
12 | ```nginx
13 | worker_processes 4; #nginx worker 数量
14 | error_log logs/error.log debug; #指定错误日志文件路径,及日志级别
15 | events {
16 | worker_connections 1024;
17 | }
18 |
19 | http {
20 | #STEP.1.在http模块引入conf/http_block.conf
21 | include conf/http_block.conf;
22 | #STEP.2.需要健康检查还得引入以下语句
23 | #include conf/http_block_healthcheck.conf;
24 |
25 | server {
26 | listen 80;
27 |
28 | location / {
29 | default_type text/plain;
30 | content_by_lua 'ngx.say("hello world!")';
31 | }
32 |
33 | location /ups1 {
34 | #STEP.3.设置当前location代理所对应的upstream,用ups变量设置
35 | set $ups 'ups1';
36 | proxy_pass http://dynamic_backend;
37 | }
38 |
39 | location /ups2 {
40 | set $ups 'ups2';
41 | proxy_pass http://dynamic_backend;
42 | }
43 | }
44 | }
45 | ```
46 |
47 | ## API说明
48 | ### Servers
49 | #### list server
50 | ```c++
51 | curl http://ip:9527/servers
52 | //返回格式:
53 | {
54 | "normal": {
55 | "upstream1": [
56 | [
57 | "192.168.56.101", //服务ip
58 | 8081, //服务端口
59 | true, //服务是否健康
60 | 0 //服务负载权重
61 | ],
62 | ...
63 | ],
64 | ...
65 | },
66 | "gray": {
67 | //和normal格式一样
68 | }
69 | }
70 | ```
71 | #### add server
72 | ```c++
73 | curl -X POST -d "{\"gray\": true, \"upstream\":\"ups1\", \"servers\":[[\"10.10.1.1\", 8080]]}" http://ip:9527/servers
74 | ```
75 |
76 | #### del server
77 | ```c++
78 | curl -X DELETE -d "{\"gray\": true, \"upstream\":\"ups1\", \"servers\":[[\"10.10.1.1\", 8080]]}" http://ip:9527/servers
79 | ```
80 |
81 | #### switch server
82 | ```c++
83 | curl -X POST -d "{\"gray\": true, \"upstream\":\"ups1\", \"servers\":[[\"10.10.1.1\", 8080]]}" http://ip:9527/servers/switch
84 | ```
85 | ### Users
86 | #### list users
87 | ```c++
88 | curl http://ip:9527/users
89 | ```
90 | #### add users
91 | ```c++
92 | curl -X POST -d "[\"username1\", \"username2\"]" http://ip:9527/servers
93 | ```
94 |
95 | #### del server
96 | ```c++
97 | curl -X DELETE -d "[\"username1\", \"username2\"]" http://ip:9527/servers
98 | ```
99 | ### 认证
100 | WS-SE认证:APK KEY认证方式,校验请求消息头中“Appkey+AppSecret”进行鉴权。鉴权通过后,处理此条请求消息,否则返回鉴权失败原因
101 | #### 请求头
102 |
103 |
104 |
105 | | 消息头字段 |
106 | 说明 |
107 |
108 |
109 | | Authorization |
110 | 取值为: WSSE realm="CLIENT1",profile="UsernameToken",type="Appkey"。 |
111 |
112 |
113 | | X-WSSE |
114 |
115 | 取值为: UsernameToken Username="App Key的值", PasswordDigest="PasswordDigest的值", Nonce="随机数", Created="随机数生成时间"。 PasswordDigest:根据公式PasswordDigest = Base64 (SHA256 (Nonce + Created + Password))生成。其中,Password即App Secret的值。
116 | Nonce:App发送请求时生成的一个不超过36位的随机数,允许是数字或字母。例如,QWERTYUIOPASDFGHJKLZXCVBNM1234567890。
117 | Created:随机数生成时间。采用标准UTC格式,为yyyy-MM-dd'T'HH:mm:ss 'Z'。例如,2017-07-09T13:07:06Z。
118 | 其中,Appkey为DIC分配的应用接入密钥。App Secret为DIC分配的应用接入密钥密码。
119 | |
120 |
121 |
122 | | Body-Sign |
123 |
124 | 取值为:sign_type="HMAC-SHA256", signature= "加密后的值";
125 | sign_type:加密方式,默认为"HMAC-SHA256"
126 | signature:根据公式base64_encode(HMAC_SHA256(http body, key)生成;请求消息中key=appKey&appSecret&Nonce,响应消息中key=appKey&appSecret。Key中包含”&”
127 | |
128 |
129 |
130 |
131 |
132 | #### 请求头样例
133 | Authorization: WSSE realm="CLIENT1", profile="UsernameToken", type="Appkey"
134 | X-WSSE: UsernameToken Username="", PasswordDigest="", Nonce="", Created=""
135 | Body-Sign: sign_type="HMAC-SHA256",signature="qwertyuiopasdfghjklZxcvbnnnnnnnnnnnm12234"
136 |
137 | #### 注意
138 | GET请求只校验appkey和appsecret。
139 | 非GET请求必须校验body-sign。
140 |
141 | ## 后续
142 | * 可视化后台
143 | * 自定义灰度路由规则
144 | * 流量限制
145 | * 请求数据聚合展示
146 |
--------------------------------------------------------------------------------
/lualib/wsse_auth.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :access阶段进行WS-SE验证
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | local hmac = require "firstep.hmac"
8 | local resty_sha256 = require "resty.sha256"
9 | local base64 = require "resty.core.base64"
10 | local str = require "resty.string"
11 | local re_find = ngx.re.find
12 | local sub = string.sub
13 |
14 | --========================================================================
15 | --
16 | -- 工具方法
17 | --
18 | --========================================================================
19 |
20 | -- 生成appkey和appsecret的密文
21 | local function gen_pwd_digest(secret_key, nonce, created)
22 | local sha256 = resty_sha256:new()
23 | sha256:update(tostring(nonce) .. tostring(created) .. secret_key)
24 | local digest = sha256:final()
25 | return ngx.encode_base64(digest)
26 | end
27 |
28 | -- 生成postbody的签名
29 | local function gen_body_sign(key, raw_body)
30 | local hmac_sha256 = hmac:new(key, hmac.ALGOS.SHA256)
31 | if not hmac_sha256 then
32 | return nil, "failed to create the hmac_sha256 object"
33 | end
34 | hmac_sha256:update(raw_body)
35 | local digest = hmac_sha256:final()
36 | return ngx.encode_base64(digest)
37 | end
38 |
39 | local function parse_realm()
40 | local text = ngx.req.get_headers()["Authorization"]
41 | if not text then
42 | return nil, [[http request header "Authorization" is not found]]
43 | end
44 |
45 | local from, to = re_find(text,[[realm="([^"]+)"]], "joi", nil, 1)
46 | if from == nil or to == nil then
47 | return nil, [[header "Authorization" can't found realm.]]
48 | end
49 | return sub(text, from, to)
50 | end
51 |
52 | local function parse_username_token()
53 | local token = ngx.req.get_headers()["X-WSSE"]
54 | if not token then
55 | return nil, nil, nil, nil, [[http request header "X-WSSE" is not found]]
56 | end
57 | local from, to = re_find(token,[[Username="([^"]+)"]], "joi", nil, 1)
58 | if from == nil or to == nil then
59 | return nil, nil, nil, nil, [[header "X-WSSE" can't found username.]]
60 | end
61 | local uname = sub(token, from, to)
62 |
63 | local from, to = re_find(token,[[PasswordDigest="([^"]+)"]], "joi", nil, 1)
64 | if from == nil or to == nil then
65 | return nil, nil, nil, nil, [[header "X-WSSE" can't found PasswordDigest.]]
66 | end
67 | local pwd_digest = sub(token, from, to)
68 |
69 | local from, to = re_find(token,[[Nonce="([^"]+)"]], "joi", nil, 1)
70 | if from == nil or to == nil then
71 | return nil, nil, nil, nil, [[header "X-WSSE" can't found Nonce.]]
72 | end
73 | local nonce = sub(token, from, to)
74 |
75 | local from, to = re_find(token,[[Created="([^"]+)"]], "joi", nil, 1)
76 | if from == nil or to == nil then
77 | return nil, nil, nil, nil, [[header "X-WSSE" can't found Created.]]
78 | end
79 | local created = sub(token, from, to)
80 |
81 | return uname, nonce, created, pwd_digest
82 | end
83 |
84 | local function parse_sign_body()
85 | local token = ngx.req.get_headers()["Body-Sign"]
86 | if not token then
87 | return nil, [[http request header "Body-Sign" is not found]]
88 | end
89 | local from, to = re_find(token,[[signature="([^"]+)"]], "joi", nil, 1)
90 | if from == nil or to == nil then
91 | return nil, [[header "Body-Sign" can't found signature.]]
92 | end
93 | local signature = sub(token, from, to)
94 | return signature
95 | end
96 |
97 | --========================================================================
98 | --
99 | -- 对外提供的服务方法
100 | --
101 | --========================================================================
102 |
103 | local _M = {}
104 |
105 | function _M.run()
106 |
107 | local ok, err = _M.check_realm()
108 | if not ok then
109 | ngx.log(ngx.DEBUG, "auth faild: ", err)
110 | return ngx.exit(ngx.HTTP_UNAUTHORIZED)
111 | end
112 |
113 | local app_key, nonce, err = _M.check_identity()
114 | if not app_key or not nonce then
115 | ngx.log(ngx.DEBUG, "auth faild: ", err)
116 | return ngx.exit(ngx.HTTP_UNAUTHORIZED)
117 | end
118 |
119 | if ngx.req.get_method() ~= "GET" then
120 | ngx.req.read_body();
121 | local raw = ngx.req.get_body_data();
122 | local ok, err = _M.check_body_sign(app_key, nonce, raw)
123 | if not ok then
124 | ngx.log(ngx.DEBUG, "auth faild: ", err)
125 | return ngx.exit(ngx.HTTP_UNAUTHORIZED)
126 | end
127 | ngx.ctx["body_raw"] = raw
128 | end
129 | end
130 |
131 | function _M.check_identity()
132 | local k, n, c, p, err = parse_username_token()
133 | if not k then
134 | return false, err
135 | end
136 |
137 | local secret_key = system.get_config()["auth"]["app"][k]
138 | if not secret_key then
139 | return nil, nil, "invalid app key"
140 | end
141 |
142 | local _digest, err = gen_pwd_digest(secret_key, n, c)
143 | if not _digest then
144 | return nil, nil, err
145 | end
146 |
147 | if _digest ~= p then
148 | return nil, nil, "the app-key and app-secret is not match"
149 | end
150 | return k, n
151 | end
152 |
153 | function _M.check_body_sign(app_key, nonce, raw_body)
154 | local signature, err = parse_sign_body()
155 | if not signature then
156 | return nil, err
157 | end
158 |
159 | local secret_key = system.get_config()["auth"]["app"][app_key]
160 | if not secret_key then
161 | return nil, "invalid app key"
162 | end
163 |
164 | local _signature, err = gen_body_sign(app_key .. "&" .. secret_key .. "&" .. nonce, raw_body)
165 | if not _signature then
166 | return nil, err
167 | end
168 | if _signature ~= signature then
169 | return nil, "the request body signature is not valid"
170 | end
171 | return true
172 | end
173 |
174 | -- 检查认证的领域
175 | function _M.check_realm()
176 | local realm, err = parse_realm()
177 | if not realm then
178 | return nil, err
179 | end
180 |
181 | if config.get_auth_clients()[realm] then
182 | return true
183 | else
184 | return nil, "invalid realm."
185 | end
186 | end
187 |
188 | return _M
--------------------------------------------------------------------------------
/lualib/server.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :服务配置类
3 | author :firstep@qq.com
4 | date :20170609
5 | ]]
6 |
7 | local IDX_HOST = 1;
8 | local IDX_PORT = 2;
9 | local IDX_LIVE = 3;
10 | local IDX_WEIGHT = 4;
11 |
12 | local POLICY_DEFAULT = 0;
13 | local POLICY_WEIGHT = 1;
14 | local POLICY_IPHASH = 2;
15 |
16 | --========================================================================
17 | --
18 | -- 工具方法
19 | --
20 | --========================================================================
21 | local function comp(v1, v2, is_array)
22 | if is_array then
23 | return v1[IDX_HOST] == v2[IDX_HOST] and v1[IDX_PORT] == v2[IDX_PORT]
24 | else
25 | return v1 == v2
26 | end
27 | end
28 |
29 | local function merge(dest, src, is_array)
30 | local find = false;
31 | for i, v in pairs(src) do
32 | find = false;
33 | for j, _v in pairs(dest) do
34 | if comp(v, _v, is_array) then
35 | --把live状态及权重合并
36 | if is_array then
37 | if v[IDX_LIVE] ~= nil then
38 | _v[IDX_LIVE] = v[IDX_LIVE]
39 | end
40 | if v[IDX_WEIGHT] ~= nil and v[IDX_WEIGHT] > 0 then
41 | _v[IDX_WEIGHT] = v[IDX_WEIGHT]
42 | end
43 | end
44 | find = true;
45 | break;
46 | end
47 | end
48 | if find == false then
49 | if is_array then
50 | if v[IDX_LIVE] == nil then
51 | v[IDX_LIVE] = true
52 | end
53 | if v[IDX_WEIGHT] == nil then
54 | v[IDX_WEIGHT] = 1
55 | end
56 | end
57 | table.insert(dest, v)
58 | end
59 | end
60 | end
61 |
62 | local function remove(dest, src, is_array)
63 | for i, v in pairs(src) do
64 | --逆向遍历
65 | for j = #dest, 1, -1 do
66 | if comp(v, dest[j], is_array) then
67 | table.remove(dest, j)
68 | break;
69 | end
70 | end
71 | end
72 | end
73 |
74 | -- 在目标数组中能找到一个就算ok
75 | local function contains(array, val, is_array)
76 | for _,v in ipairs(array) do
77 | if comp(v, val, is_array) then
78 | return true
79 | end
80 | end
81 | return false
82 | end
83 |
84 | local function modify(dest, src, is_array, is_add)
85 | if is_add then
86 | merge(dest, src, is_array)
87 | else
88 | remove(dest, src, is_array)
89 | end
90 | end
91 |
92 | --========================================================================
93 | --
94 | -- 负载算法
95 | --
96 | --========================================================================
97 | local function weighted_round_robin(peers)
98 | local host
99 | local weight = 0
100 | local total = 0
101 | local best_weight = 0
102 | local best_peer = nil
103 | local best_host
104 | for i,peer in ipairs(peers) do
105 | if peer[IDX_LIVE] == true then
106 | weight = peer[IDX_WEIGHT] or 1
107 | total = total + weight
108 |
109 | host = "cw:" .. peer[IDX_HOST] .. ":" .. peer[IDX_PORT]
110 | local curr_weight, err = cfg_dict:incr(host, weight, 0)
111 |
112 | if best_peer == nil or curr_weight > best_weight then
113 | best_peer = peer
114 | best_weight = curr_weight
115 | best_host = host
116 | end
117 | end
118 | end
119 |
120 | if best_peer == nil then
121 | return nil, "not selected."
122 | end
123 | cfg_dict:incr(best_host, total * -1)
124 | return best_peer
125 | end
126 |
127 | local function round_robin(key, len)
128 | local v,err = cfg_dict:incr(key, 1, 0);
129 | local ret = (v % len) + 1;
130 | if ret == 1 then
131 | cfg_dict:set(key, 0)
132 | end
133 | return ret;
134 | end
135 |
136 | local function get_real_ip()
137 | local ip = ngx.req.get_headers()["X-Real-IP"]
138 | if ip == nil then
139 | ip = ngx.req.get_headers()["x_forwarded_for"]
140 | end
141 | if ip == nil then
142 | ip = ngx.var.remote_addr
143 | end
144 | return ip;
145 | end
146 |
147 | local function ip_hash(key, len)
148 | local ip = get_real_ip()
149 | local hash = ngx.crc32_long(ip .. key)
150 | hash = (hash % len) + 1
151 | return hash;
152 | end
153 |
154 | --========================================================================
155 | --
156 | -- 对外提供的服务方法
157 | --
158 | --========================================================================
159 |
160 | local _M = {_VERSION = '0.01'}
161 |
162 | -- 获取所有服务节点
163 | function _M.get_servers()
164 | local tab = {normal = config.get_servers("normal"), gray = config.get_servers("gray")}
165 | return cjson.encode(tab)
166 | end
167 |
168 | -- 获取指定upstream所有节点
169 | function _M.get_peers(is_gray, upstream)
170 | local kind = is_gray and "gray" or "normal"
171 | return config.get_servers()[kind][upstream]
172 | end
173 |
174 | -- 获取指定upstream存活的服务
175 | function _M.get_up_peers(is_gray, upstream)
176 | local peers = _M.get_peers(is_gray, upstream)
177 | if not peers then
178 | return peers
179 | end
180 |
181 | local ret = {}
182 | for _,v in ipairs(peers) do
183 | if v[IDX_LIVE] then
184 | table.insert(ret, v)
185 | end
186 | end
187 | if #ret == 0 then
188 | return nil
189 | else
190 | return ret
191 | end
192 | end
193 |
194 | --新增或修改服务
195 | function _M.add_server(is_gray, upstream, array, valid)
196 | if valid then
197 | local _peers = _M.get_peers(not is_gray, upstream)
198 | if not _peers then
199 | return nil, "kind of peer \"" ..upstream .. ":"..tostring(is_gray).."\" not found."
200 | end
201 | for _,v in ipairs(array) do
202 | if contains(_peers, v, true) then
203 | return nil, "this host[" .. v[1] .. ":" .. v[2] .. "] is exist, statu is " .. (is_gray and "normal" or "gray")
204 | end
205 | end
206 | end
207 |
208 | local peers = _M.get_peers(is_gray, upstream)
209 | if not peers then
210 | return nil, "kind of peer \"" ..upstream .. ":"..tostring(is_gray).."\" not found."
211 | end
212 |
213 | modify(peers, array, true, true)
214 | return config:flush()
215 | end
216 |
217 | -- 删除服务
218 | function _M.del_server(is_gray, upstream, array)
219 | local peers = _M.get_peers(is_gray, upstream)
220 | if not peers then
221 | return nil, "kind of peer \"" ..upstream .. ":"..tostring(is_gray).."\" not found."
222 | end
223 |
224 | modify(peers, array, true, false)
225 | return config:flush()
226 | end
227 |
228 | -- 服务节点在灰度和正常节点中转换
229 | function _M.switch_server(is_gray, upstream, array)
230 | local ok, err = _M.add_server(not is_gray, upstream, array)
231 | if not ok then
232 | return nil, err
233 | end
234 | local ok, err = _M.del_server(is_gray, upstream, array)
235 | if not ok then
236 | return nil, err
237 | end
238 | return true
239 | end
240 |
241 | -- 获取所有的灰度用户列表
242 | function _M.get_users()
243 | return cjson.encode(config.get_users())
244 | end
245 |
246 | -- 增加灰度账号
247 | function _M.add_user(array)
248 | modify(config.get_users(), array, false, true)
249 | return config:flush()
250 | end
251 |
252 | -- 删除灰度用户
253 | function _M.del_user(array)
254 | modify(config.get_users(), array, false, false)
255 | return config:flush()
256 | end
257 |
258 | -- 以轮训的方式获取下个负载节点
259 | function _M.next_peer(upstream, username)
260 | local is_gray = username and contains(config.get_users(), username, false)
261 | local peers = _M.get_up_peers(is_gray, upstream)
262 | if not peers then
263 | return nil, "kind of peer \"" ..upstream .. ":"..tostring(is_gray).."\" not found."
264 | end
265 |
266 | local size = #peers
267 | if size == 1 then
268 | return peers[1]
269 | end
270 |
271 | local curr_policy = config.get_balance_policy()
272 | local peer
273 | if POLICY_WEIGHT == curr_policy then
274 | peer = weighted_round_robin(peers)
275 | elseif POLICY_IPHASH == curr_policy then
276 | local index = ip_hash("rr:"..(is_gray and "1" or "0")..upstream, size)
277 | peer = peers[index]
278 | else
279 | local index = round_robin("rr:"..(is_gray and "1" or "0")..upstream, size)
280 | peer = peers[index]
281 | end
282 |
283 | if peer then
284 | ngx.log(ngx.DEBUG, "config: ", "select next peer, user:"..(username or "nil").." upstream:"..upstream.." gray:"..tostring(is_gray).." peer:"..peer[1]..":"..peer[2])
285 | end
286 | return peer
287 | end
288 |
289 | return _M
--------------------------------------------------------------------------------
/lualib/healthcheck.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | desc :upstream健康检查类
3 | author :firstep@qq.com
4 | date :20170228
5 | ]]
6 |
7 | local new_timer = ngx.timer.at
8 | local shared = ngx.shared
9 | local spawn = ngx.thread.spawn
10 | local wait = ngx.thread.wait
11 | local stream_sock = ngx.socket.tcp
12 | local re_find = ngx.re.find
13 | local sub = string.sub
14 |
15 | local GRAY_PEER = true
16 | local NORMAL_PEER = false
17 |
18 | local IDX_HOST = 1;
19 | local IDX_PORT = 2;
20 | local IDX_LIVE = 3;
21 |
22 | g_need_flush = false --用于标记,当前定时检查做完后,是否需要flush配置到配置文件
23 |
24 | local _M = {}
25 |
26 | local ok, new_tab = pcall(require, "table.new")
27 | if not ok or type(new_tab) ~= "function" then
28 | new_tab = function (narr, nrec) return {} end
29 | end
30 |
31 | local function debug( ... )
32 | ngx.log(ngx.DEBUG, "healthcheck: ", ...)
33 | end
34 |
35 | local function errlog( ... )
36 | ngx.log(ngx.ERR, "healthcheck: ", ...)
37 | end
38 |
39 | local function peer_err(ctx, peer, ...)
40 | local peer_live = peer[IDX_LIVE]
41 |
42 | debug("check result: failed live: "..tostring(peer_live))
43 | if peer_live then
44 | errlog(...)
45 | else
46 | return --本来就是下线的就不再继续了
47 | end
48 |
49 | local peer_name = peer[IDX_HOST] .. ":" .. tostring(peer[IDX_PORT])
50 | local key = "down:" .. peer_name
51 |
52 | local count, err = ctx.dict:incr(key, 1, 0)
53 | if not count then
54 | errlog("failed to set peer down key: ", key, " err: ", err)
55 | return
56 | end
57 |
58 | debug("peer: ", key, " fail count: ", count)
59 |
60 | if count >= ctx.fall then
61 | debug("peer: ", key, " will down.")
62 | ctx.dict:delete(key)
63 | peer[IDX_LIVE] = false
64 | g_need_flush = true
65 | end
66 | end
67 |
68 | local function peer_ok(ctx, peer)
69 | local peer_live = peer[IDX_LIVE]
70 | debug("check result: ok live: "..tostring(peer_live))
71 |
72 | if peer_live then
73 | return --本来存活的服务就不再继续了
74 | end
75 | local peer_name = peer[IDX_HOST] .. ":" .. tostring(peer[IDX_PORT])
76 | local key = "rise:" .. peer_name
77 |
78 | local count, err = ctx.dict:incr(key, 1, 0)
79 | if not count then
80 | errlog("failed to set peer up key: ", key, " err: ", err)
81 | return
82 | end
83 |
84 | debug("peer: ", key, " success count: ", count)
85 |
86 | if count >= ctx.rise then
87 | debug("peer: ", key, " will up.")
88 | ctx.dict:delete(key)
89 | peer[IDX_LIVE] = true
90 | g_need_flush = true
91 | end
92 | end
93 |
94 | local function check_peer(ctx, peer)
95 | local ok, err
96 | local req = ctx.http_req
97 | local statuses = ctx.statuses
98 | local peer_name = peer[IDX_HOST] .. ":" .. tostring(peer[IDX_PORT])
99 | debug("do checking peer: ".. peer_name)
100 |
101 | local sock, err = stream_sock()
102 | if not sock then
103 | errlog("failed to create stream socket", err)
104 | return
105 | end
106 |
107 | sock:settimeout(ctx.timeout)
108 |
109 | ok, err = sock:connect(peer[IDX_HOST], peer[IDX_PORT])
110 |
111 | if not ok then
112 | return peer_err(ctx, peer,
113 | "failed to connect to ", peer_name, " : ", err)
114 | end
115 |
116 | local bytes, err = sock:send(req)
117 | if not bytes then
118 | return peer_err(ctx, peer,
119 | "failed to send request to ", peer_name, " : ", err)
120 | end
121 |
122 | local status_line, err = sock:receive()
123 | if not status_line then
124 | peer_err(ctx, peer,
125 | "failed to receive status line from ", peer_name, " : ", err)
126 | if err == "timeout" then
127 | sock:close()
128 | end
129 | return
130 | end
131 |
132 | if statuses then
133 | local from, to, err = re_find(status_line,
134 | [[^HTTP/\d+\.\d+\s+(\d+)]],
135 | "joi", nil, 1)
136 | if not from then
137 | peer_err(ctx, peer,
138 | "bad status line from ", peer_name, " : ", status_line)
139 | sock:close()
140 | return
141 | end
142 |
143 | local status = tonumber(sub(status_line, from, to))
144 | if not statuses[status] then
145 | peer_err(ctx, peer,
146 | "bad status code from ", peer_name, " : ", status)
147 | sock:close()
148 | return
149 | end
150 | end
151 |
152 | peer_ok(ctx, peer)
153 | sock:close()
154 | end
155 |
156 | local function check_peer_range(ctx, from, to, peers)
157 | for i = from, to do
158 | check_peer(ctx, peers[i])
159 | end
160 | end
161 |
162 | local function check_peers(ctx, peers)
163 | local n = #peers
164 | if n == 0 then
165 | return
166 | end
167 |
168 | local concur = ctx.concurrency
169 | if concur <= 1 then
170 | for i=1, n do
171 | check_peer(ctx, peers[i])
172 | end
173 | else
174 | local threads
175 | local nthr
176 |
177 | if n <= concur then
178 | nthr = n - 1
179 | threads = new_tab(nthr, 0)
180 | for i = 1, nthr do
181 | debug("spawn a thread checking", " peer ", i - 1)
182 | threads[i] = spawn(check_peer, ctx, peers[i])
183 | end
184 |
185 | debug("check peer ", n - 1)
186 | check_peer(ctx, peers[n])
187 | else
188 | local group_size = math.ceil(n / concur)
189 | local nthr = math.ceil(n / group_size) - 1
190 |
191 | threads = new_tab(nthr, 0)
192 | local from = 1
193 | local to
194 | for i = 1, nthr do
195 | to = from + group_size -1
196 |
197 | debug("spawn a thread checking peers ", from -1, " to ", to - 1)
198 |
199 | threads[i] = spawn(check_peer_range, ctx, from, to, peers)
200 |
201 | from = from + group_size
202 | end
203 |
204 | if from <= n then
205 | to = n
206 | debug("spawn a thread checking peers ", from -1, " to ", to - 1)
207 |
208 | check_peer_range(ctx, from, to, peers)
209 | end
210 | end
211 |
212 | --等待线程结束
213 | if nthr and nthr > 0 then
214 | for i = 1, nthr do
215 | local t = threads[i]
216 | if t then
217 | wait(t)
218 | end
219 | end
220 | end
221 | end
222 |
223 | debug("check peers done.")
224 | end
225 |
226 | local function get_lock(ctx)
227 | local dict = ctx.dict
228 | local key = "l:" .. ctx.upstream
229 | local ok, err = dict:add(key, true, ctx.interval - 0.001)
230 | if not ok then
231 | if err == "exists" then
232 | debug("key " .. key .. " is exists.")
233 | return nil
234 | end
235 | errlog("failed to add key \"", key, "\": ", err)
236 | return nil
237 | end
238 | return true
239 | end
240 |
241 | local function do_check(ctx)
242 | debug("run a check cycle")
243 |
244 | if not get_lock(ctx) then
245 | debug("no lock exit.")
246 | return
247 | end
248 |
249 | debug("get lock...")
250 |
251 | g_need_flush = false
252 | config:init()
253 |
254 | local normal_peers = config.get_peers(NORMAL_PEER, ctx.upstream)
255 | if not normal_peers then
256 | return nil, "not found normal peers"
257 | end
258 | local gray_peers = config.get_peers(GRAY_PEER, ctx.upstream)
259 | if not gray_peers then
260 | return nil, "not found gray peers"
261 | end
262 |
263 | --debug(config.get_servers())
264 |
265 | check_peers(ctx, normal_peers)
266 | check_peers(ctx, gray_peers)
267 |
268 | if g_need_flush then
269 | local ok, err = config:flush()
270 | if not ok then
271 | errlog("flush config fail: ", err)
272 | return false, err
273 | end
274 | end
275 | debug("all check done.")
276 | return true
277 | end
278 |
279 | local check
280 | check = function(premature, ctx)
281 | if premature then
282 | return
283 | end
284 |
285 | local ok, err = pcall(do_check, ctx)
286 | if not ok then
287 | errlog("failed to run healthcheck cycle:", err)
288 | end
289 |
290 | local ok, err = new_timer(ctx.interval, check, ctx)
291 | if not ok then
292 | if err ~= "process exiting" then
293 | errlog("failed to create timer: ", err)
294 | end
295 | end
296 | end
297 |
298 | function _M.spawn_checker(opts)
299 | local http_req = opts.http_req
300 | if not http_req then
301 | return nil, "\"http_req\" option required"
302 | end
303 |
304 | local timeout = opts.timeout or 1000
305 | local interval = opts.interval
306 | if not interval then
307 | interval = 1
308 | else
309 | interval = interval / 1000
310 | if interval < 0.002 then
311 | interval = 0.002
312 | end
313 | end
314 |
315 | local fall = opts.fall or 4
316 |
317 | local rise = opts.rise or 2
318 |
319 | local valid_statuses = opts.valid_statuses
320 | local statuses
321 | if valid_statuses then
322 | statuses = new_tab(0, #valid_statuses)
323 | for _,v in ipairs(valid_statuses) do
324 | statuses[v] = true
325 | end
326 | end
327 |
328 | local concur = opts.concurrency or 1
329 | local key = opts.key
330 | if not key then
331 | return nil, "\"key\" option required"
332 | end
333 |
334 | local dict = shared[key]
335 | if not dict then
336 | return nil, "key \"" .. tostring(key) .. " \" not found"
337 | end
338 |
339 | local u = opts.upstream
340 | if not u then
341 | return nil, "no upstream specified"
342 | end
343 |
344 | local ctx = {
345 | upstream = u, -- 当前检查的upstream
346 | http_req = http_req, -- 请求体
347 | timeout = timeout, -- 检查超时时间
348 | interval = interval, -- 执行间隔
349 | fall = fall, -- 服务累积错误次数达到时把服务down
350 | rise = rise, -- 服务累积成功次数达到把服务up
351 | dict = dict, -- 健康检查所需要的缓存
352 | statuses = statuses, -- 验证通过的状态码列表
353 | concurrency = concur -- 最大线程并发数
354 | }
355 |
356 | local ok, err = new_timer(0, check, ctx)
357 | if not ok then
358 | return nil, "failed to create timmer:" .. err
359 | end
360 |
361 | return true
362 | end
363 |
364 | return _M
365 |
--------------------------------------------------------------------------------