Nacos本地缓存配置实践

2023年 10月 7日 141.2k 0

背景

前段时间做了一个项目,由于nacos的不稳定性,导致了生产环境拉取配置失败了,从而影响了生产环境的业务。

于是团队就做了一个大胆的决定,为了避免因为依赖nacos导致业务的不可用,我们一致决定,在本地做nacos的配置缓存。本篇文章只讨论nacos配置缓存的实践,不涉及注册中心。

经过几轮测试和验证,最终这个方案落地了,做了实际的故障演练,把nacos断了之后,应用是能正确的拿到缓存的配置。

下面我们就来看看如何实现的。

缓存配置的步骤

实现本地缓存配置数据的步骤,我这里做了几个具体步骤的总结:

  • 首先,有一个Nacos服务器,这个是必须的,并且已经创建了相应的配置。
  • 其次,在程序中引入Nacos的客户端SDK依赖,这个也是必须的。
  • 然后使用SDK从Nacos服务器获取配置数据,这个也是必须的。
  • 重点步骤来了,我的实现方式是,将从nacos服务端获取到的配置数据保存在本地缓存中,可以使用内存、文件或其他缓存机制。
  • 然后在需要访问配置数据的地方,首先检查本地缓存是否存在数据。如果存在,直接使用缓存数据;如果不存在,再从Nacos服务器获取最新的配置数据。
  • 最后就是要定期刷新本地缓存,以确保获取到的配置数据是最新的。
  • 这只是其中一种实现方式,还有主动轮训的方式,这里就不讲了。

    本次实践不涉及强实时要求的配置的更新。

    测试代码

    先用java代码测试一下可行性,试一下在本地缓存配置数据:

    import com.alibaba.nacos.api.config.ConfigService;
    import com.alibaba.nacos.api.config.listener.Listener;
    import com.alibaba.nacos.api.exception.NacosException;
    import com.alibaba.nacos.api.utils.StringUtils;
    
    import java.util.Properties;
    import java.util.concurrent.Executor;
    
    public class NacosConfigCacheExample {
        private static final String SERVER_ADDR = "localhost:8848";
        private static final String GROUP_ID = "DEFAULT_GROUP";
        private static final String DATA_ID = "example-config";
        private static final String CACHE_FILE_PATH = "/path/to/cache/file";
    
        private static ConfigService configService;
    
        public static void main(String[] args) throws NacosException {
            // 创建Nacos配置服务实例
            Properties properties = new Properties();
            properties.put("serverAddr", SERVER_ADDR);
            configService = NacosFactory.createConfigService(properties);
    
            // 从Nacos服务器获取配置数据
            String configData = configService.getConfig(DATA_ID, GROUP_ID, 5000);
    
            // 将配置数据保存到本地缓存文件
            saveConfigToCache(configData);
    
            // 注册配置变更监听器
            configService.addListener(DATA_ID, GROUP_ID, new Listener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    // 当配置发生变化时,更新本地缓存
                    saveConfigToCache(configInfo);
                }
    
                @Override
                public Executor getExecutor() {
                    return null; // 使用默认的执行器
                }
            });
    
            // 从本地缓存获取配置数据
            String cachedConfigData = readConfigFromCache();
            if (StringUtils.isNotBlank(cachedConfigData)) {
                // 使用缓存数据
                System.out.println("Using cached config data: " + cachedConfigData);
            } else {
                // 缓存数据为空,从Nacos服务器获取最新的配置数据
                String latestConfigData = configService.getConfig(DATA_ID, GROUP_ID, 5000);
                System.out.println("Using latest config data: " + latestConfigData);
            }
        }
    
        private static void saveConfigToCache(String configData) {
            // 将配置数据保存到本地缓存文件
            // 这里使用了简单的文件存储方式,你可以根据实际需求选择其他缓存机制
            try (FileWriter writer = new FileWriter(CACHE_FILE_PATH)) {
                writer.write(configData);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static String readConfigFromCache() {
            // 从本地缓存文件读取配置数据
            // 这里使用了简单的文件存储方式,你可以根据实际需求选择其他缓存机制
            try (BufferedReader reader = new BufferedReader(new FileReader(CACHE_FILE_PATH))) {
                StringBuilder configData = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    configData.append(line);
                }
                return configData.toString();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    跑了一下,是可以成功拿到数据,并缓存到文件中的。同时在缓存数据为空的时候,是可以从nacos读取最新的数据的。

    那么下面就在正式的spring项目中开干。

    Spring Boot代码DEMO

    项目是用的Spring Boot,那么在项目中引入nacos这种简单的操作就浅写一下吧。

    
        com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-config
    
    

    然后,在配置文件中添加Nacos相关的配置这种简单的操作也浅写一下吧。

    spring:
      cloud:
        nacos:
          config:
            server-addr: localhost:8848
            group: DEFAULT_GROUP
            namespace: your-namespace
    

    接下来,创建一个配置类,用于获取和缓存配置数据:

    import com.alibaba.nacos.api.config.annotation.NacosConfigListener;
    import com.alibaba.nacos.api.config.annotation.NacosValue;
    import org.springframework.stereotype.Component;
    
    import java.io.*;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class ConfigCache {
        private static final String CACHE_FILE_PATH = "/path/to/cache/file";
    
        private Map configDataMap = new HashMap();
    
        public String getConfigData(String dataId) {
            String configData = configDataMap.get(dataId);
            if (configData == null || configData.isEmpty()) {
                configData = readConfigFromCache(dataId);
            }
            return configData;
        }
    
        @NacosConfigListener(dataId = "example-config", groupId = "DEFAULT_GROUP")
        public void onConfigUpdate(String config, String dataId) {
            configDataMap.put(dataId, config);
            saveConfigToCache(config, dataId);
        }
    
        private void saveConfigToCache(String configData, String dataId) {
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(CACHE_FILE_PATH + dataId))) {
                writer.write(configData);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private String readConfigFromCache(String dataId) {
            try (BufferedReader reader = new BufferedReader(new FileReader(CACHE_FILE_PATH + dataId))) {
                StringBuilder configData = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    configData.append(line);
                }
                return configData.toString();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    这里配置不多的话,可以用一个map来存储。可以加快获取的速度。

    当map中没有的时候,再去文件中读取缓存的配置。这里展示的是用文件存储的。

    实际的项目中,最终是替换成了redis来存储的。

    文件存储的问题在于是,配置很多的话,会产生很多个文件。如果写一个文件的话,当配置很多的时候IO效率又很慢。

    用map存在内存中的问题是,如果配置很多,会占用很多内存。但好处是它查询很快。

    最后,在业务代码中可以使用ConfigCache类来获取配置数据:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class ExampleController {
        @Autowired
        private ConfigCache configCache;
    
        @GetMapping("/config")
        public String getConfigData() {
            return configCache.getConfigData("example-config");
        }
    }
    

    总结一下里面的坑

    实际测试下来,最终发现,不同的项目会用不同的缓存。

    只用MAP

    在配置很少的那种服务里,比如只有几个或者十几个,就简单的用map缓存在内存中就可以了。

    • 但它的问题是,如果是服务本身挂了,那么重启后,这些map中的数据就丢失了。
    • 另外一个问题是,服务有多个实例的时候,每个实例中缓存的map是不一样的。当正好没有缓存的实例去访问nacos拿数据的时候,nacos正好挂了,那么一样会失败。虽然概率很低。

    用MAP+文件

    对于配置多,也不在乎读取效率的服务里,就用MAP+文件。其实我们的项目中最终只有一个服务用了这种模式。

    • 它的问题在于,配置很多的服务中,随着服务的使用,MAP占用的内存会越来越大。
    • 用文件存储的问题在于,它有一定的概率会出现IO问题。导致读写文件失败。所以兜底的操作就是最终都去nacos读取。
    • 另外一个问题在于,写到文件中的配置都是明文的。运维或者开发登陆到机器上就能看到配置的信息。有一定的安全风险。

    用Redis

    绝大部分的服务最终都用了这种模式。因为用了redis,所以也不需要用MAP了。经过测试,从MAP中读取,和从Redis读取的时间差可以忽略不计,几乎无感知。

    • 最明显的缺点肯定就是会增加一个redis,在整体架构上多了一环,那就多了一个不稳定因素。因为redis也有挂掉的可能。
    • 这个redis到底是独立的只用于配置,还是和其他的缓存的redis合用一个,也是一个纠结的问题。不过实际的决策还是要根据业务来。
    • 还有一个坑是,如果redis和nacos在同一个可用区,那这个可用区挂掉之后,会导致两边都拿不到数据,一样导致不可用。所以redis和nacos本身的高可用部署也是需要考虑的。

    通用问题

    • 数据一致性问题。缓存中的数据和nacos中的数据可能是不一致的。虽然有监听更新,但在大数据量和频繁读取的场景中,也有可能导致不一致的情况出现。所以这种对一致性要求很高的场景,建议做一些一致性保障的逻辑。
    • 如果Nacos配置数据量非常大或者数量众多,如果是用的缓存到本地文件的方式,可能会占用大量的存储空间,并且读取和写入大量的配置数据可能会影响应用的性能。所以量大的场景不太建议用文件缓存。
    • 如果在配置中有一些敏感数据,比如密码、敏感数据等,用缓存的方式都可能增加新的安全风险。比如缓存到文件是会被读取到的,缓存到redis通常都是明文存储的。

    最后总结

    考虑了多种实现方式,最终选了上述的方式。另外一种实现方式是,可以主动轮训。

    但它的问题在于,主动轮训会需要确定好时间间隔,太短可能占用应用的性能,太长可能数据更新不及时等。另外就是轮训也有数据一致性问题。

    还有一种缓存方式,就是把量大的配置做分批缓存。比如每批缓存500个配置。由于和我们的业务不太匹配,就没有尝试这种方式。

    总之,我们做nacos本地缓存是为了避免nacos故障导致的业务不可用。每个企业每个项目每个业务遇到的问题可能不一样,大家应该根据自己的实际场景,来寻找最适合的解决方案。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论