Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Консервативный
Backend
на Node.js
Ляпин Дмитрий
Версия 5
Recrumatic
платформа для рекрутёров
2015 2016 2017 2018
Начало v1 v2 v3
О системе
• SaaS
• 3 инструмента:
One-Way-Video
Quiz
Automatic Scheduler
One-Way-Video
Quiz
Automatic Scheduler
Кандидаты
Система крупным планом
Frontend
(SPA)
Backend
(REST API)
Система крупным планом
Frontend
(SPA)
Backend
(REST API)
4 истории
про архитектуру backend-а
4 истории
про простоту и надёжность
Node.js и REST API
История #1
Почему Node.js
• Full Stack
• Async I/O
• Single Thread
Классическая модель
Req1
Thread1
t
Классическая модель
Req1
Thread1
t
Классическая модель
Req1
Thread1
t
Классическая модель
Req1
Thread1
t
Классическая модель
Req1
Thread1
t
Классическая модель
Req1
Thread1
t
Res1
Классическая модель
Req1
Thread1
Req2
Thread2
t
Res1
Res2
I/O – это дорого
L1-cache 0.5 ns
L2-cache 7 ns
RAM 100 ns
SSD 150 000 ns
Network 500 000 ns
Классическая модель
Req1
Thread1
Req2
Thread2
t
Res1
Res2
Async I/O
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1 Req2
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1 Req2
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1 Req2
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1 Req2
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1 Req2 Res2
Single Thread
Event Loop + Thread Pool
t
Async I/O
Req1 Req2 Res2 Res1
Single Thread
Event Loop + Thread Pool
t
Претензии к Node.js
• Single Thread
• “Callback hell”
Претензии к Node.js
• Single Thread
• “Callback hell”
Не нужны блокировки –
проще код
4 реализации
// Sign in by password:
router.post('/signin', (req, res, next) => {
// req.body.email
// req.body.password
/...
“Callback hell”
(29 строк)
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email.toLo...
“Callback hell”
(29 строк)
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email.toLo...
“Callback hell”
(29 строк)
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email.toLo...
“Callback hell”
(29 строк)
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email.toLo...
“Callback hell”
(29 строк)
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email.toLo...
Библиотека “async”
(26 строк)
var account;
async.waterfall([
cb => {
var sql = 'SELECT id, password FROM account WHERE ema...
var account;
async.waterfall([
cb => {
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.bod...
Promises
(27 строк)
var account;
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.emai...
Promises
(27 строк)
var account;
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.emai...
async / await
(18 строк)
try {
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email....
async / await
(18 строк)
try {
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email....
async / await
(18 строк)
try {
var sql = 'SELECT id, password FROM account WHERE email = ?';
var params = [req.body.email....
Проектирование REST-путей
Проектирование REST-путей
• Дай информацию о компании #123:

GET /companies/123
Проектирование REST-путей
• Дай информацию о компании #123:

GET /companies/123
• Теперь дай все интервью этой компании:

...
Проектирование REST-путей
• Дай информацию о компании #123:

GET /companies/123
• Теперь дай все интервью этой компании:

...
Проектирование REST-путей
GET /companies/123/interviews/987/answers

GET /interviews/987/answers
Используйте минимально во...
Резюме #1
1. Не блокируйте единственный поток
2. Используйте async/await
3. Убирайте лишние ID из REST-путей
Данные и SQL
История #2
Данные – это самое главное
Почему PostgreSQL
• Реляционная БД:
Свобода при анализе данных, SQL
MVCC, транзакции
• Сообщество
• JSONB
SQL vs ORM
• ORM генерирует много запросов
• ORM генерирует неэффективные запросы
• “Mapping” – не нужно для JavaScript
• ...
4 помощника
1. db.js
2. VIEWS
3. TRIGGERS
4. Красивая схема
var sql = `
SELECT
a.id,
a.email,
a.name,
a.avatar_bucket,
a.avatar_object
FROM hiring_team ht
JOIN v_account a ON a.id = ...
var sql = `
SELECT status_id
FROM interview
WHERE id = ?
FOR UPDATE
`;
db.queryValue(sql, [id], cb);
db.insert('comment', {
text: req.body.text,
created_by: req.user.id
}, cb);
db.delete('comment', commentId, cb);
VIEWS
CREATE OR REPLACE VIEW v_fact AS
SELECT
f.id,
f.company_id,
f.is_trial,
f.opened_on,
f.closed_on,
f.opened_on <= now...
TRIGGERS
CREATE OR REPLACE FUNCTION photo_before_all() RETURNS trigger AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE media S...
Красивая схема
JSONB
Резюме #2
1. SQL – мощный и выразительный язык
2. Нарисуйте схему БД
3. JSONB позволяет сократить количество таблиц и стол...
JWT
История #3
Что такое JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI
1NiJ9.eyJpZCI6NCwiaWF0IjoxNDk2
MjE5OTc5LCJleHAiOjE0OTg4MTE5
Nzl9.8npIS2y73jf...
Как мы используем JWT
1. Сессии
2. Гостевой доступ
3. Загрузка файлов
JWT-сессии
Client API
POST /auth/signin
{email, password}
JWT-сессии
Client API
POST /auth/signin
OK, {token}
{email, password}
JWT-сессии
Client API
POST /auth/signin
Следующий запрос
Authorization: Bearer token
{email, password}
OK, {token}
JWT-сессии
• Сессии не нужно хранить на сервере
• Сессиями нельзя управлять (почти)
JWT и гостевой доступ
https://app.recrumatic.com/d8iXZh4X
JWT и гостевой доступ
Client API
POST /links/d8iXZh4X/click
JWT и гостевой доступ
Client API
OK, {token, …}
POST /links/d8iXZh4X/click
JWT и гостевой доступ
Client API
POST /links/d8iXZh4X/click
OK, {token, …}
GET /interviews/987
Authorization: Bearer token
JWT и загрузка файлов
Client API
JWT и загрузка файлов
Client API
POST /media
JWT и загрузка файлов
Client API
POST /media
OK, {token}
JWT и загрузка файлов
Client API
POST /media
PUT /companies/123
{logo: token}
OK, {token}
Резюме #3
1. JWT можно использовать вместо сессий, но не всегда
2. JWT удобен и для других задач
3. Signature = f(Payload,...
Медиа-сервис
История #4
Картинки и их размеры
800 x 534
400 x 267
300 x 300
Что умеет наш сервис
GET /media/images/ZVcMOPyJWXahP9sB.jpg?w=440&h=240
Что умеет наш сервис
GET /media/images/ZVcMOPyJWXahP9sB.jpg?w=440&h=240
• Проверить кэш
• Если там нет:
• Взять оригинал
•...
Что умеет наш сервис
GET /media/images/ZVcMOPyJWXahP9sB.jpg?w=440&h=240
https://recrumatic-cache.s3.amazonaws.com/ZVcMOPyJ...
Выводы
1. Хороший API не знает о размерах картинок
2. JWT удобен не только как замена сессиям
3. Не используйте ORM
4. Sin...
Спасибо!
• dmlyapin@yandex.ru
• https://www.facebook.com/dmlyapin
• http://t.me/dmlyapin
Upcoming SlideShare
Loading in …5
×

Консервативный Backend на Node.js / Дмитрий Ляпин (Recrumatic)

438 views

Published on

РИТ++ 2017, Backend Conf
Зал Кейптаун, 5 июня, 14:00

Тезисы:
http://backendconf.ru/2017/abstracts/2510.html

Я расскажу об опыте разработки REST API сервиса одной рекрутинговой платформы. Стремясь найти простое и масштабируемое решение, мы выбираем PostgreSQL и Node.js, а вместо сессий используем JWT-токены. Избегая ORM, мы пишем большие и сложные, но эффективные SQL-запросы. На помощь приходят SQL-представления, триггеры и небольшая собственная JS-библиотека.
...

Published in: Engineering
  • Be the first to comment

  • Be the first to like this

Консервативный Backend на Node.js / Дмитрий Ляпин (Recrumatic)

  1. 1. Консервативный Backend на Node.js Ляпин Дмитрий Версия 5
  2. 2. Recrumatic платформа для рекрутёров 2015 2016 2017 2018 Начало v1 v2 v3
  3. 3. О системе • SaaS • 3 инструмента: One-Way-Video Quiz Automatic Scheduler
  4. 4. One-Way-Video
  5. 5. Quiz
  6. 6. Automatic Scheduler
  7. 7. Кандидаты
  8. 8. Система крупным планом Frontend (SPA) Backend (REST API)
  9. 9. Система крупным планом Frontend (SPA) Backend (REST API)
  10. 10. 4 истории про архитектуру backend-а
  11. 11. 4 истории про простоту и надёжность
  12. 12. Node.js и REST API История #1
  13. 13. Почему Node.js • Full Stack • Async I/O • Single Thread
  14. 14. Классическая модель Req1 Thread1 t
  15. 15. Классическая модель Req1 Thread1 t
  16. 16. Классическая модель Req1 Thread1 t
  17. 17. Классическая модель Req1 Thread1 t
  18. 18. Классическая модель Req1 Thread1 t
  19. 19. Классическая модель Req1 Thread1 t Res1
  20. 20. Классическая модель Req1 Thread1 Req2 Thread2 t Res1 Res2
  21. 21. I/O – это дорого L1-cache 0.5 ns L2-cache 7 ns RAM 100 ns SSD 150 000 ns Network 500 000 ns
  22. 22. Классическая модель Req1 Thread1 Req2 Thread2 t Res1 Res2
  23. 23. Async I/O Single Thread Event Loop + Thread Pool t
  24. 24. Async I/O Req1 Single Thread Event Loop + Thread Pool t
  25. 25. Async I/O Req1 Single Thread Event Loop + Thread Pool t
  26. 26. Async I/O Req1 Req2 Single Thread Event Loop + Thread Pool t
  27. 27. Async I/O Req1 Req2 Single Thread Event Loop + Thread Pool t
  28. 28. Async I/O Req1 Req2 Single Thread Event Loop + Thread Pool t
  29. 29. Async I/O Req1 Req2 Single Thread Event Loop + Thread Pool t
  30. 30. Async I/O Req1 Req2 Res2 Single Thread Event Loop + Thread Pool t
  31. 31. Async I/O Req1 Req2 Res2 Res1 Single Thread Event Loop + Thread Pool t
  32. 32. Претензии к Node.js • Single Thread • “Callback hell”
  33. 33. Претензии к Node.js • Single Thread • “Callback hell” Не нужны блокировки – проще код
  34. 34. 4 реализации // Sign in by password: router.post('/signin', (req, res, next) => { // req.body.email // req.body.password // => // 200 OK + JSON // или // 401 Unauthorized });
  35. 35. “Callback hell” (29 строк) var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params, (err, account) => { if (err) { next(err); return; } if (!account) { res.sendStatus(401); return; } bcrypt.compare(req.body.password, account.password, (err, isMatch) => { if (err) { next(err); return; } if (!isMatch) { res.sendStatus(401); return; } getAuthResponse(account.id, (err, json) => { if (err) { next(err); return; } res.send(json); }); }); });
  36. 36. “Callback hell” (29 строк) var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params, (err, account) => { if (err) { next(err); return; } if (!account) { res.sendStatus(401); return; } bcrypt.compare(req.body.password, account.password, (err, isMatch) => { if (err) { next(err); return; } if (!isMatch) { res.sendStatus(401); return; } getAuthResponse(account.id, (err, json) => { if (err) { next(err); return; } res.send(json); }); }); });
  37. 37. “Callback hell” (29 строк) var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params, (err, account) => { if (err) { next(err); return; } if (!account) { res.sendStatus(401); return; } bcrypt.compare(req.body.password, account.password, (err, isMatch) => { if (err) { next(err); return; } if (!isMatch) { res.sendStatus(401); return; } getAuthResponse(account.id, (err, json) => { if (err) { next(err); return; } res.send(json); }); }); });
  38. 38. “Callback hell” (29 строк) var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params, (err, account) => { if (err) { next(err); return; } if (!account) { res.sendStatus(401); return; } bcrypt.compare(req.body.password, account.password, (err, isMatch) => { if (err) { next(err); return; } if (!isMatch) { res.sendStatus(401); return; } getAuthResponse(account.id, (err, json) => { if (err) { next(err); return; } res.send(json); }); }); });
  39. 39. “Callback hell” (29 строк) var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params, (err, account) => { if (err) { next(err); return; } if (!account) { res.sendStatus(401); return; } bcrypt.compare(req.body.password, account.password, (err, isMatch) => { if (err) { next(err); return; } if (!isMatch) { res.sendStatus(401); return; } getAuthResponse(account.id, (err, json) => { if (err) { next(err); return; } res.send(json); }); }); });
  40. 40. Библиотека “async” (26 строк) var account; async.waterfall([ cb => { var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params, cb); }, (row, cb) => { if (!row) { res.sendStatus(401); return; } account = row; bcrypt.compare(req.body.password, account.password, cb); }, (isMatch, cb) => { if (!isMatch) { res.sendStatus(401); return; } getAuthResponse(account.id, cb); }, json => { res.send(json); } ], next);
  41. 41. var account; async.waterfall([ cb => { var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params, cb); }, (row, cb) => { if (!row) { res.sendStatus(401); return; } account = row; bcrypt.compare(req.body.password, account.password, cb); }, (isMatch, cb) => { if (!isMatch) { res.sendStatus(401); return; } getAuthResponse(account.id, cb); }, json => { res.send(json); } ], next); Библиотека “async” (26 строк)
  42. 42. Promises (27 строк) var account; var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params) .then(row => { if (!row) { res.sendStatus(401); throw 'break'; } account = row; return bcrypt.compare(req.body.password, account.password); }) .then(isMatch => { if (!isMatch) { res.sendStatus(401); throw 'break'; } return getAuthResponse(account.id); }) .then(json => { res.send(json); }) .catch(err => { if (err !== 'break') { next(err); } });
  43. 43. Promises (27 строк) var account; var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; db.queryRow(sql, params) .then(row => { if (!row) { res.sendStatus(401); throw 'break'; } account = row; return bcrypt.compare(req.body.password, account.password); }) .then(isMatch => { if (!isMatch) { res.sendStatus(401); throw 'break'; } return getAuthResponse(account.id); }) .then(json => { res.send(json); }) .catch(err => { if (err !== 'break') { next(err); } });
  44. 44. async / await (18 строк) try { var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; var account = await db.queryRow(sql, params); if (!account) { res.sendStatus(401); return; } var isMatch = await bcrypt.compare(req.body.password, account.password); if (!isMatch) { res.sendStatus(401); return; } var json = await getAuthResponse(account.id); res.send(json); } catch (err) { next(err); } Node 7.6
  45. 45. async / await (18 строк) try { var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; var account = await db.queryRow(sql, params); if (!account) { res.sendStatus(401); return; } var isMatch = await bcrypt.compare(req.body.password, account.password); if (!isMatch) { res.sendStatus(401); return; } var json = await getAuthResponse(account.id); res.send(json); } catch (err) { next(err); } Node 7.6
  46. 46. async / await (18 строк) try { var sql = 'SELECT id, password FROM account WHERE email = ?'; var params = [req.body.email.toLowerCase()]; var account = await db.queryRow(sql, params); if (!account) { res.sendStatus(401); return; } var isMatch = await bcrypt.compare(req.body.password, account.password); if (!isMatch) { res.sendStatus(401); return; } var json = await getAuthResponse(account.id); res.send(json); } catch (err) { next(err); } Node 8.0: util.promisify(original) Node 7.6
  47. 47. Проектирование REST-путей
  48. 48. Проектирование REST-путей • Дай информацию о компании #123:
 GET /companies/123
  49. 49. Проектирование REST-путей • Дай информацию о компании #123:
 GET /companies/123 • Теперь дай все интервью этой компании:
 GET /companies/123/interviews
  50. 50. Проектирование REST-путей • Дай информацию о компании #123:
 GET /companies/123 • Теперь дай все интервью этой компании:
 GET /companies/123/interviews • А теперь давай ответы из интервью #987:
 GET /companies/123/interviews/987/answers
  51. 51. Проектирование REST-путей GET /companies/123/interviews/987/answers
 GET /interviews/987/answers Используйте минимально возможное количество ID
  52. 52. Резюме #1 1. Не блокируйте единственный поток 2. Используйте async/await 3. Убирайте лишние ID из REST-путей
  53. 53. Данные и SQL История #2
  54. 54. Данные – это самое главное
  55. 55. Почему PostgreSQL • Реляционная БД: Свобода при анализе данных, SQL MVCC, транзакции • Сообщество • JSONB
  56. 56. SQL vs ORM • ORM генерирует много запросов • ORM генерирует неэффективные запросы • “Mapping” – не нужно для JavaScript • SQL – хороший декларативный язык
  57. 57. 4 помощника 1. db.js 2. VIEWS 3. TRIGGERS 4. Красивая схема
  58. 58. var sql = ` SELECT a.id, a.email, a.name, a.avatar_bucket, a.avatar_object FROM hiring_team ht JOIN v_account a ON a.id = ht.account_id WHERE ht.activity_id = ? ORDER BY a.name `; db.query(sql, [id], cb);
  59. 59. var sql = ` SELECT status_id FROM interview WHERE id = ? FOR UPDATE `; db.queryValue(sql, [id], cb);
  60. 60. db.insert('comment', { text: req.body.text, created_by: req.user.id }, cb);
  61. 61. db.delete('comment', commentId, cb);
  62. 62. VIEWS CREATE OR REPLACE VIEW v_fact AS SELECT f.id, f.company_id, f.is_trial, f.opened_on, f.closed_on, f.opened_on <= now() AND now() < f.closed_on AS is_active, now() < f.opened_on AS in_future, ( SELECT -sum(amount) FROM credit_transaction WHERE fact_id = f.id ) AS credits FROM fact f;
  63. 63. TRIGGERS CREATE OR REPLACE FUNCTION photo_before_all() RETURNS trigger AS $$ BEGIN IF TG_OP = 'INSERT' THEN UPDATE media SET links = links + 1 WHERE id = NEW.media_id; RETURN NEW; ELSIF TG_OP = 'UPDATE' THEN UPDATE media SET links = links - 1 WHERE id = OLD.media_id; UPDATE media SET links = links + 1 WHERE id = NEW.media_id; RETURN NEW; ELSIF TG_OP = 'DELETE' THEN UPDATE media SET links = links - 1 WHERE id = OLD.media_id; RETURN OLD; END IF; END; $$ LANGUAGE plpgsql;
  64. 64. Красивая схема
  65. 65. JSONB
  66. 66. Резюме #2 1. SQL – мощный и выразительный язык 2. Нарисуйте схему БД 3. JSONB позволяет сократить количество таблиц и столбцов
  67. 67. JWT История #3
  68. 68. Что такое JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI 1NiJ9.eyJpZCI6NCwiaWF0IjoxNDk2 MjE5OTc5LCJleHAiOjE0OTg4MTE5 Nzl9.8npIS2y73jfuGWDNPTvRKfdQ qKNlC_8ymUwVBCtFUP4 { "typ": "JWT", "alg": "HS256" } { "iat": 1496219979, "exp": 1498811979, "id": 4 }Signature = f(Header, Payload, Secret)
  69. 69. Как мы используем JWT 1. Сессии 2. Гостевой доступ 3. Загрузка файлов
  70. 70. JWT-сессии Client API POST /auth/signin {email, password}
  71. 71. JWT-сессии Client API POST /auth/signin OK, {token} {email, password}
  72. 72. JWT-сессии Client API POST /auth/signin Следующий запрос Authorization: Bearer token {email, password} OK, {token}
  73. 73. JWT-сессии • Сессии не нужно хранить на сервере • Сессиями нельзя управлять (почти)
  74. 74. JWT и гостевой доступ
  75. 75. https://app.recrumatic.com/d8iXZh4X
  76. 76. JWT и гостевой доступ Client API POST /links/d8iXZh4X/click
  77. 77. JWT и гостевой доступ Client API OK, {token, …} POST /links/d8iXZh4X/click
  78. 78. JWT и гостевой доступ Client API POST /links/d8iXZh4X/click OK, {token, …} GET /interviews/987 Authorization: Bearer token
  79. 79. JWT и загрузка файлов Client API
  80. 80. JWT и загрузка файлов Client API POST /media
  81. 81. JWT и загрузка файлов Client API POST /media OK, {token}
  82. 82. JWT и загрузка файлов Client API POST /media PUT /companies/123 {logo: token} OK, {token}
  83. 83. Резюме #3 1. JWT можно использовать вместо сессий, но не всегда 2. JWT удобен и для других задач 3. Signature = f(Payload, Secret)
  84. 84. Медиа-сервис История #4
  85. 85. Картинки и их размеры 800 x 534 400 x 267 300 x 300
  86. 86. Что умеет наш сервис GET /media/images/ZVcMOPyJWXahP9sB.jpg?w=440&h=240
  87. 87. Что умеет наш сервис GET /media/images/ZVcMOPyJWXahP9sB.jpg?w=440&h=240 • Проверить кэш • Если там нет: • Взять оригинал • ImageMagick • Положить в кэш • Редирект
  88. 88. Что умеет наш сервис GET /media/images/ZVcMOPyJWXahP9sB.jpg?w=440&h=240 https://recrumatic-cache.s3.amazonaws.com/ZVcMOPyJWXahP9sB/440x240.jpg redirects to
  89. 89. Выводы 1. Хороший API не знает о размерах картинок 2. JWT удобен не только как замена сессиям 3. Не используйте ORM 4. Single Thread + Async I/O = ♥
  90. 90. Спасибо! • dmlyapin@yandex.ru • https://www.facebook.com/dmlyapin • http://t.me/dmlyapin

×