拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从Node到Deno:用YugabyteDB重构高扩展JavaScript API实践

去年我接手了一个用 Express 写的订单 API代码量不算大但每次改动都像在拆雷package.json 里躺着 60 多个直接依赖TypeScript 配置和转译配置叠了三层升级一个底层库要连带排查四五个中间件。最让我难受的是权限模型约等于没有node_modules 里任何一行代码都能访问文件系统和网络安全全靠自觉。那次重构我抱着试试看的心态用了 Deno数据库也顺手从单机 PostgreSQL 换成了完全兼容 PG 协议的 YugabyteDB结果这套组合把我对 JavaScript API 的认知直接刷新了一遍。这篇文章会完整讲清楚我为什么这样选型、如何一步步写出可运行的 API、数据库层的分布式设计怎么做以及部署和踩坑的细节。适合正在维护 Node API、动了换引擎心思的团队也适合对分布式数据库感兴趣但不想换语言的前端和全栈开发者。即使你目前还在用 Express前面讲到的很多理念也能直接迁移过去。1. 为什么把 API 从 Node 迁到 Deno一次选型复盘先说结论Deno 不是 Node 的简单替代品它对 JavaScript 运行时做了一次面向云原生时代的重新定义。而 YugabyteDB 解决的是数据库在数据量涨上去之后怎么继续优雅扩展的问题。这两者组合在一起带来的是一种更省心的 API 生产方式。1.1 我在 Node.js API 上踩过的三组坑第一组坑是依赖地狱。Express 项目跑久了直接依赖和传递依赖加起来动辄几百个包每次 npm install 都要看运气。某次升级 express-validator 从 6.x 到 7.x内部报错和我一丁点关系都没有仅仅因为某个中间件只适配了旧版本整个 CI 就挂了。排错花了两天最后结论是先锁版本等上游适配。第二组坑是 TypeScript 配置链太长。tsconfig.json、Babel 配置、ts-node-dev、ESLint 的 TS 插件一环扣一环。团队新同学入职第一周光是把开发环境跑起来就要处理各种版本匹配问题。这些其实都不是业务问题但每天都在消耗精力。第三组坑是权限。Node 的设计哲学是运行时给足权限由应用自己负责边界这在浏览器里没问题但在服务端会放大供应链攻击的风险。只要某个依赖被污染它可以静默读取你的 /etc/shadow 或者向外部发送环境变量。而 Deno 从架构上把这个问题堵死了没显式授权的能力代码就是不能用。1.2 Deno 给 JavaScript API 带来的现代化能力Deno 的几个特性刚好打在 Node 的痛点上。第一是原生 TypeScript。Deno 内置了类型检查和转译不需要 ts-node不需要 Babel不需要 tsc 构建链。你写的 .ts 文件直接能跑类型检查可以单独用 deno check 在 CI 里做。第二是权限安全模型。启动一个脚本时必须显式声明 --allow-net、--allow-env、--allow-read 等权限默认全部拒绝。这非常像 Android 的运行时权限代码可以访问什么由使用者说了算。第三是标准库完善。Deno 官方标准库提供了 HTTP 服务器、URL 解析、文件操作、测试框架等基础能力用不到第三方依赖就能把 API 写出来。Deno.serve 这个原生 API 性能不错做 REST 服务完全够用。它不像 Express 那样需要先 npm install 一堆东西才能动弹。第四是模块分发现代化。Deno 可以直接通过 URL 引用代码也支持 jsr.io 这种原生 TypeScript 包仓库。去掉 node_modules 之后项目结构清爽太多。依赖统一在 deno.json 的 imports 字段里管理一眼能看完。1.3 YugabyteDB为什么选兼容 PostgreSQL 的分布式数据库数据库层我原本用的是单机 PostgreSQL说实话用着挺好SQL 标准、事务、生态都满意。但业务数据量涨到几十 GB 之后我开始担心几件事磁盘满怎么办主库挂了恢复要多长时间读写压力上来后怎么扩展传统思路是上读写分离、分区、中间件那一套的运维成本对小团队来说并不低。YugabyteDB 吸引我的是它把分布式这件事做成了默认能力同时对外保持 PostgreSQL 兼容。它的 YSQL 接口直接使用 PostgreSQL 的查询层和线协议意味着现成的 pg 驱动、ORM、迁移工具都能用业务代码几乎不用为分布式三个字改写。它底层用 Raft 共识协议做多副本复制节点故障时自动选主不需要人为介入。数据按主键自动分片到多个 tablet节点不够的时候加节点就能水平扩展。这些能力如果自己在单机 PG 上搭工程量会非常可观。和单机 PostgreSQL 对比大家态度通常是这样的对比维度单机 PostgreSQLYugabyteDB扩展方式主从复制、读写分离节点写入能力有限水平分片加节点即可扩展写入能力故障恢复通过 Patroni 等外部工具运维复杂Raft 自动选主节点故障自动恢复驱动兼容PostgreSQL 生态完全复用 PostgreSQL 生态运维成本低数据量下较低自带管理界面和云托管中规中矩适用规模适合百 GB 以下的单体业务适合需要跨区容灾、数据增长快的业务当然如果你的数据量一直不大、也没有高可用要求单机 PG 完全够用没必要为了分布式而分布式。但如果你在做 SaaS 产品、API 服务又要考虑未来三五年增长那用 YugabyteDB 可以让数据库层晚一点成为瓶颈。2. 搭建 Deno 项目骨架与 YugabyteDB 连接层选型归选型真正动手的时候要解决的问题都很具体Deno 怎么装项目结构怎么摆数据库连接串怎么写连接池怎么管。这一章是完整可复现的部分。2.1 环境准备与项目初始化安装 Deno 用官方脚本macOS 和 Linux 执行curl -fsSL https://deno.land/install.sh | shWindows 用户用 PowerShell 或者 Scoopscoop install deno # 或者 irm https://deno.land/install.ps1 | iex装完验证一下版本我当时用的是 1.4x 以上现在建议直接装 Deno 2.xAPI 变化不大长期维护更有利deno --version然后创建一个目录把项目结构先定下来api/ deno.json main.ts config.ts db.ts handlers/ books.ts models/ book.ts middleware/ errorHandler.ts logger.ts cors.ts tests/ api_test.ts .env这个结构参考了常见的分层main.ts 只负责启动 HTTP 服务和组装路由handlers 处理请求响应middleware 处理横切关注点db.ts 管理数据库连接池config.ts 统一读取配置。小项目这样分层不显冗余后期加功能也找得到地方。deno.json 是 Deno 的配置文件我一般这样写{ tasks: { dev: deno run --allow-net --allow-env --allow-read.env --env-file.env --watch main.ts, start: deno run --allow-net --allow-env --allow-read.env --env-file.env main.ts, test: deno test --allow-net --allow-env --allow-read.env --env-file.env }, imports: { postgres: https://deno.land/x/postgresv0.19.3/mod.ts, zod: https://deno.land/x/zodv3.23.8/mod.ts } }这里有几个细节值得说一下。--env-file.env 让 Deno 自动读取 .env 文件注入环境变量不用自己在代码里解析。--allow-read.env 只放行读取 .env比直接 --allow-read 只读整个文件系统要收敛。开发时加 --watch 会热重启体验和 nodemon 差不多。2.2 连接 YugabyteDB端口、SSL 与连接串YugabyteDB 有多个接口最容易踩坑的是端口PostgreSQL 兼容接口 YSQL 的默认端口是 5433不是 5432。YCQLCassandra 兼容是 9042Redis 兼容是 6379。很多人第一次连不上就是下意识用了 5432。本地快速起一个 YugabyteDB 实例用 Docker 最省事docker run -d --name yugabytedb \ -p 5433:5433 \ -p 9042:9042 \ -p 15433:15433 \ yugabytedb/yugabyte:2024.2.0.0启动后YSQL 的连接串是postgres://yugabyte:yugabytelocalhost:5433/yugabyte如果你用的是云托管的 YugabyteDB连接串里通常还要带 sslmode 参数类似这样postgresql://admin:密码节点地址:5433/yugabyte?sslmoderequire我把连接串放到项目根目录的 .env 里DATABASE_URLpostgres://yugabyte:yugabytelocalhost:5433/yugabyte DB_POOL_SIZE10 PORT8000config.ts 里统一读取export const DATABASE_URL Deno.env.get(DATABASE_URL)!; export const DB_POOL_SIZE Number(Deno.env.get(DB_POOL_SIZE) ?? 10); export const PORT Number(Deno.env.get(PORT) ?? 8000);这里用 ! 是告诉 TypeScript 环境变量一定会存在如果缺失尽早报错而不是带病运行。2.3 连接池与请求生命周期管理连接数据库最大的注意点就是连接要复用不能每个请求都新建连接。新连接握手过程开销不小高并发下会直接把数据库打垮。我使用的是 deno postgres 驱动里的 Pool。db.tsimport { Pool } from postgres; import { DATABASE_URL, DB_POOL_SIZE } from ./config.ts; export const pool new Pool(DATABASE_URL, DB_POOL_SIZE); export async function withClientT( fn: (client: AwaitedReturnTypetypeof pool.connect) PromiseT, ): PromiseT { const client await pool.connect(); try { return await fn(client); } finally { client.release(); } }withClient 这个封装很值得养成习惯。它保证无论查询成功还是抛出异常连接都能回到池子里。很多线上事故都是因为忘了在 finally 里 release连接被一个个占死。使用示例export async function findBooks() { return withClient(async (client) { const result await client.queryObject( SELECT id, title, author, price, created_at FROM books ORDER BY created_at DESC LIMIT 100, ); return result.rows; }); }注意 queryObject 返回的对象是行对象数组直接可以用 result.rows 拿数据。老版本的 query 接口返回的是 Row 数组字段要用 index 访问改起来很别扭。如果你用的是 0.19.x 之后的驱动推荐直接用 queryObject。连接池大小也不是越大越好。每个连接在数据库端都要占用一个后端进程资源连接数翻倍不等于性能翻倍反而可能加剧锁竞争。起步设 10 就够了后期根据 P99 延迟再调。3. 用原生 TypeScript 实现 REST 路由与请求校验数据库连上了接下来就是 API 本身。我不打算引入 Express 或 Fastify 这类框架而是直接用 Deno 原生的 HTTP 能力实现这样依赖面最小、权限问题最少也最容易看明白整个请求生命周期。3.1 用标准库 serve还是上框架Deno 生态里有几个选择直接用 Deno.serve或者用 Hono、Oak、Fresh 这类框架。我最终用 Deno.serve 加 URLPattern 手写路由理由是它们能覆盖 90% 的 CRUD 场景而且不引第三方代码。Deno.serve 是原生实现性能很不错底层的 HTTP 实现是经过生产验证的。什么样的项目该考虑框架如果你需要大量现成中间件、复杂的路由嵌套或者团队已经非常熟悉某个框架那用 Hono 更高效。它和 Express 中间件风格很像迁移成本低。但如果你是给团队做一个长期维护的内部 API我的经验是少依赖一个框架就少一份升级负担。框架本身没有问题问题是它会周期性地提醒你又该升版本了。3.2 路由与 CRUD 核心实现我定义了两组路由一组处理集合一组处理单条资源。main.ts 大致长这样import { serve } from https://deno.land/std0.224.0/http/server.ts; import { listBooks, createBook } from ./handlers/books.ts; import { getBook, updateBook, deleteBook } from ./handlers/books.ts; import { withErrorHandler } from ./middleware/errorHandler.ts; import { withLogger } from ./middleware/logger.ts; import { withCors } from ./middleware/cors.ts; const booksPattern new URLPattern({ pathname: /api/books }); const bookPattern new URLPattern({ pathname: /api/books/:id }); Deno.serve(async (req) { return withErrorHandler(withLogger(withCors(async (req) { const url new URL(req.url); if (booksPattern.test(url)) { if (req.method GET) return await listBooks(req); if (req.method POST) return await createBook(req); } if (bookPattern.test(url)) { if (req.method GET) return await getBook(req, url); if (req.method PUT) return await updateBook(req, url); if (req.method DELETE) return await deleteBook(req, url); } return Response.json({ error: Not Found }, { status: 404 }); }))); });URLPattern 是浏览器标准 APIDeno 直接内置了不用额外安装。它从 pathname 里提取 :id 参数比正则表达式直观得多。withCors、withLogger、withErrorHandler 都是自己写的中间件本质上就是把真正处理请求的函数包一层这也是函数式中间件最简单的形态。处理单本书的逻辑export async function getBook(req: Request, url: URL): PromiseResponse { const match new URLPattern({ pathname: /api/books/:id }).exec(url); const id match?.pathname.groups.id; if (!id) { return Response.json({ error: Invalid book id }, { status: 400 }); } const book await findBookById(id); if (!book) { return Response.json({ error: Book not found }, { status: 404 }); } return Response.json(book); }这里要注意URLPattern 每次 new 一个是有开销的所以正式代码应该把 pattern 提到模块顶层复用示例里为了可读性写得随意了些。我实际项目中会把所有路由 pattern 集中在一个 router.ts 里而不是每次请求 new。3.3 请求校验别把前端的不信任数据直接丢给数据库写 API 最基本的原则是永远不信任入站请求体。之前我有一次对接前端时对方传的 createdAt 字段类型不对请求到达数据库驱动后直接报了一个类似400 invalid schema for function的错。这类错误表述往往非常底层调用方根本看不懂而且问题是出在参数校验阶段却要查数据库日志才能定位。所以我在所有修改类接口里都接了 zod 做校验。zod 是一个运行时校验库它能在请求体解析阶段就把数据类型卡死让非法数据根本走不到 SQL 层。定义 Book 的写模型import { z } from zod; export const createBookSchema z.object({ title: z.string().min(1).max(200), author: z.string().min(1).max(100), price: z.number().positive().optional(), tags: z.array(z.string()).max(10).optional(), }); export type CreateBookInput z.infertypeof createBookSchema;在 handler 里export async function createBook(req: Request): PromiseResponse { let body: unknown; try { body await req.json(); } catch { return Response.json({ error: Invalid JSON body }, { status: 400 }); } const parsed createBookSchema.safeParse(body); if (!parsed.success) { return Response.json({ error: Validation failed, fields: parsed.error.flatten().fieldErrors, }, { status: 422 }); } const book await insertBook(parsed.data); return Response.json(book, { status: 201 }); }safeParse 比 try/catch parse 更温和它能一次性把所有字段错误收集起来返回给调用方。用过之后你会发现前端联调的争吵少了一大半不是你后端玄学报错而是字段规则清清楚楚写在响应里。3.4 日志、CORS 与统一错误响应中间件我写了三种。日志中间件负责记录每个请求的方法、路径、状态码、耗时并给请求生成一个 requestId贯穿到数据库查询日志里对排查线上问题非常关键export async function withLogger(req: Request, next: (req: Request) PromiseResponse): PromiseResponse { const start performance.now(); const requestId crypto.randomUUID(); const res await next(req); const duration (performance.now() - start).toFixed(2); console.log(${req.method} ${new URL(req.url).pathname} ${res.status} ${duration}ms ${requestId}); return res; }CORS 中间件要处理 OPTIONS 预检请求不要把它漏掉否则前端浏览器跨域调用会莫名其妙失败const corsHeaders { Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization, }; export async function withCors(req: Request, next: (req: Request) PromiseResponse): PromiseResponse { if (req.method OPTIONS) { return new Response(null, { status: 204, headers: corsHeaders }); } const res await next(req); const wrapped new Response(res.body, res); for (const [k, v] of Object.entries(corsHeaders)) { wrapped.headers.set(k, v); } return wrapped; }错误处理中间件包住整个路由任何 handler 抛出的异常都会转成统一 JSON避免栈信息直接暴露export async function withErrorHandler(req: Request, next: (req: Request) PromiseResponse): PromiseResponse { try { return await next(req); } catch (err) { console.error(unhandled error:, err); return Response.json({ error: Internal Server Error }, { status: 500 }); } }这一套组合下来API 的行为是可预测的成功有成功的数据结构失败有失败的数据结构调用方可以通过 error 字段判断问题方向而不是对接一个又一个零散报错。4. 数据层设计从 PostgreSQL 兼容性到分布式表策略API 的复杂性最终都会沉淀到数据层。YugabyteDB 最大的好处是你依然能用纯 SQL 思考问题但它毕竟是分布式数据库建表的时候需要多考虑一层数据怎么分布。4.1 Schema 设计与建表语句我用 ysqlsh 连接数据库执行建表语句。ysqlsh 相当于 psql用法一致docker exec -it yugabytedb bin/ysqlsh -h localhost -p 5433 -U yugabyte -d yugabyte建一张 books 表CREATE TABLE books ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL, price NUMERIC(10, 2), tags TEXT[], created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );几个设计选择说明一下id 用 UUID 而不是自增 bigint。分布式环境下自增主键会变成全局热点所有插入都落在最后一个 tablet 上。UUID 随机分布写压力能均匀散到各个分片。TEXT[] 数组类型是 PostgreSQL 的特色YugabyteDB 同样支持。如果标签只在展示层用数组比关联表轻量得多不必任何多值都建一张子表。等以后真的需要按标签复杂筛选时再提成关联表也不迟。TIMESTAMPTZ 是带时区的时间类型存储的是 UTC 绝对时间客户端读取时按自己的时区展示。这一点对分布式部署格外重要节点可能分布在不同区域必须统一用绝对时间。4.2 分布式表、分片与主键设计YugabyteDB 默认会把表按主键做 Hash 分片分成多个 tablet 分散在不同节点上。所以你不需要像传统中间件那样自己指定分片键但主键设计直接决定了查询能不能落在少数 tablet 上。写这条 SQL 时我显式指定了分片数量ALTER TABLE books SPLIT INTO 8 TABLETS;8 个 tablet 对多数业务足够了。tablet 数量不是越多越好因为每个 tablet 都有副本和后台任务小表分几十个 tablet 纯属浪费资源。如果表很小、业务也不大甚至可以不拆YugabyteDB 的 colocated 表模式能节省资源。主键设计上要抓住一个核心原则等值查询条件尽量匹配主键的最左列。比如你要频繁按 author 查书那应该把 author 作为主键的一部分或者单独建索引。在分布式环境里一个跨 tablet 的全表扫描可比单机库里慢一个量级所以查询模式要在建表前想清楚。索引同样遵循这个逻辑。我想支持按 title 模糊搜索但这在分布式库里做 LIKE %xxx% 会很吃力我选择先做一个简单的 trigram 索引后面数据量大再考虑全文检索方案。建一个降序索引方便按时间排序分页CREATE INDEX idx_books_created_at ON books (created_at DESC);4.3 事务与并发控制事务在 YugabyteDB 里是完全支持的可以放心用 ACID 事务。但我必须强调一个坑事务必须保持在同一个数据库连接上执行。很多新手在单机库里习惯用类似 pool.query 的便捷函数连续执行多条 SQL这在连接池模式下每条语句可能走了不同连接事务根本建立不起来。正确写法export async function createBookWithAudit(book: CreateBookInput, operatorId: string) { return withClient(async (client) { await client.queryObject(BEGIN); try { await client.queryObject( INSERT INTO books (title, author, price, tags) VALUES ($1, $2, $3, $4) RETURNING id, book.title, book.author, book.price ?? null, book.tags ?? null, ); await client.queryObject( INSERT INTO audit_log (entity, operation, operator_id) VALUES (book, CREATE, $1), operatorId, ); await client.queryObject(COMMIT); } catch (err) { await client.queryObject(ROLLBACK); throw err; } }); }所有语句都在 withClient 拿到的同一个 client 上执行事务边界才有效。另外一个常见问题是并发冲突。分布式数据库发生事务冲突的概率比单机高因为数据副本之间存在同步延迟。YugabyteDB 发生冲突时会返回 PostgreSQL 风格的 40001 序列化失败错误码业务上要做重试。重试逻辑不能写得太激进我一般做成最多 3 次、指数退避第一次等 20ms第二次等 50msconst MAX_RETRY 3; for (let attempt 1; attempt MAX_RETRY; attempt) { try { return await createBookWithAudit(data, operatorId); } catch (err) { if (err?.code 40001 attempt MAX_RETRY) { await delay(20 * attempt * attempt); continue; } throw err; } }这段重试逻辑看起来简单但在真实并发场景下能挡掉很多偶发失败。错误信息里如果看到 40001不用慌这是分布式事务的正常现象关键是应用层怎么优雅处理。4.4 从旧 API 迁移数据与代码重构思路如果你像我一样是从 Node Express pg 的老项目迁移过来路径会比想象中顺畅。YugabyteDB 的 YSQL 兼容 PostgreSQL你原来写的 SQL 大部分可以原样执行。真正要改的是代码层。老的 pg 驱动里查询结果集要自己处理 RowDataPacket 之类的东西而 deno postgres 的 queryObject 直接返回对象数组字段名就是列名序列化成本低很多。在迁移时我做到先通后优先把原接口的所有 SQL 原封不动跑通让响应结构和原来一致再逐条优化变成分布式友好的写法。数据迁移我用的是 pg_dump 大约十万行的表先在旧库里导出为自定义格式再用 ysqlsh 导入。这个操作对数据量不大几十 GB 以内的库完全可行而且不需要停机太久。数据导入完成后跑一遍对账脚本逐表比对行数和关键字段确认无差异再切流量。有个容易忽略的点是 NUMERIC 类型的序列化。PostgreSQL 驱动在查询 NUMERIC 时常返回字符串避免 JavaScript 大数精度丢失。这意味着旧 API 的响应里 price 可能曾经是个数字现在可能是字符串。如果前端有强类型校验这个细节会导致字段类型变更迁移时要主动统一处理要么在 SQL 里 cast要么在序列化层转数字。5. 测试、部署与生产环境踩坑记录代码写完只是开始真正考验人的是如何保证它上线后稳定。这一章讲自动化测试、Docker 部署以及我在这个组合上踩过的几个真坑。5.1 Deno 内置测试框架与集成测试Deno 自带测试框架和 Jest、Vitest 那套比胜在零依赖、无配置。我用 Deno.test 写了一组集成测试直接拉起服务对真实数据库发请求验证完整链路。import { assertEquals, assert } from https://deno.land/std0.224.0/assert/mod.ts; Deno.test(POST /api/books creates a book, async () { const res await fetch(http://localhost:8000/api/books, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ title: Deno in Action, author: Alice, price: 59.9, tags: [deno, api], }), }); assertEquals(res.status, 201); const data await res.json(); assert(data.id); assertEquals(data.title, Deno in Action); });跑测试用 deno task test。测试之间要注意数据隔离我一般每个测试开头先 truncate 相关表避免用例互相影响。如果是更重的测试可以把测试数据库独立成一个 schema跟开发库完全隔离。Deno 的权限参数在测试时也要给全像 --allow-net、--allow-env、--allow-read.env 都不能少否则 fetch 和数据库连接都会被运行时拒绝。如果发现测试莫名其妙报权限错误先检查是不是忘了在 task 里加权限。5.2 生产部署Docker 镜像与最小化权限生产环境我用 Docker 构建镜像。因为 Deno 支持编译为单文件二进制镜像可以做得非常小FROM denoland/deno:2.2.1 AS builder WORKDIR /app COPY . . RUN deno compile --outputserver --allow-net --allow-env --allow-read.env main.ts FROM debian:bookworm-slim WORKDIR /app COPY --frombuilder /app/server . COPY .env .env EXPOSE 8000 CMD [./server]两个明显好处第一最终镜像没有源码和依赖目录攻击面小很多第二deno compile 出来的二进制启动速度非常快内存占用也不大适合在容器环境中频繁调度。如果不想编译直接在容器里跑 deno run 也可以但一定要锁依赖版本。deno.json 里的 imports 已经是固定版本了再加一个 deno.lock 保证可复现构建。默认 Deno 会用 lock 文件提交到仓库就行。健康检查我加了一个轻量接口Deno.serve(async (req) { if (new URL(req.url).pathname /healthz) { return Response.json({ status: ok }, { status: 200 }); } // 其余路由... });在编排平台里配个探针每 10 秒打一次 /healthz后面扩容、发布都能自动摘除不健康实例。5.3 生产环境踩坑连接池耗尽、SSL、批量写入性能连接池耗尽是我遇到最多的生产事故。症状很典型API 请求开始变慢然后全部卡住日志里出现 couldnt connect to postgres 或 connection timeout。排查后基本是同一个原因某个 handler 在执行事务或长查询时抛了异常却没走 release连接被占着不放。修复就是统一用 withClient 模式把释放连接放进 finally。上线后加一个指标比如当前连接池活跃连接数超过 80% 就告警。SSL 问题主要出现在连云托管实例时。YugabyteDB 云服务要求 SSL 连接但自签名证书在本地开发时经常不被信任。我当时的报错是 connection terminated unexpectedly 这类模糊提示。解决方法是本地连的时候在连接串里用 sslmoderequire生产环境用云厂商提供的证书路径配置 sslrootcert。连接串看起来像这样postgresql://user:passwordhost:5433/db?sslmodeverify-fullsslrootcert/path/to/root.crt批量写入性能是第三个坑。最开始我在 API 里循环执行 INSERT一次导 5 万行数据跑了近两分钟。问题不在 Deno而在逐条插入的事务开销。后来改成多行 INSERT一次写 500 行性能提升近一个数量级如果数据量更大用 COPY 命令几乎是唯一高效方案。YugabyteDB 对 COPY 的支持很成熟大批量历史数据迁移我都推荐先用 COPY。另外留意一个细节YugabyteDB 默认端口是 5433但有些云厂商的端口分配和默认不同连接前一定要把控制台上给的完整连接串粘进 .env而不是自己拼接。我见过有人把端口写成 5432 或者 3306折腾了半天才发现是端口问题。5.4 我给现代化 JS API的最后一公里建议走到这一步你的 API 已经用 Deno 跑起来、数据落在 YugabyteDB 上、有测试、有 Docker 镜像。再往后我会建议你补三件事。第一件是保留请求级日志关联 ID。现在日志里已经有了 requestId下一步把数据库查询、第三方调用、响应耗时串到同一个 trace 里。不用上特别重的链路追踪组件先在日志结构化里做好出问题能快速定位到具体请求就够了。第二件是接入 OpenAPI 描述文档。Deno 生态里有 openapi 相关库可以从代码生成规范文档也可以直接用 JSDoc 注释配插件。有了 OpenAPI 文档前端可以直接生成类型安全的客户端 SDK接口变化时编译期就能发现问题这比维护一份 Markdown 接口文档靠谱得多。第三件是把关键接口加上缓存层。YugabyteDB 的响应速度很快但热数据走缓存可以省掉大量数据库连接和网络开销。我通常对列表类接口做 30 秒的短期缓存对单条资源做 60 秒数据库压力能降一半以上。等业务量再涨数据库节点也能从容扩容不必在架构上过度设计。我实际用下来的最大感受是Deno 让 JavaScript API 的开发和运维都变得更简单了YugabyteDB 又让数据库这一层不用过早成为瓶颈。前端团队可以完整掌控从请求路由到数据存储的整条链路而且每一环都有清晰的安全边界。如果你正打算重构手头的 Node API不妨找一个小服务先用 Deno 和 YugabyteDB 试试水跑通一个接口后再决定要不要全量迁移。这套组合的收益不是看文档能体会到的得真正跑起来才知道。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门