索引
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
/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
Part 2 日志落盘处理
对于日志落盘需要有一个特别要注意的地方就是落盘的时候回吧favicon也作为一次请求写入,需要将其在onRequest中设置为404。这里实现的日志落盘的access log其实在Nginx中是默认开启的,这里使用了swoole的异步写入来实现一个和Nginx的日志落盘差不多的功能。
通过日志的落盘,我们可以进行对日志进一步的分析从而找到系统瓶颈进行进一步的优化,一般大型的系统都会这样做。一般的做法是:加入有20台机器通过agent -> spark(计算)-> 然后放进数据库中 -> 用elasticsearch
ws.php
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中进行日志的写入,其实就是每次连接成功的时候就获取这次请求的超全局变量信息_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 负载均衡
之前的代码中是直接走到swoole的http服务器,静态资源和php都是直接走到swoole。其实在实际的工作中这样的架构是不好的,swoole建立应用层的服务是ok的,但是建议在上层增加Nginx服务器,静态资源放在Nginx中是非常合适的,可以设置静态资源的失效时间,比如说缓存住静态资源。可以使用Nginx做一个转发,把用于请求转发到swoole的http server里面去,即用户请求的时候先看有没有缓存的静态资源,如果有的话那么就直接去swoole的http server的php,如果没有就把静态资源转发到swoole的http server和php一起返回,这样效果就好得多。
更加进一步的好处是可以使用Nginx做一个负载均衡,使用多个swoole的机器。注意,集群的每台机器的php代码必须是一致的啊。
实际上在大型的工程中,集群是这样的:
即对集群进行拆分,以加快访问,使用就近原则找到最近的集群然后下发。
安装需要注意配置日志等等:
./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服务器
通过访问一个地址,先走到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访问)。
负载均衡的实现
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任务的分发。