1#!/usr/bin/env ruby 2 3# TODO (temporary here, we'll move this into the Github issues once 4# redis-trib initial implementation is completed). 5# 6# - Make sure that if the rehashing fails in the middle redis-trib will try 7# to recover. 8# - When redis-trib performs a cluster check, if it detects a slot move in 9# progress it should prompt the user to continue the move from where it 10# stopped. 11# - Gracefully handle Ctrl+C in move_slot to prompt the user if really stop 12# while rehashing, and performing the best cleanup possible if the user 13# forces the quit. 14# - When doing "fix" set a global Fix to true, and prompt the user to 15# fix the problem if automatically fixable every time there is something 16# to fix. For instance: 17# 1) If there is a node that pretend to receive a slot, or to migrate a 18# slot, but has no entries in that slot, fix it. 19# 2) If there is a node having keys in slots that are not owned by it 20# fix this condition moving the entries in the same node. 21# 3) Perform more possibly slow tests about the state of the cluster. 22# 4) When aborted slot migration is detected, fix it. 23 24require 'rubygems' 25require 'redis' 26 27ClusterHashSlots = 16384 28 29def xputs(s) 30 case s[0..2] 31 when ">>>" 32 color="29;1" 33 when "[ER" 34 color="31;1" 35 when "[OK" 36 color="32" 37 when "[FA","***" 38 color="33" 39 else 40 color=nil 41 end 42 43 color = nil if ENV['TERM'] != "xterm" 44 print "\033[#{color}m" if color 45 print s 46 print "\033[0m" if color 47 print "\n" 48end 49 50class ClusterNode 51 def initialize(addr) 52 s = addr.split(":") 53 if s.length != 2 54 puts "Invalid node name #{addr}" 55 exit 1 56 end 57 @r = nil 58 @info = {} 59 @info[:host] = s[0] 60 @info[:port] = s[1] 61 @info[:slots] = {} 62 @info[:migrating] = {} 63 @info[:importing] = {} 64 @info[:replicate] = false 65 @dirty = false # True if we need to flush slots info into node. 66 @friends = [] 67 end 68 69 def friends 70 @friends 71 end 72 73 def slots 74 @info[:slots] 75 end 76 77 def has_flag?(flag) 78 @info[:flags].index(flag) 79 end 80 81 def to_s 82 "#{@info[:host]}:#{@info[:port]}" 83 end 84 85 def connect(o={}) 86 return if @r 87 print "Connecting to node #{self}: " 88 STDOUT.flush 89 begin 90 @r = Redis.new(:host => @info[:host], :port => @info[:port], :timeout => 60) 91 @r.ping 92 rescue 93 xputs "[ERR] Sorry, can't connect to node #{self}" 94 exit 1 if o[:abort] 95 @r = nil 96 end 97 xputs "OK" 98 end 99 100 def assert_cluster 101 info = @r.info 102 if !info["cluster_enabled"] || info["cluster_enabled"].to_i == 0 103 xputs "[ERR] Node #{self} is not configured as a cluster node." 104 exit 1 105 end 106 end 107 108 def assert_empty 109 if !(@r.cluster("info").split("\r\n").index("cluster_known_nodes:1")) || 110 (@r.info['db0']) 111 xputs "[ERR] Node #{self} is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0." 112 exit 1 113 end 114 end 115 116 def load_info(o={}) 117 self.connect 118 nodes = @r.cluster("nodes").split("\n") 119 nodes.each{|n| 120 # name addr flags role ping_sent ping_recv link_status slots 121 split = n.split 122 name,addr,flags,master_id,ping_sent,ping_recv,config_epoch,link_status = split[0..6] 123 slots = split[8..-1] 124 info = { 125 :name => name, 126 :addr => addr, 127 :flags => flags.split(","), 128 :replicate => master_id, 129 :ping_sent => ping_sent.to_i, 130 :ping_recv => ping_recv.to_i, 131 :link_status => link_status 132 } 133 info[:replicate] = false if master_id == "-" 134 135 if info[:flags].index("myself") 136 @info = @info.merge(info) 137 @info[:slots] = {} 138 slots.each{|s| 139 if s[0..0] == '[' 140 if s.index("->-") # Migrating 141 slot,dst = s[1..-1].split("->-") 142 @info[:migrating][slot.to_i] = dst 143 elsif s.index("-<-") # Importing 144 slot,src = s[1..-1].split("-<-") 145 @info[:importing][slot.to_i] = src 146 end 147 elsif s.index("-") 148 start,stop = s.split("-") 149 self.add_slots((start.to_i)..(stop.to_i)) 150 else 151 self.add_slots((s.to_i)..(s.to_i)) 152 end 153 } if slots 154 @dirty = false 155 @r.cluster("info").split("\n").each{|e| 156 k,v=e.split(":") 157 k = k.to_sym 158 v.chop! 159 if k != :cluster_state 160 @info[k] = v.to_i 161 else 162 @info[k] = v 163 end 164 } 165 elsif o[:getfriends] 166 @friends << info 167 end 168 } 169 end 170 171 def add_slots(slots) 172 slots.each{|s| 173 @info[:slots][s] = :new 174 } 175 @dirty = true 176 end 177 178 def set_as_replica(node_id) 179 @info[:replicate] = node_id 180 @dirty = true 181 end 182 183 def flush_node_config 184 return if !@dirty 185 if @info[:replicate] 186 begin 187 @r.cluster("replicate",@info[:replicate]) 188 rescue 189 # If the cluster did not already joined it is possible that 190 # the slave does not know the master node yet. So on errors 191 # we return ASAP leaving the dirty flag set, to flush the 192 # config later. 193 return 194 end 195 else 196 new = [] 197 @info[:slots].each{|s,val| 198 if val == :new 199 new << s 200 @info[:slots][s] = true 201 end 202 } 203 @r.cluster("addslots",*new) 204 end 205 @dirty = false 206 end 207 208 def info_string 209 # We want to display the hash slots assigned to this node 210 # as ranges, like in: "1-5,8-9,20-25,30" 211 # 212 # Note: this could be easily written without side effects, 213 # we use 'slots' just to split the computation into steps. 214 215 # First step: we want an increasing array of integers 216 # for instance: [1,2,3,4,5,8,9,20,21,22,23,24,25,30] 217 slots = @info[:slots].keys.sort 218 219 # As we want to aggregate adjacent slots we convert all the 220 # slot integers into ranges (with just one element) 221 # So we have something like [1..1,2..2, ... and so forth. 222 slots.map!{|x| x..x} 223 224 # Finally we group ranges with adjacent elements. 225 slots = slots.reduce([]) {|a,b| 226 if !a.empty? && b.first == (a[-1].last)+1 227 a[0..-2] + [(a[-1].first)..(b.last)] 228 else 229 a + [b] 230 end 231 } 232 233 # Now our task is easy, we just convert ranges with just one 234 # element into a number, and a real range into a start-end format. 235 # Finally we join the array using the comma as separator. 236 slots = slots.map{|x| 237 x.count == 1 ? x.first.to_s : "#{x.first}-#{x.last}" 238 }.join(",") 239 240 role = self.has_flag?("master") ? "M" : "S" 241 242 if self.info[:replicate] and @dirty 243 is = "S: #{self.info[:name]} #{self.to_s}" 244 else 245 is = "#{role}: #{self.info[:name]} #{self.to_s}\n"+ 246 " slots:#{slots} (#{self.slots.length} slots) "+ 247 "#{(self.info[:flags]-["myself"]).join(",")}" 248 end 249 if self.info[:replicate] 250 is += "\n replicates #{info[:replicate]}" 251 elsif self.has_flag?("master") && self.info[:replicas] 252 is += "\n #{info[:replicas].length} additional replica(s)" 253 end 254 is 255 end 256 257 # Return a single string representing nodes and associated slots. 258 # TODO: remove slaves from config when slaves will be handled 259 # by Redis Cluster. 260 def get_config_signature 261 config = [] 262 @r.cluster("nodes").each_line{|l| 263 s = l.split 264 slots = s[8..-1].select {|x| x[0..0] != "["} 265 next if slots.length == 0 266 config << s[0]+":"+(slots.sort.join(",")) 267 } 268 config.sort.join("|") 269 end 270 271 def info 272 @info 273 end 274 275 def is_dirty? 276 @dirty 277 end 278 279 def r 280 @r 281 end 282end 283 284class RedisTrib 285 def initialize 286 @nodes = [] 287 @fix = false 288 @errors = [] 289 end 290 291 def check_arity(req_args, num_args) 292 if ((req_args > 0 and num_args != req_args) || 293 (req_args < 0 and num_args < req_args.abs)) 294 xputs "[ERR] Wrong number of arguments for specified sub command" 295 exit 1 296 end 297 end 298 299 def add_node(node) 300 @nodes << node 301 end 302 303 def cluster_error(msg) 304 @errors << msg 305 xputs msg 306 end 307 308 def get_node_by_name(name) 309 @nodes.each{|n| 310 return n if n.info[:name] == name.downcase 311 } 312 return nil 313 end 314 315 # This function returns the master that has the least number of replicas 316 # in the cluster. If there are multiple masters with the same smaller 317 # number of replicas, one at random is returned. 318 def get_master_with_least_replicas 319 masters = @nodes.select{|n| n.has_flag? "master"} 320 sorted = masters.sort{|a,b| 321 a.info[:replicas].length <=> b.info[:replicas].length 322 } 323 sorted[0] 324 end 325 326 def check_cluster 327 xputs ">>> Performing Cluster Check (using node #{@nodes[0]})" 328 show_nodes 329 check_config_consistency 330 check_open_slots 331 check_slots_coverage 332 end 333 334 # Merge slots of every known node. If the resulting slots are equal 335 # to ClusterHashSlots, then all slots are served. 336 def covered_slots 337 slots = {} 338 @nodes.each{|n| 339 slots = slots.merge(n.slots) 340 } 341 slots 342 end 343 344 def check_slots_coverage 345 xputs ">>> Check slots coverage..." 346 slots = covered_slots 347 if slots.length == ClusterHashSlots 348 xputs "[OK] All #{ClusterHashSlots} slots covered." 349 else 350 cluster_error \ 351 "[ERR] Not all #{ClusterHashSlots} slots are covered by nodes." 352 fix_slots_coverage if @fix 353 end 354 end 355 356 def check_open_slots 357 xputs ">>> Check for open slots..." 358 open_slots = [] 359 @nodes.each{|n| 360 if n.info[:migrating].size > 0 361 cluster_error \ 362 "[WARNING] Node #{n} has slots in migrating state (#{n.info[:migrating].keys.join(",")})." 363 open_slots += n.info[:migrating].keys 364 elsif n.info[:importing].size > 0 365 cluster_error \ 366 "[WARNING] Node #{n} has slots in importing state (#{n.info[:importing].keys.join(",")})." 367 open_slots += n.info[:importing].keys 368 end 369 } 370 open_slots.uniq! 371 if open_slots.length > 0 372 xputs "[WARNING] The following slots are open: #{open_slots.join(",")}" 373 end 374 if @fix 375 open_slots.each{|slot| fix_open_slot slot} 376 end 377 end 378 379 def nodes_with_keys_in_slot(slot) 380 nodes = [] 381 @nodes.each{|n| 382 nodes << n if n.r.cluster("getkeysinslot",slot,1).length > 0 383 } 384 nodes 385 end 386 387 def fix_slots_coverage 388 not_covered = (0...ClusterHashSlots).to_a - covered_slots.keys 389 xputs ">>> Fixing slots coverage..." 390 xputs "List of not covered slots: " + not_covered.join(",") 391 392 # For every slot, take action depending on the actual condition: 393 # 1) No node has keys for this slot. 394 # 2) A single node has keys for this slot. 395 # 3) Multiple nodes have keys for this slot. 396 slots = {} 397 not_covered.each{|slot| 398 nodes = nodes_with_keys_in_slot(slot) 399 slots[slot] = nodes 400 xputs "Slot #{slot} has keys in #{nodes.length} nodes: #{nodes.join}" 401 } 402 403 none = slots.select {|k,v| v.length == 0} 404 single = slots.select {|k,v| v.length == 1} 405 multi = slots.select {|k,v| v.length > 1} 406 407 # Handle case "1": keys in no node. 408 if none.length > 0 409 xputs "The folowing uncovered slots have no keys across the cluster:" 410 xputs none.keys.join(",") 411 yes_or_die "Fix these slots by covering with a random node?" 412 none.each{|slot,nodes| 413 node = @nodes.sample 414 xputs ">>> Covering slot #{slot} with #{node}" 415 node.r.cluster("addslots",slot) 416 } 417 end 418 419 # Handle case "2": keys only in one node. 420 if single.length > 0 421 xputs "The folowing uncovered slots have keys in just one node:" 422 puts single.keys.join(",") 423 yes_or_die "Fix these slots by covering with those nodes?" 424 single.each{|slot,nodes| 425 xputs ">>> Covering slot #{slot} with #{nodes[0]}" 426 nodes[0].r.cluster("addslots",slot) 427 } 428 end 429 430 # Handle case "3": keys in multiple nodes. 431 if multi.length > 0 432 xputs "The folowing uncovered slots have keys in multiple nodes:" 433 xputs multi.keys.join(",") 434 yes_or_die "Fix these slots by moving keys into a single node?" 435 multi.each{|slot,nodes| 436 xputs ">>> Covering slot #{slot} moving keys to #{nodes[0]}" 437 # TODO 438 # 1) Set all nodes as "MIGRATING" for this slot, so that we 439 # can access keys in the hash slot using ASKING. 440 # 2) Move everything to node[0] 441 # 3) Clear MIGRATING from nodes, and ADDSLOTS the slot to 442 # node[0]. 443 raise "TODO: Work in progress" 444 } 445 end 446 end 447 448 # Return the owner of the specified slot 449 def get_slot_owner(slot) 450 @nodes.each{|n| 451 n.slots.each{|s,_| 452 return n if s == slot 453 } 454 } 455 nil 456 end 457 458 # Slot 'slot' was found to be in importing or migrating state in one or 459 # more nodes. This function fixes this condition by migrating keys where 460 # it seems more sensible. 461 def fix_open_slot(slot) 462 puts ">>> Fixing open slot #{slot}" 463 464 # Try to obtain the current slot owner, according to the current 465 # nodes configuration. 466 owner = get_slot_owner(slot) 467 468 # If there is no slot owner, set as owner the slot with the biggest 469 # number of keys, among the set of migrating / importing nodes. 470 if !owner 471 xputs "*** Fix me, some work to do here." 472 # Select owner... 473 # Use ADDSLOTS to assign the slot. 474 exit 1 475 end 476 477 migrating = [] 478 importing = [] 479 @nodes.each{|n| 480 next if n.has_flag? "slave" 481 if n.info[:migrating][slot] 482 migrating << n 483 elsif n.info[:importing][slot] 484 importing << n 485 elsif n.r.cluster("countkeysinslot",slot) > 0 && n != owner 486 xputs "*** Found keys about slot #{slot} in node #{n}!" 487 importing << n 488 end 489 } 490 puts "Set as migrating in: #{migrating.join(",")}" 491 puts "Set as importing in: #{importing.join(",")}" 492 493 # Case 1: The slot is in migrating state in one slot, and in 494 # importing state in 1 slot. That's trivial to address. 495 if migrating.length == 1 && importing.length == 1 496 move_slot(migrating[0],importing[0],slot,:verbose=>true,:fix=>true) 497 elsif migrating.length == 0 && importing.length > 0 498 xputs ">>> Moving all the #{slot} slot keys to its owner #{owner}" 499 importing.each {|node| 500 next if node == owner 501 move_slot(node,owner,slot,:verbose=>true,:fix=>true,:cold=>true) 502 xputs ">>> Setting #{slot} as STABLE in #{node}" 503 node.r.cluster("setslot",slot,"stable") 504 } 505 else 506 xputs "[ERR] Sorry, Redis-trib can't fix this slot yet (work in progress)" 507 end 508 end 509 510 # Check if all the nodes agree about the cluster configuration 511 def check_config_consistency 512 if !is_config_consistent? 513 cluster_error "[ERR] Nodes don't agree about configuration!" 514 else 515 xputs "[OK] All nodes agree about slots configuration." 516 end 517 end 518 519 def is_config_consistent? 520 signatures=[] 521 @nodes.each{|n| 522 signatures << n.get_config_signature 523 } 524 return signatures.uniq.length == 1 525 end 526 527 def wait_cluster_join 528 print "Waiting for the cluster to join" 529 while !is_config_consistent? 530 print "." 531 STDOUT.flush 532 sleep 1 533 end 534 print "\n" 535 end 536 537 def alloc_slots 538 nodes_count = @nodes.length 539 masters_count = @nodes.length / (@replicas+1) 540 masters = [] 541 slaves = [] 542 543 # The first step is to split instances by IP. This is useful as 544 # we'll try to allocate master nodes in different physical machines 545 # (as much as possible) and to allocate slaves of a given master in 546 # different physical machines as well. 547 # 548 # This code assumes just that if the IP is different, than it is more 549 # likely that the instance is running in a different physical host 550 # or at least a different virtual machine. 551 ips = {} 552 @nodes.each{|n| 553 ips[n.info[:host]] = [] if !ips[n.info[:host]] 554 ips[n.info[:host]] << n 555 } 556 557 # Select master instances 558 puts "Using #{masters_count} masters:" 559 while masters.length < masters_count 560 ips.each{|ip,nodes_list| 561 next if nodes_list.length == 0 562 masters << nodes_list.shift 563 puts masters[-1] 564 nodes_count -= 1 565 break if masters.length == masters_count 566 } 567 end 568 569 # Alloc slots on masters 570 slots_per_node = ClusterHashSlots.to_f / masters_count 571 first = 0 572 cursor = 0.0 573 masters.each_with_index{|n,masternum| 574 last = (cursor+slots_per_node-1).round 575 if last > ClusterHashSlots || masternum == masters.length-1 576 last = ClusterHashSlots-1 577 end 578 last = first if last < first # Min step is 1. 579 n.add_slots first..last 580 first = last+1 581 cursor += slots_per_node 582 } 583 584 # Select N replicas for every master. 585 # We try to split the replicas among all the IPs with spare nodes 586 # trying to avoid the host where the master is running, if possible. 587 # 588 # Note we loop two times. The first loop assigns the requested 589 # number of replicas to each master. The second loop assigns any 590 # remaining instances as extra replicas to masters. Some masters 591 # may end up with more than their requested number of replicas, but 592 # all nodes will be used. 593 assignment_verbose = false 594 595 [:requested,:unused].each{|assign| 596 masters.each{|m| 597 assigned_replicas = 0 598 while assigned_replicas < @replicas 599 break if nodes_count == 0 600 if assignment_verbose 601 if assign == :requested 602 puts "Requesting total of #{@replicas} replicas " \ 603 "(#{assigned_replicas} replicas assigned " \ 604 "so far with #{nodes_count} total remaining)." 605 elsif assign == :unused 606 puts "Assigning extra instance to replication " \ 607 "role too (#{nodes_count} remaining)." 608 end 609 end 610 ips.each{|ip,nodes_list| 611 next if nodes_list.length == 0 612 # Skip instances with the same IP as the master if we 613 # have some more IPs available. 614 next if ip == m.info[:host] && nodes_count > nodes_list.length 615 slave = nodes_list.shift 616 slave.set_as_replica(m.info[:name]) 617 nodes_count -= 1 618 assigned_replicas += 1 619 puts "Adding replica #{slave} to #{m}" 620 break 621 } 622 end 623 } 624 } 625 end 626 627 def flush_nodes_config 628 @nodes.each{|n| 629 n.flush_node_config 630 } 631 end 632 633 def show_nodes 634 @nodes.each{|n| 635 xputs n.info_string 636 } 637 end 638 639 # Redis Cluster config epoch collision resolution code is able to eventually 640 # set a different epoch to each node after a new cluster is created, but 641 # it is slow compared to assign a progressive config epoch to each node 642 # before joining the cluster. However we do just a best-effort try here 643 # since if we fail is not a problem. 644 def assign_config_epoch 645 config_epoch = 1 646 @nodes.each{|n| 647 begin 648 n.r.cluster("set-config-epoch",config_epoch) 649 rescue 650 end 651 config_epoch += 1 652 } 653 end 654 655 def join_cluster 656 # We use a brute force approach to make sure the node will meet 657 # each other, that is, sending CLUSTER MEET messages to all the nodes 658 # about the very same node. 659 # Thanks to gossip this information should propagate across all the 660 # cluster in a matter of seconds. 661 first = false 662 @nodes.each{|n| 663 if !first then first = n.info; next; end # Skip the first node 664 n.r.cluster("meet",first[:host],first[:port]) 665 } 666 end 667 668 def yes_or_die(msg) 669 print "#{msg} (type 'yes' to accept): " 670 STDOUT.flush 671 if !(STDIN.gets.chomp.downcase == "yes") 672 xputs "*** Aborting..." 673 exit 1 674 end 675 end 676 677 def load_cluster_info_from_node(nodeaddr) 678 node = ClusterNode.new(nodeaddr) 679 node.connect(:abort => true) 680 node.assert_cluster 681 node.load_info(:getfriends => true) 682 add_node(node) 683 node.friends.each{|f| 684 next if f[:flags].index("noaddr") || 685 f[:flags].index("disconnected") || 686 f[:flags].index("fail") 687 fnode = ClusterNode.new(f[:addr]) 688 fnode.connect() 689 fnode.load_info() 690 add_node(fnode) 691 } 692 populate_nodes_replicas_info 693 end 694 695 # This function is called by load_cluster_info_from_node in order to 696 # add additional information to every node as a list of replicas. 697 def populate_nodes_replicas_info 698 # Start adding the new field to every node. 699 @nodes.each{|n| 700 n.info[:replicas] = [] 701 } 702 703 # Populate the replicas field using the replicate field of slave 704 # nodes. 705 @nodes.each{|n| 706 if n.info[:replicate] 707 master = get_node_by_name(n.info[:replicate]) 708 if !master 709 xputs "*** WARNING: #{n} claims to be slave of unknown node ID #{n.info[:replicate]}." 710 else 711 master.info[:replicas] << n 712 end 713 end 714 } 715 end 716 717 # Given a list of source nodes return a "resharding plan" 718 # with what slots to move in order to move "numslots" slots to another 719 # instance. 720 def compute_reshard_table(sources,numslots) 721 moved = [] 722 # Sort from bigger to smaller instance, for two reasons: 723 # 1) If we take less slots than instances it is better to start 724 # getting from the biggest instances. 725 # 2) We take one slot more from the first instance in the case of not 726 # perfect divisibility. Like we have 3 nodes and need to get 10 727 # slots, we take 4 from the first, and 3 from the rest. So the 728 # biggest is always the first. 729 sources = sources.sort{|a,b| b.slots.length <=> a.slots.length} 730 source_tot_slots = sources.inject(0) {|sum,source| 731 sum+source.slots.length 732 } 733 sources.each_with_index{|s,i| 734 # Every node will provide a number of slots proportional to the 735 # slots it has assigned. 736 n = (numslots.to_f/source_tot_slots*s.slots.length) 737 if i == 0 738 n = n.ceil 739 else 740 n = n.floor 741 end 742 s.slots.keys.sort[(0...n)].each{|slot| 743 if moved.length < numslots 744 moved << {:source => s, :slot => slot} 745 end 746 } 747 } 748 return moved 749 end 750 751 def show_reshard_table(table) 752 table.each{|e| 753 puts " Moving slot #{e[:slot]} from #{e[:source].info[:name]}" 754 } 755 end 756 757 # Move slots between source and target nodes using MIGRATE. 758 # 759 # Options: 760 # :verbose -- Print a dot for every moved key. 761 # :fix -- We are moving in the context of a fix. Use REPLACE. 762 # :cold -- Move keys without opening / reconfiguring the nodes. 763 def move_slot(source,target,slot,o={}) 764 # We start marking the slot as importing in the destination node, 765 # and the slot as migrating in the target host. Note that the order of 766 # the operations is important, as otherwise a client may be redirected 767 # to the target node that does not yet know it is importing this slot. 768 print "Moving slot #{slot} from #{source} to #{target}: "; STDOUT.flush 769 if !o[:cold] 770 target.r.cluster("setslot",slot,"importing",source.info[:name]) 771 source.r.cluster("setslot",slot,"migrating",target.info[:name]) 772 end 773 # Migrate all the keys from source to target using the MIGRATE command 774 while true 775 keys = source.r.cluster("getkeysinslot",slot,10) 776 break if keys.length == 0 777 keys.each{|key| 778 begin 779 source.r.client.call(["migrate",target.info[:host],target.info[:port],key,0,15000]) 780 rescue => e 781 if o[:fix] && e.to_s =~ /BUSYKEY/ 782 xputs "*** Target key #{key} exists. Replace it for FIX." 783 source.r.client.call(["migrate",target.info[:host],target.info[:port],key,0,15000,:replace]) 784 else 785 puts "" 786 xputs "[ERR] #{e}" 787 exit 1 788 end 789 end 790 print "." if o[:verbose] 791 STDOUT.flush 792 } 793 end 794 795 puts 796 # Set the new node as the owner of the slot in all the known nodes. 797 if !o[:cold] 798 @nodes.each{|n| 799 n.r.cluster("setslot",slot,"node",target.info[:name]) 800 } 801 end 802 end 803 804 # redis-trib subcommands implementations 805 806 def check_cluster_cmd(argv,opt) 807 load_cluster_info_from_node(argv[0]) 808 check_cluster 809 end 810 811 def fix_cluster_cmd(argv,opt) 812 @fix = true 813 load_cluster_info_from_node(argv[0]) 814 check_cluster 815 end 816 817 def reshard_cluster_cmd(argv,opt) 818 load_cluster_info_from_node(argv[0]) 819 check_cluster 820 if @errors.length != 0 821 puts "*** Please fix your cluster problems before resharding" 822 exit 1 823 end 824 numslots = 0 825 while numslots <= 0 or numslots > ClusterHashSlots 826 print "How many slots do you want to move (from 1 to #{ClusterHashSlots})? " 827 numslots = STDIN.gets.to_i 828 end 829 target = nil 830 while not target 831 print "What is the receiving node ID? " 832 target = get_node_by_name(STDIN.gets.chop) 833 if !target || target.has_flag?("slave") 834 xputs "*** The specified node is not known or not a master, please retry." 835 target = nil 836 end 837 end 838 sources = [] 839 xputs "Please enter all the source node IDs." 840 xputs " Type 'all' to use all the nodes as source nodes for the hash slots." 841 xputs " Type 'done' once you entered all the source nodes IDs." 842 while true 843 print "Source node ##{sources.length+1}:" 844 line = STDIN.gets.chop 845 src = get_node_by_name(line) 846 if line == "done" 847 if sources.length == 0 848 puts "No source nodes given, operation aborted" 849 exit 1 850 else 851 break 852 end 853 elsif line == "all" 854 @nodes.each{|n| 855 next if n.info[:name] == target.info[:name] 856 next if n.has_flag?("slave") 857 sources << n 858 } 859 break 860 elsif !src || src.has_flag?("slave") 861 xputs "*** The specified node is not known or is not a master, please retry." 862 elsif src.info[:name] == target.info[:name] 863 xputs "*** It is not possible to use the target node as source node." 864 else 865 sources << src 866 end 867 end 868 puts "\nReady to move #{numslots} slots." 869 puts " Source nodes:" 870 sources.each{|s| puts " "+s.info_string} 871 puts " Destination node:" 872 puts " #{target.info_string}" 873 reshard_table = compute_reshard_table(sources,numslots) 874 puts " Resharding plan:" 875 show_reshard_table(reshard_table) 876 print "Do you want to proceed with the proposed reshard plan (yes/no)? " 877 yesno = STDIN.gets.chop 878 exit(1) if (yesno != "yes") 879 reshard_table.each{|e| 880 move_slot(e[:source],target,e[:slot],:verbose=>true) 881 } 882 end 883 884 # This is an helper function for create_cluster_cmd that verifies if 885 # the number of nodes and the specified replicas have a valid configuration 886 # where there are at least three master nodes and enough replicas per node. 887 def check_create_parameters 888 masters = @nodes.length/(@replicas+1) 889 if masters < 3 890 puts "*** ERROR: Invalid configuration for cluster creation." 891 puts "*** Redis Cluster requires at least 3 master nodes." 892 puts "*** This is not possible with #{@nodes.length} nodes and #{@replicas} replicas per node." 893 puts "*** At least #{3*(@replicas+1)} nodes are required." 894 exit 1 895 end 896 end 897 898 def create_cluster_cmd(argv,opt) 899 opt = {'replicas' => 0}.merge(opt) 900 @replicas = opt['replicas'].to_i 901 902 xputs ">>> Creating cluster" 903 argv[0..-1].each{|n| 904 node = ClusterNode.new(n) 905 node.connect(:abort => true) 906 node.assert_cluster 907 node.load_info 908 node.assert_empty 909 add_node(node) 910 } 911 check_create_parameters 912 xputs ">>> Performing hash slots allocation on #{@nodes.length} nodes..." 913 alloc_slots 914 show_nodes 915 yes_or_die "Can I set the above configuration?" 916 flush_nodes_config 917 xputs ">>> Nodes configuration updated" 918 xputs ">>> Assign a different config epoch to each node" 919 assign_config_epoch 920 xputs ">>> Sending CLUSTER MEET messages to join the cluster" 921 join_cluster 922 # Give one second for the join to start, in order to avoid that 923 # wait_cluster_join will find all the nodes agree about the config as 924 # they are still empty with unassigned slots. 925 sleep 1 926 wait_cluster_join 927 flush_nodes_config # Useful for the replicas 928 check_cluster 929 end 930 931 def addnode_cluster_cmd(argv,opt) 932 xputs ">>> Adding node #{argv[0]} to cluster #{argv[1]}" 933 934 # Check the existing cluster 935 load_cluster_info_from_node(argv[1]) 936 check_cluster 937 938 # If --master-id was specified, try to resolve it now so that we 939 # abort before starting with the node configuration. 940 if opt['slave'] 941 if opt['master-id'] 942 master = get_node_by_name(opt['master-id']) 943 if !master 944 xputs "[ERR] No such master ID #{opt['master-id']}" 945 end 946 else 947 master = get_master_with_least_replicas 948 xputs "Automatically selected master #{master}" 949 end 950 end 951 952 # Add the new node 953 new = ClusterNode.new(argv[0]) 954 new.connect(:abort => true) 955 new.assert_cluster 956 new.load_info 957 new.assert_empty 958 first = @nodes.first.info 959 add_node(new) 960 961 # Send CLUSTER MEET command to the new node 962 xputs ">>> Send CLUSTER MEET to node #{new} to make it join the cluster." 963 new.r.cluster("meet",first[:host],first[:port]) 964 965 # Additional configuration is needed if the node is added as 966 # a slave. 967 if opt['slave'] 968 wait_cluster_join 969 xputs ">>> Configure node as replica of #{master}." 970 new.r.cluster("replicate",master.info[:name]) 971 end 972 xputs "[OK] New node added correctly." 973 end 974 975 def delnode_cluster_cmd(argv,opt) 976 id = argv[1].downcase 977 xputs ">>> Removing node #{id} from cluster #{argv[0]}" 978 979 # Load cluster information 980 load_cluster_info_from_node(argv[0]) 981 982 # Check if the node exists and is not empty 983 node = get_node_by_name(id) 984 985 if !node 986 xputs "[ERR] No such node ID #{id}" 987 exit 1 988 end 989 990 if node.slots.length != 0 991 xputs "[ERR] Node #{node} is not empty! Reshard data away and try again." 992 exit 1 993 end 994 995 # Send CLUSTER FORGET to all the nodes but the node to remove 996 xputs ">>> Sending CLUSTER FORGET messages to the cluster..." 997 @nodes.each{|n| 998 next if n == node 999 if n.info[:replicate] && n.info[:replicate].downcase == id 1000 # Reconfigure the slave to replicate with some other node 1001 master = get_master_with_least_replicas 1002 xputs ">>> #{n} as replica of #{master}" 1003 n.r.cluster("replicate",master.info[:name]) 1004 end 1005 n.r.cluster("forget",argv[1]) 1006 } 1007 1008 # Finally shutdown the node 1009 xputs ">>> SHUTDOWN the node." 1010 node.r.shutdown 1011 end 1012 1013 def set_timeout_cluster_cmd(argv,opt) 1014 timeout = argv[1].to_i 1015 if timeout < 100 1016 puts "Setting a node timeout of less than 100 milliseconds is a bad idea." 1017 exit 1 1018 end 1019 1020 # Load cluster information 1021 load_cluster_info_from_node(argv[0]) 1022 ok_count = 0 1023 err_count = 0 1024 1025 # Send CLUSTER FORGET to all the nodes but the node to remove 1026 xputs ">>> Reconfiguring node timeout in every cluster node..." 1027 @nodes.each{|n| 1028 begin 1029 n.r.config("set","cluster-node-timeout",timeout) 1030 n.r.config("rewrite") 1031 ok_count += 1 1032 xputs "*** New timeout set for #{n}" 1033 rescue => e 1034 puts "ERR setting node-timeot for #{n}: #{e}" 1035 err_count += 1 1036 end 1037 } 1038 xputs ">>> New node timeout set. #{ok_count} OK, #{err_count} ERR." 1039 end 1040 1041 def call_cluster_cmd(argv,opt) 1042 cmd = argv[1..-1] 1043 cmd[0] = cmd[0].upcase 1044 1045 # Load cluster information 1046 load_cluster_info_from_node(argv[0]) 1047 xputs ">>> Calling #{cmd.join(" ")}" 1048 @nodes.each{|n| 1049 begin 1050 res = n.r.send(*cmd) 1051 puts "#{n}: #{res}" 1052 rescue => e 1053 puts "#{n}: #{e}" 1054 end 1055 } 1056 end 1057 1058 def import_cluster_cmd(argv,opt) 1059 source_addr = opt['from'] 1060 xputs ">>> Importing data from #{source_addr} to cluster #{argv[1]}" 1061 1062 # Check the existing cluster. 1063 load_cluster_info_from_node(argv[0]) 1064 check_cluster 1065 1066 # Connect to the source node. 1067 xputs ">>> Connecting to the source Redis instance" 1068 src_host,src_port = source_addr.split(":") 1069 source = Redis.new(:host =>src_host, :port =>src_port) 1070 if source.info['cluster_enabled'].to_i == 1 1071 xputs "[ERR] The source node should not be a cluster node." 1072 end 1073 xputs "*** Importing #{source.dbsize} keys from DB 0" 1074 1075 # Build a slot -> node map 1076 slots = {} 1077 @nodes.each{|n| 1078 n.slots.each{|s,_| 1079 slots[s] = n 1080 } 1081 } 1082 1083 # Use SCAN to iterate over the keys, migrating to the 1084 # right node as needed. 1085 cursor = nil 1086 while cursor != 0 1087 cursor,keys = source.scan(cursor,:count,1000) 1088 cursor = cursor.to_i 1089 keys.each{|k| 1090 # Migrate keys using the MIGRATE command. 1091 slot = key_to_slot(k) 1092 target = slots[slot] 1093 print "Migrating #{k} to #{target}: " 1094 STDOUT.flush 1095 begin 1096 source.client.call(["migrate",target.info[:host],target.info[:port],k,0,15000]) 1097 rescue => e 1098 puts e 1099 else 1100 puts "OK" 1101 end 1102 } 1103 end 1104 end 1105 1106 def help_cluster_cmd(argv,opt) 1107 show_help 1108 exit 0 1109 end 1110 1111 # Parse the options for the specific command "cmd". 1112 # Returns an hash populate with option => value pairs, and the index of 1113 # the first non-option argument in ARGV. 1114 def parse_options(cmd) 1115 idx = 1 ; # Current index into ARGV 1116 options={} 1117 while idx < ARGV.length && ARGV[idx][0..1] == '--' 1118 if ARGV[idx][0..1] == "--" 1119 option = ARGV[idx][2..-1] 1120 idx += 1 1121 if ALLOWED_OPTIONS[cmd] == nil || ALLOWED_OPTIONS[cmd][option] == nil 1122 puts "Unknown option '#{option}' for command '#{cmd}'" 1123 exit 1 1124 end 1125 if ALLOWED_OPTIONS[cmd][option] 1126 value = ARGV[idx] 1127 idx += 1 1128 else 1129 value = true 1130 end 1131 options[option] = value 1132 else 1133 # Remaining arguments are not options. 1134 break 1135 end 1136 end 1137 1138 # Enforce mandatory options 1139 if ALLOWED_OPTIONS[cmd] 1140 ALLOWED_OPTIONS[cmd].each {|option,val| 1141 if !options[option] && val == :required 1142 puts "Option '--#{option}' is required "+ \ 1143 "for subcommand '#{cmd}'" 1144 exit 1 1145 end 1146 } 1147 end 1148 return options,idx 1149 end 1150end 1151 1152################################################################################# 1153# Libraries 1154# 1155# We try to don't depend on external libs since this is a critical part 1156# of Redis Cluster. 1157################################################################################# 1158 1159# This is the CRC16 algorithm used by Redis Cluster to hash keys. 1160# Implementation according to CCITT standards. 1161# 1162# This is actually the XMODEM CRC 16 algorithm, using the 1163# following parameters: 1164# 1165# Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN" 1166# Width : 16 bit 1167# Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1) 1168# Initialization : 0000 1169# Reflect Input byte : False 1170# Reflect Output CRC : False 1171# Xor constant to output CRC : 0000 1172# Output for "123456789" : 31C3 1173 1174module RedisClusterCRC16 1175 def RedisClusterCRC16.crc16(bytes) 1176 crc = 0 1177 bytes.each_byte{|b| 1178 crc = ((crc<<8) & 0xffff) ^ XMODEMCRC16Lookup[((crc>>8)^b) & 0xff] 1179 } 1180 crc 1181 end 1182 1183private 1184 XMODEMCRC16Lookup = [ 1185 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, 1186 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, 1187 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, 1188 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, 1189 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, 1190 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, 1191 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, 1192 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, 1193 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, 1194 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, 1195 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, 1196 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, 1197 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, 1198 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, 1199 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, 1200 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, 1201 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, 1202 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, 1203 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, 1204 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, 1205 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, 1206 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, 1207 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, 1208 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, 1209 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, 1210 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, 1211 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, 1212 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, 1213 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, 1214 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, 1215 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, 1216 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0 1217 ] 1218end 1219 1220# Turn a key name into the corrisponding Redis Cluster slot. 1221def key_to_slot(key) 1222 # Only hash what is inside {...} if there is such a pattern in the key. 1223 # Note that the specification requires the content that is between 1224 # the first { and the first } after the first {. If we found {} without 1225 # nothing in the middle, the whole key is hashed as usually. 1226 s = key.index "{" 1227 if s 1228 e = key.index "}",s+1 1229 if e && e != s+1 1230 key = key[s+1..e-1] 1231 end 1232 end 1233 RedisClusterCRC16.crc16(key) % 16384 1234end 1235 1236################################################################################# 1237# Definition of commands 1238################################################################################# 1239 1240COMMANDS={ 1241 "create" => ["create_cluster_cmd", -2, "host1:port1 ... hostN:portN"], 1242 "check" => ["check_cluster_cmd", 2, "host:port"], 1243 "fix" => ["fix_cluster_cmd", 2, "host:port"], 1244 "reshard" => ["reshard_cluster_cmd", 2, "host:port"], 1245 "add-node" => ["addnode_cluster_cmd", 3, "new_host:new_port existing_host:existing_port"], 1246 "del-node" => ["delnode_cluster_cmd", 3, "host:port node_id"], 1247 "set-timeout" => ["set_timeout_cluster_cmd", 3, "host:port milliseconds"], 1248 "call" => ["call_cluster_cmd", -3, "host:port command arg arg .. arg"], 1249 "import" => ["import_cluster_cmd", 2, "host:port"], 1250 "help" => ["help_cluster_cmd", 1, "(show this help)"] 1251} 1252 1253ALLOWED_OPTIONS={ 1254 "create" => {"replicas" => true}, 1255 "add-node" => {"slave" => false, "master-id" => true}, 1256 "import" => {"from" => :required} 1257} 1258 1259def show_help 1260 puts "Usage: redis-trib <command> <options> <arguments ...>\n\n" 1261 COMMANDS.each{|k,v| 1262 o = "" 1263 puts " #{k.ljust(15)} #{v[2]}" 1264 if ALLOWED_OPTIONS[k] 1265 ALLOWED_OPTIONS[k].each{|optname,has_arg| 1266 puts " --#{optname}" + (has_arg ? " <arg>" : "") 1267 } 1268 end 1269 } 1270 puts "\nFor check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.\n" 1271end 1272 1273# Sanity check 1274if ARGV.length == 0 1275 show_help 1276 exit 1 1277end 1278 1279rt = RedisTrib.new 1280cmd_spec = COMMANDS[ARGV[0].downcase] 1281if !cmd_spec 1282 puts "Unknown redis-trib subcommand '#{ARGV[0]}'" 1283 exit 1 1284end 1285 1286# Parse options 1287cmd_options,first_non_option = rt.parse_options(ARGV[0].downcase) 1288rt.check_arity(cmd_spec[1],ARGV.length-(first_non_option-1)) 1289 1290# Dispatch 1291rt.send(cmd_spec[0],ARGV[first_non_option..-1],cmd_options) 1292