├── .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 |
--------------------------------------------------------------------------------