因爲歷史緣由,筆者所在的公司原有的ES查詢驅動採用的是 PlainElastic.Net, 通過詢問原來是以前PlainElastic.Net在園子裏文檔較多,上手比較容易,因此最初做者選用了該驅動,而發佈也因爲歷史緣由都部署在 windows 服務器上,基於 .NET Framework開發。html
後來因爲遷移 .NET CORE 平臺的須要,對代碼進行了升級,同時部署平臺也遷移至 CentOS7 服務器,升級過程比較順利,因爲沒有使用特殊API,因此幾乎沒有對業務代碼作更多的修改,同時測試階段因爲沒有多餘的機器,仍然放在了原有Windows服務器上作的測試,一切都沒有問題,完美上線。git
事發忽然,某天接到運維部門反饋,部署查詢服務的機器忽然出現 TCP 鏈接數超高的問題,同時這臺機器其餘的TCP服務也沒法創建新的鏈接,但已經創建的鏈接不受影響。聯想到 ElasticSearch 查詢服務是基於HTTP 請求的,腦子裏立刻聯想到 .NET Core 下 HttpClient 若是每次訪問都建立新實例,則會每次都創建新的TCP鏈接,而 Linux 對已釋放端口回收的時間窗口,會致使在高併發狀況下,客戶端機器端口占用持續增長,同時被調用服務端鏈接數也會持續增長。github
基於此猜想,立馬去扒了一下PlainElastic.Net源代碼:windows
源碼地址:https://github.com/Yegoroff/PlainElastic.Net/blob/master/src/PlainElastic.Net/Connection/ElasticConnection.cs
果真如猜想的那樣,每次都建立了新的 HttpWebRequest 實例,看了做者的最後維護時間也已是3年前了,多是後來官方驅動日趨完善,做者也便中止了維護。api
既然如此,那麼讓咱們看下官方最新驅動源碼是否如咱們想象,是基於HttpClientFactory來解決這個問題的?
安全
上述代碼看來,官方驅動並不是是採用微軟官方建議的 HttpClientFactory ,而是官方底層本身維護的一個線程安全的字典來管理 HttpClient 實例池,雖是本身實現,但效果同樣:相同地址的請求,是連接複用的,這樣就解決不斷開啓 TCP 鏈接的問題。併發
問題找到,立馬進行驅動升級:框架
說明: ElasticSearch.Net官方驅動地址:https://www.elastic.co/guide/en/elasticsearch/client/net-api/6.x/index.html運維
官方驅動分爲 Low Level Client 和 NEST(Heigh Level Client),其中Low Level Client 僅僅作了最基本的封裝,幾乎等價於HTTP原生調用,帶來了極大的靈活性的同時,也帶來使用成本,而對於開發人員來講使用 NEST 提供的更加高級的API,能夠更加快速的進行開發工做,也同時能夠利用到 .NET 所提供的各類語法糖,好比 => 表達式。
話很少說,看示例代碼:
public ElasticService() { var uris = new Uri[] { new Uri("http://172.17.78.111:9200"), new Uri("http://172.17.78.112:9200") }; //支持多個節點 var connectionPool = new SniffingConnectionPool(uris); var settings = new ConnectionSettings(connectionPool).DefaultIndex("testindex");//注意index不能夠大寫 settings.BasicAuthentication("", ""); //設置帳號密碼,沒有能夠跳過 this._client = new ElasticClient(settings); }
public class People { public Guid Id { get; set; } public string Name { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } public bool Gender { get; set; } public string Address { get; set; } public DateTime CreateTime { get; set; } = DateTime.Now; } //批量插入 public async Task<IBulkResponse> AddPeopleAsync(People[] peoples) { var descriptor = new BulkDescriptor(); foreach (var p in peoples) { var response = await _client.IndexDocumentAsync(p); descriptor.Index<People>(op => op.Document(p)); } return await _client.BulkAsync(descriptor);//批量插入 }
public QueryContainer BuildQueryContainer(SearchCondition condition) { var queryCombin = new List<Func<QueryContainerDescriptor<People>, QueryContainer>>(); if (!string.IsNullOrEmpty(condition.Name)) queryCombin.Add(mt => mt.Match(m => m.Field(t => t.Name).Query(condition.Name))); //字符串匹配 if (condition.Age.HasValue) queryCombin.Add(mt => mt.Range(m => m.Field(t => t.Address).GreaterThanOrEquals(condition.Age))); //數值區間匹配 if (!string.IsNullOrEmpty(condition.Address)) queryCombin.Add(mt => mt.MatchPhrase(m => m.Field(t => t.Address).Query(condition.Address))); //短語匹配 if (!condition.Gender.HasValue) queryCombin.Add(mt => mt.Term(m => m.Field(t => t.Gender).Value(condition.Gender)));//精確匹配 return Query<People>.Bool(b => b .Must(queryCombin) .Filter(f => f .DateRange(dr => dr.Field(t => t.CreateTime) //時間範圍匹配 .GreaterThanOrEquals(DateMath.Anchored(condition.BeginCreateTime.ToString("yyyy-MM-ddTHH:mm:ss"))) .LessThanOrEquals(DateMath.Anchored(condition.EndCreateTime.ToString("yyyy-MM-ddTHH:mm:ss")))))); }
提示:Match 和 MatchPhrase 的區別,例如對於"長寧區"
- Match 會將"長寧區"進行分詞匹配,例如只要包含"區"的數據(好比靜安區),也會被查詢命中
- MatchPhrase 則能夠理解爲短語匹配,只有當數據包含「長寧區」完整短語的數據,纔會被查詢命中
public async Task<PagedResult<People[]>> QueryPeopleAsync(SearchCondition condition, int pageIndex, int pageSize) { var query = this.BuildQueryContainer(condition); var response = await this._client.SearchAsync<People>(s => s .Index("testindex") .From(pageIndex * pageSize) .Size(pageSize) .Query(q => query) .Sort(st => st.Descending(d => d.CreateTime))); if (response.ApiCall.Success) { return new PagedResult<People[]> { PageIndex = pageIndex, PageSize = pageSize, Total = response.Total, ReturnObj = response.Hits.Select(s => s.Source).ToArray() }; } return new PagedResult<People[]> { IsSuccess = false }; }
[TestMethod] public async Task QueryPeopleTest() { var condition = new SearchCondition { Address="長寧區", BeginCreateTime = DateTime.Now.AddDays(-1), EndCreateTime = DateTime.Now }; var result = await this._elasticService.QueryPeopleAsync(condition, 0, 3); Assert.IsTrue(result.IsSuccess); }
將抓包的數據轉換爲HTTP流,查看請求細節:
提示:經過wireshark抓包是排查錯誤頗有效的方式,有時候經過查詢文檔進行分析,還不如先抓包查看請求數據來得直接,同時能夠將抓包數據放在Kabana所提供的 Dev Tools中驗證本身的想法。
從.NET Framework 平臺轉向 .Net Core 平臺,其實不只僅是開發框架的升級,或者從 Windows 轉向 Linux 的遷移,而是須要咱們有更多的開源思惟,即:
本文示例代碼地址:https://github.com/xBoo/articles/tree/master/src/ElasticSearchNetDemo