一、Docker基础之容器(container)创建命令的用法
docker container create --name myetcd etcd_cluster:gc4.0
create命令完成的工作,分为以下步骤:
1、 在hots主机上根据用户指定的配置生成一个container的工作目录,/var/lib/docker/containers/{id},同时把配置属性给持久化下来。
2、然后向daemon注册该container,注册之后,daemon就可以通过<container.ID>来使用该容器。
3、 并没后把该容器run起来!不涉及到底层containerd
等工具的调用。
二、创建主机容器
这是docker daemon响应docker client端命令的handler method。
1、 解析request,得到相关参数
2、调用backend.ContainerCreate(),其中backend其实就是daemon
func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
}
if err := httputils.CheckForJSON(r); err != nil {
return err
}
name := r.Form.Get("name")
//从这里可以学习go是如何把复杂的json数据转化为响应的数据结构的!
config, hostConfig, networkingConfig, err := s.decoder.DecodeConfig(r.Body)
if err != nil {
return err
}
version := httputils.VersionFromContext(ctx)
adjustCPUShares := versions.LessThan(version, "1.19")
/创建一个container
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)
}
三、创建一个容器
下面函数实现了创建一个容器
// ContainerCreate函数创建了一个定时的容器
func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
return daemon.containerCreate(params, false)
}
func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (containertypes.ContainerCreateCreatedBody, error) {
start := time.Now()
//验证HostConfig、Config、NetworkingConfig、AdjustCPUShares配置信息
if params.Config == nil {
return containertypes.ContainerCreateCreatedBody{}, fmt.Errorf("Config cannot be empty in order to create a container")
}
warnings, err := daemon.verifyContainerSettings(params.HostConfig, params.Config, false)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
}
err = daemon.verifyNetworkingConfig(params.NetworkingConfig)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
}
if params.HostConfig == nil {
params.HostConfig = &containertypes.HostConfig{}
}
err = daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
}
//调用create(daemon *Daemon)函数
*/
container, err := daemon.create(params, managed)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, daemon.imageNotExistToErrcode(err)
}
containerActions.WithValues("create").UpdateSince(start)
return containertypes.ContainerCreateCreatedBody{ID: container.ID, Warnings: warnings}, nil
}
四、create函数
create函数的执行步骤分为以下步骤:
1、获取到image
2、调用func (daemon *Daemon) newContainer 创建一个baseContainer
3、设置baseContainer的读写层、config文件
4、向daemon注册该container,注册之后,daemon就可以通过<container.ID>来使用该容器
// Create函数实现使用给定的名称从给定的配置创建一个新的容器。
func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {
//创建一个container需要三个元素
var (
container *container.Container
img *image.Image
imgID
err error
)
if params.Config.Image != "" {
img, err = daemon.GetImage(params.Config.Image) //镜像
if err != nil {
return nil, err
}
if runtime.GOOS == "solaris" && img.OS != "solaris " {
return nil, errors.New("Platform on which parent image was created is not Solaris")
}
imgID = img.ID() //镜像ID
}
if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil {
return nil, err
}
if err := daemon.mergeAndVerifyLogConfig(¶ms.HostConfig.LogConfig); err != nil {
return nil, err
}
//继续调用函数(daemon *Daemon) newContainer
if container, err = daemon.newContainer(params.Name, params.Config, params.HostConfig, imgID, managed); err != nil {
return nil, err
}
defer func() {
//创建失败的话,调用func (daemon *Daemon) cleanupContainer执行cleanup动作
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
//设置容器的可读写层layer
if err := daemon.setRWLayer(container); err != nil {
return nil, err
}
rootUID, rootGID, err := idtools.GetRootUIDGID(daemon.uidMaps, daemon.gidMaps)
if err != nil {
return nil, err
}
if err := idtools.MkdirAs(container.Root, 0700, rootUID, rootGID); err != nil {
return nil, err
}
//创建目录 /var/lib/docker/containers/{id}/checkpoints并设置ownership
if err := idtools.MkdirAs(container.CheckpointDir(), 0700, rootUID, rootGID); err != nil {
return nil, err
}
//执行到这里,/var/lib/docker/containers/{id}下仅仅创建了一个checkpoints目录。setHostConfig()设置container的一些属性(包括MountPoints),然后把容器的配置信息持久化
if err := daemon.setHostConfig(container, params.HostConfig); err != nil {
return nil, err
}
if err := daemon.createContainerPlatformSpecificSettings(container, params.Config, params.HostConfig); err != nil {
return nil, err
}
var endpointsConfigs map[string]*networktypes.EndpointSettings
if params.NetworkingConfig != nil {
endpointsConfigs = params.NetworkingConfig.EndpointsConfig
}
// 确保NetworkMode有一个可接受的值。我们这样做是为了确保API向后兼容性。
container.HostConfig = runconfig.SetDefaultNetModeIfBlank(container.HostConfig)
daemon.updateContainerNetworkSettings(container, endpointsConfigs)
if err := container.ToDisk(); err != nil {
logrus.Errorf("Error saving new container to disk: %v", err)
return nil, err
}
//向daemon注册该container。注册之后,daemon就可以通过<container.ID>来使用该容器
if err := daemon.Register(container); err != nil {
return nil, err
}
daemon.LogContainerEvent(container, "create")
return container, nil
}
五、newContainer函数
func (daemon *Daemon) newContainer(name string, config *containertypes.Config, hostConfig *containertypes.HostConfig, imgID , managed bool) (*container.Container, error) {
var (
id string
err error
noExplicitName = name == ""
)
//生成容器的ID和name
id, name, err = daemon.generateIDAndName(name)
if err != nil {
return nil, err
}
if hostConfig.NetworkMode.IsHost() {
if config.Hostname == "" {
config.Hostname, err = os.Hostname()
if err != nil {
return nil, err
}
}
} else {
daemon.generateHostname(id, config)
}
entrypoint, args := daemon.getEntrypointAndArgs(config.Entrypoint, config.Cmd)
//创建一个BaseContainer
base := daemon.newBaseContainer(id)
base.Created = time.Now().UTC()
base.Managed = managed
base.Path = entrypoint
base.Args = args //FIXME: de-duplicate from config
base.Config = config
base.HostConfig = &containertypes.HostConfig{}
base.ImageID = imgID
base.NetworkSettings = &network.Settings{IsAnonymousEndpoint: noExplicitName}
= name
base.Driver = daemon.GraphDriverName()
return base, err
}
1、BaseContainer
// NewBaseContainer使用其基本配置创建一个新容器。
func NewBaseContainer(id, root string) *Container {
return &Container{
CommonContainer: CommonContainer{
ID: id,
State: NewState(),
ExecCommands: exec.NewStore(),
Root: root,
MountPoints: make(map[string]*volume.MountPoint),
StreamConfig: stream.NewConfig(),
attachContext: &attachContext{},
},
}
}
至此,创建一个BaseContainer流程已经完成,那么后面将由daemon继续完成设置该container的属性配置、读写layer设置和注册等工作。
2、CommonContainer构造对象
// CommonContainer保存容器的字段,这些字段适用于守护进程支持的所有平台。
type CommonContainer struct {
StreamConfig *stream.Config
//集成一个可以直接支持平台的容器
*State `json:"State"` //需要驱动引擎版本号小于1.11
Root string `json:"-"` //容器的“主”路径,包括元数据。
BaseFS string `json:"-"` //graphdriver挂载点的路径
RWLayer layer.RWLayer `json:"-"`
ID string
Created time.Time
Managed bool
Path string
Args []string
Config *containertypes.Config
ImageID `json:"Image"`
NetworkSettings *network.Settings
LogPath string
Name string
Driver string
//MountLabel包含'mount'命令的选项
MountLabel string
ProcessLabel string
RestartCount int
HasBeenStartedBefore bool
HasBeenManuallyStopped bool //用于非停止重启策略
MountPoints map[string]*volume.MountPoint
HostConfig *containertypes.HostConfig `json:"-"` // 不要在json中序列化主机配置,否则我们会使容器不可移植
ExecCommands *exec.Store `json:"-"`
SecretStore agentexec.SecretGetter `json:"-"`
SecretReferences []*swarmtypes.SecretReference
//关闭日志驱动器
LogDriver logger.Logger `json:"-"`
LogCopier *logger.Copier `json:"-"`
restartManager restartmanager.RestartManager
attachContext *attachContext
}
3、创建一个container的RWLayer
func (daemon *Daemon) setRWLayer(container *container.Container) error {
var layerID layer.ChainID
if container.ImageID != "" {
img, err := daemon.imageStore.Get(container.ImageID)
if err != nil {
return err
}
//根据img.RootFS中记录的diffID计算出该image的最后一个chainID
layerID = img.RootFS.ChainID()
}
//创建该container的RWLayer
rwLayer, err := daemon.layerStore.CreateRWLayer(container.ID, layerID, container.MountLabel, daemon.getLayerInit(), container.HostConfig.StorageOpt)
if err != nil {
return err
}
container.RWLayer = rwLayer
return nil
}
4、CreateRWLayer()函数
func (ls *layerStore) CreateRWLayer(name string, parent ChainID, mountLabel string, initFunc MountInit, storageOpt map[string]string) (RWLayer, error) {
ls.mountL.Lock()
defer ls.mountL.Unlock()
m, ok := ls.mounts[name]
if ok {
return nil, ErrMountNameConflict
}
var err error
var pid string
var p *roLayer
if string(parent) != "" {
p = ls.get(parent)
if p == nil {
return nil, ErrLayerDoesNotExist
}
pid = p.cacheID
// Release parent chain if error
defer func() {
if err != nil {
ls.layerL.Lock()
ls.releaseLayer(p)
ls.layerL.Unlock()
}
}()
}
m = &mountedLayer{
name: name, //容器编号
parent: p, //一个container的读写layer的parent属性是其使用的image的最后一层的ChainID
//mountID 对应的目录是 /var/docker/overlay/{mountID} 由Random随机生成的
mountID: ls.mountID(name),
layerStore: ls,
references: map[RWLayer]*referencedRWLayer{},
}
if initFunc != nil {
pid, err = ls.initMount(m.mountID, pid, mountLabel, initFunc, storageOpt)
if err != nil {
return nil, err
}
m.initID = pid
}
createOpts := &graphdriver.CreateOpts{
StorageOpt: storageOpt,
}
if err = ls.driver.CreateReadWrite(m.mountID, pid, createOpts); err != nil {
return nil, err
}
if err = ls.saveMount(m); err != nil {
return nil, err
}
return m.getReference(), nil
}
5、 mountedLayer 结构体函数
//实现了/layer/layer.go中的type RWLayer interface
type mountedLayer struct {
name string //其值一般是container.ID
mountID string //对应的目录是 /var/docker/overlay/{mountID}
initID string
parent *roLayer //一个container的读写layer的parent属性是其使用的image的最后一层的ChainID
path string
layerStore *layerStore
references map[RWLayer]*referencedRWLayer
}
6、MountPoints的设置
最后,关于该container挂载点的设置需要注意一下规则,一个容器可能会有多个MountPoints,指行步骤如下:
1、如果有的话,选择容器的先前配置的MountPoints。
2、选择从另一个容器装入的卷。 覆盖以前配置的MountPoints。
3、 选择client端设置的 bind mounts。 覆盖以前配置的MountPoints。
4、 清理即将被重新分配的旧volumes。
/* registerMountPoints使用配置的卷初始化容器挂载点并绑定挂载。
:registerMountPoints使用配置的卷初始化容器挂载点并绑定挂载。
1. 如果容器有挂载点,请选择之前配置的挂载点。
2. 选择从其他容器安装的卷。覆盖先前配置的挂载点目的地。
3.选择客户端设置的绑定挂载。覆盖先前配置的挂载点目的地。
4. 清理即将重新分配的旧卷。
*/
func (daemon *Daemon) registerMountPoints(container *container.Container, hostConfig *containertypes.HostConfig) (retErr error) {
binds := map[string]bool{}
mountPoints := map[string]*volume.MountPoint{}
defer func() {
// 在返回错误时清理容器挂载点
if retErr != nil {
for _, m := range mountPoints {
if m.Volume == nil {
continue
}
daemon.volumes.Dereference(m.Volume, container.ID)
}
}
}()
// 1. 读取已配置的挂载点。
for destination, point := range container.MountPoints {
mountPoints[destination] = point
}
// 2. 从其他容器中读取卷。
for _, v := range hostConfig.VolumesFrom {
containerID, mode, err := volume.ParseVolumesFrom(v)
if err != nil {
return err
}
c, err := daemon.GetContainer(containerID)
if err != nil {
return err
}
for _, m := range c.MountPoints {
cp := &volume.MountPoint{
Name: ,
Source: m.Source,
RW: m.RW && volume.ReadWrite(mode),
Driver: m.Driver,
Destination: m.Destination,
Propagation: m.Propagation,
Spec: m.Spec,
CopyData: false,
}
if len(cp.Source) == 0 {
v, err := daemon.volumes.GetWithRef(, cp.Driver, container.ID)
if err != nil {
return err
}
cp.Volume = v
}
mountPoints[cp.Destination] = cp
}
}
//3.读取绑定安装
for _, b := range hostConfig.Binds {
bind, err := volume.ParseMountRaw(b, hostConfig.VolumeDriver)
if err != nil {
return err
}
_, tmpfsExists := hostConfig.Tmpfs[bind.Destination]
if binds[bind.Destination] || tmpfsExists {
return fmt.Errorf("Duplicate mount point '%s'", bind.Destination)
}
if bind.Type == mounttypes.TypeVolume {
//创建卷
v, err := daemon.volumes.CreateWithRef(bind.Name, bind.Driver, container.ID, nil, nil)
if err != nil {
return err
}
bind.Volume = v
bind.Source = v.Path()
//Name是一个已经存在的卷,我们需要在这里使用它
bind.Driver = v.DriverName()
if bind.Driver == volume.DefaultDriverName {
setBindModeIfNull(bind)
}
}
binds[bind.Destination] = true
mountPoints[bind.Destination] = bind
}
for _, cfg := range hostConfig.Mounts {
mp, err := volume.ParseMountSpec(cfg)
if err != nil {
return dockererrors.NewBadRequestError(err)
}
if binds[mp.Destination] {
return fmt.Errorf("Duplicate mount point '%s'", cfg.Target)
}
if mp.Type == mounttypes.TypeVolume {
var v volume.Volume
if cfg.VolumeOptions != nil {
var driverOpts map[string]string
if cfg.VolumeOptions.DriverConfig != nil {
driverOpts = cfg.VolumeOptions.DriverConfig.Options
}
v, err = daemon.volumes.CreateWithRef(, mp.Driver, container.ID, driverOpts, cfg.VolumeOptions.Labels)
} else {
v, err = daemon.volumes.CreateWithRef(, mp.Driver, container.ID, nil, nil)
}
if err != nil {
return err
}
if err := label.Relabel(mp.Source, container.MountLabel, false); err != nil {
return err
}
mp.Volume = v
= ()
mp.Driver = v.DriverName()
//在这里只使用缓存的路径,因为现在不需要获取路径,并且调用path()可能会很慢
if cv, ok := v.(interface {
CachedPath() string
}); ok {
mp.Source = cv.CachedPath()
}
}
binds[mp.Destination] = true
mountPoints[mp.Destination] = mp
}
container.Lock()
//4. 清理即将重新分配的旧卷。
for _, m := range mountPoints {
if m.BackwardsCompatible() {
if mp, exists := container.MountPoints[m.Destination]; exists && mp.Volume != nil {
daemon.volumes.Dereference(mp.Volume, container.ID)
}
}
}
container.MountPoints = mountPoints
container.Unlock()
return nil
}