高并发写利器-组提交,我的Spring组件实战

高并发写优化理论

对于高并发的读QPS优化手段较多,最经济简单的方式是上缓存。但是对于高并发写TPS该如何提升?业界常用的有分库分表、异步写入等技术手段。但是分库分表对于业务的改造十分巨大,涉及迁移数据的麻烦工作,不会作为常用的优化手段。异步写入到时经常在实际工作中使用,但是也不适合所有场景,特别对于带有事务的写入请求,带事务的写入请求通常是需要同步告知用户处理结果,所以不适用异步处理。

我们都知道批处理会比单条处理快很多,只需要发起一次网络请求,在网络层面节省了N次TCP连接获取和发送数据的步骤。实际我测试过,通过shark抓包,发现建立一条TCP连接可能需要耗费10ms~50ms左右。如果是跨洲际的TCP连接更久,可能耗费几百毫秒。单是节省的多次TCP连接就能节省不少时间,其次还有程序代码的循环执行时间。所以将多个写请求聚合成一个合适大小的批量写请求,一次性将数据发送给服务器进行批量写入是最高效的。

MySQL的组提交原理

在MySQL层面,为了保证事务的可靠性和数据同步给备节点、从节点的可靠性。通常会开启双一设置。在双一设置开启后,就会在事务提交前将redo log、binlog落盘,事务才返回成功,这就是WAL机制。

sync_binlog=1

innodb_flush_log_at_trx_commit=1

我们知道由于WAL机制,写入请求在修改了数据页后不会立即刷回磁盘,而是通过记录rodo log和binlog保证事务的持久性和同步给从节点。写rodo log和binlog就是顺序写入的,涉及磁盘的顺序写机制。磁盘顺序写会比随机写快很多。MySQL为了进一步提升多个事务在高并发下写入binlog的性能,采用了“组提交”的概念。顾名思义就是将多个事务在单位时间内聚集起来,一起写入磁盘,就变成了多事务的批量顺序写入,性能高很多。

这里简单介绍组提交。首先MySQL有2个参数控制组提交的等待时间和组大小。

binlog_group_commit_sync_delay=N:在等待N μs后,开始事务刷盘(图中Sync binlog)
binlog_group_commit_sync_no_delay_count=N:如果队列中的事务数达到N个,就忽视binlog_group_commit_sync_delay的设置,直接开始刷盘

解释下这张图。首先在第一步就已经将redo log刷到磁盘了,接下来就是将多个事务聚合在一个组调用write函数写入OS的缓冲。第一个到达的事务就会开启一个新组,等待N个事务到达或者等待N微秒之后主动提交。假设事务T1到达并开启新组1,等待T2来到加入组1,等待时间满后T1主动调用write函数将T1、T2事务都写入OS缓冲。此时T1、T2组成的组1进入第二个阶段,准备调用flush函数将缓冲区的数据刷入磁盘。组1在第二阶段继续等待新事务加入,此时有新组到达就会将组2和组1合并新组,再调用flush函数将组1、组2数据刷入磁盘。整个过程是批量+顺序写入磁盘,是很高效的。

我的组提交Spring组件

 我把这个组提交管理器的组件放到我GitHub上了,大家觉得不错的请Star,或觉得有优化空间的请提出mr,有错误的请斧正。

GitHub-组提交管理器

我们基于以上的理论分析,可以得出如果我们在高并发写入的时候能够模仿MySQL的组提交,实现一个主动等待和被动唤醒提交的组提交机制,将多个写入请求合成一个请求发送给MySQL就能提高写入性能。

总结MySQL的组提交机制原理:

  • 第一个到达的线程开启新组作为本组Leader领导本组的数据提交
  • Leader等待指定X毫秒时间,时间到后主动发起提交
  • 第K个线程到达,若发现本组负载满了唤醒Leader进行本组提交
  • 组与组之间互不阻塞,单位时间内可能有多个组并发提交

基于以上原理,我设计了两个类:GroupManager组管理器、GroupCommit组提交对象。GroupManager负责接收外部线程提交的数据,然后放到当前组里。并且实现整个组提交的流程。GroupCommit是一个组的具象化对象,提供一个组的入队,提交数据,挂起等待,唤醒Leader等基础方法,给GroupManager调用以实现组提交机制。

为了避免高并发时多线程竞相进入组内,导致组错乱,使用了两把锁解决。大部分线程都会被挡在第一关,每次只会放一个线程进到临界区尝试入组。入组之前要先获得当前组的锁,为什么要第二把锁?因为Leader会主动醒来提交本组的数据队列,所以提交时要确保所有资源都是排他的,需要组内锁来保证。入组的线程抢到组内锁之后就代表可以安全入组,此时有三种情况:

  • 如果此时入组前发现组已经满了就开一个新组自己当Leader并唤醒当前组的Leader让它赶快提交
  • 如果入组后发现组满了,唤醒当前组Leader让它赶快提交,自己则挂起等待提交后唤醒
  • 入组后发现还未满,挂起自己等待唤醒

线程在获取到组内锁后都会立即释放GroupManager的锁,目的是让后续线程如果发现当前组满了,就立即开新组提交,提高效率。

系统架构

 因为我们工作中大多数使用的是Tomcat容器,目前Tomcat的IO处理模型是Reactor+线程池的模式。

在整个系统架构层面,组提交影响性能的有两个参数:组大小和等待时间。组大小就是在组内挂起等待的线程数,等待时间是Leader主动等待的毫秒数。组大小直接影响到剩余可工作的线程数,Tomcat线程数量默认200,通常我们根据业务场景和硬件资源调整,线程数量也就几百左右。如果组大小太大同时等待时间太久!!直接把Tomcat所有线程都挂起了这时服务器就假死了,所以对组大小的设置建议通过压测来确定,按照下面的压测经验一般建议设置为Tomcat线程数量的1/4~1/2。这样最大1/2能确保还有一半线程可以服务其它请求。

等待时间,因为这个参数会导致接口RT上升,建议设置在5ms~20ms之间。我们生产MySQL的组提交等待时间设置500微秒,是很短的。我经过反复压测和调参发现,纯MySQL插入操作,等待时间5ms左右就合适了。

总结起来,组大小和等待时间需要根据业务类型和Tomcat线程数量和CPU数量,经过测试来决定一个合适的参数,没有通用的方法论能决定。

在整个系统架构层面,负载均衡器和服务器Pod,Tomcat线程池和多个组提交的关系。

压测报告

环境介绍

  • Mac OS M2 10核16G,SSD
  • MySQL 8.0
  • JDK8u221
  • SpringBoot,Tomcat线程池400
  • Druid数据库连接池 40连接数
  • Jmeter 5.3,700线程并发,循环1000,共70万请求
  • JVM参数设置

-XX:-ClassUnloadingWithConcurrentMark -Xms4g -Xmx4g -Xmn3g -XX:G1HeapRegionSize=4m -XX:InitiatingHeapOccupancyPercent=30 -XX:MaxGCPauseMillis=200 -XX:MaxMetaspaceSize=268435456 -XX:MetaspaceSize=268435456 -XX:ParallelGCThreads=10 -XX:+ParallelRefProcEnabled -XX:-ReduceInitialCardMarks -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC

MySQL没有经过调优都是默认的参数。MySQL和应用服务还有Jmeter都是在Mac上运行的。对比两种测试用例:1.使用组提交组件 2.单条数据写入。

    @PostMapping("/submit")
    public Boolean submit() {
        long tid = Thread.currentThread().getId();
        log.info("threadId={}", tid);
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderNo(UUID.randomUUID().toString());
        orderInfo.setAddressId(123321123321123L);
        orderInfo.setMerchantId(123321123321123L);
        orderInfo.setUserId(123321123321123L);
        orderInfo.setOrderAmount(BigDecimal.valueOf(123123L));
        return groupManager.queueGroup(orderInfo);
    }

    @PostMapping("/submit2")
    public Boolean submit2() {
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderNo(UUID.randomUUID().toString());
        orderInfo.setAddressId(123321123321123L);
        orderInfo.setMerchantId(123321123321123L);
        orderInfo.setUserId(123321123321123L);
        orderInfo.setOrderAmount(BigDecimal.valueOf(123123L));
        return orderInfoService.save(orderInfo);
    }

经过反复实验以及调整组提交的组大小、等待时间参数,得出组大小200,等待时间5ms,得出的TPS是比较好的。TPS达到近8800。接口错误率几乎没有

单提交(每次请求提交一次)所有配置和环境一致的情况下。并发700,循环1000次,70万请求。TPS在5200。错误率0

可以看出组提交比单提交TPS高出68%左右,优化比较明显。如果能针对组大小和等待时间继续调整优化,可能TPS会更高。RT上平均时间比但提交快了1倍,但是P99、P95、P90都比单提交要慢1倍。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/948294.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++Primer 变量

欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…

【模型】Qwen2-VL 服务端UI

1. 前言 最近在测试VLM模型,发现官方的网页demo,代码中视频与图片分辨率可能由于高并发设置的很小,导致达不到预期效果,于是自己研究了一下,搞了一个简单的前端部署,自己在服务器部署了下UI界面&#xff0…

分布式事务介绍 Seata架构与原理+部署TC服务 示例:黑马商城

1. 什么是分布式事务? 在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中的每个服务的事务就是一个分支事务。整个业务称为全局事务。 打个比…

uni-app:实现普通选择器,时间选择器,日期选择器,多列选择器

效果 选择前效果 1、时间选择器 2、日期选择器 3、普通选择器 4、多列选择器 选择后效果 代码 <template><!-- 时间选择器 --><view class"line"><view classitem1><view classleft>时间</view><view class"right&quo…

C++Primer 基本类型

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

纯前端实现将pdf转为图片(插件pdfjs)

需求来源 预览简历功能在移动端&#xff0c;由于用了一层iframe把这个功能嵌套在了app端&#xff0c;再用一个iframe来预览&#xff0c;只有ios能看到&#xff0c;安卓就不支持&#xff0c;查了很多资料和插件&#xff0c;原理基本上都是用iframe实现的。最终转换思路&#xf…

基于FPGA的出租车里程时间计费器

基于FPGA的出租车里程时间计费器 功能描述一、系统框图二、verilog代码里程增加模块时间增加模块计算价格模块上板视频演示 总结 功能描述 &#xff08;1&#xff09;&#xff1b;里程计费功能&#xff1a;3公里以内起步价8元&#xff0c;超过3公里后每公里2元&#xff0c;其中…

Unix 域协议汇总整理

Unix 域协议是一种用于同一台计算机上进程间通信&#xff08;IPC&#xff09;的技术。它提供了一种比基于网络的套接字更高效的方式来在本地进程中交换数据。Unix 域协议使用文件系统作为通信的媒介&#xff0c;并且只限于在同一台计算机上运行的进程之间进行通信。 Unix 域套接…

JVM学习:CMS和G1收集器浅析

总框架 一、Java自动内存管理基础 1、运行时数据区 运行时数据区可分为线程隔离和线程共享两个维度&#xff0c;垃圾回收主要是针对堆内存进行回收 &#xff08;1&#xff09;线程隔离 程序计数器 虚拟机多线程是通过线程轮流切换、分配处理器执行时间来实现的。为了线程切换…

1.C语言教程:历史、特点、版本与入门示例

目录 1.历史2.特点3.版本4.编译5.Hello World 示例 1.历史 本篇原文为&#xff1a;C语言教程&#xff1a;历史、特点、版本与入门示例。 更多C进阶、rust、python、逆向等等教程&#xff0c;可去此站查看&#xff1a;酷程网 C 语言的诞生源于 Unix 系统的开发需求。 1969 年…

lec7-路由与路由器

lec7-路由与路由器 1. 路由器硬件 路由器的硬件部分&#xff1a; 断电失去&#xff1a; RAM断电不失去&#xff1a;NVRAM&#xff0c; Flash&#xff0c; ROMinterface也算是一部分 路由器是特殊组件的计算机 console 口进行具体的调试 辅助口&#xff08;Auxiliary&…

spring防止重复点击,两种注解实现(AOP)

第一种&#xff1a;EasyLock 简介 为了简化可复用注解&#xff0c;自己实现的注解&#xff0c;代码简单随拿随用 使用方式 1.创建一个注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) Documented public interface EasyLock {long waitTime() default …

Linux-Ubuntu之I2C通信

Linux-Ubuntu之I2C通信 一&#xff0c;I2C通信原理1.写时序2.读时序 二&#xff0c;代码实现三&#xff0c;显示 一&#xff0c;I2C通信原理 使用I2C接口驱动AP3216C传感器&#xff0c;该传感器能实现两个效果&#xff0c;一个是感应光强&#xff0c;另一个是探测物体与传感器…

音视频入门基础:MPEG2-PS专题(4)——FFmpeg源码中,判断某文件是否为PS文件的实现

一、引言 通过FFmpeg命令&#xff1a; ./ffmpeg -i XXX.ps 可以判断出某个文件是否为PS文件&#xff1a; 所以FFmpeg是怎样判断出某个文件是否为PS文件呢&#xff1f;它内部其实是通过mpegps_probe函数来判断的。从《FFmpeg源码&#xff1a;av_probe_input_format3函数和AVI…

框架模块说明 #09 日志模块_01

背景 日志模块是系统的重要组成部分&#xff0c;主要负责记录系统运行状态和定位错误问题的功能。通常&#xff0c;日志分为系统日志、操作日志和安全日志三类。虽然分布式数据平台是当前微服务架构中的重要部分&#xff0c;但本文的重点并不在此&#xff0c;而是聚焦于自定义…

【数据仓库】hadoop3.3.6 安装配置

文章目录 概述下载解压安装伪分布式模式配置hdfs配置hadoop-env.shssh免密登录模式设置初始化HDFS启动hdfs配置yarn启动yarn 概述 该文档是基于hadoop3.2.2版本升级到hadoop3.3.6版本&#xff0c;所以有些配置&#xff0c;是可以不用做的&#xff0c;下面仅记录新增操作&#…

算法题(25):只出现一次的数字(三)

审题&#xff1a; 该题中有两个元素只出现一次并且其他元素都出现两次&#xff0c;需要返回这两个只出现一次的数&#xff0c;并且不要求返回顺序 思路: 由于对空间复杂度有要求&#xff0c;我们这里不考虑哈希表。我们采用位运算的方法解题 方法&#xff1a;位运算 首先&#…

将机器学习预测模型融入AI agent的尝试(一)

将机器学习临床预测模型融入AI agent的尝试&#xff08;一&#xff09; 我主要是使用机器学习制作临床预测模型和相关的应用&#xff0c;最近考虑的事情是自己之前的的工作能不能和AI agent进行融合&#xff0c;将AI 对自然语言理解能力和预测模型的预测能力结合在一起&#x…

51单片机——按键实验

由于机械点的弹性作用&#xff0c;按键开关在闭合时不会马上稳定的接通&#xff0c;在断开时也不会一下子断开&#xff0c;因而在闭合和断开的瞬间均伴随着一连串的抖动。抖动时间的长短由按键的机械特性决定的&#xff0c;一般为 5ms 到 10ms&#xff0c;为了确保 CPU 对按键的…

电子邮件对网络安全的需求

&#xff08; 1&#xff09;机密性&#xff1a;传输过程中不被第三方阅读到邮件内容&#xff0c;只有真正的接收方才可以阅读邮件。&#xff08; 1.5 分&#xff09; &#xff08; 2&#xff09;完整性&#xff1a;支持在邮件传输过程中不被篡改&#xff0c;若发生篡改&#…