Skip to content

Commit

Permalink
feat: add dns discovery
Browse files Browse the repository at this point in the history
Fix #3517
Signed-off-by: spacewander <spacewanderlzx@gmail.com>
  • Loading branch information
spacewander committed Feb 22, 2021
1 parent 7dde426 commit b3c6ecf
Show file tree
Hide file tree
Showing 14 changed files with 615 additions and 36 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ install: default
$(INSTALL) -d $(INST_LUADIR)/apisix/core
$(INSTALL) apisix/core/*.lua $(INST_LUADIR)/apisix/core/

$(INSTALL) -d $(INST_LUADIR)/apisix/core/dns
$(INSTALL) apisix/core/dns/*.lua $(INST_LUADIR)/apisix/core/dns

$(INSTALL) -d $(INST_LUADIR)/apisix/cli
$(INSTALL) apisix/cli/*.lua $(INST_LUADIR)/apisix/cli/

Expand Down
1 change: 1 addition & 0 deletions apisix/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ return {
timer = require("apisix.core.timer"),
id = require("apisix.core.id"),
utils = utils,
dns_client = require("apisix.core.dns.client"),
etcd = require("apisix.core.etcd"),
tablepool = require("tablepool"),
empty_tab = {},
Expand Down
81 changes: 81 additions & 0 deletions apisix/core/dns/client.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
--
-- 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 require = require
local log = require("apisix.core.log")
local json = require("apisix.core.json")
local table = require("apisix.core.table")
local math_random = math.random
local package_loaded = package.loaded
local setmetatable = setmetatable


local _M = {
RETURN_RANDOM = 1,
RETURN_ALL = 2,
}


function _M.resolve(self, domain, selector)
local client = self.client

-- this function will dereference the CNAME records
local answers, err = client.resolve(domain)
if not answers then
return nil, "failed to query the DNS server: " .. err
end

if answers.errcode then
return nil, "server returned error code: " .. answers.errcode
.. ": " .. answers.errstr
end

if selector == _M.RETURN_ALL then
log.info("dns resolve ", domain, ", result: ", json.delay_encode(answers))
return table.deepcopy(answers)
end

local idx = math_random(1, #answers)
local answer = answers[idx]
local dns_type = answer.type
if dns_type == client.TYPE_A or dns_type == client.TYPE_AAAA then
log.info("dns resolve ", domain, ", result: ", json.delay_encode(answer))
return table.deepcopy(answer)
end

return nil, "unsupport DNS answer"
end


function _M.new(opts)
opts.ipv6 = true
opts.timeout = 2000 -- 2 sec
opts.retrans = 5 -- 5 retransmissions on receive timeout

-- make sure each client has its separate room
package_loaded["resty.dns.client"] = nil
local dns_client_mod = require("resty.dns.client")

local ok, err = dns_client_mod.init(opts)
if not ok then
return nil, "failed to init the dns client: " .. err
end

return setmetatable({client = dns_client_mod}, {__index = _M})
end


return _M
42 changes: 10 additions & 32 deletions apisix/core/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@
local config_local = require("apisix.core.config_local")
local core_str = require("apisix.core.string")
local table = require("apisix.core.table")
local json = require("apisix.core.json")
local log = require("apisix.core.log")
local string = require("apisix.core.string")
local dns_client = require("apisix.core.dns.client")
local ngx_re = require("ngx.re")
local dns_client = require("resty.dns.client")
local ipmatcher = require("resty.ipmatcher")
local ffi = require("ffi")
local base = require("resty.core.base")
local open = io.open
local math = math
local sub_str = string.sub
local str_byte = string.byte
local tonumber = tonumber
Expand All @@ -43,6 +41,7 @@ local ngx_sleep = ngx.sleep
local hostname
local dns_resolvers
local current_inited_resolvers
local current_dns_client
local max_sleep_interval = 1

ffi.cdef[[
Expand Down Expand Up @@ -84,54 +83,33 @@ function _M.split_uri(uri)
end


local function dns_parse(domain)
local function dns_parse(domain, selector)
if dns_resolvers ~= current_inited_resolvers then
local local_conf = config_local.local_conf()
local valid = table.try_read_attr(local_conf, "apisix", "dns_resolver_valid")
local enable_resolv_search_opt = table.try_read_attr(local_conf, "apisix",
"enable_resolv_search_opt")

local opts = {
ipv6 = true,
nameservers = table.clone(dns_resolvers),
retrans = 5, -- 5 retransmissions on receive timeout
timeout = 2000, -- 2 sec
order = {"last", "A", "AAAA", "CNAME"}, -- avoid querying SRV (we don't support it yet)
validTtl = valid,
order = {"last", "A", "AAAA", "CNAME"}, -- avoid querying SRV
}

opts.validTtl = valid

if not enable_resolv_search_opt then
opts.search = {}
end

local ok, err = dns_client.init(opts)
if not ok then
local client, err = dns_client.new(opts)
if not client then
return nil, "failed to init the dns client: " .. err
end

current_dns_client = client
current_inited_resolvers = dns_resolvers
end

-- this function will dereference the CNAME records
local answers, err = dns_client.resolve(domain)
if not answers then
return nil, "failed to query the DNS server: " .. err
end

if answers.errcode then
return nil, "server returned error code: " .. answers.errcode
.. ": " .. answers.errstr
end

local idx = math.random(1, #answers)
local answer = answers[idx]
local dns_type = answer.type
if dns_type == dns_client.TYPE_A or dns_type == dns_client.TYPE_AAAA then
log.info("dns resolve ", domain, ", result: ", json.delay_encode(answer))
return table.deepcopy(answer)
end

return nil, "unsupport DNS answer"
return current_dns_client:resolve(domain, selector)
end
_M.dns_parse = dns_parse

Expand Down
90 changes: 90 additions & 0 deletions apisix/discovery/dns.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
--
-- 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 core = require("apisix.core")
local config_local = require("apisix.core.config_local")
local ipairs = ipairs
local error = error


local dns_client
local schema = {
type = "object",
properties = {
servers = {
type = "array",
minItems = 1,
items = {
type = "string",
},
},
},
required = {"servers"}
}


local _M = {}


function _M.nodes(service_name)
local host, port = core.utils.parse_addr(service_name)
core.log.info("discovery dns with host ", host, ", port ", port)

local records, err = dns_client:resolve(host, core.dns_client.RETURN_ALL)
if not records then
return nil, err
end

local nodes = core.table.new(#records, 0)
for i, r in ipairs(records) do
if r.address then
nodes[i] = {host = r.address, weight = 1, port = port}
end
end

return nodes
end


function _M.init_worker()
local local_conf = config_local.local_conf()
local ok, err = core.schema.check(schema, local_conf.discovery.dns)
if not ok then
error("invalid dns discovery configuration: " .. err)
return
end

local servers = core.table.try_read_attr(local_conf, "discovery", "dns", "servers")

local opts = {
hosts = {},
resolvConf = {},
nameservers = servers,
order = {"last", "A", "AAAA", "CNAME"}, -- avoid querying SRV (we don't support it yet)
}

local client, err = core.dns_client.new(opts)
if not client then
error("failed to init the dns client: ", err)
return
end

dns_client = client
end


return _M
7 changes: 6 additions & 1 deletion apisix/upstream.lua
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,12 @@ function _M.set_by_route(route, api_ctx)
if not dis then
return 500, "discovery " .. up_conf.discovery_type .. " is uninitialized"
end
local new_nodes = dis.nodes(up_conf.service_name)

local new_nodes, err = dis.nodes(up_conf.service_name)
if not new_nodes then
return http_code_upstream_unavailable, "no valid upstream node: " .. err
end

local same = upstream_util.compare_upstream_node(up_conf.nodes, new_nodes)
if not same then
up_conf.nodes = new_nodes
Expand Down
5 changes: 4 additions & 1 deletion conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ etcd:
verify: true # whether to verify the etcd endpoint certificate when setup a TLS connection to etcd,
# the default value is true, e.g. the certificate will be verified strictly.

# discovery: # service discovery center
discovery: # service discovery center
# dns:
# resolver:
# - "127.0.0.1:8600" # use the real address of your dns server
# eureka:
# host: # it's possible to define multiple eureka hosts addresses of the same eureka cluster.
# - "http://127.0.0.1:8761"
Expand Down
1 change: 1 addition & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* [Stream Proxy](stream-proxy.md)
* [gRPC Proxy](grpc-proxy.md)
* [Customize Nginx Configuration](./customize-nginx-configuration.md)
* [DNS](./dns.md)
* [Changelog](../CHANGELOG.md)
* [Benchmark](benchmark.md)
* [Code Style](../CODE_STYLE.md)
Expand Down
13 changes: 11 additions & 2 deletions doc/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
# Integration service discovery registry

* [**Summary**](#summary)
* [**How extend the discovery client?**](#how-extend-the-discovery-client)
* [**Supported discovery registries**](#supported-discovery-registries)
* [**How to extend the discovery client?**](#how-to-extend-the-discovery-client)
* [**Basic steps**](#basic-steps)
* [**the example of Eureka**](#the-example-of-eureka)
* [**Implementation of eureka.lua**](#implementation-of-eurekalua)
Expand All @@ -43,7 +44,15 @@ When system traffic changes, the number of servers of the upstream service also

Common registries: Eureka, Etcd, Consul, Zookeeper, Nacos etc.

## How extend the discovery client?
## Supported discovery registries

Currently we support Eureka and service discovery via DNS, like Consul.

For service discovery via DNS, see [service discovery via DNS](dns.md#service-discovery-via-dns).

For Eureka, see below.

## How to extend the discovery client?

### Basic steps

Expand Down
Loading

0 comments on commit b3c6ecf

Please sign in to comment.