最近公司在做一个Web3.0相关的项目,涉及区块链钱包连接、智能合约交互、NFT铸造、链上数据查询等功能。技术栈是:前端用React + ethers.js,智能合约用Solidity写的,部署在以太坊测试网Rinkeby上,后端用Node.js做服务端签名和元数据管理,数据库用PostgreSQL存用户信息和NFT元数据。
项目上线后一切正常,用户连接钱包、铸造NFT、查看藏品,都很顺利。直到某天晚上,用户反馈部分用户铸造NFT失败,而且是偶发的,时好时坏,有的用户能成功,有的用户失败,同一个用户有时候成功有时候失败。
我从晚上9点开始排查,一直查到第二天早上6点,终于找到了问题的根源。这一夜的排查过程非常曲折,踩了很多坑,也学到了很多Web3.0开发的经验教训。本文记录这次Bug的排查过程,从问题现象、排查思路、各种尝试,到最终找到根因和解决方案,希望能给做Web3.0开发的朋友一些参考。
先说明一下:为了不泄露公司的业务细节,文中的具体业务逻辑、合约代码、地址等都做了简化和脱敏处理,但排查过程和问题根源是真实的。
一、问题现象
晚上9点,我刚吃完饭,正准备看会儿剧放松一下,手机响了,是客服打来的:"有用户反馈铸造NFT失败,已经有好几个用户反馈了,你赶紧看看。"
我心里一紧,赶紧打开电脑,登录后台查看日志。
问题描述:
- 用户在前端点击"铸造NFT"按钮,调用智能合约的mint函数
- 部分用户交易失败,MetaMask钱包提示"交易失败"或者"out of gas"
- 偶发性:不是所有用户都失败,大概30%左右的失败率
- 同一个用户,有时候成功有时候失败
- 失败的交易,有的是用户拒绝了交易(这个正常),有的是用户确认了但交易上链失败
- 成功的交易一切正常,NFT能正常铸造,元数据能正常显示
初步信息:
- 智能合约代码最近没有更新,已经稳定运行了一周
- 前端代码最近也没有发布,版本是3天前的
- 后端服务正常,没有报错,CPU、内存、数据库都正常
- 区块链网络正常,Rinkeby测试网没有拥堵,gas价格正常
- 不是所有用户都失败,失败率大概30%,没有明显的规律
看起来一切正常,但就是有部分用户失败,而且是偶发的。这种偶发的、没有明显规律的Bug,是最难排查的。我知道,这将是一个漫长的夜晚。
二、第一轮排查:从最常见的原因开始
排查Bug,要从最常见、最可能的原因开始,逐一排除。我先列了一下可能的原因:
- 智能合约代码有bug,某些情况下mint函数执行失败
- 前端调用合约的参数有问题,某些情况下传错了参数
- gas价格设置不合理,导致交易失败或者out of gas
- nonce管理有问题,导致交易冲突
- 区块链网络问题,节点不稳定
- 钱包(MetaMask)的问题
- 用户操作问题,比如网络选错了、余额不够
- 后端签名有问题,导致白名单校验失败
我开始逐一排查。
检查1:查看智能合约代码和最近的部署记录。
我先看了一下智能合约的代码,mint函数的逻辑很简单:
- 校验用户是否在白名单里(用后端签名的merkle proof)
- 校验是否已经铸造过(每个地址只能铸造一个)
- 校验铸造数量是否超过上限
- 收取铸造费用(0.01 ETH)
- 铸造NFT,设置tokenURI
- 触发Transfer事件
逻辑很清晰,没有明显的bug。而且合约已经稳定运行了一周,铸造了几千个NFT都没问题,最近也没有更新合约,所以合约代码有bug的可能性不大。
我又查了一下合约的部署记录,确认线上运行的合约地址是正确的,没有部署错版本。
检查2:查看失败交易的链上记录。
我让客服收集了几个失败用户的钱包地址,然后去区块链浏览器(Etherscan)上查这些地址的交易记录。
发现了一个有趣的现象:失败的交易,有的根本没有上链(用户在钱包里拒绝了,或者交易没有广播出去),有的上链了但执行失败了(状态是Fail)。
对于上链失败的交易,我点进去看了一下具体的错误信息,大部分是"out of gas"(gas不够),少数是"execution reverted"(执行回滚)。
"out of gas"?这就奇怪了,我们前端设置的gas limit是300000,mint函数的实际gas消耗大概是150000左右,300000应该足够了,怎么会out of gas呢?
我又看了几个失败交易的gas limit,发现有的交易gas limit只有21000(普通转账的gas limit),有的是50000,有的是100000,都低于我们设置的300000。
这就更奇怪了,我们前端明明设置了gasLimit: 300000,为什么实际交易的gas limit不一样?难道是MetaMask覆盖了我们的设置?
我查了一下ethers.js的文档,发现如果调用合约的时候设置了gasLimit,MetaMask应该会用这个值,用户可以在钱包里修改,但默认应该是我们设置的值。
那为什么有的交易gas limit很低呢?我怀疑可能是某些情况下,前端没有正确设置gasLimit,或者ethers.js在某些情况下没有把gasLimit传进去。
检查3:查看前端代码,确认gasLimit的设置。
我打开前端代码,找到调用mint函数的地方:
const tx = await contract.mint(merkleProof, tokenId, {
value: ethers.utils.parseEther("0.01"),
gasLimit: 300000,
});看起来没问题,gasLimit确实设置了300000。
但我注意到一个细节:这个mint函数是在一个async函数里调用的,前面还有一些异步操作,比如获取merkle proof、获取tokenId、检查用户是否已经铸造过等。
我仔细看了一下整个流程:
- 用户点击铸造按钮
- 前端调用后端接口,获取merkle proof和tokenId
- 前端调用合约的balanceOf函数,检查用户是否已经铸造过
- 如果没铸造过,调用合约的mint函数,设置gasLimit: 300000
- 等待交易确认
- 交易确认后,调用后端接口,记录铸造信息
看起来流程也没问题。
但我突然想到一个可能性:会不会是某些情况下,用户的网络比较慢,或者后端接口响应慢,导致前端状态出问题,gasLimit没有正确设置?或者是ethers.js的某个bug,在某些情况下gasLimit没有生效?
我决定先做个实验,在自己的测试环境里复现这个问题。
第一轮排查的结论:
- 合约代码没问题,最近没有更新
- 失败交易大部分是out of gas,而且实际gas limit低于设置的300000
- 前端代码看起来设置了gasLimit,但实际交易中gas limit不对
- 怀疑是前端gasLimit设置没有生效,或者ethers.js/MetaMask的问题
时间已经到了晚上11点,我决定深入排查gasLimit的问题。
三、第二轮排查:深入排查gasLimit问题
我决定在本地测试环境复现这个问题,看看gasLimit到底是怎么回事。
实验1:在本地测试环境调用mint函数,查看实际的gasLimit。
我启动了本地开发环境,连接Rinkeby测试网,用测试钱包调用mint函数。在MetaMask弹出确认窗口的时候,我仔细看了一下gas limit的数值。
第一次:gas limit显示300000,正确。 第二次:gas limit显示300000,正确。 第三次:gas limit显示300000,正确。
试了十几次,都是300000,没有复现问题。
这就奇怪了,为什么线上用户会出现gas limit很低的情况?难道是特定用户的问题?或者是特定环境的问题?
实验2:用不同的钱包、不同的网络环境测试。
我又用了几个不同的测试钱包,切换不同的网络(有的用默认的Rinkeby RPC,有的用自己的Infura节点,有的用Alchemy节点),测试了几十次,还是没有复现,gas limit都是300000。
这时候我有点困惑了,线上明明有30%的失败率,为什么我本地复现不了?
换个思路:查看前端日志,看看失败用户的具体情况。
我让客服联系了一个失败用户,让他打开浏览器控制台,重新操作一次,把控制台的日志和网络请求截图发给我。
用户配合地操作了一次,这次又失败了。他发来了截图,我仔细看了一下:
- 控制台没有报错
- 后端接口调用正常,返回了merkle proof和tokenId
- 合约的balanceOf调用正常,返回0(没铸造过)
- MetaMask弹出了确认窗口,用户确认了
- 交易上链后失败,原因是out of gas
- 交易的gas limit是21000
21000?这是普通ETH转账的gas limit,不是合约调用的gas limit。为什么会是21000?
我突然想到一个可能性:会不会是某些情况下,ethers.js没有正确识别这是一个合约调用,而是当成了普通转账,所以用了默认的21000 gas limit?
但为什么会这样?我们明明是通过contract.mint()调用的,ethers.js应该能识别这是合约调用啊。
深入排查:查看ethers.js的源码和文档。
我开始查ethers.js的文档和GitHub issues,搜索"gas limit not working"、"gas limit 21000"、"contract call gas limit"等关键词。
查了半天,发现了一个相关的issue:在某些情况下,如果合约的ABI有问题,或者调用的函数在ABI中不存在,ethers.js可能会把调用当成普通的转账,而不是合约调用,这时候gas limit就会用默认的21000。
看到这个,我心里一动:会不会是我们的合约ABI有问题?或者某些情况下,前端加载的ABI不对?
我赶紧查看前端的合约ABI文件,确认mint函数在ABI中是存在的,参数也正确。看起来没问题。
但我又想到一个细节:我们的项目里,合约ABI是从后端接口动态获取的,不是写死在前端代码里的。因为我们有多个合约(不同的NFT系列),前端会根据用户选择的NFT系列,从后端获取对应的合约地址和ABI。
会不会是某些情况下,后端返回的ABI有问题?或者前端加载ABI的时候出错了?
检查后端返回的ABI。
我调用了后端的合约配置接口,查看返回的ABI数据。发现返回的ABI是正常的,mint函数在里面,参数也正确。
但我注意到一个问题:后端返回的ABI是一个JSON字符串,前端拿到之后用JSON.parse解析,然后用new ethers.Contract(address, abi, signer)创建合约实例。
会不会是某些情况下,JSON.parse出错了,或者ABI解析不完整,导致mint函数不在ABI里?
我在前端加了一些日志,打印创建合约实例后的ABI,看看mint函数是否存在。但因为是线上问题,我不能直接改线上代码,只能在本地测试。
在本地测试了很多次,ABI都是正常的,mint函数都在,没有复现问题。
这时候已经是凌晨1点了,我有点疲惫,但问题还没有找到。我决定换个思路,不从gasLimit入手了,而是从其他可能的原因排查。
四、第三轮排查:换个思路,从nonce和交易管理入手
排查了几个小时gasLimit的问题,没有找到根本原因,本地也复现不了。我决定换个思路,从其他可能的原因入手。
检查nonce管理。
在以太坊上,每个地址的交易有一个nonce,是递增的序号。如果nonce管理有问题,比如两笔交易用了同一个nonce,或者nonce跳号了,就会导致交易失败。
但我们的项目里,交易都是用户在自己的钱包里发起的,nonce是MetaMask管理的,我们前端不直接管理nonce。所以nonce出问题的可能性不大,除非是MetaMask本身的bug。
不过,我还是查了一下失败用户的交易记录,看看有没有nonce冲突的情况。查了几个,发现失败交易的nonce都是连续的,没有冲突,所以nonce问题可以排除。
检查用户的网络和钱包设置。
我让客服问了几个失败用户一些问题:
- 你用的是什么钱包?(大部分是MetaMask,少数是WalletConnect)
- 你连接的是什么网络?(都是Rinkeby测试网,没有选错)
- 你的钱包余额够吗?(都够,0.01 ETH + gas费)
- 你用的是什么设备和浏览器?(有Windows的Chrome,有Mac的Safari,有手机端的MetaMask App)
没有发现明显的规律,各种设备、浏览器、钱包都有失败的。
检查智能合约的事件和日志。
我又仔细看了一下失败交易的链上日志,发现那些"execution reverted"的失败交易,没有触发任何合约事件,说明交易在执行早期就回滚了。而"out of gas"的失败交易,有的触发了部分事件,有的没有。
这说明,失败确实是在合约执行过程中发生的,不是前端或者网络的问题。
但为什么会out of gas呢?如果gas limit足够的话,不应该out of gas啊。除非,实际的gas消耗比我们预期的高很多?
重新估算mint函数的gas消耗。
我开始重新估算mint函数的gas消耗。之前我们测试的时候,mint一次大概消耗150000 gas,所以设置了300000的gas limit,留了一倍的余量,应该足够了。
但有没有可能,某些情况下gas消耗会高很多?比如:
- 铸造的tokenId很大,存储成本高?
- merkle proof很长,验证成本高?
- 用户地址之前有过交易,状态不同?
- 区块链网络拥堵,gas价格高,导致同样的ETH能支付的gas变少?(不对,gas limit是gas的数量,不是ETH的数量,和gas价格无关)
我仔细看了一下合约代码,mint函数里有一个merkle proof验证的逻辑,用的是OpenZeppelin的MerkleProof库。merkle proof的长度取决于用户在merkle树中的位置,一般是log2(用户数),比如1000个用户就是10层左右,gas消耗应该是固定的,不会有太大变化。
tokenId的存储,第一次存储一个新的slot需要20000 gas,之后更新只需要5000 gas。我们的tokenId是递增的,每次都是新的,所以存储成本是固定的20000 gas。
看起来gas消耗应该是固定的,不会有太大变化,300000的gas limit应该足够。
除非……除非某些情况下,合约执行了更多的操作?比如,mint函数里有一些条件分支,某些情况下会执行更多的代码?
我又仔细看了一遍合约代码,发现mint函数里有一个逻辑:如果是白名单用户,铸造费用是0.01 ETH;如果不是白名单用户,铸造费用是0.05 ETH。费用校验用的是require(msg.value == price)。
但这个不会影响gas消耗啊,只是校验msg.value的值,执行的代码是一样的。
等等,我突然发现了一个问题:合约里有一个函数,是给管理员用的,可以设置白名单的merkle根。这个函数会更新合约的状态,会不会是管理员更新了merkle根之后,某些用户的merkle proof就不对了,导致验证失败,交易回滚?
但验证失败应该是"execution reverted",不是"out of gas"啊。而且如果merkle根更新了,应该是所有用户都失败,不是部分用户失败。
我查了一下合约的交易记录,最近确实有一次管理员更新merkle根的交易,是在下午3点,而用户反馈问题是在晚上8点,时间上对得上。但更新merkle根之后,新的白名单用户应该能正常铸造,旧的白名单用户如果不在新的merkle树里,会验证失败,但这应该是reverted,不是out of gas。
这个方向好像也不对。
这时候已经是凌晨3点了,我喝了杯咖啡,继续排查。
五、第四轮排查:终于发现了线索
排查了这么久,还是没有找到根本原因,我有点焦虑了。我决定从头开始,把整个流程再梳理一遍,看看有没有遗漏的地方。
重新梳理整个铸造流程:
- 用户进入铸造页面,前端调用后端接口
/api/contract/config,获取合约地址、ABI、铸造价格、是否开启铸造等配置 - 用户连接钱包
- 用户点击"铸造"按钮
- 前端调用后端接口
/api/mint/whitelist,传入用户地址,获取merkle proof和tokenId - 前端调用合约的
balanceOf(userAddress),检查用户是否已经铸造过 - 如果没铸造过,前端创建合约实例,调用
mint(merkleProof, tokenId),设置value=0.01 ETH,gasLimit=300000 - MetaMask弹出确认窗口,用户确认
- 交易广播到区块链网络,等待确认
- 交易确认后,前端调用后端接口
/api/mint/record,记录铸造信息 - 显示铸造成功,展示NFT
整个流程看起来没问题。但我突然注意到一个细节:第1步,前端从后端获取合约配置,包括ABI。第6步,前端用这个ABI创建合约实例,调用mint函数。
如果后端返回的ABI有问题,或者前端解析ABI有问题,那么创建的合约实例就可能有问题,调用mint函数的时候就可能出错。
但之前检查过后端返回的ABI是正常的啊。除非……除非后端返回的ABI在某些情况下有问题?比如,并发请求的时候?或者缓存的问题?
我决定再仔细检查一下后端的合约配置接口。
检查后端合约配置接口。
我打开后端代码,找到/api/contract/config接口的实现。这个接口的逻辑很简单:
- 从数据库查询合约配置(合约地址、ABI、铸造价格、是否开启等)
- 返回给前端
我看了一下数据库里的ABI数据,是正常的JSON格式,mint函数在里面。
但我注意到一个问题:这个接口加了缓存,用的是Redis缓存,缓存时间是1小时。也就是说,第一次请求之后,后续1小时内的请求都直接返回缓存的数据。
缓存的数据应该和数据库里的一样啊,除非……除非缓存的数据在序列化/反序列化的时候出了问题?或者缓存的数据被截断了?
我查了一下Redis里缓存的合约配置数据,发现了一个惊人的问题:缓存的ABI数据被截断了!
Redis里缓存的ABI,最后几个函数不见了,包括mint函数!完整的ABI应该有20多个函数,但缓存里只有15个,最后几个被截断了。
为什么会被截断?我查了一下,发现是因为我们设置Redis缓存的时候,用了一个错误的序列化方式,导致长字符串被截断了。具体来说,我们用了一个第三方的缓存库,这个库在序列化的时候,对字符串长度有一个默认的限制(大概是10000字符),超过的部分会被截断。我们的ABI字符串有12000多字符,超过了限制,所以被截断了,后面的函数(包括mint)都不见了。
但为什么之前没有问题?因为这个缓存是最近才加上的,是3天前的一次发布加的,目的是减少数据库查询。加了缓存之后,第一次请求会从数据库读取完整的ABI,然后缓存到Redis(被截断了),之后1小时内的请求都从Redis读取被截断的ABI。
所以,问题的时间线是:
- 3天前发布,加了缓存
- 缓存的ABI被截断,mint函数不见了
- 但因为缓存时间是1小时,有的时候缓存过期了,第一次请求从数据库读取完整的ABI,这时候前端拿到的是完整的ABI,mint函数存在,交易成功
- 有的时候缓存还没过期,前端拿到的是被截断的ABI,mint函数不存在,这时候ethers.js调用mint函数的时候,因为ABI里没有这个函数,ethers.js就把它当成了普通的转账交易,gas limit用了默认的21000,导致交易out of gas失败
这就解释了为什么问题是偶发的,为什么有的用户成功有的用户失败,为什么同一个用户有时候成功有时候失败——完全取决于请求的时候缓存有没有过期!
我终于找到问题的根源了!这时候已经是凌晨5点了,窗外天都快亮了。
六、问题验证和解决方案
找到问题根源之后,我赶紧验证一下。
验证:
- 清除Redis里的合约配置缓存
- 重新请求后端接口,确认返回的ABI是完整的,mint函数存在
- 用完整的ABI创建合约实例,调用mint函数,确认gas limit是300000,交易成功
- 再把被截断的ABI存到Redis缓存里,请求接口,确认返回的ABI是被截断的,mint函数不存在
- 用被截断的ABI创建合约实例,调用mint函数,确认ethers.js把它当成了普通转账,gas limit是21000,交易out of gas失败
验证通过!问题完全复现了,根源就是Redis缓存的ABI被截断,导致前端拿到的ABI不完整,mint函数不存在,ethers.js把合约调用当成了普通转账,gas limit用了默认的21000,导致交易out of gas失败。
解决方案:
找到了根源,解决方案就简单了:
- 立即修复:清除Redis缓存,临时去掉缓存,或者修复缓存的序列化问题。 我先紧急清除了Redis里的合约配置缓存,然后把这个接口的缓存暂时关掉,确保前端能拿到完整的ABI。操作之后,让客服通知失败用户重试,用户反馈铸造成功了,问题暂时解决。
- 根本修复:修复缓存库的字符串长度限制。 查了一下缓存库的文档,发现可以配置字符串的最大长度,我把最大长度配置成了1000000(1MB),足够存储ABI了。然后重新加上缓存,测试确认缓存的ABI是完整的,没有被截断。
- 增加监控和告警。 在后端接口里增加了ABI完整性校验,如果发现ABI里没有mint函数,就记录错误日志并告警,避免类似问题再次发生。
- 前端增加容错。 在前端也增加了校验,创建合约实例之后,检查合约实例上是否有mint方法,如果没有,就提示用户"合约配置加载异常,请刷新页面重试",而不是直接调用导致交易失败。
- 增加集成测试。 在测试环境增加了端到端的集成测试,模拟用户铸造NFT的完整流程,确保缓存、ABI、合约调用都正常。
修复完成之后,观察了几个小时,没有再出现铸造失败的情况,问题彻底解决了。这时候已经是第二天早上6点了,我熬了整整一夜,虽然很累,但找到问题并解决了,心里还是很有成就感的。
七、这次排查的经验教训
这次Web3.0 Bug的排查过程非常曲折,熬了一夜才找到问题根源。回顾整个过程,总结了一些经验教训,希望能给大家一些参考。
1. Web3.0开发的特殊性,要注意ABI的完整性。
Web3.0开发和传统的Web开发有很多不同,其中一个重要的区别就是智能合约的ABI。ABI是前端和智能合约交互的接口定义,如果ABI不完整或者不正确,前端调用合约函数的时候就会出问题,而且问题可能很隐蔽,比如这次的gas limit不对,交易out of gas,很难直接想到是ABI的问题。
所以,在Web3.0开发中,一定要注意ABI的完整性和正确性:
- ABI要从可靠的来源获取,最好是写死在前端代码里,或者从可信的后端接口获取
- 后端返回ABI的时候,要确保完整,不要被缓存、序列化、代理等环节截断
- 前端拿到ABI之后,最好校验一下关键函数是否存在,不存在就报错提示,不要直接调用
- 调用合约函数的时候,最好显式设置gasLimit,并且在交易失败的时候检查gas limit是否正确
2. 偶发问题要考虑缓存、并发等因素。
这次的问题是偶发的,时好时坏,排查了很久才找到原因。偶发问题通常和缓存、并发、网络、用户环境等因素有关,排查的时候要重点考虑这些因素。
尤其是缓存,缓存是好东西,能提高性能,但缓存也容易出问题,比如缓存数据不一致、缓存被截断、缓存过期时间设置不合理、缓存击穿/穿透/雪崩等。加缓存的时候一定要仔细测试,确保缓存的数据是正确的、完整的,并且要有监控和告警,发现缓存异常及时处理。
这次的问题就是因为加缓存的时候没有仔细测试,导致长字符串被截断,而且因为缓存时间是1小时,问题是偶发的,增加了排查难度。如果加缓存的时候做了完整的测试,或者有ABI完整性的监控,问题就能更早发现。
3. 排查问题要从现象出发,大胆假设,小心求证。
排查Bug是一个不断假设、验证、排除的过程。要从问题现象出发,列出所有可能的原因,然后逐一验证排除,不要凭感觉盲目排查。
这次排查中,我一开始就列出了8个可能的原因,然后逐一排查,虽然走了一些弯路,但整体方向是对的。在排查gasLimit问题的时候,虽然一开始没有找到根本原因,但收集到了很多有用的信息(失败交易的gas limit是21000),这个信息最后成为了找到根源的关键线索。
排查问题的时候,不要怕走弯路,每一次排除一个错误的假设,都是在向正确答案靠近。要保持耐心,不要急躁,更不要随便改代码碰运气,那样可能会引入新的问题。
4. 日志和监控很重要,要在关键节点记录日志。
这次排查中,如果前端有更详细的日志(比如记录创建合约实例后的ABI、记录调用mint函数时的gasLimit、记录交易失败的详细原因),问题就能更快定位。后来我在前端和后端都增加了详细的日志和监控,就是为了以后再出问题的时候能更快排查。
在Web3.0开发中,日志和监控尤其重要,因为区块链上的交易是不可逆的,一旦交易失败,用户的gas费就白花了,体验很差。如果有完善的日志和监控,就能在问题出现的时候快速定位和修复,减少用户的损失。
建议在Web3.0项目中,在这些关键节点记录日志:
- 合约配置加载(合约地址、ABI是否完整)
- 钱包连接(钱包地址、网络、链ID)
- 合约函数调用(函数名、参数、gasLimit、gasPrice)
- 交易广播(交易hash、from、to、value、nonce)
- 交易确认(交易状态、blockNumber、gasUsed)
- 交易失败(失败原因、错误信息)
有了这些日志,出问题的时候就能快速定位,不用像这次一样熬一夜才找到原因。
5. 第三方库要了解其限制,不要盲目信任。
这次的问题,直接原因是第三方缓存库对字符串长度有默认限制,导致长字符串被截断。我们在使用第三方库的时候,往往会默认它是正确的、可靠的,不会有问题,但实际上,任何第三方库都可能有bug、有限制、有坑。
所以,使用第三方库的时候,要了解它的配置项、限制、已知问题,不要盲目信任。尤其是涉及到数据存储、序列化、网络请求这些关键环节,更要仔细测试,确保数据的完整性和正确性。
加缓存这种操作,看起来简单,但实际上有很多细节要注意:缓存的key怎么设计、缓存的value怎么序列化、缓存过期时间怎么设置、缓存更新策略是什么、缓存异常怎么处理、缓存数据怎么校验。这些细节不注意,就可能出问题。
6. 线上问题要冷静,不要慌,更不要盲目操作。
线上出问题的时候,很容易慌,尤其是用户在催、领导在问的时候,更容易乱了阵脚,盲目改代码、重启服务、回滚版本,这样可能会让问题更严重,或者引入新的问题。
这次排查中,虽然问题很棘手,用户一直在催,但我一直保持冷静,按照排查流程一步步来,没有盲目改代码。最后找到了根本原因,一次性修复,没有反复折腾。
线上问题处理的正确姿势是:
- 先评估影响范围,看问题有多严重,影响多少用户
- 如果影响很大,先做紧急处理(比如回滚、降级、临时关闭功能),减少影响
- 然后冷静排查,收集信息,分析日志,复现问题,找到根本原因
- 找到原因之后,制定修复方案,在测试环境验证
- 验证通过后,再上线修复,观察一段时间,确认问题解决
- 最后总结复盘,避免类似问题再次发生
不要在没有找到根本原因的时候就盲目改代码,那样可能会越改越乱,问题更难排查。
八、写在最后
这次Web3.0的Bug,我从晚上9点排查到第二天早上6点,整整熬了一夜,终于找到了问题根源并修复。虽然过程很曲折,很累,但也学到了很多东西,对Web3.0开发有了更深的理解。
Web3.0是一个新兴的领域,技术栈还不成熟,开发工具还不完善,坑很多。做Web3.0开发,需要比传统Web开发更细心、更谨慎,因为区块链上的交易是不可逆的,一旦出问题,用户的资产就可能损失,影响很大。
这次的问题,根源其实很简单——缓存截断了ABI,但因为Web3.0的特殊性,表现出来的现象很隐蔽(偶发的out of gas),排查起来很困难。这也提醒我们,在Web3.0开发中,每一个环节都要仔细,不能有侥幸心理,一个小小的缓存问题,就可能导致用户的交易失败,影响用户体验和项目声誉。
最后,总结一下这次排查的关键经验:
- Web3.0开发要注意ABI的完整性,前端最好校验关键函数是否存在
- 偶发问题要重点考虑缓存、并发、网络等因素
- 排查问题要从现象出发,大胆假设,小心求证,不要盲目改代码
- 完善的日志和监控是快速排查问题的关键
- 第三方库要了解其限制,不要盲目信任
- 线上问题要冷静处理,先评估影响,再排查修复
希望这篇文章能给做Web3.0开发的朋友一些参考,让大家在遇到类似问题的时候能少走弯路,不用像我一样熬一夜才找到原因。
也希望Web3.0的生态能越来越成熟,开发工具能越来越完善,让开发者少踩坑,把更多精力放在产品和业务上,而不是排查各种奇怪的Bug。
天已经亮了,我也要去补觉了。希望这篇文章对你有帮助,我们下次再见。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录