提到应用网关系统,我们脑海中或多或少都会闪过一些关键词,比如统一入口、高并发、大流量、限流、防刷、实时监控等等。 Youzan Application Gateway Center(公司内部称之为Carmen系统),它就是目前有赞的应用层网关系统。每天承载着亿级别的请求量,持续的为开放平台和多个有赞App应用提供着稳定的服务。
今天我们来一起剖析下整个有赞的应用层网关,聊聊网关的当前的概况、整个网关系统的构成、遇到的一些问题的思考和解决方案。当然在进入正题之前,我们不妨先对这个网关系统提出几个问题:
- 网关在有赞技术生态系统中的使用场景是什么?
- 一个请求通过网关,它会有多少额外的性能消耗?
- 网关系统能给我们带来哪些显而易见的好处?
- 网关系统流量不断上涨的过程中,有遇到过哪些棘手问题?
- 如果我要设计一个网关系统,有哪些值得推荐的设计思路?
- 在网关这样一个高并发系统中,有没有遇到一些有意思的并发问题?
- 网关系统如何保障服务的可靠性?
以上这些问题在您耐心读完整篇文章后,将会一一得到答案。
首先,网关作为一个系统入口,目前它在有赞技术生态圈中主要面向开放平台和移动app两种应用场景。开放平台属于外部调用,移动app属于内部调用。内部与外部调用使用不同的域名,以便合理分配资源、有效进行权限控制、流量控制等。
一、应用网关系统概况
整个网关系统拆分为3个子系统,都由java实现:
- 控制台,负责API的创建与管理。
- 核心系统,负责接收、分发请求并且返回结果。
- 监控系统,负责呈现API的各项监控指标。
图 1-1 系统整体概况
图1-1展示的整个网关系统需要完成下列目标:
- 提供API管理平台,包括API的配置、沙箱测试、发布、修改、下线。
- 对外统一使用http协议,网关屏蔽了后端协议的差异性,后端可以专注于提供服务,无需考虑协议的转换。
- 保护后端服务,提供完善的流控、服务降级、ACL、鉴权机制。
- 提供API层面的监控与报表,做到对业务自身的掌控,以及对用户行为等进行分析。
- 为开放平台的持续完善提供基础服务。
二、核心系统
1.主要功能模块:
- 请求上下文数据准备模块
- 安全校验模块
- 流控,服务降级模块
- 鉴权模块
- 缓存模块
- 后端调用模块,超时控制
- 结果处理模块
- 监控数据收集模块
2.整体架构
图 2-1 网关总体架构图
3.下面我分别从系统的健壮性、高性能、扩展性、安全性、可靠性五个方面来介绍下为了达到这些目标,有赞网关是采用了一些什么方法。
3.1 健壮性:
压力测试:
为了提供稳定的服务,首先我们需要充分了解自己的程序以及其运行的服务器,所以前期需要充分地压测,通过压测具体数据来找出系统的性能瓶颈以及性能拐点,同时也让自己了解程序与机器配合度,最后根据压测报告来评估系统的容量。 有赞网关在压测过程中,通过模拟用户场景,对比各种机器指标被打满的情况,帮助我们提前暴露了下列瓶颈点:
- 一次请求会频繁访问codis,这不仅是一个性能消耗点,而且让系统在高并发、大流量场景下存在雪崩的风险。 因为一旦codis有问题,流量会打到DB,DB扛不住高并发的狂轰滥炸。
- 同步方式调用后端服务在高并发场景下大大限制了系统的吞吐量,具体多少根据不同项目不同机器等环境不同而不同,我们的场景至少是成倍地被限制。
- 使用logback日志系统,开启不同级别日志对系统的影响差异较大。过多地写日志直接对磁盘和CPU的消耗较大,间接影响了服务质量。 同步异步磁盘IO在访问量很大的时候性能拐点差异不大,当时的压测数据是10%左右的QPS差异。最终为了保证数据的可靠性选择了同步模式。
- 高并发情况下,使用短连接会让time-wait状态的连接迅速堆积,最终耗尽文件句柄,直接造成服务不可用。根据实际场景选择使用长连接。 整个压测过程就是围绕寻找系统性能拐点推进的,此处顺便推荐一款高性能、易上手的web压测工具gatling。
一个有效的压力测试会极大帮助开发者了解自己的系统,对容量规划做到心里有底。
并发问题处理:
并发编程一书的作者曾说过:没有人能保证写出完全正确的并发程序。 编写好并发程序是一项有富有挑战性的工作,下面谈谈有赞应用网关在线上实际遇到的并发场景:
- 第一个是在系统出入口的并发访问。 入口的高并发会带来线程膨胀,资源耗尽,吞吐量下降的问题,还好我们有Nginx,有Jetty的NIO以及Servlet3.0异步特性。 此三者很好的为我们解决了系统入口的并发问题:Nginx做负载均衡、NIO使用IO多路复用解决空闲网络连接过多引发的线程膨胀问题,Servlet 3.0的异步特性释放了入口的吞吐能力。 出口的高并发在同步模式会显著降低吞吐量,使用异步调用模式会很好的缓解这个问题。
- 第二个是代码中并发问题
没有共享,就不会有并发问题,但java中多线程场景无处不在。
代码中临界区就像幽灵一样,让人防不胜防。当并发量小或者无并发的时候,这些临界区能正常work。一旦流量超出阈值,这种并发就会触发异常或者带来逻辑上的错误,甚至让系统奔溃。
- 触发异常这种情况比较好定位,比如典型的ConcurrentModificationException,这种异常会有详细的堆栈,借助就能帮助我们快速找到是是哪个线程不安全的集合类被并发修改了。 并发编程中优先选择并发集合类!
- 最头疼的还是引发业务逻辑混乱,比如缓存不一致,缓存中出现多份相同数据等,排查问题就没那么直观了,这需要根据现象去仔细去推敲代码。
最后对于并发编程,强烈建议找两个并发编程的老司机一起来review下代码,尤其是核心模块!!
内存管理实践:
1) 内存回收算法的选择。
虽然Java会自动进行内存回收,但是网关系统中也必须要重视内存的合理配置与使用,否则OOM,频繁FGC会缠上你的系统。 有赞网关在流量不断攀升的过程中也在不断调整GC相关参数去解决FGC频繁的问题。其中的经验之谈就是使用JVM相关的命令不断观察GC回收的次数、耗时,最终在吞吐量和响应时间之间找到一个系统平衡点。比如有赞网关之前维持的一个平衡点是一次YGC时间约25ms,一次FGC100ms(Young Generation和Old Generation分别给2g, CMS回收器)。
网关系统曾经大半年都是使用CMS回收器,近来已经在尝试使用使用增量回收算法的G1回收器。 下面小结下这两种回收器在我们网关系统上的实际效果(4核8G的VM服务器):
- CMS,堆内存给4g,Young Generation和Old Generation分别给2g,每天进行一次FGC,耗时约100ms,YGC次数较CMS会频繁一些,但是YGC总耗时G1较CMS高一些,这个可参考图 2-2。
- G1,堆内存给官方建议的6g(曾经有段时间给的是4g),YGC次数较CMS少些,通常运行10天之后堆的总占用率会达到90%,一直没有观测到有FGC,由于我们系统监控在堆总占用率超过90%会一直报警,所以这时候得手动重启下应用,没有让堆继续增加。 当前图是切换成G1之后几天的监控数据,左边12天是使用CMS的数据,右边12天是使用G1的数据。红线是young GC的回收次数,蓝线是GC回收时间,G1的YGC时间要比CMS的大一些。 当前图是CMS和G1回收器在堆变化趋势的对比。红线是Young区,蓝线是Old区。 左边CMS能明显看到有FGC,右边G1的Old区大小不是线性增长,实际监控数据显示它没有进行FGC,而且G1的Young区占据空间比CMS的Young区大。
小结:目前的使用经验看来G1回收算法并没有比CMS用得舒服,反而带来了不触发FGC的问题以及增加了OOM后分析这么大的堆文件的时间成本。 当然,在尝试新技术的过程中,我们会不断去探索G1这种增量回收算法的最佳使用姿势。
2) 内存泄漏 内存泄漏跟临界区也类似,让人防不胜防。 集合实现类是内存泄漏的重灾区,比如对于一个线程池配上无限大队列时一定要注意这个无限队列在某个时刻会让系统发生OOM。 我们会仔细检查系统中是否存在线程池配无限大队列的情况,同时在容易造成内存泄漏的集合类指定一个大小,比如本地缓存,我们通常会设置一个上界。 我们网关线上实际碰到过线程池的无界队列引发的OOM,一台服务后端阻塞导致一个线程的队列撑爆了堆内存,一直FGC,CPU暂用率100%,当前服务器无法提供正常服务,系统dump了一个超过6GB大小的堆文件。最后只有把它拷贝到一台大内存服务器用命令行分析后,再把结果文件拷贝回本机使用HeapAnalyzer分析。它能清晰展示对象层次关系,直接定位问题,尤其适合分析较大的dump文件。 遇到OOM并不可怕,使用一款优秀的分析工具很容易定位到具体的问题代码。
网络优化: 网络方面的优化我们主要做了几方面的事:
- 短连接改用长连接。一开始我们使用短连接,流量稍微一上来,大量time-wait状态的链接耗尽文件句柄资源,让系统无法正常提供服务。
- 运维层面在服务器限制time-wait状态的上限,比如10000,这样无论如何time-wait都不会超过这个上限。
- 容器版本的选择,之前在使用Jetty9.3.0时,偶尔会遇到大量time-close的情况,后来同时升级了操作系统内核版本以及Jetty版本之后再没出现这种问题。
服务部署:
- 服务多机集群部署,我们一般按照3~5倍当前流量来评估机器数量,而且运维都会配置上完善的系统监控,一旦某个指标,磁盘、CPU,内存等超标立马报警。
- 应用配置健康检查,当一台机器服务宕机,应该自动被下线。
- 容量不够时,运维会快速扩容,或者应用手动启动服务降级。
- 网关上面一层的Nginx会把请求均匀地分发给后端服务器,使得每台服务器压力保持一致。
服务对稀缺资源都是弱依赖,比如对redis和mysql是弱依赖。这主要得益于本地缓存的使用。
- 对外部的依赖系统设置合理超时时间,比如redis设置1s,db设置2s,这会在一定程度上保护我们的系统。
3.2 高性能:
网关接受到请求到发起后端请求之间的平均时耗约2ms,即网关本身平均每次请求消耗了约2ms。 当今一个主频3GHz的CPU每秒能处理30亿条指令,所以耗在执行程序指令上的时间相比网络IO和磁盘IO可以忽略不计。根据这个思路,我们考虑使用本地缓存来降低网络IO的消耗。
- 充分利用缓存技术:
前期仅使用redis,压测的时候QPS并不是很理想。每次访问一个接口会组装参数,这个过程会多次读redis(当时的压测环境读redis的速度跟我们线上还是有一定差距)。 根据API不怎么修改的特点,再添加一层本地缓存,仅做这个改动之后QPS直接翻了好几倍。 当然本地缓存固然是好,不仅提升了性能,还增加了系统的健壮性,但是同样要增加缓存维护的复杂度。 总的来说,两级缓存的使用是网关性能的保障。
3.3 扩展性:
网关核心逻辑采用类责任链模式(Filter Chain),每个filter处理一件事情,这样无论增加处理逻辑还是增加不同协议的服务,仅需新增一个Filter到调度逻辑;想要禁用某个Filter,也能静态或者动态排除它,即可插拔性。 当前图展示的已经是我们当前版本的逻辑流,第一版是每个pipe顺序执行,当前版本不一样的地方是由一个统一的调度中心来执行的这些已经排好序的pipe。
3.4 安全性:
- 完善的限流防刷机制,既有默认的限流方案,也允许API责任人随时配置定制化限流方案。
- 可以通过ACL模块随时禁用或者启用某个来访者。
- 服务降级,利用限流机制,可以从不同维度对异常的服务或者接口降低访问频次。
- 鉴权,接口的访问通过签名和免签两种模式来鉴别用户身份。免签使用业界通用的OAuth2.0。
3.5 可靠性:
线上系统环境错综复杂,问题在所难免。 所以保障可靠性除了服务自身需要高质量以外,主要还是依赖强大的监控系统来作为我们的千里眼、顺风耳。监控系统将会在第四节介绍。
三、控制台系统
控制台的主要功能点:
- API的创建、功能测试、发布、编辑、自动生成文档
- API流控、ACL的配置
- 本地缓存的管理
- 用户权限管理
- 鉴权相关的配置与管理
在以上常规功能点之上,我只想重点介绍下API的设计思路,这个设计成功解耦了外部调用者和内部开发者之间的耦合,不仅让网关后期的功能扩展工作事半功倍,而且为API开发者在配置的时候提供了灵活性。
- 我们使用API名字和版本号来唯一标识一个API,比如kdt.item.add.1.0.0。
- 外部API名字与内部API名字分离;外部参数与内部参数也完全分离。中间通过一层映射关系联系起来。
- 分离API名字是为了修改内部API名字不影响外部API,而且多个外部API名字可以映射到同一个内部API名字。
- 分离内外参数为了可以在修改内部参数时不会影响外部参数,同时可以在不同参数之间映射 在实际应用中,外部API名字跟内部API名字不一样,外部参数跟内部参数名也时常有不一样,这通过内外分离可以很好的解决这种改变内部不影响外部调用的场景。
四、监控系统
- 网关自身有个针对API层面的监控系统,主要功能如下:
- 统计每个请求经过每个Filter的时间,这可以让我们监控到每个阶段的性能,帮助我们进行优化和排查问题。
- 统计API的调用量、成功次数、异常次数。
- 实时统计调用量、错误量,异常量的top10。
- 系统监控
主要针对内存、磁盘、CPU等指标,它们超出阈值的之后会自动触发告警,这时相关负责人会及时处理异常。
印象中已经有好几次是监控系统帮助我们网关系统及时发现问题,避免了故障的发生。
五、结语:
有赞应用层网关系统的概况以及遇到的一些网络、并发、GC问题的处理思路大致如上所述。重点交流一下思路,不再续说一些模块实现细节,相信对具体的实现方案每个人都有自己的一套解决方案。
随着业务不断地发展,有赞应用层网关系统将会面向更多的应用场景,同时也会面临诸多新的挑战,当然网关的未来也值得期待。
最后谢谢您对有赞的关注,同时如果双方合适,期待在往后的日子里并肩作战。强势插入招人内推邮箱 dingdongsheng@youzan.com