1)当服务越来越多时,服务URL配置管理变得非常困难,F5硬件负载均衡器的单点压力也越来越大。
2) 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。
3) 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?
我们迫切需要解决的问题:需要有一个注册中心,动态的注册和发现服务,使服务的位置透明。
分布式的特点和概念
- 是一种远程接口的调用
- 集群:在不同的服务器上部署相同的服务模块,通过负载均衡设备进行调度。
- 分布式:在不同的服务器上部署不同的服务模块,通过RPC/RMI或者自有的内部机制进行调度。
- 分布式集群:将某一个服务进行集群化
- 有利于开发速度,降低成本
- 网络延迟等,调试程序不太方便,对开发人员的要求比较高,抽取服务模块的时候不知道抽取成哪些模块
分布式的结构
传统模式
服务太多,调用太多的时候,URL/IP地址可能要配置到程序中的properties文件中,如果对方的地址变更(宕机)都需要手工处理。
注册中心模式
将提供服务的一端(provider)地址都统一维护到一个注册中心中,调用它的一方消费者(consumer)在每次调用的时候到注册中心去获取,当提供者地址更改或者宕机的时候,这个更改对消费者来说是透明的,即消费者不需要知道提供者具体的地址,具体的地址由注册中心分配。
认识dubbo和zookeeper
Dubbo
1 RPC框架:远程调用框架,服务通信(RPC)和服务管理两部分必不可少
2 功能:远程调用、注册中心、宕机检测、恢复后自动识别、负载均衡(软负载均衡,特点:成本比较低,硬件负载均衡:F5服务器)、统计功能(dubbo-monitor),管控台(dubbo-admin),其中后2者不是必须的,主要给运维使用。
3 依赖JDK和spring
Zookeeper
- apache软件基金会维护,是hadoop的一个组件,用的非常广泛
- 它也有注册中心的基本特点,例如:远程调用、注册中心、宕机检测、恢复后自动识别、负载均衡。
- 运行的时候依赖JDK
dubbo+zookeeper模型
zookeeper的安装和启动
zookeeper的依赖环境
Jdk 1.7
zookeeper的安装过程
[root@iZ2ze8y50ep4lui61ttg4pZ local]# wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.10/zookeeper-3.4.10.tar.gz
[root@iZ2ze8y50ep4lui61ttg4pZ local]# tar -zxvf zookeeper-3.4.10.tar.gz
[root@iZ2ze8y50ep4lui61ttg4pZ conf]# cp zoo_sample.cfg zoo.cfg
[root@iZ2ze8y50ep4lui61ttg4pZ conf]# cd ..
[root@iZ2ze8y50ep4lui61ttg4pZ zookeeper-3.4.10]# cd bin
[root@iZ2ze8y50ep4lui61ttg4pZ bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper-3.4.10/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
启动成功后可以看到如上内容
- 在任何一个目录下启动zkServer.sh
需要按照自己的实际目录配置/etc/profile环境变量,内容类似为:
如果远程连接(provider/consumer)需要打开2181端口。在防火墙添加相应的命令即可,如果使用的是ECS服务器,则在安全策略中打开该端口。
改造传统开发工程的思路和开发准备
1 什么是provider?provider都有哪组成部分?
通常拆分service层,把接口和接口的实现拆分成2个工程,POJO、DAO不需要改动。
2 什么是consumer?Consumer都有哪些组成部分?
consumer就是web层,即之前的controller层,即它的变化是多种多样的,例如OA可能要调用service用户身份认证,一个EB商城系统也可能调用service层的身份认证。
3 provider/consumer改造之后的变化?
服务器的使用
- zookeeper这台需要开放2181端口
- providerA/providerB需要开放20880端口
- consumer如果远程连接provider的话,provider主机还需要修改/etc/hosts文件
开发我的第一个分布式应用系统
- 创建父工程,并在父工程中依次创建pojo,dao,api,service,controller等工程,父工程聚合这些子工程
- 添加依赖
添加工程之间的依赖,详情参考上面的分析图
3 导入第三方依赖
- 在父工程中导入常用的第三方jar包,子工程自动依赖
- 注意:servlet.jar,servlet-jsp.jar等单独放到web工程中
- 配置service工程
- 配置pom.xml文件以便能够打出可以执行的jar包及其依赖,见service工程的pom.xml文件。
- 配置spring.xml文件
写程序
见各个程序和pom.xml/spring.xml等配置文件
配置crmweb工程的web.xml、spring.xml和springmvc.xml
web.xml中只需要配置spring和springmvc部分
spring.xml内容如下:
接口包名必须跟service中interface保持一一对应(一致),否则提示找不到provider,id不要求一致
发布我的第一个SOA分布式应用程序
- 服务器环境的准备
开启zk
[root@iZwz922gt8apce892igiynZ bin]# ./zkServer.sh start
上传provider程序
在provider服务器的/usr/local目录下简历crm目录,将工程service中的 lib和provider.jar上传到该目录
备注:程序可以上传到任何一个目录,不影响运行结果,但必须保证lib和provider.jar在同一级目录。
启动provider程序
[root@iZ2ze8y50ep4lui61ttg4pZ local]# cd crm
[root@iZ2ze8y50ep4lui61ttg4pZ crm]# java -jar provider.jar &
[root@iZ2ze8y50ep4lui61ttg4pZ crm]# log4j:WARN No appenders could be found for logger (com.alibaba.dubbo.common.logger.LoggerFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
[2018-03-31 15:08:46] Dubbo service server started!
启动consumer端
分别运行http://localhost:8080/crmweb/setname?name=zhangsanfeng
和http://localhost:8080/crmweb/getusers查看调用效果
[2018-03-31 15:08:46] Dubbo service server started!
客户端传过来的名称为:张三丰
客户端传过来的名称为:张三丰
客户端传过来的名称为:张三丰
客户端传过来的名称为:张三丰
客户端传过来的名称为:张三丰
客户端传过来的名称为:张三丰
- 必须保证provider开启20880端口
- provider端的日志打印在ECS控制台,consumer端的日志打印在eclipse控制台或者tomcat控制台。
基于dubbo的分布式系统的运行机理和启动日志分析
- Provider.jar包启动的原理和过程
在provider.jar包中跟目录下有一个MANIFEST.MF文件,该文件内容大致为:
Built-By: HP
Build-Jdk: 1.7.0_72
Main-Class: com.alibaba.dubbo.container.Main
Class-Path: . lib/crmdao-0.0.1-SNAPSHOT.jar lib/crmpojo-0.0.1-SNAPSHOT
.jar lib/crmapi-0.0.1-SNAPSHOT.jar lib/dubbo-2.5.8.jar lib/spring-con
text-4.3.10.RELEASE.jar lib/spring-beans-4.3.10.RELEASE.jar lib/sprin
g-web-4.3.10.RELEASE.jar lib/javassist-3.20.0-GA.jar lib/netty-3.2.5.
Final.jar lib/zkclient-0.10.jar lib/slf4j-api-1.6.1.jar lib/zookeeper
-3.4.8.jar lib/slf4j-log4j12-1.6.1.jar
其中main-class指定了正序的入口,也就是交给dubbo的Main类去启动,这个类默认会读取/spring/spring.xml配置文件。当读到配置文件中 <dubbo>标签时会交给DubboNamespaceHandler去处理,由它来解析xml配置文件,并将解析的内容封装到一个对象中,发送给ZK.
dubbo的容器
使用Main类启动程序的话,可以实现“优雅关机”
- consumer端启动原理和过程
基本过程跟provider端差不多,唯一不同的是consmer一般由tomcat等web容器来启动,通过web.xml里面配置spring.xml来完成spring容器的初始化工作。可看到,两端工作的核心都是spring容器的初始化。
- consumer和provider和zookeeper的通信过程
通信过程图
- 启动过程的日志分析
启动zookeeper时:
正常日志
启动provider时
——provider端的日志如下:
[DUBBO] Export dubbo service cn.crm.api.UserApi to url dubbo://192.168.187.173:20880/cn.crm.api.UserApi?anyhost=true&application=dubboprovider&bind.ip=192.168.187.173&bind.port=20880&dubbo=2.5.8&generic=false&interface=cn.crm.api.UserApi&methods=setName,getUsers&pid=9516&revision=0.0.1-SNAPSHOT&side=provider×tamp=1522804354272, dubbo version: 2.5.8, current host: 127.0.0.1
[DUBBO] Notify urls for subscribe url provider://192.168.187.173:20880/cn.crm.api.UserApi?anyhost=true&application=dubboprovider&category=configurators&check=false&dubbo=2.5.8&generic=false&interface=cn.crm.api.UserApi&methods=setName,getUsers&pid=9516&revision=0.0.1-SNAPSHOT&side=provider×tamp=1522804354272
——zookeeper端的日志如下:
2018-04-03 14:01:01,346 [myid:] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:58885
2018-04-03 14:01:01,353 [myid:] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:ZooKeeperServer@942] - Client attempting to establish new session at /127.0.0.1:58885
2018-04-03 14:01:01,359 [myid:] - INFO [SyncThread:0:ZooKeeperServer@687] - Established session 0x1628a1781e60000 with negotiated timeout 30000 for client /127.0.0.1:58885
启动consumer时:
——zookeeper端的日志如下:
[NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52429
[NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:ZooKeeperServer@942] - Client attempting to establish new session at /127.0.0.1:52429
[SyncThread:0:ZooKeeperServer@687] - Established session 0x1628e3fd60a0002 with negotiated timeout 30000 for client /127.0.0.1:52429
——provider端日志如下:
无打印
- 关闭provider时:
——comsumer端日志如下:
com.alibaba.dubbo.remoting.RemotingException: client(url: dubbo://192.168.187.173:20880/cn.crm.api.UserApi?anyhost=true&application=dubboconsumer&check=false&codec=dubbo&dubbo=2.5.8&generic=false&heartbeat=60000&interface=cn.crm.api.UserApi&methods=setName,getUsers&pid=188®ister.ip=192.168.187.173&remote.timestamp=1522804991189&revision=0.0.1-SNAPSHOT&side=consumer×tamp=1522805139189) failed to connect to server 192.168.187.173:20880, error message is:Connection refused: no further information
at com.alibaba.dubbo.remoting.transport.netty.NettyClient.doConnect(NettyClient.java:124)
——zookeeper端日志如下:
WARN [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@373] - Exception causing close of session 0x1628e3fd60a0000 due to java.io.IOException: 远程主机强迫关闭了一个现有的连接。
重启provider时:
——comsumer端日志如下:
[ZkClient-EventThread-18-127.0.0.1:2181] INFO com.alibaba.dubbo.remoting.transport.AbstractClient - [DUBBO] Successed connect to server /192.168.187.173:20880 from NettyClient 192.168.187.173 using dubbo version 2.5.8, channel is NettyChannel [channel=[id: 0x4d912f56, /192.168.187.173:52775 => /192.168.187.173:20880]], dubbo version: 2.5.8, current host: 192.168.187.173
——zookeeper端日志如下:
[SyncThread:0:ZooKeeperServer@687] - Established session 0x1628e46335e0001 with negotiated timeout 30000 for client /127.0.0.1:52774
[ProcessThread(sid:0 cport:2181)::PrepRequestProcessor@648] - Got user-level KeeperException when processing sessionid:0x1628e46335e0001 type:create cxid:0x4 zxid:0xd8 txntype:-1 reqpath:n/a Error Path:/dubbo/cn.crm.api.UserApi/configurators Error:KeeperErrorCode = NodeExists for /dubbo/cn.crm.api.UserApi/configurators
同理,重启comsumer 也有类似上述的日志。
关闭zookeeper时
——comsumer和provider端日志如下:
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
at sun.nio.ch.SocketDispatcher.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
java.net.ConnectException: Connection refused: no further information
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:739)
at org.apache.zookeeper.ClientCnxnSocketNIO.doTransport(ClientCnxnSocketNIO.java:361)
at org.apache.zookeeper.ClientCnxn$SendThread.run(ClientCnxn.java:1141)
上述日志一直处于打印状态。
- 总结
- provider,consumer,zookeeper三者之间是可以相互感知的,即对方的启动、关闭另一方都会获取到他的状态信息(打印日志可以看出),因此可以认为“互联互通”,但是provider并不是跟consumer直连的,而是provider状态的改变通过ZK推送给的consumer。他们之间的连接是一个长连接。
- provider,consumer,zookeeper三者之间又是相互独立的,服务可以自动发现。
- 如果注册中心宕机了,重新启动之后,provider/consumer会去重新注册的。
- provider.jar重复启动就会报端口占用的错误,可以修改dubbo端口后再执行。
- 提供者provider,是一个无状态的java应用,无状态,即不需要进行类似web中session信息的保留,无状态的应用特别适合宕机切换。
- 消费者consumer,通常是一个有状态的web应用。可见,在provider这端不要进行有关session,request的处理——因为必须得有web容器(例如tomcat等)的支持。如果必须要传递session中的内容的话,可以封装成为一个实现了序列化接口的类对象,传递给provider进行处理。
- 从provider/consumer日志可看出,它们第一次连接zookeeper的过程基本相同。
- 第一次请求consumer和再次请求consumer过程对比:第一次运行consumer程序的时候,创建了consumer到zk和dubbo的连接,consumer得到provider返回结果的时间是10649毫秒。第二次运行consumer程序的时候,不再创建consumer到zk和dubbo的连接,consumer得到provider返回结果的时间是875毫秒。可见以后再请求的时候就不会再创建,并且是通过netty创建的一个长连接,这也是为什么第一次请求consumer比较耗时的原因。另外,第一次请求和第二次请求,tomcat后台打印的日志不一样,从日志也可以看出请求的过程。