2022年第009篇

Hadoop 3 发布已有5年,最新版本已经更新到3.3.2。在这5年中,Hadoop发布了许多重大特性。HDFS EC编码趋于成熟,在提升容错的前提下降低了存储空间;HDFS RBF简化了客户端的配置,平衡了amenode负载;多Standby Namenodes的支持进一步增加容错;Yarn增加对docker的支持,提供更好的隔离性;增加动态配置资源的API,Federated的支持提高了集群的扩展性。

在介绍Hadoop升级之前,我们先来看一下升级前的集群架构。Hulu的Hadoop集群已经在2.6.0-cdh5.7.3上运行多年,整体的结构如下图所示。

图1:Hulu大数据集群架构

可以看到HDFS,Yarn,Hive为整个集群提供了存储、计算、和元数据服务,是支撑上层应用的核心组件。整个集群有大约数千台服务器,数百PB数据。集群访问通过Firework客户端控制。Firework是一个封装了开源客户端和集群配置的工具,支持动态拉取集群配置,动态更新客户端版本,是所有Query和App提交的入口,也为我们在升级过程中获取用户APP的提交记录提供了诸多便利。

此次Hadoop的升级涵盖了几乎所有的相关组件,包括Cloudera,HDFS,Yarn,Hive,Hbase,Zookeeper,Sentry等。由于受到license的限制,我们从 2.6.0-cdh5.7.3 升级到3.0.0-cdh6.3.3(注,CDH6商业版有额外的支持周期)。

我们从2021年第二季度开始测试,到7月份正式上线,历时4个月时间。HDFS,Yarn,Hive作为升级的核心组件,我们对其进行了详细的测试。其他组件的升级暂且不在本文中介绍。

在升级过程中我们尽可能地平衡升级的复杂度与对用户的影响。在Hulu,Hadoop的主要用户分布在美国西海岸时区和北京时区,少量在美国东海岸。受疫情影响,大多数office依然在关闭状态,协调工作会更加困难。因此我们需要考虑在升级前后保证用户的App在不替换Hadoop依赖的情况下能够运行最新的集群,尽可能地减少或者延后升级带来的变动。

因此,在升级过程中我们增加了只依赖Hadoop2的兼容版本,使得用户端依赖的升级可以延后进行。这个版本在所有用户升级到新的Hadoop3依赖后被淘汰掉 。整个过程的时间线如图2所示。

图2:Hadoop升级规划

由于Cloudera不提供滚动升级的方案,只提供停机升级和迁移升级。我们在前人的经验基础上,探索了新的升级方案。

在升级过程中,Hadoop各个服务的兼容性从以下四个方面考虑:

  • 客户端与服务之间的接口兼容性

  • 服务各个组件之间的接口兼容性

  • 各个组件与其存储状态的兼容性

  • 用户接口在语法和语义上的兼容性

Hadoop3的升级过程中的兼容问题已经有很多技术博客讨论,我们在升级过程中也参考了这些经验,对上述问题进行了一一验证。下面针对我们在实际升级过程中各个组件遇到的额外问题,做一个补充。

HDFS Client以及各个组件之间接口兼容良好。组件之间的问题在《HDFS3.2升级在滴滴的实践》一文中有详细的介绍。其中FSImage的兼容问题的patch已经打在了CDH6.3的发行版中。

在我们的实践中,发现仍有两个问题,一是HDFS Namenode与Datanode之间的Block Access Token在2.7中增加标识storage type的字段(HDFS-6708),这导致启用security后Hadoop 2.6与3.0在协议上会有兼容性的问题,因此需要升级之前在2.6中打上HDFS-15191。另一个是Datanode存储目录结构的变化,Datanode在2.8(HDFS-8791)中修改了block的目录的hash结构,将256*256个目录改为32*32个以改善在ext4文件系统上的性能。

由于我们从2.6直接升级到3.0会受到这个变化的影响,在Datanode的滚动升级过程中,我们计算了block新的目录结构,在Datanode启动之前,将block移动到正确的目录上。

除了上述问题以外,在HDFS的命令行客户端上,chmod命令对sticky bit的用法有轻微的修改(HDFS-10689),客户端不再支持HADOOP_HEAPSIZE和JAVA_HEAP_MAX,而是用HADOOP_HEAPSIZE_MAX和HADOOP_HEAPSIZE_MIN来调整客户端内存。

Yarn 各个组件以及客户端与服务端之间的兼容性问题在[1]中有很详细的介绍。其中对于YARN-668中将token identifier的序列化协议改为Protocol Buffer后引起的兼容问题,YARN-8310做了向下兼容,且这个patch已经包含在CDH6中。但是它仅能够使得Hadoop3的Yarn能够解析Hadoop2的token,认证依然会失败。这是由于token中用byte arrary表示的token identifier,如果是由Hadoop2的RM生成,在Hadoop3中经由反序列化、序列化之后的结果与原始byte array不一致。服务端会用密匙计算这个byte array的签名,然后与Token中的签名对比,如果不一致则会认证失败。这个问题与HDFS-15191解决的问题类似,需要增加一个cache用来存储并返回原始的byte array。

另外,从Hadoop 2.6.1 开始,同一NodeManager节点只能打一个label,如果存在Nodemanager拥有多个Label,在升级之前需要对Queue和Node的映射关系进行调整,但是在CDH5-2.6.0中没有这个限制。

CDH5中Hive一直使用1.2版本,已经十分老旧,在CDH6中终于升级到Hive 2.1。虽然距离最新的Hive 3依然有一定的差距,但是在性能和稳定性都得到了很大的改善。在2.1中,Hive的存储表结构的元数据发生了的变化,升级过程需要在停止服务的前提下将元数据升级到新的版本。Hive的SQL也在这个版本中有很多变化[6],具体总结如下

  • RLIKE (A, B)不再支持,该用A RLIKE B

  • 反引号之内的点被识别为表名的一部分,从而报错。如create table `schema_name.new_table`需要被改为create table `schema_name`.`new_table`

  • Union All不再支持类型的隐式转换

  • UNION ALL不再支持在自查询上进行SORT BY, CLUSTER BY, ORDER BY, LIMIT, 以及 DISTRIBUTE BY

  • GenericUDF.getConstantLongValue 被deprecated

  • 增加保留的关键字,如TIME,HOUR

  • 语法`desc table.column_name`不再支持,仅能够使用`desc table_name column_name`

  • 不再支持ALTER TABLE中的OFFLINE和NO_DROP标志

在Hive增加的保留关键字中,hour、day等已经在线上被广泛di用作partition key。这个变化意味着大量的列名或者客户端的SQL语句需要改变。我们revert了Hive中相关关键字以避免升级过程中大量的用户程序修改。

目前在我们的集群中大部分的线上Spark app主要使用社区2.3,2.4版本,社区Spark 2.x主要依赖Hive1.x和Hadoop2.x进行编译。在升级到Hadoop3.0+Hive2.1后,虽然Spark 2.x中的Hive1.x客户端与Hive2.x服务端之间保持兼容,但是Hadoop3.x的依赖包与Hive1.x之间存在冲突。虽然Spark3.x 能够使用Hive2.x和Hadoop3.x,但是升级Spark也需要用户端进行适配工作。

为了继续支持Spark 2.x,需要打上HIVE-15016,HIVE-16081,HIVE-16131。这几个patch中的文件目录结构发生了变化,需要在理解patch的基础上手动打到hive1.x的分支中。同时需要修复JvmPauseMonitor接口变化造成的编译错误

此外,MR主要由Hive提交,主要通过HiveSQL生成,版本随升级切换,不需要考虑兼容行问题。Flink则与Hive2.x,Hadoop3.x工作良好。

在上一章,我们讨论了Hadoop各个服务之间、服务内组件之间、客户端与服务之间接口上的兼容性问题。尽管可以解决接口上的冲突,但是由于Hadoop生态复杂的依赖,依然可能与用户的Jar包在升级过程中产生冲突。为了满足图2中用户延后升级App端依赖,我们对Spark和Flink加载Jar包的过程进行了详细的分析。

对于升级过程中可能的依赖问题,我们可以分为三类:

  • 用户App的Assembly Jar中引入hadoop依赖并开启user class first(在Spark中为spark.driver.userClassPathFirst和spark.executor.userClassPathFirst; 在Flink中为 classloader.resolve-order=“child-first”,这也是flink的默认配置)

  • 用户App与Hadoop依赖相同的第三方包,并且升级后Hadoop与用户App的版本之间无法兼容

  • SPI 机制造成的调用错误

在介绍上述问题之前,我们先来理解Spark和Flink用户任务线程中Jar包加载机制。JVM中ClassLoader加载类的原理可以参考[4]。Spark、Flink包含自定义的ClassLoader,并将其设置到用户任务线程的contextClassLoader中,如图3所示。

图3:Spark,Flink的依赖包加载

所有的Classloader由类成员parent连接起来形成一个链表。其中AppClassloader, MutableURLClassLoader,FlinkUserCodeClassLoader均继承自URLClassLoader,并且按照内部存储的URL列表加载Jar包,因此排在前面的Jar包实际上会先被搜索,不过JVM规范并不承诺这一点。而BootStrapClassLoader和ExtClassLoader主要管理JVM的runtime包和相关扩展包。

AppClassLoader,MutableURLClassLoader,FlinkUserCodeClassLoader管理的Jar包大致可分为三类:

  • 用户App的Jar包。Spark中由命令行第一个Jar包,--jars,spark.yarn.dist.jars, spark.yarn.dist.archives等决定。Flink中由命令行中第一个Jar包或--jarfile决定

  • Spark,Flink分发在HDFS上的runtime jar包。在Spark中,由spark.yarn.archive,spark.yarn.jars,以及环境变量SPARK_HOME决定。在Flink中,由yarn.provided.lib.dirs和环境变量FLINK_LIB_DIR决定

  • 系统环境中的Hadoop相关的Jar包。在Spark中,由yarn.application.classpath, mapreduce.application.classpath决定。Flink仅加载yarn.application.classpath。这两个参数的默认值是由一组环境变量定义,如HADOOP_HDFS_HOME, HADOOP_YARN_HOME等。这些环境变量在application运行时,由NodeManager注入。

默认情况下,MutableURLClassLoader与一般的URLClassLoader的类加载顺序相同,都会先加载parent中的类,当开启user class first时,则会优先加载自身管理的Jar包。而FlinkUserCodeClassLoader默认情况下则是用其子类ChildFirstClassLoader代替,与Spark默认情况相反。在Flink的per-job模式下,Flink user jars的加载用的ClassLoader和加载顺序受yarn.per-job-cluster.include-user-jar参数的影响,默认情况下加载在AppClassLoader中,按包名的字典序排列。

一般用户App端主要依赖Spark,Flink的编程接口,因此用户的Assembly Jar中常常仅包含了Spark/Flink客户端的依赖包。当App提交到Yarn上后, 容器启动入口类依然多从升级后系统提供的包中加载。如果用户开启user class first,用户的Jar包中包含的Hadoop2的类会覆盖一部分Hadoop3的类,在调用过程中会出现Hadoop2,3类之间的内部调用,非常容易抛出MethodNotFound异常。因此对于开启了user class first的App均需要将hadoop相关的依赖在pom中设置为provided,或者提前在hadoop3上进行测试解决兼容性问题。

Hadoop大版本的升级的同时也更新了其众多第三方依赖包,如果存在用户依赖的第三方包已不再向下兼容,有可能产生冲突。虽然这类问题可以通过Maven Shade等插件解决,但是Hadoop生态的依赖包数量十分巨大,而用户依赖具有不确定性,难以提前确定需要shade的类。

为了向前兼容,我们在集群中分发了一个向前兼容的Spark和Flink版本。兼容版本中包含Hadoop2的完整依赖,通过3.1中对ClassLoader中Jar包的加载顺序的分析,Hadoop3的依赖会被完全覆盖。在解决第二章的兼容性问题后,这个版本对于大部分用户App来说都够正常在Hadoop3的环境中工作。然而,SPI的存在使得App依然有可能发生调用错误,我们在下一节介绍。

SPI(Service Provider Interface)[5]常常被用于反射机制中,在Hadoop包中也有大量使用。反射机制的存在,会使得在ClassLoader加载链上被覆盖的Jar包中的类也有机会得到加载,从而引发错误。

我们通过一个例子来说明。为了能使集群中运行不同的Spark,Flink版本,我们在firework中提交Spark和Flink时加载了HDFS上不同版本对应目录下的runtime jar到container的CLASSPATH。在早期测试中,这些目录中的assembly jar包含了所有的依赖并覆盖了CDH系统中原本的Jar包来解决3.3中提到的问题。由于HDFS的接口在2和3之间是兼容的,因此assembly jar中Hadoop client保持了2.6。结果,有用户提交job时发现所有http协议通过java URL打开资源时,handler被替换为hadoop中的HttpFileSystem而出错。这个HttpFileSystem仅用于通过http协议访问HDFS上的文件,但是结果是针对非HFDS上的资源,java URL也被设置成了用HttpFileSystem处理。

问题的起因在于Spark的SharedState初始化过程中,设置了URL的handler工厂`URL.setURLStreamHandlerFactory*(new FsUrlStreamHandlerFactory())`, FsUrlStreamHandlerFactory初始化过程中会加载FileSystem的子类及其支持的URL schema,并在访问URL时根据URL的schema调用对应的子类,而FileSystem子类的加载过程的第一步便是通过SPI查找配置在Jar包中META-INF/services/org.apache.hadoop.fs.FileSystem里的Provider。在2.6中,FsUrlStreamHandlerFactory会加载FileSystem返回的所有子类,但是Jar包中没有配置HttpFileSystem。在3.0中,FsUrlStreamHandlerFactory会忽略掉支持http和https协议的FileSystem子类,但是Jar包中配置了HttpFileSystem。这两个包单独运行都不会加载HttpFileSystem到java的URL类中。当他们同时存在时,SPI机制会将ClassLoader及其所有父节点可加载的包中的provider的配置文件进行合并,尽管我们用2.6的版本完全覆盖3.0的版本,FsUrlStreamHandlerFactory, FileSystem和HttpFileSystem也都加载为2.6版本,但配置在Hadoop 3 中的provider生效了,而2.6版本的FsUrlStreamHandlerFactory则读取了合并后的provider列表,URL中默认打开http协议的类被替换为HttpFileSystem。

SPI造成的问题非常复杂,也意味着通过反射机制,被覆盖的包依然可以改变实际加载的包的行为。因此为了使兼容版本的Spark和Flink能够工作,我们在升级过程中保留了CDH5在系统中的包,同时在兼容版本提交Container的ApplicationSubmissionContext中将所有的CDH6包替换为CDH5包的路径。由此在hadoop3集群中生成一个完全只依赖Hadoop2的环境。由于接口上的兼容,运行在兼容版本的App能够正常在hadoop3的环境中工作。

测试过程共分为5轮,三个阶段。

  1. 二轮测试,兼容性测试,解决发现的问题以及打上patch

  2. 四轮测试,压力测试。在100个节点左右的测试集群上采样线上的数据和任务,并进行压测

  3. 上线前演练

在前两轮的测试中,重点针对兼容性问题进行测试。我们通过firework客户端和服务端audit日志,收集了所有用户的提交命令,jar包,query,和环境变量。对使用了不兼容的关键词的query进行了粗筛,并通知对应的用户在Hadoop3的集群中进行测试,对不兼容的query进行修改。

升级过程分为三个阶段。

我们首先升级Cloudera,Sentry,Zookeeper等组件。这些组件与升级前的版本兼容良好,没有downtime。

然后,由于Hive的metadata已经不再兼容,升级采用停机升级。在停机的状态下升级metadata,然后重新拉起服务。由于在Hive停机的状态下,大部分Yarn上的app无法正常工作,因此我们在这个过程中对Yarn也进行了了停机升级。在Yarn升级的过程中,我们清除了Yarn上application以及NM本地的所有状态。基本上重新搭建了Yarn服务。整个过程大约花费2个小时。

最后,HDFS采用Rolling Upgrade。按照JournalNode,Namenode,Datanode的顺序依次滚动升级。线上的6个namespace每个升级大约2个小时。Datanode则按照机架依次升级,花费了3周左右的时间。

本次升级整体比较顺利,我们也由此对大数据的整个架构有了更进一步的了解。但是,本次升级仍与社区的最新版本3.3有一定的差距,在稳定性和性能方面依然有诸多地方可以优化。随着容器化技术的成熟,隔离性上还有很大的改进空间,App的依赖问题也能够由此彻底解决。在Hive升级之后Spark也能够release更新的版本。与云上服务更友好的集成会是未来的方向之一。

[1] Hadoop2.6升级到3.2在58同城的实践. https://mp.weixin.qq.com/s/LqbTNa7ZA_InL843eDP5_w

[2] HDFS3.2升级在滴滴的实践. https://mp.weixin.qq.com/s/bv9NHFPLCCAV_IYIi4FQ8g

[3] Apache Hadoop3.3.2 – HDFS Rolling Upgrade. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsRollingUpgrade.html

[4] 一看你就懂,超详细java中的ClassLoader详解.https://blog.csdn.net/briblue/article/details/54973413

[5] Introduction to the Service Provider Interfaces. https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html

[6] Incompatible Changes in CDH 6.3.3. https://docs.cloudera.com/documentation/enterprise/6/release-notes/topics/rg_cdh_633_incompatible_changes.html#hive_hos_hcatalog_ic_631

Anning Luo,高级软件工程经理

北京大数据团队是整个Disney流媒体大数据团队的一部分。北京大数据团队涵盖了Audience,Marketing,Personalization,和基础架构等多个领域。团队主要致力于构建大数据架构和数据服务,为用户分析,商业营销,个性化推荐提供数据平台。

欢迎与大数据团队一起用数据影响用户,构建Hulu/Disney Streaming的数据乐园。

感兴趣的同学发送简历至:

bjindustry@disney.com(烦请标注申请职位+姓名)

点击阅读原文

查看大数据团队所有在招职位,欢迎投递!

⬇️⬇️⬇️

技术博客|第9期:Hadoop3升级实践相关推荐

  1. 技术博客|第15期:流媒体传输协议简介 - HLS协议

    在Hulu及Disney+流媒体平台上,自适应流媒体传输技术被广泛使用,它可以根据用户的网络情况相应地调整视频码率,为用户带来良好的观看体验.为了实现这一点,视频将被编码成不同码率的版本,并且在每个版 ...

  2. 技术博客|第8期:广告流量匹配算法在Hulu/Disney Streaming平台的实战

    2022年第008篇 订单优先级 保量广告订单按照订单类型.单价.广告主的不同会被切分为从高到低不少于十个优先级.当流量不足以满足所有订单目标需求的情况下,需要首先保证高优先级的订单可以满足投放需求. ...

  3. 技术博客|第16期:个性化视频搜索引擎:排序篇(上)

    我们通过之前的两期文章[1][2]介绍了视频搜索引擎要解决的基本问题以及召回系统的基本结构.搜索引擎是由多个模块构成的系统工程, 最终结果由索引,召回,查询理解,排序等决定,如图1所示.召回系统更加关 ...

  4. 技术博客|第16期:个性化视频搜索引擎:排序篇(下)

    模型结构决定了信息提取的效率,在搜广推领域有大量的相关研究.我们借鉴业内排序模型的优秀实践并结合视频搜索的特点,设计了一套以深度编码网络为基础提取信息.适合多目标优化的多任务专家网络以及解决偏差问题的 ...

  5. 技术博客|第4期:个性化视频搜索引擎简介

    2022年第004篇 1:Hulu 上的经典美剧<权利的游戏>,<老友记>和<蜘蛛侠> Hulu 是一家美国头部付费流媒体平台,它上面的内容以美剧和电影为主,包括一 ...

  6. 12. 橡皮擦技术博客写作课,第一版收尾篇,写作也要懂点心理学

    欢迎来到橡皮擦的写作课 本文内容:那些博客中的写作心理学 心理学用在写作中 随着不断的写作,橡皮擦发现其实博客写作与产品设计有很多相似的地方,在写作的时候,你可以主动去设计一些"套路&quo ...

  7. 如何运营个人技术博客

    前言   本篇和大家聊聊如何运营个人技术博客,定位下做技术写作的目的,有哪些交流平台和输出方式,如何把控内容质量,整理了一些写作技巧和自己常用的写作工具,最后分享下如何在有限的时间里合理安排保证写作与 ...

  8. JAVA相关的深度技术博客链接

    JAVA相关的深度技术博客链接 深入聊聊Java 垃圾回收机制[附原理图及调优方法] 说说Java 多线程之悲观锁与乐观锁 深入JAVA 的JVM核心原理解决线上各种故障[附案例] 谈谈你对volat ...

  9. “我有必要写技术博客吗?” 写技术博客一年,谈谈其得失优劣

    本文全文不含任何推广,所提到的博主.公众号皆为笔者真真切切接触.学习过的. 大三上学期读了许多"程序员"的公众号,当时计算机基础知识并不好,很多文章读的一知半解.只记得关注了几个很 ...

最新文章

  1. 《高阶Perl》——导读
  2. 【Go】从键盘输入字符串和数字
  3. Eclipse假死,一直LoadingDescriptFor,找到原因了
  4. 编译tslib1.4出现错误undefined reference to rpl_malloc错误
  5. RTMPdump(libRTMP) 源代码分析 6: 建立一个流媒体连接 (NetStream部分 1)
  6. 原生js实现一个tab栏的标签操作
  7. xss防御补丁_Discuz论坛最新dom xss漏洞的解决方法
  8. 教你怎么使用你的电脑
  9. glassfish 是oracle的,GlassFish“百天”小版本 彰显Oracle的大功力
  10. poj 1679(次小生成树)
  11. cocoapods应用第一部分-xcode创建.framework相关
  12. 中国省-市-县(区)三级城市数据(json和数组)
  13. CSDN博客去广告-谷歌插件
  14. 2016最新淘宝客申请高佣金以及分析抓包详情
  15. 怎么把两个音频合成一个?
  16. MQTT的学习之Mosquitto简要教程(安装使用)
  17. 【NLP】Python NLTK结合Stanford NLP工具包进行分词、词性标注、句法分析
  18. xgboost 毒蘑菇mushroom数据集分类
  19. 第十二章:使用C语言(Python语言)操作Sqlserver2019数据库
  20. Ubuntu1804安装nginx

热门文章

  1. 使用JavaScript创建音乐播放器的案例详解
  2. java 地图四色着色算法_继陈景润之后周立敬攻破世界三大数学难题之一:地图四色难题...
  3. .aspx简单易懂的介绍
  4. python画spc控制图_实施SPC控制图的八个步骤详解
  5. 《AANet: Adaptive Aggregation Network for Efficient Stereo Matching》
  6. 「自控原理」3.1 时域分析法、一阶系统时域分析
  7. c语言程序设计中北答案详解,C语言程序设计试题及答案解析汇编.doc
  8. mysql怎么给用户加权限_mysql怎么给用户加权限
  9. 交通标志识别技术的原理
  10. 知乎万人点赞推荐的五款APP