java程序设计精编教程微课版,一次Dubbo拥堵的分析

for (;😉 {

for (int i = writeSpinCount; i > 0; i --) { //每次最多尝试16次

localWrittenBytes = buf.transferTo(ch);

if (localWrittenBytes != 0) {

writtenBytes += localWrittenBytes;

break;

}

if (buf.finished()) {

break;

}

}

if (buf.finished()) {

// Successful write - proceed to the next message.

buf.release();

channel.currentWriteEvent = null;

channel.currentWriteBuffer = null;

evt = null;

buf = null;

future.setSuccess();

} else {

// Not written fully - perhaps the kernel buffer is full.

//重点在这,如果写16次还没写完,可能是内核缓冲区满了,writeSuspended被设置为true

addOpWrite = true;

channel.writeSuspended = true;

}

if (open) {

if (addOpWrite) {

setOpWrite(channel);

} else if (removeOpWrite) {

clearOpWrite(channel);

}

}

}

fireWriteComplete(channel, writtenBytes);

}

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.42.43.44.45.46.47.48.49.50.51.

正常情况下,队列中的写请求要通过processWriteTaskQueue处理掉,但是这些写请求也同时注册到了selector上,如果processWriteTaskQueue写成功,就会删掉selector上的写请求。如果Socket的写缓冲区满了,对于NIO,会立刻返回,对于BIO,会一直等待。Netty使用的是NIO,它尝试16次后,还是不能写成功,它就把writeSuspended设置为true,这样接下来的所有写请求都会被跳过。那什么时候会再写呢?这时候就得靠selector了,它如果发现socket可写,就把这些数据写进去。

下面是processSelectedKeys里写的过程,因为它是发现socket可写才会写,所以直接把writeSuspended设为false。

void writeFromSelectorLoop(final SelectionKey k) {

NioSocketChannel ch = (NioSocketChannel) k.attachment();

ch.writeSuspended = false;

write0(ch);

}

1.2.3.4.5.

5.数据从消费者的socket发送缓冲区传输到提供者的接收缓冲区

================================

这个是操作系统和网卡实现的,应用层的write写成功了,并不代表对面能收到,当然tcp会通过重传能机制尽量保证对端收到。

6.服务端IO线程从缓冲区读取请求数据

===================

这个是服务端的NIO线程实现的,在processSelectedKeys中。

public void run() {

for (;😉 {

SelectorUtil.select(selector);

proce***egisterTaskQueue();

processWriteTaskQueue();

processSelectedKeys(selector.selectedKeys()); //再处理select事件,读写都可能有

}

}

private void processSelectedKeys(Set selectedKeys) throws IOException {

for (Iterator i = selectedKeys.iterator(); i.hasNext()😉 {

SelectionKey k = i.next();

i.remove();

try {

int readyOps = k.readyOps();

if ((readyOps & SelectionKey.OP_READ) != 0 || readyOps == 0) {

if (!read(k)) {

// Connection already closed - no need to handle write.

continue;

}

}

if ((readyOps & SelectionKey.OP_WRITE) != 0) {

writeFromSelectorLoop(k);

}

} catch (CancelledKeyException e) {

close(k);

}

if (cleanUpCancelledKeys()) {

break; // break the loop to avoid ConcurrentModificationException

}

}

}

private boolean read(SelectionKey k) {

// Fire the event.

fireMessageReceived(channel, buffer); //读取完后,最终会调用这个函数,发送一个收到信息的事件

}

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.42.43.44.

7.IO线程把请求交给Dubbo线程池

===================

按配置不同,走的Handler不同,配置dispatch为all,走的handler如下。下面IO线程直接交给一个ExecutorService来处理这个请求,出现了熟悉的报错“Threadpool is exhausted",业务线程池满时,如果没有队列,就会报这个错。

public class AllChannelHandler extends WrappedChannelHandler {

public void received(Channel channel, Object message) throws RemotingException {

ExecutorService cexecutor = getExecutorService();

try {

cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));

} catch (Throwable t) {

//TODO A temporary solution to the problem that the exception information can not be sent to the opposite end after the thread pool is full. Need a refactoring

//fix The thread pool is full, refuses to call, does not return, and causes the consumer to wait for time out

if(message instanceof Request && t instanceof RejectedExecutionException){

Request request = (Request)message;

if(request.isTwoWay()){

String msg = “Server side(” + url.getIp() + “,” + url.getPort() + “) threadpool is exhausted ,detail msg:” + t.getMessage();

Response response = new Response(request.getId(), request.getVersion());

response.setStatus(Response.SERVER_THREADPOOL_EXHAUSTED_ERROR);

response.setErrorMessage(msg);

channel.send(response);

return;

}

}

throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);

}

}

}

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.

8.服务端Dubbo线程池处理完请求后,把返回报文放入队列

=============================

线程池会调起下面的函数

public class HeaderExchangeHandler implements ChannelHandlerDelegate {

Response handleRequest(ExchangeChannel channel, Request req) throws RemotingException {

Response res = new Response(req.getId(), req.getVersion());

// find handler by message class.

Object msg = req.getData();

try {

// handle data.

Object result = handler.reply(channel, msg); //真正的业务逻辑类

res.setStatus(Response.OK);

res.setResult(result);

} catch (Throwable e) {

res.setStatus(Response.SERVICE_ERROR);

res.setErrorMessage(StringUtils.toString(e));

}

return res;

}

public void received(Channel channel, Object message) throws RemotingException {

if (message instanceof Request) {

// handle request.

Request request = (Request) message;

if (request.isTwoWay()) {

Response response = handleRequest(exchangeChannel, request); //处理业务逻辑,得到一个Response

channel.send(response); //回写response

}

}

}

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.

channel.send(response)最终调用了NioServerSocketPipelineSink里的方法把返回报文放入队列。

9.服务端IO线程从队列中取出数据

=================

与流程3一样

10.服务端IO线程把回复数据写入Socket发送缓冲区

============================

IO线程写数据的时候,写入到TCP缓冲区就算成功了。但是如果缓冲区满了,会写不进去。对于阻塞和非阻塞IO,返回结果不一样,阻塞IO会一直等,而非阻塞IO会立刻失败,让调用者选择策略。

Netty的策略是尝试最多写16次,如果不成功,则暂时停掉IO线程的写操作,等待连接可写时再写,writeSpinCount默认是16,可以通过参数调整。

for (int i = writeSpinCount; i > 0; i --) {

localWrittenBytes = buf.transferTo(ch);

if (localWrittenBytes != 0) {

writtenBytes += localWrittenBytes;

break;

}

if (buf.finished()) {

break;

}

}

if (buf.finished()) {

// Successful write - proceed to the next message.

buf.release();

channel.currentWriteEvent = null;

channel.currentWriteBuffer = null;

evt = null;

buf = null;

future.setSuccess();

} else {

// Not written fully - perhaps the kernel buffer is full.

addOpWrite = true;

channel.writeSuspended = true;

1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.

11.数据传输

=======

数据在网络上传输主要取决于带宽和网络环境。

12.客户端IO线程把数据从缓冲区读出

===================

这个过程跟流程6是一样的

13.IO线程把数据交给Dubbo业务线程池

======================

这一步与流程7是一样的,这个线程池名字为DubboClientHandler。

14.业务线程池根据消息ID通知主线程

===================

先通过HeaderExchangeHandler的received函数得知是Response,然后调用handleResponse,

public class HeaderExchangeHandler implements ChannelHandlerDelegate {

static void handleResponse(Channel channel, Response response) throws RemotingException {

if (response != null && !response.isHeartbeat()) {

DefaultFuture.received(channel, response);

}

}

public void received(Channel channel, Object message) throws RemotingException {

if (message instanceof Response) {

handleResponse(channel, (Response) message);

}

}

1.2.3.4.5.6.7.8.9.10.11.12.13.

DefaultFuture根据ID获取Future,通知调用线程

public static void received(Channel channel, Response response) {

DefaultFuture future = FUTURES.remove(response.getId());

if (future != null) {

future.doReceived(response);

}

}

1.2.3.4.5.6.7.8.

至此,主线程获取了返回数据,调用结束。

三、影响上述流程的关键参数

=============

协议参数

====

我们在使用Dubbo时,需要在服务端配置协议,例如

<dubbo:protocol name=“dubbo” port=“20880” dispatcher=“all” threadpool=“fixed” threads=“2000” />

下面是协议中与性能相关的一些参数,在我们的使用场景中,线程池选用了fixed,大小是500,队列为0,其他都是默认值。

属性

对应URL参数

类型

是否必填

缺省值

作用

描述

name

<protocol>

string

必填

dubbo

性能调优

协议名称

threadpool

threadpool

string

可选

fixed

性能调优

线程池类型,可选:fixed/cached。

threads

threads

int

可选

200

性能调优

服务线程池大小(固定大小)

queues

queues

int

可选

0

性能调优

线程池队列大小,当线程池满时,排队等待执行的队列大小,建议不要设置,当线程池满时应立即失败,重试其它服务提供机器,而不是排队,除非有特殊需求。

iothreads

iothreads

int

可选

cpu个数+1

性能调优

io线程池大小(固定大小)

accepts

accepts

int

可选

0

性能调优

服务提供方最大可接受连接数,这个是整个服务端可以建的最大连接数,比如设置成2000,如果已经建立了2000个连接,新来的会被拒绝,是为了保护服务提供方。

dispatcher

dispatcher

string

可选

dubbo协议缺省为all

性能调优

协议的消息派发方式,用于指定线程模型,比如:dubbo协议的all, direct, message, execution, connection等。 这个主要牵涉到IO线程池和业务线程池的分工问题,一般情况下,让业务线程池处理建立连接、心跳等,不会有太大影响。

payload

payload

int

可选

8388608(=8M)

性能调优

请求及响应数据包大小限制,单位:字节。 这个是单个报文允许的最大长度,Dubbo不适合报文很长的请求,所以加了限制。

buffer

buffer

int

可选

8192

性能调优

网络读写缓冲区大小。注意这个不是TCP缓冲区,这个是在读写网络报文时,应用层的Buffer。

codec

codec

string

可选

dubbo

性能调优

协议编码方式

serialization

serialization

string

可选

dubbo协议缺省为hessian2,rmi协议缺省为java,http协议缺省为json

性能调优

协议序列化方式,当协议支持多种序列化方式时使用,比如:dubbo协议的dubbo,hessian2,java,compactedjava,以及http协议的json等

transporter

transporter

string

可选

dubbo协议缺省为netty

性能调优

协议的服务端和客户端实现类型,比如:dubbo协议的mina,netty等,可以分拆为server和client配置

server

server

string

可选

dubbo协议缺省为netty,http协议缺省为servlet

性能调优

协议的服务器端实现类型,比如:dubbo协议的mina,netty等,http协议的jetty,servlet等

client

client

string

可选

dubbo协议缺省为netty

性能调优

协议的客户端实现类型,比如:dubbo协议的mina,netty等

charset

charset

string

可选

UTF-8

性能调优

序列化编码

heartbeat

heartbeat

int

可选

0

性能调优

心跳间隔,对于长连接,当物理层断开时,比如拔网线,TCP的FIN消息来不及发送,对方收不到断开事件,此时需要心跳来帮助检查连接是否已断开

服务参数

====

针对每个Dubbo服务,都会有一个配置,全部的参数配置在这:http://dubbo.apache.org/zh-cn/docs/user/references/xml/dubbo-service.html。

我们关注几个与性能相关的。在我们的使用场景中,重试次数设置成了0,集群方式用的failfast,其他是默认值。

属性

对应URL参数

类型

是否必填

缺省值

作用

描述

兼容性

delay

delay

int

可选

0

性能调优

延迟注册服务时间(毫秒) ,设为-1时,表示延迟到Spring容器初始化完成时暴露服务

1.0.14以上版本

timeout

timeout

int

可选

1000

性能调优

远程服务调用超时时间(毫秒)

2.0.0以上版本

retries

retries

int

可选

2

性能调优

远程服务调用重试次数,不包括第一次调用,不需要重试请设为0

2.0.0以上版本

connections

connections

int

可选

1

性能调优

对每个提供者的最大连接数,rmi、http、hessian等短连接协议表示限制连接数,dubbo等长连接协表示建立的长连接个数

2.0.0以上版本

loadbalance

loadbalance

string

可选

random

性能调优

负载均衡策略,可选值:random,roundrobin,leastactive,分别表示:随机,轮询,最少活跃调用

2.0.0以上版本

async

async

boolean

可选

false

性能调优

是否缺省异步执行,不可靠异步,只是忽略返回值,不阻塞执行线程

2.0.0以上版本

weight

weight

int

可选

性能调优

服务权重

2.0.5以上版本

executes

executes

int

可选

0

性能调优

服务提供者每服务每方法最大可并行执行请求数

2.0.5以上版本

proxy

proxy

string

可选

javassist

性能调优

生成动态代理方式,可选:jdk/javassist

2.0.5以上版本

cluster

cluster

string

可选

failover

性能调优

集群方式,可选:
failover/failfast/failsafe/failback/forking

2.0.5以上版本

这次拥堵的主要原因,应该就是服务的connections设置的太小,dubbo不提供全局的连接数配置,只能针对某一个交易做个性化的连接数配置。

四、连接数与Socket缓冲区对性能影响的实验

=======================

通过简单的Dubbo服务,验证一下连接数与缓冲区大小对传输性能的影响。

我们可以通过修改系统参数,调节TCP缓冲区的大小。

在 /etc/sysctl.conf 修改如下内容, tcp_rmem是发送缓冲区,tcp_wmem是接收缓冲区,三个数值表示最小值,默认值和最大值,我们可以都设置成一样。

net.ipv4.tcp_rmem = 4096 873800 16777216

net.ipv4.tcp_wmem = 4096 873800 16777216

1.2.

然后执行sysctl –p 使之生效。

服务端代码如下,接受一个报文,然后返回两倍的报文长度,随机sleep 0-300ms,所以均值应该是150ms。服务端每10s打印一次tps和响应时间,这里的tps是指完成函数调用的tps,而不涉及传输,响应时间也是这个函数的时间。

//服务端实现

public String sayHello(String name) {

counter.getAndIncrement();

long start = System.currentTimeMillis();

try {

Thread.sleep(rand.nextInt(300));

} catch (InterruptedException e) {

}

String result = "Hello " + name + name + ", response form provider: " + RpcContext.getContext().getLocalAddress();

long end = System.currentTimeMillis();

timer.getAndAdd(end-start);

return result;

}

1.2.3.4.5.6.7.8.9.10.11.12.13.

客户端起N个线程,每个线程不停的调用Dubbo服务,每10s打印一次qps和响应时间,这个qps和响应时间是包含了网络传输时间的。

for(int i = 0; i < N; i ++) {

threads[i] = new Thread(new Runnable() {

@Override

public void run() {

while(true) {

Long start = System.currentTimeMillis();

String hello = service.sayHello(z);

Long end = System.currentTimeMillis();

totalTime.getAndAdd(end-start);

counter.getAndIncrement();

}

}});

threads[i].start();

}

1.2.3.4.5.6.7.8.9.10.11.12.13.14.

通过ss -it命令可以看当前tcp socket的详细信息,包含待对端回复ack的数据Send-Q,最大窗口cwnd,rtt(round trip time)等。

(base) niuxinli@ubuntu:~$ ss -it

State Recv-Q Send-Q Local Address:Port Peer Address:Port

ESTAB 0 36 192.168.1.7:ssh 192.168.1.4:58931

cubic wscale:8,2 rto:236 rtt:33.837/8.625 ato:40 mss:1460 pmtu:1500 rcvmss:1460 advmss:1460 cwnd:10 bytes_acked:559805 bytes_received:54694 segs_out:2754 segs_in:2971 data_segs_out:2299 data_segs_in:1398 send 3.5Mbps pacing_rate 6.9Mbps delivery_rate 44.8Mbps busy:36820ms unacked:1 rcv_rtt:513649 rcv_space:16130 rcv_ssthresh:14924 minrtt:0.112

ESTAB 0 0 192.168.1.7:36666 192.168.1.7:2181

cubic wscale:7,7 rto:204 rtt:0.273/0.04 ato:40 mss:33344 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_acked:2781 bytes_received:3941 segs_out:332 segs_in:170 data_segs_out:165 data_segs_in:165 send 9771.1Mbps lastsnd:4960 lastrcv:4960 lastack:4960 pacing_rate 19497.6Mbps delivery_rate 7621.5Mbps app_limited busy:60ms rcv_space:65535 rcv_ssthresh:66607 minrtt:0.035

ESTAB 0 27474 192.168.1.7:20880 192.168.1.5:60760

cubic wscale:7,7 rto:204 rtt:1.277/0.239 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:625 ssthresh:20 bytes_acked:96432644704 bytes_received:49286576300 segs_out:68505947 segs_in:36666870 data_segs_out:67058676 data_segs_in:35833689 send 5669.5Mbps pacing_rate 6801.4Mbps delivery_rate 627.4Mbps app_limited busy:1340536ms rwnd_limited:400372ms(29.9%) sndbuf_limited:433724ms(32.4%) unacked:70 retrans:0/5 rcv_rtt:1.308 rcv_space:336692 rcv_ssthresh:2095692 notsent:6638 minrtt:0.097

1.2.3.4.5.6.7.8.9.

通过netstat -nat也能查看当前tcp socket的一些信息,比如Recv-Q, Send-Q。

(base) niuxinli@ubuntu:~$ netstat -nat

Active Internet connections (servers and established)

Proto Recv-Q Send-Q Local Address Foreign Address State

tcp 0 0 0.0.0.0:20880 0.0.0.0:* LISTEN

tcp 0 36 192.168.1.7:22 192.168.1.4:58931 ESTABLISHED

tcp 0 0 192.168.1.7:36666 192.168.1.7:2181 ESTABLISHED

tcp 0 65160 192.168.1.7:20880 192.168.1.5:60760 ESTABLISHED

1.2.3.4.5.6.7.

可以看以下Recv-Q和Send-Q的具体含义:

Recv-Q

Established: The count of bytes not copied by the user program connected to this socket.

Send-Q

Established: The count of bytes not acknowledged by the remote host.

1.2.3.4.5.

Recv-Q是已经到了接受缓冲区,但是还没被应用代码读走的数据。Send-Q是已经到了发送缓冲区,但是对方还没有回复Ack的数据。这两种数据正常一般不会堆积,如果堆积了,可能就有问题了。

第一组实验:单连接,改变TCP缓冲区

==================

结果:

角色

Socket缓冲区

响应时间

服务端

32k/32k

150ms

客户端(800并发)

32k/32k

430ms

客户端(1并发)

32k/32k

150ms

继续调大缓冲区

角色

Socket缓冲区

响应时间

CPU

服务端

64k/64k

150ms

user 2%, sys 9%

客户端(800并发)

64k/64k

275ms

user 4%, sys 13%

客户端(1并发)

64k/64k

150ms

user 4%, sys 13%

我们用netstat或者ss命令可以看到当前的socket情况,下面的第二列是Send-Q大小,是写入缓冲区还没有被对端确认的数据,发送缓冲区最大时64k左右,说明缓冲区不够用。

一次Dubbo拥堵的分析

继续增大缓冲区,到4M,我们可以看到,响应时间进一步下降,但是还是在传输上浪费了不少时间,因为服务端应用层没有压力。

角色

Socket缓冲区

响应时间

CPU

服务端

4M/4M

150ms

user 4%, sys 10%

客户端(800并发)

4M/4M

210ms

user 10%, sys 12%

客户端(1并发)

4M/4M

150ms

user 10%, sys 12%

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

给大家送一个小福利

附高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、准备面试跳槽、自身职业规划迷茫的朋友们。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-EH6QuwoI-1713452689140)]

[外链图片转存中…(img-CtiL3pn9-1713452689143)]

[外链图片转存中…(img-w2sA3WhS-1713452689144)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

给大家送一个小福利

[外链图片转存中…(img-Cr3YHF7U-1713452689145)]

附高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、准备面试跳槽、自身职业规划迷茫的朋友们。

[外链图片转存中…(img-LefWt3UP-1713452689147)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 26
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值