序列化成本

2025-06-10

序列化成本

在上一篇文章中,我们讨论了检查任何给定查询的性能,但这并不是导致“慢查询”的唯一原因。将数据从数据库移动到请求者的成本也可能很高,有时甚至比查询本身的成本还要高。在本文中,我将尝试向您展示为什么这很重要,并介绍一些相关信息。

对于本文,我们将重点介绍其中的某些部分,主要是从数据库向后端发送数据,以及其他一些方面:

概括

  1. 通常会发生什么
  2. 正在传输的数据
  3. 双重序列化
  4. 总结

通常会发生什么

当你有一个来自后端的解耦客户端时,你通常会有类似这样的情况:

  • 客户提出请求
  • 后端接收请求
  • 验证用户身份
    • 也许可以通过数据库调用来获取用户
  • 进行一个或多个数据库调用
    • 尽可能使用单个查询是避免向数据库请求数据负担的最佳方法
  • 将响应发送回客户端
    • 也许你会在发回数据之前对其进行转换

您在列表中看不到的是与操作相关的多个序列化和反序列化成本。从数据库或任何外部信息源获取数据都会产生反序列化成本,而当您序列化回客户端时,您又需要再次支付这笔费用。

“我为什么要为此付费?”你问,每个数据交互点:数据库、服务器、客户端;它们每个都有自己的数据处理域,即使是同一个域(TypeScript 服务器/客户端),你仍然需要在它们之间发送数据,为此你需要一种通用语言来发送,并让接收者理解和解析它。


正在传输的数据

传输的数据是什么样的?Postgres 有两种数据格式:文本和二进制。

# Packet: t=1695825669.456395, session=213070643341250
PGSQL: type=Query, F -> B
QUERY query=select * from users limit 200;

# Packet: t=1695825669.458287, session=213070643341250
PGSQL: type=RowDescription, B -> F
ROW DESCRIPTION: num_fields=3
  ---[Field 01]---
  name='id'
  type=1043
  type_len=65535
  type_mod=4294967295
  relid=57371
  attnum=1
  format=0
  ---[Field 02]---
  name='username'
  type=25
  type_len=65535
  type_mod=4294967295
  relid=57371
  attnum=2
  format=0
  ---[Field 03]---
  name='created_at'
  type=1114
  type_len=8
  type_mod=4294967295
  relid=57371
  attnum=3
  format=0

# Packet: t=1695825669.458287, session=213070643341250
PGSQL: type=DataRow, B -> F
DATA ROW num_values=3
  ---[Value 0001]---
  length=26
  value='01H7GXCFN7K4ZQEDP59G31SY4D'
  ---[Value 0002]---
  length=10
  value='username-1'
  ---[Value 0003]---
  length=26
  value='2023-08-10 23:44:21.689678'

# Packet: t=1695825669.458287, session=213070643341250
PGSQL: type=DataRow, B -> F
DATA ROW num_values=3
  ---[Value 0001]---
  length=26
  value='01H7GXCFN7865YD7Q16BXEAB8N'
  ---[Value 0002]---
  length=10
  value='username-2'
  ---[Value 0003]---
  length=26
  value='2023-08-10 23:44:21.689678'

...

# Packet: t=1695825677.943246, session=213070643341250
PGSQL: type=BackendKeyData, B -> F
BACKEND KEY DATA pid=1463956811, key=1412773965

# Packet: t=1695825677.943246, session=213070643341250
PGSQL: type=NotificationResponse, B -> F
NOTIFICATION RESPONSE pid=1970496882, channel='name-8785', payload=''

...
Enter fullscreen mode Exit fullscreen mode

这是从后端(postgres)发送到客户端(psql)的部分数据;

  1. PGSQL: type=RowDescription, B -> F:此部分消息表明这是一条“RowDescription”消息。此消息由后端(B)发送到前端(F),通常作为查询执行时结果集描述的一部分。
  2. 行描述:num_fields=3:此部分告诉您 RowDescription 消息正在描述一行包含三个字段(列)。
  3. [字段 01]:此部分提供有关该行中第一个字段(列)的信息。
  • name='id':该字段名为“id”,表示它对应结果集中名为“id”的列。
  • type=1043:type 字段指定该列的数据类型。在本例中,type 表示为整数,“1043”对应于 PostgreSQL 中的特定数据类型。
  • type_len=65535:“type_len”字段指示数据类型的长度。在本例中,它被设置为一个非常大的值 65535,该值可能用于表示最大长度未定义的可变长度字符串(例如 varchar)。
  • type_mod=4294967295:“type_mod”字段通常表示数据类型的修饰符。在本例中,较大的值“4294967295”可能表示该数据类型没有修饰符。
  • relid=57371:relid 字段通常指的是该列所属表的 OID(对象 ID)。在本例中,57371 就是关联表的 OID。
  • attnum=1:“attnum”字段表示表中列的属性编号。它设置为“1”,表示这是表中的第一列。
  • format=0:“format”字段指定数据的格式。值为“0”表示数据为文本格式。在二进制模式下,此值将被设置为“1”。

如果你有更深入的了解,你可能会想:“为什么不使用像 protobuf 这样的东西?你可以让这个过程更快!”

是的,没错。但是使用protobuf,你需要事先知道服务器和客户端发送的数据的结构,这就违背了获取更少数据或使用sql聚合的初衷。


双重序列化

文章中的序列化是指获取数据,转换为共享的通用语言(json、yaml、xml 等),然后将其发送给请求将其反序列化为已知数据的人员。

但究竟是什么呢double serialization?双重序列化是指针对同一请求对数据进行两次序列化/反序列化。想知道具体是怎么回事吗?数据库 → API → 客户端。每个箭头都表示数据经过序列化、传输,然后反序列化。如果不谨慎处理数据传递,最终可能会产生大量垃圾数据,并导致性能问题,因为这可能是一项繁重的操作。需要传输的数据越多,产生的垃圾就越多。

async function someFunction(_: Request) {
  // Data is first serialized in the database, shipped,
  // then deserialized here to have a valid data object
  const data = await fetchFromDb()

  // Maybe you need to transform the data to aggregate or remove fields
  // const transformedData = someMapFunction(data);

  // You serialize data again here,
  // shipped for the client deserialize it,
  // then transform into something useful for them
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: {
      'Content-Type': 'application/json'
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

总结

或许你从未想过自己搜索和发送了多少信息。毫不留情地收集所有信息要简单得多,但正是这些小细节,像滚雪球一样,开始制造问题。

悲观地思考:

  • 您获取的信息超出了您的需要。
  • 服务器必须做更多的工作来处理和发送信息。
  • 需要通过网络发送的数据包更多,这增加了出现问题和数据包重新发送的可能性,从而增加了延迟。
  • 应用程序需要处理比必要更多的信息,因此需要做更多的工作。
    • 如果它是一个对内存采取更“宽松”方法的应用程序,那么应用程序本身就会变得更重(垃圾收集)。
  • 随着需要发送的信息越来越多,响应也变得越来越繁重。既需要将信息从一个域转换为通用格式,又需要将所有信息再次通过网络发送。
  • 传输了更多信息,现在您也开始为通过网络传输的数据传输付费。
  • 接收信息的客户将需要付出更多努力来处理信息。

仅仅决定获取超出必要范围的信息就产生了巨大的滚雪球效应。或许,当一切都需要快速传递时,​​事后重新考虑传输的信息量是有意义的。但认为这个问题不存在却存在相当大的风险,可能会滚雪球效应越滚越大。

参考

鏂囩珷鏉ユ簮锛�https://dev.to/stneto1/cost-of-serialization-408i
PREV
使用 HTTP 标头实现更快的响应
NEXT
`useEffect()` 和 `async`