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.
- data/README +32 -0
- data/bin/redis_mmm +360 -0
- 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
|
+
|