[Перевод] Golang в AeroFS
Перевод статьи инженера компании AeroFS о переводе их микросервис-архитектуры с Java на Go.
TLDR; Портировав некоторые наши микросервисы с Java на Go, мы уменьшили использование памяти на несколько порядков.
В начале была Java
Архитектура AeroFS Appliance состоит из многих микросервисов, и подавляющее большинство из них написаны на Java. Это никогда не создавало нам проблем, вся система обслуживает тысячи пользователей от разных клиентов без каких-либо проблем с производительностью.
Однако после нашего перехода на Docker, мы отметили резкое повышение использования памяти нашей системой. Опробовав несколько модных утилит для мониторинга докеров, мы остановились на этом несколько гиковском, но очень полезном скрипте:
for line in `docker ps | awk '{print $1}' | grep -v CONTAINER`; do \
echo $(( `cat /sys/fs/cgroup/memory/docker/$line*/memory.usage_in_bytes` / 1024 / 1024 ))MB \
$(docker ps | grep $line | awk '{printf $NF" "}') ; \
done | sort -n
Он выводит список запущенных контейнеров, отсортированных по количеству используемой резидентной памяти. Пример вывода скрипта:
46MB web
66MB verification
74MB openid
82MB havre
105MB logcollection
146MB sp
181MB sparta
Исследовав проблему, мы обнаружили, что некоторые Java сервисы использовали на удивление много памяти, зачастую никак не коррелируя с их сложностью или отсутствием таковой. Мы выделили несколько главных факторов, которые приводили к такому использованию памяти.
- увеличение количества запущенных JVM, так как каждый tomcat servlet бежал в отдельном контейнере
- урезанная возможность для нескольких JVM разделять read-only-память: саму JVM, все зависимые библиотеки, и, конечно, множество JAR-ов, используемых разными сервисами
- изоляция памяти в некоторых случаях сбивала с толку эвристику расчета памяти, что приводило к большим аллокациям кеша в некоторых сервисах
Будучи человеком старой закалки, привыкшим писать на ассемблере для Z80 для устройств с 64 кб памяти на борту, меня очень воодушевляла мысль о том, как бы вернуть сотни мегабайт ценной оперативки. По счастливому случаю, наш следующий хакатон был всего через несколько дней, и это был отличный шанс, чтобы сфокусироваться на проблеме и отличное оправдание, чтобы попробовать новые инструменты.
Кодовое имя: Greasefire
(прим. переводчика: greasefire — «пожар, возникший от возгорания масла/жира на кухонной плите»)
Моей главной целью этого хакатона было прожечь слои метафорического жира, чтобы уменьшить общее количество используемой памяти AeroFS системы.
В частности, моими критериями успеха были:
- использование CPU не должно заметно возрасти
- стабильность и безопасность памяти должно остаться
- использование резидентной памяти должно уменьшиться в 2 или более раз
В духе хакатона, я также хотел попробовать новые языки и инструменты, поэтому просто подправить существующие сервисы было не вариант.
И чтобы увеличить вероятность получения результата, который можно будет показать и, возможно, даже задеплоить, было важно выбрать адекватного размера и сложности цель. Очевидным выбором стал сервис TeamServer probe (team-servers на странице appliance status) — маленький tomcat servlet с единственным HTTP-вызовом и очень ясной внутренней логикой.
В итоге, цель была следующая — создать сервер:
- с полностью идентичным API
- упакованный в docker-имидж
Пробуем новые инструменты
Чтобы уложиться в критерии по CPU и памяти, основными кандидатами стали компилируемые языки, созданные для системного программирования. И хотя хакатон не подразумевал, что результат будет сразу же юзабельным, я всё же придерживался того, что код должен быть легко поддерживаемым и уходил от более тёмных альтернатив.
Пул кандидатов быстро сузился до двух участников — Go и Rust. Оба достаточно легко компилируют код в небольшие статические бинарные файлы, идеально заточенные для запуска в минимальных контейнерах. Оба обещали адекватную производительность, сохранность памяти, хорошую поддержку конкурентного программирования и, что было особо важно для меня, меньшее использование памяти, чем в случае с JVM.
Замысловатая система типов Rust выглядела особенно интригующе. Но при этом, Rust был намного менее зрелым, чем Go, на тот момент ещё даже не достигшим версии 1.0. Выбору Rust также мешало отсутствие хороших библиотек для HTTP и низкоуровневой работы с сетью.
Ранее мы уже пробовали портировать один из наших сервисов на Go, году этак в 2013-м, но на тот момент мы попали на какую-то утечку памяти, и решили прекратить эксперимент. Спустя два года, Go выглядел гораздо более зрелым и был выбрал как самый подходящий кандидат для нашего эксперимента.
Go достаточно похож на языки из C-семейства, что позволяет очень быстро его подхватить, но при этом содержит достаточно особенностей, требующих первые несколько дней постоянного переключения между кодом и документацией. К счастью, документация отлично написана и очень удачно инкорпорирована в исходные коды стандартной библиотеки, что было невероятно полезно, для прояснения различных моментов и понимания идиоматичного кода.
Я также был очень обрадован наличием единого стандарта форматирования языка, и магической утилиты gofmt, которая принуждает к нему и очень легко интегрируется с текстовым редактором вроде vim (небольшой инсайт: этот хакатон также был моей первой попыткой использовать vim для чего-то большего, чем однострочное редактирование)
Результаты
У меня заняло около дня, чтобы познакомиться с Go и портировать простой сервис, выбранный для хакатона. Результаты были очень многообещающими:
- Размер кода был уменьшен вдвое, с 175 строк до 96
- Использование резидентной памяти упало с 87MB до всего-лишь 3MB, 29х уменьшение!
- Результирующий docker-имидж уменьшился с 668MB до 4.3MB — это 155х уменьшение! Согласен, что наибольшие слои докер-имиджей все равно переиспользовались разными сервисами, поэтому реальное уменьшение использования диска было намного меньше при использовании многих Java-сервисов. Тем не менее, эти цифры очень радовали глаз.
До хакатона оставался ещё почти целый день, и я обратил внимание на ещё один сервис — Certificate Authority (ca на appliance status page). Этот сервис принимает запросы на подпись сертификатов от внутренних сервисов и десктоп-клиентов и возвращает подписанные сертификаты, использующиеся для шифрования пересылки как peer-to-peer контента между клиентами, так и для клиент-серверной коммуникации.
Когда этот новый CA наконец-то заменил свой Java-эквивалент, через несколько дней после окончания хакатона, он уменьшил использование памяти на невероятные 100х!
Этот проект выиграл номинацию «Техническая крутизна» (ориг. «Technical Amazingness»), и превратился в продолжающиеся усилия по уменьшению использования памяти всей системы.
К версии 1.1.5 ещё четыре сервиса были портированы на Go — 6 в целом — и суммарная экономия памяти составила 1 Гигабайт. В каждом случае мы получали аналогичное уменьшение в размере кода, и в некоторых случаях мы даже получили значительное уменьшение использования процессора или лучшую пропускную способность.
— Hugues & the AeroFS Team.