[转]mysql加密解密函数

在MySQL中,加密和压缩函数返回二进制串。对其中的许多函数而言,结果可能包含任意的字节值,如果想存储这些结果,你应该使用一个具有varbinary或者blob二进制串数据类型的列,这可避免潜在的删除尾部空白问题或者字符集转换问题。这些问题可能导致数据值的改变。一般而言,上述问题可能在你使用非二进制串数据类型(如char,varchar,text等数据类型)的情况下发生。

  • AES_ENCRYPT()和AES_DECRYPT()

AES_ENCRYPT()和AES_DECRYPT()可以加密/解密使用官方AES算法的数据。该算法使用128位密钥来编码,但用户可以将其扩展到256位。MySQL选用128位密钥,因为这样算法实现更快,而且对大多数用户而言它也足够安全了。

AES_ENCRYPT(str,key_str)函数加密一个字符串并返回一个二进制串。AES_DECRYPT(crypt_str, key_str)函数可以解密使用官方AES(Advanced Encryption Standard)算法加密的数据并返回原有字符串,输入变量可以是任意长度。如果输入变量为NULL,那么该函数返回结果也为NULL。

因为AES是一个块级算法,需要使用补白来编码非偶数长度的字符串。

  • ENCODE()和DECODE()

ENCODE(str, pass_str):该函数使用pass_str作为密码来加密字符串str,其加密的结果可以通过DECODE()函数来解密。该函数返回的结果是一个同str等长。DECODE(crypt_str, pass_str):该函数使用pass_str作为密码来解密使用ENCODE()加密后的字符串crypt_str。

  • DES_ENCRYPT()和DES_ENCRYPT()

DES_ENCRYPT(str[, {key_num|key_str}]):该函数使用三重DES算法连同给定的密钥来加密加密字符串。
DES_DECRYPT(crypt_str[, key_str]):该函数解密一个通过DES_ENCRYPT()加密的字符串,如果出现错误,该函数返回NULL。

  • COMPRESS()和UNCOMPRESS()

COMPRESS(string_to_compress):该函数压缩一个字符串并且返回一个二进制串。该函数需要MySQL已连同一个压缩库一块编译,比如zlib,否则该函数的返回值总为NULL。压缩后的字符串可以通过UNCOMPRESS()函数来解压缩。UNCOMPRESS(string_to_uncompress):该函数解压缩一个通过COMPRESS()函数压缩的字符串。如果变量不是一个压缩值,则结果返回为NULL。

  • PASSWORD()

PASSWORD(str):该函数用来加密存储在user表中password列的MySQL密码。PASSWORD()函数由MySQL服务器中的认证系统使用,用户不应该在自己的应用中使用该函数。如果需要使用加密函数,可以考虑使用MD5()或者SHA1()来代替。

其加密结果示例如下:

在MySQL的系统数据库mysql的user表中,有一个名为Password的列,其中保存由password函数加密后的user的密码数据。如下所示:

  • ENCRYPT()

ENCRYPT(str[, salt]):该函数通过使用Unix crypt()系统调用来加密str,并返回一个二进制串。其中,salt变量应该是一个包含多于两个字符的字符串。如果salt没有给定,则使用一个随机值。如果crypt()系统调用在用户的操作系统上不可用(Windows操作系统便如此),该函数返回为NULL。

  • MD5()

MD5(str):该函数计算一个字符串的128位MD5校验和,返回的结果是由32个十六进制数字组成的二进制串。如果变量为NULL,则返回为NULL。

其加密结果示例如下:

  • SHA1()/SHA():

SHA1(str)/SHA(str)函数计算字符串str的160位SHA-1校验和。返回值是一个由40个十六进制数字组成的二进制串。如果变量为NULL,则返回NULL。

 

参考:https://dev.mysql.com/doc/refman/5.5/en/encryption-functions.html

[转]关于mysql事务行锁for update实现写锁的功能

在电子商务里,经常会出现库存数量少,购买的人又特别多,大并发情况下如何确保商品数量不会被多次购买.

其实很简单,利用事务+for update就可以解决.

我们都知道for update实际上是共享锁,是可以被读取的.但是如何在执行时,不被读取呢.

简单来说:假设现在库存为1,现在有A和B同时购买

先开启一个事务

begin;

select stock from good where id=1 for update;//查询good表某个商品中stock的数量

查出来后,在程序里在判断这个stock是否为0(你用什么语言,不关我事)

最后在执行

update good set stock=stock-1 where id=1

最后在

commit

但是这个时候B也是select stock from good where id=1 for update;注意:for update不能省略..这个时候会出现被锁住,无法被读取.

所以这就能够保证了商品剩余数量为1的一致性.

[转]百度移动端前端性能优化

这一期,咱们一起聊一聊—-百度移动端首页前端速度的那些事儿

1 长什么样?

我们的业务就是 https://m.baidu.com
别以为只有一个搜索框,我们还有下面丰富的卡片内容,可以提供各式各样的服务。如图1.1

图1.1
其实整个页面的逻辑相对是比较复杂的(虽然,可能各位看官没用过)。
还有各式各样的卡片,轻轻下拉,即可看到,如图1.2

图1.2

2 面临的挑战

可能代码的量级没有很多webapp恐怖,可是“百度首页要秒开”却是一个共识,可以看到(如图2.1),在利用上了缓存的情况下,我们的首页包大小gzip后只有11.1k左右。耗时也就是500多毫秒。大部分用户“秒开”不是事儿。

图2.1

但是,我们的业务在不断的增长的同时,要维持这样的包大小,就是一门艺术了。
要快,但是我们的服务也必须万无一失,(后续我会分享百度移动端首页的前端架构设计)那么这样的优化,是如何做到的呢,又如何兼顾稳定性,架构性,与速度呢?别急,让我们把这些优化一一道来。

3 我们的业务与我们的优化

3.1 静态文件在哪里?

为了求快,首页是没有js和css外链的,这样会再发起多次请求,相信对于各位前端小能手来说,也是老生常谈的前端优化了。所以,整个首页渲染出来,只需要一次请求(除了iconfont)。其他的首屏所需要的js与css,全部在上线前,编译时,编译内联至HTML中,如图3.1.1。


图3.1.1

3.2 缓存!缓存!

然而,首页并不满足于此,首页的很多样式和脚本,需要在同步的时候就初始化,但是,如果每次都传输一些不变的静态文件或者html,实在是太浪费了,如果html/css/js一直不变,那直接缓存到客户端不就好了。
于是首页的第二项优化,就展示了威力,localstorage,关于这个客户端存储,陌生的同学可以查一查。也可以直接阅读我接下来要写的聊一聊系列文章。
我们把不变的js/css/html全部存储到localstorage中去,下次加载首页的时候。在特定的位置,不必再从服务端把特定位置的js/css/html传过来。只需要传一句话—-“<script>readlocalstorage();</script>”就行。
至于存储的方法,例子如下:

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8"/>
    </head>
    <body>
        <div data-local="test1">
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
        </div>
        <script>
            function cacheOne(attrid) {
                var content = document.querySelector('[data-local="' + attrid + '"]').outerHTML;
                localStorage.setItem(attrid, content);
            }
            cacheOne('test1');
        </script>
    </body>
</html>

我们将html的内容存储到了localstorage中,如图3.2.1


图3.2.1
下次,再访问的时候,我们使用服务端把缓存起来的html不传送,而是只传送读取相关的js,如图3.2.2

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8"/>
    </head>
    <body>
        <script type="text/javascript" data-local="test1">
            function readOne(attrid) {
                var content = localStorage.getItem(attrid);
                document.querySelector('[data-local="' + attrid + '"]').outerHTML = content;
            }
            readOne('test1');
        </script>
    </body>
</html>

图3.2.2
我们看到,虽然展示内容相同,但是第二次传输的时候,页面的量明显减小。而且使用这种方式我们使用的地方越多,这种优势就越明显。

百度移动端首页的很多css/html/js就是这样缓存在客户端的。

有同学会说,那么如何知道什么时候该传读local,什么时候该传写local呢?
很简单,我们在写入local的时候,同时在cookie中种下当前所有要缓存的内容的版本(MD5戳)就行。
因为cookie是会在同步访问的时候,传送到服务端的,而local不会,所以,我们在服务端决定要传送内容,还是传送读取local代码。就靠我们种下的cookie了。我们在这里,使用php来做一个实验:

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8"/>
    </head>
    <body>
        <?php $curversion='1'?>
        <?php if ($_COOKIE['localversion'] !== $curversion) {?> 
        <div data-local="test1">
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
            这部分内容非常多将会缓存起来
        </div>
        <script>
            function cacheOne(attrid) {
                var content = document.querySelector('[data-local="' + attrid +'"]').outerHTML
;
                localStorage.setItem(attrid, content);
            }
            cacheOne('test1');
            document.cookie="localversion=<?php echo $curversion?>;";
        </script>
        <?php } else {?>
            <script type="text/javascript" data-local="test1">
                function readOne(attrid) {
                    var content = localStorage.getItem(attrid);
                    document.querySelector('[data-local="' + attrid + '"]').outerHTML = content
;
                }
                readOne('test1');
            </script>
        <?php }?>
    </body>
</html>

我们在php中判断,如果cookie中有version,证明种过cookie,写过local,所以,不用传内容了,直接传script就好了,如果没有就要传输并且写入。我们可以看到效果,同样的页面,第一次访问的时候,内容大小是1.6K,如图3.2.3

图3.2.3
再次刷新的时候,内容量已经减小到了474b,如图3.2.4。


图3.2.4

如果用户的cookie和local不一致怎么办,如果用户不支持local怎么办?这些疑问其他读者自行思考一下(其实很简单)。

3.3 外链!外链!

毕竟业务庞大,光首屏的那些css/js/html已经无法满足我们了。我们需要更多的脚本,更多的css。你可能会很轻松的想到,外链引入呗。但是,经过调研,我们发现移动端的文件缓存率非常的低(大约30%左右)。也就是说移动端的缓存环境是非常残酷的。所以,我们又开始了极限优化。我们将所有的js/css等静态文件,通过一个接口全部返回。如图3.3.1


图3.3.1
这样可以达到合并外链请求的目的,我们又将这些静态文件,也一一缓存到localstorage中,如图3.3.2:


图3.3.2
每个文件以自己文件内容生成的版本号为戳,标识自己的唯一性。每次服务端返回页面时,会把当前在服务器上的所有静态文件版本号,返给前端,如图3.3.3


图3.3.3
前端首屏加载完成后,会用这些版本号与local中进行一一对比,发现不一致的js/css,会一起发送一个合并请求。如图3.3.1所示。这样可以保证每个文件的缓存与版本迭代。同时,也避免了过多的外链。

3.4 DOM也缓存

我们的模板和数据,也会被缓存至localstorage中,,有同学可能会问,那什么东西不缓存?答案就是,变化的东西,如果有部分html与数据在刷新的时候会经常性的变动的话,这种缓存方式就失去了它的意义,我们的宗旨是,不变的数据,缓存下来是可以带来信息量的不重复传输的。如图3.4.1


图3.4.1

3.5 使用iconfont

由于我们的很多业务是不需要多彩色图的,所以这个时候,iconfont就派上了用场,在满足UE高清的需求下,可以节省大量的资源。如图3.5.1,这些icon就可以使用iconfont。

图3.5.1

3.6 卡片的异步加载与缓存

随着我们的业务越来越多,我们的卡片也越来越多了,但是!!!依旧不能影响我们首页的速度。我们又开始了极限优化。首先,我们首屏也就需要2张卡片,按照市售手机的尺寸来看。我们两张卡片足够填充满首屏了。特殊情况,我们会有特殊处理(针对大屏幕手机),在用户下拉的时候,再去加载更多的卡片。这样可以节省用户流量,还能够提升首页速度。接下来,我们如法炮制,也将卡片内容(html/css/js)存储到了local中。异步拉取卡片的时候,如果卡片内容没有变。服务端就不要返回了。

3.7 不在首屏的就要异步化!

我们有很多用户功能,用户不一定每次都会用,如果上来就开始加载,必然会浪费速度与流量,于是,我们将一些“第二步操作”,只有在触发时才会进行加载。这样,保证了按需加载。
如,我们的管理页面功能,是个点击才能进入的浮层,对于这种功能设计,就可以采用按需加载,而不是伴随首页一起加载,如图3.7.1


图3.7.1

3.8 少量静态文件的域名

我们的logo与iconfont均是放在m.baidu.com域下的,这样节省了DNS的解析。虽然可能带来的问题是,发送图片请求的时候,会携带cookie。但是我们的cookie也是极少的。这点上性能还是有所提升的。如图3.8.1我们的logo就是在m.baidu.com域名下:


图3.8.1

3.9 极小的图片base64化

对于小于1k的图片,我们将其变为base64编码,并融入到css中,一起换存到localstorage中去,这样即节省了网络请求,同时使图片也可以缓存到local中去了。

图3.7.1

不要走开,请关注我。下一章,我们将继续聊聊web分辨率那些事儿。

https://segmentfault.com/a/1190000005884985
原创文章,版权所有,转载请注明出处

测试了下写cache


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head> </head>
<body>
<h1>test</h1>
</body>
<script>
localStorage.setItem("hello", "world");
</script>
</html>

[php]in_array函数性能问题

今天遇到一个奇怪的问题

foreach($a as $tmp) {

       if(in_array($tmp[‘test’], $b) {

              //TODO

}

}

 

$a,$b大概1W左右的int数组,结果比较非常慢

$b = array_flip($b);

foreach($a as $tmp) {

       if(isset ($b[$tmp[‘test’]]) {

              //TODO

}

}

通过isset来确定是否包含,明显快多了

isset是通过hash组织的,查询很快。而in_array是通过遍历。并且在一篇文章中提到:

http://www.server110.com/php/201309/1150.html

in_array这种松比较,会将两个字符型数字串先转换为长整型再进行比较,却不知性能就耗在这上面了。

所以在大数组比较时,非常不建议使用in_array方法。

[转]Web系统大规模并发——电商秒杀与抢购

原文链接:http://www.csdn.net/article/2014-11-28/2822858
摘要:电商的秒杀和抢购,从技术的角度来说,会对Web系统产生巨大的考验。本期

电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定至关重要。这次我们会关注秒杀和抢购的技术实现和优化,同时,从技术层面揭开,为什么我们总是不容易抢到火车票的原因?

一、大规模并发带来的挑战

在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战。如果Web系统不做针对性的优化,会轻而易举地陷入到异常状态。我们现在一起来讨论下,优化的思路和方法哈。

1. 请求接口的合理设计

一个秒杀或者抢购页面,通常分为2个部分,一个是静态的HTML等内容,另一个就是参与秒杀的Web后台请求接口。

通常静态HTML等内容,是通过CDN的部署,一般压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须能够支持高并发请求,同时,非常重要的一点,必须尽可能“快”,在最短的时间里返回用户的请求结果。为了实现尽可能快这一点,接口的后端存储使用内存级别的操作会更好一点。仍然直接面向MySQL之类的存储是不合适的,如果有这种复杂业务的需求,都建议采用异步写入。

当然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才可以从页面中看到用户是否秒杀成功。但是,这种属于“偷懒”行为,同时给用户的体验也不好,容易被用户认为是“暗箱操作”。

2. 高并发的挑战:一定要“快”

我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。

那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加。

就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。

那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

然后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

3. 重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。

二、作弊的手段:进攻与防守

秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。不少用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽可能多的请求到服务器。还有一部分高级用户,制作强大的自动请求脚本。这种做法的理由也很简单,就是在参与秒杀和抢购的请求中,自己的请求数目占比越多,成功的概率越高。

这些都是属于“作弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。

1. 同一个账号,一次性发出多个请求

部分用户通过浏览器的插件或者其他工具,在秒杀开始的时间里,以自己的账号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。

这种请求在某些没有做数据安全处理的系统里,也可能造成另外一种破坏,导致某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记录,如果没有则领取成功,最后写入到参与记录中。这是个非常简单的逻辑,但是,在高并发的场景下,存在深深的漏洞。多个并发请求通过负载均衡服务器,分配到内网的多台Web服务器,它们首先向存储发送查询请求,然后,在某个请求成功写入参与记录的时间差内,其他的请求获查询到的结果都是“没有参与记录”。这里,就存在逻辑判断被绕过的风险。

应对方案:

在程序入口处,一个账号只允许接受1个请求,其他请求过滤。不仅解决了同一个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案,可以通过Redis这种内存缓存服务,写入一个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续参加。

或者,自己实现一个服务,将同一个账号的请求放入一个队列中,处理完一个,再处理下一个。

2. 多个账号,一次性发送多个请求

很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批“僵尸账号”,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转发抽奖的活动,如果我们使用几万个“僵尸号”去混进去转发,这样就可以大大提升我们中奖的概率。

这种账号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。

应对方案:

这种场景,可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求:

  1. 弹出验证码,最核心的追求,就是分辨出真实用户。因此,大家可能经常发现,网站弹出的验证码,有些是“鬼神乱舞”的样子,有时让我们根本无法看清。他们这样做的原因,其实也是为了让验证码的图片不被轻易识别,因为强大的“自动脚本”可以通过图片识别里面的字符,然后让脚本自动填写验证码。实际上,有一些非常创新的验证码,效果会比较好,例如给你一个简单问题让你回答,或者让你完成某些简单操作(例如百度贴吧的验证码)。
  2. 直接禁止IP,实际上是有些粗暴的,因为有些真实用户的网络场景恰好是同一出口IP的,可能会有“误伤“。但是这一个做法简单高效,根据实际场景使用可以获得很好的效果。

3. 多个账号,不同IP发送不同请求

所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工作室”,发现你对单机IP请求频率有控制之后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP。

有同学会好奇,这些随机IP服务怎么来的。有一些是某些机构自己占据一批独立IP,然后做成一个随机代理IP的服务,有偿提供给这些“工作室”使用。还有一些更为黑暗一点的,就是通过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运作,只做一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口。通过这种做法,黑客就拿到了大量的独立IP,然后搭建为随机IP服务,就是为了挣钱。

应对方案:

说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想做分辨很困难。再做进一步的限制很容易“误伤“真实用户,这个时候,通常只能通过设置业务门槛高来限制这种请求了,或者通过账号行为的”数据挖掘“来提前清理掉它们。

僵尸账号也还是有一些共同特征的,例如账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号。

4. 火车票的抢购

看到这里,同学们是否明白你为什么抢不到火车票?如果你只是老老实实地去抢票,真的很难。通过多账号的方式,火车票的黄牛将很多车票的名额占据,部分强大的黄牛,在处理验证码方面,更是“技高一筹“。

高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展示验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式,验证码的保护限制作用被废除了,目前也没有很好的解决方案。

因为火车票是根据身份证实名制的,这里还有一个火车票的转让操作方式。大致的操作方式,是先用买家的身份证开启一个抢票工具,持续发送请求,黄牛账号选择退票,然后黄牛买家成功通过自己的身份证购票成功。当一列车厢没有票了的时候,是没有很多人盯着看的,况且黄牛们的抢票工具也很强大,即使让我们看见有退票,我们也不一定能抢得过他们哈。

最终,黄牛顺利将火车票转移到买家的身份证下。

解决方案:

并没有很好的解决方案,唯一可以动心思的也许是对账号数据进行“数据挖掘”,这些黄牛账号也是有一些共同特征的,例如经常抢票和退票,节假日异常活跃等等。将它们分析出来,再做进一步处理和甄别。

三、高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

1. 超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

2. 悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

3. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

4. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

四、小结

互联网正在高速发展,使用互联网服务的用户越多,高并发的场景也变得越来越多。电商秒杀和抢购,是两个比较典型的互联网高并发场景。虽然我们解决问题的具体技术方案可能千差万别,但是遇到的挑战却是相似的,因此解决问题的思路也异曲同工。

[转]谈谈Redis的SETNX

在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果,不过很多人没有意识到 SETNX 有陷阱!

比如说:某个查询数据库的接口,因为调用量比较大,所以加了缓存,并设定缓存过期后刷新,问题是当并发量比较大的时候,如果没有锁机制,那么缓存过期的瞬间,大量并发请求会穿透缓存直接查询数据库,造成雪崩效应,如果有锁机制,那么就可以控制只有一个请求去更新缓存,其它的请求视情况要么等待,要么使用过期的缓存。

下面以目前 PHP 社区里最流行的 PHPRedis 扩展为例,实现一段演示代码:

<?php

$ok = $redis->setNX($key, $value);

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

缓存过期时,通过 SetNX  获取锁,如果成功了,那么更新缓存,然后删除锁。看上去逻辑非常简单,可惜有问题:如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防不测:

<?php

$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec();

?>

因为 SetNX 不具备设置过期时间的功能,所以我们需要借助 Expire 来设置,同时我们需要把两者用 Multi/Exec 包裹起来以确保请求的原子性,以免 SetNX 成功了 Expire 却失败了。 可惜还有问题:当多个请求到达时,虽然只有一个请求的 SetNX 可以成功,但是任何一个请求的 Expire 却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求比较密集的话,那么过期时间会一直被刷新,导致锁一直有效。于是乎我们需要在保证原子性的同时,有条件的执行 Expire,接着便有了如下 Lua 代码:

local key   = KEYS[1]
local value = KEYS[2]
local ttl   = KEYS[3]

local ok = redis.call('setnx', key, value)
 
if ok == 1 then
  redis.call('expire', key, ttl)
end
 
return ok

没想到实现一个看起来很简单的功能还要用到 Lua 脚本,着实有些麻烦。其实 Redis 已经考虑到了大家的疾苦,从 2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。

<?php

$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

如上代码是完美的吗?答案是还差一点!设想一下,如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新过程中,锁就失效了,此时另一个请求会获取锁,但前一个请求在缓存更新完毕的时候,如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况,所以我们在创建锁的时候需要引入一个随机值:

<?php

$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();

    if ($redis->get($key) == $random) {
        $redis->del($key);
    }
}

?>

如此基本实现了单机锁,假如要实现分布锁,请参考:Distributed locks with Redis,这里就不深入讨论了,总结:避免掉入 SETNX 陷阱的最好方法就是永远不要使用它!

原文链接:http://huoding.com/2015/09/14/463

Sphinx的安装和使用

一直想搭一个检索的工具,刚好看到用C写的sphinx.之前安装过一阵子,但是一直没搞定。这次决定安装一次。

因为要支持中文切词,安装的方式有两种。
Sphinx + Coreseek和sphinx for Chinese
这里采用偷懒的sphinx for Chinese
一 安装sphinx for Chinese
官方链接: http://sphinxsearchcn.github.io/
1. 下载所需的安装包

2. 解压 tar -zxvf sphinx-for-chinese-0.9.9-r2117.tar.gz

3. 编译安装
$ cd sphinx-for-chinese-0.9.9-r2117.tar.gz
$ ./configure
$ make
$ sudo make install

4. 指定sphinx配置文件
$ cd /usr/local/etc
$ cp sphinx.conf.dist sphinx.conf

5. 编辑该配置文件
vi sphinx.conf
改动内容如下:
sql_host = localhost
sql_user = root
sql_pass = xxxxx
sql_db = your db
sql_port = 3306 # optional, default is 3306
说明:加粗部分是修改的内容

二 安装中文切词
到这里为止,sphinx已经可以使用了,但还不能支持中文切词,以下是加入中文切词的步骤
1. 解压字典文件 xdict_1.1.tar.gz

$ tar zxvf xdict_1.1.tar.gz
2. 借助先前安装的 mkdict 工具生成字典

$ /usr/local/sphinx/bin/mkdict xdict.txt xdict
3. 将字典 xdict 拷贝到 /usr/local/sphinx/etc目录下

4. 配置中文切词
打开 sphinx.conf文件,找到 ‘charset_type = sbcs’ 字样,将其改为:

charset_type = utf-8
chinese_dictionary = /usr/local/sphinx/etc/xdict
至此中文切词配置完成,下面做一个简单的测试
三 简单测试

1. 编辑sphinx-for-chinese自带的SQL脚本,加入中文数据
$ vi /usr/local/sphinx/etc/example.sql

REPLACE INTO test.documents ( id, group_id, group_id2, date_added, title, content ) VALUES
( 1, 1, 5, NOW(), ‘test one’, ‘this is my test document number one. also checking search within phrases.’ ),
( 2, 1, 6, NOW(), ‘test two’, ‘this is my test document number two’ ),
( 3, 2, 7, NOW(), ‘another doc’, ‘this is another group’ ),
( 4, 2, 8, NOW(), ‘doc number four’, ‘this is to test groups’ ),
( 5, 2, 8, NOW(), ‘doc number five’, ‘一个’ ),
( 6, 2, 8, NOW(), ‘doc number six’, ‘我’ ),
( 7, 2, 8, NOW(), ‘doc number seven’, ‘中国人’ );
说明:加粗部分是添加的中文测试数据

2. 导入数据

$ mysql -usphinx -psphinx < example.sql

3. 建立索引

$ sudo /usr/local/sphinx/bin/indexer –all –rotate
如果出以下错误:就给他建 一个
FATAL: failed to open /var/data/test1.spl: No such file or directory
2011-03-26 09:03
FATAL: failed to open /var/data/test1.spl: No such file or directory, will not index. Try –rotate option.

Thats not trying to read that file, but rather create it.
Does /var/data/ folder exist, and is it writable?
mkdir data
http://sphinxsearch.com/forum/view.html?id=3511

4. 检索
$ /usr/local/sphinx/bin/search 测试

words:
‘测试’: 30 documents, 187 hits
四 安装php扩展
1, 到官网http://pecl.php.net/package/sphinx下载最新的扩展
2, 安装安装扩展的步骤,
2.1 phpize
2.2 ./configure –with-php-config=/your/dir/php-config
2.3 make & make install
2.4 修改php.ini并重启
[sphinx]
extension=sphinx.so
3,测试php扩展

        $s = new SphinxClient;
        $s-&gt;setServer(&quot;localhost&quot;, 9312);
        $s-&gt;setMatchMode(SPH_MATCH_ANY);
        $s-&gt;setMaxQueryTime(3);
        $result = $s-&gt;query(&quot;海淀的山后真有可能取代中关村&quot;);
        var_dump($result);

最后遇到一个坑,php7安装sphinx扩展失败!害得我把环境重新装为php5.6,所以现在看到的所有应用都基于php5.6.不过之前有写过两个环境共存的情况,也是非常方便。
 

[转]TokuDB的特点验证

原文地址:http://www.tuicool.com/articles/vAbIFjb

随着数据量越来越大,越来越频繁的遇到需要进行结构拆分的情况,每一次拆分都耗时很久,并且需要多方配合,非常的不想搞这个事情。于是在@zolker的提醒下想到了13年开源tokuDB,来解决我们迫在眉睫的容量问题。

坊间流传tokuDB有如下几个看着令人垂涎欲滴的特点,正好符合我们实际环境的需求,故针对每个特点进行了针对性测试:

1、高压缩比,官方宣称可以达到1:12。

2、高insert性能,官方称至少比innodb高9倍。

3、可以在线添加索引和字段,速度快。


(前提:由于是为了解决线上的实际问题,故本次验证并不会按照严格的测试规范进行,所得数据也不是tokuDB的极限数据,只是在实际业务上的表现)

高压缩比:

原有数据库容量1126G,使用tokudb之后,压缩到76G,压缩比达到惊人的 14倍

高insert性能:

简单比较追同步的性能,同时在innodb和tokudb上阻塞同步3分钟,然后观察追同步的性能(不代表最大性能,因为会受限于mysql单线程追同步的瓶颈。io不会被充分利用到)

基本环境为SAS服务器,12*300G 15000转SAS盘,BP size相同。

可以明显看到在不改变基本配置,不增加多线程复制的情况下,tokudb的追同步性能高于innodb,大概在 1.9倍

在线添加字段和索引:

测试目标表为425MB,所在服务器均为10块盘做RAID5的SSD服务器。

从下图可以看出

  • 添加索引,innodb快,tokudb慢
  • 删除索引,innodb快,tokudb超快
  • 增加字段,innodb慢,tokudb超快
  • 删除字段,innodb慢,tokudb超快

TokuDB在在线DDL操作的时候相对InnoDB有较大的优势。在索引方面,删除索引基本瞬间完成。在字段方案,添加/删除字段基本都是瞬间完成。

具体我们可以看下面的操作记录。

CREATE TABLE `timeline_1009` (
  `uid` bigint(16) unsigned NOT NULL,
  `vflag` tinyint(4) unsigned NOT NULL DEFAULT '0',
  `status_id` bigint(16) unsigned NOT NULL,
  `source` int(6) unsigned NOT NULL DEFAULT '0',
  `fflag` int(6) unsigned NOT NULL DEFAULT '0',
  `mflag` tinyint(4) NOT NULL DEFAULT '0',
  PRIMARY KEY (`uid`)
) ENGINE=TokuDB DEFAULT CHARSET=utf8 ROW_FORMAT=TOKUDB_LZMA;

InnoDB操作记录:

>create index idx_flag on timeline_1009 (mflag);
Query OK, 0 rows affected (36.48 sec)

>drop index idx_flag on timeline_1009;
Query OK, 0 rows affected (0.29 sec)

>alter table timeline_1009 add column test_flag tinyint;
Query OK, 4549087 rows affected (28.64 sec)

>alter table timeline_1009 drop column test_flag;           
Query OK, 4549087 rows affected (27.29 sec)

TokuDB操作记录:

>create index idx_flag on timeline_1009(mflag);
Query OK, 0 rows affected (56.56 sec)

>drop index idx_flag on timeline_1009;
Query OK, 0 rows affected (0.05 sec)

>alter table timeline_1009 add column test_flag tinyint;
Query OK, 0 rows affected (0.01 sec)

>alter table timeline_1009 drop column test_flag;           
Query OK, 0 rows affected (0.00 sec)

innodb添加字段的时候会建立一个temp table,修改表结构后,会删除原表,并将临时表rename回原名字,所以就相当于对表进行了一次optimization,清理各种碎片,这也就是为什么add column的时候会有百万级别的rows affected。但是可以很明显的看到tokudb的rows affected为0,推测是tokudb在添加字段的时候,并不会采用innodb的方法。从官网的文档中获得的信息是,TokuDB会将添加字段的工作放在后台执行,而其快速的原因是将B-tree改为了Fractal-tree,其将随机IO替换为了顺序IO。领用Fractal-tree的特性,将HCAD命令广播到所有行上,而不是想InnoDB那样,需要open table并消耗很多的内存资源。


以上都是TokuDB的特点,接下来我们看看其对CPU、IOPS和RT的影响。以上3点是影响服务器负载和对外提供服务质量的关键数据。

测试背景,相同的slave,只不过引擎不通,在同一个时间点抓取数据,理论上认为承担的服务量相等。

CPU消耗:

理论上经过大压缩比的数据库一定会比较消耗cpu的usr态,果然从下图中我们可以看出,tokudb比innodb对usr态的cpu消耗要多,平均在 2倍 左右。

IOPS消耗:

根据官方文档的说明,相同QPS的情况下,tokudb应该比innodb消耗更少的iops。我们从下面两个图可以看出,读的IOPS并没有太大的差别,但是写的IOPS innodb比tokudb平均多消耗了 5.5倍

RT响应时间:

在响应时间上,由于整体数据库的size变小了,测试目标库innodb版本743G,tokudb版本61G。相对于BP=30G来说,明显tokudb更占优势。但是由于tokkudb的数据是经过高压缩的,在响应时间上应该还会多一部分解压的时间消耗,所以最终结果不好确认。

从测试结果看,tokudb的响应时间明显高于innodb的,其平均值大概高 2.2倍 。看来如果上了tokudb,响应时间是需要付出的代价。


总结: 

TokuDB的优点:1、高压缩比 2、高insert性能 3、增删字段秒级。

TokuDB的缺点:1、cpu usr态消耗高 2、响应时间变长。

总体来说,TokuDB的特性非常的吸引人,能解决我们很棘手的问题。但是,看上去很美的东西,一定会有坑存在,排雷将是我们下一步的重点工作,这也是决定TokuDB到底能不能真正在线上使用的关键。

[转]NGINX引入线程池 性能提升9倍

本篇文字还没深入理解,做个标记。

原文地址:https://yq.aliyun.com/articles/26635

摘要1. 引言 正如我们所知,NGINX采用了异步、事件驱动的方法来处理连接。这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作进程中处理多个连接和请求。为此,NGINX工作在非阻塞的socket模式下,并使用了epoll 和 kqueue这样有效的方法…

1. 引言

正如我们所知,NGINX采用了异步、事件驱动的方法来处理连接。这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作进程中处理多个连接和请求。为此,NGINX工作在非阻塞的socket模式下,并使用了epollkqueue这样有效的方法。

因为满负载进程的数量很少(通常每核CPU只有一个)而且恒定,所以任务切换只消耗很少的内存,而且不会浪费CPU周期。通过NGINX本身的实例,这种方法的优点已经为众人所知。NGINX可以非常好地处理百万级规模的并发请求。

0622000.jpg

每个进程都消耗额外的内存,而且每次进程间的切换都会消耗CPU周期并丢弃CPU高速缓存中的数据。

但是,异步、事件驱动方法仍然存在问题。或者,我喜欢将这一问题称为“敌兵”,这个敌兵的名字叫阻塞(blocking)。不幸的是,很多第三方模块使用了阻塞调用,然而用户(有时甚至是模块的开发者)并不知道阻塞的缺点。阻塞操作可以毁掉NGINX的性能,我们必须不惜一切代价避免使用阻塞。

即使在当前官方的NGINX代码中,依然无法在全部场景中避免使用阻塞,NGINX1.7.11中实现的线程池机制解决了这个问题。我们将在后面讲述这个线程池是什么以及该如何使用。现在,让我们先和我们的“敌兵”进行一次面对面的碰撞。

2. 问题

首先,为了更好地理解这一问题,我们用几句话说明下NGINX是如何工作的。

通常情况下,NGINX是一个事件处理器,即一个接收来自内核的所有连接事件的信息,然后向操作系统发出做什么指令的控制器。实际上,NGINX干了编排操作系统的全部脏活累活,而操作系统做的是读取和发送字节这样的日常工作。所以,对于NGINX来说,快速和及时的响应是非常重要的。

0622001.png

工作进程监听并处理来自内核的事件

事件可以是超时、socket读写就绪的通知,或者发生错误的通知。NGINX接收大量的事件,然后一个接一个地处理它们,并执行必要的操作。因此,所有的处理过程是通过一个线程中的队列,在一个简单循环中完成的。NGINX从队列中取出一个事件并对其做出响应,比如读写socket。在多数情况下,这种方式是非常快的(也许只需要几个CPU周期,将一些数据复制到内存中),NGINX可以在一瞬间处理掉队列中的所有事件。

0622002.jpg

所有处理过程是在一个简单的循环中,由一个线程完成

但是,如果NGINX要处理的操作是一些又长又重的操作,又会发生什么呢?整个事件处理循环将会卡住,等待这个操作执行完毕。

因此,所谓“阻塞操作”是指任何导致事件处理循环显著停止一段时间的操作。操作可以由于各种原因成为阻塞操作。例如,NGINX可能因长时间、CPU密集型处理,或者可能等待访问某个资源(比如硬盘,或者一个互斥体,亦或要从处于同步方式的数据库获得相应的库函数调用等)而繁忙。关键是在处理这样的操作期间,工作进程无法做其他事情或者处理其他事件,即使有更多的可用系统资源可以被队列中的一些事件所利用。

我们来打个比方,一个商店的营业员要接待他面前排起的一长队顾客。队伍中的第一位顾客想要的某件商品不在店里而在仓库中。这位营业员跑去仓库把东西拿来。现在整个队伍必须为这样的配货方式等待数个小时,队伍中的每个人都很不爽。你可以想见人们的反应吧?队伍中每个人的等待时间都要增加这些时间,除非他们要买的东西就在店里。

0622003.jpg

队伍中的每个人不得不等待第一个人的购买

在NGINX中会发生几乎同样的情况,比如当读取一个文件的时候,如果该文件没有缓存在内存中,就要从磁盘上读取。从磁盘(特别是旋转式的磁盘)读取是很慢的,而当队列中等待的其他请求可能不需要访问磁盘时,它们也得被迫等待。导致的结果是,延迟增加并且系统资源没有得到充分利用。

0622004.png

一个阻塞操作足以显著地延缓所有接下来的操作

一些操作系统为读写文件提供了异步接口,NGINX可以使用这样的接口(见AIO指令)。FreeBSD就是个很好的例子。不幸的是,我们不能在Linux上得到相同的福利。虽然Linux为读取文件提供了一种异步接口,但是存在明显的缺点。其中之一是要求文件访问和缓冲要对齐,但NGINX很好地处理了这个问题。但是,另一个缺点更糟糕。异步接口要求文件描述符中要设置O_DIRECT标记,就是说任何对文件的访问都将绕过内存中的缓存,这增加了磁盘的负载。在很多场景中,这都绝对不是最佳选择。

为了有针对性地解决这一问题,在NGINX 1.7.11中引入了线程池。默认情况下,NGINX+还没有包含线程池,但是如果你想试试的话,可以联系销售人员,NGINX+ R6是一个已经启用了线程池的构建版本。

现在,让我们走进线程池,看看它是什么以及如何工作的。

3. 线程池

让我们回到那个可怜的,要从大老远的仓库去配货的售货员那儿。这回,他已经变聪明了(或者也许是在一群愤怒的顾客教训了一番之后,他才变得聪明的?),雇用了一个配货服务团队。现在,当任何人要买的东西在大老远的仓库时,他不再亲自去仓库了,只需要将订单丢给配货服务,他们将处理订单,同时,我们的售货员依然可以继续为其他顾客服务。因此,只有那些要买仓库里东西的顾客需要等待配货,其他顾客可以得到即时服务。

0622005.jpg

传递订单给配货服务不会阻塞队伍

对NGINX而言,线程池执行的就是配货服务的功能。它由一个任务队列和一组处理这个队列的线程组成。
当工作进程需要执行一个潜在的长操作时,工作进程不再自己执行这个操作,而是将任务放到线程池队列中,任何空闲的线程都可以从队列中获取并执行这个任务。

0622006.jpg

工作进程将阻塞操作卸给线程池

那么,这就像我们有了另外一个队列。是这样的,但是在这个场景中,队列受限于特殊的资源。磁盘的读取速度不能比磁盘产生数据的速度快。不管怎么说,至少现在磁盘不再延误其他事件,只有访问文件的请求需要等待。

“从磁盘读取”这个操作通常是阻塞操作最常见的示例,但是实际上,NGINX中实现的线程池可用于处理任何不适合在主循环中执行的任务。

目前,卸载到线程池中执行的两个基本操作是大多数操作系统中的read()系统调用和Linux中的sendfile()。接下来,我们将对线程池进行测试(test)和基准测试(benchmark),在未来的版本中,如果有明显的优势,我们可能会卸载其他操作到线程池中。

4. 基准测试

现在让我们从理论过度到实践。我们将进行一次模拟基准测试(synthetic benchmark),模拟在阻塞操作和非阻塞操作的最差混合条件下,使用线程池的效果。

另外,我们需要一个内存肯定放不下的数据集。在一台48GB内存的机器上,我们已经产生了每文件大小为4MB的随机数据,总共256GB,然后配置NGINX,版本为1.9.0。

配置很简单:

worker_processes 16;

events {
    accept_mutex off;
}

http {
    include mime.types;
    default_type application/octet-stream;

    access_log off;
    sendfile on;
    sendfile_max_chunk 512k;

    server {
        listen 8000;

        location / {
            root /storage;
        }
    }
}

如上所示,为了达到更好的性能,我们调整了几个参数:禁用了loggingaccept_mutex,同时,启用了sendfile并设置了sendfile_max_chunk的大小。最后一个指令可以减少阻塞调用sendfile()所花费的最长时间,因为NGINX不会尝试一次将整个文件发送出去,而是每次发送大小为512KB的块数据。

这台测试服务器有2个Intel Xeon E5645处理器(共计:12核、24超线程)和10-Gbps的网络接口。磁盘子系统是由4块西部数据WD1003FBYX 磁盘组成的RAID10阵列。所有这些硬件由Ubuntu服务器14.04.1 LTS供电。

0622007.jpg

为基准测试配置负载生成器和NGINX

客户端有2台服务器,它们的规格相同。在其中一台上,在wrk中使用Lua脚本创建了负载程序。脚本使用200个并行连接向服务器请求文件,每个请求都可能未命中缓存而从磁盘阻塞读取。我们将这种负载称作随机负载

在另一台客户端机器上,我们将运行wrk的另一个副本,使用50个并行连接多次请求同一个文件。因为这个文件将被频繁地访问,所以它会一直驻留在内存中。在正常情况下,NGINX能够非常快速地服务这些请求,但是如果工作进程被其他请求阻塞的话,性能将会下降。我们将这种负载称作恒定负载

性能将由服务器上ifstat监测的吞吐率(throughput)和从第二台客户端获取的wrk结果来度量。

现在,没有使用线程池的第一次运行将不会带给我们非常振奋的结果:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
5531.24  1.03e+06
4855.23  812922.7
5994.66  1.07e+06
5476.27  981529.3
6353.62  1.12e+06
5166.17  892770.3
5522.81  978540.8
6208.10  985466.7
6370.79  1.12e+06
6123.33  1.07e+06

如上所示,使用这种配置,服务器产生的总流量约为1Gbps。从下面所示的top输出,我们可以看到,工作进程的大部分时间花在阻塞I/O上(它们处于top的D状态):

top - 10:40:47 up 11 days,  1:32,  1 user,  load average: 49.61, 45.77 62.89
Tasks: 375 total,  2 running, 373 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 67.7 id, 31.9 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:  49453440 total, 49149308 used,   304132 free,    98780 buffers
KiB Swap: 10474236 total,    20124 used, 10454112 free, 46903412 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4639 vbart    20   0   47180  28152     496 D   0.7  0.1  0:00.17 nginx
 4632 vbart    20   0   47180  28196     536 D   0.3  0.1  0:00.11 nginx
 4633 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.11 nginx
 4635 vbart    20   0   47180  28136     480 D   0.3  0.1  0:00.12 nginx
 4636 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.14 nginx
 4637 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.10 nginx
 4638 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4640 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4641 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4642 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.11 nginx
 4643 vbart    20   0   47180  28276     536 D   0.3  0.1  0:00.29 nginx
 4644 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.11 nginx
 4645 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.17 nginx
 4646 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4647 vbart    20   0   47180  28208     532 D   0.3  0.1  0:00.17 nginx
 4631 vbart    20   0   47180    756     252 S   0.0  0.1  0:00.00 nginx
 4634 vbart    20   0   47180  28208     536 D   0.0  0.1  0:00.11 nginx
 4648 vbart    20   0   25232   1956    1160 R   0.0  0.0  0:00.08 top
25921 vbart    20   0  121956   2232    1056 S   0.0  0.0  0:01.97 sshd
25923 vbart    20   0   40304   4160    2208 S   0.0  0.0  0:00.53 zsh

在这种情况下,吞吐率受限于磁盘子系统,而CPU在大部分时间里是空闲的。从wrk获得的结果也非常低:

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg    Stdev     Max  +/- Stdev
    Latency     7.42s  5.31s   24.41s   74.73%
    Req/Sec     0.15    0.36     1.00    84.62%
  488 requests in 1.01m, 2.01GB read
Requests/sec:      8.08
Transfer/sec:     34.07MB

请记住,文件是从内存送达的!第一个客户端的200个连接创建的随机负载,使服务器端的全部的工作进程忙于从磁盘读取文件,因此产生了过大的延迟,并且无法在合理的时间内处理我们的请求。

现在,我们的线程池要登场了。为此,我们只需在location块中添加aio threads指令:

location / {
    root /storage;
    aio threads;
}

接着,执行NGINX reload重新加载配置。

然后,我们重复上述的测试:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
60915.19  9.51e+06
59978.89  9.51e+06
60122.38  9.51e+06
61179.06  9.51e+06
61798.40  9.51e+06
57072.97  9.50e+06
56072.61  9.51e+06
61279.63  9.51e+06
61243.54  9.51e+06
59632.50  9.50e+06

现在,我们的服务器产生的流量是9.5Gbps,相比之下,没有使用线程池时只有约1Gbps!

理论上还可以产生更多的流量,但是这已经达到了机器的最大网络吞吐能力,所以在这次NGINX的测试中,NGINX受限于网络接口。工作进程的大部分时间只是休眠和等待新的事件(它们处于top的S状态):

top - 10:43:17 up 11 days,  1:35,  1 user,  load average: 172.71, 93.84, 77.90
Tasks: 376 total,  1 running, 375 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.2 us,  1.2 sy,  0.0 ni, 34.8 id, 61.5 wa,  0.0 hi,  2.3 si,  0.0 st
KiB Mem:  49453440 total, 49096836 used,   356604 free,    97236 buffers
KiB Swap: 10474236 total,    22860 used, 10451376 free, 46836580 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4654 vbart    20   0  309708  28844     596 S   9.0  0.1  0:08.65 nginx
 4660 vbart    20   0  309748  28920     596 S   6.6  0.1  0:14.82 nginx
 4658 vbart    20   0  309452  28424     520 S   4.3  0.1  0:01.40 nginx
 4663 vbart    20   0  309452  28476     572 S   4.3  0.1  0:01.32 nginx
 4667 vbart    20   0  309584  28712     588 S   3.7  0.1  0:05.19 nginx
 4656 vbart    20   0  309452  28476     572 S   3.3  0.1  0:01.84 nginx
 4664 vbart    20   0  309452  28428     524 S   3.3  0.1  0:01.29 nginx
 4652 vbart    20   0  309452  28476     572 S   3.0  0.1  0:01.46 nginx
 4662 vbart    20   0  309552  28700     596 S   2.7  0.1  0:05.92 nginx
 4661 vbart    20   0  309464  28636     596 S   2.3  0.1  0:01.59 nginx
 4653 vbart    20   0  309452  28476     572 S   1.7  0.1  0:01.70 nginx
 4666 vbart    20   0  309452  28428     524 S   1.3  0.1  0:01.63 nginx
 4657 vbart    20   0  309584  28696     592 S   1.0  0.1  0:00.64 nginx
 4655 vbart    20   0  30958   28476     572 S   0.7  0.1  0:02.81 nginx
 4659 vbart    20   0  309452  28468     564 S   0.3  0.1  0:01.20 nginx
 4665 vbart    20   0  309452  28476     572 S   0.3  0.1  0:00.71 nginx
 5180 vbart    20   0   25232   1952    1156 R   0.0  0.0  0:00.45 top
 4651 vbart    20   0   20032    752     252 S   0.0  0.0  0:00.00 nginx
25921 vbart    20   0  121956   2176    1000 S   0.0  0.0  0:01.98 sshd
25923 vbart    20   0   40304   3840    2208 S   0.0  0.0  0:00.54 zsh

如上所示,基准测试中还有大量的CPU资源剩余。

wrk的结果如下:

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg      Stdev     Max  +/- Stdev
    Latency   226.32ms  392.76ms   1.72s   93.48%
    Req/Sec    20.02     10.84    59.00    65.91%
  15045 requests in 1.00m, 58.86GB read
Requests/sec:    250.57
Transfer/sec:      0.98GB

服务器处理4MB文件的平均时间从7.42秒降到226.32毫秒(减少了33倍),每秒请求处理数提升了31倍(250 vs 8)!

对此,我们的解释是请求不再因为工作进程被阻塞在读文件,而滞留在事件队列中,等待处理,它们可以被空闲的进程处理掉。只要磁盘子系统能做到最好,就能服务好第一个客户端上的随机负载,NGINX可以使用剩余的CPU资源和网络容量,从内存中读取,以服务于上述的第二个客户端的请求。

5. 依然没有银弹

在抛出我们对阻塞操作的担忧并给出一些令人振奋的结果后,可能大部分人已经打算在你的服务器上配置线程池了。先别着急。

实际上,最幸运的情况是,读取和发送文件操作不去处理缓慢的硬盘驱动器。如果我们有足够多的内存来存储数据集,那么操作系统将会足够聪明地在被称作“页面缓存”的地方,缓存频繁使用的文件。

“页面缓存”的效果很好,可以让NGINX在几乎所有常见的用例中展示优异的性能。从页面缓存中读取比较快,没有人会说这种操作是“阻塞”。而另一方面,卸载任务到一个线程池是有一定开销的。

因此,如果内存有合理的大小并且待处理的数据集不是很大的话,那么无需使用线程池,NGINX已经工作在最优化的方式下。

卸载读操作到线程池是一种适用于非常特殊任务的技术。只有当经常请求的内容的大小,不适合操作系统的虚拟机缓存时,这种技术才是最有用的。至于可能适用的场景,比如,基于NGINX的高负载流媒体服务器。这正是我们已经模拟的基准测试的场景。

我们如果可以改进卸载读操作到线程池,将会非常有意义。我们只需要知道所需的文件数据是否在内存中,只有不在内存中时,读操作才应该卸载到一个单独的线程中。

再回到售货员那个比喻的场景中,这回,售货员不知道要买的商品是否在店里,他必须要么总是将所有的订单提交给配货服务,要么总是亲自处理它们。

人艰不拆,操作系统缺少这样的功能。第一次尝试是在2010年,人们试图将这一功能添加到Linux作为fincore()系统调用,但是没有成功。后来还有一些尝试,是使用RWF_NONBLOCK标记作为preadv2()系统调用来实现这一功能(详情见LWN.net上的非阻塞缓冲文件读取操作异步缓冲读操作)。但所有这些补丁的命运目前还不明朗。悲催的是,这些补丁尚没有被内核接受的主要原因,貌似是因为旷日持久的撕逼大战(bikeshedding)。

另一方面,FreeBSD的用户完全不必担心。FreeBSD已经具备足够好的异步读取文件接口,我们应该用这个接口而不是线程池。

6. 配置线程池

所以,如果你确信在你的场景中使用线程池可以带来好处,那么现在是时候深入了解线程池的配置了。

线程池的配置非常简单、灵活。首先,获取NGINX 1.7.11或更高版本的源代码,使用--with-threads配置参数编译。在最简单的场景中,配置看起来很朴实。我们只需要在httpserver,或者location上下文中包含aio threads指令即可:

aio threads;

这是线程池的最简配置。实际上的精简版本示例如下:

thread_pool default threads=32 max_queue=65536;
aio threads=default;

这里定义了一个名为“default”,包含32个线程,任务队列最多支持65536个请求的线程池。如果任务队列过载,NGINX将输出如下错误日志并拒绝请求:

thread pool "NAME" queue overflow: N tasks waiting

错误输出意味着线程处理作业的速度有可能低于任务入队的速度了。你可以尝试增加队列的最大值,但是如果这无济于事,那么这说明你的系统没有能力处理如此多的请求了。

正如你已经注意到的,你可以使用thread_pool指令,配置线程的数量、队列的最大值,以及线程池的名称。最后要说明的是,可以配置多个独立的线程池,将它们置于不同的配置文件中,用做不同的目的:

http {
    thread_pool one threads=128 max_queue=0;
    thread_pool two threads=32;

    server {
        location /one {
            aio threads=one;
        }

        location /two {
            aio threads=two;
        }
    }
…
}

如果没有指定max_queue参数的值,默认使用的值是65536。如上所示,可以设置max_queue为0。在这种情况下,线程池将使用配置中全部数量的线程,尽可能地同时处理多个任务;队列中不会有等待的任务。

现在,假设我们有一台服务器,挂了3块硬盘,我们希望把该服务器用作“缓存代理”,缓存后端服务器的全部响应信息。预期的缓存数据量远大于可用的内存。它实际上是我们个人CDN的一个缓存节点。毫无疑问,在这种情况下,最重要的事情是发挥硬盘的最大性能。

我们的选择之一是配置一个RAID阵列。这种方法毁誉参半,现在,有了NGINX,我们可以有其他的选择:

# 我们假设每块硬盘挂载在相应的目录中:/mnt/disk1、/mnt/disk2、/mnt/disk3

proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G
                 use_temp_path=off;
proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G
                 use_temp_path=off;
proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G
                 use_temp_path=off;

thread_pool pool_1 threads=16;
thread_pool pool_2 threads=16;
thread_pool pool_3 threads=16;

split_clients $request_uri $disk {
    33.3%     1;
    33.3%     2;
    *         3;
}

location / {
    proxy_pass http://backend;
    proxy_cache_key $request_uri;
    proxy_cache cache_$disk;
    aio threads=pool_$disk;
    sendfile on;
}

在这份配置中,使用了3个独立的缓存,每个缓存专用一块硬盘,另外,3个独立的线程池也各自专用一块硬盘。

缓存之间(其结果就是磁盘之间)的负载均衡使用split_clients模块,split_clients非常适用于这个任务。

proxy_cache_path指令中设置use_temp_path=off,表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了避免在更新缓存时,磁盘之间互相复制响应数据。

这些调优将带给我们磁盘子系统的最大性能,因为NGINX通过单独的线程池并行且独立地与每块磁盘交互。每块磁盘由16个独立线程和读取和发送文件专用任务队列提供服务。

我敢打赌,你的客户喜欢这种量身定制的方法。请确保你的磁盘也持有同样的观点。

这个示例很好地证明了NGINX可以为硬件专门调优的灵活性。这就像你给NGINX下了一道命令,让机器和数据用最佳姿势来搞基。而且,通过NGINX在用户空间中细粒度的调优,我们可以确保软件、操作系统和硬件工作在最优模式下,尽可能有效地利用系统资源。

7. 总结

综上所述,线程池是一个伟大的功能,将NGINX推向了新的性能水平,除掉了一个众所周知的长期危害——阻塞——尤其是当我们真正面对大量内容的时候。

甚至,还有更多的惊喜。正如前面提到的,这个全新的接口,有可能没有任何性能损失地卸载任何长期阻塞操作。NGINX在拥有大量的新模块和新功能方面,开辟了一方新天地。许多流行的库仍然没有提供异步非阻塞接口,此前,这使得它们无法与NGINX兼容。我们可以花大量的时间和资源,去开发我们自己的无阻塞原型库,但这么做始终都是值得的吗?现在,有了线程池,我们可以相对容易地使用这些库,而不会影响这些模块的性能。

[转]HTTP/2 对 Web 性能的影响

【上】

一.前言

HTTP/2 于 2015 年 5 月正式推出。诞生以来,它就一直在影响着网络性能最佳实践。在本篇文章中,我们将讨论 HTTP/2 的二进制帧、延迟削减、潜在利弊以及相应的应对措施。
超文本传输协议(简称 HTTP)正是万维网与网络空间的基石。现在,HTTP 听起来已经有些过时,毕竟该协议中使用最广泛的版本——HTTP 1.1,已快迎来它的第二十个年头。时光回溯到1997年,HTTP1.1刚刚出现的年代,当时,软驱与调制解调器还是 PC 设备的必备周边产品,Java 也仅仅是一个刚刚崭露头角的、前途一片光明的编程语言。
而今,2015 年 5 月,HTTP/2 正式亮相,致力于解决 HTTP 1.1 在现代网络时代下无法应对的某些重大性能难题。过去一年以来,越来越多的浏览器、Web 服务、商用代理以及主要内容交付网络开始支持 HTTP/2。
遗憾的是,对于负责编写 Web 代码的开发人员而言,HTTP/2 的过渡工作并不简单,所谓的速度提升也不会自行出现,必要的 Web APM 工具和厂商仍然是当今时代不可或缺的,例如 OneAPM Browser Insight、Newrelic、APPdynamic 等等。
如何才能采用这个新协议打造高性能 Web 应用,该怎样处理那些尚不支持该协议的现有工具——例如 debug 代理工具,这些问题对所有人来说仍是一个挑战。今天的文章将向您介绍 HTTP/2,以及其如何改变 Web 性能的最佳实践。

二.二进制帧:HTTP/2的「基本单位」

HTTP 1.1 的一大优势(至少相较于非安全连接而言)在于其支持在端口 80 的telnet 会话中利用文本与 Web 服务器进行交互:在大多数 Web 服务器上输入 GET/HTTP/1.1,都能返回一个 HTML 文档。由于这是一项文本协议,因此其调试工作相对简单。
相对于 HTTP1.1 这个文本协议,HTTP/2 中的请求与响应则通过二进制帧流的形式来表现,我们将其称为 HTTP/2 RFC 中的「基本协议单位」。每一帧都拥有自己的类型,用于实现不同的作用。考虑到 HTTP 1.1 将会「永远」存在(毕竟 Gopher 协议都还在用!),因此 HTTP/2 的作者们要求,HTTP/2 请求的二进制帧都必须被映射到 HTTP 1.1 请求上去,从而确保其向下兼容的能力。
当然,HTTP/2 当中也有一些其他新特性,无法映射至 HTTP 1.1。例如,服务器推送(也叫“缓存推送”)和流重置都是利用二进制帧类型实现的新特性。帧也可以支持优先级排序,允许客户端向服务器发送排序,从而优先处理一部分资产类别。
除了使用 Wireshark 2.0 之外,对个别二进制帧进行查看的最简便方法就是利用谷歌 Chrome 浏览器的 net-internals 标签(在浏览器地址栏中输入chrome://net-internals/#http2)。由于理解大型网页的数据通常比较困难,Rebecca Murphey 特意编写了一款极为实用的可视化工具,从而将其显示在命令行中。
除此之外,这个用于获取资产的协议还可以显示在 Chrome Web 的开发者工具当中–只需右键点击列标题,接着选择「协议」即可:

在谷歌Chrome开发者工具中查看协议类型。
这里列出的所有 HTTP/2 请求都使用通过传输层安全(TLS)建立的安全连接。各主流浏览器都要求 HTTP/2 连接是安全的。这样做是有切实理由的:TLS 的一套称为应用层协议协商(ALPN)的扩展,让服务器知道浏览器支持 HTTP2(除了其他协议以外),从而避免了额外的数据往来。这也能保住那些无法解读 HTTP/2 的服务,例如代理——只看得见传输线路上的加密数据。

三.多路复用减少延迟

HTTP 1.1 的一大问题是延迟,换而言之就是它花在提出请求和接受响应上的时间。随着典型网页中图片数量、JavaScript 和 CSS 的使用量不断增加,这个问题日益严重。每获取一项资产,通常都得新建一个 TCP 连接。
这种需求因为两个理由很重要:每台主机能同时打开的 TCP 连接数受浏览器的限制;新建连接都会引发性能损失。如果物理服务器离用户很远(如:一位新加坡用户向美国东海岸数据中心请求一个页面),延迟会变多。这不是罕见的情况——近期一份报告表示全球 70% 以上的互联网流通都会通过北佛吉尼亚一些不知名的数据中心。
其实对于 Web 页面的优化从前端页面方面反而可能见效比较大,我们都知道页面资源对响应速度的影响是非常巨大的,常见的图片压缩、css 聚合都可以帮助我们优化 Web 性能,问题就是如何找到相应的页面慢加载元素。
这也是国内外 APM 行业兴起的最初原因之一。
拿国内的一个页面优化工具 Browser Insight 举例,这种通过页面插码来获取真实用户体验的 APM 工具,虽然部署起来有些麻烦,但是这类的工具也没有更好的部署方法,手动的反而更稳定。

HTTP 1.1 提供了多种方案以解决延迟问题,包括通道传输与 Keep-Alive header。然而,通道传输从未被广泛采纳过,而 Keep-Alive header 则饱受 head line 阻塞的困扰:只有在当前请求必须彻底完成后,下一请求才能被发送出去。
在 HTTP/2 当中,多条资产请求可以重复利用单一 TCP 连接。与使用 Keep-Alive header 的 HTTP 1.1 请求不同,HTTP/2 的请求与响应二进制帧以交错方式进行,线头阻塞问题也不复存在。建立连接的成本(即著名的的‘三方握手’)在每台主机上只进行一次。多路复用对于安全连接来说尤为重要,因为多次 TLS 协商方案的性能成本将会得到显著提高。
在 HTTP/2 中,单一主机内的多资产请求只使用单一 TCP 连接。

四.总结

其实 Web 性能优化已经不是一个新的话题了,从 21 世纪初期直到现在,很多成熟的互联网公司已经开始关注除了产品、研发之外的工作,例如用户体验、性能优化等与产品使用者息息相关的事情,伴随着的就是 APM 行业的全面兴起。
本文主要和大家聊了一些关于 http1 和 http2 有关的基础内容,之后还会有一篇,预计与大家分享一些 http2 使用利弊、以及正在进行的相关工作等等。
Browser Insight 是一个基于真实用户的 Web 前端性能监控平台,能够帮大家定位网站性能瓶颈,网站加速效果可视化;支持浏览器、微信、App 浏览 HTML 和 HTML5 页面。想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客
【下】

一.前言

我们在 HTTP/2 对 Web 性能的影响(上)已经和大家分享了一些关于 Http2 的二项制帧、多用复路以及 APM 工具等,本文作为姊妹篇,主要从 http2 对 Web 性能的影响、http2 使用的利弊以及一些正在进行中的相关工作等方面与大家进行分享。

二.Web 性能影响:与内联、级联及图像精灵说再见?

HTTP/2 多路复用对前端 Web 开发人员造成了深远的影响。长久以来,人们用尽方法,试图通过捆绑相关资产来削减连接的数量,而现在这一切都不需要了。人们曾经尝试过的方法包括:

  • JavaScript 与 CSS 文件级联:将多个小文件合成一个大文件,从而降低总体请求数量。
  • 图像精灵:将多个小图像合成一张大图像。
  • 域名分片:在多个域之间发送静态资产请求,从而增加浏览器所能允许的总体开放 TCP 连接数量。
  • 内联资产:将资产同 HTML 文档源绑定,包括 base-64 编码图片,以及直接写入 script 标签的 JavaScript 代码。

因为不需要再绑定资产,我们就有了更多机会将 Web 应用程序中的小片段加以缓存。举个例子可以帮我们更好的理解这一点:
一个级联且指纹验证型 CSS 文件被解绑为四个较小的指纹验证文件。
常见的级联模式是将一个应用程序内不同页面的样式表文件进行绑定,形成单一的 CSS 文件,以减少资产请求的数量。这个大文件随后会通过文件名内的 MD5 哈希值进行指纹校验,确保其能够被浏览器主动缓存。遗憾的是,这样的解决方案意味着,当站点的可视化布局中出现了任何一点小的改变,如标题字体的改变,都需要重新下载整个级联文件。
当对小型资产文件进行指纹校验时,相当一部分的 JavaScript 与 CSS 组件都不会频繁产生变动,因此可以被浏览器缓存 –也就是说,任何一个单一功能的小型重构,不会再导致大量的 JavaScript 应用程序代码或者 CSS 失效。
最后,级联机制的消失能够降低前端构建基础的复杂性。与以往通过一系列预置步骤来级联资产不同,现在它们作为小型文件,可以直接被放入 HTML 文档中。

三.实际使用 HTTP/2 的潜在弊端

仅仅针对支持 HTTP/2 客户端而做出的各类优化,意味着那些不支持 HTTP/2 的浏览器可能因此陷入不利境地。那些“有年头”的浏览器们仍然倾向于绑定资产,以此降低连接数量。截至 2016 年 2 月,caniuse.com 网站报道称,全球浏览器中能够支持 HTTP/2 的占比 71%。与之前浏览器们决定放弃支持 IE 8.0 时一样,支持 HTTP/2 或采取某种混合作业的方式——这样的决定只能根据各个网站身的相关数据来做出。
但是我们相信大规模支持肯定是不可避免的,就像一开始只有 Chrome 浏览器支持 window.performance 接口,方便一些 Web 工具进行数据的采集,像上面说过的Browser Insight ,我曾经和他们的技术支持聊过,就是靠这种方式来实时的采集用户对网站的访问信息等。之后,大势所趋,各个浏览器厂商都纷纷开放了相关接口
正如可汗学院的博文所述,他们曾分析其网站上的 HTTP/2 流量,事实上,拆分大量资产会增加所传输字节的总量。而使用zlib压缩单一大型文件,比压缩多个小型文件要更有效率。对于拥有成百上千解绑资产的 HTTP/2 站点来说,这种效应更为显著。
在浏览器中使用 HTTP/2 还要求我们通过 TLS 进行资产传递。对于菜鸟们来说,设置 TLS 证书就是个烦人的活儿。幸运的是,诸如 Let’s Encrypt 的开源项目正努力让证书注册工作变得更加便捷。

四.仍在进行中的工作

大部分用户并不在意你的站点用了啥协议——他们只想要它速度快,运行如人预期。虽然 HTTP/2 已经获得正式批准快一年了,开发人员还在学习如何利用它来建立更快速网站的最优实践。换用 HTTP/2 的好处更多取决于具体站点的架构情况以及使用现代浏览器的用户比率。再有就是,调试新协议很有挑战性,更易用的开发工具还在研制中。
虽然有这些挑战,HTTP/2 的采纳度仍在增加。根据研究人员扫描流行网站属性的结果,排名前列的站点中使用 HTTP/2 的一直在增加,特别是 CloudFlare 和WordPress 在 2015 年宣布提供支持之后。在考虑转换到新协议时,很重要的一点是利用 Browser insight 和 NewRelic 之类的 APM 工具,仔细测量资源和页面在不同环境下的加载时间。
如下图所示,可以看到每一次慢加载的详细情况,非常方便。

供应商和专业网站人员都熟悉这一转换背后的含义,从真实用户数据中做出判断才是关键的。在网站臃肿危机的当下,无论何种协议,都应该以削减资源数量为目标。
在此 HTTP/2 系列的第二部分中,我们会聚焦于如何在服务上实现 HTTP/2 和调试真实网络通信的具体实现细节。
本文作者为 Clay Smith,由 OneAPM 产品运营进行翻译编辑
原文地址:https://dzone.com/articles/how-http2-is-changing-web-performance-best-practic
Browser Insight 是一个基于真实用户的 Web 前端性能监控平台,能够帮大家定位网站性能瓶颈,网站加速效果可视化;支持浏览器、微信、App 浏览 HTML 和 HTML5 页面。想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客