Как я обработку на альтернативный сервер выносил

Публикация № 998389

Программирование - Практика программирования

экспорт импорт обработка com C# 1c альтернативный сервер тяжелая обработка

5
В данном посте хочу поделиться опытом. Однажды возник инцидент, который смотивировал реализовать обработку, которую запускал бы обычный пользователь 1С, в основной системе. Но весь процесс обработки должен происходить за пределами рабочей базы. А юзабилити должно остаться на уровне простого пользователя. В качестве решения я выбрал службу Windows (С#), приложение инициации на клиенте и далее прошу под кат...

Всем доброго времени суток. Не буду сейчас вдаваться во все подробности, ибо это займет много Вашего и моего времени. 

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

Суть обработки проста. Выполняется сложный запрос с массой соединений и расчетов. Результирующая таблица пишется в ряд регистров сведений для последующего извлечения конечными отчетами. Своего рода OLAB куб внутри 1С. Пишется это все в ряд регистров, во имя нормализации и оптимизации дискового пространства, занимаемого базой. 

Как происходит запуск? Ежедекадно, специалисты департамента актуарных расчетов, получив отмашку от департамента статистики, как правило это происходит ближе к 19:00 вечера, запускают обработочку, где указывают дату сведений, на которую необходимо сформировать базу действующих договоров и связанные с ней расчеты, указывают флажками отчеты которые необходимо получить по завершении (отчеты выгружаются в Excel, в заранее определенную директорию), и нажимают кнопку "Сформировать". Все. Далее специалисты уходят домой, а 1С продолжает работать приблизительно до полуночи. Но это не суть.

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

Решение оказалось весьма простым. 

На альтернативном сервере я установил службу Windows, которая прослушивает IP и Port. Как только по указанному порту на этот IP адрес, поступает сообщение с определенным кодовым словом, запускается процесс. В данном процессе я прописал все то, что до этого я какое-то время делал руками. А именно - программа снимает свежий бэкап рабочей базы. Данный бэкап помещается в расшаренную директорию рабочего сервера. Затем из этой директории файл перемещается на альтернативный сервер. Свежий бэкап восстанавливается на специальную базу для подготовки отчетности. А далее через COM соединение, на альтернативном сервере, в специальной базе данных запускается та самая обработка, с теми самыми параметрами, которые до этого указал пользователь. По завершению обработки, данные, средствами SQL bcp (bulk copy program) переносятся на рабочую базу данных. А тем временем, на альтернативном сервере запускается регламентное задание по заданному расписанию, и начинает выгружать необходимые отчеты. Таким образом, все тяжелые запросы и обработки данных выносятся за пределы рабочего сервера, что существенно снижает нагрузку. А так же предотвращает ошибки связанные с блокировками. Единственная серьезная нагрузка в этом алгоритме происходит в момент коммита данных переносимых с помощью sql bcp. 

 

А теперь попробую привести некоторые наглядные фрагменты этого всего процесса. 

Весь процесс у нас начинается с приложения-инициатора. Там все просто. 

Создаем в VS консольное приложение на C#. В тексте основного модуля пишем такой код:

using System;
using System.Net.Sockets;
using System.Text;


namespace ExtFS_Client_Consol
{
    class Program
    {

        /// <summary>
        /// Отправляем на сервер запрос на инициацию процесса формирования БДН
        /// </summary>
        private static string SendMess(string IP, int Port)
        {

            //Инициализация
            string SMess = @"StartFormation";
            char[] outMessage = SMess.ToCharArray();

            try
            {
                TcpClient client = new TcpClient(IP, Port); 
                Byte[] data = Encoding.UTF8.GetBytes(outMessage);
                NetworkStream stream = client.GetStream();

                try
                {
                    stream.Write(data, 0, data.Length);

                    Byte[] comingdata = new Byte[256];
                    stream = client.GetStream();

                    string responseData = String.Empty;
                    StringBuilder completeMessage = new StringBuilder();
                    int numberOfBytesRead = 0;
                    do
                    {
                        numberOfBytesRead = stream.Read(comingdata, 0, comingdata.Length);
                        completeMessage.AppendFormat("{0}", Encoding.UTF8.GetString(comingdata, 0, numberOfBytesRead));
                    }
                    while (stream.DataAvailable);
                    responseData = completeMessage.ToString();
                    return responseData;
                }
                finally
                {
                    stream.Close();
                    client.Close();
                }
            }
            catch (Exception err)
            {
                Console.WriteLine(err.Message);
                return @"";
            }
        }

        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                try
                {
                    string IP = args[0];
                    int Port = Convert.ToInt16(args[1]);
                    string response = SendMess(IP, Port);
                }
                catch (Exception err)
                {
                    Console.WriteLine(err.Message);
                }
            }
            else
            {
                Console.WriteLine(@"Не переданы параметры запуска! Выполнение - не возможно!");
                Console.ReadKey();
            }
        }
    }
}

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

Далее, создаем в нашей системе 1С, константу с типом "ХранилищеЗначений". Она нам понадобиться для хранения скомпилированного консольного приложения. Я не буду тут описывать как именно этот exe попадет в константу. А вот что мы делаем когда пользователь нажимает кнопку "Сформировать" смотрите далее.

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

// Метод инициализации запуска формирования БДН/ЯО на альтернативном сервере. 
// Происходит сохранение во временную директорию exe файла, и запуск его с 
// параметрами (ip, port). EXE файл в свою очередь отправляет через tcp/ip 
// команду старта на сервер.
Процедура ИнициироватьЗапускФормированияНаАльтернативномСервере() Экспорт
	
	Попытка
		ПФ = ПолучитьПараметрыВнешнегоФормирования();
		ВремКаталог = КаталогВременныхФайлов();
		ФайловыйПакет = Константы.ИсполняемыйФайлИнициацииВнешнегоФормированияЯО.Получить().Получить();
		ДвоичныеДанные = ФайловыйПакет.Файл.Получить();
		ПутьФайла = ВремКаталог + ФайловыйПакет.ИмяФайла;
		НашФайл = Новый Файл(ПутьФайла);
		Если НашФайл.Существует() Тогда
			УдалитьФайлы(ПутьФайла);
			ДвоичныеДанные.Записать(ПутьФайла);
		Иначе
			ДвоичныеДанные.Записать(ПутьФайла);
		КонецЕсли;
		WSHShell = Новый COMОбъект("WScript.Shell");
		WSHShell.CurrentDirectory = ВремКаталог;
		WSHShell.Run(ФайловыйПакет.ИмяФайла + " " + ПФ.IP + " " + ПФ.Port, 0);
	Исключение
		#Если Клиент Тогда
			Сообщить("Ошибка при попытки запустить программу инициализации удаленного формирования БДН\ЯО: " + ОписаниеОшибки());
		#КонецЕсли
	КонецПопытки;
	
КонецПроцедуры

Сразу после запуска данного метода подключаем обработчик ожидания такого плана:

		ПодключитьОбработчикОжидания("ПроверкаСтатусаЗапускаВнешнегоФормирования", 4);

Сам подключаемый метод:  

// Обработчик каждые 2 секунды проверяет соответствующий регистр сведений, поменялся ли там статус на Истина.
// Если после 7й попытки статус не изменился - выводится сообщение о возможной ошибке при инициализации. 
// Если статус поменялся - выводится сообщение об успешном запуске внешнего формирования.
// В любом случае после заверешения обработчика ожидания, запись в регистре сведений - удаляется. 
Процедура ПроверкаСтатусаЗапускаВнешнегоФормирования()
	Запрос = Новый Запрос;
	Запрос.Текст = "ВЫБРАТЬ
	               |	УдаленноеФормированиеБДН.ДатаСведений КАК ДатаСведений
	               |ИЗ
	               |	РегистрСведений.УдаленноеФормированиеБДН КАК УдаленноеФормированиеБДН
	               |ГДЕ
	               |	УдаленноеФормированиеБДН.ДатаСведений = &ДатаСведений
	               |	И УдаленноеФормированиеБДН.ПроцессЗапущен";
	Запрос.УстановитьПараметр("ДатаСведений", ДатаСведений);
	Если Запрос.Выполнить().Выгрузить().Количество() > 0 Тогда
		Сообщить("Внешнее формирование успешно запущено!");
		ОтключитьОбработчикОжидания("ПроверкаСтатусаЗапускаВнешнегоФормирования");
		Набор = РегистрыСведений.УдаленноеФормированиеБДН.СоздатьНаборЗаписей();
		Набор.Очистить();
		Набор.Записать();
	Иначе
		КолПроверокСтатусаВФЯО = КолПроверокСтатусаВФЯО + 1;
		Если КолПроверокСтатусаВФЯО = 15 Тогда
			Сообщить("Внешнее формирование не было запущено на стороне альтернативного сервера, либо возникли проблемы с обратной связью. Необходимо проверить процесс выполнения.", СтатусСообщения.Важное);
			ОтключитьОбработчикОжидания("ПроверкаСтатусаЗапускаВнешнегоФормирования");
			Набор = РегистрыСведений.УдаленноеФормированиеБДН.СоздатьНаборЗаписей();
			Набор.Очистить();
			Набор.Записать();
		КонецЕсли;
	КонецЕсли;
КонецПроцедуры

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

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

Создаем проект Службы Windows. Добавляем новый класс. Назовем его по смыслу. В моем случае это удаленный сервис формирования, поэтому я назвал его RFS_Serv. Содержимое класса привожу ниже.

using System;
using System.ServiceProcess;
using System.IO;
using System.Threading;

namespace RFS_Service
{
    public partial class RFS_Serv : ServiceBase
    {
        private static IPListener MyListener = null;
        private static Thread MyThread;
        private LogWriter Logger;
        private Props XMLSett;

        public RFS_Serv()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            try
            {
                Logger = new LogWriter(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location));

                XMLSett = new Props();
                XMLSett.ReadXml();

                MyListener = new IPListener(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), XMLSett.Fields.LocalIP, XMLSett.Fields.Port);

                MyThread = new Thread(new ThreadStart(MyListener.StartListener));
                MyThread.Name = "ListenerThread";
                MyThread.Start();
                Logger.Log(@"Служба запущена");
            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"error: " + mess);
            }
        }

        protected override void OnStop()
        {
            try
            {
                MyListener.NeedListen = false;
                Logger.Log(@"Служба остановлена");

                MyListener.Dispose();
                Logger.Dispose();
                GC.SuppressFinalize(XMLSett);
                GC.SuppressFinalize(MyThread);
            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"error: " + mess);
            }
        }
    }
}

Данный класс является ключевым классом сервиса, и в обработчике события OnStart мы инициируем запуск "прослушки" ip адреса. Для этого мы описываем класс IPListener. Его описание так же привожу ниже:

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace RFS_Service
{
    class IPListener
    {

        /// <summary>
        /// Путь от куда была запущена программа
        /// </summary>
        private static string _ProgrammLocation = @"";
        public string ProgrammLocation { get { return _ProgrammLocation; } set { _ProgrammLocation = value; } }

        /// <summary>
        /// IP адресс по которому ведем прослушивание
        /// </summary>
        private static string _curIPAdress = @"";
        public string curIPAdress { get { return _curIPAdress; } set { _curIPAdress = value; } }

        /// <summary>
        /// Port по котором ведем прослушивание
        /// </summary>
        private static int _curPort = 951;
        public int curPort { get { return _curPort; } set { _curPort = value; } }

        private static bool _NeedListen = false;
        public bool NeedListen { get { return _NeedListen; } set { _NeedListen = value; } }

        LogWriter Logger;

        /// <summary>
        /// Конструктор экземпляра класса
        /// </summary>
        /// <param name="ApplicationPath">Директория из которой запущено приложение</param>
        public IPListener(string ApplicationPath, string IP, int Port)
        {
            Logger = new LogWriter(ApplicationPath);
            ProgrammLocation = ApplicationPath;
            curIPAdress = IP;
            curPort = Port;
            NeedListen = false; //по умолчанию ничего не слушаем
        }

        /// <summary>
        /// Деструктор экземпляра класса
        /// </summary>
        public void Dispose()
        {
            Logger.Dispose();
            GC.SuppressFinalize(this);
        }

        public void StartListener()
        {

            _NeedListen = true;

            IPAddress localAddr = IPAddress.Parse(_curIPAdress);
            int port = _curPort;
            TcpListener server = new TcpListener(localAddr, port);
            string CommandFromClient = @"";
            char[] TrimChars = { ' ', '0', '\0' };
            server.Start();

            while (_NeedListen)
            {
                try
                {
                    TcpClient client = server.AcceptTcpClient();
                    NetworkStream stream = client.GetStream();
                    try
                    {
                        if (stream.CanRead)
                        {
                            byte[] myReadBuffer = new byte[1024];
                            StringBuilder myCompleteMessage = new StringBuilder();
                            int numberOfBytesRead = 0;
                            do
                            {
                                numberOfBytesRead = stream.Read(myReadBuffer, 0, myReadBuffer.Length);
                                myCompleteMessage.AppendFormat("{0}", Encoding.UTF8.GetString(myReadBuffer, 0, myReadBuffer.Length));

                            }
                            while (stream.DataAvailable);

                            CommandFromClient = myCompleteMessage.ToString().Trim(TrimChars);
                            if (CommandFromClient == @"StartFormation")
                            {
                                Logger.Log(@"Принята команда запуска формирования.");
                                RFS rfs = new RFS(); //Запустим процесс формирования ЯО БДН на текущем сервере. 
                                rfs.Execute();
                                rfs.Dispose();
                            }
                            else
                            {
                                Byte[] responseData = Encoding.UTF8.GetBytes(DateTime.Now.ToString(@"Полученный запрос не имеет описания на сервере!"));
                                stream.Write(responseData, 0, responseData.Length);
                                Logger.Log(@"Принята не опознанная команда!");
                            }
                        }

                    }
                    finally
                    {
                        stream.Close();
                        client.Close();
                    }
                }
                catch
                {
                    Logger.Log(@"Ошибка во время прослушивания");
                    break;
                }
            }

        }

        public void StopListen()
        {
            _NeedListen = false;
        }

    }
}

Как видим из содержимого, суть класса проста. При определенных условиях запускается цикл в отдельном потоке, в котором происходит прослушка IP И Port. Приходящие сообщения анализируются, и если пришла определенная команда, например "StartFormation", программа инициализирует класс RFS и запускает процесс обработки на удаленном (альтернативном) сервере. Класс RFS в моем случае расшифровывается как Remote Forming System.

Давайте рассмотрим подробнее, как именно происходит работа в этом классе. 

Но для начала, в основной базе (конфигурации) 1С, необходимо заготовить одну функцию. Она понадобится для получения из вне, структуры хранения в базе данных. 

// Получает из структуры хранения наименование таблицы в СУБД(SQL) и наименования всех её полей.
// Предназначена для вызова из других программ, не предназначена для использования внутри 1С. 
// Возвращает строку разделенную точкой с запятой. На первом месте имя таблицы, затем список полей.
//
// Параметры: 
//	ИмяТаблицы - Строка - Наименование таблицы которую необходимо транслировать.
Функция GetTableStructure(ИмяТаблицы) Экспорт
	Попытка
		ОбъектМетаданных = Метаданные.НайтиПоПолномуИмени(ИмяТаблицы); 
		Если Не ОбъектМетаданных = Неопределено Тогда
			СписокПолей = "";
			Объект = Новый Массив;
			Объект.Добавить(ОбъектМетаданных);
			СХ = ПолучитьСтруктуруХраненияБазыДанных(Объект, Истина);
			Для Каждого ТекТаб Из СХ Цикл
				Если ТекТаб.Метаданные = ИмяТаблицы Тогда
					НаименованиеТаблицы = ТекТаб.ИмяТаблицыХранения;
				КонецЕсли;
				Для Каждого ТекПоле Из ТекТаб.Поля Цикл
					СписокПолей = СписокПолей + ";" + ТекПоле.ИмяПоляХранения;
				КонецЦикла;
				Возврат НаименованиеТаблицы + СписокПолей;
			КонецЦикла;
		Иначе
			Возврат "";
		КонецЕсли;
	Исключение
		ЯО.ВЛог(ОписаниеОшибки(), "TSQLMethods.GetTableStructure");
		Возврат "";
	КонецПопытки;
КонецФункции

Далее в классе RFS, в методе Execute пишем следующее.

                //Получим наименование таблицы статусов удаленного запуска
                Logger.Log(@"Получаем наименование таблицы рег.сведений УдаленноеФормированиеБДН");
                My1C Com1C = new My1C(AppPath);
                string StateTablesName = Com1C.GetTableStructure(WorkDBComConnect, @"РегистрСведений.УдаленноеФормированиеБДН");
                Com1C.Dispose();
                Com1C = null;

                //Отметимся в рабочей базе о том что процесс формирования запущен успешно
                Logger.Log(@"Установили статус в рабочей базе означающий что все запустилось");
                SetGoodStateOnMainServer(WorkUDLFile, StateTablesName);

В этом фрагменте кода видно как мы используем выше описанную функцию 1С. Она овзращает в виде строки наименование таблицы на уровне SQL, так же и список её полей. Получив имя физической таблицы на уровне SQL мы можем проставить в регистре сведений рабочей базы, флаг об успешном старте работы RFS. Используем для этого SQL скрипт Update.

Выглядеть это будет вот так:

        /// <summary>
        /// Метод проставляет в рабочей базе 1С, в регистре сведений "УдаленноеФормированиеБДН" признак "ПроцессЗапущен" в значение истина. Для всех существующих записей.
        /// </summary>
        /// <param name="UDLFile">Имя файла с настройками подключения к рабочей базе данных</param>
        /// <param name="TableStruct">Строка с физическим наименованием таблицы и её полей через ";"</param>
        private void SetGoodStateOnMainServer(string UDLFile, string TableStruct)
        {
            try
            {
                string[] TableNames = TableStruct.Split(';');
                string QueryText = String.Format(@"UPDATE {0} SET {2} = 0x01 where {2} = 0x00", TableNames);
                OleDbConnection con = new OleDbConnection(String.Format("File Name = {0}", UDLFile));
                OleDbCommand cmd = new OleDbCommand();

                cmd.Connection = con;
                con.Open();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = QueryText;
                cmd.CommandTimeout = 0;
                cmd.ExecuteScalar();
            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"RFS->SetGoodStateOnMainServer() error: " + mess);
            }
        }

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

                //Запустим процесс создания Backup'а на рабочем сервере
                Logger.Log(@"Запущен процесс создания бэкапа рабочей базы");
                string BackupFileName = @"";
                bool WeHaveBackup = CreateFreshBK(WorkUDLFile, out BackupFileName);

Код функции CreateFreshBK не имеет ничего особенного но все же приведу его текст. 

        /// <summary>
        /// Метод создает на рабочем сервере БД, новый, самый свежий бэкап и помещает его в специальную, публичную директорию для скачивания на локальный.
        /// </summary>
        /// <param name="UDLFile">Имя UDL файла с настройками подключения к рабочей базе данных</param>
        /// <param name="FileName">Имя файла создаваемого бэкапа. Имя возвращается наружу для дальнейшего использования.</param>
        /// <returns>Если бэкап удалось создать - возвращается true</returns>
        private bool CreateFreshBK(string UDLFile, out string FileName)
        {

            string BackUpFileName = @"upp_eurasia_" + DateTime.Now.ToString(@"yyyy-MM-dd_hhmmss") + @".bak";
            FileName = BackUpFileName; //Вернем наружу имя файла бэкапа

            try
            {
                Logger.Log(@"Создаем новый бэкап рабочей базы данных в: " + MainServerCreateBackUpToPath + @"\" + BackUpFileName);

                string[] Params = new string[2];
                Params[0] = WorkDBConnect.DataBase;
                Params[1] = MainServerCreateBackUpToPath + @"\" + BackUpFileName;

                string query = String.Format(@"
                BACKUP DATABASE[{0}]
                TO DISK = N'{1}'
                WITH COMPRESSION, 
                INIT,
                NOFORMAT, 
                SKIP, 
                STATS = 10
                ", Params);

                string QueryText = query;
                OleDbConnection con = new OleDbConnection(String.Format(@"File Name = {0}", UDLFile));
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = con;
                con.Open();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = QueryText;
                cmd.CommandTimeout = 0; 
                cmd.ExecuteScalar();

                return true;

            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"RFS->CreateFreshBK() error: " + mess);
                return false;
            }
        }

После того как отработает метод создания бэкапа, проверим, был ли он создан успешно. Если все прошло успешно, продолжаем основное действие.

                // Если бэкап базы был успешно создан - продолжаем дальнейшие действия
                if (WeHaveBackup)
                {
                    //Перенесем созданный бэкап на текущий сервер
                    Logger.Log(@"Переносим свежий бэкап на локальный сервер");
                    FileInfo BackupFile = new FileInfo(MainServerBackUpPublicPath + @"\" + BackupFileName);
                    if (BackupFile.Exists)
                    {
                        BackupFile.MoveTo(LocalBackupPath + @"\" + BackupFileName);
                    }

                    //Зачистим все активные подключения к базе данных перед накатыванием бэкапа.
                    Logger.Log(@"Перед разверткой свежего бэкапа, удалим все существующие соединения с базой для формирования отчетности");
                    KillAllDBConnections(MasterUDLFile, ReportsDBConnect.DataBase);

                    //Развернем бэкап на базу для формирования регистров ЯО
                    Logger.Log(@"Запустили восстановление базы из бэкапа");
                    bool DB_Is_Restored = RestoreDB(MasterUDLFile, ReportsDBConnect.DataBase);

                    //Удалим файл бэкапа из которого подняли базу для формирования
                    FileInfo localBackupFile = new FileInfo(LocalBackupPath + @"\" + BackupFileName);
                    if (localBackupFile.Exists)
                    {
                        localBackupFile.Delete();
                    }

                    if (DB_Is_Restored)
                    {
                        //Запустим процесс формирования ЯО на местной базе
                        Com1C = new My1C(AppPath);
                        Logger.Log(@"Выставим корректные настройки подключения к БД для тестовой базы.");
                        //Выставим корректные настройки подключения к БД для тестовой базы.
                        Com1C.SetTestDBConnectionFor1C(ReportsDBComConnect, ReportsDBServerName);
                        Logger.Log(@"Запускаем на тестовой базе формирование регистров ЯО.");
                        //Запустим формирования БДН/ЯО и выгрузку отчетов.
                        Com1C.StartFormationReportCore(ReportsDBComConnect, ReportDay, @"", @"", @"");

                        //Проверим статус формирования в базе формирования отчености. Убедимся что формирование прошло успешно.
                        FormationgState state = (FormationgState)Com1C.GetState(ReportsDBComConnect, ReportDay);
                        Com1C.Dispose(); // больше нам не нужно COM соединение с базой
                        Com1C = null;

                        if (state == FormationgState.fsOK) // Перенесем данные за период с тестовой базы на боевую, при условии что все удачно сформировалось.
                        {
                            //Когда все сформированно выгрузим все необходимые регистры в спец. директорию с помощью bcp
                            Logger.Log(@"Экспортируем данные в файлы");
                            ExportData(ReportDay, ReportsDBConnect, ReportsDBComConnect);

                            //Если стоит соответствующая настройка - перенесем выгруженные данные на рабочую базу данных
                            if (NeedExportDataToWorkBase)
                            {
                                Logger.Log(@"Импортируем данные из файлов в рабочую базу");
                                ImportData(ReportDay, WorkDBConnect, WorkDBComConnect);
                            }
                            else
                            {
                                Logger.Log(@"Импорт данных в рабочую базу - отключен");
                            }
                        }
                        else
                        {
                            SMSBox.SendSMS(@"RFS State not fsOK! Data not migrated!");
                        }
                    }
                    else
                    {
                        Logger.Log(@"Так как не удалось восстановить базу из свежего бэкапа, дальнейшие действия - не возможны. Работа программы - завершена.");
                        SMSBox.SendSMS(@"RFS can't restore DB.");
                        return false;
                    }

                }
                else
                {
                    SMSBox.SendSMS(@"RFS: No backup Work stopped.");
                    return false;
                }

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

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

        /// <summary>
        /// Убивает все подключения к базе на уровне MS SQL.
        /// </summary>
        /// <param name="UDLFile">Путь к UDL файлу с подключением к базе данных</param>
        /// <param name="DBName">Имя базы данных, подключения к которой - необходимо зачистить.</param>
        /// <returns>Если все прошло успешно - возвращает истину.</returns>
        private bool KillAllDBConnections(string UDLFile, string DBName)
        {
            try
            {
                string QueryText = String.Format(@"
                --Убиваем все активные сеансы подключенные к базе NameDB
                DECLARE @spid VARCHAR(200)
                DECLARE @kill_spid VARCHAR(200)
                DECLARE kill_session CURSOR FOR
                SELECT spid FROM [master].dbo.sysprocesses
                WHERE dbid=db_id('{0}')
                --spid идентификатор сеанса SQL Server.
                OPEN kill_session;
                FETCH NEXT FROM kill_session INTO @spid
                WHILE @@FETCH_STATUS = 0
                BEGIN
                    SET @kill_spid='KILL ' + @spid + char(10)
                    EXEC (@kill_spid)
                    FETCH NEXT FROM kill_session
                    INTO @spid;
                END;
                DEALLOCATE kill_session;", DBName);
                OleDbConnection con = new OleDbConnection(String.Format(@"File Name = {0}", UDLFile));
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = con;
                con.Open();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = QueryText;
                cmd.CommandTimeout = 0; 
                cmd.ExecuteScalar();
            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"EFS->KillAllDBConnections() error: " + mess);
                return false;
            }
            return true;
        }

Сам процесс восстановления базы тоже не хитрый. Смотрим далее

        /// <summary>
        /// Накатывает на указанную базу данных - последний актуальный бэкап рабочей базы.
        /// </summary>
        /// <param name="UDLFile">Имя UDL файла с настройками подключения к SQL инстансу, на котором числится база что необходимо восстановить из свежего бэкапа.</param>
        /// <param name="DBName">Имя базы данных - которую необходимо восстановить из бэкапа.</param>
        /// <returns>Если восстановление из бэкапа прошло успешно - возвращает истину.</returns>
        public bool RestoreDB(string UDLFile, string DBName)
        {

            bool result = false;

            string BackUpFileName = @"";

            try
            {
                string[] pfileslocal = Directory.GetFiles(LocalBackupPath, @"*.bak", SearchOption.TopDirectoryOnly);
                foreach (string pf in pfileslocal)
                {
                    BackUpFileName = pf.ToString();
                    break;
                }
                if (BackUpFileName != "")
                {
                    FileInfo fi = new FileInfo(BackUpFileName);
                    if (!fi.Exists)
                    {
                        result = false;
                        return result;
                    }
                }
                else
                    return false;

                Logger.Log(@"Восстанавливаем базу данных из бэкапа: " + BackUpFileName);

                string[] Params = new string[5];
                Params[0] = DBName;
                Params[1] = BackUpFileName;
                Params[2] = ReportsSQLDataPath;
                Params[3] = LogicalDBName;
                Params[4] = LogicalDBLogName;

                string query = String.Format(@"RESTORE DATABASE [{0}]
                FROM DISK = N'{1}'
                WITH FILE = 1,
                MOVE N'{3}' TO
                N'{2}\{0}.mdf',  
                MOVE N'{4}' TO
                N'{2}\{0}_log.ldf',  
                NOUNLOAD,  
                REPLACE,  
                STATS = 10", Params);

                string QueryText = query;
                OleDbConnection con = new OleDbConnection(String.Format(@"File Name = {0}", UDLFile));
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = con;
                con.Open();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = QueryText;
                cmd.CommandTimeout = 0; 
                cmd.ExecuteScalar();

                return true;

            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"RFS->RestoreDB() error: " + mess);
                return false;
            }
        }

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

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

В самой 1С, у меня так же имеется регистр сведений, куда пишется флаг об успешном завершении моей обработки, поэтому в конце я проверяю что этот флаг установлен. Потому как если его нет, значит в процессе могла возникнуть ошибка, и тогда необходимо об этом уведомить меня, что бы я заблаговременно начал с этим разбираться. Разумеется это больше для перестраховки. И разумеется если была ошибка, нет большого смысла переносить данные в рабочую базу данных. А вот если все в порядке, то начинаем процесс переноса таблиц средствами BCP (bulk copy program). Как это происходит? Очень просто. Для этого у меня заготовлено два метода:

        /// <summary>
        /// Основной метод экспорта таблицы базы данных в файл с помощью утилиты bcp.
        /// </summary>
        /// <param name="RegName">Наименование регистра 1С, который необходимо выгрузить.</param>
        /// <param name="FileID">Идентификатор регистра 1С, который будет содержаться в наименовании файла, в который будут выгружаться данные.</param>
        /// <param name="InfoDate">Дата сведений, на которую необходимо произвести выгрузку.</param>
        /// <param name="ConnectionString">Строка подключения к базе данных.</param>
        /// <param name="Connect">Параметры подключения к базе данных (SQL).</param>
        /// <param name="ComConnect">Параметры подключения к базе данных (COM).</param>
        /// <param name="DateFilterFieldIndex">Индекс поля, по которому будет происходить отбор по периоду. Значение по умолчанию = 0 (_Period) (Необходимо для не переодических регистров)</param>
        private void ExportTable(string RegName, string FileID, DateTime InfoDate, string ConnectionString, DBConnectStruct Connect, DBConnectStruct ComConnect, int DateFilterFieldIndex = 0)
        {
            try
            {
                string DateFilterFieldName = @"_Period";
                My1C etCom1C = new My1C(AppPath);
                if (DateFilterFieldIndex != 0)
                {
                    string TableNameAndField = etCom1C.GetTableStructure(ComConnect, RegName);
                    string[] Fiels = TableNameAndField.Split(';');
                    DateFilterFieldName = Fiels[DateFilterFieldIndex];
                }
                string QueryText = @"Select * from " + Connect.DataBase + @".dbo." + etCom1C.GetTableStructure(ComConnect, RegName, true) + @" where " + DateFilterFieldName + " = " + QuotedStr(InfoDate.ToString("yyyy-MM-dd"), "'");
                string FileName = BCPTransitPath + @"\" + bcpFilePrefix + "_" + FileID + "_" + DateTime.Now.ToString(bcpFileMask) + ".bcp";
                string CommandBCP = GetExportBCPCommandText(QueryText, FileName, ConnectionString);
                ExecuteAndWait(@"bcp ", CommandBCP);
                etCom1C.Dispose();
            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"RFS->ExportTable() error: " + mess);
            }
        }

        /// <summary>
        /// Основной метод импорта таблицы базы данных из файла с помощью утилиты bcp.
        /// </summary>
        /// <param name="RegName">Наименование регистра 1С, который необходимо загрузить из файла.</param>
        /// <param name="FileID">Идентификатор регистра 1С, который будет содержаться в наименовании файла, из которого будут загружаться данные.</param>
        /// <param name="InfoDate">Дата сведений, на которую необходимо произвести загрузку данных.</param>
        /// <param name="ConnectionString">Строка подключения к базе данных.</param>
        /// <param name="Connect">Параметры подключения к базе данных (SQL).</param>
        /// <param name="ComConnect">Параметры подключения к базе данных (COM).</param>
        /// <param name="UDLConnect">Наименование UDL файла с параметрами подключения к базе, в которую будет производиться импорт данных.</param>
        /// <param name="DateFilterFieldIndex">Индекс поля, по которому будет происходить отбор по периоду. Значение по умолчанию = 0 (_Period) (Необходимо для не переодических регистров)</param>
        private void ImportTable(string RegName, string FileID, DateTime InfoDate, string ConnectionString, DBConnectStruct Connect, DBConnectStruct ComConnect, string UDLConnect, int DateFilterFieldIndex = 0)
        {
            try
            {
                string DateFilterFieldName = @"_Period";
                My1C itCom1C = new My1C(AppPath);
                if (DateFilterFieldIndex != 0)
                {
                    string TableNameAndField = itCom1C.GetTableStructure(ComConnect, RegName);
                    string[] Fiels = TableNameAndField.Split(';');
                    DateFilterFieldName = Fiels[DateFilterFieldIndex];
                }
                string SQLTableName = Connect.DataBase + @".dbo." + itCom1C.GetTableStructure(ComConnect, RegName, true);
                itCom1C.Dispose();
                string QueryText = @"WITH CTE_DELETE AS (SELECT * FROM " + SQLTableName + @" where " + DateFilterFieldName + " = " + QuotedStr(InfoDate.ToString("yyyy-MM-dd"), "'") + ") Delete From CTE_DELETE"; //Запрос для удаления записей, которые будем сейчас вставлять. 
                OleDbConnection con = new OleDbConnection(String.Format(@"File Name = {0}", UDLConnect));
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = con;
                con.Open();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = QueryText;
                cmd.CommandTimeout = 0; 
                cmd.ExecuteScalar();

                string FileName = BCPTransitPath + @"\" + bcpFilePrefix + "_" + FileID + "_" + DateTime.Now.ToString(bcpFileMask) + ".bcp";
                string CommandBCP = SQLTableName + @" in " + GetImportBCPCommandText(FileName, ConnectionString);

                ExecuteAndWait(@"bcp ", CommandBCP);
            }
            catch (Exception err)
            {
                string mess = new ExceptionMessages().GetExMess(err);
                Logger.Log(@"RFS->ImportTable() error: " + mess);
            }
        }
		

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

Вот собственно и все. Данные успешно сформированы и перенесены. Если же что-то пошло не так, ответственные люди получают СМС (при наличии такой возможности в компании), либо e-mail c предупреждением об ошибке. 

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

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

p.s.

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

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

К статье прикрепляю архив с основными классами на C#. Весь проект целиком, разумеется, выложить не могу по вполне ясным причинам. 

Всем добра! :)

(Если на Ваш взгляд, в статье не хватает какой-то более развернутой информации, пишите, постараюсь дополнить).

5

Скачать файлы

Наименование Файл Версия Размер
Архив с исходным кодом класса My1C для C#, ряд вспомогательных классов. Может выступить в качестве шаблона для нового проекта. Не универсальное решение.
.zip 8,31Kb
08.02.19
1
.zip 1.0 8,31Kb 1 Скачать

См. также

Специальные предложения

Комментарии
Избранное Подписка Сортировка: Древо
1. Lem0n 163 08.02.19 16:16 Сейчас в теме
"Еще больший интерес предоставляет возможность запускать «только фоновые задания» на рабочем сервере кластера без сеансов пользователей. Таким образом можно высоконагруженные задачи (код) вынести на отдельный машины. При чем можно одно фоновое задание «закрытия месяца» через «Значение дополнительного параметра» запускать на одном компьютере, а фоновое задание «Обновление полнотекстового индекса» на другом.Уточнение происходит через указание «Значение дополнительного параметра». Например если указать BackgroundJob.CommonModule в качестве значения, то можно ограничить работу рабочего сервера в кластере только фоновыми заданиями с любым содержимым. Значение BackgroundJob.CommonModule.<Имя модуля>.<Имя метода> — укажет конкретный код"
2. ixilimuse 173 11.02.19 06:16 Сейчас в теме
(1) Безусловно кластеризация 1С крутая штука, но попробую влить ложку дегтя. Если где-то буду не прав - исправьте меня.

1. Тот самый сервер, который сейчас я использую для того, что описано в посте, мы заказывали как раз для кластеризации. Была такая идея фикс улучшить производительность и отказоустойчивость. На деле, мы не ощутили значимого прироста в производительности, а вот в стабильности даже потеряли. В чем это проявилось? Наша база (конфигурация) обновляется минимум 1 раз в сутки, а бывает что 2-3 раза в сутки. (Вынуждены подстраиваться под бизнес очень динамично) Так вот, очень часто возникал баг, после обновления конфигурации, 1С переставала запускаться. Для фикса приходилось останавливать сервер приложений на обоих серверах, перезапускать SQL серверную службу, и лишь после этого все оживало. Это было накладно по времени, особенно в тех случаях, когда обновление необходимо сделать в рабочее время, и тут возникает такой глюк. В результате от кластеризации отказались.
(Тут конечно есть такой фактор как версия... Возможно в последней версии это исправлено, но мы, увы, ради стабильности не прыгаем на каждый новый релиз платформы во избежании испытывать на себе все новые баги. Ибо нас за это не похвалят. Поэтому, на текущий момент положение дел такое, что мы не используем кластеризацию по вполне объяснимым причинам.)
2. Помимо пункта 1, все таки, кластеризация - подразумевает распределение процессорной нагрузки и нагрузки по ОЗУ. Но при этом по прежнему БД остается тонким местом. И в моем примере, мой запрос вытягивает приблизительно от 3 до 5 млн записей (на текущий момент, но это количество неуклонно растет с каждым месяцем), затем часть этих данных распределяется по регистрам. Это хорошо если это все запущено ночью и лишь 2-3 человека в этот момент могут сидеть и получать тоже довольно увесистые отчеты. Но допустим бывают ситуации когда все же необходимо эту "мега обработку" запустить днем. Днем в базе работают все филиалы со всех городов. Все они дружно заключают договора, получают отчеты, тем временем из разных источников интеграционными методами в базу льются тысячи договоров и заключаются (проводятся) автоматически, что в общей массе создает не слабую очередь, периодические блокировки. Засим, выполнять мою "мега обработку", куда более приятно и спокойно, на отдельной базе данных, где будет работать только моя обработка.

p.s. У нас еще есть одна идея фикс, это сделать зеркалирование БД, и для отчетов использовать зеркало, а запись вести в основную базу (диск). Но тут тоже море подводных граблей, и как пишут опытные в этом деле люди, жизнь зеркалированной БД для 1С длится ровно до первой реструктуризации БД. И поэтому для нас это вообще не подходит) Слишком часто придется сталкиваться с лишними проблемами. Поэтому сложившихся условиях, то что описано в посте, очень помогает и разгружает наше решение. Но разумеется это не панацея и не в любых решениях будет оправдано.
3. A_Max 17 11.02.19 14:21 Сейчас в теме
(2)
p.s. У нас еще есть одна идея фикс, это сделать зеркалирование БД, и для отчетов использовать зеркало

Скоро это будет уже в типовых механизмах, т.ч. эксперименты можно отложить. 1С озвучила эту фичу ещё весной прошлого года на партнёрской конференции.
ixilimuse; +1 Ответить
4. ixilimuse 173 11.02.19 15:07 Сейчас в теме
(3) Благодарю за наводку, будем ожидать с трепетом и надеждами =) Экспериментировать даже и не пытались, так как предварительная разведка показала, что шансов на успех маловато)
5. DonAlPatino 53 12.02.19 15:09 Сейчас в теме
(3)Учитывая, что весна этого года уже на носу, то "обещанного три года ждут?" :-)
6. DonAlPatino 53 12.02.19 15:09 Сейчас в теме
(1)Вроде это только в корпоративной версии работает?
Оставьте свое сообщение