Kafka 消费者

1. Kafka 消费方式

2 Kafka 消费者工作流程

2.1 消费者总体工作流程

2.2 消费者组原理

Consumer Group(CG):消费者组,由多个consumer组成。形成一个消费者组的条件,是所有消费者的groupid相同。

• 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费。

• 消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。

消费者组初始化流程

1、coordinator:辅助实现消费者组的初始化和分区的分配。

​ coordinator节点选择 = groupid的hashcode值 % 50( __consumer_offsets的分区数量)

​ 例如: groupid的hashcode值 = 1,1% 50 = 1,那么__consumer_offsets 主题的1号分区,在哪个broker上,就选择这个节点的coordinator

作为这个消费者组的老大。消费者组下的所有的消费者提交offset的时候就往这个分区去提交offset。

消费者组详细消费流程

2.3 消费者重要参数

-参数名称 -描述
bootstrap.servers 向 Kafka 集群建立初始连接用到的 host/port 列表。
key.deserializer 和value.deserializer 指定接收消息的 key 和 value 的反序列化类型。一定要写全类名。
group.id 标记消费者所属的消费者组。
enable.auto.commit 默认值为 true,消费者会自动周期性地向服务器提交偏移量。
auto.commit.interval.ms 如果设置了 enable.auto.commit 的值为 true, 则该值定义了消费者偏移量向 Kafka 提交的频率,默认 5s。
auto.offset.reset 当Kafka中没有初始偏移量或当前偏移量在服务器中不存在(如,数据被删除了),该如何处理?earliest:自动重置偏移量到最早的偏移量。latest:默认,自动重置偏移量为最新的偏移量。none:如果消费组原来的(previous)偏移量不存在,则向消费者抛异常。anything:向消费者抛异常。
offsets.topic.num.partitions __consumer_offsets 的分区数,默认是 50 个分区。
heartbeat.interval.ms Kafka 消费者和 coordinator 之间的心跳时间,默认 3s。该条目的值必须小于 session.timeout.ms ,也不应该高于session.timeout.ms的 1/3。
session.timeout.ms Kafka 消费者和 coordinator 之间连接超时时间,默认 45s。超过该值,该消费者被移除,消费者组执行再平衡。
max.poll.interval.ms 消费者处理消息的最大时长,默认是 5 分钟。超过该值,该消费者被移除,消费者组执行再平衡。
fetch.min.bytes 默认 1 个字节。消费者获取服务器端一批消息最小的字节数。
fetch.max.wait.ms 默认 500ms。如果没有从服务器端获取到一批数据的最小字节数。该时间到,仍然会返回数据。
fetch.max.bytes 默认Default:52428800(50m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)ormax.message.bytes(topicconfig)影响。
max.poll.records 一次 poll 拉取数据返回消息的最大条数,默认是 500 条。

3. 消费者 API

3.1 独立消费者案例(订阅主题)

1)需求:

创建一个独立消费者,消费 first 主题中数据。

注意在消费者 API 代码中必须配置消费者组 id。命令行启动消费者不填写消费者组id 会被自动填写随机的消费者组 id。

2)实现步骤

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Properties;public class CustomConsumer {public static void main(String[] args) {// 1.创建消费者的配置对象Properties properties = new Properties();// 2.给消费者配置对象添加参数properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");// 配置序列化 必须properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());// 配置消费者组(组名任意起名) 必须properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");// 创建消费者对象KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);// 注册要消费的主题(可以消费多个主题)ArrayList<String> topics = new ArrayList<>();topics.add("first");kafkaConsumer.subscribe(topics);// 拉取数据打印while (true) {// 设置 1s 中消费一批数据ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));// 打印消费到的数据for (ConsumerRecord<String, String> consumerRecord :consumerRecords) {System.out.println(consumerRecord);}}}
}

3)测试

在 Kafka 集群控制台,创建 Kafka 生产者,并输入数据

bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first
>hello

在 IDEA 控制台观察接收到的数据。

3.2 独立消费者案例(订阅分区)

1)需求:创建一个独立消费者,消费 first 主题 0 号分区的数据。

2)实现步骤

// 消费某个主题的某个分区数据ArrayList<TopicPartition> topicPartitions = new ArrayList<>();topicPartitions.add(new TopicPartition("first", 0));kafkaConsumer.assign(topicPartitions);

3.3 消费者组案例

1)需求:测试同一个主题的分区数据,只能由一个消费者组中的一个消费。

2)案例实操

(1)复制一份基础消费者的代码,在 IDEA 中同时启动,即可启动同一个消费者组中的两个消费者。

(2)启动代码中的生产者发送消息,在 IDEA 控制台即可看到两个消费者在消费不同分区的数据(如果只发生到一个分区,可以在发送时增加延迟代码 Thread.sleep(2);)。

(3)重新发送到一个全新的主题中,由于默认创建的主题分区数为 1,可以看到只能有一个消费者消费到数据

4 分区的分配以及再平衡

1、一个consumer group中有多个consumer组成,一个 topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个partition的数据。

2、Kafka有四种主流的分区分配策略: Range、RoundRobin、Sticky、CooperativeSticky。
可以通过配置参数partition.assignment.strategy,修改分区的分配策略。默认策略是Range + CooperativeSticky。Kafka可以同时使用多个分区分配策略。

-参数名称 -描述
heartbeat.interval.ms Kafka 消费者和 coordinator 之间的心跳时间,默认 3s。该条目的值必须小于 session.timeout.ms,也不应该高于session.timeout.ms 的 1/3。
session.timeout.ms Kafka 消费者和 coordinator 之间连接超时时间,默认 45s。超过该值,该消费者被移除,消费者组执行再平衡。
max.poll.interval.ms 消费者处理消息的最大时长,默认是 5 分钟。超过该值,该消费者被移除,消费者组执行再平衡。
partition.assignment.strategy 消费者分区分配策略,默认策略是Range +CooperativeSticky。Kafka可以同时使用多个分区分配策略。可以选择的策略包括:Range 、RoundRobin 、 Sticky 、CooperativeSticky

4.1 Range 以及再平衡

1Range分区策略原理

2Range分区分配策略案例

​ (1)修改主题 first 为 7 个分区。

bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --alter --topic first --partitions 7

注意:分区数可以增加,但是不能减少。

​ (2)复制 CustomConsumer 类,创建 CustomConsumer2。这样可以由三个消费者CustomConsumer、CustomConsumer1、CustomConsumer2 组成消费者组,组名都为“test”,同时启动 3 个消费者。

​ (3)启动 CustomProducer 生产者,发送 500 条消息,随机发送到不同的分区。

说明:Kafka 默认的分区分配策略就是 Range + CooperativeSticky,所以不需要修改策略。

​ (4)观看 3 个消费者分别消费哪些分区的数据。

3Range分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

​ 1 号消费者:消费到 3、4 号分区数据。

​ 2 号消费者:消费到 5、6 号分区数据。

​ 0 号消费者的任务会整体被分配到 1 号消费者或者 2 号消费者。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

​ 1 号消费者:消费到 0、1、2、3 号分区数据。

​ 2 号消费者:消费到 4、5、6 号分区数据。

​ 说明:消费者 0 已经被踢出消费者组,所以重新按照 range 方式分配。

4.2 RoundRobin 以及再平衡

1RoundRobin分区策略原理

2RoundRobin分区分配策略案例

(1)依次在 CustomConsumer、CustomConsumer1、CustomConsumer2 三个消费者代

码中修改分区分配策略为 RoundRobin

// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.RoundRobinAssignor");

(2)重启 3 个消费者,重复发送消息的步骤,观看分区结果。

3RoundRobin分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

​ 1 号消费者:消费到 2、5 号分区数据

​ 2 号消费者:消费到 4、1 号分区数据

​ 0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费。

​ 说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

​ 1 号消费者:消费到 0、2、4、6 号分区数据

​ 2 号消费者:消费到 1、3、5 号分区数据

说明:消费者 0 已经被踢出消费者组,所以重新按照 RoundRobin 方式分配。

4.3 Sticky 以及再平衡

粘性分区定义可以理解为分配的结果带有“粘性的”。即在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。

粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。

1)需求

​ 设置主题为 first,7 个分区;准备 3 个消费者,采用粘性分区策略,并进行消费,观察消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。

2)步骤

​ (1)修改分区分配策略为粘性。

​ 注意:3 个消费者都应该注释掉,之后重启 3 个消费者,如果出现报错,全部停止等会再重启,或者修改为全新的消费者组。

// 修改分区分配策略
ArrayList<String> startegys = new ArrayList<>();
startegys.add("org.apache.kafka.clients.consumer.StickyAssignor");
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, startegys);

​ (2)使用同样的生产者发送 500 条消息

​ 可以看到会尽量保持分区的个数近似划分分区。

3Sticky分区分配再平衡案例

​ (1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

​ 1 号消费者:消费到 2、5、3 号分区数据。

​ 2 号消费者:消费到 4、6 号分区数据。

0 号消费者的任务会按照粘性规则,尽可能均衡的随机分成 0 和 1 号分区数据,分别由 1 号消费者或者 2 号消费者消费。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

​ (2)再次重新发送消息观看结果(45s 以后)。

​ 1 号消费者:消费到 2、3、5 号分区数据。

​ 2 号消费者:消费到 0、1、4、6 号分区数据。

​ 说明:消费者 0 已经被踢出消费者组,所以重新按照粘性方式分配。

5. offset 位移

5.1 offset 的默认维护位置

​ __consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行compact,也就是每个 group.id+topic+分区号就保留最新数据。

1)消费 offset 案例

(0)思想:__consumer_offsets 为 Kafka 中的 topic,那就可以通过消费者进行消费。

(1)在配置文件 config/consumer.properties 中添加配置 exclude.internal.topics=false,默认是 true,表示不能消费系统主题。为了查看该系统主题数据,所以该参数修改为 false。

(2)采用命令行方式,创建一个新的 topic。

bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --topic atguigu --partitions 2 --replication-factor 2

(3)启动生产者往 atguigu 生产数据。

bin/kafka-console-producer.sh --topic atguigu --bootstrap-server hadoop102:9092

(4)启动消费者消费 atguigu 数据。

bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic atguigu --group test

注意:指定消费者组名称,更好观察数据存储位置(key 是 group.id+topic+分区号)。

(5)查看消费者消费主题__consumer_offsets。

bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server hadoop102:9092 --consumer.config config/consumer.properties --formatter
"kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning[offset,atguigu,1]::OffsetAndMetadata(offset=7,
leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203,
expireTimestamp=None)
[offset,atguigu,0]::OffsetAndMetadata(offset=8,
leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203,
expireTimestamp=None)

5.2 自动提交 offset

为了使我们能够专注于自己的业务逻辑,Kafka提供了自动提交offset的功能。5s

自动提交offset的相关参数:

  • enable.auto.commit:是否开启自动提交offset功能,默认是true

  • auto.commit.interval.ms:自动提交offset的时间间隔,默认是5s

-参数名称 -描述
enable.auto.commit 默认值为 true,消费者会自动周期性地向服务器提交偏移量。
auto.commit.interval.ms 如果设置了 enable.auto.commit 的值为 true, 则该值定义了消费者偏移量向 Kafka 提交的频率,默认 5s。

1)消费者自动提交 offset

 // 是否自动提交 offsetproperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);// 提交 offset 的时间周期 1000ms,默认 5sproperties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000)

5.3 手动提交 offset

​ 虽然自动提交offset十分简单便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机。因

此Kafka还提供了手动提交offset的API。

​ 手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。两者的相

同点是,都会将本次提交的一批数据最高的偏移量提交;不同点是,同步提交阻塞当前线程,一直到提交成

功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。

• commitSync(同步提交):必须等待offset提交完毕,再去消费下一批数据。

• commitAsync(异步提交) :发送完提交offset请求后,就开始消费下一批数据了。

1)同步提交 offset

​ 由于同步提交 offset 有失败重试机制,故更加可靠,但是由于一直等待提交结果,提

交的效率比较低。以下为同步提交 offset 的示例。

// 是否自动提交 offsetproperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);//消费// 同步提交 offset
consumer.commitSync();

2)异步提交offset

​ 虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会受到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。

// 是否自动提交 offsetproperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");//消费// 异步提交 offsetconsumer.commitAsync();

5.4 指定 Offset 消费

auto.offset.reset = earliest | latest | none 默认是 latest。

​ 当 Kafka 中没有初始偏移量(消费者组第一次消费)或服务器上不再存在当前偏移量时(例如该数据已被删除),该怎么办?

(1)earliest:自动将偏移量重置为最早的偏移量,–from-beginning。

(2)latest(默认值):自动将偏移量重置为最新偏移量。

(3)none:如果未找到消费者组的先前偏移量,则向消费者抛出异常。

(4)任意指定 offset 位移开始消费

// 1 创建一个消费者
// 2 订阅一个主题//指定offset
Set<TopicPartition> assignment= new HashSet<>();while (assignment.size() == 0) {kafkaConsumer.poll(Duration.ofSeconds(1));// 获取消费者分区分配信息(有了分区分配信息才能开始消费)assignment = kafkaConsumer.assignment();}// 遍历所有分区,并指定 offset 从 1700 的位置开始消费for (TopicPartition tp: assignment) {kafkaConsumer.seek(tp, 1700);}

注意:每次执行完,需要修改消费者组名;

5.5 指定时间消费

在生产环境中,会遇到最近消费的几个小时数据异常,想重新按照时间消费。例如要求按照时间消费前一天的数据,怎么处理?

//1 创建一个消费者
// 2 订阅一个主题
....Set<TopicPartition> assignment = new HashSet<>();while (assignment.size() == 0) {kafkaConsumer.poll(Duration.ofSeconds(1));// 获取消费者分区分配信息(有了分区分配信息才能开始消费)assignment = kafkaConsumer.assignment();}HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();// 封装集合存储,每个分区对应一天前的数据for (TopicPartition topicPartition : assignment) {timestampToSearch.put(topicPartition, System.currentTimeMillis() - 1 * 24 * 3600 * 1000);}// 获取从 1 天前开始消费的每个分区的 offsetMap<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);// 遍历每个分区,对每个分区设置消费时间。for (TopicPartition topicPartition : assignment) {OffsetAndTimestamp offsetAndTimestamp = offsets.get(topicPartition);// 根据时间指定开始消费的位置if (offsetAndTimestamp != null){kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());}}

5.6 漏消费和重复消费

重复消费已经消费了数据,但是 offset 没提交。

漏消费先提交 offset 后消费,有可能会造成数据的漏消费。

怎么能做到既不漏消费也不重复消费呢?

6. 消费者事务

如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将Kafka的offset保存到支持事务的自定义介质(比 如MySQL)。

7 数据积压(消费者如何提高吞吐量)

1)如果是Kafka消费能力不足,则可以考虑增 加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。(两者缺一不可)

2)如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压。

-参数名称 -描述
fetch.max.bytes 默认Default:52428800(50m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)ormax.message.bytes(topicconfig)影响。
max.poll.records 一次 poll 拉取数据返回消息的最大条数,默认是 500 条

笔记来自b站尚硅谷

Kafka快速入门(Kafka消费者)相关推荐

  1. 【kafka】Kafka 快速入门

    1. kafka 简介和环境搭建 Kafka 是由 Linkedin 公司开发的,它是一个分布式的,支持多分区.多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模 ...

  2. 大数据技术之Kafka(一)Kafka概述、Kafka快速入门、Kafka架构深入

    文章目录 1 Kafka 概述 1.1 定义 1.2 消息队列 1.2.1 传统消息队列的应用场景 1.2.2 消息队列的两种模式 1.3 Kafka 基础架构 2 Kafka 快速入门 2.1 安装 ...

  3. Kafka 快速入门(安装)

    kafka学习目录:kafka目录 二.Kafka 快速入门 2.1.windows版安装 2.1.1.Quick Start 本次安装学习在Windows操作系统进行.(Linux版本的差别不大,运 ...

  4. 消息中间件 --- Kafka快速入门

    消息中间件 --- Kafka 快速入门 消息中间件:https://blog.51cto.com/u_9291927/category33 GitHub: GitHub - scorpiostudi ...

  5. 大数据技术之 Kafka (第 2 章 Kafka快速入门)

    第 2 章 Kafka 快速入门 下载安装kafka集群 1.需要jdk 2.需要zookeeper,这个东西在最新版的Kafka中内置. 3.下载Kafka安装包 (下载官网地址:Apache Ka ...

  6. 一.Kafka入门到精通-Kafka快速入门

    前言 在内卷严重的程序员圈子中,原地踏步就是退步,所以不能再躺平啦,赶紧爬起来学习,接下来博主将推出<Kafka入门到精通>系列文章,让你可以在企业中玩起Kafka来得心应手,此乃升职加薪 ...

  7. 《Kafka系列》Kafka详细教程入门

    Kafka 1 消息队列--消息中间件 1.1 消息队列的作用 1.2 消息队列的概念--MQ Message 在互联网中,多台设备产生通信的数据的总称:可以是视频.文本.音频等等. Quene 一种 ...

  8. Apache Kafka教程--Kafka新手入门

    Apache Kafka教程–Kafka新手入门 Kafka Assistant 是一款 Kafka GUI 管理工具--管理Broker,Topic,Group.查看消费详情.监控服务器状态.支持多 ...

  9. 2021年大数据Spark(四十二):SparkStreaming的Kafka快速回顾与整合说明

    目录 Kafka快速回顾 消息队列: 发布/订阅模式: Kafka 重要概念: 常用命令 整合说明 两种方式 两个版本API 在实际项目中,无论使用Storm还是SparkStreaming与Flin ...

最新文章

  1. 阿里巴巴为什么不建议直接使用 Async 注解?
  2. 有赞客户行为收集与实时处理系统设计
  3. 在Emacs中使用ECB(转载)
  4. 视频云的全景蓝图,想象力的允诺之地
  5. Redis学习笔记(11)——Redis缓存集群方案
  6. Self Crossing
  7. python序列类型举例说明_Python(第八课,序列类型)
  8. 重装系统后电脑没有计算机也没有网络连接,电脑网络重置以后没有了wifi连接...
  9. Django的cmdb探索与开发(三)
  10. Atitit 外出活动实名制条例sak令[2018]第920号 《外出活动实名制管理条例》     SAK安全部令 第920号 现发布《外出活动实名制管理条例》,自2018年9月1日起施行。
  11. 计算机显示器刷新率怎么调,电脑显示器刷新率如何设置,免费教你如何快手设置刷新率...
  12. oracle ogg输出格式,关于OGG同步中日期格式超出正常范围的问题处理
  13. Centos7 Springboot 启动脚本
  14. 【C】C语言判断是否质数
  15. 快速清除系统中的木马病毒
  16. 做到这3点,你也能成为一个高情商的人
  17. unity3d在UGUI中显示带表情的微信昵称
  18. SQL语句中不完整数据的补足处理技巧
  19. android通讯录完整功能实现,Android实现通讯录功能
  20. 周边检索POI技术方案设计

热门文章

  1. 物联网设备分为哪几种,NB-IoT主要有什么优势?
  2. ccpc 2016 合肥站 (5道题)
  3. 学习中如何建立自己的资源库?
  4. 音视频基础——libyuv格式转换
  5. 华纳云:香港机房基础网络架构
  6. html点击按钮换css文件夹,按钮如何使用外部css样式
  7. mini6410 用fb显示bitmap
  8. layui laydate 渲染失效问题,lay-key导致动态生成代码二次渲染失败
  9. Uselessness
  10. CDH 20个实战案例