hello云胜

技术与生活

0%

Docker横空出世

2013年:Docker项目由Dotcloud公司(后更名为Docker Inc.)开源发布,最初基于Linux容器(LXC)技术,使用Go语言开发,主要在Ubuntu 12.04上实现,并迅速获得广泛关注 。

“容器”这个概念从来就不是什么新鲜的东西,也不是 Docker 公司发明的。

回到10年前,在2013年的时候,PaaS技术栈指的是以 Cloud Foundry 为代表的开源技术,当时主流的开源方案是基于OpenStack平台(iaas)部署CloudFoundry平台(paas)。

在此的方案里,CloudFoundry会将应用进行打包和分发。cf为每种主流开发语言都定义了各自的打包格式,然后传到自己的存储中,再通过调度器将应用包分发到虚拟机中运行。(是不是和现在的容器+k8s的方案有点像)

那为什么docker开源之后,直接干翻了这套技术方案呢?

关键点就是docker镜像。CloudFoundry方案中对应用打包极其繁琐且不稳定,让人抓狂。因为cf的打包只包含了代码和执行脚本,忽略了基础运行环境,就是说我打包时的环境和最终代码运行的环境并不能保证是一样的,这就导致代码在运行时可能出现各种异常问题。

Docker 镜像恰好解决了这个痛点。Docker 镜像直接把软件包需要的运行环一起打包进镜像里了(不包括操作系统内核),确保了镜像不管在那里运行,应用都是运行在我期望的操作系统环境中,和开发者的本地系统、打包的操作系统、运行的服务器系统都没有关系。

这是一种非常宝贵的能力,只要我在本地开发完成,打好镜像,测试没问题。然后将镜像发布。那么我就可以保证应用获得了一样的运行环境。

按理说docker其实正好解决了CloudFoundry的打包难题,但是docker此时并没有CloudFoundry已经建成的调度能力。CloudFoundry完全可以和Docker合作,替换调自己的打包流程,说不定现在我们学的就不是k8s,而是cf了。

很快其他更敏捷的公司跟进,推出基于Docker的容器集群管理项目。他们称自己为CaaS 即 Container-as-a-Service,以便和过时的PaaS划清界限。

Docker的进击

2014年:Docker推出了Docker Compose工具,用于定义和运行多容器Docker应用程序,这标志着Docker从单机容器管理向集群管理的转变 。

2014 年底的 DockerCon 上,Docker 公司雄心勃勃地对外发布了自家研发的“Docker 原生”容器集群管理项目 Swarm。

如果Swarm能如愿推广成功,那么Docker 公司这个小公司将正式迈入伟大公司的行列。

Docker很火,但是说到底,Docker只是一个用来打包和启停应用的小工具。一个公司想要的都是用户来上他们的平台,而不是做一个平台背后的底层工具。

所以Docker公司推出Swarm技术方案,向PaaS平台能力进化。

此时要提到另一个基础设施领域的创业公司CoreOS。CoreOS公司发现可以把容器技术集成到自己的方案中,从而优化自己的PaaS平台方案。于是积极参与到Docker开源社区,很快成了Docker项目中第二重要的力量。

(CoreOS另一个著名产品是etcd)

看到这里,你也会发现Docker公司和CoreOS公司在业务战略上有严重的重合,两家公司很快从合作转向了竞争。

2014 年底,CoreOS 公司以强烈的措辞宣布与 Docker 公司停止合作,并直接推出了自己研制的 Rocket(后来叫 rkt)容器。

Docker公司此时可谓当红炸子鸡,获得大量注资。不差钱的Docker并购了多个项目来完善自己的平台能力。迅速建立起一个非常繁荣的“Docker 生态”,此时的Docker看起来里最后的成功一步之遥。

Kubernetes 项目诞生

CoreOS 和Docker分手之后过的并不好,一方面广大开发者已经习惯了Docker及其配套技术,他推出的rkt容器完全打不开局面。

同样郁闷的还有一个老牌公司RedHat。RedHat最初也积极参与了Docker开源社区,同样是因为Docker激进的推动Docker Swarm项目与自己的战略利益冲突而退出Docker社区。RedHat此时只有OpenShift这个跟 Cloud Foundry 同时代的经典 PaaS 一张牌可以打,在和Docker Swarm的竞争中处于下风。

没想到,就在此时一个新项目的诞生拯救了CoreOS和RedHat。

Docker公司在Docker生态中的强势地位,直接影响了很多大公司的切身利益。

几家大公司开始合谋降低Docer公司的话语权,手段也很经典:成立一个中立的开放的云原生基金会。

2015 年 6 月 22 日,由 Docker 公司牵头,CoreOS、Google、RedHat 等公司共同宣布,Docker 公司将 Libcontainer 捐出,(Libcontainer是Docker公司开发的一个容器运行时库),并改名为 RunC 项目,交由一个完全中立的基金会管理,然后以 RunC 为依据,大家共同制定一套容器和镜像的标准和规范。

这套标准和规范,就是 OCI( Open Container Initiative )。

OCI 提出的目的就在于将容器运行时的实现和镜像的实现从 Docker 项目中完全剥离出来。这样各大公司可以不依赖于Docker,构建自己的平台能力。

很明显Docker公司对OCI标准的推进并不会很积极。OCI标准也没有动摇Docker在容器领域的霸主地位,因为Docker 项目已经是容器生态的事实标准,而且 Docker 社区也足够庞大。

眼看OCI并没有对Docker产生实质性的打击,各大公司又祭出第二招。

瞄准了刚推出不久的Swarm,Google、RedHat 等开源基础设施领域玩家们,共同牵头发起了一个名为 CNCF(Cloud Native Computing Foundation)的基金会。建立一个以 Kubernetes 项目为基础的开放云原生社区。

Kubernetes 项目来源于Google内部的Borg 和 Omega 系统,是Google 公司在容器化基础设施领域多年来实践经验的沉淀与升华。

Kubernetes 开源之后,RedHat 积极参与,迅速在云原生时代占得一席之地。RedHat 与 Google 联盟成立。

CNCF 社区发展迅猛,很快拿下了Prometheus这一容器监控的事实标准,又加入了Fluentd、OpenTracing等知名项目。现在cncf社区已经有接近200个项目

Cloud Native Computing Foundation (cncf.io)

image-20240830213826310

为什么k8s会击败Docker公司的swarm

一是k8s来源于Google内部使用多年的项目,其设计理念和架构都非常先进。

二是有富爸爸撑腰,人力财力支持

三更重要的是对开源社区的运作非常成功。Docker在取得容器的霸主地位之后,与开源社区渐行渐远。cncf社区则在推进其民主化架构。从 API 到容器运行时的每一层,Kubernetes 项目都为开发者暴露出了可以扩展的插件机制,鼓励用户通过代码的方式介入 Kubernetes 项目的每一个阶段。在整个容器社区中催生出了大量的、基于 Kubernetes API 和扩展接口的二次创新工作,比如:目前热度极高的微服务治理项目 Istio;被广泛采用的有状态应用部署框架 Operator;

cncf的繁荣

2016 年之后k8s得到了空前的发展,不同于之前局限于“打包、发布”这样的 PaaS 化路线,这一次容器社区的繁荣,是一次完全以 Kubernetes 项目为核心的“百家争鸣”。

相对应的就是Docker公司的落寞,之前拒绝了微软的天价收购。现在无奈豪赌失败之后选择逐步放弃开源社区而专注于自己的商业化转型。

从 2017 年开始,Docker 公司将 Docker 项目的容器运行时部分 Containerd 捐赠给 CNCF 社区

2018 年 1 月 30 日,RedHat 宣布斥资 2.5 亿美元收购 CoreOS

2022年:Kubernetes宣布从v1.20起不再支持Docker运行时,并在v1.24中完全移除,这标志着Docker在Kubernetes生态中的地位发生变化 。

回首看容器技术快速普及发展的那几年,真有种波澜壮阔之感,不只是技术的发展进步。各大公司之间的斗法也同样精彩。CNCF开源社区的运作也是漂亮。

containerd,docker-shim,runc,cri-docker

OCI

Open Container Initiative,开放容器标准

在 Linux 基金会的支持下成立,致力于围绕容器格式和运行时创建开放的行业标准

CRI

容器运行时接口,Container Runtime Interface

是由K8S发布制定的,统一了容器运行时接口的一种标准协议,

容器运行时接口(CRI)是 kubelet 和容器运行时之间通信的主要协议。

K8S最开始只支持docker作为容器运行时。之后为了和docker进行博弈,k8s搞出了cri标准接口,凡是支持CRI的容器运行时,都可以作为K8S的底层容器运行时。

图片

Docker

关于docker是啥不需要过多解释了。

在docker称为容器化的事实标准之后,docker项目本身逐渐成为了一个庞然大物。为了能够降低项目维护的成本,以及其他一些原因。

docker 开始自行拆分自己项目中的代码并形成一个个新的开源项目。

2015 年 6 月 22 日 ,在 OCI 项目启动后,Docker 公司将 Libcontainer 捐出,并改名为 RunC 项目

然后以 RunC 为依据,大家共同制定一套容器和镜像的标准和规范,这套标准和规范,就是 OCI

然后 2016 年,docker 开源并将 containerd 捐赠给了 CNCF,

containerd 几乎囊括了单机运行一个容器运行时所需要的一切:执行,分发,监控,网络,构建,日志等

docker不支持K8S的CRI标准

runC

上面说到的是docker公司将 libcontainer 的实现移动到 runC 并捐赠给了 OCI。

这样,容器社区就有了第一个 OCI Runtime 的参考实现。所以runC只是一个OCI标准的运行时的参考实现。

runC在最底层,我们用 Docker 或者 containerd 去启动容器,最后都会调用 runC 在 Linux 中把容器启动起来 ,它调用namespace、cgroup等系统接口创建容器。

containerd

containerd: 是符合OCI标准的容器运行时。从docker项目中拆分出来的。

目前主流的k8s底层运行时

containerd向上承接CRI接口,向下去调用runC

libcontainer是containerd的前身,现在不提了

所以从上往下的关系是

docker - containerd - runc - OCI格式的容器

dockershim

shim的意思是垫片。相当于一个转换层。

dockershim存在的原因在于上面提到的cri标准。k8s直接调用的是cri标准的接口。

但是Docker是Kubernetes使用的第一个容器运行时。从docker向cri标准过度的过程中,需要一个权宜之计,以实现与其他各种容器运行时的可移植性。这个权宜之计就是 dockershim。

preview

之后,从 containerd 1.0 开始,为了能够减少一层调用的开销,containerd 开发了一个新的 daemon,叫做 CRI-Containerd,直接与 containerd 通信,从而取代了 dockershim:

img

但是这仍然多了一个独立的 daemon,从 containerd 1.1 开始,社区选择在 containerd 中直接内建 CRI plugin,通过方法调用来进行交互,从而减少一层 gRPC 的开销,最终的容器启动流程如下:

img

Kubernetes 1.24 版本删除了内置的 dockershim

cri-dockerd

如上所属,docker不支持K8S的CRI标准。随着docker-shim的下线。从kubernetes 1.24开始,如果想继续使用docker的话,可以在kubelet和docker之间加上一个中间层cri-docker。

虽然 Kubernetes 已经不再包含 dockershim,但 Docker 公司却把这部分代码接管了过来,另建了一个叫 cri-dockerdhttps://github.com/mirantis/cri-dockerd)的项目,作用也是一样的

其他的容器运行时

和containerd对标的有:cri-o, kata,mcr等

cri-o也是符合cri标准的,他也是redhat的openshift选中的做生产环境的运行时

容器内存使用排查

配置国内yum源

问题

在部署docker时,遇到官方源链接不上的问题

1
2
3
4
5
# yum list docker-ce --showduplicates | sort -r
...
Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=7&arch=x86_64&repo=os&infra=stock error was
14: curl#7 - "Failed to connect to 2001:4178:5:200::10: Network is unreachable"
...

解决

切换国内镜像源。

使用yum install软件,yum获取源仓库中寻找对应的软件,并处理各种依赖问题。当时经常因为网络原因,官方的源连接不上,就会出现这个问题。

现在切换成国内清华的源。

地址: https://mirrors.cnnic.cn/

image-20200803165821156

点进去发现版本很多,查看自己的centos版本

1
2
# rpm -q centos-release
centos-release-7-8.2003.0.el7.centos.x86_64

image-20200803165907753

image-20200803170443372

点后面的小问号。

image-20200803170552047

按照使用帮助操作即可。

1,先备份
1
sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak
2,修改CentOS-Base.repo

编辑 /etc/yum.repos.d/CentOS-Base.repo 文件,

mirrorlist= 开头行前面加 # 注释掉;

并将 baseurl= 开头行取消注释(如果被注释的话),把该行内的域名(例如mirror.centos.org)替换为 mirrors.tuna.tsinghua.edu.cn

注意是https

3,更新软件包缓存
1
2
yum clean all     # 清除系统所有的yum缓存
yum makecache # 生成yum缓存

何修复容器中的 top 指令

lxcfs

lxcfs 是什么? 怎样通过 lxcfs 在容器内显示容器的 CPU、内存状态By李佶澳 (lijiaocn.com)

Kubernetes之路 2 - 利用LXCFS提升容器资源可见性-阿里云开发者社区 (aliyun.com)

容器中的top/free/df等命令,展示的状态信息是从/proc目录中的相关文件里读取出来的

1
2
3
4
5
6
/proc/cpuinfo
/proc/diskstats
/proc/meminfo
/proc/stat
/proc/swaps
/proc/uptime

比如查看cpuinfo,meminfo。都是宿主机的信息。

所以在容器内top,free命令显示的都是宿主机的状态

LXCFS简介

社区中常见的做法是利用 lxcfs来提供容器中的资源可见性。lxcfs 是一个开源的FUSE(用户态文件系统)实现来支持LXC容器,它也可以支持Docker容器。

Docker网络

核心原理

docker0网桥

veth对

1
2
docker network ls
docker network inspect xxx

自定义网络

1
docker network create 命令进行创建

docker run时通过–network指定使用自定义的网络

自定义网络的容器,可以直接使用容器名互相访问

将其他容器加入网络

1
docker network connect 网络名 容器名

网络寄生

场景:有一些镜像及其精简,连ping和ip命令都没有。这个时候我们排查问题会很麻烦。

解决方法:启动一个alpine容器。关键是启动时要指定network为container:目标容器myredis

这样在容器alpine中执行命令,排查的就是目标容器myredis的网络

1
docker run -it --network container:myredis alpine

Docker部署Mysql

1,拉取Mysql5.7的镜像

1
docker pull mysql:5.7

2,配置启动命令

创建mysql数据相关的挂载目录

1
mkdir -p  /mydata/mysql/data /mydata/mysql/log /mydata/mysql/conf

启动命令

1
2
3
4
5
6
7
8
9
docker run -p 3266:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-e LANG=C.UTF-8 \
-d mysql:5.7 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci

参数说明

  • -p 3266:3306:将容器的3306端口映射到主机的3266端口
  • -v /mydata/mysql/conf:/etc/mysql:将配置文件夹挂在到主机
  • -v /mydata/mysql/log:/var/log/mysql:将日志文件夹挂载到主机
  • -v /mydata/mysql/data:/var/lib/mysql/:将数据文件夹挂载到主机
  • -e MYSQL_ROOT_PASSWORD=root:初始化root用户的密码

3,创建数据库

  • 进入MySQL容器
1
docker exec -it mysql /bin/bash
  • 创建数据库及用户

    进入mysql控制台

    1
    mysql -uroot -proot --default-character-set=utf8

    创建数据库myworld

    1
    create database myworld character set utf8

    创建一个用户myworld:xxxxxxx帐号并修改权限,使得任何ip都能访问:

    1
    grant all privileges on *.* to 'myworld' @'%' identified by 'xxxxxxx';

4, 数据dump操作

进入容器

mysqldump 在 /usr/bin下

1
/usr/bin/mysqldump -umyworld -p --host=127.0.0.1 --port=3306 --databases beautybox > /tmp/beautybox.sql

copy到服务器上

1
docker cp mysql:/tmp/beautybox.sql /tmp

Docker部署Nginx

部署Docker

下载nginx1.15.12的docker镜像:

1
docker pull nginx:1.15.12

  1. 手动创建下目录,不会有权限问题
1
mkdir -p /mydata/nginx/html /mydata//logs /mydata//conf
  1. 在conf目录下放置一个conf文件
    一个默认的default.conf
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
server {
listen 80;
server_name localhost;

#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

根据自己的需要修改配置文件

使用docker命令启动:

挂载目录和配置文件

1
docker run -p 9090:80 --name nginx -v /mydata/nginx/html:/usr/share/nginx/html -v /mydata/nginx/logs:/var/log/nginx -v /mydata/nginx/conf:/etc/nginx/conf.d -d nginx:1.15.12

刷新ng

$ docker exec -it nginx bash
> service nginx reload

或者

docker exec -i [nginx容器名/id] nginx -s reload

一键部署springboot项目到docker

docker端

docker端需要打开远程访问端口,这样才能允许我们本地打好的镜像直接传到docker容器里

修改docker配置文件

1
vim /usr/lib/systemd/system/docker.service

修改ExecStart行为,加入以下内容

1
-H tcp://0.0.0.0:6009  -H unix:///var/run/docker.sock

注意,默认是2375端口,我这里用6009是因为我这台服务器的这个端口恰好被别人用了。

Dockerfile中的ENV和ARG区别

在Dockerfile中,ENVARG是两个非常重要的指令。虽然它们看起来有点相似,但它们的作用和使用情境却有很大的不同。

ARG指令

ARG指令用于定义构建参数。它允许在构建映像时从外部传递参数,例如版本号、密钥等。在Dockerfile中,你可以使用ARG指令来声明这些参数,并在后续的指令中使用这些参数。例如:

1
2
3
ARG VERSION=1.0
RUN echo "Version: $VERSION"

在这个例子中,我们定义了一个名为VERSION的构建参数,并在RUN指令中使用它。当我们使用docker build命令构建映像时,可以使用--build-arg选项来传递该参数的值。例如:

1
2
docker build --build-arg VERSION=2.0 .

ENV指令

相比之下,ENV指令用于定义环境变量。这些变量在容器运行时是可用的,并且可以在容器内部的任何进程中使用。例如:

1
2
ENV DB_HOST localhost

在这个例子中,我们定义了一个名为DB_HOST的环境变量,并将其设置为localhost。在容器运行时,这个环境变量将在整个容器中可用。

区别

ARGENV指令的最大区别在于它们的作用域。ARG指令定义的参数仅在构建映像期间可用,而ENV指令定义的环境变量在容器运行时可用。因此,你可以使用ARG指令来传递构建参数,而使用ENV指令来设置容器的环境变量。

另一个区别是,ARG指令可以由--build-arg选项在构建时进行设置,而ENV指令在构建时无法更改。因此,如果你需要在构建时传递某些参数,你应该使用ARG指令。

重点来了

所以,有一种情况是,我希望在cmd中使用这个环境变量,而且这个变量我希望是通过构建传入的,现在我在编写Jenkinsfile时就遇到了这种场景。我会根据部署环境的不同,比如test和prod环境,而传入不同的java启动参数。

这种情况怎么处理呢?

解决方案是

1
2
3
4
5
6
7
# 先定义一个ARG变量
ARG VAR_A 5
# 再将ARG变量赋给ENV环境变量
ENV VAR_B $VAR_A

# 之后就可以再cmd执行时使用了
CMD echo "My variable is $VAR_B"

build时可以传入参数

1
docker build --build-arg VAR_A=yunsheng -t argtest:4.0.0 .

最后,ARG指令可以在FROM指令之前使用,但ENV指令则不能。这是因为FROM指令之前的任何指令都在构建上下文中执行,而FROM指令之后的指令则在新的构建阶段中执行。