squbs-17. 持久化緩沖區(PersistentBuffer)

PersistentBuffer 是一系列 Akka Streams 流組建的第一個。它像 Akka Streams緩沖區一樣工作,不同的是,緩存的內容存儲于一系列內存映射的文件中,由 PersistentBuffer構造提供。這可以讓緩沖區大小實際無上限,不使用JVM堆來存儲,在同時處理百萬級/每秒時性能優異。

依賴

以下依賴在Persistent Buffer工作時需要:

"org.squbs" %% "squbs-pattern" % squbsVersion,
"net.openhft" % "chronicle-queue" % "4.5.13"

例子

以下的例子顯示了 PersistentBuffer在流中的用法:

implicit val serializer = QueueSerializer[ByteString]()
val source = Source(1 to 1000000).map { n => ByteString(s"Hello $n") }
val buffer = new PersistentBuffer[ByteString](new File("/tmp/myqueue"))
val counter = Flow[Any].map( _ => 1L).reduce(_ + _).toMat(Sink.head)(Keep.right)
val countFuture = source.via(buffer.async).runWith(counter)

此版本在GraphDSL顯示相同

implicit val serializer = QueueSerializer[ByteString]()
val source = Source(1 to 1000000).map { n => ByteString(s"Hello $n") }
val buffer = new PersistentBuffer[ByteString](new File("/tmp/myqueue"))
val counter = Flow[Any].map( _ => 1L).reduce(_ + _).toMat(Sink.head)(Keep.right)
val streamGraph = RunnableGraph.fromGraph(GraphDSL.create(counter) { implicit builder =>
  sink =>
    import GraphDSL.Implicits._
    source ~> buffer.async ~> sink
    ClosedShape
})
val countFuture = streamGraph.run()

背壓(Back-Pressure)

PersistentBuffer不背壓上游流量。它將獲取給它的所有的流元素,并且通過增長或旋轉隊列文件的數量來增長存儲。它沒有任何方法確定緩沖區的限制或存儲大小。下游流量背壓按照每個Akka Streams和Reactive Streams的要求進行。

如果PersistentBuffer stage被下游流量混淆, PersistentBuffer不會緩存并且它實際上會背壓。為了保證PersistentBuffer確實運行在它自己的空間,在這之后加入一個async 邊界。

失敗 & 恢復

由于它的持久特性, PersistentBuffer可以從突然的流關閉,故障,JVM故障甚至潛在的系統故障中恢復。在同一個目錄通過 PersistentBuffer重啟流將啟動發出存貯在緩存中的元素,在新的元素加入進來之前不會消費。在先前的流故障或關閉時緩存中正在消費的元素(并未消費完成)將會丟失。

因為緩存通過本地存儲、心軸或SSD,因此這個緩存的性能和耐久性同樣取決于存儲的耐久性。所以,理解和推斷緩存的耐久性非常重要,與那些數據庫和其他熱離線持久存儲不是同一個級別,以換取更高的性能。

Akka Streams stage批處理請求并在內部緩存記錄。 PersistentBuffer保證回復和記錄的持久化到達onPush, Akka Stream stage內部緩存的記錄未到達onPush 將會在故障中丟失。

提交保證(Commit Guarantee)

在一個不可預知的故障情況中,從 PersistentBuffer stage發出的元素卻未抵達sink將會丟失。有些情況下,它可能需要避免此類數據丟失。在 sink之前使用commit stage對這類情況有幫助。加入 commit stage,使用 PersistentBufferAtLeastOnce 。請參考下面commit stage 用法的例子:

implicit val serializer = QueueSerializer[ByteString]()
val source = Source(1 to 1000000).map { n => ByteString(s"Hello $n") }
val tempPath = new File("/tmp/myqueue")
val config = ConfigFactory.parseMap {
    Map(
      "persist-dir" -> s"${tempPath.getAbsolutePath}"
    )
  }
val buffer = new PersistentBufferAtLeastOnce[ByteString](config)
val commit = buffer.commit[ByteString]
val flowSink = // do some transformation or a sink flow with expected failure
val counter = Flow[Any].map( _ => 1L).reduce(_ + _).toMat(Sink.head)(Keep.right)
val streamGraph = RunnableGraph.fromGraph(GraphDSL.create(counter) { implicit builder =>
  sink =>
    import GraphDSL.Implicits._
    // ensures that records are reprocessed when something fails at tranform flow
    source ~> buffer ~> flowSink ~> commit ~> sink 
    ClosedShape
})
val countFuture = streamGraph.run()

請注意,commit 無法防止在 sink(或者其他commit之后的stage)內部緩存中的丟失。

提交訂單(Commit Order)

commit stage應該正常按照順序接收元素。然而,流中的一個潛在的bug可能引起一個元素丟棄或不按順序抵達 commit stage。默認的commit-order-policy設置為 lenient,使流繼續運行在這個場景中。你可以設置為 strict,以便拋出CommitOrderException 異常,并讓Supervision.Decider確定要執行的操作。

空間管理

一個典型的持久隊列目錄查看如下:

$ ls -l
-rw-r--r--  1 squbs_user     110054053  83886080 May 17 20:00 20160518.cq4
-rw-r--r--  1 squbs_user     110054053      8192 May 17 20:00 tailer.idx

當所有的讀者成功處理讀取queue,隊列文件自動刪除。

配置

隊列通過傳遞一個保存了所有默認配置的持久化目錄的地址創建。所有的例子可以在上面看到。或者,它可以通過傳遞在構建時一個 Config對象創建。 Config 對象是一個標準的HOCON 配置。下面的例子展示了使用Config構建PersistentBuffer

val configText =
  """
    | persist-dir = /tmp/myQueue
    | roll-cycle = xlarge_daily
    | wire-type = compressed_binary
    | block-size = 80m
  """.stripMargin
val config = ConfigFactory.parseString(configText)

//使用Config構建緩存
val buffer = new PersistentBuffer[ByteString](config)

下面的配置屬性用于 PersistentBuffer

persist-dir = /tmp/myQueue # Required
roll-cycle = daily         # Optional, defaults to daily
wire-type = binary         # Optional, defaults to binary
block-size = 80m           # Optional, defaults to 64m
index-spacing = 16k        # Optional, defaults to roll-cycle's spacing 
index-count = 16           # Optional, defaults to roll-cycle's count
commit-order-policy = lenient # Optional, default to lenient

Roll-cycle可以用大寫或小寫指定。roll-cycle支持以下值:

Roll Cycle 容量(Capacity)
MINUTELY 64 million entries per minute
HOURLY 256 million entries per hour
SMALL_DAILY 512 million entries per day
DAILY 4 billion entries per day
LARGE_DAILY 32 billion entries per day
XLARGE_DAILY 2 trillion entries per day
HUGE_DAILY 256 trillion entries per day

Wire-type可以通過大寫或者小寫指定。wire-type支持以下值:

  • TEXT
  • BINARY
  • FIELDLESS_BINARY
  • COMPRESSED_BINARY
  • JSON
  • RAW
  • CSV

內存大小諸如 block-sizeindex-spacing 依據memory size format defined in the HOCON specification指定。

序列化(Serialization)

QueueSerializer[T] 需要被隱式的提供給PersistentBuffer[T],如上面的例子所示:

implicit val serializer = QueueSerializer[ByteString]()

QueueSerializer[T]() 為你的目標類型調用生產一個序列化器(Serializer)。它基于基礎設施的序列化和反序列化。

實現Serializer

控制隊列中細粒度的持久化格式,你可能需要實現你自己的序列化器(serializer)如下:

case class Person(name: String, age: Int)

class PersonSerializer extends QueueSerializer[Person] {

  override def readElement(wire: WireIn): Option[Person] = {
    for {
      name <- Option(wire.read().`object`(classOf[String]))
      age <- Option(wire.read().int32)
    } yield { Person(name, age) }
  }

  override def writeElement(element: Person, wire: WireOut): Unit = {
    wire.write().`object`(classOf[String], element.name)
    wire.write().int32(element.age)
  }
}

使用這個序列化器(serializer),只需要在構建PersistentBuffer之前聲明它為隱式的,如下:

implicit val serializer = new PersonSerializer()
val buffer = new PersistentBuffer[Person](new File("/tmp/myqueue")

廣播緩存(Broadcast Buffer)

BroadcastBuffer是持久化緩存的一個變種。這個工作與PersistentBuffer相似,流元素廣播至多個輸出端口。因此它是緩存和廣播stage的組合。這個配置采用一個名為output-ports的附加參數,用于指定輸出端口的數量。

當流元素從每個輸出端口發出(以獨立的速度,取決于下游的速度要求)時,特別需要廣播緩存。

val configText =
  """
    | persist-dir = /tmp/myQueue
    | roll-cycle = xlarge_daily
    | wire-type = compressed_binary
    | block-size = 80m
    | output-ports = 3
  """.stripMargin
val config = ConfigFactory.parseString(configText)

// Construct the buffer using a Config.
val bcBuffer = new BroadcastBuffer[ByteString](config)

例子

implicit val serializer = QueueSerializer[ByteString]()

val in = Source(1 to 100000)
val flowCounter = Flow[Any].map(_ => 1L).reduce(_ + _).toMat(Sink.head)(Keep.right)

val streamGraph = RunnableGraph.fromGraph(GraphDSL.create(flowCounter) { implicit builder =>
      sink =>
        import GraphDSL.Implicits._
        val buffer = new BroadcastBufferAtLeastOnce[ByteString](config)
        val commit = buffer.commit[ByteString]
        val bcBuffer = builder.add(buffer.async)
        val mr = builder.add(merge)
        in ~> transform ~> bcBuffer ~> commit ~> mr ~> sink
                           bcBuffer ~> commit ~> mr
                           bcBuffer ~> commit ~> mr
        ClosedShape
    })
    
val countFuture = streamGraph.run()

積分(Credits)

PersistentBuffer 利用Chronicle-Queue 4.x作為高性能內存映射隊列持久化。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容