返回首页

Fastify 优化:Bloom Filters 和可扩展后端

探索在 Fastify 上创建高负载后端的实用解决方案:自定义 ID、用于用户和图像的 Bloom filters、无登录授权、异步 DB 写入、API 保护,以及 TypeScript 类型定义。

Fastify 后端优化:Bloom Filters 和可扩展架构
Advertisement 728x90

Fastify高负载后端优化:实用解决方案

构建可伸缩、高性能的后端需要深入理解架构权衡。在本文中,我们将探讨我们使用Fastify开发高负载应用的经验,重点关注如何最小化数据库负载、在无传统认证方式下有效识别用户,以及如何借助布隆过滤器和异步操作等工具防范恶意请求。

ID生成策略与高效数据管理

在设计处理大量实体(如用户或图片)的系统时,标识符(ID)格式的选择至关重要。标准的GUID或UUID虽然能保证唯一性,但对于数据库索引和后端处理而言,资源开销可能过大。为了减少这种开销,我们实现了一种混合ID格式:19bc19e00ab-eh5kkeh99q7。其中,第一部分是转换为十六进制的Unix时间戳,第二部分是随机字符序列。连字符纯粹是为了提高可读性。

以下是生成此类ID的示例函数:

Google AdInline article slot
function generateID(): string {
  const timestamp = Date.now().toString(16);
  const randomPart = generateRandomSequence( 11 );
  return timestamp + '-' + randomPart;
}

这种方法提供了一种“条件递增”的ID,因为其初始部分每毫秒都会变化,这有助于提高数据库索引效率。另一个好处是能够直接从ID中解码出实体的创建日期,从而无需在表中单独设置created_at字段。这减少了存储数据量和写入操作次数,尽管它确实对基于日期的查询构建便利性造成了轻微限制。

利用布隆过滤器和无密码认证进行扩展

开发一个旨在支持数千并发会话和数万张图片的应用程序,需要采用最小化CPU和内存消耗的方法。传统的SQL查询来检查已浏览图片或现有用户很快就会成为瓶颈。在这种情况下,布隆过滤器(Bloom filter)发挥了不可或缺的作用。

布隆过滤器是一种概率型数据结构,可以高效地(例如,以99.9999%的概率)判断某个元素是否不在集合中。与传统数据结构相比,其主要优势在于进行成员资格检查时,内存和CPU开销极低。布隆过滤器在本项目中的主要应用包括:

Google AdInline article slot
  • 追踪已浏览图片: 为每个用户维护一个布隆过滤器,存储已展示图片的ID。这使得能够快速检查图片是否已向用户展示过,从而从新批次中消除重复项。
  • 识别现有用户: 后端启动时,所有注册用户ID都会加载到一个全局布隆过滤器中。这提供了即时验证,以判断传入的device_id是否已为系统所知。

为了提升用户便利性并简化应用商店审核,我们实现了一套无需显式认证的识别系统。当应用程序安装时,会生成一个唯一的device_id,该ID随后用于在后端识别用户。为了防范伪造device_id和滥用,我们采用了多阶段验证流程:

  • Fastify JSON Schema 验证: device_id格式的初步验证在Fastify层面进行,使用定义了minLengthmaxLengthpattern的JSON schema。

```javascript

fastify.post( '/backend', {

Google AdInline article slot

  schema: {

    body: {

      type: 'object',

      required: [ 'device_id' ],

      properties: {

        device_id: {

          type: 'string',

          minLength: 23,

          maxLength: 23,

          pattern: '^[a-z0-9]{11}-[a-z0-9]{11}$'

        }

      }

    }

  }

} );

```

  • 用户布隆过滤器: 通过Fastify schema验证后,device_id会与现有用户布隆过滤器进行比对。
  • ID正确性检查:

* 创建日期: ID的第一部分(编码时间戳)必须落在项目启动日期和当前日期之间。

* 随机部分熵值: ID的第二部分(代表随机序列)会检查其熵值。这可以防止使用简单、可预测的ID(例如aaaaaaaaaaaa)。熵值必须高于某个阈值(例如2)。

```javascript

function getEntropyString( str: string ): number {

  const frequencies = new Map();

  for (const char of str) {

    frequencies.set(char, (frequencies.get(char) || 0) + 1);   }

  return [...frequencies.values()].reduce((acc, count) => {

    const p = count / str.length;

    return acc - p * Math.log2(p);

  },

0);

}

```

  • 异步用户插入: 如果ID被认为是新的且有效,它将被添加到现有用户布隆过滤器中,然后异步发送INSERT查询到数据库。省略await可避免阻塞请求处理,显著降低后端负载。这种方法接受在后端意外重启时丢失新用户数据的极低概率,对于此类应用而言,这是一个可接受的权衡,因为“丢失”仅仅意味着重置该用户的滑动历史记录。

优化客户端交互和API保护

移动应用程序与后端之间的高效交互对性能至关重要。对于提供静态内容(如图片),我们使用Nginx,它运行在单独的域上。这最大程度地卸载了Fastify服务器,使其能够专注于业务逻辑,并实现有效的内容缓存。

移动应用程序以10张图片为一批接收。这些图片的ID也会添加到每个用户特有的“已浏览图片”布隆过滤器中。这种方法最大限度地减少了API请求数量:应用程序在启动时加载两批,然后在每10次点赞后请求新批次。这确保了用户在与当前批次交互时,内容流的连续性。

为了保护API免受过载和滥用,我们使用Fastify的rateLimit插件实现了速率限制。根据架构,它可以将Redis用于分布式数据存储,或者在Node.js应用程序的内存中运行以用于单实例。

另一个重要方面是对用户反馈(点赞/踩)的验证,这些反馈以10个为一批发送。此过程涉及多个级别的验证:

  • Fastify Schema 验证: 用户ID、图片ID以及反馈本身(作为enum类型,'like'或'dislike')会根据JSON schema进行检查。
  • 用户布隆过滤器: 通过“现有用户”布隆过滤器确认user_id的存在。
  • 已浏览图片布隆过滤器: 使用用户的个人“已浏览图片”布隆过滤器,验证image_id确实已向当前用户展示过。
  • 反馈批次真实性检查: 实现了一个名为trustLikeInBatch10的函数,用于分析10个反馈批次中的模式。如果所有反馈都相同(全部点赞或全部踩),或者存在过于频繁的交替(例如,8次或更多次切换),则该批次被视为不可靠并被忽略。这有助于过滤掉自动化或恶意行为。

```javascript

function trustLikeInBatch10( batch: string[] ): boolean {

  if( batch.length != feedLimit ) return false;

  const likes = batch.filter( v => v === 'like' ).length;

  if( likes === 0 || likes === feedLimit ) return false;

  const switches = batch.slice(1).reduce( ( acc, val, i ) => acc + ( val !== batch[ i ] ? 1 : 0 ), 0 );

  if( switches >= 8 ) return false;

  return true;

}

```

  • 异步反馈写入: 与新用户类似,反馈会异步写入数据库,无需等待操作完成,且不使用FOREIGN KEY约束。这种方法虽然非传统,但在高强度数据流期间显著降低了数据库负载,牺牲了严格一致性以换取吞吐量。在这种情况下,少量反馈的潜在丢失对于整体统计数据和推荐系统而言并不关键。

为了增强安全性并防止与API验证细节相关的漏洞,Fastify因JSON schema不匹配而生成的错误会被隐藏。后端会返回一个通用响应,而不是详细的问题描述。这使得攻击者更难逆向工程数据模式。

fastify.setErrorHandler((
  error: FastifyError,
  request: FastifyRequest,
  reply: FastifyReply ) =>
{
  console.error( 'setErrorHandler', error );
  countErrorStatistics();
  reply.code(200).send( 'Hello World!' );
});

客户端应用程序必须准备好处理此类通用响应,这是整体安全策略的一部分。

利用TypeScript提升代码可靠性

使用TypeScript显著提升了代码的可靠性和可维护性。具体来说,在处理固定字符串状态集(例如,图片状态)时,您可以优雅地从字符串数组创建类型。这确保了变量的严格类型,消除了因拼写错误或使用无效值引起的错误。

// 通常是这样:
const imageStateArray1 = [ 'new', 'broken', 'gpt', 'ready', 'download', 'done', 'bad' ];
type tpImageState1 = 'new' | 'broken' | 'gpt' | 'ready' | 'download' | 'done' | 'bad';

// 或者更好:
const imageStateArray2 = [ 'new', 'broken', 'gpt', 'ready', 'download', 'done', 'bad' ] as const;
type tpImageState2 = ( typeof imageStateArray2 )[ number ];

第二种方法,使用as const(typeof array)[number],更受青睐,因为它会自动从数组推断字面量类型,确保数组值与其类型同步,避免重复。

核心要点

  • 布隆过滤器在检查元素是否存在时显著降低数据库负载,这对高负载系统至关重要。
  • 结合时间戳的自定义ID生成,允许嵌入元数据并优化索引,同时消除冗余的created_at字段。
  • 异步数据库写入操作和省略FOREIGN KEY约束,以潜在但可接受的数据丢失为代价,提升了高强度应用中的后端吞吐量。
  • 在Fastify层面进行详细的入站请求验证和数据熵值检查,可保护API免受滥用和格式错误数据的侵害。
  • 隐藏Fastify验证错误通过模糊化模式的内部结构,使潜在攻击者更难进行逆向工程,从而增强了API安全性。

— Editorial Team

Advertisement 728x90

继续阅读