作者:张华 发表于:2015-11-13
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明
(http://blog.csdn.net/quqi99 )
- 四层的负载均衡,就是通过发布三层的IP地址(VIP),然后加四层的端口号,来决定哪些流量需要做负载均衡,对需要处理的流量进行NAT处理,转发至后台服务器,并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理。所以四层的负载均衡器更像是一个NAT路由器。
- 七层的负载均衡,就是在四层的基础上,再考虑应用层的特征,比如同一个Web服务器的负载均衡,除了根据VIP加80端口辨别是否需要处理的流量,还可根据七层的URL、浏览器类别、语言来决定是否要进行负载均衡。负载均衡如果要真正理解七层应用层的内容,只能先代理最终的服务器和客户端建立三次握手连接后,才可能接受到客户端发送的真正应用层内容的报文,所以七层的负载均衡更像是代理服务器。
UDP通信如下图:

服务端代码示例:
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#define LOCALPORT 4567
int main(int argc,char *argv[])
{
int mysock,len;
struct sockaddr_in addr;
int i=0;
char msg[256];
int addr_len;
if (( mysock= socket(AF_INET, SOCK_DGRAM, 0) )<0)
{
perror("error");
exit(1);
}
addr_len=sizeof(struct sockaddr_in);
bzero(&addr,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_port=htons(LOCALPORT);
addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(mysock,&addr,sizeof(addr))<0)
{
perror("connect");
exit(1);
}
while (1)
{
bzero(msg,sizeof(msg));
len= recvfrom (mysock,msg,sizeof(msg),0,&addr,&addr_len);
printf("%d :",i);
i++;
printf("message from : %s \n",inet_ntoa(addr.sin_addr));
printf(" message : %s \n\n",msg);
sendto(mysock,msg,len,0,&addr,addr_len);
}
}
mysock= socket(AF_INET, SOCK_DGRAM, 0) )<0)
{
perror("error");
exit(1);
}
addr_len=sizeof(struct sockaddr_in);
bzero(&addr,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_port=htons(LOCALPORT);
addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(mysock,&addr,sizeof(addr))<0)
{
perror("connect");
exit(1);
}
while (1)
{
bzero(msg,sizeof(msg));
len= recvfrom (mysock,msg,sizeof(msg),0,&addr,&addr_len);
printf("%d :",i);
i++;
printf("message from : %s \n",inet_ntoa(addr.sin_addr));
printf(" message : %s \n\n",msg);
sendto(mysock,msg,len,0,&addr,addr_len);
}
}
客户端代码如下:
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#define REMOTEPORT 4567
#define REMOTEIP "127.0.0.1"
int main(int argc,char *argv[])
{
int s,len;
struct sockaddr_in addr;
int addr_len;
char msg[256];
int i=0;
if (( s= socket(AF_INET, SOCK_DGRAM, 0) )<0)
{
perror("error");
exit(1);
}
len=sizeof(struct sockaddr_in);
bzero(&addr,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_port=htons(REMOTEPORT);
addr.sin_addr.s_addr=inet_addr(REMOTEIP);
while (1)
{
bzero(msg,sizeof(msg));
len = read(STDIN_FILENO,msg,sizeof(msg));
sendto(s,msg,len,0,&addr,addr_len);
printf("\nInput message: %s \n",msg);
len= recvfrom (s,msg,sizeof(msg),0,&addr,&addr_len);
printf("%d :",i);
i++;
printf("Received message: %s \n",msg);
}
}
s= socket(AF_INET, SOCK_DGRAM, 0) )<0)
{
perror("error");
exit(1);
}
len=sizeof(struct sockaddr_in);
bzero(&addr,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_port=htons(REMOTEPORT);
addr.sin_addr.s_addr=inet_addr(REMOTEIP);
while (1)
{
bzero(msg,sizeof(msg));
len = read(STDIN_FILENO,msg,sizeof(msg));
sendto(s,msg,len,0,&addr,addr_len);
printf("\nInput message: %s \n",msg);
len= recvfrom (s,msg,sizeof(msg),0,&addr,&addr_len);
printf("%d :",i);
i++;
printf("Received message: %s \n",msg);
}
}
UDP无状态,服务端无记性,服务端有一堆udp server socket(构建时需四元组(协议、本地地址、本地端口、远程地址)唯一才能创建),它们以链表存在。客户端来请求后,SO_REUSEADDR是使用对端的的IP+Port来查找upd socket(显然,如果服务端在一个IP上启动了多个socket server的话,只有找到的第一个可以服务)。SO_REUSEPORT允许使用相同的IP+Port创建多个socket, 也是使用 对端的的IP+Port来查找udp socket, 但是它找到一个后不停止继续找,最后再通过源IP+Port使用Hash找到一个唯一的socket来服务,这样相当于对对端做了负载均衡。 对于客户端而言也是这样,故反之亦然。
- SO_REUSEADDR仅仅表示可以客户端socket可以重用本地地址、本地端口,整个相关五元组还是唯一确定的。在对于UDP无连接状态没有办法通过“将新的连接匹配保存下来的已有的五元组连接”来判断是否五元组唯一。于是它就是简单地基于目标IP与目标端口查找UDP socket链表结构,找到第一个就返回。SO_REUSEADDR只是重用本地IP与端口,所以你可以在在一个进程或者多个线程里绑定相同的远程IP与端口的UDP连接,但只有最后一个(或者其他固定位置,依算法而定)UDP连接可以接收到数据。
- SO_REUSEPORT, 于是,从Linux3.9内核开始有了SO_REUSEPORT这个socket选项,它支持多个客户端套接字(多个进程或者线程)绑定到同一端口, 这样每一个线程拥有了自己的服务器套接字,从而在服务器套入字上没有了锁的竞争提升了性能。它的算法在查找UDP socket链表结构时,不仅使用了目标IP与端口,这样就会找到多个,最后再通过源IP+Port Hash选择一个,这样实现了内核层面的负载均衡。这样在服务端创建多个udp socket并绑定相同的IP和端口,不同源IP与端口的客户端socket会自动负载到不同的服务端socket上去。这样,想利用多CPU的话,可以预先创建多个进程,每一个进程分配一个UDP socket来处理数据的收发(代码实现可以在主进程中预先fork多个进程,每一个进程只处理一个UDP socket)。如果有一个UDP socket死掉的话,就会改变socket链表的位置,所以办法是使用CLOEXEC参数保证UDP不被关闭。
- SO_REUSEADDR这个套接字选项通知内核,如果端口忙,但TCP状态位于TIME_WAIT,可以重用端口。这个一般用于当你的程序停止后想立即重启的时候,如果没有设定这个选项,会报错EADDRINUSE,需要等到TIME_WAIT结束才能重新绑定到同一个ip+port上。而SO_REUSEPORT用于多核环境下,允许多个线程或者进程绑定和监听同一个ip+port,无论UDP、TCP(以及TCP是什么状态)
Haproxy目前不支持上述的UDP方式在每一个进程上启动一个UDP socket来给客户端作负载均衡(后端还是代理)。corosync支持使用udpu方式作各节点间的心跳检查,但前提是配置host列表。
参考:
1, http://kb.cnblogs.com/page/188170/
2, http://blog.csdn.net/dog250/article/details/17061277