Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/controller/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ whisk {
# extra time to increase the timeout for forced active acks
# default is 1.minute
timeout-addon = 1m
# amount of lookback on the invokers that have been used for an action most recently
max-recent-invokers = 8
}
controller {
protocol: http
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ package org.apache.openwhisk.core.loadBalancer

import akka.actor.ActorRef
import akka.actor.ActorRefFactory
import java.util.concurrent.ThreadLocalRandom

import java.util.concurrent.ThreadLocalRandom
import akka.actor.{Actor, ActorSystem, Cancellable, Props}
import akka.cluster.ClusterEvent._
import akka.cluster.{Cluster, Member, MemberStatus}
Expand All @@ -40,7 +40,10 @@ import org.apache.openwhisk.core.loadBalancer.InvokerState.{Healthy, Offline, Un
import org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}
import org.apache.openwhisk.spi.SpiLoader

import java.time.Instant
import scala.annotation.tailrec
import scala.collection.immutable.Queue
import scala.collection.mutable
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration

Expand Down Expand Up @@ -204,9 +207,34 @@ class ShardingContainerPoolBalancer(
MetricEmitter.emitGaugeMetric(OFFLINE_INVOKER_BLACKBOX, schedulingState.blackboxInvokers.count(_.status == Offline))
}

class RecentInvokerQueue {
private val maxRecentInvokers = lbConfig.maxRecentInvokers
private var actionLastInvokers: Queue[(Int, Long)] = Queue.empty[(Int, Long)]

def enqueue(invoker: Int): Unit = {
actionLastInvokers = actionLastInvokers.enqueue((invoker, Instant.now.toEpochMilli))
while (actionLastInvokers.size > maxRecentInvokers) { actionLastInvokers = actionLastInvokers.dequeue._2 }
}

/** current algorithm just orders by most frequent the last x invokers completed activations for an action */
def getPreferredInvokers: Seq[Int] = {
val lookbackTimeout = 600000 //10 minutes
//filter out invokers that would represent an activation over 10 minutes old
val validActionLastInvokers =
actionLastInvokers.filter(invokerWithTimestamp => Instant.now.toEpochMilli - invokerWithTimestamp._2 < lookbackTimeout)
if (validActionLastInvokers.size < maxRecentInvokers) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for having this case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to bootstrap by using the home invoker algorithm. This method won't kick in in making scheduling decisions until you have the n lookback activations completed in the last ten minutes for the action.

@quintenp01 quintenp01 Jan 8, 2021

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking there should be two dials then, a min number of activations completed before making scheduling decisions with the lookback and also a max number of invokers to keep in the lookback queue.

@bdoyle0182 bdoyle0182 Jan 9, 2021

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I thought of something similar where you don't include the invoker in the final list unless the number of occurrences in the queue is over some ratio threshold. Is the concern that say the queue has three invokers in it with a max lookback of ten. Invoker 0 has 6 occurrences, invoker 1 has 3 occurrences, and invoker2 has 1 occurrence. You're thinking that invoker2 shouldn't be included in the returned list because the number of occurrences is too low right? I don't think that should affect things too much because the invokers it uses are still going to be bootstrapped off the home invoker algorithm and the same steps and it still prefers the most frequent as in it will try 0, then 1, then 2, and then go back to home invoker stepping. I don't think that should affect things really for an initial test

Seq.empty[Int]
} else {
validActionLastInvokers.map(_._1).groupBy(identity).mapValues(_.size).toSeq.sortWith(_._2 > _._2).map(_._1)
}
}
}

/** State needed for scheduling. */
val schedulingState = ShardingContainerPoolBalancerState()(lbConfig)

private val actionInvokerHistory = mutable.Map[String, RecentInvokerQueue]()

/**
* Monitors invoker supervision and the cluster to update the state sequentially
*
Expand Down Expand Up @@ -273,7 +301,11 @@ class ShardingContainerPoolBalancer(
schedulingState.invokerSlots,
action.limits.memory.megabytes,
homeInvoker,
stepSize)
stepSize,
0,
actionInvokerHistory
.getOrElseUpdate(action.fullyQualifiedName(true).asString, new RecentInvokerQueue)
.getPreferredInvokers)
invoker.foreach {
case (_, true) =>
val metric =
Expand Down Expand Up @@ -325,6 +357,9 @@ class ShardingContainerPoolBalancer(
Some(monitor))

override protected def releaseInvoker(invoker: InvokerInstanceId, entry: ActivationEntry) = {
actionInvokerHistory
.getOrElseUpdate(entry.fullyQualifiedEntityName.asString, new RecentInvokerQueue)
.enqueue(invoker.instance)
schedulingState.invokerSlots
.lift(invoker.toInt)
.foreach(_.releaseConcurrent(entry.fullyQualifiedEntityName, entry.maxConcurrent, entry.memoryLimit.toMB.toInt))
Expand Down Expand Up @@ -396,38 +431,55 @@ object ShardingContainerPoolBalancer extends LoadBalancerProvider {
* @return an invoker to schedule to or None of no invoker is available
*/
@tailrec
def schedule(
maxConcurrent: Int,
fqn: FullyQualifiedEntityName,
invokers: IndexedSeq[InvokerHealth],
dispatched: IndexedSeq[NestedSemaphore[FullyQualifiedEntityName]],
slots: Int,
index: Int,
step: Int,
stepsDone: Int = 0)(implicit logging: Logging, transId: TransactionId): Option[(InvokerInstanceId, Boolean)] = {
def schedule(maxConcurrent: Int,
fqn: FullyQualifiedEntityName,
invokers: IndexedSeq[InvokerHealth],
dispatched: IndexedSeq[NestedSemaphore[FullyQualifiedEntityName]],
slots: Int,
index: Int,
step: Int,
stepsDone: Int = 0,
preferredInvokers: Seq[Int] = Seq.empty[Int])(
implicit logging: Logging,
transId: TransactionId): Option[(InvokerInstanceId, Boolean)] = {
val numInvokers = invokers.size

if (numInvokers > 0) {
val invoker = invokers(index)
//test this invoker - if this action supports concurrency, use the scheduleConcurrent function
if (invoker.status.isUsable && dispatched(invoker.id.toInt).tryAcquireConcurrent(fqn, maxConcurrent, slots)) {
Some(invoker.id, false)
if (preferredInvokers.nonEmpty) {
val preferredInvoker = preferredInvokers
.find(
invokerIndex =>
invokerIndex < invokers.size && invokers(invokerIndex).status.isUsable && dispatched(
invokers(invokerIndex).id.toInt).tryAcquireConcurrent(fqn, maxConcurrent, slots))
.map(invokerIndex => Some((invokers(invokerIndex).id, false)))
if (preferredInvoker.nonEmpty) {
logging.info(this, s"using preferred invoker ${preferredInvoker.get.get} for ${fqn.asString}")
preferredInvoker.get
} else {
schedule(maxConcurrent, fqn, invokers, dispatched, slots, index, step, stepsDone)
}
} else {
// If we've gone through all invokers
if (stepsDone == numInvokers + 1) {
val healthyInvokers = invokers.filter(_.status.isUsable)
if (healthyInvokers.nonEmpty) {
// Choose a healthy invoker randomly
val random = healthyInvokers(ThreadLocalRandom.current().nextInt(healthyInvokers.size)).id
dispatched(random.toInt).forceAcquireConcurrent(fqn, maxConcurrent, slots)
logging.warn(this, s"system is overloaded. Chose invoker${random.toInt} by random assignment.")
Some(random, true)
val invoker = invokers(index)
//test this invoker - if this action supports concurrency, use the scheduleConcurrent function
if (invoker.status.isUsable && dispatched(invoker.id.toInt).tryAcquireConcurrent(fqn, maxConcurrent, slots)) {
Some(invoker.id, false)
} else {
// If we've gone through all invokers
if (stepsDone == numInvokers + 1) {
val healthyInvokers = invokers.filter(_.status.isUsable)
if (healthyInvokers.nonEmpty) {
// Choose a healthy invoker randomly
val random = healthyInvokers(ThreadLocalRandom.current().nextInt(healthyInvokers.size)).id
dispatched(random.toInt).forceAcquireConcurrent(fqn, maxConcurrent, slots)
logging.warn(this, s"system is overloaded. Chose invoker${random.toInt} by random assignment.")
Some(random, true)
} else {
None
}
} else {
None
val newIndex = (index + step) % numInvokers
schedule(maxConcurrent, fqn, invokers, dispatched, slots, newIndex, step, stepsDone + 1)
}
} else {
val newIndex = (index + step) % numInvokers
schedule(maxConcurrent, fqn, invokers, dispatched, slots, newIndex, step, stepsDone + 1)
}
}
} else {
Expand Down Expand Up @@ -601,7 +653,8 @@ case class ClusterConfig(useClusterBootstrap: Boolean)
case class ShardingContainerPoolBalancerConfig(managedFraction: Double,
blackboxFraction: Double,
timeoutFactor: Int,
timeoutAddon: FiniteDuration)
timeoutAddon: FiniteDuration,
maxRecentInvokers: Int)

/**
* State kept for each activation slot until completion.
Expand Down
25 changes: 24 additions & 1 deletion tests/dat/actions/zippedaction/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ class ShardingContainerPoolBalancerTests
managedFraction.getOrElse(1.0 - blackboxFraction),
blackboxFraction,
1,
1.minute)
1.minute,
3)

it should "update invoker's state, growing the slots data and keeping valid old data" in {
// start empty
Expand Down