开年第一天,很多人的手机都被“中国银行APP崩了”这个话题刷了屏。紧接着,技术群里出现了一个流传很广的猜想:是不是因为用了华为高斯数据库?这个猜想一出,讨论很快分成两派,一派翻旧账说国产数据库还是不行,另一派则急着替高斯正名。作为这几年一直在跟PostgreSQL生态打交道、也给客户做过数据库高可用方案的人,我倒觉得这件事最有价值的点,不在于“是不是高斯的锅”这个疑问本身,而在于它暴露了我们在面对线上故障时常犯的一个思维误区——把整条调用链路想成了一条直线,然后从最末端、最显眼的那个组件开始找凶手。
这篇文章不打算替谁洗白,也不想给谁的故障定案,毕竟没有现场日志和数据,任何结论都是猜测。我更想从一个数据库从业者的视角,把这类“APP崩溃→数据库背锅”的完整分析链路讲清楚:一次登录请求到底经过了哪些节点、高斯数据库在银行系统里可能以什么形态存在、数据库故障通常以哪几种方式呈现、以及作为DBA或SRE,在没有内部监控的情况下,应该如何科学地逼近真相。文章后半部分还会给出一套本地复现方案,把高斯环境搭起来,实际看看连接打满和锁等待是怎么把系统拖死的。
1. 从“APP崩了”到“数据库背锅”,这条推理链哪里不靠谱
1.1 一次银行APP登录,请求到底经过了哪些节点
很多人一想到“APP打不开”,第一反应就是后台数据库坏了,但实际上,一条登录请求从手机屏幕到数据库,中间要跨过的节点多得惊人。
典型路径大致是这样的:手机APP先请求DNS或CDN,然后流量进入负载均衡器,再转发到API网关做鉴权和限流,接着到达应用服务的登录接口。这个接口通常不会直接读写数据库,而是先查Redis缓存拿会话信息,经过用户体系服务做密码校验或验证码校验,再通过消息队列或RPC调用其他业务服务,最后才会“有可能”访问到数据库。
这中间任何一个环节出问题,用户看到的都是“转圈”“系统繁忙”“请求超时”。如果要说“数据库的锅”,至少得先排除掉网络出口、运营商链路、CDN节点、网关限流、应用发布、缓存击穿这些前置环节,否则就是把一个多变量问题强行压缩成了单变量问题。
1.2 用户感知到的“崩”,和数据库故障的对应关系
我见过很多次故障复盘,用户端描述和后台根因经常对不上。把“崩溃”具体化,通常能帮我们更快锁定怀疑区间。下面这张表是我平时做排查时心里会过一遍的映射关系。
| 用户端的异常表现 | 更可能的故障层级 | 与数据库的关联度 |
|---|---|---|
| 页面秒开但提示系统繁忙 | 网关限流、服务熔断、业务逻辑拒绝 | 通常不高 |
| 请求一直转圈,直到超时 | 应用线程阻塞、RPC超时、数据库连接等待 | 中高 |
| 部分功能可用,部分功能报错 | 单服务发布异常、缓存失效、特定接口慢SQL | 需要看具体功能 |
| 整体完全无法访问 | DNS/CDN/网关故障或大面积机房网络问题 | 数据库单点故障可能性较低 |
| 登录成功后偶发会话丢失 | Redis异常、会话粘滞失效 | 低 |
| 偶发提交失败,重试后成功 | 数据库主备切换、连接池短暂耗尽 | 高 |
注意,即便出现了“请求超时”这种数据库关联度高的现象,也还有一层逻辑要拆:数据库无响应,到底是数据库自己扛不住了,还是应用侧的连接池已经耗尽,把所有请求都堵在了获取连接的瞬间?前者是数据库问题,后者是应用配置问题,但监控面板上看到的DB CPU和慢SQL可能都是正常值。
1.3 高可用架构反而可能放大故障
银行系统的数据库很少是裸奔单机,通常会做主备、同城双活或者三副本。这种架构本来是为了高可用,但故障放大效应往往也藏在这里。
比如主库和备库之间的复制延迟一旦过大,或者脑裂后触发自动切换,整个集群会进入短暂的不可用状态。应用层如果不做快速的连接重建,所有连接池里的老连接会同时失效,瞬间爆发大量重连请求,把刚刚恢复的数据库再次冲垮。还有一种常见情况是代理层或中间件做了超时重试,业务侧的失败请求会在短时间内被重复发送多次,导致数据库实际承受的负载是用户可感知负载的好几倍。这些机制叠加起来,最终的现象就是“整个系统瞬间崩溃”,但这并不代表数据库内核真的出了致命问题。
所以每当有人抛出一句“XX数据库不行啊”,我第一反应都是先反问一句:你看到的是数据库的error log,还是整个调用链路的症状?症状不等于根因,这个道理在数据库领域比在任何一个技术领域都更值得反复强调。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高斯数据库到底是什么来头:产品谱系和它在银行系统的真实位置
2.1 openGauss和GaussDB,先别混为一谈
“高斯数据库”这个名字在市面上其实指代两个经常被混淆的东西。一个是华为开源出来的openGauss,另一个是华为云上的企业级数据库GaussDB。openGauss的代码基于PostgreSQL 9.2.4演进而来,但做了大量内核级改造,比如把进程模型改成了线程池模型、增加了原地更新机制、重构了内存管理,还自研了WAL回收等能力。GaussDB则是一个更庞大的产品族,其中既有基于openGauss的集中式版本,也有分布式形态的版本。
很多人以为高斯和PostgreSQL只是换了个皮肤,实际操作过后就会发现两者的差异相当大。高斯的线程池模型让它在高并发短连接场景下比传统PostgreSQL的进程模型占有一定优势,但这也意味着一些依赖进程模型的运维习惯要改,比如用ps命令数进程那一套在高斯里就看不到了。
2.2 在金融系统里,高斯通常以什么形态出现
银行引入国产数据库,一般不会一上来就把核心账务系统整个搬过去,而是会分层推进。外围系统、渠道类系统、风控系统往往先行,核心账务系统则要经历漫长的验证和并行阶段。高斯的集中式版本多数部署形态是“一主一备同步复制加一个级联备机”,对外提供一个虚拟IP,由集群管理软件负责探活和切换。分布式版本则更多出现在需要水平扩展的互联网金融类业务上。
回到“APP崩了”这个事件,如果中国银行的某个系统确实用了高斯数据库,它既有可能是某套外围系统的业务库,也有可能是用户体系或交易链路上的一个关键节点。但我们必须承认,我们手上没有任何证据能说明这次故障到底发生在哪个系统、哪一层数据库。与其根据一个名字去站队,不如把高斯数据库在技术上可能存在的薄弱点都过一遍。
2.3 从PostgreSQL迁移到高斯时,开发人员最爱踩的坑
如果你之前只用过原生PostgreSQL,第一次接触高斯数据库时,有几个地方特别容易翻车。
第一是大小写敏感策略。高斯在默认参数下,未加引号的标识符会转换为小写,这个和PostgreSQL一致,但很多从MySQL迁移过来的团队习惯用大写表名,经常出现“表不存在”的诡异报错。第二是序列和自增列的使用。高斯虽然支持SERIAL和IDENTITY,但因为内核改造过,某些迁移工具生成的序列归属语句在高斯上执行会有兼容性问题。第三是分区表的语法。PostgreSQL的分区语法和高斯的内置分区语法有一段时期是不完全兼容的,如果从Oracle迁移过来的SQL直接照搬,执行计划往往会走偏。第四是锁机制。高斯的gs_locks视图和PostgreSQL的pg_locks字段不完全一样,依赖老视图查锁的脚本可能要改。
还有一个隐藏差异在驱动上。虽然高斯JDBC驱动可以从PostgreSQL驱动改配置来连,但生产环境强烈不建议混用,因为服务端预编译、extended query协议、某些数据类型的编码处理都有差异。我会在后面的实操部分专门演示。
3. 如果数据库真的是故障源,最值得怀疑的那几类根因
3.1 连接数被打满:最不起眼却最高频的“元凶”
数据库故障中最常见的原因不是CPU烧满、也不是磁盘写爆,而是连接数被耗尽。很多系统在设计时给应用层配了比较大的连接池,比如单实例200个连接,一共20个应用节点,加起来就是4000个连接。如果数据库的max_connections只配了2000,那么在业务高峰期,部分应用节点就会一直拿不到连接,表现就是请求超时。
更麻烦的是连接泄漏。应用代码里如果存在异常分支忘记归还连接,连接池会一点点被蚕食,可能白天一切正常,到某个业务量高点突然崩溃。很多开发人员第一反应是“加数据库连接数”,但根子上的解法是:检查应用侧连接池的空闲连接回收策略、最大等待时间、以及是否存在慢SQL把连接长时间占住不放。
在高斯数据库里,你可以用下面这条SQL快速看当前连接状态:
sql复制SELECT pid, usename, state, wait_event_type, wait_event,
now() - query_start AS query_duration, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
注意,这条SQL在高斯和PostgreSQL上是通用的。如果结果里大量连接都卡在client_read或者wait_event_type为Lock的状态,那就不是单纯的连接数不够,而是更底层的阻塞问题。
3.2 锁等待与阻塞:一个未提交事务能拖垮整个交易链路
数据库连接被打满时,如果观察活跃查询,经常会看到一条极其简单的UPDATE语句在那里跑了很久。这种诡异现象十有八九是锁等待。
举个例子,某个应用开启了事务,对一张订单表执行了UPDATE,但事务一直没提交,也没回滚。此时另一个请求想更新同一行,就会进入锁等待状态。一旦这种“僵尸事务”多起来,新的请求全部堆积,最终表现为系统整体无响应。
在高斯环境里,查锁的经典SQL大致长这样:
sql复制SELECT blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocker.pid AS blocker_pid,
blocker.query AS blocker_query
FROM pg_stat_activity blocked
JOIN pg_locks blocked_locks ON blocked_locks.pid = blocked.pid
LEFT JOIN pg_stat_activity blocker ON blocker.pid = blocked_locks.pid
LEFT JOIN pg_locks blocker_locks ON blocker_locks.pid = blocker.pid
WHERE blocked_locks.granted = false
AND blocker_locks.granted = true;
实际在高斯产品里,可能更习惯查pg_locks或者gs_locks获取等锁关系。这里更重要的是排查思路:先找到不提交的idle in transaction连接,然后找到它持有的锁,再决定是联系业务方提交,还是直接终止这个会话。没有这个排查习惯的团队,遇到“数据库宕了”的误判时,最容易做出错误决策——直接重启数据库。重启虽然能清掉所有锁,但如果是长事务回滚,重启后恢复的时间可能比故障持续的时间还要长。
3.3 慢SQL成为吞吐杀手:统计信息不准与执行计划漂移
第三种典型的数据库故障源是慢SQL。正常情况下一条SQL可能几十毫秒,但某天数据量涨到一定规模,或者索引统计信息过期,执行计划突然从索引扫描变成了全表扫描,单条SQL耗时飙到几秒钟,并发一多,数据库的整体吞吐就断崖式下降。
高斯的优化器整体上是基于代价的,统计信息是否及时更新直接影响执行计划。迁移上来的系统最容易忽略的就是ANALYZE,因为像Oracle那样的数据库会在夜间自动维护统计信息,而高斯很多版本里如果没有显式配置自动分析任务,统计信息可能一直停留在初始状态。
当APP出现大面积超时,DBA需要先捞TOP SQL,看每条SQL的执行时间、扫描行数、返回行数,再对比执行计划中预估行数和实际行数的差异。差异越大,统计信息越可疑。处理方式通常就是ANALYZE TABLE或者针对问题SQL做绑定执行计划。
3.4 资源竞争和“雪崩式”连锁反应
数据库本身没有明显慢SQL、锁等待也正常,但仍然无响应,这时就要看资源竞争了。磁盘IO延迟突然飙升、WAL日志目录空间被打满、临时文件落到慢盘上、内存不够导致进程被OOM Kill,这些都是数据库侧可能出现的故障模态。
不过我更想提醒的是雪崩式连锁反应。当应用服务出现大量超时后,上游网关会有重试机制,用户也会因为焦急反复点击。重试流量在数据库已经接近临界点时突然涌入,会瞬间压垮数据库。这种情况下,即便数据库内核毫发无伤,它也会在监控面板上表现出CPU和负载飙升,让不懂全链路的人误以为是数据库本身崩溃了。这也是为什么我一直强调:看到数据库高负载时,第一步不是急着杀进程,而是先看是谁在发起请求、请求量是否合理、有没有重试风暴。
4. 银行为什么换数据库这么谨慎,以及“压测通过”为什么依然不可信
4.1 集中式和分布式不是替代关系,而是不同形态
聊到国产数据库替换,很多人喜欢问一句:“高斯能扛住银行核心的交易量吗?”这个问题本身其实问得不严谨。因为“高斯”如果指的是集中式形态,它的水平扩展能力天然有限,主要靠把单机性能做扎实、把主备切换做可靠来支撑业务。如果指的是分布式形态,它要考虑的是数据分片、分布式事务、全局时钟这些东西,单机性能反而不是最核心的指标。
银行对数据库的需求从来不是“快”,而是“稳”和“准”。账务系统哪怕丢一分钱都是重大生产事故,所以选型时最看重的是数据一致性、容灾能力和可运维性,而不是跑分。这也解释了为什么银行换数据库的周期以年为单位——不是买来装上就行,而是要从容灾演练、备份恢复、性能容量、兼容性改造、人员培训等方面逐项验证。
4.2 替换数据库相当于换发动机,不改周边就等于原地翻车
很多人以为数据库替换就是把数据导过去、把连接串改一下,剩下的让DBA去折腾就行。实际上,一条业务SQL里可能藏着大量依赖Oracle或PostgreSQL特性的写法,这些在高斯上跑出来结果可能就是错的。
拿序列来说,Oracle的序列是独立于表的数据库对象,应用可以在插入前先取一个值;PostgreSQL的序列默认行为在事务回滚后会“空洞”,高斯的实现又有自己的差异。再比如隐式类型转换,Oracle能把字符串和数字做隐式比较,而高斯必须严格看参数配置。任何一点语义差异,在高并发下都可能变成线上bug。
所以完善的兼容性改造,通常要在测试环境做全量SQL回归,还要做双跑比对,也就是同一笔交易在旧库和新库各执行一遍,对所有字段做一致性比对。只有比对结果一致,才敢灰度切流量。
4.3 “压测通过”为什么不一定可靠
几乎每一套国产数据库上线前都会做压力测试,但压测通过和真实流量扛得住之间,往往隔着三道坎。
第一道坎是压测模型失真。压测脚本如果只做简单的“INSERT+SELECT”,没有模拟真实业务里事务的嵌套结构、各表的关联关系、锁操作的等价并发,那结果根本不能反映真实场景的压力。第二道坎是数据量差异。测试库只有几百万行数据,执行计划走索引没问题,上线后数据量到了几亿行,统计信息和索引结构完全不同,SQL性能就会出现指数级退化。第三道坎是时间维度。压测通常只跑几小时,而真实系统要经历日切、月末批量、节假日高峰,这些特定时间点的峰值模型根本无法通过常规压测来覆盖。
这也是为什么很多银行在国产数据库上线初期,会保留一段“并行观察期”,两边系统同时跑,以旧库为准,新库持续比对。这个阶段虽然成本很高,但的确是最稳妥的方式。
5. 用数据库视角拆解一次APP故障:三个黄金问题和一个复现试验
5.1 第一问:故障发生时,数据库的活跃连接是正常水位还是异常水位
我现在带团队处理线上故障时,一定会先让DBA回答三个问题。第一个问题是:“数据库当前的活跃会话数、空闲会话数、等待会话数分别是多少?和平时同一时段的均值比,差了多少?”
如果活跃会话数接近连接池上限,那就是连接/线程资源问题;如果活跃会话数很低,但应用仍然超时,那问题很可能不在数据库侧,而在应用获取连接的环节、网络链路或者网关层。
这三个数据在高斯里用一条SQL就能捞到:
sql复制SELECT state, wait_event_type, count(*)
FROM pg_stat_activity
GROUP BY state, wait_event_type
ORDER BY count(*) DESC;
看到结果后,再和监控系统里的历史趋势比对,就能快速确认数据库是不是处于“过载”或“锁死”状态。没有历史基线的团队,建议从今天开始做数据库指标的周期性采集,否则故障来了连“正常水位”是多少都不知道。
5.2 第二问:有没有带锁的慢查询在堆积
第二个问题是:“当前有没有长时间未结束的查询?有没有idle in transaction的会话?锁等待链条有多长?”
有时候查pg_stat_activity会发现大量会话state为idle in transaction,这是最容易被忽略的雷区。它们的SQL文本已经执行完,但事务一直挂着,持有的锁不会释放,后续所有涉及同一行数据的请求都会被堵住。
我见过不少团队把这类会话当成“无害的空闲连接”,直到系统全线超时才发现,罪魁祸首就是几个开发晚上跑批量任务没有提交事务。处理办法很简单,定期巡检,发现超过一定阈值的idle in transaction会话就告警,必要时自动终止。下面的SQL可以找出所有“开启事务但卡住超5分钟”的会话:
sql复制SELECT pid,
usename,
state,
now() - xact_start AS xact_duration,
query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - xact_start > interval '5 minutes';
5.3 第三问:数据库在故障前是否有变更
第三个问题是:“故障前15分钟到30分钟内,数据库侧有没有发生过变更?”这里的变更包括配置参数调整、索引新增、统计信息更新、死锁自动解除、主备切换、版本升级、批量任务启动等。
系统崩溃很少无缘无故,它往往是某个变更在特定流量模型下被触发而放大的结果。比如有人加了一个索引,因为表数据量大,创建索引的过程中持有了锁,阻塞了业务写入,而DDL的锁等待又引发了连接池耗尽,最终让整个APP毫无响应。如果日志能证明故障前确实有DDL操作,那排查方向就会清晰很多。
5.4 把排查思路固化成一场故障推演
光有方法论还不够,我强烈建议每套重要系统每年至少做一次“故障推演”——不是光动嘴开会,而是真的在测试环境模拟一遍连接池耗尽、锁等待、主备切换三种常见场景,让研发和DBA都亲手操作一遍定位和恢复流程。
比如,测试环境里人为造一个锁等待场景:开两个会话,会话A更新一行但不提交,会话B更新同一行,然后观察B的等待事件。再比如,把连接池最大连接数临时调到很小,然后批量发请求,看应用侧报什么错、数据库侧连接数如何涨。这些演练不需要什么昂贵工具,但对团队应急能力提升的效果,远比看十遍文档要好。
6. 没有生产环境也能复现:本地搭一套高斯环境,把锁和连接问题查个明白
6.1 从部署到初始化,最小资源也能跑起来
网上讨论高斯数据库的人很多,但真正动手部署过的人其实不多。这里我记录一下我本地的搭建过程,用的是openGauss的开源版本,方法论同样适用于GaussDB集中式版本。
我用的是一台4核8G的虚拟机,操作系统是openEuler。openGauss对操作系统有要求,CentOS 7.6以上也可以,但官方推荐openEuler。下载好安装包后,创建opengauss用户,解压到/opt/software/openGauss,然后执行初始化。
bash复制su - opengauss
gs_initdb -D /opt/software/openGauss/data --nodename=primary \
--pwfile=/tmp/opengauss_passwd --encoding=UTF-8
初始化完成后,用gs_ctl启动数据库:
bash复制gs_ctl start -D /opt/software/openGauss/data
启动成功后,可以用gsql连接本地库:
bash复制gsql -d postgres -p 5432 -U gaussdb -W '你的密码'
这里有个网上经常被问到的点:默认端口是5432,而不是很多人习惯的Oracle 1521或MySQL 3306,初次上手容易搞混。另外,安装时如果开了SSL,客户端连接需要指定sslmode或者下载证书,否则DBeaver连接时会弹出一堆看不懂的SSL报错。
6.2 用DBeaver连高斯,驱动选择和参数不能乱来
DBeaver想连高斯数据库,一种做法是选择PostgreSQL驱动,然后改连接串。这种办法有时候能通,但会碰到两类问题:一是在读取数据库列表时可能拿不到完整schema信息,二是某些数据类型在结果集里显示乱码或者报“类型未知”。更稳的做法是下载openGauss的JDBC驱动,然后在DBeaver里新建驱动,上传高斯专用的jar包。
连接参数的几个关键项我踩过坑,列一下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| JDBC URL | jdbc:opengauss://127.0.0.1:5432/postgres |
驱动如果是PostgreSQL版,URL开头的协议也要改成jdbc:postgresql |
| 用户名 | 一般为gaussdb,安装时可自定义 |
默认超级用户不叫postgres |
| 密码 | 安装时设置 | openGauss的默认密码复杂度要求较高,太简单会初始化失败 |
| SSL | 如果服务端没关SSL,连接时可能报错 | 可以在服务端postgresql.conf里关闭,或者客户端配置sslrootcert |
连接上之后,先跑一遍SELECT version();,确认当前版本。然后就可以把之前的锁查询SQL、连接查询SQL拿过去实战了。
6.3 真实的锁等待复现:一个事务不提交,系统就卡给你看
下面这组操作,我建议每个DBA新手都亲手跑一遍,跑过之后对“数据库崩了”的理解会完全不一样。
会话A执行:
sql复制BEGIN;
UPDATE account SET balance = balance - 100 WHERE account_id = 1;
不提交。然后会话B执行:
sql复制UPDATE account SET balance = balance + 100 WHERE account_id = 1;
此时会话B会一直卡住,不会报错,也不会超时,就这么静静地等着。这时候去查锁视图:
sql复制SELECT a.pid AS waiting_pid,
a.query AS waiting_query,
b.pid AS holding_pid,
b.query AS holding_query
FROM pg_stat_activity a
JOIN pg_locks l1 ON l1.pid = a.pid
LEFT JOIN pg_locks l2 ON l2.pid = a.pid AND l2.granted
JOIN pg_stat_activity b ON b.pid = l2.pid
WHERE NOT l1.granted;
如果这时候业务侧有一堆请求都在做账户余额更新,那它们会全部排队在会话A后面。等连接池耗尽的一瞬间,整个数据库对外表现就是“无法处理新请求”,但这其实不是数据库内核坏了,而是一个连接持有锁迟迟不放导致的阻塞链。
处理这种问题时,真正的正解是找到持有锁的会话,确认它对应的业务能否提交或回滚:
sql复制-- 如果确认是异常会话,可以终止它
SELECT pg_terminate_backend(<holding_pid>);
如果持有锁的会话是一个长事务,强行终止可能导致回滚时间很长。所以动手前一定要先看xact_start,判断事务运行了多久,回滚代价有多大。
6.4 建好环境之后,推荐做的几个日常巡检习惯
环境搭好之后,不要只当摆设。我习惯在本地库上做三个巡检脚本:一个查连接数趋势,一个查活跃会话,一个查锁等待。不需要多复杂,SQL能输出当前状态即可,重点是每天都看一遍,形成对数据库正常表现的感觉。等到生产环境真出问题时,你才能一眼看出“这个状态不正常”。
另一个值得做的是把高斯文档里关于线程池的配置参数都过一遍。比如max_connections、thread_pool_attr、resilience_adaptor_enable这些参数,分别控制什么行为,调整后会有什么副作用。很多人只关心参数能不能调大,却忽略了一些参数调大后会让系统在故障时恢复得更慢,甚至让OOM风险大幅上升。这些细节不踩一次坑,很难真正理解。
最后再分享一个我个人的体会:处理数据库故障,真正的分水岭往往不是技术高低,而是能不能在混乱中保持“先定位,再动手”的定力。每次听到“把数据库重启一下”这种建议,我都会先问一句:重启需要多久,如果回滚要2小时,这2小时业务能等吗?与其赌运气,不如花5分钟把活跃会话和锁等待看一眼。很多时候,真相就藏在那些平时没人看的等待事件里。
