年關臨近,各種雜事很多,開啟短文模式,春節(jié)假期過后再恢復正常。
在Spark Streaming程序中,我們經常需要使用有狀態(tài)的流來統(tǒng)計一些累積性的指標,比如各個商品的PV。簡單的代碼描述如下,使用mapWithState()算子:
val productPvStream = stream.mapPartitions(records => {
var result = new ListBuffer[(String, Int)]
for (record <- records) {
result += Tuple2(record.key(), 1)
}
result.iterator
}).reduceByKey(_ + _).mapWithState(
StateSpec.function((productId: String, pv: Option[Int], state: State[Int]) => {
val sum = pv.getOrElse(0) + state.getOption().getOrElse(0)
state.update(sum)
(productId, sum)
})).stateSnapshots()
現(xiàn)在的問題是,PV并不是一直累加的,而是每天歸零,重新統(tǒng)計數據。如果是Flink的話,我們可以通過在算子里注冊Timer直接觸發(fā)狀態(tài)清除。而在Spark Streaming中要達到在凌晨0點清除狀態(tài)的目的,有以下兩種不那么直接的方法。
編寫腳本重啟Streaming程序
用crontab、Azkaban等在凌晨0點調度執(zhí)行下面的Shell腳本:
stream_app_name='com.xyz.streaming.MallForwardStreaming'
cnt=`ps aux | grep SparkSubmit | grep ${stream_app_name} | wc -l`
if [ ${cnt} -eq 1 ]; then
pid=`ps aux | grep SparkSubmit | grep ${stream_app_name} | awk '{print $2}'`
kill -9 ${pid}
sleep 20
cnt=`ps aux | grep SparkSubmit | grep ${stream_app_name} | wc -l`
if [ ${cnt} -eq 0 ]; then
nohup sh /path/to/streaming/bin/mall_forward.sh > /path/to/streaming/logs/mall_forward.log 2>&1
fi
fi
這種方式最簡單,也不需要對程序本身做任何改動。但隨著同時運行的Streaming任務越來越多,就會顯得越來越累贅了。
給StreamingContext設置超時
在程序啟動的時候,同時計算出當前時間點距離第二天凌晨0點的毫秒數:
def msTillTomorrow = {
val now = new Date()
val tomorrow = new Date(now.getYear, now.getMonth, now.getDate + 1)
tomorrow.getTime - now.getTime
}
然后將Streaming程序的主要邏輯寫在一個while(true)循環(huán)中,并且不像平常一樣調用StreamingContext.awaitTermination()方法,而改用awaitTerminationOrTimeout()方法,即:
while (true) {
val ssc = new StreamingContext(sc, Seconds(BATCH_INTERVAL))
ssc.checkpoint(CHECKPOINT_DIR)
// ...處理邏輯...
ssc.start()
ssc.awaitTerminationOrTimeout(msTillTomorrow)
ssc.stop(false, true)
Thread.sleep(BATCH_INTERVAL * 1000)
}
在經過msTillTomorrow毫秒之后,StreamingContext就會超時,再調用其stop()方法(注意它有兩個參數,stopSparkContext表示是否順帶停止關聯(lián)的SparkContext,stopGracefully表示是否優(yōu)雅停止),就可以停止并重啟StreamingContext。
以上兩種方法都是仍然采用Spark Streaming的機制進行狀態(tài)計算的。如果其他條件允許的話,我們還可以拋棄mapWithState(),直接借助外部存儲自己維護狀態(tài)。比如將Redis的Key設計為product_pv:[product_id]:[date],然后在Spark Streaming的每個批次中使用incrby指令,就能方便地統(tǒng)計PV了,不必考慮定時的問題。