ILD

ILD架构设计与实现
作者:Herbert Yuan 邮箱:yuanjp89@163.com
发布时间:2017-5-16 站点:Inside Linux Development

1. 前言

一直想有一个自己技术博客,激励自己每天去学习,终于终于下定决心做了(距写这篇博客好像有2个多月了)。既然是程序员,就决定自己做一个简单的网站,不求外观漂亮,但求基本功能。其实最开始规划的功能还是挺全的,包括编辑、评论、评分及mailing list功能等。考虑的方案最开始是纯php后台,文章编辑和存储都用php读写文件的方式。但是考虑mailing list用Perl做,就有两套代码,所以考虑php + Perl的方式,php做用户交互后台,Perl做数据后台,Perl做后台还有一个好处,可以缓存一些数据到内存,提高性能。于是花了2个星期学习了Perl脚本。Perl脚本非常优雅,学起来有点难度。后来考虑到复杂性,把评论和mailing list给去掉了。

2. Perl数据后台

    主要包括数据文件存储、数据内存索引、交互接口等几个部分。

2.1 数据存储设计

共有3种存储类型:已删除、草稿和文章。建立3个目录存储,在db目录下,分别为deleted/draft/paper。每篇文章又存储3种类型的数据:索引、附件和文章。存储在对应的文件中,索引文件为id.index,文章为id.paper,附件为id.attach_id.attach。附件存储附件的二进制内容,文章存储文章的html格式内容。索引存储文章的相关元数据,用键值对表示,在Perl中可以很方便的用Hash数据结果存储,如下:

cat /test

email xxx@163.com

abstract xxx

modtime 1494939529

title xxx

paper_pid a001rp

author xxx

2.2 数据索引与操作设计

这里简单介绍一下,索引驻留在内存,设计的数据结构如下:

1
2
3
4
my @draft_list;
my %cat_index;
my @cats;
my @allpapers;

每一篇草稿都是一个Hash变量,存储2.1中表示的键值对。草稿Hash变量和索引文件保持同步。草稿的哈希引用存储到@draft_list中,用于索引所有的草稿,草稿id从0开始连续编号。

@allpapers数组存储所有已发布文章的Hash变量的引用。数组的下标即是文章的id。同时文章有一个 pseudo id,用户看到的是这个id,其格式如:a004j0。前4位为id值,a004的id为4,后两位为随机盐值,在发布文章的时候生成。

@cats存储所有的分类,按照分类排序,每发布一篇文章,都将该文章的分类更新到@cats数组中。

%cat_index哈希变量用于按分类索引文章,每个键表示一个分类,每个值是该分类的所有文章的数组的索引。

Perl脚本启动过程中也会从文件中,加载文章到上述数据结构。

2.3 交互接口设计

采用unix socket的方式和php交互数据。Perl后台ildd循环接收web server的请求并响应数据给它,如下:

域套接字的类型选择stream还是datagram,前者对消息的长度没有限制,但是需要解析消息边界。后者的缺点是Linux内核对长度有限制,但是这个长度已经很大了,有几十上百KB,关于datagram的长度可以参考下述链接:

What is the max size of AF_UNIX datagram message that can be sent in linux?

最终考虑选择datagram,对于超长数据的传输可以采用及其它机制,如php将文章存储到临时文件,将文件名传输到ildd进行处理。

请求消息的格式为:cmd \n params1 \n params2 ...。cmd表示请求命令类型,params为键值对类型的参数,键值之间用空格分开,命令类型和各参数之间用\n分开,所以这里要求参数的值中不能有\n字符。

响应消息的格式为:code desc \n params1 \n params2 \n \n params3 \n ... 。code表示响应结果,如200表示ok,300表示错误。响应的数据也是键值对形式,可以响应多组数据,用\n\n分开。如查询某一分类的文章时,可能响应多组数据。

2.4 Daemonize与init脚本

Perl后台以守护进程的方式运行,和通常的守护进程处理方式类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sub daemonize
{
    $subpid fork;
    die "can't fork: $!\n" if not defined $subpid;
    if ($subpid) {
        if (defined $pidfile) {
            if (open my $fd">"$pidfile) {
                print $fd $subpid;
                close($fd);
            }
        }
        exit 0;
    }
    POSIX::setsid();
    open STDIN , '<''/dev/null';
    if (defined $logfile) {
        open STDOUT, '>>'$logfile;
        open STDERR, '>>'$logfile;
    else {
        open STDOUT , '>''/dev/null';
        open STDERR , '>''/dev/null';
    }
}

相关参数为-d,以守护进程方式运行。--pidfile file,将进程id存储到file。--logfile file输出log信息到file。

添加init脚本,并添加到rc.d中。

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
ILDD_DIR="/run/ildd"
ILDD_CLIENT_DIR="/run/ildd/client"
PHP_TMP_DIR="/run/php"
DRAFT_TMP_DIR="${PHP_TMP_DIR}/draft"
STAT_TMP_DIR="${PHP_TMP_DIR}/stat"
 
DAEMON=/work/insidelinuxdev.net/perl/ildd.pl
PIDFILE=${ILDD_DIR}/ildd.pid
LOGFILE=${ILDD_DIR}/ildd.log
 
# check bin exists and executable
[ -x $DAEMON ] || {
    echo $DAEMON is not executable
    exit 1
}
 
prep_env ()
{
    if [ ! -d $ILDD_DIR ] ; then
        echo mkdir $ILDD_DIR
        mkdir $ILDD_DIR
    fi
 
    if [ ! -d $ILDD_CLIENT_DIR ] ; then
        echo mkdir $ILDD_CLIENT_DIR
        mkdir $ILDD_CLIENT_DIR
        chown -R www-data:www-data $ILDD_CLIENT_DIR
    fi
 
    if [ ! -d $DRAFT_TMP_DIR ] ; then
        mkdir $DRAFT_TMP_DIR
        chown -R www-data:www-data $DRAFT_TMP_DIR
    fi
 
    if [ ! -d $STAT_TMP_DIR ] ; then
        mkdir $STAT_TMP_DIR
        chown -R www-data:www-data $STAT_TMP_DIR
    fi
}
 
 
do_start ()
{
    if [ -f $PIDFILE ] ; then
        echo "error: ildd is running" >&2
        exit 1;
    fi
 
    # mk dirs
    prep_env
 
    # start daemon
    $DAEMON -d --pid $PIDFILE --log $LOGFILE
}
 
do_stop()
{
    if [ -f $PIDFILE ] ; then
        kill -3 `cat $PIDFILE `
        rm $PIDFILE
    else
        echo "error: ildd is not running" >&2
        exit 1;       
    fi
}
 
case "$1" in
    start)
        do_start
    ;;
    stop)
        do_stop
    ;;
    restart)
        do_stop
        sleep 1
        do_start
    ;;
    *)
        echo "Usage: /etc/init.d/ildd {start|stop}" >&2
        exit 3
    ;;
esac

上述init脚本有点缺陷,pidfile是在该脚本删除的,同时没有检测进程的运行状态,对于restart操作,stop之后1s再执行start。编写脚本,用于安装上述启动脚本,如下:

3 php后台设计

web请求的数据,通过php转发ildd,同时读取ildd的响应,发给web client。php相当于一个中转和封装中心。

3.1 ildc类

封装一个ildc类,用于建立连接、发送请求,读取响应等。建立Unix套接字连接接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function connect() 
{
    $sock = socket_create(AF_UNIX, SOCK_DGRAM, 0);
    if ($sock == false) {
        return false;
    }
    $uniqid $_SERVER['UNIQUE_ID'];
    if ($uniqid == "") {
        socket_close($sock);
        return false;
    }
    $local_path = ILD_CLIENT_PATH_PREFIX.$uniqid;
    if (!socket_bind($sock$local_path)) {
        socket_close($sock);
        return false;
    }
    $this->sock = $sock;
    $this->local_path = $local_path;
    return true;
}

这里使用了$_SERVER['UNIQUE_ID'],来确保不同请求的unix socket addr不会冲突,需要开启Aapche2的unique_id模块来支持。发送请求、接收响应并解析数据接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function req($cmd, &$res_arr)
{
    $len strlen($cmd);
    if ($len <= 0)
        return false;
 
    $ret = socket_sendto($this->sock, $cmdstrlen($cmd), 0, ILD_SERVER_PATH);
    if ($ret <= 0)
        return false;
 
    $res = socket_recvfrom($this->sock, $buf, ILD_MSG_LEN, 0, $path);
    if ($res === false)
        return false;
 
    $res_arr = ildc::idlc_explode($buf);
    if (count($res_arr) < 1) {
        return array();
    }
 
    $res explode(" "$res_arr[0], 2);
    array_shift($res_arr);
 
    return $res;
}

还有其它一些辅助函数,这里不介绍。

3.2 cgi接口

cgi.php用于封装所有的命令,如读取分类列表,编辑文章等。它调用ildc类与ildd交互数据。cgi接口同时支持POST和GET两种请求。对于编辑文章等使用POST请求,下载附件等使用GET请求。首先有两个命令数组,表示支持哪些命令,携带哪些参数,参数是强制还是可选,如支持的GET命令如下:

1
2
3
4
5
$get_cmds array (
    "draft_down_attach" => array("draft_id""_attach_id"),
    "draft_get_paper" => array("draft_id"),
    "paper_get_attach" => array("paper_pid""_attach_id"),
);

还有一个权限数组,哪些操作需要权限,如编辑相关的操作,均需要管理员权限。阅读文章等操作,则没有权限要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
$rights array (
    "draft_get" => 1,
    "draft_save" => 1,
    "draft_del" => 1,
    "draft_up_attach" => 1,
    "draft_del_attach" => 1,
    "draft_down_attach" => 1,
    "draft_get_paper" => 1,
     
    "publish" => 1,
    "paper_del" => 1,
    "paper_edit" => 1,
);

执行ildc接口,并响应数据给web client。响应,对于POST请求,响应json格式。对于GET请求,响应plain text格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ildc new ildc();
if (!$ildc->connect()) 
{
    res_func(600, "can't connect to ildd", null);
    exit;
}
 
$res $ildc->$op($ild_data$res_data);
if ($res === false || count($res) < 1){
    res_func(601, "interval error", null);  
else {
    res_func($res[0], $res[1], $res_data);
}
 
$ildc->close();

3.3 重定向

重定向用于实现伪静态,重定向规则如下

1
2
3
4
5
6
7
8
9
<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteCond %{HTTP_HOST} ^www.insidelinuxdev.net [NC]
    RewriteRule ^/?(.*)$ http://insidelinuxdev.net/$1 [L,R=301]
 
    RewriteRule ^/?article/([a-z0-9]+).html$ /read.php?paper_pid=$1 [NC]
    RewriteRule ^/?cat((?:/[^/]+)*)/([0-9]+).html$ /list.php?cat=$1&p=$2 [NC]
    RewriteRule ^/?attachment/([a-z0-9]+)/([0-9]+)/?$ /cgi.php?op=paper_get_attach&paper_pid=$1&_attach_id=$2
</IfModule>

实现下述URL:

www.insidelinuxdev.net重定向到insidelinuxdev.net

文章页面:/article/a004jq.html

分类页面:/cat/project/1.html

附件页面:/attachment/a004jq/0

3.4 其它页面

包括登录页面、编辑页面、首页、文章列表、文章阅读页面等。编辑器原先使用wangEditor,后来切换为ueditor。图片以base64存储到html中,对于文章阅读页面,所有的资源都嵌入到该页面,即为单一页面,方便用户离线存储,同时排版也考虑便于打印。

4 Performance Test

由于以数组的方式组合所有文章、草稿、分类索引,以id读取数组元素,所以读复杂度为O(1),读包括文章按分类分页,读取文章。发布新文章,插入到文章的结尾,复杂度也为O(1)。但是编辑已发布文章,需要删除旧文章,再插入(按文章原id排序),这里涉及移位,所以复杂度为O(n)。

4.1 发布文章性能

使用页面js脚本循环发布文章,性能大概在30篇/s,这里性能限制恐怕是页面请求交互过程,考虑到发布文章是离散事件,所以30篇/s完全能接受。

4.2 首页并发性能

使用ab测试,结果在3000 Requests per second左右。ildd占11%的cpu资源,其余负载在众多apache进程。

4.3 分类索引页面

分别测试第1页,第20页和最后一页。结果如下,看以看出并发差不多,也证明其复杂度为O(1)。

 page 20 46
 Requests per second 2144.45 2010.34 2036.65

可以看出,ildd的负载为22.7%,比首页重。分页时,需要检索10篇文章,读取文章的标题、摘要等信息,所以负载会重一些。

4.4 文章阅读页

由于文章存储到硬盘,没有被ildd缓存,所以涉及磁盘I/O,性能必然下降。测试结果为1708.59 Requests per second。性能有些许下降,测试的文章为40KB,是不是特别大。而且内核也有I/O缓存。所以下降不是特别多。ildd 15%,ksoftirqd/1 7.6%,其余为apache进程。由于涉及磁盘I/O,所以软中断明显吃掉一些CPU。

注意:单实例ab测试,可能并不能反映真实的并发能力。

Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.