基于Swoole的高性能系统监控及Nginx负载均衡的实现

索引

Part 1 服务监控
Part 2 日志落盘处理
Part 3 平滑重启
Part 4 负载均衡

监控是非常重要的,如果没有监控的情况下服务挂掉了,用户体验极差会丢失掉用户,在本文中我们会结合Linux+swoole+php来实现系统性能的监控。在一些方面性能也需要进行优化。优化可以从很多的角度处理,比如说底层代码,系统调优比如要加大核数或者是否借助es查mysql,或者不同地区用户走到哪个机房。

Part 1 服务监控

之前我们是直接使用命令行来开启server,如果服务挂掉了,我们需要使用服务进行通知。监控的方式是结合linux的命令

netstat -an 2>/dev/null| grep 8811 | grep LISTEN | wc -l

然后使用定时任务来执行,其实linux自带了crontab,但是执行粒度是每分钟,如果需要精确到秒就可以使用swoole的定时器。
server.php

<?php
/**
 * Created by bingxiong.
 * Date: 5/25/18
 * Time: 5:40 PM
 * Description: 监控服务
 */
 
class Server{
    const PORT = 8811;
 
    public function port(){
        $shell = "netstat -an 2>/dev/null| grep 8811 | grep LISTEN | wc -l";
        $result = shell_exec($shell);
        if($result != 1){
//            发送报警服务 邮件 短信
//            todo
            echo date("Ymd H:i:s")."error".PHP_EOL;
        }else{
            echo date("Ymd H:i:s")."success".PHP_EOL;
        }
    }
}
 
//(new Server()) -> port();
//这里使用swoole的定时器每两秒执行一次检测
swoole_timer_tick(2000, function ($timer_id){
    (new Server()) -> port();
    echo "time-start".PHP_EOL;
});

说明:

  • 本质上就是执行一个shell脚本来监控端口,通过脚本的返回值来判断端口运行情况然后使用了swoole把没两秒调用一次把数据打出来
  • 进一步的可以把数据使用重定向写入文件(也可以在shell脚本中进行重定向)
 /Users/bingxiong/swoole/php/bin/php /Users/bingxiong/swoole/hdtocs/thinkphp/script/monitor/server.php > /Users/bingxiong/swoole/hdtocs/thinkphp/script/monitor/a.txt
1.png

Part 2 日志落盘处理

对于日志落盘需要有一个特别要注意的地方就是落盘的时候回吧favicon也作为一次请求写入,需要将其在onRequest中设置为404。这里实现的日志落盘的access log其实在Nginx中是默认开启的,这里使用了swoole的异步写入来实现一个和Nginx的日志落盘差不多的功能。

通过日志的落盘,我们可以进行对日志进一步的分析从而找到系统瓶颈进行进一步的优化,一般大型的系统都会这样做。一般的做法是:加入有20台机器通过agent -> spark(计算)-> 然后放进数据库中 -> 用elasticsearch

ws.php

<?php
/**
 * Created by bingxiong.
 * Date: 5/19/18
 * Time: 7:43 PM
 * Description:
 */
class Ws{
    CONST HOST="0.0.0.0";
    CONST PORT=8811;
//    public $ws = null;
    CONST CHART_PORT =8812;
 
    public function __construct()
    {
//        重启的时候要获取sMembers获取看看key有没有值,有的话要删掉
        $this->ws = new swoole_websocket_server(self::HOST,self::PORT);
        $this->ws->listen(self::HOST,self::CHART_PORT,SWOOLE_SOCK_TCP);
        $this->ws->set([
            'enable_static_handler' => true, //
            'document_root' => "/Users/bingxiong/swoole/hdtocs/thinkphp/public/static",
            'worker_num' => 4,
            'task_worker_num' => 4,
        ]);
        $this->ws->on("workerstart",[$this,'onWorkerStart']);
        $this->ws->on("request",[$this,'onRequest']);
        $this->ws->on("open",[$this,'onOpen']);
        $this->ws->on("message",[$this,'onMessage']);
        $this->ws->on("task",[$this,'onTask']);
        $this->ws->on("finish",[$this,'onFinish']);
        $this->ws->on("close",[$this,'onClose']);
        $this->ws->start();
    }
 
    /**
     * onWorkerStart的回调
     * 加载框架文件
     * @param $server
     * @param $worker_id
     */
    public function onWorkerStart($server, $worker_id){
        // 定义应用目录
        define('APP_PATH', __DIR__ . '/../../../application/');
        // 加载框架引导文件
        require __DIR__ . '/../../../thinkphp/start.php';
    }
 
    /**
     * request的回调
     * @param $request
     * @param $response
     */
    public function onRequest($request,$response){
//        print_r($request->server);
//        把图标状态设置成404,防止日志中输出
        if($request -> server['request_uri'] == '/favicon.ico'){
            $response->status(404);
            $response->end();
            return;
        }
 
        //    将swoole中一些特别的用法装换成原生的php
        $_SERVER =[];
        if(isset($request->server)){
            foreach ($request->server as $k => $v){
                $_SERVER[strtoupper($k)] = $v;
            }
        }
 
        $_GET = [];
        if(isset($request->get)){
            foreach ($request->get as $k => $v){
                $_GET[$k] = $v;
            }
        }
 
        $_FILES = [];
        if(isset($request->files)){
            foreach ($request->files as $k => $v){
                $_FILES[$k] = $v;
            }
        }
 
        $_POST = [];
        if(isset($request -> post)){
            foreach ($request->server as $k => $v){
                $_POST[$k] = $v;
            }
        }
 
//        日志落盘
        $this->writeLog();
 
        $_POST['http_server'] = $this->ws;
//    执行框架中的内容
        ob_start();
        try {
            think\Container::get('app', [APP_PATH])
                ->run()
                ->send();
        }catch (\Exception $e){
            // todo
        }
        $res = ob_get_contents();
        ob_end_clean();
        $response->end($res);
    }
 
    /**
     * @param $serv
     * @param $taskId
     * @param $workerId
     * @param $data
     * @return string
     */
    public function onTask($serv,$taskId,$workerId,$data){
//        分发task任务机制,让不同的任务走不通的逻辑
        $obj = new app\common\lib\task\Task;
        $method = $data['method'];
        $flag = $obj -> $method($data['data'],$serv);
 
        return $flag; // 告诉worker进程
    }
 
    /**
     * @param $serv
     * @param $taskId
     * @param $data
     */
    public function onFinish($serv,$taskId,$data){
        echo "taskId:{$taskId}\n";
        echo "finish-data-success:{$data}\n";
    }
 
    /**
     * 监听ws打开事件
     * @param $ws
     * @param $request
     */
    public function onOpen($ws,$request){
//        print_r($ws);
        \app\common\lib\redis\Predis::getInstance()->sAdd(config('redis.live_game_key'),$request->fd);
        var_dump($request->fd);
    }
 
    /**
     * 监听ws消息事件
     * @param $ws
     * @param $frame
     */
    public function onMessage($ws,$frame){
        echo "server-push-message:{$frame->data}\n";
        $ws->push($frame->fd,"server-push:".date("Y-m-d H:i:s"));
    }
 
    /**
     * 监听关闭事件
     * @param $ws
     * @param $fd
     */
    public function onClose($ws,$fd){
//        fd 删除
        \app\common\lib\redis\Predis::getInstance()->sRem(config('redis.live_game_key'),$fd);
        echo "clientId:{$fd}\n";
    }
 
    public function writeLog(){
//        获取请求信息
        $data = array_merge(['data'=>date("Ymd H:i:s")],$_GET,$_POST,$_SERVER);
//        遍历写入信息,因为获取到信息是索引数组
        $logs = "";
        foreach ($data as $key => $value){
            $logs .= $key.":".$value." ";
        }
        swoole_async_writefile(APP_PATH.'../runtime/log/'.date("Ym")."/".date("d")."_access.log", $logs.PHP_EOL,function ($filename){
//            todo
        },FILE_APPEND);
    }
}
 
// 直接new来开启服务
new Ws();

说明:

  • 使用了swoole异步文件写入来实现高性能日志的写入
  • 在onRequest中进行日志的写入,其实就是每次连接成功的时候就获取这次请求的超全局变量信息_GET,_POST, $_SERVER然后写入
  • 需要注意favicon会被视为一次请求,因此需要在onRequest中把这个请求视为404,查看这个请求的方法是打印request -> server然后找到这个请求的索引是request_uri然后局把它的状态设置为404.

Part 3 平滑重启

平滑重启是当服务器上有了代码的修改,我们无需重启服务就来更新文件,在swoole中内置了几个信号源:

  • sigterm:用于停止服务器
  • sigsr1:用于重启worker进程
  • sigsr2:用于重启task worker进程
    我们框架中的代码是放在onWorkerStart中的,我们需要在启动server,监听所有TCP/UDP端口的onStart方法中设置进程名,然后调用shell脚本,具体的步骤是:
    添加onStart方法
$this->ws->on("start",[$this,'onStart']);

在onStart方法中设置进程名,这是为了shell脚本找到进程使用的

    public function onStart($server){
//        设置主进程的进程名
        swoole_set_process_name("live_master");
    }

执行shell脚本,通过刚才设置的进程名找到对应的进程

echo "loading...";
pid=`pidof live_master`
echo $pid
kill -USR1 $pid
echo "loading success"

Part 4 负载均衡

2.png

之前的代码中是直接走到swoole的http服务器,静态资源和php都是直接走到swoole。其实在实际的工作中这样的架构是不好的,swoole建立应用层的服务是ok的,但是建议在上层增加Nginx服务器,静态资源放在Nginx中是非常合适的,可以设置静态资源的失效时间,比如说缓存住静态资源。可以使用Nginx做一个转发,把用于请求转发到swoole的http server里面去,即用户请求的时候先看有没有缓存的静态资源,如果有的话那么就直接去swoole的http server的php,如果没有就把静态资源转发到swoole的http server和php一起返回,这样效果就好得多。

更加进一步的好处是可以使用Nginx做一个负载均衡,使用多个swoole的机器。注意,集群的每台机器的php代码必须是一致的啊。

实际上在大型的工程中,集群是这样的:


3.png

即对集群进行拆分,以加快访问,使用就近原则找到最近的集群然后下发。

安装需要注意配置日志等等:

./configure 
--prefix=/Users/bingxiong/swoole/nginx 
--sbin-path=/Users/bingxiong/swoole/nginx 
--conf-path=/Users/bingxiong/swoole/nginx/config/nginx.conf 
--error-log-path=/Users/bingxiong/swoole/nginx/logs/error.log 
--pid-path=/Users/bingxiong/swoole/nginx/logs/nginx.pid 
--http-log-path=/Users/bingxiong/swoole/nginx/logs/access.log

在Nginx.conf中开启

  • pid
  • error_log notice
  • log_format
  • access_log
  • 更改监听端口 8823
    启动并查看这个端口
./sbin/nginx
netstat -an | grep 8823

内网ip:

hostname -i

访问ip:8823即可访问Nginx的欢迎界面

访问静态页面

之前的静态页面在:/Users/bingxiong/swoole/hdtocs/thinkphp/public/static目录,其实访问静态页面也是改配置在Nginx.conf中的root中绑定这个目录,然后重启Nginx,用一个比较笨的方法就是先获取这个端口的pid然后kill掉再开启。其实重启的方式完全可以结合swoole平滑重启的方式来进行。

这样就可以使用Nginx通过ip:端口:文件来访问了。

Nginx负载,转发到Swoole服务器

4.png

通过访问一个地址,先走到Nginx的这个地址比如一个html文件存在,那么Nginx直接返回,如果不存在会让它转发到Swoole服务器走到swoole的php逻辑最终返回给Nginx然后返回给用户。实现这个功能其实是一个Nginx的配置。

转发实际上要考虑两种场景因为有两种访问模式:

?s=xxx/xxx/xxx
xxx/xxx/xxx
就在server模块的location里面进行配置,通过一个判断来做,先去找这个地址,看看有没有,如果没有就转到8811端口的swoole服务器:

if (!-e $request_filename){
    proxy_pass http://127.0.0.1:8811;
}

注意,这里要是转发到另外一台机器的话需要把另一台机器的外网ip写进来即可(原则上是Nginx一台机器,swoole是另一台机器),这里是在内网里面。另外一个要注意的地方就是if后面有空格然后重启一下Nginx。现在实现的其实是这样的架构,Nginx一台机器,swoole是另一台机器(外网ip访问)。

负载均衡的实现

5.png

swoole服务器肯定是一个分布式集群,能够分流减少每台服务器的压力。做负载均衡有两步,在Nginx配置中的server模块上面,相当于做一些转发:

这是通过权重的策略,这里的权重就是转发到第一台的概率比下面的这个机器概率大一倍,注意这里的=不能有空格。

upstream swoole_http{
     server 机器的ip:端口 weight=2;
     server 机器的ip:端口 weight=1;
}

也可以通过hash的策略,这是通过客户端的ip来绑定机器的策略,是根据用户的hash结果来分配的,就会说一个客户访问一个server之后都是一个。

upstream swoole_http{
     server 机器的ip:端口;
     server 机器的ip:端口;
}

策略一共有五种,这里只介绍了两种,

然后来到location中之前转发的地址:

if (!-e $request_filename){
    proxy_pass http://swoole_http;
}

注意,这里的swoole_http是之前的upstream的名字。

测试的话可以给页面不同的值,然后多次访问(权重的方法)就可以看到返回的结果不同,因为访问的是不同的机器。注意Nginx必须是单台来做转发,Nginx其实就是做转发,转发是很耗CPU的,因此要注意购买CPU性能高的服务器来进行Nginx任务的分发。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容