#OpenSearch
29 нотаток
- ActiveRecord
- AmazonRedshift
- API
- AppleScript
- AWS
- AWSLambda
- Cadencia
- CGO
- Chezmoi
- CI
- Clojure
- Cloudflare
- CloudflarePages
- CloudWatch
- CssParser
- CтохастичнийТаймтрекер
- DNS
- Docker
- Dotfiles
- Fly.io
- GCP
- Git
- GitHub
- GitHubActions
- Go
- Golang
- GTD
- HomeAssistant
- Hugo
- I18next
- JavaScript
- Jira
- JSON
- Kafka
- MacMiniВДорозі
- MacOS
- Markdown
- Mastodon
- Obsidian
- ObsidianCanvas
- OmniWOPE
- OpenSearch
- Oura
- PagerDuty
- Ping
- Plausible
- PostgreSQL
- ReactNative
- Redis
- RSS
- Ruby
- RubyOnRails
- Sentry
- Sintra
- Slack
- SMTP
- SQL
- SQLite
- Svelte
- Swift
- SwiftData
- SwiftUI
- Telegram
- Terraform
- TLS
- TypeScript
- Vercel
- VPN
- WeightPlot
- WordPress
- XCode
- Адвент2024
- Бази Даних
- БДС
- Безпека
- Блокнот
- Вебтехнології
- ВолюІнформації
- Гаджети
- ДизайнМовПрограмування
- Ігри
- Інструменти
- ІнтеграційніТести
- Кава
- КеруваннняЗадачами
- Кодогенерація
- Криптографія
- Локалізація
- Маркетинг
- МетаПост
- МоїПроєкти
- Навігація
- Оптимізація
- ОсновиІнтернетБезпеки
- Помічник ШІ
- ПомічникШІ
- ПостПроПохід
- Програмування
- Продуктивність
- Проєкти
- Проза
- РДУГ
- Рівночасність
- РобочийКомп
- Розробка
- РозумнийБудинок
- СинПопросивПриготувати
- Сон
- СтохастичнийТаймтрекер
- Танці
- ХмарніТехнології
-
OpenSearch в тестовому оточенні
Як забезпечити кожному тесту чистий аркуш в OpenSearch? В теорії — просто. Проте щоб це ще й не гальмувало збірку, довелося попрацювати.
-
Зазвичай з PostgreSQL ми вживаємо транзакції. В OpenSearch транзакцій немає. Значить, просто скасувати зміни після тесту не вийде. Другий підхід — це команда
TRUNCATE. Такої в OpenSearch теж немає; щоправда, є API “delete by query”, яким можна видалити з індексу всі документи, але вона не ефективна. -
Отже, залишається тільки видаляти цілі індекси та створювати наново. На жаль, створення індексів не така швидка операція, щоб робити її без потреби. Тому я порефакторив код запитів, щоб він допускав відсутність індексів (тобто інтерпретував як порожній результат.) Таким чином, індекси ми створюємо тільки тоді, коли в них пишуться дані. (А значить, і видаляти потрібно не так багато, а коли тест не звертається до OpenSearch - то взагалі нічого.)
-
До речі, на відміну від PostgreSQL, схема даних зберігається в шаблонах, а не в самих індексах. Тому створювати індекси можна навіть не окремою операцією, а неявно, під час додавання до індексу першого документу.
-
Нарешті, спочатку я видаляв індекси так, як чищу таблиці в PostgreSQL, тобто кожний індекс окремим викликом. Виявилося, що це повільно (і навіть паралелізація викликів не допомагає.) Тому перейшов до очищення всіх індексів за префіксом
test. -
Ідея, якою я ще не скористався: для кожного тесту генерувати унікальний префікс для ізоляції даних. Тоді можна видаляти всі тестові дані наприкінці.
-
-
Перевірка унікальності в OpenSearch
…А немає її! В ті чи інші причини. Певно, головним чином тому, що індексація відбувається асинхронно, а до індексації перевірки не буде.
Проте є один метод, перевірений часом. Це зробити унікальний ключ ідентифікатором. Таке працює в багатьох NoSQL базах, наприклад, в DynamoDB практично лягає в основу. Тільки з міркувань рівномірного розподілення значень в OpenSearch краще брати не прямо ключ (скажімо, URL сторінки), а хеш від нього.
(Взагалі підхід з використанням справжніх значень як ідентифікаторів називається “натуральні ключі” та був популярним в часи “голих” SQL баз. А з приходом ORM як головного методу доступу — занепав.)
Ідентифікатори в OpenSearch, звісно, унікальні. Але й тут криється нюанс - звичайна операція індексації не заборонить повторюваний ідентифікатор, а просто перезапише документ (тобто зробить “upsert”). А нам потрібна операція
create.Я вже натрапляв на проблему в тестах, що згенеровані фабрикою документи час від часу збігалися ідентифікаторами, вставлялися без
create, та призводили до випадкових помилок. -
OpenSearch: база, де все відбувається не відразу
Нарешті зміг для себе зрозуміти, що мене понад усе дратує в роботі з OpenSearch: майже нічого в цій базі не відбувається синхронно. Я звик, що база даних є синхронною; звісно, реплікація займає час, але той вузол, з яким працюєш, все робить коли йому скажеш. В OpenSearch все не так.
Навіть з базовими діями — наприклад, індексацією одного документа — є спеціальний параметр
refresh=wait_for, який змусить виклик чекати, поки документ не стане проіндексований. За замовчуванням індексація буде відкладена. Це відразу помічаєш у тестах, бо без цього параметра вони ніколи не будуть стабільними.А коли маєш справу зі складнішими операціями, взагалі краще думати про них як про процеси; в деяких випадках — буквально. Наприклад, в PostgreSQL масове оновлення (
UPDATE ... WHERE) теж може тривати навіть години, але таблиця може бути тільки у двох станах: не оновлена та оновлена. З OpenSearch масове оновлення опрацює скільки встигне та повернеться — далі запускай наново.Так само і перетворення хоч виглядають, як матеріалізовані розрізи, але працюють сторінка за сторінкою, тобто результат завжди буде неповним: причому не в тому сенсі, що відповідати деякому старому стану вхідних даних — а бути десь посередині, причому ми ніколи не впевнені, де саме.
Все це добре, коли вас влаштовує кінцева узгодженість, а чи буде вона досягнена зараз, чи через хвилину — байдуже. Та це, певно, найбільша перешкода у використанні OpenSearch як традиційної бази даних.
-
Оптимізація: треба йти глибше!
Далі, коли шукаєш причини уповільнення, мусиш ставити під сумнів кожний свій висновок та знаходити для нього обʼєктивні обґрунтування.
Вчора хотів написати пост “чому OpenSearch повільний, коли у вас надто багато шардів?” (Шард — це неподільний блок даних, як партиція; індекс — тобто колекція — складається з фіксованого набору шардів.) Всі радять робити шарди розміром у 10-50 Гб… але чи значить це, що робити їх меншими обовʼязково погано?
Виявилося, не дуже; дійсно, кожен шард коштує трохи памʼяті, але поки памʼять не вичерпана, додаткові шарди не суттєво впливають на швидкість операцій. Негативно — ні, але позитивно — теж ні, навіть попри те, що шард також є одиницею паралелізації. Бо щоб пришвидшити щось паралелізацією, потрібно більше ядер процесора, а не потоків.
Але це я знаю після того, як перевірив. А міг би залишитись із такою гадкою, витратити час на переформування індексів, та… нічого не отримати на заміну. Та це не перша та не остання ланка в розслідуванні! Перевіряти потрібно кожну.
Легко вказати на неприємну частину проєкту та сказати: проблема тут! (Мені, наприклад, доводилося витрачати час на алгоритмічну оптимізацію “некрасивого коду” там, де можна було обійтися як максимум кешем.) Але взагалі шукати причини — це ціле мистецтво. Треба знати, що важливо, що не важливо, та параноїдально собі не довіряти (що, до речі, називається науковим методом.)
Ото було колись — шукав, чому API повільний. Локально наче все було чудово, але більшість часу API проводив у серіалізації, тому я довго обирав оптимальний формат. А потім виявилося, що в продакшні є ще рукостискання TLS, та вся проблема зовсім не в серіалізації, а у встановленні підключення. Так що завжди перевіряти!
-
Затримка оновлення в OpenSearch
В OpenSearch/ElasticSearch є така цікава особливість, що між внесенням змін та їхнім ефектом минає кілька секунд. Тобто буквально доведеться почекати, щоб побачити, що щось змінилось.
Причиною є те, що OpenSearch видає результати тільки через словники (“індекси” мовою інших баз), а оновлення словника відбувається періодично — як мінімум, щосекундно. Бо оновлення є дорогою операцією та краще, коли воно працює з цілою пачкою змін.
Зауважу, що затримка ніяк не є наслідком того, що OpenSearch є розподіленою базою. Навіть один індекс на одній машині буде оновлюватися так саме повільно. Взагалі OpenSearch оптимізований для великого масштабу, а на малому працює не набагато швидше. Також затримка не впливає на надійність — зміни нікуди не дінуться.
З досвідом традиційних СУБД це є повною нісенітницею. Проте типові використання OpenSearch - наприклад, аналіз журналів — не потребують моментального застосування змін. Будь-яка архітектура з OpenSearch всередині повинна розділяти запис та читання та передбачати ці затримки. Тому на роль головної бази - OLTP - вона фізично не підходить.
-
Наскільки важливо розуміти спеціалізацію бази даних
Трохи допомагав хлопцям із проблемним навантаженням на ElasticSearch. Виявилося, проблема з запитами, де фігурував пошук зі скриптом. Скрипт виконував операцію на кшталт
foo - bar > THRESHOLD.Як побачив, то відразу все стало зрозуміло. OpenSearch будує з документів словники — де кожному значенню поля відповідає множина документів. OpenSearch швидкий, поки ти робиш пошук за словниками. Такий пошук виконується як операції над множинами, та тому є дійсно надзвичайно швидким, навіть коли в запиті згадується багато полів. Це — головна сила OpenSearch.
Але як тільки для пошуку доводиться читати самі документи — швидкість провалюється на порядок. Бо з операцій над множинами ми скочуємося до операцій над документами.
Тобто можливість пошуку за скриптом існує, щоб закрити рідку потребу, зробити щось неможливе можливим. Але вона ніяк не годиться для базового функціоналу. Це може приголомшити, якщо ти приходиш з бази загального призначення — як Postgres - де пошук по
foo - barне сильно відрізняється за пошуком поfooчиbar.В цьому сенсі я завжди любив документацію з Redis - в ній для кожної команди виписана обчислювана складність. Колись я робив складний рушій на основі Redis, та це було критично важливо для планування.
А у випадку з OpenSearch відповідь така: хочеш шукати за різницею полів — зберігай цю різницю в ще одно поле -
fooMinusBar. Та левову частку інтеграції OpenSearch складає саме планування наперед потрібних операцій та полів, які їх уможливлюють. -
Трошки з адміністрування OpenSearch
…Передісторія: так склалося що в одного з індексів OpenSearch цілих 100 шардів на приблизно 5 Гб даних. Стандартні рекомендації — це шарди розміром у 10-50 Гб, тобто десь у 1000 разів більші. Ну буває.
Десь місяців із 9 тому я писав, що це ні на що не впливає, але виявляється, все залежить від навантаження. Отже, стала задача звести кількість шардів до розумної… ну, наприклад, 2.
Робити це “наживу” OpenSearch не вміє. Тобто нам доведеться замінити індекс. (До речі, саме тому в OpenSearch також є псевдоніми індексів (alias) - щоб легко підміняти індекси або цілі набори.)
Є операція reindex. Вона бере кожний документ та копіює його в новий індекс. Триває все це кілька годин. Я вже писав, що все в OpenSearch, що працює на рівні документів — дуже повільне. Під час цього, якщо старий індекс відкритий до запису, накопичуються ще зміни, які наздоганяти мені не захотілося.
Тому є інша операція: shrink. Оце вже краще: вона на рівні структур бази обʼєднує шарди. Працює замість годин — лічені хвилини. Але ж не все так просто.
Перепоною стало те, що для виконання
shrinkвсі шарди індексу повинні бути на одному вузлі. Для того є інструкція прямо в документації вище. Але на практиці в нас OpenSearch починав цей переніс та не закінчував. Виявилося, що є обмеження на кількість шардів на вузол.Причому (!) в AWS OpenSearch обмеження це — як ми визначили — залежить не від розміру вузла. Та точно не від налаштувань — такі речі AWS не дає змінити, щоб ми не “наробили дурниць”. Дякуємо!
Ні — насправді AWS OpenSearch прагне зберегти баланс в кластері, а саме рівний розподіл шардів між вузлами. Ну й добре… створили ще один, порожній, індекс на 100 шардів. Після чого перенесення справжніх шардів на один вузол завершилося успіхом. Ну а далі й
shrinkвідбувся без подальших сюрпризів. Роботу зроблено.Це я ще не торкнувся того, наскільки безпорадним є UI OpenSearch Dashboards, в якому одне світле місце - Dev Console, тобто можливість надсилати до кластера команди HTTP API.
-
OpenSearch - база даних, яка не дуже шанує свої дані
От цікаво ж, що в OpenSearch/ElasticSearch головним змістом бази є індекси, а не дані.
Дані, тобто документи, можна взагалі не зберігати. Є режим, коли документи не зберігаються, а реконструюються з індексів -
synthetic _source. Або взагалі ніяк не зберігати — повертати тільки ID. Пішло це, як я розумію, від того, що ElasticSearch починалася як рушій пошуку для іншої бази.Але навіть коли ми зберігаємо документи, вони займають другорядну посаду. Доступ до них повільний. Як тільки ми спираємось на інформацію, недоступну з індексів — наприклад, робимо пошук за скриптом - запит уповільнюється в рази.
Також стикнувся з тим, що навіть просто запит, що повертає велику кількість документів — ненормально витратна операція. Тобто якщо хочете забрати з OpenSearch дамп чи щось схоже, краще обрати невеликий розмір сторінки. Так, запитів зробите більше, зате вони економніше використають памʼять та тому загальний час буде менше.
Тобто шукати ми можемо багато, а читати — менше. Ще одна цікава відмінність від типової бази даних. Та дивись, для деяких сценаріїв це дуже вигідно — наприклад, якщо документів дуже багато, але цікавлять нас одиниці. Як-от типове зберігання журналу. А для інших сценаріїв — взагалі не підходить.
-
_id в OpenSearch - не так ефективно, як можна подумати
В продовження теми того, що розуміння однієї бази даних не завжди переноситься на іншу.
В OpenSearch (чи ElasticSearch, бо одно й те саме) в кожного документа є ідентифікатор -
_id. Наче зрозуміле явище, за ним ми швидко знаходимо та змінюємо документи. Так саме як в PostgreSQL, наприклад.Але. Якщо вам раптом захочеться відсортувати за
_id, то виявиться, що прямої можливості це зробити в OpenSearch немає. Словник того не підтримує. Та тому для виконання запиту з"sort": "_id"база завантажить всі ID в памʼять. Цей маневр називається field data.З мільйонами документів в індексі ця
fielddataзайматиме гігабайт RAM, а то й більше — саме так я про неї й дізнався. Та дуже дивно було, що саме_id, з усіх полів, спричиняє такі витрати.Що робити натомість? Можна дублювати
_idвсередині документу. Можна сортувати за якимсь іншим полем. Зокрема, якщо задачею сортування є стабільність видачі (бо навіщо ще сортувати за ID?) - то в пошуку є спеціальне поле_shard_docяк раз для такої потреби.Власне, в останніх версіях OpenSearch навіть є налаштування
indices.id_field_data.enabled: false- воно примусово заборонить створенняfielddataз_id. Тож то й таке.