EFK подробнее

Thank you for reading this post, don't forget to subscribe!

Клиентская часть (Агент)

Рас­смот­рим при­мер достав­ки лога от Docker контейнера.

Есть два вари­ан­та (на самом деле их боль­ше тыц) как писать лог Docker кон­тей­не­ров. В сво­ем дефолт­ном состо­я­нии, исполь­зу­ет­ся Logging Driver — “JSON file”, т.е. пишет клас­си­че­ским мето­дом в файл. Вто­рой спо­соб — это пере­опре­де­лить Logging Driver и слать лог сра­зу в Fluentd.

Реши­ли мы попро­бо­вать вто­рой вариант.

Далее столк­ну­лись с такой про­бле­мой, что если про­сто сме­нить Logging Driver и, если вдруг, TCP сокет Fluentd, куда долж­ны сыпать­ся логи, не досту­пен, то кон­тей­нер вооб­ще не запу­стит­ся. А если TCP сокет Fluentd про­па­дет, когда кон­тей­нер ранит­ся, то логи начи­на­ют копить­ся в буфер.

Про­ис­хо­дит это пото­му, что у Docker есть два режи­ма достав­ки сооб­ще­ний от кон­тей­не­ра к Logging Driver:

direct — явля­ет­ся дефолт­ным. При­ло­же­ние пада­ет, когда не может запи­сать лог в stdout и/или stderr.

non-blocking — поз­во­ля­ет запи­сы­вать логи при­ло­же­ния в буфер при недо­ступ­но­сти соке­та кол­лек­то­ра. При этом, если буфер полон, новые логи начи­на­ют вытес­нять старые.

Все эти вещи под­тя­ну­ли допол­ни­тель­ны­ми опци­я­ми Logging Driver
(--log-opt).

Но, так как нас не устро­ил ни пер­вый, ни вто­рой вари­ант, то мы реши­ли не пере­опре­де­лять Logging Driver и оста­вить его дефолтным.

Вот тут и настал момент, когда мы заду­ма­лись о том, что необ­хо­ди­мо уста­нав­ли­вать аген­та, кото­рый бы ста­рым, доб­рым спо­со­бом вычи­ты­вал с помо­щью tail лог файл. Кста­ти, я думаю не все заду­мы­ва­лись над тем, что про­ис­хо­дит с tail, когда ротей­тит­ся лог-файл, а ока­за­лось все доста­точ­но про­сто, так как tail снаб­жен клю­чем -F (same as --follow=”name” --retry), кото­рый спо­со­бен сле­дить за фай­лом и переот­кры­вать его в слу­чае необходимости.

Итак, пер­вое на что было обра­ще­но вни­ма­ние — это про­дукт FluentBit.

Настрой­ка FluentBit доста­точ­на про­ста, но мы столк­ну­лись с несколь­ки­ми непри­ят­ны­ми моментами.

Про­бле­ма заклю­ча­лась в том, что если TCP сокет кол­лек­то­ра не досту­пен, то FluentBit начи­на­ет актив­но писать в буфер, кото­рый нахо­дит­ся в памя­ти и по мере его запол­не­ния, начи­на­ет вытес­нять (пере­за­пи­сы­вать) лог, до тех пор, пока не вос­ста­но­вит соеди­не­ние с кол­лек­то­ром, а сле­до­ва­тель­но, мы теря­ем логи. Это не кри­ти­кал, но крайне неприятно.

Реши­ли эту про­бле­му сле­ду­ю­щим образом.

FluentBit в tail пла­гине име­ет сле­ду­ю­щие параметры:

DB — опре­де­ля­ет файл базы дан­ных, в кото­ром будет вестись учет фай­лов, кото­рые мони­то­рят­ся, а так­же пози­цию в этих фай­лах (offsets).

DB.Sync — уста­нав­ли­ва­ет дефолт­ный метод син­хро­ни­за­ции (I/O). Дан­ная опция отве­ча­ет за то, как внут­рен­ний дви­жок SQLite будет син­хро­ни­зи­ро­вать­ся на диск.

И ста­ло все отлич­но, логи мы не теря­ли, но столк­ну­лись со сле­ду­ю­щей про­бле­мой — эта шту­ка очень жут­ко нача­ла потреб­лять IOPs. Обра­ти­ли на это вни­ма­ние когда заме­ти­ли, что инстан­сы нача­ли тупить и упи­рать­ся в IO, кото­ро­го рань­ше хва­та­ло за гла­за и сей­час объ­яс­ню почему.

Мы исполь­зу­ем EC2 инстан­сы (AWS) и тип EBS — gp2 (General Purpose SSD). У них есть такая шту­ка, кото­рая назы­ва­ет­ся “Burst performance”. Это когда EBS спо­со­бен выдер­жи­вать неко­то­рые пики воз­рас­та­ю­щей нагруз­ки на фай­ло­вую систе­му, напри­мер запуск какой-нибудь крон зада­чи, кото­рая тре­бу­ет интен­сив­но­го IO и если ему недо­ста­точ­но “baseline performance”, инстанс начи­на­ет потреб­лять накоп­лен­ные кре­ди­ты на IO т.е. “Burst performance”.

Так вот, наша про­бле­ма заклю­ча­лась в том, что были вычер­па­ны кре­ди­ты IO и EBS начал захле­бы­вать­ся от его недостатка.

Ок. Прой­ден­ный этап.

После это­го мы реши­ли, а поче­му бы не исполь­зо­вать тот же Fluentd в каче­стве агента.

Пер­вый запуск Fluentd, при нали­чии боль­шо­го чис­ла фай­лов логов, ока­зал­ся доста­точ­но вол­ни­тель­ным, так как в логе само­го демо­на гово­ри­лось о том, что он видит толь­ко часть задан­ных ему фай­лов (и тут мы немно­го напряг­лись), при том, что когда его рестар­ту­ешь, он под­хва­ты­ва­ет раз­ные фай­лы логов. Ока­за­лось, это ожи­да­е­мое пове­де­ние, так как пла­гин tail явля­ет­ся одно­по­точ­ным и одно­вре­мен­но, он может оси­лить толь­ко 4 фай­ла (види­мо сле­дуя здра­во­му смыс­лу), выде­ляя какую-то часть вре­ме­ни на каж­дый файл и пере­клю­чая кон­тек­сты по исте­че­нию это­го вре­ме­ни. Но после вычит­ки всех фай­лов — его пове­де­ние нормализуется.

Эта ситу­а­ция спра­вед­ли­ва тогда, когда есть доста­точ­но объ­ем­ные фай­лы логов (как было в нашем слу­чае) и с вклю­чен­ной опци­ей read_from_head (про­из­во­дить вычит­ку фай­ла с само­го нача­ла). Если эту опцию не добав­лять, то вычит­ка будет про­из­во­дить­ся по стан­дарт­но­му пове­де­нию tail, т.е. с кон­ца файла.

Марш­ру­ти­за­ция логов Fluentd про­из­во­дит­ся на осно­ва­нии тэгов, т.е. каж­дый кон­тей­нер име­ет свой тэг и если он сов­па­да­ет с мет­кой на филь­тре — про­из­во­дит его после­до­ва­тель­ную обра­бот­ку (свер­ху вниз). По-умол­ча­нию тэг при­сва­и­ва­ет­ся вот такой:

В соот­вет­ствии с тегом осу­ществ­ля­ет­ся филь­тра­ция (напри­мер, если необ­хо­ди­мо доба­вить какое-то поле в JSON лог или напра­вить сра­зу на output).

Весь про­цесс выгля­дит сле­ду­ю­щим образом:

Fluentd вычи­ты­ва­ет лог файл.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Марш­ру­ти­зи­ру­ет на осно­ва­нии тэгов (филь­тру­ет). В дан­ном слу­чае допи­сы­ва­ет имя хосто­вой систе­мы, на кото­рой запу­щен контейнер.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Выпол­ня­ет раз­бив­ку на пор­ции дан­ных (chunks ) и пишет их в буфер (buffer), кото­рый нахо­дит­ся на дис­ке. Пор­ции дан­ных поме­ща­ет в оче­редь. Отправ­ля­ет коллектору.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Если TCP сокет кол­лек­то­ра не досту­пен — про­дол­жа­ет читать файл и акку­рат­но скла­ды­ва­ет пор­ции дан­ных до уста­нов­лен­но­го пре­де­ла, достиг пре­де­ла, оста­нав­ли­ва­ет чте­ние фай­ла и запо­ми­на­ет пози­цию. Связь воз­об­но­ви­лась — ок, начи­на­ет пере­да­чу собрав­ших­ся дан­ных и воз­об­нов­ля­ет чте­ние фай­ла с места остановки.

Вот так выгля­дит сфор­ми­ро­ван­ный фор­мат JSON лога, кото­рый полу­ча­ет­ся в ито­ге на выхо­де для пере­да­чи на коллектор:

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Итак разо­бра­лись с тем, что про­ис­хо­дит на клиенте.

Коллектор

Теперь рас­смот­рим, что про­ис­хо­дит на коллекторе.

Стрим посту­па­ет на TCP сокет кол­лек­то­ра и начи­на­ет про­цесс марш­ру­ти­за­ции (филь­тра­ции), и про­хо­дит сле­ду­ю­щий воркфлоу.

Попа­да­ет на цепоч­ку docker_before — дан­ная цепоч­ка пред­на­зна­че­на для пере­опре­де­ле­ния тегов вхо­дя­щих логов от Docker кон­тей­не­ров. Каж­дый кон­тей­нер име­ет допол­ни­тель­ные атри­бу­ты, такие как “attrs.service” и “attrs.container_name”. Пере­опре­де­ле­ние тега про­ис­хо­дит на осно­ва­нии “attrs.service”, напри­мер nginx. Все осталь­ное, напри­мер, нам нет необ­хо­ди­мо­сти соби­рать логи с кон­тей­не­ра mongodb, все что нам нуж­но это убрать его из цепоч­ки опре­де­ле­ния сер­ви­са и он про­сто упа­дет в null. Выгля­дит вот так:

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Далее, выпол­ня­ет­ся марш­ру­ти­за­ция (филь­тра­ция) на осно­ва­нии пере­опре­де­лен­но­го тега “docker.nginx” и попа­да­ет в сле­ду­ю­щий кон­фиг файл.

Nginx_common — дан­ная цепоч­ка выпол­ня­ет раз­бив­ку логов на error и access на осно­ва­нии JSON поля “stream” и пере­опре­де­ля­ет тэги на “docker.nginx.stdout” и “docker.nginx.stderr”. Даль­ней­шее про­дви­же­ние лога про­ис­хо­дит на осно­ва­нии уже пере­опре­де­лен­ных тэгов.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Далее про­ис­хо­дит мат­чинг по тэгу “docker.nginx.stdout” и при­ме­ня­ет к нему пар­сер с опре­де­лен­ным форматом.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Далее моди­фи­ци­ру­ет поле upstream_response_time (мы пере­опре­де­ля­ем зна­че­ния неко­то­рых полей (mapping)в Elasticsearch индек­се для кор­рект­но­го поис­ка и филь­тра­ции), что­бы поле содер­жа­ло толь­ко цифир­ные зна­че­ния, а не про­чер­ки, если зна­че­ние отсутствует.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Далее добав­ля­ем GeoIP дан­ные, такие как “country_code”, “country_name”, “city” и “geoip_hash”.

Вот тут, кста­ти, заме­ти­ли, что память “тек­ла” при исполь­зо­ва­нии биб­лио­те­ки “geoip2_c”, поэто­му выбра­ли “geoip2_compat”.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Так как поля “stream” и “log” мы рас­пар­си­ли, то сме­ло можем их уда­лять. Поле “log” уда­ля­ем толь­ко с логов, тэги­ро­ван­ных как “docker.nginx.stdout”, а поле “stream” уда­ля­ем со всех логов (это поле на осно­ва­нии, кото­ро­го мы дела­ли пере­опре­де­ле­ние тэга в нача­ле этой цепочки).

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Пар­сим логи, тэги­ро­ван­ные как “nginx.docker.stderr”. Тут вооб­ще все про­сто, раз­би­ра­ем по мини­му­му: вре­мя, тип (severity type), pid, ну и соб­ствен­но само тело сооб­ще­ния (остав­ля­ем как есть).

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Далее акку­рат­но рас­кла­ды­ва­ем по индек­сам, пред­ва­ри­тель­но, заки­ды­вая шаб­лон для индек­са. Но, преж­де чем отпра­вить в индекс дан­ные, они скла­ды­ва­ют­ся пор­ци­я­ми дан­ных в буфер, кото­рый рас­по­ло­жен на дис­ке, попа­да­ют в оче­редь и сбра­сы­ва­ют­ся пор­ци­я­ми (bulk flush) каж­дые 5 с.

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

На выхо­де из кол­лек­то­ра лог полу­ча­ет­ся вот таким:

[codesyntax lang="php" blockstate="collapsed"]

[/codesyntax]

Что каса­ет­ся настрой­ки индек­са в Elasticsearch, то мы изме­ня­ем пара­метр “number_of_shards: 1”, кото­рый по дефол­ту равен 5. Уве­ли­чи­вать коли­че­ство шард (shard) есть смысл, когда собран кла­стер, так как при запро­сах Elasticsearch парал­ле­лит поиск по шар­дам в дата нодах, а так как дата нода у нас одна, то и коли­че­ство шард долж­но быть рав­ным коли­че­ству дата нод кластера.

Еще один пара­метр, кото­рый мы меня­ем это “number_of_replica: 0” — не дер­жим репли­ку для Primary Shard, так как реп­ли­ци­ро­вать, соб­ствен­но гово­ря, и некуда.

В мапин­ге (mapping) индек­са Elasticsearch мы изме­ня­ем типы сле­ду­ю­щих полей:

  • bytes_sent — integer
  • geoip_hash — geo_point
  • remote_host — ip
  • request_time — half_float
  • status — short
  • upstream_response_time — half_float