go-graphite/graphite-clickhouse

escape special characters to clickhouse (ru lang issue)

selfowner opened this issue · 22 comments

ПРивет!

Предпринимаем вполне успешные попытки впилить ch в качестве бэкенда для графита, используя ваш стек.
вообщем-то после танцев начало всё работать, но всплыл нюанс

У нас используются в путях метрик спец символы, пример метрики
$type.app.$cluster.topoviy_service.$host.$hostname.$metric.qwer*.TransactionHandler.transaction.time.m1_rate
проблема в том, что по итогу в кх попадает запрос вида and match('^$[...blablabla]), результата ноль естественно

пришлось погрузиться в основы го, и удалось пофиксить в моменте finder, но на пулл реквест не тянет

фикс (workaround, костыль) выглядит так:
было

        idx.body, err = clickhouse.Query(
                scope.WithTable(ctx, idx.table),
                idx.url,
                fmt.Sprintf("SELECT Path FROM %s WHERE %s GROUP BY Path", idx.table, w),
                idx.opts,
        )

стало

        idx.body, err = clickhouse.Query(
                scope.WithTable(ctx, idx.table),
                idx.url,
                strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("SELECT Path FROM %s WHERE %s GROUP BY Path", idx.table, w), `^$`,`^\$`), `[.]$`, `[.]\$`),
                idx.opts,
        )

правда шкварится на variable из grafana, видимо надо там иначе спасать(эскейпить), но пока этим не раскуривался.
не уверен, что graphite-ch это лучшее место для эскейпа этой хренотени, карбонапи только буду смотреть

что думаете на счет этого в целом и на счет втащить это в проект? возможно можно как-то более лаконично оформить в коде, и распространить и на другие функции, помимо файндера, например на теги

А как это решалось в whisper? Он позволял хранить на диске файлы с символами долларов в именах?

Ну $ это ASCII, даже не UTF-8. Его надо эскейпить если шелл, но виспер - не шелл:

% touch \$cluster
% ls -al \$cluster
-rw-r--r--  1 deniszh  deniszh  0 Oct  1 10:36 $cluster

значит, надо все символы из запроса, которые имеют специальное значение, экранировать. Но так предпочтительнее .->[.], $->[$]

Я попробую посмотреть, как это сделано для точки.

@selfowner попробуй #94?

А как это решалось в whisper? Он позволял хранить на диске файлы с символами долларов в именах?

Привет!
О виспере сложилось впечатление что как позволяет фс так можно и хранить =) карбон классической формы не застал, но go-carbon из коробки складывает в обычный виспер as is, жрёт любые легальные specials. из популярного только - не запишет метрику с двумя точками подряд, папка без названия не может быть создана =))
уточнение* - по факту в виспере файлы у нас были = тип агрегации, - м1, п95, мин, макс и т.п. а вот подкаталоги по пути к файлу, даа, захардкожены с долларом в начале файла.

Такое чудесное именование у нас повелось исторически - когда-то графана или иной инструмент визуализации точечек не оч умел в variables, и накостылили творцы свой вариант метаданных а-ля через рядом стоящие leafs, олицетворяющие имя подметрики..

В сорцах используемой ранее рисовалки, такие эскейпы:
https://github.com/brutasse/graphite-api/blob/master/graphite_api/finders/whisper.py
https://github.com/brutasse/graphite-api/blob/master/graphite_api/render/grammar.py
А официальный поинт такой (https://graphite.readthedocs.io/en/latest/tags.html про формат тегов, а про метрики без тегов не особо нашел "official character list" =)))

Помимо таких спешл чарактеров ещё встречались куски кода вместо метрики - '$type.app.$cluster.topoviy_service.$host.hostname*.$metric.$service.datamatching.matched.!is_b2c_p3p_unscs_er667_dt_l_&&is_vip&&is_rf&&(server_protocol_is_ICQ7.0||server_protocol_is_MIRANDAIM||server_protocol_is_PIDJIN)&&place_is_OUTOFSPACE', висперу-то понравилось, если не упиралось в ограничения по длине имени файла, то исправно складировал это добро. рендерилось или нет - хз, я при обнаружении эти куски программного обеспечения удолил.. Видимо нет, так как это сопровождалось вопросами "почему графит опять лагает и когда уже почините, рукопопы клятые, у нас релиз был а МЕТРИКИ ПЕРЕСТАЛИ РИСОВАТЬСЯ(("

В целом не ловить такое позволяет dropregexp на приемнике, и проблемы как таковой нету с символами, которые не хочется видеть, - доллар просто плотно въелся в депенденси которой все у нас отправляют метрики. Но ничто не вечно и он будет упразднен!)

Вернусь в реалтайм, итак, подняли 2 прод инстанса carbon-ch;ch-server;carbonapi;graphite-clickhouse (в carbon-ch симметрично шлют броадкастом метрики другие карбоны, на входе в них dropregexp деструктивных последовательностей))
программно-аппаратная конфигурация на данный момент следующая:

миррор рейд на скази хдд, 500гб пространства - занято около 30%, чуть меньше месяца метрик с дефолтным пресиженом
48 cpu e5 2697 @ 2.7 - ла 0.25-0.45 на ядро
128gb ram - всё сожрано под кеши и фиг его знает что
Каждая нода содержит все продакшн метрики (заливается параллельно силами карбон, репликаций пока нет),
на обоих carbonapi раундробином обе базы кх используются. 
балансировка между carbonapi происходит по принципу"fail over", в случае если ответ carbonapi/lb_check !Ok - балансировка траф идет в карбонапи второй ноды, который тоже смотрит в оба кх раундробином.

пока что такая схема не очень устраивает, в случае отказа/высокой нагрузки, переключение происходит не довольно-таки шумно, видимо есть смысл размазать нагрузку более равномерно до момента внедрения тегов (судя по метрикам carbonapi, если я смотрел нужные, кеш работает так себе, работает ли. 
Если работает, то хотелось бы чуть подробнее раскрыть тему конфигурирования кешей (несколько видов ведь))

Ну и кстати прикольная фича, доработать lb_check таким образом, что-бы он содержал информацию о доступных для выполнения запросов ресурсах - %ram free, running query quantity. Я не то что бы годный погромист, да и не погромист вовсе, но тут попробую.

О дальнейших планах: что бы система хорошо работала и была модненькой клевенькой и удобненькой - нужен ребрендинг метрик!

tagged опробован кустарно через добавление функции в carbon-clickhouse:

func letstrytags(name []byte) []byte {
        namestr := string(name)
        pattern := regexp.MustCompile(`(?m).*\.\$cluster\.(?P<cluster_value>.*)\.\$(?P<host>.*)\.(?P<host_value>.*)\.\$metric\.(?P<metric>[a-zA-Z0-9-_\.]+)$`)
        tagtemplate := `$metricservice=$cluster_value;host=$host_value`
        taggedname := []byte{}
        for _, submatches := range pattern.FindAllSubmatchIndex(name, -1) {
                taggedname =  pattern.ExpandString(taggedname, tagtemplate, namestr, submatches)
        }
        return taggedname
}

субъективные ощущения:
перенос 2 expandable leafes в теги, решил проблему выборки из больших ветвлений (тяжелые запросы перестают быть таковыми ввиду более эффективного доступа к нужным данным).
Вменяемые запросы метрик в таком формате, уходящие за 15-30сек, соорудить не удалось, - работает максимально быстро.
Для сравнения, некоторые рендеры на продакшене занимают 40-80 сек (самые тяжелые удалось ускорить тюнингом конфигурации и выбором используемых в отрисовке графитовых функций (Методом осмотра состава и структуры метрик на пробемных дашбордах, было выявлено что aliasSub неоднократно усложняет выборку метрик с множеством экспандов, groupByNode заменяет нам его на 98%)). В целом после правок в карбонапи параметровmaxbatchsize (20k), maxGlobs (50), find/render timeout (90/180)s, и carbonlink threads (-> 24)в графит-кх - потребление ресурсов стало на уровне "не уходит в оом при двухкратном rps стандартных графиков)".

В целом, на бумажке план на входе смотреть наличие 4-5 обязательных `tag_name` - `dc, env, servicename, hostname, aggregation_type` _(Если набора нету - отправляем в специальноразвернутый под старый формат junkite xD)_
Для метрик большинства (~70%) наших сервисов этого более чем хватит и на прикидку сделает запросы в [0-9]{2} раз выгодней. В рамках "не оставления без внимания оставшихся 30%" - добавить опциональных несколько тегов, например, `module_name, module_metric_subtype, module_metric_subtype_id, src, ds`t для больших программ включающих в себя множество модулей -` jvm, kafka, oauth, icquinbruteproccesor, sixdigitcredssender ` и т.п. а у модулей классы, а у классов методы. Иногда они общаются каждый с каждым/им что-то присылают/они что-то отправляют; ну и всё и сразу тоже есть xD)
Как я вижу, после разбора метрики на составляющие её метаданные - оставшиеся имена метрик тоже по мере возмоги надо унифицировать, если например у апп uincreator есть RequestTime (ms), а у uincredssender ReqTime - если назвать их одинаково, экономим строки, улучшаем навигацию и ускоряем поиск глазами человека, вычисления компьютера.

Отталкиваясь от этих возможности дифференциации по такому набору метаданных, и учитывая возможности carbonapi в плане lb + агрегации результатов, в перспективе видятся такие profits&benefits, как: 
Высокая производительность выборки данных;
next level user experience у пользователей в области визуализации метрик; 
удобное масштабирование бд - можно без особых проблем разнести данные по инстансам кх в разрезе datacenter/env/service_name  
возможность снижения precision для метрик -> реквесты более высокой детализации значений происходят всё чаще
возможность использовать один источник данных для нескольких сред - это открывает не только всякие там опции для A/B, QA и прочих аббревиатур, но и в целом повысят UX без риска заниматься увлекательным отстрелом тестового мусора из продуктивной базы.

Главн не создать лишних проблем, но вроде технических сложностей не просматривается у каких-либо вовлеченных сторон.

_ну и чтоб цели уходили за горизонт - после Прихода к адекватному формату, в мыслях есть попробовать реализовать что-то вроде graphitted-clickhouse2clickhouse - представление тех же данных, но с другим форматом таблиц (для использования SQL для работы с данными)

абстрактно -

т-ца metrics будет содержать например path, value, ts, date, tag_name_and_value_md5,
т-ца hashishmaps - tag_name_and_value_md5, column[0-9]{1,2} будет содержать tag_value, а имя колонки являться tag_name; ну и видимо
т-ца indexes.
Я не администратор ДБА, и не архитектор таблиц, но видится мне, что это позволит использовать SQL инструментарий ch без omg % \$regexpов[.]and[.]pains.m1_rate, и прочего добра, выполнять всякие вычислительные действия над данными, =)_

Бтв, в процессе воспоминаний красивых имён при написании данной стены, возникла идея превзойти инлайны коллег, иии на барабане выпал сектор приз. carbonapi@graphite-ch,
image
Выглядит просто бомбически офигенно, почти как жаваскрипт переведенный на язык эмодзи. Рендер ломается о конвертацию эмодзи в код символа, графана что-ль шакалит.. но as is берёшь и отправляешь, при этом экспандится в дереве всё остальное вроде штатно, просто без этой метрики. Когда тегов станет мало, ужмемся в эмодзи)

Кстати, есть/была проблема с регистрами - карбонапи ранжирует с учётом регистра (и метрики и функции). Т.е. получается сортировка a-zA-Z, кожаные же мешки в простоте своей не привыкли оперировать регистром в такие моменты, им хочется a-Z =)) Но если размышлять через призму используемых компонент - основная проблема, что в бд регистр - критерий неуникальности, т.е. env=prod != env=PROd != env='prod'. Произведя размышления, пришёл к выводу что логичней не со стороны рендера ещё в одном месте регреплейсами оборачивать, а до того как оно попадет в очередь carbon-ch, не без пачки синтаксических ошибок видится так: [A-Z]+ -> [a-z]+ && (.*;[A-Z]+=.*) -> (.*;[a-z]+=.*) && (.*;[a-z0-9_\-.])([=!~]+)('.*')(.*) -> (.*;[a-z0-9_\-.])([=!~]+)(.*)(.*)

Спасибо!

@selfowner попробуй #94?

instaoom+queryerrors, откатился на билд конца сентября + хотфикс из шапки

А можно диагностику из логов?

оки, но не уверен что вернусь ранее +-середины след недели

щас еще пару кластерков сделаю что бы производство не беспокоить, и постмортем. надо подождать коллег которые их создадут

может я и ошибся, по поводу причин, надо было юзать просто твой билд без своей регулярки, а то на выходе наверное мясо из \. из того что заметил. там еще параллельно aliasSub нагибали, вот недавно довыпиливал и жизнь наладилась прям
вообще щас карбонапи дает 10-20rps и 60% запросов укладывается в 100ms, это действительно классно учитывая оч плохие деревья у нас.
вернусь! спасибо

то есть, #94 работает, если ничего туда не добавлять?

пока не знаю, щас работает на моем но есть баганые запросы (что логично, подразобрался вроде в коде чуть)

рендерит, но с файндером никак - INFO [query] query {"query": "SELECT Path FROM graphite.graphite_index WHERE ((Level=20005) AND (Path LIKE '$var.randomvar.$name.%' AND match(Path, '^$var[.]randomvar[.]$name[.]([^.]*?)[.]([^.]*?)[.]?$')))"
внутри match надо заменить -> $ и тогда запрос отработает. т.е. SELECT Path FROM graphite.graphite_index WHERE ((Level=20005) AND (Path LIKE '$var.randomvar.$name.%' AND match(Path, '^\$var[.]randomvar[.]\$name[.]([^.]*?)[.]([^.]*?)[.]?$')))
но если запрос без *, таких ситуаций не случается

нет, заменять надо не на \$, а на [$]. В целом, именно это и сделано в пулл-реквесте

$ curl 'http://localhost:8080/render?target=some.$test.metri\{c,2\}.a.*&from=-24h&until=now&format=json&maxDataPoints=1521'

log:
[2020-10-13T18:42:34.236+0200] INFO [query] query {"query": "SELECT Path FROM graphite.index WHERE ((Level=20005) AND (Path LIKE 'some.$test.metri%' AND match(Path, '^some[.][$]test[.]metri(c|2)[.]a[.]([^.]*?)[.]?$'))) AND (Date='1970-02-12') GROUP BY Path", "request_id": "b0a7d755f6c53666992c07b7a1096299", "query_id": "b0a7d755f6c53666992c07b7a1096299::91b4d6a502d86b30", "time": 0.100637785}

Точно последний коммит из пулл-реквеста собран?

угу, что бы метрика начиналась с $. глянь как условие начинается AND match(Path, '^ =)

бэкслэши в index.go удваивает походу func escape из where.go, похоже по кр мере
06f678f#diff-2bcfb71e35390d14e02de193f982f28c181636468c06562d0772f0eca0232a7aL41

+пробелм где-то рядом: а что фильтрует метрику при смене leaf? если более дальний leaf содержит */$, то при смене значения в нем (например с * на host123 - обнуляется весь путь до метрики от измененного лифа, и надо нажимать по новой)

т.е. SELECT Path FROM graphite.graphite_index WHERE ((Level=20010) AND (Path LIKE '$var.randomvar.$supervar.service\\\\_name.$host.server27\\\\_domain\\\\_com.$metric.asdsd%' .....
получили, а должно быть SELECT Path FROM graphite.graphite_index WHERE ((Level=20010) AND (Path LIKE '$var.randomvar.$supervar.service_name.$host.server27_domain_com.$metric.asdsd%' .....

Ещё раз, вот этот лог ((Level=20005) AND (Path LIKE '$var.randomvar.$name.%' AND match(Path, '^\$var[.]randomvar[.]\$name[.]([^.]*?)[.]([^.]*?)[.]?$'))) явно показывает, что ты используешь не мою версию. Я не экранирую доллары бэкслешом.

Двойное экранирование в LIKE выглядит совершенно верно, не надо его трогать.

Пожалуйста, собери и проверь бинарь из #94

мде, совсем туплю я в этом мультиплеере файловом =((

ну, теперь походу билд тот:
git checkout dollar_in_metric_name
git pull
git reset --hard
собрал, поменял бинарь, и запросы в этих местах чудесным образом преобразились в SELECT Path FROM graphite.graphite_index WHERE ((Level=20008) AND (Path LIKE '$var.randomvar.$name.%' AND match(Path, '^[$]var[.]randomvar[.][$]name[.]([^.]*?)remote[.]([^.]*?)[.]([^.]*?)[.]([^.]*?)[.]([^.]*?)[.]?$'))) AND (Date='1970-02-12') GROUP BY Path
сорри что ввел в заблуждение, видимо я слишком переговнякал сорцы и примеры из последних моих комментов выходит что вымышленные)

но с этим никуда не делся баг с поломкой метрики при её редактировании -

1makemetric
2editleafwithoutspecials_thatsok
3now take leaf behind leaf with specials
4metric_path is broken
5butifurestoreleafs-queryisfine

Сорян, что делать с долларами в графане я не знаю. Там это переменные.

Можно переключиться в режиме редактирования и поправить руками. Но лучше спросить на форуме графаны. Может быть, есть настройка, которая позволяет использовать знак доллара без превращения в переменную.

П.с. добавить картинки можно просто сбросив их в поле ответа

не, $ не обязательно переменная в графане, только если определена.
тут выглядит как проблема в пересоздании метрики на бэке в процессе смены leafs - без спец символов не воспроизводится проблемы (но ломается не только с $ но и _), судя по query инспектору в graphite-api и carbonapi уходят одинаковые запросы

[2020-10-13T21:51:23.822+0300] INFO [query] query {"query": "SELECT Path FROM graphite.graphite_index WHERE ((Level=20009) AND (Path LIKE '$var.randomvar.$name.z\\\\_offset\\\\_x.$host.%' AND match(Path, '^[$]var[.]randomvar[.][$]name[.]z_offset_x[.][$]host[.](serveralias|full_fq_dn)[.][$]metric[.]lags[.]([^.]*?)[.]?$'))) AND (Date='1970-02-12') GROUP BY Path", "request_id": "96a10c5d2adce817ba7d28123460b70b", "query_id": "96a10c5d2adce817ba7d28123465b70b::e0052eac35b3d969", "time": 0.008395982}
\\\\ явно лишние, сборка родная

tl;dr: с LIKE всё в порядке, он покрыт тестами и работает

Полный ответ: символ _ в LIKE означает любой символ, экранирование в коде выглядит так 1bdb9c2#diff-2bcfb71e35390d14e02de193f982f28c181636468c06562d0772f0eca0232a7aR47-R50

Итак, есть запрос curl 'http://localhost:8080/metrics/find?query=some.$test.me_tric.a.*'
Так он выглядит в логе

[2020-10-13T22:28:30.187+0200] INFO [query] query {"query": "SELECT Path FROM graphite.index WHERE ((Level=20005) AND (Path LIKE 'some.$test.me\\\\_tric.a.%')) AND (Date='1970-02-12') GROUP BY Path", "request_id": "39c62f3ba6c4ec027e46939b929c28b4", "query_id": "39c62f3ba6c4ec027e46939b929c28b4::5f63f115f57d6b6f", "time": 0.099642222}

А вот так этот запрос выглядит в tcpdump

SELECT Path FROM graphite.index WHERE ((Level=20005) AND (Path LIKE 'some.$test.me\\_tric.a.%')) AND (Date='1970-02-12') GROUP BY Path

4-хкратное экранирование - это экранирование json для лога.

Двойное нужно, потому что https://clickhouse.tech/docs/en/sql-reference/functions/string-search-functions/#matchhaystack-pattern

Note that the backslash symbol (\) is used for escaping in the regular expression. The same symbol is used for escaping in string literals. So in order to escape the symbol in a regular expression, you must write two backslashes (\\) in a string literal.


Интересное наблюдение по поводу спецсимволов в графане, я посмотрю, что там такого происходит, и в чём могут быть различия между кейсами, когда метрики сбрасываются, и нет

Я попробовал поиграться с символами, поподставлять разные значения в разные ноды имени метрики, но не воспроизвелось. Думаю, надо брать дампы ручки /metrics/find? и идти с вопросом на форум графаны. finder на стороне graphite-clickhouse отрабатывает честно, насколько я могу судить

судя по query инспектору в graphite-api и carbonapi уходят одинаковые запросы

Я бы посмотрел на ответы в консоли разработчика, что возвращает рабочий graphite-api и неработающий carbonapi, но в любом случае, это выходит далеко за рамки проблемы со знаком доллара в именах метрик =)