feat: support SRV record (#3686)

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 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 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 @@
 
     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 @@
     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 @@
         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 @@
 -->
 
 * [service discovery via DNS](#service-discovery-via-dns)
+    * [SRV record](#src-record)
 
 ## service discovery via DNS
 
@@ -56,17 +57,16 @@
 {
     "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 @@
     "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 @@
 
 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 @@
 --- 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