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-size
和index-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作為高性能內存映射隊列持久化。