Kafka工作原理解析以及主要配置详解
作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
无论是是Kafka集群,还是producer和consumer都依赖于Zookeeper集群保存一些mate信息,来保证系统可用性!这个特点会产生一个现象,即会产生大量的网络IO,所以说在企业生产环境中会单独开3到5台集群,这集群什么都不干,只开Zookeeper集群。所以说Zookeeper开放的节点一定要开网络监控告警,这是一个大数据运维的基本功!
一.Kafka简介
1>.什么是JMS
答:在Java中有一个角消息系统的东西,我们叫他Java Message Service,简称JMS。比如各种MQ。我们举个简单的例子,在java中进程之间通信需要socket,“路人甲”要向“路人乙”发送数据,需要“路人乙”开启服务,暴露端口。这样面临的问题就是如果“路人乙”不在线,“路人甲”就不能发送数据给“路人乙”。为了解决该问题就需要在“路人甲”和“路人乙”之间引入消息中间件,进行解耦。如下图所示:
2>.JMS的两种工作模式
第一种模式:点到点(point to point,简称P2P),典型的一对一模式(一个人发送数据的同时只有一个人接收数据),也有人称之为端到端(peer to peer)或者队列模式(queue)。
第二种模式:发布订阅模式(publish subscribe,简称P-S),典型的一对多模式(一个人发送数据的同时可以有多个人接收数据),也有人称为主题模式(在生产者和消费者之间加入了topic(主题),主题相当于公告栏,生产者发送消息到主题后,所有消费者都可以看到,功能类似于咱们平时接触的微信公众号)。
3>.Kafka的工作模式
答:Kafka的工作模式可以把JMS的两种模式结合在一起,我们称之为消费者组模式。
4>.什么是Kafka
答:Kafka和flume以及Sqoop一样,他们都是中间件(不含有业务的技术组件)。Kafka在官方定义是分布式消息系统。当然Kafka还可以用在做分布式数据库,除此之外,它还可以当做分布式缓存。
5>.ApacheKafka是一个分布式流媒体平台
ApacheKafka是一个分布式流媒体平台,这到底是什么意思呢?接下来我们看一下流媒体平台有三个关键功能如下:
第一:发布和订阅记录流,类似于消息队列或企业消息传递系统。
第二:以容错持久的方式存储记录流。
第三:处理记录发生的流。
6>.Kafka通常用于两大类应用
第一:构建可在系统或应用程序之间可靠获取数据的实时流数据管道。
第二:构建实时流应用程序,用于转换或响应数据流。
7>.kafka版本介绍
kafka起先由领英(linkedin创建)公司,开源后被Apache基金会纳入子项目。我们在下载Kafka时,你是如何区分它的版本呢?比如本篇博客下载kafka的版本是“kafka_2.11-1.1.0”,这个“2.11”是scala(java语言脚本化)版本而“1.1.0”是kafka版本。
关于Kafka本地部署请参考:https://www.cnblogs.com/yinzhengjie/p/9209058.html。
关于Scala的介绍请参考:https://www.cnblogs.com/yinzhengjie/tag/Scala%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/。
8>.kafka特点
第一:可以处理大量数据,TB级别; 第二:高吞吐量,支持每秒种百万消息,传输速度可达到300MB/s; 第三:分布式,支持在多个Server之间进行消息分区; 第四:多客户端支持,和多种语言进行协同; 第五:它是一个集群,扩容起来也相当方便;
9.关于Kafka知识总结
第一:在流式计算中,Kafka一般用来缓存数据,Storm通过消费Kafka的数据进行计算。
第二:Apache Kafka是一个开源消息系统,由Scala写成。是由Apache软件基金会开发的一个开源消息系统项目。
第三:Kafka最初是由LinkedIn公司开发,并于 2011年初开源。2012年10月从Apache Incubator毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台。
第四:Kafka是一个分布式消息队列。Kafka对消息保存时根据Topic进行归类,发送消息者称为Producer,消息接受者称为Consumer,此外kafka集群有多个kafka实例组成,每个实例(server)成为broker。
第五:无论是kafka集群,还是producer和consumer都依赖于zookeeper集群保存一些meta信息,来保证系统可用性。
第六:kafak官方下载地址:http://kafka.apache.org/downloads。
二.kafka消息队列
1>.kafka消息队列内部实现原理
点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除,pull) 点对点模型通常是一个基于拉取或者轮询的消息传送模型,这种模型从队列中请求信息,而不是将消息推送到客户端。这个模型的特点是发送到队列的消息被一个且只有一个接收者接收处理,即使有多个消息监听者也是如此。 发布/订阅模式(一对多,数据生产后,推送给所有订阅者,push) 发布订阅模型则是一个基于推送的消息传送模型。发布订阅模型可以有多种不同的订阅者,临时订阅者只在主动监听主题时才接收消息,而持久订阅者则监听主题的所有消息,即使当前订阅者不可用,处于离线状态。
2>.为什么需要消息队列
1>.解耦: 允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。 2>.冗余: 消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。 3>.扩展性: 因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。 4>.灵活性 & 峰值处理能力: 在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。 5>.可恢复性: 系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。 6>.顺序保证: 在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka保证一个Partition内的消息的有序性) 7>.缓冲: 有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。 8>.异步通信: 很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
3>. Kafka架构图简介
Kafka关键名词介绍: 一.Producer : 消息生产者,就是向kafka broker发消息的客户端。 二.Consumer : 消息消费者,向kafka broker取消息的客户端 三.Topic : 可以理解为一个队列,它是Kafka管理消息的实例。 四.Consumer Group (我们这里简称CG,即消费者组): 这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制-给consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。 关于Consumer Group我们要注意以下几点: 1>.在同一个CG的中,同时只能有一个consumer对topic进行消费; 2>.在同一个CG中,所有的consumer是不会重复消费数据的,也就是说,同一个topic中的某个Partition中的数据被当前CG的一个consumer消费后,是不会再被这个GC中的其它consumer再次进行消费啦; 3>.在同一个CG中,每一个consumer消费单元是都以Partition为消费单元的,换句话说,在同一个CG中,只要consumer和topic中的Partition建立RPC连接后,那么这个Partition中的所有数据只会被这个consumer消费。 五.Broker : 一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic。 六.Partition: Kafka集群中,Partition是生产者和消费者操作的最小单元。为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序。 七.Offset: kafka的存储文件都是按照offset.kafka来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。当然the first offset就是00000000000.kafka 八.Zookeeper集群 Zookeeper保存的东西有两个: 1>.Kafka集群节点的状态信息(便于管理leader和follower角色); 2>.消费者当前正在消费消息的状态信息(比如保存消费者消费的偏移量(Offset))。 温馨提示:它并没有保存生产者的一些元数据信息。 九.Replication Kafka存储数据的分区分为主从,即Leader和Follower。也就是说,kafka自己就有冗余机制,它将数据写入Leader的Partition之后,会将这份数据拷贝到其它broker的follower的Partition之中。温馨提示:这个存储在follower的Partition数据不会直接和消息生成者沟通,更不会跟消息消费者进行沟通,它仅仅是起到一个数据备份的作用!当Kafka的leader节点挂掉时,follower的各个节点会重写选举出新的leader,并由新的leader向外提供服务。 十.Event Kafka集群保存消息是以Partition去保存的,每一个Partition是按照队列去保存的,消息是以Event来包装消息的,一个消息就是一个event。因此每个Partition的消息是有序的,多个Partition之间的消息是无序的!
三.Kafka配置信息
1>.Broker配置信息
属性 | 默认值 | 描述 |
broker.id |
| 必填参数,broker的唯一标识 |
log.dirs | /tmp/kafka-logs | Kafka数据存放的目录。可以指定多个目录,中间用逗号分隔,当新partition被创建的时会被存放到当前存放partition最少的目录。换句话说,多个目录分布在不同磁盘上可以提高读写性能。 |
port | 9092 | BrokerServer接受客户端连接的端口号 |
zookeeper.connect | null | Zookeeper的连接串,格式为:hostname1:port1,hostname2:port2,hostname3:port3。可以填一个或多个,为了提高可靠性,建议都填上。注意,此配置允许我们指定一个zookeeper路径来存放此kafka集群的所有数据,为了与其他应用集群区分开,建议在此配置中指定本集群存放目录,格式为:hostname1:port1,hostname2:port2,hostname3:port3/chroot/path 。需要注意的是,消费者的参数要和此参数一致。 |
message.max.bytes | 1000000 | 服务器可以接收到的最大的消息大小。注意此参数要和consumer的maximum.message.size大小一致,否则会因为生产者生产的消息太大导致消费者无法消费。 |
num.io.threads | 8 | 服务器用来执行读写请求的IO线程数,此参数的数量至少要等于服务器上磁盘的数量。 |
queued.max.requests | 500 | I/O线程可以处理请求的队列大小,若实际请求数超过此大小,网络线程将停止接收新的请求。 |
socket.send.buffer.bytes | 100 * 1024 | The SO_SNDBUFF buffer the server prefers for socket connections. |
socket.receive.buffer.bytes | 100 * 1024 | The SO_RCVBUFF buffer the server prefers for socket connections. |
socket.request.max.bytes | 100 * 1024 * 1024 | 服务器允许请求的最大值, 用来防止内存溢出,其值应该小于 Java heap size. |
num.partitions | 1 | 默认partition数量,如果topic在创建时没有指定partition数量,默认使用此值,建议改为5 |
log.segment.bytes | 1024 * 1024 * 1024 | Segment文件的大小,超过此值将会自动新建一个segment,此值可以被topic级别的参数覆盖。 |
log.roll.{ms,hours} | 24 * 7 hours | 新建segment文件的时间,此值可以被topic级别的参数覆盖。 |
log.retention.{ms,minutes,hours} | 7 days | Kafka segment log的保存周期,保存周期超过此时间日志就会被删除。此参数可以被topic级别参数覆盖。数据量大时,建议减小此值。 |
log.retention.bytes | -1 | 每个partition的最大容量,若数据量超过此值,partition数据将会被删除。注意这个参数控制的是每个partition而不是topic。此参数可以被log级别参数覆盖。 |
log.retention.check.interval.ms | 5 minutes | 删除策略的检查周期 |
auto.create.topics.enable | true | 自动创建topic参数,建议此值设置为false,严格控制topic管理,防止生产者错写topic。 |
default.replication.factor | 1 | 默认副本数量,建议改为2。 |
replica.lag.time.max.ms | 10000 | 在此窗口时间内没有收到follower的fetch请求,leader会将其从ISR(in-sync replicas)中移除。 |
replica.lag.max.messages | 4000 | 如果replica节点落后leader节点此值大小的消息数量,leader节点就会将其从ISR中移除。 |
replica.socket.timeout.ms | 30 * 1000 | replica向leader发送请求的超时时间。 |
replica.socket.receive.buffer.bytes | 64 * 1024 | 套接字接收缓冲器,用于网络对领导者的复制数据请求。 |
replica.fetch.max.bytes | 1024 * 1024 | 尝试获取每个分区中的消息的Bayes请求副本发送给领导者。 |
replica.fetch.wait.max.ms | 500 | 将数据发送到领导的等待时间的最大时间,在由副本发送给领导者的获取请求中。 |
num.replica.fetchers | 1 | 用于复制来自领导者的消息的线程数。增加这个值可以增加跟随代理的I/O并行度。 |
fetch.purgatory.purge.interval.requests | 1000 | 取回请求炼狱的清除间隔(请求次数)。 |
zookeeper.session.timeout.ms | 6000 | ZooKeeper session 超时时间。如果在此时间内server没有向zookeeper发送心跳,zookeeper就会认为此节点已挂掉。 此值太低导致节点容易被标记死亡;若太高,.会导致太迟发现节点死亡。 |
zookeeper.connection.timeout.ms | 6000 | 客户端连接zookeeper的超时时间。 |
zookeeper.sync.time.ms | 2000 | H ZK follower落后 ZK leader的时间。 |
controlled.shutdown.enable | true | 允许broker shutdown。如果启用,broker在关闭自己之前会把它上面的所有leaders转移到其它brokers上,建议启用,增加集群稳定性。 |
auto.leader.rebalance.enable | true | 如果启用此功能,则控制器将自动尝试通过周期性地将领导力返回到每个分区的“首选”副本(如果可用)来平衡代理之间的分区领导。 |
leader.imbalance.per.broker.percentage | 10 | 每个经纪人的领导失衡比例。如果该比率高于经纪人的配置价值,则控制器将重新平衡领导地位。 |
leader.imbalance.check.interval.seconds | 300 | 检查领导者不平衡的频率。 |
offset.metadata.max.bytes | 4096 | 允许客户端以其偏移量保存的最大元数据量。 |
connections.max.idle.ms | 600000 | 空闲连接超时:服务器套接字处理器线程关闭比此更空闲的连接。 |
num.recovery.threads.per.data.dir | 1 | 启动时日志恢复和关闭时刷新的每个数据目录的线程数。 |
unclean.leader.election.enable | true | 指示是否启用ISR集合中未启用的副本作为最后手段被选为领导者,即使这样做可能导致数据丢失。 |
delete.topic.enable | false | 启用deletetopic参数,建议设置为true。 |
offsets.topic.num.partitions | 50 | 偏移提交主题的分区数。由于部署后更改此项目前不受支持,因此建议使用更高的设置来进行生产(例如100-200) |
offsets.topic.retention.minutes | 1440 | 年龄大于此年龄的偏移将被标记为删除。当日志清洁器压缩偏移主题时,实际清除将发生 |
offsets.retention.check.interval.ms | 600000 | 偏移管理器检查过时偏移的频率。 |
offsets.topic.replication.factor | 3 | 偏移提交主题的复制因子。为了确保更高的可用性,推荐更高的设置(例如,三或四)。如果偏移主题是在代理少于复制因子时创建的,那么将用较少的副本创建偏移主题。 |
offsets.topic.segment.bytes | 104857600 | 偏移量主题的段大小。由于它使用了压缩的主题,因此应保持相对较低,以便于更快的日志压缩和加载。 |
offsets.load.buffer.size | 5242880 | 当一个代理成为一组使用者组的偏移管理器时(即,当它成为偏移主题分区的领导时),就会发生偏移负载。此设置对应于在将偏移加载到偏移管理器的缓存中时从偏移片段读取时要使用的批处理大小(以字节为单位)。 |
offsets.commit.required.acks | -1 | 可以接受偏移提交之前所需的确认数。这类似于生产者的确认设置。一般来说,默认值不应该被重写。 |
offsets.commit.timeout.ms | 5000 | 偏移提交将被延迟,直到该超时或所需副本数已接收到偏移提交。这类似于生产者请求超时。 |
2>.Producer配置信息
属性 | 默认值 | 描述 |
metadata.broker.list |
| 启动时producer查询brokers的列表,可以是集群中所有brokers的一个子集。注意,这个参数只是用来获取topic的元信息用,producer会从元信息中挑选合适的broker并与之建立socket连接。格式是:host1:port1,host2:port2。 |
request.required.acks | 0 | 0:这意味着生产者producer不等待来自broker同步完成的确认继续发送下一条(批)消息。此选项提供最低的延迟但最弱的耐久性保证(当服务器发生故障时某些数据会丢失,如leader已死,但producer并不知情,发出去的信息broker就收不到)。
1:这意味着producer在leader已成功收到的数据并得到确认后发送下一条message。此选项提供了更好的耐久性为客户等待服务器确认请求成功(被写入死亡leader但尚未复制将失去了唯一的消息)。
-1:这意味着producer在follower副本确认接收到数据后才算一次发送完成。 此选项提供最好的耐久性,我们保证没有信息将丢失,只要至少一个同步副本保持存活。
三种机制,性能依次递减 (producer吞吐量降低),数据健壮性则依次递增。 |
request.timeout.ms | 10000 | Broker等待ack的超时时间,若等待时间超过此值,会返回客户端错误信息。 |
producer.type | sync | 同步异步模式。async表示异步,sync表示同步。如果设置成异步模式,可以允许生产者以batch的形式push数据,这样会极大的提高broker性能,推荐设置为异步。 |
serializer.class | kafka.serializer.DefaultEncoder | 序列号类,.默认序列化成 byte[] 。 |
key.serializer.class |
| Key的序列化类,默认同上。 |
partitioner.class | kafka.producer.DefaultPartitioner | Partition类,默认对key进行hash。 |
compression.codec | none | 指定producer消息的压缩格式,可选参数为: “none”, “gzip” and “snappy”。 |
compressed.topics | null | 启用压缩的topic名称。若上面参数选择了一个压缩格式,那么压缩仅对本参数指定的topic有效,若本参数为空,则对所有topic有效。 |
message.send.max.retries | 3 | Producer发送失败时重试次数。若网络出现问题,可能会导致不断重试。 |
retry.backoff.ms | 100 | 在每次重试之前,制作人刷新相关主题的元数据以查看是否已经选出新的领导者。由于领导者选举需要一些时间,因此此属性指定生产者在刷新元数据之前等待的时间量。 |
topic.metadata.refresh.interval.ms | 600 * 1000 | 当出现故障(分区丢失,领导不可用……)时,生产者通常刷新来自代理的主题元数据。它也会定期轮询(默认值:每10分钟6 000毫秒)。如果将此设置为负值,元数据只会在故障时刷新。如果将此设置为零,则在发送的每个消息(不推荐)之后,元数据将被刷新。重要提示:刷新只在发送消息之后进行,所以如果生产者从不发送消息,则元数据永远不会被刷新 |
queue.buffering.max.ms | 5000 | 启用异步模式时,producer缓存消息的时间。比如我们设置成1000时,它会缓存1秒的数据再一次发送出去,这样可以极大的增加broker吞吐量,但也会造成时效性的降低。 |
queue.buffering.max.messages | 10000 | 采用异步模式时producer buffer 队列里最大缓存的消息数量,如果超过这个数值,producer就会阻塞或者丢掉消息。 |
queue.enqueue.timeout.ms | -1 | 当达到上面参数值时producer阻塞等待的时间。如果值设置为0,buffer队列满时producer不会阻塞,消息直接被丢掉。若值设置为-1,producer会被阻塞,不会丢消息。 |
batch.num.messages | 200 | 采用异步模式时,一个batch缓存的消息数量。达到这个数量值时producer才会发送消息。 |
send.buffer.bytes | 100 * 1024 | Socket write buffer size |
client.id | “” | 客户端ID是在每个请求中发送的帮助用户跟踪调用的用户指定字符串。它应该在逻辑上识别请求的应用程序 |
3>.Consumer配置信息
属性 | 默认值 | 描述 |
group.id |
| Consumer的组ID,相同goup.id的consumer属于同一个组。 |
zookeeper.connect |
| Consumer的zookeeper连接串,要和broker的配置一致。 |
consumer.id | null | 如果不设置会自动生成。 |
socket.timeout.ms | 30 * 1000 | 网络请求的socket超时时间。实际超时时间由max.fetch.wait + socket.timeout.ms 确定。 |
socket.receive.buffer.bytes | 64 * 1024 | The socket receive buffer for network requests. |
fetch.message.max.bytes | 1024 * 1024 | 查询topic-partition时允许的最大消息大小。consumer会为每个partition缓存此大小的消息到内存,因此,这个参数可以控制consumer的内存使用量。这个值应该至少比server允许的最大消息大小大,以免producer发送的消息大于consumer允许的消息。 |
num.consumer.fetchers | 1 | The number fetcher threads used to fetch data. |
auto.commit.enable | true | 如果此值设置为true,consumer会周期性的把当前消费的offset值保存到zookeeper。当consumer失败重启之后将会使用此值作为新开始消费的值。 |
auto.commit.interval.ms | 60 * 1000 | Consumer提交offset值到zookeeper的周期。 |
queued.max.message.chunks | 2 | 用来被consumer消费的message chunks 数量, 每个chunk可以缓存fetch.message.max.bytes大小的数据量。 |
auto.commit.interval.ms | 60 * 1000 | Consumer提交offset值到zookeeper的周期。 |
queued.max.message.chunks | 2 | 用来被consumer消费的message chunks 数量, 每个chunk可以缓存fetch.message.max.bytes大小的数据量。 |
fetch.min.bytes | 1 | 服务器为获取请求返回的最小数据量。如果可用的数据不足,请求将等待大量的数据在回答请求之前累积。 |
fetch.wait.max.ms | 100 | 如果没有足够的数据立即满足fetch.min.bytes,服务器在回答获取请求之前将阻塞的最大时间。 |
rebalance.backoff.ms | 2000 | 再平衡期间重试的退避时间。 |
refresh.leader.backoff.ms | 200 | 在试图确定刚刚失去它的首领的分区的首领之前等待退避时间。 |
auto.offset.reset | largest | 当ZooKeeper中没有初始偏移量或偏移量超出范围时该怎么做;最小:自动将偏移量重置为最小偏移;最大:自动将偏移量重置为最大偏移;其他任何事情:向使用者抛出异常; |
consumer.timeout.ms | -1 | 若在指定时间内没有消息消费,consumer将会抛出异常。 |
exclude.internal.topics | true | 来自内部主题(如偏移)的消息是否应该暴露给消费者。 |
zookeeper.session.timeout.ms | 6000 | zookeeper会话超时。如果消费者在这段时间内没有向饲养员心跳,则被认为是死亡的,并且会发生再平衡。 |
zookeeper.connection.timeout.ms | 6000 | 客户端在向动物管理员建立连接时等待的最大时间。 |
zookeeper.sync.time.ms | 2000 | ZK追随者能在ZK领导下走多远 |
4>.kafka最新版本的配置参数
其实上述参数只是目前来说我们可能会用到一些部分参数举例,随着Kafka版本的升级变更(截止2018-09-15日,发布最新的kafka版本是2018-07-28日发布的kafka-2.0.0),有的参数消失,也有的参数被加入进来,而传承下来的参数一般都不太会修改既定的默认值,参数可能会有略微的调整,详情请参考官网链接:http://kafka.apache.org/documentation.html#configuration。
博主推荐阅读,不是给他们打广告,而是这些博主写的是真不错,指的我去学习(大家也可以一起跟着扫个忙):
推荐一:Kafka配置项unclean.leader.election.enable造成consumer出现offset重置现象(https://www.cnblogs.com/felixzh/p/9088787.html)
推荐二:Kafka参数图鉴——unclean.leader.election.enable(https://blog.youkuaiyun.com/u013256816/article/details/80790185)
推荐三:kafka各版本差异(https://www.aliyun.com/zixun/content/1_5_2002969.html)