尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

直播高并发后端实战:从500并发崩溃到稳定支撑十万在线

直播高并发后端实战:从500并发崩溃到稳定支撑十万在线 直播这行当表面看是主播对着镜头说话背后其实是一整套跟秒杀性质差不多的实时系统。我刚开始接触这块的时候以为搭个推流服务器、配个 CDN 就完事了结果第一次做压测500 个并发连接就把服务打挂了——弹幕延迟飙到十几秒礼物消息丢了一大半排行榜数据直接错乱。那次之后我才明白直播的高并发跟电商秒杀完全是两码事秒杀是瞬时脉冲扛过那一秒就结束了直播是持续高压一场两小时的直播峰值可能持续四十分钟以上系统得在这种状态下稳定运行不能有任何抖动。这篇笔记记录的是我从零搭一套能扛住直播场景的高并发后端环境的完整过程。我不是什么架构大牛就是个普通后端踩过的坑比看过的文档多。所以这篇东西不会跟你扯太多理论重点放在我当时为什么这么选这个参数为什么调成这个值哪里差点翻车这些实际问题上。如果你也是刚接触直播后端或者正在为公司的直播功能做技术选型这篇应该能帮你少走不少弯路。1. 直播高并发到底难在哪先把问题定义清楚1.1 直播场景的流量特征跟普通 Web 完全不是一回事普通 Web 应用的流量曲线通常是平缓的白天高一点、夜里低一点峰值和均值差个两三倍就算波动大了。直播不一样它的流量特征可以用三个词概括持续高压、脉冲叠加、读写极度不对称。持续高压好理解一场直播开播后观众是陆续进来的但一旦进入热门推荐位在线人数会在几分钟内从几百涨到几万甚至几十万而且这个量级会维持到直播结束。这意味着你的系统不能靠弹性扩容扛过峰值然后缩容这种策略因为峰值持续时间太长扩容速度跟不上涌入速度。脉冲叠加是指直播过程中会不断出现各种事件主播喊一句扣 1弹幕瞬间爆炸整点抽奖几万人同时点按钮PK 环节两边粉丝疯狂刷礼物。这些事件会在原本就很高的基础流量上再叠一层脉冲系统要能扛住这种叠加。读写极度不对称是直播最特殊的地方。一个直播间里发弹幕的人是少数看弹幕的人是多数比例可能是 1:50 甚至 1:100。也就是说写请求相对少但读请求量极大而且读请求要求极低延迟——弹幕晚到三秒观众就觉得卡了体验直接崩。我一开始用传统的 MVC 架构每个弹幕都写库、每个观众都轮询查库数据库连接池瞬间被打满。后来才想明白直播场景的核心矛盾不是写不进去而是读不过来。1.2 三个必须解决的核心问题把问题定义清楚之后我发现直播后端要解决的核心问题其实就三个第一消息的实时分发。弹幕、礼物、系统通知、在线人数变化这些消息需要以极低延迟推送到所有在线观众。用 HTTP 轮询肯定不行延迟高、浪费带宽、服务器压力大。必须用长连接方案让服务器主动推。第二热点数据的读写分离。直播间信息、在线人数、排行榜、礼物列表这些数据被高频读取但更新频率相对低。如果每次都查数据库数据库扛不住。必须加缓存层而且缓存的设计要考虑到热点 key问题——一个热门直播间的 key 可能被每秒几十万次访问。第三突发流量的削峰填谷。抽奖、秒杀礼物、PK 冲榜这些场景瞬间写入量会暴涨。如果直接打到数据库轻则慢查询重则锁表。需要引入消息队列做缓冲把同步写变成异步写。这三个问题对应到技术选型上就是长连接网关、Redis 缓存集群、消息队列三件套。后面我会逐个拆解我是怎么选、怎么配、怎么调的。1.3 一个容易被忽略的坑连接数不等于并发数这里有个概念必须先掰扯清楚不然压测数据会骗你。很多人把同时在线人数和并发连接数混为一谈实际上两者差得很远。一个观众打开直播间可能建立一条 WebSocket 长连接但他在直播间里可能同时触发多个请求拉取历史弹幕、查询主播信息、获取礼物列表、上报心跳。这些请求如果都走同一条连接那连接数就是在线人数但如果部分走 HTTP 短连接实际并发请求数可能是在线人数的三到五倍。更麻烦的是长连接本身有开销。每条 WebSocket 连接在服务器端都要占内存、占文件描述符还要定期发心跳包维持。一台 4 核 8G 的机器理论上能维持几万条连接但实际能稳定维持多少取决于你的心跳策略、内存管理和内核参数调优。我第一次压测的时候用 JMeter 模拟了 5000 个并发用户结果显示服务正常。但真实上线后3000 人在线就开始出现连接断开。后来才发现JMeter 的 WebSocket 插件默认不复用心跳而真实客户端会持续发心跳实际连接开销比压测时大得多。2. 技术选型为什么我最终选了这套组合2.1 长连接网关Netty 还是 Go 还是 Node.js长连接网关是整个直播后端的地基选错了后面全白搭。我当时对比了三个方案方案优势劣势适用场景Java Netty生态成熟跟现有 Java 后端无缝集成线程模型可控内存占用高JVM 调优门槛高团队是 Java 技术栈需要跟业务系统深度集成Go Goroutine并发模型天然适合长连接内存占用低部署简单跟 Java 业务系统集成需要跨语言调用团队有 Go 经验追求极致性能Node.js Socket.io开发速度快前后端同构单线程模型CPU 密集场景弱大规模连接下 GC 压力大快速原型验证中小规模场景我最终选了Netty原因很实际我们后端本来就是 Java 技术栈用 Netty 可以直接复用现有的用户认证、权限校验、日志体系不用搞跨语言调用。而且 Netty 的EpollEventLoopGroup在 Linux 上用的是 epoll 边缘触发模式单机维持十万连接不是问题。但 Netty 有个坑必须提前说它的内存管理是手动式的ByteBuf用完必须释放否则会内存泄漏。我一开始没注意跑了两天发现堆外内存一直涨最后用-Dio.netty.leakDetection.levelparanoid才定位到问题。2.2 缓存层Redis 集群怎么部署才不翻车Redis 在直播场景里承担了太多职责缓存直播间信息、存储在线用户集合、做排行榜、做消息去重、做限流计数器。可以说 Redis 挂了整个直播后端就瘫了。我一开始用单机 Redis压测到 8000 QPS 的时候开始出现超时。后来换成主从加哨兵读性能上去了但写还是单点。最终方案是Redis Cluster6 个节点3 主 3 从每个主节点负责一部分槽位。这里有几个关键决策为什么是 3 主而不是更多因为 Redis Cluster 的槽位是固定的 16384 个主节点越多每个节点负责的槽位越少但节点间通信开销越大。3 主在中小规模直播场景下足够单主能扛 5 万到 8 万 QPS。为什么用 Cluster 而不是 CodisCodis 是代理模式多一层转发延迟会高一点。Cluster 是去中心化的客户端直连节点延迟更低。但 Cluster 的客户端要支持槽位重定向我用的是 Lettuce它对 Cluster 支持比较好。热点 key 怎么处理这是直播场景最头疼的问题。一个热门直播间的在线用户集合 key可能被每秒几十万次访问单个 Redis 节点根本扛不住。我的做法是给热点 key 加随机后缀做分片比如live:room:123:online:0到live:room:123:online:9读的时候随机选一个写的时候写所有分片。这样把一个 key 的压力分散到 10 个 key 上。但分片有个代价获取在线人数时需要聚合 10 个分片的值有一点点延迟。对于在线人数这种允许几秒误差的数据完全可以接受。但如果是礼物排行榜这种要求强一致的就不能这么搞。2.3 消息队列Kafka 还是 RocketMQ 还是 RabbitMQ消息队列在直播场景里主要做两件事削峰和解耦。抽奖请求先写队列后端慢慢消费礼物消息写队列多个下游系统排行榜、统计、通知各自消费。我选了Kafka理由是高吞吐。直播场景的消息量很大一场热门直播每秒可能产生几万条弹幕消息Kafka 的单分区写入能到十万级 QPS完全够用。而且 Kafka 的持久化机制成熟消息不会丢。但 Kafka 有个问题它的延迟比 RocketMQ 高一点。Kafka 为了吞吐做了批量发送默认linger.ms0的时候延迟还好但一旦调大批量参数延迟就上去了。直播弹幕对延迟敏感所以我把linger.ms设成 5msbatch.size设成 16KB在吞吐和延迟之间找了个平衡。RocketMQ 其实更适合直播场景它的延迟更低而且支持事务消息和延迟消息做抽奖倒计时之类的功能很方便。但我们团队对 Kafka 更熟运维成本低所以最终选了 Kafka。2.4 数据库MySQL 分库分表还是 TiDB数据库这块我纠结了很久。直播场景的写入量不算特别大但读取量很大而且数据增长快——弹幕消息一天可能产生几千万条。MySQL 单表超过 500 万行性能就开始下降弹幕表肯定要分表。我一开始想用 ShardingSphere 做分库分表但配置复杂而且跨分片查询很麻烦。后来考虑 TiDB它是分布式数据库自动分片兼容 MySQL 协议但运维成本高至少需要 3 个节点起步。最终我的方案是核心业务数据用 MySQL 主从弹幕历史消息用 MongoDB。MySQL 存用户、直播间、礼物、订单这些结构化数据MongoDB 存弹幕这种文档型数据。MongoDB 的写入性能好而且支持 TTL 索引可以自动过期清理老弹幕。这个选型不一定适合所有人。如果团队没有 MongoDB 运维经验老老实实用 MySQL 分表也行只是要提前规划好分片键。我见过有人用room_id做分片键结果热门直播间的数据全落在一个分片上分表等于没分。3. 核心模块的落地细节从连接建立到消息分发3.1 连接建立认证、限流、心跳一个都不能少WebSocket 连接建立的过程比普通 HTTP 请求复杂得多因为它是长连接一旦建立就会持续占用资源。所以连接建立阶段必须做好三件事认证、限流、心跳协商。认证这块我的做法是客户端先用 HTTP 请求拿到一个短期 token然后用这个 token 发起 WebSocket 连接。网关收到连接请求后先校验 token 有效性再从 Redis 里查用户信息最后把用户信息和连接绑定。这里有个细节token 校验不能查数据库。连接建立是高频操作如果每次都查库数据库扛不住。我的做法是 token 里直接加密了用户 ID 和过期时间网关本地解密校验只有解密失败才回源查库。限流是防止恶意连接的关键。我用了两层限流IP 维度和用户维度。同一个 IP 每秒最多建立 10 条连接同一个用户最多维持 3 条连接多设备登录。限流用 Redis 的滑动窗口实现INCR加EXPIRE就够了。// 简化的连接限流逻辑 public boolean allowConnection(String ip, String userId) { String ipKey limit:ip: ip; String userKey limit:user: userId; Long ipCount redis.incr(ipKey); if (ipCount 1) { redis.expire(ipKey, 1); // 1秒窗口 } if (ipCount 10) { return false; } Long userCount redis.incr(userKey); if (userCount 1) { redis.expire(userKey, 60); // 60秒窗口 } return userCount 3; }心跳协商经常被忽略但它直接影响连接稳定性。WebSocket 协议本身有 ping/pong 帧但很多客户端不主动发。我的做法是服务端每 30 秒发一次 ping客户端收到后回 pong如果连续 3 次没收到 pong就主动断开连接。这样能及时清理死连接释放资源。心跳间隔不能太短否则浪费带宽和 CPU也不能太长否则死连接清理不及时。30 秒是我实测下来比较平衡的值。如果是移动端场景可以放宽到 60 秒因为移动网络切换频繁太短的心跳容易误判。3.2 消息分发怎么把一条弹幕推给十万人消息分发是直播后端最核心的逻辑。一条弹幕从主播发出到十万观众看到中间要经过好几个环节。第一步消息接收。主播客户端发弹幕到网关网关做基础校验长度、敏感词、频率然后写 Kafka。这里不直接写 Redis 或数据库因为写 Kafka 最快而且能削峰。第二步消息消费。后端有个消费者服务从 Kafka 拉消息做业务处理存 MongoDB、更新 Redis 里的弹幕列表、触发敏感词二次校验、计算弹幕热度。第三步消息推送。消费者处理完后把消息推给网关集群。这里有个关键问题网关是多台的观众连接分散在不同网关上怎么保证消息推给所有相关网关我的方案是用Redis Pub/Sub 做网关间广播。每个网关订阅自己负责的直播间频道消费者把消息发到对应频道所有订阅了该频道的网关都能收到然后各自推给自己持有的连接。// 网关订阅直播间频道 public void subscribeRoom(String roomId) { redisPubSub.subscribe(live:room: roomId, (channel, message) - { // 收到消息后推给本网关持有的该直播间连接 pushToLocalConnections(roomId, message); }); }但这个方案有个问题Redis Pub/Sub 不保证消息可靠投递如果网关在消息发布时正好重启这条消息就丢了。对于弹幕这种允许少量丢失的场景可以接受但礼物消息不能丢。所以礼物消息我走了另一条路写 Kafka网关作为消费者直接消费。每个网关消费所有礼物消息然后过滤出自己负责的直播间。这样虽然每个网关都要消费全量消息但保证了可靠性。这里有个取舍弹幕走 Pub/Sub延迟低但可能丢礼物走 Kafka可靠但延迟略高。实际场景里弹幕丢一两条没人发现礼物丢一条用户会投诉所以这个取舍是合理的。3.3 在线人数统计怎么做到既准确又不拖垮系统在线人数是个看起来简单、做起来很烦的功能。观众进进出出你要实时统计当前在线人数还要保证这个数字不能太离谱。我试过三种方案方案一Redis Set 存储。每个直播间的在线用户存在一个 Set 里用户进来SADD出去SREM人数用SCARD查。这个方案准确但问题是大直播间 Set 可能有几十万个元素SCARD是 O(1) 的还好但SREM在元素多的时候会慢而且内存占用大。方案二Redis HyperLogLog。HyperLogLog 是概率数据结构用极小的内存估算基数误差在 0.81% 左右。对于在线人数这种不需要精确到个位的场景完全够用。但 HyperLogLog 不支持删除元素用户退出后没法从计数里减掉。方案三分片计数 定时校准。这是我最终用的方案。把在线用户按用户 ID 哈希分散到 10 个 Redis 分片每个分片用 Set 存储。查人数时把 10 个分片的SCARD加起来。同时每 5 分钟做一次全量校准清理掉因为异常断开而残留的用户。// 分片存储在线用户 public void userOnline(String roomId, String userId) { int shard Math.abs(userId.hashCode()) % 10; String key live:room: roomId :online: shard; redis.sadd(key, userId); redis.expire(key, 3600); // 1小时过期防止残留 } public long getOnlineCount(String roomId) { long total 0; for (int i 0; i 10; i) { String key live:room: roomId :online: i; total redis.scard(key); } return total; }这个方案的好处是写入分散到 10 个分片单分片压力小查询时 10 次SCARD可以 pipeline 批量执行延迟很低定时校准保证数据不会偏差太大。校准逻辑要小心不能直接把 Set 清空重建那样会误删真实在线用户。我的做法是遍历 Set 里的用户检查他们的连接是否还活着通过网关的心跳记录不活的才删。3.4 礼物与排行榜强一致场景下的缓存设计礼物和排行榜是直播里最赚钱的功能也是最容易出问题的功能。用户刷了一个礼物排行榜必须立刻更新而且不能算错否则用户会投诉。这块我没用缓存分片因为排行榜要求强一致。我的方案是Redis Sorted Set 做实时排行榜MySQL 做持久化定时对账。礼物消息进来后先写 Kafka消费者处理时做两件事ZINCRBY更新 Redis 排行榜同时写一条流水到 MySQL。Redis 排行榜用ZREVRANGE查前 N 名延迟极低。但这里有个坑Redis 和 MySQL 的数据可能不一致。比如 Redis 更新成功但 MySQL 写入失败或者反过来。我的做法是以 MySQL 为准Redis 只做展示。每 5 分钟跑一次对账任务从 MySQL 聚合出正确的排行榜覆盖 Redis 里的数据。// 对账任务从 MySQL 聚合排行榜覆盖 Redis public void reconcileRanking(String roomId) { ListRankItem dbRank mysql.query( SELECT user_id, SUM(amount) as total FROM gift_record WHERE room_id ? AND create_time ? GROUP BY user_id, roomId, startTime ); String redisKey live:room: roomId :rank; redis.del(redisKey); for (RankItem item : dbRank) { redis.zadd(redisKey, item.getTotal(), item.getUserId()); } }对账的代价是 Redis 排行榜会短暂为空所以我的做法是双 key 切换维护rank:A和rank:B两个 key对账时写备用 key写完原子切换。这样用户无感知。礼物场景还有个特殊问题大额礼物需要事务保证。用户余额扣减和礼物记录写入必须在一个事务里否则会出现扣了钱没收到礼物的情况。这块我用的是本地消息表 定时补偿保证最终一致。4. 压测与调优那些只有真跑起来才知道的事4.1 压测环境搭建别在本地测数据会骗你压测这块我踩的坑最多。一开始我在本地 Mac 上压测结果 2000 并发就卡得不行以为是代码问题排查了半天才发现是本地网络和文件描述符限制。后来我把压测环境搬到云上用独立的压测机4 核 8G跑 JMeter被测服务在另一台机器上两台机器同区域内网互通。这样压测结果才靠谱。压测脚本我用的是 JMeter WebSocket 插件模拟真实用户行为建立连接、发心跳、随机发弹幕、随机刷礼物、定时查询在线人数。脚本里加了随机思考时间1 到 5 秒模拟真实用户的节奏。压测机本身也可能成为瓶颈。我用 4 核 8G 的机器跑 JMeter最多模拟 5000 个 WebSocket 连接就到顶了。如果要压更大规模得用多台压测机分布式压测或者用更轻量的压测工具比如 wrk 或 Gatling。4.2 第一次压测500 并发就崩了第一次正式压测我设了 500 并发结果服务在 3 分钟内就出现大量超时。排查过程如下第一步看监控。CPU 使用率不高40%内存正常但 Redis 的 QPS 飙到了 8 万而且有大量慢查询。第二步定位慢查询。用SLOWLOG GET查 Redis 慢日志发现大量SMEMBERS操作。原来是我在查在线用户列表时用了SMEMBERS这个命令在 Set 元素多的时候是 O(N) 的几十万元素的 Set 直接卡死 Redis。第三步修复。把SMEMBERS换成SRANDMEMBER抽样或者用SSCAN分批遍历。在线用户列表这种数据前端只需要展示一部分不需要全量拉取。第四步复测。修复后重新压测500 并发稳定通过Redis QPS 降到 2 万左右。这个坑的教训是Redis 的命令复杂度必须心里有数。O(N) 的命令在数据量大的时候就是灾难。常用的 O(N) 命令有KEYS、SMEMBERS、HGETALL、LRANGE大范围这些在生产环境都要慎用。4.3 第二次压测连接数上去了但消息延迟高修完 Redis 问题后我把并发提到 3000连接建立没问题了但消息延迟很高——弹幕从发出到收到平均要 3 秒峰值到 8 秒。排查发现两个问题问题一Kafka 消费者处理慢。消费者里做了太多同步操作写 MongoDB、更新 Redis、调敏感词服务。每个操作都要几十毫秒累积起来就慢了。修复把非核心操作异步化。写 MongoDB 改成批量写每 100 条或每 500ms 写一次敏感词校验改成异步先推送再校验发现违规再撤回。问题二网关推送是单线程的。我一开始用单个线程遍历连接推送消息连接多了之后遍历本身就很慢。修复改成多线程推送每个网关起一个线程池按连接哈希分配到不同线程。同时用 Netty 的ChannelGroup批量写减少系统调用。// 用 ChannelGroup 批量推送 ChannelGroup group roomChannelGroups.get(roomId); if (group ! null) { group.writeAndFlush(new TextWebSocketFrame(message)); }修复后重新压测3000 并发下弹幕延迟降到 200ms 以内达标。4.4 第三次压测模拟真实场景的混合负载前两次压测都是单一场景第三次我做了混合负载70% 用户在观看只收弹幕不发20% 用户偶尔发弹幕5% 用户刷礼物5% 用户在抽奖。这次压测暴露了一个新问题抽奖场景下 Redis 出现热点 key。抽奖按钮的计数器 key 被每秒几万次INCR单个 Redis 节点 CPU 飙到 90%。修复方案抽奖计数器做本地缓存 批量提交。每个网关本地维护一个计数器用户点击时先加本地计数每 100ms 把本地计数批量提交到 Redis。这样 Redis 的写入量降低了两个数量级。// 本地计数器批量提交 private AtomicLong localCounter new AtomicLong(0); public void onLotteryClick() { localCounter.incrementAndGet(); } // 定时任务每100ms提交一次 Scheduled(fixedRate 100) public void flushCounter() { long count localCounter.getAndSet(0); if (count 0) { redis.incrBy(lottery:count: roomId, count); } }这个方案的代价是计数有最多 100ms 的延迟而且网关重启会丢失未提交的计数。对于抽奖这种场景100ms 延迟可以接受丢失几条计数也无所谓。但如果是涉及金钱的场景就不能这么搞。4.5 调优清单那些必须改的内核参数压测到后期瓶颈往往不在应用层而在操作系统。以下是我调整过的关键参数参数默认值调整值作用net.core.somaxconn12865535增大 TCP 连接队列net.ipv4.tcp_max_syn_backlog102465535增大 SYN 队列net.ipv4.tcp_tw_reuse01复用 TIME_WAIT 连接net.ipv4.ip_local_port_range32768-609991024-65535扩大本地端口范围fs.file-max系统默认1000000增大文件描述符上限net.ipv4.tcp_keepalive_time7200300加快死连接回收这些参数改完之后单机维持的连接数从 1 万提升到了 5 万以上。但要注意tcp_tw_reuse在某些场景下可能导致 NAT 问题如果服务部署在容器里要谨慎开启。文件描述符限制要改两处/etc/security/limits.conf里的nofile以及 systemd 服务的LimitNOFILE。只改一处不生效这个坑我踩过。5. 上线后的真实表现与后续优化方向5.1 真实流量下的表现跟压测差距有多大上线第一周我盯监控盯得眼睛都快瞎了。真实流量跟压测确实有差距主要体现在三个方面连接建立更分散。压测时连接是瞬间建立的真实场景下用户是陆续进来的这对系统反而更友好因为压力是渐进的。消息类型更杂。压测时我只模拟了弹幕和礼物真实场景还有系统通知、进房欢迎、关注提醒、分享消息等十几种类型。消息类型多了之后序列化和反序列化的开销上去了我后来把 JSON 换成了 Protobuf体积小了 60%序列化速度快了一倍。用户行为更随机。压测脚本的行为是固定的真实用户会突然大量发弹幕、突然集体退出、突然疯狂刷礼物。这种随机性对系统的弹性要求更高。我的应对是加了自动扩容策略当单网关连接数超过阈值时自动触发扩容新连接分配到新网关。5.2 还没解决的问题消息顺序性和重复消费有两个问题我到现在还没完全解决写出来给后来人提个醒。消息顺序性。弹幕消息理论上应该按发送顺序展示但走 Kafka 多分区 多消费者之后顺序没法保证。用户看到的是弹幕 A 在弹幕 B 后面发出但显示在 B 前面。我的缓解方案是给每条消息加时间戳客户端按时间戳排序展示。但这只是缓解不是根治。重复消费。Kafka 的 at-least-once 语义意味着消息可能重复。我用了 Redis 做去重每条消息带唯一 ID消费前先查 Redis 是否已处理。但 Redis 去重有窗口期窗口外的重复没法防。对于弹幕这种场景偶尔重复一条用户能接受但礼物重复就是事故了。礼物消息我加了数据库唯一索引兜底重复插入会失败然后走补偿逻辑。5.3 如果重来一次我会怎么改如果让我重新搭这套环境有几个地方我会改第一网关用 Go 重写。Netty 虽然成熟但 JVM 的内存占用和 GC 停顿在长连接场景下确实是负担。Go 的 goroutine 模型更适合这种场景单机连接数能翻倍。第二消息队列换成 Pulsar。Pulsar 的计算存储分离架构更适合直播这种流量波动大的场景扩容更灵活而且支持多租户不同直播间可以隔离。第三缓存层加本地缓存。现在所有读都走 Redis其实像直播间基本信息这种变化很少的数据可以在网关本地缓存一份减少 Redis 压力。用 Caffeine 做本地缓存配合 Redis 的发布订阅做失效通知。这套环境从零搭到现在稳定运行前后花了大概两个月。中间踩的坑、熬的夜、看的监控图都浓缩在这篇笔记里了。直播高并发这个方向理论是一回事真跑起来是另一回事。希望这些实际经验能帮到正在做类似事情的你。
返回列表