Sane Sharding with Akka Cluster
-
Upload
miciek -
Category
Technology
-
view
139 -
download
0
Transcript of Sane Sharding with Akka Cluster
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
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
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
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 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