大家好,我是58沈剑,今天我分享的主题是《58怎么玩数据库架构》,我的PPT页数非常少, 讨论的问题非常的聚焦。
一、数据库的基本概念
基本概念就一页PPT,让大家就一些数据库方面的概念达成一致。 ./images/2017-03-10-58/640.webp
首先是“/单库/”,最开始的时候数据库都是这么玩的,几乎所有公司都会经历这个阶段。 ./images/2017-03-10-58/640-1.webp
接下来是“/分片/”,也就是水平切分,它是/用来解决数据量大的问题/。有一些数据库支持 auto sharding,自动分片,例如 mongoDB,58 同城也用过两年 mongoDB,后来发现 auto sharding 不太可控,不知道什么时候会进行数据迁移,数据迁移过程中会有大粒度的锁, 读写被阻塞,业务会有抖动和毛刺,这些是业务不能接受的,于是后来又被我们弃用。
一旦进行分片,就会面临“/数据路由/”的问题,来了一个请求,要将请求路由到对应的数据库 分片上。互联网常用的数据路由方法有三种:
- (1)一个是按照数据范围路由,比如有两个分片,一个范围是0-1亿,一个范围是1亿-2亿, 这样来路由。 这个方式的优点是非常的/简单/,并且/扩展性好/,假如两个分片不够了,增加一个2亿-3亿的 分片即可。 这个方式的缺点是:虽然数据的分布是均衡的,每一个库的数据量差不多,但/请求的负载会 不均衡/。例如有一些业务场景,新注册的用户活跃度更高,大范围的分片请求负载会更高。
- (2)二个是按照hash路由,比如有两个分片,数据模2寻库即可。 这个方式的优点是路由方式很/简单/,/数据分布也是均衡的,请求负载也是均衡的/。 这个方式的缺点是如果两个分片数据量过大,要变成三个分片,数据迁移会比较麻烦, 即/扩展性会受限/。
- (3)三个是路由服务。前面两个数据路由方法均有一个缺点,/业务线需要耦合路由规则/, 如果路由规则发生变化,业务线是需要配合升级的。路由服务可以实现业务线与路由规则的 解耦,业务线每次访问数据库之前先调用路由服务,来知道数据究竟存放在哪个分库上。
./images/2017-03-10-58/640-2.webp 接下来是“/分组/”与“/复制/”,这/解决的是扩展读性能,保证读高可用的问题/。
根据经验,大部分互联网的业务都是读多写少。淘宝、京东查询商品,搜索商品的请求可 能占了99%,只有下单和支付的时候有写请求。58同城搜索帖子,察看列表页,查看详情页 都是读请求,发布帖子是写请求,写请求的量也是比较少的。大部分互联网的场景都读多 写少,一般来说读性能会最先成为瓶颈,怎么快速解决这个问题呢?我们通常使用读写分离, 扩充读库的方式来提升系统的读性能,同时多个读库也保证了读的可用性,一台读库挂了, 另外一台读库可以持续的提供服务。
./images/2017-03-10-58/640-3.webp /常见数据库软件架构的的玩法综合了“分片”和“分组”/,数据量大进行分片,为了提高读性 能,保证读的高可用,进行了分组,80%互联网公司数据库都是上图这种软件架构。
二、可用性架构实践
数据库大家都用,平时除了根据业务设计表结构,根据访问来设计索引之外,还应该在设计 时考虑数据的可用性,可用性又分为读的高可用与写的高可用。
./images/2017-03-10-58/640-4.webp 上图是“读”高可用的常见玩法,我们是怎么样保证读库的高可用的呢? 解决高可用这个问题的思路是冗余。
解决站点的可用性问题冗余多个站点,解决服务的可用性问题冗余多个服务,解决数据的 可用性问题冗余多份数据。
如果用一个读库,保证不了读高可用,就复制读库,一个读库挂了另一个仍然可以提供服务 ,这么用复制的方式来保证读的可用性。
数据的冗余会引发一个副作用,就是一致性的问题。
如果是单库,读和写都落在同一个库上,每次读到的都是最新的数据库,不存在一致性的问题。
但是为了保证可用性将数据复制到多个地方,而这多个地方的数据绝对不是实时同步的, 会有同步时延,所以有可能会读到旧的数据。如何解决主从数据库一致性问题我们在后面 再来讲。
很多互联网公司的数据库软件架构都是一主两从或者一主三从,不能够保证“写”的高可用, 因为写其实还是只有一个库,仍是单点,如果这个库挂了的话,写会受影响。那小伙伴们 为什么还使用这个架构呢?
我刚才提到大部分互联网公司99%的业务都是“读”业务,写库不是主要矛盾,写库挂了, 可能只有1%的用户会受影响。
如果要做到“写”的高可用,对数据库软件架构的冲击比较大,不一定值得,为了解决 1% 的问 题引入了 80% 的复杂度,所以很多互联网公司都没有解决写数据库的高可用的问题。
怎么来解决写的高可用问题呢?/思路还是冗余,读的高可用是冗余读库/,写的高可用是冗余 写库。把一个写变成两个写,做一个双主同步,一个挂了的话,可以将写的流量自动切到另 外一个,写库的高可用性。
用双主同步的方式保证写高可用性会存在什么样的问题?
刚才提到,*用冗余的方式保证可用性会存在一致性问题*。因为两个主相互同步,这个同 步是有时延的,很多公司用到 auto-increment-id 这样的一些数据库的特性,如果用双主 同步的架构,一个主 id 由10变成11,在数据没有同步过去之前,另一个主又来了一个写请求, 也由 10 变成 11,双向同步会主键冲突,同步失败,造成数据丢失。
解决这个双主同步id冲突的方案有两种: - (1)一个是*双主使用不同的初始值,相同的步长来生成id*,一个库从0开始(生成02468), 一个库从1开始(生成13579),步长都为2,这样两边同步数据就不会冲突。 - (2)另一个方式是*不要使用数据库的 auto-increment-id,而由业务层来保证生成的id不冲突*。
58同城没有使用上述两种方式来保证读写的可用性,我们使用的是/“双主”当“主从”的方 式来保证数据库的读写可用性/。
虽然看上去是双主同步,但是读写都在一个主上,另一个主库没有读写流量,完全 standby。 当一个主库挂掉的时候,流量会自动的切换到另外一个主库上,这一切对业务线都是透明的, 自动完成。
58同城的这种方案,读写都在一个主库上,就不存同步延时而引发的一致性问题了,但缺点 有两个: - 第一是数据库*资源利用率只有50%*; - 第二个是没有办法通过增加读库的方式来扩展系统的读性能;
58同城的数据库软件架构如何来扩展读性能呢,我们接着来看下一章。
三、读性能架构实践
如何增加数据库的读性能,先看下传统的玩法:
- (1)第一种玩法是/增加从库/,通过增加从库来提升读性能,缺点是什么呢?从库越多, 写的性能越慢,同步的时间越长,不一致的可能性越高。
- (2)第二种常见的玩法是/增加缓存/,缓存是大家用的非常多的一种提高系统读性能的 方法,特别是对于读多写少的互联网场景非常的有效。常用的缓存玩法如上图,上游是 业务线,下游是读写分离主从同步和一个 cache。
*对于写操作*:会先淘汰cache,再写数据库。 *对于读操作*:先读cache,如果cache hit则返回数据,如果cachemiss则读从库, 然后把读出来的数据再入缓存。
这是常见的cache玩法。
传统的cache玩法在一种异常时序下,会引发*严重的一致性问题*,考虑这样一个特殊的时序:
- (1)先来了一个写请求,淘汰了cache,写了数据库; -
(2)又来了一个读请求,读cache,cache miss了,然后读从库,此时写请求还没有同步到 从库上,于是读了一个脏数据,接着脏数据入缓存; - (3)最后主从同步完成;
这个时序会导致脏数据一直在缓存中没有办法被淘汰掉,数据库和缓存中的数据严重不一致。 58同城也是采用缓存的方式来提升读性能的,那我们会不会有数据一致性问题呢,接着往下看。
四、一致性架构实践
./images/2017-03-10-58/640-5.webp 58同城采用/“服务+缓存+数据库”一套的方式来保证数据的一致性/,由于58同城使用 “双主当主从用”的数据库读写高可用架构,读写都在一个主库上,不会读到所谓 “读库的脏数据”,所以数据库与缓存的不一致情况也不会存在。
传统玩法中,*主从不一致的问题有一些什么样的解决方案呢*?我们一起来看一下。
./images/2017-03-10-58/640-6.webp 主从为什么会不一致?刚才提到读写会有时延,有可能读到从库上的旧数据。常见的方法 是/引入中间件/,业务层不直接访问数据库,而是通过中间件访问数据库,这个中间件会记录 哪一些 key 上发生了写请求,在数据主从同步时间窗口之内,如果 key 上又出了读请求,就将 这个请求也路由到主库上去(因为此时从库可能还没有同步完成,是旧数据),使用这个方 法来保证数据的一致性。
中间件的方案很理想,*那为什么大部分的互联网的公司都没有使用这种方案来保证主从数 据的一致性呢*?那是因为数据库中间件的技术门槛比较高,有一些大公司,例如百度,腾讯, 阿里他们可能有自己的中间件,并不是所有的创业公司互联网公司有自己的中间件产品, /况且很多互联网公司的业务对数据一致性的要求并没有那么高/。比如说同城搜一个帖子, 可能5秒钟之后才搜出来,对用户的体验并没有多大的影响。
./images/2017-03-10-58/640-7.webp 除了中间件,/读写都路由到主库/,58同城就是这么干的,也是一种解决主从不一致的常用方案。
解决完主从不一致,第二个要解决的是*数据库和缓存的不一致*,刚才提到cache传统的 玩法,脏数据有可能入cache,我们怎么解决呢?
两个实践:第一个是缓存双淘汰机制,第二个是建议为所有item设定过期时间(前提是允许cache miss)。
- (1)/缓存双淘汰/,传统的玩法在进行写操作的时候,先淘汰cache再写主库。上文提到,在主 从同步时间窗口之内可能有脏数据入cache,此时如果再发起一个异步的淘汰,即使不一致 时间窗内脏数据入了cache,也会再次淘汰掉。
- (2)/为所有item设定超时时间/,例如10分钟。极限时序下,即使有脏数据入cache,这个 脏数据也最多存在十分钟。带来的副作用是,可能每十分钟,这个key上有一个读请求会 穿透到数据库上,但我们认为这对数据库的从库压力增加是非常小的。
五、扩展性架构实践
扩展性也是架构师在做数据库架构设计的时候需要考虑的一点。我分享一个58同城非常 帅气的/秒级数据扩容/的方案。这个方案解决什么问题呢?原来数据库水平切分成N个库,现 在要扩容成2N个库,要解决这个问题。
./images/2017-03-10-58/640-8.webp 假设原来分成两个库,假设按照hash的方式分片,如上图分为奇数库和偶数库。
./images/2017-03-10-58/640-9.webp
- 第一个步骤/提升从库/,底下一个从库放到上面来(其实什么动作都没有做);
- 第二个步骤/修改配置,此时扩容完成/,原来是2个分片,修改配置后变成4个分片,这个 过程没有数据的迁移。原来偶数的那一部分现在变成了两个部分,一部分是0,一部分是2, 奇数的部分现在变成1和3。0库和2库没有数据冲突,只是扩容之后在短时间内双主的可用性 这个特性丢失掉了。 ./images/2017-03-10-58/640-10.webp
- 第三个步骤还要做一些收尾操作:把/旧的双主给解除掉/,为了保证可用性增加/新的双主 同步/,原来拥有全部的数据,现在只为一半的数据提供服务了,我们/把多余的数据删除掉/, 结尾这三个步骤可以事后慢慢操作。整个扩容在过程在第二步提升从库,修改配置其实 就秒级完成了,非常的帅气。
这个方案的缺点是/只能实现N库到2N 库的扩容/,2变4、4变8,不能实现2库变3库,2库变5库 的扩容,如何能够实现这种扩容呢?
数据库扩展性方面有很多的需求,例如刚才说的2库扩3库,2库扩5库。产品经理经常变化 需求,扩充表的属性也是经常的事情,今年的数据库大会同行也介绍了一些使用触发器来 做online schema change的方案,但是触发器的局限性在于:
- 第一、触发器对数据库性能的影响比较大;
- 第二、触发器只能在同一个库上才有效,而互联网的场景特点是数据量非常大,并发量 非常大,库都分布在不同的物理机器上,触发器没法弄。
最后还有一类扩展性需求,*底层存储介质发生变化*,原来是 mongodb 存储,现在要变为 mysql 存储,这也是扩展性需求(虽然很少),这三类需求怎么扩展?
/方法是导库,迁移数据/,迁移数据有几种做法,第一种/停服务/,如果大家的业务能够 接受这种方法,强烈建议使用这种方法,例如有一些游戏公司,晚上一点到两点服务器维护, 可能就是在干分区或者合区这类导库的事情。
./images/2017-03-10-58/640-11.webp 如果业务上/不允许停服务,想做到平滑迁移,双写法可以解决这类问题/。 - (1)双写法迁移数据的第一步是/升级服务/,原来的服务是写一个库,现在建立新的数据库, 双写。比如底层存储介质的变化,我们原来是 mongo 数据库,现在建立好新的 mysql 数据库, 然后对服务的所有写接口进行双库写升级。 - (2)第二步写一个小程序去进行数据的迁移。比如写一个离线的程序,把两个库的数据 重新分片,分到三个库里。也可能是把一个只有三个属性的用户表导到五个属性的数据表里面。 这个数据迁移要限速,导完之后两个库的数据一致吗?只要提前双写,如果没有什么意外, 两边的数据应该是一致的。 /什么时候会有意外呢?/在导某一条数据的过程当中正好发生了一个删除操作,这个数据 刚被服务双写删除,又被迁移数据的程序插入到了新库中,这种非常极限的情况下会造成 两边的数据不一致。 - (3)建议第三步/再开发一个小脚本,对两边的数据进行比对/,如果发现了不一致,就将数据 修复。当修复完成之后,我们认为数据是一致的,再将双写又变成单写,数据完成迁移。
这个方式的/优点/: - 第一、改动是非常小的,对服务的影响比较小,单写变双写,开发两个小工具,一个是迁移 程序,从一个库读数据,另外一个库插进去;还有一个数据校验程序,两个数据进行比对, 改动是比较小的。 - 第二、随时可回滚的,方案风险比较小,在任何一个步骤如果发现问题,可以随时停止操作。 比如迁移数据的过程当中发现不对,就把新的数据库干掉,重新再迁。因为在切换之前, 所有线上的读服务和写服务都是旧库提供,只有切了以后,才是新库提供的服务。这是我们 非常帅气的一个平滑导库的方式。
六、总结
今天的内容就这么多,大概做一个简单的总结:
首先介绍了单库、分片、复制、分组、路由规则的概念。分片解决的是数据量大的问题, 复制和分组解决的是提高读性能,保证读的可用性的问题。分片会引入路由,常用的三种 路由的方法,按照范围、按照hash,或者新增服务来路由。
怎么保证数据的可用性,保证数据可用性的思路是冗余,但会引发数据的不一致,58同城保 证可用性的实践是双主当主从用,读写流量都在一个库上,另一个库standby,一个主库挂 掉流量自动迁移到另外一个主库,只是资源利用率是50%,并且不能通过增加从库的方式提 高读性。
读性能的实践,传统的玩法是增加从库或者增加缓存。存在的问题是,主从可能不一致, 同城的玩法是服务加数据库加缓存一套的方式来解决这些问题。
一致性的实践,解决主从不一致性有两种方法,一种是增加中间件,中间件记录哪些key上 发生了写操作,在主从同步时间窗口之内的读操作也路由到主库。第二种方法是强制读主。 数据库与缓存的一致性,我们的实践是双淘汰,在发生写请求的时候,淘汰缓存,写入 数据库,再做一个延时的缓存淘汰操作。第二个实践是建议为所有的item设置一个超时时间。
扩展性今天分享了58同城一个非常帅气的N库扩2N库的秒级扩容方案,还分享了一个平滑 双写导库的方案,解决两库扩三库,数据库字段的增加,以及底层介质的变化的问题。
我今天分享的内容就这么多,谢谢大家,希望大家有收获。