Bootstrap

springboot第69集:字节跳动后端二面经,一文让你走出微服务迷雾架构周刊

1.  简介

1.1  消息队列简介

1.1.1  什么是消息队列

消息队列,英文名:Message Queue,经常缩写为MQ。从字面上来理解,消息队列是一种用来存储消息的队列。来看一下下面的代码:

// 1. 创建一个保存字符串的队列
QueuestringQueue = new LinkedList();

// 2. 往消息队列中放入消息
stringQueue.offer( "hello" );

// 3. 从消息队列中取出消息并打印
System.out.println(stringQueue.poll());

上述代码,创建了一个队列,先往队列中添加了一个消息,然后又从队列中取出了一个消息。这说明了队列是可以用来存取消息的。

我们可以简单理解消息队列就是将需要传输的数据存放在队列中

1.1.1  消息队列中间件

消息队列中间件就是用来存储消息的软件(组件)。举个例子来理解,为了分析网站的用户行为,我们需要记录用户的访问日志。这些一条条的日志,可以看成是一条条的消息,我们可以将它们保存到消息队列中。将来有一些应用程序需要处理这些日志,就可以随时将这些消息取出来处理。

目前市面上的消息队列有很多,例如:Kafka、RabbitMQ、ActiveMQ、RocketMQ、ZeroMQ等。

消息队列的应用场景

电商网站中,新的用户注册时,需要将用户的信息保存到数据库中,同时还需要额外发送注册的邮件通知、以及短信注册码给用户。但因为发送邮件、发送注册短信需要连接外部的服务器,需要额外等待一段时间,此时,就可以使用消息队列来进行异步处理,从而实现快速响应。

22709c50c4ce5015790565fe686a86c7.png
image.png
fc3974dc9b6f9aa35624948d6f2fa5bd.png
image.png

af742d61a9749f97c586fb4ad3c36309.png1.1.1.1  日志处理(大数据领域常见)

大型电商网站(淘宝、京东、国美、苏宁...)、App(抖音、美团、滴滴等)等需要分析用户行为,要根据用户的访问行为来发现用户的喜好以及活跃情况,需要在页面上收集大量的用户访问信息。

179666fd61444ea384815061b174ca13.png
image.png
b1eb5c6c229b2e30a2fe09a27e3a3af1.png
image.png
08d33d615ac628339d602103aaa9498c.png
image.png
3be94c2e34d3d7c6cadad915fafc3fc7.png
image.png
d4fa5a6a4a32db81f29b598551e629af.png
image.png

点对点模式特点:

每个消息只有一个接收者(Consumer)(即一旦被消费,消息就不再在消息队列中)

发送者和接收者间没有依赖性,发送者发送消息之后,不管有没有接收者在运行,都不会影响到发送者下次发送消息;

接收者在成功接收消息之后需向队列应答成功,以便消息队列删除当前接收的消息;
d735510816ec75638946616d768c1dd2.png
image.png
7590897799fc4a08b097c6b045c5cf98.png
image.png

Kafka是由Apache软件基金会开发的一个开源流平台,由Scala和Java编写。Kafka的Apache官网是这样介绍Kakfa的。

Apache Kafka是一个分布式流平台。一个分布式的流平台应该包含3点关键的能力:

1. 发布和订阅流数据流,类似于消息队列或者是企业消息传递系统

2. 以容错的持久化方式存储数据流

处理数据流

1. Publish and subscribe:发布与订阅

2. Store:存储

3. Process:处理

我们通常将Apache Kafka用在两类程序:

1. 建立实时数据管道,以可靠地在系统或应用程序之间获取数据

2. 构建实时流应用程序,以转换或响应数据流

d98869cb89133c7db0ad34cc5acc1950.png
image.png

上图,我们可以看到:

1. Producers:可以有很多的应用程序,将消息数据放入到Kafka集群中。

2. Consumers:可以有很多的应用程序,将消息数据从Kafka集群中拉取出来。

3. Connectors:Kafka的连接器可以将数据库中的数据导入到Kafka,也可以将Kafka的数据导出到

数据库中。

4. Stream Processors:流处理器可以Kafka中拉取数据,也可以将数据写入到Kafka中。

Kafka比ActiveMQ牛逼得多

特性ActiveMQRabbitMQKafkaRocketMQ
所属社区/公司ApacheMozilla Public LicenseApacheApache/Ali
成熟度成熟成熟成熟比较成熟
生产者-消费者模式支持支持支持支持
发布-订阅支持支持支持支持
REQUEST-REPLY支持支持-支持
API完备性低(静态配置)
多语言支持支持JAVA优先语言无关支持,JAVA优先支持
单机呑吐量万级(最差)万级十万级十万级(最高)
消息延迟-微秒级毫秒级-
可用性高(主从)高(主从)非常高(分布式)
消息丢失-理论上不会丢失-
消息重复-可控制理论上会有重复-
事务支持不支持支持支持
文档的完备性
提供快速入门
首次部署难度-

可以注意到Kafka的版本号为:kafka_2.12-2.4.1,因为kafka主要是使用scala语言开发的,2.12为scala的版本号。http://kafka.apache.org/downloads可以查看到每个版本的发布时间。

7ceddcdaf89b0c0732cfaaa61be069ab.png
image.png
ab9f733bc9bab4cc775df4641189b859.png
image.png
662489c8f06234ea418fcee92030b60d.png
image.png
47bad20b06ac38bb2405808915891b75.png
image.png
381fc1f613df02573a2c712ad959085a.png
image.png
目录名称说明
binKafka的所有执行脚本都在这里。例如:启动Kafka服务器、创建Topic、生产者、消费者程序等等
configKafka的所有配置文件
libs运行Kafka所需要的所有JAR包
logsKafka的所有日志文件,如果Kafka出现一些问题,需要到该目录中去查看异常信息
site-docsKafka的网站帮助文件
9d2bf1f2e4a37d342f2b17dc4aa43926.png
image.png
073c75ce65bc7313fa39a485b43e17d1.png
image.png
9ec975649e379c3b93f918553caedd6d.png
image.png
1645b38a338e56aa3a1b1f35e9b8c6d3.png
image.png
8ea2187641a8b707907e8bf6b38520d5.png
image.png
f58608acf89dae0e23873508be6902ab.png
image.png
2cc3f9b00567344ffbaff14d98948012.png
image.png
c2b006a8f6ede522c3ad9f66ac20465b.png
image.png
1fe665119c0d7987e9382655f12c328b.png
image.png
7f3d757e900fada83945f5e0577ba123.png
image.png

基于1个分区1个副本的基准测试

测试步骤:

1. 启动Kafka集群

2. 创建一个1个分区1个副本的topic: benchmark

3. 同时运行生产者、消费者基准测试程序

4. 观察结果

f133c0a1d234aaf79cafbd2a51206bdd.png
image.png
f17c374d2a730489bdbf3ca6b2197727.png
image.png
557f2c6f9ccb609835d6d2a5927d827c.png
image.png
37c71d1966b5f5eff328d6be5bc01a90.png
image.png
9eb241bacac223171dda609a7bd70db2.png
image.png
6d2b1de3019442469fd1412291f46c5b.png
image.png
ecb0c48d8aa6dbf94e9f584f3bdc86ba.png
image.png
a59735c092ecfb3f79958e566bcbf034.png
image.png
6f401e9a8c7189154db04b6ec605b3e2.png
image.png
5b44c82671860078469bea8ddd1ed257.png
image.png
4902a2fd949ebca8f5689c0816eacec7.png
image.png
734593dbaa3fab104f6749e8bee25748.png
image.png
047222f5c7d69b05ba62024daae6c8c5.png
image.png
3edf75d0264c4ce4aee9aa7a21e104eb.png
image.png
4461c5a23df00284e589828bb4b5e347.png
image.png
1b937ad980d7c6632ce13ade7e5c61b8.png
image.png
e5286332a0baca1bd968108c84caab7e.png
image.png
ef0a68245579a8f8c2bfa7bacd8ef648.png
image.png
<**repositories**><!-- 代码库 -->  
    <**repository**>  
        <**id**>central</**id**>  
        <**url**>http://maven.aliyun.com/nexus/content/groups/public//</**url**>  
        <**releases**>  
            <**enabled**>true</**enabled**>  
        </**releases**>  
        <**snapshots**>  
            <**enabled**>true</**enabled**>  
            <**updatePolicy**>always</**updatePolicy**>  
            <**checksumPolicy**>fail</**checksumPolicy**>  
        </**snapshots**>  
    </**repository**>  
</**repositories**>  
  
<**dependencies**>  
    <!-- kafka客户端工具 -->  
    <**dependency**>  
        <**groupId**>org.apache.kafka</**groupId**>  
        <**artifactId**>kafka-clients</**artifactId**>  
        <**version**>2.4.1</**version**>  
    </**dependency**>  
  
    <!-- 工具类 -->  
    <**dependency**>  
        <**groupId**>org.apache.commons</**groupId**>  
        <**artifactId**>commons-io</**artifactId**>  
        <**version**>1.3.2</**version**>  
    </**dependency**>  
  
    <!-- SLF桥接LOG4J日志 -->  
    <**dependency**>  
        <**groupId**>org.slf4j</**groupId**>  
        <**artifactId**>slf4j-log4j12</**artifactId**>  
        <**version**>1.7.6</**version**>  
    </**dependency**>  
  
    <!-- SLOG4J日志 -->  
    <**dependency**>  
        <**groupId**>log4j</**groupId**>  
        <**artifactId**>log4j</**artifactId**>  
        <**version**>1.2.16</**version**>  
    </**dependency**>  
</**dependencies**>  
  
<**build**>  
    <**plugins**>  
        <**plugin**>  
            <**groupId**>org.apache.maven.plugins</**groupId**>  
            <**artifactId**>maven-compiler-plugin</**artifactId**>  
            <**version**>3.7.0</**version**>  
            <**configuration**>  
                <**source**>1.8</**source**>  
                <**target**>1.8</**target**>  
            </**configuration**>  
        </**plugin**>  
    </**plugins**>  
</**build**>

将log4j.properties配置文件放入到resources文件夹中

**log4j.rootLogger**=**INFO,stdout****  
****log4j.appender.stdout**=**org.apache.log4j.ConsoleAppender****  
****log4j.appender.stdout.layout**=**org.apache.log4j.PatternLayout****  
****log4j.appender.stdout.layout.ConversionPattern**= **%5p - %m%n**
f716a16a30e86e5da0d8ce6004d4623e.png
image.png
**public class** KafkaProducerTest {  
    **public static void** main(String[] args) {  
        // 1. 创建用于连接Kafka的Properties配置  
        Properties props = **new** Properties();  
        props.put( **"bootstrap.servers"** , **"192.168.88.100:9092"** );  
        props.put( **"acks"** , **"all"** );  
        props.put( **"key.serializer"** , **"org.apache.kafka.common.serialization.StringSerializer"** );  
        props.put( **"value.serializer"** , **"org.apache.kafka.common.serialization.StringSerializer"** );  
  
        // 2. 创建一个生产者对象KafkaProducer  
        KafkaProducer<String, String> producer = **new** KafkaProducer<String, String>(props);  
  
        // 3. 调用send发送1-100消息到指定Topic test  
        **for**(**int** i = 0; i < 100; ++i) {  
            **try** {  
                // 获取返回值Future,该对象封装了返回值  
                Future<RecordMetadata> future = producer.send(**new** ProducerRecord<String, String>( **"test"** , **null**, i + **""** ));  
                // 调用一个Future.get()方法等待响应  
                future.get();  
            } **catch** (InterruptedException e) {  
                e.printStackTrace();  
            } **catch** (ExecutionException e) {  
                e.printStackTrace();  
            }  
        }  
  
        // 5. 关闭生产者  
        producer.close();  
    }  
}
c7f8e24a2b827f774598e2de57a39567.png
image.png
a6b635388fba60206bc7ab0c112a3bfb.png
image.png
**public class** KafkaProducerTest {  
    **public static void** main(String[] args) {  
        // 1. 创建用于连接Kafka的Properties配置  
        Properties props = **new** Properties();  
        props.put( **"bootstrap.servers"** , **"node1.itcast.cn:9092"** );  
        props.put( **"acks"** , **"all"** );  
        props.put( **"key.serializer"** , **"org.apache.kafka.common.serialization.StringSerializer"** );  
        props.put( **"value.serializer"** , **"org.apache.kafka.common.serialization.StringSerializer"** );  
  
        // 2. 创建一个生产者对象KafkaProducer  
        KafkaProducer<String, String> producer = **new** KafkaProducer<String, String>(props);  
  
        // 3. 调用send发送1-100消息到指定Topic test  
        **for**(**int** i = 0; i < 100; ++i) {  
            **try** {  
                // 获取返回值Future,该对象封装了返回值  
                Future<RecordMetadata> future = producer.send(**new** ProducerRecord<String, String>( **"test"** , **null**, i + **""** ));  
                // 调用一个Future.get()方法等待响应  
                future.get();  
            } **catch** (InterruptedException e) {  
                e.printStackTrace();  
            } **catch** (ExecutionException e) {  
                e.printStackTrace();  
            }  
        }  
  
        // 5. 关闭生产者  
        producer.close();  
    }  
}
**public class** KafkaProducerTest {  
    **public static void** main(String[] args) {  
        // 1. 创建用于连接Kafka的Properties配置  
        Properties props = **new** Properties();  
        props.put( **"bootstrap.servers"** , **"node1.itcast.cn:9092"** );  
        props.put( **"acks"** , **"all"** );  
        props.put( **"key.serializer"** , **"org.apache.kafka.common.serialization.StringSerializer"** );  
        props.put( **"value.serializer"** , **"org.apache.kafka.common.serialization.StringSerializer"** );  
  
        // 2. 创建一个生产者对象KafkaProducer  
        KafkaProducer<String, String> producer = **new** KafkaProducer<String, String>(props);  
  
        // 3. 调用send发送1-100消息到指定Topic test  
        **for**(**int** i = 0; i < 100; ++i) {  
            // 一、同步方式  
            // 获取返回值Future,该对象封装了返回值  
            // Future<RecordMetadata> future = producer.send(new ProducerRecord<String, String>("test", null, i + ""));  
            // 调用一个Future.get()方法等待响应  
            // future.get();  
  
            // 二、带回调函数异步方式  
            producer.send(**new** ProducerRecord<String, String>( **"test"** , **null**, i + **""** ), **new** Callback() {  
                @Override  
                **public void** onCompletion(RecordMetadata metadata, Exception exception) {  
                    **if**(exception != **null**) {  
                        System.***out***.println( **"** **发送消息出现异常** **"** );  
                    }  
                    **else** {  
                        String topic = metadata.topic();  
                        **int** partition = metadata.partition();  
                        **long** offset = metadata.offset();  
  
                        System.***out***.println( **"** **发送消息到** **Kafka** **中的名字为** **"** + topic + **"** **的主题,第** **"** + partition + **"** **分区,第** **"** + offset + **"** **条数据成功** **!"** );  
                    }  
                }  
            });  
        }  
  
        // 5. 关闭生产者  
        producer.close();  
    }  
}
64a629825d3bd26dd1b1af0b041775da.png
image.png
63445e5fa415b3804a9f1798b1421f5f.png
image.png
e6fcf4c3da5bd92939ecf0b11a496799.png
image.png

ZK用来管理和协调broker,并且存储了Kafka的元数据(例如:有多少topic、partition、consumer)

ZK服务主要用于通知生产者和消费者Kafka集群中有新的broker加入、或者Kafka集群中出现故障的broker。

Kafka正在逐步想办法将ZooKeeper剥离,维护两套集群成本较高,社区提出KIP-500就是要替换掉ZooKeeper的依赖。“Kafka on Kafka”——Kafka自己来管理自己的元数据

1.1.1  producer(生产者)

生产者负责将数据推送给broker的topic

1.1.2  consumer(消费者)

消费者负责从broker的topic中拉取数据,并自己进行处理

1.1.3  consumer group(消费者组)
ee762be7cc9b33cee5acebacaf1c507b.png
image.png
5d5afc9956894c806eedd82ac60230e9.png
image.png
b6d7966b5099fc4d7863adf0f3a90fdf.png
image.png
13245a957d808283d0ce9dcee156d0ce.png
image.png
6c0c63756a1b6a222eb33ed3269631b0.png
image.png
c3b3bd38ba44dca5556ab96cf6f5fc46.png
image.png
// 3. 发送1-100数字到Kafka的test主题中  
**while**(**true**) {  
    **for** (**int** i = 1; i <= 100; ++i) {  
        // 注意:send方法是一个异步方法,它会将要发送的数据放入到一个buffer中,然后立即返回  
        // 这样可以让消息发送变得更高效  
        producer.send(**new** ProducerRecord<>( **"test"** , i + **""** ));  
    }  
    Thread.*sleep*(3000);  
}
9e6acda9d94bfdc6fe9d41f126d93fc2.png
image.png
8a8e70c271044643062c7d1bf3ca5786.png
image.png
ab98a004e2fb14a5ade977f45db6c5d3.png
image.png
1445a88281d41317fc3772ff38f8d2b0.png
image.png
e762798a0ca0cd7b5721e24f14c52c2a.png
image.png
a57413f3c609a4f7012c6124c9af353e.png
image.png
a300f3f8a06b62c0554888b4ad9ebaea.png
image.png
c032281f4cb86d04b182a7abf7d6a6ea.png
image.png
bf315ef2a5486905769a02b4b4fbe55a.png
image.png
34b6448f590af06940a95b98d9599c07.png
image.png
*// 1. 创建消费者*

    **public** **static** Consumer **<** String, String **>**  createConsumer() {

         *// 1. 创建Kafka消费者配置*

        Properties **props** **=** **new** Properties();

        **props**.setProperty( **"** **bootstrap.servers** **"** ,  **"** **node1.itcast.cn:9092** **"** );

        **props**.setProperty( **"** **group.id** **"** ,  **"** **ods_user** **"** );

        **props**.put( **"** **isolation.level** **"** , **"** **read_committed** **"** );

        **props**.setProperty( **"** **enable.auto.commit** **"** ,  **"** **false** **"** );

        **props**.setProperty( **"** **key.deserializer** **"** ,  **"** **org.apache.kafka.common.serialization.StringDeserializer** **"** );

        **props**.setProperty( **"** **value.deserializer** **"** ,  **"** **org.apache.kafka.common.serialization.StringDeserializer** **"** );

 

         *// 2. 创建Kafka消费者*

        KafkaConsumer<String, String> **consumer** **=** **new** KafkaConsumer<>(props);

 

         *// 3. 订阅要消费的主题*

        **consumer**.subscribe(**Arrays**.asList( **"** **ods_user** **"** ));

        

        **return** consumer;

}

编写一个方法 createProducer,返回一个生产者对象。注意:需要配置事务的id,开启了事务会默认开启幂等性。

fea6da42b91d5be6c0f48eaa0cde3cd5.png
image.png
ee49b8ea56c65bd4c83c97775f7b5c19.png
image.png
1.1.1.1  编写代码消费并生产数据

实现步骤:

1. 调用之前实现的方法,创建消费者、生产者对象

2. 生产者调用initTransactions初始化事务

3. 编写一个while死循环,在while循环中不断拉取数据,进行处理后,再写入到指定的topic

(1) 生产者开启事务

(2) 消费者拉取消息

(3) 遍历拉取到的消息,并进行预处理(将1转换为男,0转换为女)

(4) 生产消息到dwd_user topic中

(5) 提交偏移量到事务中

(6) 提交事务

(7) 捕获异常,如果出现异常,则取消事务

**public** **static** void main(String[] args) {

        Consumer<String, String> **consumer** **=** createConsumer();

        Producer<String, String> **producer** **=** createProducer();

         *// 初始化事务*

        **producer**.initTransactions();

 

        **while**(true) {

            **try** {

                 *// 1. 开启事务*

                **producer**.beginTransaction();

                 *// 2. 定义Map结构,用于保存分区对应的offset*

                Map<TopicPartition, OffsetAndMetadata> **offsetCommits** **=** **new** HashMap<>();

                 *// 2. 拉取消息*

                ConsumerRecords<String, String> **records** **=** **consumer**.poll(**Duration**.ofSeconds(2));

                **for** (ConsumerRecord<String, String> **record**  **:**  records) {

                     *// 3. 保存偏移量*

                    **offsetCommits**.put(**new** TopicPartition(**record**.topic(), **record**.partition()),

                            **new** OffsetAndMetadata(**record**.offset() + 1));

                     *// 4. 进行转换处理*

                    String[] **fields** **=** **record**.value().split( **"** **,** **"** );

                    fields[1] **=** fields[1].equalsIgnoreCase( **"** **1** **"** )  **?**   **"** **男** **"** **:** **"** **女** **"** ;

                    String **message** **=** fields[0]  **+**   **"** **,** **"**   **+**  fields[1]  **+**   **"** **,** **"**   **+**  fields[2];

                     *// 5. 生产消息到dwd_user*

                    **producer**.send(**new** ProducerRecord<>( **"** **dwd_user** **"** , message));

                }

                 *// 6. 提交偏移量到事务*

                **producer**.sendOffsetsToTransaction(offsetCommits,  **"** **ods_user** **"** );

                 *// 7. 提交事务*

                **producer**.commitTransaction();

            } **catch** (Exception **e**) {

                 *// 8. 放弃事务*

                **producer**.abortTransaction();

            }

        }

    }
8e8e231d3e89b9af6f3817770ddf1043.png
image.png
93886463917e48d4bd2d1088962c4c70.png
image.png

1.  分区和副本机制

1.1  生产者分区写入策略

生产者写入消息到topic,Kafka将依据不同的策略将数据分配到不同的分区中

1. 轮询分区策略

2. 随机分区策略

3. 按key分区分配策略

4. 自定义分区策略

1.1.1  轮询策略
27ea5d2457c8b82ce6e3ed279653eae0.png
image.png
f9c1512d4b642b5ca8a29a5c74d4fbe9.png
image.png
324239113b130e2b5c2340c1cf5572a1.png
image.png
73865c725ae9c84b934903ab7e6395d1.png
image.png
926f428f13082eebc09d127d0dfc9e13.png
image.png
4e30e11c6993d564e8df2d5113c6d797.png
image.png
0cbea632daf6d9739c88e1790ba2adcb.png
image.png
4b29fa8ced7162f4dbec6afb73e2e7dd.png
image.png
指标意义
Brokers Spreadbroker使用率
Brokers Skew分区是否倾斜
Brokers Leader Skewleader partition是否存在倾斜
4964434ead544e1b54b95e54bf445b18.png
image.png
55a434be2ead8241ffc173367d9f96d8.png
image.png
1cff013de5aec0b957d66b44e3d3d532.png
image.png
752c1d7b061479ac9660415266540917.png
image.png
883d4c0e951572ef1a5141d29b1f7a71.png
image.png
f72a9123ac4493a5222aa5649a428855.png
image.png
960f61e540abc94f72561df5ae11ccd1.png
image.png
b7dbf40490763eeccbea7f15c150a5c5.png
image.png
指标****单分区单副本(ack=0)****单分区单副本(ack=1)****单分区单副本(ack=-1/all)****
吞吐量165875.991109/s每秒16.5W条记录93092.533979/s每秒9.3W条记录73586.766156 /s每秒7.3W调记录
吞吐速率158.19 MB/sec88.78 MB/sec70.18 MB
平均延迟时间192.43 ms346.62 ms438.77 ms
最大延迟时间670.00 ms1003.00 ms1884.00 ms
2ccb043c3b53770a6645428d3602aa45.png
image.png
928f56c1f61b3b02b5e92ee841f407e9.png
image.png
指标****单分区单副本(ack=0)****单分区单副本(ack=1)****
吞吐量165875.991109 records/sec每秒16.5W条记录93092.533979 records/sec每秒9.3W条记录
吞吐速率158.19 MB/sec每秒约160MB数据88.78 MB/sec每秒约89MB数据
平均延迟时间192.43 ms avg latency346.62 ms avg latency
最大延迟时间670.00 ms max latency1003.00 ms max latency
fd6b52ae748953d7b557bc30323d3560.png
image.png

加群联系作者vx:xiaoda0423

仓库地址:https://github.com/webVueBlog/JavaGuideInterview

;