Использование Tarantool в .NET-проекте на Windows

в 8:04, , рубрики: .net, backend, C#, docker, nosql, tarantool, Visual Studio, базы данных, Блог компании Mail.Ru Group, разработка под windows

В последнее время на Хабре появляется достаточно много статей про Tarantool — базу данных и сервер приложений, который используется в Mail.Ru Group, Avito, Yota на разных высоконагруженных проектах. И вот, когда в маленьком стартапе, который я иногда консультирую, возникла необходимость разделения прекрасного, но, к сожалению, монолитного приложения на микросервисы, я подумал: а чем мы хуже других компаний? — и решил посмотреть в сторону Tarantool. Однако, в отличие от большинства компаний, где используется Tarantool, в нашем случае разработка проекта ведётся в Visual Studio на Windows. Предполагается, что даже с переходом на микросервисную архитектуру большинство микросервисов будет написано на языке C#. А Tarantool… Стоит зайти на официальный сайт — и сразу понимаешь: Tarantool даже установить на Windows проблематично, так как на эту операционную систему он не портирован. Как я боролся с такими сложностями, для какого именно микросервиса выбрал Tarantool и как вы можете использовать Tarantool в своих .NET-проектах, я расскажу в данной статье. А пока спойлер — практически все трудности преодолимы, и мой опыт можно без сомнений назвать положительным. Например, на то, чтобы скачать и запустить Tarantool, а потом сделать к нему запрос из кода на языке C#, у меня ушло менее десяти минут. И я покажу вам, как это сделать!

image

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

План

Использование Tarantool

Один из микросервисов, который наиболее важен практически для любого проекта (и создание которого меня волновало больше всего), — микросервис аутентификации и авторизации. И в первую очередь необходимо было определиться с тем, какие требования предъявляются к системе/базе данных, где будет храниться информация о пользователях. Требований не так уж много, но удовлетворяет им на самом деле очень небольшое число продуктов, доступных на рынке. Кроме того, всегда хочется обойтись минимальным количеством этих продуктов, чтобы упростить конфигурирование и поддержку. Поэтому сочетание MySQL + Redis + RabbitMQ или что-то в таком духе — это явно было бы перебором. Итак, вот наши базовые требования:

  • Данные о пользователях должны надёжно храниться на диске.
  • Должна поддерживаться репликация данных на несколько узлов. С одной виртуальной машиной говорить о каком-то приемлемом аптайме и надёжности вообще не приходится. Поэтому нужно принять как должное то, что мы уже много лет живём в мире распределённых систем, и отделаться одним сервером с MS SQL Server, как в далёком 2005 году, уже не получится.
  • Так как на каждый запрос в системе (включая запросы от пользователей и запросы к API) проверяются права доступа, нужна максимальная производительность чтения данных. Поэтому желательно иметь все данные в памяти.
  • Необходима поддержка вторичных индексов, с помощью которых будет возможно делать сложные запросы по списку пользователей. Например, быстро получить всех пользователей с рейтингом от 10 до 25 баллов — или что-то в таком духе.
  • Очень желательно, чтобы система предоставляла поддержку очередей, так как в большинстве проектов возникает потребность в таком механизме.

Хочется отметить, что по своим характеристикам Tarantool является очень удачным сочетанием in-memory базы данных для кеширования и полноценной NoSQL СУБД для постоянного надёжного хранения данных. А кроме того, он подходит под все наши требования, обеспечивая достойную производительность. Поэтому после сравнения по этим и другим характеристикам мы и сделали выбор в его пользу. Ну и кроме того, всегда приятно разделаться с задачей с помощью одного хорошего решения, а не настраивать целый зоопарк различных систем.

Важный момент, относящийся к микросервису авторизации и аутентификации. Обратите внимание на то, что нельзя хранить данные о пользователях и группах в какой-либо дисковой СУБД, а потом просто взять и положить эти данные в in-memory кеш. Так как кеш в таком случае должен иметь быстрый и надёжный механизм синхронизации с основной базой данных. Ведь нам не хочется, чтобы заблокированный пользователь мог продолжать работу только потому, что кеш и основная база данных ещё не синхронизировались. А новый пользователь не смог бы войти в систему по той же причине. Кроме того, мне как разработчику очень не хочется писать логику такой синхронизации самому: надежность решения сомнительна, а вероятность ошибок синхронизации, наоборот, велика. Поэтому то, что в Tarantool есть WAL (write-ahead log) и данные на диске и в памяти всегда синхронизированы, — просто огромное преимущество. Наверное, это одна из самых важных причин выбора Tarantool. Ну и производительность, конечно — ещё одна важная причина.

Итак, пришло время, собственно, поделиться с вами самим рассказом о том, как я запускал Tarantool на Windows. После этого я в формате туториала расскажу про работу с Tarantool из .NET-приложения.

Работаем с Tarantool на Windows

Когда Node.js только набирал популярность, его Windows-версии просто не существовало. И нам, разработчикам, сидящим на Windows, приходилось только ждать и кусать локти, ну, или переходить на Mac, что многие со временем и сделали. Сейчас же Microsoft понимает, что разработчики привыкли к окружению macOS и Linux, и переманить их обратно на Windows практически невозможно. Кроме того, даже если это и получится, разработчикам многого будет не хватать. Не знаю, так ли рассуждали в Microsoft или нет, но в Windows 10 появилась возможность запуска неизменённых исполняемых файлов из разных дистрибутивов Linux (сначала поддерживалась только Ubuntu, а теперь доступна и SuSe) без эмуляции или виртуализации — Windows Subsystem for Linux (WSL). Новая подсистема ядра Windows реализует вызовы ядра Linux, а механизм minimal/pico-процессов позволяет не загружать ntdll и не делать другие Windows-специфичные вещи. Данная функциональность появилась в Windows 10 Anniversary Update летом 2016 года. Но в этой версии слишком уж многое не работало, поэтому, если вы захотите запускать Tarantool таким образом, лучше воспользоваться последними сборками из Fast цикла Insider Preview или Windows 10 Creators Update, когда он станет доступен. Что я и сделал при написании данной статьи (я пользовался сборкой 15031). В сборке 15031 Tarantool хоть и работает с некоторыми ошибками, но ошибки некритичны и в целом он стабилен. В более ранних же сборках Windows были проблемы даже с запуском.

Естественно, когда мы говорим о Windows Subsystem for Linux, мы рассматриваем это исключительно для целей разработки. Так как на Windows Server в production такое решение работать не будет. Тут без нативной поддержки не обойтись. Поэтому в production мы используем Tarantool, работающий в Docker-контейнерах. Контейнеры же, в свою очередь, функционируют в Azure Container Service. Собственно, там же работают и наши ASP.NET Core микросервисы, поэтому никаких новых сущностей в проект внедрять не пришлось. Работа с Docker прекрасно поддерживается в Visual Studio 2017, что очень помогает при разработке. Но Docker тем и прекрасен, что мы без проблем можем всё перенести хоть на Digital Ocean, хоть на Amazon Lightsail. Привязки к облачному провайдеру нет.

Так как в production всё равно используется Docker, то и при разработке тоже лучше применять Docker. По моим впечатлениям, Docker на Windows ничем не хуже Docker’а на macOS. Естественно, при этом будет использоваться виртуализация (как и на macOS), но зато таким образом мы избавимся от множества проблем совместимости при развёртывании проекта. Ну и кроме того, для полноценной работы с Docker вам не потребуется последняя сборка Insider Preview или самая свежая версия Windows.

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

Tarantool в Windows Subsystem for Linux

Ну что же, пришло время за пять простых шагов запустить Tarantool на Windows 10 с помощью Windows Subsystem for Linux. У меня весь процесс занял примерно 15 минут. Но тут многое зависит от вашего железа и скорости соединения с интернетом. Также ещё раз хочу напомнить, что все эти манипуляции я проделывал на Windows 10 Insider Preview build 15031.

1. В меню Пуск выберите Settings -> Update and Security -> For developers и включите Developer mode, если он у вас ещё не включён.

image

2. После этого запустите PowerShell и выполните простую команду включения Windows Subsystem for Linux (> — приглашение командной строки вводить, естественно, не нужно):

> Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

image

Как и на предыдущем шаге, потребуется перезагрузка.

3. Откройте командную строку и выполните команду bash. Вам будет предложено скачать user-mode дистрибутив Ubuntu, создать Linux пользователя и задать ему пароль. После установки и настройки Ubuntu в меню Пуск появится тайл для запуска Bash on Ubuntu on Windows напрямую.

image

Ура! Теперь перед нами полноценный Bash, и мы можем запускать практически любой софт, доступный на Ubuntu, а главное — поддерживается команда apt-get. Грех этим не воспользоваться, давайте установим Tarantool.

4. Воспользуемся инструкцией по установке на Ubuntu, которая находится на официальном сайте:

https://tarantool.org/download.html

curl http://download.tarantool.org/tarantool/1.7/gpgkey | sudo apt-key add -
release=`lsb_release -c -s`

# install https download transport for APT
sudo apt-get -y install apt-transport-https

# append two lines to a list of source repositories
sudo rm -f /etc/apt/sources.list.d/*tarantool*.list
sudo tee /etc/apt/sources.list.d/tarantool_1_7.list <<- EOF
deb http://download.tarantool.org/tarantool/1.7/ubuntu/ $release main
deb-src http://download.tarantool.org/tarantool/1.7/ubuntu/ $release main
EOF

# install
sudo apt-get update
sudo apt-get -y install tarantool

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

5. Запустите Tarantool с помощью одноимённой команды:

> tarantool

Tarantool использует Lua в качестве встроенного языка. И вы увидите приглашение интерпретатора:

tarantool>

Отличительная особенность Lua: это очень простой язык, на изучение его основных возможностей хватает буквально 15 минут. На своём опыте могу сказать, что для понимания практически всего кода, который встречается в примерах по Tarantool, 15-минутного туториала действительно достаточно.

Итак, выполните команду tarantool, и перед вами откроется Lua-интерпретатор. Теперь сконфигурируем Tarantool и запустим его на порту 3311 с помощью основной библиотеки Tarantool под названием box.

tarantool> box.cfg({listen = 3311})

В данном случае, так как единственным параметром функции cfg выступает таблица с параметрами (таблицы в Lua очень похожи на объекты в JavaScript и не имеют ничего общего с таблицами в реляционных СУБД), можно опустить круглые скобки и написать код следующим образом:

tarantool> box.cfg{listen = 3311}

Теперь создадим space для хранения данных. Space’ы в Tarantool — это аналог коллекций в MongoDB, ну, или таблиц в других СУБД:

tarantool> box.schema.space.create('customers')

А также создадим первичный индекс по первому полю (ещё мы можем создавать вторичные и составные индексы):

tarantool> box.space.customers:create_index('primary', {type = 'tree', parts = {1, 'UNSIGNED'}})

Двоеточие в вызове customers:create_index необходимо для эмуляции объектной ориентированности и корректной передачи параметра self. Следующие два вызова будут эквивалентны (обратите внимание на точку в одном случае и двоеточие в другом):

tarantool> box.space.customers:select({})

и

tarantool> box.space.customers.select(box.space.customers,{})

Теперь вставим в space customers несколько записей. Одну с явным указанием первичного ключа, а другую — с помощью автоматического инкремента:

tarantool> box.space.customers:insert{1, 'Sergey', 'Moscow'}
tarantool> box.space.customers:auto_increment{'Ivan', 'San-Francisco'}

Теперь запросим все записи:

tarantool> box.space.customers:select{}

Перед нами в консоли в YAML-формате отобразились две только что добавленные записи.

image

Если вы только начинаете работать с Tarantool, то я рекомендую пройти туториал. Запускает туториал простая команда:

tarantool> tutorial()

Также на Хабре можно найти прекрасные статьи, описывающие архитектуру Tarantool, его возможности и то, как можно быстро начать с ним работать.

Итак, всего за пять простых шагов мы получили работающий Tarantool с помощью Windows Subsystem for Linux. Tarantool работает как процесс в Windows без использования виртуализации. Теперь пришло время разобраться, как мы можем добиться похожего результата с Docker.

Tarantool в Docker на Windows

Мне кажется, с точки зрения инфраструктуры Docker — самое лучшее, что случилось в индустрии за последнее десятилетие. Теперь можно попробовать практически любой продукт без длительной настройки с помощью простой команды docker run. И Tarantool тут не исключение. Поэтому установите Docker для Windows, если он у вас не установлен, зайдите в консоль и введите следующую команду:

> docker run --name mytarantool -d tarantool/tarantool:1.7

Если образ Tarantool 1.7 ещё не скачан, то он автоматически скачается, и новый контейнер, созданный из этого образа, будет запущен. Всё. Мы запустили Tarantool! Параметр -d (detached) говорит о том, что контейнер необходимо запустить в фоне. Поэтому всё, что вы увидите в консоли после выполнения команды, — это ID контейнера.

Теперь самое время подключиться к нашему новому экземпляру Tarantool через его собственную консоль:

> docker exec -it mytarantool console

Мы подключаемся к контейнеру с именем mytarantool и выполняем в нём команду console в интерактивном режиме. После этого откроется консоль Tarantool, и вы сможете выполнять в ней любые команды.

Как видите, после установки Docker для запуска Tarantool потребовалось выполнить одно действие, ещё одна команда нужна, чтобы подключиться к Tarantool. Это даже проще, чем в предыдущем примере с Windows Subsystem for Linux. Именно поэтому мне гораздо больше нравится вариант с Docker. Ну и кроме того, Tarantool в Docker работает в той же среде, в которой он будет работать в production.

image

Сейчас экземпляр Tarantool запущен и работает в Docker. И он даже где-то хранит данные, что уже неплохо. Проблема только в том, что мы не знаем, где именно данные хранятся. Нужно указать место на Windows-хосте, где Tarantool сохранял бы логи и snaphot’ы. Также надо сделать Tarantool доступным из Windows по определённому порту, например 3301. Всё это требуется, чтобы к нему можно было подключиться из нашего .NET-приложения по localhost:3301.

Но перед тем как это сделать, остановим и удалим наш существующий контейнер:

> docker stop mytarantool && docker rm mytarantool

Создадим папку C:tarantooldata, в которой будут размещаться логи и snaphot’ы нашего экземпляра Tarantool. Имя и месторасположение папки выбрано произвольно. Не забудьте только сделать диск, на котором находится папка, доступным для Docker: кликните на иконку Docker в системном трее и выберите в контекстном меню пункт Settings… В открывшемся диалоговом окне в разделе Shared Drives необходимо указать нужные диски.

image

Теперь запустим новый Docker-контейнер с Tarantool:

> docker run --name mytarantool -d -p 3301:3301 -v c:/tarantool/data:/var/lib/tarantool tarantool/tarantool:1.7

Мы связали папку C:tarantooldata на Windows с /var/lib/tarantool контейнера — местом, где (согласно настройкам контейнера) хранятся данные. После запуска контейнера можно увидеть, что в папке C:tarantooldata появились файл логов и snapshot.

image

Теперь Tarantool доступен из Windows по адресу localhost:3301. Мы можем обратиться к нему из нашего .NET-приложения (или любого другого). Единственная проблема — Tarantool на данный момент пуст. Создадим space’ы и при необходимости добавим в них какие-то данные. Также создадим пользователей и назначим им права. Вручную делать это в консоли — не самая лучшая идея, поэтому давайте напишем для этих целей скрипт инициализации на Lua. Детально создание полноценного Lua-скрипта инициализации мы рассмотрим в следующем разделе, а пока давайте сделаем минимальный скрипт, который дальше будем улучшать. Для этого создадим папку C:tarantoolapp и в ней файл app.init.lua. Детали конфигурации сейчас не очень важны. Вот минимальный Lua-скрипт, который нужно создать:

app.init.lua

#!/usr/bin/env tarantool

box.cfg
{
    pid_file = nil,
    background = false,
    log_level = 5
}

Также для запуска Docker-контейнера не хочется каждый раз вводить сложные команды. Поэтому давайте воспользуемся Docker Compose и пропишем всю конфигурацию в YAML файле docker-compose.yml, который поместим в C:tarantool.

docker-compose.yml

version: '2'

services:
  tarantool:
    container_name: mytarantool
    image: tarantool/tarantool:1.7
    command: tarantool /usr/local/share/tarantool/app.init.lua
    ports:
      - 3301:3301
    volumes:
      - c:/tarantool/app:/usr/local/share/tarantool
      - c:/tarantool/data:/var/lib/tarantool

Здесь мы создаём сервис tarantool с именем mytarantool из образа tarantool/tarantool:1.7. При этом в качестве аргумента при запуске передаём наш конфигурационный файл app.init.lua, а также делаем Tarantool доступным из Windows на порту 3301. Кроме того, мы связываем Windows-папки app и data c Docker-контейнером.

Теперь можно запустить Tarantool с помощью Docker Compose одной простой командой:

> docker-compose -f C:/tarantool/docker-compose.yml up -d

Остановить и удалить контейнер с Tarantool можно командой:

> docker-compose -f C:/tarantool/docker-compose.yml down

Если вы уже находитесь в папке с файлом docker-compose.yml, то путь к файлу можно не указывать:

> cd C:/tarantool/
> docker-compose up -d

Ну что же. Мы настроили запуск Tarantool с помощью одной команды docker-compose и создали простой скрипт инициализации. Сейчас структура папок выглядит следующим образом:

image

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

Создание скрипта инициализации

Наш скрипт инициализации будет делать следующее. Во-первых, создавать пользователя operator с паролем 123123. Этому пользователю предоставляются права на чтение, запись и выполнение. Во-вторых, создавать space users. А также три индекса: уникальный индекс на первое поле (поля считаются с единицы, а не с нуля), где будет храниться ID пользователя в виде GUID-строки, уникальный индекс на третье поле, где будет храниться логин, и неуникальный индекс на пятое поле, где будет храниться рейтинг пользователя в виде числа. Создание пользователей, space’а и индексов происходит в функции init. А в функции load_data мы добавим три записи в space’е users. Функция box.once позволяет выполнить функцию один раз для текущей базы данных, что мне кажется очень полезной возможностью. Далее приведён полный код app.init.lua:

app.init.lua

#!/usr/bin/env tarantool

local log = require('log')
local uuid = require('uuid')

local function init()
    box.schema.user.create('operator', {
        password = '123123', 
        if_not_exists = true
    })

    box.schema.user.grant('operator', 'read,write,execute', 
    'universe', nil, {
        if_not_exists = true
    })

    local users_space = box.schema.space.create('users', {
        if_not_exists = true
    })

    users_space:create_index('primary_id', {
        if_not_exists = true,
        type = 'HASH',
        unique = true,
        parts = {1, 'STRING'}
    })

    users_space:create_index('secondary_login', {
        if_not_exists = true,
        type = 'HASH',
        unique = true,
        parts = {3, 'STRING'}
    })

    users_space:create_index('secondary_rating', {
        if_not_exists = true,
        type = 'TREE',
        unique = false,
        parts = {5, 'INT'}
    })
end

local function load_data()
    local users_space = box.space.users

    users_space:insert{uuid.str(), 
    'Ivan Ivanov', 'ivanov', 
    'iivanov@domain.com', 10}

    users_space:insert{uuid.str(), 
    'Petr Petrov', 'petrov', 
    'ppetrov@domain.com', 15}

    users_space:insert{uuid.str(), 
    'Vasily Sidorov', 'sidorov', 
    'vsidorov@domain.com', 20}
end

box.cfg
{
    pid_file = nil,
    background = false,
    log_level = 5
}

box.once('init', init)
box.once('load_data', load_data)

Внесите изменения в файл app.init.lua и перезапустите Docker-контейнер с Tarantool-командой:

> docker-compose restart

Теперь у нас есть полноценно работающий Tarantool с данными, с которыми мы можем работать из .NET-приложения.

Работа с Tarantool в .NET/C#

Самой популярной библиотекой для работы с Tarantool в .NET-приложениях является библиотека ProGaudi/tarantool-csharp, работу с ней мы и рассмотрим в данном разделе.

В репозитории данной библиотеки на GitHub есть прекрасный пример ASP.NET Core приложения, использующего Docker и работающего с Tarantool. В этом примере вы увидите уже знакомую нам настройку Tarantool с помощью Docker Compose. Я рекомендую скачать и посмотреть пример, ну а сейчас мы рассмотрим более простое консольное приложение.

Весь код и проекты, рассматриваемые в данной статье, можно найти на GitHub.

Создайте новое консольное приложение в Visual Studio и подключите tarantool-csharp с помощью NuGet. Для этого в контекстном меню проекта в окне Solution Explorer выберите Manage NuGet packages… Далее найдите и установите пакет Tarantool.CSharp.

image

Выборка данных

Теперь пришло время первого запроса к Tarantool из C# кода. Запрос будем делать к Tarantool, который мы в предыдущем разделе запустили в Docker-контейнере и сконфигурировали с помощью скрипта app.init.lua. В скрипте мы создали пользователя operator с паролем 123123. Ниже приведён код подключения к Tarantool, получения space’а users и первичного индекса primary_id, а также выборки всех записей по этому индексу.

using System;
using System.Threading.Tasks;
using ProGaudi.Tarantool.Client;
using ProGaudi.Tarantool.Client.Model;
using ProGaudi.Tarantool.Client.Model.Enums;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            DoWork().Wait();
        }

        static async Task DoWork()
        {
            using (var box = await Box.Connect(
                "operator:123123@localhost:3301"))
            {
                var schema = box.GetSchema();

                var space = await schema.GetSpace("users");
                var primaryIndex = await space.GetIndex("primary_id");

                var data = await primaryIndex.Select<TarantoolTuple<string>,
                    TarantoolTuple<string, string, string, string, long>>(
                    TarantoolTuple.Create(String.Empty), new SelectOptions
                    {
                        Iterator = Iterator.All
                    });

                foreach (var item in data.Data)
                {
                    Console.WriteLine(item);
                }
            }
        }
    }
}

Код по большей части говорит сам за себя. Хотелось бы только остановиться подробнее на функции Select. Для этой функции необходимо задать тип ключа, по которому будет происходить выборка, а также тип возвращаемого значения. Так как у нас идентификаторами выступают GUID’ы, хранящиеся в строках, то в качестве типа ключа необходимо указать TarantoolTuple<string>.

Данные о пользователе содержат его ID, имя, логин, email и рейтинг: четыре текстовых поля и одно числовое. Поэтому в качестве типа возвращаемого значения указывается TarantoolTuple<string, string, string, string, long>.

С указанием нужных типов функция Select выглядит следующим образом:

primaryIndex.Select<TarantoolTuple<string>,
TarantoolTuple<string, string, string, string, long>>(...)

Далее функции Select передаются аргументы. В качестве первого аргумента выступает тупл, содержащий пустую строку: TarantoolTuple.Create(String.Empty). Здесь мы могли бы передать ID конкретного пользователя, если бы требовалось сделать выборку по пользователю. Но с пустой строкой в качестве ключа мы можем получить все записи сразу. То, что нам нужно получить все записи, мы также указываем с помощью параметра типа SelectOptions, в котором задаём в качестве итератора Iterator.All.

В данном примере создаётся новое подключение к Tarantool при каждом вызове функции DoWork. На самом деле необязательно создавать новое соединение на каждый запрос, одно соединение вполне можно переиспользовать.

Запустите приложение и проверьте, что все пользователи из space’а users выводятся.

image

Добавление данных

Теперь добавим новую запись в space users. Для добавления записи мы создадим тупл, состоящий из пяти полей. В качестве значения первого поля генерируем новый GUID.

await space.Insert(TarantoolTuple.Create(Guid.NewGuid().ToString(), 
"Vladimir Vladimirov", "vvladimirov", "vvladimirov@domsin.com", 10L));

Обновление данных

Обновление выглядит почти так же, как и выборка. Более того, Update возвращает обновлённое значение, что делает его ещё более похожим на выборку. В коде ниже мы присваиваем пользователю с заданным ID новое значение рейтинга.

var updatedData = await space.Update<TarantoolTuple<string>,
TarantoolTuple<string, string, string, string, long>>(
TarantoolTuple.Create("4e574d2f-1c82-4e14-aba8-95c6412e357c"),
new UpdateOperation[]{ UpdateOperation.CreateAssign<long>(4, 47L) });

Выборка данных по вторичному ключу

Кроме первичного ключа, мы можем делать запросы и по вторичным ключам. В примере ниже показывается поиск записи со значением petrov (то есть поиск в данном случае происходит по полю логина) по вторичному индексу secondary_login:

var loginIndex = await space.GetIndex("secondary_login");
var users = await loginIndex.Select<TarantoolTuple<string>,
    TarantoolTuple<string, string, string, string, long>>(
    TarantoolTuple.Create("petrov"));
var petrov = users.Data;

Выполним поиск всех пользователей, рейтинг которых больше или равен (Ge — Greater or Equal) пятнадцати:

var ratingIndex = await space.GetIndex("secondary_rating");
var ratingUsers = await ratingIndex.Select<TarantoolTuple<long>,
    TarantoolTuple<string, string, string, string, long>>(
    TarantoolTuple.Create(15L), new SelectOptions
    {
        Iterator = Iterator.Ge
    });

Вызов Lua-функций

Кроме манипуляций с данными из C#-кода с помощью функций Select, Insert, Update и других, часто возникает необходимость выполнить обработку данных как можно ближе к ним самим. Самое близкое к данным место — это сервер Tarantool. Давайте напишем функцию на Lua, которая будет инкрементировать рейтинг всех пользователей прямо на сервере. Назовем её update_rating. Эта функция состоит всего из нескольких строк. Вначале она получает итератор по space’у users с помощью вызова функции pairs(). А дальше производит обход итератора. Каждая запись обновляется с увеличением значения рейтинга на единицу. Далее приведён код функции. Добавьте его в конец файла app.init.lua и перезапустите Docker-контейнер с Tarantool. Конечно, перезапуск контейнера — не лучшее решение на production, но при разработке и отладке это самый быстрый вариант.

app.init.lua

function update_rating()
    for k,v in box.space.users:pairs() do
        box.space.users:update(v[1], {{'+', 5, 1}}) 
    end
end

Теперь вызовем Lua-функцию update_rating из C#:

Program.cs

await box.Call("update_rating");

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

Возможность писать функции на Lua — одно из существенных преимуществ Tarantool. Такие функции могут производить сложную обработку данных без необходимости их передачи между Tarantool и клиентским приложением. Кроме того, так как Tarantool ещё и сервер приложений сам по себе, можно писать микросервисы, работающие исключительно в Tarantool без необходимости стороннего кода на C#, Java, Go, PHP или других языках. Такие микросервисы обычно предоставляют REST API, который также может быть реализован исключительно с помощью кода на Lua.

Мы рассмотрели основные способы взаимодействия с Tarantool из .NET-приложения, далее я расскажу о впечатлениях от использования ProGaudi/tarantool-csharp.

Впечатления от ProGaudi/tarantool-csharp

У меня сложилось двоякое впечатление от использования ProGaudi/tarantool-csharp. С одной стороны, библиотека позволяет выполнить почти все необходимые запросы, которые требуются обычному приложению. А когда что-то не поддерживается, можно написать функцию на Lua, которую потом легко вызвать из C#-кода. С другой стороны, библиотека, конечно, слишком низкоуровневая. Требуется писать много шаблонного вспомогательного кода. И ощущается явный недостаток хорошего ORM (Object-relational mapper), чтобы можно было писать код не в терминах туплов, а в терминах классов предметной области. Поэтому я сам решил подумать над тем, как бы такой ORM для Tarantool мог выглядеть. Идея пока в зачаточном состоянии, предложения и пожелания приветствуются. А мне бы очень хотелось сделать что-нибудь типа такого:

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Login { get; set; }
    public string Email { get; set; }
    public long Rating { get; set; }
}

private static async Task DoWork()
{
    var box = (await Box.Connect(
   "operator:123123@localhost:3301")).Wrap();

    var space = box.Space("users");
    var primaryIndex = space.Index("primary_id");
    var ratingIndex = space.Index("secondary_rating");

    var all = await primaryIndex.Select<User>();
    var user = await primaryIndex.Select<User>("f73ee542-7d3e-4dec-b7c6-ce5dc7a02920");

    var top = await ratingIndex.Select<User>(20, Iterator.Ge);
    var low = await ratingIndex.Select<User>(20, Iterator.Lt);     
}

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

Создание REST-сервисов в Tarantool

При работе с Tarantool часто возникает ощущение, что для решения многих задач достаточно возможностей самого Tarantool и нет необходимости, например, в дополнительном коде на C#. Возьмём хотя бы микросервис, описанный в этой статье. Требуется, чтобы он предоставлял REST API, который будут вызывать другие микросервисы. Изначально предполагалось, что в качестве микросервиса должно выступать ASP.NET Core приложение, использующее возможности ASP.NET Web API. А уже код самого приложения будет обращаться к Tarantool. Но, как оказалось, ASP.NET Core приложение не делает практически ничего, кроме передачи данных между пользователями микросервиса и Tarantool. Поэтому можно пойти по пути, когда Tarantool непосредственно предоставляет REST API пользователям микросервиса. Давайте посмотрим, как это можно сделать.

В нашем примере REST-сервис будет поддерживать только один GET-запрос — /users. Также мы создадим красивую главную страницу, на которой отобразим список всех пользователей. Вообще хочется отметить, что с Tarantool очень удобно делать админки для микросервисов.

REST-сервис будет доступен на порту 8080. Поэтому нам нужно сделать доступным из Windows порт 8080 Docker-контейнера. Добавьте связывание этого порта в docker-compose.yml:

docker-compose.yml

ports:
  - 3301:3301
  - 8080:8080

Теперь воспользуемся модулем Tarantool http.server, который уже доступен в нашем Docker-контейнере, и запустим сервер. Для этого в файл app.init.lua добавьте следующий код:

app.init.lua

local function users_handler(self)
    local data = { 
        users = {},
        count = box.space.users:len()
    }

    for k, v in box.space.users:pairs{} do
        table.insert(data.users, {
                id = v[1], 
                user = v[2] .. ' (' .. v[3] .. ')' , 
                rating = v[5]
        })
    end

    return self:render{ json = data }
end

local httpd = require('http.server')
local server = httpd.new(nil, 8080, {
        app_dir = '/usr/local/share/tarantool/'
    })

server:route({ path = '/', file = 'index.html.el'  })
server:route({ path = '/users'  }, users_handler)
server:start()

Здесь мы определяем два пути: корень сайта и /users. Для корня сайта будет отдаваться файл index.html.el (el — embedded Lua.), а для /users вызывается функция users_handler. В этой функции генерируются данные — количество записей в space users и массив пользователей, содержащий ID пользователя, его имя и в скобках логин, а также рейтинг. Далее данные отдаются в формате JSON. Отдаваемый JSON выглядит следующим образом:

{
   "count":3,
   "users":[
      {
         "user":"Ivan Ivanov (ivanov)",
         "rating":10,
         "id":"c371b2b1-5090-450b-8a5d-7dba87ad5116"
      },
      {
         "user":"Petr Petrov (petrov)",
         "rating":15,
         "id":"b4f80aad-f3c2-42b0-8088-84846aef9997"
      },
      {
         "user":"Vasily Sidorov (sidorov)",
         "rating":20,
         "id":"07c82a50-4d8c-489a-9cc1-b4fdd95afbd8"
      }
   ]
}

Теперь создайте файл index.html.el в новой папке C:tarantoolapptemplates. Структура папок должна выглядеть следующим образом:

image

На странице index.html.el сделаем запрос к REST API пользователей и выведем их в таблицу.

index.html.el

<!DOCTYPE html>
<html lang="en">
    <head>
      <title>Users Dahsboard</title>

    <link rel="stylesheet"    
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    </head>
    <body>
        <div class="container">
            <div class="page-header">
                <h1>Users Dasbboard</h1>
            </div>
            <table class="table" id="tblUsers">
            <thead>
                <tr>
                    <th>#</th>
                    <th>Id</th>
                    <th>User</th>
                    <th>Rating</th>
                </tr>
            </thead>
            </table>
        </div>
        <script>
        fetch('/users').then(resp => {
            resp.json().then(data => {

                function createCell(tr, txt){
                    tr.insertCell().appendChild(
                    document.createTextNode(txt));
                    }

                let tblUsers = document.getElementById('tblUsers');

                for(let i=0;i<data.users.length;i++){
                    var tr = tblUsers.insertRow();
                    createCell(tr, i + 1);
                    createCell(tr, data.users[i].id);
                    createCell(tr, data.users[i].user);
                    createCell(tr, data.users[i].rating);                                               
                }
            });
        });  
        </script>
    </body>
</html>

Комментировать подробно JavaScript-код не буду, так как это выходит за рамки статьи. Отмечу только, что для выполнения запросов тут используется Fetch API.

Всё, REST API для нашего микросервиса готов. Теперь можно перезапустить контейнер и получить JSON с пользователями. Например, с помощью Postman.

image

Также зайдите на главную страницу. Перед вами откроется созданная нами админка.

image

Итоги

Как я уже писал в начале, мой опыт использования Tarantool оказался весьма положительным. Tarantool — зрелый продукт, и с ним вполне можно иметь дело, в том числе и Windows-разработчикам. Больше всего мне понравилось то, что Tarantool обладает возможностями сразу нескольких продуктов. И для ряда задач кроме него, по сути, ничего устанавливать и поддерживать больше не нужно. Что определённо не может не радовать.

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

С другой стороны, работать из C# с Tarantool не так просто, как хотелось бы. Но эту проблему я планирую решить написанием полноценного ORM.

Весь код, рассмотренный в статье, доступен на GitHub.

Спасибо!

Автор: Mail.Ru Group

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js