Docker架构了解

首先我们需要了解整个Docker的架构,首先我们需要知道的是,Docker是一个典型的Client-Server架构的应用,也就是说Docker是一个单机的应用,Dcoker这个应用主要包含如下几个组件:

  • Docker Daemon Server:作为守护进程存在(dockerd命令)
  • Docker Daemon REST APIDocker Daemon提供了REST API,通过REST API我们可以来指示它做什么
  • Docker Command-Line Interface:一个命令行用户界面(docker命令)

以上三个组件组成了Docker Engine,缺一不可。

Docker Architecture

如果需要阅读更多关于Docker架构相关的一些信息,阅读下官方文档 - Docker架构是很有帮助的。

Docker目录结构

我们此章需要阅读的是关于Docker Daemon的相关实现。

打开项目后后我们会发现大量的文件夹以及文件,不慌,我们只关注其中和Docker Daemon实现有关的即可。

我们先不逐个介绍每个目录的含义,我们先从最关键的cmd/dockerd/docker.go来阅读,这个go文件是整个Docker Daemon的入口文件。

从Docker Daemon启动流程开始

我们首先来看newDaemonCommand()这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmd := &cobra.Command{
Use: "dockerd [OPTIONS]",
Short: "A self-sufficient runtime for containers.",
SilenceUsage: true,
SilenceErrors: true,
Args: cli.NoArgs,
// 这里是我们需要关注的地方,在main函数中cmd.Execute()所执行的其实就是
// 这个匿名函数
RunE: func(cmd *cobra.Command, args []string) error {
opts.flags = cmd.Flags()
return runDaemon(opts)
},
DisableFlagsInUseLine: true,
Version: fmt.Sprintf("%s, build %s", dockerversion.Version, dockerversion.GitCommit),
}
cli.SetupRootCommand(cmd)

接下来我们进入runDaemon()函数来一探究竟:

1
2
3
4
5
func runDaemon(opts *daemonOptions) error {
daemonCli := NewDaemonCli()
// daemonCli.start()函数中才是真的逻辑所在
return daemonCli.start(opts)
}

这里我们首先要看下NewDaemonCli()做了些什么,进去看了一下很简单,就是返回了一个空的DaemonCli结构体的指针,那么我们来看下DaemonCli这个结构体中包含哪些字段吧:

1
2
3
4
5
6
7
8
9
type DaemonCli struct {
*config.Config // 组合config.Config结构体,Config结构体中包含了很多Docker Daemon的配置项
configFile *string // 配置文件路径
flags *pflag.FlagSet // 启动时指定的命令行参数

api *apiserver.Server // Docker Daemon REST API 服务器的结构体指针
d *daemon.Daemon // Docker Daemon结构体指针
authzMiddleware *authorization.Middleware // authzMiddleware enables to dynamically reload the authorization plugins
}

接下来我们进一步去看下daemonCli.start()函数中,都做了哪些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
stopc := make(chan bool)
defer close(stopc)

// warn from uuid package when running the daemon
uuid.Loggerf = logrus.Warnf

// 这里我们如果在启动时并未指定启动参数,这里会通过这个函数调用来设置一些参数的默认值
opts.SetDefaultOptions(opts.flags)

// 使用启动参数来构建一个cli.Config结构体实例
if cli.Config, err = loadDaemonCliConfig(opts); err != nil {
return err
}
// 配置Docker Daemon日志相关配置项
if err := configureDaemonLogs(cli.Config); err != nil {
return err
}

logrus.Info("Starting up")

cli.configFile = &opts.configFile
cli.flags = opts.flags

if cli.Config.Debug {
debug.Enable()
}

if cli.Config.Experimental {
logrus.Warn("Running experimental build")
if cli.Config.IsRootless() {
logrus.Warn("Running in rootless mode. Cgroups, AppArmor, and CRIU are disabled.")
}
if rootless.RunningWithRootlessKit() {
logrus.Info("Running with RootlessKit integration")
if !cli.Config.IsRootless() {
return fmt.Errorf("rootless mode needs to be enabled for running with RootlessKit")
}
}
} else {
if cli.Config.IsRootless() {
return fmt.Errorf("rootless mode is supported only when running in experimental mode")
}
}
// return human-friendly error before creating files
if runtime.GOOS == "linux" && os.Geteuid() != 0 {
return fmt.Errorf("dockerd needs to be started with root. To see how to run dockerd in rootless mode with unprivileged user, see the documentation")
}
// 初始化LCOW特性,如果我们是在Windows环境下,此函数将进行LCOW特性初始化逻辑,Linux和Unix并不会做任何事
// 因为LCOW特性是Windows独有
system.InitLCOW(cli.Config.Experimental)

// 设置默认umask权限来避免umask权限问题
// 那么什么是umask呢?这里科普一下,在Linux和Unix环境下,我们在创建一个新的文件时都会被赋予一个默认
// 的权限,那么这个默认的权限赋予操作是谁来完成的呢?是的,就是umask。
if err := setDefaultUmask(); err != nil {
return err
}

// 在创建任何和Docker Daemon相关的文件之前,我们需要首先对Docker Daemon根元数据目录进行相关配置
// 这个目录在Rootless下是$HOME/.share/share,在非Rootless目录下是/var/lib/docker
// Create the daemon root before we create ANY other files (PID, or migrate keys)
// to ensure the appropriate ACL is set (particularly relevant on Windows)
if err := daemon.CreateDaemonRoot(cli.Config); err != nil {
return err
}

// 创建执行文件存放目录,这个目录在非Rootless下是/var/run/docker
if err := system.MkdirAll(cli.Config.ExecRoot, 0700, ""); err != nil {
return err
}

potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot}

// 创建PID文件
if cli.Pidfile != "" {
pf, err := pidfile.New(cli.Pidfile)
if err != nil {
return errors.Wrap(err, "failed to start daemon")
}
potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile)
defer func() {
if err := pf.Remove(); err != nil {
logrus.Error(err)
}
}()
}

if cli.Config.IsRootless() {
// Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR
if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil {
// StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset
logrus.WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR")
}
}

// 加载REST API服务器配置
serverConfig, err := newAPIServerConfig(cli)
if err != nil {
return errors.Wrap(err, "failed to create API server")
}
// 创建一个新的REST API服务器
cli.api = apiserver.New(serverConfig)

// 加载REST API服务器的监听器
hosts, err := loadListeners(cli, serverConfig)
if err != nil {
return errors.Wrap(err, "failed to load listeners")
}

ctx, cancel := context.WithCancel(context.Background())
waitForContainerDShutdown, err := cli.initContainerD(ctx)
if waitForContainerDShutdown != nil {
defer waitForContainerDShutdown(10 * time.Second)
}
if err != nil {
cancel()
return err
}
defer cancel()

signal.Trap(func() {
cli.stop()
<-stopc // wait for daemonCli.start() to return
}, logrus.StandardLogger())

// 在Docker Daemon配置好之前提前通知REST API服务已经在活动状态
// Notify that the API is active, but before daemon is set up.
preNotifySystem()

// 插件管理相关
pluginStore := plugin.NewStore()

if err := cli.initMiddlewares(cli.api, serverConfig, pluginStore); err != nil {
logrus.Fatalf("Error creating middlewares: %v", err)
}

// 创建Docker Daemon
d, err := daemon.NewDaemon(ctx, cli.Config, pluginStore)
if err != nil {
return errors.Wrap(err, "failed to start daemon")
}

d.StoreHosts(hosts)

// validate after NewDaemon has restored enabled plugins. Don't change order.
if err := validateAuthzPlugins(cli.Config.AuthorizationPlugins, pluginStore); err != nil {
return errors.Wrap(err, "failed to validate authorization plugin")
}

// TODO: move into startMetricsServer()
if cli.Config.MetricsAddress != "" {
if !d.HasExperimental() {
return errors.Wrap(err, "metrics-addr is only supported when experimental is enabled")
}
if err := startMetricsServer(cli.Config.MetricsAddress); err != nil {
return err
}
}

// 创建并设置Cluister
c, err := createAndStartCluster(cli, d)
if err != nil {
logrus.Fatalf("Error starting cluster component: %v", err)
}

// 重新启动所有Swarm容器
// Restart all autostart containers which has a swarm endpoint
// and is not yet running now that we have successfully
// initialized the cluster.
d.RestartSwarmContainers()

logrus.Info("Daemon has completed initialization")

cli.d = d

routerOptions, err := newRouterOptions(cli.Config, d)
if err != nil {
return err
}
routerOptions.api = cli.api
routerOptions.cluster = c

// 初始化REST API服务器的路由
initRouter(routerOptions)

go d.ProcessClusterNotifications(ctx, c.GetWatchStream())

// 配置后台协程来通过USR2信号来重新加载配置
cli.setupConfigReloadTrap()

// The serve API routine never exits unless an error occurs
// We need to start it as a goroutine and wait on it so
// daemon doesn't exit
serveAPIWait := make(chan error)
go cli.api.Wait(serveAPIWait)

// 在Docker Daemon配置并启动完毕之后,我们需要通知systemd
// after the daemon is done setting up we can notify systemd api
notifySystem()

// Daemon is fully initialized and handling API traffic
// Wait for serve API to complete
errAPI := <-serveAPIWait
c.Cleanup()

// 接收到关闭信号,关闭Docker Daemon
shutdownDaemon(d)

// 停止通知接收处理和任何后台协程
// Stop notification processing and any background processes
cancel()

if errAPI != nil {
return errors.Wrap(errAPI, "shutting down due to ServeAPI error")
}

logrus.Info("Daemon shutdown complete")
return nil
}

我们可以看到在这一步中做了非常多的工作,无外乎于加载/初始化配置、创建REST API服务以及创建并启动Docker Daemon服务。

在这里面我们需要进一步的去深究一下Docker Daemon的启动过程,这里我们需要关注此函数中所调用的函数daemon.NewDaemon(),下一篇我们将单独去看看,这个函数里究竟做了些什么。