elasticsearch-drain 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +13 -10
- data/lib/elasticsearch/drain.rb +2 -2
- data/lib/elasticsearch/drain/autoscaling.rb +17 -10
- data/lib/elasticsearch/drain/cli.rb +98 -28
- data/lib/elasticsearch/drain/cluster.rb +5 -0
- data/lib/elasticsearch/drain/node.rb +1 -1
- metadata +2 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 087e3d64398481deae576f955f1b22d5cd0e5e36
|
4
|
+
data.tar.gz: cf6ad7098dc51e8881d63d739cf53cd46e137e9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c75c5d28d018e42f29e39f91a8cd8c94bf9596efe90baf742e72d39986c4fa1c6ed246c23de8277cefbda1713c1b224dbb4178b4c8c8bf227aee7781ab8d3586
|
7
|
+
data.tar.gz: 37c5eac72feea75743dfabd03858fdee00b9e033353e69e0cd5163a857931c8276a5472b314c81effeb6038ecbe3e9e499f76f5b9c3e62084f754471b758cab2
|
data/Gemfile.lock
CHANGED
@@ -14,25 +14,25 @@ GEM
|
|
14
14
|
ast (2.1.0)
|
15
15
|
astrolabe (1.3.1)
|
16
16
|
parser (~> 2.2)
|
17
|
-
aws-sdk (2.
|
18
|
-
aws-sdk-resources (= 2.
|
19
|
-
aws-sdk-core (2.
|
17
|
+
aws-sdk (2.6.14)
|
18
|
+
aws-sdk-resources (= 2.6.14)
|
19
|
+
aws-sdk-core (2.6.14)
|
20
20
|
jmespath (~> 1.0)
|
21
|
-
aws-sdk-resources (2.
|
22
|
-
aws-sdk-core (= 2.
|
21
|
+
aws-sdk-resources (2.6.14)
|
22
|
+
aws-sdk-core (= 2.6.14)
|
23
23
|
coderay (1.1.0)
|
24
24
|
crack (0.4.2)
|
25
25
|
safe_yaml (~> 1.0.0)
|
26
26
|
docile (1.1.5)
|
27
|
-
elasticsearch (1.0
|
28
|
-
elasticsearch-api (= 1.0
|
29
|
-
elasticsearch-transport (= 1.0
|
30
|
-
elasticsearch-api (1.0
|
27
|
+
elasticsearch (1.1.0)
|
28
|
+
elasticsearch-api (= 1.1.0)
|
29
|
+
elasticsearch-transport (= 1.1.0)
|
30
|
+
elasticsearch-api (1.1.0)
|
31
31
|
multi_json
|
32
32
|
elasticsearch-extensions (0.0.18)
|
33
33
|
ansi
|
34
34
|
ruby-prof
|
35
|
-
elasticsearch-transport (1.0
|
35
|
+
elasticsearch-transport (1.1.0)
|
36
36
|
faraday
|
37
37
|
multi_json
|
38
38
|
faraday (0.9.2)
|
@@ -127,3 +127,6 @@ DEPENDENCIES
|
|
127
127
|
vcr (~> 2.9)
|
128
128
|
webmock (~> 1.21)
|
129
129
|
yard (~> 0.8)
|
130
|
+
|
131
|
+
BUNDLED WITH
|
132
|
+
1.13.2
|
data/lib/elasticsearch/drain.rb
CHANGED
@@ -38,12 +38,12 @@ module Elasticsearch
|
|
38
38
|
@asg_client ||= AutoScaling.new(@asg_name, @region)
|
39
39
|
end
|
40
40
|
|
41
|
-
#
|
41
|
+
# Convenience method to access {Elasticsearch::Drain::Nodes}
|
42
42
|
def nodes
|
43
43
|
@nodes ||= Nodes.new(client, asg)
|
44
44
|
end
|
45
45
|
|
46
|
-
#
|
46
|
+
# Convenience method to access {Elasticsearch::Drain::Cluster#cluster}
|
47
47
|
#
|
48
48
|
# @return [Elasticsearch::API::Cluster] Elasticsearch cluster client
|
49
49
|
def cluster
|
@@ -8,20 +8,27 @@ module Elasticsearch
|
|
8
8
|
attr_reader :asg
|
9
9
|
|
10
10
|
# @attribute [r]
|
11
|
-
#
|
12
|
-
attr_reader :
|
11
|
+
# AWS region
|
12
|
+
attr_reader :region
|
13
13
|
|
14
14
|
def initialize(asg, region)
|
15
15
|
@asg = asg
|
16
|
-
@
|
17
|
-
@ec2_client = Aws::EC2::Client.new(region: region)
|
16
|
+
@region = region
|
18
17
|
@instances = nil
|
19
18
|
@instance_ids = nil
|
20
19
|
end
|
21
20
|
|
21
|
+
def asg_client
|
22
|
+
Aws::AutoScaling::Client.new(region: region)
|
23
|
+
end
|
24
|
+
|
25
|
+
def ec2_client
|
26
|
+
Aws::EC2::Client.new(region: region)
|
27
|
+
end
|
28
|
+
|
22
29
|
def find_instances_in_asg
|
23
30
|
instances = []
|
24
|
-
|
31
|
+
asg_client.describe_auto_scaling_instances.each do |page|
|
25
32
|
instances << page.auto_scaling_instances.map do |i|
|
26
33
|
i.instance_id if i.auto_scaling_group_name == asg
|
27
34
|
end
|
@@ -38,7 +45,7 @@ module Elasticsearch
|
|
38
45
|
instances = []
|
39
46
|
find_instances_in_asg if @instance_ids.nil?
|
40
47
|
return [] if @instance_ids.empty?
|
41
|
-
|
48
|
+
ec2_client.describe_instances(instance_ids: @instance_ids).each do |page|
|
42
49
|
instances << page.reservations.map(&:instances)
|
43
50
|
end
|
44
51
|
instances.flatten!
|
@@ -49,7 +56,7 @@ module Elasticsearch
|
|
49
56
|
#
|
50
57
|
# @return [Struct] AutoScaling Group
|
51
58
|
def describe_autoscaling_group
|
52
|
-
groups =
|
59
|
+
groups = asg_client.describe_auto_scaling_groups(
|
53
60
|
auto_scaling_group_names: [asg]
|
54
61
|
)
|
55
62
|
groups.auto_scaling_groups.first
|
@@ -81,11 +88,11 @@ module Elasticsearch
|
|
81
88
|
# @option [FixNum] count (0) The new MinSize of the AutoScalingGroup
|
82
89
|
# @return [Struct] Empty response from the sdk
|
83
90
|
def min_size=(count = 0)
|
84
|
-
|
91
|
+
asg_client.update_auto_scaling_group(
|
85
92
|
auto_scaling_group_name: asg,
|
86
93
|
min_size: count
|
87
94
|
)
|
88
|
-
wait_until(
|
95
|
+
wait_until(count) do
|
89
96
|
min_size
|
90
97
|
end
|
91
98
|
end
|
@@ -108,7 +115,7 @@ module Elasticsearch
|
|
108
115
|
|
109
116
|
def detach_instance(instance_id)
|
110
117
|
current_desired_capacity = desired_capacity
|
111
|
-
|
118
|
+
asg_client.detach_instances(
|
112
119
|
instance_ids: [instance_id],
|
113
120
|
auto_scaling_group_name: asg,
|
114
121
|
should_decrement_desired_capacity: true
|
@@ -2,35 +2,93 @@ require 'thor'
|
|
2
2
|
|
3
3
|
module Elasticsearch
|
4
4
|
class Drain
|
5
|
-
class CLI < ::Thor
|
5
|
+
class CLI < ::Thor # rubocop:disable Metrics/ClassLength
|
6
6
|
package_name :elasticsearch
|
7
7
|
|
8
8
|
attr_reader :drainer
|
9
9
|
attr_accessor :active_nodes
|
10
10
|
|
11
|
+
# rubocop:disable Metrics/LineLength
|
11
12
|
desc 'asg', 'Drain all documents from all nodes in an EC2 AutoScaling Group'
|
12
13
|
option :host, default: 'localhost:9200'
|
13
14
|
option :asg, required: true
|
14
15
|
option :region, required: true
|
15
|
-
option :nodes
|
16
|
+
option :nodes, type: :array, desc: 'A comma separated list of node IDs to drain. If specified, the --number option has no effect'
|
17
|
+
option :number, type: :numeric, desc: 'The number of nodes to drain'
|
18
|
+
option :continue, type: :boolean, default: true, desc: 'Whether to continue draining nodes once the first iteration of --number is complete'
|
19
|
+
# rubocop:enable Metrics/LineLength
|
16
20
|
def asg # rubocop:disable Metrics/MethodLength
|
17
21
|
@drainer = Elasticsearch::Drain.new(options[:host],
|
18
22
|
options[:asg],
|
19
23
|
options[:region])
|
24
|
+
|
20
25
|
ensure_cluster_healthy
|
21
26
|
@active_nodes = drainer.active_nodes_in_asg
|
22
27
|
|
28
|
+
# If :nodes are specified, :number has no effect
|
29
|
+
if options[:nodes]
|
30
|
+
say "Nodes #{options[:nodes].join(', ')} have been specified, the --number option has no effect"
|
31
|
+
number_to_drain = nil
|
32
|
+
currently_draining_nodes = nil
|
33
|
+
else
|
34
|
+
number_to_drain = options[:number]
|
35
|
+
currently_draining_nodes = drainer.cluster.currently_draining('_id')
|
36
|
+
end
|
37
|
+
|
23
38
|
# If a node or nodes are specified, only drain the requested node(s)
|
24
39
|
@active_nodes = active_nodes.find_all do |n|
|
25
40
|
instance_id = drainer.asg.instance(n.ipaddress).instance_id
|
26
|
-
options[:nodes].
|
41
|
+
options[:nodes].include?(instance_id)
|
27
42
|
end if options[:nodes]
|
28
43
|
|
29
44
|
do_exit { say_status 'Complete', 'Nothing to do', :green } if active_nodes.empty?
|
30
|
-
say_status 'Found Nodes', "AutoScalingGroup: #{instances}", :magenta
|
31
|
-
|
32
|
-
|
33
|
-
|
45
|
+
say_status 'Found Nodes', "AutoScalingGroup: #{instances(active_nodes)}", :magenta
|
46
|
+
|
47
|
+
until active_nodes.empty?
|
48
|
+
ensure_cluster_healthy
|
49
|
+
|
50
|
+
nodes = active_nodes
|
51
|
+
|
52
|
+
# If there are nodes in cluster settings "transient.cluster.routing.allocation.exclude"
|
53
|
+
# test if those nodes are still in the ASG. If so, work on them first unless nodes are
|
54
|
+
# specified.
|
55
|
+
if currently_draining_nodes
|
56
|
+
nodes_to_drain = active_nodes.find_all { |n| currently_draining_nodes.split(',').include?(n.id) }
|
57
|
+
|
58
|
+
# If the list of nodes_to_drain isn't empty, we want to set nodes to the list of nodes
|
59
|
+
# we've already been working on.
|
60
|
+
unless nodes_to_drain.empty?
|
61
|
+
nodes = nodes_to_drain
|
62
|
+
|
63
|
+
say_status 'Active Nodes', "Resuming drain process on #{instances(nodes)}", :magenta
|
64
|
+
end
|
65
|
+
|
66
|
+
# We should only process currently_draining_nodes once
|
67
|
+
currently_draining_nodes = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# If we specify a number but DON'T specify nodes, sample the active_nodes.
|
71
|
+
if number_to_drain
|
72
|
+
nodes = nodes.sample(number_to_drain.to_i)
|
73
|
+
say_status 'Active Nodes', "Sampled #{number_to_drain} nodes and got #{instances(nodes)}", :magenta
|
74
|
+
end
|
75
|
+
|
76
|
+
@active_nodes = nodes unless options[:continue]
|
77
|
+
|
78
|
+
drain_nodes(nodes)
|
79
|
+
remove_nodes(nodes)
|
80
|
+
|
81
|
+
# Remove the drained nodes from the list of active_nodes
|
82
|
+
@active_nodes -= nodes
|
83
|
+
|
84
|
+
unless active_nodes.empty?
|
85
|
+
say_status 'Drain Nodes', "#{active_nodes.length} nodes remaining", :green
|
86
|
+
|
87
|
+
sleep_time = wait_sleep_time
|
88
|
+
say_status 'Waiting', "Sleeping for #{sleep_time} seconds before the next iteration", :green
|
89
|
+
sleep sleep_time
|
90
|
+
end
|
91
|
+
end
|
34
92
|
say_status 'Complete', 'Draining nodes complete!', :green
|
35
93
|
end
|
36
94
|
|
@@ -48,42 +106,53 @@ module Elasticsearch
|
|
48
106
|
exit code
|
49
107
|
end
|
50
108
|
|
51
|
-
def instances
|
52
|
-
instances =
|
109
|
+
def instances(nodes)
|
110
|
+
instances = nodes.map(&:ipaddress)
|
53
111
|
instances.join(' ')
|
54
112
|
end
|
55
113
|
|
56
|
-
def adjusted_min_size
|
114
|
+
def adjusted_min_size(nodes)
|
57
115
|
min_size = drainer.asg.min_size
|
58
116
|
desired_capacity = drainer.asg.desired_capacity
|
59
|
-
if (desired_capacity -
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
117
|
+
desired_min_size = if (desired_capacity - nodes.length) >= min_size # Removing the nodes won't violate the min_size
|
118
|
+
# Reduce the asg min_size proportionally
|
119
|
+
(min_size - nodes.length) <= 0 ? 0 : (min_size - nodes.length)
|
120
|
+
else
|
121
|
+
# Removing the nodes will result in the min_size being violated
|
122
|
+
desired_capacity - nodes.length
|
123
|
+
end
|
66
124
|
desired_min_size
|
67
125
|
end
|
68
126
|
|
69
|
-
def drain_nodes
|
70
|
-
drainer.asg.min_size = adjusted_min_size
|
71
|
-
nodes_to_drain =
|
127
|
+
def drain_nodes(nodes)
|
128
|
+
drainer.asg.min_size = adjusted_min_size(nodes)
|
129
|
+
nodes_to_drain = nodes.map(&:id).join(',')
|
72
130
|
say_status 'Drain Nodes', "Draining nodes: #{nodes_to_drain}", :magenta
|
73
131
|
drainer.cluster.drain_nodes(nodes_to_drain, '_id')
|
74
132
|
end
|
75
133
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
79
|
-
|
134
|
+
def wait_sleep_time
|
135
|
+
ips = active_nodes.map(&:ipaddress)
|
136
|
+
bytes = drainer.nodes.filter_nodes(ips).map(&:bytes_stored)
|
137
|
+
sleep_time = 10
|
138
|
+
sleep_time = 30 if bytes.any? { |b| b >= 100_000 }
|
139
|
+
sleep_time = 60 if bytes.any? { |b| b >= 1_000_000 }
|
140
|
+
sleep_time = 120 if bytes.any? { |b| b >= 10_000_000_000 }
|
141
|
+
sleep_time
|
142
|
+
end
|
143
|
+
|
144
|
+
def remove_nodes(nodes) # rubocop:disable Metrics/MethodLength
|
145
|
+
while nodes.length > 0
|
146
|
+
sleep_time = wait_sleep_time
|
147
|
+
nodes.each do |instance|
|
148
|
+
instance = drainer.nodes.filter_nodes([instance.ipaddress], true).first
|
80
149
|
if instance.bytes_stored > 0
|
81
150
|
say_status 'Drain Status', "Node #{instance.ipaddress} has #{instance.bytes_stored} bytes to move", :blue
|
82
|
-
sleep
|
151
|
+
sleep sleep_time
|
83
152
|
else
|
84
153
|
next unless remove_node(instance)
|
85
|
-
|
86
|
-
break if
|
154
|
+
nodes.delete_if { |n| n.ipaddress == instance.ipaddress }
|
155
|
+
break if nodes.length < 1
|
87
156
|
say_status 'Waiting', 'Sleeping for 1 minute before removing the next node', :green
|
88
157
|
sleep 60
|
89
158
|
end
|
@@ -97,7 +166,8 @@ module Elasticsearch
|
|
97
166
|
say_status(
|
98
167
|
'Removing Node',
|
99
168
|
"Removing #{instance.ipaddress} from Elasticsearch cluster and #{drainer.asg.asg} AutoScalingGroup",
|
100
|
-
:magenta
|
169
|
+
:magenta
|
170
|
+
)
|
101
171
|
sleep 5 unless instance.in_recovery?
|
102
172
|
node = "#{instance.instance_id}(#{instance.ipaddress})"
|
103
173
|
ensure_cluster_healthy
|
@@ -34,6 +34,11 @@ module Elasticsearch
|
|
34
34
|
}
|
35
35
|
)
|
36
36
|
end
|
37
|
+
|
38
|
+
def currently_draining(exclude_by = '_ip')
|
39
|
+
settings = cluster.get_settings(:flat_settings => true)
|
40
|
+
settings.fetch('transient', {}).fetch("cluster.routing.allocation.exclude.#{exclude_by}", nil)
|
41
|
+
end
|
37
42
|
end
|
38
43
|
end
|
39
44
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elasticsearch-drain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Thompson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -158,4 +158,3 @@ test_files:
|
|
158
158
|
- test/elasticsearch/drain/test_nodes.rb
|
159
159
|
- test/elasticsearch/test_drain.rb
|
160
160
|
- test/test_helper.rb
|
161
|
-
has_rdoc:
|