Добрый день. В этой статье расскажем о том, как устроена печать в нашей платформе.
Немного истории
Для понимания, полезно будет рассказать о истории возникновения этой функции. Изначально решение что называется было «в лоб». Использовался виндовый спулер и печать осуществлялась через стандартные процедуры. Собственно, такое решение продержалось недолго, ровно до того момента, как потребовалось реализовать печать с сервера приложений, а не из клиентского приложения.
На всякий случай приведу схему из первой статьи:
Как видно, сервера приложений стояли в датацентре. И офисы были соединены в единую сеть. Как только мы реализовали печать с сервера приложений образовалось несколько проблем:
- Загрузка каналов связи выросла очень значительно. В некоторых офисах до 100%
- Сервер приложений начал зависать.
- Иногда отправленная на печать бумажка просто не распечатывалась.
- Упала производительность системы (пользователи начали жаловаться, что им приходится ждать завершения операций по нескольку минут)
- Перепутывание порядка документов при печати
Собственно расследование быстро показало, что причинами высокой загрузки каналов, как, наверное, все догадались, была передача отрендеренной печатной формы непосредственно на принтер.
С причиной номер 2 пришлось повозиться как следует, но выяснилось, что драйвера принтеров пишут тоже люди! Это они (драйвера) зависали при высокой нагрузке и тянули за собой весь сервер приложений. Вывести его из кататонического состояния можно было только его перезапуском.
Причины 3 и 5 нашлись в принт-спулере Windows.
С проблемой 4 удалось разобраться, внимательно исследуя ожидания сессий. Причины, как теперь кажется, лежат на поверхности, но тогда мы были молоды… Обнаружилось, что из-за печати с сервера приложений сильно выросло время доступа к принтеру — теперь мы печатали через интернет-каналы, а не по локальной сети. Все это время, транзакция продолжала блокировать объекты системы, на которые натыкались другие серверные вызовы и устраивали нам веселые минутки. Можно было бы конечно фиксировать транзакцию перед отпавкой на печать, но это неправильно с нашей точки зрения. Приносим извинения, но про это в другой статье про управление транзакциями.
По рассказам знакомых, я знаю что такие проблемы были не только у нас.
Тогда и решили сделать то, что на схеме обозначено как принт-сервер.
Печатные формы
Нужно сделать отступление и рассказать, что же собственно мы печатали.
Для печати в платформе реализована такая штука как «Печатные формы». Извините, более оригинального названия мы не смогли придумать. Печатная форма — это шаблон для отчетника и скрипт, который готовит табличку с данными на основе какого-то заданного разработчиком алгоритма. Да, иногда встречаются случаи, когда это нельзя сформулировать в виде одного запроса. Надо уточнить — не в виде таблицы в базе данных, а в виде объекта “типа” DataTable. В кавычках — потому, что используется собственный класс для хранения таблиц, менее требовательный к памяти и компактнее сериализуемый.
Если говорить о деталях, то используется отчетник от DevExpress XtraReports.
И скрипт и шаблон отчета доступны для редактирования в run-time через GUI главного клиентского приложения.
Скрипт, напомню, всегда выполняется на сервере приложений. Подавляющая часть бумаг, печатаемых торговой организацией — это всевозможные счета, счета-фактуры, товарные накладные, товарные чеки, листы набора на складе, и прочая, и прочая. При этом, шаблон то не меняется, меняются только данные!
Таким образом, первое, что мы сделали — вынесли на сервер печати задачу рендеринга и отсылки на принтер отрендеренной формы. Сервер приложений только готовил данные и отсылал их на сервер печати. Экономия на трафике — с (в среднем) 2Мб на документ до 40кб, шаблоны сервер печати кеширует локально.
Устройство и возможности
Последовательно увеличивая функциональность сервера печати, мы смогли избавиться от перечисленных проблем.
Итак, что мы имеем сейчас.
Синхронная и асинхронная печать.
Как правило, нет необходимости в гарантированной по времени доставке печатной формы до принтера (т.е. распечатать прямо сейчас). И, с другой стороны, скрипт, который отправляет на печать документ уже сделал какие-то изменения в базе данных и велика вероятность, что другая транзакция может нарваться на эти блокировки. В этом случае требуется как можно скорее зафиксировать изменения. Для этого сервер приложений может принять задание на печать чтобы обработать его асинхронно. В отдельной транзакции выполнить скрипт подготовки данных и отправить его на соответствующий сервер печати. Синхронная печать подразумевает, что скрипт подготовки данных будет выполняться в текущей транзакции.
Иллюстрирующая схема:
Работающие в отдельных транзакциях (и потоках) внутри сервера приложений процедуры PrintBuilder и PrintSender занимаются обработкой очередей поступивших заданий. Технически — это потоки внутри сервера приложений, исполняющие бесконечный цикл. Количество этих поток настраивается администратором в зависимости от нагрузки. PrintBuilder обрабатывает задания поступившие асинхронно, выполняет скрипты для подготовки данных печатных форм и передает их в следующую очередь к PrintSender. В ту же очередь попадают и синхронные задания на печать.
В свою очередь PrintSender достает задания из очереди и передает их на сервер печати.
Как видно, при таком подходе радикально сокращается время блокирования в Транзакции 1, а чем меньше время блокирования, тем выше количество транзакций которое может обработать сервер!
Пакетная печать
Гарантированная печать на принтере документов в заданной последовательности. Вне зависимости от других заданий, все документы в пакете будут распечатаны неразрывно. Возможность потребовалась нам при реализации печати документов для водителей (пример применения в практике реального бизнеса). Это задача когда на принтер надо печатать Маршрутный лист (документ с перечислением всех адресов и картой) и далее бухгалтерские документы по каждому заказу. Все было хорошо пока водителей не стало больше 100 и виндовый спулер не начал путаться. Ситуация ухудшалась, если кто-то другой пытался напечатать другие документы на этот же принтер. Эти документы по какой-то причине иногда оказывались среди документов для водителя. Пришлось отказаться от спулера и сделать свой.
Гарантированная доставка
Документ, отправленный на печать, будет гарантированно отправлен на принтер. Если принтер недоступен, или недоступен офис с принтсервером система будет дожидаться восстановления связи и слать администраторам SMS и email.
Вот схема, иллюстрирующая схему работы принт-сервера:
На данной схеме PrintWorker — отдельные процессы, запускаемые для каждого подключенного принтера. Если драйвер принтера зависает, то процесс перестает отвечать, и по истечению таймаута принтсервер убивает процесс и запускает заново.
Учет печати
Побочный эффект — учет кто, когда, что и сколько напечатал, ну и пользователи перестали иметь прямой доступ к принтерам, что сильно сократило расход бумаги на печать диссертаций. Является ли это классной штукой — решайте сами.
Виртуализация принтеров
Принтеры, на которые можно печатать из системы перечисляются в настройках системы и подключаются только к серверу печати. Это значительно упрощает работу админам — не надо подключать принтера к каждому узлу кластера серверов приложений, и на каждой клиентской машине, устанавливать там драйвера и заниматься прочим шаманством. Кроме того, перечисление принтеров в системе позволяет легко реализовать UI для выбора принтера обычным пользователям. Например на какой принтер печатать бумаги на складе.
Таким образом, каждый принтер имеет свой идентификатор, который сохраняется при изменении принтера, и код для печати выглядит примерно так:
В данном примере распечатывается печатная форма для заданного документа на принтер, указанный в сотруднике.
Очереди в RDBMS
Обещали рассказать про очереди. Рассказываем.
В подсистеме печати, как видно, активно используется механизм очередей. К счастью, в Oracle Database есть возможность организовать их внутри базы данных, не прибегая к специальным отдельным сервисам. Реализуются они достаточно просто с использованием конструкции
select .. for update skip locked
. Выполнение такого запроса возвращает только НЕ заблокированные другими транзакциями строки. Некоторое неудобство доставляет то, что его нельзя использовать совместно с rownum, т.е. можно только все строки получить. Однако, есть обходной маневр — все сказанное верно только для выполнения запроса из клиентского (по отношению к БД) приложения! А значит, можно написать процедуру:
Главное в этой процедуре — объявить курсор и вытащить одну запись. Таким образом без лишних блокировок и очень удобно можно организовать параллельную обработку различных очередей.
Например, аналогичным образом решается задача пересчета цен для 400 000 товаров.
Предвосхищая вопрос, почему не воспользовались Oracle Advanced Queuing, отвечаем:
- Мы используем Managed ODP.NET, а он до сих пор не поддерживает AQ.
- Использование своих очередей позволяет хранить данные в структурированном виде, и реализовать встроенные механизмы управления этими самыми очередями:
Заключение
Надеюсь, удалось примерно раскрыть реализацию подсистемы печати в нашей платформе и натолкнуть на идеи решения проблем в Ваших приложениях. Дальше попробуем раскрыть тему управления транзакциями и связанными с ними проблемами.
Автор: Rupper