LiveRestore 特性从 Docker Engine 1.12 版本开始支持(不支持 Windows),在 dockerd 停止时保证已启动的 Running 容器持续运行,并在 daemon 进程启动后重新接管。

Docker LiveRestore 特性分析

本文基于 Docker Version 1.13.1 进行代码分析

Live Restore ???

该特性从 Docker Engine 1.12 版本开始支持(不支持 Windows),主要且唯一的作用就是:在 dockerd 停止时保证已启动的 Running 容器持续运行,并在 daemon 进程启动后重新接管。几个应用场景

必备条件

Docker daemon 实现

docker/daemon/config_unix.go 中提供了 --live-restore 参数并针对于 Swarm 模式的兼容性进行判断(注:LiveRestore 不能在 Swarm 模式下工作)。

func (config *Config) InstallFlags(flags *pflag.FlagSet) {
    // 其他参数
    flags.BoolVar(&config.LiveRestoreEnabled, "live-restore", false, "Enable live restore of docker when containers are still running")
}

func (config *Config) isSwarmCompatible() error {
    if config.LiveRestoreEnabled {
        return fmt.Errorf("--live-restore daemon configuration is incompatible with swarm mode")
    }
    return nil
}

随后在 docker/cmd/dockerd/daemon.go 中将该参数通过 containerd.RemoteOption 作为 containerdRemote 初始化的参数

func (cli *DaemonCli) start(opts daemonOptions) (err error) {
    // 省略无数
    // cli.getPlatformRemoteOptions() 中含有 LiveRestore 参数
    containerdRemote, err := libcontainerd.New(cli.getLibcontainerdRoot(), cli.getPlatformRemoteOptions()...)
    if err != nil {
        return err
    }
    
    // ...
    // 初始化完了总该用起来了吧
    // 拿刚才初始化了的 containerdRemote 传给 daemon.NewDaemon 来初始化一个真正的 Docker Daemon
    d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote)
    if err != nil {
        return fmt.Errorf("Error starting daemon: %v", err)
    }
}

containerd.Remote 初始化细节

初始化 containerdRemote 的时候会对所有的 Option 进行 Apply,其中 LiveRestore 的特性会加载到当前 Remote 的所有 Client 中

docker/libcontainerd/remote_unix.go

// 向 Remote 增加 Client 并使用预先初始化的 remote.LiveRestore 配置设置 Client
func (r *remote) Client(b Backend) (Client, error) {
    c := &client{
        clientCommon: clientCommon{
            backend:    b,
            containers: make(map[string]*container),
            locker:     locker.New(),
        },
        remote:        r,
        exitNotifiers: make(map[string]*exitNotifier),
        liveRestore:   r.liveRestore,
    }

    r.Lock()
    r.clients = append(r.clients, c)
    r.Unlock()
    return c, nil
}

// WithLiveRestore defines if containers are stopped on shutdown or restored.
func WithLiveRestore(v bool) RemoteOption {
    return liveRestore(v)
}

type liveRestore bool

func (l liveRestore) Apply(r Remote) error {
    if remote, ok := r.(*remote); ok {
        // 保留该配置到当前的 containerdRemote,以确保后续新增 Client 的时候能够从 Remote 中读取并设置
        remote.liveRestore = bool(l)
        for _, c := range remote.clients {
            c.liveRestore = bool(l)
        }
        return nil
    }
    return fmt.Errorf("WithLiveRestore option not supported for this remote")
}

daemon.NewDaemon 做了什么

// NewDaemon sets up everything for the daemon to be able to service
// requests from the webserver.
func NewDaemon(config *Config, registryService registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {

从这段注释来看,启动了各种各样所需要的东西让 daemon 可以开始工作,继续看这段函数

    d.nameIndex = registrar.NewRegistrar()
    d.linkIndex = newLinkIndex()
    // 这不就是刚才初始化传入的 containerdRemote 嘛
    d.containerdRemote = containerdRemote

    ...
    
    // 诶,这个操作是加了一个 Client
    d.containerd, err = containerdRemote.Client(d)
    if err != nil {
        return nil, err
    }
    
    // 好家伙,开始 restore 了恢复或者清理整个先前留下的现场
    if err := d.restore(); err != nil {
        return nil, err
    }

Restore 具体实现

daemon.restore

docker/daemon/daemon.go

func (daemon *Daemon) restore() error {
    var (
        currentDriver = daemon.GraphDriverName()
        containers    = make(map[string]*container.Container)
    )

    logrus.Info("Loading containers: start.")

    dir, err := ioutil.ReadDir(daemon.repository)
    if err != nil {
        return err
    }

    for _, v := range dir {
        id := v.Name()
        container, err := daemon.load(id)
        if err != nil {
            logrus.Errorf("Failed to load container %v: %v", id, err)
            continue
        }
        // Ignore the container if it does not support the current driver being used by the graph
        if (container.Driver == "" && currentDriver == "aufs") || container.Driver == currentDriver {
            // 设置 RWLayer
            logrus.Debugf("Loaded container %v", container.ID)

            containers[container.ID] = container
        } else {
            logrus.Debugf("Cannot load container %s because it was created with another graph driver.", container.ID)
        }
    }
    ...
    
}

首先从 dockerd 启动的参数 --graph(默认是 /var/lib/docker)所对应的目录下的 containers 目录读取当前所有的容器 ID,根据存储类型选择,忽略掉当前 daemon 设置的存储驱动不支持的容器。然后将上面筛选出的容器注册到 daemon。

接下来是重头戏,开始对容器进行恢复操作

for _, c := range containers {
    wg.Add(1)
    go func(c *container.Container) {
        defer wg.Done()
        // 对于运行中的或者被暂停的容器**异步**进行 Restore
        if c.IsRunning() || c.IsPaused() {
            // 调用 libcontainerd.Client.Restore
            if err := daemon.containerd.Restore(c.ID, c.InitializeStdio); err != nil {
                logrus.Errorf("Failed to restore %s with containerd: %s", c.ID, err)
                return
            }
        }
        if !c.IsRunning() && !c.IsPaused() {
        }
        if c.RemovalInProgress {
        }
    }
}

docker/libcontainerd/client_linix.go
func (clnt *client) Restore(containerID string, attachStdio StdioCallback, options ...CreateOption) error {
    // Synchronize with live events
    clnt.remote.Lock()
    defer clnt.remote.Unlock()
    // Check that containerd still knows this container.
    //
    // In the unlikely event that Restore for this container process
    // the its past event before the main loop, the event will be
    // processed twice. However, this is not an issue as all those
    // events will do is change the state of the container to be
    // exactly the same.
    cont, err := clnt.getContainerdContainer(containerID)
    // Get its last event
    ev, eerr := clnt.getContainerLastEvent(containerID)
    if err != nil || cont.Status == "Stopped" {
        // 针对于获取异常或者需要停止的容器,执行退出(stop)的操作
        
    }
    
    // 走到这里的都是状态 Running 或者 Paused 的容器
    
    // 如果开启了 LiveRestore 特性,那么开始 restore 这个容器
    if clnt.liveRestore {
        if err := clnt.restore(cont, ev, attachStdio, options...); err != nil {
            logrus.Errorf("libcontainerd: error restoring %s: %v", containerID, err)
        }
        return nil
    }
    
    // 否则干掉这个容器
    // 执行退出(stop)操作
}

具体到某个容器的 restore 由 containerd.Client 执行

func (clnt *client) restore(cont *containerd.Container, lastEvent *containerd.Event, attachStdio StdioCallback, options ...CreateOption) (err error) {
    clnt.lock(cont.Id)
    defer clnt.unlock(cont.Id)

    logrus.Debugf("libcontainerd: restore container %s state %s", cont.Id, cont.Status)

    containerID := cont.Id
    if _, err := clnt.getContainer(containerID); err == nil {
        return fmt.Errorf("container %s is already active", containerID)
    }

    defer func() {
        // 如果最后出错了,我们就不管这个容器了,把它从当前的 client 容器列表中移除(注:不是真的删除容器)        if err != nil {
            clnt.deleteContainer(cont.Id)
        }
    }()

    container := clnt.newContainer(cont.BundlePath, options...)
    container.systemPid = systemPid(cont)

    // 找到容器内 PID 的进程状态
    var terminal bool
    for _, p := range cont.Processes {
        if p.Pid == InitFriendlyName {
            terminal = p.Terminal
        }
    }

    // 以下创建一个 FIFO 的管道用于日志收集
    fifoCtx, cancel := context.WithCancel(context.Background())
    defer func() {
        if err != nil {
            cancel()
        }
    }()

    iopipe, err := container.openFifos(fifoCtx, terminal)
    if err != nil {
        return err
    }
    var stdinOnce sync.Once
    stdin := iopipe.Stdin
    iopipe.Stdin = ioutils.NewWriteCloserWrapper(stdin, func() error {
        var err error
        stdinOnce.Do(func() { // on error from attach we don't know if stdin was already closed
            err = stdin.Close()
        })
        return err
    })

    // 将 STD IO 转到管道中
    if err := attachStdio(*iopipe); err != nil {
        container.closeFifos(iopipe)
        return err
    }

    // 看起来都没问题了,这个容器归 Client 管了
    clnt.appendContainer(container)

    // 更新下 event,告知这容器被 Restore 了
    err = clnt.backend.StateChanged(containerID, StateInfo{
        CommonStateInfo: CommonStateInfo{
            State: StateRestore,
            Pid:   container.systemPid,
        }})

    if err != nil {
        container.closeFifos(iopipe)
        return err
    }
    
    // 发现有 event 要处理,那就改改状态保持下一致性
    if lastEvent != nil {
        // This should only be a pause or resume event
        if lastEvent.Type == StatePause || lastEvent.Type == StateResume {
            return clnt.backend.StateChanged(containerID, StateInfo{
                CommonStateInfo: CommonStateInfo{
                    State: lastEvent.Type,
                    Pid:   container.systemPid,
                }})
        }

        logrus.Warnf("libcontainerd: unexpected backlog event: %#v", lastEvent)
    }

    return nil
}

Daemon 退出时的特殊处理

在 dockerd 启动方法 docker/cmd/dockerd/daemon.go -> DaemonCli.start 最后处理了退出的场景

func (cli *DaemonCli) start(opts daemonOptions) (err error) {
    // ...
    // Wait for serve API to complete
    errAPI := <-serveAPIWait
    c.Cleanup()
    // 关闭 daemon
    shutdownDaemon(d)
    // 断开 containerd
    containerdRemote.Cleanup()
    if errAPI != nil {
        return fmt.Errorf("Shutting down due to ServeAPI error: %v", errAPI)
    }
    return nil
}


// shutdownDaemon just wraps daemon.Shutdown() to handle a timeout in case
// d.Shutdown() is waiting too long to kill container or worst it's
// blocked there
func shutdownDaemon(d *daemon.Daemon) {
    // 算算合理的超时时间
    shutdownTimeout := d.ShutdownTimeout()
    ch := make(chan struct{})
    go func() {
        // 开始真的 shutdown daemon
        d.Shutdown()
        close(ch)
    }()
    // 一坨的超时处理,无视
    if shutdownTimeout < 0 {
        <-ch
        logrus.Debug("Clean shutdown succeeded")
        return
    }
    select {
    case <-ch:
        logrus.Debug("Clean shutdown succeeded")
    case <-time.After(time.Duration(shutdownTimeout) * time.Second):
        logrus.Error("Force shutdown daemon")
    }
}



// Shutdown stops the daemon.
func (daemon *Daemon) Shutdown() error {
    daemon.shutdown = true
    // Keep mounts and networking running on daemon shutdown if
    // we are to keep containers running and restore them.

    // 针对于 LiveRestore 开启的状态,不做清理
    if daemon.configStore.LiveRestoreEnabled && daemon.containers != nil {
        // check if there are any running containers, if none we should do some cleanup
        if ls, err := daemon.Containers(&types.ContainerListOptions{}); len(ls) != 0 || err != nil {
            return nil
        }
    }
    // .....
    // 清理挂载
    // 清理网络
}

至此,dockerd 的 LiveRestore 特性相关代码已经分析完成。

隐藏点

std 日志 buffer 问题

containerd-shim 中的应用服务向 FIFO Pipe 输出日志(如果 dockerd 活着就会从这里面取出日志),buffer 的大小受限于 Pipe Size。在日志 buffer 满了之后将阻塞容器内日志输出

systemd

在设置 KillMode 为 control-group mixed 时,containerd-shim 会被 kill 掉但容器内的进程还在。在恢复启动的时旧进程才会被回收。因此这部分要确保 KillMode 设置为 process(只 kill 主进程)。

升级的时候应该怎么做?

假定在 Kubernetes 场景下

跨 Release 版本

例如从 17.03 -> 17.06,这种属于跨 Release 版本升级的操作,建议操作前先将节点置为不可调度,让上面的业务容器漂移到别的节点上。然后手动将该节点上的其他容器关闭,接着进行升级操作

小版本升级

例如从 17.03.0 -> 17.03.2,保险起见,先将节点置为不可调度,而后直接升级

参考