Блеск и нищета клиент-серверныхтехнологий

Первоисточник:http://offline.computerra.ru/1999/302/3766/

Автор: Андрей Акопянц
Опубликовано в журнале "Компьютерра" №24 от 15 июня 1999 года

В некотором смысле эта статья продолжает линию, начатую в "КТ" #275 ("Автоматизация хаоса"). Но не по тематике (корпоративная информатизация), а как попытка рефлексии некоторого пласта знаний, лежащего между философией и методологией разработки систем, с одной стороны, и технической документацией на конкретные продукты, с другой.

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

История вопроса - от 4GL до RAD

Еще десять лет назад все знали, что разработка клиент-серверных многопользовательских систем - это сложно. Разработка велась в основном на языках четвертого поколения, входящих в комплект соответствующих СУБД. За это брали много денег, и этим занимались в основном серьезные профессионалы. Нишу настольных приложений оккупировали умельцы, орудующие "народными" СУБД типа Clipper, FoxPro и Paradox, и слои практически не пересекались.

Но в начале 90-х радикально подешевели средства организации локальных сетей с разделяемыми файлами (файл-серверов), и появились "сетевые" версии настольных СУБД, позволяющие как-то обеспечить многопользовательскую работу. Они заняли пАндрей Акопянцромежуточную ценовую и квалификационную нишу между чисто настольными и клиент-серверными системами (ближе к настольным, естественно). Клиент-серверные разработки в нашей бедной (финансово) стране оказались вытеснены в область критически важных high-end-решений типа резервирования авиабилетов, учета на очень больших предприятиях, в крупных банках и др.

Но неумолимая поступь прогресса привела к тому, что в последние 3-4 года на рынке уверенно возобладали реляционные СУБД (сейчас уже многие не знают, что бывают и другие), и произошло сближение функциональности ряда лидирующих систем (Oracle, Informix, Sybase, DB2, Interbase, Progress). Цены на эти системы также заметно снизились (в разы), и клиент-серверная архитектура снова стала модной. Как всегда, важную роль в этом сыграла Microsoft, выпустив по демпинговым ценам "народный сервер" MS SQL, пользоваться которым поначалу было практически невозможно, но конкуренты вынуждены были снижать цены.

Это породило новые поколения инструментальных средств разработки, ориентированных на не слишком квалифицированного пользователя и стирающих различия в разработке настольных и клиент-серверных систем. Они имели графический пользовательский интерфейс (GUI) и получили собирательное название RAD (rapid application development) - новое модное слово, пришедшее на смену языкам 4-го поколения.

Их триумфу способствовал массовый переход на MS Windows, поменявший стандарты пользовательского интерфейса. Системы, не сумевшие быстро к ним приспособиться, либо вымерли (например любимая мною DataEase), либо перешли под Windows, но были дисквалифицированы общественным мнением за несоблюдение новых интерфейсных стандартов (Oracle Forms).

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

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

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

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

Как устроены клиент-серверные приложения и зачем они нужны

Базовая схема взаимодействия клиента и сервера в СУБД-ориентированных приложениях известна, но имеет смысл ее еще раз привести, так как именно из этой простой схемы следуют все реально имеющиеся сложности.

Происходящие при этом взаимодействия в основном выглядят так:

- клиент запрашивает у сервера данные, тот ему их возвращает;
- клиент велит серверу изменить данные, тот выполняет операцию, если может.

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

Принципиальными являются еще два понятия - транзакция и блокировка.

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

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

Есть несколько причин, определяющих преимущества клиент-серверной архитектуры перед файл-серверной:

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

Но за все приходится платить. Масса проблем возникает из-за того, что данные дублируются и на клиенте, и на сервере, причем сервер не знает, что известно клиенту, а клиент не знает, что поменялось на сервере. Держать слишком много данных на клиенте - плохо, так как они часто будут "портиться", и их придется пересчитывать, да и клиента перегружать не хочется.

Держать слишком мало - тоже плохо, чаще надо обращаться к серверу. А сервер устроен так, что ему наплевать, одну запись выдать или десять, - основное время все равно уходит на то, чтобы понять, что от него хотят.

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

Блокировать все данные, которые я собираюсь менять, - хорошо, так как никто мне не помешает, но плохо, так как я могу помешать другим.

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

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

Три иллюзии

Беглое знакомство практически с любой современной средой разработки клиент-серверных приложений может создать несколько опасных иллюзий.Иллюзия подобия настольным СУБД

Все системы, выросшие из средств разработки настольных СУБД, пытаются предоставить разработчикам "родные" метафоры настольных СУБД: таблицы, индексы, поиск и позиционирование в индексе, и т. д. Так и тянет реализовать задачу в виде таблички, где можно искать по первым буквам названия...

Но нельзя забывать, что так называемые таблицы и индексы все равно реализуются SQL-вызовами, и на запрос открытия таблицы скорее всего выполнится что-то типа Select * from table ordered by Index. А на такой запрос разные серверы реагируют по-разному.

Несколько лет назад мне рассказали трагическую историю: в "ТверьУниверсалБанке", бывшем тогда в полном расцвете сил, решили осовременить систему межбанковских расчетов, по которым "ТверьУниверсалБанк" был лидирующим банком страны. Прежняя система была написана на Clipper, работала на Nowell'овской сетке и задыхалась от огромного объема проводок, вводимых несколькими десятками операторов.

За шестизначную сумму был приобретен крутой сервер Sun и комплект средств разработки Oracle. Задача была довольно быстро переписана и запущена... После чего выяснилось, что она не тянет больше пяти пользователей, да и тех с трудом. Длительные разбирательства показали, что новая программа, воспроизводящая старую, "клипперную" идеологию, начинала с того, что открывала все нужные ей таблицы с помощью приведенных выше запросов.

Честный Oracle в свободное от обслуживания запросов время скачивал все эти громадные таблицы в память для каждого оператора отдельно (так называемая упреждающая буферизация). Чем благополучно забивал всю память. После любого изменения, произведенного оператором, он начинал "освежать" эти буферы. На что у него уходил весь процессорный ресурс. А дисковая подсистема трудолюбиво занималась подкачкой страниц виртуальной памяти, также сажая при этом процессор.

За этой проблемой полезли другие. В общем, в течение года систему запустить так и не удалось, а потом банк помер. Когда я поведал эту историю одному знакомому квалифицированному ораклисту, он чрезвычайно возбудился и начал рассказывать, что, оказывается, нужно было просто подкрутить Oracle некоторые настройки, правда, не описанные в основной документации...

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

У иллюзии подобия настольным СУБД есть и другие неприятные следствия, которые мы рассмотрим в разделе, посвященном организации пользовательского интерфейса.

Иллюзия эффективной исполнимости SQL

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

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

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

Развитые серверы содержат средства борьбы с такими ситуациями в виде так называемых планов исполнения: у сервера можно спросить, как именно он собирается исполнять запрос, и подсказать ему, как это нужно делать.

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

Иллюзия идентичности разных серверов

Почти все современные средства разработки позволяют работать с разными серверами баз данных. В значительной степени этому способствовало сближение функциональных возможностей серверов баз данных и появление стандартных программных интерфейсов для работы с ними (ODBC, IDAPI, JDBC).

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

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

А- У разных серверов разный синтаксис и функциональные возможности языков разработки хранимых процедур и триггеров.

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

В- Местами не совпадает даже синтаксис SQL - в части, например, внешних соединений таблиц (outer join).

Г- Поскольку разные серверы пользуются разными принципами блокировок и организации транзакций, то для эффективной многопользовательской работы нужны разные способы организации программы. Дальше мы об этом поговорим подробнее.

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

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

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

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