Переход с Node.js на Bun в продакшене: как мы реально мигрировали сервис без остановки API

mr. Cooper 1 день назад Инсайды и новости
Переход с Node.js на Bun в продакшене: как мы реально мигрировали сервис без остановки API

Когда Bun только появился, он выглядел как очередной “быстрый Node.js”. Обычно такие проекты быстро исчезают из поля зрения. Но в какой-то момент становится понятно, что Bun - это уже не эксперимент, а реальный runtime, который можно поставить рядом с Node.js в продакшене.

И вот тут возникает нормальный инженерный вопрос: можно ли вообще заменить Node.js, который уже обслуживает трафик, на Bun так, чтобы ничего не упало.

Я разберу именно такой сценарий - не “hello world”, а живой backend под нагрузкой.

Как выглядел исходный сервис

Допустим, у нас обычный backend на Node.js. Ничего экзотического: Express, несколько API-роутов, база данных и nginx перед ним.

import express from "express";

const app = express();

app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

app.get("/api/data", async (req, res) => {
  res.json({ source: "node", data: "hello" });
});

app.listen(3000, () => {
  console.log("Node.js service running on :3000");
});

Он уже в продакшене, и у него есть трафик. То есть это не лаборатория, где можно “перезапустить и посмотреть”.

Первое, что ломает иллюзию “простой замены”

Инстинктивно кажется, что можно просто взять и запустить это через Bun:

bun run server.js

И иногда это действительно работает.

Но в реальном проекте почти сразу всплывает другая картина: часть зависимостей ведёт себя по-другому, какие-то npm пакеты начинают ругаться, а где-то поведение stream или файловой системы отличается настолько, что приложение вроде бы запускается, но работает не так, как ожидалось.

И вот в этот момент становится понятно, что это не “замена Node.js”, а отдельный runtime со своими особенностями.

Как нормальные миграции выглядят в реальности

Самая большая ошибка - пытаться заменить runtime сразу.

В продакшене никто так не делает, потому что это гарантированный риск даунтайма.

Правильная стратегия всегда одинаковая: Node.js остаётся работать, а Bun поднимается рядом, как второй сервис.

Поднимаем Bun рядом с Node.js

Мы просто переносим тот же API, но уже на Bun runtime.

import { serve } from "bun";

serve({
  port: 3001,
  fetch(req) {
    const url = new URL(req.url);

    if (url.pathname === "/health") {
      return Response.json({ status: "ok", runtime: "bun" });
    }

    if (url.pathname === "/api/data") {
      return Response.json({ source: "bun", data: "hello" });
    }

    return new Response("Not found", { status: 404 });
  },
});

И запускаем:

bun run server.ts

На этом этапе у тебя уже две системы:

Node.js живёт на 3000
Bun живёт на 3001

И самое важное - старый сервис всё ещё обслуживает прод.

Переключение происходит не в коде, а в инфраструктуре

Вся магия миграции на самом деле происходит в nginx.

Сначала он вообще ничего не знает про Bun и продолжает отправлять весь трафик в Node.js:

location / {
    proxy_pass http://127.0.0.1:3000;
}

А потом появляется первый аккуратный шаг - выделяем часть маршрутов под Bun:

location /api/bun/ {
    proxy_pass http://127.0.0.1:3001/;
}

И вот это момент, который обычно недооценивают. Ты не “переписываешь систему”, ты начинаешь делить трафик между двумя runtime-ами.

Что начинает ломаться (и это нормально)

Самое интересное начинается не в момент запуска, а в момент реальной нагрузки.

Первое, что обычно всплывает - зависимости. Некоторые npm пакеты просто не дружат с Bun. Особенно те, что завязаны на низкоуровневые Node.js API или native модули.

Вторая вещь - различия в файловой системе и stream API. Где-то код работает одинаково, а где-то появляются неожиданные расхождения в поведении.

И третье - это неочевидные edge cases, которые в Node.js “как-то работали”, а в Bun начинают вести себя честнее, но неожиданнее.

Как выглядит продакшен-запуск

Когда всё более-менее стабильно, Bun обычно заводят как systemd-сервис.

[Unit]
Description=Bun API
After=network.target

[Service]
WorkingDirectory=/var/www/app
ExecStart=/root/.bun/bin/bun run server.ts
Restart=always

[Install]
WantedBy=multi-user.target

И дальше система уже живёт как два параллельных сервиса.

Самый важный момент миграции

Если отбросить все технические детали, суть миграции очень простая:

Ты не “переписываешь Node.js на Bun”.

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

И только после этого принимается решение - оставлять ли его полностью или нет.

Когда Bun действительно выигрывает

В реальности Bun чаще всего выигрывает не “скоростью API”, а тем, что упрощает инфраструктуру:

  • быстрее стартует приложение

  • быстрее ставятся зависимости

  • проще dev-цикл

  • меньше инструментов вокруг

Но это не магическая замена Node.js. Это просто более новый runtime, который хорошо работает в определённых сценариях.

Финальный вывод

Самая здоровая стратегия миграции выглядит так:

Node.js остаётся стабильной основой
Bun подключается рядом
трафик переводится постепенно
и только потом принимается решение

И это, пожалуй, главный инсайт, который редко пишут в статьях про Bun.

Комментарии

Пока нет комментариев. Будьте первым, кто напишет.

Чтобы оставить комментарий, войдите в аккаунт.

Похожие статьи