Поддержка параллельных систем.
Одно из общепризнанных достоинств сервера Oracle - его высокая степень масштабируемости: как горизонтальной, так и вертикальной. Возможности работы сервера Oracle в распределенных системах я не предполагаю обсуждать в данной статье *, а вот обсудить особенности поддержки параллелизма в Oracle мне кажется полезным (тем более, что по не совсем понятным причинам по этому вопросе в массовой компьютерной прессе об Oracle порой дают - скажем так - не вполне правильную информацию, а из встреч со многими специалистами я вынес впечатление, что они часто имеют об этом и вовсе легендарное представление). Я впрочем не намерен вдаваться в фундаментальные аспекты параллелизма в СУБД вообще*, а попытаюсь объяснить (и в какой-то мере обосновать) подход разработчиков Oracle к данному вопросу.
Начну с констатации общеизвестного факта, что без использования параллелизма достижение высоких показателей производительности СУБД невозможно. Разработчики сервера Oracle приложили немало усилий для реализации этой истины на практике, и не случайно Oracle владеет в настоящий момент абсолютными рекордами производительности как в OLTP-тестах TPC-C (причем этот рекорд держится с апреля 1996 года), так и в DSS-тестах TPC-D (в варианте с объемом данных 300 ГБ).*
Начнем с краткой классификации параллельных систем. Наиболее широко распространены симметрично-параллельные (SMP) системы, т.е. такие, где процессоры равноправно используют все остальные системные ресурсы (прежде всего оперативную память и диски), являющиеся общими для них *. Количество процессоров в таких системах, предлагаемых на рынке, может доходить до 64. Для SMP-систем часто еще употребляют определение “система с полным разделением ресурсов” (shared-everything system). Следующий тип параллельной архитектуры - кластер: в нем узлы, имеющие свою собственную оперативную память (а возможно и собственные диски), через специальный контроллер имеют доступ к общим дискам (“система с разделяемыми дисками” - shared-disks system). Как правило каждый из узлов кластера представляет собой SMP-систему, а количество узлов в кластерах, предлагаемых на рынке, доходит до 8. Наконец, третий тип архитектуры - массивно-параллельный (MPP). В ней узлы живут практически независимой жизнью, но между ними каким-то образом реализуется очень быстрая связь. Количество узлов в такой системе вполне может достигать ста и больше. Безусловно, чтобы называться массивно-параллельной система должна в той или иной степени обеспечивать взаимодействие и совместное пользование ресурсами для своих узлов, тем не менее к системам с данной архитектурой часто применяют определение “система без разделения ресурсов” (shared-nothing system).
В свою очередь, говоря о параллелизме в СУБД, подразумевают два разных его аспекта: параллелизм при выполнении потока операций (что актуально для OLTP-приложений) и параллелизм при выполнении отдельных операций (что актуально для DSS-приложений и соответственно хранилищ данных).
Сервер Oracle в любой конфигурации поддерживает параллелизм при выполнении потока операций (он архитектурно спроектирован под это) в SMP-архитектуре, для параллельного выполнения отдельных запросов требуется установка Parallel Query Option. Для кластеров и MPP-систем Oracle предлагает архитектуру, позволяющую всем узлам этих систем
параллельно осуществлять доступ к одной БД: чтобы добиться этого достаточно установить Parallel Server Option*
Поговорим сначала о параллелизме в режиме OLTP. Для его обеспечения в SMP-системах Oracle предлагает возможность использования многопотоковых разделяемых серверных процессов. Поскольку я уже давал характеристику данной возможности в статье [2], не буду повторяться, а вместо этого предлагаю обсудить особенности поддержки OLTP на кластерах и MPP.
Для начала разберемся, что такое опция Oracle Parallel Server. Как я уже упоминал, она позволяет нескольким узлам системы (фактически всем, функционирующим в данный момент времени) параллельно работать с одной БД, находящейся на общих дисках (в MPP-системе это будут “виртуальные” общие диски, поддерживаемые ОС). Пользовательские сессии взаимодействуют каждая со своим узлом, но при этом фактически работают с одними и теми же данными* Помимо очевидной возможности использования полной мощности параллельной системы для работы с БД, Oracle Parallel Server (OPS) дает еще одно важное преимущество: он обеспечивает повышенную живучесть БД. Дело в том, что при выходе из строя одного из узлов системы один из “оставшихся в живых” автоматически выполняет восстановление транзакций сбойного узла, не переписанных из буфера в файлы БД, так что для “пострадавших” пользователей достаточно повторить операцию “соединения” с БД, чтобы продолжить работу на одном из оставшихся узлов.
Можно заметить, что в Oracle8 даже эта операция будет не обязательна: новая версия сервера (еще раз напомню, находящаяся в бета-тестировании) позволяет выполнять автоматическое переключение сессий со сбойного узла, так что например прерванные запросы попросту продолжают выполняться после небольшой задержки.
Однако было бы нечестно утверждать, что при применении OPS не возникает никаких проблем. Собственно говоря по сравнению с SMP-системами возникает одна, но достаточно неприятная, проблема: синхронизация кэшей в оперативной памяти узлов. В самом деле, каждый узел системы кэширует данные БД в своей оперативной памяти и может держать их там достаточно долгое время без переписывания на диск. Если один из узлов модифицировал некую запись БД, но не переписал ее на диск, то при обращении к той же записи другой узел не имеет права ни пользоваться ее копией в своей памяти (она уже не актуальна), ни даже считать ее с диска. Для разрешения этой проблемы вводятся т.н. блокировки параллельного кэша: при модификации данных узел параллельной системы как бы вешает на них свой “замок”, так что любой другой узел при обращении к этим данным должен сначала “снять замок”, что включает в себя передачу ему актуальных данных. Ясно, что если различные узлы будут часто модифицировать одни и те же данные, то блокировки параллельного кэша могут заметно снизить производительность сервера в целом.
К сожалению данная проблема является принципиальной, т.е. от нее нельзя полностью избавиться ни с помощью технических ухищрений, ни с помощью альтернативных решений* . К счастью проблема не столь страшна, как может показаться: во всяком случае понятно, как можно с нею бороться. Если пользователи, работающие с разными узлами, редко модифицируют одни и те же записи, то и блокировки параллельного кэша возникают редко. Такой режим легко обеспечивается, если, например, на разные узлы сервера “назначаются” пользователи, работающие с разными приложениями, или работающие с данными различных отделов (филиалов) и пр. Приложения, осуществляющие “хаотичные” обращения к большой БД также имеют слабую тенденцию к порождению блокировок параллельного кэша. Тем не менее, распределение пользователей между узлами сервера должно осуществляться не наобум, а с учетом того, с какими данными и в каком режиме они работают *. Как бы то ни было, OPS уже достаточно давно и успешно используется - особенно в инсталляциях, требующих повышенной надежности системы. Нелишне заметить, что и рекорд в тестах TPC-C поставлен с использованием OPS на кластере (Digital Alpha 8400)* . Конечно рекорд рекордом, но для пользователей важно найти систему, отвечающую масштабам их задач. Надо сказать, что до последнего времени понятия “кластер” и “параллельный сервер” ассоциировались только с весьма мощными и дорогостоящими конфигурациями аппаратуры. Отчасти это было связано с реальными потребностями рынка, а отчасти с тем фактом, что поддержка кластерного режима работы требует весьма значительных системных ресурсов. Одним из первых пожирателей ресурсов является т.н. менеджер распределенных блокировок (Distributed Locks Manager - DLM). Это программная компонента (реализованная обычно в виде набора процессов), обычно поставляемая фирмой-разработчиком ОС, задача которой - управление доступом к разделяемым ресурсам на уровне системы в целом. Именно с помощью DLM Oracle реализует блокировки параллельного кэша и вообще синхронизацию работы узлов. Универсальность DLM в сочетании с тем, что он является “внешней составляющей” OPS, приводит к тому, что общее количество блокировок параллельного кэша становится критичным ресурсом. Чтобы снизить потребность в нем, в Oracle 7.3 введен ряд усовершенствований в управлении выделением этих блокировок, но для радикального решения проблемы безусловно требовался другой подход к реализации DLM. В частности по этой причине уже в версии 7.3 Oracle постепенно переходит к реализации DLM собственными средствами в составе ядра сервера - окончательно этот процесс будет завершен с выходом Oracle8. Как бы то ни было уже в ближайшем (“весеннем”) релизе Oracle 7.3.3 ожидается поставка параллельного сервера для кластеров, функционирующих под управлением таких “легковесных” ОС, как SCO UnixWare и Windows NT (последней - как для платформы Intel, так и для DEC Alpha).
Теперь поговорим о параллелизме при выполнении отдельных операций (прежде всего запросов, ибо это наиболее важно для задач типа DSS). Как всегда, оптимизатор выбирает один из возможных алгоритмов выполнения запросов (при этом важно, что в Oracle он с самого начала при оценке стоимости того или иного решения учитывает заданную для данного запроса степень параллелизма), затем каждый шаг алгоритма разбивается на несколько параллельных потоков. Т.н. координатор выполнения запроса запускает нужное число процессов (при этом используются все наличные процессоры - включая различные узлы кластера или MPP-системы) и обеспечивает как внутриоперационный (параллельные потоки внутри шага алгоритма), так и межоперационный параллелизм. В список операций, подлежащих распараллеливанию помимо просмотра таблиц включены также все алгоритмы соединения (и т.н. “антисоединения” - конструкции типа NOT IN) таблиц, сортировки, операции агрегирования (SUM, AVG, GROUP BY и пр.), вложенные подзапросы, объединения (UNION, UNION ALL) и некоторые другие. Кроме того возможно параллельное выполнение таких операций, как создание таблицы по результатам запроса (CREATE TABLE AS SELECT), загрузка данных, сброс и восстановление БД, выполнение операций тиражирования данных. В Oracle8 к этому списку добавятся операции INSERT, UPDATE и DELETE.
Одним из наиболее фундаментальных вопросов, которые приходится решать при реализации параллельного выполнения запросов, является выбор метода распределения данных между параллельными потоками при выполнении таких операций, как полный просмотр таблиц. Самым простым (и исторически реализованным первым - фирмой Tandem) методом является “привязка” параллелизма к статическому разбиению нужных таблиц на разделы, проводимому по правилу, заданному администратором системы. Этот метод и до сих пор является краеугольным камнем параллелизма в ряде СУБД.
В принципе в самой идее разбиения таблиц на разделы безусловно есть серьезные положительные стороны, особенно когда это разбиение осуществляется на основе диапазонов значений содержательных параметров либо функций от них. Тогда, во-первых, может быть облегчена работа администратора БД в случае, когда таблица содержит большой объем данных (например, если разбить таблицу фактических продаж по месяцам, то можно выполнять сбросы только последнего раздела таблицы), во-вторых, становится возможным т.н. исключение разделов при выполнении запросов, содержащих условие на параметр разбиения (скажем, в вышеупомянутом примере, если требуется посчитать общий объем продаж с начала текущего года, то разделы, относящиеся к более ранним датам, можно не рассматривать).
Однако когда параллелизм в выполнении запросов ставится в зависимость от статичного разбиения таблиц, это приводит к ряду проблем. Дело в том, что для достижения оптимального параллелизма в этом случае требуется (по очевидным причинам), чтобы данные были распределены по разделам равномерно. В принципе этого нетрудно добиться, если, скажем, помещать каждую новую запись в новый раздел по циклическому алгоритму (round-robin). Но в этом случае, как нетрудно заметить, полностью теряются указанные выше два преимущества. И наоборот, если выполнять разбиение по содержательному критерию, то весьма часто получается, что данные распределяются по разделам неравномерно, что неизбежно приводит к тому, что закончив свою работу, параллельные процессы ждут “отстающего товарища”, которому не повезло с разделом. Если речь идет об устоявшихся (т.е. фактически не обновляемых) данных и о конкретном запросе с небольшими вариациями, то практически всегда можно найти некий компромиссный вариант разбиения, убивающий обоих зайцев, но в реальных системах типа DSS запросы как правило носят нерегламентированный характер (ad-hoc), а данные - опять-таки как правило - периодически обновляются. Все это как минимум приводит к серьезной административной работе связанной с перестройкой разделов (что становится попросту обязательным, если требуется сменить степень параллелизма), но даже это не гарантирует оптимального параллельного выполнения запросов.
Такие соображения побудили разработчиков Oracle7 отказаться от принципа “статичного” параллелизма и реализовать алгоритм т.н. динамического разбиения таблиц при параллельном выполнении запросов. Упрощенно его суть в том, что таблица логически “разбивается” непосредственно при выполнении запроса в соответствии с заданной степенью параллелизма. Это не означает впрочем, что она попросту делится на равные части произвольным образом - все гораздо изощреннее. Дело в том, что скорость обработки одного и того же объема данных в разных разделах может быть различна в зависимости как от характера запроса, так и от того, на каких физических устройствах располагается динамический раздел, да и от других порой трудно предсказуемых причин. Поэтому таблица делится реально на число разделов гораздо большее, чем степень параллелизма (и разделы эти бывают различного размера), а их назначение параллельным процессам регулируется динамически в зависимости от того, с какой скоростью они справляются с уже порученной работой.
Надо сказать, что алгоритм динамического разбиения таблиц весьма непрост, и было бы нечестно утверждать, что в нем с самого начала все было сделано самым оптимальным образом. Однако одно из самых важных преимуществ этого алгоритма в его гибкости, поэтому в него постоянно вносились усовершенствования на основании накопленного опыта эксплуатации в реальных инсталляциях, в результате чего от версии к версии Oracle7 добивался все более оптимальных характеристик параллелизма в выполнении запросов. К примеру, в версии 7.3 основные усовершенствования были связаны с поддержкой MPP-архитектур. Дело в том, что в них диски не равноценны по скорости доступа для каждого из узлов системы (к “своим” дискам доступ осуществляется быстрее, чем к “чужим”), поэтому и динамические разделы стали выделяться параллельным процессам преимущественно на локальных для соответствующих узлов дисках (преимущественно - опять-таки потому, что завершив свою “локальную” работу процесс не прекращает свою деятельность, а начинает помогать “отстающим”).
Как бы то ни было, сейчас можно с уверенностью констатировать, что метод динамического разбиения таблиц оправдал себя, позволив при минимальной дополнительной нагрузке на администратора БД добиться тем не менее практически оптимального распараллеливания выполнения запросов. Высокая масштабируемость Oracle в параллельном выполнении запросов на системах с различной архитектурой иллюстрируется также и тем фактом, что Oracle 7.3 сумел показать рекордные параметры в TPC-D тесте (в варианте с объемом данных 300 ГБ) как среди MPP-систем (на IBM SP/2), так и среди SMP-систем (на Sun Enterprise Server 10000)* .
Чтобы завершить разговор о разбиениях, хочу отметить, что Oracle7 к сожалению не включает в себя явной операции построения статических разделов таблицы (эта возможность вводится в Oracle8), но в неявном виде это тем не менее можно сделать с помощью имитации разделов отдельными таблицами, объединенными в единое представление с помощью операции UNION ALL. При выполнении запросов к такому представлению оптимизатор Oracle7 трактует его именно как таблицу, разбитую на разделы, в частности выполняет исключение разделов, если это возможно (хотя - еще раз подчеркну - параллелизм с этими разделами жестко не увязывается)* .
Содержание раздела