知识广场

按学科筛选:计算机科学 / 分布式与云计算 / 容器
清除筛选

«计算机科学 / 分布式与云计算 / 容器» 分类下共 4 篇帖子

podman rootless:不需要 root daemon 跑容器(更安全 + systemd 集成)

## 起因 我的服务器上 Docker daemon 用 root 跑。任何能跟 docker.sock 通信的用户 都等于 root(passing socket 到容器 = 容器逃逸)。多用户 / 共享开发机 上风险大。 `podman` 是 Red Hat 主导的 OCI 兼容容器引擎,几个区别: - **no daemon**:每个容器一个进程 fork 出来,systemd 直接 supervise - **rootless first**:默认非 root 用户跑 - **Docker CLI 兼容**:`alias docker=podman` 大多数命令直接 work - **支持 pod**(K8s 那种多容器组) ## 装 ```bash # Debian 12+ / Ubuntu 22.04+ sudo apt install -y podman # 给当前用户 subuid / subgid(rootless 必须) # /etc/subuid + /etc/subgid 应当各有: # me:100000:65536 # 默认装好就有;没有就: sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 me # 配置 storage(rootless 在 ~/.local/share/containers/) podman info | grep -A 3 storage ``` ## 基础用法(跟 Docker 几乎一样) ```bash podman pull nginx:alpine podman run -d --name web -p 8080:80 nginx:alpine podman ps podman logs -f web podman exec -it web sh podman stop web podman rm web ``` `alias docker=podman` 后大部分 docker 命令直接用。 ## rootless 网络 Rootless 容器不能 bind 80/443(< 1024 需 root)。 解决: ```bash # 方案 A:bind 8080,前置 nginx 反代 podman run -d -p 8080:80 nginx # 方案 B:开 unprivileged port range sudo sysctl net.ipv4.ip_unprivileged_port_start=80 # 之后 rootless 也能 bind 80 ``` 方案 B 安全风险:任何用户能 bind 80。多人共享机器不建议。 ## podman generate systemd 杀手锏:把 container 转成 systemd unit: ```bash podman run -d --name web -p 8080:80 nginx:alpine # 生成 unit 文件 podman generate systemd --name web --files --new # 输出:container-web.service mkdir -p ~/.config/systemd/user/ mv container-web.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now container-web.service # 开机自启(必须 enable linger) loginctl enable-linger $USER ``` 之后 systemctl 全程管理 container: ```bash systemctl --user status container-web systemctl --user restart container-web journalctl --user -u container-web -f ``` 容器 = systemd service。一致性极佳。 ## Quadlet(podman 4.4+ 推荐) 更现代的方式:`.container` 文件直接被 systemd 当 unit: `~/.config/containers/systemd/web.container`: ```ini [Unit] Description=Web server [Container] Image=docker.io/library/nginx:alpine PublishPort=8080:80 Volume=/home/me/www:/usr/share/nginx/html:Z AutoUpdate=registry [Service] Restart=always [Install] WantedBy=default.target ``` ```bash systemctl --user daemon-reload systemctl --user start web # 注意是 web 不是 web.container ``` 比 generate systemd 简洁得多。每改 image / volume / port 直接改 .container 文件 + daemon-reload。 ## Pod(多容器组) K8s 风格的 Pod(共享 network + IPC): ```bash podman pod create --name myapp -p 8080:80 podman run -d --pod myapp --name api myapi:latest podman run -d --pod myapp --name worker myworker:latest podman pod ls ``` api 和 worker 共享网络(同 localhost),共享 IPC namespace。 适合"sidecar" 模式。 K8s YAML 直接转 podman pod: ```bash podman play kube k8s-pod.yaml ``` 支持基础 K8s YAML。 ## auto-update ```bash # 容器加 label podman run -d --label io.containers.autoupdate=registry nginx:alpine # 系统级 timer 自动检查 + 升级 systemctl --user enable --now podman-auto-update.timer ``` 每天检查 registry 上 image 新版,有就 pull + restart 容器。Watchtower 等价。 ## docker-compose 兼容 ```bash sudo apt install -y podman-compose # 或:pip install podman-compose podman-compose up -d # 大部分 docker-compose.yml 直接 work ``` 或者用 `podman compose`(subcommand,跟 Docker Compose v2 接近)。 少数 docker-only feature(如 `network_mode: bridge` 的特定行为) 偶尔不兼容,看具体场景。 ## 与 Docker 对比 | | Docker | podman | |---|---|---| | daemon | dockerd (root) | 无 daemon(每容器自己进程) | | rootless | 实验性 + 有限 | 默认 + 一等公民 | | systemd 集成 | 弱 | 极强(Quadlet) | | Docker CLI 兼容 | 原生 | 极高 | | Compose | docker compose | podman-compose / podman compose | | K8s 友好 | Pod 概念无 | 原生 Pod | | 性能 | 略好(daemon overhead 摊销) | 接近 | | 生态成熟度 | 极高 | 高 + 增长中 | | RHEL / Fedora 默认 | ❌ | ✅ | Red Hat 系(RHEL / CentOS Stream / Fedora)官方推 podman。 其它 distro 都装得上。 ## 适用场景 ✅ **podman 适合**: - 单机部署 + 喜欢 systemd 集成 - 多用户开发机(rootless 安全) - 不需要 docker swarm - RHEL / Fedora 用户 - K8s 学习(pod 概念友好) ❌ **保 Docker 适合**: - 重度用 docker swarm / docker compose 高级 feature - 团队工具链都基于 docker - 公司 SaaS 工具明确支持 Docker(如 GitHub Actions docker-container action) ## 我的实际迁移 家用服务器(10+ 容器:nginx / db / 监控 / 个人 app)从 docker 迁 podman + Quadlet: ```ini # ~/.config/containers/systemd/postgres.container [Container] Image=postgres:16-alpine PublishPort=127.0.0.1:5432:5432 Environment=POSTGRES_PASSWORD=... Volume=postgres-data.volume:/var/lib/postgresql/data HealthCmd=pg_isready -U postgres HealthInterval=10s [Install] WantedBy=default.target ``` 每个容器一个 .container 文件,git 管。 重启服务器后 systemd 自动按顺序起所有容器。 ### 效果 - 总内存:少 200 MB(无 dockerd 常驻) - 部署 / 重启容器跟 systemctl service 一致 - 容器崩溃 / OOM / 异常退出:journal 里完整 log + restart 历史 - rootless 让"容器漏洞 → 拿 host root" 这条路被堵 - 一致 backup:备份 ~/.local/share/containers + ~/.config = 完整状态 ## 踩过的坑 1. **rootless 网络默认 slirp4netns** 慢:吞吐量大幅低于 root docker。 高流量场景配 `pasta` (新版默认) 或者 root pod。 2. **rootless 不能跨用户访问 image**:每个用户独立 storage。 `podman --root=/path/to/system-storage` 可以共享但复杂。 3. **某些 docker-only env var**:如 `DOCKER_HOST` socket 路径。 podman 用 unix socket 在 `$XDG_RUNTIME_DIR/podman/podman.sock`。 4. **selinux 标签**:volume mount 缺 `:Z` / `:z` 后缀容器内访问权限错。 `-v /path:/path:Z` 让 SELinux 自动 relabel。 5. **GPU passthrough**:rootless + GPU 不直接 work。`--device /dev/nvidia*` + udev rule 配。生产推荐 root podman 跑 GPU 容器。

Docker BuildKit + cache mount:CI build 从 8 分钟降到 90 秒

## 起因 我们的 Node + Python 微服务 CI 每次 build: 1. `apt install` 系统依赖:1 分钟 2. `npm ci` 装 node_modules:2.5 分钟 3. `pip install` 装 Python 依赖:1.5 分钟 4. webpack 编译:2 分钟 5. push image:1 分钟 总 8 分钟。改一行代码 → 等 8 分钟。每天 30 次 build = 4 小时浪费。 BuildKit 是 Docker 的新 builder(默认开启),支持 cache mount / secret mount / 并行 stage 构建。配合 CI 端 layer 缓存, 重复 build 大部分步骤跳过。 ## 解决方案 ### 1. 启用 BuildKit ```bash # 现代 Docker Desktop / docker 23+ 默认开 # 如果没开: export DOCKER_BUILDKIT=1 docker build . # Compose 用 buildx: docker compose build --progress plain ``` ### 2. cache mount for package managers 旧 Dockerfile: ```dockerfile FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci # 每次都从零下载 + 装 COPY . . RUN npm run build ``` 每改一行代码 → COPY 之后所有 layer 失效 → `npm ci` 重新下整套依赖。 加 cache mount: ```dockerfile # syntax=docker/dockerfile:1.7 FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm,sharing=locked \ npm ci --prefer-offline COPY . . RUN --mount=type=cache,target=/root/.npm,sharing=locked \ npm run build ``` `--mount=type=cache` 创建一个跨 build 持久的 cache 目录(在 buildkit 之内,不进最终 image)。npm 下载的包缓存到那里,下次 build 直接命中。 第一行 `# syntax=docker/dockerfile:1.7` 必须,开启高级 Dockerfile 语法。 效果:第一次 build 2.5 分钟;后续 npm install 5-10 秒。 ### 3. apt cache mount ```dockerfile FROM ubuntu:24.04 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt update \ && apt install -y --no-install-recommends \ build-essential libpq-dev \ && rm -rf /var/lib/apt/lists/* ``` 第一次跑 apt-get update 30 秒;后续每次 build 这一步 < 5 秒。 ### 4. pip / uv cache ```dockerfile FROM python:3.12-slim # 用 uv 极快 COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /app COPY pyproject.toml uv.lock ./ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-install-project COPY . . RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen CMD ["uv", "run", "gunicorn", "myapp:app"] ``` uv 本身就快,加 cache mount 几乎是"第一次外,永远秒级"。 ### 5. multi-stage build:让产物 image 不带 build dep ```dockerfile # === build stage === FROM node:20-slim AS builder WORKDIR /app COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci COPY . . RUN --mount=type=cache,target=/root/.npm \ npm run build # === runtime stage === FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 ``` 最终 image 只有 nginx + 静态文件,不含 node_modules / 源码。 小、安全、启动快。 ### 6. 让 CI 跨 job 缓存 layer GitHub Actions: ```yaml - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v6 with: context: . push: true tags: ghcr.io/myorg/myapp:latest cache-from: type=gha cache-to: type=gha,mode=max ``` `type=gha` 用 GitHub Actions 自带 cache 后端。 重复 build 跨 job 命中 cache。 GitLab CI: ```yaml build: image: docker:cli services: [docker:dind] script: - docker buildx create --use - docker buildx build --cache-from type=registry,ref=$CI_REGISTRY_IMAGE/cache --cache-to type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max --push -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . ``` ### 7. secret mount(不让 secret 进 image layer) ```dockerfile RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci ``` ```bash docker build --secret id=npmrc,src=$HOME/.npmrc . ``` `.npmrc` 在 build 时挂入,build 完不留在 image 任何 layer。 比 ARG 安全(ARG 进 history 可被 `docker history` 看到)。 ### 8. 并行 stage ```dockerfile FROM base AS deps RUN install_deps FROM base AS assets RUN compile_assets FROM runtime COPY --from=deps /opt/deps /opt/deps COPY --from=assets /opt/assets /opt/assets ``` BuildKit 自动并行无依赖的 stage(`deps` 和 `assets` 同时跑)。 ## 效果 按上面优化后我们的 build: | 步骤 | 之前 | 之后 | |---|---|---| | apt install | 60s | 5s | | npm ci | 150s | 8s | | pip install | 90s | 5s | | webpack | 120s | 60s(业务代码改了仍要 build) | | push | 60s | 10s(只 push 改了的 layer) | | **总** | **8m** | **~90s** | CI iteration 速度提升 5x,开发体验改善明显。 ## 调试 BuildKit ```bash # 详细输出 docker build --progress plain -t myapp . # 看 build cache docker buildx du # 清 cache(如果 cache 损坏 / 太大) docker buildx prune docker buildx prune --all ``` ## 踩过的坑 1. **没写 `# syntax=...` 注释**:cache mount 等高级语法不识别, 报 "unknown flag --mount"。第一行必加。 2. **`sharing=locked` 重要**:多个 build 并发跑同一 cache → race condition 损坏。`locked` 让 buildkit 串行访问。 3. **CI cache 太大被 evict**:GitHub Actions cache 上限 10 GB / repo, `mode=max` 缓存所有 layer 容易超。改 `mode=min` 只缓存最终层, 或定期清理。 4. **multi-arch build 慢**:`--platform linux/amd64,linux/arm64` 时 QEMU 模拟另一架构很慢。改用 native runner(GitHub 提供 arm64 runner, 或 self-host)。 5. **layer 顺序错**:把变化频繁的 `COPY . .` 放在装依赖前 → 每次代码 改都让依赖层失效。永远先 COPY package files 装依赖,再 COPY 代码。

在 LXC 容器里跑一个隔离的开发环境(比 Docker 更像虚拟机)

Docker 适合"一进程 + 镜像分发"。LXC(system container)适合"我要一个 完整的 Linux,但不想多装一台 VM"——它共享宿主内核但提供完整的 init / cron / ssh / multiple processes,像轻量 VM。 适合场景:本地开发环境隔离(一个项目一个 LXC,不脏宿主)、 跑老版本 Ubuntu 试旧软件、做 CI 隔离。 ## 安装 LXD(LXC 的现代封装) ```bash sudo snap install lxd sudo lxd init # 一路 Enter 用默认值:dir storage、新的 lxdbr0 网桥、不加 cluster sudo usermod -aG lxd $USER # 重新登录让 group 生效 ``` ## 启动一个容器 ```bash # 列可用镜像 lxc image list images: ubuntu/22.04 | head # 启动 lxc launch images:ubuntu/22.04 devbox lxc list # +--------+---------+----------------------+------+-----------+-----------+ # | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | # +--------+---------+----------------------+------+-----------+-----------+ # | devbox | RUNNING | 10.10.20.42 (eth0) | | CONTAINER | 0 | # +--------+---------+----------------------+------+-----------+-----------+ ``` ## 进去 / 跑命令 ```bash # 交互 shell lxc exec devbox -- bash # 一次性命令 lxc exec devbox -- apt update lxc exec devbox -- apt install -y python3-pip git # 以普通用户身份进 lxc exec devbox -- su - ubuntu ``` ## 挂载宿主目录 ```bash # 把宿主 /home/yourname/code/myproj 挂到容器 /opt/myproj lxc config device add devbox myproj disk \ source=/home/yourname/code/myproj \ path=/opt/myproj ``` ID 映射的小坑:默认 LXD 用 unprivileged container(UID 映射), 宿主的 `1000:1000` 在容器内是 `1000:1000`,权限正确。但如果宿主用了 NFS、 overlay 等,映射可能不直观,遇到再 `lxc config show devbox` 看。 ## 端口映射 / 反代 容器有 IP 但 NAT 在宿主桥后。要把容器的 8080 暴露到宿主: ```bash lxc config device add devbox webproxy proxy \ listen=tcp:0.0.0.0:18080 \ connect=tcp:127.0.0.1:8080 ``` 之后 `curl http://<宿主-IP>:18080/` 转到容器内 8080。 ## 拍快照 / 还原 ```bash lxc snapshot devbox before-upgrade lxc restore devbox before-upgrade lxc info devbox | grep -A 5 Snapshots ``` 非常适合"我要装一个可能搞坏环境的东西,万一不行就回去"。 ## 限制资源 ```bash lxc config set devbox limits.cpu 2 lxc config set devbox limits.memory 2GB lxc config set devbox limits.memory.swap false lxc restart devbox ``` ## 模板化 ```bash # 把 devbox 当模板 lxc snapshot devbox base lxc publish devbox/base --alias my-dev-base # 之后随时拉一份新的出来 lxc launch my-dev-base devbox2 ``` ## 自动启动 ```bash lxc config set devbox boot.autostart true lxc config set devbox boot.autostart.priority 10 ``` ## 与 Docker 对比 | 维度 | LXC (system container) | Docker (app container) | |---|---|---| | 进程数 | 多进程,有 init | 默认单进程(PID 1) | | 文件系统 | 完整 distro | 极简 image | | 启动速度 | 1-2 秒 | < 1 秒 | | 生命周期 | 长(像 VM) | 短(每次启动是新实例) | | 应用分发 | 不方便 | 主流(image registry) | | 编排生态 | LXD cluster | Kubernetes、Swarm、Compose | ## 踩过的坑 - 默认 storage backend 是 `dir`(裸目录),性能差且不支持快照高效复制。 生产 / 重度使用换 `btrfs` 或 `zfs`:`sudo lxd init` 时选。 - 容器内 dmesg / mount 等命令可能权限不够 —— 这是 unprivileged container 的预期行为,不是 bug。需要的话 `lxc config set devbox security.privileged true`, 但安全等同 root。 - LXD vs LXC:`apt install lxc` 装的是底层工具;建议直接 `snap install lxd` 用更现代的 API。两者底层兼容但命令完全不同。 - Snap 版 LXD 升级时容器会随之重启,业务跑在 LXD 上的要避开升级窗口。

用 kubeadm 在裸金属 / VPS 上起一个最小 Kubernetes 集群

云厂商的 K8s(EKS / GKE / AKS)按月几十美元。自己买几台便宜 VPS 也能跑 K8s——一台 control plane + 2 台 worker,每月几美元。 适合自学 / 个人项目 / 小规模生产。 下面用 kubeadm 起 v1.30 集群。 ## 准备(每台机器都做) ```bash # 关 swap(K8s 要求) sudo swapoff -a sudo sed -i '/swap/d' /etc/fstab # 内核模块 cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay br_netfilter # sysctl cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system ``` ## 装 containerd ```bash sudo apt update sudo apt install -y containerd sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml # 启 SystemdCgroup(K8s 要求) sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml sudo systemctl restart containerd ``` ## 装 kubeadm / kubelet / kubectl ```bash sudo apt install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \ | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' \ | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt update sudo apt install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl ``` ## 初始化 control plane(只在 master 上) ```bash sudo kubeadm init \ --pod-network-cidr=10.244.0.0/16 \ --apiserver-advertise-address=<MASTER_IP> ``` 完成后会打印一段 `kubeadm join ...` 命令——保存下来,下面 worker 用。 让普通用户用 kubectl: ```bash mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config kubectl get nodes # 状态会是 NotReady —— 因为没装 CNI ``` ## 装 CNI(这里用 Flannel) ```bash kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml # 等 1-2 分钟 kubectl get pods -A kubectl get nodes # Ready 了 ``` 其它 CNI 选择: - **Calico**:网络策略 + BGP,生产更常用 - **Cilium**:eBPF,性能 + 观测最好 - **Flannel**:最简单,仅 overlay 网络 ## Worker 加入 每台 worker 上: ```bash sudo kubeadm join <MASTER_IP>:6443 \ --token <TOKEN> \ --discovery-token-ca-cert-hash sha256:<HASH> ``` Master 上: ```bash kubectl get nodes # NAME STATUS ROLES AGE VERSION # master Ready control-plane 5m v1.30.0 # worker-1 Ready <none> 1m v1.30.0 # worker-2 Ready <none> 1m v1.30.0 ``` ## 部署个 demo ```yaml # nginx-demo.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 3 selector: matchLabels: { app: nginx } template: metadata: { labels: { app: nginx } } spec: containers: - name: nginx image: nginx:alpine ports: [{ containerPort: 80 }] --- apiVersion: v1 kind: Service metadata: name: nginx spec: type: NodePort selector: { app: nginx } ports: - port: 80 nodePort: 30080 ``` ```bash kubectl apply -f nginx-demo.yaml kubectl get pods -o wide curl http://<任意-Node-IP>:30080 ``` ## Ingress(暴露 80/443 到公网) NodePort 30000+ 端口不友好,用 Ingress: ```bash kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/baremetal/deploy.yaml ``` ```yaml # ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: { name: nginx, port: { number: 80 } } ``` 把 `<MASTER_IP>` 或 worker IP DNS 指到 `app.example.com`, 80/443 流量进 ingress-nginx,再分发到 service。 ## cert-manager 自动 HTTPS ```bash kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml ``` ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: { name: letsencrypt-prod } spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: { name: letsencrypt-prod } solvers: - http01: ingress: { class: nginx } ``` Ingress 加 annotation `cert-manager.io/cluster-issuer: letsencrypt-prod`, 自动签发 + 续期证书。 ## 备份 etcd ```bash # 在 master 上 sudo ETCDCTL_API=3 etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /tmp/etcd-$(date +%F).db ``` 定时备份这个文件到对象存储是单 master K8s 集群唯一的 DR 手段。 ## 升级 ```bash sudo apt-mark unhold kubeadm sudo apt install -y kubeadm=1.30.5-* sudo apt-mark hold kubeadm sudo kubeadm upgrade plan sudo kubeadm upgrade apply v1.30.5 # 然后升 kubelet / kubectl ``` ## 踩过的坑 - swap 没关 kubelet 启动失败:v1.22 之前默认要求;v1.22+ 可以保留 swap 但要在 kubelet 配置里显式 `failSwapOn: false`。 - 防火墙:control plane 需要开 6443、10250、2379-2380,CNI 用的端口 各异(Flannel UDP 8472)。最简单:把 master / worker 之间的网络 完全开放(内网或 VPC)。 - Token 24 小时过期:worker 加入时 `kubeadm token create --print-join-command` 生成新 token。 - 单 master 没冗余:宕机就完蛋。生产多 master + HA load balancer + 外部 etcd cluster。kubeadm 也支持。