Serverless 数据库作为近几年云原生数据库领域的重要发展方向,自 2018 年 AWS 率先推出 Aurora Serverless MySQL 服务,打响 Serverless 数据库之战的第一枪以来,各大云平台厂商一直在该领域不断深耕探索。9 月 7 日,在 2023 腾讯全球数字生态大会云原生数据库技术演进与实践专场上,腾讯云数据库团队重磅发布了云原生数据库 TDSQL- C Serverless 2.0 版本。在这场分享中,腾讯云数据库产品经理陈昊老师介绍了腾讯云 TDSQL-C Serverless 独有的弹性伸缩方案,本文就以此为引,深度探索一下 TDSQL-C Serverless 的纵向弹性伸缩策略及稳定性。
TDSQL-C Serverless 服务是腾讯云自研的新一代云原生关系型数据库 TDSQL-C MySQL 版的无服务器架构版,是全 Serverless 架构的云原生数据库。架构图如下:
TDSQL-C Serverless 的三大核心特性:
关于 Serverless 数据库的纵向弹性方案,业内通用的方案如上图左侧所示,低负载时分配较低规格的计算资源,当负载压力触发阈值后,再扩容更多的计算资源。这种方案的弊端是,对计算资源的调整速度有很高的要求,计算资源调整速度不及时且数据库负载压力极大的情况下可能会触发实例 OOM,如果多个实例同时面临负载高峰时,还可能会发生资源抢占的问题。这可能也是 Serverless 数据库在早期只能用于开发环境或测试环境的原因之一。
TDSQL-C Serverless 的弹性伸缩方案与这种“抠抠搜搜”的释放计算资源的方案不同,TDSQL-C Serverless 会根据用户配置的最大 CCU(1CCU ≈ 1C2G)在一开始就将 CPU、内存资源限制到最大规格,极大程度降低因 CPU 和内存扩容带来的时间影响和使用限制,之后通过监控计算层的负载情况,当集群触发到自动弹性的负载阈值后,Buffer Pool 会根据监控进行秒级扩容,准秒级缩容。在这个方案下用户使用数据库可以无感知进行计算资源扩容,并且不会因为连接突增导致实例 OOM 和资源抢占的问题。
相比于计算资源的动态调整,调整 Buffer Pool 的大小更为轻量便捷,调整速度也会更快。总结来说,前者的方案更像是传统人工扩缩容的云端自动化实现,后者则是从业务角度出发,去做了更多的思考和优化来提供更好的使用体验。
因为 TDSQL-C Serverless 控制台和数据库智能管家 DBbrain 给出的监控信息最小粒度只有 5 秒,无法做到秒级的指标监控,因此实测方案整体参考周振兴老师(《高性能 MySQL》第三、四版的译者)针对 Aurora Serverless v2 的测试方案,并结合 TDSQL-C Serverless 的特性进行了部分调整。
oltp_read_write
,将 --report-interval
设置成 1s,将 --percentile
设置为 99 作为平均延迟(响应时间 rt)SHOW VARIABLES LIKE "innodb_buffer_pool_size"
命令持续观测 Buffer Pool Size,以该数值的大小变化作为资源调整变化的指标数据库管理
功能创建测试库 test_scaling
# -*- coding: utf-8 -*-
import subprocess
import re
import time
import csv
import threading
import mysql.connector
from mysql.connector import pooling
# 配置数据库连接参数
db_config = {
"host": "172.21.0.15",
"port": 3306,
"user": "root",
"password": "xxxxxx",
"database": "test_scaling",
}
# 创建数据库连接池
pool = pooling.MySQLConnectionPool(
pool_name="my_pool",
pool_size=5,
**db_config
)
# 函数:连接数据库并查询innodb_buffer_pool_size
def query_innodb_buffer_pool_size():
with pool.get_connection() as connection:
cursor = connection.cursor()
cursor.execute('SHOW VARIABLES LIKE "innodb_buffer_pool_size"')
result = cursor.fetchone()
return int(result[1])
# 函数:运行sysbench命令并解析输出
def run_sysbench(command_type, command):
print('command_type: ' + command_type + ', command: ' + command)
result_list = [];
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT, text=True)
for line in iter(process.stdout.readline, ''):
print(line, end='')
# 连接数据库查询innodb_buffer_pool_size
innodb_buffer_pool_size = query_innodb_buffer_pool_size()
# 获取当前时间并格式化为时分秒
time_now = time.strftime("%H:%M:%S", time.localtime())
# 输出示例:
# [ 1s ] thds: 16 tps: 850.05 qps: 17191.55 (r/w/o: 12053.34/3422.15/1716.06) lat (ms,99%): 27.17 err/s: 0.00 reconn/s: 0.00
# 解析输出结果,获取lat (ms,99%):
if line.startswith('[ '):
times = re.search(r'\[ (\d+)s \]', line).group(1)
latency = re.search('lat \(ms,99%\): (\d+\.\d+)', line).group(1)
result_list.append([time_now, times, command_type, innodb_buffer_pool_size, latency])
process.wait()
return result_list
# 函数:运行Sysbench测试
def run_sysbench_thread(command_type, command, result_list):
result_list.extend(run_sysbench(command_type, command))
if __name__ == '__main__':
# main:1 sub:24
sysbench_command_main = 'sysbench --db-driver=mysql --mysql-host=' + db_config['host'] + ' --mysql-port=' + str(db_config['port']) + ' --mysql-user=' + db_config['user'] + ' --mysql-password=' + db_config['password'] + ' --mysql-db=' + db_config['database'] + ' --table-size=100000 --tables=1 --threads=1 --time=1200 --percentile=99 --report-interval=1 oltp_read_write run'
sysbench_command_sub = 'sysbench --db-driver=mysql --mysql-host=' + db_config['host'] + ' --mysql-port=' + str(db_config['port']) + ' --mysql-user=' + db_config['user'] + ' --mysql-password=' + db_config['password'] + ' --mysql-db=' + db_config['database'] + ' --table-size=100000 --tables=1 --threads=24 --time=300 --percentile=99 --report-interval=1 oltp_read_write run'
result_list_main = []
result_list_sub = []
# 创建两个线程分别运行主测试和子测试
main_thread = threading.Thread(target=run_sysbench_thread, args=('main', sysbench_command_main, result_list_main))
sub_thread = threading.Thread(target=run_sysbench_thread, args=('sub', sysbench_command_sub, result_list_sub))
# 启动主线程
main_thread.start()
# 创建定时器,等待300秒后启动子线程
sub_thread_timer = threading.Timer(300, sub_thread.start)
sub_thread_timer.start()
# 等待线程完成
main_thread.join()
sub_thread.join()
# 合并结果
result_list_main.extend(result_list_sub)
print(result_list_main)
# 指定要写入的CSV文件的文件名
csv_file_name = 'test_scaling.csv'
# 打开CSV文件并将数据写入
with open(csv_file_name, mode='w', newline='') as file:
writer = csv.writer(file)
writer.writerow(['time_now', 'times', 'command_type','innodb_buffer_pool_size', 'rt'])
for data_row in result_list_main:
writer.writerow(data_row)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ECharts Scatter Plot from CSV</title>
<!-- 引入 ECharts 文件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="scatter-plot" style="width: 100vw; height: 80vh;"></div>
<script>
// 初始化ECharts实例
var myChart = echarts.init(document.getElementById('scatter-plot'), { pixelRatio: 2 });
// 异步加载CSV文件
fetch('test_scaling.csv')
.then(function (response) {
return response.text();
})
.then(function (csvData) {
// 解析CSV数据
var lines = csvData.split('\n');
var data = [];
for (var i = 1; i < lines.length; i++) {
var values = lines[i].split(',');
data.push({
time_now: values[0],
times: values[1],
command_type: values[2],
innodb_buffer_pool_size: parseFloat(values[3]),
rt: parseFloat(values[4])
});
}
// 创建ECharts选项,其中左侧纵坐标显示rt(毫秒),右侧纵坐标显示Buffer Pool Size(MB)。
// 其中,红点代表 "主进程" 响应时间(rt),灰点代表 "压力进程" 响应时间(rt),蓝色点代表Buffer Pool Size。
// 时间以秒为单位显示在横坐标上,时间间隔为1秒。
var option = {
backgroundColor: '#FFFFFF',
grid: {
left: 50,
right: 50,
bottom: 60,
top: 30,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: ['主进程响应时间(RT)', '压力进程响应时间(RT)', 'Buffer Pool Size']
},
// dataZoom: [{
// type: 'slider',
// start: 20,
// end: 50,
// }, {
// type: 'inside',
// start: 20,
// end: 50,
// }],
toolbox: {
show: true,
feature: {
saveAsImage: {
show: true,
pixelRatio: 2,
name: "TDSQL-C_Test_Scaling"
}
}
},
xAxis: {
type: 'value',
name: 'Time',
nameLocation: 'middle',
nameGap: 25,
interval: 30,
minInterval: 1,
splitLine: {
show: false
},
axisLabel: {
formatter: function (value) {
value = parseInt(value.toFixed(0));
return value;
}
}
},
yAxis: [{
type: 'value',
name: 'RT(ms)',
nameLocation: 'middle',
nameGap: 30,
splitLine: {
show: false
}
}, {
type: 'value',
name: 'Buffer Pool Size (MB)',
nameLocation: 'middle',
nameGap: 50,
splitLine: {
show: false
}
}],
series: [{
name: '主进程响应时间(RT)',
type: 'scatter',
symbolSize: 4,
data: data.filter(function (item) {
return item.command_type === 'main';
}).map(function (item) {
return [item.times, item.rt];
}),
itemStyle: {
color: 'red'
},
markLine: {
silent: true,
symbol: "none",
label: {
show: true,
position: 'insideMiddle',
formatter: '{b}'
},
data: [{
name: '压力进程开始',
xAxis: 301
}, {
name: '压力进程结束',
xAxis: 600
}]
}
}, {
name: '压力进程响应时间(RT)',
type: 'scatter',
symbolSize: 4,
data: data.filter(function (item) {
return item.command_type === 'sub';
}).map(function (item) {
// time加上秒数
return [Number(item.times) + 300, item.rt];
}),
itemStyle: {
color: 'gray'
}
}, {
name: 'Buffer Pool Size',
type: 'scatter',
symbolSize: 2,
yAxisIndex: 1,
data: data.map(function (item) {
return [item.command_type === 'sub' ? Number(item.times) + 300 : item.times,
item.innodb_buffer_pool_size / 1024 / 1024]; // Convert to MB
}),
itemStyle: {
color: 'blue'
}
}]
};
myChart.setOption(option);
});
</script>
</body>
</html>
python3 test\_tdsqlc\_scaling.py | tee test\_tdsqlc\_scaling.log
Tips:下文中的图片如果查看效果不佳,可点击鼠标右键,选择
在新标签页中打开图片
散点图说明:
整个测试过程中,主进程响应时间(rt)、压力进程响应时间(rt)和 Buffer Pool Size 变化过程如下:
如上图所示,第 300 秒压力进程开始运行后,Buffer Pool 共经历 5 次扩容,每次扩容平均耗时 35 秒,这个耗时与数字生态大会上分享的 Buffer Pool 会根据监控进行秒级扩容,准秒级缩容
差异还是很大的,个人猜测 秒级扩容
应该只是指 Buffer Pool 扩容动作本身的耗时,而不包括这之前的监控采集、分析决策、指令下达等动作。
关于扩容期间的响应时间,测试前的预期变化是在压力进程开启后,响应时间上升到一个较高的值,之后随着 Buffer Pool 的扩容响应时间逐渐减低。但实测后发现,响应时间除了在最后 3 次完成扩容的那一秒有明显的增长(最大 63.32ms)外,其他时间响应时间都很稳定的维持在 15ms 上下。从这个角度来看,TDSQL-C Serverless 的弹性伸缩方案优势很明显,配合上合理的弹性伸缩策略,其最大程度的保证了业务高峰时的稳定性。
TDSQL-C Serverless 的缩容过程同样是经历了 5 次 Buffer Pool 的调整,每一次的缩容规格都与扩容过程中的规格变化一致。与扩容过程不同的是,缩容的过程整体策略更保守,从监控采集到最后缩容成功的耗时更长。
从上图中可以看到,5 次缩容的耗时分别是 137 秒、93 秒、49 秒、53 秒、60 秒。这个时长相比于扩容耗时翻了至少一倍。之所以耗时这么久,应该是为了保证缩容过程中清除出内存池的数据页都是确确实实不再使用的,避免出现性能波动。观察缩容过程中的响应时间变化也可以证明这一点,从第 600 秒压力进程退出后,响应时间就回落至一开始的 5ms 上下,整个过程中未出现明显的异常点。
经过上述的实测可以发现,归功于其独有的弹性伸缩方案、合理的弹性策略,以及底层内核的针对性优化,TDSQL-C Serverless 的稳定性已经直逼传统数据库,实现了业务无感的平滑扩缩容。
在 Serverless 数据库扩缩容性能波动问题的解决方案上,TDSQL-C Serverless 交上了一份几乎完美的答卷。这份答卷意味着 Serverless 数据库已经不再是以前那个只能用于开发测试环境的玩具,而是可以承担更多的实际业务场景。再搭配上 TDSQL-C 的 集群能力
,数据库代理
、智能数据库管家
等能力和生态,更是具备了承载企业核心业务的资格。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。