别再纠结容器和虚拟机的区别了,你只需要知道"Docker把应用和环境打包成标准集装箱,K8s就是管理这堆集装箱的码头吊机"
写在前面
Docker和Kubernetes(K8s)是"云原生"时代的基石。面试问到这块,本质上是在问两件事:①你的应用怎么打包和交付的?②上了生产环境怎么管理、怎么扩缩容?
Docker解决了什么问题?
你以前上线一个Java应用:打Jar包 → 扔到Linux服务器上 → 装JDK → 配环境变量 → 装Redis客户端 → 装Nginx......搞了半天,换一台服务器又得重来一遍。更别说十台、一百台服务器了。
Docker说:把应用和它需要的所有环境(JDK、依赖库、配置文件)一起打包成一个"镜像",在任何装了Docker的机器上,一个docker run命令就能跑起来。
Kubernetes解决了什么问题?
Docker解决了"单机部署环境"的问题,但它管不了:100台机器上100个容器怎么调度?挂掉了谁重启?流量来了谁负责扩容?
Kubernetes说:我来管。 它是"容器编排平台",负责在成百上千台服务器上调度、管理、扩缩容这些容器。
一、Docker的核心概念:镜像 vs 容器
很多人把镜像和容器搞混。一句话区分:
- 镜像(Image) :死的,是"类"(静态模板)。比如你打包好的
my-app:v1.0。 - 容器(Container) :活的,是"实例"(运行中的进程)。
docker run my-app:v1.0跑起来就是一个容器。
好比:镜像就是装好所有家具的"样板间图纸",容器就是按照图纸建出来的"真实房间"。你可以用一张图纸建出无数个一模一样的房间。
二、Docker和虚拟机的区别(面试必问)
这是面试绝对高频题,你要讲清楚本质区别。
2.1 虚拟机(VM)
- 虚拟的是"硬件" :虚拟机软件(VMware/VirtualBox)模拟出一套完整的硬件(CPU、内存、硬盘、网卡),然后在上面装一个完整的客户操作系统(Guest OS)。
- 资源开销大:每个虚拟机都有一个完整OS(几GB到几十GB),启动分钟级。
- 隔离性强:完全隔离,一个VM崩溃不影响另一个。
2.2 容器(Docker)
- 虚拟的是"操作系统内核" :所有容器共享宿主机的内核(Linux内核) ,只是通过Linux的Namespace 技术给自己造了个"虚假的独立王国"(自己独立的PID、网络、文件系统、用户空间),通过Cgroups技术限制自己能用的CPU和内存。
- 资源开销小:容器只是一个进程,几MB到几百MB,启动秒级。
- 隔离性弱:共享内核,一个容器把内核搞崩了(概率极低),所有容器都崩。
面试官追问:"Linux的Namespace有哪些?"
不需要全背,记住几个关键的:
- PID Namespace:容器里看到的进程是自己的,看不到宿主机其他进程。
- Network Namespace:容器有自己的IP、端口,互不冲突。
- Mount Namespace:容器有自己的文件系统根目录。
- UTS Namespace:容器有自己的主机名。
- IPC Namespace:容器有自己的共享内存、信号量。
一句话总结:虚拟机=模拟硬件+装整套OS(重),容器=共享内核+隔离视野(轻)。
三、镜像分层(轻量的核心秘密)
面试官常问:"Docker镜像为什么比虚拟机镜像轻那么多?"
因为Docker镜像是分层存储的(UnionFS/OverlayFS)。
3.1 分层是怎么工作的?
比如你写一个Java应用,Dockerfile可能是:
dockerfile
FROM openjdk:17-jdk-alpine # 第1层:JDK
COPY target/app.jar /app.jar # 第2层:你的Jar包
CMD ["java", "-jar", "/app.jar"] # 第3层:启动命令(metadata层)
执行docker build时,每一行指令生成一个只读层(Layer)。
如果你有100个不同的Java应用,它们都基于同一个openjdk:17-jdk-alpine基础镜像,这层JDK在硬盘上只存一份,100个应用共享这一层。
这就好比100个集装箱共用同一款标准底盘,只有上层货物(Jar包)不同。
3.2 容器读写层(Copy-on-Write)
容器运行时,会在只读镜像层上再加一个读写层(容器层) 。所有写操作(比如程序生成的日志、临时文件)都写在容器层,不修改底层镜像。
好处:
- 多个容器共享同一个镜像,内存和磁盘占用少。
- 容器删除时,只删除读写层,镜像还在。
所以docker pull时,只有你没下载过的层才会下载;docker push也同理,只上传变动的层,不是整个镜像重新上传。
四、Dockerfile怎么写的(面试常问)
虽然不用背全部命令,但常见的你要懂:
| 指令 | 作用 | 示例 |
|---|---|---|
FROM |
基于哪个基础镜像 | FROM openjdk:17-jdk-alpine |
COPY / ADD |
把文件拷进镜像 | COPY target/app.jar /app.jar |
RUN |
构建时执行的命令 | RUN apt-get update && apt-get install -y curl |
CMD |
容器启动时执行的命令(可被覆盖) | CMD ["java", "-jar", "/app.jar"] |
ENTRYPOINT |
容器启动时执行的命令(不可被覆盖) | ENTRYPOINT ["java", "-jar"] |
EXPOSE |
声明容器监听的端口(文档作用) | EXPOSE 8080 |
ENV |
设置环境变量 | ENV JAVA_OPTS="-Xmx512m" |
CMD vs ENTRYPOINT的区别(面试考点) :
- CMD :可以被
docker run后面的命令覆盖。比如docker run my-app bash会覆盖CMD里的java -jar,执行bash。 - ENTRYPOINT :不会被覆盖,
docker run后面跟的参数会作为追加参数传给ENTRYPOINT。
dockerfile
# 写法1:固定命令
ENTRYPOINT ["java", "-jar", "/app.jar"]
# 写法2:可变的JVM参数
ENTRYPOINT ["java", "-jar", "/app.jar"]
CMD ["--spring.profiles.active=prod"] # 默认参数,可以被覆盖
五、Kubernetes(K8s):容器编排平台
Docker负责"打包+运行单个容器",K8s负责"在成千上万台机器上调度这些容器"。
5.1 核心概念(必须搞懂)
① Pod(最小调度单元)
Pod是K8s里最小的部署单元 。一个Pod里可以跑一个或多个容器(通常就一个)。
- 同一个Pod里的容器共享:网络(同一个IP)、存储卷、PID空间。
- 典型场景:主容器(你的Java应用)+ Sidecar(日志收集、监控Agent)放在同一个Pod里,共享网络和存储。
面试官追问:"为什么不直接调度容器,要加个Pod?"
因为有些应用需要"紧耦合"地协同工作(比如应用日志需要实时被sidecar收集),它们必须部署在同一台机器上,共享网络和存储。Pod就是K8s提供的这个"最小部署单元",里面的容器"同生共死"。
② Deployment(无状态应用的管理器)
Deployment管理着一组Pod的期望状态:
- 副本数量(Replicas):我要跑3个Pod → Deployment确保始终有3个在运行。
- 滚动更新:新版本发布时,逐个替换Pod,不停机。
- 回滚:新版本出问题了,一键回滚到上一个版本。
③ Service(稳定的访问入口)
Pod的IP是飘忽不定 的(挂了重启就变了),K8s的Service给一组Pod提供固定的访问入口(ClusterIP + 端口)。
- ClusterIP (默认):集群内部访问,比如A服务通过
order-service这个域名调B服务。 - NodePort:把端口暴露到宿主机上(外部可访问)。
- LoadBalancer:对接云厂商的负载均衡器(阿里云SLB、AWS ELB)。
④ Namespace(环境隔离)
把集群按环境隔开:dev、test、prod。不同Namespace里的资源互相隔离(但网络默认互通,可以通过NetworkPolicy限制)。一个K8s集群可以跑多个环境,节省服务器成本。
5.2 K8s是怎么工作的(节点角色)
- Master节点(控制平面) :管理集群,调度Pod,监控状态。包含API Server(统一入口)、Scheduler(调度器)、Controller Manager(控制器)、etcd(存集群状态)。
- Node节点(工作节点) :实际跑容器的机器,上面运行Kubelet(接收Master指令)、Kube-proxy(网络规则)。
5.3 滚动更新和回滚(面试必问)
场景:你的应用要从v1升级到v2。如果是传统方式:先停掉v1 → 部署v2 → 启动v2 → 这中间服务是不可用的。
K8s的滚动更新:
- 先启动一个新Pod(v2),等它就绪后。
- 再停掉一个旧Pod(v1)。
- 重复以上步骤,直到所有Pod都替换成v2。
整个过程服务始终可用(至少有一个Pod在服务)。
控制策略:
maxSurge:可以多启动多少个新Pod(比如25%)。maxUnavailable:最多有多少个旧Pod可以不可用(比如0,保证零停机)。
回滚 :kubectl rollout undo deployment/my-app,一键回到上一个版本。如果是线上故障(比如v2有Bug),这是最快最安全的止血手段。
5.4 探针(健康检查)
K8s怎么知道一个容器是"活着"还是"挂了"?靠探针(Probe) 。
- livenessProbe(存活探针) :如果失败了,K8s认为容器死了,重启容器。
- readinessProbe(就绪探针) :如果失败了,K8s认为容器还没准备好接收流量,不会把请求转发给它。
配置方式:
- HTTP GET:访问
/actuator/health,返回200就正常。 - TCP Socket:尝试连端口。
- Exec:执行脚本,返回0就正常。
六、面试官真正想听什么
场景1:"Docker和虚拟机有什么区别?"
普通回答:"容器轻量,虚拟机重量。"
加分回答:
"本质区别是虚拟化层级不同 。虚拟机虚拟的是硬件,需要完整的Guest OS,所以镜像有几个G,启动要几分钟。容器虚拟的是操作系统内核,它只是Linux宿主机上的一个普通进程,通过Namespace 实现资源隔离(PID、Network、Mount等),通过Cgroups 限制CPU和内存。所以容器镜像是MB级别,启动是毫秒级。在云原生时代,容器是更经济的资源利用方式,一台物理机可以跑几百个容器,虚拟机只能跑几十个。"
场景2:"你们项目里Docker和K8s怎么用的?"
加分回答:
"我们CI/CD流水线里,代码合并到主干后自动触发构建,用Maven打Jar包,然后
docker build构建镜像,推送到阿里云镜像仓库(ACR)。然后通过GitOps方式(ArgoCD)自动更新K8s的Deployment镜像版本,触发滚动更新。配合readinessProbe ,新版本就绪了才会切流量,整个上线过程零停机。"
场景3:"K8s滚动更新回滚过吗?"
加分回答(体现实战经验):
"有一次我们发版,新版本有个内存泄漏,上线后Pod内存持续飙升。我们通过监控发现异常后,执行
kubectl rollout undo秒级回滚到上一个稳定版本,整个回滚过程5秒内完成,用户几乎无感。之后我们调整了maxSurge和maxUnavailable参数,把滚动更新的"步长"调小了,减少回滚时的影响面。"
场景4:"容器里怎么看日志?"
加分回答:
"单个Pod用
kubectl logs -f pod-name。但集群环境下我们不会这么查,而是用ELK(Elasticsearch + Logstash + Kibana)或Loki ,在每个节点上部署Filebeat(作为DaemonSet,每个节点一个),收集所有Pod的日志统一打到Elasticsearch,在Kibana里按namespace、pod、traceId检索。生产环境根本不会去机器上docker logs,那是灾难。"
小结
| 核心概念 | 大白话 | 面试考点 |
|---|---|---|
| 镜像 | 静态模板(类) | Dockerfile、分层存储 |
| 容器 | 运行中的进程(实例) | 容器 vs 虚拟机 |
| Namespace | 隔离视野(假装自己是独立系统) | Linux底层隔离机制 |
| Cgroups | 限制资源(CPU/内存配额) | 容器不抢宿主资源 |
| Pod | 最小调度单元(1个或多个容器) | 为什么需要Pod |
| Deployment | 管理Pod期望状态 | 副本数、滚动更新、回滚 |
| Service | 稳定的访问入口 | ClusterIP/NodePort |
| 探针 | 健康检查 | liveness vs readiness |
记住一句话:Docker把应用和环境打包成标准集装箱,K8s是管理这堆集装箱的码头调度中心。理解了这个,云原生的大门就对你敞开了。
下一篇我们讲网络与操作系统基础------TCP三次握手、HTTP/HTTPS、Linux排查命令......面试里这些"底层常识"到底怎么答才能显得你真的懂?下期见。