目录
简介
先来回顾下整体的微服务架构
在发布微服务时,可连接 ZooKeeper 来注册微服务,实现“服务注册”。当浏览器发送请求后,可使用 Node.js 充当 service gateway,处理浏览器的请求,连接 ZooKeeper,发现服务配置,实现服务发现。
实现服务注册组件
Service Registry(服务注册表),内部拥有一个数据结构,用于存储已发布服务的配置信息。本节会使用 Spring Boot 与 Zookeeper 开发一款轻量级服务注册组件。开发之前,先要做一个简单的设计。
设计服务注册表数据结构
首先在 Znode 树状模型下定义一个 根节点,而且这个节点是持久的。
在根节点下再添加若干子节点,并使用服务名称作为这些子节点的名称,并称之为 服务节点。为了确保服务的高可用性,我们可能会发布多个相同功能的服务,但由于 zookeeper 不允许存在同名的服务,因此需要再服务节点下再添加一层节点。因此服务节点则是持久的。
服务节点下的这些子节点称为 地址节点 。每个地址节点都对应于一个特定的服务,我们将服务配置存放在该节点中。服务配置中可存放服务的 IP 和端口。一旦某个服务成功注册到 Zookeeper 中, Zookeeper 服务器就会与服务所在的客户端进行心跳检测,如果某个服务出现了故障,心跳检测就会失效,客户端将自动断开与服务端的会话,对应的地址节点也需要从 Znode 树状模型中移除。因此 地址节点必须是临时而且有顺序的。
根据上面的分析,服务注册表数据结构模型图如下所示
真实的服务注册实例如下:
由上图可见,只有地址节点才有数据,这些数据就是每个服务的配置信息,即 IP 与端口,而且地址节点是临时且顺序的,根节点与服务节点都是持久的。
下面会根据这个设计思路,实现服务注册表的相关细节。但是在开发具体细节之前,我们先搭建一个代码框架。手续爱你我们需要创建两个项目,分别是:
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>
定义服务注册表接口
服务注册表接口用于注册相关服务信息,包括
- 服务名称
- 服务地址包括
- 服务所在机器的 IP
- 服务所在机器的端口
在 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 节点,从而实现服务注册。
使用 ZooKeeper 实现服务注册
在 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 个模块,分别是
- express : web Server 应用框架
- node-zookeeper-client: node.js zooKeeper 客户端
- http-proxy : 代理模块
使用下面命令来依次安装它们
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 是一种微服务架构的核心模式,它一般与服务注册模式共同使用。
服务发现模式分为两种:
- 客户端发现 client side discovery
- 是指服务发现机制在客户端中实现
- 服务端发现 server side discovery
- 服务发现机制通过一个路由中间件来实现
- 当前实现的就是服务端发现模式
Ribbon 是一款基于 Java 的 HTTP 客户端附件,它可以查询 Eureka,将 HTTP请求路由到可用的服务接口上。
参考
- 《架构探险—轻量级微服务架构》