本次演讲由百度资深前端工程师张立理为大家分享百度基于FAAS服务如何构建从NPM到CDN的全自动同步机制。本文主要介绍了云服务存在的问题及其解决方案,在包占用内存、时间较大时进行同步操作的细节问题及其处理方法。
演讲嘉宾简介:张立理, 百度资深前端工程师。
本次分享主要围绕以下四个方面:
一、背景
二、目的——从NPM到CDN全自动同步
三、为何基于FAAS?
四、云服务存在的问题
1.为何需要CDN?
百度有很多形态的产品和项目,其中一部分产品不使用NPM,如一些简单产品、简单内部系统甚至没有FE的团队研发系统。但这些产品需要依赖一些通用包。还有一部分产品会使用webpack构建,但是如果将所有依赖都放入webpack构建,bundle会比较大。如果能在HTTP/2的加持下,拆分大bundle,分散引入多个体积合理的资源,可带来性能收益。同一个URL资源,共享使用者越多,缓存效应更强。
2.CDN服务流程
基于以上原因,可以使用一个CDN来服务。但是当需要直接访问NPM上的某个文件,需要先开通CDN、下载NPM包、 解压、上传至CDN、获取URL、最后修改HTML。上述流程非常繁琐,对很多开发者而言非常麻烦,不如将代码直接放进代码库操作上线。而将代码直接放进代码库会带来几点问题。首先,代码存储中会有许多重复代码文件。其次,失去CDN的性能优势。第三,不同业务不能使用同一个CDN的URL资源,无法共享CDN缓存。而工程师的需求如下图所示,当任何一个开发者发送一个包到NPM,该包最好能够直接在CDN上使用,即自动同步到CDN。并且产品开发者使用时不需要主动到CDN上寻找,而是有可预测的、有规则的URL资源。
3.社区现有方案
目前,最典型的产品是UNPKG,其背后为Cloudflare CDN,较为常见的有jsDelivr,其背后为Cloudflare、Fastly、NS1、StackPatch等基础设施。以上方案被应用于国内产品时仍存在一些问题。首先,国外网络存在延迟、连通性问题。第二,这些产品按需同步,首次访问速度较慢,耗时可能达分钟级。另外,内部除了访问需求以外,还存在扩展需求。例如需要分析依赖的代码,对其进行自主存储,指纹库建设,漏洞分析等。
1.逻辑
在国内,CNPM和 Taobao NPM在官方NPM基础上提供了两个接口。一个接口可以拉到所有的包,另一个接口可以拉到一定时间内检查到的增量包。如果用户需要进行全量操作,需要拉到所有包名,请求每一个包的版本列表,下载每个版本的压缩包并进行解压后上传到存储。如果需要进行增量更新,首先记录上一次更新的时间,获取增量包,比对增量版本。然后同样下载每个版本的压缩包,进行解压后上传到存储。
如下图所示,上述逻辑实现有三大核心部分。中间部分为所有需要的函数,即逻辑。包括全量同步,增量同步,从版本列表中过滤出需要更新的东西,然后同步一个具体的包,再同步一个包下面的版本。相对应地,NPM镜像提供全量列表、增量列表,获取一个包的元数据,获取一个版本的压缩包等。左侧是存储层面,需要已有同步的状态、每一个包的etag,用于确认是否更新,存储压缩包,存储包的元数据,存储解压包后的每一个文件,可能还需要处理一些加锁的机制。
三、为何基于FAAS?
1.Why FAAS?
NPM同步CDN过程中为何要使用Serverless方案,而非直接使用一台服务器操作部署?
调用量大:NPM现有约110W包,每一个包分不同版本,共有千万级别的版本需要同步。单机并发能力不足,需要Serverless的弹性能力。
包更新随机,负载高低峰差异大:全球开发者中欧美国家开发者较多,在欧美地区晚上八点钟左右包的更新量较多,其它时间更新量较少。如果开发者不按需使用函数,而始终使用一台服务器进行处理,独占设备利用率就会比较低。
包更新是天生MapReduce结构:每个包分为多个版本,版本下面分为多个文件。使用Fork机制可以将一个任务变成几十、几百、几千个小任务,使用函数解决问题。
持续运行不可中断:在增量更新的情况下,如每五分钟做一次增量更新,可能只需要处理20个包,负载正常。如果更新同步挂掉了,而五天后才被发现,则此时需要重新同步五天的量,负载将会非常大,因此需要保证其持续运行不可中断。如果使用单机,需要做监控、重启等能力,系统和任务运维、监控运维成本高。下图曲线为典型函数的整体弹性情况,每分钟调用量约5万到8万次,与普通请求不同,每一个函数少则二三十秒,长则可能三五分钟。
2.若只使用Serverless
能否放弃其它方案,只基于Serverless进行操作?目前,只基于云服务构建有一系列优势。如完善的执行环境。Serverless的任何厂商都会直接提供完整的Node环境。第二,可以自动化部署与更新。第三,无限存储与计算能力,因为云服务本身具备弹性。第四,优秀的云带宽和延迟。此外,只基于云服务进行构建还是存在一些问题,主要存在于开发环节。首先,测试需要本地构建函数发布到云上,再调用云的函数的测试,测试流程冗长,并且不便于本地调度。同时,开发的所有东西放在云上,测试也运行在云上,虽然前100万次调度不收费,但是已经在云上大量使用函数的情况下,显然测试的时间、内存、CPU的消耗、调用次数等都会消耗云服务费用。
3.抽象函数与存储能力
基于以上原因,开发者希望改良本地调试和测试的过程。
抽象函数能力:在函数基础上进行抽象。抽象函数能力核心就是存在一个函数,无论是在云上、本地还是其它地方都能够调用。抽象能力的基础是DISPATCHER调用器。DISPATCHER能够防止开发者被厂商锁定,如调用阿里云、百度云、亚马逊云的接口是不同的,可以使用DISPATCHER进行归一化。另外也包括云、本地和不同本地实现之间的统一。作为调试使用的基本上有两种,一种是LOCAL,即直接在本地调用函数,一种是WORKER。使用WORKER调用函数的优势是如果卡住了,WORKER可以杀掉,而如果LOCAL调用卡住,上下游可能都会卡住。
抽象存储能力:云上有完整的存储方案,如同步文件存储在对象存储中,其它元数据信息存储在表格存储中。将二者结合在一起就是一套基于云的存储接口。在本地测试时会基于本地硬盘做一套实现。可与云的存储接口统一为一套存储接口。同时还需要对远程NPM镜像进行处理。在国内可以使用淘宝镜像,但是存在ip被加入黑名单的风险。因为Serverless弹性较强,当一个高峰过去易被自动化屏蔽ip。因此百度考虑分流压力,例如一部分可以到官方镜像,一部分可以到CNPM,一部分可以到淘宝镜像。对于镜像做抽象,淘宝镜像和官方镜像返回的结构会有所不同,中间需要做屏蔽。
基于以上抽象能力,操作既可以运行在函数云上,也可以运行在本地或者运行在任何一台虚拟机上。对应配置如下图所示。在不同的函数中,用户可以使用环境变量说明所使用的实现,也可以用默认实现。只要在本地覆盖了环境变量,就可以有一套完整的、可直接本地调用的版本。若使用默认的环境变量,则可以完整使用一套Serverless函数的版本。
Serverless服务整体非常完善,然而云服务上的用法并不经典,存在如下问题。
1.事务
缺乏事务性:同步过程的上下游有串联关系。从包到版本,版本未结束同步,则包不能完成同步。从版本到文件,文件没有同步上传结束,则版本不能完成同步。但是文件、版本、包的同步等过程是在不同函数中进行。FAAS和对象存储异步,函数调用相互独立,缺乏事务性。例如当版本同步失败时,并不能知道包同步是否成功,因为包同步操作不能等待版本同步完成再进行,只能将其发出去,假设会自行同步。
解决方案:异步环境下事务性问题不易解决,只能在一定程度上进行处理,如下图所示。使用单个文件作为锁,锁文件内写入启动时间、重试次数等同步相关信息。例如,要求当一个包的所有版本同步完成才能说明包同步完成。因此在开始同步包时,在每一个版本中加入一个lock文件作为锁,当一个版本同步完成后将删除lock文件。若lock文件最终未被删除,说明该文件所在版本未完成同步。并且由于难以判断该版本下已完成同步文件数,因此再次执行时需要重新同步该版本下所有文件。每一个锁有超时机制,设定时间为10分钟。若某版本启动同步3分钟时lock文件依然存在,可能是相关函数仍在执行。若启动同步10分钟后锁文件依然存在,则说明锁无效,重新触发。该方案仅支持简单不重复同步,不妨并发。要求同步过程幂等,少量重复执行并不会存在并发冲突的数据不一致性。若同步过程不是幂等的,需要更好的解决方案。
2.长任务
长任务执行困难:与访问数据库、请求web接口等短时间操作不同,同步可能是长时间操作。例如所需同步的包很大、文件很多时,因此同步过程占用较多内存和时间。多数任务所需内存超过1G级别。而云函数时间与内存等资源有限,例如百度云最大占用内存为512MB,最长执行时间为5分钟。因此长任务执行存在困难。
解决方案:首先尝试复用并发Map切割任务至多个函数。将大型任务切割为多个小任务送往下游函数。以同步包为例,需要获取包的版本列表。如果在一个函数中依次for循环处理所有版本,函数时间不足。因此可尝试将每一个版本作为一个任务送往下游函数,同时关注函数利用率。假设每一个函数时间为5分钟,而处理一个版本只需要3秒或者10秒,函数使用效率低且过多函数调用将导致收费高。因此采取chunk模式,根据实际运行时间等统计情况给下游每一个函数分发多个版本。例如一个函数5分钟可以处理200个版本,则以200个 版本为一个任务单位送往下游函数。
第二,同步过程涉及从镜像源下载压缩包、计算并校验SHA值、统计文件个数、解压文件、全部上传至BOS(百度对象存储)等多个步骤。上述步骤如果依次进行,进行其中一步时其它资源均处于闲置状态,总耗时长。为解决该问题,不再基于任务步骤进行编程,而采取经典流式编程。即开始下载镜像源或者下载一部分后,开始同时也进行后续计算校验、统计、解压、上传步骤。流失编程可以最大程度上节省时间与资源,提高效率,使更多、更大的压缩包能在指定时间内完成处理。但是流式编程较函数任务等待编程更为复杂,处理更加困难。好在由于同步任务是幂等过程,仍适用流式编程。对应代码如下图所示。获取response后将其写入文件系统。若压缩包解压后文件很多,一边解压一边上传,会因为上传时间较长导致网络流超时,以文件系统作为中转,一边写进文件系统,一边从另一端读取文件流,同时将其分出3个子流进行计算校验、统计、解压、上传工作,则可以有效避免网络流超时问题。
3.频控
云函数追求高资源利用率:云函数追求高弹性、高资源利用率,可以瞬间达到非常高的执行QPS。但是远端外部服务不能承受过高的执行频率,因此需要进行任务频控。例如要求2W个任务需要在30分钟内执行完成,而不能在5分钟内就执行完成。
解决方案:频控需要通过sleep函数等函数等待执行。而函数在云上运行时即使在等待时间也需要根据其配置的内存按秒收费,因此希望能在高弹性环境下进行频控的同时尽可能减少函数无意义等待时间,减少浪费,节省费用。下图所示为经典频控示例。处理某项任务时设置timeout时间,timeout时间结束再处理下一个任务。该机制存在一些问题。第一,如果处理10个任务,前9项任务各等待30秒是正常的,但是最后一项任务处理后无意义等待30秒是浪费的。第二,如果5分钟函数时间内只能处理5项任务,如果处理完第5 项任务再等待30秒也是无意义的。因此需要优化频控方案。
如下图所示,在函数开始执行时记录时间,每一项任务完成后判断剩余时间是否足够进行等待。若剩余时间不够尽兴sleep等待,直接退出函数执行。另外,最后一项任务单独执行,不需要进行等待,处理完成即退出函数执行。
4.自循环
永久执行任务需特殊处理自循环:同步函数过程需要不断检查是否有新增,是永久执行的任务。而云函数机制是仅在需要时进行调用。因此需要特殊处理自循环机制,使同步操作持续不断地进行。
解决方案:第一,尝试让函数复活自己以保持永久持续执行。在一次完整的函数过程中,首先要预留足够时间供存储当前状态供下一次执行使用。例如需要处理1W项任务,在一个函数时间内处理270个后,要有预留时间记录并存储当前已完成任务状态,在下一次函数执行开始时直接从第271项任务开始处理。假设一个函数时间最长5分钟,预留30秒时间供存储状态,其余270秒可用于执行。每一项任务处理完成时计算剩余安全时间是否足够30秒,安全时间剩余30秒时停止任务执行,存储执行状态。然后函数递归调用自己,重新开始新的5分钟,取出上一次的任务状态继续执行。函数调用自己也可能因超时、内存不足、网络等原因调用失败。一旦函数调用失败同步任务就会停止,并且难以监控,因此风险较大。
第二,使用定时触发器定时触发函数,假设函数时间为5分钟,则每隔5分钟触发一次函数执行。定时触发器由Serverless服务商进行保障,不会触发失败,保持同步过程稳定运行,比函数自调用更加安全可靠。
5.编排
函数、存储、服务分散:同步服务涉及Serverless函数、表格存储、对象存储、缓存机制、CDN服务、虚拟机等多种资源,以上资源分散并具有复杂的依赖关系,整体部署编排复杂。
解决方案:最初以手工方式处理。首先在云console界面手动申请一个对象存储空间、一个存放日志的存储空间、两个表格存储的表格、新建5~6个函数,本地打包函数、将函数上传到云上等多种操作。然后尝试调用,测试其能否跑起来,操作处理复杂麻烦。
后来尝试用SDK自动化,通过写脚本部署任务操作。如下图所示,部署函数、配置,然后分别调用云SDK进行创建或更新操作。然而使用SDK自动化仍然存在许多问题。存在先有存储才能有函数的依赖关系。函数版本和别名需要保持一致。环境变量不能写错。函数间有调用关系、调用方和被调用方发布有先后顺序。触发器、对象存储生命时长、配置复杂。不能每做一项工作每一次调整部署都写一堆脚本,每一次脚本测试也较为复杂。
为解决以上问题,在社区中找到了Terraform自动编排方案。Terraform是当前较为流行的希望统一云的资源管理的方案。Terraform可以让用户声明各个资源,相互引用。比如某个函数要向对象存储写入内容,此时有环境变量统一管理调度,告知该函数应该向哪一空间进行写入。环境变量的值可以选择使用另外一个存储着资源的name字段。建立了依赖关系后Terraform将自动解析资源间的依赖关系,明确先后执行的顺序。同时Terraform可以基于配置文件制定计划,并且预测、可视化资源的变更影响,例如可能新建存储或更新函数等。当用户确认后再进行连接更新。Terraform变量、环境可以使用专门文件进行统一管理,避免出现写入错误、和系统环境变量做耦合、被其它工具冲掉环境变量等问题。同时Terraform支持不同云服务商。Terraform并不能解决调用一个函数时接口不同的问题,而能够解决将函数更新到云服务时不同云服务的接口不同的问题。Terraform可以做到优秀的可视化部署。
6.完善服务
以上问题解决后,尝试在云上运行测试,仍然发现许多错误,以及许多方面与预期不符。完善服务有复杂之处。
NPM的包形形色色:下图分析了NPM上所有包的大小和文件数量。大部分的包大小在20KB以下,同时存在一些比较大的包,比如存在一部分大于650KB的包。大的包会影响执行时间与效率。从文件数量方面而言,大部分包的文件数量在20以下,但是存在一些文件数量非常大的包,1%的包文件数量大于500。统计数据如下:
平均尺寸635.4KB,最小10KB,最大267MB;
平均文件数45.2,最少0字节,最多140442个字节;
大体积、多文件的包,往往版本多,大部分采用定时发布策略。
由于内存、时间有限,NPM上部分包不适合使用FAAS进行同步。少部分大尺寸、多文件的包拉高了平均水平。数量500+文件、体积600KB+的包几乎无法在有限时间、内存下完成同步。
解决方案:在Serverless基础上备用本地同步环境+CDN服务。上文提到为了能够在本地进行测试,对镜像、存储、函数调用等进行了抽象。再将本地测试的这一套东西做成Docker镜像,放入Docker K8S容器中,就可以运行。如果不做成镜像,直接将源码放入虚拟机,也可以运行。本地同步本身就可以运行在多种不同运行环境中。
将本地同步与CDN服务结合在一起,如下图所示。除了中间云函数部分,将通过K8S集群做一套备份,再通过虚拟机集群做一套备份。备份专门用于检查长时间同步失败的包,并通过本地无限时长、大容量的环境进行恢复。在上述完整环境下,基本所有包都可以同步成功。
辅以开发插件自动使用CDN:上述服务完善基础上,同步服务可以较好地运行,接下来考虑如何令开发者更好地使用CDN,对研发环境与设施进行了处理。如下图所示,百度制作了一套Webpack插件。当开发者在前端项目中,可以随意编写dependencies,例如依赖”react”以及”react-don”。编写完成后不需要开发者自己进行繁杂的配置工作,只需要引入plugins。plugins可以自动识别出其中CDN上有的常用依赖,将其提取出来从开发者的module中删除,并将其指向CDN文件。相对应地,开发者的系统构建出来就会从CDN上加载”react”以及”react-don”,而开发者的代码中不需要感知这些操作。百度已经使用该Webpack插件支撑了内部五、六个产品。有效见证了该插件的启用缩减了百度一部分内部产品的启动响应时间约500~600毫秒。