-Поиск по дневнику

Поиск сообщений в rss_rss_hh_new

 -Подписка по e-mail

 

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 51

Habrahabr/New








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/new/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-new-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

Все, что вы хотели знать о компонуемой инфраструктуре HPE Synergy, в вопросах и ответах

Понедельник, 26 Июня 2017 г. 09:05 + в цитатник
В январе 2017 года Hewlett Packard Enterprise начала поставлять первую платформу для компонуемой инфраструктуры HPE Synergy по всему миру, и на сегодняшний день уже есть ряд заказов в России – как среди крупных компаний, так и предприятий малого и среднего бизнеса. С апреля по июнь мы провели цикл вебинаров, на которых подробно рассказали об архитектуреHPE Synergy, а теперь собрали для вас FAQ – конечно же, с ответами :)



Если понятие компонуемой инфраструктуры для вас в целом новое, рекомендуем ознакомиться с ним подробнее (например, на нашем сайте или скачав электронную книгу «HPE Synergy для начинающих»). К тому же, приглашаем посмотреть записи всех вебинаров, посвященных HPE Synergy. А теперь, собственно, вопросы и ответы с вебинаров:

1. Если нет потребности в 5 стойках (20 корзин), а достаточно 1 стойки (1-2 блэйд корзин), то есть ли смысл использовать HPE Synergy, особенно если в организации статичная инфраструктура, нет такого, что каждый день всё меняется?

Ответ: Synergy отлично подходит и для статичной, и для динамичной инфраструктур. Любая система в некоторой степени динамична – добавляются новые серверы, выводятся из эксплуатации старые. Две корзины – отличный пример для Synergy. Будет 2 мастер-модуля и 2 модуля-спутника. Даже одна корзина Synergy с 6-8 серверами – нормальное стартовое решение, вполне оправданное экономически. Для стоечных серверов мы также предлагаем систему управления на базе OneView. Ограничением по сравнению с Synergy/Blade будет невозможность настроить сетевые подключения (SAN и LAN).

2. Для создания HCI на базе VMware vSAN на каждый серверный слот необходимо 2 выделенных порта 10GbE под трафик vSAN и vMotion. Возможно ли это в Synergy? Сколько сетевых портов на Вычислитель в корзине возможно в HPE Synergy, какие это порты?

Ответ: Если использовать одну пару коммутаторов или модулей Virtual Connect на корзину, то от каждого сервера будет идти по 2 порта 20 Гб/с. Каждый физический порт можно поделить на 4 виртуальных (4 физических функции) и для каждого задать его минимально гарантированную и максимально доступную пропускную способность, шаг 200 Мб/с. Выйти в сумме за 20 Гб/с по минимально гарантированной пропускной способности (МГПС) нельзя. То есть если вы хотите выдать и vSAN, и vMotion по 10 Гб/с МГПС, то это возможно, но больше свободной полосы на другие порты не останется. Если это обязательное условие, то необходимо ставить дополнительные коммутаторы в корзину. Если же будет достаточно, например, 4-5-6 Гб/с на каждый из этих видов трафика, то оставшуюся пропускную способность вы сможете выделить на сеть управления и на продуктивный трафик.

3. Если не предполагается использование внешней СХД, для создания виртуальных сред нужно использовать SDS (HPE StoreVirtual или VMware vSAN)?

Ответ: Если нужно общее дисковое хранилище, то да. Диски из дискового модуля в Synergy подключаются напрямую к выбранным серверам в эксклюзивное пользование.

4. Возможно ли установить в Synergy бездисковые лезвия?

Ответ: Да, можно не ставить диски в сервер. И можно выбрать модель сервера вообще без дискового бэкплейна.

5. По какому протоколу подключаются диски из дискового модуля D3940 к вычислителям (SAS, FC…)?

Ответ: По протоколу SAS с использованием коммутаторов SAS.

6. Подскажите сколько Virtual Connect мастеров можно объединить в IRF стек?

Ответ: IRF – это проприетарная технология для коммутаторов HPE Networking на базе ОС Comware. В VC используется протокол MLAG. Вышестоящие коммутаторы должны использовать протоколы IRF, vPC или аналог.

7. Что будет, если Virtual Connect мастер-модуль выйдет из строя?

Ответ: Каждый вычислитель (сервер) имеет 2 порта, которые подключены к разным мастер-модулям (ММ). Если один ММ вышел из строя, то коммутация и связь с «внешним миром» будет осуществляться через второй ММ.

8. Есть возможность подключить внутреннее хранилище одновременно к нескольким вычислителям?

Ответ: Диски в дисковых модулях можно распределить между всеми вычислителями в шасси, в котором стоит сам дисковый модуль. RAID-группы создает контроллер Smart Array, размещенный в каждом вычислителе. Один набор дисков в дисковом модуле может быть предоставлен только одному вычислителю.

9. Зачем в полностью компонуемой среде используется и 3PAR и StoreVirtual, последний же все равно будет располагаться на 3PAR?

Ответ: StoreVirtual VSA может располагаться на дисках в дисковых модулях самого шасси. Накладывать ограничения, запрещая подключение внешних массивов, смысла нет. Большинство компаний используют в своей ИТ-инфраструктуре внешние дисковые массивы. Пользователь сам вправе выбрать, по какому пути идти – SDS внутри Synergy или классические внешние массивы.

10. Почему сделали SAS хранилище? Почему не FC?

Ответ: Вопрос философский. SAS дешевле и универсальнее внутри корзины. FC или iSCSI может быть воссоздан, используя некоторые серверы, как контроллеры. Существующий пример – StoreVirtual VSA. Кроме того, FC требует использования коммутаторов сторонних производителей (Brocade, Cisco), что уменьшает гибкость общего решения.

11. Есть ли возможность использовать совместно диски NVME и SAS?

Ответ: Да, в варианте: NVMe – внутренние диски сервера, SAS – диски из дискового модуля.

12. Есть ли возможность расширения iSCSI фабрики за пределы стойки (логического шасси)

Ответ: Ограничений нет. Если логические шасси в подключены к одной сети, например, к коммутаторам ядра, трафик iSCSI можно пустить через внешнюю, с точки зрения логического шасси, сеть.

13. Если не планируются дисковые модули в шасси, SAS-коммутатор всё равно обязателен или можно занять слот чем-то другим?

Ответ: Если не планируются дисковые модули, SAS-коммутаторы совсем не нужны. Synergy – компонуемая архитектура. Можно брать только то, что нужно, и в том количестве, в котором нужно.

14. Правильно ли я понимаю, что на 1 сервер при полной нагрузке выходит 1 кВт электроэнергии?

Ответ: Нет. Энергопотребление можно проверить через сайзер для Synergy. Вычислитель Synergy – обычный сервер в своеобразном форм-факторе. Его энергопотребление не превышает потребление обычного сервера. Скорее всего, оно будет ниже за счет использования общих блоков питания для всей корзины, что повышает энергоэффективность решения в целом.

15. Можно ли изменить период в 4 минуты, в течение которых дисковый модуль может находится в открытом состоянии? То есть, если «перетянул»(случайно или намеренно), то шасси аварийно выключится?

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

16. Диски из дискового модуля назначаются серверному профилю произвольно (разные места в дисковом модуле – модулях) или есть какая-то логика?

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

17. При использовании дискового модуля с VSA, какие модули коммутации следует использовать? Обычно же для соединения серверов используются только коммутаторы Ethernet.

Ответ: HPE Virtual Connect SE 40Gb F8 Module или HPE Synergy 40Gb F8 Switch Module в качестве мастер-модулей. HPE Synergy 20G Interconnect Link Module или HPE Synergy 10Gb Interconnect Link Module, как модули-спутники. Для подключения дисков из дисковых модулей – HPE Synergy 12Gb SAS Connection Module.

18. Какое количество внутренних и внешних USB есть в вычислителе SY480?

Ответ: Один внешний и один внутренний USB 3.0
Цитата из QuickSpec:
Micro SDHC Slot – One (1) internal Micro Secure Digital High Capacity (Micro SDHC) card slot
USB 3.0 Port – One (1) internal USB 3.0 connector for USB flash media drive keys
NOTE: The above options are intended for integrated hypervisor virtualization environments.
USB 3.0 Port – One (1) external USB 3.0 connector for USB flash media drive keys

19. В каких сферах лучше применять Synergy? Например, есть ли смысл применять одно шасси Synergy в организации со стандартным набором сервисов: AD, DNS, 1C, СЭД и прочее? Или все-таки можно обойтись стандартными стоечными серверами?

Ответ: Сама архитектура Synergy спроектирована так, чтобы подходить под самый широкий круг задач, исключая узкоспециализированные. Например, для высокопроизводительных вычислений Synergy не будет хорошим выбором. Максимально раскрывается платформа при необходимости достаточно динамично перераспределять ресурсы под разнородные задачи. Однако для статических ландшафтов Synergy также отлично подходит. При использовании системы в связке с дисковым массивом 3PAR заказчик получает дополнительные преимущества за счет единой системы управления ИТ-инфраструктурой и полной автоматизации процесса развертывания ресурсов вычисления и хранения данных. С перечисленными в вопросе задачами Synergy отлично справится.
Но всегда стоит помнить об экономической целесообразности. Synergy оптимальна с финансовой точки зрения по сравнению со стоечными серверами, если заполнение шасси полное или почти полное. Также при сравнении со стоечными серверами надо учитывать особенности архитектуры, например, использование Ethernet 10/40 Гб/с и Fibre Channel 8/16 Гб/с. Перечисленные в вопросе сервисы, с большой вероятностью, не потребуют 12 серверов. Если же общее количество серверов составляет от 8 штук, и в дальнейшем планируется рост их количества, то стоит рассматривать Synergy, как основу вашей серверной инфраструктуры.

20. Возможно ли в Flex-20 получить Storage Network до 16Gb? И планируется ли возможность вывода Native FC 16Gb на порту с конвергентного коммутатора для Synergy?

Ответ: Нет. Модуль HPE Virtual Connect SE 40Gb F8 поддерживает только FC 8 Gb. Поддержка FC 16 Gb планируется в следующем поколении конвергентных модулей. Сейчас для подключения к сетям FC 16 Gb следует использовать либо модуль HPE Virtual Connect SE 16Gb FC, либо SAN коммутатор Brocade.

21. Планируются ли карты с Remote DMA (RoCE, iWARP)?

Ответ: Да, планируются. Во втором полугодии 2017 г.

22. Верно ли, что для того, чтобы все было идеологически правильно, нужно ставить как минимум 2 шасси? Это к вопросу использования резервируемых стримера и компоновщика.

Ответ: Для использования Раздатчика образов (Image Streamer) необходимо минимум 3 шасси для промышленной эксплуатации.

23. Где именно хранится созданный образ ОС?

Ответ: Золотые образы ОС и дельта-диски хранятся на внутренних дисках «Раздатчика образов» (Image Streamer). В Synergy «Раздатчики образов» устанавливаются парами. Библиотека образов синхронизируется между «Раздатчиками» для предотвращения потери данных при выходе одного из них из строя. Для синхронизации данных между «Раздатчиками» и создания снимков томов (дельта-дисков) используется функционал StoreVirtual, встроенный в «Раздатчик».

24. Есть ли запись семинаров, подробнее знакомящих с архитектурой HPE Synergy?

Ответ: Да, конечно. Записи вебинаров для технических специалистов доступны для просмотра on-demand после короткой регистрации:
Часть 1. Обзор
Часть 2. Инфраструктура управления
Часть 3. Организация вычислений и хранения данных
Часть 4. Сетевая инфраструктура
Часть 5. Модуль доставки образов ОС – Image Streamer

Если же после этих FAQ и просмотра вебинаров у вас останутся вопросы по компонуемой инфраструктуре HPE Synergy, ждем их в комментариях!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331628/


Метки:  

Зачем нужен Kubernetes и почему он больше, чем PaaS?

Понедельник, 26 Июня 2017 г. 08:52 + в цитатник


В большой production пришёл не только Docker, но и Kubernetes. И если даже с контейнерами далеко не всегда всё достаточно просто, то уж «кормчий» и подавно остаётся за гранью правильного понимания среди многих системных администраторов, DevOps-инженеров, разработчиков. В этой небольшой статье предпринята попытка ответить на один из вечных вопросов (в контексте Kubernetes) с помощью наглядного объяснения идеи и особенностей данного проекта. Возможно, именно этого вам не хватало для того, чтобы начать плотное знакомство с Kubernetes или даже его эксплуатацию?

Соучредитель и архитектор крупного онлайн-сервиса Box (около 1400 сотрудников) Sam Ghods в своём прошлогоднем выступлении на KubeCon указал на типовую ошибку восприятия Kubernetes. Многие рассматривают этот продукт как очередной фреймворк для оркестровки контейнеров. Но если бы всё действительно было так, то зачем его разработчики неустанно напоминают про «корни Kubernetes API, уходящие в архитектуру*, создаваемую более 10 лет в рамках проекта Google Borg»?..

Google Borg — это менеджер кластеров, предназначенный для параллельного запуска тысяч задач, распределённых по тысячам приложений, запускаемых на многочисленных кластерах. Именно эту архитектуру и унаследовал Kubernetes, перенося кластерные идеи на современную почву контейнеров. Чем же это отличается от многочисленных облачных платформ, существующих сегодня? Начнём с самого понятия платформы.

* Архитектура Kubernetes создана таким образом, что позволяет расширять эту платформу, но разработчиков при этом просят следовать основополагающим принципам.

Kubernetes как платформа


Архитектор Box даёт такой вариант определения: «Платформа предоставляет уровень абстракции, забирающий у вас какую-либо проблему, чтобы вы могли творить поверх неё [не думая о ней]». Примеры: платформа Linux даёт возможность исполнять системные вызовы вне зависимости от аппаратного обеспечения компьютера, а платформа Java — исполнять приложения вне зависимости от операционной системы. Какова же должна быть платформа для запуска приложений, созданных по принципам микросервисной архитектуры?

Ключевые характеристики такой платформы — портируемость и расширяемость. Каждая облачная платформа предлагает свои варианты для достижения этих целей. Например, для автоматического масштабирования у AWS имеются Auto Scaling Groups, у Google Cloud Platform — Autoscaler, у Microsoft Azure — Scale Set, у OpenStack — autoscaling API в Heat. Всё это само по себе неплохо, т.к. потребности выполняются, однако у конечного пользователя начинаются сложности. Чтобы разместить приложение, для каждого сервисного провайдера необходимо поддерживать его механизмы: добавлять поддержку API, учитывать специфику работы используемой реализации и т.п. Вдобавок, всем этим решениям не хватает системы обнаружения сервисов для по-настоящему удобного и автоматизированного развёртывания микросервисов. А если вам нужно быть гибким и иметь возможность размещать приложения в разных окружениях (в публичном облаке, в своём дата-центре, на серверах клиента…)?



В этом заключается первый плюс и суть Kubernetes как платформы, то есть по-настоящему универсальной системы для развёртывания приложений, физическое размещение которых может производиться где и как угодно: на голом железе, в публичных или частных облаках вне зависимости от их разработчиков и специфичных API. Но здорово в Kubernetes не только то, где запускать, но и что: ведь это могут приложения на разных языках и под разные ОС, они могут быть stateless и stateful. Поддерживается принцип «если приложение может запускаться в контейнере, оно должно отлично запускаться в Kubernetes».

Предназначение Kubernetes как платформы — предоставить базовую, абстрактную платформу как услугу (Platform as a Service, PaaS), сохранив гибкость в выборе конкретных компонентов этой PaaS.

Kubernetes и PaaS


О том, что Kubernetes не является традиционной PaaS, рассказывается в документации проекта, где поясняется, что авторы стремятся сохранить возможность пользовательского выбора в местах, где это важно. В частности, поэтому:
  • Kubernetes не предлагает никаких встроенных служб для обмена сообщениями, обработки данных, СУБД и т.п.
  • Kubernetes не имеет своего магазина с готовыми сервисами для деплоя в один клик.
  • Kubernetes не деплоит исходный код и не собирает приложения. Процессы непрерывной интеграции (CI) поддерживаются, но их реализация оставлена для других инструментов.
  • Аналогично для систем журналирования и мониторинга.

Таким образом, если в PaaS обычно делается акцент на предоставление функциональных возможностей, то в Kubernetes первичен универсальный, абстрактный подход. Несмотря на то, что Kubernetes предлагает ряд функций, которые традиционно присущи PaaS: развёртывание приложений, масштабирование, балансировка нагрузок, журналирование и т.п., — платформа является модульной и предлагает пользователям самим выбирать конкретные решения для тех или иных задач. Такой подход сделал Kubernetes базой для таких PaaS, как OpenShift от Red Hat и Deis.

Заключение


Kubernetes следует принципу, к которому обычно (впрочем, не всегда) стремятся в стандартах. Его хорошо проиллюстрировал Бьёрн Страуструп в своей классической книге «The C++ Programming Language»:
Что должно быть в стандартной библиотеке C++? Идеалом для программиста является возможность найти каждый интересный, значимый и разумно обобщённый класс, функцию, шаблон и т.п. в библиотеке. Однако вопрос не в том, «что должно быть в какой-то библиотеке», а в том, «что должно быть в стандартной библиотеке». И ответ «Всё!» — первое разумное приближение к ответу на первый вопрос, но не последний. Стандартная библиотека — то, что должен предоставлять каждый автор и делать это так, чтобы каждый программист мог на это положиться [т.е. действительно нуждался в этом — прим. перев.].

Применительно к Kubernetes можно сказать, что эта система — фундамент, та самая «стандартная библиотека» для построения PaaS и других подобных решений.



P.S. Если хотите разобраться в техническом устройстве и практике применения Kubernetes — смотрите видео из моего недавнего доклада «Наш опыт с Kubernetes в небольших проектах». Для более подробного и глубокого погружения мы готовим цикл статей по Kubernetes — подписывайтесь на хаб и ожидайте их уже в ближайшее время.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/327338/


Метки:  

«База знаний»: 100 практических материалов по безопасности, экономике и инструментарию IaaS

Понедельник, 26 Июня 2017 г. 08:51 + в цитатник
Сегодня мы подготовили мегаподборку из материалов «Первого блога о корпоративном IaaS». Здесь собраны руководства, практические советы по ИБ, сетям и железу. Плюс мы привели наиболее интересные кейсы в сфере взаимодействия бизнеса и IaaS-провайдера.

/ фото Leonardo Rizzi CC

Информационная безопасность






Процессы и экономика





Сети и железо





/ фото gothopotam CC

Практические кейсы









Производительность




Аналитика и тренды






Специализированные решения



Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331620/


Метки:  

Платформа ServiceNow: Искусственный интеллект автоматизирует рабочие процессы

Понедельник, 26 Июня 2017 г. 08:35 + в цитатник
Компания ServiceNow дополнит технологиями машинного обучения свой продукт Now Platform, предназначенный для автоматизации бизнес-процессов, предотвращения возможных простоев у клиентов, перенаправления запросов на обслуживание, а также оценки эффективности IT.

Изображение Matthew Hurst CC

Функции искусственного интеллекта будут предоставляться с помощью грядущей платформы Intelligent Automation Engine, которую компания анонсировала на конференции Knowledge. По словам представителей компании, это решение укрепит позиции ServiceNow на рынке управления IT-сервисами и откроет путь к другим частям корпоративной сферы.

«Сферы обслуживания клиентов и управления персоналом хорошо поддаются автоматизации, — отмечает технический директор ServiceNow Аллан Лейнволд. — Но бизнес тратит большое количество времени на решение типовых, рутинных задач, например на запросы информации об отгулах к отделу кадров».

Алгоритмы Intelligent Automation Engine основаны на решениях компании DxContinuum, приобретенной ServiceNow и занимающейся разработкой технологий машинного обучения.

Новые сервисы будут направлены на решение следующих задач:

  • Предотвращение простоев. Благодаря технологии выявления аномалий решение Intelligent Automation Engine определяет паттерны, которые с высокой долей вероятности ведут к простоям. Эта функция появится на платформе ServiceNow Jakarta, которая будет выпущена в конце года.

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

  • Прогнозирование эффективности. Алгоритмы будут работать с приложениями Performance Analytics от ServiceNow, способными отслеживать статистику в реальном времени. Они помогут компаниям определять момент достижения заданных уровней эффективности (например, времени ответа службой поддержки).

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

ServiceNow за последний год приобрела и несколько других компаний (помимо DxContinuum), включая поставщика средств IT-безопасности BrightPoint Security и компанию, занимающуюся разработкой инструментов для управления облаком ITapp.

«Покупая компанию, мы первым делом переписываем ее продукты под свою платформу, — рассказывает директор по стратегии ServiceNow Дэйв Райт (Dave Wright). — Такой подход исключает неразбериху с ворохом плохо интегрированных продуктов».

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

Пара наших хабрадайджестов:

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331626/


Метки:  

Spark-in.me. Часть 4 — Базовое админство для обычных человеков

Понедельник, 26 Июня 2017 г. 07:01 + в цитатник


У слова backup есть несколько значений, как и у других похожих слов, например setup — это еще и «прикид», а не только набор настроек, устройств и еще бог весть чего.



Статьи цикла
  1. Spark-in.me. Часть 1 — Зачем и почему?
  2. Spark-in.me. Часть 2 — Архитектура приложения и структура БД
  3. Spark-in.me. Часть 3 — DIY поддержка и админство сайта
  4. Spark-in.me. Часть 4 — Базовое админство для обычных человеков
  5. Spark-in.me. Часть 5 — Переход на HTTPS
  6. Spark-in.me. Часть 6 — Исходный код и настройка бекенда
  7. Spark-in.me. Часть 7 — Исходный код и настройка фронтенда




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


image

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


image
Купаться в тазиках можно по-разному...

Я уже успела упомянуть логи. Логи — это такие летописи всякой ерунды, происходящей в вашей системе. Ведут эти летописи как сама система, так и разного рода приложения. Приложения могут писать в отдельные файлы, а могут и в системные. Зачем нужны логи? Логи позволяют «найти все» — найти ошибку и подробности ее возникновения, что здорово помогает затем ее локализовать и устранить. Логи незаменимы, когда ошибку трудно воспроизвести и кажется, что условием возникновения ошибки является святой рандом. Есть целые энтерпрайз решения для хранения, структуризации и анализа логов it-систем, и, надо сказать, это очень удобно, здорово и дорого.


Бекапы бд Postgres


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


Мы не админы, мы обычные человеки, которые поставили себе прекрасную СУБД. Но данные терять не хотим и не хотим предоставить воле случая возможность испепелить наше детище. Что же делать? Настраивать бекапы и логи, конечно же. В самом простом и неказистом виде.


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


Положим, я создала в кластере две базы данных. Я делаю бекап каждой из них и складываю бекапы в папку, папку называю текущей датой. Делается это так:


TIME=`date '+%m-%d-%Y'`
mkdir -p /path/to-your-db-backups/$TIME
pg_dump mydb1 > /path/to-your-db-backups/$TIME/mydb1.sql
pg_dump mydb2 > /path/to-your-db-backups/$TIME/mydb2.sql

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


Чтобы делать это автоматически, лучше настроить ssh без пассфразы (кому интересно, почитайте про Алису и Боба и RSA, стандартная штука). Хороший и простой гайд, который я периодически навещаю, — тут. Нужно только определиться, где вы будете выполнять скрипт бекапа. Делать это можно как на системе, которую вы бекапите, так и на системе, в которую вы складываете бекап. Для переноса файлов используйте secure copy — scp. После переноса не забудьте удалить файл из системы, которую бекапили, чтобы не захламлять. И будьте осторожны с удалением.


Вот пример скрипта, который бекапит одну из наших удаленных баз. Скрипт запускается по крону на домашнем сервере, выполняя ssh-команды на удаленном сервере.


TIME=`date '+%m-%d-%Y'`
mkdir -p /path/to/your/backups/local/backup-$TIME
ssh sshuser@remote "pg_dump mydb > /etc/postgresql/x.y/backups/backup-$TIME.sql"
scp sshuser@remote:/etc/postgresql/x.y/backups/backup-$TIME.sql /path/to/you/backups/local/backup-$TIME
sleep 2m
ssh rsshuser@remote "rm /etc/postgresql/x.y/backups/backup-$TIME.sql"


Вот пример скрипта, который бекапит одну из наших систем целиком. Скрипт запускается по крону на домашнем сервере, выполняя ssh-команды на удаленном сервере.


#START
# This Command will add date in Backup File Name.
TIME=`date +%b-%d-%y`
# Here i define Backup file name format.
FILENAME=some-system-backup-$TIME.tar.gz
# Backed up folder (system root) location
SRCDIR=/
# Destination of backup file
DESDIR=/home/backups
# exclude folder list
EXCLUDE='--exclude /home/backups --exclude=/another'
#  Do not include files on a different filesystem
ONEFSYSPARAM='--one-file-system'
ssh sshuser@remote "tar -cpzf $DESDIR/$FILENAME $EXCLUDE $ONEFSYSPARAM $SRCDIR"
scp sshuser@remote:$DESDIR/$FILENAME /place/backup/here/
ssh sshuser@remote "echo 'WHOLE_SYSTEM_BACKUP is successful: $(date)' >> /home/bash-scripts/cron_log.log"
ssh sshuser@remote "rm $DESDIR/$FILENAME" #END


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


Ну и наконец не забывайте о порядке. Лучше всего делать бекапы автоматически и так же автоматически их удалять. Закиньте скрипты в крон (crontab -e). Храните не один последний бекап, храните несколько. Пример команды для автоматической чистки, которая удаляет все кроме последних 4 файлов в директории (настоятельно рекомендую прочесть, почему не надо парсить ответ ls и почему эта команда не подходит для бекапов с названиями, содержащими пробел):


DIR=/path/to-your-backups/somesystem/somedate
ls -dt $DIR/* | tail -n +5 | xargs rm -r --


И, конечно же, не забывайте просматривать свои бекапы вручную время от времени. Они могут перестать создаваться — смотрите на даты. Заглядывайте в папки на предмет отсутсвия содержимого. Еще рекомендую чекать размер бекапов. Если бекап три дня назад весил 14 гб, а сегодня 9 гб, при этом вы не помните масштабной чистки в системе, это повод задуматься. Если бекап маленький, можно даже заглянуть внутрь архива.



Разворачиваем бекапы


Я ни разу не восстанавливала систему из архива файлов, не пробовала. Знаю только, что для этого нужен загрузочный диск и физический доступ к серверу) Тут я ничего особо не смогу рассказать. Вероятно, cтоит как-нибудь научиться этому на примере распберри. Статьи, обязательные к прочтению, на эту тему — раз, два, три.


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


tar -cpzf /destination/directory/filename --exclude /exclude/directory1 --exclude /exclude/directory2 --one-file-system /source/directory


Мне эти бекапы нужны в одном случае — когда мои бекапы (то есть бекапы базы данных) утеряны или не существовали никогда (было такое).


Обычные sql дампы разворачиваются очень легко. Надо заметить, что сначала нужно дропнуть остатки старой базы, чтобы не возникло конфликтов. Затем создать ее заново и развернуть в нее дамп:


psql mydb1 < /path/to-your-db-backup/mydb1.sql


Что касается разворачивания базы из архива, то это можно сделать, надо лишь учесть несколько нюансов. Восстановите данные из /var/lib/postgresql, а настройки из /etc/postgresql (если ваши директории нестандартные — восстанавливайте оттуда). Если настройки утеряны — восстановите вручную, по сути, просто нужно поправить конфиг. Юзеров в моем случае приходится пересоздавать. Все файлы должны иметь соответсвующие настройки доступа (chmod в помощь) и оунером, конечно же, должен быть юзер postgres (поможет команда chown).


Логирование в postgres


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


В постгрес логи настраиваются очень просто, через postgresql.conf в разделе error reporting and logging. Вам просто нужно задать удобные для вас настройки. Лучше всего в таком случае открыть документацию и выбрать нужные настройки. Рекомендую настроить логи по дням, хранить неделю (в документации есть пример того, как это сделать), а дальше в зависмости от уровня паранойи и уровня загрузки базы: можно логировать любые входы, любые запросы, а можно логировать только запросы, которые выполняются долго. Еще можно логировать дедлоки (почитайте про взаимные блокировки). Такие штуки постгрес разруливает сам, но в общем, не забывайте про их существование.


Ну вот и все. Можно еще рассказать немного про юзеров, о том, как их настроить, чтобы не ломали в общем случае ничего. Ну и убийство зомби-коннектов.


Создать пользователя — минимум прав и чтение базы


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


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


create role somerole nocreaterole nocreateuser nocreatedb noinherit login noreplication connection limit 5 password 'somepassword';
alter role somerole set statement_timeout=30000;
grant select on all tables in schema public to somerole;

Терминировать зомби-коннекты


Зачастую так бывает, что коннект был создан, но не был закрыт. Такие коннекты продолжают висеть некоторое время, потом постгрес сам их находит и предает праведному огню. Чтобы такие коннекты не плодились почем зря, мы и задали нашему юзеру ограничение по количеству коннектов. Тем не менее, может случиться так, что нужно все-таки вручную запустить чистку зомбаков. Я использую решение отсюда, его можно подредактировать под ваши нужды.


WITH inactive_connections AS (
    SELECT
        pid,
        rank() over (partition by client_addr order by backend_start ASC) as rank
    FROM 
        pg_stat_activity
    WHERE
        -- Exclude the thread owned connection (ie no auto-kill)
        pid <> pg_backend_pid( )
    AND
        -- Exclude known applications connections
        application_name !~ '(?:psql)|(?:pgAdmin.+)'
    AND
        -- Include connections to the same database the thread is connected to
        datname = current_database() 
    AND
        -- Include connections using the same thread username connection
        usename = current_user 
    AND
        -- Include inactive connections only
        state in ('idle', 'idle in transaction', 'idle in transaction (aborted)', 'disabled') 
    AND
        -- Include old connections (found with the state_change field)
        current_timestamp - state_change > interval '5 minutes' 
)
SELECT
    pg_terminate_backend(pid)
FROM
    inactive_connections 
WHERE
    rank > 1 -- Leave one connection for each application connected to the database

Остается только пожелать удачи и поменьше тазиков =)

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330742/


Метки:  

Spark-in.me. Часть 1 — зачем и почему?

Понедельник, 26 Июня 2017 г. 07:01 + в цитатник
Мы сделали сайт spark-in.me весьма современным и прогрессивным образом и хотели бы поделиться с вами как, зачем и почему. Эта статья будет посвящена вопросу «зачем и почему»?

Остальные статьи цикла будут посвящены вопросам «как» и деталям практической имплементации.

Статьи цикла
  1. Spark-in.me. Часть 1 — Зачем и почему?
  2. Spark-in.me. Часть 2 — Архитектура приложения и структура БД
  3. Spark-in.me. Часть 3 — DIY поддержка и админство сайта
  4. Spark-in.me. Часть 4 — Базовое админство для обычных человеков
  5. Spark-in.me. Часть 5 — Переход на HTTPS
  6. Spark-in.me. Часть 6 — Исходный код и настройка бекенда
  7. Spark-in.me. Часть 7 — Исходный код и настройка фронтенда








Посмотрите короткую вырезку из фильма Blade Runner по ссылке выше.

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

  • Не гены делают человека, а его поступки, мысли и вклад во что-либо значимое;
  • Не факт наличия генов или юридические права делают человека человеком, а его вклад в некое дело и то, как он взаимодействует с обществом (посмотрите это кино, если вам интересна эта идея);
  • В современном мире, который уже не так далек от фантастики людям зачастую важнее виртуальные и идеи, сообщества и общение. Вместо имплантантов в мозг вы просто носите с собой смартфон. Зачастую важнее бывает, что происходит с вашими друзьями далеко от вас, нежели чем в непосредственной близости;
  • Очень многие люди являются мастерами своего дела и постоянно видят что-то прекрасное, удивительное, новое, свежее. Они создают что-то уникальное и новое, находят маленькие вещи, которые никто до них не видел;
  • Иногда наоборот по жизни мы встречаем такие сгустки сконцентрированного треша и угара, что избегание такого же тоже является чем-то прекрасным — пример;
  • Но мир работает таким образом, что зачастую вы не можете поделиться этим прекрасным даже в интернете, потому что каналы информации забиваются рекламой, информационным шумом, помоями, платным контентом, политикой, ложью. Список бесконечен;
  • В интернете как нельзя прекрасно работает принцип 20-80 (закон Парето, распределение Пуассона — называйте как хотите). Простыми словами — чтобы докричаться до кого-то, нужно кричать в 10, 100, 1000, ..., 10^n раз громче каждый раз. Каналы информации нелинейны и монополизируются деньгами и шумом;
  • По идее наука и научный подход должны решать такую проблему (на самом деле нет — это видео и канал про доказательную медицину намекают, какие части системы не работают в средне-срочной перспективе, но работают в долго-срочной). Но на практике, учитывая мой бекграунд и то, что в России не финансируются фундаментальные исследования и есть парадокс в том, что математики и люди «про сложные вещи и данные» или стоят дешево или нужны нескольким крупным компаниям, где неинтересно и нет свободы принятия решений, получается что можно получать очень очень мало и заниматься интересным с нулем перспектив или искать свой путь. Я за поиск своего пути;
  • Вообще в более крупных и зрелых бизнесах как правило в определенный момент происходит подмена понятий (коммунисты по призванию сменяются коммунистами по названию =) ) — и фанаты своего дела сменяются безликими ремесленниками, которые правильно продали себя HR-ам, которые не понимают ничего в предмете;
  • В принципе тот факт, что при общей «бедности» населения с точки зрения бизнеса то, что мне интересно (данные, алгоритмы, наука о данных, применение данных при принятии решений) нужно только крупным компаниями сразу накладывает ограничения на развитие в этом направлении;


Собрав это все воедино в голове несколько месяцев назад у меня в голове возник некий план:

  1. Нужно выкладывать все самое лучшее из своих наработок и из найденного в интернете на канал и на свой сайт, параллельно получая самообразование и делясь своими наработками, так, чтобы это в принципе не мешало основной работе;
  2. Содержимое канала в телеграме должно индексировать поисковиками. Спасибо этим людям за сервис постинга контента канала сюда (кстати есть уже второй автор на сайте со своей лентой тут);
  3. Чтобы было future-proof надо иметь свои АПИ, базу данных, CMS. Морду всегда можно поменять на новую. А подаренный другим платформам контент — тяжело вернуть;
  4. Плагины для комментариев, рассылки, онлайн аналитику лучше отдать условно бесплатным сервисам (ибо там работы настолько много, что ужас). Список таких сервисов, которые я использую: disqus, google analytics, tinyletter;
  5. Фронтенд конечно можно написать на PHP, но с точки зрения скорости и future-proof и собственного развития — я выбрал react.js;


Изначально предполагался такой список фич (гугл-док), но некоторая часть из них ушла в бой, часть показала свою бессмысленность. Краткое саммари, того что «прилипло к стене»:

Список основых фич Что в итоге с ними стало
База, структура
  • Написал сам + с девушкой. Девушка админит базу, я — все остальное
  • Все хостится на VDS за 5 баксов, деплой и старт делается примитивными bash-скриптами
  • Про нашу логику бекапов — отдельная статья

АПИ, проверки, логирование Написал сам + взял свой код из прошлых проектов для АПИ
Сессии и запоминание юзера PHP, управление юзерами, права сессий Написал сам + взял свой код из прошлых проектов для АПИ
Клиентская часть админки, CMS Сделал через одно место сам (я ноль в JS и фронтенде) используя свои прошлые наработки и этот фреймворк
Шаблон блога Взял отсюда
Морда на react.js Заказал у этого разработчика . Был на 95% доволен работой.
Фичи:
  • SEO (og, ld-json, schema.org)
  • Сайтмапы
  • Теги и облака тегов
  • Похожие авторы, похожие статьи, полнотекстовый поиск
  • RSS

Все сделал сам сочетанием тулзов
  • Bash скрипты
  • php
  • Большая часть завязана на SQL запросах (а что вы хотите — я аналитик)
  • Проброс на морду — помогало несколько человек

Интеграция с телеграмом
  • Сделана через этот сервис.
  • От идеи мультипостинга из своей админки на канал, в ВК и ФБ автором отказался из-за сложности и корпоративной политоты при интеграции с ФБ, к примеру.
  • У ВК я смог найти окольные методы, чтобы постить на свою ленту (там это немного закрыто в АПИ), но потом я понял что аудитория в ВК и ФБ полностью увлечена треш-пабликами и рекламой и с ними не стоит конкурировать.
  • Общества во ВК скорее мертвы в 2017 году. А жаль.

Комментарии, подписка, аналитика
  • disqus
  • google analytics
  • tinyletter
  • серверные логи вызовов АПИ прописанные в логике АПИ



Изначально я хотел купить домен в .space (меня вдохновил лютый сайт spacemorgue.com), но потом когда я пропустил все самые классные домены, которые я нашел, я сменил решение.

Смеха ради список доменов, которые мы рассматривали есть тут. Я как-то все пропустил и не решился купить такие домены:

  • name.it
  • implo.de
  • explo.de
  • chri.st
  • lemona.de
  • voi.de
  • sha.de
  • fa.de


Итоговый домен spark-in.me я нашел случайно в последнюю минуту когда деплоил АПИ. Как оказалось, потом поиск выдал некий бизнес snake-oil проект по домену spark.me — вряд ли получится его купить.

Вот в принципе все про зачем и почему. Дальше уже будут более приземленные детали.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331574/


Метки:  

Spark-in.me. Часть 2 — Архитектура приложения и структура БД

Понедельник, 26 Июня 2017 г. 07:01 + в цитатник
image
Статьи in a nutshell

Описав зачем и почему мы решились сделать свой фан-проект можно попробовать начать описывать как мы делали конкретные вещи.

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

Статьи цикла
  1. Spark-in.me. Часть 1 — Зачем и почему?
  2. Spark-in.me. Часть 2 — Архитектура приложения и структура БД
  3. Spark-in.me. Часть 3 — DIY поддержка и админство сайта
  4. Spark-in.me. Часть 4 — Базовое админство для обычных человеков
  5. Spark-in.me. Часть 5 — Переход на HTTPS
  6. Spark-in.me. Часть 6 — Исходный код и настройка бекенда
  7. Spark-in.me. Часть 7 — Исходный код и настройка фронтенда




0 Введение



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

На практике часто получается немного иначе:
  • Мы умеем то-то и у нас есть небольшая кодовая база;
  • Мы понимаем, что есть такие-то потребности в виде списка, но формализация всего по времени сопоставима с непосредственной имплементацией;
  • Поэтому лучше планирование и исполнение заложить в несколько коротких итераций;
  • Готовые решения не нравятся по причине отсутствия каких-либо фич или отсутствия владения контентом;


Сразу скажу, что с точки зрения дизайна самого приложения / архитектуры я частично вдохновлялся такими примерами:

  • Админка медузы и сама медуза как самое прогрессивное с технической точки зрения СМИ (про контент молчу, он везде одинаковый);
  • Тем, как работала публикация на моем прошлом месте работы;
  • Принципом максимального отсутствия бизнес-логики в business tier и на клиенте — по сути большая часть новых вещей просто берется из базы и прокидывается в АПИ и на морду (зачем плодить уровни, если я один и контент простой?);
  • Не возьмусь вспоминать где это сделано лучше всего, но все SEO фичи должны быть с самого начала (RSS, Sitemap, schema.org/ld+json, og-теги, мета-теги);


Из этого небольшого списка следуют такие принципы:
  1. Должно поддерживаться много авторов;
  2. Создание статей делается в простом WYSIWYG редакторе, картинки заливаются отдельно на страничке;
  3. Контент должен быть максимально отделен от формы, и желательно представим во всех возможных форматах без проблем (на практике, остался формат статей HTML, который всеяден, канал в телеграме, его веб-трансляция, и email-подборки статей с канала и сайта. От идеи мультипостинга я отказался, т.к. там слишком высокие пороги на вход и поддержку тучи социальных аккаунтов, хотя технически можно это сделать это хоть сейчас — собственно идея мультипостинга и породила мысль о том, чтобы использовать имеющуюся кодовую базу в таком формате);
  4. Контент должен быть представлен в такой форме, чтобы любой в интернете мог им поделиться, и при этом автоматически бы подтягивались open-graph теги;
  5. Нужно хранить минимально нужное количество SEO сущностей в базе, чтобы не пришлось потом их разделять;
  6. Принцип расширяемости и отделимости каждого компонента приложения от другого. Не совсем соблюдается, т.к. очень много завязано на PostgreSQL;
  7. Каждая единица контента должна иметь уникальный URL (главная, авторы, теги, статьи, фиксированные страницы). При этом никто не мешает делать автоматизированные подборки (сейчас есть теги и группы тегов + страницы авторов, по идее надо делать также страницы групп тегов);
  8. Добавление любой, даже самой лютой фичи, должно сводиться к простейшему набору из действий: i) Набор таблиц ii) Набор методов АПИ iii) Набор предобработок по крону или операций с данными (пока не нужно) iv) Прокидывание в админку или на морду
  9. Если нужно замутить какой-то динамический или нестандартный контент, то это должно делаться настройкой отдельного виртуального сервера и прокидыванием такого контента. Примеры раз и два
;

Несложно увидеть, что современные решения типа Medium, Tumblr или менее современные типа Wordpress не совсем этим принципам удовлетворяют. Самая главная проблема таких систем — по сути вы не владеете своим контентом или не можете ничего поменять или добавить. Я сталкивался с несколькими open-source минималистскими приложениями для блогов, которые в принципе можно использовать, но если они написаны на языке, который вам незнаком, то добавление, допустим, блока «похожие статьи» может стать путешествием в ад.

Имея эти требования в голове, выбор на такие технические решения:
  • База PostgreSQL — возможно самая продвинутая open-source база в мире, которая сочетает в себе как реляционную базу так и много нереляционных фич;
  • PHP для бекенда админки и для АПИ как самый простой, документированный и быстрый в применении и освоении инструмент;
  • ReactJS для морды (хотя можно было бы на PHP все сделать). Изначально предполагалось сделать второй фановый проект, где нужны были приложения через React Native, но он отвалился;


Давайте теперь пройдемся по каждому компоненту отдельно. Я понимаю, что для части вещей сделанное может показаться overkill'ом, но часть этой кодовой базы у меня просто была, часть я дописывал с целью сделать тот же мультипостинг и иметь «правильное» АПИ и прошивку.

1 Статьи


image

  1. Изначально планировалось, что будет мульти-постинг, поэтому тут есть ключ для публикации и цели публикации. На практике используется только публикация на сайт (совершенству нет предела, но никто из нас, как мне кажется, не будет поддерживать паблики ВК и ФБ);
  2. Статья — основная сущность, она публикуется в теги на определенном языке;
  3. У тегов есть типы тегов;
  4. Также глобальные переменные хранятся в виде json в таблице;
  5. Автор имеет пользователя, от имени которого он входит в админку;
  6. Обратите внимание, что основные SEO-сущности хранятся в таблицах;


2. Пользователи, права и сессии пользователей

image

  1. У пользователя есть роли, которые объединяются в права. По сути получается, что любой пользователь может иметь любые права через роли;
  2. Проверка доступности методов делается по статической сессии;
  3. Для пользователей сайта статическая сессия создается заранее, пользователи работают по ключу (обратите внимание, я знаю про разные криптографические методы, динамические сессии, токены, protobuf и прочее — но я не использую это для простоты приложения — это же информационный контент, зачем его прятать?);
  4. При входе в админку у автора создается сессия и прописывается в cookies. При повторном заходе не нужно вводить пароль, если в браузере есть кука;


3. Методы АПИ


image

Самая неоднозначная вещь, поскольку по большей части состоит из legacy кода, написанного для другой цели. Архитектура вкратце написана для того, чтобы:

  • Поддерживалась версионность методов АПИ с логами прошлых версий, чтобы не возиться с поддержкой;
  • Добавление метода АПИ сводилось к:
    1. Внесению записей в таблицы и созданию прав;
    2. Прописыванию текста фиксированного запроса в таблицу или создании специальной кастомной функции;
    3. По сути структура выше описывает хранимые php-функции АПИ и тексты простых функций АПИ в виде запросов, хранимых в базе;
  • Да, в базе хранятся права доступа к этой же базе. Но запросы с такими данными ходят только в локальной сети, поэтому это не должно быть проблемой;
  • Значительная часть логики тут разнесена так:
    1. Большой класс apiHandler.php, который принимает внешние запросы, проверяет их на валидность, форматирование, верные параметры и выбрасывает ошибки, как в логи так и обратно;
    2. Большой класс apiExecutor.php, который в случае наличия валидного запроса этот запрос исполняет. Нетрудно догадаться, что изначально между этими двумя сущностями планировался балансировщик нагрузки, который просто пока не нужен;
  • Работа с ошибками вынесена в отдельные классы и сущности, которые будут описаны ниже;
  • Тут используются функции PostgreSQL по работе с json — многие конфиги и параметры хранятся в виде json, чтобы обеспечить развиваемость и обратную совместимость (и отсутствие геморроя для меня =) );


В общем случае работу АПИ можно представить так:

  • Приходит запрос. Проходит простейший фаервол (все ненужные порты закрыты);
  • В виде большой древовидной структуры запрос парсится и проверяется на:
    1. Валидность типа запроса и его содержимого;
    2. Валидность типов параметров и их содержимого;
    3. Наличие обязательных параметров;
    4. Наличие прав у запрашивающего;
  • Если все проверки пройдены, в локальной сети отправляется запрос в apiExecutor.php (разделение сделано было в legacy коде с определенной целью), который делает ряд проверок, смотрит где найти запрос для функции или функцию на php, которую нужно исполнить;
  • Естественно везде стоят проверки на SQL-инъекции, проверяется искейпинг и прочее;
  • Функции, которые хранятся в виде запросов, естественно имеют экранированные параметры;


Пример параметров одного из запросов АПИ — я не большой фанат технических извращений, по мне простой json внутри POST запросов нормально работает.

{  
	"key": "very_secret_key",
	 "method": {
	     "name": "getTagByAlias",
	      "version": 1  
	      },
	      "params": {
	          "targetId": 2,
	           "tagAlias": "not-buying-bs",
	            "getFullArticles": 1  
	      }
}


Статистика по вызовам запросов АПИ на момент написания статьи
  • getSimilarArticlesByArticleAlias 25421
  • getBlogObjects 17075
  • getAuthorByAlias 12977
  • getArticleByAlias 11412
  • getArticleFeed 7124
  • getTagInfo 3989
  • getTagByAlias 1628
  • getArticleById 717
  • getBlogSearch 336
  • getMyArticles 290
  • getRussianDsCourses 278
  • getArticlesByAuthor 271


Понятное дело, что в такой архитектуре рука так и просится спросить, а почему методы АПИ не являются хранимыми процедурами? Ответ прост — потому, что мне быстрее написать функцию на php или в примитивном случае запрос, чем хранимую процедуру. По идее наличие хранимых процедур увеличило бы независимость от СУБД, но это не же не коммерческое решение с командой разработчиков =).

Интересным образом я решил вопрос генерации SEO — обвеса страниц. По сути поставил себе пари — смогу ли я все запихать внутрь 1 SQL запроса, хоть и динамического? В итоге смог. При разбитии на языки навереное будет геморрой, но он может решиться простым разнесением на пару функций (да, я не программист, но я как-то не верю в идеальное ООП, мне больше скорее нравится идея микро-сервисов и «сгустков сути», а не красивого кода).

Вот пример самого извращенного SQL-запроса (вместе с php кодом, чтобы было понятно что к чему). Запрос выдает тег по алиасу и schema.org SEO обвес:

многобукаф
/*
{"params":{"targetId":"integer"}}
*/
function getTagByAlias($params){

	/*
	Check necessary param consistency
	*/

	if ( !isset($params['targetId']) ) {
		try {
	        $erLogData = new ErLogData(
	        				'API_INTERNAL_PARAM_TRANSMISSION_ERROR', 
	                        57, 
	                        get_class($e), 
	                        __FILE__, 
	                        __CLASS__, 
	                        __FUNCTION__, 
	                        __LINE__, 
	                        date('Y-m-d H:i:s')
	                        );            
	        $erLog = ErLogFactory::create($erLogData, $e);
	        $response = json_encode(array('response' => array('error' => ['message' => $erLog->_user_message, 'code' => $erlog->_code])), JSON_UNESCAPED_UNICODE);
	        throw $erLog;
	    }
	    catch (ErLog $ee) {
	        ErLog::full_log_v1($ee);
	        return $response;
	    }
	}

	if ( !isset($params['tagAlias']) ) {
		try {
	        $erLogData = new ErLogData(
	        				'API_INTERNAL_PARAM_TRANSMISSION_ERROR', 
	                        57, 
	                        get_class($e), 
	                        __FILE__, 
	                        __CLASS__, 
	                        __FUNCTION__, 
	                        __LINE__, 
	                        date('Y-m-d H:i:s')
	                        );            
	        $erLog = ErLogFactory::create($erLogData, $e);
	        $response = json_encode(array('response' => array('error' => ['message' => $erLog->_user_message, 'code' => $erlog->_code])), JSON_UNESCAPED_UNICODE);
	        throw $erLog;
	    }
	    catch (ErLog $ee) {
	        ErLog::full_log_v1($ee);
	        return $response;
	    }
	}

	if ( !isset($params['getFullArticles']) ) {
		try {
	        $erLogData = new ErLogData(
	        				'API_INTERNAL_PARAM_TRANSMISSION_ERROR', 
	                        57, 
	                        get_class($e), 
	                        __FILE__, 
	                        __CLASS__, 
	                        __FUNCTION__, 
	                        __LINE__, 
	                        date('Y-m-d H:i:s')
	                        );            
	        $erLog = ErLogFactory::create($erLogData, $e);
	        $response = json_encode(array('response' => array('error' => ['message' => $erLog->_user_message, 'code' => $erlog->_code])), JSON_UNESCAPED_UNICODE);
	        throw $erLog;
	    }
	    catch (ErLog $ee) {
	        ErLog::full_log_v1($ee);
	        return $response;
	    }
	}


	if ($params['tagAlias']=='all-tags') {
		$whereClause  = '';
	} else {
		$params['tagAlias'] = "'" . $params['tagAlias'] . "'";
		$whereClause = 'AND at.alias =' . $params['tagAlias'];
	}


	if ($params['getFullArticles']==0) {
		$fullClause  = '';
	} else {
		$fullClause = '
					,article.creation_date as created,
					article.html_text as content,
					article.main_picture as main_picture,
					article.feed_picture as feed_picture,						
					article.title as title,
					article.subtitle as subtitle,
					article."alias" as "slug",
					article.creation_date as published,
					article.author_id as author_id		
					';
	}

	/*
	Language - setting param by default
	*/
	if (!isset($params['language']) ) {

		$params['language'] = 1;

	}

	if ( !is_int($params['language']) ) {

		try {
	        $erLogData = new ErLogData(
	        				'LANGUAGE IS NOT INT', 
	                        58, 
	                        get_class($e), 
	                        __FILE__, 
	                        __CLASS__, 
	                        __FUNCTION__, 
	                        __LINE__, 
	                        date('Y-m-d H:i:s')
	                        );            
	        $erLog = ErLogFactory::create($erLogData, $e);
	        $response = json_encode(array('response' => array('error' => ['message' => $erLog->_user_message, 'code' => $erlog->_code])), JSON_UNESCAPED_UNICODE);
	        throw $erLog;
	    }
	    catch (ErLog $ee) {
	        ErLog::full_log_v1($ee);
	        return $response;
	    }

	}	


	if (is_file('../Credentials/db_credentials.php')){
		include '../Credentials/db_credentials.php';
	} 
	else {
		exit("No ../Credentials/db_credentials.php credentials available");
	}

	$credentials =
	[
		'host' 	=> $host,
        'db' 	=> $db,
        'user'	=> $user,
        'pass' 	=> $pass,		
	];


	/*
	Create a new article
	*/

	try {
        $queryString = 
        "
		SELECT
			to_json((a)) as tag_info
		FROM
		(
		SELECT
			to_json(\"array_agg\"(b)) as tag_data,
			(SELECT publication_targets.title FROM publication_targets  WHERE publication_targets.\"id\" = ".$params['targetId'].") as publication_target_title,
			(SELECT publication_targets.\"id\" FROM publication_targets  WHERE publication_targets.\"id\" = ".$params['targetId'].") as publication_target_id
		FROM
		(
		SELECT
			raw_data.*,

			(SELECT to_json(array_agg(f)) FROM (
				SELECT 
					article_list.*,
					(SELECT to_json(array_agg(e)) FROM (
						SELECT 
							author.\"id\" as author_id,
							author.alias as author_alias,
							author.contact_json as author_contacts,
							author.description as author_description,
							author.header_picture as main_picture			
						FROM 
							author
						WHERE
							author.\"id\" = article_list.article_author_id
					) e) as author_info				
				FROM
					(
						SELECT DISTINCT
							article.\"id\" as article_id,
							article.author_id as article_author_id
							".$fullClause."													
						FROM
							article_tags
							JOIN article_publication 	ON article_tags.\"id\" = raw_data.tag_id AND article_tags.\"id\" = article_publication.tag_id AND article_publication.is_actual = 't' AND article_publication.language_id = ".$params['language']."
							JOIN article 				ON article.\"id\" = article_publication.article_id
					) as article_list
			) f) as article_list,
			(
				SELECT 
					count(article.\"id\") as article_count		
				FROM
					article_tags
					JOIN article_publication 	ON article_tags.\"id\" = raw_data.tag_id AND article_tags.\"id\" = article_publication.tag_id AND article_publication.is_actual = 't' AND article_publication.language_id = ".$params['language']."
					JOIN article 				ON article.\"id\" = article_publication.article_id
			) as article_count,
			(SELECT to_json(array_agg(f)) FROM (
				SELECT DISTINCT
					article.author_id as author_id,
					author.alias as author_alias,
					author.contact_json::TEXT as author_contacts,
					author.description as author_description,
					author.header_picture as main_picture					
				FROM
					article_tags
					JOIN article_publication 	ON article_tags.\"id\" = raw_data.tag_id AND article_tags.\"id\" = article_publication.tag_id AND article_publication.is_actual = 't' AND article_publication.language_id = ".$params['language']."
					JOIN article 				ON article.\"id\" = article_publication.article_id
					JOIN author 				ON author.id = article.author_id
			) f) as author_list,
			array_to_json(
					array[
						json_build_object (
							'type',
							'rel',
							'key',
							'canonical',
							'content',
							'spark-in.me/tag/'||raw_data.tag_alias
						),
						json_build_object (
							'type',
							'name',
							'key',
							'title',
							'content',
							raw_data.tag_title
						),
						json_build_object (
							'type',
							'name',
							'key',
							'description',
							'content',
							raw_data.tag_description
						),
						json_build_object (
							'type',
							'property',
							'key',
							'og:site_name',
							'content',
							'Spark in me'
						),
						json_build_object (
							'type',
							'property',
							'key',
							'og:title',
							'content',
							raw_data.tag_title
						),
						json_build_object (
							'type',
							'property',
							'key',
							'og:url',
							'content',
							'spark-in.me/tag/'||raw_data.tag_alias
						),
						json_build_object (
							'type',
							'property',
							'key',
							'og:description',
							'content',
							raw_data.tag_description
						)
					]
				) as tag_meta
		FROM
		(
			SELECT DISTINCT
				\"at\".\"id\" as tag_id,
				at.\"alias\" as tag_alias,
				at.title as tag_title,
				at.header_picture as main_picture,
				at.description as tag_description,
				att.title as att_title,
				att.colour as att_colour,
				att.description as att_description,
				att.sort_order as att_sort_order							
			FROM
				article_tags at 
				JOIN article_tag_types att 	ON at.tag_type_id = att.id				
				JOIN article_publication ap ON  at.\"id\" = ap.tag_id AND ap.language_id = ".$params['language']."
				JOIN publication_targets pt ON pt.\"id\" = ap.target_id
			WHERE 1=1 
				AND pt.\"id\" = ".$params['targetId']."
				".$whereClause." 
		) raw_data
		ORDER BY
			7 DESC
		) b
		) a
        ";
	} catch (Exception $e){
		try {
	        $erLogData = new ErLogData(
	        				'API_QUERY_CONSTRUCTION_ERROR', 
	                        59, 
	                        get_class($e), 
	                        __FILE__, 
	                        __CLASS__, 
	                        __FUNCTION__, 
	                        __LINE__, 
	                        date('Y-m-d H:i:s')
	                        );            
	        $erLog = ErLogFactory::create($erLogData, $e);
	        $response = json_encode(array('response' => array('error' => ['message' => $erLog->_user_message, 'code' => $erlog->_code])), JSON_UNESCAPED_UNICODE);
	        throw $erLog;
	    }
	    catch (ErLog $ee) {
	        ErLog::full_log_v1($ee);
	        return $response;
	    }
	}
	$result =  queryWrapper ($credentials, $queryString);
	return $result;	

}		



4. Обработка ошибок


image

Делается двумя способами:
  • Есть класс Erlog, который расширяет классы php для работы с ошибками, рекурсивно прокидывает ошибки и логирует их — по идее он достоин отдельной статьи, но его автор смущается, что интернет-публика из всезнающих людей не воспримет адекватно;
  • Если приложение не может подключиться к базе в локальной сети, то я получаю email (при отладке приложения были проблемы, по сути не хотелось поднимать нормальное АПИ 2 раза), то такой гениальный кусок кода отправляет аларму мне:


        if (!$dbconn) {

            $errorString = $appName . "\r\n". date('m/d/Y h:i:s a', time()) . "\r\n".  $credentials['db'] . "\r\n".  $queryString . "\r\n".  implode(",", $queryParamArray);

            error_log($errorString, 1, "aveysov@gmail.com");

            $ret = file_put_contents('offline-errors.log', $errorString);

            die('Could not connect (logged)');

        } else {
            /*
            Continue executing code
            */
        }


5. Сущности для прошивки админки


image

Тоже часть legacy-кода. Изначально проектировалась, чтобы позволить создавать новые странички в админке максимально быстро. По сути является просто веб-фреймворком, разобранным на зависимости.

HTML код и JS-инклюды библиотек собираются конкатенацией строк на php в большом классе из констант. Тупо, грубо, медленно (грузится 1-2 секунды), но зато работает. Естественно вся фронтенд логика сделана аяксом через одно место, т.к. на аякс и фронтенд ни знаний и усилий в свое время ну совсем не хватило.

По идее тут нет ничего интересного, кроме:

  • Изначально даже была написана интеграция c АПИ redmine для отправки тикета в саппорт. По сути не пригодилось;
  • У админки есть права на доступ к определенным страницам. Пользователь получает права на методы АПИ и доступ к страницам админки отдельно (они могут быть частью одной роли);
  • Связь страниц и иерархия описаны в виде бинарного дерева в базе;
  • У каждой страницы есть свой uuid по которому она собственно и собирается;
  • Есть витиватая логика логина на php даже с минимальной криптографией;


Пишите в комментариях, если какая-то часть покажется вам достойной пошагового разбора.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331578/


Метки:  

Spark-in.me. Часть 3 — DIY поддержка и админство сайта

Понедельник, 26 Июня 2017 г. 07:00 + в цитатник
image
Если вы не профессиональный админ, то зачастую деплой / саппорт / внесение правок в архитектуру может выглядеть для вас именно так.

В данной статье пойдет речь про наш подход к поддержке и админству нашего сайта https://spark-in.me. Дисклеймер — если будете пытаться повторить какие-то строки — сначала читайте документацию и разберитесь, что они значат.

Статьи цикла
  1. Spark-in.me. Часть 1 — Зачем и почему?
  2. Spark-in.me. Часть 2 — Архитектура приложения и структура БД
  3. Spark-in.me. Часть 3 — DIY поддержка и админство сайта
  4. Spark-in.me. Часть 4 — Базовое админство для обычных человеков
  5. Spark-in.me. Часть 5 — Переход на HTTPS
  6. Spark-in.me. Часть 6 — Исходный код и настройка бекенда
  7. Spark-in.me. Часть 7 — Исходный код и настройка фронтенда




Тут описан наш общий подход к админству наших проектов. Он не претендует на полноту, но работает для нас в комбинации с курением форумов и документации Ubuntu. Для начала несколько полезных ссылок на полезную копипасту, которую мы постили на канале и на сайте:

0. Покупка VDS



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

  • Должна быть (в идеале отдельная при росте нагрузки) машина для базы;
  • Должна быть (в идеале отдельная при росте нагрузки) машина для админки;
  • Должна быть (в идеале отдельная при росте нагрузки) машина для фронтенда;


При росте нагрузки, понятное дело, основная нагрузка ложится на базу, т.к. на ней держится большая часть приложения. Фронтенд сделан на изоморфном реакте, он прожорлив, но он перекладывает нагрузку на клиент (браузер) пользователя. С учетом этого начнем нашу логическую цепочку. Пока пользователей мало (одновременно на сайте вряд ли бывает больше чем 10-20 человек), все можно задеплоить на одну машину. С учетом прожорливости ноды, у VDS, которую нужно купить для этого, должно быть 1-2 ГБ памяти. У Digital Ocean такое удовольствие стоит порядка 10 долларов в месяц. У Digital Ocean также есть отличные гайды по админству Linux, когда будете гуглить — ищите в том числе на их сайте.

Не вижу смысла останавливать на выборе провайдера VDS — есть провайдеры и дешевле, чем за 10 баксов, но тут все подкупает наличием дополнительных бекапов и простотой и интуитивностью их интерфейса. Могу только отметить:

  • Чем чернее сервис и чем хреновее интерфейс, тем меньше гарантий и нет сервиса;
  • Есть старые конторы типа Hetzer, которые дают dedicated сервера и VDS, там чуть дешевле, но вряд ли все так просто;
  • Есть сервисы, которые сразу дают ячейки с установленным софтом, но они дороже в несколько раз и не поддерживают извращения;


1. Покупка доменов и прокидывание субдоменов



Первым шагом на нашей стороне будет покупка домена. Допустим мы купили домен spark-in.me и всю мелочевку, которая на него хоть как-то похожа. Далее нужно сделать редирект всех мусорных доменов на spark-in.me в кабинете регистратора. Далее мне кажется логичным завести такие субдомены через кабинет регистратора (это можно сделать через Apache или Nginx, но я выбрал самый простой способ) и направить их на машину.

  • author.spark-in.me, который ведет на админку автора;
  • api.spark-in.me, который ведет на АПИ;
  • spark-in.me, который ведет на сайт;
  • pics.spark-in.me, который ведет на микросервис по хранению картинок;


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

2. Настройка сервера



Поскольку мы не ищем простых путей и в паре мест мы срезаем углы (мы делаем RSS и Sitemap по крону), то проще купить дешевую VDS и самому настроить деплой и поставить софт.

Базовые вещи, нужные для системы — апдейт пакетов и софт для мониторинга нагрузки. Это не заменяет такие вещи как Zabbix, но Digital Ocean к примеру предоставляет базовую статистику по нагрузке и так (потом при установке https оказалось, что что есть сервисы мониторинга, которые все таки настраиваются за 30 секунд).

# installation of the key software
sudo apt-get update
# server monitoring tool
sudo apt-get install glances
# just invoke glances to monitor the system


image

Есть явные spikes во время ночных бекапов по крону, но ничего критического пока нет.

Постгрес и апач (потом был заменен на nginx)

# postgresql
# installing contrib version via ppa
sudo apt-get install postgresql postgresql-contrib
# installing apache2
sudo apt-get install apache2
# checking syntax of apache2 config files
sudo apache2ctl configtest
sudo systemctl status apache2
# restarting apache2
sudo systemctl restart apache2
# apache2 specific mods
sudo a2enmod proxy # for reverse proxy


Php и node-js (все гуглится по ссылкам ниже или ключевым словам)

Также при использовании apache2 рекоммендуется сразу выключить directory listing мод, иначе будут видны папки на вашем сервере (было заменено на nginx).

В официальном репозитории Убунты на момент настройки была устаревшая нода — но в интернете также были гайды по апгрейду.

# checking ufw
sudo ufw app list
# disable apache2 directory listing mod
sudo a2dismod autoindex -f
# install php
# https://www.digitalocean.com/community/tutorials/how-to-install-linux-apache-mysql-php-lamp-stack-on-ubuntu-16-04
sudo apt-get install php libapache2-mod-php php-mcrypt
# install postgresql driver
sudo apt-get install php-pgsql
# install curl
sudo apt-get install php-curl
# locate the php.ini file ans set
# memory to 256m, time to 60s and error reporting to on
# installing node js and npm
sudo apt-get install nodejs
# this actually installs node 4.3
# do this to install 6.9+
wget -qO- https://deb.nodesource.com/setup_7.x | sudo bash -
sudo apt-get install -y nodejs
sudo apt-get install npm 
# install yarn
# https://yarnpkg.com/lang/en/docs/install/#linux-tab
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn
# disable the apache2 default conf
sudo a2dissite 000-default.conf
sudo systemctl restart apache2


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

Сборник ссылок про vsftpd.

# vsftpd installation and config
sudo apt-get install vsftpd
sudo cp /etc/vsftpd.conf /etc/vsftpd.conf.original
sudo nano /etc/vsftpd.conf
# key changes
# uncomment local_umask=022
# local_enable=YES
# write_enable=YES
# anonymous_enable=NO
# chroot_local_user=YES
sudo adduser spark-in-me
sudo chown spark-in-me:spark-in-me /var/www/spark-in-me/
sudo usermod -d /var/www/spark-in-me spark-in-me
sudo chmod a-w /var/www/spark-in-me
sudo service vsftpd restart
chown spark-in-me:spark-in-me /var/www/spark-in-me/blog/
chown spark-in-me:spark-in-me /var/www/spark-in-me/admin/
# after this commands vsftpd should start properly working


Настройка файла крона

Ссылки про то, как начать использовать крон 1 2 3 4

# crontab setup and usage
crontab -e
# CRONTAB CONTENTS START HERE
# Borrowed from anacron
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=aveysov@gmail.com
#End borrowed from anacron
# 0 1 * * * /home/snakers41/bash-scripts/WHOLE_SYSTEM_BACKUP.sh
# CRONTAB CONTENTS END HERE


Пример банального скрипта для бекапа системы (в итоге дергаем через ssh по ночам с другого сервера)

# backups
# plain backup script
mkdir /home/bash-scripts
mkdir /media/backups
sudo nano /home/bash-scripts/WHOLE-SYSTEM-BACKUP.sh
# SCRIPT STARTS HERE
# http://help.ubuntu.ru/wiki/backup#восстановление_из_архива
#/bin/bash
#Purpose = Whole system backup
#Created on 25-08-2016
#Last modified on 29-08-2016
#Author = aveysov@gmail.com
#Version 1.1
#START
# This Command will add date in Backup File Name.
TIME=`date +%b-%d-%y`
# Here i define Backup file name format.
FILENAME=test-system-backup-$TIME.tar.gz
# Backed up folder (system root) location
SRCDIR=/
# Destination of backup file
DESDIR=/media/backups
# exclude folder list
EXCLUDE='--exclude=/media/server --exclude=/media/ext_storage --exclude=/media --exclude=/proc --exclude=/lost+found --exclude=/backup.tgz --exclude=/mnt --exclude=/sys'
#  Do not include files on a different filesystem
ONEFSYSPARAM='--one-file-system'
# test command validity
# echo -e tar -cvpzf $DESDIR/$FILENAME $EXCLUDE $ONEFSYSPARAM $SRCDIR
sudo tar -cpzf $DESDIR/$FILENAME $EXCLUDE $ONEFSYSPARAM $SRCDIR
echo "WHOLE_SYSTEM_BACKUP is successful: $(date)" >> /home/bash-scripts/cron_log.log
#END
# SCRIPT ENDS HERE


Настройка почтовых уведомлений через ssmpt

# email alerts set-up - useful for cron
sudo apt-get install ssmtp
# START CONFIG
# Config file for sSMTP sendmail
# The person who gets all mail for userids < 1000
# Make this empty to disable rewriting.
# root=postmaster
root=gmail-addresscom
# The place where the mail goes. The actual machine name is required no
# MX records are consulted. Commonly mailhosts are named mail.domain.com
# mailhub=mail
mailhub=smtp.gmail.com:587
AuthUser=gmail-addresscom
AuthPass=your_pass
UseTLS=YES
UseSTARTTLS=YES
# Where will the mail seem to come from?
rewriteDomain=gmail.com
# The full hostname
# hostname=snakers41-ubuntu
hostname=localhost
# Are users allowed to set their own From: address?
# YES - Allow the user to specify their own From: address
# NO - Use the system generated From: address
FromLineOverride=YES
# END CONFIG
# IMPORTANT - turn on less secure apps in google account settings
# https://support.google.com/accounts/answer/6010255
# test email
# echo "Test message from Linux server using ssmtp" | sudo ssmtp -vvv destination-email-address@some-domain.com


Конфигурация БД

# database setup
# 
mkdir -p /etc/postgresql/dumps
root@ubuntu-512mb-fra1-01:~# scp -P 8023 snakers41@77.37.164.97:'/media/server/04_BACKUP/localdbrestore/03-20-2017/news.sql' /etc/postgresql/dumps
root@ubuntu-512mb-fra1-01:~# passwd postgres
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
root@ubuntu-512mb-fra1-01:~# su postgres
postgres@ubuntu-512mb-fra1-01:/root$ psql
could not change directory to "/root": Permission denied
psql (9.5.6)
Type "help" for help.
postgres=# create database news
postgres-# ;
CREATE DATABASE
postgres=# create role aveysov login superuser password 'yourpassword';
postgres=# \q
postgres@ubuntu-512mb-fra1-01:/root$ psql news < /etc/postgresql/dumps/news.sql
postgres@ubuntu-512mb-fra1-01:/root$ nano /etc/postgresql/9.5/main/postgresql.conf
#?????? ???????? ??: listen_addresses = *
postgres@ubuntu-512mb-fra1-01:/root$ nano /etc/postgresql/9.5/main/pg_hba.conf  
#?????? ???????? ?????? 
#host news aveysov 0.0.0.0/0 md5
postgres@ubuntu-512mb-fra1-01:/root$ exit
exit
root@ubuntu-512mb-fra1-01:~# service postgresql restart


3. Пример деплоя



Деплой сайта из гита с созданием RSS Sitemap и прочим

Доступ к репозиторию делается через SSH-ключ.
# redeploy script
rm -rf /var/www/spark-in-me/blog/*
rm -rf /var/www/spark-in-me/blog/.*
eval $(ssh-agent -s)
ssh-add ~/.ssh/git
cd /var/www/spark-in-me/blog
git clone git@github.com:snakers4/spark-in-me-vds .
sudo yarn install
sudo yarn run build --release
touch /var/www/spark-in-me/blog/build/public/sitemap.xml
touch /var/www/spark-in-me/blog/build/public/main.rss
chmod 777 /var/www/spark-in-me/blog/build/public/sitemap.xml
chmod 777 /var/www/spark-in-me/blog/build/public/robots.txt
chmod 777 /var/www/spark-in-me/blog/build/public/main.rss
echo "# www.robotstxt.org/
Sitemap: http://www.spark-in.me/sitemap.xml
# Allow crawling of all content
User-agent: *
Disallow:" > /var/www/spark-in-me/blog/build/public/robots.txt
php /var/www/spark-in-me/admin/Api/Rss/rssMakerBash.php


После деплоя сервер перезагружается (пока руками) и по крону выполняется незатейливый скрипт старта yarn
#/bin/bash
#Purpose = Start node js webserver via yarn
#Created on 21-03-2017
#Last modified on  21-03-2017
#Author = aveysov@gmail.com
#Version 1.1
#START
# This Command will add date in Backup File Name.
cd  /var/www/spark-in-me/blog/build
yarn start
#END


4. Минимальная безопасность



Я не фанат security through obscurity, но когда в одиночку нужно поддерживать все компоненты сайта, не хочется особо много париться памятую про анекдот про неуловимого Джо. Для безопасности мы используем:

  • Доступ к серверу и репозиторию через SSH ключ;
  • Tar-бекапы и pg_dump бекапы с неизвестной никому машины через SSH ключ;
  • Бекапы через Digital Ocean;
  • Все порты закрыты через ufw, открыты только нужные;
  • Доступ к базе разрешен только из локалхоста и набора фиксированных IP, откуда я вношу правки;
  • АПИ стучится в базу через локалхост, если хоть раз нет коннекта, я получаю email;
  • В php коде соблюдается параноидальное правило: если не коннекта, нет данных или есть ошибка — коннект закрывается;
  • Ограничений на коннекты почти не стоит, но они не плодятся и все по сути работает на 5-10 коннектах (число одновременных пользователей);


Напоследок сборка ссылок про прикладное админство, которая кажется мне полезной
  • Азы админства постгреса и бекапов;
  • Полезные скрипты для переноса и деплоя приложений
  • Курсы про админство Постгреса на русском;


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

Про https будет целая отдельная статья.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331580/


Метки:  

Spark-in.me. Часть 5 — переход на HTTPS

Понедельник, 26 Июня 2017 г. 07:00 + в цитатник
image
Заманчивый шильдик скрывает много деталей

В данной статье пойдет речь про перевод нашего сайта Spark-in.me на https.

TLDR
СТАВЬТЕ HTTPS СРАЗУ ЧЕРЕЗ https://letsencrypt.org И НЕ МУЧАЙТЕСЬ

Статьи цикла
  1. Spark-in.me. Часть 1 — Зачем и почему?
  2. Spark-in.me. Часть 2 — Архитектура приложения и структура БД
  3. Spark-in.me. Часть 3 — DIY поддержка и админство сайта
  4. Spark-in.me. Часть 4 — Базовое админство для обычных человеков
  5. Spark-in.me. Часть 5 — Переход на HTTPS
  6. Spark-in.me. Часть 6 — Исходный код и настройка бекенда
  7. Spark-in.me. Часть 7 — Исходный код и настройка фронтенда




Когда я планировал функционал канала и сайта, я думал про https в последнюю очередь. Ни для кого не секрет, что Google в ранжировании сайтов использует более 200 различных параметров, также среди которых есть наличие https. Памятуя о том, какие муки бывают при переходе на https у крупных компаний и памятуя о том, как я лично лицезрел такой переход в менее крупной компании, я внес несколько вещей в архитектуру spark-in-me, которые потом как оказалось позволили сделать миграцию без проблем:

  1. Картинки для статей лежат на отдельном сервисе по отдельному url;
  2. Список картинок / файлов ведется с самого начала;
  3. В статье картинки явно прописаны в виде колонок в БД (не считая текста статьи);
  4. Все картинки навигации веб-сайта (например шапка) прописаны в специальных таблицах в БД;
  5. Почти все сущности (автор, главная, тег) имеют свои картинки, прописанные в БД, на клиенте нет хардкода, кроме адреса АПИ (меняется заменой 1 строки);
  6. Уровни максимально разделены — база, морда, АПИ и веб-сервер (nginx) почти никак не связаны друг с другом;


image
Гугл намекает, что как бы он даже не против, если вы зальете все комбинации своего сайта


Вообще взгляды на переход на https ранжируются от «закинул сертификат в корень и все» и «он должен быть сразу» до «все и так работает». На самом деле в свете последних событий в 2017 году https считается как бы уже обязательным и показателем, что вам не безразлично. Потом в один знакомый разработчик рассказал мне про существование бесплатной CA (certification authority), которая не только выдает бесплатные сертификаты, но и поддерживает плагины для apache и nginx (в том числе), чтобы установка сертификатов делалась 1 командой (обновлять можно по крону). Вот ссылки:

  • Главная страница — letsencrypt.org;
  • Конкретные команды для набора систем;
  • Если хотите закоптиться самостоятельно, то вот подробный гайд от Digital Ocean, где эта CA используется только для получения сертификата, остальное делается руками;


Когда я увидел, что список команд выглядит примерно вот так, я понял что надо все сделать и быстро:
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx 
$ sudo certbot --nginx


Чтобы ничего не падало (а оно все-таки падало ненадолго) я разбил мигацию на следующие шаги.

1. Установка сертификатов



Делается командами по ссылкам описанным выше. Пара тонких моментов

  • не забудьте прописать в своей CDN или консоли вашего регистратора доменов, записи CNAME для www версий своих доменов (пример www.example.com => example.com);
  • Если у вас уже стоят некие конфиги серверов, то (и по дефолту тоже) отвечайте «нет» на вопросы утилиты certbot, когда она попросит сменить конфиги;


2. Правки виртуальных хостов



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

Обратите внимание на то, что я немного подчистил вывод certbot, разбил на 2 блока и добавил header, который в итоге не пригодился.

# http://nginx.org/en/docs/varindex.html
# https://serverfault.com/questions/638097/passing-ssl-protocol-info-to-backend-via-http-header
# https://serverfault.com/questions/213185/how-to-restart-nginx
# https://serverfault.com/questions/527780/nginx-detect-https-connection-using-a-header
# https://stackoverflow.com/questions/17483641/nginx-to-node-js-pass-params

server {
    listen 80;
    server_name spark-in.me www.spark-in.me;

    root /var/www/spark-in-me/blog;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X_SPARK_SSL 0;
    }
    location ~ /\.(ht|git) {
        deny all;
    }

}
server {
    listen 443 ssl; # managed by Certbot
    server_name spark-in.me www.spark-in.me;
    ssl_certificate /etc/letsencrypt/live/spark-in.me/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/spark-in.me/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    root /var/www/spark-in-me/blog;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X_SPARK_SSL 1;
    }
    location ~ /\.(ht|git) {
        deny all;
    }
}


Для меня как оказалось такой шаг был скорее избыточным, но для более крупного проекта, где нельзя «сразу» все сделать, он может быть вполне оправданным с целью тестирования «а что же отваливается?» без безвозвратной миграции. Протестировали, но все наши ссылки продолжают работать, SEO не падает, никто не жалуется…

3. Правки архитектуры вызова сервисов



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

/*Protections against CSRF attacks*/
if ("POST" == $_SERVER["REQUEST_METHOD"]) {
    if (isset($_SERVER["HTTP_ORIGIN"])) {
    	
    	$http_origin = $_SERVER['HTTP_ORIGIN'];
        $address = "http://".$_SERVER["SERVER_NAME"];
        
        /*
        Uncomment the protection bit during deploy
        if (strpos($address, $_SERVER["HTTP_ORIGIN"]) !== 0) {
            exit("CSRF protection in POST request: detected invalid Origin header: ".$_SERVER["HTTP_ORIGIN"]);
        }
        */
    } else {
   		if(!isset($http_origin)) {
   			$http_origin = '';
   		}
    }
}
/*Headers for modern http-request libraries*/
if (
	$http_origin == "http://spark-in.me" 
	|| $http_origin == "http://api.spark-in.me" 
	|| $http_origin == "http://admin.spark-in.me" 
	|| $http_origin == "http://pics.spark-in.me"  
	|| $http_origin == "http://author.spark-in.me" 
	|| $http_origin == "https://spark-in.me" 
	|| $http_origin == "https://api.spark-in.me" 
	|| $http_origin == "https://admin.spark-in.me" 
	|| $http_origin == "https://pics.spark-in.me"  
	|| $http_origin == "https://author.spark-in.me"
	|| $http_origin == "http://www.spark-in.me" 
	|| $http_origin == "http://www.api.spark-in.me" 
	|| $http_origin == "http://www.admin.spark-in.me" 
	|| $http_origin == "http://www.pics.spark-in.me"  
	|| $http_origin == "http://www.author.spark-in.me" 
	|| $http_origin == "https://www.spark-in.me" 
	|| $http_origin == "https://www.api.spark-in.me" 
	|| $http_origin == "https://www.admin.spark-in.me" 
	|| $http_origin == "https://www.pics.spark-in.me"  
	|| $http_origin == "https://www.author.spark-in.me"
	) {  
 	  header("Access-Control-Allow-Origin: $http_origin");
	}
else {
	// Do nothing
}
header("Access-Control-Allow-Headers: X-Requested-With");


После этого во всех сервисах надо сменить адреса API-endpoint-ов на https. После этого нужно все протестировать.

4. Правки контента



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

В моем случае все свелось к паре взглядов на ER диаграмму в БД и такую замену (названия колонки и таблицы случайные):

UPDATE file SET host = replace(host, 'https://pics.spark-in.me/', 'https://pics.spark-in.me/')


После этого у меня перестали вываливаться ошибки (почти, но перестали появляться ошибки связанные со смешанным контентом и https, остались неведомые баги клиента).

5. Установка редиректа



Полезные ссылки по теме 1 2. Тут важно помнить, что по-хорошему вам важно будет сохранить не только свои «старые» ссылки, но и параметры, с которыми ваши клиенты и партнеры шлют вам трафик. В моем случае я ограничился добавлением этого в виртуальный хост nginx

return         301 https://$server_name$request_uri;


6. Заливка изменений в Google Search Console



Иногда документация Гугла это кладезь знаний — раз и два. А вообще Гугл, судя по картинке выше, «хочет» чтобы вы имели условно 4 версии вашего сайта (с и без www * с и без https). Убедитесь, что там не начинают лезть новые ошибки, что все индексируется и доступно. Залейте сайтмапы для всех версий сайта.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331592/


Метки:  

Быстрый анализ сайтов конкурентов через сайтмапы. Часть 1 — парсинг

Понедельник, 26 Июня 2017 г. 06:56 + в цитатник
image
Как всегда надо начать с заманчивой картинки, но не пояснять ее смысл!

Пару дней назад ко мне обратился далекий знакомый, мол у нас политесы в компании и мы не можем никак выбрать направление развития. Ситуация типичная — лебедь, рак и щука и надо бы очень быстро проанализировать сайты его конкурентов, вот держи картинку со списком самых крупных компаний в этой сфере. Оказалось, что конкуренты из сферы финансов. Это меня смутило, но не остановило. Я посмотрел на их сайты (а, забегая вперед, некоторые из них просто огромны — миллионы страниц) и тут у меня родилась гениальная идея.

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


image
Картинка, которую мне прислали. Даже nutshell не нужен.

После визита на пару сайтов, у меня в голове промелькнули такие мысли:
  1. Парсинг (или «скрепинг») сайтов в современном мире — это лучшее средство business intelligence. Ибо сейчас все выкладывают весь свой контент для индексации в интернет;
  2. Недавно я видел отличную и простую статью, про то, что мол парсинг это чуть ли ваша обязанность, если вы аналитик;
  3. У меня есть знакомый, которому один раз предлагали спарсить весь вконтакте за 300,000 рублей 1 раз. Так вот он говорил, что в одноразовом парсинге на питоне вообще нет ничего сложного. Потоковый парсинг сложнее, там надо иметь прокси, VPN и балансировщик нагрузки и очередь;
  4. Самые крупные сайты как правило имеют сайтмапы;
  5. По этой причине этот подход в принципе применим для любой отрасли, где присутствует много контента;
  6. Идея также расширяется до выборочного парсинга самих страниц, но это на порядок сложнее технически (оставим это более прошаренным коллегам);


Эта статья будет скорее оформлена в виде пошагового гайда, что немного в новинку для меня. Но почему нет, давайте попробуем?

1. Идея



Через секунду после того, как я подумал про сайтмапы, у меня сразу родился план действий:
  • Найти все сайтмапы сайтов;
  • Внести в массив, указав рекурсивные ли они;
  • Собрать итоговый список сайтмапов (а их явно будут сотни или тысячи);
  • Спарсить их;
  • Распарсить урлы на составляющие;
  • Сделать семантический анализ составляющий урлов;
  • Если хватит сил, прогнать bag-of-words анализ, снизить размерность через метод главных компонент (PCA) и посмотреть что будет;
  • Построить визуализации для самых популярных слов;
  • Сделать простейшие сводные таблицы;
  • Если будет нужно и полезно — подключить к процессу еще и словесные вектора (word2vec);


Забегая вперед, что получилось посмотреть можно тут:


Проще всего вам будет следить за повествованием установив себе зависимости, запустив jupyter notebook и выполняя код последовательно. Внимание (!) — иногда из-за размерности файлов отжирается вся память (на моей песочнице 16ГБ) — будьте внимательны! Для простейшего мониторинга рекомендую glances (ну или просто делайте все на 10% датасета).

2. Зависимости (Common libraries, Code progress utility)



Все делается на третьем питоне.

  • По сути вам нужен сервер с python3 и jupyter notebook (без этого будет гораздо дольше).
  • Также я очень советую поставить себе плагин сollapsable / expandable jupyter cells;
  • Список основных библиотек и зависимостей указан ниже (или я буду добавлять их в отдельных ячейках);


from __future__ import print_function
import os.path
from collections import defaultdict
import string
import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
from sklearn.feature_extraction.text import CountVectorizer
import wordcloud
%matplotlib inline


Если у вас все работает, то .ipynb файл откроется примерно так (сверните ячейки, если они не свернуты по умолчанию):
image

Также отсюда я взял небольшую утилитку для демонстрации прогресса парсинга. Инструкции по установке также по ссылке.
image

3. Список сайтмапов (Sitemap list)


Тут все банально, гуляем по сайтам, ищем сайтмапы (обычно они лежат в корне с названием sitemap.xml). Поиск google по сайту также помогает. Записываем в лист словарей.

sitemap_list = [ 
    {'url': 'https://www.ig.com/sitemap.xml', 'recursive': 1},
    {'url': 'https://www.home.saxo/sitemap.xml', 'recursive': 0},    
    {'url': 'https://www.fxcm.com/sitemap.xml', 'recursive': 1},  
    {'url': 'https://www.icmarkets.com/sitemap_index.xml', 'recursive': 1},  
    {'url': 'https://www.cmcmarkets.com/en/sitemap.xml', 'recursive': 0},
    {'url': 'https://www.oanda.com/sitemap.xml', 'recursive': 0},     
    {'url': 'http://www.fxpro.co.uk/en_sitemap.xml', 'recursive': 0}, 
    {'url': 'https://en.swissquote.com/sitemap.xml', 'recursive': 0}, 
    {'url': 'https://admiralmarkets.com/sitemap.xml', 'recursive': 0},     
    {'url': 'https://www.xtb.com/sitemap.xml', 'recursive': 1},       
    {'url': 'https://www.ufx.com/en-GB/sitemap.xml', 'recursive': 0},   
    {'url': 'https://www.markets.com/sitemap.xml', 'recursive': 0},   
    {'url': 'https://www.fxclub.org/sitemap.xml', 'recursive': 1},       
    {'url': 'https://www.teletrade.eu/sitemap.xml', 'recursive': 1},       
    {'url': 'https://bmfn.com/sitemap.xml', 'recursive': 0},       
    {'url': 'https://www.thinkmarkets.com/en/sitemap.xml', 'recursive': 0},  
    {'url': 'https://www.etoro.com/sitemap.xml', 'recursive': 1},  
    {'url': 'https://www.activtrades.com/en/sitemap_index.xml', 'recursive': 1},  
    {'url': 'http://www.fxprimus.com/sitemap.xml', 'recursive': 0}
]


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

4. Собственно сам сбор сайтмапов (Web scraping)



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

Для этого нам поможет библиотека fake_useragent:

from fake_useragent import UserAgent
ua = UserAgent()
headers = ua.chrome
headers = {'User-Agent': headers}


Если мы попробуем забрать один сайтмап, то мы увидим, что его надо декодировать


result = requests.get(sitemap_list[3]['url'])
c = result.content
c = c.decode("utf-8-sig")
c


Ответ выглядит примерно так (это сайтмап сайтмапов):

'\n\n\t\n\t\thttps://www.icmarkets.com/post-sitemap.xml\n\t\t2016-12-16T07:13:32-01:00\n\t\n\t\n\t\thttps://www.icmarkets.com/page-sitemap.xml\n\t\t2017-06-20T07:11:01+00:00\n\t\n\t\n\t\thttps://www.icmarkets.com/attachment-sitemap1.xml\n\t\t2014-07-01T15:44:46+00:00\n\t\n\t\n\t\thttps://www.icmarkets.com/attachment-sitemap2.xml\n\t\t2014-10-29T02:36:07-01:00\n\t\n\t\n\t\thttps://www.icmarkets.com/attachment-sitemap3.xml\n\t\t2015-03-15T18:41:51-01:00\n\t\n\t\n\t\thttps://www.icmarkets.com/attachment-sitemap4.xml\n\t\t2017-05-30T12:33:34+00:00\n\t\n\t\n\t\thttps://www.icmarkets.com/category-sitemap.xml\n\t\t2016-12-16T07:13:32-01:00\n\t\n\t\n\t\thttps://www.icmarkets.com/post_tag-sitemap.xml\n\t\t2014-03-27T01:14:54-01:00\n\t\n\t\n\t\thttps://www.icmarkets.com/csscategory-sitemap.xml\n\t\t2013-06-11T00:02:10+00:00\n\t\n\t\n\t\thttps://www.icmarkets.com/author-sitemap.xml\n\t\t2017-05-05T06:44:19+00:00\n\t\n\n'


Эта функция найденная на просторах интернета поможет нам декодировать XML дерево, которым является сайтмап:

# xml tree parsing
import xml.etree.ElementTree as ET

def xml2df(xml_data):
    root = ET.XML(xml_data) # element tree
    all_records = []
    for i, child in enumerate(root):
        record = {}
        for subchild in child:
            record[subchild.tag] = subchild.text
            all_records.append(record)
    return pd.DataFrame(all_records)


Далее эта функция поможет нам собрать все сайтмапы в одном листе:

end_sitemap_list = []
for sitemap in log_progress(sitemap_list, every=1):
    if(sitemap['recursive']==1):
        try:
            result = requests.get(sitemap['url'], headers=headers)
            c = result.content
            c = c.decode("utf-8-sig")
            df = xml2df(c)
            end_sitemap_list.extend(list(df['{http://www.sitemaps.org/schemas/sitemap/0.9}loc'].values))
        except:
            print(sitemap)
    else:
        end_sitemap_list.extend([sitemap['url']])

В разное время у меня получалось от 200 до 250 сайтмапов.

В итоге эта функция поможет нам собственно собрать данные сайтмапов и сохранить их в датафрейм pandas.

result_df = pd.DataFrame(columns=['changefreq','loc','priority'])
for sitemap in log_progress(end_sitemap_list, every=1):
    
    result = requests.get(sitemap, headers=headers)
    c = result.content
    try:
        c = c.decode("utf-8-sig")
        df = xml2df(c)
        columns = [
            '{http://www.sitemaps.org/schemas/sitemap/0.9}changefreq',
            '{http://www.sitemaps.org/schemas/sitemap/0.9}loc',
            '{http://www.sitemaps.org/schemas/sitemap/0.9}priority'
        ]
        try: 
            df2 = df[columns]
            df2['source'] = sitemap
            df2.columns = ['changefreq','loc','priority','source']
        except:
            df2['loc'] = df['{http://www.sitemaps.org/schemas/sitemap/0.9}loc']
            df2['changefreq'] = ''
            df2['priority'] = ''
            df2['source'] = sitemap
        result_df = result_df.append(df2)
    except:
        print(sitemap)

После нескольких минут ожидания у нас получается таблица размером (14047393, 4), что весьма неплохо для такого «наколеночного» решения!

Если вам понравился новый формат, пишите в личке, будем продолжать в таком же формате. Ну и эта статья — первая в цикле.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331572/


Z-order в 8D

Понедельник, 26 Июня 2017 г. 06:45 + в цитатник

Метки:  

Phoenix Framework – Webpack вместо Brunch, деплой с помощью Distillery и немного systemd

Понедельник, 26 Июня 2017 г. 01:02 + в цитатник

logo


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


Кому интересно, почему не Brunch и как совместить миграции с Distillery – прошу под кат.


Phoenix для работы с JS-кодом и ассетами по-умолчанию использует Brunch – возможно, очень простой и быстрый бандлер, но уж точно не самый распространенный и не самый мощный по возможностям и размеру экосистемы (и ответам на StackOverflow, конечно же). Поэтому и произошла замена Brunch на Webpack, де-факто – бандлер номер один в текущем мире фронт-энда.


А вот для решения вопросов деплоя, фреймворк не предоставляет практически ничего, кроме возможности подложить разные конфигурации для разных окружений. Судя по ответам разных разработчиков на форумах и прочих площадках, многие из них разворачивают свои приложения путем установки инструментов разработки прямо на боевом сервере и компилируя и запуская приложение с помощью Mix. По ряду причин, считаю такой подход неприемлемым, потому, перепробовав несколько вариантов упаковки приложения в self-contained пакет, я остановился на Distillery.


Т.к. статья является туториалом, то в качестве примера будет разработано абсолютно ненужное приложение, отображающее некий список неких пользователей. Весь код доступен на GitHub, каждый шаг зафиксирован в виде отдельного коммита, потому рекомендую смотреть историю изменений. Также, я буду давать ссылки на коммиты на определенных шагах, чтобы, с одной стороны, хорошо было видно по diff'у, какие изменения были сделаны, а с другой – чтобы не загромождать текст листингами.


Подготовка


Итак, создадим шаблон нашего проекта, с указанием того, что Brunch мы использовать не будем:


$ mix phoenix.new userlist --no-brunch

Тут ничего интересного не происходит. Надо зайти внутрь нового проекта, поправить настройки базы данных в файле config/dev.exs, запустить создание репозитория Ecto и миграций (коммит):


$ mix ecto.create && mix ecto.migrate

Для того, чтобы сделать пример хоть немного нагляднее, я добавил модель сущности User, содержащую два поля – имя и бинарный признак, активен ли пользователь или нет (коммит):


$ mix phoenix.gen.model User users name active:boolean

Далее, чтобы наполнить БД хоть какими-то данными, я добавил три экземпляра "пользователей" в файл priv/repo/seeds.exs, который и служит для таких целей. После этого можно выполнить миграцию и вставить данные в БД:


$ mix ecto.migrate && mix run priv/repo/seeds.exs

Теперь у нас есть миграция в priv/repo/migrations/ – она нам пригодится в дальнейшем, а пока, надо еще добавить http API, по которому приложение сможет забрать список пользователей в формате JSON-объекта (коммит). Не буду загромождать текст листингами, diff на ГитХабе будет более нагляден, скажу лишь, что был добавлен контроллер, вью и изменен роутинг так, что у нас появилась "http-ручка" по пути /api/users, которая будет возвращать JSON с пользователями.


На этом все с приготовлениями, и на данном этапе приложение можно запустить командой


$ mix phoenix.server

и убедится, что все работает, как задумано.


Статические файлы и JS


Теперь обратим внимание на структуру каталогов проекта, а именно, на два из них – priv/static/ и web/static/. В первом из них уже лежат файлы, которые нужны для отображения фениксовской "Hello, World!" страницы, и именно этот каталог используется приложением, когда оно запущенно, для отдачи статических файлов. Второй каталог, web/static/, по-умолчанию задействован при разработке, и Brunch (в проектах с ним), грубо говоря, перекладывает файлы из него в priv/static, попутно обрабатывая их (статья в официальной документации об этом).


Оба вышеозначенных каталога находятся под управлением системы контроля версий, в оба из них можно добавлять файлы, вот только если вы добавите файлы сразу в priv/static/, то Brunch'ем они обработаны не будут, а если в web/static/, то будут, но если вы положите файл в web/static/assets/, то снова не будут… Мне кажется, что тут что-то пошло не так, потому я предлагаю более строгий подход, а именно:


  • содержимое каталога priv/static/ никогда не оказывается там в результате неких ручных действий, только в результате работы какого-то пайплайна. Более того, этот каталог выносится из VCS и добавляется в .gitignore;
  • каталог web/static/ содержит статические файлы, которые без изменений будут скопированы в /priv/static соответствующим пайплайном при компиляции, сборке релизного пакета и т.д.
  • все остальное, что должно оказаться в priv/static/ (js, например) лежит где-то в другом месте в дереве исходников и попадает в результирующий каталог только через соответствующий пайплайн бандлера.

Итак, следующим шагом я почистил priv/static от ненужных файлов, а robots.txt и favicon.ico перенес в web/static/ – вернемся к ним позже. Также, почистил html разметку главной страницы и ее шаблона (коммит).


Перед тем, как добавлять Webpack, надо инициализировать сам NPM:


$ npm init

Получившийся package.json я почистил, оставив в нем только самое главное (коммит):


{
  "name": "userlist",
  "version": "1.0.0",
  "description": "Phoenix example application",
  "scripts": {
  },
  "license": "MIT"
}

И после этого добавляем сам Webpack (коммит):


$ npm install --save-dev webpack

Теперь давайте добавим какой-то минимально возможный JS код к проекту, например, такой:


console.log("App js loaded.");

Для JS-файлов я создал каталог web/js/, куда и положил файл app.js с кодом выше. Подключим его в шаблоне web/templates/layout/app.html.eex, вставив перед закрывающим тегом :


js/app.js") %>">

Очень важно использовать макрос static_path, иначе вы потеряете возможность загружать ресурсы с digest-меткой, что приведет к проблемам с инвалидацией кешей у клиентов и вообще, так не по правилам.


Создаем конфигурацию Webpack'а – файл webpack.config.js в корне проекта:


module.exports = {
  entry: __dirname + "/web/js/app.js",
  output: {
    path: __dirname + "/priv/static",
    filename: "js/app.js"
  }
};

Из кода видно, что результирующий файл app.js будет находится в каталоге priv/static/js/ как и задумывалось. На данном этапе можно запустить Webpack вручную, но это не очень удобно, так что добавим автоматизации, благо фреймворк это позволяет. Первое, что надо сделать, это добавить шорткат watch в секцию scripts файла package.json:


"scripts": {
  "watch": "webpack --watch-stdin --progress --color"
},

Теперь Webpack можно запускать командой


$ npm run watch

Но и этого делать не надо, пускай этим занимается Phoenix, тем более, что у эндпоинта вашего приложения есть опция watchers, как раз и предназначенная для запуска подобных внешних утилит. Изменим файл config/dev.exs, добавив вызов npm:


watchers: [npm: ["run", "watch"]]

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


$ mix phoenix.server

Коммит со всеми вышеозначенными изменениями тут.


C JS кодом немного разобрались, но еще остаются файлы в web/static/. Задачу по их копированию я тоже возложил на Webpack, добавив в него расширение copy:


$ npm install --save-dev copy-webpack-plugin

Сконфигурируем плагин в в файле webpack.config.js(коммит):


var CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: __dirname + "/web/js/app.js",
  output: {
    path: __dirname + "/priv/static",
    filename: "js/app.js"
  },
  plugins: [
    new CopyWebpackPlugin([{ from: __dirname + "/web/static" }])
  ]
};

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


Может возникнуть вопрос – что делать, если надо производить пред-обработку, например, CSS. Ответ банален – выносить CSS в отдельный каталог, добавлять в Webpack соответствующие плагины и настраивать пайплайн, аналогичный используемому для JS. Либо использовать css-loader'ы, но это отдельная история.


Сборка релизного пакета. Distillery.


Distillery это второй заход автора Exrm в попытке сделать хороший инструмент для пакетирования и создания релизных пакетов для проектов на Elixir. Ошибки первого были учтены, многое исправлено, пользоваться Distillery удобно. Добавим его в проект, указав как зависимость в mix.exs:


{:distillery, "~> 1.4"}

Обновим зависимости и создадим шаблон релизной конфигурации (коммит):


$  mix deps.get && mix release.init

Последняя команда создаст файл rel/config.exs примерно такого содержания:


Много кода
Path.join(["rel", "plugins", "*.exs"])
|> Path.wildcard()
|> Enum.map(&Code.eval_file(&1))

use Mix.Releases.Config,
    # This sets the default release built by `mix release`
    default_release: :default,
    # This sets the default environment used by `mix release`
    default_environment: Mix.env()

environment :dev do
  set dev_mode: true
  set include_erts: false
  set cookie: :"Mp@oK==RSu$@QW.`F9(oYks&xDCzAWCpS*?jkSC?Zo{p5m9Qq!pKD8!;Cl~gTC?k"
end

environment :prod do
  set include_erts: true
  set include_src: false
  set cookie: :"/s[5Vq9hW(*IA>grelN4p*NjBHTH~[gfl;vD;:kc}qAShL$MtAI1es!VzyYFcC%p"
end

release :userlist do
  set version: current_version(:userlist)
  set applications: [
    :runtime_tools
  ]
end

Предлагаю оставить его пока таким, как он есть. Указанного в конфигурации вполне достаточно: один релиз :userlist, он же :default, т.к. первый и единственный в списке релизов, а так же два окружения :dev и :prod. Под релизом здесь понимается OTP Release – набор приложений, который войдет в результирующий пакет, версию ERTS. В данном случае, наш релиз соответствует приложению :userlist, чего нам достаточно. Но, мы можем иметь несколько релизов и несколько окружений и комбинировать их по необходимости.


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


Подготовим приложение к релизу. В первую очередь, надо отредактировать файл config/prod.secret.exs, поправим в нем настройки БД. Этот файл не добавляется в VCS, потому, в случае его отсутствия, его надо создать самому с примерно следующий содержанием:


prod.secret.exs
use Mix.Config

config :userlist, Userlist.Endpoint,
  secret_key_base: "uE1oi7t7E/mH1OWo/vpYf0JLqwnBa7bTztVPZvEarv9VTbPMALRnqXKykzaESfMo"

# Configure your database
config :userlist, Userlist.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "phoenix",
  password: "",
  database: "userlist_prod",
  pool_size: 20

Следующим важным этапом будет поправить конфигурацию Userlist.Endpoint в файле config/prod.exs. Прежде всего, заменить хост на нужный, а порт с 80 на читаемый из окружения параметр PORT и добавить важнейшую опцию server, которая является признаком того, что именно этот эндпоинт запустит Cowboy:


url: [host: "localhost", port: {:system, "PORT"}],
...
server: true

Далее, я добавил Babel к пайплайну обработки JS кода, т.к. UglifyJS, используемый по-умолчанию в Webpack, не обучен обращению с ES6:


$ npm install --save-dev babel-loader babel-core babel-preset-es2015

И секция настройки Babel в webpack.config.js после plugins:


webpack.config.js
module: {
  loaders: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader",
      query: {
        presets: ["es2015"]
      }
    }
  ]
}

И последнее – добавляем шорткат deploy в конфигурацию NPM (коммит):


"scripts": {
  "watch": "webpack --watch-stdin --progress --color",
  "deploy": "webpack -p"
},

На данном этапе можно попробовать собрать и запустить релиз:


$ npm run deploy
$ MIX_ENV=prod mix phoenix.digest
$ MIX_ENV=prod mix release
$ PORT=8080 _build/prod/rel/userlist/bin/userlist console

Первой командой мы подготавливаем JS (минификация и т.п.), копируем static-файлы; вторая генерирует для всех файлов дайджест; третья непосредственно собирает релиз для соответствующего окружения. Ну и в конце – запуск приложения в интерактивном режиме, с консолью.


После релиза в каталоге _build будет находится распакованная (exploded) версия пакета, а архив будет лежать по пути _build/prod/rel/userlist/releases/0.0.1/userlist.tar.gz.


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


Миграции


После сборки, исполняемый файл приложения предоставляет нам одну из опций, которая называется command:


command   [] # execute the given MFA

Это очень похоже на rpc, с разницей в том, что command выполнится и на не запущенном приложении – что нам и надо. Создадим модуль с функцией миграции, помня о том, что приложение запущенно не будет. Я разместил этот файл по пути lib/userlist/release_tasks.ex (коммит):


defmodule Release.Tasks do
  alias Userlist.Repo

  def migrate do
    Application.load(:userlist)
    {:ok, _} = Application.ensure_all_started(:ecto)
    {:ok, _} = Repo.__adapter__.ensure_all_started(Repo, :temporary)
    {:ok, _} = Repo.start_link(pool_size: 1)

    path = Application.app_dir(:userlist, "priv/repo/migrations")

    Ecto.Migrator.run(Repo, path, :up, all: true)

    :init.stop()
  end
end

Как видно из кода, мы загружаем, а потом запускаем не все приложения, а ровно необходимые – в данном случае, это только Ecto. Теперь все, что осталось, это пересобрать релиз (только Elixir, т.к. остальное не менялось):


$ MIX_ENV=prod mix release

запустить миграции:


$ _build/prod/rel/userlist/bin/userlist command 'Elixir.Release.Tasks' migrate

и запустить приложение:


$ PORT=8080 _build/prod/rel/userlist/bin/userlist console

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


Хуки и команды Distillery


Концепция хуков и команд проста – это обычные shell-скрипты, которые вызываются на определенном этапе жизни приложения (хуки), либо вручную (команды) и которые являются расширением главного исполняемого boot-скрипта. Хуки могут быть четырех видов: pre/post_start и pre/post_stop.


Я добавил пример двух хуков в проект, смотрите код, он лучше всего объяснит, как это сделать.


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


$ _build/prod/rel/userlist/bin/userlist migrate

Если вам нужен manifest.json


При сборке релиза, после выполнения команды phoenix.digest, все статические файлы получают хеш-сумму в свое имя (плюс добавляются сжатые версии), и генерируется таблица соответствия между исходным именем файла и новым, которая находится в файле priv/static/manifest.json, если вы не меняли его положение в конфигурации. Если вдруг вам понадобится информация из него во время выполнения приложения, то у вас два варианта:


  • добавить его в список файлов, которые отдаются из каталога со статикой в lib/userlist/endpoint.ex:


    only: ~w(css fonts images js favicon.ico robots.txt manifest.json)

    после чего, его можно будет забрать Ajax'ом, например;


  • если он нужен на бекенде, или если вы хотите рендерить его в шаблоне (я не знаю, зачем, но вдруг надо), то можно расширить LayoutView до такого:


    defmodule Userlist.LayoutView do
    use Userlist.Web, :view
    
    def digest do
      manifest =
        Application.get_env(:userlist, Userlist.Endpoint, %{})[:cache_static_manifest]
        || "priv/static/manifest.json"
    
      manifest_file = Application.app_dir(:userlist, manifest)
    
      if File.exists?(manifest_file) do
        manifest_file
        |> File.read!
      else
        %{}
      end
    end
    end

    чтобы потом, где-то в шаблоне, написать следующее:




Коммит с эти безумием тут.


systemd


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


Допустим, что мы будем разворачивать архив с приложением в /opt/userlist/ и запускать от имени пользователя userlist. Создадим файл userlist.service следующего содержания (коммит):


# Userlis is a Phoenix, Webpack and Distillery demo application

[Unit]
Description=Userlist application
After=network.target

[Service]
Type=simple
User=userlist
RemainAfterExit=yes
Environment=PORT=8080
WorkingDirectory=/opt/userlist
ExecStart=/opt/userlist/bin/userlist start
ExecStop=/opt/userlist/bin/userlist stop
Restart=on-failure
TimeoutSec=300

[Install]
WantedBy=multi-user.target

После чего, все, что надо сделать, это скопировать его в /etc/systemd/system/:


$ sudo cp userlist.service /etc/systemd/system

Включить в "автозагрузку":


$ sudo systemctl enable userlist.service

И запустить приложение:


$ sudo systemctl start userlist

Заключение


Целью данной статьи была попытка собрать воедино разрозненную информацию по разным темам, касающуюся Phoenix'а и дать какое-то более-менее цельное представление о жизненном цикле приложений, написанных на этом замечательном фреймворке. Очень много осталось за кадром, есть куча тем, достойная отдельных статей, например, способы доставки релизных пакетов на сервер и т.п.


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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331598/


Метки:  

Перевод отрывков из книги Роберта Хайнлайна «Заберите себе правительство» — часть 25

Воскресенье, 25 Июня 2017 г. 23:59 + в цитатник

Глава 10 Как выиграть выборы: заключение


Финишная прямая


Рассылка напоминаний в последнюю неделю перед выборами


За время кампании ваш кандидат посетил дома более 3 000 избирателей, а, возможно даже до 5 000. (Скажете, фантастика? Вовсе нет: в свое время, в одной из своих кампаний, я посетил 8 000 избирателей). Ваши агитаторы посетили еще 25 000 человек, и вы, конечно же, тоже участвовали в обходе избирателей. Говорите, у вас не было времени? Мои дорогие леди или сэр, на это у вас должно найтись время! Советую выделить для посещений избирателей вторую половину дня вторника и четверга, с часу дня до пяти вечера. И не назначайте никаких дел на это время.


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


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


«Пять тысяч личных писем?» — скажете вы – «Да ведь даже профессиональная машинистка потратит на их печать не менее четырех месяцев!» И это действительно так. Но не все так плохо: существует механизм, изобретенный человеком по имени Гувен, способный с введенного текста, отпечатать много его копий – штука наподобие механического пианино. В образец текста можно даже вставить команды, встретив которые механизм делает паузу, позволяя оператору впечатать имя, дату, личное обращение, и любые другие добавления в исходный текст, никак не влияя на печать остального текста. Письмо, отпечатанное этим механизмом, не отличить от напечатанного человеком вручную. Службы печати, использующие механизм Гувена, существуют во всех больших городах. Если в вашем городе такой нет, вы можете воспользоваться их услугами по почте. Этот способ печати дороже, чем типографская печать, но намного дешевле услуг профессиональной машинистки («Когда-нибудь» – размечтался он – «и наш окружной комитет обзаведется этой замечательной машиной»)


Текст письма старайтесь сделать кратким и ёмким – для эффективности и экономии средств. Вот примерный образец такого письма:


Заголовок письма

Дата

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

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

Искренне вам,
Джонатан Честняга

В этом письме, имя избирателя и дата визита к нему – единственные поля, требующие остановки автомата Гувена, чтобы можно было вписать конкретные данные. Если же вы используете открытки с напечатанным в типографии текстом, тогда вам придется ограничиться общими формулировками, вроде «Уважаемый Демпубликанец» и «недавний визит». Но все же, лучше разослать такие открытки всем избирателям из вашего списка, чем разослать личные письма, только части избирателей. Только не поддавайтесь искушению разослать напоминания поголовно всем зарегистрированным в округе избирателям: такая рассылка не оправдает ваших затрат.


Вашему кандидату желательно пролистать уже готовые открытки, чтобы снабдить хотя бы часть из них постскриптумами, обращенными к избирателю лично. Эти постскриптумы могут быть приписаны от руки тем же человеком, который будет открытки подписывать. Например: «P.S.Привет вашему щеночку – Дж.Ч.», или «Ваш сын совсем взрослый, надеюсь на его голос на выборах 1960 года» «Кстати, напишите мне ваше мнение по поводу того предложения, которое мы обсуждали», и «Надеюсь, ваш муж уже полностью выздоровел».


Возможно, некоторые из ваших агитаторов в своем избирательном участке могут себе позволить сами воспользоваться службой печати Гувена, или же оказаться настолько усердными, что смогут подготовить личные письма вручную – от руки, или на печатной машинке. Это большая работа, но в масштабах одного избирательного участка она выполнима. Или же, вы можете снабдить своих агитаторов открытками с отпечатанной на них вашей «торговой маркой» – портретом мистера Честняги, занимающим примерно треть открытки, обращением: «Уважаемый избиратель», и основным текстом вашего напоминания избирателям. Используйте шрифт, имитирующий шрифт печатной машинки и оставьте место для подписи агитатора.


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


Особое внимание надо уделить незарегистрированным избирателям, которых в ходе кампании нашли мистер Честняга и ваши агитаторы, и которые могут за вас проголосовать. Ведь вы регулярно получали отчеты от своих сотрудников о найденных ими незарегистрированных избирателях, имена которых вы передавали имена регистратору избирателей, с которым, конечно же, поддерживаете дружеские отношения. Эти голоса – ваши, если вы хорошенько попросите их обладателей проголосовать. Их может набраться до нескольких тысяч, чего достаточно для превращения досадного поражения в трудную победу. Это те самые голоса, которых Томасу Дьюи не хватило на выборах 1944 года – голоса «спящих» избирателей. Уделите особое внимание посланным им напоминаниям, и опекайте их в день выборов. Можно в напоминаниях для них использовать более индивидуализированные тексты, чем ваш основной текст напоминания.


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


День выборов


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


Но такой идеальной организации у вас не будет. И ничего похожего вы нигде не увидите, за исключением некоторых организаций в больших городах на Миссисипи, но и там они не будут настолько идеальными. Идеал вашей организации, которого вы в лучшем случае, достигнете на 80% – это три полевых сотрудника на каждом избирательном участке, по одному – на каждом пункте голосования, один человек на телефоне, один – с автомобилем наготове, командиры групп, доступные по телефону, в случае необходимости, выезжающие в нужное место вверенной им территории, телефон, группа ваших личных помощников, и два юриста на связи, немедленно выезжающие на место в случае возникновения проблем. Для силового давления вы обходитесь своей группой помощников, надеясь на то, что, даже самые бессовестные полицейские не допустят беззакония, если им известно, что за их действиями наблюдает юрист. Мистер Честняга проведет этот день, разъезжая по участкам, и воодушевляя сотрудников на местах.


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


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

  • Постарайтесь охватить своей опекой те участки, где вы сами обходили избирателей.

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

  • Если на участке у вас работает только один сотрудник, то он или она могут сделать почти столько же, сколько сделали бы три человека, следуя следующему расписанию: в вечер перед выборами и с 8 до 10 часов утра дня выборов сотрудник должен обзвонить столько избирателей, сколько сможет. Тем из избирателей, кого нужно привезти на пункт голосования, нужно назначить время встречи с 10 до 12 часов утра, собрать их в назначенное время, и отвезти всей группой на участок. После обеда сотрудник едет на избирательный пункт, чтобы посмотреть, кто уже проголосовал, вычеркнув их из своих списков. А после этого – пора приниматься за работу с теми, кто еще не проголосовал, назначая им время встречи с 4 до 6 часов вечера, чтобы отвезти их проголосовать.

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

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

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

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

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

  • Сотрудников для координации ваших действий по телефону можно найти среди ваших сторонников, или жен ваших агитаторов, тех из них, которые сидят дома с детьми, или заболели, но могут работать по телефону. Их надо снабдить составленными вашими агитаторами списками избирателей, и подготовленными вами инструкциями, описывающими как им говорить с избирателями. Вот подходящая формулировка такого диалога: «Здравствуйте, это миссис Дуплекс? Миссис Дуплекс, вас беспокоит предвыборный штаб кандидата в Конгресс Джонатана Честняги. Вы уже проголосовали сегодня? Может быть, вас нужно отвезти на избирательный участок и обратно? Ребенка вы можете взять с собой, мы присмотрим за ним те несколько минут, пока вы будете голосовать. Есть ли в вашей семье кто-то еще, кого нужно отвезти голосовать? Хорошо, тогда мы подъедем за вами с 10 до 12 дня, вам удобно это время? Нет? Мы можем подъехать за вами с 4 до 6 вечера. А во сколько вам удобнее? Хорошо, мы заедем за вами отдельно, в 3 часа дня, я запишу. Не благодарите, мы рады вам помочь».

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

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


-> Часть 1, где есть ссылки на все остальные части

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331634/


Метки:  

Дайджест свежих материалов из мира фронтенда за последнюю неделю №268 (19 — 25 июня 2017)

Воскресенье, 25 Июня 2017 г. 23:55 + в цитатник
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Веб-разработка
CSS
Javascript
Браузеры
Занимательное

Веб-разработка



CSS



JavaScript



Браузеры



Занимательное



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



Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331632/


PHP-Дайджест № 111 – свежие новости, материалы и инструменты (12 – 25 июня 2017)

Воскресенье, 25 Июня 2017 г. 23:39 + в цитатник


Свежая подборка со ссылками на новости и материалы. В выпуске: PHP 7.2.0 Alpha 2, пара новых RFC, материалы с YiiConf и FWDays, PHP руткит, и многое другое.
Приятного чтения!



Новости и релизы




PHP Internals


  • RFC: Retry functionality — Предлагается расширить try-catch-finally блоком и ключевым словом retry. В случае бросания соответствующего исключения при наличии retry, блок try будет повторяться:
    try {
        somethingSketchy();
    } retry 3 (RecoverableException $e, $attempt) {
        echo "Failed doing sketchy thing on try #{$attempt}. Retrying...";
        sleep(1);
    } catch (RecoverableException $e) {
        echo $e->getMessage();
    }
    

    try {
        somethingSketchy();
    } catch (RecoverableException $e)
        retry; // Go to top of try block
    }
    

  • RFC: Unary null coalescing operator — Предлагается реализовать унарную версию оператора ??, добавленного в PHP 7.0:
    if ($_POST["action"]?? === "submit") {
        // Form submission logic
    } else {
        // Form display logic
    }
    


Инструменты


  • amphp/amp 2.0.0 — Мощный асинхронный фреймворк с лаконичным интерфейсом благодаря генераторам. Также доступен ряд дополнительных компонентов: асинхронные mysql и postgres клиенты, DNS-резолвер, HTTP/WebSocket сервер, и другие.
  • wapmorgan/ServerAvailabilityMonitor — Утилита мониторит серверы на доступность и присылает отчеты об ошибках на почту. Поддерживает http, mysql, pgsql, memcache и redis. Прислал wapmorgan.
  • paragonie/sapient — Библиотека для обеспечения безопасности API, даже когда TLS сломан. Пост в поддержку.
  • genkgo/mail — Библиотека для отправки почты. Годная альтернатива PHPMailer или Swift Mailer.
  • prooph/event-store — EventStore на PHP 7.1 для реализации паттерна Event Sourcing.
  • jonathantorres/construct — Инструмент генерирует структуру папок и файлы для нового PHP-проекта.
  • wikimedia/composer-merge-plugin — Плагин для Composer, который объединяет несколько composer.json файлов налету. Удобно для разделения проекта на внутренние компоненты со своими зависимостями.


Материалы для обучения



Спасибо за внимание!

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

Прислать ссылку
Быстрый поиск по всем дайджестам
<- Предыдущий выпуск: PHP-Дайджест № 110

Стоит ли добавить функциональность retry в PHP?

Проголосовал 1 человек. Воздержался 1 человек.

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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331630/


Метки:  

[Из песочницы] Сглаживание изображений фильтром анизотропной диффузии Перона и Малика

Воскресенье, 25 Июня 2017 г. 19:40 + в цитатник
Фильтр анизотропной диффузии Перона и Малика — это сглаживающий цифровые изображения фильтр, ключевая особенность которого состоит в том, что при сглаживании он сохраняет и «усиливает» границы областей на изображении.

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


Крайнее левое изображение — оригинальное, справа от оригинального — фильтрованные с различными параметрами.

Введение


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

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

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

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

Фильтр Перона и Малика


Впервые был описан в статье указанных математиков в 1990 году (ссылка на статью приведена в «источниках» в конце поста). Не будем вдаваться в глубокие теоретические детали, а хотя бы поверхностно рассмотрим как работает фильтр.

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

Теория


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

$$display$$I_{t}(x,y,t)=div(c(x,y,t)\nabla I(x,y,t)) \: \: \: \: \: \: \: \: (1)$$display$$


C начальным условием:

$$display$$I(x,y,0) = I_{0}(x,y)$$display$$


Где:

$inline$I(x,y,t)$inline$ — однопараметрическое семейство изображений; чем больше $inline$t$inline$, тем больше степень размытия исходного изображения;
$inline$I_{0}(x,y)$inline$ — исходное изображение;
$inline$I_{t}$inline$ — производная по $inline$t$inline$;
$inline$div$inline$ — оператор дивергенции;
$inline$\nabla$inline$ — градиент;

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

По своей сути фильтр анизотропной диффузии является модификацией фильтра Гаусса.

Если подставить вместо, пока еще не рассмотренной, функции $inline$c(x,y,t)$inline$ единицу, то есть $inline$с(x,y,t)\equiv 1$inline$, то получается уравнение изотропной диффузии:

$$display$$I_{t}(x,y,t)=div(\nabla I(x,y,t))=\frac{\partial^{2}I}{\partial x^{2}}+\frac{\partial^{2}I}{\partial y^{2}}$$display$$


Математики Коендеринк и Гумел показали, что такое семейство размытых изображений по параметру $inline$t$inline$, можно эквивалентно получить как решение уравнения свертки функции изображения с ядром Гаусса (это и есть фильтр Гаусса):

$$display$$I(x,y,t)= I_{0}(x,y)*G(x,y;t)$$display$$


Где:

$inline$*$inline$ — оператор свертки;
$inline$G(x,y;t)={\frac {1}{2\pi t^{2}}}e^{-{\frac {x^{2}+y^{2}}{2t^{2}}}}$inline$ — функция ядра Гаусса c нулевым математическим ожиданием и $inline$t$inline$ среднеквадратичным отклонением;

Очевидно, функция $inline$с(x,y,t)$inline$ играет роль некоторого «регулировщика» сглаживания.

Исходя из уравнения фильтра (1), для сохранения изначального значения в точке изображения нужно, чтобы производная по времени равнялась нулю (то есть значение на любых слоях размытия было константой). Получаем следующие условия на функцию $inline$c$inline$:

$inline$c(x,y,t)=0$inline$ — на границах;
$inline$c(x,y,t)=1$inline$ — внутри областей, иначе говоря, внутри областей должно происходить обычное гауссово размытие;

Перона и Малик использовали градиент функции изображения $inline$\nabla I$inline$ как простую для расчета и достаточно точную аппроксимацию границ областей. Чем больше норма градиента, тем четче граница. Исходя из этого получаем:

$$display$$c(x,y,t)=g(||\nabla I(x,y,t)||)$$display$$


Где $inline$g(.)$inline$ — некоторая функция с областью значений на отрезке $inline$[0;1]$inline$.

Из-за нечеткого определения границ через норму градиента, требуют также, чтобы функция $inline$g$inline$ была монотонной убывающей.

Перона и Малик предложили два варианта функции $inline$g$inline$:

$$display$$g(x)=e^{-(\frac{x}{k})^{2}} \: \: \: \: \: \: \: \: (2)$$display$$


$$display$$g(x)=\frac{1}{1+(\frac{x}{k})^{2}} \: \: \: \: \: \: \: \: (3)$$display$$


Где $inline$k$inline$ — параметр, определяемый либо опытным путем, либо некоторым «измерителем» зашумленности.

Рассмотрим детальнее вторую функцию (формула 3). Для этого построим графики функции $inline$g$inline$ для нескольких разных $inline$k$inline$.



Видно, что чем больше $inline$k$inline$, тем больше значение функции $inline$g$inline$ для любой фиксированной точки.

Например, пусть в некоторой точке изображения, назовем ее $inline$(\tilde{x},\tilde{y})$inline$, мы знаем значение нормы градиента на уровне $inline$\tilde{t}$inline$: $inline$||\nabla I(\tilde{x},\tilde{y}, \tilde{t})||=4$inline$. Тогда $inline$g_{k=2}(||\nabla I(\tilde{x},\tilde{y}, \tilde{t})||)=g_{k=2}(4)=0.2$inline$, в то время как $inline$g_{k=16}(4)\approx 0.94$inline$. Получается в первом случае мы слабо «сгладили» значение в точке, так как по введенному ранее критерию она скорее всего лежит на границе, а во втором — значение функции $inline$g$inline$ практически единица, соответственно точка не считается граничной и будет сглажена обычным гауссовым размытием.

Таким образом, $inline$k$inline$ выступает в роли «барьера» для шумов, и с возрастанием $inline$k$inline$ возрастает «требование» на то, что точка будет считаться граничной.

Дискретизация


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

$$display$$\frac{\partial }{\partial x}I(x,y,t)=\frac{\partial }{\partial x}\left [ c(x,y,t)\frac{\partial }{\partial x}I(x,y,t) \right ]+\frac{\partial }{\partial y}\left [ c(x,y,t)\frac{\partial }{\partial y}I(x,y,t) \right ]$$display$$


Теперь запишем конечно-разностную явную схему:

$inline$ I(x,y,t+\Delta t)-I(x,y,t)=\\ \hspace{55mm}=\frac{1}{(\Delta x)^{2}}\left [ c(x+\frac{\Delta x}{2},y,t)\cdot (I(x+\Delta x,y,t)-I(x,y,t))-\\ \hspace{68mm}-c(x-\frac{\Delta x}{2},y,t)\cdot (I(x,y,t)-I(x-\Delta x,y,t))\right ]+\\ \hspace{55mm}+\frac{1}{(\Delta y)^{2}}\left [ c(x,y+\frac{\Delta y}{2},t)\cdot (I(x,y+\Delta y,t)-I(x,y,t))-\\ \hspace{68mm}-c(x,y-\frac{\Delta y}{2},t)\cdot (I(x,y,t)-I(x,y-\Delta y,t))\right ]$inline$

Записать условие устойчивости для получившейся явной схемы — нетривиальная задача из-за нелинейности уравнения. Но Перона и Малик определили, что при $inline$\Delta x=\Delta y=1$inline$ схема будет устойчива для всех $inline$0\leq \Delta t \leq \frac{1}{4}$inline$. Учитывая этот факт и то, что дискретным представлением функции изображения будет матрица значений пикселей, перепишем основную расчетную схему в матричном виде:

$$display$$I_{i,j}^{t+1}=I_{i,j}^{t}+\Delta t\left [ {C_{N}}_{i,j}^{t}\cdot \bigtriangledown_{N}I_{i,j}^{t}+{C_{S}}_{i,j}^{t}\cdot \bigtriangledown_{S}I_{i,j}^{t}+{C_{E}}_{i,j}^{t}\cdot \bigtriangledown_{E}I_{i,j}^{t}+{C_{W}}_{i,j}^{t}\cdot \bigtriangledown_{W}I_{i,j}^{t} \right ] $$display$$


Где:

$inline$\bigtriangledown_{N}I_{i,j}^{t}=I_{i-1,j}^{t}-I_{i,j}^{t}$inline$
$inline$\bigtriangledown_{S}I_{i,j}^{t}=I_{i+1,j}^{t}-I_{i,j}^{t}$inline$
$inline$\bigtriangledown_{E}I_{i,j}^{t}=I_{i,j+1}^{t}-I_{i,j}^{t}$inline$
$inline$\bigtriangledown_{W}I_{i,j}^{t}=I_{i,j-1}^{t}-I_{i,j}^{t}$inline$

$inline${C_{N}}_{i,j}^{t}=g(||{(\nabla I)}_{i-1/2,j}^{t}||)$inline$
$inline${C_{S}}_{i,j}^{t}=g(||{(\nabla I)}_{i+1/2,j}^{t}||)$inline$
$inline${C_{E}}_{i,j}^{t}=g(||{(\nabla I)}_{i,j+1/2}^{t}||)$inline$
$inline${C_{W}}_{i,j}^{t}=g(||{(\nabla I)}_{i,j-1/2}^{t}||)$inline$

Для расчета коэффициентов $inline$C$inline$ вначале необходимо вычислить норму градиента. Самый простой способ аппроксимировать норму градиента — это заменить ее на длину проекции градиента на соответствующую ось разностной сетки.

$inline${C_{N}}_{i,j}^{t}=g(|\bigtriangledown_{N}I_{i,j}^{t}|)$inline$
$inline${C_{S}}_{i,j}^{t}=g(|\bigtriangledown_{S}I_{i,j}^{t}|)$inline$
$inline${C_{E}}_{i,j}^{t}=g(|\bigtriangledown_{E}I_{i,j}^{t}|)$inline$
$inline${C_{W}}_{i,j}^{t}=g(|\bigtriangledown_{W}I_{i,j}^{t}|)$inline$

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

Окончательно расчетная формула будет выглядеть так:

$$display$$I_{i,j}^{t+1}=I_{i,j}^{t}+\Delta t\left [ g(|N|)\cdot N+g(|S|)\cdot S+g(|E|)\cdot E+g(|W|)\cdot W \right ] \: \: \: \: \: \: \: \: (4) $$display$$


Где:

$inline$N=\bigtriangledown_{N}I_{i,j}^{t}$inline$
$inline$S=\bigtriangledown_{S}I_{i,j}^{t}$inline$
$inline$E=\bigtriangledown_{E}I_{i,j}^{t}$inline$
$inline$W=\bigtriangledown_{W}I_{i,j}^{t}$inline$

Схематично расчетную схему можно изобразить как на рисунке ниже.


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


Чтобы фильтр менял значения яркости в ячейках в рамках максимума и минимума яркости исходного изображения, процесс, описанный основным уравнением, должен быть адиабатическим. Отсюда имеем граничное условие Дирихле вида: $inline$I(D)=0$inline$. То есть просто заполняем границы нулями.


Алгоритм и реализация на языке Fortran


Исходя из расчетной формулы в памяти всегда придется держать как минимум два двумерных массива, первый будет соответствовать оригинальному изображению, второй — размытому. Если провести аналогию с фильтром Гаусса, то для размытия изображения с радиусом $inline$t$inline$ в фильтре Перона и Малика нам нужно выполнить $inline$\frac{t}{\Delta t}$inline$ повторений полного пересчета изображения, каждый раз заменяя изображение с предыдущего слоя размытия на «оригинальное».
Последовательность действий будет примерно такая:

  1. Определяем ширину и высоту изображения (назовем w и h соответственно);
  2. Выделяем память под двумерный массив размерностью (w+2)*(h+2) (назовем I);
  3. Выделяем память под двумерный массив размерностью (w+2)*(h+2) (назовем BlurI);
  4. Заполняем массив нулями I=0;
  5. Считываем изображение и записываем данные пикселей в центр массива I (то есть оставляем слева, справа, сверху, снизу по одному нулевому столбцу или строке);
  6. Повторяем пока level
    1. Пересчитываем по формуле 4 все ячейки массива I, учитывая смещенную индексацию из-за нулевых полей. Считаем, что $inline$I_{i,j}^{t+1}$inline$ — это BlurI, а $inline$I_{i,j}^{t}$inline$ — I;
    2. Заменяем данные изображение в I на BlurI;
    3. Увеличиваем счетчик, level=level+deltaT, где deltaT — параметр, шаг по времени;

  7. Создаем и сохраняем изображение из данных массива BlurI. Это наше размытое изображение;
  8. Освобождаем память, выходим из программы;

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

Реализация на Fortran


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

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

Самым простым форматом, который к тому же понимают и многие графические редакторы, мне показался PGM P5. Это открытый формат хранения растровых изображений типа bitmap без сжатия в оттенках серого. У него простой ASCII заголовок, а само изображение есть последовательность однобайтных (для оттенков серого от 0 до 255) целых чисел без знака. Подробнее о формате вы можете прочитать непосредственно в самом стандарте, ссылка приведена в разделе источников.

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

Начнем программу с подключения модуля для работы с PGM и объявления всех необходимых переменных.

program blur
  use pgmio

  implicit none
  
  double precision, parameter :: t=10.0d0, deltaT=0.2d0, k=10.0d0
  character*(*), parameter :: input='dif_tomography.pgm', output='output.pgm'

  double precision, allocatable :: u(:,:), nu(:,:)
  double precision :: north, south, east, west, level
  integer :: w, h, offset, n, i, j

end program blur

Параметр t — это уровень размытия, на котором мы хотим прекратить работу алгоритма, deltaT — шаг по времени, k — параметр для пока еще не описанной функции g. Input и output — файл с исходным изображением и выходной файл соответственно.

Теперь считаем размеры входного изображения в переменные w и h.

call pgmsize(input, w, h, offset)

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

Выделим память для массива изображения и массива сглаженного изображения и считаем в первый данные из файла input.

allocate(u(0:w+1,0:h+1))
u=0
allocate(nu(w,h))
call pgmread(input, offset, w, h, u, 0, 0)

Фортран позволяет программисту самому определять индекс первого элемента в массиве, поэтому, учитывая, что вокруг изображения должны быть нули, заранее заполняем матрицу u нулями, задав ей размер от 0 до w+1 по первому измерения, и от 0 до h+1 по второму. Это позволит при дальнейшем пересчете использовать естественную индексацию без смещения.

Процедура pgmread считываем w*h байт, пропуская offset байт (занимаемых заголовком PGM) в массив u. Последние два параметра сообщают процедуре, что отсчет в матрице u начинается с нуля по каждому измерению.

Хотя в данном случае это неважно, хочу отметить, что изображение будет считано в транспонированном виде, так как фортран хранит двумерные массивы как последовательности столбцов, а формат PGM хранит изображения как последовательности строк.

Теперь перейдем с самому сглаживанию.

  level = 0 !счетчик уровня сглаживания
  do while (levelcode>

Тут все очевидно, единственное, что может быть не совсем очевидно для новичков в Fortran — это вложенность циклов, а именно — цикл по j идет раньше цикла по i. Все, опять же, из-за того, что фортран хранит двумерные массивы в памяти как последовательности столбцов. Если помять циклы местами, программа также будет работать, но значительно медленнее — программа будет обходить память не последовательно, а перебегать из разных ячеек «туда-сюда».

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

  deallocate(u)

  call pgmwriteheader(output, w, h) 
  call pgmappendbytes(output, nu, 1, 1) 

  deallocate(nu)

Процедура pgmwriteheader создает файл output и записывает в него заголовок PGM P5. Процедура pgmappendbytes записывает в конец файла output последовательность байт из nu, учитывая, что индексы nu начинаются с 1 по обоим измерениям. Замечу, что pgmappendbytes записывает байты из двумерного массива опять же в порядке столбцов, поэтому, хотя в памяти и находилось транспонированная версия изображения, при записи изображение транспонируется обратно.

Осталось определить функцию g. Например, реализуем функцию по формуле 3.

  contains 
      function g(x) result(v)
        implicit none
        double precision, intent(in) :: x
        double precision :: v
        v = 1/(1+(x/k)**2)
      end function g

-> Ссылка на полный код программы

Компилировал все через gfortran следующей командой (при условии, что все файлы лежат в одной директории):

gfortran pgmio.f90 blur.f90

Примеры работы фильтра


Для начала сравним размытие фильтра Перона и Малика с гауссовым размытие при
одинаковых t.

Исходное изображение.



Сгладим это изображение фильтром Гаусса и фильтром анизотропной диффузии.



Рассмотрим еще одно изображение.



Сглаживание фильтром Гаусса (t=10).



Сглаживание фильтром Перона и Малика (t=10, k=8).



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

Поэкспериментируем теперь с параметрами. Будем варьировать k при одинаковых t и deltaT (t=10, deltaT=0.2).



Заметим, что границы больших областей не смещаются при увеличении k. Но более мелкие области постепенно начинают сливаться. При достаточно большом k фактически получим гауссово размытие, так как условие на границу не пройдет ни одна точка.

Посмотрим как поведет себя алгоритм для разных шагов по времени (t=10,k=5).



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



Как мы видим, при небольших отклонениях некоторое размытие еще происходит, но начинают появляться артефакты. При deltaT=10 они уже заполняют все изображение.

Заключение


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

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

Повторно приведу ссылки на исходные коды: модуль для работы с PGM, реализация фильтра.

Источники


  1. Оригинальная статья Перона и Малика. Scale-Space and Edge Detection Using Anisotropic Diffusion
  2. Численный расчет уравнения анизотропной диффузии
  3. Стандарт формата PGM
  4. Учебник по современному фортрану
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331618/


[Перевод] Сжатие фотографий без видимой потери качества: опыт Yelp

Воскресенье, 25 Июня 2017 г. 19:30 + в цитатник
На Yelp хранится более 100 миллионов пользовательских фотографий, от картинок ужинов и причёсок до одной из наших последних фич, #yelfies. Эти изображения составляют основную часть трафика для пользователей приложения и веб-сайта, а их хранение и передача обходятся недёшево. Стараясь предоставить людям наилучший сервис, мы усиленно работали над оптимизацией всех фотографий и добились среднего уменьшения размера на 30%. Это экономит людям время и трафик, а также сокращает наши расходы на обслуживание этих изображений. Ах да, и мы сделали это без ухудшения качества фотографий!

Исходные данные


Yelp хранит пользовательские фотографии уже 12 лет. Мы сохраняем lossless-форматы (PNG, GIF) как PNG, а все остальные форматы в JPEG. Для сохранения файлов используются Python и Pillow, а загрузки фотографий начинаются примерно с такого сниппета:

# do a typical thumbnail, preserving aspect ratio
new_photo = photo.copy()
new_photo.thumbnail(
    (width, height),
    resample=PIL.Image.ANTIALIAS,
)
thumbfile = cStringIO.StringIO()
save_args = {'format': format}
if format == 'JPEG':
    save_args['quality'] = 85
new_photo.save(thumbfile, **save_args)
После этого мы начинаем искать варианты для оптимизации размера файла без потери качества.

Оптимизации


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

  1. Изменения в Pillow
    • Флаг Optimize
    • Progressive JPEG
  2. Изменения в логике фотоприложения
    • Распознавание больших PNG
    • Динамическое качество JPEG
  3. Изменения в энкодере JPEG
    • Mozjpeg (треллис-квантование, кастомная матрица квантования)

Изменения в Pillow


Флаг Optimize


Это одно из самых простых изменений, которые мы сделали: передать Pillow ответственность за дополнительную экономию размера файла за счёт времени CPU (optimize=True). По определению, это никак не отразится на качестве фотографий.

Для JPEG этот флаг означает указание энкодеру найти оптимальный код Хаффмана, сделав дополнительный проход при сканировании каждого изображения. Каждый первый проход, вместо записи в файл, вычисляет статистику вхождений по каждому значению, эта информация нужна для идеального кодирования. В стандарте PNG используется zlib, так что флаг оптимизации в данном случае указывает энкодеру использовать gzip -9 вместо gzip -6.

Такое изменение было просто сделать, но выяснилось, что оно не является идеальным решением, сокращая размер файлов всего на несколько процентов.

Progressive JPEG


При сохранении JPEG можно выбрать несколько различных типов:

  • Baseline JPEG, которые загружаются сверху вниз
  • Progressive JPEG, которые загружаются от размытых к чётким. Опцию прогрессирующих изображений легко активировать в Pillow (progressive=True). В результате, качество субъективно повышается (так и есть, легче заметить частичное отсутствие изображения, чем его неидеальную резкость)

Вдобавок, метод упаковки прогрессирующих изображений таков, что обычно это приводит к меньшему размеру файла. Как более полно объясняется в статье Википедии, в формате JPEG применяется зигзагообразная проходка по блоку 8x8 пикселей для энтропийного кодирования. Когда значения этих блоков пикселей не упакованы и расположены по порядку, то обычно сначала идут ненулевые значения, а затем последовательности нулей, и такой паттерн повторяется и чередуется для каждого блока 8x8 на изображении. С прогрессивным кодированием изменяется порядок обработки пиксельных блоков. Первыми в файле идут большие значения для каждого блока (что даёт первым сканам прогрессирующего изображения такую характерную блочность), а ближе к концу хранятся длинные диапазоны малых значений, включая больше нулей, эти диапазоны обеспечивают тонкую детализацию. Такое перераспределение данных в файле не меняет само изображение, но увеличивает количество нулей в ряду друг за другом (которые легче сжать).

Сравнение Baseline JPEG и Progressive JPEG

Пример, как работает рендеринг Baseline JPEG


Пример, как работает рендеринг Progressive JPEG

Изменения в логике фотоприложения


Распознавание больших PNG


Yelp работает с двумя форматами для пользовательского контента — JPEG и PNG. JPEG отлично подходит для фотографий, но обычно не справляется с высококонтрастным дизайнерским контентом (таким как логотипы). В отличие от него, PNG сжимает изображение абсолютно без потерь, отлично подходит для графики, но слишком громоздок для фотографий, где маленькие искажения всё равно не заметны. В тех случаях, когда пользователи загружают фотографии в формате PNG, мы можем сэкономить много места, если распознаем такие файлы и сохраним их в JPEG. Один из основных источников фотографий PNG на Yelp — это скриншоты с мобильных устройств и приложений, которые изменяют фотографии, накладывая эффекты и добавляя рамки.


Слева: типичный скомбинированный PNG с логотипом и рамкой. Справа: типичный PNG, полученный со скриншота

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

Проведя проверку на экспериментальной выборке из 2500 изображений, мы выяснили, что сочетание размера файла и количества уникальных пикселей позволяет довольно точно определить фотографии. Мы генерируем уменьшенную копию на максимальном разрешении и проверяем, если размер файла больше 300 КиБ. Если так, то проверяем пиксели изображения, есть ли там больше 216 уникальных цветов (Yelp конвертирует загруженные изображения RGBA в RGB, но если бы мы этого не делали, то всё равно проверяли бы это).

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

Динамическое качество JPEG


Первый и самый известный способ уменьшить размер файлов JPEG — настройка под названием quality. Многие приложения, способные сохранять в формате JPEG, определяют quality в виде числа.

Качество — это некая абстракция. На самом деле, существуют отдельные уровни качества для каждого из цветовых каналов изображения JPEG. Уровни качества от 0 до 100 соответствуют различным таблицам квантования для цветовых каналов и определяют, сколько данных будет потеряно (обычно в высоких частотах). Квантование сигнала — это один из шагов в процессе кодирования JPEG, когда теряется информация.

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

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

  • Снизу вверх (Bottom-up): Эти алгоритмы генерируют настроенные таблицы квантования, обрабатывая изображение на уровне блоков 8x8 пикселей. Они одновременно рассчитывают, сколько теоретического качества было потеряно и как эти потерянные данные усиливают или сокращают видимость искажений для человеческого глаза.
  • Сверху вниз (Top-down): Эти алгоритмы сравнивают целое изображение с его оригинальной версией и определяют, сколько информации было потеряно. Последовательно генерируя кандидатов с различными настройками качества, мы можем выбрать того, который соответствует минимальному уровню оценки, смотря какой алгоритм оценки мы используем.

Мы оценили работу алгоритма bottom-up и пришли к выводу, что он не обеспечивает должных результатов на высших настройках качества, которые мы хотели использовать (хотя кажется, что у него есть потенциал в среднем диапазоне качества, где энкодер может быть более смелым относительно выбора отбрасываемых байтов). Многие научные работы по этой стратегии были опубликованы в начале 90-х, когда вычислительные ресурсы были в дефиците, так что было сложно использовать ресурсоёмкие методы, которые использует вариант Б, такие как оценка взаимосвязей между блоками.

Так что мы обратились ко второму подходу: использование делённого пополам алгоритма для генерации изображений-кандидатов на разных уровнях качества и оценка падения качества каждого изображения путём вычисления его индекса структурного сходства (SSIM) с помощью pyssim до тех пор, пока это значение находится в пределах настраиваемого, но статичного порога. Это позволило нам выборочно понизить средний размер файла (и среднее качество) только для изображений, которые были выше воспринимаемого порога.

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

  1. Оригинальные изображения, созданные с помощью текущего метода при quality = 85, показаны синим цветом.
  2. Альтернативный подход для снижения размера файлов, со снижением настройки качества до quality = 80, показан красным цветом.
  3. И наконец подход, на котором мы в итоге остановились, динамическое качество SSIM 80-85, показан оранжевым цветом. Здесь качество выбирается из диапазона от 80 до 85 (включительно), в зависимости от совпадения или превышения соотношения SSIM: предварительно вычисляемого статической величины, которая совершает этот переход где-то посредине диапазона изображений. Это позволяет нам снизить средний размер файла без понижения качества плохо выглядящих изображений.



Индексы SSIM для 2500 изображений с тремя разными стратегиями изменения настроек качества

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

  1. Чувствителен к ошибке квантования JPEG
  2. Быстрый, простой алгоритм
  3. Может быть рассчитан на нативных объектах PIL без конвертации изображений в PNG и передачи их в приложения CLI (см. #2)

Пример кода для динамического качества:

import cStringIO
import PIL.Image
from ssim import compute_ssim
 
 
def get_ssim_at_quality(photo, quality):
    """Return the ssim for this JPEG image saved at the specified quality"""
    ssim_photo = cStringIO.StringIO()
    # optimize is omitted here as it doesn't affect
    # quality but requires additional memory and cpu
    photo.save(ssim_photo, format="JPEG", quality=quality, progressive=True)
    ssim_photo.seek(0)
    ssim_score = compute_ssim(photo, PIL.Image.open(ssim_photo))
    return ssim_score
 
 
def _ssim_iteration_count(lo, hi):
    """Return the depth of the binary search tree for this range"""
    if lo >= hi:
        return 0
    else:
        return int(log(hi - lo, 2)) + 1
 
 
def jpeg_dynamic_quality(original_photo):
    """Return an integer representing the quality that this JPEG image should be
    saved at to attain the quality threshold specified for this photo class.
 
    Args:
        original_photo - a prepared PIL JPEG image (only JPEG is supported)
    """
    ssim_goal = 0.95
    hi = 85
    lo = 80
 
    # working on a smaller size image doesn't give worse results but is faster
    # changing this value requires updating the calculated thresholds
    photo = original_photo.resize((400, 400))
 
    if not _should_use_dynamic_quality():
        default_ssim = get_ssim_at_quality(photo, hi)
        return hi, default_ssim
 
    # 95 is the highest useful value for JPEG. Higher values cause different behavior
    # Used to establish the image's intrinsic ssim without encoder artifacts
    normalized_ssim = get_ssim_at_quality(photo, 95)
    selected_quality = selected_ssim = None
 
    # loop bisection. ssim function increases monotonically so this will converge
    for i in xrange(_ssim_iteration_count(lo, hi)):
        curr_quality = (lo + hi) // 2
        curr_ssim = get_ssim_at_quality(photo, curr_quality)
        ssim_ratio = curr_ssim / normalized_ssim
 
        if ssim_ratio >= ssim_goal:
            # continue to check whether a lower quality level also exceeds the goal
            selected_quality = curr_quality
            selected_ssim = curr_ssim
            hi = curr_quality
        else:
            lo = curr_quality
 
    if selected_quality:
        return selected_quality, selected_ssim
    else:
        default_ssim = get_ssim_at_quality(photo, hi)
        return hi, default_ssim

Есть несколько других статей в блогах об этой технике, здесь одна от Кольта Маканлиса. И когда мы собирались публиковаться, Etsy тоже опубликовала свою! Дай пять, быстрый интернет!

Изменения в энкодере JPEG


Mozjpeg


Mozjpeg — это open-source форк libjpeg-turbo, который пожертвовал временем выполнения ради размера файлов. Такой подход хорошо совместим с офлайновыи конвейером по регенерации файлов. С потреблением ресурсов в 3-5 раз больше, чем libjpeg-turbo, этот алгоритм делает изображения меньше по размеру!

Одно из отличий mozjpeg в том, что он использует альтернативную таблицу квантования. Как упоминалось выше, качество — это абстракция таблиц квантования для каждого цветового канала. Всё указывает на то, что дефолтные таблицы квантования JPEG довольно легко превзойти. Как говорится в спецификациях JPEG:

Эти таблицы приводятся только как примеры и необязательно подходят для какого-то конкретного приложения.

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

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

Mozjpeg + Pillow


В большинстве дистрибутивов Linux по умолчанию установлен libjpeg. Так что mozjpeg под Pillow не работает по умолчанию, но это не слишком сложно настроить в конфигурации. При сборке mozjpeg используйте флаг --with-jpeg8 и убедитесь, что он может быть залинкован с Pillow. Если вы используете Docker, то можно сделать такой Dockerfile:

FROM ubuntu:xenial
 
RUN apt-get update \
	&& DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install \
	# build tools
	nasm \
	build-essential \
	autoconf \
	automake \
	libtool \
	pkg-config \
	# python tools
	python \
	python-dev \
	python-pip \
	python-setuptools \
	# cleanup
	&& apt-get clean \
	&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
 
# Download and compile mozjpeg
ADD https://github.com/mozilla/mozjpeg/archive/v3.2-pre.tar.gz /mozjpeg-src/v3.2-pre.tar.gz
RUN tar -xzf /mozjpeg-src/v3.2-pre.tar.gz -C /mozjpeg-src/
WORKDIR /mozjpeg-src/mozjpeg-3.2-pre
RUN autoreconf -fiv \
	&& ./configure --with-jpeg8 \
	&& make install prefix=/usr libdir=/usr/lib64
RUN echo "/usr/lib64\n" > /etc/ld.so.conf.d/mozjpeg.conf
RUN ldconfig
 
# Build Pillow
RUN pip install virtualenv \
	&& virtualenv /virtualenv_run \
	&& /virtualenv_run/bin/pip install --upgrade pip \
	&& /virtualenv_run/bin/pip install --no-binary=:all: Pillow==4.0.0

Это всё! Собирайте и сможете использовать Pillow с mozjpeg в нормальном процессе обработки изображений.

Эффект


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

  1. Изменения в настройках Pillow дали экономию 4,5%
  2. Определение больших PNG дало экономию 6,2%
  3. Динамическое качество дало экономию 4,5%
  4. Переход на энкодер mozjpeg дал экономию 13,8%

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


Изменение среднего размера файла со временем, у CDN (вместе с другими файлами, которые не являются изображениями)

Чего мы не делали


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

Субдискретизация


Субдискретизация — основной фактор в определении и качества, и размера файлов веб-изображений. В интернете можно найти более подробное описание субдискретизации, но для этой статьи достаточно сказать, что мы уже выполняем субдискретизацию до 4:1:1 (это настройки по умолчанию Pillow, если не указать другие настройки), так что мы вряд ли получим какой-то выигрыш при дальнейшей оптимизации.

Кодирование PNG с потерями


Зная то, что мы делаем с PNG, вариант с сохранением этих изображений в прежнем формате, но используя энкодер с потерями вроде pngmini, имеет смысл, но мы всё равно выбрали вариант сжатия в JPEG. Тем не менее, автор энкодера говорит о сжатии файлов на 72-85%, так что это альтернативный вариант с обоснованными результатами.

Более современные форматы


Поддержка более современных форматов вроде WebP или JPEG2k определённо рассматривалась нами. Но даже если бы мы реализовали этот гипотетический проект, всё равно остался бы длинный хвост пользователей, которым нужны изображения JPEG/PNG, так что усилия по их оптимизации в любом случае были не напрасными.

SVG


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

Магия вендора


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

Дополнительная литература


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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331588/


Метки:  

Скрипт статического коллтрекинга

Воскресенье, 25 Июня 2017 г. 19:01 + в цитатник


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


Для чего нужен скрипт


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


Скрипт также запоминает откуда пришел посетитель. Если в первый раз он пришел из Яндекс, а затем, повторно из закладок, ему показывается номер телефона, связанный с Яндекс.


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


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


Как установить и настроить скрипт


Скачайте скрипт и подключите на сайте.



Добавьте CSS класс ct_phone в элементах, где будет происходить подмена номеров. Если номер есть в шапке и в подвале сайта, то для всех номеров нужно добавить класс.


+7 888 888-88-88

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



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


Без особой причины не рекомендую выполнять код в событии готовности DOM модели или в Google Tag Manager, потому что подмена номеров становится заметнее.


Настройка с помощью конструктора


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


В первой секции нужно создать источники трафика и условия как их определить. Ниже, вы задаете соответсвие источников трафика и номеров телефона. Готовый скрипт внизу страницы.



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




Когда все настроили готовый код можно скопировать внизу страницы.



Настройка источников трафика


В массиве sources задаются источники трафика.


'articles':{'ref': /(habrahabr|oborot.ru)/ig}

В этом примере, articles это название источника. Это название используется в массиве phones. Ключ ref означает искать в адресе источника, а значение — регулярное выражение, с помощью которого выполянть поиск.


Поддерживаются следующие места поиска:


  • ref — адрес, с которого посетитель перешел на ваш сайт.
  • dst — адрес страницы, на которую пришел посетитель.
  • utm — только параметр начинающийся с utm.

В этом примере, в utm_medium будет искаться подстрока email.


'email': {'utm_medium': 'email'}

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


'email': {'utm_medium': /(email|eml)/ig }

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


 'ydirect_openstat': {'dst': function(subject){
                                      var o = query.getRawParam(subject, '_openstat');
                                      return (o && a2b(o).indexOf('direct.yandex.ru')>-1);
                             }
                      }

В этой функции распаковывается параметр _openstat из адреса страницы и проверяется, что он от Яндекс Директ.


Соответствие источников трафика и номеров телефона


После настройки источников трафика, нужно задать соответсвие номеров и источников.


phones: [
       {'src':'yadirect', 'phone':['+74951111114']},
       {'src':'articles', 'phone':['+74952222224']}
],

Если на сайте одновременно отображаются несколько номеров, их нужно перечислить в массиве phone.


targets: ['.phone1', '.phone2'],
phones: [
       {'src':'yadirect', 'phone':['+74951111114', '+78121111114']},
       {'src':'articles', 'phone':['+74952222224', '+78122222224']}
],

Параметр targets задает селекторы для элементов, в которых заменять номера. Количество в targets должно соответсвовать количеству телефонов в параметре phone


HTML будет выглядеть так:


+7 495 888-88-88
+7 812 888-88-88 

По умолчанию в targets задан один селектор — .ct_phone


Отображение номеров


Номера отображаются в соответствтии с параметром pattern. По умолчанию он равен


+# (###) ###-##-##  

Вместо символов # будут подставлены цифры из номера.


Шаблон нужно задавать под конкретные номера. Шаблон по умолчанию позволяет отображать 11-ти значные российские номера, но не сможет правильно показать белорусский номер, потому что в нем 12 цифр.


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


+# (###) ###-##-## 

Если параметр pattern сделать пустым, номер будет выводится как он записан в секции phones.


Если шаблона недостаточно, отображение номера можно сделать при помощи callback функции. В параметр функции подается массив из списка phones.


function substCallback(p){
     if(p){
         document.getElementById('top_phone').innerText=someComplexModification(p.phone[0]);
     } 
}

sipuniCalltracking({
    callback: substCallback, 
    sources: {
       ...        
    },
    phones: [
       {'src':'yadirect', 'phone':['+74951111114']},
       {'src':'articles', 'phone':['+74952222224']}
    ],      
}, window);

Подмена заголовков страницы для разных источников трафика


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


Подмена заголовков для разных ключевых слов


Простой вариант подмены, когда вместо номеров телефона указывается заголовок.


sipuniCalltracking({
  pattern: '',  
  sources: {
       'yadirect_ustanovka':{'utm_keyword': 'установка'},
       'yadirect_rassrochka':{'utm_keyword': 'рассрочку'}
    },
    phones: [
       {'src':'yadirect_ustanovka', 'phone':['Газовые котлы с установкой']},
       {'src':'yadirect_rassrochka', 'phone':['Газовые котлы в рассрочку']}
    ]
}, window);

Теперь нужно добавить CSS класс в тэге заголовка.


Газовые котлы


Если посетитель пришел по объявлению о газовых котлах с установкой, ему покажется заголовок “Газовые котлы с установкой”. Поскольку скрипт запоминает, откуда пришел посетитель в первый раз, то при повторном заходе на сайт он все равно увидит “Газовые котлы с установкой”.


В параметр pattern подаем пустую строку, чтобы шаблон не применялся к строкам при выводе.


Можно менять заголовок и подзаголовок, и сделать CSS классы понятнее при помощи настройки targets.


pattern:'',
targets: ['.title', '.subtitle'],    
phones: [
       {'src':'yadirect_ustanovka', 'phone':[
               'Газовые котлы с установкой', 
               'Доставим и установим газовый котел в вашем доме']},
       {'src':'yadirect_rassrochka', 'phone':[
               'Газовые котлы в рассрочку', 
               'Безпроцентная рассрочка до 6 месяцев']}
    ],

Размечаем CSS классами title и subtitle элементы, в которых будем менять содержимое.


Газовые котлы

Доставка и установка газовых котлов


Подмена содержимого при помощи callback функции.


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


function substCallback(p){
     if(p){
        document.getElementById('header_img').src=p.phone[1];
     }else{
        document.getElementById('header_img').src = '/default.png';
     } 
}

sipuniCalltracking({
  pattern:'',
  targets:['.title'],
  callback: substCallback,
  sources:{
      ...
  },
  phones:[
     {src:'yadirect_dostavka', phone:['Котлы с доставкой', '/kotel_dostavka.png']},
     {src:'yadirect_ras',  phone:['Котлы в рассрочку', '/kotel_ras.png']}
  ]
}, window);

Проверка и отладка кода подмены


Проверить работу скрипта можно в jsFiddle. Для того чтобы подключить файл скрипта используйте эту ссылку. Напрямую jsFiddle не позволяет подключить файлы из GitHub поэтому ссылка преобразована при помощи сервиса rawgit.com


Страница приземления и реферрер


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


var wnd = {
   document:{referrer:'http://yandex.ru'},
   location:{href:'http://mysite.com/?utm_keyword=dostavka'}
};

sipuniCalltracking({
 …
}, wnd);

Отключение запоминания источника посетителя


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


cookie_ttl_days: 0,
cookie_key: 'test1',

Пример


Пример jsFiddle для проверки работы скрипта: https://jsfiddle.net/xndmnydj/1/


Не забыть


После отладки не забудьте убрать cookie_ttl_days и cookie_key и заменить параметр wnd на window.


Заключение


У статического коллтрекинга есть ряд преимуществ по сравнению с динамическим: не возникает нехватки номеров при увеличении трафика, не возникает коллизий, когда номер показан другому человеку, а по нему звонит предыдущий посетитель.


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


Скрипт мы создавали для наших клиентов, но поскольку он работает с номерами любых операторов, решили, сделать его открытым, лицензия — MIT.


В планах добавить установку через менеджер пакетов, например Browserify или Webpack.


Репозиторий GitHub

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331540/


Метки:  

Еще один пример синтеза асинхронных схем: VME bus controller

Воскресенье, 25 Июня 2017 г. 17:25 + в цитатник
Полистал недавно книжку (не всю, только то что позволили):
image
Нашел пример, как делать схемы с помощью Petrify. Исходное задание выглядит так:
image
В результате получилась вот такая SI схема:
image
Да, от Маллера ушли не далеко, ну только что C-элементы подвесили. Думаю, все-таки надо показать, как надо делать асинхронные схемы. Для начала лучше ознакомиться с этим.

А здесь разберу приведенный пример.
Для начала нарисую исходное поведение поудобнее и заменю длинные имена сигналов:
dsr — a
dsw — b
ldtack — c
lds — x
d — y
dtack — z
image
Чтобы синтезировать SI схему на двухвходовых элементах, надо дополнить исходное поведение новыми сигналами так, чтобы каждый невходной сигнал (f) можно было бы вписать в один из следующих шаблонов (или в их комбинацию):
image
Чтобы понять, как пользоваться шаблонами применительно к поведению с выбором, рассмотрим поведение, состоящее из двух альтернативных ветвей. А потом рассмотрим поведение из тех же двух ветвей, но расположенных последовательно друг за другом.
image
Нетрудно проверить, что логические функции для обоих этих поведений одни и те же. Это и понятно, второе поведение это то же что и первое, только с зафиксированной дисциплиной переключений входных сигналов. Соответственно, чтобы использовать шаблон применительно к поведению с выбором, нужно последовательно хотя бы один раз пройти по всем ветвям в произвольном порядке (но только таком, который допускает исходное задание). И еще одно замечание: дополнительные сигналы будут вставляться без фиксации знака. Знаки будут расставлены в самом конце.
Итак, посмотрим на исходное поведение. Проблема, которую нужно решить в первую очередь: выбор происходит одновременно с параллельной ветвью x- c-. Необходимо синхронизировать эту ветвь. Имея в виду дальнейшее дробление поведения, лучше синхронизацию сделать отдельно для каждой альтернативной ветви. а заодно и изолировать сигнал c.
image
Добавленный сигнал f вписывается в шаблон (т.е. имеет двухвходовую реализацию) с помощью уже имеющихся сигналов (f= ac). Сигнал g можно было бы вписать в шаблон с помощью сигналов y и c, но лучше добавить сигнал h, который позже можно будет использовать для изоляции сигнала y. Сигналы f, g объявляем псевдовходными, т.е. запрещаем перед ними ставить новые сигналы (чтобы не испортить двухвходовую реализацию).
Исследуемое поведение четко разделяется на две альтернативные ветви. Поэтому его можно разделить на два независимых поведения, каждое из которых будет представлять из себя одну из альтернативных ветвей. Для этого нужно изолировать сигналы, встречающиеся в обеих ветвях. Изолировать сигнал x — значит получить двухвходовую реализацию (вписать в шаблон) для этого сигнала x и для всех сигналов-следствий (сигналов, переключения которых являются следствием переключения сигнала x). Таким образом мы устраним влияние сигнала x на схему и далее его можем не рассматривать.
Общими для обеих ветвей являются сигналы — c, x, y, z. Сигнал c — входной, реализации не требует. Его следствия f, g уже реализованы. Следствия сигналов x и z — входные сигналы, для них реализация не требуется. Для следствий сигнала y используем h и новый сигнал i (h=yb, i=yb). Сигналы h и i теперь псевдовходные. Для реализации сигнала y вставим новые j и k (y=jk). Сигнал z реализуем с помощью уже имеющегося сигнала i и нового сигнала m (z=im). Для реализации сигнала x добавим новые сигналы n и p (x=np). Сигналы y, z, x — теперь псевдовходные.
image
Удалим из графа изолированные сигналы. Действия такие же, как при добавлении сигнала, только последовательность обратная. Основание для удаления сигналов следующее: чтобы синтезировать схему для поведения с изолированными сигналами, достаточно построить схему для того же поведения, но с удаленными изолированными сигналами.
image
Сигналы a, b — входные, f, g, h, i — псевдовходные.
Чтобы синтезировать схему для такого поведения (с уникальными сигналами в каждой ветви), достаточно синтезировать две схемы для каждой альтернативной ветви по-отдельности. То что ветвь n параллельна точке выбора, не является препятствием, т.к. эта ветвь синхронизирована псевдовходным сигналом g.
Рассмотрим отдельно верхнюю ветвь.
image
Сигналы b, g, h — входные или псевдовходные.
Рассмотрим готовность к декомпозиции. Нарушений CSC нет. Сигнал m уже имеет двухвходовую реализацию (m=ph). Для сигнала j в качестве дуального можно использовать сигнал g. Для сигнала p необходимо добавить дуальный сигнал q.
image
Теперь найдем логические функции. Пока знаки не расставлены — вид функции условный.
m=qh
p=bq
q=gp
j=g+bp или j=g+bq
Проведем декомпозицию j (тем самым введем новый сигнал r).
j=rg
r=bp или r=bq
image
Запомним, что r может быть следствием p (вместо — q). Это может пригодиться при расстановке знаков.
Теперь рассмотрим отдельно нижнюю ветвь.
image
Сигналы a, f, i — входные или псевдовходные.
Нарушения CSC есть. Это последовательности: n f n f и k i a- k i a+. Устраняется единственно возможным способом (сигнал s).
image
Сигналы n, k уже имеют двухвходовую реализацию (n=si, k=fs). Для сигнала s необходим дуальный сигнал t.
image
Теперь логические функции выглядят так:
n=ti
r=fs
s=ft
t=as
Для обеих альтернативных ветвей все сигналы приведены к реализуемости в двухвходовом базисе. Теперь восстановим поведение в полном объеме, со всеми добавленными сигналами.
image
И наконец, расставим знаки. Возникшие рассогласования входов для сигналов i, f, q, p, n исправляются добавлением инверторов v, w, u, d, e соответственно.
image
А вот логические функции для всех сигналов.
$x=NAND(n,p) $
$y=NAND(k,j) $
$z=NAND(i,m) $
$f=NOR(c,w) $
$g=NOR(c,h) $
$h=NAND(y,b) $
$i=NAND(y,v) $
$j=NOR(r,g) $
$k=OR(s,f) $
$m=NAND(q,h) $
$n=AND(e,i) $
$p=NAND(d,q) $
$q=NAND(u,p) $
$r=NOR(q,v) $
$s=NOR(f,t) $
$t=NOR(w,s) $
$v=NOT(b) $
$w=NOT(a) $
$u=NOT(g) $
$d=NOT(v) $
$e=NOT(t) $
Схема выглядит так:
image
Посчитал транзисторные пары. Оглушительного превосходства не получилось. 78 на 94, если не ошибся. Ну, да ладно. Зря старался, что ли. Публикую.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331614/


Метки:  

Ограничение количества выполнений метода в секунду

Воскресенье, 25 Июня 2017 г. 17:06 + в цитатник
Задача: разработать возможность запускать на выполнение заданное количество операций в секунду.

Требования:
  • Решение должно отрабатывать как можно быстрее (иначе в нем теряется смысл)
  • Решение должно быть потокобезопасным


В результате у меня получилась функция (естественно в составе отдельного класса), которая возвращает true либо false (разрешение для выполнения).



Общий принцип таков. Есть коллекция отпечатков DateTime. При запросе разрешения проверяется, не превышает ли количество запросов за последнюю секунду максимальное указанное значение. Если превышает, возвращается ложь, иначе в коллекцию добавляется слепок текущей даты и возвращается истина.

Изначально я делал все на основе листов (List<>), но листы имеют динамический набор данных, а значит они расширяются по надобности и при добавлении новых элементов. А надо еще и удалять. Поэтому я решил использовать коллекцию заданного размера. Поэтому от листа решил отказаться в пользу массива. В результате у меня получился функционал круговой коллекции, основанной на массиве, поэтому в итоге я сделал круговую коллекцию.
Получился пул из отпечатков даты заданного размера на основе круговой коллекции.
Класс пула содержит текущий элемент, в котором есть отпечаток даты и ссылка на следующий элемент. Изначально все даты имеют значение по умолчанию.

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

Теперь о потоках. Я не придумал ничего лучше, как поставить lock на функцию разрешения.

Вот и все.

Для тестирования я создал метод, который выполняет заданное количество запросов с заданным количеством потоков. Запустил его со следующими параметрами. Количество операций в секунду — 6, количество потоков — 4, количество запросов — 50.

Получились вот такие результаты


Вот ссылка на гитхаб
github.com/iRumba/RpsBarrier

По скрину видно, что код отработал верно.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331612/


Метки:  

Поиск сообщений в rss_rss_hh_new
Страницы: 1437 ... 1022 1021 [1020] 1019 1018 ..
.. 1 Календарь