Translate

вівторок, 19 січня 2016 р.

Elasticsearch Cluster: Overview And Setup

Elasticsearch - це пошукова платформа майже реального часу (near real time, NRT). На практиці це означає, що існує лише мінімальна затримка (зазвичай біля секунди) між часом додавання документу до індексу і часом, коли він стає доступним для пошуку.

Elasticsearch, як і його основний конкурент Solr, базується на бібліотеці Lucene. Сам по собі Apache Lucene не являється повноцінним сервісом: це просто бібліотека для побудови пошукових систем, вона займається лише індексом та пошуком. А, власне, за введення даних, налаштування сервісу, кластеризацію, маршрутизацію пошукових запитів та ін. відповідає Elasticsearch чи інша "обгортка".

Використання та налаштування (переважно) здійснюється за допомогою RESTful API запитів. У випадку з unix-подібною OC з цим може допомогти curl. Але про це дещо далі.

Спочатку почнемо з базової термінології Elasticsearch.

Кластер (Cluster)

Сервери, що обслуговують один набір даних та забезпечують індексацію та їх пошук по всіх серверах. Кластер також може складатись лише з єдиного сервера, проте в цьому випадку не може йти мова про високу доступність.

Про те як створити кластер я напишу нижче.

Вузол (Node)

Це сервер, що входить в кластер. Як частина кластеру, звісно, також бере участь в індексації, пошуку та зберіганні даних (частини чи всіх, в залежності від конфігурації). В кластер також можуть входити вузли, що не зберігають данних. Їхня участь в цьому разі обмежується маршрутизацією запитів (щось на зразок балансувальника запитів) чи участі в обиранні майстра/майстрів.

Індекс (Index)

Індекс - це колекція документів, що мають певні подібні ознаки. Наприклад, окремий індекс для даних користувача, інший - для каталогу продуктів та ще один для даних замовлень. К-ть індексів, що можуть існувати в кластері необмежена. Якщо провести аналогію з MySQL - то це таблиця в базі даних.

Тип (Type)

У межах індексу, може бути описано один чи більше типів. Тип - це логічна категорія індексу, структура якої описується за потребою самим розробником. Загалом, тип описується для документів, що мають схожий загальний набір полів. Наприклад, в межах одного індексу, можуть існувати окремі типи для даних користувача, даних записів блогу та, наприклад, даних коментарів користувачів. Я думаю, в певному сенсі, можна провести аналогію з таблицею в реляційній базі данних.

Документ (Document)

Документ - базова одиниця інформації, що може бути проіндексована. Наприклад може існувати документ для окремого користувача, документа чи одного замовлення. Документ описується в JSON (JavaScript Object Notation) форматі.

В межах індексу/типу може зберігатись будь-яка кількість документів, що необхідна. Документ фізично розміщується в індексі та йому має бути призначений певний тип.

Шарди (Shards)

Індекси потенційно можуть зберігати великий обсяг даних, що можуть перевищувати обсяг постійної пам’яті вузла. Для вирішення цієї проблеми Elasticsearch надає можливість ділити індекси на шматки, що звуться шардами (shards).

Під час конфігурації демона (відповідно, це значення буде виставлятися по замовчуванню для кожного нового індексу) чи створення нового індексу можна указати на скільки шардів необхідно ділити індекс. Кожен шард - це повноцінний індекс (точніше його частина), що може розміщуватись на будь-якій ноді кластеру.

Поділ індексів на шарди забезпечує такі можливості:
  • Горизонтальне масштабування кластеру. Завжди можна приєднати додатковий вузол до кластеру та збільшити кількість шард індексу. Внаслідок чого, шарди будуть перерозподілені і на додаткову ноду кластеру
  • Дистрибуція шард в кластері збільшує швидкість обробки операцій читання завдяки розпаралелювання запитів навколо вузлів кластеру. Тобто певна операція може бути виконана на першому вузлі одного шарда, і водночас з тим наступна її частина на іншому шарді того ж індексу другого вузла
Elsticsearch самостійно управляє механікою розподілу шард по кластеру, а також те, як документи агрегуються задля відповідей на запити.

Репліки (Replicas)

Репліки - це копії основних шард. Головне їхнє покликання - забезпечити збереження даних та повноцінну роботу кластеру у разі падіння одного із вузлів. Також репліки можуть слугувати як середовище запуску read-only операцій і таким чином розвантажувати вузли із основними шардами.

Тож що ми маємо в сумі? Кластер складається з вузлів (нод), які зберігають частини індексів (шарди). Шарди можуть також мати репліки, задля підвищення рівня доступності.

Кожний шард (shard) Elasticsearch - це повноцінний Lucene індекс. В одному Lucene індексі можна зберігати до 2 147 483 519 документів. Тобто теоретично, за умови малої кількості шардів та надто великої кількості документів можна втрапити в це обмеження.

Як я вже згадував, управління та взаємодія з Elasticsearch кластером відбуває завдяки REST командам. За допомогою API можна:
  • Перевірити стан кластера, вузлів, шо входять до нього, індексів та переглянути різноманітну статистику
  • Адмініструвати кластер, вузли, дані та метадані індексів
  • Виконувати операції створення, читання, оновлення, видалення (CRUD: Create, Read, Update, Delete) та операції пошуку документів в індексах
  • Запуск складних пошукових операцій, таких як пагінація, сортування, фільтрація, агрегація та ін.
Завершимо з теоретичним мінімумом, перейдемо до створення власного Elasticsearch кластера. Тестовий кластер буде складатись із 3 вузлів (серверів). Задля забезпечення високої доступності, розробники не радять використовувати менше 3 вузлів: із єдиним сервером і так все ясно, а у разі лише двох може статись split brain кластеру. Остання подія дуже не бажана, адже може призвести до втрати цілісності даних.

Отже, у моєму випадку адреси вузлів кластеру будуть такі:

192.168.1.11 - es1
192.168.1.12 - es2
192.168.1.13 - es3

У якості операційної системи я обрав Ubuntu 14.04.3 LTS. Проте тут вже кому як зручніше.

Elasticsearch потребує Java (JVM) для роботи. Отже, проінсталюємо спочатку її на всі вузли, використовуючи репозиторій ppa:webupd8team:

# add-apt-repository ppa:webupd8team/java
# aptitude update
# aptitude install oracle-java8-installer
# update-java-alternatives -s java-8-oracle

Розробник рекомендує саме Oracle JDK реалізацію. Для prod середовищ, мабуть краще не користуватись кастомними репозиторіями і встановити все як рекомендує розробник http://docs.oracle.com/javase/8/docs/technotes/guides/install/install_overview.html

Коректність установки можна перевірити по виводу версії Java:

# java -version
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)

Elastic пропонує готовий deb-пакет та репозиторій для установки свого продукту, то чому б цим не скористатись? Проінсталюємо пакет elasticsearch на всі ноди майбутнього кластеру:

# wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -

# echo "deb http://packages.elastic.co/elasticsearch/2.x/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-2.x.list

# aptitude update && aptitude install elasticsearch

У розділі download також присутні zip та tar.gz архіви програми та rpm-пакет.

Перше, на що необхідно звернути увагу після установки Elasticsearch - обсяг ES_HEAP_SIZE. Це максимальна кількість оперативної пам’яті, яку зможе використовувати демон, параметр буде передаватись ключем -Xmx під час запуску Java-процесу. Офіційна документація рекомендує використовувати 50% від оперативної пам’яті серверу (але не більше 31Гб) для Java-мишини Elasticsearch, що працюватимуть в prod-середовищах. Не варто виділяти більше пам’яті, адже інші 50% будуть використовуватись на кешування операційною системою та Lucene індекси, робота яких лежить поза Java-процесом Elasticsearch.

Необхідне значення ES_HEAP_SIZE можна вказати в /etc/default/elasticsearch.

# vim /etc/default/elasticsearch
...
# Heap size defaults to 256m min, 1g max
# Set ES_HEAP_SIZE to 50% of available RAM, but no more than 31g
ES_HEAP_SIZE=1g
...

У залежності від запланованих навантажень можливо варто підняти ліміти для максимальної кількості відкритих файлів для користувача elasticsearch в /etc/security/limits.conf.

Конфігураційний файл Elasticsearch, що був створений після установки пакету, забекапимо:

# cp /etc/elasticsearch/elasticsearch.yml /etc/elasticsearch/elasticsearch.yml_orig

А новий для першого вузла es1 приведемо до такого вигляду:

# vim /etc/elasticsearch/elasticsearch.yml

cluster.name: GravityFalls
node.name: deeper
bootstrap.mlockall: true
network.host: 0.0.0.0
discovery.zen.ping.multicast.enabled: false
discovery.zen.ping.unicast.hosts: ["192.168.1.11" , "192.168.1.12", "192.168.1.13"]
discovery.zen.minimum_master_nodes: 2
index.number_of_shards: 3
index.number_of_replicas: 2
gateway.recover_after_nodes: 1

cluster.name: GravityFalls - ім’я кластеру.

node.name: deeper - ім’я ноди в кластері. У кожної ноди має бути своє оригінальне ім’я.

bootstrap.mlockall: true - блокування адресного простору, що було виділено на Elasticsearch в RAM після старту процесу. Це запобігає використанню віртуальної пам’ять на сервері, що може сильно впливати на продуктивність вузла. Elastic радить повністю вимикати swap на сервері, проте з деякими зауваженнями.

network.host: 0.0.0.0 - сокет процесу буде відповідати на запити на усіх інтерфейсах. У залежності від потреби можна встановити параметр в значення IP серверу.

discovery.zen.ping.multicast.enabled: false - згідно офіційної документації, по-замовчуванню вузли кластеру можуть знаходити один одного за допомогою multicast повідомлень і єдине, що необхідно мати для об’єднання вузлів в кластер - це однакове ім’я кластеру для кожного із вузлів. Насправді, це не дуже безпечно для production середовищ, тому і вимкнене. Також у мене з VirtualBox знаходження вузлів по мультикасту не працювало.

discovery.zen.ping.unicast.hosts: ["192.168.1.11" , "192.168.1.12", "192.168.1.13"] - перерахунок всіх вузлів, що входять в кластер. Деякі блог-пости радять не включати в цей список адресу ноди, яку налаштовують.

discovery.zen.minimum_master_nodes: 2 - значення має бути встановлене згідно формули N/2 + 1, де N - кількість вузлів в кластері. У нашому випадку всі 3 вузли можуть бути майстрами, та щоб його обрати, у разі падіння попереднього, необхідно хоча б голоси 2 нод. Це власне задля унеможливлення "split-brain" сценарію.

index.number_of_shards: 3 - кожен індекс Elasticsearch, буде поділено на шарди. У нашому випадку по одній на кожен вузол. По замовчуванню цей параметр встановлено в значення 5.

index.number_of_replicas: 2 - кожна із основних шард матиме також 2 копії для підвищення рівня доступності кластеру. Тобто в сумі наш кластер матиме 6 реплік та 3 основних шарди для кожного індексу і зможе працювати навіть при падінні двох вузлів із трьох. Розподілом шард та реплік займається Elasticsearch самостійно і таким чином, що репліки основного шарду не лежать на тому ж вузлі, що і сам основний шард.
На малюнку вище продемонстровано розподіл реплік і шардів одного індексу, за умови number_of_shards: 2 та number_of_replicas: 1. Тобто Elasticsearch самостійно розподілить шарди та їх копії по всім вузлам задля максимальної продуктивності та рівня доступності.

gateway.recover_after_nodes - процес відновлення роботи розпочнеться у разі, коли хоча б N нод кластеру будуть запущені. У нашому випадку це значення рівне 1, адже кожна із нод разом із репліками має повну копію даних кластеру. Відповідно, у разі коли параметр index.number_of_replicas рівний 1 - для повноцінної роботи кластеру необхідно 2 робочих вузла і параметр gateway.recover_after_nodes варто встановити у значення 2.

Для другого та третього вузлів треба скопіювати цей самий конфігураційний файл elasticsearch.yml, лише необхідно замінити параметр node.name. Другий вузол es2 я назвав 'mabel', а третій відповідно - 'soos'.
Після правки конфігураційних файлів можна сміливо (пере)запускати Elasticsearch на всіх вузлах. Перевіримо наразі що там і як з кластером:

root@es1:~# curl -XGET 'http://localhost:9200/_cluster/health?pretty=true'
{
  "cluster_name" : "GravityFalls",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 3,
  "number_of_data_nodes" : 3,
  "active_primary_shards" : 0,
  "active_shards" : 0,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch" : 0,
  "task_max_waiting_in_queue_millis" : 0,
  "active_shards_percent_as_number" : 100.0
}

Отже, кластер працює (green статус) і до нього, як і має бути, входить 3 вузли (number_of_nodes). Усі 3 вузли можуть зберігати дані на дисках (number_of_data_nodes). Всі інші параметри мають нульове значення, адже кластер поки не має жодних корисних даних.

https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html

Кластер може перебувати в 3 станах (статусах):

  • Зелений (green). Кластер повністю робочий: основні шарди та репліки знаходяться на необхідних місцях
  • Жовтий (yellow). Основні шарди в порядку, проте репліки не розподілені в повному обсязі серед вузлів. Кластер повністю робочий, проте якщо одночасно з цим якийсь вузол впаде - можлива зупинка в роботі, адже жодної копії шарду не буде.
  • Червоний (red). Основні шарди не розподілені по кластеру. Кластер не працює повністю або частково.

Якщо Elasticsearch працює лише на одному вузлі (кластер з одного вузла) - статус буде лише yellow.

Перевіримо, хто наразі майстер в кластері:

# curl -XGET "http://127.0.0.1:9200/_cat/nodes?v&h=name,id,ip,port,v,m"
name   id   ip           port v     m 
deeper NNL2 192.168.1.11 9300 2.1.1 m 
mabel  Y_Tc 192.168.1.12 9300 2.1.1 m 
soos   4x6h 192.168.1.13 9300 2.1.1 * 

Зірочка вказує, що майстром наразі є 'soos'.

Кожен вузол кластеру, у нашій конфігурації, може стати майстром. Нового майстер обирають інші робочі вузли, у разі якщо з попереднім щось сталось і він став недоступним. Майстер відповідає лише за задачі по управлінні кластером: створення чи видалення індексів, додаванням чи видаленням вузлів із кластеру і т.п. Тому його падіння ніяк не зашкодить основним задачам кластеру: як змінам на рівні документів, так і пошуку. Тобто відмова майстра не являється слабким місцем роботи всього кластеру - просто в такому разі, при наявності кворуму, буде обраний новий майстер серед доступних серверів.

Створимо новий індекс та запишемо в нього деякі тестові дані:

# curl -XPUT 'http://localhost:9200/shop/basket/1' -d '
{
    "buyer": "ipeacocks",
    "buyDate": "01-14-2016",
    "product": "bread",
    "price": 10
}'

# curl -XPUT 'http://localhost:9200/shop/basket/2' -d '
{
    "buyer": "vpupkin",
    "buyDate": "01-15-2016",
    "product": "banana",
    "price": 9.3
}'

# curl -XPUT 'http://localhost:9200/shop/basket/3' -d '
{
    "buyer": "ipeacocks",
    "buyDate": "01-09-2015",
    "product": "meat",
    "price": 70
}'

Задля створення коректних типів документів, попередньо можна власноруч створити мапінг змінних (чого я в своїх прикладах не роблю), інакше буде використовуватись динамічний маппінг.

Для виконання одразу груп інструкцій також можна скористатись bulk api Elasticsearch:
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/_batch_processing.html

Як користувачі, ми можемо звертатись до будь-якої ноди кластеру, в т.ч. і майстер-ноди. Кожен вузол знає де розміщений кожен документ і може правильно перенаправляти запити до необхідних шард. Саме тому, задля власної зручності, всі запити приведені з вузла es1 (deeper ES node).

У наступному запиті виведемо всі записані документи (_search?q=*, тобто будь-які значення):

# curl -XGET 'http://localhost:9200/shop/_search?q=*&pretty'
{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "2",
      "_score" : 1.0,
      "_source":
{
    "buyer": "vpupkin",
    "buyDate": "01-15-2016",
    "product": "banana",
    "price": 9.3
}
    }, {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "1",
      "_score" : 1.0,
      "_source":
{
    "buyer": "ipeacocks",
    "buyDate": "01-14-2016",
    "product": "bread",
    "price": 10
}
    }
    ...
    ]
  }
}

Отримати лише один документ можна так:

# curl -XGET http://localhost:9200/shop/basket/3?pretty=true
{
  "_index" : "shop",
  "_type" : "basket",
  "_id" : "3",
  "_version" : 1,
  "found" : true,
  "_source":
{
    "buyer": "ipeacocks",
    "buyDate": "01-09-2015",
    "product": "meat",
    "price": 70
}
}

Тепер можна зіставити термінологію, описану на початку, та те як дані записуються насправді: що таке індекс, тип і т.п.

Простий пошук в Elasticsearch виконується наступним чином:

# curl -XGET 'http://localhost:9200/shop/_search?q=buyer:vpupkin&pretty'
{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.30685282,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "2",
      "_score" : 0.30685282,
      "_source":
{
    "buyer": "vpupkin",
    "buyDate": "01-15-2016",
    "product": "banana",
    "price": 9.3
}
    } ]
  }
}

Отже відомо, що, коли та за яку ціну купив покупець vpupkin.

Щоб знайти всіх покупців, окрім vpupkin, необхідно виконати такий запит:

# curl -XGET 'http://localhost:9200/shop/_search?q=-buyer:vpupkin&pretty'
{
  "took" : 20,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "1",
      "_score" : 1.0,
      "_source":
{
    "buyer": "ipeacocks",
    "buyDate": "01-14-2016",
    "product": "bread",
    "price": 10
}
    }, {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "3",
      "_score" : 1.0,
      "_source":
{
    "buyer": "ipeacocks",
    "buyDate": "01-09-2015",
    "product": "meat",
    "price": 70
}
    } ]
  }
}

Створимо більш комплексний запит. Знайдемо всі документи, в котрих покупець ipeacocks та куплений товар - не banana. В результаті виведемо лише поля buyer та product:

# curl -XGET 'http://localhost:9200/shop/_search?q=buyer:ipeacocks%20-product:banana&pretty=true&fields=buyer,product&pretty'
{
  "took" : 55,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.5945348,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "1",
      "_score" : 0.5945348,
      "fields" : {
        "product" : [ "bread" ],
        "buyer" : [ "ipeacocks" ]
      }
    }, {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "3",
      "_score" : 0.5945348,
      "fields" : {
        "product" : [ "meat" ],
        "buyer" : [ "ipeacocks" ]
      }
    } ]
  }
}

Про більше корисних запитів на пошук можна прочитати тут https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html

Є два варіанти запуску запитів на пошук: REST request URI (всі приклади, що були приведені до цього моменту) та REST request body (приклади, що будуть приведені нижче). REST request URI - це пошукові запити, параметри до яких описані одразу в адресі запиту. А відповідно REST request body - запити, параметри яких описані в самому тілі запиту (як аргумент до параметру "-d" програми curl), за допомогою Elasticsearch Query DSL.

Elasticsearch дозволяє фільтрувати запити за певними умовами. Наприклад, виведемо лише покупців, що купили продукти за ціною від 50 гривень включно:

# curl -XGET 'http://localhost:9200/_search?pretty' -d '
{
  "query": {
    "filtered": { 
      "filter": {
        "range": { "price": { "gte": "50" }}
      }
    }
  }
}'

А відповідь буде наступною:

{
  "took" : 60,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "3",
      "_score" : 1.0,
      "_source":
{
    "buyer": "ipeacocks",
    "buyDate": "01-09-2015",
    "product": "meat",
    "price": 70
}
    } ]
  }
}

Пошук з повним збігом полів. Виведено всіх покупців, хто придбав banana:

# curl -XGET 'http://localhost:9200/shop/basket/_search?pretty' -d '{
    "query" : {
        "match" : {
            "product" : "banana"
        }
    }
}'

{
  "took" : 45,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.30685282,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "2",
      "_score" : 0.30685282,
      "_source":
{
    "buyer": "vpupkin",
    "buyDate": "01-15-2016",
    "product": "banana",
    "price": 9.3
}
    } ]
  }
}

Чи збіг одразу по двом полям: product та buyer (логічна операція AND):

# curl -XGET 'http://localhost:9200/shop/basket/_search?pretty' -d '{
    "query" : {
        "bool" : {
            "must" : [
                {
                    "match" : {"product" : "banana"}
                },
                {
                    "match" : {"buyer" : "vpupkin"}
                }

            ]
        }
    }
}'

{
  "took" : 11,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.4339554,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "2",
      "_score" : 0.4339554,
      "_source":
{
    "buyer": "vpupkin",
    "buyDate": "01-15-2016",
    "product": "banana",
    "price": 9.3
}
    } ]
  }
}

За допомогою 'should' фільтра можна реалізувати логічну операцію OR:

# curl -XGET 'http://localhost:9200/shop/basket/_search?pretty' -d '{
    "query" : {
        "bool" : {
            "should" : [
                {
                    "match" : {"product" : "banana"}
                },
                {
                    "match" : {"price" : 70}
                }

            ]
        }
    }
}'

{
  "took" : 15,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.25427115,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "3",
      "_score" : 0.25427115,
      "_source":
{
    "buyer": "ipeacocks",
    "buyDate": "01-09-2015",
    "product": "meat",
    "price": 70
}
    }, {
      "_index" : "shop",
      "_type" : "basket",
      "_id" : "2",
      "_score" : 0.04500804,
      "_source":
{
    "buyer": "vpupkin",
    "buyDate": "01-15-2016",
    "product": "banana",
    "price": 9.3
}
    } ]
  }


Детальніше про бульові операції в Elasticsearch можна прочитати за посиланнями:

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
https://www.elastic.co/guide/en/elasticsearch/guide/current/combining-filters.html

Звісно, є можливість змінювати значення уже створених документів. Проте насправді, це не зовсім оновлення одного поля чи декількох. Щоразу, коли визивається операція оновлення, Elasticsearch видаляє старий документ та індексує новий з необхідним указаним оновленням. Для прикладу приведу операцію зміни ціни продукту покупцем "vpupkin":

# curl -XPOST 'localhost:9200/shop/basket/2/_update?pretty' -d '
{
  "doc": { "price": 35 }
}'

І результат виглядатиме так:

# curl -XGET "http://localhost:9200/shop/basket/2?pretty"
{
  "_index" : "shop",
  "_type" : "basket",
  "_id" : "2",
  "_version" : 2,
  "found" : true,
  "_source":{"buyer":"vpupkin","buyDate":"01-15-2016","product":"banana","price":35}
}

Логіка оновлення вже існуючих записів Elasticsearch дуже схожа на логіку Cassandra, адже це менше фрагментує диск і, як наслідок, в перспективі, менше уповільнює роботу.

Після додавання тестових даних, знову перевіримо статус кластера:

# curl -XGET 'http://localhost:9200/_cluster/health?pretty=true'
{
  "cluster_name" : "GravityFalls",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 3,
  "number_of_data_nodes" : 3,
  "active_primary_shards" : 3,
  "active_shards" : 9,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch" : 0,
  "task_max_waiting_in_queue_millis" : 0,
  "active_shards_percent_as_number" : 100.0
}

Тепер видно наочно, що індекс 'shop' при створенні було поділено на 3 основні шарди, та до кожної шарди є дві репліки. Тобто в сумі всіх 9, на що і вказує параметр "active_shards". Все як і має бути.

Переглянути статус кожного індекса також можливо за допомогою _сat API:

# curl -s -XGET 'http://localhost:9200/_cat/indices?v'
health status index pri rep docs.count docs.deleted store.size pri.store.size 
green  open   shop    3   2          3            0       34kb         11.3kb 

Тут також є статистика по шардам, реплікам і т.п. Знову ж таки, подібно до БД Cassandra, к-ть шард та реплік до них може вказуватись окремо для кожного індексу, а в конфігураційному файлі містяться лише дані по замовчуванню:

# curl -XPUT 'http://localhost:9200/blog/' -d '
index:
    number_of_shards: 2
    number_of_replicas: 1
'

І інформація про індекси наразі виглядатиме таким чином:

# curl -s -XGET 'http://localhost:9200/_cat/indices?v'
health status index pri rep docs.count docs.deleted store.size pri.store.size 
green  open   blog    2   1          0            0       520b           260b 
green  open   shop    3   2          3            0       34kb         11.3kb 

Отже, індекс 'blog' має персональні налаштування щодо шард та реплік.

Дізнатись розподіл шард по серверам також не важко (приведено лише частину виводу):

# curl -XGET http://localhost:9200/_cat/shards
...
blog        2 p STARTED          411  360.1kb 192.168.1.13 es3 
blog        2 r STARTED          411  325.8kb 192.168.1.12 es2 
blog        3 p STARTED          387  378.5kb 192.168.1.13 es3 
blog        3 r STARTED          387  342.5kb 192.168.1.12 es1 
... 

p, r означають primary і replica шарди відповідно.

Ба більше, REST API Elasticsearch дозволяє переглянути сервісну інформацію на кшталт максимально дозволених та відкритих файлів в конкретний момент часу:

# curl -s -XGET 'localhost:9200/_cat/nodes?v&h=ip,fdc,fdm'
ip             fdc   fdm
192.168.1.11  1806 65535
192.168.1.12   837 65535
192.168.1.13  1501 65535

fdc - поточна кількість відкритих файлів
fdm - максимально дозволена кількість відкритих файлів

Іноді виникає необхідність проведення технічного обслуговування на одному із вузлів кластеру. Справа в тому, що при звичайному вимкненні однієї з нод одразу почнеться ребалансування шард та їх реплік в кластері на вузли, що лишились робочими. Звісно, це займає значну кількість часу - тому для відносно коротких даунтаймів варто виконати наступний запит до кластеру:

# curl -XPUT http://localhost:9200/_cluster/settings -d '{
    "transient" : {
        "cluster.routing.allocation.enable" : "none"
    }
}'

Котрий зупинить ребелансування. А наступний запит варто виконати по закінченні технічного обслуговування:

# curl -XPUT http://localhost:9200/_cluster/settings -d '{
    "transient" : {
        "cluster.routing.allocation.enable" : "all"
    }
}'

У такому разі кластер набуде green-статусу значно швидше.

Для певного рівня адміністрування кластеру можна скористатись графічним веб-інтерфейсом elasticsearch-head, котрий попередньо необхідно встановити на всіх Elasticsearch нодах:

# /usr/share/elasticsearch/bin/elasticsearch/bin/plugin -install mobz/elasticsearch-head

Можна провести його установку лише на одному вузлі кластера - але тоді, очевидно, веб-інтерфейс може стати недоступним у разі падіння саме цього вузла. Вигляд він має наступний:


Із тією ж задачею може також впоратись kopf:

# /usr/share/elasticsearch/bin/elasticsearch/bin/plugin install lmenezes/elasticsearch-kopf/2.0


Kopf виглядає значно сучасніше. Та він ще може змінювати колір верхньої панелі адмінки, в залежності від статусу кластеру:


Більше статистичної інформації щодо кластеру можна дізнатись, виконавши команду:

# curl -XGET 'http://localhost:9200/_nodes/stats?pretty=true'

Видалення документу проходить також зовсім не складно:

# curl -XDELETE 'http://localhost:9200/shop/basket/1'

Буде видалено документ з індексом 1. Видалення цілого індексу проходить подібним чином:

# curl -XDELETE 'http://localhost:9200/shop/'

Більше того, без опції 'action.destructive_requires_name: true', можна знести всі індекси по певній масці чи використовуючи мета-індекс '_all':

# curl -XDELETE 'http://localhost:9200/_all'
# curl -XDELETE 'http://localhost:9200/*'

Для текстового дампу індексів, можна скористатись утілітою elasticdump, що написано на мові nodejs. Тут повно прикладів її використання https://www.npmjs.com/package/elasticdump
Проте для переміщення великих обсягів даних краще скористатись бінарним бекапом, котрий звісно гарно описаний в офіційній документації:

https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html
https://www.elastic.co/guide/en/elasticsearch/guide/current/backing-up-your-cluster.html

Можливо це буде трохи відірваним від контексту, проте хотілось би також згадати про те, як Elasticsearch обробляє запити на рівні кластеру.

Запити створення, індексу та видалення (операції запису) - це операції, що мають спочатку бути виконані на основних шардах і лише потім скопійовані до всіх відповідних шард-реплік.


На малюнку вище представлений кластер, індекс який поділяється на 2 основні шарди та 4 репліки: по дві на кожен основний шард.

Для успішної write-операції має бути виконана така послідовність:
  1. Клієнт надсилає запит на створення, індекс чи видалення документу до Node 1. Цей вузол не обов'язково має бути майстер - запит може бути надісланий на будь-який вузол кластеру.
  2. Вузол, використовує _id документу (в запитах вище, добре видно це поле), для перенаправлення запиту в необхідний основний шард. В нашому випадку це шард P0, що лежить на Node 3.
  3. Node 3 виконує необхідну операцію на основному шарді P0. У разі успіху, Node 3 пересилає запит на шарди репліки R0 на Node2 та R0 на Node 1. І лише у випадку, якщо виконання запитів буде успішним на обох репліках, Node 3 відзвітує ноді-координатору, а та вже клієнту, що запит виконаний успішно. Нода-координатор - це вузол, з якого ініціюється запит.
Elasticsearch має опції, що можуть впливати на описаний вище звичний процес:

* consistency. По-замовчуванню, для виконання операцій, що призводять до зміни даних, основному шарду необхідний кворум шард-копій (реплік та основного шарду). Кворум розраховується за наступною схемою:

int( (primary + number_of_replicas) / 2 ) + 1

У кластері, що зображений на малюнку, consistency по замовчуванню буде рівним 3:

int( (primary + 3 replicas) / 2 ) + 1 = 3

Тобто, щоб запит на зміну даних був виконаний, необхідно щоб були доступні як мінімум 3 копії однієї шарди.

Параметр 'consistency' може приймати значення one (має бути доступна лише вузол, що володіє основним шардом), all (мають бути доступні всі шарди та репліки) та quorum (значення по-замовчуванню). Отже при значенні one, швидкість запису до кластеру Elasticsearch буде вищою, проте рівень безпеки збереження даних впаде.

* timeout. У випадку, якщо необхідна кількість шард для виконання операцій запису не буде доступна, Elasticsearch зачекає певний час в надії, що вони з'являться. По замовчуванню, цей час очікування має значення 1 хв. За необхідності цей час може бути змінений.

https://www.elastic.co/guide/en/elasticsearch/guide/current/distrib-write.html

Читання документу відбувається за іншою схемою:

  1. Клієнт звертається до одного із вузлів кластера. У нашому випадку це Node 1.
  2. Використовуючи _id документа, нода-координатор визначає, що основний шард для цього документа - P0. У нашому прикладі, зображеному на малюнку, копії цього шарду присутні на всіх вузлах кластеру. Тому вузол-координатор перенаправить запит на Node 2 та виконає операцію читання на шерді-репліці. Кожен наступний запит на читання документів, що належать цьому ж основному шарду можуть бути виконані уже на інших репліках чи основному шардя задля забезпечення балансування та підвищення швидкості читання.
  3. Node 2 повертає запит вузлу координатору, а той, в свою чергу, повертає його клієнту, що ініціював запит.
Операції на зміну вже існуючих даних комбінують у собі як операції читання, так і запису.

  1. Клієнт надсилає запит на оновлення до одного із вузлів кластеру. У нашому випадку це Node 1.
  2. Перший вузол стає координатором запиту та пересилає запит на вузол із основним шардом, де містяться необхідні для оновлення записи. Основний шард знаходиться на Node 3.
  3. Третій вузол перечитує документ з основного шарду, змінює поле '_source' JSON об’єкта та намагається зробити реіндекс документа в цьому ж основному шарді. Якщо він вже був змінений іншим процесом, то він повторить спробу реіндексу 'retry_on_conflict' разів. Тобто по-суті, це видалення старого документу і запис нового, такого ж, проте з певними необхідними змінами.
  4. У разі успіху 3 етапу, Node 3 паралельно надішле нову версію документу нодам Node1,2 із репліками задля реіндексації. У разі якщо реіндекс документу буде успішним всюди, третій вузол повідомить ноду-координатор, а та в свою чергу клієнта, що запит був виконаний успішно.
https://www.elastic.co/guide/en/elasticsearch/guide/current/_partial_updates_to_a_document.html

Логіка виконання mget та bulk APIs операцій відбувається за дещо іншим сценарієм https://www.elastic.co/guide/en/elasticsearch/guide/current/distrib-multi-doc.html

У разі конфліктів, Elasticsearch вирішує їх наступним чином https://www.elastic.co/guide/en/elasticsearch/guide/current/version-control.html

Як я вже згадував, основний конкурент Elasticsearch - Apache Solr. Різниця між ними описана в цій статті http://blog.sematext.com/2015/01/30/solr-elasticsearch-comparison/ і, коротше кажучи, вони однаково хороші і активно розвиваються.

Посилання:
https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
https://www.elastic.co/guide/en/elasticsearch/guide/current/index.html
http://www.wilfred.me.uk/blog/2015/01/31/taming-a-wild-elasticsearch-cluster/
https://wiki.deimos.fr/ElasticSearch:_powerful_search_and_analytics_engine
http://tecadmin.net/install-elasticsearch-multi-node-cluster-on-linux/
https://deviantony.wordpress.com/2014/09/23/how-to-setup-an-elasticsearch-cluster-with-logstash-on-ubuntu-12-04/
http://blog.trifork.com/2013/10/24/how-to-avoid-the-split-brain-problem-in-elasticsearch/
http://radar.oreilly.com/2015/04/10-elasticsearch-metrics-to-watch.html
http://www.slideshare.net/arafalov/solr-vs-elasticsearch-case-by-case
http://www.elasticsearchtutorial.com/elasticsearch-in-5-minutes.html
http://joelabrahamsson.com/elasticsearch-101/
https://xakep.ru/2015/06/11/elasticsearch-tutorial/
http://stackoverflow.com/questions/22544461/elasticsearch-optimal-number-of-shards-per-node/27865188#27865188
http://cpratt.co/how-many-shards-should-elasticsearch-indexes-have/
https://www.elastic.co/guide/en/elasticsearch/guide/current/_rolling_restarts.html
http://www.cubrid.org/blog/dev-platform/our-experience-creating-large-scale-log-search-system-using-elasticsearch/
https://www.elastic.co/blog/index-vs-type

Немає коментарів:

Дописати коментар