最近在做一個要求統計api-gateway模塊的所有請求數據展示的一個功能,所以查找了幾個方案,包括以下:
1,alibaba sentinel 哨兵(最後決定使用這個,因爲功能比較豐富,以後可能可以用到,而且想試一下阿里的框架,控制檯dashboard的前端實時請求展示用的框架也是螞蟻金服的G2可視化圖表框架)
2,大衆點評(這個功能很完善,而且也很豐富,完全符合要求,後端需要部署,前端集成,但是查資料網友說不支持api-gateway,所以放棄了)
3,spring boot Admin(主要針對服務的監控)
4,Hystrix Dashboard(單個微服務監控)+ Turbine 聚合監控(集羣微服務監控)(可以監控,但是一些統計界面還是要自己統計實現)
5,Spring Cloud Sleuth + Zipkin +ELK(雖然集成了ELK,但是這個工程比較麻煩)
6,springboot aop 切面攔截http請求(可以攔截,但要考慮到數據量持久化的性能問題)
以上使用到的方案,基本都試驗過了,最後決定還是用阿里的哨兵吧:
附上github地址:https://github.com/alibaba/Sentinel/wiki (手動點擊看介紹)
現在的版本已經更新到1.6了,因爲本人目前只是需要其中的一個數據請求統計的功能,所以就只下載了sentinel dashboard的源碼來操作,修改裏面的東西,實現自己想要的功能。
sentinel dashboard集成spring cloud的操作非常簡單,只需要配置pom依賴和配置文件dashboard的地址就能連上sentinel dashboard控制檯了;
第一步,在需要的監控的模塊,配置依賴pom: 由於項目的spring boot版本是1.4.1的低版本,所以目前可以依賴0.1.0.RELEASE包,依賴0.2.0.RELEASE或以上的話就會報錯,目前已經更新到0.9.0.RELEASE依賴包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>0.1.0.RELEASE</version>
</dependency>
第二步,配置文件配置dashboard訪問地址:(這裏我改成了31017的訪問地址,防止和其他端口衝突)
spring:
cloud:
sentinel:
transport:
dashboard: localhost:31017
好了,以上兩步spring cloud配置就集成了sentinel dashboard了,簡單吧
第三步,網上下載運行sentinel dashboard.jar包,一開始我下載的是網上已經打包好的1.6.0-jar包的,不過很抱歉,那個網址不知道扔哪裏去了,不過既然下載了源碼下來,我們也可以自己打包一個jar給自己用,而且後面的操作都會涉及到源碼,打開源碼,切到sentinel dashboard的模塊下,在終端運行打包命令:mvn clean package
生成target包,進去裏面找打包好的jar包,並且運行此jar包,命令:java -jar sentinel-dashboard.jar
注意:打包的源碼中的配置文件application.properties,記得修改裏面的端口號爲:31017
添加代碼: server.port = 31017
前端訪問地址:gulpfile.js文件,拉倒最下面看到打開瀏覽器那裏,修改你的前端訪問端口號:
運行起來,就可以在瀏覽器打開鏈接:localhost:31017,看到控制檯的頁面了,如下(爲了展示,下面兩張圖都是取別人的):
不修改其他代碼的話,大致實現圖如下:(左邊是圖表展示格式,右邊是表格展示格式)
圖表最左邊是一堆功能選項列表,其中我想要的功能就在實時監控裏面,這裏展示的數據是實時的,並且是秒級展示的。
引用官網的一段話:Sentinel 會記錄資源訪問的秒級數據(若沒有訪問則不進行記錄)並保存在本地日誌中,具體格式請見 秒級監控日誌文檔。Sentinel 控制檯可以通過 Sentinel 客戶端預留的 HTTP API 從秒級監控日誌中拉取監控數據,並進行聚合。
目前 Sentinel 控制檯中監控數據聚合後直接存在內存中,還沒有進行持久化,且僅保留最近 5 分鐘的監控數據。若需要監控數據持久化的功能,可以擴展實現 MetricsRepository
接口(0.2.0 版本);
注意:網上給出的持久化到配置中心apollo或者nacox等,實際上主要是持久化規則而已,實現數據持久化展示,還得自己持久化到自己的數據庫中
但是我想要的功能其實不需要這麼多,暫時只是需要實時的功能,所以其他功能用來作爲以後續項目需求再拓展使用,暫先屏蔽
下面先給出我改造後的功能圖:
左邊列表暫時不需要用到的功能-前後端都屏蔽了,只留下了個實時功能,目前的效果是因爲訪問數據量過於龐大,故數據庫裏只保存了前6個小時以內的數據,超過6個小時便會刪除數據,前端頁面一分鐘請求統計一次,亦可點擊搜索按鈕直接請求,後端實時攔截數據,內存保存時間改爲1分鐘,過濾其他不需要統計的請求url,前端圖表只留下柱狀圖,並按分鐘統計展示(取消秒級統計),曾測試過10萬條假數據,瀏覽器很容易奔潰,減至5千條也仍然有此隱患,故前端修改爲按分頁懶加載查詢,每頁最大3千條數據統計爲準。
下面一一給出持久化到數據庫以及前後端修改的源碼:
1,創建數據庫:ssc_sentinel,並創建數據表sentinel_metric(建表語句如下):
CREATE TABLE `sentinel_metric` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id,主鍵',
`gmt_create` datetime DEFAULT NULL COMMENT '創建時間',
`gmt_modified` datetime DEFAULT NULL COMMENT '修改時間',
`app` varchar(100) DEFAULT NULL COMMENT '應用名稱',
`timestamp` datetime DEFAULT NULL COMMENT '統計時間',
`resource` varchar(500) DEFAULT NULL COMMENT '資源名稱',
`pass_qps` int(11) DEFAULT NULL COMMENT '通過qps',
`success_qps` int(11) DEFAULT NULL COMMENT '成功qps',
`block_qps` int(11) DEFAULT NULL COMMENT '限流qps',
`exception_qps` int(11) DEFAULT NULL COMMENT '發送異常的次數',
`rt` double DEFAULT NULL COMMENT '耗時時間(ms)',
`_count` int(11) DEFAULT NULL COMMENT '本次聚合的總條數',
`resource_code` int(11) DEFAULT NULL COMMENT '資源的hashCode',
PRIMARY KEY (`id`),
KEY `app_idx` (`app`) USING BTREE,
KEY `resource_idx` (`resource`) USING BTREE,
KEY `timestamp_idx` (`timestamp`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=105043 DEFAULT CHARSET=utf8;
2,依賴jap包和mysql驅動包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
3,配置文件添加數據庫連接和jap配置:
# datasource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ssc_sentinel?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
# spring data jpa
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.show-sql=false
4,創建實體MetricPO,實例化數據表:
package com.alibaba.csp.sentinel.dashboard.datasource.entity.jpa;
/**
* @author KL
* @version 1.0
* @date 2019/7/29
**/
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Table(name = "sentinel_metric")
public class MetricPO implements Serializable {
private static final long serialVersionUID = 7200023615444172715L;
/**id,主鍵*/
@Id
@GeneratedValue
@Column(name = "id")
private Long id;
/**創建時間*/
@Column(name = "gmt_create")
private Date gmtCreate;
/**修改時間*/
@Column(name = "gmt_modified")
private Date gmtModified;
/**應用名稱*/
@Column(name = "app")
private String app;
/**統計時間*/
@Column(name = "timestamp")
private Date timestamp;
/**資源名稱*/
@Column(name = "resource")
private String resource;
/**通過qps*/
@Column(name = "pass_qps")
private Long passQps;
/**成功qps*/
@Column(name = "success_qps")
private Long successQps;
/**限流qps*/
@Column(name = "block_qps")
private Long blockQps;
/**發送異常的次數*/
@Column(name = "exception_qps")
private Long exceptionQps;
/**耗時時間(ms)*/
@Column(name = "rt")
private Double rt;
/**本次聚合的總條數*/
@Column(name = "_count")
private Integer count;
/**資源的hashCode*/
@Column(name = "resource_code")
private Integer resourceCode;
public static long getSerialVersionUID() {
return serialVersionUID;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getGmtCreate() {
return gmtCreate;
}
public void setGmtCreate(Date gmtCreate) {
this.gmtCreate = gmtCreate;
}
public Date getGmtModified() {
return gmtModified;
}
public void setGmtModified(Date gmtModified) {
this.gmtModified = gmtModified;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public String getResource() {
return resource;
}
public void setResource(String resource) {
this.resource = resource;
}
public Long getPassQps() {
return passQps;
}
public void setPassQps(Long passQps) {
this.passQps = passQps;
}
public Long getSuccessQps() {
return successQps;
}
public void setSuccessQps(Long successQps) {
this.successQps = successQps;
}
public Long getBlockQps() {
return blockQps;
}
public void setBlockQps(Long blockQps) {
this.blockQps = blockQps;
}
public Long getExceptionQps() {
return exceptionQps;
}
public void setExceptionQps(Long exceptionQps) {
this.exceptionQps = exceptionQps;
}
public Double getRt() {
return rt;
}
public void setRt(Double rt) {
this.rt = rt;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getResourceCode() {
return resourceCode;
}
public void setResourceCode(Integer resourceCode) {
this.resourceCode = resourceCode;
}
}
5,找到MetricsRepository,並添加下面幾個方法(分頁查詢,刪除等)
List<String> listResourcesOfApp(String app);
List<T> queryByTime(Integer pageIndex, Integer pageSize,String key);
Integer countByTime(String key);
void removeAll();
6,新增一個dao繼承MetricsRepository,實現查詢與持久化操作,代碼如下:
package com.alibaba.csp.sentinel.dashboard.repository.metric;
/**
* @author KL
* @version 1.0
* @date 2019/7/29
**/
import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.jpa.MetricPO;
import com.alibaba.csp.sentinel.util.StringUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
@Transactional
@Repository("jpaMetricsRepository")
public class JpaMetricsRepository implements MetricsRepository<MetricEntity> {
@PersistenceContext
private EntityManager em;
@Override
public void save(MetricEntity metric) {
if (metric == null || StringUtil.isBlank(metric.getApp())) {
return;
}
MetricPO metricPO = new MetricPO();
BeanUtils.copyProperties(metric, metricPO);
StringBuilder hql = new StringBuilder();
hql.append("FROM MetricPO");
hql.append(" WHERE resource=:resource");
hql.append(" AND timestamp>=:startTime");
hql.append(" AND timestamp<=:endTime");
Query query = em.createQuery(hql.toString());
query.setParameter("resource", metricPO.getResource());
query.setParameter("startTime", Date.from(Instant.ofEpochMilli(metricPO.getTimestamp().getTime() - (metricPO.getTimestamp().getSeconds() * 1000))));
query.setParameter("endTime", Date.from(Instant.ofEpochMilli(metricPO.getTimestamp().getTime() - (metricPO.getTimestamp().getSeconds() * 1000) + 59 * 1000)));
query.setMaxResults(1);
List<MetricPO> metricPOs = query.getResultList();
if (CollectionUtils.isEmpty(metricPOs)) {
em.persist(metricPO);
} else {
MetricPO saveVo = metricPOs.get(0);
saveVo.setPassQps(metricPO.getPassQps() + saveVo.getPassQps());
saveVo.setBlockQps(metricPO.getBlockQps() + saveVo.getBlockQps());
saveVo.setSuccessQps(metricPO.getSuccessQps() + saveVo.getSuccessQps());
saveVo.setExceptionQps(metricPO.getExceptionQps() + saveVo.getExceptionQps());
saveVo.setRt(metricPO.getRt() + saveVo.getRt());
saveVo.setCount(metricPO.getCount() + saveVo.getCount());
saveVo.setTimestamp(metricPO.getTimestamp());
saveVo.setGmtModified(metricPO.getGmtModified());
em.merge(saveVo);
}
}
@Override
public void saveAll(Iterable<MetricEntity> metrics) {
if (metrics == null) {
return;
}
removeAll();
metrics.forEach(this::save);
}
@Override
public List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) {
List<MetricEntity> results = new ArrayList<MetricEntity>();
return results;
}
@Override
public List<MetricEntity> queryByTime(Integer pageIndex,Integer pageSize,String key) {
List<MetricEntity> results = new ArrayList<MetricEntity>();
StringBuilder hql = new StringBuilder();
hql.append("FROM MetricPO");
hql.append(" WHERE timestamp<=:endTime");
if (StringUtil.isNotBlank(key)){
hql.append(" AND resource LIKE :key ");
}
hql.append(" order by timestamp desc");
Query query = em.createQuery(hql.toString());
query.setMaxResults(pageSize);
query.setFirstResult((pageIndex-1)*pageSize);
query.setParameter("endTime", Date.from(Instant.ofEpochMilli(System.currentTimeMillis())));
if (StringUtil.isNotBlank(key)){
query.setParameter("key","%"+key+"%");
}
List<MetricPO> metricPOs = query.getResultList();
if (CollectionUtils.isEmpty(metricPOs)) {
return results;
}
for (MetricPO metricPO : metricPOs) {
if (metricPO.getGmtCreate().after(new Date(System.currentTimeMillis() - 6 * 3600 * 1000))) {
MetricEntity metricEntity = new MetricEntity();
BeanUtils.copyProperties(metricPO, metricEntity);
results.add(metricEntity);
}
}
return results;
}
@Override
public Integer countByTime(String key) {
Integer totalCount = 0;
StringBuilder hql = new StringBuilder();
hql.append("FROM MetricPO");
hql.append(" WHERE timestamp<=:endTime");
if (StringUtil.isNotBlank(key)){
hql.append(" AND resource LIKE :key ");
}
Query query = em.createQuery(hql.toString());
query.setParameter("endTime", Date.from(Instant.ofEpochMilli(System.currentTimeMillis())));
if (StringUtil.isNotBlank(key)){
query.setParameter("key","%"+key+"%");
}
List<MetricPO> metricPOs = query.getResultList();
if (CollectionUtils.isEmpty(metricPOs)) {
return totalCount;
}
for (MetricPO metricPO : metricPOs) {
if (metricPO.getGmtCreate().after(new Date(System.currentTimeMillis() - 6 * 3600 * 1000))) {
MetricEntity metricEntity = new MetricEntity();
BeanUtils.copyProperties(metricPO, metricEntity);
totalCount++;
}
}
return totalCount;
}
@Override
public void removeAll() {
StringBuilder hql = new StringBuilder();
hql.append("FROM MetricPO");
hql.append(" WHERE timestamp<:time");
Query query = em.createQuery(hql.toString());
query.setParameter("time", Date.from(Instant.ofEpochMilli(System.currentTimeMillis() - 6 * 3600 * 1000)));
List<MetricPO> metricPOs = query.getResultList();
if (CollectionUtils.isEmpty(metricPOs)) {
return;
}
for (MetricPO metricPO : metricPOs) {
//超過6個小時則刪除
em.remove(metricPO);
}
}
@Override
public List<String> listResourcesOfApp(String app) {
return null;
}
}
7,因爲InMemoryMetricsRepository類中也繼承了MetricsRepository接口,所以記得實現上面新增的幾個方法,不用操作,實現就行:
8,MetricFetcher類中添加@Qualifier("jpaMetricsRepository")註解,如下位置:
@Qualifier("jpaMetricsRepository")
@Autowired
private MetricsRepository<MetricEntity> metricStore;
9,MetricController類中也同樣添加@Qualifier("jpaMetricsRepository")註解,如下位置:
@Qualifier("jpaMetricsRepository")
@Autowired
private MetricsRepository<MetricEntity> metricStore;
10,修改MetricController類中的@RequestMapping("/queryTopResourceMetric.json")對應的請求方法,此方法主要是實現展示數據庫中持久化的數據,修改如下:
@Autowired
private JpaMetricsRepository jpaMetricsRepository1;
@ResponseBody
@RequestMapping("/queryTopResourceMetric.json")
public Result<?> queryTopResourceMetric(final String app,
Integer pageIndex,
Integer pageSize,
Boolean desc,
Long startTime, Long endTime, String searchKey) {
List<String> topResource = new ArrayList<>();
final Map<String, Iterable<MetricVo>> map = new ConcurrentHashMap<>();
Integer totalCount = 0;
Integer totalPage = 0;
totalCount = jpaMetricsRepository1.countByTime(searchKey);
if (totalCount==0){
return Result.ofSuccess(null);
}
totalPage = (totalCount + pageSize - 1) / pageSize;
if(StringUtil.isNotBlank(searchKey)&&totalPage==1){
pageIndex = 1;
}
List<MetricEntity> entities = jpaMetricsRepository1.queryByTime(pageIndex,pageSize,searchKey);
if (entities!=null && entities.size()!=0) {
for (MetricEntity entity:entities) {
String res = entity.getResource();
List<MetricVo> vos = MetricVo.fromMetricEntities(entities, res);
map.put(res, vos);
}
topResource = entities.stream().map(MetricEntity::getResource).collect(Collectors.toList());
}
if (topResource == null || topResource.isEmpty()) {
return Result.ofSuccess(null);
}
Map<String, Object> resultMap = new HashMap<>(16);
resultMap.put("totalCount", totalCount);
resultMap.put("totalPage", totalPage);
resultMap.put("pageIndex", pageIndex);
resultMap.put("pageSize", pageSize);
Map<String, Iterable<MetricVo>> map2 = new LinkedHashMap<>();
// order matters.
for (String identity : topResource) {
map2.put(identity, map.get(identity));
}
resultMap.put("metric", map2);
return Result.ofSuccess(resultMap);
}
11,修改DashboardConfig,根據自己需要修改,這裏設置了隱藏時間爲一分鐘,最好對應前端一分鐘之後請求統計一次的時間來修改,所示:
public static int getHideAppNoMachineMillis() {
return getConfigInt(CONFIG_HIDE_APP_NO_MACHINE_MILLIS, 0, 60000);
}
public static int getRemoveAppNoMachineMillis() {
return getConfigInt(CONFIG_REMOVE_APP_NO_MACHINE_MILLIS, 0, 120000);
}
public static int getAutoRemoveMachineMillis() {
return getConfigInt(CONFIG_AUTO_REMOVE_MACHINE_MILLIS, 0, 180000);
}
public static int getUnhealthyMachineMillis() {
return getConfigInt(CONFIG_UNHEALTHY_MACHINE_MILLIS, DEFAULT_MACHINE_HEALTHY_TIMEOUT_MS, 30000);
}
參考配置文件說明:
sentinel.dashboard.auth.username | String | sentinel | 無 | 登錄控制檯的用戶名,默認爲 `sentinel`
sentinel.dashboard.auth.password | String | sentinel | 無 | 登錄控制檯的密碼,默認爲 `sentinel`
sentinel.dashboard.app.hideAppNoMachineMillis | Integer | 0 | 60000 | 是否隱藏無健康節點的應用,距離最近一次主機心跳時間的毫秒數,默認關閉
sentinel.dashboard.removeAppNoMachineMillis | Integer | 0 | 120000 | 是否自動刪除無健康節點的應用,距離最近一次其下節點的心跳時間毫秒數,默認關閉
sentinel.dashboard.unhealthyMachineMillis | Integer | 60000 | 30000 | 主機失聯判定,不可關閉
sentinel.dashboard.autoRemoveMachineMillis | Integer | 0 | 300000 | 距離最近心跳時間超過指定時間是否自動刪除失聯節點,默認關閉
12,修改了時間後,打包時,會運行測試類,設置的斷言assert可能會報錯,只需要修改時間,成功打包即可:
13,修改前端metric.js文件,修改自動刷新時間和初始化pageSize等:
14,sidebar.html前端註釋不需要展示的列,後端請求代碼根據需要自行註釋:
15,app.js註釋請求路由,停止請求:
16,metric.html註釋表格:
17,配置url過濾配置以及賬號密碼:
18,配置多環境打包圖:
對應的application添加對應的active,例如local本地環境添加配置文件:spring.profiles.active=local
19,配置修改pom多環境配置(兩處):
添加profile
<profiles>
<profile>
<id>local</id>
<properties>
<profiles.active>local</profiles.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>dev</id>
<properties>
<profiles.active>dev</profiles.active>
</properties>
</profile>
<profile>
<id>dev_v30</id>
<properties>
<profiles.active>dev_v30</profiles.active>
</properties>
</profile>
</profiles>
添加修改recourse
<resources>
<resource>
<directory>src/main/webapp/</directory>
<excludes>
<exclude>resources/node_modules/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>application-{profiles.active}.properties</exclude>
</excludes>
<filtering>true</filtering>
</resource>
</resources>
20,終端運行打包:mvn clean package,jar包生成目錄在target下
21,不同環境jar包運行命令,如本地環境:java -jar sentinel-dashboard.jar --spring.profiles.active=local
以上,就是集成sentinel dashboard到spring cloud 中實現數據(按分鐘)持久化統計展示,多環境運行單個jar的例子了,
最後部署到docker鏡像裏面,通過上面運行命令即可部署。
後續拓展功能:添加所有請求的排行榜統計頁面如下(其中小於1.5S耗時不展示):
主要參考文檔如下:
1,持久化mysql:https://www.cnblogs.com/cdfive2018/p/9838577.html
2,源碼結構:https://www.jianshu.com/p/affbb66c15e6
3,官網wiki:https://github.com/alibaba/Sentinel/wiki