├── 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 | 110 | 111 | 112 | 113 | 114 |     120 | 121 | 122 | 123 |     128 | 129 |
消息头字段说明
Authorization取值为: WSSE realm="CLIENT1",profile="UsernameToken",type="Appkey"。
X-WSSE 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 |
Body-Sign 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 |
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 | --------------------------------------------------------------------------------