Как стать автором
Обновить

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

Обычно администраторы не пишут код, и часто ограничены в выборе решений. Например, какое-то покупное приложение может требовать работы под апачем или IIS, а под nginx разработчик не тестировал, и гарантий не дает. В этой ситуации админ скорее должен знать из каких кубиков-компонентов и как собрать инфраструктуру, чтоб «все работало» и без явных узких мест: может nginx стоит поставить перед апачем в качестве reverse proxy, или как сконфигурировать high-availability, или какие бэкап-решения подходят под требования, и т.п.
Поэтому, unix-way и выигрывает — стандартизация способов взаимодействия, упрощает выбор конкретного «кубика».

И вопрос выбора IIS/apache — проверенные производителем или nginx — не проверенный, сводится к тому, понимает ли админ, что нужно программе, от httpd, и если эти «нужности» обеспечивает IIS/apache, сможет ли обеспечить nginx. А еще лучше, конечно, уметь протестировать.
сможет ли обеспечить nginx
И если сможет, то выбрать apache. Потому что иначе админ затруднит себе общение с техподдержкой, даже если проблема не будет связана с веб-сервером.
хороший критерий, но тоже нужно взвешивать
— как часто требуется общаться с поддержкой, насколько дороже обойдется этот процесс, против преимуществ nginx?
— можно ли поддержку сфокусировать на проблему продукта, а не возможную проблему взаимодействия с «нетестированным компонентом инфраструктуры»?

Ужаснулся от решения, использумого Apache — число одновременно выполняемых запросов ограничено числом процессов. Если каждый из запросов выполняется долго (идёт долгое обращение к удалённой БД, например), то быстро наступит DoS. Но на самом деле такое поведение — это цена за универсальность. Архитектура Apache просто не позволяет использовать кооперативную многозадачность внутри обработчика запроса.

Позволяет. И есть тредные исполнители для врача.

НЛО прилетело и опубликовало эту надпись здесь
Ну поток всё же легче процесса.
а event так вообще фактически поток на исполнение, а не на соединение — то есть keepalive'ы не занимают потоков.

у одного потока на всё тоже есть свои минусы, в том числе — изолязия между исполнителями. или вон недавняя хохма, что текущее время в nginx может начать отставать при недостатке нагрузки.
текущее время в nginx может начать отставать при недостатке нагрузки

Это что-то новенькое. Можно подробнее?

На самом деле время обновляется каждую итерацию цикла обработки событий. В нормальной ситуации события обрабатываются мгновенно. Если у вас итерация обработки событий затянулась на секунды, то скорее всего это следствие кривого стороннего модуля, который заблокировал nginx или сервер уже не справляется с нагрузкой (рабочий процесс nginx оказался выгружен в своп, ему не дают процессорного времени или заблокирован на диске).

Cпасибо, больше похоже на правду. Я еще ни разу не видел отставаний в своих задачах.

Интересно, в xslt модуль «кривой» или нормальный, годный?

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


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

Ну, если совсем по чесноку, то не все =)


На больших нагрузках nginx может блокировать eventloop на:


  • Записи логов. В случае access_log'ов — может сильно помочь директива buffer=. Однако в идеале лучше писать логи напрямую в syslog.
  • Записи тел запросов и ответов на диск. Тут хорошо помогает костыль в виде aio threads. Однако, насколько я понимаю, он не работает для приёма файлов, только на отдачу.
    (Почему костыль? Потому что в идеале aio должно работать через нативный аснихронный интерфейс к файловой системе, а не эмулироваться через thread pool, но в *nix ОС с этим всё плохо).
  • В худшем случае, nginx начинает блокироваться на TLS хендшейках: если вы используете RSA 2048 это примерно 2мс на handshake. В новом OpenSSL 1.1.0 появилось асинхронное API, но в nginx оно если и попадёт, то не скоро. (Патчи по интернетам ходят, но до продакшна я бы их не допускал пока).
  • Были ещё сложные случаи со сжатием, когда люди пытаются максимально сжать статику (например, gzip 9 и brotli 11). в таких случаях сильно лучше статику pre-сжимать в офлайне и использовать gzip_static и brotli_static. Что делать если хочется по-максимуму сжимать динамку пока не понятно, но оно обычно того не стоит. (Можно, наверное, сжимать на backend'е(или маленьком sidecar-демоне), но это значит больше нельзя применять никакие body-filter'ы).
  • Image Filter'ы скорее всего тоже могут блокировать eventloop, но я, если честно, код не смотрел, ибо не использовал — все конторы в которых я работал писали простенькие backend'ы для "тяжёлых" манипуляций типа resize/recompress/crop/etc.

Конечно, при желании, у пользователя есть способы отстрелить себе обе ноги (как тот же gzip 9), но не надо этого делать.


Что касается записи, то есть директива aio_write.

Не переживайте DoS будет, но только не на уровне httpd. httpd обычно не делает запросы к базе. Количество запущенных процессов php/python/ruby ограничено и количество одновременных соединений к базе то же.

Разве mod_php работает в отдельном процессе?

Это не ограничение архитектуры и у apache много решений. В статье речь идёт про используемый по умолчанию prefork, есть ещё event, использующий тот же epoll/kqueue и worker, являющийся чем-то средним между предыдущими. Ну и кучка более экзотических модулей.

Если говорить про все возможные варианты, то ИМХО проблема apache не в том, что он кривой и медленный, а в том, что в попытке объединить в себе web и application сервер, он стал слишком навороченным и сложным.

Можно даже использовать один экземпляр apache с mpm_event/mpm_worker/mpmt_os2/mpm_netware + mod_proxy как проксирующий web сервер, второй с mpm_prefork как application сервер и возможно эта связка не сильно уступит классическим nginx+apache/php-fpm/etc.

Я не фанат apache и предпочту использовать nginx, если такой вариант в принципе возможен, но в контексте данной статья мне показалось уместным упомянуть о том, что не prefork'ом единым жив старый индеец.
НЛО прилетело и опубликовало эту надпись здесь
Большое спасибо за статью. Скажите, в чем смысл регулярного выражения [h]ttp или [n]ginx?
Это очень интересный хак, я хоть и имею огромный опыт в никсах, его отчего-то пропустил. Он позволяет без дополнительных костылей убрать вывод самого грепа в выводе. Пример:
# ps x|grep mysql
24383 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid
25301 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid
29419 pts/2    S+     0:00 grep mysql
# ps x|grep [m]ysql
24383 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid
25301 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid


Заметили разницу? Обычно в примерах в сети видна конструкция «grep something | grep -v grep», а если сделать «grep [s]omething», то something попадет, а сам grep — уже нет, т.к. при строковом сравнении something!=[s]omething.

Это очень поверхностно, можно подробно найти в сети (выше дали неплохую ссылку). Но очень удобно.
А не проще использовать pgrep, вместо двух утилит и хака?
В моем комментарии речь не про ps|grep, а про то, что же такое grep [m]atch

Что же до автора статьи — он решил именно так. Я pgrep тоже использую, но привычка — страшная сила! Да и само по себе это отвечает конвейерной сути никсов: вначале просто ps -aux, потом немного отсортировать и т.п., не всегда стоит задача сразу отгрепать.

В любом случае — спасибо за напоминание. Кто-то начнет с правильных утилит сразу.

Только не ps -aux, если вы, конечно, не хотите увидеть все процессы пользователя x. Есть у ps небольшое западло с поддержкой unix, bsd и long-gnu style options.

Дело не в том кто там что форкает или тредает, дело в архитектуре изначально: если нужно выполнить серверный код, то этот код сам по себе процесс, покуда вы «не перепилили апач» (ну или модуль к нему сделали).

Nginx ставят на статику потому что он не содержит серверной прикладной логики.
Apache форкает потому что ему надо выполнить логику, выполнение которой процесс.

Особнячком node.js со своей асинхронностью.

Конечно, поселектить можно, но любой юниксовый пайп, сокет, ммап и т.п. — это InterProcessCommunication (тот же баш форкает при | grep), то есть если надо выполнить код, отвязанный от обработки запроса, ничем кроме как треда/форка его не сделать. И то если тред, то интерпретатор нужен (как модуль возможно) встроенный. А треды хуже процессов в том плане, что у них общая память с процессом, и соответственно и корка (core dump) общая :)

Это все на самом деле причина того, что бывает масштабируют горизонтально.
Конечно, поселектить можно, но любой юниксовый пайп, сокет, ммап и т.п. — это InterProcessCommunication

Сейчас приложения часто запускаются в виде демонов и сами слушают tcp сокет, в который можно слать запросы по http/fastcgi.
На самом сервере (на машине) может даже не быть веб сервера. Это я про бекенд сервера логики, естественно.

Это не только сейчас, так и 10 лет назад бывало делали (и я так делал), и еще раньше (до меня много раз). Речь-то шла про nginx и apache, и про fork против event loop, как я понял.

Если не отвязывать код от сервера, как в том же node.js, то ту же БД всё равно не встроишь в логику, значит опять пайп, сокет, ммап и т.п.
У select есть большой недостаток — мы не можем знать, какое именно событие произошло и с каким именно сокетом. Каждый раз, когда мы получаем процессорное время, нам приходится обрабатывать все наши коннекты и проверять их на получение данных, делая с них read. Если у нас будет 1000 соединений, а данные придут только по одному из них, то мы обработаем все 1000 соединений, чтобы найти нужный.

Тут написана неправда. Процитирую man: On exit, the sets are modified in place to indicate which file descriptors actually changed status.

Тут написана неправда

Да, неправда — read не нужно делать. Но нюанс всё же есть. В случае select на вход подаётся не список дескрипторов, а битовая маска, она же является и выходным параметром. Битовая маска имеет размер (в битах), равный максимальному количеству дескрипторов (зависит от настроек операционной системы, обычно 1024, но может быть и 8192, и больше). Соответственно, нужно пробегаться по всей маске, чтобы найти все сокеты, по которым произошли события.

По маске пробегать относительно дешево, а написано другое, я же процитировал: нам приходится обрабатывать все наши коннекты и проверять их на получение данных, делая с них read


И код в примере именно так и делает зачем-то.

А, пардон, код в примере маску всё же проверяет. Но комментарий опять же вводит в заблуждение:


//Читаем данные из каждого сокета, так как не знаем какие события заставил ОС дать нам CPU

Ок. Вы изменили комментарий. =)


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

Коллеги, если посмотреть тот же man fd_set, то:
select() has no sigmask argument, and behaves as pselect() called with NULL sigmask.
Описывал я select, а pselect использовал, так как в свежем ядре он заменил select. Суть статьи была не в этом. Но в любом случае спасибо, что читаете. Все комментарии выше относятся к pselect, который используется в исходниках.

sigmask — это вообще про обработчики сигналов. И единственное отличие pselect() от select() в наличии дополнительного аргумента, который позволяет переопределить обработку сигналов на время вызова select().


Опять же, обратимся к man:


sigmask is a pointer to a signal mask (see sigprocmask(2)); if it is not NULL, then pselect() first replaces the current signal mask by the one pointed to by sigmask, then does the "select" function, and then restores the original signal mask.


Ну и man signal процитирую:


Signal mask and pending signals


A signal may be blocked, which means that it will not be delivered until it is later unblocked. Between the time when it is generated and when it is delivered a signal is said to be pending.


Each thread in a process has an independent signal mask, which indicates the set of signals that the thread is currently blocking.

Да спасибо, убрал это предложение из текста.
Ок. Вы изменили комментарий. =)

Да у меня даже и в мыслях не было, что кто-то после select будет проверять каждый сокет по отдельности.


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

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

Хм. Впервые узнал что в Линуксе fd_set является битовой маской...

Можно просто libevent использовать, там уже все написано

Хороший системный администратор при выборе решения при заданных требованиях ориентируется на два условия: минимальное потребление ресурсов и их сбалансированное распределение.

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

Я совсем не системный администратор и мне в статье не хватило еще одного абзаца: вся статья подвела меня к мысли что подход апача плохой и устаревший: плодит процессы, жрет память, а вот ngnix весь такой современный и красивый. Но какова цена всей этой красивости? Или же апач настолько устарел, что во всем проигрывает nginx? Но нет, в конце статьи написано что в некоторых случаях выгоднее использовать только апач. Так в чем же преимущества prefork подхода и в каких случаях он будет уместен? И кстати, а если клонировать процессы не заранее, а по запросу — насколько просядет скорость работы?

Апач выигрывает тем, что это — default сервер. Любой PHP-разработчик параллельно изучает возможности mod_rewrite и сопровождает файлы .htaccess.


При попытке перейти на nginx все эти .htaccess приходится переписывать вручную.

Гм… мы тут, вроде, о системных администраторах говорили. При чем тут разработчики? Все уже разработано — надо только сервер выбрать, под которым все эти разработки крутиться будут.

Так о том и речь. Часть конфига для Apache уже написана разработчиком. Конфиг для Nginx надо писать самому, причем важную часть документации придется получать реверс-инженерингом конфига Apache.

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

Сомневаюсь, что вы купите вместо машины конструктор "собери машину сам", даже если отзывы обещают исключительные ездовые качества.


Если веб-сервер не суметь настроить — он не будет работать. Не смотря на свою крутую архитектуру.


Для настройки Nginx требуется намного большая квалификация, нежели для настройки Apache. Если для нас с вами настройка Nginx не является проблемой — это не значит что с ней справится каждый.

Вас в какие-то дебри понесло — «настройка», «собери сам». В статье говорилось о различиях серверов. При этом мы считаем что админы в состоянии настроить и то и другое — это их профессия, в конце концов. Я задал вполне конкретный вопрос (см. мой первый пост): в каких случаях архитектура apache будет иметь преимущества перед nginx. Трудности настройки и разработки рассматривать я не вижу смысла — будем считать что квалификация разработчиков и администраторов достаточно высока, чтобы не быть препятствием для работы серверов.
Я задал вполне конкретный вопрос (см. мой первый пост): в каких случаях архитектура apache будет иметь преимущества перед nginx.

Я более-менее разбирал этот вопрос в статье.

Спасибо. Правильно ли я понял: если сервер практически не отдает статику и имеет мало памяти, то подход апача с распараллеливанием процессов может обставить по производительности nginx? Или пулы потоков и в этом случае не оставят апачу шансов на реванш?:)

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


Но если сравнивать Nginx и Apache по потенциальным возможностям — то пулы потоков и правда спасут nginx.

Как раз наоборот. Статика + мало памяти — было единственным слабым местом nginx, которое и устранили с помощью пула потоков. =)

Тогда я окончательно запутался — разве медленный бекэнд не приводит к блокированию очереди?
И в каких же тогда случаях имеет смысл использовать apache?

Когда продукт прибит к Apache гвоздями.

Работа с соединениями всегда происходит в неблокирующем режиме асинхронно. Проблема могла возникнуть только с файлами из-за отсутствия аналогичной поддержки со стороны ядра.


В пользу Apache можно привести ряд аргументов:


  1. Можно отдать ему предпочтение при прочих равных, если вы умеете его настраивать и знаете гораздо лучше чем nginx.
  2. Ваш сервис опирается на некоторую функциональность Apache, которой либо нет в nginx, либо она реализована иначе. Распространенный пример в данном случае — это виртуальный хостинг с использованием .htaccess файлов вашими клиентами.
  3. Вы используете Apache в качестве сервера приложений, как менеджер процессов для php, python, java, etc..., чем nginx самостоятельно не занимается, а работает в паре с другими, например php-fpm, gunicorn, wildfly, тем же Apache.

В рунете более чем 76% веб-сайтов обслуживает nginx по крайней мере в качестве фронтенда. В мировом масштабе успехи nginx скромнее, но тенденция такова, что доля Apache стабильно снижается, а доля nginx стабильно растет.

Ваш сервис опирается на некоторую функциональность Apache, которой либо нет в nginx, либо она реализована иначе. Распространенный пример в данном случае — это виртуальный хостинг с использованием .htaccess файлов вашими клиентами.

Другой классический пример — использование mod_jk с ajp для интеграции с Apache Tomcat (прокидывание TLS-сессий в appserver).

https://github.com/yaoweibin/nginx_ajp_module?
TODO
  • SSL

https://github.com/yaoweibin/nginx_ajp_module#todo, т. е. то, ради чего я бы стал связываться с ajp не реализовано.


Плюс оно требует пересборки nginx'а. И поддержка такого проекта без community выглядит довольно печально: некоторые тикеты висят с 14 года.

Другой классический пример — использование mod_jk с ajp для интеграции с Apache Tomcat (прокидывание TLS-сессий в appserver).

Знаю не мало случаев перехода с ajp на nginx проксирующий по http, а вот такой хотелки как-то не припоминаю. Если чего-то очень не хватает, об этом имеет смысл пойти и написать feature request в trac.

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


Мы используем связки nginx + tomcat, nginx + jetty и nginx + wildfly (в зависимости от приложения), везде http от nginx'а до соответствующего upstream'а.


Когда мне нужен был аналог того что apache + tomcat-ajp даёт из коробки, я передавал данные о клиентском сертификате через заголовки и использовал простой сервлет-фильтр для обработки x509 из заголовков.

А что насчет mpm-event в апач начиная с версии 2.4?
По идее апач в этом режиме не должен уступать в производительности на статике если на сервере хватает памяти для всех его worker-threads и listener-threads…

Event MPM работает только для keep-alive соединений. От этого Apache не стал nginx-ом. Обработка запроса в Apache, как происходила многопоточно, так и происходит, и это не масштабируется.

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

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

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

А можно пруф про устаревший fork?
man fork:
fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.

The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes, file mappings (mmap(2)), and unmappings (mun‐
map(2)) performed by one of the processes do not affect the other.


man clone
clone() creates a new process, in a manner similar to fork(2)

Unlike fork(2), clone() allows the child process to share parts of its execution context with the calling process, such as the memory space, the table of file descriptors, and the table of signal
handlers. (Note that on this manual page, «calling process» normally corresponds to «parent process». But see the description of CLONE_PARENT below.)

One use of clone() is to implement threads: multiple threads of control in a program that run concurrently in a shared memory space.

А дальше ОС сама разбирается с памятью, т.к. процесс выделяет не физическую память, а виртуальную. И пока процесс не начал менять страницы в памяти они общие (shared) для parent-а и child.

Вы не тот фрагмент процитировали. Системный вызов fork устарел потому что он практически никогда не делает то, что нужно, делая при этом кучу лишней работы. Вместо него лучше использовать или clone, или posix_spawn.


А в посте, конечно же, ерунда написана.

man vfork

Historic description
Under Linux, fork(2) is implemented using copy-on-write pages, so the
only penalty incurred by fork(2) is the time and memory required to
duplicate the parent's page tables, and to create a unique task
structure for the child. However, in the bad old days a fork(2)
would require making a complete copy of the caller's data space,
often needlessly, since usually immediately afterward an exec(3) is
done. Thus, for greater efficiency, BSD introduced the vfork()
system call, which did not fully copy the address space of the parent
process, but borrowed the parent's memory and thread of control until
a call to execve(2) or an exit occurred. The parent process was
suspended while the child was using its resources. The use of
vfork() was tricky: for example, not modifying data in the parent
process depended on knowing which variables were held in a register.

Вы это к чему? Если вы хотели напомнить про существование vfork — да, это тоже вариант замены устаревшего fork. Но его описание, с моей точки зрения, выглядит как костыль. Тот же posix_spawn выглядит куда красивее архитектурно.


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

вот отсюда http://man7.org/linux/man-pages/man3/posix_spawn.3.html
The posix_spawn() and posix_spawnp() functions are used to create a
new child process that executes a specified file. These functions
were specified by POSIX to provide a standardized method of creating
new processes on machines that lack the capability to support the
fork(2) system call. These machines are generally small, embedded
systems lacking MMU support.

Если не нужно шарить память и прочее с родительским процессом, то clone() не требуется.
C library/kernel differences
Since version 2.3.3, rather than invoking the kernel's fork() system call, the glibc fork() wrapper that is provided as part of the NPTL threading implementation invokes clone(2) with flags that pro‐
vide the same effect as the traditional system call. (A call to fork() is equivalent to a call to clone(2) specifying flags as just SIGCHLD.)

Т.е. GlibС-шный fork() из glibc по факту вызывает clone(2).
Этот пост не про то, как правильно писать программы плодя чайлды, а про то, что делает системный вызов fork (именно про него спрашивают чаще всего на собесах админов, а не про vfork, не про clone, и не про posix_spawn). Но я с удовольствием прочитаю вашу статью на эту тему. Тема, безусловно, интересная.

И что дальше? Хватит говорить цитатами.

2 VBart
Валентин, пользуясь случаем задам вопрос:
http://nginx.org/ru/docs/http/ngx_http_core_module.html#location
Если location задан префиксной строкой со слэшом в конце и запросы обрабатываются при помощи proxy_pass, fastcgi_pass, uwsgi_pass, scgi_pass или memcached_pass, происходит специальная обработка. В ответ на запрос с URI равным этой строке, но без завершающего слэша, будет возвращено постоянное перенаправление с кодом 301 на URI с добавленным в конец слэшом. Если такое поведение нежелательно, можно задать точное совпадение URI и location, например:

Почему 301-й? Дело в том, что в связи с повсеместно используемой браузерами моделью Post/Redirect/Get при отправке POST-запроса клиент получит редирект и последующий GET без аргументов.
Может было бы разумнее в этом случае делать 307-й, чтобы сохранить метод?

Это классическая практика добавлять слэш в конце и традиционно это делается через 301-ый редирект, который кэшируется браузером. Если у вас путь в форме или скриптах приписан неверно (а откуда ещё возьмется POST-запрос?), то нужно править код, а не затыкать проблему редиректом. Редирект на запросы с телом — это вообще плохо, вне зависимости от кода.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории