对于数据库来说,慢查问往往意味着危险。SQL 执行得越慢,耗费的 CPU 资源或 IO 资源也会越大。大量的慢查问可间接引发业务故障,关注慢查问即是关注故障自身。本文次要介绍了美团如何利用数据库的代价优化器来优化慢查问,并给出索引倡议,评估跟踪倡议品质,经营治理慢查问。
1 背景
慢查问是指数据库中查问工夫超过指定阈值(美团设置为 100ms)的 SQL,它是数据库的性能杀手,也是业务优化数据库拜访的重要抓手。随着美团业务的高速增长,日均慢查问量曾经过亿条,此前因慢查问导致的故障约占数据库故障总数的 10% 以上,而且高级别的故障呈日益增长趋势。因而,对慢查问的优化曾经变得迫不及待。
那么如何优化慢查问呢?最间接无效的办法就是选用一个查问效率高的索引。对于高效率的索引举荐,次要在日常工作中,基于教训规定的举荐随处可见,对于简略的 SQL,如select * from sync_test1 where name like 'Bobby%'
,间接增加索引 IX(name) 就能够获得不错的成果;但对于略微简单点的 SQL,如select * from sync_test1 where name like 'Bobby%' and dt > '2021-07-06'
,到底抉择 IX(name)、IX(dt)、IX(dt,name) 还是 IX(name,dt),该办法也无奈给出精确的答复。更别说像多表 Join、子查问这样简单的场景了。所以采纳基于代价的举荐来解决该问题会更加普适,因为基于代价的办法应用了和数据库优化器雷同的形式,去量化评估所有的可能性,选出的是执行 SQL 消耗代价最小的索引。
2 基于代价的优化器介绍
2.1 SQL 执行与优化器
一条 SQL 在 MySQL 服务器中执行流程次要蕴含:SQL 解析、基于语法树的筹备工作、优化器的逻辑变动、优化器的代价筹备工作、基于代价模型的优化、进行额定的优化和运行执行打算等局部。具体如下图所示:
2.2 代价模型介绍
而对于优化器来说,执行一条 SQL 有各种各样的计划可供选择,如表是否用索引、抉择哪个索引、是否应用范畴扫描、多表 Join 的连贯程序和子查问的执行形式等。如何从这些可选计划中选出耗时最短的计划呢?这就须要定义一个量化数值指标,这个指标就是代价(Cost),咱们别离计算出可选计划的操作耗时,从中选出最小值。
代价模型将操作分为 Server 层和 Engine(存储引擎)层两类,Server 层次要是 CPU 代价,Engine 层次要是 IO 代价,比方 MySQL 从磁盘读取一个数据页的代价 io_block_read_cost 为 1,计算符合条件的行代价为 row_evaluate_cost 为 0.2。除此之外还有:
- memory_temptable_create_cost (default 2.0) 内存长期表的创立代价。
- memory_temptable_row_cost (default 0.2) 内存长期表的行代价。
- key_compare_cost (default 0.1) 键比拟的代价,例如排序。
- disk_temptable_create_cost (default 40.0) 外部 myisam 或 innodb 长期表的创立代价。
- disk_temptable_row_cost (default 1.0) 外部 myisam 或 innodb 长期表的行代价。
在 MySQL 5.7 中,这些操作代价的默认值都能够进行配置。为了计算出计划的总代价,还须要参考一些统计数据,如表数据量大小、元数据和索引信息等。MySQL 的代价优化器模型整体如下图所示:
2.3 基于代价的索引抉择
还是持续拿上述的 SQL select * from sync_test1 where name like 'Bobby%' and dt > '2021-07-06'
为例,咱们看看 MySQL 优化器是如何依据代价模型抉择索引的。首先,咱们间接在建表时退出四个候选索引。
Create Table: CREATE TABLE `sync_test1` (`id` int(11) NOT NULL AUTO_INCREMENT,
`cid` int(11) NOT NULL,
`phone` int(11) NOT NULL,
`name` varchar(10) NOT NULL,
`address` varchar(255) DEFAULT NULL,
`dt` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `IX_name` (`name`),
KEY `IX_dt` (`dt`),
KEY `IX_dt_name` (`dt`,`name`),
KEY `IX_name_dt` (`name`,`dt`)
) ENGINE=InnoDB
通过执行 explain 看出 MySQL 最终抉择了 IX_name 索引。
mysql> explain select * from sync_test1 where name like 'Bobby%' and dt > '2021-07-06';
+----+-------------+------------+------------+-------+-------------------------------------+---------+---------+------+------+----------+------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+-------------------------------------+---------+---------+------+------+----------+------------------------------------+
| 1 | SIMPLE | sync_test1 | NULL | range | IX_name,IX_dt,IX_dt_name,IX_name_dt | IX_name | 12 | NULL | 572 | 36.83 | Using index condition; Using where |
+----+-------------+------------+------------+-------+-------------------------------------+---------+---------+------+------+----------+------------------------------------+
而后再关上 MySQL 追踪优化器 Trace 性能。能够看出,没有抉择其余三个索引的起因均是因为在其余三个索引上应用 range scan 的代价均 >= IX_name。
mysql> select * from INFORMATION_SCHEMA.OPTIMIZER_TRACE\G;
*************************** 1. row ***************************
TRACE: {
...
"rows_estimation": [
{
"table": "`sync_test1`",
"range_analysis": {
"table_scan": {
"rows": 105084,
"cost": 21628
},
...
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "IX_name",
"ranges": ["Bobby\u0000\u0000\u0000\u0000\u0000 <= name <= Bobbyÿÿÿÿÿ"],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 572,
"cost": 687.41,
"chosen": true
},
{
"index": "IX_dt",
"ranges": ["0x99aa0c0000 < dt"],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 38698,
"cost": 46439,
"chosen": false,
"cause": "cost"
},
{
"index": "IX_dt_name",
"ranges": ["0x99aa0c0000 < dt"],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 38292,
"cost": 45951,
"chosen": false,
"cause": "cost"
},
{
"index": "IX_name_dt",
"ranges": ["Bobby\u0000\u0000\u0000\u0000\u0000 <= name <= Bobbyÿÿÿÿÿ"],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 572,
"cost": 687.41,
"chosen": false,
"cause": "cost"
}
],
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
}
},
"chosen_range_access_summary": {
"range_access_plan": {
"type": "range_scan",
"index": "IX_name",
"rows": 572,
"ranges": ["Bobby\u0000\u0000\u0000\u0000\u0000 <= name <= Bobbyÿÿÿÿÿ"]
},
"rows_for_plan": 572,
"cost_for_plan": 687.41,
"chosen": true
}
...
}
上面咱们依据代价模型来推演一下代价的计算过程:
- 走全表扫描的代价:io_cost + cpu_cost =(数据页个数 io_block_read_cost)+ (数据行数 row_evaluate_cost + 1.1) =(data_length / block_size + 1)+ (rows 0.2 + 1.1) = (9977856 / 16384 + 1) + (105084 0.2 + 1.1) = 21627.9。
- 走二级索引 IX_name 的代价:io_cost + cpu_cost = (预估范畴行数 io_block_read_cost + 1) + (数据行数 row_evaluate_cost + 0.01) = (572 1 + 1) + (5720.2 + 0.01) = 687.41。
- 走二级索引 IX_dt 的代价:io_cost + cpu_cost = (预估范畴行数 io_block_read_cost + 1) + (数据行数 row_evaluate_cost + 0.01) = (38698 1 + 1) + (386980.2 + 0.01) = 46438.61。
- 走二级索引 IX_dt_name 的代价: io_cost + cpu_cost = (预估范畴行数 io_block_read_cost + 1) + (数据行数 row_evaluate_cost + 0.01) = (38292 1 + 1) + (38292 0.2 + 0.01) = 45951.41。
- 走二级索引 IX_name_dt 的代价:io_cost + cpu_cost = (预估范畴行数 io_block_read_cost + 1) + (数据行数 row_evaluate_cost + 0.01) = (572 1 + 1) + (5720.2 + 0.01) = 687.41。
补充阐明
- 计算结果在小数上有偏差,因为 MySQL 应用 %g 打印浮点数,小数会以最短的形式输入。
- 除“+1.1 +1”这种调节值外,Cost 计算还会呈现 +0.01, 它是为了防止 index scan 和 range scan 呈现 Cost 的竞争。
- Cost 计算是基于 MySQL 的默认参数配置,如果 Cost Model 参数扭转,optimizer_switch 的选项不同,数据分布不同都会导致最终 Cost 的计算结果不同。
- data_length 可查问 information_schema.tables,block_size 默认 16K。
2.4 基于代价的索引举荐思路
如果想借助 MySQL 优化器给慢查问计算出最佳索引,那么须要实在地在业务表上增加所有候选索引。对于线上业务来说,间接增加索引的工夫空间老本太高,是不可承受的。MySQL 优化器选最佳索引用到的数据是索引元数据和统计数据,所以咱们想是否能够通过给它提供候选索引的这些数据,而非实在增加索引的这种形式来实现。
通过深刻调研 MySQL 的代码构造和优化器流程,咱们发现是可行的:一部分存在于 Server 层的 frm 文件中,比方索引定义;另一部分存在于 Engine 层中,或者通过调用 Engine 层的接口函数来获取,比方索引中某个列的不同值个数、索引占据的页面大小等。索引相干的信息,如下图所示:
因为 MySQL 自身就反对自定义存储引擎,所以索引举荐思路是构建一个反对虚伪索引的存储引擎,在它下面建设蕴含候选索引的空表,再采集样本数据,计算出统计数据提供给优化器,让优化器选出最优索引,整个调用关系如下图所示:
3 索引举荐实现
因为存储引擎自身并不具备对外提供服务的能力,间接在 MySQL Server 层批改也难以保护,所以咱们将整个索引举荐零碎拆分成反对虚伪索引的 Fakeindex 存储引擎和对外提供服务的 Go-Server 两局部,整体架构图如下:
首先简要介绍一下 Fakeindex 存储引擎,这是一个轻量级的存储引擎,负责将索引的相干接口透传到 Go-Server 局部。因为它必须采纳 C ++ 实现,与 Go-Server 间存在跨语言调用的问题,咱们应用了 Go 原生的轻量级 RPC 技术 +cgo 来防止引入重量级的 RPC 框架,也不用引入第三方依赖包。函数调用链路如下所示,MySQL 优化器调用 Fakeindex 的 C ++ 函数,参数转换成 C 语言,而后通过 cgo 调用到 Go 语言的办法,再通过 Go 自带的 RPC 客户端向服务端发动调用。
上面将重点论述外围逻辑 Go-Server 局部,次要流程步骤如下。
3.1 前置校验
首先依据教训规定,排除一些不反对通过增加索引来进步查问效率的场景,如查零碎库的 SQL,非 select、update、delete SQL 等。
3.2 提取要害列名
这一步提取 SQL 可用来增加索引的候选列名,除了抉择给出当初 where 中的列增加索引,MySQL 对排序、聚合、表连贯、聚合函数(如 max)也反对应用索引来进步查问效率。咱们对 SQL 进行语法树解析,在树节点的 where、join、order by、group by、聚合函数中提取列名,作为索引的候选列。值得注意的是,对于某些 SQL,还需联合表构造能力精确地提取,比方:
select * from tb1, tb2 where a = 1
,列 a 归属 tb1 还是 tb2 取决于谁惟一蕴含列 a。select * from tb1 natural join tb2 where tb1.a = 1
,在天然连贯中,tb1 和 tb2 默认应用了雷同列名进行连贯,但 SQL 中并没有暴露出这些可用于增加索引的列。
3.3 生成候选索引
将提取出的要害列名进行全排列即蕴含所有的索引组合,如列 A、B、C 的所有索引组合是[‘A’, ‘B’, ‘C’, ‘AB’, ‘AC’, ‘BA’, ‘BC’, ‘CA’, ‘CB’, ‘ABC’, ‘ACB’, ‘BAC’, ‘BCA’, ‘CAB’, ‘CBA’],但还需排除一些索引能力失去所有的候选索引,比方:
- 曾经存在的索引,如存在 AB,需排除 AB、A,因为 MySQL 反对应用前缀索引。
- 超过最大索引长度 3072 字节限度的索引。
- 一些临时不反对的索引,如带天文数据类型列的空间索引。
3.4 数据采集
间接从业务数据库采集,数据分成元数据、统计数据、样本数据三局部:
- 元数据:即表的定义数据,包含列定义、索引定义,可通过 show create table 获取。
- 统计数据:如表的行数、表数据大小、索引大小,能够通过查问 infromation_schema.tables 获取;已存在索引的 cardinality(要害值:即索引列的不同值个数,值越大,索引优化成果越显著),能够通过查问 mysql.innodb_index_stats 表获取。
- 样本数据:候选索引为假索引,采集的统计数据并不蕴含假索引的数据,这里咱们通过采集原表的样本数据来计算出假索引的统计数据。
上面介绍样本数据的采样算法,好的采样算法应该尽最大可能采集到合乎原表数据分布的样本。比方基于平均随机采样的形式select * from table where rand() < rate
,然而它会给线上数据库造成大量 I / O 的问题,重大时可引发数据库故障。所以咱们采纳了基于块的采样形式:它参考了 MySQL 8.0 的直方图采样算法,如对于一张 100 万的表,采集 10 万行数,依据主键的最小值最大值将表数据均分成 100 个区间,每个区间取一块 1000 行数据,采集数据的 SQL,最初将采集到的数据塞入采样表中。代码如下:
select A,B,C,id from table where id >= 1000 and id <= 10000 limit 1000;
select A,B,C,id from table where id >= 10000 and id <= 20000 limit 1000;
...
3.5 统计数据计算
上面举例说明两个外围统计数据的计算形式。首先是 records_in_range,优化器在解决范畴查问时,如果能够用索引,就会调用该函数估算走该索引可过滤出的行数,以此决定最终选用的索引。
比方,对于 SQLselect * from table1 where A > 100 and B < 1000
,候选索引 A、B 来说,优化器会调用此函数在索引页 A 上估算 A > 100 有多少行数,在索引页 B 上预计 B <1000 的行数,例如满足条件的 A 有 200 行,B 有 50 行,那么优化器会优先选择应用索引 B。对于假索引来说,咱们依照该公式:样本满足条件的范畴行数 * (原表行数 / 样本表行数),间接样本数据中查找,而后依照采样比例放大即可估算出原表中满足条件的范畴行数。
其次是用于计算索引区分度的 cardinality。如果间接套用上述公式:样本列上不同值个数 (原表行数 / 样本表行数),如上述的候选索引 A,依据样本统计出共有 100 个不同值,那么在原表中,该列有多少不同值?个别认为是 10,000 =100 (1,000,000/100,000)。但这样计算不实用某些场景,比方状态码字段,可能最多 100 个不同值。针对该问题,咱们引入斜率和两趟计算来躲避,流程如下:
- 第一趟计算:取样本数据一半来统计 A 的不同值个数 R1,区间[min_id, min_id+(max_id – min_id) / 2]。
- 第二趟计算 :取所有样本据统计 A 的不同值个数 R2,区间[min_id, max_id]
计算斜率:R2/R1。 - 判断斜率:如果斜率小于 1.1,为固定值 100,否则依据采样比例放大,为 10,000。
3.6 候选索引代价评估
这一步让优化器帮忙咱们从候选索引中选出最佳索引,次要步骤如下:
- 建蕴含候选索引的表:将候选索引塞入原表定义,并把存储引擎改为 Fakeindex,在举荐引擎的 mysqld 上创立表。
- 通过在举荐引擎 mysqld 上 explain format=json SQL,获取优化器抉择的索引。
值得注意的是,MySQL 表最多建 64 个索引(二级索引),计算所有候选索引的可能时,应用的是增幅比指数还恐怖的全排列算法。如下图所示,随着列数的减少,候选索引数量急剧回升,在 5 个候选列时的索引组合数量就超过了 MySQL 最大值,显然不能满足一些简单 SQL 的需要。统计美团线上索引列数散布后,咱们发现,95% 以上的索引列数都 <= 3 个。同时基于教训思考,3 列索引也可满足绝大部分场景,残余场景会通过其余形式,如库表拆分来进步查问性能,而不是减少索引列个数。
但即使最多举荐 3 列索引,在 5 个候选列时其排列数量 85=$A_{5}^{1}+A_{5}^{2}+A_{5}^{3}$ 也远超 64。这里咱们采纳归并思路。如下图所示,将所有候选索引拆分到多个表中,采纳两次计算,先让 MySQL 优化器选出批次一的最佳索引,可采纳并行计算保障时效性,再 MySQL 选出批次一所有最佳索引的最佳索引,该计划能够最多反对 4096 个候选索引,联合最大索引 3 列限度,能够反对计算出 17 个候选列的最佳索引。
4 举荐质量保证
为了失去索引举荐品质大抵的整体数据,咱们应用美团数据库最近一周的线下慢查问数据,共 246G、约 3 万个 SQL 模板用例做了一个初步测试。
从后果能够看出,零碎根本能笼罩到大部分的慢查问。但还是会呈现有效的举荐,大抵起因如下:
- 索引举荐计算出的 Cost 重大依赖样本数据的品质,在当表数据分布不均或数据歪斜时会导致统计数据呈现误差,导致举荐出谬误索引。
- 索引举荐零碎自身存在缺点,从而导致举荐出谬误索引。
- MySQL 优化器本身存在的缺点,导致举荐出谬误索引。
因而,咱们在业务增加索引前后减少了索引的有效性验证和成果追踪两个步骤,整个流程如下所示:
4.1 有效性验证
因为目前还不具备大规模数据库备份疾速还原的能力,所以无奈应用残缺的备份数据做验证。咱们近似地认为,如果举荐索引在业务库上获得较好的成果,那么在样本库也会获得不错成果。通过真正地在样本库上实在执行 SQL,并增加索引来验证其有效性,验证后果展现如下:
4.2 成果追踪
思考到应用采样数据验证的局限性,所以当在生产环境索引增加结束之后,会立刻对增加的索引进行成果追踪。一方面通过 explain 验证索引是否被真正用到,以及 Cost 是否减小;另一方面用 Flink 实时跟踪该数据库的全量 SQL 拜访数据,通过比照索引增加前后,该 SQL 的实在执行工夫来判断索引是否无效。如果发现有性能方面的回退,则立刻收回告警,周知到 DBA 和研发人员。生成的报告如下:
4.3 仿真环境
当举荐链路呈现问题时,间接在线上排查验证问题的话,很容易给业务带来安全隐患,同时也升高了零碎的稳定性。对此咱们搭建了离线仿真环境,利用数据库备份构建了和生产环境一样的数据源,并残缺复刻了线上举荐链路的各个步骤,在仿真环境回放异样案例,复现问题、排查根因,重复验证改良计划后再上线到生产零碎,进而一直优化现有零碎,晋升举荐品质。
4.4 测试案例库
在上线过程中,往往会呈现改良计划修复了一个 Bug,带来了更多 Bug 的状况。是否做好索引举荐能力的回归测试,间接决定了举荐品质的稳定性。于是,咱们参考了阿里云的技术计划,打算构建一个尽可能齐备的测试案例库用于掂量索引举荐服务能力强弱。但思考影响 MySQL 索引抉择的因素泛滥,各因素间的组合,SQL 的复杂性,如果人为去设计测试用例是是不切实际的,咱们通过下列办法自动化收集测试用例:
- 利用美团线上的丰盛数据,以影响 MySQL 索引抉择的因素特色为抓手,间接从全量 SQL 和慢 SQL 中抽取最实在的案例,不断更新现有测试案例库。
- 在生产的举荐零碎链路上埋点,主动收集异样案例,回流到现有的测试案例库。
- 对于现有数据没有笼罩到的极其场景,采纳人为结构的计划,补充测试用例。
5 慢查问治理经营
咱们次要从工夫维度的三个方向将慢查问接入索引举荐,推广治理:
5.1 过来 - 历史慢查问
这类慢查问属于过来产生的,并且始终存在,数量较多,治理推动力有余,可通过收集历史慢查问日志发现,分成两类接入:
- 外围数据库:该类慢查问通常会被周期性地关注,如慢查问周报、月报,可间接将优化倡议提前生成进去,接入它们,一并经营治理。
- 一般数据库:可将优化倡议间接接入数据库平台的慢查问模块,让研发自助地抉择治理哪些慢查问。
5.2 当初 - 新增慢查问
这类慢查问属于以后产生的,数量较少,属于治理的重点,也可通过实时收集慢查问日志发现,分成两类接入:
- 影响水平个别的慢查问:可通过实时剖析慢查问日志,比照历史慢查问,辨认出新增慢查问,并生成优化倡议,为用户创立数据库危险项,跟进治理。
- 影响水平较大的慢查问:该类通常会引发数据库告警,如慢查问导致数据库 Load 过高,可通过故障诊断根因零碎,辨认出具体的慢查问 SQL,并生成优化倡议,及时推送到故障解决群,升高故障解决时长。
5.3 将来 - 潜在慢查问
这类查问属于以后还没被定义成慢查问,随着工夫推动可能变成演变成慢查问,对于一些外围业务来说,往往会引发故障,属于他们治理的重点,分成两类接入:
- 未上线的准慢查问:我的项目筹备上线而引入的新的准慢查问,可接入公布前的集成测试流水线,Java 我的项目可通过 agentmain 的代理形式拦挡被测试用例笼罩到的 SQL,再通过教训 +explain 辨认出慢查问,并生成优化倡议,给用户在需要管理系统上创立缺点工作,解决后能力公布上线。
- 已上线的准慢查问:该类属于以后执行工夫较快的 SQL,随着表数据量的减少,调演变成慢查问,最常见的就是全表扫描,这类可通过减少慢查问配置参数 log_queries_not_using_indexes 记录到慢日志,并生成优化倡议,为用户创立数据库危险项,跟进治理。
6 我的项目运行状况
以后,次要以新增慢查问为突破点,重点为全表扫描举荐优化倡议。目前咱们曾经灰度接入了一小部分业务,共剖析了六千多条慢查问,举荐了一千多条高效索引倡议。另外,美团外部的研发同学也可通过数据库平台自助发动 SQL 优化倡议工单,如下图所示:
另外在美团外部,咱们曾经和数据库告警买通,实现了故障发现、根因剖析、解决方案的自动化解决,极大地提高了故障解决效率。上面是一个展现案例,当数据库集群产生告警,咱们会拉一个故障群,先通过根因定位系统,如果辨认出慢查问造成的,会马上调用 SQL 优化倡议零碎,举荐出索引,整个解决流程是分钟级别,都会在群外面推送最新消息。如下图所示:
7 将来布局
思考到美团日均产生近亿级别的慢查问数据,为了实现对它们的诊断剖析,咱们还须要进步零碎大规模的数据并发解决的能力。另外,以后该零碎还是针对单 SQL 的优化,没有思考保护新索引带来的代价,如占用额定的磁盘空间,使写操作变慢,也没有思考到 MySQL 选错索引引发其余 SQL 的性能回退。对于业务或者 DBA 来说,咱们更多关怀的是整个数据库或者集群层面的优化。
业界如阿里云的 DAS 则是站在全局的角度考量,综合思考各个因素,输入须要创立的新索引、须要改写的索引、须要删除的索引,实现数据库性能最大化晋升,同时最大化升高磁盘空间耗费。将来咱们也将一直优化和改良,实现相似基于 Workload 的全局优化。
参考资料
- MySQL Writing a Custom Storage Engine
- MySQL Optimizer Guide
- MySQL 直方图
- Golang cgo
- 阿里云 -DAS 之基于 Workload 的全局主动优化实际
- SQL 诊断优化,当前就都交给数据库自治服务 DAS 吧
- MySQL 索引原理及慢查问优化
本文作者
粟含,美团根底研发平台 / 根底技术部 / 数据库平台研发组工程师。
浏览美团技术团队更多技术文章合集
前端 | 算法 | 后端 | 数据 | 平安 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2021 年货】、【2020 年货】、【2019 年货】、【2018 年货】、【2017 年货】等关键词,可查看美团技术团队历年技术文章合集。
| 本文系美团技术团队出品,著作权归属美团。欢送出于分享和交换等非商业目标转载或应用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者应用。任何商用行为,请发送邮件至 tech@meituan.com 申请受权。