1. 项目概述:当软件交付变成“集装箱运输”
“DOCKER — Shipping Containers to the Innovative World!!”这个标题不是修辞,而是精准的工程隐喻。我第一次在2014年看到它时,正被三台测试服务器上反复出现的“在我机器上是好的”问题折磨得睡不着觉——开发说代码没问题,运维说环境配置全对,QA说用例跑不通,而我夹在中间,每天花两小时手动比对Python版本、pip包依赖树、系统库路径和防火墙规则。直到我把一个Web服务打包成Docker镜像,在本地build完直接scp到生产服务器docker run -d,三分钟启动,零配置差异,那一刻我才真正理解:Docker不是又一个虚拟化工具,它是把“软件交付”这件事,从靠人肉记忆和文档传递的“手工业时代”,推进到标准化、可验证、可追溯的“工业流水线时代”。
核心关键词“Docker”“Shipping Containers”“Innovative World”已经点明本质:它解决的从来不是技术炫技问题,而是 交付确定性 这个卡住所有创新落地的咽喉。所谓“创新世界”,不是指实验室里的新算法,而是指业务部门催着上线的A/B测试功能、安全团队要求72小时内修复的漏洞补丁、还有凌晨三点告警后必须秒级回滚的线上事故。这些场景里,最致命的从来不是代码bug,而是“环境漂移”——开发机上用OpenSSL 1.1.1w跑通的TLS握手,在生产环境CentOS 7默认的1.0.2k上直接失败;或者CI流水线里npm install成功的依赖,在运维手动部署时因网络策略变了就卡死。Docker用镜像(Image)作为不可变交付单元,把应用二进制、运行时、系统工具、库文件甚至内核模块参数全部固化为一层层只读文件系统快照,再通过容器(Container)运行时按需挂载可写层。这就像海运集装箱:无论里面装的是汽车零件还是冷冻海鲜,只要符合ISO标准尺寸,就能用同一套吊装设备、同一张提单、同一套海关流程运到全球任何港口。你不需要知道货柜里具体怎么堆码,只需要确认集装箱编号、封条完好、重量合规——Docker镜像ID、SHA256摘要、运行时资源限制,就是新时代的“集装箱提单号”。
适合谁来读?如果你是刚接触容器的新手,这篇会告诉你为什么非要用docker build而不是直接在服务器上pip install;如果你是资深运维,这里会拆解cgroups v2内存压力测试的真实数据;如果你是架构师,你会看到如何用多阶段构建把3GB的Node.js镜像压缩到87MB;如果你是安全工程师,我会展示如何用Trivy扫描出镜像里潜伏三年的CVE-2021-44228(Log4j)漏洞。这不是Docker官方文档的翻译,而是我在电商大促压测、金融系统等保加固、AI模型服务化落地中,用掉27个不同Linux发行版、踩过137次OOM Killer误杀、重写了41版Dockerfile后沉淀下来的硬核经验。接下来的内容,每一行命令都经过生产环境验证,每一个参数都有明确的取舍逻辑,每一条警告都来自血泪教训。
2. 核心设计思路:为什么是容器,而不是虚拟机或配置管理?
2.1 交付链路的断点分析:从代码到生产的七道关卡
要理解Docker的设计哲学,得先画出传统软件交付的完整链条。以一个Python Flask API为例,从开发者敲下git commit开始,到最终在Kubernetes集群里稳定提供服务,中间至少经历七个关键断点:
- 开发环境断点 :MacBook上用Homebrew安装的PostgreSQL 15.3,与CentOS 7.9默认的9.2.24存在SQL语法兼容性差异;
- 构建环境断点 :Jenkins slave节点用的Docker 20.10.12,而生产服务器是19.03.15,导致--platform参数解析异常;
- 依赖解析断点 :requirements.txt里写requests>=2.25.0,但pip install实际拉取2.28.2,其底层urllib3在高并发下触发内核TCP连接复用bug;
- 系统配置断点 :开发机ulimit -n 65536,生产环境默认1024,导致连接池耗尽;
- 安全策略断点 :SELinux enforcing模式下,容器进程无法绑定80端口,错误日志却只显示"Permission denied";
- 监控埋点断点 :Prometheus exporter在容器内监听localhost:9100,但宿主机网络策略禁止loopback流量;
- 回滚验证断点 :紧急回滚到v2.1.0镜像,却发现该版本Dockerfile里用了已被废弃的--no-cache-dir参数,导致构建失败。
这些断点共同构成“环境不确定性三角”: 时间不确定性 (不同时间部署的环境状态不同)、 空间不确定性 (不同服务器的配置基线不同)、 操作不确定性 (人工执行步骤的微小差异)。虚拟机(VM)方案试图用“完整操作系统快照”解决,但代价是启动慢(分钟级)、资源开销大(每个VM独占GB级内存)、镜像体积臃肿(动辄5-10GB)。Ansible/Puppet这类配置管理工具则走向另一个极端:它把环境当作“待修复的问题”,每次部署都要执行一长串幂等性检查和修正操作,而这些操作本身又引入新的不确定性——比如Ansible的yum模块在CentOS 8上默认使用dnf,但某些定制镜像里dnf被卸载了,任务就静默失败。
Docker的破局点在于 分层抽象 :它不模拟硬件(如VM),也不编排操作(如Ansible),而是把“软件运行所需的一切”封装为 声明式文件系统快照 。这个设计有三个不可替代的优势:
- 启动速度 :容器进程直接运行在宿主机内核上,没有Hypervisor层开销。实测一个Nginx容器从docker run到响应HTTP请求,平均耗时127ms(i7-10875H+NVMe SSD),而同等配置的KVM虚拟机需要2.3秒;
- 资源效率 :10个相同镜像的容器共享基础镜像层,内存页去重(KSM)后实际占用仅为单个进程的1.3倍,而非10倍;
- 可验证性 :镜像ID是内容寻址哈希值(如sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fd4ed0c6c63295a05765a5369d586),任何人pull同一个ID的镜像,解压后字节级完全一致——这是VM快照或Ansible Playbook永远做不到的确定性。
提示:不要被“轻量级虚拟化”这种说法误导。Docker容器与虚拟机在技术原理上毫无关系:VM通过CPU硬件虚拟化指令(Intel VT-x/AMD-V)截获特权指令,而容器完全依赖Linux内核的namespaces(隔离PID/NET/UTS等命名空间)和cgroups(控制CPU/内存/IO资源配额)。你可以用unshare命令手动创建namespace,但无法用QEMU启动一个“容器”。
2.2 镜像分层机制:如何用5个层实现99%的复用率
Docker镜像的分层存储是其高效协作的核心。以官方Python镜像为例,执行docker history python:3.11-slim,能看到典型的五层结构:
| LAYER | SIZE | CREATED | COMMENT |
|---|---|---|---|
| sha256:7a04... | 127MB | 2 weeks ago | ADD rootfs.tar.xz |
| sha256:9b8e... | 1.2MB | 2 weeks ago | CMD ["python3"] |
| sha256:5c3e... | 34MB | 3 weeks ago | RUN set -eux; apt-get update ... |
| sha256:8d2e... | 28MB | 3 weeks ago | RUN apt-get update && apt-get install -y ca-certificates ... |
| sha256:4f4f... | 0B | 3 weeks ago | FROM debian:bookworm-slim |
关键洞察在于: 最底层的debian:bookworm-slim是所有基于Debian镜像的公共基座 。当你构建自己的应用镜像时,Docker daemon会自动复用本地已有的基础层,只下载新增的层。我们团队曾统计过2023年Q3的镜像仓库数据:在127个微服务镜像中,89%的镜像共享同一组基础层(debian/ubuntu + Python/Node.js运行时),导致镜像仓库总存储节省了63%。更妙的是,分层还天然支持 增量构建 ——修改Dockerfile第15行RUN指令后,Docker只会重新执行该层及之后的层,前面的apt-get install层完全复用缓存。
但分层也带来陷阱。最常见的反模式是“单层巨镜像”:
# ❌ 危险:把所有操作塞进一层
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3-pip nginx && \
pip3 install flask gunicorn && \
echo "hello" > /var/www/index.html
这会导致:① 每次修改HTML文件都要重新执行整个apt-get流程(网络不稳定时可能失败);② pip install生成的wheel缓存无法跨镜像复用;③ 安全扫描时,nginx和flask的漏洞会混在同一层,无法精准定位修复点。
正确做法是遵循 分层原则 :
# ✅ 推荐:按变更频率分层
FROM ubuntu:22.04
# 第1层:操作系统基础(极低频变更)
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl && \
rm -rf /var/lib/apt/lists/*
# 第2层:运行时环境(低频变更)
RUN curl -sSL https://install.python-poetry.org | python3 - && \
ln -sf /root/.local/bin/poetry /usr/local/bin/poetry
# 第3层:应用依赖(中频变更)
COPY pyproject.toml poetry.lock ./
RUN poetry install --no-dev --no-interaction
# 第4层:应用代码(高频变更)
COPY src/ /app/src/
WORKDIR /app
这样设计后,开发人员修改src/目录下的代码,构建时只会重新加载第4层(通常<1MB),而前3层(约200MB)全部命中缓存。我们在CI流水线中实测,这种分层使平均构建时间从4分32秒降至58秒,提速近5倍。
2.3 容器运行时演进:从runc到containerd再到gVisor
很多人以为“Docker = 容器”,其实Docker只是容器生态的入口。它的架构是典型的客户端-服务器模式:docker CLI发送HTTP请求给dockerd守护进程,dockerd再调用底层运行时(Runtime)创建容器。这个分层设计让Docker可以灵活切换不同的运行时引擎。
- runc :当前事实标准,由OCI(Open Container Initiative)制定规范,直接调用Linux内核API创建namespace和cgroups。它的优势是极致性能(启动延迟<10ms),但隔离性较弱——容器内进程仍能访问宿主机/proc/sys,存在逃逸风险;
- containerd :Docker公司开源的工业级容器运行时,比dockerd更轻量、更稳定。Kubernetes自1.20起弃用dockershim,全面转向containerd。它支持插件化存储驱动(如ZFS、Btrfs)和网络插件(CNI),是云厂商首选;
- gVisor :Google开发的安全沙箱,通过用户态内核(runsc)拦截所有系统调用,提供类似VM的强隔离。实测在gVisor下运行恶意代码(如fork炸弹)不会影响宿主机,但性能损失达40%(CPU密集型场景)。
选择运行时的关键决策点:
- 生产环境 :无脑选containerd。它经过万亿级容器调度验证,crictl命令比docker CLI更简洁(如crictl ps -a替代docker ps -a);
- 多租户平台 :混合使用。可信服务用containerd,第三方插件用gVisor沙箱;
- 边缘计算 :考虑Firecracker(AWS FireLens用的微虚拟机),启动时间120ms,内存占用仅5MB,比容器更适合IoT设备。
注意:不要在生产环境混用运行时。我们曾因在K8s节点上同时安装docker-ce和containerd,导致kubelet无法识别容器状态,引发滚动更新卡死。解决方案是彻底卸载docker-ce,只保留containerd,并用crictl调试。
3. 核心实操要点:从Dockerfile编写到生产就绪
3.1 Dockerfile黄金十二法则:每一条都是血换来的
Dockerfile不是脚本,而是 构建时的声明式契约 。以下十二条是我在200+个生产镜像中提炼的硬性规范,违反任意一条都可能导致线上事故:
-
永远指定基础镜像标签
FROM python:3.11-slim✅
FROM python❌(会拉取latest,某天突然变成3.12,破坏兼容性) -
用ARG定义构建参数,而非ENV
# ✅ 构建时注入,不污染镜像环境变量 ARG BUILD_DATE LABEL org.opencontainers.image.created=$BUILD_DATE # ❌ ENV在镜像中永久存在,可能泄露敏感信息 ENV BUILD_DATE=2023-10-01 -
多阶段构建强制启用
编译型语言(Go/Rust)必须分离构建环境和运行环境:# 第一阶段:构建 FROM golang:1.21-alpine AS builder WORKDIR /app COPY . . RUN go build -o myapp . # 第二阶段:精简运行 FROM alpine:3.18 COPY --from=builder /app/myapp /usr/local/bin/myapp CMD ["myapp"]实测将Go应用镜像从1.2GB压缩至12MB,且无libc.so等冗余库。
-
非root用户是底线
RUN addgroup -g 1001 -f appgroup && \ adduser -S appuser -u 1001 USER appuserKubernetes PodSecurityPolicy已强制要求non-root,否则拒绝调度。
-
健康检查必须真实有效
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1
❌ 错误:用ps aux | grep myapp检测进程存在,但进程可能已假死;
✅ 正确:调用应用内置的健康端点,返回HTTP 200才认为存活。 -
清理构建缓存和临时文件
RUN apt-get update && apt-get install -y build-essential && \ pip3 install --no-cache-dir -r requirements.txt && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*否则镜像体积膨胀300%,且apt缓存可能包含已撤销的GPG密钥。
-
COPY优于ADD
ADD会自动解压tar文件并从URL下载,带来安全风险(如ADD http://malicious.com/exploit.tgz);COPY只做文件复制,语义清晰。 -
WORKDIR必须绝对路径
WORKDIR /app✅
WORKDIR app❌(相对路径在不同基础镜像中行为不一致) -
暴露端口用EXPOSE声明,但不绑定
EXPOSE 8000只是文档说明,实际端口映射由docker run -p控制。避免在Dockerfile里写-p 8000:8000。 -
CMD用JSON数组格式
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]✅
CMD gunicorn --bind 0.0.0.0:8000 app:app❌(shell格式会启动/bin/sh -c,导致PID 1不是gunicorn,无法接收信号) -
设置合理的STOPSIGNAL
STOPSIGNAL SIGTERM(默认是SIGTERM,但显式声明可避免误解) -
镜像元数据必须完整
LABEL org.opencontainers.image.authors="devops@company.com" \ org.opencontainers.image.documentation="https://docs.company.com/docker" \ org.opencontainers.image.source="https://gitlab.company.com/app/repo"这是审计追踪的法律依据,等保2.0要求必须提供。
3.2 生产环境容器配置:不只是docker run -d
在生产环境,
docker run
只是玩具。真正的就绪配置需要覆盖五大维度:
3.2.1 资源限制:防止邻居效应(Noisy Neighbor)
不设限制的容器会吞噬所有可用资源。用
docker stats
观察未限制容器的内存使用,常看到RSS持续增长直至触发OOM Killer。正确姿势:
# 内存:硬限制+软限制+交换禁用
docker run -m 512m --memory-reservation 256m --memory-swap 512m \
--oom-kill-disable=false nginx
# CPU:份额+限额+集
docker run --cpus 2.5 --cpu-shares 512 --cpuset-cpus "0-3" \
--pids-limit 100 nginx
-
--memory是硬上限,超限立即OOM Kill; -
--memory-reservation是软限制,当系统内存紧张时开始回收; -
--memory-swap控制swap使用量,设为等于--memory即禁用swap(推荐); -
--cpus 2.5等价于--cpu-quota 250000 --cpu-period 100000,精确控制CPU时间片。
实操心得:在Kubernetes中,request/limit对应这里的reservation/limit。我们曾因将Java应用的Xmx设为2G,但K8s limit只设1.5G,导致JVM频繁Full GC。解决方案是:Xmx = limit * 0.75,并开启-XX:+UseContainerSupport(JDK8u191+原生支持cgroups内存限制)。
3.2.2 安全加固:从根用户到seccomp
默认容器拥有大量危险系统调用权限。用
docker run --cap-drop=ALL
禁用所有能力,再按需添加:
# 最小能力集:仅允许网络和文件操作
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--cap-add=CHOWN --cap-add=FOWNER nginx
# 禁用特定系统调用(如禁用mount防止挂载宿主机磁盘)
docker run --security-opt seccomp=./no-mount.json nginx
seccomp配置文件
no-mount.json
示例:
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["mount", "umount", "umount2"],
"action": "SCMP_ACT_ERRNO"
}
]
}
3.2.3 日志管理:避免容器内磁盘爆满
Docker默认用json-file日志驱动,日志文件无限增长。生产必须配置轮转:
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
对于高吞吐日志(如ELK栈),改用
fluentd
驱动直传:
docker run --log-driver=fluentd \
--log-opt fluentd-address=localhost:24224 \
--log-opt tag="app.web" nginx
3.2.4 存储卷:区分tmpfs、bind mount和volume
-
tmpfs: 内存临时存储,适合session缓存(--tmpfs /app/sessions:rw,size=64m); -
bind mount: 宿主机目录映射,用于配置热更新(-v /host/config:/app/config:ro),但注意SELinux上下文; -
named volume: Docker管理的持久化存储,适合数据库(-v db-data:/var/lib/mysql),自动处理权限和备份。
警告:永远不要用
-v /home/user/data:/data这种绝对路径bind mount。当容器在另一台服务器运行时,路径不存在导致启动失败。应统一用named volume。
3.2.5 网络配置:bridge、host、none的取舍
-
--network bridge(默认):Docker网桥,端口映射(-p)安全但有NAT开销; -
--network host:共享宿主机网络命名空间,性能最佳(无iptables转发),但端口冲突风险高,K8s中需privileged权限; -
--network none:完全隔离,需手动配置网络,适合安全沙箱。
我们生产环境采用混合策略:API网关用host网络(降低延迟),业务服务用bridge(保障隔离),批处理作业用none(防网络攻击)。
4. 实战全流程:从本地开发到CI/CD流水线
4.1 本地开发工作流:docker-compose的正确打开方式
docker-compose.yml
不是简单的容器编排,而是
本地开发环境的契约说明书
。一个健壮的配置需包含:
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile.dev # 开发专用Dockerfile
args:
- NODE_ENV=development
image: myapp-web:dev
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://db:5432/myapp
- REDIS_URL=redis://cache:6379
volumes:
- ./src:/app/src:delegated # 代码热重载
- node_modules:/app/node_modules # 避免覆盖node_modules
depends_on:
- db
- cache
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: devpass
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
interval: 30s
timeout: 10s
retries: 5
volumes:
pgdata:
node_modules:
关键技巧:
- volumes的delegated模式 :在macOS/Windows上解决文件同步延迟问题(默认cached模式导致nodemon检测不到文件变化);
-
健康检查驱动依赖
:
depends_on只控制启动顺序,不等待服务就绪。必须配合healthcheck和condition: service_healthy(v2.1+); -
开发专用Dockerfile
:启用source map、禁用minify、挂载debugger端口(
EXPOSE 9229)。
4.2 CI/CD流水线设计:GitOps驱动的自动化
我们采用GitLab CI实现全自动镜像构建与部署,
.gitlab-ci.yml
核心逻辑:
stages:
- build
- test
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
build-image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
# 多平台构建(amd64/arm64)
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG \
--tag $CI_REGISTRY_IMAGE:latest \
--push .
rules:
- if: $CI_COMMIT_TAG
test-container:
stage: test
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
script:
- curl -f http://localhost:8000/health
- python -m pytest tests/ --cov=app
services:
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
alias: app
关键实践:
- 镜像签名 :用cosign对推送的镜像签名,K8s admission controller验证签名后才允许部署;
-
漏洞扫描集成
:在build阶段插入Trivy扫描:
trivy image --severity CRITICAL,HIGH --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG - 语义化版本控制 :Git tag v1.2.3自动触发构建,镜像标签严格对应,便于回溯。
4.3 生产部署检查清单:上线前的15个必答问题
每次生产部署前,我都会逐条核对这份清单,漏掉任何一项都暂停发布:
| 序号 | 检查项 | 验证方法 | 不通过后果 |
|---|---|---|---|
| 1 | 镜像是否通过安全扫描? |
trivy image --severity CRITICAL myapp:v1.2.3
| 高危漏洞导致RCE |
| 2 | 是否启用非root用户? |
docker inspect myapp:v1.2.3 | jq '.[0].Config.User'
| PodSecurityPolicy拒绝调度 |
| 3 | 健康检查端点是否真实可用? |
curl -I http://localhost:8000/health
| K8s认为Pod不健康,反复重启 |
| 4 | 内存限制是否大于JVM Xmx? |
docker inspect myapp:v1.2.3 | jq '.[0].HostConfig.Memory'
| JVM OOM Kill,但容器未退出 |
| 5 | 日志驱动是否配置轮转? |
docker info | grep "Logging Driver"
| 磁盘100%,节点失联 |
| 6 | 是否禁用swap? |
docker inspect myapp:v1.2.3 | jq '.[0].HostConfig.MemorySwap'
| 内存压力下性能骤降 |
| 7 | 网络模式是否匹配? |
docker inspect myapp:v1.2.3 | jq '.[0].HostConfig.NetworkMode'
| host模式端口冲突 |
| 8 | 卷挂载是否使用named volume? |
docker inspect myapp:v1.2.3 | jq '.[0].Mounts'
| bind mount路径不存在导致启动失败 |
| 9 | 是否设置STOPSIGNAL? |
docker inspect myapp:v1.2.3 | jq '.[0].Config.StopSignal'
| SIGTERM无法优雅关闭,数据丢失 |
| 10 | 镜像标签是否唯一? |
docker images | grep myapp
| latest标签被覆盖,回滚失效 |
| 11 | 是否启用cgroup v2? |
stat -fc %T /sys/fs/cgroup
| cgroup v1下内存限制不准确 |
| 12 | 容器内时区是否正确? |
docker run --rm myapp:v1.2.3 date
| 日志时间错乱,排查困难 |
| 13 | 是否禁用不必要能力? |
docker inspect myapp:v1.2.3 | jq '.[0].HostConfig.CapDrop'
| 容器逃逸风险 |
| 14 | 镜像大小是否合理? |
docker images myapp:v1.2.3
| 过大镜像拖慢部署,增加攻击面 |
| 15 | 是否有回滚预案? |
kubectl rollout undo deployment/myapp
| 故障时无法快速恢复 |
这份清单源于我们一次惨痛教训:某次部署因忘记检查第4项(内存限制),导致JVM在cgroup内存限制下触发OOM,但容器进程未退出,K8s认为Pod健康,结果所有请求堆积在应用队列,APM监控显示TP99飙升至12秒。从发现问题到定位原因花了47分钟,而如果提前执行这条检查,30秒就能发现。
5. 常见问题与排查技巧实录
5.1 典型故障速查表:从症状到根因
| 症状 | 可能根因 | 排查命令 | 解决方案 |
|---|---|---|---|
docker run
后容器立即退出
| CMD格式错误(shell vs exec);应用启动后立即退出 |
docker logs <container>
;
docker inspect <container> | jq '.[0].State.Status'
|
改用JSON数组格式CMD;添加
tail -f /dev/null
调试
|
| 容器内无法解析域名 | DNS配置错误;宿主机DNS被劫持 |
docker run --rm alpine nslookup google.com
;
cat /etc/resolv.conf
|
--dns 8.8.8.8
;或修改
/etc/docker/daemon.json
的dns字段
|
docker build
卡在
apt-get update
| 阿里云镜像源失效;网络策略拦截 |
docker run --rm ubuntu:22.04 apt-get update
|
换用清华源:
sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list
|
docker push
报
denied: requested access to the resource is denied
| 未登录Registry;镜像标签格式错误 |
docker login
;
docker tag old new
| 确认registry地址和权限;标签必须含registry域名 |
容器内
df -h
显示磁盘100%
| overlay2存储驱动元数据损坏 |
docker system df -v
;
ls -lh /var/lib/docker/overlay2/
|
docker system prune -a
;严重时重置Docker(丢失所有镜像)
|
docker stats
显示内存持续增长
| 应用内存泄漏;未设置内存限制 |
docker stats --no-stream <container>
;
docker exec -it <container> top
|
用pprof分析应用;设置
-m 512m
硬限制
|
docker-compose up
报
port is already allocated
| 宿主机端口被占用;其他compose项目占用 |
sudo lsof -i :3000
;
docker-compose ps
|
kill -9 <pid>
;或改用随机端口
-p 3000
|
| 容器内时区错误 | 基础镜像未设置TZ;应用未读取TZ环境变量 |
docker run --rm alpine date
;
docker exec <container> env | grep TZ
|
ENV TZ=Asia/Shanghai
;
-e TZ=Asia/Shanghai
|
docker pull
超时
| Registry网络不通;镜像名拼写错误 |
ping registry.gitlab.com
;
curl -I https://registry.hub.docker.com/v2/
| 配置镜像加速器;检查镜像名大小写 |
| 容器内无法访问宿主机服务 | Docker网络隔离;host.docker.internal未启用 |
docker run --add-host=host.docker.internal:host-gateway --rm alpine ping host.docker.internal
|
启用
--add-host
;或用宿主机真实IP
|
5.2 深度调试技巧:超越docker logs的真相
当
docker logs
显示空白,问题往往藏在更深的层次:
技巧1:进入容器命名空间调试
普通
docker exec
无法查看网络命名空间信息。用nsenter进入:
# 获取容器PID
PID=$(docker inspect -f '{{.State.Pid}}' mycontainer)
# 查看容器网络命名空间
sudo nsenter -t $PID -n ip addr
# 查看容器挂载命名空间
sudo nsenter -t $PID -m mount \| grep overlay
技巧2:实时跟踪系统调用
用strace捕获容器内进程的系统调用:
# 在宿主机上跟踪容器内PID 1进程
sudo strace -p $(docker inspect -f '{{.State.Pid}}' mycontainer) -e trace=connect,openat,read -s 100
曾用此法发现某Java应用在启动时反复open
/dev/random
,因容器内熵池不足卡住,解决方案是挂载
/dev/urandom
到
/dev/random
。
技巧3:cgroups资源使用透视
直接读取cgroups文件系统:
# 查看内存使用详情
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.stat
# 查看CPU使用率
cat /sys/fs/cgroup/cpu/docker/<container-id>/cpuacct.usage
这比
docker stats
更底层,能发现指标采集延迟导致的误判。
技巧4:网络连通性终极验证
当ping通但应用连不通时,用tcpdump抓包:
# 在宿主机上抓容器网络接口
sudo tcpdump -i docker0 -w container.pcap host <container-ip>
# 在容器内抓包(需安装tcpdump)
docker exec -it mycontainer apk add --no-cache tcpdump
docker exec -it mycontainer tcpdump -w /tmp/app.pcap port 8000
我们曾用此法发现K8s CNI插件在高并发下丢弃SYN包,根源是iptables conntrack表溢出。
5.3 性能调优实战:让容器跑得更快更稳
内存优化
Java应用在容器内常因cgroups内存限制

907

被折叠的 条评论
为什么被折叠?



