docker的前世今生
docker来自哪里?
从我们运维部署的历史来看,宿主机从最初的物理机到虚拟机,再到docker,一步步演进到现在。技术演进其实是为了解决当前技术的痛点,那我们来看看有哪些痛点以及如何克服痛点的。
物理机
一般来说,一台物理服务器的配置都很高,都会部署较多的服务以充分利用物理机的资源,这就会带来许多的问题
- 服务之间互相影响,如A服务占满了CPU或内存,就会把机器上所有服务都卡死
- 资源浪费,若服务对基础环境的要求不一致,如linux或java版本等,那么服务就得放在别的机器上
- 故障恢复困难,一旦系统出现故障或卡死,需要重启机器上的所有服务,经历过的都知道,时间漫长且困难
虚拟机
既然物理机有以上问题,那么怎么解决呢?从解决问题的思路上来说,就是隔离,把基础环境隔离,服务隔离,这就引入了虚拟机,通过虚拟化技术,将一台物理机隔离成多个独立的虚拟机,互不受影响,这样就能部分解决物理机的问题,所有的影响都限制到了单台虚拟机,而不是整台物理机。当然,引入虚拟机也有对应的问题,以及提出了更高的要求
- 虚拟机的管理复杂,存在管理虚机的消耗
- 虚机的依赖管理复杂,常常遇到测试环境没问题,而生产环境报错的情况
docker
那么,虚拟机上面的问题,该如何解决呢?再次隔离,把每个服务都进行隔离,互不影响,这样就将影响降低到最小。那么docker就应运而生,每个服务就是一个容器,互相隔离,环境版本都可以根据自己的需求进行配置,服务出现故障不会影响其他服务,也不会因为资源占用问题,夯死整台机器。此外容器的故障恢复速度非常快,基本是秒级。
关于解决虚拟机的问题,docker解决了虚机的依赖管理复杂的问题,这是毋庸置疑的。此外,docker是不需要虚拟机的,若直接部署在物理机上,那么就解决虚拟机管理复杂的问题(因为实际原因,docker可能是部署在虚机上的)。当然任何的新技术都会引入新的问题,关于docker的问题,后面介绍k8s的时候再介绍。
docker现在何处
docker的基本使用
docker可以简单分为两块,一个是镜像,一个是容器。镜像是静态的,容器是运行时。那么我们先看看镜像是如何创建的。
编写dockerfile
# 基础镜像
FROM dockerhub.kubekey.local/public/openjdk:8
# 对外暴露的端口
EXPOSE 8082
EXPOSE 9998
#启动时接受的参数
ARG profile
ENV profile ${profile}
RUN echo "profile: ${profile}"
# 将原路径复制到镜像内的目录下
COPY ./OaCenter-1.0-SNAPSHOT.jar /jar/OaCenter-1.0-SNAPSHOT.jar
##统一内部时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone
# 启动时执行命令
ENTRYPOINT java -server -Xms4g -Xmx4g -Xss512M -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/outOfMemory.hprof -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20m -Xloggc:/logs/gc.log -jar /jar/OaCenter-1.0-SNAPSHOT.jar --spring.profiles.active=${profile}
构建镜像
docker build --build-arg profile=uat -f Dockerfile -t $REGISTRY/$DOCKERHUB_NAMESPACE/$APP_NAME:$BUILD_NUMBER .
运行镜像
docker run -d $REGISTRY/$DOCKERHUB_NAMESPACE/$APP_NAME:$BUILD_NUMBER --name xxx
至此,容器就运行起来,对外提供服务了。
docker运维的常用命令
# 停止容器
docker stop xxx
# 重启容器
docker restart xxx
#进入容器
docker exec -it xxx bash
# 进入 shell启动的容器
docker exec -it xxx sh
# 查看所有容器
docker ps
# 查看所有容器,包括停止的
docker ps -a
# 容器的状态
docker stats
# 查看某个容器的详细信息
docker inspect xxx
# 复制容器的文件到宿主机
docker cp xxx:src_path dest_path
# 复制宿主机文件到容器
docker cp src_path xxx:dest_path
# 查看容器的日志
docker logs -f xxx
# 清理未使用的镜像、容器、卷和网络。
docker system prune
docker三板斧
一个容器服务突然报错,该如何处理?
第一步:docker ps
docker ps查看所有容器,若没看到,那么docker ps -a,如果docker ps -a也没看到,那么恭喜你,三板斧失败,你需要找到镜像文件,用启动命令或脚本去启动了。反之,如遇到下面三种情况,那么直接docker restart xxx,一般80%的情况下都能解决问题。
- docker ps没看到,docker ps -a看到了
- status为Exited
- status为UP,括号里unhealthy
docker ps
CONTAINER ID IMAGE COMMAND PORTS status NAMES
09b93464c2f7 nginx:latest "nginx -g 'daemon off" 80/tcp, 443/tcp Up 16 hours (healthy) myrunoob
96f7f14e99ab mysql:5.6 "docker-entrypoint.sh" 0.0.0.0:3306->3306/tcp Exited(0) 16 hours ago mymysql
Up:表示容器正在运行中。
Created:表示容器已经被创建,但还未启动。
Exited:表示容器已经停止运行
第二步:docker logs -f xxx
容器若重启失败,那么就得看容器的日志了,从容器的日志查看失败的原因,可能内存不足,配置缺失等等,如果找到服务失败的原因,并解决掉,然后docker restart xxx,应该也能解决问题。
第三步:docker inspect xxx
主要检查State,Env,HostConfig里面的信息,一般来说就能看到错误的原因,针对性的去处理即可。
docker inspect xxx
[
{
"Id": "d2f5e3f19a6a",
"Created": "2024-07-23T00:00:00Z",
"Path": "bash",
"Args": [],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 12345,
"ExitCode": 0,
"Error": "",
"StartedAt": "2024-07-23T00:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
"Image": "sha256:abc123",
"ResolvConfPath": "/var/lib/docker/containers/d2f5e3f19a6a/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/d2f5e3f19a6a/hostname",
"HostsPath": "/var/lib/docker/containers/d2f5e3f19a6a/hosts",
"LogPath": "/var/lib/docker/containers/d2f5e3f19a6a/d2f5e3f19a6a-json.log",
"Name": "/my_container",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "",
"ExecIDs": null,
"HostConfig": {
"Binds": null,
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "default",
"PortBindings": {},
"RestartPolicy": {
"Name": "no",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"CapAdd": null,
"CapDrop": null,
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"ConsoleSize": [
0,
0
],
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": null,
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": null,
"DiskQuota": 0,
"KernelMemory": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": false,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"Mounts": [],
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/l/abc123/diff",
"MergedDir": "/var/lib/docker/overlay2/merged",
"UpperDir": "/var/lib/docker/overlay2/upper",
"WorkDir": "/var/lib/docker/overlay2/work"
},
"Name": "overlay2"
},
"Mounts": [],
"Config": {
"Hostname": "d2f5e3f19a6a",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"bash"
],
"Image": "ubuntu",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "abc123",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {},
"SandboxKey": "/var/run/docker/netns/abc123",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "abc123",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:02",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "abc123",
"EndpointID": "abc123",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
}
}
]
一般来说,三板斧能解决95%以上的问题。当然还有一些情况,比如我近期遇到过修改宿主机iptables,导致容器挂掉的情况,重启也会失败,这种情况,需要重启宿主机就能解决此问题。
docker的原理和优势
原理
主要是利用了Linux内核的特性:namespace,cgroup,rootfs。
- namespace将文件,网络,进程和权限进行隔离。
- cgroup限制namespace可使用的资源,如cpu,内存等
- rootfs用以创建新的根文件系统
优势
- 独立性,每个容器完全隔离,不会互相影响
- 启动快,容器因为简单和资源少,启动速度在秒级
- 运行环境一致,不会出现测试环境可以,生产环境失败的情况
- 迁移快,更换机器或者扩展节点,只需保存容器,在新机器上启动即可
docker去向何方
docker已经很方便了,为何现在k8s更如火如荼呢?这时候就得说到docker的缺点了。在大型系统中,采用微服务+docker部署方式之后,同时可能存在几百上千的容器在同时运行,同时产生的就是管理问题。我怎么把上千的容器部署到对应的节点上,怎么对容器进行部署重启,总不能一台机器一台机器上去运维部署,这显然不符合技术自动化发展的趋势。在这种情况下,k8s应运而生(其实不仅仅有k8s,还有docker swam等,最后k8s脱颖而出)。k8s简单来说就是容器编排工具,那么我们看看k8s的架构图。
Controller Manager 负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
API Server 提供了资源操作的唯一入口,并提供认证、授权、访问控制、API 注册和发现等机制;
Scheduler 负责资源的调度,按照预定的调度策略将 Pod 调度到相应的机器上;
Kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡;
etcd 保存了整个集群的状态;
Kubelet 负责维护容器的生命周期,同时也负责 Volume(CVI)和网络(CNI)的管理;
总结
k8s为使用大规模容器服务提供了极大的便利,对于持续部署和自动化运维几乎都是一键操作。但是在最新的k8s版本已经舍弃了docker,后面可以继续聊聊这个的原因以及k8s的详细介绍和运维