基于 Spring Boot、Spring Cloud、Nacos 的微服务架构,在性能测试下的层层优化

翻录整理自己的技术笔记,分享下以前在gitchat上发布的文章

1.概述

1.1问题概述

基于 Spring Boot、Spring Cloud、Nacos 的标准微服务架构,在性能测试下的层层优化;并且整体架构使用到了 Nginx、Redis、RocketMQ 等中间件,针对这些中间件在集群高可用测试场景下的层层修复,达到真正的高可用。

以上内容是最近在一个大型银行项目的心得总结,会以该实际项目中遇到的问题和解决过程来展开。

在本场 Chat 中,会讲到如下内容:

  • 在性能测试下,如何发现问题,一步步优化 CPU,内存和性能,找出应用的性能瓶颈。
  • 在高可用测试下,如何修复 Nginx、Redis、RocketMQ 的集群高可用问题。

适合人群: 对性能优化、高可用感兴趣的技术人员

1.2 名词解析

名词 解释 备注
nginx 高性能web容器,本项目作为接入服务器。
nacos 微服务的配置中心和注册中心
redis 高性能缓存服务器,本项目token和大量信息会缓冲在redis中,提供项目性能
rocketmq 消息中间件,本性能只要用于异步处理
QPS 每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准
TPS Transactions Per Second(每秒传输的事物处理个数),即服务器每秒处理的事务数。

2. 介绍

本文涉及的内容主要依托于某大型银行的互联网金融手机银行项目。

2.1 项目介绍

互联网金融项目主要分核心系统,业务系统和手机银行。核心系统处理整个项目的基础核心架构和业务,各业务系统包括反欺诈,信贷,理财,支付等系统实现独立的业务模块,手机银行做为项目的互联网入口。

2.2 网络部署介绍

不影响阅读理解的基础下,本项目网络部署简单分为互联网接入区,互联网应用区和核心区。

  • 互联网接入区: 又叫DMZ停火区, 该区是互联网直接接入,出于网络安全考虑,该区会部署apache或者nginx做为接入服务器。该区通常不允许访问数据库和各中间件。
  • 互联网应用区:主要部署手机银行的业务系统,出于网络的安全考虑。
  • 核心区: 核心系统和各业务子系统部署在该区域。

2.3 技术介绍

介绍下项目用到的技术:

  • 基于spring boot+spring cloud的微服务架构
  • 采用的基础软件:
    • nginx 做为接入服务器
    • redis 提供缓存服务
    • rocketmq 消息中间件
    • fastdfs 做为文件存储服务器
    • nacos 做为注册中心和配置中心
  • 采用filebeat采集流水日志

3.优化实现

项目专门做了压力测试和高可用测试,基于以上测试,项目做了很多的优化,一步步给大家介绍下。

3.1 性能优化

3.1.1 系统参数优化

首先是测试的准备,系统参数优化。

  • 单进程最大打开文件数限制
    一般的发行版,限制单进程最大可以打开1024个文件,这是远远不能满足高并发需求的,调整过程如下:
shell 复制代码
# vim /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535
  • tcp参数优化
    该参数优化,行里做了些统一配置,自己未亲自实际调整,这里不做展开介绍。
    但是net.ipv4.tcp_tw_reuse = 1 这个配置需要慎重使用。
    该配置相当于开了端口复用,tcp rfc规定,默认情况下同一个端口两次新建连接的时间应该大于2个msl。如果优化了这些配置,就会在2MSL时间内重用端口。导致中间的4-7层负载设备可能会来不及处理。
    本项目采用的是默认配置net.ipv4.tcp_tw_reuse = 0

3.1.2 jdk参数优化

本项目采用的是jdk8,对jdk做了些参数调整。

  1. MetaspaceSize元空间设置
    由于jdk8开始,没有了永久区的概念,所以在jvm参数配置上不再需要

    -XX:PermSize
    -XX:MaxPermSize

取而代之的是((元空间默认大小和元空间最大大小)

复制代码
 -XX:MetaspaceSize=128m 
 -XX:MaxMetaspaceSize=128m 

JDK 8开始把类的元数据放到本地化的堆内存(native heap)中,这一块区域就叫Metaspace元空间。

  1. 使用G1收集器

    -XX:+UseG1GC
    -XX:G1HeapRegionSize=16m
    -XX:G1ReservePercent=25

  2. 设置堆大小

shell 复制代码
-server -Xms4g -Xmx4g -Xmn2g

可根据实际服务器内存大小进行调整,需要设置最大和最小堆内存一致。

3.1.3 连接池优化

项目测试中前期遇到的一些瓶颈问题主要来自于各个框架的连接池的未设置正确。

3.1.3.1 restTemplate的连接池设置

restTemplate底层采用了httpclient实现,下面是resttemplate使用httpclient配置的关键代码

java 复制代码
 	/**
     * 创建HTTP客户端工厂
     */
    @Bean(name = "clientHttpRequestFactory")
    public ClientHttpRequestFactory clientHttpRequestFactory() {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient());
        // 连接超时
        clientHttpRequestFactory.setConnectTimeout(5000);
        // 数据读取超时时间,即SocketTimeout
        clientHttpRequestFactory.setReadTimeout(30000);
        // 从连接池获取请求连接的超时时间,不宜过长,必须设置,比如连接不够用时,时间过长将是灾难性的
        clientHttpRequestFactory.setConnectionRequestTimeout(5000);
        return clientHttpRequestFactory;
    }
    /**
     * 初始化RestTemplate,并加入spring的Bean工厂,由spring统一管理
     */
    @Bean(name = "myRestTemplate")
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        return createRestTemplate(factory);
    }
    /**
     * 配置httpClient
     * @return
     */
    @Bean(name = "httpClient")
    public HttpClient httpClient() {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        try {
            //设置信任ssl访问
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                    // 注册http和https请求
                    .register("http", PlainConnectionSocketFactory.getSocketFactory()).build();
            //使用Httpclient连接池的方式配置(推荐),同时支持netty,okHttp以及其他http框架
            PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
            // 最大连接数
            poolingHttpClientConnectionManager.setMaxTotal(1000);
            // 同路由并发数
            poolingHttpClientConnectionManager.setDefaultMaxPerRoute(500);
            //配置连接池
            httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
            // 重试次数
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(1, true));
            //设置默认请求头
            List<Header> headers = getDefaultHeaders();
            httpClientBuilder.setDefaultHeaders(headers);
            //设置长连接保持策略
            httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
            return httpClientBuilder.build();
        } catch (Exception e) {
            log.error("初始化HTTP连接池出错", e);
        }
        return null;
    }

实际使用

java 复制代码
  @Qualifier("myRestTemplate")
  @Autowired
  private RestTemplate restTemplate;

里面关键参数:

  • 最大连接数
    poolingHttpClientConnectionManager.setMaxTotal(1000);
  • 同路由并发数
    poolingHttpClientConnectionManager.setDefaultMaxPerRoute(500);
  • 设置长连接保持策略
    httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());

以上仅供参考,具体参数配置可以优化下从nacos中获取,而不是代码写死,这里为了方便。可以自己调整参数大小,通过loadrunner或者jmeter压测看实际情况调整最优化。

3.1.3.2 feign的连接池优化

在压测的时候fegin调用也会产生性能瓶颈,feign支持httpclient和okhttp3,一开始尝试优化feign也使用httpclient连接池,但实际会有问题,最后迫于时间压力下,采用了okhttp3作为feign的底层网络请求框架。

maven的依赖添加

xml 复制代码
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

fegin的配置

复制代码
feign.httpclient.enabled=false
feign.okhttp.enabled=true
java 复制代码
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig { 
  @Bean
  public okhttp3.OkHttpClient okHttpClient(){
    return new okhttp3.OkHttpClient.Builder()
        //设置连接超时
        .connectTimeout(10, TimeUnit.SECONDS)
        //设置读超时
        .readTimeout(10, TimeUnit.SECONDS)
        //设置写超时
        .writeTimeout(10, TimeUnit.SECONDS)
        //是否自动重连
        .retryOnConnectionFailure(true)
        .connectionPool(new ConnectionPool(20, 5L, TimeUnit.MINUTES))
        .build(); 
  } 
}

其中ConnectionPool 默认创建5个线程,保持5分钟长连接,可根据情况进行调整。

3.1.3.3 rocketmq消费者线程数

压力测试中,遇到rocketmq的消费比较慢,压测结束很久,消息还未消费完。

研究发现采用spring cloud stream调用rocketmq需要额外参数设置并发数。

配置如下:

properties 复制代码
spring.cloud.stream.bindings.input.consumer.concurrency=15

这里使用了15个线程并发处理,提高了消费速度。

3.1.3.4 fastdfsclient连接池
shell 复制代码
# 读取超时时间
fdfs.so-timeout=1500
# 连接超时时间
fdfs.connect-timeout=1000
# 读取超时时间
fdfs.so-timeout=1500
# 连接超时时间
fdfs.connect-timeout=10000
# tracker地址列表
fdfs.tracker-list=${FASTDFS_TRACKER_LIST}
fdfs.pool.max-total=1000
fdfs.pool.max-wait-millis=5000
fdfs.pool.jmx-name-prefix=dobank_tbs

采用的fastdfs client框架是

xml 复制代码
<dependency>
	<groupId>com.github.tobato</groupId>
	<artifactId>fastdfs-client</artifactId>
	<version>${fastdfs-version}</version>
</dependency>

由于fastdfs是存储文件服务器,需要和核心区各系统共用,所以fastdfs部署在了核心区,

手机银行是跨区调用,需要通过nginx代理。但是fastdfs 会存在ip的问题,具体解决方案参考我的这篇文章
FastDFS内网/公网ip不一致问题的另一种解决方案

3.1.3.5 redis连接池
shell 复制代码
spring.redis.jedis.pool.max-active=200
spring.redis.jedis.pool.max-idle=5
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-wait=5000
spring.redis.jedis.testOnBorrow=true
spring.redis.jedis.testOnReturn=false
spring.redis.jedis.testWhileIdle=true

设置redis client = jedis的连接池,其它参数会在高可用redis一节会再介绍。

3.1.3.6 数据库连接池
复制代码
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
# 最小空闲连接
spring.datasource.hikari.minimum-idle=10
# 连接池大小
spring.datasource.hikari.maximum-pool-size=30
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
spring.datasource.hikari.auto-commit=true
# 空闲连接存活最大时间,默认300000 
spring.datasource.hikari.idle-timeout=30000
# 连接池名称
spring.datasource.hikari.pool-name=HikariCP
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认900000即15分钟
spring.datasource.hikari.max-lifetime=900000
# 数据库连接超时时间,默认10秒,即10000
spring.datasource.hikari.connection-timeout=10000
# 检测是否有效
spring.datasource.hikari.connection-test-query=SELECT 1

Spring Boot 2.0 默认连接池就是 Hikari 。

3.1.4 cpu高优化

压测中遇到cpu会非常高,按实际解决情况记录下,供大家参考

  • filebeat占用cpu高
    优化了filebeat配置

    max_procs: 1

这里啰嗦下,项目组犯了一个错误,没有多方面验证,只是按filebeat自身的介绍:"是一个轻量级程序,性能高,占用资源少。"

大家就一直都是这么认为了,包括我自己。 最后还是实践出真知,filebeat本身介绍没有错,但是需要反思项目情况是否一样?是否需要额外的配置才行?当日志量剧增的时候,filebeat配置不优化,占用的cpu一点也不低。

  • 写日志占用cpu高

    一开始基于logback的日志优化,包括异步等,cpu还是没法降下去,最后切换到log4j2,有明显的下降。

    最后通过日志输出本身优化删除和log4j2最终让cpu降了下去,log4j2的disruptor高性能队列,还带来了明显的异步日志的性能。

    • 日志改用log4j2
    • 采用异步写,使用高性能日志队列disruptor
    • 无用日志优化删除
  • 最后

    其它的cpu高,一般还是代码写的有问题,简单分享几个,供大家参考:

    • while循环,执行一遍通常没有设置sleep一段时间。
    • while循环,一些条件未触发,导致未跳出,一直循环。
    • hashmap非线程安全,用户多线程访问同一hashmap,进行put/get操作的时候导致hashmap死循环。

3.1.5 内存优化

压测中发现内存占用越来越高。通过dump,用ibm的内存分析工具分析,发现session占用非常高,但是实际项目采用token,数据也都是存储在redis缓存中的。

通过工具发现是大量的session和sessionid累计占用的。

shell 复制代码
server.servlet.session.timeout=PT1M

通过这个设置,内存就下降了,session1分钟超时清空。

其它的内存高或者内存泄露问题,一般还是代码写的有问题,简单分享几个,供大家参考。

  • staitc 集合类变量一直添加数据,没有清空机制,导致内存泄露。
  • 一些api对应的close未调用,导致内存和资源未释放。
  • 使用ThreadLocal不当造成内存泄露。

3.1.6 springboot内嵌tomcat优化

原来war包运行,直接优化tomcat的配置,现在tomcat作为jar内嵌到springboot应用中。

以下代码参考:

java 复制代码
@Slf4j
@Component
public class AppTomcatConnectorCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        ((TomcatServletWebServerFactory) factory).setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
        ((TomcatServletWebServerFactory) factory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                Http11Nio2Protocol protocol = (Http11Nio2Protocol)connector.getProtocolHandler();
                //设置最大连接数
                protocol.setMaxConnections(1000);
                //设置最大线程数
                protocol.setMaxThreads(1000);
                protocol.setMinSpareThreads(200);
                protocol.setConnectionTimeout(30000);
                protocol.setKeepAliveTimeout(30000);
               protocol.setMaxKeepAliveRequests(2000);
               protocol.setAcceptCount(1000);         
            }
        });
    }
}

3.1.7 分布式id优化

最开始采用了基于redis的分布式锁和获取一个批次的流水号使用,

但是批次太小导致性能下降,太大容易服务器重启等因素造成浪费这个批次号,

最终选择了分布式雪花id算法生成ID,无锁机制,优化了性能。

3.1.8 redis缓存优化

redis性能确实很高,但是项目有些数据都是管理台维护的字典数据,

如果频繁读也会对性能有所影响,

引入了j2cache,通过本地二级缓存和缓存变化通知机制,可以大幅度降低redis的读操作。

3.1.9 流水日志性能优化

由于redis和rocketmq多台集群部署和线性水平扩展,最终性能瓶颈卡在数据库的流水记录上面。

由于要求访问流水和交易流水必须入库和记录日志。

统一做了优化:

  1. 日志记录
  2. filebeat采集日志
  3. filebeat推送日志内容到rocketmq
  4. rocketmq消费消息慢慢的入库

以上操作把数据库的瓶颈优化掉了,tps提升明显。

解释下手机银行作为整个项目的互联网入口,没有太多的数据库操作,

业务逻辑的数据库操作都在对应业务系统实现了,手机银行主要就是流水的记录,

查询通过接口调用和redis缓存机制。

3.1.10 挡板性能优化

对一个单系统进行压力测试的时候,会遇到访问别的关联系统怎么办?

这时候就需要自己实现一个挡板系统代替关联系统。

此次压力测试采用了spring-boot-starter-webflux做为底层实现,报文缓存到内存中,性能非常高。

有兴趣的同学可以尝试下webflux。

挡板性能高的好处不会因为挡板性能的原因影响了单系统本身的性能测试。

3.2 高可用测试优化

项目各环节都要无单点,达到高可用。

所以高可用测试主要测试的就是关机或关掉服务,看项目的实际情况是否达到高可用,

由于高可用不是为了测试性能,所以通常都会只是10-20个线程并发请求测试。

3.2.1 nginx的高可用

项目中使用了nginx为微服务网关进行负载分发。

在实际测试场景中会发现,以下配置会有问题:

shell 复制代码
upstream gateway{
        #upstream的负载均衡,weight是权重,可以根据机器配置定义权重。
        #weigth参数表示权值,权值越高被分配到的几率越大。
        server xx.xx.xx.1:9400 weight=1;
        server xx.xx.xx.2:9400 weight=1;
        server xx.xx.xx.3:9400 weight=1;
}

在做高可用测试的时候,关掉任意一台xx.xx.xx.1/2/3会导致QPS直线下降。

经过分析:

  1. nginx upstream配置,默认"连接超时"时间是60s
    其中故意关掉一台nginx服务后,nginx发现连接无效需要60s时间,正常10-20并发的请求小于100ms,所以在loadrunner下观察QPS看起来会直线下降。
  2. nginx 处理节点失效和恢复的触发条件
    nginx可以通过设置max_fails(最大尝试失败次数)和fail_timeout(失效时间,在到达最大尝试失败次数后,在fail_timeout的时间范围内节点被置为失效,除非所有节点都失效,否则该时间内,节点不进行恢复)对节点失败的尝试次数和失效时间进行设置,当超过最大尝试次数或失效时间未超过配置失效时间,则nginx会对节点状会置为失效状态,nginx不对该后端进行连接,直到超过失效时间或者所有节点都失效后,该节点重新置为有效,重新探测。

基于以上分析,nginx高可用最终优化的配置

shell 复制代码
#连接超时时间
proxy_connect_timeout 5s;
upstream gateway{
        #upstream的负载均衡,weight是权重,可以根据机器配置定义权重。
        #weigth参数表示权值,权值越高被分配到的几率越大。
        server xx.xx.xx.1:9400 weight=1  max_fails=3  fail_timeout=30s;
        server xx.xx.xx.2:9400 weight=1  max_fails=3  fail_timeout=30s;
        server xx.xx.xx.3:9400 weight=1  max_fails=3  fail_timeout=30s;
}

3.2.2 redis的高可用

redis集群高可用测试,杀死redis服务或者关闭带有redis的物理机器,QPS下降90%,重新拉起,依然不能恢复,并持续报错。

问题分析和解决:

  1. 首先redis关掉一台后,redis集群的主从切换是正常切换。
  2. 问题最终锁定在redis的连接池上。
  3. 客户端连接redis超时默认是60s,调整到10s,可以加速定位该连接池的这个连接已经失败了。
  4. redis连接池中此时会存在无效的连接,调整配置,每次从连接池获取连接,验证下连接的有效性,无效重新获取。
  5. redis连接池原来设置了最少的空闲连接=5,
    修改设置为0,防止因为最少空闲连接数的限制,导致无效连接无法释放。

修改的配置如下:

复制代码
spring.redis.timeout=10000ms
spring.redis.testonBorrow=true
spring.redis.jedis.pool.min-idel=0

3.3.3 rocketmq高可用

rocketmq高可用测试,刚开始陷入了redis集群的思路里,觉得要主从切换。

最后了解了下rocketmq高可用原理:

rocketmq是通过broker主从机制来实现高可用的。相同broker名称,不同brokerid的机器组成一个broker组,brokerId=0表明这个broker是master,brokerId>0表明这个broker是slave。

  1. 消息生产的高可用:创建topic时,把topic的多个message queue创建在多个broker组上。这样当一个broker组的master不可用后,producer仍然可以给其他组的master发送消息。 rocketmq目前还不支持主从切换,需要手动切换,但是不影响高可用。

  2. 消息消费的高可用:consumer并不能配置从master读还是slave读。当master不可用或者繁忙的时候consumer会被自动切换到从slave读。这样当master出现故障后,consumer仍然可以从slave读,保证了消息消费的高可用

所以rocketmq高可用测试通过。

3.3.4 应用高可用

应用高可用正常,基于nacos的注册中心实现,这里就不展开说了。

4.注意事项

性能测试还有很多别的一些注意点。

4.1 system.out和异常打印

system.out 和java运行程序运行在同一线程,而且还有锁。异常打印也是,非常影响高并发的性能。

该用日志框架打印。

4.2 log日志打印,不要+

java 复制代码
//错误
 log.debug("user:"+user);
//正确
 log.debug("user:{}",user);

因为如果当前打印级别是info,则正确的写法,不会做字符串相加操作。

4.3 close方法确保调用

close方法需要在finally里调用

4.4 性能测试遇到的线程安全问题

一般业务测试没法发现线程问题,压力测试就会报漏出来。

分享本项目遇到的几个:

  1. SimpleDateFormat线程安全问题

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    新手把SimpleDateFormat类new出后,写成了static变量,不断复用,导致了线程安全。

  2. java8的stream api使用不当导致线程安全问题

    java8使用parallelStream遍历导致线程不安全问题

5. 最后

最后分享到这里,一些工具的使用帮忙排查问题下次再分享了。

希望以上文章内容对大家有所帮助。

相关推荐
安卓开发者2 小时前
OkHttp 与 RxJava/RxAndroid 完美结合:构建响应式网络请求架构
okhttp·架构·rxjava
谢平康3 小时前
支持不限制大小,大文件分段批量上传功能(不受nginx /apache 上传大小限制)
java·vue.js·spring boot
weixin_552444204 小时前
【Deepseek】RAG 技术与模型架构的创新变革
架构
架构师汤师爷4 小时前
扣子Coze智能体实战:自动化拆解抖音对标账号,输出完整分析报告(喂饭级教程)
架构
congvee5 小时前
springboot学习第5期 - spring data jpa
spring boot
夜斗小神社5 小时前
【黑马SpringCloud微服务开发与实战】(四)微服务02
spring·spring cloud·微服务
DanB246 小时前
Spring-boot实战demo
spring boot
cherishSpring6 小时前
gradle7.6.1+springboot3.2.4创建微服务工程
微服务·云原生·架构
快乐肚皮6 小时前
ZooKeeper学习专栏(四):单机模式部署与基础操作详解
学习·zookeeper·架构·debian·部署
Tacy02136 小时前
微服务基础环境搭建-centos7
微服务·云原生·架构