Доўгатэрміновае захоўванне дадзеных у Elasticsearch

Доўгатэрміновае захоўванне дадзеных у Elasticsearch

Мяне клічуць Ігар Сідарэнка, я тэхлід у камандзе адмінаў, якія падтрымліваюць у працоўным стане ўсю інфраструктуру Домклік.

Жадаю падзяліцца сваім досведам у наладзе размеркаванага захоўвання дадзеных у Elasticsearch. Мы разгледзім, якія налады на нодах адказваюць за размеркаванне шардаў, як уладкованы і працуе ILM.

Тыя, хто працуюць з логамі, так ці інакш сутыкаюцца з праблемай доўгатэрміновага захоўвання для наступнага аналізу. У Elasticsearch гэта асабліва актуальна, таму што з функцыянальнасцю куратара ўсё было сумна. У версіі 6.6/4 з'явіўся функцыянал ILM. Ён складаецца з XNUMX фаз:

  • Hot – індэкс актыўна абнаўляецца і запытваецца.
  • Warm – індэкс больш не абнаўляецца, але ўсё яшчэ запытваецца.
  • Cold – індэкс больш не абнаўляецца і рэдка запытваецца. Інфармацыя ўсё яшчэ павінна быць даступна для пошуку, але запыты могуць выконвацца больш павольна.
  • Delete - індэкс больш не патрэбен і можа быць бяспечна выдалены.

Дана

  • Elasticsearch Data Hot: 24 працэсара, 128 Гб памяці, 1,8 Тб SSD RAID 10 (8 нод).
  • Elasticsearch Data Warm: 24 працэсара, 64 Гб памяці, 8 Тб NetApp SSD Policy (4 ноды).
  • Elasticsearch Data Cold: 8 працэсараў, 32 Гб памяці, 128 Тб HDD RAID 10 (4 ноды).

Мэта

Гэтыя налады індывідуальныя, усё залежыць ад месца на нодах, колькасці індэксаў, логаў і г.д. У нас гэта 2-3 Тб дадзеных за суткі.

  • 5 дзён - фаза Hot (8 асноўных / 1 рэпліка).
  • 20 дзён - фаза Warm (shrink-індэкс 4 асноўных / 1 рэпліка).
  • 90 дзён - фаза Cold (freeze-індэкс 4 асноўных / 1 рэпліка).
  • 120 дзён - фаза Delete.

Настройка Elasticsearch

Для размеркавання шард па нодах патрэбен усяго адзін параметр:

  • гарачы-ноды:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: hot
  • Цёплы-ноды:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: warm
  • Халодны-ноды:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: cold

Настройка Logstash

Як гэта ўсё працуе і як мы рэалізавалі гэтую функцыю? Давайце пачнем з траплення логаў у Elasticsearch. Ёсць два спосабы:

  1. Logstash забірае логі з Kafka. Можа забраць чыстымі або пераўтварыць на сваім баку.
  2. Нешта само піша ў Elasticsearch, напрыклад, APM-сервер.

Разгледзім прыклад кіравання азначнікамі праз Logstash. Ён стварае індэкс і прымяняе да яго шаблон індэкса і адпаведны ILM.

k8s-ingress.conf

input {
    kafka {
        bootstrap_servers => "node01, node02, node03"
        topics => ["ingress-k8s"]
        decorate_events => false
        codec => "json"
    }
}

filter {
    ruby {
        path => "/etc/logstash/conf.d/k8s-normalize.rb"
    }
    if [log] =~ "[warn]" or [log] =~ "[error]" or [log] =~ "[notice]" or [log] =~ "[alert]" {
        grok {
            match => { "log" => "%{DATA:[nginx][error][time]} [%{DATA:[nginx][error][level]}] %{NUMBER:[nginx][error][pid]}#%{NUMBER:[nginx][error][tid]}: *%{NUMBER:[nginx][error][connection_id]} %{DATA:[nginx][error][message]}, client: %{IPORHOST:[nginx][error][remote_ip]}, server: %{DATA:[nginx][error][server]}, request: "%{WORD:[nginx][error][method]} %{DATA:[nginx][error][url]} HTTP/%{NUMBER:[nginx][error][http_version]}", (?:upstream: "%{DATA:[nginx][error][upstream][proto]}://%{DATA:[nginx][error][upstream][host]}:%{DATA:[nginx][error][upstream][port]}/%{DATA:[nginx][error][upstream][url]}", )?host: "%{DATA:[nginx][error][host]}"(?:, referrer: "%{DATA:[nginx][error][referrer]}")?" }
            remove_field => "log"
        }
    }
    else {
        grok {
            match => { "log" => "%{IPORHOST:[nginx][access][host]} - [%{IPORHOST:[nginx][access][remote_ip]}] - %{DATA:[nginx][access][remote_user]} [%{HTTPDATE:[nginx][access][time]}] "%{WORD:[nginx][access][method]} %{DATA:[nginx][access][url]} HTTP/%{NUMBER:[nginx][access][http_version]}" %{NUMBER:[nginx][access][response_code]} %{NUMBER:[nginx][access][bytes_sent]} "%{DATA:[nginx][access][referrer]}" "%{DATA:[nginx][access][agent]}" %{NUMBER:[nginx][access][request_lenght]} %{NUMBER:[nginx][access][request_time]} [%{DATA:[nginx][access][upstream][name]}] (?:-|%{IPORHOST:[nginx][access][upstream][addr]}:%{NUMBER:[nginx][access][upstream][port]}) (?:-|%{NUMBER:[nginx][access][upstream][response_lenght]}) %{DATA:[nginx][access][upstream][response_time]} %{DATA:[nginx][access][upstream][status]} %{DATA:[nginx][access][request_id]}" }
            remove_field => "log"
        }
    }
}
output {
    elasticsearch {
        id => "k8s-ingress"
        hosts => ["node01", "node02", "node03", "node04", "node05", "node06", "node07", "node08"]
        manage_template => true # включаем управление шаблонами
        template_name => "k8s-ingress" # имя применяемого шаблона
        ilm_enabled => true # включаем управление ILM
        ilm_rollover_alias => "k8s-ingress" # alias для записи в индексы, должен быть уникальным
        ilm_pattern => "{now/d}-000001" # шаблон для создания индексов, может быть как "{now/d}-000001" так и "000001"
        ilm_policy => "k8s-ingress" # политика прикрепляемая к индексу
        index => "k8s-ingress-%{+YYYY.MM.dd}" # название создаваемого индекса, может содержать %{+YYYY.MM.dd}, зависит от ilm_pattern
    }
}

Настройка Kibana

Ёсць базавы шаблон, які ўжываецца да ўсіх новых азначнікаў. Ён задае размеркаванне гарачых індэксаў, колькасць шардаў, рэплік і т.д. Вага пра шаблон вызначаецца опцыяй order. Шаблоны з больш высокай вагай перавызначаюць ужо існуючыя параметры шаблону ці дадаюць новыя.

Доўгатэрміновае захоўванне дадзеных у Elasticsearch
Доўгатэрміновае захоўванне дадзеных у Elasticsearch

GET _template/default

{
  "default" : {
    "order" : -1, # вес шаблона
    "version" : 1,
    "index_patterns" : [
      "*" # применяем ко всем индексам
    ],
    "settings" : {
      "index" : {
        "codec" : "best_compression", # уровень сжатия
        "routing" : {
          "allocation" : {
            "require" : {
              "box_type" : "hot" # распределяем только по горячим нодам
            },
            "total_shards_per_node" : "8" # максимальное количество шардов на ноду от одного индекса
          }
        },
        "refresh_interval" : "5s", # интервал обновления индекса
        "number_of_shards" : "8", # количество шардов
        "auto_expand_replicas" : "0-1", # количество реплик на ноду от одного индекса
        "number_of_replicas" : "1" # количество реплик
      }
    },
    "mappings" : {
      "_meta" : { },
      "_source" : { },
      "properties" : { }
    },
    "aliases" : { }
  }
}

Затым ужыем мапінг да азначнікаў k8s-ingress-* з дапамогай шаблону з больш высокай вагай.

Доўгатэрміновае захоўванне дадзеных у Elasticsearch
Доўгатэрміновае захоўванне дадзеных у Elasticsearch

GET _template/k8s-ingress

{
  "k8s-ingress" : {
    "order" : 100,
    "index_patterns" : [
      "k8s-ingress-*"
    ],
    "settings" : {
      "index" : {
        "lifecycle" : {
          "name" : "k8s-ingress",
          "rollover_alias" : "k8s-ingress"
        },
        "codec" : "best_compression",
        "routing" : {
          "allocation" : {
            "require" : {
              "box_type" : "hot"
            }
          }
        },
        "number_of_shards" : "8",
        "number_of_replicas" : "1"
      }
    },
    "mappings" : {
      "numeric_detection" : false,
      "_meta" : { },
      "_source" : { },
      "dynamic_templates" : [
        {
          "all_fields" : {
            "mapping" : {
              "index" : false,
              "type" : "text"
            },
            "match" : "*"
          }
        }
      ],
      "date_detection" : false,
      "properties" : {
        "kubernetes" : {
          "type" : "object",
          "properties" : {
            "container_name" : {
              "type" : "keyword"
            },
            "container_hash" : {
              "index" : false,
              "type" : "keyword"
            },
            "host" : {
              "type" : "keyword"
            },
            "annotations" : {
              "type" : "object",
              "properties" : {
                "value" : {
                  "index" : false,
                  "type" : "text"
                },
                "key" : {
                  "index" : false,
                  "type" : "keyword"
                }
              }
            },
            "docker_id" : {
              "index" : false,
              "type" : "keyword"
            },
            "pod_id" : {
              "type" : "keyword"
            },
            "labels" : {
              "type" : "object",
              "properties" : {
                "value" : {
                  "type" : "keyword"
                },
                "key" : {
                  "type" : "keyword"
                }
              }
            },
            "namespace_name" : {
              "type" : "keyword"
            },
            "pod_name" : {
              "type" : "keyword"
            }
          }
        },
        "@timestamp" : {
          "type" : "date"
        },
        "nginx" : {
          "type" : "object",
          "properties" : {
            "access" : {
              "type" : "object",
              "properties" : {
                "agent" : {
                  "type" : "text"
                },
                "response_code" : {
                  "type" : "integer"
                },
                "upstream" : {
                  "type" : "object",
                  "properties" : {
                    "port" : {
                      "type" : "keyword"
                    },
                    "name" : {
                      "type" : "keyword"
                    },
                    "response_lenght" : {
                      "type" : "integer"
                    },
                    "response_time" : {
                      "index" : false,
                      "type" : "text"
                    },
                    "addr" : {
                      "type" : "keyword"
                    },
                    "status" : {
                      "index" : false,
                      "type" : "text"
                    }
                  }
                },
                "method" : {
                  "type" : "keyword"
                },
                "http_version" : {
                  "type" : "keyword"
                },
                "bytes_sent" : {
                  "type" : "integer"
                },
                "request_lenght" : {
                  "type" : "integer"
                },
                "url" : {
                  "type" : "text",
                  "fields" : {
                    "keyword" : {
                      "type" : "keyword"
                    }
                  }
                },
                "remote_user" : {
                  "type" : "text"
                },
                "referrer" : {
                  "type" : "text"
                },
                "remote_ip" : {
                  "type" : "ip"
                },
                "request_time" : {
                  "format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis||dd/MMM/YYYY:H:m:s Z",
                  "type" : "date"
                },
                "host" : {
                  "type" : "keyword"
                },
                "time" : {
                  "format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis||dd/MMM/YYYY:H:m:s Z",
                  "type" : "date"
                }
              }
            },
            "error" : {
              "type" : "object",
              "properties" : {
                "server" : {
                  "type" : "keyword"
                },
                "upstream" : {
                  "type" : "object",
                  "properties" : {
                    "port" : {
                      "type" : "keyword"
                    },
                    "proto" : {
                      "type" : "keyword"
                    },
                    "host" : {
                      "type" : "keyword"
                    },
                    "url" : {
                      "type" : "text",
                      "fields" : {
                        "keyword" : {
                          "type" : "keyword"
                        }
                      }
                    }
                  }
                },
                "method" : {
                  "type" : "keyword"
                },
                "level" : {
                  "type" : "keyword"
                },
                "http_version" : {
                  "type" : "keyword"
                },
                "pid" : {
                  "index" : false,
                  "type" : "integer"
                },
                "message" : {
                  "type" : "text"
                },
                "tid" : {
                  "index" : false,
                  "type" : "keyword"
                },
                "url" : {
                  "type" : "text",
                  "fields" : {
                    "keyword" : {
                      "type" : "keyword"
                    }
                  }
                },
                "referrer" : {
                  "type" : "text"
                },
                "remote_ip" : {
                  "type" : "ip"
                },
                "connection_id" : {
                  "index" : false,
                  "type" : "keyword"
                },
                "host" : {
                  "type" : "keyword"
                },
                "time" : {
                  "format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis||dd/MMM/YYYY:H:m:s Z",
                  "type" : "date"
                }
              }
            }
          }
        },
        "log" : {
          "type" : "text"
        },
        "@version" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "ignore_above" : 256,
              "type" : "keyword"
            }
          }
        },
        "eventtime" : {
          "type" : "float"
        }
      }
    },
    "aliases" : { }
  }
}

Пасля ўжывання ўсіх шаблонаў мы ўжываем ILM-палітыку і пачынаем сачыць за жыццём азначнікаў.

Доўгатэрміновае захоўванне дадзеных у Elasticsearch

Доўгатэрміновае захоўванне дадзеных у Elasticsearch

Доўгатэрміновае захоўванне дадзеных у Elasticsearch

GET _ilm/policy/k8s-ingress

{
  "k8s-ingress" : {
    "version" : 14,
    "modified_date" : "2020-06-11T10:27:01.448Z",
    "policy" : {
      "phases" : {
        "warm" : { # теплая фаза
          "min_age" : "5d", # срок жизни индекса после ротации до наступления теплой фазы
          "actions" : {
            "allocate" : {
              "include" : { },
              "exclude" : { },
              "require" : {
                "box_type" : "warm" # куда перемещаем индекс
              }
            },
            "shrink" : {
              "number_of_shards" : 4 # обрезание индексов, т.к. у нас 4 ноды
            }
          }
        },
        "cold" : { # холодная фаза
          "min_age" : "25d", # срок жизни индекса после ротации до наступления холодной фазы
          "actions" : {
            "allocate" : {
              "include" : { },
              "exclude" : { },
              "require" : {
                "box_type" : "cold" # куда перемещаем индекс
              }
            },
            "freeze" : { } # замораживаем для оптимизации
          }
        },
        "hot" : { # горячая фаза
          "min_age" : "0ms",
          "actions" : {
            "rollover" : {
              "max_size" : "50gb", # максимальный размер индекса до ротации (будет х2, т.к. есть 1 реплика)
              "max_age" : "1d" # максимальный срок жизни индекса до ротации
            },
            "set_priority" : {
              "priority" : 100
            }
          }
        },
        "delete" : { # фаза удаления
          "min_age" : "120d", # максимальный срок жизни после ротации перед удалением
          "actions" : {
            "delete" : { }
          }
        }
      }
    }
  }
}

Праблемы

Былі праблемы на этапе наладкі і адладкі.

Hot-фаза

Для карэктнай ратацыі індэксаў крытычна прысутнасць у канцы index_name-date-000026 лікаў фармату 000001. У кодзе ёсць радкі, якія правяраюць азначнікі з дапамогай рэгулярнага выраза на наяўнасць лікаў у канцы. Інакш будзе памылка, да індэкса не прымяняцца палітыкі і ён заўсёды будзе ў hot-фазе.

Warm-фаза

Сціскацца (абразанне) - памяншэнне колькасці шардаў, таму што нод у цёплай і халоднай фазах у нас па 4. У дакументацыі ёсць такія радкі:

  • The index must be read-only.
  • Зьвесткі зь кожнай шпалы ў index маю reside на самім нумары.
  • У cluster health status павінны быць уgreen.

Каб зрэзаць азначнік, Elasticsearch перасоўвае ўсе асноўныя (primary) шарды на адну ноду, дублюе зрэзаны азначнік з неабходнымі параметрамі, а потым выдаляе стары. Параметр total_shards_per_node павінен быць роўны ці больш колькасці асноўных шардаў, каб змясціць іх на адной нодзе. У адваротным выпадку будуць апавяшчэння і шарды не пераедуць на патрэбныя ноды.

Доўгатэрміновае захоўванне дадзеных у Elasticsearch
Доўгатэрміновае захоўванне дадзеных у Elasticsearch

GET /shrink-k8s-ingress-2020.06.06-000025/_settings

{
  "shrink-k8s-ingress-2020.06.06-000025" : {
    "settings" : {
      "index" : {
        "refresh_interval" : "5s",
        "auto_expand_replicas" : "0-1",
        "blocks" : {
          "write" : "true"
        },
        "provided_name" : "shrink-k8s-ingress-2020.06.06-000025",
        "creation_date" : "1592225525569",
        "priority" : "100",
        "number_of_replicas" : "1",
        "uuid" : "psF4MiFGQRmi8EstYUQS4w",
        "version" : {
          "created" : "7060299",
          "upgraded" : "7060299"
        },
        "lifecycle" : {
          "name" : "k8s-ingress",
          "rollover_alias" : "k8s-ingress",
          "indexing_complete" : "true"
        },
        "codec" : "best_compression",
        "routing" : {
          "allocation" : {
            "initial_recovery" : {
              "_id" : "_Le0Ww96RZ-o76bEPAWWag"
            },
            "require" : {
              "_id" : null,
              "box_type" : "cold"
            },
            "total_shards_per_node" : "8"
          }
        },
        "number_of_shards" : "4",
        "routing_partition_size" : "1",
        "resize" : {
          "source" : {
            "name" : "k8s-ingress-2020.06.06-000025",
            "uuid" : "gNhYixO6Skqi54lBjg5bpQ"
          }
        }
      }
    }
  }
}

Cold-фаза

Замарожваць (замарозка) - мы замарожваем індэкс для аптымізацыі запытаў па гістарычных дадзеных.

Searches performed on frozen indexs use the small, dedicated, search_throttled threadpool to control number of concurrent searches that hit frozen shards on each node. Гэтыя ліміты сумевыя дадатковыя памяшканні запатрабаваныя для існуючых дадзеных структур адпавядаюць пярэчаным зножванням, якія, зрэшты, абараняюць nodes proti excessive memory consumption.
Выкапаныя лічбы read-only: Вы не будзеце index in the them.
Searches on frozen indices expected to execute slowly. Frozen indices nejsou intended for high search load. Гэта магчыма, што даследаванне рэдагавання indexа можа пазбыцца секунд або хвілін да ўсяго, толькі калі тыя searches completed in milliseconds when indices no frozen.

Вынікі

Мы навучыліся падрыхтоўваць ноды для працы з ILM, наладзілі шаблон для размеркавання шардаў па гарачых нодах і наладзілі ILM на азначнік са ўсімі фазамі жыцця.

Карысныя спасылкі

Крыніца: habr.com