Docker的二次開發(fā):一個(gè)docker容器的守護(hù)程序

介紹

docker的sdk的官方介紹的樣例有g(shù)o和Python的,并包含了如下對(duì)docker二次開發(fā)的幾種簡單的實(shí)現(xiàn)

具體代碼請(qǐng)移步上述鏈接。

這篇主要講講怎樣用go對(duì)docker進(jìn)行簡單的二次開發(fā):一個(gè)docker容器的守護(hù)程序

kubernetes就是利用go對(duì)docker進(jìn)行二次開發(fā)以管理成千上萬的docker容器的成功案例

kubernetes部署需要踩很多坑,但是有時(shí)我們只需要對(duì)docker進(jìn)行一次簡單的二次開發(fā)以滿足業(yè)務(wù)的需求,如新上線一個(gè)版本,我們需要在docker容器中部署,此時(shí)就可以對(duì)docker進(jìn)行二次開發(fā)以滿足我們的需求。
需求一

  • 從git或svn上拉取最新的代碼,并將其編譯成go的二進(jìn)制可運(yùn)行文件
  • 從docker倉庫中拉需要的鏡像
  • 在鏡像的基礎(chǔ)上創(chuàng)建容器,包括配置容器的一些參數(shù)等等
  • 啟動(dòng)容器

這樣當(dāng)我們需要發(fā)布一個(gè)項(xiàng)目的新版本時(shí)直接運(yùn)行這個(gè)程序就能做到一鍵發(fā)布。一個(gè)容器運(yùn)行時(shí),就像一個(gè)操作系統(tǒng)運(yùn)行一樣,也有崩潰的時(shí)候,此時(shí)我們需要一個(gè)監(jiān)聽docker容器的健康狀況來以防一些意外
需求二

  • 監(jiān)聽docker容器運(yùn)行時(shí)的相關(guān)參數(shù)
  • 針對(duì)獲取到的參數(shù)做出相應(yīng)的處理,如mem使用打到80%時(shí)發(fā)送郵件通知小組的開發(fā)人員
  • 在docker容器崩潰時(shí)能重新啟動(dòng)該容器

假設(shè)需求

現(xiàn)在我就上面介紹的兩個(gè)需求簡單綜合一下,以完成一個(gè)自己的需求

  1. 假設(shè)本地已有我們需要的docker image
  2. 檢查docker container中是否已存在目標(biāo)容器
  3. 若有,則跳轉(zhuǎn)到第5步
  4. 若沒有,創(chuàng)建一個(gè)從container
  5. 啟動(dòng)該容器并按時(shí)檢查該container的狀態(tài)
  6. 若該container已崩潰,那么該程序能自動(dòng)重啟container

附:我們所期望的container內(nèi)部還掛在了一個(gè)宿主機(jī)的目錄

以上就是本篇文章將要實(shí)現(xiàn)的功能

正篇

SDK的安裝

go get github.com/docker/docker/client

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

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

實(shí)現(xiàn)

注:最終目的是啟動(dòng)docker容器之后還要運(yùn)行其中的ginDocker服務(wù),本篇程序?qū)崿F(xiàn)的 部分功能 和如下的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

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

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

當(dāng)創(chuàng)建一個(gè)container時(shí),顯示的給函數(shù)傳遞一個(gè)container name,那么之后我們?cè)俅芜\(yùn)行這個(gè)程序時(shí)同樣會(huì)創(chuàng)建同名的container。但是,docker中不允許存在同名的container,所以會(huì)創(chuàng)建失敗,這樣就可以在創(chuàng)建container時(shí)確認(rèn)該container是否存在,代碼如下

    imageName := "my-gin:latest"
    cont, err := cli.ContainerCreate(ctx, &container.Config{
        Image:      imageName,               //Docker基于該鏡像創(chuàng)建容器
        Tty:        true,                    //docker run 命令的-t
        OpenStdin:  true,                    //docker run命令的-i
        Cmd:        []string{"./ginDocker2"},//docker容器中執(zhí)行的命令
        WorkingDir: "/go/src/ginDocker2",    //docker容器工作目錄
        ExposedPorts: nat.PortSet{            //docker容器對(duì)外開放的端口
            "7070": struct{}{},
        },
    }, &container.HostConfig{
        PortBindings: nat.PortMap{
            "7070": []nat.PortBinding{nat.PortBinding{//docker容器映射到宿主機(jī)的端口
                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")

關(guān)于上述代碼作如下簡述:

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

啟動(dòng)容器

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

獲取容器內(nèi)部的運(yùn)行狀態(tài)

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

將status.Body輸出到標(biāo)準(zhǔn)輸出中,你會(huì)看到控制臺(tái)不斷的輸出容器的狀態(tài)參數(shù)等,你可以根據(jù)status.Body獲取你關(guān)心的一些參數(shù)

4.若該container已崩潰,那么該程序能自動(dòng)重啟container

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

        //獲取正在運(yùn)行的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為空,則該容器未運(yùn)行;反之,正在運(yùn)行
                    break
                }
            }
        }

綜合

目前,每一步最基本的做法我們已經(jīng)實(shí)現(xiàn)并貼出了代碼,接下來的工作就是將這個(gè)工作整合到一起,做一個(gè)簡單的封裝并做好流程調(diào)度即可。

ginDocker2

是我們要在docker容器中發(fā)布的一個(gè)Go項(xiàng)目
代碼如下

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")
}

注:由于在下面的這個(gè)程序中創(chuàng)建容器時(shí)沒辦法一次執(zhí)行多個(gè)cmd命令,所以這里的ginDocker2是先在外面的終端執(zhí)行g(shù)o build ginDocker2,在目錄ginDocker2下生成一個(gè)可執(zhí)行的二進(jìn)制文件ginDocker2

守護(hù)程序containerDeamon

聲明

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

代碼

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"                       //運(yùn)行的cmd命令,用于啟動(dòng)container中的程序
    workDir       string   = "/go/src/ginDocker2"                 //container工作目錄
    openPort      nat.Port = "7070"                               //container開放端口
    hostPort      string   = "7070"                               //container映射到宿主機(jī)的端口
    containerDir  string   = "/go/src/ginDocker2"                 //容器掛在目錄
    hostDir       string   = "/home/youngblood/Go/src/ginDocker2" //容器掛在到宿主機(jī)的目錄
    n             int      = 5                                    //每5s檢查一個(gè)容器是否在運(yùn)行

)

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

//創(chuàng)建容器
func createContainer(ctx context.Context, cli *client.Client) {
    //創(chuàng)建容器
    cont, err := cli.ContainerCreate(ctx, &container.Config{
        Image:      imageName,     //鏡像名稱
        Tty:        true,          //docker run命令中的-t選項(xiàng)
        OpenStdin:  true,          //docker run命令中的-i選項(xiàng)
        Cmd:        []string{cmd}, //docker 容器中執(zhí)行的命令
        WorkingDir: workDir,       //docker容器中的工作目錄
        ExposedPorts: nat.PortSet{
            openPort: struct{}{}, //docker容器對(duì)外開放的端口
        },
    }, &container.HostConfig{
        PortBindings: nat.PortMap{
            openPort: []nat.PortBinding{nat.PortBinding{
                HostIP:   "0.0.0.0", //docker容器映射的宿主機(jī)的ip
                HostPort: hostPort,  //docker 容器映射到宿主機(jī)的端口
            }},
        },
        Mounts: []mount.Mount{ //docker 容器目錄掛在到宿主機(jī)目錄
            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!!!!!!!!!!!!!")
    }
}

//啟動(dòng)容器
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
}

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

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

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

//容器是否正在運(yùn)行
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檢查一次容器是否運(yùn)行

            timer = time.NewTicker(time.Duration(n) * time.Second)
            select {
            case <-timer.C:
                //獲取正在運(yùn)行的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沒有運(yùn)行
                    isRun <- true
                } else {
                    log.Print(":YES")
                    //說明該container正在運(yùn)行
                    go printConsole(ctx, cli, contTemp.ID)
                }
            }

        }
    }(ctx, cli)
    return isRun
}

說明:

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

現(xiàn)在沒有名為mygin-latest的docker容器,啟動(dòng)containerDeamon守護(hù)進(jìn)程之后看看控制臺(tái)打印了什么

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

此時(shí)用命令sudo docker ps查看正在運(yùn)行的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停掉該容器之后會(huì)在containerDeamon的控制臺(tái)看到如下輸出

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

此時(shí)再用命令sudo docker ps查看正在運(yùn)行的docker容器,發(fā)現(xiàn)該容器已被啟動(dòng)。

最后,讓我們?cè)跒g覽器訪問localhost:7070/hello/初級(jí)賽亞人看看容器的程序啟用的端口是否成功映射到了宿主機(jī)的7070端口
截圖

hello初級(jí)賽亞人.png

至此,一個(gè)docker container的守護(hù)程序就完成了,如果你對(duì)上面的代碼有任何疑問歡迎提問,覺得不好的地方請(qǐng)斧正。
關(guān)注喜歡隨便點(diǎn),看看也行——支持是對(duì)我的最大鼓勵(lì)(初級(jí)賽亞人)。
——待更——
最近對(duì)這個(gè)守護(hù)程序,忽然發(fā)現(xiàn)有一種情況被忽略了,就是在檢查容器是否在運(yùn)行時(shí),如果容器已在運(yùn)行,而容器中的go程序并沒有在運(yùn)行,所以會(huì)導(dǎo)致雖然容器在運(yùn)行,但是我們的服務(wù)并沒有發(fā)發(fā)布,之后會(huì)抽個(gè)時(shí)間將這種情況補(bǔ)上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容