├── img └── orange │ ├── db.png │ ├── sad.jpg │ ├── happy.jpg │ ├── rate.png │ ├── divide1.png │ ├── or_rule.png │ ├── perfect.jpg │ ├── redirect.png │ ├── rewrite.png │ ├── zhuangbi.jpg │ ├── orange_db.png │ ├── custom_liuliang.png │ ├── r_single_match.png │ └── s_all_continue_s_no_log.png ├── README.md ├── orange-divide.md ├── orange-plugin.md └── understanding-orange.md /img/orange/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/db.png -------------------------------------------------------------------------------- /img/orange/sad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/sad.jpg -------------------------------------------------------------------------------- /img/orange/happy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/happy.jpg -------------------------------------------------------------------------------- /img/orange/rate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/rate.png -------------------------------------------------------------------------------- /img/orange/divide1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/divide1.png -------------------------------------------------------------------------------- /img/orange/or_rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/or_rule.png -------------------------------------------------------------------------------- /img/orange/perfect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/perfect.jpg -------------------------------------------------------------------------------- /img/orange/redirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/redirect.png -------------------------------------------------------------------------------- /img/orange/rewrite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/rewrite.png -------------------------------------------------------------------------------- /img/orange/zhuangbi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/zhuangbi.jpg -------------------------------------------------------------------------------- /img/orange/orange_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/orange_db.png -------------------------------------------------------------------------------- /img/orange/custom_liuliang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/custom_liuliang.png -------------------------------------------------------------------------------- /img/orange/r_single_match.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/r_single_match.png -------------------------------------------------------------------------------- /img/orange/s_all_continue_s_no_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linger1216/understanding-orange/HEAD/img/orange/s_all_continue_s_no_log.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 深入理解orange 3 | 4 | 5 | 这些文章的契机, 其实只是我随意记录的笔记而已, 没有标题那么夸张, 严格说来只是个入门教程. 6 | 7 | 阅读orange能更好的理解ngxin和openresty的原理, 能更清晰的认识网关, 对我来说也是一个知识的梳理. 8 | 9 | 废话不多说, 希望这些小小的文章, 能帮助到你. 10 | 11 | 12 | 如果你看的还行, 请star下. 13 | 14 | 15 | [深入理解orange>>](./understanding-orange.md) 16 | 17 | [深入理解orange之Divide插件>>](./orange-divide.md) 18 | 19 | [深入理解orange之插件全家桶>>](./orange-plugin.md) 20 | -------------------------------------------------------------------------------- /orange-divide.md: -------------------------------------------------------------------------------- 1 | # 深入理解Orange之分流插件 2 | 3 | 4 | 5 | 相信在座的都看过`Orange`的`Issues`, 其中问题最多的应该就是分流插件, 本着柿子捡**硬**的捏的原则, 今儿选了这个插件给大家分析分析, 各位看官且听我再唠叨唠叨. 6 | 7 | 8 | 9 | 10 | 11 | ## 试验环境 12 | 13 | 14 | 15 | | host | url | port | method | mark | 16 | | ------- | ------------ | ----- | -------- | ---- | 17 | | xxx.xxx | /api/v1/test | 12000 | get/post | foo | 18 | | yyy.yyy | /api/v1/test | 12000 | get/post | bar | 19 | 20 | 21 | 22 | 接口可以附带参数p1,p2, 返回时会打印出来, 如下: 23 | 24 | ``` 25 | curl "http://xxx.xxx:12000/api/v1/test?p1=1&p2=2" 26 | 27 | { 28 | "res": { 29 | "host": "foo", 30 | "p1": "1", 31 | "p2": "2" 32 | }, 33 | "message": "success", 34 | "code": 0 35 | } 36 | ``` 37 | 38 | 39 | 40 | 其中foo标记代表是`xxx.xxx`处理的,` yyy.yyy`用bar标记 41 | 42 | 43 | 44 | ## 运行分析 45 | 46 | 47 | 48 | 以下面的规则来说 49 | 50 | ![](./img/orange/divide1.png) 51 | 52 | 53 | 54 | 如果配置中有脑残的地方, 请忽略, 仅仅只是为了演示. 55 | 56 | 57 | 58 | 那就开始分析吧, 让我按下此命令时 59 | 60 | ``` 61 | curl "http://localhost:18888/api/v1/test?p1=1&p2=2" 62 | ``` 63 | 64 | 65 | 66 | [说的迟那时快](./understanding-orange.md), divide插件唰的进入了access流程 67 | 68 | `plugins/divide/handler.lua` 69 | 70 | ``` 71 | function DivideHandler:access(conf) 72 | local selector = selectors[sid] 73 | if selector and selector.enable == true then 74 | local selector_pass 75 | if selector.type == 0 then -- 全流量选择器 76 | selector_pass = true 77 | else 78 | selector_pass = judge_util.judge_selector(selector, "divide")-- selector judge 79 | end 80 | selector_pass = judge_util.judge_selector(selector, "divide") 81 | 82 | if selector_pass then 83 | local stop = filter_rules(sid, "divide", ngx_var, ngx_var_uri, ngx_var_host) 84 | if stop then -- 不再执行此插件其他逻辑 85 | return 86 | en 87 | end 88 | 89 | if selector.handle and selector.handle.continue == true then 90 | -- continue next selector 91 | else 92 | break 93 | end 94 | end 95 | end 96 | ``` 97 | 98 | 99 | 100 | 依然是熟悉的流程. 首先进行`judge_selector`, 因为我们这次的主题不在筛选selector, 所以我们一律全流量了. 101 | 102 | 103 | 104 | 直接进入了filter_rules 105 | 106 | `plugins/divide/handler.lua` 107 | 108 | ``` 109 | local function filter_rules(sid, plugin, ngx_var, ngx_var_uri, ngx_var_host) 110 | -- judge阶段 111 | local pass = judge_util.judge_rule(rule, plugin) 112 | 113 | -- extract阶段 114 | local variables = extractor_util.extract_variables(rule.extractor) 115 | 116 | -- handle阶段 117 | if pass then 118 | local extractor_type = rule.extractor.type 119 | if rule.upstream_url then 120 | if not rule.upstream_host or rule.upstream_host=="" then -- host默认取请求的host 121 | ngx_var.upstream_host = ngx_var_host 122 | else 123 | ngx_var.upstream_host = handle_util.build_upstream_host(extractor_type, rule.upstream_host, variables, plugin) 124 | end 125 | ngx_var.upstream_url = handle_util.build_upstream_url(extractor_type, rule.upstream_url, variables, plugin) 126 | end 127 | return true 128 | end 129 | return false 130 | end 131 | ``` 132 | 133 | 134 | 135 | 代码分为了3个阶段, 让我们一一认识下 136 | 137 | - judge 138 | - extract 139 | - handle 140 | 141 | 142 | 143 | **judge阶段** 144 | 145 | url显然正确 146 | 147 | 148 | 149 | 150 | **extract阶段** 151 | 152 | 这个东西本质上就是从用户指定的地方读取数据, 然后把数据保存起来, 备用 153 | 154 | ``` 155 | function _M.extract_variables(extractor) 156 | _M.extract(extractor_type, extractions) 157 | end 158 | ``` 159 | 160 | 161 | 162 | 我们的数据大致如下: 163 | 164 | ``` 165 | ["extractor"] = { 166 | ["extractions"] = { 167 | [1] = { 168 | ["name"] = "/api/v1/(.*)", 169 | ["type"] = "URI", 170 | }, 171 | [2] = { 172 | ["name"] = "p1", 173 | ["default"] = "99", 174 | ["type"] = "Query", 175 | }, 176 | }, 177 | ["type"] = 1, 178 | } 179 | ``` 180 | 181 | 182 | 183 | type解释如下: 184 | 185 | - 索引式提取为1 186 | - 模板式提取为2 187 | 188 | 189 | 190 | **模板式提取** 191 | 192 | ``` 193 | function _M.extract(extractor_type, extractions) 194 | if not extractor_type then 195 | extractor_type = 1 196 | end 197 | 198 | local result = {} 199 | if extractor_type == 1 200 | for i, extraction in ipairs(extractions) do 201 | local variable = extract_variable(extraction) or extraction.default or "" 202 | table_insert(result, variable) 203 | end 204 | elseif extractor_type == 2 then -- tempalte variables extractor 205 | result = extract_variable_for_template(extractions) 206 | end 207 | return result 208 | end 209 | ``` 210 | 211 | 如果没有标注怎么提取变量, 那默认测试就是索引提取, 但实际上一切设置都在用Dashboard来操作, 所以基本上不会出现extractor_type不存在的地方. 212 | 213 | 214 | 215 | 接着我们就到了下面的代码: 216 | 217 | ``` 218 | local function extract_variable_for_template(extractions) 219 | local result = {} 220 | local ngx_var = ngx.var 221 | for i, extraction in ipairs(extractions) do 222 | local etype = extraction.type 223 | if etype == "URI" then -- URI模式通过正则可以提取出N个值 224 | result["uri"] = {} -- fixbug: nil `uri` variable for tempalte parse 225 | local uri = ngx_var.uri 226 | local m, err = ngx_re_match(uri, extraction.name) 227 | if not err and m and m[1] then 228 | if not result["uri"] then result["uri"] = {} end 229 | for j, v in ipairs(m) do 230 | if j >= 1 then 231 | result["uri"]["v" .. j] = v 232 | end 233 | end 234 | end 235 | end 236 | return result 237 | end 238 | ``` 239 | 240 | 241 | 242 | 这里先分析URI的提取, 逻辑也很简单, 因为extraction.name存放的是正则表达式, 通过match匹配, 把值给匹配出来, 然后依次塞到result["uri"]里, 请注意索引是v1,v2,v3,v4... 243 | 244 | > 作者: 注意, 若从URI中提取, 仍然要根据顺序来使用, 如{{uri.v1}}、{{uri.v2}}、{{uri.v3}}. 245 | 246 | 247 | 248 | 上面的代码其实有很多的else, 我们再来看一个Query的提取 249 | 250 | ``` 251 | if etype == "Query" then 252 | local query = ngx.req.get_uri_args() 253 | if not result["query"] then result["query"] = {} end 254 | result["query"][extraction.name] = query[extraction.name] or extraction.default 255 | end 256 | ``` 257 | 258 | 259 | 260 | 代码很简单, 就不分析了, 分析出结果大概如下: 261 | 262 | ``` 263 | variables:{ 264 | ["uri"] = { 265 | ["v1"] = "test", 266 | }, 267 | ["query"] = { 268 | ["p1"] = "1", 269 | }, 270 | } 271 | ``` 272 | 273 | 274 | 275 | 顺便我们把索引也看了. 276 | 277 | 278 | 279 | **索引式提取** 280 | 281 | ``` 282 | for i, extraction in ipairs(extractions) do 283 | local variable = extract_variable(extraction) or extraction.default or "" 284 | table_insert(result, variable) 285 | end 286 | 287 | local function extract_variable(extraction) 288 | local etype = extraction.type 289 | local result = "" 290 | 291 | if etype == "URI" then -- 为简化逻辑,URI模式每次只允许提取一个变量 292 | local uri = ngx.var.uri 293 | local m, err = ngx_re_match(uri, extraction.name) 294 | if not err and m and m[1] then 295 | result = m[1] -- 提取第一个匹配的子模式 296 | end 297 | elseif etype == "Query" then 298 | local query = ngx.req.get_uri_args() 299 | result = query[extraction.name] 300 | end 301 | end 302 | ``` 303 | 304 | 305 | 306 | 逻辑跟模板提取几乎没区别, 注意在索引提取时, 所有值是存到 array 里的, 所以只能索引去获取. 307 | 308 | > 作者: 索引式提取之后就可以通过`${1}、${2}、${3}`来使用 309 | 310 | 311 | 312 | 提出取出来的variables大概如下: 313 | 314 | ``` 315 | variables:{ 316 | [1] = "test", 317 | [2] = "1", 318 | } 319 | ``` 320 | 321 | 322 | 323 | 324 | 325 | 现在变量有了, 准备工作也差不多了, 开始看看handler阶段吧. 326 | 327 | 328 | 329 | **handle阶段** 330 | 331 | 把handle代码总结下, 大概这么个两句: 332 | 333 | ``` 334 | ngx_var.upstream_host = handle_util.build_upstream_host(extractor_type, rule.upstream_host, variables, plugin) 335 | 336 | ngx_var.upstream_url = handle_util.build_upstream_url(extractor_type, rule.upstream_url, variables, plugin) 337 | ``` 338 | 339 | 340 | 341 | 先看组成upstream_host 342 | 343 | ``` 344 | function _M.build_upstream_host(extractor_type, upstream_host_tmpl, variables) 345 | return compose(extractor_type, upstream_host_tmpl, variables) 346 | end 347 | ``` 348 | 349 | 350 | 351 | 委托更底层函数compose来实现 352 | 353 | ``` 354 | local function compose(extractor_type, tmpl, variables) 355 | if not extractor_type or extractor_type == 1 then 356 | local result = string_gsub(tmpl, "%${([1-9]+)}", function(m) 357 | local t = type(variables[tonumber(m)]) 358 | if t ~= "string" and t ~= "number" then 359 | return "${" .. m .. "}" 360 | end 361 | return variables[tonumber(m)] 362 | end) 363 | return result 364 | elseif extractor_type == 2 then 365 | return template.render(tmpl, variables, ngx_md5(tmpl), true) 366 | end 367 | end 368 | ``` 369 | 370 | 371 | 372 | 模板式使用[lua-resty-template](https://github.com/bungle/lua-resty-template) 格式化host/url 为我们生成最后的host/url, 具体template的代码我就没往里看了. 373 | 374 | 索引式更简单, 索引是通过字符串替换的办法将${Number}部分给替换掉. 375 | 376 | 377 | 378 | 就这样最后一个阶段也结束了. 379 | 380 | 381 | 382 | 最后看一下, 我们到底做了个啥: 383 | 384 | 我们把/api/v1/test 中的test当做变量提取出来, 用来拼接我们的upstream, 当然这里的test_upstream提前要在nginx中注册, 以后我们如果有别的业务, 例如foobar, 我们也可以利用这个特性,切换到foobar_upstream里面. 385 | 386 | 387 | 388 | - upstream_host = your_host1 389 | - upstream_url = http://test_upstream 390 | 391 | 392 | 393 | **别慌关, 还有** 394 | 395 | 396 | upstream不像kong那样, 可以动态配置, orange需要在nginx启动前就写好. 397 | 398 | 399 | 400 | 测试: 401 | 402 | ``` 403 | $ curl "http://localhost:18888/api/v1/test?p1=1&p2=2" 404 | {"res":{"host":"bar","p1":"1","p2":"2"},"message":"success","code":0} 405 | 406 | 407 | $ curl -X "POST" "http://localhost:18888/api/v1/test" \ 408 | > -H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \ 409 | > --data-urlencode "p2=2" \ 410 | > --data-urlencode "p1=1" 411 | {"res":{"host":"foo"},"message":"success","code":0} 412 | 413 | 414 | $ curl -X "POST" "http://localhost:18888/api/v1/test" \ 415 | > -H "Content-Type: application/json" \ 416 | > -d $'{ 417 | > "p2": "2", 418 | > "p1": "1" 419 | > }' 420 | {"res":{"host":"bar","p1":"1","p2":"2"},"message":"success","code":0} 421 | 422 | 423 | ``` 424 | 425 | 426 | 427 | 428 | 429 | -------------------------------------------------------------------------------- /orange-plugin.md: -------------------------------------------------------------------------------- 1 | # 深入理解Orange之插件全家桶 2 | 3 | 4 | 5 | [TOC] 6 | 7 | 8 | 9 | 相信各位看官都看过我之前写的文章, 其实orange插件大部分代码都类似, 我们有了基础后, 就可以只读一些关键代码了, 下面我们就只讲解最核心的代码. 10 | 11 | 12 | 13 | ## rewrite 14 | 15 | 16 | 17 | ![](./img/orange/rewrite.png) 18 | 19 | 20 | 21 | 我们配置的rule如上图, 意图是将v2的请求重写到v1去, 然后脑补orange结构如下(其实是我看db的): 22 | 23 | ``` 24 | rewrite.selector.4157d6e2-18bf-428e-9a47-84079874c98b.rules={ 25 | [1] = { 26 | ["extractor"] = { 27 | ["extractions"] = { 28 | [1] = { 29 | ["name"] = "/api/v2/(.*)", 30 | ["type"] = "URI", 31 | }, 32 | }, 33 | ["type"] = 1, 34 | }, 35 | ["enable"] = true, 36 | ["id"] = "4b829f20-480b-4acf-9d28-f4d13e81da16", 37 | ["judge"] = { 38 | ["conditions"] = { 39 | [1] = { 40 | ["value"] = "/api/v2", 41 | ["operator"] = "match", 42 | ["type"] = "URI", 43 | }, 44 | }, 45 | ["type"] = 0, 46 | }, 47 | ["name"] = "rewrite_rule", 48 | ["handle"] = { 49 | ["uri_tmpl"] = "/api/v1/${1}", 50 | ["log"] = true, 51 | }, 52 | ["time"] = "2017-06-15 15:33:56", 53 | }, 54 | } 55 | ``` 56 | 57 | 58 | 59 | `plugins/rewrite/handler.lua` 60 | 61 | ``` 62 | if handle and handle.uri_tmpl then 63 | local to_rewrite = handle_util.build_uri(rule.extractor.type, handle.uri_tmpl, variables) 64 | if to_rewrite and to_rewrite ~= ngx_var_uri then 65 | local from, to, err = ngx_re_find(to_rewrite, "[%?]{1}", "jo") 66 | if not err and from and from >= 1 then 67 | local qs = string_sub(to_rewrite, from+1) 68 | if qs then 69 | local args = ngx_decode_args(qs, 0) 70 | if args then 71 | ngx_set_uri_args(args) 72 | end 73 | end 74 | end 75 | ngx_set_uri(to_rewrite, true) 76 | end 77 | end 78 | ``` 79 | 80 | 81 | 82 | 想象我们进行如下的请求: 83 | 84 | ``` 85 | curl "http://localhost:18888/api/v2/test?p1=1&p2=2" 86 | ``` 87 | 88 | 89 | 90 | 分析: 91 | 92 | 第二行代码是生成重写后的地址, 也就是对应本例子中的`/api/v1/test`, 具体生成算法说白了还是`compose`算法, 以variables中的值去替换, variables肯定就是"test"啦. 93 | 94 | 我们现在已经有了需要重写的地址, 继而就要判断是不是这个地址是不是和之前的一样, 如果一样那还重写个毛, 但如果不是openresty, 仅仅只是nginx的话, nginx好像会重试10次, 最后抛个500, 具体记不清了, 反正nginx是不会让同样的地址进入死循环的. 95 | 96 | 接着是看重写后的地址, 有没有query parameter, 如果有就把参数提取出来并`ngx.req.set_uri_args`, 我们上述例子是木有的, 你可以自己添加rule试试. 97 | 98 | 最后一步, 进行重写动作`ngx.req.set_uri` 99 | 100 | 101 | 102 | 103 | 104 | ## redirect 105 | 106 | 107 | 108 | ![](./img/orange/redirect.png) 109 | 110 | 我们配置的rule如上图, 意图是将v2的重定向到v1去 111 | 112 | ``` 113 | redirect.selector.b5461974-163a-4b5b-9305-8d8f07c4ba43.rules={ 114 | [1] = { 115 | ["extractor"] = { 116 | ["extractions"] = { 117 | [1] = { 118 | ["name"] = "/api/v2/(.*)", 119 | ["type"] = "URI", 120 | }, 121 | }, 122 | ["type"] = 1, 123 | }, 124 | ["enable"] = true, 125 | ["id"] = "e3a09f97-884d-4063-b137-1683de15e89c", 126 | ["judge"] = { 127 | ["conditions"] = { 128 | [1] = { 129 | ["value"] = "/api/v2", 130 | ["operator"] = "match", 131 | ["type"] = "URI", 132 | }, 133 | }, 134 | ["type"] = 0, 135 | }, 136 | ["name"] = "redirect_rule", 137 | ["handle"] = { 138 | ["redirect_status"] = "301", 139 | ["url_tmpl"] = "/api/v1/${1}", 140 | ["log"] = true, 141 | ["trim_qs"] = false, 142 | }, 143 | ["time"] = "2017-06-16 06:26:11", 144 | }, 145 | } 146 | ``` 147 | 148 | 149 | 150 | `plugins/redirect/handler.lua` 151 | 152 | ``` 153 | local function filter_rules(sid, plugin, ngx_var_uri, ngx_var_host, ngx_var_scheme, ngx_var_args) 154 | local handle = rule.handle 155 | if handle and handle.url_tmpl then 156 | local to_redirect = handle_util.build_url(rule.extractor.type, handle.url_tmpl, variables) 157 | if to_redirect and to_redirect ~= ngx_var_uri then 158 | local redirect_status = tonumber(handle.redirect_status) 159 | if redirect_status ~= 301 and redirect_status ~= 302 then 160 | redirect_status = 301 161 | end 162 | 163 | if string_find(to_redirect, 'http') ~= 1 then 164 | to_redirect = ngx_var_scheme .. "://" .. ngx_var_host .. to_redirect 165 | end 166 | 167 | if ngx_var_args ~= nil then 168 | if string_find(to_redirect, '?') then -- 不存在?,直接缀上url args 169 | if handle.trim_qs ~= true then 170 | to_redirect = to_redirect .. "&" .. ngx_var_args 171 | end 172 | else 173 | if handle.trim_qs ~= true then 174 | to_redirect = to_redirect .. "?" .. ngx_var_args 175 | end 176 | end 177 | end 178 | ngx_redirect(to_redirect, redirect_status) 179 | end 180 | end 181 | end 182 | ``` 183 | 184 | 185 | 186 | 想象我们进行如下的请求: 187 | 188 | ``` 189 | curl "http://localhost:18888/api/v2/test?p1=1&p2=2" 190 | ``` 191 | 192 | 193 | 194 | 分析: 195 | 196 | 首先拼接字符串, 根据规则生成了/api/v1/test, 然后还是老规矩, 既然跳转就不能和之前重定向的url一致, 顺便检查了用户有没有设置跳转的http status, 如果没有或者不合法, 就默认设定为301, 接着对重定向地址继续检查, 如果发现这不是一个完整url,将其补全, 比如`/api/v1/test` 变成 `http://localhost:18888/api/v1/test`, 最后再对参数进行检查, 根据trim_qs标志, 进行query string的附带 197 | 198 | 最后, 产生一个301/302的重定向.就完事了. 199 | 200 | 201 | 202 | 203 | 204 | ## waf 205 | 206 | 207 | 208 | 防火墙的配置如下: 209 | 210 | ``` 211 | waf.selector.685e689c-c537-425b-af88-7c3310f55e1d.rules={ 212 | [1] = { 213 | ["time"] = "2017-06-16 09:38:43", 214 | ["enable"] = true, 215 | ["id"] = "c90de39f-8782-4bb5-8f09-57892fea31b3", 216 | ["judge"] = { 217 | ["conditions"] = { 218 | [1] = { 219 | ["value"] = "/api/v1/test", 220 | ["operator"] = "match", 221 | ["type"] = "URI", 222 | }, 223 | }, 224 | ["type"] = 0, 225 | }, 226 | ["name"] = "waf_rule", 227 | ["handle"] = { 228 | ["log"] = true, 229 | ["stat"] = true, 230 | ["perform"] = "deny", 231 | ["code"] = 403, 232 | }, 233 | }, 234 | } 235 | ``` 236 | 237 | 238 | 239 | `plugins/waf/handler.lua` 240 | 241 | ``` 242 | -- judge阶段 243 | local pass = judge_util.judge_rule(rule, plugin) 244 | 245 | -- extract阶段 246 | local variables = extractor_util.extract_variables(rule.extractor) 247 | 248 | -- handle阶段 249 | if pass then 250 | local handle = rule.handle 251 | if handle.stat == true then 252 | local key = rule.id -- rule.name .. ":" .. rule.id 253 | stat.count(key, 1) 254 | end 255 | 256 | if handle.perform == 'allow' then 257 | if handle.log == true then 258 | ngx.log(ngx.INFO, "[WAF-Pass-Rule] ", rule.name, " uri:", ngx_var_uri) 259 | end 260 | else 261 | if handle.log == true then 262 | ngx.log(ngx.INFO, "[WAF-Forbidden-Rule] ", rule.name, " uri:", ngx_var_uri) 263 | end 264 | ngx.exit(tonumber(handle.code or 403)) 265 | return true 266 | end 267 | end 268 | ``` 269 | 270 | 271 | 272 | 条件是否满足就是`pass`变量, 这个条件是通过还是拒绝是根据`handle.perform`变量来指定 273 | 274 | 这里还有个计数器是根据`handle.stat`来指定是否计数. 275 | 276 | 没了. 277 | 278 | 279 | 280 | 281 | 282 | ## rate limiting 283 | 284 | 285 | 286 | ![](./img/orange/rate.png) 287 | 288 | 289 | 290 | ``` 291 | { 292 | "name": "rate_limit", 293 | "judge": { 294 | "type": 0, 295 | "conditions": [ 296 | { 297 | "type": "URI", 298 | "operator": "match", 299 | "value": "/api/v1/test" 300 | } 301 | ] 302 | }, 303 | "handle": { 304 | "period": 60, 305 | "count": 5, 306 | "log": true 307 | }, 308 | "enable": true 309 | } 310 | ``` 311 | 312 | 313 | 314 | `plugins/rate_limiting/handler` 315 | 316 | ``` 317 | if rule.enable == true then 318 | -- judge阶段 319 | local pass = judge_util.judge_rule(rule, plugin) 320 | 321 | -- handle阶段 322 | local handle = rule.handle 323 | if pass then 324 | local limit_type = get_limit_type(handle.period) 325 | if limit_type then 326 | local current_timetable = utils.current_timetable() 327 | local time_key = current_timetable[limit_type] 328 | local limit_key = rule.id .. "#" .. time_key 329 | local current_stat = get_current_stat(limit_key) or 0 330 | 331 | ngx.header["X-RateLimit-Limit" .. "-" .. limit_type] = handle.count 332 | if current_stat >= handle.count then 333 | ngx.header["X-RateLimit-Remaining" .. "-" .. limit_type] = 0 334 | ngx.exit(429) 335 | return true 336 | else 337 | ngx.header["X-RateLimit-Remaining" .. "-" .. limit_type] = handle.count - current_stat - 1 338 | incr_stat(limit_key, limit_type) 339 | end 340 | end 341 | end -- end `pass` 342 | end -- end `enable` 343 | ``` 344 | 345 | 346 | 347 | 本例中limit_type等于"Minute", current_timetable大概如下: 348 | 349 | ``` 350 | current_timetable:{ 351 | ["Hour"] = "2017-6-17 7", 352 | ["Second"] = "2017-6-17 7:50:56", 353 | ["Minute"] = "2017-6-17 7:50", 354 | ["Day"] = "2017-6-17", 355 | } 356 | ``` 357 | 358 | time_key 对应着当前的关心的时间, limit_key唯一标示计数器, 身下的逻辑就很简单了, 计数器如果超过了用户设置的最大值, 就exit(429), 咦, 这地方作者为毛没有让用户自定义设置, 如果计数器还没到, 使用incr_stat累加 359 | 360 | 361 | 362 | > 注意: `get_current_stat`, `incr_stat` 计数器相关都是使用lua_shared_dict的rate_limit 363 | 364 | 365 | 366 | 贴一个我被限速了的header 367 | 368 | ``` 369 | HTTP/1.1 429 370 | Server: openresty/1.11.2.3 371 | Date: Sat, 17 Jun 2017 08:17:33 GMT 372 | Content-Length: 0 373 | Connection: close 374 | X-RateLimit-Limit-Minute: 5 375 | X-RateLimit-Remaining-Minute: 0 376 | ``` 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | ## property rate limiting 385 | 386 | ``` 387 | { 388 | "name": "property_rule", 389 | "judge": {}, 390 | "extractor": { 391 | "type": 1, 392 | "extractions": [ 393 | { 394 | "type": "Query", 395 | "name": "p1" 396 | }, 397 | { 398 | "type": "Query", 399 | "name": "p2" 400 | }, 401 | { 402 | "type": "URI", 403 | "name": "/api/v1/(.*)" 404 | } 405 | ] 406 | }, 407 | "handle": { 408 | "period": 60, 409 | "count": 5, 410 | "log": true 411 | }, 412 | "enable": true 413 | } 414 | ``` 415 | 416 | 417 | 418 | 这里插件的pass判断是: 419 | 420 | ``` 421 | local real_value = table_concat( extractor_util.extract_variables(rule.extractor),"#") 422 | local pass = (real_value ~= ''); 423 | ``` 424 | 425 | 例子中`real_value等于1#2#test`, 只要这个字符串不为空那么就可以进行limit运算. 426 | 427 | 428 | 429 | *我觉得这里功能有点偏弱了, 比如我只想让userid等于123的时候, 进行限速, 不为空的判断我觉得可以改善一下, 可以和rate_limiting插件合并为一个插件* 430 | 431 | 432 | 433 | 434 | 435 | ## monitor 436 | 437 | ``` 438 | { 439 | "name": "monitor_rule", 440 | "judge": { 441 | "type": 0, 442 | "conditions": [ 443 | { 444 | "type": "URI", 445 | "operator": "match", 446 | "value": "/api/v1/test" 447 | } 448 | ] 449 | }, 450 | "handle": { 451 | "continue": true, 452 | "log": false 453 | }, 454 | "enable": true 455 | } 456 | ``` 457 | 458 | 459 | 460 | 核心代码如下: 461 | 462 | ``` 463 | local key_suffix = rule.id 464 | stat.count(key_suffix) 465 | ``` 466 | 467 | 468 | 469 | ``` 470 | function _M.count(key_suffix) 471 | local ngx_var = ngx.var 472 | safe_count(TOTAL_COUNT .. key_suffix, 1) 473 | 474 | local http_status = tonumber(ngx_var.status) 475 | if http_status >= 200 and http_status < 300 then 476 | safe_count(REQUEST_2XX .. key_suffix, 1) 477 | elseif http_status >= 300 and http_status < 400 then 478 | safe_count(REQUEST_3XX .. key_suffix, 1) 479 | elseif http_status >= 400 and http_status < 500 then 480 | safe_count(REQUEST_4XX .. key_suffix, 1) 481 | elseif http_status >= 500 and http_status < 600 then 482 | safe_count(REQUEST_5XX .. key_suffix, 1) 483 | end 484 | 485 | safe_count(TRAFFIC_READ .. key_suffix, tonumber(ngx_var.request_length)) 486 | safe_count(TRAFFIC_WRITE .. key_suffix, tonumber(ngx_var.bytes_sent)) 487 | safe_count(TOTAL_REQUEST_TIME .. key_suffix, ngx.now() - ngx.req.start_time()) 488 | end 489 | ``` 490 | 491 | 492 | 493 | 上述对各种维度进行了统计, 最后在以api的形式交给web显示. 494 | 495 | 496 | 497 | -------------------------------------------------------------------------------- /understanding-orange.md: -------------------------------------------------------------------------------- 1 | # 深入理解 Orange 2 | 3 | [TOC] 4 | 5 | 6 | 7 | ## 前言 8 | 9 | 本文在分析`orange`是时候满足了以下**约束**, 请观众老爷们知晓. 10 | 11 | * 为了格式美观, 在讲解代码的时候会酌情删减. 12 | * 本文涉及到的[orange](https://github.com/sumory/orange)代码版本为v0.6.3 13 | * [orange](https://github.com/sumory/orange)的**Api Server**部分, 以及**Dashboard**部分不涉及分析. 14 | * 分析代码是以**basic\_auth**插件为例子来进行讲解 15 | * [orange](https://github.com/sumory/orange)安装环境是mac, 网关端口是18888, 而不是默认的8888 16 | 17 | 18 | 19 | 20 | 21 | 22 | ## 安装 23 | 24 | 安装过程比较简单, 请参考[orange install]() 25 | 26 | or 27 | 28 | 你也可以使用docker 安装, 下面简单说明下安装过程 29 | 30 | 31 | 32 | **1. 首先创建mysql数据库** 33 | 34 | 你也可以选择你喜欢的mysql tag 35 | 36 | ``` 37 | docker run --name orange-database -e MYSQL_ROOT_PASSWORD=ur_pwd -p 3306:3306 -d mysql:5.7 38 | ``` 39 | 40 | 41 | 42 | **2. 连接mysql** 43 | 44 | 连接mysql后创建根据自己需要创建数据库, 用户, 并导入[orange table](https://raw.githubusercontent.com/sumory/orange/master/install/orange-v0.6.0.sql). 45 | 46 | 47 | 48 | 49 | **3. 创建orange** 50 | 51 | ``` 52 | docker run -d --name orange \ 53 | --link orange-database:orange-database \ 54 | -p 7777:7777 \ 55 | -p 8888:8888 \ 56 | -p 9999:9999 \ 57 | --security-opt seccomp:unconfined \ 58 | -e ORANGE_DATABASE=ur_db \ 59 | -e ORANGE_HOST=orange-database \ 60 | -e ORANGE_PORT=3306 \ 61 | -e ORANGE_USER=ur_user \ 62 | -e ORANGE_PWD=ur_pwd \ 63 | syhily/orange 64 | ``` 65 | 66 | 67 | 68 | 69 | 70 | 71 | ## 一. 需要了解的概念 72 | 73 | 74 | 75 | ### 1.1 orange缓存 76 | 77 | 78 | 79 | `nginx.conf` 80 | 81 | ``` 82 | lua_shared_dict orange_data 20m; # should not removed. used for orange data, e.g. plugins configurations.. 83 | 84 | lua_shared_dict status 1m; # used for global statistic, see plugin: stat 85 | lua_shared_dict waf_status 1m; # used for waf statistic, see plugin: waf 86 | lua_shared_dict monitor 10m; # used for url monitor statistic, see plugin: monitor 87 | lua_shared_dict rate_limit 10m; # used for rate limiting count, see plugin: rate_limiting 88 | lua_shared_dict property_rate_limiting 10m; # used for rate limiting count, see plugin: rate_limiting 89 | ``` 90 | 91 | 92 | 93 | 这玩意主要为了`orange`在运行期间的效率, 内存肯定比io快, 毕竟网关不能有耗时操作. 94 | 95 | 以`orange_db`为例子, 在`orange`运行期间会从mysql数据库读取相关配置, 并保存在orange cache中, 详细可见`store/orange_db.lua`文件 96 | 97 | 98 | 99 | 100 | 101 | ### 1.2 store 102 | 103 | 打开store文件夹, 会发现如下文件 104 | 105 | ``` 106 | . 107 | ├── base.lua 108 | ├── dao.lua 109 | ├── mysql_db.lua 110 | ├── mysql_store.lua 111 | └── orange_db.lua 112 | ``` 113 | 114 | 115 | 116 | 光看dao,mysql,db的字样也能大概了解是负责数据读取等的工作 117 | 118 | 以`mysql_store`为例, MySQLStore只是一个封装, `mysql_db`才是真正的执行mysql语句的类 119 | 120 | 121 | 122 | 123 | 124 | ### 1.3 Object 125 | 126 | 据作者说, 这一块包括插件主要参考的是[kong](https://getkong.org/), 参考谁我也不管了, 反正学啥都是学. 127 | 128 | 129 | 130 | 这是`orange`最底层的类, 实现也没什么, 就一普通父类, 加了`super`等字段属性, 另外还实现了`tostring`元方法, `call`元方法: 131 | 132 | ``` 133 | local Object = {} 134 | Object.__index = Object 135 | 136 | function Object:extend() 137 | local cls = {} 138 | for k, v in pairs(self) do 139 | if k:find("__") == 1 then 140 | cls[k] = v 141 | end 142 | end 143 | cls.__index = cls 144 | cls.super = self 145 | setmetatable(cls, self) 146 | return cls 147 | end 148 | 149 | ... 150 | 151 | function Object:__call(...) 152 | local obj = setmetatable({}, self) 153 | obj:new(...) 154 | return obj 155 | end 156 | ``` 157 | 158 | 159 | 160 | 提醒一下, 如果实现了`call`的元方法, 那么这个类可以以仿函数的形式调用, 而仿函数中又委托new函数来实现 161 | 162 | 163 | 164 | 165 | 166 | ### 1.4 meta & selector & rule 167 | 168 | 169 | 170 | ![](./img/orange/db.png) 171 | 172 | 173 | 174 | orange之前的commit我没有阅读, 但根据作者的介绍: 175 | 176 | 177 | 178 | > Orange v0.6.0版本是一个重构版本, 着重为了解决之前版本在有大量规则配置时性能损耗的问题。 基本的设计思路是将原来的规则细分成两层, 第一层叫做[selector](http://orange.sumory.com/docs/concept/selector/), 用于将流量进行第一步划分, 在进入某个selector后才按照之前的设计进行[规则](http://orange.sumory.com/docs/concept/rule/)匹配, 匹配到后进行相关处理。 179 | 180 | 181 | 182 | **meta** 183 | 184 | 是辅助记录, 主要存放着selector的id 185 | 186 | 187 | 188 | **selector** 189 | 190 | 选择器, 设计为流量筛选的第一道大门 191 | 192 | 193 | 194 | **rule** 195 | 196 | 规则, 每个选择器有很多规则, orange根据规则进行第二次流量筛选 197 | 198 | 199 | 200 | 201 | 202 | ## 二. 启动过程 203 | 204 | 205 | 206 | ### 2.1 init 207 | 208 | `nginx.conf` 209 | 210 | ``` 211 | init_by_lua_block { 212 | local orange = require("orange.orange") 213 | local env_orange_conf = os.getenv("ORANGE_CONF") 214 | local config_file = env_orange_conf or ngx.config.prefix().. "/conf/orange.conf" 215 | local config, store = orange.init({ 216 | config = config_file 217 | }) 218 | 219 | context = { 220 | orange = orange, 221 | store = store, 222 | config = config 223 | } 224 | } 225 | ``` 226 | 227 | 228 | 229 | 上述代码就是读取orange.conf的配置, 并且进行init的过程, 返回的orange, store, 作为orange的上下文, 供其他场景使用, 值得一提的是ORANGE_CONF环境变量一直为空, 所以配置档的路径是通过`ngx.config.prefix()`拿到的, 原因环境变量在master读取不到, 在worker中可以. 230 | 231 | 232 | 233 | 不废话继续分析 234 | 235 | `orange.lua` 236 | 237 | ``` 238 | function Orange.init(options) 239 | options = options or {} 240 | local store, config 241 | local conf_file_path = options.config 242 | config = config_loader.load(conf_file_path) 243 | store = require("orange.store.mysql_store")(config.store_mysql) 244 | loaded_plugins = load_node_plugins(config, store) 245 | ngx.update_time() 246 | config.orange_start_at = ngx.now() 247 | return config, store 248 | end 249 | ``` 250 | 251 | 252 | 253 | 这段代码主要就干了两件事 254 | 255 | - 实例化对象store 256 | - 加载插件 257 | 258 | 259 | 260 | store的加载是通过仿函数, 因为mysql_store间接继承了Object. 261 | 262 | 263 | 264 | 265 | 266 | **store初始化** 267 | 268 | ``` 269 | function MySQLStore:new(options) 270 | self._name = options.name or "mysql-store" 271 | MySQLStore.super.new(self, self._name) 272 | self.store_type = "mysql" 273 | local connect_config = options.connect_config 274 | self.mysql_addr = connect_config.host .. ":" .. connect_config.port 275 | self.data = {} 276 | self.db = mysql_db:new(options) 277 | end 278 | ``` 279 | 280 | 281 | 282 | 前面没什么, 都是一堆赋值, 注意db对象是mysql_db类 283 | 284 | 285 | 286 | `store/mysql_db` 287 | 288 | ``` 289 | function DB:new(conf) 290 | local instance = {} 291 | instance.conf = conf 292 | setmetatable(instance, { __index = self}) 293 | return instance 294 | end 295 | ``` 296 | 297 | 298 | 299 | 这里并没有直接连接数据库, 而且在exec期间连接, 原因很简单, 防止重新连接..要防止就得加上连接检查, 说不定还会弄出来连接池啥的...咱要的就是不麻烦. 300 | 301 | 302 | 303 | *其实这一段代码, 隐隐约约能感觉出作者可能当初是想写多数据库支持的,通过定义store的type能看出, 后来肯定因为一些事情没有写了, 如果是这样就我个人感觉应该还少一个更上层的Store类, 或者干脆就MysqlStore改个名字也可以, 不同的是实例化db对象的时候, 通过orange.conf字段去加载不同数据库的xxx_db, 再加上store定义好的通用interface, 这事没毛病.* 304 | 305 | 306 | 307 | 不扯远了, 让我们继续, storeok后, 我们开始加载插件 308 | 309 | 310 | 311 | **加载插件** 312 | 313 | 314 | 315 | ``` 316 | local function load_node_plugins(config, store) 317 | 318 | local sorted_plugins = {} 319 | local loaded, plugin_handler = utils.load_module_if_exists("orange.plugins." .. v .. ".handler") 320 | 321 | table.insert(sorted_plugins, { 322 | name = v, 323 | handler = plugin_handler(store), 324 | }) 325 | 326 | table.sort(sorted_plugins, function(a, b) 327 | local priority_a = a.handler.PRIORITY or 0 328 | local priority_b = b.handler.PRIORITY or 0 329 | return priority_a > priority_b 330 | end) 331 | 332 | return sorted_plugins 333 | end 334 | ``` 335 | 336 | 337 | 338 | 上述代码其实是循环加载插件, 我把循环的部分给注释了, v就是插件的名字, 加载完成根据插件的priority的进行排序, 如果你的插件是有依赖关系的, 请把priority给填上. 339 | 340 | 341 | 342 | orange.conf配置插件部分 343 | 344 | ``` 345 | "plugins": [ 346 | "stat", 347 | "monitor", 348 | "redirect", 349 | "rewrite", 350 | "rate_limiting", 351 | "property_rate_limiting", 352 | "basic_auth", 353 | "key_auth", 354 | "signature_auth", 355 | "waf", 356 | "divide", 357 | "kvstore" 358 | ] 359 | ``` 360 | 361 | 362 | 363 | 364 | `utils/utils.lua` 365 | 366 | ``` 367 | function _M.load_module_if_exists(module_name) 368 | local status, res = pcall(require, module_name) 369 | if status then 370 | return true, res 371 | elseif type(res) == "string" and string_find(res, "module '"..module_name.."' not found", nil, true) then 372 | return false 373 | else 374 | error(res) 375 | end 376 | end 377 | ``` 378 | 379 | 380 | 381 | 这个真没啥, pcall调用,主要是为了hold住一些错误, 如果是basic_auth, 如下 382 | 383 | ``` 384 | require("orange.plugins.basic_auth.handler") 385 | ``` 386 | 387 | 388 | 389 | 那`plugin_handler(store)`其实就是 390 | 391 | ``` 392 | function BasicAuthHandler:new(store) 393 | BasicAuthHandler.super.new(self, "basic_auth-plugin") 394 | self.store = store 395 | end 396 | ``` 397 | 398 | 将store保存起来, 也就是说具备了操作db的能力, 后面排个序完事. 399 | 400 | 401 | 402 | 看到现在init其实没做什么干货, 其实也不用去干, 正如其名初始化一些东西而已, 为后面的init_worker做准备 403 | 404 | 405 | 406 | 407 | 408 | ### 2.2 init_worker 409 | 410 | 411 | 412 | `nginx.conf` 413 | 414 | ``` 415 | init_worker_by_lua_block { 416 | local orange = context.orange 417 | orange.init_worker() 418 | } 419 | ``` 420 | 421 | 422 | 上面没啥, 直接看下方实现 423 | 424 | ``` 425 | function Orange.init_worker() 426 | if Orange.data and Orange.data.store and Orange.data.config.store == "mysql" then 427 | local ok, err = ngx.timer.at(0, function(premature, store, config) 428 | local available_plugins = config.plugins 429 | for _, v in ipairs(available_plugins) do 430 | local load_success = dao.load_data_by_mysql(store, v) 431 | if not load_success then 432 | os.exit(1) 433 | end 434 | end 435 | end, Orange.data.store, Orange.data.config) 436 | end 437 | 438 | for _, plugin in ipairs(loaded_plugins) do 439 | plugin.handler:init_worker() 440 | end 441 | end 442 | ``` 443 | 444 | 445 | 这里面的store, config就是context对象, 从第一行可以看出, orange只支持mysql 446 | 447 | 后面会使用ngx.timer.at执行. 448 | 449 | 450 | 451 | 让我把代码再精简一下, 上述代码主要在做如下2句代码 452 | 453 | ``` 454 | dao.load_data_by_mysql(store, plugin) 455 | plugin.handler:init_worker() 456 | ``` 457 | 458 | 459 | `orange/store/dao.lua` 460 | 461 | ``` 462 | function _M.load_data_by_mysql(store, plugin) 463 | if v == "stat" then 464 | return 465 | elseif v == "kvstore" then 466 | local init_enable = _M.init_enable_of_plugin(v, store) 467 | else -- ignore `stat` and `kvstore` 468 | local init_enable = _M.init_enable_of_plugin(v, store) 469 | local init_meta = _M.init_meta_of_plugin(v, store) 470 | local init_selectors_and_rules = _M.init_selectors_of_plugin(v, store) 471 | end 472 | end 473 | ``` 474 | 475 | 476 | 这里可以看到stat和kvstore插件初始化过程是较其他插件不一样的, 我们选择的basic_auth走的是else流程,也是最多的代码, 运气真不好, 早知道选stat了.. 477 | 478 | ![](./img/orange/sad.jpg) 479 | 480 | 481 | 482 | 483 | 484 | 咳咳, 划重点了, 下面可是很重要. 485 | 486 | 487 | 488 | `orange/store/dao.lua` 489 | 490 | ``` 491 | function _M.init_enable_of_plugin(plugin, store) 492 | -- 查找enable 493 | local enables, err = store:query({ 494 | sql = "select `key`, `value` from meta where `key`=?", 495 | params = {plugin .. ".enable"} 496 | }) 497 | 498 | if enables and type(enables) == "table" and #enables > 0 then 499 | orange_db.set(plugin .. ".enable", enables[1].value == "1") 500 | else 501 | orange_db.set(plugin .. ".enable", false) 502 | end 503 | 504 | return true 505 | end 506 | ``` 507 | 508 | 509 | 进行一次sql查询 510 | 511 | ``` 512 | select `key`, `value` from meta where `key`='basic_auth.enable' 513 | ``` 514 | 515 | 然后把basic_auth.enable 为key, 值为value, 保存到orange_db内存中, 保存在内存中肯定是必要的, 不能让未来的judge query 成为性能瓶颈 516 | 517 | 518 | 519 | 如果启用了: 520 | 521 | ``` 522 | basic_auth.enable=true 523 | ``` 524 | 525 | 526 | 527 | 528 | 529 | 顺便再说下store.query的过程, 之前咱们说到初始化时实际上没有进行数据库连接. 530 | 531 | `store/mysql_db.lua` 532 | 533 | ``` 534 | function DB:query(sql, params) 535 | sql = self:parse_sql(sql, params) 536 | return self:exec(sql) 537 | end 538 | 539 | function DB:exec(sql) 540 | local conf = self.conf 541 | local db, err = mysql:new() 542 | db:set_timeout(conf.timeout) 543 | local ok, err, errno, sqlstate = db:connect(conf.connect_config) 544 | db:query("SET NAMES utf8") 545 | local res, err, errno, sqlstate = db:query(sql) 546 | local ok, err = db:set_keepalive(conf.pool_config.max_idle_timeout, conf.pool_config.pool_size) 547 | return res, err, errno, sqlstate 548 | end 549 | ``` 550 | 551 | 552 | 553 | 因为mysql的增删改查不是本文的重点, 就不深入分析了, 大概每次执行 554 | 555 | - 连接 556 | - 设置超时时间 557 | - sql执行 558 | - 返回 559 | 560 | 561 | 562 | 563 | 564 | `orange/store/dao.lua` 565 | 566 | ``` 567 | function _M.init_meta_of_plugin(plugin, store) 568 | local meta, err = store:query({ 569 | sql = "select * from " .. plugin .. " where `type` = ? limit 1", 570 | params = {"meta"} 571 | }) 572 | 573 | if meta and type(meta) == "table" and #meta > 0 then 574 | local success, err, forcible = orange_db.set(plugin .. ".meta", meta[1].value or '{}') 575 | end 576 | 577 | return true 578 | end 579 | ``` 580 | 581 | 582 | 操作类似, 对数据库进行type为meta的查询, 因为meta记录的是selector的id, 所以数据大概如下: 583 | 584 | ``` 585 | basic_auth.meta={ 586 | ["selectors"] = { 587 | [1] = "6e6c7e97-8ece-45bd-9f88-d816c574ba40", 588 | }, 589 | } 590 | ``` 591 | 592 | 593 | 594 | `orange/store/dao.lua` 595 | 596 | ``` 597 | function _M.init_selectors_of_plugin(plugin, store) 598 | local selectors, err = store:query({ 599 | sql = "select * from " .. plugin .. " where `type` = ?", 600 | params = {"selector"} 601 | }) 602 | 603 | local to_update_selectors = {} 604 | if selectors and type(selectors) == "table" then 605 | for _, s in ipairs(selectors) do 606 | to_update_selectors[s.key] = json.decode(s.value or "{}") 607 | local init_rules_of_it = _M.init_rules_of_selector(plugin, store, s.key) 608 | end 609 | 610 | local success, err, forcible = orange_db.set_json(plugin .. ".selectors", to_update_selectors) 611 | else 612 | local success, err, forcible = orange_db.set_json(plugin .. ".selectors", {}) 613 | end 614 | 615 | return true 616 | end 617 | ``` 618 | 619 | 620 | 上面的代码大同小异, 实际写入的数据大概是: 621 | 622 | ``` 623 | basic_auth.selector.6e6c7e97-8ece-45bd-9f88-d816c574ba40.rules={ 624 | [1] = { 625 | ["time"] = "2017-06-07 11:24:12", 626 | ["enable"] = true, 627 | ["id"] = "0b9cf684-6202-4676-b82d-a80b93f0b282", 628 | ["judge"] = { 629 | ["conditions"] = { 630 | [1] = { 631 | ["value"] = "test_url", 632 | ["operator"] = "match", 633 | ["type"] = "URI", 634 | }, 635 | }, 636 | ["type"] = 0, 637 | }, 638 | ["name"] = "r_single_match", 639 | ["handle"] = { 640 | ["log"] = true, 641 | ["credentials"] = { 642 | [1] = { 643 | ["username"] = "test_user", 644 | ["password"] = "test_password", 645 | }, 646 | }, 647 | ["code"] = 401, 648 | }, 649 | }, 650 | } 651 | ``` 652 | 653 | 654 | 655 | **handler:init_worker** 656 | 657 | 执行完load_data_by_mysql, 接着就执行插件本身的init_worker函数, 具体逻辑得看插件的功能与特性, 就本文来说, basic_auth 插件不需要执行任何代码. 658 | 659 | 660 | 661 | ![](./img/orange/happy.jpg) 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | ## 三. 插件 670 | 671 | 672 | 673 | ### 3.1 BasePlugin 674 | 675 | 这是所有插件的基类, 所有的插件都必须继承于此, 他实现了一系列的虚函数, 留给自己的子类继承 676 | 677 | ``` 678 | local BasePlugin = Object:extend() 679 | 680 | function BasePlugin:new(name) 681 | self._name = name 682 | end 683 | 684 | function BasePlugin:init_worker() 685 | function BasePlugin:redirect() 686 | function BasePlugin:rewrite() 687 | function BasePlugin:access() 688 | function BasePlugin:header_filter() 689 | function BasePlugin:body_filter() 690 | function BasePlugin:log() 691 | 692 | 693 | local BasicAuthHandler = BasePlugin:extend() 694 | BasicAuthHandler.PRIORITY = 200 695 | ``` 696 | 697 | 698 | 699 | ### 3.2 Handler 700 | 701 | 这玩意就是插件自身的逻辑了, 具体逻辑我们后文详细说明 702 | 703 | 704 | 705 | ### 3.3 文件命名 706 | 707 | 根据初始化逻辑 708 | 709 | ``` 710 | local loaded, plugin_handler = utils.load_module_if_exists("orange.plugins." .. v .. ".handler") 711 | ``` 712 | 713 | 由上面代码看出, 插件需要满足一下约束 714 | 715 | - 插件要放在plugins文件夹下面 716 | - 插件文件夹里面一定要有handler.lua文件 717 | 718 | 719 | 720 | 721 | ### 3.4 配置登记 722 | 723 | 插件是按照在orange.conf中记录的插件名字加载的. 724 | 725 | ``` 726 | "plugins": [ 727 | "basic_auth" 728 | ... 729 | ], 730 | ``` 731 | 732 | 733 | *这里在叨叨两句, 个人感觉在插件的handler处理部分还可以继续封装, 因为selector和ruleda大多数时候都是common的, 插件与插件的不同应该体现在其他的逻辑, 所以handler部分应该不用写那么多代码, 框架层可以做更多事* 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | ## 四. 执行过程 744 | 745 | 746 | 747 | ### 4.1 单一条件匹配 748 | 749 | 750 | 751 | ![](./img/orange/s_all_continue_s_no_log.png) 752 | 753 | 754 | 755 | ![](./img/orange/r_single_match.png) 756 | 757 | 老司机们分析前得做点准备工作: 758 | 759 | 760 | 761 | 首先得有个正常的API 762 | 763 | ``` 764 | MacBook-Pro:~ tuyou$ curl http://your_host:12000/api/v1/test 765 | {"res":[],"message":"success","code":0} 766 | ``` 767 | 768 | 769 | 770 | 配置nginx的upstream 771 | 772 | ``` 773 | upstream default_upstream { 774 | server test.aituyou.me:12000; 775 | } 776 | 777 | set $upstream_host $host; 778 | set $upstream_url 'http://default_upstream'; 779 | ``` 780 | 781 | 782 | 783 | ok, 那立即试验一下, 记得把basic_auth plugin, selector, rule都开启啊 784 | 785 | ``` 786 | MacBook-Pro:~ tuyou$ curl -i http://localhost:18888/api/v1/test 787 | HTTP/1.1 401 Unauthorized 788 | Server: openresty/1.11.2.3 789 | Date: Wed, 07 Jun 2017 03:57:44 GMT 790 | Content-Type: text/html; charset=UTF-8 791 | Content-Length: 201 792 | Connection: keep-alive 793 | 794 | 795 | 401 Authorization Required 796 | 797 |

401 Authorization Required

798 |
openresty/1.11.2.3
799 | 800 | 801 | ``` 802 | 803 | 804 | 805 | 我们看到了, 报Authorization Required错, 下面让我们看看, 到底怎么出错的. 806 | 807 | 808 | 809 | 首先看ngxin配置, 基本是一个代理模式 810 | 811 | ``` 812 | ... 813 | access_by_lua_block { 814 | local orange = context.orange 815 | orange.access() 816 | } 817 | ... 818 | ``` 819 | 820 | 821 | 822 | 在不同的时期调用orange不同的函数, 由于`basic_auth.handler`只重载了access函数, 那么此插件的关键逻辑就在于此. 823 | 824 | 825 | 826 | ``` 827 | function BasicAuthHandler:access(conf) 828 | BasicAuthHandler.super.access(self) 829 | 830 | local enable = orange_db.get("basic_auth.enable") 831 | local meta = orange_db.get_json("basic_auth.meta") 832 | local selectors = orange_db.get_json("basic_auth.selectors") 833 | local ordered_selectors = meta and meta.selectors 834 | 835 | if not enable or enable ~= true or not meta or not ordered_selectors or not selectors then 836 | return 837 | end 838 | 839 | local ngx_var_uri = ngx.var.uri 840 | local headers = ngx.req.get_headers() 841 | local authorization = headers and (headers["Authorization"] or headers["authorization"]) 842 | end 843 | ``` 844 | 845 | 846 | 847 | 这一段是准备过程, 主要是把当时init_xxx_of_plugin赋值的变量, 再给提取出来 848 | 849 | 1. meta主要存放的是selectors的id 850 | 2. selectors存放的是真正的selectors数据 851 | 852 | 853 | 854 | 大概如下: 855 | 856 | ``` 857 | meta:{ 858 | ["selectors"] = { 859 | [1] = "0d7f2265-a914-4bc8-ae0f-3115c0e6c845", 860 | }, 861 | } 862 | 863 | selector:{ 864 | ["0d7f2265-a914-4bc8-ae0f-3115c0e6c845"] = { 865 | ["time"] = "2017-06-06 08:38:11", 866 | ["enable"] = true, 867 | ["rules"] = { 868 | [1] = "900740f0-9e6f-4a38-ad9d-4429d4788b8e", 869 | }, 870 | ["id"] = "0d7f2265-a914-4bc8-ae0f-3115c0e6c845", 871 | ["judge"] = { 872 | }, 873 | ["name"] = "s_all_continue_s_no_log", 874 | ["handle"] = { 875 | ["continue"] = true, 876 | ["log"] = false, 877 | }, 878 | ["type"] = 0, 879 | }, 880 | } 881 | ``` 882 | 883 | 884 | 885 | 接着看 886 | 887 | ``` 888 | for i, sid in ipairs(ordered_selectors) do 889 | ngx.log(ngx.INFO, "==[BasicAuth][PASS THROUGH SELECTOR:", sid, "]") 890 | local selector = selectors[sid] 891 | if selector and selector.enable == true then 892 | local selector_pass 893 | if selector.type == 0 then -- 全流量选择器 894 | selector_pass = true 895 | else 896 | selector_pass = judge_util.judge_selector(selector, "basic_auth")-- selector judge 897 | end 898 | 899 | if selector_pass then 900 | local stop = filter_rules(sid, "basic_auth", ngx_var_uri, authorization) 901 | if stop then -- 不再执行此插件其他逻辑 902 | return 903 | end 904 | end 905 | end 906 | end 907 | ``` 908 | 909 | 910 | 911 | 对于我们的情况, 其实只有一个selector, 所以for循环只有一次, 但如果一个插件有多个selector的情况下, 就涉及到一个selector能处理还要不要继续的过程, 这个字段判断主要由continue负责 912 | 913 | ``` 914 | -- if continue or break the loop 915 | if selector.handle and selector.handle.continue == true then 916 | -- continue next selector 917 | else 918 | break 919 | end 920 | ``` 921 | 922 | 923 | 924 | 另外还有个log字段, 负责打印出更多的日志, 如果不需要, 就设置为false 925 | 926 | 927 | 928 | 由于我们是全流量`selector.type = 0`, 那么很自然的代码执行到`filter_rules` 929 | 930 | ``` 931 | local function filter_rules(sid, plugin, ngx_var_uri, authorization) 932 | local rules = orange_db.get_json(plugin .. ".selector." .. sid .. ".rules") 933 | 934 | for i, rule in ipairs(rules) do 935 | if rule.enable == true then 936 | -- judge阶段 937 | local pass = judge_util.judge_rule(rule, plugin) 938 | 939 | -- handle阶段 940 | local handle = rule.handle or {} 941 | if pass then 942 | if handle.credentials then 943 | local authorized = is_authorized(authorization, handle.credentials) 944 | if authorized then 945 | return true 946 | else 947 | ngx.exit(tonumber(handle.code) or 401) 948 | return true 949 | end 950 | else 951 | ngx.exit(tonumber(handle.code) or 401) 952 | return true 953 | end 954 | end 955 | end 956 | end 957 | return false 958 | end 959 | ``` 960 | 961 | 962 | 963 | 同理我们只有一条rule, for循环只有一次, 但如果有很多的rule, 都会进行judge处理, 如果某一条rule没有匹配, 就继续执行, 直到完结或命中为止. 964 | 965 | 代码分为2个阶段: 966 | 967 | - judge阶段, 主要处理url的match 968 | - handle阶段, 主要处理插件逻辑, 也就是basic_auth的authorization逻辑 969 | 970 | 971 | 972 | 973 | 974 | 975 | **judge阶段** 976 | 977 | 978 | 979 | 980 | `utils/judge.lua` 981 | 982 | ``` 983 | function _M.judge_rule(rule, plugin_name) 984 | if not rule or not rule.judge then return false end 985 | 986 | local judge = rule.judge 987 | local judge_type = judge.type 988 | local conditions = judge.conditions 989 | local pass = false 990 | if judge_type == 0 or judge_type == 1 then 991 | pass = _M.filter_and_conditions(conditions) 992 | elseif judge_type == 2 then 993 | pass = _M.filter_or_conditions(conditions) 994 | elseif judge_type == 3 then 995 | pass = _M.filter_complicated_conditions(judge.expression, conditions, plugin_name) 996 | end 997 | 998 | return pass 999 | end 1000 | ``` 1001 | 1002 | 1003 | 1004 | 这段基本是个马甲, 大概意思是根据type的字段去不同的condition进行下一步判断: 1005 | 1006 | 1007 | 1008 | 参考作者的注释即可 1009 | 1010 | > - 0表示只有一个匹配条件,1表示对所有条件**与**操作,2表示对所有条件**或**操作,3表示按照表达式(另一个字段`expression`)对所有条件求值 1011 | > - 注意当使用type为3的表达式求值时性能有一定损耗,请自行做好相关测试工作 1012 | > - expression当type为3时,此字段不为空,它的格式是一个lua的逻辑判断表达式表达式中每个值的格式为`v[index]`, 比如v[1]对应的就是第一个条件的值。示例:(v[1] or v[2]) and v[3],即前两个条件至少一个为真并且第三个条件为真时,规则为真 1013 | 1014 | 1015 | 1016 | 我们的type=0所以直接`filter_and_conditions` 1017 | 1018 | 1019 | 1020 | `utils/judge.lua` 1021 | 1022 | ``` 1023 | function _M.filter_and_conditions(conditions) 1024 | if not conditions then return false end 1025 | 1026 | local pass = false 1027 | for i, c in ipairs(conditions) do 1028 | pass = condition.judge(c) 1029 | if not pass then 1030 | return false 1031 | end 1032 | end 1033 | 1034 | return pass 1035 | end 1036 | ``` 1037 | 1038 | 1039 | 1040 | 如果有多个条件就一个一个验证, 只要有一个没通过就领盒饭, 这代码还是没到肉, 我们继续看 1041 | 1042 | 1043 | 1044 | `utils/condition.lua` 1045 | 1046 | ``` 1047 | function _M.judge(condition) 1048 | local condition_type = condition and condition.type 1049 | local operator = condition.operator 1050 | local expected = condition.value 1051 | local real 1052 | 1053 | if condition_type == "URI" then 1054 | real = ngx.var.uri 1055 | elseif 1056 | -- 其他逻辑 参考 http://orange.sumory.com/docs/concept/condition/ 1057 | end 1058 | return assert_condition(real, operator, expected) 1059 | end 1060 | ``` 1061 | 1062 | 1063 | 1064 | 代码精简了不少, 实际上最后就是一个断言, 当然这是个假断言, 它并不会再release模式下消失. 它要做的就是个比较工作 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | `utils/condition.lua` 1071 | 1072 | ``` 1073 | local function assert_condition(real, operator, expected) 1074 | if operator == 'match' then 1075 | if ngx_re_find(real, expected, 'isjo') ~= nil then 1076 | return true 1077 | end 1078 | end 1079 | return false 1080 | end 1081 | ``` 1082 | 1083 | 1084 | 1085 | ngx_re_find是正则表达式, 看到这里内心很鸡冻 1086 | 1087 | - operator = match 1088 | - real = /api/v1/test 1089 | - expected = /api/v1/test 1090 | 1091 | 1092 | 1093 | 1094 | 妥妥的PASS. 1095 | 1096 | 1097 | 1098 | ![](./img/orange/perfect.jpg) 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 虽然很高兴, 但事情还没完, 老司机要稳, 毕竟调试是报错了, 接着是第二阶段 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | **handle阶段** 1111 | 1112 | ``` 1113 | local handle = rule.handle or {} 1114 | if pass then 1115 | if handle.credentials then 1116 | local authorized = is_authorized(authorization, handle.credentials) 1117 | if authorized then 1118 | return true 1119 | else 1120 | ngx.exit(tonumber(handle.code) or 401) 1121 | return true 1122 | end 1123 | else 1124 | ngx.exit(tonumber(handle.code) or 401) 1125 | return true 1126 | end 1127 | end 1128 | ``` 1129 | 1130 | 1131 | 1132 | 首先是看handle有没有, 这个实际上在配置rule的时候就自动生成了, 接着判断有没有credentials, 这玩意是藏在header里面的. 1133 | 1134 | ``` 1135 | local authorization = headers and (headers["Authorization"] or headers["authorization"]) 1136 | ``` 1137 | 1138 | 1139 | 1140 | 但我们其实没有header, 所以进入else流程, 返回了handle.code, 杯具在此... 1141 | 1142 | 1143 | 1144 | 那么问题来了, 什么才是打开basic_auth的正确姿势呢? 1145 | 1146 | 1147 | 1148 | `utils/basic_auth/handler.lua` 1149 | 1150 | ``` 1151 | local function get_encoded_credential(origin) 1152 | local result = string_gsub(origin, "^ *[B|b]asic *", "") 1153 | result = string_gsub(result, "( *)$", "") 1154 | return result 1155 | end 1156 | 1157 | local function is_authorized(authorization, credentials) 1158 | if type(authorization) == "string" and authorization ~= "" then 1159 | local encoded_credential = get_encoded_credential(authorization) 1160 | for j, v in ipairs(credentials) do 1161 | local allowd = encode_base64(string_format("%s:%s", v.username, v.password)) 1162 | if allowd == encoded_credential then -- authorization passed 1163 | return true 1164 | end 1165 | end 1166 | end 1167 | return false 1168 | end 1169 | ``` 1170 | 1171 | 1172 | 1173 | authorization是你在http 头中传递的字符串, credentials是rule中记录的规则 1174 | 1175 | 首先把basic字样从authorization去掉, 然后base64(user:password)去比较, 如果对就PASS,不对就拒绝 1176 | 1177 | 1178 | 1179 | 嗯, 应该是这样...让我们来装个逼, 突然想起来侯捷大师经常说的一句话… 1180 | 1181 | 1182 | 1183 | ![](./img/orange/zhuangbi.jpg) 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 算了不装了. 1190 | 1191 | 1192 | 1193 | 再来调一把: 1194 | 1195 | ``` 1196 | MacBook-Pro:~ tuyou$ curl -H "Authorization: Basic dGVzdF91c2VyOnRlc3RfcGFzc3dvcmQ=" http://localhost:18888/api/v1/test 1197 | {"res":[],"message":"success","code":0} 1198 | ``` 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | ### 4.2 多模式匹配 1206 | 1207 | 1208 | 1209 | 有了上一节的基础, 在看这个会清楚很多 1210 | 1211 | 1212 | 1213 | 1214 | 1215 | **or匹配** 1216 | 1217 | `utils/judge.lua` 1218 | 1219 | ``` 1220 | function _M.filter_or_conditions(conditions) 1221 | local pass = false 1222 | for i, c in ipairs(conditions) do 1223 | pass = condition.judge(c) 1224 | if pass then 1225 | return true 1226 | end 1227 | end 1228 | return pass 1229 | end 1230 | ``` 1231 | 1232 | 1233 | 1234 | 很明显只要有一个rule pass就return, 大家自己根据下图体会 1235 | 1236 | ![](./img/orange/or_rule.png) 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1245 | 1246 | ### 4.3 流量 1247 | 1248 | 1249 | 1250 | 上面说了rule的match规则, 还有个全流量, 半流量, 1/3流量的概念, 哈哈, 后面是我编的, 实际上作者叫 自定义流量 1251 | 1252 | 1253 | 1254 | > ``` 1255 | > 0指该选择器是“全流量”选择器, 1指该选择器有一个“条件判断”模块,筛选出的流量才能进入此选择器 1256 | > ``` 1257 | 1258 | 1259 | 1260 | ![](./img/orange/custom_liuliang.png) 1261 | 1262 | 1263 | 1264 | 那我们就来看看这个, 再回到当初的地方 1265 | 1266 | 1267 | 1268 | `plugins/basic_auth/handler.lua` 1269 | 1270 | ``` 1271 | for i, sid in ipairs(ordered_selectors) do 1272 | ngx.log(ngx.INFO, "==[BasicAuth][PASS THROUGH SELECTOR:", sid, "]") 1273 | local selector = selectors[sid] 1274 | if selector and selector.enable == true then 1275 | local selector_pass 1276 | if selector.type == 0 then -- 全流量选择器 1277 | selector_pass = true 1278 | else 1279 | selector_pass = judge_util.judge_selector(selector, "basic_auth")-- selector judge 1280 | end 1281 | 1282 | if selector_pass then 1283 | local stop = filter_rules(sid, "basic_auth", ngx_var_uri, authorization) 1284 | if stop then -- 不再执行此插件其他逻辑 1285 | return 1286 | end 1287 | end 1288 | end 1289 | end 1290 | ``` 1291 | 1292 | 1293 | 1294 | 1295 | 1296 | 这次我们不是全流量了, 我们进去judge_selector 1297 | 1298 | `utils/judge.lua` 1299 | 1300 | ``` 1301 | function _M.judge_selector(selector, plugin_name) 1302 | local selector_judge = selector.judge 1303 | local judge_type = selector_judge.type 1304 | local conditions = selector_judge.conditions 1305 | 1306 | local selector_pass = false 1307 | if judge_type == 0 or judge_type == 1 then 1308 | selector_pass = _M.filter_and_conditions(conditions) 1309 | elseif judge_type == 2 then 1310 | selector_pass = _M.filter_or_conditions(conditions) 1311 | elseif judge_type == 3 then 1312 | selector_pass = _M.filter_complicated_conditions(selector_judge.expression, conditions, plugin_name) 1313 | end 1314 | 1315 | return selector_pass 1316 | end 1317 | ``` 1318 | 1319 | 1320 | 1321 | 我擦一模一样, 依靠同一套底层封装, 同时筛选selector和rule. 1322 | 1323 | 这么看来, 本质上就是处理和不处理, 只是发生在不同阶段罢了, 作者利用对逻辑操作符的封装, 通过小粒度组合成成千上万的可能, 这点上是非常灵活,敏捷的. 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | ## 最后 1330 | 1331 | 1332 | 1333 | 非常感谢作者, 写了那么优秀代码, 在此收益良多, 万分感谢. 1334 | 1335 | 1336 | 1337 | 1338 | 1339 | 1340 | 1341 | ## 附录 1342 | 1343 | 1344 | 1345 | ### lua_shared_dict 1346 | 1347 | > 语法:*lua_shared_dict * 1348 | > 1349 | > 1350 | > 1351 | > 该命令主要是定义一块名为name的共享内存空间,内存大小为size。通过该命令定义的共享内存对象对于Nginx中所有worker进程都是可见的,当Nginx通过reload命令重启时,共享内存字典项会从新获取它的内容,当时当Nginx退出时,字典项的值将会丢失 1352 | 1353 | 1354 | 1355 | 1356 | 1357 | ### init_by_lua 1358 | 1359 | > 语法:init_by_lua 1360 | > 1361 | > 语境:http 1362 | > 1363 | > 阶段:loading-config 1364 | > 1365 | > 当nginx master进程在加载nginx配置文件时运行指定的lua脚本,通常用来注册lua的全局变量或在服务器启动时预加载lua模块: 1366 | 1367 | 1368 | 1369 | ### ngx.timer.at 1370 | 1371 | > ## ngx.timer.at 1372 | > 1373 | > **syntax:** *hdl, err = ngx.timer.at(delay, callback, user_arg1, user_arg2, ...)* 1374 | > 1375 | > **context:** *init_worker_by_lua\*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua** 1376 | > 1377 | > Creates an Nginx timer with a user callback function as well as optional user arguments. 1378 | > 1379 | > The first argument, `delay`, specifies the delay for the timer, in seconds. One can specify fractional seconds like `0.001` to mean 1 millisecond here. `0` delay can also be specified, in which case the timer will immediately expire when the current handler yields execution. 1380 | > 1381 | > The second argument, `callback`, can be any Lua function, which will be invoked later in a background "light thread" after the delay specified. The user callback will be called automatically by the Nginx core with the arguments `premature`, `user_arg1`, `user_arg2`, and etc, where the `premature` argument takes a boolean value indicating whether it is a premature timer expiration or not, and `user_arg1`, `user_arg2`, and etc, are those (extra) user arguments specified when calling `ngx.timer.at` as the remaining arguments. 1382 | > 1383 | > Premature timer expiration happens when the Nginx worker process is trying to shut down, as in an Nginx configuration reload triggered by the `HUP` signal or in an Nginx server shutdown. When the Nginx worker is trying to shut down, one can no longer call `ngx.timer.at` to create new timers with nonzero delays and in that case `ngx.timer.at` will return a "conditional false" value and a string describing the error, that is, "process exiting". 1384 | > 1385 | > Starting from the `v0.9.3` release, it is allowed to create zero-delay timers even when the Nginx worker process starts shutting down. 1386 | > 1387 | > When a timer expires, the user Lua code in the timer callback is running in a "light thread" detached completely from the original request creating the timer. So objects with the same lifetime as the request creating them, like [cosockets](https://github.com/openresty/lua-nginx-module#ngxsockettcp), cannot be shared between the original request and the timer user callback function. 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | ## 参考 1394 | 1395 | 1. https://moonbingbing.gitbooks.io/openresty-best-practices 1396 | 2. http://geek.csdn.net/news/detail/37345 1397 | 3. http://openresty.org/download/agentzh-nginx-tutorials-zhcn.html 1398 | 4. http://www.cnblogs.com/wangxusummer/p/4309007.html 1399 | 5. http://jinnianshilongnian.iteye.com/blog/2280928 1400 | 1401 | 1402 | 1403 | --------------------------------------------------------------------------------