elasticsearch-drain 0.0.6 → 0.1.4
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.
- 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 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59b1a35ac25472ad0caf43d4f30b5370bf59250f
|
4
|
+
data.tar.gz: 43dfb6c5d57c216ad4a947d86402c13ecd8148b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9844b71974cd6f7b838e7eb5a40de1f0d0440e0108c4dbe26dfa827329aeb95e7306624e22d2a2d5946bd9d3f8cd193bbb276e8a3bb9a5e4cffe60857a77223e
|
7
|
+
data.tar.gz: 3445323f27e4b549e27de00af09285cfec0ab9db3fa9a648c261a7b80d657a957d306008e754344432183359ca815f4938122e9ef2180eb7c5dc3b90367092e9
|
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) <= 0 ? 0 : (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.
|
4
|
+
version: 0.1.4
|
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
|