什么是企业服务总线
企业服务总线:一种提供服务之间调用的框架,是粗粒度的服务调用,相对微服务,企业总线更多的体现组织架构的影响,即:总线更多的体现在跨组织之间的服务调用,而微服务更多的是同一组织内部的服务调用。
举两个例子说明:
1、集团公司下面有多个子公司,子公司A调用子公司B提供的服务就要用到企业服务总线(服务网关,权限统一管控)。
2、集团公司下面有多个子公司,子公司A下面的B部门调用C部门提供的服务也会用到企业服务总线(去中心的服务注册发现框架,权限统一管控)
为什么需要链路追踪
企业服务总线使服务之间的调用关系复杂,越来越多的组件开始走向分布式化,使得后台服务构成了一种复杂的分布式网络。在服务能力提升的同时,复杂的网络结构也使问题定位更加困难。在一个请求在经过诸多服务过程中,出现了某一个调用失败的情况,查询具体的异常由哪一个服务引起的就变得十分抓狂,问题定位和处理效率是也会非常低。
分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
相关的资料网上有很多,都是参考google dapper实现,原理大同小异,无外乎都是 数据采集->存储->数据展示呈现。
监控平台需要
我们的企业服务总线是14年开始建设,到15年的时候每天的调用量是1000多万,随着业务系统开始大量接入,监控需求越来越迫切了。最开始采用的是mango+hbase方案,当时公司还没有专门的HBASE团队维护,要用HBASE都是自己搭建,由于HBASE的复杂性,没有专门团队维护困难,此方案由于极差的性能及稳定性在上线后不久就遭到用户的大量投诉。此后我们又尝试把数据直接存ORACLE DB,然后在ORACLEDB中使用PLSQL做统计分析,千万级别的数据统计性能极差,也不能满足问题分析要求的快速分析,快速定位的要求。
为了尽快解决分布式服务调用运维监控问题,我们重新梳理需求,提出一些具体要求,
1、问题快速定位:
链路追踪:个体分析,快速定位异常点
实时统计分析:宏观分析,分析问题根因,数据统计结果要在事件发生后1分钟内完成;
2、异常监控:根据异常调用链路的统计数据发告警邮件
3、性能预警:对应用的耗时统计与历史数据进行对比,发现异常情况时发出预警邮件
4、各钟维度粒度的统计报表:
业务维度:消费方公司、服务方公司、消费方系统、服务方系统、URI
时间粒度:分钟、小时、天、月、年
技术架构方案,如下图:
整个架构非常简单,主要包括三部分内容
1,自开发实时计算引擎
a、trace实时统计
b、异常实时告警
c、性能预警
2,数据存储方案,按时间先后使用了两种DB
a、oracle
b、cassandra.
3、报表查询
作为一个监控系统,首先要要有非常高的稳定性及性能,要从应用层及存储层整体进行优化设计
应用层:1,横向扩容:应用无状态,自开发了实时计算引擎,通过redis保存中间计算状态,可随着业务量的增长横向扩容。
· 2、减少DB访问:系统配置信息定时更新,常驻内存,应用计算都基于内存数据。
3、读写分离:查询机、运算机通过不同的实例完成。
4、JVM性能优化。
数据层:只提供存储及查询功能,数据的分析计算都在应用层完成(老方案 plsql在DB中统计)。
下面就对主要的几个功能做详细的介绍。
自开发实时计算引擎
上图是流式计算引擎的数据流图,在一分钟内trace数据达到500条时,就会推送500条trace给监控平台,若一分钟都没有到达500条,则一分钟会推送一次数据,重点步骤说明如下:
1、统计trace 生成统计结果 min-statis-500,计算的详细代码如下:
private void statis(Trace trace, string minkey){
if(null == trace) return;
//statis500Map本地缓存的统计临时数据
StatisData statisRes = statis500Map.get(minkey);
if(null == statisRes){
statisRes = new StatisData();
statis500Map.put(minkey,statisRes);
}
statisRes.incTotalCount();
if(ERROR.equals(trace.getStatus)){
statisRes.incFailCount();
}
long cost = trace.getTraceCost();
statisRes.setMax(Math.max(statisRes.getMax(),cost));
statisRes.setMin(Math.min(statisRes.getMin(),cost));
statisRes.setSum(statisRes.getSum()+cost);
double sum2 = cost *cost;
//计算STD
statisRes.setSum2(statisRes.getSum2() + sum2);
//计算95线、99线
int allDuration = ((int)statisData.computeDuration(cost));
statisRes.findOrCreateDuration(allDuration).incCount();
}
public double computeDuration(double duration){
if(duration < 20){
return duration;
}else if(duration < 200){
return duration - duration % 5;
}else if(duration < 2000){
return duration - duration % 50;
}else {
return duration - duration % 500;
}
}
public Duration findOrCreateDuration(int value){
Duration dura = m_durations.get(value);
if(null == dura){
dura = m_durations.get(value);
if(null == dura){
dura = new Duration(value);
m_durations.put(value,dura);
}
}
}
public class Duration{
private int m_value;
private int m_count;
public Duration(int value){
m_value = value;
}
@Override
public boolean equals(object obj){
if(obj instanceof Duration){
Duration _o = (Duration) obj;
if(m_value != _o.getValue()){
return false;
}
return true;
}
return false;
}
public int getCount(){
return m_count;
}
public int getValue(){
return m_value;
}
@Override
public int hashCode(){
int hash = 0 ;
hash = hash * 31 + m_value;
return hash;
}
public Duration incCount(){
m_count++;
return this;
}
public Duration incCount(int count){
m_count += count;
return this;
}
}
2、保存min-statis-500 到redis,生成min-key和queue-min-key两个数据结构,代码如下:
void saveStatis500ToRedis(){
if(null == statis500Map) return;
try{
Iterator iter = statis500Map.entrySet().iterator();
while(iter.hasNext()){
Map.Entry<String,StatisData> entry = (Map.Entry<String,StatisData>) iter.next();
String minkey = entry.getKey();
String statisData = entry.getValue().toString();
//写入min-key队列
int minIndex = redisDao.rpush(minkey,statisData).intValue();
//写入queue-min-key队列,队列中的数据每个都代表唯一的一个任务。
if(minIndex == 1){
//queueKey形如 queue-min-01
String queue = “queue-min-” + currentMin;
redisDao.rpush(queue,minKey);
redisDao.expire(minKey,460)
}
}
}
}
3、合并分钟统计数据,代码如下:
void mergeStatis500ToMinFromRedis(){
for(int i=2;i>0;i–){
while(true){
String queue = “queue-min-” + currentMin;
String minkey = redisDao.lpop(queue);
if(null == minKey) break;
List staties = redisDao.lrange(minkey,0,-1);
List sucesslist = mergeMin(staties);
long bfLength = staties.size();
long bfPttlTime = redisDao.pttl(minkey);
redisDao.ltrim(minkey,bfLength,-1);
long newPttlTime = redisDao.pttl(minkey);
if(newPttlTime > 0 && newPttlTime > bfPttlTime){
String queueN = “queue-min-” + currentMin;
redisDao.rpush(queueN,minkey);
redisDao.expire(minkey,460);
}
//合并小时入口
List hourKeys = new ArrayList();
for(int i=0;i<sucesslist.size();i++){
StatisData sData= sucesslist.get(i);
String minKey = sData.getKey();
String hourkey = convert(minKey);
int hourIndex = redisDao.rpush(hourkey,sData.toString()).intValue();
if(hourIndex == 1){
hourKeys.add(hourkey);
redisDao.expire(hourkey,4*60);
}
}
if(hourkeys.size() > 0) mergeMinToHour(hourkeys);
}
}
}
List<StatisData> mergeMin(staties){
Map<String,StatisData> mergStatis = new HashMap<String,StatisData>();
StatisData m_statis = new StatisData();
for(String o: staties){
StatisData statis = convert(o);
//合并指标
m_statis.mergeStatis(statis);
}
//集合运算99线、95线、avg 等,需要合并完成之后计算
m_statis.computeStatis();
List<StatisData> rtn = new ArrayList<StatisData>();
rtn.add(m_statis);
}
void mergeStatis(StatisData other){
this.setM_failCount(this.getM_failCount() + other.getM_failCount());
....sum/totalCount/max/min/sum2等
this.mergM_durations(other.getM_durations);
}
//集合运算
void computeStatis(){
this.setM_line95Value(computeLineValue(this.getM_durations(),95));
this.setM_line99Value(computeLineValue(this.getM_durations(),99.9));
this.setM_avg(this.getM_sum()/this.getM_totalCount());
this.setM_std(std(this.getM_totalCount(),this.getM_avg(),this.getM_SUM2(),this.getM_max()));
}
double computeLineValue(){
int totalCount = 0;
Map<Integer,Duration> sorted = new TreeMap<Integer,Duration>(DurationCompartor.DESC);
sorted.putALL(durations);
for(Duration dura :durations.values()){
totalCount += duration.getCount();
}
int reing = (int) (totalCount * (100 - percent) / 100);
for(Entry<Integer,Duration> entry : sorted.entrySet()){
reing -= entry.getValue().getCount();
if(reing <= 0) {
return entry.getKey();
}
}
return 0.0;
}
double std(long count,double avg,double sum2,double max){
double value = sum2/count - avg * avg;
if(value <= 0 || count <= 1){
return 0;
}else if(count == 2){
return max -avg;
}else{
return Math.sqrt(value);
}
}
void mergM_durations(Map<Integer,Duration> other){
if(m_durations.size() == 0){
this.m_durations.putAll(other);
}else{
Iterator iter = other.entrySet().iterator();
while(iter.hasNext()){
Map.Entry<Integer,Duration> entry = (Map.Entry<Integer,Duration>) iter.next();
Duration val = entry.getValue();
findOrCreateDuration(val.getValue()).incCount(val.getCount());
}
}
}
4、合并小时统计数据,代码如下:
void mergeMinToHour(List hourkeys){
for(String hkey:hourkeys){
StatisData totalStat = new StatisData();
String totalHkey = hkey + “TOTAL”;
//判断当前数据是否是上个小时数据延迟5分钟到达的,若是则丢弃
String preHour = DateUtils.getPreviousHour(new Date());
int curMin = DateUtils.getCurrentMin();
if(hkey.indexOf(preHour) > 0 && curMin > 5){
redisDao.del(totalHkey);
redisDao.del(hkey);
}
//获取小时统计汇总数据
List hTotalStat = redisDao.lrange(totalHkey,0,-1);
//获取小时增量数据
List hIncStat = redisDao.lrange(hkey,0,-1);
if(null != hTotalStat && hIncStat.size() > 0){
totalStat = convert(hTotalStat.get(0));
}
for(String strStatis:hIncStat){
StatisData st = convert(strStatis);
totalStat.mergeStatis(st);
}
totalStat.computeStatis();
//小时汇总数据回写Redis
if(0 == redisDao.llen(totalHkey)){
redisDao.rpush(totalHkey,totalStat.toString());
redisDao.expire(totalHkey,4 * 60);
}else{
redisDao.lset(totalHkey,0,totalStat.toString());
}
//保存过程中是否有新添加的数据,hourkey需要重新加入队列
long bfStatisLength = hIncStat.size();
Long bfPttlTime = redisDao.pttl(hkey);
redisDao.ltrim(hkey,bfStatisLength,-1);
Long curPttlTime = redisDao.pttl(hkey);
if(curPttlTime > 0 && curPttlTime < bfPttlTime){
List skeys = new ArrayList();
skeys.add(hkey);
//可以用线程池处理 threadPool.execute();
mergeMinToHour(skeys);
redisDao.expire(hkey, 4 * 60);
}
}
}
数据存储方案-ORACLE
待补充
数据存储方案-cassandra
待补充
与业界同类产品的对比与思考
待补充