# Docker Run的背后流程

如果你想启动一个nginx容器，你可能会在命令行输入以下命令
```
➜  ~ docker run -d nginx
```
然后满怀期望等待奇迹发生，结果看到输出
```
f021105c2da60f1f3930070e917668c7ffcabc45e2cf64c622a2d68f216d0c2b
```
注意

>-d, --detach                         Run container in background and print container ID

然后我们发现系统里面多了一个nginx容器，这个过程的背后究竟发生了什么？我们来一探究竟

## docker
>docker - Docker image and container command line interface

docker是镜像和命令行接口

docker本身其实是一个C/S模型的架构

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1651764123297/YPZJLVEss.png align="left")
[image.png](https://upload-images.jianshu.io/upload_images/44480-fd0680205782fbd9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

它主要由三部分组成

- 一个Server服务端作为后台守护进程
- 跟Server端通信的REST API
- 一个Client客户端工具也就是我们上面用到的`docker`

`run`这个命令是告诉`docker`: 我要新创建一个容器, `nginx`代表这个容器的启动镜像

### 第一个接口请求
```
HEAD /_ping HTTP/1.1
Host: docker
User-Agent: Docker-Client/19.03.4 (darwin)
```
请求响应
```
HTTP/1.1 200 OK
Api-Version: 1.40
Cache-Control: no-cache, no-store, must-revalidate
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Date: Fri, 08 Nov 2019 05:40:58 GMT
Docker-Experimental: false
Ostype: linux
Pragma: no-cache
Server: Do
```
从功能上来讲，是一个探活接口，看看服务端是否正在运行

### 创建镜像
服务端运行正常，接下来可以发起创建镜像的请求
```
POST /v1.40/containers/create HTTP/1.1
Host: docker
User-Agent: Docker-Client/19.03.4 (darwin)
Content-Length: 1514
Content-Type: application/json

{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false"
...
```
事实上我是使用以下[`dtruss`](https://opensource.apple.com/source/dtrace/dtrace-147/DTTk/dtruss.auto.html)命令来跟踪docker的执行进程
```
dtruss -saL docker run -d nginx 2> ~/Desktop/dockerrun.log
```

是的，没错，就是大名鼎鼎的[DTrace](https://en.wikipedia.org/wiki/DTrace)的Mac版，（但是打印出来的POST请求内容不全，目前还不知道如何完整打印，猜测应该是参数过多，尴尬。）
```
HTTP/1.1 404 Not Found
Api-Version: 1.40
Content-Length: 42
Content-Type: application/json
Date: Fri, 08 Nov 2019 05:40:58 GMT
Docker-Experimental: false
Ostype: linux
Server: Docker/19.03.4 (linux)

{"message":"No such image: nginx:latest"}
```
从响应结果看客户端向服务器查询是否有`nginx:latest`的镜像存在，然而并没有

### 系统信息
接下来有一次系统信息的接口调用
```
GET /v1.40/info HTTP/1.1
Host: docker
User-Agent: Docker-Client/19.03.4 (darwin)
```
响应如下
```
HTTP/1.1 200 OK
Api-Version: 1.40
Content-Type: application/json
Date: Fri, 08 Nov 2019 05:40:58 GMT
Docker-Experimental: false
Ostype: linux
Server: Docker/19.03.4 (linux)
Transfer-Encoding: chunked

914
{"ID":"A2GJ:TQBK:V4H5:GFXT:KGB7:QRV5:GWJ7
...
```
这一步是获取系统信息，比如docker镜像的根目录这些，为镜像下载做准备（虽然多数情况下docker的C/S是在一台机器上，但是这种设计本身就具有比较好的扩展性，C、S两端可以分离，这跟我们本地使用MySQL的客户端服务端都是在本地一样）

### 镜像下载
```
HTTP/1.1 200 OK
Api-Version: 1.40
Content-Type: application/json
Date: Fri, 08 Nov 2019 05:41:02 GMT
Docker-Experimental: false
Ostype: linux
Server: Docker/19.03.4 (linux)
Transfer-Encoding: chunked

37
{"status":"Pulling from library/nginx","id
...
```
当然这之前自然少不了读取本地docker的配置文件，获取镜像仓库地址（默认为：https://index.docker.io/v1/），然后开始下载，直到下载完成
再次发起创建容器请求，比较奇怪的是只拿到响应信息，尴尬了

### 再次创建容器
```
POST /v1.40/containers/create HTTP/1.1
Host: docker
User-Agent: Docker-Client/19.03.4 (darwin)
Content-Length: 1514
Content-Type: application/json

{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false
...
```
镜像已存在
```
HTTP/1.1 201 Created
Api-Version: 1.40
Content-Length: 88
Content-Type: application/json
Date: Fri, 08 Nov 2019 05:41:03 GMT
Docker-Experimental: false
Ostype: linux
Server: Docker/19.03.4 (linux)
```

### 容器同名ID检查
接下来这一步比较奇怪
```
POST /v1.40/containers/f021105c2da60f1f3930070e917668c7ffcabc45e2cf64c622a2d68f216d0c2b/wait?condition=next-exit HTTP/1.1
Host: docker
User-Agent: Docker-Client/19.03.4 (darwin)
Content-Length: 0
Content-Type: text/plain
```
这个接口的功能是等待`id`为`f021105c2da60f1f3930070e917668c7ffcabc45e2cf64c622a2d68f216d0c2b`的容器退出
这一步应该是为了避免重复创建同名的容器
容器的ID生成代码[`GenerateRandomID`](https://github.com/moby/moby/blob/e511b3be894465d81c21cab35b4f292d9250a4ca/pkg/stringid/stringid.go#L40:6)的实现如下，是一段64位的随机字符串，有可能会重复？
```
func GenerateRandomID() string {
	b := make([]byte, 32)
	for {
		if _, err := rand.Read(b); err != nil {
			panic(err) // This shouldn't happen
		}
		id := hex.EncodeToString(b)
		// if we try to parse the truncated for as an int and we don't have
		// an error then the value is all numeric and causes issues when
		// used as a hostname. ref #3869
		if _, err := strconv.ParseInt(TruncateID(id), 10, 64); err == nil {
			continue
		}
		return id
	}
}
```
因为没有同名的容器，所以返回正常
```
HTTP/1.1 200 OK
Api-Version: 1.40
Content-Type: application/json
Date: Fri, 08 Nov 2019 05:41:03 GMT
Docker-Experimental: false
Ostype: linux
Server: Docker/19.03.4 (linux)
Transfer-Encoding: chunked
```
### 启动容器
此时可以启动容器了
```
POST /v1.40/containers/f021105c2da60f1f3930070e917668c7ffcabc45e2cf64c622a2d68f216d0c2b/start HTTP/1.1
Host: docker
User-Agent: Docker-Client/19.03.4 (darwin)
Content-Length: 0
Content-Type: text/plain
```
返回启动成功的响应
```
HTTP/1.1 204 No Content
Api-Version: 1.40
Date: Fri, 08 Nov 2019 05:41:04 GMT
Docker-Experimental: false
Ostype: linux
Server: Docker/19.03.4 (linux)
```
至此，容器启动完成
## 总结
以下是我整理的接口调用流程


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1651764218674/kmBsETgI9.png align="left")

可以看到，整个流程是非常简洁清晰而且严谨的，尤其是REST API的接口设计，非常规范，实在是很有借鉴意义，另外，第一次使用`dtruss`，真的是神器。

Refer
- [https://docs.docker.com/engine/docker-overview/](https://docs.docker.com/engine/docker-overview/)
- [https://docs.docker.com/engine/api/v1.24/](https://docs.docker.com/engine/api/v1.24/)
- [http://www.brendangregg.com/dtrace.html](http://www.brendangregg.com/dtrace.html)

