海量Node.js网关的架构设计与工程实践!

Posted 云加社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了海量Node.js网关的架构设计与工程实践!相关的知识,希望对你有一定的参考价值。

express requestUpstream, resolveUpstream app = express() upstream = resolveUpstream(req.method, req.path, req.headers) response = requestUpstream(upstream, req.body) res.send(response)port = => console.log(`App listening at $port`))

这段示例代码在做的事情很简单,即我们收到一个请求之后,会根据请求的方法或者路径进行解析,找出它的上游是什么,然后再去请求上游,这样就完成一个网关的逻辑。

当然这是最简的一个代码了,实际上里面有很多东西是没有考虑到的,比如技术框架以及内部架构模块的治理,比如性能优化、海量的日志系统、高可用保障、DevOps等等。当然这样展开就非常大了,所以我今天也不会面面俱到,会选其中几个方向来讲的比较深一点,这样我觉得会对大家比较有收获。

(二)云开发CloudBase(TCB)是个啥?

说到这个,顺便介绍一下我们云开发CloudBase是什么,要介绍我们网关肯定要知道我们业务,像小程序·云开发、Web应用托管、微搭低代码平台,还有微信云托管这样的服务都是在我们体系内的。

这些服务它的资源都会过我们的网关来进行鉴权,你可以在云开发体系下的控制台上,看到我们URL的入口,实际上这些URL它的背后就是我们的网关。

整个网关最简版的一个架构如上图所示,我们会给用户免费提供一个公网的默认域名,这个域名它背后实际上是一套CDN的分发网络,然后CDN回源到核心网关上面来。我们网关本身是无状态的服务,收到请求之后,它需要知道如何把请求分发到后端的云资源上去,所以有一个旁路的后端服务可以读取这样一套数据。

网关的后面就是用户自己的云上资源了,你在云开发用到任何资源几乎都可以通过这样的链路来进行访问。

网关内部是基于Nest.js来做的,选Nest.js是因为它本身自带一套设计模式,很Spring那一套,更多的是做IOC容器这一套设计模式。

从上图可以看到,我们把它的内部架构分成了两层,一层是Controller,一层是Service。Controller主要是控制各种访问资源的逻辑。比如说你去访问一个云函数(SCF),和你去访问一个静态托管的资源,它所需要的访问信息肯定是不一样的,所以这也就是分成了几种Controller来实现。

底层的话Service这一层是非常“厚”的,Service内部又分成逻辑模块和功能性模块。

首先第一大块是我们的逻辑模块,逻辑模块主要是处理我们内部服务模块的很多东西,最上面这一层主要就是处理跟资源访问相关的一些请求的逻辑,跟各种资源使用不同的协议、方法来对接。然后中间这一层,更多的是做我们内部的一些集群的逻辑。比如集群管理,作为一个公有云的服务,我们对于客户也是会分等级的,像VIP客户可能就需要最后来发布,我们肯定是先验证一些灰度的流量,像这块逻辑就属于中间这一层来管理。最下面这一层就会有各种负责I/O的Client,我这一次只画了一个HTTP的Client,实际上还会有一些别的Client。

除此之外还有一些旁路的功能性模块,包括像怎么打日志、配置、本地缓存的管理、错误处理,还有本地配置管理DNS、调用链追踪等这些旁路的服务。

这一套设计其实就是老生常谈的高内聚低耦合,业务逻辑和真正的I/O实现要解耦开。因为只有解耦开,你才能够针对你的业务逻辑进行单元化的测试,可以很方便的把它底层的这种I/O读写逻辑给Mook起来,保证核心业务逻辑模块的可测试性。

上图是网关整体的链路架构,稍微更全面一点。最上层是分布在边缘的CDN节点,然后这些CDN节点会回源到我们部署在各地的集群,然后这些集群它又可以访问后面不同区域的资源,因为公有云它的资源其实也是按区域来划分的,所以这就讲到了我们网关的两个核心的要求,“快”和“稳”

首先作为一个网关肯定是要快的,因为网关作为CloudBase云函数、云托管、静态资源的公网出口,性能要求极高,需要承接C端流量,应对各种地域、各种设备的终端接入场景。如果说过你网关这一层就可能就花了几百毫秒甚至一两秒钟的时间,对于客户来讲是不可接受的。因为客户他自己的一个函数可能只跑了20毫秒,如果网关也引入20毫秒的延迟,对于客户来讲他就觉得你这条链路不行。

其次就是要稳,我们是大租户模式,要扛住海量的C端请求,我们需要极高的可用性。作为数据面的核心组件,需要极高的可用性,任何故障将会直接影响下游客户的业务稳定性。如果在座的有四川或者云南的同学,你回家每次打开健康码扫码,其实请求都会经过我这个网关的。

所以今天主要就讲两个部分,既然是快和稳,分别对应性能优化可用性,所以我现在从性能优化开始讲起。

二、性能优化

网关性能优化思路

性能优化的思路,首先是看时间都花在哪个地方了,网关是一个网络的组件,大部分时间都是耗在I/O上,而不是本身的计算,所以它是一个高I/O、低计算的一个组件。

网关有几个技术特点:首先,它的自身业务逻辑多,是重I/O、轻计算的一个组件;其次它的请求模式是比较固定的,模式固定我们可以理解为,你的一个客户他发送过来的请求实际上就是那么几种,它的路径、包体大小、请求头等这些都是比较趋向于固定的,很难会有一个客户他的请求是完全随机生成的,这对我们后面针对这种情况做缓存设计会有一些帮助。最后,网关的核心链路很长,涉及到多个网络平面。

那么我们就找到了我们的一些优化方向:减少整个IO的消耗,并且优化核心链路。所以优化部分我就分成了两块内容来讲,第一块是网关自身核心服务优化,第二块是整体架构链路优化。


(一)核心服务性能优化

第一部分,核心的服务怎么做优化?先提几个方向。

第一,网关自身的业务逻辑很多,调用很多外部服务,其中有一些是不需要同步阻塞调用的,因此我们会把部分业务逻辑做异步化,让到后台去异步运行。比如说自定义域名来源的请求,我们要先确定这个域名是不是合法绑定的,这里的校验就会放到后台异步来进行,网关只是读取校验的结果。

第二,网关类似代理,转发请求响应体,这里我们使用了流式的传输方式。其实Node原生的HTTP模块里,HTTP Body已经是一个流对象(Stream),并不需要额外的引入类似body-parser这样的组件把Stream转成一个javascript对象。为此我们在网关的设计上就尽量避免把请求相关的元数据放到Body里,于是网关就可以只解析请求头,而不解析用户的请求体,原封不动地流式转给后端就可以了。

第三,我们请求后端资源的时候,改用长连接,减少短连接带来的握手消耗。像nginx这样的组件,它通常都是短连接的模式,因为我们这些业务情况比较特殊一点,是一个大租户的模式,类似于所有用户共用同个Nginx,那么你再启用短连接模式的话,就会有一个TCP TIME_WAIT的问题,下面会详细讨论。

最后,我们的请求模式比较固定,我们会针对实际情况设计一些比较合理的缓存的机制。

  • 优化点:启用长连接机制

  • 首先,为什么短连接会有问题?

    我们去请求用户资源的时候,网关所在的网络平面是内部服务的平面,但是每个用户的公有云资源实际上是另一个网络平面。那么这两个网络平面之间是需要通过一个穿透网关来通信的。这个穿透网关可以理解为是一种网络层虚拟设备,或者你可以理解为它就是一个四层转发的Nginx,作为代理客户端,单个实例可以最大承载6.5W的TCP的连接数。


    如果做过一些传输层协议的同学应该会知道,一个TCP连接断开之后,客户端会进入一个TIME_WAIT的阶段,然后在Linux内核里面它会等待两倍的时间,默认是60秒,两倍是120秒,120秒之后才会释放这个连接,也就是说在120秒内,客户端的端口实际上都是处于被占用的状态的。所以我们很容易能算出来单个传统网关它能够承载的最大的额定QPS大概就是不到600的样子,这个肯定是不能满足用户需求的。

    那么我们怎么去解决短连接TIME_WAIT这个问题?其实有好几种方法。

    第一种是修改Linux的TCP传输层的内核参数,去启用重用、快速回收等机制。但对于我们的服务来说并不合适,这需要定制这样一个系统内核,维护成本会非常高。

    第二种,云上类似的组件怎么解决?比如腾讯内部的负载均衡,其实很简单,就是直接扩张集群内的VM数量。比如一台反向代理服务器加上TIME_WAIT快速回收,可以承载5000多QPS,但想要二十万QPS怎么办,做40个虚拟实例就行了。但这种做法,一是需要内核定制,二是需要我们付出很大的虚拟实例成本,就没有选择这种经典方案。

    最后一种就是我们改成长连接的机制,类似Nginx的Upstream Keepalive这样的机制。改成这样一个机制之后,其实效果还挺好的,单个穿透网关就可以最大承载6.5W个连接数,相当于几乎6.5W个并发。对于同一个目标IP PORT,它可以直接复用连接,所以它穿透网关的连接数限制就不再是瓶颈了。

  • 长连接的问题

  • 那么是不是长连接就是完美的?其实并不是。长连接会导致另外一个问题,竞态问题(keep-alive race condition),如果在座里有用HTTP长连接的方式做RPC调用的同学,应该经常会看到这个问题。

    客户端与服务端成功建立了长连接连接静默一段时间(无HTTP请求)服务端因为在一段时间内没有收到任何数据,主动关闭了TCP连接客户端在收到 TCP关闭的信息前,发送了一个新的HTTP请求服务端收到请求后拒绝,客户端报错ECONNRESET。

    所以怎么解决?

    第一种方案,就是把客户端的keep-alive超时时间设置得短一些(短于服务端即可)。这样就可以保证永远是客户端这边超时关闭的TCP连接,消除了错误的暂态。

    但这样在实际生产环境中是没法100%解决问题的,因为无论把客户端超时时间如何设置到多少,因为网络延迟的存在,始终无法保证所有的服务端的 keep-alive超时时间都长于客户端的值;如果把客户端超时时间设置得太小(比如1秒),又失去了意义。

    那么正确方法就是用短连接去重新试一次。遇到这个错误,并且它是长连接的,那么你就用短连接来发起一次重试。这个也是参考了Chrome的做法,Chromium自己的内核里面处理了这样一种情况,浏览器里它其实这种长连接也是时刻存在的,下图是一段它自己里面的内核的代码。

    2019年的时候,社区里常用的agentkeepalive不支持识别当前请求是否开启keepalive,我们给社区提交过一个PR,支持了这个特性。也就说你只要使用了agentkeepalive这样一个包,就可以写一段代码来识别出这种情况,并且进行重试。

    这是我们一个日常统计的量,大概万分之1.3的概率,会命中这样一个竞态的情况。

    小结

  • 非必要情况,不要用HTTP协议作为RPC底层协议。因为HTTP本身最适合的场景是浏览器跟服务端来做的,而不是一个服务端和服务端之间的一个IPC协议,尽量使用gRPC或者类似的这样的协议来做。


  • 如果不得已使用HTTP,你的后端可能非常老旧,开启长连接是一种较好的方案。


  • 长连接需要解决Keep Alive的竞态问题。如果你用长连接,记得一定要处理这个问题,不然这个问题会成为一个幽灵一样存在。像刚才说的,万分之1.3非常难复现,但是这个错误又会不停地出现在你业务里。

  • 优化点:设计缓存机制


  • 缓存在后台设计里是个万金油,“哪里慢了抹哪里”,但是如何设计缓存其实也是一门学问。

    前面提到我们的请求模式都是非常固定的,我们可以根据请求模式来决定缓存数据。缓存都是些什么东西呢?是路由配置,像域名配置、环境信息、临时密钥等这些信息。

    这些数据有哪些特点?首先是活跃数据占比小,这确实也是现状。假设我们全量的用户里面每天只有大概5%~10%的用户才是活跃的,这个数据才是真的会经过你的网关。其次是模式比较固定。第三是对实时性的要求不高。比如说变更了路由之后,客户通常是能够接受有1~3分钟不定的延迟的,并不要求说变更了路由之后就即刻生效。

    因此我们可以针对以上这些特点来设计缓存。第一是因为我们的活跃数据占比很小,所以我们是缓存局部数据,从来不会缓存全量的数据。第二是我们会选取域名、环境这种几乎是固定的信息作为缓存Key,这样缓存的覆盖面就可以得到保证。第三是读时缓存要大于写时缓存,这个后续会提到为什么会选用读时缓存,而不是写入数据的时候把缓存推到我们的网关里。

  • 本地缓存的局限性

  • 最早的时候,实际上我们是有一个最简单的设计,就是加了一个非常简单的本地缓存,它可能就是以域名或以路径作为缓存的Key,这样实现简单但有很多局限性:

    首先,要写大量这样的代码,要去先读本地有没有缓存,有缓存就缓存,没缓存去后台要数据。


    其次,因为网关不是一个单独的实例,它不是一个单进程的Node,单进程的 Node是扛不了这么多量的,我们是有很多很多实例,大概是有几千核,也就是说有几千个Node进程,如果这些进程它本身都有一份自己独有的内存,也就导致它这个缓存没有办法在所有实例上生效。因此当我们的网关规模变得越来越大的时候,缓存也就永远都只能出现在局部。

    为了解决这样的问题,我们加入了Redis中心化的缓存。我们是本地内存+Redis两层缓存,本地内存主要是为了降低Redis负载。当Redis故障的时候也可以降级到本地缓存,这样可以避免缓存击穿问题。Redis作为一个中心化的缓存,使缓存可以在所有实例上生效,也就是说只要请求过了一次网关,Redis缓存就会生效,并且所有的网关实例上都会读到这样一个缓存。

    既然有了缓存,那必然有缓存淘汰的机制,怎么样合理地淘汰你的缓存?这里是用了TTL+LRU两重的机制来保证,针对不同的数据类别,单独设置参数,为什么是TTL+LRU?后面在容灾部分会进行解释。

    最后就是抽象出数据加载层,它是专门用来封装读操作,包括缓存的管理、请求、刷新、容灾这样一套机制,我们内部会有一个专门模块来处理。

    有了Redis之后,我们的缓存是中心化的了,只要你的请求经过了我们之后,你的东西就可以在所有的实例上生效。但是这样会引来另一个问题,因为淘汰机制是TTL的,必然遇到缓存过期。假设是每秒钟都会回头发起一次请求,那么缓存是一定是会过期的,一分钟或两分钟之后你的缓存就过期了,在过期之后的请求一定是不会命中缓存的,这导致了请求毛刺的问题。这对于在持续流量的下游业务上,体现非常明显,下图是我们的一个截图。

    可以看到图上有很多毛刺,这些毛刺的尖尖就是它没有命中缓存的时候,为了解决缓存的毛刺问题,我们加入了Refresh-Ahead这样一个机制,就是说每次请求进来的时候,我们首先会去Redis里去读,使用缓存的数据来运行逻辑。

    同时我们也会判断,如果缓存剩余TTL小于一定值,它就会即触发异步刷新的逻辑,这时候我们会去请求后端服务,并且把更新鲜一点的数据刷新到Redis里,这就是我们数据加载层内实现Refresh-Ahead机制的大概逻辑。

    Refresh-Ahead其实非常简单,字面意义就是说提前去刷新缓存,缓存数据快到TTL了,那么就去提前更新一下。

    能够这样设计,更多是基于一个先验的逻辑,就是说当下这一刻被访问的数据,大概率在未来的一段时间内会再次被访问。

    下图是我们加入了Refresh-Ahead之后的一个效果,红色箭头处是上线时间,上线完之后发现毛刺就明显变少了。但是为什么还会有一点毛刺?因为有一些数据它可能真的就是很长的时间,刷新了之后它也依然过期了,依然会产生这样的毛刺。

    最后解释一下,为什么我们是网关去后台读数据,而不是后台把数据推给网关?或者说,为什么是“拉”而不是“推”

    这其实有几个考量点,第一,因为是数据局部缓存,所以我们全量数据完全推过来体积很大,大概有几十个G,而活跃占比很小,如果完全存在内存里,其实也是一种反模式的做法,不太经济。

    第二,后台能不能只推局部活跃的数据给到网关呢?其实也是不太合适的,后台很难去识别哪些数据是活跃的,哪些数据不是活跃的,这样实现复杂,难度很大。

    第三,网关和它的持久化的后台之间会产生一个缓存Key上的耦合,所谓的 Key上的耦合就是说双方要约定一组Key,我这个数据是在Key上面去读,然后你后台要把数据推到Key上面。那么就会带来另一个问题,一旦Key写错了,或者说出现了一些不可预料的问题,那就会产生一些比较灾难性的后果,所以我们就没有使用“推”这样一种方式。

    小结


  • 在现代大规模服务里,缓存是必选项,不是可选项。

  • 缓存系统本质是一个小型的分布式系统,无法逾越CAP理论。


  • 根据业务场景,合理地权衡性能、一致性和可用性。


  • (二)架构、链路性能优化

    前面讲的是服务跟服务自己核心的优化,接下来讲一讲架构和链路上的一些性能优化。

    上图是我们整体的一个架构,可以看到一个请求,它从前面的接入层一直走到后端云资源之间,其实整个链路是很长的,这里分析一下。

    首先链路很长,涉及边缘节点、核心业务、后端资源。其次网关是承接C端流量的,它其实对终端的性能是很敏感的。第三个就是网络环境复杂,它涉及到数个网络平面的打通。因此我们就有了优化方向,第一个是让链路更快更短。第二个是核心服务Set化,便于多地域铺设,终端用户可以就近接入。第三个就是我们在网络平面之间会做一些针对性的优化,针对性优化怎么做,后面会提。

  • 前置链路:CDN就近回源

  • 首先先讲一下我们就近接入是怎么做的,在网关最开始上线的时候,其实会存在一个问题,你的CDN节点它其实是通过公网回源的,那为什么是公网回源?

    其实这涉及到国内这几家大厂的一个网络架构,简单地说就是,诸多的CDN节点中,有部分可能不是腾讯自建的,所处的网络可能不是腾讯的内网,它可能是某个运营商,比如说电信、联通或者网通这样的边缘节点,然后它是要走公网回源到腾讯的入口的,这里的公网回源就非常慢。

    比如说广州的节点回源到上海,并且走HTTPS协议,那就是60~100毫秒,但问题在于CDN节点是有很多的,HTTPS握手之后,这个链接还是没有办法复用的,等于说每次请求都要跟源站之间进行一次HTTPS握手,这个延迟是不可接受的。

    最后我们在网关的回源接入点上做了一层就近接入,也就是说你CDN在广州的节点,可以很就近地接入到我们在部署在广州的网关,然后网关内部再进行跨地域的访问,因为这个时候就已经是内网了,速度就会很快。

    为了能更好地铺设网关多地接入点,我们就把网关改造成了地域无感的,即业务逻辑和它所在的地域是解耦的。其次,网关支持跨地域访问后端资源。最后,配置收归统一,所有地域用同样的后端资源配置,减少了我们不同地域的配置发散的问题。

  • 服务本体:SET化部署

  • 把这些事情做了之后,网关其实达到了“SET化部署”的概念,降低就近接入成本,任意集群能访问任意地域的后端资源。相当于网关在所有地域的集群,服务能力都是一模一样的。你可以使用任意域名去任意网关访问,获得到结果都是一样,这样SET化部署带来很多好处:

  • 新地域接入点的部署、维护成本极大下降。

  • 便于铺设就近接入点,加速CDN接入。

  • 不同地域的集群之间服务能力完全等价,带来容灾能力上的提升:流量拆分、故障隔离。

  • 也就是说全网只要只剩一个地域的网关可用,我们的服务就可以正常的运行。

  • 底层组件同可用区部署

  • 接下来涉及到网络平面之间的部署,刚才提到了我们在访问用户的资源的时候,其实会经过一个穿透网关,这个是不可避免的,因为它涉及到两个网络平面的打通,在穿透网关的这一条链路也是可以优化的。

    我们可以看一个数据,就是像这种穿透网关和我们云上的资源,它通常是部署在不同的机房的。

    举个例子,像图上的上海二区,它实际上是在上海的花桥机房,穿透网关因为它是网络层提供的设备,它会部署在上海六区。查一下地理位置可以看到,二区到六区之间其实相隔了可能有七八十公里,后端资源是在上海三区的,宝信。在地理位置上讲,它整个请求就经过了下图这样一段链路。

    但实际上这也是完全没有必要的,我们可以将网关和穿透网关部署在同样一个区域,这样就会极大降低从网关到后端资源这样的一个延迟。当然这个事情我们正在慢慢地铺设中,现在还在验证可行性的阶段,我们设想是这样来做。

    最后来看效果,我们总体的缓存命中率大概有99.98%。你可以自己部署一个很简单的服务到我们的平台上,然后跑一下测速,你会发现全国其实都是绿的,这个也是我们觉得做的还不错的一个证明。网关自身的耗时,其实99% 的请求都会在14毫秒内被处理完毕。当然你说平均值能不能进一步降低,我觉得是可以的。但是你再进一步降低的话,可能就涉及到Node.js本身事件驱动模型这样一个调度的问题。

    小结

  • 大规模服务不能只考虑自身性能,前置/后置链路都可能成为性能瓶颈。

  • 前置/后置链路通常与公司基建、网络架构密切相关,服务研发团队需要深刻理解。

  • Node.js受限于自身异步模型,很难精细化地控制、调度异步IO,并非万金油。

  • 三、高可用保障


    讲完性能优化,最后一个部分就是可用性保障,那么我们通常的服务怎么来做可用性保障?

    第一,不要出事故。服务的健壮性,你本身服务要足够的健壮,这里有很多机制,包括灰度发布、热更新、流量管理、限流、熔断、防缓存击穿,还有缓存容灾、特性开关、柔性降级……这些东西。

    第二,出事故了能感知到。事故永远是不可避免的,每天都会发生乱七八糟的各种事故,出了事故的时候你是要能够感知到,并且能够让你的系统自修复,或者说你自己人员上来修复。这就涉及到监控告警系统,还有像外部的拨测,用户反馈监控,社群里面的一些监控。

    第三,能立刻修复事故。出了事故的时候,能够有机制去立刻修复,比如快速扩容,当然最好的是整个系统它能够自愈。比如说有个节点它出问题了,你的系统可以自动剔除它,但如果做不到的话,你可以去做一些人工介入的故障隔离,还有多实例灾备切换、逻辑降级等。

    上图是我们网关整体的架构,哪些地方容易出现问题?其实每一层都会出问题,所以每一层其实都要相应的去做容灾,比如CDN到CLB这一层,CLB是不是有多个实例的灾备?像CLB到网关这一层,是不是网关也是有同样的多实例,还有一些监控的指标。当然这个篇幅就非常大了,所以我今天只讲我们最核心业务层的容灾。

    核心业务层

    先讲讲我们核心业务层面临的一些挑战

  • 下游客户业务随时有突发大流量,要能抗住冲击。因为我们是承载公有云流量的,大概有上万的客户他的服务是部署在这里的,我们永远不知道这些客户什么时候会突然来一个秒杀活动,他可能也从来不给我们报备,这个客户的流量可能随时就会翻个几千倍甚至几万倍,所以这时候我们要能扛住这样一个冲击。

  • 网关本身依赖服务多,稳定性差异大,要有足够的自动容错兜底机制。

  • 能应对多个可用区故障,需要流量调度、灾备、多地多活等机制。

  • 我们能先于客户发现问题,需要业务维度的监控告警机制。

  • (一)核心业务层:应对大流量冲击

    那我们怎么样去应对一个大流量的冲击?实际上对于一个系统来讲,它其实是非常具有破坏性的,它有可能直接把你的缓存还有你的DB击穿,导致你的DB直接就夯住了,CPU被打满。下图是我们一次真实的例子,我也不是很排斥说出来。

    这是我们今年年初1月份的时候,有一个客户他的流量突然翻了100多倍,你可以看到图上它的量就突然提升,这造成一个什么问题?它的缓存都是冷的,也就是说访问量突然提升100倍,这100倍的请求,可能都要去后台读它的一些数据,导致直接把后台数据库的CPU打满了,也导致这个灾难进一步扩散,扩散到到所有用户的数据都读不出来了。

    后来我们就反思了一下这个是不是有问题的?对,是有问题。我们要做什么事情来防止这样的问题出现的?

  • 提升服务承载能力

  • 大流量来了,你自己本身要能扛得住,这个时候要去提升你整个服务快速扩容的能力。我们的网关实际上当时已经是完全容器化的,所以这一点还好,它可以快速做到横向扩容,瞬间扩出几百核几千核的资源,可以在几分钟之内完成。


    其次,我们是使用了单POD多Node进程,就是我们1个POD会带有8核,每个核心跑一个进程。这个在Kubernetes里面实际上是一个反模式,因为Kubernetes要求POD要尽量得小,然后里面就只跑Node一个进程。但在工程实践的时候,我们发现这样跑虽然没有问题,但是它扩容速度非常慢,因为每次实例扩出来,都是批量的。比如说我们内部的容器系统,只能说一次扩 100个实例,也就是说一批也就扩100核,并且这100核都要分配内部的虚拟 IP,可能会导致内部的IP池被耗尽了。最后我们做了合并,起码一个POD能扩出8核的资源出来。

  • 保证服务健壮性,不被打垮

  • 当然,除了提升自己抗冲击的能力以外,还要保证你的后端,保护好你后面的服务。

    比如说我们要保证服务的健壮性,在大量冲击的时候不会被打垮。首先单个实例会做一个限频限流,防止雪崩,流量限频是说你一个实例最多可能只能承载 1000QPS的流量,再多你这个实例就直接放弃掉,就不请求了,这样可以防止你的整个后台雪崩,不至于说一个POD崩了,然后其他POD请求又更多,把其他POD全部带崩。


    其次,我们要做DB的旁路化,网关它读的永远是缓存,缓存里面读不到,那就是读不到,它永远不会直接把请求请求到DB里面去。当有数据写入或者说数据变更的时候,后台同学会先落DB,然后再把DB的数据推送到缓存里面,大概就是下图这样一个逻辑,防止缓存击穿问题。

    第三,服务降级机制。假设真的出现问题了,比如说你缓存也出问题了,我们可以做一些服务的降级。它可能有一些功能没有了,比如说有些特殊的HTTP请求,响应头可能没有了,但是它不会干扰你的主干逻辑,这个也是可以做的。



    (二)核心业务层:应对外部事故

    本身服务构建状态是没有用的,依赖的外部组件服务也一定会出问题,而且它们的可用性说不定远远比你想象的要低,那要怎么做?

    首先,我们内部是有一套集群控制系统的,我们内部分成了主机群、VIP集群和灰度集群这三个集群。每次发布的时候永远是会先发灰度集群,验证一段时间之后才会全让到其他集群上。这样的集群隔离也给我们带来另一个好处,一旦其中有一个集群出现了问题,比如说灰度集群的DB挂了,或者DB被写满了等其他的事故,我们可以很快速地把流量切换到主机群和VIP集群上面去,这得益于我们内部其实有一套集群管理的快速切换机制。

  • 服务降级:容灾缓存

  • 其次,做容灾缓存,假设依赖的服务全挂,服务自动启用容灾缓存,使用旧数据保证基本的可用性。

    年初我们有一次这样的事故,整个机房停电,机房就相当于消失了,导致后台服务全部都没有了,这种情况怎么做?这时候就只能是启用缓存容灾。我们网关本地的缓存是永远不会主动清除的,因为你使用旧数据也比直接报错要好,这时候我们就会使用一个旧数据来保证它的可用性。

    这个怎么理解呢?我们网关内部的数据它永远不会被清理,它只会说通过LRU的形式被清理掉,比如说我的内存里有可能会有很老的数据,昨天或者前天的数据,但是你在灾难发生的时候,即使是昨天还是前天数据它依然是有用的,它依然可以拿出来保证你最基本的可用性,下面是我们一个逻辑图,大家可以了解。

  • 服务降级:跳过非核心链路

  • 你的服务有可能会降级,我刚提到我们网关有鉴权的功能,鉴权功能其实依赖我们腾讯内部的一个组件。这样一个组件,它其实也是不稳定的,有时候会出问题,那么遇到这种问题怎么办?鉴权都没有办法鉴权了。这个时候我们在一些场景允许的情况下,会直接把鉴权的逻辑给跳过,我们不鉴权了,先放过一段时间,总比说我直接拒绝掉,直接报错这个请求要好得多。



    (三)核心业务层:网关自身灾备、异地多活

    最后一点就是我刚刚提到的,因为我们网关做了服务SET化改造、部署后,天然获得了跨AZ、跨地域热切换的能力。简单来讲,只要全网还剩一个网关可用区,业务流量就可以切换,网关的服务就不会宕机,当然切换现在还没有做到完全自动化,因为涉及到跨地域的切换,这个是需要人工介入的,不过说实话我们还没有遇到过这么大的灾难。

    做个小结,我们做了多集群切换、缓存容灾、柔性降级这些事情之后可以达到怎样一个效果:

  • 容许后台最多 (N-1) 个集群长时间故障

  • 容许后台全部集群短时间故障

  • 容许内部DNS全网故障



  • (四)核心业务层:还有什么能做的?

    我们还有什么能做的?如果某天,容器平台全网故障,怎么办?其实也是我们现在构思的一个东西,我们是不是可以做到一个异构部署这样一个形态。

    服务异构部署,即使容器平台全地域全可用区故障,也能切换到基于虚拟机的架构上,这也是我们正在筹划的一个事情。

    最后说完了容灾,接下来说怎么做监控告警?做监控告警其实比较老生常谈了,但是也可以在这里稍微扫个盲,我们的所有网关,它会把自己所有的访问日志推送到我们的ES(elasticsearch)的集群上,然后我们会有一个专门的TCB Alarm这样一个模块,它会去定期的轮询这样的日志,去检查这些日志里面有没有一些异常,比如说某个用户的流量突然高了,或者某个错误码突然增多,它会把这样的信息通过电话或者企业微信推送给我们。

    因为是基于ES的,所以监控可以做得非常精细,甚至可以做到感知到某个接口,今天的耗时比昨天要高超过50%,那这个接口是不是今天做什么变更让它变慢了?

    我们也可以做针对下游重点客户、业务的一些监控,比如说几个省的健康码,都可以做重点的监控。

    四、总结

    其实我只是选取了整个Node服务里面非常小的两个切面来讲,性能优化和高可用保障。可能很难覆盖到很全面,但是我想讲的稍微深一点,能够让大家有些足够的益处。

    首先,服务核心优化这里讲了长连接和缓存机制,可能是大部分服务或多或少都会遇到的问题。然后,链路架构优化这里讲了就近接入和Set化部署这样一个机制。高可用保障我主要是介绍了核心业务层的一些高可用保障,包括应对大流量冲击,怎么做缓存容灾,柔性降级,多可用区、多地域切换,监控告警这些东西。

    最后我想就演讲做一个总结。

    第一,Node.js服务与其它后台服务并无二致,遵循同一套方法论Node.js服务本质上也是做后台开发的,与其它后台服务并无二致,遵循同一套方法论。我今天的演讲如果把Node.js改成Golang改成Java,我就不站在这里了,可能我就去Golang的会上讲,实际上是一样的。

    第二,Node.js足以承载核心大规模服务,无须妄自菲薄我们这套网关其实也现网验证两年了,它跟别的技术栈的这种后台服务来讲,其实并没有太大的缺点。所以大家在拿Node.js做这种海量服务的时候,可以不用觉得Node.js好像只是个前端的小玩具,好像不是很适合这种成熟的业务,成熟业务是不是还是用Java来写,拿 C++来写,其实是没有必要的。

    当然,如果你真的需要对你的IO调度非常精细的时候,那么你可能得选用C++或者Rust,这样可以直接调度IO的方案。

    第三,前端处在技术的十字路口,不应自我局限于“Web前端”领域

    最后一个也是我今天想提的,可能我讲这么多,大家觉得我不是一个前端工程师对不对?但实际上我在公司内部的职级确实是个前端工程师。我一直觉得前端它是站在一个技术的十字路口的,所以大家工作中也好,还是学习中也好,不用把自己局限在“Web前端”这样一个领域。这次GMTC大会也可以看到,前端现在也不只是大家传统意义上的可能就是写页面这样一个领域。

    这是一个当年乔布斯演讲用的一个图,他说苹果是站在技术和人文的十字路口,实际上前端也是站在很多技术的十字路口上。

    那么我的演讲就到此结束,谢谢大家。


     作者简介


    王伟嘉

    腾讯前端开发工程师

    腾讯前端开发工程师,毕业于复旦大学,现任腾讯云CloudBase前端负责人,Node.js Core Collaborator,腾讯TC39代表。目前在腾讯云CloudBase团队负责微信云开发(原小程序·云开发)、Webify 等公有云产品的核心设计和研发,服务了下游数十万开发者和用户,对Node.js服务架构、全栈开发、云原生开发、Serverless有较丰富的经验,先后在阿里D2、GMTC、腾讯TWeb等大会上发表过技术演讲。



     推荐阅读


    Monorepo——探秘源码管理新姿势!

    Golang高质量单测之Table-Driven:从入门到真香!

    10分钟搞懂!消息队列选型全方位对比

    在线教程!C++如何在云应用中快速实现编译优化?

    CGO让Go与C手牵手,打破双方“壁垒”!


    Leo|20页PPT剖析唯品会API网关设计与实践

    编辑:友强


    摘要:API网关做为流量入口、公共服务接入点、公共技术扩展点,是互联网公司技术架构中重要的基础组件。这次刘璟宇Leo和大家一起分享唯品会API网关的设计、性能优化、实际应用过程中的一些经验。


    Leo|20页PPT剖析唯品会API网关设计与实践

    刘璟宇Leo

    唯品会资深研发工程师,在大型高性能分布式系统设计和开发方面有丰富的经验。目前在唯品会平台与架构部负责唯品会API网关和服务安全方面的设计、开发、运营工作。


    内容解析

    Leo|20页PPT剖析唯品会API网关设计与实践


    1. 为什么引入网关

    Leo|20页PPT剖析唯品会API网关设计与实践

    唯品会是一家专门做特卖的网站,唯品会网站是一个巨大型的网站,每张页面背后,都有多个服务提供静态资源和动态数据。

    这是唯品会网站上一张商品详情页面,内容是一款女式针织衫。页面里,除去静态页面、图片之外,有些动态内容:商品价格、促销提示语、产品介绍、商品库存等。每个部分都会从后端的一个或几个服务拉取数据。

    在唯品会公司内部,已经采用服务化的方式把服务进行了拆分,内部服务之间采用基于thrift的二进制协议通讯。这些服务不能直接对外部提供服务。

    在引入API网关前,我们在外部app、浏览器和内部服务之间会做一层webapp,起到两个作用:

    一个是从外部的http协议,适配到内部的二进制协议。

    另一个是对数据进行聚合。

    另外这些webapp里面还集成了如oauth等的一些公共服务。

    Leo|20页PPT剖析唯品会API网关设计与实践

    由于唯品会网站的业务众多、业务量也非常大,这种webapp的数量有数百个,实例数量数千个。

    在数量达到这种规模后,产生了一些问题,我们设想一个场景,比如某种安全防护技术需要升级一下,那么安全开发组需要先跟业务开发团队协商开发时间,等排期开发,然后需要测试,再排期发版。这样几十个业务开发团队升级下来,几个月可能就过去了。

    再设想一个场景,例如,我可能想app支持一下二进制协议,可以提升数据交换效率。

    一般我们做webapp,都是tomcat+springmvc这种结构进行开发,支持二进制协议就很困难。

    所以,目前这种webapp的架构,对于公共服务集成升级和公共技术的升级不是很友好。

    Leo|20页PPT剖析唯品会API网关设计与实践

    我们对架构进行了优化,引入了网关。网关的主要作用有三个:

    一个是协议适配

    另一个是公共服务接入

    最后是公共接入技术优化

    在外网和内网中间有了网关,网关本身和业务程序分离,就可以独立的对这些技术进行集成和升级。

    Leo|20页PPT剖析唯品会API网关设计与实践

    http://microservices.io/ 总结的微服务模式中,网关已经成为服务化中的一种标准模式。http://microservices.io/patterns/apigateway.html

    Leo|20页PPT剖析唯品会API网关设计与实践

    网关模式,被一些大型的互联网公司采用。国内主要有唯品会、百度、阿里、京东、携程、有赞等,国外主要有Netflix, Amazon, Mashape等。


    2. 选型和设计

    Leo|20页PPT剖析唯品会API网关设计与实践

    开源网关按照平台可以分为基于nginx平台的网关和自研网关

    基于nginx平台的网关有:

    KONG

    API Umbrella

    自研的网关有:

    apigee

    StrongLoop

    Zuul

    Tyk

    按照语言分类,可以见上图,有基于lua(nginx平台), nodejs, java, go等语言的网关。

    Leo|20页PPT剖析唯品会API网关设计与实践

    基于nginx平台的网关和自研网关的优势和劣势如下:


    基于nginx

    自研

    优势

    1. nginx有完善的处理http协议的能力

    2. 全异步高性能基础处理能力

    3. http处理过程中多个扩展点可进行扩展

    4. 开箱即用,基于openresty开发相对简单

    1. 可以完全掌控对http协议的处理过程

    2. 可以完全掌控异步化业务处理过程

    3. 对内部协议支持可以较好掌控

    4. 和内部的配置中心、注册中心结合较好

    劣势

    1. nginx工作流程复杂,对大多数人来说,只能当作黑盒子用,出问题难以真正在代码级理解根本原因,扩展核心功能较为困难。

    2. 基于openresty扩展,本身有性能开销,对javaerlanggo的性能优势不明显

    3. 对内部协议和基础组件支持不方便

    1. http协议处理有较多的坑需要踩

    2. 需要大量的性能优化过程,不像nginx经过大量实践,本身有较好的性能基础

    唯品会网关是基于netty自研的API网关。

    Leo|20页PPT剖析唯品会API网关设计与实践

    唯品会网关参考各种开源网关的实现,和业内各大电商网站的成熟经验,网关逻辑上可以分为四层;

    第一层是接入层,负责接入技术的优化。

    第二层是业务层,负责实现网关本身的一些业务实现。

    第三层是网关依赖的基于netty实现的各种公共组件

    最底层是netty负责NIO、内存管理、提供各种基础库、异步化框架等。

    Leo|20页PPT剖析唯品会API网关设计与实践

    业务层前面跟大家分享过,主要包括路由、协议转换、安全、认证验签、加密解密等,大家一看估计就可以看出,这些业务逻辑已经划分的比较独立,可以按照模块进行划分。实际上我们也是这样做的。

    业务层设计需要考虑哪些方面呢?

    一方面,是流程的组织。

    另一方面,网关需要依赖外部服务,需要考虑怎样异步化的调用外部服务。

    最后,网关需要考虑高可用,高可用在程序设计方面主要是不停机发布。唯品会网关的所有业务配置,都可以通过管理界面动态管理、动态下发、动态生效,并且支持灰度。

    Leo|20页PPT剖析唯品会API网关设计与实践

    业务层实现,最重要的一点,是将逻辑和数据分离,我们的实现方式,是业务逻辑实现在模块里,数据通过context传递,context通过模块之间相互调用时,通过接口传递。在异步化调用其他服务时,context保存在Channel的AttributeMap里,在异步完成时,回调,取出context。


    有了最基本的模块设计,我们再来看唯品网关怎样设计把这些流程串在一起。

    大家看一下上面的图,在执行业务逻辑时,有些业务逻辑需要串行,比如,路由校验、参数校验、IP黑白名单、WAF等,由于性能方面考虑,一般情况下,我们会先执行黑白名单模块,因为这块是cpu消耗最小、能拦掉部分请求的模块。

    后面再执行路由、参数等的校验。这部分是内存运算,效率也比较高,也能拦掉一些非法请求,所以先执行。

    然后进入outh、风控、设备指纹等的外部服务调用,这些调用将会并发的执行。

    执行后,将进行结果合并校验,如果在认证验签或风控等校验未通过的情况下,将会直接返回,如果校验通过,再进入后续的服务调用。

    服务调用过程,又进行了多选一的流程,可能用二进制协议也可能用HTTP协议等。最终进行后处理。

    Leo|20页PPT剖析唯品会API网关设计与实践

    Leo|20页PPT剖析唯品会API网关设计与实践

    大家可能会想,这些模块看上去可以使用actor模式进行封装,为何没有使用开源异步框架呢?我们也对开源的异步框架进行了详细的调研。在将异步框架结合进网关时发现对网关的性能产生了一些影响。

    目前较为流行的异步框架,主要有akka和quasar fibers。他们的实现形式不同,但原理基本差不多。

    为什么唯品网关没有引入异步框架呢。

    一方面是引入异步框架后,网关的抖动增加。

    一方面是成熟度问题,quasar fibiers quasar fibers的模式,更加友好一些,可以以接近同步编程的模式实现异步编程。但最新的release是0.7.6,没有大规模的验证过,我们也在实际使用踩了一些坑,例如,注解的问题、代码织入冲突问题、长时间运行突然响应变慢问题,强烈建议大家如果生产使用,需要慎重再慎重。


    我们总结了一下异步化框架适用于,大量依赖其他服务,经常被block的情况。

    网关的瓶颈在cpu运算,因为有验签、加解密、协议转换等cpu密集运算,其他的调用已经是全异步的,所以,引入异步框架的收益并不明显。

     

    上面分享了业务层的设计,下面分享一下公共组件的设计。

    网关不论调用依赖的服务还是后端的服务,都会遇到大量并发调用的情况。如果对连接不加以复用和控制,将造成大量的资源消耗和性能问题。因此,唯品网关自己设计优化了连接池。


    下面就分享一下唯品网关在连接池方面的设计。

    Leo|20页PPT剖析唯品会API网关设计与实践

    Leo|20页PPT剖析唯品会API网关设计与实践

    连接复用主要是指,一个连接可以被多个使用者同时使用,且互相之间不受影响,可以并发的发送多个请求,而应答是异步的,可复用的连接一般用于私有协议的连接,因为可复用的连接,请求可以一直发送,应答也不一定是按照请求顺序进行应答,就带来了一个问题,应答怎样才能和请求对应上。私有协议就比较容易在协议包内,增加sequence id,所以能达到连接复用的要求。唯品会网关调用唯品会内部的私有协议服务时,就采用的这种连接复用模式。

    连接复用还有一种实现模式,是spymemcache的模式,memcached本身不支持sequenceid,但同一个连接上的操作会保证顺序性,所以,spymemcache通过把请求缓存在queue中的形式,顺序匹配返回结果,达到连接复用。

    Leo|20页PPT剖析唯品会API网关设计与实践

    独占的连接模式,主要是指,一个连接同一时间只能被一个使用者使用,在一个连接上,发送完一个请求后,必须等待应答后,才能发送第二个请求。一般使用HTTP协议时,比较多使用这种独占的模式。因为如果HTTP协议需要支持连接复用,需要在HTTP协议头上增加sequence id,一般的服务端都不支持这种扩展,所以,我们针对HTTP协议,使用的是独占连接模式。

    Leo|20页PPT剖析唯品会API网关设计与实践

    连接池的异步化,在连接池使用的所有阶段都应该异步化。我们在设计网关的连接池时,考虑了以下几个方面:

    获取连接的异步化。从连接池获取连接,一般情况被认为是个没有block的动作,实际上分解来看,获取连接池,可能需要锁连接池对象所在的队列,操作连接池计数器时,可能会遇到锁、超时等问题。后面我会跟大家分享我们怎样去做的优化。

    连接使用就是说实际用连接去调用其他服务,这块的异步化,大家基本都会考虑到。

    归还连接的异步化。归还连接时,也会操作连接池中的连接队列,有时连接已经异常还会执行关闭连接等动作,所以也会产生锁的问题。和获取连接时类似,我们也把操作封装为task,交由netty做cpu亲缘性路由。


    Leo|20页PPT剖析唯品会API网关设计与实践


    3. 实践经验

    上面是给大家分享了我们在连接池设计中的几个关键点,接下来跟大家分享一下我们在实践过程中实际进行的优化。


    Leo|20页PPT剖析唯品会API网关设计与实践

    jvm启动后,会在/tmp下建立一个文件,是一个内存映射文件,JVM用来导出状态数据给其它进程使用,比如jstat,jconsole等。当到达安全点时,JVM会把安全点的相关信息写入到这个文件中去。安全点是说,jvm会在这个点上,把所有其他线程都停下来,自己安全的做一些事情,GC是一种安全点,还有其他种类的安全点。而gc log和这种监控数据的写入,就是在安全点上进行写入。当IO频发且负载均重时,可能写数据动作刚好赶上操作系统将磁盘缓存刷到磁盘的过程,此时写性能数据文件的操作就会被block。最终表现为jvm暂停。解决方法,是将这些性能数据写到内存文件中,避免和其他操作抢占磁盘io。

     

    StringBuffer在写日志等处理字符串拼接的场景下经常用到,大多数情况下,我们会new一个StringBuffer,向里面追加字符串,在高并发场景,这个过程会产生大量的内存重新分配并拷贝内容的动作,造成cpu热点。我们的优化方法是,在threadlocal缓存使用过的stringbuffer,在下次使用时,直接复用。

      

    我们在初期实际使用网关时观察到,网关的OLD区使用会缓慢上升,大概两天会产生一次FGC,经过仔细的分析,发现,java NIO的server socket类由finalize最后进行释放。而GC过程是第一次GC先将没有引用的对象放入finalize队列,下次GC的时候,调用finalize,并将对象释放。而在高并发的情况下,server socket的finalize并不保证被调用,所以存活时间可能超过了升级阈值,就会有对象不断进入old区。

    即使ref queue很快被执行,也可能跨两次ygc,比如创建后接着一次ygc1,然后用完后在下一次ygc2中添加到ref queue,ref queue没有堆积的情况下,需要在ygc3中释放这些对象。

    Leo|20页PPT剖析唯品会API网关设计与实践

    由于网关会并发接受大量的请求,所以写日志的量非常大。我们实际压测的时候发现,写日志的IO操作,会周期性的被block,从而产生抖动。经过分析发现,被block的时候,操作系统在刷磁盘缓存。linux默认是脏数据超过10%,或5s刷一次缓存,而这时可能会有大量数据在缓存里等待写入磁盘,操作系统再去刷盘的时候,就会消耗比较多的时间,而这些时间内,应用无法将数据写入磁盘缓存,发生block。有两个参数可以调整,一个是脏数据占比,一个是脏数据两个取较小值生效。我们通过调小脏数据比率,让刷盘动作在数据量较小的时候就开始,减小了毛刺率。

     

    招贤纳士,联系Leo

    Leo|20页PPT剖析唯品会API网关设计与实践

    Leo|20页PPT剖析唯品会API网关设计与实践

    为庆祝中生代技术成立一周年, 由中生代技术组织的软件技术领域顶级盛会,将于2017年3月于北京、成都、上海开幕。3月11日上海站年度大会,集结了百度,阿里,华为,蘑菇街,拍拍贷,七牛,携程,盛派网,沪江网等公司大咖,共襄盛举。3.11,等你来约!6大主题,20余话题,大数据,FinTech,架构之道,移动技术,研发管理等主题,囊括国内顶尖技术专家,一起学习面基,促进技术交流!


    最后小编来送

    福利1:输入优惠码“youjun”,给你更多折扣

    福利2:评论精彩者获得特惠优惠码【单人票5折】5人; 点赞最多获得免费票1张

    福利3:欢迎各公司CEO/CTO/架构师/行业专家参与分享,参与分享者可免费获得大会门票。

    以上是关于海量Node.js网关的架构设计与工程实践!的主要内容,如果未能解决你的问题,请参考以下文章

    大规模 Node.js 网关架构设计与工程实践

    腾讯云十亿级 Node.js 网关的架构设计与工程实践

    API网关很重要

    Leo|20页PPT剖析唯品会API网关设计与实践

    万字干货 | 荔枝魔方基于云原生的架构设计与实践

    设计 Node.js REST API 的 10 条最佳实践