50

I use nginx to proxy and hold persistent connections to far away servers for me.

I have configured about 15 blocks similar to this example:

upstream rinu-test {
    server test.rinu.test:443;
    keepalive 20;
}
server {
    listen 80;
    server_name test.rinu.test;
    location / {
        proxy_pass https://rinu-test;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $http_host;
    }
}

The problem is if the hostname can not be resolved in one or more of the upstream blocks, nginx will not (re)start. I can't use static IPs either, some of these hosts explicitly said not to do that because IPs will change. Every other solution I've seen to this error message says to get rid of upstream and do everything in the location block. That it not possible here because keepalive is only available under upstream.

I can temporarily afford to lose one server but not all 15.

Edit: Turns out nginx is not suitable for this use case. An alternative backend (upstream) keepalive proxy should be used. A custom Node.js alternative is in my answer. So far I haven't found any other alternatives that actually work.

rinu
  • 745
  • 1
  • 5
  • 11
  • There are two things you can try. Change `proxy_pass https://rinu-test;` to `proxy_pass $proxyurl;` and before that you can set the variable `set $proxyurl $scheme://$host$request_uri` And next is to try use variable in upstream, I have not tested the 2nd option and can't verify yet. But using a variable in `proxy_pass` disables dns caching in nginx – Tarun Lalwani May 11 '18 at 15:44
  • Proxying without the upstream is pointless. Variables cannot be used in upstream. – rinu May 12 '18 at 17:04
  • I meant you could try something like `proxy_pass https://rinu-test$request_uri;` – Tarun Lalwani May 13 '18 at 02:39
  • How about exploring HAProxy instead of nginx for this? If you use upstreams then this use case cannot be handled by default – Tarun Lalwani May 18 '18 at 12:56
  • I tried HAProxy but it didn't work. It did proxy but didn't keep connections open or failed to reuse them. – rinu May 18 '18 at 13:30
  • It should have keep-alive enabled by default? https://stackoverflow.com/questions/46966813/how-to-enable-keep-alive-in-haproxy – Tarun Lalwani May 18 '18 at 17:22
  • That is the theory yes but I guess it only works on the frontend side. My testing has clearly shown that it doesn't work on backend side. I tried all the options, spent like 4 hours on it. – rinu May 19 '18 at 19:09

5 Answers5

17

Earlier versions of nginx (before 1.1.4), which already powered a huge number of the most visited websites worldwide (and some still do even nowdays, if the server headers are to be believed), didn't even support keepalive on the upstream side, because there is very little benefit for doing so in the datacentre setting, unless you have a non-trivial latency between your various hosts; see https://serverfault.com/a/883019/110020 for some explanation.

Basically, unless you know you specifically need keepalive between your upstream and front-end, chances are it's only making your architecture less resilient and worse-off.

(Note that your current solution is also wrong because a change in the IP address will likewise go undetected, because you're doing hostname resolution at config reload only; so, even if nginx does start, it'll basically stop working once IP addresses of the upstream servers do change.)

Potential solutions, pick one:

  • The best solution would seem to just get rid of upstream keepalive as likely unnecessary in a datacentre environment, and use variables with proxy_pass for up-to-date DNS resolution for each request (nginx is still smart-enough to still do the caching of such resolutions)

  • Another option would be to get a paid version of nginx through a commercial subscription, which has a resolve parameter for the server directive within the upstream context.

  • Finally, another thing to try might be to use a set variable and/or a map to specify the servers within upstream; this is neither confirmed nor denied to have been implemented; e.g., it may or may not work.

cnst
  • 21,785
  • 2
  • 73
  • 108
  • I know what I'm doing. Keepalive has made requests to external service providers up to a whole 1 second faster and saving time in that application is critical. – rinu May 12 '18 at 16:58
  • 1
    Variables cannot be used to define the server in upstream. – rinu May 12 '18 at 16:59
  • Paid nginx is too expensive. It's cheaper to rewrite the application that needs this proxy. Which is already in progress but I would estimate another 3-4 years to complete it. – rinu May 12 '18 at 17:09
  • @rinu TBH, if keepalive between the front-end and backend shaves a whole second, it sounds like the whole architecture of the application may be quite suboptimal. Why is there so much latency? Do you proxy_pass between Europe and America/Asia? If so, might as well make more sense to put some extra front-end servers closer to where your backends actually live. Else, there shouldn't be that much latency, or, perhaps if you're doing in-the-field IoT, then the data being proxied should probably be processed outside of the context of a proxy server first. – cnst May 13 '18 at 04:02
  • 1
    There are about 100 independent service providers all over the world, mainly in America and Europe. Several are queried in parallel, at most I've seen about 20. In one service provider's example average response times dropped from 1.3 to 0.5 seconds. The lowest gain was 150 ms. So far none have been slower with the keepalive proxy. The application is in PHP which can make requests in parallel but due to it's short lifespan nature can't hold connections open. – rinu May 14 '18 at 06:32
  • 2
    @rinu, I don't see how you could have 1.3s savings from a single reuse of a connection through keepalive, it seems like you may be measuring something else than the keepalive savings. Also, I'm kinda confused about the usecase here — you're basically trying to query 100 independent providers through PHP, and use nginx as a connection cache? Then your question is very poorly formulated for what you're trying to accomplish, as there'd be many other better ways to do keepalive, possibly even with nginx stream — you unfairly restrict the scope of the solution through an incomplete specification. – cnst May 14 '18 at 14:19
  • That request on average used to take 1.3s and now take 0.5s. Keealive saved the difference, 0.8s. Request times are measured by curl. At this point I'm convinced I shouldn't even use nginx for this, alternatives are being investigated. – rinu May 15 '18 at 08:03
9

Your scenario is very similar to the one when using aws ELB as uptreams in where is critical to resolve the proper IP of the defined domain.

The first thing you need to do and ensure is that the DNS servers you are using can resolve to your domains, then you could create your config like this:

resolver 10.0.0.2 valid=300s;
resolver_timeout 10s;

location /foo {
    set $foo_backend_servers foo_backends.example.com;
    proxy_pass http://$foo_backend_servers;
 }

location /bar {
    set $bar_backend_servers bar_backends.example.com;
    proxy_pass http://$bar_backend_servers;
 }

Notice the resolver 10.0.0.2 it should be IP of the DNS server that works and answer your queries, depending on your setup this could be a local cache service like unbound. and then just use resolve 127.0.0.1

Now, is very important to use a variable to specify the domain name, from the docs:

When you use a variable to specify the domain name in the proxy_pass directive, NGINX re‑resolves the domain name when its TTL expires.

You could check your resolver by using tools like dig for example:

$ dig +short stackoverflow.com

In case is a must to use keepalive in the upstreams, and if is not an option to use Nginx +, then you could give a try to openresty balancer, you will need to use/implement lua-resty-dns

nbari
  • 20,729
  • 5
  • 51
  • 86
  • Could you please go into more detail about openresty, lua and how these could be used here? I haven't heard of these before and it's not obvious from the documentation how to even install these or how to apply to my use case. – rinu May 16 '18 at 07:29
  • Hi @rinu, I found this gist probably can give you some more ideas: https://gist.github.com/toritori0318/f9be21fb8df9e4d5768bb5f484567175 `lua` + `nginx` works pretty nice also you could extend and implement a WAF etc, and cover many of the things missing on the standard Nginx – nbari May 16 '18 at 07:33
3

A one possible solution is to involve a local DNS cache. It can be a local DNS server like Bind or Dnsmasq (with some crafty configuration, note that nginx can also use specified dns server in place of the system default), or just maintaining the cache in hosts file.

It seems that using hosts file with some scripting is quite straightforward way. The hosts file should be spitted into the static and dynamic parts (i.e. cat hosts.static hosts.dynamic > hosts), and the dynamic part should be generated (and updated) automatically by a script.

Perhaps it make sense to check from time to time the hostnames for changing IPs, and update hosts file and reload configuration in nginx on changes. In case of some hostname cannot be resolved the old IP or some default IP (like 127.0.1.9) should be used.

If you don't need the hostnames in the nginx config file (i.e., IPs are enough), the upstream section with IPs (resolved hostnames) can be generated by a script and included into nginx config — and no need to touch the hosts file in such case.

ruvim
  • 5,241
  • 2
  • 19
  • 27
  • We already had the DNS cache idea and it will be investigated soon. My sysadmin had a big laugh about the generated `hosts` file. Guess it's not happening. But I like your answer anyway, so far it's the only one that would actually work. – rinu May 14 '18 at 13:45
  • @rinu, could you please mention some constructive argument what is wrong with using hosts file? – ruvim May 14 '18 at 15:30
  • Nothing wrong with it which is why I voted you up. Managing this in all the servers is just too much work. Alternatives to nginx or a better general DNS service are way easier to manage. – rinu May 15 '18 at 08:07
1

I put the resolve parameter on server and you need to set the Nginx Resolver in nginx.conf as below:

/etc/nginx/nginx.conf:

http {
    resolver 192.168.0.2 ipv6=off valid=40s;  # The DNS IP server
} 

Site.conf:

upstream rinu-test {
    server test.rinu.test:443;
    keepalive 20;
}
Bruno Paiuca
  • 103
  • 6
  • i think that is for Nginx+, but if you set location /foo { set $foo_backend_servers foo_backends.example.com; proxy_pass http://$foo_backend_servers; } will work. If not solve post your conf and logs – Bruno Paiuca May 16 '18 at 11:15
  • http://nginx.org/r/resolver is a standard directive, it's not restricted to nginx plus at all; it's the `resolve` keyword within the upstream context that's Plus-only; however, this question completely ignores the keepalive issue that the OP specifically mentions that they require. – cnst May 16 '18 at 15:45
  • you are correct, resolver is open and the resolve parameter form the server command is a on-the-fly reconfiguration option. If you use the variable you will lost keepalive and if you use the upstream you will lost a DNS check that will check only in startup time. – Bruno Paiuca May 16 '18 at 17:13
  • This doe snot seem to wokr to solve the original problem - setting the global resolver option does nothing: nginx still doesn't start/restart it if tcan't resolve hosts in the upstream block. – Remember Monica May 03 '21 at 19:06
  • The name always must be resolved, but the resolved offer the option to keep the DNS up-to-date without lost the upstream configuration. Many users try to use the dns directly in the proxy_pass and fail when whatever lag impact the DNS resolution, besides remove optional confirations in the upstream like keepalive and different LB algorithm – Bruno Paiuca May 03 '21 at 19:35
-3

An alternative is to write a new service that only does what I want. The following replaces nginx for proxying https connections using Node.js

const http = require('http');
const https = require('https');

const httpsKeepAliveAgent = new https.Agent({ keepAlive: true });

http.createServer(onRequest).listen(3000);

function onRequest(client_req, client_res) {
    https.pipe(
        protocol.request({
            host: client_req.headers.host,
            port: 443,
            path: client_req.url,
            method: client_req.method,
            headers: client_req.headers,
            agent: httpsKeepAliveAgent
        }, (res) => {
            res.pipe(client_res);
        }).on('error', (e) => {
            client_res.end();
        })
    );
}

Example usage: curl http://localhost:3000/request_uri -H "Host: test.rinu.test" which is equivalent to: curl https://test.rinu.test/request_uri

rinu
  • 745
  • 1
  • 5
  • 11
  • 7
    This solution is just out of the question scope, since the question is about nginx only (i.e. how to solve the issue with nginx). It is need to reformulate the question to make it conform to this solution :) – ruvim May 18 '18 at 09:11
  • In it's current form the question doesn't have an answer. What I'm asking is simply not possible. I also agree that this answer is no exception. But in the end this is how I solved my problem which is why I included it here. – rinu May 18 '18 at 12:03