共计 9079 个字符,预计需要花费 23 分钟才能阅读完成。
本文是 2021 年 12 月 26 日,第三十五届 – 前端早早聊【前端搞 Node.js】专场,来自涂鸦的大前端根底建设团队 —— 龙野的分享。感激 AI 的倒退,借助 GPT 的能力,最近咱们终于能够十分高效地将各位讲师的精彩分享文本化后,分享给大家。(完整版含演示请看录播视频和 PPT):https://www.zaozao.run/video/c35
注释如下
大家好,我是涂鸦智能的龙野,目前在团队中次要负责前端网关和 Node 这个语言根底方向的一些建设。明天我分享的主题是《如何用 Node 建设企业级利用网关》。
背景
前端网关的背景能够追溯到几年前,当微服务刚刚呈现时,咱们在 Java 技术体系中有一张架构图。
整个微服务大抵是这样的,内部申请从用户端进来后,先通过负载均衡器,再达到一个内部网关,最初被转发到集群外部的各种服务,不论是 Java 还是其余语言的服务。在集群外部可能存在许多各式各样的服务,服务之间的上上层划分并不是十分明确。因而,内部网关到外部服务之间的边界并不是特地清晰。
当某个服务呈现问题时,内部网关路由层代理到外部时,咱们须要定位或排查问题时可能会比较复杂。而且,如果这个服务呈现问题后,须要迅速公布一个修复版本,因为各个公司的公布零碎稳定性依赖于整个流程,可能须要较长的工夫。
所以,咱们心愿可能在内部网关到外部服务之间架设一个业务方向的网关,这就是当初 BFF 网关产生的背景。相似于当初的前置 BFF 网关,它是挂在内部网关之后,并且挂在外部服务集群的业务之前的一两头的代理层。这层代理层也就是聚合服务,即 BFF 网关最后产生的背景,它的目标是想要将不同的上游服务的数据聚合后反馈给用户。
在这个根底服务层中,咱们能够扩大其性能。
- 当上游的某个服务呈现问题时,能够在 BFF 网关中进行兜底操作,防止申请达到呈现问题的服务。
- BFF 网关能够统计针对某个业务方向的流量,同时对歹意申请进行过滤。
- 引入了 BFF 网关,用于在罕用的服务中进行封装和泛化,以解决不同的调用办法。业务方只需在网关中配置一个通用的接口,并应用通用的配置形式,即可申请不同的服务。
- BFF 网关还具备聚合接口数据的性能,以缩小前端浏览器发动的申请数量,进步页面的晦涩度。
- BFF 网关能够通过配置实现多接口的聚合,使前端浏览器只需通过一次申请即可取得多个接口的数据。
- 咱们封装了一些公共能力,通过网关插件,容许对业务有更深理解的人间接编写网关插件以解决业务问题,升高业务开发接入的复杂度。
- 除此之外,应用这种网关还有一些其余附加价值。
a. 例如,具备数据伪造性能,网关能够通过配置间接提供假数据,在云端或服务端临时无奈提供理论数据环境下进行联调;
b. 自带服务发现能力,用户只需晓得网关地址,即可申请网关,无需关怀网关外部的路由和数据起源;
c. 缓存能力,如果在非凡场景下这些服务呈现了问题,咱们能够返回上一次失常响应的页面。
明天要讲的内容一共分为以下几个局部,前面咱们将依照这个程序进行简要解说。
网关根本架构
咱们简略聊一下 BFF 网关的架构。
作为外部用户,对于网关,我心愿它能实现申请达到网关后代理到特定的服务,从而实现我要做的事件。然而业务的网关需要较为简单,不仅仅是简略的代理。如果只是简略的代理,那么间接部署一个 Nginx 不就好了吗?
咱们心愿做的是最后的构想,即具备动态化配置 Nginx 的能力。默认状况下,Nginx 的规定是写在 Nginx 的 conf 文件中的,然而如果要动静批改这些规定怎么办呢?理解过 Nginx 配置的同学应该晓得,批改完后执行 reload 命令就能够使新的配置失效。
那么作为一个开发人员而不是运维人员,我应该如何批改这个 Nginx 配置呢?社区中有很多相似的实际,其中一个典型的例子就是将 Nginx 的配置写入到 Consul 中,Consul 会以文件的模式将配置下发到 Nginx 特定的目录。这样,Nginx 能够通过执行 Nginx 的 reload 命令来加载新的配置。咱们最后设计网关时的想法实际上就是这样的。咱们心愿用户可能自定义他们的规定,并将这些规定下发到线上的各种环境中。因而,咱们先开发了一个网关后盾零碎,外部用户能够在后盾零碎中配置代理规定。而后,咱们对这些规定进行了形象,引入了我的项目的概念,我的项目上面蕴含了路由,这些路由函数的逻辑由咱们本人来定义。用户能够将这些配置和数据写入线上特定环境的长久化存储中。
在这种状况下,咱们曾经解决了外部链路的问题。当内部申请进入 BFF 网关时,它应该执行怎么的代理逻辑呢?
间接从 MySQL 中读取数据显然不适合,因为网关通常会解决大量的流量,而依赖于一个 IO 型的数据库会导致性能问题。因而,咱们进行了公布操作,将数据从 MySQL 间接发送到线上的 Redis 中,因为 Redis 的读写速度十分高,能够反对足够的业务申请。因而,网关的所有依赖配置都是从 Redis 中读取的,这样能够确保在高并发的状况下,网关依然可能解决足够的业务申请。
对于失常的内部申请,咱们这里有一个简略的流程图。
在这种状况下,内部申请首先通过你的 DNS 解析,DNS 解析会将其转发到特定的 LB(负载平衡)。依据这个 LB,申请将被代理到特定的 Nginx 和 Ingress。
一旦申请被代理到 Nginx,Nginx 将依据配置规定进行解决。在这个配置中,你能够决定将这种申请代理到集群外部的网关服务,或者是其余服务,因为有些业务方可能心愿将申请代理到本人的服务,而不须要通过网关服务。这种状况通常产生在前端业务或者与前端相干的业务中。如果我是一个云端业务,我必定不会特意走网关。一旦你的业务申请达到网关,网关将在运行时执行特定的逻辑,匹配到之前存储在 Redis 中的配置,并依据规定进行解决。规定能够包含间接代理到像 Nginx 这样的代理服务,或者申请云端服务,以及从申请中获取哪些数据等。这些规定能够通过应用 Node.js 编写代码的形式由用户自定义,这也是咱们网关的一个重要指标,即赋予前端云端服务端开发的能力。
接下来,咱们将简略地看一下接下来要关注的几个要点。
- 用户在后盾能够进行配置我的项目的治理,包含路由的配置和插件相干的配置。而这些配置的治理流程是怎么的?
- 在运行时,咱们须要解析路由代码和插件代码,运行时的执行流程是怎么的?
- 插件的治理和失常代码的治理可能存在差别,须要关注运行时如何解决插件代码。
- 为了提供更好的用户体验,目前咱们应用 JSON 作为用户编写路由函数的代替形式。咱们须要关注如何将 JSON 解析为 TS 代码的逻辑。
- Docker 呈现后的益处之一是隔离性,即多个实例运行在虚拟机中环境是独立的。在网关场景下,咱们也须要关注这一点,甚至在将来的其余场景下,都须要关注代码的动静执行能力。如果波及到动静执行代码,须要特地关注安全性和资源应用状况。
后盾管理系统
在咱们公司的网关后盾治理页面中,能够看到我的项目的域名配置。这个域名会被解析到 Nginx,而后再到网关。域名的解析形式能够应用泛解析,例如 *.xxx.com 的形式。默认状况下,申请会解析到 fast 网关,也就是咱们的 BFF 网关。
这些路由像你写代码的时候,Controller 层的一个一个的办法,它把这些办法写到运行时外面,运行时就是 BFF 网关,运行时会解析申请,而后依据配置进行代理。咱们简略地新建一条路由,试一试这种配置形式。
路由的规定是默认的,用户须要查看网关相干的文档,能力晓得这些货色应该如何配置。比如说在这个中央配置一条 GET 申请的路由,我失常拜访的时候,就能够响应一个 “hello”。
至于这外面的这些代码运行时,咱们也提供了一些依赖服务供应用户应用,包含一些链路相干的数据,还有一些全局变量和一些工具类的函数。这样用户在写这些路由的时候相对来说会比拟不便。
接下来,咱们简略看一下我的项目的列表页面。
须要提一下,因为咱们公司是一个全球化的业务场景,为了让用户在应用时不须要拜访多个零碎,以升高应用老本,咱们开发了一个性能,让用户能够在一个零碎上将数据配置散发到各个国家的数据库或 Redis 中。这种形式在安全性方面须要思考一些因素,须要与公司的安全部门切磋,思考各国家的政策,以确定数据是否可能传输到不同地区,具体依据各公司的状况而定。
除了之前提到的列表模式之外,咱们还为用户提供了一个基于 Web 的 IDE 编辑模式。
这种编辑模式相似于应用 Web IDE 进行编辑,编写路由、网关和配置信息。通过这种形式,用户能够在不同的区域内同步数据,并且零碎会生成 diff 操作,不便用户理解批改的内容。
目前咱们次要应用微软提供的 monaco-editor 的 npm 包来实现,该包专门用于实现相似于 VS Code 在线编辑器的性能。
对于后盾零碎的外围性能次要有以下几点。
路由的减少、删除、批改和查问是咱们的次要关注点。另外咱们还提到了一个配置公布的流程,只管在咱们的理论应用中没有提到,但为了晋升用户的应用体验,咱们在日常开发和预公布环境中采纳了一些技术手段来简化公布流程,以缩小繁琐的操作。
此外咱们还反对路由的援用性能,容许其余业务方援用咱们的路由信息,从而防止反复保护代码的问题。这一性能能够进步开发效率,同时也须要与其余业务方进行充沛的沟通和协调,以确保数据的一致性和安全性。
当波及到企业级利用时,除了之前提到的一些性能外,还须要思考权限治理。用户在操作某种数据时须要具备相应的权限,并且这些权限须要有记录。另外,还须要一些插件、日志链路以及接口的统计等能力,来保障网关的健壮性。
运行时要如何设计
有了后盾零碎,咱们就能够把这个数据写到 MySQL 数据库,也能够把这个数据从 MySQL 这种长久化数据库外面去写到 Redis 里。那么在运行时怎么从 Redis 外面去读这个数据,并且响应给用户呢?
咱们借鉴了 Koa 框架的核心思想,即通过在申请或响应阶段的任何一层,附加一个相似于 AOP 的思维,在任何一层进行拦挡和解决。
应用这个框架时,一个常见问题是如何治理上下文数据。这个框架未能提供足够的倡议,导致许多用户在上下文中挂载大量不明数据。这会导致后续用户不晓得上下文中到底挂载了哪些数据。
为了解决这个问题,咱们要求所有的数据只能挂载到 context.state 对象上,并对这个对象的类型进行束缚。这样我能够通过这个 context 对象理解所有后续申请的信息。
另外,Koa 框架的申请,进来就是全副进来了,进来就是全副进来了,没有某个申请进来时能够进行执行的性能。因而,基于 Koa 框架,咱们封装了 Cube 和 CubeFlow 这两个概念,其中 Cube 的概念实际上就是 Koa 的中间件,只是在这个中间件外面咱们引入了 enable 或者 disable 的概念。这样,在申请执行到这个中间件时,咱们能够间接进行 next 跳转。通过这样的形式,咱们将多个 Cube 进行了串联,造成了一条 flow,这个就是申请的执行链路。
在图片右边咱们还能够看到,main.ts 是申请进入的执行入口,在这个入口外面次要做了两件事件,启动运行时的监听端口,启动另外一个实例。这次要用于监听另外一个端口以裸露一些指标,例如以后网关服务的健壮性和运行时的历史记录统计信息等,供其余服务进行采集和告警。
咱们简略来看一下这个目录。适配器是一个用于解决路由规定的组件。这种适配器次要指的是路由规定的解决形式,咱们默认状况下将路由规定写入 Redis 中,然而前面咱们可能会对其进行扩大。因 Redis 不适宜存储较大的数据,在间接读取较大数据时可能会导致阻塞。对于一些私有化部署的场景,可能没有 Redis 这样的需要。那么咱们就提供一种基于文件配置的形式,事后将客户的一些路由规定写入文件中,而后从文件中读取。
很多人在我的项目中可能应用的是本地文件存储的形式,这种形式的确简便,但也带来了一个很大的隐患。如果将一些账号、明码等数据写入文件中,一旦文件泄露,对公司来说将是一个很大的平安问题。尤其是很多人喜爱将代码寄存在相似 GitHub 这样的中央,相干的密钥的泄露影响会十分重大。此外,还包含一些工具函数的裸露,以及对于日志的解决。
咱们为了便于后续的日志查找,须要对日志进行标准化解决,包含为日志增加等级和类型字段。这样,咱们能够通过这些字段来索引和查找以后申请通过的所有日志的类型。咱们还应用了日志链路和指标工具来观测网关的健壮性和危险,以保障网关的可观测性。
在链路方面,目前最风行且次要应用的工具有 SkyWalking 和 Jaeger。Jaeger 是一款用 Golang 编写的工具,合乎云原生架构的规范,具备良好的扩展性。咱们同时在前端和云端都应用了这两套工具。
通过标准化解决后,咱们目前在日志链路中采纳了 B3 数据传输标准。尽管它的官网提供了不同的部署模式,但不管哪种部署模式,因为不足前置队列,在 span 数量过多时,可能会导致数据失落。因而日志链路尽管不便,但并不齐全牢靠。如果须要更牢靠的链路追踪,还需依赖日志记录。
在罕用的中间件中,因为裸露了 HTTP 服务,须要留神解决申请中的 body 数据,否则无奈解析 POST 申请的 body。网关还有一个外围性能,即对于登录 session 的保留和 cookie 的解决。在前面的根底中间件中,还波及到 CSRF 平安中间件,这将在后续具体探讨。
当初,让咱们来看一下申请进入代理流程的具体步骤。
对于相熟 Node.js 并且有前端插件编写教训的人来说,可能会对这一过程更加理解。通过 http-proxy 将上游申请代理到指定的上游服务,外围代码如上所示,官网文档中还蕴含其余用法,其实都大同小异。
在封装时,咱们须要关注运行时的几个重要点。例如,如果监听失败、用户主动勾销、代理超时以及上游异样应该如何解决?应该返回什么样的信息给用户?
咱们次要做了以下解决,当申请到来时,咱们首先判断这是否为代理申请还是 Ajax 接口申请。如果是代理申请失败,咱们会间接通过模板引擎进渲染一个失败页。如果是接口申请,咱们会间接返回错误信息,并携带网关指定的错误码。这些错误码会在文档中进行记录,以便用户在看到谬误时可能理解产生了什么问题。
对于运行时的代码执行形式,咱们须要查看上面的图表。
用户配置的数据会被写入 router 对象,并在运行时同步到 Redis 中。当申请到来时,我会先从 Redis 中获取这个数据。获取到的数据实际上是一个字符串。而后咱们将 JS 字符串丢到 Node 提供的 vm 模块中,而后在 vm 外部运行的代码中,思考到用户可能应用咱们提供的 context 或者内部依赖 RPC 的状况,须要先将这些能力替换为特定的函数,这样 vm 能力正确辨认这些数据。
当然,须要额定提及的是官网的 vm 模块存在一些安全性问题。动静执行代码有很多形式,比方 function 等,但为什么在最佳实际中,这些形式基本上都没有被应用呢?最大的问题就是,vm 间接将一些敏感的运行环境数据裸露给了用户,用户能够在虚拟机外部进行批改,从而波及到了较大的安全性问题。因而,社区当初基本上都在应用 vm2 来代替官网的 vm 模块,解决一些动静执行的场景。
目前,咱们应用最广泛且隔离性最好的工具就是 Docker。Docker 在线程级别或过程级别,对不同的服务和资源进行了隔离。刚刚提到的 vm2,其安全性和隔离性方面还不够。因而,如果咱们想从线程层面对隔离进行更加严格的管制,咱们能够思考在 Docker 根底上进行封装,或者自行基于 vm 进行更上一层的线程级别封装。
须要额定提到的是,Docker 在容器隔离方面的安全性可能还不够。尤其是在波及底层 Linux 文件描述符等配置操作时,容器可能会泄露一些数据到内部。社区目前也在致力于开发更加平安的容器隔离技术,倡议大家亲密关注相干的最新进展。如果你从事与阿里云或其余云服务商相干的产品开发,更须要关注容器隔离技术的安全性。
接下来,简略地应用 JSON 来形容这段代码。
与之前的流程相似,首先从配置中提取这些数据,提取到了这些数据之后,咱们会对一些前置的变量和工具进行初始化,并在运行时,当解析到须要执行的路由时,进行编译。这个编译的过程实际上就是将 JSON 对象提取进去,而后进行替换的工作。因为这样的替换波及到了有限的层级配置,咱们不不便在代码中进行适配。因而,咱们做了一个斗争,只容许承受 JSON 的第一层数据。
在代码中,外围的局部其实是 router.run(),实际上执行的是相似于以下的步骤。
首先创立一个虚拟机,而后将代码或者插件放入其中。最初,通过运行虚拟机实例来运行须要的脚本,并获取代码的返回值。
Drone 是一个基于 Docker 的、更不便隔离的工具。它的下层能够应用 vm2 来封装,但具体应用的形式取决于团队的需要和能力。作者写的内容很不错,有趣味的读者能够在 GitHub 上理解更多。
插件的加载和治理
业务方认为在后盾配置数据和工具很麻烦,咱们为他们提供了一份文档,通知他们如何进行插件开发,让他们能够依据本人的能力自行编写插件,缩小沟通老本。平台容许用户新增插件,默认应用内网的 GitLab 进行治理。代码保留形式可能须要变更,不用保留在仓库,能够间接存储在 MySQL 数据库或对象存储中。
目前的插件源码放在 GitLab 上进行治理。后续布局中,咱们打算应用文件存储的形式,间接将对象存储为文件。这是因为相似于 vm2 这样的工具能够间接加载文件并执行其中的代码。对于数据同步性能,大家能够自行查看。
插件开发的模版如下图所示,须要先定义输出参数,并裸露一个函数作为插件的执行逻辑。业务方本人实现具体的业务逻辑,能够通过函数申请业务依赖的服务,也能够依据登录相干的数据来批改上下文中的内容。插件是作用在全局的整个我的项目中,配置某个插件后,每个路由都会默认应用该插件。如果特定路由不应用插件,则插件需提供路由白名单。
在插件的应用上,有以下倡议:
- 我的项目级和路由级配置,最终运行时在路由级辨认最终插件代码,并在链路上体现。
- 个别配我的项目级插件即可,非凡路由通过白名单跳过插件逻辑,或配置路由级插件。
- 不倡议适度应用插件,就好比不倡议 Koa 中间件过多,导致申请上下文过于简单。
- 应用插件前需浏览插件文档,明确插件用处再来应用;如有疑难可分割插件开发者。
- 网关插件有版本性能,插件版本号须要遵循 semver 标准。
网关的安全性加固
最初要提到的一个性能点是网关的安全性,这在之前的分享中也提到过,安全性是一个不可漠视的问题。网关的安全性可能会导致公司陷入法律诉讼等重大问题,因而须要引起足够的器重。
网关上能够实现一些在安全性方面的加强性能。例如,如果我常常裸露这种接口,那么我的接口是否反对避免申请的重放攻打呢?
防重攻打
在接口防重攻打方面,有很多种计划可供选择。
咱们这边次要是通过右边四个点的认证来确定申请是否非法。只有申请可能通过这个认证,咱们就认为该接口具备了防重攻打的能力。不过,理论的防重攻打力度应该达到什么级别,这须要依据各个公司的平安委员会的要求来确定。具体而言,每个申请都必须可能互相区别,并且能够回绝反复的申请,避免申请内容被篡改,同时答应容错。
在咱们的场景中,咱们发现大多数业务方在应用接口时,先从网关加载到前端的页面,而后在页面上发动 Ajax 申请。在这种状况下,当文档加载实现时,网关会返回一个 nonce 作为响应。后续的申请中,限度前端必须应用本人的框架去发动 HTTP 申请,框架会在发动申请时进行封装,将以后工夫戳和 nonce 一起携带过来。当申请达到网关时,网关会解析参数,并判断以后时间差是否在容错范畴内。而后,网关会在 Redis 查看申请中携带的 nonce 字段是否非法。
防 XSS 攻打
对于常见的防 Web 平安的办法,其中波及到的一个问题是 XSS 攻打。
这种攻打次要分为两种类型,一种是反射型 XSS 攻打。这种攻击方式是用户在提交表单数据中携带一些歹意脚本,当后端存储了这些数据后,就会呈现存储型 XSS 攻打。当前面用户的申请从后端取出这些数据时,前端的脚本会在浏览器中执行,从而导致网站被攻打。
能够通过配置两种常见的包来配置 XSS 防护参数,一个是 XSS 包,另外一个是 Hamlet 包。在前端的场景中,个别会封装一个函数来应用这些包。在平安防护上,次要是减少平安攻打的难度,而不是完全避免。
防 CSRF
CSRF 这个跨站脚本伪造的问题和 XSS 攻打可能是以后 Web 畛域中最常见且危害较大的两个问题。
与之前提到的防重攻打计划相似,网关在渲染时会返回一个 token 值,而后前端框架在发动申请时会携带这个 token 值。通常状况下,前端会把 token 放在 cookie 里与登录态一起应用。
另外,网关次要对 post、put 和 delete 等可能对数据库数据造成影响的接口做一些合法性校验,对 get 等获取申请大多数状况下间接放行。
内存级别和 IP 级别的限流
最初,安全性还波及到内存级别和 IP 级别的限流。
当你的流量申请较大时,你服务的稳定性或响应工夫可能会受到肯定影响。如果你的网关服务多个业务方,相似之前提到的默认状况,多个业务方通过你的网关代理申请,那么如果某个业务方发动歹意攻打,一分钟内发送成千上万个申请,导致你的网关负载能力减少,QPS 回升,从而导致每个响应的提早减少到四五百毫秒甚至一两秒,其余业务方也会受到影响。
因而,首先咱们须要对你的网关在特定配置下进行压力测试,理解你的 QPS 大概可能反对多少,响应提早、内存、CPU 等资源是否在可控范畴内。能够在代码中增加一个限流插件来进行限流。一个简略的限流插件能够基于 map 来实现,即申请进来时进行计数,超过限定值时进行限度,而后申请进来时将计数缩小。如果您想要更加强壮的限流计划,能够思考社区上的一些限流 npm 包。咱们当初在网关上实现 Mesh 化,当某个服务呈现问题,不会影响其余服务。同时,在这种场景下,业务方到网关之间的网络带宽和提早也能够失去性能优化。
其余性能
这个后盾零碎的性能在很多零碎中都很常见,次要包含安全性合规、数据加密存储、日志输入时防止敏感信息泄露以及配置核心接入。
结束语
最初须要关注的是,咱们正在开发一个业务服务的 BFF 网关,这将帮忙解决一些痛点。随着业务的一直变动,咱们还须要对这个 BFF 网关进行优化,因为业务需要变幻无穷。例如,方才提到的治理形式的匹配化、后续的可观测性晋升,提供低代码化或可视化的编排能力以晋升用户体验等。此外,在线配置路由等性能时,须要提供在线和线下的单元测试,并对路由信息的相干参数进行欠缺的校验,这些都是咱们须要尝试实现的一些指标。
最初
以上就是我的全副分享内容。最初,举荐一本讲经济的书《将来二十年,经济大趋势》。