接口开发注意事项

1、确认需要哪些接口

确定每个接口的功能点,接口功能尽量不要交叉

2、确定每个接口具体数据

接口发送或者接收的参数名,参数含义,参数类型,参数长度;接口的格式,json还是xml;接口的编码

3、确认数据交互的安全性

4、编码注意点,a、切忌在程序的各个地方直接调用其它系统的接口;b、会产生数据交易的接口,一定要有log;c、接收的数据进行数据校验,确保符合系统要求,并异常准确返回

不同版本PHP之间cURL的区别

之前在做一个采集的工具,实现采集回来的文章,图片保存起来.文章内容是保存在数据库,图片是先需要上传到图片服务器,再返回图片地址,替换掉文章的图片地址.

问题来了:都能成功采集都东西,但是,本地测试是正常的,图片也可以上传成功,但是生产环境就是一直没有图片.然后自己就一步一步调试,,发现数据都有,但为什么偏偏生产上没有成功上传图片呢.

后来折腾了几天,经过一步步的看代码,调试,百度,终于找到答案了.真是一个大坑.

上传到图片服务器是用curl post过去的,

PHP的cURL支持通过给CURL_POSTFIELDS传递关联数组(而不是字符串)来生成multipart/form-data的POST请求。

传统上,PHP的cURL支持通过在数组数据中,使用“@+文件全路径”的语法附加文件,供cURL读取上传。这与命令行直接调用cURL程序的语法是一致的:

curl_setopt(ch, CURLOPT_POSTFIELDS, array(
    'file' => '@'.realpath('image.png'), 
)); 
equals
$ curl -F "file=@/absolute/path/to/image.png" <url>

但PHP从5.5开始引入了新的CURLFile类用来指向文件。CURLFile类也可以详细定义MIME类型、文件名等可能出现在multipart/form-data数据中的附加信息。PHP推荐使用CURLFile替代旧的@语法:

curl_setopt(ch, CURLOPT_POSTFIELDS, [
    'file' => new CURLFile(realpath('image.png')), 
]); 

PHP 5.5另外引入了CURL_SAFE_UPLOAD选项,可以强制PHP的cURL模块拒绝旧的@语法,仅接受CURLFile式的文件。5.5的默认值为false,5.6的默认值为true。

但是坑的一点在于:@语法在5.5就已经被打了deprecated,在5.6中就直接被删除了(会产生 ErorException: The usage of the @filename API for file uploading is deprecated. Please use the CURLFile class instead)。

对于PHP 5.6+而言,手动设置CURL_SAFE_UPLOAD为false是毫无意义的。根本不是字面意义理解的“设置成false,就能开启旧的unsafe的方式”——旧的方式已经作为废弃语法彻底不存在了。PHP 5.6+ == CURLFile only,不要有任何的幻想。

我的部署环境是5.4(仅@语法),但开发环境是5.6(仅CURLFile)。都没有压在5.5这个两者都支持过渡版本上,结果就是必须写出带有环境判断的两套代码。

现在问题来了……

 

环境判断:小心魔法数字!

我见过这种环境判断的代码:

if (version_compare(phpversion(), '5.4.0') >= 0)

我对这种代码的评价只有一个字:

这个判断掉入了典型的魔法数字陷阱。版本号莫名其妙的出现在代码之中,不查半天PHP手册和更新历史,很难明白作者被卡在了哪个功能的变更上。

代码应该回归本源。我们的实际需求其实是:有CURLFile就优先采用,没有再退化到传统@语法。那么代码就来了:

if (class_exists('\CURLFile')) {
    $field = array('fieldname' => new \CURLFile(realpath($filepath)));
} else {
    $field = array('fieldname' => '@' . realpath($filepath));
}

建议明确指定的退化选项

从可靠的角度,推荐指定CURL_SAFE_UPLOAD的值,明确告知php是容忍还是禁止旧的@语法。注意在低版本PHP中CURLOPT_SAFE_UPLOAD常量本身可能不存在,需要判断:

if (class_exists('\CURLFile')) {
    curl_setopt($ch, CURLOPT_SAFE_UPLOAD, true);
} else {
    if (defined('CURLOPT_SAFE_UPLOAD')) {
        curl_setopt($ch, CURLOPT_SAFE_UPLOAD, false);
    }
}

cURL选项设置的顺序

不管是curl_setopt()单发还是curl_setopt_array()批量,cURL的选项总是设置一个生效一个,而设置好的选项立刻就会影响cURL在设置后续选项时的行为。

例如CURLOPT_SAFE_UPLOAD就和CURLOPT_POSTFIELDS的行为有关。如果先设置CURLOPT_POSTFIELDS再设置CURLOPT_SAFE_UPLOAD,那么后者的约束作用就不会生效。因为设置前者时cURL就已经把数据实际的识读处理完毕了!

cURL有那么几个选项存在这种坑,务必小心。还好这种存在“依赖关系”的选项不多,机制也不复杂,简单处理即可。我的方法是先批量设置所有的选项,然后直到curl_exec()的前一刻才用curl_setopt()单发设置CURLOPT_POSTFIELDS

实际上在curl_setopt_array()用的数组中,保证CURLOPT_POSTFIELDS的位置在后边也是可靠的。PHP的关联数组是有顺序保障的,我们也可以假设curl_setopt_array()内部的执行顺序一定是从头到尾按顺序[注A],所以尽可放心。

我的做法只是在代码表现上加个多余的保险,突出强调顺序的重要性防以后手贱。

命名空间

PHP 5.2或以下的版本没有命名空间。代码中用到了空间分隔符\就会引发解析器错误。要照顾PHP 5.2其实容易想,放弃命名空间即可。

要注意的反倒是有命名空间的PHP 5.3+。无论是调用CURLFile还是用class_exists()判断CURLFile的存在性,都推荐写成\CURLFile明确指定顶层空间,防止代码包裹在命名空间内的时候崩掉。

好了,这坑挖得好深,跳出来就分享下.

(以上解决方法是转载网站的,感谢让我找到了你这篇东西!)

一款基于Bootstrap扁平化的后台框架Ace

最近一段时间在做一个管理系统,在网上找了很久的前端展示框架,终于找到一款基于Bootstrap的后台管理系统模版:Ace。Bootstrap是Twitter 于2010年开发出来的前端框架,用过的同学应该知道,这款前端框架不仅界面很美观,而且兼容了很多的浏览器,大大加速了我们开发网站的速度!这篇文章讲到的Ace是基于Bootstrap的,所以界面自然也非常美观,这款后台管理系统组合了好多插件以及组件,可以满足很大一部分的后台管理系统模版.

ace.jpg

下载资源:ace

防止网站被刷票的一些思路与方法

在Web开发中,投票模块会经常出现。这样就使得防止刷票,成了至关重要的技术。以下是试验过或者网上找到的防止刷票方法:

1. IP限制

这是使用的最多,也是最广泛,不可少的刷票限制。而且由于专题都没有用户模块,使得这个方法,几乎成了唯一可行的手段。该方式,通过获取访问游客的IP地址,来限制其在一段时间内所能使用的票数。当然,正常访问页面投票的游客,肯定都能很好的被这个手段所限制,但是,各种投票的活动和专题,以及丰厚的奖品,使得一些人想方设法的想要进行刷票。

  • ADSL用户,可以通过断线重拨来更换IP地址。
  • 使用代理访问的用户,我们也无法获得其真实的IP地址。
  • 还有我们未知的刷票工具。

以上都是我们没有办法克服的难题,而且对于网吧/内网用户来说,这样的设置也会显得不够公平。

2. Cookies 验证

这也是网上所用到较多的手段,不过,所有来自于客户端的信息和内容都是不可靠的。熟知cookies原理的人,就可以很容易的绕过限制。

  • cookies 可以禁用。
  • 使用代理访问的用户,我们也无法获得其真实的IP地址。
  • Cookies可以随意清除、修改。

3. Session 验证

session会给所有访问的游客,指定一个唯一的sessionID。这样,似乎对于防止刷票有一些作用。可惜session有致命的缺点。

  • 关闭浏览器,session就会被销毁。
  • 客户端禁用cookies,session也会失效。

4. 验证码

注册,登陆,回复,发帖……验证码使用的范围很广,很多。可惜,至于具体效果呢,长久下来,只发现这样的方式,其实只是加大了普通游客的投票难度,而且刷票频繁的访问验证码,也大大加大了服务器负担。

有一种验证码是可行的,google推出的图形验证码,旨在让用户将图片翻转至正确的方向,方可完成验证。想必很少有软件或者电脑操作可以很好的绕过这样的验证码。不过,缺点呢,实现难度很高。

5. MAC地址限制

作为web程序,很难获取真实的Mac地址(我用php/js实验过,并没有成功的获取到Mac地址)。当我们读取客户端Mac地址时,读取的也是存储于注册表的一个信息,它也是可以进行修改的。

6. 用户模块

这应该算很有用的方式。每个游客,必须注册了账户才能进行投票,通过限制账户ID来限制投票,并且可以限制初始注册用户,一段时间内不能参与投票。而且,真有大量的注册用户涌入,也可以增加网站的流量。可是,对于小地区网站来说,这样会让网友觉得很麻烦,可能放弃投票,从而可能流失网站的用户。

7. 行为记录

其实,很多刷票行为我们是没办法进行限制的。可是有时候,我们却可以很好的记录下来。而且当检测到非正常投票的时候,自动提出票数,要么返回投票成功的假象,也可以很好的限制刷票。

缺点就是,记录过多的数据,会让服务器压力很大。而且,如果记录行为的记录点不够多,不够复杂,也可能被刷票的专业户所参透。

8. 回答问题

第一次使用这个方式限制刷票的时候,我们很欣慰的看到了不错的效果。可惜,如果没有足够海量的题库,很快问题的内容和答案就会被收集。反而让正常投票的用户,觉得投票很恶心、麻烦,产生厌恶心理。渐渐的也被我们抛弃了。

9. 随机投票地址

该方法, 让每一个访问页面的用户得到一个随机唯一的KEY,通过这个KEY,生成一个投票地址,该地址只能访问一次,使用过后便作废。

可惜,指定的KEY的内容,我目前是采用的sessionid+ip+随机数 生成的MD5码,而sessionid和ip在上面的内容都提到,是可以销毁和篡改的。这样的方式,也不过是在ip限制和session限制无效时候的垂死挣扎。

10. 填写信息

投票时,让游客填写身份证,姓名,手机号码等可以表示一个人身份的信息,来进行唯一性验证。不过,身份证你知道格式,姓名随便填,手机号码随便填写,也是没办法限制的。反而让正常投票的人觉得麻烦。

11. 投票码/排号系统

投票前,用户都需要在其他页面/邮箱先获取到一个唯一投票的编码/卷,使用一次作废。不过,如何验证‘人’的唯一呢,如果有用户,他的邮箱足够多,是不是就可以无限获取投票码呢。

对!所以,这也只是治标不治本的方法了。不过,至少,这个方式也会让刷票的人觉得很麻烦。但是同时,也会让正常投票的人觉得不方便。不过,能够让刷票党,刷得不是那么痛快,也算是有用了。

12. 人民币投票

虎,这样最好,不管是发短信,打电话,还是支付宝,一票一元人民币,你如果是足够舍得,当然,随便你投,随便你刷,咱也乐意不是。通过支付接口的结合,作弊是相当难的了。

不过,公平性有待考究,毕竟不是所有人的支持者都有钱,都舍得花钱,可能让好的作品因为‘穷’而得不到票数,这是在咱公平、民主的社会中,是不提倡的!

好了,差不多就以上这些方式。引用一个网友的原文:“要想完全杜绝刷票,基本只能靠人”。

抛砖引玉:

  • flash投票,不过不是很清楚原理,有空的时候会投入测试。
  • 插件。 web插件,可能需要用户下载安装,普通用户会觉得很麻烦,可行性有待考究。
  • 一些有趣的设置 例如,短时间内的多次投票,可能导致投票数下降;可以给作品投负票数,不过有一定几率无效或者反射到其他作品;类似农场的‘偷菜’,我们可以投票等。也许可以增加一点可玩性,可是公平性就无法保证了。

总结:个人觉得目前最有效的只能是人民币投票了,可以很好的限制刷票行为。

PHP快速读取大文件方法比较

在PHP中,对于文件的读取时,最快捷的方式莫过于使用一些诸如file、file_get_contents之类的函数,简简单单的几行代码就能 很漂亮的完成我们所需要的功能。但当所操作的文件是一个比较大的文件时,这些函数可能就显的力不从心, 下面将从一个需求入手来说明对于读取大文件时,常用的操作方法。

需求需求

有一个800M的日志文件,大约有500多万行, 用PHP返回最后几行的内容。

实现方法

1. 直接采用file函数来操作

由于 file函数是一次性将所有内容读入内存,而PHP为了防止一些写的比较糟糕的程序占用太多的内存而导致系统内存不足,使服务器出现宕机,所以默认情况下限制只能最大使用内存16M,这是通过php.ini里的 memory_limit = 16M 来进行设置,这个值如果设置-1,则内存使用量不受限制。

下面是一段用file来取出这具文件最后一行的代码:

view sourceprint?

1.<?php

2.ini_set('memory_limit''-1');

3.$file 'access.log';

4.$data = file($file);

5.$line $data[count($data) - 1];

6.echo $line;

7.?>

整个代码执行完成耗时 116.9613 (s)。

我机器是2个G的内存,当按下F5运行时,系统直接变灰,差不多20分钟后才恢复过来,可见将这么大的文件全部直接读入内存,后果是多少严重,所以不在万 不得以,memory_limit这东西不能调得太高,否则只有打电话给机房,让reset机器了。

2.直接调用Linux的 tail 命令来显示最 后几行

在Linux命令行下,可以直接使用 tail -n 10 access.log 很轻易的显示日志文件最后几行,可以直接用PHP来调用tail命令,执行PHP代码如下:

view sourceprint?

1.<?php

2.$file 'access.log';

3.$file escapeshellarg($file); // 对命令行参数进行安全转义

4.$line = `tail -n 1 $file`;

5.echo $line;

6.?>

整个代码执行完成耗时 0.0034 (s)

3. 直接使用PHP的 fseek 来进行文件操作

这种方式是最为普遍的方式,它不需要将文件的内容全部读入内容,而是直接通过指针来操作,所以效率是相当高效的。在使用fseek来对文件进行操作时,也有多种不同的方法,效率可能也是略有差别的,下面是常用的两种方法:

方法一

首先通过fseek找到文件的最后一位EOF,然后找最后一行的起始位置,取这一行的数据,再找次一行的起始位置, 再取这一行的位置,依次类推,直到找到了$num行。

#实现代码如下

view sourceprint?

01.<?php

02.$fp fopen($file"r");

03.$line = 10;

04.$pos = -2;

05.$t " ";

06.$data "";

07.while ($line > 0)

08.{

09.while ($t != "n")

10.{

11.fseek($fp$pos, SEEK_END);

12.$t fgetc($fp);

13.$pos--;

14.}

15.$t " ";

16.$data .= fgets($fp);

17.$line--;

18.}

19.fclose($fp);

20.echo $data

21.?>

整个代码执行完成耗时 0.0095 (s)

方法二

还是采用fseek的方式从文件最后开始读,但这时不是一位一位的读,而是一块一块的读,每读一块数据时,就将读取后的数据放在一个buf里,然后通过换 行符(n)的个数来判断是否已经读完最后$num行数据。

#实现代码如下

view sourceprint?

01.<?php

02.$fp fopen($file"r");

03.$num = 10;

04.$chunk = 4096;

05.$fs = sprintf("%u"filesize($file));

06.$max = (intval($fs) == PHP_INT_MAX) ? PHP_INT_MAX : filesize($file);

07.for ($len = 0; $len $max$len += $chunk)

08.{

09.$seekSize = ($max $len $chunk) ? $chunk $max $len;

10.fseek($fp, ($len $seekSize) * -1, SEEK_END);

11.$readData fread($fp$seekSize) . $readData;

12.if (substr_count($readData"n") >= $num + 1)

13.{

14.preg_match("!(.*?n){" . ($num) . "}$!"$readData$match);

15.$data $match[0];

16.break;

17.}

18.}

19.fclose($fp);

20.echo $data;

21.?>

整个代码执行完成耗时 0.0009(s)。

方法三

view sourceprint?

01.<?php

02.function tail($fp$n$base = 5)

03.{

04.assert($n > 0);

05.$pos $n + 1;

06.$lines array();

07.while (count($lines) <= $n)

08.{

09.try

10.{

11.fseek($fp, -$pos, SEEK_END);

12.}

13.catch (Exception $e)

14.{

15.fseek(0);

16.break;

17.}

18.$pos *= $base;

19.while (!feof($fp))

20.{

21.array_unshift($linesfgets($fp));

22.}

23.}

24. 

25.return array_slice($lines, 0, $n);

26.}

27. 

28.var_dump(tail(fopen("access.log""r+"), 10));

29.?>

整个代码执行完成耗时 0.0003(s)

微信开发中emoji表情的处理方法

57fb1242a4197.gif

背景

做微信开发的时候就会发现,存储微信昵称必不可少。

可这万恶的微信支持emoji表情做昵称,这就有点蛋疼了

一般Mysql表设计时,都是用UTF8字符集的。把带有emoji的昵称字段往里面insert一下就没了,整个字段变成了空字符串。这是怎么回事呢?

原来是因为Mysql的utf8字符集是3字节的,而emoji是4字节,这样整个昵称就无法存储了。这要怎么办呢?我来介绍几种方法

解决方案

1、使用utf8mb4字符集

如果你的mysql版本>=5.5.3,你大可直接将utf8直接升级为utf8mb4字符集
这种4字节的utf8编码可完美兼容旧的3字节utf8字符集,并且可以直接存储emoji表情,是最好的解决方案
至于字节增大带来的性能损耗,我看过一些评测,几乎是可以忽略不计的

2、使用base64编码

如果你因为某些原因无法使用utf8mb4的话,你还可以使用base64来曲线救国
使用例如base64_encode之类的函数编码过后的emoji可以直接存储在utf8字节集的数据表中,取出时decode一下即可

3、干掉emoji表情

emoji表情是个麻烦的东西,即使你能存储,也不一定能完美显示。在iOS以外的平台上,例如PC或者android。如果你需要显示emoji,就得准备一大堆emoji图片并使用第三方前端类库才行。即便如此,还是可能因为emoji图片不够全而出现无法显示的情况在大多数业务场景下,emoji也不是非要不可的。我们可以适当地考虑干掉它,节约各种成本

经过一番苦苦的google,终于找到靠谱能用的代码:

// 过滤掉emoji表情function filterEmoji($str){
 $str = preg_replace_callback(
   '/./u',
   function (array $match) {
    return strlen($match[0]) >= 4 ? '' : $match[0];
   },
   $str);
 
  return $str;
 }

以上就是为大家总结的PHP微信开发中涉及到emoji表情的几种处理方法,基本思想就是遍历字符串中的每个字符,如果该字符的长度为4个字节,就将其删除。

Http请求方法和响应状态码整理

一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,格式如下:
Http请求方法和响应状态吗整理
可见请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。
HTTP请求方法:

  • GET: 请求指定的页面信息,并返回实体主体。
  • HEAD: 只请求页面的首部。
  • POST: 请求服务器接受所指定的文档作为对所标识的URI的新的从属实体。
  • PUT: 从客户端向服务器传送的数据取代指定的文档的内容。
  • DELETE: 请求服务器删除指定的页面。
  • OPTIONS: 允许客户端查看服务器的性能。
  • TRACE: 请求服务器在响应中的实体主体部分返回所得到的内容。
  • PATCH: 实体中包含一个表,表中说明与该URI所表示的原内容的区别。
  • MOVE: 请求服务器将指定的页面移至另一个网络地址。
  • COPY: 请求服务器将指定的页面拷贝至另一个网络地址。
  • LINK: 请求服务器建立链接关系。
  • UNLINK: 断开链接关系。
  • WRAPPED: 允许客户端发送经过封装的请求。
  • Extension-mothed:在不改动协议的前提下,可增加另外的方法。

HTTP响应状态码:
① 客户方错误
100  继续
101  交换协议
② 成功
200  OK
201  已创建
202  接收
203  非认证信息
204  无内容
205  重置内容
206  部分内容
③ 重定向
300  多路选择
301  永久转移
302  暂时转移
303  参见其它
304  未修改(Not Modified)
305  使用代理
④ 客户方错误
400  错误请求(Bad Request)
401  未认证
402  需要付费
403  禁止(Forbidden)
404  未找到(Not Found)
405  方法不允许
406  不接受
407  需要代理认证
408  请求超时
409  冲突
410  失败
411  需要长度
412  条件失败
413  请求实体太大
414  请求URI太长
415  不支持媒体类型
⑤ 服务器错误
500  服务器内部错误
501  未实现(Not Implemented)
502  网关失败
504  网关超时
505 HTTP版本不支持

php活动限流排队demo

082152524074980.gif

技术实现目的:单位时间内控制一定数量的用户进入活动页面

实现原理:使用漏桶算法,每次用户进去活动页面时,检查限流缓存计数值是否存在,如果存在,允许进入,并且计数递减一位;当计数器归零后,表示该单位时间内可参与人数已满,后续用户进入排队系统。排队静态页面按照固定秒数时间后,自动会跳到限流控制页面,重新查询缓存计数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$config array(
    //redis服务器参数
    'redis_auth'=>array(
        'host' => '',
        'port' => '',
        'password' => ''
    ),
    //活动页面地址
    'activity_url' => '',
    //限流静态页面地址
    'static_html' => '',
    //预置限流阈值,控制的单位时间内允许进去的次数
    'max_num' => '100',
    //限流redis键名
    'cache_key' => 'xianliu_activity_limit',
);
$redis = getRedis($config['redis_auth']);
//整理正确的缓存键名,带有时间(测试时,定位到分钟,实际使用时,可以限制到秒)
$cacheKey $config['cache_key'].'_'date('Y-m-d-H:i:s', time());
//如果不存在缓存,计数器初始化
if(!$redis->exists($cacheKey)){
    $redis->set($cacheKey$config['max_num']);
    $redis->expire($cacheKey, 100);//100秒后销毁缓存
}
$counts = (int)$redis->get($cacheKey);
if($counts  0){
        $redis->decr($cacheKey);
    }
}
header("location:$redirect_url");
/**
 * 获取redis资源
 */
function getRedis($config){
    $redis new Redis();
    $redis->connect($config['host'], $config['port']);
    if($config['password']){
        $redis->auth($config['password']);
    }
    return $redis;
}

HTML Diff,php版本,文本对比(标记出两个HTML的差异)

其实就是一个 文本对比代码,但是不会将HTML标签对比出。类似于Beyond compare、Ultra compare

原代码是C#,其实C#也是别人修改的Python。
觉得效果挺好,就修改为了PHP, 且支持汉字或其它字符(原来代码中,汉字会被无情的一个接着一个拆分)

效果可以参见「知乎网」或下图, 红色部分为已删除文字, 绿色部分代表新添文字

C#:http://www.rohland.co.za/index.php/2009/10/31/csharp-html-diff-algorithm/
Ruby:https://github.com/myobie/htmldiff
Python:https://github.com/aaronsw/htmldiff

使用本代码必须遵循GPL协议,如果您用于商业用途,虽然我不能将您怎样,但是您会打击我的积极性,以后还有没有这么好的代码,就难说了。(不要觉得我脸皮厚,已经有很多人找我要过,只是因为他们用于商业用途,我没给)
我修改回来也不容易,也花了一天时间, 并且为汉字等做了大量的调试,退一万步来说,原作者发布该代码也是遵循协议。您拿过去就可以用,却用在赚钱上,您有钱不妨招聘一个员工替您去写……

新加了Javascript代码部分,作者是楼下评论的那位,但是我修改了一些BUG(Ps:我通读代码之后,修改了一些错误,作者你仍然需要加强Javascript方面的能力)

26153920_yQTo.png

资源下载:

html_diff.class

js

小米抢购限流峰值系统「大秒」架构解密

整合的抢购限流峰值系统——「大秒」

2014年初,公司决定举办一场“米粉节”活动,全天6轮活动,多个国家、多款爆品同时参与抢购。业务场景将变得更加复杂,当天的并发压力也会有一个量级的提升,原有的抢购系统已经不能适应如此复杂的业务场景了。
为此,小米网技术团队基于对 golang 应对高并发、大规模分布式系统能力的肯定,完全基于 golang,重新设计了抢购系统,也就是我们目前使用的抢购限流峰值系统——“大秒”。

在整个系统设计的之初,我们充分考虑了

  1. 灵活性及可运营性;
  2. 可运维性及可伸缩性;
  3. 限流与抢购放号的精准性;

从大秒第一天诞生到演化至今有很多次重构与优化,但一直沿用了设计之初的结构,接下来我们一起了解下小米网抢购限流峰值系统当前的架构以及填过的一些坑。

大秒系统的架构设计

1

大秒系统主要由如下几个模块构成

  1. 限流集群 HTTP 服务
  2. 放号策略集群 Middle 服务
  3. 监控数据中心 Dcacenter
  4. 监控管理体系 Master
  5. 准实时防刷模块 antiblack
  6. 基础存储与日志队列服务: Redis 集群、Kafka 集群等

整个大秒体系中大秒前端模块 (HTTP/middle/antiblack) 和监控数据中心使用 golang 开发,大秒监控管理体系使用 Python + golang 开发。

大秒的前端架构设计

大秒前端的架构设计从三个系统展开

  1. 限流集群 HTTP 服务
  2. 策略集群 Middle 服务
  3. 准实时反作弊 antiblack 服务

2

1、限流集群 HTTP 服务

抢购高峰时,通常会有几百万的用户同时请求,瞬时流量非常大,HTTP 集群顶在最前线,接受用户的请求,将合法的请求发送的处理队列,处理队列设置一定的长度限制,通常情况下,抢购用户数与销售商品的比例在100:1,甚至更高,为了避免系统不被冲垮,保障绝大多数用户的体验,我们认为流量是部分可丢失的,当处理队列满时,丢弃入队请求;

虽然设计上过载流量是部分可丢弃的,但是策略层处理能力是非常 power 的,即便是需要丢弃流量,也是按流量的恶意程度,逐级丢弃的,正常用户购买请求不受影响。

我们使用基于规则的识别、离线画像信息、机器学习逻辑回归等方法,识别恶意用户,在系统高负载的情况下,这部分请求可以优先阻击其发送到策略层,优先处理正常用户的请求,保障用户体验过。

HTTP集群中不同节点之间的所持用的状态数据是一致的,处理逻辑也是一致的,所以整个集群中的任何一个节点挂掉,在前端负载均衡能力下,服务的准确性与一致性不受任何影响。

2、策略集群 Middle 服务
HTTP 模块将满足条件用户的请求按照 uid 哈希的规则,转发到 Middle 集群中相应的节点,Middle 集群根据商品放号策略判断 (uid:sku:time) 组合是否可以分配购买资格,并返回给相应的 HTTP 服务;

使用 Middle 服务本地内存维护用户的购买记录信息,支持各种购买规则,比如:单次活动不限购买数量,单次活动仅限购买一款商品,单次活动每款商品仅限购买一次。

我们将 Middle 的放号逻辑抽象成一个有限状态机,由商品的放号策略配置阈值来触发放号状态转换,整个配置由 Master 节点统一管理与调度。

为了提升整个系统的处理能力,我们将用户状态数据局部化,单用户(uid)的所有相关信息全部路由到一台 Middle 节点上处理。

但是有一点风险是,Middle 集群中服务可能会出现活动过程中挂掉的风险,在抢购场景下,商品基本上是瞬时卖完,为了保障系统的处理能力,我们主要从代码层面做优化,review 代码逻辑,保证服务应对异常的处理能力。

虽然理论上存在风险,但是在实际工程中,经历过几百次活动,还没出现 Middle 节点挂掉的情况。

3、准实时防刷 antiblack 服务

3

基于日志流的防刷架构,在每台 HTTP 节点上部署日志收集 Agent,使用高吞吐量的 Kafka 做日志转储队列,antiblack 模块实时分析用户请求日志,基于 IP 粒度、Uid 粒度等做防刷。

虽然此处将 antiblack 模块定义为准实时防刷模块,但是作弊信息识别的延迟时长在 1 分钟之内,其中主要的时延发生在日志的转储过程中。

大秒的监控管理体系

1、监控数据中心 dcacenter

4

监控数据中心数据种类

(1) 业务级数据:过大秒的商品配置数据与实时状态数据,当前活动的配置与状态数据等;
(2) 系统级数据: 大秒前端服务集群通信地址配置,限流队列初始长度配置,系统服务资源占用情况,包括:CPU、MEM、连接数等;

数据采集方式

同时使用push和pull模式采集业务级监控数据和系统级监控数据,业务级数据越实时越好,做到1秒采集处理,3秒可视化;

对于 HTTP 节点和 Middle 节点采用pull的模式拉去系统监控数据和业务监控数据,优点如下

(1) 灵活性高
由数据中心控制监控数据采集的粒度,在数据中心处理能力既定的情况下,可以根据前端集群的伸缩规模,灵活的调整数据采集的粒度,比如米粉节时,大秒前端集群扩容至过百台,管理的过大秒商品的数量在400个左右,业务级监控数据量很大,此时监控数据采集时间间隔很容易降配至 2s。

对于除Http服务和Middle服务之外的服务集群,如:redis,管理平台各个模块等可以使用监控数据采集agent,将采集到的数据周期性的push到redis队列,dcacenter采集协程实时的从redis队列中拉去消息,对于基础服务以及python实现的服务,增加了监控数据采集灵活性。

(2) 增强服务的可靠性与伸缩性

大秒在设计之初采用push的方式,在每台前端机器上部署一个数据采集agent,agent和大秒前端服务同时alive,才代表抢购系统健康运行。这样即增加了系统的不稳定因素,由不利于系统的伸缩,将监控数据采集逻辑内置到前端golang程序中,提供tcp管理端口,在数据中心使用pull方式采集数据,很好的解决了这个问题。减少了服务的数量,增强了整个系统的可靠性与伸缩性。

数据ETL与数据缓存

dcacenter同时负责将采集到的业务级数据及系统级监控数据,实时清洗,提取,转换,结构化,并将结构化的数据存储在自身内存中,定制通信协议(golang实现类redis通信协议),作为一个数据中心,对整个管理体系Master及其他系统提供实时数据支持。

将dcacenter直接作为数据中心,主要是出于数据的实时性考虑,省去中间转储环节,上层可视化系统、自动化活动控制系统、规则引擎系统等可以第一时间获得前端实时的销售状态数据及服务的状态数据。

2、监控管理中心 Master

监控管理中心的主要模块如下。

a.仓储库存同步服务StockKeeper
同步商品的仓储系统中的实时库存到秒杀系统,大秒系统拥有双库存保障,一个是实时仓储库存,一个是虚拟库存也就是资格号,在抢购场景下只有当两个库存都有货时,才能正常销售。

b.商品策略控制器PolicyKeeper
基于相应的策略触发器(时间区间与库存区间),当策略触发时,比如12点整,抢购开始,为相应的商品配置策略,并向大秒前端广播商品配置变更命令,在通信基础模块的保障下,整个过程秒级内完成。

c.活动自动化控制ActKeeper
基于监控数据中心获取大秒前端的实时销售数据,自动化的控制活动中的各个状态,活动开始前逐层打开开关,活动开始时打开最后开关,活动过程中维护活动的售罄状态,活动结束后初始化,整个抢购活动的过程无需人工介入;

d.数据可视化
从监控数据中心提取实时的结构化系统级监控数据和业务级监控数据,将活动过程中的详细数据实时可视化到管理页面上,让运营与销售以及大秒管理员能够及时了解当前活动状态,并人工干预活动;

e.监控规则引擎
监控规则引擎建立在监控数据中心之上,根据结构化监控数据判断当前整个抢购系统的状态,及时报警,以及半自动化控制。

f.其他
大秒管理端管理大秒前端所有的数据、配置以及状态,Master体系提供了详细的管理工具与自动化服务。如果清理大秒前端Middle服务中的用户购买信息等。

3、大秒配置管理数据流

5

整个抢购系统由 Master 体系中各个服务做统一的控制的,Master 控制商品状态及配置数据的变更,控制当前活动的状态,控制商品放号的策略等。

为了保证时效性,商品、活动、系统等配置状态的变更都需要将变更命令广播前端集群,这期间发生了大量的分布式系统间通信,为了保障命令及时下行,我们提取出了命令转发服务:MdwRouter,用于广播控制命令到大秒前端集群。该服务模块维护了到大秒前端长连接,接收 Master 下发的控制命令,并瞬时广播,保障了整个控制流的处理能力。

举个例子,2015 年米粉节,我们单机房大秒集群的规模在过百台级别,假设为 100 台,管理的独立的商品id的数量在 400 个左右,在这种量级的活动下,商品的放行策略是批量管理的,比如我们根据后端交易系统的压力反馈,调整所有商品的放行速度,这时候需要广播的命令条数在: 100*400=40000 级别,Mdwrouter 很好的保障了系统命令下行的速度,秒级完成命令下行。

小米抢购技术架构

1、小米抢购服务闭环设计

6

小米网抢购系统服务见上图

  1. bigtap体系中大秒前端服务负责抢购时限流放号,并控制放号策略以及维护用户在本地缓存中的购买记录。
  2. cart服务验证token的有效性,并向counter服务发起销量验证请求;
  3. counter服务是整个抢购系统最终的计数器, 海量的请求在bigtap服务的作用下已经被限制在可以承受的压力范围内,并且复杂的放号策略已经在大秒Middle服务中实现,counter只负责最终的计数即可。counter服务采用redis记录相应商品的放号情况,根据预设的销量,判断当前请求加购物车商品是否有库存余量,并维护商品销量;
  4. bigtap体系中的dcacenter服务实时采集商品销量,Master中活动自动化控制服务依据商品销量判断当前商品是否售罄,售罄则通过设置商品的售罄状态,并通知大秒前端;

2、2015年米粉节介绍

从上述整个服务闭环设计可以看出,大秒的功能完全可以抽象成限流系统,只有在处理抢购活动时,数据的管理与一致性要求才使整个系统变得复杂。

2015年米粉节,我们完全使用大秒的限流功能,不限用户的购买数量,很便捷的将系统部署在两个机房,一个物理机房,一个公有云集群,两者同时服务,大秒系统作为整个商城的最前端,能够根据后端服务的压力状态,瞬时调整整个集群放行流量大小,非常好的保障了整个米粉节的正常举行。
在上述文章中,已经介绍了一些服务设计的出发点,每一次优化的背后,都至少有一次惨痛的经历。

大秒系统架构的几点经验总结

1、Golang GC 优化方法

我们从 golang 1.2 版本开始在线上抢购系统中大规模使用,最初上线的 TC 限流集群在抢购的过程中通过过载重启的方式瘸腿前行。

在当前的大秒系统中,对于限流集群主要是 goroutine 资源、HTTP 协议数据结构、TCP 连接读写缓冲区等频繁动态开销,造成内存 GC 压力大,在现有 GC 能力下,我们对 GC 优化从以下几个方面考虑

  1. 减少垃圾产生:降低数据结构或者缓冲区的开销;
  2. 手动管理内存:使用内存池,手动管理内存;
  3. 脏数据尽快释放,增大空闲内存比。

我们使用了以下 3 种 golang GC 优化方法

1)定制 golang HTTP 包

调整 HTTP 协议 conn 数据结构默认分配读写缓冲区的大小,以及手动维护读写缓存池,减少动态开辟内存的次数,降低 GC 压力。

在 Go 语言原生的 HTTP 包中会为每个请求默认分配 8KB 的缓冲区,读、写缓冲区各 4K。而在我们的服务场景中只有 GET 请求,服务需要的信息都包含在 HTTP header 中,并没有 body,实际上不需要如此大的内存进行存储,所以我们调小了读写缓冲区,将读缓冲区调小到 1K,写缓冲区调小到 32B,golang 的 bufio 在写缓冲区较小时,会直接写出。
从 golang 1.3 开始,HTTP 原生的包中已经使用了sync.Pool 维护读写缓存池,但是 sync.Pool 中的数据会被自动的回收,同样会小量的增加 GC 压力,我们此处自己维护缓存池来减少垃圾回收。

2)加快资源释放
原生的 HTTP 包默认使用 keep-alive 的方式,小米抢购场景下,恶意流量占用了大量的连接,我们通过主动设置 response header 的 connection 为 close 来主动关闭恶意连接,加快 goroutine 资源的释放。

3)升级版本
跟进使用 golang 最新的版本,golang 后续的每个版本都有针对 GC 能力的调整。

得益于开源技术力量,以及大秒系统在 GC 优化上的努力,以及系统层的调优,我们的 HTTP 限流层已经可以余量前行。

7

从上图可以看出,得益于 GC 的优化,2015 年米粉节,每轮抢购,HTTP 服务的内存不会有特别大的抖动。

2、HTTP 服务器内存调优之操作系统参数调整

我们的服务场景下绝大多数的请求数都是恶意请求,恶意请求通常都是短连接请求,大量的短连接会处于 timewait 状态,几分钟之后才会释放,这样会占用大量的资源,通过调整内核参数,尽快释放或者重用 timewait 状态的连接,减少资源的开销。
具体参数调整如下:

net.ipv4.tcp_tw_recycle = 1 (打开TIME-WAIT sockets快速回收)
net.ipv4.tcp_tw_reuse = 1 (允许TIME-WAIT sockets复用)
net.ipv4.tcp_max_tw_buckets=10000  (降低系统连接数和资源占用,默认为18w)

高并发场景下,操作系统层网络模块参数的调整,会起到事半功倍的效果。

3、没有通信就谈不上分布式系统

整个大秒系统模块之间面临的通信要求是非常苛刻的,Master 节点与 HTTP、Middle 节点要频繁的广播控制命令,dcacenter要实时的收集 HTTP、Middle 节点的监控管理数据,HTTP 要将用户的购买请求路由到 Middle 节点之间,Middle 节点要返回给相应的 HTTP 节点放号信息;

我们基于 TCP 定制了简单、高效的通信协议,对于 HTTP 层和 Middle 层通信,通信模块能够合并用户请求,减少通信开销,保障整个大秒系统的高效通信,增加服务的处理能力。

4、服务闭环设计

从上述抢购的服务闭环架构中可以看出,整个抢购流程处理bigtap系统之外,还有 cart 服务,中心 counter 服务,这三者与 bigtap 系统构成了一个数据流的闭环,但是在大秒最初的设计中,是没有 counter 服务的,Middle层策略集群在放号的同时,又作为计数服务存在,但是整个抢购流程却是以商品加入购物车代表最终的抢购成功,这在设计上有一个漏洞,假如 bigtap 计数了,但是token 并没有请求加购物车成功,这是不合理的。为了保证整个系统的准确性,我们增加了计数器服务,计数操作发生在加购物车下游,bigtap 在从计数中心取出商品实时销量,由此,构成一个服务闭环设计。在提升了系统的准确性,同时也保证了用户体验。

5、技术的选择要可控

我们一开始选择使用 ZooKeeper 存放商品的配置信息,在抢购活动的过程伴随着大量的配置变更操作,ZooKeeper 的 watch 机制不适合用于频繁写的场景,造成消息丢失,大秒前端集群状态与配置不一致。

后来,我们将所有的配置信息存放在 Redis 中,基于通信模块,在发生配置变更时,伴随着一次配置项变更的广播通知,大秒前端根据相应的通知命令,拉取 Redis 中相应的配置信息,变更内存中配置及状态。

大秒的几点设计原则

1 分治是解决复杂问题的通则;我们从第一代抢购系统演进到当前的大秒系统,衍生出了很多服务,每个服务的产生都是为了专门解决一个问题,分离整个复杂系统,针对每个服务需要解决的问题,各个击破,重点优化。由此,才保障了秒杀体系整体性能、可靠性的提升;

2 服务化设计;系统解耦,增强系统的伸缩性与可靠性;

3 无状态设计,增强系统的伸缩性,提升集群整体处理能力;

4 状态数据局部化,相对于数据中心化,提升集群整体处理能力。

5 中心化监控管理,热备部署,既保证了服务的高可用性,又能够提升开发和管理效率。随着集群规模的增大以及管理数据的增多,分离管理信息到不同的数据管理节点,实现管理能力的扩容。通常情况下,中小型分布式系统,单机管理能力即可满足。

6 避免过度设计,过早的优化;小步快跑,频繁迭代。

7 没有华丽的技术,把细小的点做好,不回避问题,特别是在高并发系统中,一个细小的问题,都可以引发整个服务雪崩。

 

Q&A

1、实时仓库怎么避免超卖?

我们的抢购系统以加入购物车代表购买成功,因为用户要买配件等,库存是由计数器控制的,先限流,在计数,在可控的并发量情况下,不会出现超卖。

2、有了放号系统计算放号规则,为什么还需要一个外围的 counter?

主要是 bigtap 到 cart 的环节 token 有丢失,在 cart 之后再加一个计数器,保障销量,bigtap 再读取计数器的数据控制前端商品销售状态,整个延迟不超 3s。

3、HTTP 集群通过 uuid hash 到 Middle,如果目标 Middle 已经死掉怎么应对?

这个问题在文章中有强调,在我们的场景下,商品迅速卖完,这块没有做高可用,只是从代码层面做 review,完善异常处理机制,并且通常情况下,middle 负载不是特别高,几百次活动下来,还没出现过挂掉情况。

4、防刷系统是离线计算的吗,还是有在线识别的策略?

基于日志,准实时,因为请求量比较大,专门搭了一套 Kafka 服务转储日志,基于 golang 开发 logcollect 与 antiblack 模块,可以达到很高的处理性能。

5、请问如何模拟大量请求做测试?

我们遇到的情况是,由于压测机单机端口限制造成早期不好测试,我们这边压测团队基于开源模块开发了能够模拟虚拟IP的模块,打破了单机端口的限制。

6、即使广播和 Redis 拉取商品配置信息,仍有可能配置信息不一致如何解决?

这个主要是商品的配置和状态信息,不涉及到强一致性要求的场景,我们这样可以在秒级达到最终一致性。