文章首发于我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

当容器逐步向容器集群,容器云技术演进的时候,一个不得不面对的问题就是各个容器的管理问题,有些容器需要交互,有些容器需要隔离,如何保证这些操作能够顺利地进行,这个时候,很多容器管理和编排的平台就应运而生了。首先,当然是 Docker 社区自己开发的 Swarm+Machine+Compose 的集群管理套件,然后还有 Twitter 主推 Apache 的 Mesos,最有名的应该为 Google 开源的 Kubernetes。

这些平台综合了容器集群资源管理,服务发现和扩容缩融等问题,是一个集大成的解决方案,但其实这些问题本质上都离不开网络,资源在各容器之间调度需要网络,服务发现需要网络,所以,可以说网络是容器集群环境下最基础的一环。

Docker 容器网络根据容器的部署位置,可以分为单主机网络(host)和多主机网络(multi-host),本文先看 Docker host 网络。

Docker host 网络分为 none,host,joined container 和 bridge 网络。

none 网络模式下,Docker 容器拥有自己的 network namespace,但是并不为容器进行任何的网络配置,也就是说容器除了 network namespace 本身自带的 localback 网卡外什么都没有,包括网卡、IP、路由等信息。用户如何要使用 none 网络,就需要自己添加特定的网卡,并配置 IP、路由等信息,但一般不会这么干,none 网络很好地做到了隔离,一般是用来跑那些对安全性要求极高且不需要联网的应用。

比如某个容器的唯一用途是生成随机密码,就可以放到 none 网络中避免密码被窃取。

想要使 Docker 使用 none 网络,只需要在创建容器的时候附带 –network = none 即可。

host 网络,顾名思义就是 Docker 容器使用宿主机的网络,相当于和 host 共用了同一个 network namespace,Docker 容器使用 host 的网卡、IP 和路由等功能对外通信。

虽然这种模式下 Docker 没有创建独立的 network namespace,但其他 namespace 仍然是隔离的,如文件系统、进程列表等。host 网络最大的好处就是使得 Docker 容器对外通信变得简单,直接使用 host 的 IP 进行通信即可,但缺点也很明显,就是降低了隔离性,同时还会存在和其他容器对网络资源的竞争与冲突的问题。

同样要使用 host 网络,只需创建容器时附带 –network = host 即可。

joined container 网络和 host 网络不同的是,它是和其他的 container 共享一个 network namespace,一个 network namespace 可以被一个或者多个 Docker 容器共享。在这种模式下,两个容器之间可以通过 localback 回环网卡通信,增加了容器间通信的便利性。

同样可以在创建容器时,使用参数 –network = container:another_container_name 来和另外容器共享网络。

bridge 网络是最常用的网络模式,它兼顾了安全性和功能的完备性,但其与外部通信要通过 NAT 转换,在复杂的网络场景下会存在诸多不便。

bridge 网络在创建的时候通过 –network = bridge 指定,Docker daemon 会为创建的容器自动创建一个 Docker 网桥——docker0(也可以人为指定名称 –driver bridge my_net),这个 docker0 就用来联结 Docker 容器和 host 的桥梁。

然后,Docker daemon 会创建一对虚拟网卡 veth pair,一块网卡留在宿主机的 root network namespace 中,并绑定到 docker0 上,另一块放在新创建的 network namespace 中,命名为 eth0,并为其配置 IP 和路由,将其网关设为 docker0,如下,这样整个容器的 bridge 网络通信环境就建立起来了,其他容器的建立方式也是如此,最终各容器之间就通过 docker0 这个桥梁互联互通了。

下文将讨论更为复杂的 multi-host 网络。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发于我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

前面两篇文章我们总结了 Docker 背后使用的资源隔离技术 Linux namespace,本篇将讨论另外一个技术——资源限额,这是由 Linux cgroups 来实现的。

cgroups 是 Linux 内核提供的一种机制,这种机制可以根据需求把一系列任务及子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。(来自 《Docker 容器与容器云》)

通俗来说,cgroups 可以限制和记录任务组(进程组或线程组)使用的物理资源(包括 CPU、内存、IO 等)。

为了方便用户(程序员)操作,cgroups 以一个伪文件系统的方式实现,并对外提供 API,用户对文件系统的操作就是对 cgroups 的操作。

从实现上来,cgroups 实际上是给每个执行任务挂了一个钩子,当任务执行过程中涉及到对资源的分配使用时,就会触发钩子上的函数对相应的资源进行检测,从而对资源进行限制和优先级分配。

cgroups 的作用


总结下来,cgroups 提供以下四个功能:

资源限制: cgroups 可以对任务使用的资源总额进行限制,如设定应用运行时使用内存的上限,一旦超过这个配额就发出 OOM(Out of Memory)提示。

优先级分配: 通过分配的 CPU 时间片数量和磁盘 IO 带宽大小,实际上就相当于控制了任务运行的优先级。

资源统计: cgroups 可以统计系统的资源使用,如 CPU 使用时长、内存用量等,这个功能非常适用于计费。

任务控制: cgroups 可以对任务执行挂起、恢复等操作。

cgroups 的子系统


cgroups 在设计时根据不同的资源类别分为不同的子系统,一个子系统本质上是一个资源控制器,比如 CPU 资源对应 CPU 子系统,负责控制 CPU 时间片的分配,内存对应内存子系统,负责限制内存的使用量。进一步,一个子系统或多个子系统可以组成一个 cgroup,cgroups 中的资源控制都是以 cgroup 为单位来实现,一个任务(或进程或线程)可以加入某个 cgroup,也可以从一个 cgroup 移动到另一个 cgroup,但这里有一些限制,在此就不再赘述了,详细查阅相关资料了解。

对于我们来说,最关键的是知道怎么用,下面就针对 CPU、内存和 IO 资源来看 Docker 是如何使用的?

对于 CPU,Docker 使用参数 -c 或 –cpu-shares 来设置一个容器使用的 CPU 权重,权重的大小也影响了 CPU 使用的优先级。

如下,启动两个容器,并分配不同的 CPU 权重,最终 CPU 使用率情况:

1
2
docker run --name "container_A" -c 1024 ubuntu
docker run --name "container_B" -c 512 ubuntu

当只有一个容器时,即使指定较少的 CPU 权重,它也会占满整个 CPU,说明这个权重只是相对权重,如下将上面的 “container_A” 停止,“container_B” 就分配到全部可用的 CPU。

对于内存,Docker 使用 -m(设置内存的限额)和 –memory-swap(设置内存和 swap 的限额)来控制容器内存的使用量,如下,给容器限制 200M 的内存和 100M 的 swap,然后给容器内的一个工作线程分配 280M 的内存,因为 280M 在容许的 300M 范围内,没有问题。其内存分配过程是不断分配又释放,如下:

如果让工作线程使用内存超过 300M,则出现内存超限的错误,容器退出,如下:

对于 IO 资源,其使用方式与 CPU 一样,使用 –blkio-weight 来设置其使用权重,IO 衡量的两个指标是 bps(byte per second,每秒读写的数据量) 和 iops(io per second, 每秒 IO 的次数),实际使用,一般使用这两个指标来衡量 IO 读写的带宽,几种使用参数如下:

  • –device-read-bps,限制读某个设备的 bps。
  • –device-write-bps,限制写某个设备的 bps。
  • –device-read-iops,限制读某个设备的 iops。
  • –device-write-iops,限制写某个设备的 iops。

假如限制容器对其文件系统 /dev/sda 的 bps 写速率为 30MB/s,则在容器中用 dd 测试其写磁盘的速率如下,可见小于 30MB/s。

如果是正常情况下,我的机器可以达到 56.7MB/s,一般都是超 1G 的。

上面几个资源使用限制的例子,本质上都是调用了 Linux kernel 的 cgroups 机制来实现的,每个容器创建后,Linux 会为每个容器创建一个 cgroup 目录,以容器的 ID 命名,目录在 /sys/fs/cgroup/ 中,针对上面的 CPU 资源限制的例子,我们可以在 /sys/fs/cgroup/cpu/docker 中看到相关信息,如下:

其中,cpu.shares 中保存的就是限制的数值,其他还有很多项,感兴趣可以动手实验看看。

总结


cgroups 的作用,cgroups 的实现,cgroups 的子系统机制,CPU、内存和 IO 的使用方式,以及对应 Linux 的 cgroups 文件目录。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发于我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

上篇我们从进程 clone 的角度,结合代码简单分析了 Linux 提供的 6 种 namespace,本篇从源码上进一步分析 Linux namespace,让你对 Docker namespace 的隔离机制有更深的认识。我用的是 Linux-4.1.19 的版本,由于 namespace 模块更新都比较少,所以,只要 3.0 以上的版本都是差不多的。

从内核进程描述符 task_struct 开始切入


由于 Linux namespace 是用来做进程资源隔离的,所以在进程描述符中,一定有 namespace 所对应的信息,我们可以从这里开始切入代码。

首先找到描述进程信息 task_struct,找到指向 namespace 的结构 struct *nsproxy(sched.h):

1
2
3
4
5
6
struct task_struct {
......
/* namespaces */
struct nsproxy *nsproxy;
......
}

其中 nsproxy 结构体定义在 nsproxy.h 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
*
* 'count' is the number of tasks holding a reference.
* The count for each namespace, then, will be the number
* of nsproxies pointing to it, not the number of tasks.
*
* The nsproxy is shared by tasks which share all namespaces.
* As soon as a single namespace is cloned or unshared, the
* nsproxy is copied.
*/
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns;
struct net *net_ns;
};
extern struct nsproxy init_nsproxy;

这个结构是被所有 namespace 所共享的,只要一个 namespace 被 clone 了,nsproxy 也会被 clone。注意到,由于 user namespace 是和其他 namespace 耦合在一起的,所以没出现在上述结构中。

同时,nsproxy.h 中还定义了一些对 namespace 的操作,包括 copy_namespaces 等。

1
2
3
4
5
6
int copy_namespaces(unsigned long flags, struct task_struct *tsk);
void exit_task_namespaces(struct task_struct *tsk);
void switch_task_namespaces(struct task_struct *tsk, struct nsproxy *new);
void free_nsproxy(struct nsproxy *ns);
int unshare_nsproxy_namespaces(unsigned long, struct nsproxy **,
struct fs_struct *);

task_struct,nsproxy,几种 namespace 之间的关系如下所示:

各个 namespace 的初始化


在各个 namespace 结构定义下都有个 init 函数,nsproxy 也有个 init_nsproxy 函数,init_nsproxy 在 task 初始化的时候会被初始化,附带的,init_nsproxy 中定义了各个 namespace 的 init 函数,如下:
在 init_task 函数中(init_task.h):

1
2
3
4
5
6
7
8
9
10
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x1fffff (=2MB)
*/
#define INIT_TASK(tsk) \
{
......
.nsproxy = &init_nsproxy,
......
}

继续跟进 init_nsproxy,在 nsproxy.c 中:

1
2
3
4
5
6
7
8
9
10
11
12
struct nsproxy init_nsproxy = {
.count = ATOMIC_INIT(1),
.uts_ns = &init_uts_ns,
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
.ipc_ns = &init_ipc_ns,
#endif
.mnt_ns = NULL,
.pid_ns_for_children = &init_pid_ns,
#ifdef CONFIG_NET
.net_ns = &init_net,
#endif
};

可见,init_nsproxy 中,对 uts, ipc, pid, net 都进行了初始化,但 mount 却没有。

创建新的 namespace


初始化完之后,下面看看如何创建一个新的 namespace,通过前面的文章,我们知道是通过 clone 函数来完成的,在 Linux kernel 中,fork/vfork() 对 clone 进行了封装,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL);
}
#endif

#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif

可以看到,无论是 fork() 还是 vfork(),最终都会调用到 do_fork() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
// 创建进程描述符指针
struct task_struct *p;
int trace = 0;
long nr;

/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;

if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}

// 复制进程描述符,返回值是 task_struct
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;

trace_sched_process_fork(current, p);

// 得到新进程描述符的 pid
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);

if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);

// 调用 vfork() 方法,完成相关的初始化工作
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}

// 将新进程加入到调度器中,为其分配 CPU,准备执行
wake_up_new_task(p);

// fork() 完成,子进程开始运行,并让 ptrace 跟踪
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);

// 如果是 vfork(),将父进程加入等待队列,等待子进程完成
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}

put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}

do_fork() 首先调用 copy_process 将父进程信息复制给子进程,然后调用 vfork() 完成相关的初始化工作,接着调用 wake_up_new_task() 将进程加入调度器中,为之分配 CPU。最后,等待子进程退出。

copy_process():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
// 创建进程描述符指针
struct task_struct *p;

// 检查 clone flags 的合法性,比如 CLONE_NEWNS 与 CLONE_FS 是互斥的
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);

if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
return ERR_PTR(-EINVAL);

/*
* Thread groups must share signals as well, and detached threads
* can only be started up within the thread group.
*/
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);

/*
* Shared signal handlers imply shared VM. By way of the above,
* thread groups also imply shared VM. Blocking this case allows
* for various simplifications in other code.
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);

/*
* Siblings of global init remain as zombies on exit since they are
* not reaped by their parent (swapper). To solve this and to avoid
* multi-rooted process trees, prevent global and container-inits
* from creating siblings.
*/
// 比如CLONE_PARENT时得检查当前signal flags是否为SIGNAL_UNKILLABLE,防止kill init进程。
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);

/*
* If the new process will be in a different pid or user namespace
* do not allow it to share a thread group or signal handlers or
* parent with the forking task.
*/
if (clone_flags & CLONE_SIGHAND) {
if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
(task_active_pid_ns(current) !=
current->nsproxy->pid_ns_for_children))
return ERR_PTR(-EINVAL);
}

retval = security_task_create(clone_flags);
if (retval)
goto fork_out;

retval = -ENOMEM;
// 复制当前的 task_struct
p = dup_task_struct(current);
if (!p)
goto fork_out;

ftrace_graph_init_task(p);

rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
retval = -EAGAIN;

// 检查进程是否超过限制,由 OS 定义
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
current->flags &= ~PF_NPROC_EXCEEDED;

retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;

/*
* If multiple threads are within copy_process(), then this check
* triggers too late. This doesn't hurt, the check is only there
* to stop root fork bombs.
*/
retval = -EAGAIN;
// 检查进程数是否超过 max_threads,由内存大小定义
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;

// ......

// 初始化 io 计数器
task_io_accounting_init(&p->ioac);
acct_clear_integrals(p);

// 初始化 CPU 定时器
posix_cpu_timers_init(p);

// ......

// 初始化进程数据结构,并为进程分配 CPU,进程状态设置为 TASK_RUNNING
/* Perform scheduler related setup. Assign this task to a CPU. */
retval = sched_fork(clone_flags, p);

if (retval)
goto bad_fork_cleanup_policy;

retval = perf_event_init_task(p);
if (retval)
goto bad_fork_cleanup_policy;
retval = audit_alloc(p);
if (retval)
goto bad_fork_cleanup_perf;
/* copy all the process information */
// 复制所有进程信息,包括文件系统,信号处理函数、信号、内存管理等
shm_init_task(p);
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_files(clone_flags, p);
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
// !!! 复制 namespace
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
// 初始化子进程内核栈
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
goto bad_fork_cleanup_io;
// 为新进程分配新的 pid
if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_io;
}
}

// ......

// 返回新进程 p
return p;
}

copy_process 主要分为三步:首先调用 dup_task_struct() 复制当前的进程描述符信息 task_struct,为新进程分配新的堆栈,第二步调用 sched_fork() 初始化进程数据结构,为其分配 CPU,把进程状态设置为 TASK_RUNNING,最后一步就是调用 copy_namespaces() 复制 namesapces。我们重点关注最后一步 copy_namespaces():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* called from clone. This now handles copy for nsproxy and all
* namespaces therein.
*/
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{
struct nsproxy *old_ns = tsk->nsproxy;
struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
struct nsproxy *new_ns;

if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
CLONE_NEWPID | CLONE_NEWNET)))) {
get_nsproxy(old_ns);
return 0;
}

if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;

/*
* CLONE_NEWIPC must detach from the undolist: after switching
* to a new ipc namespace, the semaphore arrays from the old
* namespace are unreachable. In clone parlance, CLONE_SYSVSEM
* means share undolist with parent, so we must forbid using
* it along with CLONE_NEWIPC.
*/
if ((flags & (CLONE_NEWIPC | CLONE_SYSVSEM)) ==
(CLONE_NEWIPC | CLONE_SYSVSEM))
return -EINVAL;

new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs);
if (IS_ERR(new_ns))
return PTR_ERR(new_ns);

tsk->nsproxy = new_ns;
return 0;
}

可见,copy_namespace() 主要基于“旧的” namespace 创建“新的” namespace,核心函数在于 create_new_namespaces:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
* Create new nsproxy and all of its the associated namespaces.
* Return the newly created nsproxy. Do not attach this to the task,
* leave it to the caller to do proper locking and attach it to task.
*/
static struct nsproxy *create_new_namespaces(unsigned long flags,
struct task_struct *tsk, struct user_namespace *user_ns,
struct fs_struct *new_fs)
{
struct nsproxy *new_nsp;
int err;

// 创建新的 nsproxy
new_nsp = create_nsproxy();
if (!new_nsp)
return ERR_PTR(-ENOMEM);

//创建 mnt namespace
new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);
if (IS_ERR(new_nsp->mnt_ns)) {
err = PTR_ERR(new_nsp->mnt_ns);
goto out_ns;
}
//创建 uts namespace
new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);
if (IS_ERR(new_nsp->uts_ns)) {
err = PTR_ERR(new_nsp->uts_ns);
goto out_uts;
}
//创建 ipc namespace
new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);
if (IS_ERR(new_nsp->ipc_ns)) {
err = PTR_ERR(new_nsp->ipc_ns);
goto out_ipc;
}
//创建 pid namespace
new_nsp->pid_ns_for_children =
copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);
if (IS_ERR(new_nsp->pid_ns_for_children)) {
err = PTR_ERR(new_nsp->pid_ns_for_children);
goto out_pid;
}
//创建 network namespace
new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);
if (IS_ERR(new_nsp->net_ns)) {
err = PTR_ERR(new_nsp->net_ns);
goto out_net;
}

return new_nsp;
// 出错处理
out_net:
if (new_nsp->pid_ns_for_children)
put_pid_ns(new_nsp->pid_ns_for_children);
out_pid:
if (new_nsp->ipc_ns)
put_ipc_ns(new_nsp->ipc_ns);
out_ipc:
if (new_nsp->uts_ns)
put_uts_ns(new_nsp->uts_ns);
out_uts:
if (new_nsp->mnt_ns)
put_mnt_ns(new_nsp->mnt_ns);
out_ns:
kmem_cache_free(nsproxy_cachep, new_nsp);
return ERR_PTR(err);
}

在create_new_namespaces()中,分别调用 create_nsproxy(), create_utsname(), create_ipcs(), create_pid_ns(), create_net_ns(), create_mnt_ns() 来创建 nsproxy 结构,uts,ipcs,pid,mnt,net。

具体的函数我们就不再分析,基本到此为止,我们从子进程创建,到子进程相关的信息的初始化,包括文件系统,CPU,内存管理等,再到各个 namespace 的创建,都走了一遍,下面附上 namespace 创建的代码流程图。

mnt namespace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
mnt namespace:
struct mnt_namespace *copy_mnt_ns(unsigned long flags, struct mnt_namespace *ns,
struct user_namespace *user_ns, struct fs_struct *new_fs)
{
struct mnt_namespace *new_ns;
struct vfsmount *rootmnt = NULL, *pwdmnt = NULL;
struct mount *p, *q;
struct mount *old;
struct mount *new;
int copy_flags;

BUG_ON(!ns);

if (likely(!(flags & CLONE_NEWNS))) {
get_mnt_ns(ns);
return ns;
}

old = ns->root;
// 分配新的 mnt namespace
new_ns = alloc_mnt_ns(user_ns);
if (IS_ERR(new_ns))
return new_ns;

namespace_lock();
/* First pass: copy the tree topology */
// 首先 copy root 路径
copy_flags = CL_COPY_UNBINDABLE | CL_EXPIRE;
if (user_ns != ns->user_ns)
copy_flags |= CL_SHARED_TO_SLAVE | CL_UNPRIVILEGED;
new = copy_tree(old, old->mnt.mnt_root, copy_flags);
if (IS_ERR(new)) {
namespace_unlock();
free_mnt_ns(new_ns);
return ERR_CAST(new);
}
new_ns->root = new;
list_add_tail(&new_ns->list, &new->mnt_list);

/*
* Second pass: switch the tsk->fs->* elements and mark new vfsmounts
* as belonging to new namespace. We have already acquired a private
* fs_struct, so tsk->fs->lock is not needed.
*/
// 为新进程设置 fs 信息
p = old;
q = new;
while (p) {
q->mnt_ns = new_ns;
if (new_fs) {
if (&p->mnt == new_fs->root.mnt) {
new_fs->root.mnt = mntget(&q->mnt);
rootmnt = &p->mnt;
}
if (&p->mnt == new_fs->pwd.mnt) {
new_fs->pwd.mnt = mntget(&q->mnt);
pwdmnt = &p->mnt;
}
}
p = next_mnt(p, old);
q = next_mnt(q, new);
if (!q)
break;
while (p->mnt.mnt_root != q->mnt.mnt_root)
p = next_mnt(p, old);
}
namespace_unlock();

if (rootmnt)
mntput(rootmnt);
if (pwdmnt)
mntput(pwdmnt);

return new_ns;
}

可以看到,mount namespace 在新建时会新建一个新的 namespace,然后将父进程的 namespace 拷贝过来,并将 mount->mnt_ns 指向新的 namespace。接着设置进程的 root 路径以及当前路径到新的 namespace,然后为新进程设置新的 vfs 等。从这里就可以看出,在子进程中进行 mount 操作不会影响到父进程中的 mount 信息。

uts namespace:

1
2
3
4
5
6
7
8
static inline struct uts_namespace *copy_utsname(unsigned long flags,
struct user_namespace *user_ns, struct uts_namespace *old_ns)
{
if (flags & CLONE_NEWUTS)
return ERR_PTR(-EINVAL);

return old_ns;
}

uts namespace 直接返回父进程 namespace 信息。

ipc namespace:

1
2
3
4
5
6
7
struct ipc_namespace *copy_ipcs(unsigned long flags,
struct user_namespace *user_ns, struct ipc_namespace *ns)
{
if (!(flags & CLONE_NEWIPC))
return get_ipc_ns(ns);
return create_ipc_ns(user_ns, ns);
}

ipc namespace 如果是设置了参数 CLONE_NEWIPC,则直接返回父进程的 namespace,否则返回新创建的 namespace。

pid namespace:

1
2
3
4
5
6
7
static inline struct pid_namespace *copy_pid_ns(unsigned long flags,
struct user_namespace *user_ns, struct pid_namespace *ns)
{
if (flags & CLONE_NEWPID)
ns = ERR_PTR(-EINVAL);
return ns;
}

pid namespace 直接返回父进程的 namespace。

net namespace

1
2
3
4
5
6
7
static inline struct net *copy_net_ns(unsigned long flags,
struct user_namespace *user_ns, struct net *old_net)
{
if (flags & CLONE_NEWNET)
return ERR_PTR(-EINVAL);
return old_net;
}

net namespace 也是直接返回父进程的 namespace。

OK,不知不觉写了这么多,但回头去看,这更像是代码走读,分析深度不够,更详细的大家可以参照源码,源码结构还是比较清晰的。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发于我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

Docker 是“新瓶装旧酒”的产物,依赖于 Linux 内核技术 chroot 、namespace 和 cgroup。本篇先来看 namespace 技术。

Docker 和虚拟机技术一样,从操作系统级上实现了资源的隔离,它本质上是宿主机上的进程(容器进程),所以资源隔离主要就是指进程资源的隔离。实现资源隔离的核心技术就是 Linux namespace。这技术和很多语言的命名空间的设计思想是一致的(如 C++ 的 namespace)。

隔离意味着可以抽象出多个轻量级的内核(容器进程),这些进程可以充分利用宿主机的资源,宿主机有的资源容器进程都可以享有,但彼此之间是隔离的,同样,不同容器进程之间使用资源也是隔离的,这样,彼此之间进行相同的操作,都不会互相干扰,安全性得到保障。

为了支持这些特性,Linux namespace 实现了 6 项资源隔离,基本上涵盖了一个小型操作系统的运行要素,包括主机名、用户权限、文件系统、网络、进程号、进程间通信。

这 6 项资源隔离分别对应 6 种系统调用,通过传入上表中的参数,调用 clone() 函数来完成。

1
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

clone() 函数相信大家都不陌生了,它是 fork() 函数更通用的实现方式,通过调用 clone(),并传入需要隔离资源对应的参数,就可以建立一个容器了(隔离什么我们自己控制)。

一个容器进程也可以再 clone() 出一个容器进程,这是容器的嵌套。

如果想要查看当前进程下有哪些 namespace 隔离,可以查看文件 /proc/[pid]/ns (注:该方法仅限于 3.8 版本以后的内核)。

可以看到,每一项 namespace 都附带一个编号,这是唯一标识 namespace 的,如果两个进程指向的 namespace 编号相同,则表示它们同在该 namespace 下。同时也注意到,多了一个 cgroup,这个 namespace 是 4.6 版本的内核才支持的。Docker 目前对它的支持普及度还不高。所以我们暂时先不考虑它。

下面通过简单的代码来实现 6 种 namespace 的隔离效果,让大家有个直观的印象。

UTS namespace


UTS namespace 提供了主机名和域名的隔离,这样每个容器就拥有独立的主机名和域名了,在网络上就可以被视为一个独立的节点,在容器中对 hostname 的命名不会对宿主机造成任何影响。

首先,先看总体的代码骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)

static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};

// 容器进程运行的程序主函数
int container_main(void *args)
{
printf("在容器进程中!\n");
execv(container_args[0], container_args); // 执行/bin/bash return 1;
}

int main(int args, char *argv[])
{
printf("程序开始\n");
// clone 容器进程
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD, NULL);
// 等待容器进程结束
waitpid(container_pid, NULL, 0);
return 0;
}

该程序骨架调用 clone() 函数实现了子进程的创建工作,并定义子进程的执行函数,clone() 第二个参数指定了子进程运行的栈空间大小,第三个参数即为创建不同 namespace 隔离的关键。

对于 UTS namespace,传入 CLONE_NEWUTS,如下:

1
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS, NULL);

为了能够看出容器内和容器外主机名的变化,我们子进程执行函数中加入:

1
sethostname("container", 9);

最终运行可以看到效果如下:

IPC namespace


IPC namespace 实现了进程间通信的隔离,包括常见的几种进程间通信机制,如信号量,消息队列和共享内存。我们知道,要完成 IPC,需要申请一个全局唯一的标识符,即 IPC 标识符,所以 IPC 资源隔离主要完成的就是隔离 IPC 标识符。

同样,代码修改仅需要加入参数 CLONE_NEWIPC 即可,如下:

1
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC, NULL);

为了看出变化,首先在宿主机上建立一个消息队列:

然后运行程序,进入容器查看 IPC,没有找到原先建立的 IPC 标识,达到了 IPC 隔离。

PID namespace


PID namespace 完成的是进程号的隔离,同样在 clone() 中加入 CLONE_NEWPID 参数,如:

1
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID, NULL);

效果如下,echo $$ 输出 shell 的 PID 号,发生了变化。

但是对于 ps/top 之类命令却没有改变:

原因是 ps/top 之类的命令底层调用的是文件系统的 /proc 文件内容,由于 /proc 文件系统(procfs)还没有挂载到一个与原 /proc 不同的位置,自然在容器中显示的就是宿主机的进程。

我们可以通过在容器中重新挂载 /proc 即可实现隔离,如下:

这种方式会破坏 root namespace 中的文件系统,当退出容器时,如果 ps 会出现错误,只有再重新挂载一次 /proc 才能恢复。

一劳永逸地解决这个问题最好的方法就是用接下来介绍的 mount namespace。

mount namespace


mount namespace 通过隔离文件系统的挂载点来达到对文件系统的隔离。我们依然在代码中加入 CLONE_NEWNS 参数:

1
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS, NULL);

我验证的效果,当退出容器时,还是会有 mount 错误,这没道理,经多方查阅,没有找到问题的根源(有谁知道,可以留言指出)。

Network namespace


Network namespace 实现了网络资源的隔离,包括网络设备、IPv4 和 IPv6 协议栈,IP 路由表,防火墙,/proc/net 目录,/sys/class/net 目录,套接字等。

Network namespace 不同于其他 namespace 可以独立工作,要使得容器进程和宿主机或其他容器进程之间通信,需要某种“桥梁机制”来连接彼此(并没有真正的隔离),这是通过创建 veth pair (虚拟网络设备对,有两端,类似于管道,数据从一端传入能从另一端收到,反之亦然)来实现的。当建立 Network namespace 后,内核会首先建立一个 docker0 网桥,功能类似于 Bridge,用于建立各容器之间和宿主机之间的通信,具体就是分别将 veth pair 的两端分别绑定到 docker0 和新建的 namespace 中。

和其他 namespace 一样,Network namespace 的创建也是加入 CLONE_NEWNET 参数即可。我们可以简单验证下 IP 地址的情况,如下,IP 被隔离了。

User namespace


User namespace 主要隔离了安全相关的标识符和属性,包括用户 ID、用户组 ID、root 目录、key 以及特殊权限。简单说,就是一个普通用户的进程通过 clone() 之后在新的 user namespace 中可以拥有不同的用户和用户组,比如可能是超级用户。

同样,可以加入 CLONE_NEWUSER 参数来创建一个 User namespace。然后再子进程执行函数中加入 getuid() 和 getpid() 得到 namespace 内部的 User ID,效果如下:

可以看到,容器内部看到的 UID 和 GID 和外部不同了,默认显示为 65534。这是因为容器找不到其真正的 UID ,所以设置上了最大的UID(其设置定义在/proc/sys/kernel/overflowuid)。另外就是用户变为了 nobody,不再是 root,达到了隔离。

总结


以上就是对 6 种 namespace 从代码上简单直观地演示其实现,当然,真正的实现比这个要复杂得多,然后这 6 种 namespace 实际上也没有完全隔离 Linux 的资源,比如 SElinux、cgroup 以及 /sys 等目录下的资源没有隔离。目前,Docker 在很多方面已经做的很好,但相比虚拟机,仍然有许多安全性问题急需解决。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发于我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

说起生态,不禁让人想起贾跃亭的乐视,想当初我多次被它的生态布局给震撼到,一度相信它将要超越百度,坐拥互联网三大江山的宝座,但没过时日,各种劲爆的新闻就把它推到了风口浪尖上,现在想想也是让人唏嘘,但不管怎么说,愿它好吧,毕竟这种敢想敢做的精神还是值得敬佩的。

回到技术这个领域,不得不说,技术更新迭代的速度快得让人应接不暇,就容器技术这个领域来说,从 Docker 面世短短的 2-3 年时间里,就衍生出多种与之相关的技术框架,由此形成了一个小小的生态系统。

一谈到容器,大家都会想到 Docker,本文也主要从 Docker 角度来讲容器生态系统。

1 容器基础技术


Docker 的本质是利用 Linux 内核的 namespace 和 cgroups 机制,构建出一个隔离的进程(容器进程)。所以,容器的基础技术主要涉及到 Linux 内核的 namespace 和 cgroups 技术。

2 容器核心技术


容器核心技术保证容器能够在主机上运行起来,包括容器规范、容器 runtime、容器管理工具、容器定义工具、Registry 和容器 OS。

容器规范旨在将多种容器(如 OpenVZ,rkt,Docker 等)融合在一起,解决各种兼容问题,为此还专门成立了一个叫 OCI(Open Container Initiative)的组织来专门制定相关的容器规范。

容器 runtime 是容器真正运行的地方,一般需要依赖内核,也有运行在专门制定的容器 OS 上,关于容器 OS,下面会做介绍。lxc 、runc 和 rkt 是目前三种主流的 runtime。

lxc 是 Linux 上老牌的容器 runtime。Docker 最初也是用 lxc 作为 runtime。
runc 是 Docker 自己开发的容器 runtime,符合 oci 规范,也是现在 Docker 的默认 runtime。
rkt 是 CoreOS 开发的容器 runtime,符合 oci 规范,因而能够运行 Docker 的容器。

容器管理工具是对外提供给用户的 CLI 接口,方便用户管理容器,对内与 runtime 交互。对应于不同的 runtime,分别有三种不同的管理工具:lxd、docker engine 和 rkt cli。

容器定义工具允许用户定义容器的内容和属性,如容器需要什么镜像,装载什么应用等。常用有三种工具:docker images、Dockerfile 和 ACL(App Container Image)。

docker images 是容器镜像,runtime 依据 docker images 创建容器。dockerfile 是包含若干命令的文本文件,可以通过这些命令创建出 docker images。ACI 与 docker images 类似,只不过它是由 CoreOS 开发的 rkt 容器的 images 格式。

Registry 是存放容器镜像的仓库,包括 Docker Registry、Docker Hub 和 Quay.io,以及国内的 DaoCloud.io。企业可以用 Docker Registry 构建私有的 Registry。

容器 OS 不同于 runtime,是专门制定出来运行容器的操作系统,与常规 OS 相比,容器 OS 通常体积更小,启动更快。因为是为容器定制的 OS,通常它们运行容器的效率会更高。目前已经存在不少容器 OS,CoreOS、atomic 和 ubuntu core 是其中的杰出代表。

3 容器平台技术


随着容器部署的增多,容器也逐步过渡到容器云,容器平台技术就是让容器作为集群在分布式的环境中运行,包括了容器编排引擎、容器管理平台和基于容器的 PaaS。

容器编排引擎就是管理、调度容器在集群中运行,以保障资源的合理利用。有名的三大编排引擎为 docker swarm、kubernetes 和 mesos。其中,kubernetes 这两年脱颖而出,成为其中的佼佼者。

容器管理平台是在编排引擎之上更为通用的一个平台,它抽象了编排引擎的底层实现细节,能够支持多种编排引擎,提供友好的接口给用户,极大方便了管理。Rancher 和 ContainerShip 是容器管理平台的典型代表。

基于容器的 PaaS 基于容器的 PaaS 为微服务应用开发人员和公司提供了开发、部署和管理应用的平台,使用户不必关心底层基础设施而专注于应用的开发。Deis、Flynn 和 Dokku 都是开源容器 PaaS 的代表。

4 容器支持技术


容器的出现又重新让一些古老的技术焕发第二春,如监控、网络、数据管理、日志等技术,由于容器技术的不同,需要制定相应的符合容器规范的技术框架,由此有了容器支持技术,用于支持容器提供更丰富能力的基础设施。

其中包括容器网络、服务发现、监控、数据管理、日志管理和安全性。

容器网络主要用于解决容器与容器之间,容器与其他实体之间的连通性和隔离性。包括 Docker 原生的网络解决方案 docker network,以及第三方的网络解决方案,如 flannel、weave 和 calico。

服务发现保证容器使用过程中资源动态变化的感知性,如当负载增加时,集群会自动创建新的容器;负载减小,多余的容器会被销毁。容器也会根据 host 的资源使用情况在不同 host 中迁移,容器的 IP 和端口也会随之发生变化。在这种动态环境下,就需要有一种机制来感知这种变化,服务发现就是做这样的工作。etcd、consul 和 zookeeper 是服务发现的典型解决方案。

监控室保证容器健康运行,且让用户实时了解应用运行状态的工具,除了 Docker 原生的监控工具 docker ps/top/stats 之外,也有第三方的监控方案,如 sysdig、cAdvisor/Heapster 和 Weave Scope 。

数据管理保证容器在不同的 host 之间迁移时数据的动态迁移。有名的方案是 Flocker。

日志管理为问题排查和事件管理提供了重要依据。docker logs 是 Docker 原生的日志工具。而 logspout 对日志提供了路由功能,它可以收集不同容器的日志并转发给其他工具进行后处理。

容器安全性保证容器的安全,不被攻击,OpenSCAP 能够对容器镜像进行扫描,发现潜在的漏洞。

PS:本文借鉴了知名云计算博主 CloudMan 的博文:
http://www.cnblogs.com/CloudMan6/p/6706546.html,感谢 CloudMan 呈现这么好的内容。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

容器进化史

 容器

文章首发于我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

和虚拟机一样,容器技术也是一种资源隔离的虚拟化技术。我们追溯它的历史,会发现它的技术雏形早已有之。

1 容器简史


容器概念始于 1979 年提出的 UNIX chroot,它是一个 UNIX 操作系统的系统调用,将一个进程及其子进程的根目录改变到文件系统中的一个新位置,让这些进程只能访问到这个新的位置,从而达到了进程隔离的目的。

2000 年的时候 FreeBSD 开发了一个类似于 chroot 的容器技术 Jails,这是最早期,也是功能最多的容器技术。Jails 英译过来是监狱的意思,这个“监狱”(用沙盒更为准确)包含了文件系统、用户、网络、进程等的隔离。

2001 Linux 也发布自己的容器技术 Linux VServer,2004 Solaris 也发布了 Solaris Containers,两者都将资源进行划分,形成一个个 zones,又叫做虚拟服务器。

2005 年推出 OpenVZ,它通过对 Linux 内核进行补丁来提供虚拟化的支持,每个 OpenVZ 容器完整支持了文件系统、用户及用户组、进程、网络、设备和 IPC 对象的隔离。

2007 年 Google 实现了 Control Groups( cgroups ),并加入到 Linux 内核中,这是划时代的,为后期容器的资源配额提供了技术保障。

2008 年基于 cgroups 和 linux namespace 推出了第一个最为完善的 Linux 容器 LXC。

2013 年推出到现在为止最为流行和使用最广泛的容器 Docker,相比其他早期的容器技术,Docker 引入了一整套容器管理的生态系统,包括分层的镜像模型,容器注册库,友好的 Rest API 等。

2014 年 CoreOS 也推出了一个类似于 Docker 的容器 Rocket,CoreOS 是一个更加轻量级的 Linux 操作系统,在安全性上比 Docker 更严格。

2016 年微软也在 Windows 上提供了容器的支持,Docker 可以以原生方式运行在 Windows 上,而不是需要使用 Linux 虚拟机。

基本上到这个时间节点,容器技术就已经很成熟了,再往后就是容器云的发展,由此也衍生出多种容器云的平台管理技术,其中以 kubernetes 最为出众,有了这样一些细粒度的容器集群管理技术,也为微服务的发展奠定了基石。因此,对于未来来说,应用的微服务化是一个较大的趋势。

2 为什么需要容器


其一,这是技术演进的一种创新结果,其二,这是人们追求高效生产活动的一种工具。

随着软件开发的发展,相比于早期的集中式应用部署方式,现在的应用基本都是采用分布式的部署方式,一个应用可能包含多种服务或多个模块,因此多种服务可能部署在多种环境中,如虚拟服务器、公有云、私有云等,由于多种服务之间存在一些依赖关系,所以可能存在应用在运行过程中的动态迁移问题,那这时如何保证不同服务在不同环境中都能平滑的适配,不需要根据环境的不同而去进行相应的定制,就显得尤为重要。

就像货物的运输问题一样,如何将不同的货物放在不同的运输机器上,减少因货物的不同而频繁进行货物的装载和卸载,浪费大量的人力物力。

为此人们发明了集装箱,将货物根据尺寸形状等的不同,用不同规格的集装箱装载,然后再放到运输机上运输,由于集装箱密封,只有货物到达目的地才需拆封,在运输过程能够在不同运输机上平滑过渡,避免了资源的浪费。

因此集装箱被誉为是运输业与世界贸易最重要的发明。

Docker 容器的思想就是采用集装箱思想,为应用提供了一个基于容器的标准化运输系统。Docker 可以将任何应用及其依赖打包成一个轻量级、可移植、自包含的容器。容器可以运行在几乎所有的操作系统上。这样容器就可以跑在任何环境中,因此才有了那句话:

Build Once, Run Anywhere

这种集装箱的思想我们也能从 Docker 的 Logo 中看出来,这不就是一堆集装箱吗?

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发于我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

作为一个技术人,保持对技术的敏感是必要的,尤其是在互联网这个行业,昨天还觉得移动互联网挺酷炫的,今天就被人工智能这波潮流给掩盖掉了锋芒,泯然众人矣。虽然我们不一定要赶时髦,踏踏实实做好技术才是好的,但保持这样一份好奇心,也许会让我们的技术之路走得更加顺畅一点。

对于云计算这个领域,虽然早已是陈词滥调,但新瓶装旧酒,总会玩出很多新鲜的花样。作为一个没什么经验的初学者,我斗胆预测一下 2018 年云计算领域的几大技术趋势,仅供参考。

混合云


混合云其实叫嚣了很多年,但一直没有得到真正的应用,现在大部分企业和个人还是部署公有云和私有云为主,随着市场的增长和用户需求的增加,尤其是对云安全这块的需求,相信在 2018 年会有众多客户将业务迁移到混合云上。公有云市场的一些大公司,像阿里云,腾讯云会大力布局其混合云市场,而私有云市场则会更多选择与公有云大公司合作来布局自己的混合云市场,另外,像 zstack 这样的混合云开源技术框架会得到较多青睐,兴许可以成为下一个 OpenStack。

微服务


2014 年,AWS 推出了 serverless (无服务器)计算服务 Lambda,微服务便是在这样一种模式下提出的一种架构设计思想。此后几年各大互联网公司都在引入微服务的设计思想,应用在自家的业务产品中。可以说,微服务架构设计得好,不管是开发测试,还是运维管理都会大大提升效率和增强产品的可维护性和可扩展性。2018 年,微服务将会引发一波小高潮。很多大公司会开放他们的微服务设计框架,及相关的开源项目。

kubernetes( k8s ) 容器编排


容器技术现在基本上已经成为各大互联网公司的标配,随着部署的容器应用的增多,容器云的编排与管理已经成了不得不面对的问题。k8s 的强势崛起,在短短时间之内就刮起一阵旋风,令 docker 家族闻风丧胆,最终不得不缴械投降,2018 年,k8s 会得到井喷式应用。

雾计算


雾计算,或者边缘计算,在学术界已经不算什么新鲜词了,但在工业界还少有耳闻,但其实 cisco 和一些物联网公司早已开发出自己的雾计算产品,并大量应用在网络边缘的计算设备中。据我所知的,智云这家公司早已推出自己的雾计算平台,并应用在很多物联网项目中,大公司中,百度在 2018 年年初也率先推出了自己的雾计算项目「智能边缘」,旨在通过提供 “IoT Edge SDK” 组件的方式进行数据的本地处理,结合 AI 技术,为用户打造实时响应,智能推断,安全可靠的服务体验。相信在 2018 年会有更多的雾计算产品出现,我们拭目以待吧。

以上就是我对 2018 年云计算这个领域技术趋势的大致预测,预测本身就代表着不确定性,但保持对新技术的敏感性,能让我们提前做一些准备,毕竟作为技术人,终身学习是最基本的职业素养。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

在前文「从 Bridge 到 OVS」中,我们已经对 OVS 进行了一番探索。本文决定从 OVS 的整体架构到各个组件都进行一个详细的介绍。

OVS 架构


OVS 是产品级的虚拟交换机,大量应用在生产环境中,支撑整个数据中心虚拟网络的运转。OVS 基于 SDN 的思想,将整个核心架构分为控制面和数据面,数据面负责数据的交换工作,控制面实现交换策略,指导数据面工作。

从整体上看,OVS 可以划分为三大块,管理面、数据面和控制面。

数据面就是以用户态的 ovs-vswitchd 和内核态的 datapath 为主的转发模块,以及与之相关联的数据库模块 ovsdb-server,控制面主要是由 ovs-ofctl 模块负责,基于 OpenFlow 协议与数据面进行交互。而管理面则是由 OVS 提供的各种工具来负责,这些工具的提供也是为了方便用户对底层各个模块的控制管理,提高用户体验。下面就对这些工具进行一个逐一的阐述。

ovs-ofctl: 这个是控制面的模块,但本质上它也是一个管理工具,主要是基于 OpenFlow 协议对 OpenFlow 交换机进行监控和管理,通过它可以显示一个 OpenFlow 交换机的当前状态,包括功能、配置和表中的项。使用时,有很多参数,我们可以通过 ovs-ofctl –help 查看。

常用命令:

1
2
3
4
5
ovs-ofctl show switch-name :输出交换机信息,包括其流量表和端口信息。

ovs-ofctl dump-ports switch-name:输出交换机的端口统计信息,包括收发包、丢包、错误包等数量。

ovs-ofctl add-flow switch-name:为交换机配置流策略。

ovs-dpctl: 用来配置交换机的内核模块 datapath,它可以创建,修改和删除 datapath,一般,单个机器上的 datapath 有 256 条(0-255)。一条 datapath 对应一个虚拟网络设备。该工具还可以统计每条 datapath 上的设备通过的流量,打印流的信息等,更过参数通过 ovs-dpctl –help 查看。

常用命令:

1
2
3
4
5
ovs-dpctl show :显示所有 datapath 的基本信息。

ovs-dpctl dump-dps :显示所有 datapath 的名字。

ovs-dpctl dump-flows DP :显示一条 datapath DP 上的流信息。

ovs-appctl: 查询和控制运行中的 OVS 守护进程,包括 ovs-switchd,datapath,OpenFlow 控制器等,兼具 ovs-ofctl、ovs-dpctl 的功能,是一个非常强大的命令。ovs-vswitchd 等进程启动之后就以一个守护进程的形式运行,为了能够很好的让用户控制这些进程,就有了这个命令。详细可以 ovs-appctl –help 查看。

ovs-vsctl: 查询和更新 ovs-vswitchd 的配置,这也是一个很强大的命令,网桥、端口、协议等相关的命令都由它来完成。此外,还负责和 ovsdb-server 相关的数据库操作。

常用命令:

1
2
3
ovs-vsctl show :显示主机上已有的网桥及端口信息。

ovs-vsctl add-br br0:添加网桥 br0。

ovsdb-client: 访问 ovsdb-server 的客户端程序,通过 ovsdb-server 执行一些数据库操作。

常用命令:

1
2
3
ovsdb-client dump:用来查看ovsdb内容。

ovsdb-client transact :用来执行一条类 sql。

ovsdb-tool: 和 ovsdb-client 要借助 ovsdb-server 才能进行相关数据库操作不同,ovsdb-tool 可以直接操作数据库。

OVS 源码结构


OVS 源码结构中,主要包含以下几个主要的模块,数据交换逻辑在 vswitchd 和 datapath 中实现,vswitchd 是最核心的模块,OpenFlow 的相关逻辑都在 vswitchd 中实现,datapath 则不是必须的模块。ovsdb 用于存储 vswitch 本身的配置信息,如端口、拓扑、规则等。控制面部分采用的是 OVS 自家实现的 OVN,和其他控制器相比,OVN 对 OVS 和 OpenStack 有更好的兼容性和性能。

从图中可以看出 OVS 的分层结构,最上层 vswitchd 主要与 ovsdb 通信,做配置下发和更新等,中间层是 ofproto ,用于和 OpenFlow 控制器通信,并基于下层的 ofproto provider 提供的接口,完成具体的设备操作和流表操作等工作。

dpif 层实现对流表的操作。

netdev 层实现了对网络设备(如 Ethernet)的抽象,基于 netdev provider 接口实现多种不同平台的设备,如 Linux 内核的 system, tap, internal 等,dpdk 系的 vhost, vhost-user 等,以及隧道相关的 gre, vxlan 等。

数据转发流程


通过一个例子来看看 OVS 中数据包是如何进行转发的。

1)ovs 的 datapath 接收到从 ovs 连接的某个网络端口发来的数据包,从数据包中提取源/目的 IP、源/目的 MAC、端口等信息。

2)ovs 在内核态查看流表结构(通过 hash),如果命中,则快速转发。

3)如果没有命中,内核态不知道如何处置这个数据包,所以,通过 netlink upcall 机制从内核态通知用户态,发送给 ovs-vswitchd 组件处理。

4)ovs-vswitchd 查询用户态精确流表和模糊流表,如果还不命中,在 SDN 控制器接入的情况下,经过 OpenFlow 协议,通告给控制器,由控制器处理。

5)如果模糊命中, ovs-vswitchd 会同时刷新用户态精确流表和内核态精确流表,如果精确命中,则只更新内核态流表。

6)刷新后,重新把该数据包注入给内核态 datapath 模块处理。

7)datapath 重新发起选路,查询内核流表,匹配;报文转发,结束。
总结

OVS 为了方便用户操作,提供了很多管理工具,我们平常在使用过程中只需记住每个工具的作用,具体的命令可以使用 -h 或 –help 查看。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

和物理网络一样,虚拟网络要通信,必须借助一些交换设备来转发数据。因此,对于网络虚拟化来说,交换设备的虚拟化是很关键的一环。

上文「网络虚拟化」已经大致介绍了 Linux 内核为了满足网络虚拟化的要求,实现了一套虚拟交换设备——Bridge。本文重点介绍下 Bridge 的加强版——Open vSwitch(OVS),并从 Bridge 过渡到 OVS 的缘由讲起,让大家有个全面的认识。

借助 Linux Bridge 功能,同主机或跨主机的虚拟机之间能够轻松实现通信,也能够让虚拟机访问到外网,这就是我们所熟知的桥接模式,一般在装 VMware 虚拟机或者 VirtualBox 虚拟机的时候,都会提示我们要选择哪种模式,常用的两种模式是桥接和 NAT。

NAT 也很好理解,可以简单理解为当虚拟机启用了 NAT 模式之后,宿主机便通过 DHCP 为其生成可以访问外网的 IP,当 VM 访问外网的时候,就可以用该 IP 访问,其实就是宿主机为其做了地址转换。更详细的内容请自行搜索了解。

物理交换机有个重要的功能,就是虚拟局域网(VLAN),是对局域网(LAN)的软件化升级。一般,两台计算机通过一台交换机连接在一起就构成了一个 LAN。

一个 LAN 表示一个广播域,这意味着这个 LAN 中的任何节点发的数据包,其他节点都能收到,这有两个问题,一个是容易形成广播风暴,造成网络拥塞,另一个是广播包无法隔离,比如节点 B 不想接收节点 A 的包,但节点 A 强行要发,这就有点说不过去了。

解决这个问题的方案就是 VLAN,VLAN 能够对广播包进行有效隔离,它的做法是从软件上将交换机的端口虚拟出多个子端口,用 tag 来标记,相当于将交换机的端口划分多个 LAN,同一个 LAN 中的节点发出的数据包打上本 LAN 的 tag,这样,其他 LAN 中的节点就无法收到包,达到隔离的目的。

Bridge 本身是支持 VLAN 功能的,如下图所示,通过配置,Bridge 可以将一个物理网卡设备 eth0 划分成两个子设备 eth0.10,eth0.20,分别挂到 Bridge 虚拟出的两个 VLAN 上,VLAN id 分别为 VLAN 10 和 VLAN 20。同样,两个 VM 的虚拟网卡设备 vnet0 和 vnet 1 也分别挂到相应的 VLAN 上。这样配好的最终效果就是 VM1 不能和 VM2 通信了,达到了隔离。

Linux Bridge + VLAN 便可以构成一个和物理交换机具备相同功能的虚拟交换机了。对于网络虚拟化来说,Bridge 已经能够很好地充当交换设备的角色了。

但是为什么还有很多厂商都在做自己的虚拟交换机,比如比较流行的有 VMware virtual switch、Cisco Nexus 1000V,以及 Open vSwitch。究其原因,主要有以下几点(我们重点关注 OVS):

1) 方便网络管理与监控。OVS 的引入,可以方便管理员对整套云环境中的网络状态和数据流量进行监控,比如可以分析网络中流淌的数据包是来自哪个 VM、哪个 OS 及哪个用户,这些都可以借助 OVS 提供的工具来达到。

2) 加速数据包的寻路与转发。相比 Bridge 单纯的基于 MAC 地址学习的转发规则,OVS 引入流缓存的机制,可以加快数据包的转发效率。

3) 基于 SDN 控制面与数据面分离的思想。上面两点其实都跟这一点有关,OVS 控制面负责流表的学习与下发,具体的转发动作则有数据面来完成。可扩展性强。

4) 隧道协议支持。Bridge 只支持 VxLAN,OVS 支持 gre/vxlan/IPsec 等。

5) 适用于 Xen、KVM、VirtualBox、VMware 等多种 Hypervisors。

……

除此之外,OVS 还有很多高级特性,详情可以查阅官网自行了解。

下面简单看下 OVS 的整体架构,如下图所示,OVS 在 Linux 用户态和内核态都实现了相应的模块,用户态主要组件有数据库服务 ovsdb-server 和守护进程 ovs-vswitchd。内核态中实现了 datapath 模块。

其中, ovs-vswitchd 和 datapath 共同构成了 OVS 的数据面,控制面由 controller 模块来完成,controller 一般表示的是 OpenFlow 控制器,在 OVS 中,它可以借由第三方来完成,只要支持 OpenFlow 协议即可。

这里额外提一点,很多的一些产品级的虚拟交换机都是自身集成了控制器,比如 Cisco 1000V 的 Virtual Supervisor Manager(VSM),VMware 的分布式交换机中的 vCenter,而 OVS 是把这个事交由第三方去做,这么做的意义还是比较大的,可以让自己的产品很好地融入到各种解决方案中。

OpenFlow


OpenFlow 是控制面和数据面通信的一套协议,我们常常把支持 OpenFlow 协议的交换机称为 OpenFlow 交换机,控制器称为 OpenFlow 控制器,业界比较知名的 OpenFlow 控制器有 OpenDaylight、ONOS 等。

OpenFlow 是一个独立的完整的流表协议,不依赖于 OVS,OVS 只是支持 OpenFlow 协议,有了支持,就可以使用 OpenFlow 控制器来管理 OVS 中的流表。OpenFlow 不仅仅支持虚拟交换机,某些硬件交换机也支持 OpenFlow 协议。

ovs-vswitchd


ovs-vswitchd 是 OVS 的核心组件,它和内核模块 datapath 共同构成了 OVS 的数据面。它使用 OpenFlow 协议与 OpenFlow 控制器通信,使用 OVSDB 协议与 ovsdb-server 通信,使用 netlink 和 datapath 内核模块通信。

ovsdb-server


ovsdb-server 是 OVS 轻量级的数据库服务,用于整个 OVS 的配置信息,包括接口、交换内容、VLAN 等,ovs-vswitchd 根据这些配置信息工作。

OpenFlow 控制器


OpenFlow 控制器可以通过 OpenFlow 协议连接到任何支持 OpenFlow 的交换机,比如 OVS 。控制器通过向交换机下发流表规则来控制数据流向。

Kernel Datapath


datapath 内核模块和 ovs-vswitchd 是相互协作工作的,datapath 负责具体的收发包,而 ovs-vswitchd 通过 controller 下发的流表规则指导 datapath 如何转发包。

举个例子,datapath 从主机物理网卡 NIC 或者 VM 的 虚拟网卡 vNIC 收到包,如果是第一次收到包,datapath 不知道怎么处理这个包,于是将其丢给 ovs-vswitchd , ovs-vswitchd 决定该如何处理这个包之后又丢给 datapath,datapath 根据 ovs-vswitchd 的指示执行相应的动作,是丢弃还是从哪个口传出去。同时,ovs-vswitchd 会让 datapath 缓存好这个包的动作,下次再来就可以直接执行动作。

如果不是第一次收到包,就是按照之前缓存好的动作执行,这样极大地提高了数据处理的速度。

本文先对 OVS 有个初步印象,下文再详细介绍 OVS 的其他组件。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。

文章首发我的公众号「Linux云计算网络」,欢迎关注,第一时间掌握技术干货!

网络虚拟化相对计算、存储虚拟化来说是比较抽象的,以我们在学校书本上学的那点网络知识来理解网络虚拟化可能是不够的。

在我们的印象中,网络就是由各种网络设备(如交换机、路由器)相连组成的一个网状结构,世界上的任何两个人都可以通过网络建立起连接。

带着这样一种思路去理解网络虚拟化可能会感觉云里雾里——这样一个庞大的网络如何实现虚拟化?

其实,网络虚拟化更多关注的是数据中心网络、主机网络这样比较「细粒度」的网络,所谓细粒度,是相对来说的,是深入到某一台物理主机之上的网络结构来谈的。

如果把传统的网络看作「宏观网络」的话,那网络虚拟化关注的就是「微观网络」。网络虚拟化的目的,是要节省物理主机的网卡设备资源。从资源这个角度去理解,可能会比较好理解一点。

传统网络架构


在传统网络环境中,一台物理主机包含一个或多个网卡(NIC),要实现与其他物理主机之间的通信,需要通过自身的 NIC 连接到外部的网络设施,如交换机上,如下图所示。


传统网络(图片来源于网络,侵权必删)

这种架构下,为了对应用进行隔离,往往是将一个应用部署在一台物理设备上,这样会存在两个问题,1)是某些应用大部分情况可能处于空闲状态,2)是当应用增多的时候,只能通过增加物理设备来解决扩展性问题。不管怎么样,这种架构都会对物理资源造成极大的浪费。

虚拟化网络架构


为了解决这个问题,可以借助虚拟化技术对一台物理资源进行抽象,将一张物理网卡虚拟成多张虚拟网卡(vNIC),通过虚拟机来隔离不同的应用。

这样对于上面的问题 1),可以利用虚拟化层 Hypervisor 的调度技术,将资源从空闲的应用上调度到繁忙的应用上,达到资源的合理利用;针对问题 2),可以根据物理设备的资源使用情况进行横向扩容,除非设备资源已经用尽,否则没有必要新增设备。这种架构如下所示。


虚拟化网络(图片来源于网络,侵权必删)

其中虚拟机与虚拟机之间的通信,由虚拟交换机完成,虚拟网卡和虚拟交换机之间的链路也是虚拟的链路,整个主机内部构成了一个虚拟的网络,如果虚拟机之间涉及到三层的网络包转发,则又由另外一个角色——虚拟路由器来完成。

一般,这一整套虚拟网络的模块都可以独立出去,由第三方来完成,如其中比较出名的一个解决方案就是 Open vSwitch(OVS)。

OVS 的优势在于它基于 SDN 的设计原则,方便虚拟机集群的控制与管理,另外就是它分布式的特性,可以「透明」地实现跨主机之间的虚拟机通信,如下是跨主机启用 OVS 通信的图示。


分布式虚拟交换机(图片来源于网络,侵权必删)

总结下来,网络虚拟化主要解决的是虚拟机构成的网络通信问题,完成的是各种网络设备的虚拟化,如网卡、交换设备、路由设备等。

Linux 下网络设备虚拟化的几种形式


为了完成虚拟机在同主机和跨主机之间的通信,需要借助某种“桥梁”来完成用户态到内核态(Guest 到 Host)的数据传输,这种桥梁的角色就是由虚拟的网络设备来完成,上面介绍了一个第三方的开源方案——OVS,它其实是一个融合了各种虚拟网络设备的集大成者,是一个产品级的解决方案。

但 Linux 本身由于虚拟化技术的演进,也集成了一些虚拟网络设备的解决方案,主要有以下几种:

(1)TAP/TUN/VETH


TAP/TUN 是 Linux 内核实现的一对虚拟网络设备,TAP 工作在二层,TUN 工作在三层。Linux 内核通过 TAP/TUN 设备向绑定该设备的用户空间程序发送数据,反之,用户空间程序也可以像操作物理网络设备那样,向 TAP/TUN 设备发送数据。

基于 TAP 驱动,即可实现虚拟机 vNIC 的功能,虚拟机的每个 vNIC 都与一个 TAP 设备相连,vNIC 之于 TAP 就如同 NIC 之于 eth。

当一个 TAP 设备被创建时,在 Linux 设备文件目录下会生成一个对应的字符设备文件,用户程序可以像打开一个普通文件一样对这个文件进行读写。

比如,当对这个 TAP 文件执行 write 操作时,相当于 TAP 设备收到了数据,并请求内核接受它,内核收到数据后将根据网络配置进行后续处理,处理过程类似于普通物理网卡从外界收到数据。当用户程序执行 read 请求时,相当于向内核查询 TAP 设备是否有数据要发送,有的话则发送,从而完成 TAP 设备的数据发送。

TUN 则属于网络中三层的概念,数据收发过程和 TAP 是类似的,只不过它要指定一段 IPv4 地址或 IPv6 地址,并描述其相关的配置信息,其数据处理过程也是类似于普通物理网卡收到三层 IP 报文数据。

VETH 设备总是成对出现,一端连着内核协议栈,另一端连着另一个设备,一个设备收到内核发送的数据后,会发送到另一个设备上去,这种设备通常用于容器中两个 namespace 之间的通信。

(2)Bridge


Bridge 也是 Linux 内核实现的一个工作在二层的虚拟网络设备,但不同于 TAP/TUN 这种单端口的设备,Bridge 实现为多端口,本质上是一个虚拟交换机,具备和物理交换机类似的功能。

Bridge 可以绑定其他 Linux 网络设备作为从设备,并将这些从设备虚拟化为端口,当一个从设备被绑定到 Bridge 上时,就相当于真实网络中的交换机端口上插入了一根连有终端的网线。

如下图所示,Bridge 设备 br0 绑定了实际设备 eth0 和 虚拟设备设备 tap0/tap1,当这些从设备接收到数据时,会发送给 br0 ,br0 会根据 MAC 地址与端口的映射关系进行转发。


Bridge 与 TAP/TUN 的关系

因为 Bridge 工作在二层,所以绑定到它上面的从设备 eth0、tap0、tap1 均不需要设 IP,但是需要为 br0 设置 IP,因为对于上层路由器来说,这些设备位于同一个子网,需要一个统一的 IP 将其加入路由表中。

这里有人可能会有疑问,Bridge 不是工作在二层吗,为什么会有 IP 的说法?其实 Bridge 虽然工作在二层,但它只是 Linux 网络设备抽象的一种,能设 IP 也不足为奇。

对于实际设备 eth0 来说,本来它是有自己的 IP 的,但是绑定到 br0 之后,其 IP 就生效了,就和 br0 共享一个 IP 网段了,在设路由表的时候,就需要将 br0 设为目标网段的地址。

总结


传统网络架构到虚拟化的网络架构,可以看作是宏观网络到微观网络的过渡

TAP/TUN/VETH、Bridge 这些虚拟的网络设备是 Linux 为了实现网络虚拟化而实现的网络设备模块,很多的云开源项目的网络功能都是基于这些技术做的,比如 Neutron、Docker network 等。

OVS 是一个开源的成熟的产品级分布式虚拟交换机,基于 SDN 的思想,被大量应用在生产环境中。

PS:文章未经我允许,不得转载,否则后果自负。

–END–

欢迎扫👇的二维码关注我的微信公众号,后台回复「m」,可以获取往期所有技术博文推送,更多资料回复下列关键字获取。