company_banner

Опыт внедрения Tarantool в сервисе Calltouch

    image


    В современном мире информационных технологий у всех — и у крупных, и у небольших компаний — существует большое количество различных API. И отказоустойчивость, несмотря на многие best practices, чаще всего не позволяет гарантировать 100%-й возможности корректно обрабатывать запросы клиентов, а также восстанавливаться после сбоя и продолжать обработку запросов, утерянных из-за сбоя. Эта проблема возникает даже у больших игроков в интернете, не говоря уже о не очень крупных компаниях.


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


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


    Предыстория


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


    1. Объём памяти, необходимый для сервиса, может требовать нового оборудования.
    2. Стоимость оборудования сейчас высока.
    3. От ошибки с killer request никто не застрахован.

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


    Исследование


    Было несколько вариантов, как хранить входящие данные.


    Первый вариант


    Сохранять запросы с данными, используя логи nginx, и складывать в какое-то место. Если возникнут проблемы, сервис API обратится к данным, которые где-то сохранены, и после сделает необходимую обработку.


    Второй вариант


    Сделать дублирование запросов HTTP в несколько мест. Плюс написать дополнительный сервис, который будет куда-то складывать данные.


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


    Реализация


    Мы выбирали между nginx, goreplay и lwan.


    Первым отпал lwan, так как goreplay умеет сразу всё, что нам надо. Осталось лишь выбрать nginx с @post_action или goreplay. Goreplay был эталоном для этой схемы, но мы решили остановиться и поразмыслить о запросах: где и как их лучше хранить.


    О хранении можно было особо не задумываться до определённого момента. Нам потребовалась обратная связь между уже обработанными и ещё не обработанными данными. В API, для которого мы делаем дублирование запросов, не предусматривались ID в запросах с клиентской стороны. И возникла такая ситуация: понадобилась возможность подставлять дополнительные данные во входящий запрос. Это позволило бы получить обратную связь между обработанными и необработанными данными, ведь в базу попадут все данные, а не только необработанные. Затем каким-то образом нужно разбираться со всеми входящими данными.


    Чтобы разделаться с ID запросов, мы решили добавлять на стороне веб-сервера заголовок с UUID и проксировать такие запросы к API — чтобы сервис API после обработки изменял/удалял те запросы, которые мы дублируем в базе данных. На этом моменте мы отказываемся и от goreplay в пользу nginx, так как nginx поддерживает множество модулей, включая возможность записи в различные базы данных. Это позволит упростить схему обработки данных и уменьшить количество вспомогательных сервисов при решении данной технической задачи. Не придётся тратить время на изучение дополнительных языков и дорабатывать goreplay для соответствия требованиям.


    Наиболее простым вариантом было бы взять модуль для nginx, который может писать в какую-нибудь базу данных всё содержимое входящих запросов. Дополнительного кода и программирования в конфигах не очень бы хотелось. Самым гибким и подходящим для нас оказался модуль для Tarantool, им можно проксировать все данные в Tarantool без каких-либо дополнительных действий.


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


    image


    Для этого нам требуется nginx с набором модулей и Tarantool.


    Дополнительные модули к nginx:



    Пример конфигурации апстрима в nginx для работы с Tarantool:


    upstream tnt {
        server 127.0.0.1:3301 max_fails=1 fail_timeout=1s;
        keepalive 10;
    }

    Конфигурация проксирования данных в Tarantool с использованием post_action:


    location @send_to_tnt {
        tnt_method http_handler;
    
        tnt_http_rest_methods all;
        tnt_pass_http_request on pass_body parse_args pass_headers_out;
    
        tnt_pass tnt;
    }
    
    location / {
        uuid4 $req_uuid;
        proxy_set_header x-request-uuid $req_uuid;
    
        add_header x-request-uuid $req_uuid always;
    
        proxy_pass http://127.0.0.1:8080/;
        post_action @send_to_tnt;
    }

    Пример процедуры в Tarantool, которая принимает входящие данные из nginx:


    box.cfg {
        log_level = 5;
        listen = 3301;
    }
    
    log  = require('log')
    
    box.once('grant', function()
        box.schema.user.grant('guest', 'read,write,execute', 'universe')
        box.schema.create_space('example')
    end)
    
    function http_handler(req)
        local headers = req.headers
        local body    = req.body
    
        if not body then
            log.error('no data')
            return false
        end
        if not headers['x-request-uuid'] then
            log.error('header x-request-uuid not found')
            return false
        end
    
        local s, e = pcall(box.space.example.insert,
            box.space.example, {headers['x-request-uuid'], body})
    
        if not s then
            log.error('can not insert error:\n%s', e)
        return false
        end
        return true
    end

    Решением можно считать небольшой размер кода на Lua и довольно простую конфигурацию nginx. Часть с API здесь учитывать нет смысла, именно её необходимо делать в любом варианте реализации. Можно легко расширить эту схему master-master репликацией в Tarantool и сделать балансировку нагрузки на несколько нод, используя nginx или twemproxy.


    Поскольку post_action отправляет данные в Tarantool на несколько миллисекунд позже, чем запрос приходит в API и обрабатывается, в схеме есть один нюанс. Если API работает так же быстро, как Calltouch, то придётся сделать несколько запросов на удаление или тайм-аут перед запросом в Tarantool. Мы выбрали несколько запросов вместо тайм-аутов, чтобы наши сервисы работали без задержек, так же быстро, как и ранее.


    Заключение


    В заключение можно добавить тот факт, что всего лишь nginx и модуль nginx_upstream_module совместно с Tarantool позволяют достичь невероятной гибкости и простоты в работе с http-запросами, высокой скорости доступа к данным без нарушения работы основных сервисов, значительных перемен при внедрении в существующую инфраструктуру. Охват задач — от создания сложных статистик до обычного сохранения запросов. Не говоря о том, что можно использовать как обычный веб-сервис и реализовать API на базе данного модуля для nginx и Tarantool.


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

    Метки:
    Mail.Ru Group 826,94
    Строим Интернет
    Поделиться публикацией
    Похожие публикации
    Комментарии 15
    • 0

      Только мне кажется, что для решения описанной задачи предназначены Message Queues​?

      • +1
        С ними реализация будет в разы сложней (нужны будут дополнительные демоны и т.п.) плюс Message Queues это все же не БД.
        • 0

          В приведённом решении тоже использован дополнительный демон Tarantool, и складываются туда, как я понял из статьи, достаточно сырые данные. Свойства БД в этом решении нужны в момент удаления данных, чтобы, грубо говоря, DELETE WHEN guid=%1 выполнялся достаточно быстро.

          • +1
            Ну непропорционально — это мой тезис.
            С очередью будет выглядит так: демоны который читает и пишет (возможно это сам nginx), отдельный демон очереди. Итого — большая система получается, плюс очередь не != БД, но это ты и сам заметил :)
            • 0

              См. моё описание похожего, кмк, кейса ниже.

        • +3
          Запилите то же самое через Message Queues, выложите в продакшн, пустите нагрузку от реальных пользователей. И дальше мы с удовольствием почитаем на Хабре про ваш опыт.
          • 0

            Я, может быть, не до конца понял суть задачи; статья всё же написана немного суховато. Я решал, как мне кажется, похожую задачу: надо было по https получать запрос к пикселю-счётчику, расположенному на страницах многих сайтов, и складывать в Cassandra (потом её поменяли на ClickHouse). Запросов могло быть много, данные было очень важно не терять, загрузке страниц этот пиксель должен был не мешать по минимуму. Похоже на ваш кейс?


            В итоге была такая конфигурация: за nginx стоял php-fpm, под ним выполнялся скрипт из буквально 15 строк, который брал $_REQUEST, проверял что в cookie есть user-id, если нет — генерировал новый, устанавливал cookie, отправлял данные в RabbitMQ и завершался. Размер одного сообщения был типа 4 килобайт.


            По крону раз в минуту запускался скрипт уже под php-cli, забирал пачками данные из Rabbit и сливал в базу. Сам Rabbit крутился на той же виртуалке. Память fpm была ограничена каким-то смешным количеством, число процессов было типа 10 (как я понимаю сейчас — даже много). Умещалась вся конструкция в 200-рублёвую виртуалку на VScale, 100 rps из Танка выдерживала, 99% ответов умещались в 80ms. Вся работа уложилась в 2 человеко-дня, нагрузка от реальных пользователей, правда, выше 50 rps не поднималась. Собственно всё, на статью для Хабра не тянет, максимум на коммент)

            • 0
              Ну у тебя задача другая, а именно, хранить Хиты. А тут хранить консистентно запросы. У вас цели иные.
              Твою задачу можно решить проксированием каждого хита в ClickHouse прям из nginx, я бы сделал так :)
              • 0

                А uuid кто будет генерировать?)

                • +1
                  Данный вопрос возник, из-за того что Вы не читали данную статью. Но добавлю здесь описание, через nginx мы добавляем заголовок с UUID, а он в свою очередь создается используя этот модуль.
                  • 0

                    Я прочитал, но быстро забыл, виноват)

                    • 0

                      А нет, в смысле да) Прочитал, забыл, снова вспомнил: обработчик хита должен читать cookie и если в ней нет uuid — генерировать и выставлять новый, чтобы собирать хиты в сессии. nginx так умеет?

                • +3
                  Запросов могло быть много, данные было очень важно не терять

                  Из описания ClickHouse, в руководстве:
                  • транзакции отсутствуют;
                  • низкие требования к консистентности данных;

                  Похоже на ваш кейс?

                  Задачи похожи, но в нашем случае мы действительно ничего не теряем. Так как есть и репликация, и транзакции и балансировка нагрузки.
              • +1
                Основная задача состоит не в использовании какого-то подхода, а решение задачи в наиболее короткие сроки, и менее затратно. Можно сделать множество различных реализаций, но не всегда время и срок решения проблемы будет очевиден.
              • 0

                Я автор GoReplay, спасибо за упоминание :)


                Да, для вашей цели post_action пожалуй неплохое решение.


                Стоит добавить что задача добавления uuid в GoReplay решается довольно просто с помощью middleware https://github.com/buger/goreplay/tree/master/middleware


                Например такая NodeJS программа


                var uuidV4 = require('uuid/v4');
                var gor = require("goreplay_middleware");
                gor.init()
                
                gor.on('request', function(req) {
                   req.http = gor.setHttpHeader(req.http, "X-Request-ID", uuidV4()); 
                   return req;
                })

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


                Ну и конечно GoReplay также может записывать и ответы сервера, и при их проигрывании на другую среду, можно сравнивать ответы оригильные и новые.


                Как то так https://goreplay.org :)

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

                Самое читаемое