背景
前段时间做了一个项目,由于nacos的不稳定性,导致了生产环境拉取配置失败了,从而影响了生产环境的业务。
于是团队就做了一个大胆的决定,为了避免因为依赖nacos导致业务的不可用,我们一致决定,在本地做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故障导致的业务不可用。每个企业每个项目每个业务遇到的问题可能不一样,大家应该根据自己的实际场景,来寻找最适合的解决方案。