1. 项目概述为什么要在JMeter中连接ClickHouse做性能测试的朋友尤其是和数据打交道的估计都遇到过这样的场景业务系统重度依赖ClickHouse这类列式数据库做实时分析你需要压测的不仅仅是前端的API更是后端数据查询和写入的吞吐量。这时候如果还只是用JMeter测测HTTP接口总觉得隔靴搔痒没打到真正的“七寸”。直接在JMeter里配置并操作ClickHouse就成了一个非常实际且硬核的需求。简单来说这个操作的核心价值在于将性能测试的触角直接延伸到数据库层。你可以模拟真实业务中成百上千个并发用户同时向ClickHouse发起复杂查询、批量插入或更新操作从而精准评估数据库服务的极限处理能力在特定硬件和配置下ClickHouse的QPS每秒查询数和写入吞吐量瓶颈在哪里。SQL查询语句的性能你的那条核心分析SQL在并发压力下响应时间是否线性增长是否存在潜在的死锁或资源争用网络与连接池的稳定性大量的JDBC连接建立、销毁对应用服务器和数据库服务器的影响。验证架构合理性比如面对突然的流量高峰当前的数据表引擎如MergeTree、索引设计、物化视图是否能扛得住。这不仅仅是“连接上一个数据库”那么简单它关乎如何在一个压力测试工具中真实地模拟出对分析型数据库的混合读写负载从而为容量规划、SQL优化和架构调整提供第一手的压测数据。接下来我会拆解从驱动准备到脚本编写的完整流程并分享几个我趟过坑才总结出来的核心技巧。2. 核心组件准备与环境配置在开始编写JMeter脚本之前我们需要把“武器”准备好。这里的关键是让JMeter能够通过标准的Java数据库连接JDBC协议与ClickHouse对话。2.1 JDBC驱动选择与放置ClickHouse官方提供了两种主流的JDBC驱动选择哪一种会直接影响后续的配置和功能。官方驱动ClickHouse-JDBC 这是最直接的选择。你需要到Maven中央仓库或ClickHouse的GitHub Release页面下载最新的JAR文件例如clickhouse-jdbc-0.4.6-all.jar。注意务必下载带有-all后缀的版本这个“胖JAR”包含了驱动运行所需的所有依赖如HTTP客户端、JSON解析器等。如果下载了普通版本你可能会陷入无尽的“ClassNotFound”异常地狱。第三方驱动如Housepower/ClickHouse-Native-JDBC 这个驱动声称通过原生TCP协议而非HTTP与ClickHouse通信理论上能获得更低的延迟和更高的吞吐特别适合高性能压测场景。但它的稳定性和兼容性需要你在具体环境中验证。对于生产级压测我建议先从官方驱动开始它经过了更广泛的测试。驱动放置位置 将下载好的JAR文件复制到JMeter安装目录的lib/ext文件夹下。这是JMeter加载第三方库的标准路径。放置完成后必须重启JMeter否则驱动不会被加载。注意永远不要将驱动JAR包放在lib目录下这可能导致冲突。lib/ext是唯一正确的选择。2.2 创建JDBC连接配置驱动就位后我们开始在JMeter中建立数据库连接池。添加线程组首先新建一个Thread Group这里将定义并发用户数、循环次数等压力模型。添加配置元件在线程组上右键依次选择Add-Config Element-JDBC Connection Configuration。这个元件是连接池的核心。关键参数配置在JDBC Connection Configuration的控制面板中需要填写以下关键信息Variable Name: 连接池变量名例如ClickHouse_Connection。后续的JDBC请求会通过这个名字引用这个连接池。这个名称必须全局唯一。Database URL: JDBC连接字符串。格式通常为jdbc:clickhouse://host:port/database例如jdbc:clickhouse://192.168.1.100:8123/analytics。默认端口是8123HTTP协议。如果需要使用原生TCP端口如9000驱动和URL格式会有所不同这通常对应第三方驱动。JDBC Driver class: 驱动类名。对于官方驱动填写com.clickhouse.jdbc.ClickHouseDriver。Username/Password: 你的ClickHouse用户名和密码。连接池参数调优性能关键 这部分配置直接影响压测时连接管理的效率不当的设置可能导致连接泄露或成为性能瓶颈。Max Number of Connections: 连接池中允许的最大连接数。这应该与你线程组中的“线程数”并发用户数相匹配或略高。例如100个并发线程可以设置最大连接数为120。Connection Timeout建立连接的超时时间秒。在压测中如果数据库繁忙可以适当调高例如设置为10秒。Transaction Isolation事务隔离级别。ClickHouse对事务的支持有限主要针对MergeTree引擎的读写通常保持默认TRANSACTION_NONE即可。Test While Idle和Validation Query建议勾选Test While Idle并设置一个简单的验证查询如SELECT 1。这可以让连接池定期检查空闲连接是否有效避免使用已断开的连接导致测试失败。配置完成后这个元件会为每个线程虚拟用户按需从池中获取连接并在请求结束后归还而不是为每个请求新建连接这极大地提升了效率。3. 编写与调试JDBC请求采样器连接池配置好后我们就可以发送SQL命令了。这是通过JDBC Request采样器实现的。3.1 构建基础的JDBC请求添加采样器在线程组下右键选择Add-Sampler-JDBC Request。绑定连接池在JDBC Request的控制面板中Variable Name字段必须填入之前在JDBC Connection Configuration中定义的名称例如ClickHouse_Connection。这样这个请求就知道使用哪个连接池。选择请求类型Select Statement用于执行查询语句如SELECT。查询结果可以被保存到JMeter变量中供后续断言或其它请求使用。Update Statement用于执行更新语句如INSERT,ALTER,OPTIMIZE等。它返回的是受影响的行数。Callable Statement用于调用存储过程ClickHouse中较少使用。Prepared Select/Update Statement强烈推荐使用。这是“预处理语句”SQL语句中的参数用?占位。它的好处是a) 数据库只需编译一次SQL后续执行效率高b) 天然防止SQL注入c) 方便使用JMeter变量进行参数化。编写SQL语句如果是Prepared类型在SQL Query区域写入带?的语句例如SELECT user_id, sum(amount) FROM orders WHERE date ? AND region ? GROUP BY user_id在Parameter values中按顺序填入对应?的值如${__time(yyyy-MM-dd)}当前日期和East。也可以使用Parameter types指定类型如Date,String。如果是普通Select或Update直接写入完整SQL。3.2 处理查询结果与变量提取执行SELECT语句后你可能需要将结果用于断言或作为下游请求的输入。结果变量命名在JDBC Request的底部有Variable names和Result variable name两个字段。Variable names为查询结果的每一列指定一个变量名前缀。例如如果查询返回两列user_id,total_amount你可以填写userId,amount。那么第一行数据会产生变量userId_1,amount_1第二行是userId_2,amount_2以此类推。userId和amount则保存第一行的值。Result variable name将整个结果集一个ArrayList对象保存到一个变量中供后续的JSR223 PostProcessor等高级元件进行复杂处理。使用断言验证结果添加Response Assertion或JSR223 Assertion。例如你可以断言amount_1这个变量值大于0或者通过JSR223用Groovy脚本遍历Result variable name保存的结果集检查数据条数或业务逻辑的正确性。3.3 参数化与数据动态化真实的压测需要多样化的数据避免因缓存导致失真的高性能假象。CSV数据文件最常用的方法。使用CSV Data Set Config元件读取一个包含测试数据的文件如用户ID、商品SKU、时间戳等。在SQL的Parameter values中引用CSV文件中的列变量如${userId}。JMeter内置函数__time函数生成时间戳__Random函数生成随机数__counter函数生成自增数字。这些非常适合生成动态的查询条件或插入数据。JSR223 PreProcessor对于更复杂的参数生成逻辑如根据特定规则生成JSON字符串插入到ClickHouse的JSON类型字段中可以使用Groovy或Java代码在请求前动态生成SQL参数值。一个插入数据的参数化示例 假设你有一个CSV Data Set Config配置了变量event_id,event_time。你的Prepared Update Statement可以这样写INSERT INTO user_events (id, event_type, event_time, properties) VALUES (?, click, ?, ?)Parameter values设置为${event_id}, ${event_time}, ${__StringFromFile(/path/to/json_templates.txt)}这里ID和时间来自CSV而properties字段的内容从一个文本文件中循环读取不同的JSON模板。4. 设计有效的性能测试场景仅仅能发送SQL请求还不够如何设计场景才能真实模拟生产负载这是体现经验的地方。4.1 混合读写比例建模纯读或纯写的测试意义有限。你需要根据业务特点设计读写混合的场景。分析型场景可能是95%的复杂聚合查询SELECT ... GROUP BY ... WITH TOTALS加上5%的批量数据插入用于模拟T1的数据导入。可以使用Throughput Controller或Switch Controller来控制不同请求类型的执行比例。事件流场景可能是持续的流式插入如INSERT INTO table FORMAT JSONEachRow为主夹杂少量对最新分区的实时查询。实现方法创建多个JDBC Request采样器分别代表读和写操作。然后使用Random Controller或Throughput Controller来按权重随机执行它们。例如在Random Controller下放两个请求通过调整其权重来模拟8:2的读写比。4.2 控制查询的“热度”与数据分布如果所有并发线程都查询完全相同的数据那么ClickHouse的缓存会发挥巨大作用结果可能过于乐观。数据倾斜使用__Random函数或CSV文件让查询条件如user_id、date在一定范围内随机分布模拟真实用户访问不同数据分区的情况。热点数据可以设计一小部分数据例如10%被更高频率地访问。这可以通过逻辑判断实现if (__Random(1,100) 10) { vars.put(hot_key, fixed_hot_value); } else { vars.put(hot_key, random_value); }然后在SQL中使用${hot_key}。4.3 模拟峰值与疲劳测试阶梯加压使用Concurrency Thread Group或Ultimate Thread Group插件模拟用户数随时间逐步上升如每2分钟增加50个用户直到系统出现瓶颈或响应时间超标。这有助于找到“拐点”。稳定性测试设置一个较高的并发用户数持续运行数小时甚至更长时间。观察ClickHouse的内存使用如Merge内存、磁盘IO、ZooKeeper连接如果使用了复制表是否稳定有无内存泄漏或性能缓慢下降的趋势。这时需要配合服务器监控如clickhouse-systemd、Prometheus一起看。5. 监控、断言与结果分析压测过程中除了看JMeter的聚合报告更需要关注数据库本身的健康度。5.1 关键监控指标在压测执行时你应该同时监控ClickHouse服务器系统层面CPU使用率、内存使用量重点观察clickhouse-server进程、磁盘IOPS和吞吐量、网络流量。ClickHouse层面查询相关Query、SelectQuery、InsertQuery的每秒计数QueryTime、SelectQueryTime等耗时指标。资源相关MemoryTracking下的各项内存使用量IO相关的读写字节数。连接数TCPConnection和HTTPConnection的数量确保没有异常增长。表引擎相关对于MergeTree关注Merge、PartMutation等后台操作的频率和耗时过多的Merge会在高写入压力下消耗大量资源。你可以通过查询system.metrics、system.events、system.processes等系统表来获取这些信息或者集成到Grafana中可视化。5.2 设计有意义的断言JMeter断言不应只是检查请求是否成功返回200而应关注业务正确性和性能SLA。响应时间断言使用Response Assertion添加“响应时间”的断言例如检查95%的查询响应时间是否小于200毫秒。业务逻辑断言使用JSR223 Assertion。例如对于一个聚合查询你可以用Groovy脚本解析返回的结果集断言总行数在预期范围内或者某个汇总值符合业务逻辑。采样结果断言对于插入操作可以断言UPDATE_COUNT这个JMeter变量保存受影响行数是否大于0。5.3 结果分析与瓶颈定位压测结束后结合JMeter结果和ClickHouse监控进行分析JMeter聚合报告重点关注Throughput吞吐量请求数/秒、Average/95% Line/99% Line响应时间、Error %。如果错误率上升查看具体是什么错误超时、连接拒绝、语法错误。交叉对比当并发数增加时吞吐量是否线性增长响应时间曲线是平缓上升还是陡然上升找到性能拐点。瓶颈定位如果CPU持续在100%可能是查询计算过于复杂或并发太高需要考虑优化SQL或升级CPU。如果内存使用率很高且频繁发生Memory limit exceeded错误需要检查查询的内存设置max_memory_usage或者是否涉及了大表的JOIN。如果磁盘IO持续饱和可能是数据写入量太大或Merge操作过于频繁需要考虑使用更高性能的磁盘如SSD或调整MergeTree的合并策略merge_with_ttl_timeout。如果网络带宽成为瓶颈在云环境下常见考虑压缩传输的数据ClickHouse JDBC驱动支持压缩或者将压测客户端部署到离数据库更近的网络区域。6. 高级技巧与避坑指南这里分享一些从实际压测项目中总结出来的经验这些在官方文档里不一定找得到。6.1 处理批量插入以最大化吞吐量单条INSERT语句的性能极差。对于压测必须使用批量插入。方法一在一条INSERT语句中包含多行数据。INSERT INTO test_table (id, name) VALUES (1, a), (2, b), (3, c), ...在JMeter中你可以用JSR223 PreProcessor动态生成这样一个包含几百甚至上千行值的SQL字符串。但要注意SQL语句的长度是有限制的。方法二使用PreparedStatement配合addBatch()和executeBatch()。这需要在JSR223 Sampler中编写Java/Groovy代码来手动操作JDBC连接灵活性最高性能也最好。你可以累积一定数量的参数组后一次性提交。import java.sql.PreparedStatement def conn vars.getObject(ClickHouse_Connection_pool).getConnection() // 从池中获取连接 def ps conn.prepareStatement(INSERT INTO test_table (id, name) VALUES (?, ?)) 100.times { i - ps.setInt(1, i) ps.setString(2, name_${i}) ps.addBatch() } int[] results ps.executeBatch() conn.close()注意此方法需要自行管理连接务必在finally块中或使用try-with-resources确保连接归还给池或关闭否则会导致连接泄露。6.2 应对连接超时与空闲断开在长时间稳定性测试中可能会遇到数据库主动断开空闲连接的情况。现象压测运行一段时间后开始出现Connection is closed或Socket timeout错误。解决方案确保JDBC Connection Configuration中启用了Test While Idle和Validation Query。调整ClickHouse服务端的配置config.xml增加keep_alive_timeout和tcp_keep_alive_timeout的值。在JMeter脚本中可以定期例如每30分钟通过一个仅自己执行的线程组发送一个简单的SELECT 1查询来保持连接的活跃。或者使用JSR223 Timer在请求间执行保活逻辑。6.3 管理测试数据与清理压测会产生大量临时数据需要妥善管理。数据生成使用专门的脚本或工具如clickhouse-benchmark配合INSERT SELECT从生成器表生成预先灌入基础数据。JMeter脚本应主要操作这些数据范围内的内容。数据隔离为每次压测创建单独的数据表或数据库例如stress_test_20240517。这样便于清理也避免影响线上数据。自动化清理在测试计划的tearDown Thread Group中添加JDBC Request来执行DROP TABLE或TRUNCATE TABLE操作。tearDown Thread Group会在所有普通线程组执行完毕后运行无论测试成功与否。6.4 调试与日志排查当脚本不按预期工作时需要逐层排查。查看JMeter日志首先打开JMeter的日志查看器Options-Log Viewer将日志级别调整为DEBUG。这里会显示详细的JDBC驱动交互信息包括发送的SQL和接收的响应。检查ClickHouse查询日志登录ClickHouse服务器查看/var/log/clickhouse-server/clickhouse-server.log路径可能不同。这里记录了所有接收到的查询、执行耗时和可能的错误。通过grep过滤你的测试IP或用户名可以精确定位。简化与验证先写一个最简单的SELECT 1请求确保基础连接是通的。然后逐步增加SQL复杂度每次只改变一个变量定位问题点。使用“查看结果树”在调试阶段务必添加View Results Tree监听器。它可以让你看到每个请求的详细请求数据、响应数据和JMeter变量。调试完成后切记在正式压测前禁用或删除它因为它会消耗大量内存并严重影响JMeter性能。