synapse 0.12.1 → 0.12.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.
data/.travis.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  language: ruby
2
2
  cache: bundler
3
+ sudo: false
3
4
  rvm:
4
5
  - 1.9.3
5
6
  - 2.0.0
data/README.md CHANGED
@@ -15,7 +15,7 @@ In an environment like Amazon's EC2, all of the available workarounds are subopt
15
15
 
16
16
  * Round-robin DNS: Slow to converge, and doesn't work when applications cache DNS lookups (which is frequent)
17
17
  * Elastic IPs: slow to converge, limited in number, public-facing-only, which makes them less useful for internal services
18
- * ELB: Again, public-facing only, and only useful for HTTP
18
+ * ELB: ultimately uses DNS (see above), can't tune load balancing, have to launch a new one for every service * region, autoscaling doesn't happen fast enough
19
19
 
20
20
  One solution to this problem is a discovery service, like [Apache Zookeeper](http://zookeeper.apache.org/).
21
21
  However, Zookeeper and similar services have their own problems:
@@ -92,38 +92,50 @@ HAProxy will be transparently reloaded, and your application will keep running w
92
92
 
93
93
  ## Installation
94
94
 
95
- Add this line to your application's Gemfile:
95
+ To download and run the synapse binary, first install a version of ruby. Then,
96
+ install synapse with:
96
97
 
97
- gem 'synapse'
98
-
99
- And then execute:
98
+ ```bash
99
+ $ mkdir -p /opt/smartstack/synapse
100
+ # If you are on Ruby 2.X use --no-document instead of --no-ri --no-rdoc
101
+ $ gem install synapse --install-dir /opt/smartstack/synapse --no-ri --no-rdoc
102
+ ```
100
103
 
101
- $ bundle
104
+ This will download synapse and its dependencies into /opt/smartstack/synapse. You
105
+ might wish to omit the `--install-dir` flag to use your system's default gem
106
+ path, however this will require you to run `gem install synapse` with root
107
+ permissions.
102
108
 
103
- Or install it yourself as:
109
+ You can now run the synapse binary like:
104
110
 
105
- $ gem install synapse
106
-
111
+ ```bash
112
+ export GEM_PATH=/opt/smartstack/synapse
113
+ /opt/smartstack/synapse/bin/synapse --help
114
+ ```
107
115
 
108
- Don't forget to install HAProxy prior to installing Synapse.
116
+ Don't forget to install HAProxy too.
109
117
 
110
118
  ## Configuration ##
111
119
 
112
120
  Synapse depends on a single config file in JSON format; it's usually called `synapse.conf.json`.
113
- The file has two main sections.
114
- The first is the `services` section, which lists the services you'd like to connect.
115
- The second is the `haproxy` section, which specifies how to configure and interact with HAProxy.
121
+ The file has three main sections.
122
+
123
+ 1. [`services`](#services): lists the services you'd like to connect.
124
+ 2. [`haproxy`](#haproxy): specifies how to configure and interact with HAProxy.
125
+ 3. [`file_output`](#file) (optional): specifies where to write service state to on the filesystem.
116
126
 
127
+ <a name="services"/>
117
128
  ### Configuring a Service ###
118
129
 
119
- The services are a hash, where the keys are the `name` of the service to be configured.
130
+ The `services` section is a hash, where the keys are the `name` of the service to be configured.
120
131
  The name is just a human-readable string; it will be used in logs and notifications.
121
132
  Each value in the services hash is also a hash, and should contain the following keys:
122
133
 
123
- * `discovery`: how synapse will discover hosts providing this service (see below)
134
+ * [`discovery`](#discovery): how synapse will discover hosts providing this service (see below)
124
135
  * `default_servers`: the list of default servers providing this service; synapse uses these if no others can be discovered
125
- * `haproxy`: how will the haproxy section for this service be configured
136
+ * [`haproxy`](#haproxysvc): how will the haproxy section for this service be configured
126
137
 
138
+ <a name="discovery"/>
127
139
  #### Service Discovery ####
128
140
 
129
141
  We've included a number of `watchers` which provide service discovery.
@@ -183,6 +195,18 @@ be used in preference to the `AWS_` environment variables.
183
195
  * `aws_secret_access_key`: AWS secret key or set `AWS_SECRET_ACCESS_KEY` in the environment.
184
196
  * `aws_region`: AWS region (i.e. `us-east-1`) or set `AWS_REGION` in the environment.
185
197
 
198
+ ##### Marathon #####
199
+
200
+ This watcher polls the Marathon API and retrieves a list of instances for a
201
+ given application.
202
+
203
+ It takes the following options:
204
+
205
+ * `marathon_api_url`: Address of the marathon API (e.g. `http://marathon-master:8080`)
206
+ * `application_name`: Name of the application in Marathon
207
+ * `check_interval`: How often to request the list of tasks from Marathon (default: 10 seconds)
208
+ * `port_index`: Index of the backend port in the task's "ports" array. (default: 0)
209
+
186
210
  #### Listing Default Servers ####
187
211
 
188
212
  You may list a number of default servers providing a service.
@@ -202,15 +226,7 @@ If you do not list any `default_servers`, and all backends for a service
202
226
  disappear then the previous known backends will be used. Disable this behavior
203
227
  by unsetting `use_previous_backends`.
204
228
 
205
- #### The `file_output` Section ####
206
-
207
- This section controls whether or not synapse will write out service state
208
- to the filesystem in json format. This can be used for services that want to
209
- use discovery information but not go through HAProxy.
210
-
211
- * `output_directory`: the path to a directory on disk that service registrations
212
- should be written to.
213
-
229
+ <a name="haproxysvc"/>
214
230
  #### The `haproxy` Section ####
215
231
 
216
232
  This section is its own hash, which should contain the following keys:
@@ -220,12 +236,15 @@ This section is its own hash, which should contain the following keys:
220
236
  * `server_options`: the haproxy options for each `server` line of the service in HAProxy config; it may be left out.
221
237
  * `frontend`: additional lines passed to the HAProxy config in the `frontend` stanza of this service
222
238
  * `backend`: additional lines passed to the HAProxy config in the `backend` stanza of this service
239
+ * `backend_name`: The name of the generated HAProxy backend for this service
240
+ (defaults to the service's key in the `services` section)
223
241
  * `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.
224
242
  * `shared_frontend`: optional: haproxy configuration directives for a shared http frontend (see below)
225
243
 
244
+ <a name="haproxy"/>
226
245
  ### Configuring HAProxy ###
227
246
 
228
- The `haproxy` section of the config file has the following options:
247
+ The top level `haproxy` section of the config file has the following options:
229
248
 
230
249
  * `reload_command`: the command Synapse will run to reload HAProxy
231
250
  * `config_file_path`: where Synapse will write the HAProxy config file
@@ -249,6 +268,17 @@ The `haproxy` section of the config file has the following options:
249
268
  Note that a non-default `bind_address` can be dangerous.
250
269
  If you configure an `address:port` combination that is already in use on the system, haproxy will fail to start.
251
270
 
271
+ <a name="file"/>
272
+ ### Configuring `file_output` ###
273
+
274
+ This section controls whether or not synapse will write out service state
275
+ to the filesystem in json format. This can be used for services that want to
276
+ use discovery information but not go through HAProxy.
277
+
278
+ * `output_directory`: the path to a directory on disk that service registrations
279
+ should be written to.
280
+
281
+
252
282
  ### HAProxy shared HTTP Frontend ###
253
283
 
254
284
  For HTTP-only services, it is not always necessary or desirable to dedicate a TCP port per service, since HAProxy can route traffic based on host headers.
@@ -260,7 +290,8 @@ For example:
260
290
 
261
291
  ```yaml
262
292
  haproxy:
263
- shared_frontend: "bind 127.0.0.1:8081"
293
+ shared_frontend:
294
+ - "bind 127.0.0.1:8081"
264
295
  reload_command: "service haproxy reload"
265
296
  config_file_path: "/etc/haproxy/haproxy.cfg"
266
297
  socket_file_path: "/var/run/haproxy.sock"
@@ -279,7 +310,8 @@ For example:
279
310
  discovery:
280
311
  method: "zookeeper"
281
312
  path: "/nerve/services/service1"
282
- hosts: "0.zookeeper.example.com:2181"
313
+ hosts:
314
+ - "0.zookeeper.example.com:2181"
283
315
  haproxy:
284
316
  server_options: "check inter 2s rise 3 fall 2"
285
317
  shared_frontend:
@@ -298,7 +330,8 @@ For example:
298
330
  shared_frontend:
299
331
  - "acl is_service1 hdr_dom(host) -i service2.lb.example.com"
300
332
  - "use_backend service2 if is_service2
301
- backend: "mode http"
333
+ backend:
334
+ - "mode http"
302
335
 
303
336
  ```
304
337
 
@@ -333,29 +366,5 @@ Non-HTTP backends such as MySQL or RabbitMQ will obviously continue to need thei
333
366
 
334
367
  ### Creating a Service Watcher ###
335
368
 
336
- If you'd like to create a new service watcher:
337
-
338
- 1. Create a file for your watcher in `service_watcher` dir
339
- 2. Use the following template:
340
- ```ruby
341
- require 'synapse/service_watcher/base'
342
-
343
- module Synapse
344
- class NewWatcher < BaseWatcher
345
- def start
346
- # write code which begins running service discovery
347
- end
348
-
349
- private
350
- def validate_discovery_opts
351
- # here, validate any required options in @discovery
352
- end
353
- end
354
- end
355
- ```
356
-
357
- 3. Implement the `start` and `validate_discovery_opts` methods
358
- 4. Implement whatever additional methods your discovery requires
359
-
360
- When your watcher detects a list of new backends, you should call `set_backends` to
361
- store the new backends and update the HAProxy config.
369
+ See the Service Watcher [README](lib/synapse/service_watcher/README.md) for
370
+ how to add new Service Watchers.
data/lib/synapse.rb CHANGED
@@ -1,18 +1,18 @@
1
+ require 'logger'
2
+ require 'json'
3
+
1
4
  require "synapse/version"
2
- require "synapse/service_watcher/base"
5
+ require "synapse/log"
3
6
  require "synapse/haproxy"
4
7
  require "synapse/file_output"
5
8
  require "synapse/service_watcher"
6
- require "synapse/log"
7
9
 
8
- require 'logger'
9
- require 'json'
10
-
11
- include Synapse
12
10
 
13
11
  module Synapse
14
12
  class Synapse
13
+
15
14
  include Logging
15
+
16
16
  def initialize(opts={})
17
17
  # create the service watchers for all our services
18
18
  raise "specify a list of services to connect in the config" unless opts.has_key?('services')
@@ -1,4 +1,3 @@
1
- require 'synapse/log'
2
1
  require 'fileutils'
3
2
  require 'tempfile'
4
3
 
@@ -29,6 +28,7 @@ module Synapse
29
28
  watchers.each do |watcher|
30
29
  write_backends_to_file(watcher.name, watcher.backends)
31
30
  end
31
+ clean_old_watchers(watchers)
32
32
  end
33
33
 
34
34
  def write_backends_to_file(service_name, new_backends)
@@ -53,5 +53,16 @@ module Synapse
53
53
  return true
54
54
  end
55
55
  end
56
+
57
+ def clean_old_watchers(current_watchers)
58
+ # Cleanup old services that Synapse no longer manages
59
+ FileUtils.cd(@opts['output_directory']) do
60
+ present_files = Dir.glob('*.json')
61
+ managed_files = current_watchers.collect {|watcher| "#{watcher.name}.json"}
62
+ files_to_purge = present_files.select {|svc| not managed_files.include?(svc)}
63
+ log.info "synapse: purging unknown service files #{files_to_purge}" if files_to_purge.length > 0
64
+ FileUtils.rm(files_to_purge)
65
+ end
66
+ end
56
67
  end
57
68
  end
@@ -1,6 +1,5 @@
1
1
  require 'fileutils'
2
2
  require 'json'
3
- require 'synapse/log'
4
3
  require 'socket'
5
4
 
6
5
  module Synapse
@@ -688,7 +687,7 @@ module Synapse
688
687
  "\nfrontend #{watcher.name}",
689
688
  config.map {|c| "\t#{c}"},
690
689
  "\tbind #{@opts['bind_address'] || 'localhost'}:#{watcher.haproxy['port']}",
691
- "\tdefault_backend #{watcher.name}"
690
+ "\tdefault_backend #{watcher.haproxy.fetch('backend_name', watcher.name)}"
692
691
  ]
693
692
  end
694
693
 
@@ -705,6 +704,16 @@ module Synapse
705
704
  # setting the enabled state.
706
705
  watcher.backends.each do |backend|
707
706
  backend_name = construct_name(backend)
707
+ # If we have information in the state file that allows us to detect
708
+ # server option changes, use that to potentially force a restart
709
+ if backends.has_key?(backend_name)
710
+ old_backend = backends[backend_name]
711
+ if (old_backend.fetch('haproxy_server_options', "") !=
712
+ backend.fetch('haproxy_server_options', ""))
713
+ log.info "synapse: restart required because haproxy_server_options changed for #{backend_name}"
714
+ @restart_required = true
715
+ end
716
+ end
708
717
  backends[backend_name] = backend.merge('enabled' => true)
709
718
  end
710
719
 
@@ -713,13 +722,14 @@ module Synapse
713
722
  end
714
723
 
715
724
  stanza = [
716
- "\nbackend #{watcher.name}",
725
+ "\nbackend #{watcher.haproxy.fetch('backend_name', watcher.name)}",
717
726
  config.map {|c| "\t#{c}"},
718
727
  backends.keys.shuffle.map {|backend_name|
719
728
  backend = backends[backend_name]
720
729
  b = "\tserver #{backend_name} #{backend['host']}:#{backend['port']}"
721
730
  b = "#{b} cookie #{backend_name}" unless config.include?('mode tcp')
722
- b = "#{b} #{watcher.haproxy['server_options']}"
731
+ b = "#{b} #{watcher.haproxy['server_options']}" if watcher.haproxy['server_options']
732
+ b = "#{b} #{backend['haproxy_server_options']}" if backend['haproxy_server_options']
723
733
  b = "#{b} disabled" unless backend['enabled']
724
734
  b }
725
735
  ]
@@ -1,22 +1,8 @@
1
+ require "synapse/log"
1
2
  require "synapse/service_watcher/base"
2
- require "synapse/service_watcher/zookeeper"
3
- require "synapse/service_watcher/ec2tag"
4
- require "synapse/service_watcher/dns"
5
- require "synapse/service_watcher/docker"
6
- require "synapse/service_watcher/zookeeper_dns"
7
3
 
8
4
  module Synapse
9
5
  class ServiceWatcher
10
-
11
- @watchers = {
12
- 'base' => BaseWatcher,
13
- 'zookeeper' => ZookeeperWatcher,
14
- 'ec2tag' => EC2Watcher,
15
- 'dns' => DnsWatcher,
16
- 'docker' => DockerWatcher,
17
- 'zookeeper_dns' => ZookeeperDnsWatcher,
18
- }
19
-
20
6
  # the method which actually dispatches watcher creation requests
21
7
  def self.create(name, opts, synapse)
22
8
  opts['name'] = name
@@ -25,10 +11,16 @@ module Synapse
25
11
  unless opts.has_key?('discovery') && opts['discovery'].has_key?('method')
26
12
 
27
13
  discovery_method = opts['discovery']['method']
28
- raise ArgumentError, "Invalid discovery method #{discovery_method}" \
29
- unless @watchers.has_key?(discovery_method)
30
-
31
- return @watchers[discovery_method].new(opts, synapse)
14
+ watcher = begin
15
+ method = discovery_method.downcase
16
+ require "synapse/service_watcher/#{method}"
17
+ # zookeeper_dns => ZookeeperDnsWatcher, ec2tag => Ec2tagWatcher, etc ...
18
+ method_class = method.split('_').map{|x| x.capitalize}.join.concat('Watcher')
19
+ self.const_get("#{method_class}")
20
+ rescue Exception => e
21
+ raise ArgumentError, "Specified a discovery method of #{discovery_method}, which could not be found: #{e}"
22
+ end
23
+ return watcher.new(opts, synapse)
32
24
  end
33
25
  end
34
26
  end
@@ -0,0 +1,84 @@
1
+ ## Watcher Classes
2
+
3
+ Watchers are the piece of Synapse that watch an external service registry
4
+ and reflect those changes in the local HAProxy state. Watchers should conform
5
+ to the interface specified by `BaseWatcher` and when your watcher has received
6
+ an update from the service registry you should call
7
+ `set_backends(new_backends)` to trigger a sync of your watcher state with local
8
+ HAProxy state. See the [`Backend Interface`](#backend_interface) section for
9
+ what service registrations Synapse understands.
10
+
11
+ ```ruby
12
+ require "synapse/service_watcher/base"
13
+
14
+ class Synapse::ServiceWatcher
15
+ class MyWatcher < BaseWatcher
16
+ def start
17
+ # write code which begins running service discovery
18
+ end
19
+
20
+ def stop
21
+ # write code which tears down the service discovery
22
+ end
23
+
24
+ def ping?
25
+ # write code to check in on the health of the watcher
26
+ end
27
+
28
+ private
29
+ def validate_discovery_opts
30
+ # here, validate any required options in @discovery
31
+ end
32
+
33
+ ... setup watches, poll, etc ... and call set_backends when you have new
34
+ ... backends to set
35
+
36
+ end
37
+ end
38
+ ```
39
+
40
+ ### Watcher Plugin Inteface
41
+ Synapse deduces both the class path and class name from the `method` key within
42
+ the watcher configuration. Every watcher is passed configuration with the
43
+ `method` key, e.g. `zookeeper` or `ec2tag`.
44
+
45
+ #### Class Location
46
+ Synapse expects to find your class at `synapse/service_watcher/#{method}`. You
47
+ must make your watcher available at that path, and Synapse can "just work" and
48
+ find it.
49
+
50
+ #### Class Name
51
+ These method strings are then transformed into class names via the following
52
+ function:
53
+
54
+ ```
55
+ method_class = method.split('_').map{|x| x.capitalize}.join.concat('Watcher')
56
+ ```
57
+
58
+ This has the effect of taking the method, splitting on '_', capitalizing each
59
+ part and recombining with an added 'Watcher' on the end. So `zookeeper_dns`
60
+ becomes `ZookeeperDnsWatcher`, and `zookeeper` becomes `Zookeeper`. Make sure
61
+ your class name is correct.
62
+
63
+ <a name="backend_interface"/>
64
+ ### Backend interface
65
+ Synapse understands the following fields in service backends (which are pulled
66
+ from the service registries):
67
+
68
+ `host` (string): The hostname of the service instance
69
+
70
+ `port` (integer): The port running the service on `host`
71
+
72
+ `name` (string, optional): The human readable name to refer to this service instance by
73
+
74
+ `weight` (float, optional): The weight that this backend should get when load
75
+ balancing to this service instance. Full support for updating HAProxy based on
76
+ this is still a WIP.
77
+
78
+ `haproxy_server_options` (string, optional): Any haproxy server options
79
+ specific to this particular server. They will be applied to the generated
80
+ `server` line in the HAProxy configuration. If you want Synapse to react to
81
+ changes in these lines you will need to enable the `state_file_path` option
82
+ in the main synapse configuration. In general the HAProxy backend level
83
+ `haproxy.server_options` setting is preferred to setting this per server
84
+ in your backends.
@@ -1,9 +1,9 @@
1
- require 'set'
2
1
  require 'synapse/log'
2
+ require 'set'
3
3
 
4
- module Synapse
4
+ class Synapse::ServiceWatcher
5
5
  class BaseWatcher
6
- include Logging
6
+ include Synapse::Logging
7
7
 
8
8
  LEADER_WARN_INTERVAL = 30
9
9