美文网首页
第3篇:mysql性能与深入理解并发

第3篇:mysql性能与深入理解并发

作者: 小肥爬爬 | 来源:发表于2023-06-01 17:42 被阅读0次

    思路

    本篇会针对单机环境的mysql进行模拟插入测试, 最终得到性能指标, 据此进行架构的设计与优化.

    原先我的写作思路是写一步代码写一步要点, 让大家更好理解. 后来想既然我踩了那么多坑, 为啥让大家再踩一次... 所以直接先把程序放出, 根据代码进行讲解.

    代码的地址如下:

    git clone https://gitee.com/xiaofeipapa/springcloud-adv -b high-concurrency
    

    工程是这个:


    image.png

    工程要点

    用 hikari 数据源代替druid

    网上很多springcloud的教程都写着用druid数据源, 其实这已经是很落后的信息了. druid在并发数稍大的时候就会出现连接丢失, 用hikari就不会有这个问题.

    
            <!-- 主要增加 HikariCP 依赖 -->
            <dependency>
                <groupId>com.zaxxer</groupId>
                <artifactId>HikariCP</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
                <exclusions>
                    <!-- 排除 tomcat-jdbc 以使用 HikariCP -->
                    <exclusion>
                        <groupId>org.apache.tomcat</groupId>
                        <artifactId>tomcat-jdbc</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    

    sql与模拟程序

    我们的目标是模拟创建用户. 脚本如下:

    
    # -- mysql 性能测试
    DROP TABLE IF EXISTS `mock_user`;
    CREATE TABLE `mock_user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `name` varchar(500) NOT NULL comment '用户名',
      `mobile` varchar(500) NOT NULL comment '手机号',
      `create_time` datetime NOT NULL  COMMENT '创建时间',
      `mock_1` varchar(500) NOT NULL,
      `mock_2` varchar(500) NOT NULL,
      `mock_3` varchar(500) NOT NULL,
      `mock_4` varchar(500) NOT NULL,
      `mock_5` varchar(500) NOT NULL,
      `mock_6` varchar(500) NOT NULL,
      `mock_7` varchar(500) NOT NULL,
      `mock_8` varchar(500) NOT NULL,
      `mock_9` varchar(500) NOT NULL,
      `mock_10` varchar(500) NOT NULL,
      `mock_11` varchar(500) NOT NULL,
      `mock_12` varchar(500) NOT NULL,
      `mock_13` varchar(500) NOT NULL,
      `mock_14` varchar(500) NOT NULL,
      `mock_15` varchar(500) NOT NULL,
      `mock_16` varchar(500) NOT NULL,
      `mock_17` varchar(500) NOT NULL,
      `mock_18` varchar(500) NOT NULL,
      `mock_19` varchar(500) NOT NULL,
      `mock_20` varchar(500) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8;
    

    程序如下:

    // --------------- service 程序
    
    
        @Override
        public String saveUser(String phone) {
    
            // 生成随机姓名
            String name = PersonInfoSource.getInstance().randomChineseName();
    
            MockUser mockUser = new MockUser();
            mockUser.setName(name);
            mockUser.setMobile(phone);
            mockUser.setCreateTime(LocalDateTime.now());
    
            // 反射设置属性
            for (int k=1; k <= 20;k++){
                String key = "mock"+k;
    
                //生成个随机汉字
                String xx = OtherSource.getInstance().randomChinese(200);
                ReflectUtil.setFieldValue(mockUser, key, xx);
            }
    
            try{
                Long start = System.nanoTime();
                this.save(mockUser);
                log.info("---- 线程id: {} , 运行总时间: {}", Thread.currentThread().getId(), TimeUtils.catSecond(start));
                return "成功, 新用户: " + mockUser.getName();
            }catch (Exception e){
                log.error("#### 新增用户失败. ", e);
                return "失败";
            }
        }
    
    
    // ---------------------- controller
    
        @Resource
        IMockUserService mockUserService;
    
        @GetMapping("/test/mock")
        public String mockInsert(@RequestParam String phone, @RequestParam Integer actId, @RequestParam Integer num) {
            return mockUserService.saveUser(phone);
        }
    

    为了尽量真实模拟业务场景, 我在用户表增加了20个比较大的字段, 这些字段内容用循环生成.

    mysql 服务端配置

    如果在本机装好mysql(8的版本), 并且没有任何优化, 那么插入数据很很难看. 所以要修改mysql的配置. 我这里是ubuntu系统, 其他系统请搜索相关帖子, 看看配置文件在哪...

    sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
    
    
    [mysqld]
    #
    # * Basic Settings
    #
    user        = mysql
    # pid-file  = /var/run/mysqld/mysqld.pid
    # socket    = /var/run/mysqld/mysqld.sock
    # port      = 3306
    # datadir   = /var/lib/mysql
    
    
    # If MySQL is running as a replication slave, this should be
    # changed. Ref https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_tmpdir
    # tmpdir        = /tmp
    #
    # Instead of skip-networking the default is now to listen only on
    # localhost which is more compatible and is not less secure.
    bind-address        = 127.0.0.1
    mysqlx-bind-address = 127.0.0.1
    #
    # * Fine Tuning
    #
    # key_buffer_size       = 16M
    # max_allowed_packet    = 64M
    # thread_stack      = 256K
    
    # thread_cache_size       = -1
    
    # This replaces the startup script and checks MyISAM tables if needed
    # the first time they are touched
    myisam-recover-options  = BACKUP
    
    # max_connections        = 2000
    
    # table_open_cache       = 4000
    
    #
    # * Logging and Replication
    #
    # Both location gets rotated by the cronjob.
    #
    # Log all queries
    # Be aware that this log type is a performance killer.
    # general_log_file        = /var/log/mysql/query.log
    # general_log             = 1
    #
    # Error log - should be very few entries.
    #
    # log_error = /var/log/mysql/error.log
    #
    # Here you can see queries with especially long duration
    # slow_query_log        = 1
    # slow_query_log_file   = /var/log/mysql/mysql-slow.log
    # long_query_time = 5
    # log-queries-not-using-indexes
    #
    # The following can be used as easy to replay backup logs or for replication.
    # note: if you are setting up a replication slave, see README.Debian about
    #       other settings you may need to change.
    # server-id     = 1
    # log_bin           = /var/log/mysql/mysql-bin.log
    # binlog_expire_logs_seconds    = 2592000
    # max_binlog_size   = 100M
    # binlog_do_db      = include_database_name
    # binlog_ignore_db  = include_database_name
    
    # ---- mysql 性能调整
    key_buffer_size     = 256M
    max_allowed_packet  = 64M
    thread_stack        = 256K
    open_files_limit    = 65535
    back_log = 1024
    max_connections = 3000
    max_connect_errors = 1000000
    table_open_cache = 1024
    table_definition_cache = 1024
    thread_stack = 512K
    sort_buffer_size = 4M
    join_buffer_size = 4M
    read_buffer_size = 8M
    read_rnd_buffer_size = 4M
    bulk_insert_buffer_size = 128M
    thread_cache_size = 768
    interactive_timeout = 600
    wait_timeout = 600
    tmp_table_size = 32M
    max_heap_table_size = 32M
    
    # ---- log settings
    log_timestamps = SYSTEM
    log_error = /var/lib/mysql/error.log
    log_error_verbosity = 3
    slow_query_log = 1
    log_slow_extra = 1
    slow_query_log_file = /var/lib/mysql/slow.log
    long_query_time = 0.1
    log_queries_not_using_indexes = 1
    log_throttle_queries_not_using_indexes = 60
    min_examined_row_limit = 100
    log_slow_admin_statements = 1
    log_slow_slave_statements = 1
    log_bin = /var/lib/mysql/mybinlog
    binlog_format = ROW
    sync_binlog = 1 #MGR环境中由其他节点提供容错性,可不设置双1以提高本地节点性能
    binlog_cache_size = 4M
    max_binlog_cache_size = 2G
    max_binlog_size = 1G
    binlog_rows_query_log_events = 1
    binlog_expire_logs_seconds = 604800
    #MySQL 8.0.22前,想启用MGR的话,需要设置binlog_checksum=NONE才行
    # binlog_checksum = CRC32
    # gtid_mode = ON
    # enforce_gtid_consistency = TRUE
    
    # ---- innodb settings
    transaction_isolation = REPEATABLE-READ
    innodb_buffer_pool_size = 6144M
    innodb_buffer_pool_instances = 4
    innodb_data_file_path = ibdata1:12M:autoextend
    innodb_flush_log_at_trx_commit = 1 #MGR环境中由其他节点提供容错性,可不设置双1以提高本地节点性能
    innodb_log_buffer_size = 32M
    innodb_log_file_size = 1G #如果线上环境的TPS较高,建议加大至1G以上,如果压力不大可以调小
    innodb_log_files_in_group = 3
    innodb_max_undo_log_size = 4G
    # 根据您的服务器IOPS能力适当调整
    # 一般配普通SSD盘的话,可以调整到 10000 - 20000
    # 配置高端PCIe SSD卡的话,则可以调整的更高,比如 50000 - 80000
    innodb_io_capacity = 4000
    innodb_io_capacity_max = 8000
    innodb_open_files = 65535
    innodb_flush_method = O_DIRECT
    innodb_lru_scan_depth = 4000
    innodb_lock_wait_timeout = 10
    innodb_rollback_on_timeout = 1
    innodb_print_all_deadlocks = 1
    innodb_online_alter_log_max_size = 4G
    innodb_print_ddl_logs = 1
    innodb_status_file = 1
    #注意: 开启 innodb_status_output & innodb_status_output_locks 后, 可能会导致log_error文件增长较快
    innodb_status_output = 0
    innodb_status_output_locks = 1
    innodb_sort_buffer_size = 67108864
    innodb_adaptive_hash_index = OFF
    innodb_autoextend_increment=100
    
    
    sudo service mysql restart
    

    网上一些文章写mysql配置的时候, 只写改变 max_connections 这个参数, 其实是不对的, 需要调整的参数非常多. 建议读一下<高性能MySql> 这本书.

    事实上多数公司的生产环境数据库会购买阿里云/aws的产品,不需要程序员自己调优. 所以本篇不作过多mysql的深入介绍, 让这个配置足以满足本机测试即可.

    测试与结果分析

    这个工程只需要独立测试, 因此不需要再启动nacos. 启动工程, 然后把上一章的jmeter测试用例修改修改, 改成这样:
    链接地址: /test/mock
    端口: 16000
    线程数: 从500-2000逐个测试.

    并且, 增加一个 Aggregate Report


    image.png

    这个报表能够看到中位数数据, 会更精准.

    最终我的本机测试数据如下(循环次数10次, 每次测试均执行5次,取平均值):

    请求数 线程数 平均完成时间(ms) 中位数(ms) 90%请求时间(ms) 95%请求时间(ms) 99%请求时间(ms) 请求最长时间(ms)
    5000 500 352 192 879 1118 1663 2637
    7500 750 774 454 1964 2536 3821 5902
    10000 1000 1039 653 2638 3408 5096 9111
    15000 1500 1887 1347 4515 5748 8380 16567

    可以看到, 随着线程数的增加, 数据逐渐高得离谱起来. 为什么会这样呢? 简单说就是mysql的事务机制(红黑树/binlog.. 等) 会很频繁地创建IO, 性能高不到哪去. 那么怎么调整呢? 可以从硬件优化, 代码级别优化和架构优化层面去进行.

    我们来逐步分析.

    硬件优化

    购买更高配的mysql服务器是主要解决手段之一. 但不能只买硬件而不做架构和代码优化. 就算新的服务器能抗住4000个并发插入, 那么当并发数变成 8000个的时候怎么办? 再次买高配服务器?

    当然我不得不说这也是一个思路, 事实上一些没有架构师的小公司就是这么做的. 又或者经过一段时期的观察发现公司实际上没有多大的并发量, 那么硬件优化足以解决问题.

    牢记架构师的原则: 观察真实业务情况, 不要过度设计/开发

    代码层面优化

    当然我写这篇博客的目的, 是针对那些真的有不错流量的公司/程序员. 假设我们就真的要做优化. 代码层面优化有两个主要思路. 一个是减少事务数, 把单条插入语句合并到多条.

    INSERT INTO 
    [表名]([列名],[列名]) 
     VALUES
    ([列值],[列值])),
    ([列值],[列值])),
    ([列值],[列值]));
    

    然后数据库配置设置 auto_commit = false , 每次都主动开启/关闭事务.

    这种代码的写法能极大提高mysql的性能. 问题是对应用代码的侵入性非常大. 批量语句每批多少条数据? 如果事务失败了, 这些数据怎么重新插入? 这些都把问题扔给了应用开发程序员.

    所以代码层面的优化要做, 不过优先级应该排于架构优化之后. 通过设计一套能利用上代码优点, 又尽量对应用开发人员透明的架构, 才是好的设计. (不过通常情况下, 架构和代码写法需要互相适应)

    架构优化与业务再次思考

    如上分析, 最终我们简单的controller->service->mysql的架构, 是没办法满足真实的高并发场景的, 我们要做出优化. 我们先来重新思考业务和指标: 什么叫做tps, 什么叫做用户同时访问.

    业务的再次分析

    从我多年的工作经验来看, "支持1万个用户抢单, 其中2000个能抢成功" (上文定的单机测试指标) 这个需求, 对于业务人员来说就是 2000并发, 很多程序员由此推导出系统需要2000 tps, 甚至直接推导数据库需要2000 tps ..... 其实不是的, 我们来从http请求的角度来分析这个需求

    何谓支持1万个用户?

    我们来看看这个图, 假设有1万个用户访问应用集群如下:


    image.png

    那么tomcat是怎么支持这些请求的呢? tomcat的配置有几个核心参数:

      tomcat:
        threads:
          max: 200
          # ---------- springboot 旧版本配置
    #    # 最大工作线程数, 默认200
    #    max-threads: 2000
    #    # 最大连接数 从 100 变成 200
       max-connections: 2000
    #    # 等待队列
       accept-count: 100
    #    min-spare-threads: 500
    

    max-connections: 最大连接数. (不同版本的tomcat, yaml配置不同)
    accept-count: 等待队列.

    可以这样去理解, 假设只有1个tomcat, 它的最大连接数是 2000, 那么其实剩下的8000个请求都在 accept-count 这个队列等待. 如果等到指定超时时间都没有连上, 客户端最终会得到连接失败这个应答.

    所以首先来说, 不能出现有用户连接失败这个情况. 然后我们上篇文章说过, 用户的通用软件体验是单个请求完成时间最好在1-4秒. 也就是, 所有用户(注意是所有)的请求时间都要处于1-4秒之间, 当然越快越好.

    计算架构tps

    电商的购物和支付功能背后都会调用一系列内部服务和外部服务(如库存子系统, 消息子系统, 物流子系统, 积分子系统, 第三方支付系统... ) , 这些子系统都各有自己的tps, 我们可以先推导出整个架构的tps, 再推导子系统的tps.

    假设请求是一个写操作(如上面的创建用户), 那么可以近似理解成一个请求就是一个架构 tps, 那么"支持1万个用户抢单", 就可以整理成如下变量表格:

    请求完成时间(ms) tomcat 每秒可处理请求数 近似架构tps
    50 40000 40 000 tps
    100 20000 20 000 tps
    200 10000 10 000 tps
    300 6600 6 667 tps
    400 5000 5000 tps

    (以上按照单台tomcat 2000连接数设置)

    根据这个表格, 我们只要把 nacos和两三台tomcat 连起来, 就轻松满足 1万tps 的目标了.

    为什么请求完成时间是400ms

    之前说过用户能够接受的请求完成体验是1-4秒, 但别忘了这1-4秒是整个系统共享的时间, 包括前端应用(网站/app) 和后端接口时间. 从经验来看, 400ms是比较标准的答案, 最大不要超过800ms. 否则整个请求完成时间很可能超过4秒.

    优化架构的理由找到了

    现在我们最终完成了从口头需求 -> tps数据的量化分析和整理, 总结下了变量指标. 那么优化架构的理由也找到了. 从之前的测试来看, 无数据库操作的请求处理, 单机能轻松超过1万tps, 每个请求小于200ms. 但是mysql 无论怎么优化都不能达到这个数据. 为此架构要做调整, 从同步架构变成异步架构. 这是下一章的内容了.

    再次提醒, 代码在这:

    git clone https://gitee.com/xiaofeipapa/springcloud-adv -b high-concurrency
    

    相关文章

      网友评论

          本文标题:第3篇:mysql性能与深入理解并发

          本文链接:https://www.haomeiwen.com/subject/klwuedtx.html