Wherever you go, there you are.
Showing posts with label linux. Show all posts
Showing posts with label linux. Show all posts

Running Minecraft with a managed console (STDIN) using systemd, tail, and mkfifo+pipes

2

This post shows you how to control a systemd-managed Minecraft server by connecting its console (STDIN) to a named pipe.

If you have tried to setup a Minecraft server on linux using standard systemd service units you have likely run into the same pain I have. A Minecraft server is a process that receives control commands via console input (STDIN). There is no control application you can use to send messages to the server via IPC; even the simple act of stopping a vanilla Minecraft server requires a "stop" console command sent via STDIN rather than the traditional SIGTERM that you would expect to work for a linux service.

a crane lifting a pipe

There are many possible work-arounds to this problem. The most common I've seen, and the one I've used in the past, is to wrap the Minecraft Java process using a console "window manager" such as screen or tmux. But this solution has some downsides. These tools are not commonly installed by default, for one. Another issue is that trying to share a screen or tmux session with other users, or automate sending commands to that console, can be difficult and potentially brittle. Yet another problem is that screen or tmux controls the process' output. This means systemd cannot grab STDOUT and direct it as desired. If you prefer journalctl for your logging, for instance, losing access to the STDOUT dump is a bit of a bummer.

Perhaps most importantly, though, is that using a screen or tmux wrapper simply doesn't feel like the UNIX way. When we hear that we want to control a process via STDIN, the UNIX mind immediately jumps to pipes. Instinctually it feels like any solution not using pipes is somehow missing a better approach.

With these issues in mind, I was able to formulate a set of goals for managing my Minecraft servers that were not being met by my original screen-based solution:

  • server console input should be handled through a named pipe (mkfifo); this makes it easy to apply access controls and script commands, and feels appropriately UNIX-y
  • systemd should know that the "main process" is the Java process so it properly tracks when it dies
  • as few non-default tools as possible should be required (ideally, none)
  • the server output should remain unmodified on STDOUT so it can be sent wherever desired

I will first provide the files that enable the new solution, then go over the details of how and why it meets our criteria.

The necessary files

This method works to control any vanilla, Spigot, or PaperMC Minecraft server. It will also work on a BungeeCord server, though you will need to modify the stop command to send "end" instead.

Console commands can be issued to the server through the named pipe "/run/mcsrv/stdin". For example, running sudo -u minecraft sh -c 'echo tps > /run/mcsrv/stdin' will execute the tps command on the server console.

The systemd service unit file

[Unit]
Description=Minecraft Server
After=syslog.target network.target

[Install]
WantedBy=multi-user.target

[Service]
Type=forking
User=minecraft
Group=minecraft
Restart=no
KillMode=control-group
WorkingDirectory=/srv/minecraft
RuntimeDirectory=mcsrv
StandardOutput=journal
PIDFile=/run/mcsrv/server.pid
KillSignal=SIGCONT
FinalKillSignal=SIGTERM
ExecStartPre=mkfifo --mode=0660 /run/mcsrv/stdin
ExecStart=/bin/bash start.sh /run/mcsrv
ExecStop=/bin/sh -c 'echo stop > /run/mcsrv/stdin'

The shell script to start the server

Based on the service definition above this script should be saved as "/srv/minecraft/start.sh":

#!/bin/bash

MCDIR=$1
MCJAR=minecraft.jar
MCARG="-Xms2G -Xmx2G"

{ coproc JAVA (/usr/bin/java -server ${MCARG} -jar ${MCJAR} nogui) 1>&3; } 3>&1
echo ${JAVA_PID} > ${MCDIR}/server.pid
{ coproc TAIL (/usr/bin/tail -n +1 -f --pid=${JAVA_PID} ${MCDIR}/stdin 1>&${JAVA[1]}) } 2>/dev/null

The magic explained

Our service unit file specifies that a runtime directory should be created for this service. We create a named pipe inside the rundir using mkfifo as defined in the ExecStartPre directive. The --mode parameter for this call can be modified as needed to control the access permissions that are set on this pipe. This pipe is where we will hook the server's STDIN; anything sent to this named pipe will be received by the server console. Running sudo -u minecraft sh -c 'echo tps > /run/mcsrv/stdin', for instance, would execute the tps command via the server console.

We then start the server by executing the start.sh script. This script has what is probably our most specific requirement; it must execute with a shell that understands the coproc command. Luckily this includes bash, zsh, ksh, and a number of other very common default shell processors.

The startup script uses coproc to capture the STDIN handle when we launch the Java server process. Because this also ordinarily eats the process' STDOUT, we apply a pair of redirects to make sure that the STDOUT for the server remains attached to this script's STDOUT (which is, ultimately, what systemd will use as its interpretation of STDOUT for the overall process group). If you don't want the server output going into the systemd journal then change the StandardOutput directive to your preferred logging method.

Next we dump the server's PID into a pidfile that is also stored in the runtime directory. In our service unit we specify this file using the PIDFile directive. This allows systemd to properly associate the Java process as the main process for this service unit. If the Java process flat out dies for any reason, systemd will know. Change the Restart directive to your preferred failure behavior.

a graph of nodes

Now we need to hook our named pipe to the server's console. If you have investigated this before you may have realized how difficult it is to do this in the context of a systemd service. This is mainly owing to the semantics of a pipe being automatically closed after a single succesful writer closes its handle. A bit of research into the problem of keeping the pipe open puts us on the tail of, well, tail.

The script starts and backgrounds a tail instance that handles our pipe magic. While our server lives this tail instance constantly reads one line at a time from our named pipe and redirects this output into STDIN for the server. It is also set to "wait" on the PID of the Java process using the --pid parameter. This ensures it will clean itself up when the server stops.

To stop the service we tell systemd to send a "stop" command to the named pipe to shutdown the server cleanly. It will also send SIGCONT to the process group to make sure both tail and Java processes are not suspended. If the server fails to cleanly stop in the allowed time period systemd will then send SIGTERM to the processes. If you are running a vanilla server then this will basically force-stop the server. If you run spigot or papermc these servers will attempt to shutdown gracefully on receiving SIGTERM.

Note that the above signal methods can be modified to your preference. If you prefer to guarantee absolutely no chance of a forcibly stopped server causing data loss then you may want to change the KillMode directive to "none". This ensures that the server processes are left alone if the "stop" command doesn't do its job. The downside is the potential to leave orphaned processes that require manual intervention and cleanup. Also, if your server takes longer than the default service timeout to cleanly stop (90s) you will want to add the TimeoutStopSec directive to tell systemd to wait longer before sending the final kill signals.

Linux router conntrack settings

0

Linux firewall? Conntrack? NAT? Connection issues?

This is one of those "I'll never remember this again if I don't write it down" types of posts. So entirely for my own purposes, basically :)

I have a linux box acting as a firewall/NAT device for my local network. Among other things, I'm using conntrack modules for NAT connection tracking to handle proper NAT port forwards and also for firewall rules to filter active connections properly.

A few issues cropped up in weird places like Netflix, YouTube, VEVO, and other streaming services where streams would die for no apparent reason. I always just chalked this up to ephemeral internet issues and did not investigate it deeply as it was not common enough to be more than a minor, infrequent, and random inconvenience. However, a consistent, reproducible, and most irritatingly CONSTANT problem trying to watch twitch.tv streams finally got me looking into this in detail.

Now, I don't know the details of WebRTC, HLS, RTP, and all the other protocols under the hood for video streaming tunnels through HTTP. What I do know (now) is that the default timeouts in conntrack in 3.x kernels seem to be too aggressive (at least for my internet connection), causing conntrack to often drop the TCP connection tracking for computers using these streaming protocols.

The result? Random drop-outs and network connectivity problems in HTTP-based video streaming.

The fix ends up being stupid simple. I just doubled (or sometimes tripled/quadrupled) the TCP connection timeouts for conntrack and, at least so far, streaming stability has improved dramatically (and twitch.tv actually works). The new timeouts are still short enough that for my limited network size I'm in no danger of running out of conntrack entries in any reasonable timeframe.

So, to the end of my /etc/sysctl.conf file, I simply added these timeouts (Ubuntu 14 LTS system, btw):

net.netfilter.nf_conntrack_icmp_timeout = 60
net.netfilter.nf_conntrack_tcp_timeout_close = 60
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 240
net.netfilter.nf_conntrack_tcp_timeout_last_ack = 120
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 120
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 240
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 240
net.netfilter.nf_conntrack_tcp_timeout_unacknowledged = 300
net.netfilter.nf_conntrack_udp_timeout = 60
net.netfilter.nf_conntrack_udp_timeout_stream = 240

Oh, and while I'm on the topic... I also updated my conntrack modules to handle a few of those irritating protocols. These work much more cleanly now with the proper conntrack handling in place (especially PPTP). To my /etc/modules I have added:

nf_conntrack_proto_gre
nf_conntrack_pptp
nf_conntrack_sip
nf_conntrack_h323
nf_conntrack_ftp
nf_nat_proto_gre
nf_nat_pptp
nf_nat_sip
nf_nat_h323
nf_nat_ftp

How Not to Upgrade

0

I love doing stupid things. No, really, I mean that with all my heart. Every time I do something stupid and manage to recover from it without too much pain, I feel as if some part of my mandatory universal stupidity quota has just been filled. My normal sense of complacency is repealed, I pay better attention to all the buttons I normally push without thinking, and I become hyper-vigilant to avoid doing anything glaringly dumb. For a while.

The latest stupid thing I did happened while upgrading to Ubuntu Feisty. Actually, I upgraded several hosts without a hitch, so really, power to the people at Ubuntu for making such clean upgrade paths (and, I suppose, to the Debian folks for the underlying package manager on which it all depends). Unfortunately, when I pulled the big handle on my primary server and X11 platform, I wasn't paying enough attention. The upgrade rolled around just fine, updating old packages and removing obsolete ones, while I blindly agreed to every prompt that popped up.

The time to restart comes along and... oh, what's this? What do you mean you can't find a kernel?

It's not fair to say that it couldn't find the kernel, exactly. The kernel loaded just fine. Unfortunately the kernel is a Xen Hypervisor, which points to another kernel to use for the Dom0 machine instance which runs my actual server. And that kernel was nowhere to be found. After a bit of digging it became painfully obvious that one of the obsolete packages I had blindly thrown out was, in fact, my precompiled Xen generic x86 kernel.

Oops.

Thankfully I have lots of other internet connected machines at my disposal, and it was a quick matter to grab an Ubuntu Live-CD and boot into a root shell on my server from there. Another few minutes of fiddling and I had apt-get'ted myself the latest Xen generic x86 kernel package. One last smidgen of editing in the GRUB list, and all was well once again.

So other than my heart skipping a beat or ten, this was happily a very painless act of stupidity. At my current quota-filling rate, I expect I'll be posting something eerily similar to this in, oh, another 5 months or so. I can only hope it'll be as easy to fix as this one was.