1. 上云之路
在上文(详情戳此处)中,我们探讨了云原生的、存储和计算分离的数据湖架构为何将会成为数仓分析技术的演进的趋势。 从企业方的应用角度来看 ,对云上的分析类产品有以下这些共同的诉求:
弹性伸缩,满足业务高峰需求的同时控制整体 TCO;
企业级安全,通过产品本身的权限管控和基础设施本身的安全来保证用户数据安全;
和 on-premise 版本一样,在高可用、 多租户隔离、高吞吐、负载均衡等方面令客户满意,且监控运维负担较低;
自助式的产品体验。
我们的企业级数据分析平台最早依托于 Hadoop 生态建设,虽然在上述的安全性、可用性、高性能等方面已经做了不少投入,但是在云环境中特有的安全、资源治理、存储计算天然分离等方面的挑战面前,我们的 Hadoop 原生的数据分析平台遇到了一系列亟需解决的问题。 粗略总结下来,这些问题主要涵盖了存储、计算、网络、安全、可用性和费用等方面。 我们将对这些问题做了简单的梳理,如下图所示。在接下来的篇幅中,我们将主要介绍这些方面遇到的具体问题,并给出我们的思考和经验。
2. 计算:面向伸缩的计算引擎
云上的成本包括:计算的成本、存储的成本、网络的成本、应用和管理成本。其中,计算的比重相对较大。 因此,为了降低 TCO,通过使用动态伸缩的虚拟机、或者 Serverless 的服务提供算力,是上云之路中的必然选择。
Kyligence 企业级数据分析平台的主体源自 Apache Kylin,也最大程度集成了它的可插拔的模块设计。在 Apache Kylin 中,离线引擎使用的是 MapReduce,在线计算引擎和存储引擎主要使用的是 HBase。但是无论是 MapReduce 还是 HBase 在云上要伸缩都不是很容易。以 HBase 为例,不仅在增加节点的时候配置复杂,难以扩容,更重要的是,在缩容的情况下,则部分 Region 需要被转移到其它继续存活的 Region Server 上,转移期间这部分 Region 处于不可用的状态。这对于一个需要连续保持服务高可用的生产系统来说是难以接受的。
由于整体的架构是可插拔的,在云上,我们使用 Standalone Spark 替换了 MapReduce,用 Parquet(on object storage) 取代 HBase 来存放数据。通过监测 Spark 集群的负载,系统预判需要对现有的 Spark 集群进行扩容还是缩容。对于 Standalone Spark 来说,增加新的 Worker,或者减少现有的 Worker,只需要进行简单的注册,不会造成服务的中断,因此非常适合计算资源的动态伸缩。
由于使用了 Parquet on Object Storage,因此存储也不再像 HBase 那样需要专门的节点,对象存储作为完全托管的服务,让我们不需要去操心存储资源的伸缩。
关于容器化
以 K8S 为代表的容器管理平台能够集中所有的计算资源调度,能够让在夜间需求量较大的离线计算任务和在日间需求量较大的应用计算任务共存于同一套平台中,通过错峰的资源使用,最大化计算资源的利用率。同时,容器管理平台带来的负载均衡、故障检测和恢复、运维管理等方面的优势也比较具有吸引力。目前我们已经在开源社区进行相关的原型验证,但是还并未在企业级产品中默认使用。
3. 存储:对象存储带来的挑战
在数仓都构建在 on-premise 环境中的时代,HDFS 被 HBASE、HIVE、PRESTO、KYLIN 等诸多数仓或分析类系统视作主要的文件存储系统。作为一个不完全兼容 POSIX 的文件系统,HDFS 已经放弃支持了很多 POSIX 特性,以保持简单高效,其中最典型的一个例子就是 HDFS 的 append only 的特性。不过,HBASE、HIVE、PRESTO、KYLIN 等系统在设计的时候就很好的接纳了 HDFS 的这些妥协设计,在 HDFS 上大家相安无事。
当这些系统要被迁移到云上时,由于这些软件对文件系统的访问都是走 HDFS 接口,那么一种很自然的做法是在云上搭建一套 HDFS 文件系统来支撑。在 Databricks 的一篇文章中(https://databricks.com/blog/2017/05/31/top-5-reasons-for-choosing-s3-over-hdfs.html),作者详细分析云上使用对象存储,对比在云上搭建 HDFS 的方案,在弹性、费用、可用性、持久性等方面的的全面优势。为了让我们的用户享受到低 TCO 和存储的高可用性,我们也选择了使用对象存储。
Azure 的 Blob Storage 和 AWS 的 S3 都是对象存储,他们都用「对象」来代表文件和目录。在存储方式上,也不是像 POSIX 或者 HDFS 那样用目录树组织文件,而是用一种类似 KV store 的形式来组织(有点像 HBASE,用一个 KV 代表一个文件)。社区也早有人为对象存储适配了 HDFS 协议:目前,Hadoop 社区官方文档上认为兼容 HDFS 协议的文件系统(基上都是对象存储)的有 Aliyun OSS、Amazon S3、Azure Blob Storage、Azure Data Lake Storage(ADLS)、OpenStack Swift 和 Tencent COS。
虽然如此,这些毕竟只是「HDFS Compatible」,在一些比较关键的特性上,这些伪装成 HDFS 的文件系统和真实的 HDFS 还是存在较大差别 ,这些差别包括:
HDFS 保证当文件被删除、创建、重命名后,操作的影响可被所有后续调用立刻感知,即「一致性」,但是对象存储并不一定保证;
在 HDFS 中,Rename 是一个非常高效的操作,但是在对象存储中往往是通过 List + Copy + Delete 来实现的,比较低效;
在 HDFS 中,数据的访问具备一定的 locality,计算调度框架可以优先把计算安排在本地性更强的节点进行。但是使用了对象存储后,天然不具备本地性。
在 HDFS 中,目录的 list 甚至递归的 list 是一个正常的操作,但是在对象存储中效率并不高,而且如果涉及大量的对象存储 API 调用(对对象存储的所有访问都是通过 API 调用来实现的)调用还要额外收费;
HDFS 会在目录和文件维护 Timestamp 信息,且在文件操作时有比较明确的时间戳变更定义,但是对象存储并不一定保证;
在对象存储中,对同一个目录下的文件的访问有并发限制,以 S3 为例,官方的说法是「You can send 3,500 PUT/COPY/POST/DELETE and 5,500 GET/HEAD requests per second per prefix in an S3 bucket 」,而 HDFS 并无这种限制;
HDFS 保证删除、创建、重命名文件必须都是原子的,即「原子性」,但是对象存储并不一定保证;
文件和目录上需要存储 ACL 等权限信息,但是对象存储并不一定保证。
我们将对其中的几个主要问题进行展开:
1)一致性问题
从实际经验来看,最容易遇到的还是一致性相关的问题。一个最普通的例子是,前序步骤在文件系统中创建了一个文件,后续步骤去访问它的时候找不到该文件,系统报错。
为了解决这种一致性问题,开源社区中有 S3Guard 的方案,它的大体思路是在一个独立的 DB 中维持一个 Consistent View 表,来记录文件和目录的变化。文件写入成功后,会插入一条信息到表中。当用 List 操作去列举对象存储上面的文件时,会将结果与该表中的记录进行对比,如果发现列举结果不完整,就会等待一段时间再去列举,直到二者信息一致才会继续其他操作。这种方式的缺点也比较明显,即可能带来性能的严重下降,同时也可能会增加一笔可观的 DB 成本。 针对这个问题,我们梳理了我们产品中所有可能受到一致性影响的点,发现所有的问题都能通过在 HDFS 客户端植入简单的检查-等待-重试策略来规避,这种做法也与 AWS EMRFS 中 HDFS 客户端的行为接近。采用这种简单的策略,我们既摆脱了对象存储一致性的问题,又避免了单独维护一个 Consistent View 的成本。
2)Rename 问题
如上文所述,Kyligence 使用 Spark 加工数据,并把加工好的数据存储到对象存中供分析使用。无论是 Hadoop 还是 Spark,他们在运行每个作业的时候都有多个 Task 并行独立地输出结果文件。在每个作业的结果输出上,Spark 需要考虑 2 个问题:
Task 可能运行失败,即每个 Task 对应一次或者多次 Task Attempt,更糟糕的是,同一个 Task 的不同 Attempt 可能会同时执行。因此,不同 Task Attempt 的输出不能混在一起,Spark 最后只认成功的那次 Task Attempt 所输出的结果;
Job 可能运行失败并且重跑,不同 Job Attempt 的输出不能混在一起。
因此,一直以来 Spark 都是使用下图(图片来自 CSDN )所示的 FileOutputCommitter,这种 Committer 会为每个 Task Attempt 创建一个临时的目录(1),在 Task Attempt 成功后对它的输出目录进行重命名(2)。同时,整个 Job 的输出目录一开始也是一个临时目录中,在 Job 完成时,对 Job 的输出目录进行重命名(3)。
在这种模式下,每个 Task 的输出都要经历两轮 Rename 才行。当有大量 Task 写入时,即使所有 Task 都完成了,还需要等待很长一段时间 Job 才能结束,这个时间主要花在 Driver 端做第二次 Rename 上。
于是需要想办法优化这两次 Rename。 这里有一个细节,不同于 Task,同一个 Job 不会在同时有并行的 Attempt。 因此 Spark 可以选择下图所示的新的 FileOutputCommitterV2,在这种模式下,不再为 Job 的数据目录设定一个临时目录,每个 Task Attempt 在成功的时候直接把目录 Rename 到最终的目录中。这样就把每个 Task 输出目录的 2 次 Rename 简化成了一次 Rename。
这种做法也有一些弊端:在 v2 中,如果部分 Task 已执行成功,而此时 Job 失败了,就会导致有部分数据已经对外可见了,需要数据的消费者自己根据是否有 _SUCCESS 标记来判断其完整性。不过这个问题对我们的产品来说不是主要问题。
3)本地性问题
对象存储的本地性的缺失,主要影响了延迟敏感的分析查询类请求的体验。 为了改进访问的性能,我们使用了 Alluxio 来作为数据缓存层。 Alluxio 在每个 Worker 节点上预留一部分内存作为缓存的存储空间,其内置的换入换出算法可以帮助最大化有限的内存资源的利用。如果分析请求所需的数据文件恰好已经被 Alluxio 缓存,则直接使用缓存中的数据处理,而不用再访问对象存储。对于一次性读写的场景,增加缓存层并不会有明显的效果,就不再试图通过 Alluxio 加速。
上面的结论对应到我们云上的部署架构图,可以看到在以分析查询请求为主的 Read Cluster 中,每台 Spark Worker 所在的节点都安装了 Alluxio 实例,而以一次性读写作业为主的 Write Cluster 中则没有安装 Alluxio 实例。
4. 小结
在本文中,我们先初步归纳了六个考虑要素中的计算和存储两大要素。 在下一篇文章中,我们会进一步展开剩下的网络、安全、可用性和费用相关的见解和思考。
作者介绍:
马洪宾
Kyligence 创始合伙人 & 研发副总裁
Apache Kylin PMC Member
专注于大数据相关的基础架构和平台设计。曾经是微软亚洲研究院的图数据库 Trinity 的核心贡献者。作为 Kyligence 企业级产品的研发负责人,帮助客户从传统数据仓库升级到云原生的、低 TCO 的现代数据仓库。
吴毅华
Kyligence 云产品研发总监
专注于云原生与数据仓库的结合和实践,8 年云计算与 DevOps 领域的资深技术及管理经验。前嘉银金科运维总监,前 eBay PaaS 中国区核心成员,前携程私有云创始成员。
本文转载自公众号 Kyligence(ID:Kyligence)。
原文链接:
从 Hadoop 到云原生(2):Kyligence 在云原生巨浪中的思考
评论