Как создать свой собственный бессерверный инструмент Pastebin
1 марта 2022 г.Сегодня мы будем создавать клон Pastebin — веб-сервис, который позволяет пользователям загружать текст и делиться им с помощью ссылок, известных как «вставки». Далее следует мой путь создания клона Pastebin с использованием бессерверных функций через [Cloudflare Worker] (https://workers.cloudflare.com/). Если вы не знакомы с Pastebin, я настоятельно рекомендую вам попробовать его, прежде чем читать дальше.
«Почему Пастебин?» Вы можете спросить. Что ж, отправка >50-строчного блока текста (или кода) через чат-приложение (глядя на вас, IRC) не совсем лучший вариант. способ общаться.
TL;DR — Создайте бессерверную вставку
- Создание клона Pastebin с использованием Cloudflare Worker и KV
- Планирование требований и ограничений проекта
- Вставьте логику генерации UUID URL с помощью службы генерации ключей (KGS)
- Дизайн и реализация GraphQL API
- Живая демонстрация на [paste.jerrynsh.com] (https://paste.jerrynsh.com/)
- [Репозиторий GitHub] (https://github.com/ngshiheng/paste-story)
Дизайн этого клона Pastebin будет очень похож на [создание клона TinyURL] (https://dev.to/jerrynsh/i-built-my-own-tinyurl-heres-how-i-did-it-11ah) , за исключением того, что нам нужно сохранить содержимое вставки вместо исходного несокращенного URL-адреса.
Прежде чем мы начнем, это НЕ учебник или руководство по:
- Как пройти настоящее собеседование по проектированию системы
- Создание коммерческого инструмента вставки, такого как [Pastebin] (https://en.wikipedia.org/wiki/Pastebin) или [GitHub Gist] (https://gist.github.com/).
Скорее, это доказательство концепции (POC) того, как создать простой инструмент вставки с использованием бессерверных вычислений с Cloudflare Worker. Чтобы изучить эту статью, ознакомьтесь с шагами 1–3 этого [Руководства по началу работы] (https://developers.cloudflare.com/workers/get-started/guide).
Поехали!
Требования для вашего бессерверного Pastebin
Давайте начнем с уточнения вариантов использования и ограничений нашего проекта.
Функциональный
- Всякий раз, когда пользователь вводит блок текста (или кода), наша веб-служба должна генерировать URL-адрес со случайным ключом (UUID), например.
paste.jerrynsh.com/aj7kLmN9
- Всякий раз, когда пользователь посещает сгенерированный URL-адрес, пользователь должен быть перенаправлен для просмотра исходного содержимого вставки, т. е. исходного блока текста.
- Ссылка на вставку должна истечь через 24 часа
- UUID должен содержать только буквенно-цифровые символы (Base62)
- Длина нашего UUID должна быть 8 символов
Не работает
- Низкая задержка
- Высокая доступность
Планирование бюджета, мощностей и ограничений
Как и в нашей предыдущей попытке, цель здесь — разместить этот сервис бесплатно. Учитывая цены Cloudflare Worker и лимиты платформы, наши ограничения являются:
- 100 тыс. запросов/день при 1 тыс. запросов/мин.
- Время работы процессора не более 10 мс
Ожидается, что наше приложение, как и программа для сокращения URL-адресов, будет иметь высокое соотношение чтения и записи. При этом мы будем использовать Cloudflare KV (далее KV), хранилище ключей и значений с малой задержкой для этого проект.
На момент написания бесплатный уровень KV имел следующие лимиты:
- 100 тыс. прочтений/день
- 1к записей/день
- 1 ГБ хранимых данных (размер ключа 512 байт, размер значения 25 МБ)
Сколько паст мы можем хранить?
В этом разделе мы собираемся оценить, сколько вставок может хранить наш клон Pastebin, учитывая указанные выше ограничения. В отличие от хранения URL, хранение текстовых блоков может занимать гораздо больше места (относительно). Вот предположения, которые мы собираемся сделать:
- 1 символ равен 1 байту (используя этот счетчик байтов)
- Предполагая, что в среднем одна вставка (файл) может состоять примерно из 200 строк кода (текста), это будет означать, что размер каждой вставки будет около 10 КБ.
- При максимальном размере хранилища 1 ГБ это означает, что наш клон Pastebin может хранить только до 100 000 вставок.
Обратите внимание, что ограничения применяются для каждой учетной записи.
Хранилище и база данных
Cloudflare Worker KV
Для этого POC мы собираемся использовать KV в качестве базы данных. Давайте немного углубимся в то, что он делает.
В настоящее время теорема CAP часто используется для моделирования распределенных хранилищ данных. Теорема CAP утверждает, что распределенная система может обеспечить только 2 из следующих 3 гарантий (источник):
- Cсогласованность — везде ли мои данные одинаковы?
- Aдоступность — всегда ли доступны мои данные?
- Допуск Pразделов — устойчивы ли мои данные к региональным сбоям?
В случае KV Cloudflare решает гарантировать Aдоступность и Pтолерантность к разделам, что соответствует нашим нефункциональным требованиям. Несмотря на то, что эта комбинация кричит об окончательной согласованности, это компромисс, с которым мы согласны.
Не забываем упомянуть, что KV поддерживает исключительно большие объемы чтения со сверхнизкой задержкой — идеально подходит для нашего приложения с высоким соотношением чтения и записи.
Теперь, когда мы поняли компромиссы, давайте двигаться дальше!
Как реализовать свой бессерверный Pastebin
Логика генерации URL
Логика генерации UUID URL-адреса будет очень похожа на сокращатель URL-адресов. Вот краткий обзор возможных подходов:
- Используйте генератор UUID для генерации UUID по запросу для каждого нового запроса.
- Используйте хэш (MD5) содержимого вставки в качестве нашего UUID, затем используйте первые N символов хеша как часть нашего URL-адреса.
- Использование комбинации хэширования + кодирования Base62
- Используйте автоматически увеличивающееся целое число в качестве нашего UUID
Однако мы используем другое решение, не упомянутое выше.
Предварительно сгенерировать ключ UUID
Для этого POC мы предварительно сгенерируем список UUID в KV с помощью отдельного работника. Мы будем называть работника сервисом генератора ключей (KGS). Всякий раз, когда мы хотим создать новую пасту, мы назначаем новой пасте предварительно сгенерированный UUID.
Итак, каковы преимущества ведения дел таким образом?
При таком подходе нам не придется беспокоиться о дублировании ключей или коллизиях хэшей (например, из подходов 2 или 3), поскольку наш генератор ключей гарантирует, что ключи, вставленные в наш KV, уникальны.
Здесь мы будем использовать 2 KV:
KEY_KV
— используется нашим KGS для хранения предварительно сгенерированного списка UUID
PASTE_KV
— используется нашим основным сервером приложений для хранения пары ключ-значение; где ключ — это UUID, а значение — содержимое вставки.
Чтобы создать KV, просто выполните следующие команды с помощью интерфейса командной строки Wrangler (источник).
``` ударить
Рабочее пространство имен:
wrangler kv: пространство имен создать "PASTE_DB"
wrangler kv: пространство имен создать "KEY_DB"
Это пространство имен используется для локального тестирования wrangler dev
:
wrangler kv: пространство имен создать "PASTE_DB" --предварительный просмотр
wrangler kv: пространство имен создать "KEY_DB" --preview
Для создания этих пространств имен KV нам потребуется обновить наши файлы wrangler.toml
, чтобы включить соответствующие привязки пространств имен. Чтобы просмотреть доску dash вашего KV, посетите https://dash.cloudflare.com/<your_cloudflare_account_id>/workers/kv/namespaces
.
Как сгенерировать UUID
Чтобы KGS генерировал новые UUID, мы будем использовать пакет nanoid. Если вы заблудились, вы всегда можете обратиться к папке /kgs
в репозитории GitHub.
Как KGS узнает о наличии дубликата ключа? Всякий раз, когда KGS создает ключ, он всегда должен проверять, существует ли уже UUID в KEY_DB
и PASTE_DB
.
Кроме того, UUID следует удалить из KEY_DB и создать в PASTE_DB при создании новой вставки. Мы рассмотрим код в разделе API.
```javascript
// /kgs/src/utils/keyGenerator.js
импортировать { customAlphabet } из "nanoid";
импортировать {АЛФАВИТ} из "./константы";
Сгенерируйте uuid с помощью пакета nanoid.
Продолжайте повторять попытки, пока не будет сгенерирован uuid
, которого нет в обоих KV (PASTE_DB
и KEY_DB
).
KGS гарантирует, что предварительно сгенерированные ключи всегда уникальны.
экспортировать константу generateUUIDKey = async () => {
const nanoId = customAlphabet (АЛФАВИТ, 8);
пусть uuid = nanoId();
пока (
(ждите KEY_DB.get(uuid)) !== ноль &&
(ждите PASTE_DB.get(uuid)) !== ноль
uuid = nanoId();
вернуть UUID;
Недостаточно уникальных ключей для генерации
Другая потенциальная проблема, с которой мы можем столкнуться, — что нам делать, когда все наши UUID в нашем KEY_KV полностью израсходованы?
Для этого мы настроим триггер Cron, который периодически ежедневно пополняет наш список UUID. Чтобы реагировать на триггер Cron, мы должны добавить «запланированный» прослушиватель событий к сценарию Workers, как показано ниже в коде ниже.
```javascript
// /kgs/src/index.js
импортировать {MAX_KEYS} из "./utils/constants";
импортировать { generateUUIDKey } из "./utils/keyGenerator";
Предварительно сгенерируйте список уникальных uuid.
Гарантирует, что предварительно сгенерированный список KV uuid всегда имеет количество ключей MAX_KEYS.
const handleRequest = асинхронный () => {
const existsUUIDs = await KEY_DB.list();
let keysToGenerate = MAX_KEYS - existsUUIDs.keys.length;
console.log(Существующее количество ключей: ${existingUUIDs.keys.length}.
);
console.log(Приблизительное количество ключей для генерации: ${keysToGenerate}.
);
в то время как (keysToGenerate! = 0) {
const newKey = await generateUUIDKey();
ожидайте KEY_DB.put (новый ключ, "");
console.log(Сгенерирован новый ключ в KEY_DB: ${newKey}.
);
ключи для генерации--;
const currentUUIDs = await KEY_DB.list();
console.log(Текущее количество ключей: ${currentUUIDs.keys.length}.
);
addEventListener("запланировано", (событие) => {
event.waitUntil (handleRequest (событие));
Поскольку наш POC может поддерживать только до 1000 записей в день, мы установим MAX_KEYS для генерации 1000. Не стесняйтесь настраивать в соответствии с ограничениями вашей учетной записи.
API
На высоком уровне нам, вероятно, понадобятся 2 API:
- Создание URL для вставки содержимого
- Перенаправление на исходный контент вставки
Для этого POC мы будем разрабатывать наш API в [GraphQL] (https://graphql.org/) с использованием сервера Apollo GraphQL. В частности, мы будем использовать рабочий шаблон itty-router вместе с workers-graphql-server.
Прежде чем мы продолжим, вы можете напрямую взаимодействовать с GraphQL API этого POC через конечную точку GraphQL Playground, если вы не знакомы с GraphQL.
В случае утери вы всегда можете обратиться к папке /server
.
Маршрутизация
Для начала точка входа нашего API-сервера находится в src/index.js
, где вся логика маршрутизации обрабатывается itty-router
.
```javascript
// сервер/источник/index.js
const { отсутствует, ThrowableRouter, withParams } = require("itty-router-extras");
const apollo = require("./handlers/apollo");
const index = require("./handlers/index");
const paste = require("./handlers/paste");
константная игровая площадка = require("./handlers/playground");
const router = ThrowableRouter();
router.get("/", индекс);
router.all("/graphql", игровая площадка);
router.all("/__graphql", аполлон);
router.get("/:uuid", withParams, paste);
router.all("*", () => отсутствует("Не найдено"));
addEventListener("выборка", (событие) => {
event.respondWith(router.handle(event.request));
Создание вставки
Обычно для создания любого ресурса в GraphQL нам нужна [мутация] (https://graphql.org/learn/queries/#mutations). В мире REST API создание мутации GraphQL будет очень похоже на отправку запроса в конечную точку POST, например. /v1/api/вставить
. Вот как будет выглядеть наша мутация GraphQL:
```иди
мутация {
createPaste(content: "Привет, мир!") {
UUID
содержание
создано на
expireAt
Под капотом обработчик (преобразователь) должен вызывать createPaste
, который принимает контент
из тела HTTP JSON. Ожидается, что эта конечная точка вернет следующее:
```json
"данные": {
"создать вставить": {
"uuid": "0pZUDXzd",
"content": "Привет, мир!",
"createdOn": "2022-01-29T04:07:06+00:00",
"expireAt": "2022-01-30T04:07:06+00:00"
Вы можете проверить схему GraphQL [здесь] (https://github.com/ngshiheng/paste-story/blob/main/server/src/schema.js).
Вот реализация в коде наших преобразователей:
```javascript
// /сервер/src/resolvers.js
const { ApolloError } = require («apollo-server-cloudflare»);
модуль.экспорт = {
Запрос: {
getPaste: асинхронный (_source, {uuid}, {dataSources}) => {
вернуть dataSources.pasteAPI.getPaste(uuid);
Мутация: {
createPaste: async (_source, {content}, {dataSources}) => {
если (!content || /^\s*$/.test(content)) {
throw new ApolloError("Вставляемое содержимое пусто");
вернуть dataSources.pasteAPI.createPaste (контент);
Для защиты от спама мы также добавили небольшую проверку для предотвращения создания пустых вставок.
Вставить источник данных создания
Мы храним логику API, которая взаимодействует с нашей базой данных (KV) внутри /datasources
.
Как упоминалось ранее, нам нужно удалить ключ, используемый из KGS KEY_DB
KV, чтобы избежать риска назначения дубликатов ключей для новых вставок.
Здесь мы также можем установить для нашего ключа срок действия expirationTtl
, равный одному дню после создания вставки:
```javascript
// /сервер/источник/источники данных/paste.js
const { ApolloError } = require('apollo-server-cloudflare')
постоянный момент = требуется ('момент')
Создайте новую вставку в PASTE_DB
.
Получить новый ключ uuid из базы данных KEY_DB.
Затем UUID удаляется из KEY_DB, чтобы избежать дублирования.
асинхронный createPaste (контент) {
пытаться {
const {keys} = await KEY_DB.list({limit: 1})
если (!ключи.длина) {
выбросить новый ApolloError('Кончились ключи')
const { имя: uuid } = ключи [0]
const createdOn = момент().формат()
const expireAt = moment().add(ONE_DAY_FROM_NOW, 'секунды').format()
await KEY_DB.delete(uuid) // Удалить ключ из KGS
ожидайте PASTE_DB.put (uuid, содержимое, {
метаданные: { createdOn, expireAt },
expireTtl: ONE_DAY_FROM_NOW,
вернуть {
уид,
содержание,
создано на,
истекает,
} поймать (ошибка) {
throw new ApolloError(Не удалось создать вставку. ${error.message}
)
Точно так же я также создал getPaste
[запрос GraphQL] (https://graphql.org/learn/queries/) для извлечения содержимого вставки через UUID. Мы не будем рассматривать это в этой статье, но вы можете проверить это в [исходном коде] (https://github.com/ngshiheng/paste-story/tree/main/server/src). Чтобы попробовать на игровой площадке:
запрос {
getPaste(uuid: "0pZUDXzd") {
UUID
содержание
создано на
expireAt
В этом POC мы не будем поддерживать удаление паст, поскольку срок их действия истекает через 24 часа.
Получение вставки
Всякий раз, когда пользователь посещает URL-адрес вставки (GET /:uuid
), исходное содержимое вставки должно быть возвращено. Если введен недопустимый URL-адрес, пользователи должны получить отсутствующий код ошибки. Просмотрите полный HTML-код [здесь] (https://github.com/ngshiheng/paste-story/blob/main/server/src/handlers/paste.js#L4).
```javascript
// /сервер/источник/обработчики/paste.js
const { отсутствует } = require("itty-router-extras");
постоянный момент = требуется ("момент");
константный обработчик = асинхронный ({uuid}) => {
const {значение: содержимое, метаданные} = await PASTE_DB.getWithMetadata(uuid);
если (!контент) {
возврат отсутствует("Неверная ссылка для вставки");
const expiringIn = момент (метаданные.expireAt).from(metadata.createdOn);
вернуть новый ответ (html (содержимое, срок действия), {
заголовки: { "Content-Type": "text/html" },
Наконец, чтобы запустить сервер API разработки локально, просто запустите wrangler dev
Развертывание вашего Pastebin
Перед публикацией кода вам нужно будет отредактировать файлы wrangler.toml
(внутри server/
и kgs/
) и добавить в них свой Cloudflare account_id
. Дополнительную информацию о настройке и публикации кода можно найти в официальной документации.
Убедитесь, что привязки пространства имен KV также добавлены в ваши файлы wrangler.toml
.
Чтобы опубликовать любые новые изменения в Cloudflare Worker, просто запустите wrangler publish
в соответствующем сервисе.
Чтобы развернуть приложение в личном домене, посмотрите этот короткий ролик.
CI/CD
В [репозитории GitHub] (https://github.com/ngshiheng/atomic-url) я также настроил рабочий процесс CI/CD с помощью GitHub Actions. Чтобы использовать действия Wrangler, добавьте CF_API_TOKEN в секреты репозитория GitHub.
Заключение: сборка Pastebin
Я не ожидал, что этот POC займет у меня так много времени, чтобы написать и завершить, я, вероятно, расслабился больше, чем должен.
Как и в предыдущем посте, я хотел бы закончить его некоторыми потенциальными улучшениями, которые могут быть сделаны (или затянуты в черную дыру отставания на вечность) в будущем:
- Разрешение пользователям устанавливать пользовательский срок действия
- Вставки редактирования и удаления
- Подсветка синтаксиса
- Аналитика
- Частные пасты с защитой паролем
Как и средства сокращения URL-адресов, инструменты Paste имеют определенное клеймо: оба инструмента делают URL-адреса непрозрачными, чем любят злоупотреблять спамеры. Что ж, по крайней мере, в следующий раз, когда вы спросите «почему этот код не работает?», у вас будет свой собственный инструмент вставки, по крайней мере, до тех пор, пока вы не добавите подсветку синтаксиса.
Оригинал