⑴ 数据多的时候为什么要使用redis而不用mysql
通常来说,当数据多、并发量大的时候,架构中可以引入Redis,帮助提升架构的整体性能,减少Mysql(或其他数据库)的压力,但不是使用Redis,就不用MySQL。
因为Redis的性能十分优越,可以支持每秒十几万此的读/写操作,并且它还支持持久化、集群部署、分布式、主从同步等,Redis在高并发的场景下数据的安全和一致性,所以它经常用于两个场景:
缓存
判断数据是否适合缓存到Redis中,可以从几个方面考虑: 会经常查询么?命中率如何?写操作多么?数据大小?
我们经常采用这样的方式将数据刷到Redis中:查询的请求过来,现在Redis中查询,如果查询不到,就查询数据库拿到数据,再放到缓存中,这样第二次相同的查询请求过来,就可以直接在Redis中拿到数据;不过要注意【缓存穿透】的问题。
缓存的刷新会比较复杂,通常是修改完数据库之后,还需要对Redis中的数据进行操作;代码很简单,但是需要保证这两步为同一事务,或最终的事务一致性。
高速读写
常见的就是计数器,比如一篇文章的阅读量,不可能每一次阅读就在数据库里面update一次。
高并发的场景很适合使用Redis,比如双11秒杀,库存一共就一千件,到了秒杀的时间,通常会在极为短暂的时间内,有数万级的请求达到服务器,如果使用数据库的话,很可能在这一瞬间造成数据库的崩溃,所以通常会使用Redis(秒杀的场景会比较复杂,Redis只是其中之一,例如如果请求超过某个数量的时候,多余的请求就会被限流)。
这种高并发的场景,是当请求达到服务器的时候,直接在Redis上读写,请求不会访问到数据库;程序会在合适的时间,比如一千件库存都被秒杀,再将数据批量写到数据库中。
所以通常来说,在必要的时候引入Redis,可以减少MySQL(或其他)数据库的压力,两者不是替代的关系 。
我将持续分享Java开发、架构设计、程序员职业发展等方面的见解,希望能得到你的关注。
Redis和MySQL的应用场景是不同的。
通常来说,没有说用Redis就不用MySQL的这种情况。
因为Redis是一种非关系型数据库(NoSQL),而MySQL是一种关系型数据库。
和Redis同类的数据库还有MongoDB和Memchache(其实并没有持久化数据)
那关系型数据库现在常用的一般有MySQL,SQL Server,Oracle。
我们先来了解一下关系型数据库和非关系型数据库的区别吧。
1.存储方式关系型数据库是表格式的,因此存储在表的行和列中。他们之间很容易关联协作存储,提取数据很方便。而Nosql数据库则与其相反,他是大块的组合在一起。通常存储在数据集中,就像文档、键值对或者图结构。
2.存储结构关系型数据库对应的是结构化数据,数据表都预先定义了结构(列的定义),结构描述了数据的形式和内容。这一点对数据建模至关重要,虽然预定义结构带来了可靠性和稳定性,但是修改这些数据比较困难。而Nosql数据库基于动态结构,使用与非结构化数据。因为Nosql数据库是动态结构,可以很容易适应数据类型和结构的变化。
3.存储规范关系型数据库的数据存储为了更高的规范性,把数据分割为最小的关系表以避免重复,获得精简的空间利用。虽然管理起来很清晰,但是单个操作设计到多张表的时候,数据管理就显得有点麻烦。而Nosql数据存储在平面数据集中,数据经常可能会重复。单个数据库很少被分隔开,而是存储成了一个整体,这样整块数据更加便于读写
4.存储扩展这可能是两者之间最大的区别,关系型数据库是纵向扩展,也就是说想要提高处理能力,要使用速度更快的计算机。因为数据存储在关系表中,操作的性能瓶颈可能涉及到多个表,需要通过提升计算机性能来克服。虽然有很大的扩展空间,但是最终会达到纵向扩展的上限。而Nosql数据库是横向扩展的,它的存储天然就是分布式的,可以通过给资源池添加更多的普通数据库服务器来分担负载。
5.查询方式关系型数据库通过结构化查询语言来操作数据库(就是我们通常说的SQL)。SQL支持数据库CURD操作的功能非常强大,是业界的标准用法。而Nosql查询以块为单元操作数据,使用的是非结构化查询语言(UnQl),它是没有标准的。关系型数据库表中主键的概念对应Nosql中存储文档的ID。关系型数据库使用预定义优化方式(比如索引)来加快查询操作,而Nosql更简单更精确的数据访问模式。
6.事务关系型数据库遵循ACID规则(原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)),而Nosql数据库遵循BASE原则(基本可用(Basically Availble)、软/柔性事务(Soft-state )、最终一致性(Eventual Consistency))。由于关系型数据库的数据强一致性,所以对事务的支持很好。关系型数据库支持对事务原子性细粒度控制,并且易于回滚事务。而Nosql数据库是在CAP(一致性、可用性、分区容忍度)中任选两项,因为基于节点的分布式系统中,很难全部满足,所以对事务的支持不是很好,虽然也可以使用事务,但是并不是Nosql的闪光点。
7.性能关系型数据库为了维护数据的一致性付出了巨大的代价,读写性能比较差。在面对高并发读写性能非常差,面对海量数据的时候效率非常低。而Nosql存储的格式都是key-value类型的,并且存储在内存中,非常容易存储,而且对于数据的 一致性是 弱要求。Nosql无需sql的解析,提高了读写性能。
8.授权方式大多数的关系型数据库都是付费的并且价格昂贵,成本较大(MySQL是开源的,所以应用的场景最多),而Nosql数据库通常都是开源的。
所以,在实际的应用环境中,我们一般会使用MySQL存储我们的业务过程中的数据,因为这些数据之间的关系比较复杂,我们常常会需要在查询一个表的数据时候,将其他关系表的数据查询出来,例如,查询某个用户的订单,那至少是需要用户表和订单表的数据。
查询某个商品的销售数据,那可能就会需要用户表,订单表,订单明细表,商品表等等。
而在这样的使用场景中,我们使用Redis来存储的话,也就是KeyValue形式存储的话,其实并不能满足我们的需要。
即使Redis的读取效率再高,我们也没法用。
但,对于某些没有关联少,且需要高频率读写,我们使用Redis就能够很好的提高整个体统的并发能力。
例如商品的库存信息,我们虽然在MySQL中会有这样的字段,但是我们并不想MySQL的数据库被高频的读写,因为使用这样会导致我的商品表或者库存表IO非常高,从而影响整个体统的效率。
所以,对于这样的数据,且有没有什么复杂逻辑关系(就只是隶属于SKU)的数据,我们就可以放在Redis里面,下单直接在Redis中减掉库存,这样,我们的订单的并发能力就能够提高了。
个人觉得应该站出来更正一下,相反的数据量大,更不应该用redis。
因为redis是内存型数据库啊,是放在内存里的。
设想一下,假如你的电脑100G的资料,都用redis来存储,那么你需要100G以上的内存!
使用场景Redis最明显的用例之一是将其用作缓存。只是保存热数据,或者具有过期的cache。
例如facebook,使用Memcached来作为其会话缓存。
总之,没有见过哪个大公司数据量大了,换掉mysql用redis的。
题主你错了,不是用redis代替MySQL,而是引入redis来优化。
BAT里越来越多的项目组已经采用了redis+MySQL的架构来开发平台工具。
如题主所说,当数据多的时候,MySQL的查询效率会大打折扣。我们通常默认如果查询的字段包含索引的话,返回是毫秒级别的。但是在实际工作中,我曾经遇到过一张包含10个字段的表,1800万+条数据,当某种场景下,我们不得不根据一个未加索引的字段进行精确查询的时候,单条sql语句的执行时长有时能够达到2min以上,就更别提如果用like这种模糊查询的话,其效率将会多么低下。
我们最开始是希望能够通过增加索引的方式解决,但是面对千万级别的数据量,我们也不敢贸然加索引,因为一旦数据库hang住,期间的所有数据库写入请求都会被放到等待队列中,如果请求是通过http请求发过来的,很有可能导致服务发生分钟级别的超时不响应。
经过一番调研,最终敲定的解决方案是引入redis作为缓存。redis具有运行效率高,数据查询速度快,支持多种存储类型以及事务等优势,我们把经常读取,而不经常改动的数据放入redis中,服务器读取这类数据的时候时候,直接与redis通信,极大的缓解了MySQL的压力。
然而,我在上面也说了,是redis+MySQL结合的方式,而不是替代。原因就是redis虽然读写很快,但是不适合做数据持久层,主要原因是使用redis做数据落盘是要以效率作为代价的,即每隔制定的时间,redis就要去进行数据备份/落盘,这对于单线程的它来说,势必会因“分心”而影响效率,结果得不偿失。
楼主你好,首先纠正下,数据多并不是一定就用Redis,Redis归属于NoSQL数据库中,其特点拥有高性能读写数据速度,主要解决业务效率瓶颈。下面就详细说下Redis的相比MySQL优点。( 关于Redis详细了解参见我近期文章:https://www.toutiao.com/i6543810796214813187/ )
读写异常快
Redis非常快,每秒可执行大约10万次的读写速度。
Redis支持丰富的数据类型,有二进制字符串、列表、集合、排序集和散列等等。这使得Redis很容易被用来解决各种问题,因为我们知道哪些问题可以更好使用地哪些数据类型来处理解决。
原子性Redis的所有操作都是原子操作,这确保如果两个客户端并发访问,Redis服务器能接收更新的值。
丰富实用工具 支持异机主从复制Redis支持主从复制的配置,它可以实现主服务器的完全拷贝。
以上为开发者青睐Redis的主要几个可取之处。但是,请注意实际生产环境中企业都是结合Redis和MySQL的特定进行不同应用场景的取舍。 如缓存——热数据、计数器、消息队列(与ActiveMQ,RocketMQ等工具类似)、位操作(大数据处理)、分布式锁与单线程机制、最新列表(如新闻列表页面最新的新闻列表)以及排行榜等等 可以看见Redis大显身手的场景。可是对于严谨的数据准确度和复杂的关系型应用MySQL等关系型数据库依然不可替。
web应用中一般采用MySQL+Redis的方式,web应用每次先访问Redis,如果没有找到数据,才去访问MySQL。
本质区别1、mysql:数据放在磁盘 redis:数据放在内存。
首先要知道mysql存储在磁盘里,redis存储在内存里,redis既可以用来做持久存储,也可以做缓存,而目前大多数公司的存储都是mysql + redis,mysql作为主存储,redis作为辅助存储被用作缓存,加快访问读取的速度,提高性能。
使用场景区别1、mysql支持sql查询,可以实现一些关联的查询以及统计;
2、redis对内存要求比较高,在有限的条件下不能把所有数据都放在redis;
3、mysql偏向于存数据,redis偏向于快速取数据,但redis查询复杂的表关系时不如mysql,所以可以把热门的数据放redis,mysql存基本数据。
mysql的运行机制mysql作为持久化存储的关系型数据库,相对薄弱的地方在于每次请求访问数据库时,都存在着I/O操作,如果反复频繁的访问数据库。第一:会在反复链接数据库上花费大量时间,从而导致运行效率过慢;第二:反复地访问数据库也会导致数据库的负载过高,那么此时缓存的概念就衍生了出来。
Redis持久化由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时mp到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。
redis是放在内存的~!
数据量多少绝对不是选择redis和mysql的准则,因为无论是mysql和redis都可以集群扩展,约束它们的只是硬件(即你有没有那么多钱搭建上千个组成的集群),我个人觉得数据读取的快慢可能是选择的标准之一,另外工作中往往是两者同是使用,因为mysql存储在硬盘,做持久化存储,而redis存储在内存中做缓存提升效率。
关系型数据库是必不可少的,因为只有关系型数据库才能提供给你各种各样的查询方式。如果有一系列的数据会频繁的查询,那么就用redis进行非持久化的存储,以供查询使用,是解决并发性能问题的其中一个手段
⑵ 关于API网关(四)——限流
通俗的说,流量控制就是控制用户请求的策略,主要包括:权限、限流、流量调度。
权限上一篇已经讲过了,这一篇讲限流,下一篇讲流量调度。
限流是指限制用户调用的频率(QPS/QPM)或者次数。
流量限制,站在用户或者运营的角度看,最直观能感受到的作用是——收费
各大主流开放平台的对外API,一般都有一些免费的额度,可以供个人测试用,一旦想大规模调用,就需要付费购买更大的额度(频率、次数),根据调用次数或者频率进行收费。一旦超过拥有的额度,就会被限制调用。
其实这才是限流最大的用处,只是用户或者运营同学无感,所以不太被大多数人了解。
网关后面是各个服务,各个服务的接口通过网关透出去给用户调用。理论上说,用户的流量是不可预知的,随时可能来一波,一旦流量的峰值超过了服务的承载能力,服务就挂了,比如有大新闻发生时的某浪微博,比如前些年的12306.
所以, 网关必须保证,放过去到达后端服务的流量一定不可以超过服务可以承载的上限 。这个上限,是网关和各个服务协商出来的。
由简到难,限流可以 分为单机限流、单集群限流、全集群限流 。
这里不讨论具体的如漏桶、令牌桶等限流算法,只说概念和思想。
单机限流的思想很简单,就是每个机器的限流值 x 机器数量 = 总的限流值。
举个例子,A用户的QPS限制是100,网关部署了10台机器,那么,每台机器限制10QPS就可以了。
先说好处,这种方法实现起来非常简单,每台机器在本地内存计算qps就可以了,超过阈值就拒流。
不过单机限流的缺陷也十分明显,主要体现在两点:
当网关部署的机器数量发生变化时,每台机器的限流值需要根据机器数调整。现实中,因为扩容、缩容、机器宕机等原因,机器数的变化是常有的事。
单机限流的前提是,每台网关承载的用户的流量是平均的,但是事实上,在某些时间,用户的流量并不是完全平均分布在每台机器上的。
举个例子:
10台机器,每台限qps10,其中3台每台实际qps是15,因为超限导致用户流量被拒。其余7台每台qps是7。这样用户总的qps = 15 * 3 + 7 * 7 = 94. 用户qps并没有超限,但是却有一部分流量被拒了,这样就很有问题。
实际上,单台限流的阈值也会设置的稍微大一些,以抵消流量不均的问题。
因为上面的问题, 单机限流通常作为一种兜底的备用手段,大多数时候用的还是集群限流 。
先来看一个示意图:
相比单机限流,集群限流的计数工作上移到redis集群内进行,解决了单机限流的缺陷。
但是集群限流也不是完美的,因为引入了redis,那么,当网关和redis之间的网络抖动、redis本身故障时,集群限流就失效了,这时候,还是得依靠单机限流进行兜底。
也就是说, 集群限流 + 单机限流配合,才是一个比稳妥的方案 。
接下来我们来思考这样一个问题:大型网关一般都是多机房、多地域部署的,当然,后端的服务也是多机房、多地域部署的,在保护服务这一点来说,集群限流是够用了。但是对用户来说,还是有一些问题:
比如,用户购买的QPS上限是30,我们的网关部署在中国北、中、南三个地域,那么这30QPS怎么分配呢?
平均肯定不行,用户的流量可能是明显不均衡的,比如用户的业务主要集中在中国北方,那么用户的流量大部分都会进入北方的网关,网关如果限制QPS为10的话,用户肯定来投诉。
那每个地域都限制为30行不行?也不行,如果用户的流量比较均匀的分布在各个地域,那么用户购买了30QPS,实际上可能使用了90QPS,这太亏了。
按照解决单机限流流量不均的思路,搞一个公共的redis集群来计数行不行?
也不行,受限于信号传播速度和天朝的广阔疆域,每个流量都计数,肯定不现实,rt太高会导致限流失去意义,带宽成本也会变得极其昂贵,对redis的规格要求也会很高。总之,很贵还解决不了问题。
有一种巧妙的解决办法是:本地集群阶梯计数 + 全集群检查。
还是刚才的例子:
限流阈值时90,那么三个地域各自计数,当本地域的数值达到30时,去其他两个地域取一次对方当前的计数值,三个地域的计数值加起来,如果超了,告诉另外两个地域超了,开始拒流。如果没超,本地QPS每上涨10,重复一次上述的动作。
这样就能有效的减少与redis的交互次数,同时实现了全地域真·集群限流。
当然,这种全地域集群限流,因为rt和阶梯计数间隔的存在,一定是不准的,但是,比单集群限流还是好很多。
当某个用户流量特别大的时候,redis计数就会遇到典型的热点key问题,导致redis集群单节点压力过大, 有两种办法可以解决这个问题:打散和抽样。
打散是指,把热点key加一些后缀,使其变成多个key,从而hash到不通的redis节点上,均摊压力。
比如热点key是abcd,那么打散后,key变成了abcd1、abcd2、abcd3、abcd4。技术时,轮流加1、2、3、4的后缀就可以了。
抽样是指,针对热点key,不是每个每个请求到来时都进行计数,而是进行一个抽样,比如每10个请求记一次数,这样redis的压力就会降低到十分之一。
说着把流量调度的也说完了哈哈,那下一篇再说说监控好了,顺便推一下我现在在用的国产网关:GOKU,来自Eolinker。我觉得比KONG好用,感兴趣的同学可以自行去了解一下。
www.eolinker.com
⑶ 高并发,你真的理解透彻了吗
高并发,几乎是每个程序员都想拥有的经验。原因很简单:随着流量变大,会遇到各种各样的技术问题,比如接口响应超时、CPU load升高、GC频繁、死锁、大数据量存储等等,这些问题能推动我们在技术深度上不断精进。
在过往的面试中,如果候选人做过高并发的项目,我通常会让对方谈谈对于高并发的理解,但是能系统性地回答好此问题的人并不多。
大概分成这样几类:
1、对数据化的指标没有概念 :不清楚选择什么样的指标来衡量高并发系统?分不清并发量和QPS,甚至不知道自己系统的总用户量、活跃用户量,平峰和高峰时的QPS和TPS等关键数据。
3、理解片面,把高并发设计等同于性能优化 :大谈并发编程、多级缓存、异步化、水平扩容,却忽视高可用设计、服务治理和运维保障。
4、掌握大方案,却忽视最基本的东西 :能讲清楚垂直分层、水平分区、缓存等大思路,却没意识去分析数据结构是否合理,算法是否高效,没想过从最根本的IO和计算两个维度去做细节优化。
这篇文章,我想结合自己的高并发项目经验,系统性地总结下高并发需要掌握的知识和实践思路,希望对你有所帮助。内容分成以下3个部分:
高并发意味着大流量,需要运用技术手段抵抗流量的冲击,这些手段好比操作流量,能让流量更平稳地被系统所处理,带给用户更好的体验。
我们常见的高并发场景有:淘宝的双11、春运时的抢票、微博大V的热点新闻等。除了这些典型事情,每秒几十万请求的秒杀系统、每天千万级的订单系统、每天亿级日活的信息流系统等,都可以归为高并发。
很显然,上面谈到的高并发场景,并发量各不相同, 那到底多大并发才算高并发呢?
1、不能只看数字,要看具体的业务场景。不能说10W QPS的秒杀是高并发,而1W QPS的信息流就不是高并发。信息流场景涉及复杂的推荐模型和各种人工策略,它的业务逻辑可能比秒杀场景复杂10倍不止。因此,不在同一个维度,没有任何比较意义。
2、业务都是从0到1做起来的,并发量和QPS只是参考指标,最重要的是:在业务量逐渐变成原来的10倍、100倍的过程中,你是否用到了高并发的处理方法去演进你的系统,从架构设计、编码实现、甚至产品方案等维度去预防和解决高并发引起的问题?而不是一味的升级硬件、加机器做水平扩展。
此外,各个高并发场景的业务特点完全不同:有读多写少的信息流场景、有读多写多的交易场景, 那是否有通用的技术方案解决不同场景的高并发问题呢?
我觉得大的思路可以借鉴,别人的方案也可以参考,但是真正落地过程中,细节上还会有无数的坑。另外,由于软硬件环境、技术栈、以及产品逻辑都没法做到完全一致,这些都会导致同样的业务场景,就算用相同的技术方案也会面临不同的问题,这些坑还得一个个趟。
因此,这篇文章我会将重点放在基础知识、通用思路、和我曾经实践过的有效经验上,希望让你对高并发有更深的理解。
先搞清楚高并发系统设计的目标,在此基础上再讨论设计方案和实践经验才有意义和针对性。
高并发绝不意味着只追求高性能,这是很多人片面的理解。从宏观角度看,高并发系统设计的目标有三个:高性能、高可用,以及高可扩展。
1、高性能:性能体现了系统的并行处理能力,在有限的硬件投入下,提高性能意味着节省成本。同时,性能也反映了用户体验,响应时间分别是100毫秒和1秒,给用户的感受是完全不同的。
2、高可用:表示系统可以正常服务的时间。一个全年不停机、无故障;另一个隔三差五出线上事故、宕机,用户肯定选择前者。另外,如果系统只能做到90%可用,也会大大拖累业务。
3、高扩展:表示系统的扩展能力,流量高峰时能否在短时间内完成扩容,更平稳地承接峰值流量,比如双11活动、明星离婚等热点事件。
这3个目标是需要通盘考虑的,因为它们互相关联、甚至也会相互影响。
比如说:考虑系统的扩展能力,你会将服务设计成无状态的,这种集群设计保证了高扩展性,其实也间接提升了系统的性能和可用性。
再比如说:为了保证可用性,通常会对服务接口进行超时设置,以防大量线程阻塞在慢请求上造成系统雪崩,那超时时间设置成多少合理呢?一般,我们会参考依赖服务的性能表现进行设置。
再从微观角度来看,高性能、高可用和高扩展又有哪些具体的指标来衡量?为什么会选择这些指标呢?
2.2.1 性能指标
通过性能指标可以度量目前存在的性能问题,同时作为性能优化的评估依据。一般来说,会采用一段时间内的接口响应时间作为指标。
1、平均响应时间:最常用,但是缺陷很明显,对于慢请求不敏感。比如1万次请求,其中9900次是1ms,100次是100ms,则平均响应时间为1.99ms,虽然平均耗时仅增加了0.99ms,但是1%请求的响应时间已经增加了100倍。
2、TP90、TP99等分位值:将响应时间按照从小到大排序,TP90表示排在第90分位的响应时间, 分位值越大,对慢请求越敏感。
3、吞吐量:和响应时间呈反比,比如响应时间是1ms,则吞吐量为每秒1000次。
通常,设定性能目标时会兼顾吞吐量和响应时间,比如这样表述:在每秒1万次请求下,AVG控制在50ms以下,TP99控制在100ms以下。对于高并发系统,AVG和TP分位值必须同时要考虑。
另外,从用户体验角度来看,200毫秒被认为是第一个分界点,用户感觉不到延迟,1秒是第二个分界点,用户能感受到延迟,但是可以接受。
因此,对于一个 健康 的高并发系统,TP99应该控制在200毫秒以内,TP999或者TP9999应该控制在1秒以内。
2.2.2 可用性指标
高可用性是指系统具有较高的无故障运行能力,可用性 = 正常运行时间 / 系统总运行时间,一般使用几个9来描述系统的可用性。
对于高并发系统来说,最基本的要求是:保证3个9或者4个9。原因很简单,如果你只能做到2个9,意味着有1%的故障时间,像一些大公司每年动辄千亿以上的GMV或者收入,1%就是10亿级别的业务影响。
2.2.3 可扩展性指标
面对突发流量,不可能临时改造架构,最快的方式就是增加机器来线性提高系统的处理能力。
对于业务集群或者基础组件来说,扩展性 = 性能提升比例 / 机器增加比例,理想的扩展能力是:资源增加几倍,性能提升几倍。通常来说,扩展能力要维持在70%以上。
但是从高并发系统的整体架构角度来看,扩展的目标不仅仅是把服务设计成无状态就行了,因为当流量增加10倍,业务服务可以快速扩容10倍,但是数据库可能就成为了新的瓶颈。
像MySQL这种有状态的存储服务通常是扩展的技术难点,如果架构上没提前做好规划(垂直和水平拆分),就会涉及到大量数据的迁移。
因此,高扩展性需要考虑:服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等,当并发达到某一个量级后,上述每个因素都可能成为扩展的瓶颈点。
了解了高并发设计的3大目标后,再系统性总结下高并发的设计方案,会从以下两部分展开:先总结下通用的设计方法,然后再围绕高性能、高可用、高扩展分别给出具体的实践方案。
通用的设计方法主要是从“纵向”和“横向”两个维度出发,俗称高并发处理的两板斧:纵向扩展和横向扩展。
3.1.1 纵向扩展(scale-up)
它的目标是提升单机的处理能力,方案又包括:
1、提升单机的硬件性能:通过增加内存、 CPU核数、存储容量、或者将磁盘 升级成SSD 等堆硬件的方式来提升。
2、提升单机的软件性能:使用缓存减少IO次数,使用并发或者异步的方式增加吞吐量。
3.1.2 横向扩展(scale-out)
因为单机性能总会存在极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,又包括以下2个方向:
1、做好分层架构:这是横向扩展的提前,因为高并发系统往往业务复杂,通过分层处理可以简化复杂问题,更容易做到横向扩展。
上面这种图是互联网最常见的分层架构,当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN,反向代理层可以是LVS+Nginx,Web层可以是统一的API网关,业务服务层可进一步按垂直业务做微服务化,存储层可以是各种异构数据库。
2、各层进行水平扩展:无状态水平扩容,有状态做分片路由。业务集群通常能设计成无状态的,而数据库和缓存往往是有状态的,因此需要设计分区键做好存储分片,当然也可以通过主从同步、读写分离的方案提升读性能。
下面再结合我的个人经验,针对高性能、高可用、高扩展3个方面,总结下可落地的实践方案。
3.2.1 高性能的实践方案
1、集群部署,通过负载均衡减轻单机压力。
2、多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。
3、分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
4、考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。
5、异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
6、限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。
7、对流量进行 削峰填谷 ,通过 MQ承接流量。
8、并发处理,通过多线程将串行逻辑并行化。
9、预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
10、 缓存预热 ,通过异步 任务 提前 预热数据到本地缓存或者分布式缓存中。
11、减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
12、减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
13、程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。
14、各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
15、JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
16、锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。
上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。
3.2.2 高可用的实践方案
1、对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。
2、非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。
3、接口层面的超时设置、重试策略和幂等设计。
4、降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。
5、限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。
6、MQ场景的消息可靠性保证,包括procer端的重试机制、broker侧的持久化、consumer端的ack机制等。
7、灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。
8、监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。
9、灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。
高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。
3.2.3 高扩展的实践方案
1、合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。
2、存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。
3、业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5 )。
高并发确实是一个复杂且系统性的问题,由于篇幅有限,诸如分布式Trace、全链路压测、柔性事务都是要考虑的技术点。另外,如果业务场景不同,高并发的落地方案也会存在差异,但是总体的设计思路和可借鉴的方案基本类似。
高并发设计同样要秉承架构设计的3个原则:简单、合适和演进。"过早的优化是万恶之源",不能脱离业务的实际情况,更不要过度设计,合适的方案就是最完美的。
作者简介:985硕士,前亚马逊工程师,现大厂技术管理者。
⑷ 阿里sentinel源码解析
sentinel是阿里巴巴开源的流量整形(限流、熔断)框架,目前在github拥有15k+的star,sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
我们以sentinel的主流程入手,分析sentinel是怎么搜集流量指标,完成流量整形的。
首先我们先看一个sentinel的简单使用demo,只需要调用SphU.entry获取到entry,然后在完成业务方法之后调用entry.exit即可。
SphU.entry会调用Env.sph.entry,将name和流量流向封装成StringResourceWrapper,然后继续调用entry处理。
进入CtSph的entry方法,最终来到entryWithPriority,调用InternalContextUtil.internalEnter初始化ThreadLocal的Context,然后调用lookProcessChain初始化责任链,最终调用chain.entry进入责任链进行处理。
InternalContextUtil.internalEnter会调用trueEnter方法,主要是生成DefaultNode到contextNameNodeMap,然后生成Context设置到contextHolder的过程。
lookProcessChain已经做过优化,支持spi加载自定义的责任链bulider,如果没有定义则使用默认的DefaultSlotChainBuilder进行加载。默认加载的slot和顺序可见镇楼图,不再细说。
最后来到重头戏chain.entry进入责任链进行处理,下面会按照顺序分别对每个处理器进行分析。
首先来到NodeSelectorSlot,主要是获取到name对应的DefaultNode并缓存起来,设置为context的当前节点,然后通知下一个节点。
下一个节点是ClusterBuilderSlot,继续对DefaultNode设置ClusterNode与OriginNode,然后通知下一节点。
下一个节点是LogSlot,只是单纯的打印日志,不再细说。
下一个节点是StatisticSlot,是一个后置节点,先通知下一个节点处理完后,
1.如果没有报错,则对node、clusterNode、originNode、ENTRY_NODE的线程数、通过请求数进行增加。
2.如果报错是PriorityWaitException,则只对线程数进行增加。
3.如果报错是BlockException,设置报错到node,然后对阻挡请求数进行增加。
4.如果是其他报错,设置报错到node即可。
下一个节点是FlowSlot,这个节点就是重要的限流处理节点,进入此节点是调用checker.checkFlow进行限流处理。
来到FlowRuleChecker的checkFlow方法,调用ruleProvider.apply获取到资源对应的FlowRule列表,然后遍历FlowRule调用canPassCheck校验限流规则。
canPassCheck会根据rule的限流模式,选择集群限流或者本地限流,这里分别作出分析。
passLocalCheck是本地限流的入口,首先会调用选出限流的node,然后调用canPass进行校验。
会根据以下规则选中node。
1.strategy是STRATEGY_DIRECT。
1.1.limitApp不是other和default,并且等于orgin时,选择originNode。
1.2.limitApp是other,选择originNode。
1.3.limitApp是default,选择clusterNode。
2.strategy是STRATEGY_RELATE,选择clusterNode。
3.strategy是STRATEGY_CHAIN,选择node。
选择好对应的node后就是调用canPass校验限流规则,目前sentinel有三种本地限流规则:普通限流、匀速限流、冷启动限流。
普通限流的实现是DefaultController,就是统计当前的线程数或者qps加上需要通过的数量有没有大于限定值,小于等于则直接通过,否则阻挡。
匀速限流的实现是RateLimiterController,使用了AtomicLong保证了latestPassedTime的原子增长,因此停顿的时间是根据latestPassedTime-currentTime计算出来,得到一个匀速的睡眠时间。
冷启动限流的实现是WarmUpController,是sentinel中最难懂的限流方式,其实不太需要关注这些复杂公式的计算,也可以得出冷启动的限流思路:
1.当qps已经达到温热状态时,按照正常的添加令牌消耗令牌即可。
2.当qps处于过冷状态时,会添加令牌使得算法继续降温。
3.当qps逐渐回升,大于过冷的边界qps值时,不再添加令牌,慢慢消耗令牌使得逐渐增大单位时间可通过的请求数,让算法继续回温。
总结出一点,可通过的请求数跟令牌桶剩余令牌数量成反比,以达到冷启动的作用。
接下来是集群限流,passClusterCheck是集群限流的入口,会根据flowId调用clusterSerivce获取指定数量的token,然后根据其结果判断是否通过、睡眠、降级到本地限流、阻挡。
接下来看一下ClusterService的处理,会根据ruleId获取到对应的FlowRule,然后调用ClusterFlowChecker.acquireClusterToken获取结果返回。ClusterFlowChecker.acquireClusterToken的处理方式跟普通限流是一样的,只是会将集群的请求都集中在一个service中处理,来达到集群限流的效果,不再细说。
FlowSlot的下一个节点是DegradeSlot,是熔断处理器,进入时会调用performChecking,进而获取到CircuitBreaker列表,然后调用其tryPass校验是否熔断。
来到AbstractCircuitBreaker的tryPass方法,主要是判断熔断器状态,如果是close直接放行,如果是open则会校验是否到达开启halfopen的时间,如果成功将状态cas成halfopen则继续放行,其他情况都是阻拦。
那怎么将熔断器的状态从close变成open呢?怎么将halfopen变成close或者open呢?sentinel由两种熔断器:错误数熔断器ExceptionCircuitBreaker、响应时间熔断器ResponseTimeCircuitBreaker,都分析一遍。
当业务方法报错时会调用Tracer.traceEntry将报错设置到entry上。
当调用entry.exit时,会随着责任链来到DegradeSlot的exit方法,会遍历熔断器列表调用其onRequestComplete方法。
ExceptionCircuitBreaker的onRequestComplete会记录错误数和总请求数,然后调用继续处理。
1.当前状态是open时,不应该由熔断器底层去转换状态,直接退出。
2.当前状态是halfopen时,如果没有报错,则将halfopen变成close,否则将halfopen变成open。
3.当前状态时close时,则根据是否总请求达到了最低请求数,如果达到了话再比较错误数/错误比例是否大于限定值,如果大于则直接转换成open。
ExceptionCircuitBreaker的onRequestComplete会记录慢响应数和总请求数,然后调用继续处理。
1.当前状态是open时,不应该由熔断器底层去转换状态,直接退出。
2.当前状态是halfopen时,如果当前响应时间小于限定值,则将halfopen变成close,否则将halfopen变成open。
3.当前状态时close时,则根据是否总请求达到了最低请求数,如果达到了话再比较慢请求数/慢请求比例是否大于限定值,如果大于则直接转换成open。
下一个节点是AuthoritySlot,权限控制器,这个控制器就是看当前origin是否被允许进入请求,不允许则报错,不再细说。
终于来到最后一个节点SystemSlot了,此节点是自适应处理器,主要是根据系统自身负载(qps、最大线程数、最高响应时间、cpu使用率、系统bbr)来判断请求是否能够通过,保证系统处于一个能稳定处理请求的安全状态。
尤其值得一提的是bbr算法,作者参考了tcp bbr的设计,通过最大的qps和最小的响应时间动态计算出可进入的线程数,而不是一个粗暴的固定可进入的线程数,为什么能通过这两个值就能计算出可进入的线程数?可以网上搜索一下tcp bbr算法的解析,十分巧妙,不再细说。
⑸ 软件更新丨Spring Cloud Alibaba发布第二个版本,Spring发来贺电
还是熟悉的面孔,还是熟悉的味道,不同的是,这次的 配方升级 了。
时隔 51天,Spencer Gibb再次在Spring官网的博客页面宣布:Spring Cloud Alibaba发布了其开源后的 第二个版本0.2.1 ,随后,Spring Cloud 官方Twitter也转发了此消息。圣诞节的前一周,Josh Long向他的老朋友许晓斌发来贺电:
视频地址:https://spring.io/blog/2018/12/26/spring-tips-bootiful-alibaba
一、新版本概要
Spring Cloud Alibaba RocketMQ
Spring Cloud Alibaba SchelerX
Spring Cloud Alibaba Nacos Config
Spring Cloud Alibaba Nacos Discovery
Spring Cloud Alibaba Sentinel
二、新版本背后的思考
Spring Cloud Alibaba Nacos Discovery
Nacos Discovery 在这个版本最大的更新就是支持在初始化的时候不使用本地文件缓存,目前初始化的时候已经默认不使用本地文件缓存。
为什么要有缓存?首先我们来了解一下本地缓存的概念,为什么需要这个本地缓存?
我们都知道,服务注册与发现应该只是服务调用中的辅助性的一个环节,而不是一个关键的环节。一个良好的服务注册与发现的设计,需要保证以下两点。
要实现以上两点,缓存就不可或缺,而为了适应不同的场景,缓存又可以分成内存缓存和本地文件缓存,他们的概念和适用场景如下。
内存中的缓存
将服务提供者列表维护在内存中,每次调用时直接从内存中的列表获取节点即可。内存缓存通过定时任务更新,或者在收到服务注册中心的推送之后再更新。确保了即使在服务注册中心宕机的情况下,也能保证服务仍能正常调用。
本地文件缓存
将上述提到的内存中的缓存,保留在本地的某个文件中,这样在调用服务注册中心失败的时候,可以从本机的文件缓存中获取服务提供者列表。这样就保证了在服务注册中心宕机的情况下,应用在重启后也能找到服务提供者。
为什么要关闭
有了以上背景知识后,读者可能会有疑问,既然缓存这么好,你们为什么默认要把它置为默认关闭呢?
我们在发布出第一个版本之后,很多用户反馈,为什么我服务下线之后还是有节点,仍旧可以被查询到呢?这样导致我这个监控数据完全不准,你们这个有 bug,完全不对。其实这是阿里巴巴在多年业务积累的经验,对于服务发现来说,一个即使是已经过时的节点,也比没有任何数据好。而且还有可能其实这个服务只是和服务注册中心失去了心跳,但是应用本身是正常的。
当然,这也暴露了我们设计中存在的一些问题,没有把服务发现本身,以及缓存的分层给做好,两者糅合在一块。所以在这一次的版本更新中我们还是选择适配开源标准,默认关闭了本地文件缓存,留了一个开关给用户自由选择。
Spring Cloud Alibaba Nacos Config
Nacos Config 在这个版本中有两个大的特性,支持了“共享配置”,修正了动态刷新的语义和行为。
共享配置的实现
在第一个版本还没发布到时候,社区里对配置管理中心的讨论就没停止过,其中听到最多的反馈应该就是支持多个应用共享一个配置。我们也通过 github issue 的方式,征集了很多意见,详情见 #12 , #141。
后来我们将这个模型抽象了一下,认清这些需求本质是一个应用可以从多个 DataID 和 GroupID 组合中获取配置,并且这些配置还可以单独指定优先级和是否动态刷新。
最后我们推荐了这种使用方式,既保证了使用场景的灵活性,又保证了业务语义的明确性。更多详情可以参考 WIKI。
注意 data-id 的值必须带文件扩展名,文件扩展名支持 properties、yaml 和 yml。通过这种自定义扩展的配置项,既可以支持一个应用从多个配置项中获取数据,也解决多个应用间配置共享的问题。
头脑风暴,@fudali 同学还提出了更加灵活的一种方式 #161,就是可以通过一个配置项来配置所有的 DataID 的信息,然后可以通过修改这个配置项,可以修改整体配置项的逻辑。
这是一个非常好的想法,只不过这一期中我们没有实现,原因是这种方式太灵活了。我们认为配置管理其实是一件很严肃的事情,太灵活导致生产中变更比较不可控。
虽然目前的逻辑也可以支持这种用法,但是我们并没有把这种模式当做推荐模式,后续如果社区有更多的反馈认为这是一个强烈的需求,欢迎提 PR。
动态刷新的修正
简单好用、实时可监控的动态刷新也许是 Nacos Config 相对于其他开源中间件相比最核心的优势了,不同于 Spring Cloud Config Server 必须使用 Spring Cloud Bus 才能实现动态刷新,Nacos Config 无需依赖其他任何中间件就可以实现实时动态刷新,而且推送成功与否和刷新 历史 还支持实时查询。
但是我们发现在第一个版本中的实现发现两个问题:
在这个版本中,我们修复了这两个问题。
首先,动态刷新不再是直接去调用 ContextRefresher.refresh() 方法,而是 publish 了一个 RefreshEvent,让 spring-cloud-commons 里的 RefreshEventListener 去触发这个 ContextRefresher.refresh() 方法。
其次,我们修正了动态刷新的语义后,这一次是真正做到了,只有 refresh 属性为 true 的配置项,才会在运行的过程中变更为新的值,refresh 属性为 false 的配置项再也不用担心应用在运行的过程中发生莫名其妙的变更了。
更深入一点,在上个月 SpringOne Tour 中,我们和 Spring Cloud 的创始人 Spencer 聊到 Spring Cloud 的 Context.refresh() 成本太高,会刷新整个 Spring Context。他反复强调了两次 Context.refresh() 只对 @RefreshScope 和 @ConfigurationProperties 有效,成本一点也不高。
之前我们接收到很多社区的反馈都是 Nacos Config 动态刷新支不支持 xxxx,支不支持 xxxx。之前我们都是回答只支持 @RefreshScope 和 @ConfigurationProperties ,如果他内置没有支持,那就得自己加上相应的注解。
今天我们可以很愉快地回复,他监听了 RefreshEvent 也能直接支持。而且如果添加 @RefreshScope 和 @ConfigurationProperties 都不满足你的需求时,可以通过实现自己的 RefreshEventListener 更多高级的玩法。
Spring Cloud Alibaba Sentinel
Sentinel 在这个版本中有三个大的特性:全面支持 FeignClient ,完善了 RestTemplate 的支持,添加了热点限流、集群限流。
FeignClient 集成 Sentinel
其实在这之前,Sentinel 支持 FeignClient 已经设计了很久了,之前我们的想法做一个兼容性较强的方案,支持 Sentinel 和 Hystrix 在 FeignClient 中同时使用,尽量做到对原有应用的侵入性做到最小。
这个方案我们也思考调研了很久,但是实现难度确实比较大,需要修改 FeignClient 的代码才能实现两者共存。正好前段时间在 Spring Cloud 届最大的新闻就是 Hystrix 宣布不在维护了,于是我们就换了一个思路,直接使用 Sentinel 替代 Hystrix,不再去追求支持两者共存。
我们实现了自己的 Feign.Builder,在构建的 FeignClient 执行调用的过程中,通过 SentinelInvocationHandler 完成 Sentinel 的流量统计和保护的动作。如果想使用 Sentinel 为 FeignClient 限流降级,首先需要引入 sentinel-starter 的依赖,然后打开 Sentinel 限流降级的开关 feign.sentinel.enabled=true ,就完成了 Sentinel 的接入。如果需要使用更加定制化的功能,则需要在 @FeignClient 添加 fallback 和 configuration 这些属性的配置。
注意 @FeignClient 注解中的所有属性,Sentinel 都做了兼容。
RestTemplate 集成 Sentinel
Spring Cloud Alibaba Sentinel 支持对 RestTemplate 的服务调用使用 Sentinel 进行保护,补全了 Hystrix 这一块的空白。接入的方式也不复杂,在构造 RestTemplate bean 的时候需要加上 @SentinelRestTemplate 注解,然后在控制台配置相应的规则即可。
在触发了限流降级时,默认的处理方式是返回 RestTemplate request block by sentinel 信息。
RestTemplate 的限流降级 ?Sentinel 也承包了!
热点参数限流和集群限流
首先解释一下什么是热点参数限流和集群限流。
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
集群流控主要解决的问题是:当我们需要控制整个集群流量总量,但是单机流量可能会不均匀,如果是单机维度去限制的话会无法精确地限制总体流量,因此需要引入集群维度的流量控制。
Sentinel v1.4.0 的 新功能 ,也能第一时间愉快地在 Spring Cloud Alibaba 上使用了。
三、新组件
Spring Cloud Alibaba RocketMQ
Spring Cloud Stream 是一个用于构建基于消息的微服务应用框架,它基于 SpringBoot 来创建具有生产级别的单机 Spring 应用,并且使用 Spring Integration 与 Broker 进行连接。它提供了消息中间件的统一抽象,推出了 publish-subscribe、consumer groups、partition 这些统一的概念。
RocketMQ 是一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。具有以下特点:能够保证严格的消息顺序、提供丰富的消息拉取模式、高效的订阅者水平扩展能力、实时的消息订阅机制、亿级消息堆积能力。
在这次的 Spring Cloud Stream Binder RocketMQ 的实现中,我们适配了 Spring Cloud Stream 对于 message 抽象的 API,支持了 RocketMQ 的事务消息。消息订阅时支持以 tags、SQL 表达式过滤消息,同时还支持顺序、并发、延迟以及广播消费模式。
Spring Cloud Alibaba SchelerX
SchelerX 是阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务,同时提供分布式的任务执行模型,如网格任务,网格任务支持海量子任务均匀分配到所有 Worker(schelerx-client)上执行。
简单易用的轻量分布式任务调度
您不需要关心调度逻辑,只需要在在 JobProcessor 接口的实现中添加业务逻辑即可,然后在自主运维控制台配置上一个 Job 即可完成使用。
高可用的分布式任务
不管是 SchelerX 服务端还是客户端都是分布式架构设计,任务可以在多台客户端机器里的任何一台机器执行,如果客户端出现宕机的情况,服务端会自动选择正常运行的客户端去执行 Job,每个 Job 在服务端的不同机器均有备份,SchelerX 服务端任意宕掉部分机器仍能保证 Job 正常调度。
友好的用户界面
SchelerX 提供了非常友好的页面方便您创建、删除或修改 Job,提供了立即触发执行一次的功能,方便您测试以及关键时刻手动立即执行一次,还提供了 历史 执行记录查询的功能,您可以看到任何一个 Job 过去 100 次的 历史 执行记录。
功能强大
提供了秒级、精准的定时任务调度服务,且提供了丰富的任务执行模型,包括单机执行,广播执行,以及子任务的分布式执行。
四、What's Next?
Spring Cloud Alibaba Cloud SLS 针对日志类数据的一站式服务,在阿⾥巴巴集团经历大量大数据场景锤炼⽽成。您⽆需开发就能快捷地完成日志数据采集、消费、投递以及查询分析等功能,提升运维、运营效率,建立 DT 时代海量日志处理能力。
Spring Cloud Alibaba Dubbo Dubbo 是一款流行的开源 RPC 框架,我们会把 Dubbo 整合到 Spring Cloud Alibaba 中,让大家在开发 Dubbo 时也能享受到 Spring Cloud 带来的便利。
致谢
Spring Cloud Alibaba 从开源建设以来,受到了很多社区同学的关注。社区的每一个 issue ,每一个 PR,都是对整个项目的帮助,都在为建设更好用的 Spring Cloud 添砖加瓦。
↓↓↓
⑹ 该怎么解决 Redis 缓存穿透和缓存雪崩问题
缓存雪崩: 由于缓存层承载着大量请求,有效地 保护了存储层,但是如果缓存层由于某些原因不能提供服务,比如 Redis 节点挂掉了,热点 key 全部失效了,在这些情况下,所有的请求都会直接请求到数据库,可能会造成数据库宕机的情况。
预防和解决缓存雪崩问题,可以从以下三个方面进行着手:
1、使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉
2、缓存时间不一致: 给缓存的失效时间,加上一个随机值,避免集体失效
3、限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务
缓存穿透: 缓存穿透是指查询一个根本不存在的数据,这样的数据肯定不在缓存中,这会导致请求全部落到数据库上,有可能出现数据库宕机的情况。
预防和解决缓存穿透问题,可以考虑以下两种方法:
1、缓存空对象: 将空值缓存起来,但是这样就有一个问题,大量无效的空值将占用空间,非常浪费。
2、布隆过滤器拦截: 将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器有一定的误判,所以需要你的业务允许一定的容错性。
⑺ 一次限流的引发思考
一个api接口 /srm/api2/disabletime 需要提供最大600qps的能力,超过600qps之后需要进行限流,返回429 http code。
现在业务节点一共20台,为了异地多活,分布在2机房gz和gz6,俩机房各十台机器上。
服务限流通常有以下俩种方式:
俩种方式各有优点和缺点
本次实验就是使用的第一种方式,为了保证qps达到600之后能限流,所以理想情况下(也就是流量分部均匀的情况)每台机器的qps为30=600/20,但是实际情况发现,每天机器上的该api的请求量非常的不均,有的是个位数,有的达到了50多个。
可能有人有疑惑,每个节点流量不均有什么影响呢?那我解释下。当整个服务的流量来了600qps时,由于流量不均,有的节点流量分布是40或者50的请求量,有的节点是个位数8,5的请求量。现在每台机器的配置的qps为30,那么大于30qps的那些节点就出触发限流了,最终导致整个服务没有达到600qps时就触发限流了。
想要了解流量为什么不均匀,需要首先搞清楚业务整体架构。
最初理解的架构如下,可以很明显的发现,在gz和gz6俩机房的slb数量不一致,也就导致了gz的业务机器流量大于gz6的业务机器流量。
理论上 ,gz 的 slb流量会发给 gz 的 nginx,gz6 的slb流量会发给 gz6 的 nginx。腾讯云的负载均衡,发给 slb 的流量,不区分 gz 与 gz6 ,在所有 slb 的机器里执行轮询策略的负载均衡。
那么gz 的 slb 机器数量比 gz6 多,那 gz机房获得的流量就比 gz6机房多, gz 的 nignx获得的流量就比 gz6的nginx多。
上面解释看似合理,结论就是:由于gz和gz6俩机房的业务节点数量相同,而gz和gz6的slb的数量不同, 最终导致了俩机房的流量不均,进而导致了gz和gz6的业务节点的流量不均 。
下图是对接口压测2分钟,每秒600qps的流量趋势图,可以出在gz和gz6的流量大致分部均匀,大致在300Qps左右,这也就间接的证明了上面的想法不对。
上面俩张的流量分部印证了上面的架构是不对的,通过查询,正确的架构如下,可以看出和上面的架构图有几点不同
架构看起来是没问题,流量到最终端的业务节点也是均匀的,但是事实就是流量是不均匀的,我们结合实际情况来看下,比如轮训业务节点为a->b->c:
首先slb的轮训 据指定url来轮训 的,是 根据整个服务的所有请求 的来轮训的 ,比如第一个压测请求打到的是a业务节点,如果当前时间整个服务没有别的请求,那么第二个压测请求肯定会打到b业务节点,但实际情况是,当前时间肯定有别的请求,所以第二个压测请求时可能打到的还是a业务节点,也可能是b业务节点,可能是集群中的任何一个业务节点,我猜测就是这个原因导致了最终的流量不均匀。
为了证明这个猜想,我们可以看看 21:49:00 这个时间点所有机器上的所有请求是不是大致平均的,如果是平均的,则证明我们猜想正确。
可以看出所有机器的请求量大致基本相同,我们的猜想正确。
使用第一种方式通常有个大前提: 是各个机器流量分布必须非常均匀的,每台机器配置的qps=总qps/机器节点数量。但是由于网络总是不稳定性或者其他原因通常流量是不均匀的,所以需要每台节点配置的qps加一些Buffer,40或者50qps 。
如果使用第二种方式的话,我们直接在redis配置600qps即可,因为不需要关注每台机器流量的流量分布,管你节点的流量是50还8呢,只要总和大于600qps后,服务就会触发限流了。
如果第二种方式能够实现,建议使用第二种方式。
⑻ 如何配置Kettle集群运行环境
Kettle是一款开源的ETL工具,以其高效和可扩展性而闻名于业内。其高效的一个重要原因就是其多线程和集群功能。
Kettle的多线程采用的是一种流水线并发的机制,我们在另外的文章中专门有介绍。这里主要介绍的是kettle的集群。
集群允许转换以及转换中的步骤在多个服务器上并发执行。在使用kettle集群时,首先需要定义的是Cluster schema。所谓的Cluster schema就是一系列的子服务器的集合。在一个集群中,它包含一个主服务器(Master)和多个从属服务器服务器(slave)。如下图所示:
子服务器(Slave servers)允许你在远程服务器上执行转换。建立一个子服务器需要你在远程服务器上建立一个叫做“Carte”的 web 服务器,该服务器可以从Spoon(远程或者集群执行)或者转换任务中接受输入。
在以后的描述中,如果我们提到的是子服务器,则包括集群中的主服务器和从属服务器;否则我们会以主服务器和从属服务器来进行特别指定。
选项 描述
服务器名称
子服务器的名称
主机名称或IP地址
用作子服务器的机器的地址
端口号
与远程服务通信的端口号
用户名
获取远程服务器的用户名
密码
获取远程服务器的密码
是主服务器吗
在转换以集群形式执行时,该子服务器将作为
注意: 在集群环境下执行转化时,你必须有一个子服务器作为主服务器(master server)而其余所有的子服务器都作从属服务器(slave server)
选项 描述
代理服务器主机名
设置你要通过代理进行连接的主机名
代理服务器端口
设置与代理进行连接时所需的端口号
Ignore proxy for hosts: regexp|separated
指定哪些服务器不需要通过代理来进行连接。该选项支持你使用正则表达式来制定多个服务器,多个服务器之间以' | ' 字符来进行分割
创建cluster schema
选项 描述
Schema 名称
集群schema的名称
端口号
这里定义的端口号是指从哪一个端口号开始分配给子服务器。每一个在子服务器中执行的步骤都要消耗一端口号。注意: 确保没有别的网络协议会使用你定义的范围之类的端口,否则会引起问题
Sockets缓存大小
TCP内部缓存的大小
Sockets刷新间隔(rows)
当TCP的内部缓存通过网络完全发送出去并且被清空时处理的行数
Sockets数据是否压缩
如果该选项被选中,则所有的数据都会使用Gzip压缩算法进行压缩以减轻网络传输量
Dynamic Cluster
动态集群指的是在运行的时候才能获知从属服务器的信息。这种情形适用于主机可以自动增加或者去除的情形,例如云计算。
主服务器的设置不变,但是它可以接受从属服务器的注册。一旦接受了某个从属服务器的注册,则每隔30秒去监视该从属服务器是否还处于有效状态
子服务器
这里是一个要在集群中使用的服务器列表。这个列表中包含一个主服务器和任意数目的从属服务器。
在dynamic Cluster的情况下,只需要选择主服务器即可
定义转换
定义完了 cluster schema 后,下一步就是定义在集群环境下执行的转换。我们这里展现的只是一个最简单的例子,完全是为了演示而用。现实情况中的集群有可能非常复杂。
首先你像平时一样创建转换,以hop连接连个两个步骤。然后你指定第二个步骤将在集群下执行
然后选择需要使用的集群。转换如图一样显示在GUI中。
注意 Cx4显示这个步骤将在集群中运行,而这个集群中有4个从属服务器。假设我们将计算结果再次存入到数据表中
这个转换虽然定义了集群,但是我们同样可以让它在单机环境下执行,而且可以得到相同的结果。这意味着你可以使用普通的本地模式来测试它。
执行转换
要想以集群方式来运行转换或者作业,首先需要启动在Cluster schema中定义的主服务器和从属服务器,然后再运行转换或者作业。
启动子服务器
子服务器其实是一个嵌入式的名为 Carte 的 小web server。要进行集群转换,首先需要启动cluster schema中的子服务器
脚本启动
kettle 提供了 carte.bat 和 carte.sh ( linux )批处理脚本来启动子服务器,这种启动方式分为两种
使用主机号和端口号
Carte 127.0.0.1 8080
Carte 192.168.1.221 8081
使用配置文件
Carte /foo/bar/carte-config.xml
Carte 网站/carte-config.xml
⑼ 分布式解决方案之:限流
限流在日常生活中限流很常见,例如去有些景区玩,每天售卖的门票数是有限的,例如 2000 张,即每天最多只有 2000 个人能进去游玩。那在我们工程上限流是什么呢?限制的是 “流”,在不同场景下“流”的定义不同,可以是 每秒请求数、每秒事务处理数、网络流量 等等。通常意义我们说的限流指代的是限制到达系统的并发请求数,使得系统能够正常的处理部分用户的请求,来保证系统的稳定性。
日常的业务上有类似秒杀活动、双十一大促或者突发新闻等场景,用户的流量突增,后端服务的处理能力是有限的,如果不能处理好突发流量,后端服务很容易就被打垮。另外像爬虫之类的不正常流量,我们对外暴露的服务都要以最大恶意为前提去防备调用者。我们不清楚调用者会如何调用我们的服务,假设某个调用者开几十个线程一天二十四小时疯狂调用你的服务,如果不做啥处理咱服务基本也玩完了,更胜者还有ddos攻击。
对于很多第三方开放平台来说,不仅仅要防备不正常流量,还要保证资源的公平利用,一些接口资源不可能一直都被一个客户端占着,也需要保证其他客户端能正常调用。
计数器限流也就是最简单的限流算法就是计数限流了。例如系统能同时处理 100 个请求,保存一个计数器,处理了一个请求,计数器就加一,一个请求处理完毕之后计数器减一。每次请求来的时候看看计数器的值,如果超过阈值就拒绝。计数器的值要是存内存中就算单机限流算法,如果放在第三方存储里(例如Redis中)集群机器访问就算分布式限流算法。
一般的限流都是为了限制在指定时间间隔内的访问量,因此还有个算法叫固定窗口。
它相比于计数限流主要是多了个时间窗口的概念,计数器每过一个时间窗口就重置。规则如下:
这种方式也会面临一些问题,例如固定窗口临界问题:假设系统每秒允许 100 个请求,假设第一个时间窗口是 0-1s,在第 0.55s 处一下次涌入 100 个请求,过了 1 秒的时间窗口后计数清零,此时在 1.05 s 的时候又一下次涌入100个请求。虽然窗口内的计数没超过阈值,但是全局来看在 0.55s-1.05s 这 0.1 秒内涌入了 200 个请求,这其实对于阈值是 100/s 的系统来说是无法接受的。
为了解决这个问题,业界又提出另外一种限流算法,即滑动窗口限流。
滑动窗口限流解决固定窗口临界值的问题,可以保证在任意时间窗口内都不会超过阈值。相对于固定窗口,滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点,因此对内存的占用会比较多。
规则如下,假设时间窗口为 1 秒:
但是滑动窗口和固定窗口都无法解决短时间之内集中流量的冲击问题。 我们所想的限流场景是: 每秒限制 100 个请求。希望请求每 10ms 来一个,这样我们的流量处理就很平滑,但是真实场景很难控制请求的频率,因为可能就算我们设置了1s内只能有100个请求,也可能存在 5ms 内就打满了阈值的情况。当然对于这种情况还是有变型处理的,例如设置多条限流规则。不仅限制每秒 100 个请求,再设置每 10ms 不超过 2 个,不过带来的就是比较差的用户体验。
而漏桶算法,可以解决时间窗口类的痛点,使得流量更加平滑。
如下图所示,水滴持续滴入漏桶中,底部定速流出。如果水滴滴入的速率大于流出的速率,当存水超过桶的大小的时候就会溢出。
规则如下:
水滴对应的就是请求。
与线程池实现的方式方式如出一辙。
面对突发请求,服务的处理速度和平时是一样的,这并非我们实际想要的。我们希望的是在突发流量时,在保证系统平稳的同时,也要尽可能提升用户体验,也就是能更快地处理并响应请求,而不是和正常流量一样循规蹈矩地处理。
而令牌桶在应对突击流量的时候,可以更加的“激进”。
令牌桶其实和漏桶的原理类似,只不过漏桶是定速地流出,而令牌桶是定速地往桶里塞入令牌,然后请求只有拿到了令牌才能通过,之后再被服务器处理。
当然令牌桶的大小也是有限制的,假设桶里的令牌满了之后,定速生成的令牌会丢弃。
规则:
令牌桶的原理与JUC的Semaphore 信号量很相似,信号量可控制某个资源被同时访问的个数,其实和拿令牌思想一样,不同的是一个是拿信号量,一个是拿令牌。信号量用完了返还,而令牌用了不归还,因为令牌会定时再填充。
对比漏桶算法可以看出 令牌桶更适合应对突发流量 ,假如桶内有 100 个令牌,那么这100个令牌可以马上被取走,而不像漏桶那样匀速的消费。不过上面批量获取令牌也会致使一些新的问题出现,比如导致一定范围内的限流误差,举个例子你取了 10 个此时不用,等下一秒再用,那同一时刻集群机器总处理量可能会超过阈值,所以现实中使用时,可能不会去考虑redis频繁读取问题,转而直接采用一次获取一个令牌的方式,具体采用哪种策略还是要根据真实场景而定。
1、计数器 VS 固定窗口 VS 滑动窗口
2、漏桶算法 VS 令牌桶算法
总的来说
单机限流和分布式限流本质上的区别在于 “阈值” 存放的位置,单机限流就是“阀值”存放在单机部署的服务/内存中,但我们的服务往往是集群部署的,因此需要多台机器协同提供限流功能。像上述的计数器或者时间窗口的算法,可以将计数器存放至 Redis 等分布式 K-V 存储中。又如滑动窗口的每个请求的时间记录可以利用 Redis 的 zset 存储,利用 ZREMRANGEBYSCORE 删除时间窗口之外的数据,再用 ZCARD 计数,
可以看到,每个限流都有个阈值,这个阈值如何定是个难点。定大了服务器可能顶不住,定小了就“误杀”了,没有资源利用最大化,对用户体验不好。一般的做法是限流上线之后先预估个大概的阈值,然后不执行真正的限流操作,而是采取日志记录方式,对日志进行分析查看限流的效果,然后调整阈值,推算出集群总的处理能力,和每台机子的处理能力(方便扩缩容)。然后将线上的流量进行重放,测试真正的限流效果,最终阈值确定,然后上线。
其实真实的业务场景很复杂,需要限流的条件和资源很多,每个资源限流要求还不一样。
一般而言,我们不需要自己实现限流算法来达到限流的目的,不管是接入层限流还是细粒度的接口限流,都有现成的轮子使用,其实现也是用了上述我们所说的限流算法。
具体的使用还是很简单的,有兴趣的同学可以自行搜索,对内部实现感兴趣的同学可以下个源码看看,学习下生产级别的限流是如何实现的。
限流具体应用到工程还是有很多点需要考虑的,并且限流只是保证系统稳定性中的一个环节,还需要配合降级、熔断等相关内容。
⑽ redis常见问题
1. 缓存击穿
缓存击穿是指一个请求要访问的数据,缓存中没有,但数据库中有的情况。这种情况一般都是缓存过期了。
但是这时由于并发访问这个缓存的用户特别多,这是一个热点 key,这么多用户的请求同时过来,在缓存里面没有取到数据,所以又同时去访问数据库取数据,引起数据库流量激增,压力瞬间增大,直接崩溃给你看。
所以一个数据有缓存,每次请求都从缓存中快速的返回了数据,但是某个时间点缓存失效了,某个请求在缓存中没有请求到数据,这时候我们就说这个请求就"击穿"了缓存。
针对这个场景,对应的解决方案一般来说有三种。
借助Redis setNX命令设置一个标志位就行。设置成功的放行,设置失败的就轮询等待。就是在更新缓存时加把锁
后台开一个定时任务,专门主动更新过期数据
比如程序中设置 why 这个热点 key 的时候,同时设置了过期时间为 10 分钟,那后台程序在第 8 分钟的时候,会去数据库查询数据并重新放到缓存中,同时再次设置缓存为 10 分钟。
其实上面的后台续命思想的最终体现是也是永不过期。
只是后台续命的思想,会主动更新缓存,适用于缓存会变的场景。会出现缓存不一致的情况,取决于你的业务场景能接受多长时间的缓存不一致。
2. 缓存穿透
缓存穿透是指一个请求要访问的数据,缓存和数据库中都没有,而用户短时间、高密度的发起这样的请求,每次都打到数据库服务上,给数据库造成了压力。一般来说这样的请求属于恶意请求。
解决方案有两种:
就是在数据库即使没有查询到数据,我们也把这次请求当做 key 缓存起来,value 可以是 NULL。下次同样请求就会命中这个 NULL,缓存层就处理了这个请求,不会对数据库产生压力。这样实现起来简单,开发成本很低。
3. 缓存雪崩
缓存雪崩是指缓存中大多数的数据在同一时间到达过期时间,而查询数据量巨大,这时候,又是缓存中没有,数据库中有的情况了。
防止雪崩的方案简单来说就是错峰过期。
在设置 key 过期时间的时候,在加上一个短的随机过期时间,这样就能避免大量缓存在同一时间过期,引起的缓存雪崩。
如果发了雪崩,我们可以有服务降级、熔断、限流手段来拒绝一些请求,保证服务的正常。但是,这些对用户体验是有一定影响的。
4. Redis 高可用架构
Redis 高可用架构,大家基本上都能想到主从、哨兵、集群这三种模式。
哨兵模式:
它主要执行三种类型的任务:
哨兵其实也是一个分布式系统,我们可以运行多个哨兵。
然后这些哨兵之间需要相互通气,交流信息,通过投票来决定是否执行自动故障迁移,以及选择哪个从服务器作为新的主服务器。
哨兵之间采用的协议是 gossip,是一种去中心化的协议,达成的是最终一致性。
选举规则: