转载:京东京麦:微服务架构下的高可用网关与容错实践(有彩蛋)
本文根据DBAplus社群第130期线上分享整理而成,文末还有好书送哦~
讲师介绍
王新栋
京东资深架构师
从事京麦平台的架构设计与开发工作。熟悉各种开源软件架构,在Web开发,架构优化上有较丰富的实战经历。
有多年在NIO领域的设计、开发经验,对HTTP、TCP长连接技术有深入研究与领悟,目前主要致力于移动与PC平台网关技术的优化与实现。
个人公众号:程序架道(xindongbook17)
自微服务概念诞生以来,众多的软件架构都在践行着这一优秀的设计理念。各自的系统在这一指导思想下收获了优雅的可维护性,但一方面也给接口调用提出了新的要求。比如众多的API调用急需一个统一的入口来支持客户端的调用。在这种情况下API Gateway诞生,我们将接入、路由、限流等功能统一由网关负责,各自的服务提供方专注于业务逻辑的实现,从而给客户端调用提供了一个稳健的服务调用环境。之后,我们在网关大调用量的情况下,还要保证网关的可降级、可限流、可隔离等等一系列容错能力。
今天借社群这个平台,跟大家分享微服务下京麦开放平台的网关实现,以及我们如何抗量、如何在大访问量的情况下做容错处理。重点介绍容错的方法及每种容错方法的使用场景与经验。
一、网关
这里说的网关是指API网关,直面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。
单体应用
业务简单、团队组织很小时,我们常常把功能都集中于一个应用中,统一部署、统一测试,玩得不易乐乎。但随着业务迅速发展,组织成员日益增多,我们再将所有的功能集中到一个Tomcat中去时,每当更新一个功能模块,势必要更新所有的程序,搞不好,还要牵一发动全身,实在难以维护。
微服务
单体应用满足不了我们逐渐增长的扩展需求之后,微服务就出现了,它将原来集中于一体的比如商品功能、订单功能、用户功能拆分出去,各自有各自的自成体系的发布、运维等,这样就解决了在单体应用下的弊端。
API网关
进行微服务后,原先客户端调用服务端的地方就要有N多个URL地址,包括商品的、订单的、用户的。这时就必须要有个统一的入口和出口,这种情况下,我们的API Gateway就出现了,很好地帮助我们解决了微服务下客户端调用的问题。
泛化调用
对于普通的RPC调用,我要拿到服务端提供的class或者jar包,这样过于繁重,更不好维护。不过成熟的RPC框架都支持泛化调用,我们的网关就是基于这种泛化调用来实现的。服务端开放出来他们的API文档,我们拿到接口、参数、参数类型通过泛化调用到服务端程序。
public Object $invoke(String method, String[] parameterTypes, Object[] args);
二、容错
容错,这个词的理解,书面意思就是可以容下错误,不让错误再次扩张,让这个错误产生的影响在一个固定的边界之内,“千里之堤,毁于蚁穴”,我们采用容错的方式就是不让这种蚁穴继续变大。在工作中,降级、限流、熔断器、超时重试等都是常见的容错方法。
抗量
所谓的抗量,其实就是增大我们系统的吞吐量。所以容错的第一步就是系统要能抗量,没有量的情况下几乎用不到容错。我们的容器使用的是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工作线程池。另外,将业务异步处理之后,我们可以对业务线程池进行线程池隔离,这样就避免了因一个业务性能问题而影响了其它的业务。
总结一下异步的优势:
1、可以用来做消息推送,通过Nginx做代理,设置连接超时时间,客户端通过心跳探测。
2、提高吞吐量,就像上面说的。
3、请求线程和业务线程分开,从而可以通过业务线程池对业务线程做隔离。
关于Servlet3的异步原理与实践可以参看笔者以前写的文章《Servlet3异步原理与实践》。
脱离DB
这里不是说DB的性能不行,分库分表、DB集群化之后,在一定量的情况下是没有问题的。但是,如果从抗量的角度说的话,为何不使用Redis呢?如果软件架构里面有一种银弹的话,那么Redis就是这种银弹。
另外一个脱离DB的原因是,每当大促备战前夕我们一项重点的工作就是优化慢SQL,但它就像小强一样生命力是那样的顽强,杀不绝。如果有那么一个慢SQL,平时是没有问题的,比如一个查询大字段的SQL,平时量小不会暴露问题,但量一上来了,就是个灾难。
再就是我们的网关,包括接入、分发、限流等这些功能都应该是很轻的,所以我们就通过数据异构的方式把数据重新转载到Redis中,而且是将数据持久化到Redis里面去。当然,使用Redis的过程中也需要注意大key,大访问量下也能让集群趴下。
还有一个很重要的原因,我觉得必须说一下,我们使用的DB是MySQL,鉴于MySQL的failover机制生效时间总是要长于Redis集群,最后就是因为DB切换的时候,常常伴随Web应用服务器要重启,将原来的连接释放掉,才能方便使用新的数据库连接。
多级缓存
最简单的缓存就是查一次数据库然后将数据写入缓存,比如在Redis中设置过期时间。因为有过期失效,因此我们要关注下缓存的穿透率,这个穿透率的计算公式,比如查询方法queryOrder(调用次数1000/1s)里面嵌套查询DB方法query Product From Db(调用次数300/s),那么Redis的穿透率就是300/1000,在这种使用缓存的方式下,是要重视穿透率的,穿透率大了说明缓存的效果不好。
还有一种使用缓存的方式就是将缓存持久化,也就是不设置过期时间,这个会面临一个数据更新的问题。
一般有两种办法,一个是利用时间戳,查询默认以Redis为主,每次设置数据的时候放入一个时间戳,每次读取数据的时候用系统当前时间和上次设置的这个时间戳做对比,比如超过5分钟,那么就再查一次数据库,这样可以保证Redis里面永远有数据,一般是对DB的一种容错方法。
还有一个就是让Redis真正作为DB来使用。就是图里画的通过订阅数据库的binlog,通过数据异构系统将数据推送给缓存,同时将将缓存设置为多级。可以通过使用jvmcache作为应用内的一级缓存,一般是体积小,访问频率大的更适合这种jvmcache方式,将一套Redis作为二级remoto缓存,另外的最外层三级Redis作为持久化缓存。
超时与重试
超时与重试机制也是容错的一种方法,凡是发生RPC调用的地方,比如读取Redis、DB、MQ等,因为网络故障或者是所依赖的服务故障了,长时间不能返回结果,就会导致线程增加,加大CPU负载,甚至导致雪崩。所以对每一个RPC调用都要设置超时时间。
对于强依赖RPC调用资源的情况,还要有重试机制,但重试的次数建议1-2次,另外如果有重试,超时时间还要相应都调小,比如重试1次,那么一共是发生2次调用。如果超时时间配置的是2s,那么客户端就要等待4s才能返回。因此重试+超时的方式,超时时间要调小。
这里也再谈一下一次PRC调用的时间都消耗在哪些环节。一次正常的调用统计的耗时主要包括:
①调用端RPC框架执行时间 + ②网络发送时间 + ③服务端RPC框架执行时间 + ④服务端业务代码时间。
调用方和服务方都有各自的性能监控,比如调用方tp99是500ms,服务方tp99是100ms,找了网络组的同事确认网络没有问题。那么时间都花在什么地方了呢?两种原因:客户端调用方,还有一个原因是网络发生TCP重传,所以要注意这两点。RPC耗时详细内容可以参看笔者写的另外一篇文章《一次RPC调用时间都去哪儿了》。
熔断
熔断技术可以说是一种“智能化的容错”,当调用满足失败次数,失败比例就会触发熔断器打开,有程序自动切断当前的RPC调用,来防止错误进一步扩大。实现一个熔断器主要是考虑三种模式:关闭、打开、半开。各个状态的转换如下图。
在了解了熔断器的状态机制后,我们可以自己来实现一个熔断器。当然也可以使用开源的解决方案,比如Hystrix中的breaker。下图是一个熔断器打开关闭的示意图。
这里要谈的是熔断器的使用注意项。我们在处理异常时,要根据具体的业务情况来决定处理方式,比如我们调用商品接口,对方只是临时做了降级处理,那么作为网关调用就要切到可替换的服务上来执行或者获取托底数据,给用户友好提示。还有要区分异常的类型,比如依赖的服务崩溃了,这个可能需要花费比较久的时间来解决,也可能是由于服务器负载临时过高导致超时。作为熔断器应该能够甄别这种异常类型,从而根据具体的错误类型调整熔断策略。增加手动设置,在失败的服务恢复时间不确定的情况下,管理员可以手动强制切换熔断状态。最后,熔断器的使用场景是调用可能失败的远程服务程序或者共享资源。如果是本地缓存本地私有资源,使用熔断器则会增加系统的额外开销。还要注意,熔断器不能作为应用程序中业务逻辑的异常处理替代品。
关于熔断的原理与实践可以参照这篇文章《Hystrix熔断器技术解析》。
线程池隔离
在抗量这个环节,Servlet3异步时,有提到过线程隔离。线程隔离的之间优势就是防止级联故障,甚至是雪崩。当网关调用N多个接口服务的时候,我们要对每个接口进行线程隔离,比如我们有调用订单、商品、用户。那么订单的业务不能够影响到商品和用户的请求处理。如果不做线程隔离,当访问订单服务出现网络故障导致延时,线程积压最终导致整个服务CPU负载满,就是我们说的服务全部不可用了,有多少机器都会被此刻的请求塞满。那么,有了线程隔离就会使得我们的网关能保证局部问题不会影响全局。
降级、限流
关于降级限流的方法业界都已经有很成熟的方法了,比如Failback机制,限流方法令牌桶、漏桶、信号量等。这里谈一下我们的一些经验。
降级一般都是由统一配置中心的降级开关来实现的,那么当有很多个接口来自同一个提供方,这个提供方的系统或这机器所在机房网络出现了问题,我们就要有一个统一的降级开关,不然就要一个接口一个接口地来降级,也就是要对业务类型有一个大闸刀。
还有就是降级切记暴力降级,什么是暴力降级?比如把论坛功能降调,结果用户显示一个大白板,我们要实现缓存住一些数据,也就是有托底数据。限流一般分为分布式限流和单机限流,如果实现分布式限流的话就要一个公共的后端存储服务比如Redis,在大Nginx节点上利用Lua读取Redis配置信息。我们现在的限流都是单机限流,并没有实施分布式限流。
网关监控与统计
API网关是一个串行的调用,每一步发生的异常都要记录下来,统一存储到一个地方比如Elasticserach中,便于后续对调用异常的分析。鉴于公司Docker申请都是统一分配,而且分配之前Docker上已经存在3个agnet了,不再允许增加。我们自己实现了一个agnet程序,来负责采集服务器上面的日志输出,然后发送到kafka集群,再消费到Elasticserach中,通过Web查询。现在做的追踪功能还比较简单,这块还需要继续丰富。
三、总结
网关基本功能有统一接入、安全防护、协议适配等。这篇文章里我们并没有讲如何来实现这些基本的功能,因为现在有很多成熟的解决方案可以直接拿过来使用,比如Spring Cloud这种全家桶里面的很多组件,Mashape的API层Kong等。我们更关注的是实现了这些网关的基本功能之后,如何保证一个网关的运行,在大访问量的情况下如何能更好的支持客户端的调用,在突发情况下又是如何及时地响应这种突然的异常,如何将错误最小化,防止级联故障。我们的重点关注的是网关容错方面的经验与实践。
参考资料
https://martinfowler.com/articles/microservices.htm
lhttps://tomcat.apache.org/tomcat-7.0-doc/config/http.html
http://www.cnblogs.com/davenkin/p/async-servlet.html
《Tomcat内核设计剖析》
Q&A
Q1:异构数据库推送数据到Redis,是表数据还是联合查询后的数据?
A1:热点数据,不会是全量查询表,根据业务条件查询出来的结果集合。你可以理解为视图那样的方式,只是落地了另外一个库。
Q2:8c8g的机器,连接超过10w,机器调整哪些参数?
A2:
# 查看当前用户允许TCP打开的文件句柄最大数
ulimit -n
# 修改文件句柄
vim /etc
curity
mits.conf
* soft nofile 655350
* hard nofile 655350
注意:
soft nofile (软限制)是指Linux在当前系统能够承受的范围内进一步限制用户同时打开的文件数;
hard nofile (硬限制)是根据系统硬件资源状况(主要是系统内存)计算出来的系统最多可同时打开的文件数量;
通常软限制小于或等于硬限制。
Q3:业务线程池使用JDK自带的还是贵公司研发的?如何优雅地关停,尤其是还有事务在处理?
A3:JDK自带的。事务,我们主要是考虑最终一致性就好了。互联网应用CAP里面 ,保证AP。
Q4:Tomcat上还要调整那些参数?
A4:Tomcat 改成 NIO。1000并发,目前线上是这样配置的。
直播链接
https://m.qlchat.com/topic/details?topicId=2000000447502593