一、背景介绍
首先对知乎做一个简要介绍。
1. 高质量的在线问答社区
用户可以在知乎上进行提问、回答、点赞和评论,获取有特色且高质量的内容。
2. 混合云架构
使用了多个云厂商的服务,包括 LaaS、SaaS、PaaS 平台等,这也提高了降本的难度。
3. FinOps
知乎内部 2022 年开始建设一套经济化计费体系——FinOps,以此系统为抓手推动公司内部降本实践。FinOps 的运用强调了降本增效不仅是研发侧的问题,更多的是与组织相关的,是需要团队之间共同合作才能达成的目标。
二、FinOps 驱动降本
1. 企业内降本的挑战
(1)供应商众多:需要多个云厂商,会采购不同的资源,比如物理机、虚拟机等一些基础资源,同时也会采购 SaaS 服务使用的资源,资源种类和供应商众多都为管理成本带来了困扰。
(2)组织架构:为不断提高组织效率,公司组织架构会在一定周期内进行调整。但是在统计计费成本时,计费对象通常就是一些团队和组织,若企业组织架构发生调整,计费对象就可能消失了。此时,我们需要一种妥善的手段去应对组织架构调整,保证计费体系的连续性。
(3)难以持续:降本增效虽然在过去已经有无数人进行了实践,但是对于运动式的降本增效,虽然在半年或三个月有了收益,但是一段时间后,组织结构成本又逐渐提高。也就是说,整个降本增效并不是一个可持续的过程,根据 Gartner 的统计,只有 11% 的企业能够在三年的时间粒度上实现持续的成本降低。我们在执行降本增效的过程中不仅要考虑如何通过运动式的降本增效把成本降低,还要考虑细水长流,实现可持续的降本。
2. FinOps
FinOps 是通过财务来驱动研发,两个部门进行协同,共同实现组织降本增效的一种文化。它提出了六个原则:团队协作、成本节省人人有责、中心化团队驱动 FinOps、实时报表助力决策、业务价值驱动决策、灵活利用云上成本模型。这些原则可以浓缩成三个重要的方面:
- 第一方面需要真实且透明地度量成本,其中强调真实,难点在于真实刻画业务活动中的开销,比如我们平台团队在向业务提供服务时并不是直接采购一个硬件给业务方使用,而是在上面架构一个平台,平台再提供业务能力,让业务去用而产生成本,真实的开销不好计算,因为常常存在多个业务共享一个平台。另外还要具有可比性,公司内部提供的服务和公司外部提供的服务应该是一样的,我们需要具备相对的竞争力,如果我们提供一个数据库的成本远高于在云上购买一个数据库,那么就更应该去云上购买这个服务而不是由组织内部建设,所以我们的成本应该是可以和业内的主流成本去对标,不应该有特别大的差别。透明是指我们需要让成本越精细越好,需要看到每一项资源的准确开销,同时团队间也能看到对方的成本,从而驱动业务进行降本。以上均为成本度量的问题。
- 第二方面是中心化团队的支持,我们在实施降本的过程中,必须要有一个专门的全职团队来负责整个计费和整个 FinOps 的建设,这个团队需要得到老板的授权,通过技术管理运营的手段去驱动降本的步伐往前走。因为降本是整个公司的目标而不是某个人某个团队的目标,需要专门的团队进行协调沟通。这将影响到最终的降本成果。
- 第三方面是需要对降本的结果进行阶段性总结,包括绩效考评,比如在降本前需指定降本目标,在一个周期完成之后,回顾这个目标,谁做的好,谁做的不好,好的原因是什么,不好是出现了什么问题。我们将好的实践推广出去,不好的团队也需要有惩罚措施,奖惩分明,使降本动作是可持续的。
3. 成本分摊体系
从费用来源来看,有两种分摊方式:直接计费和二次分摊。直接计费,例如业务需要存储图片,在云上买了一个对象存储,这个对象存储只有一个业务方在用,所以我们可以将这个云上存储的账单直接分给业务,这种方式为直接计费。这种方式在公司内部并不多,更多的时候服务是由平台团队间接提供的。比如容器团队购买一批虚拟机或物理机,在这上面去搭建 PBS 服务,业务使用的其实是 PBS 上的这些容器,并没有直接去使用这个机器。同时为了保证业务的扩容缩容等特殊需求,平台需要进行池化策略,比如一些业务会共享资源池,而且这个资源池可能会有 buffer,这些冗余也会提高成本。这时就不能采用直接计费的方式,而是需要通过一些计费形式来额外地和业务方去核算。
在上图右侧图中可以看到,最上面是最原始的一些成本来源,比如机器、SaaS 服务,或者从第三方供应商采购的软件或硬件;中间是平台层,包括数据团队、技术架构团队,容器、DB 和缓存,甚至 MQ 等都是属于 PaaS 层,PaaS 也就是平台团队采购这些机器,包装成服务提供给业务使用,这时就需要进行成本的二次分摊;最下层就是业务团队,又分为很多级,每一级有自己的分摊逻辑。
从计费方式上来看,分成固定单价模型和百分比分摊模型。固定单价模型顾名思义,以 HDFS 存储为例,根据用户用的 GB 或 PB,用每台的单价乘存储数量即为成本。百分比分摊模型的含义是,比如两个业务一起购买了一个向量数据库服务,但是因为用量较小,约定这个数据库的成本一人一半,这样就是各自 50% 的分摊。在实际中需要根据资源的特点选择合适的计费方式,在设计时可能会忽略的一个问题——激励相容问题,指业务的降本动作和它的计费模型应该是匹配的,计费模型应该为业务的降本动作提供动机。以百分比分摊为例,比如两个业务约定各 50% 的分摊比例,但是有业务把它的存储和使用做了很好的优化,但是因为计费模式还是 50%,因此团队的成本并没有降低,此时我们给了业务一个负反馈,虽然降低了成本提高了效率,但在账单上没有任何的体现。如果采用固定单价模型的话就会有所体现,比如按照存储量来计费,当存储量降低的时候,这个降本的收益会体现在团队的成果上面,这时业务人员就会更有动力进行降本的动作。这也是在做计费体系的时候需要考虑的问题。
第三个是使用按 Usage/ Capacity 付费的问题,两种计费方式,第一种是按 Usage 计费,跟云上很相似,付费与用量成正比;第二种是按 Capacity 计费,即按照资源包付费,比如买一个 RDS 实例,用的时候即便磁盘没有用完,但仍需要支付一个实例的费用。在我们公司,计费系统通常采用容量计费的方式设计。如果按使用量计费,由于每个服务会根据实际需求动态扩缩容,每个容器的规格和数量都在不断变化,为了准确计算资源消耗,需要对每个容器的使用时间和消耗量进行积分计算,这种按量付费的模式会增加计算负担,并且由于变化太快不利于平台团队进行容量规划。因此,大多数情况下,我们采用容量付费模式。
在这种模式下,业务团队需要预先申请资源配额,比如团队的项目一共需要多少内存和存储空间。我们的计费方式是按照申请的存储容量乘以单价进行收费,无论资源是否被完全利用,费用都是相同的。这样,业务可以根据实际需求调整配额,并通过设置使用上限来控制成本。由于调整配额的成本较高,业务通常会减少调整频率,使得成本在一定时期内保持稳定。对于平台方来说,这种容量计费方式有助于更好地规划资源,可以提前采购或归还机器,从而实现更高效的资源管理。
4. 运营体系
在建立了完善的计费体系后,我们应能够准确计算每个业务和项目的实际开销。然而,要真正实现降本的效果,还需要一套完善的运营体系。这套体系包括成本预警、异动归因和定期会议三部分。
成本预警指的是当成本出现较大波动,特别是在成本突然上升并超出业务预算时,系统应发出警报,提醒业务团队及时干预并解决问题。
异动归因是指当业务团队发现成本变化时,最需要的是了解成本波动的原因。成本的变化可能超出预期,甚至可能是由某个特定功能引起的额外成本。在这一方面,FinOps 平台能够通过丰富的计费项目帮助业务快速分析成本变化的原因,避免手动归因带来的巨大人力消耗。类似的,我们在 HDFS 上也采用了一套基于归因算法的体系。例如,某项目组在一周内的 HDFS 存储成本下降了 1.85%,而其中一个项目的存储减少了 29%,对整体成本下降贡献了 164%。通过对各项贡献度的排序,我们可以迅速找出导致成本变化的原因。
定期会议也是成本运营的重要环节。通过这些会议,我们可以定期回顾每个业务团队的降本措施,分析其成效和变化。此外,管理层的支持至关重要,尤其是在绩效考评和激励机制上,为这些降本措施提供支持和动力。通过这样的运营体系,我们能够更加有效地驱动成本管理,助力业务的可持续发展。
三、技术驱动降本
在大数据领域,从 2022 年到 2024 年,我们采取了多项措施,逐步降低了整体成本,主要分为存储优化和计算优化两大部分。其中,有四项措施在成本收益比方面表现尤为突出,存储优化方面包括 EC(Erasure Coding),Zstd 压缩。计算优化方面包括 Spark 自动调参,Remote Shuffle Service。下面对这些优化做详细介绍。
1. EC(Erasure Coding)
EC(Erasure Coding)技术起源于舟山,最早应用于通信行业。当信息在传输过程中,信道可能会受到干扰,导致数据的损坏或丢失。为了确保数据的可靠传输,舟山码策略问世,用于在数据传输时添加适量的冗余,从而提高数据的可靠性。EC 正是基于这一原理的算法,其常见实现方式之一是 RS(Reed-Solomon)编码。
EC 的基本原理是将数据分成多个块,并通过矩阵运算为每个数据块生成相应的校验块。如上图所示,数据块包括 d0、d1、d2、d3,经过 EC 的计算后生成了两个校验块 c0 和 c1,最终形成六个数据块。这意味着,原来的数据冗余了 0.5 倍,但系统可以容忍任意一个数据块的丢失,并可以通过矩阵运算将数据还原回来。
在大数据存储领域,EC 的降本效果显著。传统的分布式存储通常为了保证数据的可靠性,采用三副本冗余策略,也就是说,写入 1GB 的数据时,底层会存储 3GB 的数据量。而通过 EC 算法,仅需 1.5 倍的冗余即可提供相同的可靠性,这不仅大幅减少了存储需求,还降低了存储成本,特别是在大规模数据存储环境中效果尤为突出。
然而,使用 EC(Erasure Coding)技术也存在一定的代价。首先,EC 的计算需要额外的 CPU 资源,因此对特别高频访问的数据(即“热数据”)并不适合进行 EC 转码。为了有效利用 EC,必须对数据进行评估,判断哪些数据适合进行 EC 转码,哪些不适合。比如,刚写入的数据和频繁读取的数据由于可能带来额外的 CPU 消耗,通常不适合立即进行 EC 转码。因此,EC 主要适用于“温数据”和“冷数据”。
在实际应用中,我们对 EC 的性能进行了测试,使用了 DFSIO 读写工具来模拟操作。测试结果如右图所示,蓝色线代表传统的 HDFS,橙色线代表使用 EC 的 HDFS。我们采用了 RS-6-3 算法,将一个数据分成九块,其中六块为数据块,三块为校验块,这意味着即使有两个块(或两块磁盘或两台机器)发生故障,数据仍能恢复。随着客户端读写并发性的增加,EC 的读写性能出现了延迟,这是因为在更高的并发度下,需要更多的机器参与读写操作。相比之下,传统 HDFS 可能只需一两台机器来完成读写,但 EC 能够将 I/O 负载均摊到更多节点上。
对于新写入的数据或即将过期的数据,不建议使用 EC,因为 EC 编码需要消耗大量算力,而这些数据的转码并不划算。另外,EC 转码还可能导致小文件问题,因块数量的增加(如从六块增加到九块)而给 NameNode 增加压力。因此,优先选择相对较大的文件进行 EC 转码,以减少对系统的负担和资源消耗。
在引入 EC 方法后,为了有效管理和执行 EC 转码任务,必须构建一个专门的 EC 服务来自动化完成这项复杂的工作。由于数据量庞大且数据状态不断变化,仅依靠人工是无法完成的,尤其是在新数据不断生成和旧数据逐渐过期的动态环境下。因此,全自动服务是必不可少的。
整个 EC 服务体系分为两个主要部分:首先是元数据仓库,它负责为整个 Hive 表建立详细的元数据画像。这个画像包含了诸如 Hive 表的分区大小、文件数、平均文件大小、访问热度,以及分区是否已经过 EC 处理等关键信息。通过这些维度的综合评估,系统可以筛选出最适合进行 EC 转码的分区。筛选出来的分区随后会被交由 EC Worker 服务进行处理。EC Worker 是一个自研的专门执行 EC 转码的服务。这个服务的设计考虑了数据的可靠性,确保数据在转码过程中不会丢失,并具备严格的事务性保障。这意味着对一个目录进行转码时,要么所有数据成功转码,要么如果出现问题则整个任务回滚,以避免中间状态或数据丢失的情况。
此外,EC Worker 服务还需要具备幂等性,即在文件已经过一次 EC 转码后,系统需要能够判断并跳过已处理过的目录,避免重复转码。特别是在目录内容因底层重写而发生变化的情况下,系统必须能够准确识别这种变化,并根据当前状态决定是否再次执行 EC 转码。
总之,EC 服务的成功运行依赖于其自动化、事务性和幂等性的特性。这些关键因素确保了整个 EC 过程的可靠性和高效性,使得大规模数据环境下的 EC 转码任务得以顺利完成。
整体收益来看,EC 方法显著优化了存储资源的使用效率。通过将三副本降低为 1.5 副本的方式,在确保数据可靠性的前提下,没有因 EC 而产生任何数据损失。在 2023 年初,该项目已节省了 25PB 的存储容量,并将在未来继续产生效益。然而,在实践过程中也遇到了一些挑战。例如,使用的 Hadoop 3.1.x 版本存在一个 bug,具体表现为在 EC 块丢失并重建时,可能会导致重建的块出现脏数据或乱码,而带来数据丢失的风险。
2. ZSTD 压缩
ZSTD 是 Meta 开源的一种压缩算法,相较于传统的压缩算法如 Snappy 和 LZ4,它在压缩率和压缩速度方面取得了很好的平衡。如上图中的官方性能测试结果所示,ZSTD 的压缩率比 Snappy 提升了 30%,而压缩性能保持不变。常用的压缩方法是 ZSTD 1.5.6 版本中的 ‘fast=3’ 的参数设置。在知乎的数据处理中,主要采用 Parquet 的存储格式。然而,由于部分早期的表未在 Parquet 格式上进行压缩,而较新的表则使用了 Snappy 压缩算法。经过测试,将这些表从 Snappy 压缩算法转换为 ZSTD 压缩后,整体存储将减少约 30%;如果原来的 Parquet 表未进行压缩,整体存储将减少 50%~60%,收益是非常可观的。
在对表的新产生数据更换压缩算法时,需要考虑 ZSTD 的兼容性问题。因为此压缩算法较新,许多早期版本的 Hive 和 Spark 可能并不支持,因此需要将一些支持的补丁回移到 Spark 2 和 Hive 2.1 版本中,确保表可以被下游消费和读取。
此外,对于历史数据的压缩算法处理,有两种方案可以考虑。方案一是对表进行一次 ETL 处理,通过 INSERT OVERWRITE 对表进行重写。然而,这种方法存在处理速度慢的问题,因为它需要将所有数据读取出来,进行反序列化和计算,然后再重新序列化回去。这个过程还存在一定的风险,例如在业务演化过程中表的 schema 可能发生变化,重写可能改变底层的数据结构,导致预期之外的风险;除此之外,重新 ETL 可能会改变数据的分布,导致压缩率下降。
因此,更为理想的方案是采用一种能够绕过 SQL 对文件直接进行处理的算法。Parquet 文件的存储结构本质上是一种行列混存的格式,具体来说,对于一个大的表格,先按行进行切分(例如每一万行切一块),每个块称为一个行组。每个行组内部包含多个列,这些列数据按照列存的方式进行组织和存储。因此,一个 Parquet 文件可能包含多个 row group,每个 row group 是按行组织的数据组,而每列数据在具体存储时是通过 page 的方式进行压缩。压缩操作发生在 page 层次上,而非对整个 Parquet 文件进行压缩。我们希望将旧的 page 读取出来,用老压缩算法解压缩后,再用新的压缩算法重新压缩,这在官方的 Parquet 实现中是可行的,并且官方提供了名为 parquet-tools 的工具,其中定义了一部分对底层 page 进行操作的 API,我们基于这些 API 开发了 ZSTD 转化工具,该工具能够非常高效地对底层文件进行重写,而不涉及到 schema 的变更,也无需对数据进行任何反序列化,只是单纯地将数据取出并重新压缩。
这种方式显著提高了处理效率,能够在较短时间内完成对历史数据的处理,同时还可以在转码过程中对一些小文件进行合并。根据整体估算,ZSTD 每分钟可以处理 30GB 的数据,在一个月中节省了 10PB 的存储,新的作业已默认采用 ZSTD 压缩算法。同时,ZSTD 和 EC 是可以并存的,它们在不同层次上进行存储,收益是可以乘算的。
3. Spark 自动调参
Spark 自动调参是许多公司面临的一个难题,主要难点在于如何合理定义 Spark 作业所需的资源。对于业务数据人员,自动调参可以帮助他们将大数据调参过程自动化。该系统基于作业的历史执行数据,包括真实的内存和 CPU 消耗,来计算出最适合作业的参数配置。
整个自动调参系统分为两部分,作业画像系统和自动调参服务。作业画像系统负责收集并保存每个作业运行的各项指标数据;自动调参服务基于这些历史指标数据和预先设定的调参规则,来决定每个作业的提交参数。
作业画像系统的关键指标采集包括 CPU 和内存使用率、GC 耗时、Shuffle 数据量、HDFS 读写统计值。系统采用了 jvm-profile 和 sparklens 开源组件,分别从 JVM 和 Spark 端采集数据,这些数据汇总后为自动调参提供基础支持。在提交 Spark 作业时,通常会过多申请内存资源,实际使用的峰值可能低于预期。通过这种自动化的采集与分析,系统能根据历史数据自动调整内存分配,避免资源浪费,提高资源利用率。
作业调参系统通过对 Spark 作业的 spark-launcher 进行劫持,在提交作业前先请求调参服务。调参服务会根据作业历史的执行数据,推测最佳的 CPU 与内存比以及执行器数量,并将优化后的参数返回给长字符。整个过程采用启发式算法,逐步逼近最佳内存配置。例如,作业最初申请了 30GB 内存,但实际峰值仅为 16GB,系统会先将内存下调至 24GB,观察执行耗时和 GC 情况,如果没有显著变化,则继续调整,直到找到最优参数。这个渐进式的调优过程能够逐步优化资源配置,提升作业效率。
在公司实践中,经过 3 个月的优化,整体作业资源节省了约 30%。上图中右侧图中红色和蓝色曲线分别显示了 CPU 和内存的优化量,以核时和 GB 时为统计单位。这种调参服务现已覆盖所有 Spark 作业,显著提升了资源利用效率。
4. Remote Shuffle Service
在 Spark 中,Shuffle 是数据处理的重要环节。默认情况下,Spark 提供了三种 Shuffle 方式,其中的 Start Shuffle 在实际使用中较少被采用,因为一旦出现故障重启,Shuffle 数据会丢失,必须重新计算。一般情况下,External Shuffle Service (ESS)是更常用的方式。
ESS 的工作原理是:在 NodeManager 上启动一个 Shuffle 服务,Spark 的 Executor 将 Shuffle 数据写入本地磁盘,然后下一个计算任务的 Reducer 通过 NodeManager 读取这些数据。
ESS 的优势在于,即使 Executor 关闭,数据仍然保存在磁盘上,不需要重新计算,只需恢复数据,极大地提升了稳定性。
然而,ESS 也存在一些问题。比如 Spark Shuffle 可能涉及大量分区,每个分区在 Shuffle 时都会生成一个文件。HDFS 磁盘在使用时,IOPS(磁盘每秒读取/写入操作数)较低,读写性能不佳。而 ESS 会导致大量数据的读写,大量的磁盘 IO 资源浪费在 Shuffle 过程的等待时间上,而不是用于真正的 CPU 密集型计算。
如图所示,这个任务的 Shuffle Read 数据量为 9.5MB,但生成了 1.8 万个文件,总共产出 11 分钟,这是典型的磁盘处理性能。但其实应该只需要几秒钟,这就是 RSS 引入的背景。
RSS(Remote Shuffle Service)实质上为 push based shuffle,和 ESS 不同,它是由 executor 主动将数据推送到下一个 Reducer 所需的存储位置,也就是说数据写入的时候会按照读的要求做一定的合并和整理。
在众多的开源 RSS 中我们选择了阿里的 Apache Celeborn,经过一系列的测试,Celeborn 的稳定性和性能都是比较好的。Celeborn 的一个重要收益是将 shuffle 过程中大量的随机读写操作转化为顺序读写操作。写入的时候会按照需求对 partition 做合并,数据已经被按照顺序整理好,可以进行直接读取。这样减少了随机读写操作,提高了磁盘吞吐量。此外,Celeborn 还支持 PartitionSplit 的功能,当发现 partition 太大的时候可以进行主动的分裂,避免一个 worker 处理过多的数据。Partition 分裂这一功能在平滑升级中非常有用,例如当某个节点需要维护或下线时,Celeborn 可以将该节点正在处理的 Partition 分裂并转移到其他 worker,从而避免新数据经过该节点。这使得在不中断业务的情况下,可以平滑地处理节点下线或进行滚动升级,确保集群的稳定运行,对业务不产生任何负面影响。
统计结果表明,Spark 作业的 shuffle read 耗时经过逐步将作业接入到 Celeborn worker 之后有了明显的下降,具体来说,整体的 shuffle read P99(即 99% 作业的最大耗时)平均损耗降低了 30%。
四、总结与展望
整体来看,我们主要通过两大手段保证了降本增效的效果,即 FinOps 系统建设和运营,以及技术优化,实现了可持续的降本。
未来规划,首先会推动 Hive 引擎逐步向 Spark 引擎迁移;其次利用主流方法,如 gluten+velox 进一步提升计算效率;同时探索通过弹性EMR 和固定资源池混合的方式提高资源利用效率。