第一部分:文章简介 及 云原生思维概览 第二部分:Go从入门到精通语言 第三部分:Docker从入门到精通 第四部分:Kubernetes从入门到精通
文章目录
- 1、引言
-
- 1.1、环境介绍
- 1.传统的分层架构 vs 微服务
- 1.3.微服务改造
- 1.4.微服务间通信
-
- 点对点
- API网关
- 1.5、Docker技术是什么
- 1.6.为什么要用?Docker
- 1.7.对比虚拟机和容器运行态
- 1.8、性能对比
- 1.9、容器标准
- 1.10、Docker优势
- 2、docker安装、卸载、设置
-
- 2.1、安装docker
-
- 使用仓库安装
- 从包安装
- 安装脚本方便
- 使用Vagrant file方式在virtualbox安装ubuntu、docker、k8s
- 2.2、卸载docker
- 2.3、为K8S做准备
-
- 设置docker的cgroup driver为systemd
- 关闭系统交换区(swap area)
- 3、Dockerfile讲解
-
- 3.1、重申12 Factor - Docker管理和构建应用原则
- 3.理解构建上下文(Build Context)
- 3.3、Dockerfile文件示例
-
-
- docker build:把Dockerfile文件build成docker镜像
-
- 3.4.镜像构建日志分析
- 3.5、Build Cache
- 3.6、多段构建(Multi-stage build)
- 3.7、Dockerfile常用指令
-
- FROM:选择基本镜像
- LABELS:按标签组织项目
- RUN:运行命令
- CMD、ENTRYPOINT:默认执行的命令在容器启动时执行
- EXPOSE:发布端口
- ENV、ARG:设置变量构建参数和环境变量
-
- ENV
- ARG
- ADD、COPY:文件复制
- WORKDIR:切换工作目录
- VOLUME:将指定目录定义为插件存储卷
- USER:切换运行镜像的用户和用户组
- 不常用指令
- 3.8、Dockerfile最佳实践
- 3.9.多过程容器镜像
-
- tini
- 4、docker实现原理和技术
-
- 4.1.容器的主要特性
- 4.2、Linux-Namespace技术
-
- Linux内核代码中Namespace的实现
- Linux对Namespace操作方法
- Linux Namespace种类
- 理解多个Namespace关系和差异
- Namespace详解
- 关于namespace的常用操作
-
- 查看docker集装箱网络配置信息
- 4.3、Linux-Cgroups
-
- 简介
- Linux内核代码中Cgroups的实现
- 可配额/可测量 - Control Groups (cgroups)
-
- CPU子系统
- cpuacct子系统
- memory子系统
- CPU子系统练习
- Memory子系统练习
- Cgroup driver
- 4.4、Linux进程调度策略
-
- CFS调度器
- vruntime红黑树
- CFS进程调度
- 4.5、文件系统 Union FS(联合文件系统)
-
- 文件系统是如何工作的?
- 典型的Linux由文件系统组成
- Docker如何启动文件系统?
- 写操作
- 容器存储驱动器
-
- OverlayFS
- 4.6、Docker引擎架构
-
- shim
- 4.7、Docker网络
-
- Null模式
-
- 手动模拟Docker为容器建立bridge网络
- 默认模式 - 网桥和NAT
-
- 用命令深入理解
- 4.8.解决跨主机网络问题 - Underlay模式、Overlay模式
-
- 方法一:Underlay模式
- 方法二:Docker Libnetwork Overlay模式
-
- Flannel工具
- Docker命令手册
-
- Docker程序相关
- image相关
-
- 从registry拉取image
- 检查现有的镜像image
- 删除镜像image
- 显示image更详细的信息
- 将镜像保存到本地
- 载入本地镜像
- build dockerfile
- 镜像重命名
- 根据已经存在的一个image创造新的image并使用新的tag
- 显示镜像分层
- 把镜像推进仓库
- container相关
- 查看容器
- 创建容器
- 进入容器内部
- 启动容器
- 停止容器
- 删除容器
- 查看容器ip地址
- 查看容器产生的log
- 查看容器内运行的进程
- 通过容器创建新的image
- 更新容器启动参数
- 获取容器/镜像的元数据
- 拷贝文件至容器内
1、引言
1.1、环境介绍
建议所有人使用Ubuntu虚拟机,防止后面因为一些网络配置或其他操作失误,导致你的电脑出现一些未知问题!!!
- MacOS 10.14.6
- VirtualBox 6.1.34
- ubuntu-20.04.4-live-server-amd64.iso
1.2、传统分层架构 vs 微服务
1.3、微服务改造
分离微服务的方法建议:
- 审视并发现可以分离的业务逻辑
- 寻找天生隔离的代码模块,可以借助于静态代码分析工具
- 不同并发规模,不同内存需求的模块都可以分离出不同的微服务,此方法可提高资源利用率,节省成本
一些常用的可微服务化的组件:
- 用户和账户管理
- 授权和会话管理
- 系统配置
- 通知和通讯服务
- 照片、多媒体、元数据等
1.4、微服务间通讯
点对点
- 多用于系统内部多组件之间通讯
- 有大量的重复模块如认证授权
- 缺少统一规范,如监控、审计等功能
- 后期维护成本高,服务和服务的依赖关系错综复杂难以管理
API网关
- 基于一个轻量级的message gateway
- 新API通过注册至Gateway实现
- 整合实现Common function
1.5、Docker技术是什么
- 基于Linux内核的Cgroup、Namespace 以及Union FS等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术,由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。
- 最初实现是基于LXC,从0.7以后开始去除LXC,转而使用自行开发的Libcontainer,从1.11开始,则进一步演进为使用runC和Containerd。
- Docker在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护,使得Docker技术比虚拟机技术更为轻便、快捷。
1.6、为什么要用Docker
-
更高效地利用系统资源
-
更快速的启动时间
-
一致的运行环境
-
持续交付和部署
-
更轻松地迁移
-
更轻松地维护和扩展
-
…
1.7、虚拟机和容器运行态的对比
Docker Engine就是一个docker进程(docker daemon),当我们要跑一个容器的时候,就是docker daemon fork一个新进程,仅此而已。
1.8、性能对比
1.9、容器标准
Docker是容器技术的一种实现方案(一种产品、一家公司),但是因为容器技术是docker带火的,所以在早期我们谈容器都是指docker,由于当时docker如日中天有自己的标准,但是后来因为历史原因docker最终还是实现了OCI标准。
- Open Container Initiative(OCI)
- 轻量级开放式管理组织(项目)
- OCI主要定义两个规范
- Runtime Specification:运行时标准
- 文件系统包如何解压至硬盘,供运行时运行。
- Image Specification:镜像标准
- 如何通过构建系统打包,生成镜像清单(Manifest)、文件系统系列化文件、镜像配置。
- Runtime Specification:运行时标准
1.10、Docker优势
- 封装性:
- 不需要再启动内核,所以应用缩扩容时可以秒速启动。
- 资源利用率高,直接使用宿主机内核调度资源,性能损失小。
- 方便的CPU、内存资源调整。
- 能实现秒级快速回滚。
- 一键启动所有依赖服务,测试不用为搭建环境犯愁,PE也不用为建站复杂担心。
- 镜像一次编译,随处可用。
- 测试、生产环境高度一致(数据除外)。
- 镜像增量分发:
- 由于采用Union FS,简单来说就是支持将不同的目录挂载到同一个虚拟文件系统下,并实现一种layer的概念,每次发布只传输变化的部分,节约带宽。
- 隔离性:
- 应用的运行环境和宿主机环境无关,完全由镜像控制,一台物理机上部署多种环境的镜像测试。
- 多个应用版本可以并存在机器上。
- 社区活跃:
- Docker命令简单、易用,社区十分活跃,且周边组件丰富。
2、docker的安装、卸载、设置
2.1、安装docker
官方安装手册
有三种安装方式:
- 使用存储库安装
- 推荐的方法
- 下载DEB 包并手动安装
- 适合在无法访问Internet的系统上安装 Docker
- 自动化便利脚本来安装
- 适合测试和开发环境
使用存储库安装
# 更新apt包索引并安装包以允许apt通过 HTTPS 使用存储库
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
# 添加 Docker 的官方 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 使用以下命令设置存储库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 更新apt包索引,安装最新版本的 Docker Engine、containerd 和 Docker Compose
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 或者列出可用版本,并指定版本安装
apt-cache madison docker-ce
sudo apt-get install docker-ce=<VERSION_STRING> docker-ce-cli=<VERSION_STRING> containerd.io docker-compose-plugin
从包安装
# 转到https://download.docker.com/linux/ubuntu/dists/,选择您的 Ubuntu 版本,然后浏览到pool/stable/、选择amd64、 armhf、arm64或s390x,然后下载.deb您要安装的 Docker 引擎版本的文件。
# 安装 Docker Engine,将下面的路径更改为您下载 Docker 包的路径。
sudo dpkg -i /path/to/package.deb
使用便捷脚本安装
- 使用安装脚本安装
# 从get.docker.com下载安装脚本
curl -fsSL get.docker.com -o get-docker.sh
- -o:在当前目录下生成一个get-docker.sh文件
# 执行安装脚本(脚本里面需要一些sudo的权限)
sh get-docker.sh
- 查看是否安装成功
docker version
- 启动docker server
systemctl start docker
# 启动docker server后即会看到Server也出现了
docker version
使用Vagrant file方式在virtualbox安装ubuntu、docker、k8s
# -*- mode: ruby -*-
# vi: set ft=ruby :
# 使用方法
# 1. 安装 virtualbox
# 2. 安装 vagrant
# 3. 添加镜像(可选,适用于网络不好的状况)
# 4. vagrant up
# 5. vagrant ssh 即可(默认用户名 vagrant,有 sudo 执行权限)
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.box_check_update = false
config.vm.network "private_network", ip: "192.168.34.2", name: "vboxnet0"
config.vm.provider "virtualbox" do |vb|
# 配置虚拟机为 4 个核心,6GB 内存
vb.cpus = 4
vb.memory = "6144"
end
config.vm.provision "shell", inline: <<-SHELL sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list apt-get update apt-get -y install \ apt-transport-https \ ca-certificates \ curl \ gnupg-agent \ software-properties-common curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | apt-key add - add-apt-repository \ "deb [arch=amd64] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu \ $(lsb_release -cs) \ stable" echo "deb https://mirrors.ustc.edu.cn/kubernetes/apt kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list curl -s https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io apt-get install -y kubelet=1.19.15-00 kubeadm=1.19.15-00 kubectl=1.19.15-00 apt-mark hold kubelet kubeadm kubectl adduser vagrant docker SHELL
end
2.2、卸载docker
# 卸载 Docker Engine、CLI、Containerd 和 Docker Compose 软件包 sudo apt-get remove docker docker-engine docker.io containerd runc # 主机上的映像、容器、卷或
自定义配置文件不会自动删除。要删除所有映像、容器和卷: sudo rm -rf /var/lib/docker sudo rm -rf /var/lib/containerd
2.3、为K8S做准备
设置docker的cgroup driver为systemd
- docker默认用cgroupfs作为cgroup驱动。
存在问题:
- 在systemd作为init system的系统中,默认并存着两套groupdriver
- 这会使得系统中docker和kubelet管理的 进程被cgroupfs驱动管,而systemd拉起的服务由systemd驱动管,让cgroup管理混乱且容易在资源紧张时引发问题。 因此kubelet会默认
--cgroup-driver=systemd
,若运行时cgroup不一致时,kubelet会报错
# 默认是没有的,需要我们手动创建
vi /etc/docker/daemon.json
{
"exec-opts": ["native.cgroupdriver=systemd"]
}
systemctl daemon-reload
systemctl restart docker
# 查看是否成功
docker info | grep Cgroup
关闭系统交换区(swap area)
K8S现在版本要求是要关闭。
swapoff -a
vi /etc/fstab
# remove the line with "swap" keyword
3、Dockerfile讲解
Dockerfile官方语法说明文档
- Dockerfile是用于构建docker镜像的文本文件
- Dockerfile里包含了构建镜像所需的“指令”
- Dockerfile有其特定的语法规则
3.1、重申12 Factor - Docker管理和构建应用的原则
- 运行环境中,应用程序通常是以一个或多个进程的。
- 12-Factor应用的进程必须无状态(Stateless)且无共享(Share nothing)
- 任何需要持久化的数据都要存储在后端服务内,比如数据库
- 应用构建阶段将源代码编译成待执行应用
- Session Sticky是12-Factor极力反对的
- Session中的数据应该保存在诸如Memcached或Redis这样的带有过期时间的缓存中
3.2、理解构建上下文(Build Context)
- 当运行
docker build
命令时,当前工作目录被称为构建上下文。 docker build
默认查找当前目录的Dockerfile作为构建输入,也可以通过-f
指定Dockerfile。docker build -f ./Dockerfile
- 当
docker build
运行时,首先会把构建上下文传输给,把没用的文件包含在构建上下文时,会导致传输时间长,构建需要的资源多,构建出的镜像大等问题。- 试着到一个包含文件很多的目录运行docker build命令,会感受到差异。
- 可以通过 文件从编译上下文排除某些文件。
- 因此需要确保构建上下文清晰,比如创建一个专门的目录放置Dockerfile,并在目录中运行
docker build
。
举例说明:
$ cd /tmp
$ docker build .
# 可以看到执行完命令后第一句日志提示就是把build context发送给Docker daemon
Sending build context to Docker daemon 9.728kB
$ cd /
$ docker build .
# 这里会卡很久,因为根目录文件太多了
所以我们要保证执行docker build命令的目录要尽可能干净,排除一些不必要的文件。
3.3、Dockerfile文件示例
Dockerfile:
# 从docker hub去拉Ubuntu镜像
From ubuntu
# 设置环境变量
ENV MY_SERVICE_PORT=80
ENV MY_SERVICE_PORT1=80
ENV MY_SERVICE_PORT2=80
ENV MY_SERVICE_PORT3=80
# 打一些LABEL,可以通过docker ps去查打某个LABEL的镜像
LABEL multi.label1="value1" multi.label2="value2" other="value3"
# 添加主机文件到镜像中
ADD bin/amd64/httpserver /httpserver
# 容器要打开的端口
EXPOSE 80
# 容器在运行的时候要跑哪条命令
ENTRYPOINT /httpserver
docker build:把Dockerfile文件build成docker镜像
# build当前目录的Dockerfile
$ docker build .
# -f:指定要使用的Dockerfile路径
$ docker build -f ./Dockerfile_terraformexec .
# -t:就是tag,代表镜像的名字,名字后可以使用:来打一个版本号
$ docker image build -t hello:1.0 .
3.4、镜像构建日志解析
# 这里build的就是上面3.3章节的Dockerfile文件
$ docker build .
Sending build context to Docker daemon 14.57MB
Step 1/9 : FROM ubuntu
---> 27941809078c
Step 2/9 : ENV MY_SERVICE_PORT=80
---> Using cache
---> 054288d7d037
Step 3/9 : ENV MY_SERVICE_PORT1=80
---> Using cache
---> 8bce1c2fb020
Step 4/9 : ENV MY_SERVICE_PORT2=80
---> Using cache
---> 9e8e9021cf78
Step 5/9 : ENV MY_SERVICE_PORT3=80
---> Using cache
---> e8a4a34fe650
Step 6/9 : LABEL multi.label1="value1" multi.label2="value2" other="value3"
---> Using cache
---> fedeb7ed2081
Step 7/9 : ADD bin/amd64/httpserver /httpserver
---> 31743efc0c55
Step 8/9 : EXPOSE 80
---> Running in df89116684e2
Removing intermediate container df89116684e2
---> 5365008d10cc
Step 9/9 : ENTRYPOINT /httpserver
---> Running in 44aa0fe65377
Removing intermediate container 44aa0fe65377
---> 0090532f9e62
Successfully built 0090532f9e62
- 把 构建上下文 传输给docker daemon,一共14.57MB大小。
- 每一个Step对应Dockerfile中的一条指令
- 每一条指令都是对应一个镜像层,并为每个层去计算一个checksum([后文中有详解](#4.5、文件系统 Union FS(联合文件系统)))即“校验和”,如果校验和一致的时候,就认为这个层和刚才的层是一致的,就会用原来的缓存,即“Using cache”。
3.5、Build Cache
构建容器镜像时,Docker依次读取Dockerfile中的指令,并按顺序依次执行构建指令。
Docker读取指令后,会先判断缓存中是否有可用的已知镜像,只有已存镜像不存在时才会重新构建。
- 通常Docker简单判断Dockerfile中的指令与镜像。
- 针对ADD和COPY指令,Docker判断该镜像层每一个文件的内容并生成一个checksum,与现存镜像比较时,Docker比较的是二者的checksum。
- 其他指令,比如RUN、apt-get -y update ,Docker不会去checksum,只是简单比较与现存镜像中的指令字串是否一致。
- 当某一层cache失效以后,上面所有层级的cache均一并失效,后续指令都重新构建镜像。
所以我们应该把变动不频繁的层放在下面(即在写Dockerfile时应该把那些趋于稳定的命令写在前面,变动比较多的应该放在后面),这样的话可以最大利用缓存。
3.6、多段构建(Multi-stage build)
较晚版本的Docker才支持。
什么是多段构建?有时候我们构建一个应用,这个应用需要很多的依赖包,正常情况来讲,如果我们不用多段构建方式的话,首先要先有一个Base Image,然后拉依赖包,然后去构建,构建完了可能我们只需要最后的一个文件,那么中间这些下载的依赖包、配置,我有可能就留在那了,那么最后build出来的镜像就会很大,而且会有安全隐患,要么我就得去清理,可是有些时候我都不知道这些依赖包装在哪了,可能有好几个地方。那么这样的话,就会导致中间出现了一些不必要的依赖包,我们有什么方式去解决这个问题吗?Docker提供了多段构建的概念。
多段构建的概念:在同一Dockerfile里面,指定FROM多个部分,目的就是为了达到一个干净产线的docker image,可以把一些临时过渡的文件都放在多段构建早期的镜像里面。
- 有效减少镜像层级的方式
# AS:给镜像起个别名,别名叫tmpbuild
FROM golang:1.16-alpine AS tmpbuild
# 装git插件
RUN apk add --no-cache git
# 通过go get命令把代码下载下来
RUN go get github.com/golang/dep/cmd/dep
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# dep是Go语言依赖管理的软件
RUN dep ensure -vendor-only
#把源代码COPY到镜像里
COPY . /go/src/project/
# go build -o 把我们的源代码构建成二进制文件,我们最后就需要这个在/bin目录下的名为project的二进制文件,其他的都是waste
RUN go build -o /bin/project
# ========上面这些都是为了build出我们要想的二进制文件========
# 构建新的镜像,FROM scratch不指定任何镜像为基础
FROM scratch
# 从别名为tmpbuild的镜像中COPY /bin/project二进制文件到当前新镜像中
COPY --from=tmpbuild /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
- 所以,所谓的多段构建就是通过上面的方式先把我们想要的二进制文件给build出来,接下来我们去构建新的镜像,从新FROM,然后把之前需要的二进制文件COPY到新镜像中,这样我们新镜像是干干净净的,只有这个二进制可执行文件,所有原来的那些依赖都是不存在的。
- 多段构建的目的就是为了达到一个干净产线的docker image,把一些临时过渡的文件都放在多段构建早期的镜像里面。
3.7、Dockerfile常用指令
FROM:选择基础镜像
推荐alpine
-
FROM [--platform=<platform>] <image>[@<digest>][AS <name>] FROM golang:1.16.5-alpine3.13 AS tmpbuild
- FROM scratch:不指定任何镜像为基础,是一个空的Docker镜像
- 官方镜像优于非官方的镜像,如果没有官方镜像,则尽量选择Dockerfile开源的
- 固定版本tag而不是每次都使用latest
- 尽量选择体积小的镜像
LABELS:按标签组织项目
- 方便管理
LABEL multi.label1="value1" multi.label2="value2" other="value3"
- 配合label filter可过滤镜像查询结果
docker images -f label=multi.label1="value1"
RUN:运行命令
最常用的,比如使用命令安装软件,下载文件等。
RUN apt-get install -y wget
RUN wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
RUN tar zxf ipinfo_2.0.1_linux_amd64.tar.gz && \
mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
- 最常见的用法是
RUN apt-get update && apt-get install
,这两条命令应该永远用&&连接,如果分开执行,RUN apt-get update构建层被缓存,可能会导致新package无法安装。 - 每一行的RUN命令都会产生一层image layer,会导致镜像的臃肿。
- 建议用&&连接多个命令,这样docker就会把多条命令变成一条命令来执行,有效减少了镜像的层级,镜像层级越少,OverlayFS里面的层数越少([后文中有详解](#4.5、文件系统 Union FS(联合文件系统))),效率上会越高的。
CMD、ENTRYPOINT:容器启动时默认执行的命令
CMD和ENTRYPOINT同时支持shell格式和Exec格式。更多的是使用Exec格式。
Shell格式:
ENTRYPOINT echo "hello docker" CMD echo "hello docker"
Exec格式:
ENTRYPOINT ["echo", "hello docker"] CMD ["echo", "hello docker"]
-
CMD
:容器启动时默认执行的命令。 -
容器镜像中应用的运行命令,需要带参数。
-
如果docker run启动容器时指定了其它命令,则CMD命令会被忽略。
FROM ubuntu CMD ["echo", "hello docker"]
$ docker run -it --rm 9ea0677b6d32 hello docker $ docker run -it --rm 9ea0677b6d32 echo "hello world" hello world
- 如果定义了多个CMD,只有最后一个会被执行。
- 需要注意的是CMD不是必写的,因为有些基础镜像自己有CMD,例如在ubuntu:21.04就会有CMD [“/bin/bash”],但是如果我们自己写了CMD就会覆盖基础镜像自带的,因为我们定义的会比基础镜像定义的CMD靠后。即使我们写CMD []也会把ubuntu基础镜像里面的CMD给覆盖掉。
-
ENTRYPOINT
:容器启动时默认执行的命令。- ENTRYPOINT的最佳实践是
FROM ubuntu ENTRYPOINT ["echo"] CMD ["hello", "docker","jenrey"]
$ docker run -it --rm 2e6 hello docker jenrey
- ENTRYPOINT是使得容器镜像和虚拟机镜像有一个本质区别的点,容器镜像是面向容器内应用的,容器镜像在启动的时候不仅仅起了一个虚的OS运行环境壳子,最重要的是用ENTRYPOINT指定要起哪个应用的,所有的docker image其实都是为了某一个固定应用而构建的,容器的运维是面向应用的运维而不是面向操作系统的运维,ENTRYPOINT就使得这一切有了意义。
- 与CMD略有不同,ENTRYPOINT所设置的命令是 的。
-
FROM ubuntu:21.04 ENTRYPOINT ["echo", "hello docker"]
$ docker run -it --rm demo-entrypoint hello docker $ docker run -it --rm demo-entrypoint echo "hello world" # 这里注意echo也打印出来了。这是因为我们把创建容器时指定命令的echo "hello world"作为参数传给了dockerfile的ENTRYPOINT的echo。这是因为ENTRYPOINT所定义的echo一定会被执行的。 hello docker echo hello world $
-
FROM ubuntu:21.04 ENTRYPOINT ["echo"] CMD []
# 我们什么都不加直接创建容器,不会打印出任何内容,这是因为ENTRYPOINT的echo参数为空。 $ docker container run --rm -it demo-both # 如果我们加一些内容,就会把test通过CMD作为参数传给了ENTRYPOINT的echo $ docker container run --rm -it demo-both test test $
EXPOSE:发布端口
EXPOSE <prot> [<port>/<protocol>...]
EXPOSE 8080
- 是镜像创建者和使用者的 约定,所以不定义也没事,EXPOSE其实只是一个声明,因为从容器提供的应用服务本身是看不到开了哪个端口的,声明一下别人就知道你的镜像是开了哪个端口。
- 并不会直接将端口自动和宿主机某个端口建立映射关系
- 容器的端口映射,还是要使用docker run -p去指定容器端口映射到主机哪个端口上的,一般来说我们不会直接用docker,都使用K8S,所以也不是很常用
- 在docker run -P时,docker会自动映射所有EXPOSE定义的端口到主机随机大端口(比较大的端口号),如0.0.0.0:32768->80/tcp
- 通过docker inspect 命令,查看“Ports”字段可以看到容器端口的信息
ENV、ARG:设置变量构建参数和设置环境变量
- 区别:
ENV
ENV <key>=<value>...
ENV MY_SERVICE_PORT=80
- ENV设置的变量可以Dockerfile中保持,也在Image中保持,也会永久的保存到容器的系统环境变量里面。
ARG
-
ARG设置的变量只在Dockerfile中保持。
-
ARG 可以在镜像build的时候动态修改value, 通过
--build-arg
$ docker build -f .\Dockerfile-arg -t ipinfo-arg-2.0.0 --build-arg VERSION=2.0.0 .
ADD、COPY:文件复制
-
ADD:从源地址(文件、目录或者URL)复制文件到目标路径
ADD [--chown=<user>:<group>]<src>...<dest>
ADD [--chown=<user>:<group>]"<src>",..."<dest>"
路径中有空格时使用- ADD支持Go语言风格的通配符,如ADD check* /testdir/
- src如果是文件,则必须包含在编译上下文中(Build context),ADD指令无法添加编译上下文之外的文件
- src如果是URL,如果dest结尾没有/,那么dest是目标文件名;如果dest结尾有/,那么dest是目标目录名
- 如果src是一个目录,则所有文件都会被复制至dest
- 如果dest不存在,则ADD指令会创建目标目录
- 尽量减少通过ADD URL添加remote文件,建议使用curl或wget && untar
-
COPY:从源地址(文件、目录)复制文件到目标路径
-
COPY [--chown=<user>:<group>]<src>...<dest>
-
COPY [--chown=<user>:<group>]"<src>",..."<dest>"
路径中有空格时使用 -
COPY requirements.txt . COPY Gopkg.lock Gopkg.toml /go/src/project/
-
只支持本地文件的复制,不支持URL
-
如果目标目录不存在,则和ADD一样,会自动创建。
-
COPY只会单纯的去复制,不会自动解压。
-
建议所有的本地文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。
-
COPY可以用于多阶段编译场景,可以从前一个临时镜像中拷贝文件
COPY --from=tmpbuild /bin/project /bin/project
-
WORKDIR:切换工作目录
- 等价于cd命令,切换工作目录
VOLUME:将指定目录定义为外挂存储卷
-
Dockerfile中在该指令之后所有对同一目录的修改都无效
-
FROM ubuntu # 将容器内的/test目录(不存在会自动创建)定义为外挂存储卷 VOLUME ["/test"]
使用
docker inspect 容器id
查看“Mounts”-“Source”和“Destination”即可以看到- Source:为该容器的外挂存储卷在宿主机上的存储地址
- Linux为:
/var/lib/docker/<卷名>/_data
- Linux为:
- Destination:为容器内的映射到外部存储卷的目录,即Dockerfile的VOLUME指令所指定的目录
- Source:为该容器的外挂存储卷在宿主机上的存储地址
-
等价于使用
docker run -it -d -v="/test"
-
不管是用Dockerfile VOLUME指令还是docker run -v,均不会覆盖,即同时使用会挂载两个外部存储卷。
USER:切换运行镜像的用户和用户组
-
因安全性要求,越来越多的场景要求容器应用要以non-root身份运行
-
USER <user>[:<group>]
-
一般如果不指定USER的话,在容器里面就是root用户
-
# 用户和用户组必须提前已经存在 USER user1
不常用指令
- ONBUILD指令可以为镜像添加触发器。其参数是任意一个Dockerfile 指令。
当我们在一个Dockerfile文件中加上ONBUILD指令,该指令对利用该Dockerfile构建镜像(比如为A镜像)不会产生实质性影响。
但是当我们编写一个新的Dockerfile文件来基于A镜像构建一个镜像(比如为B镜像)时,这时构造A镜像的Dockerfile文件中的ONBUILD指令就生效了,在构建B镜像的过程中,首先会执行ONBUILD指令指定的指令,然后才会执行其它指令。
需要注意的是,如果是再利用B镜像构造新的镜像时,那个ONBUILD指令就无效了,也就是说只能再构建子镜像中执行,对孙子镜像构建无效。其实想想是合理的,因为在构建子镜像中已经执行了,如果孙子镜像构建还要执行,相当于重复执行,这就有问题了。
利用ONBUILD指令,实际上就是相当于创建一个模板镜像,后续可以根据该模板镜像创建特定的子镜像,需要在子镜像构建过程中执行的一些通用操作就可以在模板镜像对应的dockerfile文件中用ONBUILD指令指定。 从而减少dockerfile文件的重复内容编写。
- STOPSIGNAL
- HEALTHCHECK
- SHELL
3.8、Dockerfile最佳实践
目标:易管理、少漏洞、镜像小、层级少、利用缓存
- 不要安装无效软件包。
- 应简化镜像中同时运行的进程数,理想状况下,每个镜像应该只有一个进程。
- 当无法避免同一镜像运行多进程时,应选择合理的初始化进程(init process)。
- 最小化层级数:
- 最新的docker只有RUN、COPY、ADD创建新层,其他指令创建临时层,不会增加镜像大小,比如EXPOSE指令就不会生成新层。
- 多条RUN命令可通过&&连接符连接成一条指令集以减少层数。
- 通过多段构建减少镜像层数。
- 把多行参数按字母排序,可以减少可能出现的重复参数,并且提高可读性。
- 编写Dockerfile的时候,应该把变更频率低的编译指令优先构建以便放在镜像底层以有效利用build cache。
- 复制文件时,每个文件应独立复制,这确保某个文件变更时,只影响改文件对应的缓存。
3.9、多进程的容器镜像
对于任何的容器,它的PID=1的进程就是它的entrypoint进程,这个容器里的所有其他进程都是entrypoint进程fork出来的,如果entrypoint进程没有去管理子进程的能力,那么很可能就出现各种的问题。
- 选择适当的init进程
- 需要捕获SIGTERM 信号并完成子进程的优雅终止
- 负责清理退出的子进程以避免僵尸进程
tini
开源项目:https://github.com/krallin/tini
如果我们一个容器里面需要多个进程并行的时候,可以用tini作为初始化进程。
4、docker的原理及技术实现
4.1、容器主要特性
- 安全性
- 隔离性
- Namespace
- 便携性
- Union FS
- 可配额
- Cgroup
4.2、Linux-Namespace技术
这里讲的Namespace是Linux里面的进程隔离技术,主要目的是两个进程互相不干扰,不要和K8S里面的Namespace混淆。
K8S里面的Namespace是一个对象,可以理解为一个个的文件目录,然后把k8s内嵌对象组织起来,然后可以通过权限控制说你可以访问这个目录,他不可以访问这个目录。
并不是新的方案,在docker出现之前就有Namespace方案。
- Linux Namespace 是一种 Linux Kernel 提供的资源隔离方案;
- 系统可以为进程分配不同的Namespace;
- 并保证不同的Namespace资源独立分配、进程彼此隔离,即不同的Namespace下的进程互不干扰;
Linux内核代码中Namespace的实现
Namespace名空间一般就是用来做隔离的,在Linux里面,任何一个进程在运行的时候都是需要放在Namespace下的。Namespace是进程的一个属性。
在Linux Kernel里面无论是线程还是进程,从Linux Kernel来看都是task,在Kernel里面用来描述task的数据结构就叫task_struct
,里面有一个属性叫nsproxy,即任何的task都有自己的属性nsproxy。
-
进程数据结构
struct task_struct{ ... /* Namespace */ struct nsproxy *nsproxy; ... }
-
Namespace数据结构
struct nsproxy{ atomic_t count; struct uts_namespace *uts_ns; struct ipc_namespace *ipc_ns; struct mnt_namespace *mnt_ns; struct pid_namespace *pid_ns_for_children; struct net *net_ns; }
Linux对Namespace操作方法
一个进程是如何分namespace的?
系统的第一个进程是init PID=1(较新的Linux系统上都使用了systemd取代了init成功系统的第一个进程即PID=1,字母d是守护进程daemon的缩写;systemctl
是 systemd 的主命令,用于管理系统),其本身会分一个默认的namespace,当要起其它进程的时候,比如clone()的时候是可以指定新的namespace、通过setns()可以把进程加到某个已经存在namespace中、通过unshare()把一个进程移动到新的namespace里。
-
clone()
:在创建新进程的系统调用时,可以通过flags参数指定需要新建的Namespace类型。/* CLONE_NEWCGROUP / CLONE_NEWIPC / CLONE_NEWNET / CLONE_NEWNS / CLONE_NEWPID / CLONE_NEWUSER / CLONE_NEWUTS */ int clone(int (*fn)(void *), void *child_stack, int flags, void *arg)
-
setns()
:该系统调用 可以让调用进程加入某个已经存在的Namespace中int setns(int fd, int nstype)
-
unshare()
:该系统调用 可以将调用进程移动到新的Namespace下int unshare(int flags)
Linux Namespace种类
Namespace有多种。
理解多个Namespace的关系和区别
主机即系统本身有一个namespace,一般来讲用户的进程都是run在主机namespace上的,当主机fork一个新的进程,可以让新进程去新的namespace。
我们可以创建很多namespace,把不同进程塞到不同的namespace里。
多个Namespace彼此之间是隔离的,所以不同的Namespace里看到的pid是不一样的。
net namespace:在不同的namespace里,网络的配置是不一样的,是完全独立的,可以有自己独立的网卡,独立的ip。
但是pid namespace:相当于是继承的,Namespace A的pid在主机上是能看得到的,但是是有一个映射关系,但是里面的pid号不一样。
Namespace详解
- Pid namespace
- 不同用户的进程就是通过 Pid namespace 隔离开的,且不同 namespace中可以有相同Pid。
- 有了Pid namespcae,每个 namespace 中的Pid能够互相隔离。
- 应用场景:容器技术是没有虚拟化层的,即没有从操作系统做隔离,任何的进程其实都是主机上开辟的进程,如果不做隔离,可能一个机器上开几百几千个进程,ps命令一执行就弹出几千个进程,不方便管理。有了Pid namespace,这一组进程放在A namespace,这一组进程放在B namespace,我们只需要进到namespace里,就很容易管理当前这一组进程了。而且不同的namespace彼此看不到,当我们在另外一个namespace中想杀其他namespace的进程是杀不到的,因为看不到pid。
- net namespcae
- 网络隔离是通过 net namespace 实现的,每个net namespace 有独立的 network device、IP address、IP routing tables、/proc/net 目录。
- Docker默认采用的方式将container中的虚拟网卡同host上的一个docker bridge:docker0连接在一起;这里后面有详细讲解。
- 应用场景:每个net namespace里面可以有独立的网络设备、独立的ip路由表等,回到微服务架构,服务A去调服务B时,B是要有一个独立的IP网络地址、独立的端口,那就意味着我这一个tomact进程放到一个独立的net namespace下,就可以分配一个独立的IP、端口,那么只需要我这个IP能和外面世界连通,相当于就是虚拟了一台主机出来,微服务架构就实现了,实现微服务架构就是为了让服务跑在某一个IP加端口上。
- ipc namespace
- Container中进程交互还是采用Linux常见的进程间交互方法(interprocess communication - IPC),包括常见的