1。概要:
1.1環境
バージョン情報は次のとおりです。a
。オペレーティングシステム:centos 7.6、amd64
b、サーバーdockerバージョン:v18.09.2
c、dockerストレージドライバー:overlay2
2ソースコードの簡単な分析:
ユーザーdockerが実行されると、クライアントは最大で3つのdockerデーモンへのリモート呼び出しを開始します。これは、イメージの作成(プル)、コンテナーの作成、およびコンテナーの開始です。この記事では、サーバー側でコンテナーを作成するプロセスを分析します。
2.1サーバー側の登録ルートinitRoutes()
func (r *containerRouter) initRoutes() {
r.routes = []router.Route{
/*
其他接口
*/
router.NewPostRoute("/containers/create", r.postContainersCreate),
/*
其他接口
*/
}
}
2.1 postContainersCreate(...)メソッド
func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
/*
检查请求对象的数据,对输入数据进行校验是非常有必要的
*/
// 获取容器名
name := r.Form.Get("name")
// 从http body中解析出几个对象
// networkingConfig这个map一般为空map,因为一般不在命令行中设置容器ip等网络信息。
// config是容器的配置,包括镜像、容器名称、环境变量、启动命令entrypoint、是否挂载终端、容器端口映射等。
// hostConfig是主机相关的配置,包括挂载目录映射关系、网络模式、重启策略、cgroup设置,是否privileged、DNS设置、日志配置等。
config, hostConfig, networkingConfig, err := s.decoder.DecodeConfig(r.Body)
if err != nil {
return err
}
// 获取api版本
version := httputils.VersionFromContext(ctx)
adjustCPUShares := versions.LessThan(version, "1.19")
// When using API 1.24 and under, the client is responsible for removing the container
if hostConfig != nil && versions.LessThan(version, "1.25") {
hostConfig.AutoRemove = false
}
// 创建容器
ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
Name: name,
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
AdjustCPUShares: adjustCPUShares,
})
if err != nil {
return err
}
// 创建容器成功,给客户端返回响应
return httputils.WriteJSON(w, http.StatusCreated, ccr)
}
s.backendの実装は次のとおりです。
// daemon/create.go文件
// 创建一个普通容器
func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
return daemon.containerCreate(params, false)
}
2.2コンテナ構成オブジェクト
configは、イメージ、コンテナ名、環境変数、起動コマンドエントリポイント、端末をマウントするかどうか、コンテナポートマッピングなどを含むコンテナの構成です。
2.3コンテナhostConfigオブジェクト
hostConfigは、マウントディレクトリマッピング、ネットワークモード、再起動戦略、cgroup、特権があるかどうか、DNS設定、ログ構成、OOMスコア調整などを含むホストレベルの構成です。
2.4 containerCreate(...)メソッド
入力パラメーターを確認し、最後にdaemon.create(...)を呼び出してコンテナーを作成します。
// daemon/create.go文件
func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (containertypes.ContainerCreateCreatedBody, error) {
start := time.Now()
// 校验1,客户端的参数是不合法的
if params.Config == nil {
return containertypes.ContainerCreateCreatedBody{}, errdefs.InvalidParameter(errors.New("Config cannot be empty in order to create a container"))
}
os := runtime.GOOS
if params.Config.Image != "" {
// 获取镜像
img, err := daemon.imageService.GetImage(params.Config.Image)
if err == nil {
os = img.OS
}
} else {
// This mean scratch. On Windows, we can safely assume that this is a linux
// container. On other platforms, it's the host OS (which it already is)
if runtime.GOOS == "windows" && system.LCOWSupported() {
os = "linux"
}
}
// 校验2,客户端的参数是不合法的
warnings, err := daemon.verifyContainerSettings(os, params.HostConfig, params.Config, false)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
}
// 校验3,客户端的参数是不合法的
err = verifyNetworkingConfig(params.NetworkingConfig)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
}
// 如果需要,稍微调整一下params对象的内容
if params.HostConfig == nil {
params.HostConfig = &containertypes.HostConfig{}
}
err = daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
}
// 调用核心方法创建容器对象
// 在内存中创建了container对象,并在宿主机上创建一些目录和文件。
container, err := daemon.create(params, managed)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
}
// prometheus指标
containerActions.WithValues("create").UpdateSince(start)
// 创建成功,将容器ID返回
return containertypes.ContainerCreateCreatedBody{ID: container.ID, Warnings: warnings}, nil
}
2.5daemon.create(…)メソッド
実際にコンテナを作成する方法。ビジネスロジックは主に、メモリ内にコンテナオブジェクトを作成し、インデックスを作成し、ホスト上にいくつかのディレクトリとファイルを作成することです。これらのディレクトリとファイルには、次のものが含まれます
。1)/ var / lib / docker / overlay2 / {ID} /ディレクトリの下のサブディレクトリ(diff、work)とファイル(link以下)。
2)/ var / lib / docker / image / overlay2 / layerdb / mounts / <container ID> / {init-id、mount-id、parent}ファイル
3)/ var / lib / docker / containers / <containerID>ディレクトリconfig.v2.jsonおよびhostconfig.jsonの下にテキストファイルを作成します
// daemon/create.go文件
func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {
var (
container *container.Container
img *image.Image
imgID image.ID
err error
)
os := runtime.GOOS
if params.Config.Image != "" {
img, err = daemon.imageService.GetImage(params.Config.Image)
if err != nil {
return nil, err
}
if img.OS != "" {
os = img.OS
} else {
// default to the host OS except on Windows with LCOW
if runtime.GOOS == "windows" && system.LCOWSupported() {
os = "linux"
}
}
imgID = img.ID()
if runtime.GOOS == "windows" && img.OS == "linux" && !system.LCOWSupported() {
return nil, errors.New("operating system on which parent image was created is not Windows")
}
} else {
if runtime.GOOS == "windows" {
os = "linux" // 'scratch' case.
}
}
// 合并指的是:params.Config有些内容为空,则用img中的值来进行赋值
// 校验指的是:检查cmd和entrypoint是否都为空,如果都是空则返回错误
if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil {
return nil, errdefs.InvalidParameter(err)
}
// 合并指的是:container级别的日志配置项为空,则用daemon的日志配置项来进行赋值
// 校验指的是:检查日志驱动名称、日志模式,max-buffer-size等等与日志相关的配置项
if err := daemon.mergeAndVerifyLogConfig(¶ms.HostConfig.LogConfig); err != nil {
return nil, errdefs.InvalidParameter(err)
}
// 创建一个container结构体,此时它的属性RWLayer还是为空(那应该在后续操作中会进行赋值,确实如此)。
if container, err = daemon.newContainer(params.Name, os, params.Config, params.HostConfig, imgID, managed); err != nil {
return nil, err
}
defer func() {
if retErr != nil {
if err := daemon.cleanupContainer(container, true, true); err != nil {
logrus.Errorf("failed to cleanup container on create error: %v", err)
}
}
}()
if err := daemon.setSecurityOptions(container, params.HostConfig); err != nil {
return nil, err
}
container.HostConfig.StorageOpt = params.HostConfig.StorageOpt
/*
如果是windows操作系统,进行一些操作:
if runtime.GOOS == "windows" {
修改container.HostConfig.StorageOpt
}
/*
daemon.imageService.CreateLayer(...)主要做的事情:
1)创建/var/lib/docker/overlay2/{ID-init}/目录下的子目录(diff、work)和文件(link和lower)
2)创建/var/lib/docker/overlay2/{ID}/目录下的子目录(diff、work)和文件(link和lower)
3)创建 /var/lib/docker/image/overlay2/layerdb/mounts/<容器ID>/{init-id,mount-id,parent}文件
*/
rwLayer, err := daemon.imageService.CreateLayer(container, setupInitLayer(daemon.idMapping))
if err != nil {
return nil, errdefs.System(err)
}
// container对象的属性RWLayer进行赋值
container.RWLayer = rwLayer
// rootIDs是一个结构体,里面包括了UID、GID和SID,一般这三者都是0。
rootIDs := daemon.idMapping.RootPair()
// 创建/var/lib/docker/containers/{容器ID}目录,并设置相应的用户ID、组ID、权限等属性
if err := idtools.MkdirAndChown(container.Root, 0700, rootIDs); err != nil {
return nil, err
}
// 创建/var/lib/docker/containers/{容器ID}/checkpoints目录,并设置相应的用户ID、组ID、权限等属性
if err := idtools.MkdirAndChown(container.CheckpointDir(), 0700, rootIDs); err != nil {
return nil, err
}
// daemon.setHostConfig(...)做的两件事:
// 1)设置入参container对象的属性MountPoints和属性HostConfig
// 2)在/var/lib/docker/containers/<容器ID>目录下创建文本文件:config.v2.json和hostconfig.json
if err := daemon.setHostConfig(container, params.HostConfig); err != nil {
return nil, err
}
// 挂载又卸载容器的merged目录,以及如果使用docker volume,则发生数据复制。
if err := daemon.createContainerOSSpecificSettings(container, params.Config, params.HostConfig); err != nil {
return nil, err
}
var endpointsConfigs map[string]*networktypes.EndpointSettings
if params.NetworkingConfig != nil {
// params.NetworkingConfig.EndpointsConfig往往是一个空map
endpointsConfigs = params.NetworkingConfig.EndpointsConfig
}
runconfig.SetDefaultNetModeIfBlank(container.HostConfig)
// 在非用户自定义网络模式下,做的事情很简单:为container对象的属性NetworkSettings的属性Networks添加一个key,key就是"bridge"
daemon.updateContainerNetworkSettings(container, endpointsConfigs)
// container对象注册到内存中,并使用前缀树来索引
// 将此时的container对象持久化至磁盘:/var/lib/docker/containers/{容器ID}/config.v2.json
if err := daemon.Register(container); err != nil {
return nil, err
}
stateCtr.set(container.ID, "stopped")
daemon.LogContainerEvent(container, "create")
return container, nil
}
2.6createContainerOSSpecificSettings方法
マージされたディレクトリをマウントおよびマウント解除します。Dockerボリュームが使用されている場合は、データレプリケーションが発生します。
// daemon/create.go文件
func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Container, config *containertypes.Config, hostConfig *containertypes.HostConfig) error {
// merged目录是在此处创建,并进行绑定挂载
/*
mountTarget := merged目录
mountdata的内容类似: index=off,lowerdir=/var/lib/docker/overlay2/l/3EWF6KYE4B65XPHTQIH5PJAVEQ:/var/lib/docker/overlay2/l/3SIMO6NI4MYP7HAZCQHFISBBV3,upperdir=/var/lib/docker/overlay2/9b30aee99a63b6f5b06a13fdbe78970ac034f5a6e292fdc62620d669cd9715dd/diff,workdir=/var/lib/docker/overlay2/9b30aee99a63b6f5b06a13fdbe78970ac034f5a6e292fdc62620d669cd9715dd/work
mount("overlay", mountTarget, "overlay", 0, mountData);
*/
if err := daemon.Mount(container); err != nil {
return err
}
// 在函数返回时卸载merged目录
defer daemon.Unmount(container)
rootIDs := daemon.idMapping.RootPair()
if err := container.SetupWorkingDirectory(rootIDs); err != nil {
return err
}
// 有默认的一些路径是masked和只读
// 例如/proc/acpi是masked path,/proc/bus是readonly path。
if hostConfig.MaskedPaths == nil && !hostConfig.Privileged {
hostConfig.MaskedPaths = oci.DefaultSpec().Linux.MaskedPaths // Set it to the default if nil
container.HostConfig.MaskedPaths = hostConfig.MaskedPaths
}
if hostConfig.ReadonlyPaths == nil && !hostConfig.Privileged {
hostConfig.ReadonlyPaths = oci.DefaultSpec().Linux.ReadonlyPaths // Set it to the default if nil
container.HostConfig.ReadonlyPaths = hostConfig.ReadonlyPaths
}
// 有机会发生数据复制
// docker run -v同时指定宿主机目录和容器目录时,不会发生数据复制,这种mountPoint对象的voloume字段是nil,因此被跳过
// docker run -v 不指定宿主目录时,就使用docker volume(即/var/lib/docker/volumes目录下的子目录),此时如果容器中的挂载点已有数据,则把容器中挂载点中的数据复制到docker volume中。
return daemon.populateVolumes(container)
}
2.7コンテナ内のマスクされたパスと読み取り専用パス
2.8 setHostConfig(…)メソッド
1)コンテナオブジェクトの属性MountPointsと属性HostConfigを設定します。
2)/ var / lib / docker / containers / <container ID>ディレクトリにテキストファイルを作成します:config.v2.jsonおよびhostconfig.json
// daemon/create.go文件
// 设置入参container对象的属性MountPoints和属性HostConfig
func (daemon *Daemon) setHostConfig(container *container.Container, hostConfig *containertypes.HostConfig) error {
/*
registerMountPoints()本质是设置入参container的属性MountPoints。
挂载点包括镜像Dockerfile中指定的挂载点、用户命令行指定的来自其他容器的volume和命令中指定的绑定挂载。
*/
if err := daemon.registerMountPoints(container, hostConfig); err != nil {
return err
}
container.Lock()
defer container.Unlock()
// 1)hostConfig使用了link机制的话,则进行相应的操作。
// 2)将hostConfig对象写到磁盘:/var/lib/docker/containers/{容器ID}/hostconfig.json
if err := daemon.registerLinks(container, hostConfig); err != nil {
return err
}
// 如果入参hostConfig的NetworkMode为"",则设置为"default"
runconfig.SetDefaultNetModeIfBlank(hostConfig)
// container对象的属性HostConfig的内容是一堆空值,因此将它直接设置为入参hostConfig
container.HostConfig = hostConfig
// 把container对象持久化到磁盘中:/var/lib/docker/containers/{容器ID}/config.v2.json
return container.CheckpointTo(daemon.containersReplica)
}
3要約:
コンテナを作成するプロセスは比較的単純です。本質は、メモリ内にコンテナオブジェクトを作成し、ホスト上にディレクトリとファイルを作成することと、いくつかのマウント操作と可能なデータレプリケーション(Dockerボリューム)です。