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 CHANGED
@@ -15,3 +15,6 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *~
19
+ .vagrant
20
+ .*sw?
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
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
- Write a gem description
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
- ## Usage
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
- Write usage instructions here
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,4 @@
1
+ #!/bin/bash -e
2
+
3
+ cd `dirname $0`
4
+ sudo /opt/vagrant_ruby/bin/chef-solo -c run.rb -j run.json
@@ -0,0 +1,2 @@
1
+ package 'lxc'
2
+
@@ -0,0 +1 @@
1
+ default.synapse.dir = '/opt/synapse'
@@ -0,0 +1,6 @@
1
+ package 'haproxy'
2
+ gem_package 'bundler'
3
+
4
+ directory node.synapse.dir do
5
+ recursive true
6
+ end
data/chef/run.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "run_list": [
3
+ "recipe[synapse]"
4
+ ],
5
+ "synapse": {
6
+ }
7
+ }
8
+
data/chef/run.rb ADDED
@@ -0,0 +1,2 @@
1
+ this_dir = File.dirname(File.expand_path __FILE__)
2
+ cookbook_path File.join(this_dir,'cookbooks')
data/client/.RData ADDED
Binary file