公司的几个核心应用采用restful风格的服务接口对外提供服务,这些应用均采用Java spring mvc来实现rest服务,调用这些服务的消费者应用使用的语言则有多种,主要有php、python、Ruby、Java、C++、C#。
随着服务接口数量和消费者应用的增多,需要有一个rest服务治理框架来进行支撑,该服务治理框架提供的功能主要有:
服务注册和发现
负载均衡与容错
服务授权和鉴权
服务依赖关系管理
服务性能采集和监控
借鉴阿里dubbo框架,设计了公司的http rest服务治理框架,整个架构如下。
采用zookeeper作为服务注册中心,服务提供者(上图右边所示的java rest服务应用)将服务发布注册到zookeeper中,服务消费者(上图左边所示的client应用)根据服务名和版本等限定参数查找对应的服务,获取服务endpoint url列表。在endpoint url列表有变动时,通过zookeeper的watch机制将变动推送到服务消费者。
服务控制中心是一个web控制台,提供的功能主要有:
管理应用(服务提供者、服务消费者应用):每个应用都有一个应用id和应用密钥以及应用负责人等信息。
应用信息同时要写到zookeeper上。
展示服务依赖关系的全局视图(每个应用提供有哪些服务,这些服务被哪些消费应用依赖)、
服务访问授权和鉴权:控制每个服务可被哪些消费者应用调用,支持应用白名单和黑名单以及客户端ip地址授权。服务调用时的鉴权:授权信息要写到zookeeper上,然后推送到服务提供者应用以便进行服务鉴权。
展示服务的部署信息:服务地址endpoint、健康状况、调用性能等运行信息。
服务生命周期管理
服务发布
对Java spring mvc实现的rest服务,要发布服务,需要3处修改:
1)在spring dispatch servlet对应的bean配置文件中加入定制标签,增加内容如下:
<!-- 加入spring定制名称空间yyservice -->
<beans ... xmlns:yyservice="http://code.duowantech.com/schema/yyservice"
xsi:schemaLocation="http://code.duowantech.com/schema/yyservice http://code.duowantech.com/schema/yyservice/yyservice.xsd"
...><yyservice:application name="xxx-webservice" version="1.0"/>
<yyservice:serviceRegistry name="zk1" address="58.249.117.107:2181"/>
<yyservice:restServiceExporter appEndpoint="*:8080" context="" urlPattern="/api/*"/>
...
</beans>
yyservice:application指定应用名称和应用版本;yyservice:serviceRegistry指定zookeeper的
连接字符串(ip:端口);yyservice:restServiceExporter用来将spring mvc rest服务注册到zookeeper,appEndpoint表示应用的ip和http端口,context是servletContext名称,对root servletContext,context为""。urlPattern是spring dispatch servlet对应的url pattern,要和web.xml中的url pattern保持一致。
2)在服务提供者应用的pom文件中加入治理框架提供的1个jar包依赖。这个jar包实现了上面的定制名称空间yyservice,同时提供spring mvc intercept来进行客户端调用鉴权和调用性能采集、服务健康监控接口等。
3)这步是可选的。在spring mvc controller类(就是它实现了rest服务)中加入框架提供的注解信息。
//ServerResource.java
@Controller
@RequestMapping(value="/servers")
@ResourceDesc(name = "ServerResource",desc="服务器资源")
public class ServerResource {
...
@MethodDesc(desc="根据server id查找服务器")
@RequestMapping(value = "/id-{serverId}",method=RequestMethod.GET)
@ResponseBody
public Result<Server> findServer(@PathVariable Integer serverId) {
...
}
@MethodDesc(desc="创建服务器")
@RequestMapping(method=RequestMethod.POST)
@ResponseBody
// serialNo流水号唯一,防止失败重试时重复重建
public Result<CreateResult> createServer(String serialNo,Server server) {
...
}
@MethodDesc(desc="更新服务器")
@RequestMapping(value = "/id-{serverId}",method=RequestMethod.PUT)
@ResponseBody
public Result<DeleteResult> updateServer(@PathVariable Integer serverId,Server server) {
...
}
@MethodDesc(desc="删除服务器")
@RequestMapping(value = "/id-{serverId}",method=RequestMethod.DELETE)
@ResponseBody
public Result<DeleteResult> delServer(@PathVariable Integer serverId) {
...
}
}
上图中的ServerResource类(该类表示服务器rest资源)中,ResourceDesc和MethodDesc注解是治理框架提供的,承载资源的描述信息,这两个注解是可选的,根据需要添加,其他注解是spring mvc的。每种类型的rest 资源都有一个唯一的资源名称,缺省情况下,资源名称是java类的全限定名(含包名),如果ResourceDesc的name属性不为空,则使用该名称作为资源名。ResourceDesc和MethodDesc中的信息会展示在服务控制中心的展示页面中。
通过三面的3步,服务应用启动后会自动将服务注册到注册中心,ResourceDesc和MethodDesc注解中的信息也会自动添加到数据库中,最终展示在服务控制中心的展示页面中。
如果rest服务不是采用spring mvc实现或非java应用(如python应用)提供的rest服务,需要提供一个服务信息xml文件,在这些应用代码中调用框架提供的服务注册api,将这个xml文件传给api的注册方法即可将服务注册到zookeeper。
服务发现和服务调用
java应用调用rest服务,步骤如下
1)spring bean配置文件中加入如下的定制标签
<beans ... xmlns:yyservice="http://code.duowantech.com/schema/yyservice"
xsi:schemaLocation="
...
http://code.duowantech.com/schema/yyservice http://code.duowantech.com/schema/yyservice/yyservice.xsd ..."
>
...
<yyservice:application name="dcm" version="1.0"/>
<yyservice:serviceRegistry name="zk1" address="58.249.117.107:2181"/>
<yyservice:restServiceReference id="serverResource" resourceName="ServerResource" connTimeout="1000" readTimeout="3000"
/>
yyservice:application和yyservice:serviceRegistry标签先前已有介绍。
yyservice:restServiceReference标签会生成一个rest客户端bean(其类型是RestServiceClient,父类是spring中的RestTemplate类 ),resourceName为ServerResource,表示该客户端bean可用来调用前面提到的ServerResource中的服务。
2)和服务提供者应用相同,在消费者应用的pom文件中加入治理框架提供的1个jar包依赖。
3)在需要调用这个ServerResource资源服务的其他bean中注入该客户端bean。
其他bean可通过bean配置文件中的ref属性(其值为restServiceReference标签中的id值serverResource)来引用该bean,或在类中使用Resource注解来自动注入,例如:
//import javax.annotation.Resource;
@Resource(name="serverResource")
private RestTemplate serverResourceClient;
//或private RestServiceClient serverResourceClient;
调用接口文档见spring的RestTemplate类。例如要调用serverResource中的删除服务器,使用
serverResourceClient.delete("/servers/id-1234"); //删除server id为1234的服务器。
唯一需要注意的地方是url中只要提供spring mvc controller(rest资源)RequestMapping注解中的url即可,
不需要要写完整的url,例如上面的delete方法中传的是/server/id-1234,没有http://ip:端口、serverletContext和urlPattern,这些ip、端口等都是和部署相关的信息,编写客户端调用代码时不需要关心这些。serverResourceClient会从服务注册中心根据资源名称找到这些信息(服务发现),然后拼接成完整的url发起调用。如果有多个ip和端口提供服务,会采用轮询机制选取一个发起调用(服务负载均衡)。
非java应用调用rest服务
对非java应用,用c++编写了调用客户端(linux下yyhttpserviceclient.so和windows下yyhttpserviceclient.dll),c++客户端可使用该客户端来调用rest服务。该客户端调用模型如下:
c++调用实例如下:
/*调用instance静态方法返回单例资源工厂*/
YyHttpResourceFactory* resourceFactory = YyHttpResourceFactory::instance( );
/*只需初始化一次,参数分别是appId,zookeeper链接串、应用版本*/
resourceFactory->init("myappId","58.249.117.107:2181","1.0");
/*调用资源工厂的resource方法返回http服务(资源)指针,该对象可重用。*/
YyHttpResource* pServerResource = resourceFactory->resource("serverResource");
/*创建该资源下的请求*/
YyHttpRequest* req = pServerResource->url("/servers/id-1234");
//和java RestTamplate相同,也可使用url模板来设置url中的参数,如下:
/*
std::map<std::string,std:string> paraMap;
map.insert(make_pair("serverId","1234"));
YyHttpRequest* req = pServerResource->url("/servers/id-{serverId}",paraMap);
*/
/*设置查询参数、http头、请求body、失败重试次数、链接和读超时时间等
// req->appendQueryString("参数名","参数值")-> appendHeader("http头名称","值")->body("...")->retries(2)
//调用delete方法发送http请求,返回应答resp*/
YyHttpResponse* resp = req->delete();
/*是否有网络错误*/
resp->isNetworkError( );
/*获取网络错误码*/
resp->getErrorNo( );
/*没有网络错误,查看http状态码*/
resp->getStatusCode( );
/*http应答body*/
resp->getResponseBody( );
/*body的content type*/
resp->getContentType( );
/*req、resp为一次性对象,不能重复使用。释放资源,否则会有内存泄漏*/
req->release( );
resp->release( )
对除Java和C++之外的语言,如python、php、ruby、C#等调用rest服务,框架使用swig来支持多语言客户端。swig是个可将C或者C++编写的代码变成能与其它各种高级编程语言进行嵌入联接的开发工具。采用swig后,可以通过c++客户端自动生成python、php等使用的客户端。例如将前面的c++客户端代码经过swig处理后可编译生成python调用的_yyhttpserviceclient.so和yyhttpserviceclient.py。在这些语言中的调用过程和c++客户端相同,均遵从相同的调用模型和方法。在python中删除服务器资源示例如下:
import yyhttpserviceclient
yyHttpResourceFactory = yyhttpserviceclient.YyHttpResourceFactory.instance( )
yyHttpResourceFactory.init('dcm','58.249.117.107:2181','1.0')
resource = yyHttpResourceFactory.resource('serverResource')
req = resource.url('/servers/id-1234')
//req.appendHeader('x','1').appendHeader('y','2')
//返回response
resp = req.delete()
resp.getResponseBody( )
resp.release( )
req.release( )