ZooKeeper核心原理与大数据生态整合实战

ZooKeeper核心原理与大数据生态整合实战 在分布式系统和大数据平台里摸爬滚打久了你会发现一个现象几乎所有核心组件——HDFS、Kafka、HBase、Flink——都绕不开一个叫做Zookeeper的东西。它看起来不起眼就是一个独立的小服务但一旦它出问题整个上层的数据管道、任务调度、高可用切换瞬间就会瘫痪。很多同学对Zookeeper的理解停留在“啊它就是个注册中心”或者“用来做Leader选举的工具”这远远不够。今天这篇就围绕Zookeeper的分布式协调原理把它的数据模型、选举机制、ZAB协议、集群部署以及和大数据生态的整合实战一次说透希望能帮你把这块硬骨头啃下来。这篇内容适合正在准备大数据面试的开发者、正在维护Hadoop/Kafka/HBase集群的运维工程师以及刚开始接触分布式系统的同学。我会尽量用大白话拆原理再配合实际部署和排障经验让你既能明白“它为什么这么设计”也能知道“真出问题时该怎么办”。1. Zookeeper到底在协调什么1.1 分布式系统的“三座大山”和Zookeeper的角色先想一个问题为什么单机系统不需要Zookeeper因为单机环境下所有状态都在一个进程里大家对同一份数据的读写天然一致。但分布式系统不一样几十台、几百台机器通过网络协作会遇到三个典型问题第一个是信息不对称。节点A改了配置节点B不知道整个集群的行为就不一致了。第二个是决策权争夺。多个节点同时想做主到底听谁的第三个是状态漂移。一个节点挂了其他节点怎么感知、怎么处理Zookeeper解决的就是这些问题它是分布式系统的“协调者”负责维护一份所有节点都认可的公共状态并提供原语级别的操作——比如临时节点、顺序节点、监听通知——来帮助上层应用实现配置同步、命名服务、分布式锁和集群管理。可以把它理解成公司里的行政部各个部门节点不需要互相盯着对方在干嘛只要按照行政部Zookeeper发布的公告来行动就对了。1.2 为什么不用Redis或数据库来做协调有的同学可能会问Redis不是也能存数据吗MySQL不也有主从同步吗为什么非要Zookeeper关键在于Zookeeper提供的一致性保证和协调语义。顺序一致性所有写请求会被分配一个全局递增的ZXID客户端看到的变更顺序在全局是唯一的。原子性一次写操作要么全部节点都成功要么全部失败不会出现“部分节点更新了、部分没更新”的情况。单一视图无论客户端连接的是集群中哪台机器看到的服务端数据状态都是一致的。可靠性消息一旦被过半节点确认就会被持久化即使后续有节点宕机数据也不会丢。实时性最终一致虽然Zookeeper不保证强一致但它保证在会话有效期内客户端能在一个时间窗口内看到最新状态。Redis的主从复制是异步的主节点写入成功但还没同步给从节点时主节点挂了数据就丢了MySQL的同步机制更擅长事务处理而不是节点协调。Zookeeper的ZAB协议针对“分布式协调”这个场景做了极致优化——写请求必须过半节点确认才算成功这既保证了可靠性又避免了等待全部节点确认带来的性能瓶颈。这就是为什么Hadoop、Kafka这些重量级组件都选择Zookeeper当协调者而不是直接用数据库或缓存。1.3 Zookeeper在大数据生态中的典型应用场景具体到大数据平台里Zookeeper主要承担以下几类工作HDFS HA高可用Active NameNode和Standby NameNode通过Zookeeper进行故障自动转移。Active节点会在Zookeeper上创建一个临时节点当Active宕机时临时节点自动消失Standby通过监听机制感知到变化后马上接管服务。Kafka Broker管理Kafka的Controller负责分区Leader选举、分区分配等就是通过Zookeeper的临时节点和Watch机制动态选出来的。Broker上下线时也会通过Zookeeper通知Controller和其他Broker。HBase集群协调HBase使用Zookeeper管理HMaster的选举、RegionServer的注册和元数据存储meta表的位置。还有一个常见的场景是分布式锁。多个任务节点抢同一个资源时可以在Zookeeper上创建一个顺序临时节点通过节点的顺序号来决定谁先获得锁。相比Redis的分布式锁Zookeeper的锁天然支持阻塞等待和公平性在任务调度场景比如多个Flink作业抢同一个外部系统写入权里非常实用。2. 核心原理数据模型、节点类型与Watch机制2.1 ZNode树状结构像文件系统但又不是文件系统Zookeeper的数据模型是一棵倒置的树每个节点叫ZNode。每个ZNode既可以存储数据数据量建议控制在1MB以内也可以拥有子节点。看起来和文件系统很像——路径用“/”分隔比如/hadoop-ha/mycluster/ActiveBreadCrumb。但有几个关键差异值得注意每个ZNode的数据量非常小它不是给业务存大对象的存的都是元数据、状态信息、配置项。每个ZNode通过路径唯一标识不允许递归创建父节点不存在时子节点创建会报错。ZNode存储的不只是数据还包括stat状态信息版本号、时间戳、数据长度、子节点数量等。这个设计保证了它即使在高并发写入下也能维持低延迟的读写性能。同时因为数据量小同步成本也低ZAB协议广播一次事务的花销也就相对可控。2.2 节点类型详解什么时候用临时节点什么时候用顺序节点ZNode的类型并不只是“持久”和“临时”两种生产环境里实际有五种持久节点Persistent创建后一直存在除非手动删除。适合存储配置信息、公共元数据比如/config/app1。临时节点Ephemeral和客户端会话绑定。客户端会话结束超时或主动断开后临时节点自动消失。这个特性特别适合做服务注册和故障感知——比如HDFS的Active NameNode会在Zookeeper上创建一个临时节点一旦进程退出节点自动消失备用节点立刻顶上。持久顺序节点Persistent Sequential在持久节点基础上Zookeeper会为每个节点自动追加一个单调递增的序号。适合做分布式队列、公平锁等场景通过序号可以判断节点的先后顺序。临时顺序节点Ephemeral Sequential结合了临时和顺序两者的特性。分布式锁的标准实现就是用它——多个客户端同时创建临时顺序节点序号最小的获得锁其他客户端监听前一个节点当前一个节点被删除释放锁时下一个客户端获得锁。容器节点Container4.x版本引入当容器节点的所有子节点被删除后容器节点会自动被Zookeeper回收。适合做业务上的“目录”节点。TTL节点3.5版本以上支持可以为节点设置过期时间到期后自动删除。在大数据平台里主要用于临时性的缓存状态。选型建议需要感知“节点是否存活”的用临时节点需要保证顺序和公平性的用顺序节点单纯存储数据的用持久节点。2.3 Watch机制一次性的通知别指望它反复推送Watch是Zookeeper实现协调的核心特性。客户端可以在ZNode上设置监视器当这个节点发生变化数据变更、子节点变化、节点删除时Zookeeper会主动推送一个通知给客户端。但有三个非常要注意的点第一Watch是一次性的。触发一次之后如果想要继续监听必须重新注册。很多初学者踩过的坑就是收到一次通知后以为后续还会自动推送结果错过了后面的状态变化。正确做法是在每次收到通知后重新设置Watcher。第二通知语义是“有变化”而不是“变化详情”。Watch只告诉客户端“节点数据变了”不会直接推送变更后的数据。客户端收到通知后要主动再调一次getData或getChildren去拉最新状态。第三Watch事件是有可能丢失的。Zookeeper只保证在Watch触发到下一次设置的时间窗口内客户端能收到通知。如果在设置新Watch之前发生了变化客户端可能会错过这个变化。所以对于关键状态比如Leader的存活标记不能只依赖Watch还要配合定期轮询。在大数据组件里Watch机制用得最多的地方就是HDFS的FailoverController和Kafka的Controller选举——它们监听临时节点的变化来感知主节点的存活状态。3. ZAB协议与Leader选举Zookeeper如何保证一致3.1 ZAB协议的设计思路两种模式交替运行Zookeeper使用ZABZookeeper Atomic Broadcast原子广播协议来保证集群数据的强一致性。ZAB协议解决的是“一个Leader和多个Follower之间如何保持数据一致”的问题。ZAB把运行过程分成两个模式崩溃恢复模式Recovery集群刚启动或者Leader宕机后集群进入恢复模式。此时所有节点都会参与Leader选举选出一个新的Leader并且让所有Follower的数据同步到Leader的最新状态。消息广播模式Broadcast选举完成、数据同步完毕后集群进入广播模式。所有写请求都由Leader接收Leader给每个请求分配一个全局递增的ZXID然后把提案Proposal广播给所有Follower当过半节点确认收到后Leader再发送Commit消息所有节点执行提交。这里有个关键点ZAB不是简单的二阶段提交2PC。2PC是“所有参与者都准备好了才提交”ZAB是“过半节点确认就提交”。为什么要过半因为过半就能保证“新选出的Leader至少包含了一条已经提交的事务”从而避免数据丢失。这也是为什么Zookeeper在生产环境至少得部署3台容忍1台故障如果要容忍2台故障就得部署5台——集群规模不是拍脑袋定的是由过半原则决定的。3.2 Leader选举的细节myid、ZXID和逻辑时钟选举过程可以拆成四个阶段投票准备、发起投票、汇总决策、确认结果。每一步都有特定的目标下面我结合参数讲清楚。3.2.1 一阶段投票准备每个节点在选举开始时会先把自己当前的投票投给自己。投票的关键信息是(myid, ZXID)其中myid是节点在myid文件里配置的唯一整数ID初始选举阶段比较的是ZXID谁的数据新谁优先。ZXIDZooKeeper Transaction ID是一个64位的编号高32位是epoch时代编号每选出一轮新的Leaderepoch就会递增一次低32位是事务计数器。通过ZXID就能知道一个节点经历了哪些事务、数据是否最新。3.2.2 二阶段发起投票并接收他人投票每个节点会把包含自己(myid, ZXID)的投票消息广播给集群中所有其他节点。同时每个节点会持续接收其他节点的投票并累加统计。3.2.3 三阶段汇总决策每个节点在收到其他投票后会进行一次本地的逻辑比较判断要不要更新自己的投票。更新规则是优先比较ZXIDZXID大的优先如果ZXID相同则比较myidmyid大的优先。这个过程会反复进行每台节点都会拿着“当前自己认为最优的投票”继续广播。直到某个节点发现自己收到的投票中有超过半数节点的投票都指向同一个候选人优先比较ZXID与myid它就认为这个候选人可以当选Leader。3.2.4 四阶段确认Leader并切换状态节点确定Leader后会向该Leader发送确认消息并进入“跟随者”状态。Leader收到过半确认后会广播一条“NEW_LEADER”消息宣布自己正式成为Leader。所有节点在收到这条消息后会进行最后的元数据同步把Leader上已提交的事务同步到自己本地同步完成后集群退出崩溃恢复模式进入消息广播模式。这个过程听起来复杂但在实际环境中Zookeeper 3.4之后的默认选举算法Fast Leader Election用TCP连接和高效的投票轮次3台节点的集群在Leader宕机后通常能在几十到几百毫秒内完成选举和状态同步应用层几乎无感知。3.3 为什么过半节点确认能防止脑裂“脑裂”是指一个集群出现了两个“大脑”各自认为自己是Leader导致数据写入冲突。ZAB的过半机制正好从数学上消除了这个可能性。假设集群有5个节点如果一个分区分裂成了2个和3个的两个部分那么占多数的3个节点仍然能满足“过半”条件正常选举Leader而只有2个节点的分区因为凑不齐过半3/5就无法选举出合法的Leader也就不会出现“双主”的混乱局面。这就是为什么Zookeeper要求集群节点数为奇数——奇数的节点数可以让“过半”和“不超过一半”之间的边界更清晰。3.4 实操如何观察选举过程和集群状态在集群运行过程中可以通过Zookeeper的四字命令Four Letter Words来查看状态。比如用echo stat | nc zk_ip 2181查看节点的角色Leader还是Follower、连接数、ZXID等信息用echo mntr | nc zk_ip 2181查看监控指标包括znode_count、outstanding_requests、avg_latency等。我一般在排查集群故障时会先对每台节点执行mntr对比它们的zk_server_state和zk_last_processed_zxid快速判断哪台节点数据落后了。4. 企业级集群部署与参数调优4.1 集群规模怎么定3台还是5台部署Zookeeper集群第一个问题是规模。我见过不少团队图省事只部署2台结果Leader挂了之后剩下的1台永远凑不到过半集群直接不可用。还有个更严重的问题——2台节点网络分区时两边都无法满足过半条件整个集群变成“聋哑”状态。所以生产环境最低要求是3台。3台可以容忍1台故障5台可以容忍2台故障。具体选哪个取决于你对可用性的要求集群规模可容忍故障数适用场景3台1台大多数中小规模大数据平台5台2台核心生产集群要求较高可用性上层的Hadoop、Kafka集群通常会复用同一套Zookeeper但如果你有多个独立的大数据框架比如既跑HDFS又跑Kafka还跑HBase建议按框架拆分独立的Zookeeper集群避免一个集群上的波动影响所有组件。4.2 核心配置项逐一解读Zookeeper的配置文件是zoo.cfg在conf目录下。拿一份典型的生产配置来看tickTime2000 initLimit10 syncLimit5 dataDir/data/zookeeper/data dataLogDir/data/zookeeper/logs clientPort2181 maxClientCnxns300 autopurge.snapRetainCount5 autopurge.purgeInterval24 server.1zk1:2888:3888 server.2zk2:2888:3888 server.3zk3:2888:3888tickTime基础时间单元单位毫秒。Zookeeper里所有超时时间都是tickTime的整数倍。默认2000ms够用不要随便改小改小了会增大网络抖动导致的超时误判概率。initLimitFollower启动后能容忍最多多少个tickTime不同步Leader数据。10个tickTime20秒。如果集群规模大、快照文件多同步时间可能较长这时要适当调大。syncLimitFollower与Leader之间心跳的超时时间同样以tickTime计。默认5个tickTime10秒。如果网络延迟高建议稍微调大到10-15否则很容易出现“假死”误判。dataDir快照文件目录。注意——dataDir不是日志目录在Zookeeper里事务日志log默认也写在dataDir下面。生产环境建议单独设dataLogDir把事务日志放到独立的磁盘上。事务日志是顺序写的对IO延迟极其敏感如果和系统盘混在一起一旦磁盘IO打满整个Zookeeper的写入性能会明显抖动。clientPort客户端连接端口默认2181。server.1zk1:2888:3888声明集群节点。2888端口用于Leader和Follower之间的数据同步学习3888端口用于Leader选举时的投票通信。这两个端口不能被防火墙拦截。maxClientCnxns单台节点允许的最大客户端连接数默认60看起来有点小生产环境几十个客户端同时连接HDFS、Kafka、HBase都连着同一个集群很容易打满建议改到300以上。autopurge.snapRetainCount和autopurge.purgeInterval自动清理快照文件避免磁盘被历史快照占满。4.3 JVM与系统层调优Zookeeper是Java进程JVM堆内存设置很关键。多数情况下256MB到1GB就够用了ZNode存储的是轻量数据但如果你维护的是大规模Kafka集群上万个Broker或Partition元数据堆内存可以适当调到2GB-4GB。注意不要盲目给堆内存设太大因为Zookeeper的垃圾回收停顿会直接影响会话超时判断。一个典型的故障现象就是GC暂停时间过长导致节点被误判为宕机触发不必要的Leader切换。建议使用G1垃圾回收器并把MaxGCPauseMillis目标设置为200ms以下。启动脚本在bin/zkServer.sh里找到JVMFLAGS环境变量进行设置。比如export JVMFLAGS-Xmx2g -Xms2g -XX:UseG1GC -XX:MaxGCPauseMillis200系统层面还需要调整文件描述符限制。Zookeeper的连接数上来之后文件描述符不够会导致“Too many open files”错误。建议在/etc/security/limits.conf里把nofile设置为65535以上。另外Zookeeper节点和上层大数据组件之间的时钟偏差不能太大最好配置NTP时钟同步因为Zookeeper依赖租约和超时机制时钟漂移会引发会话错乱和选举异常。4.4 部署时最容易踩的几个坑第一myid文件写错。每个节点的dataDir下必须有一个myid文件内容是对应的server.N的编号比如server.1对应的节点myid内容就是1。如果编号和集群配置不一致节点之间根本组不成集群。第二防火墙没放通2888/3888端口。客户端连接2181没问题但节点之间相互发现不了一直处于“LOOKING”状态。第三单机混部时端口冲突。有些人喜欢把Zookeeper和大数据组件部署在同一台机器上如果同时部署多个实例要注意2888/3888不能重复还要额外设置quorumPort和electionPort。5. 与大数据生态的整合实战5.1 HDFS HANameNode自动故障转移的幕后推手HDFS高可用方案的标配是“两个NameNode JournalNode Zookeeper”。Active NameNode会在Zookeeper的/hadoop-ha/nameservice路径下创建临时节点ActiveBreadCrumb同时FailoverController进程会监听这个节点。正常的故障转移流程是这样的Active NameNode异常宕机它创建的临时节点因为会话超时而自动消失。Standby NameNode上的FailoverController通过Watch感知到临时节点消失。FailoverController触发SSH到Active NameNode执行fencing隔离操作防止它“假死”后继续写数据。Standby节点通过JournalNode同步最新元数据升级为Active并在Zookeeper上创建自己的临时节点。在实际整合过程中要注意hdfs-site.xml中的dfs.ha.automatic-failover.enabled必须设为true否则不会启用自动故障转移。同时core-site.xml里要配置ha.zookeeper.quorum指向Zookeeper集群的地址列表。5.2 KafkaController选举与Broker管理Kafka在2.8之前版本深度依赖Zookeeper。每个Broker启动时会在Zookeeper的/brokers/ids下注册一个临时节点节点数据包含Broker的IP、端口、机架信息。每个Consumer Group的offset也存储在Zookeeper的/consumers路径下新版已迁移到Kafka内部Topic但Broker协调依然依赖ZK。Kafka集群中的Controller负责分区Leader选举和分区分配也是通过Zookeeper竞选出来的。多个Broker同时尝试在/controller路径下创建临时节点谁创建成功谁就是Controller。当Controller宕机后临时节点消失其他Broker通过Watch感知到变化后再次竞争。这里有一个实际运维案例可以分享有一次Kafka集群频繁出现Controller already exists日志排查后发现是有两个Broker同时在抢Controller原因是Zookeeper的会话超时时间设置太短导致Controller所在的Broker频繁被Zookeeper判定为会话过期实际进程还活着形成了“假双主”。后来把session.timeout.ms从默认的6000ms调到10000ms配合Zookeeper的syncLimit调大问题就解决了。5.3 HBase协调层与元数据存储HBase是老牌Zookeeper重度用户。它会在Zookeeper上创建/hbase根节点下面挂载meta-region-server存放HBase元数据表Region的位置、rsRegionServer注册信息、masterHMaster选举节点。HMaster的选举逻辑和Kafka Controller类似多个HMaster节点竞争创建/hbase/master临时节点。RegionServer启动时会在/hbase/rs下创建临时节点并注册自身信息HMaster通过监听这些节点感知RegionServer的上下线。在HBase整合Zookeeper时我遇到过的一个高频问题是ZNode配额sessionTimeout冲突。HBase默认的zookeeper.session.timeout是90秒Zookeeper的minSessionTimeout默认是tickTime * 2 4000msmaxSessionTimeout默认是tickTime * 20 40000ms。如果HBase的session timeout设置超过40秒Zookeeper会自动把它钳制到40秒导致HBase以为会话还活着Zookeeper这边却已经超时删除了临时节点触发不必要的RegionServer下线。解决方案是在Zookeeper的zoo.cfg里显式调大maxSessionTimeout比如改成60000。5.4 Hadoop与Zookeeper整合配置示例以HDFS HA为例core-site.xml里的关键配置property nameha.zookeeper.quorum/name valuezk1:2181,zk2:2181,zk3:2181/value /propertyhdfs-site.xml里的关键配置property namedfs.ha.automatic-failover.enabled/name valuetrue/value /property property namedfs.namenode.shared.edits.dir/name valueqjournal://nn1:8485;nn2:8485;nn3:8485/mycluster/value /property然后在Hadoop的NameNode节点上执行初始化命令hdfs zkfc -formatZK这条命令会在Zookeeper上创建/hadoop-ha路径并初始化HA状态。配置完成后分别启动Zookeeper集群、JournalNode、两个NameNode最后启动FailoverControllerZKFC高可用体系就生效了。验证方式很简单Kill掉Active NameNode进程几秒内Standby节点会自动切换成Active。6. 常见问题与排查技巧实录6.1 高频故障速查表故障现象可能原因排查思路集群一直处于LOOKING状态选举不成功节点间2888/3888端口不通myid配置不一致防火墙拦截先用telnet测试端口连通性检查每台myid文件内容看zookeeper.out日志客户端大量ConnectionLossLeader频繁切换网络抖动服务器负载过高执行mntr查看zk_server_state检查是否频繁发生选举看GC日志临时节点不自动消失客户端会话没超时网络延迟导致心跳仍能续期检查sessionTimeout设置查看zk_session相关监控上层组件频繁发生主备切换Zookeeper所在机器负载过高导致JVM GC停顿检查CPU/GC指标适当调大会话超时时间考虑独立部署ZookeeperToo many open files文件描述符限制过低调大nofile重启Zookeeper进程磁盘空间被快照占满没有开启自动清理设置autopurge.snapRetainCount和autopurge.purgeInterval6.2 排查“三步法”与实战案例遇到Zookeeper相关的线上问题时我习惯按“日志 → 网络 → 资源”三步走。第一步看日志。Zookeeper的日志文件在启动时输出到zookeeper.out或logs/目录里面有大量的连接、选举、快照加载记录。重点关注WARN和ERROR级别的信息比如Exception when following the leader、Unexpected exception causing shutdown等。第二步测网络。用ping测试节点间延迟用telnet测试2181/2888/3888端口的连通性用ss -tn查看连接状态。Zookeeper对网络抖动非常敏感偶尔一次的5秒延迟就可能导致会话超时触发重连。第三步看资源。用top看CPU和内存用dmesg看是否有OOM、IO错误。我遇到过一次“所有客户端都报ConnectionLoss但Zookeeper进程明明活着”的故障最后排查发现是磁盘IO 100%因为同一台机器上还跑了YARN的NodeManager日志把磁盘打满了。6.3 一个值得警惕的经验别把Zookeeper和大数据组件混部很多团队为了节省机器把Zookeeper和Kafka Broker、HDFS DataNode混部在同一批节点上。这样做短时间没问题但一旦某个组件出现IO风暴或内存压力会立刻传导到Zookeeper导致整个平台抖动。我经历过的真实案例是某节点上的Kafka Broker因为磁盘故障导致大量IO等待Zookeeper的Follower在这个节点上无法及时处理心跳被Leader判定为超时移除出集群。本该是Kafka单点故障结果波及了全平台的协调层HDFS那边也跟着踢了一脚把Active NameNode也给切了。所以我的建议是生产环境的核心Zookeeper集群尽量独立部署至少也要保证它的数据盘、日志盘和其他组件物理隔离。如果预算实在紧张可以把Zookeeper部署在边缘节点或master节点上但必须做好资源隔离cgroup、独立磁盘。6.4 会话超时时间的设置经验关于session timeout我给一个可参考的经验公式如果网络质量好同机房延迟低于1msZookeeper的syncLimit保持5上层组件的session.timeout设置为10秒到20秒之间。如果跨机房或网络波动大把syncLimit调大到10-15上层的session timeout也对应调到30秒以上。但注意session timeout不是越大越好。太大会导致故障感知延迟——比如Active NameNode挂了如果ZKFC超时时间设太长主备切换的时间就会被拉长业务影响变大。这块需要根据你对“故障切换时间”的要求来权衡。在调整完这些参数之后一定要做一次故障演练主动kill掉一个节点或直接断网观察Zookeeper的选举耗时和上层组件的切换时间确认参数设置没有让故障恢复变得不可控。写在最后的一些经验在大数据平台里待久了你会慢慢发现Zookeeper这玩意儿就像一个“定海神针”平时存在感很低但一旦出问题就是大事。我自己踩过不少坑也围观过不少事故最后沉淀下来的核心经验就几条Zookeeper能独立就独立别和业务应用混部tickTime、syncLimit、session timeout这些参数要成体系地去调别只看单点Watch机制确实方便但别过度依赖它关键状态一定要配合轮询兜底每次变更集群节点或升级版本之前先备份dataDir再搞一套小的验证环境试一遍。如果你正在准备大数据相关的面试Zookeeper这一块经常会问到Leader选举细节、ZAB协议与2PC的区别、Watch机制的特性、为什么过半节点就能保证一致还有Kafka/HDFS/HBase各自如何使用Zookeeper。把这些原理吃透了不管面试官怎么变着花样问你都能把底层逻辑讲清楚。希望这篇能帮你省下不少自己摸索的时间。