Raft算法实战:从选举机制到日志复制的完整流程解析
1. 为什么我们需要Raft从“拜占庭将军”到“日志复制机”如果你刚接触分布式系统可能会被一堆术语搞晕一致性、共识、高可用、分区容错……别慌咱们先从一个故事开始。想象一下你是一个将军和另外四个将军一起围困一座城堡。你们只能通过信使传递消息来商量“进攻”还是“撤退”。但问题是信使可能被敌人截获甚至队伍里可能有叛徒故意传递假消息。这就是著名的“拜占庭将军问题”。在真实的计算机世界里我们通常不担心“叛徒”恶意节点但“信使被截获”太常见了——网络会延迟、会丢包、服务器会突然宕机。Raft算法要解决的就是这个简化版的“将军问题”在允许机器宕机、网络不可靠的环境下让一组服务器对一个值或者一系列操作指令达成一致。这个“一致”的结果就是所有存活的、正常的服务器最终看到的数据都是一样的。我刚开始接触Raft时觉得它特别像在管理一个“日志复制机”。这个机器有一个核心任务保证所有节点上的操作日志比如“把A设为1”、“把B加3”完全一样并且按相同的顺序执行。只要日志一样执行顺序一样那么每个节点算出来的最终状态比如数据库里A和B的值就肯定一样。这就是Raft最核心的魔法。所以Raft不是什么高深莫测的理论它就是一个管理复制日志的协议。它把复杂的分布式一致性问题拆解成了三个相对独立、更好理解的子问题领导选举Leader Election总得有个“话事人”来拍板决定日志的顺序。不能大家七嘴八舌一起写。日志复制Log Replication“话事人”把客户端的指令记到自己的小本本日志上然后让所有“小弟”都抄一份。安全性Safety立下规矩确保无论发生什么故障比如“话事人”突然挂了都不会出现数据错乱比如同一个位置日志出现两个不同的指令。接下来我们就钻进这个“日志复制机”的内部看看它是怎么运转的。我会结合我这些年做分布式存储系统的实战经验把每个环节的细节、踩过的坑都掰开揉碎了讲给你听。2. 角色与任期Raft世界的“君主立宪制”在Raft的世界里每个服务器节点在任意时刻都扮演着三种角色之一领导者Leader、跟随者Follower和候选人Candidate。这个设计非常清晰不像有些算法那样角色模糊。领导者Leader绝对的“话事人”。所有客户端的请求都必须发给它。它负责接收请求、把请求包装成日志条目并推动这些日志在所有节点间复制。一个集群里同一时刻有且只能有一个有效的领导者。跟随者Follower沉默的“实干家”。它们不主动发起任何请求只响应来自领导者和候选人的请求。它们的工作就是复制领导者的日志并在领导者心跳超时时“揭竿而起”参与选举。候选人Candidate临时的“竞选者”。当跟随者等待领导者心跳超时后它会转换为此角色发起一轮选举争取成为新的领导者。这三个角色之间的关系是动态流转的下图清晰地展示了它们之间的转换关系stateDiagram-v2 [*] -- Follower Follower -- Candidate: 选举超时未收到Leader心跳 Candidate -- Follower: 发现更高任期Term或选举超时 Candidate -- Leader: 获得超过半数选票 Leader -- Follower: 发现更高任期Term任期Term是Raft中一个非常巧妙的设计你可以把它理解为逻辑时钟或者“朝代编号”。它是一个连续递增的整数1, 2, 3...。每一个任期都从一次选举开始如果选举成功就会产生一个领导者来管理这个任期直到任期结束。如果选举失败比如票数分散那么这个任期会立刻结束并立刻开始一个新的任期重新选举。任期有几个关键作用识别过时信息每个RPC消息都携带发送方的当前任期号。如果一个节点收到任期号比自己小的请求它会拒绝并告知对方最新的任期号迫使对方更新。这能迅速让落后的节点“认清现实”。保证同一任期只有一个领导者这是Raft安全性的基石。通过任期和投票机制数学上可以证明不可能在同一任期内选出两个领导者。充当逻辑时间戳日志条目里也记录了它创建时的任期号。这在解决日志冲突和判断数据新旧时至关重要。在我实现第一个Raft库的时候就曾在任期处理上栽过跟头。有一次网络分区恢复后一个旧任期的领导者试图发送心跳结果因为任期号太小被已经进入新任期的小弟们无情拒绝并被告知了新的任期号它立马“退位”成了跟随者。这个机制自动处理了“脑裂”后恢复的场景非常优雅。3. 领导选举随机超时与“心跳夺权”选举是Raft一切活动的开端。没有领导者整个集群就无法处理写请求。选举的目标很简单快速、安全地选出一个唯一的领导者。3.1 触发选举等待的艺术所有节点启动时都是跟随者。它们会启动一个选举超时计时器这个时间是在一个固定区间内随机选取的例如150ms-300ms。为什么是随机的这是为了避免多个跟随者同时超时、同时发起选举导致选票分散谁也当不上领导者。只要跟随者能定期收到来自当前领导者的心跳一种特殊的AppendEntries RPC不包含日志它就会重置这个计时器安心做小弟。一旦计时器超时意味着这个跟随者“感觉”领导者可能挂了也可能是网络问题它就会开始自己的“竞选之路”。3.2 竞选流程拉票与投票自增任期转换角色节点首先将自己的当前任期号加1然后从跟随者转变为候选人。投自己一票先给自己投上一票自信是成功的第一步。并行拉票向集群中的所有其他节点并行发送RequestVote RPC请求它们给自己投票。等待裁决然后进入等待状态直到发生以下三种情况之一赢得选举收到了超过半数包括自己的赞成票。立刻晋升为领导者开始向所有节点发送心跳确立统治地位。他人胜出在等待期间收到了来自其他领导者的AppendEntries RPC心跳。如果这个RPC中的任期号大于等于自己的当前任期说明已经有合法的领导者产生了自己心服口服地退回跟随者状态。如果任期号比自己小则拒绝该RPC并告知对方最新任期。选举僵局一段时间内没有收到足够选票也没有发现新领导者。这说明选票可能被分散了比如多个候选人同时竞选。此时候选人会递增任期号重置随机选举超时发起新一轮选举。3.3 投票规则不是谁都能当领导一个节点在收到RequestVote RPC时投票是非常谨慎的它遵循“先来后到”和“德才兼备”的原则一个任期一票在每个任期内一个节点最多只能投出一票。这从根本上防止了同一个任期内出现两个领导者。日志必须足够新候选人必须拥有不旧于投票者本地的日志投票者才会考虑投票。具体比较规则是先比较最后一条日志的任期号任期号大的更新如果任期号相同则日志索引长度大的更新。这条规则至关重要它保证了被选出的领导者一定包含了所有已提交的日志条目从而避免了数据丢失。我踩过的坑早期测试时我忽略了日志比较规则结果一个日志落后的节点被选为了领导者。它一上台就用自己陈旧的日志覆盖了其他节点上更新的日志导致已提交的数据被回滚造成了严重的数据不一致。这个教训让我深刻理解了“日志足够新”这条规则的安全性意义。3.4 网络分区与选举安全网络分区是分布式系统的噩梦。假设一个5节点的集群被分割成两部分一边3个节点一边2个节点。拥有3个节点的那边可以正常选举出领导者获得35/2票并继续处理客户端请求。但由于无法联系到另一边的2个节点它无法将日志复制到“大多数”因此客户端的写请求会阻塞无法完成因为Raft要求日志必须复制到大多数节点才算提交。但读请求如果配置了正确的方式可能仍然可以服务。只有2个节点的那边由于无法获得超过半数的选票2不大于5/2会不断地发起选举、超时、再选举但永远选不出领导者处于不可用状态。当网络恢复后少数派那边的节点会收到来自多数派那边领导者的心跳发现对方的任期号更高便会自动降级为跟随者并同步自己缺失的日志。整个集群最终恢复一致。4. 日志复制强一致性的核心引擎选举出领导者后它就要开始履行核心职责处理客户端请求并保证日志在所有节点间一致地复制。这是实现强一致性的关键。4.1 日志的结构与提交每个节点的日志都是一个按顺序排列的条目序列。每个日志条目包含三个关键信息指令Command客户端请求的实际操作比如set x 1。任期号Term创建该条目时领导者的任期。这是判断日志新旧和解决冲突的关键。索引Index日志中的位置从1开始单调递增。领导者收到客户端写请求后的处理流程堪称一场精密的协作本地追加领导者将请求包装成一个日志条目持久化追加到自己的本地日志末尾。这里必须持久化如果只是写在内存里领导者宕机后这条记录就丢了可能导致数据丢失。并行复制领导者通过AppendEntries RPC将这条新日志条目并行地发送给所有跟随者。跟随者确认每个跟随者收到RPC后会进行一致性检查检查领导者发来的前一条日志的索引和任期是否与自己本地日志匹配。如果匹配说明日志是连续的则安心地将新条目追加到本地日志同样需要持久化并回复成功。如果不匹配比如自己缺了日志或者有冲突则回复失败。领导者提交当领导者收到超过半数节点的成功回复后就认为这条日志条目已经安全了可以提交Commit。领导者会更新本地的commitIndex已提交的最高日志索引。应用状态机领导者将这条已提交的日志条目应用Apply到自己的状态机比如真正执行set x1这个操作然后将执行结果返回给客户端。通知跟随者提交在后续的心跳或AppendEntries RPC中领导者会将最新的commitIndex告知跟随者。跟随者一旦发现自己的commitIndex落后于领导者的就会按顺序将自己日志中已提交但未应用的条目应用到本地状态机。这里有个非常重要的细节领导者提交一条日志和跟随者应用这条日志到状态机是两个不同的步骤并且可能存在时间差。但Raft保证一旦领导者提交了某条日志那么未来所有的领导者都会拥有这条日志并且最终所有存活节点的状态机都会以相同的顺序应用它。4.2 日志冲突与强制覆盖领导者的权威天下没有不散的筵席也没有永不中断的网络。跟随者的日志可能因为网络延迟、节点重启等原因与领导者不一致。Raft处理不一致的方式非常霸道且有效领导者强制跟随者复制自己的日志覆盖掉冲突的部分。领导者为每个跟随者维护一个nextIndex表示下一个要发送给该跟随者的日志索引。当一个AppendEntries RPC因日志不匹配而失败时领导者会将nextIndex递减然后重试。这个过程一直持续到找到一个领导者与跟随者日志一致的位置。然后领导者会从这个位置开始发送之后的所有日志条目跟随者会删除从该位置之后自己本地的所有冲突条目并接受领导者的日志。这个机制保证了领导者的日志永远不会被覆盖日志的一致性总是以领导者的日志为准。这被称为Leader Append-Only特性是Raft安全性的又一个基石。实战经验在实现nextIndex回退时如果每次只减1在网络抖动、日志差异很大时效率会很低。一个常见的优化是让跟随者在拒绝RPC时附带一些额外信息比如自己冲突日志的任期和该任期的第一个索引。领导者可以据此快速回退到冲突任期的开始位置加速日志同步过程。4.3 提交旧任期日志的“陷阱”与规则这是Raft中最精妙也最容易出错的地方之一。考虑一个场景一条日志已经复制到了大多数节点但领导者在提交它之前宕机了。新领导者上台后它应该提交这条“前任未竟的事业”吗Raft的答案是新领导者不能直接根据“大多数”原则来提交旧任期的日志。它必须通过提交一条自己任期内的新日志来“顺带”提交之前任期的所有日志。这个规则写在论文里我当初看了好几遍才彻底明白。为什么想象一个分裂投票的场景一条旧日志S在大多数节点上存在但未提交然后一个没有这条日志的节点被选为了新领导者因为它日志足够新。如果新领导者可以提交旧日志S那么它可能会用另一条日志覆盖S导致已复制到大多数节点的数据被回滚违反了安全性。Raft的解决方案是只有当前任期的日志条目才能通过“计数大多数”的方式直接提交。一旦当前任期的某条日志被提交根据日志匹配特性Log Matching Property所有之前的日志也就都被间接提交了。这个设计确保了数据的绝对安全。5. 集群成员变更与日志压缩实战中的高级话题前面的章节构成了Raft的核心。但在生产环境中我们还需要处理两个实际问题动态增减机器以及日志无限增长的问题。5.1 安全地变更集群成员你不能直接简单粗暴地把新节点的配置发给所有节点因为可能在同一时刻一部分节点用了新配置另一部分还用旧配置这可能导致在一个任期内出现两个“大多数”从而选出两个领导者脑裂。Raft采用了一种称为“联合共识Joint Consensus”的两阶段方法切换到过渡配置领导者首先向日志中写入一个Cold,new的特殊配置条目。这个配置结合了旧配置和新配置。在此配置下无论是旧配置的多数派还是新配置的多数派都需要同意才能做出决策如提交日志、选举领导者。这保证了在切换过程中不会出现脑裂。提交新配置一旦Cold,new被提交系统就安全地进入了这个中间状态。接着领导者再写入并提交只包含新节点配置的Cnew条目。提交后变更完成旧节点可以安全下线。这个过程虽然复杂但保证了在配置变更的整个过程中集群的安全性不受影响。像etcd、TiKV这些系统都实现了这个机制。5.2 日志压缩不可能无限增长的日志如果每条客户端命令都永久记录在日志中日志文件会无限膨胀。Raft通过快照Snapshot来解决。每个节点可以独立地、在任意时刻对自己的当前状态机状态生成一个快照并截取到快照点之前的日志。快照包含了状态机数据如KV数据。最后一条被包含在快照中的日志的索引和任期。集群配置信息。当领导者需要向一个落后的跟随者同步日志而所需的日志条目已经被压缩成快照时领导者会发送一个InstallSnapshot RPC将快照直接发送给跟随者。跟随者加载快照后其状态机就跳到了快照点然后只需要同步快照点之后的增量日志即可。我的经验生成快照是一个耗I/O的操作最好在后台线程异步进行避免阻塞正常的主流程日志复制。同时快照的频率需要权衡太频繁浪费资源太稀疏则日志恢复慢且跟随者可能需要传输巨大的快照文件。6. 从理论到代码一个极简的Raft核心逻辑示例说了这么多我们来看一段高度简化的伪代码感受一下Raft领导者处理客户端写请求和日志复制的核心循环。这能帮你把前面的概念串联起来。class RaftNode: def __init__(self): self.state follower self.current_term 0 self.log [] # 日志条目列表 self.commit_index 0 self.last_applied 0 self.next_index {} # 每个follower的下一个发送索引 self.match_index {} # 每个follower已复制的最高索引 # 领导者处理客户端写请求 def handle_client_write(self, command): if self.state ! leader: raise NotLeaderError(请求请发送给领导者) # 1. 创建日志条目本地持久化 new_entry LogEntry(termself.current_term, commandcommand, indexlen(self.log)1) self.log.append(new_entry) self.persist_to_storage() # 关键持久化 # 2. 并行复制给所有跟随者 for follower in self.followers: self.send_append_entries(follower) # 3. 异步等待响应检查是否提交 # (在实际实现中这会由响应处理线程或回调来完成) # 发送 AppendEntries RPC 给特定跟随者 def send_append_entries(self, follower_id): prev_index self.next_index[follower_id] - 1 prev_term self.log[prev_index].term if prev_index 0 else 0 entries_to_send self.log[self.next_index[follower_id]-1:] # 发送从next_index开始的日志 rpc AppendEntriesRPC( termself.current_term, leader_idself.id, prev_log_indexprev_index, prev_log_termprev_term, entriesentries_to_send, leader_commitself.commit_index ) send_rpc(follower_id, rpc, self.handle_append_entries_response) # 处理 AppendEntries 响应 def handle_append_entries_response(self, follower_id, success, term): if term self.current_term: self.step_down_to_follower(term) # 发现更高任期主动退位 return if success: # 复制成功更新对应follower的匹配索引和下一个索引 self.match_index[follower_id] max(self.match_index[follower_id], last_sent_index) self.next_index[follower_id] self.match_index[follower_id] 1 # 检查是否有日志条目可以提交了 self.update_commit_index() else: # 复制失败日志不匹配回退next_index重试 self.next_index[follower_id] - 1 self.send_append_entries(follower_id) # 立即重试 # 更新已提交索引 def update_commit_index(self): # 遍历所有索引 N (大于当前commit_index) for n in range(self.commit_index 1, len(self.log) 1): # 统计有多少个follower的match_index n count 1 # 领导者自己肯定有 for fid in self.followers: if self.match_index.get(fid, 0) n: count 1 # 如果超过半数并且这个日志条目是当前任期的 if count len(self.followers) / 2 and self.log[n-1].term self.current_term: self.commit_index n # 应用已提交但未应用的日志 self.apply_committed_entries()这段伪代码省略了选举、持久化、网络通信等大量细节但它清晰地勾勒出了领导者进行日志复制的骨架本地追加 - 并行发送 - 收集确认 - 推进提交 - 应用状态机。在实际的工程实现中每一个步骤都需要仔细处理并发、超时、重试和错误。7. 总结与最佳实践走完了从选举到日志复制的完整流程你会发现Raft的成功在于它将一个复杂问题分解成几个状态清晰、规则明确的子问题。它通过强领导者模型简化了逻辑通过随机化超时避免了活锁通过任期和日志索引保证了安全性。在真正基于Raft构建系统时我有几点深刻的体会持久化是关键任期、投票记录、日志这三样东西必须在响应RPC之前就持久化到磁盘。否则节点重启后可能“失忆”导致违反安全性承诺。这是很多新手实现时容易忽略的致命点。一切皆状态机将Raft的核心逻辑角色、任期、日志、提交索引等视为一个状态机所有的RPC处理和超时触发都是事件驱动状态机变迁。这种思维模型会让你的代码结构非常清晰。测试测试再测试分布式系统的诡异之处在于你永远不知道错误会在什么组合条件下出现。一定要进行大量的、包括网络分区、机器宕机、随机延迟、乱序等情况的模拟测试。Jepsen这类工具是你的好朋友。理解“线性一致性读”领导者处理读请求时不能直接读状态机因为可能读到未提交的数据。常见的方案是ReadIndex或Lease Read核心思想都是确保领导者身份有效并且读到的数据至少是已提交的。奇数个节点部署集群时选择3、5、7这样的奇数个节点。因为Raft的“大多数”原则一个3节点集群可以容忍1个节点故障一个4节点集群同样只能容忍1个故障但多了1个节点的成本和通信开销。奇数个节点在容错和成本上性价比最高。Raft算法像是一份设计精妙的乐谱而真正的挑战在于如何用代码这支乐队将它完美地演奏出来。它没有魔法有的只是对状态、时序和规则的严谨定义。当你理解了每个环节“为什么这么做”之后不仅能够更好地使用etcd、Consul、TiKV这些基于Raft的系统更能获得一种设计和构建可靠分布式系统的底层思维。这才是学习Raft最大的收获。

相关新闻

只有 IPA 没有源码时,如何给 iOS 应用做安全处理

只有 IPA 没有源码时,如何给 iOS 应用做安全处理

有一次接手维护一个旧项目,开发团队已经解散,仓库权限也丢失,手里只剩下一个可以运行的 IPA。需求很明确,这个应用需要继续发布新版本,但同时要做一定程度的保护,避免被直接反编译分析。 没有源码意味着很多…

2026/7/3 9:53:36 阅读更多 →
LightOnOCR-2-1B实战案例:跨境电商平台多语言商品详情页OCR生成SEO文案

LightOnOCR-2-1B实战案例:跨境电商平台多语言商品详情页OCR生成SEO文案

LightOnOCR-2-1B实战案例:跨境电商平台多语言商品详情页OCR生成SEO文案 1. 引言:当跨境电商遇上多语言OCR 想象一下这个场景:你是一家跨境电商公司的运营,每天要处理来自全球各地的商品图片。日本供应商发来的产品说明书、德国工…

2026/6/27 11:05:17 阅读更多 →
从数据整合到场景落地,JBoltAI 打造企业 AI 全流程

从数据整合到场景落地,JBoltAI 打造企业 AI 全流程

企业 AI 落地的过程中,往往会陷入 “数据散、资源乱、场景断” 的困境:数据分散在各类数据库、文档、系统接口中难以整合,大模型、向量库等 AI 资源对接繁琐,智能应用难以与实际业务场景深度融合。而 JBoltAI 凭借全流程的技术体系…

2026/6/26 6:59:51 阅读更多 →

最新新闻

Qt项目引入第三方库,使用已编译库文件和源码编译方式的区别

Qt项目引入第三方库,使用已编译库文件和源码编译方式的区别

Qt项目引入第三方库,使用已编译库文件和源码编译方式的区别 一、对比总览维度已编译库文件方式(预编译)源码编译方式(源码集成)构建速度快,直接链接预编译好的二进制,跳过编译过程慢&#xff0c…

2026/7/3 9:54:54 阅读更多 →
3分钟掌握Adobe-GenP:Adobe全家桶免费激活终极指南

3分钟掌握Adobe-GenP:Adobe全家桶免费激活终极指南

3分钟掌握Adobe-GenP:Adobe全家桶免费激活终极指南 【免费下载链接】Adobe-GenP Adobe CC 2019/2020/2021/2022/2023 GenP Universal Patch 3.0 项目地址: https://gitcode.com/gh_mirrors/ad/Adobe-GenP Adobe-GenP是一款专为Adobe Creative Cloud系列软件设…

2026/7/3 9:52:54 阅读更多 →
终极指南:Mammoth.js如何实现Word文档到HTML的智能转换

终极指南:Mammoth.js如何实现Word文档到HTML的智能转换

终极指南:Mammoth.js如何实现Word文档到HTML的智能转换 【免费下载链接】mammoth.js Convert Word documents (.docx files) to HTML 项目地址: https://gitcode.com/gh_mirrors/ma/mammoth.js Mammoth.js是一个强大的JavaScript库,专门用于将Mic…

2026/7/3 9:52:53 阅读更多 →
村长团队ZM3从零制作GTA5可旋转风车模型+轴心绑定+物理动画超详细步骤教程

村长团队ZM3从零制作GTA5可旋转风车模型+轴心绑定+物理动画超详细步骤教程

ZM3从零制作GTA5可旋转风车完整模型轴心绑定物理动画全套超详细无脑实操教程一、打开ZM3并提前调好所有GTA5专用基础环境(不调后面百分百报错)1.直接双击电脑桌面上的zModeler3软件图标,等软件完全打开,不要点任何弹窗广告&#x…

2026/7/3 9:48:52 阅读更多 →
不懂 GEO 优化容易踩坑!苏州昆山服务商挑选完整实操教程

不懂 GEO 优化容易踩坑!苏州昆山服务商挑选完整实操教程

2026 年,昆山的大量外贸与制造业老板发现,过去砸钱做百度竞价、1688 店铺还能接到询盘,但现在年轻采购商和工程师更倾向于直接问 AI:“昆山哪家做精密模具好?”"江苏地区推荐什么品牌的自动化设备?&qu…

2026/7/3 9:46:51 阅读更多 →
Adobe-GenP 3.0终极破解教程:3分钟免费解锁Adobe全家桶完整指南

Adobe-GenP 3.0终极破解教程:3分钟免费解锁Adobe全家桶完整指南

Adobe-GenP 3.0终极破解教程:3分钟免费解锁Adobe全家桶完整指南 【免费下载链接】Adobe-GenP Adobe CC 2019/2020/2021/2022/2023 GenP Universal Patch 3.0 项目地址: https://gitcode.com/gh_mirrors/ad/Adobe-GenP Adobe-GenP是一款专为Adobe Creative Cl…

2026/7/3 9:46:51 阅读更多 →

日新闻

Nginx防御TLS重协商攻击实战:从原理到配置与监控

Nginx防御TLS重协商攻击实战:从原理到配置与监控

1. 项目概述:为什么TLS重协商攻击至今仍需警惕十多年前的CVE-2011-1473,一个关于TLS/SSL协议重协商机制的漏洞,现在提起来还有必要吗?很多运维和开发朋友可能会觉得,这都老掉牙了,现代服务器和客户端不都默…

2026/7/3 0:03:59 阅读更多 →
华为防火墙双通道远程管理实战:Web与SSH配置详解

华为防火墙双通道远程管理实战:Web与SSH配置详解

1. 项目概述:为什么需要双通道远程管理防火墙?在任何一个稍具规模的企业网络里,防火墙都是那个默默守护在边界的关键角色。作为网络工程师,我们不可能每次都跑到机房,插上console线去配置它。远程管理能力,…

2026/7/3 0:03:59 阅读更多 →
AD74413R与PIC18F65K40的高精度工业数据采集方案

AD74413R与PIC18F65K40的高精度工业数据采集方案

1. 项目概述:AD74413R与PIC18F65K40的协同工作在工业自动化和精密测量领域,同时实现高精度模数转换(ADC)和数模转换(DAC)功能是许多复杂系统的核心需求。AD74413R作为一款四通道可配置模拟输入/输出器件,与PIC18F65K40微控制器的组合&#xf…

2026/7/3 0:05:59 阅读更多 →

周新闻

月新闻