1. 项目概述:千万级QPS对Doris意味着什么
做数据服务这几年,Doris在我这边的出现频率越来越高。尤其是当业务方拿着“实时大屏要扛住千万级QPS”“用户画像查询不能超过50毫秒”这类需求过来的时候,传统MySQL扛不住、ES又觉得太重,最后绕一圈基本都会回到Doris上。这个项目标题里提到的“千万级QPS”,听着吓人,但拆开看其实是有章可循的:Doris的MPP架构、列式存储、向量化执行引擎,加上合理的分区分桶和缓存设计,确实能把高并发查询压到一个很可观的量级。
这篇文章我想从实际搭建和调优的角度,把Doris在高并发查询优化这件事从头到尾捋一遍。不会只讲理论,而是会结合我在CentOS上部署3节点Doris、用Doris替换ES、做MySQL实时同步、踩OOM和各种连不上9030的坑,把这些真实经验整理成一份能直接拿去用的优化清单。适合正在做Doris选型评估、已经部署了Doris但查询性能不达标、或者准备把部分在线查询从ES/MySQL迁移到Doris的同学。
先给一个整体判断:Doris从来不是“部署完就快”的数据库,它的高并发能力是一层层优化出来的。数据模型怎么设计、分区分桶怎么切、SQL怎么写、缓存开多大、FE和BE的内存怎么分配,任何一层出了问题,最后的QPS都会大打折扣。所以这篇文章的核心思路,就是把每一层都拆开看,讲清楚每个参数和每个设计选择背后的原因。
2. 架构规划:从3节点CentOS部署到FE/BE协同
2.1 节点规划与组件分工
先聊部署。Doris的架构里两个核心角色,FE和BE。FE是前端节点,负责SQL解析、查询计划生成、元数据管理和客户端连接;BE是后端节点,负责数据存储、计算执行和副本管理。你可以把FE理解成“前台接待”,把BE理解成“后厨”。前台要能快速响应客人需求,后厨要能稳定出菜,两者缺一不可。
我当时在CentOS下部署3节点Doris,采用的是1个FE(独立节点)+ 3个BE的经典组合。但要注意,生产环境里FE至少要部署2个,用Follower角色做高可用,因为FE单点挂了整个集群就不可用了。3节点如果资源紧张,可以把FE和BE混部,但混部时一定要控制内存分配,避免FE的JVM和BE的brpc互相抢内存,后面讲OOM的时候会细说。
磁盘规划上,BE的存储目录建议独立挂载,不要和系统盘共用。因为Doris的BE默认需要三副本,数据量上来以后磁盘IO很容易成为瓶颈。高并发查询场景下,随机读IOPS比顺序读写更重要,有条件的话BE存储用SSD,顺序扫描和谓词下推的性能会有明显改善。
2.2 FE的关键配置与images文件夹
部署FE的时候有个很容易被忽略的目录,就是fe/conf目录下的images文件夹。很多人第一次看到这个目录会懵,不知道它是干嘛的。实际上images是Doris FE的元数据镜像目录,FE会定期把内存中的元数据快照落盘到这个目录里,配合editlog日志文件一起保证元数据的一致性。
我在实际运维中遇到过一种情况:集群运行很久之后,images目录下的元数据镜像越来越大,FE启动变慢,甚至出现启动超时。原因就是FE默认保留的元数据镜像数量太多。image目录下的文件可以适当清理吗?可以,但必须小心。Doris FE启动时会先从最新的image加载元数据,再回放editlog,如果你把最新的image删了,FE可能无法正常恢复。稳妥的做法是参考fe.conf里的meta_dir配置,把整个元数据目录定期备份,而不是手动乱删images下的文件。
FE的JVM配置也是高并发场景的关键。fe.conf里默认的Xmx和Xms一般偏保守,如果查询并发高、查询计划复杂,FE的GC会成为瓶颈。我的建议是Xmx和Xms设成一样的值,避免堆动态伸缩带来的性能抖动,同时适当调大Young Generation。常用的一个起点是8G到16G,具体要看FE节点物理内存和QPS预期。但别贪心,FE堆内存设得过大,GC暂停时间反而会变长。
2.3 BE的内存管理与OOM预防
接下来说BE。BE是真正干活的地方,也是OOM的重灾区。Doris BE的内存管理比较复杂,它会把MemTable、BlockCache、PageCache、查询中间结果都算进内存。特别是高并发查询场景,如果有很多大查询同时跑,BE内存会瞬间飙高。
防止BE OOM,我建议从三个层面同时下手。第一是操作系统层面,BE所在的CentOS节点要关闭swap,或者把vm.swappiness调到接近0,避免Doris的内存被换到磁盘上,导致性能骤降。第二是BE配置层面,通过修改be.conf里的mem_limit参数,限制BE进程可用的最大内存比例,给操作系统和其他进程留出余地。第三是查询层面,控制单查询的内存上限和并发查询数量,这个在后面的查询优化部分会详细讲。
另外一个很实际的经验:BE的存储目录如果是多块盘,Doris支持通过storage_root_path配置多个目录,用分号隔开。多目录可以分散IO压力,但要注意每块盘的容量不均时,Doris的磁盘均衡策略可能不会立刻把数据均衡过去。我踩过的坑是业务增长后某块盘快满了,另几块还有很多空间,需要手动触发一下磁盘迁移,让数据分布更均匀。
3. 数据模型设计:支撑高并发查询的地基
3.1 分区分桶与排序键的选择
Doris高并发优化的第一步,不在SQL里,而在建表语句里。分区分桶设计好不好,直接决定了查询能不能“裁剪”掉大量无关数据。我见过很多性能差的Doris查询,本质都是全表扫描,那就是因为表设计时没有把查询条件落到分区和分桶键上。
分区的作用是按时间或其他维度把数据拆成多个物理区间,查询时如果能命中分区裁剪,Doris就只扫对应分区的数据,IO开销瞬间降下来。我常用的设计是把业务日期作为分区字段,按天或按月分区,具体看数据量和查询跨度。分桶则是把分区内的数据再按哈希拆成多个桶,分桶键的选择很关键,它决定了数据在BE节点间的分布,以及查询时的本地化程度。
分桶键的选法有两条经验。第一,分桶键要尽量选择查询中高频出现的等值条件字段,这样Doris能通过分桶裁剪直接定位到少量桶,而不是全桶扫描。第二,分桶键要和排序键配合考虑。Doris的每个分桶内部,数据是按照排序键有序排列的,如果你在排序键上做范围查询,性能会非常好。我见过一个用户表,把uid作为分桶键和排序键,查询条件也几乎都是uid等值过滤,QPS压测结果非常理想,单表几亿数据,单查询延迟稳定在个位数毫秒。
3.2 用Rollup和物化视图武装查询
Doris里有Rollup和物化视图这两个非常实用的加速手段。Rollup可以理解为表的分层汇总,它把原始数据按指定的维度预先聚合,查询时可以自动路由到最合适的Rollup上,避免每次都对明细数据做实时聚合。物料视图则更通用,可以针对特定查询模式预先计算,查询时透明命中。
在高并发场景下,Rollup的价值尤其大。举个例子,原始表有几十个字段,但业务上最频繁的查询是按某几个维度统计某个指标的和,这时候建一个只包含这几个维度和指标的Rollup,数据量可能只有原表的十分之一甚至更少。查询命中Rollup后,扫描的数据量小,CPU开销也低,QPS自然就上去了。
但Rollup不是越多越好。每建一个Rollup,就意味着导入数据时要多维护一份副本,占用额外的存储和写入开销。我的经验是先统计线上真实查询的TopN模式,针对热点查询建Rollup,而不是凭感觉建。Doris的query profile可以记录每个查询命中了哪张表、扫描了多少行,利用它来辅助Rollup设计会比较靠谱。
物化视图在Doris里的用法类似,但更适合复杂的聚合表达式或Join场景。需要注意,物化视图和Rollup的维护也是异步的,导入数据后需要一定的延迟才能看到最新结果,所以不适合要求强一致的实时查询。
3.3 Doris替换Elasticsearch的实践思路
Doris替换ES是最近讨论度很高的场景。很多团队用ES做日志分析、订单查询和用户画像,但ES在数据量上来后的集群运维成本和内存压力都不小,而且精准的聚合分析和SQL支持相对弱。Doris在OLAP分析场景有明显优势,所以不少团队开始把一部分ES场景迁到Doris。
替换ES的关键是理解两者的差异。ES擅长全文检索,Doris擅长结构化数据的OLAP查询。如果业务里大量依赖分词搜索、模糊匹配,Doris的like查询虽然也能用,但性能上和ES的倒排索引没法比。所以我的判断是:Doris适合替换ES里那种“结构化字段过滤+聚合统计”的查询,而不适合替换全文检索为主的场景。
在实践中,迁移ES到Doris可以做三层工作。第一层,把ES中的索引映射成Doris表结构,把常用的keyword字段设计成分桶键或排序键。第二层,把原本通过ES进行时间范围+聚合的查询改写成Doris的SQL,利用分区裁剪和Rollup加速。第三层,在数据同步上,如果ES数据本身来自业务库或消息队列,可以直接调整链路,让数据先入Doris再视需要写入ES,或者直接用Doris承担在线查询。我做过一个真实案例,把原本ES集群上的一类画像查询迁到Doris后,查询延迟从几百毫秒降到几十毫秒,集群内存压力也明显缓解。
4. 查询优化:从SQL到缓存的层层加速
4.1 SQL写法中的“隐形杀手”
到了SQL优化这一步,很多人的第一反应是加缓存,但SQL本身的写法才是决定查询能不能走对执行计划的关键。Doris的查询优化器虽然越来越智能,但一些“隐形杀手”仍然会导致性能坍塌。
第一个隐形杀手是函数包裹列。比如where条件里写成where date_format(create_time, '%Y-%m-%d') = '2025-01-01',这会导致Doris无法使用分区裁剪,因为它必须对每一行做函数计算才能判断。正确的写法是where create_time >= '2025-01-01 00:00:00' and create_time < '2025-01-02 00:00:00',这样就能直接命中分区。
第二个是隐式类型转换。如果表里是字符串字段,但查询传了数字类型,或者反过来,Doris在比较时可能要做类型转换,导致索引失效。建表时字段类型定义要严格,应用端传参时也要保持一致,这是我在排查慢查询时最常发现的问题。
第三个是大范围in查询。高并发接口里如果in条件里有几百上千个值,Doris需要处理大量的等值判断,性能开销很大。遇到这种情况,要评估是否可以改成Join,或者把数据从业务侧分批传入。如果一定要用in,也尽量保证in的列表是分桶键,这样Doris能直接定位到对应的桶,而不是扫全表。
4.2 查询缓存与分区缓存的合理使用
Doris的查询缓存是提升高并发QPS最快的手段之一。它的原理是,如果多个查询的SQL完全一致,并且命中的分区数据没有更新,Doris可以直接返回缓存结果,跳过计算过程。这个特性在处理大量重复查询的场景下效果极其明显。
Doris的缓存分两层,一层是SQL Cache,一层是Partition Cache。SQL Cache要求整个查询的SQL文本完全一致,适合报表类的固定查询。Partition Cache则更灵活,它按分区粒度缓存结果,如果查询涉及的分区没有被导入任务更新,就会用缓存。我先说结论:如果你的查询条件里有时间范围,Partition Cache比SQL Cache好用得多,因为SQL里日期值稍微一变,SQL Cache就失效了,但Partition Cache还是能命中。
不过缓存不是万能的。开启缓存前要确认业务上对数据实时性的容忍度。如果查询的结果是从MySQL或消息队列实时同步过来的,每次导入都会使对应分区缓存失效,那么后续第一次查询还是要走全计算流程。所以在高并发场景下,我会把缓存和实时同步的时效性要求做一个匹配,不要盲目开启。
另外要提一个容易踩的坑:Doris的缓存默认是关闭的,需要在fe.conf里设置enable_sql_cache和enable_partition_cache为true,同时配置cache_result_max_row_count等参数。这些配置改完后要重启FE才能生效,而且不同版本的Doris配置项名称可能有差异,升级时要留意。
4.3 连接池与会话并发参数的调优
高并发查询除了要优化单条SQL,还要把连接管理和并发参数调到合理范围。Doris的FE默认有一个最大连接数max_connection_number,如果客户端连接池配置得太激进,连接数超过限制,就会出现连不上的报错。
我遇到过最典型的情况是应用端用HikariCP连接Doris,连接池的最大连接数设了500,但Doris FE的max_connection_number默认才1024,加上其他服务和运维连接,一旦连接池空闲连接没有及时释放,很快就撑爆了。排查后发现应用的连接池keepalive配置不当,导致连接长时间不被回收。解决方法是应用连接池的maximumPoolSize控制在200以内,同时配置connectionTimeout和idleTimeout,尽量保持连接稳定。
另一个影响高并发的是Doris的查询并发度配置。BE端的并行度由parallel_pipeline_task_num控制,它决定了每个查询在BE内部能拆分成多少个并行任务。这个值不是越大越好,因为每个任务都会占用线程和内存,并发高的时候反而会因为线程切换和内存争用导致性能下降。我的经验是单查询并行度设置为BE节点CPU核心数的一半左右,然后在压测中逐步调整,观察QPS和延迟曲线的拐点。
5. 数据同步链路:MySQL实时同步与Oracle批量接入
5.1 MySQL实时同步到Doris的常用方案
Doris通常是做分析查询,数据往往要实时或准实时地从业务库同步过来。MySQL实时同步到Doris的玩法,我这边用过两种主流方案:一种是基于Doris的Canal适配器,通过Canal监听MySQL的binlog,再把解析后的数据写入Doris;另一种是通过Flink CDC,把MySQL的变更流直接写入Doris。
Canal的方式架构简单,适合数据量中等、同步延迟要求秒级的场景。部署时需要把Canal Server和Doris的Canal适配器一起跑起来,适配器负责把Canal的protobuf消息转换为Doris的Stream Load请求。这里有个容易忽视的点:MySQL表的DDL变更不会自动同步到Doris,比如加列、改类型,需要你手动维护Doris侧的表结构,否则同步任务会报字段不匹配的错。
Flink CDC的方案更灵活,尤其是全量加增量同步的场景。Flink CDC能把MySQL全量历史数据先打出来,再无缝切换到增量binlog,整个同步过程对业务无感。Doris提供了官方的Flink Doris Connector,支持Stream Load方式写入,写入吞吐高,而且能保证顺序性和一致性。我个人的经验是,实时性要求高、数据流后续还要做清洗或Join的话,优先选Flink CDC;纯同步不想引入太多组件,Canal更轻。
5.2 Oracle通过DataX同步到Doris
除了MySQL,存量系统里Oracle也不少。Oracle数据同步到Doris,我之前用的是DataX。DataX是阿里开源的数据同步工具,通过插件机制支持各种数据源。Oracle Reader读取Oracle表,Doris Writer通过Stream Load把数据写入Doris,整个过程可以做成离线批同步。
DataX同步Oracle到Doris有几个要注意的点。第一,Oracle的字段类型和Doris不是完全对等,比如Oracle的NUMBER类型在Doris里要定义成DECIMAL或BIGINT,需要先做好字段映射。第二,DataX Reader的where条件尽量按主键或时间字段做切分,保证多个channel能并行读取,不会造成单库压力过大。第三,Doris Writer的批量提交大小batchSize和channel数要配合调整,我一般先设channel为4到8,观察Doris BE的CPU和内存水位后再调整。
还有一个我自己踩过的坑:DataX同步大批量数据时,如果Doris表的副本数设置为3,同时写入量很大,BE的磁盘IO可能会成为瓶颈,导致Stream Load超时。这时候要调大DataX Writer侧的写出超时时间,同时看BE的streaming_load_max_mb和write_buffer_size等参数是否够用。更稳妥的做法是错峰同步,把大批量任务安排在业务低峰期。
5.3 同步链路的监控与防延迟
数据同步链路如果出现问题,最直接的后果就是Doris里的数据不是最新的,高并发查询即使跑得再快,结果也是错的。所以同步链路的监控和延迟治理,是高并发查询优化里必不可少的一环。
我的监控经验是至少盯四个指标:同步延迟时间、同步任务成功率、Doris导入的任务队列长度、以及BE的磁盘使用率。同步延迟可以通过比对源库和目标库的某个时间字段或自增ID来估算,也可以在同步链路里打点记录数据时间戳和入库时间戳的差值。
防延迟的思路要从源头控制。实时同步链路里,如果MySQL的binlog产生速度超过Doris导入吞吐,延迟就会越积越大。这时候要么扩充Doris的导入并发,要么在源头做过滤,比如只同步需要的字段和表,减少无效写入。批量同步链路里,DataX任务要设置合理的channel和batchSize,同时要留意任务有没有互相争抢资源。我遇到过DataX多个任务同时跑,把BE的导入内存占满,导致其他查询延迟明显上升的情况,后来加了并发限制和排队机制才稳定下来。
6. 常见问题排查与避坑记录
6.1 连不上9030端口怎么办
一开始部署Doris,很多人卡在客户端连接这步,报错信息通常是“can't connect to MySQL server on '127.0.0.1:9030'”。这里要提醒,Doris的FE对外服务端口默认是8030(HTTP)和9030(MySQL协议),但不同版本和默认配置有差异,如果你用的是mysql命令行连接,要确认连的是9030。
排查这类问题,我按这个顺序来。第一,检查FE进程是否真的存活,用ps或jps看有没有DorisFE进程。第二,检查FE的query_port配置,在fe.conf里确认是否被改过。第三,检查防火墙和网络策略,9030端口是否对客户端IP开放,我遇到过CentOS防火墙开着但没放行端口,导致从外部永远连不上。第四,检查客户端mysql版本,Doris兼容MySQL协议,但过旧或过新的mysql客户端可能出现握手失败,这时换一个常用的8.x客户端试试。
另一个容易被忽略的点是:如果你部署了多个FE,通过MySQL协议连接时最好连接Follower或Leader的地址,或者通过负载均衡器做VIP。有的团队直接连一个挂掉的FE地址,自然就报连不上,切换到存活节点就好了。
6.2 Doris OOM的几种典型现场
Doris OOM是运维里最头疼的问题之一,而且现场五花八门。我见过BE进程直接崩溃、BE启动失败、查询报memory limit exceeded,还有操作系统OOM Killer把BE进程杀掉的情况。每种场景的排查思路不一样。
如果是查询报memory limit exceeded,说明单个查询的内存超过了BE的内存限制,需要调大query mem limit,或者优化SQL减少中间结果集。如果是BE进程被OOM Killer杀掉,去看/var/log/messages里有没有对应的记录,同时检查be.conf的mem_limit是不是设置得过高,给系统预留内存不够。
还有一种现场是导入时OOM。大批量Stream Load导入时,Doris会把数据写入MemTable,如果MemTable还没来得及刷盘,导入并发又高,内存就会爆。这种场景要调节mem_table_writer_max_rows和write_buffer_size,让MemTable更快地刷到磁盘,或者降低导入的并发数。我自己的经验是,Doris的OOM问题大多数不是单一参数造成的,而是BE内存上限、导入并发、查询并发、操作系统内存四者的平衡没做好。
6.3 Kettle 8.3 Doris插件使用注意事项
Kettle作为ETL工具,社区版默认没有Doris插件,需要自己编译或下载第三方插件。Kettle 8.3对应的是较老的版本,如果你要在这里面用Doris插件,得确认插件和Doris客户端的版本兼容性。
我实际用下来的坑主要有这几个。第一,Kettle的Doris插件底层走的是Stream Load或JDBC,如果你在Kettle里配置了Doris连接,却报驱动类找不到,多半是缺少Doris的JDBC驱动jar包,要把驱动放到Kettle的lib目录下。第二,Kettle里的字段类型映射要手动调,比如DATE类型在Doris里如果定义成了DATETIME,Kettle写入时可能会出现格式解析问题,最简单的办法是统一用字符串或时间戳字段过渡。第三,Kettle任务跑批时如果Doris端有大批量查询在跑,Kettle的写入延迟会变大,尽量不要让ETL任务和高并发查询高峰重叠。
6.4 权限账户问题:vc db中的doris账户
最后说一个权限相关的小问题。很多时候业务方给过来一个“vc db”中的doris账户,本以为建一个用户就能查,结果发现表查不了。Doris的权限模型控制得很细,包括库级、表级、行级,甚至列级权限。如果你用doris账户登录后发现只能看到部分库表,或者查询报权限不足,说明该账户的授权只覆盖了部分资源。
我的建议是,在业务上线前就把账户权限申请清楚。Doris的授权SQL是GRANT SELECT ON db.table TO user,如果需要读写和导入权限,要单独授权LOAD和CREATE。如果业务方提供的doris账户是统一入口,尽量不要让所有应用共用同一个账户,否则后面做慢查询分析和权限审计会非常痛苦。我在实际项目中就遇到过,两个应用共用一个doris账户,其中一个应用误操作把表删了,另一个应用直接受牵连,后来彻底改成每个应用独立账户才消停。
高并发优化的路上,踩坑是难免的,但每个坑都会让你对Doris的认知更深一层。我现在做新项目时,第一步一定是先对着架构图和查询模式把表结构和同步链路定下来,再谈SQL和参数调优。这套顺序看似简单,能帮你避开大多数后期返工的问题。