標籤:

基於Docker、NodeJs實現高可用的服務發現

本文轉載來自 jasonGeng88/blog ,原文鏈接:jasonGeng88/blog

本文所有服務均採用docker容器化方式部署

當前環境

  1. Mac OS 10.11.x
  2. docker 1.12.1
  3. docker-compose 1.8.0
  4. node 6.10.2

前言

基於上一篇的 「基於Docker、Registrator、Zookeeper實現的服務自動註冊」,完成了 「服務註冊與發現」 的上半部分(即上圖中的1)。本文就來講講圖中的2、3、4、5 分別是如何實現的。

功能點

  • 服務訂閱
    • 動態獲取服務列表
    • 獲取服務節點信息(IP、Port)
  • 本地緩存
    • 緩存服務路由表
  • 服務調用
    • 服務請求的負載均衡策略
    • 反向代理
  • 變更通知
    • 監聽服務節點變化
    • 更新服務路由表

技術方案

服務發現方式

關於服務發現的方式,主要分為兩種方式:客戶端發現與服務端發現。它們的主要區別為:前者是由調用者本身去調用服務,後者是將調用者請求統一指向類似服務網關的服務,由服務網關代為調用。

這裡採用服務端發現機制,即服務網關(注意:服務網關的作用不僅僅是服務發現)。

與客戶端發現相比,可見的優勢有:

  1. 服務調用的統一管理;
  2. 減少客戶端與註冊中心不必要的連接數;
  3. 將後端服務與調用者相隔離,降低服務對外暴露的風險;

所選技術

本文採用 NodeJs 作為服務網關的實現技術。當然,這不是唯一的技術手段,像nginx+lua,php等都能實現類似的功能。我這裡採用 NodeJs 主要出於以下幾個原因:

  1. NodeJs 採用的是事件驅動、非阻塞 I/O 模型,具有天生的非同步性。在處理服務網關這種以IO密集型為主的業務時,正是 NodeJs 所擅長的。
  2. NodeJs 基於Chrome V8 引擎的 JavaScript 語言的運行環境,對於有一定 JavaScript 基礎的同學,上手相對簡單。

所有技術都有其優劣所在,NodeJs 在這裡的使用也存在一定的問題(本文最後會講述它的高可用策略):

  1. NodeJs 是基於單進程單線程的方式,這種方式存在一定的不可靠性。一旦進程崩潰,對應的服務將變得不可用;
  2. 單進程單線程方式,也導致了只能利用單核CPU。為了充分利用計算機資源,還需進行服務的水平擴展;

代碼示例

代碼地址: jasonGeng88/service_registry_discovery

代碼目錄

本文主要介紹服務發現相關實現,其他部分已在上篇中介紹過,感興趣的同學可查看上篇。

目錄結構(discovery項目)

依賴配置(package.json)

{n "name": "service-discovery",n "version": "0.0.0",n "private": true,n "scripts": {n "start": "node ./bin/www"n },n "dependencies": {n "debug": "~2.6.3",n "express": "~4.15.2",n "http-proxy": "^1.16.2",n "loadbalance": "^0.2.7",n "node-zookeeper-client": "^0.2.2"n }n}n

  • debug:用於開發調試;
  • express:作為 NodeJs 的Web應用框架,這裡主要用到了它的響應HTTP請求以及路由規則功能;
  • http-proxy:用作反向代理;
  • loadbalance:負載均衡策略,目前提供隨機、輪詢、權重;
  • node-zookeeper-client:ZK 客戶端,用作獲取註冊中心服務信息與節點監聽;

常量設置(constants.js)

"use strict";nnfunction define(name, value) {n Object.defineProperty(exports, name, {n value: value,n enumerable: truen });n}nndefine(ZK_HOSTS, ${PRIVATE_IP}:2181,${PRIVATE_IP}:2182,${PRIVATE_IP}:2183);ndefine(SERVICE_ROOT_PATH, /services);ndefine(ROUTE_KEY, services);ndefine(SERVICE_NAME, service_name);ndefine(API_NAME, api_name);n

功能點具體實現

下面會對上面提供的功能點依次進行實現(展示代碼中只保留核心代碼,詳細請見代碼

服務訂閱 - 動態獲取服務列表

var zookeeper = require(node-zookeeper-client);nvar constants = require(../constants);nvar debug = require(debug)(dev:discovery);nnvar zkClient = zookeeper.createClient(constants.ZK_HOSTS);nn/**n * 連接ZKn */nfunction connect() {n zkClient.connect();nn zkClient.once(connected, function() {n console.log(Connected to ZooKeeper.);n getServices(constants.SERVICE_ROOT_PATH);n });n}nn/**n * 獲取服務列表n */nfunction getServices(path) {n zkClient.getChildren(n path,n null,n function(error, children, stat) {n if (error) {n console.log(n Failed to list children of %s due to: %s.,n path,n errorn );n return;n }nn // 遍歷服務列表,獲取服務節點信息n children.forEach(function(item) {n getService(path + / + item);n })nn }n );n}n

服務訂閱 - 獲取服務節點信息(IP、Port)

/**n * 獲取服務節點信息(IP,Port)n */nfunction getService(path) {n zkClient.getChildren(n path,n null,n function(error, children, stat) {n if (error) {n console.log(n Failed to list children of %s due to: %s.,n path,n errorn );n return;n }n // 列印節點信息n debug(path: + path + , children is + children);n }n );n}n

本地緩存 - 緩存服務路由表

// 初始化緩存nvar cache = require(./local-storage);ncache.setItem(constants.ROUTE_KEY, {});n n/**n * 獲取服務節點信息(IP,Port)n */nfunction getService(path) {n ...n // 列印節點信息n debug(path: + path + , children is + children);nn if (children.length > 0) {n //設置本地路由緩存n cache.getItem(constants.ROUTE_KEY)[path] = children;n }n ...n}n

服務調用 - 負載均衡策略

/**n * 獲取服務節點信息(IP,Port)n */nfunction getService(path) {n ...n if (children.length > 0) {n //設置負載策略(輪詢)n cache.getItem(constants.ROUTE_KEY)[path] = loadbalance.roundRobin(children);n }n ...n}n

請求的負載均衡,本質是對路由表中請求地址進行記錄與分發。記錄:上一次請求的地址;分發:按照策略選擇接下來請求的地址。這裡為了簡便起見,將負載與緩存並在一起。

var proxy = require(http-proxy).createProxyServer({});nvar cache = require(../middlewares/local-storage);nvar constants = require("../constants");nvar debug = require(debug)(dev:reserveProxy);nn/**n * 根據headers的 service_name 與 api_name 進行代理請求n */nfunction reverseProxy(req, res, next) {n var serviceName = req.headers[constants.SERVICE_NAME];n var apiName = req.headers[constants.API_NAME];n var serviceNode = constants.SERVICE_ROOT_PATH + / + serviceName;nn debug(cache.getItem(constants.ROUTE_KEY)[serviceNode]);nn var host = cache.getItem(constants.ROUTE_KEY)[serviceNode].pick();n var url = http:// + host + apiName;n debug(The proxy url is + url);n proxy.web(req, res, {n target: urln });n}n

變更通知 - 監聽服務節點 && 更新路由緩存

/**n * 獲取服務列表n */nfunction getServices(path) {n zkClient.getChildren(n path,n // 監聽列表變化n function(event) {n console.log(Got Services watcher event: %s, event);n getServices(constants.SERVICE_ROOT_PATH);n },n function(error, children, stat) {n ...n }n );n}nn/**n * 獲取服務節點信息(IP,Port)n */nfunction getService(path) {n zkClient.getChildren(n path,n // 監聽服務節點變化n function(event) {n console.log(Got Serivce watcher event: %s, event);n getService(path);n },n function(error, children, stat) {n t ...n }n );n}n

主文件(src/app.js)

var express = require(express);nvar reverseProxy = require(./routes/reverse-proxy);nvar discovery = require(./middlewares/discovery);nvar app = express();nn// service discovery startndiscovery();nn// define the home page routenapp.get(/, function(req, res) {n res.send(This is a Service Gateway Demo)n});nn// define proxy routenapp.use(/services, reverseProxy);n

啟動腳本(src/bin/www)

腳本中含有單進程與多進程兩種啟動方式,由於 NodeJs 單進程的不可靠性,一般生產環境中採用多進程方式啟動,保證它的穩定性。

#!/usr/bin/env nodennvar app = require(../app);nvar http = require(http);nnvar port = 8080;nn//單進程運行n//http.createServer(app).listen(port);nn//多進程運行nvar cluster = require(cluster);nvar numCPUs = require(os).cpus().length;nnif (cluster.isMaster) {n console.log("master start...");nn // Fork workers.n for (var i = 0; i < numCPUs; i++) {n cluster.fork();n }nn cluster.on(listening,function(worker,address){n console.log(listening: worker + worker.process.pid +, Address: +address.address+":"+address.port);n });nn cluster.on(exit, function(worker, code, signal) {n console.log(worker + worker.process.pid + died);n cluster.fork();n });n} else if (cluster.isWorker) {n http.createServer(app).listen(port);n}n

鏡像構建

為演示方便,採用單進程方式

# DockerfilenFROM node:6.10.2nMAINTAINER jasongeng88@gmail.comnENV TZ="Asia/Shanghai" HOME="/usr/src/app"nWORKDIR ${HOME}nCOPY src/ ${HOME}/nRUN npm installnEXPOSE 8080nENTRYPOINT ["npm", "run", "start"]n

# 構建命令ndocker build -t node_discovery .n

場景演示

改動

坑:由於 docker --net=host 在 Mac 上存在問題,所以對 Registrator 做出調整。原先向註冊中心註冊的是 127.0.0.1 改為 內網IP,保證容器內可以訪問。

Linux 環境下不需要此改動,服務網關網路模式應為host。

準備工作

為了方便演示,對原先的服務模塊進行調整,提供如下服務:

服務模塊啟動如下(service_1:2個,service_2:1個)

啟動服務網關

cd discovery && docker-compose up -dn

輸出效果(docker logs -f discovery_discovery_1 看出日誌輸出):

場景1:GET方式,請求服務2,API路徑為 / ,無請求參數

場景2:GET方式,請求服務1,API路徑為 /user ,請求參數為id=1

場景3:GET方式,多次請求服務1,API路徑為 / ,查看負載均衡情況

場景4:啟停服務2實例,觀察路由表變化

// 停用 serivce_2ncd services && docker-compose stop service_2n

查看網關監聽變化:

高可用

NodeJs 自身通過 cluster 模塊,進行多進程啟動,防止單進程崩潰的不穩定性;

通過 Docker 容器化啟動,在啟動時設置restart策略,一旦服務崩潰將立即重啟;

上述的使用場景都在單機上運行,在分散式情況下,可以將 NodeJs 容器多主機部署,採用 nginx + NodeJs 的架構進行水平擴展;

總結

本文以上篇 「服務自動化註冊」 遺留的功能點開頭,講述了服務發現的2種實現方式,以及其優劣。並以 NodeJs 作為服務網關的實現手段,詳細介紹了其中各功能點的實現細節。最後通過場景代入的方式,展示了其效果。

對於網關的高可用,也通過了2種方式進行了保證。自身高可用通過多進程、失敗重啟策略進行保證;分散式下則以 nginx + NodeJs 的架構進行了保證。

文中也提到,服務發現實則只是服務網關的一個部分,服務網關還包括服務鑒權、訪問控制等。這裡的代碼僅是個Demo示例,目的是讓大家更好的看清它的本質,希望對大家有所幫助~

推薦閱讀:

Docker從入門到部署-初識Docker
如何使用OpenDroneMap對航拍圖像快速建模
基於Docker、Registrator、Zookeeper實現的服務自動註冊

TAG:Docker |