News for hackers. Stuff that matters.
Re:How We Designed for Performance and Scale by nodesocket on Wednesday June 10, 2015 @02:05PM (#9694142)
"NGINX’s binary upgrade process achieves the holy grail of high-availability; you can upgrade the software on the fly, without any dropped connections, downtime or interruption in service."

Is this really true? I remember seeing an article[1] recently on using an iptables hack to prevent dropping connections when reloading haproxy. Does nginx actually provide zero-downtime configuration reloads?

[1] https://medium.com/@Drew_Stokes/actual-zero-downtime-with-ha...

Re:How We Designed for Performance and Scale by ploxiln on Wednesday June 10, 2015 @02:32PM (#9694339)
I didn't want to be a negative nancy in the comments for that haproxy article... but that is a ridiculous ugly hack. It's really a lot easier to achieve real and robust zero-downtime upgrades for a simple unix process.

Remember, fork()ed and exec()ed processes inherit file descriptors (except those marked CLOEXEC), including the listen() fd. Pending connections will queue in the kernel until userspace calls accept() on the listening fd.

So one simple model is to stop calling accept(), cleanly/quickly finish up current connections, set an environment variable to tell the future instance that the listening fd X is already open, and exec your own binary again.

A more complicated one is to fork, have the (identical) child just finish the current connections, while the parent execs itself similar to above. (The client connection fds should be marked CLOEXEC in this case.)

With a more complicated service with more moving parts, libraries, threads, getting the above to work out is more complicated. But that's basically how you want to do it.

Re:How We Designed for Performance and Scale by mortehu on Wednesday June 10, 2015 @04:11PM (#9694953)
I program all my server programs to have a --listen-fd=N command line option. That way, the process that starts the server is responsible for creating the listening socket, and it can create multiple server instances using the same socket. The servers can then be programmed to handle SIGUSR1 by no longer calling accept, and terminating when the last client exits. This way, the amount of code required in each server program is tiny; less than 15 lines, with no calls to fork or execve.

systemd already supports creating listening sockets, but its mode of operation is more similar to inetd. I think supporting something like what my proprietary launcher program does would require very little changes.

Another advantage of passing the listening socket like this is that the server process can be in a private network namespace, without requiring any type of NAT setup or port mapping.

Re:How We Designed for Performance and Scale by Denzel on Wednesday June 10, 2015 @02:31PM (#9694330)
Yes, nginx allows you to upgrade the binary and reload the configuration without any downtime: http://wiki.nginx.org/CommandLine#Upgrading_To_a_New_Binary_...
Re:How We Designed for Performance and Scale by yeukhon on Wednesday June 10, 2015 @08:50PM (#9696710)
Does this work when you update with package manager?
Re:How We Designed for Performance and Scale by LukeShu on Thursday June 11, 2015 @12:25AM (#9697536)
Yes.
Re:How We Designed for Performance and Scale by gull on Wednesday June 10, 2015 @06:22PM (#9696020)
The holy grail of high-availability isn't upgrading stateless software. It's upgrading stateful ones.

Like upgrading when a data structure changes between versions. The HTTP protocol nginx serves is stateless and by comparison far simpler. Same goes for Erlang. It offers nothing more than simple function replacement, and that's not enough to handle data structure changes either.

Re:How We Designed for Performance and Scale by toast0 on Wednesday June 10, 2015 @07:01PM (#9696240)
In Erlang, if you need a new data structure for your state, you can check if your state is old, and upgrade it and continue on. In a gen_server, you might have something like

  handle_call(Request, From, State) when is_record(State, state) -> handle_call(Request, From, upgrade_state(State);
  handle_call(Request, From, State) when is_record(State, state2) -> ...
(you'll want to do something similar on handle_cast and handle_info if you use those). You have to do a little work, but I don't see how you avoid that?
Re:How We Designed for Performance and Scale by gull on Wednesday June 10, 2015 @11:12PM (#9697227)
That's not guaranteed to be safe.

Having new code check if your state is old and upgrading isn't enough. You also need to check old code doesn't process new state. That becomes harder under concurrency.

http://en.wikipedia.org/wiki/Dynamic_software_updating#Updat...

Erlang doesn't offer this check.

"Old code may still be evaluated because of processes lingering in the old code."

http://www.erlang.org/doc/reference_manual/code_loading.html...

Re:How We Designed for Performance and Scale by serialx on Thursday June 11, 2015 @12:06AM (#9697459)
If you write the code within the `gen_server` guidelines, state migration is supported by `code_change`:

http://www.erlang.org/doc/man/gen_server.html#Module:code_ch...

For example:

http://stackoverflow.com/questions/1840717/achieving-code-sw...

BTW, You can even support downgrade. :)

Re:How We Designed for Performance and Scale by toast0 on Thursday June 11, 2015 @01:38AM (#9697816)
> Having new code check if your state is old and upgrading isn't enough. You also need to check old code doesn't process new state. That becomes harder under concurrency.

If we're talking about a gen_server, the state is per process, and once the process has switched to the new code, it won't go back, so there's no problem with old code and new state. In non gen_server code, you do need to be careful about when you hit a boundary that gets you into new code; you'd typically want it to be your process's main loop, since that usually tail recurses and doesn't leave a stack in the old code. It is difficult to reason about a situation where you call into new code, and that returns to old code; it's much better to avoid it.

The concurrent case is OK too, each process manages its own state, and upgrades it when it switches to new code. Are you thinking about changes to messages that are being passed and/or global state? In that case, like with any distributed system, you need to load in stages: first load code that can handle old and new messages, then trigger sending new messages (code load or config setting), then load code that only handles new messages.

Re:How We Designed for Performance and Scale by gull on Thursday June 11, 2015 @02:57AM (#9698064)
You touched on most points, primarily avoiding the situation of reasoning about concurrent old and new code. I didn't know a gen_server manages code loading like this, thank you for that.

An upgrade doesn't involve only the in-memory state per process though. It also involves state outside the process, like state on disk. Even if each process upgrades it's own state (I'm assuming the gen_server isn't limited to in-memory state; I don't know), an old process accessing from disk a data structure that differs from the one used by the new process isn't safe. You can't just upgrade old processes in stages.

An upgrade can also involve multiple processes. It's hard to upgrade all of them at once. As you mentioned, in the hardest case of all, a distributed system, loading in stages may be the only option, provided the system was explicitly designed such that old and new processes can coexist without safety issues.

http://pmg.csail.mit.edu/upgrades/

Re:How We Designed for Performance and Scale by vardump on Wednesday June 10, 2015 @06:26PM (#9696045)
Synchronize to a checkpoint. Serialize all state. Run data structure upgrading code. Deserialize upgraded state.

Of course possible to do in-place, but I'd imagine serialization makes testing it easier. Not to mention sending error reports if something goes wrong. Having all that state in a bug report could help a bit!

Re:How We Designed for Performance and Scale by Thaxll on Wednesday June 10, 2015 @10:03PM (#9697033)
What about running websockets?
Re:How We Designed for Performance and Scale by gregham on Wednesday June 10, 2015 @04:33PM (#9695142)
Yes, it's true. NGINX does zero-downtime configuration reloads and binary upgrades since 2004 without any dirty hacks.
Re:How We Designed for Performance and Scale by 0x0 on Wednesday June 10, 2015 @04:18PM (#9695005)
The irc client irssi has something like this in /upgrade where it spawns a new binary but passing along active socket connections and their associated irc room states, I believe
Re:How We Designed for Performance and Scale by lorenzhs on Wednesday June 10, 2015 @04:38PM (#9695180)
WeeChat definitely does this. I don't know whether irssi supports it as well. Too bad WeeChat can't keep TLS connections open on /upgrade though
Re:How We Designed for Performance and Scale by zobzu on Thursday June 11, 2015 @04:21AM (#9698390)
yeah weechat is one of the rare C apps to actually preserve state on upgrade (passes fds, saves state, load in new process, kill old process)

its not that hard to do if the program is made for this, even in C, its pretty hard to add when it isnt tho since you need a compat. layer.

Sometimes the new program simply has import functions for the old memory layout.

Re:How We Designed for Performance and Scale by MCRed on Wednesday June 10, 2015 @02:14PM (#9694206)
If it's the holy grail, then erlang has had it for a couple decades... and whether it actually works in nginx I don't know, but it does work in erlang.

The way it's handled is that the process where the previous socket was connected remains in place until it terminates but all new sockets connect to the new code.

Re:How We Designed for Performance and Scale by nodesocket on Wednesday June 10, 2015 @02:18PM (#9694233)
"The way it's handled is that the process where the previous socket was connected remains in place until it terminates but all new sockets connect to the new code."

That would imply that two separate processes are bound to the same port right? I thought that was not possible.

Re:How We Designed for Performance and Scale by ericmoritz on Wednesday June 10, 2015 @02:28PM (#9694312)
No, usually there's an accept process spawning working processes and passing the connection off to it.

Erlang can have two versions of a module present in memory at a time. At the moment of a code upgrade, the processes working with existing connections continue to use the old code while newly accepted connections are passed to the version of the module that was loaded in between connection accepts.

Re:How We Designed for Performance and Scale by ericmoritz on Wednesday June 10, 2015 @02:30PM (#9694320)
Keep in mind that Erlang processes != System processes. Erlang processes are preemptively scheduled microthreads that are scheduled by the Erlang VM.
Re:How We Designed for Performance and Scale by SEJeff on Wednesday June 10, 2015 @02:24PM (#9694283)
Things have changed :)

https://lwn.net/Articles/542629/

Re:How We Designed for Performance and Scale by bkirkbri on Wednesday June 10, 2015 @02:37PM (#9694374)
If only the JVM could take advantage of this.
Re:How We Designed for Performance and Scale by SEJeff on Wednesday June 10, 2015 @05:03PM (#9695370)
Just about bet you could do that via some grotesque JNI work, but I wouldn't wish JNI on really anyone.
Re:How We Designed for Performance and Scale by noselasd on Wednesday June 10, 2015 @05:02PM (#9695357)
While there are certain socket options to share a port - two processes can share a port easily in another elegant way; just call fork().

One way of doing such upgrades is then for the existing code to fork() then exec() the new version. The existing old code closes the original server socket, which leaves accepting new requests to the new code. When all in-flight requests served by the old code is finished, that process can exit.

Re:How We Designed for Performance and Scale by rb12345 on Wednesday June 10, 2015 @02:52PM (#9694459)
Presumably the original server process uses exec() to start the new process, which inherits the open socket handles, and the descriptor is passed as an argument to the new process.

(e.g. http://stackoverflow.com/questions/14351147/perl-passing-an-...)

Re:How We Designed for Performance and Scale by Blackthorn on Wednesday June 10, 2015 @05:49PM (#9695757)
This is known to me as a "lame duck" mode, and I thought it was a standard thing to do!