This is an automated email from the ASF dual-hosted git repository. spacewander pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push: new 6731ca1 feat: support SRV record (#3686) 6731ca1 is described below commit 6731ca148309c43c65ffcddb349c1c8c18fa46fb Author: 罗泽轩 <spacewander...@gmail.com> AuthorDate: Sat Feb 27 23:02:57 2021 +0800 feat: support SRV record (#3686) --- apisix/core/dns/client.lua | 68 +++++++++++++++++++++++++++++++++++++++ apisix/discovery/dns.lua | 4 +-- docs/en/latest/dns.md | 63 ++++++++++++++++++++++++++++++++++-- t/coredns/db.test.local | 21 ++++++++++++ t/discovery/dns/sanity.t | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 231 insertions(+), 5 deletions(-) diff --git a/apisix/core/dns/client.lua b/apisix/core/dns/client.lua index 8ace4b6..2b28542 100644 --- a/apisix/core/dns/client.lua +++ b/apisix/core/dns/client.lua @@ -18,8 +18,10 @@ local require = require local log = require("apisix.core.log") local json = require("apisix.core.json") local table = require("apisix.core.table") +local insert_tab = table.insert local math_random = math.random local package_loaded = package.loaded +local ipairs = ipairs local setmetatable = setmetatable @@ -29,6 +31,67 @@ local _M = { } +local function gcd(a, b) + if b == 0 then + return a + end + + return gcd(b, a % b) +end + + +local function resolve_srv(client, answers) + if #answers == 0 then + return nil, "empty SRV record" + end + + local resolved_answers = {} + local answer_to_count = {} + for _, answer in ipairs(answers) do + if answer.type ~= client.TYPE_SRV then + return nil, "mess SRV with other record" + end + + local resolved, err = client.resolve(answer.target) + if not resolved then + local msg = "failed to resolve SRV record " .. answer.target .. ": " .. err + return nil, msg + end + + log.info("dns resolve SRV ", answer.target, ", result: ", + json.delay_encode(resolved)) + + local weight = answer.weight + if weight == 0 then + weight = 1 + end + + local count = #resolved + answer_to_count[answer] = count + -- one target may have multiple resolved results + for _, res in ipairs(resolved) do + local copy = table.deepcopy(res) + copy.weight = weight / count + copy.port = answer.port + insert_tab(resolved_answers, copy) + end + end + + -- find the least common multiple of the counts + local lcm = answer_to_count[answers[1]] + for i = 2, #answers do + local count = answer_to_count[answers[i]] + lcm = count * lcm / gcd(count, lcm) + end + -- fix the weight as the weight should be integer + for _, res in ipairs(resolved_answers) do + res.weight = res.weight * lcm + end + + return resolved_answers +end + + function _M.resolve(self, domain, selector) local client = self.client @@ -45,6 +108,11 @@ function _M.resolve(self, domain, selector) if selector == _M.RETURN_ALL then log.info("dns resolve ", domain, ", result: ", json.delay_encode(answers)) + for _, answer in ipairs(answers) do + if answer.type == client.TYPE_SRV then + return resolve_srv(client, answers) + end + end return table.deepcopy(answers) end diff --git a/apisix/discovery/dns.lua b/apisix/discovery/dns.lua index d254db2..64ffe1d 100644 --- a/apisix/discovery/dns.lua +++ b/apisix/discovery/dns.lua @@ -52,7 +52,7 @@ function _M.nodes(service_name) 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} + nodes[i] = {host = r.address, weight = r.weight or 1, port = r.port or port} end end @@ -74,7 +74,7 @@ function _M.init_worker() hosts = {}, resolvConf = {}, nameservers = servers, - order = {"last", "A", "AAAA", "CNAME"}, -- avoid querying SRV (we don't support it yet) + order = {"last", "A", "AAAA", "SRV", "CNAME"}, } local client, err = core.dns_client.new(opts) diff --git a/docs/en/latest/dns.md b/docs/en/latest/dns.md index 7d074ba..909b688 100644 --- a/docs/en/latest/dns.md +++ b/docs/en/latest/dns.md @@ -22,6 +22,7 @@ title: DNS --> * [service discovery via DNS](#service-discovery-via-dns) + * [SRV record](#src-record) ## service discovery via DNS @@ -56,17 +57,16 @@ and `test.consul.service` be resolved as `1.1.1.1` and `1.1.1.2`, this result wi { "id": 1, "type": "roundrobin", - "nodes": { + "nodes": [ {"host": "1.1.1.1", "weight": 1}, {"host": "1.1.1.2", "weight": 1} - } + ] } ``` Note that all the IPs from `test.consul.service` share the same weight. If a service has both A and AAAA records, A record is preferred. -Currently we support A / AAAA records, SRV has not been supported yet. If you want to specify the port for the upstream server, you can add it to the `service_name`: @@ -78,3 +78,60 @@ If you want to specify the port for the upstream server, you can add it to the ` "type": "roundrobin" } ``` + +Another way to do it is via the SRV record, see below. + +### SRV record + +By using SRV record you can specify the port and the weight of a service. + +Assumed you have the SRV record like this: + +``` +; under the section of blah.service +A 300 IN A 1.1.1.1 +B 300 IN A 1.1.1.2 +B 300 IN A 1.1.1.3 +srv 86400 IN SRV 10 60 1980 A +srv 86400 IN SRV 10 20 1981 B +``` + +Upstream configuration like: + +```json +{ + "id": 1, + "discovery_type": "dns", + "service_name": "srv.blah.service", + "type": "roundrobin" +} +``` + +is the same as: + +```json +{ + "id": 1, + "type": "roundrobin", + "nodes": [ + {"host": "1.1.1.1", "port": 1980, "weight": 60}, + {"host": "1.1.1.2", "port": 1981, "weight": 10}, + {"host": "1.1.1.3", "port": 1981, "weight": 10} + ] +} +``` + +Note that two records of domain B split the weight evenly. + +As for 0 weight SRV record, the [RFC 2782](https://www.ietf.org/rfc/rfc2782.txt) says: + +> Domain administrators SHOULD use Weight 0 when there isn't any server +selection to do, to make the RR easier to read for humans (less +noisy). In the presence of records containing weights greater +than 0, records with weight 0 should have a very small chance of +being selected. + +We treat weight 0 record has a weight of 1 so the node "have a very small chance of +being selected", which is also the common way to treat this type of record. + +TODO: support priority. diff --git a/t/coredns/db.test.local b/t/coredns/db.test.local index fa2fb64..e4bb2fa 100644 --- a/t/coredns/db.test.local +++ b/t/coredns/db.test.local @@ -21,3 +21,24 @@ ipv6 IN AAAA ::1 ttl 300 IN A 127.0.0.1 ttl.1s 1 IN A 127.0.0.1 + +; SRV +A IN A 127.0.0.1 +B IN A 127.0.0.2 +C IN A 127.0.0.3 +C IN A 127.0.0.4 +; RFC 2782 style +_sip._tcp.srv 86400 IN SRV 10 60 1980 A +_sip._tcp.srv 86400 IN SRV 10 20 1980 B +; standard style +srv 86400 IN SRV 10 60 1980 A +srv 86400 IN SRV 10 20 1980 B + +port.srv 86400 IN SRV 10 60 1980 A +port.srv 86400 IN SRV 10 20 1981 B + +zero-weight.srv 86400 IN SRV 10 60 1980 A +zero-weight.srv 86400 IN SRV 10 0 1980 B + +split-weight.srv 86400 IN SRV 10 100 1980 A +split-weight.srv 86400 IN SRV 10 0 1980 C diff --git a/t/discovery/dns/sanity.t b/t/discovery/dns/sanity.t index 3ab4b20..89b67a1 100644 --- a/t/discovery/dns/sanity.t +++ b/t/discovery/dns/sanity.t @@ -186,3 +186,83 @@ upstreams: --- error_log invalid dns discovery configuration --- error_code: 500 + + + +=== TEST 8: SRV +--- apisix_yaml +upstreams: + - service_name: "srv.test.local" + discovery_type: dns + type: roundrobin + id: 1 +--- grep_error_log eval +qr/upstream nodes: \{[^}]+\}/ +--- grep_error_log_out eval +qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1980":20|"127.0.0.2:1980":20,"127.0.0.1:1980":60)\}/ +--- response_body +hello world + + + +=== TEST 9: SRV (RFC 2782 style) +--- apisix_yaml +upstreams: + - service_name: "_sip._tcp.srv.test.local" + discovery_type: dns + type: roundrobin + id: 1 +--- grep_error_log eval +qr/upstream nodes: \{[^}]+\}/ +--- grep_error_log_out eval +qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1980":20|"127.0.0.2:1980":20,"127.0.0.1:1980":60)\}/ +--- response_body +hello world + + + +=== TEST 10: SRV (different port) +--- apisix_yaml +upstreams: + - service_name: "port.srv.test.local" + discovery_type: dns + type: roundrobin + id: 1 +--- grep_error_log eval +qr/upstream nodes: \{[^}]+\}/ +--- grep_error_log_out eval +qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1981":20|"127.0.0.2:1981":20,"127.0.0.1:1980":60)\}/ +--- response_body +hello world + + + +=== TEST 11: SRV (zero weight) +--- apisix_yaml +upstreams: + - service_name: "zero-weight.srv.test.local" + discovery_type: dns + type: roundrobin + id: 1 +--- grep_error_log eval +qr/upstream nodes: \{[^}]+\}/ +--- grep_error_log_out eval +qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1980":1|"127.0.0.2:1980":1,"127.0.0.1:1980":60)\}/ +--- response_body +hello world + + + +=== TEST 12: SRV (split weight) +--- apisix_yaml +upstreams: + - service_name: "split-weight.srv.test.local" + discovery_type: dns + type: roundrobin + id: 1 +--- grep_error_log eval +qr/upstream nodes: \{[^}]+\}/ +--- grep_error_log_out eval +qr/upstream nodes: \{(,?"127.0.0.(1:1980":200|3:1980":1|4:1980":1)){3}\}/ +--- response_body +hello world