OpenYurt:延伸原生 K8s 的能力到邊緣
阿里雲邊緣容器服務上線 1 年後,正式開源了雲原生邊緣計算解決方案 OpenYurt,跟其他開源的容器化邊緣計算方案不同的地方在於:OpenYurt 秉持 Extending your native Kubernetes to edge 的理念,對 Kubernetes 系統零修改,並提供一鍵式轉換原生 Kubernetes 爲 OpenYurt,讓原生 K8s 集羣具備邊緣集羣能力。
同時隨着 OpenYurt 的持續演進,也一定會繼續保持如下發展理念:
- 非侵入式增強 K8s
- 保持和雲原生社區主流技術同步演進
OpenYurt 如何解決邊緣自治問題
想要實現將 Kubernetes 系統延展到邊緣計算場景,那麼邊緣節點將通過公網和雲端連接,網絡連接有很大不可控因素,可能帶來邊緣業務運行的不穩定因素,這是雲原生和邊緣計算融合的主要難點之一。
解決這個問題,需要使邊緣側具有自治能力,即當雲邊網絡斷開或者連接不穩定時,確保邊緣業務可以持續運行。在 OpenYurt 中,該能力由 yurt-controller-manager 和 YurtHub 組件提供。
1. YurtHub 架構
在之前的文章中,我們詳細介紹了 YurtHub 組件的能力。其架構圖如下:
YurtHub 是一個帶有數據緩存功能的“透明網關”,和雲端網絡斷連狀態下,如果節點或者組件重啓,各個組件(kubelet/kube-proxy 等)將從 YurtHub 中獲取到業務容器相關數據,有效解決邊緣自治的問題。這也意味着我們需要實現一個輕量的帶數據緩存能力的反向代理。
2. 第一想法
實現一個緩存數據的反向代理,第一想法就是從 response.Body 中讀取數據,然後分別返回給請求 client 和本地的 Cache 模塊。僞代碼如下:
func HandleResponse(rw http.ResponseWriter, resp *http.Response) {
bodyBytes, _ := ioutil.ReadAll(resp.Body)
go func() {
// cache response on local disk
cacher.Write(bodyBytes)
}
// client reads data from response
rw.Write(bodyBytes)
}
當深入思考後,在 Kubernetes 系統中,上述實現會引發下面的問題:
- 問題 1:流式數據需要如何處理(如: K8s 中的 watch 請求),意味 ioutil.ReadAll() 一次調用無法返回所有數據。即如何可以返回流數據同時又緩存流數據。
- 問題 2:同時在本地緩存數據前,有可能需要對傳入的 byte slice 數據先進行清洗處理。這意味着需要修改 byte slice,或者先備份 byte slice 再處理。這樣會造成內存的大量消耗,同時針對流式數據,到底申請多大的 slice 也不好處理。
3. 優雅實現探討
針對上面的問題,我們將問題逐個抽象,可以發現更優雅的實現方法。
- 問題 1:如何對流數據同時進行讀寫
針對流式數據的讀寫(一邊返回一邊緩存),如下圖所示,其實需要的不過是把 response.Body(io.Reader) 轉換成一個 io.Reader 和一個 io.Writer。或者說是一個 io.Reader 和 io.Writer 合成一個 io.Reader。這很容易就聯想到 Linux 裏面的 Tee 命令。
而在 Golang 中 Tee 命令是實現就是io.TeeReader,那問題 1 的僞代碼如下:
func HandleResponse(rw http.ResponseWriter, resp *http.Response) {
// create TeeReader with response.Body and cacher
newRespBody := io.TeeReader(resp.Body, cacher)
// client reads data from response
io.Copy(rw, newRespBody)
}
通過 TeeReader 的對 Response.Body 和 Cacher 的整合,當請求 client 端從 response.Body 中讀取數據時,將同時向 Cache 中寫入返回數據,優雅的解決了流式數據的處理。
- 問題 2:如何在緩存前先清洗流數據
如下圖所示,緩存前先清洗流數據,請求端和過濾端需要同時讀取 response.Body(2 次讀取問題)。也就是需要將 response.Body(io.Reader) 轉換成兩個 io.Reader。
也意味着問題 2 轉化成:問題 1 中緩存端的 io.Writer 轉換成 Data Filter 的 io.Reader。其實在 Linux 命令中也能找到類似命令,就是管道。因此問題 2 的僞代碼如下:
func HandleResponse(rw http.ResponseWriter, resp *http.Response) {
pr, pw := io.Pipe()
// create TeeReader with response.Body and Pipe writer
newRespBody := io.TeeReader(resp.Body, pw)
go func() {
// filter reads data from response
io.Copy(dataFilter, pr)
}
// client reads data from response
io.Copy(rw, newRespBody)
}
通過 io.TeeReader 和 io.PiPe,當請求 client 端從 response.Body 中讀取數據時,Filter 將同時從 Response 讀取到數據,優雅的解決了流式數據的 2 次讀取問題。
YurtHub 實現
最後看一下 YurtHub 中相關實現,由於 Response.Body 爲 io.ReadCloser,所以實現了 dualReadCloser。同時 YurtHub 可能也面臨對 http.Request 的緩存,所以增加了 isRespBody 參數用於判定是否需要負責關閉 response.Body。
// https://github.com/openyurtio/openyurt/blob/master/pkg/yurthub/util/util.go#L156
// NewDualReadCloser create an dualReadCloser object
func NewDualReadCloser(rc io.ReadCloser, isRespBody bool) (io.ReadCloser, io.ReadCloser) {
pr, pw := io.Pipe()
dr := &dualReadCloser{
rc: rc,
pw: pw,
isRespBody: isRespBody,
}
return dr, pr
}
type dualReadCloser struct {
rc io.ReadCloser
pw *io.PipeWriter
// isRespBody shows rc(is.ReadCloser) is a response.Body
// or not(maybe a request.Body). if it is true(it's a response.Body),
// we should close the response body in Close func, else not,
// it(request body) will be closed by http request caller
isRespBody bool
}
// Read read data into p and write into pipe
func (dr *dualReadCloser) Read(p []byte) (n int, err error) {
n, err = dr.rc.Read(p)
if n > 0 {
if n, err := dr.pw.Write(p[:n]); err != nil {
klog.Errorf("dualReader: failed to write %v", err)
return n, err
}
}
return
}
// Close close two readers
func (dr *dualReadCloser) Close() error {
errs := make([]error, 0)
if dr.isRespBody {
if err := dr.rc.Close(); err != nil {
errs = append(errs, err)
}
}
if err := dr.pw.Close(); err != nil {
errs = append(errs, err)
}
if len(errs) != 0 {
return fmt.Errorf("failed to close dualReader, %v", errs)
}
return nil
}
在使用 dualReadCloser 時,可以在
httputil.NewSingleHostReverseProxy的modifyResponse()方法中看到。代碼如下:
// https://github.com/openyurtio/openyurt/blob/master/pkg/yurthub/proxy/remote/remote.go#L85
func (rp *RemoteProxy) modifyResponse(resp *http.Response) error {rambohe-ch, 10 months ago: • hello openyurt
// 省略部分前置檢查
rc, prc := util.NewDualReadCloser(resp.Body, true)
go func(ctx context.Context, prc io.ReadCloser, stopCh <-chan struct{}) {
err := rp.cacheMgr.CacheResponse(ctx, prc, stopCh)
if err != nil && err != io.EOF && err != context.Canceled {
klog.Errorf("%s response cache ended with error, %v", util.ReqString(req), err)
}
}(ctx, prc, rp.stopCh)
resp.Body = rc
}
總結
OpenYurt 於 2020 年 9 月進入 CNCF 沙箱後,持續保持了快速發展和迭代,在社區同學一起努力下,目前已經開源的能力有:
- 邊緣自治
- 邊緣單元化管理
- 雲邊協同運維
- 一鍵式無縫轉換能力
作者 | 何淋波(新勝)
本文爲阿里雲原創內容,未經允許不得轉載。