为了更好地紧跟大数据发展先进性的技术趋势,追赶业界最顶尖的发展技术,我们把线上环境的Hadoop 1.2.1升级到2.6.0版本。这次的升级其实早有预谋,但鉴于升级过程较为繁琐,并且不但涉及HDFS的底层存储的升级,还涉及Hive、Hbase等上层系统的使用,有一定的风险,因此一直都在等待一个合适的时机。直到最近,我们的利用了Hadoop2.6.0和HBase的0.98.8的API的ETL系统正式完成开发,要在线上环境进行部署的时候,我们才决心要升级。
虽然之前已经在测试环境进行了一次升级的尝试,并且已经踩过不少的坑,但在线上环境进行升级的过程,还是出现了各种问题,不过最终还是顺利完成了整个升级过程。我们的线上环境之前总共13个节点,其中有12个DataNode和TaskTracker,另外还有1个NameNode和JobTracker,在这次的升级之前,刚好有一台新机器可以一并部署,于是就决定全部14台机器一并升级。另外集群升级之前,所有DataNode节点总共约有60TB的HDFS空间,并且已经使用其中的26TB来存放HDFS数据。
整个升级步骤总体分为三步:
1、Hadoop升级。Hadoop 1.2.1 -> Hadoop 2.6.0
2、HBase升级。HBase 0.94.17 -> HBase 0.96.2 -> HBase 0.98.8
3、Hive依赖配置。
另外要说明的是,这次的升级没有进行HDFS的HA特性的配置升级,由于考虑到HA特性需要更改相关的Hive、HBase相关元数据,所以还是用回旧的secondary namenode特性。建议在参考本文升级步骤之前,请先在官网阅读相关文档,理解相关特性。
(1)停止相关服务。包括HBase,HiveServer2。
首先停止上层系统,防止升级过程中对Hadoop的影响。
(2)进入安全模式。
Hadoop有一个安全模式,进入安全模式之后,会禁止HDFS系统的文件进行修改和删除,一般是在系统启动的时候,进入初始化的过程中会进入该状态。由于我们在之后的备份需要读取HDFS节点的状态,因此进入安全模式防止对文件系统的修改。相关命令:
[hadoop@namenode]$ hadoop dfsadmin -safemode enter
(3)备份相关数据。
运行这些命令来进行相关HDFS状态的备份。
//文件块数情况 [hadoop@namenode]$ hadoop fsck / -files -blocks -locations > dfs-old-fsck-1.log //目录情况 [hadoop@namenode]$ hadoop dfs -lsr / > dfs-old-lsr-1.log //HDFS结点情况 [hadoop@namenode]$ hadoop dfsadmin -report > dfs-old-report-1.log //把NameNode在内存中的HDFS节点情况保存到硬盘里 [hadoop@namenode]$ hadoop dfsadmin -saveNamespace //手动备份NameNode数据目录,{dfs.name.dir}是hdfs-site.xml设置的路径 [hadoop@namenode]$ cp {dfs.name.dir} .
fsck命令会在最后输出HDFS是否为健康状态(Status: HEALTHY),若有损坏block则要进行恢复。
(4)终结之前的升级状态。
防止在升级过程中,存有上次升级过程遗留的状态,需要清理掉。
[hadoop@namenode]$ hadoop namenode -finalize
(5)备份Hive元数据(可选)
我们集群使用了mysql来保留,因此对mysql进行数据库备份。
(6)停止Hadoop集群。
[hadoop@namenode]$ {$hadoop_home}/bin/stop-all.sh
(7)校验HDFS修改记录是否为空。
Hadoop把HDFS文件的修改命令保留到${dfs.name.dir}/name/current/edits*该文件中, 一般只有4bytes数据。如果发现仍然有其它数据,则需要重启Hadoop集群,然后等待Hadoop把修改应用到fsimage上。
(1)分发文件,设置软链接。
分发Hadoop-2.6.0目录到各个节点。重新设置软链接;另外还要注意修改目录用户权限。
(2)修改对应的conf文件夹里的配置。
要注意Hadoop 2.x开始很多属性都更改了名称,如果要复制原来的旧配置,要修改对应的配置名称。这些旧的废弃配置名称可以在这里找到。
(3)执行升级脚本
[hadoop@namenode]$ {$hadoop_home}/sbin/hadoop-daemon.sh start namenode -upgrade [hadoop@namenode]$ {$hadoop_home}/sbin/hadoop-daemon.sh start secondarynamenode //在每个datanode上执行 [hadoop@datanode]$ {$hadoop_home}/sbin/hadoop-daemon.sh start datanode //以上命令顺利执行后,等待退出安全模式 [hadoop@namenode]$ hdfs dfsadmin -safemode wait
先用-upgrade参数启动namenode(注意由于namenode自动在后台运行,不需要screen或者nohup等防止会话中断的措施),这样namenode就会在后台执行升级过程,并且等待DataNode的连接,然后再启动secondary namenode,接着就在每个datanode节点上启动DataNode进程。要注意使用jps命令来确认每次进程都能顺利启动。
在这里,我们遇到第一个意外:尝试启动DataNode发现失败,查看log发现是由于我们使用了short-circuit特性,但该特性需要加载本地so库,需要更高版本的GLIBC,因此需要先升级GLIBC,顺利升级后,启动DataNode成功。之前在测试环境上也出现过此问题,只不过当时是后来升级完成才启动该特性,才进行GLIBC升级。
大约过了10分钟后,集群关闭了安全模式,完成升级过程。
查看NameNode的50070端口的时候,发现大约有18000个block处于under replicated 状态,貌似是HDFS的一次机架之间的节点平衡。原本打算等待复制完成,但等待了约30分钟后,考虑到Hadoop自行关闭了安全模式,于是决定继续进行升级步骤。事实上,这些1万多个block花费了5-6个小时才完成复制,我们实在无法等待那么长的时间,毕竟还要晚上进行数据导入和处理的任务。
(4)校验HDFS状态
[hadoop@namenode]$ hadoop fsck / -files -blocks -locations > dfs-new-fsck-1.log [hadoop@namenode]$ hadoop dfs -lsr / > dfs-new-lsr-1.log [hadoop@namenode]$ hadoop dfsadmin -report > dfs-new-report-1.log
根据文件的内容和之前的状态备份进行对比,查看是否有问题。
(5)确认升级完毕
[hadoop@namenode]$ hadoop dfsadmin -finalizeUpgrade
在之前执行的升级命令,会把升级前的${dfs.name.dir}/name/current/该目录下的内容进行备份,具体会备份在${dfs.name.dir}/name/previous目录。该备份是用于升级失败进行回滚的。因此如果我们确认升级没有问题后,可以执行以上命令,把之前的备份数据删除。另外要注意的是,如果进行了回滚,在升级之后的数据写入会完全丢失。建议在运行一段时间没有出现问题后才执行该命令。
//启动YARN集群 [hadoop@namenode]$ {$hadoop_home}/sbin/start-yarn.sh //启动MapReduce的history server [hadoop@namenode]$ {$hadoop_home}/sbin/mr-jobhistory-daemon.sh start historyserver
由于MapReduce不再需要后台守护进程,不过提供了一个历史纪录的服务,我们可以启动它来记录MR任务的执行情况。
确认所有进程都顺利启动,并且log里没有异常信息后,我们可以运行测试命令来验证Hadoop是否成功升级。
[hadoop@namenode]$ hadoop jar {$hadoop_home}/share/hadoop/mapreduce/*examples*.jar randomwriter -Dmapreduce.randomwriter.totalbytes=10000000 test-after-upgrade
执行以上命令后,则会向HDFS目录/user/hadoop/test-after-upgrade随机写入10000000 bytes数据。执行完成后,查看目录输出:
-rw-r--r-- 3 hadoop supergroup 0 2015-01-27 14:23 /user/hadoop/test-after-upgrade/_SUCCESS
-rw-r--r-- 3 hadoop supergroup 10038747 2015-01-27 14:23 /user/hadoop/test-after-upgrade/part-m-00000
可见,随机写任务成功,Hadoop升级完成。
由于线上的HBase的0.94版本不兼容Hadoop 2.6.0,因此需要进行HBase的升级。根据官网说明为了升级到HBase 0.98.8版本,必须先升级到0.96.2。在整个升级出现了很多问题,也是耗费了最长时间的地方。有些地方是之前没有考虑到,也有一些是之前遇到过,比如说某个jar包忘记添加,导致要重新添加然后重启。
(1)分发hbase-0.94.17到节点,修改配置文件
(2)检查损坏文件以及HFileV1格式
[hadoop@hmaster]$ hbase upgrade -check
要注意这个hbase命令是0.96.2版本目录下的hbase。最后会输出是否有损坏文件和HFileV1格式。由于0.96.2不兼容HFileV1,因此如果有该格式文件要进行修改,具体可以查看官网指导。
(3)执行升级命令
[hadoop@hmaster]$ hbase upgrade -execute
执行命令后会有类似如下内容:
2015-01-27 14:32:43,051 INFO [main] migration.UpgradeTo96: Successfully completed Znode upgrade
2015-01-27 14:32:43,051 INFO [main] migration.UpgradeTo96: Starting Log splitting
2015-01-27 14:32:43,054 INFO [main] migration.UpgradeTo96: No log directories to split, returning
可以看到升级完成。
这里要重点注意一个问题。执行命令的机器必须是启动HMaster的机器!之前由于测试环境上HMaster和NameNode都在同一个机器,所以没有留意这个问题,待执行升级命令后,准备启动HBase的时候一直报错,后来重新在HMaster机器上执行命令后才成功启动HMaster。
这个升级步骤和之前大体类似,但由于0.96版本是一个过渡升级,因此不需要太多设置。0.98.8要启动HMaster和HRegionServer进程,因此要小心两者区别。
(1)分发hbase-0.94.17到节点,修改配置文件,设置软链接
(2)检查损坏文件以及HFileV1格式
[hadoop@hmaster]$ hbase upgrade -check
(3)执行升级命令
[hadoop@hmaster]$ hbase upgrade -execute
看到和之前0.96.2升级过程一样的输出后,成功升级。
(4)替换Hadoop的jar包
由于HBase 0.98.8还是用Hadoop 2.2.0的包进行编译,因此我们需要替换掉里面Hadoop 2.2.0相关的jar包。还注意要添加htrace-core-3.0.4.jar的包。由于之前用到了Phoenix,还需要把jar包替换为4.1.0版本。
之前在测试环境配置的时候,只是把lib目录下所有以hadoop开头的包替换成对应的2.6.0版本,但发现由于hadoop 2.6.0的RPC依赖的一个包htrace-core版本从2.04升级到3.0.4,但由于HBase自身依赖2.04版本,因此需要把两个包共存,这样才能成功启动HRegionServer。但当时忘记了做备份,导致后面启动的时候失败,查看log才发现问题。
(5)启动HMaster和HRegionServer
[hadoop@hmaster]$ {$HBASE_HOME}/bin/start-hbase.sh
这个简单的命令进行启动即可,但这个过程遇到各种问题导致无法顺利启动HMaster和HRegion进程,其中包括:
1、由于之前运行HBase的升级命令的时候,不是在HMaster的节点上,导致启动的时候,初始化访问Zookeeper出现ClusterId read in ZooKeeper is null的错误。
后来重新在HMaster节点上运行升级命令。
2、替换Hadoop依赖包的时候,忘记添加一个htrace-core-3.0.4.jar的包,导致HRegionServer启动失败;
重新添加htrace-core-3.0.4.jar包。
3、复制以前旧的配置的时候,hbase.hregion.memstore.mslab.chunksize和hbase.hregion.memstore.mslab.max.allocation这两个属性,用的是2m和256k这样的字符串,导致HBase无法解析;
修改为对应的bytes为单位的数值。
4、LZO和Snappy压缩方法无效;
由于之前在测试环境升级过程中,没有去配置这两个压缩方法,导致在线上升级要进行配置的时候遇到不少坑。
(1)LZO需要重新编译,把jar包依赖放到lib下,然后把so放到hadoop/lib/native路径下。
(2)Snappy的问题最为奇怪。
在hadoop/lib/native目录下建立软链后,依然一直提示压缩方法失效,HRegionServer无法启动。当时查看HBase的Web状态页面的时候用户表全为空,当时还以为是丢数据了。。。后来发现HRegionServer一直都没有启动起来。调查发现libsnappy.so一直都没有被加载起来。
奇怪的是,在某个节点上运行HBase使用Snappy的压缩进行测试则显示成功,该命令如下:
[hadoop@hmaster]$$HBASE_HOME/bin/hbase org.apache.hadoop.hbase.util.CompressionTest file:///tmp/test.txt snappy
该测试命令最后打印出SUCCESS,表示能够顺利利用Snappy压缩方法对本地文件进行压缩。
无奈之下,最后通过在hbase-env.sh下设置HBASE_LIBRARY_PATH环境变量到$HADOOP_HOME/lib/native才成功加载。
这个问题后来经过阅读HBase的启动脚本,并做了大量的测试,发现了根本原因。具体原因的描述见后记。
解决了以上4个大问题后,终于顺利启动了HBase集群,并且数据也没有丢失,也最起码达到预期的目标。
由于线上的Hive 0.13.1版本兼容Hadoop 2.6.0,因此我们并不需要对其进行升级。但由于Hive创建了HBase的external表,因此我们要替换这些jar包。由于HBase 0.98.8把之前一个jar包的内容分散到不同的jar包里,经过了多次尝试才把具体的依赖jar包找全。另外还要注意修改hive.aux.jars.path的属性。
最终从早上10点开始,到晚上20:00左右完成了整个集群的升级。由于升级时间过长,导致当天来不及导入数据,到第二天的时候很多日常任务都跑失败,幸好后来重跑一下任务恢复了数据。整个升级过程虽然错综曲折,但最后还是能够顶着压力完成升级。
完成升级之后,我们的下一步就是OLAP框架的搭建。为了能够更好地提供快速的SQL查询过程,OLAP的框架的使用是必须的。另外,是否进行HDFS HA的升级也是需要考虑的,毕竟HDFS的高可用特性能够解决单点故障问题,之前在测试环境的NameNode崩溃的时候,该特性能够顺利启用,让我们对这个特性有了很大的信心。
HBase和Hadoop一样,通过ssh到其它节点上执行启动命令来启动进程。但在ssh启动HRegionServer的过程中,java.library.path路径并没有设置!这个环境变量就是Java加载so的查找路径,因此没法加载libsnappy.so。查看启动脚本hbase,有这样的一段代码:
#If avail, add Hadoop to the CLASSPATH and to the JAVA_LIBRARY_PATH HADOOP_IN_PATH=$(PATH="${HADOOP_HOME:-${HADOOP_PREFIX}}/bin:$PATH"which hadoop 2>/dev/null) if[ -f ${HADOOP_IN_PATH} ]; then HADOOP_JAVA_LIBRARY_PATH=$(HADOOP_CLASSPATH="$CLASSPATH"${HADOOP_IN_PATH} \ org.apache.hadoop.hbase.util.GetJavaProperty java.library.path 2>/dev/null) if[ -n "$HADOOP_JAVA_LIBRARY_PATH"]; then JAVA_LIBRARY_PATH=$(append_path"${JAVA_LIBRARY_PATH}""$HADOOP_JAVA_LIBRARY_PATH") fi CLASSPATH=$(append_path"${CLASSPATH}"`${HADOOP_IN_PATH} classpath 2>/dev/null`) fi
这段代码的作用是通过启动hadoop命令加载类GetJavaProperty,原本是Hadoop提供用来方便CLI执行自定义MR任务的。这个类的实现如下:
publicclass GetJavaProperty { publicstatic void main(String args[]) { if(args.length == 0) { for(Object prop: System.getProperties().keySet()) { System.out.println(prop + "="+ System.getProperty((String)prop, "")); } }else{ for(String prop: args) { System.out.println(System.getProperty(prop,"")); } } } }
该类的作用是调用Java函数System.getProperty来打印出环境变量。由于是利用hadoop命令来加载该类,因此hadoop会把自己的本地so库目录设置到java.library.path环境变量。这样来确定具体的hadoop本地so路径,这样而不是直接hardcode用{$HADOOP_HOME}/lib/native能够避免由于后来hadoop的目录修改导致路径设置实效,这是相当灵活的配置方法。
但为什么不起效呢?原来因为HBase节点上ssh到其它节点启动HRegionServer的时候,由于我们之前升级Hadoop的时候,更换软链接/usr/local/hadoop到hadoop-2.6.0的新目录,而/usr/local/hadoop是hadoop的用户目录,里面包含几个hadoop用户相关的配置,更换软链接的时候,并没有复制到hadoop-2.6.0里面,这样导致设置环境变量的脚本.bashrc和.bash_profile两个脚本的丢失,所以Hbase启动的时候通过SSH来启动命令的时候会因为无法找到$HADOOP_HOME环境变量而导致失效。虽然之前在测试环境的时候已经发现这个严重的问题,但在线上升级的时候,仍然没有引起足够的重视,才导致到这样的问题,以后需要多加注意。