34
WITH AKKA-CLUSTER SANE SHARDING MichaĿ PĿachta

Sane Sharding with Akka Cluster

  • Upload
    miciek

  • View
    138

  • Download
    0

Embed Size (px)

Citation preview

WITH AKKA-CLUSTERSANE SHARDINGMichaĿ PĿachta

CLUSTERING IS HARD★ no up-front design

★ load-balance everything!

★ TIP: design it into your application!

MEMBERSHIP SERVICEAKKA-CLUSTER

★ fault-tolerant ★ decentralized ★ peer-to-peer ★ gossip protocols ★ failure detection

PERSIST & RECOVERAKKA-PERSISTENCE

★ persist internal state of an actor

★ recover after crash ★ recover after cluster

migration

THE SHOESORTER CASE

MESSAGEScase 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 1RestInterface

Router

v vSortingDecider

Node 2

Journal

v vSortingDecider

SCALABILITY AS AFTERTHOUGHT

★ Akka still helps

★ migrations are held gracefully

★ centralized routing

★ journal contention

SHARDINGAKKA-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 1RestInterface

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

ShardedAppobject 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" }

[[email protected]:2551 akka://shoesorter-cluster/user/sharding/sortingDecider/2}] Container 1 on junction 2 directed to CVR_2_2 [[email protected]: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 SHARDINGMichaĿ PĿachta

Thank you