[转]golang 实现延迟消息原理与方法

实现延迟消息最主要的两个结构:

环形队列:通过golang中的数组实现,分成3600个slot。

任务集合:通过map[key]*Task,每个slot一个map,map的值就是我们要执行的任务。

原理图如下:

140030-20170726142613250-1793759044

实现代码如下:

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
package main;
import (
    "time"
    "errors"
    "fmt"
)
//延迟消息
type DelayMessage struct {
    //当前下标
    curIndex int;
    //环形槽
    slots [3600]map[string]*Task;
    //关闭
    closed chan bool;
    //任务关闭
    taskClose chan bool;
    //时间关闭
    timeClose chan bool;
    //启动时间
    startTime time.Time;
}
//执行的任务函数
type TaskFunc func(args ...interface{});
//任务
type Task struct {
    //循环次数
    cycleNum int;
    //执行的函数
    exec   TaskFunc;
    params []interface{};
}
//创建一个延迟消息
func NewDelayMessage() *DelayMessage {
    dm := &DelayMessage{
        curIndex:  0,
        closed:    make(chan bool),
        taskClose: make(chan bool),
        timeClose: make(chan bool),
        startTime: time.Now(),
    };
    for i := 0; i < 3600; i++ {
        dm.slots[i] = make(map[string]*Task);
    }
    return dm;
}
//启动延迟消息
func (dm *DelayMessage) Start() {
    go dm.taskLoop();
    go dm.timeLoop();
    select {
    case <-dm.closed:
        {
            dm.taskClose <- true;
            dm.timeClose <- true;
            break;
        }
    };
}
//关闭延迟消息
func (dm *DelayMessage) Close() {
    dm.closed <- true;
}
//处理每1秒的任务
func (dm *DelayMessage) taskLoop() {
    defer func() {
        fmt.Println("taskLoop exit");
    }();
    for {
        select {
        case <-dm.taskClose:
            {
                return;
            }
        default:
            {
                //取出当前的槽的任务
                tasks := dm.slots[dm.curIndex];
                if len(tasks) > 0 {
                    //遍历任务,判断任务循环次数等于0,则运行任务
                    //否则任务循环次数减1
                    for k, v := range tasks {
                        if v.cycleNum == 0 {
                            go v.exec(v.params...);
                            //删除运行过的任务
                            delete(tasks, k);
                        else {
                            v.cycleNum--;
                        }
                    }
                }
            }
        }
    }
}
//处理每1秒移动下标
func (dm *DelayMessage) timeLoop() {
    defer func() {
        fmt.Println("timeLoop exit");
    }();
    tick := time.NewTicker(time.Second);
    for {
        select {
        case <-dm.timeClose:
            {
                return;
            }
        case <-tick.C:
            {
                fmt.Println(time.Now().Format("2006-01-02 15:04:05"));
                //判断当前下标,如果等于3599则重置为0,否则加1
                if dm.curIndex == 3599 {
                    dm.curIndex = 0;
                else {
                    dm.curIndex++;
                }
            }
        }
    }
}
//添加任务
func (dm *DelayMessage) AddTask(t time.Time, key string, exec TaskFunc, params []interface{}) error {
    if dm.startTime.After(t) {
        return errors.New("时间错误");
    }
    //当前时间与指定时间相差秒数
    subSecond := t.Unix() - dm.startTime.Unix();
    //计算循环次数
    cycleNum := int(subSecond / 3600);
    //计算任务所在的slots的下标
    ix := subSecond % 3600;
    //把任务加入tasks中
    tasks := dm.slots[ix];
    if _, ok := tasks[key]; ok {
        return errors.New("该slots中已存在key为" + key + "的任务");
    }
    tasks[key] = &Task{
        cycleNum: cycleNum,
        exec:     exec,
        params:   params,
    };
    return nil;
}
func main() {
    //创建延迟消息
    dm := NewDelayMessage();
    //添加任务
    dm.AddTask(time.Now().Add(time.Second*10), "test1"func(args ...interface{}) {
        fmt.Println(args...);
    }, []interface{}{1, 2, 3});
    dm.AddTask(time.Now().Add(time.Second*10), "test2"func(args ...interface{}) {
        fmt.Println(args...);
    }, []interface{}{4, 5, 6});
    dm.AddTask(time.Now().Add(time.Second*20), "test3"func(args ...interface{}) {
        fmt.Println(args...);
    }, []interface{}{"hello""world""test"});
    dm.AddTask(time.Now().Add(time.Second*30), "test4"func(args ...interface{}) {
        sum := 0;
        for arg := range args {
            sum += arg;
        }
        fmt.Println("sum : ", sum);
    }, []interface{}{1, 2, 3});
    //40秒后关闭
    time.AfterFunc(time.Second*40, func() {
        dm.Close();
    });
    dm.Start();
}

测试结果如下:

docker安装jenkins最新版本

1.pull一个jenkins镜像 docker pull jenkins/jenkins:lts;
这个是安装最新版的jenkins,如果安装旧版本,很多插件安装不上,docker环境下升级又比较麻烦。

image.png

2.查看已经安装的jenkins镜像 docker images;

image.png

查看是否是最新版 docker inspect ba607c18aeb7

image.png

3.创建一个jenkins目录 mkdir /home/jenkins_home;
4.启动一个jenkins容器 docker run -d –name jenkins_01 -p 8081:8080 -v /home/jenkins_01:/home/jenkins_01 jenkins/jenkins:lts ;

image.png

5.查看jenkins服务 docker ps | grep jenkins;

image.png

6.启动服务端 。localhost:8081;

image.png

7.进入容器内部docker exec -it jenkins_01 bash;
8.执行:cat /var/jenkins_home/secrets/initialAdminPassword,得到密码并粘贴过去

image.png


9.输入密码之后,重启docker镜像 docker restart {CONTAINER ID},安装完毕。

image.png

作者:王大合
链接:https://www.jianshu.com/p/12c9a9654f83
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Docker初级入门教程

在上一篇文章面向后端的Docker初级入门教程:DockerFile 命令详解 中,我们比较详细的讲解了docker镜像构建脚本DockerFile的使用和命令,DockerFile的出现让构建Docker镜像的过程更加直观和高效,但是,和我之前大多数文章中所提到的那句疑问一样。

难道这些就是全部吗?

当然不是

本篇文章是Docker初级入门教程的第五篇,在前四篇,基础篇 概念篇 实战篇 以及DockerFile那篇,我们从Docker是什么,到使用DockerFile构建自己的镜像,一步一步走来,我相信完整看完这些教程的人已经对Docker有了一个比较好的了解,并可以处理一部分现实中遇到的实际问题,但是仍然还有许多问题有着更好的解决方式,本篇文章呢,我将为大家介绍一个docker自动化部署神器,docker-compose,它可以使我们将传统的那些繁琐的docker操作指令做到自动化完成,并可以控制多个容器,实现多个容器的批量启动。

不说废话,直接看东西。

docker-compose解决了什么样的问题?

如果大家之前了解过微服务架构的话,对docker-compose的自动化部署绝对是相见恨晚,这里简单提一下,微服务架构就是将传统的单一服务拆分成多个单一的小服务,从而实现了应用的横向扩展,就拿一个大的电商平台为例,微服务就是将之前一个巨大的单体应用拆分成多个服务,比如仓库系统单独出来作为一个服务,订单系统单独作为一个应用提供服务,这样带来的好处是我们不需要像传统的方式那样升级整个服务器,而只需要根据特定业务的压力情况升级对应的服务器就好,比如双十一订单系统压力比较大,我单独把订单系统的服务器升级了就好,而不需要升级整套系统的硬件配置。

但是由于微服务各个服务间存在一定的依赖关系,比如SpringCloud里面,Eureka作为注册中心,就是要先启动的,要不然后面的服务启动的时候连接不上注册中心,注册不上去可还行,而微服务落地到Docker中,大概就是下面这么个启动过程,运维人员需要依次输入很多命令来确保各个服务按照正确的顺序启动:

docker run -d 服务A
docker run -d 服务B,必须在A之后启动
复制代码

要是只有两个服务还好说,如果十个八个咬咬牙也能接受,但是几十个,几百个服务呢?按照顺序启动的话,万一哪个没整好,换来的就是运维人员的一句我操。简直就是灾难,于是docker-compose应运而生了,docker-compose和DockerFile有着异曲同工之妙,只不过DockerFile是将镜像的构建过程给封装到了脚本里,而docker-compose则是可以将镜像的运行过程封装到了特定的脚本里,这就意味这我们可以把各个容器的启动顺序整理好,写到脚本里,运维工程师每次只需要运行这个脚本就行了,完全不用依次执行run 命令启动容器了。

为什么Docker-Compose被称作大杀器,是因为它真的解决了痛点,知识点呐,朋友们,要考的。

docker-compose 安装:

关于docker-compose安装这块,网上仍然有着非常多的教程,但是无一例外三个字,太麻烦,本次依然延续传统,只说最简单的那一种,首先确保电脑上安装了python3 和 docker

不用yum?

答:这次先不用,pip安装更好用

为啥不用python2 ? 爱python2用户表示强烈谴责

答:我用pip2装了一下,死活装不上,pip3 一下子就好了,所以我推荐pip3.

注:不会装python3 的朋友,算了,我也顺便写上去吧。另外,确保你的服务器已经装好了docker。

打开linux终端,输入以下命令:

##安装python3
yum install python3

#pip方式安装docker-compose,pip会自动寻找和你主机上docker版本相匹配的docker-compose版本
pip3 install docker-compose 
复制代码

查看是否安装成功,在终端输入:

docker-compose version
复制代码

如果显示出版本的话,则代表docker-compose在我们这台机器上已经算是按照成功了。对了,我的版本是1.24.1。

docker-compose 实战:

首先新建一个文件夹,不建也行,防止一会找不到自己把yml文件放哪了,对了,docker-compose的脚本格式是yaml文件格式,不了解的朋友可以下去补补,默认文件名是docker-compose.yml或者docker-compose.yaml

在新建的文件夹里新建一个docker-compose.yml文件,输入以下内容,这里我们仍然以tomcat为例:

mytomcat:
    image: tomcat
    ports:
       - "8086:8080"
复制代码

然后我们在当前目录(记得一定要是docker-compose.yml文件所在的目录哦,docker-compose默认是从当前目录搜索的) 输入:

docker-compose up ##根据yml文件启动容器
复制代码

然后,屏幕冒出来一大堆tomcat的日志输出,ctrl+c退出的话整个容器都退出了,这是因为默认的docker-compose up命令是前台启动的,容器内的日志输出都会在前台输出,类似于docker run -it

如果想要容器从后台启动,只需要在后面加一个 -d 就行了,如下:

docker-compose up -d
复制代码

如果启动成功,会显示

[root@iZbp1d7upppth01hp demo]# docker-compose up -d
Starting demo_mytomcat_1 ... done
复制代码

此时再执行docker ps,会发现我们的tomcat已经正常启动了,名字是demo_mytomcat_1 ,分别对应文件夹,容器名,以及编号,如果再启动一次,新的tomcat容器名字就会变成demo_mytomcat_2

docker-compose构建脚本详解:

既然容器已经运行成功了,那么接下来我们便深入了解一下docker-compose.yml 文件应该遵循的格式是如何的。

首先第一层:

  • mytomcat :我们声明构建的容器的名称,一个yaml文件可以定义多个容器。

然后是:

  • image :我们构建的镜像来源,这里是tomcat镜像,如果需要指定版本,可以写成tomcat:8 这种格式

    这个时候有人可能要问了,我如果想用我自己定义的镜像怎么办?同样是可以的,只需要写成如下这种格式即可:

    mytomcat:
        bulid: . #如果是 . docker-compose 便会在当前目录找DockerFile 文件,执行构建镜像然后启动,镜像名字是  当前目录_mytomcat
        ports:
           - "8086:8080"
    复制代码
  • ports: 相当于docker run 的 -p 参数,用来映射端口。列出端口的时候可以不带引号,但是像遇到56:56这种情况的时候,YAML会把它解析为60为基数的六十进制数字,所以强烈建议大家在写的时候加上引号。

就这么点?没了?不是,同样我们可以在yml脚本里面执行诸如设置环境变量,容器卷,链接,命令等操作。

  • environment:相当于docker run 命令的 -e 参数,用来设置环境变量。
  • volumes:相当于docker rum 命令的 -v 参数,用于配置数据卷,用法如下:
    mytomcat:
        image: tomcat
        ports:
           - "8086:8080"
        volumes: 
           - ./data:/data #把当前目录下的data文件夹挂载到容器内的data文件夹中
    复制代码
  • **links:**相当于docker run 命令中的 –link 参数,用来链接两个容器,links支持链接多个容器,用法如下:
    mytomcat:
        image: tomcat
        ports:
           - "8086:8080"
        links:
           -redis #链接到redis容器
           -mysql #链接到mysql容器,如果只需要链接一个容器,删掉一个就行了
        volumes: 
           - ./data:/data #把当前目录下的data文件夹挂载到容器内的data文件夹中
    复制代码
  • command: 使用 command 可以覆盖容器启动后默认执行的命令。
  • container_name: 如果不想使用默认生成的 <项目名称><服务名称><序号> 格式名称,可以使用container_name选项来自定义容器名称。

等,当然,docker-compose支持的命令肯定不止这几个,但是上面这几个命令无一例外是我们经常会用的,至于其他的比如日志什么的,我这里就不一一列举了,需要的时候去网上搜索就可以了。

前面有提到过,一个yml脚本是可以同时定义多个容器的,如果需要定义多个容器,直接另起一行写就行了,不过,一定要注意yaml文件本身的缩进格式

mytomcat01:
    image: tomcat
    ports:
       - "8086:8080"
       
mytomcat02:
    image: tomcat
    ports:
       - "8087:8080"
复制代码

当然,这个时候可能有人还有一个疑问,yml文件必须要是docker-compose.yml这个名字吗,我要是想用另外一个名字比如 xswl.yml 怎么办,当然是可以的,只需要加上 -f 选项 然后指定 yml文件的路径就可以了。

docker-compose -f xswl.yml up -d
复制代码

docker-compose命令:

到这里,我们的构建脚本常见的命令已经说的差不多了,当然,包括yml文件,这些都是针对docker 容器来进行操作的,而docker-compose这个软件如docker一样本身也提供了很多的命令供我们使用:

  • up: 启动所有在compose文件中定义的容器,并且把它们的日志信息汇集到一起,通常搭配 -d 使用
  • ps: 获取由Compose管理的容器的状态信息。
  • run: 启动一个容器,并允许一个一次性的命令,被链接的容器会同时启动。
  • bulid: 重新建造由DockerFile所构建的镜像,除非镜像不存在,否则up命令不会执行构建已经存在的镜像的命令,常常在需要更新镜像时使用build这个命令。
  • logs :汇集由Compose管理的容器产生的日志信息,并以彩色输出。
  • stop: 停止容器。
  • rm: 删除已经停止的容器,记得不要忘了加上 -v 来删除任何由Docker管理的数据卷。

如果说我突然不想用docker-compose这个软件了,可以执行

docker-compose stop #停止docker-compose
复制代码

如果说我又突然想用了,可以执行:

docker-compose start 或者 docker-compose up #重启相同的容器
复制代码

至于更加细致入微的骚操作,大家可以去docker官网参观学习,那么多命令,我实在是写不完(没时间写,而且有的命令我也没见过)

作者:韩数
链接:https://juejin.im/post/5dc220126fb9a04aa660dcfb
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在上一篇文章写给后端的Docker初级入门教程:实战篇最后我们有提到用DockerFile来构建和定制属于我们自己的镜像,因为时间和篇幅问题,上一篇文章对DockerFile只做了一个简单的介绍和使用,并没有对DockerFile具体的指令进行详细的介绍和解释,本篇,作为上一篇实战篇的额外补充篇,我们将从DockerFile基础的命令入手,一步一步的去构建一个属于我们自己的镜像出来。

DockerFile介绍:

Dockerfile是由一系列命令和参数构成的脚本,一个Dockerfile里面包含了构建整个image的完整命令。Docker通过docker build执行Dockerfile中的一系列命令自动构建image。

实例:

这里我们仍然选择我们上一篇使用的在centos基础上定制我们自己的镜像为本章的代码实例,代码如下:

FROM centos  //继承至centos
ENV mypath /tmp  //设置环境变量
WORKDIR $mypath //指定工作目录

RUN yum -y install vim //执行yum命令安装vim
RUN yum -y install net-tools //执行yum命令安装net-tools

EXPOSE 80 //对外默认暴露的端口是80
CMD /bin/bash //CMD 容器启动命令,在运行容器的时候会自动执行这行命令,比如当我们 docker run -it centos 的时候,就会直接进入bash
复制代码

之后再通过docker build 命令编译该DockerFile便可以得到一个属于自己的镜像了。

然后编译该镜像
docker build -f ./DockerFile -t mycentos:1.3.
-t 新镜像名字:版本
-f 文件 -d 文件夹
复制代码

运行该镜像会发现vim和net-tools在我们新的容器中已经可以正常使用了。

接下来呢,我们将从FROM命令开始逐行介绍,最终完成对DockerFile常用命令的了解和掌握。

常用命令:

FROM命令:

既然我们是在原有的centos镜像的基础上做定制,那么我们的新镜像也一定是需要以centos这个镜像为基础的,而FROM命令则代表了这个意思,在DockerFile中,基础镜像是必须指定的,FROM指令的作用就是指定基础镜像,因此一个DockerFile中,FROM是必备的指令,而且就像java,python的import关键字一样,在DockerFile中,FROM指令必须放在第一条指令的位置

当然,这个时候可能有朋友会问了,我要是不想在其他的镜像上定制镜像怎么办呢,没问题啊,Docker 提供了scratch 这个虚拟镜像,如果你选择 FROM scratch 的话,则意味着你不以任何镜像为基础,接下来所写的指令将作为镜像的第一层开始存在,当然,在某些情况下,比如linux下静态编译的程序,运行的时候不需要操作系统提供运行时的支持,这个时候FROM scratch 是没有问题的,反而会大幅降低我们的镜像体积。

ENV指令

功能:设置环境变量

同样的,DockerFile也提供了两种格式:

  • ENV key value
  • ENV key1=value1 key2=value2

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN, 还是运行时的应用,都可以直接使用这里定义的环境变量。

可以看到我们示例中使用ENV设置mypath变量之后,在下一行WORKDIR则使用到了mypath这个变量

ENV mypath /tmp  //设置环境变量
WORKDIR $mypath //指定工作目录
复制代码

WORKDIR 指令:

功能,指定工作目录

格式为:WORKDIR 工作目录路径,如果这个目录不存在的话,WORKDIR则会帮助我们创建这个目录。

设置过工作目录之后,当我们启动容器,会直接进入该工作目录

[root@8081304919c9 tmp]#
复制代码

RUN命令:

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令也是在定制镜像时是较为常用的指令之一。

RUN命令的格式一共有两种,分别是:

  • Shell 格式

    RUN 命令,就像直接在命令行中输入命令一样,比如RUN yum -y install vim就是使用的这种格式

  • exec 格式

    RUN[“可执行文件”,”参数1″,”参数2″],感觉就像调用函数一样

就像我们在上一篇文章中说过的那样,DockerFile中每一条指令都会建立一层,比如我们上面执行过下面这条命令

RUN yum -y install vim 
复制代码

执行结束之后,则调用commit提交这一层的修改,使之构成一个新的镜像,怎么样,是不是豁然开朗了呢。

并没有

那好吧

同样的,Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以 及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

提示:

如果使用apt方式安装的话,最后不要忘记清理掉额外产生的apt缓存文件,如果不清理的话会让我们的镜像显得非常臃肿。因为DockerFile生成一层新的镜像的时候,并不会删除上一层镜像所残留的文件。

EXPOSE指令:

功能:声明端口

格式: EXPOSE 端口1 端口2

EXPOSE 指令是声明运行时容器提供服务端口,这当然只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。这样声明主要是为了方便后期我们配置端口映射。

CMD指令:

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

同样的,DockerFile也为我们提供了两种格式来使用CMD命令:

  • shell 格式:CMD 命令
  • exec 格式:CMD [“可执行文件”, “参数 1”, “参数 2″…]

示例中,我们使用的是第一种:

CMD /bin/bash
复制代码

这条指令带来的效果就是,当我们通过run -it 启动命令的时候,容器会自动执行/bin/bash,centos默认也是CMD /bin/bash,所以当我们运行centos镜像的时候,会自动进入bash环境里面。

当然,我们也可以通过运行时指定命令的方式来体换默认的命令,比如:

docker run -it centos cat /etc/os-release
复制代码

这样当我们运行镜像的时候,cat /etc/os-release就会替代默认的CMD /bin/bash 输出系统的版本信息了。

如果使用 shell 格式的话, 实际的命令会被包装为 sh -c 的参数的形式进行执行。

比如:

CMD echo $HOME
复制代码

在实际执行中,会将其变更为

CMD [ "sh", "-c", "echo $HOME" ]
复制代码

当然还有很多初学者特别容易犯的问题,就是去启动后台服务,比如:

CMD service nginx start
复制代码

这样子去用,会发现容器运行了一会就自动退出了。

所以,?????

我们之前不止一次的提醒过,容器不是虚拟机,容器就是进程,容器内的应用都应该以前台运行,而不是像虚拟机,物理机那样去运行后台服务,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

怎么理解呢?想想偶像剧,容器是女主角,主进程是男主角

你走了,我也不活了(撕心裂肺大哭),大概就是这么个意思。

正如我们前面所提出的,实际上CMD service nginx start 最终会被理解为:

CMD [ "sh", "-c", "service nginx start"]
复制代码

在这里,我们主进程实际就是sh,当我们service nginx start执行完毕之后,那么sh自然就会退出了,主进程退出,容器自然就会相应的停止。争取的做法是直接执行nginx可执行文件,并且声明以前台的形式运行:

CMD ["nginx", "-g", "daemon off;"]
复制代码

到这里,我们示例中所涉及到的命令已经讲完了,当然,这并不够,Docker中仍然有很多命令是我们使用比较频繁的,下面我们的部分作为补充,讲一下其他常用的DockerFile命令。

COPY 命令:

功能:复制文件

Docker依旧提供了两种格式供我们选择:

  • COPY [–chown=:] <源路径>… <目标路径>
  • COPY [–chown=:] [“<源路径 1>”,… “<目标路径>”]

到这里大家其实会发现,Docker提供的两种格式其实都是差不多的用法,一种类似于命令行,一种则类似于函数调用。

第一种例如(将package.json拷贝到/usr/src/app/目录下):

COPY package.json /usr/src/app/
复制代码

其次,目标路径 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径 ,工作目录可以用 WORKDIR 指令来指定,如果需要改变文件所属的用户或者用户组,可以加上–chown 选项。

需要注意的是,使用 COPY 指 令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这 个特性对于镜像定制很有用。

ADD命令:

ADD命令可以理解为COPY命令的高级版,格式和用法与COPY几乎一致,ADD在COPY的基础上增加了一些功能,比如源路径可以是一个URL链接,当你这么用的时候,Docker会尝试着先将该URL代表的文件下载下来,然后复制到目标目录上去,其他的则是在COPY的基础上增加了解压缩之类的操作,码字码的手疼,需要了解的朋友可以去官网查看相关的文档,这里我就不延申了。

VOLUME 定义匿名卷:

在上一篇中,我们有讲容器卷这个概念,为了防止运行时用户忘记 将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些 目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运 行,不会向容器存储层写入大量数据。

例如:

VOLUME /data
复制代码

运行时通过-v参数即可以覆盖默认的匿名卷设置。

USER 命令:

功能:指定当前用户

格式:USER 用户名:用户组

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户。

当然这个大前提是,你的User用户是事先存在好的。

完结撒花?

不知不觉间,Docker系列初级入门教程已经发到了第四篇,篇幅也到了一万多字,前三篇文章加起来在掘金上慢慢有了大概1500左右的阅读量,我知道这点对于很多掘金大佬来说只是微不足道的一点,但对于现阶段的我来说已经非常满足了,从来没有想到过有一天自己也可以通过分享去帮助到别人,正如我之前通过别人的技术博客学习那样。

这个系列完结了吗?我想初级篇应该是完结了,但是Nginx的初级入门教程,即将到来的Mysql,Netty等等并没有,由于目前尚未毕业,还没有接受过工作的毒打(滑稽),所以只能尽自己的能力去写一些基础的入门教程,所以完结了吗?并没有,技术之路永无止境,只要我们一直在坚持学习,我想,我们可以一直继续下去。

作者:韩数
链接:https://juejin.im/post/5dafa60ff265da5b62535bcf
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。