ktl 1.0.0-java

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