226
京东网上商城
速卖通(aliexpress)活动页面优化第二轮,qps大幅提高
回顾
话说在以前文章中,我小小的吹了一个牛,要让qps飞起来,但是在第一次的优化中,我们也只是让qps从170左右上升到了500左右,但是这并没有让qps并没有“飞”起来。
而且我发现在我们的场景下gzip,已经成为势要解决的最大问题,于是我花了一段时间,对我们活动页面的场景做了一系列降低压缩级别的测试测试,测试结果如下:
通过一系列的计算,最终发现压缩级别降低带来的机器成本的下降,和带宽带来的提升带来的成本上升相互抵消了。
把gzip去掉,或者降低压缩级别会提高我们的带宽消耗,经过详细的压测(对不同大小的页面,通过不断的调整压缩级别)和计算,降低压缩级别带来的qps的提高所节约的机器费用和带宽提升所带来的费用提升几乎相互抵消(机器按照3年折旧来算),所以降低压缩级别看上去很美,但是却没法操作。由于某些数字比较敏感,所以这里就不给出详细的计算公式了。
同时由于降低压缩级别,导致压缩之后的页面大出了几KB,极有可能导致在网络通信时导致RT中增加新的RTT出来,也许这在国内的网络环境中影响不是特别大,但是对于世界范围内的网络环境,一个RTT有可能到达数百毫秒,对RT还是有一定的影响的,这一点,后面还会阐述。
节约的机器的钱和带宽增加带来的成本投入是差不多的。而且降低压缩级别导致的RT上升也是对国际友人的用户体验是不友好的,尤其是目前中俄局势的大环境下,我们更要考虑俄罗斯妹子的体验,所以降低压缩级别在我们的场景里也是不可靠的。
思路和问题
如果说压缩是不可避免的,那么我们怎么做这个优化呢,我们能否将压缩提前做掉呢,如果我提前压缩好,放在内存中,用户请求过的时候直接返回内存中的数据,岂不妙哉?直接把CPU密集型应用改造成了IO密集型应用,这个应该很有意思。
看起来很美的一个方案哦,但是对我这种php都没有入门的同学来说,我还是得好好研究一番,现在,于是我用大脚拇指想了一下,这一想就想出了好几个问题。
- php是进程模型,压缩过的数据我应该放哪里
- 如果是预先压缩,打点怎么搞
- 没写过php代码,这段代码应该怎么写
- 我们得告诉nginx无需再压缩了
这些问题不解决,我们就无法继续前进,那么预先gzip这个方案看上去就无法在activity上实时了,显然这也不是金牛座的风格,金牛座是一个省钱的星座是吧,所以节约机器是金牛座的一个天性。
问题和解决方案
在上一章中,我列举了一些问题,这些问题应该如何解决呢,下面我们来一个个分析一下这些问题
压缩后数据放哪里
-
放进程中?
由于php是进程模型,为了提高并发处理能力,我们都会开很多条进程,如果我们有一千个页面,每个页面消耗20KB的内存,然后我们开了40条进程,那么总内存消耗数变成了20KB * 1000 * 40 = 800MB内存。每个进程都存储了相同的页面数据。如果在java里,我们只需要20MB的消耗。所以一个20MB空间的需求活生生的弄成了800MB,这是广大处女座程序员同志们所不能接受的。 -
放tair中?
没办法,那怎么解决这个问题呢,有人说了,我们放tair里,耶,确实可以哦,看上去很美哦,但是使用tair来存储也有几个问题,我们来看看有哪些问题,这些问题都是已经预见过的:- tair网卡流量有可能跑满,819大促某个应用即使只放了2KB的数据,但是由于qps高,也将tair网卡流量跑满了,不得不先扩容,因为tair的server一般会比web server少很多,而且基本都是公用的,跑满还影响其他服务
- 每个请求20KB的数据,都要从tair返回,影响了RT
- 没法迁移到海外CDN home集群,我们总不可能在CDN home集群里部署tair集群吧,我靠,这个PE会跟我们着急的
所以放到tair中这事我是不敢指望了,比如说带宽跑满这事,不是说一定会发生,但是指不定哪天给你发生一下,君子不处危地,所以咱们还是别这样玩
更何况后面还要上海外CDN home集群呢,是吧。 -
放磁盘中?
那好像我们选择的余地也不是很多啊,想来想去,还有一个地方可以放,就是本机内存啊,或者本机磁盘啊,但是本机磁盘这事我不确定,后面CDN home集群上是不是SSD,我们现有的activity是不是SSD,这些都是放磁盘的一些限制。 或者用内存做磁盘镜像出来(tmpfs之流),这样限制更少,但是带来一个问题,就是容量控制和运维的难度增加了,我们的方案要优先节约各方的工作量,尤其是工程师和PE的工作量。所以放磁盘在我们的场景里不是最优方案。 -
放共享内存中?
而放在共享内存中就简单的多,但是我们需要一个工具,这个工具能够让php把数据放到系统的共享内存中。最好是现在的activity集群就支持,最好是现有的CDN home集群就支持,有这样的东西吗??????有的,就是APC,上文提过的APC ,上文中,我们讲到APC可以将php代码的字节码缓存起来,但是我们没有讲的是APC其实还有一个功能,叫做user cache。何为user cache,既可以把用户数据存储在这里的一种cache。而不只是php的字节码数据。
APC的user cache
其实在activity的第一轮优化中,我已经使用了user cache,我是怎么使用的呢?这个我在上一篇文章中讲过我走过的弯路(基于APC的片段缓存),当时直接使用APC的user cache来缓存 html片段,结果发现gzip上来之后,这个优化起到的作用很小,从另外一个侧面证明了在activity的场景下,gzip是影响qps的罪魁祸首。不过大家不要随便乱套哈,我说是activity这个场景,其他的具体场景要具体分析的哦(如何分析?要看你的CPU TIME是用在哪里了)。后来又把片段缓存去掉了,因为在gzip面前,这个片段缓存起到的作用简直太小儿科了(请看上文的测试数据)。
虽然走了这个“弯”路,但是让我对APC的user cache和php代码却有了一定的了解,后来我实现了php中的pre-gzip也是根据之前我写的apc片段缓存这段代码改造的(而片段缓存改造自网上一个代码片段,把片段保存在磁盘的PHP代码片段)。所以在第一轮优化中的这个“弯”路其实也为第二轮优化做了准备,环环相扣啊,这个世界没有无缘无故的爱呢。
不过使用user cache之前,我们还有些知识需要储备一下,尤其是缓存的清空策略,我们常见的缓存清空策略有LFU,LRU,FIFO等,最最常见还是LRU,比如说memcached中使用就是LRU的清空策略,6年前我踩过一个“坑”。那么APC中的缓存是使用什么样的清空策略呢,如果我们的数据量过大,那么会不会导致服务发生问题呢?比如说,页面较多,导致刚刚放到缓存中的页面就被LRU掉了,那么缓存基本失效。所以我得研究一下先。
- 文档研究 首先研究的是APC的文档,在连接中,有几个参数是跟user cache相关的:
apc.shm_segments
编译器缓存要分配的共享内存块的数目。如果 APC 用光了共享内存但是已经将 apc.shm_size 设为了系统所能允许的最大值,可以尝试增大此值。apc.shm_size
以 MB 为单位的每个共享内存块的大小。默认时,有些系统(包括大多数 BSD 变种)的共享内存块大小非常低。apc.user_ttl
缓存条目在缓冲区中允许逗留的秒数。0 表示永不超时。建议值为7200~86400 设为 0 意味着缓冲区有可能被旧的缓存条目填满,从而导致无法缓存新条目。只是针对每个用户而言,建议值为7200~86400。 设为 0 意味着缓冲区有可能被旧的缓存条目填满,从而导致无法缓存新条目。 如果大于0,APC将尝试删除过期条目。apc.gc_ttl
缓存条目在垃圾回收表中能够存在的秒数。此值提供了一个安全措施,即在服务器进程在执行缓存的源文件时,如果该文件被修改则旧版本将不会被回收,直到达到此 TTL 为止。设为零将禁用此特性。
看上去也没啥,完全没有提到LRU相关的问题,而且user_ttl和gc_ttl我也没有完全搞明白是怎么回事,有点模糊。那我就不得不去看看代码了了,这个问题还是搞清楚点比较好。
-
APC代码研究
于是我找到APC的源代码,值得注意的是,由于我们线上使用的是APC3.0.9,所以我看的是3.0.9的源代码
代码里,我简单的写了一些中文注释(英文注释是代码自带)- APC中user cache的存储结构
这个结构是一个典型的散列链表,和hashmap的实现是类似的道理,但是没有hashmap这么精致,我们来看一段代码:
apc_cache_entry_t* apc_cache_user_find(apc_cache_t* cache, char *strkey, int keylen, time_t t) { slot_t** slot; LOCK(cache); /* cache里有一个slot的数组,叫做slots,然后,然后取膜之后找到对应的slot */ slot = &cache->slots[string_nhash_8(strkey, keylen) % cache->num_slots]; /* 找到slot之后,拿到一个链表,开始遍历这个链表,这个结构和HashMap是一样的,但是取膜的问题上,HashMap有更巧妙的算法 */ while (*slot) { if (!memcmp((*slot)->key.data.user.identifier, strkey, keylen)) { /* Check to make sure this entry isn't expired by a hard TTL */ if((*slot)->value->data.user.ttl && ((*slot)->creation_time + (*slot)->value->data.user.ttl) < t) { remove_slot(cache, slot); break; } /* Otherwise we are fine, increase counters and return the cache entry */ (*slot)->num_hits++; (*slot)->value->ref_count++; (*slot)->access_time = t; /* 这种代码看起来是不是很熟悉的咧 */ cache->header->num_hits++; UNLOCK(cache); return (*slot)->value; } slot = &(*slot)->next; } cache->header->num_misses++; UNLOCK(cache); return NULL; }
从上面一段代码中,我们基本得知了APC中user cache的结构,那么下面我们来看看APC如何插入新值的。
- APC中user cache的insert
- APC中user cache的存储结构
int apc_cache_user_insert(apc_cache_t* cache, apc_cache_key_t key, apc_cache_entry_t* value, time_t t TSRMLS_DC)
{
slot_t** slot;
size_t* mem_size_ptr = NULL;
if (!value) {
return 0;
}
LOCK(cache);
process_pending_removals(cache);
slot = &cache->slots[string_nhash_8(key.data.user.identifier, key.data.user.identifier_len) % cache->num_slots];
if (APCG(mem_size_ptr) != NULL) {
mem_size_ptr = APCG(mem_size_ptr);
APCG(mem_size_ptr) = NULL;
}
while (*slot) {
if (!memcmp((*slot)->key.data.user.identifier, key.data.user.identifier, key.data.user.identifier_len)) {
/* If a slot with the same identifier already exists, remove it */
remove_slot(cache, slot);
break;
} else
/*
* This is a bit nasty. The idea here is to do runtime cleanup of the linked list of
* slot entries so we don't always have to skip past a bunch of stale entries. We check
* for staleness here and get rid of them by first checking to see if the cache has a global
* access ttl on it and removing entries that haven't been accessed for ttl seconds and secondly
* we see if the entry has a hard ttl on it and remove it if it has been around longer than its ttl
*/
if((cache->ttl && (*slot)->access_time < (t - cache->ttl)) ||
((*slot)->value->data.user.ttl && ((*slot)->creation_time + (*slot)->value->data.user.ttl) < t)) {
remove_slot(cache, slot);
continue;
}
slot = &(*slot)->next;
}
if (mem_size_ptr != NULL) {
APCG(mem_size_ptr) = mem_size_ptr;
}
/* 如果不能创建slot,那么则返回0 */
if ((*slot = make_slot(key, value, *slot, t)) == NULL) {
UNLOCK(cache);
return 0;
}
if (APCG(mem_size_ptr) != NULL) {
value->mem_size = *APCG(mem_size_ptr);
}
UNLOCK(cache);
return 1;
}
代码中写道:如果不能创建slot,那么就返回一个0告知用户这次缓存没有成功。同时在上面代码的第二段注释中,我们可以看到,用户在insert的时候,需要遍历slot的链表,根据cache的ttl和cache里的这个slot链表中所有元素的ttl找出可以被回收的空间。
而且这样的操作,在find方法中也存在,所以我们可以看做APC在执行find和insert操作时,会在对应的slot链表上根据TTL来做缓存的清除动作。这是user_ttl所起的作用了。
当然APC在删除slot链表时还有一些逻辑,根据源代码中remove_slot方法所示,在remove时,如果ref_count小于等于0,那么直接释放这个slot,如果ref_count大于0,但是ttl相关的时间条件是满足了,那么就会将这个slot放到一个deleted_list中,供APC中的gc来回收这个slot对象。这就是gc_ttl这个参数的作用:控制slot在deleted_list中存活的时间。
-
APC中user cache的调研总结
- APC的缓存清空是跟TTL相关的,而不是LRU,所以先进缓存的,即使没有人使用,不到时间不会被清除,这会导致先进缓存的数据在缓存过期之前一直在缓存中,**所以user_ttl时间不要设置为0,且gc_ttl也要大于0,这样长时间不被访问的页面会被请出缓存**,这样其实也是不错的选择。我之所以要把APC代码拿下来,看看它的缓存清空策略,其中一个非常重要的原因是我怕它是LRU,如果是LRU的清空策略,那么我们就必须更加小心,因为共享内存不多的情况下,且访问比较平均的情况下,有发生LRU命中率低的可能性。因为刚刚放进去的页面,有可能因为LRU被清掉。虽然是极端情况(我也告诉自己,不要想太多)但是819大促告诉我们,凡是有可能发生的,哪一天它就会冷不防的冒出来,不得不小心一点。
- 如果内存不足,APC会返回失败告知php进程。也就是最差情况下,在共享内存不够的情况下,我们的页面就得不到缓存,那么就需要每次都做gzip,这个最差的结果和我们目前的情况是一样的,也就是说最差也不过就是回到现状,只不过打点的工作需要PHP代码实现了,这个还是可以接受的。所以我决定用APC来存储压缩后的html页面。
重复压缩
如何避免重复压缩
由于返回的html已经被php压缩过了,那么nginx或者apache再压缩一遍其实是浪费了,而且不光是浪费,在firefox下,重复压缩的数据还不能正常展示。我们不能简单粗暴的关闭掉nginx压缩,因为不使用这套方案的php页面或者nginx后面的其他进程,比如nodejs之类,还是需要用到nginx的压缩的,最好的方案就是我们在返回头有一个标示,有了这个标示之后,nginx就不再压缩返回数据,而且还不影响浏览器显示。
-
gzip_types
我可以在gzip_types上做点手脚吗,可以是可以,比如说只要返回content-type=text/plain,那就不执行压缩。普通的php页面没有使用指定content-type,会默认使用text/html,而这个是默认会压缩的,这也不失为一种方案。 -
gzip_min_length
比如说压缩过的页面,都是小于50KB的,那么我们可以设置大于50KB才压缩,小于50KB不压缩
打点
-
AE打点现状
由于我们打点是依赖nginx,而现状通过nginx时,数据已经被gzip过了,所以nginx不会再做ungzip,打点再gzip这种事情,那么打点这个事情就需要交给程序来做了,这话说起来很轻巧(老大要求我们举重若轻),但是实际上,这里是最麻烦的地方,要找到解决方案,不得不先把情况了解清楚 -
预压缩的打点方案一,cookie传递time
-
预压缩的打点方案二,分段压缩
说起分段压缩,这里还有个小故事,之前在网上看过一篇文章,是讲分段压缩的问题的,文章的结论是浏览器不支持分段压缩,所以我的脑海里一直有这个印象,后来在CDN群里和同事聊天,同事建议我去看看varnish的ESI实现,同时给了我3个资料和提示,正是这个机缘巧合下,我才能找到替换用cookie传time的打点方案,在这里非常感谢同事
使用cookie传递打点需要的time属性是满足现状的,但是后面如果打点迁移到alilog(据说属性很多,不只是time一个动态的值),那么需要存放在cookie里的值会很多了,维护管理将会不太方便,所以使用分段压缩是一个比较好的选择。
也许你会想,那简单啊,我直接把打点前的html压缩成一个gzip流,然后打点数据再压缩一下,最后再压缩一下打点数据之后的html,这样就可以压缩成3个完整的gzip文件,返回给浏览器,这样做是最简单,最省力,最省事的。没错,我开始也是这么想的,但是在不断的查资料的过程中,发现HTTP规范中,明确指出,返回给浏览器的应该是整段的gzip文件
在我后续的文章也会说明,即使使用varnish中的ESI实现,对于PHP来说是行不通的,除非自己写压缩扩展。
所以这个优化,我们在PHP上只能使用cookie传递time值的方案。
这段代码应该怎么实现呢
-
流程图
要写代码,先定流程,所谓谋定而后动,所以我就整了一张流程图:
实际上apc3.0.9有一个stats配置, 改变这个指令值要非常小心。 默认值 On 表示APC在每次请求脚本时都检查脚本是否被更新, 如果被更新则自动重新编译和缓存编译后的内容。但这样做对性能有不利影响。 如果设为 Off 则表示不进行检查,从而使性能得到大幅提高。 但是为了使更新的内容生效,你必须重启Web服务器(译者注:如果采用cgi/fcgi类似的,需重启cgi/fcgi进程)。 生产服务器上脚本文件很少更改, 可以通过禁用本选项获得显着的性能提升。**不过一般情况下,检查文件是否最新并不是性能瓶颈所在,gzip才是。所以建议大家stats=on,这样php文件更新时可以立马自动重新编译,并缓存编译之后的内容。这一点非常重要。**那么,如果页面改变了,但是缓存中的数据没有改变,应该如何解决这个问题呢。直接修改缓存的key取值即可。这样能保证最新的数据会生效。
如果不想修改缓存的key呢,那就需要我们将缓存时间设置的短一点了,比如说5分钟,这样5分钟之后缓存中的数据失效,这样5分钟之后,新的PHP文件就生效了。为了性能,这点付出也是需要的。
-
代码实现
<?php
ob_start();//缓冲区开始
//定义一个开始缓存的标示,这个函数将在后面的代码中被调用到
function cache_start_apc() {
//如果客户端接受gzip数据
if (canBeGzip()) {
//根据URL生成一个user cache的key,并从apc user cache获取对应的值
$key = sha1($_SERVER['REQUEST_URI']);
$content = apc_fetch($key);
// $content = none;
//如果缓存中该key对应的value不为空,那么直接返回缓存中的数据,否则,退出函数,开始渲染页面
if (!empty($content)) {
header('Content-Encoding: gzip');
header("Vary: Accept-Encoding");
//清除缓冲区的任何内容
ob_clean();
//输出数据到缓冲区,并flush之
echo $content;
ob_end_flush();
exit;
}
}
//如果客户端不接受gzip数据,那么直接往下执行,渲染页面
}
//如果浏览器接收压缩数据,那么使用zlib的库进行压缩,压缩级别为6
function ob_beacon_and_gzip($content) {
//TODO 先打点,再压缩,打点模板需要改一下,把time改成变量
return gzencode($content, 6);
}
function cache_end_apc() {
//客户端接收压缩数据的话,返回压缩数据,不接受压缩数据就无需压缩并缓存了
if(canBeGzip()) {
//从缓冲区中拿到渲染好的html,并进行压缩
$content = ob_beacon_and_gzip(ob_get_contents());
//压缩完之后放到user cache,就算放失败了,内存不够了,也没有关系,直接返回压缩后的数据
$key = sha1($_SERVER['REQUEST_URI']);
apc_add($key, $content);
//清空缓冲区
ob_clean();
//设置压缩头
header('Content-Encoding: gzip');
header("Vary: Accept-Encoding");
//压缩内容输出
echo $content;
//flush缓冲区
ob_end_flush();
} else {
//如果客户端不接受压缩数据,则把缓冲区中的原始html直接返回
ob_end_flush();
}
}
function canBeGzip() {
return !headers_sent()&&extension_loaded("zlib")
&&strstr($_SERVER["HTTP_ACCEPT_ENCODING"],"gzip");
}
?>
-
一定要5分钟之后生效吗?
当然不一定,尤其现在的大促活动页面都是定制的情况下,如果有紧急发布,我们只需要将修改时将缓存的key改一下即可,比如说原来APC user cache中存储的gzip对应的key是123,那么紧急发布时,新的页面的key是456即可,原来的压缩数据在5分钟之后会被放入GC队列,然后等待被GC回收。 -
你很担心php的压缩效率是不是?
不用担心,php的压缩和nginx的压缩是调用相同的库,都是使用zlib库,而且如果我们将也没全部缓存的话,同一个页面几分钟才需要做一次压缩,所以PHP的执行效率在这个场景下是不用担心的。
测试
我尝试在不同的压缩级别和不同的页面大小的情况下做测试,并观察CPU和Load的情况,测试脚本如下:
ab -n 10000 -c 10 -H 'Accept-Encoding:gzip' https://localhost:8888/xxx.php
这些页面都是来自于325大促的真实页面。
- 压缩级别=6
原始页面大小 | 压缩后的大小 | 优化后QPS | RT |
---|---|---|---|
92KB | 17KB | 2024 | 4.9ms |
138KB | 8.7KB | 1859 | 3.3ms |
182KB | 11.4KB | 2083 | 4.8ms |
248KB | 32KB | 1977 | 5.0ms |
295KB | 34.4KB | 1722 | 5.8ms |
整个测试没有经过网卡,而且是在一台5年前的mac book pro上测试:双核8G,三星的SSD。
在把压缩级别调高之后,295KB的页面压到了33.9KB,和34.4KB没有太大区别,所以对gzip-level=9没有进行更加深入的测试。
根据之前在4核虚拟机的对比来看,我预估:同样的程序如果放到4核虚拟机上,刨去网卡带宽限制不计,qps上到3000以上是没有压力的,原因是第一轮优化中的页面,在生产机器上qps是500左右,在我机器上是200左右,第二轮优化中,我机器上同样的页面达到了2000左右,所以有理由相信生产机器上会达到3000以上,由于现在资源紧缺,所以这个优化方案并不会在速卖通双11大促上线
从以上测试结果来看,有几个结论比较抢眼:
- QPS飙的很高啊,高出现有的10倍左右。没有缓存压缩数据时qps大多在100-200
- RT下降的很厉害,相对值很高,但是绝对值不高,50ms以内的下降
- 压缩级别调高,
- 带宽消耗降低,但是不是特别明显
- 由于包数量变少,所以假设MTU=1500,MSS=1460, 而我们亲爱的俄国妹子的window size=16328(我连英文的amazon时,window size是16328,所以拿这个值举例,server端的初始化拥塞窗口是10),诶,好了,如果我们的页面gzip level=9,那么压完之后,数据量小于1460*10,那么俄罗斯妹子会很爽,因为我们会一下子发1460*10/1460=10个包过去,理想情况下一个RTT内,妹子就拿到了商品数据。如果gzip level=6,那么压完之后,数据有可能大于14600,那么我们就只会先发10个包过去,理想情况下等10个包的ACK最大seq的那个包返回,再接着发剩余的包。这个时候,就不是一个RTT的问题了。在国际网络环境下,RTT=300ms也是有的。当然这些都是估算,针对我们的场景,具体能出现什么样的优化效果,也是需要长期的测试的。而且一旦海外CDN Home集群上了之后,RTT有可能10ms,那么就提升压缩级别的效果就不明显了。
能否使用swift来缓存压缩之后的数据
是否可以其实取决于二个条件,如果这二个条件有一个不满足,那么就无法用swift来缓存压缩之后的数据:
- 第一,需要在CDN home集群上部署swift,现在是没有的,不过部署起来也不是难事
- 第二,对于php代码中出现根据user-agent等header属性决定显示什么样的html来说
这个需求直接用代码来压缩的方案来实现就很方便了,根据user-agent中的部分核心属性(为啥是部分核心属性,因为user-agent太多了,每个user-agent都作为一个Key话,同一个页面也会产生大量的副本,对我们的819需求来说只是为了分辨出是mobile还是pc,所以只要为数不多的几种key而已,而且每种key对应的html也是不一样的,不存在同一个页面有不同副本的问题),渲染出不同的html,然后压缩并通过不同的key缓存在共享内存。就好像这个需求和方案是天生一对一样,如果用swift来缓存不同user-agent的页面,同一个php页面,将会产生很多份缓存,这样热点不明显了,命中率也会受到影响。因为在web cache中间件上是根据完整的user-agent来做key的一部分的,所以user-agent越多,那么副本越多。
后来在CDN群里也提到了这个事情,确实是vary:user-agent会产生大量的副本,虽然CDN的同事说的是会影响命中率,但是我觉得是影响热点的集中度和命中率都有,对于有多层cache的缓存中间件来说,热点集中与否直接影响页面在哪一层cache上,从而影响到页面的响应速度。
总结
在这个优化中,研究了APC相关的实现,整理了整个流程,并且用代码实现之,唯一不完美的地方,是打点,目前只能把time参数放置在cookie中。通过这样的优化,请求过来的时候,也没啥PHP代码要执行了,那些TMS天窗啥的都不用执行,只需要执行文件头的几段缓存相关和user-agent相关的逻辑,然后直接拿了共享内存中的压缩包就返回了。
下面对比一下优化前后的两种方案,我一般都会使用表格法,所以下面简单列一下优化前后的对比:
各维度 | 优化前 | 优化后 |
---|---|---|
TPS | 100-200左右 | 2000以上,在自己的老掉牙的笔记本上 |
RT | 40ms | 10ms |
php及时生效 | 及时生效 | 5分钟之后生效,如果再TMS中可以随机生成缓存的key,那么也可以做到及时生效,老的缓存数据让其自动过期 |
代码侵入 | 无代码侵入 | 少量代码侵入,把埋点代码向应用迁移 |
额外内存消耗 | 无额外内存消耗 | 有额外内存消耗(压缩后20KB的页面有1000个,需要20MB的共享内存) |
压缩级别 | 不能改变压缩级别 | 可以增加压缩级别,降低带宽消耗和RT,提高用户体验,但在上了海外CDN home集群之后,gzip level调整的必要性不高 |
如果你只有20台机器,那么优化后只需要3-5台,我们可以不在乎,叠机器不是问题。如果你有100台机器,保守可以优化到30台以内,极端点,15台也不是没有可能。这个时候,少量的改造,带来的就是大量机器的成本的降低,投入不大,但是产出是很大的。
再总结
虽然文章写的差不多了,但是我还要多啰嗦几句,这里预先gzip并压缩只是一个优化思路,实际上我想看到这篇文章的人在工作中不会涉及到php,那么这篇文章对和PHP无关的你有什么助益呢,我简单列一下:
- 知道在一些场景下,gzip可能是消耗CPU的大户,大家可以观察一下自己的应用
- 预先把浏览器需要的数据压缩之后放缓存会带来qps的极大提高(多高?我这个场景是10倍,取决于gzip在整个cpu time中的比重,你的场景未必有这么多,也有可能更多)
- 虽然我是预压缩的html,但是不代表你不能压缩ajax返回的json,对应返回的json数据超过100KB的。每次都压缩一下这个json和把json压缩完放在内存的效率我就不说了,you know what i say.
- 100kB压完之后,只有17KB左右,内存占用少
- 长时间内,只压缩一次,CPU占用很少,提高QPS
- 没有打点问题,操作起来非常简单
文章最后,表示一下感谢,在整个优化的过程中,得到太多同学的帮助,不一一列举,谢谢大家。
最后更新:2017-04-01 13:44:32