参数调优

2018/10/27 posted in  Spring Boot

参考:https://github.com/gudaoxuri/dew/blob/spring-boot-1.x/docs/src/main/asciidoc/_chapter/best-practices.adoc

谨防被删,以下做了备份

最佳实践

JDBC框架的选择

主流JDBC框架有Hibernate、MyBatis、Spring JDBC Template、Ebean、DBUtils等,Dew基于Spring Boot,所以对于这些框架都提供了很好的兼容。那么如何选择呢?

  • 先说Hibernate,如果你的数据表结构与对象模型基本吻合,那么使用JPA会带来很大的便捷,推荐Hibernate
  • 如果你的数据表结构与对象模型严重不吻合或是你希望对SQL有更多主动权(SQL优化、复杂查询等),那JPA就没什么优势了,这时:
    • 如果你追求极简、不需要ORMPPING,那么DBUtils会是最佳选择
    • 如果你喜欢敏捷开发,推崇充血模型,那么尝试一下Ebean吧,与Play!结合最合适不过
    • 如果你既要有一定的ORMPPING能力,又希望自己写SQL,那么MyBatis会是不错的选择
    • 如果你使用了Spring,希望框架简单些,可以接受自己写ORMPPING,未来无切换关系型数据库的计划,那么Spring JDBC Template将是个很好的选择

服务调用开发期使用

Spring Cloud 体系下,服务调用需要启动 Eureka 服务(对于 Dew 中的 Registry 组件),这对开发阶段并不友好:

  1. 开发期间会不断启停服务,Eureka 保护机制会影响服务注册(当然这是可以关闭的)
  2. 多人协作时可能会出现调用到他人服务的情况(同一服务多个实例)
  3. 需要启动 Eureka 服务,多了一个依赖

为解决上述问题,在使用 Spring CloudRestTemplate 时,增加 Ribbon 的服务配置.

# <client>为service-id
<client>.ribbon.listOfServers: <直接访问的IPs>
# 如
x-service.ribbon.listOfServers: 127.0.0.1:8812

@Validated 注解

  • 在Spring Controller类里,@Validated 注解初使用会比较不易上手,在此做下总结
    1. 对于基本数据类型和String类型,要使校验的注解生效,需在该类上方加 @Validated 注解
    2. 对于抽象数据类型,需在形式参数前加@Validated注解
Tip Spring对抽象数据类型校验抛出异常为MethodArgumentNotValidException,http状态码为400,对基本数据类型校验抛出异常为ConstraintViolationException,http状态码为500,dew对这两种异常做了统一处理,http状态码均返回200,code为400

jackson 对于 Java8 时间转换( SpringMVCjackson 接收 json 数据)

  1. 对于 LocalDateTime 类型,需在参数上加 @JsonFormat 注解,如下:@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
  2. LocalDate,LocalTime,Instant 等,无需配置可自行转换
Tip jackson 对于 LocalDateTime 类型的支持与其他三种类型不具有一致性,这是 jackson 需要优化的一个点

Ribbon 负载均衡

example: service-dew.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
Note service-dew为服务名,配置时自行选取规则,类均在com.netflix.loadbalancer包下

若指定zone,默认会优先调用相同zone的服务,此优先级高于策略配置,配置如下

#指定属于哪个zone
eureka:
  instance:
    metadata-map:
      zone: #zone 名称

#指定region(此处region为项目在不同区域的部署,为项目规范,不同region间能互相调用)
eureka:
  client:
    region: #region名称

Feign 配置特定方法超时时间

Hystrix 超时时间配置

# 配置默认的hystrix超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
# 配置特定方法的超时时间,优于默认配置
hystrix.command.<hystrixcommandkey>.execution.isolation.thread.timeoutInMilliseconds=10000
# <hystrixcommandkey>的format为FeignClassName#methodSignature,下面是示例配置
hystrix.command.PressureService#getBalance(int).execution.isolation.thread.timeoutInMilliseconds=10000

Ribbon 超时时间配置

# 配置默认ribbon超时时间
ribbon.ReadTimeout=60000
# 配置特定服务超时时间,优于默认配置
<client>.ribbon.ReadTimeout=6000
# <client>为实际服务名,下面是示例配置
x-service.ribbon.ReadTimeout=5000

Hystrix 和 Ribbon 的超时时间配置相互独立,以低为准,使用时请根据实际情况进行配置

Tip 如果要针对某个服务做超时设置,建议使用 Ribbon 的配置;在同时使用 RibbonHystrix 时,请特别注意超时时间的配置。

Feign 接口添加 Http 请求头信息

Tip @FeignClient 修饰类中的接口方法里添加新的形参,并加上 @RequestHeader 注解指定key值

示例

@PostMapping(value = "ca/all", consumes = MediaType.APPLICATION_JSON_VALUE)
Resp<CustomerInfoVO> applyCA(@RequestBody CAIdentificationDTO params,
     @RequestHeader Map<String, Object> headers);

Feign 文件上传实践

  • SDK 工程处,添加包依赖

pom

        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form-spring</artifactId>
            <version>3.0.1</version>
        </dependency>
  • SDK 工程处,创建一个 Configuration

MultipartSupportConfig

import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MultipartSupportConfig {

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }

}
  • 修改接口

FeginExample

@FeignClient(name = "demo")
public interface FeginExample {
@PostMapping(value = "images", consumes = MULTIPART_FORM_DATA_VALUE)
 Resp<String> uploadImage(
            @RequestParam MultipartFile image,
            @RequestParam("id") String id);
}

@RequestPart@RequestParam 效果是一样的,大家就不用花时间在这上面了。

  • 修改服务器接口

FeginServiceExample

@RestController
public class FeginServiceExample {
  @PostMapping(value = "images", consumes = MULTIPART_FORM_DATA_VALUE)
    public Resp<String> uploadImage(
            @RequestParam("image") MultipartFile image,
            @RequestParam("id") String id,
            HttpServletRequest request) {
              return Resp.success(null);
            }
}

常见问题:

  • HTTP Status 400 - Required request part 'file' is not present
请求文件参数的名称与实际接口接受名称不一致
  • feign.codec.EncodeException: Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.mock.web.MockMultipartFile] and content type [multipart/form-data]
转换器没有生效,检查一下MultipartSupportConfig

自定义降级方法

Note 构建类继承HystrixCommand抽象类,重写run方法,getFallback方法,getFallback为run的降级,再执行excute方法即可
Tip 每个HystrixCommand的子类的实例只能execute一次。

下面附上代码

public class HelloHystrixCommand extends HystrixCommand<HelloHystrixCommand.Model> {

    public static final Logger logger = LoggerFactory.getLogger(HelloHystrixCommand.class);

    private Model model;

    protected HelloHystrixCommand(HystrixCommandGroupKey group) {
        super(group);
    }

    protected HelloHystrixCommand(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool) {
        super(group, threadPool);
    }

    protected HelloHystrixCommand(HystrixCommandGroupKey group, int executionIsolationThreadTimeoutInMilliseconds) {
        super(group, executionIsolationThreadTimeoutInMilliseconds);
    }

    protected HelloHystrixCommand(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool, int executionIsolationThreadTimeoutInMilliseconds) {
        super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds);
    }

    protected HelloHystrixCommand(Setter setter) {
        super(setter);
    }

    public static HelloHystrixCommand getInstance(String key){
        return new HelloHystrixCommand(HystrixCommandGroupKey.Factory.asKey(key));
    }

    @Override
    protected Model run() throws Exception {
        int i = 1 / 0;
        logger.info("run:   thread id:  " + Thread.currentThread().getId());
        return model;
    }

    @Override
    protected Model getFallback() {
        return new Model("fallback");
    }

    public static void main(String[] args) throws Exception {
        HelloHystrixCommand helloHystrixCommand = HelloHystrixCommand.getInstance("dew");
        helloHystrixCommand.model = helloHystrixCommand.new Model("run");
        logger.info("main:      " + helloHystrixCommand.model + "thread id: " + Thread.currentThread().getId());
        System.out.println(helloHystrixCommand.execute());

    }


    class Model {

        public Model(String name) {
            this.name = name;
        }

        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Model{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}

断路保护

Hystrix配置

# 执行的隔离策略 THREAD, SEMAPHORE 默认THREAD
hystrix.command.default.execution.isolation.strategy=THREAD
# 执行hystrix command的超时时间,超时后会进入fallback方法 默认1000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
# 执行hystrix command是否限制超时,默认是true
hystrix.command.default.execution.timeout.enabled=true
# hystrix command 执行超时后是否中断 默认true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
# 使用信号量隔离时,信号量大小,默认10
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=10
# fallback方法最大并发请求数 默认是10
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests=10
# 服务降级是否开启,默认为true
hystrix.command.default.fallback.enabled=true
# 是否使用断路器来跟踪健康指标和熔断请求
hystrix.command.default.circuitBreaker.enabled=true
# 熔断器的最小请求数,默认20. (这个不是很理解,欢迎补充)
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
# 断路器打开后的休眠时间,默认5000
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
# 断路器打开的容错比,默认50
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 强制打开断路器,拒绝所有请求. 默认false, 优先级高于forceClosed
hystrix.command.default.circuitBreaker.forceOpen=false
# 强制关闭断路器,接收所有请求,默认false,优先级低于forceOpen
hystrix.command.default.circuitBreaker.forceClosed=false

# hystrix command 命令执行核心线程数,最大并发 默认10
hystrix.threadpool.default.coreSize=10

使用断路保护可有效果的防止系统雪崩,Spring CloudHystrix 做了封装,详见: http://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#_circuit_breaker_hystrix_clients

需要说明的是 Hystrix 使用新线程执行代码,导致 Threadlocal 数据不能同步,使用时需要将用到的数据做为参数传入,如果需要使用 Dew 框架的上下文(请求链路/用户等获取)需要先传入再设值,e.g.

Hystrix Command 示例,及Context处理

public class HystrixExampleService {
    @HystrixCommand(fallbackMethod = "defaultFallback", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
    })
    public String someMethod(Map<String, Object> parameters, DewContext context) {
        // !!! Hystrix使用新线程执行代码,导致Threadlocal数据不能同步,
        // 使用时需要将用到的数据做为参数传入,如果需要使用Dew框架的上下文需要先传入再设值
        DewContext.setContext(context);
        try {
            Thread.sleep(new Random().nextInt(3000));
            logger.info("Normal Service Token:" + Dew.context().getToken());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "ok";
    }

    // 降级处理方法定义
    public String defaultFallback(Map<String, Object> parameters, DewContext context, Throwable e) {
        DewContext.setContext(context);
        logger.info("Error Service Token:" + Dew.context().getToken());
        return "fail";
    }
}

定时任务

使用 Spring Config 配置中心 refresh 时,在 @RefreshScope 注解的类中, @Scheduled 注解的自动任务会失效。 建议使用实现 SchedulingConfigurer 接口的方式添加自动任务。

自动任务添加

@Configuration
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {

    private Logger logger = LoggerFactory.getLogger(SchedulingConfiguration.class);

    @Autowired
    private ConfigExampleConfig config;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> logger.info("task1: " + config.getVersion()), triggerContext -> {
            Instant instant = Instant.now().plus(5, SECONDS);
            return Date.from(instant);
        });

        taskRegistrar.addTriggerTask(() -> logger.info("task2: " + config.getVersion()), new CronTrigger("1/3 * * * * ?"));
    }
}

主要性能影响参数

  • 内置 Tomcat 参数调整效果并不大,如果需要调整,建议适当调大 max-treadsaccept-count

    # 最大等待请求数 默认100
    server.tomcat.accept-count=1000
    # 最大并发数 默认200
    server.tomcat.max-threads=1000
    # 最大连接数 默认BIO:200 NIO:10000 APR:8192
    server.tomcat.max-connections=2000
  • Zuul 性能参数说明

    # 连接池最大连接,默认是200
    zuul.host.maxTotalConnections=1000
    每个route可用的最大连接数,默认值是20
    zuul.host.maxPerRouteConnections=1000
    Hystrix最大的并发请求 默认值是100
    zuul.semaphore.maxSemaphores=1000
Note Zuul 的最大并发数主要调整 maxSemaphores 优先级高于 Hystrix 的最大线程数配置.
  • Ribbon 性能参数说明调整 MaxTotalConnectionsMaxConnectionsPerHost 时建议同比调整 Pool 相关的参数

    # ribbon 单主机最大连接数,默认50
    ribbon.MaxConnectionsPerHost=500
    # ribbon 总连接数,默认 200
    ribbon.MaxTotalConnections=1000
    # 默认200
    ribbon.PoolMaxThreads=1000
    # 默认1
    ribbon.PoolMinThreads=500
Note Zuul 和其它使用 Ribbon 的服务一样,TPS主要调整 RibbonMaxConnectionsPerHostMaxTotalConnections
  • Hystrix 性能参数说明

    # 并发执行的最大线程数,默认10
    hystrix.threadpool.default.coreSize=100
    
Note 普通 Service 使用 Hystrix 时,最大并发主要调整 hystrix.threadpool.default.coreSize
Warning Hystrix 的默认超时时间为1s,在高并发下容易出现超时,建议将默认超时时间适当调长, 特殊接口需要将时间调短或更长的,使用特定配置,见上面 Feign 配置特定方法超时时间.

Zuul 保护(隐藏)内部服务的 Http 接口

在yml配置文件里配置(ignored-patterns,ignored-services)这两项中的一项即可

配置示例

zuul: #配置一项即可!
  ignored-patterns: /dew-example/**   #排除此路径
  ignored-services: dew-example       #排除此服务

缓存处理

Spring Cache 提供了很好的注解式缓存,但默认没有超时,需要根据使用的缓存容器特殊配置

Redis缓存过期时间设置

@Bean
RedisCacheManager cacheManager() {
    final RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
    redisCacheManager.setUsePrefix(true);
    redisCacheManager.setDefaultExpiration(<过期秒数>);
    return redisCacheManager;
}

Spring Boot Admin 监控实践

Spring Boot Actuator 中提供很多像 healthmetrics 等实时监控接口,可以方便我们随时跟踪服务的性能指标。Spring Boot 默认是开放这些接口提供调用的,那么就问题来了,如果这些接口公开在外网中,很容易被不法分子所利用,这肯定不是我们想要的结果。 在这里我们提供一种比较好的解决方案

  • 被监控的服务配置
management:
  security:
    enabled: false # 关闭管理认证
  context-path: /management //(1)
eureka:
  instance:
    metadata-map:
      cluster: default (2)
  1. 管理前缀
  2. 集群名称

  • Zuul 网关配置
zuul:
  ignoredPatterns: /*/management/** //(1)
  1. 同上文 management.context-path , 这里之所以不是 /management/** ,由于网关存在项目前缀,需要往前一级,大家可以具体场景具体配置

  • Spring Boot Admin 配置
spring:
  application:
    name: monitor
  boot:
    admin:
      discovery:
        converter:
          management-context-path: ${management.context-path}
      routes:
        endpoints: env,metrics,dump,jolokia,info,configprops,trace,logfile,refresh,flyway,liquibase,heapdump,loggers,auditevents,hystrix.stream,turbine.stream  (1)
      turbine:
        clusters: default  (2)
        location: ${spring.application.name}

turbine:
  instanceUrlSuffix: ${management.context-path}/hystrix.stream
  aggregator:
    clusterConfig: default (2)
  appConfig: monitor-example,hystrix-example (3)
  clusterNameExpression: metadata['cluster']

security:
  basic:
    enabled: false

server:
  port: ...

eureka:
  instance:
    metadata-map:
      cluster: default (2)
  client:
    serviceUrl:
      defaultZone: ...

management:
  security:
    enabled: false
  context-path: /management (4)
  1. 要监控的内容
  2. 要监控的集群名称
  3. 添加需要被监控的应用 Service-Id ,以逗号分隔
  4. 同上文 management.context-path

jdbc 批量插入性能问题

如果不开启rewriteBatchedStatements=true,那么jdbc会把批量插入当做一行行的单条处理,也就没有达到批量插入的效果

jdbc配置示例

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/dew?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true
    username: root
    password: 123456
  • 对于一张七列的表,插入1500条数据,分别对mybatis和jdbctemplate进行测试,记录三次数据如下,可以看到,该配置对于jdbctemplate影响是极大的,而对于mybatis影响却不大,后续有时间再继续深入了解

Table 1. 测试数据

rewriteBatchedStatements mybatis(ms) jdbctemplate(ms) dew(ms)
true 401 88 174
true 427 78 167
true 422 75 176
false 428 1967 2065
false 410 2641 2744
false 369 2299 2398

http请求并发数性能瓶颈

  • 当策略为Thread时(默认是Thread),hystrix.threadpool.default.maximumSize为第一个性能瓶颈,默认值为10.
Tip 修改值时,需要先设置hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSize为true,默认为false

hystrix详细配置参见https://github.com/Netflix/Hystrix/wiki/configuration#allowMaximumSizeToDivergeFromCoreSize

  • 第二个瓶颈为springboot内置的tomcat的最大连接数,参数为server.tomcat.maxThreads,默认值为200

日志中解析message,动态显示property

  1. 在启动类的main方法中注册converter,如下PatternLayout.defaultConverterMap.put("dew", TestConverter.class.getName());
Note 这个是解析%dew的内容
  1. 自定义Converter继承DynamicConverter,解析message,获取有效信息并返回解析后得到的字符串。

Converter代码示例

public class TestConverter extends DynamicConverter<ILoggingEvent> {

    @Override
    public String convert(ILoggingEvent event) {
        // 这里未做解析,示例代码
        return event.getMessage();
    }
}