kafka一篇文章解答90%以上面试问题

kafka整体架构

整体架构分为producer、broker、consumer三部分,3.0版本之前依赖zookeeper做集群管理,3.0版本之后通过KRaft进行集群管理。

consumer有消费者组概念,同一个组内不同消费者负责消费不同的partation,一个分区只能由一个组内消费者消费;消费者组之间互不影响

集群中的broker会选举出一个leader作为Controller负责管理整个集群中所有分区和副本的状态

每个topic由多个partation组成,partation为真实存储数据的地方,每个partation以文件夹的形式存储在文件系统中。每个对应的partation数据目录下存储*.index,*log ,*timeindex三个文件

每个partation都有对应的副本,分散在不同的broker中来实现分布式存储。

整体使用主写主读架构,通过partation分布不同的broker上,尽量保证每个broker既有replicas分区拉数据也有leader分区生产数据,实现负载

kafka的基本组件

1.  Broker:通俗理解成一台部署了kafka的服务器就是一个Broker,一个kafka集群由多个Broker组成,每个Broker包含多个Topic

2.  Controller:broker的领导者,主写主读,它负责管理整个集群中所有分区和副本的状态

3.  Producer:消息生产者,自己决定向哪个partaion发送数据,hash或轮询

4.  Consumer:消息消费者,通过zookeeper维护offset

5.  Consumer Group:消费者组,同一个组内不同消费者负责消费不同的partation,也就是一个分区只能由一个组内消费者消费;消费者组之间互不影响。每条消息只能被Consumer Group中的一个Consumer消费;但是可以被多个Consumer Group组消费

6.  Topic:消息主题,一类消息的总称/消息队里,逻辑概念,真实数据存放在partation中,一个 topic 由多个 partions 组成

7.  Partation:分区,真实存储数据的地方,负载均衡与扩展性考虑,一个Topic可以分为多个Partition,物理存储在Kafka集群中的多个Broker上。可靠性上考虑,每个Partition都会有备份Replica。partation保持分区顺序

8.  Replica副本:Partition的副本,为了保证集群中的某个节点发生故障时,该节点上的Partition数据不会丢失,且Kafka仍能继续工作,所以Kafka提供了副本机制,一个Topic的每个Partition都有若干个副本,一个Leader和若干个Follower。

9.  Leader:Replica的主角色,Producer与Consumer只跟Leader交互。

10. Follwer:Replica的从角色,实时从Leader中同步数据,保持和Leader数据的同步。Leader发生故障时,经过一系列选举算法,某个Follower会变成新的Leader。

11. Offset:每个分区日志都有一个offset来唯一的标记一条消息,offset的值为8个字节的数字,表示此消息在此partition中所处的起始位置

kafka数据存储原理

1. partation为真实存储数据的地方,每个partation以文件夹的形式存储在文件系统中。每个对应的每个partation文件夹下的日志被分割成很多segment段。

2. 日志分段名通过偏移量确定,比如segment1的段号是509,segment2的段号是1397,那么segment1就存储了偏移量509-1397的消息。

3. 定位到段后通过稀疏索引的方式,也就是利用*.index文件。之所以成为稀疏索引是因为并没有维护所有数据的索引,定位数据的时候要通过二分查找的方式定位索引的位置,再通过索引对应的真实数据的位置回表查询。

4. *.timeindex 和kafka清理数据有着密切的关系,kafka默认保留7天内的数据,对于超过7天的数据,会被清理掉,这里的清理逻辑主要根据timeindex时间索引文件里最大的时间来判断的,如果最大时间与当前时间差值超过7天,那么对应的数据段就会被清理掉

5.由于生产者生产的消息会不断追加到 log 文件末尾,为防止log文件过大导致数据定位效率低下,Kafka采取了分段和索引机制,

6.partition 在服务器上的表现形式就是一个一个的文件夹(命名规则:topic名称+分区序号),每个 partition 文件夹下面会有多组 segment(逻辑分组,并不是真实存在),每个 segment 对应三个文件 (.log文件、.index文件、.timeindex文件)。

7.log文件就实际是存储 message 的地方,而 index 和 timeindex 文件为索引文件,用于检索消息。

如上图,这个 partition 有三组 segment 文件,每个 log 文件的大小是一样的,但是存储的 message 数量是不一定相等的(每条的 message 大小不一致)。文件的命名是以该 segment 最小 offset 来命名的,如 000.index 存储offset为0~368795的消息,kafka就是利用分段+索引的方式来解决查找效率的问题。

Message结构

log 文件就是实际存储 message 的地方,在 producer 往 kafka 写入的也是一条一条的 message,消息主要包含消息体、消息大小、offset、压缩类型…等!主要是下面三个:

1、 offset:offset是一个占8byte的有序id号,它可以唯一确定每条消息在parition内的位置。

2、 消息大小:消息大小占用4byte,用于描述消息的大小。

3、 消息体:消息体存放的是实际的消息数据(被压缩过),占用的空间根据具体的消息而不一样。

存储策略

  无论消息是否被消费,kafka都会保存所有的消息。那对于旧数据有什么删除策略?

1、 基于时间,默认配置是168小时(7天)。

2、 基于大小,默认配置是1073741824。

索引机制

在kafka中,每个日志分段文件都对应了两个索引文件——.inde偏移量索引文件和 .timeindex时间戳索引文件,主要用来提高查找消息的效率。

偏移量索引文件:用来建立消息偏移量(offset)到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置。

时间戳索引文件:则根据指定的时间戳(timestamp)来查找对应的偏移量信息。

Kafka 中的索引文件,以稀疏索引(sparse index)的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量(由 broker 端参数 log.index.interval.bytes 指定,默认值为 4096,即 4KB)的消息时,偏移量索引文件 和 时间戳索引文件 分别增加一个偏移量索引项和时间戳索引项,增大或减小 log.index.interval.bytes 的值,对应地可以缩小或增加索引项的密度。

稀疏索引通过 MappedByteBuffer 将索引文件映射到内存中,以加快索引的查询速度。

偏移量索引文件中的偏移量是单调递增的,查询指定偏移量时,使用二分查找法来快速定位偏移量的位置,如果指定的偏移量不在索引文件中,则会返回小于指定偏移量的最大偏移量。

时间戳索引文件中的时间戳也保持严格的单调递增,查询指定时间戳时,也根据二分查找法来查找不大于该时间戳的最大偏移量,至于要找到对应的物理文件位置还需要根据偏移量索引文件来进行再次定位。

稀疏索引的方式是在磁盘空间、内存空间、查找时间等多方面之间的一个折中。

如何通过offset找到对应的消息?

1、 先找到 offset=3 的 message 所在的 segment文件(利用二分法查找),先判断.index文件名称offset(baseOffset )是否小于3;

若小于,则继续二分与下一个.inde文件名称offset比较;

若大于,则返回上次小于3的.index文件,这里找到的就是在第一个segment文件。

2、找到的 segment 中的.index文件,用查找的offset 减去.index文件名的offset,也就是00000.index文件,我们要查找的offset为3的message在该.index文件内的索引为3(index采用稀疏存储的方式,它不会为每一条message都建立索引,而是每隔4k左右,建立一条索引,避免索引文件占用过多的空间。缺点是没有建立索引的offset不能一次定位到message的位置,需要做一次顺序扫描,但是扫描的范围很小)。

3、 根据找到的相对offset为3的索引,确定message存储的物理偏移地址为756。

4、 根据物理偏移地址,去.log文件找相应的Message

offset篇

1、offset概念

1.1 生产者offset:

producer 向 topic 中写数据,是以磁盘顺序追加的方式写入,写入的时候就会附加offset来确定消息索引。

1.2 消费者offset:

consumer 从 topic 中读数据,会根据不同节点数据的offset的读取,多节点offset不连续,想实现连续offset的读取方式,就设置单broker,但也就失去了kafka分布式高性能的特点。

四种状态(数据准确,容灾要考虑的重点)

1、Last Committed Offset:consumer group 最新一次 commit 的 offset,表示这个 group 已经把 Last Committed Offset 之前的数据都消费成功了。

2、Current Position:consumer group 当前消费数据的 offset,也就是说,Last Committed Offset 到 Current Position 之间的数据已经拉取成功,可能正在处理,但是还未 commit。

3、Log End Offset(LEO):记录底层日志 (log) 中的下一条消息的 offset。, 对 producer 来说,就是即将插入下一条消息的 offset。

4、High Watermark(HW):已经成功备份到其他 replicas 中的最新一条数据的 offset,也就是说 Log End Offset 与 High Watermark 之间的数据已经写入到该 partition 的 leader 中,但是还未完全备份到其他的 replicas 中,consumer 是无法消费这部分消息 (未提交消息)。

2、offset提交方式

三种

1、自动提交方式

“enable.auto.commit”, “true”

是否自动提交

“auto.commit.interval.ms”, “5000”

间隔多久ms提交

2、手动提交 —— 同步

consumer.commitSync();

3、手动提交 —— 异步

consumer.commitAsync();

3、3种提交方式优缺点

3.1、自动提交

优点:不用自己管理offset

缺点:可能会出现数据重复

原因:

调用poll()方法时将offset提交,所以如果没有调用poll(),时间到了也不会提交。

这就会导致时间偏差,在默认配置下,假如此批数据offset为101,消费处理需要20s,而你在10s的时候应用挂掉,就会导致此时offset没有提交,下次启动接着从101重新消费,而你先前处理掉的10s数据就会重复。

总结

保证了数据的不丢失,却无法保证不重复。

3.2、手动提交

同步提交方式只是保证了提交offset和kafka之间的事务性,就是调用poll函数之后等到反馈才会继续下一步。

异步相反。

优点:可以根据自己代码操作,将提交offset和消费处理实现原子性,保证数据不丢失不重复

缺点:自己管理offset,可能需要增加额外框架辅助

总结

对非去重的数据,还是手动提交比较安全。

4、offset管理方式

1、Spark Checkpoint:在 Spark Streaming 执行Checkpoint 操作时,将 Kafka Offset 一并保存到 HDFS 中。这种方式的问题在于:当 Spark Streaming 应用升级或更新时,以及当Spark 本身更新时,Checkpoint 可能无法恢复。因而,不推荐采用这种方式。

2、HBASE、Redis 等外部 NOSQL 数据库:这一方式可以支持大吞吐量的 Offset 更新,但它最大的问题在于:用户需要自行编写 HBASE 或 Redis 的读写程序,并且需要维护一个额外的组件。

3、ZOOKEEPER:kafka-0.10.1.X版本之前: auto.offset.reset 的值为smallest,和,largest.(offest保存在zk中),目录结构是 :/consumers/<group.id>/offsets/ / ,但是由于 ZOOKEEPER 的写入能力并不会随着 ZOOKEEPER 节点数量的增加而扩大,因而,当存在频繁的 Offset 更新时,ZOOKEEPER 集群本身可能成为瓶颈。因而,不推荐采用这种方式。

4、kafka-0.10.1.X版本之后: auto.offset.reset 的值更改为:earliest,latest,和none (offest保存在kafka的一个特殊的topic名为:__consumer_offsets里面);不需要手动编写 Offset 管理程序或者维护一套额外的集群,因而是迄今为止最为理想的一种实现方式。

kafka的消费者是pull(拉)还是push(推)模式,这种模式有什么好处?

kafka replicas是如何管理的

kafka为了保证数据安全性,在producer写入数据时会通过副本机制对当前数据进行复制备份,其他分区副本通过拉取的方式进行数据同步,依赖多副本机制进行故障转移。

HW: 高水位,标识consumer可见的offset,取所有ISR中最小的那个,只有所有的副本都同步完成HW才会增加,消费者只能消费到HW之后的数据

LEO: 每个partation的log最后一条message位置

AR: 所有的分区副本集合

ISR: 同步的分区集合队列,属于AR的一个子集,ISR中如果同步慢了或挂起会被t出ISR队列。

OSR:从同步队列中被踢出的分区集合

当partation leader挂掉后由Controller在ISR集合中顺序查找出第一个选举新leader

Kafka 数据一致性原理

一致性就是说不论是老的 Leader 还是新选举的 Leader,Consumer 都能读到一样的数据。

假设分区的副本为3,其中副本0是 Leader,副本1和副本2是 follower,并且在 ISR 列表里面。虽然副本0已经写入了 Message4,但是 Consumer 只能读取到 Message2。因为所有的 ISR 都同步了 Message2,只有 High Water Mark 以上的消息才支持 Consumer 读取,而 High Water Mark 取决于 ISR 列表里面偏移量最小的分区,对应于上图的副本2,这个很类似于木桶原理。

这样做的原因是还没有被足够多副本复制的消息被认为是“不安全”的,如果 Leader 发生崩溃,另一个副本成为新 Leader,那么这些消息很可能丢失了。如果我们允许消费者读取这些消息,可能就会破坏一致性。试想,一个消费者从当前 Leader(副本0) 读取并处理了 Message4,这个时候 Leader 挂掉了,选举了副本1为新的 Leader,这时候另一个消费者再去从新的 Leader 读取消息,发现这个消息其实并不存在,这就导致了数据不一致性问题。

当然,引入了 High Water Mark 机制,会导致 Broker 间的消息复制因为某些原因变慢,那么消息到达消费者的时间也会随之变长(因为我们会先等待消息复制完毕)。延迟时间可以通过参数 replica.lag.time.max.ms 参数配置,它指定了副本在复制消息时可被允许的最大延迟时间。

kafka如何保证数据不丢失

Producer保证发送数据不丢,生产者发送消息有三种模式,`发完即忘``同步``异步`,可以通过设置同步或异步的方式获取响应结果,失败做重试来保证消息在发送阶段不丢(broker接受produer数据做了幂等性保证)

Broker保证接收数据保证不丢失,当生产者向leader发数据时通过request.required.acks参数设置数据可靠性的级别。

1(默认):producer在ISR中的leader已成功收到的数据并得到确认后发送下一条message。如果leader宕机了,则会丢失数据。

0:producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的

-1或者all:producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。通过设置ack=1,broker内部做副本同步保证broker内部数据不丢失。

Consumer保证消费数据不丢失,默认情况下,当消费者消费到消息后,会自动提交offset。但是如果消费者消费出错,没有进入真正的业务处理,那么就可能会导致这条消息消费失败,从而丢失。可以通过开启手动提交位移,等待业务正常处理完成后,再提交offset。

kafka为什么那么快,吞吐量高

1. kafka生产消息时通过异步发送机制,首先通过main线程将数据缓存起来,sender线程批量搬运数据,broker定时去poll数据。

2. 数据批量读写、批量压缩,消息发送到broker之前会压缩消息,达到一定数据量压缩一次性发送。

3. 顺序写磁盘:新的消息顺序添加到日志文件末尾,而且磁盘上的 数据不会一直存着,后台会维护一个线程 来定期检测是否有数据该删除。

4. PageCache页缓存:充分利用Linux操作系统对磁盘的访问优化,Cache层在内存中缓存了磁盘上的部分数据。(类似mysql的bufferpool)Broker收到数据后先将生产者的数据写入page cache,再定期刷到磁盘中

5. 零拷贝技术:所谓的零拷贝是指将数据在内核空间直接从磁盘文件复制到网卡中,而不需要经由用户态的应用程序之手。这样既可以提高数据读取的性能,也能减少核心态和用户态之间的上下文切换,提高数据传输效率。

(1) 传统的拷贝

传统的数据文件拷贝过程如下图所示,大概可以分成四个过程:

磁盘----》read buffer-----》application buffer-------》socket buffer---------》网卡-------》发送给消费者

(2)Kafka零拷贝过程

在正式介绍零拷贝结束(Zero-Copy)之前,我们先简单介绍一下DMA(Direct Memory Access)技术。DMA,又称之为直接内存访问,是零拷贝技术的基石。DMA 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。因此通过DMA,硬件则可以绕过CPU,自己去直接访问系统主内存。很多硬件都支持DMA,其中就包括网卡、声卡、磁盘驱动控制器等。

有了DMA技术的支持之后,网卡就可以直接区访问内核空间的内存,这样就可以实现内核空间和应用空间之间的零拷贝了,极大地提升传输性能。下图展示了Kafka零拷贝的数据传输过程。数据传输的的过程就简化成了:

磁盘------》readbuff--------》网卡推送

通过零拷贝技术,就不需要把 内核空间页缓存里的数据拷贝到应用层缓存,再从应用层缓存拷贝到 Socket 缓存了,两次拷贝都省略了,所以叫做零拷贝。这个过程大大的提升了数据消费时读取文件数据的性能。Kafka 从磁盘读数据的时候,会先看看内核空间的页缓存中是否有,如果有的话,直接通过网关发送出去。

6. 数据分区分段 + 稀疏索引:Kafka 的 message 消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的 segment。为了进一步的查询优化,Kafka 又默认为分段后的数据文件建立了索引文件,就是文件系统上的 .index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。

kafka rebalance

1. consumer group多个消费者组成起来的一个组,它们共同消费 topic 的所有消息,并且一个 topic 的一个 partition 只能被一个 consumer 消费。reblance就是为了kafka对提升消费效率做的优化,规定了一个ConsumerGroup下的所有consumer均匀分配订阅 Topic 的每个分区。

2. 触发时机:①新consumer加入consumer group ②组内consumer离开或崩溃

3. 触发原因:生产环境一般出现rebalance现象大部分原因是`消费者心跳超时`、`消费者消费数据超时`

4. 主要参数:

session.timeout.ms 表示 consumer 向 broker 发送心跳的超时时间。例如 session.timeout.ms = 180000 表示在最长 180 秒内 broker 没收到 consumer 的心跳,那么 broker 就认为该 consumer 死亡了,会启动 rebalance。

heartbeat.interval.ms 表示 consumer 每次向 broker 发送心跳的时间间隔。heartbeat.interval.ms = 60000 表示 consumer 每 60 秒向 broker 发送一次心跳。一般来说,session.timeout.ms 的值是 heartbeat.interval.ms 值的 3 倍以上。

max.poll.interval.ms 表示 consumer 每两次 poll 消息的时间间隔。简单地说,其实就是 consumer 每次消费消息的时长。如果消息处理的逻辑很重,那么市场就要相应延长。否则如果时间到了 consumer 还么消费完,broker 会默认认为 consumer 死了,发起 rebalance。

max.poll.records 表示每次消费的时候,获取多少条消息。获取的消息条数越多,需要处理的时间越长。所以每次拉取的消息数不能太多,需要保证在 max.poll.interval.ms 设置的时间内能消费完,否则会发生 rebalance。

5. 解决方案:

心跳超时就调整session.timeout.msheartbeat.interval.ms.

消费处理超时一般是增加消费者处理的时间(max.poll.interval.ms),减少每次处理的消息数(max.poll.records)

如何增加消费能力

1. 可以考虑增加 topic 的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。

2. 如果是消费者消费不及时,可以采用多线程的方式进行消费,并且优化业务方法流程,同样的分区数,查看为什么并发那么高。

kafka数据倾斜怎么办

1. 刚才提到kafka broker内部结构会出现随着topic数量不断增多,每个topic的分区数量又不一致,最终就会出现topic分区在Kafka集群内分配不均的情况。

2. 比如:topic1是10个分区、topic2是15个分区、topic3是3个分区,集群有6台机器。那6台broker上总会有4台broker有两个topic1的分区,有3台broke上有3个topic3分区等等。这样就会导致分区多的broker上的出入流量可能要比其他broker上要高,最终导致资源问题。

3. 出现这种情况如果仅仅知识新增broker扩展并不会起作用,要手动编辑内置副本迁移脚本`vi topic-reassignment.json`手动调整各broker与partation的关系。当然网上也有很多自动迁移工具。

4. 最近很火的pulsar天然支持动态伸缩能力,就不用这么费劲

分区数越多越好吗

1. 在一定条件下,分区数的数量是和吞吐量成正比的,分区数和性能也是成正比。

2. 超过了一定限度,客户端和服务端需要使用的内存会激增

服务端在很多组件中都维护了分区级别的缓存,分区数越大,缓存成本也就越大。

消费端的消费线程数是和分区数挂钩的,分区数越大消费线程数也就越多,线程的开销成本也就越大

生产者发送消息有缓存的概念,会为每个分区缓存消息,当积累到一定程度或者时间时会将消息发送到分区,分区越多,这部分的缓存也就越大

3. 文件句柄的开销,partation底层存储对应一个log文件,文件句柄数量增加

4. 增加数据同步负担,降低高可用

kafka与zookeeper之间的关系

在kafka中,zookeeper主要负责这几个部分:

 broker注册

从上面的图中可以看到,broker分布式部署,就需要一个注册中心来进行统一管理。Zookeeper用一个专门节点保存Broker服务列表,也就是  /brokers/ids。

broker在启动时,向Zookeeper发送注册请求,Zookeeper会在/brokers/ids下创建这个broker节点,如/brokers/ids/[0...N],并保存broker的IP地址和端口。

这个节点临时节点,一旦broker宕机,这个临时节点会被自动删除。

topic注册

Zookeeper也会为topic分配一个单独节点,每个topic都会以/brokers/topics/[topic_name]的形式记录在Zookeeper。

一个topic的消息会被保存到多个partition,这些partition跟broker的对应关系也需要保存到Zookeeper。

partition是多副本保存的,上图中红色partition是leader副本。当leader副本所在的broker发生故障时,partition需要重新选举leader,这个需要由Zookeeper主导完成。

broker启动后,会把自己的Broker ID注册到到对应topic节点的分区列表中。

当broker退出后,Zookeeper会更新其对应topic的分区列表。

consumer注册

消费者组也会向Zookeeper进行注册,Zookeeper会为其分配节点来保存相关数据,节点路径为/consumers/{group_id},有3个子节点,如下图:

这样Zookeeper可以记录分区跟消费者的关系,以及分区的offset。

负载均衡

broker向Zookeeper进行注册后,生产者根据broker节点来感知broker服务列表变化,这样可以实现动态负载均衡。

consumer group中的消费者,可以根据topic节点信息来拉取特定分区的消息,实现负载均衡。

实际上,Kafka在Zookeeper中保存的元数据非常多,看下面这张图:

随着broker、topic和partition增多,保存的数据量会越来越大。

Zookeeper带来的问题

Kafka本身就是一个分布式系统,但是需要另一个分布式系统来管理,复杂性无疑增加了。

4.1 运维复杂度

使用了Zookeeper,部署Kafka的时候必须要部署两套系统,Kafka的运维人员必须要具备Zookeeper的运维能力。

4.2 Controller故障处理

Kafaka依赖一个单一Controller节点跟Zookeeper进行交互,如果这个Controller节点发生了故障,就需要从broker中选择新的Controller。如下图,新的Controller变成了broker3。

新的Controller选举成功后,会重新从Zookeeper拉取元数据进行初始化,并且需要通知其他所有的broker更新ActiveControllerId。老的Controller需要关闭监听、事件处理线程和定时任务。分区数非常多时,这个过程非常耗时,而且这个过程中Kafka集群是不能工作的。

4.3 分区瓶颈

当分区数增加时,Zookeeper保存的元数据变多,Zookeeper集群压力变大,达到一定级别后,监听延迟增加,给Kafaka的工作带来了影响。

所以,Kafka单集群承载的分区数量是一个瓶颈。而这又恰恰是一些业务场景需要的。

5.升级

升级前后的架构图对比如下:

KIP-500用Quorum  Controller代替之前的Controller,Quorum中每个Controller节点都会保存所有元数据,通过KRaft协议保证副本的一致性。这样即使Quorum  Controller节点出故障了,新的Controller迁移也会非常快。

官方介绍,升级之后,Kafka可以轻松支持百万级别的分区。

Kafak团队把通过Raft协议同步数据的方式Kafka Raft Metadata mode,简称KRaft

Kafka的用户体量非常大,在不停服的情况下升级是必要的。

目前去除Zookeeper的Kafka代码KIP-500已经提交到trunk分支,并且计划在未来的2.8版本发布。

Kafaka计划在3.0版本会兼容Zookeeper Controller和Quorum Controller,这样用户可以进行灰度测试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值