开篇前言

前面我们说了体系化去打造系统监控、把监控和CR和性能调优联合在一起才能成功形成这个“闭环”,而没有直接我就开讲这么核心的一章“全景式的技术架构”。这是因为,像这样的一套复杂系统无论它是不是中台,但只要它是一个“流量端”的业务交易系统如:零售中的全渠道O2O平台或者是金融很行中的“银保、基金交易、小贷”类交易平台”或者是“要面临大流量的平台“那么它们都会面临:不可测流量的场景。当不可测流量到来时,一切原本运行了好好代码、SQL都会变形。

因此在发生问题时,如果没有一整套完整的监控体系打造我们是做不到快速反映、快速发现或者说找到“根源”性问题。

特别是根源性问题,改改可能最终就10条语句,但是很多公司(笔者亲眼见证过)几个月甚至半年都找不到问题的根源。这就是源于缺少了“体系化监控”的设计和打造。

因此前面一系列的博文各位可以认为只是“配菜”,现在才开始进入“做菜”环节。

这边我相信有人去过天津,吃过天津街道的早餐小食吧?我早年在北京生活了3年多,其中有一段时间就是在天津。

天津早上,7:00不到,11月中下旬这种天气,天气冷了,早早上街天还蒙蒙亮,路边老板一个手推车早饭铺子,做老面茶的。那铺子上摆着:面粉、牛肉馅、盐水虾、鸡蛋2个、大葱、香葱末、花椒粉、盐、香油、植物油、老抽,10多种。再排开八个或者铺子上横板够长也有排开十二个大海碗。一边烧着滚开的水,一边是海碗里面都是已经配好的底儿。

来一客人:老板,来一老面茶。

老板吆喝一声:来了您哎!

然后老板拿那个长柄大勺,往滚开的水里这么一撩,往那海碗里这么一浇。端到您面前:趁热慢吃了您呢。

然后客人喝一口老面茶、咬一口果子煎饼,那叫一个香、那叫一个暖。

所以技术架构它本身就是一种技术型活儿,技术型活儿忌讳的就是眉毛胡子一把抓。因此越是复杂的东西,我们越是在之前花点时间把一些预先准备、提前量都做足了。

到时一合成、一集成,这东西就成了。

忌讳现打现造、也忌讳一股脑堆上线。

如前文所述,我们至少是有一个框子,它里面提供了那些业务模型db table和基本的CRUD,也有框架级的MQ、HTTP、Redis、Mongo等调用API,只是不是太strong。因此我也隐约感觉到了这后面的故事呢。。。会长得很。

所以我把监控体系推进到接近完善,于是我就开始围着这层框子(只有业务功能模块的CRUD)开始架构我们的系统了。

这个系统的目标是:百家门店、至少300万用户、可以应对双11或者打折大促时发生的瞬时万级并发交易。

我手里的预算极其有限,因为我们比不了那些大厂,但是现实业务目标又不低。于是经过一段时间的设计以及最终经过了各种波折,我们的系统的最终形态是长下面这个样的。

可面对1秒1.7万TPS、5万QPS的零售O2O全渠道技术架构

各位看了这个图上的点就知道为什么我说这个系列会达百万字,因为每一个点我都是会用专文去展开分析的。

同时,这个架构它并不是一蹴而就的。它也是经过了各个实际生产发生的case以及若干大促,尤其是这次疫情期间突然暴发的在线买菜带来的“大流量”的考验。

标题上的这个值我说了有点保守,实际观察我们遭遇过7万QPS的量。在这种量下系统不白屏、不卡顿、顺畅可供下单、正常显示“库存卖空了”。。。真心我们的团队打得太猛了、太顽强了。我们的领导指挥也相当的给力。大家朝着这个系统架构的目标竟然硬是靠着有限的硬件和人力把它给完成了。相当的不易。

我们回首来看,其实这样的一套架构看看点很多,真正如果用得合理合法,我这样告诉你吧,你完全可以在有限硬件的条件下以中型的投入规模实现天猫、淘宝这种系统的性能。

用更白话一点来比喻:刘备3万人马照样可以火烧赤壁大败曹操百万雄兵。

治理互联网流量尤如治理洪水非一蹴而就

由于我之前经历过两个从0打造的中台,因此我在这一次系统架构设计和调优时有了长足的经验,其实一开始我手上就有一张这样的“蓝图”了。只是领导给力全力支持着我不断的朝着这个蓝图推进并最终完成了它。

而对于绝大多数大、中型企业来说,在一开始是根本不具备这种体系化蓝图的意识的。这就是为什么往往很多团队在现出问题时:头痛医头痛,脚痛医脚痛,眉毛胡子一把抓的原因。

因此,我才掩去所有敏感信息,只对技术做公开展示。因为这些技术,说白了,每一个点其实网上多多少少都有讲解。

关键的还是在于这套“体系化”打法。

我们说,做一个互联网应用,我们都知道这么几个老四样:

  1. 要有CDN
  2. 要有WAF
  3. 要有NG
  4. 要有Redis

对不?最多加一条,优化慢SQL。

各位看看这个图,有多少?远远不够的这老4样。

因此,我们需要从互联网流量这个东西来说起。因为流量是我们的“业务目标”,只有业务目标才可以带来以结果为导向的技术底层架构。

流量是不可测的它宛如洪水、暴风、雪崩倾刻间发生因此需要“层层削峰”

有人说,我们现在都是云。我们用弹性计算不就可以应对流量的突增了吗?

也对也错!

对是对在弹性计算只是占了所有手段中的百分之一的成份(没错,1%,不能再多了)。错是错在以为有了云计算、有了弹性就有了一切了?

我亲身在近7年多来经历过的真正的流量场景是下面这样的,您平平这个味。

一秒内170万个不同的、还是真实的请求一起对着你的注册访问来一下、对着你的搜索来一下。全天两亿多的总Http请求落在你的网站上。

我们说用云计算,好,我们来用,我们假设一个调优到极致的tomcat,一个tomcat一秒1,000个连接,我上线时有20个tomcat做这个底,往100个弹。100*1,000这才10万个并发?

那么再弹,1000个tomcat。。。等等,等等,这导致的硬件成本已经是大厂的规模了。其实就算你弹的出,也不够顶的。为什么就算弹出了1,000个tomcat都不够?

每个tomcat里有没有Redis连接、有没有mongo连接、有没有db连接。

每个tomcat里我们设有3,000个redis连接、3,000个mongo连接、3,000个mysql连接。。。好家伙,1,000个tomcat,300万个redis连接,300万个mongo连接。。。来来来,你redis、mongo跟着升吧。

升级、扩完相应的中间组件一通折腾后,那么再来弹tomcat吧,一堆成本投下去刚把系统重启后,结果这发觉这个乐子更大了。之前你只看到了170万个ip这么过来访问了一下是因为到170万你的系统已经“死挺”了。

而现在因为你扩了、所以这次在10分钟内看到了1,000万的请求一秒内来做各种混合并发的访问。。。那么我们说tomcat可以继续扩,相应的中间件呢?跟着一起扩吗?那就扩呗。。。再来一轮、10分钟又崩掉,再来一轮、二轮。。。?

你去试试看,网上各大云厂商有公开的“云资源计费模式计算器”,你把1,000个tomcat所需要的CPU、内存、网络IO、磁盘(SSD)还有几十个各种中间组件如:mongo、redis(不要多,你算redis 6个,mongo 3个分片每个分片3节点做replic模式)输进去,再把流量算出来,平均一G算它0.3元的成本加进去。。。一年加在一起请问:多少个百万没了。。。你去数一下这个零。。。1,2,3,4.。。哎呀。。。我刚投了这么多钱还是只挺了10分钟。。。而且这个流量持续到下午17:00,你等于9:00-9:10分钟挺了一下,然后从9:10分开始你的系统一直死到下进17:00都没有营业。。。你觉得你会是什么结果?

你的硬件的扩张速度是永远跟不上流量的增长速度的!

流量,它是一个像漏斗样的东西,你的硬件的集群永远只处在最最底层这一层,上面的流量如洪水一样,你根本看不到“边”。

那怎么办?这样大家都没得玩了。

因此,我们才把要进入“对流量层层削峰”手法的介绍。

我这边把流量画成了一个漏斗,在流量行进过程当中,进行了:6层削峰、四道防线

而最最底层的那道防线还作了:五横三纵的互联网式的应用架构设计。哪五纵三横啊?

五纵:

  • 纵一、任何即时类交易不要用mysql,更不得使用db锁,要把mysql沦为存储而不是参与业务、逻辑的运算;
  • 纵二、API层必须要有限流、模块间必须要有熔断、降级设计;
  • 纵三、把To B端当成To C端的性能要求来设计和开发;
  • 纵四、异步MQ去保护后台交易、提交、支付、供应链端应用;
  • 纵五、用Redis加速API的访问速度(一般使用redis和不使用redis可以相差千倍性能);

三横:

  • 横一、DB设计规范以及慢SQL规范,特别是对于单SQL在基于交叉数据千万级数据的底上进行查询的时间不得超过500ms;
  • 横二、系统日志、历史数据要可archive(归档),即需要有明确的字段、标识以便于DEVOPS、DB巡检维度,不断去发现不合理设置,尽可能提前对系统进行扩、缩、Archive动作;
  • 横三、代码设计规范包括:
    1. Java编码规范;
    2. 多线程开发规范;
    3. Redis使用规范;
    4. Http使用规范;
    5. MQ使用规范;
    6. 异步跑批设计编码规范;
    7. 封装规范;
    8. 系统日志输出规范;
    9. 代码安全规范;
    10. MVC规范;
    11. 长事务处理设计规范
    12. 所有规范的日常、定期及抽查Review机制

特别是对于“规范”,我们的规范不仅仅是告诉你“不该做什么、应该做什么”,而是以这样的形式去告诉开发和设计者们曾经有一个什么样的血坑、导致了什么样的的惨痛教训,怎么避免的具体细节以及直接上正反例代码,然后再告诉你因此为了所有人的“幸福”你必须要遵守一个什么样的规范

规范这一块我们内部就撰有数十万字,一个个都是直接可以运行的Sample,从而形成了我们内部技术规范上的一套“军令”,规范在我们这就是红线、是雷线,谁碰了就“炸”谁。

大家可以把5纵(层)想像成一个空心漏斗,“水(流量)”是无缝不渗透的。这才有了这三个横向的维度形成了包裹着这个漏斗的三层防洪堤。

因此我才在上图中把应用架构设计、代码、慢SQL给画作“底线”的最终原因。如果这一层底线我们没有守好,那么它的结果就是“到处漏水”。如果换成家里装修房子后每天到处不是这漏气、就是那漏水,你得多心烦?因此它是底线!

这部分我会在后面以一个个“血案”和“触目惊心”来展示给各位看此篇暂不作展开。

6层削峰、四道防线的原理

这是我们本章的重点,即原理。

上文中提过了,这不是一个理论模型、也不是一个意想天开。是实际并成功经历过高并发场景的系统设计模型。

它已经不需要论证了,因为它正运行在我们的生产环境中、和我历来经历的三个生产环境内。并且系统“刚刚的”。任何一个技术不存在“黑盒”和什么特别高深的机密。

关键在于它们的组合应用,可能我是第一个这样成体系化把它表现出来的人而己。

第一道防线中的那些个DDOS、CC、BOT、OWASP TOP 10是指的什么

当前有一这股流量,持续1小时进来,累积为1.6亿次。这1.6亿次流量里。。。你觉得有多少真实的?20%?30%?50%?这不是一个拍脑袋的问题,而是个层层“削除”的问题,我们来看科学的思考:

各位想一下,你不是淘宝、不是二微一抖,凭什么你的流量有这么大?你的业务量在每天达到了千万、亿了吗?

如果没有这高的业务量,那么流量是你业务量的2,000倍,这正常吗?它们都是真的用户吗?

我们一直几乎是每天都会碰到黑产来搞,这是很正常的一件事而关键是要能够精准把“不要的舍弃”,要的留下。

我这边只说了“精准”而不是“100%可以识别黑产”,因为和黑产、恶意流量斗是一种“博弈学”,它早就超出了“技术范畴”。当一个企业或者一个人追求“100%打掉”时,对于真实业务的损失也会达到很大的比重。

因此我们用“精准”,实际这里面会漏过一些恶意流量,因为这些漏过的,真心不太好判断,那么漏过的怎么办?

能怎么办?这些漏过的就要靠自身系统去扛了。这就是为什么我前文中所述:系统安全高地和系统性能高地的当中有一条交通壕,它们不能孤立的、剥离的去看问题,而是要联系在一起来看系统的性能问题。我们来看下面几个点:

1. 你是一个APP,访问的人的http里的user-agent写得明明白白是http client4.1或者是:user-agent=浏览器,这样的请求你要它干吗?因为正常通过APP来访问你的人user-agent都为:mobile。中台跨系统、或者与第三方对接走的是ip白名单绑定或者是基于jwt/oauth协议的,怎么可能会有什么To C端进来的请求带着browser、httpclient?因此我们可以直接切掉它;

2. 目前网上代理泛滥,在你的APP里的请求是一个国内IP实际背后的使用者是来自于什么开曼群岛、索罗门群岛。你的公司只在大陆有生意,送货也只送大陆。然后来了一堆坦桑尼亚、开曼群岛的IP来领你的优惠券。。。这种流量你要它吗?也可以切掉它;

3. 一个同样的IP在一秒钟内点击了1,000次添加购物车。。。这个手速。。。你点点试试,如果是一个正常的人真的拿着APP,用手点击来这个速率我算你牛。有人说了:我用网上类似于“筋膜枪”那种机械点击是不是也可以达到这个速率这不是合法的吗?好吧,我回答你:我要切掉的场景也包括这种“碎屏机器人”,因为。。。这不是正常人可以做得出的事,切了;

4. BOT,这个东西特别讨厌,我在之前:家乐福618安全与性能保卫战(一)-安全高地保卫战里提到过一个“BOT有多少类型”的一个“BOT攻击类型概括”如下图。各位还记得吧?对于这一块BOT又有“三道最前端防火墙”去抗衡我也会在后面展开这“三道BOT防火墙设计”;

5. 基本的数据泄漏、SQL注入、脚本注入、JS注入,这些你得防吧?这一套叫OWASP TOP 10规范,违犯了这些规范的流量,你要么?

以上这5个点,全部有一种东西,WAF!用它去防,即Web Application Firewall,目前好的WAF都是有这些功能的,你所要做的就是按照策略一个个去用鼠标勾勾选选就全部可以完成上述这些工作了。

CDN和图片无损压缩是什么梗

先说一下什么是CDN

一般来说我们都知道一个互联网应用必须要有CDN。因为CDN起到了一个近源静态文件的加速作用。

比如说你的应用在上海,而用户遍布五湖四海。

我们的应用一般都会有一些个.js、.jpg、.gif、.html这些静态内容。如果说你的应用有一部分用户人在西安,你就算是走云的布署,但云的站点物理地点是在上海,那么用户的手机在云南,而从上海站点的服务器上去下载一堆的这种静态文件,不仅仅是网路链路很长同时万一遇到了网络拥堵风暴,这是很影响用户的手机端应用体验的,其表现就是手机上的页面打开时如果遇有一些图片或者是静态内容一点点、逐步的加载显示出来这种效果,这在零售、电商、金融场景其实属于block bug一类级别的issues,因为零售、金融、电商是非常讲用户体验的。

这个CDN的数据流设计上除了图片我们会做这种“预”处理,是因为需要提高用户第一次的打开速度和体验。对于.js、.html一类的其它静态内容,CDN就和Redis一样,它会先找CDN内有没有缓存,没有的话走一次“源站”获取真实内容,然后放在CDN上以便于下次用户再来访问时直接从CDN返还而不走源站了。

再说一下有了CDN为什么又要无缩压缩呢

我们打开手机APP上的淘宝、JD、或者是一些如:家乐福、沃尔玛、JD超市等应用或者是小程序上去看看。是不是在一些商品介绍时有不少“动图”啊?

这些动图都做的一个个鲜艳动人、让人垂延欲滴。还有不少“高保真”大图,如:三门冰箱、烘干机、烤鸡、草霉那个水灵灵的。

这种图片如:GIF类一个这样的动图动辄就达30多兆,一个高保真的图片动不动要17兆。

这样的一个尺寸的图片就算走了CDN,但是它在被CDN从你的源站抽取到网上以及在用户的手机端加载时,这个速度得多慢?整个首页、分类、商品详情都是那种“逐步一点点显示、加载图片”的体验,你们觉得这样的APP和小程序,谁能忍受?

于是业务这时跑过来了,告诉IT:这些大图片、高保真的加载太慢了。能不能在用户打开的第一次就提高图片加载速度?

出于这个需求,我们才有了无损压缩技术。所谓无损压缩,说白了就是把个30多兆、17兆的图片压到100K都不到同时还能显示的照样很正常。是不是挺夸张的。只是绝大部分人不知道这个事因此觉得很夸张。

这边我先打一下招呼,这个技术不是程序员自己用GIF或者是JPEG流打开后等比例压缩这么简单的事。这是无损算法而是非常复杂的算法。

因此,一般好点的云上的CDN会有额外的一个“外挂”,这个外挂就是“图片无损压缩”。它自动会把用户的请求里的图片做预先压缩,压缩后告诉商城应用、系统被压缩后的url是什么样的,然后系统、应用在显示图片时用无损压缩系统压缩后还给系统的压缩过几十倍后图片所在的url来显示这个图片就可以了。

在原有的CDN上如何接入无损图片压缩算法的“A/B Testing布署“方式,我们采用的是A/B Testing发布即金丝雀发布手法,这样做的目的是减少因为代码改动影响范围、逐步把一些图片替换成“无损压缩算法”。

原有数据流设计不浪费,我们继续可以用。

虽然好的云厂商的无损压缩本身就已经带了CDN功能(选择SAAS化无损压缩在选型时必备条件-就是它本身已经带了CDN功能或者说和CDN是一体的)。但是一个金融或者是零售类的To C端应用,涉及到静态内容包罗万象、千变万化,静态内容指的不是光图片就够了。因此不可能把100%的静态全部从CDN迁到无损压缩上去,因此如我经历的几个大型零售系统,至少有25%左右的静态内容还是保留在了CDN。因此这个数据流在原有的基础上就变成了下面这个样子了:

经过这种带有“金丝雀发布的”,可以做AB切换的,逐波次的一点点把一些图片往无损压缩上迁移时,即保证了风险不会太高(极个别图片会出现被压缩后出现图片损坏-GIF类,这个损坏率很低低到可忽略不计)又可以达到前端访问特别是用户第一次打开时的体验提升的效果。最终我们达到了这样的一个效果。

第二道防线在干什么

NG优化:没错,这一层的NG最好是按照云原生走PAAS模式。目前几家好的云厂商的NG的PAAS已经相关完备了。这一块不展开,主要是网上太多了,但我可以给出我调优的核心配置,很关键的一个点是必须启用ng内的epoll机制。

#user  nobody;worker_processes  6; #error_log  logs/error.log;#error_log  logs/error.log  notice;#error_log  logs/error.log  info; #pid        logs/nginx.pid;  events {    use epoll; mac用不了, linux内核2.6以上用得到    worker_connections  60000;}  http {    include       mime.types;    default_type  application/octet-stream;     #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '    #                  '$status $body_bytes_sent "$http_referer" '    #                  '"$http_user_agent" "$http_x_forwarded_for"';     #access_log  logs/access.log  main;     sendfile        on;    tcp_nopush on;  #tcp_nopush 来控制它,并且只有在启用了 sendfile 之后才生效。启用它之后,数据包会累计到一定大小之后才会发送,减小了额外开销,提高网络效率    tcp_nodelay on;  #TCP_NODELAY 也是一个 socket 选项,启用后会禁用 Nagle 算法,尽快发送数据,某些情况下可以节约 200ms    gzip on;  #开启资源压缩    gzip_http_version 1.1;    gzip_vary on;    gzip_comp_level 6;    gzip_proxied any;    gzip_types application/atom+xml       application/x-javascript       application/javascript       application/json       application/vnd.ms-fontobject       application/x-font-ttf       application/x-web-app-manifest+json       application/xhtml+xml       application/xml       font/opentype       image/svg+xml       image/x-icon       text/javascript       text/css       text/plain       text/xml;    gzip_buffers 16 16k;    gzip_min_length 1k;    gzip_disable "MSIE [1-6]\.(" />

这是一张标准的新零售行业中的微服务化组件从前到后的架构概览图。

我们都知道,凡是静态内容一般我们都有cdn来进行缓存,cdn缓过的内容之前会从cdn处返回给到前端流量层客户端。

但是,往往我们有一些这样的东西,相信大家并不陌生,如:http://localhost:8081/service/getBigResponse?userid=ymk,然后该api接口会返回下面这样的一个json串。

    [{"name":"abc","barcode":"","quantities":"33","price":"12.00","id":504,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":505,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":506,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":507,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":508,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":509,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":510,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":511,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":512,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":513,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":514,"userid":"ymk"},    {"name":"abc","barcode":"","quantities":"33","price":"12.00","id":515,"userid":"ymk"},

这个json串,一般在企业级环境特别是零售电商都不下千行,假设我们说它的大小是25k。

25k?大吗?不大!

好,我们套上你的真实生产环境,并发我们说假设1万,1万的并发其实很小,除非你之前的项目实在是。。。此处省略一些字!

我们把1万并发*25k=差不多要245mb的流量,而且这个1万并发是每秒哦,我们假设这样的行为持续了:5分钟,那你的网站从前到api层、到bkend层都就会被造成:5*60*245mb的压力。

所以,业界在很早前就已经相过,我们是不是可以把api里,根据访问路径,每一个参数如:?userid=ymk和userid=abc这样的请求后返回的不同的值也可以类似cdn一样缓存起来?

很可惜,一些主流的cdn暂时不支持这种缓存,因此在5年多前我们是自己在gateway处,书写相应的规则来作到根据请求的url、参数名的不同而缓存访问后的响应的。

这样,我们就可以直接在web这一层直接把用户请求的同样的内容给返回回去了,一方面减少了网站的整体压力另一方面加速了前端访问的请求。

不过,需要注意的点是

不是什么东西都可以这样做缓存的!特别是一些实时性很强的数据如:下完订单库存的显示也是动态的扣减的,这种就不可以把订单请求给缓存哦,这会造成数据上的“脏读”。

那么什么东西可以被缓存呢?我举一例来说:sku为红富士产地南海a-001的相关产品信息,这种信息就可以使用“缓存”,因为你的这个sku除非被“下架”,要不然,它的信息是始终不会变的,那么不管全国是a还是b还是c来访问这个sku,你的这个sku信息都是不会在短时间内改变的,对吧?

那么我们为了解决根据api的请求、参数的不同而缓存不同的商品,大家试试看可以自己利用redis来写这个url解析,如果你只是解决一个请求,那么你会觉得写一个这样的解释器并不难,但如果你要考虑电商、o2o类网站内上千个url的处理,光一个这样的解析器你可能就要开发好几个月呢!

所以我们才有了varnish,varnish就是用来解决类似上述这样的web层api请求访问缓存用的,它是一个开箱即用的开源组件,你不需要写那么复杂的http协议、url路径解压,你要做的事就是:哪一些url请求需要被缓存,把它们一个个列进varnish,varnish自动会根据请求的url、参数的不同而动态的去缓存不同的返回内容。
关于Varnish的具体使用可以看我这一篇博客,也是生产级环境搭建,这一篇我之前写的技术点在此完成了“它的闭环”。这就是我说的体系化工程打造的一个表现:API接口的加速利器-varnish使用大全(含生产集群环境布署)

黑产对抗

到第二道防线Varnish这一关,流量从一开始的我们假设1亿流量,往往会被削掉(其实就是“挡掉”的意思)387%(实际生产环境中我们这得到过的数据中的一个)。

我们继续打、继续削、继续扛。

这边的黑产对抗指的是一种对抗机制。大家看到过这样的场景吧?在一些大厂的应用内会有:输错两次就会出现一个弹窗,弹窗里让你用鼠标按照提示的4个汉字的顺序依次去窗体内一堆汉字中做点击,点对了而且顺序对了才让你进入到继续下一步的请求,连续输出了几次后就会出现:往后再每输错一次都会出现弹窗了。有些应用还会提示用户”进入错误次数倒计时“,再错几次这个用户会在15分钟内不能再来访问应用了。

另一种,每次做一个”HVT“即High Value Target(高价值目标)业务操作时如:领券、领红包、抢购时,每次都会出现一个滑块,让你滑一下。

上面这两个例子,都属于“黑产对抗”。黑产对抗的作用是这么一个意思:

图形是滑块、点汉字、还是转圆圈图形把它转正都是表现手法倒并不是最主要的。主要核心关键的是通过这个用户的操作过程在基于上述:user agent, cc对抗、ddos对抗、bot对抗的基础上会漏过一部分请求,这部分被漏过的请求属于高仿真机、仿真实客户请求。我们需要通过这种用户交互操作过程来识别出这一部分请求是“人还是机”,这一过程的内在本质就是“黑产对抗、人机识别”。

  • 用user agent去判断,可以筛掉一大批非法请求,但这部分请求是属于很low层次的无水平攻击而己;
  • 用cc、ddos、bot筛掉的是low层次,也只比user agent非法请求的水平高了那么一点点;

而漏过上述判断的攻击,从表面怎么看怎么像一个真实的人,但怎么还有那么多、几千万的请求呢?

我不信有这么大的真实流量。

于是,我们引入了黑产对抗,所以此处黑产对抗又叫“人机对抗”。

我们拿手机APP应用来说,高水平黑产可以在手机内开虚拟机、用仿真器来模拟APP真实请求,一个仿真器内可以开出若干个“APP应用”,每一个请求头还不同,什么HUAWEI、IPHONE、XIAOMI,怎么看着怎么像个“人”。

而黑产对抗就是利用用户去“玩图”的那段时间来判断正在发生的这个请求到底是不是真人?这里面有两层判断:

  1. 根据用户发起请求时,埋于本企业APP应用内的“探针”,会在HVT请求时对本机是否有蓝牙、是否有CCMOS、是否为市面上常见的伪真器、是否有MAC地址等等等,这些硬件不仅仅要判断是否有还要“心跳”一下看看是不是模拟的ccmos或者是蓝牙模块这些因素我们把它们综合在一起判断才可以有概率(还是会有遗漏)筛选出这一部分可能是机,仅仅是可能;
  2. 因此才有了第二层,因为如果是一个人它在界面操作时有一个“离散的熵值”,而机和人的熵值的区别是有明确的边界点的。因此你在“玩图”时得出的这个“熵”就是一个判断依据,如Google自带的recaptcha(是免费的,只是国内不能用,它是开源的API,用于人机识别)判断,在打开HVT页面或者是业务操作行为时它会有一个半透明的浮层、要你等一下、滑几下鼠标,然后出现一个绿色勾才允许你继续下一步的操作就是这种人机对抗;

硬机判断+行为判断这两个判断的层次不可孤立而行,一孤立就会有大概率误伤,而结合起来就可以精准的判断出当前操作不是人,是机。如果不是人,那么很简单了,不断的弹窗让它去“玩图”,那么如果请求过来的是“机”那么机是很难过的了这两层判断的。因此就可以挡掉大部分这种“高仿”请求了。

看,这个过程就叫对抗。或者也叫人机识别。

经过了人机识别、人机对抗,你不一定说可以把这个原有流量上再往下降多少个百分点,这和减肥一样,越到后面越觉得哪怕减下去一公斤都觉得很难。在这个黑产对抗(人机识别)的过程中我们可以启到另一个至关重要的作用就是:“把原来的海量并发,打散、强制排队”。以次来削低整体应用面临的不合理流量。

动态限流

Spring Cloud的API GATEWAY,我们用的是最新的API GATEWAY 2.0并在此之上改动量达到70%以上,我们新增出来了一堆原有API GATEWAY没有的功能,新增功能量达到了原先的27倍。

其目的就在于“动态限流”。

我们知道,当流量特别大或者漏过前面几层防御后的流量“汹涌过来后”而此时我们的系统依旧没有完成整体的5纵3横的整体调整和规划时,这个“底线”就很容易被流量击跨、冲破。

请各位记住一句口决,整个系统的性能无非就是“外练金钟罩,内吃大补丸”。就是意指:外面打的是黑产、内部加强自身性能。

在性能还没有完全到位前怎么办?那么就是限流,这是一个没有办法的办法,你必须要有一个限流。

而且这个限,是热生效的。改一个数值系统上就生效了,不需要做任何的服务重启。

比如说我系统目前只能吃得了:一秒2,000的流量,你硬撑要吃一秒10,000并发,系统崩了这种打肿脸去充胖子的事也是没意义的,那么此时就只能舍卒保车了。这个策略其实和当Redis集群挂了时你会怎么办一样的道理。那你恢复集群需要30分钟时间,在这30分钟里你难道一单不做?那么事先代码设计时有一个“DB兜底或者Mongo兜底“此时此景就可以用DB或者是Mongo扛了,虽然此时整体系统应用响应会比原来慢的多、体验也变差了,但是在此时系统能进来一单再怎么讲这也算是一单总比你一单未进要好吧。要知道蚊子再小也有几毫克肉,多多少少是一点是吧?

因此当发生了流量超越了你系统现有可以承受的并发值时,就得限。

所以我们的API GATEWAY在第二道防线上,此时这个流量其实已经达到了应用层了,API GATEWAY它在应用层就是“第一道守门员”。

限了就完了吗?

好,我限了!

用户前台受到一个http error 403或者就是一个生硬的”前方排队“,这个是不是体验相当的不好?

那什么样的限流会更友好呢?此时还要结合”业务“来做这个限流。限住了没有问题,你得给用户一个”安抚“方式吧?

我们说:

  1. 直接显示一个http error 403,这个肯定没人会采纳;
  2. 前方拥堵页面显示,这个似乎好一点但还没有真正要达到我们的目的;

那么我们从以下几个点来思考限流后理想状态应该是什么:

  • 对于某一个API做限流,用户不是被“杀伤”而只是对于该API的访问进入“排队”,并且可以让用户在访问相关API时还能看到一个“安抚”页面
  • 受限的API暂时无法访问,系统中其它相关的页面照样可以使用?
  • 当受限的API队列空闲时此时用户再访问该API看到的就是真实的页面了
  • 以上系统的挂安抚页面、撤安抚页面不为人干涉,一切根据“预设阀值”自动处理

看,业务导向决定了你的技术设计。因此这个设计在我们一开始规范我们的系统应用时就要做好这种设计和预留。

技术上我们以为我们做到这些点已经足够了,但是此时运营部的领导又来了,他说:我理解IT要保护系统、IT调优也需要时间。但是就算你们这样限流了,有一部分用户还是永远的流失了。能不能把这些流失的用户里多多少少能挽留住10%的人也好吗?蚊子再小。。。也有几两。。。

IT一听。。。一边说蚊子,但实际要的是两。。。你看到过城市里哪里的蚊子有用两去称的?这得多大一个蚊子呢?唉。。。算了,不理他说什么,我们做到力所能及吧。于是限流设计开始进入进一步的演变。

大家可以看到,2014年前淘宝、JD等大厂的双11流量高峰时都是我上面说到的这一类限流,到了2014年后它的限流开始变化了。

它变化成了这么一种应用限流模式:可以局部限、限住的地方可以手工刷新,没有问题的一些界面照样可以点。

然后到了2015年中左右,业界出现了更进一步的限流的做法。即:自动自刷新。

你在双11秒杀时,点了一下“抢”,界面按钮点下去后显示“你的请求接受中请稍侯“。

用户稍侯了或许10秒或许15秒,最终这个界面会自己变成”处理完成/处理失败/已售馨”。这种就叫自动回放。

请求回放的核心设计思路

那么“限流”基本上就需要考虑这些个点,它并不是简单的舍弃,而是可以把这个舍的“抛弃系数”降到最低。

这也是为什么我们对Spring Cloud API GATEWAY 2.0进行了深度开发的原因。这一部分的源码一点无法开放和展示产,因为这是我们的一种独门工具了,它里面还含有本企业业务规则和一部分“自学习、自建模”,但是它的核心设计思路就是像以上这此卜素、本质的需求演变而来的。

系统熔断的设计

很多人都知道微服务、很多人都知道微服务中有熔断。可是同样也是很多人的架构用了微服务却一点没有经历过什么是真正的“微服务场景”或者真的用到了微服务的特性。这也是由于大部分的业务场景、系统复杂度、用户量、交易量达不到而导致的。因此微服务一时间似乎成了一个时髦、fashion的名词而己。

我经历了3次重大系统重构、优化、建设,微服务在我们这真的被100%的所有的“概念”都用到了。其中最有感受的就是熔断。

熔断上的设计不是一个事中、事后的事,而应该是一个事前的事。

这也是为什么我这要写这一系列博客的原因。我在此写出的这些不是理论、而是已经经历过真实高并发高流量环境的实战中的总结。所以深刻意识到无论是限流还是熔断,它们的设计必须定位在设计的初期阶段就需要布局。

为什么这样说?

我们来看微服务里的熔断,拿spring cloud 2.0来说,它有两个很重要的阀值:

  1. 出错率;
  2. 保护窗口;

99%的人用到了出错率,出错率spring cloud 2.0默认设为该请求如果在你设置的时间窗口里的请求的出错率在10秒内超过了50%就断开(断路器)。

但很多人忘记了一件事,熔断了。。。代表这根服务或者这根请求已经出错了。你断开了做的没问题。那么下一秒呢?继续流量进来,那么再断开。。。流量再进来,再断开。

很可能这根服务接口是因为吃不消承载了所以才会触发“断开”。此时你应该做的是尽快找到原因、快速找到替代方案。或者把这个请求“偏”到其它备用服务上。

而如果你只是一味的“断开“,那么当发生这一类问题时,你的这条服务它在做的事就是永远的不断的断开,而不留给你有任何的”补救手段和时间“,甚至你断开了。。。也因为请求没有真正和业务功能模块断,于是和这根接口有关的级联服务也开始出现了堆积,进一步造成了级联雪崩。

这就是很多系统某个服务就算”断开“了,系统还是照样崩了的原因。

因此我们才有一个保护窗口。保护窗口就是指:一旦触发了熔断,请你给我留一口缓过气来的时间。

这两者结合去使用才能在技术上正确使用好熔断。

技术上使用好了熔断就够了吗?业务还是受影响呀。

有人此时会说了:都熔断了,还怎么不受影响!

别急。。。此时我们用业务的思维模式可以极大程度帮助你做好这个架构:蚊子再小也有几两。。。

所以我们在熔断后是不是有兜底方案呢?

我举例来说:Elastic Search的搜索服务挂了,此时我的APP上搜索不能用和Elastic Search提供的搜索服务了,但是我走Redis搜索这条路依然是好的只是搜的内容、准确度不如Elastic Search的搜索。但是这个应用在使用上还能用呀?

这就叫兜底

看到没。。。从单纯断开到保护窗口到兜底。这一系列设计是一个完整的闭环思路,因此我的建议是如果有可能这种设计阶段越前置越好。

尤其是这个兜底方案,它完全是需要业务一起来配合的。我再举一例:

O2O里会有一种场景,就是你下单后系统返回一个“预计送达时间”。这个预计送达时间其实是在下单时把用户的收单地址和门店的地址送到“第三方外卖平台”里,第三方外卖平台用地图算法算完后返回你的系统这个“预计送达时间”的。

现在这根第三方外卖平台提供的接口“挂”掉了。。。不要以为这不可能,我们碰到过不知道多少起这样的第三方平台引起的生产事故了(微服务设计中原则上“互不信任”都要做好熔断超时兜底的设计)。当初没有做好兜底就造成了不佳的用户体验。而后期再去改这个动静就会动到这根“主流程”,付出的代价就会相对来说“更大”!

那么这个预计送达时间一直得不到呀,显示为空呀!怎么改进?

O2O平台一般都会对客户有一个许诺的最长送达时间如:1小时达,1个半小时达。

那么第三方外卖平台的这个预计送达时间接口返回时“挂了”,马上此时代码走“业务兜底”去系统里预设的一个“最长送达时间”,拿了这个时间去给前端APP显示。

这样就构成了这个业务闭环了。

所以熔断的设计不仅要从技术上去考虑合理、有效的熔断更要在业务上把损失做到最底。

用一个比较恰当的比喻:填报考大学志愿时100分完美进一本,80分进二本。好过于:100分进一本而99分因为我没有填二本志愿然后我就“待业”了。

如果不能GET得到那么我再来作一个比喻:你已经快饿死了这时给了一碗白米饭,可是你说因为没有小饮料、因为没有火腿冬瓜汤、因为没有糖醋开口活鱼所以我不吃。然后不吃的下场是饿死。吃了可以活。这就叫“业务兜底方案”。

所以整个微服务中的熔断设计必须要和业务一起配合设计这个兜底。因此我才说了,这个设计如果有可能越提前进行这样的设计越好。

第三道防线在干什么

在这道防线上,我们的流量经过了前两道“防线”其实已经落进入我们的应用层了。这时这个流量较之之前已经“纯净”了不少。

但还是那个“漏斗筛选”原理,依旧需要层层削削峰。因此下面开始进入继续削峰上的设计。

Redis布隆

我之前的博客中有过详细叙述。技术、细节、实现这边就不多提了。主要要讲解的是,大都数企业、架构师和IT开发团队不知道这个Redis布隆的作用或者说就算知道了也不觉得有必要上一个Redis布隆防御。

这样告诉大家吧,我这边近5年对于Redis布隆的使用上,在极端打掉过79%的“非法请求”,少的每天打掉7%左右的非法流量,平均每天可以打掉12-13%左右的“非法”请求。

什么是非法请求,此时我们就要结合本企业业务来看这个布隆的设计了。

典型案例:我们要做一个注册时间超过1年的会员才能领5元券活动。于是设计来了,用Redis、用双缓存、用锁,都没错。然后会员是3,000万数据,请求来了1亿多,真的。我还常碰到,黑产就是这么恶心。

1亿多请求对着这个领券点击,你都吃下来?可以,不是说Redis利害吗?Redis唯快不破吗?好吗,你开一个10万connection的连接池,让1亿这个量级的请求来访问。

领券动作伴随着不是简单的一个返回code成功不成功、还有绑定券和手机号、还有扣减数量、还有判断。。。1亿个这么来一下,你的Redis爆了。。。你的Redis不爆,你的业务模块也得爆几个。再不信。。。那我没办法了,那么。。。你可以真的去试试看。

那么回过头来我们被打了个头破血流,怎么防呢?就是redis布隆。

我们来看这么一种会引来大规模流量的业务需求:注册时间超过1年的会员才能领!

换句话说,不满一年的、领券当天才注册会员的这一类的领这个“门槛券”的会员的那些个请求你系统都可以统统不要的。

于是我们在API GATEWAY处设计一个定时/手工跑批,在活动开放前从3,000万会员里把符合业务门槛条件的会员差不多1,100万条数据都选出来,然后做成Redis布隆值塞到Redis里(占用资源很少,不要只看到1,100万这个数字觉得太大,其实很少的)以Redis布隆的方式作防御。然后在活动开始时,假设现在有一个请求过来了,那么判断一下这个会员如果都不在Redis布隆里,这条请求也就不要往后继续走下去了,在API GATEWAY处直接返回一个“不符合参加活动条件的Code“给到前端这事就完了。

不要小看每天平均打掉的”非法“请求只有12-13%,告诉大家一句口决”系统性能的改善上:勿以善小而不为、勿以恶小而为之“,况且这都不需要什么特殊的脑力、精力,只是一个1+1=2的简单实施问题而己。

逻辑漏洞防御

逻辑漏洞堵住了能提高系统性能?

太能了!来,实例解说。

逻辑漏洞例子:有一个应用,它可以添加收货地址。收货地址添加没有限制。哈哈哈,于是当天被人注册了100万会员,每个会员30个地址。然后这100万会员也不需要并发就在10分钟不到内提交他们的购物车内容。然后最惨的是在提交购物车时后端会做这么一件事:收获地址和门店间距离的校验以确认是不是在配送范围内,然后系统上由于每个会员没有设计默认地址,然后逻辑上就依次把用户的所有收货地址和门店地址循环校验一下。然后你的购物车模块就会“崩溃”。或者第三方地图系统把你系统给限流了,这么一个提交甚至都超过了第三方提供地图服务的限流了,这得有多惨?

如何改进呢?

一个会员我们允许他多一些地址也没错,但是购物那一刻是不是这个会员一定会用的是当前地址?当前地址只有一个,那么就拿它当前的地址去后台做一个和购物门店的匹配不就完了?因此业务在设计上时就应该把获取客户当前下单时的收货地址作为默认地址去做后续匹配。客户也可以点一下首页的“定位”按钮,更改他的当前收货地址。

我们曾经问过业务,为什么你们要把这个匹配去放到提交购物车这一步才去匹配?

业务说:为了让客户尽可能快的下单不要在下单时多几次点击,因此一开始让客户“选择”的步骤少一点。

其实这是一个朝三暮四的问题,手机类应用客户操作越方便越好是没错,但是再省也不能省这种步骤呀?

当然,对于逻辑漏洞引起的性能问题是技术才会发觉的。因此技术要在一开始设计时基于本企业、本业务领域在事先就要想到这一类问题。这也是本系列的作用。

横向垂直越权

其实它也是属于一种“逻辑漏洞”,只是造成的危害性更严重,来看一个例子:

横向越权例子: 大都APP或者是小程序没有“加固”,没有“加固”的小程序和APP是可以被“抓包”和反编译的,而且很简单。抓包后,我把订单查询时的订单ID最后一位改一个阿拉伯数字。好家伙这造成了一个在上海客户看到了不属于自己的在北京的另一个客户的订单详情了,然后这个订单上还有北京那个客户的收货地址、手机。。。用户四要素都泄漏了。。。这就叫横向越权!出了这种事网安都要找上门了。

黑产清洗规则

此时我们的流量是已经处于应用层了。因此它的流量内依旧存在着许许多多的可疑流量。你即不能精准的知道这个非法请求比例在多少?也不可能100%的杜绝所有的黑产。

这就回到了我们前文中提及的“精准”二字。

IT系统上对恶意请求的”精准“不只是依靠单一技术解决的,它需要众多技术+业务逻辑形成一个个的小闭环,才能做到”精准“。

因此对于一次、两次我们允许”漏过黑产“,但如果永远放过了那些”黑产流量“后,时间一久堆积在那边就容易量变引起质变。

因此我们会要不断清洗黑产,把前几次允许漏过的黑产在后续通过业务模型清洗出来,那么当这样的黑产非法请求再来时就不允许再进入了,所以我们才有了”清洗规则“。

清洗也分事后清洗和事中清洗。

举一个“事中”清洗的规则,相当有用,很多人都没想到:

说有一商城在上海、苏州、无锡的店提供100元省50的券仅限于在上海、苏州、无锡的店内和线上同时使用。然后来了比平时多1万倍的流量,流量中大都是注册了好几个月甚至1年的会员来领券,把券给领了不说还把系统给搞挂了。于是我们打开流量一分析,这些来抢”券“的收货地址五花八门:有广东、四川、云南、东北、江西、上海、湖北、湖南。。。开曼群岛。。。那么我们反问一句:这个券只可以用于上海、苏州、无锡的线上和线下商城使用那么一个人是一直在广东的收货地址、历史痕迹、手机号也是在广东的人、手机使用时的请求定位也是可以确定人在广东,这个人的领券的这个请求行为系统为什么要去接受?

这就是根据业务逻辑的“事中”分析。至于它的手机号定位、人在请求手机商城应用时的定位这些数据只要你使用的是成熟点的、好一点的云厂商是完全可以做得到”实时“获取和判断的呀。

那么我们在API GATEWAY处把这一类”事中“数据清洗(用实时计算类技术或者是手法)规则给一个个设上,又可以避免一次系统被冲击,不是吗?

本篇结束语

第三道防线说了上半部分,下半部分以及后面深蓝色部分我没有在本篇说, 这是为什么呢?

就如我上文中提到的:

其一、到现在为止我们的流量已经落入到了应用层了,第三道防线的下半部分以及再往下”渗透“就属于:五纵三横的体系范畴了。对于五纵三横体系我会着重用一个个实例结合着去讲解,每一个实例就是一个惨痛的教训。

因为这一大块只有伴随着大量的实例,我们才可以有着深刻的认知和感受、才能引起大都数人的重视和在自己的工作中避免去踩这些”血坑“或者说是”地雷“。

总结

本篇所描述的技术架构不存在任何的核心机密、也不是一本天书。一个个技术点全部在网上到处都有一大堆资料。唯一缺少的就是把它们去体系化的编织在一起,并且聪明的去做出一些”取舍“。在我看来这一点不属于什么高技术难度。一切只需要时间、刻苦加上实事求是的态度就可以到达的事,都不属于高科技,只是一个”认知“的问题。它们只是我使用了这样的一套体系经历了三个大型中台回首的一个总结。

这套体系你已经不用再去证明它有没有用,在我这近5年多来,这样的一套体系打出了在原有系统性能上提高了:2,000倍吞吐、整体的API接口在前端9,000-10,000并发请求全系统>700张表,单表数据超过1千万情况下全站API平均响应时间<75毫秒的战绩。差不多提高了3,000多倍原有系统性能。关键是我还没用了太多硬件费用。

在第一个大型项目建设时我摸索了一段时间,用了5个月建立完成这样的一整套体系和落地,后面两个中台由于熟练度到了加上有了各方面的best practice,因此也不过只用了3个月不到就完成这一套架构的实现了。

在本篇中我们有几句口决我在这边再给各位提示一下:

  1. 6层削峰、4道防线;
  2. 五纵三横;
  3. 漏斗式削峰流量;
  4. 系统性能调优:勿以善小而不为,勿以恶小而为之;

后面篇章我就会逐步展开,进入到五纵三横的详细叙述中去。同时在五纵三横里又处处存在着“漏斗式削峰”和“系统性能调优:勿以善小而不为,勿以恶小而为之”的哲理。因此整体系统的调优不能孤立的看问题它是一件体系化的事情,系统整体性能调优就是以这样的一个个的小闭环,再去把这些小闭环一个个拼接成一件“鱼鳞锁子甲”用来防身的过程。