The FreeBSD Diary

The FreeBSD Diary (TM)

Providing practical examples since 1998

If you buy from Amazon USA, please support us by using this link.
[ HOME | TOPICS | INDEX | WEB RESOURCES | BOOKS | CONTRIBUTE | SEARCH | FEEDBACK | FAQ | FORUMS ]
Creating multiple jails 10 September 2007
Need more help on this topic? Click here
This article has 3 comments
Show me similar articles

Jails are a useful tool for achieving a certain level of virtualization. I have used jails in the past for creating sandboxes for testing of the Bacula project. Today, I will be setting up 7 jails, each for regression tests on different configurations. In the process, I'll show you some shortcuts that Ryan Lortie (desrt) told me about. It saves time. I'll also share some scripts that will help with the setup of various jail items.

This article should be read in conjunction with my other jail articles. It won't stand alone unless you are already familiar with jails and how to configure them.

Building and configuring the jails

According to man jail, these are the basic steps for building a jail:

D=/here/is/the/jail
cd /usr/src
mkdir -p $D
make world DESTDIR=$D
make distribution DESTDIR=$D
mount -t devfs devfs $D/dev

desrt pointed out, and I think I've heard this before, after the first build, you don't have to do a full make world. For the subsequent jails, you can get away with this:

D=/here/is/the/OTHER/jail
cd /usr/src
mkdir -p $D
make installworld DESTDIR=$D
make distribution DESTDIR=$D
mount -t devfs devfs $D/dev

The key differences are:

  1. Change the value of D to point to the new jail.
  2. Do a make installworld, not a make world.

"make world" consists of two main steps: make buildworld && make installworld. There is no advantage in doing the buildworld part again. That has already be done.

During the building of my first jail, I tried this:

export D=mysql41.example.org

This fail, because the install could not find the directory mysql41.example.org. I should have included the full path to the directory: /usr/jail/mysql41.example.org. To complete the installation of the jail, I issued this series of commands:

export D=/usr/jail/mysql41.example.org
make installworld DESTDIR=$D
make distribution DESTDIR=$D
mount -t devfs devfs $D/dev

After the above completed, I had the following mount points:

# mount
/dev/ad0s1a on / (ufs, local)
/devfs on /dev (devfs, local)
/dev/ad0s1e on /tmp (ufs, local, soft-updates)
/dev/ad0s1f on /usr (ufs, local, soft-updates)
/dev/ad0s1d on /var (ufs, local, soft-updates)
/devfs on /usr/jail/mysql41.example.org/dev (devfs, local)

There are a few things that I like to configure within the jail before starting it. The files in the jail environment are accessible from the host environment. So it is a simple matter of using your favourite editor to make the changes. Here are the files I altered in /usr/jail/mysql41.example.org:

  • etc/rc.conf - added: sshd_enable="YES"
  • etc/resolv.conf - added entries to get DNS working
  • etc/syslog.conf - changed the "*.err;kern.warning;auth.notice;mail.crit" line to echo to /var/log/messages instead of the console

It would be easy to script that. For example, here is the script I used to do the installworld for each jail:

#!/bin/sh
JAILDIR="/usr/jail"
JAILS=`ls ${JAILDIR}`
for jail in ${JAILS}
do
   echo $jail
   D=${JAILDIR}/${jail}
   make installworld DESTDIR=$D
   make distribution DESTDIR=$D
   mount -t devfs devfs $D/dev
done
This script assumes that each directory in /usr/jail needs to be setup. If this was not the case, you could hardcode the value of JAILS like this:
JAILS="mysql51.example.org pg73.example.org pg74.example.org pg80.example.org"

Similar scripts can be created for populating the ports tree. You may think that a single shared ports tree might be sufficient for multiple jails. I've decided to go with a distinct tree for each jail. I'll be building different ports concurrently in each jail. I don't want any interference caused by another jail.

Setting up the host environment

Setting up multiple jails involves adding several entries to /etc/rc.conf, some of which specify defaults for all jails. The default settings in my server are:

# Defaults for all jails:
jail_interface="fxp0"    # Interface to create the IP alias on
jail_devfs_enable="YES"  # mount devfs in the jail
jail_procfs_enable="YES" # mount procfs in jail

I also specify a list of the jails on this machine:

# list of jails on this machine
jail_list="mysql41 mysql50 mysql51 pg73 pg74 pg80 pg81 pg82"

For each of the above mentioned jails, entries similar to this appear:

# values for each jail listed above
jail_mysql41_rootdir="/usr/home/jails/mysql41.example.org"  # Jail's root directory
jail_mysql41_hostname="mysql41.example.org"  # Jail's hostname
jail_mysql41_ip="192.168.0.100"              # Jail's IP number

It can be tedious to setup thse values. So a script can be useful and it could be based upon the script that appears above.

PostgreSQL in a jail

Running PostgreSQL in a jail is interesting. There are shared memory issues which can be resolved by setting security.jail.sysvipc_allowed via sysctl. I tried that. I failed. The problem I encountered during initdb is:

# su -l pgsql -c initdb
The files belonging to this database system will be owned by user "pgsql".
This user must also own the server process.

The database cluster will be initialized with locale C.

creating directory /usr/local/pgsql/data ... ok
creating directory /usr/local/pgsql/data/global ... ok
creating directory /usr/local/pgsql/data/pg_xlog ... ok
creating directory /usr/local/pgsql/data/pg_xlog/archive_status ... ok
creating directory /usr/local/pgsql/data/pg_clog ... ok
creating directory /usr/local/pgsql/data/pg_subtrans ... ok
creating directory /usr/local/pgsql/data/pg_twophase ... ok
creating directory /usr/local/pgsql/data/pg_multixact/members ... ok
creating directory /usr/local/pgsql/data/pg_multixact/offsets ... ok
creating directory /usr/local/pgsql/data/base ... ok
creating directory /usr/local/pgsql/data/base/1 ... ok
creating directory /usr/local/pgsql/data/pg_tblspc ... ok
selecting default max_connections ... 10
selecting default shared_buffers ... 50
creating configuration files ... ok
creating template1 database in /usr/local/pgsql/data/base/1 ... FATAL:  could 
not create shared memory segment: Function not implemented
DETAIL:  Failed system call was shmget(key=1, size=1327104, 03600).
child process exited with exit code 1
initdb: removing data directory "/usr/local/pgsql/data"

I initially thought I had to set this flag on each of the jails. I was wrong. After a bit of trial and error, I discovered the problem. I had added this value to /etc/sysctl.conf:

# For PostgreSQL jails
security.jail.sysvipc_allowed=1

After a reboot, the above entry ensures this setting after a reboot:

# sysctl security.jail.sysvipc_allowed
security.jail.sysvipc_allowed: 1

When starting a jail, I noticed this:

# /etc/rc.d/jail start pg82
Configuring jails: sysvipc_allow=NO.
Starting jails: pg82.unixathome.org.
Why was it saying sysvipc_allow=NO? Additionally, in the jail and in the host system, I was now seeing this:
# sysctl security.jail.sysvipc_allowed
security.jail.sysvipc_allowed: 0

Not only was the jail not getting the right value, the value in the host system was being reset.

Looking at /etc/rc.d/jail for "Configuring jails", I found more clues leading me to /etc/defaults/rc.conf, where I found this setting:

jail_sysvipc_allow="NO" # Allow SystemV IPC use from within a jail

Ahh, OK, so now I needed to set this in /etc/rc.conf

jail_sysvipc_allow="YES" # For PostgreSQL

After restarting the pg82 jail, I found the security.jail.sysvipc_allowed was correctly set, in both the jail and the host. I tried the initdb again:

# su -l pgsql -c initdb
The files belonging to this database system will be owned by user "pgsql".
This user must also own the server process.

The database cluster will be initialized with locale C.

creating directory /usr/local/pgsql/data ... ok
creating directory /usr/local/pgsql/data/global ... ok
creating directory /usr/local/pgsql/data/pg_xlog ... ok
creating directory /usr/local/pgsql/data/pg_xlog/archive_status ... ok
creating directory /usr/local/pgsql/data/pg_clog ... ok
creating directory /usr/local/pgsql/data/pg_subtrans ... ok
creating directory /usr/local/pgsql/data/base ... ok
creating directory /usr/local/pgsql/data/base/1 ... ok
creating directory /usr/local/pgsql/data/pg_tblspc ... ok
selecting default max_connections ... 10
selecting default shared_buffers ... 50
creating configuration files ... ok
creating template1 database in /usr/local/pgsql/data/base/1 ... FATAL:  could 
not create semaphores: No space left on device
DETAIL:  Failed system call was semget(1, 17, 03600).
HINT:  This error does *not* mean that you have run out of disk space.
       It occurs when either the system limit for the maximum number of 
semaphore sets (SEMMNI), or the system wide maximum number of semaphores
(SEMMNS), would be exceeded.  You need to raise the respective kernel parameter.
Alternatively, reduce PostgreSQL's consumption of semaphores by reducing its 
max_connections parameter (currently 10).
The PostgreSQL documentation contains more information about configuring your system for PostgreSQL.
child process exited with exit code 1
initdb: removing data directory "/usr/local/pgsql/data"
I am familiar with this message. It is related to kernel settings. I added these values to /boot/loader.conf on the host system and rebooted:
kern.ipc.semmni=256
kern.ipc.semmns=512
kern.ipc.semmnu=256
The reboot is necessary because those settings are read-only and cannot be modified after booting the kernel. With those changes, the initdb ran to completion:

# su -l pgsql -c initdb
The files belonging to this database system will be owned by user "pgsql".
This user must also own the server process.

The database cluster will be initialized with locale C.

creating directory /usr/local/pgsql/data ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers/max_fsm_pages ... 24MB/153600
creating configuration files ... ok
creating template1 database in /usr/local/pgsql/data/base/1 ... ok
initializing pg_authid ... ok
initializing dependencies ... ok
creating system views ... ok
loading system objects' descriptions ... ok
creating conversions ... ok
setting privileges on built-in objects ... ok
creating information schema ... ok
vacuuming database template1 ... ok
copying template1 to template0 ... ok
copying template1 to postgres ... ok

WARNING: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the -A option the
next time you run initdb.

Success. You can now start the database server using:

    postgres -D /usr/local/pgsql/data
or
    pg_ctl -D /usr/local/pgsql/data -l logfile start

I was then able to start PostgrSQL, create users, etc. I repeated the process for two other jails, then ran into this problem for the third jail:

# su -l pgsql -c initdb
The files belonging to this database system will be owned by user "pgsql".
This user must also own the server process.

The database cluster will be initialized with locale C.

creating directory /usr/local/pgsql/data ... ok
creating directory /usr/local/pgsql/data/global ... ok
creating directory /usr/local/pgsql/data/pg_xlog ... ok
creating directory /usr/local/pgsql/data/pg_xlog/archive_status ... ok
creating directory /usr/local/pgsql/data/pg_clog ... ok
creating directory /usr/local/pgsql/data/pg_subtrans ... ok
creating directory /usr/local/pgsql/data/base ... ok
creating directory /usr/local/pgsql/data/base/1 ... ok
creating directory /usr/local/pgsql/data/pg_tblspc ... ok
selecting default max_connections ... 10
selecting default shared_buffers ... 50
creating configuration files ... ok
creating template1 database in /usr/local/pgsql/data/base/1 ... FATAL:  could
not create shared memory segment: Cannot allocate memory
DETAIL:  Failed system call was shmget(key=1, size=1122304, 03600).
HINT:  This error usually means that PostgreSQL's request for a shared memory
segment exceeded available memory or swap space. To reduce the request size 
(currently 1122304 bytes), reduce PostgreSQL's shared_buffers parameter 
(currently 50) and/or its max_connections parameter (currently 10).
The PostgreSQL documentation contains more information about shared memory
configuration.
child process exited with exit code 1
initdb: removing data directory "/usr/local/pgsql/data"

I decided to double the settings in /etc/sysctl.conf on the host system:

kern.ipc.semmni=512
kern.ipc.semmns=1024
kern.ipc.semmnu=512

But that did not solve the problem. Stopping PostgreSQL on the other two jails did. This allowed the initdb to complete. But I was still unable to get all the PostgreSQL servers running in each jail.

After a few more trial and error attempts, I discovered that the settings I had in /boot/loader.conf contained syntax errors. Instead of SETTING=VALUE, I had SETTING: VALUE. Fixing that, and rebooting, allowed me to get all five instances of PostgreSQL running concurrently. But there was a bit more to it than just that.

I tried various things. I tried running each postmaster on a different port (didn't help). I also tried running each postmaster as a different GID (that did help). That is the key. I will explain.

Shared memory and UID

PostgreSQL makes use of shared memory. When running multiple instances of PostgreSQL the shared memory for one instance can be stomped on by another instance. That's not nice. The key to avoiding this is using a different UID for each instance. You can see that here:

$ grep -h pgsql /usr/jail/*.unixathome.org/etc/passwd
pgsql:*:1073:70:PostgreSQL Daemon:/usr/local/pgsql:/bin/sh
pgsql:*:1074:70:PostgreSQL Daemon:/usr/local/pgsql:/bin/sh
pgsql:*:1080:70:PostgreSQL Daemon:/usr/local/pgsql:/bin/sh
pgsql:*:1081:70:PostgreSQL Daemon:/usr/local/pgsql:/bin/sh
pgsql:*:1082:70:PostgreSQL Daemon:/usr/local/pgsql:/bin/sh

I used a UID that would relate to the version of PostgreSQL that was running. For example, UID=1073 is PostgreSQL version 7.3. There is no need to follow this convention. I did it merely because I could.

When I changed the UID in each jail, I first made sure PostgreSQL was not running in that jail. Then I ran vipw and altered the UID and exited. Then I did a chown to ensure the file attributes were owned by the new UID:

chown -R pgsql /usr/local/pgsql
Then I restarted PostgreSQL. No more memory stomping. The symptoms of this memory stomping (a term I made up and probably does not reflect reality) are:
$ psql -l
psql: FATAL: semctl(262144, 4, SETVAL, 0) failed: Invalid argument

The above type of problem would arise only when I ran multiple PostgreSQL servers with the same UID.

The sysctl settings

These are the various sysctl settings I'm using on this server:

[dan@polo:~] $ cat /boot/loader.conf
kern.ipc.semaem=32767
kern.ipc.semvmx=65534
kern.ipc.semusz=184
kern.ipc.semume=80
kern.ipc.semopm=200
kern.ipc.semmsl=120
kern.ipc.semmnu=4096
kern.ipc.semmns=8192
kern.ipc.semmni=32767
kern.ipc.semmap=60

[dan@polo:~] $ cat /etc/sysctl.conf
# For PostgreSQL jails
security.jail.sysvipc_allowed=1

# for more shared memory for jails/PostgreSQL
kern.ipc.shmall=65536
kern.ipc.shmmax=134217728
kern.ipc.semmap=4096
[dan@polo:~] $

I am confident that I am over allocating resources here. I kept increasing the values and trying them, without success. Then I discovered my syntax error. I didn't want to reverse-engineer my changes so I have left them as they appear above. You may wish to use lesser values.

You may be interested in the host system dmesg output. As you can see, this isn't a very high spec system. It has only 512M of RAM.

/var/run/dmesg.boot

top - do not be afraid

When running top, I noticed something I had not see before. Number in the username field.

last pid: 82356;  load averages:  2.47,  1.30,  1.01   up 1+21:46:07  10:25:39
285 processes: 6 running, 277 sleeping, 1 stopped, 1 lock
CPU states: 29.2% user,  5.8% nice, 33.1% system,  1.3% interrupt, 30.7% idle
Mem: 173M Active, 100M Inact, 118M Wired, 14M Cache, 60M Buf, 81M Free
Swap: 989M Total, 282M Used, 708M Free, 28% Inuse

  PID USERNAME  THR PRI NICE   SIZE    RES STATE  C   TIME   WCPU COMMAND
  81261   1080      1 105    0 10432K  4728K *Giant 1   0:28 16.85% postgres
  82147   1074      1   4    0 10392K  3136K sbwait 0   0:07 16.58% postgres
  81830   1073      1   4    0 11016K  4548K sbwait 0   0:16 15.87% postgres
  81322   1081      1 103    0 11664K  4244K CPU0   1   0:23 13.57% postgres
  81826 dan         6 107   10 11832K  3744K RUN    0   0:04  3.13% bacula-dir
  82143 dan         6 108   10 11856K  3852K RUN    0   0:02  3.03% bacula-dir
  81252 dan        12 106   10 15700K  4296K ucond  0   0:07  1.51% bacula-dir
  81257 dan        12 106   10 15700K  4304K ucond  1   0:07  1.12% bacula-dir

With a bit of thought, you can figure this out. top was running in the host environment. The UID for pgsql has been changed in the jail. The host environment has no knowledge of UIDs from the jails. When top cannot convert a UID to a username, it displays the UID. This is fine. An added benefit is seeing exactly which jail is still running. In this case, it's PostgreSQL 8.0, 7.4, 7.3, and 8.1.

Jails - just right for this

Jails are just right what for what I'm trying here. I certainly do not want 8 servers sitting around the house, just for regression testing of Bacula. I'll be writing up some scripts to automate much of this testing and to upload the results to a website.

Jails: sometimes the right tool for the job.


Need more help on this topic? Click here
This article has 3 comments
Show me similar articles