目录javascript
先来回顾下总体的微服务架构html
在发布微服务时,可链接 ZooKeeper 来注册微服务,实现“服务注册”。当浏览器发送请求后,可以使用 Node.js 充当 service gateway,处理浏览器的请求,链接 ZooKeeper,发现服务配置,实现服务发现。前端
Service Registry(服务注册表),内部拥有一个数据结构,用于存储已发布服务的配置信息。本节会使用 Spring Boot 与 Zookeeper 开发一款轻量级服务注册组件。开发以前,先要作一个简单的设计。java
首先在 Znode 树状模型下定义一个 根节点,并且这个节点是持久的。node
在根节点下再添加若干子节点,并使用服务名称做为这些子节点的名称,并称之为 服务节点。为了确保服务的高可用性,咱们可能会发布多个相同功能的服务,但因为 zookeeper 不容许存在同名的服务,所以须要再服务节点下再添加一层节点。所以服务节点则是持久的。web
服务节点下的这些子节点称为 地址节点 。每一个地址节点都对应于一个特定的服务,咱们将服务配置存放在该节点中。服务配置中可存放服务的 IP 和端口。一旦某个服务成功注册到 Zookeeper 中, Zookeeper 服务器就会与服务所在的客户端进行心跳检测,若是某个服务出现了故障,心跳检测就会失效,客户端将自动断开与服务端的会话,对应的地址节点也须要从 Znode 树状模型中移除。所以 地址节点必须是临时并且有顺序的。算法
根据上面的分析,服务注册表数据结构模型图以下所示spring
真实的服务注册实例以下:
shell
由上图可见,只有地址节点才有数据,这些数据就是每一个服务的配置信息,即 IP 与端口,并且地址节点是临时且顺序的,根节点与服务节点都是持久的。express
下面会根据这个设计思路,实现服务注册表的相关细节。可是在开发具体细节以前,咱们先搭建一个代码框架。手续爱你咱们须要建立两个项目,分别是:
msa-sample-api
用于存放服务 API 代码,包含服务定义相关细节。msa-framework
存放框架性代码,包含服务注册表细节定义好项目后,就须要再 msa-sample-api
项目中编写服务的业务细节,在 msa-framework
项目中完成服务注册表的具体实现。
在 msa-sample-api
项目中搭建 Spring Boot 应用程序框架,建立一个名为 HelloApplication
的类,该类包含一个 hello()
方法,用于处理 GET:/hello
请求。
package demo.msa.sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @SpringBootApplication public class HelloApplication { public static void main(String[] args) { SpringApplication.run(HelloApplication.class, args); } @RequestMapping (method= RequestMethod.GET, path = "/hello") public String hello() { return "hello"; } }
随后,在 application.properties
文件中添加以下配置项
server.port=8080 spring.application.name=msa-sample-api registry.zk.servers=127.0.0.1:2181
之因此设置 spring.application.name
配置项,是由于咱们正好将其做为服务名称来使用。registry.zk.servers
配置项表示服务注册表的 IP 与端口,实际上就是 Zookeeper 的链接字符串。若是链接到 Zookeeper 集群环境,就可使用逗号来分隔多个 IP 与端口,例如: ip1:port,ip2:port,ip3:port
。
最后配置 maven 依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>demo.msa</groupId> <artifactId>msa-sample-api</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>msa-sample</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>demo.msa</groupId> <artifactId>msa-framework</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
服务注册表接口用于注册相关服务信息,包括
在 msa-framework
项目中建立一个名为 ServiceRegistry
的 Java 接口类,代码以下:
package demo.msa.framework.registry; public interface ServiceRegistry { /** * 注册服务信息 * @param serviceName 服务名称 * @param serviceAddress 服务地址 */ void register(String serviceName, String serviceAddress); }
下面来实现 ServiceRegistry
接口,它会经过 ZooKeeper 客户端建立响应的 ZNode 节点,从而实现服务注册。
在 msa-framework
中建立一个 ServiceRegistry
的实现类 ServiceRegistryImpl
。同时还须要实现 ZooKeeper 的 Watch 接口,便于监控 SyncConnected
事件,以链接 ZooKeeper 客户端。
package demo.msa.framework.registry; import java.util.concurrent.CountDownLatch; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.ZooKeeper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ServiceRegistryImpl implements ServiceRegistry, Watcher { private static final String REGISTRY_PATH = "/registry"; private static final int SESSION_TIMEOUT = 5000; private static final Logger logger = LoggerFactory.getLogger(ServiceRegistryImpl.class); private static CountDownLatch latch = new CountDownLatch(1); private ZooKeeper zk; public ServiceRegistryImpl() { // TODO Auto-generated constructor stub } public ServiceRegistryImpl(String zkServers) { try { // 建立 zookeeper zk = new ZooKeeper(zkServers, SESSION_TIMEOUT, this); latch.await(); logger.debug("connect to zookeeper"); } catch (Exception ex) { logger.error("create zk client fail", ex); } } @Override public void register(String serviceName, String serviceAddress) { try { // 建立根节点(持久节点) if (zk.exists(REGISTRY_PATH, false) == null) { zk.create(REGISTRY_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); logger.debug("create registry node: {}", REGISTRY_PATH); } // 建立服务节点 (持久节点) String servicePath = REGISTRY_PATH + "/" + serviceName; if (zk.exists(servicePath, false) == null) { zk.create(servicePath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); logger.debug("create registry node: {}", REGISTRY_PATH); } // 建立地址节点 (临时有序节点) String addresspath = servicePath + "/address-"; String addressNode = zk.create(addresspath, serviceAddress.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); logger.debug("create address node: {} => {}", addressNode, serviceAddress); if (zk.exists(REGISTRY_PATH, false) == null) { zk.create(REGISTRY_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); logger.debug("create registry node: {}", REGISTRY_PATH); } String servicePath = REGISTRY_PATH + "/" + serviceName; if (zk.exists(servicePath, false) == null) { zk.create(servicePath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); logger.debug("create registry node: {}", REGISTRY_PATH); } String addresspath = servicePath + "/address-"; String addressNode = zk.create(addresspath, serviceAddress.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); logger.debug("create address node: {} => {}", addressNode, serviceAddress); } catch(Exception ex) { logger.error("create node fail", ex); } } @Override public void process(WatchedEvent event) { if (event.getState() == Event.KeeperState.SyncConnected) { latch.countDown(); } } }
使用 ZooKeeper 的客户端 API, 很容易建立 ZNode 节点,只是在调用节点以前有必要调用 exists()
方法,判断将要建立的的节点是否已经存在。须要注意, 根节点和服务节点都是持久节点 ,只有地址节点是临时有序节点。而且有必要在建立节点完成后输出一些调试信息,来获知节点是否建立成功了。
咱们的指望是,当 HelloApplication
程序启动时,框架会将其服务器 IP 与端口注册到服务注册表中。实际上,在 ZooKeeper 的 ZNode 树状模型上将建立 /registry/msa-sample-api/address-0000000000
节点,该节点所包含的数据为 127.0.0.1:8080
。msa-framework
项目则封装了这些服务注册行为,这些行为对应用端彻底透明,对 ServiceRegistry
接口而言,则须要在框架中调用 register()
方法,并传入 serviceName
参数(/registry/msa-sample-api/address-0000000000
)与 serviceAddress
参数(127.0.0.1:8080
)。
接下来要作的就是经过编写 Spring 的 @configuration
配置类来建立 ServiceRegistry
对象,并调用 register()
方法。具体代码以下:
package demo.msa.sample.config; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import demo.msa.framework.registry.ServiceRegistry; import demo.msa.framework.registry.ServiceRegistryImpl; @Configuration public class RegistryConfig { @Value("${registry.zk.servers}") private String servers; @Value("${server.port}") private int serverPort; @Value("${spring.application.name}") private String serviceName; @Bean public ServiceRegistry serviceRegistry() { ServiceRegistry serverRegistry = new ServiceRegistryImpl(servers); String serviceAdress = getServiceAddress(); serverRegistry.register(serviceName, serviceAdress); return serverRegistry; } private String getServiceAddress() { InetAddress localHost = null; try { localHost = Inet4Address.getLocalHost(); } catch (UnknownHostException e) { } String ip = localHost.getHostAddress(); return ip + ":" + serverPort; } }
其中,getServiceAddress
方法用来获取服务运行的本机地址和端口。
此时,服务注册组件已经基本开发完毕,此时可启动 msa-sample-api
应用程序,并经过命令客户端来观察 ZooKeeper 的 ZNode 节点信息。经过下面命令链接到 ZooKeeper 服务器,并观察注册表中的数据结构:
$ bin/zkCli.sh
服务注册表数据结构以下所示:
[zk: localhost:2181(CONNECTED) 4] ls /registry/msa-sample-api [address-0000000001] [zk: localhost:2181(CONNECTED) 5] get /registry/msa-sample-api/address-0000000001 127.0.0.1:8080 cZxid = 0x79 ctime = Sun Jan 06 18:22:18 CST 2019 mZxid = 0x79 mtime = Sun Jan 06 18:22:18 CST 2019 pZxid = 0x79 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x16817f3391b002c dataLength = 16 numChildren = 0
服务注册 (Service Registry) 是一种微服务架构的核心模式,咱们能够在微服务网站上了解它的详细内容。
Service Registry 模式: https://microservices.io/patterns/service-registry.html
有两种服务注册模式
除了 ZooKeeper,还有一些其余的开源服务注册组件,好比 Eureka, Etcd, Consul 等。
服务发现组件在微服务架构中由 Service Gateway(服务网关)提供支持,前端发送的 HTTP 请求首先会进入服务网关,此时服务网关将从服务注册表中获取当前可用服务对应的服务配置,随后将经过 反向代理技术 调用具体的服务。像这样获取可用服务配置的过程称为 服务发现。服务发现是整个微服务架构中的 核心组件,该组件不只须要 高性能,还要支持 高并发,还需具有 高可用。
当咱们启动多个 msa-sample-api
服务(调整为不一样的端口)时,会在服务注册表中注册以下信息:
/registry/msa-sample-api/address-0000000000 => 127.0.0.1:8080 /registry/msa-sample-api/address-0000000001 => 127.0.0.1:8081 /registry/msa-sample-api/address-0000000002 => 127.0.0.1:8082
以上结构表示同一个 msa-sample-api
服务节点包含 3 个地址节点,每一个地址节点都包含一组服务配置(IP 和端口)。咱们的目标是,经过服务节点的名称来获取其中某个地址节点所对应的服务配置。最简单的作法是随机获取一个地址节点,固然能够根据 轮询 或者 哈希 算法来获取地址节点。
所以,要实现以上过程,咱们必须得知服务节点的名称是什么,也就是服务名称是什么,能够经过服务名称来获取服务配置,那么,如何获取服务名称呢?
当服务网关接收 HTTP 请求时,咱们可以很轻松的获取请求的相关信息,最容易获取服务名称的地方就是请求头,咱们不妨 添加一个名为 Service-Name 的自定义请求头,用它来定义服务名称,随后可在服务网关中获取该服务名称,并在服务注册表中根据服务名称来获取对应的服务配置。
咱们再建立一个项目,名为 msa-service-gateway
,它至关于整个微服务架构中的前端部分,其中包括一个服务发现框架。至于测试请求,可使用 firefox 插件 RESTClient 来完成。
项目msa-service-gateway
包含两个文件
app.js
:服务网关应用程序,经过 Node.js 来实现package.json
用于存放 Node.js 的基本信息,以及所依赖的 NPM 模块。首先在 package.json
文件中添加代码
{ "name": "msa-service-gateway", "version": "1.0.0", "dependencies": { } }
实现服务发现,须要安装 3 个模块,分别是
使用下面命令来依次安装它们
npm install express -save npm install node-zookeeper-client -save npm install http-proxy -save
app.js 的代码以下所示
var express = require('express') var zookeeper = require('node-zookeeper-client') var httpProxy = require('http-proxy') var REGISTRY_ROOT = '/registry'; var CONNECTION_STRING = '127.0.0.1:2181'; var PORT = 1234; // 链接 zookeeper var zk = zookeeper.createClient(CONNECTION_STRING); zk.connect(); // 建立代理服务器对象并监听错误事件 var proxy = httpProxy.createProxyServer() proxy.on('error', function(err, req, res) { res.end(); }) var app = express(); // 拦截全部请求 app.all('*', function (req, res) { // 处理图标请求 if (req.path == '/favicon.ico') { res.end(); return; } // 获取服务名称 var serviceName = req.get('Service-Name'); console.log('serviceName: %s', serviceName); if (!serviceName) { console.log('Service-Name request header is not exist'); res.end(); return } // 获取服务路径 var servicePath = REGISTRY_ROOT + '/' + serviceName; console.log('serviceName: %s', servicePath) // 获取服务路径下的地址节点 zk.getChildren(servicePath, function (error, addressNodes) { if (error) { console.log(error.stack); res.end(); return; } var size = addressNodes.length; if (size == 0) { console.log('address node is not exist'); res.end(); return; } // 生成地址路径 var addressPath = servicePath + '/'; if (size === 1) { // 若是只有一个地址,则获取该地址 addressPath += addressNodes[0]; } else { // 若存在多个地址,则随机获取一个地址 addressPath += addressNodes[parseInt(Math.random()*size)] } console.log('addressPath: %s', addressPath) zk.getData(addressPath, function(error, serviceAddress) { if (error) { console.log(error.stack); res.end(); return; } console.log('serviceAddress: %s', serviceAddress) if (!serviceAddress) { console.log('service address is not exist') res.end() return } proxy.web(req, res, { target: 'http://' + serviceAddress }); }) }) }); app.listen(PORT, function() { console.log('server is running at %d', PORT) })
使用下面命令启动 web server:
$ node app.js
此时,使用 firefox 插件 RESTClient 向地址 http://localhost:1234/hello
发送请求,记得要配置 HTTP 头字段 Service-Name=msa-sample-api
。能够获取到结果 hello
。
在 Node.js 控制台能够看到以下输出结果。
$ node app.js server is running at 1234 serviceName: msa-sample-api serviceName: /registry/msa-sample-api addressPath: /registry/msa-sample-api/address-0000000001 serviceAddress: 127.0.0.1:8080
服务发现组件虽然基本可用,但实际上代码中还存在着大量的不足,须要咱们不断优化(这部份内容后续完善)。
链接 ZooKeeper 集群环境
对服务发现的目标地址进行缓存
使服务网关具有高可用性
服务发现 servicer discovery 是一种微服务架构的核心模式,它通常与服务注册模式共同使用。
服务发现模式分为两种:
Ribbon 是一款基于 Java 的 HTTP 客户端附件,它能够查询 Eureka,将 HTTP请求路由到可用的服务接口上。