synapse 0.0.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/Gemfile.lock +45 -0
- data/README.md +185 -4
- data/Vagrantfile +112 -0
- data/bin/synapse +54 -0
- data/chef/converge +4 -0
- data/chef/cookbooks/lxc/recipes/default.rb +2 -0
- data/chef/cookbooks/synapse/attributes/default.rb +1 -0
- data/chef/cookbooks/synapse/recipes/default.rb +6 -0
- data/chef/run.json +8 -0
- data/chef/run.rb +2 -0
- data/client/.RData +0 -0
- data/client/.Rhistory +294 -0
- data/client/bench_rewrite_config.dat +2013 -0
- data/client/benchmark-client.iml +20 -0
- data/client/pom.xml +45 -0
- data/client/src/main/java/ClientArsch.java +68 -0
- data/client/src/main/java/META-INF/MANIFEST.MF +3 -0
- data/config/synapse.conf.json +96 -0
- data/haproxy.pid +1 -0
- data/lib/gen-rb/endpoint_types.rb +65 -0
- data/lib/gen-rb/thrift.rb +65 -0
- data/lib/synapse/base.rb +5 -0
- data/lib/synapse/haproxy.rb +200 -0
- data/lib/synapse/service_watcher/base.rb +44 -0
- data/lib/synapse/service_watcher/dns.rb +98 -0
- data/lib/synapse/service_watcher/ec2tag.rb +26 -0
- data/lib/synapse/service_watcher/zookeeper.rb +138 -0
- data/lib/synapse/service_watcher.rb +28 -0
- data/lib/synapse/version.rb +1 -1
- data/lib/synapse.rb +61 -2
- data/spec/lib/synapse/haproxy_spec.rb +14 -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 +7 -0
- data/test.sh +3 -0
- metadata +123 -5
data/.gitignore
CHANGED
data/.rspec
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
synapse (0.1.4)
|
5
|
+
thrift (~> 0.9.0)
|
6
|
+
zk (~> 1.7.4)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
coderay (1.0.8)
|
12
|
+
diff-lcs (1.1.3)
|
13
|
+
little-plugger (1.1.3)
|
14
|
+
logging (1.7.2)
|
15
|
+
little-plugger (>= 1.1.3)
|
16
|
+
method_source (0.8.1)
|
17
|
+
pry (0.9.10)
|
18
|
+
coderay (~> 1.0.5)
|
19
|
+
method_source (~> 0.8)
|
20
|
+
slop (~> 3.3.1)
|
21
|
+
pry-nav (0.2.2)
|
22
|
+
pry (~> 0.9.10)
|
23
|
+
rspec (2.12.0)
|
24
|
+
rspec-core (~> 2.12.0)
|
25
|
+
rspec-expectations (~> 2.12.0)
|
26
|
+
rspec-mocks (~> 2.12.0)
|
27
|
+
rspec-core (2.12.0)
|
28
|
+
rspec-expectations (2.12.0)
|
29
|
+
diff-lcs (~> 1.1.3)
|
30
|
+
rspec-mocks (2.12.0)
|
31
|
+
slop (3.3.3)
|
32
|
+
thrift (0.9.0)
|
33
|
+
zk (1.7.5)
|
34
|
+
logging (~> 1.7.2)
|
35
|
+
zookeeper (~> 1.4.0)
|
36
|
+
zookeeper (1.4.4)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
pry
|
43
|
+
pry-nav
|
44
|
+
rspec
|
45
|
+
synapse!
|
data/README.md
CHANGED
@@ -1,6 +1,95 @@
|
|
1
|
-
# Synapse
|
1
|
+
# Synapse #
|
2
2
|
|
3
|
-
|
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
|
+
{
|
58
|
+
"name": "proddb",
|
59
|
+
"local_port": 3219,
|
60
|
+
"server_options": "check inter 2000 rise 3 fall 2",
|
61
|
+
"default_servers": [
|
62
|
+
{
|
63
|
+
"name": "default-db",
|
64
|
+
"host": "mydb.example.com",
|
65
|
+
"port": 5432
|
66
|
+
}
|
67
|
+
],
|
68
|
+
"discovery": {
|
69
|
+
"method": "awstag",
|
70
|
+
"tag": "proddb",
|
71
|
+
"value": "true"
|
72
|
+
},
|
73
|
+
"listen": [
|
74
|
+
]
|
75
|
+
},
|
76
|
+
...
|
77
|
+
```
|
78
|
+
|
79
|
+
And then change your database.yaml file to look like this:
|
80
|
+
|
81
|
+
```yaml
|
82
|
+
production:
|
83
|
+
database: mydb
|
84
|
+
host: localhost
|
85
|
+
port: 3219
|
86
|
+
```
|
87
|
+
|
88
|
+
Start up synapse.
|
89
|
+
It will configure HAProxy with a proxy from `localhost:3219` to your DB.
|
90
|
+
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`.
|
91
|
+
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.
|
92
|
+
HAProxy will be transparently reloaded, and your application will keep running without a hiccup.
|
4
93
|
|
5
94
|
## Installation
|
6
95
|
|
@@ -16,9 +105,73 @@ Or install it yourself as:
|
|
16
105
|
|
17
106
|
$ gem install synapse
|
18
107
|
|
19
|
-
##
|
108
|
+
## Configuration ##
|
109
|
+
|
110
|
+
Synapse depends on a single config file in JSON format; it's usually called `synapse.conf.json`.
|
111
|
+
The file has two main sections.
|
112
|
+
The first is the `services` section, which lists the services you'd like to connect.
|
113
|
+
The second is the `haproxy` section, which specifies how to configure and interact with HAProxy.
|
114
|
+
|
115
|
+
### Configuring a Service ###
|
116
|
+
|
117
|
+
Each service hash has the following options:
|
118
|
+
|
119
|
+
* `name`: a human-readable name for the service, this is used in logs and notifications
|
120
|
+
* `local_port`: the port (on localhost) where HAProxy will listen for connections to the serivce
|
121
|
+
* `discovery`: how synapse will discover hosts providing this service (see below)
|
122
|
+
* `default_servers`: the list of default servers providing this service; synapse uses these if none others can be discovered
|
123
|
+
* `server_port_override`: the port that discovered servers listen on; 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
|
124
|
+
* `server_options`: the haproxy options for each `server` line of the service in HAProxy config; may be left out
|
125
|
+
* `listen`: additional lines passed to the HAProxy config in the `listen` stanza of this service
|
20
126
|
|
21
|
-
|
127
|
+
#### Service Discovery ####
|
128
|
+
|
129
|
+
We've included a number of `watchers` which provide service discovery.
|
130
|
+
Put these into the `discovery` section of the service hash, with these options:
|
131
|
+
|
132
|
+
##### Stub #####
|
133
|
+
|
134
|
+
The stub watcher, this is useful in situations where you only want to use the servers in the `default_servers` list.
|
135
|
+
It has only one option:
|
136
|
+
|
137
|
+
* `method`: stub
|
138
|
+
|
139
|
+
##### Zookeeper #####
|
140
|
+
|
141
|
+
This watcher retrieves a list of servers from zookeeper.
|
142
|
+
It takes the following options:
|
143
|
+
|
144
|
+
* `method`: zookeeper
|
145
|
+
* `path`: the zookeeper path where ephemeral nodes will be created for each available service server
|
146
|
+
* `hosts`: the list of zookeeper servers to query
|
147
|
+
|
148
|
+
The watcher assumes that each node under `path` represents a service server.
|
149
|
+
Synapse attempts to decode the data in each of these nodes using JSON and also using Thrift under the standard Twitter service encoding.
|
150
|
+
We assume that the data contains a hostname and a port for service servers.
|
151
|
+
|
152
|
+
#### Listing Default Servers ####
|
153
|
+
|
154
|
+
You may list a number of default servers providing a service.
|
155
|
+
Each hash in that section has the following options:
|
156
|
+
|
157
|
+
* `name`: a human-readable name for the default server; must be unique
|
158
|
+
* `host`: the host or IP address of the server
|
159
|
+
* `port`: the port where the service runs on the `host`
|
160
|
+
|
161
|
+
The `default_servers` list is used only when service discovery returns no servers.
|
162
|
+
In that case, the service proxy will be created with the servers listed here.
|
163
|
+
If you do not list any default servers, no proxy will be created.
|
164
|
+
|
165
|
+
### Configuring HAProxy ###
|
166
|
+
|
167
|
+
The `haproxy` section of the config file has the following options:
|
168
|
+
|
169
|
+
* `reload_command`: the command Synapse will run to reload HAProxy
|
170
|
+
* `config_file_path`: where Synapse will write the HAProxy config file
|
171
|
+
* `do_writes`: whether or not the config file will be written (default to `true`)
|
172
|
+
* `do_reloads`: whether or not Synapse will reload HAProxy (default to `true`)
|
173
|
+
* `global`: options listed here will be written into the `global` section of the HAProxy config
|
174
|
+
* `defaults`: options listed here will be written into the `defaults` section of the HAProxy config
|
22
175
|
|
23
176
|
## Contributing
|
24
177
|
|
@@ -27,3 +180,31 @@ Write usage instructions here
|
|
27
180
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
181
|
4. Push to the branch (`git push origin my-new-feature`)
|
29
182
|
5. Create new Pull Request
|
183
|
+
|
184
|
+
### Creating a Service Watcher ###
|
185
|
+
|
186
|
+
If you'd like to create a new service watcher:
|
187
|
+
|
188
|
+
1. Create a file for your watcher in `service_watcher` dir
|
189
|
+
2. Use the following template:
|
190
|
+
```ruby
|
191
|
+
require_relative "./base"
|
192
|
+
module Synapse
|
193
|
+
class NewWatcher < BaseWatcher
|
194
|
+
def start
|
195
|
+
# write code which begins running service discovery
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
def validate_discovery_opts
|
200
|
+
# here, validate any required options in @discovery
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
3. Implement the `start` and `validate_discovery_opts` methods
|
207
|
+
4. Implement whatever additional methods your discovery requires
|
208
|
+
|
209
|
+
When your watcher detects a list of new backends, they should be written to `@backends`.
|
210
|
+
You should then call `@synapse.configure` to force synapse to update the HAProxy config.
|
data/Vagrantfile
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# -*- mode: ruby -*-
|
2
|
+
# vi: set ft=ruby :
|
3
|
+
|
4
|
+
this_dir = File.dirname(File.expand_path __FILE__)
|
5
|
+
|
6
|
+
Vagrant::Config.run do |config|
|
7
|
+
# All Vagrant configuration is done here. The most common configuration
|
8
|
+
# options are documented and commented below. For a complete reference,
|
9
|
+
# please see the online documentation at vagrantup.com.
|
10
|
+
|
11
|
+
# Every Vagrant virtual environment requires a box to build off of.
|
12
|
+
config.vm.box = "precise64"
|
13
|
+
|
14
|
+
# The url from where the 'config.vm.box' box will be fetched if it
|
15
|
+
# doesn't already exist on the user's system.
|
16
|
+
# config.vm.box_url = "http://domain.com/path/to/above.box"
|
17
|
+
|
18
|
+
config.vm.box_url = 'http://files.vagrantup.com/precise64.box'
|
19
|
+
|
20
|
+
# Boot with a GUI so you can see the screen. (Default is headless)
|
21
|
+
# config.vm.boot_mode = :gui
|
22
|
+
|
23
|
+
# Assign this VM to a host-only network IP, allowing you to access it
|
24
|
+
# via the IP. Host-only networks can talk to the host machine as well as
|
25
|
+
# any other machines on the same network, but cannot be accessed (through this
|
26
|
+
# network interface) by any external networks.
|
27
|
+
# config.vm.network :hostonly, "192.168.33.10"
|
28
|
+
|
29
|
+
# Assign this VM to a bridged network, allowing you to connect directly to a
|
30
|
+
# network using the host's network device. This makes the VM appear as another
|
31
|
+
# physical device on your network.
|
32
|
+
# config.vm.network :bridged
|
33
|
+
|
34
|
+
# Forward a port from the guest to the host, which allows for outside
|
35
|
+
# computers to access the VM, whereas host only networking does not.
|
36
|
+
# config.vm.forward_port 80, 8080
|
37
|
+
|
38
|
+
# Share an additional folder to the guest VM. The first argument is
|
39
|
+
# an identifier, the second is the path on the guest to mount the
|
40
|
+
# folder, and the third is the path on the host to the actual folder.
|
41
|
+
# config.vm.share_folder "v-data", "/vagrant_data", "../data"
|
42
|
+
|
43
|
+
config.vm.share_folder 'this_dir', '/this_dir', this_dir
|
44
|
+
|
45
|
+
# Enable provisioning with Puppet stand alone. Puppet manifests
|
46
|
+
# are contained in a directory path relative to this Vagrantfile.
|
47
|
+
# You will need to create the manifests directory and a manifest in
|
48
|
+
# the file base.pp in the manifests_path directory.
|
49
|
+
#
|
50
|
+
# An example Puppet manifest to provision the message of the day:
|
51
|
+
#
|
52
|
+
# # group { "puppet":
|
53
|
+
# # ensure => "present",
|
54
|
+
# # }
|
55
|
+
# #
|
56
|
+
# # File { owner => 0, group => 0, mode => 0644 }
|
57
|
+
# #
|
58
|
+
# # file { '/etc/motd':
|
59
|
+
# # content => "Welcome to your Vagrant-built virtual machine!
|
60
|
+
# # Managed by Puppet.\n"
|
61
|
+
# # }
|
62
|
+
#
|
63
|
+
# config.vm.provision :puppet do |puppet|
|
64
|
+
# puppet.manifests_path = "manifests"
|
65
|
+
# puppet.manifest_file = "base.pp"
|
66
|
+
# end
|
67
|
+
|
68
|
+
# Enable provisioning with chef solo, specifying a cookbooks path, roles
|
69
|
+
# path, and data_bags path (all relative to this Vagrantfile), and adding
|
70
|
+
# some recipes and/or roles.
|
71
|
+
#
|
72
|
+
config.vm.provision :chef_solo do |chef|
|
73
|
+
# chef.cookbooks_path = "../my-recipes/cookbooks"
|
74
|
+
# chef.roles_path = "../my-recipes/roles"
|
75
|
+
# chef.data_bags_path = "../my-recipes/data_bags"
|
76
|
+
# chef.add_recipe "mysql"
|
77
|
+
# chef.add_role "web"
|
78
|
+
|
79
|
+
# You may also specify custom JSON attributes:
|
80
|
+
# chef.json = { :mysql_password => "foo" }
|
81
|
+
|
82
|
+
chef.cookbooks_path = File.join(this_dir, "chef/cookbooks")
|
83
|
+
chef.add_recipe "synapse"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Enable provisioning with chef server, specifying the chef server URL,
|
87
|
+
# and the path to the validation key (relative to this Vagrantfile).
|
88
|
+
#
|
89
|
+
# The Opscode Platform uses HTTPS. Substitute your organization for
|
90
|
+
# ORGNAME in the URL and validation key.
|
91
|
+
#
|
92
|
+
# If you have your own Chef Server, use the appropriate URL, which may be
|
93
|
+
# HTTP instead of HTTPS depending on your configuration. Also change the
|
94
|
+
# validation key to validation.pem.
|
95
|
+
#
|
96
|
+
# config.vm.provision :chef_client do |chef|
|
97
|
+
# chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME"
|
98
|
+
# chef.validation_key_path = "ORGNAME-validator.pem"
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# If you're using the Opscode platform, your validator client is
|
102
|
+
# ORGNAME-validator, replacing ORGNAME with your organization name.
|
103
|
+
#
|
104
|
+
# IF you have your own Chef Server, the default validation client name is
|
105
|
+
# chef-validator, unless you changed the configuration.
|
106
|
+
#
|
107
|
+
# chef.validation_client_name = "ORGNAME-validator"
|
108
|
+
|
109
|
+
Vagrant::Config.run do |config|
|
110
|
+
config.vm.provision :shell, :path => "test.sh"
|
111
|
+
end
|
112
|
+
end
|
data/bin/synapse
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
require_relative "../lib/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
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# parse command line arguments
|
32
|
+
optparse.parse!
|
33
|
+
|
34
|
+
|
35
|
+
# parse synapse config file
|
36
|
+
begin
|
37
|
+
config = JSON::parse(File.read(options[:config]))
|
38
|
+
rescue Errno::ENOENT => e
|
39
|
+
raise ArgumentError, "config file does not exist:\n#{e.inspect}"
|
40
|
+
rescue Errno::EACCES => e
|
41
|
+
raise ArgumentError, "could not open config file:\n#{e.inspect}"
|
42
|
+
rescue JSON::ParserError => e
|
43
|
+
raise "config file #{options[:config]} is not json:\n#{e.inspect}"
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# create synapse object
|
48
|
+
s = Synapse::Synapse.new config
|
49
|
+
|
50
|
+
# start synapse
|
51
|
+
s.run
|
52
|
+
|
53
|
+
|
54
|
+
puts "exiting synapse"
|
data/chef/converge
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default.synapse.dir = '/opt/synapse'
|
data/chef/run.json
ADDED
data/chef/run.rb
ADDED
data/client/.RData
ADDED
Binary file
|