一次对API响应时间的优化探索

电子说

1.3w人已加入

描述

 

一次对 API 响应时间的优化探索。

在这普通的一天,我们用着普通的 API,突然发现响应速度过慢的警报意外亮起。

结果显示,我们的 API 需要约 70 秒的时间才能对常规流量下的客户端做出响应。开什么玩笑……

从问题入手

先向大家汇报一下我们的这个慢速 API 是做什么,又是怎么做的。

在这款应用程序中,我们把书籍及其作者的目录存储在 MySQL 数据库中。其中共包含约 6800 万本书,每本书对应一家出版社。

下面来看书籍和作者的表结构。

CREATE TABLE `book` (
  `id` int NOT NULL AUTO_INCREMENT,
  `book_uuid_bin` binary(16) NOT NULL,
  `publishing_house_uuid_bin` binary(16) NOT NULL,
  `display_name` varchar(750)  NOT NULL,
  `normalized_name` varchar(750) NOT NULL,
  `description` varchar(1000) DEFAULT NULL,
  `level` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_book_uuid_bin` (`book_uuid_bin`),
  KEY `book_description_idx` (`description`(768)),
  KEY `book_display_name_idx` (`display_name`),
  KEY `book_normalized_name_idx` (`normalized_name`),
  KEY `publishing_house_uuid_bin_idx` (`publishing_house_uuid_bin`),
  KEY `book_uuid_bin_idx` (`book_uuid_bin`)
)
CREATE TABLE `publishing_house` (
  `id` int NOT NULL AUTO_INCREMENT,
  `publishing_house_uuid_bin` binary(16) DEFAULT NULL,
  `display_name` varchar(750) NOT NULL,
  `normalized_name` varchar(750) NOT NULL,
  `alias_uuid_bin` binary(16) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_publishing_house_uuid_bin` (`author_uuid_bin`),
  KEY `publishing_house_normalized_name_idx` (`normalized_name`),
  KEY `publishing_house_display_name_idx` (`display_name`)
)

再说回 API,其用例是在 UI 上提供自动补全功能,方便用户更好地查找特定出版社出版的书籍,同时保证用户的查询字符串同书籍名称或描述的前缀相匹配。

API 中使用的 MySQL 查询如下所示:

select book_uuid_bin,
       display_name,
       normalized_name,
       description,      
       author_uuid_bin 
from book
where
     ((lower(display_name) like lower("%Software E%") or  lower(description) like lower("%Software E%")) and publishing_house_uuid_bin = UUID_TO_BIN("d2230981-e570-5ba4-9a3a-16028c51d54f"))order by display_name asc limit 100;

即使查询在单表上就能完成,不需要连接作者表,这条 SQL 查询也需要 7 秒左右才能执行完成。

我们在 where 子句所使用的列上建立了索引。但这一实现还是存在问题,包括:

1、display_name 和 description 等列属于 VARCHAR 类型。

2、会在 VARCHAR 类型列上使用带有 OR 子句的 LIKE 运算符。

3、会使用 ORDER BY。

4、 WHERE 子句中使用的所有列,都缺少复合索引。

5、表中共包含 5800 万条记录。

我们曾尝试在查询中使用的各列上创建一个复合索引,但最终发现无济于事。因为对于 RDBMS 数据库内的大表来说,在 VARCHAR 列上搜索文本的效率就不可能太高。

我们知道 Elasticsearch 提供全文本搜索功能,所以想在自己的用例中试试看。我们一直在用 AWS 的云服务,因此选择了相应的 AWS OpenSearch 服务。

Amazon OpenSearch 托管服务能帮助用户轻松在 AWS 云中部署、操作和扩展 OpenSearch 集群。Amazon OpenSearch 是 Amazon Elasticsearch 的继任方案。

开始行动

我们通过脚本将表数据从 MySQL 加载到了 AWS OpenSearch 集群当中。整个数据迁移过程大概用了几个小时。

我们为索引保留了 5 个分片和 1 个副本因子。

我们还为用例编写了一条等效的 OpenSearch 查询,具体如下所示:

API — POST /books-catalog/_search

{
  "query": {
    "bool": {
      "must": [
        {
          "match_phrase": {
            "publisherUuid": "1f754fc0-610c-5b29-b22b-fa8140afb7be"
          }
        },
        {
          "bool": {
            "should": [
              {
                "match_phrase": {
                  "displayName": "Software E"
                }
              },
              {
                "match_phrase": {
                  "description": "Software E"
                }
              }
            ]
          }
        }
      ]
    }
  },
  "size": 100,
  "sort": [
    {
      "displayName.keyword": {
        "unmapped_type": "keyword",
        "order": "asc"
      }
    }
  ]
}
结果

我们的 API 响应速度直接缩短至 70 毫秒以内。

API 响应速度提高了 1000 倍!

关于 OpenSearch 全文搜索的一些细节 :

在 ElasticSearch 中对文档进行索引(创建)时,AWS OpenSearch 会对字符串类型的字段使用文本分析器。

文本分析器会将字符串字段拆分为多个 token,为各 token 构建内部索引,然后根据查询中提供的 token 进行匹配。

权衡取舍

为了避免重写整个服务,同时尽快在 MySQL 切换至 AWS OpenSearch 后恢复正常生产,我们决定只在这个特定用例中使用 OpenSearch。

而且速度提升 1000 倍的代价,就是多了一套需要在 OpenSearch 当中维护的数据副本。但由于我们的数据大多是静态的,持续更新量非常有限,所以维护强度和成本都很低。

可以看到,选择正确的数据库引擎往往会给业务用例带来翻天覆地的提升。

希望我们的经历能给大家带来一点启发,祝编程愉快!

  审核编辑:汤梓红
 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分