В рамках моего непрерывного обучения я недавно выбрал курс по распределенным системам в Массачусетском технологическом институте, который использует Golang в своих лабораториях. Курс отличный: все записи на уроки и вспомогательные материалы доступны онлайн и сведены в учебную программу. Мне особенно понравилась идея, что я могу узнать что-то новое о распределенных системах, отточить свои навыки работы с Golang и создать что-то классное, параллельно разрабатывая домашние задания.

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

На 1 уровень глубже

Поскольку я реализовывал Raft с нуля, мне пришлось строить непосредственно поверх самого языка Go, не прибегая к внешним библиотекам или фрагментам кода. Конечно, это означает, что я приобрел довольно близкие отношения с основными строительными блоками, такими как sync и net/rpc пакет. Часто, чтобы действительно понять, что я делаю, мне нужно было заглянуть в сам исходный код Go, что я много раз отвергал как «ненужное».

Одним из преимуществ Go является то, что это небольшой язык: новичок с базовыми знаниями его синтаксиса может продвинуться довольно далеко, прежде чем запутаться. Вдобавок ко всему, стандартные пакеты Go действительно прекрасны и изначально предоставляют многие полезные функции (сеть, модульное тестирование, ведение журналов), которые другие языки предоставляют только через сторонние пакеты. Следовательно, заглянуть в такие библиотеки, как sync, не так уж и страшно, как может показаться, потому что все построено из ограниченного набора логических элементов.

Получение знаний о том, что происходит на 1 или 2 уровня ниже моего собственного кода, дало мне новый уровень уверенности в том, что я делаю, особенно когда в документации упоминаются некоторые детали просто мимоходом, и мое воображение разыгрывается из-за возможного низкоуровневого кода. поведение определенной библиотеки. Интересно, что у меня появилась привычка исследовать и углубляться в совершенно разные языки и кодовые базы ради удовольствия от открытия: таким образом я смог устранить некоторые ошибки, которые раньше казались довольно эзотерическими.

Параллелизм в Go

90% моих выделенных строк кода написаны на Golang. Это мой основной язык, который я использую как на работе, так и для отдыха. Однако использование языка в типичном проекте обычно вращается вокруг:

  • разработка и внедрение RPC API
  • кодирование бизнес-логики
  • определение наборов модульных и интеграционных тестов
  • расширение некоторого служебного инструмента CLI

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

Реализация моей собственной версии Raft позволила мне с головой окунуться в параллелизм без ограничений. Я уже знал о горутинах и в некоторой степени использовал их в прошлом, но никогда до того уровня, когда мне приходилось слишком много думать о них. Обнаружение кандидатов на параллелизм — это не сложно, так как обычно очевидно, что сетевые операции и длительные фоновые задачи заслуживают отдельного потока. Задача состоит в том, чтобы правильно управлять одновременным доступом к данным из различных горутин и выяснить, как различные потоки могут быть переплетены во время выполнения.

Блокировка критических участков кода — это работа стандартного пакета sync. Я выбираю некоторые из самых простых, но эффективных инструментов: Mutex, Cond и встроенный тип chan.

Mutex был моим основным выбором при защите доступа к критически важным данным, таким как внутреннее состояние узла Raft. Я использовал очень грубую степень детализации при блокировке кода, снимая блокировку только перед началом операций ввода-вывода или явно приостанавливая выполнение горутины. Это работало довольно хорошо, чтобы избежать взаимоблокировок и высвобождения ресурсов для других потоков, даже если теоретически можно было бы повысить производительность за счет более точного управления доступом к данным.

Cond было новым интересным открытием: вместо того, чтобы ждать определенного количества потоков в WaitGroup (которые могут привести или не привести систему в желаемое состояние), я мог ждать, пока каждый поток сообщит об этом основному процессу, а затем немедленно продолжить если какой-либо из них вернул данные, которых я ждал. Прежде чем узнать о Cond, я достиг той же цели, вставив дополнительные проверки и логику в каждую горутину, которая функционально похожа, но, на мой взгляд, сложнее читать и понимать.

Строительство из белой бумаги

Создание непосредственно из белой бумаги определенно ощущалось по-другому. Очень приятно обращаться непосредственно к первоисточнику знаний, избегая глупых 10-минутных видеороликов на YouTube или раздутых статей, которые продолжают ходить вокруг да около, не затрагивая при этом основных концепций. Кроме того, официальный документ, такой как Raft, не только объясняет дизайн высокого уровня, но также объясняет, почему некоторые функции были разработаны определенным образом.

Знание причин вещей было очень важно для меня, так как это позволяло мне принимать правильные решения, сохраняя при этом некоторую степень свободы в принятии любого разумного (или глупого) решения, которое мне хотелось принять в любое время.

В качестве дополнительного приятного бонуса Raft был специально разработан, чтобы быть простым для понимания алгоритмом консенсуса и координации для распределенных систем. Хотя он не обязательно используется в производственной среде из-за неоптимальной производительности, он имеет дело с важными концепциями и проблемами, с которыми сталкиваются все распределенные системы, обеспечивая прочную основу, которую я позже использовал для понимания и интерпретации решений, принятых в других, более сложных алгоритмах. Это очень сложно сделать, следуя объяснению алгоритма из вторых рук: я действительно понял Raft только после прочтения расширенного технического документа И реализации его самостоятельно.

Заключение

Хотя внедрение Raft было сложным и временами граничащим с недоумением, в целом это был отличный опыт, который добавил мне много практических инструментов и улучшил мои навыки работы с Go.

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

Первоначально опубликовано на https://blog.lorisocchipinti.com 3 июля 2023 г.