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%]

倒流’s Bolg

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、Nginx 如何按天分割日志

默认情况下 Nginx 和 Apache 等web服务器,是没有帮我们按天分日志的,而是把所有日志放到一个文件,当流量大而日志多的时候,管理起来非常方便,一方面是内容多不易查找定位问题,另一方面文件也会越来越大,无效内容越来越多。
所以我们需要把日志按天分割,网上很多通过 写脚本+定时任务 来做的,这其实有点多余,而且不好维护,Linux本身自带了很多日志处理程序,比如:logrotate ,它可以实现日志的自动滚动,日志归档等功能。
下面我们介绍使用 logrotate 实现「Apache按天分割日志」「Nginx按天分割日志」

Apache

Apache自身即成了 logrotate,一般在 /usr/local/apache2/bin/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小时

清理日志Shell脚本:

#!/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

Nginx

所以我们也可以用它做日期分割,假设:
a. 日志在 /data/logs/nginx/ 下面
b. Nginx 安装在 /usr/local/nginx/

1、在 /etc/logrotate.d 目录下创建一个 Nginx 的配置文件 Nginx 配置内容如下

/data/logs/nginx/xxx.qq.com-access_log /data/logs/nginx/xxx.qq.com-error_log {
    su nobody nobody
    daily
    rotate 30
    notifempty
    sharedscripts
    postrotate
    if [ -f /usr/local/nginx/logs/nginx.pid ]; then
        /bin/kill -USR1 /bin/cat /usr/local/nginx/logs/nginx.pid
    fi
    endscript
}

配置解释:

1)配置需要分割的日志文件,也可以用 * 代替
2)su nobody nobody :一般我们的日志目录是允许所有用户进行写操作的,logrotate 认为这不是不安全的,这时 logrotate
会报错如下:

error: skipping “/data/logs/nginx/cafe.qq.com-error_log” because parent directory has insecure permissions (It’s world writable or writable by group which is not “root”) Set “su” directive in config file to tell logrotate which user/group should be used for rotation.

所以我们需要在配置里使用 su 切换身份执行
3)daily :日志文件每天进行滚动
4)rotate 30 :保留30天的日志
5)notifempty:日志文件为空不进行滚动
6)sharedscripts :运行postrotate脚本
9)/bin/kill -USR1 \/bin/cat /usr/local/nginx/logs/nginx.pid\ :Nginx平滑重启,也即让Nginx重新生成文件

2、执行logrotate

/usr/sbin/logrotate -f /etc/logrotate.d/nginx

PHP运行错误监控,PHP Monitor

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

这篇文章要讲啥?

  1. 了解PHP错误机制
  2. 注册PHP的异常处理函数、错误处理函数、脚本退出函数
  3. 配置PHP预加载(重要)
  4. 搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)
  5. 基于 ElasticSearch RESTful 实现告警

要达到的目的?

所有PHP程序里的错误或潜在错误(支持自定义)都全部写入日志并作警,包含错误的所在文件名、行号、函数(如果有)、错误信息等等
PHP程序不需要做任何修改或接入!

一、了解PHP错误机制

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

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

二、注册PHP的异常处理函数、错误处理函数、脚本退出函数

PHP有三个很重要的注册回调函数的函数

  • register_shutdown_function 注册PHP退出时的回调函数
  • set_error_handler 注册错误处理函数
  • set_exception_handler 注册异常处理函数

我们先定义一个日志处理类,专门用于写日志(也可以用于用户日志哦)

<?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['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0';
        $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
        $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'CLI';
        if (!is_array($trace) || empty($trace)) {
            $dtrace = debug_backtrace();
            $trace  = $dtrace[0];
            if (count($dtrace) == 1) {
                //不是在类或函数内调用
                $trace['function'] = '';
            } else {
                if ($dtrace[1]['function'] == '__callStatic') {
                    $trace['file'] = $dtrace[2]['file'];
                    $trace['line'] = $dtrace[2]['line'];
                    $trace['function'] = empty($dtrace[3]['function']) ? '' : $dtrace[3]['function'];
                } else {
                    $trace['function'] = $dtrace[1]['function'];
                }
            }
        }
        $ace = $trace;
        $now = date('Y-m-d H:i:s');
        $pre = "[{$now}][%s][{$ace['file']}][{$ace['line']}][{$ace['function']}][{$remote}][{$method}][{$server}]%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;
    }
}

接下来,再写一个系统处理类,定义回调函数和注册回调函数

<?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['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) {
            $error['class'] = $error['function'] = '';
            Loger::write($error['message'], 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['message'] = "[$errno] $errstr";
        $error['file'] = $errfile;
        $error['line'] = $errline;
        $error['class'] = $error['function'] = '';
        if (!in_array($errno, array(E_STRICT, E_DEPRECATED))) {
            Loger::write($error['message'], 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['message']  = $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);
    }
}

二、自动加载脚本(auto_append_file)

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

//是否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);

因为 $tencent 未定义,所以这时候就会回调我们注册的函数,可以看到已经有错误日志了

[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

三、搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)

因为我们的Web服务器一般都是分布式的,线网可能有N台服务器,我们的日志又是写本地,所以这时候就需要一个日志中心了,
日志系统目前已经有很多成熟的解决方案了,这里推荐 ELK 部署

  • ElasticSearch,一个基于 Lucene 的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 架构。
  • Logstash,分布式日志收集、分析、存储工具
  • Kibana,基于 ElasticSearch 的前端展示工具
  • ELK 是比较常见的日志系统搭建部署方案,网上已经有很多教程了,推荐两篇:
    https://my.oschina.net/itblog/blog/547250
    http://baidu.blog.51cto.com/71938/1676798
    我这里就不重复写了!

    下面是基于 ELK 搭建好的日志中心,好不好用,用了就知道了!^_^
    倒流’s Bolg

    四、利用 ElasticSearch RESTful 实现告警功能

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

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

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

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

    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

    坐等告警吧!!
    倒流’s Bolg

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

    PHP实现MySQL并发查询

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

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

    如果是并发查询,那么流程就成了:
    倒流’s Bolg
    运行时间 = 发送请求时间 + 0.05 + 返回时间

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

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

    一、利用MySQL的异步查询功能

    目前 MySQL 的异步查询,只在 MySQLi 扩展提供,查询方法分别是:

    1. 使用 MYSQLI_ASYNC 模式执行 mysqli::query
    2. 获取异步查询结果:mysqli::reap_async_query

    需要注意的是,使用异步查询,需要使用 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

    点击了解 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[$uid][$i] = $tmp = array(); $tmp['uid'] = $uid; $tmp['month'] = $i; $tmp['resource'] = $resources[] = new \mysqli('localhost', 'root', '123456', 'sae', '3306'); $tmp['resource']->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);

    二、cURL实现并发请求

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

    我们知道在很多系统架构里,PHP是不会直接操作DB的,而是 RESTful 架构,这时候所有操作都接口化了,
    这时上述所讲的 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[$uid][$i] = $tmp = array(); $tmp['url'] = sprintf('http://127.0.0.1/ParallelSQL/consume.php?ym=2016%02d&uid=%d&product=SAE', $i, $uid); $tmp['uid'] = $uid; $tmp['month'] = $i; $tmp['resource'] = curl_init($tmp['url']); curl_setopt_array($tmp['resource'], $opt); curl_multi_add_handle($mch, $tmp['resource']); $cmd[] = $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);

    查询效率对比

    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:
    倒流’s Bolg

    总结

    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,具体实现方式这里不再详细介绍!

    svn: E000022: Can’t convert string from ‘UTF-8’ to native encoding

    今天在服务器上执行 svn checkout 时候,出现这样一行错误:

    svn: Can’t convert string from ‘UTF-8’ to native encoding

    然后 checkout 程序就退出了!

    问题定位后,原因是因为版本库里存在以中文命名的文件,

    locale

    LANG=”C”
    LC_COLLATE=”C”
    LC_CTYPE=”C”
    LC_MESSAGES=”C”
    LC_MONETARY=”C”
    LC_NUMERIC=”C”
    LC_TIME=”C”
    LC_ALL=”C”

    “C” 是系统默认的locale,”POSIX” 是 “C” 的别名。
    所以当我们新安装完一个系统时,默认的 locale 就是 C 或 POSIX。

    解决办法很简单,正确设置当前系统的 locale:

    export LC_CTYPE="zh_CN.UTF-8"

    LC_TYPE 变量是设置系统的 语言符号及其分类

    然后重新 checkout 即可!

    你可以直接修改

    注意,根据你的系统字符集设置变量,如果 zh_CN.UTF-8 不行,有可能要改成 GB2312:

    export LC_CTYPE="zh_CN.GB2312"

    但是这只是临时解决方案,如果不想每次都执行这样的命令,
    你可以把这条命令放进 ~/.bashrc 里面,即用户登录时自动执行,
    也可以把以上内容直接加到svn的钩子里面,解决提交时自动输出处理遇到的问题

    了解更多locale:https://979137.com/archives/157.html

    locale的设定及LANG、LC_CTYPE、LC_ALL环境变量

    locale

    LANG=C
    LC_CTYPE=”C”
    LC_NUMERIC=”C”
    LC_TIME=”C”
    LC_COLLATE=”C”
    LC_MONETARY=”C”
    LC_MESSAGES=”C”
    LC_PAPER=”C”
    LC_NAME=”C”
    LC_ADDRESS=”C”
    LC_TELEPHONE=”C”
    LC_MEASUREMENT=”C”
    LC_IDENTIFICATION=”C”
    LC_ALL=

    locale这个单词中文翻译成地区或者地域,其实这个单词包含的意义要宽泛很多。Locale是根据计算机用户所使用的语言,所在国家或者地区,以及当地的文化传统所定义的一个软件运行时的语言环境。

    locale

    LANG=en_US.UTF-8
    LC_CTYPE=”en_US.UTF-8″
    LC_NUMERIC=”en_US.UTF-8″
    LC_TIME=”en_US.UTF-8″
    LC_COLLATE=”en_US.UTF-8″
    LC_MONETARY=”en_US.UTF-8″
    LC_MESSAGES=”en_US.UTF-8″
    LC_PAPER=”en_US.UTF-8″
    LC_NAME=”en_US.UTF-8″
    LC_ADDRESS=”en_US.UTF-8″
    LC_TELEPHONE=”en_US.UTF-8″
    LC_MEASUREMENT=”en_US.UTF-8″
    LC_IDENTIFICATION=”en_US.UTF-8″
    LC_ALL=en_US.UTF-8
    [oracle@game ~]$

    locale把按照所涉及到的文化传统的各个方面分成12个大类,这12个大类分别是:

    1、语言符号及其分类(LC_CTYPE)
    2、数字(LC_NUMERIC)
    3、比较和排序习惯(LC_COLLATE)
    4、时间显示格式(LC_TIME)
    5、货币单位(LC_MONETARY)
    6、信息主要是提示信息,错误信息,状态信息,标题,标签,按钮和菜单等(LC_MESSAGES)
    7、姓名书写方式(LC_NAME)
    8、地址书写方式(LC_ADDRESS)
    9、电话号码书写方式(LC_TELEPHONE)
    10、度量衡表达方式 (LC_MEASUREMENT)
    11、默认纸张尺寸大小(LC_PAPER)
    12、对locale自身包含信息的概述(LC_IDENTIFICATION)。

    所以说,locale就是某一个地域内的人们的语言习惯和文化传统和生活习惯。一个地区的locale就是根据这几大类的习惯定义的,这些locale定 义文件放在/usr/share/i18n/locales目录下面,例如en_US, zh_CN and de_DE@euro都是locale的定义文件,这些文件都是用文本格式书写的,你可以用写字板打开,看看里边的内容,当然出了有限的注释以外,大部分 东西可能你都看不懂,因为是用的Unicode的字符索引方式。

    [oracle@game ~]$ cd /usr/share/i18n/locales
    [oracle@game locales]$ ls
    aa_DJ ar_YE el_GR es_ES fr_CH iso14651_t1 ne_NP so_ET translit_hangul
    aa_ER az_AZ el_GR@euro es_ES@euro fr_FR it_CH nl_BE so_KE translit_narrow
    aa_ER@saaho be_BY en_AU es_GT fr_FR@euro it_IT nl_BE@euro so_SO translit_neutral
    aa_ET bg_BG en_BW es_HN fr_LU it_IT@euro nl_NL sq_AL translit_small
    af_ZA bn_BD en_CA es_MX fr_LU@euro iw_IL nl_NL@euro sr_CS translit_wide
    am_ET bn_IN en_DK es_NI ga_IE ja_JP nn_NO st_ZA tr_TR
    an_ES br_FR en_GB es_PA ga_IE@euro ka_GE no_NO sv_FI tt_RU
    ar_AE br_FR@euro en_HK es_PE gd_GB kk_KZ oc_FR sv_FI@euro uk_UA
    ar_BH bs_BA en_IE es_PR gez_ER kl_GL om_ET sv_SE ur_PK
    ar_DZ byn_ER en_IE@euro es_PY gez_ER@abegede kn_IN om_KE ta_IN uz_UZ
    ar_EG ca_ES en_IN es_SV gez_ET ko_KR pa_IN te_IN uz_UZ@cyrillic
    ar_IN ca_ES@euro en_NZ es_US gez_ET@abegede kw_GB pl_PL tg_TJ vi_VN
    ar_IQ cs_CZ en_PH es_UY gl_ES lg_UG POSIX th_TH wa_BE
    ar_JO cy_GB en_SG es_VE gl_ES@euro lo_LA pt_BR ti_ER wa_BE@euro
    ar_KW da_DK en_US et_EE gu_IN lt_LT pt_PT ti_ET wal_ET
    ar_LB de_AT en_ZA eu_ES gv_GB lv_LV pt_PT@euro tig_ER xh_ZA
    ar_LY de_AT@euro en_ZW eu_ES@euro he_IL mi_NZ ro_RO tl_PH yi_US
    ar_MA de_BE es_AR fa_IR hi_IN mk_MK ru_RU translit_circle zh_CN
    ar_OM de_BE@euro es_BO fi_FI hr_HR ml_IN ru_UA translit_cjk_compat zh_HK
    ar_QA de_CH es_CL fi_FI@euro hu_HU mn_MN se_NO translit_cjk_variants zh_SG
    ar_SA de_DE es_CO fo_FO hy_AM mr_IN sid_ET translit_combining zh_TW
    ar_SD de_DE@euro es_CR fr_BE i18n ms_MY sk_SK translit_compat zu_ZA
    ar_SY de_LU es_DO fr_BE@euro id_ID mt_MT sl_SI translit_font
    ar_TN de_LU@euro es_EC fr_CA is_IS nb_NO so_DJ translit_fraction
    [oracle@game locales]$

    对于de_DE@euro的一点说明,@后边是修正项,也就是说你可以看到两个德国的locale:/usr/share/i18n/locales /de_DE@euro和/usr/share/i18n/locales/de_DE。打开这两个locale定义,你就会知道它们的差别在于 de_DE@euro使用的是欧洲的排序、比较和缩进习惯,而de_DE用的是德国的标准习惯。

    上面我们说到了zh_CN.GB18030的前半部分,后半部分是什么呢?大部分Linux用户都知道是系统采用的字符集。

    zh_CN.GB2312到底是在说什么? Locale是软件在运行时的语言环境, 它包括语言(Language), 地域 (Territory) 和字符集(Codeset)。一个locale的书写格式为: 语言[_地域[.字符集]]。所以说呢,locale总是和一定的字符集相联系的。下面举几个例子:

    1、我说中文,身处中华人民共和国,使用国标2312字符集来表达字符。zh_CN.GB2312=中文_中华人民共和国+国标2312字符集。

    2、我说中文,身处中华人民共和国,使用国标18030字符集来表达字符。zh_CN.GB18030=中文_中华人民共和国+国标18030字符集。

    3、我说中文,身处中华人民共和国台湾省,使用国标Big5字符集来表达字符。zh_TW.BIG5=中文_台湾.大五码字符集

    4、我说英文,身处大不列颠,使用ISO-8859-1字符集来表达字符。 en_GB.ISO-8859-1=英文_大不列颠.ISO-8859-1字符集

    5、我说德语,身处德国,使用UTF-8字符集,习惯了欧洲风格。de_DE.UTF-8@euro=德语_德国.UTF-8字符集@按照欧洲习惯加以修正,注意不是[url=mailto:de_DE@euro.UTF]de_DE@euro.UTF[/url]-8,所以完全的locale表达方式是 [语言[_地域][.字符集] [@修正值]。

    其中,与中文输入关系最密切的就是LC_CTYPE,LC_CTYPE规定了系统内有效的字符以及这些字符的分类, 诸如什么是大写字母,小写字母,大小写转换,标点符号、可打印字符和其他的字符属性等方面。而locale定 义zh_CN中最最重要的一项就是定义了汉字(Class“hanzi”)这一个大类,当然也是用Unicode描述的,这就让中文字符在Linux系统 中成为合法的有效字符,而且不论它们是用什么字符集编码的。

    怎样设定locale呢?

    设定locale就是设定12大类的locale分类属性,即12个LC_*。除了这12个变量可以设定以外,为了简便起见,还有两个变量:LC_ALL和LANG。它们之间有一个优先级的关系:LC_ALL > LC_* >LANG。可以这么说,LC_ALL是最上级设定或者强制设定,而LANG是默认设定值。

    1、如果你设定了LC_ALL=zh_CN.UTF-8,那么不管LC_*和LANG设定成什么值,它们都会被强制服从LC_ALL的设定,成为 zh_CN.UTF-8。

    2、假如你设定了LANG=zh_CN.UTF-8,而其他的LC_*=en_US.UTF-8,并且没有设定LC_ALL的话,那么系统的locale设定以LC_*=en_US.UTF-8。

    3、假如你设定了LANG=zh_CN.UTF-8,而其他的LC_*,和LC_ALL均未设定的话,系统会将LC_*设定成默认值,也就是LANG的值zh_CN.UTF-8。

    4、假如你设定了LANG=zh_CN.UTF-8,而其他的LC_CTYPE=en_US.UTF-8,其他的LC_*,和LC_ALL均未设定的话, 那么系统的locale设定将是:LC_CTYPE=en_US.UTF-8,其余的 LC_COLLATE,LC_MESSAGES等等均会采用默认值,也就是 LANG的值,也就是LC_COLLATE=LC_MESSAGES=……= LC_PAPER=LANG=zh_CN.UTF-8。

    所以,locale是这样设定的:

    1、如果你需要一个纯中文的系统的话,设定LC_ALL= zh_CN.XXXX,或者LANG=zh_CN.XXXX都可以,当然你可以两个都设定,但正如上面所讲,LC_ALL的值将覆盖所有其他的locale设定,不要作无用功。

    2、如果你只想要一个可以输入中文的环境,而保持菜单、标题,系统信息等等为英文界面,那么只需要设定 LC_CTYPE=zh_CN.XXXX,LANG=en_US.XXXX就可以了。这样LC_CTYPE=zh_CN.XXXX,而LC_COLLATE=LC_MESSAGES=……= LC_PAPER=LANG=en_US.XXXX。

    3、假如你高兴的话,可以把12个LC_*一一设定成你需要的值,打造一个古灵精怪的系统: LC_CTYPE=zh_CN.GBK/GBK(使用中文编码内码GBK字符集); LC_NUMERIC=en_GB.ISO-8859-1(使用大不列颠的数字系统) LC_MEASUREMEN=de_DE@euro.ISO-8859-15(德国的度量衡使用ISO-8859-15字符集) 罗马的地址书写方式,美国的纸张设定……。估计没人这么干吧。

    4、假如你什么也不做的话,也就是LC_ALL,LANG和LC_*均不指定特定值的话,系统将采用POSIX作为lcoale,也就是C locale。

    另外LANG和LANGUAGE有什么区别呢?

    LANG – Specifies the default locale for all unset locale variables
    LANGUAGE – Most programs use this for the language of its interface
    LANGUAGE是设置应用程序的界面语言。而LANG是优先级很低的一个变量,它指定所有与locale有关的变量的默认值,

    UNIX/Linux命令行提示符显示当前完整路径的方法

    查看默认方式:

    vim /etc/bashrc

    PS1=’\h:\W \u\$ ‘

    我们先了解命令释义:

    \u 当前用户账号
    \h 当前主机名
    \W 当前路径最后一个目录
    \w 当前绝对路径(当前用户目录会以 ~代替)
    $PWD 当前全路径
    \$ 显示命令行’$’或者’#’符号

    知道命令代表的意义后,
    如果要修改命令提示符的显示方式,我们可以修改环境变量PS1

    1. 命令行提示符完全显示完整的工作目录名称:
    PS1='[\u@\h $PWD]\$ ‘

    2. 命令行提示符只列出最后一个目录:
    PS1='[\u@\h \W]\$ ‘

    3. 命令行提示符显示完整工作目录,当前用户目录会以 ~代替:
    PS1='[\u@\h \w]\$ ‘

    修改完成后,执行: source /etc/bashrc 使配置生效即可。

    当然,如果你不想改变全局,只想改变当前用户的,则 vim ~/.bashrc 即可

    4行代码,Web服务器(Apache/Nginx)挂了自动重启

    有时候,我们的Web服务器如Apache、Nginx 可能因为某些原因退出了,这时候如果没有及时发现,就会影响服务,
    所以搞个监控,是很有必要滴

    #!/bin/sh
    HTTPD_NUM=`ps aux | grep -P "(bin/httpd)|(./httpd)" | grep -v "vim" | grep -v "grep" | wc -l`
    if [ ${HTTPD_NUM} -eq 0 ]; then
        /usr/local/apache/bin/apachectl restart
    fi
    

    如果是Nginx

    !/bin/sh
    NGINX_NUM=`ps aux | grep -P "nginx" | grep -v "vim" | grep -v "grep" | wc -l`
    if [ ${NGINX_NUM} -eq 0 ]; then
        /usr/local/nginx/sbin/nginx
    fi
    
    crontab -e

    * * * * * /bin/bash /data/cron/apache_restart.sh > /dev/null 2>&1
    * * * * * /bin/bash /data/cron/nginx_restart.sh > /dev/null 2>&1
    

    加入crontab,每分钟检查一次
    原理很简单,就是判断是否有httpd进程,如果木有了,执行重启操作!

    -bash: ./configure: /bin/sh^M: bad interpreter: No such file or directory

    ./configure --prefix=/usr/local/jpeg --enable-shared --enable-static
    

    -bash: ./configure: /bin/sh^M: bad interpreter: No such file or directory

    今天在编译 jpeg-6b 的时候,出现一行这样的错误,

    看见 ^M 就联想到了 Window 下的 dos 格式

    vim ./configure
    

    :set ff
    fileformat=dos

    我X,果然是!

    这是 Linux 和 Windows 换行符的差异导致的,
    说明 configure 这个文件是作者在 Window 写的,并且没有将文件格式设置成 unix 格式(一般的编辑器都有这项设置),
    所以在 Linux 打开时,在每行后面会加个 ctrl+m 就是 ^M
    所以 /bin/sh 就变成了 /bin/sh^M
    所以脚本就不能运行了

    dos2unix configure

    我把文件格式转换之后,就OK了!

    如果你的系统不支持 dos2unix,你也可以

    vim ./configure
    

    :set ff=unix
    :wq

    最新文章

    Return Top