从零构建异构高性能计算集群:Kubernetes与Ceph实战指南
1. 项目概述:从“winner1300”看高性能计算集群的平民化实践
最近在折腾一个老项目,翻出来一堆退役的服务器硬件,型号杂七杂八,性能也参差不齐。看着这些“电子垃圾”,我就在想,能不能用它们搭一个能真正干点“重活”的计算集群?不是为了跑分好看,而是能稳定处理一些并行计算任务,比如视频转码、数据清洗或者小规模的机器学习模型训练。这个想法催生了我称之为“winner1300”的项目。这个名字听起来有点中二,其实是我给这个自建集群起的代号,“1300”源于最初拼凑起来的核心线程数总和。这不仅仅是一次硬件堆砌,更是一次关于如何用最低的成本、最普通的硬件,构建一个稳定、可用且易于管理的高性能计算环境的深度探索。
你可能觉得,搞集群那是大公司、科研机构的事,动辄成千上万的节点,用的是专有网络和存储。但我想分享的是,随着开源软件生态的成熟和硬件价格的走低,构建一个服务于中小团队甚至个人开发者的“微型高性能计算集群”已经变得非常可行。winner1300项目的核心目标,就是验证并实践一套从硬件选型、系统部署、资源调度到任务分发的完整方案,让计算力变得触手可及。无论你是一个想学习分布式计算的学生,一个需要处理海量数据的小团队,还是一个热衷于硬件的极客,这个项目都能给你提供一条清晰的路径。接下来,我会详细拆解从零搭建到实际应用的全过程,分享其中踩过的坑和总结出的宝贵经验。
2. 整体架构设计与核心思路拆解
搭建一个计算集群,远不是把几台机器用网线连起来那么简单。它涉及到计算、存储、网络和管理四个层面的深度融合。在规划winner1300时,我首先明确了几个核心原则:成本可控、易于维护、高可用性、弹性扩展。基于这些原则,我设计了一套分层架构。
2.1 硬件层:异构硬件的统一管理
我的硬件来源很杂:有淘汰的企业级双路服务器,也有几台高性能的游戏台式机,甚至还有两台树莓派4B。它们的CPU架构(x86_64和ARM)、内存大小、磁盘类型都不同。这种异构性是业余项目的常态,但也带来了挑战。
我的策略是抽象化硬件差异。通过统一的Linux操作系统(我选择了Ubuntu Server LTS版本)和容器化技术,将应用与底层硬件解耦。对于x86和ARM的差异,我们在软件编译和容器镜像选择时需要注意,但通过Docker的多架构镜像或统一使用兼容性好的基础镜像(如alpine)可以很大程度上规避问题。关键在于,我们的集群管理软件(如Kubernetes或更轻量的Docker Swarm)需要能够识别并调度任务到合适的节点上。例如,我将树莓派节点标记为“arm”节点,只调度那些已经编译好ARM版本或兼容ARM架构的轻量级任务(如数据采集、消息转发)。
注意:异构集群中,性能最差的节点往往会成为整个系统的短板,尤其是在需要跨节点同步数据的场景下。因此,合理的节点角色划分至关重要。我将性能最强的双路服务器作为“计算节点”和“存储节点”的核心,而性能较弱的机器和树莓派则作为“边缘节点”或“专用任务节点”(例如专门运行日志收集服务)。
2.2 网络层:低延迟与高带宽的平衡
集群内部网络通信的效率和稳定性直接决定了并行计算的性能。家用千兆交换机是起点,但远远不够。我采用了双网卡绑定(Bonding)和专用存储网络的策略。
- 管理/业务网络:使用普通的千兆交换机,所有节点通过一个网卡连接,用于SSH管理、服务发现、API调用等常规流量。我将两个千兆网卡绑定为“mode=4”(802.3ad动态链路聚合),需要交换机支持LACP。这提供了负载均衡和故障转移,有效提升了带宽和可靠性。
- 存储网络:为了满足计算节点高速访问共享存储的需求,我额外为几台核心服务器配备了万兆光纤网卡,并通过一台二手万兆交换机直连,组建了一个独立的存储网络。在这个网络上运行NFS或Ceph这样的分布式存储服务,可以避免存储IO成为性能瓶颈。
网络规划表:
| 网络名称 | 用途 | 典型带宽 | 技术选型 | 关键考虑 |
|---|---|---|---|---|
| 管理网络 | SSH, 服务发现,集群管理 | 1Gbps (Bonded) | 千兆交换机 + Bonding | 稳定性、IP地址规划 |
| 存储网络 | 分布式存储数据同步、虚拟机/容器磁盘IO | 10Gbps | 万兆光纤交换机 | 低延迟、高带宽、隔离性 |
| 业务网络 | 对外服务访问(可选) | 1Gbps | 千兆交换机/VLAN | 安全隔离、负载均衡 |
2.3 软件栈:轻量、易用与强大的组合
软件选型上,我遵循“不重复造轮子”和“社区活跃”的原则。核心软件栈如下:
- 操作系统:Ubuntu Server 22.04 LTS。选择LTS版本是为了获得长期稳定的更新和支持,兼容性好,文档丰富。
- 容器引擎:Docker。它是现代应用打包和运行的事实标准,提供了良好的隔离性和可移植性。
- 集群编排:Kubernetes (K8s)。虽然学习曲线陡峭,但它是容器编排的王者,提供了无与伦比的自动化部署、扩展和管理能力。对于小规模集群,使用
kubeadm工具可以相对轻松地完成部署。 - 存储方案:Rook + Ceph。这是一个在K8s内部运行Ceph存储集群的Operator。它让我能用声明式的方式管理一个高可用、可扩展的分布式存储系统,完美契合K8s的哲学。数据持久化问题迎刃而解。
- 监控告警:Prometheus + Grafana。Prometheus负责采集集群和应用的各项指标(CPU、内存、磁盘、网络、服务健康状态),Grafana则用于可视化展示。再搭配Alertmanager,可以实现灵活的告警规则。
- 日志收集:EFK Stack (Elasticsearch, Fluentd, Kibana)。Fluentd作为日志收集代理部署在每个节点,将容器和系统日志统一发送到Elasticsearch进行索引和存储,最后通过Kibana进行查询和展示。
这套组合拳下来,winner1300就从一个硬件集合,变成了一个具备自我修复、弹性伸缩、服务发现、配置管理和可视化管理能力的现代化计算平台。
3. 核心组件部署与关键配置详解
有了清晰的架构设计,下一步就是动手实施。这里我重点分享Kubernetes集群和Ceph存储的部署过程中那些容易踩坑的关键点。
3.1 Kubernetes集群的“非标准”初始化
在异构硬件和混合网络环境下,用kubeadm初始化集群需要一些额外的配置。
首先,在所有节点上安装Docker和Kubeadm工具包是标准操作,这里不再赘述。关键步骤在于初始化主控制平面节点。因为我有多个网络接口,必须明确指定API Server的监听地址。
# 在主节点上执行,假设管理网络IP是192.168.1.100 sudo kubeadm init --apiserver-advertise-address=192.168.1.100 --pod-network-cidr=10.244.0.0/16--apiserver-advertise-address参数至关重要,它告诉其他节点通过哪个IP来访问API Server。务必设置为节点间能互通的那个IP(通常是管理网络IP)。
初始化成功后,按照提示配置kubectl,并安装Pod网络插件。我选择了Flannel,因为它简单可靠,对新手友好。但Flannel默认的VXLAN后端在跨节点通信时会有一定的性能开销。在我的万兆存储网络上,我尝试了Flannel的host-gw模式,它要求所有节点在同一个二层网络,性能几乎无损。
# 下载Flannel的k8s配置文件,并修改backend type wget https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml # 编辑kube-flannel.yml,在`net-conf.json`部分将`Backend`的`Type`从`vxlan`改为`host-gw` kubectl apply -f kube-flannel.yml将工作节点加入集群后,一个基础的K8s集群就搭建完成了。但此时,集群还不能很好地识别我们的异构硬件。
3.2 节点标签与污点管理:精细化调度
为了将任务调度到合适的节点,我们需要给节点打上标签(Labels)。例如,给ARM架构的树莓派打标:
kubectl label node raspberry-pi-1 node-type=arm kubectl label node raspberry-pi-1 disk-type=slow # 如果它的磁盘是SD卡然后,在部署应用的YAML文件中,可以使用nodeSelector来指定调度到带有node-type=x86的节点上。对于像存储节点这样需要特殊资源或不希望随意调度普通Pod的节点,可以设置污点(Taint),只有容忍(Toleration)该污点的Pod才能被调度上去。这在部署Ceph这样的存储服务时是标准做法。
3.3 使用Rook部署Ceph存储集群
在K8s上管理存储,Rook是目前最优雅的方案之一。部署过程是声明式的,但前期准备和参数调优决定成败。
第一步:准备工作确保计划作为存储节点的机器有额外的裸磁盘(未分区、未挂载)。在我的环境中,我为三台核心服务器各添加了一块1TB的SSD。通过lsblk命令确认磁盘路径,例如/dev/sdb。
第二步:部署Rook Operator这很简单,直接从GitHub拉取部署清单即可。
git clone --single-branch --branch v1.10.0 https://github.com/rook/rook.git cd rook/deploy/examples kubectl create -f crds.yaml -f common.yaml -f operator.yaml第三步:配置Ceph集群这是核心步骤。需要编辑cluster.yaml文件。关键配置项包括:
storage.nodes: 指定哪些节点参与存储,以及使用这些节点上的哪些磁盘。必须正确填写节点的名称(kubectl get nodes查看)和磁盘路径。storage.useAllNodes和useAllDevices: 通常设为false,以进行精确控制。network: 强烈建议为Ceph集群数据同步配置独立的存储网络。这里可以指定集群网络(Cluster Network)和公共网络(Public Network)的CIDR。例如,我的存储网络是10.10.0.0/24。
一个简化的节点配置示例:
storage: nodes: - name: "node1" devices: - name: "sdb" - name: "node2" devices: - name: "sdb"第四步:创建存储类(StorageClass)Ceph集群运行起来后,需要通过StorageClass来提供动态卷供应。Rook提供了示例文件csi/rbd/storageclass.yaml。创建后,应用就可以通过PVC(PersistentVolumeClaim)来申请持久化存储了。
实操心得:Ceph对时钟同步要求极高,所有节点的时间偏差必须非常小(建议在毫秒级)。务必确保集群内运行了可靠的NTP服务(如
chrony)。我曾因为时间不同步导致Ceph MON(监控器)无法达成共识,集群状态一直异常,排查了很久。
4. 实战应用:构建分布式视频转码流水线
集群搭建好了,存储也就绪了,是时候让它真正“干活”了。我选择“分布式视频转码”作为第一个实战应用,因为它计算密集、易于并行化,且能直观体现集群的价值。
4.1 应用架构设计
视频转码流水线可以抽象为三个阶段:
- 任务分发:接收用户上传的视频文件,将其拆分成若干片段(如按5分钟一段),并将每个片段作为一个转码任务放入任务队列。
- 并行转码:多个工作Pod从队列中领取任务,独立进行转码计算。
- 结果合并:所有片段转码完成后,将音频视频流合并成一个完整的文件。
在K8s上,我们可以这样实现:
- 任务队列:使用Redis,部署为一个StatefulSet,确保队列服务本身的高可用。
- 任务分发器:一个自定义的控制器(可以用Python+
kubernetes客户端库编写),监听文件上传事件(例如通过MinIO对象存储),负责拆分任务并推入Redis队列。它本身也部署为一个Deployment。 - 转码工作器:核心计算单元。我们创建一个包含FFmpeg工具的Docker镜像。工作器Pod从Redis队列中
POP任务,执行FFmpeg命令进行转码,完成后将输出文件写入共享存储(Ceph RBD卷),并通知任务分发器。 - 合并器:另一个独立的Pod,监听所有片段完成的事件,然后调用FFmpeg进行合并。
4.2 工作器Pod的详细配置
工作器的K8s部署文件(Deployment)需要仔细配置资源请求和限制,并利用好我们之前设置的节点标签。
apiVersion: apps/v1 kind: Deployment metadata: name: video-transcoder-worker spec: replicas: 5 # 根据集群资源决定启动多少个工作副本 selector: matchLabels: app: transcoder-worker template: metadata: labels: app: transcoder-worker spec: nodeSelector: node-type: x86 # 只调度到x86高性能节点 containers: - name: worker image: my-registry/transcoder-ffmpeg:latest resources: requests: memory: "2Gi" cpu: "1000m" limits: memory: "4Gi" cpu: "2000m" volumeMounts: - name: transcode-storage mountPath: /workspace env: - name: REDIS_HOST value: "redis-service" - name: REDIS_QUEUE value: "video_tasks" volumes: - name: transcode-storage persistentVolumeClaim: claimName: transcode-pvc # 提前创建好的PVC,指向Ceph存储关键点解析:
nodeSelector: 确保Pod只运行在标记为node-type=x86的节点上,避免调度到ARM节点。resources.requests/limits: 这是K8s进行资源调度和管理的依据。requests是调度时保证的最小资源,limits是运行时的硬性上限。为CPU和内存设置合理的值,可以防止单个Pod耗尽节点资源,也便于K8s在节点间均衡负载。FFmpeg转码是CPU密集型任务,我给了它1个核心(1000m)的请求和2个核心的限制。persistentVolumeClaim: 所有工作器挂载同一个共享存储,这样它们都能读取到输入的片段,并写入转码后的输出片段。
4.3 任务队列与工作流的协调
任务分发器将一个大视频文件拆分成N个任务,每个任务包含片段起止时间、输入文件路径、输出格式参数等。它将任务序列化为JSON字符串,推入Redis的List中。
工作器Pod内运行一个简单的Python脚本,循环执行以下逻辑:
while True: task_json = redis_client.rpop(REDIS_QUEUE) if task_json: task = json.loads(task_json) # 1. 从共享存储读取输入片段 # 2. 组装FFmpeg命令并执行 # 3. 将输出写入共享存储 # 4. 向另一个Redis Key (e.g., 'completed_tasks') 发送完成信号 else: time.sleep(5) # 队列为空,休眠等待合并器则监听completed_tasks的数量,当数量等于总片段数N时,触发合并操作。整个流程通过Redis这个简单的中间件实现了松耦合的协同。
5. 监控、运维与问题排查实录
一个集群能否稳定运行,三分靠部署,七分靠运维。完善的监控和清晰的排查思路是运维的生命线。
5.1 构建全方位的监控仪表板
使用Prometheus Operator可以非常方便地在K8s中部署监控栈。它自动为集群内的各种资源(节点、Pod、Service等)配置抓取目标。
核心监控指标:
- 集群健康度:Node的CPU/内存使用率、磁盘IOPS和容量、网络带宽。通过
node_exporter采集。 - Pod/应用状态:每个Pod的CPU/内存使用量、重启次数、就绪状态。通过K8s内置的cAdvisor和kube-state-metrics采集。
- 业务指标:对于我们自建的转码服务,需要暴露自定义指标。例如,使用Prometheus的Python客户端库,在任务分发器和工作器中暴露
tasks_in_queue、tasks_completed_total、transcode_duration_seconds等指标。 - Ceph存储状态:Rook已经集成了Ceph的Prometheus监控,我们可以直接获取到POOL的使用率、OSD状态、IOPS、延迟等关键数据。
在Grafana中,我将这些指标组织成几个核心仪表板:
- 集群概览:一眼看清所有节点的资源水位和Pod运行状态。
- 存储集群详情:聚焦Ceph各个POOL的使用量、OSD的UP/IN状态、读写延迟。
- 视频转码业务看板:显示任务队列长度、各工作器活跃任务数、平均转码时长、成功率等。这能直接反映业务流水线的健康度。
5.2 典型问题排查与解决记录
在winner1300的运行过程中,我遇到了形形色色的问题,以下是几个典型案例:
问题一:Pod一直处于Pending状态。
- 现象:
kubectl get pods显示某个Pod卡在Pending。 - 排查:
kubectl describe pod <pod-name>查看事件。最常见的原因是资源不足(Insufficient cpu/memory)或没有节点满足节点选择器/污点要求。- 检查节点资源:
kubectl describe node <node-name>,看Allocatable资源是否充足。 - 检查节点标签和污点:
kubectl describe node <node-name>查看Labels和Taints部分。
- 解决:如果是资源不足,考虑增加节点、减少Pod副本数或优化Pod的资源请求。如果是节点选择问题,修正Pod的
nodeSelector或为节点添加正确的标签。
问题二:Pod运行后频繁重启(CrashLoopBackOff)。
- 现象:Pod状态在
Running和Error/CrashLoopBackOff之间循环。 - 排查:
kubectl logs <pod-name> --previous查看上一次崩溃的日志。这通常能直接定位到应用代码错误、配置错误或依赖缺失。- 如果日志没有明显错误,检查Pod的资源限制(
limits)。可能是内存不足(OOMKilled),可以通过kubectl describe pod看到退出码是137。 - 检查存储挂载是否成功,比如PVC是否处于Pending或Bound状态,挂载路径是否存在权限问题。
- 解决:根据日志修正应用错误;适当调高内存限制;检查PVC配置和存储后端(Ceph)的健康状态。
问题三:Ceph集群健康状态为HEALTH_WARN或HEALTH_ERR。
- 现象:
kubectl -n rook-ceph exec -it deploy/rook-ceph-tools -- ceph status显示非HEALTH_OK状态。 - 常见原因及解决:
- OSD down:某个存储守护进程挂了。检查对应节点和磁盘状态。可能是磁盘故障、节点重启或网络分区。尝试重启OSD Pod:
kubectl -n rook-ceph delete pod <osd-pod-name>,Rook会自动重建。 - PG状态异常:Placement Group(PG)是Ceph数据分布的单位。出现
inactive,stale,degraded等状态,通常与OSD down或网络问题有关。先解决OSD问题,PG状态通常会自行恢复。可以使用ceph pg repair <pg_id>尝试修复,但需谨慎。 - 空间接近满:监控仪表板会提前告警。需要及时添加新的OSD或扩容现有OSD磁盘,也可以删除不必要的数据。
- OSD down:某个存储守护进程挂了。检查对应节点和磁盘状态。可能是磁盘故障、节点重启或网络分区。尝试重启OSD Pod:
问题四:跨节点Pod网络不通。
- 现象:Pod A(在Node1)无法ping通Pod B(在Node2)的ClusterIP或Pod IP。
- 排查:
- 检查Flannel(或其他CNI插件)的Pod是否在所有节点正常运行:
kubectl get pods -n kube-system -l app=flannel。 - 检查节点路由表:在Node1上
ip route show,看是否有到其他节点Pod网段(如10.244.0.0/16)的路由,下一跳是否正确指向了目标Node2的IP。 - 检查防火墙:确保所有节点之间在相关网段(如管理网的
192.168.1.0/24和Pod网的10.244.0.0/16)的流量没有被防火墙(如ufw或firewalld)阻止。这是最常见的原因!
- 检查Flannel(或其他CNI插件)的Pod是否在所有节点正常运行:
- 解决:确保CNI插件Pod健康;如果使用
host-gw模式,确认节点间二层互通;最关键的,在集群所有节点上,放行CNI所需的端口和网段。对于Flannel,通常需要放行UDP 8285和8472端口,以及VXLAN或主机网关的流量。
避坑技巧:养成“从底向上”的排查习惯。网络不通?先查物理链路和交换机,再查主机防火墙和路由,最后查K8s网络插件。Pod起不来?先看节点资源,再看调度约束,最后看容器日志。建立清晰的排查路径图,能极大提升效率。
6. 性能调优与成本控制实践
集群跑起来只是第一步,如何让它跑得更快、更省,才是体现功力的地方。在winner1300项目上,我做了以下几方面的调优。
6.1 计算密集型任务调优
对于视频转码这类CPU密集型负载:
- CPU绑核:通过设置Pod的
spec.containers[].resources.limits.cpu为整数(如2),并添加注解spec.template.spec.containers[].resources.requests.cpu等于limits,可以暗示K8s进行“静态”CPU管理,减少上下文切换,在某些场景下能提升性能。但要注意这会降低调度灵活性。 - 使用本地临时存储:转码过程中会产生大量的临时文件。如果每次都读写网络存储(Ceph),IO延迟会成为瓶颈。我为工作器Pod挂载了
emptyDir卷,并设置medium: Memory,将临时文件写入内存盘(tmpfs),速度极快。只需确保最终输出结果写入持久化存储即可。 - FFmpeg参数优化:这是应用层最大的优化点。例如,使用更高效的编码器(如
libx265vslibx264),调整线程数(-threads参数)与Pod分配的CPU核心数匹配,利用硬件加速(如果显卡支持)等。需要根据源视频格式和目标质量进行反复测试。
6.2 存储性能优化
Ceph的默认配置偏向于通用和稳定,针对特定负载可以调整。
- CRUSH Map调优:确保数据副本均匀分布在不同的主机和机架(故障域)上。在我的小规模集群中,我设置了
host级别的故障域,保证同一个PG的两个副本不会落在同一台物理主机上。 - Pool配置:为视频转码业务创建独立的存储池(Pool)。可以针对性地设置:
size: 副本数,我设置为2,在容量和可靠性间平衡。pg_num: Placement Group数量。这是一个非常关键的参数,设置过小会导致数据分布不均和OSD负载不均衡,设置过大会增加管理开销。可以使用Ceph官方提供的PG数量计算器来估算。对于我的1TB左右池,我设置了128个PG。- 使用SSD作为DB/WAL设备:如果OSD使用的是HDD,可以添加一块小容量SSD作为“日志设备”,将写操作的Journal放在SSD上,能显著提升随机写性能。我的环境全部是SSD,所以无需此步骤。
6.3 成本与能效控制
7x24小时运行一个集群,电费不容忽视。
- 节点自动启停:对于非核心的边缘节点(如树莓派),或者在工作负载低谷期,可以通过脚本结合K8s的
cordon/drain命令,安全地排空节点并关机。在需要时再远程唤醒(WoL)并恢复调度。这需要主板和网卡支持网络唤醒功能。 - Pod水平自动伸缩(HPA):为转码工作器的Deployment配置HPA,基于CPU利用率或自定义指标(如队列长度)自动调整Pod副本数。当没有任务时,副本数可以缩容到0,节省资源。
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: transcoder-worker-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: video-transcoder-worker minReplicas: 0 # 允许缩容到0 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 # CPU平均使用率超过70%则扩容 - 资源超售与服务质量(QoS):合理设置Pod的
requests和limits。对于非关键任务,可以设置较低的requests,允许一定的超售,提高集群资源利用率。K8s会根据requests来划分Pod的QoS等级(Guaranteed, Burstable, BestEffort),在节点资源紧张时,BestEffort的Pod会最先被终止。
构建和维护winner1300这样的项目,是一个持续学习和优化的过程。它让我深刻体会到,将分散的计算资源整合成一个有机整体所带来的力量。从最初的硬件拼凑,到如今能稳定处理实际工作负载,每一步都充满了挑战和收获。对于想要入门分布式系统和云原生技术的朋友,我强烈建议从这样一个具体的、有明确产出目标的小项目开始。你会遇到真实的问题,并迫使自己去理解网络、存储、调度、应用编排等每一个环节,这种经验远比单纯阅读文档要深刻得多。最后一个小建议:做好文档记录,无论是集群的配置清单、部署步骤,还是遇到的问题和解决方案,积累下来的知识库是你未来运维和扩展最宝贵的财富。
