m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

517 lines
22 KiB

6 months ago
  1. from __future__ import absolute_import, division
  2. import collections
  3. import copy
  4. import logging
  5. import threading
  6. import time
  7. from kafka.vendor import six
  8. from kafka import errors as Errors
  9. from kafka.metrics.measurable import AnonMeasurable
  10. from kafka.metrics.stats import Avg, Max, Rate
  11. from kafka.protocol.produce import ProduceRequest
  12. from kafka.structs import TopicPartition
  13. from kafka.version import __version__
  14. log = logging.getLogger(__name__)
  15. class Sender(threading.Thread):
  16. """
  17. The background thread that handles the sending of produce requests to the
  18. Kafka cluster. This thread makes metadata requests to renew its view of the
  19. cluster and then sends produce requests to the appropriate nodes.
  20. """
  21. DEFAULT_CONFIG = {
  22. 'max_request_size': 1048576,
  23. 'acks': 1,
  24. 'retries': 0,
  25. 'request_timeout_ms': 30000,
  26. 'guarantee_message_order': False,
  27. 'client_id': 'kafka-python-' + __version__,
  28. 'api_version': (0, 8, 0),
  29. }
  30. def __init__(self, client, metadata, accumulator, metrics, **configs):
  31. super(Sender, self).__init__()
  32. self.config = copy.copy(self.DEFAULT_CONFIG)
  33. for key in self.config:
  34. if key in configs:
  35. self.config[key] = configs.pop(key)
  36. self.name = self.config['client_id'] + '-network-thread'
  37. self._client = client
  38. self._accumulator = accumulator
  39. self._metadata = client.cluster
  40. self._running = True
  41. self._force_close = False
  42. self._topics_to_add = set()
  43. self._sensors = SenderMetrics(metrics, self._client, self._metadata)
  44. def run(self):
  45. """The main run loop for the sender thread."""
  46. log.debug("Starting Kafka producer I/O thread.")
  47. # main loop, runs until close is called
  48. while self._running:
  49. try:
  50. self.run_once()
  51. except Exception:
  52. log.exception("Uncaught error in kafka producer I/O thread")
  53. log.debug("Beginning shutdown of Kafka producer I/O thread, sending"
  54. " remaining records.")
  55. # okay we stopped accepting requests but there may still be
  56. # requests in the accumulator or waiting for acknowledgment,
  57. # wait until these are completed.
  58. while (not self._force_close
  59. and (self._accumulator.has_unsent()
  60. or self._client.in_flight_request_count() > 0)):
  61. try:
  62. self.run_once()
  63. except Exception:
  64. log.exception("Uncaught error in kafka producer I/O thread")
  65. if self._force_close:
  66. # We need to fail all the incomplete batches and wake up the
  67. # threads waiting on the futures.
  68. self._accumulator.abort_incomplete_batches()
  69. try:
  70. self._client.close()
  71. except Exception:
  72. log.exception("Failed to close network client")
  73. log.debug("Shutdown of Kafka producer I/O thread has completed.")
  74. def run_once(self):
  75. """Run a single iteration of sending."""
  76. while self._topics_to_add:
  77. self._client.add_topic(self._topics_to_add.pop())
  78. # get the list of partitions with data ready to send
  79. result = self._accumulator.ready(self._metadata)
  80. ready_nodes, next_ready_check_delay, unknown_leaders_exist = result
  81. # if there are any partitions whose leaders are not known yet, force
  82. # metadata update
  83. if unknown_leaders_exist:
  84. log.debug('Unknown leaders exist, requesting metadata update')
  85. self._metadata.request_update()
  86. # remove any nodes we aren't ready to send to
  87. not_ready_timeout = float('inf')
  88. for node in list(ready_nodes):
  89. if not self._client.is_ready(node):
  90. log.debug('Node %s not ready; delaying produce of accumulated batch', node)
  91. self._client.maybe_connect(node, wakeup=False)
  92. ready_nodes.remove(node)
  93. not_ready_timeout = min(not_ready_timeout,
  94. self._client.connection_delay(node))
  95. # create produce requests
  96. batches_by_node = self._accumulator.drain(
  97. self._metadata, ready_nodes, self.config['max_request_size'])
  98. if self.config['guarantee_message_order']:
  99. # Mute all the partitions drained
  100. for batch_list in six.itervalues(batches_by_node):
  101. for batch in batch_list:
  102. self._accumulator.muted.add(batch.topic_partition)
  103. expired_batches = self._accumulator.abort_expired_batches(
  104. self.config['request_timeout_ms'], self._metadata)
  105. for expired_batch in expired_batches:
  106. self._sensors.record_errors(expired_batch.topic_partition.topic, expired_batch.record_count)
  107. self._sensors.update_produce_request_metrics(batches_by_node)
  108. requests = self._create_produce_requests(batches_by_node)
  109. # If we have any nodes that are ready to send + have sendable data,
  110. # poll with 0 timeout so this can immediately loop and try sending more
  111. # data. Otherwise, the timeout is determined by nodes that have
  112. # partitions with data that isn't yet sendable (e.g. lingering, backing
  113. # off). Note that this specifically does not include nodes with
  114. # sendable data that aren't ready to send since they would cause busy
  115. # looping.
  116. poll_timeout_ms = min(next_ready_check_delay * 1000, not_ready_timeout)
  117. if ready_nodes:
  118. log.debug("Nodes with data ready to send: %s", ready_nodes) # trace
  119. log.debug("Created %d produce requests: %s", len(requests), requests) # trace
  120. poll_timeout_ms = 0
  121. for node_id, request in six.iteritems(requests):
  122. batches = batches_by_node[node_id]
  123. log.debug('Sending Produce Request: %r', request)
  124. (self._client.send(node_id, request, wakeup=False)
  125. .add_callback(
  126. self._handle_produce_response, node_id, time.time(), batches)
  127. .add_errback(
  128. self._failed_produce, batches, node_id))
  129. # if some partitions are already ready to be sent, the select time
  130. # would be 0; otherwise if some partition already has some data
  131. # accumulated but not ready yet, the select time will be the time
  132. # difference between now and its linger expiry time; otherwise the
  133. # select time will be the time difference between now and the
  134. # metadata expiry time
  135. self._client.poll(timeout_ms=poll_timeout_ms)
  136. def initiate_close(self):
  137. """Start closing the sender (won't complete until all data is sent)."""
  138. self._running = False
  139. self._accumulator.close()
  140. self.wakeup()
  141. def force_close(self):
  142. """Closes the sender without sending out any pending messages."""
  143. self._force_close = True
  144. self.initiate_close()
  145. def add_topic(self, topic):
  146. # This is generally called from a separate thread
  147. # so this needs to be a thread-safe operation
  148. # we assume that checking set membership across threads
  149. # is ok where self._client._topics should never
  150. # remove topics for a producer instance, only add them.
  151. if topic not in self._client._topics:
  152. self._topics_to_add.add(topic)
  153. self.wakeup()
  154. def _failed_produce(self, batches, node_id, error):
  155. log.debug("Error sending produce request to node %d: %s", node_id, error) # trace
  156. for batch in batches:
  157. self._complete_batch(batch, error, -1, None)
  158. def _handle_produce_response(self, node_id, send_time, batches, response):
  159. """Handle a produce response."""
  160. # if we have a response, parse it
  161. log.debug('Parsing produce response: %r', response)
  162. if response:
  163. batches_by_partition = dict([(batch.topic_partition, batch)
  164. for batch in batches])
  165. for topic, partitions in response.topics:
  166. for partition_info in partitions:
  167. global_error = None
  168. log_start_offset = None
  169. if response.API_VERSION < 2:
  170. partition, error_code, offset = partition_info
  171. ts = None
  172. elif 2 <= response.API_VERSION <= 4:
  173. partition, error_code, offset, ts = partition_info
  174. elif 5 <= response.API_VERSION <= 7:
  175. partition, error_code, offset, ts, log_start_offset = partition_info
  176. else:
  177. # the ignored parameter is record_error of type list[(batch_index: int, error_message: str)]
  178. partition, error_code, offset, ts, log_start_offset, _, global_error = partition_info
  179. tp = TopicPartition(topic, partition)
  180. error = Errors.for_code(error_code)
  181. batch = batches_by_partition[tp]
  182. self._complete_batch(batch, error, offset, ts, log_start_offset, global_error)
  183. if response.API_VERSION > 0:
  184. self._sensors.record_throttle_time(response.throttle_time_ms, node=node_id)
  185. else:
  186. # this is the acks = 0 case, just complete all requests
  187. for batch in batches:
  188. self._complete_batch(batch, None, -1, None)
  189. def _complete_batch(self, batch, error, base_offset, timestamp_ms=None, log_start_offset=None, global_error=None):
  190. """Complete or retry the given batch of records.
  191. Arguments:
  192. batch (RecordBatch): The record batch
  193. error (Exception): The error (or None if none)
  194. base_offset (int): The base offset assigned to the records if successful
  195. timestamp_ms (int, optional): The timestamp returned by the broker for this batch
  196. log_start_offset (int): The start offset of the log at the time this produce response was created
  197. global_error (str): The summarising error message
  198. """
  199. # Standardize no-error to None
  200. if error is Errors.NoError:
  201. error = None
  202. if error is not None and self._can_retry(batch, error):
  203. # retry
  204. log.warning("Got error produce response on topic-partition %s,"
  205. " retrying (%d attempts left). Error: %s",
  206. batch.topic_partition,
  207. self.config['retries'] - batch.attempts - 1,
  208. global_error or error)
  209. self._accumulator.reenqueue(batch)
  210. self._sensors.record_retries(batch.topic_partition.topic, batch.record_count)
  211. else:
  212. if error is Errors.TopicAuthorizationFailedError:
  213. error = error(batch.topic_partition.topic)
  214. # tell the user the result of their request
  215. batch.done(base_offset, timestamp_ms, error, log_start_offset, global_error)
  216. self._accumulator.deallocate(batch)
  217. if error is not None:
  218. self._sensors.record_errors(batch.topic_partition.topic, batch.record_count)
  219. if getattr(error, 'invalid_metadata', False):
  220. self._metadata.request_update()
  221. # Unmute the completed partition.
  222. if self.config['guarantee_message_order']:
  223. self._accumulator.muted.remove(batch.topic_partition)
  224. def _can_retry(self, batch, error):
  225. """
  226. We can retry a send if the error is transient and the number of
  227. attempts taken is fewer than the maximum allowed
  228. """
  229. return (batch.attempts < self.config['retries']
  230. and getattr(error, 'retriable', False))
  231. def _create_produce_requests(self, collated):
  232. """
  233. Transfer the record batches into a list of produce requests on a
  234. per-node basis.
  235. Arguments:
  236. collated: {node_id: [RecordBatch]}
  237. Returns:
  238. dict: {node_id: ProduceRequest} (version depends on api_version)
  239. """
  240. requests = {}
  241. for node_id, batches in six.iteritems(collated):
  242. requests[node_id] = self._produce_request(
  243. node_id, self.config['acks'],
  244. self.config['request_timeout_ms'], batches)
  245. return requests
  246. def _produce_request(self, node_id, acks, timeout, batches):
  247. """Create a produce request from the given record batches.
  248. Returns:
  249. ProduceRequest (version depends on api_version)
  250. """
  251. produce_records_by_partition = collections.defaultdict(dict)
  252. for batch in batches:
  253. topic = batch.topic_partition.topic
  254. partition = batch.topic_partition.partition
  255. buf = batch.records.buffer()
  256. produce_records_by_partition[topic][partition] = buf
  257. kwargs = {}
  258. if self.config['api_version'] >= (2, 1):
  259. version = 7
  260. elif self.config['api_version'] >= (2, 0):
  261. version = 6
  262. elif self.config['api_version'] >= (1, 1):
  263. version = 5
  264. elif self.config['api_version'] >= (1, 0):
  265. version = 4
  266. elif self.config['api_version'] >= (0, 11):
  267. version = 3
  268. kwargs = dict(transactional_id=None)
  269. elif self.config['api_version'] >= (0, 10):
  270. version = 2
  271. elif self.config['api_version'] == (0, 9):
  272. version = 1
  273. else:
  274. version = 0
  275. return ProduceRequest[version](
  276. required_acks=acks,
  277. timeout=timeout,
  278. topics=[(topic, list(partition_info.items()))
  279. for topic, partition_info
  280. in six.iteritems(produce_records_by_partition)],
  281. **kwargs
  282. )
  283. def wakeup(self):
  284. """Wake up the selector associated with this send thread."""
  285. self._client.wakeup()
  286. def bootstrap_connected(self):
  287. return self._client.bootstrap_connected()
  288. class SenderMetrics(object):
  289. def __init__(self, metrics, client, metadata):
  290. self.metrics = metrics
  291. self._client = client
  292. self._metadata = metadata
  293. sensor_name = 'batch-size'
  294. self.batch_size_sensor = self.metrics.sensor(sensor_name)
  295. self.add_metric('batch-size-avg', Avg(),
  296. sensor_name=sensor_name,
  297. description='The average number of bytes sent per partition per-request.')
  298. self.add_metric('batch-size-max', Max(),
  299. sensor_name=sensor_name,
  300. description='The max number of bytes sent per partition per-request.')
  301. sensor_name = 'compression-rate'
  302. self.compression_rate_sensor = self.metrics.sensor(sensor_name)
  303. self.add_metric('compression-rate-avg', Avg(),
  304. sensor_name=sensor_name,
  305. description='The average compression rate of record batches.')
  306. sensor_name = 'queue-time'
  307. self.queue_time_sensor = self.metrics.sensor(sensor_name)
  308. self.add_metric('record-queue-time-avg', Avg(),
  309. sensor_name=sensor_name,
  310. description='The average time in ms record batches spent in the record accumulator.')
  311. self.add_metric('record-queue-time-max', Max(),
  312. sensor_name=sensor_name,
  313. description='The maximum time in ms record batches spent in the record accumulator.')
  314. sensor_name = 'produce-throttle-time'
  315. self.produce_throttle_time_sensor = self.metrics.sensor(sensor_name)
  316. self.add_metric('produce-throttle-time-avg', Avg(),
  317. sensor_name=sensor_name,
  318. description='The average throttle time in ms')
  319. self.add_metric('produce-throttle-time-max', Max(),
  320. sensor_name=sensor_name,
  321. description='The maximum throttle time in ms')
  322. sensor_name = 'records-per-request'
  323. self.records_per_request_sensor = self.metrics.sensor(sensor_name)
  324. self.add_metric('record-send-rate', Rate(),
  325. sensor_name=sensor_name,
  326. description='The average number of records sent per second.')
  327. self.add_metric('records-per-request-avg', Avg(),
  328. sensor_name=sensor_name,
  329. description='The average number of records per request.')
  330. sensor_name = 'bytes'
  331. self.byte_rate_sensor = self.metrics.sensor(sensor_name)
  332. self.add_metric('byte-rate', Rate(),
  333. sensor_name=sensor_name,
  334. description='The average number of bytes sent per second.')
  335. sensor_name = 'record-retries'
  336. self.retry_sensor = self.metrics.sensor(sensor_name)
  337. self.add_metric('record-retry-rate', Rate(),
  338. sensor_name=sensor_name,
  339. description='The average per-second number of retried record sends')
  340. sensor_name = 'errors'
  341. self.error_sensor = self.metrics.sensor(sensor_name)
  342. self.add_metric('record-error-rate', Rate(),
  343. sensor_name=sensor_name,
  344. description='The average per-second number of record sends that resulted in errors')
  345. sensor_name = 'record-size-max'
  346. self.max_record_size_sensor = self.metrics.sensor(sensor_name)
  347. self.add_metric('record-size-max', Max(),
  348. sensor_name=sensor_name,
  349. description='The maximum record size across all batches')
  350. self.add_metric('record-size-avg', Avg(),
  351. sensor_name=sensor_name,
  352. description='The average maximum record size per batch')
  353. self.add_metric('requests-in-flight',
  354. AnonMeasurable(lambda *_: self._client.in_flight_request_count()),
  355. description='The current number of in-flight requests awaiting a response.')
  356. self.add_metric('metadata-age',
  357. AnonMeasurable(lambda _, now: (now - self._metadata._last_successful_refresh_ms) / 1000),
  358. description='The age in seconds of the current producer metadata being used.')
  359. def add_metric(self, metric_name, measurable, group_name='producer-metrics',
  360. description=None, tags=None,
  361. sensor_name=None):
  362. m = self.metrics
  363. metric = m.metric_name(metric_name, group_name, description, tags)
  364. if sensor_name:
  365. sensor = m.sensor(sensor_name)
  366. sensor.add(metric, measurable)
  367. else:
  368. m.add_metric(metric, measurable)
  369. def maybe_register_topic_metrics(self, topic):
  370. def sensor_name(name):
  371. return 'topic.{0}.{1}'.format(topic, name)
  372. # if one sensor of the metrics has been registered for the topic,
  373. # then all other sensors should have been registered; and vice versa
  374. if not self.metrics.get_sensor(sensor_name('records-per-batch')):
  375. self.add_metric('record-send-rate', Rate(),
  376. sensor_name=sensor_name('records-per-batch'),
  377. group_name='producer-topic-metrics.' + topic,
  378. description= 'Records sent per second for topic ' + topic)
  379. self.add_metric('byte-rate', Rate(),
  380. sensor_name=sensor_name('bytes'),
  381. group_name='producer-topic-metrics.' + topic,
  382. description='Bytes per second for topic ' + topic)
  383. self.add_metric('compression-rate', Avg(),
  384. sensor_name=sensor_name('compression-rate'),
  385. group_name='producer-topic-metrics.' + topic,
  386. description='Average Compression ratio for topic ' + topic)
  387. self.add_metric('record-retry-rate', Rate(),
  388. sensor_name=sensor_name('record-retries'),
  389. group_name='producer-topic-metrics.' + topic,
  390. description='Record retries per second for topic ' + topic)
  391. self.add_metric('record-error-rate', Rate(),
  392. sensor_name=sensor_name('record-errors'),
  393. group_name='producer-topic-metrics.' + topic,
  394. description='Record errors per second for topic ' + topic)
  395. def update_produce_request_metrics(self, batches_map):
  396. for node_batch in batches_map.values():
  397. records = 0
  398. total_bytes = 0
  399. for batch in node_batch:
  400. # register all per-topic metrics at once
  401. topic = batch.topic_partition.topic
  402. self.maybe_register_topic_metrics(topic)
  403. # per-topic record send rate
  404. topic_records_count = self.metrics.get_sensor(
  405. 'topic.' + topic + '.records-per-batch')
  406. topic_records_count.record(batch.record_count)
  407. # per-topic bytes send rate
  408. topic_byte_rate = self.metrics.get_sensor(
  409. 'topic.' + topic + '.bytes')
  410. topic_byte_rate.record(batch.records.size_in_bytes())
  411. # per-topic compression rate
  412. topic_compression_rate = self.metrics.get_sensor(
  413. 'topic.' + topic + '.compression-rate')
  414. topic_compression_rate.record(batch.records.compression_rate())
  415. # global metrics
  416. self.batch_size_sensor.record(batch.records.size_in_bytes())
  417. if batch.drained:
  418. self.queue_time_sensor.record(batch.drained - batch.created)
  419. self.compression_rate_sensor.record(batch.records.compression_rate())
  420. self.max_record_size_sensor.record(batch.max_record_size)
  421. records += batch.record_count
  422. total_bytes += batch.records.size_in_bytes()
  423. self.records_per_request_sensor.record(records)
  424. self.byte_rate_sensor.record(total_bytes)
  425. def record_retries(self, topic, count):
  426. self.retry_sensor.record(count)
  427. sensor = self.metrics.get_sensor('topic.' + topic + '.record-retries')
  428. if sensor:
  429. sensor.record(count)
  430. def record_errors(self, topic, count):
  431. self.error_sensor.record(count)
  432. sensor = self.metrics.get_sensor('topic.' + topic + '.record-errors')
  433. if sensor:
  434. sensor.record(count)
  435. def record_throttle_time(self, throttle_time_ms, node=None):
  436. self.produce_throttle_time_sensor.record(throttle_time_ms)