Hyper-V+Kubernetes 搭建实战:保留 Windows,构建三节点 Linux 容器集群
Java 微服务上云之前,团队往往已经有一台 Windows 服务器:源码、IDE、远程维护和既有工具都在上面。为了搭建容器集群,是否必须先重装 Linux?
在研发与交付验证环境中,可以保留 Windows,用 Hyper-V 运行 Linux 节点,再在这些节点上部署 Kubernetes。这样既能练习多节点服务编排,也能沿用现有维护方式。
真正需要做好的,是把虚拟化、节点身份、运行时、集群初始化和网络验收连成一条可追溯的安装链。本文还原 RuoYi Office 实验室的实际路径,并把"安装步骤"和"已经验收的结果"分开说明。

▲ 先验收一台 VM,再扩展为三节点。每跨一层都检查一次,避免把全部问题留给最后一个 NotReady。
一、从最终要部署的微服务,倒推集群边界
实验室使用四台 Linux VM:rolab-cp01 是控制面,rolab-w01/w02 是两个 worker,rolab-infra01 提供中间件。只有前三台是 Kubernetes 节点。
业务后续通过容器运行网关、系统服务、流程、OA 等模块;Nacos、数据库和 Redis 等基础服务通过 VM 网络接入。Jenkins 的临时 Agent 也在 worker 中运行。因此,节点既要能拉镜像,也要能跨节点通信,还要能访问集群外的中间件。
本文只解决从 Windows 到可用集群的底座链路。微服务清单、Feign 调用、网关路由与 CI/CD 完整编排留给后续专题;资源预算可结合系列容量规划篇阅读。
版本按本案例锁定记录列出:Ubuntu Server 24.04.5 LTS、Kubernetes 1.36.5、containerd 2.2.1、Calico 3.33.0。2026-10-31 的节点 API 与命令采样重新核对了 OS、kubelet 和运行时。版本表表示本次环境,不能替代各组件官方兼容矩阵,也不要求读者照抄旧锁定值。
KubeSphere 4.1.3 是后装管理界面;现有核验将其与本案例 Kubernetes 组合列为实验性,不能把界面可访问解读为官方生产支持。它不是 kubeadm 初始化的前置条件。

▲ 三台虚拟节点共享一台物理宿主机。本文验证多节点部署能力,不宣称物理高可用。
二、先打通一台 Linux VM,暂时不要批量创建
本案例采用 Hyper-V 内部交换机 + WinNAT ,不是把 VM 直接接入外部交换机。节点网段是 172.28.100.0/24,宿主内部接口为 172.28.100.1。
地址分工为:控制面 .11,两个 worker .21/.22,中间件 .31。另有 Pod 网段 10.244.0.0/16、Service 网段 10.96.0.0/12。
三类网段表达三种对象:节点 VM、Pod 与虚拟 Service。规划时同时检查公司局域网、VPN、宿主已有虚拟网络与路由,不能只确认这几个地址互相不同。尤其是 VPN 常见私网段,部署当天不冲突,不代表出差连 VPN 后仍不冲突。
创建脚本对候选路由做重叠检查,并在发现其他 WinNAT 时拒绝自动重配。已有网络不是可随意清理的安装残留;应先确认它被哪些工具或工作负载使用。
先解决镜像与磁盘格式,再启动 VM
实验室使用校验 SHA256 的 Ubuntu cloud image,转换成动态 VHDX。这里有两个容易混淆的概念:
- 动态 VHDX:虚拟磁盘格式的空间增长方式。
- NTFS sparse:宿主文件的稀疏属性。
实际创建脚本在转换后清除 NTFS sparse 标志,再配置磁盘上限。不能只看文件扩展名是 .vhdx 就认为宿主属性和虚拟机启动条件已经满足。
Hyper-V 使用第二代 VM、固定内存和静态 MAC;固件配置使用 MicrosoftUEFICertificateAuthority 模板。这里描述的是仓库已实施设置,不是建议遇到启动问题就直接关闭所有启动保护。
第一台 VM 先验收镜像完整性、cloud-init、固定地址、SSH、默认路由、DNS和出口,成功后再创建其余节点。脚本要求存在首台 VM 的 SSH 验收记录,才允许进入剩余 VM 阶段。
这样能减少失败面:若 seed ISO 命名或网卡匹配错了,先修一台;不必面对四台都无法登录、又无法确定是镜像还是网络的问题。
三、把 MAC、cloud-init 与 CNI 绑定到同一条身份链
克隆 VM 时最容易遗漏的不是主机名,而是几个"看起来各自正确"的身份值没有对应起来。
本案例为每台 VM 设置静态 MAC,cloud-init 根据该 MAC 匹配网卡并重命名为 lab0,随后 Calico 也按 lab0 选择节点地址。

▲ MAC 各机不同,接口名保持统一;后续选卡依据稳定接口,不依赖默认网卡名恰好一致。
控制面的网络片段可整理为以下 YAML。它对应实际 seed 生成逻辑,MAC 按 .11 节点的静态值表示;其他节点必须换成自己的值:
yaml
version: 2
ethernets:
lab0:
match:
macaddress: "00:15:5d:64:00:0b"
set-name: lab0
dhcp4: false
addresses: [172.28.100.11/24]
routes:
- to: default
via: 172.28.100.1
nameservers:
addresses: [192.168.31.1, 223.5.5.5]
来源:host/New-LabSeed.py,从其 JSON 等价转换为 YAML。DNS 地址属于本地网络条件,不能照搬到不同公司网络。
同时保证各机 hostname、instance-id 与 IP 唯一。生成器以 VM 名构造 instance-id,并拒绝覆盖既有 seed ISO。复制已经启动过的磁盘时,还要核对 cloud-init 是否会把它识别为同一实例;不能仅修改 ISO 文件名就认为初始化一定重跑。
登录采用 SSH 公钥,关闭密码登录和 root 直接登录。实验室 labops 具有便于自动化的 sudo 能力,因此私钥与该账号都属于高权限入口;不应把这套便利配置无条件复制到客户生产环境。
第一台节点可以先执行:
bash
hostname
cloud-init status --wait
ip -4 address show lab0
ip route show default
getent hosts pkgs.k8s.io
timedatectl status
hostname 不符,先查 seed;lab0 不存在,先查静态 MAC 与 match;默认路由不符,先查网络配置。只有外网域名不通时,再分辨 DNS、NAT、代理与远端限制。不要在网络层还没查清时反复重装 Kubernetes。
四、先把节点运行时配置收敛,再让它加入集群
实际 bootstrap 在安装前检查 /etc/kubernetes/kubelet.conf。如果节点已经加入集群,就停止并要求检查,避免覆盖既有状态。
节点准备包括关闭本案例的 swap、加载网络所需模块、开启 IPv4 转发与桥接过滤、配置时间同步,然后安装固定版本的运行时和 Kubernetes 工具。
bash
# 本案例节点准备片段,属于安装操作;本文没有重新执行。
modprobe overlay
modprobe br_netfilter
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.bridge.bridge-nf-call-iptables=1
sysctl -w net.bridge.bridge-nf-call-ip6tables=1
来源:guest/bootstrap-k8s-node.sh。实际脚本同时写入 modules-load 与 sysctl 配置,避免重启后只剩一次性的内核参数。此摘录不能替代完整脚本。
时间同步也应作为进入条件。首台 VM 的早期记录曾显示 NTPSynchronized=no,后续 bootstrap 安装 chrony,控制面初始化检查 Leap status: Normal。这些是不同阶段的证据,不能把"SSH 能登录"当成时间已经正确。
containerd 配置要对应正在运行的版本
本案例通过该版本的 containerd config default 生成配置,然后将 SystemdCgroup 设为 true,重启运行时并核对生效值。
不要把其他版本的完整 config.toml 直接复制过来:配置 schema 与插件路径可能变化。只搜索到一行参数也不够,还要确认它位于实际生效的插件段,服务日志没有配置错误。
当前控制面只读采样为 containerd 2.2.1,选定配置中的 SystemdCgroup = true,节点 API 报告 containerd://2.2.1。两种证据分别说明磁盘配置和节点上报运行时,结合起来比单看安装包版本更有用。

▲ 管理页展示实际节点身份与运行版本;运行时配置仍通过节点只读命令交叉核对。
包版本在仓库脚本中固定,随后执行 hold:
bash
apt-get install -y \
'kubelet=1.36.5-1.1' \
'kubeadm=1.36.5-1.1' \
'kubectl=1.36.5-1.1'
apt-mark hold kubelet kubeadm kubectl containerd
来源:本案例 bootstrap。安装前还需配置对应仓库与签名;此处只摘录锁版本部分,不能独立运行。
锁版本是为了让同一批节点有一致输入,不是永远不更新。升级时应重新核验包可用性、组件支持范围和升级顺序,再更新锁定与验收记录。
容器能否出网,要在正确的进程上验证
交互式 shell 能 curl,不代表 containerd 能拉镜像。APT、curl、containerd、构建容器各有自己的代理来源,需要分别核对。
当前 containerd 使用 systemd drop-in 配置 HTTP/HTTPS 代理,同时排除回环、节点、Pod、Service 与集群内部域名:
ini
[Service]
Environment="HTTP_PROXY=http://172.28.100.1:17890"
Environment="HTTPS_PROXY=http://172.28.100.1:17890"
Environment="NO_PROXY=localhost,127.0.0.1,::1,172.28.100.0/24,192.168.31.0/24,10.244.0.0/16,10.96.0.0/12,.svc,.cluster.local"
来源:guest/configure-containerd-proxy.sh,私网代理只表示本案例配置。不同客户端对 CIDR 与域名后缀的解释需分别验证,不能只检查环境变量字符串相同。
修改 drop-in 还涉及 daemon-reload 与服务重启,应放在受控安装/维护阶段。写博客取证不需要为展示效果重启运行时。
五、初始化控制面时,让地址与网段成为显式输入
控制面脚本检查当前主机必须是 rolab-cp01,且不存在已有 admin.conf,之后才允许初始化。它使用固定控制面地址与明确的两个集群网段:
bash
# 仅用于符合前置条件的新控制面,已初始化节点不要重跑。
kubeadm init \
--kubernetes-version v1.36.5 \
--apiserver-advertise-address 172.28.100.11 \
--control-plane-endpoint 172.28.100.11:6443 \
--node-name rolab-cp01 \
--pod-network-cidr 10.244.0.0/16 \
--service-cidr 10.96.0.0/12 \
--cri-socket unix:///run/containerd/containerd.sock \
--skip-token-print
来源:guest/init-control-plane.sh;实际脚本将输出保存到受控日志,配置 kubeconfig 访问权限。本文只是还原命令,未初始化任何节点。
control-plane-endpoint 在这里仍是单台 VM 地址。参数名字包含 endpoint,不代表前面已有负载均衡器,也不会自动把单控制面变成 HA。
worker 加入使用短期 join token。现有编排先验证 join 命令形态,令牌只在内存流转,日志脱敏,并在结束后撤销;若撤销失败,还有短期 TTL 边界。
加入成功后应核对节点名、InternalIP、运行时与角色。节点已经有 kubelet.conf 时,脚本保留现场;排障不能默认用 reset、删目录、重建 VM 抹去证据。
六、Calico 与 CoreDNS 就绪,才能开始验证跨节点调用
本案例 Calico 使用 VXLAN,关闭 BGP,自动选卡指定为 lab0,IP 池与 kubeadm 的 Pod CIDR 保持一致:
yaml
spec:
variant: Calico
calicoNetwork:
bgp: Disabled
nodeAddressAutodetectionV4:
interface: lab0
ipPools:
- cidr: 10.244.0.0/16
encapsulation: VXLAN
natOutgoing: Enabled
nodeSelector: all()
来源:k8s/calico-installation.yaml 的 spec。完整对象还包含 apiVersion、kind 与 metadata;应先准备匹配版本的 Operator/CRD,不把这一段当成独立可安装文件。
实际锁定记录保留 Calico 3.33.0 及 Operator/CRD 清单哈希。新环境应先取得与版本匹配的安装文件并校验,再应用完整 Installation;不要将文章中的锁定值配上不明日期的 latest 清单。

▲ 检查预期、当前、就绪实例及其节点分布。CNI Pod 正常是网络验收前提,仍需验证实际数据路径。

▲ CNI Pod 详情补足运行实例证据,不以一个"安装成功"提示代替各节点检查。
本轮 Calico DaemonSet 为3个预期实例、3个就绪实例。它使用 hostNetwork,详情中的 Pod IP 与所在节点地址相同;这不能推广为普通业务 Pod 的地址规则,也不能用 CNI Pod 的宿主网络通信代替普通 Pod 跨节点验收。
随后检查 CoreDNS Deployment 和对应 Pod:
bash
kubectl get nodes -o wide
kubectl -n calico-system get daemonset calico-node
kubectl -n calico-system get pods -o wide
kubectl -n kube-system rollout status deployment/coredns --timeout=60s
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
rollout status 在这里是读取状态,不触发 rollout。DNS Pod Ready 仍不等于每个业务 Pod 都能解析,也不等于跨节点 Service 调用已经正常。

▲ DNS 是从 Service 名访问网关与业务服务的基础能力,需与跨节点请求一同验证。
本轮两个 CoreDNS Pod 均位于控制面节点,页面显示 2/2 只能说明两个实例就绪,不能据此声称 DNS 已跨节点容灾。若后续要提高节点故障下的可用性,还应检查调度约束与实际分布,并设计恢复验证。
七、把安装验收拆成四级,避免 Ready 以后就交付
第一级:节点与平台组件
检查三节点 Ready、CNI 与 CoreDNS 就绪、节点地址和版本一致。这个级别通过,才适合继续部署测试工作负载。
第二级:跨节点 Pod HTTP
现有测试清单将 http-w01 固定在 worker01,将探测端固定在 worker02,访问前者的 Pod IP。它刻意避免同节点调用"恰好成功"掩盖跨节点问题。
测试 Pod 使用明确的 nodeName,适合受控网络验证。业务应用不应照搬这种固定绑定方式,否则调度弹性与迁移行为会改变。
第三级:Service 与 DNS
再从探测端解析 kubernetes.default.svc.cluster.local,并访问 http-smoke.serverlab-network-smoke.svc.cluster.local。如果直连 Pod 成功、Service 失败,排查范围与"两个节点完全不通"不同,应继续看 Service selector、EndpointSlice、DNS 与转发路径。
历史测试的核心读取动作是:
bash
# 前置:对应测试namespace、Pod、Service已经创建且Ready。
target=$(kubectl -n serverlab-network-smoke get pod http-w01 \
-o jsonpath='{.status.podIP}')
kubectl -n serverlab-network-smoke exec probe-w02 -- \
nslookup kubernetes.default.svc.cluster.local
kubectl -n serverlab-network-smoke exec probe-w02 -- \
wget -T 10 -q -O /dev/null "http://$target"
kubectl -n serverlab-network-smoke exec probe-w02 -- \
wget -T 10 -q -O /dev/null \
http://http-smoke.serverlab-network-smoke.svc.cluster.local
来源:guest/verify-cluster-network.sh。完整脚本会先 apply 测试清单,因此不是纯只读脚本;本文没有为写作重新运行它。
2026-10-28 初次记录保留 Cross-worker Pod HTTP: PASS、ClusterIP + CoreDNS HTTP: PASS;worker 重启后的记录也包含这两项。2026-10-31 本轮只读检查时,原测试 Pod 已不存在,Service 查询返回 NotFound。这个现状不能被写成"本轮再次通过",也不必为了截图擅自重建测试环境。
要重新验收,应在得到对应环境测试授权后重建受控测试资源,确认镜像可拉取、Pod 固定在不同节点,然后记录新一轮结果。没有测试资源时,命令报 NotFound 是前置条件缺失,不直接说明 CNI 故障。
第四级:节点恢复与恢复后的网络
历史 worker 重启证据先记录 boot_id,重启后确认新 boot_id 与原值不同且 SSH 恢复,再检查节点与网络。这样能区分"真的重启后恢复"和"只是重试 SSH 成功"。
SSH 恢复、节点 Ready、跨节点调用恢复是三个结果,不能合并成一个绿勾。更不能把单 worker 恢复推广为 Windows 整机重启、宿主代理、全部 VM 与业务系统都能无人值守恢复。

▲ 监控辅助观察运行阶段;本图是当前空闲时段证据,不是历史重启过程截图,也不能证明恢复过程没有业务中断。
八、为后续微服务部署准备一份可检查的交接单
底座交接至少留下以下内容:
- 节点清单:VM、hostname、MAC、IP、网卡名和 kubelet/runtime 版本。
- 地址清单:节点、Pod、Service、DNS、中间件与镜像仓库的范围和访问方向。
- 安装输入:镜像校验、包版本、CNI 文件与配置哈希,禁止混用不明 latest。
- 凭据边界:SSH 私钥、kubeconfig、临时 join token 的存放与权限。
- 验收结果:节点、跨节点 Pod、Service/DNS、重启后网络,分别保存时间与证据。
- 未验事项:物理 HA、整机恢复、业务并发和完整审批链,不用平台截图替代。
有了这份交接单,再部署 Nacos 注册、Gateway 转发、Feign 调用和业务微服务,才能知道故障先落在哪一层。基础网络没验过时,一次调用超时可能同时牵扯 CNI、代理、服务发现和业务配置,定位成本会明显增加。
对于只想尽快运行一个业务应用的团队,三节点实验室也有维护成本。可以先用更简单的容器部署验证业务,再根据独立发布、弹性和平台管理需求决定是否建设集群;当需要真实生产 HA 时,应规划独立物理故障域。
常见问题(FAQ)
Windows 上运行 Kubernetes,是否需要把业务容器改成 Windows 容器?
不需要。本案例 Linux VM 内运行 Linux 容器,Windows 承担宿主与运维工具角色。容器操作系统应与其运行节点匹配。
Hyper-V 内部交换机与外部交换机应该怎么选?
根据地址管理、局域网接入、宿主网络和出口要求选择。本文实际使用内部交换机与 WinNAT;外部交换机需要重新核对物理网卡、局域网策略及地址分配,不能直接沿用本文路由结论。
三节点都 Ready,为什么还要测跨节点 HTTP?
Ready 是节点与基础组件状态,不能覆盖每条数据路径。将客户端与服务端放在不同 worker,才能直接验证该次跨节点通信。
已经初始化失败,可以直接重复运行全部安装脚本吗?
应先检查已有状态和失败阶段。案例脚本在已有 kubelet.conf 或 admin.conf 时拒绝覆盖;保留日志,确定问题属于网络、运行时、证书或初始化,再制定恢复步骤。
worker 重启恢复通过,是否代表这套环境满足高可用?
不能。它验证单节点恢复的一部分行为。同宿主多 VM 仍共享物理故障域,整机恢复和业务可用性需要独立设计与演练。
把"集群能运行"变成一条可以解释的证据链
这个实验室最有价值的做法,是每一层都保留输入和进入条件:镜像校验后创建首台 VM,身份与网络一致后安装运行时,初始化后验收跨节点路径,节点重启后再检查恢复。
MAC、cloud-init 与 CNI 的身份链减少选卡歧义;拒绝重复初始化保护已有现场;分级验收让 SSH、Ready、网络与业务可用性各有明确边界。后续微服务部署在这样的底座上,才有可复用的排查起点。
