Skip to content

Docker LiveRestore 特性

Published: at 10:22

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,保险起见,先将节点置为不可调度,而后直接升级

参考