kafka3.x核心知识速查手册-一快速上手篇(代码片段)

roykingw roykingw     2022-12-01     231

关键词:

文章目录

Kafka3.x速查手册

一、快速上手篇

-- 楼兰

​ 各种操作尽量玩熟一点。因为以后的开发过程中,代码的使用机会多的是。但是服务本身,如果在学习阶段不多玩一玩,开发时就基本接触不到了。另外,快速熟练的搭建kafka服务,对于快速验证一些基于Kafka的解决方案,也是非常有用的。

一、Kafka介绍

1、MQ的作用:

异步、解耦、削峰

2、Kafka的适用场景:

​ 优点:吞吐量大

​ 缺点:功能比较单一,大吞吐下有丢消息的可能。Topic太多,性能会有所下降。

​ 适用场景:日志处理,大数据。

二、Kafka快速上手

1、实验环境

​ 准备了三台虚拟机 192.168.232.128~130,预备搭建三台机器的集群。

​ 三台机器均预装CentOS7 操作系统。分别配置机器名 worker1,worker2,worker3。然后需要关闭防火墙(实验环境建议关闭)。

firewall-cmd --state   查看防火墙状态
systemctl stop firewalld.service   关闭防火墙

​ 然后三台机器上都需要安装JAVA。JAVA的安装过程就不多说了。实验中采用目前用得最多的JAVA 8 版本就可以了。

​ 下载kafka,选择当前最新的3.2.0版本。下载地址:https://downloads.apache.org/kafka/3.2.0/ 选择kafka_2.13-3.2.0.tgz 进行下载。

关于kafka的版本,前面的2.13是开发kafka的scala语言的版本,后面的3.2.0是kafka应用的版本。kafka支持Scala 2.12和2.13两个版本。Scala是一种运行于JVM虚拟机之上的语言,在运行时,只需要安装JDK就可以了,选哪个版本没有区别。但是后面调试源码时就会有区别。因为Scala语言的版本并不是向后兼容的。

另外,在选择kafka版本时,建议先去kafka的官网看下发布日志,了解一下各个版本的特性。 https://kafka.apache.org/downloads。 例如3.2.0版本开始将log4j日志框架替换成了reload4j,这也是应对2021年log4j框架爆发严重BUG后的一种应对方法。

​ 下载Zookeeper,下载地址 https://zookeeper.apache.org/releases.html ,Zookeeper的版本并没有强制要求,这里我们选择比较新的3.6.1版本。

kafka的安装程序中自带了Zookeeper,可以在kafka的安装包的libs目录下查看到zookeeper的客户端jar包。但是,通常情况下,为了让应用更好维护,我们会使用单独部署的Zookeeper,而不使用kafka自带的Zookeeper。

​ 下载完成后,将这两个工具包上传到三台服务器上,解压后,分别放到/app/kafka和/app/zookeeper目录下。并将部署目录下的bin目录路径配置到path环境变量中。

2、单机服务体验

​ 下载下来的Kafka安装包不需要做任何的配置,就可以直接单击运行。这通常是快速了解Kafka的第一步。

​ **1、启动Kafka之前需要先启动Zookeeper。**这里就用Kafka自带的Zookeeper。启动脚本在bin目录下。

cd $KAKFKA_HOME
nohup bin/zookeeper-server-start.sh config/zookeeper.properties & 

注意下脚本是不是有执行权限。

​ 从nohup.out中可以看到zookeeper默认会在2181端口启动。通过jps指令看到一个QuorumPeerMain进程,确定服务启动成功。

2、启动Kafka。

nohup bin/kafka-server-start.sh config/server.properties &

​ 启动完成后,使用jps指令,看到一个kafka进程,确定服务启动成功。服务会默认在9092端口启动。

3、简单收发消息

​ Kafka的基础工作机制是消息发送者可以将消息发送到kafka上指定的topic,而消息消费者,可以从指定的topic上消费消息。

​ 首先,可以使用Kafka提供的客户端脚本创建Topic

#创建Topic
bin/kafka-topics.sh --create --topic test --bootstrap-server localhost:9092
#查看Topic
bin/kafka-topics.sh --describe --topic test --bootstrap-server localhost:9092

​ 然后,启动一个消息发送者端。往一个名为test的Topic发送消息。

bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test

​ 当命令行出现 > 符号后,随意输入一些字符。Ctrl+C 退出命令行。这样就完成了往kafka发消息的操作。

如果不提前创建Topic,那么在第一次往一个之前不存在的Topic发送消息时,消息也能正常发送,只是会抛出LEADER_NOT_AVAILABLE警告。

[oper@worker1 kafka_2.13-3.2.0]$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
>123
12[2021-03-05 14:00:23,347] WARN [Producer clientId=console-producer] Error while fetching metadata with correlation id 1 : test=LEADER_NOT_AVAILABLE (org.apache.kafka.clients.NetworkClient)
3[2021-03-05 14:00:23,479] WARN [Producer clientId=console-producer] Error while fetching metadata with correlation id 3 : test=LEADER_NOT_AVAILABLE (org.apache.kafka.clients.NetworkClient)

[2021-03-05 14:00:23,589] WARN [Producer clientId=console-producer] Error while fetching metadata with correlation id 4 : test=LEADER_NOT_AVAILABLE (org.apache.kafka.clients.NetworkClient)
>>123

这是因为Broker端在创建完主题后,会显示通知Clients端LEADER_NOT_AVAILABLE异常。Clients端接收到异常后,就会主动去更新元数据,获取新创建的主题信息。

​ 然后启动一个消息消费端,从名为test的Topic上接收消息。

[oper@worker1 kafka_2.13-3.2.0]$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test             
qwe
qwe
123
123
123
^CProcessed a total of 5 messages

​ 这样就完成了一个基础的交互。这其中,生产者和消费者并不需要同时启动。他们之间可以进行数据交互,但是又并不依赖于对方。没有生产者,消费者依然可以正常工作,反过来,没有消费者,生产者也依然可以正常工作。这也体现出了生产者和消费者之间的解耦。

3、搭建集群服务

​ 单机服务通常只是用来进行体验和测试,实际工作中,都是需要部署成集群工作,这样才能体现出Kafka的优势。 Kafka的集群架构大体是这样的:

​ 其中有几个重要的概念需要理解一下:

  • 客户端Client: 包括消息生产者 和 消息消费者。之前简单接触过。
  • 消费者组:每个消费者可以指定一个所属的消费者组,相同消费者组的消费者共同构成一个逻辑消费者组。每一个消息会被多个感兴趣的消费者组消费,但是在每一个消费者组内部,一个消息只会被消费一次。
  • 服务端Broker:一个Kafka服务器就是一个Broker。多个Broker可以通过组成集群,对外提供统一的服务。
  • 选举中心(Zookeeper):Broker集群的管理者。主要进行Broker的服务注册以及Leader选举功能。通过外置的Zookeeper服务,在Broker中选举产生Leader节点。
  • 话题Topic:这是一个逻辑概念,一个Topic被认为是业务含义相同的一组消息。客户端都通过绑定Topic来生产或者消费自己感兴趣的话题。
  • 分区Partition:Topic只是一个逻辑概念,而Partition就是实际存储消息的组件。每个Partiton就是一个queue队列结构。Kafka是面向海量消息设计的,一个Topic下的消息会非常多,这些消息会分成不同的Partition分布在多个Broker上。
  • 副本Replica:在Broker集群内部,可以给每个Partition设置一个或多个消息副本,用来备份Partition的消息。这样可以防止Partition损坏造成的数据丢失。
  • Leader * Follower:每个Partition都有一个或多个备份。Kafka会通过选举中心,给每个Partition选举出一个主节点Leader,其他节点就是从节点Follower。主节点负责响应客户端的具体业务请求,并保存消息。而从节点则负责同步主节点的数据。当主节点发生故障时,Kafka会选举出一个从节点成为新的主节点。

​ 接下来就来手动部署一下基于Zookeeper的Kafka集群。其中,选举中心部分,Zookeeper和Kraft都是一种多数同意的选举机制,允许集群中少数节点出现故障。因此,在搭建集群时,通常都是采用3,5,7这样的奇数节点,这样可以最大化集群的高可用特性。 在后续的实验过程中,我们会在三台服务器上都部署Zookeeper和Kafka。

1、部署Zookeeper集群

​ 这里采用之前单独下载的Zookeeper来部署集群。

​ 先将下载下来的Zookeeper解压到/app/zookeeper目录。

​ 然后进入conf目录,修改配置文件。在conf目录中,提供了一个zoo_sample.cfg文件,这是一个示例文件。我们只需要将这个文件复制一份zoo.cfg(cp zoo_sample.cfg zoo.cfg),修改下其中的关键配置就可以了。其中比较关键的修改参数如下:

#Zookeeper的本地数据目录,默认是/tmp/zookeeper。这是Linux的临时目录,随时会被删掉。
dataDir=/app/zookeeper/data
#Zookeeper的服务端口
clientPort=2181
#集群节点配置
server.1=worker1:2888:3888
server.2=worker2:2888:3888
server.3=worker3:2888:3888

其中2181是对客户端开放的服务端口。

集群配置部分, server.x这个x就是节点在集群中的myid。后面的2888端口是集群内部数据传输使用的端口。3888是集群内部进行选举使用的端口。

​ 接下来将整个Zookeeper的应用目录分发到另外两台机器上。就可以在三台机器上都启动Zookeeper服务了。

bin/zkServer.sh --config conf start 

​ 启动完成后,使用jps指令可以看到一个QuorumPeerMain进程就表示服务启动成功。

​ 三台机器都启动完成后,可以查看下集群状态。

[root@hadoop02 zookeeper-3.5.8]# bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /app/zookeeper/zookeeper-3.5.8/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader

这其中Mode 为leader就是主节点,follower就是从节点。

2、部署Kafka集群

​ 部署Kafka的方式跟部署Zookeeper差不多,就是解压、配置、启服务三板斧。

​ 首先将Kafka解压到/app/kafka目录下。

​ 然后进入config目录,修改server.properties。这个配置文件里面的配置项非常多,下面列出几个要重点关注的配置。

#broker 的全局唯一编号,不能重复,只能是数字。
broker.id=0
#数据文件地址。同样默认是给的/tmp目录。
log.dirs=/app/kafka/logs
#默认的每个Topic的分区数
num.partitions=1
#zookeeper的服务地址
zookeeper.connect=worker1:2181,worker2:2181,worker3:2181

配置文件中的注释非常细致,可以关注一下。

broker.id需要每个服务器上不一样,分发到其他服务器上时,要注意修改一下。

注册到同一个zookeeper集群上的节点,会自动组成集群。

​ 接下来就可以启动kafka服务了。启动服务时需要指定配置文件。

bin/kafka-server-start.sh -daemon config/server.properties

-daemon表示后台启动kafka服务,这样就不会占用当前命令窗口。

​ 通过jps指令可以查看Kafka的进程。

4、理解Topic、Partition和Broker

​ 接下来可以对比一下之前的单机服务,快速理解Kafka的集群功能。

--创建一个分布式的Topic
[oper@worker1 bin]$ ./kafka-topics.sh --bootstrap-server worker1:9092 --create --replication-factor 2 --partitions 4 --topic disTopic
Created topic disTopic.
--列出所有的Topic
[oper@worker1 bin]$ ./kafka-topics.sh --bootstrap-server worker1:9092 --list
__consumer_offsets
disTopic
--查看列表情况
[oper@worker1 bin]$ ./kafka-topics.sh --bootstrap-server worker1:9092 --describe --topic disTopic
Topic: disTopic TopicId: vX4ohhIER6aDpDZgTy10tQ PartitionCount: 4       ReplicationFactor: 2    Configs: segment.bytes=1073741824
        Topic: disTopic Partition: 0    Leader: 2       Replicas: 2,1   Isr: 2,1
        Topic: disTopic Partition: 1    Leader: 1       Replicas: 1,0   Isr: 1,0
        Topic: disTopic Partition: 2    Leader: 0       Replicas: 0,2   Isr: 0,2
        Topic: disTopic Partition: 3    Leader: 2       Replicas: 2,0   Isr: 2,0

​ 从这里可以看到,

1、–create创建集群,可以指定一些补充的参数。大部分的参数都可以在配置文件中指定默认值。

  • partitons参数表示分区数,这个Topic下的消息会分别存入这些不同的分区中。示例中创建的disTopic,指定了四个分区,也就是说数据会划分为四个部分。
  • replication-factor表示每个分区有几个备份。示例中创建的disTopic,指定了每个partition有两个备份。

2、–describe查看Topic信息。

  • partiton参数列出了四个partition,后面带有分区编号,用来标识这些分区。
  • Leader表示这一组partiton中的Leader节点是哪一个。这个Leader节点就是负责响应客户端请求的主节点。从这里可以看到,Kafka中的每一个Partition都会分配Leader,也就是说每个Partition都有不同的节点来负责响应客户端的请求。这样就可以将客户端的请求做到尽量的分散。
  • Replicas参数表示这个partition的多个备份是分配在哪些Broker上的。这里的0,1,2就对应配置集群时指定的broker.id。但是,Replicas列出的只是一个逻辑上的分配情况,并不关心数据实际是不是按照这个分配。甚至有些节点服务挂了之后,Replicas中也依然会列出节点的ID。
  • ISR参数表示partition的实际分配情况。他是Replicas的一个子集,只列出那些当前还存活,并且已经完成了数据同步的那些Broker节点。

对应之前的Kafka集群架构图就不难理解这些参数了。

​ 从这里可以看到, Kafka当中,Topic是一个数据集合的逻辑单元。同一个Topic下的数据,实际上是存储在Partition分区中的,Partition就是数据存储的物理单元。而Broker是Partition的物理载体,这些Partition分区会尽量均匀的分配到不同的Broker机器上。

​ 这其中,与数据也就是消息,联系最为紧密的,其实就是Partition了。之前在配置Kafka集群时,指定了一个log.dirs属性,指向了一个服务器上的日志目录。进入这个目录,就能看到每个Broker的实际数据承载情况。

Kafka为何要这样来设计Topic、Partition和Broker的关系呢?

1、Kafka设计需要支持海量的数据,而这样庞大的数据量,一个Broker是存不下的。那就拆分成Partition,每个Broker只存一部分的数据。这样极大的扩展了集群的吞吐量

2、每个Partition保留了一部分的消息副本,如果放到一个Broker上,就容易出现单点故障。所以就给每个Partition设计一个备份,从而保证数据安全。另外,多备份的Partition设计也提高了读取消息时的并发度

5、Kraft集群–了解

1、关于Kraft

​ Kraft是Kafka从2.8.0版本开始支持的一种新的集群架构方式。基础的工作机制和之前基于Zookeeper的集群机制是差不多的,主要的不同就是摆脱了对Zookeeper的依赖。将原本由Zookeeper来维护的集群信息,转而由Kafka集群自己管理。

​ 传统的Kafka框架,会将每个节点的状态信息统一保存在Zookeeper中,并通过Zookeeper动态选举产生一个Controller节点,通过Controller节点来管理Kafka集群。在Kraft集群中,会固定配置几台Broker节点来担任Controller的角色,各组Partition的Leader节点就会在这些Controller上产生。原本保存在Zookeeper中的元数据也转而保存到Controller节点中。

Raft协议是目前进行去中心化集群管理的一种常见算法,类似于之前的Paxos协议,是一种基于多数同意,从而产生集群共识的分布式算法。Kraft则是Kafka基于Raft协议进行的定制算法。

​ 新的Kraft集群相比传统基于Zookeeper的集群,有一些很明显的好处:

  • Kafka可以不依赖于外部框架独立运行。这样减少Zookeeper性能抖动对Kafka集群性能的影响,同时Kafka产品的版本迭代也更自由。
  • Controller不再由Zookeeper动态选举产生,而是由配置文件进行固定。这样比较适合配合一些高可用工具来保持集群的稳定性。
  • Zookeeper的产品特性决定了他不适合存储大量的数据,这对Kafka的集群规模(确切的说应该是Partition规模)是极大的限制。摆脱Zookeeper后,集群扩展时元数据的读写能力得到增强。

​ 不过,由于分布式算法的复杂性。Kraft集群和同样基于Raft协议定制的RocketMQ的Dledger集群一样,都还在实验阶段,在真实企业开发中,用得相对还是比较少。

2、配置Kraft集群

​ 在Kafka的config目录下,提供了一个kraft的文件夹,在这里面就是Kraft协议的参考配置文件。在这个文件夹中有三个配置文件,broker.properties,controller.properties,server.properties,分别给出了Kraft中三种不同角色的示例配置。

  • broker.properties: 数据节点
  • controller.properties: Controller控制节点
  • server.properties: 即可以是数据节点,又可以是Controller控制节点。

这里同样列出几个比较关键的配置项,按照自己的环境进行定制即可。

#配置当前节点的角色。Controller相当于Zookeeper的功能,负责集群管理。Broker提供具体的消息转发服务。
process.roles=broker,controller
#配置当前节点的id。与普通集群一样,要求集群内每个节点的ID不能重复。
node.id=1
#配置集群的投票节点。其中@前面的是节点的id,后面是节点的地址和端口,这个端口跟客户端访问的端口是不一样的。通常将集群内的所有Controllor节点都配置进去。
controller.quorum.voters=1@worker1:9093,2@worker2:9093,3@worker3:9093
#Broker对客户端暴露的服务地址。基于PLAINTEXT协议。
advertised.listeners=PLAINTEXT://worker1:9092
#Controller服务协议的别名。默认就是CONTROLLER
controller.listener.names=CONTROLLER
#配置监听服务。不同的服务可以绑定不同的接口。这种配置方式在端口前面是省略了一个主机IP的,主机IP默认是使用的java.net.InetAddress.getCanonicalHostName()
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
#数据文件地址。默认配置在/tmp目录下。
log.dirs=/app/kafka/kraft-log
#topic默认的partition分区数。
num.partitions=2

​ 将配置文件分发,并修改每个服务器上的node.id属性和advertised.listeners属性后,就可以像之间一样指定这个配置文件,启动kafka集群了。

kafka3.x核心速查手册二客户端使用篇-5发送应答机制(代码片段)

​这是在开发过程中比较重要的一个机制,也是面试过程中最喜欢问的一个机制,被无数教程指导吹得神乎其神。所以这里也简单介绍一下。​其实这里涉及到的,就是在Producer端一个不太起眼的属性ACKS_CONFIG。 publics... 查看详情

kafka3.x核心速查手册二客户端使用篇-5发送应答机制(代码片段)

​这是在开发过程中比较重要的一个机制,也是面试过程中最喜欢问的一个机制,被无数教程指导吹得神乎其神。所以这里也简单介绍一下。​其实这里涉及到的,就是在Producer端一个不太起眼的属性ACKS_CONFIG。 publics... 查看详情

kafka3.x核心速查手册二客户端使用篇-6消息发送幂等性(代码片段)

​当你仔细看下源码中对于acks属性的说明,会看到另外一个单词,idempotence。这个单词的意思就是幂等性。在Producer发送消息到Broker的这个场景中,幂等性是表示Producer不论向Broker发送多少次重复的数据,Broker端都... 查看详情

kafka3.x核心速查手册二客户端使用篇-6消息发送幂等性(代码片段)

​当你仔细看下源码中对于acks属性的说明,会看到另外一个单词,idempotence。这个单词的意思就是幂等性。在Producer发送消息到Broker的这个场景中,幂等性是表示Producer不论向Broker发送多少次重复的数据,Broker端都... 查看详情

kafka3.x核心速查手册二客户端使用篇-3消息序列化机制(代码片段)

​在之前的简单示例中,Producer指定了两个属性KEY_SERIALIZER_CLASS_CONFIG和VALUE_SERIALIZER_CLASS_CONFIG,对于这两个属性,在ProducerConfig中都有配套的说明属性。 publicstaticfinalStringKEY_SERIALIZER_CLASS_CONFIG=" 查看详情

kafka3.x核心速查手册三服务端原理篇-1zookeeper整体数据

​这一部分主要是理解Kafka的服务端重要原理。但是Kafak为了保证高吞吐,高性能,很多具体实现都是相当复杂的。如果直接跳进去学习研究,很快就会晕头转向。所以,找一个简单清晰的主线就显得尤为重要。这... 查看详情

kafka3.x核心速查手册三服务端原理篇-1zookeeper整体数据

​这一部分主要是理解Kafka的服务端重要原理。但是Kafak为了保证高吞吐,高性能,很多具体实现都是相当复杂的。如果直接跳进去学习研究,很快就会晕头转向。所以,找一个简单清晰的主线就显得尤为重要。这... 查看详情

kafka3.x核心速查手册三服务端原理篇-3broker故障恢复机制(代码片段)

4、LeaderPartition自动平衡机制​在一组Partiton中,LeaderPartition通常是比较繁忙的节点,因为他要负责与客户端的数据交互,以及向Follower同步数据。默认情况下,Kafka会尽量将LeaderPartition分配到不同的Broker节点上࿰... 查看详情

kafka3.x核心速查手册三服务端原理篇-3broker故障恢复机制(代码片段)

4、LeaderPartition自动平衡机制​在一组Partiton中,LeaderPartition通常是比较繁忙的节点,因为他要负责与客户端的数据交互,以及向Follower同步数据。默认情况下,Kafka会尽量将LeaderPartition分配到不同的Broker节点上࿰... 查看详情

kafka3.x核心速查手册二客户端使用篇-1从基础的客户端说起(代码片段)

​这一部分主要是从客户端使用的角度来理解Kakfa的重要机制。重点依然是要建立自己脑海中的Kafka消费模型。Kafka的HighLevelAPI使用是非常简单的,所以梳理模型时也要尽量简单化,主线清晰,细节慢慢扩展。#一、从... 查看详情

kafka3.x核心速查手册二客户端使用篇-1从基础的客户端说起(代码片段)

​这一部分主要是从客户端使用的角度来理解Kakfa的重要机制。重点依然是要建立自己脑海中的Kafka消费模型。Kafka的HighLevelAPI使用是非常简单的,所以梳理模型时也要尽量简单化,主线清晰,细节慢慢扩展。#一、从... 查看详情

kafka3.x核心速查手册二客户端使用篇-4消息路由机制(代码片段)

​了解前面两个机制后,你自然会想到一个问题。就是消息如何进行路由?也即是两个相关联的问题。Producer会根据消息的key选择Partition,具体如何通过key找Partition呢?一个消费者组会共同消费一个Topic下的多个Par... 查看详情

kafka3.x核心速查手册二客户端使用篇-4消息路由机制(代码片段)

​了解前面两个机制后,你自然会想到一个问题。就是消息如何进行路由?也即是两个相关联的问题。Producer会根据消息的key选择Partition,具体如何通过key找Partition呢?一个消费者组会共同消费一个Topic下的多个Par... 查看详情

kafka3.x核心速查手册二客户端使用篇-2分组消费机制(代码片段)

渔与鱼:Kafka的HighLevelAPI的重要目的就是想要简化客户端的使用方式,所以对于API的使用,尽量熟练就可以了。对于其他重要的属性,都可以通过源码中的描述去学习,并且可以设计一些场景去进行验证。其重... 查看详情

kafka3.x核心速查手册二客户端使用篇-2分组消费机制(代码片段)

渔与鱼:Kafka的HighLevelAPI的重要目的就是想要简化客户端的使用方式,所以对于API的使用,尽量熟练就可以了。对于其他重要的属性,都可以通过源码中的描述去学习,并且可以设计一些场景去进行验证。其重... 查看详情

kafka3.x核心速查手册二客户端使用篇-7生产者消息事务(代码片段)

​通过Kafka的幂等性特性,可以保证单条消息的数据安全性。而在此基础上,Kafka还为批量消息的数据安全性,设计提供了消息事务功能。**Kafka的生产者消息事务,是保证同一批次的多条消息,可以同时保证同... 查看详情

kafka3.x核心速查手册二客户端使用篇-7生产者消息事务(代码片段)

​通过Kafka的幂等性特性,可以保证单条消息的数据安全性。而在此基础上,Kafka还为批量消息的数据安全性,设计提供了消息事务功能。**Kafka的生产者消息事务,是保证同一批次的多条消息,可以同时保证同... 查看详情

kafka3.x核心速查手册二客户端使用篇-5发送应答机制(代码片段)

​这是在开发过程中比较重要的一个机制,也是面试过程中最喜欢问的一个机制,被无数教程指导吹得神乎其神。所以这里也简单介绍一下。​其实这里涉及到的,就是在Producer端一个不太起眼的属性ACKS_CONFIG。 publics... 查看详情