Використання Elasticsearch як основної БД
Цього року я тиждень розбирався з мікросервісом, де Elasticsearch використовується як основна єдина база даних, щоб знайти й виправити помилку. В статті наведу приклади такого використання, опишу причини та наслідки.
Лічильник як спрощений приклад
Розглянемо трійку варіантів оновлення лічильника переглядів в Elasticsearch.Аналогія в SQL:
CREATE TABLE views ( id INT PRIMARY KEY, count INT NOT NULL ); INSERT INTO views (id, count) VALUES (1, 0); SELECT id, count FROM views WHERE id = 1; -- run 1000 times UPDATE views SET count = count + 1; SELECT id, count FROM views WHERE id = 1;
Перший приклад (послідовне оновлення в циклі 1000 разів)
package main
import (
"io"
"strings"
"testing"
es8 "github.com/elastic/go-elasticsearch/v8"
"github.com/stretchr/testify/require"
)
func TestCounterSequence(t *testing.T) {
es, err := es8.NewDefaultClient()
require.NoError(t, err)
const viewsIndex = "views"
// language=JSON
const viewsMapping = `{
"properties": {
"id": {
"type": "number"
},
"views": {
"type": "number"
}
}
}`
resetMapping(t, es, viewsIndex, viewsMapping)
const documentID = "1"
// language=JSON
const document = `{
"id":"1",
"views":0
}`
createDocument(t, es, viewsIndex, documentID, document)
printDocument(t, es, viewsIndex, documentID)
const count = 1000
{
// language=JSON
const updateScript = `{
"script": {
"lang": "painless",
"source": "ctx._source.views += 1;"
},
"query": {
"term": {
"_id": "1"
}
}
}`
for i := 0; i < count; i++ {
updateDocumentByQuery(t, es, viewsIndex, updateScript)
}
}
printDocument(t, es, viewsIndex, documentID)
}
func resetMapping(t *testing.T, es *es8.Client, index, viewsMapping string) {
es.Indices.Delete([]string{index})
_, err := es.Indices.PutMapping(
[]string{index},
strings.NewReader(viewsMapping),
)
require.NoError(t, err)
}
func createDocument(t *testing.T, es *es8.Client, viewsIndex string, documentID string, document string) {
_, err := es.Create(
viewsIndex,
documentID,
strings.NewReader(document),
)
require.NoError(t, err)
}
func updateDocumentByQuery(t *testing.T, es *es8.Client, viewsIndex string, updateScript string) {
_, err := es.UpdateByQuery(
[]string{viewsIndex},
es.UpdateByQuery.WithBody(strings.NewReader(updateScript)),
)
require.NoError(t, err)
}
func printDocument(t *testing.T, es *es8.Client, index, id string) {
response, err := es.Get(
index,
id,
es.Get.WithPretty(),
)
require.NoError(t, err)
defer response.Body.Close()
content, err := io.ReadAll(response.Body)
require.NoError(t, err)
t.Log(string(content))
}go test . -v -count=1
=== RUN TestCounterSequence
main_test.go:106: {
"_index" : "views",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : "1",
"views" : 0
}
}
main_test.go:106: {
"_index" : "views",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"_seq_no" : 2,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : "1",
"views" : 2
}
}
--- PASS: TestCounterSequence (3.16s)
PASS
ok escounter 3.196sДругий приклад (послідовне оновлення в циклі 1000 разів з refresh)
До кожного запиту в Elasticsearch додам ?refresh=true й повторно запущу тест.
func createDocument(t *testing.T, es *es8.Client, viewsIndex, documentID, document string) {
_, err := es.Create(
viewsIndex,
documentID,
strings.NewReader(document),
// WithRefresh
es.Create.WithRefresh("true"),
)
require.NoError(t, err)
}func updateDocumentByQuery(t *testing.T, es *es8.Client, viewsIndex, updateScript string) {
_, err := es.UpdateByQuery(
[]string{viewsIndex},
es.UpdateByQuery.WithBody(strings.NewReader(updateScript)),
// WithRefresh
es.UpdateByQuery.WithRefresh(true),
)
require.NoError(t, err)
}func printDocument(t *testing.T, es *es8.Client, index, id string) {
response, err := es.Get(
index,
id,
es.Get.WithPretty(),
// WithRefresh
es.Get.WithRefresh(true),
)
require.NoError(t, err)
defer response.Body.Close()
content, err := io.ReadAll(response.Body)
require.NoError(t, err)
t.Log(string(content))
}go test . -v -count=1
go test . -v -count=1
=== RUN TestCounterSequence
main_test.go:111: {
"_index" : "views",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : "1",
"views" : 0
}
}
main_test.go:111: {
"_index" : "views",
"_type" : "_doc",
"_id" : "1",
"_version" : 1001,
"_seq_no" : 1000,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : "1",
"views" : 1000
}
}
--- PASS: TestCounterSequence (14.27s)
PASS
ok escounter 14.303sТест другого прикладу виконується за 14 секунд (першого за 3 секунди), але збереглись всі перегляди.
Третій приклад — розпаралелене оновлення в циклі 1000 разів з refresh)
const (
parallel = 2
)
var (
wg sync.WaitGroup
rate = make(chan struct{}, parallel)
)
for i := 0; i < count; i++ {
wg.Add(1)
rate <- struct{}{}
go func() {
defer func() {
_ = <-rate
wg.Done()
}()
updateDocumentByQuery(t, es, viewsIndex, updateScript)
}()
}
wg.Wait()go test . -run=TestCounterParallel -v -count=1
=== RUN TestCounterParallel
main_test.go:190: {
"_index" : "views",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : "1",
"views" : 0
}
}
main_test.go:190: {
"_index" : "views",
"_type" : "_doc",
"_id" : "1",
"_version" : 130,
"_seq_no" : 129,
"_primary_term" : 1,
"found" : true,
"_source" : {
"id" : "1",
"views" : 129
}
}
--- PASS: TestCounterParallel (1.87s)
PASS
ok escounter 1.892s| Приклад | Число успішних збережень | Час |
|---|---|---|
| 1 | 2 | 3.16s |
| 2 | 1000 | 14.27s |
| 3 | 129 | 1.87s |
Причини
В мене відсутня достовірна інформація стосовно причин, пройшло достатньо часу, але версія, що дійшла до мене, така:Потрібно було швидко розробити готове рішення, тому в Elasticsearch одразу зберігали документ у форматі, який далі будуть віддавати в API.
Але забули про відсутність транзакцій в Elasticsearch.
Наслідки
- Інколи трапляється, що квитків на сеанс можна купити більше, ніж є в наявності.
- Розчаровані покупці пишуть гнівні відгуки про сервіс.
- Новий розробник, якому дали цей сервіс, як першу задачу, звільнився через тиждень, але звісно підписував договір, що забороняє розголошувати комерційну таємницю.
- Переписування мікросервісу, імпорт даних та збереження сумісності зі старою версією мікросервісу буде коштувати в рази дорожче ніж написати одразу з транзактивною БД.
Як виглядає мікросервіс зараз
Хоч сервіс й використовує Elasticsearch, але відсутній функціонал, де потрібен повнотекстовий пошук. Поля, за якими відбувається фільтрація, без індексів.
У квитків є статуси, зміна яких розмазана за двома мікросервісами, що потрібно докласти значних зусиль, щоб зрозуміти зміни статусів та фільтрації з двома десятками функцій з різними запитами.
Другий мікросервіс, що підключає перший як бібліотеку, робить підключення до Elasticsearch й викликає публічні функції першого мікросервісу для створення квитків до сеансів або скасування сеансів.
Звісно, відсутня документація (README.md містить лише назву мікросервісу).
Є додаткові скрипти, які запускаються по крону та виправляють, що можуть.
Альтернативи Elasticsearch
| Назва | Написаний на | Репозиторій | Число зірочок |
|---|---|---|---|
| Meilisearch | Rust | github.com/meilisearch/meilisearch | 30.5k stars |
| Zinc | Go | github.com/zinclabs/zinc | 12.6k stars |
Епілог
Elasticsearch — хороший пошуковий рушій, яким задоволений й продовжую використовувати. Зазвичай в проєктах була одна функція для збереження в Elasticsearch й дві-три для пошуку та побудови фільтру.На щастя мікросервіс вже почали переписувати в новому репозиторії.
48 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівТанцы с бубном начинаются когда у вас миллионы записей и вам нужно изменить маппинг. Эластик не позволяет такое. Придется создать новый индекс с новым maping-ом.
Таким образом появляется задача версионирования индекса, с созданием алиаса.
Но это все тоже решаемая задача.
Виглядає на одруківку — мабуть мало бути count = count + 1
Так, дякую
Стаття про те що багато хто хотів але соромився зробити =) Дякую!
Начебто усюди пишуть, включаючи elastic search docs, що це НЕ база данних, нахіба всі ці збочення?!
Так, то може тому й пишуть, що зустрічали такі мікросервіси про які соромно писати.
techlab.bol.com/...as-the-primary-data-store
не все верят)
Але при цьому усі використовують як базу даних логів у ELK стеку ;-)
Які ознаки бази даних?
В випадку ElasticSearch це можливість вичитати назад оригінальні значення полів.
www.elastic.co/...urrent/mapping-store.html
Якщо пропадуть логи то ніхто не помре, а от якщо данні то буде боляче
Нормально — ніхто не помре. А аудит як вести?
Ви логи з метриками не плутаєте часом? Логи — шукати баги, метрики робити аудіт, малювати графіки в графані
Це ви плутаєте. Аудит це хто що коли робив. Метрики не про це.
А логи яким боком до цього?
В логах — раптово — можна побачити хто коли і що робив. Особливо якщо логати в MDC юзер айді і подібні поля.
Ви колись про gdpr чули? Якщо хочете опустити контору на гроші то логи саме те місто де треба логати user id та інші такі поля.
User ID не є PII (personal identifiable information), шановний — це не email, не номер телефону і не адреса.
Освіжіть свої знання GDPR замість того щоб вимахуватись в коментарях.
Вимахуетесь ви, аудіт на логах не роблять
logz.io/...logs-security-compliance
“A log from any network device, application, host, or operating system can be classified as an audit log if it contains the information mentioned above and is used for auditing purposes.”
www.datadoghq.com/...dge-center/audit-logging
cloud.google.com/logging/docs/audit
etc etc
Вимахуюсь я, і вся індустрія разом зі мною. А Ви ну зовсім не вимахуєтесь.
Йдіть обфускуйте databse ID для юзерів в своїх логах — бо GDPR :D
Я згоден погодитись що метрики то не зовсім аудіт, але якщо потрібен тру аудіт то це лежить в базі в виді івентів, поряд з основними даними, але ніяк не в логах. Ну і повертаючись до того з чого ми почали, я не вважаю еластік надійним сховищем, бо це в принципі не сховище, а лише інструмент для індексації
Автори ElasticSearch ридають і рвуть волосся на голові — пан Таран не вважає їх рішення надійним сховищем!
Хтось вважає так, хтось сяк, хтось ще щось як — невідомо на основі чого. «От не вважаю і фсьо — переконуйте мене!» :D
А в цей час світова індустрія зберігає логи в ELK, і зовсім не тому що їм наплювати чи логи пропадуть.
Лол ти смішний. Логи там зберігають тільки тому що зручно по них шукати.
Впевнений що «тільки»?
Так чи інакше — зберігають. Отже це БД.
Файлова система виходить теж БД
www.elastic.co/...nd-elasticsearch-as-nosql
ElasticSearch це та БД що з BASE (Basic Availability, Soft State, Eventual Consistency)
www.coursera.org/...sticsearch-overview-w38A9
“We will examine Elasticsearch as an example of a BASE-style (Basic Availability, Soft State, Eventual Consistency) database approach, as well as compare and contrast the advantages and challenges associated with ACID and BASE databases”
Так з цим ніхто не сперечається. Тут мова йде про те, що на реальних проектах не можна використовувати базу данних з «Basic Availability, Soft State, Eventual Consistency» як основну. В ELK використовують еластік, не тому, що це хороша база даних, а тому, що це компроміс, в якому пожертвували цілісністю логів задля економії місця на диску і загальному спрощенню всієї системи.
Цікаве формулювання
Веселіше було б навпаки — цього тижня я рік розбирався :D
А что, хороший подход: взять в проект базу, главная фича которой — полнотекстовый поиск, но не использовать его, зато пытаться использовать эту базу как реляционку. И что тут могло пойти не так? Хм..
Ну и про CAP теорему почитать рекомендую.
а потім
Кстати, вот этот пункт я не понял:
Он тут к чему? Автор статьи такой договор не подписывал? Или о чем тут речь?
Звісно я підписував й на подальші подібні питання зможу відповісти через пару-трійку років.
А з іншого боку взагалі-то попередній розробник успішно перейшов від класичних реляційних CA до AP і успішно вирішив проблеми горизонтального масштабування на майбутнє (якого з бізнес-логікою в ES нема).
Якщо виправленням займався я то ймовірно попередній розробник сервісу ще й успішно перейшов в іншу компанію
И какое business value это принесло в конкретном случае?
А она, эта проблема, вообще была? Я нигде не увидел такого упоминания. Имхо больше похоже на классический паттерн «поиграть в Гугл и решать не реальную проблему, а ту что интересно»
Принесло «досвід з Elasticsearch» в резюме автора, не більше.
так собi досвiд
Чому ж? В use-cases здорової людини Elasicsearch дуже потужний інструмент.
Resume-driven development
намагаєтеся менше згадувати про це — а то всі будут вважати неосиляторами.