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程序并没有在运行,所以会导致虽然容器在运行,但是我们的服务并没有发发布,之后会抽个时间将这种情况补上。

推荐阅读更多精彩内容