先说标题中的亿级网关,我的角度时用户量是亿级,访问量按小时算是时百亿级别以上,主要是api网关,也做了老程序php反向代理,区别点为是否存在页面,是否需要通配。
我们定位为:高性能,高可用,可扩展,可管理,可治理,安全的网关产品,作为唯品会所有流量的入口
网关价值:所有中心化控制点都可以统一控制
我们底层技术选型从servlet到akka到netty,servlet就跳过,底层性能模型差太多
线程模型使用netty的boss worker,目前所有业务的处理由于不会阻塞住worker线程,都跑在worker上。
目前性能数据为在12核的机器上,1k payload大小的请求http请求,后端服务为rpc为9万 http为7万多。瓶颈在cpu上
线程模型,我们进一步合并了后端请求调用的woker线程池跟接受请求线程池,考虑连接池可以让一个请求的worker线程处理,跟发送到后端的请求线程处理在一个worker上,这样性能会提高8%-10%,因为少了一次切换。
比如说是http到http, 后端http的话就是一个连接池,拿连接的时候,怎么判断拿连接就是当前线程所在的连接,很多连接就可能分在不同的worker上,需要一个标志位,这个标志位可能可以选择线程ID或者是线程特点的一个东西去做一个标志位,优先拿这个,如果没有我再去拿其他的。也看你的模型,http这种非双工到后端会需要连接数,可以均匀的分在这个24个模块线程上. 如果是rpc,可能一两条连接打天下,就需要尽量不切换或者多创建连接的代价达到不切换。
插件化方式需要不在worker线程上运行,做到隔离问题不影响,包含类的隔离与线程池隔离,目前想采用共享线程池+独立线程池分配模式,先使用共享,没有可用则独立使用一个小的线程池。
netty这块来调优实践
除了worker线程切换的优化,线程安全与共享的方式做了一些事,可以让非安全的代码在线程内共享,类似threadlocal,但我们用的netty的fastthreadlocal,更快更好,底层用的是数组,而且减少了对象的创建,服用了对象对gc也更友好
netty里比如stringbuild等都这么共享使用,我们代码用到也是直接参考netty的用法。
netty有一个平台类,可以直接使用,由于我们是jdk7,用这个netty copy了jdk8采有的一些类,比如hashmap的优化,可以直接用,包括包含mpsc这类
整个网关不会出现任何一处有锁或者有卡的问题,日志也是全异步,如果一定有锁,比如说我涉及到一些多个线程去做共享状态改变,那我们就有准确性的问题,要求必须准确,尽量通cas的方式来搞定,而不是说强行加锁,是不会做一个强行加锁,通常用自旋方式来把这一块解决掉,整个性能是比较好的。
使用netty的native epoll 性能更优,只需要很简单的更改几个类名就可以。
这块性能话我们测下来其实是有一个提升的,就是CPU会稍微低一点点,性能没有太明显,就CPU稍微比平时负载会低一点。
bytebuf的问题。netty在就是官方文档当时有几种对比,但根据你使用场景可以自己跑一下,比如你需要解包并且转换所有包内容,那么堆外不一定有优势。我们是混合场景,选择堆外。
boss线程数根据端口数决定,我们只监听一个,不需要设置cpu的一半,走这个到处都有的最佳实践。当然如果你设置一半,其实也不会用到,这个可以看netty源码逻辑。
就netty我我们目前的话大部分都是说 拿到完整的http做业务处理,但是我们其实有很多可以前置,不需要解析完整的http。比如说,我们的通常是一次请求,或者是我们的一些非法请求,包括限流防刷这种非法的教练,我们其实只需要根据ITP的一个请求函和请求头,我们就可以做一个判断,并不需要把它完整地拆包,我们就可以提前判断。这块呢就需要我们把netty的这块http的解析重写,netty本身对http是一个状态机的方式解析。
就说我们通常在做优化的时候,你到底优化哪个代码,是去一段段扣代码还是怎么弄,我觉得还是不要扣代码,扣代码这个价值点很难找,就是说你可能觉得这块可能是不是怎么样更好,我是利用一个string拼,然后我如果换一个方式是不是性能更好?可能效果并不是那么明显。
还是尽量用工具,用工具从系统底层到整个jvm部分最好是能贯通起来,贯通起来也方便于排查问题。整个如果把这块是从系统底层到jvm层,整个通了之后,你排查问题的速度也比较快,然后定位问题比较快。
比如说火焰图,我们跑其实是会跑到火焰图数据的。我们性能测试是会看一下跑出来的是不是符合我们的预期。第一个就是说系统函数底层的调动是否符合我们的预期,系统上的一个开销,然后到jvm方法栈上的一个消耗,这块比如说比较简单的作用,谷歌的火焰图这个工具,netfilx火焰图的生成,jvm上就得采样,就你可以设置采样频率,,采样下来之后会拿会拿到一个详细的一个图。
就会看到我们整个网关跑出来一个结果,当然这里面又有细节,怎么看火焰图的问题了,快速识别一些性能问题。
下面还有java 自带。最近不是这个jmc比较火嘛,就说这jmc开源之后把团队给裁了。就是jmc这块自带的一个分析工具,他其实也会到一个消耗,是整个的一个方法调用,你会看到哪一块消耗CPU比较多,但JMC的话是觉得没有火焰图这么比较直观点,看你分析哪些问题,各有特点。火焰图可能就更多的在系统函数上,jvm都可以做到就是整个都可以看到整个。
剩下就说我在写代码的时候,比如说一个选择,比如说上周的一个例子,就是我们在做这种base64的一个转码的过程当中,我们可能一些特殊需求需要转,那我现在有这么多工具类,那我到底用哪个工具的性能更好呢?我们通常做法是写一个单元测试来跑一下、测一下,但是这种单元测试很不可靠,因为这种单元测试跟最终你去jvm运行跑代码其实区别还是很大,首先他会没有去做编译优化,有很多问题。
java官方也是推荐直接基准测试,用基准测试来测它到底性能怎么样,但是这种基准测试写的话,最好去把官方的demo读一遍,它里面其实有很多坑,比如说你很容易写一个for循环在里面,for循环还很容易被优化掉。 还有就是你解决测试的时候,其实最后值并不返回,他去跑的时候就会把优化掉,
系统就会去跑一块代码,就认为最后这个值根本没有用到,没用到优化就全部盖掉,全部盖掉之后,跑出来你会发现系统数据很好,但是实际并不是这个样子。所以的话这块有很多坑,写他之前最好去把官方的demo通读一遍,会避免很多东西,他的demo也比较详尽,避免很多错误的一个写法。我后面附了一个日常的demo吧,这可能就是上周我们跑64的一个demo写的。比如刚才的坑,就说明这样去运行的时候,其实并没有返回就不会处理,它其实提供一种方式上处理,就是你把它消费掉,这块的话会跑一个性能数据出来,最终结果会是这样的一个情况。 这样话就会看到那到底选择哪一个比较好,比如说我们每秒的调动次数,那可能下来发现确实JDK8自带的这个性能比较好一点。
下面聊到这个GC优化与排查。Gc优化,其实对网关来说很难容忍这个gc问题,因为特别是高并发的情况下,比如说在上做促销的时候,流量很大的情况下,由于网关发生gc导致整个超时了是很大一个问题,所以我们在gc上尽量控制频率,我们大概现在的情况下是我们能控制到是说一个月一次cms gc,但是要想网关其实流量很大,每天都有流量过来,这种大促、更别说这种抢购之类导致的一些流量。所以我们在这块做了很多一些优化,就是整个控制它的一个频率,后面会讲到。
然后的话是我们一些常见的踩坑的问题,比如说我们会遇到这种熔断、指标统计上的一个踩坑。这可能这一块的话,比如说熔断分方法的熔断和服务器的。大家都知道垄断会要写这个计数器,计数器我们可能要统计容纳多少秒内失败次数等于多少的话,比如说我定义是十秒内失败次数——请求数(就有几个维度)请求十次,十秒内连续失败,或者按几个维度来吧,失败率是60%,我就认为这个服务不好,应该垄断。
这样的话就会导致我们其实会去统计很多这种指标,这种指标就会带来一个问题,就是说我们的统计量太大。比如说我们当时是API网关其实就是一个一个API没有什么问题,但我们现在反向代理就会带来这个问题,因为反向代理涉及一个通配,后面的这个方法很多,所以这个带来问题就是熔断上统计会导致我们内存的一个爆掉。指标统计这个容量上,也是反向代理的时候也会遇到一个问题,因为我们其实请求的url是很多的,无数的url过来,我们去统计的话,最后遇到这个问题,我们通常会统计1秒钟、10秒钟这种各种的一个指标,或者说1分钟5分钟,这种都会带来一个问题。
包括这块顺带再说一下kafka,包括我们用到kafka,这个坑是跳不过去的,原生代码里面自带的这个采样,采样的话他会统计1分钟5分钟15分钟,但是我们网关流量很大的情况下,这个根据jvm的一个原理,就是说我肯定首先对象进入这个年轻带,他年轻带里面就是倒腾几次,就老年代。流量很大,我们这块这ygc就会特别频繁,他就很快进入老年代,但很快进入老年代,就会带来这个问题。
那就就相当于它的统计指标全部在老年代堆积。堆积之后,关键是它采样就说,可能我采样指标是1分钟5分钟15分钟,old区直线增长,就会触发我们的cms gc,这块我们最后的一个时间我们就直接把这块代码干掉,因为我们其实没有办法统计资料,不用统计指标,我们不想它这个给我们带来的问题
这块也是old区的一个问题,就是说我们当时之前是15天左右发生一次,上线之后发现每天稳定增长400兆,就去排查这个问题,反查发现kafka的一个问题,反查之后发现里面有数据结构里面有个跳表,我们反查了一下代码发现只有kafka在用,然后这块就跟踪到kafka,因为已经找到这个到底是谁关联它。这个反查到了之后发现是kafka的问题,我们做了一个空实验之后,发现这个其实问题就解决掉,就回到我们原先那个频率上。
说到这个日志,我们刚才说的日志其实是权益部的一个输出日志,这块我们是按log4j2的一个方式处理,就是二点几的版本,但我们在用的时候2.6还没出来,但是后面2.6出来是很推荐大家试一下,因为这块还做什么gc优化,它号称是减少了很多gc问题,我们测下来也是基本上gc耗时也会有明显的一个降低。
这一块的话是说我们当时上线的时候,我们其实没有设cms gc到底什么时候触发的,比如说都是区,可能通常大家会设置到说75%就触发了,然后不设置,我们觉得这块让jvm去做可能更好一点,就说我们不知道什么时候触发是合理值,既然我们不知道就让jvm来自己来做自己来做动态调整。但发现一个问题,就是现网上的资料都说,包括我们去读这个几本jvm的书,他们也都会说,这个JDK你不设的话,它默认是68%处发这个cms到我线上上线之后发现并不是什么,我们92%才触发,我们就看一下我们jdk的一个版本,我们版本是7,我们就拿了一个open jdk的原代码,就算了一下,算出来,确实是92%出发。
这块我举这个例子意思说,看到这种问题,其实通过这个代码是很容易找的,我们直接可以对你的jdk版本拉下来之后去找,并不需要去相信说我去查各种资料,发现没有,到底是多少触发,这块直接代码里面很明显,其实也不复杂,就是虽然说C++这种代码,那我们其实只是从去找一些这种配置的话,就很很容易快速识别的。代码里面是没有秘密的。
还有一个问题就是说高io导致jvm停顿的问题,这种测试会发现一个问题,说我们在连续压测大概四五个小时,就会发现我们的有些四个9的数据,可能不上网看,四个9的数据,可能会出现有一秒增加,其实我们大部分时间都是在一毫秒的一个范围,但是发现有四个9会有一种一秒的特别长的一个时间,觉得这可能是一个问题,我们线上会这种其实不太能接受的,我们线上基本上时间都是经过网络的一个调动后端返回这个时间,请求发起进来出去的时间大概都在一两毫秒。所以觉得这块我们是不能接受这一点,然后就去排查问题,就发现这样一个问题说高io导致jvm的停顿
一个是把gc日志并不直接放磁盘上,放在这里面放内存文件系统上,然后必须加上这个参数,因为jvm在运行,它会输出、关联操作好几个文件,它有一个文件是你用一些jvm的命令做一些分析用的
后面我们就举例子来讲就做网关的时候,因为做这种对质量要求比较高一点。因为我们这个承载的是一线流量,其实影响是非常大的,对性能也是要求很高,而且又涉及到我们这个改动很频繁。各种业务方可能有些需求点在我们这我们要去为他们做一些定制化的一些开发。当然可以用插件化来做,但是这块其实有很多改动,所以就举一些场景,到底应该怎么做?
比如说让你来写网关的代码你应该考虑哪些点?比如说我们通常做一些抽象,我们会用到一些通用技计数器,我们以这个场景来讲,我们要去做一些考虑。这也是拿我们平时遇到一个点来发散的讲,就说网关到底哪些问题?
我们通常会用一个通用计数器这种东西,因为我们会做很多计数,比如说我们限流,后端的服务:我们要保护后端服务我们来做限流,再比如说我们要做防刷,我们可能要根据ip或者根据session各个维度来做防刷,做成保护安全上的一个东西,或者有一些技术上的一些需求,比如说是熔断,它其实也是一个技术上的需求,这种技术啊我怎么来做的?
可能想比较简单,它就Java自带的这个原子类的这种cs,我就直接上面加就可以。其实也会有问题,原子类它的维度比较单一。来看这样一个实现,其实它这里面就有一个是计数的一个概念,还有一个时间的概念,这两个概念我们就需要用时轮的方式来实现。那时间轮的方式就会带来一个问题,用时间轮的话我就要有一个清晰的概念在里面,包括时间轮怎么快速去查找,比如说你看时间轮列表的话,性能上一个问题,包括考虑我用时间轮怎么去避免产生GC或者一系列的问题。
我们这块要实现一个滑动窗口的一个时间轮。那就首先第一个就是选择,到底是提前清理的话,就使用时清理,提前清理怎么去清理这样一个问题。我们的最后选择是说我们使用时清理。
这样一个统计的话,那么实现上首先用数组去实现,做对应上的一个映射,数组里面的对象,但是这就涉及到我们要怎么去清理它?我们并不我们如果直接加一把锁去清理,比如说我们每次去锁住,它就是每个线程都会遇到这个锁,这个性能会降很低。所以我们是用一个自旋锁的方式,因为我们其实是不停的请求进来,我们去自旋就可以了,自旋一下就可以,这块时间的话性能是比较好的。
然后这块还有一个问题,就是说那我们那JDK8在后面供应对象是明显的比前面这个cas一个原生对象性能高很多了,但为什么不用后面这个,这就是一个内存上的问题。比如说用前面说用度量工具测,测完之后发现前面完全满足我们的需求,因为后面的话确实性能会好,但是后面的内存又会高很多。大概在60万的对象,后面是45兆,前面是一点几兆,就这种内存上的一个对比,我们可能内存上接受不了这个。
包括这种对象,我用完之后我要清理掉,对象清理掉之后,它就会带来gc问题,我这个对象已经在新生代里面倒腾了几次,因为流量很大,容量很大的时候,我就不停的ygc。ygc几次我就进老年带,那去的时候又会带来老年代这个对象增加,清理掉,解除这个关联关系之后,老一代又会直线增加,我们想控制1月一次的cms这个目标给它很难达成了。所以这块就是我们的作用这样一个需求就有很多点需要考虑。
有相关的一些点考虑到之后,我们还要把它做成一个抽象工具,就是说要用的话,我们直接调就可以做,这些考虑点全部在里面,而并不是说我们每个人去实现一套,
再说一个非常有意思的一个GC场景,其实我们有些技术场景,我们会用到这种lru的方式我们去统计,因为不够用。比如说我们要统计一些东西,我们只能用最近最常使用的一个算法去做,但是这就会带来几个问题。我们如果把它设置很大,准确性倒是挺高的,准确性提高了之后,GC问题就来了,如果我设小了,但是又准确性不够,那我多小合适了,这个需要根据流量来。
这个问题呢可以进一步抽象,其实会带来很多问题,就是说那这个问题进一步抽象到说所有能熬过ygc到了老年带,并且把它反复创建问题解除,其实都会有这个老年带增长的一个问题。
比如说我们可能像广泛的一些日常这种使用,如你在开发日常代码的话,肯定会涉及到连接池,这是一个比较常见的例子。就说你的连接池会设有一个共享连接,共享连接的话,几次ygc后也会进入老年带。进入老年带之后,会有配置一些参数去把这些共享连接给关掉动态变化数量,检查闲置连接啥的,会清理掉,这个对象就在老年带里待着了。但是在老年带里的连接数发现不够又会去创建它,创建之后就会,于是这个随着流量不停的在老年带里面创建对象,就会带来这个问题。
最后一点呢就是说我们写了这么多代码之后,发现一些规律性的东西,尽可能复用对象,尽快释放对象。
如我们在创建一个对象的话,能复用尽量复用,比如说我们用了一些计数器,我们移除的时候,其实放在一个队列上,我们要创建什么首先从队列上去取。队列没有,我们再用一个就是减少这种垃圾对象地方创建。
netty而且还是很有参考价值,如果你把源码过一遍的话,可能很有很多想象不到的点,会对你有些价值。
尽量状态无锁,如果一定状态,先走共享。先走线程内共享,就是模块内的一个共享,模块的共享之后,如果说发现还是解决不了问题,那你没办法,你只能cas,那cas,我尽量选择小的一把锁,怎么小怎么来,而并不是我加一把大的读写锁或者怎么样,我可能做一个循环做一个自选锁来做。
最后一点,这里比较重要,就是说在写作代码,因为网关的代码这块,入口流量,如果有问题会影响很严重。当然我们通过一些发布的方式可以避免,但这块的话就是说你在写的话,尽量了解你代码你写的每一行代码背后的逻辑。里面内部到底是怎么运行,再用这种基准测试去度量,度量你的代码到底有多少的性能损耗,到底是怎样的
我的这块分享就完了!
网友评论