Поставщик высоконагруженного API Stream перешёл с Python на Go, хотя этот язык знают немногие. Причинами решения делимся под катом к старту курса по Backend-разработке на Go.
1. Производительность
Go — быстрый. Очень и очень быстрый. Его производительность близка к Java [сегодня используется на финансовом рынке для высокочастотного трейдинга — торговли на большой скорости — прим. ред.] или C++. В нашем случае Go чаще всего оказывался в 40 раз быстрее Python. Вот небольшое сравнение производительности этих языков.
2. Производительность языка имеет значение
Для многих приложений язык программирования — это просто связующее звено между программой и базой данных. И, как правило, производительность самого языка особо не важна. Но Stream — это поставщик API, который обеспечивает работу платформы с каналами и чатами более 700 компаний и более 500 миллионов конечных пользователей. Годами мы пытались оптимизировать Cassandra, PostgreSQL, Redis и т. д., но однажды наступает момент, когда пределы возможностей языка достигнуты.
Python — отличный язык, но для таких задач, как сериализация/десериализация, ранжирование и агрегирование, он довольно медлительный. Мы регулярно сталкивались с проблемами производительности, когда Cassandra получала данные за 1 мс, а следующие 10 мс Python тратил на превращение их в объекты.
3. Продуктивность разработчика и мало возможностей для креатива
Взгляните на этот небольшой кусок кода Go из статьи How I Start Go. Это отличная обучающая статьи и хорошая отправная точка для изучения Go.
Если вы новичок в Go, то в этом небольшом фрагменте кода вас мало что удивит. В нём показаны несколько присвоений, структуры данных, указатели, форматирование и встроенная HTTP-библиотека. Когда я только начал заниматься программированием, мне нравилось использовать тонкости возможностей Python. Python позволяет импровизировать с кодом, который вы пишете. Например, вы можете:
использовать метаклассы для самостоятельной регистрации классов при инициализации кода;
менять местами True и False;
добавлять функции в список встроенных функций;
перегружать операторы через magic-методы;
использовать функции в качестве свойств через декоратор @property.
С такими штуками интересно экспериментировать, и с этим согласится большинство программистов. Но написанный другими людьми код порой сложно понять. Go вынуждает вас придерживаться основ. Поэтому читать чужой код очень просто, и вы сразу понимаете, что в нём написано.
Конечно же, это «просто» зависит от вашего конкретного случая. Если вы хотите создать базовое CRUD API, то я бы порекомендовал всё-таки Django + DRF или Rails.
4. Конкурентность и каналы
Go, как и любой язык, стремится к простоте. Для него не придумывали множество новых понятий. Целью было создать простой язык — быстрый и удобный в работе. Единственная область, где в Go появилось нечто новое, — это горутины и каналы. (Если быть на 100% точными, то понятие CSP появилось в 1977 году, так что данное новшество — скорее уж, новый подход к старой идее).
Горутины — это упрощённый подход Go к потокам, а каналы — предпочтительный способ организации связи между горутинами. Горутины очень дёшево создавать, и они занимают лишь пару КБ дополнительной памяти. Поскольку горутины так мало весят, их можно запускать сотнями или даже тысячами одновременно. Коммуникацию между горутинами реализуют через каналы.
Среда выполнения в Go справляется со всеми сложностями. Реализация конкуренции через горутины и каналы позволяет с лёгкостью использовать доступные ядра ЦП и обрабатывать конкурентный ввод-вывод — и всё это делается без усложнения разработки. Для запуска функции в горутине требуется минимальный шаблонный код (если сравнивать с Python/Java). Вы просто добавляете к вызову функции ключевое слово go
:
С подходом Go к конкурентности очень легко работать. Это интересный способ реализации, если проводить параллель с тем же Node, в котором разработчику нужно быть предельно внимательным к тому, как обрабатывается асинхронный код. Ещё один прекрасный аспект конкурентности в Go — детектор гонки. Вы всегда заметите любые состояния гонки в асинхронном коде.
Перевод твита
Тук-тук
Состояние гонки
Кто там?
Несколько полезных ресурсов для знакомства с Go и каналами вы найдёте ниже.
5. Быстрая компиляция
Сейчас наш крупнейший микросервис, написанный на Go, компилируется за 4 секунды. Быстрая компиляция Go — это его главный плюс по сравнению с такими языками, как Java и C++, которые славятся своей медленной скоростью компиляции. Мне нравится сражаться на мечах, но ещё приятнее решать задачи, пока я помню, что должен делать мой код:
Перевод
— Эй, давайте работать!
— Компиляция!
— Ох, продолжайте.
6. Возможность создать команду
Начнём с очевидного: разработчиков Go гораздо меньше, чем для более старых языков (C++ и Java). По данным StackOverflow, 38% разработчиков знают Java, 19,3% разбираются в C++ и лишь 4,6% освоили Go. В данных на GitHub прослеживается схожая тенденция: Go используется чаще таких языков, как Erlang, Scala и Elixir, но его популярность ниже, чем у Java и C++.
К счастью, Go — очень простой и лёгкий в изучении язык. В нём есть нужные вам базовые функции и ничего более. Из новшеств можно выделить оператор defer и встроенное управление конкурентностью через горутины и каналы. Для приверженцев чистоты языка: Go далеко не первый реализовал эти концепции, но именно он сделал их популярными. Благодаря простоте Go любой разработчик на Python, Elixir, C++, Scala или Java, которого вы возьмёте в свою команду, разберётся в нём буквально за месяц.
Мы заметили, что, по сравнению с другими языками программирования, гораздо проще собрать команду разработчиков на Go. И если вы нанимаете сотрудников в таких конкурентных экосистемах, как Boulder and Amsterdam, то это весомое преимущество.
7. Прочная экосистема
Для таких команд, как наша (~20 человек), экосистема важна. Вы не сможете создать ценность для своих клиентов, если придётся заново изобретать всю функциональность с нуля. В Go есть отличная поддержка используемых нами инструментов. Надёжные библиотеки уже доступны для Redis, RabbitMQ, PostgreSQL, парсинга шаблонов, задач, выражений и RocksDB.
Экосистема Go в разы лучше, чем у таких новых языков, как Rust или Elixir. Разумеется, она не так хороша, как в Java, Python или Node, но это стабильная экосистема, а для многих базовых задач уже доступны качественные пакеты.
8. Gofmt, принудительное форматирование кода
Для начала, а что такое Gofmt? И нет, это не ругательство. Gofmt — это потрясающая утилита для командной строки; она встроена в компилятор Go специально для форматирования кода. В плане функциональности она очень похожа на autopep8 для Python.
Большинство из нас вообще-то не любит спорить о табах и пробелах, что бы ни показывали в сериале «Силиконовая долина». Форматирование должно быть единообразным, а сами его стандарты не особо важны. Gofmt избавляет от всех этих дискуссий, предлагая один официальный способ по форматированию кода.
9. gRPC и буферы протокола
Go предлагает первоклассную поддержку буферов протокола и gRPC. Оба эти инструмента прекрасно работают вместе для создания микросервисов, которые должны взаимодействовать через RPC. От вас всего лишь требуется написать манифест, в котором вы определяете, какие RPC-вызовы нужно делать и какие аргументы они принимают.
Затем из этого манифеста автоматически генерируются серверный и клиентский коды. Итоговый код получается быстрым, оставляет совсем небольшой сетевой отпечаток и крайне прост в работе. Из того же манифеста вы можете сгенерировать клиентский код для многих других языков, включая C++, Java, Python и Ruby. Так что оставьте в прошлом неоднозначные конечные точки REST для внутреннего трафика, когда вам каждый раз приходится прописывать почти один и тот же клиентский и серверный код.
1. Нехватка фреймворков
В Go нет какого-то одного главного фреймворка, как, например, Rails для Ruby, Django для Python или Laravel для PHP. Это предмет самых горячих споров в сообществе Go, поскольку многие считают, что вам вообще не нужны никакие фреймворки. В некоторых случаях я полностью с ними согласен. Но если кто-то захочет создать простой CRUD API, то гораздо удобнее сделать это на Django/DJRF, Rails Laravel или Phoenix.
Дополнение: в комментариях пишут, что есть несколько проектов, которые предоставляют фреймворк для Go. Основными фаворитами называют Revel, Iris, Echo, Macaron и Buffalo. Мы предпочли не использовать фреймворки в Stream. Но для многих новых проектов, которые нацелены на предоставление простого CRUD API, отсутствие основного фреймворка является серьёзным недочётом.
2. Обработка ошибок
Обработка ошибок в Go сводится к тому, что он просто возвращает ошибку из функции и ожидает, что ваш клиентский код сам её обработает (или вернёт ошибку на стек вызывающей программы). Такой подход вполне работоспособен, но можно запросто упустить из виду, когда что-то пошло не так, из-за чего не получится выдать пользователям информативную ошибку. Эту проблему решает пакет errors, позволяющий вам добавлять в ошибки контекст и трассировку стека.
Ещё одна проблема — можно случайно забыть обработать ошибку. Тут пригодятся инструменты статического анализа (errcheck и megacheck). Несмотря на работоспособность этих обходных решений, всё это кажется не совсем правильным. Вы ждёте, что язык будет поддерживать надлежащую обработку ошибок.
3. Управление пакетами
С момента написания этой статьи Go прошёл долгий путь в управлении пакетами. Эффективными решениями являются модули Go; их единственная проблема заключается в том, что они нарушают работу таких инструментов для статического анализа, как errcheck. Вот обучающая статья по Go и использованию модулей Go.
Управление пакетами в Go нельзя назвать идеальным. Там по умолчанию отсутствует возможность задавать конкретную версию зависимости и создавать воспроизводимые сборки. Системы управления пакетами в Python, Node и Ruby гораздо лучше. Но с правильными инструментами управление пакетами в Go работает вполне прилично.
Для управления зависимостями вы можете использовать Dep — он позволяет указывать и закреплять версии. Помимо этого, мы используем инструмент с открытым кодом под названием VirtualGo, который упрощает работу с несколькими проектами, написанными на Go.
Python или Go
Обновление: с момента написания этой статьи разница в производительности Python и Go возросла. (Go стал быстрее, а Python остался тем же). Мы провели интересный эксперимент: взяли наш функционал ранжирования каналов на Python и переписали его в Go. Взгляните на этот пример метода ранжирования:
Для его поддержки код на Python и на Go должен делать следующее:
Разбирать выражение для оценки. В данном случае мы хотим превратить эту строку
"simple_gauss(time)*popularity"
в функцию, которая берёт активность в качестве входного значения и возвращает оценку на выходе.Создавать частично определённые функции на основе конфигурации JSON. Например, мы хотим, чтобы
"simple_gauss"
вызывала"decay_gauss"
со шкалой в 5 дней, смещением в 1 день и коэффициентом убывания в 0,3.Разобрать конфигурацию
"defaults"
, чтобы у вас был резервный вариант на случай, если какое-то поле в активности не будет определено.Воспользоваться функцией из шага № 1, чтобы присвоить баллы всем активностям в канале.
Разработка Python-версии кода ранжирования заняла примерно 3 дня. Сюда вошли написание кода, модульные тесты и документация. Затем около 2 недель ушло на оптимизацию кода. Одна из оптимизаций переводила выражение оценки (simple_gauss(time)*popularity)
в абстрактное синтаксическое дерево.
Кроме того, мы реализовали логику кеширования, которая предварительно вычисляла оценку для определённого времени в будущем. А разработка Go-версии кода, наоборот, заняла не более 4 дней. Дальнейшей оптимизации для работы не потребовалось. Первая часть разработки шла быстрее на Python, но в итоге версия для Go оказалась менее трудоёмкой.
Дополнительным плюсом было и то, что код Go работал примерно в 40 раз быстрее, чем наш самый хорошо оптимизированный код на Python. Это лишь один из примеров роста производительности, с которой мы столкнулись, перейдя на Go. Но, конечно же, такое сравнение — из серии «сопоставлять несопоставимое»:
код ранжирования был моим первым проектом на Go;
код Go писался после кода на Python, так что к тому времени я лучше понимал сценарий использования;
библиотека Go для разбора выражений была исключительно высокого качества.
Ваши доводы будут отличаться. На создание некоторых других компонентов нашей системы в Go уходило гораздо больше времени, чем в Python. В итоге мы заметили общую тенденцию: разрабатывать код на Go чуточку сложнее, но времени на его оптимизацию тратится в разы меньше.
Elixir или Go — заслуженное серебро
Ещё один опробованный нами язык — это Elixir. Он построен поверх виртуальной машины Erlang. Это удивительный язык; мы решили к нему присмотреться, поскольку у одного члена нашей команды имелся солидный опыт работы с Erlang. В своих примерах мы заметили, что исходная производительность Go выше.
Go и Elixir отлично справляются с тысячами параллельных запросов. Однако если присмотреться к производительности отдельного запроса, то для нас Go оказался в разы быстрее.
Ещё одна причина, почему мы выбрали Go (а не Elixir), связана с экосистемой. Go предлагал для нужных нам компонентов больше готовых библиотек, а библиотеки Elixir чаще всего не были готовы к применению.
Кроме того, труднее найти/обучить разработчиков на Elixir. Все эти причины склонили чашу весов в сторону Go. Тем не менее у Elixir есть просто потрясающий фреймворк Phoenix, который однозначно заслуживает внимания.
Заключение
Go — это высокопроизводительный язык с отличной поддержкой конкурентности. Он почти такой же быстрый, как C++ и Java. На разработку кода в Go уходит чуть больше времени, чем в Python или Ruby, но вы существенно экономите на оптимизации кода. У нас есть небольшая команда разработчиков в Stream, который поддерживает работу каналов и чатов для более 500 миллионов конечных пользователей.
Благодаря сочетанию отличной экосистемы, быстрого обучения новых разработчиков, высокой производительности, стабильной поддержке конкурентности и эффективной среде разработки Go стал для нас отличным выбором.
Stream всё ещё пользуется Python для дашбордов, сайта и машинного обучения для персонализированных каналов. В ближайшее время мы не планируем попрощаться с Python, но в дальнейшем весь производительный код будет написан на Go.
Наш новый Chat API также полностью написан на Go. Если вы хотите узнать о языке больше, почитайте статьи из списка ниже. А если интересно познакомиться со Stream, начните с этого интерактивного урока.
Полезные ссылки
А мы поможем прокачать ваши навыки или с самого начала освоить профессию, востребованную в любое время:
Выбрать другую востребованную профессию.