1. 为什么要在虚拟机里折腾K8s和Docker
先把话说在前头:这套方案不是给生产环境用的,它的核心价值在于学习和验证。我见过太多人一上来就买云服务器、开容器集群,结果账单烧了不少,底层原理还是一问三不知。用虚拟机搭一套本地实验环境,最大的好处是随便折腾、快照回滚、零成本试错。你把网络搞崩了?快照回滚,三十秒满血复活。这种安全感是云环境给不了的。
这个项目的核心目标很明确:在一台物理机上,通过虚拟化软件创建若干台虚拟机,在其中部署Docker容器运行时和K8s集群,最终形成一套可以跑通容器编排全流程的本地实验平台。它解决的是“没有真实服务器集群,但又想动手实践容器云技术”这个矛盾。适合谁看?适合正在学容器技术的学生、想从零理解K8s架构的运维新人、以及需要一套离线实验环境的开发者。你不需要有深厚的Linux功底,但至少要能熟练敲命令、看得懂YAML文件的基本结构。
我这次参考的是先电2.4容器云手册的思路,但说实话,手册给的是骨架,真正跑起来会遇到一堆手册上没写的问题。下面我把自己踩过的坑、绕过的弯、以及最终跑通的完整流程,从头到尾捋一遍。
2. 整体架构设计与方案选型
2.1 为什么选虚拟机而不是物理机或云主机
先说选型逻辑。物理机方案直接排除——你不可能为了学K8s专门买三台服务器。云主机方案看似方便,但有几个硬伤:一是成本,跑一套最小可用的K8s集群至少需要三台节点,按量付费一个月下来不少钱;二是网络环境不可控,很多云厂商对底层网络做了封装,你很难直观地看到节点间的真实通信过程;三是快照和回滚不如本地虚拟机灵活。
虚拟机方案的核心优势在于完全掌控。从BIOS到网卡到磁盘,所有东西都在你手里。你可以随意调整CPU核数、内存大小、磁盘类型,可以模拟网卡故障,可以抓包分析。这种掌控感对于理解K8s的网络模型、存储模型至关重要。而且虚拟机支持快照,你在做任何高风险操作之前拍个快照,搞砸了直接回滚,这个体验是云主机给不了的。
2.2 虚拟机软件的选择与考量
市面上主流的桌面虚拟化软件就那么几款。我最终选的是VMware Workstation Pro,原因有三:第一,网络配置灵活,支持NAT、桥接、仅主机三种模式,还能自定义虚拟网络,这对搭建K8s集群的多节点网络非常关键;第二,快照管理成熟稳定,支持多级快照树;第三,性能损耗相对较小,尤其是磁盘I/O方面,跑容器负载不会太吃力。
如果你用的是其他虚拟化方案,比如VirtualBox或者基于KVM的方案,整体思路是一样的,只是在网络配置和快照操作上有些差异。核心原则是:虚拟机之间要能互相通信,虚拟机要能访问外网(至少初期需要),宿主机要能方便地管理虚拟机。
2.3 集群拓扑规划
我规划的集群拓扑是这样的:一台虚拟机作为Master节点,两台虚拟机作为Worker节点。为什么是两台Worker?因为一台Worker体现不出调度的效果,三台又太吃资源。两台刚好能演示Pod调度、服务发现、负载均衡这些核心功能。
每台虚拟机的配置如下:
| 节点角色 | CPU核数 | 内存 | 磁盘 | 操作系统 |
|---|---|---|---|---|
| Master | 2核 | 4GB | 40GB | CentOS 7.9 |
| Worker1 | 2核 | 4GB | 40GB | CentOS 7.9 |
| Worker2 | 2核 | 4GB | 40GB | CentOS 7.9 |
这里有个经验之谈:内存千万别抠。K8s的Master节点光控制平面组件就要吃掉不少内存,再加上Docker运行时和系统本身的开销,2GB内存跑起来会非常卡,甚至直接OOM。4GB是底线,有条件的话给Master 8GB会更舒服。磁盘方面,40GB是起步,因为Docker镜像很占空间,如果你要拉取多个大镜像,建议给到60GB以上。
2.4 网络模式的选择
VMware提供了三种网络模式:桥接、NAT、仅主机。我的建议是NAT模式。为什么?桥接模式下虚拟机会直接暴露在局域网中,IP地址由路由器分配,虽然方便但不够稳定,换个网络环境IP就变了。仅主机模式下虚拟机只能和宿主机通信,无法访问外网,初期安装软件会很麻烦。NAT模式折中了两者:虚拟机通过宿主机的网络访问外网,同时虚拟机之间在一个独立的子网内互相通信,IP地址由VMware的DHCP服务分配,相对稳定。
但NAT模式有个坑:默认的DHCP租约可能会导致IP地址变化。我的做法是在VMware的虚拟网络编辑器中,把DHCP租约时间设长,或者在虚拟机内部配置静态IP。我选择后者,直接给每台虚拟机配静态IP,一劳永逸。
3. 虚拟机创建与系统初始化
3.1 创建虚拟机的关键参数设置
打开VMware Workstation,新建虚拟机,选择“自定义(高级)”模式。为什么不选典型模式?因为典型模式会帮你做很多决定,而这些决定未必适合K8s场景。比如典型模式默认给20GB磁盘,不够用;默认网络模式可能不是你想要的。
在硬件兼容性页面,选择你当前VMware版本支持的最高版本。处理器配置页面,给2核。内存给4GB。网络类型选NAT。磁盘类型选SCSI,磁盘大小给40GB,选择“将虚拟磁盘拆分成多个文件”——这个选项的好处是虚拟机文件可以分散存储,不会因为单个文件过大导致宿主机磁盘碎片化。
创建完成后,先别急着启动。进入虚拟机设置,把“处理器”里的“虚拟化Intel VT-x/EPT或AMD-V/RVI”勾上。这个选项允许在虚拟机内部再跑虚拟化,虽然我们不在虚拟机里跑虚拟机,但某些容器运行时特性需要这个支持。
3.2 操作系统安装与基础配置
安装CentOS 7.9的过程就不赘述了,标准流程。装完之后第一件事是配置静态IP。编辑/etc/sysconfig/network-scripts/ifcfg-ens33文件,关键参数如下:
BOOTPROTO=static ONBOOT=yes IPADDR=192.168.100.10 NETMASK=255.255.255.0 GATEWAY=192.168.100.2 DNS1=114.114.114.114这里的网关地址192.168.100.2是VMware NAT模式的默认网关,你可以在虚拟网络编辑器里确认。三台虚拟机的IP分别设为192.168.100.10、192.168.100.11、192.168.100.12。
配置完成后重启网络服务:systemctl restart network。然后用ping命令测试虚拟机之间、虚拟机与宿主机、虚拟机与外网的连通性。这一步很重要,网络不通后面全是白搭。
3.3 系统层面的必要调整
接下来做几项系统层面的调整,这些调整在手册上可能没写,但不做的话后面会出各种幺蛾子。
第一,关闭防火墙。K8s集群内部组件之间需要大量通信,防火墙会拦截很多端口。学习环境直接关掉:
systemctl stop firewalld systemctl disable firewalld第二,关闭SELinux。SELinux的安全策略会阻止容器访问某些资源,导致容器启动失败。编辑/etc/selinux/config,把SELINUX=enforcing改为SELINUX=disabled,然后重启。
第三,配置主机名和hosts解析。给三台虚拟机分别设置主机名:
hostnamectl set-hostname master hostnamectl set-hostname worker1 hostnamectl set-hostname worker2然后在每台机器的/etc/hosts文件里添加:
192.168.100.10 master 192.168.100.11 worker1 192.168.100.12 worker2第四,配置SSH免密登录。在Master节点生成密钥对,然后把公钥分发到所有节点:
ssh-keygen -t rsa -N "" ssh-copy-id master ssh-copy-id worker1 ssh-copy-id worker2这样后续在Master节点上操作其他节点时就不需要反复输密码了。
第五,加载内核模块。K8s的网络和存储功能依赖一些内核模块,需要提前加载:
modprobe br_netfilter modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack_ipv4同时把这些模块写入/etc/rc.local或创建/etc/modules-load.d/k8s.conf文件,确保开机自动加载。
第六,调整内核参数。编辑/etc/sysctl.d/k8s.conf:
net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1然后执行sysctl --system使配置生效。这几项参数的作用是让iptables能够正确处理桥接流量,并开启IP转发。
注意:以上所有操作需要在三台虚拟机上分别执行。如果你觉得重复劳动太烦,可以写个脚本批量执行,或者用Ansible这类工具。但手动做一遍的好处是你能记住每一步在干什么。
4. Docker容器运行时部署
4.1 Docker版本选择与安装
Docker的版本选择有个讲究。K8s对Docker版本有兼容性要求,太新的版本可能不被支持,太旧的版本又有已知bug。我选的是Docker CE 20.10.x系列,这个版本经过大量验证,与K8s 1.23兼容性良好。
安装步骤如下。首先配置Docker的yum源:
yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo然后安装指定版本的Docker:
yum install -y docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io安装完成后启动Docker并设置开机自启:
systemctl start docker systemctl enable docker验证安装:docker version,能看到Client和Server的版本信息就说明安装成功了。
4.2 Docker配置优化
默认的Docker配置有几个地方需要调整。编辑/etc/docker/daemon.json:
{ "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "storage-driver": "overlay2", "registry-mirrors": ["https://registry.cn-hangzhou.aliyuncs.com"] }逐项解释一下。native.cgroupdriver=systemd是必须的,因为K8s默认使用systemd作为cgroup驱动,如果Docker用cgroupfs,两者会冲突,导致节点状态异常。日志配置是为了防止容器日志无限增长把磁盘撑爆。storage-driver选overlay2,这是目前性能最好、最稳定的存储驱动。registry-mirrors配置镜像加速,国内拉取官方镜像会快很多。
配置完成后重启Docker:
systemctl daemon-reload systemctl restart docker4.3 验证Docker功能
跑一个测试容器验证Docker是否正常工作:
docker run --rm hello-world如果能看到“Hello from Docker!”的输出,说明Docker安装配置都没问题。再跑一个Nginx容器测试网络和端口映射:
docker run -d --name test-nginx -p 8080:80 nginx然后在宿主机浏览器访问http://192.168.100.10:8080,能看到Nginx欢迎页就说明端口映射也正常。
实操心得:如果
docker run卡在拉取镜像阶段,大概率是镜像加速没配好或者网络有问题。先检查/etc/docker/daemon.json的格式是否正确,JSON格式错误会导致Docker启动失败。可以用docker info查看当前配置是否生效。
5. K8s集群部署与初始化
5.1 K8s组件安装
K8s的安装方式有很多种,kubeadm、二进制、minikube、kind等。我选kubeadm,因为它是官方推荐的集群部署工具,既能体现集群的真实架构,又不会像二进制方式那样繁琐到让人崩溃。
首先配置K8s的yum源。创建/etc/yum.repos.d/kubernetes.repo:
[kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled=1 gpgcheck=0然后安装kubeadm、kubelet、kubectl三个组件:
yum install -y kubelet-1.23.0 kubeadm-1.23.0 kubectl-1.23.0这里指定了1.23.0版本,为什么?因为从1.24开始,K8s移除了对Docker的直接支持,改用containerd作为默认运行时。虽然可以通过额外配置继续用Docker,但为了减少不必要的麻烦,我选择1.23这个仍然原生支持Docker的版本。
安装完成后设置kubelet开机自启:
systemctl enable kubelet注意,此时不要启动kubelet,因为还没初始化集群,启动会报错。
5.2 Master节点初始化
在Master节点上执行初始化命令:
kubeadm init \ --apiserver-advertise-address=192.168.100.10 \ --image-repository=registry.aliyuncs.com/google_containers \ --kubernetes-version=v1.23.0 \ --service-cidr=10.96.0.0/12 \ --pod-network-cidr=10.244.0.0/16逐项解释参数。--apiserver-advertise-address指定API Server监听的地址,填Master节点的IP。--image-repository指定镜像仓库,因为官方镜像在国内拉取困难,换成阿里云的镜像仓库。--kubernetes-version指定版本,必须和安装的kubeadm版本一致。--service-cidr是Service的网段,--pod-network-cidr是Pod的网段,这两个网段不能和宿主机网段重叠。
初始化过程会拉取一堆镜像,视网络情况可能需要几分钟到十几分钟。初始化成功后,终端会输出一段kubeadm join命令,类似这样:
kubeadm join 192.168.100.10:6443 --token abcdef.1234567890abcdef \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx这段命令务必保存下来,Worker节点加入集群时需要用到。如果忘了保存,可以用kubeadm token create --print-join-command重新生成。
初始化完成后,按照提示配置kubectl:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config然后执行kubectl get nodes,应该能看到Master节点,但状态是NotReady。这是正常的,因为还没部署网络插件。
5.3 部署网络插件
K8s的网络插件我选Calico。为什么选Calico而不是Flannel?Calico支持网络策略,功能更强大,而且性能更好。虽然配置稍微复杂一点,但值得。
kubectl apply -f https://docs.projectcalico.org/v3.21/manifests/calico.yaml如果你的网络环境访问不了这个地址,可以先把文件下载下来再apply。部署完成后等几分钟,再执行kubectl get nodes,Master节点应该变成Ready状态。
5.4 Worker节点加入集群
在Worker1和Worker2上分别执行之前保存的kubeadm join命令。执行完成后,在Master节点上执行kubectl get nodes,应该能看到三个节点全部是Ready状态。
如果Worker节点加入失败,最常见的原因是token过期。默认token的有效期是24小时,过期后需要重新生成。另外检查Worker节点的时间是否和Master同步,时间偏差过大会导致证书验证失败。
5.5 集群状态验证
集群搭建完成后,跑几个命令验证一下:
kubectl get nodes kubectl get pods --all-namespaces kubectl cluster-infokubectl get pods --all-namespaces应该能看到Calico的Pod、CoreDNS的Pod、API Server的Pod等都在Running状态。如果有Pod一直处于Pending或CrashLoopBackOff状态,需要进一步排查。
常见问题:Calico的Pod一直处于Pending状态,通常是因为镜像拉取失败。可以
kubectl describe pod查看具体原因。如果是镜像问题,可以手动拉取镜像或者配置镜像加速。
6. 集群功能验证与常见问题排查
6.1 部署一个测试应用
集群搭好了,总得跑个东西验证一下。我部署一个Nginx Deployment:
kubectl create deployment nginx-test --image=nginx:1.21 --replicas=2然后暴露服务:
kubectl expose deployment nginx-test --port=80 --type=NodePort查看Service:
kubectl get svc nginx-test你会看到一个NodePort类型的Service,端口号在30000-32767之间。然后在浏览器访问http://192.168.100.10:端口号,应该能看到Nginx欢迎页。多刷新几次,如果能看到不同的Pod响应,说明负载均衡也正常工作了。
6.2 常见问题速查表
下面这张表是我在实际操作中遇到过的典型问题及解决方法,按出现频率排序:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 节点状态NotReady | 网络插件未部署或异常 | kubectl describe node查看Conditions | 重新部署Calico,检查Pod日志 |
| Pod一直Pending | 资源不足或调度失败 | kubectl describe pod查看Events | 检查节点资源,调整资源请求 |
| 镜像拉取失败 | 网络问题或镜像不存在 | kubectl describe pod查看Events | 配置镜像加速,或手动拉取镜像 |
| kubelet启动失败 | cgroup驱动不匹配 | journalctl -u kubelet查看日志 | 统一Docker和K8s的cgroup驱动 |
| 节点加入失败 | token过期或时间不同步 | 检查token有效期和节点时间 | 重新生成token,同步时间 |
| Service无法访问 | 网络插件异常或iptables规则问题 | 检查Calico Pod状态和iptables规则 | 重启网络插件,清理iptables规则 |
6.3 独家避坑技巧
第一个坑:虚拟机磁盘空间不足。Docker镜像和容器日志会迅速吃掉磁盘空间。我的做法是定期执行docker system prune -a清理无用镜像和容器,同时给/var/lib/docker单独挂一块盘。如果做不到单独挂盘,至少把日志限制配置好。
第二个坑:虚拟机时间不同步。虚拟机挂起再恢复后,系统时间可能会漂移,导致K8s证书验证失败。安装ntp或chrony服务,配置时间同步:
yum install -y chrony systemctl start chronyd systemctl enable chronyd第三个坑:快照回滚后网络异常。虚拟机快照回滚后,MAC地址可能会变化,导致网络配置失效。回滚后检查/etc/sysconfig/network-scripts/ifcfg-ens33中的HWADDR是否与实际MAC一致,不一致就删掉这行或者更新它。
第四个坑:kubeadm reset不彻底。如果你想重新初始化集群,执行kubeadm reset后还需要手动清理一些残留文件:
kubeadm reset -f rm -rf $HOME/.kube rm -rf /etc/cni/net.d rm -rf /var/lib/cni iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X不清理干净的话,重新初始化可能会报端口被占用或配置文件冲突。
6.4 集群日常运维命令
集群跑起来之后,日常运维离不开这些命令:
# 查看所有节点状态 kubectl get nodes -o wide # 查看所有命名空间的Pod kubectl get pods --all-namespaces -o wide # 查看某个Pod的详细信息 kubectl describe pod <pod-name> -n <namespace> # 查看Pod日志 kubectl logs <pod-name> -n <namespace> # 进入Pod内部 kubectl exec -it <pod-name> -n <namespace> -- /bin/bash # 查看集群事件 kubectl get events --sort-by='.lastTimestamp' # 查看资源使用情况 kubectl top nodes kubectl top pods --all-namespaceskubectl top命令需要部署Metrics Server才能使用。Metrics Server的部署不在本文范围内,但如果你需要监控集群资源,建议部署一个。
7. 环境固化与后续扩展
7.1 制作虚拟机模板
三台虚拟机都配置好之后,建议把Master节点做成模板。具体做法是:先清理掉集群相关的配置(执行kubeadm reset),然后关机,在VMware中右键虚拟机选择“模板”->“转换为模板”。以后需要新建节点时,直接从模板克隆,改一下IP和主机名就能用,省去了重复安装系统的时间。
克隆时选择“创建完整克隆”而不是“创建链接克隆”。链接克隆虽然省磁盘空间,但多个虚拟机共享基础磁盘,性能会受影响,而且一个虚拟机的磁盘操作可能影响其他虚拟机。
7.2 后续可以扩展的方向
这套基础环境搭好之后,可以往几个方向扩展。一是部署Dashboard,用Web界面管理集群,对新手更友好。二是部署Harbor私有镜像仓库,把常用镜像存在本地,拉取速度更快。三是部署Prometheus和Grafana做监控,观察集群和容器的资源使用情况。四是尝试部署有状态应用,比如MySQL主从集群,理解StatefulSet和持久化存储的用法。
每个方向都可以写一篇独立的文章,但前提是你先把基础环境跑通。基础不牢,地动山摇,这句话在容器技术领域尤其适用。
7.3 资源清理与快照管理
虚拟机快照不是越多越好。快照文件会占用大量磁盘空间,而且快照链太长会影响虚拟机性能。我的做法是:只在关键节点拍快照,比如系统装好之后、Docker装好之后、K8s集群跑通之后。每个快照命名清晰,比如“01-系统初始化完成”、“02-Docker部署完成”、“03-K8s集群就绪”。不需要的快照及时删除,释放磁盘空间。
另外,虚拟机的内存快照要慎用。内存快照会把虚拟机的内存状态也保存下来,文件很大,而且恢复后可能出现时钟漂移等问题。除非你要保存某个瞬间的运行状态,否则用普通快照就够了。
7.4 关于学习路径的建议
最后说点个人体会。这套环境搭起来之后,不要急着往上堆应用。先把K8s的核心概念搞清楚:Pod、Service、Deployment、ReplicaSet、ConfigMap、Secret、Volume、Namespace,这些概念之间的关系是什么,各自解决什么问题。然后动手写YAML文件,从最简单的Pod开始,逐步增加复杂度。遇到报错不要慌,kubectl describe和kubectl logs能解决百分之八十的问题。
我见过不少人环境搭好了,然后不知道干什么,最后虚拟机吃灰。学习容器技术最好的方式就是带着问题去实践。比如你想部署一个博客系统,那就试着用K8s来编排它,遇到问题解决问题,这个过程比看十篇文章都管用。
这套基于虚拟机的K8s实验环境,我前后搭了不下五次,每次都有新的收获。第一次搭的时候花了整整两天,各种报错各种查资料;第五次搭的时候两个小时搞定,因为所有的坑都踩过了。技术这东西就是这样,纸上得来终觉浅,绝知此事要躬行。希望这篇内容能帮你少走一些弯路,把更多时间花在真正理解技术上,而不是跟环境较劲。