目錄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請求路由到可用的服務接口上。