Текущие исследования по созданию интерфейса программирования высокого уровня для суперкомпьютеров и кластеров направлены на то, чтобы
• изменить существующий функциональный язык для параллельного выполнения программы, написанной на этом языке, в кластере;
• создать движок, позволяющий выполнять направленный ациклический граф задач (программ или скриптов) на кластере с учетом их зависимостей.
Преимущество первого подхода заключается в том, что если у вас уже есть последовательная программа, написанная на функциональном языке, вы можете выполнить ее на кластере с использованием либо компилятора, который генерирует параллельный код, либо библиотеки, которая предоставляет те же функциональные формы (например, map, reduce), которые реализованы для параллельного выполнения на нескольких узлах кластера. Однако этот подход представляет некоторые сложности: обработка спекулятивного выполнения различных ветвей кода и устойчивость к сбоям узлов кластера. Вероятно, главный недостаток этого подхода заключается в том, что на функциональных языках написано не так много высокопроизводительных приложений: большинство из них написано на низкоуровневых императивных языках по соображениям эффективности, и этот подход не предоставляет средств для их выполнения в кластере.
Преимущество второго метода по сравнению с подходом на функциональном языке состоит в том, что он позволяет выполнять произвольные программы и сценарии в кластере и определять информационные зависимости между ними. К сожалению, большинство механизмов рабочих процессов используют XML для определения задач, их аргументов и зависимостей. Этот подход неэффективен, поскольку XML не является языком программирования, и добавление тегов, представляющих циклы, условные выражения и другие важные конструкции потока управления, приводит к созданию языка сценариев с синтаксисом языка разметки - возможно, наиболее неинтуитивно понятным и многословным способом написания программ. Несмотря на эти недостатки, существуют реализации потоков задач, которые популярны и достаточно развиты, чтобы их можно было использовать для решения реальных проблем.
Таким образом, можно сказать, что функциональный подход не является достаточно высокоуровневым, чтобы его можно было использовать для написания сценариев, выполняющих существующие программы в сложном потоке задач, а потоки задач слишком высокоуровневы для написания сценариев общего назначения. Причина отсутствия промежуточного подхода заключается в том, что планировщики пакетных заданий предоставляют интерфейс для выделения узлов кластера и запуска на них любого исполняемого файла, но интерфейс для написания параллельных программ (MPI) - это просто библиотека, которая динамически связывается с исполняемым файлом и не предоставляется планировщиком. Функциональный подход часто основан на использовании библиотеки MPI, а потоки задач основаны на интерфейсе планировщика пакетных заданий. Это создает разрыв между технологиями, который не позволяет создать универсальный и унифицированный интерфейс для выполнения вычислений в кластере, и заставляет выбрать один из двух подходов.
В то же время планировщики заданий, которые используются при анализе больших данных, такие как YARN [21], не имеют этой проблемы, поскольку они предоставляют низкоуровневый интерфейс на основе Java для запуска приложений. Различные среды программирования, такие как Apache Hadoop [1] и Apache Storm [2], построены поверх этого интерфейса, чтобы обеспечить интерфейс высокого уровня для написания определенных видов программ, таких как пакетная обработка или работа в реальном времени.
Существуют интерфейсы более высокого уровня, такие как Oozie [11]. Эта иерархическая архитектура позволяет выбрать правильный уровень абстракции для программы и представляет собой единый интерфейс для запуска приложений на кластере.
Существует множество фреймворков и языков программирования для параллельных и распределенных вычислений [17, 19, 22, 23], которые успешно применяются как в промышленности, так и в академических кругах, однако все они изолированы и самодостаточны. Основная причина отсутствия общего знаменателя между этими фреймворками и языками заключается в том, что нет протокола или низкоуровневого языка для распределенных вычислений. Для последовательных вычислений у нас есть байт-код (например, LLVM [13], байт-код Java, байт-код Guile), который используется в качестве промежуточного, переносимого и универсального представления программы, написанной на любом языке; также у нас есть ассемблер, который не является переносимым, но все же является популярным промежуточным представлением.
Почему общий низкоуровневый язык существует для последовательных вычислений, но не существует для параллельных и распределенных вычислений? Одна из причин, которая относится как к распределенным, так и к параллельным вычислениям, заключается в том, что люди все еще думают о программах как о последовательности шагов - так же, как люди сами выполняют сложные задачи. Императивные языки, на которых программы записываются в виде последовательности шагов, по- прежнему преобладают в промышленности и академических кругах; это контрастирует с непопулярными функциональными языками, на которых программы написаны как композиции функций без подразумеваемого порядка вычислений. Другая причина, которая относится к распределенным вычислениям, заключается в том, что эти вычисления по своей сути ненадежны и не существует универсального подхода для обработки выхода из строя узлов кластера. Хотя императивные языки позволяют писать более эффективные программы, они не обеспечивают защиты от взаимных блокировок и не гарантируют отказоустойчивость. Кроме того, их намного сложнее писать, так как человеку приходится работать с изменяемым состоянием (локальные и глобальные переменные, объекты и т. д.), и трудно держать это состояние в голове при написании кода. Функциональные языки минимизируют использование изменяемого состояния, обеспечивают частичную защиту от взаимных блокировок (при условии, что программист не использует блокировки вручную) и могут быть изменены для обеспечения отказоустойчивости. С точки зрения автора, люди понимают потенциал функциональных языков, но еще не осознали этот потенциал, чтобы получить все их преимущества; люди осознали весь потенциал императивных языков, но не знают, как избавиться от их недостатков.
Одна важная особенность, которой не хватает в байт-коде и ассемблере, - это способность взаимодействовать между параллельными процессами. Это взаимодействие является общим определяющим фактором, на котором могут быть построены все фреймворки и языки для параллельных и распределенных вычислений, однако универсального низкоуровневого протокола или языка, описывающего общение, не существует.
Распределенные вычислительные системы представляются как множество индивидуальных сущностей (узлов), которые соединены между собой с помощью специализированных программ: в высокопроизводительных вычислениях — это планировщики пакетных задач, в области больших данных — это планировщики задач, тесно связанные с параллельной файловой системой, в облачных вычислениях — системы управления контейнерами приложений и виртуальными машинами. Несмотря на эффективность этих программ в их предметных областях, они не являются универсальными: вместо того чтобы предоставить пользователю программный интерфейс для проведения вычислений общего назначения на распределенной системе, они либо предоставляют ограниченный потребностями конкретной предметной области интерфейс (в случае с большими данными и облачными вычислениями), либо интерфейс для параллельного запуска программы на всех узлах и коммуникации параллельных процессов между собой, тесно связанный с конфигурацией системы (в случае высокопроизводительных вычислений).
Основным сдерживающим фактором для создания универсального интерфейса, который бы позволял программировать распределенные системы без непосредственной привязки к количеству узлов, их физическим характеристикам (количеству параллельных процессов, сетевым адресам и т.п.), является свойственная им ненадежность. Действительно, при рассмотрении одного вычислительного узла, его выход из строя является критической ситуацией, которую нельзя разрешить на уровне программного обеспечения, из-за чего все программы, предназначенные для запуска на одном узле, написаны в предположении, что этот узел абсолютно надежен. В то же время, в распределенной системе достаточно большого размера выход из строя одного из узлов является нормальной ситуацией, которая не должна влиять на ее работоспособность в целом. В современных распределенных системах решены лишь частные случаи данной проблемы: в высокопроизводительных вычислениях используются контрольные точки восстановления, в больших данных надежность обеспечивается файловой системой, а в облачных вычислениях, как правило, сетевым хранилищем. Разработчики этих систем как будто бы хотят уйти от обязанности обеспечения отказоустойчивости, переложив ее на распределенную систему хранения данных, которая вычислениями не занимается. Такой подход вряд ли можно назвать эффективным: контрольные точки восстановления работают не для каждой программы и даже для подходящих программ всегда есть шанс ошибки при восстановлении, сохранение всех промежуточных результатов вычислений в параллельных файловых системах невозможно сделать быстро, а сетевые хранилища на деле являются просто узлами с большим количеством дисков, выход из строя которых может повлечь за собой нарушения функционирования всей распределенной системы.
Таким образом, проблема заключается в специализированности механизмов обеспечения отказоустойчивости распределенных систем, а также их примитивности, что является одной из основных причин отсутствия универсальных средств и интерфейсов для программирования таких систем.
Использование аргументов процедуры для определения частей параллельной программы дает новые возможности для написания параллельных программ. В императивных языках программисты переставляют и переписывают циклы, чтобы оптимизировать шаблоны доступа к памяти и помочь компилятору векторизовать код, а в случае с параллельным вычислением аргументов в функциональных языках они могут переписывать формы, чтобы помочь интерпретатору извлечь больше параллелизма. Этот параллелизм является автоматическим и не влияет на результат работы программы. С помощью ядер и кластерного планировщика эти параллельные вычисления превращаются в распределенные вычисления: ядра обеспечивают стандартный способ выражения параллельных и распределенных частей программы, автоматическую отказоустойчивость для главных и рабочих узлов и автоматическую балансировку нагрузки с помощью планировщика кластера. Вместе ядра и параллелизм на основе аргументов обеспечивают низко- и высокоуровневое программирование.
[1] Apache Software Foundation: Hadoop, https://hadoop.apache.org
[2] Apache Software Foundation: Storm, https://storm.apache.org
[3] Brown D. K. et al. JMS: an open source workflow management system and web-based cluster front-end for high performance computing //PLoS One. - 2015. - Т. 10. - №. 8. - С. e0134273.
[4] Dean J., Ghemawat S. MapReduce: simplified data processing on large clusters //Communications of the ACM. - 2008. - Т. 51. - №. 1.
[5] Deelman E. et al. Pegasus: A framework for mapping complex scientific workflows onto distributed systems //Scientific Programming. - 2005. - Т. 13. - №. 3. - С. 219-237.
[6] Galassi M. et al. Guile Reference Manual. - 2002.
[7] Gankevich I., Tipikin Y., Korkhov V. Subordination: Providing resilience to simultaneous failure of multiple cluster nodes //2017 International Conference on High Performance Computing & Simulation (HPCS). - IEEE, 2017. - С. 832-838.
[8] Gankevich I. et al. Factory: Non-stop batch jobs without checkpointing //2016 International Conference on High Performance Computing & Simulation (HPCS). - IEEE, 2016. - С. 979-984.
[9] Gankevich I., Tipikin Y., Gaiduchok V. Subordination: Cluster management without distributed consensus //2015 International Conference on High Performance Computing & Simulation (HPCS). - IEEE, 2015. - С. 639-642.
[10] Hutton G. A tutorial on the universality and expressiveness of fold //Journal of Functional Programming. - 1999. - Т. 9. - №. 4. - С. 355-372.
[11] Islam M. et al. Oozie: towards a scalable workflow management system for hadoop //Proceedings of the 1st ACM SIGMOD Workshop on Scalable Workflow Execution Engines and Technologies. - 2012. - С. 1-10.
[12] Krajca P., Vychodil V. Software transactional memory for implicitly parallel functional language //Proceedings of the 2010 ACM Symposium on Applied Computing. - 2010. - С. 2123-2130.
[13] Lattner C., Adve V. LLVM: A compilation framework for lifelong program analysis & transformation //International Symposium on Code Generation and Optimization, 2004. CGO 2004. - IEEE, 2004. - С. 75-86.
[14] Lawrance N. R. J. et al. Ocean deployment and testing of a semi- autonomous underwater vehicle //OCEANS 2016 MTS/IEEE Monterey. - IEEE, 2016. - С. 1-6.
[15] Lord T. An Anatomy of Guile: The Interface to Tcl/Tk //Tcl/Tk Workshop. - 1995. - С. 95-114.
...