Contents

用OpenResty构建网关(2)—— 命令链实现

设计模式

网关是对HTTP流量的检查、分发,需要支持易扩展、可分发的特性。为了支持这个特性,我们很自然的想到设计模式中的职责链模式。

职责链定义(来自wikipedia): 责任链模式在面向对象程式设计里是一种软件设计模式,它包含了一些命令对象和一系列的处理对象。每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。

有了职责链,请求进来以后,经过所有的处理命令,才会到达下游的后端服务。如果我们有新的需求,可以用职责链直接扩展,修改配置直接集成新的处理命令模块。

Lua面向对象

那么问题来了,职责链在Lua这个语言怎么实现呢?

上文我们其实已经提到了,Lua可以用 MetaTable的__index操作符以及语法糖":"(a:b(param1)代表 a.b(self, param1))来支持面向对象的特性。当然,class模版定义,我们尽量使用Lua的module来做,代码看起来更加没有违和感。

Lua命令链实现

**command实现:**实现命令链的命令,我们需要定义一个Command接口,Command接口中有execute方法,Lua里没有接口的概念,我们就直接定义个Prototype作为一个抽象类的作用:

--[[
  base command of chain,to be extend
]]--

local _M = { _VERSION = '0.01' }
local mt = { __index = _M }

--Constructor
function _M:new()
  local o = {type = 'command'}
  return setmetatable(o, mt)
end

function _M:execute(context)
  ngx.log(ngx.ERR, "Not Implement!") 
  --true:stop and return; false: continue to the next chain
  return false
end

return _M

可以看到,我们通过定义一个模块_M,然后定义原型mt重载 __index操作符,使得mt具有 _M的行为,在_M的new函数中我们可以获得一个对象。

有了这个Command基础类的定义,我们可以实现一个自定义的Command,比如实现一个ip 黑白名单控制的Command:

--sub prototype of command
local command = require('aigetway.chain.command')
local _M = command:new()

local mt = { __index = _M }

--Override Constuctor
function _M:new()
  local o = {commandName = 'Ip Control Command' }
  return setmetatable(o, mt)
end

--Override function
function _M:execute(context)
    local log = context[contextKeys.LOG]
    local clientIp = get_client_ip()
    log:debug("client ip is ",clientIp)
    if not clientIp then
        log:errlog("client ip fetch failed ,pass ip control.")
        return
    end
    local ruleReadRedis = context[contextKeys.CFG_R_REDIS]
    local cache = Cache:Instance(iprulecachename,ruleReadRedis)
    local ipruleStirng = cache:get(ipcontrolRule,systemConf.cacheExpire.ipctlList)
    if (not ipruleStirng) or (ipruleStirng == 'null') then
        log:debug('no ip control rule, skipping...')
        return
    end

    local ipctlRules = cjson.decode(ipruleStirng)

    local resultKvs = dynamicBalancer.matchKvsByScore(ipctlRules,'hashkey',context,ruleReadRedis,log)
    if not resultKvs then
        log:debug('no ip control rule matched, skipping...')
    else
        _doIpControl(clientIp,resultKvs,cache)
    end
end

return _M

可以看到,我们先new了一个command赋值给_M作为super原型,然后在_M中重写new 和 command函数。new函数中,commandName可以看作为是当前这个command对象的成员变量。在重写的new函数中,利用mt继承了父原型的行为和属性,最终返回了一个新的子原型对象。

**filter 实现:**filter的Super Prototype与command一样的,只是filter多了一个postprocess函数,这一点是模仿的Apache Chain来实现的:

--[[
  base filter of chain,to be extend
]]--
local command = require('aigetway.chain.command')

local _M = command:new()
local mt = { __index = _M }

function _M:new()
    local o = {type = 'filter'}
    return setmetatable(o, mt)
end

function _M:execute(context)
    ngx.log(ngx.ERR, "execute Not Implement!")
    --true:stop and return; false: continue to the next chain
    return false
end

function _M:postprocess(context,err)
    ngx.log(ngx.ERR, "postprocess Not Implement!")
    --true:stop and return; false: continue to the next chain
    return false
end

return _M

**命令链执行器:**这个ipControl的command构建完成了,就差一个command链的执行器了,废话不说,上代码:

--[[
  executor of chian responsibility
]]--

local _M = { _VERSION = "0.01" }

local mt = { __index = _M }

xlocal ERR = ngx.ERR
local ngx_log = ngx.log

function _M:new()
  local commandArray = {}
  return setmetatable({commandArray = commandArray},mt);
end

function _M:addCommand(command)
  local commandArray = self.commandArray
  table.insert(commandArray,command)
end


function _M:execute(context)
  local commandArray = self.commandArray
  local savedError
  local handled = false

  --execute all command by ascend
  for i=1, #commandArray do
    local command = commandArray[i]
    local status,retinfo = xpcall(command.execute,handler,command,context)
    if not status then
      savedError = retinfo
      break
    else
      if retinfo == true then
        break
      end
    end
  end

  --execute all filter by descend
  for i = #commandArray , 1 , -1 do
    local command = commandArray[i]
    if command.type == 'filter' then
      local status,retinfo = xpcall(command.postprocess,handler,command,context,savedError)
      if not status then --filter will not return its own err,only print
        --return false,retinfo
        if type(retinfo) == "table" then
          local errinfo   = retinfo[1]
          local errstack  = retinfo[2]
          local err, desc = errinfo[1], errinfo[2]
          local errlogstring = oututils.dolog(err, desc, nil, errstack)
          ngx_log(ERR,errlogstring)
        end
      else
        if retinfo == true then
          handled = true
        end
      end
    end
  end

  if savedError ~= nil and handled == false then
    return false,savedError
  end
  return true,nil
end

return _M

这样我们整个命令链模块就完成了,只要实现了command和filter,把他们的名字连起来,成为一个chain,就可以顺序执行。

异常处理: command和filter的execute函数在遇到任何error时,会跳出执行栈,最终错误会传入到filter的postprocess中进行处理。