Mesos架构和工作流程简介

1. 什么是Mesos

Mesos是Apache软件基金会维护的一个开源软件,它负责管理一批服务器集群,并将所有服务器集群的CPU、GPU、内存、存储、端口和其它相关计算资源进行了抽象统一,让用户进行动态配置和使用,提高整个系统的资源利用率,并以集群分布式的方式保证系统运行的高可用和弹性扩展。

2. Mesos架构和工作流程

官网中有个Mesos的架构图如下,

主要的模块有,

  • Mesos Master:管理所有机器资源信息,一般会部署多台来保证master的高可用,以resource offers的方式定时告知scheduler可用资源。
  • ZooKeeper:实现当前工作Mesos Master的选举,并实现数据一致性。
  • Mesos Agent:集群中每个机器都会部署一台Mesos Agent,定时汇报当前机器可用资源给Master,并从Master获取分配的任务信息,调用Executor来运行。
  • Scheduler:定时得到Master发送过来的resource offers,将任务列表中各个任务的资源需求进行一一匹配,一旦资源需求获得满足,则告诉Master使用匹配到的资源启动任务。
  • Executor:被Agent调用,根据指定的任务和资源信息,执行任务。

其中Scheduler和Executor是开发者根据自己需要进行开发,业界里各种Mesos Framework也就是实现这两部分,例如图中的Hadoop Mesos/MPI Mesos,还有Mesosphere公司的marathon,HudSpot公司的Singularity,国内数人云的Swan等。

Mesos提供两层调度,一层是Agent调用Executor执行Master分配的任务,另外一层是任务的资源匹配和调度放到Scheduler,由Scheduler推送任务到Master。由于Scheduler和Executor的可动态调整,这样也使得任务调度策略和执行变得可动态配置。

整个Mesos系统的主要工作流程如下,

  1. Mesos Agent定期上报各个机器的资源(CPU、Memory、磁盘、端口号)
  2. Mesos Master收集所有Agent可用的资源,定期推送resource offer给Scheduler,offer中描述了可用的资源信息。
  3. Scheduler启动时,会找到Mesos Master并注册,定期获取Master推送给的resource offer,以此了解可用的资源。
  4. 用户向Scheduler申请任务需要的资源,执行相关任务,例如申请2CPU 4G Mem来运行程序Demo。
  5. Scheduler在获取到Master推送的offer后,当offer中的资源满足用户申请的任务需求,就向Master申请执行。
  6. Mesos master根据Scheduler申请,在相应的资源上调用Agent执行任务。

整个流程图见如下,

需要注意的是,Mesos的四大组件(Master/Slave/Scheduler/Executor)之间的通信,是通过libprocess实现actor model模型的进程间消息异步通信,每个进程是一个actor。

见上图,在Mesos的master节点中,每个Framework以及Slave都是一个远程的actor。而slave节点上,每个executor是一个actor,只不过内置的executor是在同一个进程中的,而其他自定义的executor是独立的进程,executor和slave之间通过进程间通信方式(网络端口)交互。

Actor模型通信带来的好处是省去了对消息队列的依赖,但同时由于消息都是异步的,需要actor处理消息的丢失以及超时逻辑,Mesos无法保证消息的可靠投递,提供的投递策略是 at-most-once(至多一次,不会重试)。

3. 如何开发一个Mesos Framework

Mesos提供支持多种语言的Framework开发,包括Java/Python/Scala等,下面以Java语言为例介绍Mesos Framework的开发,其它语言类似。

一个Mesos Framework主要实现两个模块,

  1. Scheduler
  2. Executor

其中Scheduler会是单独可运行的项目程序,比如一个Java程序或者一个Java Web后端服务;Executor则会是Jar包项目,打出Jar包后由Mesos Agent引用并启动。

在两个项目中各自引入依赖包,

<dependency>
<groupId>org.apache.mesos</groupId>
<artifactId>mesos</artifactId>
</dependency>

在包org.apache.mesos中提供如下主要抽象接口,

  • Scheduler:这是Framework要实现的Scheduler接口,用于Mesos Master回调。
  • Executor:这是Framework要实现的Executor接口,用于Agent回调。
  • SchedulerDriver:这是Scheduler和Mesos进行通信的抽象接口。
  • ExecutorDriver:这是Executor和Mesos进行通信的抽象接口。

在两个项目中需实现上述的Scheduler和Executor抽象接口,各个接口方法的实现需求请参见Apache Mesos项目源代码中对其的接口描述,可以通过如下git命令下载代码,
git clone git@github.com:apache/mesos.git

在Apache Mesos项目代码中有对Mesos Framework开发提供的样例实现,在其文件目录中,

  • mesos\src\examples\java\TestFramework.java
  • mesos\src\examples\java\TestExecutor.java

前一个实现了org.apache.mesos.Scheduler,是一个可单独运行的Java程序;后一个实现了org.apache.mesos.Executor,是一个Jar包。

注:在包org.apache.mesos中还有两个抽象接口:SchedulerDriver和ExecutorDriver,Apache Mesos提供两个Driver的默认具体实现MesosSchedulerDriver和MesosExecutorDriver,所以Mesos Framework可以直接使用,用于和Mesos Master通信,不用自己实现这两个Driver。

4. 参考资料

在Linux中使用namespace和cgroup实现进程的资源隔离和限制

这几年,Docker容器和其它资源虚拟化平台(比如Mesos)研究非常多,各种应用技术层出不穷,但是要想使用好容器,其底层实现技术原理必须要掌握清楚。其中,无论是LXC、Mesos还是Docker容器,都依赖于Linux内核底层技术namespace和cgroup,本文以简要方式演示如何使用namespace和cgroup来实现进程的资源隔离和限制。

1. 什么是Namespace和Cgroups

在Linux中,敲击man帮助手册命令,

- man namespaces
- man cgroups

可以获取到namespace和cgoups的相关文档定义,

A namespace wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. Changes to the global resource are visible to other processes that are members of the namespace, but are invisible to other processes. One use of namespaces is to implement containers……

Control cgroups, usually referred to as cgroups, are a Linux kernel feature which provides for grouping of tasks and resource tracking and limitations for those groups……

从上述定义,可以知道namespace/cgroup是可以控制指定进程对系统资源的支配调度,系统资源包括网络、进程间通信、进程空间、文件系统及其用户权限等。换句话说,namespace/cgroup对系统资源做了一层抽象,让系统资源对进程部分可见,在同一个namespace中的进程,可看到系统资源是一样的。

Linux提供Namespace/Cgroup功能,主要是为了实现Linux容器技术。

2. 启动一个进程

在进入演示步骤前,我们先了解下Linux如何启动一个进程。

在Linux中有三种方法来启动一个子进程,分别为

1) clone:从父进程中创建子进程,和fork不一样之处在于,可以更加灵活的指定子进程的运行上下文,也具有更为强大的进程调度功能。man namespaces
2) fork:该方法创造的子进程是几乎是父进程的完整副本(进程ID\内存锁等除外)。子进程和父进程各自独立执行,先后顺序不定。
3) vfork:该方法创建的子进程和父进程共享物理空间,包括堆栈,而且和fork不一样之处是,其会阻塞父进程的执行。换句话说,vfork保证子进程在父进程之前执行完毕。

演示2-1

创建一个代码文件clone.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];

int child_main(void * args) {
   printf("in child process, pid=%d\n", getpid());
   printf("quit child process...\n");
   return EXIT_SUCCESS;
}

int main(){
   printf("start...\n");
   printf("in parent process, pid=%d\n", getpid());
   int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD, NULL);
   waitpid(child_pid, NULL, 0);
   printf("quit...\n");
   return EXIT_SUCCESS;
}

编译命令gcc clone.c -o clone.o

运行可执行文件clone.o,有如下console输出结果,

start...
in parent process, pid=4673
in child process, pid=4674
quit child process...
quit...

可以看到一个子进程(ID=4674)通过父进程启动,然后先后退出。

演示2-2

创建一个代码文件fork.c

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

int main(void) {
    int count = 1;
    int child;

    child = fork();

    if(child < 0) {
        perror("fork error : ");
    } else if(child == 0) {
        ++count;
        printf("in child process, pid=%d, count=%d (%p)\n", getpid(), count, &count);
    } else {
        ++count;
        printf("in parent process, pid=%d, count=%d (%p)\n", getpid(), count, &count);
    }

    printf("pid=%d quit now...\n", getpid());
    return EXIT_SUCCESS;
}

编译命令gcc fork.c -o fork.o

运行可执行文件fork.o,有如下console输出结果,

in parent process, pid=4253, count=2 (0x7ffd29cf1180)
pid=4253 quit now...
in child process, pid=4254, count=2 (0x7ffd29cf1180)
pid=4254 quit now...

可以看到在fork之后,程序分成两个进程同时执行并且分两次退出,其中上面的输出结果告知父进程先退出。

演示2-3

创建一个代码文件vfork.c

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <unistd.h>

int main(void) {
    int count = 1;
    int child;

    child = vfork();

    if(child < 0) {
        perror("fork error : ");
    } else if(child == 0) {
        ++count;
        printf("in child process, pid=%d, count=%d (%p)\n", getpid(), count, &count);
        sleep(2);
        printf("pid=%d sleep 2 seconds, and now quit...\n", getpid());
        exit(0);
    } else {
        ++count;
        printf("in parent process, pid=%d, count=%d (%p)\n", getpid(), count, &count);
        printf("pid=%d quit now...\n", getpid());
        exit(0);
    }

    return EXIT_SUCCESS;
}

编译命令gcc vfork.c -o vfork.o

运行可执行文件vfork.o,有如下输出结果,

in child process, pid=4317, count=2 (0x7ffdbbed5830)
pid=4317 sleep 2 seconds, and now quit...
in parent process, pid=4316, count=3 (0x7ffdbbed5830)
pid=4316 quit now...

可以看到在vfork之后,程序分成两个进程同时执行并且分两次退出,其中父进程被子进程阻塞,在等待子进程sleep两秒退出后,父进程才退出。

3. 通过clone启动一个进程

通过clone方法可以启动一个进程,其中调用方法可以指定flags参数来标记父子进程之间需要共享/隔离的资源,

flag标记位 功能 隔离的资源 Linux
内核版本支持
CLONE_NEWIPC 在新的 IPC namespace启动新进程 进程间通信,包括信号量、消息队列和共享内存 since 2.6.19
CLONE_NEWNET 在新的network namespace启动新进程 网络设备、网络栈、端口 since 2.6.24
CLONE_NEWNS 在新的mount namespace启动新进程 挂载点(文件系统) since 2.6.19
CLONE_NEWPID 在新的PID namespace启动新进程 进程编号(新的进程树) since 2.6.24
CLONE_NEWUSER 在新的user namespace启动新进程 用户和用户组 在2.6.23引入,在3.5和3.8有更新
CLONE_NEWUTS 在新的UTS namespace启动新进程 主机名和域名 since 2.6.19
CLONE_NEWCGROUP 在新的cgroup namespace启动新进程 CPU、内存、磁盘读写等 since 4.6

更详细信息可以通过man clone命令来查看。

下面将上述各标记位来一步步实现新启子进程的各项资源隔离。

4. 隔离进程的主机名和域名

演示4

创建一个代码文件demo_uts.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];
char* const child_args[] = {
  "/bin/bash",
  NULL
};

int child_main(void * args) {
   printf("in child process\n");
   sethostname("NewHostName", 12);
   execv(child_args[0], child_args);
   printf("quit child process…\n");
   return 1;
}

int main(){
   printf("start parent process...\n");
   int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUTS | SIGCHLD, NULL);
   waitpid(child_pid, NULL, 0);
   printf("quit parent process...\n");
   return 0;
}

编译命令gcc demo_uts.c -o demo_uts.o
请切换到root用户权限下(sudo su -),运行可执行文件demo_uts.o,有如下输出结果,

root@test:/home/test# ./demo_uts.o
start parent process...
in child process
root@NewHostName:/home/test#

可以看到当前进入子进程后,当前的host名字已经更改为NewHostName,这是当前进程在网络中使用的新主机名。

演示完毕后,别忘了输入exit以退出当前子进程,然后父进程安全退出,整个程序演示完毕。

root@NewHostName:/home/test# exit
exit
quit parent process...
root@test:/home/test#

注意,上述和后续演示需要切换到root用户权限下执行,这是因为子进程会更改网络等系统设置,需要相应的权限,这也是docker run需要root权限的原因。

5. 隔离进程间通信

在Linux中,敲击man帮助手册命令,

- ipcs:查看进程间通信设施
- ipcmk:创建进程间通信设施

在使用ipcs命令可以查看当前进程所能使用的通信设施,主要包括

- 消息队列
- 共享内存
- 信号量

如果想创建一个消息队列,可以通过命令ipcmk -Q test。运行Linux命令ipcs -q可以看到刚刚创建的test消息队列,

root@test:~# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x01bbd371 0 root 644 0 0

通过上述两个命令可以查看进程间的通信是否隔离。

演示5

创建一个代码文件demo_ipc.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];
char* const child_args[] = {
  "/bin/bash",
  NULL
};

int child_main(void * args) {
   printf("in child process\n");
   sethostname("NewHostName", 12);
   execv(child_args[0], child_args);
   printf("quit child process…\n");
   return 1;
}

int main(){
   printf("start parent process...\n");
   int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL);
   waitpid(child_pid, NULL, 0);
   printf("quit parent process...\n");
   return 0;
}

编译命令gcc demo_ipc.c -o demo_ipc.o
请切换到root用户权限下运行可执行文件demo_ipc.o,进入子进程,然后执行命令ipcs查看当前可见的进程间通信资源,

root@test:/home/test# ./demo_ipc.o
start parent process...
in child process
root@NewHostName:/home/test# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages

可以看到当前进入子进程后,当前没有刚刚创建的test消息队列,这说明子进程已经看不到父进程的进程间通信资源。如果此时在子进程中创建进程间通信资源,然后到root进程空间查看,root进程也是看不到子进程的通信资源。
新启动的进程间通信资源已经被隔离。
演示完毕后,别忘了输入exit以退出当前子进程和父进程,结束整个演示。

6. 隔离进程空间

Linux中有个进程树的概念,在进程树中,有一个PID=1的进程,该进程是一个非常特殊的进程,它拥有特权,可以对所有后续启动的子进程资源监控和回收。
在Linux系统中可以通过pstree –g来查看整个进程树和进程ID。
在进程树中,所有父节点可以看到子节点的进程,并通过信号灯方式对子节点的进程产生影响(比如发送OOM信号,告知子进程退出)。子节点看不到父节点PID namespace中的任何内容。
下面的演示将为新启的进程实现独立的进程空间,拥有自己的进程树和pid为1的特权进程。

演示6

创建一个代码文件demo_pid.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];
char* const child_args[] = {
  "/bin/bash",
  NULL
};

int child_main(void * args) {
   printf("in child process\n");
   sethostname("NewHostName", 12);
   execv(child_args[0], child_args);
   printf("quit child process…\n");
   return 1;
}

int main(){
   printf("start parent process...\n");
   int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL);
   waitpid(child_pid, NULL, 0);
   printf("quit parent process...\n");
   return 0;
}

编译命令gcc demo_pid.c -o demo_pid.o
请切换到root用户权限下运行可执行文件demo_pid.o,进入子进程,

root@test:/home/test# ./demo_pid.o
start parent process...
in child process
root@NewHostName:/home/test# mount -t proc proc /proc
root@NewHostName:/home/test# pstree -g
bash(1)───pstree(13)

可以看到在子进程中看到了一个全新的进程树,里面有个pid=1的进程,按照Linux的定义,这个进程将拥有管理当前进程空间的特权。对于子进程来说,它已经拥有了一个独立的进程空间。

注意,上述中执行了mount -t proc proc /proc命令,由于子进程和父进程的文件系统还没有隔离,该命令会重新加载子进程中的proc文件,从而执行pstree时能够读取到更新后的进程空间信息。该命令会对父进程的文件系统产生影响,若要恢复,同样在父进程中执行一次mount –t proc proc /proc命令

7. 隔离文件系统

Linux的文件系统挂载方式主要包括如下几种,

- 共享挂载(shared):父子进程空间的文件变化共享
- 从属挂载(slave):
- 私有挂载(private):子进程空间的文件变化不影响
- 不可绑定挂载(unbindable):该文件不允许挂载

在Linux中可以通过命令mount –help查看支持的文件挂载方式,不同的挂载方式决定了在各个挂载点发生文件变化时,如何通知其他挂载点。
进程中是通过隔离文件系统挂载点来隔离文件系统。

演示7

获取演示6的程序代码,将flags添加CLONE_NEWNS,则新启动的进程拥有隔离的文件系统。

8. 隔离用户权限

在Linux中用户ID为0的用户是一个拥有特权的超级用户。在内核版本2.2之前,Linux只区分普通用户和超级用户(root)的进程,超级用户的进程不受任何权限检查。从内核版本2.2之后,Linux将超级用户的权限进行单元区分,各个权限单元就叫capabilities,关于capabilities的权限列表,可以通过命令man capabilities查看。
下面的演示将为新启动的进程运行在一个ID为0的超级用户中。

演示8

创建一个代码文件demo_user.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sys/capability.h>
#include <sched.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];
char* const child_args[] = {
  "/bin/bash",
  NULL
};

void set_uid_map(pid_t pid, int inside_id, int outside_id, int length){
   char path[256];
   sprintf(path, "/proc/%d/uid_map", pid);
   FILE* uid_map = fopen(path, "w");
   fprintf(uid_map, "%d %d %d", inside_id, outside_id, length);
   fclose(uid_map);
}

void set_gid_map(pid_t pid, int inside_id, int outside_id, int length){
   char path[256];
   sprintf(path, "/proc/%d/gid_map", pid);
   FILE* gid_map = fopen(path, "w");
   fprintf(gid_map, "%d %d %d", inside_id, outside_id, length);
   fclose(gid_map);
}

int child_main(void * args) {
   printf("in child process\n");
   cap_t caps;
   set_uid_map(getpid(), 0, 1000, 1);
   set_gid_map(getpid(), 0, 1000, 1);
   printf("eUID = %ld; eGID = %ld; ", (long) geteuid(), (long) getegid());
   caps = cap_get_proc();
   printf("capabilities: %s\n", cap_to_text(caps, NULL));
   execv(child_args[0], child_args);
   printf("quit child process…\n");
   return 1;
}

int main(){
   printf("start parent ...\n");
   int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, NULL);
   waitpid(child_pid, NULL, 0);
   printf("quit parent ...\n");
   return 0;
}

编译命令gcc demo_user.c -lcap -o demo_user.o
若编译出现下面的报错信息,

fatal error: sys/capability.h: No such file or directory
#include <sys/capability.h>

可以尝试安装如下依赖包来解决。

sudo apt-get install libcap-dev

为了方便查看效果,请切换到普通用户权限下运行可执行文件demo_user.o,进入子进程,

test@test:/home/test# ./demo_user.o
start parent process...
in child process
eUID = 0; eGID = 65534; capabilities: = cap_chown,……
root@test:/home/test# id
uid=0(root)……

可以看到当前子进程运行在root用户下。

注意,在程序代码中,

set_uid_map(getpid(), 0, 1000, 1);
set_gid_map(getpid(), 0, 1000, 1)

里面的1000是当前执行用户ID,该ID可能会随着系统用户发生变化,请使用Linux命令id查看用户ID,并相应进行替代。

9. Cgroup实现原理

在进行cgroup演示前,需要了解几个概念,

- Task (任务):一个进程或者线程
- Cgroup (控制组):cgroup技术对资源的控制以控制组为单位
- Subsystem(子系统):一个子系统就是一个资源调度器
- Hierachy (层级):控制组之间可以组织成层级的形式,子控制组继承父控制组的属性。

在Linux中可以通过如下命令查看相关信息,

- lscgroup:列出所有的cgroup
- lssubsys:列出所有的子系统,其中lssubsys –M列出子系统挂载点

若提示命令不存在,可以执行apt install cgroup-tools进行安装

接下来我们将演示如何通过cgroup来控制一个进程的cpu使用率。

演示9

创建一个循环执行的bash脚本long_task.sh

#!/bin/bash
x=0
while [ True ];do
    x=$x+1
    #sleep(100)
done;

执行该脚本。
通过Linux top命令,可以看到该任务已经在执行中,

PID USER %CPU %MEM TIME+ COMMAND
5947 test 97.4 0.4 1:23.82 bash

进程ID为5947,CPU占用率为97.4%,接近100%。

下一步我们切换目录到cpu子系统,创建一个test目录

cd /sys/fs/cgroup/cpu/
mkdir ./test

这个时候查看test目录,可以看到该目录下已经有了很多文件,

ls ./test
drwxr-xr-x 2 root root 0 10月 16 21:03 ./
dr-xr-xr-x 6 root root 0 10月 16 15:13 ../
-rw-r--r-- 1 root root 0 10月 16 21:03 cgroup.clone_children
-rw-r--r-- 1 root root 0 10月 16 21:03 cgroup.procs
-r--r--r-- 1 root root 0 10月 16 21:03 cpuacct.stat
-rw-r--r-- 1 root root 0 10月 16 21:03 cpuacct.usage
-r--r--r-- 1 root root 0 10月 16 21:03 cpuacct.usage_all
-r--r--r-- 1 root root 0 10月 16 21:03 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 10月 16 21:03 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0 10月 16 21:03 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 10月 16 21:03 cpuacct.usage_sys
-r--r--r-- 1 root root 0 10月 16 21:03 cpuacct.usage_user
-rw-r--r-- 1 root root 0 10月 16 21:03 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 10月 16 21:03 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 10月 16 21:03 cpu.shares
-r--r--r-- 1 root root 0 10月 16 21:03 cpu.stat
-rw-r--r-- 1 root root 0 10月 16 21:03 notify_on_release
-rw-r--r-- 1 root root 0 10月 16 21:03 tasks

我们执行如下命令,将进程5947的CPU使用率控制到50%,

cd /sys/fs/cgroup/cpu/test
echo 5947 > tasks
echo 50000 > cpu.cfs_quota_us

这个时候再通过Linux top命令查看任务执行情况,

PID USER %CPU %MEM TIME+ COMMAND
5947 test 50.1 0.4 1:23.82 bash

可以看到5947的CPU使用率已经从100%降到了50.1%,接近50%的CPU控制。

10. 结束语

到目前为止,我们使用Linux内核底层技术namespace和cgroup一步步地演示了进程的资源隔离和限制,通过这些演示可以很好地了解轻量级容器技术的实现原理。我们不仅要隔离进程的运行空间、文件系统、用户权限,让进程拥有独立的网络实体身份,而且要配置进程的CPU、内存、磁盘读写等。

目前轻量级虚拟化技术都在进程级别,可以看到,其进程的隔离性和安全性面临着挑战。

演示代码

本文演示资料运行在Ubuntu 16.10, Linux 内核版本为4.8.0-59-generic。
演示代码仓库地址:https://gitee.com/pphh/blog,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/blog.git

上述代码样例在文件路径blog\171019_namespace_cgroup中。

参考资料

Linux之shell脚本

 

1. 一个简单的脚本

一个简单的shell脚本如下,

#!/bin/bash
# this is a simple shell script

echo "$1"

str="hello,world"
echo "$str"

exit 0

其中第一行#!/bin/bash声明的这个脚本运行的shell工具,有些脚本会在第一行声明为#!/bin/sh,这两种声明会有细微差别,主要在于:

bash是普通的shell工具,各个linux系统都会提供这个工具。

sh使用的是POSIX标准模式shell工具,一般linux系统会软链到/bin/bash工具,或者在Ubuntu会软链到/bin/dash,软链的时候都会打开相应的POSIX标准模式。

2. 数学计算

一个简单的数学计算脚本如下,

#!/bin/bash

a=1
b=2
i=$(($a+$b))
j=$(($a*$b))
echo "$a+$b=$i"
echo "$a*$b=$j"

read -p "please input a number:" x
read -p "please input another number:" y
echo "$x+$y="$(($x+$y))
echo "$x*$y="$(($x*$y))

上述会将变量a和b相加相乘,并输入结果。如果希望让用户输入,可以使用read这个命令工具。

3. 流程控制

Linux Shell脚本语言提供条件、循环、case switch等用于流程控制,

#!/bin/bash

read -p "please input a number:" x
if [ $x -gt 0 ]; then
 echo "$x is greater than zero"
elif [ $x -eq 0 ]; then
 echo "$x is equal zero"
else
 echo "$x is less than zero" 
fi

for (( i=1;i<=5;i++ ))
do
 echo "for loop: $i times"
done

num=10
while [ $num -gt 0 ]
do
 read -p "please input a number less than 0: " num
done

上述脚本中,第一段让用户输入一个输入数字,然后判断输入的值是大于、等于、小于零;第二段使用for做了5次循环;第三段会让用户输入一个数字,如果数字大于零,则让用户重新输入,一直到有个小于零的数字才退出循环。

4. 用户输入

#!/bin/bash

read -p "please input your first name: " firstname
read -p "please input your last name:" lastname
echo "Your name is: $firstname $lastname"

5. 文件读写

在下面的脚本中,第一段判断/tmp/test.log文件是否在,如果不在,则创建一个,如果在,则删除;第二段则在/tmp目录下找到7天之前的*.log日志文件并删除。

#!/bin/bash

if [ ! -e /tmp/test.log ]; then
 echo "file doesn't exist, try to create one"
 touch /tmp/test.log
else
 echo "file exists there already, try to remove it"
 rm /tmp/test.log
fi

#remove log file which is created earlier than 7 days ago
find /tmp -mtime +7 -type f -name *.log -exec rm -f {} \;
find /tmp -mtime +7 -type f -name [ab].log -exec rm -f {} \;

6. 脚本的执行

脚本的执行有下面几种方式,可以在shell中直接运行脚本,也可以使用sh来调用,也可以使用source来调用。其中source的调用执行后,脚本的变量声明都会留在当前shell中。

./test.sh
sh ./test.sh
source ./test.sh
nohup sh ./loop.sh &

最后一个是运行loop.sh脚本在后台,测试脚本如下,

#!/bin/bash

while true
do
 echo "sleep 1 seconds"
 date +%Y%m%d%H%M%S >> /tmp/date.log
 sleep 1
done

结尾

上述脚本代码可以到此git代码仓库(https://git.oschina.net/pphh/shellTmpl.git)中获取,此代码仓库提供更多的shell脚本模板,比如打印系统版本信息,硬件信息等。