Docker的二次開發:一個docker容器的守護程序

介紹

docker的sdk的官方介紹的樣例有go和Python的,并包含了如下對docker二次開發的幾種簡單的實現

具體代碼請移步上述鏈接。

這篇主要講講怎樣用go對docker進行簡單的二次開發:一個docker容器的守護程序

kubernetes就是利用go對docker進行二次開發以管理成千上萬的docker容器的成功案例

kubernetes部署需要踩很多坑,但是有時我們只需要對docker進行一次簡單的二次開發以滿足業務的需求,如新上線一個版本,我們需要在docker容器中部署,此時就可以對docker進行二次開發以滿足我們的需求。
需求一

  • 從git或svn上拉取最新的代碼,并將其編譯成go的二進制可運行文件
  • 從docker倉庫中拉需要的鏡像
  • 在鏡像的基礎上創建容器,包括配置容器的一些參數等等
  • 啟動容器

這樣當我們需要發布一個項目的新版本時直接運行這個程序就能做到一鍵發布。一個容器運行時,就像一個操作系統運行一樣,也有崩潰的時候,此時我們需要一個監聽docker容器的健康狀況來以防一些意外
需求二

  • 監聽docker容器運行時的相關參數
  • 針對獲取到的參數做出相應的處理,如mem使用打到80%時發送郵件通知小組的開發人員
  • 在docker容器崩潰時能重新啟動該容器

假設需求

現在我就上面介紹的兩個需求簡單綜合一下,以完成一個自己的需求

  1. 假設本地已有我們需要的docker image
  2. 檢查docker container中是否已存在目標容器
  3. 若有,則跳轉到第5步
  4. 若沒有,創建一個從container
  5. 啟動該容器并按時檢查該container的狀態
  6. 若該container已崩潰,那么該程序能自動重啟container

附:我們所期望的container內部還掛在了一個宿主機的目錄

以上就是本篇文章將要實現的功能

正篇

SDK的安裝

go get github.com/docker/docker/client

安裝成功之后將$GOPATH/src/github.com/docker/docker下的vendor中的文件拷貝到$GOPATH/src下,然后刪除vendor文件

注:如果不進行上述操作,會有包沖突問題,比如import包github.com/docker/go-connections/nat時,程序優先找到的是github.com/docker/docker/vendor/下的github.com/docker/go-connections/nat包,而不是$GOPATH/src/github.com/docker/go-connections/nat包,所以會有包沖突

實現

注:最終目的是啟動docker容器之后還要運行其中的ginDocker服務,本篇程序實現的 部分功能 和如下的docker命令的效果一樣

docker run -it --name mygin-latest -p 7070:7070 -v /home/youngblood/Go/src/ginDocker:/go/src/ginDocker -w /go/src/ginDocker my-gin
1.檢查本地是否有我們需求的image

這里有很多方法可以實現這個,就像運行docker pull命令時一樣,docker首先會檢查本地是否有該image,如果沒有才去docker hub 拉取這個image,所以這里我們直接使用代碼拉取鏡像即可,類似于這樣(但是該篇示例程序中并沒有寫拉取鏡像的代碼,因為該鏡像是本地自己創建的一個鏡像,和Docker中go web項目部署中的鏡像是一樣的)

    rc, err := cli.ImagePull(ctx, "busybox", types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }
    defer rc.Close()
2.檢查docker container中是否已存在目標容器

當創建一個container時,顯示的給函數傳遞一個container name,那么之后我們再次運行這個程序時同樣會創建同名的container。但是,docker中不允許存在同名的container,所以會創建失敗,這樣就可以在創建container時確認該container是否存在,代碼如下

    imageName := "my-gin:latest"
    cont, err := cli.ContainerCreate(ctx, &container.Config{
        Image:      imageName,               //Docker基于該鏡像創建容器
        Tty:        true,                    //docker run 命令的-t
        OpenStdin:  true,                    //docker run命令的-i
        Cmd:        []string{"./ginDocker2"},//docker容器中執行的命令
        WorkingDir: "/go/src/ginDocker2",    //docker容器工作目錄
        ExposedPorts: nat.PortSet{            //docker容器對外開放的端口
            "7070": struct{}{},
        },
    }, &container.HostConfig{
        PortBindings: nat.PortMap{
            "7070": []nat.PortBinding{nat.PortBinding{//docker容器映射到宿主機的端口
                HostIP:   "0.0.0.0",
                HostPort: "7070",
            }},
        },
        Mounts: []mount.Mount{//docker容器卷掛載
            mount.Mount{
                Type:   mount.TypeBind,
                Source: "/home/youngblood/Go/src/ginDocker2",
                Target: "/go/src/ginDocker2",
            },
        },
    }, nil, "mygin-latest")

關于上述代碼作如下簡述:

  1. &container.Config中的Tty和OpenStdin是-it標識,WorkingDir是-w標識,ExposePorts是容器對外開放的端口。
  2. &container.HostConfig中PortMap表示端口映射,是-p標識,注意這里必須和ExposedPorts配對使用,也就是說容器開放了哪個端口,哪個端口才能映射到宿主機上,否則即使能映射成功,由于該端口容器未開放,也不能訪問服務;Mounts是-v標識,其中的Type有4種,分別是TypeBind="bind",TypeVolume="volume",TypeTmpfs="tmpfs",TypeNamedPipe="npipe",其中bind表示掛在到host dir,所以這里選擇使用TypeBind。
  3. nil表示的是*net.NetWorkingConfig,由于此處沒有配置,所以使用nil
  4. "mygin-latest"表示容器的name
3. 啟動該容器并按時檢查該container的狀態

啟動容器

        //啟動容器
        if err = cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
            panic(err)
        }

獲取容器內部的運行狀態

    status, err = cli.ContainerStats(ctx, id, true)
    if err != nil {
        panic(err)
    }
    io.Copy(os.Stdout, status.Body)

將status.Body輸出到標準輸出中,你會看到控制臺不斷的輸出容器的狀態參數等,你可以根據status.Body獲取你關心的一些參數

4.若該container已崩潰,那么該程序能自動重啟container

下列代碼能獲取到正在運行的container。利用container的name屬性來判斷該container是否是在運行。

        //獲取正在運行的container list
        containerList, err := cli.ContainerList(ctx, types.ContainerListOptions{})
        if err != nil {
            panic(err)
        }

        var contTemp types.Container
        //找出名為“mygin-latest”的container并將其存入contTemp中
        for _, v1 := range containerList {
            log.Println("name=", v1.ID)
            for _, v2 := range v1.Names {
                if v2 == "/mygin-latest" {
                    contTemp = v1//若contTemp為空,則該容器未運行;反之,正在運行
                    break
                }
            }
        }

綜合

目前,每一步最基本的做法我們已經實現并貼出了代碼,接下來的工作就是將這個工作整合到一起,做一個簡單的封裝并做好流程調度即可。

ginDocker2

是我們要在docker容器中發布的一個Go項目
代碼如下

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/hello/:name", func(c *gin.Context) {
        name := c.Param("name")
        c.String(http.StatusOK, "hello %s", name)
    })
    router.Run(":7070")
}

注:由于在下面的這個程序中創建容器時沒辦法一次執行多個cmd命令,所以這里的ginDocker2是先在外面的終端執行go build ginDocker2,在目錄ginDocker2下生成一個可執行的二進制文件ginDocker2

守護程序containerDeamon

聲明

  • 該程序相當于執行命令:docker run -it --name mygin-latest -p 7070:7070 -v /home/youngblood/Go/src/ginDocker:/go/src/ginDocker -w /go/src/ginDocker my-gin
  • 該程序會檢測名為mygin-latest的容器是否存在,并檢查該容器是否在運行,若沒有,則啟動容器并運行其中的程序

代碼

package main

import (
    "io"
    "log"
    "os"
    "time"

    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/mount"
    "github.com/docker/docker/client"
    "github.com/docker/go-connections/nat"
    "golang.org/x/net/context"
)

const (
    imageName     string   = "my-gin:latest"                      //鏡像名稱
    containerName string   = "mygin-latest"                       //容器名稱
    indexName     string   = "/" + containerName                  //容器索引名稱,用于檢查該容器是否存在是使用
    cmd           string   = "./ginDocker2"                       //運行的cmd命令,用于啟動container中的程序
    workDir       string   = "/go/src/ginDocker2"                 //container工作目錄
    openPort      nat.Port = "7070"                               //container開放端口
    hostPort      string   = "7070"                               //container映射到宿主機的端口
    containerDir  string   = "/go/src/ginDocker2"                 //容器掛在目錄
    hostDir       string   = "/home/youngblood/Go/src/ginDocker2" //容器掛在到宿主機的目錄
    n             int      = 5                                    //每5s檢查一個容器是否在運行

)

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

//創建容器
func createContainer(ctx context.Context, cli *client.Client) {
    //創建容器
    cont, err := cli.ContainerCreate(ctx, &container.Config{
        Image:      imageName,     //鏡像名稱
        Tty:        true,          //docker run命令中的-t選項
        OpenStdin:  true,          //docker run命令中的-i選項
        Cmd:        []string{cmd}, //docker 容器中執行的命令
        WorkingDir: workDir,       //docker容器中的工作目錄
        ExposedPorts: nat.PortSet{
            openPort: struct{}{}, //docker容器對外開放的端口
        },
    }, &container.HostConfig{
        PortBindings: nat.PortMap{
            openPort: []nat.PortBinding{nat.PortBinding{
                HostIP:   "0.0.0.0", //docker容器映射的宿主機的ip
                HostPort: hostPort,  //docker 容器映射到宿主機的端口
            }},
        },
        Mounts: []mount.Mount{ //docker 容器目錄掛在到宿主機目錄
            mount.Mount{
                Type:   mount.TypeBind,
                Source: hostDir,
                Target: containerDir,
            },
        },
    }, nil, containerName)
    if err == nil {
        log.Printf("success create container:%s\n", cont.ID)
    } else {
        log.Println("failed to create container!!!!!!!!!!!!!")
    }
}

//啟動容器
func startContainer(ctx context.Context, containerID string, cli *client.Client) error {
    err := cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
    if err == nil {
        log.Printf("success start container:%s\n", containerID)
    } else {
        log.Printf("failed to start container:%s!!!!!!!!!!!!!\n", containerID)
    }
    return err
}

//將容器的標準輸出輸出到控制臺中
func printConsole(ctx context.Context, cli *client.Client, id string) {
    //將容器的標準輸出顯示出來
    out, err := cli.ContainerLogs(ctx, id, types.ContainerLogsOptions{ShowStdout: true})
    if err != nil {
        panic(err)
    }
    io.Copy(os.Stdout, out)

    //容器內部的運行狀態
    status, err := cli.ContainerStats(ctx, id, true)
    if err != nil {
        panic(err)
    }
    io.Copy(os.Stdout, status.Body)
}

//檢查容器是否存在并啟動容器
func checkAndStartContainer(ctx context.Context, cli *client.Client) {
    for {
        select {
        case <-isRuning(ctx, cli):
            //該container沒有在運行
            //獲取所有的container查看該container是否存在
            contTemp := getContainer(ctx, cli, true)
            if contTemp.ID == "" {
                //該容器不存在,創建該容器
                log.Printf("the container name[%s] is not exists!!!!!!!!!!!!!\n", containerName)
                createContainer(ctx, cli)
            } else {
                //該容器存在,啟動該容器
                log.Printf("the container name[%s] is exists\n", containerName)
                startContainer(ctx, contTemp.ID, cli)
            }

        }
    }
}

//獲取container
func getContainer(ctx context.Context, cli *client.Client, all bool) types.Container {
    containerList, err := cli.ContainerList(ctx, types.ContainerListOptions{All: all})
    if err != nil {
        panic(err)
    }
    var contTemp types.Container
    //找出名為“mygin-latest”的container并將其存入contTemp中
    for _, v1 := range containerList {
        for _, v2 := range v1.Names {
            if v2 == indexName {
                contTemp = v1
                break
            }
        }
    }
    return contTemp
}

//容器是否正在運行
func isRuning(ctx context.Context, cli *client.Client) <-chan bool {
    isRun := make(chan bool)
    var timer *time.Ticker
    go func(ctx context.Context, cli *client.Client) {
        for {
            //每n s檢查一次容器是否運行

            timer = time.NewTicker(time.Duration(n) * time.Second)
            select {
            case <-timer.C:
                //獲取正在運行的container list
                log.Printf("%s is checking the container[%s]is Runing??", os.Args[0], containerName)
                contTemp := getContainer(ctx, cli, false)
                if contTemp.ID == "" {
                    log.Print(":NO")
                    //說明container沒有運行
                    isRun <- true
                } else {
                    log.Print(":YES")
                    //說明該container正在運行
                    go printConsole(ctx, cli, contTemp.ID)
                }
            }

        }
    }(ctx, cli)
    return isRun
}

說明:

  • const中的變量按自己的需求定制
  • 該程序名稱叫做containerDeamon,運行時前面加上sudo,否則會提示權限不夠
  • 運行成功之后控制臺會打印很多日志,此時可以注釋掉isRuning函數中的 go printConsole()函數重新編譯運行,此時的日志更方便于閱讀

現在沒有名為mygin-latest的docker容器,啟動containerDeamon守護進程之后看看控制臺打印了什么

2017/11/18 00:41:16 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:16 :NO
2017/11/18 00:41:16 the container name[mygin-latest] is not exists!!!!!!!!!!!!!
2017/11/18 00:41:16 success create container:e1d91ca1cc2adf84675c9bb90854e2ce709617088a6f7090127090ad4230fcf8
2017/11/18 00:41:21 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:21 :NO
2017/11/18 00:41:21 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:21 :NO
2017/11/18 00:41:21 the container name[mygin-latest] is exists
2017/11/18 00:41:22 success start container:e1d91ca1cc2adf84675c9bb90854e2ce709617088a6f7090127090ad4230fcf8
2017/11/18 00:41:26 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:26 :YES
2017/11/18 00:41:27 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:27 :YES

此時用命令sudo docker ps查看正在運行的docker容器

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
e1d91ca1cc2a        my-gin:latest       "./ginDocker2"      18 seconds ago      Up 12 seconds       0.0.0.0:7070->7070/tcp   mygin-latest

用docker stop e1d91ca1cc2a停掉該容器之后會在containerDeamon的控制臺看到如下輸出

2017/11/18 00:41:41 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:41 :YES
2017/11/18 00:41:42 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:42 :NO
2017/11/18 00:41:42 the container name[mygin-latest] is exists
2017/11/18 00:41:43 success start container:e1d91ca1cc2adf84675c9bb90854e2ce709617088a6f7090127090ad4230fcf8
2017/11/18 00:41:46 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:46 :YES
2017/11/18 00:41:47 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:47 :YES
2017/11/18 00:41:48 ./containerDeamon is checking the container[mygin-latest]is Runing??
2017/11/18 00:41:48 :YES

此時再用命令sudo docker ps查看正在運行的docker容器,發現該容器已被啟動。

最后,讓我們在瀏覽器訪問localhost:7070/hello/初級賽亞人看看容器的程序啟用的端口是否成功映射到了宿主機的7070端口
截圖

hello初級賽亞人.png

至此,一個docker container的守護程序就完成了,如果你對上面的代碼有任何疑問歡迎提問,覺得不好的地方請斧正。
關注喜歡隨便點,看看也行——支持是對我的最大鼓勵(初級賽亞人)。
——待更——
最近對這個守護程序,忽然發現有一種情況被忽略了,就是在檢查容器是否在運行時,如果容器已在運行,而容器中的go程序并沒有在運行,所以會導致雖然容器在運行,但是我們的服務并沒有發發布,之后會抽個時間將這種情況補上。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容