Перейти к основному содержимому

Hacking essentials

· 2 мин. чтения

Прошёл двухдневный workshop компании Clarified Security по основам взлома в сети и серверы. Оплатил это Pipedrive, но вы можете заказать себе такую же тренировку. Тренинги построены по шагам с такой оценочной системой, что какое-то время можно самому искать решение, потом тебе объясняют как это сделать в действительности.

Спешу поделиться самым интересным. Вначале было введение в историю, в т.ч. последние уязвимости типа KRACK

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

Второй день мы провели в Kali Linux за двумя главными программками - Meterpreter и Armitage. Первая это библиотека эксплоитов с их поддержкой в виде сервера, а вторая это UI для управления атакой.

Итог пугающий. Уязвимости в java, flash и прочих неизвестных 0-day штуках позволяет влезть через веб к человеку на комп. Дальше обычно приходится делать повышение привилегий процесса. В винде можно прыгать с процесса на процесс, так что если клиент заразился через открытый word файл, то связь со злоумышленником остаётся переходом под более постоянный процесс, типа explorer. Повышение привилегий даёт в итоге полный контроль над машиной. Дальше можно захватывать компьютеры во внутренней сети, менять настройки файрволла так что-бы ничего не заподозрил владелец. Можно взламывать и линуксовые машины, так же поднимаясь до рута.

Кроме этих двух утилит в Kali предустановлены wordlistы, с которыми можно перебирать пароли к линуксовым shadow-файлам, есть сканеры путей для вебсайта и проч.

Code review и конфликт в динамике команды

· 9 мин. чтения

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

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

Умный дом — отопление с пеллетным котлом

· 9 мин. чтения

Год назад мы взяли в кредит дом. Старенький, с деревянными продуваемыми окнами, без утепления и с основным дровяным отоплением. Коллеги брали наоборот новые, рядные дома где всё уже готово. Я расчитывал что так я выиграю в цене, гибкости, а в итоге и в качестве. 

Заказав 16 м³ дров за ~700€ и протопив сезон, я понял что это вам не квартирные удобства. Хотя дрова самые дешёвые и экологические в отоплении, с ними неудобно

  • Надо дровник и тратить пару дней на их переноску и выкладывание

  • Дрова бывают разные и по-разному разгораются (берёза лучше осины)

  • Зимой надо минимум 2-3 вязанки дров за вечер что-бы натопить. Таскать их в мороз и складировать у плиты — то ещё удовольствие.

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

  • Температура  неравномерна и не постоянна

Когда топишь то возле плиты воздух может быть 30˚C уже, тогда как где-то в дальнем углу ещё 18˚C. 

Приходишь домой — дома 15˚C, надо ждать 2 часа покуда прогреется часть этажа. Встаёшь утром - тоже холодно, душ принимать становится просто опасным для здоровья — легко простудиться.

Кроме того, есть теплонасос воздух-воздух, но его мощности хватало только на четверть дома, он достаточно шумный и при морозах его мощность сильно падала. Поэтому приходилось дополнительно топить электрическими конвекторами. Но электричество достаточно дорогое — примерно +50€/мес при отоплении только спальни. Отапливать весь дом электрическими обогревателями влетело бы в ~300 €/мес.

Итого, Требования

  • ​Автономность (1-2 недель беспрерывной работы, что-бы можно было съездить в отпуск зимой)

  • Равномерность отопления дома

  • Достаточная мощность при морозах в -30˚C (объём дома ~ 350м³ без внешнего утепления)

Варианты

Уже исходя из требований, выходит что минимальная мощность источника отопления при зимних тепло-потерях — 20 квт

Я рассматривал разные источники тепла: 

  • Газ хоть и дешёвый, но проводить трассу от Eesti energia, или создавать поземное хранилище вышло бы слишком дорого. Баллоны таскать было бы слишком напряжно, да и с точки зрения безопасности казалось опасным
  • Дизелем топит сосед. Достаточно дорого, на зиму запас проблематично делать, нужен большой бак. Удобно если бы у меня была машина на дизеле
  • Воздух-вода очень заманчивый вариант экономичностью и автономностью, но как и воздух-воздух, эти насосы теряют мощность при низких температурах. Грубо говоря при -30 градусах, насос который рекламируется как 15 квт, и стоит 8 к€ станет работать как 3 квт, периодически подключая ТЭН
  • Вода-вода, или как у нас рекламируют maaküte ("отопление земли"). Закапывать трубы на участке вертикально (15м) или горизонтально (пару км) мне кажется сомнительным — будет промораживать землю, да и подходящего места нет.
  • Камин. Если продолжать топить дровами то вместо старой узкой печки можно использовать камин. Но для этого надо установить металлическую сердцевину и провести в разные комнаты отводы тёплого воздуха — это улучшит равномерность прогревания, но не автономность

Для равномерного прогревания дома, система тёплых полов самая идеальная. Но для этого трубки должны быть встроены в бетон, а у меня увы, перекрытия деревянные. 

Победитель RTB30

Я получил 3 предложения от 5 фирм, в итоге одно из них выбрал и чуть-чуть поменял.

Выбрал я в итоге пеллетный котёл от датской NBE, RTB30 (30квт). Степень сгорания более 91%, что значит что для смены бака надо сжечь порядка тонны пеллетов (т.е. менять золу надо будет раз в пару месяцев)

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

Интерфейс

На самом котле есть dashboard где показано подключение и состояние в целом. Внутри - меню offline управление.

Котёл подключается по wi-fi к интернету к порталу stokkercloud.dk, где можно посмотреть как публичную информацию разных котлов в мире, так и залогиниться в админку своего котла (об этом ниже). Админка кстати написана на ангуляре, а бэкэнд на php

Теперь что можно делать в интернете..

Радиаторы и трубы

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

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

Сеть из труб достаточно интересная и полна нюансов. 

  1. сразу на выходе из котла стоит труба соединяющая горячую воду сразу со входом. Эта обратка разрывается как только котёл достаточно нагреется.
  2. у котла внизу есть предохранительный клапан, который откроется самостоятельно если в трубах будет критическое давление воды
  3. вода в бойлере и вода в радиаторной сети никак не связаны и нагрев происходит за счёт теплоотдачи, т.е. вода в радиаторах никуда не девается и циркулирует по кругу
  4. поскольку горячая вода находится в подвале, то при открытии крана, ей прийдётся подняться на высоту 3м по трубам длиной ~20 м, под давлением насосной мембраны в 2.5 атмосфер. Это достаточно долго (30 секунд) и хочется мгновенной горячей воды. Поэтому горячая вода замкнута в цикл и гоняется она постоянно циркуляционным насосом.

Что-бы понять нюансы советую послушать специалиста...

Недостатки

Из проблем или неудобств, стоит выделить высокую минимальную мощность. Минимально настраиваемая мощность в 10% потребляет достаточно много топлива и быстро нагревает дом до 26˚C для экономии осенью проще поставить вариацию по времени (включение через каждые 2 часа), но частое включение горелки плохо сказывается на её надёжности. В этом плане идеально было бы подключить дополнительно систему воздух-вода для температур 0 — +10˚C. Ближе к зиме я настрою постоянное включение

Факапы установки

Устанавливали всё товарищи из Küttemeister.

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

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

Я было уже начал думать что сам котёл слишком мощный вышел, а трубы и батареи хилые, но..

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

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

Из-за этого в лучшем, полуоткрытом случае вода подавалась только на 50%, показывая при этом правильные температуры. Насосы при этом надрывались на максимальной мощности, но потока было мало и нагретая вода направлялась обратно в котёл, перегревая его.

Топливо

Пеллетные гранулы бывают 6мм и 8мм в длину. Разница лишь в том, что это влияет на объём который забирает hopper из бака (для распаливания 40г например надо).

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

В Эстонии заказать можно машину (2-4 тонны), а можно купить пакетами по 15-25 кг. Расчитывая расход осенью в 10кг в день (как вы видели на скриншотах выше), получается 300кг в месяц (40 евро/мес). В зимний период я думаю расход будет раза в два больше (под 100 евро/мес). Оптовые производители:

  • Pro Wood (165 евро за тонну, в 15 кг пакетах чуть дороже, но в строительных их же продукция в магазинах выходит аж по 230 евро за тонну)
  • Pelletiküte (160-180 евро за тонну) - увы эти товарищи в Таллинне складов не имеют, можно только заказать доставку (4 тонны самое оптимальное)
  • Nordic Pellets (в строительных последние можно найти по 215 евро за тонну, 20 кг пакеты)
  • Briketipoisid

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

Опыт работы

После нескольких месяцев отопления, пришёл к таким заключениям..

  • ​Автоматика работает по приоритетам - сначала нагрев воды (DHW), потом нагрев первого этажа (zone 1), потом уже нагрев второго этажа.
  • Если вы поставите скажем на zone 1 заведомо недостижимую температуру (80 градусов при температуре котла в 65), то весь напор воды будет идти только на первый этаж, а второй будет в холоде
  • Температура котла прыгает волнами, что-бы выровнять её, можно повысить минимальную мощность

Умный дом в перспективе

В идеале умный дом должен в себя включать

  • датчики аварии (протечки воды, пожара, электроотключения)

  • датчики безопасности (открытия дверей, окон, движения на участке)

  • видео-трансляция и запись

  • управление освещением (умный свет внутри дома и освещение участка, гардины/жалюзи)

  • управление поливом газона (в зависимости от прогноза погоды)

  • электрические замки/ключи

Умная техника

  • плита которая оповещает вас если вы забыли её выключить

  • холодиль​ник, в который можно посмотреть удалённо

  • стиралка, которую можно удалённо приостановить

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

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

Микросервисы 2

· 3 мин. чтения

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

Docker

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

Концепции

Images (образы) это изолированная среда, как правило с конкретным языком (node, php) или библиотекой. За это отвечает Dockerfile, который ссылается на существующий, скачиваемый образ на докер хабе.

Что-бы увидеть установленные образы, есть комманда  docker images . 

Контейнеры  это запущенные образы которые изолированы от host-машины. Обычно контейнер поднимается для какого-то сервиса, поэтому у него есть своё название, явно объявлен маппинг сетевых портов на host-машину и способ монтирования файловой системы -  за это отвечает docker-compose.yml

Что-бы увидеть установленные образы, есть комманда  docker images . 

Контейнеры  это запущенные образы которые изолированы от host-машины. Обычно контейнер поднимается для какого-то сервиса, поэтому у него есть своё название, явно объявлен маппинг сетевых портов на host-машину и способ монтирования файловой системы -  за это отвечает docker-compose.yml

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

Что-бы увидеть запущенные контейнеры, есть комманда  docker ps  (а лучше dry)

Что-бы запустить текущую папку есть  docker-compose down && docker-compose up  — это основная комманда которой вы будете пользоваться в разработке если будете делать изменения

Для автоматизации перезагрузки контейнера при изменении файлов на hostе, для node надо копать в сторону pm2-docker

Consul

Консул это сервер, который служит для связывания сервисов воедино (service discovery), поддержания их состояния здоровья (healthcheck) и для их конфигурации без перезапуска (key-value storage)

Каждый поднимающийся сервис сам должен зарегистрировать себя в консуле, указать на каком IP/порте он работает. Сам должен открыть HTTP endpoint который будет выдавать состояние здоровья

Сложности

При создании микросервисов самая сложная часть это граница и управление.  Сервис должен отвечать за конкретную бизнес-функцию и доменную область. Это вертикальный срез приложения.  Но при этом сервис должен выполнять весь стек работы — ui, backend, db storage, queue processor. Впихивать столько ответсвенности в один сервис технологически сложно, поэтому приходится дробить вертикальный стек ещё на несколько горизонтальных слоёв. Образно был article-manager - стал article-frontend, article-server, article-worker, над которыми нависают ещё всякие сервисы мониторинга. Это тяжело связывать вместе и деплоить разом.

Node

Поскольку нода висит постоянно в памяти, в отличие от php, который запускается при запросе, то возникает проблема перезагрузки кода при его изменении. Вот как выглядит Dockerfile с PM2:

FROM node:6-alpine
USER root
ENV PORT=3000
EXPOSE 3000
RUN npm install pm2 -g
WORKDIR /app
CMD ["pm2-docker", "start", "pm2.json", "--watch"]

Теперь в pm2.json описывается при каком случае надо перезапускать докер сервис

{
"apps": [
{
"name": "article-manager",
"script": "index.js",
"env": {
"PORT": 3000,
"CONSUL_IP": "192.168.10.10",
"CONSUL_PORT": 8500,
"SERVICE_NAME": "article-manager"
},
"watch": true,
"ignore_watch": [
"node_modules",
"npm-debug.log",
".idea",
".git",
"test/coverage"
],
"watch_options": {
"followSymlinks": false
}
}
]
}

Redis

Очень быстрая in-memory база для кеширования данных

version: '2'
services:
redis-master:
image: 'redis:2.8'
network_mode: bridge
volumes:
- ./data:/data
ports:
- 6379:6379
command: redis-server --appendonly yes

Alias

Для удобства, советую добавить в ваш ~/.bashrc или ~/.zshrc алиас на быстрое поднятие контейнера:

alias doc='docker-compose down && docker-compose up'

MySQL триггеры на страже скорости и целостности

· 1 мин. чтения

Недавно начал использовать триггеры в БД. Полезная штука.

Первый случай использования - валидация данных. Позволяет на уровне БД сохранять транзитивную целостность, т.е. более глубокую проверку, чем просто существующий foreign key. 

Вот простейший пример. Есть три таблички - site, category, article. При добавлении нового ряда в article, мы хотим проверить что categoryID использует тот же siteID что и article. Забудем сейчас про нормализацию, допустим siteID нужен и там и там. Триггером валидация решается так..

DROP TRIGGER IF EXISTS trigger_article_insert;
CREATE TRIGGER trigger_advert_insert
BEFORE INSERT ON article
FOR EACH ROW
BEGIN
IF NEW.siteID <>
(SELECT category.siteID
FROM category
WHERE category.ID=NEW.categoryID)
THEN
SIGNAL SQLSTATE '46000'
SET MESSAGE_TEXT = 'Cannot insert row: site ID is different for article and category END IF; END;

Аналогично мы можем решать и вопрос обновления данных. 

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

DROP TRIGGER IF EXISTS trigger_article_insert2;
CREATE TRIGGER trigger_article_insert2
AFTER INSERT ON advert
FOR EACH ROW UPDATE category c SET
size = (SELECT count(a.id) FROM article a WHERE a.categoryID=NEW.categoryID AND a.status='published')
WHERE c.id=NEW.campaignID;

Как правильно писать спецификацию

· 11 мин. чтения

Хождение по воде и разработка по спецификации легки, если и обе заморожены
Эдвард В. Берард

С самого начала моей карьеры, спецификация была больной темой. В маленькой веб-студии написание «спеки» сводилось в лучшем случае к двум-страничкам A4 с описанием нового модуля для уже работающей CMSки. Со временем я повидал и спеки как на 80 страниц от сторонних компаний в фиксированном pdf виде, так и в режиме постоянного scrumа где спека размазана по десяткам задачам в issue tracker'е.

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

Интеграционные тесты на дедлоки и одновременные запросы

· 2 мин. чтения

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

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

Создаём виртуальный образ ОС для среды разработки

· 2 мин. чтения

В качестве основы, будем использовать CentOS 6.5

  • Устанавливаем последние Virtualbox и Vagrant
  • Создаём новую виртуалку в virtualbox на основе федоры, поставив 2ГБ RAM и 20 GB под жёсткий диск
  • Скачиваем CentOS-6.5-x86_64-LiveCD.iso с http://mirror.nsc.liu.se/centos-store/6.5/isos/x86_64/. Он включает мгновенный бут (загрузчик), recovery и установку
  • Ставим root'у пароль vagrant (потом сменить можно)
  • Добавляем стандартного пользователя vagrant:vagrant

Обновляем систему:

#dont update unless you want CentOS 6.8 upgrade
#yum update -y

#install basic tools
yum install -y htop nano

#reboot for kernel updates to take place
shutdown -r now

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

Сначала - настроить пользователя

sudo visudo -f /etc/sudoers.d/vagrant
#adding to this file:
vagrant ALL(ALL) NOPASSWD:ALL

#setup vagrant public key access
mkdir -p /home/vagrant/.ssh
chmod 0700 /home/vagrant/.ssh
wget --no-check-certificate \
https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub \
-O /home/vagrant/.ssh/authorized_keys
chmod 0600 /home/vagrant/.ssh/authorized_keys
chown -R vagrant /home/vagrant/.ssh

nano /etc/ssh/sshd_config
#uncomment AuthorizedKeysFile %h/.ssh/authorized_keys

#enable public key authentication
service sshd restart

#start ssh on reboot
chkconfig sshd on

visudo #comment out requiretty option

Теперь для вагранта нужны Guest Additions. Это iso файл который у virtual box'а уже есть. Надо смонтировать его на диск и запустить установку пакетов

yum install -y gcc build-essential perl kernel-devel

#mount VBoxGuestAdditions.iso on CD in Virtualbox and run these:
sudo mount /dev/cdrom /mnt
cd /mnt
sudo ./VBoxLinuxAdditions.run

echo export KERN_DIR=/usr/src/kernels/`uname -r`/ >> ~/.bashrc

Теперь, у вас вероятно стоит надстройка для безопасности - SELinux. Если вы будете разрабатывать локально, то лучше его отключить

setenforce Permissive
nano /etc/sysconfig/selinux
#set to:
#SELINUX=disabled

Теперь когда всё готово, выходим из виртуалки и упаковываем её для vagrant'а. Для этого надо зайти в папку где лежит виртуалка и запустить комманду которая сгенерит package.box файлик на который в дальнейшем будет ссылаться Vagrantfile в нашем проекте:

#package box and move it to desktop
vagrant package --base CentosEmpty && mv package.box ~/Desktop/

#clean vagrant cache
vagrant global-status --prune

#cd to your project that references new box from desktop
vagrant up

См также:

http://williamwalker.me/blog/creating-a-custom-vagrant-box.html
https://www.vagrantup.com/docs/boxes/base.html
http://aruizca.com/steps-to-create-a-vagrant-base-box-with-ubuntu-14-04-desktop-gui-and-virtualbox/
https://stefanwrobel.com/how-to-make-vagrant-performance-not-suck

http://unix.stackexchange.com/questions/18435/how-to-install-virtualbox-guest-additions-on-centos-via-command-line-only

Управление количеством backend-процессов в реактивных приложениях

· 1 мин. чтения

Одна из проблем с которой сталкивается backend разработчик разрабатывая приложение с отложенными вычислениями это управление параллельными процессами. Например, если вы делаете загрузку картинок которые необходимо в дальнейшем обработать (уменьшить, вырезать, передвинуть на другой сервер), то для масштабирования такого решения под N -> infinity пользователей, каждый этап имеет смысл делать в отдельном процессе, число которых тоже можно масштабировать вместе с ростом N. 

Я по-прежнему использую php демонов, которые рождаются кроном (если на nohup полагаться не хочется). Но поскольку мне хочется одновременно делать ресайз картинки в 5 разных размеров одновременно, я хочу управлять количеством instance'ов php процессов. Это решается простой bash-коммандой:

[ "$(ps -ef| grep -v grep | grep imageResizer|wc -l)" -lt 5 ] && php imageResizer.php > /var/logs/imageResizer.log

Заметьте что ключевая комманда -lt. Если коротко, то мы ищем процессы (ps) по их имени (grep), исключая самих себя и считаем количество найденных строк (wc), количество которых должно быть меньше (lt) количества заданных мною инстансов. Как вариант вместо -lt можно использовать -eq 0, если хочется только одного демона

Datasync между браузерами

· 4 мин. чтения

При постройке качественного single-page приложения рано или поздно встаёт вопрос синхронизации данных. Это тем более актуально, когда дело касается работы разных пользователей. Приложения типа google wave, google docs, push-notifications в IOS, подняли планку ожидания пользователей и от обычных веб-систем

Что даёт синхронизация

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

Улучшает безопасность — если пользователь выходит из одного таба, его можно выкинуть из остальных автоматически

Снижает нагрузку на сервер — можно высылать оповещение о состоянии файла, если он обрабатывается в реальном времени, подобно ютубу без какого-либо взаимодействия с пользователем или поллинга

Сложности реализации

Если вы решили что это must-have фича, прежде чем начать реализовывать её у себя, задумайтесь о последствиях:

  • Вам понадобится транспортный механизм (см. ниже), который обычно ограничен количеством подключённых пользователей (макс. 10 тыс), размером сообщения и объёмом траффика
  • Безопасность. Транспортный механизм и источник оповещения должен точечно высылать данные только нужному пользователю (без broadcast-а).
    Лучше всего источник оповещения ставить на backend, сразу после закрытия транзакции. В этом случае полезно привязываться к сессии на бэкэнде
  • Я советую не использовать datasync-сервис в качестве источника данных. Это должен быть легковесный вестник адреса по которому можно прочитать детали. Более того, формат этого сообщения должен быть стандартизирован. Например {action:'file.added', id:1345}
  • Весь front-end код должен хранить данные в сыром виде в сервисах/моделях, которые будут точечно обновляться.
    Соответсвенно все события будут делится на прямое вмешательство пользователя и на datasync-действия
    Код при этом будет запускать похожие функции над данными
  • Все use-кейсы усложняются из-за ситуаций 
    • что делать если A удалил данные который B просматривает (редиректить B, блокировать A, показывать оповещение B?)
    • что делать если A и B одновременно сохраняют изменения над одним и тем же обьектом (провести обе операции, кто последний тот и победил? провести первую и заблокировать последующие до окончания операции?)
    • что делать если A составляет новый обьект 3, ссылающийся на обьект 2, который удаляет B, а потом происходит сохранение (удалять include при удалении или при сохранении?)
  • Если вы используете систему привилегий, то надо дополнительно иметь оповещения о присвоении или изъятия обьекта из видимости пользователей
  • Если ваш сервис синхронизации вдруг упадёт, то при поднимании надо сделать так что-бы сообщения не потерялись из-за ещё неподключившихся пользователей (у socket.io 5 сек)

Polling

Не ушёл в небытие, я по-прежнему вижу как некоторые сайты настойчиво стучат в бэкэнд. Например движение общественного транспорта в Таллинне. Видимо это проще всего реализовать и проще всего использовать вместо API. Что может быть проще для сторонних разработчиков - просто читай .json файл так часто как тебе это надо. А он в свою очередь может выплёвываться из кэша-в-памяти

Pusher

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

Лучше всего это делать на уровне backend'а, иначе захламите frontend и могут возникнуть редкие баги. Например — поставил заливаться файл и закрыл браузер, а клиент X не получил итогового оповещения. Для более тонкой настройки прийдётся повозиться с интеграцией авторизации и токенами

Firebase

Наконец, ещё один сервис, купленный гуглом, который больше позиционируется как nosql-база данных с синхронизацией между клиентами. Для того что-бы настроить её, как систему оповещений, но при этом оставить хранение данных у себя, прийдётся повозиться. Изначальный setup такой же простой как у pusher'а, да и авторизация по токену такая же, но поскольку это БД, то подписываться надо не на события оповещений, а на изменения json-схемы. Саму json-схему вы делаете сами, но я советую для этого случая использовать что-то типа..

{
notifications:[
2345:[
'file X added by 2346',
'file Y deleted by 2346'
],
2346:[
'file X added by 2345'
]
]
}

Websocket-сервер / Socket.io

Socket.io вы должны хостить сами и для этого нужен nodejs. Socket.io реализует браузерные веб-сокеты, протокол которых менялся несколько раз. Из-за этого могут быть трудности с подключением со стороны php к нему. Я использую elephant.io, пока полёт нормальный. Самый простой начальный вариант —  socket.io принимает подключение и делает broadcast всем подключённым клиентам. Для более тонкой настройки прийдётся повозиться с куки, php сессиями и желательно переносом их хранения в БД, что-бы backend мог высылать безопасно сообщение конкретному подключённому пользователю, после чего контролировать по session_id + socket кто что будет получать.

У socket.io есть альтернативные библиотечки — socky (любит руби), sockJs, вроде как очень быстрый wsengine.iodata.ioFaye я сам не пробовал, но судя по описанию растёт ещё со времени когда терминология с кометами была на слуху. Видимо теперь тоже работает на веб-сокетах

Микросервисы

· 7 мин. чтения

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

Микросервисы — не панацея и тоже усложняют всё приложение в плане транзактивности, логов, и обработки ошибок, управления конфигураций, версионирования, деплоя, возникает дублирование bootstrap-кода из-за изоляции сервисов. Поэтому стоит осторожно подходить к тому, что вы хотите выделить в микросервис и что он даст по характеристикам приложения (performance, стабильность, масштабирование, разделение нагрузки). Как правило, веб-приложения создаются сначала монолитом, а потом разделяются на сервисы — так эффективней и проще эволюционировать.

Паттерны взаимодействия

Общение между сервисами можно делать по-разному:

  1. Синхронно — сервис вызывают явно как веб-сервис по HTTP/REST, делая работу синхронно по-старинке, подобно веб-серверу, тогда как клиент ждёт ответа (подобно ajax)

  2. Асинхронно — cервис вызывают явно по HTTP/REST, регистрируется задача и клиент тут же получает job ID. Клиент обязан сам проверить состояние задачи, периодически спрашивая у сервиса (polling, aws transcoder)

  3. Асинхронно — сервис как работник (демон) без публичного доступа. Слушает какой-то ресурс и генерирует новые вызовы. Это может быть

  • единая шина сообщений (RabbitMQ,Gearman0MQ, Kafka, AWS SQS, AWS Kinesis)>
  • хранилище с локами (файлы, память, Memcache, Redis)
  • другие ресурсы (процессы, сеть, железо)
  1. Асинхронно, не используя MQ, регистрирует слушателей в себе и сам знает к кому куда стучать

Главное что-бы подход был единый, с предсказуемым форматом данных (JSONprotobufthriftmessagepack).Для удобной конфигурации и дружбы между сервисами, надо ставить ConsulETCD или ZooKeeper — они позволят абстрагироваться от конкретных IP адресов и PID процессов.

Пример

Например пользователь приложения имеет возможность загружать файлы.Обработка файла очень тяжёлая по CPU, генерирует несколько результатов и занимает много времени из-за долгих внутренних сетевых запросов по загрузке готовых обработанных файлов в хранилище. Допустим эта синхронная операция upload+resize+store занимает 20 секунд в монолитном приложении. В микросервисном приложении, вы решаете создать сервис обработки картинки, а для очередей сначала создаёте табличку в БД. Отлично, это даёт возможность сервис положить на другую машину и не нагружать основной сайт. Теперь возникают практические вопросы — как это всё сделать? Практически, если раньше в монолите разные слои приложения проверяли всё за вас, то теперь чуть ли не все значимые классы надо будет выделять отдельные сервисы авторизацию, логирование, менеджер картинок, менеджер транзакций и сам ресайз картинок.

Веб-сервис на PHP

Если ваш сервис должен иметь публичный веб-интерфейс, проще всего запустить php в качестве сервера под конкретную папку и она будет по запросу дёргаться. Тут пригодятся микро-фреймворки типа SilexSlim или Lumen

#run me as:
#php -S 0.0.0.0:80 -t /var/www/silex

#install me with:
#composer require silex/silex:~1.3

$loader = require __DIR__.'/vendor/autoload.php';

$app = new Silex\Application();

#синхронная работа
$app->post('/resizeImage', function(Request $request) use($app) {
#загрузить imagemagick, сделать resize, сохранить

return "{success:true}";
});

#асинхронная работа
$app->post('/resizeImageAsync', function(Request $request) use($app) {
#зарегистрировать новую работу и передать её демону через RabbitMQ, см. ниже
#клиента оповестим автоматом через websocketы

return "{success:true}";
});

Демон-работник на PHP

Хотя php изначально разрабатывался как интерпретатор html файлов, современный php может работать в постоянном режиме как сервис если его запустить в CLI режиме. В реальной жизни, php демоны как правило нестабильны из-за утечек памяти и не так эффективны по скорости как сервисы на nodejs или go. Запустить демон можно из коммандной строки..

php -f /var/www/app/daemon/myImageProcessor.php

#let it live after terminal exit:
nohup php -f myImageProcessor.php &

С демонами есть несколько особенностей

  • Работая в бесконечном цикле надо чистить ресурсы (DB-соединения, file handles)
  • Надо мониторить рост своей памяти что-бы не умереть от fatal error
  • Надо поддерживать graceful stop при получении SIGTERM и прочих сигналов что-бы не повредить данные на случай если кто-то остановит демона из консоли или перезапустит сервис
  • Надо ловить все исключения, делать альтернативную функциональность бэкапов и откатов, формировать красивый log
  • Перезапуск сервера должен запускать демоны заново — через крон или через регистрацию демона в виде системного сервиса

У вас может быть несколько экземпляров (instance) одного и того же демона в виде разных процессов. Тогда очень важно не попасть в проблемы многопоточности — дедлоки транзакций, запись в один log файл параллельно и обработка одного и того же задания. Обо всём этом чуть позже..

А пока, вот простой пример с бесконечным циклом и псевдо-кодом:

//myImageProcessor.php
//set custom process name
cli_set_process_title('php - daemon - myImageProcessor.php');

//enable process interruption handling
declare(ticks = 1);
$interrupted = false;
function handleSignal(){
$interrupted = true;
}

pcntl_signal(SIGTERM, "handleSignal");
pcntl_signal(SIGHUP, "handleSignal");
pcntl_signal(SIGUSR1, "handleSignal");
pcntl_signal(SIGINT, "handleSignal");

while(true){
$db->connect();

$db->query("SELECT * FROM images WHERE processed = 0");

//do some work here with resize & upload

$db->disconnect();

//memory cleanup & check
gc_collect_cycles();
const MAX_MEMORY_EXTRA = 10 * 1024 * 1024;
if (convertToBytes(ini_get('memory_limit')) < (memory_get_usage(true) + MAX_MEMORY_EXTRA)) {
exit();
}

//graceful shutdown
if($interrupted){
exit();
}

//wait for some time to decrease load on DB above
sleep(10);
}

Управление процессами

Проверку на УЖЕ запущенного демона можно сделать как на уровне bash, так и на уровне самого php с использованием экзотических флагов в базе, флагов во временных файлах или же используя такой же доступ к shell. С такой проверкой, комманду можно смело класть в cron и демон будет запускаться автоматом (раз в минуту)

Bash

#check if daemon is not running yet
#add logs
[ "$(ps -ef| grep -v grep | grep myImageProcessor|wc -l)" -eq 0 ] && php -f myImageProcessor.php > /var/www/logs/myImageProcessor.log

#up to 5 instances!
[ "$(ps -ef| grep -v grep | grep imageResizer|wc -l)" -lt 5 ] && php myImageProcessor.php > /var/logs/myImageProcessor.log

Остановить демона можно так же из шелла..

sudo /usr/bin/kill -9 `ps aux | grep 'myImageProcessor' | grep -v grep | awk '{print $2}'`

PM2

Для более могучего nodejs, существует отличный process manager, который может управлять и php-демонами! С его помощью можно сразу видеть только процессы демонов. Кроме того:

  • перезапускать демона сразу после его падения
  • следить за изменениями файла что-бы перезапустить демон (watch)
  • следить за памятью и CPU и перезапускать при их превышениях

Менеджеры попроще — forever и guvnor

Очереди и RabbitMQ

Как только вы начали использовать демонов, вероятней всего вы станете использовать и паттерн Command, по которому микросервисы выполняют роль работников (workers). MQ-сервер выступает в роли брокера и шины, связывающей асинхронные процессы воедино. Он отвечает за надёжность передачи сообщения сервису и за то что несколько сервисов не получат одну и ту же комманду. Реализовывать это на стандартной БД было бы сложней

Для PHP-демонов, подключение rabbitMQ делается с помощью php-amqplib библиотеки и бесконечный цикл в этом случае начинает зависеть от неё..

$callback = function($msg){
//actual work here
$data = json_decode($msg->body, true);

//Ack message that everything is done
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
}


//technical details..
$connection = new AMQPStreamConnection('localhost', 5672, 'root', 'pass');
$channel = $connection->channel();

//new queue if it was missing
$channel->queue_declare($queue, false, true, false, false);

//one entry at a time
$channel->basic_qos(null, 1, null);
$channel->basic_consume($queue, $receiverName, false, false, false, false, $callback);

//almost infinite cycle
while (count($channel->callbacks)) {
$channel->wait(null);
}

$channel->close();
$connection->close();

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

$queue = 'datasync';
$message = ['imageID'=>34, 'status'=>'processed'];
$message = json_encode($message);

$connection = new AMQPStreamConnection($server, $port, $login, $pass);
$channel = $connection->channel();
$channel->queue_declare($queue, false, true, false, false);

$channel->basic_publish(new AMQPMessage($message, ['delivery_mode' => 2]), '', $routeKey);
$channel->close();
$connection->close();

Перспективы

Более серьёзные сервисы пишутся на nodejs, а лучше на Go.

Грубо говоря, если прогнать нагрузочный тест, то с PHP вы получите 300 req/s, тогда как с node 7000 req/s, а с go 9000 req/s.

С Go можно копать в сторону go-micro

См. также

Блокировка сессий в php

· 1 мин. чтения

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

Одна проблема с БД решается транзакциями. Но вторая, более явная остаётся - сессии. В моём случае эта проблема вылезла при загрузке файлов в SPA-приложении. Делаете 20 POST запросов с файлами и параллельно ходите по сайту. Выглядит всё так, что покуда файл не загрузится и сервер не ответит, следующий GET не сработает и UI не обновится

Происходит это потому что php блокирует потоки где есть session_start(). В моём случае это все запросы, потому что есть авторизация

Решается очень просто:

session_write_close();

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

Amazon S3 direct upload

· 1 мин. чтения

вторник, 1 марта 2016 г. в 11:07:40

Amazon S3 поддерживает прямой upload. Делается всё просто со сторонними библиотеками, особенно если у вас ангуляр

bower install evaporate angular-evaporate --save

Добавляете в настройках путь к серверной подписке, где с php очень просто делается подпись:

echo base64_encode(hash_hmac('sha1', $_GET['to_sign'], AWS_SECRET, true));

Добавляете CORS-файл на S3, вбиваете в нужные места ключи — вот у вас готов аплоадер, поддерживающий multipart, параллельную загрузку файлов, необходимую для файлов в 100MB и выше

Изолированное UI тестирование с Protractor

· 2 мин. чтения

Я не так давно писал о protractor'е — он облегчает работу с selenium, хоть и завязан с jasmine. Недавно вышла 3я версия и я, заодно обновив jasmine до 2.4.1, решил двинуться дальше со сценарными тестами, а именно - лишиться их, разбив на изолированные UI-тесты. Зачем?

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

Цвета терминала и управление вывода с sh и php echo

· 1 мин. чтения

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

echo "\033[s"; //Сохранить положение курсора 
echo "\033[u"; //Восстановить положение курсора
echo "\033[<линия>;<колонка>H"; //Поставить курсор на линию и колонку

echo "\033[<линий>A"; //Сдвинуть курсор вверх
echo "\033[<линий>B"; //Сдвинуть курсор вниз
echo "\033[<колонок>C"; //Сдвинуть курсор вправо
echo "\033[<колонок>D"; //Сдвинуть курсор влево

echo "\033[2J"; //Очистить экран и перейти на начало
echo "\033[K"; //Очистить строку до конца

echo "\e"; //Аналог \033
echo "\r"; //Вернуться на начало строки

Цвета

Цвет терминала меняется как у шрифта так и у фона

echo "\033[0m"; //Безцветный текст
echo "\033[41m"; //Красный цвет фона
echo "\033[44m"; //Синий цвет фона

//Добавим цвет шрифта
echo "\033[0;31m"; //Красный цвет шрифта
echo "\033[0;37m"; //Белый цвет шрифта
echo "\033[0;33m"; //Желтый цвет шрифта

//Добавим вначало стиль шрифта
echo "\033[1;41;33m"; //Жирный (жёлтый на красном фоне)
echo "\033[4;41;33m"; //Подчёркнутый
echo "\033[1;4;41;33m"; //Жирный и подчёркнутый

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

JSDoc

· 3 мин. чтения

Скопирую статью про аннотации для javascript - JSDoc. Порой бывает очень полезно для автокомплита и прыгания в нужные места в IDE..

Общие тэги

/** 
* @deprecated Устаревшая функция
* @see nameSpace.ClassName#methodName Note, there are no { } brackets.
* @see namespace.ClassName Note, there are no { } brackets.
**//** This is a comment of how {@link nameSpace.ClassName} is similar to this object. **/

@param

/** 
* @param {paramType} paramName Basic parameter description.
* @param {String|Number} paramName Can be one of these types.
* @param {String} [paramName] Parameter is optional.
* @param {String[]} paramName Parameter is an array of strings.
* @param {function(string)} paramName Parameter is a function callback.
* @param {String} [paramName="mathew"] Parameter has a default value of "mathew" via code. (must be optional).
* @param objectName This parameter is an object with values.
* @param objectName.property1 This is a property of above object.
**/

Inline Documentation

/** Define parameter types in function arguments (useful for inline functions) **/
function(/**String**/arg1) { }

/** @type {string} msg **/ Handy for unknown return types.
var msg = foo();

Классы

/**
* @class {namespace.ClassName} Defines a reference to a class.
* @augments {namespace.ClassName} Define a derived base class.
* @extends alias for @augments
* @lends {namespace.ClassName} Declares a block to define part of a class.
* @constructor Defines a function as the creator for a class.
* @constructs Must be used with @lends to define the initialization function.
* @private Highlights invalid references to a property in PHPStorm
* @protected Highlights invalid references to a property in PHPStorm
* @override Defines a function as overriding a function of the same name in a lower class.
* @borrows namespace.ClassName#methodName Tells PHPStorm to use the documentation from another class/method for this method.
* @memberOf namespace.ClassName Declares something outside of a class block as part of that class.
**/

Функции

/**
* @this {type} Defines what object "this" referrers to.
* function(/**String**/arg1)Defines the parameters of an inline function.
* @return{type} The return type.
* @returns alias of above.
**/

Пример использования

Backbone Model and Inheritance A typical example using a Backbone model to demonstrate how to declare inheritance.

/**
* This is a model class what be will inherited.
*
* @class {gems.BaseModel}
* @extends {Backbone.Model}
**/
gems.BaseModel = Backbone.Model.extend(
/**
* @lends {gems.BaseModel}
*/
{
/**
* This is a property of the class.
*
* @type {string}
**/
message: 'Hello world',

/**
* A constructor for this class.
*
* @param {Object} attributes
* @param {Object} options
*
* @constructs
*/
initialize: function(attributes, options)
{
},

/**
* This is a virtual method on the model.
*
* @param {string} arg An argument
* @public
* @returns {boolean}
*/
foo: function(arg)
{
return false;
}
});

Теперь модель унаследовавшая BaseModel

/**
* A document model for Gems.
*
* @class {gems.DocumentModel}
* @extends {gems.BaseModel}
**/
gems.DocumentModel = gems.BaseModel.extend(
/**
* @lends {gems.DocumentModel}
**/
{
/**
* @param {string} arg An argument
* @see gems.BaseModel#foo
* @override
*
* @returns {boolean}
**/
foo: function(arg)
{
if(/**something**/)
{
return true;
}
return gems.BaseModel.prototype.foo.apply(this,[arg]);
}
});

Задержанные (Deferred) jquery объекты

· 1 мин. чтения

Небольшой know-how.. javascript асинхронный как вы понимаете, а это налагает ряд ограничений и проблем. Например когда у вас несколько асинхронных запросов (т.е. вы незнаете когда они завершатся), а от всех их зависит последующая логика приложения, то обычное не-асинхронное приложение (например на php) решает это в лоб - последовательно выполняет операции и получается такая картинка..

//Backbone пример
Model1.fetch({success:function(){
Model2.fetch({success:function(){
Model3.fetch({success:function(){
Model4.fetch({success:function(){
Model5.fetch();
}});
}});
}});
}});

Именно многопоточность даёт возможности ускорять backend на nodejs, масштабироваться на многоядерных процессорах, многомашинных обработках с map-reduce и тп. Для простого решения этой проблемы на frontend можно конечно написать костыль самому, но в jQuery уже есть Deferred объекты, с простым синтаксисом..

$.when(Model1.fetch(), Model2.fetch(), Model3.fetch(), Model4.fetch(), Model5.fetch()).then(
function(){alert('success!');}
);

Тамара Кулинкович о мотивации в IT проектах

· 1 мин. чтения

четверг, 19 марта 2015 г. в 02:57:12

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

Интеграционные тесты на дедлоки и одновременные запросы

· 4 мин. чтения

Я в последнее время всё больше люблю писать интеграционные (API) тесты — запускаю половину приложения, но не привязан к UI. Это золотая середина между очень медленными end-to-end тестами и очень быстрыми unit-тестами. Рассмотрим особый случай таких тестов, которые используют заготовленные данные под каждый тест. 

Такие тесты приходится создавать, когда проект становится таких масштабов, что тесты с одной БД начинают конфликтовать между собой и становятся нестабильными. То список где-то постоянно растёт, то ID у ресурса требуется фиксированный а у нас autoincrement, то данные хочется удалить. 

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