一、服务化的演变
分布式应用架构体系对于逻辑复杂的需求十分强烈,上层业务都想借自己已有的递增服务、来快速搭建更多、更丰富的应用,降低新业务开展的人力和时间成本,快速满足瞬息万变的市场需求,公共的业务被区分出来,形成可共用的服务,最大程度地保障了代码和逻辑的复用,避免重复建设,这种设计也被称为SOA。
SOA(Service-Oriented Architecture)面向服务的架构是一个组件模型,它将应用程序的不同功能单元(称为服务)通过这些服务之间定义良好的接口和契约联系起来,接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。
SOA架构中,服务消费者通过服务名称,在众多服务中找到要调用的服务的地址列表,称为服务的路由。
而对于负载较高的服务来说,往往对应着由多台服务器组成的集群。在请求到来时,为了将请求均衡的分配到后的服务器,负载均衡程序将服务对应的地址列表中,通过相应的负载均衡算法和规则,选取一台服务器进行访问,这个过程称为服务的负载均衡。
当服务模块较小时,可以曹勇硬编码的方式将服务地址和配置在代码中。通过编码的方式来解决服务的路由和负载均衡问题,也可以通过传统的硬件辅助均衡设备如F5等或者采用LVS或Nginx等软件解决方案,通过相关配置,来解决服务的路由和复杂均衡问题。由于服务的机器数量在可控范围内,因此维护成本能够接受。
当服务越来越多,规模越来越大时,对应的机器数量也越来越庞大,单靠人工来管理和维护服务及地址的配置信息,已经越来越空难。并且,依赖单一的硬件负载均衡设备或者使用LVS,Nginx等软件方案进行路由和负载均衡服务器宕机,依赖它的所有服务均将失效。
此时,需要一个能够动态注册和获取服务信息的地方,来统一管理服务名称和其对应的服务器列表信息,称之为服务配置中心,服务提供者在启动时,将其提供的服务名称、服务器地址注册到服务配置中心,服务消费者通过服务配置中心来获取需要调用的服务的机器列表,通过相应的负载均衡算法,选取其中一台服务器进行调用。当服务器宕机或者下线时,相应的机器需要能够动态的从配置服务中心里面移除,并通知相应的服务消费者,否则,服务消费者就有可能因为调用到已经失效的服务而发生错误。在这个过程中,服务消费者只有在第一次调用服务时需要查询服务配置中心,而不需要重新发起请求道服务配置中心去获取相应的服务地址列表,知道服务的地址列表有变更。这种无中心的结构解决了之前负载均衡设置所导致的单点故障问题,并且大大减轻了服务配置中心的压力。
基于ZooKeeper的持久和非持久节点,我们能够近乎实时地感知到后端服务器的状态。通过集群间zab协议,使得服务配置信息能够保持一致,而Zookeeper本身容错特性和leader选举机制,能保障我们方便地进行扩容。通过ZooKeeper来实现服务动态注册、机器上线与下线的动态感知,扩容方便,容错性好,且无中心结构能够解决之前使用负载均衡设备所带来的单点故障问题,只有当配置信息更新时才会去ZooKeeper上获取最新的服务地址列表,其他时候使用本地缓存即可、基于ZooKeeper的服务配置中心的搭建。
二、负载均衡算法
服务化的演进提到服务消费者从服务配置中心获取到服务的地址列表后,需要选择其中一台来发起RPC调用。如何选择,取决于具体的负载均衡算法,对应于不同的场景,选择的算法也不尽相同,常见的负载均衡算法有轮训法、随机法。源地址哈希发、加权轮询法,加权随机法、最小链接法等。
1、轮询法
将请求按顺序轮流的分配到后端服务器上,它均衡地对待后端每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
通过初始化一个serverWeightMap的Map变量来表示服务器地址和权重的映射,以此来模拟轮询算法的实现,
1 serverWeightMap=new HashMap<String,Integer>(); 2 serverWeightMap.put("192.168.1.100",1); 3 serverWeightMap.put("192.168.1.101",1); 4 serverWeightMap.put("192.168.1.102",4); 5 serverWeightMap.put("192.168.1.103",3);
轮询的关键代码:
1 public static String testRoundRobin(){ 2 3 //重新创建一个map,避免出现由于服务器上线和下线导致的并发问题 4 Map<String ,Integer> serverMap = new HashMap<String,Integer>(); 5 serverMap.putAll(serverWeightMap); 6 //取得IP地址list 7 Set<String> keySet = serverMap.keySet(); 8 ArrayList<String> keyList = new ArrayList<String>(); 9 keyList.addAll(keySet); 10 String server = null; 11 synchronized(pos){ 12 if(pos>=keySet.size()){ 13 pos=0; 14 } 15 16 server=keyList.get(pos); 17 pos++; 18 } 19 return server. 20 }
由于serverWeightMap中的地址列表是动态的,随时可能有机器上线、下线或者宕机,因此,为了避免可能出现的并发问题,如数组越界,通过新建方法内的局部变量serverMap,先将域变量复制到线程本地,以避免多个线程虚高。这样可能会引入新的问题,复制一会serverWeightMap的修改将无法反映给serverMap,也就是说,在这一轮选择服务器的的过程中。新增服务器或者下线服务器负载均衡算法中将无法获知,新增比较好处理,而当服务器下线或者宕机时,服务消费者将有可能访问到不存在的地址,因此,在服务消费者的实现端需要考虑该问题,并且进行相应的容错处理,比如重新发起一次调用。
对于当前轮询的位置变量pos,为了保证服务器选择地顺序性,需要在操作时对其加上synchronized锁,使得在同一时刻只有一个线程能够修改pos的值,都在当pos变量被并发修改,则无法保证服务器选择的顺序性,甚至有可能导致keyList数组越界。
2、随机法
由概率论可知,随着调用量的增大,其实际效果越来越接近平均分配流量到每一台后端服务器,也就是轮询的效果。
1 public static String testRandom(){ 2 Map<String,Integer> serverMap = new HashMap<String,Integer>(); 3 serverMap.putAll(serverWeightMap); 4 //取得IP地址list 5 Set<String> keySet = serverMap.keySet(); 6 ArrayLisy<String> keyList = new ArrayList<String>(); 7 keyList.addAll(keySet); 8 Random random = new Random(); 9 int randomPos = random.nextInt(keyList.size()); 10 String server = keyList.get(randomPos); 11 return server; 12 }
3、源地址哈希法
源地址哈希的思想是获取客户端访问的IP地址值,通过哈希函数计算得到一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是要访问的服务器的序号。采用哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会被映射到同一台后端服务器进行访问。
1 public static String testConsumerHash(String remoteip){ 2 //重新创建一个map,避免出现由于服务器上线和下线导致的并发问题 3 Map<String,Integer> serverMap = new HashMap<String,Integer>(); 4 serverMap.putAll(serverWeightMap); 5 //取得IP地址list 6 Set<String> keySet = serverMap.keySet(); 7 ArrayList<String> keyList = new ArrayList<String>(); 8 keyList.addAll(keySet); 9 int hashCode = remoteip.hashCode(); 10 int serverListSize = keyList.size(); 11 int serverPos = hashCode%serverListSize; 12 return keyList.get(serverPos); 13 }
通过参数传入的客户端remoteip参数,取得它的哈希值,对服务器列表的大小取模,结果便是选用的服务器在服务器列表中的索引值。该算法保证了相同的客户端IP地址将会被“哈希”到同一台后端服务器,直到后端服务器列表变更。根据此特性可以在服务消费者与服务提供者之间建立有状态的session会话。
4、加权轮询法
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此他们抗压能力也不尽相同。给配置搞,负载低的机器配置更高的权重,让其处理更多的请求,而低配置、负载高的机器,则给其分配较低的权重,将其其系统负载,甲醛轮询能很好地处理这一问题,并将请求顺序且安装权重分配到后端。
1 public static String testWeightRoundRobin(){ 2 //重新创建一个map,避免出现由于服务器上线和下线导致的并发问题 3 Map<String,Integer> serverMap = new HashMap<String,Integer>(); 4 serverMap.putAll(serverWeightMap); 5 //取得ip地址list 6 Set<String> keySet = serverMap.keySet(); 7 Interator<String> it = keySet.iterator(); 8 List<String> serverList = new ArrayList<String>(); 9 while(it.hasNext()){ 10 String server = it.next(); 11 Integer weight = serverMap.get(server); 12 for(int i=0;i<weight;i++){ 13 serverList.add(server); 14 } 15 } 16 String server = null; 17 synchronized(pos){ 18 if(pos>=serverList.size()){ 19 pos = 0; 20 21 } 22 server = serverList.get(pos); 23 pos++; 24 } 25 return server; 26 }
5、加权随机法
1 public static String testWeightRandom(){ 2 //重新创建一个map,避免出现由于服务器上线和下线导致的并发问题 3 Map<String,Integer> serverMap = new HashMap<String,Integer>(); 4 serverMap.putAll(serverWeightMap); 5 //取得ip地址list 6 Set<String> keySet = serverMap.keySet(); 7 Interator<String> it = keySet.iterator(); 8 List<String> serverList = new ArrayList<String>(); 9 while(it.hasNext()){ 10 String server = it.next(); 11 Integer weight = serverMap.get(server); 12 for(int i=0;i<weight;i++){ 13 serverList.add(server); 14 } 15 } 16 Random random = new Random(); 17 int randomPos = random.nextInt(serverList.size()); 18 String server = serverList.get(randomPos); 19 return server; 20 }