【腾讯云 TDSQL-C Serverless 产品测评】深度实测TDSQL-C Serverless 弹性伸缩策略及稳定性

前言

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 Serverless 服务是腾讯云自研的新一代云原生关系型数据库 TDSQL-C MySQL 版的无服务器架构版,是全 Serverless 架构的云原生数据库。架构图如下:

TDSQL-C Serverless 架构介绍截图

TDSQL-C Serverless 的三大核心特性:

  • 算存分离,资源池化
    • 计算存储分离,根据负载独立弹性,不受单机瓶颈限制,根据业务发展平滑拓展集群
    • 多个计算节点共享存储,存储层容量在线平滑拓展,可承载 PB 级数据规模
    • 冷数据可落冷到 COS 存储中,进一步压缩存储成本,做到不使用不付费
  • 极致弹性,自动扩缩容
    • CPU、内存、存储三层解耦,数十种监控指标触发弹性,根据实际负载情况进行三层独立弹性,将资源利用率发挥到极致
    • 多种弹性方式融合,针对不同场景的业务形态,融合共享资源与独立资源的优势,保证资源利用率
    • 结合时序算法等预测式弹性,提前触发扩缩容
  • 全面 Serverless 化,充分调度
    • 支持集群全面 Serverless 化,将资源更细粒度进行拆解,快速添加 RO 节点,每个实例可进行独立弹性,贴合业务使用情况
    • 支持 Serverless 节点混部,供业务侧灵活搭配 Serverless 能力,丰富数据库使用场景
    • 自动读写分离,屏蔽后端扩缩容动作,用户只需关注业务层
    • 依赖调度器全方面分析资源使用情况,多种算法加持保证弹性伸缩的准确性

二、TDSQL-C Serverless 的弹性伸缩方案

数字生态大会PPT截图

关于 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 的特性进行了部分调整。

1.测试设计

  • 使用 Sysbench 作为测试程序,场景选用 oltp_read_write,将 --report-interval 设置成 1s,将 --percentile 设置为 99 作为平均延迟(响应时间 rt)
  • 同时使用 SHOW VARIABLES LIKE "innodb_buffer_pool_size" 命令持续观测 Buffer Pool Size,以该数值的大小变化作为资源调整变化的指标

2.测试流程:

  • 首先,启动一个单线程 Sysbench,作为测试 “主进程”,程序运行 1200 秒
  • “主进程” 运行 300 秒后,再启动一个 “压力进程”(24 并发的 Sysbench 进程)向数据库施压,该进程运行 300 秒后退出
  • 将 Sysbench 每秒返回的报告信息和与之对应的 Buffer Pool Size 数据存入 csv,之后读取 csv 数据使用 Echarts 生成散点图以供分析

3.测试准备工作

  • TDSQL-C Serverless 规格:MySQL5.7 引擎,单节点(只有一个读写实例),最小 CCU0.5,最大 CCU32

    在这里插入图片描述

  • 客户端规格:腾讯云轻量应用服务器,配置为 4C8G

  • 网络环境:通过云联网功能实现轻量应用服务器到 TDSQL-C Serverless 的内网互联

  • 准备测试数据

    • 使用 TDSQL-C Serverless 控制台的 数据库管理 功能创建测试库 test_scaling
      在这里插入图片描述
    • 使用 Sysbench 生成测试数据
      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
      
      在这里插入图片描述

4.开始测试

  • 编写 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
    

5.测试结果分析

Tips:下文中的图片如果查看效果不佳,可点击鼠标右键,选择在新标签页中打开图片

5.1.整体过程分析

TDSQL-C_Test_Scaling_All
散点图说明:

  • 红点代表 ”主进程” 响应时间(rt)随时间(秒)变化趋势
  • 灰点代表 ”压力进程” 响应时间(rt)随时间(秒)变化趋势
  • 蓝点(连成线)代表 Buffer Pool Size 随时间(秒)变化趋势
  • 横坐标是时间,间隔为 1 秒
  • 左侧纵坐标单位为毫秒,是 rt 的单位;右侧纵坐标单位是 MB,是 Buffer Pool Size 单位

整个测试过程中,主进程响应时间(rt)、压力进程响应时间(rt)和 Buffer Pool Size 变化过程如下:

  • 0 秒—300 秒,主线程响应时间维持在 5ms 左右,Buffer Pool Size 为512M
  • 第 300 秒,压力进程开始运行,Buffer Pool 开始扩容,经过 5 次扩容之后稳定在 22912M(22.375G)
  • 300 秒—600 秒(压力进程运行期间),主进程响应时间由之前的 5ms 左右提升至 15ms 左右
  • 第 600 秒,压力进程退出,之后 Buffer Pool 经过了 5 次缩容,恢复至最小规格 512M
  • 600 秒之后,主进程响应时间回落至 5ms 左右
  • 整个测试过程中,Buffer Pool Size 按照固定规格呈阶梯性扩/缩容,规格阶梯:0.5G → 1G → 2.375G → 5.5G → 11.125G → 22.375G
5.2.扩容过程分析

TDSQL-C_Test_Scaling_UP

如上图所示,第 300 秒压力进程开始运行后,Buffer Pool 共经历 5 次扩容,每次扩容平均耗时 35 秒,这个耗时与数字生态大会上分享的 Buffer Pool 会根据监控进行秒级扩容,准秒级缩容 差异还是很大的,个人猜测 秒级扩容 应该只是指 Buffer Pool 扩容动作本身的耗时,而不包括这之前的监控采集、分析决策、指令下达等动作。

关于扩容期间的响应时间,测试前的预期变化是在压力进程开启后,响应时间上升到一个较高的值,之后随着 Buffer Pool 的扩容响应时间逐渐减低。但实测后发现,响应时间除了在最后 3 次完成扩容的那一秒有明显的增长(最大 63.32ms)外,其他时间响应时间都很稳定的维持在 15ms 上下。从这个角度来看,TDSQL-C Serverless 的弹性伸缩方案优势很明显,配合上合理的弹性伸缩策略,其最大程度的保证了业务高峰时的稳定性。

5.3.缩容过程分析

TDSQL-C_Test_Scaling_Down
TDSQL-C Serverless 的缩容过程同样是经历了 5 次 Buffer Pool 的调整,每一次的缩容规格都与扩容过程中的规格变化一致。与扩容过程不同的是,缩容的过程整体策略更保守,从监控采集到最后缩容成功的耗时更长。

从上图中可以看到,5 次缩容的耗时分别是 137 秒、93 秒、49 秒、53 秒、60 秒。这个时长相比于扩容耗时翻了至少一倍。之所以耗时这么久,应该是为了保证缩容过程中清除出内存池的数据页都是确确实实不再使用的,避免出现性能波动。观察缩容过程中的响应时间变化也可以证明这一点,从第 600 秒压力进程退出后,响应时间就回落至一开始的 5ms 上下,整个过程中未出现明显的异常点。

四、总结

经过上述的实测可以发现,归功于其独有的弹性伸缩方案、合理的弹性策略,以及底层内核的针对性优化,TDSQL-C Serverless 的稳定性已经直逼传统数据库,实现了业务无感的平滑扩缩容。

在 Serverless 数据库扩缩容性能波动问题的解决方案上,TDSQL-C Serverless 交上了一份几乎完美的答卷。这份答卷意味着 Serverless 数据库已经不再是以前那个只能用于开发测试环境的玩具,而是可以承担更多的实际业务场景。再搭配上 TDSQL-C 的 集群能力数据库代理智能数据库管家 等能力和生态,更是具备了承载企业核心业务的资格。