构建API时最常见的重大错误概述
无论构建哪种API服务器,你都会面临一系列基本问题。大多数这些问题其实是可以避免的,但我仍然看到有着数十年经验的专业工程师年复一年地重复同样的斗争。
让我们一起漫步在数据库性能陷阱的花园里。我们将讨论你可能会犯的错误,如何识别它们,如何解决它们,以及我们是否可以采取一些预防措施。
一个充满美丽事物的花园,然而却可能出现那么多问题(CC,rawpixel)
错误 1:查询不会改变的信息当我开发Avalara AvaTax REST API时,我需要允许用户发送地址。由于他们的数据杂乱无章,有时他们会发送ISO国家代码、国家名称或别名。我可以应对这种情况,因为有很多国家 数据 来源可以在GitHub上以宽松的许可协议获取,但最终我选择了购买官方ISO 3166国家代码表。
接下来的步骤是让我的API服务器在启动时加载这些数据。代码不必过于复杂——这里有一些类似于C#的伪代码,展示了如何让其运行起来:
private static Task<List<Country>>? _cachedQuery = null;
private Task<List<Country>> GetCachedCountries()
{
// 将查询结果保存到静态变量中
if (_cachedQuery == null) {
_cachedQuery = Database.Countries.ToListAsync();
}
// 所有调用者都将使用相同的查询结果
return _cachedQuery;
}
为什么要这样做呢?新国家的创建并不常见,幸好如此。如果需要更新国家列表,我们会在每月的应用程序部署期间通过执行一个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调用的bearer token加上他们的IP地址进行哈希处理,并在缓存中查找所有的认证和授权数据:
- 首先检查服务器内存中的哈希表。实际上,这将需要10-20 _微秒_。
- 如果承载令牌(Bearer Token)不在服务器的内存缓存中,则检查 REDIS 或其他等效的键值对服务器。这将需要1-2毫秒。
- 如果值在两个缓存中均未找到,则创建一个获取所需数据的承诺。如果承诺已经存在,则加入该承诺,以免同时有多个请求。
- 如果认证数据比特定时间更旧,则启动一个新的承诺来重新获取数据,以便在旧数据过期时准备好。
详细了解请参阅我在博客中关于认证缓存策略的文章——其中包含许多难以处理的额外细节。
错误 4:在循环中查询的对象关系映射器现代技术,如 Entity Framework(实体框架),使得访问数据库变得极其容易。事实上,简单到可以编写一个执行数据库调用的方法的程度——然后发现人们在没有意识到它访问了数据库的情况下使用了这个方法。
一个简单的例子可能是这样的:
public async Task<int> 获取用户数量(int id) {
var count = 0;
var records = await _database.获取记录(id);
foreach (var item in records) {
count += 计算项目中的用户数(item);
}
return count;
}
这段代码可能看起来很简单,但如果 CountUsersPerItem
方法访问数据库,例如,查询标志或子表,你可能会发现,看似一次查询实际上变成了数百上千次的查询。
更糟糕的是,此功能在开发者的桌面上可能表现良好,但在真实世界中客户遇到相同情况时,其性能可能会突然下降。
我发现了一些有用的技巧来追踪这个问题。
- 在当前 API 调用栈中增加一个计数器,用于统计每个 API 请求中的数据库调用次数。记录这些信息,然后追踪执行异常多的查询的 API 调用。
- 使用类似 Activity Monitor 的工具监控数据库性能,注意突然出现的成千上万次极快的查询。然后通过用一个单一查询替换嵌套查询来优化它们,这个单一查询可以返回所需的所有数据。
- 采用一种命名策略,即每个访问数据库的方法名称中必须包含
Query
,例如将访问数据库的方法命名为CheckStatusQuery()
,而CheckStatus()
则执行相同的操作但不涉及查询。
这个问题极具隐蔽性。现代数据库技术极其强大,以至于一个简单的数据库查询往往可以与对REDIS的查询一样快,甚至更快。那些在本地工作的开发人员经常会看到非常好的性能,因为他们应用程序与数据库服务器之间没有延迟,这两者都在他们的笔记本电脑的容器中运行。
即使你的 SQL Server 或 Postgres 实例能够在一毫秒内响应,这些毫秒的累积是不可忽视的。如果你的 API 请求发出了十次一毫秒的查询,这会使你的 API 延迟增加十毫秒——当通常期望的响应时间低于一百毫秒时,这是一个不可忽视的延迟。
在这里的关键教训是在设计API时,每一个数据库查询都很重要。请注意,你的API将会高效且功能强大。
泰德·斯潘斯在ProjectManager.com担任工程负责人,并在贝尔维尤学院授课。如果你对软件工程和商业分析感兴趣,我非常愿意与你交流。你可以在Mastodon,Threads,或LinkedIn上与我交流。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章