Когда Go впервые появился в ноябре 2009 года, мы мало что слышали об этом, и наше первое взаимодействие произошло в 2012 году, когда Google официально выпустила версию 1 Go. Наша команда решила убедить нашего клиента использовать его для своего проекта, но это было трудно продать, и клиент отклонил нашу рекомендацию (в основном из-за недостатка знаний в своей команде поддержки).
С тех пор произошло много изменений: популяризация Docker, микросервисная архитектура, Go созревает больше как язык программирования (без каких-либо изменений в его синтаксисе). Итак, мы с братом решили еще раз взглянуть на Go, и наше путешествие началось. Мы начали читать официальную документацию, учебные пособия, посты в блогах и статьи о Go, особенно те, где авторы поделились своим опытом перехода с Java на Go или сравнения Java с Go, поскольку в тот момент мы использовали Java уже более 15 лет.
В одной из статей, с которыми мы сталкивались, сравнивались Java и Go для реализации микросервисов, и которые могли бы обслуживать больше пользователей, использующих подобное оборудование. Имея многолетний опыт работы с Java и зная его сильные и слабые стороны, а также имея пару месяцев опыта в Go, но уже зная его достоинства (скомпилированный язык, goroutines! = Threads), мы ожидали увидеть очень близкие результаты с меньшей площадью (CPU) / использование памяти) для Go.
Итак, мы были удивлены, увидев, что Go начал отставать от 2k одновременных пользователей. Поскольку мы решили потратить свое время на изучение Go, для нас было очень важно выяснить, что привело к таким результатам. В этой статье мы объясним, как мы исследовали проблему и как мы проверяли наши изменения.
Вам также может понравиться:
Учебное пособие по Голангу: Изучите Голанг на примерах .
Краткое описание оригинального эксперимента
Автор создал банковский сервис с 3 API:
-
POST / client / new / {balance} — создать нового клиента с начальным балансом.
-
POST / транзакция — переводит деньги с одного счета на другой.
-
GET / client / {id} / balance — возвращает текущий баланс для клиента.
и реализовал это с помощью Java и Go . Для постоянного хранения автор использует PostgreSQL. Чтобы протестировать сервисы, автор создал сценарий jmeter и запустил его с различными наборами одновременных пользователей от 1k до 10k.
Все это было запущено на AWS.
Джава |
Идти |
|||
Количество пользователи |
Время отклика (Сек) |
ошибки (%) |
Время отклика (Сек) |
ошибки (%) |
… |
… |
… |
… |
… |
4k |
5,96 |
2,63% |
14,20 |
6,62% |
… |
… |
… |
… |
… |
10k |
42,59 |
16,03% |
46,50 |
39,30% |
TLDR;
Основной причиной проблемы было ограниченное количество доступных соединений в Postgres (100 подключений по умолчанию) и неправильное использование объектов БД SQL. После исправления этих проблем обе службы показали схожие результаты, и единственное отличие заключалось в увеличении нагрузки на процессор и память в Java (в Java все больше).
Давайте погрузимся в …
Мы решили начать анализировать, почему частота ошибок была такой высокой в версии Go. Для этого мы добавили логирование в исходный код и сами запустили нагрузочные тесты. После анализа журнала мы заметили, что все ошибки были связаны с открытием соединений с базой данных.
Изучив код, первое, что привлекло наше внимание, было то, что для каждого вызова API создавался новый sql.DB ( https://github.com/nikitsenka/bank-go/blob/2ab1ef2ce8959dd1bc5eb5d324e39ab296efbbe5/bank/postgres. перейти # L57 ).
Идти
xxxxxxxxxx
1
func GetBalance(client_id int) int {
2
db, err := newDb()
3
...
4
}
5
func newDb() (*sql.DB, error) {
7
dbinfo := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable",
8
DB_HOST, DB_USER, DB_PASSWORD, DB_NAME)
9
db, err := sql.Open("postgres", dbinfo)
10
...
11
return db, err
12
}
Первое, что вам нужно знать, это то, что sql.DB - это не соединение с базой данных. Вот что официальная документация говорит об этом:
DB - это дескриптор базы данных, представляющий пул из нуля или более базовых соединений. Это безопасно для одновременного использования несколькими программами. Пакет sql создает и освобождает соединения автоматически; он также поддерживает свободный пул свободных соединений. Возвращенная БД безопасна для одновременного использования несколькими программами и поддерживает собственный пул свободных соединений. Таким образом, функция Open должна вызываться только один раз. Редко нужно закрывать БД.
Если приложению не удается освободить соединения обратно в пул, это может привести к тому, что db.SQL откроет много других соединений, возможно, из-за нехватки ресурсов (слишком много соединений, слишком много дескрипторов открытых файлов, отсутствие доступных сетевых портов и т. Д.). И следующий фрагмент кода может вызвать утечку соединения ( https://github.com/nikitsenka/bank-go/blob/2ab1ef2ce8959dd1bc5eb5d324e39ab296efbbe5/bank/postgres.go#L74 ):
Идти
xxxxxxxxxx
1
func GetBalance(client_id int) int {
2
...
3
err = db.QueryRow(...)
4
checkErr(err)
5
db.Close()
6
...
7
}
8
func checkErr(err error) {
9
if err != nil {
10
fmt.Println(err);
11
panic(err)
12
}
13
}
Если QueryRow завершился с ошибкой, он вызовет панику во время checkErr
вызова и db.Close
никогда не будет вызван.
Реализация Java также не использует пул соединений, но, по крайней мере, она не пропускает соединения, поскольку открытие соединений находится в блоке try ( https://github.com/nikitsenka/bank-java/blob/4708bccaff32023078fbd8e6a1e8e4c1d1d4296f/src/main /java/com/nikitsenka/bankjava/BankPostgresRepository.java#L67 ):
Джава
xxxxxxxxxx
1
public Balance getBalance(Integer clientId) {
2
Balance balance = new Balance();
3
try (Connection con = getConnection();
4
PreparedStatement ps = getBalanceStatement(con, clientId);
5
ResultSet rs = ps.executeQuery()) {
6
...
7
}
Еще одна вещь, которая привлекла наше внимание, заключается в том, что количество программ и соединений с БД в приложении не ограничивалось, но у Postgres DB есть лимит соединений. Поскольку не было упоминания о настройке Postgres DB, значение по умолчанию для ограничения соединения равно 100. Это означает, что при тестировании 10k пользователей все потоки / программы Java конкурировали за ограниченные ресурсы.
После этого мы начали анализировать сценарий тестирования JMeter ( https://github.com/nikitsenka/bank-test/blob/master/jmeter/bank-test.jmx ). Каждый прогон создает нового пользователя, затем переводит деньги от пользователя с идентификатором 1 пользователю с идентификатором 2, а затем получает баланс для пользователя с идентификатором 1. Вновь созданный пользователь игнорируется. Вставка и выбор из таблицы транзакций происходит для того же идентификатора пользователя, что также может снизить производительность, особенно если БД не стирается после каждого запуска.
Последнее, что привлекло наше внимание, это тип экземпляра, который использовался для запуска сервисов Java и Go. Экземпляры T2 имеют хорошую денежную ценность, но они не лучший выбор для тестирования производительности из-за их пакетного характера . Вы не можете гарантировать, что результат будет одинаковым при запуске.
Что дальше...
Перед запуском тестов мы решили заняться найденными проблемами. Нашей целью было не сделать идеальный / усовершенствованный код, а исправить проблемы. Мы скопировали авторский репозиторий и применили исправления.
Для реализации Go мы переместили создание sql.DB
в автозагрузку приложения и закрыли его, когда приложение закрывается. Мы также убрали панику, когда операция с БД не удалась. Вместо паники приложение возвращает код ошибки 500 с сообщением об ошибке клиенту. Мы только продолжали паниковать во время sql.DB
создания. Не было смысла запускать приложение для тестирования, если БД не запущена. Кроме того, мы позволили настроить лимит подключений с помощью переменной среды.
Для реализации Java мы добавили пул соединений. Мы выбрали пул соединений Hikary, поскольку он считается легковесным и более эффективным ( https://github.com/brettwooldridge/HikariCP#jmh-benchmarks-checkered_flag ). Мы также сделали возможным настроить ограничение количества соединений с помощью переменных среды, как мы это делали для реализации Go.
Для обеих версий мы изменили Dockerfile, чтобы использовать многоступенчатую сборку на основе изображений Alpine. Это не влияет на производительность, но делает окончательное изображение значительно меньше.
Вы можете проверить окончательный результат здесь:
Мы также изменили сценарий тестирования JMeter. Новый сценарий тестирования для каждого прогона создает двух новых пользователей с предопределенными балансами. Затем он выполняет запрос на получение баланса для каждого пользователя, чтобы проверить его баланс. После этого он переводит деньги от одного пользователя другому. В конце он выполняет еще один запрос на получение баланса для каждого пользователя, чтобы проверить правильность его баланса после передачи.
Новый Эксперимент
Чтобы протестировать модифицированные версии сервиса на производительность, мы выбрали следующие типы экземпляров из AWS:
-
Сам сервис (как Java, так и Go версия) m5.large .
-
Jmter бегун m5.xlarge .
-
PosgreSQL c5d.xlarge .
Все элементы эксперимента (сервисы версий Java и Go, JMeter и PostgreSQL) выполнялись в контейнерах Docker с использованием AWS ECS. Для контейнера PostgreSQL мы создали том, в котором хранятся все данные, чтобы предотвратить использование записи контейнера, что влияет на БД, особенно при больших нагрузках.
После каждого теста производительности мы запускали PostgreSQL с нуля, чтобы предотвратить влияние предыдущих запусков.
Мы выбрали m5.large для размещения сервисов из-за баланса вычислительных ресурсов, памяти и сетевых ресурсов. Для PostgresSQL мы выбрали c5d.2xlarge, поскольку он оптимизирован для вычислений и оснащен локальным хранилищем на уровне блоков SSD на базе NVMe.
Ниже вы можете увидеть вывод JMeter одновременных пользователей 4k для версии сервиса Go :
Вот вывод JMeter одновременных пользователей 4k для версии сервиса Java :
Вывод JMeter для запуска 10k одновременных пользователей для версии сервиса Go :
И те же 10k одновременно работающих пользователей, работающих на Java- версии сервиса:
Вы можете найти отчет о результатах JMeter для версии Go здесь и здесь для версии Java.
Go версия службы CPU / объем памяти:
Java-версия службы CPU / Memory footprint
Резюме
В этой статье мы не собираемся ничего делать или выбирать победителя. Вы должны решить для себя, хотите ли вы перейти на новый язык программирования и приложить усилия, чтобы стать экспертом в этом или нет. Существует множество статей, которые рассказывают о плюсах и минусах как Java, так и Go. В конце концов, любой язык программирования, который может решить вашу проблему, является правильным.