sparkStreaming 消費下沉 kafka 以及調優

sparkStreaming 消費下沉 kafka 以及調優

1 sparkStreaming 消費kafka

主要方式有兩種:receiver方式、Direct方式

Receiver方式利用kafka高階的api,將數據存儲到exectors,這種方法會丟失數據,要確保零丟失需要開啓WAL (write ahead log)即將數據存到 hdfs上面一份 需要設置 KafkaUtils.createStream(..., StorageLevel.MEMORY_AND_DISK_SER)),即使數據會丟失也能從hdfs恢復,但是會冗餘,maven依賴如下

groupId = org.apache.spark
 artifactId = spark-streaming-kafka-0-8_2.12
 version = 2.4.5
 import org.apache.spark.streaming.kafka._

 val kafkaStream = KafkaUtils.createStream(streamingContext,
     [ZK quorum], [consumer group id], [per-topic number of Kafka partitions to consume])

需要注意的是kafka topic分區並不和rdd分區數量一致

Direct方式從spark1.3開始 比較推薦 並行 spark partition 和 kafka topic partition 1:1 沒有receiver一說

sparkStreaming會創建和kafka topic partition相同數量的rdd partition

Receiver方法使用kafka 高階api 將offset存到zookeeper中 會導致sparkStreaming消費不同步

Direct將offset存入到checkpoint 之中

 groupId = org.apache.spark
 artifactId = spark-streaming-kafka-0-8_2.12
 version = 2.4.5
import org.apache.spark.streaming.kafka._

 val directKafkaStream = KafkaUtils.createDirectStream[
     [key class], [value class], [key decoder class], [value decoder class] ](
     streamingContext, [map of Kafka parameters], [set of topics to consume])

2 sparkStreaming寫入到kafka之中

一般是處理之後寫入到數據庫 ,目前需要將數據寫入到下游的kafka topic之中,供下游去消費

一般的思路是

input.foreachRDD(rdd =>
  // 不能在這裏創建KafkaProducer
  rdd.foreachPartition(partition =>
    partition.foreach{
      case x:String=>{
        val props = new HashMap[String, Object]()
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
          "org.apache.kafka.common.serialization.StringSerializer")
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
          "org.apache.kafka.common.serialization.StringSerializer")
        println(x)
        val producer = new KafkaProducer[String,String](props)
        val message=new ProducerRecord[String, String]("output",null,x)
        producer.send(message)
      }
    }
  )
) 

這種方式缺點很明顯每個rdd的每個partition的每條數據信息都要生成一個kafkaProduce效率太低了,kafkaProducer不能在partition之外創建,因爲其不能序列化

正確的方法是:

1 首先 需要將kafkaProducer利用lazy val 的方式進行包裝如下

import java.util.concurrent.Future
import org.apache.kafka.clients.producer.{ KafkaProducer, ProducerRecord, RecordMetadata }
class KafkaSink[K, V](createProducer: () => KafkaProducer[K, V]) extends Serializable {
  /* This is the key idea that allows us to work around running into
     NotSerializableExceptions. */
  lazy val producer = createProducer()
  def send(topic: String, key: K, value: V): Future[RecordMetadata] =
    producer.send(new ProducerRecord[K, V](topic, key, value))
  def send(topic: String, value: V): Future[RecordMetadata] =
    producer.send(new ProducerRecord[K, V](topic, value))
}

object KafkaSink {
  import scala.collection.JavaConversions._
  def apply[K, V](config: Map[String, Object]): KafkaSink[K, V] = {
    val createProducerFunc = () => {
      val producer = new KafkaProducer[K, V](config)
      sys.addShutdownHook {
        // Ensure that, on executor JVM shutdown, the Kafka producer sends
        // any buffered messages to Kafka before shutting down.
        producer.close()
      }
      producer
    }
    new KafkaSink(createProducerFunc)
  }
  def apply[K, V](config: java.util.Properties): KafkaSink[K, V] = apply(config.toMap)
}

2 之後利用廣播變量的形式,將kafkaProducer廣播到每一個exector之中

//廣播kafkaProducer
val kafkaProducer:Broadcast[KafkaSink[String,String]]={
val kafkaProducerConfig={
	val p=new Properties()
	p.setProperty("bootstrap.servers", Conf.brokers)
    p.setProperty("key.serializer", classOf[StringSerializer].getName)
    p.setProperty("value.serializer", classOf[StringSerializer].getName)
    p
}
log.warn("kafka producer init done!")
ssc.sparkContext.broadcast(kafkaSink[String,String](kafkaProducerConfig))
}

這樣就能在每個exector中將數據輸入到kafka之中

//輸出到kafka
data.foreachRDD(rdd=>{
	if(!rdd.isEmpty){
		rdd.foreach(record=>{
			kafkaProducer.send(Conf.outTopics,record._1.toString,record._2)
		})
	}
})

3. sparkStreaming + kafka調優

合理的批處理時間 (batchDuration)

幾乎所有的Spark Streaming調優文檔都會提及批處理時間的調整,在StreamingContext初始化的時候,有一個參數便是批處理時間的設定。如果這個值設置的過短,即個batchDuration所產生的Job並不能在這期間完成處理,那麼就會造成數據不斷堆積,最終導致Spark Streaming發生阻塞。而且,一般對於batchDuration的設置不會小於500ms,因爲過小會導致SparkStreaming頻繁的提交作業,對整個streaming造成額外的負擔。在平時的應用中,根據不同的應用場景和硬件配置,我設在1~10s之間,我們可以根據SparkStreaming的可視化監控界面,觀察Total Delay來進行batchDuration的調整

合理的kafka拉去量(maxRatePerPartition)

對於Spark Streaming消費kafka中數據的應用場景,這個配置是非常關鍵的,配置參數爲:spark.streaming.kafka.maxRatePerPartition。這個參數默認是沒有上線的,即kafka當中有多少數據它就會直接全部拉出。而根據生產者寫入Kafka的速率以及消費者本身處理數據的速度,同時這個參數需要結合上面的batchDuration,使得每個partition拉取在每個batchDuration期間拉取的數據能夠順利的處理完畢,做到儘可能高的吞吐量,而這個參數的調整可以參考可視化監控界面中的Input Rate和Processing Time

緩存反覆使用的Dstream

Spark中的RDD和SparkStreaming中的Dstream,如果被反覆的使用,最好利用cache(),將該數據流緩存起來,防止過度的調度資源造成的網絡開銷。可以參考觀察Scheduling Delay參數

設置合理的GC

長期使用Java的小夥伴都知道,JVM中的垃圾回收機制,可以讓我們不過多的關注與內存的分配回收,更加專注於業務邏輯,JVM都會爲我們搞定。對JVM有些瞭解的小夥伴應該知道,在Java虛擬機中,將內存分爲了初生代(eden generation)、年輕代(young generation)、老年代(old generation)以及永久代(permanent generation),其中每次GC都是需要耗費一定時間的,尤其是老年代的GC回收,需要對內存碎片進行整理,通常採用標記-清楚的做法。同樣的在Spark程序中,JVM GC的頻率和時間也是影響整個Spark效率的關鍵因素。在通常的使用中建議:

--conf "spark.executor.extraJavaOptions=-XX:+UseConcMarkSweepGC"

設置合理的cpu資源數

CPU的core數量,每個executor可以佔用一個或多個core,可以通過觀察CPU的使用率變化來了解計算資源的使用情況,例如,很常見的一種浪費是一個executor佔用了多個core,但是總的CPU使用率卻不高(因爲一個executor並不總能充分利用多核的能力),這個時候可以考慮讓麼個executor佔用更少的core,同時worker下面增加更多的executor,或者一臺host上面增加更多的worker來增加並行執行的executor的數量,從而增加CPU利用率。但是增加executor的時候需要考慮好內存消耗,因爲一臺機器的內存分配給越多的executor,每個executor的內存就越小,以致出現過多的數據spill over甚至out of memory的情況。

設置合理的parallelism

partition和parallelism,partition指的就是數據分片的數量,每一次task只能處理一個partition的數據,這個值太小了會導致每片數據量太大,導致內存壓力,或者諸多executor的計算能力無法利用充分;但是如果太大了則會導致分片太多,執行效率降低。在執行action類型操作的時候(比如各種reduce操作),partition的數量會選擇parent RDD中最大的那一個。而parallelism則指的是在RDD進行reduce類操作的時候,默認返回數據的paritition數量(而在進行map類操作的時候,partition數量通常取自parent RDD中較大的一個,而且也不會涉及shuffle,因此這個parallelism的參數沒有影響)。所以說,這兩個概念密切相關,都是涉及到數據分片的,作用方式其實是統一的。通過spark.default.parallelism可以設置默認的分片數量,而很多RDD的操作都可以指定一個partition參數來顯式控制具體的分片數量。
在SparkStreaming+kafka的使用中,我們採用了Direct連接方式,前文闡述過Spark中的partition和Kafka中的Partition是一一對應的,我們一般默認設置爲Kafka中Partition的數量。

使用高性能的算子

這裏參考了美團技術團隊的博文,並沒有做過具體的性能測試,其建議如下:

  • 使用reduceByKey/aggregateByKey替代groupByKey
  • 使用mapPartitions替代普通map
  • 使用foreachPartitions替代foreach
  • 使用filter之後進行coalesce操作
  • 使用repartitionAndSortWithinPartitions替代repartition與sort類操作

使用Kyro優化序列化性能

這個優化原則我本身也沒有經過測試,但是好多優化文檔有提到,這裏也記錄下來。
在Spark中,主要有三個地方涉及到了序列化:

  • 在算子函數中使用到外部變量時,該變量會被序列化後進行網絡傳輸(見“原則七:廣播大變量”中的講解)。
  • 將自定義的類型作爲RDD的泛型類型時(比如JavaRDD,Student是自定義類型),所有自定義類型對象,都會進行序列化。因此這種情況下,也要求自定義的類必須實現Serializable接口。
  • 使用可序列化的持久化策略時(比如MEMORY_ONLY_SER),Spark會將RDD中的每個partition都序列化成一個大的字節數組。

對於這三種出現序列化的地方,我們都可以通過使用Kryo序列化類庫,來優化序列化和反序列化的性能。Spark默認使用的是Java的序列化機制,也就是ObjectOutputStream/ObjectInputStream API來進行序列化和反序列化。但是Spark同時支持使用Kryo序列化庫,Kryo序列化類庫的性能比Java序列化類庫的性能要高很多。官方介紹,Kryo序列化機制比Java序列化機制,性能高10倍左右。Spark之所以默認沒有使用Kryo作爲序列化類庫,是因爲Kryo要求最好要註冊所有需要進行序列化的自定義類型,因此對於開發者來說,這種方式比較麻煩。

以下是使用Kryo的代碼示例,我們只要設置序列化類,再註冊要序列化的自定義類型即可(比如算子函數中使用到的外部變量類型、作爲RDD泛型類型的自定義類型等)

// 創建SparkConf對象。
val conf = new SparkConf().setMaster(...).setAppName(...)
// 設置序列化器爲KryoSerializer。
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
// 註冊要序列化的自定義類型。
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

參考🔗鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章