文章目录
使用runc运行Tomcat容器并查看运行状态
尽管docker几乎成为了容器的代名词,但要创建一个容器环境并不一定必须要docker。runc作为一个命令行容器工具,与docker使用了相同的 “引擎” - libcontainer
。与docker相比,runc更加底层,主要提供符合OCI规范的容器创建、运行、状态查询等功能。本文将使用runc一步步创建一个运行Tomcat的Alpine Linux容器。
注: 本文所有脚本与代码均在64位Ubuntu18.04-server下运行通过
准备工作
runc
Ubuntu上使用sudo apt install runc
安装runc,或者从Github的官方release处下载可执行文件。当然从Go源码构建也是可以的。笔者采用的是源码构建方式。
Alpine Linux文件系统
想要创建一个容器,一个Linux操作系统是必不可少的底层文件系统是必不可少的。所谓底层文件系统实际上就是组成一个系统必须使用的一些文件及文件夹。比如/sys, /proc, /dev
等。这里使用AlpineMiniFilesystem。Alpine Linux与Ubuntu、Fedora等一样是一个Linux发行版,不过它对系统进行了精简,因此非常适合存储空间受限的设备。
下载了文件系统后,将其解压到一个名为rootfs的文件夹中即可,比如/home/yourname/alpine-bundle/rootfs/
。解压之后会在rootfs中看到/bin, /dev, /etc
等文件夹。
Tomcat
Tomcat从官方网站下载即可。下载之后,我们将其解压并放到前面文件系统rootfs
中,整个rootfs文件夹应该具有下列文件夹:
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp tomcat-9.0.29 usr var
其中的tomcat-9.0.29
即为解压后的Tomcat文件夹。
配置Config.json
OCI运行时规范详细阐述了容器运行时的生命周期,并规定了容器配置文件的格式以及详细含义,详情可以参考OCI-Github。当使用runc启动容器时,需要在bundle
(即包含rootfs的文件夹)中创建一个config.json
文件,该文件的内容是由OCI规范制定,规定了容器启动后要执行的程序、需要挂载的设备等信息。
在bundle目录中使用runc spec
能够自动创建一个config.json
,在此基础上进行更改即可。本文所创建的Tomcat容器,本质上是启动一个Alpine Linux
容器,并在容器内启动Tomcat。实际上docker中的Tomcat、Python、MySQL容器使用的方式类似,不过他们是基于debian
(linux发行版)而不是Alpine
。
完整地config.json见文末。
process
规定容器启动时要执行的程序。对于Tomcat容器来说,容器启动后需要配置安装JDK并配置JAVA_HOME, JRE_HOME
,然后再执行Tomcat的startup.sh
。如下为本文的启动脚本,启动程序为/bin/sh init.sh
,启动容器前需要将init.sh拷贝到rootfs中(或者直接在rootfs下编写该脚本)
init.sh
前面为准备工作,如网卡属性、路由表、源、jdk等,之后启动Tomcat以及/bin/ash
,通过ps
命令能够看到当前运行的Tomcat。
# setup network and start /bin/ash
ifconfig veth985 up
ifconfig veth985 10.1.1.2
ifconfig veth985 netmask 255.255.255.0
route add default gw 10.1.1.1
echo 'nameserver 202.38.64.56' > /etc/resolv.conf
echo -e 'http://mirrors.ustc.edu.cn/alpine/v3.10/main\nhttp://mirrors.ustc.edu.cn/alpine/v3.10/community' > /etc/apk/repositories
echo 'installing openjdk...'
apk add openjdk11
echo 'jdk installed, starting tomcat-9.0.29'
./tomcat-9.0.29/bin/startup.sh
/bin/ash
echo 'stopping'
apk del openjdk11
echo 'jdk uninstalled'
route del default gw 10.1.1.1
ip link del veth985
echo 'bye'
hooks
hooks
作为配置文件的一部分,主要规定了容器在prestart, poststart, poststop
(容器内进程启动前,启动后,容器销毁后)三个阶段,宿主环境内需要执行的工作,如配置容器网络等。具体来说,就是规定了三个要执行的程序、参数以及环境变量。这些程序会在前述三个阶段执行,并且容器此时的状态会通过stdin
发送给程序。由于OCI规范没有规定网络设备的创建,因此本文使用hooks为容器创建网卡设备,并将宿主环境的8080
端口(Tomcat默认)映射到主机的4399
端口。
prestart
由于需要从输入读取容器的进程标识pid,从而将网卡设备加入容器的Namespace
,因此使用C++编写的源程序并编译得到可执行程序。代码流程为read pid --> generate shell script --> exec(bash, script)
, 源代码见下
poststop
容器退出后,虚拟网卡设备会自动删除,因此poststop只需删除prestart中添加的路由表项及端口映射项即可。
sudo iptables -t nat -D PREROUTING -t nat -i ens33 -p tcp --dport 4399 -j DNAT --to 10.1.1.2:8080
sudo iptables -t filter -D FORWARD -p tcp -d 10.1.1.1 --dport 8080 -j ACCEPT
sudo iptables -t nat -D POSTROUTING -s 10.1.1.0/24 ! -d10.1.1.0/24 -j MASQUERADE
环境变量、权限属性
权限与环境变量配置用来保证容器内进程具有配置网络设备、读写文件夹的权限,从而能够正常启动容器,详细信息见config.json
.
完整config.json, prestart.cpp, poststop.sh
{
"ociVersion": "1.0.1-dev",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"/bin/ash",
"init.sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-11-openjdk/bin:/usr/lib/jvm/java-11-openjdk/jre/bin",
"JAVA_HOME=/usr/lib/jvm/java-11-openjdk/",
"JRE_HOME=/usr/lib/jvm/java-11-openjdk/jre/",
"TERM=xterm"
],
"cwd": "/",
"capabilities": {
"bounding": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_MKNOD",
"CAP_FOWNER",
"CAP_CHOWN",
"CAP_SYS_CHROOT",
"CAP_NET_BIND_SERVICE",
"CAP_NET_ADMIN",
"CAP_NET_RAW",
"CAP_SETUID",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETFCAP",
"CAP_SYS_ADMIN"
],
"effective": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_MKNOD",
"CAP_FOWNER",
"CAP_CHOWN",
"CAP_SYS_CHROOT",
"CAP_NET_BIND_SERVICE",
"CAP_NET_ADMIN",
"CAP_NET_RAW",
"CAP_SETUID",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETFCAP",
"CAP_SYS_ADMIN"
],
"inheritable": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_MKNOD",
"CAP_FOWNER",
"CAP_CHOWN",
"CAP_SYS_CHROOT",
"CAP_NET_BIND_SERVICE",
"CAP_NET_ADMIN",
"CAP_NET_RAW",
"CAP_SETUID",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETFCAP",
"CAP_SYS_ADMIN"
],
"permitted": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_MKNOD",
"CAP_FOWNER",
"CAP_CHOWN",
"CAP_SYS_CHROOT",
"CAP_NET_BIND_SERVICE",
"CAP_NET_ADMIN",
"CAP_NET_RAW",
"CAP_SETUID",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETFCAP",
"CAP_SYS_ADMIN"
],
"ambient": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_MKNOD",
"CAP_FOWNER",
"CAP_CHOWN",
"CAP_SYS_CHROOT",
"CAP_NET_BIND_SERVICE",
"CAP_NET_ADMIN",
"CAP_NET_RAW",
"CAP_SETUID",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETFCAP",
"CAP_SYS_ADMIN"
]
},
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
]
},
"root": {
"path": "rootfs",
"readonly": false
},
"hostname": "runc",
"mounts": [
{
"destination": "/proc",
"type": "proc",
"source": "proc"
},
{
"destination": "/dev",
"type": "tmpfs",
"source": "tmpfs",
"options": [
"nosuid",
"strictatime",
"mode=755",
"size=65536k"
]
},
{
"destination": "/dev/pts",
"type": "devpts",
"source": "devpts",
"options": [
"nosuid",
"noexec",
"newinstance",
"ptmxmode=0666",
"mode=0620",
"gid=5"
]
},
{
"destination": "/dev/shm",
"type": "tmpfs",
"source": "shm",
"options": [
"nosuid",
"noexec",
"nodev",
"mode=1777",
"size=65536k"
]
},
{
"destination": "/dev/mqueue",
"type": "mqueue",
"source": "mqueue",
"options": [
"nosuid",
"noexec",
"nodev"
]
},
{
"destination": "/sys",
"type": "sysfs",
"source": "sysfs",
"options": [
"nosuid",
"noexec",
"nodev",
"ro"
]
},
{
"destination": "/sys/fs/cgroup",
"type": "cgroup",
"source": "cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
],
"hooks": {
"prestart": [
{
"path": "./prestart.out",
"args": [
"prestart.out"
]
}
],
"poststop": [
{
"path": "/bin/bash",
"args": [
"bash",
"./poststop.sh"
]
}
]
},
"linux": {
"resources": {
"devices": [
{
"allow": false,
"access": "rwm"
}
]
},
"namespaces": [
{
"type": "pid"
},
{
"type": "network"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
}
],
"maskedPaths": [
"/proc/acpi",
"/proc/asound",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/proc/scsi"
],
"readonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
}
}
FILE *fp_log;
const void fail(const string tmps = "") {
if (fp_log != nullptr) {
if (!tmps.empty()) fprintf(fp_log, "%s\n", tmps.c_str());
fclose(fp_log);
}
exit(EXIT_FAILURE);
}
int main(int argc, char const *argv[]) {
fp_log = fopen("./pre_start.log", "w");
string container_state;
cin >> container_state;
fprintf(fp_log, "%s\n", container_state.c_str());
fclose(fp_log);
const int sz_container_state = container_state.size();
if (sz_container_state <= 0) fail("container state size 0");
int st = 0;
while (st + 2 < sz_container_state && container_state.substr(st, 3) != "pid") st++;
if (st >= sz_container_state) fail("cannot find 'pid' inside state");
st = st + 5;
int ed = st;
while (ed < sz_container_state && container_state[ed] != ',') ed++;
if (ed >= sz_container_state) fail("cannot find ',' after 'pid' inside state");
string pid_netns;
pid_netns = container_state.substr(st, ed - st);
string veth1 = "veth211";
string veth2 = "veth985";
string lines[] = {
"sudo ip link add " + veth1 + " type veth peer name " + veth2,
"sudo ifconfig " + veth1 + " 10.1.1.1/24 up",
"sudo ip link set " + veth2 + " netns " + pid_netns,
"sudo iptables -t nat -A POSTROUTING -s 10.1.1.0/24 ! -d 10.1.1.0/24 -j MASQUERADE",
"sudo iptables -A PREROUTING -t nat -i ens33 -p tcp --dport 4399 -j DNAT --to 10.1.1.2:8080",
"sudo iptables -A FORWARD -p tcp -d 10.1.1.1 --dport 8080 -j ACCEPT"
};
string hook_path = "./prestart.sh";
FILE *fp_sh = fopen(hook_path.c_str(), "w");
for (auto &&line : lines) {
fprintf(fp_sh, "%s\n", line.c_str());
}
fclose(fp_sh);
execl("/bin/bash", "bash", hook_path.c_str(), nullptr);
return 0;
}
poststop.sh
sudo iptables -t nat -D PREROUTING -t nat -i ens33 -p tcp --dport 4399 -j DNAT --to 10.1.1.2:8080
sudo iptables -t filter -D FORWARD -p tcp -d 10.1.1.1 --dport 8080 -j ACCEPT
sudo iptables -t nat -D POSTROUTING -s 10.1.1.0/24 ! -d10.1.1.0/24 -j MASQUERADE
分析容器资源使用量
runc events --interval 1s id
能够不断查询容器的cpu, memory以及IO使用量统计。ab
则是Apache提供的服务器压力测试工具,需要安装Apace服务器后其他服务,使用方式为ab -c numOfConcurrency -n numOfRequests http://yourweb.com/path
。如下为具体分析过程。
- 新终端 - 执行
runc events
,查看进程的属性sudo runc events --interval 0.01s helo > perf_analysis/runc_events.txt
- 新终端 - 使用ApacheBenchmark测试性能
ab -c 1000 -n 6000 http://10.1.1.2:8080/ > perf_analysis/ab_out.txt
- 使用Python分析数据
import json import matplotlib.pyplot as plt x_axis = [] cpu_usage = [] mem_usage = [] act_anon = [] pgfault = [] pgpgin = [] pgpgout = [] rss = [] pids_current = [] def read_data(): i = 1 with open("./runc_events.txt", encoding="utf-8", mode="r") as fp: lines = fp.readlines() mb = 1024 * 1024 for line in lines: if line == "": break data_line = json.loads(line)["data"] cpu_usage.append( int(data_line["cpu"]["usage"]["total"]) / 1000000) mem_usage.append(data_line["memory"]["usage"]["usage"] / mb) data_line_memraw = data_line["memory"]["raw"] act_anon.append(int(data_line_memraw["active_anon"]) / mb) pgfault.append(int(data_line_memraw["pgfault"])) pgpgin.append(int(data_line_memraw["pgpgin"])) pgpgout.append(int(data_line_memraw["pgpgout"])) rss.append(int(data_line_memraw["rss"]) / mb) pids_current.append(int(data_line["pids"]["current"])) x_axis.append(i) i += 1 def subplt(rows, index, title, x, y, cls=1): plt.subplot(rows, cls, index) plt.plot(x, y) plt.title(title) def show(): plt.figure(figsize=(30, 80)) rows, cols = 4, 1 plt.subplot(rows, cols, 1) plt.plot(x_axis, cpu_usage) plt.title('cpu usage(ms)') plt.subplot(rows, cols, 2) plt.plot(x_axis, mem_usage) plt.title('memory usage(mb)') plt.subplot(rows, cols, 3) plt.plot(x_axis, pgfault) plt.title('memory raw page fault') plt.subplot(rows, cols, 4) plt.plot(x_axis, pids_current) plt.title('pids current') # plt.show() plt.savefig("./events_runc_tomcat.png") read_data() show()
最后结果如下图所示