前端⼤规模构建演进实践

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"作者:杨伟伟 ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"掌门教育效能研发前端工程师","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"掌⻔教育⾃动化构建历程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在业内前端构建,⼀般分为三种:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"⼿动触发构建:这个阶段⾮常原始,需要我们⾃⼰在本地进⾏ git pull/npm install/npm run build 等 等操作,也容易出现问题;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"虚拟机 Jenkins 集群分布式构建:通过 Master 将任务分配到对应的 Slave 机器上执⾏构建,能极⼤ 的均衡资源,利⽤性能,同时解放双⼿;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"容器集群构建:容器构建,镜像发布,可以进⼀步的节约资源;","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不过掌⻔教育在 2019 年之前,前端研发更多是在本地进⾏构建,再通过运维的脚本来进⾏部署,也容易 导致出现⽣产故障。所以我们收集反馈,结合实际情况,开发出 v1.0 构建模式,也取得了很好的成果。但 并没有以此就认为⾼枕⽆忧,也对很多痛点进⾏持续的优化,最后迭代出 v2.0 的⽅案。在这个过程中,前 端业务壮⼤,CI 构建经过 400+ 多应⽤,每周 2000+ 次构建,300+ 次的⽣产发布的⼤考,持续的成 ⻓。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c9/c91afc131ed8c9c9064583281ea862fa.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"v1.0 ⾯对的挑战","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"v1.0 前端构建状况,⼀种是通过 webhook 来触发流⽔线构建,第⼆种是通过在 cd 上新建构建单来触构 建。如果构建任务⽐较多,按照单台机器的是远远不够,在这种情况下就需要借助 Jenkins 的 Master/Slave 的主从模式,来解决服务器的资源压⼒。让 Master 的服务器来进⾏调度资源,指定空闲 Slave 机器进⾏构建。当 Slave 机器上构建任务满了,构建任务继续在 Master 排队池中继续等待,等 Slave 空闲后,再进⾏分配。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/99/991f1b57695fda65f11a699fca11573c.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"存在的挑战","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"v1.0 不是最好的⽅案,同时暴露出第⼀次构建慢、错误⽇志反馈不明确等问题,另外⼀点就是 job 维护困 难。要解决这些问题,就需要重新开始,重新设计。⾸先就是 JOB 维护困难,v1.0 的任务模式是多个应⽤对应 1 个 job,这就导致⼏个问题,如果 job 发版 导致挂了,影响到全部。假如需要复⽤该 job,进⾏定制化开发也⽐较困难。其次第⼀次构建慢的问题,更多在资源调度上和⽆法复⽤ Workspace,根据之前的资源调度模式,当我们 把任务分配到 A 机器,该任务被执⾏成功,那么下次的任务也会⾛⼊到这台 A 机器。以此观察,就会发 现⼤部分任务都会优先去抢占 A 机器。这家就导致了⼏个问题:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"资源调度不均衡;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"npm 缓存越来越⼤;","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最后是⽆法复⽤ Workspace 模式,在 v1.0 情况下,不复⽤ Workspace 模式是会带来以下优势:保证 node_module ⽆污染问题,同时也避免了 npm run build 的各种因为 node_module 包污染的问题,导 致的意外错误。所以在 v2.0 就需要应对污染的问题。同时也要考虑在复⽤ Workspace 后,如何最⼤化 的利⽤其特点,⽐如,从 node_module 缓存、npm install 跳过等。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"v2.0 优化⽅案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"资源调度","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/67/672af85467c83b32452b550651ed8c67.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"⾸先需要对资源调度进⾏优化,那就需要重新设计,把⼀组机器分为多个切⽚组,每个切⽚组调度顺序不 同。当应⽤触发构建时,分配对应的 key值:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" 1 AppNodeKey = AppId%nodes \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再根据划分的 key,寻找对应的机器组,如 [0,1,2,3,4],构建任务去寻找 0 号机,寻找对应的 AppId 的 Workspace ⽬录地址去执⾏构建任务,假如任务被占⽤(默认是 2 个任务,这样可以优化资源不会被⼤ 量任务抢占),会再寻找下⼀台机器,这样机器资源调度就会均衡化。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"构建 job 流⽔线化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ca/cac1ba78c29fdd45c492d8e9793d2033.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们对不同的⼯程项⽬进⾏了模板化,⽐如 PC项⽬、H5 项⽬、游戏项⽬、hybrid 项⽬等等,在模板基 础上,我们⼜封装出来打包流⽔线模板,这样的好处是,我们可以⾃⼰去针对各个类型的⼯程模板做⼀些 定向的配置优化,⽐如说我们的游戏类型项⽬,我们去做⼀个构建、打包,我们就可以在对应的开发组件 库依赖这⼀块,做⼀些对应的缓存、通知、报告等等。流⽔线同时也带来了⼀些好处:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第⼀,我们把构建任务进⾏了⽣命周期化,git cone、npm install、npm build,把这些阶段全部进⾏拆 开,让整个任务流程颗粒化,这样的好处是,我们可以在每⼀个颗粒之间找到优化空间,⽐如是不是可以 不进⾏npm install,⽐如上传制品仓库的逻辑优化等等。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第⼆,可以指定DSL,我们可以实时监听打包排队的情况,在资源调度层⾯做⼀些优化。同时可以做⼀些 埋点进⾏采集数据,给后续进⾏深⼊分析。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三,我们甚⾄可以在构建、打包过程中,做⼀些交互的相关操作,⽐如,我们打包⼀个 h5 项⽬,需要测试同学来进⾏审核,只有测试审核通过完之后,才进⼊到下⼀流程,下⼀个流程可能是进⾏ UAT 打包、 ⽣产打包等等。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第四,针对项⽬依赖拉取,最开始的时候,我们做的是全量的拉取,我们现在可以优化为增量拉取,这 样,服务器的压⼒会减轻很多","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"错误治理","attrs":{}},{"type":"text","text":"不管是在本地还是 cd 平台上进⾏构建,也容易出现各种意想不到的错误。⽐如开发的疏忽,流⽔线的 git commit 未经过验证进⾏提交代码,都可能在 npm run build、或者 npm install 这两个阶段报出不同的 错误,所以就需要对⽇志提示进⾏分级集,划分为两种类型:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"warning 类型:没有 package-lock.json 提示,不影响到任务构建;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"error 类型:导致 job 任务退出,⽐如依赖包未找到等; 4","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们对 npm install、npm run build,及构建的各个阶段的观察,可以把失败归纳为 4 个触发 warning 或 error 类型:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"语法错误:代码冲突...","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"程序异常:内存溢出…","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"Install 失败:未找到安装包…","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"配置错误:构建未按照规范输出…","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遵守“观察⽇志-沉淀规则-修正反馈准确率”规则,来沉淀⽇志规则。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cc/cc2be5bcb9675c122e25aabb713be213.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"npm install 跳过","attrs":{}},{"type":"text","text":"在复⽤ Workspace 的情况下,已经 install 好的 node_modules,就没必要进⾏⼆次重复 npm install, 就需要考虑只有依赖进⾏变更时,再重新 npm isntall。同时也容易带来问题,node_modules 污染,我 们采取了多种⽅式来避免 node_modules 被污染掉。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"cache 定期检查;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"切换 node 版本时,执⾏ npm rebuild,重新进⾏编译⼀些需要依赖 gc++ 环境构建的包,如 node- sass;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"CD ⻚⾯增加⼿动清理 Workspace 选项;","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"带来的收益","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"⽬前,我们对⽐以前的 v1.0 ⽅案,整体有了 20%+ 构建速度的提升,这对我们团队来说,也算是⼀个不 ⼩的正向激励,说明我们之前努⼒的⽅向是正确的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"迁移过程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 v1.0 迁移到 v2.0,需要考虑如何进⾏平滑迁移,我们基于以下来进⾏迁移:1:每个应⽤建⽴独⽴ v2.0 Job 任务,⽅便快速变更及排查问题;2:⻚⾯上⽀持快速回滚到 v1.0;3:选择 git commit、node 版本等信息保持不变,⽆感;","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好的架构不是设计出来⽽是演进出来。在未来,构建任务可能会越来越多,项⽬也越来越复杂化,我们就 会考虑容器化⽅案,根据实际情况去考虑,容器构建,镜像发布,尽可能的节约资源。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fb/fb82a9fc5078ff5c4fc1a3ae0f590996.png","alt":"在这里插入图片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章