Tyk API Gateway反向代理設計

2021-03-30 03:00:51

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

分享科學人文隨筆

感謝您「觀看」、「點贊」和「關注」,關注我的公眾號。