--- tags: Tarantool, OTUS, trainings, practice title: OTUS — Практика «Сокращатель ссылок» --- План https://hackmd.io/@Mons/OTUS-Practice-Plan # OTUS — Сервис сокращения ссылок [TOC] ## ТЗ - Пользователь хотел бы сократить ссылку: - Чтобы, например, вместить её в смс или в твиттер - Или даже расположить в печати, из которой переходы могут происходить только «вручную» ## Общий план - Настроить хранилище `Tarantool` - Смасштабировать его - Запустить простейший `http` сервер - *Соединить их вместе и все готово:)* ## Инструмент - Установка в Centos 8 ```bash= sudo dnf install tarantool tarantool-devel ``` - Установка в других ОС https://www.tarantool.io/en/download/os-installation/ ## База данных и сервер приложений `Tarantool` - Создание приложений на языке `Lua` с компиляцией «на лету» (`LuaJIT`) - `ACID` движок базы данных c возможностью репликации между двумя и более инстансами - Обе части в одном процессе # День 1 — Архитектура - Сегодня вы скачаете и запустите `Tarantool` - Создадите спейс - Создадите тапл - Создадите индексы - Вставите и достанете таплы из спейса ## API - Настройка базы данных происходит на языке `Lua` - `box.cfg` — первоначальная настройка базы данных - Перенастройка базы тоже происходит в этой функции, но не все параметры при этом можно менять на лету - `listen` — параметр, на каком порту слушать подключения - Например ```lua box.cfg{} ``` - `box.schema.space.create(<name>)` — создание спейсов, так в `Tarantool` называются таблицы - Например, создать спейс `experience`, если она ещё не была создана ```lua box.schema.space.create('experience', {if_not_exists=true}) ``` - Таблицы в `Tarantool` называются спейсами - После создания доступ к таблице происходит через функции `box.space.<table_name>` или `box.space['<table_name>']` - `box.space.<table_name>:format({fields, ....})` — функция для указания какие колонки будут в таблице - Например ```lua box.space.experience:format({ {name='date', type='unsigned'}, {name='event', type='string'}, {name='effect', type='string'}}) ``` - `box.space.redirector:create_index` — создание индексов функцией самой таблицы - Первый созданный индекс является первичным - И он должен быть уникальным - Все остальные являются вторичными - Любой индекс может состоять из нескольких колонок - Например, первичный ```lua box.space.experience:create_index('primary', {parts={ {field='date', type='unsigned'} }}) ``` - Например, вторичный неуникальный ```lua .... box.space.experience:create_index('samsara', {parts={ {field='event', type='string'}, {field='effect', type='string'} }}) ``` ### Схема данных для сокращателя ссылок - Таблица `redirector` | source | short | |:------:|:------:| | string | string | - Для перенаправления пользователя нам понадобятся два поля: - «Короткая ссылка» (`short`) - «И исходная (длинная)» (`source`) #### Индексация - В `Tarantool` таблица не может существовать без первичного индекса — одного или нескольких полей однозначно идентифицирующих запись в таблице - Самый первый созданный индекс на таблице и будет первичным - В нашем случае исходная ссылка уникальна и однозначно идентифицирует строку - Первичный индекс `source` - Кроме этого мы будем искать исходную ссылку для короткой для перенаправления браузера пользователя - Вторичный индекс `short` ### Создание таблицы для сокращателя ссылок - Создадим директорию проекта ```bash= mkdir makeshort && cd makeshort ``` - :::spoiler `storage.lua` ```lua= local fio = require('fio') local wrkdir = './db1' fio.mktree(wrkdir) box.cfg{ memtx_dir=wrkdir, wal_dir=wrkdir,} local function init() --[[ Создание схемы данных, индексов ]] box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true}) --[[ Таблица `redirector` — хранилище коротких ссылок if_not_exists — флаг, о том чтобы не генерировать исключение, если таблица уже существует ]] box.schema.space.create('redirector', {if_not_exists=true}) box.space.redirector:format({ {name='source', type='string'}, {name='short', type='string'}, }) --[[ Первичный ключ, первый создаваемый индекс таблицы ]] box.space.redirector:create_index( 'primary', { parts={ {field='source', type='string'}}, if_not_exists=true} ) --[[ Вторичный индекс, для быстрого поиска строки по короткой ссылке ]] box.space.redirector:create_index( 'short', { parts={ {field='short', type='string'}}, unique=true, if_not_exists=true,} ) end return { init=init, } ``` ::: - Запуск ```bash tarantool ``` - `tarantool>` ```lua storage = require('storage') storage.init() ``` - Добавить данные - `tarantool>` ```lua box.space.redirector:insert( {'https://google.com', 'abcdef'}) ``` - Запросить данные - По первичному ключу (уникальному) - `tarantool>` ```lua box.space.redirector:get('https://google.com') ``` - По вторичному - `tarantool>` ```lua box.space.redirector.index.short:get('abcdef') ``` # День 2 — Репликация - Сегодня вы запустите два узла - Настроите репликацию между ними - Научитесь анализировать состояние репликации ## Intro - Tarantool содержит механизм копирования (репликации) данных между узлами - Множество узлов объединённых репликацией называется репликасет (`replicaset`) - Например, простейшая схема - На узел-лидер записываются данные - Узел-реплика эти данные с лидера забирает и применяет у себя ```graphviz digraph { node [shape = circle style = filled]; node [fillcolor=white] leader; node [fillcolor=grey] replica; leader -> replica } ``` - Схема посложнее - Оба узла могут принимать операции записи - Оба узла применяют изменения соседа у себя - Пользователь может разрешать конфликты в конфликт-триггере ```graphviz digraph { node [shape = circle style = filled]; node [fillcolor=white] leader1 leader2; leader1 -> leader2 leader1 -> leader2[dir=back] } ``` - И сложные схемы - Мультимастер звезда ```graphviz digraph cluster_star { layout=circo; node [shape = circle style = filled]; node [label=leader fillcolor=white] leader1; node [label=leader fillcolor=white] leader2 leader3; node [label=replica fillcolor=grey] replica1 replica2 replica3 replica12 replica22 replica32; leader1 -> {leader2, leader3}[style=bold] leader2 -> {leader1, leader3}[style=bold] leader3 -> {leader1, leader2}[style=bold] leader1 -> {replica1, replica12}[color=grey] leader2 -> {replica2, replica22}[color=grey] leader3 -> {replica3, replica32}[color=grey] } ``` - Или даже `fullmesh` ```graphviz digraph cluster_star { layout=circo; overlap = false; ranksep=2.5; node [shape = circle, style = filled] node [label=leader fillcolor=white] leader1 node [label=leader fillcolor=white] leader2 leader3 leader1 -> {leader2, leader3}[style=bold] leader2 -> {leader1, leader3}[style=bold] leader3 -> {leader1, leader2}[style=bold] node [label=replica fillcolor=grey] replica1 replica2 replica3 replica12 replica22 replica32 leader1 -> {replica1, replica12, replica2, replica22, replica3, replica32}[color=grey] leader2 -> {replica1, replica12, replica2, replica22, replica3, replica32}[color=grey] leader3 -> {replica1, replica12, replica2, replica22, replica3, replica32}[color=grey] } ``` ### Настройка репликации - `box.cfg` содержит параметры для настройки репликации - `replication` — адреса узлов, от которых копировать данные - `instance_uuid` — идентификатор текущего узла - `replicaset_uuid` — идентификатор репликасета, у всех принадлежащих узлов он одинаковый - `read_only` — режим «только чтение» для узла, в целом может использоваться и без репликации - Репликация работает на том порту, что был указан в `box.cfg{listen}`, таким образом `replication` это список `listen` других узлов - В случае репликации, настройка схемы происходит **только на одном** на лидере - `box.info.ro` позволяет понять, текущий узел лидер или реплика ### Репликация для сокращателя ссылок ```graphviz digraph { node [shape = circle style = filled]; node [fillcolor=white label="moscow"] leader; node [fillcolor=grey label="moscow2"] replica; leader -> replica } ``` ## Конфигурация узлов - Оба узла мы будем запускать на одной машине - Исходный код будет использоваться один и тот же, но с разной конфигурацией - Вынесем конфигурацию узла в переменные среды (`environment variable`) - Конфигурация узла будет содержать: - Бинарный (`iproto`) адрес и порт для репликации - Источник репликации и режим реплики «только для чтения» - Переменные среды - `tarantool_instance_uuid` - `tarantool_replicaset_uuid` - `tarantool_listen` - `tarantool_replication` - `tarantool_workdir` - Добавим конфигурацию в исходный код - Отредактируем модуль `storage.lua`, чтобы указать параметры репликации - :::spoiler `storage.lua` ```lua= local fio = require('fio') local instance_uuid = os.getenv('tarantool_instance_uuid') local replicaset_uuid = os.getenv('tarantool_replicaset_uuid') local listen = os.getenv('tarantool_listen') or '127.0.0.1:3301' local replication = os.getenv('tarantool_replication') local wrkdir = os.getenv('tarantool_workdir') or './db1' fio.mktree(wrkdir) box.cfg{ instance_uuid = instance_uuid, replicaset_uuid = replicaset_uuid, listen = listen, replication = {replication}, memtx_dir=wrkdir, wal_dir=wrkdir,} local function init() if box.info.ro then return end --[[ Создание схемы данных, индексов, и пользователя репликации ]] box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true}) --[[ Таблица `redirector` — хранилище коротких ссылок if_not_exists — флаг, о том чтобы не генерировать исключение, если таблица уже существует ]] box.schema.space.create('redirector', {if_not_exists=true}) box.space.redirector:format({ {name='source', type='string'}, {name='short', type='string'}, }) --[[ Первичный ключ, первый создаваемый индекс таблицы ]] box.space.redirector:create_index( 'primary', { parts={ {field='source', type='string'}}, if_not_exists=true} ) --[[ Вторичный индекс, для быстрого поиска строки по короткой ссылке ]] box.space.redirector:create_index( 'short', { parts={ {field='short', type='string'}}, unique=true, if_not_exists=true,} ) end return { init=init, } ``` - Создадим входные точки - :::spoiler `start_moscow.lua` ```lua= os.setenv('tarantool_instance_uuid', '00000000-0000-4000-a000-0000000000a1') os.setenv('tarantool_replicaset_uuid', '00000000-0000-4000-a000-000000000001') --<< одинаковые для всех узлов репликасета os.setenv('tarantool_workdir', 'moscow') os.setenv('tarantool_listen', '127.0.0.1:3301') local storage = require('storage') storage.init() ``` - :::spoiler `start_moscow2.lua` ```lua= os.setenv('tarantool_instance_uuid', '00000000-0000-4000-a000-0000000000a2') os.setenv('tarantool_replicaset_uuid', '00000000-0000-4000-a000-000000000001') --<< одинаковые для всех узлов репликасета os.setenv('tarantool_workdir', 'moscow2') os.setenv('tarantool_listen', '127.0.0.1:3302') os.setenv('tarantool_replication', '127.0.0.1:3301') local storage = require('storage') storage.init() ``` #### Запуск ```bash tarantool start_moscow.lua ``` ```bash tarantool start_moscow2.lua ``` #### Проверка - Добавить данные на `moscow` ```bash tarantoolctl connect 127.0.0.1:3301 ``` - `tarantool>` ```lua box.space.redirector:insert( {'https://google.com', 'abcdef'}) ``` - Запросить данные с реплики `moscow2` ```bash tarantoolctl connect 127.0.0.1:3302 ``` - `tarantool>` ```lua box.space.redirector:get('https://google.com') ``` ```lua box.space.redirector.index.short:get('abcdef') ``` ### Мониторинг репликации - Проверим репликацию на реплике `moscow2` ```bash tarantoolctl connect 127.0.0.1:3302 ``` - `tarantool>` ```lua box.info().replication ``` - Примерный вывод ```yaml --- - 1: id: 1 uuid: 00000000-0000-4000-a000-0000000000a1 lsn: 8 upstream: status: follow idle: 0.73673253925517 peer: 127.0.0.1:3301 lag: 0.00011086463928223 2: id: 2 uuid: 00000000-0000-4000-a000-0000000000a2 lsn: 1 ... ``` - `upstream.status` - `follow` — репликация работает - `upstream.lag` - репликация отстаёт на `lag` секунд - Если `status` не `follow` — репликация не идёт, необходим анализ и вмешательство - `upstream.idle` — укажет как давно сломалась репликация # День 3 — Шардирование - Сегодня вы запустите шардированный кластер из четырех узлов - Адаптируете хранилище под шардинг ## Intro - Шардирование позволяет распределить данные по нескольким репликасетам `Tarantool` - Данные распределяются по корзинам - Корзины распределяются по репликасетам - Внутри репликасетов данные реплицируются между узлами - Обработка данных происходит на узлах-роутерах - Хранение данных происходит на узлах-стораджах - В случае добавления новых репликасетов с узлами-стораджами, происходит автоматическая перебалансировка корзин с данными на новый репликасет - Общее количество корзин `bucket_count` задаётся только один раз при первом старте приложения - Распределение данных по стораджам происходит на узлах-роутерах - Но в целом на одном узле может быть запущено обе роли: роутер и сторадж - Каждая таблица, что хочет шардирования, должна содержать неуникальный индекс `bucket_id` ### Топология, для сокращателя ссылок - Для простоты расположим роутеры на каждом из шардов ```graphviz digraph onnode { subgraph cluster_1 { label="Московский шард" subgraph cluster_2 { label="Москва"; moscow_router[label="Роутер"] moscow_storage[label="Хранилище" shape=cylinder] } style=dashed } subgraph cluster_3 { label="Питерский шард" subgraph cluster_4 { label="Питер" spb_router[label="Роутер"] spb_storage[label="Хранилище" shape=cylinder] } style=dashed } moscow_router -> moscow_storage moscow_router -> spb_storage spb_router -> spb_storage spb_router -> moscow_storage } ``` ### Шардирование сокращателя ссылок - Удалим устаревшие рабочие директории ```bash rm -rf moscow* ``` - Установим фреймворк шардирования `vshard` ```bash tarantoolctl rocks install vshard ``` - `topology.lua` ```lua= local cfg = { BUCKET_COUNT = 16, sharding = { ['00000000-0000-4000-a000-000000000001'] = { -- replicaset #1 replicas = { ['00000000-0000-4000-a000-0000000000a1'] = { uri = 'rep:rep@127.0.0.1:3301', name = 'storage_moscow', master = true }, }, }, -- replicaset #1 ['00000000-0000-4000-a000-000000000002'] = { -- replicaset #2 replicas = { ['00000000-0000-4000-a000-0000000000b1'] = { uri = 'rep:rep@127.0.0.1:3303', name = 'storage_spb', master = true }, }, }, -- replicaset #2 }, -- sharding } return cfg ``` - `storage.lua` ```lua= local function init() if box.info.ro then return end --[[ Создание схемы данных, индексов, и пользователя репликации ]] box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true}) --[[ Таблица `redirector` — хранилище коротких ссылок if_not_exists — флаг, о том чтобы не генерировать исключение, если таблица уже существует ]] box.schema.space.create('redirector', {if_not_exists=true}) box.space.redirector:format({ {name='source', type='string'}, {name='short', type='string'}, {name='bucket_id', type='unsigned'},}) --[[ Первичный ключ, первый создаваемый индекс таблицы ]] box.space.redirector:create_index( 'primary', { parts={ {field='source', type='string'}}, if_not_exists=true}) --[[ Вторичный индекс, для быстрого поиска строки по короткой ссылке ]] box.space.redirector:create_index( 'short', { parts={ {field='short', type='string'}}, unique=true, if_not_exists=true,}) --[[ Индекс для шардирования данных ]] box.space.redirector:create_index( 'bucket_id', { parts={ {field='bucket_id', type='unsigned'}}, unique=false, if_not_exists=true,}) box.schema.user.grant( 'rep', 'read,write,execute', 'universe', nil, {if_not_exists=true} ) end return { init=init, } ``` - `start_moscow.lua` ```lua= vshard = require('vshard') local topology = require('topology') local storage = require('storage') local fio = require('fio') fio.mktree('moscow') vshard.storage.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, memtx_dir = 'moscow', wal_dir = 'moscow', replication_connect_quorum = 1, }, '00000000-0000-4000-a000-0000000000a1') storage.init() vshard.router.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding,}) ``` - `start_spb.lua` ```lua= vshard = require('vshard') local topology = require('topology') local storage = require('storage') local fio = require('fio') fio.mktree('spb') vshard.storage.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, memtx_dir = 'spb', wal_dir = 'spb', replication_connect_quorum = 1, }, '00000000-0000-4000-a000-0000000000b1') storage.init() vshard.router.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding,}) ``` ### Запуск ```bash tarantool start_moscow.lua ``` ```bash tarantool start_spb.lua ``` ### Первоначальная настройка - На данный момент в логах ``` Some buckets are not active, retry rebalancing later ``` - Требуется Бутстрап Хранилища ``` tarantoolctl connect 127.0.0.1:3301 ``` - И в запущенном подключении выполнить бутстрап - `tarantool>` ```lua vshard.router.bootstrap() ``` ### Вставка данных - Доступ к данным теперь происходит через роутер - Подключимся к роутеру ``` tarantoolctl connect 127.0.0.1:3301 ``` - Подготовим данные - Для каждого объекты мы теперь должны вычислять `bucket_id` — прямо сейчас захардкодим эти значения - В будущем мы выберем по какому полю шардировать данные, что сценарий работы с сокращателем ссылок был удобен для пользователя - Для отправки данных на шард используем функцию `vshard.router.callrw` - `tarantool>` ```lua= bucket_id=1 google={'https://google.com', 'abcdef', bucket_id} vshard.router.callrw(bucket_id, 'box.space.redirector:put', {google}) bucket_id=2 ya={'https://ya.ru', 'qwerty', bucket_id} vshard.router.callrw(bucket_id, 'box.space.redirector:put', {ya}) bucket_id=3 apple={'https://apple.com', 'aaa', bucket_id} vshard.router.callrw(bucket_id, 'box.space.redirector:put', {apple}) ``` - Для запроса данных по ключу шардирования `vshard.router.callro` ```lua= vshard.router.callro(3, 'box.space.redirector:get', {'https://apple.com'}) ``` ## Запрос данных `map/reduce` - `vshard.router.routeall` возвращает все репликасеты - Функции роутера работают только на роутере - `replica:call*` вызывает функцию на подходящей реплике ```lua do local resultset = {} shards, err = vshard.router.routeall() if err ~= nil then print(err) return end for uid, replica in pairs(shards) do local set, err = replica:callro('box.space.redirector:select', {}) if err ~= nil then print(err) return end for _, link in ipairs(set) do table.insert(resultset, link) end end table.sort(resultset, function(a, b) return a[1] < b[1] end) return resultset end ``` ### Отказоустойчивый шардинг - Доработаем исходники - `topology.lua` ```lua= local cfg = { BUCKET_COUNT = 16, sharding = { ['00000000-0000-4000-a000-000000000001'] = { -- replicaset #1 replicas = { ['00000000-0000-4000-a000-0000000000a1'] = { uri = 'rep:rep@127.0.0.1:3301', name = 'storage_moscow', master = true }, ['00000000-0000-4000-a000-0000000000a2'] = { uri = 'rep:rep@127.0.0.1:3302', name = 'storage_moscow2', }, }, }, -- replicaset #1 ['00000000-0000-4000-a000-000000000002'] = { -- replicaset #2 replicas = { ['00000000-0000-4000-a000-0000000000b1'] = { uri = 'rep:rep@127.0.0.1:3303', name = 'storage_spb', master = true }, ['00000000-0000-4000-a000-0000000000b2'] = { uri = 'rep:rep@127.0.0.1:3304', name = 'storage_spb2', }, }, }, -- replicaset #2 }, -- sharding } return cfg ``` - `start_moscow2.lua` ```lua= vshard = require('vshard') local topology = require('topology') local storage = require('storage') local fio = require('fio') fio.mktree('moscow2') vshard.storage.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, memtx_dir = 'moscow2', wal_dir = 'moscow2', replication_connect_quorum = 1, }, '00000000-0000-4000-a000-0000000000a2') storage.init() vshard.router.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding,}) ``` - `start_spb2.lua` ```lua= vshard = require('vshard') local topology = require('topology') local storage = require('storage') local fio = require('fio') fio.mktree('spb2s') vshard.storage.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, memtx_dir = 'spb2', wal_dir = 'spb2', replication_connect_quorum = 1, }, '00000000-0000-4000-a000-0000000000b2') storage.init() vshard.router.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding,}) ``` - Запуск ```bash tarantool start_moscow.lua ``` ```bash tarantool start_spb.lua ``` ```bash tarantool start_moscow2.lua ``` ```bash tarantool start_spb2.lua ``` ### Запросы ``` tarantoolctl connect 127.0.0.1 ``` - `tarantool>` - Запись ```lua vshard.router.callrw(1, 'print', {'<<CALL>>'}) ``` - Чтение предпочитая лидера ```lua vshard.router.callro(1, 'print', {'<<CALL>>'}) ``` - Чтение с балансировкой ```lua vshard.router.callbro(1, 'print', {'<<CALL>>'}) ``` - Чтение с балансировкой предпочитая реплику ```lua vshard.router.callbro(1, 'print', {'<<CALL>>'}) ``` # День 4 — Апп сервер - Сегодня вы добавите логику приложения к своему хранилищу - Напишете легковесный `http` сервер - Реализуете `http` `api` в своем приложении ## Intro ### Lua за 15 минут - https://learnxinyminutes.com/docs/ru-ru/lua-ru/ ### Код - Код на языке `Lua` выполняется в файберах (их также называют корутины, горутины, зеленые треды, легковесные сопрограммы) - Файберы работают по принципу кооперативной многозадачности - В текущий момент времени выполняется только один файбер, и этот файбер, если операция ввода-вывода или явно, передаёт управление другому файберу - `Tarantool` содержит встроенные модули, некоторыми из которых мы воспользуемся: - `log` — журналирование событий приложения и базы данных - `console` — предлагает возможность выполнения и отладки кода в уже запущенном инстансе тарантула - `clock` — функции для работы с системным временем - `digest` — вычисление хеша от строки - `fio` — работа с файлами - `socket` — работа с сетью - `box` — настройка и хранение данных - Подробнее обо всех модулях: https://www.tarantool.io/en/doc/latest/reference/ ## Сервер приложений - Сделаем легковесный `http` сервер - `socket.tcp_server(server, port, client_handler)` создаёт файбер, который - слушает tcp/ip `server:port` - для каждого нового клиента запускает `client_handler` в отдельном файбере - `client_handler` принимает первым аргументом соединение с клиентом - Функция обработки `http` запросов: - для `GET /` вернет html форму для ввода ссылки - для `GET /?url=*` сгенерирует и вернет короткую ссылку - для `GET /*` перенаправит на исходную ссылку - Таким rest api я слегка хитрю, на этом этапе мы быстренько напишем `http` парсер воспользовавшись только первой строкой протокола - Модуль `digest` помогает сгенерировать хеш от строки - Прежде чем вычислить хеш, к исходной строке добавляется рандомная «соль» - Доступ к данным будет выполняться всё также через `vshard.router.callrw`(`-ro`) ### Топология - Возьмём топологию попроще для удобства запуска и отладки ```graphviz digraph onnode { subgraph cluster_shard1 { label="Московский шард" subgraph cluster_1 { label="Москва" appserver[label="Апп сервер" shape=box] moscow_router[label="Роутер"] moscow_storage[label="Хранилище" shape=cylinder] moscow_storage[label="Хранилище" shape=cylinder] } style=dashed } subgraph cluster_shard2 { label="Питерский шард" subgraph cluster_3 { label="Питер" spb_storage[label="Хранилище" shape=cylinder] } style=dashed } appserver -> moscow_router moscow_router -> moscow_storage moscow_router -> spb_storage } ``` ### Сервер обработки http запросов - `appserver.lua` ```lua= local log = require('log') local socket = require('socket') local clock = require('clock') local digest = require('digest') local vshard = require('vshard') local serveraddr = '0.0.0.0' local port = 8080 local hostname = 'localhost' ---<<<!!!!! Вставить dns имя или ip своей виртуалки --[[ Сердце сервиса — функция генерации короткой ссылки Генерируем хеш с рандомной солью и вставляем в хранилище Шардируем по значению короткой ссылки ]] local function make_it_short(source) local short = nil short = digest.base64_encode( digest.urandom(10), {nopad=true, nowrap=true, urlsafe=true}) local bucket_id = vshard.router.bucket_id_mpcrc32(short) local now = math.floor(clock.time()) local rc, err = vshard.router.callrw(bucket_id, 'box.space.redirector:insert', {{source, short, now, bucket_id}}) if err ~= nil then return nil, err end return short end --[[ Тоже сердце — функция поиска исходной ссылки по короткой ]] local function take_me_source(short) local bucket_id = vshard.router.bucket_id_mpcrc32(short) local tuple, err = vshard.router.callro(bucket_id, 'box.space.redirector.index.short:get', {short}) if err ~= nil then log.warn(err) return nil, err end if tuple == nil then return nil end return tuple[1] end --[[ HTTP сервер обработки запросов ]] --[[ Утилиты для декодирования значений GET параметров url строки ]] local hex_to_char = function(x) return string.char(tonumber(x, 16)) end local unescape = function(url) return url:gsub("%%(%x%x)", hex_to_char) end --[[ HTML форма для создания короткой ссылки ]] local form = [[ <html> <head></head> <body> <form> <input name=url type=text /> <input type=submit /> </form> </body> </html> ]] --[[ Функция возвращающая успешный (200) http пакет с данными `data` ]] local function response200(data) return ("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: %u\r\nConnection: close\r\n\r\n%s") :format(#data, data) end --[[ Возврат ошибки 404 ]] local function response404() return "HTTP/1.1 404 Not Found\r\nContent-Type: text; charset=UTF-8\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot Found" end --[[ Редирект на страницу target ]] local function redirect(target) return ("HTTP/1.1 301 Moved Permanently\r\nLocation: %s\r\nContent-Type: text/html\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") :format(target) end --[[ TCP/IP сервер с оооочень простой (прямо таки простейшей) логикой http протокола Для каждого соединения в отдельном файбере запускается функция `function(client)`, которая обрабатывает http запросы и отвечает http ответы ]] --[[ (ну почти REST) API GET / — html форма для создания короткой cсылки GET /?url=* — создание короткой ссылки по исходной в url параметре GET /* - переход по короткой ссылке ]] local server local function init(opts) server = socket.tcp_server( serveraddr, port, function(client) local request_line = client:read('\r\n', 10) if request_line == nil then log.info('Timeout') return end --[[ Если запрос корня — предлагаем форму ввода ]] if request_line:startswith('GET / HTTP') then client:write(response200(form)) --[[ Запрос на создание короткой ссылки ]] elseif request_line:startswith('GET /?url=') then local url = request_line:sub(#'GET /?url=') url = url:sub(2, url:find(' ') - 1) url = unescape(url) local short = make_it_short(url) if short == nil then client:write(response404()) return end local pasteable_short = 'http://' .. hostname .. ':' .. tostring(port) .. '/' .. short client:write(response200(pasteable_short)) --[[ Запрос на переход по короткой ссылке Редиректим на исходную ссылку, если она была в базе ]] elseif request_line:startswith('GET /') then local short = request_line:sub(#'GET /') short = short:sub(2, short:find(' ') - 1) local source = take_me_source(short) if source == nil then client:write(response404()) end client:write(redirect(source)) end end) server:listen() end local function stop() server:close() end --[[ Возвращаем - имя роли для использования в GUI и rpc API - колбеки жизненного цикла - зависимости от других ролей - vshard-router - роль, которая поможет запрашивать шардированные данные ]] return { init=init, stop=stop, } ``` - `start_moscow.lua` ```lua= vshard = require('vshard') local topology = require('topology') local storage = require('storage') local fio = require('fio') fio.mktree('moscow') vshard.storage.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, memtx_dir = 'moscow', wal_dir = 'moscow', replication_connect_quorum = 1, }, '00000000-0000-4000-a000-0000000000a1') storage.init() vshard.router.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, }) local appserver = require('appserver') appserver.init() ``` ## Запуск ```bash tarantool start_moscow.lua ``` ```bash tarantool start_spb.lua ``` ## Оно заработало? - Откроем браузер на `http://<ip_address>:8080/` - Должна появится форма для ввода `url` - Введем любой `url` - Получим короткую ссылку - Перейдем по короткой ссылке - Или можно воспользоваться готовым скриптом, который пытается укоротить `https://google.com/?q=<random-string>` - `test.lua` ```lua= local log = require('log') local client = require('http.client') --[[ Простой тест для проверки укорачивания ссылок ]] local host = '127.0.0.1' local port = 8080 local source = 'https://google.com/?q=' .. tostring(os.time()) local makeshort = 'http://' .. host .. ':' .. port .. '/?url=' local response = client.get(makeshort .. source) local short = response.body print(short, '->', source) local redirect = client.get(short, {follow_location=true}) assert(redirect.headers['location'] == source, "Source url and redirect not equal") ``` - Запуск теста ```bash tarantool test.lua ``` - Примерный вывод ```bash http://localhost:8080/XniDtG2jrBa23Q -> https://google.com/?q=1607896232 ``` # День 5 — Cartridge приложение - Соберете приложение в пакет `Cartridge` - Поуправляете кластером через веб интерфейс - Создадите кластерный тест к своему приложению ## Intro - `Cartidge` — фреймворк, который предлагает простой способ разработки и управления кластерным приложением - Приложение разделяется на модули специальным образом — такие модули называются «роли» - После чего вы запускаете одно или несколько экземпляров приложения и конфигурируете из них кластер с помощью `web ui` - `Cartridge` содержит модуль для тестирования приложений в кластерном виде - Для деплоя `Cartridge` предоставляет интерфейс для упаковки приложения в `rpm`/`deb`/`targz` пакет ### Роль - Роль — модуль реализующий API - `role_name` - `init` - `validate_config` - `apply_config` - `stop` - `dependencies` - Роли могут взаимодействовать друг с другом: - Локально, если одна роль зависит от другой - Тогда `Cartridge` их запускает на одном узле - Удаленно, тогда между ролями используется вызов `cartridge.rpc_call` - `init`/`stop` вызываются при запуске и останове роли соответственно - `validate_config`/`apply_config` вызываются при изменении конфигурации кластера: - Топологии - Переключении мастера - Настроек failover-а - Пользовательских настроек ## Подготовка - Удаление устаревших рабочих директорий ```bash rm -rf moscow* spb* ``` - Удаление устаревших файлов ```bash rm -rf start_* topology.lua ``` - Установка фреймворка ```bash tarantoolctl rocks install cartridge 2.3.0 ``` - У всех узлов теперь входная точка с инициализацией `cartridge` - `init.lua` ```lua= #!/usr/bin/env tarantool --[[ Устанавливаем текущую директорию, как начальный путь загрузки всех модулей ]] package.setsearchroot() local cartridge = require('cartridge') local log = require('log') --[[ Конфигурируем и запускаем cartridge на узле Задаем небольшое количество корзин шардирования для удобства разработки и отладки Указываем какие роли мы будем использовать в кластере - vshard storage для хранения шардированных данных - vshard router для доступа к ним Указываем рабочую директорию для хранения снапов, икслогов и конфигурации приложения `one` ]] local _, err = cartridge.cfg({ workdir = 'moscow', bucket_count = 32, roles = { 'appserver', 'storage', 'cartridge.roles.vshard-storage', 'cartridge.roles.vshard-router' }, }) if err ~= nil then log.info(err) os.exit(1) end ``` - Для модуля апсервера добавим API для кластеризации - :::spoiler `appserver.lua` ```lua= local log = require('log') local socket = require('socket') local clock = require('clock') local digest = require('digest') local vshard = require('vshard') local serveraddr = '0.0.0.0' local port = 8080 local hostname = 'localhost' ---<<<!!!!! Вставить dns имя или ip своей виртуалки --[[ Сердце сервиса — функция генерации короткой ссылки Генерируем хеш с рандомной солью и вставляем в хранилище Шардируем по значению короткой ссылки ]] local function make_it_short(source) local short = nil short = digest.base64_encode( digest.urandom(10), {nopad=true, nowrap=true, urlsafe=true}) local bucket_id = vshard.router.bucket_id_mpcrc32(short) local now = math.floor(clock.time()) local rc, err = vshard.router.callrw(bucket_id, 'box.space.redirector:insert', {{source, short, now, bucket_id}}) if err ~= nil then return nil, err end return short end --[[ Тоже сердце — функция поиска исходной ссылки по короткой ]] local function take_me_source(short) local bucket_id = vshard.router.bucket_id_mpcrc32(short) local tuple, err = vshard.router.callro(bucket_id, 'box.space.redirector.index.short:get', {short}) if err ~= nil then log.warn(err) return nil, err end if tuple == nil then return nil end return tuple[1] end --[[ HTTP сервер обработки запросов ]] --[[ Утилиты для декодирования значений GET параметров url строки ]] local hex_to_char = function(x) return string.char(tonumber(x, 16)) end local unescape = function(url) return url:gsub("%%(%x%x)", hex_to_char) end --[[ HTML форма для создания короткой ссылки ]] local form = [[ <html> <head></head> <body> <form> <input name=url type=text /> <input type=submit /> </form> </body> </html> ]] --[[ Функция возвращающая успешный (200) http пакет с данными `data` ]] local function response200(data) return ("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: %u\r\nConnection: close\r\n\r\n%s") :format(#data, data) end --[[ Возврат ошибки 404 ]] local function response404() return "HTTP/1.1 404 Not Found\r\nContent-Type: text; charset=UTF-8\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot Found" end --[[ Редирект на страницу target ]] local function redirect(target) return ("HTTP/1.1 301 Moved Permanently\r\nLocation: %s\r\nContent-Type: text/html\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") :format(target) end --[[ TCP/IP сервер с оооочень простой (прямо таки простейшей) логикой http протокола Для каждого соединения в отдельном файбере запускается функция `function(client)`, которая обрабатывает http запросы и отвечает http ответы ]] --[[ (ну почти REST) API GET / — html форма для создания короткой cсылки GET /?url=* — создание короткой ссылки по исходной в url параметре GET /* - переход по короткой ссылке ]] local server local function init(opts) server = socket.tcp_server( serveraddr, port, function(client) local request_line = client:read('\r\n', 10) if request_line == nil then log.info('Timeout') return end --[[ Если запрос корня — предлагаем форму ввода ]] if request_line:startswith('GET / HTTP') then client:write(response200(form)) --[[ Запрос на создание короткой ссылки ]] elseif request_line:startswith('GET /?url=') then local url = request_line:sub(#'GET /?url=') url = url:sub(2, url:find(' ') - 1) url = unescape(url) local short = make_it_short(url) if short == nil then client:write(response404()) return end local pasteable_short = 'http://' .. hostname .. ':' .. tostring(port) .. '/' .. short client:write(response200(pasteable_short)) --[[ Запрос на переход по короткой ссылке Редиректим на исходную ссылку, если она была в базе ]] elseif request_line:startswith('GET /') then local short = request_line:sub(#'GET /') short = short:sub(2, short:find(' ') - 1) local source = take_me_source(short) if source == nil then client:write(response404()) end client:write(redirect(source)) end end) server:listen() end local function stop() server:close() end --[[ Возвращаем - имя роли для использования в GUI и rpc API - колбеки жизненного цикла - зависимости от других ролей - vshard-router - роль, которая поможет запрашивать шардированные данные ]] return { role_name="appserver", init=init, stop=stop, dependencies={'cartridge.roles.vshard-router'} } ``` ::: - :::spoiler `storage.lua` ```lua= --[[ Запуск роли В случае запуска на лидере репликасета создаём таблицы И для неё создаем первичный индекс и индекс для шардирования данных В случае, когда роль перезапускается, используем флаг `if_not_exists=true` для игнорирования в случае уже созданных объектов ]] local function init(opts) if not opts.is_master then return end --[[ Таблица `redirector` — хранилище коротких ссылок if_not_exists — флаг, о том чтобы не генерировать исключение, если таблица уже существует ]] box.schema.space.create('redirector', {if_not_exists=true}) box.space.redirector:format({ {name='source', type='string'}, {name='short', type='string'}, {name='bucket_id', type='unsigned'},}) --[[ Первичный ключ, первый создаваемый индекс таблицы ]] box.space.redirector:create_index( 'primary', { parts={ {field='source', type='string'}}, if_not_exists=true}) --[[ Вторичный индекс, для быстрого поиска строки по короткой ссылке ]] box.space.redirector:create_index( 'short', { parts={ {field='short', type='string'}}, unique=true, if_not_exists=true,}) --[[ Индекс для шардирования данных ]] box.space.redirector:create_index( 'bucket_id', { parts={ {field='bucket_id', type='unsigned'}}, unique=false, if_not_exists=true,}) end --[[ Роль не пользуется кластерным конфигом ]] local function validate_config(new_conf, old_conf) return true end local function apply_config(conf, opts) return true end --[[ Удаление таблиц предпочтительно не автоматизировать ]] local function stop() end --[[ Возвращаем - имя роли для использования в GUI и rpc API - колбеки жизненного цикла - зависимости от других ролей - vshard-storage, роль которая будет заботиться о шардировании данных всех спейсов, у которых есть индекс `bucket_id` ]] return { role_name="storage", init=init, validate_config=validate_config, apply_config=apply_config, stop=stop, dependencies={'cartridge.roles.vshard-storage'} } ``` ::: ## Однонодовый запуск - Запустим приложение ```bash tarantool init.lua ``` - Откроем страницу администрирования http://127.0.0.1:8081 - На вкладке `Cluster` на единственном репликасете выберем `Edit` и включим роли `appserver`, `storage` - Автоматически включатся зависимые роли ## Bootstrap vshard - Если сейчас вставлять данные, будет ошибка о том, что шардированное хранилище не инициализировано - Откроем страницу администрирования http://127.0.0.1:8081 - На вкладке `Cluster` нажмем `Bootstrap vshard` ## Кластер - Запустим ещё один узел приложения - В другую директорию - С другим портом - С отключенным `http` сервером ```bash tarantool init.lua --advertise-uri 127.0.0.1:3302 \ --http-enabled false --workdir spb ``` - На `web gui` на вкладке `Cluster` должен отобразиться несконфигурированный узел - Если не появился, нужно нажать `Probe uri` и ввести `advertise-uri` нового запущенного сервера `127.0.0.1:3302` - Нажмем `Configure`, укажем роль `storage`, создадим новый репликасет с этим узлом `Create replica set` - Таким образом получится два шарда - Запустим тест для проверки, что всё получилось ```bash tarantool test.lua ``` ## Интеграционное тестирование - Особенностью тестирования является возможность собирать произвольную топологию кластера, и проводить сценарии используя один или даже все узлы - Установить фреймворк для тестирования ```bash tarantoolctl rocks install luatest ``` - В директории `test` - Имя файла заканчивается на `_test` - <details> <summary><code>test/basic_test.lua</code></summary> ```lua= local fio = require('fio') local log = require('log') local client = require('http.client') local uri = require('uri') local t = require('luatest') local g = t.group('starwars') local helpers = require('cartridge.test-helpers') g.before_all(function() local tempdir = fio.tempdir() local cluster = helpers.Cluster:new({ datadir = tempdir, server_command = 'init.lua', use_vshard = true, replicasets = {{ roles = {'appserver'}, servers = {{ alias = 'appserver', }}}, { roles = {'storage'}, servers = {{ alias = 'storage', }}}, { roles = {'storage'}, servers = {{ alias = 'storage2', }}}, }, }) cluster:start() g.cluster = cluster end) g.after_all(function() g.cluster:stop() end) function g.test_starwars_storage() local cluster = g.cluster local server = cluster:server('appserver') --[[ Простой тест для проверки укорачивания ссылок ]] local url = uri.parse(server.advertise_uri) local host = url.host local port = 8080 local source = 'https://google.com/?q=' .. tostring(os.time()) local makeshort = 'http://' .. host .. ':' .. port .. '/?url=' local response = client.get(makeshort .. source) local short = response.body log.info(short, '->', source) local redirect = client.get(short, {follow_location=true}) t.assert_equals(redirect.headers['location'], source, "Source url and redirect not equal") end ``` </details> - `chmod +x init.lua` - Завершить запущенный кластер ```bash pkill -9 tarantool ``` - Запуск тестов ``` bash .rocks/bin/luatest test/basic_test.lua ``` ``` Ran 1 tests in 2.233 seconds, 1 success, 0 failures ``` # TODO - github.com/tarantool/metrics - curl | grep `box.space.redirector` запрос - prometheus, grafana (подчеркнуть опциональность)