Алексей Найдёнов. ITooLabs. Кейс разработки на Go (Golang) телефонной платформы. Часть 2
Алексей Найдёнов, CEO ITooLabs, рассказывает про разработку телекоммуникационной платформы для операторов связи на языке программирования Go (Golang). Алексей также делится опытом развертывания и эксплуатации платформы в одном из крупнейших азиатских операторов связи, который использовал платформу для оказания услуг голосовой почты (VoiceMail) и Виртуальной АТС (Cloud PBX).
Алексей Найдёнов. ITooLabs. Кейс разработки на Go (Golang) телефонной платформы. Часть 1
Go: channels
Понятно, что когда у вас есть параллельное выполнение, вам нужны средства синхронизации. Go предоставляет все средства синхронизации, которые нужны: mutex«ы, разделяемая память… Но штатным механизмом для него являются каналы.
Канал можно рассматривать как однонаправленная типизированная очередь в n элементов (по умолчанию равно 1). Канал сам по себе может быть параметром, членом структуры. Более того, канал можно передать по каналу — при этом у этого канала будет тип «канал каналов такого-то типа».
Что делает этот пример в 20 строк? Это счётчик http-вызовов. Он просто считает там, где он запускает http-сервер, и показывает, сколько раз к этому серверу обратились. Каким образом это обычно делается? Обычно вы делаете переменную, оборачиваете mutex«ом и инкрементируете. Либо делаете атомарную переменную, инкрементируете атомарно.
В Go принято по-другому. Разработчик, попробовав в Go, очень быстро начинает писать именно так. В Go вы запускается функция goroutine, которая называется count (8-я строка) и go count (17-я); делаете переменную;, а дальше просто инкрементируете локальную переменную — начинается отправка в канал следующего числа. Те из вас, кто вспомнил Atmos Script Generators, конечно, будут правы, но в Go каналы позволяют гораздо более широкое использование.
Go: select
Есть, наконец, оператор Select, который позволяет выбирать из нескольких каналов сразу. Это типовой шаблон в Go, каким образом можно завершить выполнение какой-либо горутины:
Вы передаёте два канала. В один канал идут запросы, а в другой идут сигналы о том, что пора бы закруглиться.
Go: C API (CGO)
Для нас, как для разработчиков платформы для телефонии, было важно, что в Go есть C API, который позволяет непосредственно в коде на Go писать какие-то куски на Си, которые будут потом нормально откомпилированы, нормально влинкуются и будут потом нормально работать.
То есть вы можете получать доступ ко всем Си-библиотекам, которые есть у вас на операционной системе. При этом они тоже будут влинкованы статически.
Go: инструменты
Инструментарий в Go — ещё одна причина, по которой его любят. Всё делается при помощи одной команды — (go). Есть команда go build, которая собирает, есть go test, запускающая специальные тестовые функции, описанные в конкретном package. Есть команда go run, которая запускает и собирает сразу. Есть команда, которая обновляет зависимости (go get).
Есть те, которые форматируют исходники: go fmt — у нас она стоит, например, на push«ах. Нет больше споров по поводу того, где, каким образом и какой стиль форматирования используем — tab или пробел: есть команда go fmt, есть определённый стиль, который она форматирует, и споров никаких нет. Не нравится что-то? Запусти go fmt — будет так, как должно быть.
Команда go doc позволяет читать документации и показывать её в html. Есть go tool pprof для профайлинга, генератор парсингов (go tool yacc) и много чего другого. Короче, инструментарий в Go очень развитой для такого молодого языка. Многие идеи в нём были не то чтобы новаторские, но очень интересные, которые мигрировали потом в другие системы — например, тот же самый Goget и возможность указывать непосредственный репозиторий в качестве имени package.
Go: IDE
Вообще, я предпочитаю Emacs. Если тут есть любители — можем потом подраться. Но для разработки я предпочитаю «взрослый» IDE:
Я пробовал их все, включая VIM. Могу сказать, что на текущий момент самый крутой IDE — это обычный, какая-нибудь IDEA Edition с плагином для Go. Лучшее! Рекомендую. Если кто будет пользоваться — надо сразу начать и не мучиться.
Go: библиотеки
Библиотеки — это был ещё один очень-очень приятный сюрприз, потому что буквально на любой чих мы что-нибудь находили сразу. Даже когда нам понадобился интерпретатор Javascript, мы подумали, что придётся писать самим — мы его просто нашли, он уже был.
На текущий момент в Git«е зарегистрировано 35 с лишним тысяч разных репозиториев, в которых поставлена хотя бы одна звёздочка. Могу повторить свою мысль: там есть и наши библиотеки, и чужие, и полно компаний, которые открывают какие-то свои внутренние библиотеки. В общем, я не могу припомнить случая, когда мне что-то понадобилось, и я тут же не нашёл бы это на GitHub (в случае Go).
Go: эксплуатация
Эксплуатация — мечта, особенно для тех, кто, например, работал с Java.
- Статическая сборка. Никаких run-time зависимостей!
- Единственный бинарник. Вплоть до того, что у нас есть ещё один компонент в платформе: медиапроцессор, который мы написали на C++ просто потому, что там не столько важна скорость разработки, сколько скорость выполнения. Никаких Garbage Collector«ов! Но мы в итоге тоже стали собирать его статиком, потому что убедили, как это удобно.
- Никаких зависимостей — об этом просто не нужно думать. В остальном опыт эксплуатации любой крупной системы вам тут отлично поможет.
- Есть стектрейсы, когда оно падает. Но они нужны бывают довольно редко.
Go: сообщество
Сообщество — это тоже подкупило. Сообщество Go просто прекрасное, и оно летит восходящей звездой. К сожалению, на слайде указано мелко и не видно: слева — репозиторий на GitHub, справа — теги Stack Overflow:
Если посмотреть на Go, то он находится в компании языков, которые старше него раз так в пять!
Go vs Node.js
Битва! Сравниваем с Node.js, например. Можно на самом деле сравнить с чем угодно. Но почему с Node?
Предположу, что Google планировал, что Go будет вытеснять Java, конкурировать с Java. На самом деле нет. Основным конкурентом Go оказался для Node.js, Python, для интерпретируемых языков. Почему? Потому что на Go за счёт выведения типов, за счёт множества мелких деталей (типа команды go run) возникает ощущение языка интерпретируемого, то есть лёгкого, на котором можно делать макеты, что-то быстро строить и прототипировать. Но при этом оно реально работает: реально компилируется, запускается и работает быстро.
Вот у нас два сервера простых — и там, и там http:
Разница.
- В Go компилируется, в Node.js интерпретируется. Запустил — сразу работает (ну, и там тоже).
- В Go — много потоков (столько, сколько есть процессоров в машине). В Node.js, как вы знаете, один поток (если хотите много потоков, будьте добры запустить cluster).
- Теперь начинаем усложнять логику — усложняем её в Handler«е. В Go вы ничего не усложняете, а просто берёте и делаете так, как вы писали линейную программу, которая блокируется (послать запрос в базу данных — рассчитать — отправить ответ клиенту).
- А в Node.js сразу возникают вопросы… Что вы будете использовать сейчас — callbacks, promise, Q, async.js или новомодный yield?
Когда у вас есть такая модель, как в Erlang или Go, жизнь становится намного проще.
Go: недостатки?
Да дофига!
- Нет параметризированного полиморфизма, шаблонов, generics. Есть только специальные операции для работы с array, массивами и ассоциативными массивами. Всего остального нет.
- Нет до сих пор отладчика. Не то чтобы он был не нужен, но его нет.
- Провоцирует стиль cut«n"paste. Если вы пишите на Go достаточно долго, замечаете, что просто берёте куски кода и вставляете — это нормально. Вас раздражает, что это нормально.
- Жёстко диктует правила, включая форматирование. Я, например, очень нежно люблю Haskell, пишу на нём много домашних проектов для себя, там есть, где размахнуться фантазии. В Go — нет! У вас есть всегда ровно один, простой, понятный путь как что-либо сделать.
- Отойти в сторону — это начинает выглядеть уже таким извращением, что вы просто не можете себя заставить это сделать. Поэтому нет фантазии — приходится фокусироваться на задаче.
- Нет исключений. Есть panic/recover, но его нельзя использовать для управления.
- Очень простой синтаксис, который иногда начинает бесить (потом к нему привыкаешь).
- Нет места фантазии — слишком просто!
Почему всё же Go?
Может, из-за быстрой сборки или из-за развёртывания одним бинарником? По причине конкурентности или эффективной утилизации железа? Может, из-за того, что он быстро развивается или благодаря крутому сообществу?
Я долго пытался ответить на этот вопрос сам себе. В конце концов, я понял: нет, ни одна из этих причин! Всё гораздо проще: Go работает.
Go — это предельно практичный язык для инженеров. Для инженеров, которые заняты разработкой веб-сервисов и распределённых систем — писать на нём интерфейсы, много чего…
Можно ли на нём написать «Битрикс»? Я не знаю. Скорее всего, нет, потому что слишком много интерфейсы. Но написать на нём платформу, на которой «Битрикс» будет работать быстрее (например, шину для передачи сообщений) — это то, что нужно, я уверен.
Кто использует Go?
Кто уже успел прыгнуть в этот вагон?
- Google — понятно: у него много проектов, которые используют Go и давным-давно на него перешли.
- Docker.
- InfluxData. Если кто не пробовал — рекомендую. Тоже восходящая звезда.
- Twitter.
- Dropbox.
- Kodding.
- Даже Baidu. Go, кстати, очень популярен в Китае.
- Многие другие.
Есть список компаний, которые так или иначе отметились, делают какие-то проекты на Go: https://github.com/golang/go/wiki/GoUsers
Что мы сделали на Go? ITooLabs Centrex
Что мы написали за год?
- Динамический кластер, который позволяет равномерно распределять нагрузку между всеми его узлами (по hash ring«у).
- Использует Protobuffer для внутренней коммуникации.
- Использует SIP/HTTP/WebSocket, что угодно для внешней коммуникации.
- У него есть медиапроцессор (оставим в сторонке, он на С++).
- У него есть встроенный интерпретатор JavaScript, причём это не node (не нужно возиться с колбэками). Он работает так же, как и Go: вы пишите в прямом, простом блокирующем стиле. Изначально у нас там был Lua, но, немного помучившись, мы от него отказались — нашли интерпретатор JavaScript на Go и теперь благополучно используем его (разработчики довольны).
- Что мы ещё сделали для себя? Понятно, когда делаешь систему, которая, по-твоему мнению, должна работать на очень крутых нагрузках, первое, что нужно сделать — это нагрузочное тестирование. И мы сделали генератор-нагрузчики / тестировщик вызовов, который может на двух машинах сгенерировать под 2 тысячи вызовов в секунду.
- Система, которая у нас есть — предельно DevOps-friendly. Например, для того чтобы новое сигнальное приложение задеплоить в продакшн, нужно выполнить всего одну команду, которая мгновенно скачает с репозитория какую-то конкретную версию этого самого сигнального приложения, распространит по всем узлам кластера, заменит, и потом уже можно будет в любой момент откатиться. Можно будет для части клиентов оставить одну версию, для части — другую.
- У нас ушёл 1 год с момента принятия решения до первого развёртывания.
Про развёртывание
Как я уже говорил, где-то в 2012 году мы поняли, что нас ждёт впереди стена. Это было довольно страшно. Мы не сильно ошиблись в своих прогнозах (что ещё два года и будет кранты), потому что в начале 2014 года у нас действительно было очень плохо…
И мы быстро смигрировали значительную часть генерирующей функциональности на свою новую платформу, в частности записи, всякие внешние транки — то, что генерирует много нагрузки на CPU, и с чем старая вендорская платформа уже категорически не справлялась.
Кейс. Голосовая почта на 70 млн абонентов
Сейчас это всё уже успешно работает и практически вся запись, которая идёт на платформе, проходит через нашу систему на Go.
Если долго работаешь на репутацию, в какой-то момент репутация начинает работать на меня. И к нам уже продолжительное время приходят разные люди от операторов с предложениями делать что-нибудь.
- В конце прошлого года «чем-нибудь» оказалась система голосовой почты на 70 миллионов абонентов для индонезийского оператора Indosat. Мы подумали и сделали на базе нашей платформы. От момента принятия решения (точнее, от момента постановки железа, потому что оно долго шло) до момента, когда развернули, прошло около трёх недель.
- Система на 16 серверах, из которых, прошу заметить, только 7 серверов — собственно Application Service и SIB-сервис на Go. Всё остальное — медиашлюзы на С++. Эти 16 серверов держат 1800 вызовов секунду и 50 000 одновременных диалогов. Это 6,5 миллионов вызовов в час (любимая операторская мерялка — BHCA).
- 280 вызовов в секунду — это текущая дневная нагрузка, которая там есть сейчас. Это 1 млн BHCA на 36 млн абонентов, плюс, 2 млн абонентов добавляет каждую неделю. Или сейчас, на текущий момент — это 100 млн вызовов в секунду.
- После того как всё это запустится в конечном итоге, целевая нагрузка, будет около 600 вызовов в секунду. В качестве хранилища записи мы используем Ceph с тройной репликацией.
- Мы проверяли и тестировали: система полностью выдерживает системный сбой до двух серверов. То есть их можно просто выключить. У нас на самом деле примерно так и произошло, потому что индонезийские люди, монтирующие кабели, немного ошиблись.
- Наконец, мы нормально делаем Rolling updates.
Смогли бы мы это сделать на каком-то другом языке в такое короткое время? Напомню, с 2012 по 2015 — 3 года на продукт, который держит такую нагрузку и работает для миллионов абонентов Индонезии.
Я не знаю, но думаю, мы смогли бы сделать это на «Эрланге». Может быть, смогли бы сделать это на Java. Поскольку я, кроме основной работы, ещё и читаю лекции по системам реального времени и параллельному программированию в Тульском университете, для меня одним из конечных итогов, причин, по которым мы решили использовать Go, был очень просто эксперимент.
Я дал студентам лабораторные и убедился, что кривая обучения Go круче любого языка, с которым я когда-либо сталкивался (именно в этой сфере). Студенты начинали писать более-менее приличный код, потому что неприличный Go просто не даёт писать. Go даёт скучный код, и только скучный код! Но неприличный не получается — очень тяжело.
Могу похвастаться, что 31 декабря 2015, всего через месяц после развёртывания, на системе за один день было записано 160 тысяч сообщений, которые абоненты друг другу передают.
Вопросы
Вопрос из зала (далее — В): — Большое спасибо за доклад! Очень интересные кейсы. Скажите, пожалуйста, а как сейчас у Go выглядит обработка ошибок? Например, когда я пишу на Node и мне нужно сделать 5 подряд асинхронных операций, мой код выглядит в 5 строк: wait, wait, wait, wait, wait.
АН: — Как я уже сказал, там нет исключений. Во всяком случае в том смысле, в каких вы можете использовать их для контроля, управления.
В: — Как вы управляете ошибками?
АН: — if«ами! Я повторюсь, что Golang позволяет писать довольно скучный код и провоцирует cut«n"paste.
В: — И сколько этих if«ов будет на 5 асинхронных операций, каждая из которых может взорваться?
АН: — С моей точки зрения, они будут синхронными (с точки зрения этой горутины). 5 if«ов.
В: — Идёт практически удвоение кода.
АН: — Наверное, если бы я платил разработчикам кода, я бы расстраивался. Но поскольку я плачу за то, что они делают хороший, работающий код, я в принципе этому рад.
В: — А динамически какую-то Си-шную библиотеку прилинковать вообще возможно?
АН: — Да.
В: — То есть этот функционал остался?
АН: — То, что я показывал и рассказывал про CGO — там линкуется со статической библиотекой, если она есть, а если нет — подключается динамическая. Более того, в Go 1.5 появилась в принципе динамическая линковка, даже позволяет теперь делать динамические библиотеки. Но мы настолько привыкли, настолько прочувствовали такой механизм, что уже не чувствуем нужды. Динамические библиотеки Си подключать, конечно, можно, С++ — нет.
В: — А бинарник в итоге получается не слишком гигантским?
АН: — Нет. Бинарник всей нашей системы, которая обрабатывает все эти вызовы (вместе с интерпретатором JavaScript внутри) — это всего-навсего 6 мегабайт. Это бинарник, который в принципе влезает во второй кэш.
В: — Сам часто слышал сравнения: Go часто сравнивают именно с интерпретируемыми языками, и мало у кого есть задача написать полноценную коммуникационную платформу. Какие более простые практические кейсы есть?
АН: — Более простой кейс — отлично. Для компании «Мегафон» мы написали предбиллинг — что-то вроде шины, которая собирает внутри себя платформы от разных компонентов (биллинг, «Мультифон» — у большого оператора их всегда очень много), всё это дело внутри себя интегрирует, складывает в базу, высчитывает то, что нужно и посылает события в разные места.
В той же самой Индонезии простой шлюз (хоть и простой, но распределённый), который принимает в себя события от нашей системы и передаёт дальше в систему, которая отсылает её SSD… Вот такого рода задачи — конечно, всё это пишется очень-очень быстро, что и привлекает.
Немного рекламы :)
Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас, оформив заказ или порекомендовав знакомым, облачные VPS для разработчиков от $4.99, уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5–2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps от $19 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).
Dell R730xd в 2 раза дешевле в дата-центре Equinix Tier IV в Амстердаме? Только у нас 2 х Intel TetraDeca-Core Xeon 2x E5–2697v3 2.6GHz 14C 64GB DDR4 4×960GB SSD 1Gbps 100 ТВ от $199 в Нидерландах! Dell R420 — 2x E5–2430 2.2Ghz 6C 128GB DDR3 2×960GB SSD 1Gbps 100TB — от $99! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5–2650 v4 стоимостью 9000 евро за копейки?