VercelとRender.comの比較評価
⚖️ VercelとRender.comの比較評価
Section titled “⚖️ VercelとRender.comの比較評価”分散システム・クラウドアーキテクチャの観点から、VercelとRender.comの比較評価を行います。
⚠️ Vercelは、トランザクション内で外部APIを呼ぶ設計や、長時間実行が必要な非同期処理には不向きです。Outboxパターンなどの適切な設計により一部の問題は解決できますが、プロセス寿命の制約や再実行の不確実性により、Render.comなどの常駐環境が適切なケースがあります。
✅ Render.comは、常駐プロセスとWorkerによる非同期処理が可能なため、トランザクション設計や冪等性が重要なシステムに適しています。ただし、コストとスケーラビリティのトレードオフを考慮する必要があります。
判断フローチャート
Section titled “判断フローチャート”1. トランザクション内で外部APIを呼んでいるか? → YES: Render.comを検討(構造改善後もVercelは困難) → NO: 次のステップへ
2. 長時間実行(5分以上)の非同期処理が必要か? → YES: Render.comを検討 → NO: 次のステップへ
3. プロセス状態を保持する必要があるか? → YES: Render.comを検討 → NO: 次のステップへ
4. Outboxパターンを実装できるか? → YES: Vercelでも可能(ただし制約あり) → NO: Render.comを検討
5. コストを最優先するか? → YES: Vercelを検討(構造改善必須) → NO: Render.comを検討
6. スケーラビリティを最優先するか? → YES: Vercelを検討(構造改善必須) → NO: Render.comを検討1. トランザクション内で外部APIを呼んではいけない理由
Section titled “1. トランザクション内で外部APIを呼んではいけない理由”問題のある実装
Section titled “問題のある実装”問題のあるコード:
// ❌ 悪い例: トランザクション内で外部APIを呼ぶasync function createOrder(orderData: OrderData) { return await db.transaction(async (tx) => { // 1. 注文をデータベースに保存 const order = await tx.order.create({ data: orderData });
// 2. トランザクション内で外部APIを呼ぶ(問題) const paymentResult = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId: order.id, amount: orderData.amount }), });
if (!paymentResult.ok) { throw new Error('Payment failed'); }
// 3. 決済結果を保存 await tx.payment.create({ data: { orderId: order.id, status: 'completed' } });
return order; });}問題点:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
- タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
影響:
- データベースのパフォーマンス低下
- デッドロックの発生
- データの不整合
- ユーザー体験の低下
解決策: Outboxパターン
Section titled “解決策: Outboxパターン”改善された実装:
// ✅ 良い例: Outboxパターンを使用async function createOrder(orderData: OrderData) { return await db.transaction(async (tx) => { // 1. 注文をデータベースに保存 const order = await tx.order.create({ data: orderData });
// 2. Outboxテーブルに外部API呼び出しのタスクを記録 await tx.outbox.create({ data: { eventType: 'PAYMENT_CHARGE', payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }), status: 'PENDING', idempotencyKey: `payment-${order.id}-${Date.now()}`, }, });
// 3. トランザクションをコミット(外部APIは呼ばない) return order; });}
// 別のプロセス/ワーカーでOutboxを処理async function processOutbox() { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, take: 10, });
for (const event of pendingEvents) { try { // 外部APIを呼ぶ const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: event.payload, headers: { 'Idempotency-Key': event.idempotencyKey, }, });
if (response.ok) { // 成功したらOutboxを更新 await db.outbox.update({ where: { id: event.id }, data: { status: 'COMPLETED' }, }); } else { // 失敗したらリトライ用に更新 await db.outbox.update({ where: { id: event.id }, data: { status: 'FAILED', retryCount: { increment: 1 }, }, }); } } catch (error) { // エラーハンドリング await db.outbox.update({ where: { id: event.id }, data: { status: 'FAILED', retryCount: { increment: 1 }, }, }); } }}メリット:
- トランザクションの短縮: データベースのロック時間が短縮される
- 外部障害の分離: 外部APIの障害がトランザクションに影響しない
- 再実行の容易さ: Outboxテーブルから再実行可能
- 冪等性の保証: 冪等キーにより重複実行を防止
2. Outboxパターンの役割と、なぜServerless環境と相性が良いか
Section titled “2. Outboxパターンの役割と、なぜServerless環境と相性が良いか”Outboxパターンの役割
Section titled “Outboxパターンの役割”定義: Outboxパターンは、トランザクション内で外部API呼び出しや非同期処理を記録し、別のプロセスで処理するパターンです。
役割:
- トランザクションの分離: データベーストランザクションと外部API呼び出しを分離
- 確実な配信: トランザクションがコミットされたら、確実に外部APIが呼ばれる
- 再実行の保証: 失敗した処理を再実行可能
- 冪等性の保証: 冪等キーにより重複実行を防止
Serverless環境との相性
Section titled “Serverless環境との相性”なぜServerless環境と相性が良いか:
- イベント駆動: Serverlessはイベント駆動で動作するため、Outboxテーブルの変更をトリガーにできる
- スケーラビリティ: 需要に応じて自動的にスケールする
- コスト効率: 処理が必要な時だけ実行される
- 障害耐性: 個別の関数が失敗しても、他の処理に影響しない
実践例(Vercel):
// Outboxテーブルの定義CREATE TABLE outbox ( id SERIAL PRIMARY KEY, event_type VARCHAR(100) NOT NULL, payload JSONB NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', idempotency_key VARCHAR(255) UNIQUE NOT NULL, retry_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
// Vercel CronでOutboxを処理// vercel.json{ "crons": [{ "path": "/api/process-outbox", "schedule": "*/1 * * * *" // 1分ごと }]}
// api/process-outbox.tsexport default async function handler(req: Request) { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, take: 10, });
for (const event of pendingEvents) { await processEvent(event); }
return Response.json({ processed: pendingEvents.length });}3. Vercelの制約
Section titled “3. Vercelの制約”プロセス寿命の制約
Section titled “プロセス寿命の制約”制約:
// VercelのServerless関数の制約// - 実行時間: 最大10秒(Hobby)、60秒(Pro)、300秒(Enterprise)// - メモリ: 最大1024MB// - プロセス状態: 実行間で保持されない問題のある実装:
// ❌ 悪い例: 長時間実行が必要な処理export default async function handler(req: Request) { // 10分かかる処理(Vercelでは不可能) await longRunningProcess();
return Response.json({ success: true });}影響:
- 長時間実行が必要な処理が実行できない
- プロセス状態を保持できない
- 接続プールなどの状態管理が困難
再実行の不確実性
Section titled “再実行の不確実性”問題:
// VercelのServerless関数は、以下の場合に再実行される可能性がある// 1. タイムアウト// 2. メモリ不足// 3. エラー発生// 4. プラットフォームの都合
// 問題: 再実行が保証されない、または予期しない再実行が発生する実践例:
// ❌ 悪い例: 冪等性がない処理export default async function handler(req: Request) { // 冪等キーがない場合、再実行で重複処理が発生する可能性 await processPayment(req.body.orderId);
return Response.json({ success: true });}
// ✅ 良い例: 冪等キーを使用export default async function handler(req: Request) { const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) { return Response.json({ error: 'Idempotency-Key required' }, { status: 400 }); }
// 冪等キーで重複実行を防止 const existing = await db.processedEvents.findUnique({ where: { idempotencyKey }, });
if (existing) { return Response.json({ success: true, cached: true }); }
await processPayment(req.body.orderId);
await db.processedEvents.create({ data: { idempotencyKey, result: 'success' }, });
return Response.json({ success: true });}非同期処理の不確実性
Section titled “非同期処理の不確実性”問題:
// VercelのServerless関数は、レスポンスを返すと終了する// バックグラウンド処理は保証されない
// ❌ 悪い例: レスポンス後に処理を続行export default async function handler(req: Request) { // レスポンスを返す const response = Response.json({ success: true });
// この処理は実行されない可能性がある await sendEmail(req.body.email);
return response;}解決策:
// ✅ 良い例: Outboxパターンを使用export default async function handler(req: Request) { await db.transaction(async (tx) => { await tx.order.create({ data: req.body });
// Outboxに記録 await tx.outbox.create({ data: { eventType: 'SEND_EMAIL', payload: JSON.stringify({ email: req.body.email }), status: 'PENDING', }, }); });
// レスポンスを返す(メール送信は別プロセスで処理) return Response.json({ success: true });}4. Render.comの特性
Section titled “4. Render.comの特性”常駐プロセス
Section titled “常駐プロセス”特徴:
// Render.comのWebサービス// - 常駐プロセス: 24時間実行される// - プロセス状態: 保持される// - 実行時間: 制限なし(実質的)// - メモリ: プランに応じて設定可能メリット:
- 長時間実行が可能
- プロセス状態を保持できる
- 接続プールなどの状態管理が容易
- バックグラウンド処理が可能
実践例:
// Render.comのWebサービスでOutboxを処理import express from 'express';
const app = express();
// Outboxを定期的に処理setInterval(async () => { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, take: 10, });
for (const event of pendingEvents) { await processEvent(event); }}, 5000); // 5秒ごと
app.listen(3000);Worker
Section titled “Worker”特徴:
// Render.comのWorkerサービス// - 専用のワーカープロセス// - スケーラビリティ: 複数のワーカーを実行可能// - キュー処理との相性が良い実践例:
// Render.comのWorkerでOutboxを処理import Bull from 'bull';
const outboxQueue = new Bull('outbox', { redis: { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || '6379'), },});
// Outboxテーブルを監視してキューに追加setInterval(async () => { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, take: 10, });
for (const event of pendingEvents) { await outboxQueue.add(event); }}, 5000);
// キューから処理outboxQueue.process(async (job) => { const event = job.data; await processEvent(event);});キュー処理との相性
Section titled “キュー処理との相性”実践例:
// Render.comのWorkerとBull/BullMQの組み合わせimport Bull from 'bull';
const paymentQueue = new Bull('payment', { redis: { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || '6379'), },});
// ジョブの処理paymentQueue.process(async (job) => { const { orderId, amount } = job.data;
// 外部APIを呼ぶ const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId, amount }), headers: { 'Idempotency-Key': job.id, // ジョブIDを冪等キーとして使用 }, });
if (!response.ok) { throw new Error('Payment failed'); }
return { success: true };});
// リトライ設定paymentQueue.on('failed', async (job, err) => { if (job.attemptsMade < 3) { // 3回までリトライ await job.retry(); } else { // 3回失敗したらアラート await sendAlert(`Payment failed for order ${job.data.orderId}`); }});5. 冪等キーがない場合に起こり得る事故
Section titled “5. 冪等キーがない場合に起こり得る事故”事故のシナリオ
Section titled “事故のシナリオ”シナリオ1: ネットワークエラーによる重複実行
// ❌ 悪い例: 冪等キーがないasync function chargePayment(orderId: string, amount: number) { // 外部APIを呼ぶ const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId, amount }), });
// ネットワークエラーが発生 // クライアントはタイムアウトエラーを受信 // しかし、サーバー側では処理が完了している可能性がある
// クライアントが再試行 // → 重複決済が発生}影響:
- 重複決済
- データの不整合
- ユーザーの不満
- 財務的な損失
解決策:
// ✅ 良い例: 冪等キーを使用async function chargePayment(orderId: string, amount: number, idempotencyKey: string) { // 冪等キーで重複実行を防止 const existing = await db.payments.findUnique({ where: { idempotencyKey }, });
if (existing) { return existing; // 既に処理済み }
// 外部APIを呼ぶ(冪等キーをヘッダーに含める) const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId, amount }), headers: { 'Idempotency-Key': idempotencyKey, }, });
// 結果を保存 const payment = await db.payments.create({ data: { orderId, amount, idempotencyKey, status: 'completed', }, });
return payment;}シナリオ2: Serverless関数の再実行
// VercelのServerless関数が再実行される場合// 1. タイムアウト// 2. メモリ不足// 3. プラットフォームの都合
// ❌ 悪い例: 冪等キーがないexport default async function handler(req: Request) { // 処理が完了する前にタイムアウト await longRunningProcess();
// 関数が再実行される // → 重複処理が発生}解決策:
// ✅ 良い例: 冪等キーを使用export default async function handler(req: Request) { const idempotencyKey = req.headers['idempotency-key'] || generateIdempotencyKey();
// 冪等キーで重複実行を防止 const existing = await db.processedEvents.findUnique({ where: { idempotencyKey }, });
if (existing) { return Response.json(existing.result); }
const result = await longRunningProcess();
// 結果を保存 await db.processedEvents.create({ data: { idempotencyKey, result: JSON.stringify(result), }, });
return Response.json(result);}6. 構造を直せばVercelで問題ないケース
Section titled “6. 構造を直せばVercelで問題ないケース”ケース1: Outboxパターンを実装できる場合
Section titled “ケース1: Outboxパターンを実装できる場合”条件:
- Outboxパターンを実装できる
- 処理時間が10秒以内(Hobby)、60秒以内(Pro)、300秒以内(Enterprise)
- 冪等キーを実装できる
- 非同期処理がVercel Cronで処理可能
実践例:
// ✅ Vercelで問題ないケース// 1. Outboxパターンを実装async function createOrder(orderData: OrderData) { return await db.transaction(async (tx) => { const order = await tx.order.create({ data: orderData });
// Outboxに記録 await tx.outbox.create({ data: { eventType: 'PAYMENT_CHARGE', payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }), status: 'PENDING', idempotencyKey: `payment-${order.id}-${Date.now()}`, }, });
return order; });}
// 2. Vercel CronでOutboxを処理// vercel.json{ "crons": [{ "path": "/api/process-outbox", "schedule": "*/1 * * * *" }]}
// api/process-outbox.tsexport default async function handler(req: Request) { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, take: 10, });
for (const event of pendingEvents) { // 10秒以内で処理可能 await processEvent(event); }
return Response.json({ processed: pendingEvents.length });}ケース2: 同期処理のみの場合
Section titled “ケース2: 同期処理のみの場合”条件:
- 外部API呼び出しが同期処理のみ
- 処理時間が短い(10秒以内)
- 冪等キーを実装できる
実践例:
// ✅ Vercelで問題ないケースexport default async function handler(req: Request) { const idempotencyKey = req.headers['idempotency-key'];
// 冪等キーで重複実行を防止 const existing = await db.processedEvents.findUnique({ where: { idempotencyKey }, });
if (existing) { return Response.json(existing.result); }
// 短時間の同期処理 const result = await shortSyncProcess();
await db.processedEvents.create({ data: { idempotencyKey, result: JSON.stringify(result), }, });
return Response.json(result);}7. 構造を直してもVercelでは厳しいケース
Section titled “7. 構造を直してもVercelでは厳しいケース”ケース1: 長時間実行が必要な処理
Section titled “ケース1: 長時間実行が必要な処理”条件:
- 処理時間が10分以上
- プロセス状態を保持する必要がある
- ストリーミング処理が必要
実践例:
// ❌ Vercelでは困難なケース// 1. 長時間実行が必要な処理async function processLargeFile(fileId: string) { // 10分かかる処理 await processFile(fileId);
// Vercelのタイムアウト(最大300秒)を超える}
// 2. ストリーミング処理async function streamData() { // ストリーミング処理はVercelでは困難 const stream = createReadStream('large-file.csv'); // 処理...}解決策: Render.comを使用
// ✅ Render.comで解決// Render.comのWebサービスで長時間実行app.post('/api/process-large-file', async (req, res) => { const { fileId } = req.body;
// バックグラウンドで処理 processLargeFile(fileId).then(() => { // 完了通知 });
res.json({ status: 'processing' });});ケース2: リアルタイム処理が必要な場合
Section titled “ケース2: リアルタイム処理が必要な場合”条件:
- WebSocket接続が必要
- 長時間の接続を保持する必要がある
- プロセス状態を保持する必要がある
実践例:
// ❌ Vercelでは困難なケース// WebSocket接続はVercelでは困難import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => { // 長時間の接続を保持 // VercelのServerless関数では困難});解決策: Render.comを使用
// ✅ Render.comで解決// Render.comのWebサービスでWebSocketimport { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => { // 長時間の接続を保持可能 ws.on('message', (message) => { // メッセージ処理 });});ケース3: 複雑なキュー処理が必要な場合
Section titled “ケース3: 複雑なキュー処理が必要な場合”条件:
- 優先度付きキュー
- 遅延実行
- 複雑なリトライロジック
実践例:
// ❌ Vercelでは困難なケース// 複雑なキュー処理はVercel Cronでは困難// - 優先度付きキュー// - 遅延実行// - 複雑なリトライロジック解決策: Render.comを使用
// ✅ Render.comで解決// Render.comのWorkerでBull/BullMQを使用import Bull from 'bull';
const queue = new Bull('tasks', { redis: { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || '6379'), },});
// 優先度付きキューqueue.add({ task: 'high-priority' }, { priority: 1 });queue.add({ task: 'low-priority' }, { priority: 10 });
// 遅延実行queue.add({ task: 'delayed' }, { delay: 60000 }); // 1分後
// 複雑なリトライロジックqueue.process(async (job) => { try { await processTask(job.data); } catch (error) { if (job.attemptsMade < 3) { throw error; // リトライ } else { // 3回失敗したら別のキューに移動 await failedQueue.add(job.data); } }});8. Render.com(または常駐環境)を選ぶべき判断基準
Section titled “8. Render.com(または常駐環境)を選ぶべき判断基準”1. 処理時間が長い(5分以上)
// 判断基準: 処理時間が5分以上// → Render.comを選ぶ
// 例: 大容量ファイルの処理async function processLargeFile(fileId: string) { // 10分かかる処理 await processFile(fileId);}2. プロセス状態を保持する必要がある
// 判断基準: プロセス状態を保持する必要がある// → Render.comを選ぶ
// 例: 接続プール、セッション管理const connectionPool = new ConnectionPool();
// プロセス間で状態を保持app.use((req, res, next) => { req.db = connectionPool.getConnection(); next();});3. リアルタイム処理が必要
// 判断基準: WebSocket、Server-Sent Eventsが必要// → Render.comを選ぶ
// 例: リアルタイムチャットconst wss = new WebSocketServer({ port: 8080 });wss.on('connection', (ws) => { // 長時間の接続を保持});4. 複雑なキュー処理が必要
// 判断基準: 優先度付きキュー、遅延実行、複雑なリトライロジック// → Render.comを選ぶ
// 例: Bull/BullMQを使用したキュー処理const queue = new Bull('tasks');queue.add({ task: 'high-priority' }, { priority: 1 });5. トランザクション内で外部APIを呼ぶ必要がある(構造改善が困難)
// 判断基準: 構造改善が困難で、トランザクション内で外部APIを呼ぶ必要がある// → Render.comを選ぶ
// 例: レガシーコードの移行が困難async function legacyFunction() { return await db.transaction(async (tx) => { // 構造改善が困難な場合 const result = await externalAPI.call(); // ... });}9. 段階的な改善ステップ(全移行しない現実解)
Section titled “9. 段階的な改善ステップ(全移行しない現実解)”ステップ1: 現状分析と優先度付け
Section titled “ステップ1: 現状分析と優先度付け”実施内容:
// 1. 現状のコードを分析// - トランザクション内で外部APIを呼んでいる箇所を特定// - 処理時間が長い箇所を特定// - 冪等キーがない箇所を特定
// 2. 優先度を決定const priorities = [ { issue: 'トランザクション内で外部APIを呼ぶ', priority: 'HIGH' }, { issue: '冪等キーがない', priority: 'HIGH' }, { issue: '処理時間が長い', priority: 'MEDIUM' }, { issue: 'エラーハンドリングが不十分', priority: 'MEDIUM' },];ステップ2: 緊急度の高い箇所から改善
Section titled “ステップ2: 緊急度の高い箇所から改善”実施内容:
// 1. トランザクション内で外部APIを呼んでいる箇所をOutboxパターンに変更// 例: 決済処理async function createOrder(orderData: OrderData) { return await db.transaction(async (tx) => { const order = await tx.order.create({ data: orderData });
// Outboxに記録 await tx.outbox.create({ data: { eventType: 'PAYMENT_CHARGE', payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }), status: 'PENDING', idempotencyKey: `payment-${order.id}-${Date.now()}`, }, });
return order; });}
// 2. 冪等キーを追加// すべての外部API呼び出しに冪等キーを追加async function callExternalAPI(data: any, idempotencyKey: string) { const response = await fetch('https://api.example.com/endpoint', { method: 'POST', body: JSON.stringify(data), headers: { 'Idempotency-Key': idempotencyKey, }, });
return response.json();}ステップ3: Vercel CronでOutboxを処理
Section titled “ステップ3: Vercel CronでOutboxを処理”実施内容:
// 1. Vercel Cronを設定// vercel.json{ "crons": [{ "path": "/api/process-outbox", "schedule": "*/1 * * * *" // 1分ごと }]}
// 2. Outbox処理関数を実装// api/process-outbox.tsexport default async function handler(req: Request) { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, take: 10, });
for (const event of pendingEvents) { try { await processEvent(event); } catch (error) { // エラーハンドリング await db.outbox.update({ where: { id: event.id }, data: { status: 'FAILED', retryCount: { increment: 1 }, }, }); } }
return Response.json({ processed: pendingEvents.length });}ステップ4: 困難な箇所をRender.comに移行
Section titled “ステップ4: 困難な箇所をRender.comに移行”実施内容:
// 1. 長時間実行が必要な処理をRender.comに移行// Render.comのWebサービスで処理app.post('/api/process-large-file', async (req, res) => { const { fileId } = req.body;
// バックグラウンドで処理 processLargeFile(fileId).then(() => { // 完了通知 });
res.json({ status: 'processing' });});
// 2. VercelからRender.comのAPIを呼ぶ// api/process-file.tsexport default async function handler(req: Request) { const { fileId } = await req.json();
// Render.comのAPIを呼ぶ const response = await fetch('https://your-app.onrender.com/api/process-large-file', { method: 'POST', body: JSON.stringify({ fileId }), });
return Response.json(await response.json());}ステップ5: ハイブリッド構成
Section titled “ステップ5: ハイブリッド構成”実施内容:
// 1. Vercel: フロントエンド、API、短時間処理// - API: 短時間の同期処理// - Outbox処理: Vercel Cron
// 2. Render.com: 長時間処理、キュー処理// - 長時間実行が必要な処理// - キュー処理(Bull/BullMQ)// - WebSocket接続
// 3. 連携方法// - VercelからRender.comのAPIを呼ぶ// - Render.comからVercelのAPIを呼ぶ// - データベースを共有Vercel向き / 不向きの整理
Section titled “Vercel向き / 不向きの整理”Vercel向き
Section titled “Vercel向き”条件:
- 処理時間が短い(10秒以内、Hobby)
- 同期処理のみ
- Outboxパターンを実装できる
- 冪等キーを実装できる
- スケーラビリティが重要
- コストを抑えたい
実践例:
// ✅ Vercel向き// 1. 短時間の同期処理export default async function handler(req: Request) { const result = await shortSyncProcess(); return Response.json(result);}
// 2. Outboxパターンを実装した非同期処理export default async function handler(req: Request) { await db.transaction(async (tx) => { await tx.order.create({ data: req.body }); await tx.outbox.create({ data: { eventType: 'SEND_EMAIL', ... } }); }); return Response.json({ success: true });}Vercel不向き
Section titled “Vercel不向き”条件:
- 処理時間が長い(5分以上)
- プロセス状態を保持する必要がある
- リアルタイム処理が必要
- 複雑なキュー処理が必要
- トランザクション内で外部APIを呼ぶ必要がある(構造改善が困難)
実践例:
// ❌ Vercel不向き// 1. 長時間実行が必要な処理export default async function handler(req: Request) { // 10分かかる処理(Vercelでは不可能) await longRunningProcess(); return Response.json({ success: true });}
// 2. WebSocket接続const wss = new WebSocketServer({ port: 8080 });// Vercelでは困難Render.com向き / 不向きの整理
Section titled “Render.com向き / 不向きの整理”Render.com向き
Section titled “Render.com向き”条件:
- 処理時間が長い(5分以上)
- プロセス状態を保持する必要がある
- リアルタイム処理が必要
- 複雑なキュー処理が必要
- トランザクション内で外部APIを呼ぶ必要がある(構造改善が困難)
実践例:
// ✅ Render.com向き// 1. 長時間実行が必要な処理app.post('/api/process-large-file', async (req, res) => { const { fileId } = req.body; await processLargeFile(fileId); // 10分かかる処理 res.json({ success: true });});
// 2. WebSocket接続const wss = new WebSocketServer({ port: 8080 });wss.on('connection', (ws) => { // 長時間の接続を保持});Render.com不向き
Section titled “Render.com不向き”条件:
- スケーラビリティが最重要
- コストを最小限に抑えたい
- 短時間の同期処理のみ
- トラフィックが不安定
実践例:
// ❌ Render.com不向き// 1. 短時間の同期処理のみ(Vercelの方が適している)app.get('/api/health', (req, res) => { res.json({ status: 'ok' });});推奨アーキテクチャ構成
Section titled “推奨アーキテクチャ構成”ハイブリッド構成(推奨)
Section titled “ハイブリッド構成(推奨)”構成:
┌─────────────────┐│ Vercel ││ - Frontend ││ - API Routes ││ - Cron Jobs │└────────┬────────┘ │ │ HTTP API │┌────────▼────────┐│ Render.com ││ - Web Service ││ - Workers ││ - Queue │└────────┬────────┘ │ │┌────────▼────────┐│ Database ││ - PostgreSQL ││ - Redis │└─────────────────┘実装例:
// Vercel: フロントエンドとAPIexport async function POST(req: Request) { const orderData = await req.json();
// Outboxパターンで記録 const order = await db.transaction(async (tx) => { const order = await tx.order.create({ data: orderData }); await tx.outbox.create({ data: { eventType: 'PROCESS_ORDER', payload: JSON.stringify({ orderId: order.id }), status: 'PENDING', idempotencyKey: `order-${order.id}`, }, }); return order; });
return Response.json(order);}
// Vercel Cron: Outbox処理// api/process-outbox.tsexport default async function handler(req: Request) { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, take: 10, });
for (const event of pendingEvents) { // Render.comのAPIを呼ぶ(長時間処理が必要な場合) if (event.eventType === 'PROCESS_LARGE_FILE') { await fetch('https://your-app.onrender.com/api/process', { method: 'POST', body: event.payload, }); } else { // 短時間処理はVercelで処理 await processEvent(event); } }
return Response.json({ processed: pendingEvents.length });}
// Render.com: 長時間処理とキュー// server.tsimport express from 'express';import Bull from 'bull';
const app = express();const queue = new Bull('tasks', { redis: { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || '6379'), },});
// 長時間処理app.post('/api/process', async (req, res) => { const { orderId } = req.body;
// キューに追加 await queue.add({ orderId }, { attempts: 3, backoff: { type: 'exponential', delay: 2000, }, });
res.json({ status: 'queued' });});
// キュー処理queue.process(async (job) => { const { orderId } = job.data;
// 長時間処理 await processOrder(orderId);});今すぐやるべき対応(優先度順)
Section titled “今すぐやるべき対応(優先度順)”優先度1: トランザクション内の外部API呼び出しを排除
Section titled “優先度1: トランザクション内の外部API呼び出しを排除”実施内容:
// 1. トランザクション内で外部APIを呼んでいる箇所を特定// 2. Outboxパターンに変更// 3. 冪等キーを追加
// 例: 決済処理async function createOrder(orderData: OrderData) { return await db.transaction(async (tx) => { const order = await tx.order.create({ data: orderData });
// Outboxに記録 await tx.outbox.create({ data: { eventType: 'PAYMENT_CHARGE', payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }), status: 'PENDING', idempotencyKey: `payment-${order.id}-${Date.now()}`, }, });
return order; });}優先度2: 冪等キーの実装
Section titled “優先度2: 冪等キーの実装”実施内容:
// 1. すべての外部API呼び出しに冪等キーを追加// 2. データベースに冪等キーのインデックスを作成// 3. 重複実行を防止
// 例: 冪等キーの実装async function callExternalAPI(data: any, idempotencyKey: string) { // 既に処理済みか確認 const existing = await db.processedEvents.findUnique({ where: { idempotencyKey }, });
if (existing) { return existing.result; }
// 外部APIを呼ぶ const response = await fetch('https://api.example.com/endpoint', { method: 'POST', body: JSON.stringify(data), headers: { 'Idempotency-Key': idempotencyKey, }, });
const result = await response.json();
// 結果を保存 await db.processedEvents.create({ data: { idempotencyKey, result: JSON.stringify(result), }, });
return result;}優先度3: Outbox処理の実装
Section titled “優先度3: Outbox処理の実装”実施内容:
// 1. Outboxテーブルを作成CREATE TABLE outbox ( id SERIAL PRIMARY KEY, event_type VARCHAR(100) NOT NULL, payload JSONB NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', idempotency_key VARCHAR(255) UNIQUE NOT NULL, retry_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE INDEX idx_outbox_status ON outbox(status);CREATE INDEX idx_outbox_idempotency ON outbox(idempotency_key);
// 2. Vercel CronでOutboxを処理// vercel.json{ "crons": [{ "path": "/api/process-outbox", "schedule": "*/1 * * * *" }]}
// 3. Outbox処理関数を実装// api/process-outbox.tsexport default async function handler(req: Request) { const pendingEvents = await db.outbox.findMany({ where: { status: 'PENDING' }, orderBy: { createdAt: 'asc' }, take: 10, });
for (const event of pendingEvents) { try { await processEvent(event);
await db.outbox.update({ where: { id: event.id }, data: { status: 'COMPLETED' }, }); } catch (error) { await db.outbox.update({ where: { id: event.id }, data: { status: 'FAILED', retryCount: { increment: 1 }, }, }); } }
return Response.json({ processed: pendingEvents.length });}優先度4: 長時間処理の分離
Section titled “優先度4: 長時間処理の分離”実施内容:
// 1. 長時間処理が必要な箇所を特定// 2. Render.comに移行するか、非同期処理に変更
// 例: 大容量ファイルの処理// VercelからRender.comのAPIを呼ぶexport default async function handler(req: Request) { const { fileId } = await req.json();
// Render.comのAPIを呼ぶ const response = await fetch('https://your-app.onrender.com/api/process-large-file', { method: 'POST', body: JSON.stringify({ fileId }), });
return Response.json(await response.json());}優先度5: エラーハンドリングとリトライの実装
Section titled “優先度5: エラーハンドリングとリトライの実装”実施内容:
// 1. エラーハンドリングを実装// 2. リトライロジックを実装// 3. アラートを設定
// 例: リトライロジックasync function processEvent(event: OutboxEvent) { const maxRetries = 3;
for (let i = 0; i < maxRetries; i++) { try { await callExternalAPI(JSON.parse(event.payload), event.idempotencyKey); return; } catch (error) { if (i === maxRetries - 1) { // 最大リトライ回数に達したらアラート await sendAlert(`Failed to process event ${event.id} after ${maxRetries} retries`); throw error; }
// 指数バックオフでリトライ await sleep(Math.pow(2, i) * 1000); } }}VercelとRender.comの比較評価のポイント:
- 結論: Vercelは適切な設計により一部の問題は解決できるが、プロセス寿命の制約によりRender.comが適切なケースがある
- 判断フローチャート: トランザクション、処理時間、プロセス状態、Outboxパターン、コスト、スケーラビリティを考慮
- Vercel向き: 短時間処理、Outboxパターン実装可能、スケーラビリティ重視
- Render.com向き: 長時間処理、プロセス状態保持、リアルタイム処理、複雑なキュー処理
- 推奨アーキテクチャ: ハイブリッド構成(Vercel + Render.com)
- 今すぐやるべき対応: トランザクション内の外部API呼び出しを排除、冪等キーの実装、Outbox処理の実装、長時間処理の分離、エラーハンドリングとリトライの実装
適切な設計と実行環境の選択により、効率的で安全なシステムを構築できます。