构建高可用网关之容错实践

本贴最后更新于 2561 天前,其中的信息可能已经时移世易

自从微服务概念以来,众多的软件架构在践行着这一优秀的设计理念。各自的系统在这一指导思想下收获了优雅的可维护性,但一方面也给接口调用提出了新的要求。比如众多的 API 调用急需一个统一的入口来支持客户端的调用。在这种情况下 API GATEWAY 诞生,我们将接入、路由、限流等功能统一由网关负责,各自的服务提供方专注于业务逻辑的实现,从而给客户端调用提供了一个稳健的服务调用环境。之后,我们在网关大调用量的情况下,还要保证网关的可降级、可限流、可隔离等等一系列容错能力。

一、网关

这里说的网关是指 API 网关,直面意思是将所有 API 调用统一接入到 API 网关层,有网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个 API 服务提供团队可以专注于自己的的业务逻辑处理,而 API 网关更专注于安全、流量、路由等问题。

1.1、单体应用

ee774d2f27984c698ae3d226c18127cc.png

单体应用.png

业务简单,团队组织很小的时候,我们常常把功能都集中于一个应用中,统一部署,统一测试,玩的不易乐乎。但随着业务迅速发展,组织成员日益增多。我们再将所有的功能集中到一个 TOMCAT 中去,每当更新一个功能模块的时候,势必要更新所有的程序。搞不好,还要牵一发动全身。实在难以维护。

1.2、微服务

5152f42e3830452d9dfddfd45b083408.png

系统微服务化.png

单体应用满足不了我们逐渐增长的扩展需求之后,微服务就出现了,它将原来集中于一体的比如商品功能、订单功能、用户功能拆分出去,各自有各自的自成体系的发布、运维等。这样就解决了在单体应用下弊端。

1.3、API 网关

b003a9c1e42d456194c39614a6151a66.png

API 网关.png

微服务后,那么原先客户端调用服务端的地方,就要有 N 多个 URL 地址,商品的、订单的、用户的。这个是就必须要有个统一的入口和出口,这种请情况下,我们的 API GATEWAY 就出现了。就很好的解决了微服务下客户端调用的问题。

1.4、泛化调用

64f2da90f66f4ae9b5539a18c8bf6046.png

泛化实现.png

普通的 RPC 调用,我要拿到服务端提供的 class 或者 jar 包。但这样实在太重,更不好维护。不过,成熟的 RPC 框架都支持泛化调用,我们的网关就是基于这种泛化调用来实现的。服务端开放出来他们的 API 文档,我们拿到接口、参数、参数类型通过泛化调用到服务端程序。
public Object $invoke(String method, String[] parameterTypes, Object[] args);

二、容错

容错,这个词的理解,直面意思就是可以容下错误,不让错误再次扩张,让这个错误产生的影响在一个固定的边界之内,“千里之堤毁于蚁穴”我们用容错的方式就是让这种蚁穴不要变大。那么我们常见的降级,限流,熔断器,超时重试等等都是容错的方法。

2.1、抗量

120e884ac60d408e844915545c4c5b79.png

TOMCAT nio.png

所谓抗量其实就是增大我们系统的吞吐量。所以容错的第一步就是系统要能抗量,没有量的情况下几乎用不到容错。我们的容器使用的是 TOMCAT,在传统的 BIO 模型下,一请求一线程,在机器线程资源有限的情况下是没有办法来实现我们的目标。NIO 给我们提供了这个机会,基于 NIO 的机制,利用较少的线程来处理更多的连接。连接多不可怕,通过调整机器的参数一台 8c8g 的机器,超过 10w 是不成问题的。tomcat 的 conector 修改成 nio 之后我们再从代码层面又引入了 Servlet3,他是从 tomcat7 以后支持的,nio 是 tomcat6 以后就支持了。利用 Servlet3 的特性,所有的 request 和 response 都有 tomcat 的工作线程来处理,我们将业务逻辑异步到别的业务线程中去。异步环境下,可以提高单位时间内的吞吐量。所有的 Servlet 请求都是有 tomcat 的 Executor 线程池的线程处理的,也就是 tomcat 的工作线程。这些线程处理的时间越短越好,就会迅速的将线程归还给 Executor 线程池。现在 Servlet 支持异步后就能将耗时的操作比如有 RPC 请求的交给业务线程池来处理,使得 tomcat 工作线程可以立即归还给 tomcat 工作线程池。另外将业务异步处理之后,我们可以对业务线程池进行线程池隔离,这样避免了因一个业务性能问题影响了其它的业务。

48b2be0b85e1463f8dd4c239e8e1b9b6.png

Servlet3 异步.png

总结了一下异步的优势
1、可以用来做消息推送,通过 nginx 做代理,设置连接超时时间,客户端通过心跳探测。
2、提高吞吐量,就像上面说的。
3、请求线程和业务线程分开,从而可以通过业务线程池对业务线程做隔离。

2.2、脱离 DB

03415bf2b8514475b8cd8550387e7d2f.png

数据异构.png

这里不是说 DB 的性能不行,分库分表,DB 集群化之后,在一定量的情况下是没有问题的。但是,如果从抗量的角度说的话为何不使用 REDIS 呢,如果软件架构里面有一种银弹的话那么 REDIS 就是这种银弹。另外一个脱离 DB 的原因是,每当大促备战前夕我们一项重点的工作就是优化慢 sql,但是它就像小强一样生命力是那样的顽强,杀不绝。如果有那么一个慢 sql 平时没有问题,比如一个查询大字段的 sql,平是量小不能暴露问题,但是量一上来就是个灾难。再就是我们的网关,包括接入,分发,限流等等这些功能都应该是很轻的。所以我们就通过数据异构的方式把数据重新转载的 redis 中,而且是将数据持久化到 redis 里面去。当然使用 redis 的过程中也需要注意大 key,大访问量下也能让集群趴下。还有一个很重要的原因,要说一下的,我们使用的 DB 是 mysql,鉴于 mysql 的 failover 机制,生效时间总是要长于 redis 集群,还有就是因为 DB 切换的时候,常常伴随 web 应用服务器要重启,将原来的连接释放掉,以便使用新的数据库的连接。

2.3、多级缓存

5d7103c369aa453599edc9f3afda409a.png

多级缓存.png

最简单的缓存就是查一次数据库然后将数据写入缓存比如 redis 中并设置过期时间。因为有过期失效因此我们要关注下缓存的穿透率,这个穿透率的计算公式,比如查询方法 queryOrder(调用次数 1000/1s)里面嵌套查询 DB 方法 queryProductFromDb(调用次数 300/s),那么 redis 的穿透率就是 300/1000,在这种使用缓存的方式下,是要重视穿透率的,穿透率大了说明缓存的效果不好。还有一种使用缓存的方式就是将缓存持久化,也就是不设置过期时间,这个就会面临一个数据更新的问题。一般有两种办法,一个是利用时间戳,查询默认以 redis 为主,每次设置数据的时候放入一个时间戳,每次读取数据的时候用系统当前时间和上次设置的这个时间戳做对比,比如超过 5 分钟,那么就再查一次数据库。这样可以保证 redis 里面永远有数据,一般是对 DB 的一种容错方法。还有一个就是真正的让 redis 做为 DB 使用。就是图里面画的通过订阅数据库的 binlog 通过数据异构系统将数据推送给缓存,同时将将缓存设置为多级。可以通过使用 jvmcache 作为应用内的一级缓存,一般是体积小,访问频率大的更适合这种 jvmcache 方式,将一套 redis 作为二级 remote 缓存,另外最外层三级 redis 作为持久化缓存。

2.4、超时与重试

超时与重试机制也是容错的一种方法,凡是发生 RPC 调用的地方,比如读取 redis,db,mq 等,因为网络故障或者是所依赖的服务故障,长时间不能返回结果,就会导致线程增加,加大 cpu 负载,甚至导致雪崩。所以对每一个 RPC 调用都要设置超时时间。对于强依赖 RPC 调用资源的情况,还要有重试机制,但是重试的次数建议 1-2 次,另外如果有重试,那么超时时间就要相应的调小,比如重试 1 次,那么一共是发生 2 次调用。如果超时时间配置的是 2s,那么客户端就要等待 4s 才能返回。因此重试 + 超时的方式,超时时间要调小。这里也再谈一下一次 PRC 调用的时间都消耗在哪些环节,一次正常的调用统计的耗时主要包括:
① 调用端 RPC 框架执行时间 + ② 网络发送时间 + ③ 服务端 RPC 框架执行时间 + ④ 服务端业务代码时间。调用方和服务方都有各自的性能监控,比如调用方 tp99 是 500ms,服务方 tp99 是 100ms,找了网络组的同事确认网络没有问题。那么时间都花在什么地方了呢,两种原因,客户端调用方,还有一个原因是网络发生 TCP 重传。所以要注意这两点。

2.5、熔断

熔断技术可以说是一种“智能化的容错”,当调用满足失败次数,失败比例就会触发熔断器打开,有程序自动切断当前的 RPC 调用,来防止错误进一步扩大。实现一个熔断器主要是考虑三种模式,关闭,打开,半开。各个状态的转换如下图。

df21d0a7c7b6448fb3a85c116aaad600.png

熔断状态.png

那么了解了熔断器的状态机制,我们可以自己来实现一个熔断器。当然也可以使用开源的解决方案比如 Hystrix 中的 breaker。下图是一个熔断器打开关闭的示意图。

4f38c1a4d1e8464b845802bcdf47f364.png

熔断状态 2.png

这里要谈的是熔断器的使用注意项。我们在处理异常的时候,要根据具体的业务情况来决定处理方式,比如我们调用商品接口,对方只是临时做了降级处理,那么作为网关调用就要切到可替换的服务上来执行或者获取托底数据,给用户友好提示。还有要区分异常的类型,比如依赖的服务崩溃了,这个可能需要花费比较久的时间来解决。也可能是由于服务器负载临时过高导致超时。作为熔断器应该能够甄别这种异常类型,从而根据具体的错误类型调整熔断策略。增加手动设置,在失败的服务恢复时间不确定的情况下,管理员可以手动强制切换熔断状态。最后,熔断器的使用场景是调用可能失败的远程服务程序或者共享资源。如果是本地缓存本地私有资源,使用熔断器则会增加系统的额外开销。还要注意,熔断器不能作为应用程序中业务逻辑的异常处理替代品。

2.6、线程池隔离

429d9579bec44641b86c539cb5cc5a72.png

线程隔离.png

在抗量这个环节,Servlet3 异步的时候,有提到过线程隔离。线程隔离的之间优势就是防止级联故障,甚至是雪崩。当网关调用 N 多个接口服务的时候,我们要对每个接口进行线程隔离。比如,我们有调用订单、商品、用户。那么订单的业务不能够影响到商品和用户的请求处理。如果不做线程隔离,当访问订单服务出现网络故障导致延时,线程积压最终导致整个服务 CPU 负载满。就是我们说的服务全部不可用了,有多少机器都会被此刻的请求塞满。那么有了线程隔离就会使得我们的网关能保证局部问题不会影响全局。

2.7、降级、限流

关于降级限流的方法业界都已经有很成熟的方法了,比如 FAILBACK 机制,限流的方法令牌桶,漏桶,信号量等。这里谈一下我们的一些经验,降级一般都是由统一配置中心的降级开关来实现的,那么当有很多个接口来自同一个提供方,这个提供方的系统或这机器所在机房网络出现了问题,我们就要有一个统一的降级开关,不然就要一个接口一个接口的来降级。也就是要对业务类型有一个大闸刀。还有就是 降级切记暴力降级,什么是暴力降级的,比如把论坛功能降调,结果用户显示一个大白板,我们要实现缓存住一些数据,也就是有托底数据。限流一般分为分布式限流和单机限流,如果实现分布式限流的话就要一个公共的后端存储服务比如 redis,在大 nginx 节点上利用 lua 读取 redis 配置信息。我们现在的限流都是单机限流,并没有实施分布式限流。

2.8、网关监控与统计

d690b3bdf88e40c3b578c40bcdae2dde.png

监控.png

API 网关是一个串行的调用,那么每一步发生的异常要记录下来,统一存储到一个地方比如 elasticserach 中,便于后续对调用异常的分析。鉴于公司 docker 申请都是统一分配,而且分配之前 docker 上已经存在 3 个 agent 了,不再允许增加。我们自己实现了一个 agent 程序,来负责采集服务器上面的日志输出,然后发送到 kafka 集群,再消费到 elasticserach 中,通过 web 查询。现在做的追踪功能还比较简单,这块还需要继续丰富。

三、总结

网关基本功能有统一接入、安全防护、协议适配等。这篇文章里面我们并没有讲如何来实现这些基本的功能,因为现在有很多成熟的解决方案可以直接拿过来使用,比如 spring cloud 这种全家桶里面的很多组件,Mashape 的 API 层 Kong 等等。我们更关注的是实现了这些网关的基本功能之后,如何保证一个网关的运行,在大访问量的情况下如何能更好的支持客户端的调用,在突发情况下又是如何及时的响应这种突然的异常,如何将错误最小化,防止级联故障。我们的重点关注的是网关容错方面的经验与实践。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...