简介:Apache Pulsar 2.9.1 的 bin.tar.gz 二进制发行包,面向需要搭建分布式消息队列环境的开发者、运维人员与架构师,可用于发布/订阅消息传递、实时流处理及云原生场景下的中间件部署验证。压缩包约 321.53MB,文件总数显示为 0、文件类型明细暂无有效数据,上游未提供可核验的清单;按发行说明,其通常包含服务端启动文件、客户端库、管理脚本与辅助工具,便于完成单机或集群环境的基础运行。已有 152 人学习下载,适合作为理解 Pulsar 与 Apache ZooKeeper 协同机制、分布式协调及高可用设计的实践入口。读者可从中接触共享、独占与故障转移等订阅类型,以及 BookKeeper 持久化、分层存储和 Pulsar Functions 等核心能力,并借助该二进制包在容器化或 Kubernetes 环境中验证消息持久化、容错与水平扩展思路,形成对云原生消息中间件的整体认识。
1. 从 tar.gz 到能跑:2.9.1 二进制包值不值得先解压
在 4 核 8G 的测试机上解压 apache pulsar 2.9.1 的 bin.tar.gz,进目录敲bin/pulsar standalone,日志刷得飞快,三五分钟后 broker 报 ready,看着一切正常;再跑十几分钟,机器开始剧烈换页,dmesg里能看到 OOM Kill 记录。这个反差基本概括了这份二进制包的性格:上手极快,默认参数却按生产规格给。
这份 tar.gz 是官方分发的完整运行包,broker、bookie、内嵌 ZooKeeper、admin 命令行、perf 压测工具、示例配置都在里面,不需要编译,也不需要额外下载依赖。standalone 启动器会把 ZooKeeper、BookKeeper、Broker 三个角色放进同一个进程里跑起来,这是体验特性的最快路径,但它不等于分布式部署,更不能直接搬到云原生环境里跑业务。
适合谁:想评估 Pulsar 的存储模型、想验证多租户和订阅类型、想拿它做一次真实压测的技术选型者。不适合把 standalone 当生产环境用的团队——后面几章讲的参数和踩坑,就是把这个"能跑"变成"敢用"的过程。
2. 拆包先看懂进程拓扑:ZooKeeper、BookKeeper、Broker 各管什么
Pulsar 最容易被误解的一点,是很多人拿它和 Kafka 对比时,默认"消息存在 broker 本地磁盘"。实际不是。broker 是无状态的,消息落在 BookKeeper 的 bookie 上,元数据落在 ZooKeeper 里。这三个角色职责分得很干净,出故障时的表现也完全不同,先搞清楚边界,后面调参和排错才有方向。
2.1 三个角色的职责边界与端口占用
在 2.9.1 这条线上,元数据存储还没有被抽象成可插拔层,ZooKeeper 是硬依赖:broker 要靠它存租户、命名空间、topic 的元信息,bookie 要靠它做集群成员发现和 ledger 元数据登记。这也是为什么你会在依赖清单里同时看到 apache zookeeper 和 pulsar——分布式部署的第一个启动项永远是 ZK,最后一个关闭项也是它。
| 角色 | 默认端口 | 存什么 | 挂了之后的直接表现 |
|---|---|---|---|
| ZooKeeper | 2181 | 租户/命名空间/topic 元数据、bookie 注册信息 | 新建 topic 失败、broker 无法启动、admin 命令超时 |
| BookKeeper bookie | 3181 | 消息体(ledger 分片)、journal 与 entry log | 生产端卡住直到超时,消费端读不到新消息 |
| Broker | 6650(二进制协议)/ 8080(HTTP 管理) | 不存数据,只做路由、缓存、分发 | 客户端全断,但已落盘消息不丢 |
这张表最实用的地方在于故障定位。客户端连不上 6650,先看 broker 进程;broker 起来了但创建 topic 报错,去看 ZK;topic 建得出来、生产却一直 Pending,去看 bookie 的磁盘和 journal。
2.2 解压后真正会被读到的文件
包解开之后目录不复杂,但新手容易被 lib 下几百个 jar 淹没,其实日常只需要盯五个位置:
tar -xzf apache-pulsar-2.9.1-bin.tar.gz cd apache-pulsar-2.9.1 ls bin/ # 启动与运维命令入口 ls conf/ # 所有配置,后面几章改的都是这里 ls lib/ # 依赖 jar,一般不动 ls examples/ # 官方示例,验证链路时可以直接跑 du -sh . # 解压后体积,规划磁盘时算进去bin/下常用的是这几个:pulsar(主启动脚本,pulsar standalone/pulsar broker/pulsar bookie都走它)、pulsar-daemon(后台启动包装)、pulsar-admin(管理面)、pulsar-client(命令行生产消费)、pulsar-perf(压测)、bookkeeper(bookie 侧的 shell 工具)。
conf/下有四个文件决定了整套系统的行为:standalone.conf(单机模式,内含 ZK + bookie + broker 三段配置)、broker.conf、bookkeeper.conf、zookeeper.conf。一个高频误区是改了broker.conf却发现 standalone 启动没生效——standalone 读的是standalone.conf,不是broker.conf,两个文件互不影响。
2.3 从 1 到 3:standalone 和分布式的分界线在哪
判断标准不是"数据量大不大",而是"能不能容忍单点"。standalone 下 ZK、bookie、broker 在一个进程里,进程一挂全挂;副本数被强制设成 1,任何磁盘损坏都是永久丢消息。它只适合开发自测和功能验证。
一旦要验证真实的容错行为,最少需要 3 台机器:3 个 ZK 组成 quorum,3 个 bookie 支撑ensembleSize=3 / writeQuorum=3 / ackQuorum=2的写入模型,broker 可以是 2 台做无状态对等。这个规模下才能测出"挂掉一个 bookie 时生产是否继续""broker 重启时客户端是否自动重连"这类真正关心的问题。
云原生场景下还会多一层考虑:容器里跑 bookie,journal 必须落在持久卷上,不能落容器的临时层;broker 可以随便重启,bookie 不行,状态是有代价的。这条原则决定了后面 K8s 部署时哪些组件用 StatefulSet、哪些用 Deployment。
3. 单机跑通:standalone 启动、验证与第一条消息
这一章的目标很明确:从解压到跑通一条完整的生产和消费链路,并且每一步都能看到可验证的输出。所有命令都在解压目录下执行,路径不要跳出去,否则 data 目录的位置会变。
3.1 环境准备:JDK、内存、文件句柄
2.9.1 对 JDK 的适配范围是 8 和 11,这两个版本是安全区。JDK 17 上跑会出现模块反射相关的异常,日志里通常是一串InaccessibleObjectException,然后进程直接退出——这不是配置问题,换 JDK 就行。
内存是第二个门槛。启动脚本里堆和直接内存有默认值,默认堆 2G、直接内存 4G,一台 8G 的机器上再叠加 bookie 的页缓存需求,很容易触发 OOM。测试环境建议先用环境变量压下来:
# 确认 JDK 版本落在 8 或 11 java -version # 小机器上显式收窄内存,堆 1G + 直接内存 1G export PULSAR_MEM="-Xms1g -Xmx1g -XX:MaxDirectMemorySize=1g" export BOOKIE_MEM="-Xms512m -Xmx512m -XX:MaxDirectMemorySize=512m" # bookie 写 journal 需要文件句柄,默认 1024 不够用 ulimit -n 65536 ulimit -n参数说明:PULSAR_MEM作用于 broker 进程,BOOKIE_MEM作用于 bookie 进程,后者不设置时会沿用前者的值。MaxDirectMemorySize是 BookKeeper 读写 ledger 的主要缓冲区来源,压得太小会出现OutOfMemoryError: Direct buffer memory,压得太大则挤占页缓存。测试机上 1G 是能跑通功能的下限,生产上要给到 4G 以上。
ulimit -n是很多人第一次跑就翻车的地方——standalone 起来后日志里不断刷Too many open files,删掉 topic 都删不动。
3.2 启动 standalone 并读懂启动日志
前台启动便于观察日志,第一次跑建议就用前台:
cd apache-pulsar-2.9.1 # 前台启动,Ctrl+C 结束;数据默认落在解压目录的 data/standalone 下 bin/pulsar standalone启动过程大致分三段,日志里能清楚看到:先是内嵌 ZooKeeper 起来并监听 2181,然后是 bookie 完成 journal 和 ledger 目录的初始化并注册到 ZK,最后 broker 加载配置、绑定 6650 和 8080、打印出带版本的启动横幅。看到messaging service is ready之类的字样,说明三个角色都就位了。
要放后台跑,换一个命令:
# 后台模式,PID 文件和日志位置由脚本决定 bin/pulsar-daemon start standalone # 停掉 bin/pulsar-daemon stop standalone # 确认端口都在监听 ss -lntp | grep -E '2181|3181|6650|8080'四个端口齐了才算真正起来。如果 8080 没监听但 6650 在,多半是 Web 服务初始化失败,去看日志里的第一个 ERROR,往往和内存或端口占用有关。
3.3 用 pulsar-admin 建租户、命名空间、topic
Pulsar 的命名体系是四段式:persistent://租户/命名空间/topic名。standalone 启动时会自动建好public/default,可以直接用;要验证多租户能力,就自己建一套:
# 建租户,管理员角色按需指定 bin/pulsar-admin tenants create demo-tenant \ --admin-roles demo-admin \ --allowed-clusters standalone # 建命名空间,绑定到集群 bin/pulsar-admin namespaces create demo-tenant/demo-ns \ --clusters standalone # 设置保留策略:1G 或 24 小时,先到先清 bin/pulsar-admin namespaces set-retention demo-tenant/demo-ns \ --size 1G --time 24h # 建一个 3 分区的 topic bin/pulsar-admin topics create-partitioned-topic \ persistent://demo-tenant/demo-ns/order-events -p 3 # 确认建好了 bin/pulsar-admin topics list demo-tenant/demo-ns参数说明:--allowed-clusters决定这个租户能落在哪些集群上,standalone 模式下集群名就是standalone,填错了建命名空间会报 cluster 不存在。set-retention的--size和--time是"或"的关系,任意一个先满足就触发清理;不设置的话默认无限保留,磁盘会被慢慢吃满,这在测试机上是个隐蔽的坑。
分区数-p 3建好之后不能再加,只能通过update-partitioned-topic扩容,且只能增不能减。生产上按峰值吞吐预留,测试环境给 1 到 3 就够。
3.4 用 Python 客户端把生产消费跑成闭环
命令行验证一条链路最快,但真实业务都是 SDK 调用。下面这段用 Python 客户端把生产和消费串起来,同时把几个关键参数显式写出来,避免默认值带来的意外:
import pulsar # 客户端版本与服务端主版本保持一致,跨大版本容易出现协议协商问题 client = pulsar.Client( 'pulsar://127.0.0.1:6650', operation_timeout_seconds=30, # 元数据操作超时,建 producer 卡住时靠它兜底 connection_timeout_ms=10000 ) producer = client.create_producer( 'persistent://public/default/demo-topic', send_timeout_millis=30000, # 发送超时,broker 侧写满时会先卡在这里 max_pending_messages=1000, # 客户端攒批上限,超了 send() 会阻塞 batching_enabled=True, batching_max_publish_delay_ms=10 ) for i in range(10): producer.send(('msg-%d' % i).encode('utf-8')) consumer = client.subscribe( 'persistent://public/default/demo-topic', subscription_name='sub-demo', consumer_type=pulsar.ConsumerType.Shared, # 多消费者分摊,注意不保证顺序 receiver_queue_size=1000 ) for _ in range(10): msg = consumer.receive(timeout_millis=5000) print(msg.data().decode('utf-8'), msg.message_id(), msg.publish_time()) consumer.acknowledge(msg) # 不 ack 会在 ack timeout 后重投,别当它是可选操作 client.close()逻辑说明:先建 producer,再建 consumer,最后关闭 client。subscription_name是持久化的订阅游标,同一个名字重复订阅会从上次 ack 的位置继续,名字写错就会从头开始消费——排查"消息重复消费"时先查这个字段。
参数上最值得关注的是consumer_type。Shared模式下多个消费者分摊分区,吞吐高但不保证顺序;要保证单分区内严格有序,得用Exclusive或Failover,配合Key_Shared还能做到按 key 保序。选错类型导致业务侧乱序,是上线后最常见的返工点之一。
4. 分布式部署:conf 目录下那几个必须改的参数
单机能跑通之后,把同样的包放到三台机器上并不会自动变成分布式。2.9.1 的分布式部署是"手工拼装"的过程:先起 ZK 集群,再初始化元数据,再起 bookie,最后起 broker。每一步的配置都在conf/下,改错一个参数,后面所有步骤都会以奇怪的方式失败。
4.1 zookeeper.conf:只改三处就够
ZK 这一层最省心的做法是用外部已有集群,如果必须自带,conf/zookeeper.conf里改这几项:
# 集群内每台机器一行,server.N 的 N 要和 myid 文件内容对应 server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888 # 数据目录,必须是独立磁盘或持久卷,别用 /tmp dataDir=/data/pulsar/zookeeper # 单机调试时才放开,生产环境务必关掉,否则会遇到连接数被打满 # maxClientCnxns=0 # 会话超时,跨机房部署时适当调大 tickTime=2000 initLimit=10 syncLimit=5注意server.N里的 N 和该节点dataDir/myid文件里的数字必须一致,这是 ZK 最经典的配置错误来源:三个节点都能起来,但选不出 leader,日志里反复刷连接超时。改完之后启动顺序无所谓,但一定要等到echo stat | nc zk1 2181能看到Mode: leader或Mode: follower再往下走。
4.2 bookkeeper.conf:journal 与 ledger 必须落独立盘
bookie 是整套系统里唯一有状态的组件,也是最容易配错的一个。
# 连到 ZK 集群做元数据登记 zkServers=zk1:2181,zk2:2181,zk3:2181 # bookie 之间互相通信的端口 bookiePort=3181 # journal 是写前日志,要求低延迟,强烈建议独立 SSD journalDirectory=/data1/pulsar/journal # ledger 是最终存储,容量盘,可以多目录 ledgerDirectories=/data2/pulsar/ledger ledgerDirectories=/data3/pulsar/ledger # 单机或容器内网回环地址需要放开,生产多机环境保持 false allowLoopback=false # 写 journal 的同步策略,性能与可靠性的分水岭 journalSyncData=truejournalDirectory和ledgerDirectories放在同一块盘上,写入延迟会被读写互相干扰,压测时 P99 会明显抖动;分成两块盘是最低成本的一档优化。journalSyncData=true表示每次写 journal 都 fsync,关掉能提升吞吐但在掉电时会丢最近一批数据,测试环境可以关,生产别赌。
另外 bookie 有个隐性的容量约束:磁盘使用率超过阈值后会主动拒绝写入,表现为生产端一直超时。部署前按"可用磁盘 × 0.9 / 副本数"估算实际能存多少消息。
4.3 broker.conf:advertisedAddress、集群名与内存
broker 无状态,配置相对简单,但有几个参数改错会让客户端连得上却收发不了:
# 集群名必须和初始化元数据时用的一致 clusterName=pulsar-cluster # 元数据存储,指到 ZK 集群 zookeeperServers=zk1:2181,zk2:2181,zk3:2181 configurationStoreServers=zk1:2181,zk2:2181,zk3:2181 # 关键项:客户端拿到的连接地址就是它,多网卡/Docker 场景必须显式指定 advertisedAddress=broker1 brokerServicePort=6650 webServicePort=8080 # 允许删除 topic,测试环境打开,生产按规约决定 allowAutoTopicCreation=true brokerDeleteInactiveTopicsEnabled=false # 单条消息最大体积,默认 5MB,超过要两端一起改 maxMessageSize=5242880advertisedAddress是最值得单独说的一项。broker 启动时会把自己的地址写进元数据,客户端通过 ZK 拿到这个地址再去连。容器里如果不指定,写进去的就是容器内网 IP,宿主机上的客户端会连到一个不存在的地址,现象是"admin 命令能通但 SDK 一直超时"。多网卡机器同理,必须显式指定对外可达的那张网卡。
4.4 初始化元数据并逐个拉起
配置改完,启动顺序是固定的:ZK → 初始化元数据 → bookie → broker。
# 1. 三台机器分别起 ZK bin/pulsar-daemon start zookeeper # 2. 只在任意一台上执行一次,写集群元数据 bin/pulsar initialize-cluster-metadata \ --cluster pulsar-cluster \ --zookeeper zk1:2181,zk2:2181,zk3:2181 \ --configuration-store zk1:2181,zk2:2181,zk3:2181 \ --web-service-url http://broker1:8080 \ --broker-service-url pulsar://broker1:6650 # 3. 三个 bookie 节点分别启动 bin/pulsar-daemon start bookie bin/bookkeeper shell bookiesanity # 验证 bookie 读写是否正常 # 4. 两个 broker 节点分别启动 bin/pulsar-daemon start broker bin/pulsar-admin brokers list pulsar-clusterinitialize-cluster-metadata是幂等的,重复执行会报已存在,不会破坏数据,但要注意--cluster的名字必须和broker.conf里的clusterName完全一致,不一致的后果是 broker 起得来但所有 topic 操作都失败。
bookiesanity这个命令值得养成习惯:它在 bookie 上写一个临时 ledger 再读回来,能在 broker 启动之前就把磁盘、权限、ZK 连接的问题暴露出来,比等到客户端报错再去翻日志省事得多。
用 systemd 托管的话,unit 大致长这样:
[Unit] Description=Pulsar Broker After=network.target [Service] Type=simple User=pulsar Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk" Environment="PULSAR_MEM=-Xms4g -Xmx4g -XX:MaxDirectMemorySize=4g" ExecStart=/opt/pulsar/bin/pulsar broker Restart=on-failure RestartSec=5 LimitNOFILE=65536 [Install] WantedBy=multi-user.targetbookie 的 unit 把ExecStart换成bin/pulsar bookie、环境变量换成BOOKIE_MEM即可,其余保持一致。LimitNOFILE=65536不能省,systemd 的默认值和 shell 里的ulimit是两套东西。
5. 避坑:五类高频启动失败与数据丢失的排查
这一章的五条都是从"日志看着没问题但就是不通"的实际场景里攒出来的。每条按现象、原因、解决三段写,能对号入座就先去改,别急着怀疑版本。
5.1 现象:standalone 启动即退出,日志报 ZK 连接失败
现象:敲完bin/pulsar standalone,几秒后进程退出,日志里出现ConnectionLossException或者Unable to connect to zookeeper。
原因:本机 2181 端口已经被别的 ZooKeeper 占用,standalone 内嵌的 ZK 绑不上,但 broker 那半边仍然按配置去连 2181,连到的其实是另一个实例。测试机上装过 Kafka、HBase 的话很容易撞上。
解决:先确认端口归属,再决定是改端口还是先停旧进程。
ss -lntp | grep 2181 # 如果被别的进程占用,改 standalone.conf 里的内嵌 ZK 端口 # 或者直接停掉占用进程后再启动改端口的话,standalone.conf里 ZK、bookie、broker 三段引用的端口都要同步改,漏一处照样连不上。
5.2 现象:JDK 17 环境下启动报模块反射异常
现象:进程刚起就退出,堆栈里能看到InaccessibleObjectException或java.lang.reflect.InaccessibleObjectException: Unable to make ... accessible。
原因:2.9.1 依赖的 Netty 等组件在 JDK 17 的模块系统下需要额外的--add-opens参数,打包时的启动脚本没有覆盖这些情况。
解决:不要试图加一堆--add-opens硬扛,直接切回 JDK 8 或 11。多个 JDK 共存时,用JAVA_HOME显式指定,别依赖 PATH 里那个。
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk $JAVA_HOME/bin/java -versionsystemd 托管的场景下,JAVA_HOME必须写进 unit 的Environment,否则用的是系统默认 JDK,你本地测试通过、机器上跑不起来。
5.3 现象:跑一段时间后 OOM,业务端全部超时
现象:standalone 或 broker 跑十几分钟到几小时后进程消失,dmesg -T | tail能看到 OOM Kill 记录,或者日志里刷OutOfMemoryError: Direct buffer memory。
原因:默认堆 2G、直接内存 4G,再加上 bookie 需要的页缓存,8G 机器根本不够。直接内存超限的表现和堆 OOM 不同,堆 dump 里看不出问题,容易误判成内存泄漏。
解决:按机器规格显式收窄,测试环境堆和直接内存各 1G 起步,然后观察top里的 RES 和 swap 使用:
export PULSAR_MEM="-Xms1g -Xmx1g -XX:MaxDirectMemorySize=1g" export BOOKIE_MEM="-Xms512m -Xmx512m -XX:MaxDirectMemorySize=512m" # 再起进程,跑一轮压测看内存曲线是否稳定如果收窄之后仍然 OOM,去看是不是 topic 没设保留策略导致数据无限堆积。
5.4 现象:重启后消息"消失",或者 topic 列表变空
现象:standalone 重启,之前建的 topic 全没了,或者消费位点回到最初。
原因:数据目录用的是相对路径data/standalone,换个工作目录启动就等于换了一套全新的存储;容器场景下更常见,容器一重建临时层被清空,元数据和 ledger 一起没了。
解决:配置里全部改成绝对路径,容器里挂持久卷。启动前先确认目录存在且有权限。
# 确认数据实际落在哪 grep -E 'journalDirectory|ledgerDirectories|dataDir' conf/standalone.conf ls -ld data/standalone 2>/dev/null || echo "数据目录不存在,说明之前跑在别处"养成习惯:每次重启前先看一眼数据目录的大小,如果它一直是几十 KB,说明你的数据根本没落在这。
5.5 现象:admin 命令能通,SDK 连接一直超时
现象:bin/pulsar-admin brokers list正常返回,但客户端连pulsar://host:6650一直重试,或者连上之后建 producer 卡住。
原因:advertisedAddress写的是容器内网 IP 或某张不可达网卡的地址。broker 把这个地址登记到元数据里,客户端拿到之后去连,自然连不上。admin 命令走的是 HTTP 8080,不经过这个地址,所以表现正常。
解决:显式指定对外可达的地址,重启 broker 让元数据刷新。
# broker.conf / standalone.conf 里都要看 advertisedAddress=10.0.0.11改完之后用bin/pulsar-admin brokers list pulsar-cluster重新确认,再从一个远端机器上用telnet 10.0.0.11 6650验证可达性。多网卡机器上,这一项配错的概率比想象中高得多。
6. 进阶:用 Key_Shared 和压测把参数调到能上生产
功能跑通只是起点,真正决定这套部署能不能用,是订阅类型选得对不对、参数调得合不合理。一个具体技巧:用Key_Shared订阅配合压测工具,能在半小时内把分区数和批处理参数定下来。
先建一个多分区 topic,用带 key 的消息验证顺序性:
# 3 分区 topic,按 key 哈希分布到不同分区 bin/pulsar-admin topics create-partitioned-topic \ persistent://demo-tenant/demo-ns/order-events -p 3 # 命令行生产,-k 指定 key,同一个 key 会落到同一分区 bin/pulsar-client produce persistent://demo-tenant/demo-ns/order-events \ -k order-1001 -m "created" -n 5消费端订阅时选Key_Shared,多个消费者分摊时,同一个 key 始终路由到同一个消费者实例,既保住单 key 顺序,又拿到水平扩展能力。这一点比Exclusive实用得多——后者在 2.9.1 里一个订阅只能挂一个消费者,吞吐上不去。
然后是压测,把分区数和批处理参数压出来:
# 生产端:每秒 1000 条,每条 1KB,跑 60 秒 bin/pulsar-perf produce \ -r 1000 -s 1024 -n 60000 \ persistent://demo-tenant/demo-ns/bench-topic # 消费端:同样跑 60 秒,观察消费速率是否跟得上 bin/pulsar-perf consume \ -s sub-bench -n 60000 \ persistent://demo-tenant/demo-ns/bench-topic关注三个数:生产端的发送延迟 P99、消费端速率是否与生产端持平、以及 bookie 磁盘的写入 IOPS。消费速率明显低于生产速率时,先加消费者实例(Shared或Key_Shared下有效),再看分区数够不够——分区数是消费并行度的上限,3 个分区最多摊到 3 个消费者,第 4 个会空转。
一个反直觉的结论:batching_max_publish_delay_ms调大能提升吞吐,但延迟会跟着涨。对延迟敏感的业务把它压到 5ms 以内,对吞吐敏感的业务可以放到 50ms,两者没有中间最优解,只能按业务定。
我最早做这套压测时没有先确认数据目录,跑完一轮发现磁盘没涨,以为是压测工具没生效,排查了半天才发现在别的工作目录下起了第二个实例。从那以后,每次压测前我都先把data/standalone的实际路径和磁盘占用打一遍,确认数据真的落在我以为的地方,再开始跑。希望帮到你。
本文还有配套的精品资源,点击获取