背景
最近手頭有個項目ESB改造,原先的ESB在系統信息統計時,例如交易異常統計,交易流水統計,交易用時統計等等統計計算時,壓測時會有瓶頸,我的老師希望將該部分查詢功能由MySQL改造爲ES。因爲也是第一次接觸ES(以前只是用過日誌收集系統ELK,但是環境搭起來就可以用,所以沒有細究。),現將一小段時間的摸索過程記錄,希望能幫到剛接觸ES和有類似需求的朋友。
改造要求
原ESB將每次進件消息進行拆解和計算,將所需要統計的字段進行字段拆解存放到mysql中,配合很多視圖來實現查詢統計功能。
現在改造,就是要將這一塊功能給換成ES的查詢和filebeat的收集。
可行性分析
本來擔心ES底層是lucene搜索引擎,因此其本質也是搜索引擎,或者說是NoSql。類似redis 可能讀取速度快,但是沒有mysql統計功能全面。經官網學習,發現ES支持聚合查詢,但是沒有找到加減乘除運算,也沒有關聯查詢。最後將官網大部分英文文檔翻了一遍發現:有加減乘除運算,也可以關聯查詢。但是關聯查詢(ES的版本是6.2.4。需要存儲的時候就要將關聯的父記錄存儲進去,而不能做關聯查詢。所以我並不想把這個功能叫做類似mysql的join查詢。)
至此總結一下可行性:
1 記錄增刪改查沒問題
2 關聯查詢,按照官網所說,使用關聯查詢,效率降低幾百倍。不過可以通過filebeat收集時,全量收集避 免join查詢
3 統計可以使用聚合查詢和script腳本計算。
總的來說可以滿足需求,聚合查詢有三種實現方式,因爲項目緊張,並沒有仔細研究,所以到底join可不可以實現,並未詳細論證,從很多人的blog來看,是的確無法做到mysql 的join on的效果。
補充說明:(本文講解從零基礎開始,爲自己的學習過程體會,膚淺之處,望高手勿噴。)
ES搭建起來支持restful訪問,網上有客戶端 elasticsearch-head 但是可以當作navicat使用。但是發送restful請求時, 我比較喜歡postman的簡潔。
ES+filebeat的使用
實戰開始
1 首先是配置索引的mapping
這個mapping相當於mysql的數據類(每一個字段的類型,如 int,char ,vchar等等類型相似。)型定義。目的是將log日誌中的字段進行拆分後,可以將制定字段存儲爲int,long等數字類型,否則默認就是String類型(ES中叫做keyword類型),將需要計算的字段名稱設置爲int 或者long,否則後邊計算是會報錯類型轉換異常。下圖爲postman截圖
(粘圖片可以強制自己敲json代碼?)
1.1特殊情況,如果mapping已經存在,就要用重命名的方式,將其替換。附件下載中有txt文件說明(https://download.csdn.net/download/liuhua121/11540325)
2 設置filebeat的解析表達式
filebeat需要配置解析表達式,目的是爲了將日誌打印的字段進行拆分。
同時還需要設置日誌是否支持換行,換行的標誌是什麼等。
在這裏配置了一個pipeline 裏面存放了解析表達式,這樣filebeat.yml文件配置如下。
上圖是filebeat的部署配置文件。
上圖是我配置好pipeline後查出來的。存放的json代碼如下
3 JavaApi的統計寫法如下。
這個也是很費勁的,ES版本更新速度極快,每個版本對應的JavaApi可能會有不同,而且聚合查詢是比較複雜的。
package spc.esb.console.elasticsearch;
import com.alibaba.fastjson.JSONObject;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.sum.ParsedSum;
import org.elasticsearch.search.aggregations.metrics.sum.SumAggregationBuilder;
import org.elasticsearch.search.aggregations.pipeline.ParsedSimpleValue;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregatorBuilders;
import org.elasticsearch.search.aggregations.pipeline.bucketscript.BucketScriptPipelineAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ESSearch
{
// @Autowired
private RestHighLevelClient restClient;
//private final Logger log = LoggerFactory.getLogger(ESSearch.class);
private static final RequestOptions COMMON_OPTIONS;
static
{
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
public List<String> search(Map<String, String> matchQueryMaps) throws Exception
{
List<String> resList = null;
final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
SearchRequest searchRequest = new SearchRequest("liuhua");
searchRequest.scroll(scroll);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.profile(true);
/* 1用來統計流水號的聚合器 */
TermsAggregationBuilder serviceIdAgg = AggregationBuilders.terms("service")
.field("serviceId");
SumAggregationBuilder successNmAgg = AggregationBuilders.sum("successNum")
.field("successnm");
SumAggregationBuilder errorNumAgg = AggregationBuilders.sum("errrorNum").field("errornum");
SumAggregationBuilder timeoutNumAgg = AggregationBuilders.sum("timeoutNum")
.field("timeoutnum");
// 用來計算每個bucket的和
Map<String, String> bucketsPaths = new HashMap<>();
bucketsPaths.put("sucnm", "successNum");
bucketsPaths.put("errnm", "errrorNum");
bucketsPaths.put("tionm", "timeoutNum");
Script successScript = new Script("params.sucnm/(params.sucnm+params.errnm+params.tionm)");
Script errorScript = new Script("params.errnm/(params.sucnm+params.errnm+params.tionm)");
Script tionmScript = new Script("params.tionm/(params.sucnm+params.errnm+params.tionm)");
BucketScriptPipelineAggregationBuilder sucessBucketScript = PipelineAggregatorBuilders
.bucketScript("successrate", bucketsPaths, successScript);
BucketScriptPipelineAggregationBuilder errorBucketScript = PipelineAggregatorBuilders
.bucketScript("errorrate", bucketsPaths, errorScript);
BucketScriptPipelineAggregationBuilder timeoutBucketScript = PipelineAggregatorBuilders
.bucketScript("timeoutrate", bucketsPaths, tionmScript);
serviceIdAgg.subAggregation(successNmAgg).subAggregation(errorNumAgg)
.subAggregation(timeoutNumAgg).subAggregation(sucessBucketScript)
.subAggregation(errorBucketScript).subAggregation(timeoutBucketScript);
searchSourceBuilder.aggregation(serviceIdAgg);
searchRequest.source(searchSourceBuilder);
System.out.println("日誌搜索查詢請求:{}"+searchRequest);
SearchResponse searchResponse = restClient.search(searchRequest, COMMON_OPTIONS);
System.out.println("日誌搜索查詢結果,條數:{}"+searchResponse.getHits().totalHits);
if (searchResponse.getHits().totalHits == 0)
{
return null;
}
else
{
if ("OK".equals(searchResponse.status().toString()))
{
/* 第一中嘗試獲取groupby的方式 */
Aggregations terms = searchResponse.getAggregations();
for (Aggregation a : terms)
{
ParsedStringTerms teamSum = (ParsedStringTerms) a;
for (Terms.Bucket bucket : teamSum.getBuckets())
{
Map subaggmap = bucket.getAggregations().asMap();
double sNum = ((ParsedSum) subaggmap.get("successNum")).getValue();
double fNum = ((ParsedSum) subaggmap.get("errrorNum")).getValue();
double tNum = ((ParsedSum) subaggmap.get("timeoutNum")).getValue();
double successrate = ((ParsedSimpleValue) subaggmap.get("successrate"))
.value();
double errorrate = ((ParsedSimpleValue) subaggmap.get("errorrate")).value();
double timeoutrate = ((ParsedSimpleValue) subaggmap.get("timeoutrate"))
.value();
System.out.println(bucket.getKeyAsString() + " " + bucket.getDocCount() + " "
+ " 成功數 : " + sNum + " " + " 失敗數 : " + fNum
+ " 超時數 : " + tNum + " 成功率 : " + successrate
+ " 錯誤率 : " + errorrate + " 超時率 : " + timeoutrate);
}
}
resList = new ArrayList<>();
SearchHit[] searchHits = searchResponse.getHits().getHits();
for (SearchHit hit : searchHits)
{
String res = hit.getSourceAsString();
if (hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0)
{
JSONObject resJson = JSONObject.parseObject(res);
HighlightField highlightField = hit.getHighlightFields().get("message");
String highlighMessage = highlightField.getFragments()[0].string();
String repMessage = highlighMessage.replace("\\tat",
" ");
resJson.put("message", repMessage);
resList.add(resJson.toJSONString());
}
else
{
String repMessage = res.replace("\\tat", " ");
resList.add(repMessage);
}
}
}
else
{
System.out.println("查詢失敗!");
}
}
return resList;
}
// public static void main(String[] args)
// {
// try
// {
// ESConfig esConfig = new ESConfig();
// RestHighLevelClient client = esConfig.highLevelClient();
// ESSearch esSearch = new ESSearch();
// esSearch.restClient = client;
// Map<String, String> matchQueryMaps = null;
// esSearch.search(matchQueryMaps);
// Thread.sleep(10000);
// }
// catch (Exception e)
// {
// System.out.println(e.getMessage());
// }
//
// }
}
以上代碼實現的SQL是
SELECT
sum( successNum ),
sum( errrorNum ),
sum( timeoutNum ),
sum( successNum ) / ( sum( successNum ) + sum( errrorNum ) + sum( timeoutNum ) ) AS 正確率,
sum( errrorNum ) / ( sum( successNum ) + sum( errrorNum ) + sum( timeoutNum ) ) AS 錯誤率,
sum( timeoutNum ) / ( sum( successNum ) + sum( errrorNum ) + sum( timeoutNum ) ) AS 超時率
FROM
索引中的 type
GROUP BY
serviceId
說明:
TermsAggregationBuilder serviceIdAgg = AggregationBuilders.terms("service").field("serviceId");
JavaAPI中這個builder就是group by 的對應API,因爲sql中group by是將所有的其他查詢字段包在裏面的,所以,可以看到Java代碼中的其他的所有聚合函數都是被這個builder作爲子查詢的。
總結
到此爲止,實戰已經差不多了,剩下的就是業務代碼的實現。
代碼敲一遍,json敲一遍,感覺就會學到很多東西。官網整整看了一週,感謝我的老師也是領導的指導與寬容 ,讓我啥也不幹,看官網英文文檔看了一週。得到的經驗就是,靜下心來打開有道邊翻譯邊看官網,比看很多網上的其他資料有用千百倍。
最後再貼一點pipeline的配置的Java代碼
算了,不貼了。