上一篇介紹了Tyk的限流設計,這篇記錄分析下它的反代設計,反代這個詞相信做後端的同學基本都聽說過(nginx的常用姿勢),代理分為正向代理和反向代理,因為我們這裡不是專門介紹代理的,我就簡單說下他們的區別,記住一個區分他們的要點就是「正向代理就是存取要出去」, 「反向代理就是存取要進來」,正向代理多用於一些需要做網際網路存取跳板機的場景,這裡就不多說了。而反向代理呢,微服務場景是用得比較多的,一個API Gateway支援反代是核心功能,Tyk作為這領域軟體的翹楚當然也得支援。GW的反代可用於負載均衡、存取中間人處理、認證等功能的實現上。
反代資料處理:
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的參照,複用資料結構,減少記憶體申請銷燬。當然真正的處理邏輯比我這邊分析的流程要複雜得多,比如對談狀態、授權這些的處理,這裡還沒列出來。
通過上一篇的限流和本篇反向的分析,細心點其實可以發現限流是擴充套件於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)
}
又回到這個為什麼設計的環節,其實關於http server/容器/框架的設計, middleware(中間人)這個詞應該是在很多著名的web框架裡面都有出現過的,比如springboot,gin,php的Laravel框架,中間人這種模式特別適合處理由上到下資料流的場景,相當於是一個資料庫的filter。
分享科學人文隨筆
感謝您「觀看」、「點贊」和「關注」,關注我的公眾號。