elasticsearch-drain 0.0.5 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +15 -12
- data/lib/elasticsearch/drain.rb +4 -4
- data/lib/elasticsearch/drain/autoscaling.rb +17 -10
- data/lib/elasticsearch/drain/cli.rb +110 -18
- data/lib/elasticsearch/drain/cluster.rb +5 -0
- data/lib/elasticsearch/drain/node.rb +1 -1
- data/lib/elasticsearch/drain/nodes.rb +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f9122250f3be77882bb0ce4c14aa3de03eba351
|
4
|
+
data.tar.gz: ea15747daa99591d805e3582366e149e37e0f673
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 017452105174c3c2c12a073b70580f61ada317653273a8951f89f310b8c51dccacf456b54dfe6554610879f6f5721995e58db26b69daa23fcff08eeb92bced30
|
7
|
+
data.tar.gz: 09e9cae06b18aa446a527e2612796af13b17d65549125a35e61717b13890e7b26ae76cc6a1f0cef7180cae50cd6b365ce5be625294ea2f0dd5f3dc11d6b918f6
|
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)
|
@@ -53,7 +53,7 @@ GEM
|
|
53
53
|
guard-compat (~> 1.2)
|
54
54
|
minitest (>= 3.0)
|
55
55
|
hashdiff (0.2.2)
|
56
|
-
jmespath (1.1
|
56
|
+
jmespath (1.3.1)
|
57
57
|
json (1.8.3)
|
58
58
|
listen (3.0.3)
|
59
59
|
rb-fsevent (>= 0.9.3)
|
@@ -62,7 +62,7 @@ GEM
|
|
62
62
|
method_source (0.8.2)
|
63
63
|
minitest (5.8.1)
|
64
64
|
mixlib-shellout (2.2.3)
|
65
|
-
multi_json (1.
|
65
|
+
multi_json (1.12.1)
|
66
66
|
multipart-post (2.0.0)
|
67
67
|
nenv (0.2.0)
|
68
68
|
notiffany (0.0.8)
|
@@ -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
|
-
Nodes.new(client, asg)
|
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
|
@@ -52,7 +52,7 @@ module Elasticsearch
|
|
52
52
|
|
53
53
|
def active_nodes_in_asg
|
54
54
|
instances = asg.instances
|
55
|
-
nodes.
|
55
|
+
nodes.filter_nodes(instances, true)
|
56
56
|
end
|
57
57
|
|
58
58
|
module Errors
|
@@ -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,27 +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
|
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
|
15
20
|
def asg # rubocop:disable Metrics/MethodLength
|
16
21
|
@drainer = Elasticsearch::Drain.new(options[:host],
|
17
22
|
options[:asg],
|
18
23
|
options[:region])
|
24
|
+
|
19
25
|
ensure_cluster_healthy
|
20
26
|
@active_nodes = drainer.active_nodes_in_asg
|
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
|
+
|
38
|
+
# If a node or nodes are specified, only drain the requested node(s)
|
39
|
+
@active_nodes = active_nodes.find_all do |n|
|
40
|
+
instance_id = drainer.asg.instance(n.ipaddress).instance_id
|
41
|
+
options[:nodes].include?(instance_id)
|
42
|
+
end if options[:nodes]
|
43
|
+
|
21
44
|
do_exit { say_status 'Complete', 'Nothing to do', :green } if active_nodes.empty?
|
22
|
-
say_status 'Found Nodes', "AutoScalingGroup: #{instances}", :magenta
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
26
92
|
say_status 'Complete', 'Draining nodes complete!', :green
|
27
93
|
end
|
28
94
|
|
@@ -40,29 +106,54 @@ module Elasticsearch
|
|
40
106
|
exit code
|
41
107
|
end
|
42
108
|
|
43
|
-
def instances
|
44
|
-
instances =
|
109
|
+
def instances(nodes)
|
110
|
+
instances = nodes.map(&:ipaddress)
|
45
111
|
instances.join(' ')
|
46
112
|
end
|
47
113
|
|
48
|
-
def
|
49
|
-
drainer.asg.min_size
|
50
|
-
|
114
|
+
def adjusted_min_size(nodes)
|
115
|
+
min_size = drainer.asg.min_size
|
116
|
+
desired_capacity = drainer.asg.desired_capacity
|
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
|
124
|
+
say_status 'Debug', "min_size = #{min_size}, desired_capacity = #{desired_capacity}, desired_min_size = #{desired_min_size}", :magenta
|
125
|
+
desired_min_size
|
126
|
+
end
|
127
|
+
|
128
|
+
def drain_nodes(nodes)
|
129
|
+
drainer.asg.min_size = adjusted_min_size(nodes)
|
130
|
+
nodes_to_drain = nodes.map(&:id).join(',')
|
51
131
|
say_status 'Drain Nodes', "Draining nodes: #{nodes_to_drain}", :magenta
|
52
132
|
drainer.cluster.drain_nodes(nodes_to_drain, '_id')
|
53
133
|
end
|
54
134
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
135
|
+
def wait_sleep_time
|
136
|
+
ips = active_nodes.map(&:ipaddress)
|
137
|
+
bytes = drainer.nodes.filter_nodes(ips).map(&:bytes_stored)
|
138
|
+
sleep_time = 10
|
139
|
+
sleep_time = 30 if bytes.any? { |b| b >= 100_000 }
|
140
|
+
sleep_time = 60 if bytes.any? { |b| b >= 1_000_000 }
|
141
|
+
sleep_time = 120 if bytes.any? { |b| b >= 10_000_000_000 }
|
142
|
+
sleep_time
|
143
|
+
end
|
144
|
+
|
145
|
+
def remove_nodes(nodes) # rubocop:disable Metrics/MethodLength
|
146
|
+
while nodes.length > 0
|
147
|
+
sleep_time = wait_sleep_time
|
148
|
+
nodes.each do |instance|
|
149
|
+
instance = drainer.nodes.filter_nodes([instance.ipaddress], true).first
|
59
150
|
if instance.bytes_stored > 0
|
60
151
|
say_status 'Drain Status', "Node #{instance.ipaddress} has #{instance.bytes_stored} bytes to move", :blue
|
61
|
-
sleep
|
152
|
+
sleep sleep_time
|
62
153
|
else
|
63
154
|
next unless remove_node(instance)
|
64
|
-
|
65
|
-
break if
|
155
|
+
nodes.delete_if { |n| n.ipaddress == instance.ipaddress }
|
156
|
+
break if nodes.length < 1
|
66
157
|
say_status 'Waiting', 'Sleeping for 1 minute before removing the next node', :green
|
67
158
|
sleep 60
|
68
159
|
end
|
@@ -76,7 +167,8 @@ module Elasticsearch
|
|
76
167
|
say_status(
|
77
168
|
'Removing Node',
|
78
169
|
"Removing #{instance.ipaddress} from Elasticsearch cluster and #{drainer.asg.asg} AutoScalingGroup",
|
79
|
-
:magenta
|
170
|
+
:magenta
|
171
|
+
)
|
80
172
|
sleep 5 unless instance.in_recovery?
|
81
173
|
node = "#{instance.instance_id}(#{instance.ipaddress})"
|
82
174
|
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
|
@@ -26,7 +26,7 @@ module Elasticsearch
|
|
26
26
|
# Get list of nodes in the cluster
|
27
27
|
#
|
28
28
|
# @return [Array<OpenStruct>] Array of node objects
|
29
|
-
def nodes(reload
|
29
|
+
def nodes(reload = false)
|
30
30
|
load if reload
|
31
31
|
@info['nodes'].map do |node|
|
32
32
|
Drain::Node.new(
|
@@ -38,8 +38,8 @@ module Elasticsearch
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
def
|
42
|
-
nodes(reload
|
41
|
+
def filter_nodes(instances, reload = false)
|
42
|
+
nodes(reload).find_all { |n| instances.include? n.ipaddress }
|
43
43
|
end
|
44
44
|
end
|
45
45
|
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.
|
4
|
+
version: 0.1.3
|
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: 2020-08-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|