惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
T
Threatpost
IT之家
IT之家
腾讯CDC
博客园 - 【当耐特】
博客园 - 聂微东
月光博客
月光博客
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
酷 壳 – CoolShell
酷 壳 – CoolShell
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - Franky
博客园 - 叶小钗
S
Security @ Cisco Blogs
雷峰网
雷峰网
N
News and Events Feed by Topic
Attack and Defense Labs
Attack and Defense Labs
Forbes - Security
Forbes - Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
SegmentFault 最新的问题
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
L
Lohrmann on Cybersecurity
Project Zero
Project Zero
www.infosecurity-magazine.com
www.infosecurity-magazine.com
宝玉的分享
宝玉的分享
阮一峰的网络日志
阮一峰的网络日志
The Last Watchdog
The Last Watchdog
Apple Machine Learning Research
Apple Machine Learning Research
W
WeLiveSecurity
C
Cisco Blogs
T
Tenable Blog
Schneier on Security
Schneier on Security
爱范儿
爱范儿
美团技术团队
V2EX - 技术
V2EX - 技术
V
V2EX
Google Online Security Blog
Google Online Security Blog
Last Week in AI
Last Week in AI
Security Archives - TechRepublic
Security Archives - TechRepublic
The Hacker News
The Hacker News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Schneier on Security
T
The Exploit Database - CXSecurity.com
博客园_首页
H
Hacker News: Front Page
量子位
N
News | PayPal Newsroom
大猫的无限游戏
大猫的无限游戏

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备 快速了解iptables kafka中listener和advertised.listeners的作用 django rest_framework 分页 django后端服务、logstash和flink接入VictoriaMetrics指标监控 python中import原理 docker容器单机网络 手动实现docker容器bridge网络模型 mysql之MVCC原理 mysql之日志 使用java开发logstash的filter插件 使用python实现单例模式的三种方式 redis之缓存 redis之分片集群 redis之哨兵机制 redis之主从库同步 redis之持久化 redis之五种基本数据类型 go中如何处理error pod中将代码与运行环境分离 ddt源码分析 python装饰器的使用方法 读书笔记:如何阅读一本书 使用ddt实现unittest的参数化测试 使用kubeadm安装k8s 优化gin表单的错误提示信息 gin中validator模块的源码分析 go简单使用grpc python简单使用grpc k8s之PV、PVC和StorageClass k8s之StatefulSet k8s之DaemonSet k8s之Job和CronJob k8s之ConfigMap和Secret k8s之Service k8s之Pod k8s之Deployment docker容器 python迭代器与生成器 python元编程 python垃圾回收机制 python上下文管理器 django rest_framework使用jwt django rest_framework异常处理 django rest_framework 自定义文档 django压缩文件下载 django rest_framework使用pytest单元测试 django restframework choice 自定义输出数据 django Filtering 使用 django viewset 和 Router 配合使用时报的错 django model的序列化 django中使用AbStractUser django.core.exceptions.ImproperlyConfigured Application labels aren't unique, duplicates users django 中 media配置 django 外键引用自身和on_delete参数 django 警告 while time zone support is active Flask使用flask_socketio实现websocket flask结合mongo tornado 文件上传 tornado 使用jwt完成用户异步认证 tornado 用户密码 bcrypt加密 tornado 结合wtforms使用表单操作 tornado finish和write区别 tornado 使用peewee-async 完成异步orm数据库操作 pyspark streaming简介 和 消费 kafka示例 使用hue创建ozzie的pyspark action workflow count的性能优化 django rest_framework Authentication django celery 结合使用 网站
容器的本质
zhengwenfeng · 2022-08-10 · via 郑文峰的博客

# 前言

使用NameSpace技术来修改进程视图,创建出独立的文件系统、主机名、进程号、网络等资源空间,再使用Cgroups来实现对进程的 CPU、内存等资源的优先级和配额限制,最后使用chroot更改进程的根目录,也就是限制访问文件系统

# NameSpace

可以创建出独立的文件系统、主机名、进程号、网络等资源空间,实现系统全局资源和进程局部资源的隔离。

NameSpace有多种隔离类型,像常见的有PID NameSpace可以隔离进程ID、NET Namespace隔离网络设备端口号等。

举个例子

NameSpace可以让当前进程只能看到当前Namespace里的进程,看不到宿主机创建的进程。并且运行容器的命令为1号进程。

# docker run -it busybox /bin/sh

/ # ps aux
PID   USER     COMMAND
    1 root     /bin/sh
    8 root     ps aux

/ # echo $$
1

1
2
3
4
5
6
7
8
9

在宿主机中可以看到该容器进程ID并不为1。再次证明容器也只是宿主机中的一个进程而已。

[root@k8s-master ~]# ps aux | grep /bin/sh | grep -v grep
root     1398061  0.1  0.3 1368728 54104 pts/1   Sl+  10:36   0:00 docker run -it mirrors.sangfor.com/busybox /bin/sh
root     1398102  0.8  0.0   3176   188 pts/0    Ss+  10:36   0:00 /bin/sh

1
2
3

# chroot

可以更改进程的根目录,限制访问文件系统。

# 手动构造一个容器

我们使用clone创建一个子进程,传入的参数是CLONE_NEWPID代表着启用了PID NameSpace,当前进程看到的是一个全新的进程空间,在该命名空间中,自己是1号进程。

#define _GNU_SOURCE
#include <sys/mount.h> 
#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* arg)
{  
  printf("Container - inside the container!\n");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}

int main()
{
  printf("Parent - start a container!\n");
  int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWPID | SIGCHLD , NULL);
  waitpid(container_pid, NULL, 0);
  printf("Parent - container stopped!\n");
  return 0;
}

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

[root@k8s-master k8s]# gcc a.c -o a

[root@k8s-master k8s]# ./a 
Parent - start a container!
Container - inside the container!

[root@k8s-master k8s]# echo $$
1

[root@k8s-master k8s]# exit
Parent - container stopped!

1
2
3
4
5
6
7
8
9
10
11

但是我们在使用ps aux时,还是看到整个宿主机的进程,并且进程ID为1的还是Systemd,为什么呢?

这是因为ps命令是读/proc文件系统的,所以我们还需要进行文件系统的隔离。

我们再使用Mount NameSpace进行文件系统的隔离,在clone中使用CLONE_NEWNS参数。

int main()
{
  printf("Parent - start a container!\n");
  int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWPID | CLONE_NEWNS  | SIGCHLD , NULL);
  waitpid(container_pid, NULL, 0);
  printf("Parent - container stopped!\n");
  return 0;
}

1
2
3
4
5
6
7
8

编译运行后,发现当前文件系统并未发生变化,这个是因为,创建子进程时,会继承父进程的挂载点。

所以我们需要在子进程中修改当前的挂载点,并且子进程在新的namespace的挂载动作只影响自身的挂载文件系统。

先拷贝一个文件系统出来作为我们容器的根文件系统

docker export 48ab2ddd04dc | tar -C ./testfs -xvf -

1

再挂载proc,并且将testfs作为该进程的根目录

int container_main(void* arg)
{  
  printf("Container - inside the container!\n");

  if (mount("proc", "testfs/proc", "proc", 0, NULL)) {
     perror("proc");
  }

  if ( chdir("./testfs")!=0 ||  chroot("./") != 0) {
     perror("chroot");
  }

  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

再次运行进入容器中,当前的根目录是上面我们构造的testfs,并且ps aux命令只能看到当前namespace的进程,而看不到宿主机namespace的进程了。

[root@k8s-worker1 k8s]# ./a
Parent - start a container!
Container - inside the container!
root@k8s-worker1:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@k8s-worker1:/# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0  26680  5452 ?        S    07:42   0:00 /bin/bash
root          94  0.0  0.0   5352   692 ?        S    07:49   0:00 ./a
root          95  0.0  0.0   4620  3872 ?        S    07:49   0:00 /bin/bash
root          99  0.0  0.0   7056  1556 ?        R+   07:49   0:00 ps aux
root@k8s-worker1:/# echo $$
1

1
2
3
4
5
6
7
8
9
10
11
12
13

# cgroup

可以实现对进程的CPU、内存等资源的配额限制

cgroup在操作系统中暴露出来的接口是文件系统,会以文件和目录在/sys/fs/cgroup/目录中展示,下面的目录都是子系统,可以限制各种资源:

[root@iZwz93q4afq8ck02cesqh4Z ~]# ls /sys/fs/cgroup/
blkio  cpuacct      cpuset   freezer  memory   net_cls,net_prio  perf_event  systemd
cpu    cpu,cpuacct  devices  hugetlb  net_cls  net_prio          pids

1
2
3

如何限制进程CPU

执行以下命令将CPU吃到100%

$ while : ; do : ; done &

1

使用top命令查看是否cpu是否满负载

# top

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                              
2503037 root      20   0   26672   5412   3520 R  99.0   0.0   0:46.09 -bash                                                                                  99.49%

1
2
3
4

在目录/sys/fs/cgroup/cpu,cpuacct/下创建目录container,操作系统会自动创建资源限制文件

[root@k8s-worker1 container]# mkdir container
[root@k8s-worker1 container]# ls
cgroup.clone_children  cpuacct.usage         cpuacct.usage_percpu_sys   cpuacct.usage_user  cpu.rt_period_us   cpu.stat
cgroup.procs           cpuacct.usage_all     cpuacct.usage_percpu_user  cpu.cfs_period_us   cpu.rt_runtime_us  notify_on_release
cpuacct.stat           cpuacct.usage_percpu  cpuacct.usage_sys          cpu.cfs_quota_us    cpu.shares         tasks

1
2
3
4
5

在文件夹下面可以查看到cpu.cfs_quota_us的默认值是-1,代表没有限制,cpu.cfs_period_us默认值为100ms(100000us)

# cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
100000
# cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
-1

1
2
3
4


向cpu.cfs_quota_us写入20ms(20000us),也就是每100ms时间里,限制的进程只能适用20ms,也就是这个进程只能使用到20%的CPU带宽

echo 20000 > /sys/fs/cgroup/cpu//cpu.cfs_quota_us

1

再将限制的进程ID写入到tasks文件中,可以看到该文件中已经包含了该容器进程ID

# echo 2503037 > tasks
# cat tasks
2503037

1
2
3

再使用top可以发现该进程的cpu被限制在20%

# top

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                              
2503037 root      20   0   26672   5412   3520 R  20.4   0.0   6:43.71 -bash

1
2
3
4

容器被cgroup的情况

当然docker已经封装好了,直接调用以下命令即可实现上面CPU的限制

docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

1

可以看到在/sys/fs/cgroup/cpu,cpuacct/docker目录下创建了该容器的目录,目录下面包含了资源限制文件

[root@k8s-worker1 docker]# pwd
/sys/fs/cgroup/cpu,cpuacct/docker
[root@k8s-worker1 docker]# ls
87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c  cpuacct.usage             cpuacct.usage_percpu_user  cpu.cfs_quota_us   cpu.stat
cgroup.clone_children                                             cpuacct.usage_all         cpuacct.usage_sys          cpu.rt_period_us   notify_on_release
cgroup.procs                                                      cpuacct.usage_percpu      cpuacct.usage_user         cpu.rt_runtime_us  tasks
cpuacct.stat
[root@k8s-worker1 docker]# cd 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c/
[root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# ls
cgroup.clone_children  cpuacct.usage         cpuacct.usage_percpu_sys   cpuacct.usage_user  cpu.rt_period_us   cpu.stat
cgroup.procs           cpuacct.usage_all     cpuacct.usage_percpu_user  cpu.cfs_period_us   cpu.rt_runtime_us  notify_on_release
cpuacct.stat           cpuacct.usage_percpu  cpuacct.usage_sys          cpu.cfs_quota_us    cpu.shares         tasks

1
2
3
4
5
6
7
8
9
10
11
12

在该目录下可以看到cpu.cfs_quota_us设置成了20000,并且tasks中包含了该容器进程的ID

[root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat cpu.cfs_quota_us 
20000
[root@k8s-worker1 87ee72386a6079ba6411ac8f3030c12407558652d28a1cd16c03f4434581500c]# cat tasks
2560954

1
2
3
4