记录些Spring+题集(23)

Redis 客户端

客户端组件1:Jedis

Jedis 是老牌的 Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令的支持。

优点:

  • 支持全面的 Redis 操作特性(可以理解为API比较全面)。 

缺点:

  • 使用阻塞的 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步;

  • Jedis 客户端实例不是线程安全的,所以需要通过连接池来使用 Jedis。

客户端组件2:Redisson

Redisson 是在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。Redisson 提供了使用Redis 的最简单和最便捷的方法。它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务。其中包括:

BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service)

Redisson 的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

优点:

  • 使用者对 Redis 的关注分离,可以类比 Spring 框架,这些框架搭建了应用程序的基础框架和功能,提升开发效率,让开发者有更多的时间来关注业务逻辑;

  • 提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列等。

  • Redisson基于Netty框架的事件驱动的通信层,其方法调用是异步的。

  • Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作

缺点:

  • Redisson 对字符串的操作支持比较差。

客户端组件3:lettuce

lettuce是可扩展的线程安全的 Redis 客户端,支持异步模式。lettuce 底层基于 Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。lettuce能够支持redis4,需要java8及以上。lettuce是基于netty实现的与redis进行同步和异步的通信。

优点:

  • 支持同步异步通信模式;

  • Lettuce 的 API 是线程安全的,如果不是执行阻塞和事务操作,如BLPOP和MULTI/EXEC,多个线程就可以共享一个连接。

三者比较

Jedis使直接连接redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个jedis实例增加物理连接 ;

lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,StatefulRedisConnection是线程安全的,所以一个连接实例可以满足多线程环境下的并发访问,当然这也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

Jedis 和 lettuce 是比较纯粹的 Redis 客户端,几乎没提供什么高级功能。Jedis 的性能比较差,所以如果你不需要使用 Redis 的高级功能的话,优先推荐使用 lettuce。

Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

如果需要分布式锁,分布式集合等分布式的高级特性,添加Redisson结合使用,因为Redisson本身对字符串的操作支持很差。Redisson 的优势是提供了很多开箱即用的 Redis 高级功能,如果你的应用中需要使用到 Redis 的高级功能,建议使用 Redisson。

具体 Redisson 的高级功能可以参考:https://redisson.org/

使用建议

建议:lettuce + Redisson。

在spring boot2之后,redis连接默认就采用了lettuce。就像 spring  的本地缓存,默认使用Caffeine一样,这就一定程度说明了,lettuce 比 Jedis在性能的更加优秀

链接断裂怎么办?

Jedis有心跳,能保持长连接,lettuce好像没有心跳。阿里 ecs 搭的redis tcp长时间没有传输就会断开,但是lettuce感知不到,再执行redis请求就会提示链接不可用。具体来说,可以通过用netty的空闲检测机制来维持连接。

注意:是空闲检测 不是心跳机制。

什么是心跳机制

心跳是在TCP长连接中,客户端和服务端定时向对方发送数据包通知对方自己还在线,保证连接的有效性的一种机制。在服务器和客户端之间一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互自然地, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性

空闲检测 是心跳的基础机制。

什么是空闲检测

就是检测通道中的读写数据包,如果一段时间内,没有收到读写数据包,就会出发  IdleStateEvent 空闲状态事件。所以,可以借助这个机制,主动关闭 空闲的、被异常断开的连接。

import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.NettyCustomizer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class ClientConfig {
 
    @Bean
    public ClientResources clientResources(){
 
        NettyCustomizer nettyCustomizer = new NettyCustomizer() {
 
            @Override
            public void afterChannelInitialized(Channel channel) {
                channel.pipeline().addLast(
                        //此处事件必须小于超时时间 
                        new IdleStateHandler(40, 0, 0));
                channel.pipeline().addLast(new ChannelDuplexHandler() {
                    @Override
                    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                        if (evt instanceof IdleStateEvent) {
                            ctx.disconnect();
                        }
                    }
                });
            }
 
            @Override
            public void afterBootstrapInitialized(Bootstrap bootstrap) {
 
            }
 
        };
 //替换掉 NettyCustomizer 通道初始化处理器
        return ClientResources.builder().nettyCustomizer(nettyCustomizer ).build();
    }
}

参考

  • RESP协议1:https://www.cnblogs.com/4a8a08f09d37b73795649038408b5f33/p/9998245.html

  • RESP协议2:https://my.oschina.net/u/2474629/blog/913805

  • RESP协议3:https://www.cnblogs.com/throwable/p/11644790.html

  • Redis的三个框架:Jedis,Redisson,Lettuce:https://www.cnblogs.com/williamjie/p/11287292.html

  • redis客户端选型-Jedis、lettuce、Redisson:https://blog.youkuaiyun.com/a5569449/article/details/106891111/

Order by 性能提升10倍方案

Order by 工作原理

Order by 执行流程,分为两步,具体如下:

图片

第1步:索引的查找

根据where 后面的字段,进行 二级索引的查找,找到后再回表 聚集索引,拿到需要的字段

第2步:原始数据的排序

原始数据的数据, 并不是按照 order  by 有序的。所以,需要按照 order  by 字段,进行排序。接下来,排序的地点在哪里呢?

  • 优先选择内存。因为内存的性能高。

  • 如果原始的数据实在规模太大,就借助磁盘进行排序。

用于排序的内存,称为 sort_buffer。原始数据放在内存 sort_buffer 中。 其实 MySQL 会给每个线程分配一块内存用于排序的 sort_buffer

Explain查看执行计划

Explain关键字查看一下执行计划。

select name, age, city from user where city = '北京' order by age limit 5;

图片

可以看到:

  • key 字段表示使用到 city 索引

  • Extra 字段中的Using index condition 表示用到了索引条件

  • Using filesort表示用到了文件排序 

用到文件排序,说明第一次查出来的 原始数据,在内存放不下, 需要借助 磁盘空间进行排序,磁盘IO的性能比较低的,所以,需要进行调优。

Order by 执行完整流程,如下:

  1. 当前线程首先初始化 sort_buffer 块。

  2. 然后从 city 索引树从查询一条满足city='北京'的主键 ID,  比如图中的id=2

  3. 接着在聚簇索引树中查询id=2的一行数据,将nameagecity三个字段的值,存到sort_buffer

  4. 继续重复前两个步骤,直到 city 索引树中找不到city='北京'的主键 ID。

  5. 最后在sort_buffer中,将所有数据根据age 进行排序,取前 5 行返回给客户端。

全字段排序就是将查询所需的字段,如nameagecity三个字段数据全部存到 sort_buffer 中。

3个核心概念

  • 全字段排序

  • 外部排序

  • rowid 排序

全字段排序

sort_buffer是 MySQL 为每个任务线程维护的一块内存区域,用于进行排序。sort_buffer 的大小可以通过 sort_buffer_size 来设置。sort_buffer_size 是一个用于控制sort_buffer内存大小的参数。

sort_buffer 是一块固定大小的内存,如果数据量太大,sort_buffer 放不下怎么办呢?

外部排序

如果要排序的数据小于 sort_buffer_size,那在 sort_buffer 内存中就可以完成排序,如果要排序的数据大于 sort_buffer_size,则需要外部排序,借助磁盘文件来进行排序。

通过执行以下命令,可以查看 SQL 语句执行中是否采用了磁盘文件辅助排序。

set optimizer_trace = "enabled=on";

select name,age,city from user where city = '北京' order by age limit 5;

select * from information_schema.optimizer_trace;

可以从number_of_tmp_files中看出,是否使用了临时文件。

{
    "join_execution": {
      "select#": 1,
      "steps": [
        {
          "filesort_information": [
            {
              "direction": "asc",
              "table": "`user`",
              "field": "age"
            }
          ],
          "filesort_priority_queue_optimization": {
            "limit": 5,
            "rows_estimate": 992,
            "row_size": 112,
            "memory_available": 262144,
            "chosen": true
          },
          "filesort_execution": [
          ],
          "filesort_summary": {
            "rows": 3,
            "examined_rows": 28,
            "number_of_tmp_files": 0,
            "sort_buffer_size": 720,
            "sort_mode": "<sort_key, additional_fields>"
          }
        }
      ]
    }
}

number_of_tmp_files 表示使用来排序的磁盘临时文件数。如果 number_of_tmp_files>0,则表示使用了磁盘文件来进行排序。

使用了磁盘临时文件后,当 sort_buffer 内存不足时,先进行排序,将排序后的数据存放到一个小磁盘文件中,清空 sort_buffer。

然后继续存放数据到 sort_buffer,重复以上步骤。最后将多个磁盘小文件合并成一个有序的大文件。

Tips: 磁盘小文件合并排序,使用的是归并排序算法

这样依然会存在问题,数据存放到临时磁盘小文件,然后还需要归并排序为大文件,再读入到内存中,返回结果集,整体排序效率低下。

rowid 排序

要解决上述问题,可以将只需要用于排序的字段和主键 ID 放入 sort_buffer 中,也就是 rowid 排序,这样就可以在 sort_buffer 中完成排序。

max_length_for_sort_data 是一个用于表示 MySQL 用于排序行数据长度的一个参数,如果单行数据的长度超过了这个值,那么可能会导致采用临时文件排序,mysql 会换用 rowid 排序。

可以通过命令看下这个参数取值。

show variables like 'max_length_for_sort_data';

图片

max_length_for_sort_data 默认值是 1024。本文中name,age,city 长度=64+4+64=132<1024, 所以走的是全字段排序。

我们执行以下命令,改下参数值,再重新执行 SQL。

set max_length_for_sort_data = 32;

select name,age,city from user where city = '北京' order by age limit 5;

使用 rowid 排序的后,执行示意图如下:

图片

对比全字段排序,rowid 排序最后需要根据主键 ID 获取对应字段数据即多了回表查询。

当需要查询的数据在索引树中不存在的时候,需要再次到聚集索引中去获取,这个过程叫做回表

我们通过执行以下命令,可以看到是否使用了 rowid 排序的:

set optimizer_trace = "enabled=on";

select name,age,city from user where city = '北京' order by age limit 5;

select * from information_schema.optimizer_trace;
{
  "join_execution": {
    "select#": 1,
    "steps": [
      {
        "filesort_information": [
          {
            "direction": "asc",
            "table": "`user`",
            "field": "age"
          }
        ],
        "filesort_priority_queue_optimization": {
          "limit": 5,
          "rows_estimate": 992,
          "row_size": 8,
          "memory_available": 262144,
          "chosen": true
        },
        "filesort_execution": [],
        "filesort_summary": {
          "rows": 3,
          "examined_rows": 28,
          "number_of_tmp_files": 0,
          "sort_buffer_size": 96,
          "sort_mode": "<sort_key, rowid>"
        }
      }
    ]
  }
}

sort_mode表示排序模式为 rowid 排序。

Order by 的优化思路

  • 联合索引优化

    如果数据本身是有序的,那就不需要排序,而索引数据本身是有序的,所以,我们可以通过建立联合索引,跳过排序步骤。

  • 参数优化

可以通过调整max_length_for_sort_data等参数优化排序算法;

参数优化

通过调整参数,也可以优化order by的执行。

  1. 调整 sort_buffer_size 参数的值。如果 sort_buffer 值太小而数据量大的话,MySQL 会采用磁盘临时文件辅助排序。MySQL 服务器配置高的情况下,可以将参数调大些。

  2. 调整 max_length_for_sort_data 的值,值太小的话 MySQL 会采用 rowid 排序,会多一次回表操作导致查询性能降低。同样可以适当调大些。

Select分页 性能提升100倍方案

MySQL分页起点越大查询速度越慢

在数据库开发过程中我们经常会使用分页,核心技术是使用用limit start, count分页语句进行数据的读取。

  • limit语句的查询时间与起始记录的位置成正比

  • mysql的limit语句是很方便,但是对记录很多的表并不适合直接使用。

SELECT * FROM 表名 limit m,n;
SELECT * FROM table LIMIT [offset,] rows;

注释:Limit子句可以被用于强制 SELECT 语句返回指定的记录数。Limit接受一个或两个数字参数,参数必须是一个整数常量。如果给定两个参数,

  • 第一个参数指定第一个返回记录行的偏移量,

  • 第二个参数指定返回记录行的最大数目。

1.m代表从m+1条记录行开始检索,n代表取出n条数据。(m可设为0)

2.值得注意的是,n可以被设置为-1,当n为-1时,表示从m+1行开始检索,直到取出最后一条数据。

如:SELECT * FROM 表名 limit 6,-1;

表示:取出第6条记录行以后的所有数据。

3.若只给出m,则表示从第1条记录行开始算一共取出m条。

MySQL百万级数据大分页查询优化

方法1: 直接使用数据库提供的SQL语句

语句样式:

SELECT * FROM 表名称 LIMIT start, count

功能

Limit限制的是从结果集的start 位置处取出count 条输出,其余抛弃。

原因/缺点:

全表扫描,速度会很慢。而且,有的数据库结果集返回不稳定。

适应场景:

适用于数据量较少的情况,元祖数量、记录数量级别:百/千级

方法2: 建立主键或唯一索引, 利用索引(假设每页10条)

语句样式:

SELECT * FROM 表名称 WHERE id_pk > (pageNum*10) LIMIT M

除了主键,也可以 利用唯一键索引快速定位部分元组,避免全表扫描

比如: 读第1000到1019行元组(pk是唯一键).

SELECT * FROM 表名称 WHERE pk>=1000 ORDER BY pk ASC LIMIT 0,20

原因:

索引扫描,速度会很快。

缺点:

如果数据查询出来并不是按照pk_id排序,并且pk_id全部数据都存在没有缺失可以作为序号使用,不然,分页会有漏掉数据。

适应场景:

  • 适用于数据量多的情况(元组数上万)

  • id数据没有缺失,可以作为序号使用

方法3: 基于索引再排序

语句样式:

SELECT * FROM 表名称 WHERE id_pk > (pageNum*10) ORDER BY id_pk ASC LIMIT M

适应场景:

适用于数据量多的情况(元组数上万),最好ORDER BY后的列对象是主键或唯一索引,id数据没有缺失,可以作为序号使用。

使得ORDERBY操作能利用索引被消除但结果集是稳定的。

原因:

索引扫描,速度会很快,但MySQL的排序操作,只有ASC没有DESC。MySQL中,索引存储的排序方式是ASC的,没有DESC的索引。这就能够理解为啥order by 默认是按照ASC来排序的。

虽然索引是ASC的,但是也可以反向进行检索,就相当于DESC。

方法4: 基于索引使用prepare

语句样式:

PREPARE stmt_name FROM SELECT * FROM 表名称 
                        WHERE id_pk > (?*10) ORDER BY id_pk ASC LIMIT M

第一个问号表示pageNum

适应场景:

大数据量

原因:

索引扫描,速度会很快。prepare语句又比一般的查询语句快一点。

方法5: 利用"子查询+索引"快速定位元组

利用"子查询+索引"快速定位元组的位置,然后再读取元组。

SELECT * FROM your_table 
WHERE 
id <= (
 SELECT id FROM your_table ORDER BY id desc LIMIT ($page-1)*$pagesize) 
ORDER BY id desc LIMIT $pagesize

方法6: 利用"连接+索引"快速定位元组的位置,读取元组

SELECT * FROM your_table AS t1 
JOIN 
(SELECT id FROM your_table ORDER BY id desc LIMIT ($page-1)*$pagesize) AS t2 
WHERE t1.id <= t2.id 
ORDER BY t1.id desc LIMIT $pagesize;

方法7: 利用表的索引覆盖来调优

利用索引查找有优化算法,且数据就在查询索引上面,不用再去找相关的数据地址了,这样节省了很多时间。另外MySQ中也有相关的索引缓存,在并发高的时候利用缓存就效果更好了。

方法8: 利用复合索引进行优化

如果对于有where 条件,又想走索引用limit的,必须设计一个索引,将where 放第一位,limit用到的主键放第2位,而且只能select 主键

MySQL数据库开发三十六条军规

一.核心军规

  • 尽量不在数据库做运算,cpu计算的事务必移至业务层

  • 控制表、行、列数量【控制单张表的数据量 1年/500W条,超出可做分表】,【单库表数据量不超过300张】 、【单张表的字段个数不超过50个,多了拆表】

  • 三大范式没有绝对的要使用,效率优先时可适当牺牲范式

  • 拒绝3B(拒绝大sql语句:big sql、拒绝大事物:big transaction、拒绝大批量:big batch)

二.字段类军规

  • 用好数值类型(用合适的字段类型节约空间); 如:一个字段注定就只有1跟2  要设计成 int(1)  而不是 int(11)

  • 字符转化为数字(能转化的最好转化,同样节约空间、提高查询性能)
    如,一个字段注定就只有1跟2,要设计成int(1) 而不是char(1)    查询优化如:字段类型是 char(1) 查询应当where xx='1' 而不是 xx=1 会导致效率慢

  • 避免使用NULL字段

NULL字段很难查询优化、NULL字段的索引需要额外空间、NULL字段的复合索引无效

如:要设计成 `c` int(10) NOT NULL DEFAULT 0 而不是 `c` int(10) NOT NULL
  • 少用text/blob类型(尽量使用varchar代替text字段), 需要请拆表

  • 不在数据库存图片,请存图片路径,然后图片文件存在项目文件夹下。

三.索引类军规

  • 合理使用索引

改善查询,减慢更新,索引一定不是越多越好;

如:不要给性别创建索引
  • 字符字段必须建前缀索引;

`pinyin` varchar(100) DEFAULT NULL COMMENT '小区拼音',  KEY `idx_pinyin` (`pinyin`(8))
  • 不在索引做列运算;

如:WHERE to_days(current_date) – to_days(date_col) <= 10   
改为:WHERE date_col >= DATE_SUB('2011-10- 22',INTERVAL 10 DAY);
  • innodb主键推荐使用自增列

主键建立聚簇索引,主键不应该被修改,字符串不应该做主键

如:用独立于业务的AUTO_INCREMENT
  • 不用外键(由程序保证约束);

四.SQL类军规

  • sql语句尽可能简单

一条sql只能在一个cpu运算,大语句拆小语句,减少锁时间,一条大sql可以堵死整个库;  
  • 简单的事务;

  • 避免使用trig/func(触发器、函数不用,由客户端程序取而代之);

  • 不用select *(消耗cpu,io,内存,带宽,这种程序不具有扩展性);

如:select a,b,c 会比 select * 好 只取需要列
  • OR改写为 IN

如:where  a=1 or a=2  改  a in(1,2)
  • OR改写为UNION

针对不同字段 where a=1 or b=1 
改:select 1 from a where a=1  union select 1 from a where b=1
  • 避免负向%;

如  where a like %北京%  改为  where a like '北京%'
  • limit高效分页(limit越大,效率越低);

如 Limit 10000,10 改为 where id >xxxx limit 11
  • 使用union all替代union(union有去重开销);

  • 高并发db少用2个表以上的join;

  • 使用group by 去除排序加快效率;

如:group by name   默认是asc排序
改:group by name  order by null 提高查询效率
  • 请使用同类型比较;

如:where 双精度=双精度  数字=数字  字符=字符  避免转换导致索引丢失
  • 打散大批量更新;

如:在凌晨空闲时期更新执行

五.约定类军规

  • 隔离线上线下

如:开发用dev库,测试用qa库,模拟用sim库, 线上用线上库,开发无线上库操作权限
  • 不在程序端加锁,即外部锁,外部锁不可控,会导致 高并发会炸,极难调试和排查

  • 统一字符 UTF-8 校对规则 utf8_general_ci  出现乱码 SET NAMES UTF8

  • 统一命名规范,库表名一律小写,索引前缀用idx_ 库名 用缩写(2-7字符),不使用系统关键字保留字命名

参考链接

https://blog.youkuaiyun.com/qq_43518425/article/details/113876287

https://blog.youkuaiyun.com/whzhaochao/article/details/49126037

https://blog.youkuaiyun.com/wuzhangweiss/article/details/101156910

https://blog.youkuaiyun.com/Jerome_s/article/details/44992549

处理Nginx后端服务大量TIME-WAIT

图片

图片

在构建TCP客户端服务器系统时,一些看似简单的错误可能会严重影响系统的可伸缩性。其中,对状态考虑不足是一个常见问题。特别是TIME_WAIT状态,它经常被误解,但它在TCP状态转换图中扮演着重要角色。套接字进入TIME_WAIT状态后,会保持一段时间,如果系统中存在大量处于该状态的套接字,那么创建新连接的能力就会受限,进而影响客户端服务器系统的扩展能力。

关于套接字为何以及如何进入TIME_WAIT状态,很多人存在误解,但实际上这一过程并不复杂。从TCP状态转换图中可以看出,TIME_WAIT通常是TCP客户端的最终状态。我们需要明确,这个过程并非神秘莫测,而是有其存在的合理性和必要性。

图片

虽然状态转换图上标明TIME_WAIT为最终状态,但这并不意味着只有客户端会进入TIME_WAIT。实际上,任何执行“主动关闭”操作的对等端,无论是客户端还是服务器,都可能达到这个最终状态。所谓“主动关闭”,指的是在TCP连接中,首先调用Close()来结束连接的一方。

在许多协议和客户端/服务器交互模式中,通常是客户端发起主动关闭。然而,在HTTP和FTP等协议的服务器端,往往是服务器来执行这一操作。因此,触发对等端进入TIME_WAIT状态的具体事件序列可以是多样的,既可能是客户端,也可能是服务器。

图片

状态转换图上虽然将TIME_WAIT标识为客户端的最终状态,但这并不意味着只有客户端会进入这一状态。实际上,任何发起“主动关闭”的对等端,无论是客户端还是服务器,都可能最终进入TIME_WAIT状态。那么,何谓“主动关闭”呢?

当TCP对等方首先调用Close()来关闭连接时,它就被认为是发起了“主动关闭”。在多数协议和客户端/服务器模型中,通常是客户端执行这一操作。然而,在HTTP和FTP等协议的服务端,往往是服务器来执行主动关闭。了解套接字如何进入TIME_WAIT状态后,我们再来探讨这个状态存在的原因及其可能带来的问题。

TIME_WAIT常被称作2MSL等待状态,这是因为套接字在这个状态下的持续时间是最大段寿命(MSL)的两倍。MSL指的是一个TCP段在网络中保持有效状态的最大时间,这个时间限制通常由IP数据报中的TTL字段决定。

不同的TCP实现会选择不同的MSL值,常见的设定为30秒、1分钟或2分钟。根据RFC 793的规定,MSL的默认值为2分钟,Windows系统默认采用这个值,但可以通过修改TcpTimedWaitDelay注册表设置来调整。

TIME_WAIT可能会影响系统的可伸缩性,因为一个完全关闭的TCP套接字将保持在TIME_WAIT状态大约4分钟。如果连接频繁地打开和关闭,系统中TIME_WAIT状态的套接字数量可能会不断累积;您可以通过netstat命令来查看这些套接字。由于本地端口的数量有限,这会限制可以建立的套接字连接数量。

如果过多的套接字处于TIME_WAIT状态,您可能会发现难以建立新的出站连接,因为缺少可用的本地端口。那么,为什么需要TIME_WAIT状态呢?存在两个主要原因。

首先,TIME_WAIT用于防止一个连接的延迟段被错误地认为是后续连接的一部分。在连接处于2MSL等待状态期间,任何到达的段都会被丢弃。

图片

在讨论TCP连接的终止时,我们注意到TIME_WAIT状态对于防止数据混淆至关重要。在上图中,我们看到了两个从端点1到端点2的连接,它们的地址和端口在每次连接中都是相同的。第一个连接是由端点2发起的主动关闭来终止的。如果端点2没有维持足够的TIME_WAIT状态以确保所有来自前一个连接的延迟段已经过期,那么这些延迟段可能会被错误地认为是第二个连接的一部分。

虽然这种情况发生的可能性较低,但仍需注意。首先,两个端点的地址和端口必须相同,这在实际操作中并不常见,因为操作系统通常会为客户端分配一个临时端口,从而在连接间产生变化。其次,延迟段的序列号必须在新连接中有效,这也是不太可能的情况。然而,如果这两个条件同时满足,TIME_WAIT状态就能防止新连接的数据被破坏。

TIME_WAIT存在的第二个原因是确保TCP连接能够可靠地实现全双工终止。如果从端点2发出的ACK被丢弃,端点1将重新发送FIN。如果此时端点2已经进入CLOSED状态,它唯一能做的响应就是发送一个RST,因为重传的FIN会被视为意外的。这会导致端点1收到错误信号,即使所有数据都已正确传输。

不幸的是,一些操作系统在实现TIME_WAIT时过于简单化。它们只允许与处于TIME_WAIT状态的连接完全匹配的套接字建立连接。这意味着,只有当客户端地址、客户端端口、服务器地址和服务器端口完全相同时,TIME_WAIT才会阻止新的出站连接。某些操作系统甚至更加严格,阻止任何包含TIME_WAIT状态的本地端口号被重新使用。如果太多的套接字进入TIME_WAIT状态,新的出站连接可能无法建立,因为没有可用的本地端口。

Windows系统的处理方式更为灵活,只阻止与TIME_WAIT状态中的连接完全匹配的出站连接。入站连接受到的影响较小,因为服务器监听的众所周知端口不会因TIME_WAIT状态而阻止新的入站连接。

尽管服务器主动关闭的连接与客户端连接在TIME_WAIT状态上是一样的,但服务器监听的本地端口不会阻止它接受新的入站连接。在Windows上,只要新连接的序列号大于当前连接的最终序列号,服务器就可以接受新的入站连接,即使该本地端口当前正处于TIME_WAIT状态。

然而,服务器上累积的TIME_WAIT连接可能会影响性能和资源使用,因为直到TIME_WAIT状态结束,这些连接仍会占用服务器资源。

为了避免因本地端口号耗尽而影响出站连接的建立,您可以确保使用了足够的临时端口范围。在Windows上,这可以通过调整MaxUserPort注册表设置来实现。

缩短套接字在TIME_WAIT状态下的时间通常不是一个好主意,因为这可能会导致更多的问题。如果您的系统在短时间内建立和关闭大量连接,调整2MSL等待周期可能会导致更多的问题,而不是解决问题。

更改2MSL延迟通常是全局性的配置更改。使用SO_REUSEADDR套接字选项可以在套接字级别解决问题,但这可能会带来安全风险,如拒绝服务攻击或数据窃取。在Windows平台上,SO_EXCLUSIVEADDRUSE套接字选项可以帮助防止这些问题,但最佳的做法是设计系统以避免TIME_WAIT成为问题。

另一种终止TCP连接的方法是通过发送RST而不是FIN来中止连接,这通常通过将SO_LINGER套接字选项设置为0来实现。这种方法会丢弃挂起的数据,并且不会进入TIME_WAIT状态,但可能会导致数据丢失。

为了避免TIME_WAIT成为问题,您可以采取以下措施:

  1. 对于不建立出站连接的服务器,除了维护连接的性能和资源意义外,不必过分担心TIME_WAIT。
  2. 对于建立出站连接并接受入站连接的服务器,最佳做法是确保TIME_WAIT状态发生在客户端而不是服务器上。这可以通过永不从服务器发起主动关闭来实现,而是在出现错误时中止连接。
  3. 在客户端,虽然TIME_WAIT状态最终会在客户端结束,但这也是有优势的。如果客户端因TIME_WAIT积累而导致连接问题,其他客户端不会受到影响。此外,设计持久连接的协议比频繁打开和关闭连接更为高效。

总之,TIME_WAIT状态的存在有其重要原因,通过缩短2MSL周期或使用SO_REUSEADDR来解决TIME_WAIT问题并不总是最佳做法。如果您能够设计协议以避免TIME_WAIT问题,那么通常可以完全避免这个问题。

TIME_WAIT的4种查询方式

1、netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

图片

2、ss -s

图片

3、netstat -nat |awk '{print $6}'|sort|uniq -c|sort -rn

4、 统计 TIME_WAIT 连接的本地地址

netstat -an | grep TIME_WAIT | awk '{print $4}' | sort | uniq -c | sort -n -k1

图片

尝试抓取 tcp 包

tcpdump tcp -i any -nn port 8080 | grep "172.11.11.11"

系统参数优化

处理方法:需要修改内核参数开启重启:

net.ipv4.tcp_tw_reuse = 1,开启快速回收
net.ipv4.tcp_tw_recycle = 1 (在NAT网络中不建议开启,要设置为0),并且开启net.ipv4.tcp_timestamps = 1以上两个参数才生产。

具体操作方法如下:

echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf; 
echo "net.ipv4.tcp_tw_recycle = 1" >> /etc/sysctl.conf;
echo "net.ipv4.tcp_timestamps = 1" >> /etc/sysctl.conf;

sysctl -p;

nginx 配置优化

当使用nginx作为反向代理时,为了支持长连接,需要做到两点:

  1. 从client到nginx的连接是长连接

  2. 从nginx到server的连接是长连接

保持和client的长连接:

http {
    keepalive_timeout 120s 120s;
    keepalive_requests 1000;
}

keepalive_timeout设置

语法

keepalive_timeout timeout [header_timeout];

第一个参数:设置keep-alive客户端(浏览器)连接在服务器端(nginx端)保持开启的超时值(默认75s);值为0会禁用keep-alive客户端连接;

第二个参数:可选、在响应的header域中设置一个值“Keep-Alive: timeout=time”;通常可以不用设置;

这个keepalive_timeout针对的是浏览器和nginx建立的一个tcp通道,没有数据传输时最长等待该时候后就关闭.

nginx和upstream中的keepalive_timeout则受到tomcat连接器的控制,tomcat中也有一个类似的keepalive_timeout参数

keepalive_requests理解设置

keepalive_requests指令用于设置一个keep-alive连接上可以服务的请求的最大数量,当最大请求数量达到时,连接被关闭。默认是100。

这个参数的真实含义,是指一个keep alive建立之后,nginx就会为这个连接设置一个计数器,记录这个keep alive的长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则nginx会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。

保持和server的长连接

为了让nginx和后端server(nginx称为upstream)之间保持长连接,典型设置如下:(默认nginx访问后端都是用的短连接(HTTP1.0),一个请求来了,Nginx 新开一个端口和后端建立连接,后端执行完毕后主动关闭该链接)

location / {
    proxy_http_version 1.1;
    proxy_set_header Connection keep-alive;
    proxy_pass http://httpurl;
}

HTTP协议中对长连接的支持是从1.1版本之后才有的,因此最好通过proxy_http_version指令设置为”1.1”;

从client过来的http header,因为即使是client和nginx之间是短连接,nginx和upstream之间也是可以开启长连接的。

keepalive理解设置

此处keepalive的含义不是开启、关闭长连接的开关;也不是用来设置超时的timeout;更不是设置长连接池最大连接数。官方解释:

  1. The connections parameter sets the maximum number of idle keepalive connections to upstream servers connections(设置到upstream服务器的空闲keepalive连接的最大数量)

  2. When this number is exceeded, the least recently used connections are closed. (当这个数量被突破时,最近使用最少的连接将被关闭)

  3. It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open.(特别提醒:keepalive指令不会限制一个nginx worker进程到upstream服务器连接的总数量)

总结:

在使用keepalive参数时,务必要谨慎配置,尤其是在处理高QPS的场景下。建议先进行一些基本的计算,根据QPS和平均响应时间,可以大致估算出所需的长连接数量。

例如,如果您的系统面临10000 QPS和100毫秒的平均响应时间,那么所需的长连接数量大约是1000个。在此基础上,可以将keepalive参数设置为长连接数量的10%到30%。如果您想要简化配置,可以直接将keepalive设置为1000左右,通常这样的设置是足够的。

以下是导致出现大量TIME_WAIT情况的原因分析:

  1. 在nginx端出现大量TIME_WAIT的情况主要有两种:

    • 如果keepalive_requests设置得太小,在高并发情况下,一旦超过这个值,nginx就会强制关闭与客户端的keepalive长连接,这会导致nginx端出现TIME_WAIT状态。
    • 如果keepalive设置得太小(即空闲连接数不足),在高并发环境下,nginx可能会频繁地关闭和重新打开与后端服务器保持的keepalive长连接,造成连接数的波动。
  2. 导致后端服务器端出现大量TIME_WAIT的情况:

    • 如果nginx没有启用与后端服务器的长连接,即没有设置proxy_http_version 1.1;proxy_set_header Connection "";,这会导致后端服务器在每次关闭连接时都处于高并发状态,从而在后端服务器端产生大量的TIME_WAIT状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值