如题


前端天坑集锦

  • 上传组件(IE / Flash 时代)
  • 动态表单
  • 拖拽生成页面
  • 工作流引擎
  • 富文本编辑器
  • Web IDE
  • 写一个新系统,还原老系统的所有功能
  • 停机问题(别笑

怎么解决的

吃一堑长一智,不要轻易接需求,仔细评估工期


(多图预警)聊一聊当下发生的事情吧。疫情期间大家都在享受延长假期的福利,吐槽在家办公的不爽,而我们则从过年开始就一直在战斗,到现在还没有好好休息过。

先说背景,我目前在腾讯IMWeb团队,负责在线教育腾讯课堂的前端研发。

都说疫情期间在线教育是风口,我想说,打的赢扛得住也许是机遇,打不赢完全是炮灰。

  1. 先说流量

从春节假期到现在,我们遭遇了前所未有的流量峰值,虽然具体数字不方便透露,但是可以预想得到,那么多所学校在期间强制网路上课,学生加老师的数量是多么庞大。

如果说双十一是所有具有消费能力和冲动的人群冲击,那么这一次则是所有学生和老师的强制访问,访问者没有选择权,这是最可怕的一点。比双十一更可怕的是,我们没有时间准备,双十一也许可以提前几个月甚至半年开始谋划,这次的流量则完全是毫无预兆的突发性事件,要求我们在短时间内必须做出快速的决策响应

截止现在,流量高峰已经冲击三波了,每一次都是几倍的增长,流量逐渐平稳,也让我能够偷闲刷一刷知乎。。

1.1 前端考验一——主域

对于前端而言,最大的影响莫过于主域,一旦我们的主域扛不住,html都打不开了,整个全玩完。

在我们团队,主域的Nginx主要是由前端负责管理,在腾讯的运维体系下,STGW在下一层统统是交由业务来维护,运维同学完全不了解业务是如何发布和控制的。从某种程度来说,我们才是真正的DevOps,夸张一点说,运维同学与我们打交道也许仅限于机器申领与容量。

除了承载核心HTML入口,主域还承接了CDN的降级策略,防止某处运营商等问题直接导致CDN无响应,之前的教训让我们做了这层容灾。所以主域的稳定性至关重要。

所幸这里仅仅是静态渲染,抗住高并发不是太难的事情,不过Nginx对于前端的能力提出了更高的要求,对于Nginx的改动,有著严格的流程把控,务必做好充分的验证。

1.2 前端考验二——音视频直播

音视频链路对于课堂而言是重中之重,老师和学生的核心目的就是通过直播来上课,一旦音视频挂了,腾讯课堂所有其他功能形同鸡肋,这是前端第二项影响巨大的考验。

课堂前端团队针对于音视频领域做了非常多的优化,在疫情期间,音视频作为核心模块被重点关注,快速上线了快直播简化WebRTC信令分摊更大的流量HLS降级WebRTC混流开关等等。

由于我不主要负责音视频开发,音视频所做的工作远远大于这里提到的,我们组负责音视频的小姐姐已经不知道通宵了多少回,十分辛苦~

1.3 前端考验三——SAS数据管理配置平台

这个平台承接了所有的运营、类目、产品配置,对接CKVCDB平台做数据存储,对接云COS做文件存储,通过JSON Schema配置出数据服务,同步ZK节点供后台查询。

目前成百上千张表都在这个平台上,一旦挂了,后果不可预料。这个平台整体运用了GraphQL技术作为访问查询,属于前端团队的第二大考验。

得益于SAS平台最初设计的简洁性,监控非常的充足,扩容也较为容易,非常轻松地挺过流量高峰。

1.4 前端考验四——IMPush

IMPush是前端团队自研的消息通道,承接了所有socket消息转发。这个系统承接了聊天区所有的消息服务,与后台保持全双工长连接通道,利用Redis进行数据缓存,整体agentcenter都会受到比较大的压力挑战。

这个服务如果挂了,所有的聊天区、弹幕都将面临瘫痪,影响也是非常大的。

同样的手段,借助于现有的负载均衡L5体系和资源,需要抗住巨大的并发量。

1.5 前端考验五——监控、日志与灰度

我习惯将监控、日志和灰度称为前端三板斧是衡量一个前端团队是否专业的重要指标。很多前端并不注重这点,最多只有一个脚本报错的监控,最基本的测速返回码等监控都没有。

单论脚本报错监控,我们其实已经准备三套方案,BadJS+Sentry+FullLink,在超高的访问量下,可以预计所有的平台基本上都会挂,而脚本监控对于前端来说是非常重要的,三套系统的降级方案保证了我们在外网出问题的时候第一时间定位到问题所在,快速响应bug。

日志上报是前端最容易忽略的,当用户量多了你就会发现,很多问题是没有脚本报错的,如果只依赖于报错监控,很多外网问题两眼一抹黑,无从下手了。作为专业的前端,我们需要全链路的日志定位。

前端团队在这里借用开源的ELK方案,与后台全链路系统打通,在基础上通过DC通道上报落地,Agent代理不同监控系统,做成了上报中台方案,在Kibana系统上统一查询和定制报表。

灰度方案其实相对是比较难做的,最简单的是按照机器灰度,但这种方案在实际环境中基本上是不可用的,对于一个需求来说,如果同时修改了老页面和新页面,会导致用户前后访问不一,甚至出现404情况。更好的方式是按照登录态灰度,这时候我们需要统一接入层,Nginx、TSW都是可以的选择,在白名单内用户进行灰度。

但针对CDN,我们无法架设统一的Node服务来接入,这时需要考虑离线方案,制作离线包以及PWA管理平台,利用离线版本进行登录态灰度,可与Node服务保持一致。

有了这三点的保障,我们才可以做到心中有底,数据支撑指导我们的行动,来抗住高并发流量。

1.6 前端考验六——后台保护

在这场战役面前,前端不能自己独善其身,不仅仅要做好自己的分内事,更要帮助后台团队共渡难关。

首先,在核心场景下,按需屏蔽不重要的介面,帮助后台减轻压力,可根据后台的负载情况动态调整。

其次,前端自己要保持柔性,除了核心CGI外,其他介面无论是超时还是返错,都不要影响页面核心功能的正常运行,这对前端的代码提出了很高的要求,所幸平时团队CR习惯养成良好,对介面的异常处理也做的比较完善,只是模拟介面测试验证花费了一些时间。

2. 再说需求

你以为上面就是全部了?Too Naive!上面的几点只是挤出时间去做一些调整,重头戏还在于极度紧张的业务需求。

腾讯课堂之前的toB部分针对的是开课机构、个人老师,现在是学校教务、学校老师、学校领导、教育局领导,老板们直接重点关注,可想而知产品的压力有多大。

我们在两天内就推出了腾讯课堂极速版(https://ke.qq.com/s),支持老师10s开课,随时随地开课,目前已经迭代到了第4版。

众所周知,对于一个系统而言,由简入繁易,由繁入简难。腾讯课堂有著一套复杂的B侧管理体系,极速版要将这一切推翻,让老师极速开课,学生极速上课,这是多么困难的一件事情。课堂在这么短时间内拿下极速版的版本发布,体现了极强的开发战斗力。

在此期间,开发承接的工作量大约在平时的五倍左右,不仅仅需要通宵达旦,更需要快速响应,课堂前端每日均发布版本达到10次以上,如何在高频次的发布中不影响质量也是巨大的考验。

要保持高强度的战斗力,对于团队的基础效率工具建设提出了很高的要求。

2.1 快速开发需求——Nohost方案

Nohost方案对于测试环境多需求并行开发做了很好的支持,不仅支持前端分发,还利用docker打通了后台环境。

开发很便捷使用分支部署,产品可以在家切不同的需求环境体验,测试也可在家访问不同环境进行测试。

2.2 快速开发需求——Tolstoy方案

Tolstoy打通了后台的PB、CGI,让后台定义的协议能够自动生成文档、Mock、声明文件、测试用例等等,尤其是TS的自动生成,为开发提供了很大的遍历,让我们的TS项目开发的更快更好。

2.3 稳定上线需求——Thanos方案

Thanos方案是我核心主导的,它解决的是发布链路的问题,对于大公司而言,发布除了CI/CD之外,还有一些其他的额外流程保障,形成发布闭环。

如果没有一个系统承载流程,这些杂乱无章的步骤可能成为发布事故的罪魁祸首。

另一方面,分支模型也是关键因素,采取分支发布的策略带来的好处很多,但缺点也有,其中很重要的是分支准入问题,以及发布覆盖问题,这两个普遍性问题在Thanos方案得到保障。

2.4 个人技术能力

在高需求量,deadline又非常紧的情况下,对每个人的技术能力要求很高。腾讯课堂的前端复杂度还有很重要的一点体现在端上,老师端、学生端、机构端、APP端、PC端、小程序端、微信公众号、QQ公众号、题库、直播间等等等等……,这些端和项目可谓是眼花缭乱,数不过来。

很多项目历史悠久,包含了众多技术栈,从古老的FIS、QQ客户端内嵌、jQuery,到React、TypeScript、RN、音视频等等,切换一个项目,如同换了家公司,需要重新适应技术栈。

在人力不足的情况下,每个人都要去应对自己不熟悉的领域,可能你还没搞清楚什么是HLS就被拉去做音视频,或者完全没接触过fis的情况下去熟悉整个项目的构建打包流程,这对于个人快速上手能力和编程速度质量都提出很高的要求。

另一方面,文档在这一刻发挥出应有的价值,一般团队不怎么注重文档建设,一来写起来废时间,二来对于晋升和成长没什么帮助,看起来完全是利他性质,但实际上是互利。这时团队的价值观和管理者就非常重要了,文档的程度可以从侧面反映出团队的管理水平。

3. 小小成果

在大家共同努力下,腾讯课堂获得了更高的曝光度和认可度,也算是对我们付出结果的肯定。

武汉90万中小学生开课,73万人选腾讯?

mp.weixin.qq.com图标

最后,回归正题,前端的复杂度也许很多,比如之前我参与的CPU负载过高问题排查,用尽手段定位一个月之后发现是一条正则语句引发的,这种性质的复杂属于特定场景下的复杂度。而我今天提到的「复杂度」则比较普适,所有团队都存在面临这种场景的可能性,而对于每个团队而言,我认为没有一个团队会觉得应对起来很简单。更多需要的是公司资源调度+团队技术积累+个人能力的配合。

成长最高效的方式,不是一个人单枪匹马孤军奋斗,而是和大家并肩作战享受狂欢。

真正复杂的需求,个人的力量是有限的,如何协调整个团队的力量更为艰难。当团队在技术视野、技术方向上有前瞻性,沉淀性,个人不仅仅是埋头写业务时,是团队在推著个人成长,在高手云集的团队中保持核心竞争力,才是个人成长最合适的方向。

最后,打个小广告...如果想要搭上在线教育的班车,与whistle作者、nohost作者、badjs作者、stone-ui作者等一起共事,快速成长,IMWeb团队欢迎你,请将简历私我~


本文作者 @朱坤坤,来自云音乐前端团队

泻药。

背景

19年初,答主在公司部门内部上线了『前端性能监控平台』。本著先上个小流量项目试试水的心态,就联系了一款日 PV 量级在『百万』的前端工程做了接入。

在应用接入最初的几小时,答主观察了一会儿 Node 机器的 CPU 开销,满意地笑了笑,对自己说:「嗯,还可以,果然是小流量应用。」

遇到的问题

但随后没过多久,手机报警简讯就飞来了。报警的伺服器并不是刚刚的 Node 集群,而是『时序资料库』InfluxDB。

观察了下机器的内存与 CPU 开销走势,可以发现:应用接入后几小时内,机器的内存持续在上涨,最终造成 OOM,服务不可用。并且在管理后台上触发的一条 SQL 查询,几乎会耗尽整台资料库机器的 CPU 资源。

当时答主内心想的是:「完了,开发了几个月的产品, 上线后,尽然是这样的,年终奖没了不要说,明年回来,肯定要重新找工作了,我不仅坑了自己,还坑了我的主管」 ,当时的内心可谓极度惶恐。

解决问题的方式

经历过一番内心挣扎后,觉得这样惶恐下去,解决不了问题,然后就冷静下来,尽可能地去查找时序资料库 InfluxDB 相关的分享文章,希望能从中找到一些性能瓶颈相关的资料。幸运的是,在饿了么团队的一次分享中,我找到一点引起问题的线索,然后开始从资料库表结构设计、计算查询优化等方面著手,逐步地进行了优化。并将整个优化过程以文档的形式做了记录(数据已脱敏,感兴趣的同学可以看下),在组内周报中做了分享。

后续

在后续持续关注 InfluxDB 的过程中,我也在网路博客中,发现了一位 InfluxDB 专家,深入浅出地介绍 InfluxDB 引擎的实现原理。非常幸运的是,这位专家正好是我们网易的同事。

之后我梳理了我们『前端错误监控平台』当前的技术架构、自己在时序资料库方面的优化经验、遇到的难点与痛点等。带著这些问题,与专家约见交流。

回来之后,收获非常大,为了避免自己遗忘,随手就写了一篇《前端性能监控-存储与计算架构展望》,之所以是展示,是因为在当时,由于一些原因没有办法很快落地。

但这次交流,为我们后续平台的技术架构升级,埋下了基础,在19年里当我们再一次遇到存储与计算瓶颈时,帮助我们逐步地完成了技术升级。

总结

  • 遇事不要恐慌,要冷静下来,逐步分析。
  • 尽可能多地参考业界的文章、分享。
  • 解决问题后,要及时进行梳理总结。
  • 积累一定实践经验后,也可与领域专家交流,进一步提升自己的认知。

以上。


最近有点魔怔了,看到问题第一反应是:如何定义「复杂需求」。

咱就不讨论什么是复杂需求了,列举以下我最近两年里做过的几段有挑战性的工作:

  • 可视化网页编辑器
  • BI 系统
  • 后端代码静态分析工具

后端代码静态分析工具

这段经历距离最近,印象还没有模糊,就只讲它好了。

大背景是我们正在推广 OpenAPI 的那套开发工作流。

大体意思是 API 是我们大家的,不是后端代码的副产物。因此我们将 API 独立出来形成契约,让前端、后端、QA 都可以围绕契约各司其职高效协作,提升 API 的可靠性。

到了实施层面,我们面临的第一个问题就是,如何让契约发挥效力,而不是一纸空文。

社区中最常见的方案是用 Schema 自动生成 API 的 entrypoint 介面,所有的代码实现都会在介面层面被限制,确保返回的结果是符合定义的。该方案效力很好,但是代价很大--侵入后端代码太多,对现存 API 的改造较多,高成本高风险,立刻就被否定了。

紧接著讨论的是在 API Gateway 上拉 raw response log ,定期检查,发现问题及时抛出。现在想来我提出的这个方案有点蠢,滞后于错误的发生:错误出现了,过几个小时,它才不疾不徐慢慢悠悠抛出警告。基本就只是个问责机制,对于解决问题不太正面。

接下来就是呼应标题了,我们决定对后端代码做静态分析--后端语言是 Java ,可以分析入口函数的返回值类型,难度不大,收益也不错。

好的,终于开始写代码了。

作为一个从没学过编译原理的假科班,我其实并不太慌。毕竟前端圈子玩 AST 不是一天两天了,虽然没接触过相关项目,但概念总归是知道一些的。

整理思路

工程师要解决一个领域的问题时,往往需要掌握以下几点:

  • 领域知识
  • runtime api
  • 常用范式
  • 编程素养

在我们熟悉的前端领域,这几点分别可以对应为:

  • Web 基础
  • 浏览器 API
  • 掌握 React、Vue 等常用框架及其对应的范式
  • 数据结构、演算法、抽象能力

而我面对的这个需求,已经掌握的情报是:可以将 Java 代码转化为 AST ,它就是一个树的数据结构,我可以遍历它来获得代码的语法信息。

至于这个 AST 怎么来,当然是调包啦。

通过知乎搜索,我找到 Antlr 4 这个由 Java 编写的工具,它可以根据语法定义生成对应语言的解析器,并提供了 JS Runtime ,还提供了 Listener 和 Visitor 两种模式,方便用户高效得浏览 AST 。

在实际的摸索和学习中,我熟练掌握了 JS Runtime API 的使用,也大体了解了 Java 语法树的关键字和规则。范式方面我选择了简单易上手的 Listener 模式。

至此,前三个要素就齐备了:

  • 知道 AST 是什么、Java 语法树有哪些关键字、大致规则
  • JS Runtime API
  • 掌握 Listener 模式

解决问题

已经学会了解析处理 AST ,接下来就该解决问题了。

这时候思路已经很清晰了,分为三步:

  1. 找到入口函数,获取 path、method 等元数据
  2. resolve types
  3. 和 Schema 中定义的类型做比对

这里 1、3 两点都很简单,值得一说的是 *resolve types*

可以分为三个小点说一说:

  • 中间 DSL
  • 范型解析
  • 类型容错和扩展

中间 DSL

将 Java 的类型解析为中间 DSL ,再拿中间 DSL 去和 Schema 做比对是一个非常符合直觉的做法。一定要说为什么不直接解析为 OpenAPI 规范的 Schema ,我觉得可以讲出以下几点:

  • OpenAPI 的 Schema 不止有类型,还有一些相对于 Java 类型来说冗余的东西,如 number 类型有最大值最小值。
  • OpenAPI 的 Schema 升级之后,我也得跟著升级,在后端使用的 Java 版本几乎不变的情况下,这是不应该发生的。
  • 有一天 OpenAPI 也可能被取代掉。

于是我定义了如下 DSL 来表示 Java 类型:

// 基础类型
string, integer, number, any, null, boolean, void

// 复合类型
// object
{
str: string,
num: number
}

// array

// string[]
[string]

// {str: string}[]
[{ str: string }]

范型解析 类型容错和扩展

这两点拿来一起说,算是我在写工具过程中碰到的有一定抽象高度的环节。

有一些类型并不在该 Repo 的代码里,而是来自 jar 包。jar 包是二进位的,我手里的工具一时半会处理不了。即使花精力处理了,收益也较低。因此,我需要处理一些类型不存在的情况。

还有一类情况是,Java 代码中类型被序列化为 JSON 是由第三方库完成的。这个第三方库有自己的转换规则,我既需要适应这个库对类型的处理,又不能完全针对这个库写特化的程序。因此,我需要加上一些扩展能力。

如下 Java 代码:

@data
public class CommonRet& implements Serializable {
private String code = SUCCESS;
private String message;
private MessageDetail messageDetail;
private T data;

public CommonRet() {}

public CommonRet(T data) {
setData(data);
}

}

我需要面对如下情景:

  • CommonRet& 中的 long 直接映射为 DSL 中的 number 基础类型
  • CommonRet& 中的 Long(注意首字母大写)也映射为 number 基础类型,即使 Long 本身不是 Java 的基础类型
  • CommonRet&&> 这里要处理好范型
  • CommonRet&&> 的范型解析应该在 List& 处结束,把 List 映射为基础类型

为了达到如上列举的效果,我做了如下设计:

定义一系列终点类型的函数,该函数直接输出 DSL 定义的类型:

const finalResolvers = [
String: () =&> string, //String =&> 『string』
List: t =&> [t] //List& =&> [string]
]

终点类型可以跟随 DSL 随时扩展。

解析范型一定是一个递归的过程,递归的终点即当前 Symbol 是 finalResolvers 的一员:

function resolveType(type) {
const [symbol, ...params] = handleType(type) // List& =&> [List, String]
if (finalResolvers[symbol]) {
return finalResolvers[symbol](...params.map(param =&> resolveType))
}
}

这样,就成功建立起一个解析的核心机制,将 Java 代码中的 Symbol 递归解析到我们定义的终点类型,终点类型映射到 DSL 中定义的类型。并且我们可以轻松地随时扩展这两种类型。

例如我们已经有 boolean 作为终点类型,代码中的 boolean 都会转为终点类型 () =&> boolean 。有一天突然发现有一个未定义的新类型:Boolean ,我们只需将 Boolean 加入扩展即可。

收尾

上述经历不是在告诉大家「xxx不过如此」,我只是粗浅入门解决了一个非常简单的问题。

从中可以窥见的是,面对未知领域时,存在一些通用的解决问题的办法。而我们在学习工作中磨练起来的编程素养,将是最值得依靠的保障。


看了各位大佬的回答,我决定不把我的需求说出来,仿佛我是一个切图仔,连切图都切不好的仔


推荐阅读:
相关文章