У цій статті я хочу поділитися своїм досвідом навчання системному проектуванню та моїми спробами спроектувати систему для скорочення URL (назвемо її paste.ly).
Обсяг
Ось кілька випадків використання системи для скорочення URL:
- Користувачі можуть вводити текст у вміст скороченого посилання
- Оновлення та видалення виключені для користувачів
- Користувачі анонімні
- Скорочене посилання має термін дії з за замовчуванням 5 хвилин
- Користувачі входять у систему скорочення URL для перегляду вмісту
- Користувачі можуть отримувати аналітику відвідувань скороченого URL
- Система видаляє застарілі URL-ссылки
- Система має високу доступність
Реалізація
Висока доступність вимагає:
- 5 мільйонів користувачів
- 5 мільйонів записів на місяць
- 50 мільйонів читань на місяць
З цих вимог можемо обчислити кількість запитів на секунду:
- ~2.5 мільйона секунд на місяць
- 1 запит на секунду = 2.5 мільйона запитів на місяць
- 2 запити на секунду = 5 мільйонів запитів на місяць
- 20 запитів на секунду = 50 мільйонів запитів на місяць
З цього розрахунку ми знаємо, що читань більше, ніж записів, і співвідношення читань до записів складає приблизно 10:1. Система повинна обробляти як мінімум 2 записи на секунду і 20 читань на секунду. За умови, що середній розмір вмісту пасти складає ~5 КБ, це буде близько 25 ГБ на місяць для дискового простору, необхідного для зберігання об'єктів.
Загальний дизайн
Загальний дизайн pastely V1
Поточний дизайн складається з:
- Сервісу
paste.ly
- Бази даних для аналітики
- Бази даних
paste.ly
для зберігання паст - Об'єктного зберігання для файлів
Сервіс paste.ly
обробляє всі вхідні запити, що надходять від балансувальника навантаження. Сервіс також буде відповідати за створення та отримання даних паст з бази даних paste.ly
за допомогою PostgreSQL як основної бази даних. Для кожного створення пасти текстовий файл буде завантажено в об'єктне зберігання, наприклад AWS S3, для зберігання вмісту пасти (для локальних тестів використовувався MinIO), а ключ об'єкта буде збережено в базі даних paste.ly
[1]. Для аналітики ми використаємо TimescaleDB, оскільки нам потрібні дані з часом.
Сервіс paste.ly має таку структуру:
.
├── cmd
├── config
├── deploy
│ ├── chart
│ │ ├── minio-pastely
│ │ ├── pastely
│ │ ├── postgres-operator
│ │ └── postgres-pastely
├── driver
│ ├── cache
│ ├── db
│ └── file-storage
│ ├── minio
│ └── s3
├── env.example
├── go.mod
├── go.sum
├── helper
│ ├── constant
│ ├── env
│ ├── logger
│ ├── pprof
│ ├── prometeus
│ └── transaction
├── internal
│ ├── v1
│ │ ├── model
│ │ ├── repository
│ │ ├── usecase
│ │ └── web
│ └── v2
│ ├── model
│ ├── repository
│ ├── usecase
│ └── web
├── main.go
├── migration
Ось повний код сервісу paste.ly
Створення Paste
При створенні пасти запит надходить від балансувальника навантаження і отримується сервісом paste.ly
. Всередині сервісу paste.ly
запит буде оброблений за допомогою Create API. Для генерування унікального короткого URL також буде оброблено на рівні use case в Create API. Після генерування короткого URL вміст пасти буде завантажено в об'єктне зберігання, а ключ об'єкта буде збережено в базі даних paste.ly
.
База даних paste.ly
матиме таблицю paste
.
Кожне коротке посилання повинно бути унікальним для кожної пасти, що буде забезпечено через таблицю та алгоритм, який ми використовуємо.
CREATE TABLE IF NOT EXISTS public.paste (
id bigserial NOT NULL,
shortlink varchar(7) NOT NULL,
paste_url varchar(255) DEFAULT ''::character varying NOT NULL,
created_at timestamp DEFAULT now() NOT NULL,
status varchar DEFAULT 'active'::character varying NULL,
expired_at timestamp NULL,
CONSTRAINT paste_pk PRIMARY KEY (id)
);
CREATE UNIQUE INDEX IF NOT EXISTS paste_shortlink_idx ON public.paste USING btree (shortlink);
Окрім використання унікального індексу, сервіс paste.ly
забезпечує унікальність короткого URL за допомогою поділу та операції модуля з базою 62. Чому саме база 62? Наше коротке посилання використовуватиме стандартний набір символів для URL, за винятком спеціальних символів [A-Za-z0-9]
, і цей набір містить 62 символи [3].
Використовуючи базу 62 і мінімальну комбінацію з 7 символів, ми отримуємо 62⁷, що становить близько 3,5 мільйона комбінацій. Ми використовуємо числа в десятковій системі для перетворення в базу 62. Ці числа отримуються шляхом комбінування UNIX timestamp та додавання до них поля первинного ключа serial
(ідентифікатора) з таблиці.
Структура даних пасти визначена нижче:
type Paste struct {
ID int64
Shortlink string
PasteURL string
CreatedAt time.Time
Status string
ExpiredAt time.Time
}
func (p *Paste) GenerateShortURLBase62() {
alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
num := time.Now().Unix() + p.ID
for num > 0 {
reminder := num % 62
p.Shortlink = string(alphabet[reminder]) + p.Shortlink
num = num / 62
}
}
Наприклад, timestamp 1735733728
та поточне збільшення для первинного ключа serial
становить 35
. Число в десятковій системі, яке потрібно перетворити в базу 62, дорівнює 1735733763
. Коли виконати функцію, вона поверне b3C7N1
, з деталями нижче:
1 * 62**5
55 * 62**4
28 * 62**3
59 * 62**2
39 * 62**1
53 * 62**0
------------- +
1735733763
Кожен залишок буде зіставлений з діапазоном символів, який ми визначаємо.
1 > b
55 > 3
28 > C
59 > 7
39 > N
53 > 1
Перегляд Paste
Коли користувач намагається отримати доступ до короткого URL, балансувальник навантаження передає запит на сервіс paste.ly
. Всередині сервісу paste.ly
запит обробляється через Read API. Дані пасти будуть отримані за допомогою унікального короткого URL. Після отримання даних пасти вміст буде також отримано з об'єктного зберігання. Коли всі ці операції завершено, дані будуть кешовані у фоновому режимі, а запит буде зареєстровано для аналітики.
Аналітика зберігатиметься в PostgreSQL з використанням розширення TimescaleDB, що перетворює таблицю на часоряди даних з партиціонуванням, яке обробляється TimescaleDB.
CREATE TABLE IF NOT EXISTS paste_log (
time TIMESTAMPTZ NOT NULL,
shortlink text not null
);
SELECT create_hypertable('paste_log', by_range('time'));
Видалення Paste
Видалення паст буде виконуватися щодня опівночі або коли звичайний трафік низький. Ця реалізація використовує об'єкт CronJob в Kubernetes. CronJob буде викликати Delete API, який оновить застарілі пасти.
Навіть якщо описана реалізація виглядає нормально, ми можемо зробити краще. Ми збільшимо масштаб поточного дизайну, намагаючись підвищити його доступність при збереженні консистентності.
Глибоке занурення
Загальний дизайн pastely v2
Для покращення продуктивності необхідно зробити кілька кроків. По-перше, ми налаштуємо кеш для Read API. В нашому випадку ми можемо реалізувати стратегію Cache-aside, яка буде кешувати тільки ті пасти, які часто запитуються.
По-друге, ми впровадимо тип бази даних master-replica/slave для нашої основної бази даних.
Оскільки ми розгорнули наш сервіс і його залежності в Kubernetes, ми можемо впровадити Postgres Operator для управління нашою ситуацією з підтримкою доступності та консистентності [4]. Postgres Operator буде керувати балансуванням навантаження, пулом з'єднань та потоком реплікації для нашої основної бази даних.
Щоб ще більше підвищити продуктивність, ми можемо використовувати попередньо підписані URL-адреси [5]. Замість того, щоб сервіс paste.ly
завантажував або отримував файл безпосередньо з об'єктного зберігання, він надаватиме клієнту захищене посилання для завантаження або завантаження файлу. Використовуючи попередньо підписану URL-адресу, ми також можемо обробляти навіть великі файли, але недоліком цього підходу є те, що нам потрібно більше обробників, коли ми хочемо впровадити CDN.
З цією реалізацією ми задовольняємо вимоги до доступності для v2 дизайну.
Цей тест навантаження був виконаний на моєму локальному комп'ютері з наступними характеристиками:
OS: MacOS
Chip: Apple M2
Memory: 8GB
Storage: SSD
Висновки
Щоб обробляти 20 запитів на читання в секунду, нам потрібно встановити запобігання високому піковому трафіку на конкретну популярну пасту. Це можна обробити за допомогою кешування, а також поєднати з Replica Read SQL, що має допомогти в разі пропусків кешу, якщо немає проблем з відставанням репліки. Для обробки 2 записів на секунду достатньо буде одного основного SQL. Для об'єктного зберігання AWS S3 здатен обробляти 25 ГБ трафіку на місяць, при цьому є варіанти для покращення продуктивності, такі як CDN з CloudFront або якщо нам потрібні ще швидші завантаження та завантаження на великі відстані з AWS Transfer Acceleration, що, звісно, буде коштувати дорожче [7].
Сподіваюся, що ця стаття надасть вам деяке розуміння при розробці подібних випадків використання. Дякую і гарного дня!
Джерела
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
- https://github.com/kadekchresna/pastely
- https://stackoverflow.com/a/1856809/6953158
- https://postgres-operator.readthedocs.io/en/stable/
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- https://stackoverflow.com/questions/742013/how-do-i-create-a-url-shortener
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimizing-performance-design-patterns.html#optimizing-performance-acceleration
Перекладено з: System design use-case: URL Shortener