redis-mmm 0.0.1

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.
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
+