数据中台配置中心详解 一般有用 看3 速 一个项目流程

第 1 章 配置中心入门
1.课程概述
本课程是讲解配置中心的设计和开发实现。配置中心作为微服务架构中,配置存储的核心构件,在微服
务项目设计中起着至关重要的作用。它统一管理着各业务微服务的具体配置信息。同时,让配置更新有
了更清晰的访问API。那么它是如何实现的呢,在市面上活跃的配置中心用了哪些技术,我们在设计时又
应该考虑哪些细节呢?
在接下来的课程中,我们将逐一解答。
1.1 课程安排
本课程分为4个部分。具体安排如下:
第一部分
微服务架构中的配置中心
配置中心实现思路和技术运用的分析
黑马应用配置管理平台简介
黑马应用配置管理平台存储方案分析(持久化方案,缓存方案)
配置中心服务端存储方案分析之容灾机制
提前准备的代码介绍(持久化配置的技术实现)
开发前的环境准备
缓存存储功能实现
容灾机制功能实现
第二部分
HACM中配置推送的实现思路分析
模拟服务器的推模式——轮询机制实现思路分析
拉取模式——利用消息队列读取配置更新思路分析
推模式技术实现
拉模式技术实现
黑马应用配置管理平台的推送轨迹需求分析
轨迹记录的技术选型介绍
黑马应用配置管理平台的轨迹记录功能实现
黑马应用配置管理平台质量监控技术分析及实现
部署及整合测试
市场流行的配置中心的简介和差异
 
 
1.2 技术要求
学习本课程需要一定的技术储备,如果同学们对接下来的一些技术和术语有困惑的话,建议先补充这部
分知识。
第一,spring boot框架
第二,spring cloud框架
第三,redis和fastd
fs的使用
第四,rabbitm
q
消息队列
第五,mongod
b
的使用和spring data mongo框架
第六,微服务架构的概念
第七,spring框架的ioc容器核心知识
第八,spring框架的事件发布机制和事件监听机制
第九,spring的ioc容器的创建和初始化过程
1.3 课程目的
通过学习本套课程,同学们可以更深入的了解数据中台下配置中心的作用。同时,对配置中心的容灾机
制有一定认识。在更新配置过程中,我们深入的分析了Spring Cloud中更新配置的具体实现过程。
1.4 课程亮点
本课程采用时下非常流行的Spring Boot + Spring Cloud环境搭建。并且结合市场流行的微服务项目引入应
用。课程中使用了 Redis+FastDFS 作为缓存和容灾的解决方案,可以让用户灵活的选择不同存储平
台。在推送配置时,也采用了市场比较流行的推和拉两种模式来实现。并且,把现在市场流行的配置中
心中推送轨迹的功能也加入了进来。最后,更新配置的部分,我们引用了了spring cloud中的核心代码,
并且对源码进行深入分析,来理清楚如何刷新配置信息。
2 配置中心基础知识
2.1 配置中心简介
2.1.1 概念
配置中心,顾名思义就是用于统一管理项目中各种配置的平台,它是现在互联网项目开发中必不可少的
部分。由于它的存在,可以让我们在开发中减少因配置更迭而产生的大量代码。同时,针对成熟的技术
从而实现配置热部署,当我们的配置发生变更时,无须停止服务,实现配置的更新。
配置中心在微服务架构中所处的位置,如下图所示:
 
 
2.1.2 配置中心作用
1) 动态加载配置
在早期的传统项目开发中,我们通常是编写项目的配置文件的。例如:
jdbc.properties,
config.properties,applicationContext.xml等等。这些配置文件中定义着项目的应用配置,环境配置,安
全配置,业务配置等等内容。此种方式的写法,针对项目来说,由于和项目发布在一起,所以安全性有
保证,同时对于配置的读取加载等等操作,也很简单。但是,当我们需要更新配置时,往往不太灵活,
很多时候需要通过重新加载项目才是使配置更新。而目前的配置中心几乎都支持动态加载和热部署的方
式,当我们配置变更时,都可以实现动态加载最新配置。
2)多环境配置
配置文件作为独立存在的一个文件,当和项目放在一起时,它无法区分项目的运行场景,即,部署环
境。但是在现在互联网项目中,我们项目正式上线运行之前,会经过反复的测试。每个阶段的测试,所
处的环境也是不一样的。例如,开发环境,生产环境,测试环境等等。每个环境,可能对系统性能的要
求是不同的,所以配置也是不一样的。我们以数据库连接池为例,在开发环境中,我们可能只需一个连
接即可。可是到了测试环境,一个连接显然是不行的。那么此时,通常的做法就是修改配置文件。而有
了配置中心之后,我们可以针对不同的环境提供不同的配置。而目前市面流行的配置中心都支持多环境
配置,配合运维工具的使用,可以快速切换到不同环境下的配置。
3)微服务架构统一管理配置
在微服务架构的项目中,每个服务都会有独立的配置文件。在此前提下,如果没有配置中心的话,当我
们部署微服务时,遇到有相关业务逻辑的微服务在修改配置时,由于每个服务都有配置文件,所以每个
服务的配置都需要找到配置,单独修改。时间一长的话,会造成配置管理混乱。当有了配置中心后,我
们的配置放到一起统一管理,修改配置也是在一个地方统一修改,管理配置变得简单清晰。
4)配置更新轨迹
在传统项目开发中,独立配置文件在变更时,如果需要记录更新轨迹的话,需要针对每次修改,手动记
录更新轨迹。如果没有人记录,或者有人忘记了记录的话,那么轨迹就不再连续,也就有可能失去了它
的价值。而现在常见的配置中心都有轨迹记录的功能,可以追溯配置变更的情况。从而,可以让我们在
微服务运行出现异常情况时,能够追溯配置变更细节。
 
 
2.2 当前市场上配置中心解决方案介绍
2.2.1 Spring Cloud Config
Cloud Config作为Spring Cloud的6大组件之一,可以极简的实现配置统一化管理。它并没有自己编写管理
配置的功能,而是借助于git仓库来保存服务配置。通过和git仓库建立http协议接口,抓取仓库中的配
置,实现配置更新。
下图,描述了Sprin
g Cloud Config配置中心的系统架构图。当然,Spring Cloud Config它在更新配置时,
借助的是Cloud Bus消息总线机制。
2.2.2 ctrip apollo
Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的
配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置
管理场景。
服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。
Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时对Spring/Spring Boot环境也有较好
的支持。
.Net客户端不依赖任何框架,能够运行于所有.Net运行时环境。
Apollo官网地址:
https://github.com/ctripcorp/apollo
Apollo的系统架构图如下:
 
 
2.2.3 alibaba-nacos(分布式配置中心)
Nacos是阿里2018年开源的注册中心中间件,它同时提供了配置中心的功能,从而实现了注册发现和配置
的融合。在官网中它的说明如下:
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您实现动态服
务发现、服务配置管理、服务及流量管理。 Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平
台。 Nacos 是构建以“服务”为中心的现代应用架构(例如微服务范式、云原生范式)的服务基础设施。
官网地址:https://nacos.io/zh-cn/docs/architecture.html
它的系统架构图如下:
 
 
2.2.4 阿里云-ACM服务(
diamond)
ACM,全称是Application Configuration Management,它是一款在分布式架构环境中对应用配置进行集中
管理和推送的产品。凭借配置变更、配置推送、历史版本管理、灰度发布、配置变更审计等配置管理工
具,ACM能帮助您集中管理所有应用环境中的配置,降低分布式系统中管理配置的成本,并降低因错误
的配置变更造成可用性下降甚至发生故障的风险。
它的前身是淘宝内部的Diamond配置中心。官网地址如下:
https://help.aliyun
.com/document_detail/59953.html?spm=a2c4g.11186623.6.542.6cad52e1PK0gFP
在官网中,提供了它的系统架构图:
阿里有两款配置中心产品,一款是nacos,一款是阿里云服务集成的ACM,在ACM的帮助文档中,官方给
出了两款产品的差异:
 
 
2.3 常用配置中心差异
 
 
对比项
目/配置
中心
spring cloud
config
apollo
nacos
开源时
2014.9
2016.5
2018.6
配置实
时推送
支持(
Spring
Clou
d Bus)
支持(HTTP长轮询1s内)
支持(HTTP长轮询1s内)
版本管
支持(
Git)
自动管理
自动管理
配置回
支持(
Git)
支持
支持
灰度发
支持
支持
待支持
权限管
支持
支持
待支持
多集群
多环境
支持
支持
支持
监听查
支持
支持
支持
多语言
只支持Java
Go,C++,Python,Java,.net,OpenAPI
Python,Java,Nodejs,OpenAPI
分布式
高可用
最小集
群数量
Config
Server2+Git+MQ
Config2+Admin3+Portal*2+Mysql=8
Nacos*3+MySql=4
配置格
式校验
不支持
支持
支持
通信协
HTTP和AMQP
HTTP
HTTP
数据一
致性
Git保证数据一
致性,Config
Server从Git读
取数据
数据库模拟消息队列,Apollo定时
读消息
HTTP异步通知
单机读
tps)
7(限流所制)
9000
15000
单机写
tps)
5(限流所制)
1100
1800
3节点
21(限流所
制)
27000
45000
3节点
5(限流所制)
3300
5600
 
 
2.4 配置中心技术解决方案浅析
2.4.1 潜在问题
课程一开始,我们已经说明了,本课程是要实现一个自定义配置中心。并且,通过前面章节的讲解,同
学们也已经看到市面上比较流行的配置中心系统架构。那么我们要想实现自定义配置中心,有哪些潜在
问题呢,或者说从方面考虑呢?
第一个问题,配置中心的数据如何接入。
第二个问题,配置中心的存储如何落盘。
第三个问题,配置中心的配置更新后如何动态推送。
第四个问题,业务微服务读取配置的方式,如何支撑高并发读取。
第五个问题,配置更新轨迹的实现。
第六个问题,配置中心的监控。高并发下稳定的运行如何实现。
2.4.2 解决方案
1)配置中心数据接入的解决方案
通过分析市场常见的配置中心,在数据接入中必不可少的一种方案就是API接入。通过提供API访问的方
式,把本地配置上传到配置中心。除此之外,像ACM中提供的可视化配置方式也是比较简介清晰的接入
方式。
2)配置中心的配置存储的解决方案
存储介质的选择,几个重要的问题必须考虑进去。第一是配置更新和获取的效率问题。第二是配置更新
的同步问题。第三是配置中心的容灾。基于此,我们在设计之初讨论了两套解决方案。第一套是
Redis+Mysql+FastDFS来实现的。第二套HBase+Mysql+HDFS来实现的。Redis或者HBase作为缓存的解决方
案,而MySQL数据库做持久化存储解决方案。FastDFS和HDFS的就是利用磁盘文件来实现容灾。当然,也
可以像Spring Cloud Config一样,存储自己不实现,而是利用Git来实现配置存储。
3)配置更新后的动态推送解决方案
动态配置更新,其含义是当配置更新后,可以动态的通知到每个业务微服务,从而在不停服务的前提下
加载最新的配置项。这也是配置中心的一大亮点,早期传统项目不具备的功能。要想实现此效果,有两
条路可行。
第一,推模式。即服务端更新完配置后,主动推送到各个业务微服务。当然,此种方式在基于http协议
的web项目中,是不能实现推这个动作的。究其原因,就是HTTP协议的一问一答的规则。必须现有发
问,才能回答。所以服务端是不能先回答的。那么此时,就需要建立长轮询机制(
Long-Looping),由
客户端不停的想服务端发问:“配置有变化吗?”,当得到的答案是有变化时,更新配置。
第二,拉模式。即客户端自己从服务端拉去最新配置。此种方式,可以由客户端在更新完配置后,主动
向服务端发送一个请求,从而把服务端存储的最新配置拉去到本地微服务。
4)业务微服务高并发读取配置的解决方案
在解决了动态推送最新配置之后,就要解决高并发的配置读取了。针对业务微服务高并发读取配置方
案,选择使用MQ来接收请求,减少加载配置的并发访问量。
 
 
5)配置更新轨迹的解决方案
在解决配置轨迹记录的场景下,选择了MongoDB作为数据存储的解决方案。同时通过log4j记录日志的方
式,把轨迹定期同步到日志中。从而保证更新轨迹不缺失。
6)配置中心运行监控的解决方案
在记录监控时,采用rancher作为容器部署工具,借助google的CAdvisor采集配置中心运行数据,由于
CAdvisor是按照时间片来展示数据的,同时它不具备存储的功能,所以要配合InfluxDB来使用,由
InfluxDB保存采集到的运行数据。最后,使用
Grafana作为监控软件,监控采集到的数据。
3.黑马
应用配置管理平台(HACM)简介
3.1 背景
随着互联网项目的迅速崛起,早期传统项目的一体化架构已经满足不了当下项目架构要求了。于是架构
开始演变,经历的垂直拆分架构,分布式架构,流式计算架构,微服务架构的几个阶段,逐步满足了当
下互联网项目的需求。
在微服务架构的项目中,程序功能的日益复杂,微服务的配置日益增多,例如:各种功能的开关、各种
配套软件参数的配置,各种服务的地址配置等等。此时,对程序配置的期望值也越来越高,希望配置修
改后能够实时生效,支持分环境发布,灰度发布,支持分集群管理配置和完善的权限、审核机制……
在这样的大环境下,各个厂商的配置中心孕育而生,虽然他们出现的或早或晚,但是都能满足我们在微
服务架构中对配置文件管理的需求。比如,像我们前面介绍的,Spring Cloud Config,Alibaba nacos
,Ctrip Apollo,阿里云的ACM等等,还有像停止维护的百度disconfig。从目前市场来看,配置中心已经是
微服务架构中不可或缺的一部分了。
而很多互联网企业的业务线逐步完备,项目组日益增多,早期独立的平台化项目设计理念不再具有优
势,逐步转向为中台化设计。我们的黑马应用配置管理平台也正是基于此立项的。在设计中借鉴了Ctrip
Apollo和阿里的ACM,同时根据企业内部需要,解决数据中台下20几个项目的配置集中管理。
是基于此立项研发的。
它致力于解决黑马内部基于微服务架构项目的配置管理。
3.2 目的
黑马应用配置管理平台的课程旨在让同学们通过学习之后,对数据中台下配置管理有更深入的认识。我
们将从接入数据,配置存储,动态更新,并发解决,轨迹记录这6个方面深入展开讲解。为同学们今后的
使用做技术储备。
3.3 系统架构
 
 
4.HACM基础功能开发
4.1 应用配置管理平台环境准备
4.1.1 安装Centos
Centos作为LInux发行版本之一,其发行的Linux版本在市场占有率非常高,究其原因,一个是它的运行稳
定,再一个就是在加入了Red Hat之后仍然采用免费政策。它的官网地址是:https://www.centos.org/
 
 
点击图中红框里的按钮,进入下载页面,下载对应的版本即可。
4.1.2 安装Docker
Docker 是PaaS(
Platform as a Service)提供商 dotCloud 开源的一个基于 LXC(
Linux Container)的高级
容器引擎,源代码托管在Github上, 基于go语言遵从Apache2.0协议开源。是目前主流的容器引擎之一。
Docker的安装在网上搜索有很多,步骤也很简单。同时下载资源包和安装过程需要消耗一些时间,所以
我们在课上就不带领同学们安装了。在给同学们的资料中,我们提供了一个Linux镜像,是Centos7版本
的,里面已经装好了Docker,同时也下载好了所需的镜像。同学们在用以学习时,可以直接通过VMWare
挂载使用。如果是用于商业目的,请支持正版。
2.1.3 系统开发环境

 

 
 
环境名称
环境版本
作用说明
windows7
64bit
开发操作系统
JDK
1.8
Java developer kit
Idea
2018.2
开发工具
Maven
3.5.2
Jar包版本管理工具
SpringBoot
2.0.1
项目框架
SpringCloud
Finchley.M9
微服务框架
Redis
4.0.11
配置存储解决方案一
FastDFS
morunchang/fastdfs
配合Redis实现配置存储
HBase
1.3
配置存储解决方案二
HDFS
singularities/hadoop
配合HBase实现配置存储解决方案二
RabbitMQ
3.8.3
利用消息队列实现客户端更新配置的拉取
MongoDB
3.2.1
用于存储配置更新轨迹
环境名称
环境版本
作用说明
Linux
centos7
项目部署的主机操作系统
Docker
18.06
运行容器的引擎
Rancher
用于部署配置中心的容器
Grafana
用于对配置中心质量监控预警的工具
InfluxDB
用于记录配置中心运行情况的数据库工具
CAdvisor
用于采集配置中心运行情况数据的工具
4.1.4 系统运行环境

 

4.2 需求说明
黑马应用配置管理平台Heima Application Configuration Management(简称HACM)和其他管理系统没有
本质区别,都是用于管理资源的,只是管理的资源类型不同。比如CRM,客户管理管理系统,它就是用
于管理客户关系的。那么在HACM中,它管理的资源就是配置文件,只是这个配置文件不只是一个项目中
的,而是公司所有基于微服务开发的项目配置文件统一放到此处管理。所以它的需求将会主要有以下几
个方法。
 
 
4.2.1 配置管理功能
它包括:
配置文件的上传。旨在当我们已经有了配置文件之后,可以直接把配置文件上传到管理平台中。
配置文件的创建。当一个新的项目或者新的微服务,没有本地配置时,可以直接在平台创建配置文件。
配置项添加和修改。它指的是通过平台提供的可视化界面创建配置项或者修改配置项的值。
配置项的删除。它可以把配置文件中没用的配置项进行移除。
配置文件的移除。它是把整个配置文件从系统中移除。
(这是一个初级程序员的工作,我们在课程中直接提供给同学们完整的工程)
4.2.2 用户
管理功能
它包括:
用户信息的增删改查操作。
用户的登录。
(这是一个初级程序员应该关注的事情,所以我们在课程讲解中弱化了此部分内容)
4.2.3 权限控制
它包括:
根据用户级别进行权限控制。它指的是,非项目负责人,只能对自己负责项目中微服务的配置进行查看
和管理。而项目负责人,则可以管理项目中所有微服务的配置。最后,项目组组长可以管理组中所有项
目的配置。不提供更高级别可以管理公司所有项目的配置,因为这样不安全。
(权限控制是很庞大的一个体系,包括粗粒度和细粒度。但是,这不是我们课程的重点内容,所以我们
在课程讲解中弱化了此部分内容)
4.2.4 配置推送功能
它包括:
配置的推送。平台需要提供了两种解决方案。第一种是,当配置发生变更时,可以及时的把配置推送到
客户端,可以采用轮询的方式模拟服务器的推模式。第二种是,客户端主动拉取配置,它的实现机制就
是借鉴Spring Cloud Config的部分。
配置推送轨迹的记录。当配置发生变更后,需要把变更的配置记录起来。以便后续使用。例如:根据轨
迹和运行日志分析哪些配置可以达到服务的稳定运行或者处理更多请求等等。
轨迹的同步。在我们记录了很长时间的轨迹之后,这些轨迹一值保存在数据库中会消耗很多数据库空
间,所以我们需要定期清理轨迹。但是,清理的轨迹并不是代表就没用价值了。所以不是真正的把轨迹
删掉,而是把它从数据库转移到另外的地方。我们这里采用定时任务调度,根据用户设定的时长,把轨
迹数据写到日志文件中。
4.3 数据库介绍
 
 
4.3.1 用户表

 

4.3.2 配置信息表

 

crea
te table acm_project_group
(
id varchar(40) not null,
project_group_name varchar(50) default NULL ,
project_manager_name varchar(64) default NULL,
state varchar(10) default NULL ,
remark varchar(255) default NULL,
primary key (id)
);
create table acm_user
(
id varchar(40) not null,
user_name varchar(50) default NULL ,
password varchar(64) default NULL ,
gender varchar(10) default NULL,
age int(2),
birthday date default NULL,
telephone varchar(11) default NULL,
email varchar(60) default NULL,
degree int(2) default NULL,
state varchar(10) default NULL comment '1启用0停用',
remark varchar(255) default NULL,
project_group_id varchar(40) default NULL,
primary key (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 
 
4.4 导入工程
create table acm_config_info
(
id varchar(40) not null,
project_name varchar(50) default NULL ,
env_name varchar(64) default NULL ,
cluster_number varchar(10) default NULL ,
service_name varchar(255) default NULL,
config_detail varchar(4000),
user_id varchar(40),
project_group_id varchar(40),
create_time date,
update_time date,
primary key (id)
)
 
 
 
黑马配置中心服务端工程启动后测试配置的增删改查操作。
第2章 配置中心配置存储解决方案
5 配置文件分析
5.1 配置文件的类型
5.1.1 xml
xml作为配置文件的格式,是历史非常悠久的一种格式。它的优点和缺点都同样明显。优势是,有严格的
语法要求,层次结构清晰,支持约束定义,在配合约束使用时可以有效避免无效项配置。它的弊端是为
了描述清楚几行配置,往往要写很多配套配置。例如:导入约束,编写标签,编写属性名称等等。同
时,当编写不符合语法或者使用了非法字符时,项目无法正常启动。下图描述了xml作为配置文件时,配
置项和额外配置内容展示:
 
 
在早期的传统项目中,使用的非常频繁。例如:基础框架
spring,struts2,hibernate,mybatis等等配置
文件都支持xml格式。但是,随着微服务架构出
现,它的弊端越来越明显,从而逐渐淡出互联网项目配置
文件的体系了。
5.1.2 prop
erties
在xml作为配置文件的问题逐步暴露出来了之后
,properties作为配置文件的使用场景和应用越来越多
了。但是properties本身作为配置文件时,也有非常明显的优势和不足。优势是,结构简单,以键值对方
式存储。看起来清晰明了。不足是,描述层级关系时它很不方便。下图描述了properties作为配置文件的
内容展示:
随着xml作为配置文件格式逐步淡出互联网项目开发,properties的应用变得多了起来。很多框架都支持
properties作为配置文件的格式。例如:Spring SpringBoot等等。但是描述出配置的层级关系,让开发者
在阅读配置时更加清晰明了,一直都是程序员心中所想。于是有了yml。
5.1.3 YAML
YML,也记做YAML,它的解释如下图:
在上图中介绍了,它从多种语言中汲取了灵感,来定义它的语法格式。越来越多的框架把它作为首要的
配置文件格式。下图描述了YML的作为配置文件格式的内容展示:
 
 
5.2 存储格式分析
在1.1小节,我们介绍了配置文件的格式分析,本小节我们将分析针对不同格式的存储方式。这里面的存
储格式又分为缓存格式和存入文件系统的格式
5.2.1 缓存分析
1)缓存什么内容
此问题其本质就是:缓存ConfigInfo还是ConfigInfo中的configDetail。到底是把整个ConfigInfo对象缓存
了,还是缓存配置详情就够了。
我们考虑,之所以有此一问,无外乎就是考虑对缓存服务器的资源消耗。其实此处大可不必有太多顾
虑,因为剩余内容也占不了多少空间,同时一些字段信息可能还有作用。所以我们缓存时,直接缓存
ConfigInfo对象即可。
2)怎么存
在考虑把整个对象存入Redis时,我们要考虑一个问题,因为配置详情内容是直接以json格式字符串存入
的,出于安全性的考虑,不建议直接把ConfigInfo对象存入Redis。那么,此时借助Kryo序列化将会解决此
问题,我们把ConfigInfo转成字节数组再缓存起来更加合适。
5.2.2 DFS存储分析
分布式文件系统DFS是用于解决当redis和mysql都出现问题时,平台可以利用它来容灾。那么存入DFS系
统的方式和时机就是我们要分析的内容
1)存什么内容
在DFS分布式文件系统中,我们也要把ConfigInfo全部的内容存入进去。其目的有二,一是当我们读取不
到缓存和持久化数据时,分布式文件系统中的数据仍是完整的;二是当我们读取完数据之后,在缓存服
务器和持久化数据库恢复后可以同步回去,此时也要求是完整数据。所以,我们要把整个ConfigInfo存
入,并且也是利用Kryo序列化成字节数组后存入。
 
 
ConfigInfo的操
存入DFS的时机
说明
上传完整配置信
通过消息队列异步存
判断是否已经有了此配置,没有直接存入,有的话替
换。
创建配置
不存入
因为此时没有任何配置项信息,所以不用存入DFS。
添加配置项
通过消息队列异步存
判断是否已经有了此配置,没有直接存入,有的话替
换。
更新配置项
通过消息队列异步存
判断是否已经有了此配置,没有直接存入,有的话替
换。
删除配置项
通过消息队列异步存
判断是否已经有了此配置,没有直接存入,有的话替
换。
删除整个配置
通过消息队列异步删
直接删除。
2)什么时候存
这个问题我们要认真思考,因为它涉及了我们配置信息操作的执行效率。我们通过下表来分析:
6 缓存和容灾的解决方案分析
6.1 缓存解决方案分析
6.1.1 Redis
Redis是用C语言开发的一个开源的高性能键值对(
key-value)的NoSQL数据库。它通过提供多种键值数
据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:
字符串类型
散列类型
列表类型
集合类型
有序集合类型。
它起源于2008年,当时意大利的一家创业公司Merzia推出了一款基于MySQL的网站实时统计系统
LLOOGG,然而没过多久该公司的创始人 Salvatore Sanfilippo便对MySQL的性能感到失望,于是他决定亲
自为LLOOGG量身定做一个数据库,并于2009年开发完成,这个数据库就是Redis。不过Salvatore
Sanfilippo并不满足只将Redis用于LLOOGG这一款产品,而是希望更多的人使用它,于是在同一年
Salvatore Sanfilippo将Redis开源发布,并开始和Redis的另一名主要的代码贡献者Pieter Noordhuis一起继
续着Redis的开发,直到今天。
Salvatore Sanfilippo自己也没有想到,短短的几年时间,Redis就拥有了庞大的用户群体。Hacker News在
2012年发布了一份数据库的使用情况调查,结果显示有近12%的公司在使用Redis。国内如新浪微博、街
旁网、知乎网,国外如GitHub、Stack Overflow、Flickr等都是Redis的用户。
 
 
VMware公司从2010年开始赞助Redis的开发, Salvatore Sanfilippo和Pieter Noordhuis也分别在3月和5月加
入VMware,全职开发Redis。
6.1.2 HBase
HBase本身是不能称之为缓存的,它只是一种存储方案。如下图:
它具有高可靠性、高性能、面向列、可伸缩的分布式存储系统等优势。它的写入速度非常快,但是读取
速度是无法和Redis相提并论的。但是它和HDFS组合使用,可能在执行效率上不如Redis+FastDFS,但是
在可靠性和伸缩性上却非常有优势。
6.1.3 两者的区别
读写性能:
HBase写快读慢,HBase的读取时长通常是几毫秒,而Redis的读取时长通常是几十微秒。性能相差非常
大。
数据类型:
HBase和Redis都支持KV类型。但是Redis支持List、Set等更丰富的类型
数据量:
Redis支持的数据量通常受内存限制,而HBase没有这个限制,可以存储远超内存大小的数据。
部署难易:
HBase部署需要依赖hadoop、zookeeper等服务,而Redis的部署非常简单。
数据可靠性:
HBase采用WAL,先记录日志再写入数据,理论上不会丢失数据。而Redis采用的是异步复制数据,在
failover时可能会丢失数据。
应用场景:
HBase适合做大数据的持久存储,而Redis比较适合做缓存。如果数据丢失是不能容忍的,那就用只能用
HBase;如果需要一个高性能的环境,而且能够容忍一定的数据丢失,那完全可以考虑使用Redis。
两者的结合:
HBase可以用来做数据的固化,也就是数据存储,做这个他非常合适。Redis适合做cache。可以用
HBase+Redis实现数据仓库加缓存数据库,速度和扩展性都兼顾。
综上所述,HBase虽然是基于key=value方式存储的NoSQL,但是在当前的缓存需求上并不适用,所以缓
存解决方案中我们选择Redis作为最终方案。
6.2 容灾解决方案分析
 
 
6.2.1 FastDFS
FastDFS是阿里巴巴开源的一套轻量级,天生就是分布式设计的文件系统,FastDFS的源代码由C语言开发,
目前可运行在Linux,FreeBSD,Unix等类操作系统上, 它的作者是余庆(happyfish100),官方地址在github
上:https://github.com/happyfish100 。主要功能包括:文件存储,文件同步和文件访问,以及高容量
和负载平衡。主要解决了海量数据存储问题,特别适合以中小文件(建议范围:4KB < file_size
<500MB)为载体的在线服务。
FastDFS 系统有三个角色
:跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)。
  Tracker Server
跟踪服务器,主要做调度工作,起到均衡的作用;负责管理所有的
storage server和 group,每个 storage
在启动后会连接 Tracker,告知自己所属 group 等信息,并保持周期性心跳。
  Storage
Server:
存储服务器,主要提供容量和备份服务;以 group 为单位,每个 group 内可以有多台 storage server,数
据互为备份。
  Client客户端:
上传下载数据的服务器,也就是我们自己的项目所部署在的服务器。
6.2.2 HDFS
HDFS它的全称是:Hadoop 文件分发系统 ( Hadoop Distributed File System )。我们通常简称为HDFS。它
是是GFS的一种实现,类似于FAT32,NTFS,是一种文件格式,是底层的。它和HBase以及Hive都是
Apache软件基金会推出的存储解决方案。只不过,Hive与HBase的数据一般都存储在HDFS上。Hadoop
HDFS为他们提供了高可靠性的底层存储支持。
GFS(
Google File System google公司研发的一种高可用,可扩展,可伸缩的分布式文件系统)
 
 
在大数据领域,HDFS和HBase组成了生态系统的关键,它的容错率很高,即便是在系统崩溃的情况下,
也能够在节点之间快速传输数据。
为什么有了HDFS后,还要推出HBase呢?它们的区别是,HDFS最适于执行批次分析。然而,它最大的缺
点是无法执行实时分析,而实时分析是信息科技行业的标配。HBase能够处理大规模数据,但它不适于
批次分析,却可以向Hadoop实时地调用数据。
他们两个虽然都可以处理结构、半结构和非结构数据。但是因为
HDFS建立在旧的MapReduc
e框架上,所
以它缺乏内存引擎,数据分析速度较慢。而
HBase使用了内存引擎,大大提高了数据的读写
速度。同
时,HDFS执行的数据分析过程是透明的。
HBase与之相反,因为其结构基于NoSQL,它通过在不同的关
键字下进行排序而获取数据。
最后说一下H
ive,它不支持更改数据的操作,它基于的是数据仓库,提供静态数据的动态查询。其使用
类SQL语言,底层经过编译转为
MapReduce程序,在Hadoop上运行,数据存储在HDFS上。
下图展示了他们之间的关系:
6.2.3 两者的区别
我们说完了Hadoop家族的成员,接下来总结一下这两个DFS。
首先说FastDFS,它是开源的,轻量级,单纯的文件存取,节点不具备运算功能。FastDFS并不是通用的
文件系统,只能通过api来访问,文件标识分为两个部分:卷名和文件名,二者缺一不可。目前提供
c,java,php客户端。phtyon由第三方开发者提供。它的服务端有两个角色:跟踪器(
tracker)和存储节点
storage)。它不支持FUSE(容灾),也没有提供可移植操作系统接口(
POSIX)。
再来说HDFS,它也是开源的,且存储节点也是计算节点用以获得高效计算,是以低成本获得大数据(
PB
级)处理能力,主要解决并行计算中分布式存储数据的问题。同时,其单个数据文件通常很大,采用了
分块(切分)存储的方式。它的移植性和熔断机制都是FastDFS所不具备的。
通过对比,我们发现在容灾解决方案上,显然简单易用,且基于API访问的FastDFS更适合目前的需求,
而具有计算能力的HDFS在当前场景下,发挥不出它的优势。
 
 
7 Redis+FastDFS技术实现
7.1 安装Redis和FastDFS
7.1.1 安装Redis
1)下载镜像
2)安装容
7.1.2 安装FastDFS
1)下载镜像
2)安装tracker
3)安装storage
4)修改Nignx配置
7.2 缓存编码实现
docker
pull redis
1
docker run -di --name=redis -p 6379:6379 redis
1
docker pull morunchang/fastdfs
1
docker run -d --name=tracker --net=host morunchang/fastdfs sh tracker.sh
1
docker run -d --name=storage --net=host -e TRACKER_IP=192.168.32.132:22122 -e
GROUP_NAME=itheima morunchang/fastdfs sh storage.sh
1
#进入nignx:
docker exec -it storage /bin/bash
#编辑配置文件:
vim /etc/nginx/conf/nginx.conf
#替换如下内容:
location ~/M00 {
# 禁止缓存
add_header Cache-Control no-store;
#只需要加入上面这一行,其他的都是已经存在的
root /data/fast_data/data;
ngx_fastdfs_module;
}
#重启容器:
docker restart storage
 
 后面没用了-----------------------------------------------------------------------------------------------
  后面没用了-----------------------------------------------------------------------------------------------
 后面没用了-----------------------------------------------------------------------------------------------
7.2.1 导入坐标
7.2.2 编写缓存
策略接口
此处提供缓存策略接口的目的是为了日后扩展缓存解决方案时使用
7.2.3 编写Redis实现
<!--spring boot data redis坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
5
/**
* 缓
提供的缓存方法
* @
a
ut
ho
r
* @Company http://www.itheima.com
*/
public interface CacheStrategy {
/**
* 缓存操作的接口方法
* @param configInfo
*/
void saveCache(ConfigInfo configInfo);
/**
* 根据key获取缓存
* @param key
* @return 存入缓存的json对象
*/
ConfigInfo findCache(String key);
/**
* 删除指定缓存
* @param key
*/
void removeCache(String key);
/**
* 根据规则获取缓存中所有的key
* @param pattern
* @return
*/
Set<String> keys(String pattern);
}
 
/**
* 基于Redis缓存的存储
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
public class RedisCacheStorage implements CacheStrategy, InitializingBean {
 
 
 
@Autowired
private RedisTemplate redisTemplate;
/**
* 把配置信息缓存到redis
* 需要判断缓存中是否已经包含了指定key的配置信息
* 当包含时,先移除,再重新存入。
* @param configInfo
*/
@Over
ride
p
u
b
l
ic
vo
id saveCache(ConfigInfo configInfo) {
t
r
y
{
/
/
1
.判
ke
y
b
o
o
l
ea
n
co
n
ta
in
s
Ke
y
=
redisTemplate.hasKey(configInfo.getId());
if (containsKey) {
//2.如果redis中已经有此key的信息,则移除
redisTemplate.delete(configInfo.getId());
}
//3.把配置信息存入redis key是对象的唯一标识,value是
redisTemplate.opsForValue().set(configInfo.getId(),
KryoSerializeUtil.serialize(configInfo));
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 根据key,获取存入的配置信息
* @param key
* @return
*/
@Override
public ConfigInfo findCache(String key) {
//1.取出redis中的配置信息字节数组
byte[] bytes = (byte[])redisTemplate.opsForValue().get(key);
//2.转成ConfigInfo对象,并返回。
return KryoSerializeUtil.unserialize(bytes,ConfigInfo.class);
}
/**
* 根据key,移除指定缓存
* @param key
*/
@Override
public void removeCache(String key) {
redisTemplate.delete(key);
}
@Override
public void afterPropertiesSet() throws Exception {
redisTemplate.setKeySerializer(new StringRedisSerializer());
}
@Override
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
 
 
 
7.2.4 配置缓存服务器地址
7.2.5 改造Con
figInfoService
}
66
#它是在spring:下的节点
redis:
host: 192.168.32.131
1
2
3
/**
* 基
于redis,fastdfs和mysql的业务层实现
*
@
a
u
t
ho
r
*
@C
o
m
p
an
y
h
tt
p
:/
/www.itheima.com
*/
@Component
public class ConfigInfoService {
@Autowired
private CacheStrategy cacheStorageStrategy;
@Autowired
private ConfigInfoDao configInfoDao;
@Autowired
private IdWorkerUtil idWorkerUtil;
/**
* 保存配置信息,并存入redis缓存
* 处理容灾,存入FastDFS
* 记录轨迹
* @param configInfo
*/
public String saveConfig(ConfigInfo configInfo) {
//生成id
String id = String.valueOf(idWorkerUtil.nextId());
//给ConfigInfo的id赋值
configInfo.setId(id);
//填充时间
configInfo.setCreateTime(new Date());
configInfo.setUpdateTime(new Date());
//1.把配置存入mysql数据库中
configInfoDao.save(configInfo);
//2.同时把配置存入redis缓存中
cacheStorageStrategy.saveCache(configInfo);
//返回生成的id
return id;
}
/**
* 根据唯一标识,获取配置信息
* 如果缓存中没有,则从mysql中获取
 
 
 
* @param id
* @return
*/
public ConfigInfo findConfig(String id) {
//1.根据id从缓存中获取配置
ConfigInfo configInfo = cacheStorageStrategy.findCache(id);
//2.判断缓存中是否有数据
if(configInfo != null){
return configInfo;
}
t
ry{
/
/3
.当
m
y
sq
l数
c
o
nfi
g
In
fo
=
co
n
fi
g
I
nfo
D
ao
.g
e
tO
ne(id);
/
/4
.
i
f
(c
o
n
fi
gI
n
fo
!=
n
u
ll
){
cacheStorageStrategy.saveCache(configInfo);
}
}catch (Exception e){
e.printStackTrace();
}
return configInfo;
}
public Page<ConfigInfo> findPage(ConfigInfo condition, int page, int size) {
//1.创建查询条件
Specification<ConfigInfo> specification =
this.generatedCondition(condition);
//2.创建分页查询对象
Pageable pageable = PageRequest.of(page-1,size);
//3.返回查询结果
return configInfoDao.findAll(pageable);
}
/**
* 生成查询条件
* @param condition
* @return
*/
private Specification<ConfigInfo> generatedCondition(ConfigInfo condition){
return new Specification<ConfigInfo>() {
/**
* 拼装条件
* @param root 操作的实体封装
* @param query 查询对象
* @param cb 条件构建对象
* @return
*/
@Override
public Predicate toPredicate(Root<ConfigInfo> root, CriteriaQuery<?>
query, CriteriaBuilder cb) {
//定义条件的集合
List<Predicate> predicates = new ArrayList<>();
 
 
//1.判断是否提供了项目组别
if(!StringUtils.isEmpty(condition.getProjectGroup())) {
Predicate p1 = cb.equal(root.get("projectGroup"),
condition.getProjectGroup());
predicates.add(p1);
}
//2.判断是否提供了项目名称
if(!StringUtils.isEmpty(condition.getProjectName())){
Predicate p2 =
cb.equal
(
r
o
o
t
.
g
e
t
(
"
p
ro
je
c
t
N
am
e
"
)
,c
o
n
di
t
ion.getProjectName());
p
re
d
i
c
at
e
s
.a
d
d
(
p2
)
;
}
/
/3.判断是否提供了环境名称
i
f
(
!S
t
r
in
g
U
t
i
l
s
.
i
s
Empty(condition.getEnvName())){
P
re
d
i
c
a
te
p
3
=
cb.equal(root.get("envName"),condition.getEnvName());
predicates.add(p3);
}
//4.判断是否提供了集群id号
if(!StringUtils.isEmpty(condition.getClusterNumber())){
Predicate p3 =
cb.equal(root.get("clusterNumber"),condition.getClusterNumber());
predicates.add(p3);
}
return cb.and(predicates.toArray(new
Predicate[predicates.size()]));
}
};
}
/**
* 把配置项存入mysql数据库中
* @param configInfo
* @return
*/
public String createConfig(ConfigInfo configInfo) {
//生成id
String id = String.valueOf(idWorkerUtil.nextId());
//给ConfigInfo的id赋值
configInfo.setId(id);
configInfo.setProjectGroup("itheima");
//填充时间
configInfo.setCreateTime(new Date());
configInfo.setUpdateTime(new Date());
//设置配置详情为''
configInfo.setConfigDetail("");
//保存
configInfoPersistence.save(configInfo);
//返回生成的id
return id;
}
/**
* 添加配置
* @param id
* @param configName
* @param configValue
 
 
 
*/
public void addConfig(String id, String configName, String configValue) {
//1.取出配置项
ConfigInfo configInfo = configInfoPersistence.getOne(id);
//2.把detail转成properties
Properties properties =
PropertiesUtil.toProperties(configInfo.getConfigDetail());
//3.添加配置
properties.put(configName,configValue);
/
/4
.转
j
so
n
c
o
nfi
g
I
n
fo
c
o
nfi
g
In
fo
.
s
et
C
o
nfi
g
D
e
t
ai
l(PropertiesUtil.toJson(properties));//由于是持
久态对
/
/5
.
以手动更新
c
o
nfi
g
In
fo
Dao.save(configInfo);
/
/6
.清
cacheStorageStrategy.removeCache(id);
}
/**
* 更新配置
* @param configInfo
*/
public void updateConfig(ConfigInfo configInfo){
ConfigInfo dbConfigInfo =
configInfoPersistence.getOne(configInfo.getId());
dbConfigInfo.setConfigDetail(configInfo.getConfigDetail());
//1.更新mysql中的配置
configInfoDao.save(dbConfigInfo);//更新和删除是一个方法
//2.清除redis中的缓存
cacheStorageStrategy.removeCache(configInfo.getId());
}
/**
* 移除指定配置
* @param id
* @param configName
*/
public void removeConfig(String id, String configName) {
//1.取出配置项
ConfigInfo configInfo = configInfoPersistence.getOne(id);
//2.把detail转成properties
Properties properties =
PropertiesUtil.toProperties(configInfo.getConfigDetail());
//3.添加配置
properties.remove(configName);
//4.转成json给configInfo赋值
configInfo.setConfigDetail(PropertiesUtil.toJson(properties));//由于是持
久态对象,所以会更新
//5.也可以手动更新
configInfoDao.save(configInfo);
//6.清除缓存
cacheStorageStrategy.removeCache(id);
}
/**
* 移除整个配置
* @param configInfo
*/
 
 
 
7.3 FastDFS
编码实现
7.3.1 导入坐标
7.3.2 配置客户端对象
1)application.yml配置
2)注解配置client
public void removeAll(ConfigInfo configInfo) {
//1.根据id删除配置
configInfoDao.delete(configInfo);
//2.清除缓存
cacheStorageStrategy.removeCache(configInfo.getId());
}
}
209
210
211
212
213
214
215
<!--fa
st dfs坐标-->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.27.2</version>
</dependency>
1
2
3
4
5
6
#分布式文件系统FastDFS配置
fdfs:
so-timeout: 1500 #socket连接超时时长
connect-timeout: 600 #连接tracker服务器超时时长
reqHost: 192.168.32.131 #nginx访问地址
reqPort: 9033 #nginx访问端口
tracker-list: 192.168.32.131:22122 #TrackerList参数,支持多个,我这里只有一个,如果有
多个在下方加- x.x.x.x:port
pool:
jmx-enabled: false
#缩略图生成参数,后期可以作为图片上传功能
thumb-image:
width: 150
height: 150
 
/**
* 配置中心服务端自动配种导入
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Import({FdfsClientConfig.class, WebmvcInitConfig.class}) // 导入FastDFS-Client组
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) // 解决jmx重
复注册bean的问题
@ComponentScan("com.itheima")
public class ConfigServerAutoImport {
1
2
3
4
5
6
7
8
9
10
11
 
 
7.3.3 编写容灾策略接口
此处提供接口,也是为了日后扩展容灾机制。
7.3.4 编写FastDFS容灾实现
@Bean
public IdWorkerUtil createIdWorkerUtil(){
return new IdWorkerUtil(1,1);
}
}
12
13
14
15
16
/**
* 容灾
提供的容灾方法
* @a
ut
ho
r
* @
Company http://www.itheima.com
*/
public interface DisasterToleranceStrategy {
/**
* 把配置信息存入Dfs中
* @param configInfo
*/
void saveDfs(ConfigInfo configInfo);
/**
* 根据id从dfs中获取配置信息
* @param id
* @return
*/
ConfigInfo findDfs(String id);
/**
* 根据id删除文件
* @param id
*/
void removeDfs(String id);
}
 
/**
* FastDFS的配置存储方法
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
public class FastDFSStorage implements DisasterToleranceStrategy {
@Autowired
private FastFileStorageClient fastFileStorageClient;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void saveDfs(ConfigInfo configInfo) {
InputStream in = null;
try {
 
 
 
//1.取出redis中存入的信息
String value = (String) redisTemplate.opsForValue().get("fastdfs_" +
configInfo.getId());
//2.当不为空时,就表明需要删除fastdfs的内容
if(!StringUtils.isEmpty(value)) {
//3.切分成group和path
String group = value.split("_heimaconfig_")[0];
String path = value.split("_heimaconfig_")[1];
//4.使用group和path,移除fastdfs的内容
fastFileStorageClient.deleteFile(group, path);
}
/
/
1
.
信息转换成InputStream
i
n
=
n
ew
Byt
e
A
r
r
a
y
I
n
pu
t
St
re
am
(
Kr
yo
Se
r
ia
l
iz
eU
t
il
.s
e
ria
l
ize
(c
on
fi
gInfo));
/
/2
.定
fa
st
fd
s的
Long fileSize = new Long(in.available());
String suffix = "yml";
Set<MetaData> set = null;
//3.把新的配置信息写入到fastdfs中
//文件上传:参数一:传输文件内容的输入流;参数二:文件的size;参数三:文件扩展
名;参数四:描述文件的元数据;返回值:上传文件在存储节点的唯一标识(卷名+文件名)
StorePath storePath = fastFileStorageClient.uploadFile(in, fileSize,
suffix, set);
//4.把返回的信息写入到redis中,用于清理fastdfs中的配置文件
redisTemplate.opsForValue().set("fastdfs_" + configInfo.getId(),
storePath.getGroup() + "_heimaconfig_" + storePath.getPath());
}catch (Exception e){
e.printStackTrace();
}finally {
if(in != null){
try{
in.close();
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
/**
* 根据id从dfs中获取配置项
* @param id
* @return
*/
@Override
public ConfigInfo findDfs(String id) {
//1.定义返回值
ConfigInfo configInfo = null;
byte[] bytes = null;
try {
//2.根据id,从redis中获取对应的group 和 fileName信息
String value = (String)redisTemplate.opsForValue().get("fastdfs_" +
configInfo.getId());
String group = value.split("_heimaconfig_")[0];
String path = value.split("_heimaconfig_")[1];
//2.从FastDFS中获取配置项
//获取文件的字节数组。一共三个参数:参数一:文件处于存储节点的卷名;参数二:文件
在存储节点的文件名;参数三:下载的回调函数;返回值:文件内容的字节数组
 
 
 
7.3.5 异步消息处理容灾
1)创建RabbiitMQ服务
2)application.yml中添加配置
bytes = fastFileStorageClient.downloadFile(group, path, new
DownloadByteArray());
//3.判断是否有内容
if(bytes.length > 0) {
//4.加载配置信息
configInfo =
KryoSerializeUtil.unserialize(bytes,ConfigInfo.class);
}
//5.返回取到的配置项
re
tu
r
n
co
nfi
g
I
nfo
;
}
c
a
tc
h
(E
x
ce
pt
io
n
e
){
e
.
p
ri
n
tS
t
a
c
k
Trace();
re
t
u
r
n
n
u
l
l
;
}
}
/**
* 根据id,从fastdfs中删除文件
* @param id
*/
@Override
public void removeDfs(String id) {
//1.取出redis中存入的信息
String value = (String) redisTemplate.opsForValue().get("fastdfs_" +
id);
//2.当不为空时,就表明需要删除fastdfs的内容
if(!StringUtils.isEmpty(value)) {
//3.切分成group和path
String group = value.split("_heimaconfig_")[0];
String path = value.split("_heimaconfig_")[1];
//4.使用group和path,移除fastdfs的内容
fastFileStorageClient.deleteFile(group, path);
}
}
}
 
下载:
docker pull rabbitmq:management
安装:
docker run -di --name=rabbitmq -p 5671:5671 -p 5672:5672 -p 15671:15671 -p
15672:15672 -p 25672:25672 -p 4369:4369 rabbitmq:management
1
2
3
4
#它是spring节点下的
rabbitmq:
host: 192.168.32.131
1
2
3
 
 
3)编写监听器
7.3.6 改造ConfigInfoService
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
@RabbitListener(queues = "synchronizationdfs")
public cl
as
s
S
ynchronizationDFSListener {
@Auto
w
i
r
e
d
@
Q
u
a
l
i
fi
e
r
(
"fa
st
D
FS
S
t
o
r
a
g
e
")
p
r
iv
a
t
e
Di
s
a
ste
rT
o
l
e
r
an
c
e
S
t
rategy disasterToleranceStrategy;
/**
* 当监听到消息后,对dfs进行操作
* @param map
*/
@RabbitHandler
public void synchronizationDFS(Map<String,Object> map){
//1.取出map中的数据
ConfigInfo configInfo = (ConfigInfo)map.get("configInfo");
String operated = (String)map.get("operated");
//2.判断是替换还是删除
switch (operated){
case "change":
disasterToleranceStrategy.saveDfs(configInfo);
break;
case "del":
disasterToleranceStrategy.removeDfs(configInfo.getId());
break;
default:
break;
}
}
}
 
/**
* 基于redis,fastdfs和mysql的业务层实现
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
public class ConfigInfoService {
@Autowired
private CacheStrategy cacheStorageStrategy;
@Autowired
private DisasterToleranceStrategy disasterToleranceStrategy;
@Autowired
private ConfigInfoDao configInfoDao;
@Autowired
 
 
 
private IdWorkerUtil idWorkerUtil;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 保存配置信息,并存入redis缓存
* 处理容灾,存入FastDFS
* 记录轨迹
* @
param configInfo
*/
p
u
b
li
c
S
tr
i
ng saveConfig(ConfigInfo configInfo) {
/
/
i
d
S
t
ri
n
g
id
=
St
ri
n
g
.
valueOf(idWorkerUtil.nextId());
/
/给
C
o
n
fi
g
I
n
fo
i
d
configInfo.setId(id);
//填充时间
configInfo.setCreateTime(new Date());
configInfo.setUpdateTime(new Date());
//1.把配置存入mysql数据库中
configInfoDao.save(configInfo);
//2.同时把配置存入redis缓存中
cacheStorageStrategy.saveCache(configInfo);
//3.容灾机制:存入dfs中(fastdfs)
//disasterToleranceStrategy.saveDfs(configInfo);
//7.同步dfs
Map<String,Object> map = new HashMap<>();
map.put("configInfo",configInfo);
map.put("operated","change");
rabbitTemplate.convertAndSend("synchronizationdfs",map);
//4.记录轨迹
locosStrategy.saveLocos(configInfo);
//返回生成的id
return id;
}
/**
* 根据唯一标识,获取配置信息
* 如果缓存中没有,则从mysql中获取
* @param id
* @return
*/
public ConfigInfo findConfig(String id) {
//1.根据id从缓存中获取配置
ConfigInfo configInfo = cacheStorageStrategy.findCache(id);
//2.判断缓存中是否有数据
if(configInfo != null){
return configInfo;
}
try{
//3.当缓存中没有时,从mysql数据库获取
configInfo = configInfoDao.getOne(id);
//4.当缓存中没有时,存入缓存
if(configInfo != null){
cacheStorageStrategy.saveCache(configInfo);
}
}catch (Exception e){
 
 
 
//4.当mysql获取配置抛异常时,启动容灾机制,从文件系统中获取(如果mysql中没有数
据,并不是需要启动容灾机制)
configInfo = disasterToleranceStrategy.findDfs(id);
}
return configInfo;
}
p
u
b
li
c
P
ag
e<
C
on
fi
gInfo> findPage(ConfigInfo condition, int page, int size) {
/
/
1
.创
S
p
e
c
i
fi
ca
t
io
n
<C
o
n
fi
gI
nfo
>
s
pecification =
thi
s
.
g
e
n
e
r
at
e
d
Co
nd
i
t
io
n
(c
o
nd
it
ion
)
;
//2.创建分页查询对象
Pageable pageable = PageRequest.of(page-1,size);
//3.返回查询结果
return configInfoDao.findAll(pageable);
}
/**
* 生成查询条件
* @param condition
* @return
*/
private Specification<ConfigInfo> generatedCondition(ConfigInfo condition){
return new Specification<ConfigInfo>() {
/**
* 拼装条件
* @param root 操作的实体封装
* @param query 查询对象
* @param cb 条件构建对象
* @return
*/
@Override
public Predicate toPredicate(Root<ConfigInfo> root, CriteriaQuery<?>
query, CriteriaBuilder cb) {
//定义条件的集合
List<Predicate> predicates = new ArrayList<>();
//1.判断是否提供了项目组别
if(!StringUtils.isEmpty(condition.getProjectGroup())) {
Predicate p1 = cb.equal(root.get("projectGroup"),
condition.getProjectGroup());
predicates.add(p1);
}
//2.判断是否提供了项目名称
if(!StringUtils.isEmpty(condition.getProjectName())){
Predicate p2 =
cb.equal(root.get("projectName"),condition.getProjectName());
predicates.add(p2);
}
//3.判断是否提供了环境名称
if(!StringUtils.isEmpty(condition.getEnvName())){
Predicate p3 =
cb.equal(root.get("envName"),condition.getEnvName());
 
 
 
predicates.add(p3);
}
//4.判断是否提供了集群id号
if(!StringUtils.isEmpty(condition.getClusterNumber())){
Predicate p3 =
cb.equal(root.get("clusterNumber"),condition.getClusterNumber());
predicates.add(p3);
}
re
t
ur
n
c
b
.a
n
d
(p
redicates.toArray(new
Predicat
e
[
p
r
e
d
i
ca
te
s
.s
i
ze
(
)
]
))
;
}
}
;
}
/**
* 把配置项存入mysql数据库中
* @param configInfo
* @return
*/
public String createConfig(ConfigInfo configInfo) {
//生成id
String id = String.valueOf(idWorkerUtil.nextId());
//给ConfigInfo的id赋值
configInfo.setId(id);
configInfo.setProjectGroup("itheima");
//填充时间
configInfo.setCreateTime(new Date());
configInfo.setUpdateTime(new Date());
//设置配置详情为''
configInfo.setConfigDetail("");
//保存
configInfoDao.save(configInfo);
//返回生成的id
return id;
}
/**
* 添加配置
* @param id
* @param configName
* @param configValue
*/
public void addConfig(String id, String configName, String configValue) {
//1.取出配置项
ConfigInfo configInfo = configInfoPersistence.getOne(id);
//2.把detail转成properties
Properties properties =
PropertiesUtil.toProperties(configInfo.getConfigDetail());
//3.添加配置
properties.put(configName,configValue);
//4.转成json给configInfo赋值
configInfo.setConfigDetail(PropertiesUtil.toJson(properties));//由于是持
久态对象,所以会更新
//5.也可以手动更新
configInfoDao.save(configInfo);
//6.清除缓存
cacheStorageStrategy.removeCache(id);
 
 
 
//7.同步dfs
Map<String,Object> map = new HashMap<>();
map.put("configInfo",configInfo);
map.put("operated","change");
rabbitTemplate.convertAndSend("synchronizationdfs",map);
}
/**
* 更新配置
* @
param configInfo
*/
p
u
b
l
i
c
v
o
id
up
d
at
eC
o
nfi
g
(C
o
nfi
gInfo configInfo){
C
o
n
fi
g
I
nfo
d
bC
o
nfi
g
In
fo
=
con
fi
g
I
n
fo
P
e
r
si
st
e
nce
.
ge
tO
n
e
(c
o
n
fi
g
I
n
fo
.g
et
I
d
())
;
d
bC
o
n
fi
g
I
nfo
.s
e
tC
o
n
fi
g
De
t
a
i
l
(c
on
fi
g
I
nfo
.g
etConfigDetail());
//1.更新mysql中的配置
configInfoDao.save(dbConfigInfo);//更新和删除是一个方法
//2.清除redis中的缓存
cacheStorageStrategy.removeCache(configInfo.getId());
//7.同步dfs
Map<String,Object> map = new HashMap<>();
map.put("configInfo",dbConfigInfo);
map.put("operated","change");
rabbitTemplate.convertAndSend("synchronizationdfs",map);
}
/**
* 移除指定配置
* @param id
* @param configName
*/
public void removeConfig(String id, String configName) {
//1.取出配置项
ConfigInfo configInfo = configInfoPersistence.getOne(id);
//2.把detail转成properties
Properties properties =
PropertiesUtil.toProperties(configInfo.getConfigDetail());
//3.添加配置
properties.remove(configName);
//4.转成json给configInfo赋值
configInfo.setConfigDetail(PropertiesUtil.toJson(properties));//由于是持
久态对象,所以会更新
//5.也可以手动更新
configInfoDao.save(configInfo);
//6.清除缓存
cacheStorageStrategy.removeCache(id);
//7.同步dfs
Map<String,Object> map = new HashMap<>();
map.put("configInfo",configInfo);
map.put("operated","change");
rabbitTemplate.convertAndSend("synchronizationdfs",map);
}
/**
* 移除整个配置
* @param configInfo
*/
public void removeAll(ConfigInfo configInfo) {
 
 
 
8 项目
扩展动态配置缓存和容灾的实现方案
8.1 Conditional注解
8.1.1 注解说明
8.1.2 改造applicaiton.yml添加配置
8.2 Condition实现
//1.根据id删除配置
configInfoDao.delete(configInfo);
//2.清除缓存
cacheStorageStrategy.removeCache(configInfo.getId());
//3.删除文件的操作写入消息
Map<String,Object> map = new HashMap<>();
map.put("configInfo",configInfo);
map.put("operated","del");
rabbitTemplate.convertAndSend("synchronizationdfs",map);
}
}
238
239
240
241
242
243
244
245
246
247
248
249
/**
* 作用:
* 它的作用是根据条件选择注入的bean对象。
**/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* 属性:
* 用于提供一个Condition接口的实现类,实现类中需要编写具体代码实现注入的条件。
*/
Class<? extends Condition>[] value();
}
1
2
#顶头开始写起
service:
disaster:
tolerance: FastDFS
cache: redis
1
2
3
4
5
 
 
8.2.1 Redis缓存的Condition实现
8.2.2 FastDFS的Condition实现
8.3.3 改造缓存和容灾策略的具体实现
1)Redis缓存策略改造
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class RedisCondition implements Condition {
@Over
r
id
e
publi
c
bo
olean matches(ConditionContext context, AnnotatedTypeMetadata
metadata) {
E
nv
i
ro
nm
e
nt
e
nv
i
ro
n
me
n
t
=
c
o
n
t
ex
t.
g
e
t
E
n
v
i
ro
n
me
n
t
()
;
S
t
r
in
g
c
ac
h
eS
e
rv
i
ce
=
e
nv
i
ro
n
m
e
nt
.
g
e
t
P
r
op
e
rt
y
(
"
se
rv
ice.cache");
i
f
(
"
r
ed
is
"
.e
q
u
al
sIgnoreCase(cacheService)){
r
e
tu
rn
t
r
ue
;
}
return false;
}
}
 
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class FastDFSCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata
metadata) {
Environment environment = context.getEnvironment();
String cacheService =
environment.getProperty("service.disaster.tolerance");
if("FastDFS".equalsIgnoreCase(cacheService)){
return true;
}
return false;
}
}
 
/**
* 基于Redis缓存的存储
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
@Conditional(RedisCondition.class)
public class RedisCacheStorage implements CacheStrategy, InitializingBean {
//其余代码略
}
1
2
3
4
5
6
7
8
9
10
 
 
2)FastDFS容灾策略改造
####
/**
* FastDFS的配置存储方法
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
@Conditio
n
a
l
(
F
a
s
t
D
F
SC
o
nd
i
t
i
on
.c
l
a
s
s
)
public cl
a
s
s
F
a
s
t
D
F
S
S
to
ra
g
e
im
pl
e
m
e
n
ts DisasterToleranceStrategy {
//其余
 
 
 
第3章 配置推送和推送轨迹
1 配置推送技术分析
1.1 推送的机制分析
配置变更后,把最新配置推送到下游业务微服务是配置中心服务的另一个核心问题。用何种方式推送,
推送的效率和数据安全如何保证是我们需要解决的具体问题。
首先我们来分析推送的模式。它其实就分为两种,第一种推模式。第二种拉模式。
1.1.1 推模式
顾名思义,就是由服务端主动把更新的配置推送到具体业务微服务。
它的优势是,配置的实时性高,同时具体业务微服务的操作少。首先实时性,是因为当我们配置变更之
后,服务端马上通知客户端有新的配置了,并且把新的配置送到客户端。其次具体业务微服务是无需任
何操作的,它只需要等待服务端把最新的配置送过来即可。
它的弊端是,消耗资源较大。因为我们是基于HTTP协议的访问。而HTTP协议是客户浏览器和服务器之间
的一问一答的规则,那么就是说必须有问有答,而且要先问后答。所以,当客户端没有发送请求时,服
务端是不能响应的。那么要想保证有新配置时,服务端马上能响应给客户端的话,就需要客户端不停地
像服务端发送请求,这就是我们说的轮询。此时,同学们可以想一下,如果客户端不停的向服务端发送
请求的话,是不是会造成资源的开销,这也就是我们说的,资源消耗大的原因。
1.1.2 拉模式
顾名思义,就是由客户端主动发送请求,把最新的配置拉去下来。
它的优势是,对客户端微服务的资源开销较小,因为不再需要轮询配置中心服务端了。
它的弊端是,实时性没有推模式高。同时,要求使用者主动发送请求,想服务端获取最新配置。所以,
操作繁琐度比推模式高。
1.1.3 推模式和拉模式的利弊分析
在1.1.1小节,我们介绍了更新机制分为推模式和拉模式。其中拉模式,是需要人为干预的,也就是说当
配置发生变化之后,需要人为发送请求,向服务端索取最新配置内容。所以相对来说,处理方式比较单
一。而推模式,我们分析了,它并不能真正做到服务端主动推送,只是利用HTTP协议的机制,做到看起
来像服务端主动推送。由此,我们就需要分析这个像的程度,即:长连接,短连接,长轮询,短轮询中
选择哪个来实现。
1) 长连接和短连接
HTTP长连接(long connection)与短连接(short connection)本质上是TCP长连接和短连接:短
连接是指在一次HTTP请求和响应之后立即关闭本次TCP连接,下次请求响应重建一个新的TCP连接;而长连接是
指请求响应之后并不立即关闭本次TCP连接,下次请求响应继续重用该TCP连接。HTTP/1.0默认短连接,
HTTP/1.1起默认长连接,长连接通过请求头Connection: keep-alive启用长连接、通过Keep-Alive:
timeout=20设置长连接的超时时间(秒)。
1
 
 
 
相同点
区别
都是基于
T
C
P
连接,并无本质不同;由
HT
T
P
请求头Co
n
n
e
ction: keep-alive控制是否长
/
连接。
1、使用次数:仅使用一次;
2、关闭时机:一次HTTP请求响应后立即
关闭该
TCP连接。
1、使用次数:重用多次;
2
、关闭时机:超时后才关闭该
TCP连接,
Keep-Alive: timeout=20控制超时时间
(秒)。
相同点
区别
都是基于HTTP连接,都将
重复发送相同请求。
1、请求发送频率:浏览器端收到HTTP响应后间隔一段自
定义时间后重复发起相同HTTP请求;
2、服务器端处理机制:无论是否有数据都立即响应;
3、特点:获取数据不实时,通过浏览器端脚本即可实
现。
1、请求发送频率:浏览器端收到HTTP响应后立即重复发
起相同HTTP请求;
2、服务器端处理机制:有数据时立即响应,无数据时等
待数据或直到超时;
3、特点:获取数据比较实时,服务器端需要较多资源以
维持众多长轮询。
2) 长轮询和短轮询
HTTP长轮询(
long polling)是指服务端收到请求后若有数据立即返回,若无数据则保持到有数据或一段
时间后超时,浏览器收到响应后立即重新发送相同的请求;HTTP短轮询(
short polling)是指服务端收
到请求后无论是否有数据都立即返回,浏览器收到响应后间隔一段时间后重新发送相同的请求。轮询建
立在连接基础上,轮询是长是短与连接是长是短无关。
1.2 推送的效率
配置中心的执行效率快慢,也是该系统好坏的衡量标准之一。而执行效率中包括配置上传和更新后的存
储效率,配置推送的处理效率两个方面。第一个方面,存储相关的问题,我们上一篇章已经分析过了。
而第二个方面,处理效率又有两部分组成。第一部分是处理速度,第二部分是吞吐量,即并发访问的极
限处理量。
针对第一部分,处理速度上,我们采用的不是Spring Cloud的endpoint发布方式,而是直接提供Controller
访问(nacos和apollo都是controller的方式),只是访问是不是由使用者访问的,而是通过HttpClient,并且
配合RestAPI获取,传输json格式数据。而第二部分,高并发访问问题,我们可以通过异步获取的方式,
即业务微服务在获取配置需求时,不直接访问配置中心服务端,而是通过消息队列来解耦,从而降低配
置中心服务端的访问压力。
 
 
2 配置推送的技术实现
2.1 基于MQ的拉取功能实现
2.1.1 编写服务端处理请求的方法
2.1.2 创建hacm_client模块并导入坐标
//-----------------
此方法是添加在hacm_server的ConfigInfoController中-----------------
-----
/**
*
配置的方法
*
@
p
ar
am
id
* @return
*/
@RequestMapping(value = "refreshConfig",method = RequestMethod.POST)
public @ResponseBody String refreshConfig(String id){
try {
//1.根据id查找配置对象
ConfigInfo configInfo = processor.findConfig(id);
//2.判断配置对象是否存在
if(configInfo == null){
throw new IllegalStateException("配置不存在");
}
//3.取出配置中的detail信息
String configDetail = configInfo.getConfigDetail();
//4.把它响应给客户端
return configDetail;
}catch (Exception e){
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
 
2.1.3 编写导入器注解
<!--配置中心依赖之消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--fast json坐标-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<!--apache commons的坐标-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ContextRefresherListener.class, ContextRefresher.class,
ContextRefreshController.class})
public @interface EnableConfigClient {
}
1
2
3
4
5
6
7
8
9
10
 
 
2.1.4 编写接收请求的控制器
2.1.5 编写处理消息的监听器
/**
* 接收刷新配置的请求控制器
*
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@RestCont
ro
ll
e
r
@RequestMa
pp
i
ng
("/config")
public class ContextRefreshController {
@A
utowired
private RabbitTemplate rabbitTemplate;
@RequestMapping(value = "/refresh", method = RequestMethod.POST)
public void refresh() {
rabbitTemplate.convertAndSend("heimaconfig", "refresh");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
@RabbitListener(queues = "heimaconfig")
public class ContextRefresherListener {
@Autowired
private ContextRefresher contextRefresher;
/**
* 读取RabbitMQ指定队列中的消息
* 根据消息的内容读取配置
* @throws Exception
*/
@RabbitHandler
public void onMessage(String message)throws Exception{
try {
Properties properties = PullConfigUtil.findConfig();
contextRefresher.refresh(properties);
}catch (Exception e){
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
 
2.1.6 编写PullConfigUtil
/**
* 拉取配置中心服务端的配置项
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class PullConfigUtil {
privat
e static String configHost;
private static String configPort;
pr
ivate static String configInfoId
s
t
a
t
ic
{
I
np
utStream in =
PullConfigUtil.class.getResourceAsStream("/bootstrap.yml");
YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new
YamlPropertiesFactoryBean();
yamlPropertiesFactoryBean.setResources(new InputStreamResource(in));
Properties properties = yamlPropertiesFactoryBean.getObject();
configInfoId = properties.getProperty("config.id");
configHost = properties.getProperty("config.host");
configPort = properties.getProperty("config.port");
}
/**
* 通过利用RestTemplate发送请求
* 前往ConfigServer获取配置信息
*/
public static Properties findConfig(){
Properties properties = null;
try{
//准备信息
//headers
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("api-version", "1.0");
requestHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
//body
MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>
();
requestBody.add("id",configInfoId);
//HttpEntity
HttpEntity<MultiValueMap> requestEntity = new
HttpEntity<MultiValueMap>(requestBody, requestHeaders);
//创建对象
RestTemplate restTemplate = new RestTemplate();
String json =
restTemplate.postForObject("http://"+configHost+":"+configPort+"/config/refreshCon
fig.do",requestEntity,String.class);
properties = JSON.parseObject(json,Properties.class);
}catch (Exception e){
e.printStackTrace();
}
return properties;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 
 
2.1.7 编写ContextRefresher
}
50
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.Scope;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import o
r
g
.
s
p
r
i
n
g
fr
a
m
e
w
o
r
k
.
b
o
o
t
.
b
u
i
l
d
e
r.
S
p
r
ing
A
p
p
l
ic
at
io
n
B
u
i
ld
e
r
;
import o
r
g
.
s
p
r
i
n
g
fr
a
m
e
w
o
r
k
.
b
o
o
t
.c
o
n
t
e
x
t
.
co
n
fi
g
.C
o
n
fi
g
Fi
le
A
p
p
l
i
ca
t
ionListener;
impor
t
o
r
g
.
s
p
r
i
n
g
fr
a
m
e
w
o
r
k
.
c
l
o
u
d
.
bo
o
t
s
t
ra
p
.
Bo
o
t
s
t
ra
pA
p
p
l
ic
at
io
n
L
i
st
e
n
e
r
;
impor
t
o
r
g
.
s
p
r
i
n
g
fr
a
m
e
w
o
r
k
.
c
l
o
u
d
.c
o
n
te
x
t
.e
n
v
i
ro
n
m
e
nt
.
En
v
i
ro
nm
en
t
C
h
a
ng
e
E
v
ent;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
imp
o
r
t
o
r
g
.
s
p
r
i
n
g
fr
a
m
e
w
o
r
k
.
c
o
n
t
e
x
t
.
C
o
n
fi
g
u
r
a
bl
eA
p
pl
ic
a
t
io
nC
o
ntext;
imp
o
r
t
o
r
g
.
s
p
r
i
n
g
fr
a
m
e
w
o
r
k
.
c
o
n
t
e
x
t
.
a
n
no
t
at
i
o
n
.C
on
fi
g
u
r
at
io
n
;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.*;
import org.springframework.web.context.support.StandardServletEnvironment;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
public class ContextRefresher {
@Autowired
private ConfigurableApplicationContext context;
private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs";
private Set<String> standardSources = new HashSet<>(
Arrays.asList(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,
"configurationProperties"));
private static final String[] DEFAULT_PROPERTY_SOURCES = new String[] {
// order matters, if cli args aren't first, things get messy
CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
"defaultProperties" };
/**
* 刷新配置
*/
public synchronized Set<String> refresh(Map after){
RefreshScope scope = (RefreshScope)context.getBean(Scope.class);
//获取目前系统的配置
Map<String, Object> before = extract(
context.getEnvironment().getPropertySources());
//获取最新配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 
 
addConfigFilesToEnvironment(context);
//对比目前系统配置和最新配置,返回修改后的属性
Set<String> keys = changes(before,after).keySet();
//通知系统配置变更
context.publishEvent(new EnvironmentChangeEvent(context,keys));
//对应的bean刷新
scope.refreshAll();
return keys;
}
p
r
iv
a
te void addConfigFilesToEnvironment(ConfigurableApplicationContext
conte
x
t
)
{
ConfigurableApplicationContext capture = null;
try {
StandardEnvironment environment = copyEnvironment(
context.getEnvironment());
SpringApplicationBuilder builder = new
SpringApplicationBuilder(Empty.class)
.bannerMode(Banner.Mode.OFF).web(WebApplicationType.NONE)
.environment(environment);
// Just the listeners that affect the environment (e.g. excluding
logging
// listener because it has side effects)
builder.application()
.setListeners(Arrays.asList(new
BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
capture = builder.run();
if
(environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
}
MutablePropertySources target = context.getEnvironment()
.getPropertySources();
String targetName = null;
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
if (!this.standardSources.contains(name)) {
if (target.contains(name)) {
target.replace(name, source);
}
else {
if (targetName != null) {
target.addAfter(targetName, source);
}
else {
// targetName was null so we are at the start of the
list
target.addFirst(source);
targetName = name;
}
}
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
 
 
}
}
}
finally {
ConfigurableApplicationContext closeable = capture;
while (closeable != null) {
try {
closeable.close();
}
c
a
t
ch
(
Ex
c
ep
ti
on e) {
/
/
I
g
no
re
;
}
i
f (closeable.getParent() instanceof
Con
fi
g
u
r
a
b
l
e
A
p
p
l
i
c
a
t
io
nC
o
nt
e
x
t
)
{
c
lo
s
ea
b
l
e
=
(ConfigurableApplicationContext)
closeable.getParent();
}
else {
break;
}
}
}
}
private StandardEnvironment copyEnvironment(ConfigurableEnvironment input) {
StandardEnvironment environment = new StandardEnvironment();
MutablePropertySources capturedPropertySources =
environment.getPropertySources();
// Only copy the default property source(s) and the profiles over from
the main
// environment (everything else should be pristine, just like it was on
startup).
for (String name : DEFAULT_PROPERTY_SOURCES) {
if (input.getPropertySources().contains(name)) {
if (capturedPropertySources.contains(name)) {
capturedPropertySources.replace(name,
input.getPropertySources().get(name));
}
else {
capturedPropertySources.addLast(input.getPropertySources().get(name));
}
}
}
environment.setActiveProfiles(input.getActiveProfiles());
environment.setDefaultProfiles(input.getDefaultProfiles());
Map<String, Object> map = new HashMap<String, Object>();
map.put("spring.jmx.enabled", false);
map.put("spring.main.sources", "");
capturedPropertySources
.addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE,
map));
return environment;
}
private Map<String, Object> changes(Map<String, Object> before,
Map<String, Object> after) {
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
 
 
Map<String, Object> result = new HashMap<String, Object>();
for (String key : before.keySet()) {
if (!after.containsKey(key)) {
result.put(key, null);
}
else if (!equal(before.get(key), after.get(key))) {
result.put(key, after.get(key));
}
}
fo
r
(S
tr
i
n
g
ke
y
:
a
ft
e
r
.
ke
y
S
e
t
(
)
)
{
i
f
(
!b
e
fo
r
e
.c
o
nt
a
in
s
K
e
y
(
ke
y
)
)
{
result.put(key, after.get(key));
}
}
return result;
}
private boolean equal(Object one, Object two) {
if (one == null && two == null) {
return true;
}
if (one == null || two == null) {
return false;
}
return one.equals(two);
}
private Map<String, Object> extract(MutablePropertySources propertySources)
{
Map<String, Object> result = new HashMap<String, Object>();
List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
for (PropertySource<?> source : propertySources) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
if (!this.standardSources.contains(source.getName())) {
extract(source, result);
}
}
return result;
}
private void extract(PropertySource<?> parent, Map<String, Object> result) {
if (parent instanceof CompositePropertySource) {
try {
List<PropertySource<?>> sources = new ArrayList<PropertySource<?
>>();
for (PropertySource<?> source : ((CompositePropertySource)
parent)
.getPropertySources()) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
extract(source, result);
}
}
catch (Exception e) {
return;
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
 
 
2.1.8 编写EnvironmentPostProcessor的实现
在工程的resources目录下创建一个META-INF目录,并创建一个spring.factories文件,添加如下配置:
}
}
else if (parent instanceof EnumerablePropertySource) {
for (String key : ((EnumerablePropertySource<?>)
parent).getPropertyNames()) {
result.put(key, parent.getProperty(key));
}
}
}
@Confi
guration
p
rotected static class Empty {
}
}
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class EnvironmentConfigPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
System.out.println("begin....");
//1.获取配置中心服务的配置信息
Properties properties = PullConfigUtil.findConfig();
//2.创建MapPropertySource
PropertySource mapPropertySource = new
PropertiesPropertySource("applicationConfig: [classpath:/application.yml]",
properties);
//3.获取配置项
MutablePropertySources mutablePropertySources =
environment.getPropertySources();
//4.判断是否有此配置项
PropertySource propertySource =
mutablePropertySources.get("applicationConfig: [classpath:/application.yml]");
if (propertySource == null) {
//添加配置
environment.getPropertySources().addLast(mapPropertySource);
;
} else {
//替换配置
environment.getPropertySources().replace("applicationConfig:
[classpath:/application.yml]", mapPropertySource);
}
application.setEnvironment(environment);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 
 
2.1.9 改造业务微服务的配置文件
2.2 基于监听器的长轮询功能实现
org.springframework.boot.env.EnvironmentPostProcessor=com.itheima.context.processo
r.EnvironmentConfigPostProcessor
1
server:
port: 9001 #基础微服务的端口号
project:
id: 1263
163507453595648 #在黑马配置平台的配置唯一编号
config:
host
:
1
2
7
.0
.
0
.
1
#
的服
地址
port
:
1
0
02
0
#
1
2
3
4
5
6
7
/**
* 长轮询监听器
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
@EnableScheduling
public class LongLoopingListener {
@Autowired
private ContextRefresher contextRefresher;
@Autowired
private ConfigurableEnvironment configurableEnvironment;
/**
* 检查配置更新
*/
@Scheduled(fixedRate = 10l)
public void synchronizedServiceConfig(){
//1.获取服务端配置信息
Properties properties = PullConfigUtil.findConfig();
//2.比较
boolean checked = this.checkConfigInfo(properties);
//3.判断
if(checked){
contextRefresher.refresh(properties);
}
}
/**
* 判断本地配置和服务端配置是否一致
* @param properties
* @return
*/
private boolean checkConfigInfo(Properties properties){
//1.取出本地的配置信息
MutablePropertySources mutablePropertySources =
configurableEnvironment.getPropertySources();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 
 
2.3 客户端生成
2.3.1 生成jar包
直接使用Maven的package命令即可。也可以在IDEA中直接点击按钮运行。
1)使用命令行
前往pom.xml所在的目录:
//2.根据Key取出value
PropertySource propertySource =
mutablePropertySources.get("applicationConfig: [classpath:/application.yml]");
//3.遍历Properties
Enumeration keys = properties.keys();
while (keys.hasMoreElements()){
//取出服务器端的key和value
String key = (String)keys.nextElement();
String value = (String)properties.get(key);
/
/判
p
r
o
p
e
r
ty
S
o
u
r
c
e
k
e
y
i
f
(p
ro
p
er
ty
S
o
u
r
c
e
.c
o
n
t
a
i
n
s
Pr
op
e
rt
y
(
key)){
/
/包
v
a
l
u
e
S
t
ri
ng
p
r
op
e
rt
yS
o
u
rc
e
V
alue =
(Str
i
n
g
)
p
r
o
p
e
r
t
yS
o
u
r
ce
.
ge
t
P
r
op
er
ty
(
ke
y
)
;
/
/
v
a
lu
e
v
a
l
ue不等时,直接返回true,表示需要更新
if(value.equals(propertySourceValue)){
return true;
}
}else {
//当服务器端有key,而本地没有key时,则表示服务端新加了配置,也需要更新
return true;
}
}
//只有在服务端的key本地都有,且服务端的值和本地一致时,才不需要更新
return false;
}
}
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
 
 
在cmd命令行窗口输入如下命令:
2.3.2 安装到maven本地仓库
前往jar包所在目录:
mvn package
1
 
 
在cmd窗口运行如下命令:
前往本地仓库查看是否已经安装好:
mvn install:install-file -DgroupId=com.itheima -DartifactId=hacm_client -
Dversion=1.0-SNAPSHOT -Dpackaging=jar -Dfile=hacm_client-1.0-SNAPSHOT.jar
1
 
 
3 轨迹和
日志转移功能实现
1.1 推送轨迹
1.1.1 需求分析
推送轨迹就是,当我们配置更新后,把最新的配置推送到客户端时,需要记录更新的轨迹内容。它很类
似于更新日志。特点就是,每次配置详情有所变化时,都需要记录此信息。
1.1.2 技术选型
在记录轨迹的功能上,我们的解决方案可选择性非常广泛。例如,mysql,redis,HBASE,mongodb等
等。此处我们选择了Mongodb作为轨迹记录的解决方案。
mongodb的优势在于,它的存储可以是非格式化话,且是No-SQL数据库,比关系型数据库的效率高一
些。并且,对轨迹的记录操作可能会较为频繁,所以不希望牺牲redis和mysql的性能,让他们专注缓存和
持久化配置信息,而轨迹相较于我们的核心配置信息来说,没它重要。
1.1.3 Docker安装Mongodb
1.1.4 记录轨迹功能实现
1)导入坐标
下载:
docker pull mongo:3.2.10
安装:
docker run -di --name=mongo -p 27017:27017 mongo:3.2.10
1
2
3
4
<!--spring boot data mongodb坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
1
2
3
4
5
 
 
2)编写application.yml的配置
3)编写实体类
#放在spring节点下
data:
mongodb:
host: 192.168.32.132
database: locos
1
2
3
4
5
/**
* @a
u
t
ho
r
* @C
o
m
p
an
y
h
tt
p
:/
/www.itheima.com
*/
publ
ic class ConfigLocos implements Serializable {
@Id
private String id;
private String configInfoId;
private String envName;
private String projectName;
private String clusterNumber;
private String serviceName;
private String configDetail;
private String userId;
private String projectGroup;
private Long createTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getConfigInfoId() {
return configInfoId;
}
public void setConfigInfoId(String configInfoId) {
this.configInfoId = configInfoId;
}
public String getEnvName() {
return envName;
}
public void setEnvName(String envName) {
this.envName = envName;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 
 
this.projectName = projectName;
}
public String getClusterNumber() {
return clusterNumber;
}
public void setClusterNumber(String clusterNumber) {
this.clusterNumber = clusterNumber;
}
p
u
b
li
c
S
t
r
in
g
ge
t
S
e
r
v
ic
eName() {
re
t
u
rn
se
rv
i
c
e
Na
m
e
;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
public String getConfigDetail() {
return configDetail;
}
public void setConfigDetail(String configDetail) {
this.configDetail = configDetail;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getProjectGroup() {
return projectGroup;
}
public void setProjectGroup(String projectGroup) {
this.projectGroup = projectGroup;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
}
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
 
 
4)编写持久层
5)编写业务
6)改造ConfigInfoService
针对ConfigInfoService的方法加入记录轨迹功能。其中添加配置项,保存配置信息,删除配置项和更新配
置项需要记录轨迹,其余方法无须记录轨迹
1.1.5 轨迹展示功能实现
1)编写ConfigLocosDao
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public interface ConfigLocosDao extends MongoRepository<ConfigLocos,String> {
}
1
2
3
4
5
6
7
8
/**
* @
author 黑马程序员
* @Company http://www.itheima.com
*/
@Service
public class ConfigLocosService {
@Autowired
private ConfigLocosDao configLocosDao;
/**
* 保存轨迹
* @param configInfo
*/
public void saveLocos(ConfigInfo configInfo){
//1.创建对象
ConfigLocos configLocos = new ConfigLocos();
//2.从配置信息对象中复制数据到轨迹对象,并且忽略id信息
BeanUtils.copyProperties(configInfo,configLocos,new String[]{"id"});
//3.处理id的数据
configLocos.setConfigInfoId(configInfo.getId());
configLocos.setId(String.valueOf(idWorkerUtil.nextId()));
//4.保存
configLocosDao.save(configLocos);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
 
2)编写ConfigL
ocosService
3)编写表现层
/**
* 分页根据配置信息id查询轨迹列表
* @param configInfoId
* @param pageable
* @return
*/
Page<ConfigLocos> findByConfigInfoId(String configInfoId, Pageable pageable);
1
2
3
4
5
6
7
/**
*
id分
查询
轨迹
*
@
p
ar
a
m
co
nfi
gI
nfo
Id
*
@
p
a
r
a
m
p
a
g
e
*
@
p
a
r
a
m
s
i
z
e
* @return
*/
public Page<ConfigLocos> findPage(String configInfoId, int page, int size){
//1.创建分页对象
Pageable pageable = PageRequest.of(page-1,size);
//2.分页查询
Page<ConfigLocos> configLocosPage =
configLocosDao.findByConfigInfoId(configInfoId,pageable);
//3.返回
return configLocosPage;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Controller
@RequestMapping("/config")
public class ConfigLocosController {
@Autowired
private ConfigLocosService configLocosService;
@Autowired
private HttpServletRequest request;
@RequestMapping("/findLocos")
public String findLocos(String id,int page,int size){
//1.根据ConfigInfo的id,查询轨迹列表
Page<ConfigLocos> configLocosPage =
configLocosService.findPage(id,page,size);
//2.创建分页结果对象
PageResult pageResult = new
PageResult(configLocosPage.getTotalElements(),configLocosPage.getContent(),page,s
ize);
//3.存入请求域
request.setAttribute("page",pageResult);
//4.前往轨迹列表页面
return "config/locos";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
 
4)编写页面
1.2 转移日志文件
1.2.1 需求
分析
mongodb的推送轨迹保存,随着项目的微服务过多,轨迹变更次数过多,它的体量会越来越大。所以,
我们需要定期对轨迹信息进行转移,把不常用的轨迹信息转移到磁盘存储,从而节省mongodb的空间。
1.2.2 技术选型
这里的技术选型就比较简单了,我们使用了apache的commons-io包中的文件工具类,直接把mongodb中
的数据写到文件中即可。
1.2.3 功能实现
1)编写ConfigLocosDao
2)编写定时任务调度类
}
26
<!--在list.jsp的按钮栏加入下面这行-->
<button type="button" class="btn bg-olive btn-xs"
onclick='location.href="${ctx}/config/findLocos.do?id=${o.id}"'>轨迹</button>
1
2
<!--此处省略
页面,可直接从源码中拷贝-->
1
/**
* 根据创建时间查询
* @param createTime
* @return
*/
List<ConfigLocos> findByCreateTimeLessThan(Long createTime);
1
2
3
4
5
6
/**
* 定期把记录的轨迹转移到日志文件的任务
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@Component
@EnableScheduling
public class LocosTransferToLogFileTask {
@Autowired
private ConfigLocosDao configLocosDao;
private static final String SYSTEM_PATH =
LocosTransferToLogFileTask.class.getResource("/").getPath();
/**
* 每天凌晨3点,执行转移任务
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
 
4 黑马应用配置平台整合
4.1 应用配置管理平台服务端发布
4.1.1 镜像库的环境准备
1)通过Dockerfile安装JDK
把本地的JDK1.8上传到服务器
@Scheduled(cron = "0 0 3 * * ?")
public void transferToLog(){
//获取当前系统时间的一年前时间
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR,-1);
long time = calendar.getTimeInMillis();
//1.查询所有超过一年时间的推送轨迹
List<ConfigLocos> mapList =
configLocosDao.findByCreateTimeLessThan(time);
/
/2
.遍
配置
fo
r(
Co
nfi
gL
oc
os configLocos : mapList){
//
1
.写
文件
t
ry
{
S
t
r
i
n
g
c
u
rr
e
nt
D
a
te
=
n
ew
S
i
m
p
le
D
a
t
e
F
o
rm
a
t("yyyy-MM-dd
HH:m
m
:
s
s
"
)
.
fo
r
m
a
t
(n
e
w
D
at
e
(S
y
st
e
m
.
c
u
r
re
n
tT
i
m
e
M
i
l
l
i
s
(
))
)
;
String fileName =
configLocos.getProjectName()+"_"+currentDate+".log";
File file = new File(SYSTEM_PATH + File.separator + "logs",
fileName);
byte[] bytes = KryoSerializeUtil.serialize(configLocos);
FileUtils.writeByteArrayToFile(file,bytes);
}catch (Exception e){
e.printStackTrace();
}
//2.删除
configLocosDao.deleteById(configLocos.getId());
}
}
}
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 
 
 
 
第一步:创建一个目录,用于存放jdk8
第二步:把上传的jdk8.tar.gz转移到创建的目录
第三步:前往/urs/local/dockerjdk8/目录,并创建Dockerfile
文件内容如下:
mkdir -p /usr/local/dockerjdk8
1
mv jdk-8u171-linux-x64.tar.gz /usr/local/dockerjdk8/
1
cd /usr/local/dockerjdk8/
vi Dockerfile
注意:Dockerfile必须这么写 D要求必须大写
1
2
3
#依赖镜像名称和ID
FROM centos:7( 写这个需要下载,而且很慢。直接写id)
#指定镜像创建者信息
MAINTAINER ITCAST
#切换工作目录
WORKDIR /usr
RUN mkdir /usr/local/java
#ADD 是相对路径jar,把java添加到容器中
ADD jdk-8u171-linux-x64.tar.gz /usr/local/java/
#配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_171
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH
$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH
#有中文问题请用下面这个
#
FROM 5182e96772bf
#
MAINTAINER ITCAST
#
WORKDIR /usr
RUN mkdir /usr/local/java
#ADD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
 
2)安装Docker私有仓库
第一步:从网上pull下私有仓库 (此处下载不下载都行)
第二步:创建私有仓库
第三步:查看创建结果
通过地址栏访问 http://192.168.32.133:5000/v2/_catalog
第四步:修改配置文件
第五步:重启docker服务
第六步:启动私服(私有仓库)
3)上传镜像到私服
第一步:给镜像打个标记
第二步:上传
ADD jdk-8u171-linux-x64.tar.gz /usr/local/java/
#
ENV JAVA_HOME /usr/local/java/jdk1.8.0_171
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH
$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH
26
27
28
29
30
31
第四步:创建d
o
k
c
e
r
docker
b
u
i
l
d
-t
='jdk1.8' .
第五步:查看容
doc
ke
r
im
a
g
e
s
第六步:
jd
k
1
.
8容
d
o
ck
e
r
r
un
-i
t
--n
am
e=
m
yj
dk
8 jdk1.8 /bin/bash
-i
t
互模
,可
1
2
3
4
5
6
7
docker pull registry
1
docker run -di --name=registry -p 5000:5000 registry
1
docker ps
1
vi /etc/docker/daemon.json
如果看到此内容就删掉:
{
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"]
}
添加以下内容:
{"insecure-registries":["192.168.32.133:5000"]}
1
2
3
4
5
6
7
systemctl restart docker
1
docker start container_id
1
docker tag jdk1.8 192.168.32.133:5000/jdk1.8
1
docker push 192.168.32.133:5000/jdk1.8
1
 
 
刷新 http://192.168.32:133:5000/v2/_catalog 地址查看结果
4)本地工程安装maven的docker插件
第一步:编辑docker的配置
第二步:刷新配置,启动私有仓库(私服)
第三步:在要部署的应用的pom.xml中加入如下配置
vi /lib/systemd/system/docker.service
其中ExecStart=后添加配置 -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock
注意:默认do
cker对远程执行是关闭的,只有加了这句配置,才表示任何地址都可以通过2375端口远程执行。
1
2
3
4
systemc
tl daemon-reload
s
y
st
e
m
ct
l
r
e
st
a
rt
d
o
ck
e
r
d
oc
k
e
r
st
a
r
t
co
n
t
a
i
ne
r
_i
d
1
2
3
<build>
<finalName>hacm_server</finalName>
<resources>
<resource>
<directory>src/main/webapp</directory>
<targetPath>META-INF/resources</targetPath>
<includes>
<include>**/**</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>**/**</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.2.RELEASE</version>
</plugin>
<!-- docker的maven插件,官网:https://github.com/spotify/docker‐maven‐
plugin -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.13</version>
<configuration>
<imageName>192.168.32.133:5000/${project.artifactId}:${project.version}
</imageName>
<baseImage>jdk1.8</baseImage>
<entryPoint>["java", "-
jar","/${project.build.finalName}.jar"]</entryPoint>
<resources>
<resource>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
 
第四步:在工程所在的
pom.xml文件夹下执行命令
第五步:查看运行结果 使用docker images查看结果
刷新浏览器 http://192.168.32.133:5000/v2/_catalog 查看结果
4.1.3 安装质量监控工具
1)InfluxDB
第一步:下载镜像
第二步:创建容器
第三步:测试安装结果
打开浏览器 http://192.168.32.133:8083/
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<dockerHost>http://192.168.32.133:2375</dockerHost>
</configuration>
</plugin>
</plugins>
</bui
ld>
36
37
38
39
40
41
42
43
44
45
mvn clean package docker:build -DpushImage(相当于docker build -t='xxx' 同时上传到私有
仓库中
)
注意:
是在windows的cmd命令行输入
1
2
docker images
1
docker pull tutum/influxdb
1
docker run -di -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 --
name=influxsrv tutum/influxdb
端口概述: 8083端口:web访问端口 8086:数据写入端口
1
2
3
 
 
3)InfluxDB
infiuxDB的操作,在浏览器中键入http://192.168.32.133:8083/
2)CAdvisor
第一步:下载镜像
第二步:创建容器
第三步:通过浏览器查看采集数据
http://192.168.32.133:8080/containers/
/*第一
步:创建数据库*/
CR
EA
T
E
DA
TABASE "cadvisor"
使
/*第二步:查看数据库创建结果*/
SHOW DATABASES
/*第三步:创建用户*/
CREATE USER "cadvisor" WITH PASSWORD 'cadvisor' WITH ALL PRIVILEGES
/*第四步:查看用户创建结果*/
SHOW USERS
/*第五步:用户授权*/
全部权限
grant all privileges on cadvisor to cadvisor
写权限
grant WRITE on cadvisor to cadvisor
读权限
grant READ on cadvisor to cadvisor
/*第六步:查看采集数据*/
SHOW MEASUREMENTS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
docker pull google/cadvisor
1
docker run --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --
volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --
publish=8080:8080 --detach=true --link influxsrv:influxsrv --name=cadvisor
google/cadvisor -storage_driver=influxdb -storage_driver_db=cadvisor -
storage_driver_host=influxsrv:8086
1
docker run --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --
volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --
publish=8080:8080 --detach=true --link influxsrv:influxsrv --name=cadvisor
google/cadvisor -storage_driver=influxdb -storage_driver_db=cadvisor -
storage_driver_host=192.168.32.133:8086
1
 
 
3)Grafana
第一步:下载镜像
第二步:创建容器
第三步:通过浏览器访问 http://192.168.32.133:3001 用户名密码均为admin
docker pull grafana/grafana
1
docker run -d -p 3001:3000 -e INFLUXDB_HOST=influxsrv -e INFLUXDB_PORT=8086 -e
INFLUXDB_NAME=cadvisor -e INFLUXDB_USER=cadvisor -e INFLUXDB_PASS=cadvisor --link
influxsrv:influxsrv --name=grafana grafana/grafana
1
 
 
 
 
 
Grafana使用 第一步:添加数据源
 
 
第二步:添加仪表盘
 
 
 
 
第三步:预警通知设置
 
 
第四步:仪表盘预警设置
 
 
posted @ 2023-05-31 18:47  十一vs十一  阅读(220)  评论(0编辑  收藏  举报