Scala Akka Streams:如何在2个相关流的系统中建模容量/速率限制?
假设我有一个比萨烤箱和一系列需要烘烤的比萨。我的烤箱一次只能烤4个披萨,可以合理地预计,在一天的过程中,至少有4个在排队,所以烤箱需要尽可能经常满负荷工作 每次我把比萨饼放进烤箱,我都会在手机上设置一个计时器。一旦它熄灭,我就把比萨饼从烤箱里拿出来,给任何想要它的人,容量就变大了 我这里有两个来源,一个是等待烹调的比萨饼的队列,另一个是当比萨饼烹调好后,鸡蛋计时器就会响。系统中还有两个水槽,一个是煮熟的比萨饼的目的地,另一个是发送确认比萨饼已放入烤箱的地方 我现在非常天真地表达了这些,如下所示:Scala Akka Streams:如何在2个相关流的系统中建模容量/速率限制?,scala,akka,akka-stream,Scala,Akka,Akka Stream,假设我有一个比萨烤箱和一系列需要烘烤的比萨。我的烤箱一次只能烤4个披萨,可以合理地预计,在一天的过程中,至少有4个在排队,所以烤箱需要尽可能经常满负荷工作 每次我把比萨饼放进烤箱,我都会在手机上设置一个计时器。一旦它熄灭,我就把比萨饼从烤箱里拿出来,给任何想要它的人,容量就变大了 我这里有两个来源,一个是等待烹调的比萨饼的队列,另一个是当比萨饼烹调好后,鸡蛋计时器就会响。系统中还有两个水槽,一个是煮熟的比萨饼的目的地,另一个是发送确认比萨饼已放入烤箱的地方 我现在非常天真地表达了这些,如下所示:
Source.fromIterator(() => pizzas)
.map(putInOven) // puts in oven and sets a timer
.runWith(Sink.actorRef(confirmationDest, EndSignal))
Source.fromIterator(() => timerAlerts)
.map(removePizza)
.runWith(Sink.actorRef(pizzaDest, EndSignal))
然而,这两个流目前完全相互独立。eggTimer功能正常,每次收集比萨饼时都会将其取出。但它无法向之前的流量发出信号,表明容量已经可用。事实上,第一流根本没有容量的概念,只要比萨饼加入生产线,它就会试图把比萨饼塞进烤箱
什么样的Akka概念可以用来组合这些流,第一个流只有在有容量时才从队列中取出比萨饼,第二个流可以在比萨饼从烤箱中取出时“提醒”第一个流容量的变化
我最初的印象是实现如下流程图:
┌─────────────┐
┌─>│CapacityAvail│>──┐
│ └─────────────┘ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ┌─────────────┐ ├──>│ Zip │>─>│ PutInOven │>─>│ Confirm │
│ │ Queue │>──┘ └─────────────┘ └─────────────┘ └─────────────┘
│ └─────────────┘
│ ┌─────────────┐ ┌─────────────┐
│ │ Done │>─────>│ SendPizza │
│ └─────────────┘ └─────────────┘
│ v
│ │
└─────────┘
支持这一点的原则是,有固定数量的CapacityAvailable对象填充CapacityAvail
源。它们通过进入比萨饼队列的事件压缩,这意味着如果没有可用的事件,则不会启动比萨饼处理,因为压缩操作将等待它们
然后,一旦比萨饼做好,一个电容可用的对象被推回池中
我看到的这个实现的主要障碍是,我不确定如何为CapacityAvail源创建和填充池,也不确定源是否也可以是接收器。是否有任何源/汇/流类型适合于此实现?此用例通常不会很好地映射到Akka流。引擎盖下有一条阿克卡河;从: Akka Streams实现使用反应流接口 在内部,在不同的处理阶段之间传递数据 您的pizza示例不适用于流,因为您有一些外部事件,它们与流的接收器一样是需求的广播者。您公开声明“第一个流根本没有容量概念”这一事实意味着您没有将流用于其预期目的 总是可以使用一些奇怪的编码ju-jitsu来笨拙地弯曲流以解决并发问题,但是您可能会在维护代码时遇到困难。我建议您考虑使用期货、演员或普通老式线程作为并发机制。如果你的烤箱有无限的容量容纳烹饪比萨饼,那么就没有必要从溪流开始 我也会重新检查你的整个设计,因为你使用时钟时间的流逝作为需求的信号(即你的“鸡蛋计时器”)。这通常表明工艺设计中存在缺陷。如果您无法绕过此要求,则应评估其他设计模式:
您可以使用
mapsyncUnordered
stage和parallelism=4
来表示烤箱。未来的完成可以通过计时器()完成,也可以因为其他原因决定将其从烤箱中取出。这就是我最后使用的。在这个问题上,它几乎是一个伪状态机的精确实现。Source.queue
的机制比我希望的要笨拙得多,但它在其他方面相当干净。真正的汇和源是作为参数提供的,并在其他地方构建,因此实际实现的样板文件比这少一些
RunnableGraph.fromGraph(GraphDSL.create() {
implicit builder: GraphDSL.Builder[NotUsed] =>
import GraphDSL.Implicits._
// Our Capacity Bucket. Can be refilled by passing CapacityAvaiable objects
// into capacitySrc. Can be consumed by using capacity as a Source.
val (capacity, capacitySrc) =
peekMatValue(Source.queue[CapacityAvailable.type](CONCURRENT_CAPACITY,
OverflowStrategy.fail))
// Set initial capacity
capacitySrc.foreach(c =>
Seq.fill(CONCURRENT_CAPACITY)(CapacityAvailable).foreach(c.offer))
// Pull pizzas from the RabbitMQ queue
val cookQ = RabbitSource(rabbitControl, channel(qos = CONCURRENT_CAPACITY),
consume(queue("pizzas-to-cook")), body(as[TaskRun]))
// Take the blocking events stream and turn into a source
// (Blocking in a separate dispatcher)
val cookEventsQ = Source.fromIterator(() => oven.events().asScala)
.withAttributes(ActorAttributes.dispatcher("blocking-dispatcher"))
// Split the events stream into two sources so 2 flows can be attached
val bc = builder.add(Broadcast[PizzaEvent](2))
// Zip pizzas with the capacity pool. Stops cooking pizzas when oven full.
// When cooking starts, send the confirmation back to rabbitMQ
cookQ.zip(AckedSource(capacity)).map(_._1)
.mapAsync(CONCURRENT_CAPACITY)(pizzaOven.cook)
.map(Message.queue(_, "pizzas-started-cooking"))
.acked ~> Sink.actorRef(rabbitControl, HostDied)
// Send the cook events stream into two flows
cookEventsQ ~> bc.in
// The first tops up the capacity pool
bc.out(0)
.mapAsync(CONCURRENT_CAPACITY)(e =>
capacitySrc.flatMap(cs => cs.offer(CapacityAvailable))
) ~> Sink.ignore
// The second sends out cooked events
bc.out(1)
.map(p => Message.queue(Cooked(p.id()), "pizzas-cooked")
) ~> Sink.actorRef(rabbitControl, HostDied)
ClosedShape
}).run()
你是想实施节流吗?在某种意义上,我想是的。但不是通过时间或速率限制进行节流,而是通过应用程序级业务逻辑进行节流。节流是否会根据我们正在处理的项目而变化,或者是您愿意动态更改的每个流的全局变量?这是任意时间量。基本上,每个“比萨饼”都是一个任务队列中的一个任务,它将被执行,可能需要0到无限秒的时间,但只能同时执行4次。好的,我喜欢你的一般想法。在创建我的初始图表之后,我还意识到我实际上已经对有限状态机进行了建模,因此我将研究akka utils,因为它可能更合适。时钟只是一个例子,现在发生的是,我分派一个任务(一个“比萨饼”),并从另一个无关的来源听到它的完成情况。“我并没有在我的应用程序中真正使用计时器。”杰克古怪我正要说和拉蒙一样的话。当我今天早上收到你的评论时,我离开了,你要么需要使用mapsync
,要么使用Futures/actors实现它。作为练习,我还尝试使用Akka FSM作为有限状态机重写,并使用工作拉动模式。