微服务的注册与发现

简介

先来回顾下整体的微服务架构

在发布微服务时,可连接 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:8080msa-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

服务发现优化方案

服务发现组件虽然基本可用,但实际上代码中还存在着大量的不足,需要我们不断优化(这部分内容后续完善)。

  1. 连接 ZooKeeper 集群环境

  2. 对服务发现的目标地址进行缓存

  3. 使服务网关具备高可用性

服务发现模式

服务发现 servicer discovery 是一种微服务架构的核心模式,它一般与服务注册模式共同使用。

服务发现模式分为两种:

  • 客户端发现 client side discovery
    • 是指服务发现机制在客户端中实现
  • 服务端发现 server side discovery
    • 服务发现机制通过一个路由中间件来实现
    • 当前实现的就是服务端发现模式

Ribbon 是一款基于 Java 的 HTTP 客户端附件,它可以查询 Eureka,将 HTTP请求路由到可用的服务接口上。

参考

  • 《架构探险—轻量级微服务架构》
posted @ 2019-01-06 19:17  ReyCG  阅读(842)  评论(0编辑  收藏  举报