高性能存储的常见设计

关系型数据库

关系型数据库依靠着它的ACID特性和强大的SQL语法,目前仍然是各种业务系统的核心存储,很多场景下高性能设计的核心部分就是关系型数据库的高性能设计。数据库厂商(Oracle、DB2、MySQL 等)在优化和提升单个数据库性能方面做了非常多的优化和改进,但是依然跟不上互联网业务的发展,集群已成为趋势。下面会介绍一些高性能数据库集群的设计。

原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability)

读写分离

顾名思义:读写分离就是数据库的读和写分散到不同的节点。
读写分离的基本实现如下:

  • 数据库服务器搭建主从集群,一主N从
  • 数据库主机负责读写操作,从机只负责读操作
  • 数据库主机通过复制将数据同步到从机(主从复制)
  • 业务服务器将写操作发给主机,将读操作发给从机

这里使用的是主从集群而不是主备集群,“从”代表“仆从”查询数据时候起到辅助读作用;“备”代表“备份”,备机在查询数据时候往往不承担作用,而一旦主机失效的时候,备机会替补上来,成为主机。
读写分离的实现逻辑并不复杂,但是有两个细节点将引入设计复杂度:主从复制延迟读写分配机制

主从复制延迟

主从复制延迟在同机房,一般情况下都在十几到几十毫秒,但是如果有大量数据同步,就可能延迟到1秒~几分钟甚至更久,这样的场景下,主库新写入的数据,访问从库读取是无法读取到最新数据,比如:用户在主库注册了会员,刷新界面登录时候,而又从从库拉取数据,这时候肯定会显示“未注册”,这样的体验想必是非常差。

解决主从复制延迟有几种常见的方法:

  • 避免大事务提交(常识)
  • 二次读取策略
    就是所谓的读库失败再读写库(特定业务场景下有用)
  • 关键业务指定主库
    比如注册登录系统,读写都指定主库,其他业务读写分离,但是这时候要注意防止有人恶意刷爆你的主库

读写分配机制

将数据库读写操作区分开来,读写访问不同的数据库服务器,一般有两种实现方式:程序代码封装中间件封装

  1. 程序代码封装,以Python为例
Django:
DATABASE_ROUTERS = [ 'path.to.PrimaryReplicaRouter']
import random
class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        """
        Reads go to a randomly-chosen replica.
        """
        return random.choice(['replica1', 'replica2'])

    def db_for_write(self, model, **hints):
        """
        Writes always go to primary.
        """
        return 'primary'
}

Sqlalchemy:
class RoutingSession(Session):
    def get_bind(self, mapper=None, clause=None):
        if self._name:
            return self.engines[self._name]
        # _flushing分离读写
        elif self._flushing:
            return self.engines[DEFAULT_DB]
        else:
            return random.choice(self.slave_engines) if self.slave_engines else self.engines[DEFAULT_DB]

    def using_bind(self, name):
        self._name = name
        return self

JAVA:
淘宝的 TDDL(Taobao Distributed Data Layer,外号:头都大了)
https://github.com/alibaba/tb_tddl

代码封装具有以下特点:

  • 实现简单
  • 不同编程语言、ORM需要自己实现一套
  • 故障发生时,需要各个系统更改自己的DB配置
  1. 中间件封装
    这里的中间件特指的是业务代码与数据库中间加的一层,特点如下:
  • 能够支持多种编程语言,因为数据库中间件对业务服务器提供的标准的SQL接口
  • 数据库中间件需要支持完成的SQL语法和数据库服务器协议
  • 所有的业务数据都经过中间件,中间件的性能要求较高
  • 数据库主从切换对业务系统无感知
  • 实现难度高,代码量大
    一般使用现有的成熟方案,如:
    MySQL官方推荐的MySQL Router;
    一些大厂的开源中间件框架;
    也可以直接使用阿里云RDS,开箱即用

分库分表

当数据量达到千万级以上,单台数据库存储能力会成为瓶颈。
具体体现如下:

  • 数据量大,读写性能会下降,即使有索引,索引也会变得很大,性能同样下降
  • 数据文件越大,数据库备份和恢复需要的时间越长
  • DDL耗时很大

这时候就要把单台数据库的量控制一下,把存储分散到多台数据库服务器上,也就是“分库分表”。
把不同的业务放在不同的数据库中,但是这样的操作也就面临着一下几个问题:

  1. 分库
  • 无法join操作
    原来一个join的操作就变成了先查出来一个表的id,再去另外一个表里去查询
  • 事务问题
    原来的一个事务里的操作,此时要重构成逻辑事务,模拟实现事务的功能,达到“同进同退”的效果
  • 成本 (机器变多了)
  1. 分表
    分表是比较常见的切分数据策略,即在一个数据库里,把一张大表分为N张小表,切分的策略一般分为垂直切分水平切分
    image.png
  • 垂直切分
    如上图,对一个表的不同字段进行切分,比如一个社交App,需要做非常频繁的筛选操作,筛选的条件集中在年龄(age)、性别(sex),其他的外号、个人简介等等的多用于展示,这时候可以把age + sex放在一张表用于频繁的筛选过滤,当用户需要查看详情的时候,再去查另外一张表
  • 水平切分
    水平切分是最常用的一种数据切分方案,根据不同规则,分散到不同的表里面,各个公司的规定不同,一些公司的规定是单表5000w必须做sharding,有的则是2000w、1000w,这个也是根据具体业务而定,但是当表的数据达到千万级别时,作为架构师就要警觉起来了。
  • Sharding路由
    路由负责把不同的数据应该存放到相应的表中,路由一般尽量满足以下几个标准:
    • 同一张表能尽最大可能的满足业务需求
      跨两张以上Sharding表操作,势必会增加复杂度,也会影响效率,一张表能满足业务需求尽量在一张表里完成,那么,路由策略和ShardingKey的选择就显得很重要了。
    • 各个表数据要尽可能的平均
      如果路由的策略做的不好,可能会有一张表特别大,读写还是很慢,一张表又特别小,资源浪费

常见的路由姿势有:

  • 范围路由
    比如:主键id从0 ~ 1000000放在表1,1000000 ~ 1999999放在表2,这样的方案是比较简单,只需要往后面加表就可以啦,但是各个表的数据有可能不均匀,而且在稍微复杂一点的场景,简单的主键id切分一般不能单表满足业务需求。

  • 查询切分
    将id和库的Mapping记录在一个单独的库中。

    image.png

    优点:id和库的Mapping可以随意更改
    缺点:引入新的单点

  • Hash切分
    一般采用Mod来切分,下面看一下Mod的策略

    image.png

    数据水平切分后我们希望是一劳永逸或者是易于水平扩展的,所以推荐采用mod 2^n这种一致性Hash。
    以统一订单库为例,假如我们分库分表的方案是32*32的,即通过UserId后四位mod 32分到32个库中,同时再将UserId后四位Div 32 Mod 32将每个库分为32个表,共计分为1024张表。线上部署情况为8个集群(主从),每个集群4个库。

    举个扩展的例子,虽然已经sharding到1024个表里面,但是业务量增长很快,单表仍然很大,这时候,可以直接把基数32变为64,UserId为1和33的人原来是表1里,现在要把数据分配到表1和表33,数据怎么迁移呢?最简单的,把表1复制一份到表33(或者从表1复制指定数据到表33),修改一下路由规则就好了,等到灰度完成,清理一下原来的数据。这个例子是扩展表,扩展库、集群也是一样的道理。关于一致性hash,展开会很多,会后面专门写一篇。
    PS:分表以后单表仍然很大,可以试试在业务允许的情况下进行数据归档

水平切分了以后,很多操作会受限:

  • join
  • count
  • order_by
    等等

非关系型数据库

NoSQL = Not Only SQL
常见的NoSQL方案分为4类:

  • k-v存储:解决关系数据库无法存储数据结构的问题,以Redis为代表。
  • 文档数据库:解决关系数据库强schema约束的问题,以MongoDB为代表。
  • 列式数据库: 解决关系数据库大数据场景下的I/O问题,以HBase为代表。
    “列式数据库”是相对于“行式数据库”的,列式数据库在存储的时候,物理硬盘的一行保存的是逻辑数据的一列,所以列式就具备了一些特点:读取多条逻辑列效率很高;对逻辑上某一列修改,具有原子性和一致性;
  • 全文搜索引擎:解决关系数据库的全文搜索性能问题,以Elasticsearch和它的“倒排索引”(Inverted index)为代表。

使用NoSQL之前,最先判断的是看业务是否需要严格的遵循ACID,如果需要,那么不建议使用NoSQL

缓存

在RDB前面加一层缓存,可以起到提高查询效率,降低RDB压力的神效,典型的场景有:

  • 需要经过复杂运算后得出的数据
  • 读多写少的数据

通过把可能重复使用的数据放在内存中,一次生成、多次使用,避免每次使用都去访问存储上系统。
单台Memcache服务器简单的key-value查询能够达到5w~10wQPS
单台Redis服务器查询也能够达到10wQPS,设计良好的缓存一般不会成为瓶颈。


redis官方基准测试工具 redis-benchmark.png

缓存虽然大大减轻了存储系统的压力,但是同时引入了更多的复杂度,下面简述缓存的架构设计要点。

  • 缓存穿透
    缓存穿透是指缓存没有发挥作用,缓存中没有数据,需要去存储系统中查询,通常情况下有两种情况:
  1. 存储数据不存在
    缓存找不到去存储中拉数据,结果存储中也没有,这样的情况不会太多,但是极端情况下,比如被黑客攻击,大量的请求,透过缓存打到存储上,很可能把存储搞挂掉。
    解决办法:很简单,如果查询存储系统的数据没有找到,则直接设置一个默认值,代表存储中没有这个值,当存储中有这个值了,再把缓存更新掉。
  2. 缓存数据生成耗费大量时间或者资源
    存储中其实是有数据的,但是缓存中数据过期了以后,重新从存储生成数据到缓存中,需要较长时间,在这一段时间内,缓存都是失效的。
    解决办法:把刷入缓存时间控制在闲时
  3. 缓存雪崩
    缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。高并发场景下,缓存过期被清除后,业务系统需要重新生成缓存,所有请求都去生成缓存,这时候性能肯定就比较差了
    常见解决办法:
    • 更新锁机制
      对缓存加分布式互斥锁,保证只有一个线程去更新缓存
    • 后台更新机制
      缓存不是由业务线程来更新,缓存过期时间设置为永久,采用后台线程更新的方式更新,后台更新的策略同时也要考虑几个特殊场景,缓存内存不够时,会“踢掉”一些缓存数据,而这时候后台线程是不知道缓存被“踢掉”了。这时候一般有两种办法:后台线程轮训检查;业务线程发现缓存失效,把缓存生成请求扔到队列里,后台消费生成请求。
    • 双key
      key1有超时时间,key2没有超时时间,读取不到key1时候,会返回key2的值,同时出发一个事件,更新key1、key2
  • 缓存热点
    1.避免大KEY
    2.突发热点。例如:微博某明星发布微博“xxx,我们恋爱了”,这时候该缓存服务器的压力也会很大。
    解决办法就是针对粉丝一百万的明星,增加100个副本,讲请求分散到多个缓存服务器上,减轻缓存热点导致单台缓存服务器的压力。有个需要注意的点是不同的副本过期时间不要设置成一样的,否则缓存同时失效,会引发雪崩。
    PS:热点数据存在相当的突发性,临时扩容似乎来不及,那怎么应对突发的类似微博宕机这类的问题呢?限流;容器化+动态化;业务降级,如限制评论

小结

架构设计没必要一上来就搞分库分表、加缓存,因为增加一个新技术的同时,势必会带来新的复杂度,而应遵循架构设计三原则来设计——简单、适用、演化。当接口性能出现瓶颈的时候,最简单的扩容加机器的同时,更需要优化本身的业务代码和SQL语句,这样也容易对自身系统能力有个正确的认识。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容