mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-18 14:38:06 +08:00
Add km module kafka
This commit is contained in:
112
docs/zh/Kafka分享/Kafka常见问题解答/Kafka客户端_断开重连策略/Kafka客户度_断开重连策略.md
Normal file
112
docs/zh/Kafka分享/Kafka常见问题解答/Kafka客户端_断开重连策略/Kafka客户度_断开重连策略.md
Normal file
@@ -0,0 +1,112 @@
|
||||
结论:按照时间的过期策略,时间是按照时间索引文件的最后一条数据里面记录的时间作为是否过期的判断标准。那么另外一个问题,这个时间是怎么写入的,继续看下面
|
||||
|
||||
```java
|
||||
/**
|
||||
* If topic deletion is enabled, delete any log segments that have either expired due to time based retention
|
||||
* or because the log size is > retentionSize.
|
||||
* Whether or not deletion is enabled, delete any log segments that are before the log start offset
|
||||
*/
|
||||
// 清理策略
|
||||
def deleteOldSegments(): Int = {
|
||||
if (config.delete) {
|
||||
// 清理策略:保存时间、保存大小、开始offset
|
||||
deleteRetentionMsBreachedSegments() + deleteRetentionSizeBreachedSegments() + deleteLogStartOffsetBreachedSegments()
|
||||
} else {
|
||||
deleteLogStartOffsetBreachedSegments()
|
||||
}
|
||||
}
|
||||
|
||||
// 调用按照时间的清理策略
|
||||
private def deleteRetentionMsBreachedSegments(): Int = {
|
||||
if (config.retentionMs < 0) return 0
|
||||
val startMs = time.milliseconds
|
||||
// segment按照 startMs - segment.largestTimestamp > config.retentionMs 的策略进行清理.
|
||||
// 那么我们再看一下 segment.largestTimestamp 的时间是怎么获取的
|
||||
deleteOldSegments((segment, _) => startMs - segment.largestTimestamp > config.retentionMs,
|
||||
reason = s"retention time ${config.retentionMs}ms breach")
|
||||
}
|
||||
|
||||
/**
|
||||
* The largest timestamp this segment contains.
|
||||
*/
|
||||
// LogSegment的代码,可以发现如果maxTimestampSoFar>0时,就是maxTimestampSoFar,否则是最近一次修改时间
|
||||
// 那么maxTimestampSoFar是怎么获取的呢
|
||||
def largestTimestamp = if (maxTimestampSoFar >= 0) maxTimestampSoFar else lastModified
|
||||
|
||||
|
||||
// maxTimestampSoFar相当于是时间索引的最后一个entry的时间,那么我们继续看一下timeIndex.lastEntry是什么时间
|
||||
def maxTimestampSoFar: Long = {
|
||||
if (_maxTimestampSoFar.isEmpty)
|
||||
_maxTimestampSoFar = Some(timeIndex.lastEntry.timestamp)
|
||||
_maxTimestampSoFar.get
|
||||
}
|
||||
|
||||
// 获取时间索引的最后一个entry里面的时间&offset
|
||||
// 在同一个时间索引文件里面,时间字段是单调递增的,因此这里获取到的是时间索引里面最大的那个时间。
|
||||
// 那么这个时间是怎么写入的呢?我们继续往下看,看时间索引的写入这块
|
||||
private def lastEntryFromIndexFile: TimestampOffset = {
|
||||
inLock(lock) {
|
||||
_entries match {
|
||||
case 0 => TimestampOffset(RecordBatch.NO_TIMESTAMP, baseOffset)
|
||||
case s => parseEntry(mmap, s - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
时间索引文件这个时间是如何写入的?
|
||||
结论:如果配置了LOG_APPEND_TIME,那么就是写入服务器的时间。如果是配置CREATE_TIME,那么就是record时间里面的最大的那一个。
|
||||
|
||||
```java
|
||||
// 从TimeIndex类的maybeAppend方法一步一步的向上查找,查看里面的时间数据的写入,我们可以发现
|
||||
// 这个时间是在 LogValidator.validateMessagesAndAssignOffsets 这个方法里面生成的
|
||||
// 遍历records
|
||||
for (batch <- records.batches.asScala) {
|
||||
validateBatch(topicPartition, firstBatch, batch, origin, toMagicValue, brokerTopicStats)
|
||||
val recordErrors = new ArrayBuffer[ApiRecordError](0)
|
||||
for ((record, batchIndex) <- batch.asScala.view.zipWithIndex) {
|
||||
validateRecord(batch, topicPartition, record, batchIndex, now, timestampType,
|
||||
timestampDiffMaxMs, compactedTopic, brokerTopicStats).foreach(recordError => recordErrors += recordError)
|
||||
// we fail the batch if any record fails, so we stop appending if any record fails
|
||||
if (recordErrors.isEmpty)
|
||||
// 拼接offset,这里还会计算那个时间戳
|
||||
builder.appendWithOffset(offsetCounter.getAndIncrement(), record)
|
||||
}
|
||||
processRecordErrors(recordErrors)
|
||||
}
|
||||
//
|
||||
private long appendLegacyRecord(long offset, long timestamp, ByteBuffer key, ByteBuffer value, byte magic) throws IOException {
|
||||
ensureOpenForRecordAppend();
|
||||
if (compressionType == CompressionType.NONE && timestampType == TimestampType.LOG_APPEND_TIME)
|
||||
// 定义了LOG_APPEND_TIME,则使用logAppendTime,
|
||||
timestamp = logAppendTime;
|
||||
|
||||
int size = LegacyRecord.recordSize(magic, key, value);
|
||||
AbstractLegacyRecordBatch.writeHeader(appendStream, toInnerOffset(offset), size);
|
||||
|
||||
if (timestampType == TimestampType.LOG_APPEND_TIME)
|
||||
timestamp = logAppendTime;
|
||||
long crc = LegacyRecord.write(appendStream, magic, timestamp, key, value, CompressionType.NONE, timestampType);
|
||||
// 时间计算
|
||||
recordWritten(offset, timestamp, size + Records.LOG_OVERHEAD);
|
||||
return crc;
|
||||
}
|
||||
|
||||
// 最值的计算
|
||||
private void recordWritten(long offset, long timestamp, int size) {
|
||||
if (numRecords == Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException("Maximum number of records per batch exceeded, max records: " + Integer.MAX_VALUE);
|
||||
if (offset - baseOffset > Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException("Maximum offset delta exceeded, base offset: " + baseOffset +
|
||||
", last offset: " + offset);
|
||||
numRecords += 1;
|
||||
uncompressedRecordsSizeInBytes += size;
|
||||
lastOffset = offset;
|
||||
if (magic > RecordBatch.MAGIC_VALUE_V0 && timestamp > maxTimestamp) {
|
||||
// 时间更新,最后时间索引记录的是maxTimestamp这个字段
|
||||
maxTimestamp = timestamp;
|
||||
offsetOfMaxTimestamp = offset;
|
||||
}
|
||||
}
|
||||
```
|
||||
112
docs/zh/Kafka分享/Kafka常见问题解答/Kafka服务端_日志清理策略/Kafka服务端-日志清理策略.md
Normal file
112
docs/zh/Kafka分享/Kafka常见问题解答/Kafka服务端_日志清理策略/Kafka服务端-日志清理策略.md
Normal file
@@ -0,0 +1,112 @@
|
||||
结论:按照时间的过期策略,时间是按照时间索引文件的最后一条数据里面记录的时间作为是否过期的判断标准。那么另外一个问题,这个时间是怎么写入的,继续看下面
|
||||
|
||||
```java
|
||||
/**
|
||||
* If topic deletion is enabled, delete any log segments that have either expired due to time based retention
|
||||
* or because the log size is > retentionSize.
|
||||
* Whether or not deletion is enabled, delete any log segments that are before the log start offset
|
||||
*/
|
||||
// 清理策略
|
||||
def deleteOldSegments(): Int = {
|
||||
if (config.delete) {
|
||||
// 清理策略:保存时间、保存大小、开始offset
|
||||
deleteRetentionMsBreachedSegments() + deleteRetentionSizeBreachedSegments() + deleteLogStartOffsetBreachedSegments()
|
||||
} else {
|
||||
deleteLogStartOffsetBreachedSegments()
|
||||
}
|
||||
}
|
||||
|
||||
// 调用按照时间的清理策略
|
||||
private def deleteRetentionMsBreachedSegments(): Int = {
|
||||
if (config.retentionMs < 0) return 0
|
||||
val startMs = time.milliseconds
|
||||
// segment按照 startMs - segment.largestTimestamp > config.retentionMs 的策略进行清理.
|
||||
// 那么我们再看一下 segment.largestTimestamp 的时间是怎么获取的
|
||||
deleteOldSegments((segment, _) => startMs - segment.largestTimestamp > config.retentionMs,
|
||||
reason = s"retention time ${config.retentionMs}ms breach")
|
||||
}
|
||||
|
||||
/**
|
||||
* The largest timestamp this segment contains.
|
||||
*/
|
||||
// LogSegment的代码,可以发现如果maxTimestampSoFar>0时,就是maxTimestampSoFar,否则是最近一次修改时间
|
||||
// 那么maxTimestampSoFar是怎么获取的呢
|
||||
def largestTimestamp = if (maxTimestampSoFar >= 0) maxTimestampSoFar else lastModified
|
||||
|
||||
|
||||
// maxTimestampSoFar相当于是时间索引的最后一个entry的时间,那么我们继续看一下timeIndex.lastEntry是什么时间
|
||||
def maxTimestampSoFar: Long = {
|
||||
if (_maxTimestampSoFar.isEmpty)
|
||||
_maxTimestampSoFar = Some(timeIndex.lastEntry.timestamp)
|
||||
_maxTimestampSoFar.get
|
||||
}
|
||||
|
||||
// 获取时间索引的最后一个entry里面的时间&offset
|
||||
// 在同一个时间索引文件里面,时间字段是单调递增的,因此这里获取到的是时间索引里面最大的那个时间。
|
||||
// 那么这个时间是怎么写入的呢?我们继续往下看,看时间索引的写入这块
|
||||
private def lastEntryFromIndexFile: TimestampOffset = {
|
||||
inLock(lock) {
|
||||
_entries match {
|
||||
case 0 => TimestampOffset(RecordBatch.NO_TIMESTAMP, baseOffset)
|
||||
case s => parseEntry(mmap, s - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
时间索引文件这个时间是如何写入的?
|
||||
结论:如果配置了LOG_APPEND_TIME,那么就是写入服务器的时间。如果是配置CREATE_TIME,那么就是record时间里面的最大的那一个。
|
||||
|
||||
```java
|
||||
// 从TimeIndex类的maybeAppend方法一步一步的向上查找,查看里面的时间数据的写入,我们可以发现
|
||||
// 这个时间是在 LogValidator.validateMessagesAndAssignOffsets 这个方法里面生成的
|
||||
// 遍历records
|
||||
for (batch <- records.batches.asScala) {
|
||||
validateBatch(topicPartition, firstBatch, batch, origin, toMagicValue, brokerTopicStats)
|
||||
val recordErrors = new ArrayBuffer[ApiRecordError](0)
|
||||
for ((record, batchIndex) <- batch.asScala.view.zipWithIndex) {
|
||||
validateRecord(batch, topicPartition, record, batchIndex, now, timestampType,
|
||||
timestampDiffMaxMs, compactedTopic, brokerTopicStats).foreach(recordError => recordErrors += recordError)
|
||||
// we fail the batch if any record fails, so we stop appending if any record fails
|
||||
if (recordErrors.isEmpty)
|
||||
// 拼接offset,这里还会计算那个时间戳
|
||||
builder.appendWithOffset(offsetCounter.getAndIncrement(), record)
|
||||
}
|
||||
processRecordErrors(recordErrors)
|
||||
}
|
||||
//
|
||||
private long appendLegacyRecord(long offset, long timestamp, ByteBuffer key, ByteBuffer value, byte magic) throws IOException {
|
||||
ensureOpenForRecordAppend();
|
||||
if (compressionType == CompressionType.NONE && timestampType == TimestampType.LOG_APPEND_TIME)
|
||||
// 定义了LOG_APPEND_TIME,则使用logAppendTime,
|
||||
timestamp = logAppendTime;
|
||||
|
||||
int size = LegacyRecord.recordSize(magic, key, value);
|
||||
AbstractLegacyRecordBatch.writeHeader(appendStream, toInnerOffset(offset), size);
|
||||
|
||||
if (timestampType == TimestampType.LOG_APPEND_TIME)
|
||||
timestamp = logAppendTime;
|
||||
long crc = LegacyRecord.write(appendStream, magic, timestamp, key, value, CompressionType.NONE, timestampType);
|
||||
// 时间计算
|
||||
recordWritten(offset, timestamp, size + Records.LOG_OVERHEAD);
|
||||
return crc;
|
||||
}
|
||||
|
||||
// 最值的计算
|
||||
private void recordWritten(long offset, long timestamp, int size) {
|
||||
if (numRecords == Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException("Maximum number of records per batch exceeded, max records: " + Integer.MAX_VALUE);
|
||||
if (offset - baseOffset > Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException("Maximum offset delta exceeded, base offset: " + baseOffset +
|
||||
", last offset: " + offset);
|
||||
numRecords += 1;
|
||||
uncompressedRecordsSizeInBytes += size;
|
||||
lastOffset = offset;
|
||||
if (magic > RecordBatch.MAGIC_VALUE_V0 && timestamp > maxTimestamp) {
|
||||
// 时间更新,最后时间索引记录的是maxTimestamp这个字段
|
||||
maxTimestamp = timestamp;
|
||||
offsetOfMaxTimestamp = offset;
|
||||
}
|
||||
}
|
||||
```
|
||||
42
docs/zh/Kafka分享/Kafka常见问题解答/Kafka服务端_权限控制/Kafka服务端_权限控制.md
Normal file
42
docs/zh/Kafka分享/Kafka常见问题解答/Kafka服务端_权限控制/Kafka服务端_权限控制.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Kafka服务端—权限控制
|
||||
|
||||
[TOC]
|
||||
|
||||
|
||||
资源类型:
|
||||
- UNKNOWN: 未知
|
||||
- ANY:任意的资源
|
||||
- TOPIC:Topic
|
||||
- GROUP:消费组
|
||||
- CLUSTER:整个集群
|
||||
- TRANSACTIONAL_ID:事物ID
|
||||
- DELEGATION_TOKEN:Token
|
||||
|
||||
|
||||
资源操作:
|
||||
- UNKNOWN:未知
|
||||
- ANY:任意的操作
|
||||
- ALL:所有的操作
|
||||
- READ:读
|
||||
- WRITE:写
|
||||
- CREATE:创建
|
||||
- DELETE:删除
|
||||
- ALTER:修改
|
||||
- DESCRIBE:描述,查看
|
||||
- CLUSTER_ACTION:集群动作
|
||||
- DESCRIBE_CONFIGS:查看配置
|
||||
- ALTER_CONFIGS:修改配置
|
||||
- IDEMPOTENT_WRITE:幂等写
|
||||
|
||||
|
||||
资源书写类型:
|
||||
- UNKNOWN:未知
|
||||
- ANY:任意
|
||||
- MATCH:满足LITERAL、PREFIXED或者*的任意中的一个即可
|
||||
- LITERAL:全匹配,完全按照原文匹配
|
||||
- PREFIXED:前缀匹配
|
||||
|
||||
|
||||
认证结果:
|
||||
- ALLOWED:允许
|
||||
- DENIED:拒绝
|
||||
180
docs/zh/Kafka分享/Kafka常见问题解答/Kafka集群滚动重启实践/Kafka集群滚动重启实践.md
Normal file
180
docs/zh/Kafka分享/Kafka常见问题解答/Kafka集群滚动重启实践/Kafka集群滚动重启实践.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Kafka集群平稳滚动重启实践
|
||||
|
||||
[TOC]
|
||||
|
||||
## 0、前言
|
||||
|
||||
Kafka集群的滚动重启,是一件非常危险的事情,操作不当的情况下可能会导致Kafka集群不可服务。即便操作上准确无误,也可能因为业务方服务非常敏感、服务健壮性不足、使用的客户端存在BUG等原因,导致业务方业务受损。
|
||||
|
||||
基于以上的原因以及我们以往的经验,我们梳理了一下在对Kafka集群滚动重启中,需要做的事情以及注意的点。
|
||||
|
||||
|
||||
## 1、用户告知
|
||||
|
||||
### 1.1、告知内容
|
||||
|
||||
提前告知用户:
|
||||
- 我们要做什么;
|
||||
- 为什么要做;
|
||||
- 可能的影响,及简单处理方式,比如因为leader会切换,node客户端可能会消费中断,需要重启等;
|
||||
- 联系人;
|
||||
- 操作时间;**在操作时间选择上,建议选择业务方在的工作时间,方便出问题后能及时协同处理**
|
||||
|
||||
告知内容例子:
|
||||
```
|
||||
标题:
|
||||
[2021-11-11] XXX-Kafka 集群升级至 kafka_2.12-xxxx
|
||||
|
||||
变更原因:
|
||||
1、性能优化
|
||||
|
||||
变更内容:
|
||||
1、集群单机连接数限制调整到 1200
|
||||
|
||||
变更影响:
|
||||
升级过程中会有leader切换,理论上无影响,有问题及时联系kafka服务号
|
||||
|
||||
联系人:
|
||||
xxxxx@xxxx.com
|
||||
|
||||
计划时间:
|
||||
2021-11-11T10:00:00+08:00 至 2021-11-11T16:30:00+08:00
|
||||
```
|
||||
|
||||
### 1.2、相关建议
|
||||
|
||||
- 增加对自身服务监控,比如监控服务对应的Topic的流量,监控消费的Lag等指标,以便出现问题时能被及时发现;
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
## 2、滚动重启
|
||||
|
||||
**真正操作前,建议演练一下。**
|
||||
|
||||
---
|
||||
|
||||
### 2.1、整体操作流程
|
||||
|
||||
- 1、再次通知用户,我们现在要开始进行重启操作,有问题随时联系;
|
||||
- 2、重启**非Controller**的一台Broker;
|
||||
- 3、观察重启后指标等是否都正常,如果出现异常则进行相应的处理;
|
||||
- 4、告知用户我们已重启一台,xxx分钟后,要操作剩余所有的机器,让用户注意自身服务是否正常,有问题随时反馈;
|
||||
- 5、xxx分钟后,剩余机器逐台重启,**Kafka-Controller放在最后重启**;
|
||||
- 6、操作完成后,告知用户已操作完成,让用户关注自身服务是否正常,有问题随时反馈;
|
||||
|
||||
---
|
||||
|
||||
|
||||
### 2.2、单台操作流程
|
||||
|
||||
单台操作时,主要分两部分,第一部分时操作进行重启,第二部分是重启完成之后观察服务是否正常。
|
||||
|
||||
#### 2.2.1、重启
|
||||
|
||||
**第一步:停服务**
|
||||
|
||||
```bash
|
||||
# 以kill的方式,停Kafka服务。
|
||||
|
||||
# 强调:不能以kill -9的方式停服!!!
|
||||
```
|
||||
|
||||
**第二步:修改配置**
|
||||
|
||||
```bash
|
||||
# 对本次重启需要进行修改的配置进行修改;
|
||||
|
||||
# 强烈要求将本次修改的配置的具体操作步骤罗列出来;
|
||||
```
|
||||
|
||||
**第三步:Broker限流**
|
||||
|
||||
```bash
|
||||
# 如果在停服务和启动服务之间的时间间隔非常的久,导致启动后需要同步非常多的数据,则在启动服务之前,我们需要做好副本同步之间的限流,否则可能会拉打满带宽,挂其他Broker等。
|
||||
|
||||
# 这里的需要同步的数据量怎么样算多,这个没有一个非常准确的值,只要说可能将leader带宽打满,拉挂其他Broker都算是数据量大。
|
||||
```
|
||||
|
||||
**第四步:起服务**
|
||||
|
||||
```bash
|
||||
# 启动Kafk服务,然后观察服务是否正常
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.2、观察
|
||||
|
||||
|
||||
**第一步:观察启动日志**
|
||||
|
||||
```bash
|
||||
# 查看server.log文件,看到该日志后表示Kafka服务端已启动完成
|
||||
[2021-11-17 14:07:22,459][INFO][main]: [KafkaServer id=2] started
|
||||
|
||||
# 查看server.log文件,检查是否存在ERROR及FATAL日志,如果出现这些日志,需要暂停升级并分析出现这些日志的影响。
|
||||
```
|
||||
|
||||
**第二步:观察服务监控**
|
||||
|
||||
如果可以做到下列指标的监控的话,建议都在监控系统中,配置上这些监控。
|
||||
|
||||
这一步正常来说只要配置上了,如果出现异常监控系统会主动通知,不需要我们细致的去看,所以虽然列的比较多,但是操作的时候不太需要主动去看所有的指标。
|
||||
|
||||
```bash
|
||||
# 服务存活监控;
|
||||
# 错误日志监控;
|
||||
# GC监控;
|
||||
# 脏选举监控;
|
||||
# ISR收缩速度监控;
|
||||
# leader=-1监控;
|
||||
# 网络处理线程负载监控;
|
||||
# 请求处理线程负载监控;
|
||||
# 副本未同步监控;
|
||||
# 系统负载监控(CPU、磁盘IO、磁盘容量、网络带宽、网络丢包、TCP连接数、TCP连接增速、文件句柄数);
|
||||
```
|
||||
|
||||
**第三步:检查变更是否生效**
|
||||
|
||||
这一步骤没有什么好说的,就是检查是否生效。
|
||||
|
||||
|
||||
|
||||
**第四步:观察流量是否正常**
|
||||
|
||||
```
|
||||
观察一:存在Broker组的概念,则可以观察重启所在的Broker组的整个流量和重启之前是否基本一致。
|
||||
|
||||
观察二:重点选取几个Broker上的Topic,观察流量是否出现异常,比如突然没有流入或流出流量了。
|
||||
```
|
||||
|
||||
|
||||
**第五步:等待副本同步完成**
|
||||
|
||||
```bash
|
||||
# 查看整个集群的副本同步状态,确保整个集群都是处于已同步的状态。该信息可以通过LogiKM查看。
|
||||
|
||||
# 实际上是不需要整个集群所有的Broker处于已同步的状态,只需要是落在所重启的Broker上的所有的分区都处于同步状态即可,但是这个不太好判断,因此简单粗暴的就是看整个集群都处于同步状态。
|
||||
```
|
||||
|
||||
### 2.3、其他重要说明
|
||||
|
||||
- 如果重启中需要同步非常大的数据量,Broker本身负载也较高,则建议重启操作要避开leader rebalance的时间;
|
||||
- 重启的过程中,会进行leader的切换,最后一台操作完成之后,需要进行leader rebalance;
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 3、信息记录
|
||||
|
||||
操作中,很难避免就不出现任何问题,出现问题时就需要我们做好相关的记录,比如记录:
|
||||
|
||||
- 1、重要的业务及其Topic;
|
||||
- 2、敏感的业务及其Topic;
|
||||
- 3、特殊客户端的业务及其Topic;
|
||||
- 4、不合理使用的业务及其Topic;
|
||||
|
||||
后续我们可以将这些Topic进行重点保障,以及再次进行操作的时候,我们能够更准确的触达到用户。
|
||||
|
||||
Reference in New Issue
Block a user