一、大数据场景下一份测试数据可能要几十G、几百G,用单机生成即浪费空间时间还长。可以用mapreduce或者spark来并行生成。
需求:使用spark生成1份几百G的测试数据,根据一年12个月均匀分布。
- 一开始没拐过弯来的是:spark要先有rdd,这个rdd怎么建,建个hdfs上空目录的rdd?此处利用rdd的惰性,先把整个大数据在内存中建好,然后在各个分区执行。
- 如果上面行不通(因为在创建rdd之前就要在内存创建好数据,这个数据很大,此时还没有牵扯到rdd,可能会内存溢出),就先在driver用parallelize创建1个以12个月份为key的rdd,这个rdd一共就12条数据,然后给这个rdd用HashPartitioner(12)来分成12个区,然后在rdd.mapPartitionsWithIndex中分别创建,这个方法100%会成功。
这个思路普遍适用于用分布式程序(不一定是分布式分析和计算)
二、使用spark
1.利用rdd的惰性,先在driver中用parallelize创建数据的rdd,然后分区,然后用mapPartition或者mapPartitionsWithIndex在各个分区将数据写入到同一个hdfs目录下。
2.须知:
- 分区号是从0开始计数的
- partitionBy(Partitioner)中的
org.apache.spark.HashPartitioner(12)
是根据key分区的。
3.example1
package cn.dc.mockdata
import java.io.{BufferedWriter, FileOutputStream, OutputStreamWriter}
import org.apache.spark.sql.SparkSession
import scala.collection.mutable.ListBuffer
object SparkMockData {
def main(args: Array[String]): Unit = {
val ss = SparkSession.builder().master("local[*]").appName("mock").getOrCreate()
case class Test(date: Int, name: String, id: String)
val list = ListBuffer(Test(1, "1", "1"), Test(2, "2", "2"))
list += Test(3, "3", "3")
list += Test(3, "4", "4")
list.append(Test(3, "4", "4"))
list :+ Test(3, "4", "4")
val rdd = ss.sparkContext.parallelize(list).map(test => (test.date -> test.name)).partitionBy(new org.apache.spark.HashPartitioner(12))
rdd.mapPartitionsWithIndex(
(partIdx, iter) => {
val writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("test/",true)))
val part_map = scala.collection.mutable.Map[Int, Test]()
while (iter.hasNext) {
val next = iter.next
println(s"$partIdx : $next")
writer.append(s"$partIdx").write(next.toString())
}
writer.flush()
writer.close()
part_map.iterator
}).take(1)
}
}
4.example2
package cn.dc.mockdata
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FileSystem, Path}
import org.apache.spark.Partitioner
import org.apache.spark.sql.SparkSession
import org.slf4j.LoggerFactory
object SparkMockData2 {
val log = LoggerFactory.getLogger(classOf[SparkMockData2])
def main(args: Array[String]): Unit = {
val ss = SparkSession.builder().appName("mock-tencent-data").getOrCreate()
val sc = ss.sparkContext
val seq = Seq(1 to 12)
/*
构建rdd
将RDD转成pairRDD
如果想要更多的分区,比如24个分区,可以设置24个数,然后MyPartitioner中对12取余即可。
*/
val rdd = sc.parallelize(seq).map(num => num -> num).partitionBy(new MyPartitioner(12))
/*
广播fs,12减少为3
亲测,会报错,并发相关的错,只能一个partition创建1个fs
val broadcast = sc.broadcast(FileSystem.get(new Configuration()))
*/
rdd.mapPartitionsWithIndex(
(partIdx, iter) => {
val fs = FileSystem.get(new Configuration())
val dirPath = new Path(f"/user/hive/dctest/tencent_big_all")
if (!fs.exists(dirPath)) fs.mkdirs(dirPath)
val filePath = new Path(f"$dirPath/2018${partIdx + 1}%02d")
if (fs.exists(filePath)) fs.delete(filePath,false)
val writer = fs.create(filePath)
//这里必须创建1个无关的map返回,直接返回iter会报错,原因未知
val part_map = scala.collection.mutable.Map[Int, Int]()
for (i <- 0 until 1000) {
val template = s"2018${partIdx + 1}" + "1"+"2"
if (i == 999) writer.write(template.getBytes("UTF-8"))
else writer.write((template + "\n").getBytes("UTF-8"))
}
part_map.iterator
}).take(1)
}
}
class MyPartitioner(numParts: Int) extends Partitioner {
//设置总分区数,使用设置的总分区数覆盖默认的分区数
override def numPartitions: Int = numParts
//根据key返回分区号,从0开始计数,比如12个分区,就是0~11
override def getPartition(key: Any): Int = key.toString.toInt - 1
}
class SparkMockData2{}
分布式场景下生成唯一id
- 尽量不用UUID,占空间,join时浪费性能。
- 一开始用的是时间戳+随机数+分区号,即使是5位随机数的情况下,并行度50的情况下,1亿条会有2.5万的重复,这是不可接受的,时间戳精确到毫秒,1毫秒会生成260多条数据。而且这个策略占用很多空间,时间戳的前半部分但部分是重复的,总长度也在20以上。
- 实际上亿只有9位,理论上10位就足够了。这里借鉴了zipWithUniqueId里的分布式递增生成主键的算法。每个分区初始值为分区号,然后每个分区递增的步长为分区数,假设总数为1亿,分区数为20,这种算法相当于把1亿分成了20份,每个分区间隔占有自己的部分。完美的分布式唯一id生成算法。
example1
一开始想在driver创建1个(o until 100000000)的rdd,然后用zipWithUniqueId,但运行时发现因为资源不够,直接卡住,给driver增加到7G内存和5个core都不行,而且这种方式本身就违反了spark的设计初衷。但小数据量下是正常运行的。
example2
创建0 until 20的rdd,然后mapPartitionWithIndex,在里面用for(i <- Range(partIdx,sum,partNum))的方式创建id,完美解决问题