Json Web Token的介绍和最佳实践

1. 什么是JSON Web Token

JSON Web Token是一个开放的、行业规范(RFC7519)的方法,用于通信双方进行可靠的数据传输。它规定了一种紧凑的数据格式,将要传输的数据以JSON对象方式的组织,然后进行编码和签名,转化成为Token。

JSON Web Token包含三部分信息数据,

Header.Payload.Signature

其中,

  • Header:数据头部,声明Token类型和加密算法(比如HMAC SHA256或者RSA)
  • Payload:数据的信息体,包含了要传输的信息体
  • Signature:数据的数字签名,用于对数据进行校验来源可靠性和不被篡改

在三部分中,header/payload都是公开信息,JSON对象数据格式,使用Base64方式进行编码,这意味着header/payload可以被任何人进行解码和阅读,所以不要在header/payload放入敏感信息。

一个Token样例为,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

其中,

Header Base64编解码 {"alg":"HS256","typ":"JWT"}
Payload Base64编解码 {"sub":"1234567890","name":"John Doe","admin":true}
Signature HS256签名算法 HS256(eyJhb……RydWV9, secret)

注:签名算法中的eyJhb……RydWV9为Base64编码后的Header.Payload

2. 应用场景

JWT一般用在如下两个场景,

  • 认证:这是最常用JWT应用场景,用于用户登录后,后端服务器颁发一个带有时效性的JWT token给前端应用,然后前端应用在后续的请求中使用该Token进行访问后端资源。
  • 信息交换:由于JWT对数据进行了签名,信息不能被篡改,可以可靠地在通信双方进行数据信息交换。

下图简单描绘了JWT在认证场景的使用流程,

使用方法

JWT的使用方法包括:签发、解码、校验,

  • 签发:用于对信息进行签名,然后颁发给使用方,供后续进行认证和信息交换
  • 解码:对JWT的信息体解码,获取信息,一般用在不进行校验的场合,比如在浏览器端
  • 校验:对JWT的校验,确认信息的来源可靠性和不被篡改、时效不过期

目前已经有如下语言的支持,

  • Java
  • Node.js
  • JavaScript
  • Microsoft .NET、.NET RT(Windows 8.1 and Windows Phone 8.1)
  • Python
  • Perl
  • Ruby
  • Go
  • Lua
  • Scala
  • Object-C
  • Swift
  • PHP

等等。

下面主要介绍JavaScript/Node.Js/Java三种语言对Jwt的签发、校验、解码的代码实现,这三种语言分别代表着web客户端/web服务器端/后端服务。

1) JavaScript

下面使用jwt-decode类库在web客户端(浏览器端)对token进行解码操作。

点击这里下载jwt-token.js脚本。

在HTML页面中引入上面的JavaScript脚本,下面为简单的解码演示,

<script type="text/javascript" src="jwt-token.js"></script>
<script>

    let jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
    let payload = jwtToken.split('.')[1];
    let payloadInfo = JSON.parse(jwt.base64urldecode(payload));

    for (let key in payloadInfo) {
        console.log(key + ": " + payloadInfo[key]);
    }

</script>
2) Node.js

下面使用Node Auth0类库在nodejs的web服务器端进行token的签发和校验
安装命令,从npm中安装jsonwebtoken
npm install jsonwebtoken

签发

let jwt = require('jsonwebtoken');
let token = jwt.sign({name: 'John'}, 'secret', { algorithm: 'HS256', expiresIn: '1d', issuer: 'pphh'});

校验

let jwt = require('jsonwebtoken');
let info = jwt.verify(token, 'secret');
console.log(info.name) // John

解码

let info = jwt.decode(token, {complete: true});
console.log(info.header);
console.log(info.payload);
3) Java

下面使用Java Auth0类库来进行token的签发和校验。

在Maven项目中添加依赖项,

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>

签发

try {
    Algorithm algorithm = Algorithm.HMAC256("secret");
    String token = JWT.create()
                      .withIssuer("auth0")
                      .sign(algorithm);
} catch (UnsupportedEncodingException exception){
    //UTF-8 encoding not supported
} catch (JWTCreationException exception){
    //Invalid Signing configuration / Couldn't convert Claims.
}

校验

String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
    Algorithm algorithm = Algorithm.HMAC256("secret");
    JWTVerifier verifier = JWT.require(algorithm)
                              .withIssuer("auth0")
                              .build(); //Reusable verifier instance
    DecodedJWT jwt = verifier.verify(token);
} catch (UnsupportedEncodingException exception){
    //UTF-8 encoding not supported
} catch (JWTVerificationException exception){
    //Invalid signature/claims
}

解码

String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
    DecodedJWT jwt = JWT.decode(token);
} catch (JWTDecodeException exception){
    //Invalid token
}

更多语言和JWT类库使用,请参考官方网站和样例。

最佳实践

  1. 在JWT信息体Payload中不要包括敏感信息,若一定要,可以尝试使用JSON Web Encryption 。
  2. 一定要添加Token过期时间expiration time (exp claim) ,用于时效性检查。
  3. 不建议以无签名算法的方式(签名算法为none)来颁发JWT token,这个有安全风险,会导致攻击者绕过某些JWT类库的校验,具体可以查看该资料
  4. 建议使用HTTPS来传输JWT token信息,在最大程度保证信息在传输过程中的安全性,防止被中间人截取。
  5. 确保签名密钥只能让颁发者和使用者知道,尽可能使用非对称加密算法来颁发Token(比如RSA算法)。
  6. 如果担心重放攻击(replay attacks),可以尝试设置jti声明(JWT ID Claim),该声明将使得每个JWT token拥有唯一的ID,使其可以只使用一次,可以用来防止重放攻击。
  7. 在浏览器端,token可以存放到sessionStorage/localStorage,但注意XSS(跨站点脚本)攻击;还有一种方法是token放到了Cookie中,建议为其加上HttpOnly和Secure标记,HttpOnly保证JavaScript无法获取该Cookie,而Secure标记将保证该Cookie信息只能通过Https传输,但注意Cookie的方式可能会引起CSRF(跨站请求伪造)攻击。

演示项目

admin demo项目代码中有一一使用上述的JWT token颁发、校验、解码方法,

  1. 在前端项目admin-front中,通过jwt-decode解析JWT token来获取登录用户账号。
  2. 在前端项目admin-front中,在配置文件webpack.config.js中有提供/test/auth接口,通过jsonwebtoken颁发JWT token。
  3. 在后端项目admin-server中,在UserService中,通过com.auth0.jwt.JWT来颁发JWT token。
  4. 在后端项目admin-server中,在JwtFilter中,通过com.auth0.jwt.JWT来校验用户登录token。

具体可以获取项目代码查看。

admin demo代码仓库地址:https://gitee.com/pphh/simple-admin,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/simple-admin.git

参考资料

在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中。

参考资料

一个统计Git代码仓库签入代码行数的简单方法

本文介绍一个使用简单的脚本方法来统计各个开发者对一个git代码项目的提交代码行数,用于衡量各个开发者对于该项目的贡献量。

查看签入的提交代码行数

使用git命令行工具git log可以用于查看签入的各种信息,其中有个参数可以查看到每次签入的提交文件及其相关代码行数增减数目

git log --numstat --author="test"

一个输出的日志样例为,

Author: test <test@gmail.com>
Date: Tue Jan 5 01:20:49 2016 +0800

 add lombok feature.

10 1 pom.xml
5 16 src/main/java/com/peipeihh/spring/po/UserPO.java

在上述日志中,可以看到每次代码签入的改动文件,及其每个文件的代码改动量,其中第一个数字为代码增加量,第二个数字为代码减少量。有了这些日志,我们就可以使用awk工具统计下当前开发者总体增减行数量,脚本如下,

git log --numstat --author="test" | awk 'BEGIN{add=0;subs=0;loc=0} {if($1~/^[0-9]+/){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }'

其中:变量add统计的为代码增减量,subs统计的为代码减少量,然后将add和subs相加得到代码改动行数loc,这样就可以获取到一个开发者在某个代码项目中改动的代码行数。

统计每人签入的代码行数

在统计每人签入的代码行数之前,还需要获取所有开发者列表,

git log --format='%aN' | sort -u

接下来要做的事情,就是逐一统计每个开发者的签入代码行数,一个简单的脚本如下,

git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --numstat --author="$name" | awk 'BEGIN{add=0;subs=0;loc=0} {if($1~/^[0-9]+/){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }'; done;

一个输出结果样例为,

test 32469 22 32491

优化建议

为了让统计更加准确和有意义,可以做如下改进,

过滤合并的代码

一般情况下需要对合并的代码量过滤掉,不计入统计,这可以通过加上--no-merges参数,

git log --numstat --author="test" --no-merges
过滤提交代码量大于10000行的提交

有时候提交的代码中不免含有引入第三方代码库的时候,这个时候可以在统计的时候过滤掉代码量过于大的代码提交(>10000),

git log --numstat --author="test" | awk 'BEGIN{add=0;subs=0;loc=0} {if(($1~/^[0-9]+/) && ($1<10000)){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }';
按时间区段

如果需要统计某个时间区段,比如统计9月份的代码签入情况,可以加上--since/--until参数,

git log --numstat --author="test" --since=2016-9-01 --until=2016-9-30

后记

上面的统计方法可以简单的统计一个git代码仓库中每个开发者提交的代码行数,依次类推还可以统计文件数目、统计签入次数等等,还可以根据情况深入优化统计数据,这个就让读者自己根据上述资料来写个简单脚本了。

参考资料

Git log: https://git-scm.com/docs/git-log