WITH AKKA-CLUSTERSANE SHARDING
MichaĿ PĿachta
CLUSTERING IS HARD
★ no up-front design
★ load-balance
everything!
★ TIP: design it into
your application!
MEMBERSHIP SERVICE
AKKA-CLUSTER
★ fault-tolerant
★ decentralized
★ peer-to-peer
★ gossip protocols
★ failure detection
PERSIST & RECOVER
AKKA-PERSISTENCE
★ persist internal state of
an actor
★ recover after crash
★ recover after cluster
migration
THE SHOESORTER CASE
MESSAGES
case class Junction(id: Int)

case class Container(id: Int)

case class Conveyor(id: Int)

case class WhereShouldIGo(junction: Junction
container: Container)

case class Go(targetConveyor: Conveyor)
SortingDecider
★ simple actor
★ event-sourced
★ one per junction
★ is asked about container faith
★ limited time to respond
SortingDecider
class SortingDecider extends PersistentActor with ActorLogging {

def receiveCommand: Receive = {

case WhereShouldIGo(junction, container) => {

val targetConveyor = makeDecision(container)

log.info(s"Container ${container.id}
on junction ${junction.id}
directed to ${targetConveyor}")

sender ! Go(targetConveyor)

}

}



def makeDecision(container: Container) = ???
override def receiveRecover: Receive = ???

override def persistenceId: String = ???
}
DecidersGuardian
★ supervising actor for SortingDeciders
★ pipes queries to proper child
★ pipes answers to sender
DecidersGuardian
class DecidersGuardian extends Actor {

implicit val timeout = Timeout(5 seconds)



def receive = {

case msg @ WhereShouldIGo(junction, _) =>

val sortingDecider = getOrCreateChild("J" + junction.id, 

SortingDecider.props)

val futureAnswer = (sortingDecider ? msg).mapTo[Go]

futureAnswer.pipeTo(sender())

}



def getChild(name: String): Option[ActorRef] = context.child(name)



def getOrCreateChild(name: String, props: Props): ActorRef = {

getChild(name) getOrElse context.actorOf(props, name)

}

}

RestInterface
★ Spray-based web service
★ receives ActorRef on creation
★ sends him questions
RestInterface
class RestInterface(exposedPort: Int, decider: ActorRef)
extends Actor with HttpService {

val routes: Route = handleExceptions(exceptionHandler) {

handleRejections(rejectionHandler) {

get {

path("decisions" / IntNumber / IntNumber) { (junctionId,
containerId) =>

complete {

decider

.ask(WhereShouldIGo(

Junction(junctionId), 

Container(containerId)))

.mapTo[Go]

}

}

}

}

}

}

SingleNodeApp
object SingleNodeApp extends App {

val config = ConfigFactory.load()



val system = ActorSystem(config getString "application.name")

sys.addShutdownHook(system.shutdown())



val decidersGuardian = system.actorOf(DecidersGuardian.props)

system.actorOf(

RestInterface.props(

config getInt "application.exposed-port", 

decidersGuardian), 

name = "restInterfaceService")

}
LET’S RUN IT
> http http://localhost:8080/decisions/2/1
HTTP/1.1 200 OK
Content-Length: 33
Content-Type: application/json; charset=UTF-8
Date: Tue, 21 Apr 2015 13:50:08 GMT
Server: spray-can/1.3.2
{
"targetConveyor": "CVR_2_1"
}
15:49:47,715 INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
15:49:48,544 INFO spray.can.server.HttpListener - Bound to /0.0.0.0:8080
15:50:08,403 INFO
c.m.shoesorter.SortingDecider - [single-node akka://shoesorter/user/$a/J2}]
Container 1 on junction 2 directed to CVR_2_1
RestInterface
DecidersGuardian
SortingDecider
SortingDecider
SortingDecider
Journal
SINGLE-NODE SOLUTION
★ non-blocking
★ concurrent
★ scaling up works
★ what about scaling
out?
Node 1
RestInterface
Router
v vSortingDecider
Node 2
Journal
v vSortingDecider
SCALABILITY AS
AFTERTHOUGHT
★ Akka still helps
★ migrations are
held gracefully
★ centralized routing
★ journal contention
SHARDING
AKKA-CLUSTER
★ distribution of actors
★ interact using logical id
★ fine-grained shard
resolution
★ less contention
A SHARD?
★ group of entries
★ entry = sharded
actor
★ managed together
A SHARD ACTOR?
★ creates entries
★ supervises entries
★ not used directly
by us
A SHARD REGION
ACTOR?
★ supervises shards
★ one per node
★ has entry identifier
★ has shard
identifier
Node 1
RestInterface
v vSortingDecider
Node 2
Sharded Journal
v vSortingDecider
ShardRegion ShardRegion
Shard Shard
SortingDecider
class SortingDecider extends PersistentActor with ActorLogging {

def receiveCommand: Receive = {

case WhereShouldIGo(junction, container) => {

val targetConveyor = makeDecision(container)

log.info(s"Container ${container.id}
on junction ${junction.id}
directed to ${targetConveyor}")

sender ! Go(targetConveyor)

}

}



def makeDecision(container: Container) = ???
override def receiveRecover: Receive = ???

override def persistenceId: String = ???
}
NO CHANGE HERE
DecidersGuardian
class DecidersGuardian extends Actor {

implicit val timeout = Timeout(5 seconds)



def receive = {

case msg @ WhereShouldIGo(junction, _) =>

val sortingDecider = getOrCreateChild("J" + junction.id, 

SortingDecider.props)

val futureAnswer = (sortingDecider ? msg).mapTo[Go]

futureAnswer.pipeTo(sender())

}



def getChild(name: String): Option[ActorRef] = context.child(name)



def getOrCreateChild(name: String, props: Props): ActorRef = {

getChild(name) getOrElse context.actorOf(props, name)

}

}

NOT NEEDED
RestInterface
class RestInterface(exposedPort: Int, decider: ActorRef)
extends Actor with HttpService {

val routes: Route = handleExceptions(exceptionHandler) {

handleRejections(rejectionHandler) {

get {

path("decisions" / IntNumber / IntNumber) { (junctionId,
containerId) =>

complete {

decider

.ask(WhereShouldIGo(

Junction(junctionId), 

Container(containerId)))

.mapTo[Go]

}

}

}

}

}

}

NO CHANGE
ShardedApp
object ShardedApp extends App {

Seq(2551, 2552) foreach { port =>

val config = ConfigFactory.parseString("akka.remote.netty.tcp.port=" + port).

withFallback(defaultConfig)



val system = ActorSystem(config getString "clustering.cluster.name", config)



ClusterSharding(system).start(

typeName = SortingDecider.shardName,

entryProps = Some(SortingDecider.props),

idExtractor = SortingDecider.idExtractor,

shardResolver = SortingDecider.shardResolver)



if(port == 2551) {

val decider = ClusterSharding(system).shardRegion(SortingDecider.shardName)

system.actorOf(

RestInterface.props(

defaultConfig getInt "application.exposed-port", 

decider), 

name = "restInterfaceService")

}

}

}

SortingDecider
object SortingDecider {

val props = Props[SortingDecider]



val idExtractor: ShardRegion.IdExtractor = {

case m: WhereShouldIGo => (m.junction.id.toString, m)

}



val shardResolver: ShardRegion.ShardResolver = msg => msg match {

case WhereShouldIGo(junction, _) => (junction.id % 2).toString

}



val shardName = "sortingDecider"

}
LET’S RUN IT
> http http://localhost:8080/decisions/2/1
HTTP/1.1 200 OK
{
"targetConveyor": "CVR_2_2"
}
> http http://localhost:8080/decisions/1/4
{
"targetConveyor": "CVR_1_2"
}
[shoesorter-cluster@127.0.0.1:2551 akka://shoesorter-cluster/user/sharding/
sortingDecider/2}] Container 1 on junction 2 directed to CVR_2_2
[shoesorter-cluster@127.0.0.1:2552 akka://shoesorter-cluster/user/sharding/
sortingDecider/1}] Container 4 on junction 1 directed to CVR_1_2
SHARD
COORDINATOR
★ cluster singleton
★ ShardRegions ask
him about Shard
location
★ pluggable
allocation strategy
★ shard locations are
persisted
ALLOCATION
STRATEGY
★ can be plugged in
to Shard
Coordinator
★ is used during
rebalancing of
shards
SHARD/NODE RATIO?
★ at least 1
★ rule of thumb: 10
★ more = too much
pressure on
coordinator
NEXT MEETUPS
★ clustering with
Docker
★ routing hacking
★ suggestions?
WITH AKKA-CLUSTERSANE SHARDING
MichaĿ PĿachta
Thank you

Sane Sharding with Akka Cluster

  • 1.
  • 2.
    CLUSTERING IS HARD ★no up-front design ★ load-balance everything! ★ TIP: design it into your application!
  • 3.
    MEMBERSHIP SERVICE AKKA-CLUSTER ★ fault-tolerant ★decentralized ★ peer-to-peer ★ gossip protocols ★ failure detection
  • 4.
    PERSIST & RECOVER AKKA-PERSISTENCE ★persist internal state of an actor ★ recover after crash ★ recover after cluster migration
  • 5.
  • 6.
    MESSAGES case class Junction(id:Int)
 case class Container(id: Int)
 case class Conveyor(id: Int)
 case class WhereShouldIGo(junction: Junction container: Container)
 case class Go(targetConveyor: Conveyor)
  • 7.
    SortingDecider ★ simple actor ★event-sourced ★ one per junction ★ is asked about container faith ★ limited time to respond
  • 8.
    SortingDecider class SortingDecider extendsPersistentActor with ActorLogging {
 def receiveCommand: Receive = {
 case WhereShouldIGo(junction, container) => {
 val targetConveyor = makeDecision(container)
 log.info(s"Container ${container.id} on junction ${junction.id} directed to ${targetConveyor}")
 sender ! Go(targetConveyor)
 }
 }
 
 def makeDecision(container: Container) = ??? override def receiveRecover: Receive = ???
 override def persistenceId: String = ??? }
  • 9.
    DecidersGuardian ★ supervising actorfor SortingDeciders ★ pipes queries to proper child ★ pipes answers to sender
  • 10.
    DecidersGuardian class DecidersGuardian extendsActor {
 implicit val timeout = Timeout(5 seconds)
 
 def receive = {
 case msg @ WhereShouldIGo(junction, _) =>
 val sortingDecider = getOrCreateChild("J" + junction.id, 
 SortingDecider.props)
 val futureAnswer = (sortingDecider ? msg).mapTo[Go]
 futureAnswer.pipeTo(sender())
 }
 
 def getChild(name: String): Option[ActorRef] = context.child(name)
 
 def getOrCreateChild(name: String, props: Props): ActorRef = {
 getChild(name) getOrElse context.actorOf(props, name)
 }
 }

  • 11.
    RestInterface ★ Spray-based webservice ★ receives ActorRef on creation ★ sends him questions
  • 12.
    RestInterface class RestInterface(exposedPort: Int,decider: ActorRef) extends Actor with HttpService {
 val routes: Route = handleExceptions(exceptionHandler) {
 handleRejections(rejectionHandler) {
 get {
 path("decisions" / IntNumber / IntNumber) { (junctionId, containerId) =>
 complete {
 decider
 .ask(WhereShouldIGo(
 Junction(junctionId), 
 Container(containerId)))
 .mapTo[Go]
 }
 }
 }
 }
 }
 }

  • 13.
    SingleNodeApp object SingleNodeApp extendsApp {
 val config = ConfigFactory.load()
 
 val system = ActorSystem(config getString "application.name")
 sys.addShutdownHook(system.shutdown())
 
 val decidersGuardian = system.actorOf(DecidersGuardian.props)
 system.actorOf(
 RestInterface.props(
 config getInt "application.exposed-port", 
 decidersGuardian), 
 name = "restInterfaceService")
 }
  • 14.
    LET’S RUN IT >http http://localhost:8080/decisions/2/1 HTTP/1.1 200 OK Content-Length: 33 Content-Type: application/json; charset=UTF-8 Date: Tue, 21 Apr 2015 13:50:08 GMT Server: spray-can/1.3.2 { "targetConveyor": "CVR_2_1" } 15:49:47,715 INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started 15:49:48,544 INFO spray.can.server.HttpListener - Bound to /0.0.0.0:8080 15:50:08,403 INFO c.m.shoesorter.SortingDecider - [single-node akka://shoesorter/user/$a/J2}] Container 1 on junction 2 directed to CVR_2_1
  • 15.
  • 16.
    SINGLE-NODE SOLUTION ★ non-blocking ★concurrent ★ scaling up works ★ what about scaling out?
  • 17.
  • 18.
    SCALABILITY AS AFTERTHOUGHT ★ Akkastill helps ★ migrations are held gracefully ★ centralized routing ★ journal contention
  • 19.
    SHARDING AKKA-CLUSTER ★ distribution ofactors ★ interact using logical id ★ fine-grained shard resolution ★ less contention
  • 20.
    A SHARD? ★ groupof entries ★ entry = sharded actor ★ managed together
  • 21.
    A SHARD ACTOR? ★creates entries ★ supervises entries ★ not used directly by us
  • 22.
    A SHARD REGION ACTOR? ★supervises shards ★ one per node ★ has entry identifier ★ has shard identifier
  • 23.
    Node 1 RestInterface v vSortingDecider Node2 Sharded Journal v vSortingDecider ShardRegion ShardRegion Shard Shard
  • 24.
    SortingDecider class SortingDecider extendsPersistentActor with ActorLogging {
 def receiveCommand: Receive = {
 case WhereShouldIGo(junction, container) => {
 val targetConveyor = makeDecision(container)
 log.info(s"Container ${container.id} on junction ${junction.id} directed to ${targetConveyor}")
 sender ! Go(targetConveyor)
 }
 }
 
 def makeDecision(container: Container) = ??? override def receiveRecover: Receive = ???
 override def persistenceId: String = ??? } NO CHANGE HERE
  • 25.
    DecidersGuardian class DecidersGuardian extendsActor {
 implicit val timeout = Timeout(5 seconds)
 
 def receive = {
 case msg @ WhereShouldIGo(junction, _) =>
 val sortingDecider = getOrCreateChild("J" + junction.id, 
 SortingDecider.props)
 val futureAnswer = (sortingDecider ? msg).mapTo[Go]
 futureAnswer.pipeTo(sender())
 }
 
 def getChild(name: String): Option[ActorRef] = context.child(name)
 
 def getOrCreateChild(name: String, props: Props): ActorRef = {
 getChild(name) getOrElse context.actorOf(props, name)
 }
 }
 NOT NEEDED
  • 26.
    RestInterface class RestInterface(exposedPort: Int,decider: ActorRef) extends Actor with HttpService {
 val routes: Route = handleExceptions(exceptionHandler) {
 handleRejections(rejectionHandler) {
 get {
 path("decisions" / IntNumber / IntNumber) { (junctionId, containerId) =>
 complete {
 decider
 .ask(WhereShouldIGo(
 Junction(junctionId), 
 Container(containerId)))
 .mapTo[Go]
 }
 }
 }
 }
 }
 }
 NO CHANGE
  • 27.
    ShardedApp object ShardedApp extendsApp {
 Seq(2551, 2552) foreach { port =>
 val config = ConfigFactory.parseString("akka.remote.netty.tcp.port=" + port).
 withFallback(defaultConfig)
 
 val system = ActorSystem(config getString "clustering.cluster.name", config)
 
 ClusterSharding(system).start(
 typeName = SortingDecider.shardName,
 entryProps = Some(SortingDecider.props),
 idExtractor = SortingDecider.idExtractor,
 shardResolver = SortingDecider.shardResolver)
 
 if(port == 2551) {
 val decider = ClusterSharding(system).shardRegion(SortingDecider.shardName)
 system.actorOf(
 RestInterface.props(
 defaultConfig getInt "application.exposed-port", 
 decider), 
 name = "restInterfaceService")
 }
 }
 }

  • 28.
    SortingDecider object SortingDecider {
 valprops = Props[SortingDecider]
 
 val idExtractor: ShardRegion.IdExtractor = {
 case m: WhereShouldIGo => (m.junction.id.toString, m)
 }
 
 val shardResolver: ShardRegion.ShardResolver = msg => msg match {
 case WhereShouldIGo(junction, _) => (junction.id % 2).toString
 }
 
 val shardName = "sortingDecider"
 }
  • 29.
    LET’S RUN IT >http http://localhost:8080/decisions/2/1 HTTP/1.1 200 OK { "targetConveyor": "CVR_2_2" } > http http://localhost:8080/decisions/1/4 { "targetConveyor": "CVR_1_2" } [shoesorter-cluster@127.0.0.1:2551 akka://shoesorter-cluster/user/sharding/ sortingDecider/2}] Container 1 on junction 2 directed to CVR_2_2 [shoesorter-cluster@127.0.0.1:2552 akka://shoesorter-cluster/user/sharding/ sortingDecider/1}] Container 4 on junction 1 directed to CVR_1_2
  • 30.
    SHARD COORDINATOR ★ cluster singleton ★ShardRegions ask him about Shard location ★ pluggable allocation strategy ★ shard locations are persisted
  • 31.
    ALLOCATION STRATEGY ★ can beplugged in to Shard Coordinator ★ is used during rebalancing of shards
  • 32.
    SHARD/NODE RATIO? ★ atleast 1 ★ rule of thumb: 10 ★ more = too much pressure on coordinator
  • 33.
    NEXT MEETUPS ★ clusteringwith Docker ★ routing hacking ★ suggestions?
  • 34.