前段時間與同事一起爲產品接入了 Elasticsearch 框架技術。從參與方案會議到搭建開發上線過程中有很多討論點,故產生本文,希望藉此總結和分享一些經驗。
1. 業務模型
接觸已有的業務時,數據模型是最早需要知道的信息。我和同事負責接入 Elasticsearch 的產品是一個業務繁多的通訊錄,簡化下來就是 3 個關鍵的模型,如下:
- 部門(Department)
- 人員(User)
- 標籤(Tag)
它們的用途和聯繫,就跟它們的詞義一樣。
由此產生的業務如下:
- 通過 標籤 查詢 部門、人員
- 通過 部門 查詢 人員
基於以上模型和業務,在典型的關係型數據庫下,爲了實現關聯關係,自然會有額外的關聯表:
- 部門人員關聯表:每條記錄包含1個部門,1個人員
- 標籤對象關聯表:每條記錄包含1個標籤,1個部門或人員
2. 需求
Elasticsearch 的特點有全文檢索、分佈式、海量數據下近實時查詢。
當時爲通訊錄業務引入 Elasticsearch 的需求和目標如下:
- 多字段的匹配或模糊查詢。這些部門、人員、標籤數據原本存儲在 MySQL 中,如果要做匹配多個字段的模糊查詢就比較吃力了,考慮一個常用功能 “輸入姓名/手機號/拼音/首字母來查詢人員”。而快速查詢此類業務是 Elasticsearch 可以提供的。
- 基礎模塊能力。其他業務模塊也提出了類似全文檢索的需求,因此在通訊錄業務首次應用 es 時,要定義和提供好 es 的訪問和工具方法,供其他模塊在未來接入時,能複用一些實現,能保持一致的接口和命名風格等。
3. 索引設計
從原 MySQL 數據庫表,到 Elasticsearch 的索引,數據模型的變化稱爲異構。
Elasticsearch 適合解決在 MySQL 中多條件或連表這樣比較慢的查詢業務,因此除了原有的信息字段,我們會再附加 3 個模型的關聯關係到 es 索引中。
索引 字段 | 原有 | 關聯關係 |
---|---|---|
部門 | 部門名、完整部門路徑名 | (無) |
人員 | 姓名、拼音、首字母、手機號 | 父部門Id、所有父級部門Id、標籤Id |
標籤 | 標籤名 | 部門Id、人員Id |
(上表略去了一些無關本篇內容的字段,如 SaaS 平臺的租戶Id、每個對象的信息詳情字段)
- 是否需要添加關聯關係的字段,是由業務需求決定的。拿人員索引的 “所有父級部門Id” 舉例子,因爲有查詢部門下所有人員(包括直屬、子部門下的)的業務需求,所以會設計這麼一個字段。
- 可以使用 Elasticsearch 的分詞功能來記錄關聯關係的字段中。爲該字段定義一個分隔模式爲豎線 “|” 的分詞器,把若干個關聯Id存成一個拼接的字符串。
4. 版本選擇
同事是個版本控,在選擇版本時瞭解和考慮了非常多的信息。不過版本選擇確實是爲平臺接入新技術時的一個重要考慮點。我們提出這個方案的當時(2018年4月),對比了主要使用的雲服務提供商的幾個版本,考慮項可以按優先級概括爲:
- 穩定的
- 案例資料多的
- 時新程度,包括 Elasticsearch版本 和 Lucene版本
- 我們已經使用了某家雲服務提供商,會偏向再用其提供的服務
幾個版本對比
我們當時選擇了 Elasticsearch 6.2.2 版本。
v5.6.4
- 是 Spring 整合的各個框架中,支持數最多的版本
- 市面使用人數較多,資料較多
- 其依賴的 Lucene 大版本是v6,較舊
v6.2.2
- 是當時穩定的版本中最新的,性能比 v5 好
v6.2.4
- 是當時最新的版本,修復了許多 bug
- 性能更好,是官方推薦的版本
- 官方的技術文檔部分還沒更新,得看舊文檔
- 市面上找不到相應的人的使用資料
版本發展(於2019年4月)
在寫本篇文章時,我再去了解了和 Elasticsearch版本 相關的變更:
Elasticsearch
穩定版本中最新的是 v7.0、v6.7- 依賴的
Lucene
版本分別爲 v8.0、v7.2 - Spring 的穩定支持程度爲:v3.2.x 的
spring data elasticsearch
支持 v6.5.0 的 elasticsearch 版本,比最新版本低一些。
5. 導入已有數據
考慮到要使用 Elasticsearch 時,通常意味着已經有很多數據了。首次使用自然會有導入已有數據的過程,而且這些數據量都是很大的。
我們的方案是 JDBC 查詢並提交給 es。設計要點有:
- 分批。數據量之大已經無法一次存到內存中。數據按明確的邊界劃分而獨立,會讓多線程處理、日誌記錄、重試都變得輕鬆。按租戶來劃分就是一種好的方式。
- 緩慢。避免影響線上的服務,同時適當給 JVM 回收和 Elasticsearch 處理留一點時間。
- 異常。信息彙總和失敗重試。
具體設計細節如下:
- 爲 SaaS 系統的每個租戶創建一個任務,提交到
ExecutorCompletionService
。 - 在該租戶的任務中:
一次查詢所有部門;
分頁查詢所有人員、部門人員關聯;
一次查詢所有標籤,標籤對象關聯; - 將關聯關係做成便於查詢的數據結構,以用於添加 es 文檔時的快速查詢。
例如,映射<人員,部門>可用於查詢:人員所屬的部門;
例如,映射<部門,標籤>可用於查詢:部門所貼的標籤;
用到了 Guava 的Multimap
,以達到類似於 Map<String, Set<String>> 的效果。 - 建立新增 es 文檔的批量請求
BulkRequest
。對於每個對象,都可以用上一步做好的結構快速獲取其關聯關係。 - 提交批量新增請求給 es。
6. 數據源同步
我們的 MySQL 數據同步到 Elasticsearch 的方案,是在應用層基於事件通知進行的。以人員對象爲例,步驟如下:
- 人員的增刪查改事件,都會通知給其他訂閱者。這是已有的邏輯;
- 設計一個“記錄人員變動”訂閱者,被通知時,將變動儲存起來;
- 設計一個“Es同步”定時任務,每天凌晨,取出變動記錄,提交到 Es,之後刪除變動記錄;
看到這個方案,你可能會問爲什麼不使用像 Logstash 等成熟的框架或插件,而是自寫一套同步方法?原因如下:
- 我們選擇的 MySQL 雲服務提供商在當時沒有提供 binlog 日誌訪問。這使我們無法選擇一些基於日誌的同步方案。
- 部門、人員、標籤的數據表原本沒有像 update_time 這樣的——能反映變更的列。故又可以排除基於時間去增量同步的方案。
- 人員、標籤表的數據量很大,如果要增加一列 update_time 並加上索引,帶來的成本有:額外的儲存空間(我們購買的雲服務空間每增長百G每年的成本大約是1000元);新字段給應用層帶來的維護成本。
- 設計出來的 Es 索引和 MySQL 表的字段不同。一些在 Es 索引中新增的字段,是需要在 MySQL 中做額外查詢才能得到的。