rem

.co

Gentoo: Running a Minecraft 1.8 server

 |  2300 words — 11 minutes  |  gentoo linux minecraft

Running a dedicated Minecraft server can be a challenging job. You have to find a balance between performance and usability using “server software” that doesn’t seem to be designed to provide for long running, resilient services.

Being a first-time Minecraft server operator I had to tackle various challenges in order to come up with a way to provide a stable and reliable service to my players. The following article is a recollection of the things I implemented and scripts I wrote in order to run a Minecraft 1.8 server. The scripts mentioned are specific to Gentoo Linux, but could also be used on most other Linux flavours, albeit with some modifications to match that platform’s init.d scripts.

Preparations

Screen

In the scripts we’re going to use we use screen as the means to deamonize the Minecraft process. I’ve given this a lot of thought, because using screen as a deamonizer feels very wrong to me when there are dedicated tools (such as the start-stop-daemon) for that particular job. Furthermore, I’m more of a tmux guy myself when it comes to terminal multiplexing.

However, after searching the internet, screen seems to be the most popular way of daemonizing Minecraft by far. I don’t dare to speculate on wether this is caused by a lack of knowledge in the Minecraft server scene or based on a sound choice.

I have my own reasons for using screen instead of start-stop-deamon or tmux:

  • screen (and/or tmux) allows me to interact with the game’s console. If I use start-stop-daemon instead, the process will be truly deamonized, and there is no way to bring it to the foreground anymore to allow for interaction. Why do we need console interaction you might ask? Because we do backups for example. In order to have reliable backups, we have to tell the Minecraft server to flush any changes to the disk and stop saving while the backup is running. This is all done using console commands.
  • screen has an awesome command, stuff, that allows us to send commands to a detached (deamonized) instance. This provides us with the possibility of interacting with a running instance using scripts/services outside of that screen instance.

So, before we continue, make sure screen is installed on your box:

1
# emerge -av app-misc/screen

Java SE 8

My first attempts on running Minecraft 1.8 used the JDK that was present on my box at that time, being IcedTea JDK 6.1.13.3 [icedtea-bin-6]. You can actually use that JDK (or any other Java SE 6 JDK/JRE for that matter), but my installation was haunted by substantial, noticeable lag while the server process was using 100% CPU non-stop (on one core, the main Minecraft server process being single-threaded) and the console and logs were spammed with these warnings:

1
[23:52:10] [Server thread/WARN]: Can't keep up! Did the system time change, or is the server overloaded? Running 19381ms behind, skipping 387 tick(s)

Despite all optimisations that you will read about in this article that were already in effect at that time, the lag remained, and would at times get so bad that Minecraft’s internal watchdog would mark the server as crashed:

1
2
3
4
[21:21:15] [Server Watchdog/FATAL]: A single server tick took 60.00 seconds (should be max 0.05)
[21:21:15] [Server Watchdog/FATAL]: Considering it to be crashed, server will forcibly shutdown.
[21:21:15] [Server Watchdog/ERROR]: This crash report has been saved to: /mnt/ramdisk/./crash-reports/crash-2014-11-27_21.21.15-server.txt
[21:21:15] [Server Shutdown Thread/INFO]: Stopping server

After some research on the internet some posts mentioned that upgrading to Java 8 could make all the difference, so I did. And it worked! While CPU usage is still reasonably high (around 70~80% at most times), the lag and warnings are gone, and subsequently, so are the crashes. So, I’ll advise to use the same JDK as I did in this article. To install:

1
# emerge -av dev-java/oracle-jdk-bin

Read the error you receive from running that command, resolve the fetch restriction by following the instructions in that error message and continue installing. Afterwards:

1
2
3
4
5
6
7
# java-config --list-available-vms
The following VMs are available for generation-2:
1)      IcedTea JDK 6.1.13.3 [icedtea-bin-6]
*)      Oracle JDK 1.8.0.25 [oracle-jdk-bin-1.8] 

# java-config --set-system-vm 2
Now using oracle-jdk-bin-1.8 as your generation-2 system JVM

The command above marks the newly installed JDK as your system JVM. This might not be a possibility for you if you have other software on your system that is incompatible with Java 8 (or the Oracle JDK). In that case, you can set the installed JDK as the User VM for the user you are using to run Minecraft. To do so:

1
2
3
# su <minecraftuser>
$ java-config --set-user-vm 2
Now using oracle-jdk-bin-1.8 as your user JVM

RAMDisk

Being haunted by performance issues, I’ve read quite a lot on Minecraft server performance and optimisation. There is one article that stands out from the crowd and is a must read for everyone running Minecraft servers. I encourage you to go and read sk89q’s Improving your Minecraft server’s performance first before continuing this article.

Among other minor optimisations, such as the Java flags that are used in my setup, one of the biggest influences is the use of a RAMDisk to host your Minecraft setup on. This eliminates any delays caused by iowait due to slow disks and speeds up various tasks, such as loading the map. I use a Dell PowerEdge 1950 III Blade server as the hardware platform for my Minecraft server, and although beefy as it may be for this purpose, it is not equipped with SSD’s which is a potential cause for performance degradation. The installation/use of a RAMDisk is quite easy, but there are some drawbacks you have to keep in mind, such as that RAMDisks are not permanent storage. In case you suffer a power outage or kernel panic, the data on your RAMDisk is gone. To counter this issue, you’ll have to implement some way to synchronise the RAMDisk from/to persistant storage.

The init.d script we’ll later discuss takes care of all this hassle, but in order for it to work, we first have to create the RAMDisk.

Edit your /etc/fstab and add a RAMDisk at a mountpoint of your choise:

1
2
# vim /etc/fstab
tmpfs                   /mnt/ramdisk    tmpfs           rw,nodev,nosuid,size=2G,uid=1000,gid=1000,mode=1700     0 0

Adjust the size= parameter to match the amount of spare (unused) RAM you have available and the size of your actual Minecraft installation. (My installation is only 300MB actually, but I have 36GB RAM in this machine which is mostly vacant so I picked a comfortably large disk size). Adjust the uid= and gid= parameters to the id’s of the dedicated user you are using to run your Minecraft server with. (Never run trivial services such as this as root!).

Next, create the mountpoint:

1
# mkdir /mnt/ramdisk

And try to mount the RAMDisk and see if it works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# mount /mnt/ramdisk
# df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda3        63G   48G   13G  80% /
udev             10M  4.0K   10M   1% /dev
tmpfs           7.9G  628K  7.9G   1% /run
shm             7.9G     0  7.9G   0% /dev/shm
cgroup_root      10M     0   10M   0% /sys/fs/cgroup
tmpfs           7.9G   72K  7.9G   1% /tmp
/dev/sdb1       200G  192G  8.2G  96% /mnt/backups
tmpfs           2.0G  297M  1.8G  15% /mnt/ramdisk 

Install Minecraft as you normally would

If you haven’t done so already, install a Minecraft like you normally would. For me, that is a directory /home/remco/minecraft which contains the server .jar, the world and all settings.

The Scripts

The init.d script is what makes this setup tick. It does all the heavy lifting. The location for this script is /etc/init.d/minecraft

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/sbin/runscript
# Copyright 2014 Rem.co Linux Solutions
# Distributed under the terms of the GNU General Public License v2
# $Header: $

extra_commands="syncup syncdown backup rotate emptyrestart watch"

description="Manages a Minecraft installation running under a dedicated user inside a daemonized screen session"
description_syncdown="Synchronizes the RAMdisk back to the persistant storage"
description_syncup="Synchronizes the persistant storage to the RAMdisk"
description_backup="Takes a full backup of the minecraft installation and rotates old backups"
description_rotate="Rotates old backups"
description_emptyrestart="Checks if any users are online in minecraft. If not, restarting the service"
description_watch="Checks if minecraft is still running if it resides in the 'started' state. Restarts the service if not."

depend() {
	need net localmount
	after bootmisc
	use logger
}

start() {
	ebegin "Starting Minecraft"
	if ! sudo -u ${RUNAS} screen -list | grep -q "${SCREENNAME}"; then
		launch || return 1
	else
		eerror "Minecraft screen session is already present, not launching new one."
	fi
	eend $?
}

stop() {
	ebegin "Stopping Minecraft"
	if ! sudo -u ${RUNAS} screen -list | grep -q "${SCREENNAME}"; then
		eerror "The Minecraft screen session cannot be found so we can't stop it"
	else
		sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X stuff "stop$(printf \\r)"
		sleep 5
		#Check if the screen session closed cleanly, otherwise kill it with fire.
		if sudo -u ${RUNAS} screen -list | grep -q "${SCREENNAME}"; then
			sudo -u ${RUNAS} screen -S ${SCREENNAME} -X kill
		fi
	fi
	eend $?
}

reload() {
	ebegin "Reload Minecraft"
	stop
	sleep 3
	start
	eend $?
}

launch() {
 	if ! mountpoint -q ${RAMDISK}; then
		mount ${RAMDISK}
	fi
	sup || return 1
	sudo -u ${RUNAS} screen -dmS ${SCREENNAME} bash -c "cd ${RAMDISK}; java -server -XX:+UseConcMarkSweepGC -Xms2G -Xmx4G -jar ${JARFILE} nogui"
}

sdown() {
	rsync --quiet --archive --delete --recursive --force ${RAMDISK}/ ${PERSISTDIR}
}

syncdown() {
	ebegin "Syncing RAMdisk to Persistant Storage"
	sdown || return 1
	eend $?
}

sup() {
	rsync --quiet --archive ${PERSISTDIR}/ ${RAMDISK}
}

syncup() {
	ebegin "Syncing Persistant Storage to RAMdisk"
	sup || return 1
	eend $?
}

backup() {
	ebegin "Taking a backup"
	if ! sudo -u ${RUNAS} screen -list | grep -q "${SCREENNAME}"; then
		eerror "Minecraft is not running, not taking a live backup"
	fi
	H=$(date +"%H")
	NOW=$(date +"%H:%M")
	sdown || return 1
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X stuff "say Hourly backup for $NOW is starting. The World is no longer saving...$(printf \\r)"
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X stuff "save-off$(printf \\r)"
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X stuff "save-all$(printf \\r)"
	sleep 5
	sudo -u ${RUNAS} /usr/bin/time -f "%e sec at %P CPU" -o /tmp/mctime sh -c "sync; sleep 5; sudo -u ${RUNAS} nice tar czf ${BACKUPDIR}/${PREFIX}$(date +"%d-%m-%Y-%H").tar.gz -C ${RAMDISK} ."
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X stuff "save-on$(printf \\r)"
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X stuff "say Hourly backup for $NOW is complete and ran for $(cat /tmp/mctime). The World is saving once more.$(printf \\r)"
	rm /tmp/mctime
	if (( 19 <= 10#$H && 10#$H < 23 )); then
		rot || return 1
	fi
	eend $?
}

rot() {
	[ -d ${BACKUPDIR}/daily-saves ] || mkdir -p ${BACKUPDIR}/daily-saves
	find ${BACKUPDIR} -maxdepth 1 -type f -name "${PREFIX}$(date -d "yesterday 20:00 " '+%d-%m-%Y-%H').tar.gz" \
		-exec cp {} ${BACKUPDIR}/daily-saves/daily_$(date -d "yesterday 20:00 " '+%d-%m-%Y').tar.gz \;
	if [ -f ${BACKUPDIR}/daily-saves/daily_$(date -d "yesterday 20:00 " '+%d-%m-%Y').tar.gz ]; then
		find ${BACKUPDIR} -maxdepth 1 -type f -name "${PREFIX}$(date -d "yesterday 20:00 " '+%d-%m-%Y')-*.tar.gz" -exec rm {} \;
	else 
		ewarn "Could not locate yesterday's daily save. Not rotating yesterday's hourly saves."
	fi
}

rotate() {
	ebegin "Starting daily backup rotation"
	rot || return 1
	eend $?
}

emptyrestart() {
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X hardcopy /tmp/${SCREENNAME}.dump.1
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X stuff "list$(printf \\r)"
	# We need to wait for the command to complete, or the diff will we unreliable
	sleep 1
	sudo -u ${RUNAS} screen -S ${SCREENNAME} -p 0 -X hardcopy /tmp/${SCREENNAME}.dump.2
	NUMP=$(diff -u /tmp/${SCREENNAME}.dump.{1,2} | grep -E "^\+" | grep -Po '\d+/\d+' | cut -d'/' -f1)
	rm /tmp/${SCREENNAME}.dump.{1,2}
	if [ ${NUMP} -eq 0 ]; then
		reload || return 1
	else
		ewarn "There are ${NUMP} player(s) online. Not restarting."
	fi
}
watch() {
	if [ $(rc-service minecraft status | grep -i "started" | wc -l) -eq 1 ]; then
		einfo "MC should be running"
		if ! sudo -u ${RUNAS} screen -list | grep -q "${SCREENNAME}"; then
                	ewarn "But it is not! Restarting service"
			sdown || return 1
			launch || return 1
		else
			einfo "And it is. Good."
		fi

	else
		einfo "MC is in stopped state. Not doing anything."
	fi
}

The init.d script loads settings from a conf.d script. The location for this script is /etc/conf.d/minecraft

1
2
3
4
5
6
7
8
9
BACKUPDIR=/mnt/backups/minecraft
RAMDISK=/mnt/ramdisk
PERSISTDIR=/home/remco/minecraft
PREFIX="mc-backup_"
RUNAS="remco"
SCREENNAME="minecraft"
#JARFILE="minecraft_server.1.8.1.jar"
#JARFILE="spigot1649.jar"
JARFILE="spigot-1.8.jar"

Finishing up

  • Install cronjobs as root:
1
# crontab -e
  • Add these lines:
1
2
0 * * * *       /etc/init.d/minecraft backup 1> /dev/null
* * * * *       /sbin/rc-service minecraft watch 1> /dev/null
  • Set service to start automatically:
1
rc-update add minecraft default