Skip to content

使用手册 中文版

neoremind edited this page Aug 28, 2015 · 37 revisions

1 Navi产生背景

你所负责的产品,业务量和代码量是否面临爆炸式增长的问题? 包括:

模块划分粒度粗,分层混乱。

代码耦合度高,缺少重用,存在代码搬运现象,开发效率低,影响了迭代速度。

系统理解困难,可维护性差。

系统划分不合理,致使各系统之间关系复杂。

系统可扩展性差,性能受到限制。

为了更好的服务于产品(这是我们的立足之本、核心使命,架构为业务服务),同时作为开发人员能够惬意、高效地进行日常开发升级,你的系统是否急需要一次大的诊断并手术,这就是“重构”,只有重构才能精简代码,避免重复拷贝,提高开发效率,进而提高团队生产力,因此在重构的背景下,可以选择了引入“服务化”,二者会起到协同效应的作用,一方面模块化,另一方面服务化,对于业务稍复杂的系统来说,这可以说是一种最佳实践。

Navi就是在服务化的背景下产生的,作为未来服务化发展的核心基础框架。

2 设计架构

角色说明

  • 服务提供者(Provider): 暴露服务的服务提供方,也叫做服务端。
  • 服务容器:服务运行容器,由spring负责管理,在servlet容器中运行。
  • 服务消费者: 调用远程服务的服务消费方,也就是调用方。
  • 服务注册中心(Zookeeper):服务注册与发现的注册中心,由zookeeper实现。

调用关系说明

  1. 启动:服务容器负责启动,加载,运行服务提供者。
  2. 注册:服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 订阅:服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 通知:注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 调用:服务消费者,从提供者地址列表中,基于软负载均衡算法(轮训或者随机),选一台提供者进行调用(可采用FAILOVER重试或者FAILFAST失败退出策略)。

3 注册中心

3.1 Zookeeper

注册中心采用zookeeper实现,Zookeeper 作为一个分布式的服务框架,是Apache Hadoop下的一个子项目,主要用来解决分布式集群中应用系统的一致性问题。

它能提供基于类似于文件系统的目录节点树方式的数据存储。但是 Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控存储数据的状态变化。

通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。

每个子目录项都被称作为 znode,znode按照path下来是可以唯一定位的。znode 可以有子节点目录,并且每个 znode 可以存储数据,节点类型可以为PERSISTENT或者EPHEMERAL类型。

所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。

和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。这也是利用zookeeper来监听服务提供者是否在线的原理。

3.2 Znode规划

服务注册到zookeeper的path,以及客户端订阅的时候需要有一个统一的规则来规定各个path的层级关系,有个规则,那么服务提供者、消费者就可以做到“言语”统一,方便管理。

Navi规则见下图,

解释如下,

  • Root: zookeeper根路径。

  • 根命名空间(global namespace): 服务化用的路径,与它并列还有其他需要用到zookeeper的项目,例如北斗的fountain项目。

  • 项目命名空间(app namespace): 项目命名空间,可以采用多路径配置,znode数量随意,只需要在服务提供者、消费者的配置文件中配置正确即可。推荐采用服务名称+版本号方式,例如/my-service/main表示自己的第一个服务的全流量main版本。

  • 服务(services): 这一层就是app暴露的服务详细了,默认一个znode + 一个“IP:PORT”命名的znode,前者表示服务名称,后者表示提供服务的地址。

注意

其中Global namespace在代码中默认设置为/navi_rpc,该节点必须存在于zookeeper中;如果想设置自己的公共默认路径请配置ZOOKEEPER_BASE_PATH#NaviCommonConstant.java

3.3 可用性(High Availability)

  1. 注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互。
  2. 注册中心、服务提供者、服务消费者三者之间均为长连接。
  3. 注册中心通过长连接感知服务提供者的存在,服务提供者down掉,注册中心将立即推送事件通知消费者。
  4. 注册中心全部down掉,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。
  5. 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。
  6. 注册中心使用对等zookeeper集群,任意一台宕掉后,将自动切换到另一台,一般采用2+1策略,即3台里允许一台down掉,zookeeper服务不受影响。
  7. 服务提供者无状态,任意一台宕掉后,不影响使用,同时可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者,从而达到了水平扩展,扩容的目的。
  8. 注册中心上可动态增加或减少服务,所有消费者将自动发现新的注册服务。

4 使用方式

为了快速了解navi如何使用,我们来做一个简单的hello world。

4.1 准备工作

服务提供者、消费者均需要依赖navi。项目中的navi-demo-server即是一个服务提供者模块,navi-demo-client即是一个服务消费者模块。

4.1.1 配置maven

服务提供者和消费者模块都需要依赖navi模块,该模块用maven进行源代码管理,在pom.xml中加入dependency:

<dependency>
    <groupId>com.baidu.beidou</groupId>
    <artifactId>navi-rpc</artifactId>
    <version>1.1.0</version>
</dependency>

通过mvn dependency:tree命令分析,navi依赖以下三方库:

[INFO] +- org.springframework:spring-beans:jar:3.1.2.RELEASE:compile
[INFO] +- org.springframework:spring-context:jar:3.1.2.RELEASE:compile
[INFO] |  +- org.springframework:spring-aop:jar:3.1.2.RELEASE:compile
[INFO] |  +- org.springframework:spring-expression:jar:3.1.2.RELEASE:compile
[INFO] |  \- org.springframework:spring-asm:jar:3.1.2.RELEASE:compile
[INFO] +- org.springframework:spring-core:jar:3.1.2.RELEASE:compile
[INFO] |  \- commons-logging:commons-logging:jar:1.1.1:compile
[INFO] +- org.springframework:spring-web:jar:3.1.2.RELEASE:compile
[INFO] |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO] +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.9:compile
[INFO] +- org.codehaus.jackson:jackson-core-asl:jar:1.9.9:compile
[INFO] +- com.dyuproject.protostuff:protostuff-runtime:jar:1.0.7:compile
[INFO] +- com.dyuproject.protostuff:protostuff-api:jar:1.0.7:compile
[INFO] +- com.dyuproject.protostuff:protostuff-collectionschema:jar:1.0.7:compile
[INFO] +- com.dyuproject.protostuff:protostuff-core:jar:1.0.7:compile
[INFO] +- org.apache.zookeeper:zookeeper:jar:3.4.0:compile
[INFO] |  +- org.slf4j:slf4j-api:jar:1.6.1:compile
[INFO] |  +- org.slf4j:slf4j-log4j12:jar:1.6.1:compile
[INFO] |  +- log4j:log4j:jar:1.2.15:compile
[INFO] |  |  \- javax.mail:mail:jar:1.4:compile
[INFO] |  |     \- javax.activation:activation:jar:1.1:compile
[INFO] |  +- jline:jline:jar:0.9.94:compile
[INFO] |  \- org.jboss.netty:netty:jar:3.2.2.Final:compile
[INFO] +- javax.servlet:servlet-api:jar:2.5:provided

4.1.2 配置日志

推荐使用logback作为日志组件,在logback.xml中加入如下配置,默认level="INFO",如想打开navi的debug模式可以设置level="DEBUG"。

<logger name="com.baidu.beidou.navi" level="INFO" additivity="true">
</logger>

如果使用slf4j+log4j组合,则在pom中去掉相应的依赖,使用log4j配置即可。

log4j.rootLogger=INFO, console
log4j.logger.com.baidu.beidou.navi=INFO
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.encoding=gbk
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%p]\t%d\t[%t]\t%c{3}\t(%F:%L)\t-%m%n

服务端和客户端更加详细的日志格式详细见附录部分。

4.2 服务提供者

服务端demo的所有代码可以参考navi-demo-server模块。

4.2.1 工程创建

Navi使用HTTP Servlet作为传输通道来进行RPC调用,因此服务端代码需要运行在servelt容器中,例如jetty、tomcat中。

Navi同时使用spring来管理处理请求的bean,因此需要spring容器的依赖,推荐使用spring3.0+以上版本,以下demo均为基于使用spring3.0+版本,同时兼容spring2.5,同时依赖于servlet-api,maven依赖为provided的scope,可以使用2.5也可以使用3.0版本。

首先创建一个war工程,配置webapp/WEB-INF/web.xml,常规spring配置省略,重点介绍需要加入一个核心的servlet配置。

<servlet>
    <display-name>navi rpc servlet</display-name>
    <servlet-name>naviRpcServlet</servlet-name>
    <servlet-class>com.baidu.beidou.navi.server.NaviRpcServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
	
<servlet-mapping>
    <servlet-name>naviRpcServlet</servlet-name>
    <url-pattern>/service_api/*</url-pattern>
</servlet-mapping>

注意

工程配置在tomcat或者jetty等容器的ROOT path上,也就是说不可以带相对路径,这对大多数应用都是适用的,线上的运维要求也是一个运行实例对一个应用,否则客户端将无法正确访问到服务端。

4.2.2 服务配置

在classpath中加入rpc-server.properties,默认启用zookeeper来进行服务的注册、订阅、更新操作,因此该配置文件中所有包含为*ZK*的均为zookeeper相关配置。

如果不想利用注册中心的zookeeper来订阅服务端地址,服务端可以关闭服务注册发布功能,只需要设置ENABLE_ZK_REGISTRY为false,那么客户端就自己直接定义访问地址,详见4.2.8小节。

################################################
#
# 是否启用zookeeper进行服务注册、发布
# 当ZK_SERVER_LIST为空时,该值忽略直接为false
# true:启用
# false:不启用
#
################################################
ENABLE_ZK_REGISTRY=true
 
################################################
#
# zookeeper集群地址IP:PORT列表,按逗号分隔
#
################################################
ZK_SERVER_LIST=127.0.0.1:8701
 
 
################################################
#
# zookeeper访问控制密串,留空表示不启用访问控制
#
################################################
ZK_DIGEST_AUTH=
 
################################################
#
# 启用zookeeper注册后,所发布服务的命名空间
# 按照"/产品线/服务名称/版本"的格式定义
#
################################################
ZK_REGISTRY_NAMESPACE=/beidou/test-service/online
 
################################################
#
# zookeeper nio连接seesion超时时间(单位ms),一般设置为30s
#
################################################
ZK_DEFAULT_SESSION_TIMEOUT_MILLS=30000
 
################################################
#
# 启动zookeeper连接时的超时时间(单位ms)
#
################################################
ZK_CONNECTION_TIMEOUT_MILLS=30000
 
 
################################################
#
# *选择性配置*
#
# 启用zookeeper注册服务时,需要提前配置端口号。
# (1) 在JPaaS1.0上环境,自动替换为环境变量中的
# $VCAP_APP_PORT或者JVM启动参数中的
# -Dport.http.nonssl。
# (2) 在JPaaS2.0上环境,自动替换为环境变量中的
# $JPAAS_HOST为注册IP,
# $JPAAS_HTTP_PORT为访问端口。
# (3) 如果不满足(1),(2)则直接取该配置值,因此通常在
# 开发环境或者测试环境中需要设置此值为tomcat或者jetty端口。
#
################################################
SERVER_PORT=8080

更多关于rpc-server.properties的配置说明见配置说明

配置加载到spring容器中,需要在applicationContext.xml中加入如下配置:

<context:annotation-config />
<context:component-scan base-package="com.baidu" />
<context:property-placeholder location="classpath*:/rpc-server.properties" />
 
<bean class="com.baidu.beidou.navi.conf.RpcServerConf">
    <property name="ENABLE_ZK_REGISTRY">
        <value>${ENABLE_ZK_REGISTRY}</value>
    </property>
    <property name="ZK_SERVER_LIST">
        <value>${ZK_SERVER_LIST}</value>
    </property>
    <property name="ZK_DIGEST_AUTH">
        <value>${ZK_DIGEST_AUTH}</value>
    </property>
    <property name="ZK_DEFAULT_SESSION_TIMEOUT_MILLS">
        <value>${ZK_DEFAULT_SESSION_TIMEOUT_MILLS}</value>
    </property>
    <property name="ZK_CONNECTION_TIMEOUT_MILLS">
        <value>${ZK_CONNECTION_TIMEOUT_MILLS}</value>
    </property>
    <property name="ZK_REGISTRY_NAMESPACE">
        <value>${ZK_REGISTRY_NAMESPACE}</value>
    </property>
    <property name="SERVER_PORT">
        <value>${SERVER_PORT}</value>
    </property>
</bean>

4.2.3 基于注解方式开发服务

推荐使用这种方式来开发服务。

Navi提供的服务接口暴露粒度为“类”级别,也就是说当需要暴露服务时,也就是把该类下面的所有方法当做服务接口暴露出去了,支持类的继承以及接口的继承。

服务参数及返回值都必需是byValue的,而不能是byRef的,消费方和提供方的参数或返回值引用并不是同一个,只是值相同,navi不支持引用远程对象

例如,我们要开发一个公司信息服务,提供公司信息的CRUD操作,首先创建一个Company类,该类可以不实现Serialization接口,POJO类建议存在默认无参构造函数,否则使用jackson-json序列化时会报错。Company类可以看做是BO(Biz Object),可以具有复杂结构,包括继承、引用其他类型泛型的属性等,但是不支持内部类(Inner class)实现。

它的服务CompanyMgr接口定义如下:

public interface CompanyMgr {
	public Company get(int id);
	public List<Company> getAll();
	public Company add(Company company);
	public void delete(int id);
	public void update(Company company);
}

其实现类CompanyMgrImpl加入注解

@NaviRpcService(serviceInterface = CompanyMgr.class)

例如,

@Service
@NaviRpcService(serviceInterface = CompanyMgr.class)
public class CompanyMgrImpl implements CompanyMgr {
	//… 实现方法
}

其中类注解@service为Spring bean的注解标示,表示该实现bean被Spring容器托管;类注解@NaviRpcService表示暴露该实现bean为navi-rpc服务,参数serviceInterface表示该bean实现的接口,客户端可以通过rpc调用方式访问通信。

服务端可以发布只包含VO/BO/接口/异常类的sdk jar,供客户端依赖使用。详细见配置说明-第4部分

服务端异常传递详细见配置说明-第8部分

服务端启动权限控制,可以加入如下注解:

@NaviRpcAuth({ @Authority(appId = "beidou", token = "123456"), @Authority(appId = "ssp", token = "111111") })

例如,这样客户端就必须配置appId+token才可以访问。

@Service
@NaviRpcService(serviceInterface = AuthService.class)
@NaviRpcAuth({ @Authority(appId = "beidou", token = "123456"), @Authority(appId = "ssp", token = "111111") })
public class AuthServiceImpl implements AuthService {

    @Override
    public String sayHello() {
        return "hi!";
    }

}

4.2.4 基于XML方式开发服务

如上所述的CompanyMgr,用XML方式暴露服务的配置如下

配置加载到spring容器中,需要在applicationContext.xml中加入如下配置:

<!-- service bean begin -->
<bean id="companyMgr" class="com.baidu.xxx.CompanMgrImpl">
</bean>
<!-- service bean end --> 

<!-- NaviRpcExporter begin -->
<bean class="com.baidu.beidou.navi.server.NaviRpcExporter"
    scope="singleton">
    <property name="serviceInterfaceName" value="com.baidu.xxx.CompanMgr"/>
    <property name="serviceBean">
        <ref bean="companyMgr"/>
    </property>
</bean>
<!-- NaviRpcExporter end -->

两种方式目前都支持服务端代理,可以将bean交由spring管理,配置成由cglib、aspectj织入的proxy。

至此,所有服务的配置工作已经结束,不存在任何API侵入,简单易用。

4.2.5 启动服务

服务的启动方式即为常规的war启动方式,可以通过jetty或者tomcat方式启动。

启动完毕后,访问服务的如下URL:

http://${IP}:${PORT}/service_api/

可以看到该服务所暴露的所有接口:

点击任意接口可以看到该接口的定义:

点击类定义看到接口内所有的参数类型及其父类的详细信息:

点击“Zookeeper configuration”可以查看目前所有注册到zookeeper注册中心的服务、数量、机器、host信息等:

4.3 服务消费者-基于JDK动态代理的Spring集成方式【推荐】

推荐一般项目使用这种客户端调用方式,依赖服务端发布的SDK jar,可以做到只依赖其暴露的VO/BO/接口/异常信息等,减少和服务端的耦合。 使用JDK动态代理技术,发起rpc调用,JDK动态代理的对象Bean,可以交由Spring容器托管,无缝和客户端其他代码融合,便于开发管理业务逻辑。 客户端demo的所有代码可以参考navi-demo-client模块。

4.3.1 工程创建

服务消费者可以是war也可以是jar,其代码依赖的服务提供者可以是war也可以是jar,并无强者要求。

具体开发过程中,往往会存在服务依赖与服务的需求,也可以理解为war依赖于war,为了避免这种不友好的方式,我们可以通过maven编译出一个war包的sdk jar包,只包含接口和需要的VO,这样就可以做到两个模块减少耦合,界限明晰,详见配置说明

4.3.2 服务配置

在classpath中加入rpc-client.properties,默认启用zookeeper来进行服务的注册、订阅、更新操作,因此该配置文件中所有包含为*ZK*的均为zookeeper相关配置。

如果不想利用注册中心的zookeeper来订阅服务端地址,客户端可以关闭服务注册发布功能,只需要设置ENABLE_ZK_REGISTRY为false,那么就使用SERVER_LIST来直接调用服务端。

################################################
#
# 客户端是否启用zookeeper进行服务发现
# 当ZK_SERVER_LIST为空时,该值忽略直接为false
# true:启用
# false:不启用
#
################################################
ENABLE_ZK_REGISTRY=true
 
################################################
#
# zookeeper集群地址IP:PORT列表,按逗号分隔
#
################################################
ZK_SERVER_LIST=127.0.0.1:8080
 
################################################
#
# zookeeper访问控制密串,留空表示不启用权限控制
#
################################################
ZK_DIGEST_AUTH=
 
################################################
#
# 客户端订阅zookeeper的path列表,
# 可以调用多个不同的服务。
#
################################################
ZK_WATCH_NAMESPACE_PATH1=/group-service/online
ZK_WATCH_NAMESPACE_PATH2=/plan-service/online
ZK_WATCH_NAMESPACE_PATH3=/creative-service/online
 
################################################
#
# zookeeper nio连接seesion超时时间(单位ms),一般设置为30s
#
################################################
ZK_DEFAULT_SESSION_TIMEOUT_MILLS=30000
 
################################################
#
# 启动zookeeper连接时的超时时间(单位ms)
#
################################################
ZK_CONNECTION_TIMEOUT_MILLS=30000
 
################################################
#
# rpc连接超时时间(单位ms)
#
################################################
RPC_CONNECTION_TIMEOUT=6000
 
################################################
#
# rpc读超时时间(单位ms)
#
################################################
RPC_READ_TIMEOUT=30000
 
################################################
#
# rpc调用失败重试次数
#
################################################
RPC_RETRY_TIMES=2

更多关于rpc-client.properties的配置说明见配置说明

配置加载到spring容器中,需要在applicationContext.xml中加入如下配置:

<context:property-placeholder location="classpath*:/rpc-client.properties" />
 
<bean class="com.baidu.beidou.navi.conf.RpcClientConf">
    <property name="ENABLE_ZK_REGISTRY">
        <value>${ENABLE_ZK_REGISTRY}</value>
    </property>
    <property name="ZK_SERVER_LIST">
        <value>${ZK_SERVER_LIST}</value>
    </property>
    <property name="ZK_DIGEST_AUTH">
        <value>${ZK_DIGEST_AUTH}</value>
    </property>
    <property name="ZK_DEFAULT_SESSION_TIMEOUT_MILLS">
        <value>${ZK_DEFAULT_SESSION_TIMEOUT_MILLS}</value>
    </property>
    <property name="ZK_CONNECTION_TIMEOUT_MILLS">
        <value>${ZK_CONNECTION_TIMEOUT_MILLS}</value>
    </property>
    <property name="ZK_WATCH_NAMESPACE_PATHS">
        <!-- 多个服务端地址按照逗号分隔,此处表示需要利用Zookeeper监控这些服务的地址以及端口变化 -->
        <value>${ZK_WATCH_NAMESPACE_PATH1},${ZK_WATCH_NAMESPACE_PATH2},${ZK_WATCH_NAMESPACE_PATH3}</value>
    </property>
    <property name="RPC_CONNECTION_TIMEOUT">
        <value>${RPC_CONNECTION_TIMEOUT}</value>
    </property>
    <property name="RPC_READ_TIMEOUT">
        <value>${RPC_READ_TIMEOUT}</value>
    </property>
    <property name="RPC_RETRY_TIMES">
        <value>${RPC_RETRY_TIMES}</value>
    </property>
</bean>

4.3.3 基于注解方式配置服务

Spring3.0以后,XML 配置方式不再是 Spring IoC 容器的唯一配置方式,如今使用注解,配置信息的载体由 XML 文件转移到了 Java 类中。通常将用于存放配置信息的类的类名以 “Config” 结尾,比如 AppDaoConfig.java、AppServiceConfig.java 等等。需要在用于指定配置信息的类上加上 @Configuration 注解,以明确指出该类是 Bean 配置的信息源。

可以采用如下的方式配置一个Navi-rpc Config类:

@Configuration
public class RpcConfig {
    ///**************************************************************
    //
    // Navi Rpc bean proxy are listed below
    //
    //***************************************************************
    @Bean
    public CompanyMgr companyMgr() throws Exception {
        return (CompanyMgr) (new NaviProxyFactoryBean("/navi-demo-server/main/", CompanyMgr.class, NaviProtocol.PROTOSTUFF, NaviFailStrategy.FAILOVER, NaviSelectorStrategy.RANDOM).getObject());
    }
}

该类中定义了一个特殊的方法,companyMgr(),该方法的作用是当某个类需要注入名字为companyMgr的bean时,该对象即由RpcConfig类的指定companyMgr()方法提供。

提供的对象利用的JDK Proxy技术,提供远程RPC调用的底层实现。

注意到如下参数,可选

NaviProtocol.PROTOSTUFF, //表示使用protostuff序列化协议,也可以指定为protobuf协议

NaviFailStrategy.FAILOVER, //表示使用failover策略,失败重试,也可以指定为立即退出

NaviSelectorStrategy.RANDOM //表示使用随机访问,也可指定为round robin软负载均衡策略

参数的详细信息见配置说明

当不想利用服务注册中心来订阅服务地址的时候,可以去掉NaviProxyFactoryBean的构造函数的第一个参数,即可通过直连的方式调用。

4.3.4 基于XML方式配置服务

如上所述的companyMgr代理,用xml方式配置服务的配置如下,配置了两个接口服务(此处的service1实际代表某个服务暴露的接口,在Java代码层次中属于service层而已,因此通常命名为service)。 配置加载到Spring容器中,需要在applicationContext.xml中加入如下配置,protocol、failStrategy、selectorStrategy均可选:

<bean id="service1" class="com.baidu.beidou.navi.client.NaviProxyFactoryBean">
    <property name="zkNodePath">
        <value>${ZK_WATCH_NAMESPACE_PATH1}</value>
    </property>
    <property name="directCallServers">
        <value>cache.beidou.baidu.com</value>
    </property>
    <property name="serviceInterface">
        <value>com.baidu.ub.beidou.app.exporter.AppService</value>
    </property>
</bean>
 
<bean id="service2" class="com.baidu.beidou.navi.client.NaviProxyFactoryBean">
    <property name="zkNodePath">
        <value>${ZK_WATCH_NAMESPACE_PATH2}</value>
    </property>
    <property name="directCallServers">
        <value>10.57.80.44:8090,10.49.57.123:8090</value>
    </property>
    <property name="serviceInterface">
        <value>com.baidu.beidou.period.api.PeriodReportAPI</value>
    </property> 
    <property name="appId">
        <value>beidou</value>
    </property>
    <property name="token">
        <value>123456</value>
    </property>
    <property name="protocol">
        <value type="com.baidu.beidou.navi.server.protocol.NaviProtocol">PROTOBUF</value>
    </property>
    <property name="failStrategy">
        <value type="com.baidu.beidou.navi.client.selector.NaviFailStrategy">FAILOVER</value>
    </property>
    <property name="selectorStrategy">
        <value type="com.baidu.beidou.navi.client.selector.NaviSelectorStrategy">RANDOM</value>
    </property>
</bean>

至此,所有服务的配置工作已经结束,不存在任何API侵入,简单易用。

4.3.5 调用服务

在前两个小节配置的服务代理bean,可以用spring的原生方式注入到任何类中,例如通过@Resource、或者@Autowire注解

例如,一个spring的controller需要调用company接口或者所有公司信息:

@Controller
public class CompanyController extends BaseController {
	
	@Resource
	private CompanyMgr companyMgr;
	
	@RequestMapping(value = "/getAll", method = RequestMethod.GET)
	@ResponseBody
	public JsonObject getAll() {
		return buildSuccess("company", companyMgr.getAll());
	}

}

4.3.6 服务订阅

如果启用了服务注册中心,那么服务端需要将一个NaviRpcServerListListener放到spring容器中,该listern是一个zookeeper的watcher,用来订阅rpc-client.properties中指定的watched namespaces,一旦服务其下注册的service的地址发生变更,即更新客户端的地址,默认该地址放到一个客户端的cache中,异步更新无缝切换,不会阻塞调用。配置需加入到applicationContext.xml中。

如果启用注册中心,那么该listener必须添加到spring容器中。

<bean class="net.neoremind.navi.client.NaviRpcServerListListener">
</bean>

4.3.7 服务通知

当服务提供者down掉,或者切换实例IP后,那么最新的服务会将自己更新注册到zookeeper中,同时由于在上面一节配置过NaviRpcServerListListener,因此变更后的服务地址会直接更新到客户端的本地缓存中。一般断开后会经过rpc-server.propertis中定义的ZK_DEFAULT_SESSION_TIMEOUT_MILLS时间后通知到客户端。 服务更新后,将会看到客户端日志中打印如下:

2014-12-28 06:08:55.998 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
Receive watched event?WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/navi_rpc/beido
u/period-report/online/PeriodReportAPI
2014-12-28 06:08:55.998 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
********************************************************
2014-12-28 06:08:55.998 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
*     Zookeeper service registry changes detected!     *
2014-12-28 06:08:55.998 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
********************************************************
2014-12-28 06:08:55.998 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
Changed path is /navi_rpc/beidou/period-report/online/PeriodReportAPI
2014-12-28 06:08:55.998 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
Last update time:2014-12-27 23:32:22
2014-12-28 06:08:56.000 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
Zookeeper global root path is /navi_rpc
2014-12-28 06:08:56.000 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
Zookeeper watched name spaces are [/beidou/bd-service-cache/online, /beidou/period-report/online]
2014-12-28 06:08:56.009 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
======>Find 1 interfaces under service path - /navi_rpc/beidou/period-report/online
2014-12-28 06:08:56.011 [main-EventThread] INFO  c.b.beidou.navi.client.NaviRpcServerListListener -
3 servers available for /navi_rpc/beidou/period-report/online/PeriodReportAPI, server list = [10.57.
201.41:63469, 10.52.124.56:61207, 10.57.201.37:63666]

4.4 客户端调用-简单客户端方式

Object[] args = new Object[] { 88 };
SimpleNaviRpcClient client = new SimpleNaviRpcClientBuilder().setIpPort(IPPORT)
        .setServiceName("CompanyService").setConnectTimeout(5000).setReadTimeout(5000)
        .build();
Company result = client.transport("get", args);
System.out.println(result);

4.5 客户端调用-HttpClient方式

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.SimpleHttpConnectionManager;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.junit.Test;
import com.baidu.beidou.navi.codec.Codec;
import com.baidu.beidou.navi.codec.protobuf.ProtobufCodec;
public class SimpleHTTPInvocationTest {
    @Test
    public void testHttpRpc() throws Exception {
        PostMethod postMethod = null;
        try {
            //1. 构造请求体
            Codec codec = new ProtobufCodec();
            RequestDTO request = new RequestDTO();
            request.setId(123);
            request.setMethod("get");
            request.setParameters(new Object[] { 100 });
            //2. 设置调用的HTTP客户端以及填充请求体
            String serviceUrl = "http://127.0.0.1:8080/service_api/CompanyMgr";
            postMethod = new PostMethod(serviceUrl);
            postMethod.setRequestHeader("content-type", "application/baidu.protobuf");
            postMethod.setRequestEntity(new ByteArrayRequestEntity(codec.encode(RequestDTO.class, request)));
            //3. 发送请求
            HttpConnectionManagerParams connectionParams = new HttpConnectionManagerParams();
            connectionParams.setConnectionTimeout(5 * 1000);
            connectionParams.setSoTimeout(5 * 1000);
            HttpConnectionManager manager = new SimpleHttpConnectionManager();
            manager.setParams(connectionParams);
            HttpClient client = new HttpClient(manager);
            int state = client.executeMethod(postMethod);
             
            //4. 解析请求返回
            if (state == HttpStatus.SC_OK) {
                ResponseDTO response = codec.decode(ResponseDTO.class, postMethod.getResponseBody());
                System.out.println(response.getId() + "\t" + response.getStatus() + "\t" + response.getResult());
            } else {
                System.err.println("Invocation failed " + serviceUrl + ", HTTP status=" + state);
            }
        } catch (Exception e) {
            System.err.println("Failed to invoke rpc. " + e.getMessage());
            e.printStackTrace();
        } finally {
            if (postMethod != null) {
                try {
                    postMethod.releaseConnection();
                } catch (Exception e) {
                    System.err.println("Failed to release connection. " + e.getMessage());
                }
            }
        }
    }
}
 
/**
 * Request
 *
 * @author Zhang Xu
 */
public class RequestDTO {
    private long id;
    private String method;
    private Object[] parameters;
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getMethod() {
        return method;
    }
    public void setMethod(String method) {
        this.method = method;
    }
    public Object[] getParameters() {
        return parameters;
    }
    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}
 
/**
 * Response
 *
 * @author Zhang Xu
 */
public class ResponseDTO {
    private long id;
    private Object result;
    /**
     * @see com.baidu.beidou.navi.constant.NaviStatus
     */
    private int status;
    private Throwable error;
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public Object getResult() {
        return result;
    }
    public void setResult(Object result) {
        this.result = result;
    }
    public int getStatus() {
        return status;
    }
    public void setStatus(int status) {
        this.status = status;
    }
    public Throwable getError() {
        return error;
    }
    public void setError(Throwable error) {
        this.error = error;
    }
}

4.6 客户端调用-CURL方式

由于navi基于HTTP协议进行通信,因此可以支持“伪”RESTful调用,并不是严格意义上的REST,只不过可以利用HTTP这一通用的协议以及无状态特性,使客户更灵活的接入系统。下面展示了利用wget或者curl等命令进行的一次调用。

wget http://cache.beidou.baidu.com/service_api/UnionSiteService --post-data '{"method":"getAll4WebData"}'  --header 'Content-Type:application/baidu.json' -O unionSite.json
wget http://smartideaconf.beidou.baidu.com/service_api/SiConfService --post-data
 '{"method":"getTplConf", "parameters":[150001], "parameterTypes":["long"]}'  --
header 'Content-Type:application/baidu.json' -O getTplConf`date -d today +"%Y%m%
d-%H%M"`.json

请求结果格式为一段Json数据,例如:

{
    "result":
        {
            "siteurl": "wagnsp.com",
            "firstTradeId": 1,
            "secondTradeId": 101,
            "srchs": 4
        }
    ]
}

附录

服务端打印日志

对于服务器端的access日志,在独立主机上可以单独配置打印到指定的文件,navi-rpc使用slf4j为日志实现接口,以下是以logback为例的一个配置:

<appender name="accessLogger"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>../logs/access.%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>365</maxHistory>
    </rollingPolicy>
    <encoder charset="GBK">
        <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
        </pattern>
    </encoder>
    <Encoding>GBK</Encoding>
</appender>
 
<logger name="com.baidu.beidou.navi.util.AccessLogSupport" level="INFO" additivity="true">
    <appender-ref ref="accessLogger" />
</logger>

每次调用会打印如下信息:

[INFO]	2015-08-08 23:37:46,450	[pool-1-thread-1]	navi.util.AccessLogSupport	(AccessLogSupport.java:116)	-192.168.31.142	BookMgr	getAll	application/baidu.protostuff	1682566910	{"traceId":1682566910,"method":"getAll","parameters":null,"paramterTypes":[],"appId":null,"token":null}	38	0	[{"id":1,"name":"xxxx","pageNum":123,"publishDate":973872000000},{"id":1,"name":"yyyy","pageNum":123,"publishDate":973872000000},{"id":12345,"name":"zzzz","pageNum":789,"publishDate":1212076800000}]	373

日志按照tab分隔字段,含义如下:

字段位置 含义
1 客户端IP
2 服务名
3 接口名
4 序列化协议
5 trace id
6 请求信息,仅展示部分
7 请求信息大小(bytes)
8 请求结果状态码。0:成功,1:失败。通常是框架造成的,例如序列化失败等,2:系统内部错误。通常是业务逻辑造成的,不属于框架问题
9 请求结果,仅展示部分或者出错的异常信息
10 服务器处理耗时(ms)

客户端打印日志

打印日志如下,共分两条,第一条为call start,第二条为call end:

[INFO]	2015-08-08 23:37:45,954	[main]	navi.client.SimpleNaviRpcClient	(SimpleNaviRpcClient.java:322)	-Navi rpc call start - 1682566910	http://192.168.31.142:8080/service_api/BookMgr	getAll	application/baidu.protostuff	38
[INFO]	2015-08-08 23:37:46,407	[main]	navi.client.SimpleNaviRpcClient	(SimpleNaviRpcClient.java:336)	-Navi rpc call end - 1682566910	http://192.168.31.142:8080/service_api/BookMgr	getAll	application/baidu.protostuff	38	231	584

日志按照tab分隔字段,含义如下:

字段位置 含义
1 trace id
2 调用服务地址
3 接口名
4 序列化协议
5 请求信息大小(bytes)
6(optinal) 响应信息大小(bytes)
7(optinal) 从发起请求到得到响应耗时,包括服务处理时间以及网络通信,客户端接收并且反序列化(ms)