democracyworks-synapse 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.mailmap +3 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/Makefile +6 -0
- data/README.md +241 -0
- data/Rakefile +1 -0
- data/bin/synapse +62 -0
- data/config/hostheader_test.json +71 -0
- data/config/svcdir_test.json +46 -0
- data/config/synapse.conf.json +90 -0
- data/config/synapse_services/service1.json +24 -0
- data/config/synapse_services/service2.json +24 -0
- data/lib/synapse.rb +79 -0
- data/lib/synapse/base.rb +5 -0
- data/lib/synapse/haproxy.rb +759 -0
- data/lib/synapse/log.rb +24 -0
- data/lib/synapse/service_watcher.rb +32 -0
- data/lib/synapse/service_watcher/base.rb +105 -0
- data/lib/synapse/service_watcher/dns.rb +103 -0
- data/lib/synapse/service_watcher/docker.rb +115 -0
- data/lib/synapse/service_watcher/ec2tag.rb +26 -0
- data/lib/synapse/service_watcher/zookeeper.rb +127 -0
- data/lib/synapse/version.rb +3 -0
- data/spec/lib/synapse/haproxy_spec.rb +13 -0
- data/spec/lib/synapse/service_watcher_base_spec.rb +55 -0
- data/spec/lib/synapse/service_watcher_docker_spec.rb +138 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/config.rb +9 -0
- data/spec/support/minimum.conf.yaml +27 -0
- data/synapse.gemspec +25 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 035f5185668b79655c47279251a52255efa293a0
|
4
|
+
data.tar.gz: 578f659d158fde0be97f2c852ecbe44df03692a8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 32eda48b033735638ca19e21c2022ca2db80886829ee303a0c03a368398a4207e01e5d39211cdd23ccf8fc65540f95b0ae32ca9d3b7ab846ec13772bbd71e6a2
|
7
|
+
data.tar.gz: 6ebf7d1cfd7f504793bf1d09191c7e49f67f564fc11e9196065d3d458d499e53457d190a8000e4f7d33abcee303a703a581346c670dc0efb0698482a4e94c7a7
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*~
|
19
|
+
.vagrant
|
20
|
+
.*sw?
|
21
|
+
vendor/
|
22
|
+
|
23
|
+
synapse.jar
|
data/.mailmap
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Original work Copyright (c) 2013 Airbnb, Inc.
|
2
|
+
Modified work Copyright (c) 2014 Democracy Works, Inc.
|
3
|
+
|
4
|
+
MIT License
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Makefile
ADDED
data/README.md
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
# Synapse #
|
2
|
+
|
3
|
+
Synapse is Airbnb's new system for service discovery.
|
4
|
+
Synapse solves the problem of automated fail-over in the cloud, where failover via network re-configuration is impossible.
|
5
|
+
The end result is the ability to connect internal services together in a scalable, fault-tolerant way.
|
6
|
+
|
7
|
+
## Motivation ##
|
8
|
+
|
9
|
+
Synapse emerged from the need to maintain high-availability applications in the cloud.
|
10
|
+
Traditional high-availability techniques, which involve using a CRM like [pacemaker](http://linux-ha.org/wiki/Pacemaker), do not work in environments where the end-user has no control over the networking.
|
11
|
+
In an environment like Amazon's EC2, all of the available workarounds are suboptimal:
|
12
|
+
|
13
|
+
* Round-robin DNS: Slow to converge, and doesn't work when applications cache DNS lookups (which is frequent)
|
14
|
+
* Elastic IPs: slow to converge, limited in number, public-facing-only, which makes them less useful for internal services
|
15
|
+
* ELB: Again, public-facing only, and only useful for HTTP
|
16
|
+
|
17
|
+
One solution to this problem is a discovery service, like [Apache Zookeeper](http://zookeeper.apache.org/).
|
18
|
+
However, Zookeeper and similar services have their own problems:
|
19
|
+
|
20
|
+
* Service discovery is embedded in all of your apps; often, integration is not simple
|
21
|
+
* The discovery layer itself it subject to failure
|
22
|
+
* Requires additional servers/instances
|
23
|
+
|
24
|
+
Synapse solves these difficulties in a simple and fault-tolerant way.
|
25
|
+
|
26
|
+
## How Synapse Works ##
|
27
|
+
|
28
|
+
Synapse runs on your application servers; here at Airbnb, we just run it on every box we deploy.
|
29
|
+
The heart of synapse is actually [HAProxy](http://haproxy.1wt.eu/), a stable and proven routing component.
|
30
|
+
For every external service that your application talks to, we assign a synapse local port on localhost.
|
31
|
+
Synapse creates a proxy from the local port to the service, and you reconfigure your application to talk to the proxy.
|
32
|
+
|
33
|
+
Synapse comes with a number of `watchers`, which are responsible for service discovery.
|
34
|
+
The synapse watchers take care of re-configuring the proxy so that it always points at available servers.
|
35
|
+
We've included a number of default watchers, including ones that query zookeeper and ones using the AWS API.
|
36
|
+
It is easy to write your own watchers for your use case, and we encourage submitting them back to the project.
|
37
|
+
|
38
|
+
## Example Migration ##
|
39
|
+
|
40
|
+
Lets suppose your rails application depends on a Postgre database instance.
|
41
|
+
The database.yaml file has the DB host and port hardcoded:
|
42
|
+
|
43
|
+
```yaml
|
44
|
+
production:
|
45
|
+
database: mydb
|
46
|
+
host: mydb.example.com
|
47
|
+
port: 5432
|
48
|
+
```
|
49
|
+
|
50
|
+
You would like to be able to fail over to a different database in case the original dies.
|
51
|
+
Let's suppose your instance is running in AWS and you're using the tag 'proddb' set to 'true' to indicate the prod DB.
|
52
|
+
You set up synapse to proxy the DB connection on `localhost:3219` in the `synapse.conf.json` file.
|
53
|
+
Add a hash under `services` that looks like this:
|
54
|
+
|
55
|
+
```json
|
56
|
+
{"services":
|
57
|
+
"proddb": {
|
58
|
+
"default_servers": [
|
59
|
+
{
|
60
|
+
"name": "default-db",
|
61
|
+
"host": "mydb.example.com",
|
62
|
+
"port": 5432
|
63
|
+
}
|
64
|
+
],
|
65
|
+
"discovery": {
|
66
|
+
"method": "awstag",
|
67
|
+
"tag": "proddb",
|
68
|
+
"value": "true"
|
69
|
+
},
|
70
|
+
"haproxy": {
|
71
|
+
"port": 3219,
|
72
|
+
"server_options": "check inter 2000 rise 3 fall 2",
|
73
|
+
"frontend": [
|
74
|
+
"mode tcp",
|
75
|
+
],
|
76
|
+
"backend": [
|
77
|
+
"mode tcp",
|
78
|
+
],
|
79
|
+
},
|
80
|
+
},
|
81
|
+
...
|
82
|
+
```
|
83
|
+
|
84
|
+
And then change your database.yaml file to look like this:
|
85
|
+
|
86
|
+
```yaml
|
87
|
+
production:
|
88
|
+
database: mydb
|
89
|
+
host: localhost
|
90
|
+
port: 3219
|
91
|
+
```
|
92
|
+
|
93
|
+
Start up synapse.
|
94
|
+
It will configure HAProxy with a proxy from `localhost:3219` to your DB.
|
95
|
+
It will attempt to find the DB using the AWS API; if that does not work, it will default to the DB given in `default_servers`.
|
96
|
+
In the worst case, if AWS API is down and you need to change which DB your application talks to, simply edit the `synapse.conf.json` file, update the `default_servers` and restart synapse.
|
97
|
+
HAProxy will be transparently reloaded, and your application will keep running without a hiccup.
|
98
|
+
|
99
|
+
## Installation
|
100
|
+
|
101
|
+
Add this line to your application's Gemfile:
|
102
|
+
|
103
|
+
gem 'synapse'
|
104
|
+
|
105
|
+
And then execute:
|
106
|
+
|
107
|
+
$ bundle
|
108
|
+
|
109
|
+
Or install it yourself as:
|
110
|
+
|
111
|
+
$ gem install synapse
|
112
|
+
|
113
|
+
## Configuration ##
|
114
|
+
|
115
|
+
Synapse depends on a single config file in JSON format; it's usually called `synapse.conf.json`.
|
116
|
+
The file has two main sections.
|
117
|
+
The first is the `services` section, which lists the services you'd like to connect.
|
118
|
+
The second is the `haproxy` section, which specifies how to configure and interact with HAProxy.
|
119
|
+
|
120
|
+
### Configuring a Service ###
|
121
|
+
|
122
|
+
The services are a hash, where the keys are the `name` of the service to be configured.
|
123
|
+
The name is just a human-readable string; it will be used in logs and notifications.
|
124
|
+
Each value in the services hash is also a hash, and should contain the following keys:
|
125
|
+
|
126
|
+
* `discovery`: how synapse will discover hosts providing this service (see below)
|
127
|
+
* `default_servers`: the list of default servers providing this service; synapse uses these if none others can be discovered
|
128
|
+
* `haproxy`: how will the haproxy section for this service be configured
|
129
|
+
|
130
|
+
#### Service Discovery ####
|
131
|
+
|
132
|
+
We've included a number of `watchers` which provide service discovery.
|
133
|
+
Put these into the `discovery` section of the service hash, with these options:
|
134
|
+
|
135
|
+
##### Stub #####
|
136
|
+
|
137
|
+
The stub watcher, this is useful in situations where you only want to use the servers in the `default_servers` list.
|
138
|
+
It has only one option:
|
139
|
+
|
140
|
+
* `method`: stub
|
141
|
+
|
142
|
+
##### Zookeeper #####
|
143
|
+
|
144
|
+
This watcher retrieves a list of servers from zookeeper.
|
145
|
+
It takes the following options:
|
146
|
+
|
147
|
+
* `method`: zookeeper
|
148
|
+
* `path`: the zookeeper path where ephemeral nodes will be created for each available service server
|
149
|
+
* `hosts`: the list of zookeeper servers to query
|
150
|
+
|
151
|
+
The watcher assumes that each node under `path` represents a service server.
|
152
|
+
Synapse attempts to decode the data in each of these nodes using JSON and also using Thrift under the standard Twitter service encoding.
|
153
|
+
We assume that the data contains a hostname and a port for service servers.
|
154
|
+
|
155
|
+
##### Docker #####
|
156
|
+
|
157
|
+
This watcher retrieves a list of [docker](http://www.docker.io/) containers via docker's [HTTP API](http://docs.docker.io/en/latest/api/docker_remote_api/).
|
158
|
+
It takes the following options:
|
159
|
+
|
160
|
+
* `method`: docker
|
161
|
+
* `servers`: a list of servers running docker as a daemon. Format is `{"name":"...", "host": "..."[, port: 4243]}`
|
162
|
+
* `image_name`: find containers running this image
|
163
|
+
* `container_port`: find containers forwarding this port
|
164
|
+
* `check_interval`: how often to poll the docker API on each server. Default is 15s.
|
165
|
+
|
166
|
+
#### Listing Default Servers ####
|
167
|
+
|
168
|
+
You may list a number of default servers providing a service.
|
169
|
+
Each hash in that section has the following options:
|
170
|
+
|
171
|
+
* `name`: a human-readable name for the default server; must be unique
|
172
|
+
* `host`: the host or IP address of the server
|
173
|
+
* `port`: the port where the service runs on the `host`
|
174
|
+
|
175
|
+
The `default_servers` list is used only when service discovery returns no servers.
|
176
|
+
In that case, the service proxy will be created with the servers listed here.
|
177
|
+
If you do not list any default servers, no proxy will be created. The
|
178
|
+
`default_servers` will also be used in addition to discovered servers if the
|
179
|
+
`keep_default_servers` option is set.
|
180
|
+
|
181
|
+
#### The `haproxy` Section ####
|
182
|
+
|
183
|
+
This section is it's own hash, which should contain the following keys:
|
184
|
+
|
185
|
+
* `port`: the port (on localhost) where HAProxy will listen for connections to the service.
|
186
|
+
* `server_port_override`: the port that discovered servers listen on; you should specify this if your discovery mechanism only discovers names or addresses (like the DNS watcher). If the discovery method discovers a port along with hostnames (like the zookeeper watcher) this option may be left out, but will be used in preference if given.
|
187
|
+
* `server_options`: the haproxy options for each `server` line of the service in HAProxy config; it may be left out.
|
188
|
+
* `frontend`: additional lines passed to the HAProxy config in the `frontend` stanza of this service
|
189
|
+
* `backend`: additional lines passed to the HAProxy config in the `backend` stanza of this service
|
190
|
+
* `listen`: these lines will be parsed and placed in the correct `frontend`/`backend` section as applicable; you can put lines which are the same for the frontend and backend here.
|
191
|
+
|
192
|
+
### Configuring HAProxy ###
|
193
|
+
|
194
|
+
The `haproxy` section of the config file has the following options:
|
195
|
+
|
196
|
+
* `reload_command`: the command Synapse will run to reload HAProxy
|
197
|
+
* `config_file_path`: where Synapse will write the HAProxy config file
|
198
|
+
* `do_writes`: whether or not the config file will be written (default to `true`)
|
199
|
+
* `do_reloads`: whether or not Synapse will reload HAProxy (default to `true`)
|
200
|
+
* `global`: options listed here will be written into the `global` section of the HAProxy config
|
201
|
+
* `defaults`: options listed here will be written into the `defaults` section of the HAProxy config
|
202
|
+
* `bind_address`: force HAProxy to listen on this address (default is localhost)
|
203
|
+
|
204
|
+
Note that a non-default `bind_address` can be dangerous: it is up to you to ensure that HAProxy will not attempt to bind an address:port combination that is not already in use by one of your services.
|
205
|
+
|
206
|
+
## Contributing
|
207
|
+
|
208
|
+
1. Fork it
|
209
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
210
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
211
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
212
|
+
5. Create new Pull Request
|
213
|
+
|
214
|
+
### Creating a Service Watcher ###
|
215
|
+
|
216
|
+
If you'd like to create a new service watcher:
|
217
|
+
|
218
|
+
1. Create a file for your watcher in `service_watcher` dir
|
219
|
+
2. Use the following template:
|
220
|
+
```ruby
|
221
|
+
require 'synapse/service_watcher/base'
|
222
|
+
|
223
|
+
module Synapse
|
224
|
+
class NewWatcher < BaseWatcher
|
225
|
+
def start
|
226
|
+
# write code which begins running service discovery
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
def validate_discovery_opts
|
231
|
+
# here, validate any required options in @discovery
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
3. Implement the `start` and `validate_discovery_opts` methods
|
238
|
+
4. Implement whatever additional methods your discovery requires
|
239
|
+
|
240
|
+
When your watcher detects a list of new backends, they should be written to `@backends`.
|
241
|
+
You should then call `@synapse.configure` to force synapse to update the HAProxy config.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/synapse
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
require 'synapse'
|
7
|
+
|
8
|
+
options={}
|
9
|
+
|
10
|
+
# set command line options
|
11
|
+
optparse = OptionParser.new do |opts|
|
12
|
+
opts.banner =<<-EOB
|
13
|
+
Welcome to synapse
|
14
|
+
|
15
|
+
Usage: synapse --config /path/to/synapse/config
|
16
|
+
EOB
|
17
|
+
|
18
|
+
options[:config] = ENV['SYNAPSE_CONFIG']
|
19
|
+
opts.on('-c config','--config config', String, 'path to synapse config') do |key,value|
|
20
|
+
options[:config] = key
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
24
|
+
puts opts
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# parse command line arguments
|
30
|
+
optparse.parse!
|
31
|
+
|
32
|
+
def parseconfig(filename)
|
33
|
+
# parse synapse config file
|
34
|
+
begin
|
35
|
+
c = YAML::parse(File.read(filename))
|
36
|
+
rescue Errno::ENOENT => e
|
37
|
+
raise ArgumentError, "config file does not exist:\n#{e.inspect}"
|
38
|
+
rescue Errno::EACCES => e
|
39
|
+
raise ArgumentError, "could not open config file:\n#{e.inspect}"
|
40
|
+
rescue YAML::ParseError => e
|
41
|
+
raise "config file #{filename} is not yaml:\n#{e.inspect}"
|
42
|
+
end
|
43
|
+
return c.to_ruby
|
44
|
+
end
|
45
|
+
|
46
|
+
config = parseconfig(options[:config])
|
47
|
+
config['services'] ||= {}
|
48
|
+
|
49
|
+
if config.has_key?('service_conf_dir')
|
50
|
+
cdir = File.expand_path(config['service_conf_dir'])
|
51
|
+
if ! Dir.exists?(cdir)
|
52
|
+
raise "service conf dir does not exist:#{cdir}"
|
53
|
+
end
|
54
|
+
cfiles = Dir.glob(File.join(cdir, '*.{json,yaml}'))
|
55
|
+
cfiles.each { |x| config['services'][File.basename(x[/(.*)\.(json|yaml)$/, 1])] = parseconfig(x) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# run synapse
|
59
|
+
s = Synapse::Synapse.new(config)
|
60
|
+
s.run
|
61
|
+
|
62
|
+
puts "synapse has exited"
|
@@ -0,0 +1,71 @@
|
|
1
|
+
{
|
2
|
+
"services": {
|
3
|
+
"service1": {
|
4
|
+
"default_servers": [
|
5
|
+
{ "name": "default1", "host": "localhost", "port": 8080 }
|
6
|
+
],
|
7
|
+
"discovery": {
|
8
|
+
"method": "dns",
|
9
|
+
"nameserver": "127.0.0.1",
|
10
|
+
"servers": [
|
11
|
+
"0.www.example.com",
|
12
|
+
"1.www.example.com"
|
13
|
+
]
|
14
|
+
},
|
15
|
+
"haproxy": {
|
16
|
+
"server_options": "check inter 2s rise 3 fall 2",
|
17
|
+
"listen": [
|
18
|
+
"mode http",
|
19
|
+
"option httplog"
|
20
|
+
],
|
21
|
+
"backend": [
|
22
|
+
"mode http",
|
23
|
+
"option httpchk GET /health HTTP/1.0"
|
24
|
+
]
|
25
|
+
}
|
26
|
+
}
|
27
|
+
},
|
28
|
+
"haproxy": {
|
29
|
+
"reload_command": "sudo service haproxy reload",
|
30
|
+
"config_file_path": "/etc/haproxy/haproxy.cfg",
|
31
|
+
"socket_file_path": "/var/haproxy/stats.sock",
|
32
|
+
"do_writes": true,
|
33
|
+
"do_reloads": true,
|
34
|
+
"do_socket": true,
|
35
|
+
"global": [
|
36
|
+
"daemon",
|
37
|
+
"user haproxy",
|
38
|
+
"group haproxy",
|
39
|
+
"maxconn 4096",
|
40
|
+
"log 127.0.0.1 local0",
|
41
|
+
"log 127.0.0.1 local1 notice",
|
42
|
+
"stats socket /var/haproxy/stats.sock mode 666 level admin"
|
43
|
+
],
|
44
|
+
"defaults": [
|
45
|
+
"log global",
|
46
|
+
"option dontlognull",
|
47
|
+
"maxconn 2000",
|
48
|
+
"retries 3",
|
49
|
+
"timeout connect 5s",
|
50
|
+
"timeout client 1m",
|
51
|
+
"timeout server 1m",
|
52
|
+
"option redispatch",
|
53
|
+
"balance roundrobin"
|
54
|
+
],
|
55
|
+
"extra_sections": {
|
56
|
+
"listen stats :3212": [
|
57
|
+
"mode http",
|
58
|
+
"stats enable",
|
59
|
+
"stats uri /",
|
60
|
+
"stats refresh 5s"
|
61
|
+
],
|
62
|
+
"frontend http-generic-in": [
|
63
|
+
"bind 127.0.0.1:80",
|
64
|
+
"acl is_service1 hdr_dom(host) -i service1.lb",
|
65
|
+
"acl is_cache hdr_dom(host) -i cache.lb",
|
66
|
+
"use_backend service1 if is_service1",
|
67
|
+
"use_backend cache if is_cache"
|
68
|
+
]
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|