ermouth: (Default)

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

Паче чаяния, проект по итогам голосования был признан лучшим и даже заказчику приз какой-то дали от Ростелекома.

Коль скоро заказчик проекта его официально презентовал, правда, больше с бизнесовой стороны, не вижу причин не расказать о solovki.pro с колокольни разработчика. Несмотря на то, что всё только-только из-под Under construction выведено, за некоторые вещи “уже не стыдно” (с).

Фронтэнд

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

Туры кастомизированы – то-есть выбирается транспорт, проживание и развлечения на месте. Калькулятор устроен таким образом, что знает capacity каждого ресурса на каждую дату и даёт заказывать соразмерно возможностям принимающей стороны.

Снимок-экрана-2016-03-17-в-19.03

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

Capacity, к слову, считается на сервере одним единственным хитрым map/reduce-ом всего на 200- SLOC.

Калькулятор – jQuery.my приложение, 120 с небольшим кб в исходных текстах. Сделан в CloudWall, там же и отлажен. На картинке вот калькулятор исполняется в отладчике, в окошке.

Снимок экрана 2016-03-17 в 19.31.50

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

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

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

Мобильную версию калькулятора пока не делали – заказчик посчитал, что подождёт.

Приложения бэкэнда

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

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

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

В самом деле приложения даже не знают, с каким набором данных они работают – всю синхронизацию за них делает платформа. Приложение просто делает this.db.get(), this.db.query() или this.db.put().

Например, вот так выглядит диспетчер со старыми тестовыми данными:

Снимок-экрана-2016-03-17-в-19.17

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

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

Естессно, все приложения – манифесты jQuery.my.

Технологии

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

Серверная часть – node.js, патченная CouchDB и nginx. То-есть, основная часть кода на JS и немножко на Erlang-е. База крутится на SSD. Памяти на инстанс надо немного совсем. В общем, ничего необычного.

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

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

Локальные данные позволяют трюки, которые затруднительны при обычной архитектуре – например, массивные map/reduce запросы. Вообще, серверные мощности бэкэнда очень скромны, потому что основной объём всякой тяжести вынесен в клиентские браузеры.

“Цена” поддержания канала постоянной синхронизации неожиданно невелика – примерно 14Кб в час, если новых данных не поступает/отправляется. Канал, кстати, гарантирует доставку в конечном итоге, сообщения не могут быть потеряны.

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

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

Поглядим, как это всё полетит )

UPD. Хехе, тут вот уже багов накидали. Аккурат на пограничные случаи. Поправили, но чует моё сердце, что это не последние.

ermouth: (Default)

То-есть, грубо, как сымитировать SQL с помощью MapReduce. Хорошее краткое, но достаточно ёмкое изложение, хотя и наукообразное.

Снимок экрана 2015-04-29 в 1.02.43

http://infolab.stanford.edu/~ullman/mmds/ch2.pdf – тут только вторая глава, сабж со страницы 14 (32).

Вся книжка – “Mining of Massive Datasets”, Jure Leskovec, Anand Rajaraman, Jeffrey D. Ullman.

ermouth: (Default)

C целью “потрогать” arrow-функции в ES6 написал вот парсер выражений в польской нотации, простенький. Работает в консоли FireFox или в io.js, больше нигде не работает.

var polish = (function () {
  var ops = "+-/*".split("").reduce((a,b)=>(a[b]=Function("x","return x[0]=x.shift()"+b+"x[0],x"),a),{});
  return (s)=>s.split(/\s+/).reduce((a,b)=>(ops[b]?ops[b](a):!isNaN(b)&&b!=''?(a.unshift(+b),a):a),[]);
})();

polish ("10 1 2 3 + + *") напишет [60].

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

ermouth: (ang)

Прочитал на Хабре про успехи npm и решил написать пост. npm – это пакетный менеджер и публичный репозиторий для node.js, и успехи реально очень впечатляющие.

Снимок экрана 2015-01-10 в 7.04.56

Фишка в том, что этот репозиторий – база CouchDB. Не “веб-сервер плюс БД”, а именно просто БД. Кластер там, с обвесами – но основные функции выполняет CouchDB, вот на её мету прямой выход. И именно CouchDB там используется неспроста.

Доступность

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

По-хорошему, CouchDB сразу после установки становится веб-сервисом. Доступ к БД – только через http(s)-запросы, через REST-интерфейс, то-есть веб-сервер уже встроен в БД. Веб-приложение админки тоже встроено в БД, аж в двух версиях.

Система контроля доступа – простая, но совершенно железобетонная – тоже встроена в БД, как и механика авторизации.

БД умеет синхронизироваться в непрерывном режиме с другими экземплярами через http(s), в тч в режиме “мастер-мастер”. Протокол репликации хорошо документирован и основан на согласовании деревьев ревизий.

Последняя фича, например, значит, что можно иметь полную локальную “живую” копию npm. Можно даже в браузере, без установки CouchDB.

Хранение и запись

Сама по себе, как БД, CouchDB представляет из себя хранилище JSON-документов, но тут тоже есть целый ряд уникальных фич.

Операция записи/обновления – просто POST запрос, например, аяксом. Запись неблокирующая, это называется MVCC, и тут он честный, а не как в табличных БД.

У каждого дока есть ревизия, которая состоит из номера версии и случайного значения (типа 15-12efdab). При каждой записи в док версия инкрементится, а значение меняется. Записать в док можно только отправив значение предыдущей ревизии, причём если сохранённая ревизия не равна отправляемой, запись отменяется.

Запись идёт в режиме “append only”, ничего не пишется поверх. Это значит, что база помнит все ревизии документов до тех пор пока не будет выполнена операция очистки/оптимизации. Также это значит, что база выжимает из SSD-дисков всё, на что они способны – и при этом их бережёт.

И самое главное – к JSON-документам возможны файловые аттачи, примерно как к емэйлам. То-есть это не просто блобы, это блобы с именем и mime-типом.

Выборка по ключу

Нет ничего проще – GET-запрос типа domain/dbname/doc_id – например https://ermouth.couchappy.com/cwmanual/cw-Demo-Controls-4vx1 – сразу отдаст JSON-документ.

В этом документе есть приаттаченный файл – картинка. Она тоже доступна по прямой ссылке https://ermouth.couchappy.com/cwmanual/cw-Demo-Controls-4vx1/turing.jpg. Вот она, отображается прямо из CouchDB.

Выборка запросами

Любая выборка запросом из CouchDB – это выполнение map/reduce и выдача запрошенного диапазона ключей.

Именованные пары map/reduce функций, к которым выполняются запросы, хранятся в самой БД в специальных документах. Документ выглядит примерно так ermouth.couchappy.com/cloudwall/_design/cloudwall. Видно, что функция – javascript.

Снимок экрана 2015-01-10 в 8.50.45

Запрос к этой map/reduce паре (в которой reduce, правда, нет) выглядит примерно так:
ermouth.couchappy.com/cloudwall/_design/cloudwall/_view/info?startkey="cw"&endkey="cwz"

На выходе – краткая информация о документах в базе, подготовленная map-функцией. В диапазоне ключей cw…cwz.

Важнейшее отличие CouchDB от других БД – результаты вычислений map/reduce кэшируются и повторно map-функции не вычисляются, если документ не обновился.

То-есть map/reduce не требует фуллскана каждый раз, как, например, это происходит в Mongo. Фактически map-функции используются для построения индексов.

Валидация записи и частичное обновление

POST-запросы на запись могут проверяться в БД функциями-валидаторами. Они тоже js и тоже хранятся прямо в БД как специальные документы. Например, вот эта функция не даст писать в БД, если вы не авторизованы:

Снимок экрана 2015-01-10 в 9.03.51

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

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

Применимость CouchDB

Везде, где reads>>writes и структура хранимых данных – более-менее сложная. Также в силу специфики http и сериализации как читать, так и писать лучше сразу помногу.

Табличка вот по кейсам, 0 – совсем не подходит, 5 – лучше не придумаешь.

Версионированные хранилища доков 5
Распределённые синхронизированные хранилища 4
Хранилища частично нормализованных связанных данных 1-4
Полностью нормализованные данные 0
Быстрые логи 2
Медленные логи / Агрегаторы логов для анализа 5
Вообще большие наборы данных для анализа 5
Необходимость транзакционной целостности 0
Сложные повторяющиеся “фигурные” выборки 4
Выборки сабсетов узлов документов (частей документов) 5
Подключенные клиенты хотят уведомлений, что база обновилась 4
Хранение файлов (типизированных блобов) 4
SSD диски как хранилище 5+++
Синхронизация / репликация по каналам с потерями и обрывами 5+++

CouchDB вместо сервера приложений

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

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

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

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

----

В общем, всем ребятам, кто в вебе или около, очень советую как минимум покрутить-попробовать. Тем более анонимные эксперты вот говорят, что эта технология – один из трендов 2015.

Есчо, на Винде тоже прекрасно работает.

ermouth: (ang)

Полгода назад morfizm написал хороший пост про то, как работают технологии map/reduce для распараллеливания обработки больших объёмов данных.

Мне есть что к той истории добавить – все системы, что я построил за последний год, используют map/reduce для сортированной/группированной выборки данных из NoSQL БД. В силу целого ряда вкусностей мы используем CouchDB и примеры будут ближе к ней, но в остальных NoSQL БД принцип примерно такой же.

С помощью применения map/reduce практически любая сложная выборка по набору JSON-документов (деревья, не таблица) успешно приводится к простой выборке (цепочке выборок) диапазона ключей. В общем, для этого достаточно только map, про reduce я в следующий раз напишу.

Работает это так.

Пусть у нас есть БД с постами и комментами, то-есть с набором документов примерно такой структуры:


{type:"post", _id:"", author:"", title:"", stamp:0, content""}
{type:"comment", parentId:"", _id:"", author:"", title:"", stamp:0, content""}

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


function Feed (doc) {
  if (doc.type==="post") {
    emit ([doc.id, 0], doc    );
    emit ( doc.stamp , doc._id);
    //       _↑_        __↑__
    //       key        value
  }
  if (doc.type==="comment") emit ([doc.parentId, 1], doc)
}

Наша map-функция, в зависимости от типа документа, эмитит пары ключ-значение. Для поста мы эмитим сразу две пары, а для коммента – одну.

Заметим, что разные комменты к одному конкретному посту эмитят себя по одинаковому ключу. Также заметим, что ключ может быть не только string, но и array.

Если пройтись этой функцией по нашей БД, мы на выходе получим набор пар ключ-значение со следующими свойствами:


  1. Выбор из этого набора диапазона ключей startStamp … endStamp даст на выходе айдишники постов в этом диапазоне дат.

  2. Выбор потом ключей вида
    [postId1, 0], [postId1, 1], [postId2, 0], [postId2, 1], [postId3, 0], [postId3, 1]…
    отдаст нам сразу и посты, и все комменты к ним.

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

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

CouchDB именно так и делает. Над каждой базой можно создать набор именованных индексов, в которых хранятся результаты вычисления различных предопределённых map-функций. Выбирать из БД можно как по ID документов, так и по ключам результатов работы этих функций.

Индексы хранятся как B-tree, соответственно с рождения заточены под выбор диапазонов.

Чуток усложнив ключи с таймстампом – скажем, до [doc.author, doc.stamp], мы сможем выбирать посты конкретного автора. Диапазоны выборки ключей для первого запроса будут выглядеть тогда примерно так:  ["ermouth", 1420000000000] … ["ermouth", 1420014425025].

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

На этой прекрасной ноте заканчиваю 2014 год.

Всем счастья, радости и приятных ништячков в 2015! Ушёл готовить оливье.

Profile

ermouth: (Default)
ermouth

November 2021

S M T W T F S
 123456
78910111213
14151617181920
21 222324252627
282930    

Syndicate

RSS Atom

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jan. 31st, 2026 01:19 pm
Powered by Dreamwidth Studios