- Регистрация
- 9 Май 2015
- Сообщения
- 1,071
- Баллы
- 155
- Возраст
- 51
Рассказывает
Интересное требование возникло на работе, когда мы обсуждали потенциальную необходимость запуска собственного сокращателя URL, потому что механизм (в iOS 9 и выше) требует JSON-манифест на https://domain.com/apple-app-site-association.
Поскольку ОС не следует переадресациям, этот манифест должен быть размещен в корневом домене URL-сокращателя.
Из-за на AppsFlyer он не может сокращать URL, когда в приложении настроены универсальные ссылки. Мы могли бы перейти на другого поставщика, но это означало бы большую нагрузку для клиентских приложений.
Возник вопрос «Стоит ли создавать свой сокращатель ссылок?», который затем плавно перешел в «Насколько тяжело создать расширяемый сокращатель ссылок в 2017 году?».
Оказалось, что совсем не трудно, так как на то, чтобы внедрить, протестировать и развернуть, у меня ушло менее двух часов.
Вот так выглядит сокращатель
Lambda во имя победы
Для нашего сокращателя нам необходимы следующие вещи:
- Конечная точка GET/{shortUrl}, которая будет перенаправлять на оригинальный URL.
- Конечная точка POST/, которая будет принимать оригинальный URL и возвращать сокращенный.
- index.html — страница, где каждый мог бы легко создать короткий URL.
- Конечная точка GET/apple-app-site-association, которая обслуживает статичный JSON-ответ.
Все эти вещи могут быть достигнуты с помощью API Gateway и Lambda.
Прим. перев. Чтобы иметь представление о всём спектре сервисов, предоставляемых платформой Amazon Web Services (AWS), советуем прочитать .
У меня получилась следующая структура проекта:
- используется шаблон aws-nodejs фреймворка ;
- каждая конечная точка имеет соответствующую функцию обработки;
- файл index.html в статичной папке;
- тесты написаны таким образом, что могут использоваться ;
- скрипт build.sh, облегчающий работу;
- интеграционные тесты ./build.sh int-test {env} {region} {aws_profile};
- приемочные тесты ./build.sh acceptance-test {env} {region} {aws_profile};
- разворачивание ./build.sh deploy {env} {region} {aws_profile}.
Структура проекта
Конечная точка GET/apple-app-site-association
Так как JSON статичный, есть смысл вычислить HTTP-ответ заранее и возвращать его каждый раз:
'use strict';
const payload = {...
};
const response = {
statusCode: 200,
body: JSON.stringify(payload)
};
module.exports.handler = (event, context, callback) => {
callback(null, response);
};
Конечная точка POST/
Для алгоритма сокращения URL можно найти простое и элегантное на StackOverflow. Вам нужен только автоматически увеличивающийся ID, вроде того, что вы обычно получаете с помощью RDBMS.
Однако я заметил, что DynamoDB будет более подходящей базой данных по следующим причинам:
- это управляемый сервис, поэтому не надо волноваться об инфраструктуре;
- расходы лучше ;
- есть возможность масштабировать пропускную способность чтения и записи, чтобы соответствовать уровню использования и обрабатывать любые скачки трафика.
Но в DynamoDB нет такого понятия, как автоматически увеличивающийся ID, который необходим для алгоритма. Вместо него вы можете использовать для симуляции автоматического увеличения за счет дополнительной единицы записи за запрос.
function* getNewId () {
console.log('fetching the next auto-incremented ID');
let params = {
TableName: 'url_shortener_long_urls',
Key: {
shortUrl: "__id"
},
UpdateExpression: 'add #counter :n',
ExpressionAttributeNames: {
'#counter': 'counter'
},
ExpressionAttributeValues: {
':n': 1
},
ReturnValues: 'UPDATE_NEW'
};
let res = yield dynamodb.updateAsync(params);
console.log(res);
return res.Attributes.counter;
}
Получится такая таблица
Конечная точка GET/{shortUrl}
Как только мы установили соответствие в таблице DynamoDB, конечная точка перенаправления стала простым взятием оригинального URL и возвращением его как части заголовка Location.
И не забудьте возвращать соответствующий статус коду HTTP, в данном случае 308 (обязательное перенаправление).
module.exports.handler = co.wrap(function* (event, context, callback) {
console.log(JSON.stringify(event));
try {
let shortUrl = event.pathParameters.shortUrl;
console.log('short url : ${shortUrl}');
let longUrl = yield getLongUrl(shortUrl);
console.log('long url : ${longUrl}');
const response = {
statusCode: 308,
headers: { location: longUrl }
};
callback(null, response);
} catch (err) {
console.log(err);
if (err.statusCode) {
callback(null, err);
} else {
callback(null, {
statusCode: 500,
body: JSON.stringify(err)
});
}
}
});
Страница GET/index
Наконец, для страницы index мы будем возвращать HTML (и другой тип контента вместе с HTML).
Я решил поместить HTML-файл в статичную папку, которая загружается и кэшируется в первый раз, когда вызывается функция.
const co = require('co');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require("fs"));
var html;
function* loadHtml () {
if (!html) {
console.log('loading index.html...');
html = fs.readFileAsync('static/index.html', 'utf-8');
console.log('loaded');
}
return htnl;
}
module.exports.handler = co.wrap(function* (event, context, callback) {
let html = yield loadHtml();
let response = {
statusCode: 200,
headers: {
'Content-Type': 'text/html; charset=UTF-8'
},
body: html
};
callback(null, response);
});
Подготовка к работе
К счастью, у меня было много опыта , поэтому я знаю, что для URL-сокращателя нам необходимо:
- настроить автомасштабируемые параметры для таблицы DynamoDB (для которой у нас есть внутренняя система управления автоматическим масштабированием);
- включить кэширование для API Gateway на производственном уровне.
Если поместить один и тот же URL несколько раз, то вы будете получать разные короткие ссылки. Для оптимизации нужно возвращать одну и ту же ссылку, используя кэш.
Чтобы это сделать, вы можете:
- Добавить GSI в таблице DynamoDB к longUrl для поддержки эффективного обратного поиска.
- В функции shortUrl выполнять GET вместе с GSI для поиска существующих коротких URL.
Более подходящим вариантом будет добавление GSI, нежели создание новой таблицы. Это поможет избежать транзакций между несколькими таблицами.
— .