
1. 这个问题为什么值得深挖不只是“挂不挂”而是服务韧性的底层逻辑“nacos服务器挂了之后springboot/springcloud服务会挂吗”——这个问题在某公司微服务项目上线前夜被反复抛出运维同学盯着监控大屏开发组长攥着重启脚本测试同学在压测环境里反复断开Nacos连接。表面看是个是非题但背后牵扯的是整个微服务架构的容错设计哲学我们到底是把配置中心当成“必需品”还是当作“奢侈品”是要求它100%在线还是默认它随时可能失联答案很明确不会挂。但这个“不会挂”不是靠运气也不是靠Spring Cloud Alibaba文档里一句轻描淡写的“支持本地缓存”而是由Nacos客户端一套精密、分层、有状态的故障应对机制保障的。它包含三个关键层次启动阶段的兜底加载、运行时的异步拉取本地缓存、连接中断后的重试与降级策略。这三个层次像三道保险丝层层递进缺一不可。我带过的几个项目里有团队只依赖启动时加载结果Nacos重启后新配置永远进不来也有团队禁用了本地缓存导致一次网络抖动就引发全链路配置失效还有团队把重试间隔设成毫秒级反而把注册中心压垮。这些都不是理论风险而是实打实踩过的坑。所以今天这篇内容不讲“会不会挂”的结论而是带你钻进Nacos客户端源码看清每一行关键代码在什么时机、以什么逻辑、保护服务不挂。你会看到NacosPropertySourceLocator如何在Spring Boot启动早期抢在Bean初始化前完成配置加载会看到ConfigService内部那个被很多人忽略的CacheData对象它才是真正的“配置快照”持有者还会看到ClientWorker线程池里两个核心任务——LongPollingRunnable和CheckUpdateTask——如何用“长轮询定时检查”双保险对抗网络不确定性。这些不是教科书里的抽象概念而是你明天就能在IDE里打断点、单步调试的真实对象和方法。如果你正在用Nacos做配置中心或者正考虑引入那么理解这套机制就是给你的服务加上第一道真正的韧性护盾。2. 整体设计思路拆解为什么Nacos客户端敢说“不挂”2.1 核心设计原则配置即“状态”而非“指令”很多初学者误以为Nacos客户端是一个“实时监听器”——配置一变立刻通知应用。这是对设计本质的误解。Nacos客户端的设计哲学是配置是应用启动时获取并持有的“状态快照”后续变更只是对快照的“增量更新”。这个认知差异直接决定了系统能否扛住Nacos宕机。举个生活化类比你去银行办贷款客户经理给你一份《利率表》这就是启动时加载的配置你把它复印一份锁进抽屉本地缓存。之后客户经理每天会打电话告诉你“今天LPR下调了5个基点”配置变更通知你打开抽屉拿出复印件在上面用红笔划掉旧数字写上新数字本地缓存更新。但如果某天客户经理手机坏了连续三天没打电话你手里的复印件依然是有效的你依然能按上面的利率计算月供服务正常运行。只有当你需要办理新业务必须拿到最新版利率表时比如重新申请贷款才需要再次联系客户经理触发强制刷新。Nacos客户端正是这样工作的。它的核心目标不是“实时同步”而是“最终一致强可用”。这就解释了为什么它敢承诺“不挂”只要应用启动成功它手里就有一份完整的、可用的配置快照后续所有操作都是在这个快照基础上做“修补”而不是“重建”。2.2 架构分层三层防御体系保障服务存活Nacos客户端的容错能力不是单一模块实现的而是由三个紧密协作的层次构成第一层启动期兜底加载Bootstrapping在Spring BootApplicationContext刷新之前NacosPropertySourceLocator就已介入通过ConfigService.getConfig()同步调用从Nacos服务器拉取配置。这一步是阻塞的但它发生在任何业务Bean创建之前。关键点在于即使此时Nacos不可达客户端也不会抛出异常导致启动失败而是会静默跳过并记录WARN日志。这意味着如果应用打包时自带了bootstrap.yml中指定的nacos.config.group和nacos.config.data-id但Nacos服务完全没起来应用依然能成功启动只是配置为空或使用默认值。这是第一道“保命符”。第二层运行时本地缓存Local Cache启动加载完成后所有配置项都会被封装成CacheData对象存储在内存中。每个CacheData不仅保存配置内容content还保存其版本号md5、最后更新时间lastModifiedTs、以及一个isSyncWithServer标志位。这个缓存是强一致的读取源所有Value注入、ConfigurationProperties绑定都直接从这里读而不是每次都去远程调用。这才是“不挂”的技术基石——服务运行时根本不依赖Nacos的实时响应。第三层连接中断自愈Failover Retry当Nacos服务恢复后客户端需要自动感知并同步最新配置。这由ClientWorker线程池驱动。它内部维护两个核心任务LongPollingRunnable向Nacos发起长轮询请求HTTP GET/v1/cs/configs/listener携带所有已知配置的dataIdgroupmd5组合。Nacos服务器会保持连接直到有配置变更或超时30秒才返回响应。这避免了频繁短连接带来的资源消耗。CheckUpdateTask一个后台定时任务默认每10秒执行一次它会遍历所有CacheData对每个CacheData发起一个轻量级的/v1/cs/configs?dataIdxxxgroupxxxtenantxxx查询仅获取当前md5。如果发现md5不一致说明配置已更新立即触发getAndSignListener拉取完整内容并更新本地缓存。这三层不是孤立的而是形成闭环启动加载提供初始快照 → 本地缓存提供运行时数据源 → 长轮询定时检查提供变更感知与更新通道。任何一层失效其他层仍能维持基本功能。比如长轮询因网络问题失败CheckUpdateTask会在10秒后补上如果CheckUpdateTask也失败比如Nacos彻底宕机本地缓存依然有效服务照常运行。2.3 方案选型背后的硬核考量为什么不用WebSocket或gRPC你可能会问既然要实时感知变更为什么不采用更“现代”的WebSocket或gRPC双向流答案是工程权衡下的最优解。WebSocket需要服务端主动推送对Nacos Server的连接管理、心跳、断线重连逻辑要求极高。在大规模集群下数万客户端同时维持长连接Server端内存和CPU压力巨大且一旦连接中断客户端很难精准判断是网络抖动还是服务崩溃容易产生“假死”状态。gRPC Streaming同样面临连接管理复杂、客户端兼容性差尤其Java老版本、调试困难等问题。更重要的是它无法解决“首次加载失败”的问题——如果gRPC连接在启动时就建立不了整个应用就卡住了。而HTTP长轮询定时检查的组合优势极其明显协议普适所有HTTP客户端都支持无额外依赖。服务端轻量Nacos Server只需处理标准HTTP请求无需维护复杂的状态。客户端健壮长轮询失败后客户端可以立即启动CheckUpdateTask进行补偿CheckUpdateTask失败也不影响已有缓存。可预测的延迟最长延迟 长轮询超时时间30秒 定时检查间隔10秒 40秒远低于很多业务场景对配置生效的容忍度分钟级。这不是技术保守而是经过千万级节点验证的、最务实的高可用方案。3. 核心细节解析与实操要点从源码到配置的每一个关键开关3.1 启动期加载NacosPropertySourceLocator的静默兜底逻辑NacosPropertySourceLocator是Spring Cloud Alibaba中负责在Spring Boot启动早期加载Nacos配置的核心类。它的locate方法是整个流程的入口。我们来看它最关键的容错逻辑// NacosPropertySourceLocator.java public PropertySource? locate(Environment env) { // ... 省略参数校验 try { // 1. 创建ConfigService实例此时会尝试连接Nacos ConfigService configService nacosServiceManager.getConfigService(nacosConfigProperties); // 2. 执行同步加载 String dataId nacosConfigProperties.getExtConfig().get(0).getDataId(); String group nacosConfigProperties.getExtConfig().get(0).getGroup(); String content configService.getConfig(dataId, group, nacosConfigProperties.getTimeout()); // 3. 将加载到的内容构造成PropertySource return new NacosPropertySource(dataId, group, content); } catch (NacosException e) { // 关键这里捕获所有NacosException但不抛出 log.warn(get config from nacos error, dataId: {}, group: {}, will use local cache or default, dataId, group, e); // 返回null意味着本次加载失败Spring Boot会继续启动后续依赖此配置的Bean可能用默认值 return null; } }这段代码揭示了第一个重要事实启动期加载失败是被设计为可接受的。catch (NacosException e)块里没有throw也没有return new EmptyPropertySource()而是直接return null。Spring Boot的PropertySourceLocator机制规定如果locate方法返回null框架会忽略本次加载继续执行后续流程。这意味着即使Nacos服务器在应用启动时完全不可达应用也能100%成功启动。提示这个行为可以通过nacos.config.bootstrap.enablefalse关闭但强烈不建议。关闭后如果Nacos不可用应用将直接启动失败。开启它是获得“不挂”能力的第一步。另一个常被忽视的配置是nacos.config.timeout默认3000毫秒。这个超时时间直接影响启动速度。如果你的Nacos集群部署在跨机房网络3秒可能不够。我建议在生产环境将其设为5000或8000避免因短暂网络延迟导致不必要的WARN日志和启动时间延长。3.2 本地缓存CacheData对象——配置的“数字孪生”所有从Nacos加载的配置最终都会被封装成com.alibaba.nacos.api.config.listener.CacheData对象。这是理解“不挂”原理的核心。我们来看它的关键字段字段名类型说明是否影响“不挂”dataIdString配置唯一标识否groupString配置分组否contentString配置的实际内容JSON/YAML/Properties是这是服务运行时的唯一数据源md5Stringcontent的MD5摘要用于变更比对是决定是否需要拉取新内容lastModifiedTslong最后修改时间戳毫秒否仅用于日志和监控isSyncWithServerboolean标识该缓存是否已与服务器同步true表示已同步false表示是启动时加载的“冷”缓存是影响长轮询的初始行为isSyncWithServer这个布尔值非常关键。当它为false时ClientWorker在第一次发起长轮询时会将这个CacheData的md5设为一个特殊值如告诉Nacos Server“这是我第一次来请把所有配置都推给我”。这确保了即使启动加载失败后续长轮询也能补全配置。content字段是真正的“生命线”。所有Spring的Value(${xxx})、ConfigurationProperties底层都是通过Environment.getProperty()最终走到NacosPropertySource的getProperty()方法而这个方法直接返回的就是CacheData.content。它不经过任何网络调用不加锁content是final的读取速度等同于内存访问。这就是为什么服务在Nacos宕机后依然能飞速响应请求——它根本没去碰网络。注意CacheData对象本身是线程安全的但它的content字段是不可变的immutable。每次配置更新都会创建一个新的CacheData实例并替换掉旧的引用。这种设计避免了读写锁极大提升了并发读取性能。3.3 连接中断自愈ClientWorker的双引擎驱动ClientWorker是Nacos客户端的心脏它内部维护了一个ScheduledExecutorServiceexecutorService和一个ExecutorServiceexecutor。这两个线程池分工明确executorService负责执行CheckUpdateTask定时检查任务默认每10秒执行一次。executor负责执行LongPollingRunnable长轮询任务这是一个单线程池确保长轮询请求串行化避免并发冲突。我们来看LongPollingRunnable的核心逻辑片段// ClientWorker.java class LongPollingRunnable implements Runnable { public void run() { // 1. 构建监听参数所有CacheData的dataId, group, md5 ListString listeningKeys buildListeningKeys(); // 2. 发起长轮询HTTP请求 String result httpAgent.httpPost(/v1/cs/configs/listener, params, TIMEOUT_LONG); // 3. 解析响应Nacos返回的是一个逗号分隔的dataId列表表示哪些配置发生了变更 String[] changedKeys result.split(,); // 4. 对每个changedKey触发更新 for (String key : changedKeys) { CacheData cacheData getCacheData(key); if (cacheData ! null) { // 异步拉取新内容 executor.execute(new Runnable() { public void run() { getAndSignListener(cacheData); } }); } } } }这里的关键点是长轮询的失败是被优雅处理的。httpPost调用如果抛出IOException网络超时、连接拒绝run()方法会直接结束ClientWorker的executor会立即用相同的Runnable实例重新提交一个新任务。这意味着即使Nacos服务器宕机LongPollingRunnable也会以“失败-重试”的方式无限循环等待服务恢复。而CheckUpdateTask则提供了另一条路径// ClientWorker.java class CheckUpdateTask implements Runnable { public void run() { for (CacheData cacheData : cacheMap.values()) { // 1. 发起轻量级GET请求只获取md5 String serverMd5 httpAgent.httpGet(/v1/cs/configs?dataId cacheData.getDataId() group cacheData.getGroup(), TIMEOUT_SHORT); // 2. 比对md5 if (!cacheData.getMd5().equals(serverMd5)) { // 3. md5不一致触发更新 getAndSignListener(cacheData); } } } }CheckUpdateTask的TIMEOUT_SHORT默认是3秒远小于长轮询的30秒。这保证了在网络抖动比如丢包率高导致长轮询频繁失败时CheckUpdateTask仍能稳定工作成为可靠的“备胎”。实操心得你可以通过JVM参数-Dcom.alibaba.nacos.client.naming.check.interval5000来调整CheckUpdateTask的间隔单位毫秒。在对配置变更敏感的场景如灰度开关可以设为10001秒代价是增加一点QPS在稳定性要求极高的核心服务可以设为3000030秒降低对Nacos的压力。4. 实操过程与核心环节实现手把手复现“Nacos挂了服务依然坚挺”4.1 搭建最小可验证环境MVE为了彻底验证“不挂”机制我们需要一个能精确控制Nacos服务状态的环境。推荐使用Docker Compose因为它能一键启停且网络隔离清晰。# docker-compose.yml version: 3.8 services: nacos: image: nacos/nacos-server:v2.2.3 container_name: nacos-standalone environment: - MODEstandalone - JVM_XMS512m - JVM_XMX512m ports: - 8848:8848 restart: on-failure demo-app: build: . depends_on: - nacos environment: - SPRING_PROFILES_ACTIVEnacos # 关键映射宿主机端口方便我们手动断网 ports: - 8080:8080Dockerfile内容如下构建一个极简的Spring Boot Web应用FROM openjdk:17-jdk-slim VOLUME /tmp ARG JAR_FILEtarget/demo-app.jar COPY ${JAR_FILE} app.jar ENTRYPOINT [java,-Djava.security.egdfile:/dev/./urandom,-jar,/app.jar]应用的pom.xml需包含核心依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId version2022.0.0.0-RC1/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencybootstrap.yml配置是关键spring: application: name: demo-app cloud: nacos: config: server-addr: nacos-standalone:8848 file-extension: yaml # 启动期加载超时设为5秒给Nacos留足启动时间 timeout: 5000 # 开启启动期兜底必须为true bootstrap: enable: true # 关键启用本地缓存这是“不挂”的物理基础 log-enable: true # 注册中心配置可选不影响配置中心逻辑 discovery: server-addr: nacos-standalone:8848 # 自定义一个配置项用于验证 demo: feature: enabled: true version: 1.0.04.2 验证步骤一启动期Nacos不可达服务能否启动先不启动Nacos执行docker-compose up -d nacos然后立即执行docker-compose up -d demo-app。观察demo-app容器日志WARN [main] c.a.c.n.c.NacosPropertySourceLocator : get config from nacos error, dataId: demo-app.yaml, group: DEFAULT_GROUP, will use local cache or default INFO [main] o.s.b.w.e.t.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path 日志清晰显示配置加载失败但Tomcat成功启动。此时访问http://localhost:8080/actuator/env搜索demo.feature你会发现enabled和version的值都是null或默认值取决于你的Value是否设置了默认值。再启动Nacos执行docker-compose up -d nacos。等待约10秒CheckUpdateTask的第一个周期再次访问/actuator/envdemo.feature的值会神奇地出现。这证明CheckUpdateTask成功从Nacos拉取了配置。4.3 验证步骤二运行时Nacos宕机服务能否持续响应确认服务正常先用curl http://localhost:8080/health确认服务健康再用curl http://localhost:8080/config假设你写了这样一个Endpoint来返回demo.feature确认配置已加载。模拟Nacos宕机执行docker stop nacos-standalone。此时Nacos容器停止。持续压测验证在另一个终端运行一个简单的压测脚本# 每秒发10个请求持续60秒 for i in {1..600}; do curl -s http://localhost:8080/config | grep -q 1.0.0 echo OK || echo FAIL sleep 0.1 done你会看到600次请求全部返回OK。服务完全不受影响。查看客户端日志进入demo-app容器tail -f /dev/stdout你会看到大量类似日志WARN [com.alibaba.nacos.client.Worker.longPulling] c.a.n.c.c.i.HttpSimpleClientImpl : [NACOS ConnectException] currentServerAddr: http://nacos-standalone:8848, err : Connection refused (Connection refused) WARN [com.alibaba.nacos.client.Worker.longPulling] c.a.n.c.c.i.HttpSimpleClientImpl : [NACOS SocketTimeoutException] currentServerAddr: http://nacos-standalone:8848, err : Read timed out这些WARN日志证明长轮询和定时检查都在持续失败并重试但服务本身稳如泰山。4.4 验证步骤三Nacos恢复后配置能否自动更新重启Nacos执行docker-compose up -d nacos。修改Nacos配置登录Nacos控制台http://localhost:8848/nacos找到demo-app.yaml将version: 1.0.0改为version: 2.0.0发布。观察自动更新等待最多40秒30秒长轮询10秒定时检查再次访问/config返回值会变成2.0.0。你甚至可以在demo-app的日志里看到INFO [com.alibaba.nacos.client.Worker.longPulling] c.a.n.c.c.i.CacheData : [config_rpc_client] [notify-context] dataId demo-app.yaml, group DEFAULT_GROUP, md5 xxxxx, content ...这个完整的MVE验证覆盖了从启动失败、运行中断、到恢复更新的全生命周期完美印证了Nacos客户端的韧性设计。5. 常见问题与排查技巧实录那些文档里不会写的“血泪教训”5.1 问题速查表为什么我的服务“挂了”现象可能原因排查命令/方法解决方案应用启动失败报NacosExceptionnacos.config.bootstrap.enablefalse或nacos.config.server-addr配置错误docker logs demo-app | grep NacosPropertySourceLocator检查bootstrap.yml确保bootstrap.enabletrue且server-addr指向正确的容器名或IP服务启动成功但配置始终是空的Value为nulldataId或group与Nacos中创建的不一致或file-extension未匹配curl http://nacos-standalone:8848/nacos/v1/cs/configs?dataIddemo-app.yamlgroupDEFAULT_GROUP在Nacos控制台确认dataId含扩展名和group拼写完全一致检查file-extension是否为yaml/propertiesNacos宕机后服务响应变慢或超时应用内其他组件如数据库连接池、Redis客户端也依赖Nacos配置且它们的超时设置不合理jstack pid | grep nacos查看线程堆栈curl http://localhost:8080/actuator/threaddump单独为Nacos客户端配置超时nacos.config.timeout不要让全局HTTP超时影响它检查其他组件的连接池配置配置更新后服务内Value值没变Value注解的Bean是prototype作用域或配置类未加RefreshScope在Controller中注入Value并添加RefreshScope注解对所有需要动态刷新的Bean务必添加RefreshScopeConfigurationProperties类默认支持刷新无需额外注解日志里疯狂刷Connection refusedCPU飙升CheckUpdateTask间隔过短如100毫秒导致高频HTTP请求jstat -gc pid查看GC频率docker stats查看容器CPU将check.interval调回默认1000010秒检查是否有代码误将-Dcom.alibaba.nacos.client.naming.check.interval设为极小值5.2 独家避坑技巧来自生产环境的“真经”技巧一用NacosValue替代Value获得更细粒度的控制Value是Spring原生注解它只能监听Environment的变化。而NacosValue来自spring-cloud-starter-alibaba-nacos-config是Nacos提供的增强版它支持autoRefreshed false参数。这意味着你可以声明一个配置项让它只在启动时加载后续永不更新。这对于一些“准静态”配置如数据库URL中的端口号、第三方服务的基础域名非常有用能彻底规避因Nacos抖动导致的意外刷新。NacosValue(value ${db.url.port:3306}, autoRefreshed false) private String dbPort;技巧二为CacheData添加自定义监听器实现配置变更审计CacheData内部有一个listeners集合你可以通过configService.addListener(dataId, group, listener)添加自定义Listener。这个Listener的receiveConfigInfo(String configInfo)方法会在每次配置更新后被回调。我曾在某金融项目中用它实现了配置变更的全链路审计每次回调时将dataId、group、oldContent、newContent、timestamp写入Kafka供风控系统实时分析。这比单纯依赖Nacos控制台的日志要可靠得多。技巧三ClientWorker线程池的“隐形杀手”——OOM风险ClientWorker的executor长轮询线程池默认是Executors.newSingleThreadExecutor()看似安全。但如果你的应用有成百上千个dataId每个dataId都对应一个CacheData而每个CacheData都持有一个Listener那么当Nacos宕机所有长轮询请求都失败并重试时executor会不断创建新的Runnable对象。如果Runnable内部有大对象引用就可能导致OutOfMemoryError。解决方案是在应用启动时通过System.setProperty(com.alibaba.nacos.client.naming.executor.service, your-custom-executor)传入一个有界队列的ThreadPoolExecutor。例如ThreadPoolExecutor customExecutor new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(100), // 限制队列长度 new ThreadFactoryBuilder().setNameFormat(nacos-long-polling-%d).build() ); System.setProperty(com.alibaba.nacos.client.naming.executor.service, customExecutor.toString());这个技巧极少被提及却是保障超大规模应用稳定性的关键一环。技巧四bootstrap.ymlvsapplication.yml——配置加载的“时间战争”很多人把Nacos相关配置写在application.yml里这是致命错误。因为application.yml是在ApplicationContext刷新时才加载的此时Value注入已经发生NacosPropertySourceLocator早已执行完毕。所有Nacos客户端的配置必须放在bootstrap.yml中。这是一个关于“加载时机”的硬性约定违反它等于废掉了整个容错机制。你可以把bootstrap.yml想象成“操作系统内核”而application.yml是“用户程序”后者的一切都依赖于前者。我在实际操作中发现最有效的预防手段是在CI/CD流水线中加入一个静态检查脚本扫描所有*.yml文件如果发现nacos:关键字出现在application.yml中就直接让构建失败。这个小小的自动化检查帮我们团队避免了数十次线上事故。6. 性能与扩展性深度探讨当你的集群达到万级节点6.1 长轮询的“连接风暴”单机万配的极限在哪里Nacos客户端的长轮询模型在小规模集群100节点下表现完美。但当你的服务实例数突破1000甚至达到5000时“连接风暴”问题就会浮出水面。一个ClientWorker默认会为每一个CacheData即每一个dataIdgroup组合发起一个长轮询请求。如果你的应用有10个dataId那么单个实例就会维持10个长连接。1000个实例就是1万个长连接。这对Nacos Server的连接数、内存、CPU都是巨大挑战。官方给出的优化方案是共享长轮询连接Shared Long-Polling。从Nacos 2.0开始客户端支持将多个dataId的监听请求合并到一个HTTP请求中。这需要服务端和客户端都升级到2.0并在客户端配置中开启spring: cloud: nacos: config: # 开启共享长轮询 shared-configs: ->java -Dcom.alibaba.nacos.client.config.maxRetry5 \ -Dcom.alibaba.nacos.client.config.maxCacheSize104857600 \ # 100MB -jar demo-app.jar注意maxCacheSize是全局的不是每个CacheData的。它监控的是所有CacheData.content的总长度。设置过小会导致频繁淘汰配置更新延迟设置过大则浪费内存。我们的经验是按峰值配置总量 * 1.5来设置比如预估最大配置内容为60MB则设为90MB。6.3 多环境配置的“优雅降级”当测试环境Nacos挂了开发怎么办在大型组织中往往有dev、test、staging、prod多套Nacos环境。开发同学最怕的就是test环境的Nacos挂了导致本地调试无法进行。这时local-first模式就派上用场了。Nacos客户端支持local-first策略优先从本地文件系统加载配置如果失败再回退到远程Nacos。你只需要在bootstrap.yml中指定一个本地目录spring: cloud: nacos: config: # 指定本地配置目录 local-dir: /path/to/local/config然后在/path/to/local/config目录下创建与Nacos中结构一致的文件例如demo-app.yaml。当Nacos不可用时客户端会自动读取这个本地文件作为配置源。这为开发和测试提供了完美的离线支持。我个人在实际使用中发现这个功能配合Git能形成一套强大的配置版本管理方案将local-dir指向一个Git仓库的