微服务架构之「 容器技术 」

微服务架构之「 容器技术 」,第1张

现在一聊到容器技术,大家就默认是指 Docker 了。但事实上,在 Docker 出现之前,PaaS社区早就有容器技术了,以 Cloud Foundry、OpenShift 为代表的就是当时的主流。

那为啥最终还是 Docker 火起来了呢?

因为传统的PaaS技术虽然也可以一键将本地应用部署到云上,并且也是采用隔离环境(容器)的形式去部署,但是其兼容性非常的不好。因为其主要原理就是将本地应用程序和启停脚本一同打包,然后上传到云服务器上,然后再在云服务器里通过脚本启动这个应用程序。

这样的做法,看起来很理想。但是在实际情况下,由于本地与云端的环境差异,导致上传到云端的应用经常各种报错、运行不起来,需要各种修改配置和参数来做兼容。甚至在项目迭代过程中不同的版本代码都需要重新去做适配,非常耗费精力。

然而 Docker 却通过一个小创新完美的解决了这个问题。在 Docker 的方案中,它不仅打包了本地应用程序,而且还将本地环境( *** 作系统的一部分)也打包了,组成一个叫做「 Docker镜像 」的文件包。所以这个「 Docker镜像 」就包含了应用运行所需的全部依赖,我们可以直接基于这个「 Docker镜像 」在本地进行开发与测试,完成之后,再直接将这个「 Docker镜像 」一键上传到云端运行即可。

Docker 实现了本地与云端的环境完全一致,做到了真正的一次开发随处运行。

一、容器到底是什么?

容器到底是什么呢?也许对于容器不太了解,但我们对虚拟机熟悉啊,那么我们就先来看一下容器与虚拟机的对比区别:

上图的左侧是虚拟机的原理,右侧是Docker容器的原理。

虚拟机是在宿主机上基于 Hypervisor 软件虚拟出一套 *** 作系统所需的硬件设备,再在这些虚拟硬件上安装 *** 作系统 Guest OS,然后不同的应用程序就可以运行在不同的 Guest OS 上,应用之间也就相互独立、资源隔离了,但是由于需要 Hypervisor 来创建虚拟机,且每个虚拟机里需要完整的运行一套 *** 作系统 Guest OS,因此这个方式会带来很多额外资源的开销。

而 Docker容器 中却没有 Hypervisor 这一层,虽然它需要在宿主机中运行 Docker Engine,但它的原理却完全不同于 Hypervisor,它并没有虚拟出硬件设备,更没有独立部署全套的 *** 作系统 Guest OS。

Docker容器没有那么复杂的实现原理,它其实就是一个普通进程而已,只不过它是一种经过特殊处理过的普通进程。

我们启动容器的时候(docker run …),Docker Engine 只不过是启动了一个进程,这个进程就运行着我们容器里的应用。但 Docker Engine 对这个进程做了一些特殊处理,通过这些特殊处理之后,这个进程所看到的外部环境就不再是宿主机的那个环境了(它看不到宿主机中的其它进程了,以为自己是当前 *** 作系统唯一一个进程),并且 Docker Engine 还对这个进程所使用得资源进行了限制,防止它对宿主机资源的无限使用。

那 Docker Engine 具体是做了哪些特殊处理才有这么神奇的效果呢?

二、容器是如何做到资源隔离和限制的?

Docker容器对这个进程的隔离主要采用2个技术点:

弄清楚了这两个技术点对理解容器的原理非常重要,它们是容器技术的核心。

下面来详细解释一下:

三、容器的镜像是什么?

一个基础的容器镜像其实就是一个 rootfs,它包含 *** 作系统的文件系统(文件和目录),但并不包含 *** 作系统的内核。

rootfs 是在容器里根目录上挂载的一个全新的文件系统,此文件系统与宿主机的文件系统无关,是一个完全独立的,用于给容器进行提供环境的文件系统。

对于一个Docker容器而言,需要基于 pivot_root 指令,将容器内的系统根目录切换到rootfs上,这样,有了这个 rootfs,容器就能够为进程构建出一个完整的文件系统,且实现了与宿主机的环境隔离,也正是有了rootfs,才能实现基于容器的本地应用与云端应用运行环境的一致。

另外,为了方便镜像的复用,Docker 在镜像中引入了层(Layer)的概念,可以将不同的镜像一层一层的迭在一起。这样,如果我们要做一个新的镜像,就可以基于之前已经做好的某个镜像的基础上继续做。

如上图,这个例子中最底层是 *** 作系统引导,往上一层就是基础镜像层(Linux的文件系统),再往上就是我们需要的各种应用镜像,Docker 会把这些镜像联合挂载在一个挂载点上,这些镜像层都是只读的。只有最上面的容器层是可读可写的。

这种分层的方案其实是基于 联合文件系统UnionFS(Union File System)的技术实现的。它可以将不同的目录全部挂载在同一个目录下。举个例子,假如有文件夹 test1 和 test2 ,这两个文件夹里面的文件 有相同的,也有不同的。然后我们可以采用联合挂载的方式,将这两个文件夹挂载到 test3 上,那么 test3 目录里就有了 test1 和 test2 的所有文件(相同的文件有去重,不同的文件都保留)。

这个原理应用在Docker镜像中,比如有2个同学,同学A已经做好了一个基于Linux的Java环境的镜像,同学S想搭建一个Java Web环境,那么他就不必再去做Java环境的镜像了,可以直接基于同学A的镜像在上面增加Tomcat后生成新镜像即可。

以上,就是对微服务架构之「 容器技术 」的一些思考。

码字不易啊,喜欢的话不妨转发朋友,或点击文章右下角的“在看”吧。

因为Docker技术的火热,因此在工作中我们经常会以容器的方式来运行一个应用。每当容器无法成功运行或者想要对容器中的应用参数、应用配置以及应用启动进行深入研究时,当然希望能够像在宿主机上调试程序一样在容器中调试应用。容器的本质包括应用与应用运行所依赖的环境, 因此首先需要创建一个空壳容器(没有运行应用的应用容器),然后进入容器中调试应用。此处的空壳容器提供了应用运行所需的环境,进而可方便的在其中调试应用。实践环境:Centos72+Docker1126。

比较规范的镜像的Dockerfile中通常会有ENTRYPOINT与CMD的定义(Docker官方推荐这样做)。因此容器的启动命令则为ENTRYPOINT所对应的脚本或可执行程序加上CMD中定义的内容。比如elasticsearch的Dockerfile定义的ENTRYPOINT与CMD分别为:ENTRYPOINT ["/docker-entrypointsh"] CMD ["elasticsearch"],则创建的容器的启动命令为: /docker-entrypointsh elasticsearch ;mysql的Dockerfile:ENTRYPOINT ["docker-entrypointsh"] CMD ["mysqld"],则创建的容器的启动命令为: /docker-entrypointsh mysqld 。所以想要知道一个容器的启动命令需要首先了解其镜像的Dockerfile中ENTRYPOINT与CMD的定义。如何查看一个镜像的ENTRYPONT与CMD的值呢?一般采用如下两种方式:

上述第一种方式适用于比较规范的镜像,这类镜像通常会提供清晰、具体的Dockerfile。第二种方式适用于各种镜像,尽管是不规范的镜像。通过history、inspect两个命令的任一个均可快速、方便的查看镜像的ENTRYPOINT与CMD的值。

若要调试容器中的应用程序,则需额外的设置实现。docker run命令提供的--entrypoint参数能够覆盖Dockerfile中默认定义的ENTRYPOINT;docker run [OPTIONS] IMAGE [COMMAND] [ARG]的COMMAND能够替换Dockerfile中定义的CMD。通过上面的示例可以发现,有的镜像的Dockerfile中ENTRYPOINT值为:/docker-entrypointsh,CMD为应用的可执行程序;有的镜像的Dockerfile中ENTRYPOINT值为应用的可执行程序,CMD为可执行程序的参数。因此针对不同的镜像想要创建空壳容器其方式是不同的。

容器其实是应用与应用运行所依赖的环境,创建空壳容器即提供了应用所需要的环境,进入此环境中可以调试应用,可以验证应用的各个参数,同样更可以像在宿主机中运行程序一样在此环境中运行应用,区别仅是容器与宿主机的两个环境。上面是对如何在容器中调试应用程序做的一些记录,希望与大家一起讨论、交流,一起学习。

Docker 容器技术目前是微服务/持续集成/持续交付领域的第一选择。而在 DevOps 中,我们需要将各种后端/前端的测试/构建环境打包成 Docker 镜像,然后在需要的时候,Jenkins 会使用这些镜像启动容器以执行 Jenkins 任务。

为了方便维护,我们的 CI 系统如 Jenkins,也会使用 Docker 方式部署。

Jenkins 任务中有些任务需要将微服务构建成 Docker 镜像,然后推送到 Harbor 私有仓库中。

或者我们所有的 Jenkins Master 镜像和 Jenkins Slave 镜像本身都不包含任何额外的构建环境,执行任务时都需要启动包含对应环境的镜像来执行任务。

我们的 Jenkins Master、Jenkins Slaves 都是跑在容器里面的, 该如何在这些容器里面调用 docker run 命令启动包含 CI 环境的镜像呢?

在这些 CI 镜像里面,我们从源码编译完成后,又如何通过 docker build 将编译结果打包成 Docker 镜像,然后推送到内网仓库呢?

答案下面揭晓。

Docker 采取的是 Client/Server 架构,我们常用的 docker xxx 命令工具,只是 docker 的 client,我们通过该命令行执行命令时,实际上是在通过 client 与 docker engine 通信。

我们通过 apt/yum 安装 docker-ce 时,会自动生成一个 systemd 的 service,所以安装完成后,需要通过 sudo systemctl enable dockerservice 来启用该服务。

这个 Docker 服务启动的,就是 docker engine,查看 /usr/lib/systemd/system/dockerservice ,能看到有这样一条语句:

默认情况下,Docker守护进程会生成一个 socket( /var/run/dockersock )文件来进行本地进程通信,因此只能在本地使用 docker 客户端或者使用 Docker API 进行 *** 作。

sock 文件是 UNIX 域套接字,它可以通过文件系统(而非网络地址)进行寻址和访问。

因此只要以数据卷的形式将 docker 客户端和上述 socket 套接字挂载到容器内部,就能实现 "Docker in Docker",在容器内使用 docker 命令了。具体的命令见后面的「示例」部分。

要记住的是,真正执行我们的 docker 命令的是 docker engine,而这个 engine 跑在宿主机上。所以这并不是真正的 "Docker in Docker"

运行过Docker Hub的Docker镜像的话,会发现其中一些容器时需要挂载/var/run/dockersock文件。这个文件是什么呢?为什么有些容器需要使用它?简单地说,它是Docker守护进程(Docker daemon)默认监听的Unix域套接字(Unix domain socket),容器中的进程可以通过它与Docker守护进程进行通信。

在容器内部使用宿主机的 docker,方法有二:

容器的启动方式也有两种,如下:

示例命令如下:

必须以 root 用户启动!(或者其他有权限读写 /var/run/dockersock 的用户) 然后,在容器内就能正常使用 docker 命令,或者访问宿主机的 docker api 了。

docker-composeyml 文件内容如下:

然后通过 docker-compose up -d 即可后台启动容器。

通过上面的 *** 作,我们在容器内执行 docker ps 时,还是很可能会遇到一个问题: 权限问题

如果你容器的默认用户是 root,那么你不会遇到这个问题,因为 /var/run/dockersock 的 onwer 就是 root

但是一般来说,为了限制用户的权限,容器的默认用户一般都是 uid 和 gid 都是 1000 的普通用户。这样我们就没有权限访问 /var/run/dockersock 了。

解决办法:

方法一(不一定有效):在构建镜像时,最后一层添加如下内容:

方法二:经测试一定有效,在Dockerfile中使用USER参数

这样我们构建的镜像就是root用户了,经测试在docker-composeyaml文件中user参数并不好用,类似如下

这样我们的默认用户,就能使用 docker 命令了。

参考

Namespace帮助容器来实现各种计算资源的隔离,Cgroups主要限制的是容器能够使用的某种资源量。

init进程创建的过程:

打开电源--> 执行BIOS/boot-loader--->boot-loader加载Linux内核(内核文件存放在/boot目录,文件名类似vmliunz)--> 执行的第一个用户态程序就是init进程。

1号进程就是第一个用户态的进程,有它直接或者间接创建了namespace中的其他进程。

特权信号就是Linux为kernel和超级用户去删除任意进程所保留的,不能被忽略也不能被捕获。

由于SIGKILL是一个特例,因为SIGKILL是不允许注册用户handler的,那么它只有SIG_DFL handler,init进程是永远不能被SIGKILL所杀,但是可以被SIGTERM杀死。

进程处理信号的选择:

1Linux内核里其实都是用task_struct这个接口来表示的。Linux里基本的调度单位是任务。任务的状态有两个TASK_RUNNING(运行态)和睡眠态(TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE)

运行态是无论进程是正在运行中,还是进程在run queue队列里随时可以运行,都处于这个状态。

睡眠是指进程需要等待某个资源而进入的状态,要等待的资源可以是一个信号量,或者是磁盘IO,这个状态的进程会被放入到wait queue队列里。

TASK_INTERRUPTIBLE是可以被打断的,显示为S stat,TASK_UNINTERRUPTIBLE 是不能被打断的,显示的进程为D stat。

在调用do_exit()的时候,有两个状态,EXIT_DEAD,就是进程在真正结束退出的那一瞬间的状态;EXIT_ZOMBIE状态,是在EXIT_DEAD之前的一个状态。

可以通过/proc/sys/kernel/pid_max设置进程最大的数量。如果机器中CPU数目小于等于32,pid_max设置为32768(32K),如果CPU数目大于32,pid_max的数目为N1024

在创建容器成功之后, 创建容器的服务会在/sys/fs/cgroups/pids下建立一个字目录,就是一个控制组,控制组里最关键的一个文件是pidsmax。

父进程在创建完子进程就不管了,这就是子进程变成僵尸进程的原因。

在主进程里,就是不断在调用带WHOHANG参数的waitpid(),通过这个方式清理容器中所有的僵尸进程。

Containerd在停止容器的时候,就会向容器的init进程发送一个SIGTERM信号,其他进程收到的是SIGKILL信号。

kill()这个系统调用,输入两个参数:进程号和信号,就把特定的信号发送给指定的进程了。

signal调用,决定了进程收到特定的信号如何来处理,SIG_DFL参数把对应信号恢复为缺省handler, 也可以用自定义的函数作为handler,或者用SIG_IGN参数让进程忽略信号。

如何解决停止容器的时候,容器内应用程序被强制杀死的问题:

在容器的init进程中对收到的信号做转发,发送到容器中的其他子进程,这样容器中的所有进程在停止时,都会收到SIGTERM,而不是SIGKILL信号了。

在/sys/fs/cgroup/cpu这个目录看到cpu的数据

Linux普通的调度的算法是CFS(完全公平调度器)

cpucfs_period_us,cfs算法的一个调度周期,是以位秒为单位。

cpucfs_quota_us,在一个调度周期里这个控制组被允许的运行时间。

cpushares,cpu cgroup对于控制组之间的cpu分配比例,缺省值为1024

由于/proc/stat文件是整个节点全局的状态文件,不属于任何一个Namespace,因此在容器中无法通过读取/proc/stat文件来获取单个容器的CPU使用率。

单个容器CPU使用率=((utime_2 - utime_1)+(stime_2 - stime_1)) 1000/(HZ et1)

无法通过CPU Cgroup来控制Load Average的平均负载。

Load Average是一种CPU资源需求的度量:

平均负载统计了这两种情况的进程:

Load Average = 可运行队列进程平均数 + 休眠队列中不可打断的进程平均数

OOM Killer是在Linux系统里如果内存不足时,就需要杀死一个正在有耐性的进程来释放一些内存。

Linux允许进程在申请内存的时候是overcommit,就是允许进程申请超过实际物理内存上线的内存。

malloc()申请的是内存虚拟地址,系统只是程序一个地址范围,由于没有写入数据,所以程序没有得到真正的物理内存。

oom_badness()函数,判断条件:

1进程已经使用的物理内存页面数;

2每个进程的OOM校准值oom_scire_adj。在/proc文件系统中,每个进程都有一个/proc/<pid>/oom_score_adj的接口文件。

用系统总的可用页面数,乘以OOM校准值oom_score_adj,再加上进程已经使用的物理页面数, 计算出来的值越大,那么这个进程被OOM Killer的几率也越大。

Memory Cgroup是对一组进程的Memory做限制,挂在/sys/fs/cgroup/memory目录下。

journalctl -k查看/var/log/message,看到的信息如下:

1容器中每一个进程使用的内存页面数量。

2oom-kill: 可以看到那个容器发生

3Killed process7445 那个进程被杀死。

Linux内存模型:RSS和Page Cache。

RSS:进程真正申请到物理页面的内存大小。

判断容器实际使用的内存量需要使用memorystat里的rss值。free获取到的内存值,需要去掉available字段下的值。

Page Cache是进程在运行中读写磁盘文件后,作为Cache而继续保留在内存中,它的目的是为了提高磁盘文件的读写性能。

内存使用量计算公式(memorykmemusage_in_bytes表示该memcg内核内存使用量):memoryusage_in_bytes=memorystat[rss]+memorystat[cache]+memorykmemusage_in_bytes

Memory Cgroup OOM不是真正依据内存使用量memoryusage_in_bytes,而是依据working set,working set的计算公式: working_set = memoryusage_in_bytes - total_inactive_file。

swappiness(/proc/sys/vm/swapiness)可以决定系统将会有多频繁地使用交换分区。取值范围为0-100,缺省值为60。

memoryswapiness(Cgroup中的参数)可以控制这个Memory Cgroup控制组下面匿名内存和page cache的回收。

当memoryswapiness=0的时候,对匿名页的回收是始终禁止的,也就是始终不会使用Swap空间。

为了有效地减少磁盘上冗余的镜像数据,同时减少冗余的镜像数据在网络上的传输,选择一种针对容器的文件系统是很有必要的,这类的文件系统被称为UnionFS。

UnionFS实现的主要功能是把多个目录一起挂载在同一目录下。

OverlayFS是Liunx发行版本里缺省使用的容器文件系统。

OverlayFS也是把多个目录合并挂载,被挂载的目录分为两大类:lowerdir和upperdir。

lowerdir允许有多个目录,在被挂载后,这些目录里的文件都是不会被修改或者删除,也就是只读的;upper只有一个,不过这个目录是可读写的,挂载点目录中的所有文件修改都会在upperdir中反映出来。

OverlayFS建立2个lowerdir目录,并且在目录中建立相同文件名的文件,然后一起做一个overlay mount,为将文件合并成为一个。

为了避免容器把宿主机的磁盘写满,对OverlayFS的upper目录做XFS Quota的限流。

docker run --storage-opt size=10M,就能限制容器OverlayFS文件系统可写入的最大数据量。

限制文件大小分为两步:

第一步:给目标目录打上一个Project ID;

第二步:为这个Project ID在XFS文件系统中,设置一个写入数据块的限制。

setProjectID()是调用ioctl(),setProjectQuota()调用quotactl()来修改内核中XFS的数据结构,从而完成project ID的设置和quota的设置。

如何判断是对那个目录做了限制:

根据/proc/mounts中容器的OverlayFS Mount信息,可以知道限制的目录/var/lib/docker2/<docker_id>,目录下的diff目录就是限制目录。

IOPS就是每秒钟磁盘读写的次数,这个数值越大,性能越好。

吞吐量是每秒钟磁盘中数据的读取量。

吞吐量 = 数据块大小 IOPS。

在Cgroup v1里,bulkio Cgroup的虚拟文件系统挂载点一半在/sys/fs/cgroup/blkio/。

Direct I/O模式,用户进程如果要写磁盘文件,就会通过Linux内核的文件系统层(fileSystem)-->块设备层(block layer)-->磁盘驱动-->磁盘硬件。

Buffer I/O模式,用户进程只是把文件写到内存中就返回,Linux内核自己有线程会被内存中的数据写入到磁盘中Cgroup v1 blkio的子系统独立于memory系统,无法统计到有Page Cache刷入到磁盘的数据量。Linux中绝大多数使用的是Buffered I/O模式。

Direct I/O可以通过blkio Cgroup来限制磁盘I/O。Cgroup V2从架构上允许一个控制组里只要同时有IO和Memory子系统,就可以对Buffered I/O做磁盘读写的限速。

dirty_backgroud_ratio和dirty_ratio,这两个值都是相对于节点可用内存的百分比值。

当dirty pages数量超过dirty_backgroud_ratio对应的内存量的时候,内核flush线程就会开始把dirty page写入磁盘;当dirty pages数量超过dirty_ratio对应的内存量,这时候程序写文件的函数调用write()就会被阻塞住,知道这次调用的dirty pages全部写入到磁盘。

在节点是大内存容量,并且dirty_ratio为系统缺省值为20%,dirty_backgroud_ratio是系统缺省值10%的情况下,通过观察/proc/vmstat中的nr_dirty数值可以发现,dirty pages不会阻塞进程的Buffered I/O写文件 *** 作。

修改网络参数的有两种方法:一种方法是直接到/proc文件系统下的/proc/sys/net目录对参数做修改;还有就是使用sysctl来修改。

创建新的network namespace的方法:系统调用clone()或者unshare()。

Network Namespace工具包:

runC也在对/proc/sys目录做read-only mount之前,预留出了修改接口,就是用来修改容器里/proc/sys下参数的,同样也是sysctl的参数。

在容器启动之前修改网络相关的内容,是可以的,如果启动之后,修改网络相关内容的是不生效的。

docker exec、kubectl exec、ip netns exec、nsenter等命令原理相同,都是基于setns系统调用,切换至指定的一个或多个namespace。

解决容器与外界通讯的问题,一共需要两步完成。

对于macvlan,每个虚拟网络接口都有自己独立的mac地址,而ipvlan的虚拟网络接口是和物理网络接口共享一个mac地址。

veth对外发送数据的时候,peer veth接口都会raise softirq来完成一次收报 *** 作,这样就会带来数据包处理的额外开销。

容器使用ipvlan/macvlan的网络接口,网络延时可以非常接近物理网络接口的延时。

对于需要使用iptables规则的容器,Kubernetes使用service的容器,就不能工作:

docker inspect lat-test-1 | jq[0]statePid

Linux capabilities就是把Linux root用户原来所有的特权做了细化,可以更加细粒度地给进程赋予不同权限。

Privileged的容器也就是允许容器中的进程可以执行所有的特权 *** 作。

容器中root用户的进程,系统也只允许了15个capabilities。

使用不同用户执行程序:

xfs quota功能

centos7 xfs 文件系统配置quota 用户磁盘配额

quota磁盘配额(xfs)

xfs_quota 磁盘配额

xfs_quota 磁盘配额限制篇

XFS文件系统中quota的使用

xfs文件系统quota

Linux学习—CentOS7磁盘配额工具quota

linux磁盘配额详解(EXT4和XFS)

Docker 特权模式(privileged mode)是一种高权限运行 Docker 容器的方式,可以让容器中的进程获得与宿主机相同的权限。如果您在使用 Docker 特权模式时遇到了问题,可能是以下原因导致的:

*** 作系统限制:在某些 *** 作系统中,使用 Docker 特权模式需要特殊的权限或配置。例如,在 Windows *** 作系统中运行 Docker 特权模式需要管理员权限。您可以检查自己的 *** 作系统版本和权限设置,确保可以运行 Docker 特权模式。

安全限制:Docker 特权模式可以让容器中的进程获得与宿主机相同的权限,可能会增加安全风险。因此,某些 Docker 环境默认禁止使用特权模式,需要手动开启。您可以检查自己的 Docker 版本和配置,确保可以使用 Docker 特权模式。

容器限制:某些 Docker 容器本身可能不支持特权模式,或者需要特殊的配置才能运行。您可以检查容器的文档或者联系容器的开发者,了解容器是否支持特权模式以及如何配置容器。

需要注意的是,Docker 特权模式可以让容器中的进程获得更高的权限,但同时也可能增加安全风险。建议仅在必要的情况下使用 Docker 特权模式,并加强容器的安全配置和管理。

以上就是关于微服务架构之「 容器技术 」全部的内容,包括:微服务架构之「 容器技术 」、Docker容器调试应用程序、docker中使用docker等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9742911.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-01
下一篇 2023-05-01

发表评论

登录后才能评论

评论列表(0条)

保存