1 概念
Kafka是最初由Linkedin公司开发,是⼀个分布式、分区的、多副本的、多⽣产者、多订阅者,基于zookeeper协 调的分布式⽇志系统(也可以当做MQ系统),常⻅可以⽤于web/nginx⽇志、访问⽇志,消息服务等等,Linkedin于 2010年贡献给了Apache基⾦会并成为顶级开源项⽬。
主要应⽤场景是:⽇志收集系统和消息系统。
Kafka主要设计⽬标如下:
·以时间复杂度为O(1)的⽅式提供消息持久化能⼒,即使对TB级以上数据也能保证常数时间的访问性能。
·⾼吞吐率。即使在⾮常廉价的商⽤机器上也能做到单机⽀持每秒100K条消息的传输。
·⽀持Kafka Server间的消息分区,及分布式消费,同时保证每个partition内的消息顺序传输。
·同时⽀持离线数据处理和实时数据处理。
·⽀持在线⽔平扩展
有两种主要的消息传递模式:点对点传递模式、发布-订阅模式。⼤部分的消息系统选⽤发布-订阅模式。Kafka就 是⼀种发布-订阅模式。
对于消息中间件,消息分推拉两种模式。Kafka只有消息的拉取,没有推送,可以通过轮询实现消息的推送。
1. Kafka在⼀个或多个可以跨越多个数据中⼼的服务器上作为集群运⾏。
2. Kafka集群中按照主题分类管理,⼀个主题可以有多个分区,⼀个分区可以有多个副本分区。
3. 每个记录由⼀个键,⼀个值和⼀个时间戳组成。
Kafka具有四个核⼼API:
1. Producer API:允许应⽤程序将记录流发布到⼀个或多个Kafka主题。
2. Consumer API:允许应⽤程序订阅⼀个或多个主题并处理为其⽣成的记录流。
3. Streams API:允许应⽤程序充当流处理器,使⽤⼀个或多个主题的输⼊流,并⽣成⼀个或多个输出主题的 输出流,从⽽有效地将输⼊流转换为输出流。
4. Connector API:允许构建和运⾏将Kafka主题连接到现有应⽤程序或数据系统的可重⽤⽣产者或使⽤者。例 如,关系数据库的连接器可能会捕获对表的所有更改。
2 kafka优势
1. ⾼吞吐量:单机每秒处理⼏⼗上百万的消息量。即使存储了许多TB的消息,它也保持稳定的性能。
2. ⾼性能:单节点⽀持上千个客户端,并保证零停机和零数据丢失。
3. 持久化数据存储:将消息持久化到磁盘。通过将数据持久化到硬盘以及replication防⽌数据丢失。
1. 零拷⻉ 2. 顺序读,顺序写 3. 利⽤Linux的⻚缓存
4. 分布式系统,易于向外扩展。所有的Producer、Broker和Consumer都会有多个,均为分布式的。⽆需停机 即可扩展机器。多个Producer、Consumer可能是不同的应⽤。
5. 可靠性 - Kafka是分布式,分区,复制和容错的。
6. 客户端状态维护:消息被处理的状态是在Consumer端维护,⽽不是由server端维护。当失败时能⾃动平 衡。
7. ⽀持online和offline的场景。
8. ⽀持多种客户端语⾔。Kafka⽀持Java、.NET、PHP、Python等多种语⾔。
3 应用场景
⽇志收集:⼀个公司可以⽤Kafka可以收集各种服务的Log,通过Kafka以统⼀接⼝服务的⽅式开放给各种 Consumer;
消息系统:解耦⽣产者和消费者、缓存消息等;
⽤户活动跟踪:Kafka经常被⽤来记录Web⽤户或者App⽤户的各种活动,如浏览⽹⻚、搜索、点击等活动,这些 活动信息被各个服务器发布到Kafka的Topic中,然后消费者通过订阅这些Topic来做实时的监控分析,亦可保存到数据 库;
运营指标:Kafka也经常⽤来记录运营监控数据。包括收集各种分布式应⽤的数据,⽣产各种操作的集中反馈,⽐ 如报警和报告;
流式处理:⽐如Spark Streaming和Storm。
4 基本架构
消息和批次
Kafka的数据单元称为消息。可以把消息看成是数据库⾥的⼀个“数据⾏”或⼀条“记录”。消息由字节数组组成。 消息有键,键也是⼀个字节数组。当消息以⼀种可控的⽅式写⼊不同的分区时,会⽤到键。 为了提⾼效率,消息被分批写⼊Kafka。批次就是⼀组消息,这些消息属于同⼀个主题和分区。 把消息分成批次可以减少⽹络开销。批次越⼤,单位时间内处理的消息就越多,单个消息的传输时间就越⻓。批 次数据会被压缩,这样可以提升数据的传输和存储能⼒,但是需要更多的计算处理。
模式
消息模式(schema)有许多可⽤的选项,以便于理解。如JSON和XML,但是它们缺乏强类型处理能⼒。Kafka的 许多开发者喜欢使⽤Apache Avro。Avro提供了⼀种紧凑的序列化格式,模式和消息体分开。当模式发⽣变化时,不 需要重新⽣成代码,它还⽀持强类型和模式进化,其版本既向前兼容,也向后兼容。 数据格式的⼀致性对Kafka很重要,因为它消除了消息读写操作之间的耦合性。
主题和分区
Kafka的消息通过主题进⾏分类。主题可⽐是数据库的表或者⽂件系统⾥的⽂件夹。主题可以被分为若⼲分区,⼀ 个主题通过分区分布于Kafka集群中,提供了横向扩展的能⼒。
⽣产者和消费者
⽣产者创建消息。消费者消费消息。 ⼀个消息被发布到⼀个特定的主题上。 ⽣产者在默认情况下把消息均衡地分布到主题的所有分区上: 1. 直接指定消息的分区 2. 根据消息的key散列取模得出分区 3. 轮询指定分区
消费者通过偏移量来区分已经读过的消息,从⽽消费消息。 消费者是消费组的⼀部分。消费组保证每个分区只能被⼀个消费者使⽤,避免重复消费。
broker和集群
⼀个独⽴的Kafka服务器称为broker。broker接收来⾃⽣产者的消息,为消息设置偏移量,并提交消息到磁盘保 存。broker为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘上的消息。单个broker可以轻松处 理数千个分区以及每秒百万级的消息量。
每个集群都有⼀个broker是集群控制器(⾃动从集群的活跃成员中选举出来)。
控制器负责管理⼯作: 将分区分配给broker ,监控broker。 集群中⼀个分区属于⼀个broker,该broker称为分区⾸领。 ⼀个分区可以分配给多个broker,此时会发⽣分区复制。 分区的复制提供了消息冗余,⾼可⽤。副本分区不负责处理消息的读写。
5 核心概念
5.1 Producer
⽣产者创建消息。
该⻆⾊将消息发布到Kafka的topic中。broker接收到⽣产者发送的消息后,broker将该消息追加到当前⽤于追加 数据的 segment ⽂件中。
⼀般情况下,⼀个消息会被发布到⼀个特定的主题上。
1. 默认情况下通过轮询把消息均衡地分布到主题的所有分区上。
2. 在某些情况下,⽣产者会把消息直接写到指定的分区。这通常是通过消息键和分区器来实现的,分区器为键 ⽣成⼀个散列值,并将其映射到指定的分区上。这样可以保证包含同⼀个键的消息会被写到同⼀个分区上。
3. ⽣产者也可以使⽤⾃定义的分区器,根据不同的业务规则将消息映射到分区。
5.2 Consumer
消费者读取消息。
1. 消费者订阅⼀个或多个主题,并按照消息⽣成的顺序读取它们。
2. 消费者通过检查消息的偏移量来区分已经读取过的消息。偏移量是另⼀种元数据,它是⼀个不断递增的整数 值,在创建消息时,Kafka 会把它添加到消息⾥。在给定的分区⾥,每个消息的偏移量都是唯⼀的。消费者 把每个分区最后读取的消息偏移量保存在Zookeeper 或Kafka 上,如果消费者关闭或重启,它的读取状态不 会丢失。
3. 消费者是消费组的⼀部分。群组保证每个分区只能被⼀个消费者使⽤。
4. 如果⼀个消费者失效,消费组⾥的其他消费者可以接管失效消费者的⼯作,再平衡,分区重新分配.
5.3 Broker
⼀个独⽴的Kafka 服务器被称为broker。
broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。
1. 如果某topic有N个partition,集群有N个broker,那么每个broker存储该topic的⼀个partition。
2. 如果某topic有N个partition,集群有(N+M)个broker,那么其中有N个broker存储该topic的⼀个partition, 剩下的M个broker不存储该topic的partition数据。
3. 如果某topic有N个partition,集群中broker数⽬少于N个,那么⼀个broker存储该topic的⼀个或多个 partition。在实际⽣产环境中,尽量避免这种情况的发⽣,这种情况容易导致Kafka集群数据不均衡。 broker 是集群的组成部分。每个集群都有⼀个broker 同时充当了集群控制器的⻆⾊(⾃动从集群的活跃成员中选 举出来)。 控制器负责管理⼯作,包括将分区分配给broker 和监控broker。 在集群中,⼀个分区从属于⼀个broker,该broker 被称为分区的⾸领。
5.4 Topic
每条发布到Kafka集群的消息都有⼀个类别,这个类别被称为Topic。 物理上不同Topic的消息分开存储。 主题就好⽐数据库的表,尤其是分库分表之后的逻辑表。
5.5 Partition
1. 主题可以被分为若⼲个分区,⼀个分区就是⼀个提交⽇志。
2. 消息以追加的⽅式写⼊分区,然后以先⼊先出的顺序读取。
3. ⽆法在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。
4. Kafka 通过分区来实现数据冗余和伸缩性。
5. 在需要严格保证消息的消费顺序的场景下,需要将partition数⽬设为1。
5.6 Replicas
Kafka 使⽤主题来组织数据,每个主题被分为若⼲个分区,每个分区有多个副本。那些副本被保存在broker 上, 每个broker 可以保存成百上千个属于不同主题和分区的副本。
副本有以下两种类型:
⾸领副本
每个分区都有⼀个⾸领副本。为了保证⼀致性,所有⽣产者请求和消费者请求都会经过这个副本。
跟随者副本
⾸领以外的副本都是跟随者副本。跟随者副本不处理来⾃客户端的请求,它们唯⼀的任务就是从⾸领那⾥复制消 息,保持与⾸领⼀致的状态。如果⾸领发⽣崩溃,其中的⼀个跟随者会被提升为新⾸领。
5.7 Offset
⽣产者Offset
消息写⼊的时候,每⼀个分区都有⼀个offset,这个offset就是⽣产者的offset,同时也是这个分区的最新最⼤的 offset。 有些时候没有指定某⼀个分区的offset,这个⼯作kafka帮我们完成。
消费者Offset
⽣产者写⼊的offset是最新最⼤的值是12,⽽当Consumer A进⾏消费时,从0开 始消费,⼀直消费到了9,消费者的offset就记录在9,Consumer B就纪录在了11。等下⼀次他们再来消费时,他们可 以选择接着上⼀次的位置消费,当然也可以选择从头消费,或者跳到最近的记录并从“现在”开始消费.
5.8 副本
Kafka通过副本保证⾼可⽤。副本分为⾸领副本(Leader)和跟随者副本(Follower)。 跟随者副本包括同步副本和不同步副本,在发⽣⾸领副本切换的时候,只有同步副本可以切换为⾸领副本。
分区中的所有副本统称为AR(Assigned Repllicas)。 AR=ISR+OSR。
ISR:所有与leader副本保持⼀定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中 的⼀个⼦集。消息会先发送到leader副本,然后follower副本才能从leader副本中拉取消息进⾏同步,同步期间内 follower副本相对于leader副本⽽⾔会有⼀定程度的滞后。前⾯所说的“⼀定程度”是指可以忍受的滞后范围,这个范围 可以通过参数进⾏配置。
OSR:与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas)。在正常情况下,所有 的follower副本都应该与leader副本保持⼀定程度的同步,即AR=ISR,OSR集合为空。
HW是High Watermak的缩写, 俗称⾼⽔位,它表示了⼀个特定消息的偏移量(offset),消费之只能拉取到这 个offset之前的消息。
LEO是Log End Offset的缩写,它表示了当前⽇志⽂件中下⼀条待写⼊消息的offset。
6 kafka实战
6.1 消息的发送与接收
⽣产者主要的对象有: KafkaProducer , ProducerRecord 。 其中 KafkaProducer 是⽤于发送消息的类, ProducerRecord 类⽤于封装Kafka的消息。 KafkaProducer 的创建需要指定的参数和含义:
其他参数可以从 org.apache.kafka.clients.producer.ProducerConfig 中找到。 消费者⽣产消息后,需要broker端的确认,可以同步确认,也可以异步确认。 同步确认效率低,异步确认效率⾼,但是需要设置回调对象。
生产者:
public class Producer { public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException { Map<String, Object> configs = new HashMap<>(); // 设置连接Kafka的初始连接⽤到的服务器地址 // 如果是集群,则可以通过此初始连接发现集群中的其他broker configs.put("bootstrap.servers", "node1:9092"); // 设置key的序列化器 configs.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer"); // 设置value的序列化器 configs.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); configs.put("acks", "1"); KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String> (configs); // ⽤于封装Producer的消息 ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>( "topic_1", // 主题名称 0, // 分区编号,现在只有⼀个分区,所以是0 0, // 数字作为key "message 0" // 字符串作为value ); // 发送消息,同步等待消息的确认 producer.send(record).get(3_000, TimeUnit.MILLISECONDS); // 关闭⽣产者 producer.close(); } }
生产者2:
public class MyProducer2 { public static void main(String[] args) { Map<String, Object> configs = new HashMap<>(); configs.put("bootstrap.servers", "node1:9092"); configs.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer"); configs.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String> (configs); ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>( "topic_1", 0, 1,//key "message 2" ); // 使⽤回调异步等待消息的确认 producer.send(record, new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if (exception == null) { System.out.println( "主题:" + metadata.topic() + "\n" + "分区:" + metadata.partition() + "\n" + "偏移量:" + metadata.offset() + "\n" + "序列化的key字节:" + metadata.serializedKeySize() + "\n" + "序列化的value字节:" + metadata.serializedValueSize() + "\n" + "时间戳:" + metadata.timestamp() ); } else { System.out.println("有异常:" + exception.getMessage()); } } }); // 关闭连接 producer.close(); } }
生产者3:
public class MyProducer3 { public static void main(String[] args) { Map<String, Object> configs = new HashMap<>(); configs.put("bootstrap.servers", "node1:9092"); configs.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer"); configs.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String> (configs); for (int i = 100; i < 200; i++) { ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>( "topic_1", 0, i, "message " + i ); // 使⽤回调异步等待消息的确认 producer.send(record, new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if (exception == null) { System.out.println( "主题:" + metadata.topic() + "\n" + "分区:" + metadata.partition() + "\n" + "偏移量:" + metadata.offset() + "\n" + "序列化的key字节:" + metadata.serializedKeySize() + "\n" + "序列化的value字节:" + metadata.serializedValueSize() + "\n" + "时间戳:" + metadata.timestamp() ); } else { System.out.println("有异常:" + exception.getMessage()); } } }); } // 关闭连接 producer.close(); } }
消费者:
public class MyConsumer1 { public static void main(String[] args) { Map<String, Object> configs = new HashMap<>(); // 指定bootstrap.servers属性作为初始化连接Kafka的服务器。 // 如果是集群,则会基于此初始化连接发现集群中的其他服务器。 configs.put("bootstrap.servers", "node1:9092"); // key的反序列化器 configs.put("key.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"); // value的反序列化器 configs.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); configs.put("group.id", "consumer.demo"); // 创建消费者对象 KafkaConsumer<Integer, String> consumer = new KafkaConsumer<Integer, String> (configs); // final Pattern pattern = Pattern.compile("topic_\\d"); final Pattern pattern = Pattern.compile("topic_[0-9]"); // 消费者订阅主题或分区 // consumer.subscribe(pattern); // consumer.subscribe(pattern, new ConsumerRebalanceListener() { final List<String> topics = Arrays.asList("topic_1"); consumer.subscribe(topics, new ConsumerRebalanceListener() { @Override public void onPartitionsRevoked(Collection<TopicPartition> partitions) { partitions.forEach(tp -> { System.out.println(tp.partition()); }); } @Override public void onPartitionsAssigned(Collection<TopicPartition> partitions) { partitions.forEach(tp -> { System.out.println(tp.partition()); }); } }); // 拉取订阅主题的消息 final ConsumerRecords<Integer, String> records = consumer.poll(3_000); // 获取topic_1主题的消息 final Iterable<ConsumerRecord<Integer, String>> topic1Iterable = records.records("topic_1"); // 遍历topic_1主题的消息 topic1Iterable.forEach(record -> { System.out.println("========================================"); System.out.println("消息头字段:" + Arrays.toString(record.headers().toArray())); System.out.println("消息的key:" + record.key()); System.out.println("消息的偏移量:" + record.offset()); System.out.println("消息的分区号:" + record.partition()); System.out.println("消息的序列化key字节数:" + record.serializedKeySize()); System.out.println("消息的序列化value字节数:" + record.serializedValueSize()); System.out.println("消息的时间戳:" + record.timestamp()); System.out.println("消息的时间戳类型:" + record.timestampType()); System.out.println("消息的主题:" + record.topic()); System.out.println("消息的值:" + record.value()); }); // 关闭消费者 consumer.close(); } }
6.2 SpringBoot Kafka
pom.xml文件:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.8.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.kafka.demo</groupId> <artifactId>demo-springboot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo-springboot</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties:
spring.application.name=springboot-kafka server.port=8080 # ⽤于建⽴初始连接的broker地址 spring.kafka.bootstrap-servers=node1:9092 # producer⽤到的key和value的序列化类 spring.kafka.producer.keyserializer=org.apache.kafka.common.serialization.IntegerSerializer spring.kafka.producer.valueserializer=org.apache.kafka.common.serialization.StringSerializer # 默认的批处理记录数 spring.kafka.producer.batch-size=16384 # 32MB的总发送缓存 spring.kafka.producer.buffer-memory=33554432 # consumer⽤到的key和value的反序列化类 spring.kafka.consumer.keydeserializer=org.apache.kafka.common.serialization.IntegerDeserializer spring.kafka.consumer.valuedeserializer=org.apache.kafka.common.serialization.StringDeserializer # consumer的消费组id spring.kafka.consumer.group-id=spring-kafka-consumer # 是否⾃动提交消费者偏移量 spring.kafka.consumer.enable-auto-commit=true # 每隔100ms向broker提交⼀次偏移量 spring.kafka.consumer.auto-commit-interval=100 # 如果该消费者的偏移量不存在,则⾃动设置为最早的偏移量 spring.kafka.consumer.auto-offset-reset=earliest
@SpringBootApplication public class DemoSpringbootApplication { public static void main(String[] args) { SpringApplication.run(DemoSpringbootApplication.class, args); } }
@Configuration public class KafkaConfig { @Bean public NewTopic topic1() { return new NewTopic("ntp-01", 5, (short) 1); } @Bean public NewTopic topic2() { return new NewTopic("ntp-02", 3, (short) 1); } }
@RestController public class KafkaSyncProducerController { @Autowired private KafkaTemplate template; @RequestMapping("send/sync/{message}") public String sendSync(@PathVariable String message) { ListenableFuture future = template.send( new ProducerRecord<Integer, String>( "topic-spring", 0, 1, message)); try { // 同步等待broker的响应 Object o = future.get(); SendResult<Integer, String> result = (SendResult<Integer, String>) o; System.out.println(result.getRecordMetadata().topic() + result.getRecordMetadata().partition() + result.getRecordMetadata().offset()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return "success"; } }
@RestController public class KafkaAsyncProducerController { @Autowired private KafkaTemplate<Integer, String> template; @RequestMapping("send/async/{message}") public String asyncSend(@PathVariable String message) { ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>( "topic-spring", 0, 3, message ); ListenableFuture<SendResult<Integer, String>> future = template.send(record); // 添加回调,异步等待响应 future.addCallback(new ListenableFutureCallback<SendResult<Integer, String>> () { @Override public void onFailure(Throwable throwable) { System.out.println("发送失败: " + throwable.getMessage()); } @Override public void onSuccess(SendResult<Integer, String> result) { System.out.println("发送成功:" + result.getRecordMetadata().topic() + "\t" + result.getRecordMetadata().partition() + "\t" + result.getRecordMetadata().offset()); } }); return "success"; } }
@Component public class MyConsumer { @KafkaListener(topics = "topic-spring") public void onMessage(ConsumerRecord<Integer, String> record) { Optional<ConsumerRecord<Integer, String>> optional = Optional.ofNullable(record); if (optional.isPresent()) { System.out.println( record.topic() + "\t" + record.partition() + "\t" + record.offset() + "\t" + record.key() + "\t" + record.value()); } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探