ktl 1.0.0-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|