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

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Webroot Blog
Webroot Blog
腾讯CDC
The Last Watchdog
The Last Watchdog
博客园 - 司徒正美
H
Hacker News: Front Page
I
InfoQ
A
Arctic Wolf
H
Hackread – Cybersecurity News, Data Breaches, AI and More
H
Heimdal Security Blog
L
LINUX DO - 最新话题
T
Threat Research - Cisco Blogs
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
Security Latest
Security Latest
D
DataBreaches.Net
C
Check Point Blog
J
Java Code Geeks
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
L
Lohrmann on Cybersecurity
雷峰网
雷峰网
Vercel News
Vercel News
WordPress大学
WordPress大学
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy
Forbes - Security
Forbes - Security
阮一峰的网络日志
阮一峰的网络日志
Hacker News: Ask HN
Hacker News: Ask HN
大猫的无限游戏
大猫的无限游戏
I
Intezer
N
News and Events Feed by Topic
小众软件
小众软件
B
Blog RSS Feed
Help Net Security
Help Net Security
Google DeepMind News
Google DeepMind News
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 三生石上(FineUI控件)
S
Security @ Cisco Blogs
美团技术团队
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
Engineering at Meta
Engineering at Meta
The GitHub Blog
The GitHub Blog
MyScale Blog
MyScale Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main

郑文峰的博客

使用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