庐陵才子

Tan RD


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

Spring Cloud Gateway学习笔记

发表于 2020-06-27 | 分类于 学习笔记

前言

Spring Cloud Gateway 版本 Hoxton.SR6。

路由

Route类是最终执行依赖的类,RouteDefine类是路由规则定义的类。Spring Cloud Gateway会将RouteDefine转换成Route以供程序使用。RouteDefineLocator接口定义了RouteDefine类的来源。
PropertiesRouteDefinitionLocator解析GatewayProperties中的路由定义,也就是yml配置中的路由定义。RouteDefinitionRepository接口继承RouteDefinitionLocator和RouteDefinitionWriter,RouteDefinitionWriter接口中有两个方法:增加和删除路由定义。InMemoryRouteDefinitionRepository路由定义来源于内存,可以通过actuator新增删除路由。
Package route.png

ribbon的listOfServers

在zuul网关中,我们可以使用ribbon来做客户端负载均衡。假如后端服务列表是固定的,不需要采用服务注册与发现组件,那么我们就可以在ribbon中给service配置listOfServers属性。

1
2
3
service:
ribbon:
listOfServers: localhost:8080,localhost:8081

当我们使用Spring Cloud Gateway的Spring Cloud loadbalancer来做负载均衡时,我们同样可以脱离服务注册与发现组件,将静态服务列表配置到properties文件中:

1
2
3
4
spring.cloud.discovery.client.simple.instances.service1[0].uri=http://s11:8080
spring.cloud.discovery.client.simple.instances.service1[1].uri=https://s12:8443
spring.cloud.discovery.client.simple.instances.service2[0].uri=https://s21:8080
spring.cloud.discovery.client.simple.instances.service2[1].uri=https://s22:443

这种方式是采用的SimpleDiscoveryClient做服务发现客户端。

后端实例健康检查

在ribbon中,我们可以使用ServerStats来维护后端状态,负载均衡器在选择后端实例的时候可以根据状态来实现断路操作。同样,在SpringCloud loadbalancer中,可以通过HealthCheckServiceInstanceListSupplier来维护后端实例的状态。
采用HealthCheckServiceInstanceListSupplier的方式,特别适用SimpleDiscoveryClient这种将后台服务地址固定写在properties文件中的情况。因为如果采用外部服务发现的情况下,例如Eureka、Nacos、Consul等,是可以剔除失效服务的。
在LoadBalancerClientConfiguration类中中可以看到如下配置:

1
2
3
4
5
6
7
8
9
10
@Bean
@ConditionalOnBean({ReactiveDiscoveryClient.class})
@ConditionalOnMissingBean
@ConditionalOnProperty(
value = {"spring.cloud.loadbalancer.configurations"},
havingValue = "health-check"
)
public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceListSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withDiscoveryClient().withHealthChecks().withCaching().build(context);
}

Kubernetes学习笔记

发表于 2020-06-14 | 分类于 学习笔记

Pod

1、Pod中包含一个或者多个容器,Docker是容器中最常用的运行时环境。
2、Pod最常见的情况是只包含一个容器应用,但是也可以包含多个容器应用,例如sidecar模式。

Service

1、可以使用ClusterIP类型,暴露集群内部IP给集群内部服务使用。
2、可以使用NodePort类型,暴露集群外部IP给外部用户使用。

响应式编程中Mono和Flux的区别

发表于 2020-06-10 | 分类于 Java

前言

当我们在使用Project Reactor,或者使用依赖于它的框架的时候。例如spring webflux,spring cloud gateway等,经常会用看到代码中有Mono和Flux两个术语。

响应式流

Reactor是由Pivotal公司开发的开源框架,它是开发响应式应用的基础。如今,它是建立在由Netflix、Pivotal和Lightbend的工程师以及其他大型JAVA玩家(Oracle和Red Hat)提出的联合倡议(reactive streams initiative)的基础上。
在这个倡议中,响应式流的规范被创建,有以下几个关键因素:

  • 响应式流应该是非阻塞的,
  • 它是一种数据流,
  • 它是异步工作的,
  • 它需要能够处理背压。

制定以上标准的原因是:当我们通常写应用程序的时候,我们会进行数据库调用、HTTP调用,我们发起请求,阻塞线程,直到有响应返回,然后继续。虽然这种方式可以工作,但是这是一种资源浪费。

发布者Publisher

在开发过程中,不再返回简单的POJO对象,而必须返回其他内容,在结果可用的时候返回。在响应式流的规范中,被称为发布者(Publisher)。发布者有一个subcribe()方法,该方法允许使用者在POJO可用时获取它。发布者可以通过以下两种形式返回结果:

  • Mono返回0个或1个结果,
  • Flux返回0个或多个结果,可能是无限个。

Mono

Mono是Publisher的一种,返回0或者1个结果,也可以返回一个Optional。
举个例子,我们有如下代码:

1
2
3
4
public Person findCurrentUser() {
if (isAuthenticated()) return new Person("Jane", "Doe");
else return null;
}

在Java8中,我们可以这样写:

1
2
3
4
public Optional<Person> findCurrentUser() {
if (isAuthenticated()) return Optional.of(new Person("Jane", "Doe"));
else return Optional.empty();
}

阅读全文 »

正则表达式导致CPU100%

发表于 2020-06-08 | 分类于 生产问题

前言

线上机器cpu百分百,通过dump分析,发现问题出在一个正则表达式匹配方法。^([A-Za-z]+\s*)+$

原因

正则表达式有三种匹配模式:贪婪模式、懒惰模式、独占模式。有个很好用的正则表达式网站:https://regex101.com/,我们先看几个示例,再来分析这几种模式。
^([A-Za-z]+\s*)+$ 这个正则表达式是匹配类似这种字符串Aa bB cB。
^匹配开头;[A-Za-z]+匹配多个英文字母;\s*匹配多个空格;+匹配多个前面括号中的正则字符串格式。

当字符串是a1时候,执行了8步。

WechatIMG272.png

阅读全文 »

TIME_WAIT状态下连接复用导致的问题

发表于 2020-05-28 | 分类于 生产问题

现象

生产环境,某个片区应用对接的一个后台服务出现大批量延迟。

背景

应用与后台服务之间经过F5负载均衡器, 通过HTTP方式通信。我们知道TCP在断开连接的时候,会有四次通信,发起方在最后会有2MSL的等待时间,进入TIME_WAIT状态。TIME_WAIT状态存在理由:
1)可靠的实现TCP全双工连接的终止。在进行关闭连接四次挥手协议时,最后的ACK是由主动关闭端发出的。如果这个最终的ACK丢失,另一方将重发最终的FIN。因此主动发起方必须维护状态信息允许它自身重发最终的ACK。假如不维持这个状态信息,那么发起方将响应RST分节,另一方将此分节解释成一个错误(java中将会抛出connection reset的SocketException)。
因此,要实现TCP全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭的一方必须维持状态信息进入TIME_WAIT状态。
2)允许老的重复分节在网络中消逝。TCP分节可能由于路由器异常而”迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个原来的迷途分节就称为lost duplicate。
在关闭这个TCP连接后,马上又重新建立起一个相同IP地址和端口之间的TCP链接,后一个连接 被称为前一个连接的化身(incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。
为了避免这个情况,TCP不允许处于TIME_WAIT状态的连接启动一个新的化身,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个TCP连接时候,来自连接先前化身的重复分组已经在网络中消逝。
TIME_WAIT状态下连接复用:

net.ipv4.tcp_tw_reuse=1,

也就是说一个TIME_WAIT状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用,需要满足以下两点的其中之一:

1.初始序列号比TIME_WAIT老连接的末序列号大。
2.如果使用了时间戳,那么新到来的连接的时间戳比旧连接的时间戳大。

原因

1、F5负载均衡器与后端服务通信的时候,未设置IP透传,采用F5本身IP与后端服务通信。
2、前台应用在采用HTTP通信的时候,修改了keepalive,将默认的长连接改成了短连接。
3、导致F5与后端服务通信的时候,经常进入TIME_WAIT状态,而后台服务设置了连接复用。
4、由于前台应用集群部署,不同机器请求到达F5的时间戳不一致,而F5透传时间戳,未进行修改。

解决

F5负载均衡器修改每次请求的时间戳,按递增顺序设置,满足后台服务连接复用。

DebugDiag分析dump报错解决方案

发表于 2020-04-26 | 分类于 学习笔记

报错

当生产机器IIS出现cpu或者内存故障的时候,我们可以在任务管理器中将w3wp.exe右键转储成dump文件,然后使用dump分析工具进行分析。常用的dump分析工具有debugDiag和windbg,当我们在使用debugdiag诊断工具进行dump分析的时候,可能会遇到报错“Failed to load dac: c:\symbols\mscordacwks_Amd64_Amd64_***.dll”。这将导致我们不能正确进行dump分析,在Performance Summary中会出现”kernal32!BaseThreadInitThunk+13”、”ntdll!RtlUserThreadStart+21”、”clr!PreBindAssemblyEx+9dbf”等错误。
出现这个问题的原因在于mscordacwks.dll与生产机器dump时候的版本不一致。

解决

1、打开本机文件目录C:\Windows\Microsoft.NET\Framework64\v4.0.30319,不同的.net framework,对应的版本号不同,找到mscordacwks.dll文件。
2、将上述文件拷贝到指定目录中,可以是任意目录。我这里拷贝到了C:\mysymbols目录中,并将该文件重命名为报错的dll版本号,例如mscordacwks_Amd64_Amd64_4.7.3468.00.dll或者mscordacwks_Amd64_Amd64_4.7.3163.00.dll。
3、打开DebugDiag Analysis,点击左上角的设置按钮,在”Symbol search paths to use for Analysis”选项中,添加上一步中的路径(C:\mysymbols),并将”_NT_SYMBOL_PATH”和”Microsoft Public Symbol Servers”的√取消选择。
4、返回,Start Analysis开始分析。

从代码视角看IO方式演进

发表于 2020-04-10 | 分类于 Java

前言

IO是输入输出,写代码离不开IO,磁盘IO、网络IO。常见的网络IO模型有BIO、NIO、AIO三种。下面以代码视角描述Java中的IO方式的演进,以及介绍同步、异步、阻塞、非阻塞的概念。

BIO

最简单的Socket通信模式就是服务端构建一个ServerSocket类,调用accept方法等待客户端连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BIOServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("Waiting for client...");
//执行到accept开始阻塞等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("Client connected.");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("Server received:");
String line;
while ((line = bufferedReader.readLine()) != null) {
//不断输出用户传输的数据
System.out.println(line);
//收到客户端传输的exit则退出
if (line.equals("exit")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BIOClient {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8888);
String input;
while (true) {
input = (new BufferedReader(new InputStreamReader(System.in))).readLine();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write(input + "\n");
writer.flush();
}


} catch (IOException e) {
e.printStackTrace();
}
}
}

这种方式,服务端只能处理客户端的一次连接,当客户端输入exit结束连接后,服务端也将退出程序。

为了解决上述问题,我们可以使用while循环,让服务端不断的接受客户端连接,在处理完一个客户端的连接时,可以马上等待接收处理下一个客户端的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class BIOServer2 {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
System.out.println("Waiting for client...");
//执行到accept开始阻塞等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("Client connected.");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("Server received:");
String line;
while ((line = bufferedReader.readLine()) != null) {
//不断输出用户传输的数据
System.out.println(line);
//收到客户端传输的exit则退出
if (line.equals("exit")) {
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

同样,这种方式也存在一个问题,服务器一次只能接收一个客户端的连接,要想处理下一个客户端连接的时候,必须处理完毕当前客户端的请求。也就是说服务器支持的最大并发数为1,一次只能处理一个连接。

在这里我们可以采用开启线程的方式来处理连接建立后的业务逻辑,while循环不断accept客户端连接,连接到来之后的处理交给线程去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class BIOServer3 {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
System.out.println("Waiting for client...");
//执行到accept开始阻塞等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("Client connected.");
new Thread(() -> {
BufferedReader bufferedReader;
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("Server received:");
String line;
while ((line = bufferedReader.readLine()) != null) {
//不断输出用户传输的数据
System.out.println(line);
//收到客户端传输的exit则退出
if (line.equals("exit")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

这种方式虽然可以大大提高吞吐量,但是每一个客户端连接都需要创建线程来处理,非常消耗系统资源。即使我们将new Thread改成线程池ThreadPoolExecutor来替换,也有可能被阻塞任务积压导致资源耗尽。

NIO

NIO是多路复用IO,对比BIO需要采用多线程来实现高吞吐而言,NIO只需要一个线程就能处理成百上千个Socket连接。Socket从本质上讲,其实是一个文件,通过文件描述符FD,我们可以知道某个Socket的状态,例如连接是否完成,数据是否准备好。我们只需要使用一个线程来遍历所有Socket的状态,即可对Socket进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class NIOServer {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8888));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//等待SelectionKey事件到来
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//删除key,否则下一次还将会读取到
iterator.remove();
SelectableChannel channel = selectionKey.channel();
if (selectionKey.isValid() && selectionKey.isAcceptable()) {
ServerSocketChannel serSocketChannel = (ServerSocketChannel) channel;
SocketChannel socketChannel = serSocketChannel.accept();
socketChannel.configureBlocking(false);
//取到了客户端的socketChannel后,注册读取事件到Selector
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isValid() && selectionKey.isReadable()) {
//可读SelectionKey到达,读取数据
SocketChannel socketChannel = (SocketChannel) channel;
ByteBuffer buffer = ByteBuffer.allocate(128);
socketChannel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data);
System.out.println(msg);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}

}
}

Java原生的NIO写法比较混乱,不够逼格,我们可以使用Netty来构建高性能的Socket服务器。

同步与异步

举两个例子,1、前端Html页面中发起Ajax请求,在Ajax的回调函数中执行页面局部刷新;2、客户端控件设置点击事件,setOnClickListener(),在用户点击按钮后执行一些业务逻辑。这两种情况,主线程发起Ajax后,或者设置点击事件后,主线程可以无需关注后续操作,回调函数中的操作交给异步线程处理。这种采用观察者设计模式的回调方式,就叫做异步。而同步,则是指主线程需要处理后续操作,要么采用阻塞方式等着处理,要么采用轮询方式判断是否可以处理。同步的这两种情况我们叫做同步阻塞IO和同步非阻塞IO。

同步阻塞IO

先看BIOServer这个类,有两个地方涉及到阻塞,一个是serverSocket.accept()方法等待客户端连接的时候,一个是bufferedReader.readLine()方法等待客户端数据的时候。显而易见,阻塞的意思就是指程序执行到这个位置后,停止不动了,需要等待相关资源。

同步非阻塞IO

再看NIOServer这个类,只需要启动一个while循环,不断从Selector中筛选所有Socket的状态,应用线程可以在Socket连接建立后、缓冲区可读后进行处理,而无需阻塞等待。只有selector.select()获取不到数据的时候才被阻塞。

其他

1、NIO多路复用中的Selector扫描各个Socket状态,在select、poll中采用的内核线程轮询所有Socket的方式,而epoll采用的则是事件驱动的中断方式,缓冲区中有数据、则给CPU发出一个中断信号。
2、Tomcat底层可以采用NIO的多路复用,数据到来后,采用的是线程池的方式处理HttpServlet。Servlet的标准规范中,read body blocking,但是read head non-blocking。其实就是采用NIO读取到head头后,将socket交给线程处理,而线程中读取body数据是阻塞的。

字节数组Base64和16进制编码对比

发表于 2020-03-30 | 分类于 学习笔记

前言

最近在开发的一个项目涉及到国密改造。国密算法有多种,常用的有SM2、SM3、SM4,分别对应非对称加密(RSA等)、消息摘要(MD5、SHA等)、对称加密(AES、DES、3DES等)。
底层国密类库,都是采用byte数组进行加密换算,而在应用层,通常需要将byte数组进行转换,以便将数据进行传输和处理。

问题

将byte数组进行不同编码格式转换,会存在多种问题。例如:

  • 将byte数组转成ASCII编码,必须确保byte数组的值在ASCII的可视字符范围,否则会出现乱码,因为ASCII的取值范围比byte小(0~255)。
  • 将byte数组转成UTF8等其他类型编码,也有可能导致转换后的String长度与byte数组长度不同,因为不同的编码格式,占用的byte个数不同。
  • 可以将byte数组转成Base64编码。
  • 可以将byte数组转成Hex的16进制字符串,一个byte占8位,正好对应两个16进制的字符。

后面两种方式在密码学中比较常用,能很好的解决byte数组转换过程中的乱码问题。然而后两种方式也有需要注意的地方,例如:
【1】Base64编码后会在结果中出现+号,如果直接拼接在url后面进行get请求,接收方在解码的时候,很有可能会被自动转换成空格,导致错误结果。+号变空格的解决方案有两种,第一种采用post方式传输,因为body内容通常都会进行application/x-www-form-urlencoded;第二种主动调用urlencode方法对字符串进行url编码,再使用get请求。
【2】将byte数组转成16进制字符串的时候,得到的结果会比Base64编码长,在传输过程中数据量更大。因为传输过程通常都是将string转成utf8传输,不同长度的string,对应的utf8字节数组长度当然不同。

1
BMtED46hiTpdTKirGEiLDCexCOy/Ztqk06L7vwuGkIEnaP0u00QF1XchLlztdb8khitlwzYdTTAQ/Ng01fJN3UEBib1LsvLO6Muk4zCxCDU/vP9KkiFVXOaqP/df3l8ar085uiLjRXj27iUrnvx4

1
04CB440F8EA1893A5D4CA8AB18488B0C27B108ECBF66DAA4D3A2FBBF0B8690812768FD2ED34405D577212E5CED75BF24862B65C3361D4D3010FCD834D5F24DDD410189BD4BB2F2CEE8CBA4E330B108353FBCFF4A9221555CE6AA3FF75FDE5F1AAF4F39BA22E34578F6EE252B9EFC78

总结

1、同样长度的byte数组转换,Base64比HexString更短,传输过程更有优势。
2、HexString不需要处理特殊字符,更加方便。
只要处理好了Base64过程中+号可能变空格的问题,使用Base64进行通信,比采用16进制的String更优。

线上日志写磁盘延时问题

发表于 2020-03-25 | 分类于 学习笔记

前言

线上环境,在高并发场景遇到日志写本地磁盘延迟的情况。例如当前时间17:00,日志最近一条记录是14:00,延迟多个小时,但是日志数量不会发生丢失。且发现内存占用达到10几G,比其他正常机器高出很多倍。

过程

应用程序在内存中维护一个队列,当用户请求到达,产生用户日志的时候,会将用户日志入队列。同时应用会启一个异步线程,不断的轮询队列,将队列的队头元素出队列,调用IO函数写入磁盘。

改造前
1
2
3
4
5
6
7
8
9
public class AsyncThread{
public void doWork(){
while(queue.Count>0){
String log = queue.dequeue();
IOUtil.WriteLine(log);
IOUtil.flush();
}
}
}

分析

通过JMeter在本地环境模拟高并发情况,复现了该问题。仔细分析原因,发现问题出在异步线程轮训的过程中,会不断调用IO函数,而该IO函数会调用flush刷新缓冲区数据,且每次都只有队头元素这一个日志写入磁盘,这就导致了频繁的上下文切换,设置的缓冲区无效,每次只有一行日志写入缓冲区,并flush刷入磁盘。使得日志入队列的速度远远大于日志写入磁盘的速度。日志不断的在队列中积压,这就导致了机器内存的不断上升。

解决

异步线程在轮训的过程中,每次都将队列所有元素取出,然后再调用IO函数进行日志写入。该线上问题,也说明CPU的上下文切换是非常耗时的,IO缓冲区的存在也是非常有必要的。

改造后
1
2
3
4
5
6
7
8
9
public class AsyncThread{
public void doWork(){
while(queue.Count>0){
String log = queue.dequeue();
IOUtil.WriteLine(log);
}
IOUtil.flush();
}
}

Java8中的Stream流操作性能较慢

发表于 2020-03-18 | 分类于 Java

笔记

采用List的steam().toArray,比直接for循环性能差很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test{
public static void main(String[] args){

LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}

long t1 = System.currentTimeMillis();
list.stream().toArray(Integer[]::new);
long t2 = System.currentTimeMillis();
System.out.println(t2 - t1);

long t3 = System.currentTimeMillis();
Integer[] res = new Integer[list.size()];
int j = 0;
for (Integer i : list) {
res[j++] = i;
}
long t4 = System.currentTimeMillis();
System.out.println(t4 - t3);

}
}

result

12…5
wpstan

wpstan

42 文章
12 分类
58 标签
© 2020 wpstan