Tyk API Gateway反向代理设计

0x1 什么是反向代理?

上一篇介绍了Tyk的限流设计,这篇记录分析下它的反代设计,反代这个词相信做后端的同学基本都听说过(nginx的常用姿势),代理分为正向代理和反向代理,因为我们这里不是专门介绍代理的,我就简单说下他们的区别,记住一个区分他们的要点就是“正向代理就是访问要出去”, “反向代理就是访问要进来”,正向代理多用于一些需要做互联网访问跳板机的场景,这里就不多说了。而反向代理呢,微服务场景是用得比较多的,一个API Gateway支持反代是核心功能,Tyk作为这领域软件的翘楚当然也得支持。GW的反代可用于负载均衡、访问中间人处理、认证等功能的实现上。

0x2 流程分析

0x3 关键代码

反代数据处理:


func (p *ReverseProxy) WrappedServeHTTP(rw http.ResponseWriter, req *http.Request, withCache bool) ProxyResponse {
 // ...
 outreq := new(http.Request)

 *outreq = *req // includes shallow copies of maps, but okay
 // remove context data from the copies
 setContext(outreq, context.Background())
 // ...
 outreq.Header = cloneHeader(req.Header)
 // 如果使用缓存 
 if withCache {
     // 直接copy reponse
     p.CopyResponse(&bodyBuffer, res.Body)
   }
 }

 // 如果不使用缓存
 p.HandleResponse(rw, res, ses)    // copy实时请求的response到body
 return ProxyResponse{UpstreamLatency: upstreamLatency, Response: inres}
}

拷贝数据:


// copy header
func copyHeader(dst, src http.Header, ignoreCanonical bool) {

   removeDuplicateCORSHeader(dst, src)

   for k, vv := range src {
       if ignoreCanonical {
           dst[k] = append(dst[k], vv...)
           continue
       }
       for _, v := range vv {
           dst.Add(k, v)
       }
   }
}

// copy reponse
func (p *ReverseProxy) CopyResponse(dst io.Writer, src io.Reader) {
   if p.FlushInterval != 0 {
       if wf, ok := dst.(writeFlusher); ok {
           mlw := &maxLatencyWriter{
               dst:     wf,
               latency: p.FlushInterval,
               done:    make(chan bool),
           }
           go mlw.flushLoop()
           defer mlw.stop()
           dst = mlw
       }
   }

   p.copyBuffer(dst, src)
}

当然还有一些细节的处理,值的注意的是,为了保持高性能,处理数据都是采用[]byte,多处用到*[]byte的引用,复用数据结构,减少内存申请销毁。当然真正的处理逻辑比我这边分析的流程要复杂得多,比如会话状态、授权这些的处理,这里还没列出来。

0x4 展开Tyk代码架构模式

通过上一篇的限流和本篇反向的分析,细心点其实可以发现限流是扩展于Tyk的中间人(TykMiddleware)设计,遵循了装饰器设计模式,继承于TykMiddleware抽象interface(java很熟悉的Component接口类),扩展并重写相关的方法。

中间人抽象:


type TykMiddleware interface {
    Init()
    Base() *BaseMiddleware
    SetName(string)
    SetRequestLogger(*http.Request)
    Logger() *logrus.Entry
    Config() (interface{}, error)
    ProcessRequest(w http.ResponseWriter, r *http.Request, conf interface{}) (error, int) // Handles request
    EnabledForSpec() bool
    Name() string
}

每一个具体的中间人主体的入口方法为ProcessRequest,例如我们上一篇的RateLimit。

而本篇的反代却是在限流设计的上一层,api_loader模块,所有处理都会通过api_loader的processSpec,GW的一些预先处理(Prepare)都会放在这里,例如会话、CORS配置、反代等、值得注意的是这里有一个统一的自定义中间件装载的封装(loadCustomMiddleware),api_loader就是通过这个封装去注册TykMiddleware的中间件,而它们之间的中间件注册数据结构就是 chainArray,一个储存链元素的列表

中间人链数据:

for _, obj := range mwPreFuncs {
        if mwDriver == apidef.GoPluginDriver {
            // ...
        } else if mwDriver != apidef.OttoDriver {
            // ...
            mwAppendEnabled(&chainArray, &CoProcessMiddleware{baseMid, coprocess.HookType_Pre, obj.Name, mwDriver, obj.RawBodyOnly, nil})
        } else {
            chainArray = append(chainArray, createDynamicMiddleware(obj.Name, true, obj.RequireSession, baseMid))
        }
    }

    mwAppendEnabled(&chainArray, &VersionCheck{BaseMiddleware: baseMid})
    mwAppendEnabled(&chainArray, &RateCheckMW{BaseMiddleware: baseMid})
    mwAppendEnabled(&chainArray, &IPWhiteListMiddleware{BaseMiddleware: baseMid})
    mwAppendEnabled(&chainArray, &IPBlackListMiddleware{BaseMiddleware: baseMid})
    // ...

中间人的ProcessRequest 统一返回error, errorCode, middleware根据这两个值来进行数据流下一步的处理


err, errCode := mw.ProcessRequest(w, r, mwConf)
if err != nil {
  // GoPluginMiddleware are expected to send response in case of error
  // but we still want to record error
  _, isGoPlugin := actualMW.(*GoPluginMiddleware)

  handler := ErrorHandler{*mw.Base()}
  handler.HandleError(w, r, err.Error(), errCode, !isGoPlugin)

  meta["error"] = err.Error()

  finishTime := time.Since(startTime)

  if instrumentationEnabled {
    job.TimingKv("exec_time", finishTime.Nanoseconds(), meta)
    job.TimingKv(eventName+".exec_time", finishTime.Nanoseconds(), meta)
  }

  mw.Logger().WithError(err).WithField("code", errCode).WithField("ns", finishTime.Nanoseconds()).Debug("Finished")
  return
}

有意思的彩蛋, middleware有一种情况就是无错误返回,,但是仍然需要返回一个状态码去匹配一些特殊情况, 这个状态码就是 const mwStatusRespond = 666,不禁让我想起难道Tyk的coder也是一位老铁?


// Special code, bypasses all other execution
if errCode != mwStatusRespond {
    // No error, carry on...
    meta["bypass"] = "1"
    h.ServeHTTP(w, r)
} else {
    mw.Base().UpdateRequestSession(r)
}

0x5 为什么这样设计?

又回到这个为什么设计的环节,其实关于http server/容器/框架的设计, middleware(中间人)这个词应该是在很多著名的web框架里面都有出现过的,比如springboot,gin,php的Laravel框架,中间人这种模式特别适合处理由上到下数据流的场景,相当于是一个数据库的filter。

  • middleware支持可插拔(装饰器模式),可随时启用/禁用中间件而整体服务不受影响
  • 符合正向性设计,功能模块都是独立的,每一个中间件从处理、日志都是根据中间人本身的需求而定制
  • 装饰go原生的 net.http的方法ServeHTTP(http.Handler抽象),其实从这个角度来看,可以套用其他go的web框架来处理http/ws请求,比如gin,httprouter等都是装饰ServeHTTP,方便扩展
  • GW load配置时统一注册中间人,不使用的中间人不会有逻辑数据交集,gw运行时的功能设计不涉及多个中间人交互,整体数据流处理是Filter Chain

image

分享科学人文随笔

感谢您「观看」、「点赞」和「关注」,关注我的公众号。

你可能感兴趣的:(Tyk API Gateway反向代理设计)