You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by me...@apache.org on 2020/05/14 15:20:53 UTC

[incubator-apisix] branch master updated: feature: support discovery center (#1440)

This is an automated email from the ASF dual-hosted git repository.

membphis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new e7d19ec  feature: support discovery center (#1440)
e7d19ec is described below

commit e7d19ec160f1fc26aa5aa3b99a90cb53cc6ab91b
Author: qiujiayu <15...@qq.com>
AuthorDate: Thu May 14 23:20:44 2020 +0800

    feature: support discovery center (#1440)
---
 .travis/linux_openresty_runner.sh            |   2 +
 .travis/linux_tengine_runner.sh              |   2 +
 .travis/osx_openresty_runner.sh              |   2 +-
 Makefile                                     |   3 +
 apisix/balancer.lua                          |  96 ++++---
 apisix/core/table.lua                        |   3 +-
 apisix/discovery/eureka.lua                  | 253 +++++++++++++++++
 t/apisix.luacov => apisix/discovery/init.lua |  34 +--
 apisix/http/service.lua                      |  36 ++-
 apisix/init.lua                              | 109 +++----
 apisix/router.lua                            |  48 +++-
 apisix/schema_def.lua                        |  45 ++-
 conf/config.yaml                             |  13 +-
 doc/discovery-cn.md                          | 253 +++++++++++++++++
 doc/discovery.md                             | 244 ++++++++++++++++
 doc/images/discovery-cn.png                  | Bin 0 -> 42581 bytes
 doc/images/discovery.png                     | Bin 0 -> 46310 bytes
 t/admin/balancer.t                           |   2 +
 t/admin/routes-array-nodes.t                 | 126 +++++++++
 t/admin/services-array-nodes.t               | 116 ++++++++
 t/admin/upstream-array-nodes.t               | 409 +++++++++++++++++++++++++++
 t/admin/upstream.t                           |   6 +-
 t/apisix.luacov                              |   1 +
 t/discovery/eureka.t                         | 131 +++++++++
 t/node/upstream-array-nodes.t                | 214 ++++++++++++++
 25 files changed, 2015 insertions(+), 133 deletions(-)

diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh
index 117ee36..ea5dce9 100755
--- a/.travis/linux_openresty_runner.sh
+++ b/.travis/linux_openresty_runner.sh
@@ -44,6 +44,8 @@ before_install() {
     docker network create kafka-net --driver bridge
     docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0
     docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest
+    docker pull bitinit/eureka
+    docker run --name eureka -d -p 8761:8761 --env ENVIRONMENT=apisix --env spring.application.name=apisix-eureka --env server.port=8761 --env eureka.instance.ip-address=127.0.0.1 --env eureka.client.registerWithEureka=true --env eureka.client.fetchRegistry=false --env eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/ bitinit/eureka
     sleep 5
     docker exec -it kafka-server1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server:2181 --replication-factor 1 --partitions 1 --topic test2
 }
diff --git a/.travis/linux_tengine_runner.sh b/.travis/linux_tengine_runner.sh
index e246c7a..74aa909 100755
--- a/.travis/linux_tengine_runner.sh
+++ b/.travis/linux_tengine_runner.sh
@@ -45,6 +45,8 @@ before_install() {
     docker network create kafka-net --driver bridge
     docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0
     docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest
+    docker pull bitinit/eureka
+    docker run --name eureka -d -p 8761:8761 --env ENVIRONMENT=apisix --env spring.application.name=apisix-eureka --env server.port=8761 --env eureka.instance.ip-address=127.0.0.1 --env eureka.client.registerWithEureka=true --env eureka.client.fetchRegistry=false --env eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/ bitinit/eureka
     sleep 5
     docker exec -it kafka-server1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server:2181 --replication-factor 1 --partitions 1 --topic test2
 }
diff --git a/.travis/osx_openresty_runner.sh b/.travis/osx_openresty_runner.sh
index 1cfce27..0f60eb9 100755
--- a/.travis/osx_openresty_runner.sh
+++ b/.travis/osx_openresty_runner.sh
@@ -43,7 +43,7 @@ do_install() {
     git clone https://github.com/iresty/test-nginx.git test-nginx
 
     wget -P utils https://raw.githubusercontent.com/openresty/openresty-devel-utils/master/lj-releng
-	chmod a+x utils/lj-releng
+    chmod a+x utils/lj-releng
 
     wget https://github.com/iresty/grpc_server_example/releases/download/20200314/grpc_server_example-darwin-amd64.tar.gz
     tar -xvf grpc_server_example-darwin-amd64.tar.gz
diff --git a/Makefile b/Makefile
index 1ecc307..66b44cc 100644
--- a/Makefile
+++ b/Makefile
@@ -133,6 +133,9 @@ install:
 	$(INSTALL) -d $(INST_LUADIR)/apisix/http/router
 	$(INSTALL) apisix/http/router/*.lua $(INST_LUADIR)/apisix/http/router/
 
+	$(INSTALL) -d $(INST_LUADIR)/apisix/discovery
+	$(INSTALL) apisix/discovery/*.lua $(INST_LUADIR)/apisix/discovery/
+
 	$(INSTALL) -d $(INST_LUADIR)/apisix/plugins
 	$(INSTALL) apisix/plugins/*.lua $(INST_LUADIR)/apisix/plugins/
 
diff --git a/apisix/balancer.lua b/apisix/balancer.lua
index a5134bc..db43af5 100644
--- a/apisix/balancer.lua
+++ b/apisix/balancer.lua
@@ -16,6 +16,7 @@
 --
 local healthcheck = require("resty.healthcheck")
 local roundrobin  = require("resty.roundrobin")
+local discovery   = require("apisix.discovery.init").discovery
 local resty_chash = require("resty.chash")
 local balancer    = require("ngx.balancer")
 local core        = require("apisix.core")
@@ -23,6 +24,7 @@ local error       = error
 local str_char    = string.char
 local str_gsub    = string.gsub
 local pairs       = pairs
+local ipairs      = ipairs
 local tostring    = tostring
 local set_more_tries   = balancer.set_more_tries
 local get_last_failure = balancer.get_last_failure
@@ -42,30 +44,37 @@ local lrucache_checker = core.lrucache.new({
 
 
 local _M = {
-    version = 0.1,
+    version = 0.2,
     name = module_name,
 }
 
 
 local function fetch_health_nodes(upstream, checker)
+    local nodes = upstream.nodes
     if not checker then
-        return upstream.nodes
+        local new_nodes = core.table.new(0, #nodes)
+        for _, node in ipairs(nodes) do
+            -- TODO filter with metadata
+            new_nodes[node.host .. ":" .. node.port] = node.weight
+        end
+        return new_nodes
     end
 
     local host = upstream.checks and upstream.checks.host
-    local up_nodes = core.table.new(0, core.table.nkeys(upstream.nodes))
-
-    for addr, weight in pairs(upstream.nodes) do
-        local ip, port = core.utils.parse_addr(addr)
-        local ok = checker:get_target_status(ip, port, host)
+    local up_nodes = core.table.new(0, #nodes)
+    for _, node in ipairs(nodes) do
+        local ok = checker:get_target_status(node.host, node.port, host)
         if ok then
-            up_nodes[addr] = weight
+            -- TODO filter with metadata
+            up_nodes[node.host .. ":" .. node.port] = node.weight
         end
     end
 
     if core.table.nkeys(up_nodes) == 0 then
         core.log.warn("all upstream nodes is unhealth, use default")
-        up_nodes = upstream.nodes
+        for _, node in ipairs(nodes) do
+            up_nodes[node.host .. ":" .. node.port] = node.weight
+        end
     end
 
     return up_nodes
@@ -78,13 +87,11 @@ local function create_checker(upstream, healthcheck_parent)
         shm_name = "upstream-healthcheck",
         checks = upstream.checks,
     })
-
-    for addr, weight in pairs(upstream.nodes) do
-        local ip, port = core.utils.parse_addr(addr)
-        local ok, err = checker:add_target(ip, port, upstream.checks.host)
+    for _, node in ipairs(upstream.nodes) do
+        local ok, err = checker:add_target(node.host, node.port, upstream.checks.host)
         if not ok then
-            core.log.error("failed to add new health check target: ", addr,
-                            " err: ", err)
+            core.log.error("failed to add new health check target: ", node.host, ":", node.port,
+                    " err: ", err)
         end
     end
 
@@ -230,7 +237,14 @@ local function pick_server(route, ctx)
         key = up_conf.type .. "#route_" .. route.value.id
     end
 
-    if core.table.nkeys(up_conf.nodes) == 0 then
+    if up_conf.service_name then
+        if not discovery then
+            return nil, nil, "discovery is uninitialized"
+        end
+        up_conf.nodes = discovery.nodes(up_conf.service_name)
+    end
+
+    if not up_conf.nodes or #up_conf.nodes == 0 then
         return nil, nil, "no valid upstream node"
     end
 
@@ -256,11 +270,10 @@ local function pick_server(route, ctx)
 
     if ctx.balancer_try_count == 1 then
         local retries = up_conf.retries
-        if retries and retries > 0 then
-            set_more_tries(retries)
-        else
-            set_more_tries(core.table.nkeys(up_conf.nodes))
+        if not retries or retries <= 0 then
+            retries = #up_conf.nodes
         end
+        set_more_tries(retries)
     end
 
     if checker then
@@ -281,8 +294,7 @@ local function pick_server(route, ctx)
 
     if up_conf.timeout then
         local timeout = up_conf.timeout
-        local ok, err = set_timeouts(timeout.connect, timeout.send,
-                                     timeout.read)
+        local ok, err = set_timeouts(timeout.connect, timeout.send, timeout.read)
         if not ok then
             core.log.error("could not set upstream timeouts: ", err)
         end
@@ -291,9 +303,11 @@ local function pick_server(route, ctx)
     local ip, port, err = core.utils.parse_addr(server)
     ctx.balancer_ip = ip
     ctx.balancer_port = port
-
+    core.log.info("proxy to ", ip, ":", port)
     return ip, port, err
 end
+
+
 -- for test
 _M.pick_server = pick_server
 
@@ -323,21 +337,39 @@ function _M.init_worker()
             item_schema = core.schema.upstream,
             filter = function(upstream)
                 upstream.has_domain = false
-                if not upstream.value then
+                if not upstream.value or not upstream.value.nodes then
                     return
                 end
 
-                for addr, _ in pairs(upstream.value.nodes or {}) do
-                    local host = core.utils.parse_addr(addr)
-                    if not core.utils.parse_ipv4(host) and
-                       not core.utils.parse_ipv6(host) then
-                        upstream.has_domain = true
-                        break
+                local nodes = upstream.value.nodes
+                if core.table.isarray(nodes) then
+                    for _, node in ipairs(nodes) do
+                        local host = node.host
+                        if not core.utils.parse_ipv4(host) and
+                                not core.utils.parse_ipv6(host) then
+                            upstream.has_domain = true
+                            break
+                        end
+                    end
+                else
+                    local new_nodes = core.table.new(core.table.nkeys(nodes), 0)
+                    for addr, weight in pairs(nodes) do
+                        local host, port = core.utils.parse_addr(addr)
+                        if not core.utils.parse_ipv4(host) and
+                                not core.utils.parse_ipv6(host) then
+                            upstream.has_domain = true
+                        end
+                        local node = {
+                            host = host,
+                            port = port,
+                            weight = weight,
+                        }
+                        core.table.insert(new_nodes, node)
                     end
+                    upstream.value.nodes = new_nodes
                 end
 
-                core.log.info("filter upstream: ",
-                              core.json.delay_encode(upstream))
+                core.log.info("filter upstream: ", core.json.delay_encode(upstream))
             end,
         })
     if not upstreams_etcd then
diff --git a/apisix/core/table.lua b/apisix/core/table.lua
index 0fc64ac..14e0165 100644
--- a/apisix/core/table.lua
+++ b/apisix/core/table.lua
@@ -25,13 +25,14 @@ local type         = type
 
 
 local _M = {
-    version = 0.1,
+    version = 0.2,
     new     = new_tab,
     clear   = require("table.clear"),
     nkeys   = nkeys,
     insert  = table.insert,
     concat  = table.concat,
     clone   = require("table.clone"),
+    isarray = require("table.isarray"),
 }
 
 
diff --git a/apisix/discovery/eureka.lua b/apisix/discovery/eureka.lua
new file mode 100644
index 0000000..d4b4368
--- /dev/null
+++ b/apisix/discovery/eureka.lua
@@ -0,0 +1,253 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+local local_conf         = require("apisix.core.config_local").local_conf()
+local http               = require("resty.http")
+local core               = require("apisix.core")
+local ipmatcher          = require("resty.ipmatcher")
+local ipairs             = ipairs
+local tostring           = tostring
+local type               = type
+local math_random        = math.random
+local error              = error
+local ngx                = ngx
+local ngx_timer_at       = ngx.timer.at
+local ngx_timer_every    = ngx.timer.every
+local string_sub         = string.sub
+local string_find        = string.find
+local log                = core.log
+
+local default_weight
+local applications
+
+local schema = {
+    type = "object",
+    properties = {
+        host = {
+            type = "array",
+            minItems = 1,
+            items = {
+                type = "string",
+            },
+        },
+        fetch_interval = {type = "integer", minimum = 1, default = 30},
+        prefix = {type = "string"},
+        weight = {type = "integer", minimum = 0},
+        timeout = {
+            type = "object",
+            properties = {
+                connect = {type = "integer", minimum = 1, default = 2000},
+                send = {type = "integer", minimum = 1, default = 2000},
+                read = {type = "integer", minimum = 1, default = 5000},
+            }
+        },
+    },
+    required = {"host"}
+}
+
+
+local _M = {
+    version = 0.1,
+}
+
+
+local function service_info()
+    local host = local_conf.eureka and local_conf.eureka.host
+    if not host then
+        log.error("do not set eureka.host")
+        return
+    end
+
+    local basic_auth
+    -- TODO Add health check to get healthy nodes.
+    local url = host[math_random(#host)]
+    local auth_idx = string_find(url, "@", 1, true)
+    if auth_idx then
+        local protocol_idx = string_find(url, "://", 1, true)
+        local protocol = string_sub(url, 1, protocol_idx + 2)
+        local user_and_password = string_sub(url, protocol_idx + 3, auth_idx - 1)
+        local other = string_sub(url, auth_idx + 1)
+        url = protocol .. other
+        basic_auth = "Basic " .. ngx.encode_base64(user_and_password)
+    end
+    if local_conf.eureka.prefix then
+        url = url .. local_conf.eureka.prefix
+    end
+    if string_sub(url, #url) ~= "/" then
+        url = url .. "/"
+    end
+
+    return url, basic_auth
+end
+
+
+local function request(request_uri, basic_auth, method, path, query, body)
+    log.info("eureka uri:", request_uri, ".")
+    local url = request_uri .. path
+    local headers = core.table.new(0, 5)
+    headers['Connection'] = 'Keep-Alive'
+    headers['Accept'] = 'application/json'
+
+    if basic_auth then
+        headers['Authorization'] = basic_auth
+    end
+
+    if body and 'table' == type(body) then
+        local err
+        body, err = core.json.encode(body)
+        if not body then
+            return nil, 'invalid body : ' .. err
+        end
+        -- log.warn(method, url, body)
+        headers['Content-Type'] = 'application/json'
+    end
+
+    local httpc = http.new()
+    local timeout = local_conf.eureka.timeout
+    local connect_timeout = timeout and timeout.connect or 2000
+    local send_timeout = timeout and timeout.send or 2000
+    local read_timeout = timeout and timeout.read or 5000
+    log.info("connect_timeout:", connect_timeout, ", send_timeout:", send_timeout,
+            ", read_timeout:", read_timeout, ".")
+    httpc:set_timeouts(connect_timeout, send_timeout, read_timeout)
+    return httpc:request_uri(url, {
+        version = 1.1,
+        method = method,
+        headers = headers,
+        query = query,
+        body = body,
+        ssl_verify = false,
+    })
+end
+
+
+local function parse_instance(instance)
+    local status = instance.status
+    local overridden_status = instance.overriddenstatus or instance.overriddenStatus
+    if overridden_status and overridden_status ~= "UNKNOWN" then
+        status = overridden_status
+    end
+
+    if status ~= "UP" then
+        return
+    end
+    local port
+    if tostring(instance.port["@enabled"]) == "true" and instance.port["$"] then
+        port = instance.port["$"]
+        -- secure = false
+    end
+    if tostring(instance.securePort["@enabled"]) == "true" and instance.securePort["$"] then
+        port = instance.securePort["$"]
+        -- secure = true
+    end
+    local ip = instance.ipAddr
+    if not ipmatcher.parse_ipv4(ip) and
+            not ipmatcher.parse_ipv6(ip) then
+        log.error(instance.app, " service ", instance.hostName, " node IP ", ip,
+                " is invalid(must be IPv4 or IPv6).")
+        return
+    end
+    return ip, port, instance.metadata
+end
+
+
+local function fetch_full_registry(premature)
+    if premature then
+        return
+    end
+
+    local request_uri, basic_auth = service_info()
+    if not request_uri then
+        return
+    end
+
+    local res, err = request(request_uri, basic_auth, "GET", "apps")
+    if not res then
+        log.error("failed to fetch registry", err)
+        return
+    end
+
+    if not res.body or res.status ~= 200 then
+        log.error("failed to fetch registry, status = ", res.status)
+        return
+    end
+
+    local json_str = res.body
+    local data, err = core.json.decode(json_str)
+    if not data then
+        log.error("invalid response body: ", json_str, " err: ", err)
+        return
+    end
+    local apps = data.applications.application
+    local up_apps = core.table.new(0, #apps)
+    for _, app in ipairs(apps) do
+        for _, instance in ipairs(app.instance) do
+            local ip, port, metadata = parse_instance(instance)
+            if ip and port then
+                local nodes = up_apps[app.name]
+                if not nodes then
+                    nodes = core.table.new(#app.instance, 0)
+                    up_apps[app.name] = nodes
+                end
+                core.table.insert(nodes, {
+                    host = ip,
+                    port = port,
+                    weight = metadata and metadata.weight or default_weight,
+                    metadata = metadata,
+                })
+                if metadata then
+                    -- remove useless data
+                    metadata.weight = nil
+                end
+            end
+        end
+    end
+    applications = up_apps
+end
+
+
+function _M.nodes(service_name)
+    if not applications then
+        log.error("failed to fetch nodes for : ", service_name)
+        return
+    end
+
+    return applications[service_name]
+end
+
+
+function _M.init_worker()
+    if not local_conf.eureka or not local_conf.eureka.host or #local_conf.eureka.host == 0 then
+        error("do not set eureka.host")
+        return
+    end
+
+    local ok, err = core.schema.check(schema, local_conf.eureka)
+    if not ok then
+        error("invalid eureka configuration: " .. err)
+        return
+    end
+    default_weight = local_conf.eureka.weight or 100
+    log.info("default_weight:", default_weight, ".")
+    local fetch_interval = local_conf.eureka.fetch_interval or 30
+    log.info("fetch_interval:", fetch_interval, ".")
+    ngx_timer_at(0, fetch_full_registry)
+    ngx_timer_every(fetch_interval, fetch_full_registry)
+end
+
+
+return _M
diff --git a/t/apisix.luacov b/apisix/discovery/init.lua
similarity index 50%
copy from t/apisix.luacov
copy to apisix/discovery/init.lua
index f9792d8..16aafe6 100644
--- a/t/apisix.luacov
+++ b/apisix/discovery/init.lua
@@ -14,24 +14,20 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
-return {
-    modules = {
-        ["lua.*"] = "lua",
-        ["apisix/*"] = "apisix",
-        ["apisix/admin/*"] = "admin",
-        ["apisix/core/*"] = "core",
-        ["apisix/http/*"] = "http",
-        ["apisix/http/router/*"] = "http/router",
-        ["apisix/plugins/*"] = "plugins",
-        ["apisix/plugins/grpc-transcode/*"] = "plugins/grpc-transcode",
-        ["apisix/plugins/limit-count/*"] = "plugins/limit-count",
-        ["apisix/plugins/prometheus/*"] = "plugins/prometheus",
-        ["apisix/plugins/zipkin/*"] = "plugins/zipkin",
-        ["apisix/utils/*"] = "utils",
 
-        -- can not enable both at http and stream, will fix it later.
-        --  ["apisix/stream/*"] = "stream",
-        --  ["apisix/stream/plugins/*"] = "stream/plugins",
-        --  ["apisix/stream/router/*"] = "stream/router",
-   },
+local log          = require("apisix.core.log")
+local local_conf   = require("apisix.core.config_local").local_conf()
+
+local discovery_type = local_conf.apisix and local_conf.apisix.discovery
+local discovery
+
+if discovery_type then
+    log.info("use discovery: ", discovery_type)
+    discovery = require("apisix.discovery." .. discovery_type)
+end
+
+
+return {
+    version = 0.1,
+    discovery = discovery
 }
diff --git a/apisix/http/service.lua b/apisix/http/service.lua
index 42d31dd..161d82f 100644
--- a/apisix/http/service.lua
+++ b/apisix/http/service.lua
@@ -14,7 +14,8 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
-local core = require("apisix.core")
+local core   = require("apisix.core")
+local ipairs = ipairs
 local services
 local error = error
 local pairs = pairs
@@ -45,17 +46,36 @@ local function filter(service)
         return
     end
 
-    if not service.value.upstream then
+    if not service.value.upstream or not service.value.upstream.nodes then
         return
     end
 
-    for addr, _ in pairs(service.value.upstream.nodes or {}) do
-        local host = core.utils.parse_addr(addr)
-        if not core.utils.parse_ipv4(host) and
-           not core.utils.parse_ipv6(host) then
-            service.has_domain = true
-            break
+    local nodes = service.value.upstream.nodes
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            local host = node.host
+            if not core.utils.parse_ipv4(host) and
+                    not core.utils.parse_ipv6(host) then
+                service.has_domain = true
+                break
+            end
         end
+    else
+        local new_nodes = core.table.new(core.table.nkeys(nodes), 0)
+        for addr, weight in pairs(nodes) do
+            local host, port = core.utils.parse_addr(addr)
+            if not core.utils.parse_ipv4(host) and
+                    not core.utils.parse_ipv6(host) then
+                service.has_domain = true
+            end
+            local node = {
+                host = host,
+                port = port,
+                weight = weight,
+            }
+            core.table.insert(new_nodes, node)
+        end
+        service.value.upstream.nodes = new_nodes
     end
 
     core.log.info("filter service: ", core.json.delay_encode(service))
diff --git a/apisix/init.lua b/apisix/init.lua
index 48c5b80..be1afac 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -28,7 +28,7 @@ local get_method    = ngx.req.get_method
 local ngx_exit      = ngx.exit
 local math          = math
 local error         = error
-local pairs         = pairs
+local ipairs        = ipairs
 local tostring      = tostring
 local load_balancer
 
@@ -42,7 +42,7 @@ local function parse_args(args)
 end
 
 
-local _M = {version = 0.3}
+local _M = {version = 0.4}
 
 
 function _M.http_init(args)
@@ -74,7 +74,10 @@ function _M.http_init_worker()
     if not ok then
         error("failed to init worker event: " .. err)
     end
-
+    local discovery = require("apisix.discovery.init").discovery
+    if discovery and discovery.init_worker then
+        discovery.init_worker()
+    end
     require("apisix.balancer").init_worker()
     load_balancer = require("apisix.balancer").run
     require("apisix.admin.init").init_worker()
@@ -179,71 +182,71 @@ function _M.http_ssl_phase()
 end
 
 
-local function parse_domain_in_up(up, ver)
-    local new_nodes = core.table.new(0, 8)
-    for addr, weight in pairs(up.value.nodes) do
-        local host, port = core.utils.parse_addr(addr)
+local function parse_domain(host)
+    local ip_info, err = core.utils.dns_parse(dns_resolver, host)
+    if not ip_info then
+        core.log.error("failed to parse domain for ", host, ", error:",err)
+        return nil, err
+    end
+
+    core.log.info("parse addr: ", core.json.delay_encode(ip_info))
+    core.log.info("resolver: ", core.json.delay_encode(dns_resolver))
+    core.log.info("host: ", host)
+    if ip_info.address then
+        core.log.info("dns resolver domain: ", host, " to ", ip_info.address)
+        return ip_info.address
+    else
+        return nil, "failed to parse domain"
+    end
+end
+
+
+local function parse_domain_for_nodes(nodes)
+    local new_nodes = core.table.new(#nodes, 0)
+    for _, node in ipairs(nodes) do
+        local host = node.host
         if not ipmatcher.parse_ipv4(host) and
-           not ipmatcher.parse_ipv6(host) then
-            local ip_info, err = core.utils.dns_parse(dns_resolver, host)
-            if not ip_info then
-                return nil, err
+                not ipmatcher.parse_ipv6(host) then
+            local ip, err = parse_domain(host)
+            if ip then
+                local new_node = core.table.clone(node)
+                new_node.host = ip
+                core.table.insert(new_nodes, new_node)
             end
 
-            core.log.info("parse addr: ", core.json.delay_encode(ip_info))
-            core.log.info("resolver: ", core.json.delay_encode(dns_resolver))
-            core.log.info("host: ", host)
-            if ip_info.address then
-                new_nodes[ip_info.address .. ":" .. port] = weight
-                core.log.info("dns resolver domain: ", host, " to ",
-                              ip_info.address)
-            else
-                return nil, "failed to parse domain in route"
+            if err then
+                return nil, err
             end
         else
-            new_nodes[addr] = weight
+            core.table.insert(new_nodes, node)
         end
     end
+    return new_nodes
+end
 
+
+local function parse_domain_in_up(up, ver)
+    local nodes = up.value.nodes
+    local new_nodes, err = parse_domain_for_nodes(nodes)
+    if not new_nodes then
+        return nil, err
+    end
     up.dns_value = core.table.clone(up.value)
     up.dns_value.nodes = new_nodes
-    core.log.info("parse upstream which contain domain: ",
-                  core.json.delay_encode(up))
+    core.log.info("parse upstream which contain domain: ", core.json.delay_encode(up))
     return up
 end
 
 
 local function parse_domain_in_route(route, ver)
-    local new_nodes = core.table.new(0, 8)
-    for addr, weight in pairs(route.value.upstream.nodes) do
-        local host, port = core.utils.parse_addr(addr)
-        if not ipmatcher.parse_ipv4(host) and
-           not ipmatcher.parse_ipv6(host) then
-            local ip_info, err = core.utils.dns_parse(dns_resolver, host)
-            if not ip_info then
-                return nil, err
-            end
-
-            core.log.info("parse addr: ", core.json.delay_encode(ip_info))
-            core.log.info("resolver: ", core.json.delay_encode(dns_resolver))
-            core.log.info("host: ", host)
-            if ip_info and ip_info.address then
-                new_nodes[ip_info.address .. ":" .. port] = weight
-                core.log.info("dns resolver domain: ", host, " to ",
-                              ip_info.address)
-            else
-                return nil, "failed to parse domain in route"
-            end
-
-        else
-            new_nodes[addr] = weight
-        end
+    local nodes = route.value.upstream.nodes
+    local new_nodes, err = parse_domain_for_nodes(nodes)
+    if not new_nodes then
+        return nil, err
     end
-
     route.dns_value = core.table.deepcopy(route.value)
     route.dns_value.upstream.nodes = new_nodes
-    core.log.info("parse route which contain domain: ",
-                  core.json.delay_encode(route))
+    core.log.info("parse route which contain domain: ", core.json.delay_encode(route))
     return route
 end
 
@@ -442,6 +445,7 @@ function _M.grpc_access_phase()
     run_plugin("access", plugins, api_ctx)
 end
 
+
 local function common_phase(plugin_name)
     local api_ctx = ngx.ctx.api_ctx
     if not api_ctx then
@@ -464,14 +468,17 @@ local function common_phase(plugin_name)
     return api_ctx
 end
 
+
 function _M.http_header_filter_phase()
     common_phase("header_filter")
 end
 
+
 function _M.http_body_filter_phase()
     common_phase("body_filter")
 end
 
+
 function _M.http_log_phase()
 
     local api_ctx = common_phase("log")
@@ -488,6 +495,7 @@ function _M.http_log_phase()
     core.tablepool.release("api_ctx", api_ctx)
 end
 
+
 function _M.http_balancer_phase()
     local api_ctx = ngx.ctx.api_ctx
     if not api_ctx then
@@ -511,6 +519,7 @@ function _M.http_balancer_phase()
     load_balancer(api_ctx.matched_route, api_ctx)
 end
 
+
 local function cors_admin()
     local local_conf = core.config.local_conf()
     if local_conf.apisix and not local_conf.apisix.enable_admin_cors then
diff --git a/apisix/router.lua b/apisix/router.lua
index d3b4594..4ba8709 100644
--- a/apisix/router.lua
+++ b/apisix/router.lua
@@ -15,12 +15,13 @@
 -- limitations under the License.
 --
 local require = require
-local core  = require("apisix.core")
-local error = error
-local pairs = pairs
+local core    = require("apisix.core")
+local error   = error
+local pairs   = pairs
+local ipairs  = ipairs
 
 
-local _M = {version = 0.2}
+local _M = {version = 0.3}
 
 
 local function filter(route)
@@ -29,17 +30,36 @@ local function filter(route)
         return
     end
 
-    if not route.value.upstream then
+    if not route.value.upstream or not route.value.upstream.nodes then
         return
     end
 
-    for addr, _ in pairs(route.value.upstream.nodes or {}) do
-        local host = core.utils.parse_addr(addr)
-        if not core.utils.parse_ipv4(host) and
-           not core.utils.parse_ipv6(host) then
-            route.has_domain = true
-            break
+    local nodes = route.value.upstream.nodes
+    if core.table.isarray(nodes) then
+        for _, node in ipairs(nodes) do
+            local host = node.host
+            if not core.utils.parse_ipv4(host) and
+                    not core.utils.parse_ipv6(host) then
+                route.has_domain = true
+                break
+            end
         end
+    else
+        local new_nodes = core.table.new(core.table.nkeys(nodes), 0)
+        for addr, weight in pairs(nodes) do
+            local host, port = core.utils.parse_addr(addr)
+            if not core.utils.parse_ipv4(host) and
+                    not core.utils.parse_ipv6(host) then
+                route.has_domain = true
+            end
+            local node = {
+                host = host,
+                port = port,
+                weight = weight,
+            }
+            core.table.insert(new_nodes, node)
+        end
+        route.value.upstream.nodes = new_nodes
     end
 
     core.log.info("filter route: ", core.json.delay_encode(route))
@@ -78,7 +98,7 @@ end
 
 function _M.stream_init_worker()
     local router_stream = require("apisix.stream.router.ip_port")
-    router_stream.stream_init_worker()
+    router_stream.stream_init_worker(filter)
     _M.router_stream = router_stream
 end
 
@@ -88,4 +108,8 @@ function _M.http_routes()
 end
 
 
+-- for test
+_M.filter_test = filter
+
+
 return _M
diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua
index d4b63eb..e8d2b75 100644
--- a/apisix/schema_def.lua
+++ b/apisix/schema_def.lua
@@ -18,7 +18,7 @@ local schema    = require('apisix.core.schema')
 local setmetatable = setmetatable
 local error     = error
 
-local _M = {version = 0.4}
+local _M = {version = 0.5}
 
 
 local plugins_schema = {
@@ -225,11 +225,9 @@ local health_checker = {
 }
 
 
-local upstream_schema = {
-    type = "object",
-    properties = {
-        nodes = {
-            description = "nodes of upstream",
+local nodes_schema = {
+    anyOf = {
+        {
             type = "object",
             patternProperties = {
                 [".*"] = {
@@ -240,6 +238,39 @@ local upstream_schema = {
             },
             minProperties = 1,
         },
+        {
+            type = "array",
+            minItems = 1,
+            items = {
+                type = "object",
+                properties = {
+                    host = host_def,
+                    port = {
+                        description = "port of node",
+                        type = "integer",
+                        minimum = 1,
+                    },
+                    weight = {
+                        description = "weight of node",
+                        type = "integer",
+                        minimum = 0,
+                    },
+                    metadata = {
+                        description = "metadata of node",
+                        type = "object",
+                    }
+                },
+                required = {"host", "port", "weight"},
+            },
+        }
+    }
+}
+
+
+local upstream_schema = {
+    type = "object",
+    properties = {
+        nodes = nodes_schema,
         retries = {
             type = "integer",
             minimum = 1,
@@ -297,11 +328,13 @@ local upstream_schema = {
             type        = "boolean"
         },
         desc = {type = "string", maxLength = 256},
+        service_name = {type = "string", maxLength = 50},
         id = id_schema
     },
     anyOf = {
         {required = {"type", "nodes"}},
         {required = {"type", "k8s_deployment_info"}},
+        {required = {"type", "service_name"}},
     },
     additionalProperties = false,
 }
diff --git a/conf/config.yaml b/conf/config.yaml
index ef3a7f8..11d0d76 100644
--- a/conf/config.yaml
+++ b/conf/config.yaml
@@ -91,7 +91,7 @@ apisix:
     listen_port: 9443
     ssl_protocols: "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"
     ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES2 [...]
-
+#  discovery: eureka               # service discovery center
 nginx_config:                     # config for render the template to genarate nginx.conf
   error_log: "logs/error.log"
   error_log_level: "warn"         # warn,error
@@ -118,6 +118,17 @@ etcd:
   prefix: "/apisix"               # apisix configurations prefix
   timeout: 3                      # 3 seconds
 
+#eureka:
+#  host:                           # it's possible to define multiple eureka hosts addresses of the same eureka cluster.
+#    - "http://127.0.0.1:8761"
+#  prefix: "/eureka/"
+#  fetch_interval: 30              # default 30s
+#  weight: 100                     # default weight for node
+#  timeout:
+#    connect: 2000                 # default 2000ms
+#    send: 2000                    # default 2000ms
+#    read: 5000                    # default 5000ms
+
 plugins:                          # plugin list
   - example-plugin
   - limit-req
diff --git a/doc/discovery-cn.md b/doc/discovery-cn.md
new file mode 100644
index 0000000..b748921
--- /dev/null
+++ b/doc/discovery-cn.md
@@ -0,0 +1,253 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+[English](discovery.md)
+
+# 集成服务发现注册中心
+
+* [**摘要**](#摘要)
+* [**如何扩展注册中心**](#如何扩展注册中心)
+    * [**基本步骤**](#基本步骤)
+    * [**以 Eureka 举例**](#以-Eureka-举例)
+        * [**实现 eureka.lua**](#实现-eurekalua)
+        * [**Eureka 与 APISIX 之间数据转换逻辑**](#Eureka-与-APISIX-之间数据转换逻辑)
+* [**注册中心配置**](#注册中心配置)
+    * [**选择注册中心**](#选择注册中心)
+    * [**Eureka 的配置**](#Eureka-的配置)
+* [**upstream 配置**](#upstream-配置)
+
+## 摘要
+
+当业务量发生变化时,需要对上游服务进行扩缩容,或者因服务器硬件故障需要更换服务器。如果网关是通过配置来维护上游服务信息,在微服务架构模式下,其带来的维护成本可想而知。再者因不能及时更新这些信息,也会对业务带来一定的影响,还有人为误操作带来的影响也不可忽视,所以网关非常必要通过服务注册中心动态获取最新的服务实例信息。架构图如下所示:
+
+![](./images/discovery-cn.png)
+
+1. 服务启动时将自身的一些信息,比如服务名、IP、端口等信息上报到注册中心;各个服务与注册中心使用一定机制(例如心跳)通信,如果注册中心与服务长时间无法通信,就会注销该实例;当服务下线时,会删除注册中心的实例信息;
+2. 网关会准实时地从注册中心获取服务实例信息;
+3. 当用户通过网关请求服务时,网关从注册中心获取的实例列表中选择一个进行代理;
+
+常见的注册中心:Eureka, Etcd, Consul, Nacos, Zookeeper等
+
+
+## 如何扩展注册中心?
+
+### 基本步骤
+
+APISIX 要扩展注册中心其实是件非常容易的事情,其基本步骤如下:
+
+1. 在 `apisix/discovery/` 目录中添加注册中心客户端的实现;
+2. 实现用于初始化的 `_M.init_worker()` 函数以及用于获取服务实例节点列表的 `_M.nodes(service_name)` 函数;
+3. 将注册中心数据转换为 APISIX 格式的数据;
+
+### 以 Eureka 举例
+
+#### 实现 eureka.lua
+
+首先在 `apisix/discovery/` 目录中添加 [`eureka.lua`](../apisix/discovery/eureka.lua);
+
+然后在 `eureka.lua` 实现用于初始化的 `init_worker` 函数以及用于获取服务实例节点列表的 `nodes` 函数即可:
+
+  ```lua
+  local _M = {
+      version = 0.1,
+  }
+
+
+  function _M.nodes(service_name)
+      ... ...
+  end
+
+
+  function _M.init_worker()
+      ... ...
+  end
+
+
+  return _M
+  ```
+
+#### Eureka 与 APISIX 之间数据转换逻辑
+
+APISIX是通过 `upstream.nodes` 来配置上游服务的,所以使用注册中心后,通过注册中心获取服务的所有 node 后,赋值给 `upstream.nodes` 来达到相同的效果。那么 APISIX 是怎么将 Eureka 的数据转成 node 的呢? 假如从 Eureka 获取如下数据:
+
+```json
+{
+  "applications": {
+      "application": [
+          {
+              "name": "USER-SERVICE",                 # 服务名称
+              "instance": [
+                  {
+                      "instanceId": "192.168.1.100:8761",
+                      "hostName": "192.168.1.100",
+                      "app": "USER-SERVICE",          # 服务名称
+                      "ipAddr": "192.168.1.100",      # 实例 IP 地址
+                      "status": "UP",                 # 状态
+                      "overriddenStatus": "UNKNOWN",  # 覆盖状态
+                      "port": {
+                          "$": 8761,                  # 端口
+                          "@enabled": "true"          # 开始端口
+                      },
+                      "securePort": {
+                          "$": 443,
+                          "@enabled": "false"
+                      },
+                      "metadata": {
+                          "management.port": "8761",
+                          "weight": 100               # 权重,需要通过 spring boot 应用的 eureka.instance.metadata-map.weight 进行配置
+                      },
+                      "homePageUrl": "http://192.168.1.100:8761/",
+                      "statusPageUrl": "http://192.168.1.100:8761/actuator/info",
+                      "healthCheckUrl": "http://192.168.1.100:8761/actuator/health",
+                      ... ...
+                  }
+              ]
+          }
+      ]
+  }
+}
+```
+
+解析 instance 数据步骤:
+
+1. 首先要选择状态为 “UP” 的实例: overriddenStatus 值不为 "UNKNOWN" 以 overriddenStatus 为准,否则以 status 的值为准;
+2. IP 地址:以 ipAddr 的值为 IP; 并且必须是 IPv4 或 IPv6 格式的;
+3. 端口:端口取值规则是,如果 port["@enabled"] 等于 "true" 那么使用 port["\$"] 的值;如果 securePort["@enabled"] 等于 "true" 那么使用 securePort["$"] 的值;
+4. 权重:权重取值顺序是,先判断 `metadata.weight` 是否有值,如果没有,则取配置中的 `eureka.weight` 的值, 如果还没有,则取默认值`100`;
+
+这个例子转成 APISIX nodes 的结果如下:
+
+```json
+[
+  {
+    "host" : "192.168.1.100",
+    "port" : 8761,
+    "weight" : 100,
+    "metadata" : {
+      "management.port": "8761",
+    }
+  }
+]
+```
+
+## 注册中心配置
+
+### 选择注册中心
+
+首先要在 `conf/config.yaml` 文件中增加如下配置,以选择注册中心的类型:
+
+```yaml
+apisix:
+  discovery: eureka
+```
+
+此名称要与 `apisix/discovery/` 目录中实现对应注册中心的文件名保持一致。
+
+现已支持注册中心有:Eureka 。
+
+### Eureka 的配置
+
+在 `conf/config.yaml` 增加如下格式的配置:
+
+```yaml
+eureka:
+  host:                            # it's possible to define multiple eureka hosts addresses of the same eureka cluster.
+    - "http://${usename}:${passowrd}@${eureka_host1}:${eureka_port1}"
+    - "http://${usename}:${passowrd}@${eureka_host2}:${eureka_port2}"
+  prefix: "/eureka/"
+  fetch_interval: 30               # 从 eureka 中拉取数据的时间间隔,默认30秒
+  weight: 100                      # default weight for node
+  timeout:
+    connect: 2000                  # 连接 eureka 的超时时间,默认2000ms
+    send: 2000                     # 向 eureka 发送数据的超时时间,默认2000ms
+    read: 5000                     # 从 eureka 读数据的超时时间,默认5000ms
+```
+
+通过 `eureka.host ` 配置 eureka 的服务器地址。
+
+如果 eureka 的地址是 `http://127.0.0.1:8761/` ,并且不需要用户名和密码验证的话,配置如下:
+
+```yaml
+eureka:
+  host:
+    - "http://127.0.0.1:8761"
+  prefix: "/eureka/"
+```
+
+## upstream 配置
+
+APISIX是通过 `upstream.service_name` 与注册中心的服务名进行关联。下面是将 uri 为 "/user/*" 的请求路由到注册中心名为 "USER-SERVICE" 的服务上例子:
+
+```shell
+$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+    "uri": "/user/*",
+    "upstream": {
+        "service_name": "USER-SERVICE",
+        "type": "roundrobin"
+    }
+}'
+
+HTTP/1.1 201 Created
+Date: Sat, 31 Aug 2019 01:17:15 GMT
+Content-Type: text/plain
+Transfer-Encoding: chunked
+Connection: keep-alive
+Server: APISIX web server
+
+{"node":{"value":{"uri":"\/user\/*","upstream": {"service_name": "USER-SERVICE", "type": "roundrobin"}},"createdIndex":61925,"key":"\/apisix\/routes\/1","modifiedIndex":61925},"action":"create"}
+```
+
+因为上游的接口 URL 可能会有冲突,通常会在网关通过前缀来进行区分:
+
+```shell
+$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+    "uri": "/a/*",
+    "plugins": {
+        "proxy-rewrite" : {
+            regex_uri: ["^/a/(.*)", "/${1}"]
+        }
+    }
+    "upstream": {
+        "service_name": "A-SERVICE",
+        "type": "roundrobin"
+    }
+}'
+
+$ curl http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+    "uri": "/b/*",
+    "plugins": {
+        "proxy-rewrite" : {
+            regex_uri: ["^/b/(.*)", "/${1}"]
+        }
+    }
+    "upstream": {
+        "service_name": "B-SERVICE",
+        "type": "roundrobin"
+    }
+}'
+```
+
+假如 A-SERVICE 和 B-SERVICE 都提供了一个 `/test` 的接口,通过上面的配置,可以通过 `/a/test` 访问 A-SERVICE 的 `/test` 接口,通过 `/b/test` 访问 B-SERVICE 的 `/test` 接口。
+
+
+**注意**:配置 `upstream.service_name` 后 `upstream.nodes` 将不再生效,而是使用从注册中心的数据来替换,即使注册中心的数据是空的。
+
+
diff --git a/doc/discovery.md b/doc/discovery.md
new file mode 100644
index 0000000..f84f294
--- /dev/null
+++ b/doc/discovery.md
@@ -0,0 +1,244 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+[Chinese](discovery-cn.md)
+
+# Integration service discovery registry
+
+* [**Summary**](#Summary)
+* [**How extend the discovery client?**](#how-extend-the-discovery-client)
+    * [**Basic steps**](#basic-steps)
+    * [**the example of Eureka**](#the-example-of-eureka)
+        * [**Implementation of eureka.lua**](#implementation-of-eurekalua)
+        * [**How convert Eureka's instance data to APISIX's node?**](#how-convert-eurekas-instance-data-to-apisixs-node)
+* [**Configuration for discovery client**](#configuration-for-discovery-client)
+    * [**Select discovery client**](#select-discovery-client)
+    * [**Configuration for Eureka**](#configuration-for-eureka)
+* [**Upstream setting**](#upstream-setting)
+
+## Summary
+
+When system traffic changes, the number of servers of the upstream service also increases or decreases, or the server needs to be replaced due to its hardware failure. If the gateway maintains upstream service information through configuration, the maintenance costs in the microservices architecture pattern are unpredictable. Furthermore, due to the untimely update of these information, will also bring a certain impact for the business, and the impact of human error operation can not be  [...]
+
+![](./images/discovery.png)
+
+1. When the service starts, it will report some of its information, such as the service name, IP, port and other information to the registry. The services communicate with the registry using a mechanism such as a heartbeat, and if the registry and the service are unable to communicate for a long time, the instance will be cancel.When the service goes offline, the registry will delete the instance information.
+2. The gateway gets service instance information from the registry in near-real time.
+3. When the user requests the service through the gateway, the gateway selects one instance from the registry for proxy.
+
+Common registries: Eureka, Etcd, Consul, Zookeeper, Nacos etc.
+
+## How extend the discovery client?
+
+### Basic steps
+
+It is very easy for APISIX to extend the discovery client. , the basic steps are as follows
+
+1. Add the implementation of registry client in the 'apisix/discovery/' directory;
+
+2. Implement the `_M. init_worker()` function for initialization and the `_M. nodes(service_name)` function for obtaining the list of service instance nodes;
+
+3. Convert the registry data into data in APISIX;
+
+
+### the example of Eureka
+
+#### Implementation of eureka.lua
+
+First, add [`eureka.lua`](../apisix/discovery/eureka.lua) in the `apisix/discovery/` directory;
+
+Then implement the `_M.init_worker()` function for initialization and the `_M.nodes(service_name)` function for obtaining the list of service instance nodes in ` eureka.lua`:
+
+  ```lua
+  local _M = {
+      version = 1.0,
+  }
+
+
+  function _M.nodes(service_name)
+      ... ...
+  end
+
+
+  function _M.init_worker()
+      ... ...
+  end
+
+
+  return _M
+  ```
+
+#### How convert Eureka's instance data to APISIX's node?
+
+Here's an example of Eureka's data:
+
+```json
+{
+  "applications": {
+      "application": [
+          {
+              "name": "USER-SERVICE",                 # service name
+              "instance": [
+                  {
+                      "instanceId": "192.168.1.100:8761",
+                      "hostName": "192.168.1.100",
+                      "app": "USER-SERVICE",          # service name
+                      "ipAddr": "192.168.1.100",      # IP address
+                      "status": "UP",
+                      "overriddenStatus": "UNKNOWN",
+                      "port": {
+                          "$": 8761,
+                          "@enabled": "true"
+                      },
+                      "securePort": {
+                          "$": 443,
+                          "@enabled": "false"
+                      },
+                      "metadata": {
+                          "management.port": "8761",
+                          "weight": 100               # Setting by 'eureka.instance.metadata-map.weight' of the spring boot application
+                      },
+                      "homePageUrl": "http://192.168.1.100:8761/",
+                      "statusPageUrl": "http://192.168.1.100:8761/actuator/info",
+                      "healthCheckUrl": "http://192.168.1.100:8761/actuator/health",
+                      ... ...
+                  }
+              ]
+          }
+      ]
+  }
+}
+```
+
+Deal with the Eureka's instance data need the following steps :
+
+1. select the UP instance. When the value of `overriddenStatus` is "UP" or the value of `overriddenStatus` is "UNKNOWN" and the value of `status` is "UP".
+2. Host. The `ipAddr` is the IP address of instance; and must be IPv4 or IPv6.
+3. Port. If the value of `port["@enabled"]` is equal to "true", using the value of `port["\$"]`, If the value of `securePort["@enabled"]` is equal to "true", using the value of `securePort["\$"]`.
+4. Weight. `local weight = metadata.weight or local_conf.eureka.weight or 100`
+
+The result of this example is as follows:
+
+```json
+[
+  {
+    "host" : "192.168.1.100",
+    "port" : 8761,
+    "weight" : 100,
+    "metadata" : {
+      "management.port": "8761",
+    }
+  }
+]
+```
+
+## Configuration for discovery client
+
+### Select discovery client
+
+Add the following configuration to `conf/config.yaml` and select one discovery client type which you want:
+
+```yaml
+apisix:
+  discovery: eureka
+```
+
+This name should be consistent with the file name of the implementation registry in the `apisix/discovery/` directory.
+
+The supported discovery client: Eureka.
+
+### Configuration for Eureka
+
+Add following configuration in `conf/config.yaml` :
+
+```yaml
+eureka:
+  host:                            # it's possible to define multiple eureka hosts addresses of the same eureka cluster.
+    - "http://${usename}:${passowrd}@${eureka_host1}:${eureka_port1}"
+    - "http://${usename}:${passowrd}@${eureka_host2}:${eureka_port2}"
+  prefix: "/eureka/"
+  fetch_interval: 30               # 30s
+  weight: 100                      # default weight for node
+  timeout:
+    connect: 2000                  # 2000ms
+    send: 2000                     # 2000ms
+    read: 5000                     # 5000ms
+```
+
+
+## Upstream setting
+
+Here is an example of routing a request with a uri of "/user/*" to a service which named "user-service"  in the registry :
+
+```shell
+$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+    "uri": "/user/*",
+    "upstream": {
+        "service_name": "USER-SERVICE",
+        "type": "roundrobin"
+    }
+}'
+
+HTTP/1.1 201 Created
+Date: Sat, 31 Aug 2019 01:17:15 GMT
+Content-Type: text/plain
+Transfer-Encoding: chunked
+Connection: keep-alive
+Server: APISIX web server
+
+{"node":{"value":{"uri":"\/user\/*","upstream": {"service_name": "USER-SERVICE", "type": "roundrobin"}},"createdIndex":61925,"key":"\/apisix\/routes\/1","modifiedIndex":61925},"action":"create"}
+```
+
+Because the upstream interface URL may have conflict, usually in the gateway by prefix to distinguish:
+
+```shell
+$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+    "uri": "/a/*",
+    "plugins": {
+        "proxy-rewrite" : {
+            regex_uri: ["^/a/(.*)", "/${1}"]
+        }
+    }
+    "upstream": {
+        "service_name": "A-SERVICE",
+        "type": "roundrobin"
+    }
+}'
+
+$ curl http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+    "uri": "/b/*",
+    "plugins": {
+        "proxy-rewrite" : {
+            regex_uri: ["^/b/(.*)", "/${1}"]
+        }
+    }
+    "upstream": {
+        "service_name": "B-SERVICE",
+        "type": "roundrobin"
+    }
+}'
+```
+
+Suppose both A-SERVICE and B-SERVICE provide a `/test` API. The above configuration allows access to A-SERVICE's `/test` API through `/a/test` and B-SERVICE's `/test` API through `/b/test`.
+
+**Notice**:When configuring `upstream.service_name`,  `upstream.nodes` will no longer take effect, but will be replaced by 'nodes' obtained from the registry.
+
+
diff --git a/doc/images/discovery-cn.png b/doc/images/discovery-cn.png
new file mode 100644
index 0000000..7b448c2
Binary files /dev/null and b/doc/images/discovery-cn.png differ
diff --git a/doc/images/discovery.png b/doc/images/discovery.png
new file mode 100644
index 0000000..6b592e3
Binary files /dev/null and b/doc/images/discovery.png differ
diff --git a/t/admin/balancer.t b/t/admin/balancer.t
index 0be70db..7054d22 100644
--- a/t/admin/balancer.t
+++ b/t/admin/balancer.t
@@ -30,6 +30,8 @@ add_block_preprocessor(sub {
 
     function test(route, ctx, count)
         local balancer = require("apisix.balancer")
+        local router = require("apisix.router")
+        router.filter_test(route)
         local res = {}
         for i = 1, count or 12 do
             local host, port, err = balancer.pick_server(route, ctx)
diff --git a/t/admin/routes-array-nodes.t b/t/admin/routes-array-nodes.t
new file mode 100644
index 0000000..3d6cafa
--- /dev/null
+++ b/t/admin/routes-array-nodes.t
@@ -0,0 +1,126 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set route(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "methods": ["GET"],
+                    "upstream": {
+                        "nodes": [{
+                            "host": "127.0.0.1",
+                            "port": 8080,
+                            "weight": 1
+                        }],
+                        "type": "roundrobin"
+                    },
+                    "desc": "new route",
+                    "uri": "/index.html"
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "methods": [
+                                "GET"
+                            ],
+                            "uri": "/index.html",
+                            "desc": "new route",
+                            "upstream": {
+                                "nodes": [{
+                                    "host": "127.0.0.1",
+                                    "port": 8080,
+                                    "weight": 1
+                                }],
+                                "type": "roundrobin"
+                            }
+                        },
+                        "key": "/apisix/routes/1"
+                    },
+                    "action": "set"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: get route(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_GET,
+                 nil,
+                [[{
+                    "node": {
+                        "value": {
+                            "methods": [
+                                "GET"
+                            ],
+                            "uri": "/index.html",
+                            "desc": "new route",
+                            "upstream": {
+                                "nodes": [{
+                                    "host": "127.0.0.1",
+                                    "port": 8080,
+                                    "weight": 1
+                                }],
+                                "type": "roundrobin"
+                            }
+                        },
+                        "key": "/apisix/routes/1"
+                    },
+                    "action": "get"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
diff --git a/t/admin/services-array-nodes.t b/t/admin/services-array-nodes.t
new file mode 100644
index 0000000..a8a6ca1
--- /dev/null
+++ b/t/admin/services-array-nodes.t
@@ -0,0 +1,116 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set service(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/services/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "upstream": {
+                        "nodes": [{
+                            "host": "127.0.0.1",
+                            "port": 8080,
+                            "weight": 1
+                        }],
+                        "type": "roundrobin"
+                    },
+                    "desc": "new service"
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "upstream": {
+                                "nodes": [{
+                                    "host": "127.0.0.1",
+                                    "port": 8080,
+                                    "weight": 1
+                                }],
+                                "type": "roundrobin"
+                            },
+                            "desc": "new service"
+                        },
+                        "key": "/apisix/services/1"
+                    },
+                    "action": "set"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: get service(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/services/1',
+                 ngx.HTTP_GET,
+                 nil,
+                [[{
+                    "node": {
+                        "value": {
+                            "upstream": {
+                                "nodes": [{
+                                    "host": "127.0.0.1",
+                                    "port": 8080,
+                                    "weight": 1
+                                }],
+                                "type": "roundrobin"
+                            },
+                            "desc": "new service"
+                        },
+                        "key": "/apisix/services/1"
+                    },
+                    "action": "get"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
diff --git a/t/admin/upstream-array-nodes.t b/t/admin/upstream-array-nodes.t
new file mode 100644
index 0000000..9f0c5b8
--- /dev/null
+++ b/t/admin/upstream-array-nodes.t
@@ -0,0 +1,409 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set upstream(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "nodes": [{
+                        "host": "127.0.0.1",
+                        "port": 8080,
+                        "weight": 1
+                    }],
+                    "type": "roundrobin",
+                    "desc": "new upstream"
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "nodes": [{
+                                 "host": "127.0.0.1",
+                                 "port": 8080,
+                                 "weight": 1
+                            }],
+                            "type": "roundrobin",
+                            "desc": "new upstream"
+                        },
+                        "key": "/apisix/upstreams/1"
+                    },
+                    "action": "set"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: get upstream(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams/1',
+                 ngx.HTTP_GET,
+                 nil,
+                [[{
+                    "node": {
+                        "value": {
+                            "nodes": [{
+                                 "host": "127.0.0.1",
+                                 "port": 8080,
+                                 "weight": 1
+                            }],
+                            "type": "roundrobin",
+                            "desc": "new upstream"
+                        },
+                        "key": "/apisix/upstreams/1"
+                    },
+                    "action": "get"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: delete upstream(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, message = t('/apisix/admin/upstreams/1',
+                 ngx.HTTP_DELETE,
+                 nil,
+                 [[{
+                    "action": "delete"
+                }]]
+                )
+            ngx.say("[delete] code: ", code, " message: ", message)
+        }
+    }
+--- request
+GET /t
+--- response_body
+[delete] code: 200 message: passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: delete upstream(id: not_found)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code = t('/apisix/admin/upstreams/not_found',
+                 ngx.HTTP_DELETE,
+                 nil,
+                 [[{
+                    "action": "delete"
+                }]]
+                )
+
+            ngx.say("[delete] code: ", code)
+        }
+    }
+--- request
+GET /t
+--- response_body
+[delete] code: 404
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: push upstream + delete
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, message, res = t('/apisix/admin/upstreams',
+                 ngx.HTTP_POST,
+                 [[{
+                    "nodes": [{
+                         "host": "127.0.0.1",
+                         "port": 8080,
+                         "weight": 1
+                    }],
+                    "type": "roundrobin"
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "nodes": [{
+                                "host": "127.0.0.1",
+                                "port": 8080,
+                                "weight": 1
+                            }],
+                            "type": "roundrobin"
+                        }
+                    },
+                    "action": "create"
+                }]]
+                )
+
+            if code ~= 200 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+
+            ngx.say("[push] code: ", code, " message: ", message)
+
+            local id = string.sub(res.node.key, #"/apisix/upstreams/" + 1)
+            code, message = t('/apisix/admin/upstreams/' .. id,
+                 ngx.HTTP_DELETE,
+                 nil,
+                 [[{
+                    "action": "delete"
+                }]]
+                )
+            ngx.say("[delete] code: ", code, " message: ", message)
+        }
+    }
+--- request
+GET /t
+--- response_body
+[push] code: 200 message: passed
+[delete] code: 200 message: passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: empty nodes
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code, message, res = t('/apisix/admin/upstreams/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "nodes": [],
+                    "type": "roundrobin"
+                }]]
+                )
+
+            if code ~= 200 then
+                ngx.status = code
+                ngx.print(message)
+                return
+            end
+
+            ngx.say("[push] code: ", code, " message: ", message)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
+
+
+
+=== TEST 7: no additional properties is valid
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "id": 1,
+                    "nodes": [{
+                          "host": "127.0.0.1",
+                          "port": 8080,
+                          "weight": 1
+                    }],
+                    "type": "roundrobin",
+                    "invalid_property": "/index.html"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid configuration: additional properties forbidden, found invalid_property"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: invalid weight of node
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "id": 1,
+                    "nodes": [{
+                          "host": "127.0.0.1",
+                          "port": 8080,
+                          "weight": "1"
+                    }],
+                    "type": "chash"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 9: invalid weight of node
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "id": 1,
+                    "nodes": [{
+                          "host": "127.0.0.1",
+                          "port": 8080,
+                          "weight": -100
+                    }],
+                    "type": "chash"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: invalid port of node
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "id": 1,
+                    "nodes": [{
+                          "host": "127.0.0.1",
+                          "port": 0,
+                          "weight": 1
+                    }],
+                    "type": "chash"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: invalid host of node
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "id": 1,
+                    "nodes": [{
+                          "host": "127.#.%.1",
+                          "port": 8080,
+                          "weight": 1
+                    }],
+                    "type": "chash"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
+--- no_error_log
+[error]
diff --git a/t/admin/upstream.t b/t/admin/upstream.t
index da13134..6ec7b22 100644
--- a/t/admin/upstream.t
+++ b/t/admin/upstream.t
@@ -235,7 +235,7 @@ GET /t
 GET /t
 --- error_code: 400
 --- response_body
-{"error_msg":"invalid configuration: property \"nodes\" validation failed: expect object to have at least 1 properties"}
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
 
 
 
@@ -523,7 +523,7 @@ GET /t
 GET /t
 --- error_code: 400
 --- response_body
-{"error_msg":"invalid configuration: property \"nodes\" validation failed: failed to validate 127.0.0.1:8080 (matching \".*\"): wrong type: expected integer, got string"}
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
 --- no_error_log
 [error]
 
@@ -553,7 +553,7 @@ GET /t
 GET /t
 --- error_code: 400
 --- response_body
-{"error_msg":"invalid configuration: property \"nodes\" validation failed: failed to validate 127.0.0.1:8080 (matching \".*\"): expected -100 to be greater than 0"}
+{"error_msg":"invalid configuration: property \"nodes\" validation failed: object matches none of the requireds"}
 --- no_error_log
 [error]
 
diff --git a/t/apisix.luacov b/t/apisix.luacov
index f9792d8..0694c2b 100644
--- a/t/apisix.luacov
+++ b/t/apisix.luacov
@@ -28,6 +28,7 @@ return {
         ["apisix/plugins/prometheus/*"] = "plugins/prometheus",
         ["apisix/plugins/zipkin/*"] = "plugins/zipkin",
         ["apisix/utils/*"] = "utils",
+        ["apisix/discovery/*"] = "discovery",
 
         -- can not enable both at http and stream, will fix it later.
         --  ["apisix/stream/*"] = "stream",
diff --git a/t/discovery/eureka.t b/t/discovery/eureka.t
new file mode 100644
index 0000000..5ae26bf
--- /dev/null
+++ b/t/discovery/eureka.t
@@ -0,0 +1,131 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+sub read_file($) {
+    my $infile = shift;
+    open my $in, $infile
+        or die "cannot open $infile for reading: $!";
+    my $cert = do { local $/; <$in> };
+    close $in;
+    $cert;
+}
+
+our $yaml_config = read_file("conf/config.yaml");
+$yaml_config =~ s/node_listen: 9080/node_listen: 1984/;
+$yaml_config =~ s/enable_heartbeat: true/enable_heartbeat: false/;
+$yaml_config =~ s/config_center: etcd/config_center: yaml/;
+$yaml_config =~ s/enable_admin: true/enable_admin: false/;
+$yaml_config =~ s/enable_admin: true/enable_admin: false/;
+$yaml_config =~ s/  discovery:/  discovery: eureka #/;
+$yaml_config =~ s/#  discovery:/  discovery: eureka #/;
+$yaml_config =~ s/error_log_level: "warn"/error_log_level: "info"/;
+
+
+$yaml_config .= <<_EOC_;
+eureka:
+ host:
+   - "http://127.0.0.1:8761"
+ prefix: "/eureka/"
+ fetch_interval: 10
+ weight: 80
+ timeout:
+   connect: 1500
+   send: 1500
+   read: 1500
+_EOC_
+
+run_tests();
+
+__DATA__
+
+
+=== TEST 1: get APISIX-EUREKA info from EUREKA
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uri: /eureka/*
+    upstream:
+      service_name: APISIX-EUREKA
+      type: roundrobin
+
+#END
+--- request
+GET /eureka/apps/APISIX-EUREKA
+--- response_body_like
+.*<name>APISIX-EUREKA</name>.*
+--- error_log
+use config_center: yaml
+default_weight:80.
+fetch_interval:10.
+eureka uri:http://127.0.0.1:8761/eureka/.
+connect_timeout:1500, send_timeout:1500, read_timeout:1500.
+--- no_error_log
+[error]
+
+
+=== TEST 2: error service_name name
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uri: /eureka/*
+    upstream:
+      service_name: APISIX-EUREKA-DEMO
+      type: roundrobin
+
+#END
+--- request
+GET /eureka/apps/APISIX-EUREKA
+--- error_code: 502
+--- error_log eval
+qr/.*failed to pick server: no valid upstream node.*/
+
+
+=== TEST 3: with proxy-rewrite
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uri: /eureka-test/*
+    plugins:
+      proxy-rewrite:
+        regex_uri: ["^/eureka-test/(.*)", "/${1}"]
+    upstream:
+      service_name: APISIX-EUREKA
+      type: roundrobin
+
+#END
+--- request
+GET /eureka-test/eureka/apps/APISIX-EUREKA
+--- response_body_like
+.*<name>APISIX-EUREKA</name>.*
+--- error_log
+use config_center: yaml
+default_weight:80.
+fetch_interval:10.
+eureka uri:http://127.0.0.1:8761/eureka/.
+connect_timeout:1500, send_timeout:1500, read_timeout:1500.
+--- no_error_log
+[error]
+
diff --git a/t/node/upstream-array-nodes.t b/t/node/upstream-array-nodes.t
new file mode 100644
index 0000000..94ad68b
--- /dev/null
+++ b/t/node/upstream-array-nodes.t
@@ -0,0 +1,214 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+log_level('info');
+worker_connections(256);
+no_root_location();
+no_shuffle();
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: set upstream(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/upstreams/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "nodes": [{
+                        "host": "127.0.0.1",
+                        "port": 1980,
+                        "weight": 1
+                    }],
+                    "type": "roundrobin",
+                    "desc": "new upstream"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: set route(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "uri": "/hello",
+                        "upstream_id": "1"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: hit routes
+--- request
+GET /hello
+--- response_body
+hello world
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: set route(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "uri": "/hello",
+                        "upstream": {
+                            "nodes": [{
+                                "host": "127.0.0.1",
+                                "port": 1980,
+                                "weight": 1
+                            }],
+                            "type": "roundrobin",
+                            "desc": "new upstream"
+                        }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: hit routes
+--- request
+GET /hello
+--- response_body
+hello world
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: set services(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/services/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "upstream": {
+                            "nodes": [{
+                                "host": "127.0.0.1",
+                                "port": 1980,
+                                "weight": 1
+                            }],
+                            "type": "roundrobin",
+                            "desc": "new upstream"
+                        }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+=== TEST 7: set route(id: 1)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "uri": "/hello",
+                        "service_id": 1
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: hit routes
+--- request
+GET /hello
+--- response_body
+hello world
+--- no_error_log
+[error]