一、前言
为什么还学Docker的容器编排?
kubernetes几年前就是容器编排的龙头老大了,感觉上想学容器编排,是不是可以直接去学学k8s了呢?
其实我是学了一阵k8s之后折回头实践使用一下Docker容器编排的,因为在学k8s的过程中难免总是和Docker的容器编排做对比。所以不学学Docker Swarm,怎么知道K8S才是最好用、最强大的容器编排工具呢?
所以整理笔记 记录实战Docker Compose和Docker Swarm
二、Docker Compose
2.1、简介
Docker Compose 是Docker提供的定义、运行多个Docker应用容器的工具,我们通过Docker Compose定义配置好应用服务后,通过一条简单的命令就能根据这个配置创建出配置中描述的容器。
Docker Compose可以运行在生产环境、测试环境和开发环境中。
使用Docker Compose需要有这三个基础的步骤:
-
使用Dockerfile定义你的应用环境。
-
编写docker-compose.yml 定义你的服务,目的是为了让Dockerfile定义的容器可以一起运行。
version: '2.0' services: # 描述我们的应用 web: build: . ports: - "5000:5000" volumes: - .:/code - logvolume01:/var/log links: # 在启动前先启动redis - redis redis: image: redis volumes: # 持久化相关挂载卷 logvolume01: {}
-
执行
docker-compose up
命令,Compose就会开始运行你描述的整个app。
参考:官方文档:https://docs.docker.com/compose/
2.2、下载安装
# 将docker-compose下载安装到 /usr/local/bin 目录下
curl -L https://github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
# docker-compse是一个二进制可执行的文件,所以给他可执行权限
sudo chmod +x /usr/local/bin/docker-compose
2.3、小实验
在这个小实验中,我们将使用Docker Compose安装一个简单的Java Web应用。
Java Web使用SpringBoot极速构建,提供一个Restful方法如下:
每次调用它都将redis里面的key=money的值+1后返回~
编写Dockerfile,这个dockerfile可以让我们将应用都jar包打包成 docker image
编写docker compose。这个描述文件就是在告诉docker-compose按照什么都规则去启动构建容器。
将 Dockerfile、docker-compose.yml、jar包都上传到服务器上~
为什么这里写服务名,而不写ip?
因为docker容器启动后会随机分配给它ip,举个例子:比如你有两个镜像A、B,那先如果先启动A,那他的ip可能是:172.16.0.2 。再启动B他的ip就可能是 172.16.0.3。 反之:如果你先启动的是容器B,那他的ip可能是172.16.0.3,而不是我们一开始说的172.16.0.2
所以这里写服务名是一个明智的选择,因为如果我们在这里固定写一个ip的话,万一哪天这个容器挂了,被重新拉起之后他的ip变了,那我们的web应用岂不是找不到这个redis了?所以用不会服务名替换ip。很明智。
容器启动后,同属一个网络下(默认一般是桥接)的容器彼此能根据对方的name彼此ping通。
使用docker-compose时,他会为我们新创建一个虚拟网卡。app中的容器启动后都会加入这个虚拟网络中。
对这块知识有疑问,可以看看这篇笔记:https://www.cnblogs.com/ZhuChangwu/p/13689736.html
web项目的配置文件如下图:
redis的host并不是某一个ip哦,而是一个服务名~
构建启动:应用服务
这其实是一个比较激动人心的事情。可以想一下,原来学docker的时候,我们直接从docker-hub上下载镜像,然后: docker run 使用镜像。
后来我们学着通过volume将容器中应用的配置文件挂载到宿主机实现简单的定制化配置,然后docker run 启动容器
再后来我们学习了Dockerfile,自定义镜像,实现了将我们本地打包好的应用做成镜像,然后docker run 启动容器。
到现在,我们下载安装好docker后,不用手动下载任何镜像,只要编写Dockerfile描述我们打包好后到程序应该做成什么镜像,通过docker-compose.yml 描述组成app的各个容器(可以是Dockerfile描述的镜像、本地已经存在的镜像、或者远程仓库中的镜像)之间有什么依赖关系。谁先启动,谁后启动。然后执行 docker-compose up命令,一键部署app。
# 一键部署
[root@VM-0-6-centos myServer]# docker-compose up
# docker-compose为我们创建了一叫做: myserver_default的网络
Creating network "myserver_default" with the default driver
# 构建web模块时,发现他depend_on redis,所以先拉去redis镜像
Pulling redis (redis:)...
latest: Pulling from library/redis
d121f8d1c412: Pull complete
2f9874741855: Pull complete
d92da09ebfd4: Pull complete
bdfa64b72752: Pull complete
e748e6f663b9: Pull complete
eb1c8b66e2a1: Pull complete
Digest: sha256:1cfb205a988a9dae5f025c57b92e9643ec0e7ccff6e66bc639d8a5f95bba928c
Status: Downloaded newer image for redis:latest
# 开始构建docker-compose.yml中指定的web模块
Building web
Step 1/5 : FROM java:8
8: Pulling from library/java
5040bd298390: Pull complete
fce5728aad85: Pull complete
76610ec20bf5: Pull complete
60170fec2151: Pull complete
e98f73de8f0d: Pull complete
11f7af24ed9c: Pull complete
49e2d6393f32: Pull complete
bb9cdec9c7f3: Pull complete
Digest: sha256:c1ff613e8ba25833d2e1940da0940c3824f03f802c449f3d1815a66b7f8c0e9d
Status: Downloaded newer image for java:8
---> d23bdf5b1b1b
Step 2/5 : COPY *.jar /app.jar
---> 15421d081257
Step 3/5 : EXPOSE 8888
---> Running in 18f52319a82d
Removing intermediate container 18f52319a82d
---> 86cb853b5711
Step 4/5 : CMD ["--server.port=8888"]
---> Running in 147d797d5848
Removing intermediate container 147d797d5848
---> 323a85aa9c61
Step 5/5 : ENTRYPOINT ["java","-jar","/app.jar"]
---> Running in 6988142d65d4
Removing intermediate container 6988142d65d4
---> 245e7675226d
Successfully built 245e7675226d
Successfully tagged myserver_web:latest
WARNING: Image for service web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
# 为我们的webapp成功创建了两个容器
Creating myserver_redis_1 ... done
Creating myserver_web_1 ... done
Attaching to myserver_redis_1, myserver_web_1
# 先启动redis
redis_1 | 1:C 21 Sep 2020 12:27:54.827 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis_1 | 1:C 21 Sep 2020 12:27:54.827 # Redis version=6.0.8, bits=64, commit=00000000, modified=0, pid=1, just started
redis_1 | 1:C 21 Sep 2020 12:27:54.827 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis_1 | 1:M 21 Sep 2020 12:27:54.828 * Running mode=standalone, port=6379.
redis_1 | 1:M 21 Sep 2020 12:27:54.828 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1 | 1:M 21 Sep 2020 12:27:54.828 # Server initialized
redis_1 | 1:M 21 Sep 2020 12:27:54.828 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
redis_1 | 1:M 21 Sep 2020 12:27:54.828 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').
redis_1 | 1:M 21 Sep 2020 12:27:54.828 * Ready to accept connections
# 启动SpringBoot
web_1 |
web_1 | . ____ _ __ _ _
web_1 | /\ / ___'_ __ _ _(_)_ __ __ _
web_1 | ( ( )\___ | '_ | '_| | '_ / _` |
web_1 | \/ ___)| |_)| | | | | || (_| | ) ) ) )
web_1 | ' |____| .__|_| |_|_| |_\__, | / / / /
web_1 | =========|_|==============|___/=/_/_/_/
web_1 | :: Spring Boot :: (v2.3.4.RELEASE)
web_1 |
web_1 | 2020-09-21 12:27:56.777 INFO 1 --- [ main] com.changwu.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT on b66d5414a322 with PID 1 (/app.jar started by root in /)
web_1 | 2020-09-21 12:27:56.780 INFO 1 --- [ main] com.changwu.DemoApplication : No active profile set, falling back to default
服务启动后可以验证一下 docker-compose up命令是否构建起了我们的服务
停止服务:
- 在yml文件所在目录执行:
docker-compose stop
- CTRL + C
docker-compose up 运行起所有的容器后,我们得到的是一个project,注意这里的这个project依然是一个单机的应用,相对于拆分前来说,现在的project是一个容器化后的单机应用。 (Docker提供的集群化部署方案在下面的章节~)
官方Demo:https://docs.docker.com/compose/gettingstarted/
2.4、小实验的细节
docker-compose up 成功执行后,可以看到他根据我们配置文件描述,为我们自动下载了redis镜像、java镜像、已经根据Dockerfile创建镜像
查看当前正在运行的容器(通过 docker-compose up 为我们自动运行起来的容器):
正在运行的容器的命名规则:目录名_镜像名_副本数
docker-compose up 命令还为我们创建了一个叫做 composetest_default的网络,整个项目中的容器都加入到这个网络中,这个网络支持我们使用 服务名访问到容器。是实现负载均衡的前提。
查看网络的详情:下面的两个容器在同一个网络下,
在同一个网络下的容器,彼此是可以通过对方的服务名访问到对方,如下:
2.5、Compose file的编写规则
参考:https://docs.docker.com/compose/compose-file/
三、Docker Swarm
3.1、简介
Docker Engine 1.12诞生了Swarm。Docker的Swarm让我们可以通过一个或者多个Docker Engine组建起一个集群,没错Swarm就是Docker公司退出的集群编排工具。
使用docker swarm构建起的集群架构如下图:
主要存在两种角色:Manager节点和Worker节点,Manager和Woker本质上也都是Docker容器。
什么是Node?
大家都在说一个集群由多个节点组成,这个Node究竟是什么呢?
其实可以把这个节点理解成下载有Docker软件的服务器,通过Docker Swarm 会将我们启动的容器运行在某个Node中的Docker里面。
然后我感觉也可以把Node直接理解成某个服务器上运行的Docker实例。意思是你可以在一个服务器上启动多个Docker软件。当然这其实就和服务容器化以及分布式部署追求的那种容错性有出入~,毕竟都放在一个服务器上,万一服务器挂了,所有Docker实例,所有容器也都挂了。
Manager:
Manager主要掌控集群的管理任务
- 维持集群的状态
- 服务的调度(所谓调度就是通过一定的算法,让容器在合适的Node上启动起来)
manager之间彼此通信,我们针对整个集群的操作都要通过manager节点下发。
Worker:
worker节点是swarm集群中普通节点,他们会被Docker Swarm调度Woker节点上面运行起用户指定的容器。
参考:https://docs.docker.com/engine/swarm/
3.2、注意点
我们使用Docker Swarm做了什么?
不要忽略一件事,使用docker swarm 为我们提供的命令,归根结底是为了搭建起一个swarm 集群,往这个集群中添加Node,从这个集群中砍掉Node。
搭建起集群之后下一步才是使用集群,使用集群使用的是另一套命令: docker service
3.3、环境搭建
初始环境
查看docker swarm 命令:
初始化一个Docker Swarm 集群
通过 --advertise-addr 指定的ip可以是公网ip,也可以是私网ip。
如何将一个普通Node加入到一个集群中?
然后当我们成功初始化一个集群时,他会提示我们一条docker swarm join --token命令,通过这个命令我们可以让一个Node加入到以当前节点为Manager的去群中。
也可以该命令生成加入令牌:docker swarm join-token worker
如何查看当前集群各个节点的状态?
在manager节点执行如下命令:
在普通节点执行 docker node ls 会报错
如何将一个Manager Node加入到一个集群中?
使用该命令获取加入令牌:docker swarm join-token manager
该命令只能从manager节点使用。
创建swarm集群后,swarm为我们自动创建了新的网络~
3.4、Raft一致性协议
manager节点组成的集群使用了Raft算法,要求集群中多数以上的节点存活,集群才可以使用。他的本意是想让集群中存在3台及以上的manager,这样即使有manager挂了,整个集群依然是不影响使用的。
因此:如果你只有一个mananger节点,那集群就是不可用,你也不能通过manager下发任何任务。如果你有三台manager,即使挂了一个manager,因为还有半数以上的manager存在,集群依然可用。如果你说我的集群中就有两个manager,可不可用呢?答案是:可用,这种情况和集群中有三个manager,然后挂了一个manager一样,但是没意义。为啥呢?因为他根本没有任何容错性。再挂一个manager,集群就不可用了。
下一篇笔记,就好好研究一下Reft协议
3.5、弹性扩容、缩容
创建服务的命令:
通过docker service启动一个服务
只有在manager节点上才能使用这个命令。
docker run
和这个docker service
挺相似的,但是通过docker service
启动容器具有扩容、缩容的能力
查看启动的容器
思考:
我们在集群中的manager节点通过上面的命令启动一个服务。这其实就是Docker中容器的调度,或者也能说它是简单的编排。docker service create 命令本意上是在集群中启动一个nginx服务。这个nginx服务也就是一个docker容器,这个docker容器,会被manager随机在四个Node中的某一个Node上启动起来!
具体是哪个Node? 可以登陆上这几个服务器,通过 docker ps 查
动态扩容:
如下,增加我们的nginx的容器的副本数
对于WebServer来无论它访问哪台服务器的ip都能访问到nginx,而且无论这台服务器上的Docker中是否运行中nginx服务。docker swarm对内部的容器运行做了一层屏障,让很多服务以一个整体的形式存在。
理解:服务的概念
这里只是在表象上理解一下服务的概念~不涉及底层实现,但是有助于捋清思路。
我们在上面通过 docker service 命令启动一个服务,什么服务呢,通过--name指定一个叫做 mynginx的服务 ,服务中运行的镜像为nginx,如果本地没有这个镜像,他会去远程拉取。服务启动后,docker swarm会在集群中随机找一个合适的Node运行Redis容器,这个Redis容器属于mynginx服务。
后来我们又用 docker service update 增加服务的副本数,就比如我们上面将服务的副本数增加到3,docker swarm就会让整个集群中再找合适的Node,然后启动新的service。
如下图,WebServer去连接Redis服务,这个Redis服务实际上就是存在于由Docker Swarm搭建起的集群中的Redis服务,并且它可能有多个副本。
对于WebServer来说,他不知道DockerSwarm集群中有多少服务的副本,对他来说,它只是在连接一个Redis实例,而不知道它连接的实际上是一个多分片集群。
Docker Swarm集群为WebServer提供Redis服务使用,WebServer可以认为Swarm集群是一个实例,然后WebServer只需要使用Swarm集群为他提供的Redis服务,而不关心Swarm集群中有多少个服务副本。
动态缩容:
动态缩容,就是通过 --replicas 指定一个比原来副本数小的数量即可
动态扩缩容的另一个命令:
移除服务:
服务被移除后,所以的服务副本都会消失,所有的相关容器也会被关闭
3.6、使用的感受
感觉上 Docker Swarm的调度设计是容器为核心的。
为啥这样说呢?
从上面的使用上来看,首先是拆解App,于是我们将应用拆分成不同容器。那不同容器之间是需要通过网络通信的,相应的DockerSwarm的实现是:Docker Swarm集群会构建一个igress网络(它是一个拥有负载均衡能力的overlay网络)所有的Node都加入到这个扁平化网络中,实现了网络中的各个Node直接通过对方的服务名就能ping通互联。
有了这样的基础后,我们就能使用Docker Swarm提供的扩容、所容的调度能力
当我们需要Docker Swarm为我们扩容时,它会根据自己的调度算法去找一个合适的Node然后去一个一个的运行起我的容器,也就是说它的思路是:为容器找一个合适的节点运行起来
spread: 默认策略,尽量均匀分布,找容器数少的结点调度
binpack: 和spread相反,尽量把一个结点占满再用其他结点
random: 随机
这种调度思想和k8s的编排思想是不一样的。
当然说起k8s难免会冒出海量的新概念,这里就不展开了。
简单来说:kubernates的编排思想是这样的,它根本不关系底层的容器是Docker还是其他的容器技术,它站在更高的角度上,允许让用户以yaml的方式去描述自己的应用,去描述哪几个镜像启动后应在绑定在一起(在一个Pod里,而不是像Docker Swarm那样,由Docker Swarm去为单个容器找一个合适的Node),允许用户指定Pod的数量,还为用户提供了 网关、监控、备份、水平扩展、滚动更新、保持指定数量的副本数、负载均衡等功能。