项目性能优化

一、梯度压测

(一)压测配置

  梯度压测主要是用来分析接口性能瓶颈,主要的四种性能指标:响应时间、并发用户数、吞吐量、资源使用率。它们之间存在一定的相关性,共同反映出性能的不同方面。

         

  压测接口(http://IP:9001/spu/goods/10000005620800):响应时间20ms,响应数据包3.8kb,请求数据包0.421kb

  1、压测配置

    情况01-模拟低延时场景,用户访问接口并发逐渐增加的过程。

    预计接口的响应时间为20ms

      线程梯度:5、10、15、20、25、30、35、40个线程。为什么是这么小的数值,而不是50、100、150、200,主要和接口的响应时间有关,如果是低延迟的接口,用少量的线程就可以模拟高TPS,如果接口响应时间较长,就需要模拟较多的线程才能达到一个很高的TPS

      循环请求次数:5000次,为什么是5000,这个主要是想让测试不要太短,也不要太久,以5000为例,一个线程测试时长就约等于20ms x 5000次 = 100s = 1.6分钟,那么8个线程的测试总时长就是12分钟,如果设置的太大,测试时间太久,设置的太小,测试时间太短,这个综合考虑即可。

      时间设置:Ramp-up period(inseconds)的值设为对应线程数,因为在实际的场景中,也不会是一下创建完成所有线程。

      配置断言:超过3s,响应状态码不为20000,则为无效请求

  2、机器配置

    应用服务器配置:4C8G;外网-网络带宽25Mbps (峰值);内网-网络带宽基础1.5/最高10Gbit/s

    集群规模:单节点

    服务版本:v1.0

    数据库服务器配置:4C8G 

    单位换算如下:

Mbps : Megabit per second (Mbit/s or Mb/s) 
MB/s : Megabyte per second 
1 byte = 8 bits 
1 bit = (1/8) bytes 
1 bit = 0.125 bytes 
1 megabyte = 10002 bytes 
1 megabit = 10002 bits 
1 megabit = 0.125 megabytes 
1 megabit/second = 0.125 megabytes/second 
1 Mbps = 0.125 MB/s

  结论:

1 Gbit/s = 1 Gbps = 125 MB/s
1 Mbps = 0.125 MB/s

  3、配置监听器

    聚合报告:添加聚合报告

    查看结果树:添加查看结果树

    活动线程数:压力机中活动的线程数

    TPS统计分析:每秒事务树

    RT统计分析:响应时间

    后置监听器,将压测信息汇总到InfluxDB,在Grafana中呈现

    压测监控平台:

      JMeter DashBoard

      应用服务器:内存、网络、磁盘、系统负载情况

      MySQL服务器:内存、网络、磁盘、系统负载情况 

(二)Jmeter配置

  由于需要梯度测试,因此需要在Jmeter中配置多个线程组。

  1、全局变量

    在多个线程组中很多内容都是一样的,因此首先配置全局变量,在测试用例上配置ip、端口、路径、循环次数

        

   2、配置线程组(梯度压测)

    添加线程组,设置线程组的线程数、和线程启动时间,同时使用变量配置循环次数

        

     3、配置线程组的http请求

      使用全局变量配置请求的ip、端口、路径

        

     4、配置线程组断言持续时间和响应断言

      配置可见 https://www.cnblogs.com/liconglong/p/16655816.html

    5、添加公共压测结果

      添加聚合报告、查看结果树、后端监听器、活跃线程数、响应时间、TPS,由于是多线程组梯度压测,因此需要创建公共的指标收集,不要创建到某个线程组下,具体创建方式可见:https://www.cnblogs.com/liconglong/p/16655816.html

         

(三)性能瓶颈剖析

  1、梯度压测,测出瓶颈

     进一步提升压力,发现性能瓶颈

      使用线程:5,然后循环5000次,共2.5万个样本

      使用线程:10,然后循环5000次,共5万个样本

      使用线程:15,然后循环5000次,共7.5万个样本

      使用线程:20,然后循环5000次,共10万个样本

      使用线程:25,然后循环5000次,共12.5万个样本

      使用线程:30,然后循环5000次,共15万个样本

      使用线程:35,然后循环5000次,共17.5万个样本

      使用线程:40,然后循环5000次,共20万个样本

    聚合报告 

         

      从聚合报告可以看出一下几个问题或结论:

        (1)吞吐量从281增长到713

        (2)响应时间中位数是19ms,P99为225ms,但是最大值为21s

        (3)异常比例为0.06%,主要是超过了规定的响应时间

     活跃线程数、响应时间、吞吐量(TPS)

            

    压测监控平台与JMeter压测结果一致

        

     压测中服务器指标

        

  2、问题1:网络到达瓶颈(注意:系统网络带宽为25Mbps)

             

    分析:

      随着压力的上升,TPS不再增加,接口响应时间逐渐在增加,偶尔出现异常,瓶颈凸显。

      但是系统的负载不高,CPU、内存正常,说明系统这部分资源利用率不高。带宽带宽显然已经触顶了。

      从监控平台上看,也能看到网络带宽固定在25Mbps左右

    优化方案:

      方案01-降低接口响应数据包大小。(上面测试的接口响应为3.8K,可以对响应结果进行压缩,或者不需要的字段不返回,以减小返回的数据包大小;返回数据包大小可以通过浏览器F12查看)

      方案02-提升带宽【或者在内网压测】(25Mbps --> 100Mbps;云服务器内网:这里在Linux中执行JMeter压测脚本:jmeter -n -t /opt/03-jmeter-example-低延迟20ms-响应3.8k.jmx -l /opt/03-jmeter-example-低延迟20ms-响应3.8k.jtl )

    优化之后:

      方案01-降低接口响应数据包大小,压测结果

        可以看到吞吐量又增加了两百,但是最大响应时间和异常率还是有问题。

        

          

       方案02-提升带宽【或者在内网压测】,压测结果

        聚合报告:查看内网压测的 jtl 文件,可以在查看项(如聚合报告、查看结果树等)中选择 jtl 文件。

        

         服务器监控

                

        问题:可不可以基于RT与TPS算出服务端并发线程数?服务端线程数计算公式:TPS/ (1000ms/ RT均值)        

RT=21ms,TPS=800,服务端线程数:= 800/ (1000ms/ RT均值) = 17
RT=500ms,TPS=800,服务端线程数:= 800/ (1000ms/ RT均值) = 400
RT=1000ms,TPS=800,服务端线程数:= 800/ (1000ms/ RT均值) = 800 

      结论:

        在低延时场景下,服务瓶颈主要在服务器带宽。

        TPS数量等于服务端线程数 乘以 (1000ms/ RT均值)

        RT=21ms,TPS=800,服务端线程数:= 800/ (1000ms/ RT均值) = 17

  2、问题2:接口的响应时间是否正常?是不是所有的接口响应都可以这么快?

    在实际项目中,有很多接口本省响应时间就较长,下面就模拟一个对响应时间为500ms的接口压测,提高TPS

      线程梯度:100、200、300、400、500、600、700、800个线程,这样设置是因为响应时间长,为了提高TPS,就只能提高线程数

      循环请求次数:200次,同样是为了测试时间,一个请求就是500ms,如果循环次数太多,会造成压测试件太长

      时间设置:Ramp-up period(inseconds)的值设为对应线程数的1/10;

      测试总时长:约等于500ms x 200次 x 8 = 800s = 13分

      配置断言:超过3s,响应状态码不为20000,则为无效请求

//慢接口 
@GetMapping("/goods/slow/{spuId}") 
public Result findGoodsBySpuIdTwo(@PathVariable String spuId){ 
    Goods goods = spuService.findBySpuId(spuId); 
    //模拟慢接口 
    try {
        //休眠500ms 
        TimeUnit.MILLISECONDS.sleep(500); 
    } catch (InterruptedException e) {
        e.printStackTrace(); 
    }
    return new Result(true, StatusCode.OK, "查询成功", goods); 
}        

    响应慢接口:500ms+,响应数据包3.8kb,请求数据包0.421kb(http://123.56.249.139:9001/spu/goods/slow/10000005620800)

    测试结果:RT、TPS、网络IO、CPU、内存、磁盘IO

    可以看到,吞吐量降为339,且最大响应时间、P99、P95、异常率明显升高。

        

           

    结论:

      在高延时场景下,服务瓶颈主要在容器最大并发线程数。

      RT=500ms,TPS=800,服务端线程数:= 800/ (1000ms/ RT) = 400;Tomcat默认的最大的线程数?200

      观察服务容器最大线程数,发现处理能力瓶颈卡在容器端,这个就需要优化Tomcat,下面会进行介绍。

  3、问题3:TPS在上升到一定的值之后,异常率较高

    可以理解为与IO模型有关系,因为当前使用的是阻塞式IO模型。这个问题我们在服务容器优化部分解决。

(三)分布式压测

  使用JMeter做大并发压力测试的场景下,单机受限与内存、CPU、网络IO,会出现服务器压力还没有上去,但是压测机压力太大已经死机!为了让JMeter拥有更强大的负载能力,JMeter提供分布式压测能力。单机网络带宽有限高延时场景下,单机可模拟最大线程数有限。

  如下是分布式压测架构:

           

  注意:在JMeter Master节点配置线程数10,循环100次【共1000次请求样本】。如果有3台Salve 节点。那么Master启动压测后,每台Salve都会对被测服务发起10x100次请求。因此,压测产生的总样本数量是:10 x 100 x 3 = 3000次。

  搭建JMeter Master控制机和JMeter Salve施压机

    第一步:三台JMeter Salve搭建在Linux【Centos7】环境下

    第二步:JMeter Master搭建在Windows Server环境下【当然也可以搭建在Linux里面,这里用win是为了方便观看】

  搭建注意事项:

    需保证Salve和Server都在一个网络中。如果在多网卡环境内,则需要保证启动的网卡都在一个网段。

    需保证Server和Salve之间的时间是同步的。

    需在内网配置JMeter主从通信端口【1个固定,1个随机】,简单的配置方式就是关闭防火墙,但存在安全隐患。

   1、Windows Server部署JMeter Master

    与Window中安装JMeter一样简单,略

  2、Linux部署JMeter Salve 

    下载

wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.4.1.tgz
tar -zxvf apache-jmeter-5.4.1.tgz
mv apache-jmeter-5.4.1 ./apache-jmeter-5.4.1-salve

    配置修改rmi主机hostname  

# 1.改ip 
vim jmeter-server
# RMI_HOST_DEF=-Djava.rmi.server.hostname=本机ip(建议使用内网ip,因为通信会有一个随机ip,如果使用外网ip,需要打开所有端口)
# 2.改端口
vim jmeter.properties
# RMI port to be used by the server (must start rmiregistry with same port) server_port=1099 
# To change the default port (1099) used to access the server: server.rmi.port=1098

    配置关闭server.rmi.ssl

# Set this if you don't want to use SSL for RMI 
server.rmi.ssl.disable=true

    启动jmeter-server服务

nohup ./jmeter-server > ./jmeter.out 2>&1 &

  3、分布式环境配置

    确保JMeter Master和Salve安装正确。

    Salve启动,并监听1099端口。

    在JMeter Master机器安装目录bin下,找到jmeter.properties文件,修改远程主机选项,添加3个Salve服务器的地址。 

remote_hosts=172.17.187.40:1099,172.17.187.41:1099,172.17.187.42:1099

    启动jmeter,如果是多网卡模式需要指定IP地址启动

jmeter -Djava.rmi.server.hostname=172.17.187.45

     验证分布式环境是否搭建成功
        

二、服务容器优化

(一)Tomcat调优

  为什么对SpringBoot嵌入式的Tomcat进行调优?

    基于应用性能瓶颈分析得出结论,当响应时间比较长时,系统瓶颈主要存在与Tomcat中!

    系统异常率较高,与Tomcat内部IO模型有关系【多线程&网络编程】

  问题:可不可以基于RT与TPS算出服务端并发线程数?

    服务端线程数计算公式:TPS/ (1000ms/ RT均值)

  1、调优:嵌入式Tomcat配置

    Springboot开发的服务使用嵌入式的Tomcat服务器,那么Tomcat配置使用的是默认配置,我们需要对Tomcat配置进行适当的优化,让Tomcat性能大幅提升。 

        

    修改配置如下所示:可以使用外挂配置,也可以修改配置文件application.yml。

    注意,做了任何修改一定要确认配置生效,否则干的再久也是白搭!

# Tomcat的maxConnections、maxThreads、acceptCount三大配置,分别表示最大连接数,最大线程数、最大的等待数,可以通过application.yml配置文件来改变这个三个值,一个标准的示例如下:
server.tomcat.uri-encoding: UTF-8 
# 思考问题:一台服务器配置多少线程合适? 
server.tomcat.accept-count: 1000 # 等待队列最多允许1000个请求在队列中等待
server.tomcat.max-connections: 20000 # 最大允许20000个链接被建立 
## 最大工作线程数,默认200, 4核8g内存,线程数经验值800 
server.tomcat.threads.max: 800 # 并发处理创建的最大的线程数量 
server.tomcat.threads.min-spare: 100 # 最大空闲连接数,防止突发流量 
# 暴露所有的监控点 
management.endpoints.web.exposure.include: '*' 
# 定义Actuator访问路径 
management.endpoints.web.base-path: /actuator
# 开启endpoint 关闭服务功能 
management.endpoint.shutdown.enabled: true

   (1)maxThreads:最大线程数

    每一次HTTP请求到达Web服务,Tomcat都会创建一个线程来处理该请求。

    最大线程数决定了Web服务容器可以同时处理多少个请求,默认值是200。最大线程数并不是越大越好!最大线程数只是TPS的影响因素之一,还不是最关键的哪个。增加线程是有成本的,不能无限制增大。线程过多不仅仅会带来线程上下文切换的成本,而且线程也需要消耗内存资源。JVM默认Xss堆栈大小1M。

    那么最大线程数的值应该设置多少合适呢?这个需要基于业务系统的监控结果来定。RT均值很低,可以不用设置,RT均值很高,可以考虑加线程数。当然,如果接口响应时间低于100毫秒,足以产生足够的TPS。系统瓶颈不在于此,则不建议设置最大线程数。我个人的最大线程数设置的经验值为:1C2G,线程数200;4C8G,线程数800

  (2)accept-count:最大等待连接数

    当调用HTTP请求数达到Tomcat的最大线程数时,还有新的请求进来,这时Tomcat会将该剩余请求放到等待队列中

    acceptCount就是指队列能够接受的最大的等待连接数,默认值是100,如果等待队列超了,新的请求会被拒绝(connection refused)

  (3)Max Connections:最大连接数

    最大连接数是指在同一时间内,Tomcat能够接受的最大连接数。如果设置为-1,则表示不限制默认值:对BIO模式,默认值是Max Threads;如果使用定制的Executor执行器,默认值将是执行器中Max Threads的值。对NIO模式,Max Connections 默认值是10000。

    Max Connections和accept-count关系:当连接数达到最大值Max Connections后系统会继续接收连接,但不会超过acceptCount限制。

    注意:在做优化的时候,一定要确定你的修改是生效,无论是做JVM、Tomcat、OpenResty等等的优化,都需要确保修改生效。

  2、调优配置生效确认

    上面配置了暴露所有的监控点,因此可以通过 http://IP:9001/actuator 查看相关的配置,其中tomcat的配置在 http://IP:9001/actuator/configprops 中,如下图所示

         

  3、调优前后的性能对比

    调优前:压力机Active,RT、TPS、系统进程运行状态【应用活动线程数】

        

        

     调优后:压力机Active,RT、TPS、系统进程运行状态【应用活动线程数】

        

        

     结论:提升Tomcat最大线程数,在高负载场景下,TPS提升了一百多,同时RT大幅降低; 

(二)网络IO模型调优

   Java的NIO是从Java 1.4版本开始引入的一套新的IO API,用于替代标准的Java IO API。众所周知文件读写性能是影响应用程序性能的关键因素之一。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO以更加高效的方式进行文件的读写操作。

  JDK1.7之后,Java对NIO再次进行了极大的改进,增强了对文件处理和文件系统特性的支持。我们称之为AIO,也可以叫NIO2。Tomcat默认使用的是NIO,那么这里可以使用NIO2进行优化

  优化方案:

    使用NIO2的Http协议实现,对请求连接器进行改写。代码如下所示,通过自动配置,创建一个TomcatServletWebServerFactory对象,在初始化时,就会使用初始化该 Bean 对象。

    在初始化 TomcatServletWebServerFactory 对象时,通过 org.apache.coyote.http11.Http11Nio2Protocol 创建一个NIO2的连接,并使用该链接获取 NIO2 的协议 Http11Nio2Protocol,通过协议设置最大线程数、最大连接数、长链接时间、自动断开长连接的请求数、等待队列最多允许的等待线程数、请求方式、端口号等

@Configuration
public class TomcatConfig {
    //自定义SpringBoot嵌入式Tomcat
    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(){};
        tomcat.addAdditionalTomcatConnectors(http11Nio2Connector());
        return tomcat;
    }
    //配置连接器nio2
    public Connector http11Nio2Connector() {
        Connector connector=new Connector("org.apache.coyote.http11.Http11Nio2Protocol");
        Http11Nio2Protocol nio2Protocol = (Http11Nio2Protocol) connector.getProtocolHandler();
        //等待队列最多允许1000个线程在队列中等待
        nio2Protocol.setAcceptCount(1000);
        // 设置最大线程数
        nio2Protocol.setMaxThreads(1000);
        // 设置最大连接数
        nio2Protocol.setMaxConnections(20000);
        //定制化keepalivetimeout,设置30秒内没有请求则服务端自动断开keepalive链接
        nio2Protocol.setKeepAliveTimeout(30000);
        //当客户端发送超过10000个请求则自动断开keepalive链接
        nio2Protocol.setMaxKeepAliveRequests(10000);
        // 请求方式
        connector.setScheme("http");
        connector.setPort(9003); //自定义的端口,与源端口9001 可以共用,知识改了连接器而已
        connector.setRedirectPort(8443); 
        return connector;
    }
}

  改完后重新启动,可以使用自定义的 9003 端口访问,http://IP:9003/spu/goods/10000005620800

  调优前后的性能对比:

    调优前:RT、TPS

        

             

     调优后:RT、TPS

        

                 

   小结:将NIO升级为AIO之后,RT的毛刺大幅降低,异常数(超时3s)几乎为0。稳如老狗~

(三)容器优化Tomcat升级Undertow

  Undertow(https://undertow.io/)是一个用Java编写的灵活的高性能Web服务器,提供基于NIO的阻塞和非阻塞API。支持Http协议、支持Http2协议、支持Web Socket、最高支持到Servlet4.0、支持嵌入式。

  SpringBoot的web环境中默认使用Tomcat作为内置服务器,其实SpringBoot提供了另外2种内置的服务器供我们选择,我们可以很方便的进行切换。

  Undertow红帽公司开发的一款基于 NIO 的高性能 Web 嵌入式服务器 。轻量级Servlet服务器,比Tomcat更轻量级没有可视化操作界面,没有其他的类似jsp的功能,只专注于服务器部署,因此undertow服务器性能略好于Tomcat服务器;

  Jetty开源的Servlet容器,它是Java的web容器。为JSP和servlet提供运行环境。Jetty也是使用Java语言编写的。

  1、配置操作过程

  (1)在spring-boot-starter-web排除tomcat

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-tomcat</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>

  (2)导入其他容器的starter

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-undertow</artifactId>
            </dependency>

  (3)配置

    这里配置线程数为800是因为这样可以和之前tomcat线程数保持一致,更好的做对比

# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接 
server.undertow.threads.io: 800
# 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
# 默认值是IO线程数*8 
server.undertow.threads.worker: 8000 
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内 存管理 
# 每块buffer的空间大小越小,空间就被利用的越充分,不要设置太大,以免影响其他应用,合适即可 
server.undertow.buffer-size: 1024
# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region 
# 是否分配的直接内存(NIO直接分配的堆外内存) 
server.undertow.direct-buffers: true

  2、调优前后的性能对比

    调优前:RT、TPS【这个是没有进行NIO2优化的Tomcat】 

         

           

     调优后:RT、TPS

        

           

     小结:

      更换了服务容器之后,RT更加平稳,TPS的增长趋势更平稳,异常数(超时3s)几乎为0。

      在低延时情况下,接口通吐量不及Tomcat。

      稳定压倒一切,如果只是写json接口,且对接口响应稳定性要求高,可以选用Undertow

      还有很多可以优化的空间,例如:ARP-IO模型、Jetty 容器、KeepAliveTimeout【默认】、MaxKeepAliveRequests等等

三、数据库调优

  什么影响数据库性能?

    服务器: OS、CPU、Memory、Network

    MySQL:数据库表结构【对性能影响最大】、低下效率的SQL语句、超大的表、大事务、数据库配置、数据库整体架构

  数据库调优到底调什么?

    优化SQL语句:根据需求创建结构良好的SQL语句

    数据库表结构调优:索引,主键,外键,多表关系等等

    MySQL配置调优:最大连接数,连接超时,线程缓存,查询缓存,排序缓存,连接查询缓存等等

    服务器硬件优化:多核CPU、更大内存

  为什么使用索引就能加快查询速度呢?

    索引就是事先排好顺序,从而在查询的时候可以使用二分法等高效率的算法查找

    除了索引查找,就是一般顺序查找,这两者的差异是数量级的差异。

    二分法查找的时间复杂度是O(log n),而一般顺序查找的时间复杂度是O(n)

    索引的数据结构是B+树,是一种比二分法查找时间复杂度更好的数据结构。

四、OpenResty调优

  OpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

  OpenResty的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached以及 Redis 等都进行一致的高性能响应。

  Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好。

  配置域名映射:

    访问主机名映射配置: 

        

    访问地址:http://hero01,这里不要加入.com结尾,未备案的带com后缀域名会被阿里云拦截

  压测配置

        

         

         

    修改openresty的配置,其实就是nginx的配置,添加监听和反向代理。

# 指定你要代理的tomcat服务器列表
upstream hero-server{
        # 内网ip:端口号 
        server 172.17.187.78:9003; 
        server 172.17.187.79:9003;
}
server {
    listen 80; 
    server_name hero01; 
    location / { 
        # 反向代理小英雄服务集群 
        proxy_pass http://hero-server;
    }
}

  测试结果:

    JMeter:聚合报告、TPS、RT

         

           

   Nginx-CPU、内存、网络:资料:压力测试\07-低延时20ms-Nginx

  数据库服务-CPU、内存、网络:资料:压力测试\07-低延时20ms-Nginx

  小英雄服务-CPU、内存、网络:资料:压力测试\07-低延时20ms-Nginx

  小结:使用了Nginx反向代理了服务,TPS会在原有的基础上再翻倍! 

五、缓存调优

  以查询首页的图片为例,我们可以有多种优化方式,最简单的就是是用 java 程序直接访问数据库,但是效率并不高,因此可以是用缓存进行优化,缓存可以分为本地缓存(App缓存和浏览器缓存),CDN缓存、Web容器缓存(Tomcat、Nginx等),应用类缓存(进程内缓存、进程外缓存、SSD缓存等)、分布式缓存。

       

  1、直接查询数据库

    最简单的访问方式就是直接通过 java 访问数据库:http://hero01/ad/ad_read?position=web_index_lb 

  2、分布式缓存

    采用Java代码链接redis的方式,首先访问缓存,如果缓存存在则直接返回,不存在则走数据库。

   (1)缓存预热

    实现思路:定义请求:用于查询数据库中的数据更新到redis中。(连接mysql ,按照广告分类ID读取广告列表,转换为json字符串。连接redis,将广告列表json字符串存入redis 。)

  (2)广告获取

    实现思路:定义请求,用户根据广告分类的ID 获取广告的列表。通过java代码从redis中获取数据即可。

  3、分布式缓存变种(不走java代码,直接走openresty)

    由于如果数据存在redis中,还需要经过一层java代码请求,则会产生多的耗时,那么可以是用openresty直接访问redis,如果redis中有数据,则直接返回。

  (1)缓存预热

    在/root/lua目录下创建ad_load.lua ,实现连接mysql 查询数据 并存储到redis中。

    大概解释一下下面的代码,首先定义了一些变量,然后设置了mysql的链接;通过sql语句查询mysql,查询到结果后,关闭mysql链接;然后再创建redis链接,设置redis的 K-V 对,最后关闭redis链接并返回成功。

ngx.header.content_type="application/json;charset=utf8"
local cjson = require("cjson")
local mysql = require("resty.mysql")
local uri_args = ngx.req.get_uri_args()
local position = uri_args["position"]

local db = mysql:new()
db:set_timeout(1000)
local props = {
    host = "172.17.187.78", 
    port = 3307, 
    database = "hero_all", 
    user = "root", 
    password = "hero@202207"
}

local res = db:connect(props)
local select_sql = "select url,image from tb_ad where status ='1' and position='"..position.."' and start_time<= NOW() AND end_time>= NOW()" 
res = db:query(select_sql)
db:close() 

local redis = require("resty.redis")
local red = redis:new()
red:set_timeout(2000)
local ip
="172.17.187.78" local port = 6379 red:connect(ip,port)

red:set("ad_"..position,cjson.encode(res)) red:close()
ngx.say(
"{flag:true}")

    修改 openresty 中 nginx 的配置文件,加上缓存预热的请求路径,实际访问该路径时,直接加载 ad_load.lua 脚本文件(修改/usr/local/hero/openresty/nginx/conf/nginx.conf文件,代码如下)

#user nobody; 
user root root; 
# 添加 
location /ad_load { 
    content_by_lua_file /root/lua/ad_load.lua; 
}

    重新加载Nginx配置

# 启动nginx 
/usr/local/openresty/nginx/sbin/nginx -c
/usr/local/openresty/nginx/conf/nginx.conf
ps -ef | grep nginx
# 停止nginx 
/usr/local/openresty/nginx/sbin/nginx -s stop
#停止服务 # 重新加载配置,修改配置后执行 
/usr/local/openresty/nginx/sbin/nginx -s reload

    测试:http://hero01/ad_load?position=web_index_lb

  (2)广告获取

  在/root/lua目录下创建ad_read.lua,再lua脚本中直接从 redis 中获取数据。

--设置响应头类型 
ngx.header.content_type="application/json;charset=utf8" 
--获取请求中的参数ID 
local uri_args = ngx.req.get_uri_args();
local position = uri_args["position"];
--引入redis库 
local redis = require("resty.redis"); 
--创建redis对象 
local red = redis:new() 
--设置超时时间 
red:set_timeout(2000) 
--连接 
local ok, err = red:connect("172.17.187.78", 6379) 
--获取key的值 
local rescontent=red:get("ad_"..position)
--输出到返回响应中 
ngx.say(rescontent) 
--关闭连接 
red:close()

 

  添加访问路径,在/usr/local/openresty/nginx/conf/nginx.conf中server下添加配置 

location /ad_read { 
    content_by_lua_file /root/lua/ad_read.lua; 
}

  测试 http://hero01/ad_read?position=web_index_lb 输出 

[{"url":"img\/banner1.jpg","image":"img\/banner1.jpg"}, 
{"url":"img\/banner2.jpg","image":"img\/banner2.jpg"}]

  (3)测试结果

    JMeter:聚合报告、TPS、RT

        

    Nginx-CPU、内存、网络:资料:压力测试\08-Nginx-Redis

    数据库服务-CPU、内存、网络:资料:压力测试\08-Nginx-Redis

    小英雄服务-CPU、内存、网络:资料:压力测试\08-Nginx-Redis

    小结:TPS大幅提升,RT显著降低

  4、进程内缓存

    如上的方式没有问题,但是如果请求都到redis,redis压力也很大,所以我们一般采用多级缓存的方式来减少下游系统的服务压力。

    (1)配置进程内缓存

      先查询openresty本地缓存 如果没有再查询redis中的数据,修改/root/lua目录下ad_read_nginx_cache文件, 内容如下: 

--设置响应头类型
ngx.header.content_type="application/json;charset=utf8"
--获取请求中的参数ID
local uri_args = ngx.req.get_uri_args();
local position = uri_args["position"];
--获取本地缓存
local cache_ngx = ngx.shared.dis_cache;
--根据ID 获取本地缓存数据
local adCache = cache_ngx:get('ad_cache_'..position);
if adCache == "" or adCache == nil then
    --引入redis库 
    local redis = require("resty.redis");
     --创建redis对象 
    local red = redis:new() 
    --设置超时时间 
    red:set_timeout(2000) 
    --连接 
    local ok, err = red:connect("172.17.187.78", 6379) 
    --获取key的值 
    local rescontent=red:get("ad_"..position) 
    --输出到返回响应中 
    ngx.say(rescontent) 
    --关闭连接 
    red:close() 
    --将redis中获取到的数据存入nginx本地缓存 
    cache_ngx:set('ad_cache_'..position, rescontent, 10*60); 
else
    --nginx本地缓存中获取到数据直接输出 
    ngx.say(adCache) 
end

    修改nginx配置文件vi /usr/local/hero/openresty/nginx/conf/nginx.conf ,http节点下添加配置: 

#包含redis初始化模块 
lua_shared_dict dis_cache 5m; 
#共享内存开启 
location /ad_read_nginx_cache { 
    content_by_lua_file /root/lua/ad_read_nginx_cache.lua; 
}

  (2)测试报告

    JMeter:聚合报告、TPS、RT

        

          

    小结:TPS继续提升,RT响应时间触底12-20ms之间;网络带宽占用大幅下降

六、JVM调优

  为什么调优JVM?

    调优的最终目的都是为了应用程序使用最小的硬件消耗来承载更大的吞吐量。

    JVM调优主要是针对垃圾收集器的收集性能进行优化,减少GC的频率和Full GC的次数。从而令运行在虚拟机上的应用,能够使用更少的内存,用更低的延迟,获得更大吞吐量,和减少应用的GC暂停时间!

    GC暂停会在高并发场景下,P99百分位的响应时间会产生影响。

  什么时候JVM调优?遇到以下情况,就需要考虑进行JVM调优:

    1、系统吞吐量与响应性能不高或下降

    2、Heap内存(老年代)持续上涨达到设置的最大内存值

    3、Full GC 次数频繁

    4、GC 停顿时间过长,运行P99百分位响应时间;

    5、应用出现OutOfMemory 等内存异常;

    6、应用中有使用本地缓存,且占用了大量的内存空间;

  调优调什么?JVM调优核心是什么?内存分配 + 垃圾回收!

    1、合理使用堆内存

    2、GC高效回收垃圾对象,释放内存空间

  是否可以把内存空间设置足够大,那么就不需要回收垃圾呢?不可以

    1、不回收垃圾,内存增长巨快,再大的空间都不够用;10w请求,3GB垃圾对象

    2、物理层面:64位操作系统可以支持非常大的内存,但不是无限

      32位操作系统: 2~32 = 4GB

      64位操作系统: 2~64 =16384PB

    3、内存设置既不能太大,也不能太小,需要基于业务场景平衡考量:内存空间设置过大,一旦内存空间触发垃圾回收,就会非常危险,寻找这个垃圾非常耗时,由于内存空间足够大,寻找这个垃圾的时候,极其的消耗时间,因此导致程序停顿;

  调优原则:

    优先原则:优先架构调优和代码调优,JVM优化是不得已的手段大多数的Java应用不需要进行JVM优化

    观测性原则:发现问题解决问题,没有问题不找问题

  调优目标:下面展示了一些JVM调优的量化目标的参考。注意:不同应用的JVM调优量化目标是不一样的。

    堆内存使用率 <= 70%;

    老年代内存使用率<= 70%;

    avg pause <= 1秒;

    Full GC 次数0 或 avg pause interval >= 24小时 ;

    创建更多的线程

 

posted @ 2022-09-17 00:32  李聪龙  阅读(1095)  评论(0编辑  收藏  举报