自微服务概念诞生以来,很多软件架构都在实践这一优秀的设计理念。在这种指导思想下,各自的系统都实现了优雅的可维护性,但一方面也对接口调用提出了新的要求。比如很多API调用,迫切需要一个统一的入口来支持客户端调用。在这种情况下,APIGateway应运而生。我们通过网关统一接入、路由、限流等功能,各自的服务提供者专注于业务逻辑的实现,从而为客户端调用提供了健壮的服务调用环境。之后在大量调用网关的情况下,还要保证网关的可降级、限流、隔离等一系列容错能力。今天给大家分享一下京麦开放平台在微服务下的网关实现,以及在大流量的情况下我们如何抗流量,如何做容错处理。网关这里所说的网关指的是API网关,意思是所有的API调用都统一连接到API网关层,由网关层统一连接输出。网关的基本功能有以下能力:统一接入安全保护协议适配流量控制长短链路支持容错有了网关,各个API服务商团队可以专注于自己的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。单体应用当业务简单,团队组织规模较小时,我们往往会把所有的功能都集中在一个应用中,统一部署和测试,玩得不亦乐乎。但是随着业务的快速发展和组织成员的不断增加,我们将把所有的功能都集中到一个Tomcat中。每当一个功能模块更新时,所有的程序都要更新,这可能会牵一发而动全身,造成真正难以维护的局面。微服务是在微服务的单体应用无法满足我们日益增长的扩展需求后出现的。它是一种集成了原始应用程序的架构。比如产品功能、订单功能、用户功能分离,各自有自己的发布、运维等自成体系,解决了单一应用的弊端。API网关实现微服务后,客户端调用服务端的URL地址必须有N个以上,包括商品、订单、用户。这时,必须有统一的出入口。在这种情况下,我们的APIGateway就出现了,帮助我们解决了微服务下客户端调用的问题。广义调用普通的RPC调用,需要获取服务器提供的class或者jar包,太重,维护难度大。但是成熟的RPC框架都支持泛化调用,我们的网关就是基于这种泛化调用实现的。服务端打开他们的API文档,我们得到接口,参数,参数类型,通过泛化调用给服务端程序。publicObject$invoke(String方法,String[]parameterTypes,Object[]args);容错,对这个词的理解,写的意思就是可以容忍错误,防止错误再次扩大,让这个错误的影响在一个固定的范围内。“千里之堤,毁于一蚁巢。”我们采用容错的方法来防止这个蚁巢继续增长。在工作中,降级、限流、熔断、超时重试等都是常见的容错方式。抵抗力所谓抵抗力就是要提高我们系统的吞吐量,所以容错的第一步就是要让系统能够抵抗,在没有体积的情况下几乎没有容错能力。我们的容器使用Tomcat。在传统的BIO模型下,一个请求一个线程,在机器线程资源有限的情况下是没有办法达到我们的目的的。NIO为我们提供了这个机会,基于NIO机制,用更少的线程处理更多的连接。连接数并不可怕。通过调整机器的参数,一台8c8g的机器,超过10w不是问题。将Tomcat的Conector修改为NIO后,我们从代码层面引入了Servlet3,从Tomcat7开始支持,而NIO是从Tomcat6开始支持的。利用Servlet3的特性,所有请求和响应都由Tomcat工作线程处理,我们将业务逻辑异步传递给其他业务线程。在异步环境下,可以提高单位时间内的吞吐量。所有的Servlet请求都是由Tomcat的Executor线程池中的线程处理的,也就是Tomcat的工作线程。这些线程的处理时间越短越好。时间越短,线程返回Executor线程池的速度就越快。现在Servlet支持异步,RPC请求等耗时操作可以交给业务线程池处理。让Tomcat工作线程立即返回到Tomcat工作线程池中。另外,业务异步处理后,我们可以隔离业务线程池的线程池,避免业务性能出现问题影响其他业务。总结一下异步的优点:可以用来推送消息,使用Nginx做代理,设置连接超时时间,客户端可以通过心跳检测。提高吞吐量。请求线程和业务线程分离,可以通过业务线程池实现业务线程的隔离。脱离DB脱离DB并不代表DB的性能不好。分库分表和DB集群后,在一定数量的情况下是没有问题的。但是在体积上为什么不用Redis呢?如果在软件架构中有一颗银弹,那么Redis就是那颗银弹。离开DB的另一个原因是:我们在准备大促前夕的一项重点工作就是优化慢SQL,但它的生命力像小强一样顽强,杀不死。如果有这么慢的SQL,一般是没有问题的。比如查询大字段的SQL,如果平时很小,问题不会暴露,但是当量大了,那就是灾难了。然后是我们的网关,包括接入、分发、限流等,这些功能应该是很轻的,所以我们通过数据异构将数据重新打印到Redis中,把数据持久化到Redis中去。当然,在使用Redis的过程中,也需要注意大key,在大流量下也会拉垮集群。还有一个很重要的原因就是我们使用的DB是MySQL。由于MySQL的failover机制总是比Rediscluster耗时长,这是因为在切换DB的时候,往往需要重启web应用服务器,而原来的连接被释放后,才能方便的使用新的数据库连接。多级缓存最简单的缓存就是查一次数据库,然后把数据写入缓存。比如Redis中设置过期时间,因为有过期时间,所以需要注意缓存的穿透率。这个渗透率的计算公式,比如如果查询方法queryOrder(调用次数1000/1s)嵌套在查询DB方法queryProductFromDB(调用次数300/s)中,那么Redis的渗透率是300/1000。这种使用缓存的方式,需要注意渗透率。渗透率高说明缓存效果不好。另一种使用缓存的方式是让缓存持久化,即不设置过期时间,这会面临一个数据更新的问题。一般有两种方法:使用时间戳,查询默认是基于Redis的,每次设置数据的时候放一个时间戳,读取数据时将系统当前时间和你上次设置的时间戳进行比较。比如超过5分钟,那么再去查数据库,这样可以保证Redis里面一直有数据,一般是DB的一种容错方式。让Redis真正的当DB使用就是如图订阅数据库的binlog,通过数据异构系统将数据推送到缓存中,并设置缓存为多级。在应用中可以使用jvm缓存作为一级缓存。一般体积小,访问频率高,比较适合jvm这种缓存方式。一组Redis作为远程二级缓存,最外层的三级Redis作为持久化缓存。缓存。超时重试超时重试机制也是一种容错的方法,凡是发生RPC调用的地方,比如读取Redis、DB、MQ等,因为网络故障或者其依赖的服务故障,如果结果不能长时间返回,会增加线程数,增加CPU负载,甚至导致雪崩。所以必须为每个RPC调用设置一个超时时间。对于RPC调用资源依赖性强的情况,也有重试机制,但重试次数建议1-2次。另外,如果有重试,超时时间也要相应减少。例如,如果有1次重试,则总共会发生2次调用。如果超时配置为2s,那么client要等4s才能返回,所以在retry+timeout的方式中,要减少超时时间。再说说一次PRC通话的时间都花在了哪里。一次正常调用统计的耗时主要包括:①调用端RPC框架执行时间+②网络发送时间+③服务端RPC框架执行时间+④服务端业务代码时间。调用者和服务器都有自己的性能监控。比如调用者的tp99是500ms,服务端的tp99是100ms。找了网络群的同事确认网络没有问题。那么时间都花在了哪里呢?有两个原因:客户端调用者,还有一个原因是网络上发生了TCP重传,所以要注意这两点。熔断器和熔断器技术可以说是一种“智能容错”。当调用达到失败次数时,失败比例会触发熔断器的开启,程序会自动切断当前的RPC调用,防止错误进一步扩大。实现熔断器主要考虑三种模式:闭合、断开和半断开。当然你也可以使用开源方案,比如Hystrix中的breaker。下图是一个熔断器的开启和关闭示意图:这里我们要说一下使用熔断器的注意事项:我们在处理异常的时候,要根据具体的业务情况来决定处理方式。比如我们调用一个产品接口,对方只是暂时降级,所以作为网关调用,必须切换到替代服务执行或获取后台数据,给用户友好提示。还有就是需要区分异常的类型,比如依赖的服务崩溃了,可能需要很长时间才能解决,也可能是服务器暂时负载过高导致超时。作为熔断器,应该能够识别出这种类型的异常,从而根据具体的错误类型调整熔断策略。添加了手动设置。在故障服务恢复时间不确定的情况下,管理员可以手动切换熔断状态。***,熔断器的使用场景是调用可能失败的远程服务程序或共享资源。如果本地私有资源缓存在本地,使用断路器会增加系统的额外开销。另请注意,断路器并非旨在替代应用程序中业务逻辑的异常处理。线程池隔离是处于阻力的环节。Servlet3异步的时候,已经提到了线程隔离。线程隔离的直接优势是防止级联故障,甚至雪崩。当网关调用N个多接口服务时,我们需要对每个接口进行线程隔离,比如我们有订单、商品、用户的调用。那么订单的业务不能影响产品的处理和用户的要求。如果没有线程隔离,当访问订单服务出现网络故障时,就会造成延迟,线程积压最终会导致整个服务的CPU负载满。就是说我们说的服务全部不可用了,这一刻会有多少机器被请求填满。那么有了线程隔离,我们的网关就可以保证局部的问题不会影响全局。降级限流业界已经有成熟的降级限流方法,比如failback机制,令牌桶、漏桶、信号量等限流方式,这里说一下我们的一些经验。降级一般是通过统一配置中心的降级交换机来实现的,所以当同一个提供商的接口很多,提供商的系统或者机器所在的机房网络出现问题的时候,我们就需要一个统一的降级开关。否则就要一个接口一个接口降级,也就是给业务类型来个大刀。还有,记得暴力降级。什么是暴力降级?比如论坛功能下沉,用户显示大白板。我们需要缓存一些数据,也就是有backingdata。限流一般分为分布式限流和单机限流。如果要实现分布式限流,则需要一个公共的后端存储服务,比如Redis。Lua用于读取大型Nginx节点上的Redis配置信息。我们目前的限流是单机限流,没有实现分布式限流。网关监控统计API网关是一个串行调用,每一步出现的异常都必须记录并保存在一个地方,比如Elasticsearch,方便后续分析调用异常。鉴于公司的Docker应用都是统一分配的,分配前Docker上已经有3个Agent,不允许再多。我们自己实现了一个Agent程序,收集服务器上的日志输出,然后发送到Kafka集群,再通过web查询消费到Elasticsearch中。目前的跟踪功能比较简单,需要进一步丰富。总结一下网关的基本功能,包括统一接入、安全防护、协议适配等。在这篇文章中,我们没有讲这些基本功能是如何实现的,因为有很多成熟的方案可以直接使用。比如全家桶中的很多组件如SpringCloud、Mashape的API层Kong等。我们比较关心的是:在实现了这些网关的基本功能之后,如何保证一个网关的运行?在访问量大的情况下,如何更好的支持客户来电?如何在突发情况下及时应对这突如其来的异常?如何最大限度地减少错误并防止级联故障?重点介绍网关容错的经验和实践。王新东,京东高级架构师,从事京麦平台的架构设计和开发工作。熟悉各种开源软件架构,在Web开发和架构优化方面有丰富的实践经验。在NIO领域有多年的设计和开发经验,对HTTP和TCP长连接技术有深入的研究和理解。目前主要致力于移动和PC平台网关技术的优化和实施。个人公众号:程序框架(xindongbook17)。
