网站功能丰富后出现的问题以及解决方法
当应用功能增加后,对网站的稳定性会产生一定的影响,这时会选择把应用拆小,保持每个应用都不会太大,具体有两种方法。
第一种方案是把大的应用拆成多个,这样的好处是可以很快的完成
但是这样仍然会存在一些问题,比如说数据库的连接数压力还是很大, 还有应用之间会存在重复的代码
第二种方法是服务化方案
在原来的应用和底层数据库、缓存系统、文件系统等系统之间添加服务层。
从结构上看,系统架构更加清晰,更加稳定。
服务框架的设计和实现
应用从集中式走向分布式所遇到的问题
在没有服务化之前,应用是通过本地调用的方式来使用其他组件,服务化会使得原来的一些本地调用变为远程调用。
从单机的单个进程中的一个方法调用分散到两个节点要经过很多步骤。
单机单进程函数调用就是把程序计数器指向相应的入口地址,而在多机之间,我们需要对调用的请求信息进行变化,然后传给远程的节点,解码之后在进行真正的调用。寻址路由是用来让调用方确定哪个实例被调用,实例定位是指在被调用的机器上找到对应的实例来进行方法调用,从而实现功能。
透过示例看服务框架原型
服务框架应该既包含调用端逻辑又包含服务端逻辑的。
单机模式
例如实现一个计算器的功能
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int minus(int a, int b) {
return a - b;
}
public static void test1() {
Calculator calculator = new Calculator();
System.out.println(calculator.add(1, 1));
}
}
实现远程服务的调用客户端
首先把Calculator抽象成一个接口,在实现类中进行实现
public interface Calculator {
public int add(int a, int b);
public int minus(int a, int b);
}
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int minus(int a, int b) {
return a - b;
}
}
在调用端调用add方法的逻辑
public int add(int a,int b){
//获得可用的服务地址列表
List<String> l = getAvailableServiceAddresses("Calculator.add");
//确定要调用服务的目标机器
String address = chooseTarget(l);
//建立连接
Socket socket = new Socket(address);
//请求序列化
byte[] request = genRequest(a,b);
//发送请求
socket.getOutputStream().write(request);
//接受结果
byte[] response = new byte[10240];
socket.getInputStream().read(response);
//解析结果
int result = getResult(response);
return result;
}
}
构造请求数据包就是把对象变成二进制数据,也就是常说的序列化,接着就通过Socket来简单的做一个实现把序列化数据发送过去。
服务调用端的设计和实现
确定服务框架的使用方式
代码角度
一般使用Spring框架,会使用一个通用的对象完成本地和远程服务的调用
一般这个对象会有三个基本的属性
- interfaceName
- 接口名称 指明要调用的接口
- version
- 版本号
- group
- 分组 远程机器根据服务分组,这样可以选择不同的分组来调用
运行期服务框架与应用和容器的关系
问题1:服务框架自身的部署方式问题
解决方法:
1.把服务框架和应用一起打包 变成应用的一个库随应用启动
存在的问题就是如果要升级框架的话也要更新应用。
2.把框架作为容器的一部分。一般WEB应用的容器是JBoss,Tomcat,Jetty之类,如果是JBoss,可以通过MBean实现服务框架的启动,部署为一个sar包来提供服务。
服务框架作为WEB应用的一个依赖包的情况
作为WEB容器的扩展
作为容器本身
服务调用者和服务提供者之间通信方式的选择
1.远程通信遇到的问题
最初写两台机器进行对话的例子时,都是定好IP地址和端口号
在实际中,提供某种服务的机器一定是多台的,调用服务的机器也可能是集群
2.采用透明代理和调用者、服务提供者直连的解决方案
第一种通过中间代理
第二种采用直连的方式
引入基于接口、方法、参数的路由
上图的问题是如果一个接口的某个方法非常耗时就会出现阻塞的情况
因此可以控制同一个集群中不同服务的路由并进行请求隔离,虽然集群中的每台机器部署的代码是一样的,提供的服务也是一样的,但是通过路由的策略,让其中对于某些服务的请求到一部门机器,另一些服务的请求到另一部分机器。
上图把调用服务A的请求送到上方的集群,调用B的请求送到下方的集群,这样可以使得请求分流。
多机房场景
根据机房之间的距离和分工决定架构和策略
可以在服务注册查找中心甄别不同机房的调用者集群,返回不同服务提供者的地址。
也可以通过路由来完成,给不同机房的调用者相同服务提供者列表,但是在框架内部进行地址过滤,过滤的原则一般是基于接口等路由规则进行集中配置管理。
服务调用端的流控处理
流量控制保证系统的稳定性,这里指的是加载到调用者的控制功能,为了控制到服务提供者的请求的流量。
一种方式是1-0开关,完全打开或完全关闭。
另一种是设置一个固定的值,表示每秒可以进行的请求次数,超过的话就拒绝远程请求,被拒绝的可以返回给调用者或者进行排队。
可以根据服务端自身的接口、方法做控制,分别设置不同的阈值
也可以根据来源做控制,同样的接口,方法根据不同来源设置不同的限制
网络通信实现选择
使用NIO的时候增加了IO线程,数据队列,通信对象队列和定时任务四个部分。
IO线程专门负责和SOCKET连接打交道,进行数据的收发。
所有需要发送的数据都会进入数据队列。
通信对象队列保存了多个线程使用的通信对象,主要为了阻塞请求线程,请求线程把数据放入数据队列中后会生成一个通信对象,会进入通信对象队列并等待。
通信对象用于唤醒请求线程,如果远程调用超时前有结果返回,IO线程会通知通信对象,通信对象就会结束请求线程的等待,并传回结果进行后续处理。
支持多种异步服务调用方式
除了同步调用外,还存在几种调用方式
第一种是Oneway.只负责发送请求不关心结果。
第二种是Callback,有相应的时候通过回调方法告知调用方
第三种是Future
Future入队列,数据入队列,请求处理结束后通过Future获得线程执行的结果
第四种是可靠异步,保证异步请求可以在远程被执行,一般通过消息中间件来保证。
服务提供端的设计和实现
两个部分,一是对本地服务的注册管理,二是根据进来的请求定位服务并执行。
服务端对请求处理的流程
首先在端口监听,等待服务调用请求。通信部分也是NIO方式实现,收到请求后,通过协议解析和反序列化,得到调用服务的具体信息,然后根据服务名称和版本号找到具体对象,再用参数调用相关的方法即可。
执行不同服务的线程池隔离