Nginx

индекс
267,67

Игры в OLTP

В последнее время на Хабре стала популярной тема реализации высокопроизводительных приложений. Решили тоже немножко поэкспериментировать в этом направлении и поделиться текущими результатами наших изысканий.

Подопытный «Hello, world!» представляет собой простейшую OLTP систему:



Требования к производительности и отказоустойчивости являются ключевыми для подобных систем. Поэтому поиск решения поставленной задачи осуществлялся в направлении: C, C++, fastcgi, nginx, lighttpd, oracle. В первую очередь нам было любопытно попробовать различные варианты построения OLTP на данных технологиях, а так же измерить производительность и пиковые нагрузки.


И так, условия задачи


Необходимо обрабатывать транзакции вида: перевести X у.е. (денег, а вы что подумали) со счета A на счет B. Чтобы не придумывать велосипед и обеспечить масштабируемость, для взаимодействия с клиентом был выбран протокол HTTP.

Запросы имеют вид


server.com/paysys.request?account=123&operator=456&money=789
что значит: перевести 789 денег со счета оператора 456 на счет пользователя 123.

В базе данных всего три таблички:


(Рис 1. Структура БД)

Процедура транзакции не намного сложнее структуры базы данных:
  1.  procedure PROCESS_TRANS(p_account_id in number,
  2.                          p_operator_id in number,
  3.                          p_sum         in number,
  4.                          o_result     out number) is
  5.     
  6.     l_cnt  number;
  7.     
  8.     cursor cur_account is
  9.      select id
  10.         from pay_account
  11.       where id = p_account_id
  12.          for update of balance;
  13.  
  14.     cursor cur_operator is
  15.      select id
  16.         from pay_operator
  17.       where id = p_operator_id
  18.          and total >= p_sum
  19.          for update of total;
  20.  
  21.  begin
  22.  
  23.     o_result := -1;  -- tansaction begins
  24.     
  25.     select count(1)  -- if such operator exist?
  26.      into l_cnt
  27.      from pay_operator opr
  28.      where opr.id = p_operator_id;
  29.     
  30.     if (l_cnt != 1)
  31.     then
  32.      o_result := -2; -- Operator not found!
  33.      return;
  34.     end if;
  35.     
  36.      open cur_account;
  37.      open cur_operator;
  38.     
  39.     fetch cur_account into l_cnt; -- need to fetch to update rows later
  40.     fetch cur_operator into l_cnt; -- 0/1 row in cursor as selected by prim_key
  41.     
  42.     if cur_account%notfound then
  43.     
  44.      o_result := -3; -- Account not found!
  45.     
  46.     elsif cur_operator%notfound then
  47.     
  48.      o_result := -4; -- Operator has not enough money
  49.     
  50.     else
  51.     
  52.      update pay_account
  53.          set balance = (balance + p_sum)
  54.       where current of cur_account;
  55.  
  56.      update pay_operator
  57.          set total = (total - p_sum)
  58.       where current of cur_operator;
  59.      
  60.      insert
  61.         into pay_transaction
  62.           (
  63.              id,
  64.              account_id,
  65.              operator_id,
  66.              money,
  67.              datetime
  68.           )
  69.      values
  70.           (
  71.              pay_transaction_seq.nextval,
  72.              p_account_id,
  73.              p_operator_id,
  74.              p_sum,
  75.              sysdate
  76.           );
  77.      
  78.      commit;
  79.      o_result := 1;    -- transaction successfully finished!
  80.  
  81.     end if;
  82.     
  83.     close cur_account;
  84.     close cur_operator;
  85.     
  86.  end;
* This source code was highlighted with Source Code Highlighter.


Наиболее идеологически и практически правильным подходом к построению высоконагруженного серверного приложения с большим количеством одновременных подключений является реализация мультиплексора (The C10K problem).
В качестве БД решили взять «нетрадиционный» Oracle 10.2 Express Edition. Транзакции (update) в БД у нас выполнялись за ~2 ms. Временами, когда Oracle сбрасывал данные на диск (видимо dbwr записывал из буферного кэша измененные блоки данных), выполнение транзакции затягивалось до ~20 ms.

Для соединения с базой данных мы решили использовать библиотеки от производителя. Таковых, в рамках нашей задачи, набралось 2 шутки:
  • Oracle Call Interface ©
  • Oracle C++ Call Interface (C++)
OCCI, в лучших традициях ООП, гораздо более нагляден и прост в использовании. Однако, его низкоуровневый собрат обладает неоспоримым преимуществом: OCI позволяет выполнять неблокирующие (асинхронные) запросы к БД, что необходимо для реализации мультиплексирующего сервера при нескольких запросах к БД в рамках обработки одного клиента. Многие недовольны тем, что ради возможности неблокирующих вызовов необходимо писать достаточно громоздкий код и просят Oracle открыть исходники OCCI, но, насколько я знаю, пока безрезультатно. По производительности 2 брата отличаются крайне незначительно, что вполне ожидаемо.

Ингредиенты:

  • Oracle XE 10.2
  • Nginx 0.7.62
  • Lighttpd 1.4.23
обитали на Ubuntu 9.04 под VMWare на ноутбуке Acer 5920G, за неимением пока другого железа :). Надеемся нехватка ресурсов (в обоих смыслах) не отразилась на объективности результатов сравнительных тестов, ведь все испытания проводились в одной и той же среде.

Сразу оговоримся, что в настройке Oracle нам далеко до Тома Кайта, поэтому с тюнингом БД и ОС сильно не заморачивались. Займемся, когда переедем на полноценный сервер.

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

Опыты


Как построить такую системку? Нам первым на ум пришло самое простое решение на базе протокола FastCGI. Думаю, не стоит в очередной раз объяснять, чем выгодно отличается FastCGI от своего прародителя CGI. Для высоконагруженного приложения эта разница приципиальна. Хотелось бы подробнее остановиться на другом нюансе, который мы выяснили далеко не сразу.

Спецификация FastCGI предусматривает сохранение пользовательского соединения для возможности реализации мультиплексированной обработки нескольких запросов одновременно. Говоря простым языком, в обработчике запроса можно вызвать асинхронную операцию и спокойно отдать процессорное время другим клиентам, а на следующей итерации цикла опроса получить результат вызванной операции и, передав ответ клиенту, закрыть соединение. К сожалению, nginx и lighttpd (а также библиотеки libfcgi и fastcgipp) пока не поддерживают подобное, требуя закрыть соединение перед обработкой следующего клиента.

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

Nginx и lighttpd позволяют взаимодействовать с FastCGI двумя методами: через TCP канал и Unix Domain Socket. Преимуществом первого подхода является возможность кластеризации FastCGI серверов, недостатком – относительно высокие накладные расходы на передачу данных (TCP-стек все же). UDS же работает только локально, за счет чего обеспечивается более высокая производительность.

О втором методе хочется сказать очень много «теплых» слов. После первых тестов обнаружилось, что до FastCGI приложения доходит едва ли десятая часть запросов при одновременных запросах более 200 клиентов (на нашей железяке). Загвоздка оказалась в переменной ядра net.unux.max_dgram_qlen, которая ограничивает размер очереди запросов при записи в сокет. По умолчанию данная опция равнялась 10, что и объяснило потери запросов. После установки значения в 10000, казалось бы, все должно было прийти в норму. Но не тут-то было. Гарантированно отваливалась почти десятая часть запросов, а nginx писал в логии страшные слова “connect() to socket failed (11: Resource temporary unavailable)”. Говоря русским языком, при большом количестве одновременных запросов сокет «захлебывался». Данную проблему нам пока решить не удалось. Хоть производительность и возросла, но кому она нужна такая, когда порядка 10% транзакций завершаются кодом «502».


FastCGI сервер:


  1. const char *bind_address = ":9000";
  2. const char *db_user_name = "orauser";
  3. const char *db_password = "pass";
  4. const char *db_conn_str = "comp:1521/xe";
  5.  
  6. int main(int argc, char* const argv[] )
  7. {
  8.     using namespace oracle::occi;
  9.  
  10.     int listenQueueBacklog = 4000;
  11.     FCGX_Request request;
  12.  
  13.     if(FCGX_Init())
  14.         exit(1);
  15.  
  16.     int listen_socket = FCGX_OpenSocket(bind_address, listenQueueBacklog);
  17.     if (listen_socket < 0)
  18.         exit(1);
  19.  
  20.     if (fchmod(listen_socket, S_IROTH | S_IWOTH))
  21.         exit(1);
  22.  
  23.     if(FCGX_InitRequest(&request, listen_socket, 0)) exit(1);
  24.  
  25.     Environment*env = Environment::createEnvironment(Environment::DEFAULT);
  26.     Connection *conn = env->createConnection(db_user_name, db_password, db_conn_str);
  27.     Statement *stmt = conn->createStatement(
  28.                       "BEGIN PAY_SYS.PROCESS_TRANS(:v1,:v2,:v3,:v4); END;");
  29.  
  30.     while(FCGX_Accept_r(&request) == 0)
  31.     {
  32.         bool noException = false, succesful = false;
  33.  
  34.         long start = GetTickCount();
  35.         char *params = FCGX_GetParam("QUERY_STRING", request.envp);
  36.         int  operatorID,accountID,moneySum, result=-1;
  37.  
  38.         operatorID = GetIntParam(params, "operator=");
  39.         accountID = GetIntParam(params, "account=");
  40.         moneySum  = GetIntParam(params, "money=");
  41.  
  42.         if (operatorID <= 0 || accountID <= 0 || moneySum <= 0)
  43.         {
  44.             FCGX_FPrintF(request.out, "HTTP/1.0 503 Params Error\n");
  45.             FCGX_FPrintF(request.out, "Content-type: text/html\r\n\r\n");
  46.             FCGX_FPrintF(request.out, "wrong params<br>\n");
  47.             FCGX_Finish_r(&request);
  48.             continue;
  49.         }
  50.  
  51.         try
  52.         {
  53.             stmt->setInt(1, accountID);
  54.             stmt->setInt(2, operatorID);
  55.             stmt->setInt(3, moneySum);
  56.             stmt->registerOutParam(4,OCCIINT,sizeof(result));
  57.             stmt->execute();
  58.             result = stmt->getInt(4);
  59.  
  60.             if (result == 1)
  61.                 succesful = true;
  62.         }
  63.         catch(SQLException &sqlExcp)
  64.         {
  65.             error_log(sqlExcp.getMessage().c_str());
  66.         }
  67.  
  68.         if (succesful)
  69.         {
  70.             FCGX_FPrintF(request.out, "HTTP/1.0 200 OK\n");
  71.             FCGX_FPrintF(request.out, "Content-type: text/html\r\n\r\n");
  72.             FCGX_FPrintF(request.out, "SQL result = %d", result);
  73.         }
  74.         else
  75.         {
  76.             FCGX_FPrintF(request.out, "HTTP/1.0 503 DatabaseError\n");
  77.             FCGX_FPrintF(request.out, "Content-type: text/html\r\n\r\n");
  78.             FCGX_FPrintF(request.out, "Database error occured");
  79.             error_log("db error");
  80.         }
  81.  
  82.         FCGX_Finish_r(&request);
  83.     }
  84.  
  85.     conn->terminateStatement(stmt);
  86.     env->terminateConnection(conn);
  87.     Environment::terminateEnvironment(env);
  88.  
  89.     return 0;
  90. }
* This source code was highlighted with Source Code Highlighter.


Итого.


Использовались siege и ab. Из-за проблем с памятью siege наотрез отказался производить более 380 запросов в секунду, ab же «выжал» до 1000 запросов, но далее сослался на чрезмерное число открытых сокетов.

Для достоверности результатов, тестирование проводилось по схеме 10000 запросов при 100, 200, 300, 400 одновременных. Для FastCGI на этой машине с дефолтными настройками ядра нагрузок хватило для преодоления критического порога в 10% потерянных запросов. Связка lighttpd+fastcgi у нас начала «спотыкаться» при отметке ~200 запросов, связка же с nginx держалась до ~300 одновременных подключений. При нагрузке более 400 одновременных подключений оба варианта FastCGI отваливались с сообщением «apr_poll: The timeout specified has expired (70007)», тогда как модуль nginx без криков расправлялся и с 1000 одновременных коннектов через ab. Тесты опять-таки уперлись в «железо». Для наглядности результаты измерения мы решили представить в виде графиков.

Итак, по оси абсцисс — число одновременных запросов, по оси ординат – в соответствии с подписью к графику.


(Рис 2. Самый длительный запрос, сек)


(Рис 3. Среднее время обработки запроса, сек)


(Рис 4. Количество обработанных за секунду запросов)


(Рис 5. Время, затраченное на обработку 10000 запросов, сек)

Вывод.


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

В наших опытах пока не был использован механизм асинхронных вызовов БД. Любопытно, как изменятся результаты тестирования, если усложнить обработку клиента до нескольких обращений к БД и реализовать полноценный мультиплексор. На FastCGI так не получится, зато это можно проделать с модулем nginx, а также для полноты экспериментов призвать в ряды «подопытных» собственного HTTP демона на основе библиотеки вроде poco, asio, pion…

В общем, to be continued…

UPD
Модули nginx — расширения сервера, статически компилируемые совместно с ним. Встроенный функционал типа SSL и FastCGI также реализован как модули. Плюс — скорость, минус — сложность обновления на работающей машине. Отличные мануалы есть здесь.
Урезанный пример — ниже. Для разнообразия, работа с ORACLE через С-библиотеку OCI.
  1. // вариант БЕЗ поддержки настроек через nginx.conf и ОБРАБОТКИ ошибок
  2.  
  3. #include <ngx_config.h>
  4. #include <ngx_core.h>
  5. #include <ngx_http.h>
  6. #include <string.h>
  7. #include <oci.h>
  8.  
  9. const text *db_user_name = (const text*)"orauser";
  10. const text *db_password = (const text*)"pass";
  11. const text *db_conn_str = (const text*)"comp:1521/xe";
  12. const text *command     = (const text*)
  13.     "BEGIN PAY_SYS.PROCESS_TRANS(:v1,:v2,:v3,:v4); END;";
  14.  
  15. OCIEnv        *env;
  16. OCISvcCtx    *context;
  17. OCISession    *session;
  18. OCIServer    *server;
  19. OCIError    *error;
  20. OCIStmt        *statement;
  21. OCIBind     *bnd1, *bnd2, *bnd3, *bnd4;
  22.  
  23. int            operatorID, accountID, sum, result;
  24. static char* ngx_http_payment_init (ngx_conf_t *cf,
  25.                                     ngx_command_t *cmd, void *conf);
  26.  
  27. // массив возможных опций модуля в nginx.conf
  28. static ngx_command_t ngx_http_payment_commands[] =
  29. {    // задаем только одну основную опцию, включающую модуль в указанном Location
  30.     { ngx_string("payment_enabled"),
  31.      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
  32.      ngx_http_payment_init,
  33.      NGX_HTTP_LOC_CONF_OFFSET,
  34.      0,
  35.      NULL },
  36.      ngx_null_command            // терминирует массив
  37. };
  38. // callback-и, не используем
  39. static ngx_http_module_t ngx_http_payment_module_ctx =
  40. {
  41.     NULL,    /* preconfiguration */
  42.     NULL,     /* postconfiguration */
  43.     NULL,    /* create main configuration */
  44.     NULL,     /* init main configuration */
  45.     NULL,    /* create server configuration */
  46.     NULL,    /* merge server configuration */
  47.     NULL,    /* create location configuration */
  48.     NULL    /* merge location configuration */
  49. };
  50. // дескриптор модуля, включает в себя все параметры
  51. ngx_module_t ngx_http_payment_module =
  52. {
  53.     NGX_MODULE_V1,
  54.     &ngx_http_payment_module_ctx, /* module context         */
  55.     ngx_http_payment_commands,     /* module directives     */
  56.     NGX_HTTP_MODULE,              /* module type         */
  57.     NULL,                         /* init master         */
  58.     NULL,                         /* init module         */
  59.     NULL,                         /* init process        */
  60.     NULL,                         /* init thread         */
  61.     NULL,                         /* exit thread        */
  62.     NULL,                         /* exit process        */
  63.     NULL,                         /* exit master        */
  64.     NGX_MODULE_V1_PADDING
  65. };
  66.  
  67. void SetCallParams(int oper, int account, int money)
  68. {
  69.     if (bnd1 != 0) OCIHandleFree((dvoid*)bnd1, OCI_HTYPE_BIND);
  70.     if (bnd2 != 0) OCIHandleFree((dvoid*)bnd2, OCI_HTYPE_BIND);
  71.     if (bnd3 != 0) OCIHandleFree((dvoid*)bnd3, OCI_HTYPE_BIND);
  72.     if (bnd4 != 0) OCIHandleFree((dvoid*)bnd4, OCI_HTYPE_BIND);
  73.  
  74.     // копируем из стека, чтобы иметь возможность передать валидный указатель
  75.     result     = 0;
  76.     operatorID = oper;
  77.     accountID = account;
  78.     sum        = money;
  79.  
  80.     // привязываем параметры к вызову SQL
  81.     OCIBindByPos(statement, &bnd1, error, 1, &accountID,
  82.                 sizeof(accountID), SQLT_INT,(dvoid *) 0,
  83.                 (ub2 *) 0, (ub2 *) 0, (ub4) 0, (ub4 *) 0, OCI_DEFAULT);
  84.     OCIBindByPos(statement, &bnd2, error, 2, &operatorID,
  85.                 sizeof(operatorID), SQLT_INT,(dvoid *) 0,
  86.                 (ub2 *) 0, (ub2 *) 0,(ub4) 0, (ub4 *) 0, OCI_DEFAULT);
  87.     OCIBindByPos(statement, &bnd3, error, 3, &sum, sizeof(sum), SQLT_INT,
  88.                 (dvoid *) 0, (ub2 *) 0, (ub2 *) 0, (ub4) 0,
  89.                 (ub4 *) 0, OCI_DEFAULT);
  90.     OCIBindByPos(statement, &bnd4, error, 4, &result, sizeof(result),SQLT_INT,
  91.                 (dvoid *) 0,(ub2 *) 0,(ub2 *) 0,(ub4) 0,(ub4 *)0, OCI_DEFAULT);
  92. }
  93. // основной обработчик, вся работа выполняется здесь
  94. static ngx_int_t ngx_http_payment_handler(ngx_http_request_t *r)
  95. {
  96.     // очищаем body-раздел
  97.     ngx_int_t rc = ngx_http_discard_request_body®;
  98.  
  99.     if (rc != NGX_OK && rc != NGX_AGAIN)
  100.     {
  101.         ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
  102.                     "Failed ngx_http_discard_request_body()");
  103.         return rc;
  104.     }
  105.  
  106.     // строки в nginx хранятся как пара length-data,
  107.     // а не как традиционный C-style zero-end
  108.     // копируем для корректной работы парсера
  109.     const int buflen = 100;
  110.     char buf[buflen+1];
  111.     int len = (r->args.len < buflen)? r->args.len : buflen;
  112.     buf[len] = '\0';
  113.     ngx_memcpy(buf, r->args.data, r->args.len);
  114.  
  115.     int successful = 0;
  116.     int operator,abonent,money, result=-1;
  117.  
  118.     // вызов простейшего парсера
  119.     operator = GetIntParam(buf, "operator=");
  120.     abonent = GetIntParam(buf, "abonent=");
  121.     money    = GetIntParam(buf, "money=");
  122.  
  123.     if (operator > 0 && abonent > 0 && money > 0)
  124.     {
  125.         SetCallParams(operator, abonent, money);
  126.  
  127.         int retCode = OCIStmtExecute(
  128.                         context, statement, error, (ub4) 1, (ub4) 0,
  129.                         (OCISnapshot *) NULL, (OCISnapshot *) NULL,
  130.                         (ub4) OCI_COMMIT_ON_SUCCESS);
  131.         
  132.         if (retCode == OCI_SUCCESS)
  133.             successful = 1;
  134.     }
  135.  
  136.     r->headers_out.content_type.len        = sizeof("text/html") - 1;
  137.     r->headers_out.content_type.data    = (u_char *) "text/html";
  138.     r->headers_out.content_length_n        = 0;
  139.     
  140.     if (successful == 1)
  141.         r->headers_out.status = NGX_HTTP_OK;
  142.     else
  143.         r->headers_out.status = NGX_HTTP_INTERNAL_SERVER_ERROR;
  144.     
  145.     // ... формирование body ответа и передача его nginx-у.. см примеры emiller
  146. }
  147.  
  148. static char* ngx_http_payment_init(ngx_conf_t *cf,
  149.                                     ngx_command_t *cmd,
  150.                                     void *our_conf)
  151. {
  152.     // set nginx handler
  153.     ngx_http_core_loc_conf_t *core_conf =
  154.         ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
  155.     core_conf->handler = ngx_http_payment_handler;
  156.  
  157.     // oracle
  158.     bnd1 = (OCIBind *) 0;
  159.     bnd2 = (OCIBind *) 0;
  160.     bnd3 = (OCIBind *) 0;
  161.     bnd4 = (OCIBind *) 0;
  162.  
  163.     OCIEnvCreate((OCIEnv **)&env,(ub4)OCI_DEFAULT,
  164.                 (dvoid *)0,(dvoid * (*)(dvoid *, size_t))0,
  165.                 (dvoid * (*)(dvoid *, dvoid *, size_t))0,
  166.                 (void (*)(dvoid *, dvoid *))0,(size_t)0,(dvoid **)0);
  167.     /* allocate a server handle */
  168.     OCIHandleAlloc ((dvoid *)env, (dvoid **)&server,
  169.                     OCI_HTYPE_SERVER, 0, (dvoid **) 0);
  170.     /* allocate an error handle */
  171.     OCIHandleAlloc ((dvoid *)env, (dvoid **)&error,
  172.                     OCI_HTYPE_ERROR, 0, (dvoid **) 0);
  173.     /* create a server context */
  174.     int retCode = OCIServerAttach (server, error, db_conn_str,
  175.                             strlen((const char*)db_conn_str), OCI_DEFAULT);
  176.     
  177.     /* allocate a service handle */
  178.     retCode = OCIHandleAlloc ((dvoid *)env, (dvoid **)&context,
  179.                                 OCI_HTYPE_SVCCTX, 0, (dvoid **) 0);
  180.     /* set the server attribute in the service context handle*/
  181.     retCode = OCIAttrSet ((dvoid *)context, OCI_HTYPE_SVCCTX,
  182.                          (dvoid *)server, (ub4) 0, OCI_ATTR_SERVER, error);
  183.     /* allocate a user session handle */
  184.     retCode = OCIHandleAlloc ((dvoid *)env, (dvoid **)&session,
  185.                                 OCI_HTYPE_SESSION, 0, (dvoid **) 0);
  186.     // set up user & password for our session
  187.     retCode = OCIAttrSet ((dvoid *)session, OCI_HTYPE_SESSION,
  188.             (void*)db_user_name,(ub4)strlen((const char*)db_user_name),
  189.             OCI_ATTR_USERNAME, error);
  190.     retCode = OCIAttrSet ((dvoid *)session, OCI_HTYPE_SESSION,
  191.             (void*)db_password,(ub4)strlen((const char*)db_password),
  192.             OCI_ATTR_PASSWORD, error);
  193.     // start session
  194.     retCode = OCISessionBegin (context, error, session,
  195.                                 OCI_CRED_RDBMS, OCI_DEFAULT);
  196.  
  197.     /* set the user session attribute in the service context handle*/
  198.     retCode = OCIAttrSet ((dvoid *)context, OCI_HTYPE_SVCCTX,
  199.                          (dvoid *)session, (ub4) 0,
  200.                          OCI_ATTR_SESSION, error);
  201.     OCIHandleAlloc((dvoid *) env, (dvoid **) &statement,
  202.                     OCI_HTYPE_STMT, (size_t) 0, (dvoid **) 0);
  203.     OCIStmtPrepare(statement, error, command, (ub4)strlen((char*)command),
  204.                     (ub4) OCI_NTV_SYNTAX, (ub4) OCI_DEFAULT);
  205.     return NGX_CONF_OK;
  206. }
* This source code was highlighted with Source Code Highlighter.


_________
Текст подготовлен в ХабраРедакторе
+33
17 сентября 2009, 22:54
42

комментарии (31)

0
ilder #
«Рис 1. Структура БД» битая ссылка. Поправьте если можно. Спасибо.
+3
SaveTheRbtz #
Отичное сравнение!
Одна заметка — в аналитике обычно принято учитывать не «Самый длительный запрос», а скорее «95% запросов выполнено в пределах ххх сек.»
0
catap #
Обычно люди берут данные и считают дисперсию случайной величины, и на основание этого дают проценты. А так, это голые «вумные» слова, которые только повышают энтропию. Увы.
+1
nblxa #
Интересно, насколько и в какую сторону изменится производительность, если заменить две крайние таблички (PAY_OPERATOR и PAY_ACCOUNT) материализованными представлениями FAST ON COMMIT, обновляемыми из таблицы PAY_TRANSACTION.

И вообще, с интересом жду продолжения игры в OLTP, как про код приложения, так и про сервер и БД.
–1
spanasik #
Спасибо!
Ещё одна статья, в которую можно тыкать носом тех, кто говорит что lighttpd лучше нашего nginx :-)
+1
lolipop #
скажем нет фанатизму? :)
0
spanasik #
Причём тут фанатизм? Достаточно посмотреть результаты тестов. Скорее, фанатизм — это утверждать в такой ситуации, что всё примерно одинаково хорошо работает, а на самом деле это не так. Фанатизм толерантности ничего общего с инженерией не имеет, так что не в тему.
0
EXSlim #
Попробуйте погонять lighttpd2.0~sandbox.. Ребята говорят что он еще быстрее, а стабильную версию следует ожидать примерно в конце года.
0
vikds #
Спасибо за ценный совет!
Обязательно поэксперементируем… руки чешутся. =)
0
superhabra #
Кода модуля нету?
А как на счёт масштабируемости решения на основе модуля веб-сервера?
0
vikds #
Масштабируемость осложняется за счет совместной компиляции модулей с nginx'ом. А обновления сервера происходят достаточно часто =)
Попробуем засунуть Business layer в динамическую библиотеку. При усложнении транзакций, как написано в конце статьи, будем использовать неблокирующие запросы к БД, чтобы не тормозить цикл мультиплексора.
0
alrond #
В статье непонятно что за «nginx module»
0
vikds #
updated
0
WebByte #
Чё ж у вас такой ораклячий код страшный?
0
vikds #
Для примера в UPD привели по-настоящему страшный код на OCI. Тут мы и сами боимся… =)
В окончательном варианте, конечно, спрячем во wrapper.
+2
WebByte #
Мой вам совет:
1. Уберите каунт — лучше обойтись экзистом или вообще вывалиться по no_data_found
2. Откажитесь от явного объявления курсоров.
3. Коммиты скидывайте пачками
4. Вместо установки флагов, лучше пользовать exception
0
vikds #
Большое Вам спасибо за советы!
Признаюсь действительно был не прав в некоторых местах:

1. Тут действительно лучше подошло бы лучше вроде:

  1.  select count(*) into l_cnt
  2.     from dual
  3.   where exists ( select NULL
  4.                     from emp
  5.                   where sal > 4000 );
* This source code was highlighted with Source Code Highlighter.
или
  1. begin
  2.  select id
  3.   from emp
  4.   where sal>4000 and rownum <= 1
  5. exception
  6.   when no_data_found then
  7.      raise_application_error(-20010, 'Not found');
  8. end;
* This source code was highlighted with Source Code Highlighter.
Tom Kyte так же советует вариант через for.

2. Неявные курсоры конечно лучше, но я ими воспользовался, чтобы одновременно залочить строчки FOR UPDATE, посмотреть есть ли в них данные (чтобы сообщить об этом клиенту) и по ним же обновиться. Так делают (ниже в комментах). Конструкция:
  1. select id
  2.  into l_var
  3.  from anytable
  4. where id = 123
  5.   for update;
* This source code was highlighted with Source Code Highlighter.
не скомпилируется.

3. Пачками не коммитили, чтобы БД нагрузить и фиксировать все выполненые транзакции (вдруг БД упадет)

4. Excetion действительно лучше. My fault. Какая-то привычка из С++ сыграла, там иногда пытаются избегать exception т.к. они занижают производительность и их применять стараются только в случае явных ошибок программ.
0
catap #
А у вас ваш модуль не блокируется? А каким таким волшебным образом?
0
vikds #
Непосредственно в данных примерах модуль тоже блокируется синхронными вызовами к БД.

Асинхронные вызовы на OCI мы планируем использовать в дальнейших опытах.
0
catap #
Собственно в чем вопрос:

вам придется расковыривать OCI и переносить из нее точку синхроноизации в epoll_wait в nginx. Да, я верю что это возможно, но есть подозрение что придется этот OCI переделать.

И плохо что вы не написали что это только прототип. И в жизни это использовать нельзя.
0
vikds #
вам придется расковыривать OCI и переносить из нее точку синхроноизации в epoll_wait в nginx. Да, я верю что это возможно, но есть подозрение что придется этот OCI переделать.
Расковырять OCI не получится, Oracle не раскрывает исходники. Если альтернативных вариантов не будет, то «допилить» попробуем nginx, благо код очень осмысленный и отлично читается. За наводку на epoll_wait спасибо!

И плохо что вы не написали что это только прототип. И в жизни это использовать нельзя.
Эта мысль подается и в заголовке, и в тексте. Все вышенаписаное — лишь эксперимент.
Претендовать на best practices по nginx даже и в мыслях не было.
0
catap #
Расковырять OCI не получится, Oracle не раскрывает исходники. Если альтернативных вариантов не будет, то «допилить» попробуем nginx, благо код очень осмысленный и отлично читается. За наводку на epoll_wait спасибо!


С этим понятно, но мне не понятно как вы хотите и что пилить? Внести точку синхронизации из nginx в OCI? Возможно, но OCI закрыт.

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

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

Кирилл спасибо за развернутый ответ.
0
catap #
А глупо делать регулировангие timeout. Вы просто будете часто получать состояние что воркеры ждут, пока OCI ответит. А клиенты будут сидеть в backlog и видеть waiting site.com в своих браузерах.
–1
catap #
Далее, а сколько у вас было worker процессов у fastcgi сервера? А если их сделать штук 100? А если написать на тредах?

Сейчас вы имеете, фактически, демонстрацию понятной проблемы — в асинхронном сервере (nginx/fastcgi) кто-то заблокировался. То, что overhead на блокировку если внести ее в nginx меньше, понятно. Только в чем тут фикус, новшество и зачем вообще, кроме письку показать, стоило писать сей текст?

Делать так в контексте высокой нагрузки нельзя. А зачем так извращаться в других случая — я не понимаю.
+1
vikds #
Подобных «а если...» очень много, но ведь с чего-то нужно начинать. Статья затевалась ради того, чтобы услышать конструктивную критику. В открытом доступе, т.к. промежуточные результаты вполне могут оказаться полезными тем, кто не близко знаком с nginx, lighttpd. Уже немало поднималось вопросов, что бы выбрать и как реализовать (модуль, fastcgi) highload?

Спасибо за Ваш профессиональный и конструктивный подход.
0
catap #
Увы, но вы не дали ответы. И даже не обозначили вопросы (не сказали про самую важную проблему). Вы просто сделали не верные выводы не до конца поняв их причину. Увы.
+1
akalend #
не надо эйфории

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

во первых — тяжелеет сам nginx
во вторых — что будет если соединение с Oracle отвалится?
грохнется весь сервер, по умолчанию в ОС время коннекта 60 сек…
При нагрузках — это будет пилипец всему и сразу…

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

Как делать постоянный коннект и следить за ним, чтоб не отвалился, я пока не знаю.
Мутить пуул коннекций, сложновато.
Мутить обмен через shm & libevent? а отдельным клиентом общаться с БД и отдавать данные через shm…

тут есть много над чем подумать.
0
vikds #
Большое Вам спасибо за развернутый комментарий!

С удовольствием над этим подумаем. =)
С момента написания статьи многие вещи в построении Transport level OLTP уже пересмотрели.

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

Будем изучать дальше.

Так же любопытно попробовать обезопаситься от «отваливания» Oracle через ихнюю же технологию TAF (Transparent Application Failover), чтобы при «смерти» одного узла (почти) мгновенно он переназначил Virtual IP другому и тот ответил, что все плохо. TCP timeout будет незначительным. Только для таких экспериментов наверное нужно будет поднять среду Oracle RAC. =)

А идея про Connection pool звучит очень заманчиво!
+1
akalend #
>А идея про Connection pool звучит очень заманчиво!
я давно думаю над пуулом (MySQL + PHP ), но я не спец по Ораклу и ничего сказать не могу.

из моих исследований по MySQL, хотел сделать демон, который бы раздавал коннекции РНР-вокерам через shmem
но наткнутся на sigfault, для ликвидации которого надо было переписать более 33% кода mysqlclientlib Сизыфов труд.

как вариант — сделать демона, который запускал бы n MySQLClient вокеров, которые постоянно держали бы коннекцию с мускулем, но в этом случае при обмене из PHP, мы клали бы в shmem запрос, оповещали демона и ждали бы оповещения выполнения запроса, после этого брали данные из shm, формировали массив и возвращали в РНР код. т.е. по сути написал бы свою новую либу, а хотелось совместимости.

В общем, история про пул — это на какие грабли мне пришлось наступать и куда копать дальше.

Вам тоже спасибо, давно искал сравнительные характеристики nginx & lighttpd (про lighttpd2.0 узнал из комментов этого блога)

посмотри в сторону использования шаред мемори и libevent
это должно быть быстрее чем fastcgi
(будет время, сам покапаю в эту сторону)
мне тоже нужно будет запускать не легкие процессы…
0
flyaway #
Очень интересно.
И как идут дела с модулем nginx для работы с oracle?

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