香港服务器如何通过 Kubernetes 统一管理多操作系统节点,解决跨平台调度难题?

夜里 1:40,香港荃湾机房的冷风从地板孔吹上来,脚底发凉,同事Zabbix 刚推了个告警:新上架的 Windows 节点没有按期接管 .NET 任务,Linux 节点上排队长度飙到 312。屏幕上是被拒的 Pod 事件:node(s) didn't match node selector。
我抬头看了眼机柜 18U 的蓝色标签,心里骂了一句:跨操作系统调度,还是得用体系化的办法来治,不是补补 YAML 就能好。于是,我把咖啡放到 PDU 上,开始把这件事一次性做对——用 Kubernetes 把 Linux(CentOS 7 / Ubuntu 22.04) 和 Windows Server 2019/2022 纳入同一个集群,做“跨平台有序调度”。
目标与边界
目标:在同一 K8s 集群中,统一纳管 Linux 与 Windows 节点,保证跨平台工作负载(.NET、Java、Go、前端 Node 等)按策略稳定调度,网络、存储、监控、CI/CD 统一。
边界:
- 使用 kubeadm 初始化控制面。
- CNI 选用 Calico(VXLAN 模式 + Windows 支持)。
- CRI 使用 containerd(Linux/Windows 统一)。
- 存储:Linux 侧 Longhorn(或 Ceph RBD),Windows 侧 SMB CSI,对业务暴露统一的 StorageClass 名称。
- 机房:香港本地两柜(HK-1/HK-2)25GbE 互联,跨大陆链路 10GbE(高时延,受限 MTU)。
现场硬件与系统版本(真实可落地)
物理与操作系统清单
| 角色 | 机柜 | 机型 | CPU | 内存 | 系统盘 | 数据盘 | 网卡 | 操作系统 | 用途 |
|---|---|---|---|---|---|---|---|---|---|
| 控制面-1 | HK-1 | Dell R650xs | Xeon Gold 6330 28C | 256G | 2×480G SATA SSD (RAID1) | 2×1.92T NVMe | 2×25GbE (Mellanox CX4-Lx) | Ubuntu 22.04.4 LTS | 控制面/etcd |
| 控制面-2 | HK-2 | Dell R650xs | Xeon Gold 6330 28C | 256G | 同上 | 同上 | 同上 | Ubuntu 22.04.4 LTS | 控制面/etcd |
| 控制面-3 | HK-1 | Supermicro 1029U | Xeon Gold 5218 16C | 192G | 同上 | 同上 | Intel X710-DA2 2×10GbE | Ubuntu 22.04.4 LTS | 控制面/etcd |
| 计算-Linux-A | HK-1 | Dell R650 | Xeon Gold 6330 28C | 256G | 480G | 2×3.84T NVMe | 2×25GbE | CentOS 7.9 | Java/Go/Node |
| 计算-Linux-B | HK-2 | HPE DL360 G10 | Xeon Gold 6248R 24C | 256G | 480G | 2×3.84T NVMe | 2×25GbE | CentOS 7.9 | 批处理/数据 |
| 计算-Win-A | HK-1 | HPE DL360 G10 | Xeon Gold 6248R 24C | 256G | 480G | 2×1.92T NVMe | 2×25GbE | Windows Server 2019 Datacenter | .NET 旧栈 |
| 计算-Win-B | HK-2 | HPE DL360 G10 | Xeon Gold 6248R 24C | 256G | 480G | 2×1.92T NVMe | 2×25GbE | Windows Server 2022 Datacenter | .NET 6/8 |
网络核心:ToR 交换机 25GbE,上联聚合 100GbE;跨柜延迟 0.120.18ms;跨大陆专线 3555ms。VLAN 隔离业务/管理/存储 3 个平面。
网络与版本矩阵
Kubernetes / CNI / CRI 兼容表(实测)
| 组件 | 版本 | Linux | Windows |
|---|---|---|---|
| Kubernetes | v1.28.7(示例) | ✔️ | ✔️(Win 2019/2022 受支持) |
| Calico | v3.26+ | ✔️(VXLAN, BGP) | ✔️(Windows 代理,VXLAN) |
| containerd | 1.7.x | ✔️ | ✔️ |
| kube-proxy | 同 K8s | ipvs | winkernel |
| CoreDNS | 1.10+ | ✔️ | ✔️ |
选择 VXLAN:香港多运营商混合网络,三层路由可控性差,VXLAN 封装更稳。
MTU:实测跨运营商链路有效 MTU 1472,VXLAN 叠加后建议 CalicoMTU=1450。
规划参数(强烈建议先落纸)
ClusterName: hk-prod
PodCIDR: 10.244.0.0/16
ServiceCIDR: 10.96.0.0/12
DNSDomain: cluster.local
CalicoEncap: VXLAN, MTU=1450
Zones: hk-1, hk-2
Node Labels:
kubernetes.io/os: linux | windows
topology.kubernetes.io/zone: hk-1 | hk-2
node.os.distro: centos7 | ubuntu22 | win2019 | win2022
Taints (Windows):
node-role.kubernetes.io/windows:NoSchedule
部署步骤(一步步复刻)
1)控制面初始化(Ubuntu 22.04)
containerd 配置(systemd cgroup)
sudo apt-get update && sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl enable --now containerd
kubeadm 配置文件
# kubeadm-controlplane.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.28.7
clusterName: hk-prod
networking:
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
dnsDomain: cluster.local
controllerManager:
extraArgs:
node-cidr-mask-size: "24"
apiServer:
extraArgs:
service-node-port-range: "30000-32767"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
cgroup-driver: "systemd"
sudo kubeadm init --config kubeadm-controlplane.yaml
mkdir -p ~/.kube && sudo cp -i /etc/kubernetes/admin.conf ~/.kube/config && sudo chown $(id -u):$(id -g) ~/.kube/config
2)安装 Calico(Linux 先行)
先部署 Linux 侧 Calico,再加 Windows 组件。
# 下载官方 manifest 后,按需改 MTU=1450 与 VXLAN
kubectl apply -f calico.yaml
kubectl -n kube-system set env ds/calico-node FELIX_VXLANMTU=1450
kubectl -n kube-system set env ds/calico-node FELIX_VXLANENABLED=true
为 Windows 加载 Calico 组件(简化示例)
kubectl apply -f https://docs.projectcalico.org/manifests/calico-windows.yaml
# 或将 windows-only 组件拆分成单独清单,便于灰度
3)加入 Linux 工作节点(CentOS 7)
内核与网络准备
swapoff -a && sed -i '/ swap / s/^/#/' /etc/fstab
yum install -y containerd iptables-services
systemctl enable --now iptables
# containerd
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
systemctl enable --now containerd
# 关闭 firewalld或切换为 iptables-legacy(CentOS7 默认 iptables)
modprobe br_netfilter
cat <<EOF >/etc/sysctl.d/99-k8s.conf
net.bridge.bridge-nf-call-iptables=1
net.ipv4.ip_forward=1
EOF
sysctl --system
kubelet/kubeadm 安装并加入
kubeadm join <API_SERVER_IP>:6443 --token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--node-name linux-a \
--cri-socket /run/containerd/containerd.sock
4)加入 Windows 工作节点(2019/2022)
在 Windows PowerShell(提升权限):
# 开启容器功能
Enable-WindowsOptionalFeature -Online -FeatureName containers -All
# 可选:启用 Hyper-V 以支持 Hyper-V 隔离
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
# 安装 containerd
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest https://github.com/containerd/containerd/releases/download/v1.7.18/containerd-1.7.18-windows-amd64.tar.gz -OutFile c:\containerd.tar.gz
mkdir C:\containerd; tar -xzf c:\containerd.tar.gz -C C:\containerd
c:\containerd\containerd.exe --register-service
Start-Service containerd
# CNI (Calico Windows 会包含 CNI,可统一到 C:\cni\bin)
mkdir C:\cni\bin
# kubelet/kubeadm/kubectl
choco install -y kubernetes-cli --version=1.28.7
choco install -y kubernetes-kubelet --version=1.28.7
choco install -y kubernetes-kubeadm --version=1.28.7
# HNS 清理(如反复测试)
Get-HnsNetwork | Remove-HnsNetwork -Force
# kubeadm join(容器运行时指定)
kubeadm.exe join <API_SERVER_IP>:6443 --token <token> `
--discovery-token-ca-cert-hash sha256:<hash> `
--node-name win-a `
--cri-socket npipe:////./pipe/containerd-containerd
注意:Windows 节点加入后会自动打上 kubernetes.io/os=windows。建议我们再补充自定义标签与污点,确保非 Windows 工作负载不会误落:
kubectl label nodes win-a topology.kubernetes.io/zone=hk-1 node.os.distro=win2019
kubectl taint nodes win-a node-role.kubernetes.io/windows=true:NoSchedule
存储统一:Linux/Windows 两套实现,一个名字暴露
方案对比与选择
| 诉求 | Linux | Windows |
|---|---|---|
| 有状态数据库/队列 | Longhorn / Ceph RBD(RWO) | 不建议(缺驱动/成熟度),改为跨区主从或容器外托管 |
| 共享文件(RWX) | NFS/Longhorn RWX | SMB CSI |
| 容器临时盘 | emptyDir + local NVMe | same |
StorageClass 与 PVC(统一名称 shared-files)
Windows:SMB CSI
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: shared-files
provisioner: smb.csi.k8s.io
parameters:
source: "\\\\10.10.10.20\\k8s-share"
csi.storage.k8s.io/node-publish-secret-name: smb-cred
csi.storage.k8s.io/node-publish-secret-namespace: kube-system
reclaimPolicy: Retain
mountOptions:
- dir_mode=0777
- file_mode=0777
- vers=3.0
Linux:NFS(与上面同名备用,按命名空间挑选)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: shared-files
provisioner: nfs.csi.k8s.io
parameters:
server: 10.10.10.21
share: /export/k8s-share
reclaimPolicy: Retain
mountOptions:
- nfsvers=4.1
实践里我们按命名空间拆分:win-* 命名空间默认指向 SMB 的 shared-files;linux-* 默认指向 NFS 的 shared-files。名字统一,使用方无感。
跨平台调度:标签、污点、亲和性一次到位
统一标签/污点初始化脚本(可直接跑)
#!/usr/bin/env bash
set -euo pipefail
for n in $(kubectl get nodes -o name | sed 's#node/##'); do
os=$(kubectl get node "$n" -o jsonpath='{.status.nodeInfo.operatingSystem}')
zone=$(kubectl get node "$n" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
[[ -z "$zone" ]] && kubectl label node "$n" topology.kubernetes.io/zone=hk-1 --overwrite
if [[ "$os" == "windows" ]]; then
kubectl taint node "$n" node-role.kubernetes.io/windows=true:NoSchedule --overwrite
else
kubectl label node "$n" node.os.distro=centos7 --overwrite || true
fi
done
跨平台 Deployment 示例(同一业务,两套容器)
关键点:
1)使用 topologySpreadConstraints 保证跨柜均衡。
2)用 nodeAffinity / nodeSelector 指定 OS。
3)Windows 节点有污点,工作负载需显式容忍。
.NET on Windows
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-win
namespace: win-prod
spec:
replicas: 4
selector:
matchLabels: app: api-win
template:
metadata:
labels:
app: api-win
spec:
tolerations:
- key: "node-role.kubernetes.io/windows"
operator: "Exists"
effect: "NoSchedule"
nodeSelector:
kubernetes.io/os: windows
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["hk-1","hk-2"]
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-win
containers:
- name: api
image: mcr.microsoft.com/dotnet/aspnet:6.0
ports:
- containerPort: 8080
volumeMounts:
- name: shared
mountPath: C:\data
volumes:
- name: shared
persistentVolumeClaim:
claimName: shared-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-pvc
namespace: win-prod
spec:
accessModes: ["ReadWriteMany"]
storageClassName: shared-files
resources:
requests:
storage: 100Gi
Java on Linux
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-linux
namespace: linux-prod
spec:
replicas: 6
selector:
matchLabels: app: api-linux
template:
metadata:
labels:
app: api-linux
spec:
nodeSelector:
kubernetes.io/os: linux
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node.os.distro
operator: In
values: ["centos7","ubuntu22"]
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-linux
containers:
- name: api
image: eclipse-temurin:17-jre
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "2Gi"
日志与监控:一套栈,两端采集
指标:Prometheus Operator +
- Linux:node-exporter(DaemonSet, kubernetes.io/os=linux)
- Windows:windows-exporter(DaemonSet, kubernetes.io/os=windows,HostProcess 模式)
日志:Fluent Bit
- Linux:tail /var/log/containers/*.log
- Windows:读取 C:\var\log\containers\*.log 或 ETW,经 Sidecar 转发
Windows HostProcess DaemonSet 样例
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: windows-exporter
namespace: monitoring
spec:
selector:
matchLabels: app: windows-exporter
template:
metadata:
labels:
app: windows-exporter
spec:
nodeSelector:
kubernetes.io/os: windows
hostNetwork: true
containers:
- name: exporter
image: ghcr.io/prometheus-community/windows-exporter:0.25.1
args: ["--telemetry.addr=:9182"]
securityContext:
windowsOptions:
hostProcess: true
runAsUserName: "NT AUTHORITY\\SYSTEM"
典型坑与当场修复记录(机房里真遇到的)
CentOS 7 cgroup 驱动不匹配
现象:cgroup driver mismatch: cgroupfs vs systemd,kubelet 报错。
解决:统一 containerd SystemdCgroup=true,kubeletExtraArgs: cgroup-driver=systemd,重启后恢复。
VXLAN MTU 错配导致跨柜丢包
现象:跨柜流量偶发丢包,抓包显示 frag needed。
解决:将 FELIX_VXLANMTU=1450,并在 Pod 启动参数里避免强制大包(如 gRPC max_receive_message_length 合理设置)。
Windows kube-proxy 端口冲突(NodePort vs Ephemeral)
现象:暴露 NodePort 服务后,Periodically 无法访问。
诊断:netsh int ipv4 show dynamicport tcp 显示动态端口范围覆盖到 30000+。
解决:收紧动态端口,或调整 NodePort 范围。
netsh int ipv4 set dynamicport tcp start=10000 num=20000
netsh int ipv4 set dynamicport udp start=10000 num=20000
同时确保 service-node-port-range: "30000-32767"。
Windows DNS 解析异常
现象:Windows Pod 偶发 Name does not exist。
根因:HNS 网络创建过早,DNS Suffix 未继承。
解决:在 Calico Windows 清单中显式设置 KUBE_DNS_NAME= kube-dns.kube-system.svc.cluster.local,并确保 kubelet --cluster-dns 与 --cluster-domain 一致。
SMB 凭据轮换
现象:月度轮换后,Windows 容器挂载失败。
解决:凭据以 Secret 挂载,改为 短期 Secret + Deployment 滚动,并对持久挂载的 StatefulSet 设置 podManagementPolicy: Parallel,缩短恢复时间。
跨大陆链路时延引起的镜像拉取超时
解决:在香港落地 Harbor 镜像仓库,所有节点优先从本地仓拉取;给 containerd 配置镜像加速与 Registry 镜像。
Windows HostProcess 权限
现象:Windows DaemonSet 无法触达系统指标。
解决:必须 windowsOptions.hostProcess=true 且 runAsUserName=NT AUTHORITY\SYSTEM,并打开 hostNetwork。
压测与观测(上线前我们做了什么)
| 项目 | 变更前 | 变更后 | 备注 |
|---|---|---|---|
| 跨平台交付时长(从 Issue 到上线) | 3~5 天 | 1 天内 | CI/CD + 模板化部署 |
| 调度拒绝率(因 OS 不匹配) | 2.3% | 0.1% | 统一标签+污点 |
| 跨柜流量丢包(p99) | 0.05% | <0.005% | MTU 调优 |
| .NET API P99 延迟 | 210ms | 168ms | 本地镜像仓+Node 亲和 |
| 故障恢复(Windows 节点重启) | 20~30 分钟 | 8~12 分钟 | DaemonSet/Sidecar 优化 |
CI/CD 与配置治理
模板化:用 Helm/Kustomize 两套 overlay:overlay/linux 与 overlay/windows。
准入控制:可用 OPA Gatekeeper 编写策略,禁止未声明 kubernetes.io/os 的工作负载进入生产命名空间;Windows 命名空间强制容忍污点。
命名空间隔离:win-*/linux-* 前缀策略 + 资源配额 + LimitRange。
示例 Gatekeeper 约束(禁止未声明 OS 的 Pod):
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredNodeSelector
metadata:
name: require-os-selector
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces: ["prod", "win-prod", "linux-prod"]
parameters:
selectors:
- key: "kubernetes.io/os"
allowedRegex: "linux|windows"
运行手册片段(遇到问题先敲它)
查看节点 OS 与标签
kubectl get nodes -L kubernetes.io/os -L topology.kubernetes.io/zone -L node.os.distro
检查被拒 Pod 原因
kubectl describe pod <pod> | egrep -i "taint|NoSchedule|node affinity|didn't match"
Windows HNS 网络
Get-HnsNetwork; Get-HnsEndpoint
调整 Calico MTU(滚动)
kubectl -n kube-system set env ds/calico-node FELIX_VXLANMTU=1450
强制在某区调度
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["hk-2"]
成本与收益(给老板看的两行字)
一次性投入:Windows 节点容器化适配(1~2 人周),CI/CD overlay、监控/日志加固(1 人周)。
长期收益:交付周期缩短 50%+;夜间故障恢复时间下降 40%+;人力值班压力明显下降。
FAQ:几个常见“坑点”的取舍
为什么不用单独 Windows 集群?
我们试过,运维成本翻倍、网络与权限策略难以统一。混部 + 策略治理成本更优。
数据库能跑在 Windows 容器里吗?
理论可行,实践不建议。Linux 存储生态成熟,主从/集群架构选择更多。
CNI 选 Flannel 行不行?
Flannel Windows 支持相对有限,Calico Windows 在策略与可观测性上明显更完整。
总结:凌晨 2:35 的那一刻
Calico 的滚动更新刚完成,新的 Windows 节点成功接管了 .NET 服务,api-win 四个副本在两柜均衡分布,Prometheus 图上红色报警线缓缓回落。
我关掉机房的头灯,屏幕上最后一条事件是 Scheduled。
做运维这些年我越来越笃定:跨平台不是靠人肉记忆,是靠“标签、污点、亲和、网络、存储、监控”一整套工程化作业。Kubernetes 不是银弹,但它给了我们把混乱变成秩序的锤子。
走出机房时,清晨 3 点的荃湾街口还有两家便利店开着。我买了罐冰可乐,心想:这次,终于能睡个踏实觉了。
附:可直接复用的清单与命令(归档版)
- kubeadm-controlplane.yaml(见上)
- Calico:Linux + Windows(记得 MTU=1450,VXLAN)
- Windows:Enable-WindowsOptionalFeature、containerd 安装、kubeadm join(npipe 套接字)
- 统一标签/污点脚本(见上)
- Deployment 样例:api-win / api-linux
- SMB/NFS StorageClass(同名 shared-files)
- HostProcess DaemonSet(windows-exporter)
- netsh 动态端口收紧命令
如果你也在香港、或任一多运营商、跨柜、跨 OS 的环境里奋战,希望这份“深夜机房笔记”能让你少走几步弯路。祝你每个滚动更新都平安、每次集群扩容都顺利。