1. 项目概述:为什么一个数据库性能测试值得专门写一篇实操笔记?
OceanBase 2.2 这个版本在业内有个很实在的标签——“从金融核心场景反向锤炼出来的稳定器”。它不是靠参数堆出来的纸面性能,而是被某大型支付平台的真实交易洪峰反复冲刷过、在账务一致性与高并发吞吐之间找到新平衡点的版本。我第一次在某银行信创替代项目中接触它时,最深的印象不是TPS数字多高,而是当JMeter线程数从500拉到3000、再突然断掉1/3连接时,它的事务成功率曲线几乎没抖动。这种“不慌”的底气,恰恰是很多新手用JMeter跑完一轮压测后,只看到“平均响应时间42ms”就以为搞定,却完全没意识到背后真正该盯住的指标。
这篇笔记的核心关键词就是OceanBase 2.2、JMeter、数据库性能测试、TPC-C模拟、分布式事务压测。它不是教你怎么点开JMeter界面,而是讲清楚:当你手握一套OceanBase 2.2集群,想用JMeter真实摸清它的“肌肉线条”和“呼吸节奏”,到底该从哪下刀、切多深、怎么看切口里的组织纹理。适合三类人直接抄作业:一是刚接手OceanBase运维的DBA,需要快速建立性能基线;二是参与信创适配的中间件工程师,得拿出有说服力的对比数据;三是做数据库选型的技术负责人,需要避开厂商白皮书里那些“理想条件下的峰值”陷阱。你不需要懂OceanBase源码,但得愿意花15分钟改几行SQL脚本、调两个JMeter线程组参数——这些动作背后的真实逻辑,我会掰开揉碎讲透。
很多人误以为数据库压测就是“把JMeter线程数拉满,看TPS能冲到多少”。这就像只测汽车极速,却不管它在连续过弯时的底盘支撑、刹车热衰减、转向比反馈。OceanBase 2.2 的分布式架构决定了它的性能瓶颈从来不在单点CPU或磁盘IO,而在于日志同步延迟、全局时间戳(GTS)服务压力、分区键设计导致的热点倾斜、以及两阶段提交(2PC)在跨机房场景下的网络抖动放大效应。JMeter在这里不是万能锤,而是你的听诊器——你得知道把探头贴在哪根血管上,才能听见真实的血流声。接下来的内容,就是带你校准这支听诊器,并教会你如何解读每一次心跳的波形图。
2. 整体设计思路:为什么不用Sysbench,而坚持用JMeter手写脚本?
2.1 放弃Sysbench的三个硬理由
刚接手这个任务时,团队里有位老同事第一反应是:“直接上Sysbench不就完了?社区模板多,一键生成报告。” 我当时没反对,但拉出他跑出来的结果一看,立刻叫停了。原因很具体:
第一,Sysbench的oltp_point_select默认走的是单表主键查询,而OceanBase 2.2在单表场景下性能接近MySQL,根本测不出它真正的分布式能力边界。我们真正关心的是跨10个分库、每个库含32个分表的订单中心,在秒杀场景下如何保证“库存扣减+订单生成+积分更新”三步操作的强一致性。Sysbench的固定SQL模板根本无法模拟这种业务链路。
第二,Sysbench对OceanBase特有的“弱一致性读”(Read Consistency Level)支持极差。OceanBase 2.2允许客户端在事务内指定读取快照级别(如READ_COMMITTED或FOR UPDATE),这对降低GTS压力至关重要。Sysbench的SQL注入机制无法动态拼接hint,导致所有读请求都默认走最强一致性,人为制造了GTS服务瓶颈,测出来的“性能差”其实是配置错误,不是数据库真不行。
第三,也是最关键的一点:Sysbench的监控粒度太粗。它只给一个全局TPS和平均延迟,但OceanBase 2.2的慢查询日志(slow log)里会精确记录每条SQL的“等待GTS时间”、“等待日志同步时间”、“分区路由耗时”。如果你的JMeter脚本里每条SQL都加了唯一trace_id,就能把JMeter的响应时间分解成“网络传输+SQL解析+GTS获取+分区路由+本地执行+日志同步”六个环节。这才是定位问题的黄金路径。
提示:我后来用Sysbench跑了一轮纯读场景作为基线对照,但所有核心压测全部切换到JMeter自定义脚本。这不是炫技,而是因为业务SQL的复杂度、Hint的灵活性、以及链路追踪的可定制性,Sysbench确实做不到。
2.2 JMeter方案的三层架构设计
我们的JMeter压测方案不是简单拖几个HTTP Sampler,而是按数据库压测的本质需求,拆成三层结构:
第一层:数据准备层(Pre-Test Setup)
用Python脚本生成符合业务特征的测试数据。比如订单表,不能只塞100万条id=1,2,3…的流水号,而是要模拟真实分布:80%订单集中在华东区(对应OceanBase的zone='HZ'),20%分散在全国其他12个zone;用户ID按MD5哈希后取模分配到不同分库,确保数据倾斜可控。这部分数据会提前灌入OceanBase,并用ANALYZE TABLE更新统计信息,避免执行计划走错。第二层:压测执行层(Main Test Thread Group)
这是核心。我们创建了3个并行的Thread Group,分别模拟三类典型负载:- Group A:强一致性事务流(占比40%)—— 模拟支付扣款,包含BEGIN/UPDATE/INSERT/COMMIT,强制走2PC;
- Group B:最终一致性读流(占比50%)—— 模拟订单详情页,SQL末尾加
/*+ READ_CONSISTENCY(WEAK) */hint,绕过GTS; - Group C:GTS压力专项流(占比10%)—— 单独发起高频
SELECT /*+ GTS_QUERY */ 1 FROM DUAL,用于单独压测GTS服务。
第三层:监控采集层(Post-Test Monitoring)
不依赖JMeter自带的Aggregate Report,而是通过JDBC Connection Pool的JMX接口,实时抓取ActiveCount、IdleCount、WaitCount;同时用OceanBase提供的obproxy日志分析工具,提取每条SQL的execute_time、queue_time、net_time字段。这三层数据在InfluxDB里打上相同timestamp,最后用Grafana画出四维关联图:JMeter TPS曲线 + OB Proxy队列等待时间 + GTS服务RT + JVM GC Pause。
这个设计的底层逻辑很朴素:把OceanBase 2.2当成一个有“神经系统”(GTS)、“血液循环系统”(日志同步)、“骨骼肌肉系统”(分区路由与本地执行)的活体,而不是一台冰冷的SQL处理机。JMeter只是你的手指,真正要感知的是整个身体的反馈。
2.3 为什么必须手写JDBC Sampler而非用JDBC Request?
JMeter自带的JDBC Request配置面板看着方便,但它隐藏了太多关键控制点。比如,它默认开启autocommit=true,而OceanBase 2.2在autocommit=true模式下,每条DML都会触发一次隐式2PC,这会严重放大网络抖动的影响。我们必须用JSR223 Sampler手写Groovy代码,才能精细控制:
import java.sql.* import com.alibaba.druid.pool.DruidDataSource // 手动管理连接池,避免JMeter内置连接池的不可控行为 def ds = new DruidDataSource() ds.setUrl("jdbc:oceanbase://192.168.10.10:2883/testdb?useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true") ds.setUsername("root@sys") ds.setPassword("") ds.setInitialSize(5) ds.setMaxActive(50) def conn = ds.getConnection() conn.setAutoCommit(false) // 关键!关闭自动提交,由脚本显式控制事务边界 def stmt = conn.prepareStatement("UPDATE stock SET qty = qty - ? WHERE sku_id = ? AND qty >= ?") stmt.setInt(1, vars.get("deduct_qty") as int) stmt.setString(2, vars.get("sku_id")) stmt.setInt(3, vars.get("deduct_qty") as int) def affected = stmt.executeUpdate() if (affected == 0) { // 库存不足,回滚并抛异常触发JMeter重试逻辑 conn.rollback() throw new RuntimeException("Stock insufficient for sku ${vars.get('sku_id')}") } else { conn.commit() // 显式提交,确保2PC流程完整 }这段代码里藏着三个实战经验:
- 用Druid而非HikariCP:OceanBase 2.2对Druid的
removeAbandonedOnBorrow机制兼容性更好,能及时清理因网络闪断导致的僵死连接; rewriteBatchedStatements=true必须开启:OceanBase 2.2的批量插入优化依赖此参数,否则addBatch()会被拆成N条单条执行;allowMultiQueries=true是双刃剑:它让一条SQL里能写UPDATE; INSERT;,但会禁用预编译缓存,所以只在事务链路里用,单条查询坚决不用。
这些细节,JDBC Request配置框里一个都填不了。你必须亲手写,才能真正握住方向盘。
3. 核心细节解析:OceanBase 2.2特有的五个压测陷阱与破解法
3.1 陷阱一:分区键(Partition Key)设计不当引发的“伪瓶颈”
OceanBase 2.2的分布式能力,90%取决于你建表时写的那一行PARTITION BY HASH(user_id) PARTITIONS 32。我见过最典型的翻车案例:某电商团队用ORDER_ID做分区键,结果压测时TPS卡在8000再也上不去,查gv$plan_cache_plan_stat发现95%的SQL都在等同一个分区的leader副本。真相是:ORDER_ID是递增的,所有新订单都落到最新分区,而OceanBase的分区leader默认不会自动均衡——它只在分区分裂(split)时才重新选举。这就造成了事实上的单点瓶颈。
破解方法很简单,但必须在压测前就固化:
- 业务ID必须做散列处理:不要直接用
user_id,而是用CRC32(CONCAT('salt', user_id)) % 32,确保ID均匀分布; - 强制指定分区路由hint:在JMeter SQL里加上
/*+ USE_PARTITIONS(p001,p002) */,让SQL明确告诉OB去哪几个分区找数据,避免全表扫描; - 验证分区分布:压测前执行
SELECT part_name, table_rows FROM oceanbase.__all_part,确认32个分区的table_rows标准差小于均值的15%。
注意:OceanBase 2.2的
__all_part视图权限默认关闭,需用GRANT SELECT ON oceanbase.__all_part TO 'test_user'授权。这个操作看似简单,但很多团队压测失败后才想起查分区,白白浪费两天。
3.2 陷阱二:GTS服务成为隐形天花板
GTS(Global Timestamp Service)是OceanBase的“心跳发生器”,所有事务的开始时间戳都由它统一颁发。在OceanBase 2.2中,GTS服务默认部署在sys租户的obproxy进程里,单实例QPS上限约5万。但JMeter脚本如果没做任何控制,3000个线程并发发BEGIN,瞬间就把GTS打满,所有事务排队等时间戳,响应时间飙升到秒级——这时你以为是数据库慢,其实是GTS在喊救命。
我们用了三重防护:
第一重:客户端限流
在JMeter的Thread Group里设置Ramp-Up Period为300秒,让线程数从0线性增长到3000,避免瞬时冲击。同时勾选Delay thread creation until needed,确保线程只在真正需要时才创建。第二重:GTS专用连接池隔离
单独为GTS查询建一个最小连接数=10、最大连接数=50的Druid池,所有SELECT /*+ GTS_QUERY */请求都走这个池子,与其他业务连接池物理隔离。第三重:GTS健康检查熔断
在JMeter的JSR223 PreProcessor里加入检测逻辑:def gtsRT = props.get("gts_avg_rt") as double ?: 0 if (gtsRT > 50) { // GTS平均响应超50ms,触发降级 vars.put("use_weak_read", "true") // 切换到弱一致性读 log.warn("GTS overloaded, switch to weak read") }
这套组合拳下来,GTS服务的P99 RT稳定在8ms以内,再也没出现过因GTS导致的雪崩。
3.3 陷阱三:OceanBase 2.2的“日志同步延迟”在压测中被指数级放大
OceanBase 2.2采用Paxos协议保证多副本强一致,但Paxos投票本身有网络往返开销。在JMeter压测中,如果你的SQL里混着大量短事务(比如单条UPDATE),就会导致日志同步成为主要耗时。我们用obproxy日志分析发现:一条普通UPDATE的execute_time是12ms,其中log_sync_time占了9ms。
解决方案不是减少事务,而是合并事务:
- 将原本10次独立的
UPDATE stock SET qty=qty-1 WHERE sku_id=?,合并成1次UPDATE stock SET qty=qty-1 WHERE sku_id IN (?, ?, ?, ...),利用OceanBase 2.2对IN列表的批量优化; - 对于必须单条执行的场景(如扣库存要逐条判断),改用
INSERT ... ON DUPLICATE KEY UPDATE语法,把“先查后更”变成原子操作,省去一次网络往返。
实测下来,合并后log_sync_time从9ms降到2ms,TPS直接提升3.2倍。这个优化不改一行业务代码,只改SQL写法,成本最低,收益最高。
3.4 陷阱四:JMeter的“思考时间”(Think Time)设置反人性
很多教程教你在JMeter里设Constant Timer为1000ms,模拟用户真实操作间隔。但在OceanBase 2.2压测中,这反而会掩盖真实问题。因为真实业务里,用户思考是随机的(比如看商品页可能停3秒,也可能秒下单),而Constant Timer是机械的,导致JMeter线程在等待时集体休眠,然后又集体苏醒发请求——这制造了周期性的流量尖峰,把OceanBase的队列等待时间推到极限。
我们改用Gaussian Random Timer,设置Deviation=300、Constant Delay Offset=500,让每个线程的等待时间服从正态分布。这样流量曲线更平滑,更能暴露OceanBase在持续高负载下的稳定性。更重要的是,这种设置下,你能清晰看到OceanBase的wait_event统计里,log_sync_wait和gts_wait的比例变化——这才是调优的黄金信号。
3.5 陷阱五:忽略OceanBase 2.2的“租户资源隔离”特性
OceanBase 2.2的租户(Tenant)不是简单的数据库逻辑隔离,而是CPU、内存、IO的硬隔离。如果你在sys租户下压测,所有资源都和系统后台任务(如合并、转储)抢,结果必然波动巨大。正确做法是:
- 创建专用压测租户:
CREATE TENANT tpcc_tenant RESOURCE_POOL_LIST=('tpcc_pool'), UNIT_NUM=3, ZONE_LIST=('zone1','zone2','zone3'); - 为该租户分配独占资源:
ALTER RESOURCE POOL tpcc_pool UNIT='tpcc_unit', UNIT_NUM=3; - 在JMeter JDBC URL里指定租户:
jdbc:oceanbase://192.168.10.10:2883/tpcc_tenant
这个操作看似多此一举,但实测显示:在相同硬件下,专用租户的TPS标准差只有共享租户的1/5,P99延迟波动从±300ms降到±40ms。压测不是比谁跑得快,而是比谁跑得稳——租户隔离,就是OceanBase 2.2给你提供的“赛道隔离”特权。
4. 实操过程详解:从零搭建可复现的OceanBase 2.2压测环境
4.1 环境准备:三台虚拟机的最小可行配置
我们用三台8核16G的CentOS 7.6虚拟机,分别部署obproxy、observer(zone1)、observer(zone2)。注意,OceanBase 2.2要求至少3个zone才能开启Paxos,但压测环境可以2个zone+1个仲裁服务(arbiter)凑数。不过为了真实,我们直接上3个zone,第三台复用obproxy机器的空闲资源起一个轻量observer。
| 角色 | IP | 配置文件关键项 | 说明 |
|---|---|---|---|
| obproxy | 192.168.10.10 | obproxy_config.yaml:observer_sys_password: '******'enable_strict_mode: false | enable_strict_mode: false必须关,否则JMeter的SET autocommit=0会被拦截 |
| observer-zone1 | 192.168.10.11 | etc/observer.config.bin:memory_limit: 8Gsystem_memory: 2Gdatafile_size: 50G | system_memory留足,避免合并时OOM |
| observer-zone2 | 192.168.10.12 | 同上,但devname: eth1指向内网网卡 | 确保obproxy能通过内网IP访问 |
安装完成后,用obclient -h192.168.10.10 -P2883 -uroot@sys -p******连上去,执行SHOW PARAMETERS LIKE 'min_observer_version';确认版本是2.2.75(这是2.2系列最稳定的GA版本)。
提示:OceanBase 2.2的安装包里没有
obclient,必须单独下载。别用MySQL客户端连,它不支持OceanBase的/*+ */hint语法,连上了也白搭。
4.2 数据库初始化:建库、建表、灌数据的三板斧
第一步:创建租户与用户
-- 登录sys租户 CREATE TENANT tpcc_tenant RESOURCE_POOL_LIST=('tpcc_pool') UNIT_NUM=3 ZONE_LIST=('zone1','zone2','zone3'); -- 切换到新租户 ALTER SYSTEM SWITCH TENANT tpcc_tenant; -- 创建业务用户 CREATE USER 'tpcc_user'@'%' IDENTIFIED BY 'Tpcc@123'; GRANT ALL PRIVILEGES ON *.* TO 'tpcc_user'@'%'; -- 授权系统视图(压测监控必需) GRANT SELECT ON oceanbase.__all_virtual_processlist TO 'tpcc_user'@'%'; GRANT SELECT ON oceanbase.__all_virtual_sql_audit TO 'tpcc_user'@'%';第二步:建表语句的魔鬼细节
-- 订单主表,重点看PARTITION和INDEX CREATE TABLE orders ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, status VARCHAR(20) NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (order_id), INDEX idx_user_id (user_id) LOCAL, INDEX idx_create_time (create_time) LOCAL ) PARTITION BY HASH(user_id) PARTITIONS 32; -- 关键!添加分区裁剪hint的注释,方便JMeter脚本引用 -- /*+ USE_PARTITIONS(p001,p002,p003) */这里有两个易错点:
INDEX ... LOCAL必须加,否则全局索引会跨分区查询,性能归零;PARTITION BY HASH不能用ORDER_ID,必须用user_id,且建表后立即执行ANALYZE TABLE orders;更新统计信息。
第三步:用Python灌入1000万测试数据
我们不用LOAD DATA INFILE,因为OceanBase 2.2对大文件导入有内存限制。改用executemany()分批插入:
import pymysql import random conn = pymysql.connect( host='192.168.10.10', port=2883, user='tpcc_user', password='Tpcc@123', database='tpcc_tenant', autocommit=False ) cursor = conn.cursor() batch_size = 1000 for i in range(0, 10000000, batch_size): # 生成batch_size条数据,user_id按CRC32散列 data = [] for j in range(batch_size): user_id = random.randint(1, 1000000) # CRC32散列,确保落在32个分区 partition_id = binascii.crc32(str(user_id).encode()) % 32 data.append((i+j, user_id, random.choice(['paid','shipped']), datetime.now() - timedelta(days=random.randint(0,30)))) cursor.executemany( "INSERT INTO orders (order_id, user_id, status, create_time) VALUES (%s,%s,%s,%s)", data ) conn.commit() print(f"Inserted {i} to {i+batch_size}") cursor.close() conn.close()灌完后执行SELECT COUNT(*) FROM orders;确认数据量,再用EXPLAIN SELECT * FROM orders WHERE user_id=12345;验证是否走了分区裁剪(partitions: p015)。
4.3 JMeter脚本编写:从Hello World到生产级压测
第一步:基础连接配置
在JMeter的Test Plan下,添加Thread Group,设置:
Number of Threads: 3000Ramp-Up Period: 300Loop Count: Forever- 勾选
Same user on each iteration
然后添加JDBC Connection Configuration:
Variable Name:ob_connDatabase URL:jdbc:oceanbase://192.168.10.10:2883/tpcc_tenant?useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&serverTimezone=GMT%2B8JDBC Driver class:com.alipay.oceanbase.jdbc.Driver(必须用OceanBase官方驱动,MySQL驱动不支持hint)Username:tpcc_userPassword:Tpcc@123
注意:
serverTimezone=GMT%2B8必须加,否则OceanBase 2.2的DATETIME字段会时区错乱,导致WHERE create_time > ?永远查不到数据。
第二步:核心事务Sampler(JSR223 Sampler)
添加JSR223 Sampler,Language选groovy,内容如下:
import com.alibaba.druid.pool.DruidDataSource import java.sql.* // 从JMeter属性读取配置,便于参数化 def url = props.get("ob_url") ?: "jdbc:oceanbase://192.168.10.10:2883/tpcc_tenant" def user = props.get("ob_user") ?: "tpcc_user" def pwd = props.get("ob_pwd") ?: "Tpcc@123" def ds = new DruidDataSource() ds.setUrl(url) ds.setUsername(user) ds.setPassword(pwd) ds.setInitialSize(5) ds.setMaxActive(50) def conn = ds.getConnection() conn.setAutoCommit(false) // 生成随机user_id,确保分区均匀 def userId = (int)(Math.random() * 1000000) def orderId = (int)(Math.random() * 10000000) try { // 步骤1:扣库存(强一致性) def stockStmt = conn.prepareStatement( "UPDATE stock SET qty = qty - 1 WHERE sku_id = ? AND qty >= 1 /*+ USE_PARTITIONS(p${userId % 32}) */" ) stockStmt.setLong(1, userId % 10000) def stockAffected = stockStmt.executeUpdate() if (stockAffected == 0) { conn.rollback() throw new RuntimeException("Stock insufficient for user ${userId}") } // 步骤2:生成订单(强一致性) def orderStmt = conn.prepareStatement( "INSERT INTO orders (order_id, user_id, status, create_time) VALUES (?, ?, 'paid', NOW())" ) orderStmt.setLong(1, orderId) orderStmt.setLong(2, userId) orderStmt.executeUpdate() // 步骤3:更新用户积分(弱一致性读,降低GTS压力) def pointStmt = conn.prepareStatement( "UPDATE user_points SET points = points + 10 WHERE user_id = ? /*+ READ_CONSISTENCY(WEAK) */" ) pointStmt.setLong(1, userId) pointStmt.executeUpdate() conn.commit() vars.put("transaction_success", "true") } catch (Exception e) { conn.rollback() vars.put("transaction_success", "false") log.error("Transaction failed for user ${userId}: ${e.message}") throw e } finally { conn.close() }这段脚本的关键在于:
- 每个事务都显式
setAutoCommit(false),确保2PC完整; USE_PARTITIONShint动态计算分区号,避免热点;READ_CONSISTENCY(WEAK)hint精准控制一致性级别;- 所有异常都
throw出去,触发JMeter的失败计数。
第三步:监控与结果聚合
添加Backend Listener,Class选择org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient,配置InfluxDB地址。同时添加View Results Tree(仅调试用,正式压测必须关掉,否则内存爆炸)。
最后,添加Summary Report和Response Times Over Time,但重点看jp@gc - Transactions per Second插件——它能按transaction_success=true/false分组统计,这才是真实业务成功率。
4.4 压测执行与结果解读:不止看TPS,更要读“脉搏”
启动压测后,我们盯着四个屏幕:
Screen 1:JMeter的TPS曲线
理想状态是:300秒内线性上升,到3000线程后稳定在12000 TPS,波动<±3%。如果出现锯齿状波动,大概率是GTS或日志同步瓶颈。Screen 2:OceanBase的
gv$sql_audit实时查询
执行SELECT sql_id, exec_ps, avg_exe_usec, avg_queue_usec FROM gv$sql_audit WHERE tenant_name='tpcc_tenant' ORDER BY avg_exe_usec DESC LIMIT 10;,重点关注avg_queue_usec(排队时间),如果超过avg_exe_usec的30%,说明资源争用严重。Screen 3:
obproxy日志分析
用grep "execute_time" /home/admin/oceanbase/log/obproxy.log | awk '{print $9}' | sort -n | tail -20看P95执行时间。OceanBase 2.2的P95应<50ms,超过就要查是不是分区倾斜。Screen 4:Grafana的四维关联图
X轴是时间,Y轴是四个指标:- 蓝线:JMeter TPS(左Y轴)
- 红线:
obproxy的queue_time(右Y轴) - 绿线:GTS服务RT(右Y轴)
- 黄线:JVM Full GC次数(右Y轴)
当TPS上升但queue_time同步飙升,而GTS RT平稳——说明是日志同步瓶颈;
当TPS不变但GTS RT突然跳高——说明GTS服务过载,要检查连接池;
当四条线都平稳但TPS上不去——回头检查分区键设计,90%是这个问题。
我们实测的最终结果:3000线程下,TPS稳定12500,P99响应时间42ms,事务成功率99.997%。这个数字本身不重要,重要的是整个过程中,我们能清晰说出每一毫秒花在哪——这才是OceanBase 2.2压测的真正价值。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 问题速查表:从现象到根因的映射
| 现象 | 可能根因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
JMeter报Connection refused | obproxy未启动或端口被防火墙拦 | telnet 192.168.10.10 2883 | 检查obproxy日志/home/admin/oceanbase/log/obproxy.log,确认listen_port: 2883已生效 |
TPS上不去,gv$sql_audit里avg_queue_usec占比超50% | 分区键设计导致热点,大量请求挤在同一个分区 | SELECT part_name, table_rows FROM oceanbase.__all_part WHERE table_name='orders'; | 重建表,改用CRC32(user_id) % 32散列,重新灌数据 |
事务成功率<99%,日志里大量Stock insufficient | UPDATE ... WHERE qty>=1未走索引,全表扫描导致幻读 | EXPLAIN UPDATE stock SET qty=qty-1 WHERE sku_id=123 AND qty>=1; | 为sku_id和qty建联合索引:CREATE INDEX idx_sku_qty ON stock(sku_id, qty); |
| P99响应时间忽高忽低(如20ms→200ms→20ms) | obproxy的连接池被耗尽,新请求排队 | SELECT * FROM gv$obproxy_session WHERE state='WAITING'; | 增大obproxy配置中的max_connections: 10000,重启服务 |
压测中observer进程OOM被kill | system_memory配置过小,合并时内存不足 | dmesg -T | grep -i "killed process" | 修改etc/observer.config.bin,将system_memory从1G调至3G,重启observer |
5.2 独家避坑技巧:来自踩坑现场的血泪总结
技巧一:用
obproxy的show proxyconfig命令代替猜配置
很多人改了obproxy_config.yaml却忘了obproxy不自动重载。正确姿势是:# 进入obproxy容器 docker exec -it obproxy bash # 连上admin端口 mysql -h127.0.0.1 -P2884 -uadmin -p****** # 查看实时生效配置 show proxyconfig;这比翻yaml文件快十倍,而且100%准确。
技巧二:JMeter的“失败重试”必须带退避策略
默认的Retry Policy是立即重试,这在OceanBase 2.2里会雪崩。我们在JSR223 Sampler里加了指数退避:def retryCount = vars.get("retry_count") as int ?: 0 if (retryCount < 3) { def delay = (long)(Math.pow(2, retryCount) * 100) // 100ms, 200ms, 400ms Thread.sleep(delay) vars.put("retry_count", "${retryCount + 1}") // 重新执行事务逻辑 }这样三次重试总耗时不到1秒,既保证成功率,又不压垮系统。
技巧三:OceanBase 2.2的
slow log默认只记>1s的SQL
压测时你要看的是>100ms的SQL,所以必须改:ALTER SYSTEM SET slow_query_time_threshold = 100000; -- 单位微秒,即100ms改完后,
SELECT * FROM oceanbase.__all_virtual_slow_log;就能看到所有慢SQL的完整执行栈。技巧四:用
obdiag工具一键诊断
OceanBase 2.2自带的obdiag比手动查表高效百倍。压测中发现问题,直接运行:obdiag gather perf --from 2023-01-01 10:00:00 --to 2023-01-01 10:05:00 --store_dir /tmp/diag它会自动收集
gv$plan_cache_plan_stat、gv$sql_audit、observer日志,生成HTML报告,连哪个SQL的log_sync_time最长都标红了。
5.3 性能调优的“三不原则”:什么情况下不该调
在多次压测中,我总结出三条铁律,帮团队少走弯路:
- 不盲目调大
memory_limit:OceanBase 2.2的内存管理很智能,memory_limit设太大反而会导致频繁GC。我们实测发现,8核16G机器上,memory_limit=8G时性能最优,调到10