Docker

docker top db
  • PID: Process ID;
  • PPID: Parent Process ID。
pgrep <program>

获取 program 进程号。

cat /proc/$PID/environ
# 一样
docker exec -it <program> env

Namespaces(命名空间)

命名空间限制了进程可以看到以及可以访问系统的特定部分(其他网络接口以及进程)。

当容器启动,container runtime e.g. Docker,会创建新的 命名空间 来沙箱进程。

进程跑在自己 Pid namespace,就像系统上只有这一个进程一样。

  • Mount (mnt)
  • Process ID (pid)
  • Network (net)
  • Interprocess Communication (ipc)
  • UTS (hostnames)
  • User ID (user)
  • Control group (cgroup)

创建命名空间

  • clone() 创建新线程和进程;

    新进程能复用 none/all/some 其父命名空间。

  • 在 shell 中 unshare()

访问命名空间setns()

声明周期

  • 命名空间最后一个进程退出,命名空间销毁;
  • 移除所有相关资源。
ll /proc/<self pid>/ns

UTS namespace

  • UTS= UNIX time sharing;
  • gethostname/sethostname;
  • container 设置自定义 hostname
  • 设置 NIS domain

Net namespace

  • 每个有自己私有的网络栈;
    • 网络接口(包括 lo);
    • 路由表;
    • iptables 链和规则;
    • sockets(通过 ss, netstat 查看)。
  • 每个创建一个 veth pair
    • 一个 veth容器网络命名空间,重命名为 eth0
    • 另一个 veth 在主机的 host,例如 docker0 bridge。

Mnt namespace

  • 进程有自己的 root fschroot);
  • 拥有私有的 mounts
    • 隔离 /tmp
    • /proc, /sys
    • mount 远程文件系统或敏感数据,只有允许的进程能见到。

PID namespace

  • 在一个 PID namespace 下的进程只能见同 namespace 下的进程;
  • 每个 PID namespace 都从 1 开始;
  • PID1 的挂了,真个命名空间被杀;
  • 可以折叠;
  • ps 可以看到其他,但是不能杀。
# 能 ps 到其他
sudo unshare --pid --fork
# mount /proc unshare mount namespace
sudo unshare --pid --fork --mount-proc

IPC

User


Chroot

  • 容器进程的最重要的部分是独立于 host 的不同文件;
  • 给进程提供了相对于宿主 OS 不同的 root 目录,这允许了 上面的1;

CGroup(Control Groups)

  • 限制了进程能 consume 的资源量; /proc 目录下,映射到 /sysfs/cgroup/ 目录;
    • memory
    • CPU
    • block I/O
  • 所有 docker 内存在:/sys/fs/cgroup/memory/docker/
  • 聚合进程干:
    • freezer(SIGSTOP/SIGCONT)
    • perf_event(收集多核性能事件)
    • cpuset(指定到哪些核运行)
  • pids cgroup限制组内的进程数量;
  • devices cgroup限制设备节点。

限制内存使用

修改

echo 8000000 > /sys/fs/cgroup/memory/docker/$DBID/memory.limit_in_bytes

查看

docker stats db --no-stream

Memory cgroup

ACCOUNTING

  • 跟踪使用的页:
    • file
    • anonymous(stack, heap, anonymous mmap)
    • active
    • inactive
  • 每个页都归于一组;
  • 每个页可在多组中共享;
cat /sys/fs/cgroup/memory/memory.stat

limits: 用于:物理内核所有(包括交换)

不能超过硬限制

  • 一组线程超过硬限制,内核无法回收内存;
  • 触发 OOM killer,进程被杀知道再次低于限制。

对于数据库等状态系统,不能杀进程,得用 oom-notifier。 同样是超过限制:

  • cgroup 中的所有进程冻结,通知发给了用户空间;
  • 用户空间可以 raise limits, migrate containers 等;
  • 低于限制才解冻。

Cpuset cgroup

  • pin 组到指定的 cpu
  • 为特定应用保留 cpu
  • 减少缓存刷新导致的性能损失;
  • NUMA 系统。

Contiainer

image

container image

  • 包含了 tar filestar file
  • files + metadata
    • metadata:
      • author;
      • 启动的 command
      • 设置的 environment variables 等;
  • 这些文件组成了我们容器的 root filesystem
  • layers 组成,概念上堆叠起来;
  • 每个 layer 可增加、删除、修改 filemetadata
  • 共享 layer 优化磁盘使用、传输时间以及内存使用。
docker save image > image.tar

busy-box 安装。

image

tar cv --files-from /dev/null | docker import - empty

空 imagescratch,允许从 scratch 上构建。 使用 docker import 加载 tarballdocker

docker commit

保存 container 的所有变化到新的 layer,创建新的 image

docker build 优先用这种。

low 方式:

docker run -it ubuntu
# in ubuntu
apt-get update && apt-get install figlet
# out ubuntu
docker diff <ubuntu container id|name>
docker commit <ubuntu container id|name> <newImageId>
docker tag <newImageId> figlet

image namespace

  • 官方镜像 e.g. ubuntu;
  • 用户或组织镜像 e.g. jpetazzo/clock
  • 自供 docker.elastic.co/elasticsearch/elasticsearch:7.9.1-arm64

layer

read-write-layer


containers vs. images

  • image: 只读文件系统;
  • container: 封装的一系列进程;跑在上面的只读文件系统的副本中;
  • 使用了 copy-on-write 代替普通的 copy

multi-layers


部署第一个 container

docker run -d image

查看

# 所有
docker ps
# 查看详情
docker insepct <container-id>
# 查看日志
docker logs <container-id>

停止

# 立即杀死
docker kill <container id|name> ...
# 发 TREM 信号, 10s 后发 KILL
docker stop <container id|name>

端口映射

# 6379 到随机端口
docker run -d --name redisDynamic -p 6379 redis:latest
# 查看映射出来的端口
docker port redisDynamic 6379

目录映射

docker run -d --name redisMapped -v <host-dir>:<contiainer-dir> redis
# e.g.
docker run -d --name redisMapped -v /opt/docker/data/redis:/data redis

前台跑

# 跑 ubuntu 并且执行 ps 命令
docker run ubuntu ps
# 进入容器访问 bash shell
docker run -it ubuntu bash
# -i: 我们与容器 stdin 连接
# -t: 我们想要一个 psedo-terminal(伪终端)

name &

# 启动时指定名字
docker run --name

# 改名
docker rename

编写第一个 Dockerfile

FROM nginx:alpine
COPY . /usr/share/nginx/html
docker build -t webserver-image:v1 .

docker run -d -p 80:80 webserver-image:v1
# 比上面多启动 nginx
FROM nginx:1.11-alpine
COPY index.html /usr/share/nginx/html/index.hml
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

CMD & ENTRYPOINT

UNIX 中开始新的程序:

  • fork() 创建子进程;
  • execve() 替换新的子进程运行。
    • execve(program, [list, of, arguments])
    • sh -c 就是用 shell 来分割命令参数。
# 这里还是用 sh,但是 figlet PID = 1
# figlet 开始时,sh 被替换
CMD exec figlet -f script hello
  • CMD 定义了一个默认执行的指令(如果没有的话);
  • 能在文件的任何位置
  • 后面的 CMD 会替换前面的;多条没有用。
FROM ubuntu
RUN apt-get update
RUN ["apt-get", "install", "figlet"]
# 定义了 base 指令以及参数
# 命令行参数加在这些后面
ENTRYPOINT ["figlet", "-f", "script"]

执行下面命令:

docker run figlet salut
# 等同于
sh -c "figlet -f script" salut

CMD + ENTRYPOINT:

ENTRYPOINT ["figlet", "-f", "script"]
# 当命令行没有参数,CMD 添加在后面,否则被替代
CMD ["hello world"]

node.js

FROM node:10-alpine

RUN mkdir -p /src/app

WORKDIR /src/app

COPY package.json /src/app/package.json
RUN npm install

COPY . /src/app

EXPOSE 3000

CMD ["npm", "start"]
# -e 设置环境变量
docker run -d --name my-production-running-app -e NODE_ENV=production -p 3000:3000 my-nodejs-app

OnBuild


ignore file

.dockerignore 文件放入不打进容器的文件列表。


Data Container

唯一用于 存放、管理数据。

# 创建一个 data container
docker create -v /config --name dataContainer busybox

# 复制到 data container 中
docker cp config.conf dataContainer:/config/

# --volumes-from 
docker run --volumes-from dataContainer ubuntu ls /config

导入、导出 data container

docker export dataContainer > dataContainer.tar
docker import dataContainer.tar

--link <container-name|id>:<alias>

做两件事:

  • 基于连接的容器设置环境变量,可以通过名字来引用端口、IP 等信息;
  • 更新 HOSTS 文件

修改 environment variables

# e.g. alpine env 命令会输出所有 environment variables
docker run --link redis-server:redis alpine env

输出:

REDIS_PORT=tcp://172.18.0.2:6379
REDIS_PORT_6379_TCP=tcp://172.18.0.2:6379
REDIS_PORT_6379_TCP_ADDR=172.18.0.2
REDIS_PORT_6379_TCP_PORT=6379
REDIS_PORT_6379_TCP_PROTO=tcp
REDIS_NAME=/elastic_sammet/redis
REDIS_ENV_GOSU_VERSION=1.10
REDIS_ENV_REDIS_VERSION=4.0.11
REDIS_ENV_REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-4.0.11.tar.gz
REDIS_ENV_REDIS_DOWNLOAD_SHA=fc53e73ae7586bcdacb4b63875d1ff04f68c5474c1ddeda78f00e5ae2eed1bbb

修改 hosts

#               alias 原始名称 hash-id
172.18.0.2      redis bac813a4509b redis-server

Networks

publish all

# exposed port -> 随机端口
docker run -d -P nginx
# 查看镜像 exposed port
docker inspect --format '' nginx
# 查看映射的端口
docker port <container id> <exposed port>
# 查看 container ip
docker inspect --format '' <yourContainerID>

network driver

single-container-network

multi-container-multi-network

  • bridge(default)
  • none
  • host
  • container

default bridge

  • 容器默认获得一个虚拟 eth0
    • 除了自己的 lo loopback interface
  • eth0 通过 veth pair(virtual Ethernet devices) 提供;
    • 连接两个 network namespace 的方法。
  • 另一端连接到 Docker Bridge
  • ip 地址是私有、内部的子集;
    • 默认 172.17.0.0/16,通过 --bip 配置;
  • 出口流量走 iptables MASQUERADE 规则;
  • 入口流量走 iptables DNAT 规则;
  • 容器有自己的路由、iptables 规则。

null driver

# 只有 lo,没有 eth0,对于独立或不可靠
docker run --net none

host driver

# 看到和访问 host 的网络接口
# 绑定任何地址和接口,流量不走 NAT,bridge, veth
# 对于性能高敏
docker run --net host

container driver

# 复用其他 container 的网络栈
# 共享 接口,ip,路由,iptables
# 通过 lo 交流
docker run --net <container:id>

network

# 创建 network
docker network create backend-network
# 容器使用 network
docker run -d --name=redis --net=backend-network redis
  • 环境变量HOSTS 都不会增加其他容器的信息;
  • 使用 Docker 内置 DNSresolv.confnameserver 设置为 127.0.0.11
# 查看所有网络
docker network ls
# 查看某网络详情
docker network inspect frontend-network
# 网络断连
docker network disconnect frontend-network redis

或者使用

# 创建网络
docker network create frontend-network
# connect 已有的容器到网络
docker network connect frontend-network redis

多container 在一个网络

docker network create dev
# 创建 es 指定网络
docker run -d --name es --net dev elasticsearch:2
# 可以 ping 通 es
docker run -ti --net dev alpine ping es

相同 image 多个 container

docker network create prod
# 创建两个 es 在网络,alias 同一名称
docker run -d --name prod-es-1 --net-alias es --net prod elasticsearch:2
docker run -d --name prod-es-2 --net-alias es --net prod elasticsearch:2
# nslookup 会返回两条记录,round robin
docker run --net prod --rm alpine nslookup es

# output
# Server:		127.0.0.11
# Address:	127.0.0.11:53

# Name:	es
# Address: 172.22.0.3
# Address: 172.22.0.2

docker-compose 自动创建网络网络别名

round robin 不可靠,不要作为负载,而且新的节点需要重新 resolve

由于一个 container 可能加入多个网络,可能 es 在多个网络中有不同 IP,可以使用 es.deves.prod 区分。


container network 原理

# 创建一个有自己 net ns 的进程
sudo unshare --net

# 在新进程中,无法 ping 通
ping 1.1
# 同样在这里,只有一个 lo 
ip link ls

# 在 host,创建 veth pair
sudo ip link add name in_host type veth peer name in_netns
# 同样,设置 host side(in_host)
sudo ip link set in_host master docker0 up

# 在新进程中,检查 PID
echo $$

# 在 host 中,将另一端(in_netns) 到新进程中
sudo ip link set in_netns netns <PID>

# 在新进程中
ip link set lo up
# 启动 veth 并且重命名为 eth0
ip link set in_netns name eth0 up

# 在 host 中查看 docker bridge
ip addr ls dev docker0
# output like:
# 6: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
#     link/ether 02:42:58:e0:aa:9d brd ff:ff:ff:ff:ff:ff
#     inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
#        valid_lft forever preferred_lft forever
#     inet6 fe80::42:58ff:fee0:aa9d/64 scope link
#        valid_lft forever preferred_lft forever

# 在新进程中,设置 ip 以及默认路由
ip addr add 172.17.0.99/24 dev eth0
ip route add default via 172.17.0.1
# 接着就能 ping 通
ping 1.1

Volumes

为了共享数据将宿主机的目录映射到容器。双向的。

VOLUME /uploads

docker run -d -v /uploads myapp

# ro: read only
docker run --volues-from <container-name|id>

特性

  • volumes 不是在 hostcontainer 间复制和同步;
  • bind mounts,类似于符号链接,但在不同级别;
  • 修改文件实时可见,是同一文件。

用于

  • 绕过 copy-on-write
    • 获取原生硬盘 I/O 性能;
    • leave some files out of docker commit
  • 多容器间共享文件夹
  • 主机和容器间共享文件夹单个文件
  • 远程存储;
  • 不能用于 k8s,swarm

使用 –mount


Log Files

容器启动,Docker 会跟踪标准输出标准错误

docker logs <container-name|id>

默认以 json 文件 存储。可替换为:

  • Syslog:写入主机的 central syslog
  • None: 关闭
docker run --log-driver=syslog <container>

日志设置

# max-size 可以日志滚动
# max-file 限制文件数量
docker run --log-opt max-size=10m --log-opt max-file=3 elasticsearch

插件:

  • json-file
  • Syslog
  • journald
  • gelf
  • fluentd
  • splunk

资源限制

RAM

如果进程用了太多的内存。

  • 如果有 swap 使用之;
  • 没有足够的 swap 空间,调用 OOM killer
  • OOM killer 尝试杀进程;
  • 可能杀无关进程。

cgroup 只杀超过限制的 container

# 物理 RAM
# 超过 100MB,如果 swap 能用,不会被杀
docker run --memory 100m python
# 总 RAM(RAM + swap)
docker run --memory 100m --memory-swap 100m python

CPU

  • --cpu-shares 1024,相对优先级;
  • --cpus 0.1 10%, --cpus 10 10 个 CPU;
  • --cpuset-cpus 3,5,7,使用 3,5,7 号 cpu。

Disk


重启策略

Docker 认为任何以非 0 状态退出都是 crash,默认 crash container 保持停止。

# 有限次数重启
docker run --restart-on-failure:<num> <container>
# 一直重启
docker run --restart=always <container>

元数据和标签

推荐格式:

  • 使用 reverse DNS 方式: io.github.gh0st99
  • 小写;
  • [a-z0-9-.]

用途:

  • vhost
  • backup schedule
  • ownership
# 加一个标签
docker -l key=value
# 从文件加入
docker -label-file=<label file path>

通过标签过滤 container

# 过滤包含特殊标签
docker ps --filter lable=key
# 过滤包含特殊值的特殊标签
docker ps --filter label=key=value
docker images --filter label=key=values

给 docker 加标签

docker -d \
  -H unix:///var/run/docker.sock \
  --label com.katacoda.environment="production" \
  --label com.katacoda.storage="ssd"
LABEL vendor=Gh0st99
LABEL io.github.gh0st99.version=0.0.1\io.github.gh0st99.build-date=2020-09-15:09:29:46z

负载均衡容器

容器互相交流有两种方式:

  • 修改 environment variableshosts 文件;
  • 使用 Service Discovery 模式。
    • 使用三方系统识别目标系统的位置。

Nginx Proxy


docker compose


docker stats

# 单一
docker stats <container name|id>
# 所有运行
docker ps -q | xargs docker stats

docker multi-stage builds

# 第一阶段, 准备 Go 环境,编译 binary
FROM golang:1.6-alpine

RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# 第二阶段,把第一阶段产物复制过来
FROM alpine
EXPOSE 80
CMD ["/app"]

# 将第一阶段生成的 main 复制到根目录改名 app
COPY --from=0 /app/main /app
# 第一阶段昵称 compiler
FROM ubuntu AS compiler
RUN apt-get update
RUN apt-get install -y build-essential
COPY hello.c /
RUN make hello

FROM ubuntu
# 将第一阶段的 hello 复制到本阶段
COPY --from=compiler /hello /hello
CMD /hello

运行:

docker build -t hellomultistage .
docker run hellomultistage
# 打中间状态的 image
docker build --stage compiler --tag hellocompiled
  • Dockerfile 每一行创建一个新 layer
  • 联合 && 以及 \

优化 dockerfile

善用 COPY

FROM python
# 这里使用 COPY 创建 layer,能更有效缓存
COPY requirements.txt /tmp/requirements.txt
# 只有 requirements.txt 变时才 install
RUN pip install -qr /tmp/requirements.txt
WORKDIR /src
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]

chown,chomd,mv 要小心

  • layers 无法有效的存储权限所有者的改变;
  • 对于 mv 也一样;
  • 所以这些指令在 RUN 之后都会拷贝整个文件。
COPY some-file .
# 拷贝 some-file
RUN chown www-data:www-data some-file
# 拷贝 some-file
RUN chmod 644 some-file
# 拷贝 some-file
RUN mv some-file /var/www

解决之道:

  • 使用一个 layer 解决;
  • 使用 COPY --chown=user:group src dest
    • user 必须在 /etc/passwd/etc/group 中。
  • 在本地设置好文件权限,COPY 会保留。

美化输出

docker ps --format ' container is using  image'
docker ps -q | xargs docker inspect --format ' -  - '

容器进程 vs. 主机进程

  • 两者每个线程都属于 namespaces & cgroups
    • 属于上面的俩决定进程的所见和所做;
  • 以特定的 UID 运行;
    • UID = 0, root

分析 container

# 查看异常退出的 container
docker ps 

# 方法 1.
## 查看变更的文件
docker diff <container id>
## 复制出来日志等有用文件查看
docker cp <container id>:/var/log/error.log .

# 方法 2.
## 创建新的镜像
docker commit <container id> debugimage
## 自定义入口
docker run -ti --entrypoint sh debugimage

## 生成 tar 文件,并查看里面的内容
docker export <container id> | tar tv

init system

init system: PID 1。

  • 创建其他进程;
  • 获得孤立的僵尸进程。
    • 进程退出时,变僵尸(top 中标记为 Z);
    • 其父进程必须 reap 僵尸进程。调用 waitpid() 获取退出状态;
    • 如果进程退出,其子进程成为孤儿;
    • 这些孤儿进程 re-parentPID 1

应用以 PID 1 跑。

Docker 自动提供一个 init(tinit) 进程:

  • 获取僵尸进程;
  • 转发信号;
  • 当子进程退出,tinit 退出。
docker run jpetazzo/hamba 80 www1:80 www2:80

dockerfile 为:

FROM alpine
RUN apk update && apk add haproxy
ADD hamba /usr/local/bin/hamba
ENV HOME /run
VOLUME /run
ENTRYPOINT ["hamba"]

这里 entrypoint 干了:

  • 解析参数;
  • 生成配置文件;
  • 启动实际服务。

安全

  • capabilities
    • 容器默认关闭所有危险的;
    • 通过 docker run --cap-add 添加;
  • seccomp: secure computing。容器搞定,不用操心;
  • security modules:
    • SELinux(Red Hat);
    • AppArmor(AppArmor).
    • LSM 给所有进程操作加了一层访问控制;
    • 容器搞定,不用操心。

copy-on-system

  • 允许共享数据的机制;
  • 出现的数据是一份拷贝,但只是链接到原来的数据;
  • 当改变共享数据时才实际复制;

AUFS

  • 按照特殊顺序合并多个分支;
  • 每个分支只是一个普通文件夹;
    • 至少一个只读分支(最底部);
    • 只有一个读写分支(最顶部)。

打开文件

  • O_RDONLY:
    • 从顶上开始每个分支找;
    • 打开第一个找到的。
  • OWRONLYO_RDWR:
    • 如果在顶上,打开之;
    • 如果在其他分支,复制到顶上;
    • 如果不存在,在顶上创建。

性能

  • mount() 快,所以创建容器快;
  • 读写拥有原生性能;
  • open() 两种情况性能昂贵:
    • 写大文件(日志、数据库);
    • 在许多层上搜索许多文件夹;
  • 当同一容器启动多次:
    • 磁盘上的数据只加载一次,在内存中只缓存一次;
    • dentries 会重复。

Overlay2

  • AUFS 简单,只能有两层;
  • 在层间使用硬连接,提高 open() & stat() 性能。

Docker 引擎组成

  • dockerd
  • containerd
  • containerd-shim
  • runc

Swarm 教程

基本概念

  • cluster
    • node
      • manager: 对其进行 api 调用,只有一个是 leader,其他的将请求转给 leader
      • worker: 从 manager 那获得指令。
    • service: leader 使用不同子系统将 service 分解城 task
      • task:与特殊的 container 对应,被分配个一个特殊的 node

启动

swarm 模式下,每个 node 将自己的 ip 宣告给其他 node

# 控制用
--advertise-addr eth0:7777
--listen-addr eth0:7777 
# 数据用
--data-path-addr

task 的声明周期:

  • assigned: 已经被分配个一个 node;
  • preparing: 拉取镜像;
  • starting
  • running

docker-swarm-break-down

manager 的容错性

2N + 1 可以容忍 N 失败。

不能使所有节点都成 manager

  • Raft都发给所有的节点;
  • 数量众多的群体很难达成一致;
  • 每个节点 ping Manager < 10ms。

最佳规范,节点数:

  • 3 - 5:所有都是 manager;
  • 5 - 10: 剩下都是 worker
  • 10 - 100:5 个最稳定的做 manager
  • > 100: manager 得 4C, 16G, SSD。

K8S 教程

部署应用

kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1

创建一个 deployment

  • 找一个应用实例能运行的合适的节点;
  • scheduled 应用使其在那个 Node 上跑;
  • 配置集群当需要的时候将实例编排到新 Node 上。

K8S 中跑的 Pods 跑在 私有隔离的网络上。

  • 默认对同 K8S 集群 上其他 Podsservices 可见;
  • 这个网络外的不可见。
kubectl proxy

会创建一个 terminalK8S 集群 的连接。终端可以直连到 API

通过 proxy endpoint 可以看见这些 APIs。e.g.

curl http://localhost:8001/version

API Server 基于 pod name 自动为每个 Pod 创建一个 endpoint


查看 pod 和工作节点

Pod:表示一组一个或多个应用程序容器,以及这些容器的一些共享资源:

  • 共享存储,当作卷;
  • 网络,作为唯一的集群 IP 地址;
  • 有关每个容器如何运行的信息(e.g. port, version)。

Pod

  • 逻辑主机;
  • 共享 IP & Port
  • 平台原子单元;
  • 创建 Deployment,会在其中创建包含容器的 Pod
    • 每个 Pod调度它的工作节点绑定直到,直到终止;
    • 如果 Node 出现问题,则在其他可用 Node 调度相同的 Pod

工作节点

主节点管理工作节点工作节点有多个 Pod

主节点自动处理集群中工作节点调度 pod

  • pod 中的容器共享一个网络空间
    • docker fun --net=container:<container_id>
  • 有一个特殊 sandbox 容器;
  • 使用 k8s.gcr.io/pause 共享网络;
  • pod 中的容器有独立的文件系统,通过 volumes 共享文件夹;