ktl 1.0.0-java
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 +7 -0
- data/LICENSE.txt +12 -0
- data/README.md +106 -0
- data/bin/ktl +8 -0
- data/lib/ext/kafka.rb +145 -0
- data/lib/ext/thor.rb +14 -0
- data/lib/ktl.rb +51 -0
- data/lib/ktl/cli.rb +11 -0
- data/lib/ktl/cluster.rb +108 -0
- data/lib/ktl/cluster_stats_task.rb +41 -0
- data/lib/ktl/command.rb +33 -0
- data/lib/ktl/decommission_plan.rb +68 -0
- data/lib/ktl/migration_plan.rb +27 -0
- data/lib/ktl/reassigner.rb +133 -0
- data/lib/ktl/reassignment_progress.rb +57 -0
- data/lib/ktl/reassignment_task.rb +46 -0
- data/lib/ktl/shell_formatter.rb +32 -0
- data/lib/ktl/shuffle_plan.rb +145 -0
- data/lib/ktl/topic.rb +123 -0
- data/lib/ktl/version.rb +5 -0
- data/lib/ktl/zookeeper_client.rb +111 -0
- metadata +101 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class ClusterStatsTask
|
5
|
+
def initialize(zk_client, shell)
|
6
|
+
@zk_client = zk_client
|
7
|
+
@shell = shell
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute
|
11
|
+
brokers = @zk_client.brokers
|
12
|
+
partitions = @zk_client.all_partitions
|
13
|
+
topics = extract_topics(partitions)
|
14
|
+
leaders = @zk_client.leader_and_isr_for(partitions)
|
15
|
+
ownership = broker_ownership(leaders)
|
16
|
+
@shell.say 'Cluster status:'
|
17
|
+
@shell.say ' topics: %d (%d partitions)' % [topics.size, partitions.size]
|
18
|
+
@shell.say ' brokers: %d' % [brokers.size]
|
19
|
+
brokers.foreach do |broker|
|
20
|
+
leader_for = ownership[broker.id]
|
21
|
+
share = leader_for.fdiv(partitions.size.to_f) * 100
|
22
|
+
@shell.say ' - %d leader for %d partitions (%.2f %%)' % [broker.id, leader_for, share]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def extract_topics(partitions)
|
29
|
+
partitions.map(proc { |tp| tp.topic }, CanBuildFrom).to_seq
|
30
|
+
end
|
31
|
+
|
32
|
+
def broker_ownership(leaders)
|
33
|
+
result = Hash.new(0)
|
34
|
+
leaders.foreach do |item|
|
35
|
+
leader = item.last.leader_and_isr.leader
|
36
|
+
result[leader] += 1
|
37
|
+
end
|
38
|
+
result
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/ktl/command.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class Command < Thor
|
5
|
+
|
6
|
+
java_import 'java.io.ByteArrayOutputStream'
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def with_zk_client
|
11
|
+
zk_client = ZookeeperClient.new(options.zookeeper).setup
|
12
|
+
yield zk_client
|
13
|
+
rescue => e
|
14
|
+
logger.error '%s (%s)' % [e.message, e.class.name]
|
15
|
+
logger.debug e.backtrace.join($/)
|
16
|
+
$stderr.puts '%s (%s)' % [e.message, e.class.name]
|
17
|
+
$stderr.puts e.backtrace.join($/)
|
18
|
+
ensure
|
19
|
+
zk_client.close if zk_client
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
24
|
+
log.formatter = ShellFormater.new(shell)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def silence_scala(&block)
|
29
|
+
baos = ByteArrayOutputStream.new
|
30
|
+
Scala::Console.with_out(baos) { block.call }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class DecommissionPlan
|
5
|
+
def initialize(zk_client, broker_id)
|
6
|
+
@zk_client = zk_client
|
7
|
+
@broker_id = broker_id
|
8
|
+
@replicas_count = Hash.new(0)
|
9
|
+
@leaders_count = Hash.new(0)
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
plan = Scala::Collection::Map.empty
|
14
|
+
brokers = @zk_client.broker_ids
|
15
|
+
brokers = brokers - @broker_id
|
16
|
+
partitions = @zk_client.all_partitions
|
17
|
+
topics = topics_from(partitions)
|
18
|
+
assignments = @zk_client.replica_assignment_for_topics(topics)
|
19
|
+
count_leaders_and_replicas(assignments)
|
20
|
+
partitions = ScalaEnumerable.new(partitions).sort_by { |tp| tp.topic + tp.partition.to_s }
|
21
|
+
partitions.each do |tp|
|
22
|
+
replicas = assignments[tp]
|
23
|
+
if replicas.contains?(@broker_id)
|
24
|
+
if brokers.size >= replicas.size
|
25
|
+
brokers_diff = ScalaEnumerable.new(brokers.diff(replicas)).sort
|
26
|
+
broker_index = replicas.index_of(@broker_id)
|
27
|
+
new_broker = elect_new_broker(broker_index, brokers_diff)
|
28
|
+
new_replicas = replicas.updated(broker_index, new_broker, CanBuildFrom)
|
29
|
+
plan += Scala::Tuple.new(tp, new_replicas)
|
30
|
+
else
|
31
|
+
raise InsufficientBrokersRemainingError, %(#{brokers.size} remaining brokers, #{replicas.size} replicas needed)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
plan
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def elect_new_broker(broker_index, diff)
|
41
|
+
if broker_index.zero?
|
42
|
+
new_broker = diff.min_by { |broker| @leaders_count[broker] }
|
43
|
+
@leaders_count[new_broker] += 1
|
44
|
+
else
|
45
|
+
new_broker = diff.min_by { |broker| @replicas_count[broker] }
|
46
|
+
@replicas_count[new_broker] += 1
|
47
|
+
end
|
48
|
+
new_broker
|
49
|
+
end
|
50
|
+
|
51
|
+
def count_leaders_and_replicas(assignments)
|
52
|
+
assignments.foreach do |assignment|
|
53
|
+
replicas = ScalaEnumerable.new(assignment.last)
|
54
|
+
replicas.each_with_index do |broker, index|
|
55
|
+
if index.zero?
|
56
|
+
@leaders_count[broker] += 1
|
57
|
+
else
|
58
|
+
@replicas_count[broker] += 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def topics_from(partitions)
|
65
|
+
partitions.map(proc { |tp| tp.topic }, CanBuildFrom).to_seq
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class MigrationPlan
|
5
|
+
def initialize(zk_client, old_leader, new_leader)
|
6
|
+
@zk_client = zk_client
|
7
|
+
@old_leader = old_leader.to_java
|
8
|
+
@new_leader = new_leader.to_java
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate
|
12
|
+
plan = Scala::Collection::Map.empty
|
13
|
+
topics = @zk_client.all_topics
|
14
|
+
assignments = ScalaEnumerable.new(@zk_client.replica_assignment_for_topics(topics))
|
15
|
+
assignments.each do |item|
|
16
|
+
topic_partition = item.first
|
17
|
+
replicas = item.last
|
18
|
+
if replicas.contains?(@old_leader)
|
19
|
+
index = replicas.index_of(@old_leader)
|
20
|
+
new_replicas = replicas.updated(index, @new_leader, CanBuildFrom)
|
21
|
+
plan += Scala::Tuple.new(topic_partition, new_replicas)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
plan
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class Reassigner
|
5
|
+
def initialize(zk_client, options={})
|
6
|
+
@zk_client = zk_client
|
7
|
+
@limit = options[:limit]
|
8
|
+
@overflow_path = '/ktl/overflow'
|
9
|
+
@state_path = '/ktl/reassign'
|
10
|
+
@logger = options[:logger] || NullLogger.new
|
11
|
+
@log_assignments = !!options[:log_assignments]
|
12
|
+
end
|
13
|
+
|
14
|
+
def reassignment_in_progress?
|
15
|
+
partitions = @zk_client.partitions_being_reassigned
|
16
|
+
partitions.size > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def overflow?
|
20
|
+
overflow_znodes = @zk_client.get_children(@overflow_path)
|
21
|
+
overflow_znodes.size > 0
|
22
|
+
rescue ZkClient::Exception::ZkNoNodeException
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_overflow
|
27
|
+
overflow = Scala::Collection::Map.empty
|
28
|
+
overflow_nodes = @zk_client.get_children(@overflow_path)
|
29
|
+
overflow_nodes.foreach do |index|
|
30
|
+
overflow_json = @zk_client.read_data(overflow_path(index)).first
|
31
|
+
data = parse_reassignment_json(overflow_json)
|
32
|
+
overflow = overflow.send('++', data)
|
33
|
+
end
|
34
|
+
delete_previous_overflow
|
35
|
+
overflow
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute(reassignment)
|
39
|
+
reassignments = split(reassignment, @limit)
|
40
|
+
actual_reassignment = reassignments.shift
|
41
|
+
if @log_assignments
|
42
|
+
Scala::Collection::JavaConversions.as_java_iterable(actual_reassignment).each do |pr|
|
43
|
+
topic_and_partition, replicas = pr.elements
|
44
|
+
brokers = Scala::Collection::JavaConversions.as_java_iterable(replicas).to_a
|
45
|
+
@logger.info "Assigning #{topic_and_partition.topic},#{topic_and_partition.partition} to #{brokers.join(',')}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
json = reassignment_json(actual_reassignment)
|
49
|
+
@zk_client.reassign_partitions(json)
|
50
|
+
manage_overflow(reassignments)
|
51
|
+
manage_progress_state(actual_reassignment)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
JSON_MAX_SIZE = 1024**2
|
57
|
+
|
58
|
+
def manage_progress_state(reassignment)
|
59
|
+
delete_previous_state
|
60
|
+
json = reassignment_json(reassignment)
|
61
|
+
@zk_client.create_znode(@state_path, json)
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete_previous_state
|
65
|
+
if @zk_client.exists?(@state_path)
|
66
|
+
@zk_client.delete_znode(@state_path)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete_previous_overflow
|
71
|
+
overflow = @zk_client.get_children(@overflow_path)
|
72
|
+
overflow.foreach do |index|
|
73
|
+
@zk_client.delete_znode(overflow_path(index))
|
74
|
+
end
|
75
|
+
rescue ZkClient::Exception::ZkNoNodeException
|
76
|
+
end
|
77
|
+
|
78
|
+
def manage_overflow(reassignments)
|
79
|
+
delete_previous_overflow
|
80
|
+
empty_map = Scala::Collection::Map.empty
|
81
|
+
overflow = reassignments.reduce(empty_map) do |acc, data|
|
82
|
+
acc.send('++', data)
|
83
|
+
end
|
84
|
+
if overflow.size > 0
|
85
|
+
write_overflow(split(overflow))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def overflow_path(index)
|
90
|
+
[@overflow_path, index].join('/')
|
91
|
+
end
|
92
|
+
|
93
|
+
def write_overflow(reassignments)
|
94
|
+
reassignments.each_with_index do |reassignment, index|
|
95
|
+
overflow_json = reassignment_json(reassignment)
|
96
|
+
@zk_client.create_znode(overflow_path(index), overflow_json)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def reassignment_json(reassignment)
|
101
|
+
zk_utils.format_as_reassignment_json(reassignment)
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_reassignment_json(json)
|
105
|
+
zk_utils.parse_partition_reassignment_data(json)
|
106
|
+
end
|
107
|
+
|
108
|
+
def zk_utils
|
109
|
+
@zk_utils ||= Kafka::Utils::ZkUtils.new(nil, nil, false)
|
110
|
+
end
|
111
|
+
|
112
|
+
def maybe_split_by_limit(reassignment, limit=nil)
|
113
|
+
if limit
|
114
|
+
splitted = ScalaEnumerable.new(reassignment.grouped(limit)).map(&:seq)
|
115
|
+
else
|
116
|
+
splitted = [reassignment]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def split(reassignment, limit=nil)
|
121
|
+
splitted = maybe_split_by_limit(reassignment, limit)
|
122
|
+
bytesize = reassignment_json(splitted.first).bytesize
|
123
|
+
while bytesize > JSON_MAX_SIZE do
|
124
|
+
splitted = splitted.flat_map do |s|
|
125
|
+
group_size = s.size.fdiv(2).round
|
126
|
+
ScalaEnumerable.new(s.grouped(group_size)).map(&:seq)
|
127
|
+
end
|
128
|
+
bytesize = reassignment_json(splitted.first).bytesize
|
129
|
+
end
|
130
|
+
splitted
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class ReassignmentProgress
|
5
|
+
def initialize(zk_client, options={})
|
6
|
+
@zk_client = zk_client
|
7
|
+
@utils = options[:utils] || Kafka::Utils::ZkUtils
|
8
|
+
@logger = options[:logger] || NullLogger.new
|
9
|
+
@state_path = '/ktl/reassign'
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def display(shell)
|
14
|
+
in_progress = reassignment_in_progress
|
15
|
+
original = original_reassignment
|
16
|
+
if in_progress && !in_progress.empty?
|
17
|
+
original_size, remaining_size = original.size, in_progress.size
|
18
|
+
done_percentage = (original_size - remaining_size).fdiv(original_size) * 100
|
19
|
+
@logger.info 'remaining partitions to reassign: %d (%.2f%% done)' % [remaining_size, done_percentage]
|
20
|
+
if @options[:verbose]
|
21
|
+
shell.print_table(table_data(in_progress), indent: 2 + 6)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
@logger.info 'no partitions remaining to reassign'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def table_data(reassignments)
|
31
|
+
topics = reassignments.group_by { |r| r['topic'] }
|
32
|
+
table = topics.map do |t, r|
|
33
|
+
reassignments = r.sort_by { |r| r['partition'] }
|
34
|
+
reassignments = reassignments.map { |r| '%d => %s' % [r['partition'], r['replicas'].inspect] }.join(', ')
|
35
|
+
[t, reassignments]
|
36
|
+
end.sort_by(&:first)
|
37
|
+
table.unshift(%w[topic assignments])
|
38
|
+
table
|
39
|
+
end
|
40
|
+
|
41
|
+
def reassignment_in_progress
|
42
|
+
read_json(@utils.reassign_partitions_path).fetch('partitions')
|
43
|
+
rescue ZkClient::Exception::ZkNoNodeException
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
|
47
|
+
def original_reassignment
|
48
|
+
read_json(@state_path).fetch('partitions')
|
49
|
+
rescue ZkClient::Exception::ZkNoNodeException
|
50
|
+
{}
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_json(path)
|
54
|
+
JSON.parse(@zk_client.read_data(path).first)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class ReassignmentTask
|
5
|
+
def initialize(reassigner, plan, shell, options={})
|
6
|
+
@reassigner = reassigner
|
7
|
+
@plan = plan
|
8
|
+
@shell = shell
|
9
|
+
@logger = options[:logger] || NullLogger.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(dryrun = false)
|
13
|
+
if @reassigner.reassignment_in_progress?
|
14
|
+
@logger.warn 'reassignment already in progress, exiting'
|
15
|
+
else
|
16
|
+
if use_overflow?
|
17
|
+
@logger.info 'loading overflow data'
|
18
|
+
reassignment = @reassigner.load_overflow
|
19
|
+
else
|
20
|
+
@logger.info 'generating a new reassignment plan'
|
21
|
+
reassignment = @plan.generate
|
22
|
+
end
|
23
|
+
if reassignment.size > 0
|
24
|
+
@logger.info 'reassigning %d partitions' % reassignment.size
|
25
|
+
if dryrun
|
26
|
+
@logger.info 'dryrun detected, skipping reassignment'
|
27
|
+
else
|
28
|
+
@reassigner.execute(reassignment)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
@logger.warn 'empty reassignment, ignoring'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def use_overflow?
|
39
|
+
if @reassigner.overflow?
|
40
|
+
@logger.info 'overflow from previous reassignment found, use? [y/n]'
|
41
|
+
@shell.yes? ' ' * 8 << '>'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ktl
|
4
|
+
class ShellFormater < Logger::Formatter
|
5
|
+
def initialize(shell)
|
6
|
+
@shell = shell
|
7
|
+
@padding = ' ' * 2
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(severity, time, progname, msg)
|
11
|
+
severity_part = sprintf('%-5s', severity.downcase)
|
12
|
+
severity_part = @shell.set_color(severity_part, color_for[severity])
|
13
|
+
line = %(#{@padding}#{severity_part} #{msg2str(msg)})
|
14
|
+
line << NEWLINE unless line.end_with?(NEWLINE)
|
15
|
+
line
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
NEWLINE = "\n".freeze
|
21
|
+
|
22
|
+
def color_for
|
23
|
+
@color_for ||= {
|
24
|
+
'DEBUG' => :cyan,
|
25
|
+
'INFO' => :magenta,
|
26
|
+
'WARN' => :yellow,
|
27
|
+
'ERROR' => :red,
|
28
|
+
'FATAL' => :red,
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|