分类 PHP 下的文章

为什么要减少HTTP请求

性能黄金法则:

只有10%~20%的最终永不响应时间花在接收请求的HTML文档上,剩下的80%~90%时间花在HTML文档所引用的所有组件(图片,script,css,flash等等)进行的HTTP请求上。

如何改善

改善响应时间的最简单途径就是减少组件的数量,并由此减少HTTP请求的数量。

HTTP链接产生的开销

域名解析 -- TCP链接 -- 发送请求 -- 等待 -- 下载资源 -- 解析时间

  • 需要注意 DNS 缓存也需要时间,多个缓存就要查找多次有可能缓存会被清除
  • HTTP1.1 协议规定请求只能串行发送,也就是说一百个请求必须一次逐个发送,前面的一个请求完成才能开始下一个请求。

减少HTTP请求的方式

图片地图

图片地图允许你在一个图片上关联多个URL。目标URL的选择取决于用户单击了图片上的哪个位置。

我们可以通过使用五个分开的图片,然后每个图片对应一个超链接产生了5个HTTP请求,我们的目标是要减少HTTP请求。

将五个图片合并成为一张图片,然后以位置信息定位超链接。

把HTTP请求减少为一个 ,可以暴增设计的完整性和功能的齐全性。

<map><area></map></map>

实例

未使用图像地图的例子:

http://stevesouders.com/hpws/imagemap-no.php

使用图像地图的例子:

http://stevesouders.com/hpws/imagemap.php

CSS Sprites

CSS Sprites 中文翻译为 CSS 精灵,通过使用合并图片,通过指定 CSS 的 backgroud-image 和backgroud-position来显示元素。

backgroud-position属性

backgroud-position:x,y; x和y可以写负值也可以写正值,我们可以想象图片的左上方为(0,0),以(0,0)坐标向右是为负数的 X 轴,以(0,0)坐标向下是为负数的 y 轴。

使用图片精灵的案例:

http://stevesouders.com/hpws/sprites.php

图片地图和 CSS Sprites 的响应时间基本相同,但比使用各自独立图片的方式要快50%以上。

合并脚本和样式表

使用外部的 JS 和 CSS 文件引用的方式,因为这要比直接写在页面中性能要更好一点。

独立的一个 JS 比用多个 JS 文件组成的页面载入要快38%。

把多个脚本合并为一个脚本,把多个样式表合并成为一个样式表。

http://stevesouders.com/hpws/combo-none.php

http://stevesouders.com/hpws/combo.php

图片使用base64编码减少页面请求数

采用Base64的编码方式将图片直接嵌入到网页中,而不是从外部载入。

<img src="data:image/gif;base64,/9j/4AAqsKZJ.....">

http://stevesouders.com/hpws/inline-images.php

http://stevesouders.com/hpws/inline-css-images.php

盗链是指通过技术手段获得他人服务器上的资源地址,绕过别人的资源展示页面,直接在自己的页面上向最终用户提供此内容。常见的是小站盗用大站的图片、音乐、视频、软件等资源。

通过盗链的方法可以减轻自己服务器的负担,因为真实的空间和流量均是来自别人的服务器。

防盗链的工作原理

在 http 协议中,我们的每一次请求在 header 中都会包含 Referer 字段,用来表示这个请求的来源,我们就可以通过这个字段来做一个白名单,只有 Referer 来源在我的白名单列表中才允许请求访问资源,如果不允许,我们可以拒绝请求,甚至是返回一张我们自定义的图片来告诉用户,这个图片在哪台服务器上。

Nginx防盗链的实现

Nginx Referer

Nginx 模块 ngx_http_referer_module 用于阻挡来源非法的域名请求。

Nginx 指令 valid_referers,全局变量$invalid_referer。

valid_referers none | blocked | server_names| string ...;

none: "Referer" 来源头部为空的情况

blocked: "Referer"来源头部不为空,但是里面的值被代理或者防火墙删除了,这些值都以 http://或者 https://开头。

server_names: "Referer" 来源头不包含当前的 server_names

location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$ {
    valid_referers none blocked maksim.website *.maksim.website;
    if ($invalid_referer)
    {
        #return 403;
        rewrite ^/http://www.maksim.website/403.jpg;
    }
}

针对目录

location /images/ {
    valid_referers none blocked maksim.website *.maksim.website;
    if ($invalid_referer)
    {
        #return 403;
        rewrite ^/http://www.maksim.website/403.jpg;
    }
}

但是这种方法存在一个问题,那就是 Referer 是可以伪造的,只要对方在每一次请求的时候设置 Referer 那么就可以完全绕考这个限制,直接访问我们的资源。

Nginx HTTPAccessKeyModule 加密签名

请求图片的时候带签名过去,当返回图片的时候判断签名是否正确,也就是暗号。

加密签名的时候需要使用第三方模块HttpAccessKeyModule,因为在服务端php里需要显示图片的时候跟一个签名,交给Nginx的时候,Nginx需要做一个判断,判断前面是否正确,Nginx在判断的时候就需要这个模块了。

accesskey on | off
accesskey_hashmethod md5 | sha-1 
accesskey_arg GET参数名称
accesskey_signatrue 加密规则
location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$
{
    accesskey on
    accesskey_hashmethod md5
    accesskey_arg "key"
    accesskey_signatrue "maksim$remote_addr"
}
<?php
  //md5(maksim.ip)
  $sign = md5('maksim'.$_SERVER['REMOTE_ADDR']);
    echo '<img src="./image/maksim.png?sign='. $sign .'">';

高并发架构的相关概念

高并发的概念

  • 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任意一个时刻点上只 有一个程序在处理机上运行。

    上面这一段是摘自百度百科的一段话,但是上面的定义很明显不是我们通常所说的并发,在互联网时代,所讲的并发、高并发,通常是指并发访问。也就是在某一个时间点,有多少个访问同时到来。

  • 高并发: 通常如果一个系统的日 PV 在千万以上,有可能是一个高并发的系统,有的公司完全不走技术路线,全靠机器堆,这不再我们的讨论范围。

高并发中需要关注相关概念

  • QPS: 每秒钟请求或者查询的数量,在互联网领域,指的是每秒响应请求数(指的是HTTP请求)。
  • 吞吐量:单位时间内处理的请求数量(通常由QPS与并发数决定)
  • 响应时间: 从请求发出到收到响应花费的时间。例如系统处理一个HTTP请求需要 100ms, 这个 100ms 就是系统的响应时间。
  • PV: 综合浏览量(Page View),即页面浏览量或者点击量,一个访客在24小时内访问的页面数量。同一个人浏览你的网站统一页面,只记录一次 PV。
  • UV: 独立访客(UniQue Visitor),即一定时间范围内相同访客多次访问网站,只计算为一个独立访客。
  • 带宽: 计算带宽大小需关注两个指标,峰值流量和页面的平均大小
  • 日网站带宽: PV/统计时间(换算到秒)*平均页面大小(单位KB)* 8,峰值一般为平均值的倍数,根据实际情况来定。
  • 压力测试: 测试服务器能够最大承受的最大并发数和QPS值,对于计算机来说,我们应该知道这台服务器最大能够承受多少QPS,而我们可以通过一天的PV来计算出峰值的QPS,这样我们就可以根据需求进行优化。

在这里我们需要注意,QPS 不等于并发数数量,QPS是每秒HTTP请求数量,并发连接数是系统同时处理的请求数量。

(总PV数 80%) / (6小时秒数 20%) = 峰值每秒请求数量(QPS), 80%的访问量集中在20%的时间,那为什么是6个小时呢?

6个小时主要是做了一个简单的估计,比如说我们访问网站中午两个小时,下午两个小时,晚上两个小时。

压力测试工具

在我们日常的工作当中下面这几款是我们比较常用的压测工具:

  • ab:Apache benchmark,是 Apache 官方推出的工具创建多个并发访问线程,模拟多个访问者同时访问某一URL 地址进行访问。
  • wrk:是一款开源的性能测试工具,简单易用,没有Load Runner那么复杂,他和 apache benchmark(ab)同属于性能测试工具,但是比 ab 功能更加强大,并且可以支持lua脚本来创建复杂的测试场景。
  • Jmeter:Jmeter 是测试人员最长用的压测工具,他的功能要比 ab 和 wrk 强大很多。

接下来,我们用 ab 来简单说明一下压力测试的使用方法。

ab 的简单使用

[root@stache34 ~]# ab -n 1000 -c 900 http://192.168.176.30/index.html
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.176.30 (be patient) #完成的进度
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests

Server Software:        Apache/2.4.6   #服务器软件版本
Server Hostname:        192.168.176.30 #服务器主机名
Server Port:            80                          #服务器端口

Document Path:          /index.html      #测试的页面
Document Length:        9 bytes            #页面的字节数

Concurrency Level:      900                     #请求的并发数,代表着访问的客户端数量
Time taken for tests:   0.489 seconds #整个测试花费的时间
Complete requests:      1000                     #成功的请求数量
Failed requests:        0                         #失败的请求数量
Write errors:           0
Total transferred:      267000 bytes     #整个测试过程的总数据大小(包括header头信息等)
HTML transferred:       9000 bytes         #整个测试过程HTML页面实际的字节数
Requests per second:    2045.81 [#/sec] (mean) #每秒处理的请求数,这是非常重要的参数,体现了服务器的吞吐量
                                               #后面括号中的 mean 表示这是一个平均值
Time per request:       439.923 [ms] (mean)      #平均请求响应时间,括号中的 mean 表示这是一个平均值

#每个请求的时间 0.489[毫秒],意思为在所有的并发请求每个请求实际运行时间的平均值
#由于对于并发请求 cpu 实际上并不是同时处理的,而是按照每个请求获得的时间片逐个轮转处理的
#所以基本上第一个 Time per request 时间约等于第二个 Time per request 时间乘以并发请求数
Time per request:       0.489 [ms] (mean, across all concurrent requests)

Transfer rate:          533.43 [Kbytes/sec] received #传输速率,平均每秒的流量
                                                     #可以帮助排除是否存在网络流量过大导致响应时间延长的问题
Connection Times (ms) #连接时间
              min  mean[+/-sd] median   max #median中间
Connect:        0   17  11.8     21      34
Processing:     1  145 153.9     35     446
Waiting:        1  145 153.9     35     446
Total:         16  163 161.2     63     474

Percentage of the requests served within a certain time (ms) #在一定的时间内提供服务的请求的百分比
  50%     63
  66%    244
  75%    255
  80%    260
  90%    468
  95%    471
  98%    474
  99%    474
 100%    474 (longest request)

注意事项

  • 测试机器与被测机器分开,因为 ab 在测试的时候同样会占用机器资源,如果在一台器上进行测试会影响测试结果。
  • 不要对生产服务做压力测试,避免影响生产环境。
  • 观察测试工具 ab 所在机器,以及被测试的前端机的 CPU,内存,网络等都不超过最高限度的75%。

QPS 的增长中会遇到的挑战

随着 QPS 的增长,每个阶段需要根据实际情况来进行优化,优化的方案也与硬件、网络带宽息息相关。

QPS解决收到解决方案
50可以称之为小型网站,一般的服务器都可以应付。
100假设关系型数据库的每次请求在0.01秒完成,并且页面中只有一个 SQL 查询,那么100 QPS 意味着1秒钟完成100个请求,但是此时我们并不能保证数据库查询能完成100次。数据库缓存层、数据库的负载均衡
800假设我们使用百兆带宽,意味着网站出口的实际带宽是8M 左右,假设每个页面只有10K,在这个并发条件下,百兆带宽已经吃完了。CDN 加速、负载均衡
1000假设使用 Memcache 缓存数据库查询数据,每个页面对 Memcache 的请求远大于直接 DB 的请求,Memcache 的悲观并发数在2W 左右,但有可能在之前内王带宽已经吃光,表现出不稳定。静态 HTML缓存
2000这个级别下,文件系统访问锁都成了灾难。做业务隔离,分布式存储

为了应对QPS 增长我们可以使用下面的手段来应对:

场景方案
流量优化防盗链
前端优化减少 HTTP 请求、添加异步请求、启用浏览器缓存和文件压缩、CDN 加速、建立独立的图片服务器
服务端优化页面静态化、并发处理、队列处理
数据库优化数据库缓存、读写分离、分库分表、分区操作、读写分离、负载均衡
集群Nginx、LVS

网上有很多数据模式的文章,这里不会按照已经成型的书和文章来进行编写,这样没有任何意义,在应用设计模式的时候一定要根据自己业务和使用的语言来进行编写。

在工厂模式中,我们基于商城的案例编写了一个书、狗和酒的工厂案例,在线我们对需求进行一次迭代,老板决定每个商品的 getList 方法都要额外加上狗的信息(狗本身除外)。

当我们接到这个需求的时候,首先第一个想法肯定是这样实现:

include 'autoload.php';
//$object = new Books();
$book = ProductFactory::getProduct('book');
$bookList = $book->getList();
$dog = ProductFactory::getProduct('dog');
$dogList = $dog->getList();

接下来,我们利用设计模式来设计一下,不过首先我们需要解决一个问题,目前的代码虽然都有 getList 但是却并不是一个硬性的规范,如果现在商城增加品类了,例如增加了一个 Phone 交给了一个新的开发人员去开发,获取列表的方法它给写成了 getPhones 那么我们对应的 ProductFactory 在实现上可能就会存在很大的麻烦。

对于约束,我们可以先定义个接口:

<?php

interface IProduct
{
    public function getList();
} 

这样无论以后我们新增什么品类都使用这个方法去进行创建,我们目前已有的三个类都去 implements ,接下来我们对工厂模式进行一些改进。

<?php

class ProductFactory
{
    public static function getProduct($type)
    {
        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = null;
        }
        if (is_subclass_of($obj, "IProduct")) {
            return $obj;
        }
        return null;
    }
}

我们在最后增加了 is_subclass_of 判断,该函数可以判断当前的变量是否是某一个类、抽象类、接口的子类,这样可以让我们的工厂更加健全,避免返回的对象不满足 IProduct 的约束,可以减少运行时的报错,因为你无法保证其他人修改工厂方法的时候放进来的类到底满不满足我们的业务要求。

如果要实现我们现在的需求,需要用到注册树模式,我们在这里叫它数据中你想你模式,我们把数据全部都放到数据中心中进行统一的管理,取数据的时候也从数据中心取,坚决不合类本身私自交往。

<?php

class ProductDataCenter
{
    public static $objectList = [];

    public static function set($k, $v)
    {
        self::$objectList[$k] = $v;
    }

    public static function get($key)
    {
        return self::$objectList[$key] ?? [];
    }
}

ProductDataCenter 的职责非常简单,就是存储我们的产品列表,对外提供两个方法:

  • set 存值

接下来我们继续改造工厂方法,工厂不再继续返回值,而是向 ProductDataCenter 中增加数据。

<?php

class ProductFactory
{
    public static function getProduct($type)
    {
        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = null;
        }
        if (is_subclass_of($obj, "IProduct")) {
            // 把创建的对象放到数据中心
            ProductDataCenter::set($type, $obj);
        }
    }
}

这样一来,我们在外面调用的时候就变成了这个样子:

<?php

include 'autoload.php';
//$object = new Books();
ProductFactory::getProduct('book');

$data = ProductDataCenter::get;
var_dump($data);

但是目前还是没有解决我们的需求,我们还需要继续修改数据中心。

<?php

class ProductDataCenter
{
    public static $objectList = [];

    public static function set($k, $v)
    {
        self::$objectList[$k] = $v;
    }

    public static function __callStatic($name, $arguments)
    {
        $result = [];
        foreach (self::$objectList as $k => $v) {
            if (method_exists($v, $name)) {
                $ret = $v->$name($arguments);

                if ($ret) {
                    foreach ($ret as $item) {
                        $result[] = $item;
                    }

                }
            }
        }
        if (count($result) > 0) {
            return $result;
        }
    }
}

在这里,我们增加了一个 __callStatic 魔术方法,这个魔术方法的作用是,当调用静态方法不存在的时候就会走到这个函数。

其主要作用就是从注册树种取出 Proudct 类,然后执行 getList 方法,并且将在注册树中存在的 Product 都执行一遍,组成成我们想要的数据返回回去。

现在如果我们想要展示 Book 和 Dog 的数据只需要这样获取即可:

<?php

include 'autoload.php';
//$object = new Books();
ProductFactory::getProduct('book');
ProductFactory::getProduct('dog');
$data = ProductDataCenter::getList();
var_export($data);

## 执行结果如下:
array (
  0 => 
  array (
    'product_id' => 101,
    'product_name' => 'java 从入门到精通',
  ),
  1 => 
  array (
    'product_id' => 102,
    'product_name' => 'PHP 从入门到精通',
  ),
  2 => 
  array (
    'product_id' => 201,
    'product_name' => '泰迪',
  ),
  3 => 
  array (
    'product_id' => 202,
    'product_name' => '金毛',
  ),
)%

既然是注册树模式,就还需要有从树上摘除的操作:

public static function remove($key)
{
        unset(self::$objectList[$key]);
}

这一节就结束了。

在正式开始之前,我们先来编写一个自动加载功能,这样我们就不用老是 include 文件了。

<?php

define('ROOT_PATH', str_replace('\\', '/', realpath(dirname(__FILE__) . '/')) . "/");
function autoload($className)
{
    $classPath = ROOT_PATH . 'src' . DIRECTORY_SEPARATOR . $className . '.class.php';
    include $classPath;
}

spl_autoload_register('autoload');
  • 首先我们声明一个 ROOT_PATH,用来帮助我们定位当前目录
  • autoload 函数,用来自动文件的函数,其中定义了加载规则,我们统一将代码放到 src 目录下,每个类文件的后缀为 .class.php
  • 通过 spl_autoload_register 来做自动加载注册,当 php 运行后如果发现当前没有要用得类就会触发 autoload 函数进行文件加载。

我们的项目目录最终结构如下:

├── autoload.php
├── index.php
└── src
    └── xxx.class.php

好了我们回到正题。很多人觉着设计模式在实际代码中离自己很远,希望看完这个系列的文章后,能够利用设计模式改善自己的代码。

最开始我们来学最简单的工厂模式,网上有很多关于工厂模式的文章,在这里,我们结合 PHP7 来进一步的演化和演进,我们写设计模式不能按照网上的通用代码,一定要结合语言和项目才能写好,很多同学在网上看到了 Java 的工厂模式,就和 Java 写的一模一样,那为什么不直接使用 Java 而使用 PHP 呢?

我们来看一个非典型的电商系统,刚起步的时候往往只有 1-2 个商品,例如只有图书,此时对的协作模式很简单,开发人员可能只有一个,编写的代码可能就是下面这个样子。

$object = new Books();

在 Book 中包含:

  1. 图书信息的维护
  2. 图书点击量
  3. 图书架构
  4. 图书的订单

如果这种规模确实这么做就可以了,随着项目越来越大,我们的库中会增加的一些其他商品除了图书还增加了酒喝狗,开发人员也越来越多。

# index.php
# 程序员 A 负责 Book 的维护迭代
$obj = new Books();
# 程序员 B 负责 Dog 的维护迭代
$obj = new Dogs();
# 程序员 C 负责 Wine 的维护迭代
$obj = new Wines();

我们大哥比方,假设每个类都有一个获取商品列表的方法:getList();

Books 的代码实现:

<?php

class Books
{
    public function getList()
    {
        return [
            ['product_id' => 101, "product_name" => "java 从入门到精通"],
            ['product_id' => 102, "product_name" => "PHP 从入门到精通"],
        ];
    }
}

Dogs 的代码实现

<?php

class Dogs
{
    public function getList()
    {
        return [
            ['product_id' => 201, "product_name" => "泰迪"],
            ['product_id' => 202, "product_name" => "金毛"],
        ];
    }
}

Wines的代码实现

<?php

class Wines
{
    public function getList()
    {
        return [
            ['product_id' => 301, "product_name" => "红酒"],
            ['product_id' => 302, "product_name" => "白酒"],
        ];
    }
}

正常业务代码中,getList 中的应该从数据库中取,这里为了演示方便直接返回数组。

如果我们做这个开发,这三个商品都应该在库中标记为商品,不过类型不同,很可能呈现的展现形式和业务逻辑是不一样的,所以需要区分成不同的商品。

这么写有没有什么问题?

如果我们把书和狗看成两个独立的子频道,开发起来是没有任何问题的。

但是假设现在有一天图书的货源出问题了,老板临时决定范式访问书的数,统一返回狗的数据。

这个时候程序员可能这么做:

<?php

include 'autoload.php';
//$object = new Books();
$obj = new Dogs();
$list = $obj->getList();

注释掉原有的代码,用新的实体类替换老的实体类,如果项目做大了,这个时候要进行这样的改动会很大。这个时候就需要涉及到工厂模式了。

工厂模式就是将我们类的实例化和取出全部有一个通过工厂方法的参数不同来取出对象,实例化对象的时候就变成了这样。

<?php

include 'autoload.php';

$object = ProductFactory::getProduct('book');
$object->getList();

实现代码如下:

<?php

class ProductFactory
{
    public static function getProduct($type)
    {

        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = false;
        }

        return $obj;
    }
}

这样工厂模式就完成了,回到刚才老板说的需求,要替代将 book 的数据提到成 dog,我们只需要修改工厂模式就可以了。

<?php

class ProductFactory
{
    public static function getProduct($type)
    {
        if ($type == "book") {
            $type = "dog";
        }

        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = false;
        }

        return $obj;
    }
}

这样一来,我们在获取图书列表的的时候就会被替代成狗的数据。