去年,我们团队在一个核心业务中全面推广了WebAssembly(简称Wasm)。本来以为这是一次技术升级,能大幅提升性能,结果上线后出了一次惊心动魄的线上故障,差点导致整个业务不可用。

这次故障持续了大约四个小时,影响了几十万用户,事后我们做了详细的复盘。这篇文章,我想把这次故障的经过、原因和解决方案分享出来,也聊聊WebAssembly在实际普及过程中遇到的坑和经验。

希望我们的经历能给正在使用或准备使用WebAssembly的团队一些参考,避免踩同样的坑。

背景:为什么要推广WebAssembly

先说说我们为什么要在业务中推广WebAssembly。

我们的产品是一个在线文档协作工具,类似于Google Docs。用户可以在浏览器中创建、编辑、协作文档,支持富文本、表格、图片等多种内容。

随着用户量的增长和功能的增加,前端的性能问题越来越突出:

  • 文档打开速度慢,尤其是大文档,打开需要好几秒。
  • 编辑的时候卡顿,输入文字有延迟,体验不好。
  • 多人协作的时候,冲突处理和同步逻辑复杂,性能差。
  • 一些复杂的计算(比如文档排版、公式计算、数据处理)在JavaScript中运行很慢。

我们尝试了很多优化方案:代码分割、懒加载、虚拟列表、Web Worker、缓存优化等等。这些方案有一定效果,但还是不够理想。

后来,我们把目光投向了WebAssembly。

WebAssembly是一种低级的汇编语言,可以在浏览器中以接近原生的速度运行。我们可以把性能敏感的代码用C++或Rust编写,编译成WebAssembly,在浏览器中运行。

我们的计划是:

  • 把文档的核心引擎(排版、渲染、冲突处理)用C++重写,编译成WebAssembly。
  • 把复杂的计算逻辑(公式计算、数据处理、加密解密)放到WebAssembly中运行。
  • 前端JavaScript只负责UI交互和事件处理,核心逻辑都交给WebAssembly。

我们做了一个原型,测试结果非常乐观:文档打开速度提升了3倍,编辑流畅度大幅提升,复杂计算的性能提升了5-10倍。

于是,我们决定在整个业务中全面推广WebAssembly。

推广过程:一切看起来很顺利

推广过程分为几个阶段:

第一阶段,灰度发布。我们先让5%的用户使用WebAssembly版本,监控性能指标和错误率。灰度了一周,数据看起来很好:性能提升明显,错误率和原来差不多,没有发现严重问题。

第二阶段,扩大灰度。我们把灰度比例扩大到20%、50%,数据依然很好。用户反馈也很正面,很多用户说文档打开更快了,编辑更流畅了。

第三阶段,全量发布。灰度了一个月之后,我们觉得没问题了,就全量发布了WebAssembly版本。

全量发布后的前两天,一切正常。性能指标全面提升,错误率稳定,用户反馈良好。我们都很高兴,觉得这次技术升级很成功。

但我们没想到,一场惊心动魄的故障正在悄悄逼近。

故障发生:线上突然大面积报错

全量发布后的第三天下午,我们的监控系统突然报警:前端错误率飙升,从平时的0.1%飙升到了5%以上。

同时,客服收到了大量用户反馈:文档打不开、编辑的时候页面崩溃、保存失败、协作不同步。

我们立刻开始排查。

首先看错误日志,发现大量的WebAssembly相关错误:

  • "WebAssembly.Instance is not a constructor"
  • "WebAssembly.compile is not a function"
  • "RuntimeError: memory access out of bounds"
  • "RuntimeError: unreachable"
  • "LinkError: import object field 'xxx' is not a Function"

这些错误集中在某些特定的浏览器和设备上。

我们进一步分析,发现了一个规律:出问题的用户,主要是使用旧版本浏览器的用户,尤其是Chrome 70以下、Firefox 65以下、Safari 12以下的版本。还有一些是使用低端Android手机的用户。

但奇怪的是,我们在灰度阶段也有这些用户,为什么那时候没有出问题?

后来我们才明白,灰度阶段这些用户的比例比较小,而且我们的监控对WebAssembly错误的捕获不够完善,很多错误没有被正确上报。全量之后,用户基数大了,问题就暴露出来了。

更严重的是,WebAssembly的错误会导致整个页面崩溃,用户无法使用文档。这不是一个小问题,而是影响核心功能的严重故障。

紧急处理:回滚还是修复

发现问题后,我们立刻开了紧急会议,讨论怎么处理。

有两个选择:

第一,立刻回滚到JavaScript版本。这样可以快速恢复服务,但之前的工作就白费了,而且回滚也有风险(数据兼容性、缓存等问题)。

第二,紧急修复WebAssembly版本的问题,然后重新发布。这样可以保留WebAssembly的性能提升,但修复需要时间,用户会继续受影响。

我们讨论了一会儿,决定先回滚,恢复服务,然后再慢慢修复问题。

因为这是核心业务,影响了几十万用户,每多一分钟都是损失。先恢复服务是第一位的。

回滚过程比我们想象的要复杂。因为WebAssembly版本引入了一些新的数据格式和缓存机制,回滚到JavaScript版本后,一些用户的本地缓存出现了兼容性问题,导致文档打不开。

我们又紧急发布了一个补丁,清理了不兼容的缓存,才让所有用户恢复正常。

从故障发生到完全恢复,一共用了四个小时。这四个小时,是我职业生涯中最惊心动魄的四个小时。

原因分析:到底出了什么问题

服务恢复之后,我们做了详细的复盘,分析故障的根本原因。

经过深入调查,我们发现了以下几个问题:

第一,浏览器兼容性问题。WebAssembly虽然已经被主流浏览器支持,但旧版本浏览器的支持不完善。有些旧版本浏览器不支持WebAssembly,或者支持的版本很老(比如只支持MVP版本,不支持后续的特性)。我们的代码使用了一些较新的WebAssembly特性(比如多值返回、引用类型、SIMD),这些特性在旧浏览器中不被支持,导致编译失败或运行时错误。

第二,内存管理问题。WebAssembly有自己的内存空间,和JavaScript的内存是分开的。我们在C++代码中使用了一些不安全的内存操作(比如越界访问、使用已释放的内存),这些问题在C++中可能不会立即崩溃,但在WebAssembly中会触发"memory access out of bounds"错误,导致整个实例崩溃。

第三,异步加载和初始化问题。WebAssembly模块的加载和初始化是异步的,我们在代码中没有正确处理初始化完成前的调用。有些用户的网络比较慢,WebAssembly模块还没加载完成,用户就开始操作了,导致调用了未初始化的函数,触发错误。

第四,错误处理不完善。WebAssembly中的错误(比如C++的异常)不能直接被JavaScript捕获,需要特殊的处理机制。我们没有完善的错误处理机制,WebAssembly中出现错误后,整个实例就崩溃了,而且没有有效的错误信息,难以排查。

第五,测试覆盖不足。我们的测试主要在最新版本的Chrome和Firefox中进行,没有覆盖旧版本浏览器和低端设备。灰度阶段虽然有这些用户,但比例小,而且监控不完善,问题没有被发现。

第六,监控和告警不完善。我们对WebAssembly相关的错误没有专门的监控和告警,很多错误被归类为普通的JavaScript错误,没有引起重视。全量发布后,错误率飙升,我们才发现问题。

这些问题叠加在一起,导致了这次严重的故障。

解决方案:怎么修复的

找到原因之后,我们开始逐一修复问题。

第一,完善浏览器兼容性处理。我们做了以下几件事:

  • 在加载WebAssembly之前,先检测浏览器是否支持WebAssembly,以及支持哪些特性。
  • 对于不支持WebAssembly的浏览器,自动降级到JavaScript版本。
  • 对于只支持旧版本WebAssembly的浏览器,编译一个兼容MVP版本的WebAssembly模块,不使用较新的特性。
  • 建立了浏览器兼容性矩阵,明确支持哪些浏览器版本,哪些版本需要降级。

第二,修复内存管理问题。我们做了以下几件事:

  • 对C++代码进行了全面的代码审查,修复了所有的内存越界、使用已释放内存等问题。
  • 使用了内存检测工具(比如AddressSanitizer),在开发阶段就发现内存问题。
  • 在WebAssembly中开启了内存保护机制,越界访问会触发明确的错误,而不是未定义行为。
  • 对关键的数据结构添加了边界检查,确保不会越界访问。

第三,完善异步加载和初始化。我们做了以下几件事:

  • 在WebAssembly初始化完成之前,禁用用户交互,显示加载提示。
  • 建立了完善的初始化状态机,确保所有的调用都在初始化完成之后。
  • 对网络慢的用户,提供了加载进度提示,并且优化了WebAssembly模块的大小,加快加载速度。
  • 使用了流式编译(streaming compilation),让浏览器在下载的同时就开始编译WebAssembly模块,减少等待时间。

第四,完善错误处理机制。我们做了以下几件事:

  • 建立了WebAssembly和JavaScript之间的错误传递机制,C++中的异常可以被正确捕获并传递到JavaScript。
  • 对所有的WebAssembly调用都添加了try-catch,确保单个错误不会导致整个实例崩溃。
  • 建立了错误恢复机制,WebAssembly崩溃后可以自动重新初始化,而不是让整个页面崩溃。
  • 完善了错误日志,记录详细的错误信息、调用栈、用户环境,便于排查问题。

第五,完善测试和监控。我们做了以下几件事:

  • 建立了完整的测试矩阵,覆盖主流浏览器的多个版本(包括旧版本)和多种设备。
  • 引入了自动化测试,在CI中运行各种浏览器和设备的测试。
  • 建立了WebAssembly专门的监控和告警,对WebAssembly相关的错误进行实时监控。
  • 完善了灰度发布机制,灰度阶段就开启完整的监控,确保问题能被及时发现。

修复这些问题,我们花了将近一个月的时间。修复完成后,我们重新进行了灰度发布,这次灰度了两个月,确保没有问题之后,才重新全量发布。

重新全量发布之后,WebAssembly版本运行稳定,性能提升明显,没有再出现严重的故障。

经验教训:WebAssembly普及中的坑

这次故障给了我们很多教训,也让我们对WebAssembly的普及有了更深刻的认识。

第一,不要低估兼容性问题。WebAssembly虽然已经比较成熟,但在不同浏览器、不同版本、不同设备上的表现差异很大。尤其是旧版本浏览器和低端设备,问题很多。在推广之前,一定要做好兼容性测试,明确支持范围,做好降级方案。

第二,内存安全是重中之重。WebAssembly的内存管理和JavaScript完全不同,C/C++中的内存问题在WebAssembly中会被放大。一个小小的越界访问,就可能导致整个实例崩溃。一定要重视内存安全,使用工具检测,做好边界检查。

第三,异步和初始化要谨慎。WebAssembly的加载和初始化是异步的,而且可能比较慢(尤其是大模块和慢网络)。一定要处理好初始化完成前的用户交互,避免调用未初始化的函数。提供良好的加载提示,优化模块大小,加快加载速度。

第四,错误处理和恢复机制必不可少。WebAssembly中的错误处理和JavaScript不同,需要专门的机制。一定要建立完善的错误处理和恢复机制,确保单个错误不会导致整个页面崩溃。同时,要完善错误日志,便于排查问题。

第五,测试和监控要先行。在推广新技术之前,一定要建立完善的测试和监控体系。测试要覆盖各种浏览器、版本和设备,监控要能实时发现问题。不要等全量发布了才发现问题,那时候代价就大了。

第六,灰度发布要充分。灰度发布是发现问题的重要手段,但灰度一定要充分。灰度的比例要逐步扩大,灰度的时间要足够长,监控要完善。不要灰度几天觉得没问题就全量,很多问题只有在大规模、长时间运行后才会暴露。

第七,要有回滚预案。任何新技术的推广,都要有回滚预案。一旦出现严重问题,能够快速回滚,恢复服务。回滚预案要提前演练,确保回滚过程顺利,不会引入新的问题。

WebAssembly的价值:值得推广吗

经历了这次故障之后,有人问我:WebAssembly还值得推广吗?

我的答案是:值得,但要谨慎。

WebAssembly的性能提升是实实在在的。对于我们的产品来说,文档打开速度提升了3倍,编辑流畅度大幅提升,复杂计算的性能提升了5-10倍。这些提升,直接改善了用户体验,也让我们能够实现一些之前在JavaScript中无法实现的功能。

但WebAssembly也不是银弹,它有自己的适用场景和局限性:

  • 适合计算密集型的任务,比如图像处理、视频编码、密码学、科学计算、游戏引擎等。
  • 适合需要跨平台复用代码的场景,比如把C/C++的库移植到浏览器中。
  • 不适合简单的UI交互和DOM操作,这些用JavaScript更合适。
  • 有一定的学习成本和开发成本,需要掌握C/C++或Rust,以及WebAssembly的工具链。
  • 有兼容性和性能方面的坑,需要谨慎处理。

我们的建议是:

  • 如果你有计算密集型的需求,而且JavaScript的性能不够用,可以考虑使用WebAssembly。
  • 先从小范围开始试点,验证效果和稳定性,再逐步推广。
  • 做好兼容性测试、错误处理、监控告警和回滚预案。
  • 不要为了用而用,WebAssembly不是万能的,很多场景用JavaScript就够了。

写在最后

这次WebAssembly普及故障,是我们团队经历过的最惊心动魄的故障之一。四个小时的线上故障,几十万用户受影响,现在想起来还有点心有余悸。

但这次故障也让我们成长了很多。我们对WebAssembly有了更深刻的理解,建立了更完善的测试和监控体系,也养成了更谨慎的技术推广习惯。

技术的发展从来都不是一帆风顺的。每一次新技术的普及,都会伴随着各种问题和挑战。重要的是,我们要从问题中学习,从失败中成长,不断完善自己的技术和流程。

WebAssembly是一项很有前景的技术,它正在改变前端的性能边界。但它也需要我们以更谨慎、更专业的态度去对待。

希望我们的这次复盘,能给正在使用或准备使用WebAssembly的朋友一些参考。愿大家都能少踩坑,顺利地把WebAssembly用起来,享受它带来的性能提升。

技术之路,道阻且长,行则将至。