Алексей Найдёнов. ITooLabs. Кейс разработки на Go (Golang) телефонной платформы. Часть 1
Алексей Найдёнов, CEO ITooLabs, рассказывает про разработку телекоммуникационной платформы для операторов связи на языке программирования Go (Golang). Алексей также делится опытом развертывания и эксплуатации платформы в одном из крупнейших азиатских операторов связи, который использовал платформу для оказания услуг голосовой почты (VoiceMail) и Виртуальной АТС (Cloud PBX).
Алексей Найдёнов (далее — АН): — Всем привет! Меня зовут Алексей Найдёнов. Я — директор компании ITooLabs. Перво-наперво я хотел бы ответить, что здесь делаю и каким образом здесь оказался.
Если вы посмотрите Bitrix24 Marketplace (раздел «Телефония»), то 14 приложений и 36, которые там есть (40%) — это мы:
Точнее сказать, это наши партнёры-операторы, но за всем этим стоит наша платформа (Platform as a Service) — то, что мы им продаём за небольшую копеечку. Собственно, о развитии этой платформы и о том, как мы пришли в Go, я бы и хотел рассказать.
Цифры по нашей платформе сейчас:
44 партнёра-оператора, включая «Мегафон». Мы, вообще говоря, очень любим пускаться в разные авантюры, и у нас есть фактический доступ к 100 миллионам абонентов 44 операторов здесь, в России. Поэтому, если у кого-то будут появляться какие-то бизнес-идеи, мы всегда их с радостью выслушаем.
- 5000 компаний-пользователей.
- 20 000 абонентов в сумме. Это всё b2b — мы работаем только с компаниями.
- 300 вызовов в минуту днём.
- 100 миллионов минут вызовов за прошлый год (мы это отпраздновали). Это без учёта внутренних переговоров, которые есть на нашей платформе.
Как это начиналось?
Как вообще правильные чуваки начинают делать свою платформу? Надо ещё учесть, что у нас в анамнезе была «хардкор-интерпрайз» разработка, да ещё и в самое точное для enterprise время года! Это было то счастливое время, когда приходишь к заказчику и говоришь: «Нам нужно ещё пару серверов». А заказчик: «Да не вопрос! У нас десятка в стойке стоит».
Поэтому мы занимались Oracle, Java, WebSphere, Db2 и всё такое. Поэтому мы взяли, конечно, лучшие вендорские решения, интегрировали их и попробовали с этим взлететь. Гуляли на свои. Это бы такой внутренний стартап.
Всё это вообще началось в 2009 году. С 2006-го мы плотно занимаемся операторскими решениями, так или иначе. Сделали несколько заказных виртуальных АТС (вроде того, что у нас сейчас имеется на заказ): посмотрели, решили, что это хорошо, и решили замутить внутренний стартап.
Взяли VMWare. Поскольку гуляли на свои, то пришлось сразу отказаться от крутого вендорского Storage. Мы всё про них знаем: что обещания нужно делить на 3, а стоимость умножать на 10. Поэтому делали DirDB и так далее.
Потом оно начало расти. К этому добавился сервис биллинга, потому что платформа перестала справляться. Затем серер биллинга с MySQL ушёл на Mongo. В итоге получилось работающее решение, которые перерабатывает все вызовы, которые туда идут:
Но где-то там, внутри, крутится тот самый вендорский продукт — главный, ядерный, который мы когда-то взяли. Примерно к концу 2011 года мы для себя поняли, что главным бутылочным горлышком для нас, конечно, будет являться именно этот продукт — мы в него упрёмся. Мы видели перед собой стену, в которую бежали на полном скаку, поскольку клиенты шли, добавлялись.
Соответственно, нам нужно было что-то делать. Конечно, мы провели довольно долгие исследования по поводу разных продуктов — и open source, и вендорских. Я не буду сейчас на этом останавливаться — не о том речь. Самый последний запасной вариант, о котором мы думали — это делать свою собственную платформу.
В конечном итоге мы пришли именно к этому варианту. Почему? Потому что все вендорские и open source продукты делались для решения проблем 10-летней давности. Хорошо, если 10-летней, а некоторые и больше! Для нас стал очевиден выбор: либо мы прощаемся с нашей прекрасной идеей об идеальной услуге (для партнёров, операторов и себя), либо мы делаем что-то своё.
Мы решили делать что-то своё!
Требования к платформе
Если долго чем-то занимаешься (эксплуатируешь чужой продукт), то в голове потихоньку складываются мысль:, а как бы я это сделал сам? Поскольку мы в компании все программисты (кроме продавцов, не программистов нет), то требования у нас сложились давно, и они были понятны:
- Высокая скорость разработки. Вендорский продукт, который нас мучил, не устраивал в первую очередь тем, что всё получалось долго и медленно. Мы хотели быстро — у нас было много идей! У нас и сейчас много идей, но тогда список идей был такой, что казалось, будто вперёд лет на десять. Сейчас только на год.
- Максимальная утилизация многоядерного железа. Это тоже для нас было важно, поскольку мы видели, что ядер будет становиться только больше и больше.
- Высокая надёжность. То, с чем мы тоже наплакались.
- Высокая устойчивость к сбоям.
- Мы хотели прийти в конечном итоге к процессу с ежедневными релизами. Для этого нам нужен был выбор языка.
Соответственно, из требований к продукту, которые мы для себя предъявили, вырастают явно логичным образом требования к языку.
- Если мы хотим поддержки мультиядерных систем, то нам нужна поддержка параллельного выполнения.
- Если нам нужна скорость разработки — нам нужен язык с поддержкой конкурентной разработки, конкурентного программирования. Если кто не сталкивался с разницей, то она очень простая:
- параллельное программирование — это о том, как два разных потока выполняются на разных ядрах;
- конкурентное выполнение, точнее поддержка конкурентности — это о том, как язык (или runtime, неважно) помогает скрыть всю сложность, которая вытекает из параллельного выполнения.
- Высокая устойчивость. Очевидно, что нам нужен был кластер, причём лучше, чем у нас был на вендорском продукте.
Вариантов у нас было не так много на самом деле, если вспомнить. Во-первых, «Эрланг» — мы его любим и знаем, это был мой личный, персональный фаворит. Во-вторых, Java — даже не Java, а конкретно Scala. В-третьих, язык, который на тот момент времени мы вообще не знали — Go. Он тогда только-только появился, точнее, примерно два года уже существовал, но ещё не вышел в релиз.
Победил Go!
История Go
Мы сделали платформу на нём. Попробую объяснить, почему.
Краткая история Go. Стартовал в 2007-м, открыт в 2009-м, первая версия вышла в 2012-м (то есть мы начали работать ещё до первого релиза). Инициатором выступал Google, который хотел заменить у себя, как я подозреваю, Java.
Авторы — весьма имениты:
- Кен Томсон, который стоял за Unix«ом, придумал UTF-8, работал над системой Plan 9;
- Роб Пайк, который вместе с Кеном придумывал UTF-8, тоже работал над Plan 9, над Inferno, Limbo в Bell Labs;
- Роберт Гизмер, которого мы знаем и любим за то, что он придумал Java HotSpot Compiler, и за то, что он работал над генератором в V8 (интерпретаторе Javascript«а от Google);
- И более 700 участников, включая некоторые наши патчи.
Go: первый взгляд
Мы видим, что язык более-менее простой, понятный. У нас есть очевидные типы: в некоторых случаях их нужно объявлять, в некоторых — не нужно (это значит, что типы выводятся, так или иначе).
Видно, что модно описывать структуры. Видно, что у нас есть понятие указателя (там, где звёздочка). Видно, что есть специальная поддержка для объявления инициализации массивов и ассоциативных массивов.
Примерно понятно — жить можно. Пробуем написать Hello, world:
Что видим? Это Си-подобный синтаксис, точка с запятой необязательна. Она может быть разделителем для двух строк, но только в том случае, если это две конструкции, которые именно на одной строке.
Видим, что скобки в управляющих структурах (в 14-й строке) необязательны, а вот фигурные обязательны всегда. Видим, что типизация статическая. Тим в большинстве случаев выводится. Этот пример чуть сложнее обычного Hello, world — просто для того, чтобы показать, что есть библиотека.
Что ещё видим важного? Код организован в пакеты. И для того чтобы пакет использовать в своём собственном коде, необходимо его импортировать при помощи директивы import — это тоже важно. Запускаем — работает. Отлично!
Пробуем дальше что-нибудь посложнее: Hello, world, но только теперь это http-сервер. Что видим интересного здесь?
Во-первых, функция выступает параметром. Это означает, что функция у нас — «первоклассный гражданин» и с ним можно делать много всего интересного в функциональном стиле. Видим далее неожиданное: директива import ссылается непосредственно на репозиторий GitHub. Всё верно, так и есть — более того, так и нужно делать.
В Go универсальным идентификатором пакета является url его репозитория. Есть специальная утилита Goget, которая сходит за всеми зависимостями, скачает их, установит, скомпилирует и подготовит к использованию, если это необходимо. При этом Goget знает про html-meta. Соответственно, можно держать http-каталог, в котором будут ссылки на конкретный свой репозиторий (как мы, например, делаем).
Что мы ещё видим? Http и Json в штатной библиотеке. Есть, очевидно, интроспекция — reflection, которая должна использоваться в encoding/json, потому что мы ему подставляем просто какой-то произвольный объект.
Запускаем и видим, что у нас в 20 строк уложился полезный код, который компилируется, запускается и отдаёт текущую среднюю загрузку машины (на машине, на которой он запущен).
Что ещё важно из того, что мы можем здесь сразу увидеть? Оно компилируется в один статический бинарник (buinary). У этого бинарника вообще нет никаких зависимостей, никаких библиотек! Его можно скопировать на любую систему, сразу запустить, и оно будет работать.
Двигаемся дальше.
Go: методы и интерфейсы
У Go есть методы. Вы можете объявить метод для любого пользовательского типа. Притом это необязательно структура, а может быть alias какого-то типа. Вы можете объявить alias для N32 и писать для него методы, чтобы делать что-нибудь полезное.
И вот здесь мы в первый раз впадаем в ступор… Выясняется, что у Go нет классов как таковых. Те, кто знает Go, может сказать, что там есть включение типов, но это совсем другое. Чем раньше разработчик перестанет думать об этом как о наследовании, тем лучше. В Go классов нет, и наследования тоже нет.
Вопрос! Что же компания авторов под руководством Google дала нам для того, чтобы отображать всю сложность мира? Нам дали интерфейсы!
Интерфейс — это такой специальный тип, который позволяет написать просто методы, сигнатуры методов. Дальше любой тип, для которого эти методы существуют (выполняются), будет соответствовать этому интерфейсу. Это значит, что вы можете просто описать соответствующую функцию для одного типа, для другого (который соответствует тому типу интерфейса). Дальше — объявить переменную типа этого интерфейса и присвоить ей любой из этих объектов.
Для любителей хардкора могу сказать, что в этой переменной будет на самом деле два указателя: один — на данные, другой — на специальную таблицу дескрипторов, которая характерна именно для этого, конкретного типа, для интерфейса этого типа. То есть компилятор такие таблицы дескрипторов делает на момент линковки.
И есть в Go, конечно, указатели на void. Слово interface {} (с двумя фигурными скобками) — это переменная, которая позволяет указывать вообще на любой объект в принципе.
Пока что всё в порядке, всё привычно. Ничего удивительного.
Go: goroutines
Теперь подходим к тому, что заинтересовало: легковесные процессы — goroutines (горутины) в терминологии Go.
- Во-первых, они действительно легковесные (меньше 2 Кб).
- Во-вторых, затраты на создание такой горутины ничтожны: их можно создавать тысячу в секунду — ничего не будет.
- Обслуживаются они своим собственным планировщиком, который просто передаёт управление от одной горутины в другую.
- При этом управление передаётся в следующих случаях:
- если встречается выражение go (если горутина запускает следующую горутину);
- если включается блокирующий вызов Input/Out;
- если запускается сборка мусора;
- если запускается какая-то операция с каналами.
То есть всякий раз, когда программа на Go запускается на компьютере, она определяет количество ядер в системе, запускает столько потоков, сколько нужно (сколько ядер в системе или сколько вы ей сказали). Соответственно, планировщик будет запускать эти легковесные потоки выполнения во всех этих потоках операционной системы в каждом ядре.
Надо отметить, что это максимально эффективный способ утилизации железа. Кроме показанного мы делаем ещё много чего. Мы делаем, например, системы DPI, которые позволяют в один unit обслуживать 40 гигабит (смотря что происходит в этих строках).
Там мы ещё до Go использовали ровно ту же схему именно по этой причине: потому что это позволяет сохранять локальность кэша процессора, значительно снизить количество переключений контекста ОС (что тоже занимает очень много времени). Повторюсь: это — максимально эффективный способ утилизировать железа.
Этот простой пример в 21 строку — пример, который делает просто echo-server. При этом обратите внимание, что функция serve — предельно простая, она линейная. Там нет никаких колбэков, никакой нужды заморачиваться и думать… Вы просто читаете и пишете!
При этом, если вы читаете и пишете, оно на самом деле должно заблокироваться — это горутина просто кладётся в очередь и достаётся планировщиком тогда, когда снова станет возможно выполнение. То есть этот простой код может работать эхо-сервером на столько соединений, сколько позволит ОС на этой машине.
Продолжение будет совсем скоро…
Немного рекламы :)
Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас, оформив заказ или порекомендовав знакомым, облачные 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 евро за копейки?