前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >五分钟学K8S系列<二> - docker 容器的本质

五分钟学K8S系列<二> - docker 容器的本质

原创
作者头像
五分钟学SRE
发布2024-04-30 02:02:22
1620
发布2024-04-30 02:02:22
举报
文章被收录于专栏:五分钟学K8S五分钟学K8S

上一篇我们从linux 容器的诞生,与架构对docker 有了初步的了解,这个篇章我们将透过现象看本质,深入的探索Linux容器化与Docker 技术 的原理与本质。

    Docker 是一种流行的容器化平台,它利用 Linux 内核中的 cgroups 和 namespaces 特性实现了轻量级的容器隔离。下面将详细介绍 Docker 的底层实现原理,并深入的看看探索其中使用到的三个系统调用与容器隔离的关系。

图片
图片

Docker 的底层实现原理主要涉及以下三个方面

cgroups

图片
图片

    cgroups(Control Groups)是 Linux 内核中的一种特性,它可以将进程分组并限制它们对系统资源(如 CPU、内存、磁盘和网络)的使用。Docker 使用 cgroups 来实现容器的资源隔离和限制,例如限制容器可以使用的 CPU 核心数量和内存大小。

    在 Linux 系统中,cgroups 的配置存放在 /sys/fs/cgroup 目录下。其中,每个子目录都对应一个 cgroup,可以对其进行资源控制。以下是一些重要的配置文件:

代码语言:javascript
复制
cpu.cfs_quota_us:CPU 时间配额,以微秒为单位。如果值为 -1,则表示没有限制。cpu.cfs_period_us:CPU 时间周期,以微秒为单位。该值确定了 CPU 时间的单位。memory.limit_in_bytes:内存限制,以字节为单位。如果值为 -1,则表示没有限制。blkio.weight:块设备 IO 权重,范围为 10-1000。

namespaces

图片
图片

    namespaces 是 Linux 内核中的一种特性(进程隔离技术),它可以将全局系统资源抽象为一组本地资源。在 Docker 中,通过使用不同的 namespaces,可以实现容器的隔离。例如,PID 命名空间可以使容器只能看到自己内部的进程,网络命名空间可以使容器拥有自己的网络接口和 IP 地址,与主机网络隔离。

    在 Linux 系统中,namespaces 的配置存放在 /proc/[pid]/ns 目录下,其中 [pid] 为进程 ID。以下是一些重要的配置文件:

代码语言:javascript
复制
ipc:进程间通信隔离,包括信号量、消息队列和共享内存等。uts:主机名和域名隔离。net:网络隔离,包括 IP 地址、网络接口、路由表等。pid:进程隔离,每个容器内的进程只能看到该容器内的进程。mnt:文件系统隔离,每个容器有自己的文件系统视图。

联合文件系统

图片
图片

    Docker 使用联合文件系统(UnionFS)实现镜像的分层存储。UnionFS 是一种文件系统堆叠技术,它允许多个文件系统层透明地合并为一个虚拟文件系统。Docker 的联合文件系统由多个镜像层组成,每个镜像层都可以看作一个只读的文件系统,只有最上层的可读写文件系统可以进行写操作。

    联合文件系统的配置信息存放在容器的元数据中,包括镜像层的 ID、镜像层的挂载路径等。Docker 会在运行容器时,将不同镜像层的文件系统堆叠在一起,形成一个完整的文件系统。

    容器的文件系统路径为 /var/lib/docker/overlay2/<container-id>/merged。

三个系统调用与容器隔离的关系

clone()

    clone() 系统调用可以创建一个新的进程,并可以通过设计参数来达到隔离的效果。在 Docker 中,每个容器都是一个独立的进程,可以拥有自己的文件系统、网络接口、进程空间和用户空间等。因此,在 Docker 中,clone() 系统调用被用于创建新的容器进程。

    使用 clone() 系统调用时,可以通过指定不同的参数来实现不同程度的隔离,例如:

代码语言:javascript
复制
CLONE_NEWUTS:创建一个新的 UTS 命名空间,用于隔离主机的主机名和域名;CLONE_NEWIPC:创建一个新的 IPC 命名空间,用于隔离进程间通信(IPC)机制,例如信号量、共享内存和消息队列;CLONE_NEWPID:创建一个新的 PID 命名空间,用于隔离进程 ID;CLONE_NEWNET:创建一个新的网络命名空间,用于隔离容器的网络接口和 IP 地址;CLONE_NEWUSER:创建一个新的用户命名空间,用于隔离用户和用户组的 ID。

unshare()

    unshare() 系统调用可以将进程从主机命名空间中分离出来,并创建一个新的命名空间,使得容器拥有自己独立的命名空间。在 Docker 中,unshare() 系统调用被用于创建新的命名空间,并将容器进程与主机进程分离开来,以实现容器的隔离。这个命名空间将成为容器进程的根命名空间,容器进程只能访问该命名空间下的资源。在这个命名空间中,容器进程将看到自己的文件系统、进程空间和网络接口等,而不是主机上的资源。

setns()

    setns() 系统调用可以将进程加入到特定的命名空间中。在 Docker 中,当容器需要访问主机网络时,可以使用 setns() 系统调用将进程切换到主机网络命名空间中,并访问主机网络资源。因此,setns() 系统调用是 Docker 实现容器与主机网络之间通信的的关键系统调用。

Docker 的工作流程

在 Docker 中,容器的创建和运行可以分为以下四个步骤:

镜像获取

    Docker 的镜像是一个文件系统层的集合,包含了应用程序运行所需的所有组件。在创建容器之前,需要先获取容器的基础镜像,这可以通过从 Docker Hub 中下载镜像,或者使用本地构建的镜像。

容器创建

    使用 Docker 命令行工具(CLI)创建容器时,Docker 首先会调用 clone() 系统调用来创建一个新的进程。该进程将拥有自己的文件系统、进程空间、网络接口等,与主机和其他容器隔离。

示例代码

代码语言:javascript
复制
package main
import (    "fmt"    "os"    "syscall")
func main() {    cmd := "/bin/bash"    args := []string{"bash"}
    cloneFlags := syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC    // 使用 clone() 系统调用创建子进程    pid, err := syscall.Clone(func() {        // 子进程中运行指定命令        err := syscall.Exec(cmd, args, os.Environ())        if err != nil {            panic(err)        }    }, nil, cloneFlags)    if err != nil {        panic(err)    }
    fmt.Printf("child process ID: %d\n", pid)
    // 等待子进程结束    var status syscall.WaitStatus    _, err = syscall.Wait4(pid, &status, 0, nil)    if err != nil {        panic(err)    }    fmt.Printf("child process exit status: %d\n", status.ExitStatus())}

容器启动

    在容器创建完成后,Docker 会使用 unshare() 系统调用创建新的命名空间,并将容器进程与主机进程隔离开来。然后,Docker 会将容器进程切换到容器的网络命名空间中,以获得独立的网络接口和 IP 地址。

示例代码

代码语言:javascript
复制
package main
import (    "fmt"    "syscall")
func main() {    // 创建新的 UTS 命名空间    err := syscall.Unshare(syscall.CLONE_NEWUTS)    if err != nil {        panic(err)    }
    // 获取当前主机的 hostname    hostname, err := syscall.Gethostname()    if err != nil {        panic(err)    }    fmt.Printf("hostname in current namespace: %s\n", hostname)
    // 修改当前命名空间中的 hostname    err = syscall.Sethostname([]byte("newhostname"))    if err != nil {        panic(err)    }
    // 再次获取当前主机的 hostname    hostname, err = syscall.Gethostname()    if err != nil {        panic(err)    }    fmt.Printf("hostname in new namespace: %s\n", hostname)}

容器运行

    在容器启动后,Docker 会使用 setns() 系统调用将容器进程切换到相应的命名空间中,并将容器进程加入到相应的 cgroups 中,以限制容器使用系统资源。此外,Docker 还会挂载容器的文件系统层,并启动容器中定义的应用程序。

示例代码

代码语言:javascript
复制
package main
import (    "fmt"    "os"    "syscall")
func main() {    pid := os.Getpid()
    // 打开指定进程的 PID namespace    fd, err := syscall.Open(fmt.Sprintf("/proc/%d/ns/pid", pid), syscall.O_RDONLY, 0)    if err != nil {        panic(err)    }    defer syscall.Close(fd)
    // 加入指定进程的 PID namespace    err = syscall.Setns(fd, syscall.CLONE_NEWPID)    if err != nil {        panic(err)    }
    // 获取当前进程的 PID    pid = os.Getpid()    fmt.Printf("PID in new namespace: %d\n", pid)}

syscall 相关的知识可以到五分钟学GO的连载学习。

Docker 利用 Linux 内核中的 cgroups 和 namespaces 特性实现了轻量级的容器隔离。在 Docker 中,clone() 系统调用被用于创建新的容器进程,unshare() 系统调用被用于创建新的命名空间,以实现容器的隔离,而 setns() 系统调用被用于将容器进程切换到相应的命名空间中。通过这些系统调用,Docker 可以在容器和主机之间实现高度的隔离和安全性,从而提供可移植和可重复的应用程序环境。

   在实际应用中,容器化技术可以大大简化应用程序的部署和管理。例如,你可以使用Docker来打包你的应用程序及其所有依赖项,然后使用Kubernetes来管理这些容器化的应用程序。这种方式可以让你的应用程序在不同环境中无缝运行,并且可以轻松地进行扩展和维护。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 三个系统调用与容器隔离的关系
  • Docker 的工作流程
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档