一、diamond-server功能介绍:
1、提供配置信息的可视化管理(新增,修改,删除)
2、保证配置服务的可用性:
a. 提供多节点无中心的集群部署,并使用通知机制保证节点数据一致性;
b. 借助于配置服务是读多写少的特性,内部使用读内存+本地磁盘文件,写DB的实现方式;
3、提供diamond-client配置获取和轮训更新的接口
二、diamond-server启动时自动加载运行的类
1、com.taobao.diamond.server.service.TimerTaskService-定时任务,主要职责是同步DB中的配置信息到本地磁盘文件中。默认是每隔10分钟同步一次。这样可以保证在diamond-client获取配置的时候,可以直接从diamond-server本地文件中取内容。
// 开启定时任务,并通过DumpConfigInfoTask实现DB->缓存同步
@PostConstruct
public void init() {
this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(THREAD_NAME);
t.setDaemon(true);
return t;
}
});
DumpConfigInfoTask dumpTask = new DumpConfigInfoTask(this);
dumpTask.run();
this.scheduledExecutorService.scheduleWithFixedDelay(dumpTask, SystemConfig.getDumpConfigInterval(),
SystemConfig.getDumpConfigInterval(), TimeUnit.SECONDS);
}
TimerTaskService借助com.taobao.diamond.server.service.DumpConfigInfoTask -> run -> updateConfigInfo来实现同步任务。
// 依次遍历数据库中所有的数据
public void run() {
try {
Page<ConfigInfo> page = this.timerTaskService.getPersistService().findAllConfigInfo(1, PAGE_SIZE);
if (page != null) {
// 总页数
int totalPages = page.getPagesAvailable();
updateConfigInfo(page);
if (totalPages > 1) {
for (int pageNo = 2; pageNo <= totalPages; pageNo++) {
page = this.timerTaskService.getPersistService().findAllConfigInfo(pageNo, PAGE_SIZE);
if (page != null) {
updateConfigInfo(page);
}
}
}
}
}
catch (Throwable t) {
log.error("dump task run error", t);
}
}
// 把从DB中查询出的配置信息更新到缓存和磁盘中
private void updateConfigInfo(Page<ConfigInfo> page) throws IOException {
for (ConfigInfo configInfo : page.getPageItems()) {
if (configInfo == null) {
continue;
}
try {
// 更新缓存
this.timerTaskService.getConfigService().updateMD5Cache(configInfo);
// 存入磁盘
this.timerTaskService.getDiskService().saveToDisk(configInfo);
}
catch (Throwable t) {
log.error(
"dump config info error, dataId=" + configInfo.getDataId() + ", group=" + configInfo.getGroup(), t);
}
}
}
2、com.taobao.diamond.server.service.PersistService-初始化DB链接,提供config在DB的存取功能。
3、com.taobao.diamond.server.service.NotifyService-通知服务,diamond-server在有数据更改的时候通知其他节点做数据同步操作。首先加载所有的diamond-server节点信息。
// 加载node.properties配置好的diamond-server节点
@PostConstruct
public void loadNodes() {
InputStream in = null;
try {
in = ResourceUtils.getResourceAsStream("node.properties");
nodeProperties.load(in);
}
catch (IOException e) {
log.error("加载节点配置文件失败");
}
finally {
try {
if (in != null)
in.close();
}
catch (IOException e) {
log.error("关闭node.properties失败", e);
}
}
log.info("节点列表:" + nodeProperties);
}
然后diamond-server在配置修改的时候通过调用notifyConfigInfoChange来进行配置信息更改通知。
// 通知配置信息改变
public void notifyConfigInfoChange(String dataId, String group) {
Enumeration<?> enu = nodeProperties.propertyNames();
while (enu.hasMoreElements()) {
String address = (String) enu.nextElement();
if (address.contains(SystemConfig.LOCAL_IP)) {
continue;
}
String urlString = generateNotifyConfigInfoPath(dataId, group, address);
final String result = invokeURL(urlString);
log.info("通知节点" + address + "分组信息改变:" + result);
}
}
// 生成通知url, 默认是:http://address:port/diamond-server/notify.do?method=notifyConfigInfo
String generateNotifyConfigInfoPath(String dataId, String group, String address) {
String specialUrl = this.nodeProperties.getProperty(address);
String urlString = PROTOCOL + address + URL_PREFIX;
// 如果有指定url,使用指定的url
if (specialUrl != null && StringUtils.hasLength(specialUrl.trim())) {
urlString = specialUrl;
}
urlString += "?method=notifyConfigInfo&dataId=" + dataId + "&group=" + group;
return urlString;
}
// http get调用
private String invokeURL(String urlString) {
HttpURLConnection conn = null;
URL url = null;
try {
url = new URL(urlString);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(TIMEOUT);
conn.setReadTimeout(TIMEOUT);
conn.setRequestMethod("GET");
conn.connect();
InputStream urlStream = conn.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(urlStream));
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
finally {
if (reader != null)
reader.close();
}
return sb.toString();
}
catch (Exception e) {
log.error("http调用失败,url=" + urlString, e);
}
finally {
if (conn != null) {
conn.disconnect();
}
}
return "error";
}
三、diamond-server新增配置流程(修改删除流程类似)
1. 提交数据进入com.taobao.diamond.server.controller.AdminController.postConfig
// 新增配置信息,通过configService.addConfigInfo来操作
@RequestMapping(params = "method=postConfig", method = RequestMethod.POST)
public String postConfig(HttpServletRequest request, HttpServletResponse response,
@RequestParam("dataId") String dataId, @RequestParam("group") String group,
@RequestParam("content") String content, ModelMap modelMap) {
response.setCharacterEncoding("GBK");
boolean checkSuccess = true;
String errorMessage = "参数错误";
if (StringUtils.isBlank(dataId) || DiamondUtils.hasInvalidChar(dataId.trim())) {
checkSuccess = false;
errorMessage = "无效的DataId";
}
if (StringUtils.isBlank(group) || DiamondUtils.hasInvalidChar(group.trim())) {
checkSuccess = false;
errorMessage = "无效的分组";
}
if (StringUtils.isBlank(content)) {
checkSuccess = false;
errorMessage = "无效的内容";
}
if (!checkSuccess) {
modelMap.addAttribute("message", errorMessage);
return "/admin/config/new";
}
dataId = dataId.trim();
group = group.trim();
this.configService.addConfigInfo(dataId, group, content);
modelMap.addAttribute("message", "提交成功!");
return listConfig(request, response, dataId, group, 1, 20, modelMap);
}
2. 进入com.taobao.diamond.server.service.ConfigService,新增顺序是:存DB -> 更新缓存 -> 存磁盘文件 -> 通知其他diamond-server节点
// 更新配置信息
public void updateConfigInfo(String dataId, String group, String content) {
checkParameter(dataId, group, content);
ConfigInfo configInfo = new ConfigInfo(dataId, group, content);
// 先更新数据库,再更新磁盘
try {
persistService.updateConfigInfo(configInfo);
// 切记更新缓存
this.contentMD5Cache.put(generateMD5CacheKey(dataId, group), configInfo.getMd5());
// 存磁盘文件
diskService.saveToDisk(configInfo);
// 通知其他节点
this.notifyOtherNodes(dataId, group);
}
catch (Exception e) {
log.error("保存ConfigInfo失败", e);
throw new ConfigServiceException(e);
}
}
3. 通知其他diamond-server节点配置信息有变更是通过http调用NotifyController.notifyConfigInfo方法实现的。
// 通知配置信息改变
@RequestMapping(method = RequestMethod.GET, params = "method=notifyConfigInfo")
public String notifyConfigInfo(@RequestParam("dataId") String dataId, @RequestParam("group") String group) {
dataId = dataId.trim();
group = group.trim();
this.configService.loadConfigInfoToDisk(dataId, group);
return "200";
}
四、给diamond-client提供配置信息接口
1、获取配置接口:com.taobao.diamond.server.controller.ConfigController.getConfig
// diamond-client请求该方法获取配置信息
public String getConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group) {
response.setHeader("Content-Type", "text/html;charset=GBK");
final String address = getRemortIP(request);
if (address == null) {
// 未找到远端地址,返回400错误
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return "400";
}
if (GlobalCounter.getCounter().decrementAndGet() >= 0) {
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return "503";
}
String md5 = this.configService.getContentMD5(dataId, group);
if (md5 == null) {
return "404";
}
response.setHeader(Constants.CONTENT_MD5, md5);
// 正在被修改,返回304,这里的检查并没有办法保证一致性,因此做double-check尽力保证
if (diskService.isModified(dataId, group)) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return "304";
}
// 从缓存中查询配置信息
String path = configService.getConfigInfoPath(dataId, group);
// 再次检查
if (diskService.isModified(dataId, group)) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return "304";
}
// 禁用缓存
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
return "forward:" + path;
}
2、轮训配置是否更新:com.taobao.diamond.server.controller.ConfigController. getProbeModifyResult
// 接收 dataId+groupId+contentMD5 集合,判断返回配置信息是否更改
public String getProbeModifyResult(HttpServletRequest request, HttpServletResponse response, String probeModify) {
response.setHeader("Content-Type", "text/html;charset=GBK");
final String address = getRemortIP(request);
if (address == null) {
// 未找到远端地址,返回400错误
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return "400";
}
if (GlobalCounter.getCounter().decrementAndGet() >= 0) {
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return "503";
}
final List<ConfigKey> configKeyList = getConfigKeyList(probeModify);
StringBuilder resultBuilder = new StringBuilder();
for (ConfigKey key : configKeyList) {
String md5 = this.configService.getContentMD5(key.getDataId(), key.getGroup());
if (!StringUtils.equals(md5, key.getMd5())) {
resultBuilder.append(key.getDataId()).append(WORD_SEPARATOR).append(key.getGroup())
.append(LINE_SEPARATOR);
}
}
String returnHeader = resultBuilder.toString();
try {
returnHeader = URLEncoder.encode(resultBuilder.toString(), "UTF-8");
}
catch (Exception e) {
// ignore
}
request.setAttribute("content", returnHeader);
// 禁用缓存
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
return "200";
}