代码的精简往往能带来很大的提升,这一点甚至比外部的调优效果更好。比如去除一些不必要的filter、选择合适的算子
所谓shuffle,就是在网络间传输数据。Spark作业运行过程中,最消耗性能的地方就是shuffle过程。shuffle过程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。比如reduceByKey、join等算子,都会触发shuffle操作。
shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。
因此在我们的开发过程中,能避免则尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子,尽量使用map类的非shuffle算子。这样的话,没有shuffle操作或者仅有较少shuffle操作的Spark作业,可以大大减少性能开销。
一般的,从kafka中接入数据直接读取Topic中的消息即可。但对于集群抖动、作业重启等等原因,消费程序没有及时处理Topic中的数据,那么就会出现数据丢失的情况。一般的解决办法是纪律kafka的偏移量。推荐使用zk去管理偏移量,也可以使用mysql去记录。
下面是一个使用Mysql管理偏移量的DEMO:
/**
* create kafka dstream, Topic: gamesdk_gamecenter_v2_mautual
* @param ssc
* @return
*/
def getOrCreateMautualDStream(ssc: StreamingContext): InputDStream[(String, GamecenterV2Mautual)] = {
readOffsetsFromDB(StreamingKafkaUtils.topics.GAMESDK_GAMECENTER_V2_MAUTUAL) match {
// 偏移量数据表里有数据,则从获取的偏移量位置开始读
case Some(offsetMap) =>
logger.info("获取偏移量 -> GET")
val msgHandler = (mmd: MessageAndMetadata[String, GamecenterV2Mautual]) => (mmd.key, mmd.message)
KafkaUtils.createDirectStream[String, GamecenterV2Mautual, StringDecoder, StreamingKafkaUtils.decoderClasses.GamecenterV2MautualDecoder
, (String, GamecenterV2Mautual)](ssc, kafkaParams2, offsetMap, msgHandler)
// 否则读取最新的数据
case None =>
logger.info("获取偏移量 -> 未获得偏移量")
KafkaUtils.createDirectStream[String, GamecenterV2Mautual, StringDecoder, StreamingKafkaUtils.decoderClasses.GamecenterV2MautualDecoder](
ssc, kafkaParams1, Set(StreamingKafkaUtils.topics.GAMESDK_GAMECENTER_V2_MAUTUAL))
}
}
/**
* 从mysql读取某个kafka topic的offsets
* @param topic
* @return
*/
@throws(classOf[Exception])
def readOffsetsFromDB(topic: String): Option[Map[TopicAndPartition, Long]] = {
var conn: Connection = null
var ps: PreparedStatement = null
val sql = "select topic, part, off from zjf_kafka_realtime_offsets where topic = ? and groupId = ?"
try {
conn = MysqlConnectionUtils.getSqlConnection(db02_driverUrl, db02_user, db02_pwd).get
ps = conn.prepareStatement(sql)
conn.setAutoCommit(false)
try {
ps.setString(1, topic)
ps.setString(2, groupId)
val resultSet = ps.executeQuery()
if (!resultSet.next()) {
None
} else {
val tapList = new ListBuffer[(TopicAndPartition, Long)]
tapList += (TopicAndPartition(resultSet.getString(1), resultSet.getInt(2)) -> resultSet.getLong(3))
while (resultSet.next()) {
tapList += (TopicAndPartition(resultSet.getString(1), resultSet.getInt(2)) -> resultSet.getLong(3))
}
Some(tapList.toMap)
}
} catch {
case e: SQLException =>
logger.error(s"错误! 读取偏移量失败! -> sql error:${e.getStackTraceString}")
None
} finally {
ps.close()
conn.close()
}
}
}
一般的,有下列几种常用的设置参数:
参数设置建议写入spark-submit的提交设置中,以–conf方式提交,无需改动代码、重复打包
1. 优雅的streaming结束方式。Spark会在JVM关闭时正常关闭StreamingContext,而不是立即关闭。
.set("spark.streaming.stopGracefullyOnShutdown", "true")
2. 开启Driver多线程机制
.set("spark.driver.allowMultipleContexts", "true")
3.开启自动调节机制,根据当前的处理速度自适应调节每次读取的kafka数据量
.set("spark.streaming.backpressure.enabled", "true")
4.限定每个分区最大的读取速度,具体值又作业代码决定
.set("spark.streaming.kafka.maxRatePerPartition", "5000")
5.开启FAIR并行BATCH执行模式
.set("spark.scheduler.mode","FAIR")
6.最大并行BATCHS:100
.set("spark.streaming.concurrentJobs","100")
7.定制优化JVM内存
--conf spark.executor.extraJavaOptions=\"-XX:MaxDirectMemorySize=4096m\" \
8.开启预测执行
--conf spark.speculation=true \
--conf spark.speculation.interval=500ms \
9.设置Java heap占用spark menory的比例(可能已废弃)
--conf spark.storage.memoryFraction=0.5 \
10.设置shuffle占用spark menory的比例(可能已废弃)
--conf spark.shuffle.memoryFraction=0.3 \
11.开启序列化超类(在诸如使用Proto结构的代码中,pb对象如果在网络间传输,不开启此项一定会报错)
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \