服务扩容的一点总结

最近一年经历了很多业务快速增长的情况,早期对容量的规划不足成为普遍现象,需要在实际业务中要不断调整。这就涉及到服务扩容。一般无状态请求的扩容,比如varnish转发http请求,因为不需要考虑状态迁移造成的问题,比较简单。复杂一点的就是有状态的扩容。有状态扩容主要的应用场景是服务器负载均衡技术以及数据库水平扩容。无论是服务器负载均衡还是数据库水平扩容,主要考虑的问题都是:1,怎么分配请求;2,怎么减少扩容对原有服务带来的震荡。

和大家分享一个真实案例,基于RTSP协议的点播消息负载均衡。RTSP是基于TCP的视频流控制协议。它的特点是点播过程中涉及使用的视频资源比较多,比如视频服务器端口,视频资源,cable资源等。因此要求同一点播会话最好分配给同一个服务器处理。这是典型的有状态(session-sticky)的负载均衡。有状态服务分配采用hash方式是最常见的解决方案,好处是即可以做到均匀分配,又可以实现session sticky,即相同session id的请求都会分配给相同的服务器。缺点是hash容易,re-hash难。以hash方式分配,扩容时的震荡较大。这也是为什么有一致性hash的原因。另一个条件是,服务器性能和配置不是完全相同的。我们希望可以在性能高的机器上多跑些业务,所以采用hash分配就不满足要求。我们考虑给服务器分配一定权重,再此基础上再作随机分配。但缺点是采用固定的权重,可能导致服务器负载严重不平衡。但如果采用某种探测算法,又有一定风险。比如某一RTSP服务器所管辖的视频服务器下属的链路断开,这样所有点播会话都会结束。但RTSP服务器不知道这个点播结束是否是正常的,这样这台负载非常轻的服务器实际是一台不能正常提供服务的服务器。为了避免这种风险,还是决定采用固定权重和随机分配相结合。好处是扩容很方便。缺点是必须使用一个session id和服务器ip的对应表,每次请求到达时先查这个表,对性能有影响。更重要的是,如果采用hash分配,前端的负载均衡服务是可以多点的,只要hash算法一致,无论哪台负载均衡服务器都会给相同的session分配相同的服务器。但如果使用对应表,则只能使用单点,而且对于session id必须使用同步锁,否则就可能为相同的会话分配不同的服务器。这是一个很严重的瓶颈!

因此,最后的方案是使用手工一致hash和分配表结合。我们用了一个很简单的手工一致性hash解决hash扩容的问题。 基本思路是,在服务上线初期,就分配有足够大的逻辑服务器地址表。当需要扩容的时候,只需要更改逻辑服务器指向的物理服务器就可以。而不需要更改逻辑服务器数量,我写了一个简单的程序来验证,当re-hash发生时,采用手工一致性hash,重复命中率达到90%。而简单增加服务器数量,选择相同服务器的几率仅有8%。这个思路也是Redis作者谈到解决re-hash问题时用的方法。这种通过大逻辑服务器表提高命中率的思路类似于一致性hash,但是更简单。我就称为手工一致性hash。

在使用手工一致性hash的情况下,再加上对应表,可以实现平滑的扩容服务器。已分配的服务器的会话继续使用已分配的服务器,而没有分配过的,则按新逻辑hash表进行分配。同时,还可以解决单点问题。这个方案并不是独创,在业界是经常使用的方案。我后来在某微博工作时,也是广泛的使用这个方案作为负载均衡应对未来扩容的解决方案。但相对大型互动社区而言,并不是所有的公司都必须采用此种方案。比如广电行业CDN服务器分配时,也是根据媒资名字做hash分配,但重新增加服务器时,往往对片源做很大调整。原先的hash分配可能会大部分失效,这是追求重复命中率高就意义不大。核心业务,在业务不需要的情况下,引入多一层架构,就增加多一份人力和维护成本,构架更需要考虑的是如何做减法,而不是做加法。

验证rehash命中率的伪代码如下,logicServers应扩大为如:

 "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "sa", "sb", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "sa", "sb"
, "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "sa", "sb", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "sa", "sb"

这样连续重复,使得逻辑server表扩大到200以上

private void test() throws Exception
{
  Integer localInteger;
  HashMap localHashMap = new HashMap();
  ArrayList localArrayList = new ArrayList();
 
  String[] logicServers = { "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "sa", "sb" };
  String[] logicServers2 = { "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "sa", "sb", "sc" };
  float f1 = 0.0F; float f2 = 0.0F; float f3 = 100000.0F;
 
  for (int i = 0; i < f3; ++i) {
    localArrayList.add(Integer.valueOf(Math.abs(new Random().nextInt())));
  }
 
  for (Iterator localIterator = localArrayList.iterator(); localIterator.hasNext(); ) { 
  	 localInteger = (Integer)localIterator.next();
    localHashMap.put(localInteger, logicServers[(localInteger.intValue() % logicServers.length)]);
  }
 
  for (localIterator = localArrayList.iterator(); localIterator.hasNext(); ) { 
    localInteger = (Integer)localIterator.next();
    String str = logicServers2[(localInteger.intValue() % logicServers2.length)];
 
    if (str.equals(localHashMap.get(localInteger))) {
      f1 += 1.0F;
    }
    else {
      f2 += 1.0F;
    }
  }
 
  System.out.println(new StringBuilder().append(logicServers.length).append("|").append(logicServers2.length).append(", same=").append(f1).append(" samepercent= ").append(f1 / f3 * 100.0F).append("%, diff=").append(f2).toString());
}

猜你喜欢

转载自maoyidao.iteye.com/blog/1328131