[PATCH 2 of 2] The "sort=" parameter of the "resolver" directive

Sergey Kandaurov pluknet at nginx.com
Tue Jul 12 14:59:39 UTC 2022


> On 8 Jul 2022, at 04:35, Maxim Dounin <mdounin at mdounin.ru> wrote:
> 
> Hello!
> 
> On Thu, Jul 07, 2022 at 07:49:51PM +0400, Sergey Kandaurov wrote:
> 
>>>> @@ -4250,7 +4269,27 @@ ngx_resolver_export(ngx_resolver_t *r, n
>>>>    }
>>>> 
>>>>    i = 0;
>>>> -    d = rotate ? ngx_random() % n : 0;
>>>> +
>>>> +    switch (r->prefer) {
>>>> +
>>>> +#if (NGX_HAVE_INET6)
>>>> +    case NGX_RESOLVE_PREFER_A:
>>>> +        d = 0;
>>>> +        break;
>>>> +
>>>> +    case NGX_RESOLVE_PREFER_AAAA:
>>>> +        d = rn->naddrs6;
>>>> +
>>>> +        if (d == n) {
>>>> +            d = 0;
>>>> +        }
>>>> +
>>>> +        break;
>>>> +#endif
>>>> +
>>>> +    default:
>>>> +        d = rotate ? ngx_random() % n : 0;
>>>> +    }
>>> 
>>> With this code, a configuration like this:
>>> 
>>>   resolver ... prefer=ipv4;
>>>   set $foo "";
>>>   proxy_pass http://example.com$foo;
>>> 
>>> will result in only IPv4 addresses being used assuming successful 
>>> connections, and IPv6 addresses being used only as a backup.  This 
>>> looks quite different from the current behaviour, as well as from 
>>> what we do with
>>> 
>>>   proxy_pass http://example.com;
>>> 
>>> when using system resolver.
>> 
>> Can you please elaborate, what specific concerns are you referring to?
>> The prefer option implements exactly the expected behaviour:
>> first, a flat array is populated with preferred addresses
>> (IPv4 for "prefer=ipv4", if any), then - with the rest, such as IPv6.
>> The API user iterates though them until she gets a "successful" address.
>> 
>> If the name is in the resolver cache, then rotation is also applied.
>> The default nginx resolver behaviour is to rotate resolved addresses
>> regardless of address families.  Unlike this, in case of "prefer=ipv4",
>> addresses are rotated within address families, that is, AFs are sorted:
>> ipv4_x, ipv4_y, ipv4_z; ipv6_x, ipv6_y, ipv6_z
>> 
>> This is close to how system resolver is used with getaddrinfo(), which
>> depends on a preference and, if applicable, AF/address reachability.
> 
> Try the two above configurations with a name which resolves to 
> 127.0.0.1 and ::1, and with both addresses responding on port 80.  
> Configuration without variables (using system resolver) will 
> balance requests between both addresses, regardless of system 
> resolver settings.  Configuration with variables and resolver with 
> "prefer=ipv4" will use only the IPv4 address.
> 
>    server {
>        listen localhost:8080;
> 
>        location /dynamic/ {
>             resolver 8.8.8.8 prefer=ipv4;
>             set $foo "";
>             proxy_pass http://test.mdounin.ru:8081$foo;
>        }
> 
>        location /static/ {
>             proxy_pass http://test.mdounin.ru:8082;
>        }
>    }
> 
>    server {
>        listen test.mdounin.ru:8081;
>        listen test.mdounin.ru:8082;
>        return 200 $server_addr\n;
>    }
> 
> Static configuration without variables uses both addresses:
> 
> $ curl http://127.0.0.1:8080/static/
> 127.0.0.1
> $ curl http://127.0.0.1:8080/static/
> ::1
> $ curl http://127.0.0.1:8080/static/
> 127.0.0.1
> $ curl http://127.0.0.1:8080/static/
> ::1
> 
> Dynamic configuration with "prefer=ipv4" will only use IPv4 (IPv6 
> addresses will be used only in case of errors):
> 
> $ curl http://127.0.0.1:8080/dynamic/
> 127.0.0.1
> $ curl http://127.0.0.1:8080/dynamic/
> 127.0.0.1
> $ curl http://127.0.0.1:8080/dynamic/
> 127.0.0.1
> $ curl http://127.0.0.1:8080/dynamic/
> 127.0.0.1
> 
> [...]

Thanks for clarification.

> 
>>> Not sure we want to introduce such behaviour.  While it might be 
>>> closer to what RFC 6724 recommends for clients, it is clearly not 
>>> in line with how we handle multiple upstream addresses in general, 
>>> and certainly will confuse users.  If we want to introduce this, 
>>> it probably should be at least consistent within resolver vs. 
>>> system resolver cases.
>> 
>> If you refer to how we balance though multiple addresses in upstream
>> implicitly defined with proxy_pass vs. proxy_pass with variable, then
>> I tend to agree with you.  In implicitly defined upstream, addresses
>> are selected with rr balancer, which eventually makes them tried all.
>> OTOH, the prefer option used for proxy_pass with variable, factually
>> moves the unprefer addresses to backup, and since the upstream group
>> isn't preserved across requests, this makes them almost never tried.
>> But this is how proxy_pass with variable is used to work.
> 
> Yes, I refer to the difference in handling of multiple upstream 
> addresses which is introduced with this change.  Right now there 
> are no difference in the behaviour of static proxy_pass (without 
> variables) and dynamic one (with variables).  With "prefer=ipv4" 
> as implemented the difference appears, and this certainly breaks 
> POLA.
> 
> One possible option would be to change "prefer=" to rotate all 
> addresses, so proxy_pass will try them all.  With this approach, 
> "prefer=ipv4" would be exactly equivalent to the default behaviour 
> (on the first resolution, resolver returns list of all IPv4 
> addresses, followed by all IPv6 addresses, and then addresses are 
> rotated) and "prefer=ipv6" would use the reverse order on the 
> first resolution (IPv6 followed by IPv4).  Not sure it is at all 
> needed though (but might be still beneficial for tasks like OCSP 
> server resolution).

Updated patch to rotate regardless of preference.
Below is hg diff -w on top off of the previous one, for clarity:

diff -r d9a8c2d87055 src/core/ngx_resolver.c
--- a/src/core/ngx_resolver.c   Wed Jul 06 17:10:24 2022 +0400
+++ b/src/core/ngx_resolver.c   Tue Jul 12 18:55:56 2022 +0400
@@ -4270,6 +4270,10 @@ ngx_resolver_export(ngx_resolver_t *r, n
 
     i = 0;
 
+    if (rotate) {
+        d = ngx_random() % n;
+
+    } else {
     switch (r->prefer) {
 
 #if (NGX_HAVE_INET6)
@@ -4288,7 +4292,8 @@ ngx_resolver_export(ngx_resolver_t *r, n
 #endif
 
     default:
-        d = rotate ? ngx_random() % n : 0;
+            d = 0;
+        }
     }
 
     if (rn->naddrs) {

Personally, I think such patch has a little sense.  Keeping preference
is reasonable e.g. to avoid connections over certain address family,
should they be slow/tunneled or otherwise uneven, which I believe is
quite common in practice.  Such way, they would be always tried as a
last resort and indeed can be seen as a backup option.
In that sense, I'm rather skeptical about rotation behaviour in static
proxy_pass (by round-robin means) that doesn't distinguish address families
and apparently is a legacy from times (4d68c486fcb0) before URLs obtained
IPv6 support in eaf95350d75c.
Changing it requires more investment though and certainly breaks POLA.
OTOH, with "ipv4=" introduction, such unpreferred connections can be
simply disabled.

# HG changeset patch
# User Ruslan Ermilov <ru at nginx.com>
# Date 1657113024 -14400
#      Wed Jul 06 17:10:24 2022 +0400
# Node ID 496a9b8d14048295723bd3e21ef4a9a1129cb97c
# Parent  d8c0fb82b7a8d3f958b13db06f2f911d95a41644
The "prefer=" parameter of the "resolver" directive.

diff -r d8c0fb82b7a8 -r 496a9b8d1404 src/core/ngx_resolver.c
--- a/src/core/ngx_resolver.c	Wed Jul 06 17:09:40 2022 +0400
+++ b/src/core/ngx_resolver.c	Wed Jul 06 17:10:24 2022 +0400
@@ -227,6 +227,7 @@ ngx_resolver_create(ngx_conf_t *cf, ngx_
         }
 
 #if (NGX_HAVE_INET6)
+
         if (ngx_strncmp(names[i].data, "ipv4=", 5) == 0) {
 
             if (ngx_strcmp(&names[i].data[5], "on") == 0) {
@@ -260,6 +261,24 @@ ngx_resolver_create(ngx_conf_t *cf, ngx_
 
             continue;
         }
+
+        if (ngx_strncmp(names[i].data, "prefer=", 7) == 0) {
+
+            if (ngx_strcmp(&names[i].data[7], "ipv4") == 0) {
+                r->prefer = NGX_RESOLVE_PREFER_A;
+
+            } else if (ngx_strcmp(&names[i].data[7], "ipv6") == 0) {
+                r->prefer = NGX_RESOLVE_PREFER_AAAA;
+
+            } else {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid parameter: %V", &names[i]);
+                return NULL;
+            }
+
+            continue;
+        }
+
 #endif
 
         ngx_memzero(&u, sizeof(ngx_url_t));
@@ -4250,7 +4269,32 @@ ngx_resolver_export(ngx_resolver_t *r, n
     }
 
     i = 0;
-    d = rotate ? ngx_random() % n : 0;
+
+    if (rotate) {
+        d = ngx_random() % n;
+
+    } else {
+        switch (r->prefer) {
+
+#if (NGX_HAVE_INET6)
+        case NGX_RESOLVE_PREFER_A:
+            d = 0;
+            break;
+
+        case NGX_RESOLVE_PREFER_AAAA:
+            d = rn->naddrs6;
+
+            if (d == n) {
+                d = 0;
+            }
+
+            break;
+#endif
+
+        default:
+            d = 0;
+        }
+    }
 
     if (rn->naddrs) {
         j = rotate ? ngx_random() % rn->naddrs : 0;
diff -r d8c0fb82b7a8 -r 496a9b8d1404 src/core/ngx_resolver.h
--- a/src/core/ngx_resolver.h	Wed Jul 06 17:09:40 2022 +0400
+++ b/src/core/ngx_resolver.h	Wed Jul 06 17:10:24 2022 +0400
@@ -36,6 +36,9 @@
 
 #define NGX_RESOLVER_MAX_RECURSION    50
 
+#define NGX_RESOLVE_PREFER_A          1
+#define NGX_RESOLVE_PREFER_AAAA       2
+
 
 typedef struct ngx_resolver_s  ngx_resolver_t;
 
@@ -175,6 +178,8 @@ struct ngx_resolver_s {
     ngx_queue_t               srv_expire_queue;
     ngx_queue_t               addr_expire_queue;
 
+    unsigned                  prefer:2;
+
     unsigned                  ipv4:1;
 
 #if (NGX_HAVE_INET6)

-- 
Sergey Kandaurov



More information about the nginx-devel mailing list