一、 简介前端
它可让你发布和订阅记录流。在这方面,它相似于一个消息队列或企业消息系统。mysql
它可让你持久化收到的记录流,从而具备容错能力。linux
首先,明确几个概念:web
• Kafka运行在一个或多个服务器上。面试
• Kafka集群分类存储的记录流被称为主题(Topics)。算法
• 每一个消息记录包含一个键,一个值和时间戳。sql
Kafka有四个核心API:mongodb
• 生产者 API 容许应用程序发布记录流至一个或多个Kafka的话题(Topics)。数据库
• 消费者API 容许应用程序订阅一个或多个主题,并处理这些主题接收到的记录流。json
• Streams API 容许应用程序充当流处理器(stream processor),从一个或多个主题获取输入流,并生产一个输出流至一个或多个的主题,可以有效地变换输入流为输出流。
• Connector API 容许构建和运行可重用的生产者或消费者,可以把 Kafka主题链接到现有的应用程序或数据系统。例如,一个链接到关系数据库的链接器(connector)可能会获取每一个表的变化。
• Kafka的客户端和服务器之间的通讯是靠一个简单的,高性能的,与语言无关的TCP协议完成的。这个协议有不一样的版本,并保持向前兼容旧版本。Kafka不光提供了一个Java客户端,还有许多语言版本的客户端。
二、 架构
2.1 Broker
每一个kafka server称为一个Broker,多个borker组成kafka cluster。一个机器上能够部署一个或者多个Broker,这多个Broker链接到相同的ZooKeeper就组成了Kafka集群。
2.2 主题Topic
让咱们先来了解Kafka的核心抽象概念记录流 – 主题。主题是一种分类或发布的一系列记录的名义上的名字。Kafka的主题始终是支持多用户订阅的; 也就是说,一个主题能够有零个,一个或多个消费者订阅写入的数据。
Topic 与broker
一个Broker上能够建立一个或者多个Topic。同一个topic能够在同一集群下的多个Broker中分布。
固然,Topic只是一个名义上的组件,真正在Broker间分布式的Partition。
2.3 分区与日志
一个主题对应多个分区,一个分区对应一个日志
Kafka会为每一个topic维护了多个分区(partition),每一个分区会映射到一个逻辑的日志(log)文件。每一个分区是一个有序的,不可变的消息序列,新的消息不断追加到这个有组织的有保证的日志上。分区会给每一个消息记录分配一个顺序ID号 – 偏移量, 可以惟一地标识该分区中的每一个记录。
日志分区是分布式的存在于一个kafka集群的多个broker上。每一个partition会被复制多份存在于不一样的broker上。这样作是为了容灾。具体会复制几份,会复制到哪些broker上,都是能够配置的。通过相关的复制策略后,每一个topic在每一个broker上会驻留一到多个partition:
2.4 保留策略与Offset
Kafka集群保留全部发布的记录,无论这个记录有没有被消费过,Kafka提供可配置的保留策略去删除旧数据(还有一种策略根据分区大小删除数据)。例如,若是将保留策略设置为两天,在记录公布后两天内,它可用于消费,以后它将被丢弃以腾出空间。Kafka的性能跟存储的数据量的大小无关, 因此将数据存储很长一段时间是没有问题的。
事实上,保留在每一个消费者元数据中的最基础的数据就是消费者正在处理的当前记录的偏移量(offset)或位置(position)。这种偏移是由消费者控制:一般偏移会随着消费者读取记录线性前进,但事实上,由于其位置是由消费者进行控制,消费者能够在任何它喜欢的位置读取记录。例如,消费者能够恢复到旧的偏移量对过去的数据再加工或者直接跳到最新的记录,并消费从“如今”开始的新的记录。
这些功能的结合意味着,实现Kafka的消费者的代价都是很小的,他们能够增长或者减小而不会对集群或其余消费者有太大影响。例如,你可使用咱们的命令行工具去追随任何主题,并且不会改变任何现有的消费者消费的记录。
2.5 Leader与Followers
一个Topic可能有不少分区,以便它可以支持海量的的数据,更重要的意义是分区是进行并行处理的基础单元。日志的分区会跨服务器的分布在Kafka集群中,每一个分区能够配置必定数量的副本分区提供容错能力。为了保证较高的处理效率,消息的读写都是在固定的一个副本上完成。这个副本就是所谓的Leader,而其余副本则是Follower,而Follower则会按期地到Leader上同步数据。
(1)leader处理全部的读取和写入分区的请求,而followers被动的从领导者拷贝数据。
(2)若是leader失败了,followers之一将自动成为新的领导者。
(3)每一个服务器可能充当一些分区的leader和其余分区的follower,这样的负载就会在集群内很好的均衡分配。
(4)一个分区在同一时刻只能有一个消费者实例进行消费。
举例:
能够看见咱们一共有3个分区分别是0,1,2, replica 有2个:
partition 0 的leader在broker1, follower在broker2
partition 1 的leader在broker2, follower在broker0
partition 2 的leader在broker0, follower在brokder1
一个broker中不会出现两个同样的Partition,replica会被均匀的分布在各个kafka server(broker)上 。Kafka并不容许replicas 数设置大于 broker数,由于在一个broker上若是有2个replica实际上是没有意义的,由于再多的replica同时在一台broker上,随着该broker的crash,一块儿不可用。
(1)Leader选举与ISR
若是某个分区所在的服务器除了问题,不可用,kafka会从该分区的其余的副本中选择一个做为新的Leader。以后全部的读写就会转移到这个新的Leader上。如今的问题是应当选择哪一个做为新的Leader。显然,只有那些跟Leader保持同步的Follower才应该被选做新的Leader。
Kafka会在Zookeeper上针对每一个Topic维护一个称为ISR(in-sync replica,已同步的副本)的集合,该集合中是一些分区的副本。只有当这些副本都跟Leader中的副本同步了以后,kafka才会认为消息已提交,并反馈给消息的生产者。若是这个集合有增减,kafka会更新zookeeper上的记录。若是某个分区的Leader不可用,Kafka就会从ISR集合中选择一个副本做为新的Leader。显然经过ISR,kafka须要的冗余度较低,能够容忍的失败数比较高。假设某个topic有f+1个副本,kafka能够容忍f个服务器不可用。
(2)为何不用少数服从多数的方法
少数服从多数是一种比较常见的一致性算法和Leader选举法。它的含义是只有超过半数的副本同步了,系统才会认为数据已同步;选择Leader时也是从超过半数的同步的副本中选择。这种算法须要较高的冗余度。譬如只容许一台机器失败,须要有三个副本;而若是只容忍两台机器失败,则须要五个副本。而kafka的ISR集合方法,分别只须要两个和三个副本。
(3)若是全部的ISR副本都失败了怎么办
此时有两种方法可选,一种是等待ISR集合中的副本复活,一种是选择任何一个当即可用的副本,而这个副本不必定是在ISR集合中。这两种方法各有利弊,实际生产中按需选择。若是要等待ISR副本复活,虽然能够保证一致性,但可能须要很长时间。而若是选择当即可用的副本,则极可能该副本并不一致。
2.6 生产者和消费者
(1)生产者
生产者发布数据到他们所选择的主题。生产者负责选择把记录分配到主题中的哪一个分区。这可使用轮询算法( round-robin)进行简单地平衡负载,也能够根据一些更复杂的语义分区算法(好比基于记录一些键值)来完成。
(2)消费者
消费者以消费群(consumer group)的名称来标识本身,每一个发布到主题的消息都会发送给订阅了这个主题的消费群里面的一个消费者实例,即一个消费群只发送一次。消费者的实例能够在单独的进程或单独的机器上。
上图中两个服务器的Kafka集群具备四个分区(P0-P3)和两个消费群。A消费群有两个消费者,B群有四个。更常见的是,咱们会发现主题有少许的消费群,每个都是“逻辑上的订阅者”。每组都是由不少消费者实例组成,从而实现可扩展性和容错性。这只不过是发布 – 订阅模式的再现,区别是这里的订阅者是一组消费者而不是一个单一的进程的消费者。
Kafka消费群的实现方式是经过分割分区给每一个Consumer实例实现的,使每一个实例在任什么时候间点的均可以“公平分享”独占的分区。维持消费群中的成员关系的这个过程是经过Kafka动态协议处理。若是新的实例加入该组,他将接管该组的其余成员的一些分区; 若是一个实例死亡,其分区将被分配到剩余的实例。
Kafka只保证一个分区内的消息有序,不能保证一个主题的不一样分区之间的消息有序。分区的消息有序与依靠主键进行数据分区的能力相结合足以知足大多数应用的要求。可是,若是你想要保证全部的消息都绝对有序能够只为一个主题分配一个分区,虽然这将意味着每一个消费群同时只能有一个消费进程在消费。
3 、数据可靠性与一致性
3.1 Partition Recovery机制
每一个Partition会在磁盘记录一个RecoveryPoint,记录已经flush到磁盘的最大offset。当broker fail 重启时,会进行loadLogs。 首先会读取该Partition的RecoveryPoint,找到包含RecoveryPoint的segment及之后的segment, 这些segment就是可能没有彻底flush到磁盘segments。而后调用segment的recover,从新读取各个segment的msg,并重建索引。
优势
• 以segment为单位管理Partition数据,方便数据生命周期的管理,删除过时数据简单。
• 在程序崩溃重启时,加快recovery速度,只需恢复未彻底flush到磁盘的segment。
• 经过index中offset与物理偏移映射,用二分查找能快速定位msg,而且经过分多个Segment,每一个index文件很小,查找速度更快。
3.2 Partition Replica同步机制
• Partition的多个replica中一个为Leader,其他为follower
• Producer只与Leader交互,把数据写入到Leader中
• Followers从Leader中拉取数据进行数据同步
• Consumer只从Leader拉取数据
ISR:in-sync replica,已同步的副本。准确的定义是“全部不落后的replica集合”。不落后有两层含义:距离上次FetchRequest的时间不大于某一个值或落后的消息数不大于某一个值, Leader失败后会从ISR中选取一个Follower作Leader。
3.4 消息的顺序消费问题
在说到消息中间件的时候,咱们一般都会谈到一个特性:消息的顺序消费问题。这个问题看起来很简单:Producer发送消息1, 2, 3;Consumer按1, 2, 3顺序消费。但实际状况倒是:不管RocketMQ,仍是Kafka,缺省都不保证消息的严格有序消费!困难以下:
(1)Producer
发送端不能异步发送,异步发送在发送失败的状况下,就没办法保证消息顺序。好比你连续发了1,2,3。 过了一会,返回结果1失败,2, 3成功。你把1再从新发送1遍,这个时候顺序就乱掉了。
(2)存储端
对于存储端,要保证消息顺序,会有如下几个问题:
消息不能分区。也就是1个topic,只能有1个队列。在Kafka中,它叫作partition;在RocketMQ中,它叫作queue。 若是你有多个队列,那同1个topic的消息,会分散到多个分区里面,天然不能保证顺序。
即便只有1个队列的状况下,会有第2个问题。该机器挂了以后,可否切换到其余机器?也就是高可用问题。好比你当前的机器挂了,上面还有消息没有消费完。此时切换到其余机器,可用性保证了。但消息顺序就乱掉了。要想保证,一方面要同步复制,不能异步复制;另1方面得保证,切机器以前,挂掉的机器上面,全部消息必须消费完了,不能有残留。很明显,这个很难。
(3)接收端
对于接收端,不能并行消费,也即不能开多线程或者多个客户端消费同1个队列。
3.5 Producer发送消息的配置
3.5.1 同步模式
kafka有同步(sync)、异步(async)以及oneway这三种发送方式,某些概念上区分也能够分为同步和异步两种,同步和异步的发送方式经过producer.type参数指定,而oneway由request.require.acks参数指定。
producer.type的默认值是sync,即同步的方式。这个参数指定了在后台线程中消息的发送方式是同步的仍是异步的。若是设置成异步的模式,能够运行生产者以batch的形式push数据,这样会极大的提升broker的性能,可是这样会增长丢失数据的风险。
3.5.2 异步模式
对于异步模式,还有4个配套的参数,以下:
3.5.3 oneway
oneway是只顾消息发出去而无论死活,消息可靠性最低,可是低延迟、高吞吐,这种对于某些彻底对可靠性没有要求的场景仍是适用的,即request.required.acks设置为0。
3.5.4 消息可靠性级别
当Producer向Leader发送数据时,能够经过request.required.acks参数设置数据可靠性的级别:
• 0: 不论写入是否成功,server不须要给Producer发送Response,若是发生异常,server会终止链接,触发Producer更新meta数据;
• 1: Leader写入成功后即发送Response,此种状况若是Leader fail,会丢失数据
• -1: 等待全部ISR接收到消息后再给Producer发送Response,这是最强保证
仅设置acks=-1也不能保证数据不丢失,当Isr列表中只有Leader时,一样有可能形成数据丢失。要保证数据不丢除了设置acks=-1, 还要保 证ISR的大小大于等于2,具体参数设置:
• (1)request.required.acks: 设置为-1 等待全部ISR列表中的Replica接收到消息后采算写成功;
• (2)min.insync.replicas: 设置为大于等于2,保证ISR中至少有两个Replica
Producer要在吞吐率和数据可靠性之间作一个权衡。
3.5.5 通常配置
四、 应用场景
4.1 消息系统
消息处理模型从来有两种:
队列模型:一组消费者能够从服务器读取记录,每一个记录都会被其中一个消费者处理,为保障消息的顺序,同一时刻只能有一个进程进行消费。
发布-订阅模型:记录被广播到全部的消费者。
Kafka的消费群的推广了这两个概念。消费群能够像队列同样让消息被一组进程处理(消费群的成员),与发布 – 订阅模式同样,Kafka可让你发送广播消息到多个消费群。
Kafka兼顾了消息的有序性和并发处理能力。传统的消息队列的消息在队列中是有序的,多个消费者从队列中消费消息,服务器按照存储的顺序派发消息。然而,尽管服务器是按照顺序派发消息,可是这些消息记录被异步传递给消费者,消费者接收到的消息也许已是乱序的了。这实际上意味着消息的排序在并行消费中都将丢失。消息系统一般靠 “排他性消费”( exclusive consumer)来解决这个问题,只容许一个进程从队列中消费,固然,这意味着没有并行处理的能力。
Kafka作的更好。经过一个概念:并行性-分区-主题实现主题内的并行处理,Kafka是可以经过一组消费者的进程同时提供排序保证和并行处理以及负载均衡的能力:
(1)排序保障
每一个主题的分区指定给每一个消费群中的一个消费者,使每一个分区只由该组中的一个消费者所消费。经过这样作,咱们确保消费者是一个分区惟一的读者,从而顺序的消费数据。
(2)并行处理
由于有许多的分区,因此负载还可以均衡的分配到不少的消费者实例上去。可是请注意,一个消费群的消费者实例不能比分区数量多,由于分区数表明了一个主题的最大并发数,消费者的数量高于这个数量意义不大。
4.2 日志采集
大多数时候,咱们的log都会输出到本地的磁盘上,排查问题也是使用linux命令来搞定,若是web程序组成负载集群,那么就有多台机器,若是有几十台机器,几十个服务,那么想快速定位log问题和排查就比较麻烦了,因此颇有必要有一个统一的平台管理log,如今大多数公司的套路都是收集重要应用的log集中到kafka中,而后在分别导入到es和hdfs上,一个作实时检索分析,另外一个作离线统计和数据备份。如何能快速收集应用日志到kafka中?
方法一:使用log4j的集成包
kafka官网已经提供了很是方便的log4j的集成包 kafka-log4j-appender,咱们只须要简单配置log4j文件,就能收集应用程序log到kafka中。
注意,须要引入maven的依赖包:
很是简单,一个maven依赖加一个log4j配置文件便可,若是依然想写入log到本地 文件依然也是能够的,这种方式最简单快速,可是默认的的log日志是一行一行的纯文本,有些场景下咱们可能须要json格式的数据。
方法二: 重写Log4jAppender
重写Log4jAppender,自定义输出格式,支持json格式,若是是json格式的数据打入到kafka中,后续收集程序可能就很是方便了,直接拿到json就能入到mongodb或者es中,若是打入到kafka中的数据是纯文本,那么收集程序,可能须要作一些etl,解析其中的一些字段而后再入到es中,因此原生的输出格式,可能稍不灵活,这样就须要咱们本身写一些类,而后达到灵活的程度。
总结:
(1)方法一简单快速,不支持json格式的输出,打到kafka的消息都是原样的log日志信息
(2)方法二稍微复杂,须要本身扩展log收集类,但支持json格式的数据输出,对于想落地json数据直接到存储系统中是很是适合的。
此外须要注意,在调试的时候log发送数据到kafka模式最好是同步模式的不然你控制台打印的数据颇有可能不会被收集kafka中,程序就中止了。生产环境最好开启异步发送数据模式,由于内部是批量的处理,因此能提高吞吐,但有必定的轻微延迟。
4.3 流处理
只是读,写,以及储存数据流是不够的,目的是可以实时处理数据流。在Kafka中,流处理器是从输入的主题连续的获取数据流,而后对输入进行一系列的处理,并生产连续的数据流到输出主题。
这些简单处理能够直接使用生产者和消费者的API作到。然而,对于更复杂的转换Kafka提供了一个彻底集成的流API。这容许应用程序把一些重要的计算过程从流中剥离或者加入流一块儿。这种设施可帮助解决这类应用面临的难题:处理杂乱的数据,改变代码去从新处理输入,执行有状态的计算等。流API创建在Kafka提供的核心基础单元之上:它使用生产者和消费者的API进行输入输出,使用Kafka存储有状态的数据,并使用群组机制在一组流处理实例中实现容错。
把功能组合起来
消息的传输,存储和流处理的组合看似不寻常,倒是Kafka做为流处理平台的关键。像HDFS分布式文件系统,容许存储静态文件进行批量处理。像这样的系统容许存储和处理过去的历史数据。传统的企业消息系统容许处理您订阅后才抵达的消息。这样的系统只能处理未来到达的数据。
Kafka结合了这些功能,这种结合对Kafka做为流应用平台以及数据流处理的管道相当重要。经过整合存储和低延迟订阅,流处理应用能够把过去和将来的数据用相同的方式处理。这样一个单独的应用程序,不但能够处理历史的,保存的数据,当它到达最后一条记录不会中止,继续等待处理将来到达的数据。这是泛化了的流处理的概念,包括了批处理应用以及消息驱动的应用。一样,流数据处理的管道结合实时事件的订阅令人们可以用Kafka实现低延迟的管道; 可靠的存储数据的能力令人们有可能使用它传输一些重要的必须保证可达的数据。能够与一个按期加载数据的线下系统集成,或者与一个由于维护长时间下线的系统集成。流处理的组件可以保证转换(处理)到达的数据。
五、Kafka与ActiveMQ对比
首先,Active MQ与Kafka的相同点只有一个,就是都是消息中间件。其余没有任何相同点。
5.1 consumer的不一样
(1)AMQ消费完的消息会被清理掉
AMQ不管在standalone仍是分布式的状况下,都会使用mysql做为存储,多一个consumer线程去消费多个queue, 消费完的message会在mysql中被清理掉。
(2)AMQ的消费逻辑在Broker中完成
做为AMQ的consume clinet的多个consumer线程去消费queue,AMQ Broker会接收到这些consume线程,阻塞在这里,有message来了就会进行消费,没有消息就会阻塞在这里。具体消费的逻辑也就是处理这些consumer线程都是AMQ Broker那面处理。
kafka是message都存在partition下的segment文件里面,有offsite偏移量去记录那条消费了,哪条没消费。某个consumer group下consumer线程消费完就会,这个consumer group 下的这个consumer对应这个partition的offset+1,kafka并不会删除这条已经被消费的message。其余的consumer group也能够再次消费这个message。在high level api中offset会自动或手动的提交到zookeeper上(若是是自动提交就有可能处理失败或还没处理完就提交offset+1了,容易出现下次再启动consumer group的时候这条message就被漏了),也可使用low level api,那么就是consumer程序中本身维护offset+1的逻辑。kafka中的message会按期删除。
(3)Kafka有consumer group的概念,AMQ没有。
一个consumer group下有多个consumer,每一个consumer都是一个线程,consumer group是一个线程组。每一个线程组consumer group之间互相独立。同一个partition中的一个message只能被一个consumer group下的一个consumer线程消费,由于消费完了这个consumer group下的这个consumer对应的这个partition的offset就+1了,这个consumer group下的其余consumer仍是这个consumer都不能在消费了。 可是另一个consumer group是彻底独立的,能够设置一个from的offset位置,从新消费这个partition。
5.2 关于存储结构
ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB
Kafka是文件存储,每一个topic有多个partition,每一个partition有多个replica副本(每一个partition和replica都是均匀分配在不一样的kafka broker上的)。每一个partition由多个segment文件组成。这些文件是顺序存储的。所以读取和写入都是顺序的,所以,速度很快,省去了磁盘寻址的时间。
不少系统、组件为了提高效率通常巴不得把全部数据都扔到内存里,而后按期flush到磁盘上;而Kafka决定直接使用页面缓存;可是随机写入的效率很慢,为了维护彼此的关系顺序还须要额外的操做和存储,而线性的顺序写入能够避免磁盘寻址时间,实际上,线性写入(linear write)的速度大约是300MB/秒,但随即写入却只有50k/秒,其中的差异接近10000倍。这样,Kafka以页面缓存为中间的设计在保证效率的同时还提供了消息的持久化,每一个consumer本身维护当前读取数据的offset(也可委托给zookeeper),以此可同时支持在线和离线的消费。
5.3 关于使用场景与吞吐量
ActiveMQ用于企业消息中间件,使得业务逻辑和前端处理逻辑解耦。AMQ的吞吐量不大,zuora的AMQ就是用做jms来使用。AMQ吞吐量不够,而且持久化message数据经过jdbc存在mysql,写入和读取message性能过低。而Kafka的吞吐量很是大。
5.4 push/pull 模型
对于消费者而言有两种方式从消息中间件获取消息:
①Push方式:由消息中间件主动地将消息推送给消费者,采用Push方式,能够尽量快地将消息发送给消费者;②Pull方式:由消费者主动向消息中间件拉取消息,会增长消息的延迟,即消息到达消费者的时间有点长
可是,Push方式会有一个坏处:若是消费者的处理消息的能力很弱(一条消息须要很长的时间处理),而消息中间件不断地向消费者Push消息,消费者的缓冲区可能会溢出。
AMQ的Push消费
ActiveMQ使用PUSH模型, 对于PUSH,broker很难控制数据发送给不一样消费者的速度。AMQ Broker将message推送给对应的BET consumer。ActiveMQ用prefetch limit 规定了一次能够向消费者Push(推送)多少条消息。当推送消息的数量到达了perfetch limit规定的数值时,消费者尚未向消息中间件返回ACK,消息中间件将再也不继续向消费者推送消息。
AMQ的Pull消费
ActiveMQ prefetch limit 设置成0意味着什么?意味着此时,消费者去轮询消息中间件获取消息。再也不是Push方式了,而是Pull方式了。即消费者主动去消息中间件拉取消息。
那么,ActiveMQ中如何采用Push方式或者Pull方式呢?从是否阻塞来看,消费者有两种方式获取消息。同步方式和异步方式。
同步方式使用的是ActiveMQMessageConsumer的receive()方法。而异步方式则是采用消费者实现MessageListener接口,监听消息。使用同步方式receive()方法获取消息时,prefetch limit便可以设置为0,也能够设置为大于0。
prefetch limit为零 意味着:“receive()方法将会首先发送一个PULL指令并阻塞,直到broker端返回消息为止,这也意味着消息只能逐个获取(相似于Request<->Response)”。
prefetch limit 大于零 意味着:“broker端将会批量push给client 必定数量的消息(<= prefetch),client端会把这些消息(unconsumedMessage)放入到本地的队列中,只要此队列有消息,那么receive方法将会当即返回,当必定量的消息ACK以后,broker端会继续批量push消息给client端。”
当使用MessageListener异步获取消息时,prefetch limit必须大于零了。由于,prefetch limit 等于零 意味着消息中间件不会主动给消费者Push消息,而此时消费者又用MessageListener被动获取消息(不会主动去轮询消息)。这两者是矛盾的。
Kafka只有Pull消费方式
Kafka使用PULL模型,PULL能够由消费者本身控制,可是PULL模型可能形成消费者在没有消息的状况下盲等,这种状况下能够经过long polling机制缓解,而对于几乎每时每刻都有消息传递的流式系统,这种影响能够忽略。Kafka 的 consumer 是以pull的形式获取消息数据的。 pruducer push消息到kafka cluster ,consumer从集群中pull消息。
如何学习呢?有没有免费资料?
我本身收集了一些Java资料,里面就包涵了一些BAT面试资料,以及一些 Java 高并发、分布式、微服务、高性能、源码分析、JVM等技术资料
资料获取方式:请加群BAT架构技术交流群:171662117
今天免费分享 免费分享!
转发 !
转发 !