Как стать автором
Обновить

Комментарии 41

НЛО прилетело и опубликовало эту надпись здесь

Вы совершенно точно зря на себя наговариваете, всё получится!

увидишь статью от mkevac, не открывай ))
шучу, открывай

а я наоборот...

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

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

от прочтения статьи испытал кайф от выбора языка

А почему не использовали какой нибудь Memcached/Redis? Вероятно это было бы гораздо лучшее решение в плане расхода памяти вцелом т.к. кеш не дублировался бы между всеми экземплярами сервиса. Скорее всего производительность также была бы выше т.к. не было бы необходимости в сборке мусора.

появляется еще одна точка отказа + это будет доп сетевой хоп
Когда строгая консистентность не требуется — мемори-кеширование выгоднее
Ожидал подобного вопроса. В данном случае я специально применил in-memory cache, т.к. если использовать Redis и Memcached у нас появляется сразу большое количество ошибок с сетью, которые регулярно происходят при использовании Redis и подобных решений. Помимо этого, у нас появляется оверхед в виде кодирования данных для отправки в Redis, отправки данных через сеть, получения ответа от Redis, раскодирования и так далее. Это сильно аффектит response time сервиса и нагружает и без того нагруженную сеть у нас. При этом, оперативной памяти у нас довольно много свободной, поэтому мы можем пожертвовать дублированием в данном случае ради большей стабильности и производительности.
1) Какая есть проблема с тем чтобы разместить редис на там же хосте где и ваша бизнес логика? Я не знаком с терминами кубернетес, возможно ограничение из-за него. Но в общем подымаете редис на одном хосте с бизнес логикой на любом ЯП, тогда оверхеда сети в коммуникации не будет большого, потому что коммуникация внутри одного хоста находится.

2) Подойдет ли для решения вашей проблемы продукт типа tarantool, где бизнес логику можно прямо внутри писать?
1. С этим проблемы имеются ввиду обширности архитектуры авито. У нас миллионы запросов в минуту и соответственно ОЧЕНЬ много серверов, которые орекстрируются при помощи kubernetes, поверх которого наш PaaS. Но тем не мене, оверхед в необходимости закодировать/декодировать данные перед отправкой в Redis остается, хоть и минимизируются сетевые издержки.
2. Не подойдет, но тем не менее, его мы используем в других задачах. Не подойдет потому, что нас придется тогда обучать всех разработчиков работе с tarantool, когда у нас все разработчики уже знают Go. Это первый момент. Второй момент — опять же загрузка сети.
1) Коммуникации внутри одного хоста тоже совсем не бесплатные. Хоть они и ничтожны по-сравнению с сетью, но все-же гораздо выше чем inmemory
Второй момент — сервис запущен в нескольких экземплярах на физически-разных машинах (повышаем отказоустойчивость). Отсюда только одна реплика сервиса работает без сетевого оверхеда.
Третий момент k8s — он базово не гарантирует что после раскатки новой версии сервис не окажется на другой машине (на самом деле почти наверняка окажется).

2) Вероятно вы предлагаете использовать tarantool как application вместо GO. Tarantool не сильно cloud native. Готовить его так, чтобы ты мог в любой момент изменить количество реплик сервиса, и не терять стейт при перевыкатке или disaster крайне не просто. Ну и в итоге — мы получим все то-же inmemory хранилище как и в Go. Просто несколько более оптимизированное.

По поводу пункта 1. Под в k8s может содержать более одного контейнера и при желании можно написать деплоймент так чтобы рядом с каждым экземпляром сервиса поднимался контейнер с кешом.


Но вцелом мне все таки интереснее мой первоначальный вопрос:
Есть ли статистика hit/miss и как долго отдельное значение живет в кеше?


Условный пример:
У вас есть 3 экземпляра сервиса и у каждого независимый кеш. В ходе работы какого-то бизнес процесса происходит 3 обращения к вашему сервису для получения значения. В дальнейшем в течении длительного времени это значение не используется. Для данного примера это означает что кеш абсолютно неээфективен т.к. изза лоад балансера запросы попадут на разные сервисы и во всех 3-х случаях это вызовет дорогую операцию вычисления значения.

Конечно можно положит по контейнеру с редисом в каждую реплику-)
Но тогда мы получим независимые кеши на каждой реплике а это не будет принципиально отличаться от memory кеширования в самом GO. Только больше CPU потратим.

Есть ли статистика hit/miss

Вы правы, говоря что каждый из 3-х реплик будет иметь независимый кеш. И что hit\miss считать нужно, дабы понимать эффективность всей этой истории.

И у нас она есть, и мы ее мониторим.
Но в целом — статья про работу планировщика в GO и нюансы профилирования, а не про
Вообще забавно, что 90% комментариев посвящены первым 10% наполнения статьи(про memory-кеш) а вовсе не про мякотку с профилированием приложения.
Проблема с профилирование вышла из выбора архитектуры, а почему такое архитектурное решение было выбрано в самой статье не обозначается. От сюда большая часть вопрос именно по первой части.

Само профилирование не то чтобы сильно сложная тема. В Idea в несколько кнопок делается и рисует схемы буквально как у вас в стать. И это уже было много много лет назад. А решение проблемы по сути просто одно библиотеку на другому поменяли, в принципе могли бы и сразу посмотреть в коде библиотек на способ хранения.

Но лишний раз память освежить не помешает, так что за статью – спасибо!
> это было бы гораздо лучшее решение
Не всегда. Чаще всего наоборот in-memory предпочтительнее.
В случае с редис/мемкеш — добавляется оверхед на коннект-маршаллинг-анмаршаллинг-сеть
Плюс если говорить про ресурсы — то сеть чаще всего дороже как ресурс чем память на отдельный железках. При масштабировании упереться в сеть гораздо проще.
Да нужно учитывать минусы in-memory — дублирование кешей. Разные поды могут содержать разные по актуальности данные. Обновление протухших кешей повлечет за собой N запросов а не 1 в случае с редисом например.
> Скорее всего производительность также была бы выше
вот тут в корне не согласен. производительность ин-мемори в подавляющем большинстве случаев будет намного выше. Отсутствие сетевого лага. маршаллинга и анмаршаллинга.
П.С. конкретно в Авито утилизация сети достигает порой 70-80%.
производительность ин-мемори в подавляющем большинстве случаев будет намного выше.

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


Автор не предоставил еще одну ключевую метрику без которой сложно о чем то судить:
Какой вообще общий процент кеш хит/мисс? Я подозреваю что если сборщик мусора постоянно занят уборкой вытесненных значений то кеш вообще не эффективен и просто тасует данные туда-сюда. Это также косвенно следует из того что время ответа сервиса упало весьма незначительно (в 1,8 раз) для эффективного кеша можно было бы ожидать улучшения на порядок и более.

GC же занять не уборкой мусора. а проверкой всей той кучи значений в кеше.

И чудовищная неэффективность — чисто из-за того что кешируем много маленьких объектов, и порождаем тонну указателей
Не обязательно нагружать гц. Большинство существующих решений для кэширования в го как раз и работают, выделяя просто слайсы байт и храня там объекты, гц тогда туда просто не полезет. Если хочется большего перфоманса, то mmap'ом выделяют память вне гц и через unsafe размещают там данные, чуть опаснее работать, но в целом почему бы и нет, если требования особые. Вроде, у дропбокса были статьи на эту тему.
Все верно. Именно это и сделали. Заменили библиотеку для кеширования.

В тему «большинства решений в го» — лично у меня статистики нет. Но популярные библиотеки, в которых такая проблема есть встречаются
Первое, что приходит в голову, — добавить кэш для того, чтобы не ходить лишний раз в сервис.

Неееет! Первое что должно приходить в голову — какого этот сервис не может за 300мс выполнить свою работу? И только после ответа на этот вопрос — кеши и ещё ещё что-то.


Вообще непонятно зачем все эти сложности? Есть юзер, у него есть верифицированный номер телефона. Есть объявление, у него тоже есть номер телефона, который либо anonymous, либо call tracking, либо берётся из данных юзера. Система решает этот вопрос в момент создания объявления. Есть отдельный сервис — пул телефонов, который связан с АТС и выдаёт номер нужного типа, в момент создания объявления. Больше к нему никто не обращается, нагрузки особой на него нет. Кажется все. И тут не видно всех тех сложностей, о которых пишут в статье, типа сервисы работы с телефонами тормозят, а их ещё и несколько штук…


Может я чего-то не понимаю, но архитектура выглядит криво.

Постараюсь ответить на все вопросы по порядку.
Неееет! Первое что должно приходить в голову — какого этот сервис не может за 300мс выполнить свою работу? И только после ответа на этот вопрос — кеши и ещё ещё что-то.

Это было известно с самого начала, но специально не упомянуто тут, т.к. выходит за рамки статьи. Сервис медленный, т.к. сделан на php и работает с внешними сервисами, которые выделяют номера. За тот сервис отвечала другая команда, но в процессе оптимизации phones-gateway, о котором и рассказано в статье, наша команда также оптимизировала тот сервис, но переписывать его на Go было не в рамках нашего пула задач, и сильно оптимизировать мы не смогли, сейчас ситуация с ним значительно лучше, т.к. его переписали и оптимизировали.
Вообще непонятно зачем все эти сложности? Есть юзер, у него есть верифицированный номер телефона. Есть объявление, у него тоже есть номер телефона, который либо anonymous, либо call tracking, либо берётся из данных юзера. Система решает этот вопрос в момент создания объявления. Есть отдельный сервис — пул телефонов, который связан с АТС и выдаёт номер нужного типа, в момент создания объявления.

Тут у нас уже появляются требования Роскомнадзора и других органов. Номер телефона пользователя является персональными данными пользователя и хранится в отдельной таблице от объявления в виде зашифрованных данных и получение этих данных происходит через специальный сервис-хранитель перс. данных. Более того, в вашей модели не учтен момент, что пользователь может захотеть поменять номер телефона и поменять его так, чтобы он поменялся сразу на всех объявлениях.
Больше к нему никто не обращается, нагрузки особой на него нет. Кажется все. И тут не видно всех тех сложностей, о которых пишут в статье, типа сервисы работы с телефонами тормозят, а их ещё и несколько штук…

Тут еще дело в том, что этими сервисами заведуют разные команды, которые решают разные бизнес задачи, поэтому есть дифференциация на разные типы номеров для переадресации, и они преследуют разные цели. Также, реальный номер телефона может потребоваться получить в админке или еще в каких-то сервисах, по рассылке СМС к примеру. Поэтому архитектура на деле оправдана и обусловлена требованиями бизнеса.
Вроде бы как
cache []int64 — не массив, а слайс.
cache [1024]int64 — это уже массив, иммутабельный.
Из-за этой мелочи читать статью становиться очень проблематично…

Благодарю за замечание! Действительно, в go чаще используется термин slice для массивов, которые не имеют заданной длины и в терминологии go array и slice действительно отличаются. Но я в статье имел ввиду массив как более общий для программирования термин, просто не очень хотелось использовать слово «срез» или англицизм «слайс» и именно поэтому я использовал слово «массив». В статью внес правку, чтобы не вводить никого в заблуждение.
спасибо за статью
неплохо расписано про работу с профилировкой
Про Memcached/Redis уже спросили. Но что мешает использовать sqlite например?
sqlite чтобы что?
sql задач вроде не стоит. Но на пустом месте ловим io нагрузку+сериализация — десериализация.
Очевидно чтобы использовать как in memory database.
Дык им не надо SQL — там же простой LRU кеш.
Очевидно что бы получить оверхед на CGo до кучи)))
Ну мне это все не очевидно. sqlite много где используется именно как кеш, умеет в многопоточность и довольно таки оптимизирована. Так же не стоит забывать что можно расширять функциональность, например написать модуль для очистки expired записей, вместо того чтобы дергать
delete from tbl where expired_at<...


Если не нравиться sqlite, то это может быть любая доступная встраиваемая бд или просто голый движок хранилища.
Ну в случае любой встраиваемой БД (ровно как и sqlite) как правило будет все равно оверхед. В моем же случае, мы просто храним в оперативной памяти данные, которые не надо никуда преобразовывать и RT сервиса в этом случае будет равняться RT получению нужных данных из оперативной памяти и декодированию/кодированию запроса, что в общем то и надо на высоких нагрузках.
Для начала разберёмся, что такое троттлинг и CPU в терминах Kubernetes. Когда мы запрашиваем один CPU в Kubernetes, это не значит, что нам выделяется конкретно одно ядро процессора из 48, например.

Есть такое. Можно выделять ядра эксклюзивно.

Можно.
Но кажется вся идея k8s как раз про то, чтобы достаточно плотно напихивать сервисы в кластер.
А прибить к сервису пачку ядер, которые и не будут утилизироваться в 50% времени, и все-равно не решить задачу из статьи…
Но кажется вся идея k8s как раз про то, чтобы достаточно плотно напихивать сервисы в кластер.

Всё же больше про управление, масштабирование и отказоустойчивость. Напихивание — уже по желанию.


А прибить к сервису пачку ядер, которые и не будут утилизироваться в 50% времени

Чтобы избежать этого, имеет смысл использовать HPA. В случае же заданий (job) подобной проблемы не стоит вовсе.


и все-равно не решить задачу из статьи…

Никто этого и не обещал.

> Чтобы избежать этого, имеет смысл использовать HPA
все вместе будет требовать определенного тюнинга каждого сервиса.
и доп тюнинга по мере развития проекта. ну и всегда есть шанс словить маркетинговую акцию и тупить пока развернуться до ядра.

В общем — это полезная штука, но использовать надо очень даже с умом.
Спасибо за статью, всегда интересно почитать про оптимизацию!
Завис немного в самом начале на первой реализации обновления массива
   if c.Has(userId) && status == 6 {
        c.Delete(userId)
     }
     if !c.Has(userId) {
        c.Add(userId)
     }

разве в случае удаления мы не будем всегда возвращать удалённое значение обратно т.к. !c.Has(userId) после удаления будет true?
Не смущает что такая реализация нарушает The Twelve Factor App, хотя конечно мб оно по делу у вас (кеш в оперативе)
Это отличный комментарий, спасибо.
Все рекомендации из The Twelve Factor App — действительно толковые.
Но не стоит их воспринимать и следовать им с библейской точностью.

Кеш, липкие сессии и прочее в среднем нарушают ряд факторов, это верно.
И за нарушение надо платить. К примеру меньшей консистентностью. Но если для бизнес-задачи оно не критично, но позволяет повысить надежность в разы, снизя утилизацию ресурсов…
Почему-бы и нет?
Такой еще вопрос остался не раскрытым.

Как вы выкатываете новые версии вашей программы? Если кеши хранятся in-memory, то выходит что при деплое новой версии все хеши обнуляются. Те на момент деплоя аппы у вас на сайте кнопка показать телефон начинает подтормаживать. Для вас это нормально?
Тут нам на помощи приходит механизм Readiness Probe и градуальная выкатка в k8s
Сначала поднимается одна новая реплика. Она прогревает кое-какой кеш. И только после этого она помечается как Redy, и заменяет одну старую.

Ничего в процессе не подтормаживает.
Понял вас, спасибо за ответ. Круто сделали!
Зарегистрируйтесь на Хабре, чтобы оставить комментарий