Работая над SocketCluster и поддерживая его, я заметил, что многие разработчики программного обеспечения часто сталкиваются с трудностями при создании масштабируемых систем. Возможно, это связано с тем, что часто самые простые и очевидные решения не масштабируются; и это может объяснить, почему их иногда называют наивными решениями.

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

Чтобы понять параллельный код, сначала вы должны понять, что такое непараллельный (последовательный) код. Как правило, серийный код делает следующие предположения:

  • Что ему нужен доступ ко всем данным о проблеме в целом, чтобы решить эту проблему.
  • Что он должен решить всю проблему к тому времени, когда он завершит выполнение.

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

Ужасно параллельный код может масштабироваться бесконечно; серийный код не может и в конечном итоге должен быть переписан; из-за этого некоторые скажут, что последовательный код плохо спроектирован - вот почему многие разработчики занимаются функциональным программированием; такие языки, как Haskell, Erlang и Scala, побуждают вас писать параллельный код (избегая изменения состояния и изменяемых данных).

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

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

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

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

Обобщить; Ключ к параллельному мышлению - думать о своих данных как о небольших подмножествах данных фиксированного размера, с которыми можно работать независимо.

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

Мы хотим построить систему реального времени, состоящую из двух отдельных процессов - назовем их ProcessA и ProcessB. Предположим, что к нашей системе подключены два пользователя (Алиса и Боб); Алиса подключена к ProcessA, а Боб подключен к ProcessB. Что мы хотим сделать, так это позволить Алисе отправить сообщение Бобу. Итак, проблема выглядит так:

Наивным, непараллельным (последовательным) решением этой проблемы было бы, если бы ProcessA просто запросил ProcessB, подключен ли он к пользователю «Боб», и если да, ProcessA может передать сообщение ProcessB, который затем отправит его Бобу. Так:

Это «наивное» серийное решение на самом деле работает очень хорошо, когда в нашей системе только два пользователя и два процесса, но что, если бы наша общая проблема выглядела примерно так:

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

Эту проблему решает простой параллельный алгоритм pub / sub. Его можно реализовать множеством способов, но его основной принцип заключается в разделении сообщений по разным каналам и отслеживании подписчиков для каждого из этих каналов. Типичная реализация pub / sub будет включать сегментирование каналов по нескольким процессам (так, чтобы каждый процесс обрабатывал подмножество всех возможных имен каналов). Итак, чтобы отправить сообщение между двумя или более пользователями, вы просто опубликуете сообщение в соответствующий канал, передав его соответствующему процессу (вы можете хэшировать имя канала, чтобы определить, какой процесс отвечает за этот канал), а затем это процесс перешлет сообщение соответствующим подписчикам.

С помощью pub / sub вы можете настроить каналы для определенных пользователей, включив имя (или идентификатор) пользователя в имя канала (например, «alice-channel», «bob-channel»), а затем разрешив пользователям подписываться только на свои собственные. каналы. «Смущающе параллельная» идея pub / sub заключается в том, что каждый процесс будет иметь дело только с фиксированным подмножеством всех каналов и сообщений, которые проходят через систему. Чем больше процессов вы добавите, тем меньше каналов / сообщений потребуется обрабатывать каждому процессу - нет ограничений на количество каналов, которые может обрабатывать ваша система; до тех пор, пока вы можете добавлять больше процессов (и больше оборудования для работы этих процессов).

Помимо ограничений на ресурсы, единственным реальным ограничением этой реализации pub / sub является пропускная способность сообщений для каждого канала; но обычно это приемлемый предел. Поскольку сообщения обычно предназначены для людей; и каждый человек может потреблять только определенное количество сообщений в минуту - Фактически, практические ограничения pub / sub обычно превышают естественные ограничения, необходимые для хорошего взаимодействия с пользователем - Тем не менее, для приложений с высокой пропускной способностью (серверная часть / большие данные), вы всегда можете разделить каналы pub / sub на более мелкие подмножества, такие как «alice-channel-1», «alice-channel-2»,…, «alice-channel-n», без необходимости вносить какие-либо изменения в алгоритм pub / sub.

Однако в конечном итоге лучший способ понять параллельный код - это начать его писать! Я просто надеюсь, что эта статья может дать вам стимул попробовать это на себе.