作为一个分布式的服务框架,服务器的负载均衡,将是一个很重要的性能指标,将能够最大限度的利用多个服务器资源,为服务的高性能,高可扩展性提供最直接的有力支持。在这篇文章中,我们就来看看Gaea是如何做到负载均衡,如何能够通过简单的添加机器,解决系统问题。
1 服务器类Server
首先所有的server是被放入到一个List中的。
public List<Server> GetAllServer() {
return ServerPool;
}
在这个Server中,Gaea除了封装了服务器的IP和端口以为,还设置了许多重要的参数,用于控制服务器的链接
private String name;
private String address;
private int port;
private int weight;
private float weightRage;
private ServerState state;
private ScoketPool scoketpool;
private int currUserCount;
private int deadTimeout;
private long deadTime;
private boolean testing = false;
其中state是客户端重连机制的最主要的参数。接下来我们慢慢的会讲解其中的一些参数是的意义,最主要的是在客户端中起到一个什么样的作用。state的几种状态:
public enum ServerState {
Dead,
Normal,
Busy,
Disable,
Reboot,
Testing
}
2 服务器的创建
服务器的创建是在客户端创建代理的加载配置文件的时候就已经创建好了,创建过程
for (ServerProfile ser : config.getServers()) {
if (ser.getWeithtRate() > 0) {
Server s = new Server(ser);
if (s.getState() != ServerState.Disable) {
ScoketPool sp = new ScoketPool(s, config);
s.setScoketpool(sp);
ServerPool.add(s);
}
}
}
在这里我们可以看到使用了weithtRate这个参数,如果这个参数配置<=0,那么这个服务器就不会被创建,其中哦在创建的时候,我们还会根据这个weithtRate参数,确定这个server的状态
this.weightRage = config.getWeithtRate();
if (this.weightRage >= 0) {
this.state = ServerState.Normal;
} else {
this.state = ServerState.Disable;
}
看到这里,程序好像有点小bug,在创建Server的时候,似乎不可能被标记为disable。创建Server的时候,我们相应的会给每个Server创建对应的线程池SocketPool.
3 服务器的选择
服务器的选择是在类Dispatcher中,可以看到在GetServer中,Gaea使用了一种取模的方式做到负载均衡。如何取模呢?这里是利用客户端的请求次数取模于服务器的个数,再根据服务器的当前状态,获取服务。
int count = ServerPool.size();
int start = requestCount.get() % count;
if (requestCount.get() > 10) {
requestCount.set(0);
} else {
requestCount.getAndIncrement();
}
for (int i = start; i < start + count; i++) {
int index = i % count;
Server server = ServerPool.get(index);
利用这种算法,那么取服务器将是按照每一次请求,对服务器进行轮询访问,这样就能做到落在每台服务器的请求数量一样多,从而做到负载均衡。
4 服务重连机制
大概的画了一个Gaea服务器状态转换图,关于重连机制,全由这流程图决定。
不大会做图,简单的用ppt做了一个,简单的围绕这幅图,来说说Gaea服务的重连机制。
4.1 获取Server时的状态
在GetServer中,当Gaea取到一个Server的时候,去看它的状态,如果是dead或者reboot的时候,修改其状态为testing后返回,等于给再给这个服务器一次机会,让它去尝试。
if (server.getState() == ServerState.Dead && (System.currentTimeMillis() - server.getDeadTime()) > server.getDeadTimeout()) {
synchronized (this) {
if (server.getState() == ServerState.Dead && (System.currentTimeMillis() - server.getDeadTime()) > server.getDeadTimeout()) {
result = server;
server.setState(ServerState.Testing);
server.setDeadTime(0);
logger.warning("find server that need to test!host:" + server.getAddress());
break;
}
}
}
if(server.getState() == ServerState.Reboot && (System.currentTimeMillis() - server.getDeadTime()) > server.getDeadTimeout()){
synchronized (this) {
if (server.getState() == ServerState.Reboot && (System.currentTimeMillis() - server.getDeadTime()) > server.getDeadTimeout()) {
result = server;
server.setState(ServerState.Testing);
server.setDeadTime(0);
requestCount.getAndDecrement();
logger.warning("find server that need to test!host:" + server.getAddress());
break;
}
}
}
在代码中我们可以看到,在检测其状态的时候,还必须要求这个服务器在deatTimeout以内,如果超过这一范围,那么此服务器将失去再次尝试链接的机会,永久不再使用。每次取得状态为dead和reboot的之后,都会将其重职位testing,再将死亡时间置为0,增加下次重试的机会。
4.2 收发数据失败
在收发数据失败的时候,我们将对其状态做一些更改,以便给他再一次机会,重连服务。这部分的状态变化,大部分实在catch语句块中执行的。
catch (IOException ex) {
logger.error("io exception", ex);
if (socket == null || !socket.connecting()) {
if (!test()) {
markAsDead();
}
}
throw ex;
} catch (Throwable ex) {
logger.error("request other Exception", ex);
throw ex;
} finally {
if (state == state.Testing) {
markAsDead();
}
if (socket != null) {
socket.unregisterRec(p.getSessionID());
}
decreaseCU();
}
以上是收发函数request中的catch部分,也就是收发失败,一般这种情况就是IO的异常导致收发数据失败,所以在IOException中,Gaea做了相应的处理。if(!test()) 这句对服务端做了探测,如果探测失败,则标记此服务器的状态为dead。探测的时候,简单的做了connect操作。
Socket socket = new Socket();
socket.connect(new InetSocketAddress(this.address, this.port), 100);
socket.close();
result = true;
如果发送失败,Gaea会去再取下一个服务器。循环的次数有一个简单的算法决定,如果都返回异常,则就向服务调用着抛出异常。
ioreconnect = serverCount - 1;
count = requestTime;
if(ioreconnect > requestTime){
count = ioreconnect;
}
以上serverCount是服务器个数,requestTime是配置文件中配置的请求次数。最终循环count次,用来访问服务器。
4.3 收到重启协议
当收到重启协议的时候,Gaea就将其服务器状态改为reboot,并再次去调用服务。用以获取正常数据
else if(receiveP.getSDPType() == SDPType.Reset){ //服务重启
logger.info(server.getName()+" server is reboot,system will change normal server!");
this.createReboot(server);
return invoke(returnType, typeName, methodName, paras);
4.4 收到正常的数据
Gaea收到正常数据的时候,服务器的状态可分为两种情况,Normal和Testing。收到normal当然是很正常的状态,这里不再多说,而当此时的状态是Testing的时候,那么就是我们获取Server时,dead和reboot状态的服务器而来的,此时既然能收到正常数据,那么就能确定,这台Server已经正常,因此将其状态改为normal。
Protocol result =Protocol.fromBytes(buffer,socket.isRights(),socket.getDESKey());
if (this.state == ServerState.Testing) {
relive();
}
当收到数据,改其状态为normal。
5 总结
以上为Gaea的服务器处理策略,能够在服务端重启,短暂出现异常的时候,很快自我恢复,这是一个服务通信框架最基本的要求,否则我们将陷入复杂的系统维护上。