容器基本概念
什么是容器
容器是一种基础工具;泛指任何可以用于容纳其他物品的工具,都叫容器;
容器一般用于容纳、存储、运输物品;
物品可以被放置在容器中,从而使得容器可以保护其内部放置的物品;
容器这个概念历史非常悠久,在日常生活中,我们可以看到各种各样的容器,如水杯、桶、盒、箱、盘等,
它们主要用于装载物品和隔离物品。
然而,容器不仅仅局限于生活用品,它还是一种技术。
因此,我们需要讨论的不是容器,而是容器技术。
为什么需要容器
容器技术在现代应用部署和运行方面具有很多优势。
我们在解释为什么需要容器之前,先了解一下在没有容器的情况下,应用程序是如何部署和运行的。
假设目前有两个业务进程(例如java、python
程序),需要在同一台宿主机上运行。
这种部署方式虽然没有什么问题,但存在以下几个问题:
- 1.各个业务程序之间会互相干扰。例如,
python
程序被入侵了,可能会影响到java
程序的正常运行。 - 2.资源无法合理分配。例如,
java
程序可能需要更多内存,而python
程序并不需要过多的内存。但是在没有进行资源限制的情况下,一旦python
程序占用过多内存,可能会导致java
进程运行缓慢或频繁产生GC
或FULL GC
等问题 - 3.网络协议栈无法分配。如果
java
和python
程序都需要监听80端口,这是无法实现的,因为宿主机内核仅提供一组网络协议栈,无法同时监听两个80端口
为了解决这些问题,虚拟化技术应运而生。虚拟化技术允许在同一台宿主机上允许多个独立的虚拟操作系统,每个虚拟操作系统都有属于自己的专属内核,拥有独立的网络协议栈,从而解决了端口冲突的问题。
同时,虚拟化技术在一定程度上实现了资源隔离(cpu
、内存、磁盘),避免各个应用进程之间互相影响,提高了其安全性。
其次每个虚拟机还可以明确指定它能使用CPU
、内存等资源。
但为了运行一个进程,需要在宿主操作系统(host os
)之上再运行一个客户操作系统(guest os
),接着运行内核,然后在内核上运行用户进程,会带来额外的系统开销。
所以为了运行一个进程而单独运行一个guest os
显然没有必要。
因此,容器化技术应运而生。它与虚拟化技术有所不同。在虚拟化技术中,每个应用都需要一个完整的操作系统,而容器技术则省略了Guest OS
,将所有进程运行在同一台宿主机上,且彼此之间实现互相隔离。
也就是说,容器技术是将用户空间划分为多个独立的环境,使它们之间相互隔离,不受干扰。
每个隔离环境只运行一个进程或部分进程,但它们仍然共享同一个底层内核。
每个容器中运行的进程只能看到属于自己容器的用户边界。
虽然这种隔离程度没有虚拟化技术那么彻底,但仍然能为进程提供安全的运行环境,并保护它们免受其他进程的干扰,这就是我们提到的容器技术。
容器间如何隔离
容器在同一台宿主机上创建多个独立、隔离的运行环境,让不同的应用程序能够互不干扰的运行。
然而,这些容器共享的是同一个宿主机的内核,因此我们需要在某些层次上对这些应用程序实现隔离。
那么问题在于,究竟需要在哪些层次上,对容器A
和容器B
进行隔离,又需要隔离哪些内容。
-
主机名(
UTS
)
每个容器都应该有属于自己的主机名称,但在没有隔离的情况下,所有容器是共享相同的主机名,这可能导致容器以下问题:- 1.日志记录和审计:当所有容器共享相同的主机名时,日志记录和审计变得困难。在解析日志时,很难区分不同容器产生的日志,问题变得难以追踪和解决。
- 2.应用程序配置:部分应用程序可能依赖主机名来实现特定功能或者用于配置。当多个容器共享相同的主机名时,这可能导致应用程序配置出现问题,从而影响应用程序的正常运行。
- 3.内部通信:在某些情况下,容器可能需要根据主机名进行通信。如果所有容器共享相同的主机名,这可能导致通信出现混乱,从而影响容器间的协作
因此,需要确保每个容器都拥有属于自己的主机名,从而避免了上述问题的产生,所以需要对主机名(UTS
)进行隔离
-
根文件系统(
Mount
)
每个容器都应该有属于自己的根文件系统,在没有文件系统隔离的情况下,容器可能会访问到其他容器的文件,这可能导致以下问题:- 1.数据安全:若容器之间没有独立的文件系统,它们可能访问彼此的文件,从而导致数据泄露或被篡改。
- 2.应用程序独立性:每个容器应该像一个独立的服务器一样运行应用程序。这需要容器拥有自己的系统文件,如:
/etc/、/bin/
等。如果没有隔离,一个容器内的改动可能会影响到另一个容器的运行环境,从而造成应用程序运行的不稳定或失败。 - 3.容器迁移:每个容器拥有独立的文件系统,使得打包和迁移过程更为简单和安全。如果容器之间没有隔离,迁移过程中可能会不小心包含了不属于该容器的文件,或者迁移后的容器因为缺少必要的文件而无法正常运行。
因此,每个容器都拥有自己的文件系统,确保数据的安全,所以需要对文件系统进行隔离(Mount
)
- 进程间通信(
IPC
)
每个容器都应该拥有自己的独立的IPC
,也就是容器内的进程可以通过socket
、管道等IPC
机制进行通信,这是正常的。但如果两个隔离的容器环境能够直接使用同一IPC
通信机制(例如,容器A
的PHP
应用通过socket
直接访问容器B
的mysql
服务,这就违背了容器隔离的初衷。
正确的做法,两个容器应该通过网络层面的通信手段来进行交互,例如通过IP
地址或http
等协议。因为每个容器都有独立的ip
地址,同时每个容器也被视为一个独立的服务器示例。这种方式确保了容器之间的通信安全,又符号隔离的原则。
- 进程(
PID
)
在容器中运行的进程,认为当前所在的用户空间是唯一的。
为确保进程能被正常管理,每个容器都需要具备自己的init
进程(pid=1
)。
但实际上init
进程只有一个。
因此,我们必须为每个容器伪装一个init
(pid=1
)的进程,让容器认为它们有独立的进程树。
这意味着对应容器内的进程,它们的进程树以pid=1
的init
进程为根,认为这就是真正的init
。
实际上,在宿主机上,容器的init
进程并非真正的init(pid=1)
,而只是一个普通进程。
容器内其他进程都是该容器init
进程的子进程。
通过这种方式,实现了容器内进程的隔离和管理。
- 进程运行身份(
USER
)
在容器内运行的进程需要以某个特定的用户身份运行。
假设每个容器的身份都为root
且uid
都为0,但实际系统中真正的root
只有一个。
因此,我们需要为每个容器伪装一个root
用户,让它可以在容器中可以执行任何操作。
但如果回到宿主机上,这些伪装的用户其实就是一个普通用户身份,有点类似chown
。
这种设计实现了用户身份隔离,避免容器内进程以真正的root
身份运行。
同时,容器内伪装的用户仍具有执行容器内所有操作的能力,满足容器运行和管理的需求。
-
网络(
NET
)
在容器技术中,每个容器都像一个独立的服务器,它们各自有自己的ip
地址、网络接口和tcp/ip
协议栈。
虽然在内核级别,tcp/ip
协议栈只有一个,但容器技术通过网络隔离确保了每个容器都能独立工作,互不干扰。- 1.独立的
ip
地址:每个容器都有自己的ip
地址,就像一个独立的服务器。它们可以使用这个ip
地址与外部网络通信,同时与其他容器保持隔离。 - 2.专用虚拟网络接口:为每个容器创建一个虚拟网络接口,容器内的进程可通过该接口与外界通信。尽管在宿主机上,这些虚拟网络接口都是通过物理网络接口实现的,但在容器内部,每个容器都认为自己拥有独立的网络接口。
- 3.独立的
tcp/ip
协议栈:每个容器拥有自己的网络命名空间,它们各自的tcp/ip
协议栈独立运行。例如所有容器都可以监听在tcp 80
端口上,并且互不影响。
- 1.独立的
为了实现上述功能,容器网络采用了以下技术:
- 虚拟网络接口:为每个容器创建一个虚拟网络接口,例如
eth0
,使其看起来像独立的网络接口。 - 网络命名空间:为每个容器提供独立的网络命名空间,实现各自的
tcp/ip
协议栈隔离。 - 虚拟交换机:通过虚拟交换机将容器内的虚拟接口连接到宿主机的物理接口上,实现容器与外部网络的互联。网络桥接技术实现容器间的通信。
因此,容器的网络通过独立的ip
地址、专用的虚拟网络接口、独立的tcp/ip
协议栈以及虚拟交换机和网络桥接技术,实现了网络隔离和容器间通信的要求。
这使得每个容器都能像一个独立的服务器一样工作,同时保持与其他容器的隔离。
- 总结
为了支持容器隔离机制的实现,linux
内核在内核级别通过namespaces
的名称空间机制对这六种资源进行原生支持,
并通过一些系统调用(clone
创建一个新进程和setns
将当前进程移动至一个namespace
中)将这些资源隔离功能提供给用户空间,让容器技术能够利用这些隔离机制。
隔离类型 | 功能 | 系统调用参数 | 内核版本 |
---|---|---|---|
MNT Namespace(mount) | 提供硬盘挂载点和文件系统的隔离能力 | CLONE_NEWNS | Linux 2.4.19 |
IPC Namespace | 提供进程间通信的隔离能力 | CLONE_NEWIPC | Linux 2.6.19 |
UTS Namespace | 提供主机名隔离能力 | CLONE_NEWUTS | Linux 2.6.19 |
PID Namespace | 提供进程隔离能力 | CLONE_NEWPID | Linux 2.6.24 |
Net Namespace | 提供网络隔离能力 | CLONE_NEWNET | Linux 2.6.29 |
User Namespace | 提供用户隔离能力 | CLONE_NEWUSER | Linux 3.8 |
容器的资源限制
主机虚拟化在创建虚拟机时,可以为每个虚拟机指定cpu
核心数、内存大小等资源限制。
假设一个虚拟机设定内存总用来为4G
,如果该虚拟机出现了内存泄漏,也不会影响到其他虚拟机,因此它的内存最多使用到4G
,如果还需要更多内存则无法申请。
而容器技术与主机虚拟化不同,多个容器运行在同一个操作系统内核上,如果我们不对容器使用的资源加以限制,那么当一个容器出现内存泄漏,大量吞噬内存,则会造成其他容器无法正常申请内存,从而造成OOM
。
为了实现容器间的资源限制,linux
内核提供了一个功能叫Cgroups(control groups)
控制组。
用来限制、控制一个进程能够使用的资源。
因此,我们可以借助Cgroups
资源限制来对容器的cpu
使用率、内存用量、设备的访问权限等进行限制。
这样我们就可以避免一个容器因内存泄漏而吞噬掉系统可用内存,造成其他容器异常的情况。
实践:容器实现的资源限制其实底层就是调用cgroups
来实现的,所以我们了解了Cgroups
如何实现控制,就能清楚Docker
的资源限制是如何实现控制的。
-
创建一个控制组
mkdir /sys/fs/cgroup/oldxu_group
-
运行一个使用
cpu
到100%的用户进程while :; do :; done& [1] 2746
-
将
cpu
资源占用较高的应用程序id
加入到控制组中
echo 2746 > /sys/fs/cgroup/oldxu_group/cgroup.procs
-
配置
Cgroup
资源限制,限制该控制组cpu
最大能使用到30%echo "30000 100000" > /sys/fs/cgroup/oldxu_group/cpu.max # 这条命令会将 cpu.max 文件设置为 “30000微秒(30毫秒) 100000微秒(100毫秒)”,即在每 100 毫秒的周期内最多使用 30 毫秒的 cpu 时间。
-
再次检查,会发现该程序的
cpu
最多使用到30%
top -p 2746
-
如果再添加一个进程到同一控制组,那么两个进程会平分
cpu
的使用率 -
总结:
docker
其实就是运行在宿主机的一个隔离进程,通过cgroup
进行资源限制,通过namespace
进行资源隔离。
Docker基本概述
什么是docker
docker
是一种基于容器技术的轻量级虚拟化解决方案。
它是一个开源的容器引擎,对linux
底层的cgroup
和namespace
等技术进行封装和抽象,为用户提供了便捷的方式来创建和管理容器。
其次,docker
可以将应用程序及其所需的运行环境打包到一个镜像中。
这个镜像就像一个便携式的盒子,里面装有应用程序和它所需的所有依赖。
这个镜像可以在任何操作系统(如windows、linux
等)上运行。
当我们需要运行这个应用程序时,可以通过打包好的镜像,创建出对应的容器实例。
那么这个容器中的进程会运行在一个隔离的环境中,与宿主系统及其他容器保持隔离。
使用docker
能解决哪些实际问题:
-
1.环境一致性:开发者的应用程序往往依赖于他们的本地环境,可能会出现在我的电脑上运行正常,在其他地方不行的问题。
docker
可以将应用程序的运行环境与应用程序捆绑在一起,消除了环境差异问题。 -
2.部署简化:开发人员编写的应用,对应运维来说部署过于繁琐。
docker
结合jenkins
等工具实现CI/CD
,能够让应用部署变得更简单,极大地提供应用部署效率。 -
3.快速更新:传统应用更新,可能会出现把宿主机环境搞坏了的问题。
docker
是调用jenkins
部署最新版本的镜像,实现应用更新,整个操作不会影响宿主机任何环境。
docker与虚拟化
虚拟化技术主要包括虚拟机与容器这两种形式,目的都是为了提高资源利用率。
但两者实现方式和隔离级别不同。
- 虚拟机:通过虚拟机管理程序可以在一台物理机上模拟多个虚拟机,而后在其上安装完整的操作系统和应用。因此虚拟机需要包含整个操作系统及应用等,体积较大,启动也较慢。但它能提供强大的隔离机制。
- 容器不是通过模拟虚拟机来实现,而是将代码及其运行环境封装在一个镜像中。通过这种方式,就可以将多个容器在一台机器上同时运行。多个容器之间共享同一操作系统内核,但容器之间是相互隔离的,并且作为独立进程运行在用户空间中。
与虚拟机相比,容器更加轻量级。由于容器镜像的体积通常只有几十MB
,所以它可以在同一台机器上运行大量的容器应用。
docker
与虚拟机(VM
)的主要区别可以从以下几个方面来说明:
方面 | docker | 虚拟机(VM ) |
---|---|---|
资源隔离 | 隔离弱,使用操作系统级别进行资源隔离 | 隔离较强,使用硬件虚拟化技术进行资源隔离 |
启动速度 | 极快,通常在几秒内 | 相对较慢,可能需要几分钟 |
系统开销 | 较小,容器共享宿主操作系统内核 | 较大,每个VM 需要运行完整的操作系统 |
镜像大小 | 较小,只包含运行应用程序所需的依赖和配置(MB ) |
程序的依赖和配置(GB ) |
可移植性 | 高读可移植性,可以在支持容器技术的任何平台上进行 | 较低,需要特定的虚拟化技术支持 |
docker架构模型
docker
是一个C/S
架构模型(client、server
);
docker daemon
:dockerd
是docker
的守护进程,负责监听docker api
的请求和管理docker
对象,如镜像、容器和网络。它在后台运行,处理客户端发来的命令和请求。
docker client
:docker client
是用户与docker
进行交互的工具。client
将客户端需要执行的docker run
等命令发送给docker daemon
进行处理。
docker registry
:docker registry
是用于存储和分发docker
镜像的仓库。docker
默认提供的是docker hub
。当然用户也可以自己搭建企业内部私有仓库。
docker images
:docker images
是将软件环境打包成一个镜像模板。它们用户创建容器,并可以在多个容器间共享。镜像包含了运行应用所需的所有依赖和配置。
containers
:容器是docker
镜像的运行示例。
docker存储引擎
镜像构建
docker
镜像构建底层其实是通过分层构建和联合挂载来实现的。
例如:在底层先创建一个基础的rockylinux
镜像,然后在该基础之上安装nginx
,从而形成一个新的nginx
镜像,但是这个nginx
镜像它仅包含nginx
本身,不包含rockylinux
镜像。需要将这两个镜像叠加在一起,才能得到一个运行在rockylinux
系统上的nginx
镜像,这种就叫分成构建。
联合挂载是指在docker
容器启动时,将各个镜像层叠加在一起,形成一个统一的文件系统试图的过程。
当运行一个基于rockylinux
的nginx
容器时,底层通过联合挂载将nginx
镜像和rockylinux
镜像叠加在一起,使得在容器内部,看起来就像是一个完整的rockylinux
上运行了一个nginx
应用。
1 [联合挂载:nginx容器]
2 |--[基础层] Rockylinux(只读)
3 |--[应用层] Nginx(只读)
4 |--[容器层] 可写层(可写)
docker
镜像分层构建和联合挂载的设计带来了诸多好处,其中最显著的就是镜像分发变得更加容易了。
这种方式在需要运行多个容器的场景下尤为明显。
例如,在一个系统上需要运行nginx、tomcat、php
这三个容器,它们的底层都是基于rockylinux
进行构建。
此时,主机上只需要准备一个rockylinux
基础镜像和三个不同的应用层(nginx、tomcat、php
),而不是为每个组合准备单独的一个完整镜像。
在运行各个容器时,docker
会将相应的应用层叠加在基础层之上。
- 1.当运行
nginx
容器时,底层是rockylinux
,上层是nginx
- 2.当运行
tomcat
容器时,底层是rockylinux
,上层是tomcat
- 3.当运行
php
容器时,底层是rockylinux
,上层是php
容器与镜像
docker
使用镜像作为容器的基础。
镜像是只读的,包含了容器运行所需的所有文件和依赖。
每个容器都基于一个镜像,并在其上添加一个可写层。
这个可写层用于存储容器运行时产生的临时数据。
由于镜像是只读的,多个容器可以同时共享对同一个底层镜像的访问,而不会相互影响。
每个容器都有自己独立的可写层,这意味着所有对容器的更改(如添加、删除或修改文件)都仅限于该容器的可写层,而不会影响到其他容器。
通过这种方式,docker
实现了镜像和容器之间的分层结构,使得容器能够高效地共享底层镜像资源,同时保证了各个容器之间的隔离。
存储引擎
docker
存储引擎就是负责在底层实现镜像分层构建以及联合挂载的功能。
docker
目前支持的主要存储引擎是overlay2
。
在早期,docker
使用的是aufs
存储引擎,但现在已经被废弃。
overlay2
存储引擎采用了一种称为叠加文件系统的技术,它允许将多个独立的文件系统层叠加在一起,形成一个统一的试图。
这种方式既节省了存储空间,又便于镜像分发和管理。
# 节省空间
rocky linux:200MB------->
# 1.如果没有进行分层构建,意味着3个容器如果都是完整的镜像,则需要只是600M
# 2.有了分层构建,仅需要200MB的rocky linux镜像,然后加上三份应用层的大小
# 加速镜像分发
# 1.如果nginx镜像是由centos层+nginx层组成
# 2.此时主机需要使用该镜像,那么如果在主机本地已经存储了centos层,仅需要拉取没有的nginx层即可。
docker
的默认存储驱动是overlay2
,它使用overlay
文件系统来组合多个层。
对于每个容器,overlay2
驱动会将只读的镜像层(lowerdir
)与容器的可写层(upperdir
)组合在一起,通过一个合并目录(mergeddir
)呈现为一个统一的文件系统。
docker安装部署
安装docker
docker.io
:Debian/ubuntu
官⽅基于docker
社区源码封装的版本
docker-ce
:docker.com
放出来的社区版,仅维护源码
docker-ee
:docker.com
维护的商业版
-
配置
docker
的yum
源
curl -o /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
-
安装并启动
docker
yum install docker-ce
systemctl enable docker
systemctl start docker
-
使用
docker run
一个容器测试docker
是否安装成功
docker run hello-world
-
通过
docker info
命令查看
docker info
Client: Docker Engine - Community
Version: 26.1.4
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.14.1
Path: /usr/libexec/docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v2.27.1
Path: /usr/libexec/docker/cli-plugins/docker-compose
Server:
Containers: 1
Running: 0
Paused: 0
Stopped: 1
Images: 2
Server Version: 26.1.4
Storage Driver: overlay2
Backing Filesystem: xfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: cgroupfs
Cgroup Version: 1
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: d2d58213f83a351ca8f528a95fbd145f5654e957
runc version: v1.1.12-0-g51d5e94
init version: de40ad0
Security Options:
seccomp
Profile: builtin
Kernel Version: 3.10.0-1160.el7.x86_64
Operating System: CentOS Linux 7 (Core)
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.871GiB
Name: docker-node01
ID: b2d812b0-60ff-4d9d-817f-1471a03c4e27
Docker Root Dir: /data/docker
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
Registry Mirrors:
https://y5rjfmsr.mirror.aliyuncs.com/
Live Restore Enabled: false
配置镜像加速
在启动docker
容器之前,建议先配置一个镜像加速器。
因为容器启动依赖于特定的镜像。
如果本地没有下载对应的镜像,docker
会尝试从官方的仓库下载。
但官方仓库通常都在国外,因此可能会出现下载速度慢或者下载失败的问题。
为了提高下载速度并确保下载的可靠性,可以使用如阿里云镜像加速器这样的服务。
- 通过修改配置文件
/etc/docker/daemon.json
使用阿里云加速器,配置操作如下:
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://xxx.mirror.aliyuncs.com"]
}
EOF
systemctl daemon-reload
systemctl restart docker
- 检查
docker
镜像加速器是否配置成功
docker info | grep -A1 "Registry Mirrors"
Registry Mirrors:
https://xxx.mirror.aliyuncs.com/
配置存储数据
docker
默认将所有重要数据(包括镜像、容器和日志)存储在/var/lib/docker
目录下。
在生产环境中,系统盘的存储空间通常有限并且不易扩展。
为避免因磁盘空间不足而导致容器运行出现故障,进而影响线上业务,建议将docker
的存储路径迁移到一个专门的数据存储盘。
以下是将docker
存储位置迁移到一个新的数据盘的步骤,这里假设新的数据盘挂载在/data
目录。
-
停止
docker
进程
systemctl stop docker.socket
-
修改
/etc/docker/daemon.json
cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://xxx.mirror.aliyuncs.com"],
"data-root": "/data/docker"
}
-
拷贝原有数据至新的存储目录(如果是全新的
docker
,可不拷贝)
cp -rp /var/lib/docker/ /data/
-
重启
docker
,并验证存储路径是否修改成功
systemctl daemon-reload
systemctl start docker
docker info | grep "Docker Root"
Docker Root Dir: /data/docker
留言