域名交易说明

本人持有的所有域名,全部隐性跳转至本页!!!

如果你想购买正在访问的域名,可以通过以下联系方式我商谈价格:

QQ:979137
微信:979137
邮箱:979137@qq.com
博客:http://979137.com

支持交易平台:ename.com 22.cn 4.cn
支持交易方式:代价push 或 中介

非常勿扰!感谢!

MySQL位运算的应用

我们知道,PHP当中的错误级别常量,是根据二进制位特性而确定的一个个整数,可以简单的通过位运算定制PHP的错误报告。我们也可以将其应用到 MySQL 当中!

你是否遇到过下面这样的情况?

一、用户可能有若干不同属性或不同状态,然后你可能会在数据表中通过一个个字段去标记和实现,比如:

id int(11) 用户ID(自增)
username char(50) 用户名
password char(50) 密码
confirm_info tinyint(1) 是否确认资料 [ 0/1 ]
confirm_contract tinyint(1) 是否确认协议 [ 0/1 ]
freeze tinyint(1) 冻结状态 [ 0/1 ]
payer tinyint(1) 笔记是否付费用户 [ 0/1 ]
high_quality tinyint(1) 标记是否优质用户 [ 0/1 ]

二、用户拥有多个用户组(角色),常见的实现方法有:

1、增加一个字段,专门存它的用户组,用逗号隔开

id int(11) 用户ID(自增)
username char(50) 用户名
password char(50) 密码
confirm_info tinyint(1) 是否确认资料 [ 0/1 ]
confirm_contract tinyint(1) 是否确认协议 [ 0/1 ]
freeze tinyint(1) 冻结状态 [ 0/1 ]
payer tinyint(1) 笔记是否付费用户 [ 0/1 ]
high_quality tinyint(1) 标记是否优质用户 [ 0/1 ]
role_id varchar(255) 如:1,3,11

使用 FIND_IN_SET() 或 LIKE 进行数据查询。这种存储方式,虽然比较直观,结构也比较接单,但是,但是查询和维护数据都不方便

2、增加一个关联表

user_id int(11) 用户ID
role_id int(11) 角色ID

通过JOIN,连表查询。这种存储方式,似乎很好扩展,无论是增加了用户组,还是重新给用户划分用户组,直接操作这张关联表就行。但是,连表查询终究是需要操作多张表的,效率肯定不如单表查询,而且如果用户属性多了(不仅限于分组),那就得关联更多的表或者查多次,使查询变得更加复杂!


我们知道,PHP当中的错误级别常量,是根据二进制位特性而确定的一个个整数,可以简单的通过位运算定制PHP的错误报告。我们也可以将其应用到 MySQL 当中,还是以上面的用户表为例,我们只用一字段 status,专门记录用户状态,其结构如下:

id int(11) 用户ID(自增)
username char(50) 用户名
password char(50) 密码
status int(11) 用户状态位

然后我们给每一种状态设定一个对应的值,这个值需要使用2的N次方来表示,比如:

1 已确认资料
2 已确认协议
4 已冻结
8 付费用户
16 优质用户

查询技巧示例:

1、查询所有已冻结的用户:

SELECT * FROM `user` WHERE `status` & 4 = 4

2、查询所有未确认协议的用户

SELECT * FROM `user` WHERE `status` & 2 = 0

3、设置ID等于186的用户为优质用户

UPDATE `user` SET `status` = `status` | 16 WHERE `id` = 186

4、取消ID等于186的优质用户身份

PDATE `user` SET `status` = `status` ^ 16 WHERE `id` = 186

5、查询已冻结的优质付费用户(4 + 8 + 16 = 28)

SELECT * FROM `user` WHERE `status` & 28 = 28

同理,上述应用场景二中的分组问题,也可以使用同样的方法,优化表结构!然后使用位运算,实现各种条件的查询!

Apache 用户认证配置之 Basic 认证

很多时候我们可能需要对服务器资源进行保护,通常的做法是在应用层通过鉴权来实现,如果你嫌自己去实现鉴权太麻烦,那就直接让Apache去帮你实现吧!
Apache常见的用户认证可以分为下面三种:

  • 基于IP,子网的访问控制(ACL)
  • 基本用户验证(Basic Authentication)
  • 消息摘要式身份验证(Digest Authentication)

基于IP的访问控制可以通过配置 Allow From实现!这里不多讲。
一般的,我们还会在IP的基础上,再增加一层 Basic Authentication,实现一个基本的服务器用户认证!

1、生成用户名密码文件

/usr/local/apache2/bin/htpasswd -bc users.pwd test hehe1234

Adding password for user test

/usr/local/apache2/bin/htpasswd -b users.pwd test2 hehe4321

Adding password for user test2

cat users.pwd

test:$apr1$4R3foyQ5$1KGHVA5HQL8M9b0K/2UWO0
test2:$apr1$pKLy86CD$W9hFUvs4F06OBXtQhCbPV/

可以看到用户名密码文件已经生成了,一行一个!

2、配置 VirtualHost,如:

<VirtualHost *:80>
    DocumentRoot /usr/local/www/pma/
    DirectoryIndex index.php index.html index.shtml
    ServerName pma.979137.com
    CustomLog "logs/pma.979137.com-access_log" common
    ErrorLog "logs/pma.979137.com-error_log"
    <Directory /usr/local/www/pma/>
        Options Includes FollowSymLinks
        AllowOverride AuthConfig
        AuthName "PMA Contents." 
        AuthType basic
        AuthUserFile /usr/local/apache/conf/users.pwd 
        Require valid-user
    </Directory>
</VirtualHost>
    • AllowOverride 表示通过配置文件进行身份验证
    • AuthName 发送给客户端报文头内容:WWW-Authenticate
    • AuthType 认证类型
    • AuthUserFile 这个就是刚刚生成的用户名密码文件
    • Require 指定哪些用户或组才能被授权访问。如:
      • require user test test2(只有用户 test 和 test2 可以访问)
      • requires groups managers (只有组 managers 中成员可以访问)
      • require valid-user (在 AuthUserFile 指定的文件中任何用户都可以访问)

我们来看一下效果:

在浏览器访问:[table=50%]

[attach]121[/attach]

cURL请求:

curl -v http://pma.979137.com/test.php

* Trying 10.223.28.1…
* Connected to pma.979137.com (10.223.28.1) port 80 (#0)
> GET /test.php HTTP/1.1
> Host: pma.979137.com
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 401 Authorization Required < Date: Fri, 06 Jan 2017 07:02:15 GMT < Server: Apache/2.2.27 (Unix) PHP/5.3.29 < WWW-Authenticate: Basic realm=" PMA Contents." < Content-Length: 490 < Content-Type: text/html; charset=iso-8859-1 < 401 Authorization Required

Authorization Required

This server could not verify that you
are authorized to access the document
requested. Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn’t understand how to supply
the credentials required.

在没有携带用户名和密码时,HTTP Code 返回了 401,并输出了 Authorization Required!
表示需要该请求需要进行认证!

我们再来看下,携带密码请求:

curl -v -u 'test:hehe1234' 'http://pma.979137.com/test.php'

* Trying 10.223.28.1…
* Connected to pma.979137.com (10.223.28.1) port 80 (#0)
* Server auth using Basic with user ‘test’
> GET /test.php HTTP/1.1
> Host: pma.979137.com
> Authorization: Basic c2hpbGlhbmd4aWU6YWl5aTEzMTQ=
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK < Date: Fri, 06 Jan 2017 07:14:14 GMT < Server: Apache/2.2.27 (Unix) PHP/5.3.29 < X-Powered-By: PHP/5.3.29 < Content-Length: 25 < Content-Type: text/html < string(11) "hello word!"

HTTP Code 已经是 200 了,并且返回了正确的内容!
至此,一个简单的 Basic 认证就OK了!这种认证一般可用于浏览器访问,也可以用于 API 认证!

Apache按天分割日志并删除指定几天前的日志

使用命令rotatelogs 对日志进行切割

先找到你的 rotatelogs 路径

[root@TENCENT64 ~]# locate rotatelogs
/usr/local/apache2/bin/rotatelogs

在你的 VirtualHost 模块添加或替换

<VirtualHost *:80>
    ......
    ErrorLog "| /usr/local/apache2/bin/rotatelogs /data/logs/apache/979137.com-error_log-%Y%m%d 86400 480"
    CustomLog "| /usr/local/apache2/bin/rotatelogs /data/logs/apache/979137.com-access_log-%Y%m%d 86400 480" common
</VirtualHost>

指定分割时间:86400 默认单位为s。也就是24小时
指定分区时差:480 默认单位m,也就是8小时

清理日志脚本:

#!/bin/bash

LOG_PATH_APACHE="/data/logs/apache/"
DAYS_AGO=`date -d "-7 day" +%Y%m%d`

\rm -rf ${LOG_PATH_APACHE}*log-${DAYS_AGO}

加入到crontab,每天执行一次即可

0 4 * * * /bin/bash /data/cron/clear_logs.sh > /dev/null 2>&1

必不可少的 PHP Monitor:运行错误监控

所有运行在线网环境的程序应该都被监控,对线网而言,无论是 Fatal error、E_NOTICE 还是 E_STRICT,都应该被消灭。对开发者而言,线网发生任何异常或潜在bug时,应该第一时间修复、优化,而不是等反馈之后才知道,然后临时去排查问题!

[font=微软雅黑][size=4][b]这篇文章要讲啥?[/b][/size][/font][list=1]
[*]了解PHP错误机制
[*]注册PHP的异常处理函数、错误处理函数、脚本退出函数
[*]配置PHP预加载(重要)
[*]搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)
[*]基于 ElasticSearch RESTful 实现告警
[/list]
[font=微软雅黑][size=4][b]要达到的目的?[/b][/size][/font]
所有PHP程序里的错误或潜在错误(支持自定义)都全部写入日志并作警,包含错误的所在文件名、行号、函数(如果有)、错误信息等等
PHP程序不需要做任何修改或接入!

[font=微软雅黑][size=5][b]一、了解PHP错误机制[/b][/size][/font]

截止PHP7.1,PHP共有16个错误级别:
[url]http://php.net/manual/zh/errorfunc.constants.php[/url]

如果你对PHP错误机制还不太了解,网上有很多关于PHP错误机制的总结,因为这个不是文章的重点,这里不再详细介绍!

[font=微软雅黑][size=5][b]二、注册PHP的异常处理函数、错误处理函数、脚本退出函数[/b][/size][/font]

PHP有三个很重要的注册回调函数的函数[list]
[*]register_shutdown_function 注册PHP退出时的回调函数
[*]set_error_handler 注册错误处理函数
[*]set_exception_handler 注册异常处理函数
[/list]
我们先定义一个日志处理类,专门用于写日志(也可以用于用户日志哦)

<?php
/**
 * 日志接口
 * @filename Loger.php
 * @since 2016-12-08 12:13:50
 * @author 979137.com
 * @version $Id$
 */
class Loger {

    // 日志目录,建议独立于Web目录,这个目录将作用于 Logstash 日志收集
    protected static $log_path = '/data/logs/';

    // 日志类型
    protected static $type = array('ERROR', 'INFO', 'WARN', 'CRITICAL');

    /**
     * 写入日志信息
     * @param mixed  $_msg  调试信息
     * @param string $_type 信息类型
     * @param string $file_prefix 日志文件名默认取当前日期,可以通过文件名前缀区分不同的业务
     * @param array  $trace TRACE信息,如果为空,则会从debug_backtrace里获取
     * @return bool
     */
    public static function write($_msg, $_type = 'info', $file_prefix = '', $trace = array()) {
        $_type = strtoupper($_type);
        $_msg  = is_string($_msg) ? $_msg : var_export($_msg, true);
        if (!in_array($_type, self::$type)) {
            return false;
        }
        $server = isset($_SERVER&#91;'SERVER_ADDR'&#93;) ? $_SERVER&#91;'SERVER_ADDR'&#93; : '0.0.0.0';
        $remote = isset($_SERVER&#91;'REMOTE_ADDR'&#93;) ? $_SERVER&#91;'REMOTE_ADDR'&#93; : '0.0.0.0';
        $method = isset($_SERVER&#91;'REQUEST_METHOD'&#93;) ? $_SERVER&#91;'REQUEST_METHOD'&#93; : 'CLI';
        if (!is_array($trace) || empty($trace)) {
            $dtrace = debug_backtrace();
            $trace  = $dtrace&#91;0&#93;;
            if (count($dtrace) == 1) {
                //不是在类或函数内调用
                $trace&#91;'function'&#93; = '';
            } else {
                if ($dtrace&#91;1&#93;&#91;'function'&#93; == '__callStatic') {
                    $trace&#91;'file'&#93; = $dtrace&#91;2&#93;&#91;'file'&#93;;
                    $trace&#91;'line'&#93; = $dtrace&#91;2&#93;&#91;'line'&#93;;
                    $trace&#91;'function'&#93; = empty($dtrace&#91;3&#93;&#91;'function'&#93;) ? '' : $dtrace&#91;3&#93;&#91;'function'&#93;;
                } else {
                    $trace&#91;'function'&#93; = $dtrace&#91;1&#93;&#91;'function'&#93;;
                }
            }
        }
        $ace = $trace;
        $now = date('Y-m-d H:i:s');
        $pre = "&#91;{$now}&#93;&#91;%s&#93;&#91;{$ace&#91;'file'&#93;}&#93;&#91;{$ace&#91;'line'&#93;}&#93;&#91;{$ace&#91;'function'&#93;}&#93;&#91;{$remote}&#93;&#91;{$method}&#93;&#91;{$server}&#93;%s";
        $msg = sprintf($pre, $_type, $_msg);

        $filename = 'phplog_' . ($file_prefix ?: 'netbar') . '_' . date('Ymd') . '.log';
        $destination = self::$log_path . $filename;
        is_dir(self::$log_path) || mkdir(self::$log_path, 0777, true);
        //文件不存在,则创建文件并加入可写权限
        if (!file_exists($destination)) {
            touch($destination);
            chmod($destination, 0777);
        }
        return error_log($msg.PHP_EOL, 3, $destination) ?: false;
    }

    /**
     * 静态魔术调用
     * @param $method
     * @param $args
     * @return mixed
     *
     * @method void error($msg) static
     * @method void info($msg) static
     * @method void warn($msg) static
     */
    public static function __callStatic($method, $args) {
        $method = strtoupper($method);
        if (in_array($method, self::$type)) {
            $_msg = array_shift($args); 
            return self::write($_msg, $method);
        }
        return false;
    }
}&#91;/code&#93;
接下来,再写一个系统处理类,定义回调函数和注册回调函数&#91;code&#93;<?php
/**
 * 注册系统处理函数
 * @filename Handler.php
 * @since 2016-12-08 12:13:50
 * @author 979137.com
 * @version $Id$
 */
class Handler {

    const LOG_FILE_PREFIX = 'handler';
    const LOG_TYPE = 'CRITICAL';

    /**
     * 函数注册
     * @return none
     */
    static public function set() {
        //注册致命错误处理方法
        register_shutdown_function(array(__CLASS__, 'fatalError'));
        //注册自定义错误处理方法
        set_error_handler(array(__CLASS__, 'appError'));
        //注册异常处理方法
        set_exception_handler(array(__CLASS__, 'appException'));
    }

    /**
     * 致命错误捕获,PHP错误级别预定义常量参考:
     * http://php.net/manual/zh/errorfunc.constants.php
     * @return none
     */
    static public function fatalError() {
        $error = error_get_last() ?: null;
        if (!is_null($error) && in_array($error&#91;'type'&#93;, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) {
            $error&#91;'class'&#93; = $error&#91;'function'&#93; = '';
            Loger::write($error&#91;'message'&#93;, self::LOG_TYPE, self::LOG_FILE_PREFIX, $error);
            self::halt($error);
        }
    }
    /**
     * 自定义错误处理
     * @param int $errno 错误类型
     * @param string $errstr 错误信息
     * @param string $errfile 错误文件
     * @param int $errline 错误行数
     * @return void
     */
    static public function appError($errno, $errstr, $errfile, $errline) {
        $error&#91;'message'&#93; = "&#91;$errno&#93; $errstr";
        $error&#91;'file'&#93; = $errfile;
        $error&#91;'line'&#93; = $errline;
        $error&#91;'class'&#93; = $error&#91;'function'&#93; = '';
        if (!in_array($errno, array(E_STRICT, E_DEPRECATED))) {
            Loger::write($error&#91;'message'&#93;, self::LOG_TYPE, self::LOG_FILE_PREFIX, $error);
            if (in_array($errno, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) {
                self::halt($error);
            }
        }
    }

    /**
     * 自定义异常处理
     * @param mixed $e 异常对象
     * @return void
     */
    static public function appException($e) {
        $error = array();
        $error&#91;'message'&#93;  = $e->getMessage();
        $error['file'] = $e->getFile();
        $error['line'] = $e->getLine();
        $trace = $e->getTrace();
        if(empty($trace[0]['function']) && $trace[0]['function'] == 'exception') {
            $error['file'] = $trace[0]['file'];
            $error['line'] = $trace[0]['line'];
        }
        //$error['trace']  = $e->getTraceAsString();
        $error['function'] = $error['class'] = '';
        Loger::write($error['message'], self::LOG_TYPE, self::LOG_FILE_PREFIX, $error);
        self::halt($error);
    }

    /**
     * 错误输出
     * @param mixed $error 错误
     * @return void
     */
    static public function halt($error) {
        ob_get_contents() && ob_end_clean();
        $e = array();
        if (IS_DEBUG || IS_CLI) {
            //调试模式下输出错误信息
            $e = $error;
            if(IS_CLI){
                $e_message  = $e['message'].' in '.$e['file'].' on line '.$e['line'].PHP_EOL;
                if (isset($e['treace']) ) {
                    $e_message .= $e['trace'];
                }
                exit($e_message);
            }
        } else {
            //线网不显示错误信息,显示固定字符串,保护系统安全
            //TODO:比较友好的做法是重定向到一个漂亮的错误页面
            exit('Sorry, the system error');
        }
        // 包含异常页面模板
        $exceptionFile = __DIR__ . '/exception.tpl';
        include $exceptionFile;
        exit(0);
    }
}

[hr]
[font=微软雅黑][size=4][b]二、自动加载脚本(auto_append_file)[/b][/size][/font]

以上两个脚本准备就绪以后,我们可以把它们合并到一个文件,并增加两个重要常量:

//是否CLI模式
define('IS_CLI', PHP_SAPI == 'cli' ? true : false);
//当前是否开发模式,用于区分线网和开发模式,默认false
//开发模式下,所有错误会打印出来。非开发模式下,不会打印到页面,但会记录日志
define('IS_DEBUG', isset($_SERVER['DEV_ENV']) ? true : false);
//注册系统处理函数
Handler::set();

假设合并后的文件名叫:auto_prepend_file.php
我们把这个文件进行预加载(即自动包含进所有PHP脚本)
这时候到了很重要的一步就是,就是配置 php.ini

auto_prepend_file = /your_path/auto_prepend_file.php

重启你的Web服务器,让配置生效!
写一个测试脚本 test.php

<?php
var_dump($tencent);&#91;/code&#93;因为 $tencent 未定义,所以这时候就会回调我们注册的函数,可以看到已经有错误日志了<blockquote>[root@TENCENT64 /data/logs]# php -f test.php
[root@TENCENT64 /data/logs]# tail -f phplog_handler_20161209.log 
[2016-12-09 16:01:05][CRITICAL][/data/logs/test.php][2][][0.0.0.0][CLI][0.0.0.0][8] Undefined variable: tencent</blockquote>[hr]
[font=微软雅黑][size=5][b]三、搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)[/b][/size][/font]

因为我们的Web服务器一般都是分布式的,线网可能有N台服务器,我们的日志又是写本地,所以这时候就需要一个日志中心了,
日志系统目前已经有很多成熟的解决方案了,这里推荐 ELK 部署[list]
[*]ElasticSearch,一个基于 Lucene 的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 架构。
[*]Logstash,分布式日志收集、分析、存储工具
[*]Kibana,基于 ElasticSearch 的前端展示工具,
[/list]
ELK 是比较常见的日志系统搭建部署方案,网上已经有很多教程了,推荐两篇:
[url]https://my.oschina.net/itblog/blog/547250[/url]
[url]http://baidu.blog.51cto.com/71938/1676798[/url]
我这里就不重复写了!

下面是基于 ELK 搭建好的日志中心,好不好用,用了就知道了!^_^
[attach]119[/attach]
[hr]
[font=微软雅黑][size=5][b]四、利用 ElasticSearch RESTful 实现告警功能[/b][/size][/font]

前面我们提到 ElasticSearch 是基于 RESTful 架构!
ElasticSearch 提供了非常多的接口,包括索引的增删改查,文档的增删改查,各种搜索

ElasticSearch官方提供了一个PHP版本的SDK:Elasticsearch-PHP
[url]https://github.com/elastic/elasticsearch-php[/url]

官方文档(全英文,目前没有中文版,看起来可能吃力,英文就英文吧,认真啃):
[url]https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html[/url]

我们要做告警要用到的就是搜索接口!
说白了,就是定时去把前面写的那些日志,全扫出来,然后告警给关系人!

Elasticsearch-PHP 安装很简单,通过 Composer 来安装,我这里以 1.0 为例子
1、编写一个 composer.json 文件{
    "require": {
        "elasticsearch/elasticsearch": "~1.0"
    }
}

2、cd到你的项目里,下载安装包

curl -s http://getcomposer.org/installer | php

3、安装 Elasticsearch-PHP 及其依赖

php composer.phar install --no-dev

完事之后,就可以写告警脚本了!下面是我写的一个示例

<?php
//ElasticSearch Server,这是你的ELK服务器和端口
$es_server = array(
    '127.0.0.1:9200',
);

//接受告警人员
$alert_staff = array(
    'shiliangxie' => 18666666666,
    'xieshiliang' => 18600000000,
);

//需要告警的错误级别
$alert_level = array('ERROR', 'WARN', 'CRITICAL');

//告警周期,单位:分钟,这个需要和 cron 设置成一样的时间
$alert_cycle = 10;

############################################################################

//搜索最近 $alert_cycle 的错误日志
$q['index'] = 'logstash-'.date('Y.m.d');
$q['ignore_unavailable'] = true;
$q['type'] = 'php';
$q['size'] = 500;
$q['sort'] = array('@timestamp:desc');
$range_time['lte'] = sprintf('%.3f', microtime(true)) * 1000;
$range_time['gte'] = $range_time['lte'] - $alert_cycle * 60 * 1000;
$range = array('@timestamp' => $range_time);
$match = array('level' => implode(' OR ', $alert_level));
$q['body']['query']['filtered'] = array(
    'filter' => array('range' => $range),
    'query'  => array('match' => $match),
);
$params['hosts'] = $es_server;
require __DIR__.'/vendor/autoload.php';
$client = new Elasticsearch\Client($params);
$ret = $client->search($q);
$hit = $ret['hits']['hits'];
$hit = is_array($hit) && count($hit) ? $hit : array();

//组织告警内容
$alerts = array();
foreach($hit as $h) {
    $source = $h['_source'];
    $tag = md5($source['level'] . $source['file'] . $source['line']. $source['msg']);
    if (isset($alerts[$tag])) {
        $alerts[$tag]['num']++;
    } else {
        $alerts[$tag]['num'] = 1;
        $content = '[%s][%s][%s][%s]%s';
        $alerts[$tag]['content'] = sprintf($content, date('Y-m-d H:i:s', strtotime($source['@timestamp'])),
            $source['level'], $source['file'], $source['line'], $source['msg']
        );
    }
}

//var_dump($alerts);exit;

//发送告警
if (is_array($alerts) && count($alerts)) {
    array_walk($alerts, function($alert, $tag) use($alert_staff) {
        $msg = sprintf('[Repeat:%d]%s', $alert['num'], $alert['content']);
        array_map(function($mobile) use($msg) {
            return send_sms($mobile, $msg);
        }, $alert_staff);
        return ;
    });
}

然后我们把告警脚本加到 crontab 即可

*/10 * * * * /usr/local/bin/php /data/cron/PHPMonitor.php

坐等告警吧!!
[attach]120[/attach]

对于报警方案,目前市场上也有很多实现方案,如 Yelp 公司的 ElastAlert,用 Python 写的一个基于 ELK 用 Elasticsearch RESTful 报警框架
[url]https://github.com/Yelp/elastalert[/url]

PHP实现MySQL并发查询

一般的,一个看似很简单的页面,一次http请求后,到达服务端,穿过Cache层,落到后台后,实际可能会有很多很多的数据查询逻辑!而这些查询实际是不相互依赖的,也即可以同时查询。比如各种用户信息,用户的APP列表,每个APP对应的流量数据、消耗记录、服务状态,平台运行状态,消息通知,新闻资讯等等。
这篇文章主要介绍了数据查询层,如何把串行变并行,提高查询效率、提升应用性能。实现方式包括:mysqlnd异步查询,cURL并发请求,Swoole异步非阻塞!

PHP脚本是按文档流的形式来执行的,所以我们在编写PHP程序时,代码基本都是串行的,尤其是SQL,比如:
[attach]116[/attach]
这种方式,是每次查询都需要等待结果返回之后再开始下一次的查询,
运行时间 = (第1次发送请求时间 + 0.01 + 第1次返回时间)+(第2次发送请求时间 + 0.05 + 第2次返回时间)+(第3次发送请求时间 + 0.03 + 第3次返回时间)

如果是并发查询,那么流程就成了:
[attach]117[/attach]
运行时间 = 发送请求时间 + 0.05 + 返回时间

显然,并发查询要比串行查询快!

那么PHP可以实现并发查询吗?答案是肯定的!

[font=微软雅黑][size=5][b]一、利用MySQL的异步查询功能[/b][/size][/font]

目前 MySQL 的异步查询,只在 MySQLi 扩展提供,查询方法分别是:
[list]
[*]使用 MYSQLI_ASYNC 模式执行 mysqli::query
[*]获取异步查询结果:mysqli::reap_async_query
[/list]
需要注意的是,使用异步查询,需要使用 mysqlnd 作为PHP的MySQL数据库驱动,
mysqlnd(MySQL Native Driver) 是 Zend 公司开发的 MySQL 数据库驱动,采用PHP开源协议,用于代替旧版的由 MySQL AB公司(现在的Oracle)开发的 libmysql,PHP 5.3 及以上版本开始提供,PHP5.4 之后的版本 mysqlnd 为默认配置选项,
如果 PHP 小于 5.4,编译时需要指定编译参数:

--with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd

MySQL异步查询示例脚本:

/**
 * MySQL异步查询示例脚本:
 * @filename p_async.php
 * @url http://test.979137.com/ParallelSQL/p_async.php
 */

//期望结果集:获取以下用户的每个月每个APP的消费统计
$top = array('979137', '555555', '666666', '888888', '999999');
$ret = array_fill_keys($top, array());

//组织结构查询
$cmd = $resources = array();
$sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_2016%s WHERE uid=%d AND product='SAE' GROUP BY access_key"; 
foreach($top as $uid) {
    for($i = 1; $i <= 12; $i++) {
        $ret&#91;$uid&#93;&#91;$i&#93; = $tmp = array();
        $tmp&#91;'uid'&#93; = $uid;
        $tmp&#91;'month'&#93; = $i; 
        $tmp&#91;'resource'&#93; = $resources&#91;&#93; = new \mysqli('localhost', 'root', '123456', 'sae', '3306');
        $tmp&#91;'resource'&#93;->set_charset('utf8');
        $tmp['resource']->query(sprintf($sql, sprintf('%02d', $i), $uid), MYSQLI_ASYNC);
        $tag = spl_object_hash($tmp['resource']);
        $cmd[$tag] = $tmp;
    }   
}

$total = $query_times = count($resources);

//获取结果
do {
    $read = $error = $reject = $resources;
    //等待查询结束
    if (!\mysqli::poll($read, $error, $reject, 1)) {
        continue;
    }   
    //批量获取结果
    foreach($read as $resource) {
        $result = $resource->reap_async_query();
        if ($result) {
            $tag = spl_object_hash($resource);
            $uid = $cmd[$tag]['uid'];
            $month = $cmd[$tag]['month'];
            while(($row = $result->fetch_assoc()) != false) {
                $ret[$uid][$month][$row['access_key']] = $row['sum_amount'];
            }   
            $result->free();
            $total--;
    
        } else die('MySQLi error: '.$resource->error);
    }   
} while ($total > 0);

var_dump($ret);

[font=微软雅黑][size=5][b]二、cURL实现并发请求[/b][/size][/font]

先解释下 cURL 和 SQL 怎么就扯上关系了呢!

我们知道在很多系统架构里,PHP是不会直接操作DB的,而是 RESTFull 架构,这时候所有操作都接口化了,
这时上述所讲的 SQL 就演变成 接口调用 了,
因为 API,所以 cURL!

以下是PHP中cURL多线程相关函数:

curl_multi_add_handle — 向curl批处理会话中添加单独的curl句柄
curl_multi_close — 关闭一组cURL句柄
curl_multi_exec — 运行当前 cURL 句柄的子连接
curl_multi_getcontent — 如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
curl_multi_info_read — 获取当前解析的cURL的相关传输信息
curl_multi_init — 返回一个新cURL批处理句柄
curl_multi_remove_handle — 移除curl批处理句柄资源中的某个句柄资源
curl_multi_select — 等待所有cURL批处理中的活动连接
curl_multi_setopt — 为 cURL 并行处理设置一个选项
curl_multi_strerror — 返回描述错误码的字符串文本

我们可以利用这些多线程函数,实现 cURL 并发请求,从而实现并发 SQL!

cURL并发请求,服务端接口示例脚本:

/**
 * cURL并发请求,服务端接口示例脚本
 * @filename consume.php
 * @url http://test.979137.com/ParallelSQL/consume.php
 */
$resource = new \mysqli('localhost', 'root', '123456', 'sae', '3306');
$resource->set_charset('utf8');

$sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_%d WHERE uid=%d AND product='%s' GROUP BY access_key";
$sql = sprintf($sql, $_GET['ym'], $_GET['uid'], $_GET['product']);
$res = $resource->query($sql);

$out['code'] = $resource->errno;
$out['message'] = $resource->error;
$data = array();
if (is_object($res)) {
    while(($row = $res->fetch_assoc()) != false) {
        $data[$row['access_key']] = $row['sum_amount'];
    }   
}
$out['data'] = $data;

header('Content-Type: application/json; charset=utf-8');
echo json_encode($out);

cURL并发请求,客户端调用示例脚本:

/**
 * cURL并发请求,客户端调用示例脚本
 * @filename p_curl.php
 * @url http://test.979137.com/ParallelSQL/p_curl.php
 */

//期望结果集:获取以下用户的每个月每个APP的消费统计
$top = array('979137', '555555', '666666', '888888', '999999');
$ret = array_fill_keys($top, array());

$mch = curl_multi_init();
$opt[CURLOPT_HEADER] = 0;
$opt[CURLOPT_CONNECTTIMEOUT] = 60; 
$opt[CURLOPT_RETURNTRANSFER] = true;
$opt[CURLOPT_HTTPHEADER] = array('Host: api.979137.com');

//生成句柄并加入到批处理
$cmd = array();
foreach($top as $uid) {
    for($i = 1; $i <= 12; $i++) {
        $ret&#91;$uid&#93;&#91;$i&#93; = $tmp = array();
        $tmp&#91;'url'&#93; = sprintf('http://127.0.0.1/ParallelSQL/consume.php?ym=2016%02d&uid=%d&product=SAE', $i, $uid);
        $tmp&#91;'uid'&#93; = $uid;
        $tmp&#91;'month'&#93; = $i; 
        $tmp&#91;'resource'&#93; = curl_init($tmp&#91;'url'&#93;);   
        curl_setopt_array($tmp&#91;'resource'&#93;, $opt);
        curl_multi_add_handle($mch, $tmp&#91;'resource'&#93;);
        $cmd&#91;&#93; = $tmp;
    }   
}

//并发执行,直到全部结束。
do {
    curl_multi_exec($mch, $active);   
} while ($active > 0); 

//获取全部结果
foreach($cmd as $c) {
    $res = curl_multi_getcontent($c['resource']);
    $http_code = curl_getinfo($c['resource'], CURLINFO_HTTP_CODE);
    if ($res === false || $http_code != 200) {
        die(curl_error($c['resource']));
    }   
    $res = json_decode($res, true);
    $res['code'] && die($res['message']);
    $ret[$c['uid']][$c['month']] = $res['data'];
}
curl_multi_close($mch);

var_dump($ret);

[font=微软雅黑][size=5][b]查询效率对比[/b][/size][/font]
1、数据表机构:

CREATE TABLE `consume_201612` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(10) unsigned NOT NULL,
  `product` enum('UID','SAE','SC2','SCE','SEM','SCS','SLS') NOT NULL DEFAULT 'SAE',
  `access_key` varchar(255) NOT NULL,
  `service_code` varchar(255) NOT NULL,
  `amount` int(10) unsigned NOT NULL,
  `remark` varchar(255) NOT NULL,
  `data` text NOT NULL,
  `times` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `uid_pa` (`uid`,`product`,`access_key`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2、数据量:总共分12张表,每张表的数量在 500万~1100万 之间,全量数据在1亿以上

3、MySQL异步查询结果:

$ mysql.server restart
$ php p_async.php
Query times: 60
Run time: 2.4453s
(CPU)User time: 0.087966s
(CPU)System time: 0.117625s

4、cURL并发请求查询结果

$ mysql.server restart
$ php p_curl.php
Query times: 60
Run time: 2.173035s
(CPU)User time: 0.40652s
(CPU)System time: 0.869902s

5、普通串行查询结果

$ mysql.server restart
$ php p_sync.php
Query times: 60
Run time: 20.485623s
(CPU)User time: 0.083185s
(CPU)System time: 0.036566s
Memory usage: 304.72kb

并发执行时,我们可以在 MySQL 服务器看到所有的正在执行的SQL:
[attach]118[/attach]

[font=微软雅黑][size=5][b]总结[/b][/size][/font]

1、在并发查询下,查询效率提高了近10倍
2、使用 MySQL 异步查询,因为需要给所有查询都创建一个新的连接,而 MySQL 服务端会为每个连接创建一个单独的线程进行处理,如果创建的线程数过多,会给系统造成负担,请谨慎使用
3、使用 cURL 并发请求后端接口时,CPU负载明显上升,所以并发请求后端接口,一定程度上会增加后端压力,这和前端大流量下的高并发原理是一样的
4、使用 cURL 并发请求,还需要考虑一个网络延时的问题,网络延时越小,查询效率提升越明显。如果你是想代替类方法或函数调用,在条件允许的情况,建议直接连接服务器本机即127.0.0.1
5、在并发请求下,因为需要一次性接收全部返回结果,所以会占用更多的内存资源

需要说明的是,在实际应用中 cURL 的并发请求,一般不只单用于数据查询,而是为了完成更多的后台业务逻辑,
所以,在服务器负载能力允许的情况下,推荐使用 cURL 并行转发的形式,提升前端响应速度!

最后说一下 Swoole,

Swoole 是 PHP 的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询!
其中的异步MySQL,其原理是通过 MYSQLI_ASYNC 模式查询,然后获取 mysql 连接的 socket,加入到 epoll 时间循环中,当数据库返回结果时会回调指定函数。
这个过程是完全异步非阻塞的,不浪费CPU,具体实现方式这里不再详细介绍!

error: Size of "void *" is less than size of "long"

编译 httpd-2.2.27 的时候报错如下:
checking for void pointer length… yes configure: error: Size of “void *” is less than size of “long”

三种解决方法:

1、最简单的方法,编译时,增加编译参数 ap_cv_void_ptr_lt_long=no 即可

2、vim configure +7256

if test "$ap_cv_void_ptr_lt_long" = "yes"; then
    as_fn_error $? "Size of \"void *\" is less than size of \"long\"" "$LINENO" 5
fi

改成:

if test "$ap_cv_void_ptr_lt_long"  != "yes"; then
    as_fn_error $? "Size of \"void *\" is less than size of \"long\"" "$LINENO" 5
fi

保存,重新编译

3、移除 –with-pcre=/xxx/xxx/pcre 选项,(没必要哈哈)

Return Top