[Перевод] Использование Golang для создания микросервисов в The Economist: ретроспектива

Всем привет! Уже 28 мая мы запускаем первую группу по крусу «Разработчик Golang». И сегодня делимся с вами первой публикацией приуроченной к запуску этого курсу. Поехали.

3rwajyps3se5vmahtiv_ioninq0.png

Ключевые выдержки

  • The Economist требовалось больше гибкости для распространения контента на все более разнообразные цифровые каналы. Для достижения этой цели и поддержания высокого уровня производительности и надежности, платформа перешла от монолитной к микросервисной архитектуре.
  • Средства, написанные на Go, были ключевым компонентом новой системы, которая позволила The Economist предоставлять масштабируемые, высокопроизводительные сервисы и быстро создавать новые продукты.
  • Go, нацеленный на параллелизм и поддержку API, вместе с его конструкцией статического компилируемого языка, облегчал разработку распределенных систем обработки событий которые могли бы масштабироваться. Поддержка тестирования также была плюсом.
  • В целом, опыт работы команды The Economist с Go был положительным, и это был один из решающих факторов, которые позволили масштабировать Content Platform.
  • Go не всегда будет являться подходящим инструментом, и это нормально. The Economist обладает платформой-полиглотом и использует разные языки там, где это имеет смысл.

Я присоединился к команде разработчиков The Economist как Drupal разработчик. Однако моя настоящая задача заключалась в том, чтобы участвовать в проекте, который коренным образом изменил бы технологию доставки контента Economist. Первые несколько месяцев я потратил на изучение Go, несколько месяцев работал с внешним консультантом над созданием MVP (minimum viable product — минимально жизнеспособный продукт), а затем снова присоединился к команде, чтобы курировать их погружение в Go.
Этот сдвиг в технологии был вызван миссией The Economist по расширению цифровой аудитории, поскольку потребление новостей отходило в сторону от печатных изданий. The Economist требовалось больше гибкости для доставки контента на все более разнообразные цифровые каналы. Для достижения этой цели и поддержания высокого уровня производительности и надежности, платформа перешла от монолитной к микросервисной архитектуре. Средства, написанные на Go, были ключевым компонентом новой системы, которая позволила The Economist предоставлять масштабируемые, высокопроизводительные сервисы и быстро создавать новые продукты.

Внедрение Go в The Economist:

  • Позволило инженерам быстро разрабатывать и внедрять новый функционал.
  • Утвердило лучшие практики для fast-failing сервисов с интеллектуальной обработкой ошибок.
  • Обеспечило надежную поддержку параллелизма и работы в сети в распределенной системе.
  • Проявило недостаток зрелости и поддержки в некоторых областях, необходимых для контента и медиа.
  • Облегчило платформу, которая могла бы масштабироваться для цифровой публикации.

Почему The Economist выбрал Go?

Чтобы ответить на этот вопрос, будет полезным осветить общую архитектуру новой платформы. Платформа, называемая Content Platform, является системой обработки событий. Она реагирует на события с разных платформ авторинга контента и запускает поток процессов, исполняемых в отдельно работающих микросервисах. Эти службы выполняют такие функции, как стандартизация данных, анализ семантических тегов, индексация в ElasticSearch и отправка контента на внешние платформы, такие как Apple News или Facebook. Платформа также имеет RESTful API, который в сочетании с GraphQL является основной точкой входа для интерфейсных клиентов и продуктов.

При разработке общей архитектуры команда исследовала, какие языки будут соответствовать потребностям платформы. Go сравнивали с Python, Ruby, Node, PHP и Java. В то время как у каждого языка есть свои сильные стороны, Go лучше всего соответствует архитектуре платформы. Go, нацеленный на параллелизм и поддержку API, вместе с его конструкцией статического компилируемого языка, облегчал разработку распределенных систем обработки событий которые могли бы масштабироваться. Кроме того, относительно простой синтаксис Go позволял легко включится в разработку и начать писать рабочий код, что сулило немедленную выгоду для команды, переживающей столь большой технологический переход. В целом было определено, что Go — это язык, наиболее подходящий для удобства использования и эффективности в распределенной облачной системе.

Три года спустя: соответствовал ли Go этим амбициозным целям?

Некоторые элементы дизайна платформы были хорошо согласованы с языком Go. Failing Fast был критически важной частью системы, поскольку он состоял из распределенных независимых сервисов. В соответствии с принципами Twelve-Factor App (»12-факторное приложение»), приложения должны были быстро запускаться и быстро отказывать (fast fail). Конструкция Go как статического, скомпилированного языка обеспечивает быстрое время запуска, а производительность компилятора постоянно улучшается и никогда не была проблемой для проектирования или развертывания. Кроме того, дизайн обработки ошибок Go позволил приложениям отказывать не только быстрее, но так же умнее.

Обработка ошибок

Особенность, которую инженеры быстро замечают в Go, заключается в том, что он имеет тип Error, нежели систему исключений. В Go все ошибки являются значениями. Тип Error предопределен и является интерфейсом. Интерфейс в Go по сути является именованной коллекцией методов, и любой другой пользовательский тип может удовлетворять интерфейсу, если он имеет те же методы. Тип Error — это интерфейс, который может описывать себя строкой.

type error interface {
    Error() string
}

Это предоставляет инженерам больший контроль и функциональность при обработке ошибок. Добавив Error-метод, который возвращает строку, в любом пользовательском модуле, вы можете создавать собственные ошибки и генерировать их, например, с помощью функции New, представленной ниже, которая поставляется из пакета Errors.

type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}

Что это значит на практике? В Go функции допускают множественные возвращаемые значения, поэтому, если ваша функция может не сработать, она, скорее всего, вернет значение ошибки. Язык поощряет вас явно проверять наличие ошибок там, где они возникают (в отличие от генерирования и перехвата исключения), поэтому в вашем коде обычно должна присутствовать проверка «if err! = Nil». Поначалу эта частая обработка ошибок может показаться монотонной. Однако, ошибка как значение позволяет вам использовать Error, чтобы упростить обработку ошибок. Например, в распределенной системе можно легко реализовать попытки повторных запросов, обернув ошибки.

Сетевые проблемы всегда будут встречаться в системе, будь то отправка данных другим внутренним службам или передача сторонним инструментам. В этом примере из пакета Net показано, как можно использовать ошибку как тип, чтобы отличить временные сетевые ошибки от постоянных. Команда Economist использовала аналогичную обертку ошибок для создания инкрементальных повторных попыток при отправке контента во внешние API.

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}

if err != nil {
    log.Fatal(err)
}

Авторы Go считают, что не все исключения являются исключительными. Инженеров направляют скорее к разумному восстановлению после ошибок, нежели к сбою приложения. Кроме того, обработка ошибок Go позволяет вам лучше контролировать ошибки, что может улучшить такие аспекты, как отладка или юзабилити ошибок. В рамках Content Platform эта конструктивная особенность Go позволила разработчикам принимать взвешенные решения в отношении ошибок, что привело к повышению надежности системы в целом.

Согласованность данных

Согласованность данных является критическим фактором в Content Platform. В The Economist контент является основой бизнеса, и цель Content Platform — обеспечить, чтобы контент мог быть опубликован единожды и доступен везде. Поэтому важно, чтобы каждый продукт и потребитель имели согласованность данных с API Content Platform. Продукты в основном используют GraphQL для запросов API, который требует статичной схемы, которая служит неким контрактом между потребителями и платформой. Контент, обрабатываемый Платформой, должен согласовываться с этой схемой. Статический язык помог реализовать это и позволил легко добиться согласованности данных.

Тестирование с Go

Еще одна особенность, способствующая согласованности, — это пакет тестирования Go. Быстрое время компиляции Go в сочетании с первоклассным тестированием как особенностью языка позволило команде внедрить эффективные методы тестирования в рабочие процессы проектирования и быстрые отказы в конвейерах сборки. Инструменты Go для тестов упрощают их настройку и запуск. Запуск «go test» запустит все тесты в текущем каталоге, а команда test имеет несколько полезных флагов. Флаг «cover» предоставляет подробный отчет о покрытии кода. «bench»-тест запускает бенчмарк-тесты, которые обозначаются путем запуска имени тестовой функции словом «Bench», а не «Test». Функция TestMain предоставляет методы для дополнительной настройки теста, такие как фиктивный сервер аутентификации.

Кроме того, Go имеет возможность создавать табличные тесты с анонимными структурами и заглушками с интерфейсами, улучшая охват тестов. Хотя тестирование не является чем-то новым с точки зрения языковых фич, Go позволяет легко создавать надежные тесты и легко встраивать их в рабочие процессы. С самого начала инженеры The Economist смогли запускать тесты как часть конвейеров сборки без специальной настройки и даже добавили Git Hooks для запуска тестов перед пушем кода в Github.

Тем не менее, проект не обошелся без усилий в достижении согласованности данных. Первой серьезной проблемой для платформы было управление динамическим контентом из непредсказуемых бэкэндов. Платформа потребляет контент из исходных систем CMS в основном через JSON-эндпоинты, где структура и типы данных не гарантированы. Это означало, что платформа не может использовать стандартный пакет Go для интерпретации json, который поддерживает десериализацию JSON в структуры, но бьет тревогу, если типы полей struct и input data не совпадают.

Чтобы преодолеть эту проблему, потребовался специальный метод для сопоставления серверной части со стандартным форматом. После нескольких итераций избранного подхода команда внедрила собственный процесс десериализации. Хотя такой подход немного напоминал переработку стандартного библиотечного пакета, он предоставил инженерам полный контроль над обработкой исходных данных.

Сетевая поддержка

Масштабируемость была во главе угла новой платформы, и это обеспечивалось стандартными библиотеками Go для работы в сети и с API. В Go вы можете быстро реализовать масштабируемые HTTP-эндпоинты без необходимости в фреймворках. В приведенном ниже примере пакет стандартной библиотеки net/http используется для настройки обработчика, который принимает средство записи запросов и ответов. Когда API Content Platform была впервые реализована, она использовал API-фреймворк. В конечном итоге он был заменен стандартной библиотекой, так как команда признала, что она отвечает всем их сетевым потребностям без дополнительных ненужный компромиссов. HTTP-обработчики Golang масштабируются, потому что каждый запрос к обработчику выполняется параллельно в Goroutine, легковесном потоке без необходимости кастомизаций.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Модель параллелизма

Модель параллелизма Go обеспечила множественную выгоду в повышении производительности на всей платформе. Работа с распределенными данными подразумевает возню с гарантиями, обещанными потребителям. Согласно теореме CAP, невозможно одновременно предоставить более двух из следующих трех гарантий: Согласованность данных. Доступность. Устойчивость к разделению. В платформе The Economist была принята согласованность в конечном счёте, что означает, что чтение из источников данных в конечном итоге будет согласованным, и допустимы умеренные задержки во всех источниках данных, достигающих согласованного состояния. Одним из способов минимизации этого разрыва является использование Goroutines.

Goroutines — это легкие потоки, управляемые средой выполнения Go для предотвращения их (потоков) исчерпания. Goroutines позволили оптимизировать на платформе асинхронные задачи. Например, одним из хранилищ данных Платформы является Elasticsearch. Когда содержимое обновляется в системе, содержимое, ссылающееся на этот элемент в Elasticsearch, обновляется и переиндексируется. Благодаря внедрению Goroutines время обработки было сокращено, что обеспечило быструю согласованность элементов. В этом примере показано, как элементы, подходящие для повторной обработки, повторно обрабатываются в Goroutine.

func reprocess(searchResult *http.Response) (int, error) {
	responses := make([]response, len(searchResult.Hits))	
	var wg sync.WaitGroup
	wg.Add(len(responses))
	
	for i, hit := range searchResult.Hits {
		wg.Add(1)
		go func(i int, item elastic.SearchHit) {
			defer wg.Done()
			code, err := reprocessItem(item)
			responses[i].code = code
			responses[i].err = err
		}(i, *hit)
	}
	wg.Wait

	return http.StatusOK, nil
}

Проектирование систем — это больше, чем просто программирование. Инженеры должны понимать, какие инструменты где и когда уместны. В то время как Go был мощным инструментом для большинства потребностей The Economist«s Content Platform, некоторые ограничения требовали других решений.

Управление зависимостями

Когда Go был только выпущен, у него не было системы управления зависимостями. В рамках сообщества было разработано несколько инструментов для удовлетворения этой потребности. The Economist использовал сабмодули Git, что имело смысл в то время, когда сообщество активно продвигало стандартный инструмент управления зависимостями. На сегодняшний день, хотя сообщество уже намного ближе к согласованному подходу к управлению зависимостями, его еще нет. В The Economist подход с использованием сабмодулей не создавал серьезных проблем, но он был сложноват для других разработчиков Go, а это следует учитывать при переходе на Go.

Существовали также требования к платформе, для которых функции или дизайн Go не были лучшим решением. Поскольку Платформа добавила поддержку обработки аудио, инструменты Go для извлечения метаданных в то время были ограничены, и поэтому команда выбрала вместо этого Exiftool Python. Службы платформы работают в докер-контейнерах, что позволило установить Exiftool и запустить его из приложения Go.

func runExif(args []string) ([]byte, error) {
	cmdOut, err := exec.Command("exiftool", args...).Output()
	if err != nil {
		return nil, err
	}
	return cmdOut, nil
}

Другим распространенным сценарием для платформы является прием нерабочего HTML-кода из исходных систем CMS, анализ HTML-кода на корректность и санация HTML-кода. Изначально для этого процесса использовался Go, но поскольку стандартная HTML-библиотека Go требует корректного HTML, перед обработкой требовалось большое количество кастомного кода для анализа HTML. Этот код быстро стал хрупким и пропускал пограничные случаи, поэтому было реализовано новое решение в Javascript. Javascript обеспечил большую гибкость и адаптивность для управления процессом проверки и санации HTML.

Javascript также был распространенным выбором для фильтрации и маршрутизации событий в Платформе. События фильтруются с помощью AWS Lambdas, которые являются легковесными функциями, запускаемыми только при вызове. Одним из вариантов использования является фильтрация событий на разных полосах, таких как быстрая и медленная. Эта фильтрация выполняется на основе единственного поля метаданных в JSON-объекте оболочки обработчика событий. В реализации фильтрации использовался пакет указателей JSON Javascript для захвата элемента в JSON-объекте. Этот подход был гораздо более эффективным по сравнению с полным демонтажем JSON, который потребовался бы для Go. В то время как функциональность такого типа могла быть достигнута и с помощью Go, использование Javascript было проще для инженеров и обеспечивало более простые лямбды.

Ретроспектива Go

После внедрения Contact Platform и поддержки его в производстве, если бы я должен был провести ретроспективу Go и Content Platform, мой отзыв был бы следующим:

Что уже хорошо?

  • Ключевые элементы дизайна языка для распределенных систем.
  • Модель параллелизма, которую относительно легко реализовать.
  • Приятное написание кода и веселое сообщество.

Что можно улучшить?

  • Дальнейшее продвижение по стандартам управления версиями и вендоринга.
  • Не хватает зрелости в некоторых областях.
  • Подробности для определенных юзкейсов.

В целом, это был положительный опыт, и Go является одним из важнейших элементов, которые позволили масштабировать Content Platform. Go не всегда будет подходящим инструментом, и это нормально. The Economist обладает платформой-полиглотом и использует разные языки там, где это имеет смысл. Go, вероятно, никогда не будет лучшим выбором, когда необходимо возиться с текстовыми объектами и динамическим содержимым, поэтому Javascript все таки находится в наборе инструментов. Тем не менее, сильные стороны Go являются основой, которая позволяет системе масштабироваться и развиваться.
При рассмотрении вопроса о том, подойдет ли вам Go, задумайтесь над ключевыми вопросами проектирования системы:

  • Каковы задачи вашей системы?
  • Какие гарантии вы предоставляете своим потребителям?
  • Какая архитектура и шаблоны подходят для вашей системы?
  • Как ваша система должна масштабироваться?

Если вы разрабатываете систему, которая направлена на решение проблем распределенных данных, асинхронных рабочих процессов, а также высокой производительности и масштабирования, я рекомендую вам рассмотреть Go и его возможности ускорить достижение ваших системных целей.

Друзья, ждем ваши комментарии и приглашаем всех желающих на открытый вебинар, который 16 числа проведет старший разработчик в Yandex и, по совместительству, наш преподаватель — Дмитрий Смаль.

© Habrahabr.ru