[OpenYurtの詳細な分析]エッジゲートウェイキャッシング機能のエレガントな実現

頭のpicture.png

著者| He Linbo(Xinsheng)
出典| AlibabaCloudネイティブ公式アカウント

OpenYurt:ネイティブK8の機能をエッジまで拡張

アリババクラウドエッジコンテナサービスの開始から1年後、クラウドネイティブエッジコンピューティングソリューションであるOpenYurtが正式にオープンソースになりました。他のオープンソースコンテナ化エッジコンピューティングソリューションとの違いは、OpenYurtがネイティブKubernetesをエッジに拡張するという概念に準拠していることです。 、およびKubernetesシステムではゼロです。ネイティブKubernetesからOpenYurtへのワンクリック変換を変更および提供して、ネイティブK8sクラスターがエッジクラスター機能を持つようにします。

同時に、OpenYurtは進化を続けているため、次の開発コンセプトを維持し続けます。

  • 非侵襲的拡張K8
  • クラウドネイティブコミュニティの主流テクノロジーで進化し続ける

OpenYurtがエッジの自律性の問題をどのように解決するか

Kubernetesシステムをエッジコンピューティングシナリオに拡張する場合、エッジノードはパブリックネットワークとクラウドを介して接続されます。ネットワーク接続には制御できない大きな要因があり、エッジサービスの動作に不安定な要因を引き起こす可能性があります。これはクラウドネイティブおよびエッジコンピューティング。統合の主な問題の1つ。

この問題を解決するには、エッジ側に自律性を持たせる必要があります。つまり、クラウドエッジネットワークが切断されている場合や接続が不安定な場合に、エッジビジネスを継続して実行できるようにする必要があります。OpenYurtでは、この機能はyurt-controller-managerおよびYurtHubコンポーネントによって提供されます。

1.YurtHubアーキテクチャ

前回の記事では、YurtHubコンポーネント機能について詳しく紹介しましたアーキテクチャ図は次のとおりです。

1.png

画像リンク

YurtHubは、データキャッシュ機能を備えた「透過ゲートウェイ」です。ノードまたはコンポーネントがクラウドネットワークの切断状態で再起動すると、各コンポーネント(kubelet / kube-proxyなど)はYurtHubからビジネスコンテナ関連のデータを取得します。効果的である限界自律性の問題を解決します。これは、データキャッシュ機能を備えた軽量のリバースプロキシを実装する必要があることも意味します。

2.最初の考え

データをキャッシュするためのリバースプロキシを実装するための最初のアイデアは、response.Bodyからデータを読み取り、それを要求元のクライアントとローカルのキャッシュモジュールにそれぞれ返すことです。擬似コードは次のとおりです。

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のウォッチリクエストなど)を処理する方法。つまり、ioutil.ReadAll()は1回の呼び出しですべてのデータを返すことはできません。つまり、ストリームデータを返す方法とストリームデータを同時にキャッシュする方法です。

  • 質問2:同時に、データをローカルにキャッシュする前に、最初に着信バイトスライスデータをクリーンアップする必要がある場合があります。これは、バイトスライスを変更するか、処理する前にバイトスライスをバックアップする必要があることを意味します。これにより大量のメモリが消費されると同時に、ストリーミングデータの場合、適用されるスライスの大きさを処理するのは簡単ではありません。

3.エレガントな実現に関する議論

上記の問題に対応して、問題を1つずつ抽象化し、より洗練された実装方法を見つけることができます。

  • 質問1:ストリームデータの読み取りと書き込みを同時に行う方法

次の図に示すように、ストリーミングデータの読み取りと書き込み(戻りながらのキャッシュ)の場合、必要なのはresponse.Body(io.Reader)をio.Readerとio.Writerに変換することだけです。つまり、io.Readerとio.Writerが組み合わされてio.Readerになります。LinuxのTeeコマンドは簡単に思い浮かびます。

2.png

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の統合により、クライアントにresponse.Bodyからのデータの読み取りを要求すると、同時に戻りデータがキャッシュに書き込まれ、ストリーミングデータの処理がエレガントに解決されます。

  • 質問2:キャッシュする前にストリームデータをクリーンアップする方法

次の図に示すように、ストリームデータはキャッシュする前にクリーンアップされます。リクエスターとフィルターは、response.Bodyを同時に読み取る必要があります(2つの読み取りの問題)。つまり、response.Body(io.Reader)を2つのio.Readerに変換する必要があります。

3.png

また、質問2が次のように変換されることも意味します。質問1のキャッシュ側のio.Writerは、データフィルターの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を介して、クライアントが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サンドボックスに入った後も、急速な開発と反復を維持し続けています。コミュニティの学生の共同の努力により、現在のオープンソース機能には次のものが含まれます。

  • 限界自律性
  • エッジユニット管理
  • クラウド側の共同運用と保守
  • ワンクリックのシームレスな変換機能

同時に、コミュニティの学生と十分に話し合った後、OpenYurtコミュニティは2021年のロードマップも発表しました。興味のある学生は一緒に貢献することを歓迎します。
OpenYurtに興味がある場合は、QRコードをスキャンしてコミュニティ交換グループに参加し、OpenYurtの公式ウェブサイトとGitHubプロジェクトのアドレスにアクセスしてください。

おすすめ

転載: blog.51cto.com/13778063/2678454