Получение top 25 вызовов в клиент-серверном режиме работы с наибольшей суммарной длительностью

03.04.24

База данных - HighLoad оптимизация

Анализ технологического журнала

Получение top 25 вызовов в клиент-серверном режиме работы с наибольшей суммарной длительностью

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

Bash-cкрипты тестировал на технологическом журнале, платформа 8.3.12. Это важно, ведь формат событий CALL значительно изменялся от версии к версии:

  • 8.3.1 добавлены свойства Report, Method, SessionID, Memory, MemoryPeak, InBytes, OutBytes, Context.
  • 8.3.6 добавлены свойства IName, MName
  • 8.3.8 добавлено свойство Usr

Получение полей Usr, Context в версии 8.2 было более трудным.

Пропустим установку bash и сбор технологического журнала. Возьмем быка за рога !

Упрощенная (первая) версия скрипта анализа длительности вызовов.

printf "%15s%15s%15s%15s%-30s%-90s", "Memory(Mb)", "Duration(sec)", "Quantity", "SessionID", "Usr", "Context" \
; printf "%s\n" \
; egrep '^[0-9][0-9].{9,25},CALL,.*,Usr=[^,]+,SessionID=[^,]+,Context=[^,]+,Interface=.*,Memory=.*,MemoryPeak=' -h -R --include "*.log" \
| awk -F'(^.{13})|(,CALL,.*Usr=)|(,SessionID=)|(,Context=)|(,Interface=.*,Memory=)|(,MemoryPeak=)' \
'{ Memory[$4,$3,$5]+=$6; Duration[$4,$3,$5]+=$2; Usr[$4,$3,$5] = $3; SessionID[$4,$3,$5] = $4; Context[$4,$3,$5] = $5; Quantity[$4,$3,$5]+=1} \
END {for (i in Memory) {printf "\n%15d%15d%15d%15s\t%-30s%-90s", Memory[i]/1000000, Duration[i]/1000000, Quantity[i], SessionID[i], Usr[i], Context[i]}}' \
| sort -rnb \
| head -n20

Комментарии к коду:

Знак «\» - в конце строки означает, что скрипт продолжается. Для каждой строки подробный комментарий внизу по номеру. Если строка начинается «|» - значит, она получает данные из предыдущей строки. Если строка начинается «;» - значит, команда выполняется самостоятельно. Команда awk разбита на строки с 4 по 6 для удобства чтения.

  1. Печать заголовка таблицы. Колонки с помощью команды printf по форматной строке "%15s%15s%15s%15s%-30s%-90s". Цифры означают ширину колонок, s – строковый формат, d – числовой. Полученная таблица прекрасно переносится из Bash-консоли в Excel.
  2. Разделительная строка между заголовком и данными табличной части
  3. Считываем данные из файлов текущего каталога и ниже, рекурсивно. Используем маску файла *.log Команда cat получает все данные, а egrep – только строки заданного формата.
  4. В параметре F разделители подобраны таким образом, чтобы переменные awk соответствовали параметрам Memory=$6; Duration=$2; Usr=$3; SessionID=$4; Context=$5;. Разделители записаны в скобках через ИЛИ. Недостаток такого метода в том, что требуется строка CALL строго определенной структуры, а эти строки имеют очень много вариантов формата. Поэтому скрипт выберет только часть строк. Параметры являются структурой (функцией, группировкой) от Usr, SessionID, Context. На мой взгляд - это самый правильный способ группировки, обоснование ниже.
  5. Этот участок команды awk выполняется для каждой строки файла. Все переменные группированы по одинаковым аргументам. Это для удобства печати. Память, длительность и количество вызовов – накапливаются.
  6. Участок «END» команды awk выполняется, когда обработаны все строки. В цикле командой printf по форматной строке "\n%15d%15d%15d%15s\t%-30s%-90s" выводятся строки таблицы. В данном случае переменная Memory печатается раньше, чем Duration, поэтому сортировка будет по объему памяти. Поменяйте их местами (в строке и заголовке), чтобы сделать сортировку по Duration. Можно сортировать по количеству вызовов Quantity, можно считать отношение Duration/ Quantity и сортировать по нему.
  7. Сортировка по убыванию
  8. Первые 25 строк выборки.

Проверочная работа.

Подсчитаем общее количество строк и количество строк, подходящих под шаблон. Выведем неподходящие строки.

egrep '^[0-9][0-9].{9,25},CALL,' -h -R --include "*.log" \
| awk '{if ($0~"Usr=[^,]+,SessionID=[^,]+,Context=[^,]+,Interface=.*,Memory=.*,MemoryPeak=") {my[2]+=1;} \
if ($0~"((Context|Module|Method).*,Usr=.*SessionID)|((Context|Module|Method).*SessionID.*,Usr=)") {my[3]+=1;} \
else if ($0~"(,Usr=.*(Context|Module|Method).*SessionID)|(,Usr=.*SessionID.*(Context|Module|Method))") {my[3]+=1;} \
else if ($0~"(SessionID.*(Context|Module|Method).*,Usr=)|(SessionID.*,Usr=.*(Context|Module|Method))") {my[3]+=1;} \
else print $0; \
{my[1]+=1;}} \
END {print my[1],my[2],my[3]}'

Для конкретного файла у меня получились цифры 3084 2235 3027. То есть из всех событий CALL только 60% удовлетворяют формату первого скрипта и 98% содержат Context|Module|Method, Usr, SessionID в какой-то последовательности. Сделаем вторую версию ТОТАЛЬНОЙ. Если переменная отсутствует – будем считать ее значение пустым. Сочетание Module-Method приравняем к контексту, если Context отсутствует.

Основная версия скрипта анализа длительности вызовов.

printf "%15s%15s%15s%15s%-30s%-90s", "Memory(Mb)", "Duration(sec)", "Quantity", "SessionID", "Usr", "Context" \
; printf "%s\n" \
; time egrep '^[0-9][0-9].{9,25},CALL,' -h -R --include "*.log" \
| awk -F'(,|=)' '{ \
ColumnName=3; ColumnVal=4; my[1]=""; my[2]=""; my[3]=""; my[4]=""; my[5]=""; my[6]=0; \
while(ColumnName<NF){ColumnName++; ColumnVal++; \
if ($ColumnName=="Usr") {my[1]=$ColumnVal;} \
else if ($ColumnName=="SessionID") {my[2]=$ColumnVal;} \
else if ($ColumnName=="Context") {my[3]=$ColumnVal;} \
else if ($ColumnName=="Module") {my[4]=$ColumnVal;} \
else if ($ColumnName=="Method") {my[5]=$ColumnVal;} \
else if ($ColumnName=="Memory") {my[6]=$ColumnVal;}}; \
if (my[3]=="") {print $1","$2","$3",Usr="my[1]",SessionID="my[2]",Context="my[4]"_"my[5]",Interface=,Memory="my[6]",MemoryPeak=";} \
else {print $1","$2","$3",Usr="my[1]",SessionID="my[2]",Context="my[3]",Interface=,Memory="my[6]",MemoryPeak=";} \
}' \
| awk -F'(^.{13})|(,CALL,.*Usr=)|(,SessionID=)|(,Context=)|(,Interface=.*,Memory=)|(,MemoryPeak=)' \
'{ Memory[$4,$3,$5]+=$6; Duration[$4,$3,$5]+=$2; Usr[$4,$3,$5] = $3; SessionID[$4,$3,$5] = $4; Context[$4,$3,$5] = $5; Quantity[$4,$3,$5]+=1} \
END {for (i in Memory) {printf "\n%15d%15d%15d%15s\t%-30s%-90s", Memory[i]/1000000, Duration[i]/1000000, Quantity[i], SessionID[i], Usr[i], Context[i]}}' \
| sort -rnb \
| head -n20

Комментарии к коду:

  1. Печать заголовка таблицы. Колонки с помощью команды printf по форматной строке "%15s%15s%15s%15s%-30s%-90s". Цифры означают ширину колонок, s – строковый формат, d – числовой. Полученная таблица прекрасно переносится из Bash-консоли в Excel.
  2. Разделительная строка между заголовком и данными табличной части
  3. Начинаем замер времени, выбираем строки событий CALL.
  4. С помощью команды awk для каждой строки в цикле перебираем элементы строки, разделители – запятые и равно. Переменная ColumnName возрастает от 3 до NF – предопределенная переменная команды awk.
  5. Если в строке где-то есть запись например ,Usr=Васильев Николай, то выполнится условие ($ColumnName=="Usr") и значение Васильев Николай попадет в переменную my[1]
  6. Номер сессии
  7. Контекст
  8. Модуль
  9. Метод
  10. Память
  11. Для каждой строки выводим переменные в нужном порядке, с соседними словами чтобы формат подходил под первый скрипт. Если контекст – пустой, то вместо него пишем Module_Method
  12. В параметре F разделители подобраны таким образом, чтобы переменные awk соответствовали параметрам Memory=$6; Duration=$2; Usr=$3; SessionID=$4; Context=$5;. Разделители записаны в скобках через ИЛИ. Параметры являются структурой (функцией, группировкой) от Usr, SessionID, Context. На мой взгляд - это самый правильный способ группировки, обоснование ниже.
  13. Этот участок команды awk выполняется для каждой строки файла. Все переменные группированы по одинаковым аргументам. Это для удобства печати. Память, длительность и количество вызовов – накапливаются.
  14. Участок «END» команды awk выполняется, когда обработаны все строки. В цикле командой printf по форматной строке "\n%15d%15d%15d%15s\t%-30s%-90s" выводятся строки таблицы. В данном случае переменная Memory печатается раньше, чем Duration, поэтому сортировка будет по объему памяти. Поменяйте их местами (в строке и заголовке), чтобы сделать сортировку по Duration. Можно сортировать по количеству вызовов Quantity, можно считать отношение Duration/Quantity и сортировать по нему.
  15. Сортировка по убыванию
  16. Первые 25 строк выборки.

Скрипт претендует на универсальность. Поэтому если у Вас он не заработает - пишите, разберемся.

Версия от 20/10/2021. Убрал PRINT, удалось обойтись одним оператором AWK. Так лучше.

printf "%15s%15s%15s%-30s%-90s", "Duration(sec)", "Memory(Mb)", "Quantity", "processName", "Context" \
; printf "%s\n" \
; time egrep '^[0-9][0-9].{9,25},CALL,' -h -R --include "*.log" \
| awk -F'(^.{13})|(,|=)' '{ \
ColumnName=3; ColumnVal=4; my[1]=""; my[2]=""; my[3]=""; my[4]=0; my[5]=""; my[6]=""; \
while(ColumnName<NF){ColumnName++; ColumnVal++; \
if ($ColumnName=="Usr") {my[1]=$ColumnVal;} \
if ($ColumnName=="SessionID") {my[2]=$ColumnVal;} \
if ($ColumnName=="p:processName") {my[6]=$ColumnVal;} \
if ($ColumnName=="Context") {my[5]=$ColumnVal;} \
if ($ColumnName=="Module") {my[3]=my[3]"_"$ColumnVal;} \
if ($ColumnName=="Method") {my[3]=my[3]"_"$ColumnVal;} \
if ($ColumnName=="Func") {my[3]=my[3]"_"$ColumnVal;} \
if ($ColumnName=="Memory") {my[4]=$ColumnVal;}}; \
if (length(my[5])<9) {my[5]=my[3];} \
Memory[my[5],my[6]]+=my[4]; Duration[my[5],my[6]]+=$2; Context[my[5],my[6]]=my[5]; \
processName[my[5],my[6]]=my[6]; Quantity[my[5],my[6]]+=1} \
END {for (i in Memory) {printf "\n%15d%15d%15d\t%-30s\t%-90s", Duration[i]/1000000, \
Memory[i]/1000000, Quantity[i], processName[i], Context[i]}}' \
| sort -rnb \
| head -n20

Дополнительные бонусы. Несколько похожих скриптов.

Сколько пользователей работает через терминальный сервер, а сколько – через веб сервер.

printf "%-30s%-30s%-30s", "applicationName", "Usr", "computerName" \
; printf "%s\n" \
; egrep '^[0-9][0-9].{9,25},CALL,.*applicationName=[^,]+,t:computerName=[^,]+,t:connectID=[^,]+,Usr=[^,]+,SessionID=' -h -R --include "*.log" \
| awk -F'(^.*applicationName=)|(,t:computerName=)|(,t:connectID=)|(,Usr=)|(,SessionID=)' \
'{applicationName[$5,$3]=$2; computerName[$5,$3]=$3; Usr[$5,$3]=$5} \
END {for (i in Usr) {printf "\n%-30s%-30s%-30s", applicationName[i], Usr[i], computerName[i]}}' \
| sort -nb

Комментарии к коду:

  1. Выводим заголовок таблицы
  2. Разделительная строка между заголовком и данными табличной части
  3. Читаем файлы рекурсивно по маске, строки определенного формата. [^,]+ означает несколько символов кроме запятой.
  4. Выбираем через или разделители так, чтобы определить переменные
  5. Выполняем группировку в переменные applicationName, Usr, computerName
  6. Выводим переменные в таблице
  7. Сортируем

Собрать все значения переменной.

time cat *.log \
| egrep '^[0-9][0-9].{9,25},CALL,.*Module=' \
| awk -F'^.*Module=' '{print $2}' \
| awk -F',' '{Module[$1]=$1} \
END {for (i in Module) {print Module[i] "\n"}}'

Поиск максимального значение Duration без сортировки, за один проход

time egrep '^[0-9][0-9].{9,25},CALL,' -H -r \
| awk -F'(,|-)' '{if($2>Max2){Max1=$0; Max2=$2;};} \
| END {print Max1}'

Результат работы:

Наименование файла, строка события с максимальным Duration.

Комментарии к коду:

  1. Читаем файлы рекурсивно с указанием имени файла, строки определенного формата.
  2. Для разделителей «,» или «-» значение Duration будет второй колонкой. Запоминаем значение и всю строку.
  3. Выводим строку события.

Куда инвестировать в кризис ?

Согласитесь, этот вопрос сейчас особенно острый. Акции Газпрома, биткоины, рубли, доллары, недвижимость, Форекс ? Мой сосед еще надеется на йены (привет, Максим !). Дочка почти верит в Деда Мороза.

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

 
 Подумайте: почему Вы останетесь в выигрыше - даже если счет не акцептуют ?
 
 Черновики и наброски.


Обновление апрель 2024.


События CALL используем для анализа загрузки сервера приложений, которая не связана с запросами СУБД. Например, отчет "Ведомость" рассчитывает сальдо по дням, сильно загружает сервер приложений. Смотрите подробнее //infostart.ru/public/2060362/ Быстродействие типовой 1С.

технологический bash context регулярные Богачев grep egrep awk gawk cat sed print printf echo sort logcfg

См. также

HighLoad оптимизация Технологический журнал Системный администратор Программист Бесплатно (free)

Обсудим поиск и разбор причин длительных серверных вызовов CALL, SCALL.

24.06.2024    5306    ivanov660    12    

56

HighLoad оптимизация Программист Платформа 1С v8.3 Бесплатно (free)

Метод очень медленно работает, когда параметр приемник содержит намного меньше свойств, чем источник.

06.06.2024    9465    Evg-Lylyk    61    

44

HighLoad оптимизация Программист Платформа 1С v8.3 Конфигурации 1cv8 Бесплатно (free)

Анализ простого плана запроса. Оптимизация нагрузки на ЦП сервера СУБД используя типовые индексы.

13.03.2024    5178    spyke    28    

49

HighLoad оптимизация Программист Платформа 1С v8.3 Бесплатно (free)

Оказывается, в типовых конфигурациях 1С есть, что улучшить!

13.03.2024    7703    vasilev2015    20    

42

HighLoad оптимизация Инструменты администратора БД Системный администратор Программист Платформа 1С v8.3 Конфигурации 1cv8 Абонемент ($m)

Обработка для простого и удобного анализа настроек, нагрузки и проблем с SQL сервером с упором на использование оного для 1С. Анализ текущих запросов на sql, ожиданий, конвертация запроса в 1С и рекомендации, где может тормозить.

2 стартмани

15.02.2024    12592    250    ZAOSTG    83    

115

HighLoad оптимизация Программист Платформа 1С v8.3 Конфигурации 1cv8 Абонемент ($m)

Встал вопрос: как быстро удалить строки из ТЗ? Рассмотрел пять вариантов реализации этой задачи. Сравнил их друг с другом на разных объёмах данных с разным процентом удаляемых строк. Также сравнил с выгрузкой с отбором по структуре.

09.01.2024    14551    doom2good    49    

71

HighLoad оптимизация Системный администратор Программист Бесплатно (free)

При переводе типовой конфигурации 1C ERP/УТ/КА на PostgreSQL придется вложить ресурсы в доработку и оптимизацию запросов. Расскажем, на что обратить внимание при потерях производительности и какие инструменты/подходы помогут расследовать проблемы после перехода.

20.11.2023    13769    ivanov660    6    

82
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. VKislitsin 1015 15.03.20 11:11 Сейчас в теме
Николай, интересная работа.
Вот только не пойму каким образом у Вас многострочные "хвосты" контекстов собираются? Или оставляете от контекста только первую строку, попадающую в одну строку с началом события?
2. bugagashenka 203 15.03.20 11:12 Сейчас в теме
3. VKislitsin 1015 15.03.20 11:15 Сейчас в теме
(2) Александр, а какой в этом смысл?
Я предварительно всё много строчное (запросы, контексты) собираю в одну строку с событием, потом произвожу нужные дейтвия - фильтры, группировки и т.д. В конце, если нужно, обратно в многострочные преобразую.
4. bugagashenka 203 15.03.20 11:19 Сейчас в теме
(3)само событие CALL пишется в одну строку. Преобразования тут излишни
6. VKislitsin 1015 15.03.20 11:30 Сейчас в теме
(4) Да, Вы правы. Проверил, у CALL-ов контекст минимален. Это у SCALL-ов простыни.
5. bugagashenka 203 15.03.20 11:20 Сейчас в теме
(3) а запросы да, нужно преобразовывать
7. vasilev2015 2725 15.03.20 11:46 Сейчас в теме
Добрый день, Коллеги !

Рад Вашему вниманию. Для платформы 8.3.12 Context входит в строку CALL, но сама строка может иметь много вариантов. Скрипт получился сложный, зато должен работать всегда. Если будут противные примеры - присылайте.
8. bugagashenka 203 15.03.20 11:56 Сейчас в теме
(7) можно дополнить скрипт на две ветки:
та, которая ищет по контаксту и та, которая для фоновых заданий
51:20.999033-20063024,CALL,1,process=rphost,OSThread=5072,t:clientID=86,callWait=0,first=true,Usr=DefUser,SessionID=13,p:processName=ExpKursy1SRF,Func=Background job,Module=ОбщийМодуль1,Method=ААА,Memory=5831,MemoryPeak=863661,InBytes=0,OutBytes=695,CpuTime=19953125

Например:
Cat 20*| grep Context=
и
Cat 20*| grep -P 'Module=.+Method | perl -pe 's/.+Module=(.+),Method=(.+?),.+/Context=\1\.\2/' Этот кусок вычленит контекст

Хотя, невнимательно прочитал второй скрипт, там как раз и обыгрывается такой сценарий
9. vasilev2015 2725 15.03.20 12:51 Сейчас в теме
(8) Второй скрипт работает, причем теги могут быть в произвольном порядке или отсутствовать. я тоже сначала пробовал заменой, но получалось слишком много вариантов. Про фоновые задания я не сам догадался - мне В.Б. подсказал.
akR00b; Smank0; acanta; +3 Ответить
10. Sloth 393 15.03.20 23:36 Сейчас в теме
Спасибо!
Очередной раз убеждаюсь, изучай чужой код, дабы совершенствовать свой ;)
11. triviumfan 97 16.03.20 15:47 Сейчас в теме
Неужели кому-то нравится "ковыряться" со скриптами вместо того, чтобы воспользоваться готовым продуктом по сбору и анализу ТЖ?!
12. vasilev2015 2725 16.03.20 16:22 Сейчас в теме
(11) Здравствуйте !

Смотря какой готовый продукт ))).
Когда я узнал, что ЦУП красиво анализирует периоды не более 15 минут - то немного разочаровался.
Отношусь с симпатией к обработке TormozIt, но не использовал.

У нас за день набегает несколько Гб - поэтому используем скрипт.
Морозов использует скрипт.

У Вас технологический журнал какого объема, чем обрабатываете ?
15. triviumfan 97 17.03.20 14:42 Сейчас в теме
(12)
Отношусь с симпатией к обработке TormozIt, но не использовал.

А зря - хорошая скорость парсинга и все нужно есть.
(12)
Когда я узнал, что ЦУП красиво анализирует периоды не более 15 минут - то немного разочаровался.

шутка?
(12)
У нас за день набегает несколько Гб - поэтому используем скрипт.

А зачем весь день, да ещё с таким фильтром...?
Прикрепленные файлы:
16. vasilev2015 2725 17.03.20 15:01 Сейчас в теме
(15) За весь день мы просматриваем события CALL. Это очень частое событие.
Видно, сколько раз сотрудник работал с документом, какая утечка памяти происходит.

Не верю, что ЦУП на рисунке работал в режиме мониторинга 23 дня.
19. triviumfan 97 17.03.20 15:40 Сейчас в теме
(16) Похоже, что Вы ни разу ЦУП не открывали.
А я не верю, что вы найдете утечку памяти, анализируя гигабайты логов. По мне это бессмысленное занятие. Причин утечки столько, что заниматься этим практически бессмысленно - искать иголку в стоге сена. Видимо у вас нагрузка низкая, раз вы маетесь этим:)
13. tech_supp19 17.03.20 08:57 Сейчас в теме
(11) Меня тоже интересует о каких готовых продуктах вы пишите?
14. vasilev2015 2725 17.03.20 09:31 Сейчас в теме
(11) Когда я увидел bash первый раз - он показался мне анахронизмом.
потом я сообразил, что это no-sql запросы с построением предварительного
плана запроса и управлением памятью. Нам такого не написать.
17. tormozit 7238 17.03.20 15:20 Сейчас в теме
Наверное в статье стоит сделать акцент на скорости однократного решения задачи предложенным методом. Врядли можно этот метод достаточно удобным и понятным большинству разработчиков. А вот по скорости однократного решения он конечно будет лидером. Поэтому его оправдано будет применять на очень больших логах или при однократном выполнении. Кстати частое выполнение этого запроса снизит его преимущества по скорости по сравнению с другими методами, хранящими курсоры чтения файлов логов (парсер Гилева, парсер Tormozitа).
18. vasilev2015 2725 17.03.20 15:29 Сейчас в теме
(17) Здравствуйте, Сергей !

Многие мои знакомые пользуются Вашим парсером :)) и отзываются положительно.

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

Написать один большой и правильный :))
20. Yashazz 4794 17.03.20 15:43 Сейчас в теме
А причём тут "куда инвестировать в кризис"? Это каким боком к теме?..
EugeneSemyonov; +1 Ответить
21. vasilev2015 2725 17.03.20 15:45 Сейчас в теме
(20) Здравствуйте, Яков !

там все очень логично. Призыв плюнуть на кризис и учиться.
22. Rioneri 96 24.08.20 15:12 Сейчас в теме
Выполнил, второй скрипт, получил строки с не очень понятным контекстом

Memory(Mb), Duration(sec), Quantity, SessionID,Usr, Context ,
32521 7856 1212969 _9
27198 38028 1663510 _7

Причину не смотрел.
24. vasilev2015 2725 26.08.20 09:25 Сейчас в теме
(22) Здравствуйте !

Спасибо за использование !
Присылайте файл ТЖ - проверю работу скрипта, отпишусь по результату.
25. Rioneri 96 26.08.20 14:18 Сейчас в теме
(24)Спасибо, разобрался. В TOP попали записи без свойства Module и со значением Method=9. Собственно и получился контекст "_9"...
23. Rioneri 96 24.08.20 15:13 Сейчас в теме
"_9" - это контекст, сессия и юзер не заполнены.
26. pinachet 31.05.22 16:42 Сейчас в теме
А если есть 3 журнала 1 – события ошибок, 2 – события серверных вызовов, 3 – события управляемых блокировок
Как можно собрать с помощью скриптов bash топ контекстов длительных серверных вызовов и обращений к СУБД по данным журналов
27. vasilev2015 2725 31.05.22 19:12 Сейчас в теме
(26) Здравствуйте !

не получается собрать журнал обычного формата ?
Оставьте свое сообщение