├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── resources ├── URLs.txt ├── application.conf ├── haproxy.conf ├── logback.xml ├── sharded.conf └── shardedURLs.txt └── scala └── com └── michalplachta └── shoesorter ├── DecidersGuardian.scala ├── Decisions.scala ├── Domain.scala ├── Messages.scala ├── SortingDecider.scala └── api ├── RestInterface.scala ├── ShardedApp.scala └── SingleNodeApp.scala /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | .idea 3 | *.iml 4 | target 5 | .classpath 6 | .project 7 | .settings 8 | .DS_Store 9 | out*g 10 | log 11 | .cache 12 | journal 13 | snapshots 14 | .ensime 15 | .ensime_cache 16 | .projectile 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 miciek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sharded Akka Application (example) 2 | 3 | A simple implementation of akka sharding. This is a simulation of [Conveyor Sorting Subsystem](http://i.imgur.com/mctb4HC.gifv). 4 | 5 | This repository serves as a support for my live-coding talk. You can look at slides on [speakerdeck](https://speakerdeck.com/miciek/sane-sharding-with-akka-cluster). There is also a blog post that you can use as a guide to create your own version. Please visit [Scalability using Sharding from Akka Cluster](http://michalplachta.com/2016/01/23/scalability-using-sharding-from-akka-cluster/). 6 | 7 | ## Measuring requests per second 8 | You can test both applications on your local machine by using: 9 | 10 | - `ab` - Apache HTTP server benchmarking tool, 11 | - `parallel` - GNU Parallel - The Command-Line Power Tool, 12 | - `haproxy` - fast and reliable http reverse proxy and load balancer, 13 | - provided [URLs.txt](src/main/resources/URLs.txt) for single-noded app, 14 | - provided [shardedURLs.txt](src/main/resources/shardedURLs.txt) and [haproxy.conf](src/main/resources/haproxy.conf). 15 | 16 | Akka configuration is capped so that we can simulate different conditions on commodity laptop. 17 | 18 | ### SingleNodedApp 19 | Just run `SingleNodedApp` from your IDE or `sbt runSingle` and then: 20 | 21 | ``` 22 | cat src/main/resources/URLs.txt | parallel -j 5 'ab -ql -n 2000 -c 1 -k {}' | grep 'Requests per second' 23 | ``` 24 | 25 | ### ShardedApp 26 | First build the application: 27 | 28 | ```mvn clean package``` 29 | 30 | This will build `target/SortingDecider-1.0-SNAPSHOT-uber.jar`. You will be able to run two nodes by overriding default values: 31 | - first node: `java -jar target/SortingDecider-1.0-SNAPSHOT-uber.jar` 32 | - second node: `java -Dclustering.port=2552 -Dapplication.exposed-port=8081 -jar target/SortingDecider-1.0-SNAPSHOT-uber.jar` 33 | 34 | For benchmarking sharded application you need to use haproxy. Simple configuration for haproxy daemon can be found in resources dir. Run it with: 35 | ```haproxy -f src/main/resources/haproxy.conf```` 36 | 37 | This will set up a round-robing load balancer with frontend on port `8000` and backends on `8080` and `8081`. You can then use different `shardedURLs.txt` file: 38 | 39 | ``` 40 | cat src/main/resources/shardedURLs.txt | parallel -j 5 'ab -ql -n 2000 -c 1 -k {}' | grep 'Requests per second' 41 | ``` 42 | 43 | ### Analysing results 44 | To analyse results of `requests per second` measurements, please read [Scalability Testing section in this blog post](http://michalplachta.com/2016/01/23/scalability-using-sharding-from-akka-cluster/#scalability-testing). 45 | 46 | ## Notes 47 | - Please note that Akka's parallelism in this project is capped in order to test being low on resources. Look at both `application.conf` and `sharded.conf`. 48 | - The project uses a very unorthodox shard resolution function, which can return only two values (`0` or `1`). It is done purely for demonstration purposes. If you want to test scalability of more than 2 nodes, please change this function accordingly. 49 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.michalplachta.shoesorter 8 | SortingDecider 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 2.11.8 13 | 2.4.10 14 | 1.3.0 15 | 1.3.2 16 | 1.1.5 17 | 4.12 18 | 3.0.0 19 | 2.15.2 20 | 2.3 21 | 1.0 22 | 2.7 23 | uber 24 | UTF-8 25 | 26 | 27 | 28 | 29 | org.scala-lang 30 | scala-library 31 | ${scala.version} 32 | 33 | 34 | com.typesafe.akka 35 | akka-actor_2.11 36 | ${akka.version} 37 | 38 | 39 | com.typesafe.akka 40 | akka-cluster_2.11 41 | ${akka.version} 42 | 43 | 44 | com.typesafe.akka 45 | akka-cluster-sharding_2.11 46 | ${akka.version} 47 | 48 | 49 | com.typesafe.akka 50 | akka-remote_2.11 51 | ${akka.version} 52 | 53 | 54 | com.typesafe.akka 55 | akka-distributed-data-experimental_2.11 56 | ${akka.version} 57 | 58 | 59 | com.typesafe.akka 60 | akka-slf4j_2.11 61 | ${akka.version} 62 | 63 | 64 | com.typesafe 65 | config 66 | ${typesafe.config.version} 67 | 68 | 69 | io.spray 70 | spray-can_2.11 71 | ${spray.version} 72 | 73 | 74 | io.spray 75 | spray-routing_2.11 76 | ${spray.version} 77 | 78 | 79 | io.spray 80 | spray-httpx_2.11 81 | ${spray.version} 82 | 83 | 84 | io.spray 85 | spray-http_2.11 86 | ${spray.version} 87 | 88 | 89 | io.spray 90 | spray-json_2.11 91 | ${spray.version} 92 | 93 | 94 | ch.qos.logback 95 | logback-classic 96 | ${logback.version} 97 | 98 | 99 | junit 100 | junit 101 | ${junit.version} 102 | test 103 | 104 | 105 | com.typesafe.akka 106 | akka-testkit_2.11 107 | ${akka.version} 108 | test 109 | 110 | 111 | org.scalatest 112 | scalatest_2.11 113 | ${scalatest.version} 114 | test 115 | 116 | 117 | 118 | 119 | 120 | typesafe 121 | Typesafe Repository 122 | http://maven/nexus/content/repositories/typesafe-releases/ 123 | 124 | 125 | 126 | 127 | src/main/scala 128 | src/test/scala 129 | 130 | 131 | org.scala-tools 132 | maven-scala-plugin 133 | ${maven-scala-plugin.version} 134 | 135 | 136 | 137 | compile 138 | testCompile 139 | 140 | 141 | 142 | 143 | ${scala.version} 144 | 145 | -language:postfixOps 146 | 147 | 148 | 149 | 150 | 151 | org.apache.maven.plugins 152 | maven-shade-plugin 153 | ${maven-shade-plugin.version} 154 | 155 | true 156 | ${shadedClassifierName} 157 | 158 | 159 | *:* 160 | 161 | 162 | 163 | 165 | reference.conf 166 | 167 | 169 | 170 | com.michalplachta.shoesorter.api.ShardedApp 171 | 172 | 173 | 174 | 175 | 176 | 177 | package 178 | 179 | shade 180 | 181 | 182 | 183 | 184 | 185 | 186 | org.apache.maven.plugins 187 | maven-surefire-plugin 188 | ${maven-surefire-plugin.version} 189 | 190 | true 191 | 192 | 193 | 194 | 195 | org.scalatest 196 | scalatest-maven-plugin 197 | ${scalatest-maven-plugin.version} 198 | 199 | 200 | scalatest-test 201 | test 202 | 203 | test 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | org.scala-tools 215 | maven-scala-plugin 216 | ${maven-scala-plugin.version} 217 | 218 | ${scala.version} 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /src/main/resources/URLs.txt: -------------------------------------------------------------------------------- 1 | http://127.0.0.1:8080/junctions/1/decisionForContainer/1 2 | http://127.0.0.1:8080/junctions/2/decisionForContainer/4 3 | http://127.0.0.1:8080/junctions/3/decisionForContainer/5 4 | http://127.0.0.1:8080/junctions/4/decisionForContainer/2 5 | http://127.0.0.1:8080/junctions/5/decisionForContainer/7 6 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = [akka.event.slf4j.Slf4jLogger] 3 | loglevel = debug 4 | 5 | actor { 6 | # capping default-dispatcher for demonstration purposes 7 | default-dispatcher { 8 | fork-join-executor { 9 | # Max number of threads to cap factor-based parallelism number to 10 | parallelism-max = 2 11 | } 12 | } 13 | } 14 | } 15 | 16 | application { 17 | name = sorter 18 | exposed-port = 8080 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/haproxy.conf: -------------------------------------------------------------------------------- 1 | global 2 | daemon 3 | maxconn 256 4 | 5 | defaults 6 | mode http 7 | timeout connect 5000ms 8 | timeout client 5000ms 9 | timeout server 5000ms 10 | 11 | frontend http-in 12 | bind *:8000 13 | default_backend servers 14 | 15 | backend servers 16 | server server1 127.0.0.1:8080 maxconn 32 check 17 | server server2 127.0.0.1:8081 maxconn 32 check 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %date{ISO8601} %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/sharded.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = [akka.event.slf4j.Slf4jLogger] 3 | loglevel = debug 4 | 5 | actor { 6 | provider = "akka.cluster.ClusterActorRefProvider" 7 | 8 | # capping default-dispatcher for demonstration purposes 9 | default-dispatcher { 10 | fork-join-executor { 11 | # Max number of threads to cap factor-based parallelism number to 12 | parallelism-max = 2 13 | } 14 | } 15 | } 16 | 17 | remote { 18 | log-remote-lifecycle-events = off 19 | netty.tcp { 20 | hostname = ${clustering.ip} 21 | port = ${clustering.port} 22 | } 23 | } 24 | 25 | cluster { 26 | seed-nodes = [ 27 | "akka.tcp://"${application.name}"@"${clustering.ip}":2551" 28 | ] 29 | 30 | auto-down-unreachable-after = 10s 31 | sharding.state-store-mode = ddata 32 | } 33 | 34 | extensions = ["akka.cluster.ddata.DistributedData"] 35 | } 36 | 37 | application { 38 | name = sorter 39 | exposed-port = 8080 40 | } 41 | 42 | clustering { 43 | ip = "127.0.0.1" 44 | port = 2551 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/main/resources/shardedURLs.txt: -------------------------------------------------------------------------------- 1 | http://127.0.0.1:8000/junctions/1/decisionForContainer/1 2 | http://127.0.0.1:8000/junctions/2/decisionForContainer/4 3 | http://127.0.0.1:8000/junctions/3/decisionForContainer/5 4 | http://127.0.0.1:8000/junctions/4/decisionForContainer/2 5 | http://127.0.0.1:8000/junctions/5/decisionForContainer/7 6 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/DecidersGuardian.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter 2 | 3 | import akka.actor.{Actor, Props} 4 | import com.michalplachta.shoesorter.Messages._ 5 | 6 | class DecidersGuardian extends Actor { 7 | def receive = { 8 | case m: WhereShouldIGo => 9 | val name = s"J${m.junction.id}" 10 | val actor = context.child(name) getOrElse context.actorOf(Props[SortingDecider], name) 11 | actor forward m 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/Decisions.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter 2 | import com.michalplachta.shoesorter.Domain.{Container, Junction} 3 | 4 | object Decisions { 5 | def whereShouldContainerGo(junction: Junction, container: Container): String = { 6 | Thread.sleep(5) // just to simulate resource hunger 7 | val seed = util.Random.nextInt(10000) 8 | s"CVR_${junction.id}_$seed" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/Domain.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter 2 | 3 | object Domain { 4 | case class Junction(id: Int) 5 | 6 | case class Container(id: Int) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/Messages.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter 2 | 3 | import com.michalplachta.shoesorter.Domain.{Container, Junction} 4 | import spray.json.DefaultJsonProtocol._ 5 | 6 | object Messages { 7 | case class WhereShouldIGo(junction: Junction, container: Container) 8 | 9 | case class Go(targetConveyor: String) 10 | 11 | object Go { 12 | implicit val goJson = jsonFormat1(Go.apply) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/SortingDecider.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter 2 | 3 | import akka.actor.{ActorLogging, Actor, Props} 4 | import akka.cluster.sharding.ShardRegion 5 | import akka.cluster.sharding.ShardRegion.{ExtractEntityId, ExtractShardId} 6 | import com.michalplachta.shoesorter.Domain.{Container, Junction} 7 | import com.michalplachta.shoesorter.Messages._ 8 | 9 | object SortingDecider { 10 | def name = "sortingDecider" 11 | 12 | def props = Props[SortingDecider] 13 | 14 | def extractShardId: ExtractShardId = { 15 | case WhereShouldIGo(junction, _) => 16 | (junction.id % 2).toString 17 | } 18 | 19 | def extractEntityId: ExtractEntityId = { 20 | case msg @ WhereShouldIGo(junction, _) => 21 | (junction.id.toString, msg) 22 | } 23 | } 24 | 25 | class SortingDecider extends Actor with ActorLogging { 26 | def receive = { 27 | case WhereShouldIGo(junction, container) => 28 | val decision = Decisions.whereShouldContainerGo(junction, container) 29 | log.info("Decision on junction {} for container {}: {}", junction.id, container.id, decision) 30 | sender ! Go(decision) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/api/RestInterface.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter.api 2 | 3 | import akka.actor.{Actor, ActorLogging, ActorRef} 4 | import akka.io.IO 5 | import akka.pattern.ask 6 | import com.michalplachta.shoesorter.Domain.{Container, Junction} 7 | import com.michalplachta.shoesorter.Messages._ 8 | import spray.can.Http 9 | import spray.httpx.SprayJsonSupport._ 10 | import spray.routing._ 11 | 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | import scala.concurrent.duration._ 14 | 15 | class RestInterface(decider: ActorRef, exposedPort: Int) extends Actor with HttpServiceBase with ActorLogging { 16 | val route: Route = { 17 | path("junctions" / IntNumber / "decisionForContainer" / IntNumber) { (junctionId, containerId) => 18 | get { 19 | complete { 20 | log.info(s"Request for junction $junctionId and container $containerId") 21 | val junction = Junction(junctionId) 22 | val container = Container(containerId) 23 | decider.ask(WhereShouldIGo(junction, container))(5 seconds).mapTo[Go] 24 | } 25 | } 26 | } 27 | } 28 | 29 | def receive = runRoute(route) 30 | 31 | implicit val system = context.system 32 | IO(Http) ! Http.Bind(self, interface = "0.0.0.0", port = exposedPort) 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/api/ShardedApp.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter.api 2 | 3 | import akka.actor.{Props, ActorSystem} 4 | import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings} 5 | import com.michalplachta.shoesorter.SortingDecider 6 | import com.typesafe.config.ConfigFactory 7 | 8 | object ShardedApp extends App { 9 | val config = ConfigFactory.load("sharded") 10 | implicit val system = ActorSystem(config getString "application.name", config) 11 | 12 | ClusterSharding(system).start( 13 | typeName = SortingDecider.name, 14 | entityProps = SortingDecider.props, 15 | settings = ClusterShardingSettings(system), 16 | extractShardId = SortingDecider.extractShardId, 17 | extractEntityId = SortingDecider.extractEntityId 18 | ) 19 | 20 | val decider = ClusterSharding(system).shardRegion(SortingDecider.name) 21 | system.actorOf(Props(new RestInterface(decider, config getInt "application.exposed-port"))) 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/com/michalplachta/shoesorter/api/SingleNodeApp.scala: -------------------------------------------------------------------------------- 1 | package com.michalplachta.shoesorter.api 2 | 3 | import akka.actor.{ActorSystem, Props} 4 | import com.michalplachta.shoesorter.DecidersGuardian 5 | import com.typesafe.config.ConfigFactory 6 | 7 | object SingleNodeApp extends App { 8 | val config = ConfigFactory.load() 9 | implicit val system = ActorSystem(config getString "application.name") 10 | 11 | val decider = system.actorOf(Props[DecidersGuardian]) 12 | system.actorOf(Props(classOf[RestInterface], decider, 8080)) 13 | } 14 | --------------------------------------------------------------------------------