欢迎转载,转载请注明出处:
Spark作为一个分布式处理框架,处理数据非常快。可是我也不知道作者基于什么样的设计哲学,限制了用户对于一些数据的操作。例如你无法改变一个RDD的内容。无法一个split把数据集分割成两份。无法获得RDD的分区信息,无法再map的时候知道自己处于哪个分区。这对于ID分配来说实在是太难办了。甚至想要一个1对1的笛卡尔积都没有。(如果有的话,我们可以计算待分配id数据的个数,然后生成一个数据个数长度的id列表,然后分配给他们即可)
经过我好几天的调查,尝试了很多方法。
最容易想到的方法将数据toLocalIterator,然后再给每条分配id。再join回去,这个方法很慢。而且会把数据放在一个节点上。代价太高。
网上给出的方法也主要是使用其他服务,例如zookeeper来做一个全局请求的发号器。
经过个人研究,实现了两套发号器思路。
1.最快速,最简单的方法适用于id不要求连续的情况,并且数据数目很少的情况。此时可以使用一个hash算法,根据数据的key来计算它的id。数据量少hash碰撞概率很小。毕竟id最主要的目的还是标识数据。这里推荐使用murmurhash。scala代码如下,这里charsets根据自己实际上求hash的key的字符集而定我这里是给URL分配的,因此ASC2的字符集就够(字符集越小,hash碰撞概率也越小):
import com.google.common.base.Charsets
import com.google.common.hash.Hashing
Hashing.murmur3_128().hashString(str, Charsets.US_ASCII).asLong.abs
2.是今天刚想到的一个方法,思路是这样:
利用spark的glom(),将rdd的每个分区转化成Array。这样就可以统计每个分区的元素个数。然后利用个数计算每个分区的数据ID起始下标,将其存到一个Map中,然后广播。话不多说上代码:
假设你的rdd存的是一行行数据,你此次分配的起始id为beginId是1。有两种思路获取带id的part第一种注释掉了,因为这种只能处理数据不同的情况,并且需要两步操作也会多一些。
val beginId = 1
val newMaxId = beginId + allNoIdData.count()//获取此次分配的最大id备用
//val allUrlPartArrayOld = allNoIdUrl.glom().map(e =>
// (if (e.length > 0) e.head else "empty", e)
//).persist(StorageLevel.MEMORY_AND_DISK_SER_2)
val allUrlPartArray = allNoIdData.mapPartitionsWithIndex((partId,iter)=>{
Iterator((partId,iter))//变成了partId+这个part的数据。
}).persist(StorageLevel.MEMORY_AND_DISK_SER_2)
val keyNumMap = allUrlPartArray.map(e => (e._1, e._2.length)).collectAsMap()
val keyBeginIdMap = mutable.HashMap[Int, Long]()
keyNumMap.foreach(e => {
keyBeginIdMap.put(e._1, beginId)
beginId += e._2
})
val keyBeginIdMapBroadCast = sc.broadcast(keyBeginIdMap)
val dataIdRdd = allUrlPartArray.flatMap(e => {
var currentId: Long = keyBeginIdMapBroadCast.value.getOrElse(e._1, newMaxId)
e._2.map(f => {
val result = (f, currentId)
currentId += 1l
result
})
})
如果有感兴趣的可以相互交流。
2018-11-14后记:
再一次表示自己孤陋寡闻了,zipWithIndex()专门做这个事的。