最近搞Hadoop集群迁移踩的坑杂记


Overview

最近一段时间都在搞集群迁移。最早公司的hadoop数据集群实在阿里云上的,机器不多,大概4台的样子,据说每个月要花7000多。从成本的角度,公司采购了4台2手服务器(E5-2420 v2 * 2+96G内存)在办公室自己搭数据集群。虽然说机房条件艰苦,没空调就算了,还有暖气呢,但是机器还是挺不错的,比阿里云32G的的机器强多了,4台大概2万,还不够阿里云烧3个月的,理论上只要能用3个月就已经很划算了。

网络拓扑图

硬件分配方面,因为磁盘不大,外加后续还有一些其他用途,所以有三台机器直接用的物理机,一台拿出来用esxi做虚拟机。用阿里云很蛋疼的是,要想通过内网访问还得走vpn,而且vpn不太稳定,也就1m左右的速度,和阿里云宣称的上传无限速差多了。而且还有一个蛋疼的问题是,从阿里云的机器访问办公网,由于没有那么多公网ip,所以只能在办公网搭一套vpn,那边需要访问的服务通过vpn接入进来。

之前公司的hadoop是用的ambari搭的,cloudera的CDH和ambari的HDP都用过一阵子,个人感觉上HDP没有CDH稳定,而且CDH的管理程序易用性也好于HDP。而且cloudera的市场占有率也好于ambari,参考CentOS之于Red Hat,这种商业公司的开源社区产品在稳定性上应该是好于纯社区版的,尽管ambari后面也有Hortonworks这家公司支撑,但是还是更倾向于用CDH。

既然连全家桶的都变了,自然每一个服务的版本也不可能完全对应兼容,话说回来,哪怕相同全家桶的不同版本,也无法保证兼容不是。

迁移相关

Hive导入/导出

Hive作为一个“数据库”,竟然没有一个逻辑备份工具,我也是醉了。虽然没看过Hive架构、源码,不过从最近的迁移工作中感觉Hive基本文件是存在HDFS上的,而HiveMetaStore(MySQL/PostgreSQL)存储一些元信息,而SQL查询就是编译成MapReduce在HDFS进行查询,所以Hive相对来说比较慢。基本上Hive在HDFS就是采用一定序列化方式的文本文件而已。尽管Hive wiki上有一篇关于怎么导入/导出的方法的介绍,但是一点不好用,而且其实没那么麻烦。刚也说了,其实Hive基本数据就是存在HDFS上的,都是存在类似/somepath/hive/warehouse/dbname.db/tablename下的,而schema是存在MetaStore的,如果两边是相同的配置方式,其实只要把warehouse全部distcp到目标集群对应目录下,再把MetaStore给dump还原回去就好了。不过因为这次迁移是从HDP(用的MySQL)迁移到CDH(用的PostgreSQL),所以并没有直接还原MetaStore,而且用show create table <table_name>;打印出创建Schema的语句,然后在新的目标集群创建新表,然后再通过类似命令load data inpath '/tmp/credit_apply/dt=2016-01-21' into table credit_apply partition (dt='2016-01-21');直接导入就好。这里写了一个Perl脚本来生成导入语句:

这个脚本用来导入本地文件系统数据文件,HDFS上也是类似的,只要稍微改下,去掉inpath前的local。提供的输入是文件完整路径列表,里面需要包含dbname.db/tablename这类信息。

#!/usr/bin/env perl

use strict;
use warnings;

my %filter = ();

# ./xiaomai_report.db/zhuanti_site/dt=2015-12-27/000000_0
# load data inpath '/tmp/credit_apply/dt=2016-01-12' into table credit_apply partition (dt='2016-01-12');
while (defined (my $line = <STDIN>)) {
    chomp $line;
    if ($line =~ m/\/([^\/]+)\.db\/([^\/]+)\/(\w\w)=(\d{4}-*\d\d-*\d\d)\/(?:([^=]+)=([^\/]+)\/)*/) {
        my $key = "$1\t$2\t$3\t$4";
        if (defined $5 and defined $6) {
            $key = "$key\t$5\t$6";
        }
        unless (exists $filter{$key}) {
            if (defined $5 and defined $6 and $5 ne '') {
                print STDOUT "load data local inpath '/var/lib/hive/data/$1.db/$2/$3=$4/$5=$6' OVERWRITE into table $1.$2 ";
                print STDOUT "partition($3='$4',$5='$6');\n";
            } else {
                print STDOUT "load data local inpath '/var/lib/hive/data/$1.db/$2/$3=$4' OVERWRITE into table $1.$2 ";
                print STDOUT "partition($3='$4');\n";
            }
            $filter{$key}++;
        }
    }
}

HBase(Phoenix)导入/导出

相较于Hive的傻大粗的迁移方式,HBase就人性化多了,提供了各种小工具方便导入/导出

$ bin/hbase org.apache.hadoop.hbase.mapreduce.Export [ [ []]]

可以看看手册,不仅仅有导入导出工具,还有一些诸如RowCount的小工具(虽然RowCount并没有输出结果……),不过坑的是导出好像不能导出表结构,还得自己手动创建,也是蛋疼,好在HBase其实只是知道CF就行了。最坑的是Phoenix,它是有schema的,但是Phoenix坑爹的是它没有命令查看怎么创建的schema,你说你这不是坑爹么……

CDH安装Apache Phoenix

值得注意的是,不像HDP,CDH全家桶里并不包含Phoenix,你得单点。好在Cloudera Labs提供了发行版以便尽可能简单的安装使用。那篇文件里介绍的比较详细,怕Cloudera挂了,所以这里留个备胎:

  1. parcels里面添加对应的分支,5.4之前的5.x对应1.1版本,而5.5是对应1.2版本,因为HBase/Spark版本和Phoenix有很大关系,所以还是要找对应的版本,不过好在即使添加错了,安装时也会告诉你错了,就是浪费时间罢了。。。。
  2. 在ClouderaManager里安装
  3. 在HBase的Configuration里修改hbase-site.xml,添加:
    <property>
      <name>hbase.regionserver.wal.codec</name>
      <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
    </property>
  4. 之后重启HBase就可以支持Phoenix了,其实就是添加Phoenix的jar包。

另外,虽然Phoenix吹的挺牛逼,但是对于复杂查询,速度还是特别慢,默认的timeout是1分钟,很多查询远远不够,可以在hbase-site.xml里添加配置:

<!-- 很多文章说是改这个,而且HDP的配置也是加的这个,但是我简单看了一下Phoenix的源码,这个值默认是10分钟,下面keepAlive是1分钟,所以应该调大下面的值,两个都调也可以
<property>
  <name>phoenix.query.timeoutMs</name>
  <value>180000</value>
</property>
-->
<property>
  <name>phoenix.query.keepAliveMs</name>
  <value>180000</value>
</property>

Hive添加额外的Jar包

公司之前的Hive用了Hive-JSON-Serde用于让Hive支持JSON,他们用的时候一般要在脚本前面加上add jar /path/to/jar,但是这样多蛋疼,让Hive启动时带着多方便。实际上Hive提供了一个环境变量HIVE_AUX_JARS_PATH用来引用额外的jar包,而CM的Hive配置里有Hive Auxiliary JARs Directory这一项,在相关机器上创建一个目录,加到这个环境变量里即可。

Spark直接引入额外Jar包(Phoenix集成)

因为公司用了Phoenix,而Spark默认是不加在相关Driver的,但是每次启动spark-shell/spark-submit时都要带上--jars参数又有点蛋疼,所以还是在CM的Spark配置里找到spark-env.sh的配置,加上:

SPARK_DIST_CLASSPATH="$SPARK_DIST_CLASSPATH:/opt/cloudera/parcels/CLABS_PHOENIX/lib/phoenix/lib/*"

修改sqoop的job信息

公司定时导数据用的sqoop,这是一个RDBMS/Hive之间互导数据的工具,据说sqoop2已经是一个服务了,但是我们用的还是sqoop1代,看起来像是一个普通工具,修改它的任务信息其实也挺简单的,直接修改~/.sqoop/metastore.db.script就行,看起来它是一个sql脚本,但是却是每次运行sqoop都重新执行一遍,大概用的一个mem数据库,不知道可不可以配置成持久化数据库,反正现在弱爆了,本来java启动就慢,每次启动再重构一遍数据表也是蛋疼。不过好处就是改了方便,直接改文本文件就行。

踩过的坑

终于进入了喜大普奔的踩坑环节,因为hadoop全家桶的各个服务实际上都是独立的社区,所以难免会有兼容性问题,所以这里就变成了各种坑让人踩。像CDH/HDP这种全家桶一般都是有过相关兼容性测试的,所以其实还好,如果自己from scratch搭建一套,估计问题更多,下面说几个比较典型的吧。

phoenix-spark不支持Spark 1.5

CDH5.5直接把Spark升级到了1.5版本,而5.4版还是在用1.3,真是挺激进。而phoenix-spark的依赖是Spark1.4,1.5有一些内部结构的变动导致了一些不兼容的问题,详见番号PHOENIX-2287,这是我实实在在遇到的一个问题,就是用spark-sql读取phoenix产生的问题,抛出异常:

java.lang.ClassCastException: org.apache.spark.sql.catalyst.expressions.GenericMutableRow cannot be cast to org.apache.spark.sql.Row

找到了官方JIRA上对应的CASE(PHOENIX-2287),看到在phoenix-spark的4.6.0和4.5.3版本已经修复了这个问题,遗憾的是Cloudera Labs提供的phoenix版本正好是4.5.2,并且他们还没提供更新的版本。这个问题直接影响到我们的一个服务的功能了,所以只能自己动手修复。好在开源大法好,cloudera早就将他们的Phoenix的发行版的源码开源了,可以去它的github上clone出来。并且自己下载2287上面的patch,用git apply功能打上补丁自己build一个phoenix-spark的jar出来替换掉原先的。需要注意的是,一定要checkout出自己CDH对应的的分支,5.5对应是1.2。替换了jar包之后重启HBase就可以了。

Hue用sqlite性能问题

刚装好CDH时Hue总遇到提示DatabaseError: database is locked,而且执行查询经常看不见结果,需要过一会点Recent Queries才能看结果。搜了一下原来是Hue默认用的是sqlite,但是在多用户场景下会存在性能问题,大概是个大锁,所以需要修改一下让它使用pgsql或者mysql。直接在CM的Hue的配置里搜索database相关的就能改了,还是挺方便的。可以参考reference3

CDH5.5中Spark-Hive对于\t分隔符的兼容性问题

这是遇到的一个比较诡异的问题,google上不知道怎么表达好,也没找到类似的按理,但确实是一个可以复现的问题。之前公司有一些Hive表是用\t分隔字段的,但是导入到新的集群之后,在spark中查询结果,返回的字段全是NULL,将分隔符改成\u0001之后或者没有partition的表就没有任何问题,猜想是一个spark1.5和hive1.1之间兼容性的问题吧,反正spark1.5的兼容性已经见怪不怪了。这个问题我在spark-user邮件组里面咨询过,不知道是我表达不好,还是怎样,反正没人鸟我,这个邮件组点击率和回复率都挺低,社区不给力啊。

下面说一下复现方法:

  • 在hive中创建\t分隔的表,并导入\t分隔的文本文件

    $ cat /var/lib/hive/dataimport/mendian/target/test.txt
    1       test
    2       xxxx
    # in hive
    hive> create table `tmp.test_d`(`id` int, `name` string) PARTITIONED BY (`dt` string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'; 
    hive> load data local inpath '/var/lib/hive/dataimport/mendian/target/test.txt' OVERWRITE into table tmp.test_d partition(dt='2016-01-25'); 
    hive> select * from tmp.test_d; 
    1       test    2016-01-25 
    2       xxxx    2016-01-25 
  • 在spark中查看数据,可以看到除partition字段以外,值全是null

    scala> sqlContext.sql("select * from tmp.test_d").collect 
    res9: Array[org.apache.spark.sql.Row] = Array([null,null,2016-01-25], [null,null,2016-01-25]) 

目前解决办法只能是不用\t,全部用\u0001

Conclusion

这次迁移数据还是踩了一些坑的,主要是兼容性方面的问题,所以做集群迁移时,保险期间还是选用相同版本的比较靠谱。总体来说,这两周还是收获颇丰的,至少快速的熟悉了一把hadoop相关的一些工具,对hive/hbase/sqoop/phoenix/spark有了一些新的了解。之前在百度时,基本只用过hadoop streaming,那时候还没有这么多上层工具。现在虽然说对这些工具只是一个了解熟悉,远没有精通,但至少丰富了自己的技术栈,以后遇到问题时可以选择的解决办法又多了一些。

Reference

  1. Apache Phoenix Joins Cloudera Labs
  2. DatabaseError: database is locked
  3. Using an External Database for Hue Using the Command Line

文章作者: Odin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Odin !
  目录