Java程序员也应该知道的一些网络知识

对于需要编写网络通信的Java程序员而言,OS/网卡/网络结构等也需要有一些了解,以确保程序运行符合期望。

Java层面本身可通过Socket API来控制一些系统层面的参数(其他的诸如Netty/Mina/Grizzly这些通信框架也都提供设置这些参数的方法),主要是下面几个:
1. setTcpNoDelay(boolean on)
在不设置的情况下,默认为false,即禁用Nagle’s algorithm,具体这个算法的细节请自行google。
从经验上来说,只有在网络通信非常大时(通常指已经到100k+/秒了),设置为false会有些许优势,因此建议大部分情况下均应设置为true。
2. setSoLinger(boolean on,int linger)
这个的作用仅为socket关闭时如发送缓冲区里还有没发送完的包,等多久后关闭,通常来说不对这个做设置。
3. setSoTimeout(int timeout)
这个的作用是在执行socket read/write等block操作时,超时的时间(单位为ms),默认为0,也就是永不超时,nio是通知机制,因此通常不设置这个参数。
4. setSendBufferSize(int size)
缓冲区的大小决定了网络通信的吞吐量,理想的计算公式为:
Throughput = buffer size / latency
例如latency为1ms,buffer size为64KB,那么Throughput = 64KB / 0.001 = 62.5MB/s = 500Mb/s,理论上跑到了千兆网卡的一半。
通常建议buffer size设置为buffer size = RTT * bandwidth,其中RT为ping的rt,bandwidth为网卡的带宽,例如ping为1ms,bandwidth为1000Mb/s,那么buffer size = 1 * 0.001 * 1000/8 * 1024 = 128KB

这个的默认值取决于os的net.ipv4.tcp_wmem中的第二个值/2,例如net.ipv4.tcp_wmem为:4096 65536 16777216,那么Java Socket默认的sendbuffersize会为65536/2 = 32768Bytes = 32KB

也可通过调用setSendBufferSize来设置,但这个不确保调用会生效(例如设置的最大值超过了os的最大值,如os同时设置了net.core.wmem_max则以这个为最大值,则会以os的最大值为准),因此最好在调用后再get下确认是否生效。

sendBufferSize如果太小,对于往外发东西的server而言,如果碰到其中某x台client接收慢,可能会很快导致send buffer满,这个时候会写不了,对于像mina/netty等框架而言,通常而言在写不了的情况下会放入队列,但不幸的是通常这都是一个没有限制大小的队列,所以有些时候在这种情况下可能会导致OOM,因此在写这块代码时要特别注意对内存的保护(宁愿写失败也不能导致OOM)。

5. setReceiveBufferSize(int size)
设置接收缓冲区的大小,和sendBufferSize的行为一致。
6. setReuseAddress(boolean on)
设置为true,即表示在连接还在timewait状态时,就可复用其端口,这个对于大量连接且经常有timewait时适用,例如短连接的http server,默认为false,因此建议显式的设置为true。

除了Socket的这些API外,对于ServerSocket,还有一个参数比较重要,不过这个参数是在构造器中传入的:
ServerSocket(int port,int backlog)
这里的backlog主要是指当ServerSocket还没执行accept时,这个时候的请求会放在os层面的一个队列里,这个队列的大小即为backlog值,这个参数对于大量连接涌入的场景非常重要,例如服务端重启,所有客户端自动重连,瞬间就会涌入很多连接,如backlog不够大的话,可能会造成客户端接到连接失败的状况,再次重连,结果就会导致服务端一直处理不过来(当然,客户端重连最好是采用类似tcp的自动退让策略),backlog的默认值为os对应的net.core.somaxconn,调整backlog队列的大小一定要确认ulimit -n中允许打开的文件数是够的。
os上还提供了net.core.netdev_max_backlog和net.ipv4.tcp_max_syn_backlog来设置全局的backlog队列大小(一个是建立连接的queue总队列的大小,一个是等到客户端ACK的SYN队列的大小)。

除了上面这些Java API层面可设置的参数外,还有一些常见的网络问题需要调整os的参数来控制,例如大量TIME_WAIT,TIME_WAIT是主动关闭连接的一端所处的状态,TIME_WAIT后需要等到2MSL(Max Segment Lifetime,linux上可通过sysctl net.ipv4.tcp_fin_timeout来查看具体的值,单位为秒)才会被彻底关闭,而处于TIME_WAIT的连接也是要占用打开的文件数的,因此如果太多的话会导致打开的文件数到达瓶颈,要避免TIME_WAIT太多,通常可以调整以下几个os参数:
net.ipv4.tcp_tw_reuse = 1 #表示可重用time_wait的socket
net.ipv4.tcp_tw_recycle = 1 #表示开启time_wait sockets的快速回收
net.ipv4.tcp_fin_timeout = 30 #表示msl的时间

如果碰到的是大量的CLOSE_WAIT则通常都是代码里的问题,就是服务器端关闭连接了,但客户端一直没关。

除了上面这些参数外,对于长连接,尤其是对于需要在断开后自动重连的长连接场景:
1、最好采用心跳机制确保连接没断开
这里有两个原因,一是有些交换机会对连接有自动断开的机制(通常不会),二是像直接拔网线等这种连接本身是不知道已经断开了的。
2、如长连接是经过了中间的负载均衡设备
那有可能会导致的现象是realserver的连接数不均衡,这种情况是长连接很悲催的场景,通常只能是采用连接每处理多少个请求就自动断开重连来缓解这个问题。

通常服务器是两块网卡或更多,网卡的bonding模式会决定能使用的带宽,因此也最好能有些了解,bonding模式可通过cat /proc/net/bonding/*bond*来查看,通常都可以在输出里看到
Bonding Mode: …
常看到的例如:
fault-tolerance (active-backup),表示的为主备模式,即只有一块网卡是活跃的,因此最大的带宽是单块网卡的带宽;
Bonding Mode: IEEE 802.3ad Dynamic link aggregation,表示的为两块网卡都启用,这种情况下通常可跑到双网卡的带宽。

最后推荐下我自己以前写nfs-rpc框架的时候的一些优化经验的总结的文章:http://bluedavy.me/?p=334

=============================
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。