Go http.Flusher 在實際項目中的應用

最近在使用 Docker Go SDK 做開發的時候,參考了官方的示例代碼:

package main

import (
	"io"
	"os"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
	"golang.org/x/net/context"
)

func main() {
	ctx := context.Background()
	cli, err := client.NewEnvClient()
	if err != nil {
		panic(err)
	}

	reader, err := cli.ImagePull(ctx, "docker.io/library/alpine", types.ImagePullOptions{})
	if err != nil {
		panic(err)
	}
	io.Copy(os.Stdout, reader)

	resp, err := cli.ContainerCreate(ctx, &container.Config{
		Image: "alpine",
		Cmd:   []string{"echo", "hello world"},
		Tty:   true,
	}, nil, nil, "")
	if err != nil {
		panic(err)
	}

	if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
		panic(err)
	}

	statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
	select {
	case err := <-errCh:
		if err != nil {
			panic(err)
		}
	case <-statusCh:
	}

	out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true})
	if err != nil {
		panic(err)
	}

	io.Copy(os.Stdout, out)
}

這段代碼主要做了 4 件事:

  1. 拉取鏡像
  2. 創建容器
  3. 啓動容器
  4. 獲取容器日誌輸出

其中有個地方引起了我的注意:

reader, err := cli.ImagePull(ctx, "docker.io/library/alpine", types.ImagePullOptions{})
if err != nil {
    panic(err)
}
io.Copy(os.Stdout, reader)

因爲沒有用到返回結果 reader,所以不假思素地將它去掉:

_, err := cli.ImagePull(ctx, "docker.io/library/alpine", types.ImagePullOptions{})
if err != nil {
    panic(err)
}

保存代碼,再次執行,當運行到 cli.ContainerCreate 的時候,會收到No such image: xx之類的錯誤。

能夠運行到 cli.ContainerCreate 說明已經執行了 cli.ImagePull,而且沒有錯誤,那爲何還會提示找不到鏡像?

想到的唯一解釋是:

當通過 cli.ImagePull 拉取鏡像的時候,雖然請求返回了結果,並不表示 Docker 服務端真的將鏡像拉取完成,因爲從遠程倉庫拉取鏡像往往耗時較長,很有可能正在拉取中。

爲了驗證猜想,我們深入 Docker 源碼,一探究竟。

Docker 源碼探究

客戶端代碼:

當客戶端執行 ImagePull 的時候,實際發送了一個 POST 請求,地址爲 /images/create, 可以參考代碼 :

 

服務端代碼:

/images/create 請求處理代碼爲:

 

這段代碼主要通過 output = ioutils.NewWriteFlusher(w) 封裝一個 WriteFlusher 返回結果。

WriteFlusher 的 Write 函數 爲 :

 

最後請求是通過 progressOutput 的 WriteProgress 返回進度:

 

到目前爲止我們大致弄明白了 Docker ImagePull 的邏輯,簡單總結一下:

  1. 客戶端發送 POST 請求到 Docker 服務端 /images/create
  2. 服務端通過 WriteFlusher 來負責請求的返回,這裏使用了 Go http.Flusher, 可以不斷向客戶端刷新數據。

所以當我們客戶端發送 ImagePull 請求後,雖然可以很快獲得 http.Response 對象,但它並不表示最終任務完成(請求結束),而是先返回請求狀態碼,再不斷返回拉取進度,那怎樣才知道任務完成了呢?

可以使用官方例子中的 io.Copy(os.Stdout, reader) 或 ioutil.ReadAll(reader) ,因爲它們讀取 Body 內容,會阻塞在這裏,直到任務結束(讀取報錯,或者 EOF 標記)。

我把 Docker API 簡單梳理了下,大致可以分爲兩類:

  • 使用 WriteFlusher 返回結果:主要用於耗時的任務,不僅可以通過快速向客戶端返回狀態碼(200)來表明請求合法已經被接受處理,還可以不斷向客戶端刷新任務進度,實現實時效果,類似 Pusher。
  • 直接返回結果:主要用於耗時較短的狀態查詢接口。

Go 中 http.Flusher 示例

在某些場景下 Go 的 http.Flusher 還是非常有用的,比如可以用它來做流式 IO,如文件上傳/下載/內容預處理等,那我們來看一個簡單 http.Flusher 的用法:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("x-request-id", "x-request-id")

		f, _ := w.(http.Flusher)

		for i := 0; i < 10; i++ {
			fmt.Fprintf(w, "time.now(): %v \n\r", time.Now())
			f.Flush()
			time.Sleep(time.Second)
		}

	})

	log.Fatal(http.ListenAndServe(":8888", nil))
}

代碼邏輯爲: 每隔一秒向客戶端 flush 當前時間。

當運行代碼,並使用 curl -i http://localhost:8888 查看結果,可以看到類似輸出:

 

注意:想要看到整個刷新過程,需要客戶端的支持 (這裏使用的是 curl, 你還可以使用 wget),我們可以看到終端每隔一秒從服務端獲取輸出結果,10 秒後請求結束。

在調用 Flush 之前,需要保證寫入 http.ResponseWriter 的內容以 \n 結尾,不然不會輸出到客戶端。

寫在最後

我們通過 Docker 客戶端 ImagePull 接口一個問題出發,通過 Docker 源碼瞭解了整個 Docker 鏡像拉取流程, 也弄明白使用 io.Copy(os.Stdout, reader) 確保拉取任務結束的必要性。

舉一反三,根據這個實現邏輯,我們可以將 Docker API 分爲使用 WriteFlusher 和直接返回 body 內容兩類。

最後還通過簡單例子學習了 Go 的 http.Flusher 用法,希望在實際的工作中對大家有所幫助。

作者:宋佳洋

51Reboot K8S、Golang 課程試聽預約wechat:17812796384

發佈了46 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章