Галоўная / Артыкулы

Чаму код для бэкенду, який працюе локальна, зламваецца пад справжнім навантажэнням у працэйной среде.

Практычны ўзвешчанні пра аптэкую, базу дадзеных, захаванне і надзею на надзейнасць, якія спакойна працуюць на localhost, але вызвалі перерывы ў роботе, калі пачынаецца справжній трафік у працэйной среде.

Выдавец: REACTAPP.TOP39 хв чытання8945 слоў

Крыніцы: 10 Backend Mistakes That Work Perfectly on localhost but Break in Production

Ён працуе ідеальна на вашам комп’ютеры. Але чы гэта будзе таксама працаваць, калі да яго падые рэальны трафік? Ёсць 10 адміністрацыйных памылак, якія можу таямніча ператворыць гладка працуючыя прыкладніку ў катастрофу у працэйным сераверы.

У локальных умовах всё выглядае нормальна. Запыткі вяртаюцца быстра. Запиты даўаюць правильныя данні. Автарызацыя працуе. Docker запускаецца без проблем. Тэсты праходзяць. А потым вы розгорнулі прыкладнік, ачынаеце жывой URL, і ўсё ламаецца.

Інодзе прыкладнік адмовляецца запускацца.

Інодзе корыстувальнікі не можаць заўязацца у своіх аблікаваннях.

Інодзе база дадзеных раптам пачынае працаваць вельмі повалка.

Інодзе всё працуе гладка калькі хвілін, а потым пачынаюць накаплявацца бяговыя праблемы заўязку.

А інодзе сервіс ззовні выглядае абсалютна нормальны, але пад спудам заводзіць таямнічыя проблемы.

Тое, што гэта так разчароўвае, — гэта тое, што большасць гэтых неудач не мае нічынага з складнымі алгорытамамі.

Яны выходзяць з маленькіх, непераканальваных прыязвенняў.

Такія прыязвенні, як:

  • "У нас не будзе так много трафіку."
  • "База дадзеных знаходзіцца на той самый серверы."
  • "Этая зменная сераўіса завжды будзе заданая."
  • "Одна з’яўленне да базы дадзеных — гэта прыемна."
  • "Запросы завжды будуць выкананыя быстра."
  • "Файлы, захаваныя на дыску, будуць там і пазней."
  • "Ніхто не будзе атакаваць гэты канец паставы."
  • "API завжды будзе получаць правільныя даннэ.
  • "У контейнеры ніколі не будзе нехапак памяці."
  • "Рабочы процес означае стабільны прыкладнік."

Ваша локальная наладка захоўвае вас ад большай часткі гэтых проблем.

У продакшн-сераўісе такой захад няма.

У этай статыцы рассматрываюцца 10 адзінакоў у работе бэкенду, якія зазвычай працуюць без проблем на localhost, але спрычыняюць серйозныя проблемы пасля развяртання.

Ні аднаго з іх не ёсць абстрактным чы рэчыянай.

Это тые штучкі, якія здаюцца абсалютна разумнымі, калі вы заняты стварэнням новай функцыі.

Саме гэта і робіць іх апаснымі.

1. Адносленне да зменных сяродавішча як да неабязковых налаштованняў

Пачнём з аднаго з найчастэйшых працяў.

Спадзяймося, вы ствараеце API на сваём ноутбуку.

Настройкі з’яўлення до базы дадзенаў выглядаюць так:

DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp
JWT_SECRET=my-super-secret-key
PORT=5000

Програма запускаецца нормальна.

Нічога не здаецца некоректным.

Вы выкладзаете яе у працоўны режым.

І тады, без жадных праблем:

Error: password authentication failed

А можа:

JWT_SECRET is undefined

Або:

ECONNREFUSED 127.0.0.1:5432

Ваша першая інстынктыўная реакція можа быть зваліць віну на платформу для размешчэння.

Гэта рэдкая прычына насправе.

На самай працо выканалася тое, што ваша аплікацыя таямніча павязвалася на настройкі, якія існавалі толькі на вашай машыне для разработки.

Пастка localhost

У локальных умовах у вас можа быць ў такім выглядзе:

DATABASE_URL=postgresql://localhost/myapp

А ў умовах прыемнай експлуатацыі настройкі выглядаюць так:

DATABASE_URL=postgresql://db.internal:5432/myapp

Это два абсалютна разныя наладжэння.

Ситуацыя пагіршваецца, калі разработчыкі вбудовуюць настройкі безпосередня ў код аплікацыі:

const dbHost = "localhost";
const dbUser = "postgres";
const dbPassword = "password";

Это працюе чыста адлічна.

Пакуль толькі не перестае працаваць.

Умовы прыемнай експлуатацыі — гэта не ваш лептоп.

База дадзеных можа знаходзіцца на:

  • атрыбутны сервер
  • іншы контейнер
  • паслявальнік базы дадзеных
  • прыняцоўскай внутраней сеті
  • іншай зоне доступнасці
  • абсалютна іншым паслявальніку ў хмаре

Ваша аплікацыя патрэбуе спосабу, каб выявіць, дзе насправе знаходзіцца тая база дадзеных.

Канфігурацыя должна выкліквачыцца з атмосферы

Болей стойкі патэрн выглядае так:

const config = {
  databaseUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET,
  port: process.env.PORT || 5000
};

Але тут яшчэ застаёцца болей тонкія памылка.

Лiчна выцягванне значэнняў з зменных атмосферы сама па сабе недастатня.

Таксама трэба пераканацца, што яны дзейсна ўсё ж такі ў наявнасці.

Напрыклад:

const requiredEnv = [
  "DATABASE_URL",  "JWT_SECRET"
];
for (const key of requiredEnv) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
}

Тепер прыяплет не можа запусціцца, якщо не хватает неабходнай настройкі.

Такая ранняя памылка на самай працоўцы ёсць добрым явішчам.

Пораўняйце яе з ситуацыяю, калі прыяплет запускаецца нормальна і ламаецца толькі тады, калі першы корыстнік прабуе ўвайсці.

Бачыць нешта такое:

Missing required environment variable: JWT_SECRET

пад час развяртання набагато краща, чым даведацца пра гэта з аграсіванага запиту на падтрымку.

Не задавайце без адзвучання небяпечныя стандартныя значэнні

Гэты шлях адчуваецца прывабным:

const jwtSecret = process.env.JWT_SECRET || "secret";

Ён спрыяе павольнаму развіццю ў локальных умовах.

Это таксама ачыяе дзверы для прасоў у безпекі.

Падазроўцуйме, што вы забылі задаць JWT_SECRET у вашай серыйнай средзе.

Програма не зупініцца.

Яна проста тыха перайдзе на:

secret

Тады ваша рэальная система падпісвае токены значэнням, якое кожны можа здагадацца.

Больш безпечная версія выглядае так:

if (!process.env.JWT_SECRET) {
  throw new Error("JWT_SECRET must be configured");
}

Значэння-замена ўсуненяюць проблемы для нешкодных налашчэнняў.

Яны ствараюць рызіку для всаго, што зв’язана з безпекай.

Праверкі налашчэнняў трэба выканаць пад час запуску

Шаблон, які варта адразу застосаваць, — ставіцца да налашчэнняў прыбліжна як да неабходной залежнасці, якая павінна быць задоволена перш чым запускаецца ўсё інше.

Напрыклад:

function loadConfig() {
  if (!process.env.DATABASE_URL) {
    throw new Error("DATABASE_URL is required");
  }
  if (!process.env.JWT_SECRET) {
    throw new Error("JWT_SECRET is required");
  }
  return {
    databaseUrl: process.env.DATABASE_URL,
    jwtSecret: process.env.JWT_SECRET,
    port: Number(process.env.PORT || 5000)
  };
}
const config = loadConfig();

Это дае вашай программе чыстае правіла для роботы.

Альбо налашчэння ў правілы, альбо процес ніколі не запускаецца.

Гэты компроміс абоўсумова варты.

2. Апэл да localhost для служб, які знаходзяцца за межама вашага дапрыгу

Гэтая памылка заслуговае на адзінакавую увагу, таму што яна постае стало калі код паказваецца за межамі комп’ютера разработчыка.

Спадзяймо, ваш бэкенд вярбуеся з PostgreSQL.

На вашам ноутбуке настройкі выглядаюць так:

Backend → localhost:5432 → PostgreSQL

І ўсё працюе.

Потым вы контейнерызуеце бэкенд за дапамою Docker.

PostgreSQL тепер запускаецца ў окремым контэйнеры.

Вы можаце не чыніць нічых з настройкамі:

DATABASE_HOST=localhost

І тады вы пабачаеце:

connection refused

Што відбуваецца?

localhost больш не апэлюецца да таго, каму, як вам здаецца, ён апэлюецца.

З усероўні контэйнера:

localhost

апэлюецца да:

гэтага конкретнага контэйнера.

А не:

контэйнера, у яком запускаецца ваша база дадзеных.

Неправильна адаптация даўжынкі гэтага адразнення ёсць прычыной вялікай часткі проблем пад час развёртвання Docker.

Для сетяў Docker патрэбна іншая ментальная модель

Уявіце сабе тры адзеленыя контэйнеры:

backend
database
redis

Вы можете ўзначыць іх так:

services:
  backend:
    build: .
    depends_on:
      - database
      - redis
  database:
    image: postgres
  redis:
    image: redis

За межамі контэйнера-бэкенду вы не можете проста з’ясавіць:

localhost:5432

У замене вы запрашываеце іншы контэйнер па імені яго службы:

database:5432

Тая ж логіка прыменшаецца да Redis:

redis:6379

Рэзультатныя сетевыя топалогіі выглядаюць так:

backend
   |
   +---- database:5432
   |
   +---- redis:6379

Спачатку гэта здаецца незнайомым, адколі локальныя наладжэння зазвычай запускаюць усё безпосередня на вашым хост-машыне. У працэйных умовах між компанентамі стваруюцца рэальныя сетевыя межы.

У службах у хмаре існуе тая ж пастка

У локальных умовах ваша прыкладна програма можа запрашываць Redis па:

localhost:6379

Але ў працэйнай средзе Redis знаходзіцца на адной з сервісаў керування, які располагаюцца зовсім індзе.

Правильная настройка там можа выглядаць так:

REDIS_URL=redis://redis-production.example.com:6379

Ваша аплікацыя не павінна знати разніцу.

Гэта і є ключовая ідея.

Напісайце свой код так, каб ён викорыстоўваў настройкі таким чынам:

createRedisClient({
  url: process.env.REDIS_URL
});

Замест таго, каб жорсткая манера задавала деталі з’ўязку так:

createRedisClient({
  host: "localhost",
  port: 6379
});

Зберагаце аднаковую структуру наяўных настройкаў у всіх средах

Значэнні будуць разныя ў розных средах.

Структура ж не павінна развіцца.

Напрыклад, у режыме разработкі:

DATABASE_URL=postgresql://localhost:5432/myapp
REDIS_URL=redis://localhost:6379

А ў працэйнай средзе:

DATABASE_URL=postgresql://production-db:5432/myapp
REDIS_URL=redis://production-redis:6379

Адна і тая ж база коду.

Разныя значэнні для розных сред.

Саме такая аднаковасць і є тым, чаго вы прагатэ.

3. Стварэнне новага з’ўязку з базай дадзенаў за кожны запит

Гэты конкрэтны бяг працюе непазбірна ўвесь час, пакуль не спрычыніць проблем.

Вы запісваеце маршрут так:

app.get("/users", async (req, res) => {
  const client = new Client({
    connectionString: process.env.DATABASE_URL
  });
  await client.connect();
  const result = await client.query("SELECT * FROM users");
  await client.end();
  res.json(result.rows);
});

Ён працюе нормальна.

Вы тэстуеце яго ручна.

Адпаведныя адказы прыходзяць быстра.

Вы выкладваете яго у прыемліванне.

Потым пачынаеся рэальны трафік.

Зрэшты вы пачынаеце бачыць такія памылкі:

too many connections

альбо:

remaining connection slots are reserved

альбо вы заўважваеце, што відсотак апыткі CPU вашай базы дадзеных стаўляеся ўсё вышэй.

У чаму прычына?

Кожны запрошэнне, яке прыходзіць, стварае абсалютна новую з’яўленне да базы дадзеных.

Пры 100 адночасных запрошэннях гэта можа стаць 100 аднародных з’яўленняў.

Пры 1 000 запрошэннях ситуацыя становіцца ўсё горшай.

Стварэнне з’яўленняў є дорогім

Стварэнне з’яўлення не є безкоштовным. Залежна ад вашай базы дадзеных і налашоўкі, гэта можа включаць:

  • налагоджэнне сетевай з’яўлення
  • Автентыкацыя
  • Узгоджэнне TLS
  • Ініцыявацыя сесіі
  • Размяшчэнне рысаў
  • Стартаванне процэсу або ніткі на бакэндзе
  • Павтарэнне всіх гэтых дзеянняў для кожнага запиту марнавае рысы. Самэй таго аплікацыі зазвычай выкорыстоўвають басэн з’яўленняў у замене.

    У PostgreSQL гэта выглядае так:

    const { Pool } = require("pg");
    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 10
    });
    

    А ваш маршрут становіцца таким:

    app.get("/users", async (req, res) => {
      const result = await pool.query(
        "SELECT id, name, email FROM users"
      );
      res.json(result.rows);
    });
    

    Басэн зберагае набор падлежачых да павторнага выкарыстоўвання з’яўленняў, якія готавы да работы.

    Візуальна гэта выглядае так:

    ┌── connection 1
    Request ────────┼── connection 2
                    ├── connection 3
                    ├── connection 4
                    └── connection 5
    

    У працоўным стане:

    Request 1 → new connection
    Request 2 → new connection
    Request 3 → new connection
    Request 4 → new connection
    

    Басэны все ж можу ствараць проблемы у масштабе

    Нават з наявнасцю басэна, можліва неправильная настройка.

    Спадзяймося, ў вас 10 экземпляраў бакэнду, і кожны з іх настаўляе:

    pool.max = 20
    

    Гэта прыносіць патэнцыйнальныя:

    10 × 20 = 200
    

    З’єднання. Якщо ваша база дадзеных обмежуе колькасць з’єднанняў 100, вы вже перакроілі ліміт, нават якщо настройкі пулу кожнага окремага экземпляра выглядаюць разумная самыя по сабе.

    Гэта самэ такі проблема, якая ніколі не выявляецца на апараты развівача.

    У локальных умовах вы можаце пабачыць:

    1 application × 10 connections = 10
    

    У працэйнай серыі:

    10 applications × 20 connections = 200
    

    Горизантальна масштабаванне вашага прыемлена можа таямна збільшыць навантажэння на базу дадзеных.

    Размер пулу — гэта проблема на рэвэле системы

    Калі вы визначаеце, насколькі великі мае быць пул, вам трэба взяць у расчыт:

    database connection limit
            ↓
    number of application instances
            ↓
    pool size per instance
            ↓
    background workers
            ↓
    admin/monitoring connections
    

    Гэта не тое, што можна выбраць на свае рашэнне.

    4. Перадача кожнага столбца базы дадзеных кліенту

    Гэта конкрэтная памылка рэдкая развалюе ваш прыемлен негаразда.

    Самэ таму гэта ўскладнена.

    Уявіце сабе таблыцу корыстнікаў з такімі полямі:

    id
    name
    email
    password_hash
    phone
    address
    created_at
    updated_at
    internal_notes
    

    Вы пісваеце запит на кшталт:

    SELECT * FROM users;
    

    А потым працяеце рядкі безпосередна ў адпаведны рэспанс:

    res.json(result.rows);
    

    Це працюе. Фронтэнд отрымае свой об’ект пользователя. Можна працяваць далей.

    Але на самай справе вы адразу ж створылі кальк проблем.

    Перша: выклаканне дадзэнняў

    Рэспанс можа выглядаць так:

    {
      "id": 12,
      "name": "John",
      "email": "john@example.com",
      "password_hash": "...",
      "internal_notes": "..."
    }
    

    Нават якщо поле з паролем хэшуецца, а не ў чыстам відметку, няма прычын, чаму ён мусіць дасягнуць кліента. А ўсе поля, прызначаны толькі для внутрашньага вжытку, якія вы забылі, таксама будуць адправлены разам з ним.

    Другая: пропускна спроможнасць

    Спакуймо, кожны запис важыць 2 КБ. Для тыячы записаў цэе будзе:

    2 KB × 1,000 = 2 MB
    

    Тепер уявіце, як таблица з часам становіцца шырэй. Можа вы аж заканчыце тым, што будзеце аддаўаць:

    20 fields
    

    колі UI насправды викорыстоўвае толькі:

    id
    name
    avatar
    

    База дадзеных не ведае, канкліво патрэбуецца вашам фронтэнду — гэтая рашынка належыць вашам шару API.

    Таму замест:

    SELECT * FROM users;
    

    вы должны напісаць:

    SELECT
      id,
      name,
      avatar_url
    FROM users;
    

    Тепер пэйлоад содержыць толькі тое, канкліво патрэбуецца данаму канчыку.

    Гэта стае ўсё бол важлівым праз з’еднанні

    Разглядзіце запит на кшталт:

    SELECT *
    FROM users
    JOIN orders ON orders.user_id = users.id
    JOIN payments ON payments.order_id = orders.id;
    

    Гэта можа выявіць вельмі большы запас дадзеных, якія вы ніколі не планавалі адправляць. Гэта прыемны штуткі, калі вы яшчэ ствараеце прылады, з такой логікай:

    "Я проста верну всё і дам фронтэнду выбраць тое, канкліво яму."

    Супротставляйцеся гэтаму прагненню.

    Форма адпаведзі AI должна быць намеранай. Напрыклад:

    const users = result.rows.map(user => ({
      id: user.id,
      name: user.name,
      avatar: user.avatar_url
    }));
    

    Альбо, яшчэ лепей, пераказаць гэту адпаведальнасць спецыяльнаму шару DTO або серіявання.

    Угода API мае значэнне

    API высокага стандарта должна могчыць адпавядаць на такі запиты:

    Што саме гарантуе цей канцэнтр павертаць?

    А не:

    Калі саме існуюць столбцы ў базе дадзеных?

    Гэта разлічэнне становіцца ўсё болей важлівым па мере зростання кодавой базы.

    5. Ігнараванне індэксаў базы дадзеных пакуль праця не стане медленай

    Гэта можа быть адным з яснаяўшыхся прыкладаў ситуацыі, калі ўсё выглядае цалкам нормальна локальна, але таямніча ператвараецца на проблему у рэальных умовах.

    Возьмімо запит на кантэкст:

    SELECT * FROM users
    WHERE email = 'john@example.com';
    

    Калі ваша дэв-база дадзеных мае:

    500 users
    

    запит вяртаецца мгновэна.

    Вы выкладзаете яго у працэю. Чыры разы прыйходзіць, і табліца тепер мае:

    5 million users
    

    Разам той жа запит можа стаць значнаясцю медленней, таму што база дадзеных можа патрабаваць сканування вялікай часткі табелю, каб знайсці падходящы элемент.

    Тыповая рэакція ў такім случае:

    „Але ж гэты запит раней быў шырокі!“

    Звяжана — таму што тады табелю была мінімальная вялічына.

    Індэксы існуюць саме з гэтай прычыны

    Даадзець ўсё такое:

    CREATE INDEX idx_users_email
    ON users(email);
    

    дазволяе PostgreSQL знаходзіць падходящыя рядкі значна эфектывней, чым праз повны скан табелю.

    Протыма, главная мысль тут не ў тым:

    Проста ствараць індэксы да всаго.

    Гэта сама по сабе ўпорчанне. Індэксы не являюцца безкоштовнымі — яны:

    • займаюць прастор на дыску
    • спамляюць процес збіркі дадзеных
    • трабуюць постаўнага адтрымання
    • не дапамагаюць у кожным типе запытаў
    • не вартаяя ў дадаванні да кожнай столбца

    Уместны вопыт замест таго ёсць:

    Калькі запытанняў насправды выконвае гэты прыкладны програма ў практыцы?

    Індэкс на аднойчынах выходу

    Якщо ваша програма рэгулярна выконвае ў такім формате:

    SELECT *
    FROM orders
    WHERE user_id = 123
    ORDER BY created_at DESC
    LIMIT 20;
    

    Можа быць прыемным стварыць компазітны індэкс працуючы з гэтымі данымі:

    CREATE INDEX idx_orders_user_created
    ON orders(user_id, created_at DESC);
    

    Правы выбар індэкса завжды залежыць ад вашага конкрэтнага двайчарога базы дадзеных, ад таго, як распадзены даныя, ад працэўкі планавальніка запытанняў, а таксама ад вашай фактычнай навантажэння — самэ гэта і ўскладнюе выбір індэкса на адгадванні.

    Іспользуйце EXPLAIN

    Аднам з найцэнныях дыагностычных інструментаў у PostgreSQL ёсць:

    EXPLAIN ANALYZE
    SELECT *
    FROM orders
    WHERE user_id = 123
    ORDER BY created_at DESC
    LIMIT 20;
    

    Ён паказвае вам аднойчына, як база дадзеных выканала заданае запытанне, заменяючы такія припускі, як:

    Гэта запытанне павінна выконвацца шырока

    на фактычныя доказы:

    Гэта тое, што насправды робіць база дадзеных

    Такая зміна способу мышлення мае вельмі важлівэ значэнне.

    Местныя даны можу быць некальківанымі

    Эта правіла, якое варта запам’ятаваць: рэзультаты выконання, апрацоўваныя на маленькай местной базе дадзеных, даюць вельмі мало інфармацыі пра тое, як аднойчы тая ж запитоўка будзе працаваць у рэальных умовах.

    Запитоўка, якая завершаецца за:

    5 ms
    

    час на 1 000 рэядоў, можа працаваць абсалютна інакша, калі вы працуеце з:

    10 million rows
    

    Ёсчымлівая перацэнка выконання запитоўкаў трэбуе об’ёмаў дадзеных, якія падобныя да тых, якія вы насправдзе пабачыце у рэальных умовах.

    6. Перакананне, што все, што вы захоўваеце на дыску, застанэцца там

    Гэтая проблема часта выяўляецца падчас завантажэння файлоў.

    На вашай сабе машыне вы можаце запісаць калякі-такі код:

    app.post("/upload", upload.single("file"), (req, res) => {
      console.log(req.file.path);
    });
    

    Завантажаны файл знаходзіцца гдысьці падобна да:

    /uploads/photo.jpg
    

    Вы перазавантажваеце прыкладку.

    Файл застаецца там, дзе вы яго заставілі.

    Нічога няўыглядае некальківаным.

    Потым вы аплікацыю выкладзеце ў контейнер або на яку-небудзь тымчасовую платформу для хоставання.

    Адбываецца новая разгрузка.

    Раптам файл зникае.

    Пачынаюць прыходзіць запиты на падтрымку:

    Куды зниклі мае фартуткі?

    Чаму така ситуацыя відбываецца?

    Таму што сістэма файлавання на вашай локальной машыне працюе зусім інакше, чым сховыш у багатох працоўных сераўнах.

    Контейнер зазвычай праходзіць праз такі цикл жыцця:

    created
    running
    destroyed
    recreated
    

    Калі такі контейнер знішчаны, все, што было запісана толькі на яго локальны дыск, зникае разам з ўсім.

    Большасць платформ для хоставання за прыродой спрацоўвання ставяцца да файлаў, запісаных локальна, як да тымчасовых.

    Перастаньце вважаць ваш сервер аплікацый довгастрокавым сховышам

    Для обробкі завантажэнняў у працоўных умовах звычны падход — запускать файлы у спецыяльны сховыш об’ектаў, а не на власны дыск сервера.

    Это дае вам архітектуру прыблізна такую:

    Client
      ↓
    Backend
      ↓
    Object Storage
      ↓
    Database stores file metadata
    

    У тым часе ваша база дадзеных патрэбуе толькі для зберагчыка кансаект — што-та на кшталт:

    id
    user_id
    filename
    storage_key
    content_type
    created_at
    

    Рэальныя байты файлу знаходзяцца ў службе зберагчыка об’ектаў, а не ў вашай реляцыйнай базе дадзеных.

    Такое раздзелэнне абавясцей значна спрощае розумеенне сітуацыі.

    База дадзеных і слой зберагчыка рашаюць разныя проблемы

    Базы дадзеных адлейваюцься у обробце:

    • структураваных записаў
    • связей між записамі
    • гарантый транзакцый
    • запытаў і фільтраў

    Службы зберагчыка об’ектаў створаныя для:

    • зображэнняў
    • відэа
    • PDF-файлаў і іншых вялікіх дакументаў
    • статычных актываў у цэлым

    Прабаўкі зробіць так, каб адна система выканавала обе задачы, зазвычай у канцэ нарабляюць проблем.

    Адзін з супаўнасных падступаў: запісванне заваношаных файлаў у вашу базу коду

    Ня рэдкасць знайсці проекты, структураваны так:

    project/
      uploads/
      src/
      package.json
    

    дзе заваноскі корыстніка знаходзяцца працоўна праза самыя выхідныя файлы прыкладнення.

    Такі падход быстра перестае работаць.

    Уявіце сабе:

    100,000 uploaded images
    

    якія знаходзяцца ўнутрь папкі вашага проекту.

    Разгортанне становіцца чрэзвычайна медленным і хаотычным.

    Код прыкладнення і контэнт, створаны корыстнікам, палягаюць на абсалютна разных цыклах жыцця, таму яны патрабуюць адналежнасці да разных систем.

    7. Аплыванне API без лімітацыі частоты

    Работа локальна стварае абманлівую ситуацыю.

    Шансы великі, што вы — едынен кліент, який запрашае API.

    Вы адправляеце:

    GET /api/users
    

    адзін раз.

    Потым знову.

    Можа, ўсьмох разоў больш.

    Нічога не ламаецца.

    У рэальных умовах все будзе аднакоўсі не так прыязна.

    Адзін абонент — прымусова чыстае намераваннем — можа адправіць тыячы запитоў за короткі час.

    Возьмемо, прыклад, такі канцэнтраба:

    POST /api/login
    

    Без жадных мераў абмежэння, зловершальнік можа атакаваць яго паўтаральнымі спробамі заходжання:

    attempt 1
    attempt 2
    attempt 3
    ...
    attempt 10,000
    

    Нават абсалютна надзеяны пераконтроль пароля не запобегае зласованню самога канцэнтрабы у масштабе.

    Абмежэнне частоты стварае пункт контролю

    Канцэптуальна, яно працюе так:

    Client
       ↓
    Rate limiter
       ↓
    Application
    

    У замест на тое, каб запыты праходзілі без ліміту, можна абмежыць певную категорыю канцэнтраба, напрыклад:

    100 requests / minute
    

    Калі абонент перакрое гэты ліміт, ён атрымае:

    429 Too Many Requests
    

    Правильныя цыфры залежаць ад сцэнарыю викорыстоўвання — канцэнтраба для публічных пошукоў не патрэбуе таго ж ліміту, як, напрыклад:

    POST /login
    

    альбо:

    POST /forgot-password
    

    Это не толькі прыбліжанне або злонамераваны трафік

    Ліміты частоты таксацый таксаваняюцься проты багоў на стороне кліента.

    Уявіце сабе некалькі кадэнтай коду фронтэнду, напрыклад:

    setInterval(() => {
      fetch("/api/notifications");
    }, 100);
    

    Этот адзін вікна браузера тепер без паузы атакуе ваш API.

    Тепер уявіце гэтую памылку на 10 000 корыстнікаў.

    У вашам бэкендзе раптам з’яўляецца справжняя проблема.

    Лімітар частоты створае для вас захоўну сетку проты самэй такой неконтрольванай дзеяльнасці.

    Лімітаванне частоты стае сложнейшым, калі вы расширяецеся горизонтальна

    Сказаўмо, ваша аплікацыя працуе за дапамогою трох разных экземпляраў бэкенду:

    Server A
    Server B
    Server C
    

    Якщо кожны экземпляр зберагае свой скалькавач у памяці, кліент можа фактычна атрымаць у тры разы больш часу, ніж запланавана, по аднаму на кожны сервер.

    Ёжчы застаўляць аднаковы ліміт на всіх экземплярах, команды зазвычай викорыстоўваюць спяльны хранілнік, напрыклад Redis.

    Тады структура выглядае працоўна так:

    ┌── Server A
    Client → LB ──┼── Server B
                  └── Server C
                        |
                      Redis
    

    Завдяк такаму спільнаму стану кожны экземпляр адмаўляе той самы ліміт адносова аднакава.

    Як і ранейш, гэта застае непазірным калі вы запускаеце адзін процес локальна — няма распраўленага стану, пра які трэба хаварыцца, пакуль у працоўнай сітцы не выйдзе проблема.

    8. Прыпуск, што запиты завжды выканаюцца за разумны час

    Гэта можа неспакоюць вас.

    Спачатку створыце канечную точку такую:

    app.get("/report", async (req, res) => {
      const data = await generateHugeReport();
      res.json(data);
    });
    

    Локальна звіт абыходзіцца:

    1,000 records
    

    і завершаецца за:

    300 ms
    

    Всё выглядае нормальна.

    У працоўнай сітцы той самы звіт тепер абыходзіцца:

    5 million records
    

    а запит тепер займае:

    45 seconds
    

    Тым часам ваш рыверс-прайкс можа быть налаштаваны так, каб здолжыў паслужэнне пасля:

    30 seconds
    

    так што кліент у канцы канца бачыць:

    504 Gateway Timeout
    

    Нават як ваш бэкенд продовжае працаваць у фоне.

    Гэта несувязанасць стварае заплутаны спосаб абяроны.

    Робота, якая трывае дзёўга, не паслужыць частю циклу запит-адказ

    Калі задача будзе выконвана за час, як правило, краща обрабоцваць ёе як фонавую задачу, а не так, каб кліент чакаў на ёе выконанне безпосередна.

    У замяну такому прайсупутку:

    Client
      ↓
    POST /generate-report
      ↓
    Wait 45 seconds
      ↓
    Response
    

    вы пераходзіце да чагосьць на кшталт:

    Client
      ↓
    POST /generate-report
      ↓
    Job created
      ↓
    202 Accepted
    

    пры чым сама обработка выконваная окрема:

    Queue
      ↓
    Worker
      ↓
    Generate report
      ↓
    Store result
    

    Кліент можа пасля чаго перакантролюваць статус за дапамогою чагосьць на кшталт:

    GET /reports/:id
    

    альбо проста паведамлены, калі задача завершыцца.

    Фонавыя кеі займаюцца важкай роботой

    Паўзловыя прыклады роботы, якая павінна выконвана за межамі главнага циклу запита, включаюць:

    • адрасаванне электранаў
    • стварэнне PDF-файлаў
  • Обробка зяўроў
  • Обробка відэа
  • Стварэнне атласоў
  • Імпорт дадзэнняў
  • Расылка паведамленняў
  • Стварэнне вялікіх экспортных файлоў
  • Выкананне запланаваных задаў
  • Забезпечэнне таго, каб гэтыя задачы выканаваліся не на главнай вясці, значыць, ваша основная API застаецца швайнаю і рэагуючай.

    Не забывайце пра таймауты

    Кожны вызов, які ваша служба выкананая да звязанага системы, павінен маты разумны таймаут.

    Як прыклад:

    fetch(url, {
      signal: AbortSignal.timeout(5000)
    });
    

    Якщо вы праігноруеце гэта, залежнасць, якая застыне, можа заставіць вашу аплікацыю чакаць незаканчэнна.

    Уявіце сабе такую ланцюговую структуру:

    API
     ↓
    Payment service
     ↓
    hangs
    

    Ваша запчытаць застаяла, чакаючы на ўтварэнне рэакціі з боку чаго-то, што можа ніколі не адпаведзець.

    Тепер уявіце, што гэта вядзецца ў часе для сотняў адночасных запчытаць.

    У певны момант ваш сервер выкаранае доступных рысоў, каб обрабатваць ўсё іншае.

    Кожная залежнасць патрэбуе план на выключэнне

    Для кожнай зовнішней службы, з якой взаўсёды вяршыцца робота вашага прыкладнага програму, вы должны можаць адпаведзець на аднай конкрэтной запытанні:

    Што будзе, якша гэтая служба не адпаведзе?

    Не гэтае:

    Што будзе, калі всё працюе нормальна?

    На гэтае другое запытанні лёгка адпаведзець.

    Першае ёсць тая, якая насправдзе вялічыць, чы рэшта вашай системы будзе працаваць пад тым напорам.

    9. Доверлівасць да вводу кліента

    Гэтую памылку майже некомфортна адзначаць, таму што код, які ёй спрычынены, часта выглядае абсалютна разумным.

    Возьмімо, напрыклад, такі канец-тупак:

    app.post("/users", async (req, res) => {
      const { name, email } = req.body;
      await createUser(name, email);
      res.json({
        message: "User created"
      });
    });
    

    На першы погляд, нічога не выглядае некоректна.

    Але падумайце, што будзе, якша тэла запытку будзе выглядаць так:

    {
      "name": "",
      "email": "hello"
    }
    

    Алеў жо так:

    {
      "name": null,
      "email": null
    }
    

    Алеў жо так:

    {
      "name": "A".repeat(1000000),
      "email": "..."
    }
    

    Што будзе, як кліент запрашае поля, які ваш код ніколі не прыглядаўся?

    Пад час розработкі локальна вы зазвычай апрашоўваеце рэштытельна падготавленыя тэстовыя запиты.

    У працоўнай суперактуальнасці ваш API прыме ўсё, што хтань рашыцца яму апрашаць.

    Праверкі трэба выкарыстоўваць на входзе

    Добра створаны API пераглядае прыходзячыя данні прытаму, як толькі запускаецца бізнес-логіка.

    Напрыклад, вы можете задаць такі формат:

    const schema = {
      name: "string",
      email: "email"
    };
    

    За дапамогою бібліятэкі праверкі вы можете застаўляць такія правілы:

    name required
    name length <= 100
    email required
    email valid
    

    Не так важна, якую самэўказану бібліятэку праверкі вы выберазе.

    Важнае — гэта базовае правіла:

    Ніколі не прыпускайце, што кліент апрашаў тое, чаго вы чакалі.

    Памятайце, што „кліент“ можа быць:

    • вааш сэрвіс фронтэнду
    • мобільныя дапыты
    • інтэграцыя з трэцімі сторонамі
  • Автаматызаваны скрыпт
  • Старая версія вашага дапрынку
  • Чалавек з злымі намерамі
  • Перакантрольванні на боку кліента не ўтварае слоя абароны

    Частая працоўка ёсць:

    "Але я вже перакантрольваю гэта на фронтэндзе, у React."

    Гэта дзейсна спрыяе павышэнню якосці выкарыстоўвання дапрынку.

    Але гэта нічога не дапамагае захаваць ваш бэкенд.

    Кожны можа проста ігнораваць ваш код фронтэндза.

    Для гэтага патрэбна толькі адпаведная дзеянне:

    curl
    

    Якая будзе спрямована адразу на вашы канцэнтрык API.

    Код вашага бэкендза все рава должен сам выкананыць усі правілы.

    Ін’екцыя SQL — гэта супаўзельны рызык

    Утримайцеся ад стварэння запытаў па сшыкаванню страк, як гэта:

    const query = `
      SELECT *
      FROM users
      WHERE email = '${email}'
    `;
    

    У замене выкорыстоўвайце параметрызаваныя запыты:

    const result = await pool.query(
      "SELECT * FROM users WHERE email = $1",
      [email]
    );
    

    Такім чынам драйвер базы дадзеных зберагае значэнні параметраў окрэмна ад структуры запыту.

    Паўеракананне вхідных дадзеных і параметрызаваныя запыты рэшаюць разныя аблікансы, але бэкенд высокага стандарта патрабуе і тое, і другое.

    10. Думка, што „працюея прыкладнэе“ значыць „прыкладнэе ў стані здароўя“, ёсць неправильная

    Гэта можа быть найбазавейшым непрацоўнікам у гэтым списку.

    Ваш сервер запускаецца:

    Server listening on port 5000
    

    Логі падтверджаюць:

    Application started successfully.
    

    У такі момент бяжыць падумаць:

    Чудазна. Мы заканчылі.

    Гэта не павінна быць правдой.

    Процес можа продаваць робіць нават тады, калі сама прыкладнэе фактычна зламана.

    Разглядзім такі стан:

    Backend process: running
    Database: unavailable
    Redis: unavailable
    Payment provider: unavailable
    Queue: stuck
    

    З точкі зору аператывной системы процес жывы і ў стані здароўя.

    Панель монітарынгу можа паказваць:

    CPU: 10%
    Memory: 20%
    Process: alive
    

    Тым часам рэальныя корыстнікі бачаць:

    500 Internal Server Error
    

    Перакананні пра стан системы заполняюць частку прасоў

    Базавы канцэнтры стану, напрыклад:

    GET /health
    

    можа проста вярнуць:

    {
      "status": "ok"
    }
    

    Але нават гэта можа быць абманлівым.

    Якщо /health толькі паўтарае, што сервер HTTP адпавядае на запыты, гэта нічога не сказвае пра тое, чы робяць свою работу залежнасці, ад каторых фактычна завісі ваша прыкладка.

    Болей полны распаддзіляе два канцэнтры. Адны:

    /health
    

    для базавага пераканання пра жывучасць, а другі:

    /ready
    

    для пераканання пра гатовасць.

    Канцэптуальна разліка выглядае так:

    Liveness: "Is this process alive?"
    

    і:

    Readiness:
    "Can this instance actually serve traffic?"
    

    Спецыяльная перацэнка стану базы дадзеных

    Перакантэкція гатовасці можа быць або шчэлёй і паказаць, чы рэальна наладка з базайнам дадзеных працюе, напрыклад, праз запуск:

    SELECT 1;
    

    Якщо не можна дасягнуць базайнам дадзеных, прыклад можа пазначыць ся як негатоўна гатовы.

    Гэта дае інструментам керавання можлівасць перастаць направляць трафік да экземпляра, який не можа належна адпаведзаць на запыткі.

    Толькі час працы не дае многа інфармацыі

    Сервер можа адналажыць:

    99.99% uptime
    

    і ў той жытак ствараць для своіх корыстувачаў ситуацыю з вельмі медленным адпаведзеннем.

    Таму монітарынг у працэйным режыме павінен фіксаваць не толькі статус жыцця процэсу, але і такія паказнікі, як:

    • вытрэчка на адпаведзенне на запыткі
    • ранг пакрычэнняў
    • пропускная здатнасць
    • працэздатнасць базайнам дадзеных
    • выкарыстоўванне CPU
    • выкарыстоўванне памяці
    • глыбіна адлога
    • пакрычэнняя ў зовнішніх службах

    Одным з асабліва корыстных паказнікаў ёсць:

    p95 latency
    

    У замест на тое, каб запытаць:

    Калі ўсерэдні час адпаведзення?

    болей інформатыўна будзе запытаць:

    Сколькі часу трэба 95% запытанняў?

    Усерэдненняя часто маскуюць найгоршыя ситуацыі.

    Разглядзім такі сцэнарый:

    99 requests = 50 ms
    1 request = 30 seconds
    

    Усерэдні час адпаведзення тут можа зовсамна не выглядаць абавяжлівым.

    Але той адзін повольны запыт усё рава стварае справжняя паслядкі для таго, хто яго адправіў.

    Іншая памылка: логаванне всього, кроме корисных частак

    Гэта памылка заслуговае на месца ў списку, таму што яна дапамагла выкарыстаць больш адной сесіі дыбагавання.

    У працоўным сераверы ўтвараецца проблема.

    Вы ачынаеце логі.

    Усё, што вы знаходзите:

    Something went wrong
    

    Это ўсё, што є ў звесці.

    Няма ідэнтыфікатора запытання.

    Няма інфармацыі пра маршрут.

    Няма контексту пользователя.

    Няма трэйса выконання.

    Няма інформацыі пра тое, які самэў вызваны ў базе дадзеных.

    Няма часовага пазначэння, якое было б дастаткова точным для супарабоцавання з чым-небудзь.

    Толькі простая лінія, якая гаворыць:

    Something went wrong
    

    У такі момент вы прабуяце расшукваць прычыну без рэальных дадзеных для аналізу.

    Як выглядае корыстны запіс журналу

    Калі ў чымсь выходзіць наперад, запитанні, на якія хочацеся атрымаць адказы, ёсу:

    When?
    Where?
    Which request?
    Which service?
    What failed?
    How long did it take?
    What dependency was involved?
    

    Якорыстна структураваны запіс журналу фіксуе той контекст, прымерна так:

    {
      "level": "error",
      "requestId": "abc-123",
      "route": "/api/orders",
      "method": "POST",
      "duration": 843,
      "error": "Database timeout"
    }
    

    Такая версія насправды дае вам матэрыял для роботы.

    Ніколи не лічуце чутлівых дадзеных

    У той жа час будзьце абераглівы, каб не запісваць бездумна:

    password
    access token
    refresh token
    credit card information
    private keys
    

    Журналізацыя існуе для таго, каб дапамогчы вам діаганаваць проблемы, а не каб даць зловерам ўсё новую проблему.

    Чаму гэтыя прыбліжанні ніколи не показуюцца на вашам ноутбуку

    Можна справедліва запытацца, чаму так многі разработчыкі павтараюць тыя ж адзіныя памылкі, якщо ўжо воны такія распашчастыя.

    Адказ — локальна настройка і сераўныя умовы ёсць прынципава разныя системы.

    Ось які ў іх разлік.

    Як выглядае локальная разработка

    Зазвычай вы маеце справу з:

    1 developer
    1 application
    1 database
    small dataset
    low traffic
    fast network
    known inputs
    stable filesystem
    single process
    

    Тым часам у сераўных умовах можа буць:

    thousands of users
    multiple application instances
    millions of database rows
    unpredictable traffic
    network latency
    invalid requests
    external dependencies
    ephemeral containers
    background workers
    multiple regions
    

    Это не меньшая версія той самай системы — це абсалютна разная система.

    Код можа застацца незменным.

    Але атмосфера навакола яго — ні.

    Звычка працаваць толькі з успешным сценарыем

    Цэлкам прыродна теставаць толькі успешны ход задачы.

    Типовы прыклад:

    Create user
    ↓
    Login
    ↓
    Create order
    ↓
    Payment succeeds
    ↓
    Return response
    

    Этая последовальнасць прадстаўляе найкращы варіант развіцця.

    Але трафік у сераўных умовах ніколі не обмежваецца толькі найкращыми сценарыямі.

    Таксама трэба врачываць ситуаціі, такія як:

    What if the database is slow?
    What if Redis is unavailable?
    What if the payment service times out?
    What if the user sends invalid JSON?
    What if the request is duplicated?
    What if two requests update the same record?
    What if the process restarts?
    What if the container disappears?
    What if the database connection pool is exhausted?
    What if the user clicks the button five times?
    What if the request arrives twice?
    What if the response is lost after the database transaction succeeds?
    

    У распрацоўванай системе такія рэдкія крайнія прыклады не ўтрымуюцца.

    Это звычныя ситуацыі.

    Перакіранне з „Чы гэта працюе?“ на „Што, якщо гэта не запрацуе?“

    Калі зрастае адпаведны дазнаўчы кашталёк у роботе з системамі бэкенду, змянююцыся і вашы інстынкты.

    Спачатку падымаецца такой вопыт:

    Чы працюе гэты код?

    Пазней ён стае такім:

    Што будзе, якщо гэты код не запрацуе?

    Другі вопыт ў дзесяць разоў корыстнейшы.

    Возьмімо гэты рядок:

    const result = await paymentService.charge();
    

    Мышленьне пачатківца зупіняецца на:

    Якшо вызов успеў, праходзім да наступнага.

    Мышленьне, спрямаванае на працэс вырабніцтва, продовжуецца:

    Што, якшо выйдзе тайма-аут?

    Потым:

    Што, як запит будзе адправлены, але час на адпаведнае запрашэнне да маёй API выйдзе за межы?

    Тады:

    Што, як кліент практыкуе новую спробу?

    Тады:

    Можа, кліенту будзе вырачана плата два разы?

    У такі момент вы расследуеце можлівыя прычыны неудач, што ў сутнасі являецца основай інжынерыі бэкенду.

    Ідэмпотентнасць: што захоўвае безпеку платежных процэсаў

    Продовжымаўшы той жа прыклад: уявіце, што корыстнік нажмае

    Pay
    

    Запит падае на ваш сервер.

    Платеж адбыўся успешна.

    Але з’язок перыяўляецца прычым адпаведнае запрашэнне не можа вернуцца.

    Кліент не можа знайсці, чы платеж дасканальна адбыўся, таму ён практыкуе новую спробу.

    Тепер сервер адобразіў:

    Payment request #1 → success
    Payment request #2 → success
    

    Што можа лёгка прынесці:

    Customer charged twice.
    

    Ідэмпотентнасць існуе самэлькі для таго, каб запобiec гэтаму.

    Ключ ідэмпотентнасці можа выглядаць так:

    payment_7f8c123
    

    Кліент прыкладзе гэты ключ да запиту, а сервер зберагае яго разам з рэзультатам той операцыі. Якщо знову прыйдзе запит з тым самым ключам, сервер проста вяртае зберажаны рэзультат уместо таго, каб выконваць операцыю ўтрое.

    У структураванам відэце:

    Request
      ↓
    Idempotency key
      ↓
    Already processed?
      ├── Yes → return previous result
      └── No → process → store result
    

    Гэты патэрн мае вельмі важлівое значэнне для такіх операцый, як:

    • аплатаў
    • стварэнне замовленняў
    • регистрацыя на адказе
    • надсылка электранаў
    • званні да API трэціх сторон

    Багі з дуплікатнымі запитамі рэдка калі з’яўляюцца пад час локальных тэстаў, але яны постаюць стала, калі ўжо задействаваны рэальныя, ненадзеянныя сеті.

    Транзакцыі: Іншая рэч, якая выглядае нормальна, пакуль гэта не станеться

    Падумайце пра тое, што выходзіць, калі ствараецца замовленне.

    Процес выканання можа складацца з калькоў спэцыяльных крокаў:

    Create order
    Create order items
    Update inventory
    Create payment record
    

    Якщо вы не абгорнёте гэтыя операцыі ў транзакцыю, адна з іх можа завершыцца неудачаю на якомнебудзь етапе.

    Уявіце сабе такую ситуацыю:

    Create order       ✓
    Create items       ✓
    Update inventory   ✓
    Payment record     ✗
    

    У гэты момент ваша база дадзеных знаходзіцца ў напалову завершаным стане.

    Транзакцыі існуюць таму, юбы можна было аб’еднаваць операцыі, якія ўсе павінны выйсці успехам або ўсе — неудачаю, як адна ентычная структура.

    Ось як гэта выглядае на практыцы:

    BEGIN;
    INSERT INTO orders (...);
    INSERT INTO order_items (...);
    UPDATE inventory
    SET quantity = quantity - 1
    WHERE product_id = 123;
    COMMIT;
    

    А калі ў процесе выканання ўтвараюцца проблемы:

    ROLLBACK;
    

    Протэму, абгортанне кожной окремай операцыі ў адну вялічезную транзакцыю таксама не ўсегда яе правільны падход.

    Межы транзакцый трэба проектаваць рэштыравана.

    Транзакцыі, якія працуюць занадта давго, можу стварыць свае сабе проблемы.

    Пытанне, якое вы завжды павінны сабе ставіць, ёсьць:

    Якшто гэта не выйдзе часткова, у якім стане находзіцца система?

    N+1 запыткі: тыхі вораг выкарыстоўнасці

    Є ўжо адна проблема, якая часта застаецца непазначанай пад час розработы локальна.

    Спачатку вы запрашваеце спіс корыстнікаў:

    SELECT * FROM users LIMIT 100;
    

    А потым, для кожнага окремага корыстніка, вы запрашваеце ўсі яго замовы:

    SELECT * FROM orders WHERE user_id = ?;
    

    Гэта дае:

    1 query for users
    +
    100 queries for orders
    =
    101 queries
    

    Это ўжо вядомая пастка N+1 запытак.

    Якшто у вас толькі 10 корыстнікаў:

    11 queries
    

    Гэта ўжо прыемна.

    Але якшто колькасць корыстнікаў дасягне 10 000:

    10,001 queries
    

    тады раптам з’яўляецца сэрьёзная проблема з выкарыстоўнасцю.

    Рашэнне не павінна быць обов’язкова „з’едыніць усё ў адны вялікі запыт“.

    Залежна ад вашай ситуацыі, вам можа знадобіцца:

    • з’едынення
    • швэйкія завантажэнні
    • групавае обробкі
  • IN (...) клазы
  • з’явальнікі дадзеных
  • запыты, створаны спецыяльна для адхоўкі з гэтым шаблонам
  • Найважнейша — гэта можлівасць адразу ж выяўіць гэты шаблон.

    Бэкенд можа здацца абліковым па вяліканьню маленькага локальнага набора дадзеных, але на самай працы пад рэальным навантажэнням выконвае тыячы запытаў.

    Пагінацыя не ёсьць неабязковай для вялікіх колекцый

    Такі канец-пункт здаецца досыта безнебяжным:

    GET /users
    

    А запыт, які стоіць за ўсім гэтым:

    SELECT *
    FROM users;
    

    выконваецца нормальна, калі у вас є:

    50 users
    

    Але што будзе, калі вам даведзяцца працаваць з:

    5 million users?
    

    Адправка пяці мільйонав записаў назад да кліента — гэта тое, чаго вы не хочаце робіць.

    Пагінацыя дапамагае абмежыць гэта.

    Вы моглі бы вяршыць пагінацыю на асоўнанні з офсэтом:

    GET /users?page=2&limit=50
    

    альбо на асоўнанні з курсарам:

    GET /users?cursor=abc123&limit=50
    

    Пагінацыя за абзацамі курсора зазвычай лепей працуе для большых або постаўна змінюючыхся набораў дадзеных, адколі яна ухілваецца ад дзяўалей з выконаннем і цэласообразнасцю, якія выступаюць у зв’язку з вельмі большымі офсетамі.

    Яшчэ раз, ключовая проблема ў тым:

    localhost data = small
    production data = potentially huge
    

    Ваш API трэба стварыць, врачываючы гэты прынт у спакоў.

    Вылівы памяці не завжды праказуюцца негаразда.

    Это ўжо іншы тип бага, які праказуецца толькі ў режыме працы.

    Ваша аплікацыя запускаецца за дапамогою:

    Memory: 150 MB
    

    Чэрез гадзіну:

    300 MB
    

    Чэрез шасць гадзін:

    600 MB
    

    І ў канцэ нарэшце:

    Out of memory
    

    Потым яна зламваецца і пачынае запускавацца зноў.

    У гэты момант усё знову выглядае нормальна.

    Такі патэрн — яскравы сігнал таго, што ўсё-такі ёсць тое, што утримвае памяць, якой не трэба.

    Пашчыраныя прычыны включаюць:

    • масівы, якія растуць без жадных меры
  • Кэшы без ліміту вывядзення
  • Слухачы запускаў, якія ніколі не чыстяцца
  • Вялікі об’екты, якія нехтачна застаюцца ў памяці
  • Рэферэнсы, якія існуюць дыявольскі даваўна
  • Стрэмы, якія не обрабоўваюцца правільна
  • Калі вы тэстуеце пры самай аплікацыі, яна рэдка калі працуе дастатнько часу, каб такія проблемы сталі виднымі.

    У рэальных умаляваннях яна працуе дастатнько часу, каб такія вылівы памяці сталі значнымі.

    Гэта ў частцы прычыны, чаму стаўленне да паведання аплікацыі пра дзейсны час ёсць такім ценным.

    Кэшаванне таксама можа нашкодзіць рэальным умаляванням

    На паперы кэшаванне выглядае як простая перамога:

    Database → slow
    Redis → fast
    

    Таму інстынкт вельмі прост: кэшаваць усё.

    Але потым базовыя даны зменяюцца.

    І API продовжае вяртаць застарелую інфармацыю.

    У результате вы практычна сталкнуліся з несасварэнням, напрыклад:

    Database:
    balance = 100
    Cache:
    balance = 80
    

    API адпавядае:

    80
    

    І корыстнік хочаць з’ясавіць:

    Чаму мой баланс некоректны?

    Кэшаванне вымагае адпаведзей на такія запытанні:

    • Калі трэба анулюваць даныя?
    • Калі кэш стае некоректным?
    • Што робіць, якщо Redis перыходзіць у стан недоступнасці?
    • Што адбываецца, калі два серверы запішуўаюць у той самы ключ?
    • Чы можна тут викорыстоўваць застарелыя даныя?
    • Што трэба зрабіць адразу пасля развяртання?

    Складнаю частка кэшавання — гэта не проста зберагчык даных.

    Гэта выявленне моменту, калі зберагнутыя даныя перестаюць быць надзеянымі.

    Пры неудачах кэшу трэба маты стратэгію

    Якшо Redis неспадзявана зникне, чы ваша прыкладна програма:

    fail completely
    

    ці ж:

    fall back to database
    

    Не існуе універсальнай адпаведзі — гэта залежыць ад системы, яку вы ствараеце.

    Важна тое, каб вы здзейснілі такі викликі намеравана, а не випадкова.

    Секрэты ў Git: Памылка, якая можа стаць інцидэнтам безпекі

    Наступны пункт здаецца простым здарозумелым, але ён продовжае трапляцца.

    Вы стварылі файл:

    DATABASE_PASSWORD=...
    JWT_SECRET=...
    API_KEY=...
    

    А потым запусцілі:

    git add .
    git commit -m "config"
    git push
    

    У такі момент секрэт стае частынай історыі вашага Git.

    Выдаленне файла пасля цього не стырае йго — значэння все ўсё можна вярнуць з паканальных комітав.

    Кращы спосаб — храніць:

    .env
    

    у списку ў .gitignore, і здзейсніць коміт шаблону заместа:

    .env.example
    

    з місцевымі значэннямі, такімі як:

    DATABASE_URL=
    JWT_SECRET=
    REDIS_URL=
    

    Рэальныя значэння трэба вводзіць через настройкі сераўісу развёртывання або спецыяльны менеджер секрэтав.

    А якщо справжній секрэт усё-такі потрапіць у коміт:

    Не проста ўдаліце гэты рядок і не называйце цього завершэнням.

    Негайна змяніце таўарышчаючы код.

    Спрыяйце да таго, каб яго вважалі скомпрометаваным, незалежна ад таго, чы можаце вы даказаць, што да яго колі-небудзь былі доступы.

    CORS — гэта не аутентыкацыя

    Это ўсё той пункт, які часта вводзіць людей у плутанне:

    Access-Control-Allow-Origin: *
    

    CORS керуе тым, як браузеры обрабоцваюць запиты з іншых выхадных точак.

    Ён нічага не робіць для фактычнай захоўкі вашага API.

    Нічы не заважае сервернаму канцэнтру быць атакаваным безпосередна через:

    curl
    Postman
    mobile apps
    backend services
    scripts
    

    CORS ніколі не быў створаны як шар аутентыкацыі.

    Вашаму API все рава патрэбны рэальныя:

    • аутентыкацыя
    • автарызацыя
    • перакананне вхідных дадзеных
    • обмежэнне частоты запытоў
    • контроль доступу

    Безпека павінна быць забезпечаная явна, а не проста чераз правілы браузера.

    Аутентыкацыя і автарызацыя — гэта разныя рашчынанні

    Плутаннеце гэтых двух падчыніць ведае да справжніх абяранняў безпекі.

    Адказы на атрыбут атхэнтыкацыі:

    Каму вы?

    Адказы на атрыбут автарызацыі:

    Чы розрашана вам гэта работа?

    Разглядзім такі запит:

    GET /users/123
    

    Верагчываець можа быць атхэнтаваны.

    Але гэта нічога не сказвае пра тое, чы розрашана яму пераглядаць:

    user 123
    

    Якшы той, хто выдае запит:

    user 456
    

    сервер усё рава должен выявіць, чы:

    456 → allowed to access 123?
    

    Частая памылка выглядае так:

    if (req.user) {
      return getUser(req.params.id);
    }
    

    Гэты код толькі пераканальваецца, што хтось увайшоў.

    Ён ніколі не пераканальваецца, чы яму розрашана адкрыць гэты конкретны рысурс.

    Безпечнейшы падход узьмае за основу прыналежнасць або явныя правы.

    if (req.user.id !== req.params.id) {
      return res.status(403).json({
        error: "Forbidden"
      });
    }
    

    Конкрэтныя правілы будуць разнымі залежна ад прыемплэю.

    Але основны прынцып застаецца тым самым у всіх месцах:

    Автарызацыя не дае безумовнага доступу да всьго ў системе.

    Развертанне — частка прыемплэю

    Урок, які варта запам’ятаць з самага пачатку, — гэта тое, што развертанне не ўзельны этап, які дадаўаецца напрыканце розрабоцы.

    Ён є часткай самой інжынернай роботы.

    Ваш прыемплэй — гэта не толькі:

    source code
    

    Гэта ўзаўсёды сумяшанне:

    source code
    +
    configuration
    +
    database
    +
    network
    +
    storage
    +
    secrets
    +
    dependencies
    +
    monitoring
    +
    deployment process
    

    Чалавек, які разумее толькі код прыемплэю, можа ўсё равно абтрымавацца, калі все перейдзе у прыемплярнай средзе.

    Ёсць не патрэба володзець кожным дакладам Kubernetes, каб быць эфектывым.

    Але патрэбна працоўная разуменне среды, у якой фактычна запускаецца ваш код.

    Спіс пераканальных пунктав для гатовасці да прыемпляра, які я насправдзе вяду

    Перш чым адзначыць кантэнт готавым да выпуску, корисна ўвесь час пераглядаць такі список пераканальнай перапыткі.

    Канфігурацыя

    • Чы роўнаважуюцца неабходныя зменныя сераўіса пад час запуску?
    • Чы секрэты не знаходзяцца ў кансольнам коде?
    • Чы стандартныя настройкі для выпуску є безпечнымі, а не толькі зручнымі?
    • Чы настройкі для разработки і выпуску зберагаюцца окрема?

    База дадзеных

    • Чы налаштавана система пуліваціі з’яўленняў?
    • Чы розмер пулі є адповідным?
    • Чы важлівыя запиты індексаваны?
    • Чы медленныя запиты сапраўды аналізаваны?
    • Чы транзакцыі выкарыстоўваны там, дзе гэта неабходна?
    • Чы міграцыі стежаныя за дапамогою керавання версіямі?

    API

    • Чы вхідныя даны перавераны?
    • Чы пакеты адпаведзей ствараны спецыяльна?
    • Чы великія наборы рэзультатаў пагрупаваны ў сторніцы?
    • Чы існуюць меры контролю розмера тэла запиту?
  • Чы гэтыя таймааўтамы настроеныя разумна?
  • Безпека

    • Чы паролі хэшуюцца за дапамою безпечнага алгорытму?
    • Чы секрэты зберагаюцца безпечна?
    • Чы дэйсна адбываецца автарызацыя?
    • Чы SQL-запыткі параметрызаваны?
    • Чы прыменяецца лімітаванне частоты запыткаў там, дзе гэта важна?
    • Чы чувствяглівыя поля не прабываюць у лаг-файлах?

    Інфраструктура

    • Чы ў чым-небудзь неправільна выкарыстоўваецца localhost?
    • Чы стойкія даны зберагаюцца не ў тымчасовых контэйнерах?
    • Чы экспансаваныя тэрміналы перагляду стану доступны?
    • Чы реалізавана павольная апышчэння?
    • Што на самай працоўкі адбываецца, калі запускаецца занова аплікацыя?

    Прыдатнасц

    • Чы система тэставаная з ужываннем рэалістычных об’ёмаў дадзенаў?
    • Чы існуюць захаваныя шаблоны запыткаў N+1?
    • Чы дорогія задачы перакладваныя на фонавыя заведаменні?
    • Чы глыба кэша мае меры?
    • Чы старанліва лістуецца выкарыстанне памяці?

    Возможнасць аблікавання

    • Чы логі з бягамі маюць достатню інфармацыю, каб быць кориснымі?
    • Чы налажана відстэйкаванне запыткаў?
    • Чы старанліва лістуецца час адпаведзення і частка бягаў?
    • Чы неудачы базы дадзеных видны ў старанлівай разведчыце?
    • Чы насправдзе налаштаваны апавешчэнні?

    Аднародны выключэнне: маленькі даклад, які людзі забываюць

    Уявіце, што ваш сервер прыме:

    SIGTERM
    

    сігнал, таму што платформа для хоставання хоча перазапусціць экземпляр.

    Якщо процес завершыцца мгновэна, будзь-якія запыткі, якія ўсё ў процесе адпаведзення, можу проста зазнаць неудачы.

    Аднародны выключэнне краща справляецца з гэтым.

    Ідея ў кодзе прыбліжна выглядае так:

    process.on("SIGTERM", async () => {
      console.log("Shutdown signal received");
    server.close(async () => {
        await pool.end();
        console.log("Server closed");
        process.exit(0);
      });
    });
    

    Точная рэалізацыя будзе разніцявацца залежна ад вашай фрэймворкі.

    Але асновныя крокі застаюцца аднаковымі:

    Stop accepting new requests
            ↓
    Finish active requests
            ↓
    Close database connections
            ↓
    Exit
    

    Гэта становіцца ўсё болей важлівым, калі вы запускаеце калькі экземпляраў прыложэння адразу.

    Логіка павторных спроб можа паглыбіць проблемы

    Калі запуск зовнішняй службы не вяртаеся, павторны спробы здаюцца явным рашэнням:

    try {
      await callService();
    } catch {
      await callService();
    }
    

    Сама по сабе гэта выглядае досыць безнебяжна.

    Але тепер уявіце сабе тысячу запитоў, якія всі следуюць таму ж патэрну.

    Падазраць, што залежнасць пачынае адпавядаць медленна.

    Кожны з тых неудачных запускаў павторна працоўвае аўтаматычна.

    Залежнасць, якая страждае, тепер прымае ўсё больш навантажэння, чым раней.

    У гэтага патэрна є назва: буря павторных спроб.

    Логіку павторных спроб трэба проектаваць цярпеліва, врачынаючы такія фактыры, як:

    • ліміт па спробам
    • экспансія часу паузы
    • нестабільнасць
    • разумныя таймауты
    • гарантіі ідэмпотентнасці
    • захопленне цыркуіту, калі гэта разумна

    Праграматыя падзеі не ўсега яўляюцца механізамам вярнення ў нормальны стан.

    У дзеяных случаях самэўсёліквідацыя ёст тым, што ператварае незначны проблему на абсалютную перашкоду.

    Чаму багі ў працоўным серавере выглядаюць так інакше

    Тыповы локальны баг зводзіцеся да чагосьць на кшталт:

    I made a mistake.
    

    Тыповая працоўная інцидэнтна ситуацыя часта выглядае так:

    Several individually reasonable things interacted badly.
    

    Як прыклад:

    Traffic increases
            ↓
    More API instances
            ↓
    More database connections
            ↓
    Database reaches connection limit
            ↓
    Queries fail
            ↓
    Requests retry
            ↓
    More load
            ↓
    System becomes even less stable
    

    Ні адна з окремых ліній коду сама па сабе не выглядае небяспечной.

    Асалоджаныя проблемы крыюцца ў тым, як яны взаімаюцца пад навантажэнням.

    Самэўсёліквідацыя ёст прычыной, чаму інжынерыя бэкенду з часам пераключаюць увагу з абсалютная функцый на поведзінку системы як цэлага.

    Найбольш небяспечная фраза ў разработцы бэкенду

    Мабыць, найбольш рызыкаваная фраза ў всій работе з бэкендам яўляецца:

    "Ніхто не будзе гэта робіць."

    Ніхто не будзе:

    • Адмініструвацыя 10 000 запитоў за адну момент
    • З'ядзжанне файла размерам 500 MB
    • Двойны клік на кнопку адправы
    • Адправа некоректнага JSON-файла
    • Запит на мільйон рэядоў за раз
    • Продовжэнне викорыстання застарелаг кліента
    • Адказ працэсу API без адвароту через інтэрфейс
    • Вызванне тайма-ауту у зовнішняй службе
    • Актывацыя двух апдэйтаў адночасна
    • Пачатак працы сервера ў момент адправы запита

    Але ў канцы канцоў хтось так і рабіць.

    Іноды працайна. Іноды чераз багі ў іншых частках системы. Іноды чераз нестабільную сеть. Іноды проста таму, што ваш продукт пачаў активна растаць.

    У прымэтнай експлуатацыі не трэба, каб паспеўныя адноснікі раскрывалі свае ненадзейныя прыпускі. Сама масштабнасць дастатняя, каб іх сама адкрыла.

    Ствараць для неудачы, а не толькі для успеху

    Серьжызныя змены ў спосабе мышлення апытных разработчыкаў бэкенду заключаюцца ў следзячым: калі вы пішаце функцыю, не трэба зупыняцца толькі на запитанні, што будзе, калі ёй удастся выконацца. Таксама неабходна запытацца, што будзе, калі ў выкананні з’явятуюцься проблэмы.

    What happens when it succeeds?
    
    What happens when it fails?
    

    Калі вы запытваеце базу дадзеных, запытацца, што будзе, якщо ёй не можна будет даць доступ.

    What if it's unavailable?
    

    Калі вы запытваеце Redis, запытацца, што будзе, якщо ён адпавядае медленна.

    What if it's slow?
    

    Калі вы адправляеце электранаўленне, запытацца, што будзе, якщо час адпаведзення вантажувача выйдзе за межы.

    What if the provider times out?
    

    Калі вы аплоадзіце файл, запытацца, што будзе, якщо зберагач дадзеных не справіцца праз тое, што рэкорд у базе дадзеных вялікі ўжо быў запісаны.

    What if storage fails after the database record is created?
    

    Калі вы ствараеце замовленне, запытацца, што будзе, якщо той самы запит прыйдзе два разы.

    What if the request is repeated?
    

    Калі вы обрабоцваеце платеж, запытацца, што будзе, якщо кліент ніколі не отрымае адпаведзення.

    What if the client never receives the response?
    

    Калі вы запускаеце розгортанне, спытайцеся, што будзе, якшо новая версія зламаецца пад запуск.

    What if the new version crashes?
    

    Постаўленне такіх запытанняў на регулярнай аснове зменяе спосаб праектавання вашай архітэктуры.

    Простая ментальная модель для разработкі бэкенду у прыметнай средзе

    Пад час стварэння функцыяналу бэкенду корисна працаваць з яго праз пяць слоёў мышлення.

    1. Правільнасць

    Чы рэальна функцыянал выпалняе тое, каму ён прызначаны?

    Create order → order actually gets created
    

    2. Безпека

    Чы хтось можа выконваць дзеяння, якія яму не павінны быць дазволеныя?

    User A → access User B's order
    

    3. Выдатнасць

    Што будзе, калі об’ём дадзеных або трафік значна зростуць?

    100 users → 10 million users
    

    4. Надзеянасць

    Шта будзе, калі системы, ад якіх залежыць гэты функцыянал, перастануць працаваць?

    Database unavailable
    

    5. Эксплуатаваннасць

    Якшо ў чымсь з’явіцца проблема, чы можна яе дыагноставаць і вылечыць?

    Request failed at 03:42:17
    

    Аналіз гэтых пяць слоёў часта выявляе значны лік проблемаў, якія не дайшлі да фазы выпуску.

    З «Це працуе» да «Це вытрымаеся»

    Існуе рэальны разлік межа програмам, якія проста працуюць, і програмамі, якія можуць вытрымаць вплыв рэальнага свету.

    Локальная версія програмы павінна падтвердзіць толькі адно:

    feature works
    

    Версія для выпуску павінна падтвердзіць калькую прычын:

    feature works
    feature fails safely
    feature handles load
    feature protects data
    feature can be monitored
    feature can recover
    feature can be deployed
    feature can be rolled back
    

    Гэта значна вышэйшы стандарт, які трэба адпрацаваць. Саме гэта і робіць інжынерыю бэкенду сапраўдзіка цікавай як даследжэння.

    Штырна працыны зноў

    Ёсьць кароткая падсумовка всага, што было рассказана.

    1. Відсутнае налаштаванне сяродовы

    Не прыпускайце, што тое, што є на вашам ноутбуку, будзе таксама ў версіи для выпуску. Пераканайцеся, што неякія неабходныя зменныя сяродовы є пад час запуску, і яўна паведзіце пра абсэнцыю, якщо ўсё не так.

    2. Жорсткая настройка localhost

    Усередине контейнера localhost апелюе толькі да самы контейнер. Неабяжвайцеся на правільную адказу пра службы і налашоўкі, якія адаптуюцца па супакой.

    3. Стварэнне з’язкаў з базай дадзеных па кожны запыт

    Ожывайце пул з’язкаў і ведзіце лёсткі пра загальную колькасць з’язкаў у всіх працуючых экземплярах вашага прыёмніка.

    4. Вернэнне всага з базай дадзеных

    Утримвайцеся ад вжывання SELECT * з прыzwычкі. Працягвайце толькі тыя поля, якія насправды патрабуюць вашы корыстнікі API.

    5. Ігнараванне індэксаў

    Запыт, які працюе добра з тыячай рэдактаў, можа перастаць працаваць з мільйонамі. Тэставайце з об’ёмамі дадзеных, якія падобныя да рэальных у працы.

    6. Адносаванне локальнага хранення да стойкага

    Контэйнеры, а таксама большая частка хмарных платформ узагалі, не даюць гарантыі таго, што локальныя файлы застануцца. Направляйце контэнт, загружаны корыстнікамі, у сховішча, створаныя для давернай роботы.

    7. Няма обмежэння частоты запыткаў

    Кліенты самі не кантролююць, як часта яны запытаюць ваш API. Застаўляйце обмежэння для тых канчыковых пунктав, якія ўязвімы або коштаўныя у эксплуатацыі.

    8. Дзейнікі сінхронных запыткаў длігага часу

    Складная обробка зазвычай павінна выконвацца у фоновай задачы, а не чакаць, пакуль будзе адпаведны HTTP-запыт.

    9. Дазволаць кліентам вводзіць даны

    Паўтарна перакананайце всё на рубежы бэкенду. Толькі перакананні фронтэнда ніколі не ўстаўляюць.

    10. Плутанне падметаў „процес жывы“ і „застосунак здаровы“

    Апіралюйцеся на перакананні стану, монітарынг, структураваныя логі, паказнікі і апавешчэнні, якія насправды маюць значэнне.

    Заключныя меркі

    Што прыкметнае ў проблемах з адпрацоўкай, так гэта тое, што па першаму вазіранню майже нявядома, каб якія-небудзь з іх былі абавяжлівымі.

    Жорсткая настройка localhost не выглядае як проблема. Відсутнае індэксаванне не выглядае як проблема. Стварэнне новага з’ѐеднання з базайной дадзеных за кожны запит не выглядае як проблема. Відсутнае тайма-аўтаматызаванне не выглядае як проблема. Маршрут API без лімітаў не выглядае як проблема. Запіс файлу безпосередна на дыск не выглядае як проблема.

    Толькі калі з’являюцца рэальныя корыстнікі.

    Тады гэтыя маленькія, на першы погляд безнэпакойныя припускі начынаюць сталачвацца адзін з другім. І раптам вы бачыце:

    slow APIs
    database connection exhaustion
    memory spikes
    timeouts
    failed deployments
    missing files
    duplicate payments
    security issues
    

    Мета не ў тым, каб заздалегідь прагнозаваць кожную можлівую неудачу. Гэта немагчыма. Мета — перастаць ствараць системы на падставе прыпуску, што все завжды будзе йшаць гладка.

    Таму што так не будзе.

    Сеті перыходзяць з працы. Базы дадзеных спавальваюць. Контэйнеры пачынаюцца перзапускацца. Пользоватары надаюць данні, якіх вы ніколі не чакалі. Завісныя элементы таксама перыходзяць з працы. Навантажэння расте без пярэджаў. У сервероў заканчываецца памяць. Запросы дуплікуюцца. Развертванні не выходзяць за межы плану.

    Нічога з гэтага не павінна стаць катастрофай, як толькі ваша система будзе спроектавана з урахоўваннем такіх можлівасцей з самага пачатку.

    Тое, што насправды адразняе бэкенд, який працуе толькі на localhost, ад таго, який вытрымае навантажэння ў працоўным сераўе, — это не мова програмавання, яку вы выбралі. Это падход да проектавання.

    На localhost вопыт прост: чы робіць гэта?

    У працоўным сераўе важлівы вопыт: што будзе, калі гэта не будзе работаць?

    Гэты другі вопыт, які ставяцца знова і знова, навучыць вас гораздка больш пра інжынерыю бэкенда, чым будзь-якая платформа чы інструмент.

    Якщо вы ствараеце свую першую систему для прыменення, стрымайцеся ад спрагі заўсёды ўсё парадкаваць адразу. Пачніце з малага: правільна налаштавьце канфігурацыю, дадзіце памерканне з’ўязкаў, пераканаўце кожны вхідны даны, стварыце індэксы на аднойчыну з тым, як вы фактычна запытваеце даны, часткальна керуйце тайма-аутамі, зберагаўце файлы на правым месцы, дадзіце значамяны логгінг, стежыце за тым, што мае значэнне, і прааналізавайце сценарыя неудач па ходу роботы. Пасля таго продовжайце павтараць гэты процес.

    Гатовасць да прыменення — гэта не пункт, які вы пазначыце раз і пераходзіце да наступнага. Гэта прычынка, якую вы развіваеце з часам як разработчык.

    І паступова гэта станавіцца разлікам межы системы, якая толькі кажа «вона працуе на майм комп’ютеры», і той, якая дэйсна працуе, калі гэта мае значэнне.