今天来说一个很诡异的问题,广播变量空指针!
有一个需求:行为表中有1000W人的行为(表名:bt_behavior),但是我只需要特定的500W人的行为,所以直接将行为表和特定的500W人的id关联(表名:dim_user)就可以了,sql大概如下~
#行为表:bt_behavior
#500W人的表:dim_user
select a.* from bt_behavior a
inner join dim_user b
on a.user_id=b.user_id
这是很简单的一个需求,但是因为数据量特别大,导致了spark程序十分不稳定(隔三差五会跑挂掉),因此想尝试使用其他手段来完成这个需求。首当其冲的我的想到广播变量~
其实之前我就有使用过广播变量来完成一些优化,甚至实现了一些用sql实现不了的功能,这里就不多加赘述。
这个需求就是将dim_user加载成一个广播变量,然后再遍历bt_behavior的每一条数据,判断该条数据的user_id是否存在于dim_user这个广播变量中,存在就保留这条数据,不存在就过滤掉~
public void function() {
Dataset btBehavior = spark.table("bt_behavior");
Dataset dimUser = spark.table("bt_behavior")
.select("user_id");
//将dimUser的user_id转成list
List dimUserList = dimUser.map(new MapFunction() {
public String call(Row value) throws Exception {
return value.getString(0);
}
}, Encoders.STRING()).collectAsList();
//初始化一个hashMap来做为广播变量的类型,并且给个初始化大小500W
HashMap map = new HashMap(5000000);
for (String user : dimUserList) {
map.put(user,1);
}
//创建广播变量
ClassTag> tag = (ClassTag) scala.reflect.ClassTag$.MODULE$.apply(HashMap.class);
final Broadcast> broadcastMap = spark.sparkContext().broadcast(map,tag);
Dataset originLog = btBehavior.mapPartitions(new MapPartitionsFunction() {
@Override
public Iterator call(Iterator input) throws Exception {
LinkedList btBehaviorDomainList = new LinkedList();
while (input.hasNext()) {
Row row = input.next();
String user_id = row.getAs("user_id");
//如果该条数据的user_id存在于广播变量中,那就把字段取出
if (broadcastMap.getValue().containsKey(user_id) ) {
String 其他字段1 = row.getAs("其他字段1");
String 其他字段2 = row.getAs("其他字段2");
//...
String 其他字段11 = row.getAs("其他字段11");
String 其他字段12 = row.getAs("其他字段12");
btBehaviorDomainList.add(new btBehaviorDomain(其他字段1,其他字段2,...其他字段11,其他字段12));
}
}
return btBehaviorDomainList.iterator();
}
},Encoders.bean(btBehaviorDomain.class));
//保存数据
saveData(originLog);
//清楚广播变量
broadcastMap.unpersist();
}
===================华丽的分割线======================
那么我就要来说说我在写这段代码的时候遇到的问题
大家看 :
if (broadcastMap.getValue().containsKey(user_id) )
最开始我不是这么写的,我的写法是:
if (broadcastMap.getValue().get(user_id)==1 )
看起来好像也没什么问题,从map中获取key,判断value是否等于1,如果等于1,证明数据存在,不等于1证明不存在
但是就是这样的写法害惨了我,它报了空指针的异常。
然后我就开始百度,大概百度到了如下的这些文章:
https://bbs.csdn.net/topics/392088491?list=lz
https://www.oschina.net/question/1996639_2234863?sort=default
https://stackoverflow.com/questions/38044082/spark-broadcast-variables-can-not-put-into-a-function-otherwise-threw-nullpoint?r=SearchResults
https://stackoverflow.com/questions/28875921/apache-spark-nullpointer-exception-on-broadcast-variables-yarn-cluster-mode
https://community.cloudera.com/t5/Advanced-Analytics-Apache-Spark/Nullpointer-Exception-on-broadcast-variables-YARN-Cluster/m-p/25314#M597
我发现有人和我遇到了一样离奇的问题,这当中的有些回答让我开始觉得是闭包的问题,因此我觉得是代码或者是spark的BUG
然后我开始研究spark的闭包:http://cwiki.apachecn.org/pages/viewpage.action?pageId=2885948
接着我尝试这样的写法:
class SelfMapPartitionsFunction implements MapPartitionsFunction{
private Broadcast> broadcastMap;
public SelfMapPartitionsFunction (Broadcast> broadcastMap){
this.broadcastMap = broadcastMap;
}
@Override
public Iterator call(Iterator input) throws Exception {
//数据处理....
return null;
}
}
//调用
btBehavior.mapPartitions(new SelfMapPartitionsFunction (broadcastMap));
通过外部传参的方式,将广播变量的引用传入到闭包中。经过测试,SelfMapPartitionsFunction类中确实可以拿到广播变量,但是还是会报空指针的错!!
后来,我终于发现了原因,HashMap在get数据的时候,如果key不存在,就会报空指针(java.lang.NullPointException),所以并不是广播变量的问题,而且HashMap在查数据我使用错API的问题,真正判断key是否存在应该使用containsKey!!!
最后我想提三点需要注意的问题:
1、如果你是真的遇到闭包的问题,外部的变量在闭包(内部类)中获取不到,可以尝试我如上的方法,将外部变量通过构造方法或者参数传入,来尝试在闭包中获得。
2、使用LinkedList,而不使用ArrayList,因为LinkedList插入数据会比ArrayList快!
3、广播变量使用HashMap,而不使用list,是因为list的判断是否存在是一个一个迭代遍历的,而HashMap是有优化的,在数据量大的情况下,性能上会好非常非常多!