- 镜像的分层框架 image-layering framework
- Dockerfile
- Dockerfile best practices
- Docker 网络
- 镜像管理
- 容器管理
- Docker Machine
- 容器编排
- Kubernetes
- Using Docker in production
- Summary
镜像的分层框架 image-layering framework
最底端是引导文件系统 bootfs. 当一个容器启动后,它将被移到内存,而 bootfs 会被 unmount,以留出更多内存供 initrd 磁盘镜像使用
第二层是根文件系统 rootfs
- 传统 Linux 引导过程,rootfs 会首先只读方式加载、引导结束并完成完整性检查后切换为读写模式
- Docker 中 rootfs 永远是只读状态,Docker 利用 union mount 技术会在 rootfs 上加载更多的 RO 文件系统
联合加载 ( union mount ) - 一次同时加载多个文件系统,但在外面看起来只能看到一个文件系统;但各层文件会叠加、最终 FS 包含所有的文件和目录
Docker 将这样的文件系统称为镜像。(基于镜像栈的关系,有 parent image / base image 概念)
最终当从一个镜像启动容器时,Docker 会在镜像的最顶层加载一个读写文件系统,我们想在镜像中运行的程序、就是在这个 RW 层执行的。当文件系统变化时(如修改一个文件),会从下层的 RO 层复制到该 RW 层,即写时复制 ( copy on write )
Dockerfile
Dockerfile 所在目录为构建上下文 ( build context ),Docker 会在构建时将文件和目录上传到 Docker daemon,因此通过 .dockerignore
减少不必要的文件拷贝,另外 Dockerfile 中的 Copy 等命令无法访问 context 之外的文件。
Dockerfile 中指令是自上而下执行,每条指令会创建一个新的镜像层 ( Layer ) 并提交(并返回一个 Image ID),这样每一层同时也被缓存。缓存特性会加速大部分情况下的镜像构建的,但对于类似 apt-get update
指令需要指定 --no-cache
以忽略
修改模板中的 ENV 环境变量,后续指令会重置缓存
FROM ubuntu 14:04
EVN REFRESHED_AT 2014-07-01
RUN apt-get -qq update
EXPOSE
告诉 Docker 该容器内的程序会使用容器的指定端口,但不会自动打开、需要 docker run
运行容器时 -P
公开所有通用 EXPOSE
指令公开的端口(或 -p
指定同宿主机的端口映射),通过 docker port <container> <port>
查看
CMD
容器启动时运行的命令(不同于 RUN 是容器构建时的命令),只能指定一条 CMD 指令;如果需要启动多进程或执行多指令,考虑使用 Supervisor 一类的服务管理工具;此外 docker run
命令会覆盖 CMD 指令
CMD ["/bin/ls", "-l"]
ENTRYPOINT
比较容易同 CMD 混淆;docker run
命令行的参数会传递给 ENTRYPOINT 指令中指定的命令
ENTRYPOINT ["/usr/sbin/nginx"]
# ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]
# docker run -it <image> -g "daemon off;"
组合使用 ENTRYPOINT 和 CMD,可以构建一个镜像,既可以运行默认指令、也支持通过 docker run
来覆盖
ENTRYPOINT ["/usr/sbin/nginx"]
CMD ["-h"]
ENV
构建过程的环境变量,可在后续指令中使用。也会持久化到创建的容器中,运行时环境变量可被 docker run -e
标志覆盖
VOLUME
向基于镜像创建的容器添加卷,一个卷是可以存在于一个或多个容器内的特定目录,这个目录可以绕过 Union FileSystem,并提供如下数据持久化功能:
- 卷可以在容器间共享和重用(而不需要提交到镜像)
- 对卷的修改立即生效
- 对卷的修改不会对更新镜像产生影响
通过 docker run -v
指令指定容器卷,docker run --volumes-from <container>
把指定容器的卷加入新创建的容器;容器文件复制使用 docker cp
命令;
一个日志容器:
docker run -d --name logstash \
--volumes-from redis \
--volumes-from nodeapp \
logstash
docker logs -f logstash
其他指令
- WORKDIR: 为 Dockerfile 中后续指令设置工作目录
- USER: 指定启动容器的用户身份(如
USER user:group
,USER uid:gid
),默认为 root - COPY / ADD: 将构建上下文中的文件和目录复制到镜像,
COPY
不会做文件解压,COPY 的目的位置必须是容器内部一个绝对路径,由 COPY 创建的文件及目录的 UID 和 GID 都会设置为 0 - LABEL: 为镜像添加元数据(键值对),可以用
docker inspect
指令查看 - ARG: 在
docker build
时传递给构建运行时的变量,可以设定默认值
Dockerfile best practices
# Replace latest with a pinned version tag from: https://hub.docker.com/_/alpine
# We suggest using the <major>.<minor> tag, not <major>.<minor>.<patch>
FROM alpine:latest
# Non-root user for security purposes
# UIDs below 10,000 are a security risk, as a container breakout could result in
# the container being ran as a more priviledged user on the host kernel with the same UID.
#
# Static GID/UID is also useful for chown'ing files outside the container
# where such a user does not exists.
RUN addgroup -g 10001 -S nonroot && adduser -u 10000 -S -G nonroot -h /home/nonroot nonroot
# --- Install packages here with `apk add --no-cache`, copy your binaries into /sbin/, etc ---
# Use tini as entrypoint:
# 1. It protects you from software that accidentally created zombie processes
# 2. It ensures that the default signal handlers work for the software you run in your Docker image.
# For example, with Tini, SIGTERM propertly terminates your process even if
# you didn't explicitly install a signal handler for it.
RUN apk add --no-cache tini
# Only store arguments in `CMD`.
ENTRYPOINT ["/sbin/tini", "--", "myapp"] # replace 'myapp' with your binary
# bind-tools is needed for DNS resolution to work in some Docker networks.
# This applies to nslookup, Go binaries, etc.
RUN apk add --no-cache bind-tools
USER nonroot
# Default arguments for your app (remove if you have none)
CMD ["--foo", "1", "--bar=2"]
制作容器最佳实践
- 避免使用
latest
Tag 以避免上游非兼容性更新造成影响 - 在 entrypoint 脚本中使用
EXEC
启动应用,如exec java $JAVA_OPTS -jar $JAR
来启动 Java 进程、确保进程可以处理容器运行时发送的信号(如TERM
/SIGKILL
) - 声明端口:在 Dockerfile 中使用
EXPOSE
指令显式声明;暴露的端口可以通过docker ps
/docker inspect
查看 - 声明环境变量:便于不查看 Dockerfile 时了解关键的信息,如产物版本、关键参数
- Non-Root:如果容器被攻破,容器 Root 逃逸出来就是宿主机上的 Root
- 多阶段构建:临时构建镜像安装编译所需的依赖项,生产就绪的镜像可以尽可能精简
- 避免将文件放入
/tmp
:对一些应用程序,会将某些缓存信息或心跳检测写如/tmp
、这对/tmp
读写性能要求较高,某些 Linux 发行版/tmp
通过tmpfs
文件系统存储在内存,但 Docker 容器默认为/tmp
使用标准的 Docker OverlayFS(docker run --rm -it nginx df
);可以使用/dev/shm
(shm 文件系统,共享内存和内存文件系统)
# multi-stage build
FROM python:3.6 as base
COPY requirements.txt /
RUN pip wheel --no-cache-dir --no-deps --whell-dir /wheels -r requirements.txt
FROM python:3.6-alpine
COPY --from=base /wheels /wheels
COPY --from=base requirements.txt .
RUN pip install --no-cache /wheels/*
WORKDIR /app
COPY . /app
Java Dockerfile Cheatsheet
https://merikan.com/2019/04/jvm-in-a-container/
- JVM 默认从
/proc
查找可用内存信息(Until Java8u131 and Java9 the JVM did not recognize memory or cpu limits set by the container. First implementation was a experimental feature and had its flaws but in Java10, memory limits are automatically recognized and enforced. This feature was then backported to Java8u191) - 因此 Java8u191 (Update 191) 以上,使用
-XX:+UseContainerSupport
(及MaxRAMPercentage
/MinRAMPercentage
注意不是MaxRAMFraction
) 而非-XX:+UseCGroupMemoryLimitForHeap
,即告诉 JVM 从/sys/fs/cgroup/memory/memory.limit_in_bytes
查找可用内存(从 cgroup 查找) -Xmx
指定堆内存大小,一般设定为 k8s memory limit 的 50% ~ 80%(除了堆内存、JVM 还有各种堆外数据结构的内存开销,如 java threads, metaspace, native memory, socket buffers, etc.)- 不同于其他容器类 VM 参数,
-Xmx
支持所有的 JVM 版本;是一个明确指定的大小、而不是估算大小(VM Settings: Estimeated) - 通过
java -XshowSettings:vm -XX:+PrintFlagsFinal -version
查看生效参数
# (Java8u131) JVM maxHeapSize=1.73G, which has no clue that it's running in container with 100MB avaiable mem
➜ docker run -m 100MB openjdk:8u131 java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 1.73G
Ergonomics Machine Class: server
Using VM: OpenJDK 64-Bit Server VM
# JVM can check the cgroup memory limit and calcuate a maxHeapSize (44M heapSize /100M maxRAM)
➜ docker run -m 100MB openjdk:8u131 java \
-XX:+UnlockExperimentalVMOptions \
-XX:+UseCGroupMemoryLimitForHeap \
-XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 44.50M
...
➜ docker run -m 100MB openjdk:8u131 java \
-XX:+UnlockExperimentalVMOptions \
-XX:+PrintFlagsFinal -version \
| grep -E "UnlockExperimentalVMOptions | UseCGroupMemoryLimitForHeap | MaxRAMFraction | InitialRAMPercentage | MaxRAMPercentage | MinRAMPercentage"
uintx MaxRAMFraction = 4 {product}
bool UnlockExperimentalVMOptions := true {experimental}
bool UseCGroupMemoryLimitForHeap = false {experimental}
其他
- Nginx:根据 CPU Limit 设定 worker processes 数(默认 auto 会根据宿主机 CPU 核数启动进程数)
Docker 网络
docker network create <network-name>
docker network ls
docker network inspect <network-name>
docker network connect <network-name> <container>
docker run --add-host=docker:10.0.0.1 --name app ...
# 在 /etc/hosts 文件中添加名为 docker,地址为 10.0.0.1 的宿主机记录;容器内 env 查看环境变量
镜像管理
docker images
docker history <image>
docker inspect <image>
# 安装 `dive` ( _brew info dive_ ) 查看镜像的层次结构
dive <image>
# 删除
docker rmi <image>
容器管理
查看容器
docker stats
docker ps —no-trunc
docker top <container>
启动容器
# 特权模式启动容器,对宿主机有 root 访问权限
docker run --priviledged ...
# 对于用完即抛的容器,--rm 自动删除
docker run --rm ...
连接容器,使用卷或网络接口而非通过 SSH
docker exec -it <container> /bin/sh
docker kill -s <signal> <container>
Docker Machine
Docker 在 Linux 操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制。Docker 利用 Linux 核心中的资源分离机制,例如 cgroups,以及 Linux 核心命名空间 ( namespace ),来建立独立的软件容器 ( containers ). Docker 以 C/S 模式执行,最初基于同一个 Binary 启动 client 及 daemon 的,新版本中已分离。
Client 和 Daemon 建立请求的方式:
tcp://host:port
unix://path/to/sock
fd://socket_fd
Docker Remote API
// TBD
容器编排
服务发现是分布式应用程序之间管理相互关系的一种机制。服务发现允许某个组件在想要与其他组件交互时,自动找到对方。由于这些应用本身是分布式的,服务发现机制也需要是分布式的。而且,服务发现作为分布式应用不同组件之间的“胶水”,其本身还需要足够动态、可靠,适应性强,而且可以快速且一致地共享关于这些服务的数据。
另外,Docker 主要关注分布式应用以及面向服务架构与微服务架构。这些关注点很适合与某个服务发现工具集成。每个 Docker 容器可以将其中运行的服务注册到服务发现工具里。注册的信息可以是 IP 地址或者端口,或两者都有,以便服务之间进行交互。
Consul 是一个使用 Raft 一致性算法的特殊数据存储器;Consul 暴露了键值存储系统和服务分类系统,并提供高可用性、高容错能力,并保证强一致性。Consul 还提供了根据 API 进行服务分类,代替了大部分传统服务发现工具的键值对存储。
Kubernetes
在容器环境中,编排通常涉及到三个方面:
- 资源编排: 负责资源的分配,如限制 Namespace 的可用资源,Scheduler 针对资源的不同调度策略
- 工作负载编排: 负责在资源之间共享工作负载,如 k8s 通过不同的 Controller 将 Pod 调度到合适的 Node 上,并且负责管理它们的生命周期
- 服务编排: 负责服务发现和高可用等,如 k8s 中可用通过 Service 来对内暴露服务,通过 Ingress 来对外暴露服务
在 k8s 中有 5 种我们经常会用到的控制器来帮助我们进行容器编排: Deployment, StatefulSet, DaemonSet, CronJob, Job. 其中 Deployment 经常被作为无状态实例控制器使用;StatefulSet 是一个有状态实例控制器;DaemonSet 可以指定在选定的 Node 上跑,每个 Node 上会跑一个副本,它有一个特点是它的 Pod 的调度不经过调度器,在 Pod 创建的时候就直接绑定 NodeName;最后一个是定时任务,它是一个上级控制器,和 Deployment 有些类似,当一个定时任务触发的时候,它会去创建一个 Job,具体的任务实际上是由 Job 来负责执行的。
Kubernetes 已经为我们对大量常用的基础资源进行了抽象和封装,我们可以非常灵活地组合、使用这些资源来解决问题,同时它还提供了一系列自动化运维的机制,如 HPA, VPA, Rollback, Rolling Update 等帮助我们进行弹性伸缩和滚动更新,而且上述所有的功能都可以用 YAML 声明式进行部署。
但是这些抽象还是在容器层面的,对于一个大型的应用而言,需要组合大量的 Kubernetes 原生资源,需要非常多的 Services, Deployments, StatefulSets 等,这里面用起来就会比较繁琐,而且其中服务之间的依赖关系需要用户自己解决,缺乏统一的依赖管理机制。
应用编排
什么是应用?一个对外提供服务的应用,首先它需要一个能够与外部通讯的网络,其次还需要能运行这个服务的载体 (Pods),如果这个应用需要存储数据,这还需要配套的存储,所以我们可以认为: 应用单元 = 网络 + 服务载体 + 存储
那么我们很容易地可以将 Kubernetes 的资源联系起来,然后将他们划分为 4 种类型的应用:
- 无状态应用 = Services + Volumes + Deployment
- 有状态应用 = Services + Volumes + StatefulSet
- 守护型应用 = Services + Volumes + DaemonSet
- 批处理应用 = Services + Volumes + CronJob/Job
8 Docker UseCases
- Simplifying Configuration
- Developer Productivity
- Server Consolidation
- Multi-tenancy
- Code Pipeline Management
- App Isolation
- Debugging Capabilities
- Rapid Deployment
Using Docker in production
Challenges
- Ops plane integrations (logging, metrics, monitoring)
- Optimized build and deploy time
- Security (vulnerabilities and capabilities that can be exploited)
- Handling secrets properly
- Persisting data
- Resource limits for collocated containers
Practice
Init an express app:
# Using express application generator:
npx express-generator
# Install dependencies and run application
npm i && npm start
Dockerfile v1:
FROM node:14
WORKDIR /usr/src/app
# using . instead of * to keep directory structure
COPY . ./
RUN npm install
EXPOSE 3000
CMD ["node", "bin/www"]
Test and run:
# add `node_modules/` to .dockerignore
docker build --tag node.local.v1 --file dockerfile-v1 .
docker run -p 3000:3000 node.local.v1
# dive node.local.v1
Dockerfile v1 Problems:
- Big container image
- Large build contexts resut in slow builds and bigger images
- Problems with build caching
- Cache busting instruction combinations cause slow builds
- Running as root
- Only the major version of node is pinned
- Lack of version pinning turns into reproducibility problems
- Not handling signals and not handling orphaned processes
Dockerfile v2
FROM node:14.16.1 as builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . ./
FROM gcr.io/distroless/nodejs:14
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app .
EXPOSE 3000
USER nonroot
CMD ["bin/www"]
Using docker-slim for resize image
Summary
Container:
- package & deploy services
- allow for process isolation
- immutability
- efficient resource utilization
- are lightweight in creation
Container orchestration:
- integrate and orchestrate these modular parts
- scale up & scale down
- fault tolerant
- provide communication across a cluster
k8s features:
- Horizontal infra scaling
- Auto-scaling: automatically change the number of running containers, based on CPU utilization or other app-provided metrics
- Replication controller: RC makes sure your cluster has an equal amount of Pods running. (terminates the extra pods / starts more pods if there are too few)
- Health checks and self-healing (auto-replacement)
- Traffic routing and load balancing
- Automated rollouts and rollbacks: handles rollouts for new version or updates without downtime while monitoring the containers’ health. In case the rollout doesn’t go well, it auto rolls back.
Adoption:
- Without k8s, large teams would have to manually script their own deployment workflows. Containers, combined with an orchestration tool, provide management of machines and services — improving the reliability of your application while reducing the amount of time and resources spent on DevOps.
- k8s has built-in features like self-healing and automated rollouts/rollbacks, effectively managing the containers for you.
- Use k8s only when your application uses a micro-service arch.