2022-10-25: ArchDays - архитектура и мышление архитекторов
В пятницу, 21.10.2022 прошла однодневная ArchDays. Конференция первый раз прошла в 2019, потом два года проходила в online-формате, а сейчас - снова в offline. Отмечу, конференция имеет свое лицо и отчетливый фокус на архитектуре и работе архитектора. Доклады по этой теме есть на Highload, но там больше фокуса на public web и высоконагруженные приложения, что вполне естественно для конференции. А на ArchDays речь идет больше про enterprise и связанные с этим темы, которые на Highload не слишком широко представлены. При этом в докладах на ArchDays больше фокус на объяснении базовых вещей. Для кого-то этот материал очевиден, а для других - является ценным. А еще было несколько докладов, посвященных мышлению архитектора и способам принятия архитектурных решений, и это, на мой взгляд, очень позитивно отличает конференцию в хорошую сторону.
Участников было относительно немного, на сайте 700, но это с онлайн, а присутствовало, по-моему, 200-300. Впрочем я могу ошибаться. Было 4 параллельных трека докладов. Большинство презентаций уже доступны в программе конференции, так что думаю, остальные тоже скоро появятся.
Я, как обычно, вел заметки с докладов на своем телеграмм-канале, а теперь их собираю в отчет. Особо хочу отметить доклад Дмитрия Таболича про мышление архитектора. Другие доклады тоже были интересными, я очень рад, что побывал на конференции. И я слышал лишь четверть от всех докладов..
Содержание
- 1 Сергей Харламов. Архитектура масштабирования: ускоряем обработку и повышаем доступность данных
- 2 Максим Юнусов. Изобретение архитектурного решения
- 3 Евгений Лукьянов. Как Event Storming, DDD и чистая архитектура помогают запустить стартап
- 4 Геннадий Круглов. SAGA — эволюция и новые смыслы
- 5 Дмитрий Таболич. Майндшифт или мысли, как Архитектор
- 6 Денис Котов из Tinkoff. BPM(N,S, engine) — нужны или нет?
- 7 Олег Захарчук. Действительно ли нам не хватает программистов или проблема в архитектурах?
Сергей Харламов. Архитектура масштабирования: ускоряем обработку и повышаем доступность данных
В современных распределенных системах транзакционность ACID на уровне системы, а не отдельной ноды или процесса практически не поддерживается, фокус на высокой доступности на смену ACID пришел BASE: Base Availability при сбоях, Soft state вместо acid-консистентности, Eventual consistency согласованность в будущем. При этом такие решения часто реализуются поверх старого RDBMS, которое используется как базовое хранилище в legacy-системах, от которых не отказываются.
Для достижения используются различные механизмы: репликация баз данных; разные виды кеширования, при которых кэш работает как промежуточная БД, организованная по-другому, чем исходная; стриминг CQRS; CDC (Change Data Capture). Эти механизмы могут быть доступны в виде готовых решений, для других в БД предусмотрены точки расширения, третьи реализуются на уровне кода приложения. Засада в том, что в этих механизмах нужно разбираться для разбора инцидентов пользователей, и так же для понимания рисков при падении и последующем восстановлении. Эти решения - не внутреннее дело разработки.
В докладе был обзор шаблонов и разбор плюсов и побочных эффектов. Но рассказывать текстом - бесполезно, надо смотреть схемы на слайдах, так что смотрите презентацию.
Максим Юнусов. Изобретение архитектурного решения
Начат с классической ситуации: получив требования по времени отклика под нагрузкой и доступность сервиса с намеком, что для этого сервис должен масштабироваться, Архитектор выдает решение: PostgreSQL + хореография на событиях + Apache Kafka. Вопрос: как решение связано с исходными требованиями?
И дальше был разбор предлагаемого порождения решений. Для начала посмотрим, что предлагают методологии: TOGAF, SEI, ADR. Интересно, что они все исходят из того, что должны быть стандартные решения, и задача выбора состоит лишь во взвешивании вариантов. Различается косметика, подходы к принятию и документированию этих решений. И это - печально.
Поэтому предлагается посмотреть на смежные области, а именно на ТРИЗ. Там много интересного и полезного.
- Исходное описание - лишь описание ситуации, в ней пока не выявлено противоречие. Необходимо до этого довести, получить задачу.
- Противоречия бывают двух видов: противоречат два нефункциональных требования или нефункциональное требование противоречит архитектурному принципу.
- Для нефункциональных требований нужна заинтересованная сторона - кто заинтересован в его исполнении.
- Нефункциональное требование стоит подвергать сомнению. Действительно ли доступность сайта ниже 99.99 или время отклика более 0.1c приведет к оттоку пользователей (если это указали как основание требования). Может быть, требование можно ослабить или вообще снять?
- Решать противоречия следует, только если требования реально актуальны. Иначе будет overdesign, необоснованно сложное решение.
- Архитектурные принципы и ограничения тоже стоит подвергать сомнению.
- Антипаттерн, если в задаче не должно быть заложен подход к решению, когда сразу говорят, что из требований следует необходимость масштабировать сервиса на кластер
- Антипаттерн, если свобода сведена к свободе выбора из известного. А именно это предлагают методологии.
Разрешение противоречия
- Идеальное решение без ограничений
- Вводим ограничения до потери качества
- Формулируем физическое противоречие
Пример: архив должен создаваться для обеспечения надежности и НЕ должен создаваться, так как деградирует время отклика. Решаем
- во времени: делаем архив, когда низкая нагрузка
- в пространстве: делаем с отдельной ноды БД, которая для этого
- в структуре: две базы данных: для записи и для архива
- взаимодействия: приостанавливаем процесс архивирования при интенсивных запросах
- Архив как идеальный объект, архивирование без архива. Например, если архив нужен для отката - делаем откат иным образом, откручивая обработку событий назад.
Евгений Лукьянов. Как Event Storming, DDD и чистая архитектура помогают запустить стартап
Реальная история стартапа в области здоровья, в котором много алгоритмики и ML. На первом шаге была проверка алгоритмов, потом - проверка концепции на знакомых с интерфейсом на чат-боте. При этом разработку вел один человек на python, и получился сложный, взаимно-перевязанный код из множества сервисов, который очень сложно дальше дорабатывать. В некоторый момент потребовалось научиться вести разработку командой, а для этого потребовалось разобраться и упростить то, что сделано, обеспечить возможность подключения новичков. А потом был следующий этап - проверка спроса на сервис с партнерами, и там был следующий такт изменений.
На первом этапе
- Event Storming, чтобы разобраться, что делает приложение. Он показал, что основной разработчик не слишком представляет.
- Шаг к гексагональной архитектуре
- Внедрили тактические паттерны DDD: сбор бизнес-логики в агрегаты, Value-object, большие сервисы разбили на UseCase.
- Сделали референсный проект для погружения новичков.
На втором этапе
- Научили аналитиков пользоваться Event storming. Получились ограниченные контексты, области поделились. И в контекстах можно менять технологии.
- Нарисовали интерфейсы, отдали партнерам на тестирование.
- Типизация стандартных решений позволила включать новичков на локальные задачи. У новичка на следующий день есть сделанная простая задача. Разделение по квалификации.
- Прогнозирование. Сложный агрегат - 2 недели, простой два дня, объект-значение 6 часов, usecase 3 часа. За счет типизации решений.
- Trunk Based Development. Без веток. Потому что решение конфликтов было дорогим. Но Trunk based - невозможен без автоматики и хорошего тестирования.
- Тесты - есть заранее подготовленные данные окружения для запуска теста.
- Локальная сборка экономит деньги. Build-server - предохранитель.
- TDD для сложных мест
- Начали допускать тех.долг на этапах апробации решений
- Ограничили свободу - типовые узлы уже изготовлены, используешь их.
Книга: Accelerate: The Science of Lean Software Development and DevOps. Jez Humble, Gene Kim, Nicole Forsgren.
Геннадий Круглов. SAGA — эволюция и новые смыслы
Сообщество архитекторов решило разобраться, что же такое SAGA, и в докладе представлен результат этого. К сожалению, не впечатляющий. Начали от базовых понятий, подняли статьи 1980-х о том, что же такое - транзакции, какие у них свойства (ACID) и какие проблемы. Одна из них - long life transaction при распределенных системах, которые при формальной реализации ведут к эскалации блокировок. И потому предложено делать промежуточный коммиты внутри транзакции, разбивая их на малые участки, после которых бизнес-консистентности нет, а SAGA - концептуальный способ восстановления этой консистентности, тоже предложенный давно, в 1986. За счет того, что есть управляющая часть, которая в случае прерывания в неконсистентном состоянии либо откатывает шаги, для чего должны быть предусмотрены средства отката, либо проталкивает до завершения, если для этого есть средства. Собственно, все.
Как это реализуется - было за пределами доклада, хотя были ссылки на статьи Microsoft, Red Hat, AWS, Camunda. В общем, жаль, что содержание ограничилось этим. Я тут хочу отметить, что в сентябре был доклад Филиппа Дельгадо "Микросервисы через боль и превозмогание" на Saint Highload (кому интересно - мой конспект), где в числе прочего он говорил про SAGA, пунктиром обозначив минимум четыре шаблона реализации. И я надеялся услышать на докладе про шаблоны и концепты более детально, а получилась лишь историческая справка про статьи 1980-х о транзакциях и SAGA.
Дмитрий Таболич. Майндшифт или мысли, как Архитектор
Очень крутой доклад про отличие мышления архитектора от мышления инженера-разработчика. Это первый сдвиг мышления, касающийся именно технологического стека. Дальше добавляются другие аспекты: business focus, governance, strategy, но начало - именно в сдвиге мышления по техническим вопросам. Если кратко, то для каждой задачи архитектор смотрит спектр вариантов решений и ответственно делает выбор с учетом контекста конкретного проекта. Это достаточно абстрактно, поэтому в докладе это иллюстрировано большим количеством кейсов.
- Есть задача от проджекта. Инженер: давайте сделаем. Архитектор: какие ограничения?
- Silver bullet. Инженер: я много раз делал, и сделаю еще раз на своем опыте. Архитектор: для начала смотрим альтернативы индустрии, потом - выбираем. Если решение одно - значит нет ни одного.
- Документируй. Инженер: хороший код self-explained. Архитектор: очевидно, необходимо описывать ключевые решения, вопрос не стоит. И надо быть готовым
- Task-driven mindset. Инженеру важна четкая постановка задачи - что сделать. Для архитектора - зачем делаем, какую проблему решаем
- CV-driven mindset. Инженер: давайте втащим фреймворк. Архитектор: многофакторная оценка уместности - безопасность, скиллы команды и т.п. Вопрос не торможения, а именно многоаспектной оценки.
- Язык бэкенда и фронтенда. Инженер - только на техническом языке структуры приложения. Архитектор - много viewpoint, в том числе - на языке бизнес-стейкхолдеров, на языке безопасников и так далее.
- Копипаст. Инженер: посмотрим на stackoverflow и заберем готовый кусок не разбираясь. Архитектор: решения надо смотреть, но надо смотреть несколько и анализировать уместность, делая выбор.
- Призма мышления. Инженер: сужаем область понимания на задачу. Архитектор: включение задачи в большую систему и влияние на нее.
- Роль или функция? Инженер: играл роль архитектора в своей команде - значит архитектор. Архитектор: может играть эту роль НЕ ТОЛЬКО в своем проекте, где есть многолетний опыт, может быстро разбираться в других проектах.
- Я-эксперт. Инженер: отслеживаешь развитие своей платформы, своих технологий. Архитектор - смотришь на множество технологий, на варианты построения решений.
- HERO. Инженер: выбрана технология, и дальше много усилий, чтобы заставить работать, даже если она не подходит. Например, сначала выбрать JVM для serverless и бороться с долгим стартом JVM. Архитектор: оценивает решения в контексте проекта.
- Тот самый YAGNI. Мы решили выбрать Mongo, потому что там - транзакции, хотя они и не нужны. Архитектор - обоснованные решения в контексте конкретного проекта, а не вообще
- IT works on my machine: Инженер: у меня все работает. Архитектор - что DevOps процесс позволяет
- Все по Scrum. Инженер: о следующей функциональности посмотрим в следующем спринте. Архитектор: видим контекст вперед, не привязываем решения к моменту.
- Ответственность. Инженер: склонен передать ответственность на коллективное принятие решений. Архитектор - не просто принимает решения, но и несет ответственность за принятые решения.
И с заключении книги:
- 97 Things Every Software Architect Should Know и
- BTABoЛ 3.0: Business Technology Architecture Body of Knowledge
В комментариях было обсуждение с Филиппом Дельгядо. Его тезис - что доклад не про разницу мышления между разработчиком-инженером и архитектором, а про разницу мышления между кодером и инженером, он отказывает в в звании инженера тем, кто решая задачу не думает, зачем ее делать и демонстрирует другие другие приведенные в докладе способы мышления, характерные для инженера. И второй - что даже простые задачи нельзя решать, не отвечая на вопрос "зачем", это ведет к резкому увеличению стоимости проекта и падению качества.
Тезисы понятные, но у меня есть возражения.
- Во-первых, по моему опыту, решить задачу, не спрашивая зачем - можно, и в большинстве случаев получаются приемлемые результаты. При этом, естественно, ты учишь спрашивать "зачем" и многим другим вещам, потому что спросить - просто и потери от того, что не спросил - действительно большие, жалко. Но в целом ситуация не катастрофическая, если в команде только 1-2 человека спрашивают "зачем".
- Во-вторых, нет такой позиции - "кодер", и квалификацию такую тоже нельзя присваивать, это слово имеет негативные коннотации и демотивирует. Так что мы имеем разработчика, и с точки зрения общепринятого словоупотребления, это - частный случай инженерной позиции: человек, решающий технические задачи.
- В-третьих, то, что в докладе разнесли линейки развития разработчиков и архитекторов, в целом, правильно и соответствует сложным сеткам специализаций, например, SFIA. Хотя я знаю компании, где архитекторов считают старшими ступенями в развитии разработчиков, так тоже можно.
Но в целом это - оттенки смысла, а не принципиальные разногласия, по-моему.
Денис Котов из Tinkoff. BPM(N,S, engine) — нужны или нет?
Интересный доклад о месте BPM-схем и движков, которые их реализуют. Тезис состоит в том, что если у вас есть хороший BPM-движок, то вы из коробки имеет мощное средство реализации процессов, которое берет на себя не только задачу выполнения экземпляра процесса для конкретной ситуации, но так же отработку особых ситуаций, перезапуск или откат процесса, миграцию экземпляров при появлении новых версий версий описания и мониторинг. При этом у вас из коробки есть таймеры, напоминания, уведомления, механизмы прерываний по событиям и многое другое, и вам нужно лишь реализовывать конкретные шаги, которые может делать конкретная автоматизированная система или человек. Процессы, которые реализует BPMN - гораздо мощнее, чем те, которые дает State machine, процесс имеет много состояний. Это Денис показывал на примере регистрации ИП, которая начиналась с 10+ последовательных шагов, которые действительно просто реализовать на State machine, и за полтора года развития превратилась в сложную схему, в которой около 11 переменных-состояний.
Вопрос в том, где взять этот хороший BPMN-движок, который все это дает из коробки. Начинали они с использования IBM process designer, который всем хорош, только безнадежно отстал по технологиям: Java 6 и никакой интеграции с современными CI/CD и системами тестирования. Потом делали свою State machine, это жило, но с ростом числа процессов, команды и без визуализации и многих других поддерживающих сервисов было сложно. Cейчас - Kotlin как язык, Camunda как движок, Spring для обвязки и Kafka для интеграций. Плюс свое решение на Prometheus и Grafana для мониторинга.
Есть нюанс про уместность использования. Маркетологи успешно продают BPM-решения как серебряную пулю для всего. А сейчас еще идет аккуратное брендирование этих движков под LowCode, когда люди смогут тыкать мышкой без программистов. Это все - маркетинг. Уместно применение для реально сложных процессов, где много взаимодействия людей и систем, и которые разворачиваются во времени. И есть ограничения по нагрузке, 1-10 млн. инстансов ежедневно. А camunda 8 позволяет нагрузку 100 млн. инстансов ежедневно и более. Но все равно, не стоит делать через эти схемы сценарии, поддерживающие поведение пользователей на сайте с реакцией на его клики.
А еще BPMN - сложен, 15 абстракций и 500 графических конструкций. Поэтому для простых задач его использовать не надо, надо использовать Workflow engine, которых много - Temporal, Cadence и так далее. Там всего пара абстракций, и гораздо меньше графических конструкций и для большого количества задач этого достаточно.
Олег Захарчук. Действительно ли нам не хватает программистов или проблема в архитектурах?
Я не слушал этот доклад на конференции, был на параллельном треке. Уже позднее, в январе, меня спросили, что я думаю об этом докладе. Я посмотрел презентацию, и мы обсудили его в переписке. Думаю, это будет интересно, поэтому переношу в отчет. Но явно оговорюсь, что впечатление может быть неточным, так как оно основано на презентации, а не на записи доклада.
Резюме обсуждения такое.
- Система, предложенная Олегом, для работы требует наличия сильного архитектора. Он - сильный архитектор, у него - будет работать. У другого - не факт. Можно перефразировать - у каждого архитектора есть предел масштаба, до которого он может заставить такую схему работать, и чем архитектор сильнее - тем больше масштаб.
- Для систем большого размера это применимо? Чем больше масштаб системы - тем выше требования к архитекторам. Вопрос в наличии таких архитекторов в доступе. Для небольших систем тоже может оказаться эффективным, если архитектор есть. А вот на большом масштабе, на мой взгляд, построение единой модели - НЕ решаемая задача. При этом саму модель предлагается описывать через решение задач, даже не через процессы и не через task flow - а этого недостаточно. А на уровне софта по сути предлагается монолит с единой моделью данных - а это получается не изменяемая конструкция, сложные завязки и прочие проблемы монолита.
- Применимость схемы также сильно зависит от возможности считать контекст на время реализации системы реализации квазистабильным. Иными словами, если у нас относительно слабо изменяющаяся область, так что мы можем разработать единую модель и положить ее в БД, разработать приложения, и внедрить их и за это время бизнес-процессы и другой контекст изменятся относительно слабо, так что достаточно будет адаптации - то оно сработает. Темп изменений разный в разных бизнес-областях. Но чем больше область автоматизации (больше предприятие) - тем больше вероятность изменений.
Теперь подробнее обсуждение.
Вопрос. По-моему у товарища получилось создать подход, при котором создается сразу единая система, а не пытаются создать куски, а потом их согласовать. Максим, ты не считаешь, что если изначально описать систему, а потом её строить, а не создавать куски, а потом их синхронизировать, настраивать обмен и т.п. - это более продвинутый подход?
Мой ответ. Тут вопрос в масштабах системы. Автор говорит об архитектуре предприятия в целом. Если мы берем большое предприятие (торговую сеть, производство, банк) - то там нельзя обойтись одной системой. Это пробовали в нулевых, все крупные ERP и банковские системы разрабатывались именно в такой парадигме - большая система закрывает все потребности предприятия. Не получилось, на практике все большие ИТ-ландшафты - фрагментарные. Не получилось по двум причинам (а) сложность - система не лезет в головы и (б) трудоемкость - ты не можешь создать такую большую систему в разумные сроки (год-два), это проект лет на 5-7, а значит у тебя фрагментарный ландшафт старого и нового. А раз так - можно и новое делать из разных вещей. Плюс за 5 лет рынок меняется так, что принятые архитектурные решения оказываются не валидными, надо пересматривать, а часть системы уже сделана.
Все эти подходы - из опыта автоматизации управления физическими объектами (ракеты, космос, атомные станции), а физика меняется гораздо медленнее. Хотя и там новые технологии сейчас сильно меняют ситуацию.
А если брать некоторый фрагмент - то все разумно. Так и делают в современном DDD - ограниченный контекст с моделью предметной области на некоторую ее часть, которую сейчас автоматизируют, и сервисы внутри. Сервисы могут быть на общей БД, а могут использовать свои, тут вопрос требуемой масштабируемости и производительности. Товарищ, кстати, живет в старой парадигме, когда мощности БД заведомо хватит.
Вопрос. Максим, ты не веришь, что:
- Товарищу удалось создать систему (надсистему), которая позволяет построить модель системы, включающую в себя домены всей системы?
- И учитывая цельность надстстемы (среды которая позволяет описывать модель), то у модели «автоматически» получаются согласованные между собой домены? И не надо потом согласовывать между собой домены.
Я понимаю на примере языков обособление доменных областей (народов), но при укрупнении (при возникновении нации) возникает какой-то доминирующий язык и вот уже нация говорит на одном языке. Но дальнейшее развитие взаимодействий в обществе приводит к необходимости появления языка межнационального общения и одним из таких становится английской язык (как и ряд других).
И по моему при каждом шаге укрупнения сообщества за счет появления общего языка общения появляются у этой системы новые свойства, те которых не было у частей (по моему системный подход это прекрасно об этом, об эмерджентности).
Может быть у созданной товарищем системы появилось (в силу эмерджентности) возможность создавать согласованные модели. Модели у которых части (домены) изначально согласованы. И не надо потом отдельные доменные области согласовывать между собой. И понятно почему эти доменные области не хотят друг другом согласовываться. Т. к. у тех кто проводит согласование нет представления (или возможности описать) систему как единое целое у увязать части системы в единое целое и получить за счет этого новое свойство системы. И согласованность частей это побочный эффект его подхода.
Тот кто придумал часы, как устройство измеряющее время, увидел (придумал) и воплотил эту систему. А тот кто не видит системы в корпусе, шестерёнках и винтиках не может их собрать в единое целое. Но если ему показать чертежи единой системы, то он с большой долей вероятности (при умении читать чертежи, при желании напрячь мозг и др.) поймет что из себя представляют часы как единый механизм. И сможет его отремонтировать, улучшить, изменить. Но пока нет понятия о проектировании, нотации для чертежа, линейки (языка для описания чертежа) нет и описания системы в виде чертежа. И строители храмов делают свои модели в натуральную величину из дерева + они гении в строительстве. Но пока не инженеры. И нет поэтому сопромата, который поможет рассчитать конструкцию заранее, не проводя натурные испытания и другие дорогостоящие мероприятия. Может товарищу всё же удалось создать нотацию для построения моделей сложный систем как единого целого?
Немного отвлеченный вопрос: Почему в системе уровня предприятия, например, бухгалтерия должна быть написана своя, а не взята промышленная система бухучета (типа 1С) и допилена до нужного состояния, но с учетом понимается как эта система встроена в целое (в модель предприятия)?
Я довольно много лет проработал в строительной отрасли (18 лет). И видел как проектируются заводы (в частности ГОКи (горные обогатительные комбинаты)). Каждый ГОК имеет свои особенности и он в этом смысле уникален. Но чаще всего берутся существующие системы техпроцесса ГОКа и на него расставляется стандартное оборудование. И проектирование ГОКа идет очень поэтапно, но сразу создаются образ будущего ГОКа (например, на стадии ТЭО (но там не только экономика описывается, не смотря на название документа), а всё предприятие в целом. А потом на каждом шаге, в том числе при строительстве (воплощения системы в «металле»), идет изменение проекта, но при этом есть рамка (образ будущего ГОКа) и она позволяет удержать целостность и непротиворечивость системы.
А я сколько слушаю про архитектуру программных решений, не вижу такого же подхода. При этом заимствование из области строительства в области ИТ уже было, когда появилась идея паттернов. Я не понимаю почему ИТ не продолжают заимствовать идеи и подходы (теперь уже в проектировании) у строительной отрасли.
Мой ответ. Объяснение разницы между ИТ-разработкой и задачей проектирования (и строительства) самолетов, автомобилей или зданий есть в старой статье Jack W. Reeves. What is software design (1992) перевод. Основная фишка - что компилятор быстро собирает продукт, поэтому тщательное проектирование становится экономически неоправданным, сильно дешевле делать итерации с выявлением ошибок и отладкой. При этом не просто дешевле по времени, но и заниматься этим могут менее квалифицированные люди - а это очень важно в условиях дефицита кадров, который начался тогда же в 90-х с появлением персоналок и будет еще долго. Да, есть шанс уйти в вечный цикл порождения новых ошибок. Но без этого ты обречен просто НЕ делать.
Система, предлагаемая товарищем - сложна и сильно поднимает требования к квалификации разработчиков. При ней задача создания ERP сложного предприятия становится сопоставима с задачей разработки новой модели самолета или автомобиля. Но больших предприятий, требующих автоматизации - много больше чем создание новых моделей, и каждому нужна относительно уникальная система или конфигурация многих систем.
А еще - в строительной отрасли успешное проектирование идет за счет сопромата (и других теорий) плюс зрелых технологий, обеспечивающих строительство из типовых решений. В ИТ аналога сопромата нет, а технологии незрелые из-за быстрого развития железа. Для уверенного проектирования требуется использовать технологии с TRL не ниже 8 (опыт Боинга), а зрелость фреймворков в мобилках и web - 4-6, они не успевают стабилизироваться.
Было еще несколько тактов обсуждения, с их резюме я начал раздел.
[ Хронологический вид ]Комментарии
Войдите, чтобы комментировать.