专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Docker原理剖析--基本介绍

何为Docker?

  • Docker 是一个开源工具,它可以将你的应用打包成一个标准格式的镜像(Image),并以容器的方式运行
  • Docker 将程序运行的所需要的一切:Code,ENV,System等等包装在一个完整的文件系统中
  • 所有容器会共享一个Kernel
|----------------|
|  CODE - Image  |
|----------------|
|  JRE - Image   |
|----------------|
|      KERNEL    |
|----------------|

Linux Namespace 介绍

Linux NamespaceKernel的一个功能,它提供了内核级别隔离系统资源的方法,通过将系统的全局资源放到不同的Namespace来实现隔离资源的目的.目前用到Namespace有:

名称 宏定义 隔离内容
IPC CLONE_NEWIPC 实现容器与宿主机、容器与容器之间的IPC隔离。IPC资源包括信号量、消息队列和共享内存
Network CLONE_NEWNET 提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙,套接字等
Mount CLONE_NEWNS 实现隔离文件系统挂载点,使容器内有独立的挂载文件系统
PID CLONE_NEWPID 实现容器内有独立的进程树 (也就意味着每个容器都有自己的PID为1的进程)
User CLONE_NEWUSER 实现用户可以将不同的主机用户映射到容器,比如user用户映射到容器内的root用户上
UTS CLONE_NEWUTS 实现容器可以拥有独立的主机名和域名,在网络上可以视为独立的节点
Cgroup CLONE_NEWCGROUP 实现资源的限制(CPU,Memory等等)

NamespaceAPI主要用到下面3个系统调用:

  • CLONE 创建新进程,并且系统调用参数会判断哪个类型的Namespace被创建(如上表格中的CLONE_NEWUSER),并且子进程也会包含到这些Namespace
  • UNSHARE 将进程移出某个Namespace
  • SETNS 将进程切换/加入到某个Namespace

Linux Namespace


UTS Namespace

UTS Namespace 主要用来隔离nodenamedomainname两个系统标识,每个Namespace允许有自己的hostname

下面使用将使用Go来做一个UTS Namespace的例子.

// UTS/clone.go

package main

import (
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags:syscall.CLONE_NEWUTS,
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run();err != nil{
        log.Fatal(err)
    }
}

解释下代码, exec.Command("sh")是来指定被fork出来的新进程内的初始命令,默认用sh来执行(当然可以换成bashzsh之类的)

下面就是设置系统调用参数,使用CLONE_NEWUTS这个标识符来创建一个UTS Namespace

syscall库封装好对clone()函数的调用,这段代码被执行后就会进入到一个sh运行环境中

执行go run clone.go的命令后,在这个交互式环境里面,使用pstree -pl查看下系统中进程之间的关系,如下:

init(1)───init(537)───init(538)───sh(539)───sh(540)───sh(545)───node(547)───node(597)──zsh(612)───go(2005)───clone(2098)───sh(2103)───pstree(2104)

然后输出一下当前的PID

# echo ?
2103

验证父进程和子进程是否不在同一个UTS Namespace

# readlink /proc/2098/ns/uts
uts:[4026532185]

# readlink /proc/2103/ns/uts
uts:[4026532195]

可以看到它们确实不在同一个UTS Namespace中,由于我们对进程的CLONE进了一个新的UTS Namespace内,所以这个环境下修改hostname/NIS domain name 对宿主主机没有任何影响,下面进行下实验

## 在clone出来的sh执行
# hostname -b air
# hostname
air

另外在宿主机另启动一个shell

root@DESKTOP-UMENNVI:~# hostname
DESKTOP-UMENNVI

可以看到,外部的hostname并没有被内部的修改所影响,由此可以了解到UTS Namespeace的作用


IPC Namespace

IPC Namespace用来隔离System V IPCPOSIX message queues.每一个IPC Namespace都有自己的System V IPCPOSIX message queue

创建IPC Namespace跟之前的方法类似,只需要把CLONE_NEWUTS替换成CLONE_NEWIPC

// IPC/clone.go
package main

import (
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags:syscall.CLONE_NEWIPC,
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

下面来演示以下隔离效果

1、 在宿主机上打开一个shell

root@DESKTOP-UMENNVI:~# ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息

1、 在宿主机创建一个message queue

root@DESKTOP-UMENNVI:~# ipcmk -Q
消息队列 id:0

# 查看消息队列
root@DESKTOP-UMENNVI:~# ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      
0x9dce743a 0          root       644        0            0 

1、 这里我们已经在宿主机上创建好一个queue了,下面将使用另一个shell去运行IPC/clone.go新建IPC Namespace

root@DESKTOP-UMENNVI:# go run clone.go 
# ipcs -q 

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息

可以发现,在新创建的Namespace里,看不到宿主机上已经创建的message queue,说明IPC Namespace创建成功,已经被隔离了


PID Namespace

PID Namespace是用来隔离进程ID(PID).同样在不同的PID Namespace里可以拥有不同的进程树.在docker container里面,使用ps -ef就会发现前台运行的进程PID1,那是因为每个容器都创建了独自的PID Namespace

基于上面的基础,把CLONE_NEWIPC替换成CLONE_NEWPID

// PID/clone.go
package main

import (
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags:syscall.CLONE_NEWPID,
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run();err != nil{
        log.Fatal(err)
    }
}

我们需要分别在宿主机和go run PID/clone.go内运行shell

1、 首先运行go run PID/clone.go查看shell的PID

root@DESKTOP-UMENNVI:# go run PID/clone.go 
# echo ?
1

1、 在宿主机上查看进程树,查看一下进程的真实PID

init(1)─┬─init(11)─┬─at-spi-bus-laun(294)─┬─dbus-daemon(307)
        ├─init(537)───init(538)───sh(539)───sh(540)───sh(545)───node(547)─┬─node(597)─┬─node(722)─┬─{node}(723)
        │                                                                 │           │           ├─{node}(724)
        │                                                                 │           │           ├─{node}(725)
        │                                                                 │           │           ├─{node}(726)
        │                                                                 │           │           ├─{node}(727)
        │                                                                 │           │           └─{node}(728)
        │                                                                 │           ├─zsh(610)───bash(3571)───pstree(4090)
        │                                                                 │           ├─zsh(612)───bash(3601)───go(3965)─┬─clone(4074)─┬─sh(4079)
        │                                                                 │           │                                  │             ├─{clone}(4075)
        │                                                                 │           │                                  │             ├─{clone}(4076)
        │                                                                 │           │                                  │             ├─{clone}(4077)
        │                                                                 │           │                                  │             └─{clone}(4078)
        │                                                                 │           │                                  ├─{go}(3966)

可以看到 go run PID/clone.go的真实PID应该是4074,也就是说这个4074被映射到PID Namespace里后为1. 当然这里还不能用ps/top来查看,因为会依赖/proc文件系统,还需要挂载/proc文件系统后才行.


Mount Namespace

Mount Namespace 用来隔离各个容器的挂载节点. 在不同的Namespace的容器中看到的文件系统层次是不一样的. 在Mount Namespace中调用mount()umount()仅仅只会影响当前Namespace内的文件系统,对全局文件系统没有影响

Mount NamespaceUnix & Linux第一个实现的Namespace类型,所以它的系统调用参数是NEWNS(New Namespace的缩写)

基于上面的基础,添加syscall.CLONE_NEWNS

// MOUNT/clone.go
package main

import (
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags:syscall.CLONE_NEWNS | syscall.CLONE_NEWPID,
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run();err != nil{
        log.Fatal(err)
    }
}

1、 运行代码go run MOUNT/clone.go 后,查看/proc的文件内容

proc 是一个(伪)文件系统,提供访问系统内核数据的操作提供接口

root@DESKTOP-UMENNVI:# go run MOUNT/clone.go 

# ls /proc
1    307   4553  545  acpi       crypto       interrupts  kmsg         modules       self         tty
11   309   4651  547  buddyinfo  devices      iomem       kpagecgroup  mounts        softirqs     uptime
192  339   4656  597  bus        diskstats    ioports     kpagecount   mtrr          stat         version
248  3571  4657  610  cgroups    dma          irq         kpageflags   net           swaps        vmallocinfo
272  4079  537   679  cmdline    driver       kallsyms    loadavg      pagetypeinfo  sys          vmstat
273  420   538   68   config.gz  execdomains  kcore       locks        partitions    sysvipc      zoneinfo
294  4315  539   69   consoles   filesystems  keys        meminfo      sched_debug   thread-self
299  4374  540   722  cpuinfo    fs           key-users   misc         schedstat     timer_list

可以看到这里的/proc还是宿主机的,下面将/proc mount到新建的Namespace中来

# mount -t proc proc /proc
# ls /proc
1          cmdline    diskstats    interrupts  keys         loadavg  mtrr          self      thread-self  vmstat
5          config.gz  dma          iomem       key-users    locks    net           softirqs  timer_list   zoneinfo
acpi       consoles   driver       ioports     kmsg         meminfo  pagetypeinfo  stat      tty
buddyinfo  cpuinfo    execdomains  irq         kpagecgroup  misc     partitions    swaps     uptime
bus        crypto     filesystems  kallsyms    kpagecount   modules  sched_debug   sys       version
cgroups    devices    fs           kcore       kpageflags   mounts   schedstat     sysvipc   vmallocinfo

可以看到少了很多文件,现在用top/ps来查看系统进程

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 14:47 pts/5    00:00:00 sh
root         6     1  0 14:58 pts/5    00:00:00 ps -ef

在当前的Namespace中,sh进程的PID1的进程,说明Mount && PID Namespace和宿主机是隔离的,mount操作没有影响到宿主机 Docker Volume 也是利用了这个特性(还有USER Namespace)


User Namespace

User Namespace 主要用来隔离用户&用户组ID. 一个进程的User IDGroup IDUser Namespace内外可以是不同的,比较常见的是在宿主机上以一个非root用户创建一个User Namespace,然后在User Namespace里面却映射成root用户 从Linux Kernel 3.8开始,非root进程也可以创建User Namespace,并且此用户在创建的Namespace里面可以被映射成root并且拥有root权限

具体代码如下:

// USER/clone.go
package main

import (
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUSER,
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run();err != nil{
        log.Fatal(err)
    }
}

1、 以root用户在宿主机上看一下当前的用户和用户组

root@DESKTOP-UMENNVI:# id
uid=0(root) gid=0(root) 组=0(root)

可以看到我们是root用户,接下来运行程序

root@DESKTOP-UMENNVI:# go run USER/clone.go 
$ id
uid=65534(nobody) gid=65534(nogroup) 组=65534(nogroup)

可以看到它们的UID是不同的说明User Namespace创建&&隔离完成了


Network Namespace

Network Namespace 是用来隔离网络设备,IPV4/IPV6等网络栈的Namespace Network Namespace可以让每个容器拥有独立的虚拟网络设备,并且容器内的应用可以绑定到容器内的端口,每个Namespace内的端口都不会互相冲突 在宿主机上搭建网桥后,就能很方便地实现容器之间的通信

在上面的基础上改成syscall.CLONE_NEWNET

package main

import (
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags:syscall.CLONE_NEWNET,
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    if err := cmd.Run();err != nil{
        log.Fatal(err)
    }
}

1、 首先在宿主机上查看一下自己的网络设备

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.25.50.16  netmask 255.255.240.0  broadcast 172.25.63.255
        inet6 fe80::215:5dff:fe50:5608  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:50:56:08  txqueuelen 1000  (以太网)
        RX packets 766496  bytes 148694705 (148.6 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 688919  bytes 3273950212 (3.2 GB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (本地环回)
        RX packets 2  bytes 100 (100.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2  bytes 100 (100.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

可以宿主机上有eth0,lo等网络设备

1、 运行程序查看网络设备 go run NET/clone.go

root@DESKTOP-UMENNVI:# go run NET/clone.go 
# ifconfig
# 

可以看到网络设备什么都没有,因为Network Namespace与宿主机之间的网络是处于隔离状态了

文章永久链接:https://tech.souyunku.com/44172

未经允许不得转载:搜云库技术团队 » Docker原理剖析--基本介绍

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们