Python实现动图生成:轻松创建自定义表情包
API 开发中五个常见的数据库性能错误
构建 API 服务器时,无论使用何种技术,您都会面临一系列基本问题。大多数问题都可以避免,但我仍然看到拥有数十年经验的专业工程师年复一年地在做着同样的工作。
让我们一起走进数据库性能陷阱的花园。我们将讨论您可能会犯哪些错误、如何发现这些错误、如何解决这些错误以及我们是否可以采取预防措施。
错误1:查询不变的信息
当我构建 Avalara AvaTax REST API 时,我必须允许用户发送地址。由于他们的数据很乱,有时他们会发送 ISO 国家代码,或者可能是国家名称,也可能是别名。我可以处理这个问题,因为 GitHub 上有很多具有宽松许可证的 国家数据源 ,但最终我选择付费购买 官方 ISO 3166 国家代码列表 。
下一步是让我的 API 服务器在启动时加载这些数据。代码不必太复杂——这里有一些类似 C# 的伪代码,大致显示了如何使其工作:
private static Task<List<Country>>? _cachedQuery = null ;
private Task<List<Country>> GetCachedCountries()
{
// 将承诺保存到静态变量
if (_cache == null ) {
_cache = Database.Countries.ToListAsync();
}
// 所有调用者加入同一个承诺
return _cache;
}
为什么要这样做?幸运的是,新国家/地区并不经常创建。如果国家/地区列表要更改,我们会在每月的应用程序部署期间发送 SQL 脚本来添加新记录。
我的 C# API 服务器不是查询数据库中的表,而是将这些数据保存在单例中。它会在输入或输出时查找正确的名称。数据只占用几千字节,为了方便起见,我有多个散列的不区分大小写的字典。
您可能有几十个这样的静态数据集。查找数据集、原因代码、配置标志 — 将它们存储在静态单例中!如果您忘记了,您可能会发现您的系统每秒对永远不会改变的数据进行数千次不必要的查询。
错误2:过度使用数据库的状态页面检查
您的 API 服务器需要一个健康检查系统。它可以是一个页面或一个 API,但它应该执行一系列基本功能测试,以确保机器能够正常工作。典型的测试包括:
- 我有正确的配置文件吗?
- 我是否可以联系我需要的外部服务,或者是否有防火墙阻止我?
- 我的服务器是否以正确的凭据和权限运行?
- 我的数据库连接字符串有效吗?
这些类型的状态检查对于启动作为自动扩展组一部分的服务器或使用容器化启动模板是必不可少的。在部署服务器之前,彻底测试所有内容非常重要——启动缺少数据库连接字符串的机器会很糟糕。
这些状态检查的一个副作用是,它们通常也用于监控部署后的服务器整体健康状况。一些云服务会每分钟多次调用此状态页面,如果服务器无法响应,则会从负载平衡器中移除该服务器。如果您的状态页面在此测试中执行查询,这可能会迅速消耗您的数据库。
可以想象,在启动时测试数据库连接至关重要。但是,一旦服务器成功部署,有效的数据库连接以后突然变为无效的可能性就很小。我发现最好将成功的结果缓存一小段时间,比如 30 秒。这意味着我的健康检查仍然可以将有问题的服务器排除在轮换之外,但不会使数据库过载:
public static DateTime LastCheckTime = DateTime.MinValue;
public const int SECONDS_FOR_RETEST = 30 ;
public static bool Status ()
{
var now = DateTime.UtcNow;
var timeSinceLastCheck = now - LastCheckTime;
if (timeSinceLastCheck.TotalSeconds > SECONDS_FOR_RETEST) {
...在这里做一些数据库健康检查 ...
LastCheckTime = now;
}
return true ;
}
错误3:使用过多查询进行 API 验证
大多数重度 API 用户会迅速发出大量请求。对于每个请求,服务器需要检查用户是否经过身份验证,以及他们是否有权执行他们请求的工作。许多这些检查都需要从数据库中提取数据:
- 检索用户和账户的状态
- 检查用户的权限
- 检索配置或首选项
对每个请求都这样做似乎很自然,但这些信息可能会浪费大量时间。幸运的是,有一种 方法可以解决缓慢的身份验证数据库查询问题 :如果调用者发出请求,您可以在短时间内缓存他们的凭据。
缓存授权可能看起来很可怕,因为更改不是即时的,但在实践中,“即时”很难定义。如果在撤销访问权限之前 API 调用正在进行中,则用户可能会或可能不会根据随机运气发出请求 — 无论 API 调用是否在撤销之前到达。
如果我们更新文档,说“更改用户权限后,请等待 5 分钟,所有服务器才会更新新权限”——那么您就可以规划性能了!这里的技巧是对 API 调用的承载令牌及其 IP 地址进行哈希处理,然后在缓存中查找所有身份验证和授权数据:
- 首先检查服务器内存中的哈希表。实际上,这将花费 10-20 微秒 。
- 如果持有者令牌不在服务器的内存缓存中,请检查 REDIS 或其他等效的键值对服务器。这将需要 1-2 毫秒。
- 如果在任一缓存中都找不到该值,则创建一个承诺来获取必要的数据。如果该承诺已存在,则加入该承诺,这样您就不会同时发出多个请求。
- 如果身份验证数据超过特定年龄,则启动一个新的承诺来再次重新获取数据,以便在旧数据从缓存中过期时数据就可以准备就绪。
错误4:循环查询的对象关系映射器
Entity Framework 等现代技术使得访问数据库变得极其简单。事实上,这非常容易,以至于我们经常可以编写一个方法来执行数据库调用 — 然后发现人们在使用这个方法时并没有意识到它接触了数据库。
一个简单的例子可能是这样的:
公共 异步任务 < int > GetNumberOfUsers ( int id ) {
var count = 0 ;
var items = await _database.GetRecords(id);
foreach ( var item in records) {
count += CountUsersPerItem(item);
}
返回count;
}
这段代码可能看起来微不足道,但如果该方法 CountUsersPerItem
联系数据库,可能是为了获取一个标志或查询一个子表,您可能会发现看似一个查询变成了数百或数千个查询。
更糟糕的是,此功能的性能在开发人员的桌面上可能看起来不错,但当现实世界的客户面临同样的情况时,可能会突然下降。
我发现了一些有助于追踪此问题的技巧:
- 在当前 API 调用堆栈上增加一个计数器,该计数器用于计算每个 API 请求的数据库调用次数。记录此信息,然后追踪执行异常大量查询的 API 调用。
- 使用活动监视器等工具监控数据库性能 ,并留意成千上万个快速查询的突然激增。然后通过将嵌套查询替换为返回所有必要数据的单个查询来优化它们。
- 标准化命名策略,其中接触数据库的每种方法的
Query
名称中都必须包含该单词,例如,CheckStatusQuery()
接触数据库的方法会CheckStatus()
执行相同的操作但没有查询。
错误5:因为速度快而忽略查询
这个问题非常隐蔽。现代数据库技术非常强大,简单的数据库查询通常可以与查询 REDIS 一样快甚至更快。在本地工作的开发人员通常会看到非常好的性能,因为他们的应用程序和数据库服务器之间没有延迟,两者都在笔记本电脑上的容器中运行。
即使您的 SQL Server 或 Postgres 实例可以在一毫秒内做出响应,这些毫秒也会累积起来。如果您的 API 请求发出十个一毫秒的查询,则可能会使您的 API 延迟增加十毫秒 – 当平均预期时间少于一百毫秒时,这是一个不可忽略的量。
这里的关键经验是,在应用程序接口设计中,每个数据库查询都很重要。关注它们,你的应用程序接口就会变得快速而实用。