Docker是开发人员和系统管理员使用容器开发、部署和运行应用程序的平台,使用Linux容器来部署应用程序称为集装箱化,使用Docker可以轻松部署应用程序。Docker 和传统部署方式最大的不同在于,它将不会限制我们使用任何工具,任何语言,任何版本的 runtime,Docker 将我们的应用看成一个只提供网络服务的盒子(也即容器),Kubernetes 则是对这些盒子进行更多自动化的操作,自动创建,自动重启,自动扩容,自动调度,这个过程称之为容器编排。
现在,容器编排技术给 Web 应用带来了巨大的灵活性,让我们轻松创建需要的程序对外提供服务。和传统的 IaaS 相比,不需要去关心云主机申请,云主机配置等信息,也不需考虑云主机故障导致的服务不可用,由 Kubernetes 的副本控制器帮我们完成云主机故障发生后容器迁移。下面就让我们来看一下Docker和Kubernetes在前端应用开发的一些应用。
Docker安装
和前端工具一样,使用Docker容器之前,需要先安装对应的工具。
Linux Debian/Ubuntu, 安装 社区版DockerCE
Windows 一键安装
macOS 一键安装
需要说明的是,Windows7 系统,需要使用 VirtualBox 安装 Linux 作为 Docker 的宿主机,Windows10 Pro 会使用 Hyper-V 安装 Linux 作为 Docker 的宿主机。
我们可以使用阿里云的镜像进行下载,然后再进行安装。
http://mirrors.aliyun.com/docker-toolbox/mac/docker-for-mac/
安装完成后,启动Docker即可。
配置镜像加速
在国内访问默认的官方镜像比较慢,我们可以使用阿里云的镜像加速,注册阿里账号并申请容器服务之后,然后点击容器镜像服务的镜像加速地址查看地址,如下图所示。
注册Docker ID
w为了方便使用和管理镜像,我们可以点击Docker官方地址来注册Docker ID。注册完成之后,就可以在Mac版Docker桌面工具中进行登录,并查看自己已有的镜像,如下图所示。
基本使用
Docker命令
打开终端,然后输入命令docker
即可查看Docker支持的命令,如下所示。
Usage: docker [OPTIONS] COMMAND
A self-sufficient runtime for containers
Options:
--config string Location of client config files (default "/Users/crane/.docker")
-c, --context string Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")
-D, --debug Enable debug mode
-H, --host list Daemon socket(s) to connect to
-l, --log-level string Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
--tls Use TLS; implied by --tlsverify
--tlscacert string Trust certs signed only by this CA (default "/Users/crane/.docker/ca.pem")
--tlscert string Path to TLS certificate file (default "/Users/crane/.docker/cert.pem")
--tlskey string Path to TLS key file (default "/Users/crane/.docker/key.pem")
--tlsverify Use TLS and verify the remote
-v, --version Print version information and quit
Management Commands:
app* Docker Application (Docker Inc., v0.8.0)
builder Manage builds
buildx* Build with BuildKit (Docker Inc., v0.3.1-tp-docker)
checkpoint Manage checkpoints
config Manage Docker configs
container Manage containers
context Manage contexts
image Manage images
manifest Manage Docker image manifests and manifest lists
network Manage networks
node Manage Swarm nodes
plugin Manage plugins
secret Manage Docker secrets
service Manage services
stack Manage Docker stacks
swarm Manage Swarm
system Manage Docker
trust Manage trust on Docker images
volume Manage volumes
Commands:
attach Attach local standard input, output, and error streams to a running container
build Build an image from a Dockerfile
commit Create a new image from a container's changes
cp Copy files/folders between a container and the local filesystem
create Create a new container
deploy Deploy a new stack or update an existing stack
diff Inspect changes to files or directories on a container's filesystem
events Get real time events from the server
exec Run a command in a running container
export Export a container's filesystem as a tar archive
history Show the history of an image
images List images
import Import the contents from a tarball to create a filesystem image
info Display system-wide information
inspect Return low-level information on Docker objects
kill Kill one or more running containers
load Load an image from a tar archive or STDIN
login Log in to a Docker registry
logout Log out from a Docker registry
logs Fetch the logs of a container
pause Pause all processes within one or more containers
port List port mappings or a specific mapping for the container
ps List containers
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rename Rename a container
restart Restart one or more containers
rm Remove one or more containers
rmi Remove one or more images
run Run a command in a new container
save Save one or more images to a tar archive (streamed to STDOUT by default)
search Search the Docker Hub for images
start Start one or more stopped containers
stats Display a live stream of container(s) resource usage statistics
stop Stop one or more running containers
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
top Display the running processes of a container
unpause Unpause all processes within one or more containers
update Update configuration of one or more containers
version Show the Docker version information
wait Block until one or more containers stop, then print their exit codes
Run 'docker COMMAND --help' for more information on a command.
关于这些命令的含义,可以看Docker 常用命令
比如,我们要搜索nginx镜像,那么只需要使用下面的命令即可。
docker search nginx
搜索结果中标记【OFFICIAL】的为官方镜像,其他为用户自定义镜像,可根据实际需要选择。
如果要获取镜像,可以使用docker pull
命令,如下所示,获取nginx。
# 拉取指定版本xxx镜像
docker pull nginx:xxx
# 拉取最新版本镜像
docker pull nginx //等价于docker pull nginx:latest
镜像拉取成功后,然后使用下面的命令启动nginx容器,如下所示。
docker run -d -p 8080:80 --name docker-nginx nginx
上面的命令会将容器内部的80端口映射到了本机的8080端口,所以启动成功后可以使用http://localhost:8080/
访问docker容器内部nginx80端口映射的地址,如下图所示。
一些常见的启动参数:
- -p 本机端口:容器端口 映射本地端口到容器
- -P 将容器端口映射为本机随机端口
- -v 本地路径或卷名:容器路径 将本地路径或者数据卷挂载到容器的指定位置
- -it 作为交互式命令启动
- -d 将容器放在后台运行 --rm 容器退出后清除资源
查看及停止容器
查看容器的基本命令如下所示。
# 查看运行中的容器
docker ps
# 查看所有容器(包括正在运行和已经停止运行的)
docker ps -a
停止容器命令使用的是kill命令,如下所示。
# 通过id直接关闭容器
# docker kill a0fbf4519279
# 通过容器名称直接关闭容器
docker kill docker-nginx
# 通过id直接容器 默认等待十秒 超时强制关闭
# docker stop a0fbf4519279
# 通过容器名称关闭容器 默认等待十秒 超时强制关闭 等价于 docker stop -t=10 docker-nginx
docker stop docker-nginx
重新启动容器的命令如下。
# 启动容器可通过容器id或者容器名称
# 通过容器名称启动容器,如果已启动则忽略
docker start docker-nginx
# 通过容器名称重新启动容器,如果未启动则直接启动,如果已启动则关闭再启动
# docker restart docker-nginx
Docker是如何工作的
Docker 使用的是 C/S 结构,即客户端/服务器体系结构。明白了 Docker 客户端与 Docker 服务器进行交互时, Docker 服务端负责构建、运行和分发 Docker 镜像。 并且Docker 客户端和服务端可以运行在一台机器上,可以通过 RESTful 、 stock 或网络接口与远程 Docker 服务端进行通信。
而Docker 的底层核心原理是利用了 Linux 内核的 namespace 以及 cgroup 特性,其中 namespace 进行资源隔离,cgroup 进行资源配额, 其中 Linux 内核中一共有 6 种 namespace,分别对应如下。
在系统调用中有三个与namespace有关的函数,分别是clone、unshare和setns。
clone:如果想让子进程拥有独立的网络地址,TCP/IP 协议栈,那么就可以使用clone,如下所示。参考:https://man7.org/linux/man-pages/man2/clone.2.html
clone(cb, *stack , CLONE_NEWNET, 0)
unshare:将当前进程转移到新的 namespace 中, 例如使用 fork 或 vfork 创建的进程将默认共享父级资源,使用 unshare 将子进程从父级取消共享。参考:https://man7.org/linux/man-pages/man2/setns.2.html
setns:给指定的PID指定 namespace, 通常用于共享 namespace。参考:https://man7.org/linux/man-pages/man2/setns.2.html
Linux 的内核层支持了在系统调用中隔离 namespace, 通过给一个进程分配单独的 namespace 从而让其在各个资源维度进行隔离,每个进程都能获取到自己的主机名,IPC、 PID、IP和根文件系统、用户组等信息,就像在一个独占系统中,不过虽然资源进行了隔离,但是内核还是共享同一个,这也是比传统虚拟机轻量的原因之一。
另外只有资源进行隔离还不够,要想保证真正的故障隔离,互不影响, 还需要对针对 CPU, 内存,GPU 等进行限制,因为如果一个程序出现死循环或者内存泄露也会导致别的程序无法运行。
Docker 网络
一个容器要想提供服务,就需要将自身的网络暴露出去。Docker 是与宿主机上的环境是隔离的,要想暴露服务就需要显示告诉 Docker 哪些端口允许外部访问,在运行 docker run -p 80:80 nginx 时这里就是将容器内部的 80 端口暴露到宿主机的 80 端口上,具体的端口转发下面会具体分析一下。容器的网络部分是容器中最重要的部分,也是构建大型集群的基石,在我们部署 Docker 的应用时,需要要对网络有个基本的了解。
目前,Docker 提供了四种网络模式,分别为 Host、Container、 None和Bridge ,我们可以使用 --net 来进行指定网络模式。
Host 模式
Host 模式不会单独为容器创建 network namespace, 容器内部直接使用宿主机网卡,此时容器内获取 ip 为宿主机 ip,端口绑定直接绑在宿主机网卡上,优点是网络传输时不用经过 NAT 转换,效率更高速度更快。但是docker host上已经使用的端口就不能再用了,网络的隔离性不好。
可以使用如下的命令开启Host模式。
docker run --net host nginx
Container 模式
和指定的 container 共享 network namespace, 共享网络配置,ip 地址和端口,其中无法共享网络模式为 Host 的容器。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的,两个容器的进程可以通过 lo 网卡设备通信。
开启Host模式的命令如下。
docker run --net container:xxx_containerid nginx
None 模式
使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。指定为 None 模式的容器内将不会分配网卡设备,仅有内部 lo 网络。
启动None 模式的命令如下所示。
docker run --net none busybox ifconfig
Bridge 模式
该模式为默认模式,容器启动时会被分配一个单独的 network namespace,同时 Docker 在安装/初始化时会在宿主机上创建一个名为 docker0 的网桥,该网桥也作为容器的默认网关,容器网络会在该网关网段内进行 ip 的分配。
Bridge 模式的启动命令如下所示。
docekr run --net bridge busybox ifconfig
在上图中,当外部请求主机网卡 3000 端口时将它进行目的地址转换(DNAT), 目的地址修改为 172.18.0.2,端口修改为 80,修改好目的地址后流量会从本机默认网卡经过 docker0 转发到对应的容器,这样当外部请求宿主机的 3000 端口,内部会将流量转发给内部容器服务,从而实现服务的暴露,流程示意图如下所示。
同样 Docker 内部访问外部接口也会进行源地址转换(SNAT), 容器内部请求 google.com, 服务器上收到的则是主机网卡的 ip。
Bridge 模式由于多了一层 NAT 转换所以效率会比 Host 模式差一些,但是能够很好的隔离外部网络环境,让容器独享 ip 且具有完整的端口空间。
上面四种网络模式是 Docker 自带的几种工作方式,但是部署 Kubernetes 需要所有的容器都工作在一个局域网中,所以在部署集群时需要多主机网络插件的支持。
Flannel
多主机网络解决方案有 CNCF 推出的 CNI 规范以及 Docker 自带的 CNM 方案,但是目前大家用的最多的还是 CNI 规范,其中一种实现就是 Flannel。
Flannel 使用了报文嵌套技术来解决多主机网络互通问题,将原始报文进行封包,指定包ip为目的主机地址,等包到达主机后再进行拆包传送到对应的容器。Flannel是 CoreOS 团队针对 Kubernetes 设计的一个覆盖网络(Overlay Network)工具,其目的在于帮助每一个使用 Kuberentes 的 CoreOS 主机拥有一个完整的子网。下图显示 flannel 使用效率更高的 UDP 协议来在主机间传输报文。
- 数据从源容器中发出后,经由所在主机的docker0虚拟网卡转发到flannel0虚拟网卡,这是个P2P的虚拟网卡,flanneld服务监听在网卡的另外一端。
- Flannel通过Etcd服务维护了一张节点间的路由表,该张表里保存了各个节点主机的子网网段信息。
- 源主机的flanneld服务将原本的数据内容UDP封装后根据自己的路由表投递给目的节点的flanneld服务,数据到达以后被解包,然后直接进入目的节点的flannel0虚拟网卡,然后被转发到目的主机的docker0虚拟网卡,最后就像本机容器通信一样的由docker0路由到达目标容器。
目前主流跨主机通信目前常用的有三种,分别是Overlay、hostgw和BGP技术。
- overlay: 即上面的报文嵌套。
hostgw: 通过修改主机路由表实现转发,不需要拆包和封包,效率更高,但同样限制比较多,只适合在相同局域网中的主机使用。
BGP:使用软件实现的 BGP(边界网关协议)以此向网络中的路由器广播路由规则。和 hostgw 一样不需要拆包,但是实现成本较高。
Kubernetes
在小规模场景下,使用 Docker 可以一键部署应用确实很方便,但是当出现需要在几百台主机上进行多副本部署,需要管理这么多主机的运行状态以及服务的故障时需要在其他主机重启服务,想象一下就知道手动的方式不是一种可取的方案,这时候就需要利用 Kubernetes 这种更高维度的编排工具来管理了。
Kubernetes 简称 K8S,是Google 2014年创建管理的大规模容器管理技术。 简单说 K8S 就是抽象了硬件资源,将 N 台物理机或云主机抽象成一个资源池,容器的调度交给 K8S 进行管理,CPU 不够用就调度到一台足够使用的机器上,内存不满足要求就会寻找一台有足够内存的机器在上面创建对应的容器,服务因为某些原因挂了, K8S 还会帮我们自动迁移重启。K8S是一个开源的平台,实现了容器集群的自动化部署、自动扩缩容、维护等功能。
Kubernetes 具有如下特点:
- 可移植: 支持公有云,私有云,混合云,多重云(multi-cloud)。
- 可扩展: 模块化, 插件化, 可挂载, 可组合。
- 自动化: 自动部署,自动重启,自动复制,自动伸缩/扩展。
Kubernetes架构
K8s集群由两节点组成:Master和Node。在Master上运行etcd,Api Server,Controller Manager和Scheduler四个组件。后三个组件构成了K8s的总控中心,负责对集群中所有资源进行管控和调度。在每个Node上运行kubect、proxy和docker daemon三个组件,负责对节点上的Pod的生命周期进行管理,以及实现服务代理的功能。另外所有节点上都可以运行kubectl命令行工具。
API Server作为集群的核心,负责集群各功能模块之间的通信。集群内的功能模块通过Api Server将信息存入到etcd,其他模块通过Api Server读取这些信息,从而实现模块之间的信息交互。Node节点上的Kubelet每隔一个时间周期,通过Api Server报告自身状态,Api Server接收到这些信息后,将节点信息保存到etcd中。Controller Manager中 的Node controller通过Api server定期读取这些节点状态信息,并做响应处理。Scheduler监听到某个Pod创建的信息后,检索所有符合该pod要求的节点列表,并将pod绑定到节点列表中最符合要求的节点上。如果scheduler监听到某个Pod被删除,则调用api server删除该Pod资源对象。kubelet监听pod信息,如果监听到pod对象被删除,则删除本节点上的相应的pod实例,如果监听到修改Pod信息,则会相应地修改本节点的Pod实例。
Kubernetes主要由以下几个核心组件组成:
- etcd保存了整个集群的状态;
- apiserver提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;
- controller manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
- scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;
- kubelet负责本Node节点上的Pod的创建、修改、监控、删除等生命周期管理,同时Kubelet定时“上报”本Node的状态信息到Api Server里;
- Container runtime负责镜像管理以及Pod和容器的真正运行(CRI);
Kubernetes安装
在Mac中安装了Docker之后,会自动安装了Kubernetes,正常情况下,我们只需要在Docker的Preferrences->Kubernetes中勾选Enable Kubernetes,然后点击Apply按钮即可。
Github上有个开源项目可以帮我们手动拉取镜像,执行下面命令拉去改项目代码到本地。
https://github.com/xiangzhihong/k8s-docker-desktop-for-mac
cd k8s-docker-desktop-for-mac
sh load_images.sh
然后,执行下面的命令验证是否安装成功。
kubectl cluster-info
K8S 与传统 IaaS 系统的不同
IaaS 就是 Infrastructure as a service, 所谓基础设施即服务,开发者想要上线一个新应用需要申请主机,ip, 域名等一系列资源,然后登录主机自行搭建所需环境,部署应用上线,这样不仅不利于大规模操作,而且还增加了出错的可能,运维或开发这常常自己写脚本自动化完成,遇到一些差异再手动修改脚本,非常痛苦。
K8S 则是将基础设施可编程化,由原来的人工申请改为一个清单文件自动创建,开发者只需要提交一份文件,K8S 将会自动为你分配创建所需的资源。对这些设施的 CRUD 都可以通过程序的方式自动化操作。
为了了解 K8S 的基础知识,下面来部署一个 Node SSR 应用。首先,初始化一个应用模版。
npm install create-next-app
npx create-next-app next-app
cd next-app
创建好工程后给添加一个 Dockerfile 用来构建服务的镜像Dockerfile。
FROM node:8.16.1-slim as build
COPY ./ /app
WORKDIR /app
RUN npm install
RUN npm run build
RUN rm -rf .git
FROM node:8.16.1-slim
COPY --from=build /app /
EXPOSE 3000
WORKDIR /app
CMD ["npm", "start"]
Dockerfile 做了两部分优化:
- 使用精简版的 node 基础镜像, 大大减少镜像体积
- 使用分步构建的方式, 能够减少镜像层数以及移除临时文件从而减少了镜像体积。
然后使用下面的命令构建镜像。
docker build . --tag next-app
之后我们就可以向 Kubernetes 提出我们应用的要求了。为了保证高可用,服务至少创建两个副本,我们还需要一个应用的域名当这个域名请求到我们集群上时自动转发到我们的服务上。那么我们对应的配置文件Deployment.yaml就可以这么写。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: app-ingress
spec:
rules:
- host: next-app-server
http:
paths:
- backend:
serviceName: app-service
servicePort: 80
---
kind: Service
apiVersion: v1
metadata:
name: app-service
spec:
selector:
app: web
ports:
- port: 80
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: next-app
name: next-app
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
上面清单主要的功能是:
- 首先需要一个 Deployment 控制器,镜像为 next-app, 服务端口为 3000,给我创建两个副本。
- 还需要创建一个 Service, 这个 Service 指向由副本控制器创建的几个 next-app。
- 申请一个 Ingress 入口, 域名为 next-app-server, 其指向刚刚的 Service。
然后,执行下面的命令提交申请给 K8S。
kubectl apply -f ./Deployment.yaml
接着就可以看到已经部署的 pod。
sh-4.4$ kubectl get pod
NAME READY STATUS RESTARTS AGE
app-deployment-594c48dbdb-4f4cg 1/1 Running 0 1m
app-deployment-594c48dbdb-snj54 1/1 Running 0 1m
然后浏览器打开 Ingress 里配置的域名即可访问对应的应用(前提是这个域名能够打到你的 K8S 集群节点上),如下图所示。
应用发布系统
K8S 仅仅负责容器的编排,实际上如果部署应用还需要外部 Pipeline 的支持,代码的构建,静态检查,镜像的打包由 Pipeline 完成。
目前,国内用的比较多的发布系统常常由下面几个服务组成:GitLab/GitHub、Jenkins,、Sonar,、Harbor等。