Мы были неподалёку от JavaScript, когда нами одолел php. Я помню сказал что-то вроде: Что-то у меня голова кружится. Может лучше тебе повести проект.



Внезапно, вокруг нас раздался ужасный бум… И весь WEB кишал этими статьями про LAMP…
Казалось, что они были написаны под любые нужды. Они разрастались и поглощали задачи для которых ранее пользовался perl, bash и даже С. Нет смысла говорить об этих статьях, подумал я. Все и так это знают.

У нас был пятый IE, немного Netscape, Opera и целое море разношёрстных cgi модулей на perl. Не то что бы это был необходимый стек технологий. Но если начал собирать дурь, становится трудно остановиться. Единственное что вызывало у меня опасение — это студент, употребляющий PHP. Нет ничего более беспомощного, безответственного и испорченного, чем это. Я знал, что рано или поздно мы перейдем и на эту дрянь.


В начале было не было


Когда-то JS между браузерами отличался как конь от пробки, а XMLHttpRequest возможен был исключительно через ActiveX. Выхода особо не было. Что на корню вырубало динамический веб. IE 6 казался революцией. Но если вы хотели кросплатформености — ваш путь пролегал через объемный MiddleWare. А большое количество статей по php, инструментов и сопутствующих новоиспеченных библиотек делали своё дело.

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

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

Конечно же, я пал жертвой повального увлечения. Обыкновенный уличный бездельник, который изучал все, что попадало под руку. В воздухе витали идеи о едином языке для Web разработки. Погоди, вот увидишь эти изменения в новом обличие, мужик.
— Давай Node.js посмотрим.
— Чего? Нет!
— Здесь нельзя останавливаться. Это толстый Middle.
— Садись.
Что за чушь они прут, не знаю сколько нужно писать на JavaScript чтобы найти сходство между Front, Middle и BackEnd? Это неподходящий вариант. Слишком толсто для выбранной цели.


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

Я почувствовал чудовищный протест против всей ситуации.

Есть же СУБД, такие как PostgreSQL, позволяющие невообразимо оперировать данными через встраиваемые функции. Бизнес логика внутри базы! Настоящий BackEnd. Не нравится бизнес логика в СУБД и ей там не место? А такие прямые потоки данных нравится?


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

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

Множественность запросов в одной сессии к СУБД ведёт к неизбежному апофеозу кончины скорости — многопроходности по таблицам базы.



Для исключения этих факторов, логику приходится переносить ближе к данным, т.е. в СУБД!
И это наиболее оптимальный способ вывести скорость на новые величины!

Становление идеи


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

Эти наивные разработчики полагали, что можно обрести душевный мир и понимание, изучив один из оплотов старого Middle, а результат — поколение толстого слоя, так и не понявших главную, старую как мир ошибку IT индустрии. Упустив убеждение, что Кто-то или Что-то может обладать фатальным недостатком.
К тому времени начал работать nginx, и уже не оставалось вопросов с чем нужно готовить предполагаемую мысль. Это больше была не идея. Теперь это было состязание программиста с ленью. Сама мысль, попытаться как-то описать архитектуру традиционным образом, через apache, казалась абсурдной.

В 2010 на github подоспел модуль ngx_postgres. Задание запросов в конфигурационном файле и выдача в JSON/CSV/ЧТОТОЕЩЕ.
Для конфигурации достаточно было простого SQL кода. Сложный можно было обернуть в функцию, которая потом вызывалась из конфига.

Но этот модуль не умел загружать тело http запроса в СУБД. Что накладывало серьезные ограничения на данные, оправляемые к серверу. URL же явно подходил только для фильтрации запроса.
Cериализация выходных данных проводилась средствами самого модуля. Что, с высоты моего дивана, казалось бессмысленным переводом ОЗУ — сериализовать умел и PostgreSQL.
Возникла мысль поправить, переписать. Я стал копать в коде этого модуля.

После бессонной ночи поисков многим хватило бы ngx_postgres. Нам нужно было что-то посильнее.
Мадам, сэр, детка, или как вас… вариант есть, вот, держите ngx_pgcopy.



NGX_PGCOPY


После эксперимента экспериментов и анализа анализов, возникла мысль переписать всё практический с нуля. Для ускорения загрузки были выбраны запросы COPY, которые перегоняют данные в базу на порядок быстрее инсертов и содержат собственный парсер. К сожалению, ввиду скудности описания подобного типа запросов, сложно сказать как будет вести СУБД при особо массовых вызовах данного метода.

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

Вместе с COPY-requests мы автоматом получили сериализацию в CSV по обоим направлениям, что избавило от необходимости заботы о преобразовании данных.

Начнём с примитива, отправка и получение всей таблицы в CSV по URL:
http://some.server/csv/some_table

CSV часть import.export.nginx.conf

pgcopy_server db_pub "host=127.0.0.1 dbname=testdb user=testuser password=123";

location ~/csv/(?<table>[0-9A-Za-z_]+) {
    pgcopy_query PUT db_pub "COPY $table FROM STDIN WITH DELIMITER as ';' null as '';";
    pgcopy_query GET db_pub "COPY $table TO STDOUT WITH DELIMITER ';';";
}



На текущий момент PostgreSQL не может в JSON и XML через COPY STDIN. Я надеюсь, что наступит день смирения с табами в код-стиле слона и я, или кто-нить, найдёт время и прикрутит этот функционал к COPY методам. Тем более, что в СУБД обработка этих форматов уже присутствует.

Однако! Способ применения здесь и сейчас всё же есть! Конфигурируем nginx на client_body_in_file_only on c последующим использованием в запросе переменной $request_body_file и передачей её в функцию pg_read_binary_file…
Разумеется придется это обернуть в COPY метод, т.к. работать будет только с моим мопедом, по причине полного body_rejecta у ngx_postgres. Других мопедов я пока не встречал, а ngx_pgcopy еще не созрел для дополнительного функционала.

Рассмотрим как это выглядит для json/xml в import.export.nginx.conf

client_body_in_file_only on;
client_body_temp_path /var/lib/postgresql/9.6/main/import;

location ~/json/(?<table>[0-9A-Za-z_]+) {
    pgcopy_query PUT db_pub 
        "COPY (SELECT * FROM import_json_to_simple_data('$request_body_file')) 
            TO STDOUT;";
    pgcopy_query GET db_pub 
        "COPY (SELECT '['||array_to_string(array_agg(row_to_json(simple_data)), 
            ',')||']' FROM simple_data) TO STDOUT;";
}

location ~/xml/(?<table>[0-9A-Za-z_]+) {
    pgcopy_query PUT db_pub 
        "COPY (SELECT import_xml_to_simple_data('$request_body_file') TO STDOUT;";
    pgcopy_query GET db_pub 
        "COPY (SELECT table_to_xml('$table', false, false, '')) TO STDOUT;";
}

Да, client_body_temp_path придётся выставить в директорию базы, а пользователю дать ALTER SUPERUSER. В ином случаем Postgres отправит наши желания за горизонт.
Экспорт, представленный в методах GET, использует встроенные функции, включённые в стандартную поставку Postgres. Все COPY выводят в STDOUT, на случай если мы захотим известить клиента о результатах этих действий. Импорт в фиксированную таблицу(simple_data) выглядит немного объемнее экспорта, посему вынесен в определяемые пользователем процедуры СУБД.

Часть из 1.import.export.sql для импорта в фиксированную таблицу

CREATE OR REPLACE FUNCTION import_json_to_simple_data(filename TEXT)
RETURNS void AS $$
BEGIN
    INSERT INTO simple_data
    SELECT * FROM 
        json_populate_recordset(null::simple_data, 
            convert_from(pg_read_binary_file(filename), 'UTF-8')::json);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION import_xml_to_simple_data(filename TEXT)
RETURNS void AS $$
BEGIN
    INSERT INTO simple_data
    SELECT (xpath('//s_id/text()', myTempTable.myXmlColumn))[1]::text::integer AS s_id,
           (xpath('//data0/text()', myTempTable.myXmlColumn))[1]::text AS data0
    FROM unnest(xpath('/*/*', 
        XMLPARSE(DOCUMENT convert_from(pg_read_binary_file(filename), 'UTF-8')))) 
    AS myTempTable(myXmlColumn);
END; 
$$ LANGUAGE plpgsql;

Функция импорта с гибким выбором таблицы для JSON особо не отличается от приведённой выше. А вот подобная гибкость для XML порождает более монструозное поделие.
Часть из 1.import.export.sql для импорта в произвольную таблицу
CREATE OR REPLACE FUNCTION import_vt_json(filename TEXT, target_table TEXT)
RETURNS void AS $$
BEGIN
    EXECUTE format(
        'INSERT INTO %I SELECT * FROM 
            json_populate_recordset(null::%I, 
                convert_from(pg_read_binary_file(%L), ''UTF-8'')::json)', 
        target_table, target_table, filename);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION import_vt_xml(filename TEXT, target_table TEXT)
RETURNS void AS $$
DECLARE
    columns_name TEXT;
BEGIN
    columns_name := (
    WITH
        xml_file AS (
            SELECT * FROM unnest(xpath( 
                '/*/*',
                XMLPARSE(DOCUMENT 
                    convert_from(pg_read_binary_file(filename), 'UTF-8'))))
    --read tags from file
        ), columns_name AS (
            SELECT DISTINCT (
                xpath('name()', 
                      unnest(xpath('//*/*', myTempTable.myXmlColumn))))[1]::text AS cn
             FROM xml_file AS myTempTable(myXmlColumn)
    --get target table cols name and type
        ), target_table_cols AS (  --
            SELECT a.attname, t.typname, a.attnum, cn.cn          
            FROM  pg_attribute a
            LEFT JOIN pg_class c ON c.oid = a.attrelid
            LEFT JOIN pg_type t ON t.oid = a.atttypid
            LEFT JOIN columns_name AS cn ON cn.cn=a.attname
            WHERE a.attnum > 0
                AND c.relname = target_table --'log_data'
            ORDER BY a.attnum
    --prepare cols to output from xpath
       ), xpath_type_str AS (
        SELECT CASE WHEN ttca.cn IS NULL THEN 'NULL AS '||ttca.attname 
                    ELSE '((xpath(''/*/'||attname||'/text()'', 
                            myTempTable.myXmlColumn))[1]::text)::'
                         ||typname||' AS '||attname
               END 
            AS xsc
        FROM target_table_cols AS ttca
       )
      SELECT array_to_string(array_agg(xsc), ',') FROM xpath_type_str
    );

    EXECUTE format('INSERT INTO %s SELECT %s FROM unnest(xpath( ''/*/*'',
             XMLPARSE(DOCUMENT convert_from(pg_read_binary_file(%L), ''UTF-8'')))) 
             AS myTempTable(myXmlColumn)', target_table, columns_name, filename);
END;
$$ LANGUAGE plpgsql;


В приведенных примерах, наименование table_name в импортируемом файле не влияет на целевую таблицу назначения, заданную в nginx. Применение иерархии xml документа table_name/rows/cols обусловлено исключительно симметрией со встроенной функцией table_to_xml.

Cами наборы данных…
simple_data_table.sql
CREATE TABLE simple_data (
    s_id    SERIAL,
    data0   TEXT
);

data.csv
0;zero
1;one

data.json
[ {"s_id": 5, "data0": "five"}, 
  {"s_id": 6, "data0": "six"}  ]

data.xml
<simple_data xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <row>
        <s_id>3</s_id>
        <data0>three</data0>
    </row>
    <row>
        <s_id>4</s_id>
        <data0>four</data0>
    </row>
</simple_data>

Мы тут отдалились, поэтому вернёмся к истокам чистого COPY…

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



Думаю я поймал страх.
Ерунда. Мы пришли, чтобы найти MidleWare мечту.
И теперь, когда мы прямо в её вихре, ты хочешь уйти?
Ты должен понять, мужик, мы нашли главный нерв.


Да, это почти так! Это отказ от CRUD.
Разумеется, многих возмутит как какой-то тип в паре предложений перечеркивает результаты работы Калифорнийских умов, стилизуя коротенькую статейку под диалоги наркоманского фильма. Однако, ещё не всё потеряно. Есть вариант передавать модификатор данных вместе с самими данными. Что всё равно уводит от привычной архитектуры RESTful.

К тому же, иногда, а когда и почаще, теоретические изыскания разбиваются о скалы реальности. Такими скалами является всё та же злополучная многопроходность. На деле, если вы допускаете изменение ряда позиций пользовательского документа, то вероятно, что данные изменения будут включать методы нескольких типов. В результате, для отправки в базу одного документа, потребуется провести несколько отдельных http запросов. А каждый http запрос, породит свою модификацию базы и свой проход по таблицам. Поэтому качественный прорыв требуют кардинальных изменений по отказу от классического понимания CRUD методов. Прогресс требует жертв.

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

Точка входа


К тому времени, как я задался этим вопросом,
Никого, кто бы мог на него ответить еще не было рядом.
Да-да, я снова начинаю…

Мы отправляем данные в средний слой, который без обработки пересылает их в базу.
СУБД их парсит и кладёт в таблицу, выполняющую роль журнала/лога. Регистрация всех входных данных.

Ключевой момент, вот он! Журналируемость/логируемость потока данных из коробки! А дальше дело за триггерами. В них, исходя из бизнес логики решаем: обновлять, добавлять или делать что-либо еще. Прикручивание модификатора данных не является обязательным, но может быть приятным бонусом.

Это приводит нас к достаточности использования HTTP методов GET и PUT. Попробуем смоделировать как это применять. Для начала определимся с разницей между журналами и логом. Ключевую разницу выделяем через приоритет между ценностью маршрута изменения и конечным значением. Первый критерий отнесём к логам, второй — к журналам.

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



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

Код из 2.jrl.log.sql:

Таблицы
CREATE TABLE rst_data (   --Output/result table 1/2
    s_id        SERIAL,
    data0       TEXT,     --Operating Data
    data1       TEXT,     --Operating Data
);

--Service variable with prefix s_, ingoring input value, it will be setting from trigers
CREATE TABLE jrl_data (   --Input/journal table 2/2
    s_id        SERIAL,   --Service variable, Current ID of record
    s_cusr      TEXT,     --Service variable, User name who created the record
    s_tmc       TEXT,     --Service variable, Time when the record was created
    p_trid      INTEGER,  --Service variable, Target ID/Parent in RST_(result) table, 
                          --    if exists for modification

    data0       TEXT,
    data1       TEXT,
);

CREATE TABLE log_data (  --Input/output log table 1/1
    s_id        SERIAL,
    s_cusr      TEXT,
    s_tmc       TEXT,
    pc_trid     INTEGER, --Service variable, Target ID(ParentIN/ChilrdenSAVE) 
                         --    in CURRENT table, if exists for modification

    data0       TEXT,
    data1       TEXT,
);

Триггер для журналов
CREATE OR REPLACE FUNCTION trg_4_jrl() RETURNS trigger AS $$
DECLARE
    update_result    INTEGER := NULL;
    target_tb        TEXT :='rst_'||substring(TG_TABLE_NAME from 5);
BEGIN
--key::text,value::text
    DROP TABLE IF EXISTS not_null_values;
    CREATE TEMP TABLE not_null_values AS
        SELECT key,value from each(hstore(NEW)) AS tmp0
	     INNER JOIN 
	     information_schema.columns
	     ON information_schema.columns.column_name=tmp0.key
	     WHERE tmp0.key NOT LIKE 's_%'
	       AND tmp0.key <> 'p_trid'
	       AND tmp0.value IS NOT NULL
	       AND information_schema.columns.table_schema = TG_TABLE_SCHEMA
	       AND information_schema.columns.table_name   = TG_TABLE_NAME;

    IF NEW.p_trid IS NOT NULL THEN
	EXECUTE (WITH keys AS (
	    SELECT (
	      string_agg((select key||'=$1.'||key from not_null_values), ','))
              AS key)
		SELECT format('UPDATE %s SET %s WHERE %s.s_id=$1.p_trid', target_tb, keys.key, target_tb)
		    FROM keys) 
        USING NEW;
    END IF;

    GET DIAGNOSTICS update_result = ROW_COUNT;
    IF NEW.p_trid IS NULL OR update_result=0 THEN
	    IF NEW.p_trid IS NOT NULL AND update_result=0 THEN
	        NEW.p_trid=NULL;
	    END IF;
    
        EXECUTE format('INSERT INTO %s (%s) VALUES (%s) RETURNING s_id', 
                       target_tb, 
                       (SELECT string_agg(key, ',') from not_null_values), 
                       (SELECT string_agg('$1.'||key, ',') from not_null_values))
		USING NEW;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Триггер для логов
CREATE OR REPLACE FUNCTION trg_4_log() RETURNS trigger AS $$
BEGIN
    IF NEW.pc_trid IS NOT NULL THEN
        EXECUTE (
        WITH
             str_arg AS (
		SELECT key AS key,
		       CASE WHEN value IS NOT NULL OR key LIKE 's_%' THEN key
		       ELSE NULL
		       END AS ekey,
		       CASE WHEN value IS NOT NULL OR key LIKE 's_%' THEN 't.'||key
		       ELSE TG_TABLE_NAME||'.'||key
		       END AS tkey,
		       CASE WHEN value IS NOT NULL OR key LIKE 's_%' THEN '$1.'||key
		       ELSE NULL
		       END AS value,
		       isc.ordinal_position
	        FROM each(hstore(NEW)) AS tmp0
		INNER JOIN information_schema.columns AS isc
		     ON isc.column_name=tmp0.key
		WHERE isc.table_schema = TG_TABLE_SCHEMA
		AND isc.table_name = TG_TABLE_NAME
		ORDER BY isc.ordinal_position)
	SELECT format('WITH upd AS (UPDATE %s SET pc_trid=%L WHERE s_id=%L)
	               SELECT %s FROM (VALUES(%s)) AS t(%s) 
	               LEFT JOIN %s ON t.pc_trid=%s.s_id',
	               TG_TABLE_NAME, NEW.s_id, NEW.pc_trid,
	               string_agg(tkey, ','), 
	               string_agg(value, ','), 
	               string_agg(ekey, ','),
	               TG_TABLE_NAME, TG_TABLE_NAME) 
	FROM str_arg
	) INTO NEW USING NEW;
	NEW.pc_trid=NULL;
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

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

Триггеры вызываются в порядке текстовой сортировки по имени. Я рекомендую использовать префиксы trg_N_. От trg_0 до trg_4 считать служебными, обслуживающими только целостность общей логики и входящую фильтрацию. А с 5 до 9 использовать для прикладных расчетов. Девять триггеров будет достаточно всем!

Так же стоит сказать, что их нужно устанавливать на BEFORE INSERT. Так как в случае c AFTER, служебная переменная NEW будет положена в таблицу до модификации триггером. В принципе, если целостность в некоторый степени не критична, подобное решение может быть хорошим ускорителем пользовательских запросов, проходящих через журнал. Это не повлияет на значения результирующей таблицы.

Еще, при AFTER, мы не сможем вернуть ошибку, если пользователь не имеет прав на изменение. Но корректный FrondEnd и не должен проводить запрещенные сервером операций. Соответственно, подобное поведения скорее свойственно взлому, который мирно будет зафиксирован в журнале.

ProФильтрацию и маршрутиризацию




Маршрутиризируем URL стандартными средствами nginx. Тем же способом фильтруем запрос от инъекций. После удвоения проблем, код похожий на результат асимметричного шифрования загоняем в директиву map nginx.conf для получения удобоваримого и безопасного SQL запроcа. Которым, в последующем, фильтруем данные.

Есть некоторые сложности. Они вызваны отсутствием в nginx синтаксиса регулярок для множественной замены по типу sed s/bad/good/g. В результате мы…

Попадаем прямо в гущу этого ебаного террариума. И ведь у кого-то хватаем ума писать эти чёртовы выражения! Еще немного и они разорвут мозг в клочья.

до 4х фильтров эквивалентности по URL
http://some.server/csv/table_name/*?col1=value&col2=value&col3=value&col4=value
Horrowshow part of filters.nginx.conf
#Составляем фильтр SQL
map $args $fst0 {
   default "";
   "~*(?<tmp00>[a-zA-Z0-9_]+=)(?<tmp01>[a-zA-Z0-9_+-.,:]+)(:?&(?<tmp10>[a-zA-Z0-9_]+=)(?<tmp11>[a-zA-Z0-9_+-.,:]+))?(:?&(?<tmp20>[a-zA-Z0-9_]+=)(?<tmp21>[a-zA-Z0-9_+-.,:]+))?(:?&(?<tmp30>[a-zA-Z0-9_]+=)(?<tmp31>[a-zA-Z0-9_+-.,:]+))?(:?&(?<tmp40>[a-zA-Z0-9_]+=)(?<tmp41>[a-zA-Z0-9_+-.,:]+))?"    "$tmp00'$tmp01' AND $tmp10'$tmp11' AND $tmp20'$tmp21' AND $tmp30'$tmp31' AND $tmp40'$tmp41'";
}

#Проверяем на корректность
map $fst0 $fst1 {
   default "";
   "~(?<tmp0>(:?[a-zA-Z0-9_]+='[a-zA-Z0-9_+-.,:]+'(?: AND )?)+)(:?( AND '')++)?" "$tmp0";
}
map $fst1 $fst2 {
   default "";
   "~(?<tmp0>[a-zA-Z0-9_+-=,.'' ]+)(?= AND *$)" "$tmp0";
}

#Если контроль корректности пройден, дописываем WHERE
map $fst2 $fst3 {
   default "";
   "~(?<tmp>.+)" "WHERE $tmp";
}

server {
    location ~/csv/(?<table>result_[a-z0-9]*)/(?<columns>\*|[a-zA-Z0-9,_]+) {
        pgcopy_query GET db_pub 
            "COPY (select $columns FROM $table $fst3) TO STDOUT WITH DELIMITER ';';";
    }
}

C фильтрацией кириллиц в URL, через конфиг nginx, тоже не всё гладко — нужна нативная конвертация из одной переменной с base64 в другую, с человеко читаемым текстом. На текущий момент, такой директивы нет. Что достаточно странно, ибо в исходниках nginx, функции перекодировки присутствуют.
Как-нить обязательно соберусь с мыслями и ликвидирую это упущение, как и проблему с sed, если коллектив nginx inc этого не решит.

Можно было бы отдавать url строку с аргументами в СУБД, для внутренней генерации динамического запроса в прямом вызове функции или вызове через тригер лог таблицы. Но так как такие данные уже логируются в nginx-access.log — эти инициативы избыточны. А с учетом того, что подобные действия могут увеличить нагрузку на планировщик базы, еще и вредны.

Smokeall FAQ


— Модули под nginx давно и успешно пишутся. К чему фанфары?
Большинство существующих аналогов это узкоспециализированные решения. В статье представлен разумный компромис скорости и гибкости!

— Работать через диск(client_body_in_file_only) — медленно!
Да прибудет с вами RAM Drive и пророк его — кэш файловой системы.

— Что с правами для пользователей?
Авторизация с plain http пробрасывается в постгрес. Там разруливаете встроенными средствами. В общем полный BackEnd.

— А шифрование?
Модуль ssl через конфиг nginx. На текущем этапе может не взлететь из-за сырости кода ngx_pgcopy.
Соединение nginx c postgres, при разнесении серверов, параноики могут прокинуть через ssh.

— Нахрена символы JS в отражении очков, в начале? Где JavaScript?
JS идёт на FrontEnd. А это уже совсем другое кино.

— Есть ли жизнь на клиенте с отключенным JS?
Как вы вероятно успели заметить ранее, в примерах, Postgres может в xml. Т.е. получить на выходе готовый HTML не составляет проблемы. Как с использованием спагетти кода, так и через xsl схемы.
Это ужасно. Тем не менее, всё будет хорошо. Вы всё правильно делаете.

— Как ресайзить картинки, паковать архивчики и считать траекторию лептонов на GPU?



И так, чтобы попроще.

Может, мне лучше поболтать с этим парнем, подумал я.
Не подходи к FastCGI!
Они только этого от нас и ждут.
Заманить нас в эту коробку,
Cпустить в подвал. Туда.

Говорим ngixу client_body_in_file_only on, берём в охапку $request_body_file и plperlu, с доступом к комадной строке. И адаптируем что-нить из:

CREATE OR REPLACE FUNCTION foo(filename TEXT) RETURNS TEXT AS $$
    return `/bin/echo -n "hello world!"`;
$$ LANGUAGE plperlu;

— Это похоже на CGI. А CGI не безопасен!

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

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

Ссылки


> ngx_pgcopy
> PostgreSQL COPY request
> slim_middle_samples (примеры из статьи + сборка демонстрации)

WARRNING


Модуль еще в разработке, поэтому возможны проблемы со стабильностью. Ввиду еще не реализованного мной keep alive соединения в сторону backend, на роль сверхзвукового истребителя сие творение, пока еще, ограничено годно. README модуля вам в чтиво.

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



В статье использованы кадры и цитаты из х/ф «Страх и ненависть в Лас-Вегасе» 1998г. Материалы применены исключительно с некоммерческими целями и в рамках стимулировании культурного, учебного и научного развития общества.
Поделиться с друзьями
-->

Комментарии (37)


  1. mayorovp
    16.06.2017 15:09
    +1

    Желание иметь прямые потоки данных, конечно же, правильное… но почему бакэндом должна быть база данных?


    1. AntonRiab
      17.06.2017 19:48
      -1

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

      Если вы об толстом Middle, то аргументация — в статье + смотрите ветку комментариев.
      Ну а для FrontEnd разработчиков (JS в браузерах и иные потребители API через http), взаимодействующих с сервером, вся серверная сторона — BackEnd.


  1. kolu4iy
    16.06.2017 15:47

    Это вы мои розовые мечты читаете. Правда, вместо Postgres у меня другой инструментарий, но тем не менее… А lua не помощник? Ну вместо семиэтажных регулярок минимальную логику проброса потока на lua собирать, например…
    Я не настолько хорошо знаю nginx, чтобы конкретное что-то посоветовать. Я в нём только со статикой и fastcgi неплохо разобрался.


    1. AntonRiab
      16.06.2017 21:00

      Lua малость выходит за рамки минимализма, но как альтернатива отсутствию синтаксиса глобальной замены во встроенных регулярках — очень даже вариант. Воркер nginx вроде не сильно толстеет.
      Пример конфига с фильтрацией через lua, кол-во переменных не ограничено:

      set $filter_by_lua "";
      location ~/lua/(?<table>[a-z0-9_]*)/(?<columns>\*|[a-zA-Z0-9,_]+) {
          access_by_lua_block {
              local rex = require "rex_pcre"
      
              local tmp, tmp2, tmp3
              tmp  = string.gsub(ngx.var.args, "&", " AND ");
              tmp2 = string.gsub(tmp, "([%a%d_]+)=([%a%d_+-.,:]+)", "%1='%2'")
              tmp3 = rex.match(tmp2, 
                  "[a-zA-Z0-9_]+='[a-zA-Z0-9_+-.,:]+'(?: AND [a-zA-Z0-9_]+='[a-zA-Z0-9_+-.,:]+')*")
      
              if string.len(tmp3) > 0 then
                  ngx.var.filter_by_lua = "WHERE " .. tmp2
              end
          }
          add_header Content-Type "text/plain; charset=UTF-8";
          pgcopy_query GET db_pub 
              "COPY (select $columns FROM $table $filter_by_lua) TO STDOUT WITH DELIMITER ';';";
      }
      

      Если работать без lua модуля rex_pcre, то выражения идущие на контроль корректности, типа '(foo)+' работать не будут см.Limitations of Lua patterns. Соответственно в tmp3 придется вручную прописывать количество переменных. Пример для двух:
      tmp3 = string.match(tmp2, "[%a%d_]+='[%a%d_+-.,:]+' AND [%a%d_]+='[%a%d_+-.,:]+'")
      

      Еще как вариант, использовать встроенный ngx_http_perl_module. По производительности lua-nginx vs ngx_http_perl, к моему сожалению, не подскажу.


    1. AntonRiab
      17.06.2017 06:40

      В догонку, решение на Nginx-Perl

          perl_set $filter_by_perl "sub { 
              my $r = shift;
              $_ = $r->args;
              s/&/ AND /g;
              s/([a-zA-Z0-9_]+)=([a-zA-Z0-9_+-.,:]+)/$1='$2'/g;
              if(m/([a-zA-Z0-9_]+='[a-zA-Z0-9_+-.,:]+'(?: AND [a-zA-Z0-9_]+='[a-zA-Z0-9_+-.,:]+')*)/g) {
                  return 'WHERE '.$1;
              }
              return '';
          }";
      
          server {
              listen       8880;
              server_name  127.0.0.1;
      
              pgcopy_server db_pub "host=127.0.0.1 dbname=testdb user=testuser password=123";
              location ~/perl/(?<table>[a-z0-9_]*)/(?<columns>\*|[a-zA-Z0-9,_]+) {
                  add_header Content-Type "text/plain; charset=UTF-8";
                  pgcopy_query GET db_pub 
                      "COPY (select $columns FROM $table $filter_by_perl) TO STDOUT WITH DELIMITER ';';";
              }
          }
      

      Еще некоторые особенности Nginx-Perl…
      На content handler результат не предсказуем, т.к. неизвестно какой хэндлер раньше вызовется(ngx_pgcopy или Nginx-Perl). Соответственно, есть вероятность, что фильтр сработает после отправки sql запроса.
      На access_handler не заработало, скорее всего дело в особенности ngx_pgcopy. Он вертится в этой фазе во время установки контакта с базой, принудительно откатывая статус соединения с клиентом. Что, вероятно не нравится Nginx-Perl и он преждевременно закрывает соединения.


      1. kolu4iy
        19.06.2017 08:45

        А nginScript, как я понимаю, аналогичен nginx-perl по реализации… Надо таки пробовать lua. Спасибо.


        1. AntonRiab
          20.06.2017 15:23

          Раз уж вспомнили про nginScript… на сегодня, статьи на хабре и nginx.org не актуальны для текущей версии интерпретатора. Последняя версия из репов меркуриала не поддерживает какой-либо код внутри nginx.conf. Только js_include файла и ссылка на функцию через js_set. Актуальная документация на www.nginx.com
          Кусок из http блока nginx.conf

          js_include SourceJavaScript.js;
          js_set $filter_by_njs FilterFunc;
          
          $filter_by_njs используем в SQL запросе по аналогии с предыдущими примерами.

          Файл SourceJavaScript.js с самым лайтовым вариантом регулярок:
          function FilterFunc(req) {
              var v0, value, data, full_filter = "";
          
              var regex_value = /([\d\w]*)/;
              var regex_data  = /[\d\w,._]*/; 
              for (v0 in req.args) {
                  value = regex_value.exec(v0);
                  if (full_filter.length > 1) full_filter += " AND";
          
                  if(value != 'undefined') {
                      data = regex_data.exec(req.args[v0]);
                      full_filter += " " + value[1] + "='" + data + "'";
                  }
              }
          
              if (full_filter.length > 0)
                  return " WHERE"+full_filter+"\n";
          }
          


          Из положительных моментов: гораздо меньше требований ко внешним библиотекам системы, в отличии от lua и perl. Если уже собирали nginx из slim_middle_samples через make likeiamlazy и есть меркуриал, то можно вытянуть последнею версию slim-a и собрать make njs.config, после чего должен завестись url типа http://127.0.0.1:8880/njs/simple_data/*?s_id=1. Полная версия js конфига в директории nginx.conf в slim_middle_samples.


  1. saw_tooth
    16.06.2017 16:01
    -2

    а что эт за технология такая phyton?


    1. AntonRiab
      19.06.2017 06:27

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


      1. saw_tooth
        19.06.2017 14:08

        Да, ознакомлен, просто у меня read-only аккаунт. За минусы спасибо)


  1. nohuhu
    17.06.2017 02:35
    +1

    Я вот сейчас страшно крамольную вещь скажу, вы только не обижайтесь: то, что вы описываете, вполне легко делается в middleware, если не гнаться за RESTful API, а использовать RPC.


    Выставлять базу напрямую для JS клиентов получится только на первых порах. Потом вас больно укусит ортогональность архитектуры stateless HTTP запросов к stateful соединениям в базу. Окажется, что кто-то должен держать в "голове" состояние процесса, и как бы middleware здесь самый логичный кандидат.


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


    В общем, идея интересная, но я буду придерживаться более проверенных решений. Всё уже украдено до нас. (с)


    1. AntonRiab
      17.06.2017 02:41

      Согласен с вами по поводу RPC. Добавление модификатора данных к самим данным, выводит на подобную реализацию.

      RESTful, по основным требованиям, описанная в статье архитектура — проходит. Отличается только RESTful-pattern — отсутствие CRUD.

      Stateless vs stateful. Вспоминаю момент, когда я начинал писать на plsql. Я часто использовал процедурные циклы внутри функций с соответствующими последствиями — проседанием скорости исполнения. Сейчас, я вкручиваю однопроходный декларативный sql в такие места, о которых раньше и не догадывался.
      Думаю это больше вопрос привычки, как и использование stateless.

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


      1. nohuhu
        17.06.2017 05:55
        +3

        RESTful, по основным требованиям, описанная в статье архитектура — проходит. Отличается только RESTful-pattern — отсутствие CRUD.

        Честно сказать, я не большой фанат REST подхода. Большей частью потому, что он религиозно наэлектризован и сильно напоминает мне пресловутую красную селёдку. "Это RESTful или недостаточно RESTful?" Да какая разница-то? Работает/не работает, подходит/не подходит, делает жизнь легче или нет, вот правильные критерии.


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


        Вот скажем, вопрос сразу в лоб: а как вы юнит-тестирование организовывать будете? Глядя на примеры конфигурации nginx выше, напрашивается ответ: никак. Когда на любой чих нужна поднятая связка nginx + Postgres с полной схемой и тестовыми данными, это уже слишком хрупко на мой вкус. Я предпочитаю среды и инструменты, которые легко изолировать и тестировать.


        И вы уж извините, но парсинг и переписывание SQL запросов регулярными выражениями это просто классический пример из шутки про "у вас есть проблема, и вы хотите использовать regex..."


        Stateless vs stateful.… Думаю это больше вопрос привычки, как и использование stateless.

        В том-то и дело, что нет. Одностраничные приложения на JavaScript накладывают архитектурные ограничения, и обойти stateless HTTP можно, но ценой отдельного колхоза. А с другой стороны — PostgreSQL, который рассчитан на stateful клиентов. Что-то должно заниматься скрещиванием ежа и ужа, и вы предлагаете это что-то размещать в самой базе. На мой взгляд это даст больше проблем, чем решений.


        Чтобы не быть голословным, приведу пример решения на стеке, который сам предпочитаю: https://github.com/nohuhu/HTML5-StarterKit. Времени на этот проект у меня не хватает, поэтому он давно не обновлялся; идею же демонстрирует вполне.


        1. AntonRiab
          17.06.2017 07:53

          Вот скажем, вопрос сразу в лоб: а как вы юнит-тестирование организовывать будете?
          1. Perl. Потом, а можно и сразу ngx_echo + парсиниг лога nginx.
          2. Точка входа — таблицы, т.е. отлаживаются простыми инсертами. Пример positive/negative standalone sql теста на журналы и логи в slim_middle_samples.

          Регулярные выражения нужны для составления и контроля. Какие альтернативы для динамических sql выражений и проверке не прямых данных на инъекции?
          Если это делать в полуручном режиме(map/for, if/case на встроенные переменные), тогда без большого среднего слоя — никуда. Узкое место подобной реализации я описал в статье + увеличение объем кода.

          PostgreSQL, который рассчитан на stateful клиентов
          Парсинг и генерация json, xml, csv, чтотоеще — из коробки. Я думаю тут как раз обратное, но скорее всего, это уже дело религии.


          1. nohuhu
            18.06.2017 01:45

            Perl. Потом, а можно и сразу ngx_echo + парсиниг лога nginx.

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


            Точка входа — таблицы, т.е. отлаживаются простыми инсертами.

            Т.е. вы предлагаете вместо своей странной middleware тестировать, как работает база? Спасибо, я вполне убеждён, что разработчики PostgreSQL уже всё, что нужно, протестировали. Мне не интересно тестировать базу. Мне нужно убедиться, что middleware делает то, что мне нужно, не делает того, что мне не нужно, и умеет отличать одно от другого. Примеры в студию.


            Регулярные выражения нужны для составления и контроля. Какие альтернативы для динамических sql выражений и проверке не прямых данных на инъекции?

            Ну как бы, prepare/execute? Такие, знаете, старые-добрые инструменты, которые давным-давно уже доступны и проверены.


            Если это делать в полуручном режиме(map/for, if/case на встроенные переменные), тогда без большого среднего слоя — никуда.

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


            Узкое место подобной реализации я описал в статье + увеличение объем кода.

            Узких мест у отдельного middleware полно, как и преимуществ. У вашего подхода тоже узких мест по самое не балуйся. О производительности вы уже подумали? О масштабируемости? Сколько одновременно открытых соединений ваша архитектура выдержит? А сколько одновременных запросов?


            PostgreSQL, который рассчитан на stateful клиентов
            Парсинг и генерация json, xml, csv, чтотоеще — из коробки. Я думаю тут как раз обратное, но скорее всего, это уже дело религии.

            Сериализация JSON и XML это такие мелочи, что о них говорить смысла нет. И речь не о религии, а о прямой 1:1 зависимости клиент: соединение в Postgres. Или я что-то проспал, и libpq уже научилась переключать пользовательский контекст в пределах одного соединения? Оно даже больше одного активного запроса на соединение не умеет, чтобы хоть как-нибудь извернуться.


            1. AntonRiab
              18.06.2017 13:21

              ngx_echo + парсиниг лога nginx. Покажите пример.
              Т.е. вы предлагаете вместо своей странной middleware тестировать, как работает база? Спасибо, я вполне убеждён, что разработчики PostgreSQL уже всё, что нужно, протестировали.

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

              Итого
              1. Тест регулярок для фильтрации, решение ngx_echo+curl+diff.
              С ngx_echo я ошибся комментарием выше — он выводит клиенту. Этот момент исправлен в приведенном примере. В тесте ngx_pgcopy подключён, но можно закоментить pgcopy_query, а echo_after_body поменять на echo и будет standalone nginx.

              2. Тест логики standalone sql.

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

              map/for, if/case и восхитительные дыры
              На страже дыр — регулярные выражения. Что эффективнее — я думаю, отдельный огромный и вечный вопрос. prepare/execute — в целевой архитектуре так же используется.
              В вашем проекте HTML5-StarterKit для работы с базой используется DBIx::Class.
              Код этого ORM содержит 113 регулярных выражений, без учёта динамических вызовов.

              О производительности вы уже подумали? О масштабируемости? Сколько одновременно открытых соединений ваша архитектура выдержит? А сколько одновременных запросов? libpq и пользовательский контекст… Оно даже больше одного активного запроса на соединение не умеет.
              Отнюдь! На каждое новое соединение клиента ngx_pgcopy открывает одно новое соединение с базой. Как и классические решения.
              Максимальное кол-во запросов, соединений и пользователей зависит только от nginx и postgresql.


              1. nohuhu
                21.06.2017 04:05

                Тестировать не субд, а собственную хранимую логику!

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


                А вся логика отлично проверяется не выходя из базы!

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


                Не покупаю.


                На страже дыр — регулярные выражения.

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


                У меня подобный опыт был, в том случае смысл обработки SQL сводился к "переводу" Informix диалекта на Postgres. В результате долгих плясок с бубном решение перевелось на Parse::RecDescent с обвязкой из вдоль и поперёк протестированных статических регулярных выражений; сделать просто на regex не получилось. Слишком много косяков лезло.


                И это всё, заметьте, принимало на вход запросы из строго контролируемых источников и только на SELECT. Делать что-то подобное для INSERT/UPDATE/DELETE я не буду никогда, слишком опасно. А уж принимать запросы в свободной форме от JavaScript клиентов вообще смерти подобно.


                В вашем проекте HTML5-StarterKit для работы с базой используется DBIx::Class.

                DBIx::Class в нём используется как вторичная зависимость, для ускорения разработки. Никто не мешает ORM выкинуть и работать с базой напрямую; суть же проекта всего лишь в демонстрации подхода.


                Отнюдь! На каждое новое соединение клиента ngx_pgcopy открывает одно новое соединение с базой. Как и классические решения.

                Классические middleware решения не открывают по соединению с базой на каждого клиента, а держат пул соединений и используют их по необходимости. Тысячи одновременно "открытых" stateless сессий с клиентами могут обслуживаться десятком соединений с базой. Ваша архитектура предполагает по соединению с базой на каждого клиента. Это совсем никак не похоже на классические решения, и гораздо более ресурсоёмко.


                Максимальное кол-во запросов, соединений и пользователей зависит только от nginx и postgresql.

                Проверил значение max_connections в postgresql.conf тестовой базы, почему-то там по умолчанию 100. Есть подозрение, что 10_000 слонёнку не понравится, а уж 100_000 тем более. Вот тут авторитетные товарищи утверждают, что больше "нескольких сотен" требует дополнительных костылей в виде connection pooling.


                Ещё раз, вы же предлагаете по соединению на каждую активную клиентскую сессию, да? А вовсе не на одновременно обрабатываемые запросы, как в классических схемах с middleware.


                А ещё у нас в какой-то момент времени может возникнуть необходимость во втором, третьем, N экземпляре nginx — и балансировки ради, и отказоустойчивости для. Сколько сладостных трудностей по синхронизации состояния это открывает!


                Максимальное кол-во запросов, соединений и пользователей зависит только от nginx и postgresql.

                А ещё, скажем, от количества памяти в сервере, т.к. libpq держит состояние открытого соединения "в голове". Как насчёт отлавливания недетерминистских отказов в обслуживании при флуктуациях доступного количества RAM? Или необходимости лимитировать количество HTTP соединений на экземпляр nginx, чтобы его процесс с разбухшим от счастья libpq внутри не сожрал всю память под соединения с базой?


                Что касается производительности при обслуживании N одновременных запросов, это тоже отдельный и очень интересный вопрос, учитывая event based архитектуру nginx и несколько кривую реализацию асинхронности в Postgres. Настолько интересный, что ну его нафиг.


                В общем, мсье знает большой толк в преодолении.


                1. AntonRiab
                  21.06.2017 09:32
                  -1

                  Получается, что всё же тестировать надо в связке?
                  Вначале, unit tests, потом всё в связке. Как и везде.

                  Классические middleware решения не открывают по соединению с базой на каждого клиента, а держат пул соединений и используют их по необходимости. Тысячи одновременно «открытых» stateless сессий с клиентами могут обслуживаться десятком соединений с базой. Есть подозрение, что 10_000 слонёнку не понравится, а уж 100_000 тем более.
                  Модуль еще в разработке, постоянно открытый пул еще не реализован — о чём и написано в конце статьи.

                  Одномоментно, на бд будет уходить столько, сколько настроите, остальные будут висеть в очереди nginx. Из-за ускорения логики, если её корректно перенести ввиде однопроходных запросов, серьезно сократится время обработки ответа. В результате — очередь пройдет быстрее, чем с толстым слоём.

                  А ещё у нас в какой-то момент времени может возникнуть необходимость во втором, третьем, N экземпляре nginx — и балансировки ради, и отказоустойчивости...10_000 100_000… лимитировать nginx чтобы libpq внутри не сожрал всю память

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

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


                  Скорость скакуна, в начале 20ого века, как и несколько тысяч лет до этого, неспешным галопом составляла ~20км/ч, для дальних дистанции. Максималка более 60км/ч, при коротких пробежках в 1-3км. На лошадь можно посадить пару людей. А если пристегнуть повозку, то и с десяток.
                  В те времена считалось, что аппараты тяжелее воздуха летать — не могут. Однако, 1903 состоялся первый полёт такого аппарата. Скорости была ~15км/ч на дальность 260м. Он мог перемещать только одного человека.

                  Где сейчас самолёты, а где лошади — вы и сами знаете.


                  1. serg_p
                    21.06.2017 12:08

                    Про фундамент — а где подписываться на комьюнити?????


                    1. AntonRiab
                      21.06.2017 15:33

                      На Github, watсh/star.
                      slim_middle_samples

                      • Общая документация по технологии
                      • Примеры реализации sql, nginx.conf
                      • Методики тестирования
                      • Туда же думаю неплохо было бы добавить примеры подключения FrontEnd

                      Модуль ngx_pgcopy, для отслеживания основного функционала. А так же для спецов по nginx, желающих поковыряться в кишках.


                  1. nohuhu
                    21.06.2017 21:16

                    Вначале, unit tests, потом всё в связке. Как и везде.

                    Ещё раз: приведите примеры. Можно просто ссылку на гигантский набор юнит-тестов в вашем проекте.


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

                    У меня есть ощущение, что вы не читаете мои сообщения, а отвечаете виртуальному собеседнику. :)


                    Основная цель статьи — попытаться создать фундамент для будущего комьюнити.
                    [...]
                    Где сейчас самолёты, а где лошади — вы и сами знаете.

                    Ах да, конечно. Laziness, impatience, and hubris. Как же я мог забыть.


                    Дерзайте!


  1. alaska332
    17.06.2017 22:19
    +2

    зачем нужен этот геморрой?


    1. AntonRiab
      18.06.2017 08:15
      -1

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

      — Требует от разработчика некоторое переосмысление устоявшийся идеологии
      — Нужно хорошее знание plpgsql и регулярный выражений

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


      1. alaska332
        18.06.2017 08:33

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


        1. AntonRiab
          19.06.2017 09:59

          На старте, все технологии относятся к академическим задачам.

          Что касается логов, то есть подробный отладочный вывод в nginx-error.log. Если нужен лог только с модуля, то включить его можно раскоментировав строку в файле ngx_http_pgcopy_module.c.
          //#define PGCOPY_DEBUG 1
          Cегодня добавил автоматическое включение отладки, если configure, перед сборкой, был c опцией
          --with-debug

          Так же есть пример скрипта для парсинга лога модуля и отображения его в виде отформатированного xml с иерархией и порядком всех вызовов.


          1. AntonRiab
            20.06.2017 15:26

            Еще важный момент, в разделе http нужно включить уровень логирования debug

            error_log /var/log/nginx-error.log debug;


  1. ncix
    17.06.2017 22:29

    Вы просто засунули MiddleWare внутрь PostgreSQL.


    1. AntonRiab
      18.06.2017 08:27

      Я разделил MiddleWare на транспортную функцию и прикладную логику.
      Транспортная функция ушла в ngx_pgcopy.
      Прикладная логика отправилась в PostgreSQL.

      По энергоемкости nginx+ngx_pgcopy = nginx+ngx_http_fastcgi_module(только модуль nginx, без толстого MiddleWare)
      Прикладную логику и раньше можно было пихать в субд, но для транспорта приходилось подгружать объемный средний слой. Теперь это не обязательно.


      1. ncix
        19.06.2017 10:09
        +1

        А то что СУБД подгружает дополнительную логику, никак на производительность не влияет?
        Хорошо бы тесты какие-то прогнать.


        Ну и отдельный вопрос, решение сделано исключительно для повышения производительности? Получается что в ущерб технологичности разработки: куда проще php-шника в проект найти чем нужного уровня специалиста по PostgreSQL. За разницу в зарплатах можно содержать дополнительные вычислительные мощности, чтоб гонять "толстый Middleware"


        1. AntonRiab
          19.06.2017 15:34

          Статья по тестам fat vs. slim middle запланирована, но очень не скоро.
          Там же думаю будет и небольшой раздел по встроенной логики против внешней. Для специалистов по PostgreSQL исход по логике - очевиден.

          Очень давно, на заре моего перехода на Postgres, я анализировал maillog. Файлики были ~100т. строк ~50MB. C помощью perl отформатировал в csv и отсеял всё лишнее, сколько длилась обработка — уже не вспомню. Далее попробовал вытащить из этих файлов обширную статистику с помощью perl. Вначале решил тестануть на одном файле и… выключил после более чем часа. Немного подумав решил, что с индексами дело пойдёт порасторопнее.

          Первое решение реализовал по следующему пути: из perl c помощью insert загнал данные в базу и потом уже через select доставал нужные куски обратно в perl и доводил результат до конца. Загрузка в БД заняла ~30 минут, анализ ~20 минут.

          Порывшись в документации СУБД наткнулся на COPY, загрузка сократилась до ~30 секунд! Вторым откровением были WITH Queries и рекурсивные запросы. В течении следующих нескольких дней я упорно оптимизировал запрос. Полностью избавился от loop во встроенной функции. Результатом был один SQL request размером с Эверест. И оно того стоило! Мне удалось оптимизировать анализ до ~10 секунд!
          Почувствуйте разницу: ~50 минут и ~40 секунд, на загрузку и анализ, на одной и той же машине!

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

          Что касается зарплат — для разработчика это не последний фактор. А актуальность для бизнеса — зависит от масштаба:
          — Для стартапа, если вам нужно привлекать специалистов снаружи — это, возможно, дорогое решение.
          — Если в основании стартапа специалисты по PostgreSQL, гуру perl старой закалки и профи по кишкам nginx — то это будет вам очень интересно.
          — Огромная компания с высокой нагрузкой? Тут уже одной экономией на электричестве можно покрыть эту разницу зарплат.


          1. mayorovp
            19.06.2017 15:40

            Не путайте бизнес-логику со сбором аналитики.


            1. AntonRiab
              19.06.2017 16:24

              Это был пример ускорения обработки. Ускорения логики вычисления — компьютерной логики!
              Что касается бизнес-логики и аналитики, то уже при частичной автоматизации принятия решений — эти разделы будут очень тесно переплетаться.


          1. ncix
            19.06.2017 16:59

            Мсье знает толк… ;) Я однажды тоже JFF написал машину Тьюринга на SQL без ХП: https://habrahabr.ru/post/113165/


  1. speller
    18.06.2017 20:02
    +1

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

    Следующим шагом в развитии данного решения станет превращение postgres в аналог субд cache, где можно писать код прямо в базе человеческим языком. Либо, автору следует (согласно его же постулатам) сменить религию и в качестве монолитного бэкэнда использовать cache.

    Возможно, всё то, что автор данной статьи хотел воплотить в своих необычных фантазиях, которые так высокохудожественно обернул в своем тексте, уже сделано в Cache: http://www.intersystems.com/ru/our-products/cache/tech-guide/chapter-4/ — там и nginx не нужен для того, чтобы обработать http запрос. Роутинг, разбор входных данных, обработку данных и выдачу результата делает один единственный СУБД сервер, и не надо писать костыли по связке nginx + postgres.

    Но за стремление к исследованиям и интересный стиль изложения — однозначно плюс. Даже если твое творение окажется никому не нужным костылём — это всё равно будет бесценным опытом.


    1. AntonRiab
      18.06.2017 20:08

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

      С Cache дело не имел, но порывшись по просторам сети нашёл следующую картинку на citforum

      Архитектура Cache CSP


  1. ocnk
    21.06.2017 00:26
    +1

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


    1. AntonRiab
      21.06.2017 09:01

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