Docker原理剖析--基本介绍

何为Docker?

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

Linux Namespace 介绍

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

名称 宏定义 隔离内容
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个系统调用:docker

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

Linux Namespace


UTS Namespace

UTS Namespace 主要用来隔离nodenamedomainname两个系统标识,每一个Namespace容许有本身的hostnameshell

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

// 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 Namespacedom

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)
复制代码

而后输出一下当前的PIDoop

# echo $$
2103
复制代码

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

# 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与宿主机之间的网络是处于隔离状态了

相关文章
相关标签/搜索