|
| 1 | +Stadis - Stand-alone Distributed System |
| 2 | +======================================= |
| 3 | +The easiest way to learn, develop and test a distributed system. |
| 4 | + |
| 5 | +Why should I use it? |
| 6 | +==================== |
| 7 | +Testing distributed system is very expensive. |
| 8 | + |
| 9 | +It takes a large amount of resources, takes a lot of time to deploy and config. |
| 10 | + |
| 11 | +More importantly, as your application is running on multiple machine, |
| 12 | +it's nearly impossible to coordinate the network state with your application state. |
| 13 | +as a result, there will be lots of error handling code leave untested, |
| 14 | +which can cause serious problem once that actually happen. |
| 15 | + |
| 16 | +With stadis, you can run a multi-data-center distributed system on a single machine. |
| 17 | + |
| 18 | +You can change the topology and node state at any time by a single API call. |
| 19 | +Everything happens exactly the way you want. |
| 20 | +Every network error handling code can be easily covered. |
| 21 | + |
| 22 | +Get started |
| 23 | +=========== |
| 24 | +Require Go 1.2+ installed. |
| 25 | + |
| 26 | +Stadis can be used as a library in Go application. |
| 27 | + |
| 28 | +For applications written in other programming language, stadis can be used as a proxy server. |
| 29 | + |
| 30 | +###Use stadis as a library in Go program. |
| 31 | + |
| 32 | +Hello world example |
| 33 | + |
| 34 | + package main |
| 35 | + |
| 36 | + import ( |
| 37 | + "fmt" |
| 38 | + "github.com/coocood/stadis" |
| 39 | + "io" |
| 40 | + "log" |
| 41 | + "net/http" |
| 42 | + "os" |
| 43 | + "time" |
| 44 | + ) |
| 45 | + |
| 46 | + func main() { |
| 47 | + //Start a api server. |
| 48 | + go http.ListenAndServe(stadis.Cli.ApiAddr, stadis.NewApiServer()) |
| 49 | + //Setup the http client dialer. |
| 50 | + http.DefaultTransport.(*http.Transport).Dial = stadis.NewDialFunc("matter.metal.gold", 5*time.Second) |
| 51 | + |
| 52 | + eagleListener, err := stadis.Listen("tcp", "localhost:8585", "animal.air.eagle") |
| 53 | + if err != nil { |
| 54 | + log.Fatal(err) |
| 55 | + } |
| 56 | + |
| 57 | + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){ |
| 58 | + w.Write([]byte("hello\n")) |
| 59 | + }) |
| 60 | + |
| 61 | + go http.Serve(eagleListener, nil) |
| 62 | + |
| 63 | + time.Sleep(time.Millisecond) |
| 64 | + before := time.Now() |
| 65 | + resp, err := http.Get("http://localhost:8585/") |
| 66 | + if err != nil { |
| 67 | + log.Fatal(err) |
| 68 | + } |
| 69 | + io.Copy(os.Stdout, resp.Body) |
| 70 | + resp.Body.Close() |
| 71 | + //the latency should be a little more than 888ms which is define at stadis.DefaultConfig. |
| 72 | + fmt.Println("latency:", time.Now().Sub(before)) |
| 73 | + } |
| 74 | + |
| 75 | +###Run stadis as a API/proxy server. |
| 76 | + |
| 77 | +Build and Run |
| 78 | + |
| 79 | + go get github.com/coocood/stadis |
| 80 | + go build github.com/coocood/stadis/stadiserver |
| 81 | + ./stadiserver |
| 82 | + |
| 83 | +Now, the stadis API server is started at port `8989`. |
| 84 | + |
| 85 | +Then start a trivial http server as the origin server on port `12345` by run |
| 86 | + |
| 87 | + go run $GOROOT/src/pkg/net/http/triv.go |
| 88 | + |
| 89 | +Then call the REST API to open a proxy |
| 90 | + |
| 91 | + curl -X POST 'http://localhost:8989/proxy?clientName=matter.metal.gold&proxyName=animal.air.eagle&proxyPort=8586&originAddr=localhost:12345' |
| 92 | + |
| 93 | +We can ask the API server for the dial state: |
| 94 | + |
| 95 | + curl 'http://localhost:8989/dialState?clientName=matter.metal.gold&serverPort=8586' |
| 96 | + |
| 97 | +You will get dial state like `{"Latency":444000000,"OK":true}`, the latency unit is nano second. |
| 98 | +Normally you should sleep for that amount of time in your program before actually dial the proxy server. |
| 99 | +If 'OK' is 'false' you should return error or throw an exception. |
| 100 | + |
| 101 | +Then request the proxy server and record the time. |
| 102 | + |
| 103 | + time curl -i 'http://localhost:8586/counter' |
| 104 | + |
| 105 | +The real time should be a little more than 444ms, which is the roundtrip time from 'matter.metal.gold' to 'animal.air.eagle'. |
| 106 | + |
| 107 | +Configuration |
| 108 | +=============== |
| 109 | +Stadis server does not use any config file, it starts with a default configuration, and you can update it by REST API call. |
| 110 | + |
| 111 | +Stadis API server maintains a virtual topology which has three level:'DataCenter', 'Rack' and 'Host'. |
| 112 | +All of them has a 'NodeState' of attributes 'Name', 'Latency', 'InternalDown' and 'ExternalDown' . |
| 113 | +The topology contains multiple 'DataCenter', which contains multiple 'Rack', which in turn contains |
| 114 | +multiple 'Host'. |
| 115 | + |
| 116 | +We all know naming is hard, so I did the hard work for you. |
| 117 | +After spent many hours, I managed to come up with 39 node names. |
| 118 | +The topology has three data centers, each data center has three racks, each rack has three hosts. |
| 119 | + |
| 120 | + { |
| 121 | + "DcDefault":{"Latency":100000000}, |
| 122 | + "RackDefault":{"Latency":10000000}, |
| 123 | + "HostDefault":{"Latency":1000000}, |
| 124 | + "DataCenters":[ |
| 125 | + { |
| 126 | + "Name":"animal", |
| 127 | + "Racks":[ |
| 128 | + { |
| 129 | + "Name":"land", |
| 130 | + "Hosts":[{"Name":"tiger"},{"Name":"lion"},{"Name":"wolf"}] |
| 131 | + }, |
| 132 | + { |
| 133 | + "Name":"sea", |
| 134 | + "Hosts":[{"Name":"shark"},{"Name":"whale"},{"Name":"cod"}] |
| 135 | + }, |
| 136 | + { |
| 137 | + "Name":"air", |
| 138 | + "Hosts":[{"Name":"eagle"},{"Name":"crow"},{"Name":"owl"}] |
| 139 | + } |
| 140 | + ] |
| 141 | + }, |
| 142 | + { |
| 143 | + "Name":"plant", |
| 144 | + "Racks":[ |
| 145 | + { |
| 146 | + "Name":"fruit", |
| 147 | + "Hosts":[{"Name":"apple"},{"Name":"pear"},{"Name":"grape"}] |
| 148 | + }, |
| 149 | + { |
| 150 | + "Name":"crop", |
| 151 | + "Hosts":[{"Name":"corn"},{"Name":"rice"},{"Name":"wheat"}] |
| 152 | + }, |
| 153 | + { |
| 154 | + "Name":"flower", |
| 155 | + "Hosts":[{"Name":"rose"},{"Name":"lily"},{"Name":"lotus"}] |
| 156 | + } |
| 157 | + ] |
| 158 | + }, |
| 159 | + { |
| 160 | + "Name":"matter", |
| 161 | + "Racks":[ |
| 162 | + { |
| 163 | + "Name":"metal", |
| 164 | + "Hosts":[{"Name":"gold"},{"Name":"silver"},{"Name":"iron"}] |
| 165 | + }, |
| 166 | + { |
| 167 | + "Name":"gem", |
| 168 | + "Hosts":[{"Name":"ruby"},{"Name":"ivory"},{"Name":"pearl"}] |
| 169 | + }, |
| 170 | + { |
| 171 | + "Name":"liquid", |
| 172 | + "Hosts":[{"Name":"water"},{"Name":"oil"},{"Name":"wine"}] |
| 173 | + } |
| 174 | + ] |
| 175 | + } |
| 176 | + ] |
| 177 | + } |
| 178 | + |
| 179 | +In default configuration, each data center has 100ms latency, each rack has 10ms latency, each host has 1ms latency. |
| 180 | + |
| 181 | +So the latency from 'matter.metal.gold' to 'animal.air.eagle' should be "1ms+10ms+100ms+100ms+10ms+1ms = 222ms" |
| 182 | + |
| 183 | +The round trip time should be 444ms, so the total time to create a connection |
| 184 | +and then make a http request from 'gold' to 'eagle' should be a little more than 888ms. |
| 185 | + |
| 186 | +REST API |
| 187 | +================ |
| 188 | + |
| 189 | +- Update configuration with json payload like the default config shown above. |
| 190 | + |
| 191 | + POST /config |
| 192 | + |
| 193 | + |
| 194 | +- Update node state with json body like `{"Latency":10000000,"InternalDown":false,"ExternalDown":true}` |
| 195 | + |
| 196 | + POST /nodeState?name=%s |
| 197 | + |
| 198 | + The 'name' parameter is the node name in the topology, e.g. "animal.air.eagle". |
| 199 | + |
| 200 | + |
| 201 | +- Get node state: |
| 202 | + |
| 203 | + GET /nodeState?name=%s |
| 204 | + |
| 205 | + |
| 206 | +- Start a proxy: |
| 207 | + |
| 208 | + POST /proxy?clientName=%s&proxyName=%s&proxyPort=%s&originAddr=%s |
| 209 | + |
| 210 | + 'clientName' defines where the client process is located in the topology. |
| 211 | + 'proxyName' defines where the proxy is located in the topology. |
| 212 | + 'proxyPort' will be opened and accepts incoming connection. |
| 213 | + 'originAddr' is the origin server address for the proxy, it can be an address in another machine, |
| 214 | + eg. `192.168.1.100:12345`, so if you can not run the origin server on your localhost, you can proxy it. |
| 215 | + |
| 216 | + |
| 217 | +- Stop a proxy: |
| 218 | + |
| 219 | + DELETE /proxy?proxyPort=%s |
| 220 | + |
| 221 | + |
| 222 | +- Get dial state from a client to server. |
| 223 | + |
| 224 | + GET /dialState?clientName={clientName}&serverPort={serverPort} |
| 225 | + |
| 226 | + The response is a json object like `{"Latency":10000000,"OK":true}` |
| 227 | + |
| 228 | + |
| 229 | +- Get the current connection state after that connection has been created. |
| 230 | + |
| 231 | + GET /connState?clientPort={clientPort}&serverPort={serverPort} |
| 232 | + |
| 233 | +Performance |
| 234 | +============ |
| 235 | +Stadis adds an extra layer on top of tcp connection, the throughput is greatly decreased. |
| 236 | +On my laptop, a proxy connection with 1ms latency gets about 30MB/s throughput. |
| 237 | +I think it is sufficient for most of applications for testing purpose. |
| 238 | +Higher latency gets lower throughput which is pretty much the way raw connections work. |
| 239 | + |
| 240 | +LICENSE |
| 241 | +======== |
| 242 | +The MIT License |
0 commit comments