Про кластер серверов 1С

    Кластер — это разновидность параллельной
    или распределённой системы, которая:
    1. состоит из нескольких связанных
    между собой компьютеров;
    2. используется как единый,
    унифицированный компьютерный ресурс

    Gregory F. Pfister, «In search of clusters».


    Дано: есть бизнес-приложение (например, ERP-система), с которым работают одновременно тысячи (возможно, десятки тысяч) пользователей.

    Требуется:
    1. Сделать приложение масштабируемым, чтобы при увеличении количества пользователей можно было за счёт наращивания аппаратных ресурсов обеспечить необходимую производительность приложения.
    2. Сделать приложение устойчивым к выходу из строя компонентов системы (как программных, так и аппаратных), потере связи между компонентами и другим возможным проблемам.
    3. Максимально эффективно задействовать системные ресурсы и обеспечить нужную производительность приложения.
    4. Сделать систему простой в развертывании и администрировании.

    Чтобы решить эти задачи, мы в платформе 1С:Предприятие используем кластерную архитектуру.

    К желаемому результату мы пришли не сразу.

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

    image

    Как писал автор эпиграфа к этой статье Грегори Пфистер в своей книге «In search of clusters», кластер был придуман не каким-либо конкретным производителем железа или софта, а клиентами, которым не хватало для работы мощностей одного компьютера или требовалось резервирование. Случилось это, по мнению Пфистера, ещё в 60-х годах прошлого века.
    Традиционно различают следующие основные виды кластеров:

    1. Отказоустойчивые кластеры (High-availability clusters, HA, кластеры высокой доступности)
    2. Кластеры с балансировкой нагрузки (Load balancing clusters, LBC)
    3. Вычислительные кластеры (High performance computing clusters, HPC)
    4. Системы распределенных вычислений (grid) иногда относят к отдельному типу кластеров, который может состоять из территориально разнесенных серверов с отличающимися операционными системами и аппаратной конфигурацией. В случае grid-вычислений взаимодействия между узлами происходят значительно реже, чем в вычислительных кластерах. В grid-системах могут быть объединены HPC-кластеры, обычные рабочие станции и другие устройства.

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

    Для тех, кто не в курсе, коротко расскажу, как устроены бизнес-приложения 1С. Это приложения, написанные на предметно-ориентированном языке, «заточенном» под автоматизацию учётных бизнес-задач. Для выполнения приложений, написанных на этом языке, на компьютере должен быть установлен рантайм платформы 1С:Предприятия.

    1С:Предприятие 8.0


    Первая версия сервера приложений 1С (еще не кластер) появилась в версии платформы 8.0. До этого 1С работала в клиент-серверном варианте, данные хранились в файловой СУБД или MS SQL, а бизнес-логика работала исключительно на клиенте. В версии же 8.0 был сделан переход на трехзвенную архитектуру «клиент – сервер приложений – СУБД».

    Сервер 1С в платформе 8.0 представлял собой СОМ+ сервер, умеющий исполнять прикладной код на языке 1С. Использование СОМ+ обеспечивало нам готовый транспорт, позволяющий клиентским приложениям общаться с сервером по сети. Очень многое в архитектуре и клиент-серверного взаимодействия, и прикладных объектов, доступных разработчику 1С, проектировалось с учетом использования СОМ+. В то время в архитектуру не было заложено отказоустойчивости, и падение сервера вызывало отключение всех клиентов. При падении серверного приложения СОМ+ поднимал его при обращении к нему первого клиента, и клиенты начинали свою работу с начала – с коннекта к серверу. В то время всех клиентов обслуживал один процесс.
    image

    1С:Предприятие 8.1


    В следующей версии мы захотели:

    • Обеспечить нашим клиентам отказоустойчивость, чтобы аварии и ошибки у одних пользователей не приводили авариям и ошибкам у других пользователей.
    • Избавиться от технологии СОМ+. СОМ+ работала только на Windows, а в то время уже начала становиться актуальной возможность работы под Linux.

    При этом мы не хотели разрабатывать новую версию платформы с нуля – это было бы слишком ресурсозатратно. Мы хотели по максимуму использовать наши наработки, а также сохранить совместимость с прикладными приложениями, разработанными для версии 8.0.

    Так в версии 8.1 появился первый кластер. Мы реализовали свой протокол удаленного вызова процедур (поверх ТСР), который по внешнему виду выглядел для конечного потребителя-клиента практически как СОМ+ (т.е. нам практически не пришлось переписывать код, отвечающий за клиент-серверные вызовы). При этом сервер, реализованный нами на С++, мы сделали платформенно-независимым, способным работать и на Windows, и на Linux.

    На смену монолитному серверу версии 8.0 пришло 3 вида процессов – рабочий процесс, обслуживающий клиентов, и 2 служебных процесса, поддерживающих работу кластера:

    • rphost – рабочий процесс, обслуживающий клиентов и исполняющий прикладной код. В составе кластера может быть больше одного рабочего процесса, разные рабочие процессы могут исполняться на разных физических серверах – за счёт этого достигается масштабируемость.
    • ragent – процесс агента сервера, запускающий все другие виды процессов, а также ведущий список кластеров, расположенных на данном сервере.
    • rmngr – менеджер кластера, управляющий функционированием всего кластера (но при этом на нем не работает прикладной код).

    Под катом – схема работы этих трёх процессов в составе кластера.
    image

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

    1С:Предприятие 8.2


    В версии 8.2 мы захотели, чтобы приложения 1С могли запускаться не только в нативном (исполняемом) клиенте, а ещё и в браузере (без модификации кода приложения). В связи с этим, в частности, встала задача отвязать текущее состояние приложения от текущего соединения с рабочим процессом rphost, сделать его stateless. Как следствие возникло понятие сеанса и сеансовых данных, которые нужно было хранить вне рабочего процесса (потому что stateless). Был разработан сервис сеансовых данных, хранящий и кэширующий сеансовую информацию. Появились и другие сервисы — сервис управляемых транзакционных блокировок, сервис полнотекстового поиска и т.д.

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

    Отказоустойчивость


    Поскольку процесс работы стал stateless и все необходимые для работы данные хранились вне текущего соединения «клиент – рабочий процесс», в случае падения рабочего процесса клиент при следующем обращении к серверу переключался на другой, «живой» рабочий процесс. В большинстве случаев такое переключение происходило незаметно для клиента.

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

    Балансировка нагрузки


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

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

    • Round-Robin (циклический) – серверам присваиваются порядковые номера, первый запрос отправляется на первый сервер, второй запрос – на второй и т. д. до достижения последнего сервера. Следующий запрос направляется на первый сервер и всё начинается с начала. Алгоритм прост в реализации, не требует связи между серверами и неплохо подходит для «легковесных» запросов. Но при балансировке по этому алгоритму не учитывается производительность серверов (которая может быть разной) и текущая загруженность серверов.
    • Weighted Round Robin – усовершенствованный Round-Robin: каждому серверу присваивается весовой коэффициент в соответствии с его производительностью, и сервера с бо́льшим весом обрабатывают больше запросов.
    • Least Connections: новый запрос передается на сервер, обрабатывающий в данный момент наименьшее количество запросов.
    • Least Response Time: сервер выбирается на основе времени его ответа: новый запрос отдаётся серверу, ответившему быстрее других серверов.

    Для нашего кластера мы выбрали алгоритм, близкий по сути к Least Response Time. У нас есть механизм, собирающий статистику производительности рабочих процессов на всех серверах кластера. Он делает эталонный вызов каждого процесса сервера в кластере; эталонный вызов задействует некоторое подмножество функций дисковой подсистемы, памяти, процессора, и оценивает, насколько быстро выполняется такой вызов. Результат этих измерений, усредненный за последние 10 минут, является критерием — какой сервер в кластере является наиболее производительным и предпочтительным для отправки к нему клиентских соединений в данный период времени. Запросы клиентов распределяются таким образом, чтобы получше нагрузить наиболее производительный сервер – грузят того, кто везет.

    Запрос от нового клиента адресуется на наиболее производительный на данный момент сервер.

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

    Запрос от существующего клиента передается в другой рабочий процесс в двух случаях:

    1. Процесса больше нет: рабочий процесс, с которым ранее взаимодействовал клиент, более недоступен (упал процесс, стал недоступен сервер и т.п.).
    2. Есть более производительный сервер: если в кластере есть сервер, отличающийся по производительности в два и более раза по сравнению с сервером, где запушен текущий рабочий процесс, то платформа считает, что даже ценой миграции клиентского контекста нам выгоднее выполнять запросы на более производительном сервере. Переноситься клиенты с одного сервера на другой будут постепенно, по одному, с периодической оценкой результата – что в плане производительности стало с серверами после переноса каждого из клиентских процессов. Цель этой процедуры – выравнивание производительности серверов в кластере (т.е. равномерная загрузка серверов).


    Резервирование кластеров


    Мы решили повысить отказоустойчивость кластера, прибегнув к схеме Active / passive. Появилась возможность конфигурировать два кластера – рабочий и резервный. В случае недоступности основного кластера (сетевые неполадки или, например, плановое техобслуживание) клиентские вызовы перенаправлялись на резервный кластер.

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

    1С:Предприятие 8.3


    В версии 8.3 мы существенно переписали код серверной части, отвечающий за отказоустойчивость. Мы решили отказаться от схемы Active / passive кластеров ввиду сложности её конфигурирования. В системе остался только один отказоустойчивый кластер, состоящий из любого количества серверов – это ближе к схеме на Active / active, в которой запросы на отказавший узел распределяются между оставшимися рабочими узлами. За счет этого кластер стал проще в настройке. Ряд операций, повышающих отказоустойчивость и улучшающих балансировку нагрузки, стали автоматизированными. Из важных нововведений:

    • Новая настройка кластера «Уровень отказоустойчивости»: число, указывающее, сколько серверов может выйти из строя без последствий в виде аварийного завершения сеансов подключенных пользователей. Исходя из этой настройки кластер будет тратить определённый объём ресурсов на синхронизацию данных между рабочими серверами, чтобы иметь всю необходимую для продолжения работы клиентов информацию на «живых» серверах в случае выхода из строя одного или нескольких серверов.
    • Количество рабочих процессов не задается вручную, как раньше, а автоматически рассчитывается исходя из описаний требований задач по отказоустойчивости и надежности.
    • Появился ряд настроек, связанных с максимальными объемами памяти, которые разрешается потреблять рабочим процессам, а также настройки, определяющие что делать, если эти объемы превышены:

    image

    image

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

    image

    Три звена отказоустойчивости


    Как известно, даже если компоненты системы по отдельности надёжны, проблемы могут возникнуть там, где компоненты системы вызывают друг друга. Мы хотели свести количество мест, критичных для работоспособности системы, к минимуму. Важным дополнительным соображением была минимизация переделок прикладных механизмов в платформе и исключение изменений в прикладных решениях. В версии 8.3 появилось 3 звена обеспечения отказоустойчивости «на стыках»:

    image
    1. Связь между клиентом, работающим по HTTP(S), и веб-сервером. В случае веб-клиента этот механизм стандартно реализуется веб-технологиями. В случае тонкого клиента, работающего по HTTP с веб-сервером, или мобильного клиента (мобильный клиент всегда работает по HTTP) мы используем библиотеку libcurl с открытым исходным кодом.
    2. Отслеживание разрывов соединений, механизм балансировки нагрузки и механизм повторов вызовов позволяют как можно раньше узнать о возникшей проблеме и предпринять действия по её устранению.
    3. Связь между ТСР-клиентом и рабочим процессом. Клиентом ТСР может выступать либо клиент 1С, либо расширение веб-сервера при работе клиента через НТТР. При выполнении каждого НТТР-вызова происходит выбор наиболее подходящего соединения с рабочим процессом и отправка этого вызова. Наиболее подходящее соединение выбирается исходя из того, в какой рабочий процесс отправлялся предыдущий вызов данного клиента. Если следующий вызов клиента можно отправить в тот же рабочий процесс, куда ушел предыдущий вызов – мы так и поступаем. Только если по какой-то причине в данный рабочий процесс вызов отправить нельзя (потому, что рабочий процесс стал недоступен, либо мы знаем, что есть другой рабочий процесс с существенно лучшей производительностью) – мы отправляем новый клиентский вызов в более подходящий рабочий процесс.
    4. Связь между рабочими процессами сервисами кластера, реализованными в процессах rmngr. Сервисов кластера около 20 (в зависимости от версии платформы) — сервис сеансовых данных, сервис транзакционных блокировок и т.д. На этом уровне существенную роль играют механизм распределения сервисов по серверам и репликация данных сервисов кластера. Балансировка нагрузки на уровне 1С:Предприятия позволяет получать приблизительно одинаковую лучшую производительность от всех рабочих серверов.


    В заключение


    Благодаря механизму отказоустойчивости приложения, созданные на платформе 1С:Предприятие, благополучно переживают разные виды отказов рабочих серверов в кластере, при этом бо́льшая часть клиентов продолжают работать без перезапуска.

    Бывают ситуации, когда мы не можем повторить вызов, или падение сервера застает платформу в очень неудачный момент времени, например, в середине транзакции и не очень понятно, что с ними делать. Мы стараемся обеспечить статистически хорошую выживаемость клиентов при падении серверов в кластере. Как правило, средние потери клиентов за отказ сервера – единицы процентов. При этом все «потерянные» клиенты могут продолжить работу в кластере после перезапуска клиентского приложения.

    Надежность кластера серверов 1С в версии 8.3 существенно повысилась. Уже давно не редкость внедрения продуктов 1С, где количество одновременно работающих пользователей достигает нескольких тысяч. Есть и внедрения, где одновременно работают и 5 000, и 10 000 пользователей — например, внедрение в «Билайне», где приложение «1С: Управление Торговлей» обслуживает все салоны продаж «Билайн» в России, или внедрение в грузоперевозчике «Деловые Линии», где приложение, самостоятельно созданное разработчиками ИТ-отдела «Деловых Линий» на платформе 1С:Предприятие, обслуживает полный цикл грузоперевозок. Наши внутренние нагрузочные тесты кластера эмулируют одновременную работу до 20 000 пользователей.

    В заключение хочется кратко перечислить что ещё полезного есть в нашем кластере (список неполный):

    • Настройки безопасности, ограничивающие прикладному решению потенциально опасные для функционирования кластера операции. Например, можно ограничить доступ к файловой системе сервера или запретить прикладному решению запускать приложения, установленные на сервере.
    • Требования назначения функциональности – механизм, позволяющий администратору указать, какие сервисы кластера, прикладные решения (или даже отдельные функции прикладных решений – отчёты, фоновые и регламентные задания и т.д.) должны работать на каждом из серверов. Например, если заранее известно, что с конкретным прикладным решением (например, бухгалтерией) будет работать существенно меньше пользователей, чем с ERP, возможно, имеет смысл настроить более мощный сервер на работу исключительно с ERP.
    • Управление потреблением ресурсов – механизм, отслеживающий потребление кластером ресурсов (память, процессорное время, вызовы СУБД и т.д.). Он позволяет, в частности, выставлять пользователям квоты ресурсов, которые они не могут превысить, и прерывать операции, выполнение которых влияет на производительность сервера в целом.
    Делаем средства разработки бизнес-приложений

    Комментарии 28

      +1
      Всё звучит хорошо. Но странная функциональность, которая приводит к появлению ошибок на клиенте «Обратитесь к администратору для решения вопросов приобретения и установки лицензий уровня КОРП», при этом в интерфейсе администрирования и настройки кластера эта функциональность никак отдельно не выделена. Это явно не «лёгкость настройки».
        +1
        Да, запрос на явное выделение в настройке КОРП-параметров есть, работаем над этим.
        0
        А как рабочий процесс анализирует оперативную память при расчете доступной производительности? Сейчас если в одном сеансе отчет потребляет 2-5 ГБ памяти, то доступная производительность рабочего процесса падает до единиц (в ТЖ в событии CLSTR растет показатель memDur) и все сеансы на нем тормозят, хотя на сервере еще много свободной памяти.
          0
          Пока объём памяти процессов не превысил «Временно допустимый объём памяти процессов» — не учитывается.
          Доступная производительность падает по другим причинам (увеличивается время эталонного вызова).
          0
          Какими данными обмениваются rphost'ы при обычном режиме работы и при передаче сеанса с одного на другой rphost?
            0
            Параметры сеанса, данные форм, временные хранилища и много чего еще. Только не rphost-ы обмениваются между собой, а получают актуальное состояние данных из rmngr, причем только ту их часть, которая изменилась и фактически потребовалась.
            0
            Погодите, но ведь клиент под linux появился только в версии 8.3!
            А у вас на картинках аж с 8.1 нарисован.
              0
              Как много мы ещё не знаем про 1С )
                0
                Ну, под вайном 8.1 неплохо работал :)
                  0
                  Это да, именно так и использовал в то время. Но очень ждал выхода нативного клиента.
                0
                В какой версии платформы работа кластера наиболее стабильна?
                  0
                  Пробуйте 8.3.15.1656 или любую последующую.
                  0

                  К большому сожалению в кластере из нескольких серверов в режиме балансировки нагрузки без использования правил назначения функциональности для "привязывания" баз а одному серверу не работает корректно доменная аутентификация в http-сервисах. Такое поведение признано 1С ошибкой. И в нашем случае это сводит на "нет" все плюсы использования кластера серверов.

                    0
                    Единственное ограничение в аутентификации пользователей информационных баз средствами операционной системы при доступе к информационным базам по http — это отсутствие возможности использования ограниченного делегирования пользователей kerberos. Ограничений, связанных именно с использованием балансировки нагрузки кластера по нескольким серверам не существует.
                      0
                      Да, балансировку хотели использовать «из коробки», напрямую она не связана с проблемой. Если из трех серверов в кластере IIS веб-сервер стоит только на одном, то при вызове http-сервисов с этого веб-сервера 9 из 10 запросов возвращают ошибку 401.5, а в журнале регистрации появляется запись с попыткой входа «NT AUTHORITY\АНОНИМНЫЙ ВХОД».
                      Ввиду наличия ошибки и невозможности использовать требования назначения функциональности с проф-лицензиями пришлось отказаться от 3 серверов и остаться на одном.
                  0
                  Спасибо за интересную статью.
                  Пользуясь случаем, хочу спросить, планируется ли реализовать возможность подключения клиентов из внешней сети к серверу 1С без использования веб-сервера или прописывания имени сервера в hosts? Сейчас нельзя подключаться к кластеру серверов по IP-адресу из сети внешней по отношению к сети кластера и поэтому, если требуется организовать доступ к серверу 1С для внешних клиентов, нужно обязательно настраивать какой-нибудь веб-сервер.
                    0
                    Для простого случая такая возможность реализована в автономном сервере (в нем есть свой встроенный узкоспециализированный веб-сервер). Использования кластера по http без веб-сервера в ближайших планах нет.
                    0
                    Прочитал, как все здорово и прослезился.
                    На одном из скриншотов есть галочка «Принудительно завершать проблемные процессы». Думаю, местные специалисты и администраторы оценят юмор разработчиков платформы, которые «завершают проблемные процессы» под Linux путем отправки им сигнала SIGSEGV.
                      0
                      Там сложнее схема.
                      SIGSEGV шлется только в случае, если надо снять дамп.
                      В обычной ситуации шлется SIGTERM и, если совсем не отвечает, SIGKILL
                        0
                        А давно это поменяли? В документации про SIGSEGV написано, но про остальное — нет. Вообще, дамп вызывается через SIGQUIT, SIGILL, SIGABRT, SIGFPE и SIGSEGV.
                        SIGABRT можно перепутать с abort (любой из библиотеки или неотловленное исключение), SIGILL, SIGFPE и SIGSEGV можно перепутать с ошибкой на C/C++ (). SIGQUIT почти никем в дикой природе не используется, а сделать Quit from keyboard для сервиса (демона) — надо постараться.
                        Под «можно перепутать» имеется в виду интерпретация стандартного админа, который будет расследовать проблему «упал рабочий процесс».
                          0
                          У нас документирован только случай, когда нужно формировать дампы аварийного завершения при принудительной остановке (https://its.1c.ru/db/v8316doc#bookmark:cs:TI000000020:sigsegv — ссылка откроется только у подписчиков ИТС, сорри).
                          Все остальное — это детали внутреннего механизма, они никогда не документировались, но сама логика была сделана достаточно давно.
                      0
                      Вроде бы в версии 8.4 хотели ещё сильнее улучшить кластеризацию. Причем начали этот процесс аж в 2015 г. Я так понял всё пока в долгой заморозке?
                      0
                      только один вопрос — почему начиная с 8.3.11 и далее на каждом релизе 8.3.12, 8.3.13, 8.3.14, 8.3.15, 8.3.16 падает общая производительность одних и тех же операций относительно предыдущего мажорного релиза? 8.3.15 работает аж на 20% медленней 8.3.11. Кластер это хорошо, но не такой же ценой.
                        0
                        Вячеслав, можете мне в личку написать? Обсудим детали — что и как меряли.
                        Спасибо!
                          0
                          1. я сюда очень редко захожу
                          2. зачем мне что-либо писать в личку? у меня очень богатая история взаимодействия с различными сотрудниками 1С, если бы кому то действительно нужно было, давно купили бы у нас услуги
                          и не надо говорить что не в курсе чем наша команда занимается
                          3. делаю вывод что красивая история про регулярное 100% покрывающее тестирование, которое неоднократно слышал от представителей фирмы 1С — фикция и сказка

                          p.s. проведите ревизю вашего тестирования, действительно ли оно происходит, действительно ли оно покрывающее или просто всё на бумаге и графиках
                        0
                        Del

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

                        Самое читаемое