1. 银行大数据平台的“旧账”:为什么非改不可
先说一个我印象特别深的深夜场景。某个月末跑批,调度平台上一堆离线任务排队,YARN资源池被打满,核心报表任务卡了两个小时没跑出来。运维同事直接在群里发了一句“集群又满了”,然后大家开始在Excel里算哪些任务能往后挪。那个晚上所有人都在手动干预,真正的原因只有一个——资源池的弹性太差了。
这不是某一家银行的个例。传统大数据平台建设多年之后,普遍存在几个绕不开的硬伤:烟囱式建设、资源错峰困难、扩容周期长、版本碎片化。每个部门各自搭一套Hadoop生态,每套集群的计算和存储强耦合,高峰时段某些队列排队排到天荒地老,低谷时段CPU却闲到个位数。想扩一批节点,硬件采购、网络配置、基础环境初始化、组件部署,一套流程走下来两三个星期算是快的。至于组件升级,更是牵一发动全身,谁都不敢先动。
所以当“云原生”这个概念开始落地到数据平台时,我们团队的目标非常朴素:把大数据平台变成一种能快速伸缩、按需交付的云服务,而不是一台台绑在一起的物理机。
这里要先说清楚,银行的云原生大数据平台不是简单把Spark、Hive这些组件塞进Docker就完事。它涉及调度体系的重构、存储架构的调整、资源隔离模型的变化,以及最容易被忽略的——整个运维和交付模式的转变。此文就围绕我们实际落地的方案来拆解,从架构设计到选型、踩坑、收益复盘,尽量把关键决策背后的“为什么”讲透,让准备做同类改造的团队少走弯路。
2. 目标架构全景:分层解耦、存算分离、弹性伸缩
2.1 核心设计逻辑:三层解耦
我们最终定下来的目标架构,核心就三个关键词:分层解耦、存算分离、弹性伸缩。
先解释分层。之前的大数据平台所有组件都部署在同一批物理机上,计算和存储混部。这样做的好处是网络延迟极低、数据本地性高,但坏处是计算高峰和存储增长互相影响,谁都不能独立扩展。改造后的架构把平台拆成了三层:
- 基础设施层:统一以Kubernetes作为资源调度的底座,管理所有计算节点的容器生命周期。物理机也好、虚拟机也好,全部抽象成资源池。
- 平台服务层:包括Spark、Flink、Hive、Presto这些计算引擎,通过K8s的Controller或Operator来管理,任务按需创建Pod,跑完自动释放。
- 存储层:历史数据统一入湖,采用对象存储作为主存储,热数据保留在分布式文件系统中用作频繁读取和Shuffle的中间件。
这个分层带来一个直接好处——计算和存储彻底解耦。存储不够就扩存储,计算不够就扩计算,互不拖累。更关键的是,计算集群可以做到真正的弹性伸缩。平时维持一个较小的常驻资源池,跑批高峰期扩出几百个Pod,任务结束再缩回去。
2.2 存算分离的实现方式:并不是完全放弃HDFS
存算分离是个很宽泛的概念。我们实际落地时,并没有粗暴地“把HDFS干掉”,而是采用了分层存储策略。底层数据用Apache Iceberg这类表格式管理,数据文件存储在对象存储中,同时保留一小部分HDFS或本地SSD作为计算引擎的临时存储,专门用于Shuffle的中间数据。
这里有个容易被忽略的点:存算分离后,最怕的就是Shuffle数据落到远端的对象存储。Spark Shuffle的数据量动辄几十GB,如果全部走网络传到对象存储,性能会非常难看。所以在初期我们保留了本地盘的Shuffle服务,让Pod在写临时数据时用的是节点本地磁盘,只有最终的结果数据才回写到对象存储。这个决策在后续性能测试中被证明了非常关键。
2.3 资源管理的核心转变:从YARN到Kubernetes
传统Hadoop生态里,YARN是当之无愧的资源调度大脑。但迁移到云原生架构后,Kubernetes替代了YARN的位置。刚开始团队内部有不少质疑,Spark on YARN已经很成熟,为什么非要迁到K8s上?
我的看法是:如果只是替代YARN,迁移K8s的意义确实不大。但K8s能带来YARN提供不了的能力:
- 统一的资源池:大数据任务和普通微服务任务可以共存,资源按优先级调度。
- 更细粒度的配额管理:基于Namespace的ResourceQuota,比YARN队列更灵活。
- 更完善的生态:监控、日志、告警、服务发现天然一致。
- 快速伸缩:Pod秒级启动,比NodeManager上启动Container快一个量级。
当然,K8s调度大数据负载也有天生短板。默认调度器是为长驻服务设计的,对Spark这种需要批量创建几十个Pod并保证它们同时调度的场景支持不够好。所以我们在调度层引入了Volcano这个批量调度器,目的就是解决Pod组调度和公平调度的问题,这一块后面会单独展开。
3. 核心选型取舍:哪些组件上K8s,哪些必须留在传统环境
3.1 计算引擎选型:Spark/Flink on K8s的可行性分析
组件选型阶段,我们做了一个分类决策:所有无状态、可横向扩展的计算引擎,全部容器化上K8s;有状态、数据强一致性的组件,暂时留在传统环境或采用Operator托管。
Spark是我们最核心的批处理引擎。Spark on K8s从2.3版本开始支持,到Spark 3.x已经相当成熟。在提交方式上,我们采用了spark-submit --master k8s://的原生提交模式,每个Application的Driver和Executor都以Pod的形式运行在K8s集群中。
Flink方面,我们用Flink Kubernetes Operator来管理作业的生命周期,通过声明式API定义作业的资源规格、并行度和保存点策略。流式作业稳定性要求极高,所以Operator的自动故障恢复能力对我们来说非常重要——JobManager挂掉之后能在秒级重建,作业从最近的有效检查点恢复。
而像HBase、Kafka这类依赖持久化存储、对网络拓扑敏感的组件,我们选择了保守策略:继续跑在物理机或虚拟机上,由传统的运维模式托管,等Operator生态足够成熟再逐步迁移。这也是很多银行真实场景的常见选择——不是所有东西都适合容器化,先算清楚重量级有状态服务的迁移成本再动手。
3.2 存储选型:对象存储的硬性要求与测试结论
存储选型上,我们对比了MinIO、Ceph以及商业对象存储方案,最终的核心决策点落在三个维度:S3协议兼容性、可靠性、运维成本。
银行数据不能丢,所以存储的持久化能力是第一优先级。对象存储通常采用多副本或纠删码方案,我们最终选择了纠删码,因为相同数据冗余度下可用容量更高。在实际压测中,12节点集群配置纠删码后,写入吞吐和副本模式相比只损失了约10%,但可用空间提升了近一倍。
另外我特别想提醒一点:对象存储和HDFS的性能模型差异极大。HDFS读取时数据本地性可以做到零网络传输,而对象存储每次读取都要走网络。所以迁移后一定要配合数据缓存层设计,比如Spark的Alluxio集成,或者让常态化读取的热数据保留一份在本地数据节点上。我们在开发环境第一次做全量数据扫描测试时,前期没加缓存,一个原本20分钟的作业直接跑成了70分钟,后来加了缓存层才恢复正常。
3.3 调度器选择:Volcano凭什么替代默认调度器
关于调度器的选型,我们的结论是:默认Kubernetes调度器不适合大数据作业的批量并行模式,必须引入批量调度器。
这里有个关键的专业概念叫做All-or-Nothing Scheduling(全有或全无调度)。一个Spark作业往往需要同时拉起几十个Executor Pod,如果默认调度器分多批把Pod逐个调度出来,会产生两个问题:一是前一批Pod已经启动,后一批却还在等待,造成资源碎片化;二是如果集群资源不足导致部分Pod无法创建,前一批已经跑起来的Executor还要一直等待,任务卡死。
Volcano解决了这两个问题,它支持PodGroup把一组Pod作为一个整体参与调度,要么全部调度成功,要么全部不调度,避免资源碎片。同时它引入了抢占和回填机制,在资源紧张时优先级高的作业可以抢占低优先级作业的资源。
我们还测试过另外一个开源调度器Yunikorn,它在多租户场景下的队列管理做得不错,但当时和Spark Operator的集成成熟度不如Volcano,最终选择了Volcano。这个选型结果不是绝对的,团队可以根据自己的调度场景灵活取舍。
4. 迁移实施路径:从“双跑”到“全切”的四阶段
4.1 阶段一:基础环境准备与K8s集群建设
改造的第一步不是迁移任务,而是先把K8s集群建好。这里最花时间的是网络方案选型。银行环境对网络安全隔离的要求高,我们具体用的是Calico配合NetworkPolicy实现命名空间级别的网络隔离。大数据的Shuffle流量巨大,需要节点间高带宽低延迟,所以Calico的IPIP模式不可取,必须开启VXLAN直连或BGP模式。我们直接用了BGP模式,让所有Pod IP在物理网络内路由,避免封装开销。
容器集群建设时有一个具体建议:为大数据任务单独划分节点池。例如节点池A跑在线业务微服务,节点池B跑批处理任务,节点池C跑流式作业。不同节点池的机型、规格、标签都不同,再通过节点亲和性和污点容忍来保证作业不会调度到不合适的节点上。
4.2 阶段二:镜像标准化与双跑验证
环境就绪后,我们做了两件事:标准化镜像和双跑验证。
镜像标准化把各业务部门五花八门的运行环境统一到一个基线版本。我们以Harbor作为私有镜像仓库,镜像Tag遵循严格的版本规范,每个组件的镜像如Spark、Flink、Hive分别构建了基础镜像、运行镜像、工具镜像三个层级。其中工具镜像内置了各种排查技巧用的小工具,比如jmap、jstack、tcpdump、arthas,方便线上排查JVM和网络问题,这个做法后来在排障时帮了大忙。
双跑验证是迁移过程中最耗时但也是最重要的环节。我们选择了几张核心报表任务,将同一份数据同时提交到旧集群和新集群,对比执行时间和计算结果。这里有个经验:双跑初期不要追求全量任务切换,先挑10个以内的核心任务跑通全链路,包括数据读取、计算、结果回写、调度依赖触发。等验证稳定后,再逐步扩大双跑范围。
我记得当时有一个日汇总任务,在旧集群跑10分钟,新集群首次跑出了25分钟的成绩。排查后发现是新集群的Executor内存规格设置偏小,导致频繁触发Shuffle落盘。把单Executor内存上调、并行度调整之后,花费约7分钟,比旧集群还快了三成。这种调优案例说明了一个问题:同样一段代码,在不同架构上跑出的效率差异可能极大,迁移前一定要针对新架构做参数校核,而不是直接照搬旧配置。
4.3 阶段三:业务分批切换与下线验证
双跑验证完成后,进入业务分批切换。我们的策略是按照业务影响面从小到大来排:先切换T+1离线报表类任务,再切换数据同步和接口类任务,最后再动实时计算作业。
切换过程中必须有一套完整的回退机制。我们的做法是:在调度平台中为每个任务保留新旧两个执行入口,通过配置开关动态路由。开关切到新集群时,任务日志实时上报,监控大屏同步展示成功率、耗时、数据量三个核心指标。一旦出现问题,可以把开关一键拨回旧入口,做到分钟级回退。
4.4 阶段四:下线旧集群,运维模式升级
旧集群下线不是简单的关停,还包括历史数据迁移、元数据清理、依赖关系梳理。这一步最容易出现的问题就是隐藏依赖——某些任务虽然没在调度平台上有明确的依赖记录,但底层却悄悄读取了旧HDFS上的某个路径。我们花了将近一个月的时间,通过扫日志、抓审计记录、对比数据血缘,才把这类隐性依赖清理干净。
旧集群下线完成之后,运维模式也彻底变了。以前扩容一套大数据集群,从提申请到环境就绪至少按周计;现在K8s集群里直接增加节点池的节点数,Pod会在几分钟内自动创建并加入计算。这个变化对运维团队的意义是跨时代的——从“管理机器”变成“管理资源池”。
5. 稳定性与合规:银行场景下的云原生改造底线
5.1 多租户隔离与资源配额设计
银行的数据平台通常有多个部门、多条业务线在共用,有些用数需要高优先级保证,比如监管报送,有些则只是临时的探索性分析。涉及多团队共享的云原生平台,租户隔离如果不做好,后面就是无休止的资源打架和事故。
我们设计了三层隔离体系:
- Namespace级隔离:每个业务线一个独立Namespace,通过网络策略实现跨命名空间访问限制。
- 资源配额级隔离:使用ResourceQuota限制每个Namespace的CPU、内存总量,再通过PriorityClass区分任务优先级。
- 数据权限级隔离:基于Ranger统一管理数据权限,对接底层的对象存储和表格式服务,保证跨部门数据不能越权访问。
这里有个细节值得注意:K8s的ResourceQuota默认会限制Pod数量,但大数据作业创建Pod数量波动极大,配额设置得太紧容易导致作业创建失败,太松又失去限制效果。我们后来把Pod数量的Quota改成了按创建速率加白名单控制,只限制常驻服务,不限制任务型Pod。
5.2 高可用架构:从单可用区到多活设计
银行核心数据链路是不允许单点故障的。虽然我们是分析平台,但由于监管报送等特殊场景的存在,对可用性的要求同样苛刻。
我们在K8s集群层面做了多可用区的设计:每个可用区部署一套独立的K8s节点池,Pod通过节点亲和性分散到不同的可用区。对象存储层的纠删码数据打散到三个可用区,保证任一可用区整体故障时数据不丢、平台能继续服务。Flink的检查点保存路径跨可用区写入,即便一个可用区挂掉,作业可在另一个可用区自动恢复。
这一层设计需要特别关注跨可用区网络延迟。如果延迟太高,分布式计算引擎的性能会严重劣化。我们当时在同一城市范围内选了延迟小于1ms的三个可用区,性能影响在可接受范围内。如果可用区间延迟超过2ms,建议优先考虑Colocation部署而不是强行跨区。
5.3 安全合规:镜像扫描、权限模型与审计日志
安全合规是银行项目的生命线,云原生架构给安全策略带来了一些新挑战。镜像作为新的交付单元,如果管理不好会直接成为漏洞入口。我们在Harbor中集成了Trivy扫描器,所有镜像入库前必须通过安全扫描,包含中高危漏洞的镜像会被阻断部署。基础镜像每周一自动重建,拉取最新的安全补丁。
权限模型方面,我们严格执行RBAC(基于角色的访问控制)。平台管理员、开发工程师、数据科学家、运维值班员,各自拥有不同的权限。有一个经验是:不要为了图省事给所有用户绑定cluster-admin权限,哪怕是最核心的人工也应该遵循最小权限原则。我们曾因为某运维同学持有过高的权限误删了一个Namespace里的所有Pod导致批处理链路中断3个小时,自此以后权限收紧成了硬性要求。
在审计方面,K8s的控制面日志、容器日志、引擎日志、数据访问日志全部接入集中式日志平台,实现对“谁在什么时间通过什么路径访问了哪些数据”的完整留痕。这些审计能力是银行客户的硬性合规要求,也是出了问题快速定位的关键。
6. 踩坑实录:我在落地过程中遇到的六个真问题
6.1 坑一:Spark作业的Pod被优先抢占导致大量重试
问题很典型:我们发现业务高峰期,核心Spark作业的Executor Pod频繁被Evict,任务反复重启,K8s事件里全是Preempting或Evicted的记录。
根因是默认优先级的设置。当时除了大数据作业,同一个K8s集群上还跑着在线微服务。在线服务的Pod在资源紧张时会被调度器优先保障,而我们的Spark Executor属于批次型Pod,默认优先级不够高,在节点内存水位超过阈值时就被Kubelet抢先回收了。
解决方案是引入PriorityClass,把Spark Driver的优先级设置为最高级别,Executor设置为中优先级,同时为Spark作业单独创建专用的节点池,节点上只允许大数据任务调度,从物理层面避免和在线服务争抢资源。改完之后,Pod被驱逐的现象基本消失。
6.2 坑二:跨节点Shuffle网络开销远超预期
前面提到过,存算分离后Shuffle性能是个大坑。实际测试时,一个中等数据量的Join任务,Shuffle阶段耗时占据了整个任务的60%以上。后来排查时发现,Spark的External Shuffle Service在K8s模式下默认是关闭的,Executor每写完一个Shuffle文件就要等Reducers来拉取,而且文件的落地位置又是远端对象存储,性能简直是一场灾难。
解决办法分两步:一是开启K8s节点上的Shuffle DaemonSet,让Shuffle中间数据写到节点本地的NVMe盘上;二是给Executor配置了较大的临时存储空间,让每个节点上的Pod能复用本地磁盘。改完之后,同一任务Shuffle阶段的耗时下降了75%,核心任务总体耗时从原来的25分钟缩短到了9分钟。
6.3 坑三:镜像仓库在高峰期变成性能瓶颈
业务切换过半后,每天的凌晨调度大幕拉开时,节点池扩容、新Pod批量拉起,所有节点同时从Harbor拉取镜像,仓库带宽直接打满,导致Pod调度严重变慢,部分任务因拉镜像超时被K8s反复重试。
这也是云原生大数据场景很常见的坑。物理机时代不存在这种问题,因为程序已经在机器上了;容器化之后每次调度都要先把镜像传过去。
我们的解决方案是给Harbor配置了P2P分发组件(类似Dragonfly的机制),镜像先由P2P集群的超级节点拉取,再通过P2P方式分发给各工作节点,大大减轻了镜像仓库的带宽压力。同时,给节点池的节点镜像增加预热功能——在扩容前先把常用镜像拉到目标节点上,Pod启动时间从原来的一两分钟压缩到10秒左右。
6.4 坑四:JVM参数跟着旧配置走,容器内存频频爆掉
从物理机迁移到容器环境,最隐蔽的坑是JVM的默认堆内存与容器内存的错配。传统模式部署时,我们习惯在脚本里写-Xmx32g,因为物理机内存大随便配。上了K8s之后,Pod的Limit如果设成8GB,但JVM启动参数还是-Xmx32g,意味着JVM会不断尝试占用超出容器Limit的内存,触发OOMKilled。
这是个非常经典的内存超卖问题,几乎所有做容器化Java应用的团队都会遇到。我们在迁移时一开始没注意到这个细节,导致晚上跑批时一批Executor连续被OOMKilled,任务大面积失败。
后来统一采用K8s推荐的策略:JVM参数不再硬编码,交给容器运行时自适应。在Spark Operator配置中添加spark.kubernetes.memoryOverheadFactor来预留JVM之外的内存空间,同时开启-XX:+UseContainerSupport,让JVM根据容器实际内存自动计算堆大小。调整之后,OOM问题基本绝迹。
6.5 坑五:对象存储的List操作成为元数据瓶颈
迁移后我们发现了一个奇怪的现象:任务数量激增后,部分Spark查询的Plan生成阶段耗时就占了总任务的30%。排查发现,Spark读取数据时需要向元数据服务或者对象存储发出大量的List请求,确定路径下有哪些文件。因为我们的数据文件在对象存储中碎成了几万个小块,每次查询都要对该路径执行全量List,对象存储对List请求的处理能力远不如HDFS的NameNode本地操作。
解决方案用到了Iceberg的元数据索引能力。Iceberg表默认维护了一套快照化的元数据,Spark查询直接读取元数据清单,不需要再逐个List文件。配合定期的Compaction把大量小文件合并成中等大小的文件,查询计划生成的耗时降到了原来的10%以内。
6.6 坑六:自动伸缩的阈值设置不当造成小任务反复震荡
启用了集群的弹性伸缩之后,又遇到一个让人头疼的问题:自动扩缩容的阈值设置不合理,导致伸缩频繁,作业调度一直在折腾。
细节是这样的:我们一开始用的是基于CPU平均利用率的方式,阈值为70%的扩容、30%的缩容。但大数据任务的特点是会出现周期性波峰——Executors启动瞬间CPU极高,任务进入Shuffle阶段CPU又会下降。这就造成集群不断重复“扩容-缩容-再扩容”的震荡。每天凌晨的跑批时段,集群规模忽大忽小,白白消耗了很多调度资源不说,还影响任务稳定性。
后来我们对伸缩策略做了多项调整:第一,把评估周期拉长到5分钟,避免瞬时波动触发扩容;第二,设置了扩容冷却时间和缩容冷却时间,防止频繁翻转;第三,按时间窗口设置了不同的基线规模,比如凌晨跑批时段强制保底几十个计算节点,白天低峰时段可以缩到个位数。调整后伸缩次数从每天几十次降到了个位数,集群稳定性大幅改善。
7. 落地效果复盘与后续演进思路
改造完成后的效果,用数据说话可能更加直观:
- 资源利用率从平均21%提升到了57%左右,高峰和低谷的资源错峰复用不再靠人工干预。
- 集群扩容时间从原来的一到两周缩短到了分钟级,节点池动态扩缩容可以做到5分钟内完成一批新节点的加入。
- 核心离线任务的平均执行耗时缩短了38%,其中存算分离和Shuffle本地化贡献了最大的性能收益。
- 运维方面的人力投入明显下降,以前三个运维人员专职维护几十台物理机集群,现在用同样的团队能同时管理多个租户、多套集群。
这些是纸面上的收益。如果要说个人感受最深的变化,其实是交付节奏和试错成本。以前业务方提了一个新需求,要评估资源够不够用、要协调排期、要规划机器采购,一套流程走下来大概率以周为周期。现在直接在Namespace里把资源配额一划,环境几分钟就出来了,数据团队可以快速做原型验证,试错了也不用担心影响生产。这种变化在组织效率上带来的价值,很难用一个简单的数字来衡量。
后续我们计划在两个方向上继续演进。一是往Serverless化的方向走,让业务方不再感知集群概念,只用提交SQL或PySpark作业,资源和调度完全由平台侧自动管理;二是在数据湖和数据仓库一体化上持续打磨,强化Iceberg在实时入湖、增量计算、跨引擎数据共享这些场景的能力。云原生架构的改造不是终局,反而是后续一切数据能力建设的基础。
写到这里想分享一个经验:做这类架构改造,技术难点肯定有,但更难的是团队思维的转变。从“守着机器”到“面向容器”,从“手动扩容”到“弹性伸缩”,从“平台能跑就行”到“平台要有SLA”,每一步都需要整个团队形成新的共识。如果你正在经历这个阶段,我的建议是:先完善可观测性和链路监控,它决定了你在半夜被叫起来排查问题时能不能快速定位;再做好隔离和配额,它决定了平台稳定性的底线;最后才是性能调优和功能增强,那是活下来之后才能考虑的事。