KafkaBroker 简析
Kafka 依赖 Zookeeper 来维护集群成员的信息:
- Kafka 使用 Zookeeper 的临时节点来选举 controller
- Zookeeper 在 broker 加入集群或退出集群时通知 controller
- controller 负责在 broker 加入或离开集群时进行分区 leader 选举
broker 管理
每个 broker 都有一个唯一标识符 ID,这个标识符可以在配置文件里指定,也可以自动生成。
在 broker 启动的时候,它通过在 Zookeeper 的 /brokers/ids
路径上创建临时节点,把自己的 ID 注册 Zookeeper。
Kafka 组件会订阅 Zookeeper 的 /brokers/ids
路径,当有 broker 加入集群或退出集群时,这些组件就可以获得通知。
在 broker 停机、出现网络分区或长时间垃圾回收停顿时,会导致其 Zookeeper 会话失效,导致其在启动时创建的临时节点会自动被移除。
监听 broker 列表的 Kafka 组件会被告知该 broker 已移除,然后处理 broker 崩溃的后续事宜。
在完全关闭一个 broker 之后,如果使用相同的 ID 启动另一个全新的 broker,它会立即加入集群,并拥有与旧 broker 相同的分区和主题。
controller 选举
controller 其实就是一个 broker,它除了具有一般 broker 的功能之外,还负责分区 leader 的选举。
为了在整个集群中指定一个唯一的 controller,broker 集群需要进行选举,该过程依赖以下两个 Zookeeper 节点:
// 临时节点 controller(保存最新的 controller 节点信息,保证唯一性)
object ControllerZNode {
def path = "/controller"
def encode(brokerId: Int, timestamp: Long): Array[Byte] = {
Json.encodeAsBytes(Map("version" -> 1, "brokerid" -> brokerId, "timestamp" -> timestamp.toString).asJava)
}
def decode(bytes: Array[Byte]): Option[Int] = Json.parseBytes(bytes).map { js =>
js.asJsonObject("brokerid").to[Int]
}
}
// 永久节点 controller_epoch(保存最新 controller 对应的任期号,用于避免脑裂)
object ControllerEpochZNode {
def path = "/controller_epoch"
def encode(epoch: Int): Array[Byte] = epoch.toString.getBytes(UTF_8)
def decode(bytes: Array[Byte]): Int = new String(bytes, UTF_8).toInt
}
broker 启动后会发起一轮选举,选举通过 Zookeeper 提供的创建节点功能来实现:
/controller
,创建成功的 broker 将成为 controller。新选出的 controller 会同时递增
/controller_epoch
中的任期号,其他 broker 可以根据任期号忽略已过期 controller 的消息。抢占失败的 broker 会收到一个 NODEEXISTS 响应,转而在节点上创建
Watcher
实时监控/controller
节点。当 controller 被关闭或者断开连接,Zookeeper 上的临时节点就会消失,集群里的其他 broker 会接收到通知并发起一轮新的选举。
broker 中的 KafkaController 对象负责发起选举:
private def elect(): Unit = {
// 检查集群中是否存在可用 controller (activeControllerId == -1)
try {
// 当前 broker 通过 KafkaZkClient 发起选举,并选举自己为新的 controller
val (epoch, epochZkVersion) = zkClient.registerControllerAndIncrementControllerEpoch(config.brokerId)
// 如果 broker 当选,则更新对应的 controller 相关信息
controllerContext.epoch = epoch
controllerContext.epochZkVersion = epochZkVersion
activeControllerId = config.brokerId
info(s"${config.brokerId} successfully elected as the controller. Epoch incremented to ${controllerContext.epoch} and epoch zk version is now ${controllerContext.epochZkVersion}")
onControllerFailover() // 选举成功后触发维护操作
} catch {
case e: ControllerMovedException =>
maybeResign()
if (activeControllerId != -1) // 其他 broker 被选为 controller
debug(s"Broker $activeControllerId was elected as controller instead of broker ${config.brokerId}", e)
else // 本轮选举没有产生 controller
warn("A controller has been elected but just resigned, this will result in another round of election", e)
case t: Throwable =>
error(s"Error while electing or becoming controller on broker ${config.brokerId}. Trigger controller movement immediately", t)
triggerControllerMove()
}
}
KafkaZkClient 中更新 Zookeeper 的逻辑如下:
def registerControllerAndIncrementControllerEpoch(controllerId: Int): (Int, Int) = {
val timestamp = time.milliseconds()
// 从 /controller_epoch 获取当前 controller 对应的 epoch 与 zkVersion
// 若 /controller_epoch 不存在则尝试创建
val (curEpoch, curEpochZkVersion) = getControllerEpoch
.map(e => (e._1, e._2.getVersion))
.getOrElse(maybeCreateControllerEpochZNode())
// 创建 /controller 并原子性更新 /controller_epoch
val newControllerEpoch = curEpoch + 1
val expectedControllerEpochZkVersion = curEpochZkVersion
debug(s"Try to create ${ControllerZNode.path} and increment controller epoch to $newControllerEpoch with expected controller epoch zkVersion $expectedControllerEpochZkVersion")
// 处理 /controller 节点已存在的情况,直接返回最新节点信息
def checkControllerAndEpoch(): (Int, Int) = {
val curControllerId = getControllerId.getOrElse(throw new ControllerMovedException(
s"The ephemeral node at ${ControllerZNode.path} went away while checking whether the controller election succeeds. " +
s"Aborting controller startup procedure"))
if (controllerId == curControllerId) {
val (epoch, stat) = getControllerEpoch.getOrElse(
throw new IllegalStateException(s"${ControllerEpochZNode.path} existed before but goes away while trying to read it"))
// 如果最新的 epoch 与 newControllerEpoch 相等,则可以推断 zkVersion 与当前 broker 已知的 zkVersion 一致
if (epoch == newControllerEpoch)
return (newControllerEpoch, stat.getVersion)
}
throw new ControllerMovedException("Controller moved to another broker. Aborting controller startup procedure")
}
// 封装 zookeeper 请求
def tryCreateControllerZNodeAndIncrementEpoch(): (Int, Int) = {
val response = retryRequestUntilConnected(
MultiRequest(Seq(
// 发送 CreateRequest 创建 /controller 临时节点
CreateOp(ControllerZNode.path, ControllerZNode.encode(controllerId, timestamp), defaultAcls(ControllerZNode.path), CreateMode.EPHEMERAL),
// 发送 SetDataRequest 更新 /controller_epoch 节点信息
SetDataOp(ControllerEpochZNode.path, ControllerEpochZNode.encode(newControllerEpoch), expectedControllerEpochZkVersion)))
)
response.resultCode match {
case Code.NODEEXISTS | Code.BADVERSION => checkControllerAndEpoch()
case Code.OK =>
val setDataResult = response.zkOpResults(1).rawOpResult.asInstanceOf[SetDataResult]
(newControllerEpoch, setDataResult.getStat.getVersion)
case code => throw KeeperException.create(code)
}
}
// 向 zookeepr 发起请求
tryCreateControllerZNodeAndIncrementEpoch()
}
分区管理
一个 Kafka 分区本质上就是一个备份日志,通过利用多份相同的冗余副本replica
保持系统高可用性。
Kafka 把分区的所有副本均匀地分配到所有 broker上,并从这些副本中挑选一个作为 leader 副本对外提供服务。
而其他副本被称为 follower 副本,不对外提供服务,只能被动地向 leader 副本请求数据,保持与 leader 副本的同步。
当 controller 发现一个 broker 加入集群时,它会使用 broker.id 来检查新加入的 broker 是否包含现有分区的副本。
如果有,controller 就把变更通知发送所有 broker,新 broker 中的分区作为 follower 副本开始从 leader 那里复制消息。
ISR
Kafka 为每个主题维护了一组同步副本集合in-sync replicas
(其中包含 leader 副本)。
只有被 ISR 中的所有副本都接收到的那部分生产者写入的消息才对消费者可见,这意味着 ISR 中的所有副本都会与 leader 保持同步状态。
为了避免出现新 leader 数据不完整导致分区数据丢失的情况,只有 ISR 中 follower 副本才有资格被选举为 leader。
若 follower 副本无法在 replica.lag.time.max.ms
毫秒内向 leader 请求数据,那么该 follower 就会被视为不同步,leader 会将其剔除出 ISR。
leader 会在 ISR 集合发生变更时,会在/isr_change_notification
下创建一个永久节点并写入变更信息。
当监控/isr_change_notification
的 controller 接收到通知后,会更新其他 broker 的元数据,最后删除已处理过的节点。
当出现瞬时峰值流量,只要 follower 不是持续性落后,就不会反复地在 ISR 中移进、移出,避免频繁访问 Zookeeper 影响性能。
首选副本
创建主题时,Kafka 会为每个分区选定一个初始分区 leaderpreferred leader
,其对应的副本被称为首选副本preferred replica
。
controller 在创建主题时会保证 leader 在 broker 之间均衡分布,因此当 leader 按照初始的首选副本分布时,broker 间的负载均衡状态最佳。
然而 broker 失效是难以避免的,重启后的首选副本只能作为 follower 副本加入 ISR 中,不能再对外提供服务。
随着集群的不断运行,leader 不均衡现象会愈发明显:集群中的一小部分 broker 上承载了大量的分区 leader 副本。
可以设置 auto.leader.rebalance.enable = true
解决这一问题:
leader.imbalance.per.broker.percentage
时会自动执行一次 leader 均衡操作。
分区重分配
当一个新的 broker 刚加入集群时,不会自动地分担己有 topic 的负载,它只会对后续新增的 topic 生效。
如果要让新增 broker 为己有的 topic 服务,用户必须手动地调整现有的 topic 的分区分布,将一部分分区搬移到新增 broker 上。这就是所谓的分区重分配reassignment
操作。
除了处理 broker 扩容导致的不均衡之外,再均衡还能用于处理 broker 存储负载不均衡的情况,在单个或多个 broker 之间的日志目录之间重新分配分区。 用于解决多个代理之间的存储负载不平衡。
首领选举
触发分区 leader 选举的几种场景:
- Offline:创建新分区或分区失去现有 leader
- Reassign:用户执行重分配操作
- PreferredReplica:将 leader 迁移回首选副本
- ControlledShutdown:分区的现有 leader 即将下线
当上述几种情况发生时,controller 会遍历所有相关的主题分区并从为其指定新的 leader。
然后向所有包含相关主题分区的 broker 发送更新请求,其中包含了最新的 leader 与 follower 副本分配信息。
更新完毕后,新 leader 会开始处理来自生产者和消费者的请求,而follower 开始从新 leader 那里复制消息。
分区状态信息在对应的节点信息:
// 节点 /brokers/topics/{topic-name}/partitions/{partition-no}/state 保存分区最新状态信息的
object TopicPartitionStateZNode {
def path(partition: TopicPartition) = s"${TopicPartitionZNode.path(partition)}/state"
def encode(leaderIsrAndControllerEpoch: LeaderIsrAndControllerEpoch): Array[Byte] = {
val leaderAndIsr = leaderIsrAndControllerEpoch.leaderAndIsr
val controllerEpoch = leaderIsrAndControllerEpoch.controllerEpoch
Json.encodeAsBytes(Map("version" -> 1, "leader" -> leaderAndIsr.leader, "leader_epoch" -> leaderAndIsr.leaderEpoch,
"controller_epoch" -> controllerEpoch, "isr" -> leaderAndIsr.isr.asJava).asJava)
}
def decode(bytes: Array[Byte], stat: Stat): Option[LeaderIsrAndControllerEpoch] = {
Json.parseBytes(bytes).map { js =>
val leaderIsrAndEpochInfo = js.asJsonObject
val leader = leaderIsrAndEpochInfo("leader").to[Int]
val epoch = leaderIsrAndEpochInfo("leader_epoch").to[Int]
val isr = leaderIsrAndEpochInfo("isr").to[List[Int]]
val controllerEpoch = leaderIsrAndEpochInfo("controller_epoch").to[Int]
val zkPathVersion = stat.getVersion
LeaderIsrAndControllerEpoch(LeaderAndIsr(leader, epoch, isr, zkPathVersion), controllerEpoch)
}
}
}
PartitionStateMachine 管理分区选举的代码:
private def doElectLeaderForPartitions(partitions: Seq[TopicPartition], partitionLeaderElectionStrategy: PartitionLeaderElectionStrategy
): (Map[TopicPartition, Either[Exception, LeaderAndIsr]], Seq[TopicPartition]) = {
// 请求 Zookeeper 获取 partition 当前状态
val getDataResponses = try {
zkClient.getTopicPartitionStatesRaw(partitions)
} catch {
case e: Exception =>
return (partitions.iterator.map(_ -> Left(e)).toMap, Seq.empty)
}
val failedElections = mutable.Map.empty[TopicPartition, Either[Exception, LeaderAndIsr]]
val validLeaderAndIsrs = mutable.Buffer.empty[(TopicPartition, LeaderAndIsr)]
getDataResponses.foreach { getDataResponse =>
val partition = getDataResponse.ctx.get.asInstanceOf[TopicPartition]
val currState = partitionState(partition)
if (getDataResponse.resultCode == Code.OK) {
// 剔除状态已失效或不存在的 partition
TopicPartitionStateZNode.decode(getDataResponse.data, getDataResponse.stat) match {
case Some(leaderIsrAndControllerEpoch) =>
if (leaderIsrAndControllerEpoch.controllerEpoch > controllerContext.epoch) {
val failMsg = s"Aborted leader election for partition $partition since the LeaderAndIsr path was " +
s"already written by another controller. This probably means that the current controller $controllerId went through " +
s"a soft failure and another controller was elected with epoch ${leaderIsrAndControllerEpoch.controllerEpoch}."
failedElections.put(partition, Left(new StateChangeFailedException(failMsg)))
} else {
validLeaderAndIsrs += partition -> leaderIsrAndControllerEpoch.leaderAndIsr
}
case None =>
val exception = new StateChangeFailedException(s"LeaderAndIsr information doesn't exist for partition $partition in $currState state")
failedElections.put(partition, Left(exception))
}
} else if (getDataResponse.resultCode == Code.NONODE) {
val exception = new StateChangeFailedException(s"LeaderAndIsr information doesn't exist for partition $partition in $currState state")
failedElections.put(partition, Left(exception))
} else {
failedElections.put(partition, Left(getDataResponse.resultException.get))
}
}
// 如果全部 partition 均失效,则跳过此次选举
if (validLeaderAndIsrs.isEmpty) {
return (failedElections.toMap, Seq.empty)
}
// 根据指定的选举策略选择 partition leader
val (partitionsWithoutLeaders, partitionsWithLeaders) = partitionLeaderElectionStrategy match {
// Elect leaders for new or offline partitions.
case OfflinePartitionLeaderElectionStrategy(allowUnclean) =>
val partitionsWithUncleanLeaderElectionState = collectUncleanLeaderElectionState(validLeaderAndIsrs, allowUnclean)
leaderForOffline(controllerContext, partitionsWithUncleanLeaderElectionState).partition(_.leaderAndIsr.isEmpty)
// Elect leaders for partitions that are undergoing reassignment.
case ReassignPartitionLeaderElectionStrategy =>
leaderForReassign(controllerContext, validLeaderAndIsrs).partition(_.leaderAndIsr.isEmpty)
// Elect preferred leaders.
case PreferredReplicaPartitionLeaderElectionStrategy =>
leaderForPreferredReplica(controllerContext, validLeaderAndIsrs).partition(_.leaderAndIsr.isEmpty)
// Elect leaders for partitions whose current leaders are shutting down.
case ControlledShutdownPartitionLeaderElectionStrategy =>
leaderForControlledShutdown(controllerContext, validLeaderAndIsrs).partition(_.leaderAndIsr.isEmpty)
}
partitionsWithoutLeaders.foreach { electionResult =>
val partition = electionResult.topicPartition
val failMsg = s"Failed to elect leader for partition $partition under strategy $partitionLeaderElectionStrategy"
failedElections.put(partition, Left(new StateChangeFailedException(failMsg)))
}
// 将选举结果同步到 TopicPartitionStateZNode 对应的 Zookeeper 节点
val recipientsPerPartition = partitionsWithLeaders.map(result => result.topicPartition -> result.liveReplicas).toMap
val adjustedLeaderAndIsrs = partitionsWithLeaders.map(result => result.topicPartition -> result.leaderAndIsr.get).toMap
val UpdateLeaderAndIsrResult(finishedUpdates, updatesToRetry) = zkClient.updateLeaderAndIsr(adjustedLeaderAndIsrs, controllerContext.epoch, controllerContext.epochZkVersion)
finishedUpdates.forKeyValue { (partition, result) =>
result.foreach { leaderAndIsr =>
val replicaAssignment = controllerContext.partitionFullReplicaAssignment(partition)
val leaderIsrAndControllerEpoch = LeaderIsrAndControllerEpoch(leaderAndIsr, controllerContext.epoch)
controllerContext.putPartitionLeadershipInfo(partition, leaderIsrAndControllerEpoch)
controllerBrokerRequestBatch.addLeaderAndIsrRequestForBrokers(recipientsPerPartition(partition), partition,
leaderIsrAndControllerEpoch, replicaAssignment, isNew = false)
}
}
(finishedUpdates ++ failedElections, updatesToRetry)
}
日志复制
复制流程
日志复制中的一些重要偏移概念:
- 起始位移
base offset
:副本所含第一条消息的 offset - 高水位值
high watermark
:副本最新一条己提交消息的 offset - 日志末端位移
log end offset
:副本中下一条待写入消息的 offset
每个副本会同时维护 HW 与 LEO 值:
- leader 保证只有 HW 及其之前的消息,才对消费者是可见的。
- follower 宕机后重启时会对其日志截断,只保留 HW 及其之前的日志消息(新版本有改动)。
Kafka 中的复制流程大致如下:
- leader 会将接收到的消息写入日志文件,同时更新 \(\tiny \textsf{leader LEO}\)
- follower 发送 fetch 请求指定 offset 的消息
- 接收到请求后 leader 会根据 offset 更新下面两个值
- \(\tiny \textsf{follower LEO} = \texttt{offset}\)
- \(\tiny \textsf{HW} = \min(\textsf{leader LEO}, \min(\textsf{follower LEO of ISR}))\)
- leader 返回消息的同时会附带上最新的 HW
- follower 接收到响应后会将消息写入日志文件,并同时更新 HW
leader epoch
在前面我们提到 follower 在重启后会对日志进行截断,这可能导致消息会丢失:
假设某个分区分布在 A 和 B 两个 broker 上,且最开始时 B 是分区 leader
- 某个时刻,follower A 从 leader B 同步消息 m2,但此时并未收到 HW 更新
- 就在此时,follower A 发生了重启,此时它会截断 m2 所在的日志,然后才向 leader B 重新请求数据
- 不巧,此时 leader B 也发生了宕机,此时 follower A 会被选为新的 leader A,这意味着消息 m2 已经永久丢失了
为了解决这一问题,Kafka 为每一届 leader 分配了一个唯一的 epoch,由其追加到日志的消息都会包含这个 epoch。
然后每个副本都在本地维护一个 epoch 快照文件,并在其中保存 (epoch, offset)
:
- epoch 表示 leader 的版本号,当 leader 变更一次 epoch 就会加 1
- offset 则对应 epoch 版本的 leader 写入第一条消息的对于的位移
回到之前的场景,增加了 leader epoch 之后的行为如下:
- follower A 发生重启后,会向 leader B 发送
LeaderEpochRequest
请求最新的 leader epoch - leader B 会在响应中返回自己的 LEO
- follower A 接收到响应后发现无需对日志进行截断,从而避免了消息 m2 丢失
更多的细节可以参考这篇文章。
文件格式
创建主题时,Kafka 会为主题的每个分区在文件系统中创建了一个对应的子目录,命名格式为主题名-分区号
,每个日志子目录的文件构成如下:
[lhop@localhost log]$ tree my-topic-*
my-topic-0
├── 00000000000050209130.index
├── 00000000000050209130.log
├── 00000000000050209130.snapshot
├── 00000000000050209130.timeindex
└── leader-epoch-checkpoint
my-topic-1
├── 00000000000048329826.index
├── 00000000000048329826.log
├── 00000000000048329826.timeindex
└── leader-epoch-checkpoint
其中的 leader-epoch-checkpoint
文件用于存储 leader epoch 快照,用于协助崩溃的副本执行恢复操作,在此就不详细展开。我们重点关注剩余的两类文件。
数据文件
日志段文件(.log)的文件保存着真实的 Kafka 记录。
Kafka 使用该文件第一条记录对应的 offset 来命名此文件。
每个日志段文件是有上限大小的,由 broker 端参数log.segment.bytes
控制。
除了键、值和偏移量外,消息里还包含了消息大小、校验和、消息格式版本号、压缩算法和时间戳。时间戳可以是生产者发送消息的时间,也可以是消息到达 broker 的时间,这个是可配置的。
如果生产者发送的是压缩过的消息,那么同一个批次的消息会被压缩在一起。broker 会原封不动的将消息存入磁盘,然后再把它发送给消费者。消费者在解压这个消息之后,会看到整个批次的消息,它们都有自己的时间戳和偏移量。
这意味着 broker 可以使用zero-copy
技术给消费者发送消息,同时避免了对生产者已经压缩过的消息进行解压和再压缩。
索引文件
位移索引文件(.index)与时间戳索引(.timeindex)是两个特殊的索引文件:
- 前者可以帮助快速定位记录所在的物理文件位置
- 后者则是根据给定的时间戳查找对应的位移信息
它们都属于稀疏索引文件,每写入若干条记录后才增加一个索引项。写入间隔可以 broker 端参数 log.index.interval.bytes
设置。
索引文件严格按照时间戳顺序保存,因此 Kafka 可以利用二分查找算法提高查找速度。
KafkaBroker 简析的更多相关文章
- 简析.NET Core 以及与 .NET Framework的关系
简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...
- 简析 .NET Core 构成体系
简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...
- RecycleView + CardView 控件简析
今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...
- Java Android 注解(Annotation) 及几个常用开源项目注解原理简析
不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...
- PHP的错误报错级别设置原理简析
原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...
- Android 启动过程简析
首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...
- Android RecycleView + CardView 控件简析
今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...
- Java Annotation 及几个常用开源项目注解原理简析
PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...
- 【ACM/ICPC2013】POJ基础图论题简析(一)
前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...
随机推荐
- charles配置
https://www.cnblogs.com/ceshijiagoushi/p/6812493.html https://www.zzzmode.com/mytools/charles/
- Maven 依赖机制
概述 在 Maven 依赖机制的帮助下自动下载所有必需的依赖库,并保持版本升级.让我们看一个案例研究,以了解它是如何工作的.假设你想使用 Log4j 作为项目的日志.这里你要做什么? 传统方式 访问 ...
- 3.kafka安装配置
kafka安装配置 ### 1.集群规划 hadoop102 hadoop103 hadoop104 zk zk zk kafka kafka kafka jar包下载 http://kafka.ap ...
- Lucene 查询原理 传统二级索引方案 倒排链合并 倒排索引 跳表 位图
提问: 1.倒排索引与传统数据库的索引相比优势? 2.在lucene中如果想做范围查找,根据上面的FST模型可以看出来,需要遍历FST找到包含这个range的一个点然后进入对应的倒排链,然后进行求并集 ...
- 苹果 M1 芯片 OpenSSL 性能测试
Apple M1(MacBook Air 2020) type 16 bytes 64 bytes 256 bytes 1024 bytes 8192 bytes md2 0.00 0.00 0.00 ...
- p2p nat 穿透原理
nat 打洞穿透原理,需要服务端. 假设有A.B两个客户端和S一个服务器 Step 1 : A.B发送UDP请求给S,S知道了A.B在公网的IP和端口. Step 2: A从S中取B在公网的IP和端口 ...
- tcpdump 参数详解及使用案例
参数 -A 以ASCII码方式显示每一个数据包(不会显示数据包中链路层头部信息). 在抓取包含网页数据的数据包时, 可方便查看数据(nt: 即Handy for capturing web pages ...
- loj10003加工生产调度
题目描述 某工厂收到了 n个产品的订单,这 个产品分别在 A.B 两个车间加工,并且必须先在 A 车间加工后才可以到 B 车间加工. 某个产品 i 在 A,B 两车间加工的时间分别为 A_i,B_i ...
- 成功解决Git:fatal: refusing to merge unrelated histories
Get 报错 如果合并了两个不同的开始提交的仓库,在新的 git 会发现这两个仓库可能不是同一个,为了防止开发者上传错误,于是就给下面的提示 fatal: refusing to merge unre ...
- Kali-2020 配置Docker
Kali 2020 安装Docke 为什么在Kali上安装Docker? Kali有很多工具,但是您想运行一个不包含的工具,最干净的方法是通过Docker容器.例如,我正在研究一个名为vulhub的靶 ...