大家好,我是一安,今天聊一下实际开发中对应用服务性能的优化。文章末尾有惊喜
正文
基于项目背景的介绍,现网硬件设备配置为 64 位 Centos6.10 系统,32G 内存,服务采用 F5+Nginx 负载均衡,在一台服务器部署多应用的策略之外,如何提升单台设备上的服务的性能呢?
系统性能是指操作系统完成任务的有效性,稳定性和响应速度。在提高系统性能的过程中,有一定的规律可循,首先数据要从用户发起请求开始,到最终再返还给用户,在整个过程中,有很多环节都是可以进行优化的,并且优化的难易程度都是从外到内逐渐递增,比如最简单的优化就是购买多核处理器,增加内存,增大网络带宽,减小网络延时的优化,再到程序处理速度的优化,最后到数据库系统的优化,其中对性能影响最大的是操作系统和应用程序。基于以上分析,我们主要从以下三方面入手:
应用服务优化
集团服务系统主要逻辑处理为接收用户侧的大量的请求转发给省内的接口服务,接收到省内接口服务的响应,并将返回的消息发给用户侧。在不考虑第三方接口的性能,服务器及网络带宽情况下,如何提升自己程序的性能呢?
首先想到了 java 中的队列,一般情况下,如果是少量消息的处理,并且处理时间很短的情况下是不需要使用队列的,直接请求接口调用就可以。但是,一旦要有大量消息处理的时候,由于系统本身的处理是有限度的,如果达到这个上限,依然有大量的消息来了,会造成消息阻塞,这极大的可能会导致系统崩溃,用户无法再使用,这显然不是我们希望看到的。鉴于项目的高并发要求,这个时候引入队列是十分有必要的。当我们接收到消息后,先把消息放到队列中,将数据进行持久化直到它们被完全处理,避免消息阻塞,同时也规避了数据丢失的风险。在 Java 的并发包中已经提供了 BlockingQueue 的实现,比较常用的有 ArrayBlockingQueue 和 LinkedBlockingQueue,前者是以数组的形式存储,后者是以 Node 节点的链表形式存储。
//存放所有接口请求队列的对象
@Component
public class RequestQueue{
@Value("$(queueTotal:1000)")
private Integer queueTotal;
//处理请求接口的队列,设置缓冲容量为queueTotal
private BlockingQueue<Map<String,Object>> queue = new LinkedBlockingQueue<>(queueTotal);
//任务加入队列
public void putRequest(Object o, int timeout) throws InterruptedException {
queue.offer(Map<String,Object> o, timeout, TimeUnit.SECONDS);
}
public BlockingQueue<Map<String,Object>> getQueue(){
return queue;
}
}
其次考虑到请求的并行,在放入消息队列成功后,对线程进行监听,为确保消息的并行,提高处理效率,同时为避免大量的创建线程,造成额外开销,引入了线程池的方式。
//队列监听器,初始化启动所有监听任务
@Component
public class QueueListener{
//处理业务
@Autowired
private QueueTask queueTask;
//初始化时启动监听请求队列
@PostConstruct
public void init(){
new Thread(queueTask).start();
}
//销毁容器时停止监听任务
@PreDestroy
public void destory(){
queueTask.setRunning(false);
}
}
@Component
public class QueueTask implements Runnable{
@Autowired
private RequestQueue queue;
private boolean running = true;
@SneakyThrows
@Override
public void run() {
while (running) {
//获取参数条件,执行具体任务,可将任务放入线程池
Map<String,Object> map = queue.getQueue().take();
}
}
public void setRunning(boolean running) {
this.running = running;
}
}
@Configuration
public class GlobalConfig {
@Value("${corePoolSize:100}")
private int corePoolSize;//线程池维护线程的最少数量
@Value("${maxPoolSize:100}")
private int maximumPoolSize;//线程池维护线程的最大数量
/**
* 默认线程池线程池
*
* @return Executor
*/
@Bean
public ThreadPoolTaskExecutor defaultThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数目
executor.setCorePoolSize(corePoolSize);
//指定最大线程数
executor.setMaxPoolSize(maximumPoolSize);
//队列中最大的数目
executor.setQueueCapacity(50);
//线程名称前缀
executor.setThreadNamePrefix("defaultThreadPool_");
//rejection-policy:当pool已经达到max size的时候,如何处理新任务
//CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
//对拒绝task的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//线程空闲后的最大存活时间
executor.setKeepAliveSeconds(60);
//加载
executor.initialize();
return executor;
}
}
最后考虑到 http 请求的处理,如果每个请求都创建一个 http 实例,即重新建立 TCP 连接(经历 3 次握手),用完就会关闭连接(4 次挥手),这会造成严重的耗时,并且在高并发情况下会导致系统资源很快被用完,导致无法建立新的连接。为了保证能够复用 http 连接,省去了 tcp 的 3 次握手和 4 次挥手的时间,降低请求响应的时间,并且保证自动管理 tcp 连接,不用人为地释放/创建连接,这里引入了 http 连接池,并交给 spring 管理。
@Configuration
@Component
public class RestTemplateConfig {
@Value("${maxTotal:100}")
private Integer maxTotal;
@Value("${perRoute:20}")
private Integer defaultMaxPerRoute ;
/**
* 让spring管理RestTemplate,参数相关配置
* @param builder
* @return
*/
@Bean(name = "restTemplate")
public RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = builder.build();// 生成一个RestTemplate实例
restTemplate.setRequestFactory(clientHttpRequestFactory());
return restTemplate;
}
/**
* 自定义RestTemplate配置
* 1、设置最大连接数
* 2、设置路由并发数
* 3、设置重试次数
* @return
*/
public ClientHttpRequestFactory clientHttpRequestFactory() {
// 长连接保持时长30秒
PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(
30, TimeUnit.SECONDS);
// 最大连接数
pollingConnectionManager.setMaxTotal(maxTotal);
// 单路由的并发数
pollingConnectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setConnectionManager(pollingConnectionManager);
// 重试次数2次,并开启
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true));
// 保持长连接配置,需要在头添加Keep-Alive
httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());//长连接配置
httpClientBuilder.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE); //是否重用
HttpClient httpClient = httpClientBuilder.build();
// httpClient连接底层配置clientHttpRequestFactory
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
clientHttpRequestFactory.setReadTimeout(2000);// ms
clientHttpRequestFactory.setConnectTimeout(5000);//ms
//设置从连接池获取连接的超时时间,不宜过长
clientHttpRequestFactory.setConnectionRequestTimeout(200);
//缓冲请求数据,默认为true。POST或者PUT大量发送数据时,建议将此更改为false,以免耗尽内存
clientHttpRequestFactory.setBufferRequestBody(false);
return clientHttpRequestFactory;
}
}
服务容器 Nginx 优化
Nginx是一个高性能的 HTTP 和反向代理 web 服务器,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名于世,在这里利用 Nginx 实现多程序负载均衡,为将 Nginx 的性能发挥最大化,故对默认参数做了如下优化:
#nginx 进程数,建议按照cpu数目来指定,一般为它的倍数(如,2个四核的cpu计为8)。
worker_processes 4;
#为每个进程分配cpu,上例中将8个进程分配到8个cpu,当然可以写多个,或者将一个进程分配到多个cpu。
worker_rlimit_nofile 65535;
events {
#每个进程允许的最多连接数,理论上每台nginx服务器的最大连接数为worker_processes*worker_connections。
worker_connections 10240;
#高效事件模型
use epoll;
#打开同时接受多个新网络连接请求的功能
multi_accept on;
#优化同一时刻只有一个请求而避免多个睡眠进程被唤醒
accept_mutex on;
}
操作系统优化
影响 linux 性能的因素有很多,除了系统硬件资源(CPU,内存,磁盘 I/O 性能,网络带宽)之外,由于我们部署的 web 应用,那么就需要根据 web 应用特性进行网络参数的优化,来满足高并发对系统资源的利用:
首先对系统连接数优化,提升系统的 I/O 流,执行命令如下:
ulimit -n 65535 ;
其次对内核参数的优化,通过修改/etc/sysctl.conf,一些配置参数如下:
#最大跟踪连接数
net.nf_conntrack_max = 655360
#理论上不用这么长,不小于net.ipv4.tcp_keepalive_time 就行了。默认432000秒(5天)
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
#web应用中listen函数的backlog默认会给我们内核参数的net.core.somaxconn限制到128,#而nginx定义的NGX LISTEN BACKLOG默认为511,所以有必要调整这个值
net.core.somaxconn = 655360
#记录的那些尚未收到客户端确认信息的连接请求的最大值。#对于有128M内存的系统而言,缺省值是1024,小内存的系统则是128
net.ipv4.tcp_max_syn_backlog = 819200
#为了打开对端的连接,内核需要发送一个SYN并附带一个回应前面一个SYN的ACK。#也就是所谓三次握手中的第二次握手。这个设置决定了内核放弃连接之前发送SYN+ACK包的数量
net.ipv4.tcp_synack_retries =1
#在内核放弃建立连接之前发送SYN包的数量
net.ipv4.tcp_syn_retries =1
#每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
net.core.netdev_max_backlog = 262144
#系统中最多有多少个TCP套接字不被关联到任何一个用户文件句柄上。这个限制仅仅是为了防止简单的Dos攻击,#不能过分依靠它或者人为地减小这个值,更应该增加这个值(如果增加了内存之后)net.ipv4.tcp_max_orphans = 262144
#如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间。
#对端可以出错并永远不关闭连接,甚至意外当机。缺省值是60秒。
#2.2 内核的通常值是180秒,你可以按这个设置,但要记住的是,
#即使你的机器是一个轻载的WEB服务器,也有因为大量的死套接字而内存溢出的风险,#FIN- WAIT-2的危险性比FIN-WAIT-1要小,因为它最多只能吃掉1.5K内存,但是它们的生存期长些。
net.ipv4.tcp_fin_timeout =1
#表示当keepalive起用的时候, TCP发送keepalive消息的频度(单位:秒)
net.ipv4.tcp_keepalive_time = 30
#对外连接端口范围
net.ipv4.ip_local_port_range = 1024 65000
#表示文件句柄的最大数量
fs.file-max = 2000000
#timewait的数量,默认是180000
net.ipv4.tcp_max_tw_buckets = 256000
#关闭TCP连接中time_wait sockets的快速回收
net.ipv4.tcp_tw_recycle =0
#开启TCP连接复用功能,允许将time wait sockets重新用于新的TCP连接(主要针对time_wait连接)
net.ipv4.tcp_tw_reuse=1
#时间戳可以避免序列号的卷绕。一个1Gbps的链路肯定会遇到以前用过的序列号。#时间戳能够让内核接受这种"异常"的数据包。
net.ipv4.tcp_timestamps=1
性能优化结果
本次性能测试工具用的是 Apache Bench 简称 ab,它是 Apache 自带的压力测试工具。ab 非常实用,它不仅可以对 Apache 服务器进行网站访问压力测试,也可以对或其它类型的服务器进行压力测试。
本次性能测试基准单台服务器为总请求量 100000,并发 3000,测试其每秒并发请求数以及单个请求响应时间对比
单个应用服务优化前后对比
服务容器 Nginx 负载本机 3 个接口服务和操作系统参数的优化前后对比
经过以上测试得出,本次通过对应用服务、服务容器、操作系统的优化后,集团服务系统性能得到了明显的提升,单接口服务每秒处理请求数平均 553.4 提升至 839.9,且响应时间满足 2s 内返回;单台 3 接口负载服务每秒处理请求数从平均 1282.5 提升到 2111.8,且响应时间满足在 2s 内返回;
关注一安未来面试题获取
,面试题几乎包含了所有Java技术栈,后台回复【888】即可获取
原文始发于微信公众号(一安未来):面对大量请求如何对系统进行性能优化
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44824.html