赞
踩
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 的测试方法,选择响应时间作为稳定性指标,同时观测innodb_buffer_pool_size
的秒级变化判断伸缩节点,并结合 TDSQL-C Serverless 的特性对测试时长等进行了部分调整。最终的测试方案如下:
oltp_read_write
,将 --report-interval
设置成 1s,将 --percentile
设置为 99 作为平均延迟(响应时间 rt)SHOW VARIABLES LIKE "innodb_buffer_pool_size"
命令持续观测 Buffer Pool Size,以该数值的大小变化作为资源调整变化的指标TDSQL-C Serverless 规格:MySQL5.7 引擎,单节点(只有一个读写实例),最小 CCU0.5,最大 CCU32
客户端规格:腾讯云轻量应用服务器,配置为 4C8G
网络环境:通过云联网功能实现轻量应用服务器到 TDSQL-C Serverless 的内网互联
准备测试数据
数据库管理
功能创建测试库 test_scaling
sysbench --db-driver=mysql --mysql-host=172.21.0.15 --mysql-port=3306 \
--mysql-user=root --mysql-password=xxxx \
--mysql-db=test_scaling --table_size=100000 --tables=1 --threads=1 \
oltp_read_write prepare
编写 Python 脚本实现测试流程
# -*- 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)
使用 Echarts 散点图分析结果数据
<!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 Serverless 2.0 版本还实现了全球首个可释放存储架构的 Serverless 服务:集群无访问时段数据可落冷归档,启动时可瞬时恢复服务,无需等待数据全量恢复,该版本还提供了集群版 Serverless,支持只读节点和 Proxy 弹性能力。随着这些能力的发布升级,极大的丰富了 Serverless 当前的应用场景,使 TDSQL-C Serverless 得以全面承载核心业务场景。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。