Kafka基础
1|0为什么要用Kafka
- 应用往Kafka写数据的原因有很多:用户行为分析、日志存储、异步通信等。多样化的使用场景带来了多样化的需求:消息是否能丢失?是否容忍重复?消息的吞吐量?消息的延迟
2|0一、kafka介绍
-
Kafka属于Apache组织,是一个高性能跨语言分布式发布订阅消息队列系统[7]。它的主要特点有:
-
以时间复杂度O(1)的方式提供消息持久化能力,并对大数据量能保证常数时间的访问性能;
高吞吐率,单台服务器可以达到每秒几十万的吞吐速率;
支持服务器间的消息分区,支持分布式消费,同时保证了每个分区内的消息顺序;
轻量级,支持实时数据处理和离线数据处理两种方式 -
Kafka集群:
- Kafka运行在一个或者多个服务器上作为集群运行
- kafka 集群存储消息记录的目录被称为topics
- 每一条消息记录包含三个要素:键(key)、值(value)、时间戳(Timestamp)
-
根据官方的说法,kafka是一个分布式流处理平台。其有3个关键功能:
1、订阅发布记录流:类似于消息队列 或 企业消息传递系统;
2、以容错的方式存储记录流;
3、实时记录流;
- 应用
- 作为消息系统、存储系统、流处理器。可以建立流数据管道,可靠性的债系统或应用之间获取数据。建立流式应用传输和响应数据。
- kafka作为消息系统的基本组件
-
有3个基本组件:Producer、Broker、Consumer
-
Producer:发布消息的客户端
-
Broker:一个从生产者接受并存储消息的客户端
-
Consumer:消费者从Broker中读取消息
-
-
- kafka消息队列的两种模式(可以根据消息传递的模式分为)
- 点对点模式
- 发布/订阅模式
2|11.1 基本概念和四大核心API
1.1.1 基本概念
-
Kafka中包含的基本概念:
-
Producer(生产者)
- 生产者,即消息的发布者,其会将某 topic 的消息发布到相应的 partition 中。生产者在默认情况下把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。不过,在某些情况下,生产者会把消息直接写到指定的分区
-
Consumer(消费者)
- 消费者,即消息的使用者,一个消费者可以消费多个 topic 的消息,对于某一个 topic 的消息,其只会消费同一个 partition 中的消息
-
Consumer Group(消费者组)
- 对于一个group而言,消费者的数量不应该多余分区的数量,因为在一个group中,每个分区至多只能绑定到一个消费者上,即一个消费者可以消费多个分区,一个分区只能给一个消费者消费
- 因此,若一个group中的消费者数量大于分区数量的话,多余的消费者将不会收到任何消息
-
Broker(代理服务器)
- Kafka 集群包含一个或多个服务器,每一个 Kafka 服务器都被称为一个 broker。broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息
-
Topic(主题)
- Topic 被称为主题,在 kafka 中,使用一个类别属性来划分消息的所属类,划分消息的这个类称为 topic。topic 相当于消息的分配标签,是一个逻辑概念。主题好比是数据库的表,或者文件系统中的文件夹
-
Partition(分区)
- partition 译为分区,topic 中的消息被分割为一个或多个的 partition,它是一个物理概念,对应到系统上的就是一个或若干个目录,一个分区就是一个 提交日志。消息以追加的形式写入分区,先后以顺序的方式读取
- 一个Topic中的消息数据按照多个分区组织,分区是kafka消息队列组织的最小单位,一个分区可以看作是一个有序的消息队列(先入先出)
- 注意:由于一个主题包含无数个分区,因此无法保证在整个 topic 中有序,但是单个 Partition 分区可以保证有序。消息被迫加写入每个分区的尾部。Kafka 通过分区来实现数据冗余和伸缩性
- 分区可以分布在不同的服务器上,也就是说,一个主题可以跨越多个服务器,以此来提供比单个服务器更强大的性能
-
Replica(副本)
- 为保障集群总某个节点发生故障,该节点的partition数据不丢失,kafka提供的容灾机制
-
Leader 和 Follower(领导者和从属): 副本中的一个负责处理读写请求,其他副本从中同步数据
-
segment
- Segment 被译为段,将 Partition 进一步细分为若干个 segment,每个 segment 文件的大小相等
-
1.1.2 核心API
- kafka有4个核心API:Producer API、Consumer API、Streams API、Connector API
- Producer API:它允许应用程序向一个或多个 topics 上发送消息记录;
- Consumer API:允许应用程序订阅一个或多个 topics 并处理为其生成的记录流;
- Streams API:它允许应用程序作为流处理器,从一个或多个主题中消费输入流并为其生成输出流,有效的将输入流转换为输出流;
- Connector API:它允许构建和运行将 Kafka 主题连接到现有应用程序或数据系统的可用生产者和消费者。例如,关系数据库的连接器可能会捕获对表的所有更改
2|21.2 使用场景
1:在系统或应用程序之间构建可靠的用于传输实时数据的管道,消息队列功能
2:构建实时的流数据处理程序来变换或处理数据流,数据处理功能
2|31.3 kafka生产者
- 生产者流程如下图
-
流程分析:
-
首先,创建ProducerRecord必须包含Topic和Value,key和partition可选。然后,序列化key和value对象为ByteArray,并发送到网络。
-
接下来,消息发送到partitioner。如果创建ProducerRecord时指定了partition,此时partitioner啥也不用做,简单的返回指定的partition即可。如果未指定partition,partitioner会基于ProducerRecord的key生成partition。producer选择好partition后,增加record到对应topic和partition的batch record。最后,专有线程负责发送batch record到合适的Kafka broker。
-
当broker收到消息时,它会返回一个应答(response)。如果消息成功写入Kafka,broker将返回RecordMetadata对象(包含topic,partition和offset);相反,broker将返回error。这时producer收到error会尝试重试发送消息几次,直到producer返回error。
-
-
实例化producer后,接着发送消息。这里主要有3种发送消息的方法:
-
立即发送:只管发送消息到server端,不care消息是否成功发送。大部分情况下,这种发送方式会成功,因为Kafka自身具有高可用性,producer会自动重试;但有时也会丢失消息;
-
同步发送:通过send()方法发送消息,并返回Future对象。get()方法会等待Future对象,看send()方法是否成功;
-
异步发送:通过带有回调函数的send()方法发送消息,当producer收到Kafka broker的response会触发回调函数
-
-
以上所有情况,一定要时刻考虑发送消息可能会失败,想清楚如何去处理异常
-
通常我们是一个producer起一个线程开始发送消息。为了优化producer的性能,一般会有下面几种方式:
- 单个producer起多个线程发送消息;
- 使用多个producer。
2|41.4 kafka消费者
1.4.1 kafka的消费模式
-
kafka的消费模式总共有3种:最多一次,最少一次,正好一次。为什么会有这3种模式,是因为客户端处理消息,提交反馈(commit)这两个动作不是原子性。
- 1.最多一次:客户端收到消息后,在处理消息前自动提交,这样kafka就认为consumer已经消费过了,偏移量增加。
- 2.最少一次:客户端收到消息,处理消息,再提交反馈。这样就可能出现消息处理完了,在提交反馈前,网络中断或者程序挂了,那么kafka认为这个消息还没有被consumer消费,产生重复消息推送。
- 3.正好一次:保证消息处理和提交反馈在同一个事务中,即有原子性。
-
可以从这几个点出发,详细阐述了如何实现以上三种方式
(1)At-most-once(最多一次)
设置enable.auto.commit为ture
设置 auto.commit.interval.ms为一个较小的时间间隔.
client不要调用commitSync(),kafka在特定的时间间隔内自动提交。
(2)At-least-once(最少一次)
方法一
设置enable.auto.commit为false
client调用commitSync(),增加消息偏移;方法二
设置enable.auto.commit为ture
设置 auto.commit.interval.ms为一个较大的时间间隔.
client调用commitSync(),增加消息偏移;
(3)Exactly-once(正好一次)
思路:
如果要实现这种方式,必须自己控制消息的offset,自己记录一下当前的offset,对消息的处理和offset的移动必须保持在同一个事务中,例如在同一个事务中,把消息处理的结果存到mysql数据库同时更新此时的消息的偏移。实现方法:
设置enable.auto.commit为false
保存ConsumerRecord中的offset到数据库
当partition分区发生变化的时候需要rebalance,有以下几个事件会触发分区变化
1 consumer订阅的topic中的分区大小发生变化
2 topic被创建或者被删除
3 consuer所在group中有个成员挂了
4 新的consumer通过调用join加入了group
此时 consumer通过实现ConsumerRebalanceListener接口,捕捉这些事件,对偏移量进行处理。consumer通过调用seek(TopicPartition, long)方法,移动到指定的分区的偏移位置。
1.4.2 消费机制
-
1、一个消息只能被同一个消费组的某个消费者消费,消费后不删除消息只是自己消费消息的offset+1,并不会删除消息,可能消息还要被其它消费组消费,可配置清除默认7天或基于大小来清除老消息。
-
2、可以手动指定那些partition由组内哪个消费者消费,不指定会首次某个消费者消费某个partition后不再改变,如果消费者offline或online会进行Rebalance力求负载均衡。
-
3、每个partition都维护有每个消费组的offset,故只能保证同一个partition的数据是顺序消费的,不能保证整个topic的消息顺序消费,这个顺序指的是partititon leader commit顺序,不是消息发送的顺序、即使同一个partition已不能绝对保证发送顺序与消费顺序一致,除非设置producer的retry次数为0,默认0。
-
4、对于一个消费者组而言,consumer个数不应该大于partition数,因为在一个group中,每个分区至多只能绑定到一个消费者上,即一个消费者可以消费多个分区,一个分区只能给一个消费者消费。如果consumer个数大于了partition数则部分consumer一直消费不到数据,处于空闲状态浪费资源,故分区数应>=同一个消费组中的消费者数
1.4.3 消费者与消费者组
-
消费者(Consumer)负责订阅 Kafka 中的主题(Topic),并从订阅的主题处获取消息。每个消费者都对应一个消费者组(Consumer Group)。在 Kafka 中消息被发布到主题后,这条消息只会被订阅该主题的消费者组的一个消费者消费,该消费者组中的其他消费者无法消费到它。
-
一个消费者组可以包含多个消费者,对于主题中的消息同一个消费者组的每个消费者消费一部分。也就是说,所有的消费者消费的消息合在一起才是一个主题的完整消息。这种消费者和消费者组的设计可以让整体的消费能力具有横向伸缩性,比如在主题消息量非常大的情况下,单个消费者处理该主题会非常吃力,可以增加更多的消费者,让它们分担负载,每个消费者只处理部分消息,这样就可以提高整体的消费能力。
-
对于多个消费者组订阅同一个主题,每个消费者组之间是互不影响的。如有消费者组A和消费者组B,同时订阅了一个主题TopicA,在 Kafka 中消费者组A会获取到 TopicA 中的所有消息,消费者B也会获取到 TopicA 的所有消息。由此可以知道每个消费者组是相互独立的,消费者组之间不会互相影响。
-
Kafka 是同时支持点对点模式的和发布/订阅模式两种模式,这些都是通过消费者和消费者组来实现的:
- 如果所有的消费者都在同一个消费者组,那么每个消息只会被一个消费者处理,这就相当于点对点模式的应用
- 如果所有的消费者不再同一个消费者组,那么所有的消息都会被广播给所有的消费者,这就相当于发布/订阅模式
-
消费者组是一个逻辑上的概念,它将属于它的消费者归为一类,每个消费者只属于一个消费组。每个消费者组都会有一个固定的名称,消费者在进行消费之前需要指定其所属消费者组的名称,这个通过消费者客户端参数 group.id 来配置
1.4.4 消费者组与分区重平衡
-
可以看到,当新的消费者加入消费组,它会消费一个或多个分区,而这些分区之前是由其他消费者负责的;另外,当消费者离开消费组(比如重启、宕机等)时,它所消费的分区会分配给其他分区。这种现象称为重平衡(rebalance)。重平衡是Kafka一个很重要的性质,这个性质保证了高可用和水平扩展。不过也需要注意到,在重平衡期间,所有消费者都不能消费消息,因此会造成整个消费组短暂的不可用。而且,将分区进行重平衡也会导致原来的消费者状态过期,从而导致消费者需要重新更新状态,这段期间也会降低消费性能。后面我们会讨论如何安全的进行重平衡以及如何尽可能避免。
-
消费者通过定期发送心跳(hearbeat)到一个作为组协调者(group coordinator)的broker来保持在消费组内存活。这个broker不是固定的,每个消费组都可能不同。当消费者拉取消息或者提交时,便会发送心跳。
-
如果消费者超过一定时间没有发送心跳,那么它的会话(session)就会过期,组协调者会认为该消费者已经宕机,然后触发重平衡。可以看到,从消费者宕机到会话过期是有一定时间的,这段时间内该消费者的分区都不能进行消息消费;通常情况下,我们可以进行优雅关闭,这样消费者会发送离开的消息到组协调者,这样组协调者可以立即进行重平衡而不需要等待会话过期。
-
在0.10.1版本,Kafka对心跳机制进行了修改,将发送心跳与拉取消息进行分离,这样使得发送心跳的频率不受拉取的频率影响。另外更高版本的Kafka支持配置一个消费者多长时间不拉取消息但仍然保持存活,这个配置可以避免活锁(livelock)。活锁,是指应用没有故障但是由于某些原因不能进一步消费
2|51.5 Broker
-
Kafka是一个高吞吐量分布式消息系统,采用Scala和Java语言编写,它提供了快速、可扩展的、分布式、分区的和可复制的日志订阅服务。它由Producer、Broker、Consumer三部分构成.
-
Producer向某个Topic发布消息,而Consumer订阅某个Topic的消息。 一旦有某个Topic新产生的消息,Broker会传递给订阅它的所有Consumer,每个Topic分为多个分区,这样的设计有利于管理数据和负载均衡。
-
Broker:消息中间件处理结点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。
-
Controller:中央控制器Control,负责管理分区和副本状态并执行管理着这些分区的重新分配。(里面涉及到partition leader 选举)
-
ISR:同步副本组
2|61.6 Topic
-
在Kafka中,消息是按Topic组织的.
-
Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。
-
Segment:partition物理上由多个segment组成
-
offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中,partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息。
1.6.1 topic中partition存储分布
- 在Kafka文件存储中,同一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为topic名称+有序序号,第一个partiton序号从0开始,序号最大值为partitions数量减1。
1.6.2 partiton中文件存储方式
-
每个partion(目录)由多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。
-
每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定
1.6.3 partiton中segment文件存储结构(了解)
-
partion中segment file的组成和物理结构。
-
segment file组成:由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀".index"和“.log”分别表示为segment索引文件、数据文件.
segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。 -
以一对segment file文件为例,说明segment中index<—->data file对应关系物理结构如下
- Index文件存储大量元数据,指向对应log文件中message的物理偏移地址。
- log数据文件存储大量消息
-
其中以Index文件中元数据3,497为例,依次在数据文件中表示第3个message(在全局partiton表示第368772个message)、以及该消息的物理偏移地址为497。
segment data file的内部
- segment data file由许多message组成,下面详细说明message物理结构如下:
关键字 | 解释说明 |
---|---|
8 byte offset | 该message在partition的offset |
4 byte message size | message大小 |
4 byte CRC32 | 用crc32校验message |
1 byte “magic” | 表示本次发布Kafka服务程序协议版本号 |
1 byte “attributes” | 表示为独立版本、或标识压缩类型、或编码类型 |
4 byte key length | 表示key的长度,当key为-1时,K byte key字段不填 |
K byte key | 可选 |
value bytes payload | 表示实际消息数据 |
1.6.4 在partition中如何通过offset查找message(了解)
-
例如读取offset=368776的message,需要通过下面2个步骤查找。
-
第一步查找segment file
- 以1.6.1节 topic中partition存储分布中的代码为例,其中00000000000000000000.index表示最开始的文件,起始偏移量(offset)为0.第二个文件00000000000000368769.index的消息量起始偏移量为368770 = 368769 + 1.同样,第三个文件00000000000000737337.index的起始偏移量为737338=737337 + 1,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据offset 二分查找 文件列表,就可以快速定位到具体文件。
- 当
offset=368776时定位到00000000000000368769.index|log
-
第二步通过segment file查找message
- 通过第一步定位到segment file,当
offset=368776
时,依次定位到00000000000000368769.index
的元数据物理位置(这个较小,可以放在内存中,直接操作)和00000000000000368769.log的物理偏移地址,然后再通过00000000000000368769.log
顺序查找 直到offset=368776
为止。
- 通过第一步定位到segment file,当
-
-
从上述1.6.3节可知这样做的优点,segment index file采取稀疏索引存储方式,它减少索引文件大小,通过map可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间
1.6.5 读写message总结(了解)
写message
- 消息从java堆转入page cache(即物理内存)。
由异步线程刷盘,消息从page cache刷入磁盘。
读message
-
消息直接从page cache转入socket发送出去。
-
当从page cache没有找到相应数据时,此时会产生磁盘IO,从磁盘Load消息到page cache,然后直接从socket发出去
Kafka高效文件存储设计特点
- topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
- 通过索引信息可以快速定位message和确定response的最大大小。
- 通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
- 通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小
3|0二、kafka的基本机制
3|12.1 kafka消息队列的两种模式
- 消息队列确实可以根据消息传递的模式分为
- 点对点模式
- 发布/订阅模式
- 下面2.2节及之后的部分介绍的是Kafka目前主要作为一个分布式的发布/订阅模式的消息系统(即消息中间件)使用时的基本机制
2.1.1 点对点模式(Point-to-Point,P2P)
- 在点对点模式中,有一个生产者(Producer)将消息发送到一个特定的队列(Queue)。
- 只有一个消费者(Consumer)可以接收和处理队列中的消息。
- 消息在队列中存储,一旦被消费者接收,就会从队列中删除。
- 这种模式适用于一对一的通信,其中生产者和消费者之间有直接的关联,通常用于任务分发和处理
2.1.2 发布/订阅模式(Publish/Subscribe,Pub/Sub)
- 在发布/订阅模式中,生产者将消息发布到一个主题(Topic)而不是队列。
- 多个消费者可以订阅一个或多个主题,以接收相关的消息。
- 消息广播给所有订阅了相应主题的消费者,每个消费者都会收到一份消息的拷贝
- 这种模式适用于一对多的通信,其中消息的发送者不需要关心谁会接收消息,通常用于事件处理、日志记录和实时通知等场景
2.1.3 小结
-
消息队列主要分为两种模式:点对点模式(一个生产者对口一个消费者)和发布/订阅模式(一对多)。
-
这两种模式有各自的优势和适用性,选择哪种模式取决于应用程序的需求。
-
点对点模式适用于有明确定位的消息接收者的情况
-
发布/订阅模式适用于需要将消息广播给多个订阅者的情况
-
3|22.2 消息传输流程
-
Producer即生产者,向Kafka集群发送消息,在发送消息之前,会对消息进行分类,即Topic,上图展示了两个producer发送了分类为topic1的消息,另外一个发送了topic2的消息。
-
Topic即主题,通过对消息指定主题可以将消息分类,消费者可以只关注自己需要的Topic中的消息
-
Consumer即消费者,消费者通过与kafka集群建立长连接的方式,不断地从集群中拉取消息,然后可以对这些消息进行处理。
-
从上图中就可以看出同一个Topic下的消费者和生产者的数量并不是对应的
3|32.3 kafka服务器消息存储策略
- 谈到kafka的存储,就不得不提到分区,即partitions,创建一个topic时,同时可以指定分区数目,分区数越多,其吞吐量也越大,但是需要的资源也越多,同时也会导致更高的不可用性,kafka在接收到生产者发送的消息之后,会根据均衡策略将消息存储到不同的分区中
-
在每个分区中,消息以顺序存储,最晚接收的的消息会最后被消费。
-
kafka中的message以topic的形式存在,topic在物理上又分为很多的partition,partition物理上由很多segment组成,segment是存放message的真正载体。
-
下面具体介绍下segment文件:
- (1) 每个partition(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。
- (2) 每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。
- (3) segment file组成:由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀”.index”和“.log”分别表示为segment索引文件、数据文件
- (4) segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。
-
segment中index<—->data file对应关系物理结构如下
-
index与log映射关系
- 从上图分析:
- .index文件存放的是message逻辑相对偏移量(相对offset=绝对offset-base offset)与在相应的.log文件中的物理位置(position)。但.index并不是为每条message都指定到物理位置的映射,而是以entry为单位,每条entry可以指定连续n条消息的物理位置映射(例如:假设有20000~20009共10条消息,.index文件可配置为每条entry
- 指定连续10条消息的物理位置映射,该例中,index entry会记录偏移量为20000的消息到其物理文件位置,一旦该条消息被定位,20001~20009可以很快查到。)。每个entry大小8字节,前4个字节是这个message相对于该log segment第一个消息offset(base offset)的相对偏移量,后4个字节是这个消息在log文件中的物理位置
3|42.4 与生产者的交互
- 生产者在向kafka集群发送消息的时候,可以通过指定分区来发送到指定的分区中
- 也可以通过指定均衡策略来将消息发送到不同的分区中
- 如果不指定,就会采用默认的随机均衡策略,将消息随机的存储到不同的分区中
3|52.5 与消费者的交互
- 在消费者消费消息时,kafka使用offset来记录当前消费的位置
- 在kafka的设计中,可以有多个不同的group(消费组)来同时消费同一个topic下的消息,如图,我们有两个不同的group同时消费,他们的的消费的记录位置offset各不项目,不互相干扰。
- 对于一个group而言,消费者的数量不应该大于分区的数量,因为在一个group中,每个分区至多只能绑定到一个消费者上,即一个消费者可以消费多个分区,一个分区只能给一个消费者消费
- 因此,若一个group中的消费者数量大于分区数量的话,多余的消费者将不会收到任何消息
__EOF__

本文链接:https://www.cnblogs.com/Mcoming/p/18087015.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下