redis-mmm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +32 -0
  2. data/bin/redis_mmm +360 -0
  3. metadata +110 -0
data/README ADDED
@@ -0,0 +1,32 @@
1
+ == Moving parts ==
2
+
3
+ * redis_mmm_mond
4
+ checks connection to all hosts
5
+ checks replication state
6
+ assigns roles
7
+ emits SLAVEOF commands
8
+ connects via ssh to hosts and assigns the role ip's
9
+
10
+ * redis_mmm_ctl
11
+ CLI
12
+ display cluster status
13
+ ability to change active master
14
+
15
+
16
+ Example configuration:
17
+
18
+ master_ip = 192.168.10.70/24
19
+ cluster_interface = eth1
20
+
21
+ [db1]
22
+ address = 127.0.0.1
23
+ port = 6380
24
+ ssh_user = admin
25
+ ssh_port = 22277
26
+
27
+ [db2]
28
+ address = 127.0.0.1
29
+ port = 6381
30
+ ssh_user = admin
31
+ ssh_port = 22277
32
+
data/bin/redis_mmm ADDED
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'redis'
5
+ require 'logger'
6
+ require 'thor'
7
+ require 'parseconfig'
8
+
9
+ $log = Logger.new(STDOUT)
10
+
11
+
12
+ class RedisMMM < Thor
13
+ class Error < ::RuntimeError
14
+ def initialize(subject = nil)
15
+ @subject = subject
16
+ end
17
+ attr_reader :subject
18
+ end
19
+
20
+ class NoMaster < Error; end
21
+ class MoreThanOneMaster < Error; end
22
+ class CurrentMasterWrong < Error; end
23
+ class MasterOffline < Error; end
24
+ class SlaveOffline < Error; end
25
+ class SlaveWrongMaster < Error; end
26
+ class SlaveMasterLinkDown < Error; end
27
+ class SlaveOffline < Error; end
28
+
29
+
30
+
31
+ class Host
32
+ def initialize(name, cfg)
33
+ @name, @cfg = name, cfg
34
+ end
35
+
36
+ attr_reader :name
37
+
38
+ %w(address port ssh_user ssh_port).each do |m|
39
+ define_method(m) do
40
+ @cfg[m.to_s]
41
+ end
42
+ end
43
+
44
+ def online?
45
+ state[:online]
46
+ end
47
+
48
+ def master?
49
+ online? && state[:role] == "master"
50
+ end
51
+
52
+ def slave?
53
+ !master?
54
+ end
55
+
56
+ def state
57
+ begin
58
+ info = redis.info
59
+
60
+ {
61
+ :online => true,
62
+ :role => info["role"],
63
+ :master_host => info["master_host"],
64
+ :master_port => info["master_port"],
65
+ :master_link_status => info["master_link_status"],
66
+ :master_last_io_seconds_ago => info["master_last_io_seconds_ago"]
67
+ }
68
+ rescue Errno::ECONNREFUSED
69
+ {
70
+ :online => false
71
+ }
72
+ end
73
+ end
74
+
75
+ def add_ip(ip, interface)
76
+ ssh("/sbin/ip addr add #{ip} dev #{interface}")
77
+ end
78
+
79
+ def remove_ip(ip, interface)
80
+ ssh("/sbin/ip addr del #{ip} dev #{interface}")
81
+ end
82
+
83
+ def slaveof(host)
84
+ if host.nil?
85
+ host, port = "no", "one"
86
+ else
87
+ host, port = host.address, host.port
88
+ end
89
+
90
+ $log.info("#{name} is slave of #{host} #{port}")
91
+ redis.slaveof(host, port)
92
+ end
93
+
94
+
95
+ def to_s
96
+ self.name.to_s
97
+ end
98
+
99
+ protected
100
+
101
+ def redis
102
+ @redis ||= Redis.new(:host => address, :port => port)
103
+ end
104
+
105
+ def ssh(command)
106
+ cmd = "/usr/bin/ssh -p #{ssh_port} #{ssh_user}@#{address} -c \"sudo #{command}\""
107
+ $log.debug "Executing #{cmd}"
108
+ # `#{cmd}`
109
+ end
110
+
111
+ end
112
+
113
+
114
+
115
+ desc "mon", "Start the monitor"
116
+ method_option :config, :default => "/etc/redis-mmm.conf", :required => true, :aliases => "-c"
117
+ def mon
118
+ init_state
119
+
120
+ loop do
121
+ $log.debug("Checking cluster status")
122
+ ensure_valid_cluster_status
123
+
124
+ sleep 2
125
+ end
126
+ end
127
+
128
+ desc "info [HOST]", "Get info about the host"
129
+ method_option :config, :default => "/etc/redis-mmm.conf", :required => true, :aliases => "-c"
130
+ def info(name)
131
+ host = self.host(name)
132
+ info = host.state
133
+
134
+ puts "Info about #{host}"
135
+ if host.online?
136
+ puts "Current role: #{info[:role]}"
137
+ if host.slave?
138
+ puts "Current master: #{info[:master_host]}:#{info[:master_port]}"
139
+ puts "Link to master: #{info[:master_link_status].upcase}"
140
+ end
141
+ else
142
+ puts "Offline!"
143
+ end
144
+ end
145
+
146
+ desc "status", "Show the current cluster status"
147
+ method_option :config, :default => "/etc/redis-mmm.conf", :required => true, :aliases => "-c"
148
+ def status
149
+ puts "Configured hosts"
150
+ hosts.each do |host|
151
+ puts "#{host}: #{host.online? ? "ONLINE" : "OFFLINE"} / #{host.master? ? "master" : "slave"}"
152
+ puts hosts.inspect
153
+ end
154
+ end
155
+
156
+ desc "set_master [HOST]", "Changes the master"
157
+ method_option :config, :default => "/etc/redis-mmm.conf", :required => true, :aliases => "-c"
158
+ def set_master(host)
159
+ init_state
160
+ change_master_to(host)
161
+ end
162
+
163
+ protected
164
+
165
+ def init_state
166
+ @current_master = find_current_master
167
+ $log.info "current master is #{@current_master}"
168
+ end
169
+
170
+
171
+ def ensure_valid_cluster_status
172
+ begin
173
+ validate_cluster_state
174
+ $log.debug("Cluster OK")
175
+
176
+ rescue RedisMMM::NoMaster
177
+ $log.fatal("THERE IS NO MASTER!")
178
+ new_master = elect_new_master!
179
+ $log.error("New master elected: #{new_master.name}")
180
+ retry
181
+
182
+ rescue RedisMMM::MoreThanOneMaster
183
+ $log.error("More than one master detected: #{$!.subject.map(&:name).join(", ")}")
184
+
185
+ $log.error("Re-electing #{@current_master.name} as master...")
186
+ change_master_to(@current_master, true)
187
+
188
+ retry
189
+
190
+ rescue RedisMMM::CurrentMasterWrong
191
+ $log.error("The server I think of as master is not a master... re-trying...")
192
+ @current_master = find_current_master
193
+ retry
194
+
195
+ rescue RedisMMM::MasterOffline
196
+ $log.error("MASTER (#{@current_master}) OFFLINE!")
197
+ new_master = elect_new_master!
198
+ $log.error("New master elected: #{new_master}")
199
+ retry
200
+
201
+ rescue RedisMMM::SlaveWrongMaster
202
+ $log.error "slave #{$!.subject} has the wrong master"
203
+ $!.subject.slaveof(@current_master)
204
+ retry
205
+
206
+ rescue RedisMMM::SlaveMasterLinkDown
207
+ rescue RedisMMM::SlaveOffline
208
+ end
209
+ end
210
+
211
+ # validates the state of the cluster
212
+ # a cluster is valid iff:
213
+ #
214
+ # * there is exactly ONE master
215
+ # * @current_master actually IS the master
216
+ # * the master is available and responding
217
+ # * all other servers are slaves
218
+ # * all slaves are either offline or replicating with the master AND in sync with master
219
+ def validate_cluster_state
220
+ master_hosts = hosts.select(&:master?)
221
+ slave_hosts = hosts.select(&:slave?)
222
+
223
+ if master_hosts.count > 1
224
+ raise MoreThanOneMaster, master_hosts
225
+ end
226
+
227
+ if master_hosts.count == 0
228
+ raise NoMaster, master_hosts
229
+ end
230
+
231
+ unless master_hosts[0] == @current_master
232
+ raise CurrentMasterWrong
233
+ end
234
+
235
+ unless @current_master.online?
236
+ raise MasterOffline
237
+ end
238
+
239
+ master_address = @current_master.address
240
+ master_port = @current_master.port
241
+
242
+ slave_hosts.each do |host|
243
+ status = host.state
244
+
245
+ unless status[:online]
246
+ raise SlaveOffline, host
247
+ end
248
+
249
+ if status[:master_host] != master_address || status[:master_port] != master_port
250
+ raise SlaveWrongMaster, host
251
+ end
252
+
253
+ if status[:master_link_status] != 'up'
254
+ raise SlaveMasterLinkDown, host
255
+ end
256
+
257
+ end
258
+
259
+
260
+ return true
261
+ end
262
+
263
+
264
+ def change_master_to(name, force = false)
265
+ host = self.host(name)
266
+
267
+ current_master = find_current_master
268
+
269
+ if current_master == host && force == false
270
+ $log.warn "#{host.name} is already master"
271
+ return
272
+ end
273
+
274
+ $log.info "Changing master from #{@current_master.name} to #{host.name}"
275
+
276
+ # remove master ip from old master
277
+ remove_master_ip_from(@current_master)
278
+ # add master ip to new master
279
+ add_master_ip_to(host)
280
+ # let new master stop replication
281
+ host.slaveof(nil)
282
+
283
+ # make all non-master hosts SLAVEOF new master
284
+ master_host = host.address
285
+ master_port = host.port
286
+ each_host do |slave|
287
+ next if slave == host
288
+
289
+ if !slave.online?
290
+ $log.warn("Skipping #{slave.name}, it's offline")
291
+ next
292
+ end
293
+
294
+ slave.slaveof(host)
295
+ end
296
+
297
+ @current_master = host
298
+ end
299
+
300
+
301
+ # * checks the slaves for a slave which could possibly be the new master
302
+ # * changes the master to the found slave
303
+ # * returns the new master host
304
+ #
305
+ def elect_new_master!
306
+ new_master = hosts.find(&:online?)
307
+
308
+ change_master_to(new_master, true)
309
+ new_master
310
+ end
311
+
312
+ def remove_master_ip_from(host)
313
+ host.remove_ip(master_ip, cluster_interface)
314
+ end
315
+
316
+ def add_master_ip_to(host)
317
+ host.add_ip(master_ip, cluster_interface)
318
+ end
319
+
320
+
321
+ def config
322
+ @config ||= ParseConfig.new(options.config)
323
+ end
324
+
325
+ def master_ip; config.params[:master_ip]; end
326
+ def cluster_interface; config.params[:cluster_interface]; end
327
+
328
+
329
+ # returns the hostname of the redis host which currently is master
330
+ def find_current_master
331
+ hosts.find(&:master?)
332
+ end
333
+
334
+ def each_host
335
+ hosts.each do |host|
336
+ yield host
337
+ end
338
+ end
339
+
340
+ def hosts
341
+ host_objects.values
342
+ end
343
+
344
+
345
+ def host_objects
346
+ @host_objects ||= config.groups.inject({}) do |hsh, hostname|
347
+ cfg = config.params[hostname.to_s]
348
+ hsh[hostname.to_sym] = Host.new(hostname, cfg)
349
+ hsh
350
+ end
351
+ end
352
+
353
+
354
+ def host(name)
355
+ Host === name ? name : host_objects[name.to_sym]
356
+ end
357
+ end
358
+
359
+ RedisMMM.start
360
+
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-mmm
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Michael Siebert
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-16 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: redis
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: thor
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: parseconfig
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ description: Automatic / manual failover
64
+ email:
65
+ - siebertm85@googlemail.com
66
+ executables:
67
+ - redis_mmm
68
+ extensions: []
69
+
70
+ extra_rdoc_files: []
71
+
72
+ files:
73
+ - README
74
+ - bin/redis_mmm
75
+ has_rdoc: true
76
+ homepage: http://github.com/siebertm/redis-mmm
77
+ licenses: []
78
+
79
+ post_install_message:
80
+ rdoc_options: []
81
+
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ requirements: []
103
+
104
+ rubyforge_project:
105
+ rubygems_version: 1.4.2
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: Manages a set of redis servers replicating each other
109
+ test_files: []
110
+