「数据密集型应用系统设计」读后感与团队高并发高性能实践案例
1.一致性(Consistency):在多个节点上维护相同的数据副本,确保所有节点在任何给定时间点都能看到相同的数据状态。这是CAP理论中的C部分(一致性、可用性和分区容错性)。
2.可用性(Availability):即使部分节点出现故障或网络分区,系统也要能够继续提供服务。这个问题与一致性相互冲突,因为为了提高可用性,可能需要牺牲一致性。
3.分区容错性(Partition Tolerance):在网络分区情况下,系统仍然可以正常工作。网络分区可能会导致节点之间的通信中断,从而影响系统的整体性能和稳定性。
4.数据同步和复制:在多个节点上复制数据以提高可用性和减少单点故障带来的风险。但这引入了数据同步和一致性问题。
5.故障恢复和容错:当某个节点或组件发生故障时,系统需要能够自动检测并恢复到正常状态,或者在某种程度上继续运作。
1)主从复制
1.1)为什么需要主从复制
(1)读写分离
(2)数据容灾
(3)分担主压力
1.2)mysql主从复制
1.2.1)原理
主从复制步骤:
①Master节点进行insert、update、delete操作时,会按顺序写入到binlog中。
②salve从库连接master主库。
③当Master节点的binlog发生变化时,binlog dump 线程会通知所有的salve节点,并将相应的binlog内容推送给slave节点。
④I/O线程接收到 binlog 内容后,将内容写入到本地的中继日志relay-log。
⑤SQL thread读取I/O线程写入的relay-log,并且根据 relay-log 的内容对从数据库做对应的操作。
1.2.2)主从复制模式
(1)同步复制
(2)异步复制:mysql默认的复制方式
(3)半同步模式
1.2.3)主从复制binlog模式
MySQL 主从复制的 binlog 模式主要有以下几种:
在这种模式下,主库会将执行的每一条 SQL 语句记录到 binlog 中,然后从库会重新执行这些 SQL 语句。 优点:binlog 文件较小,适合大部分简单的 SQL 语句。 缺点:对于某些包含不确定性或依赖于环境的 SQL 语句(如
NOW()或UUID()),复制可能会出现不一致的情况。
在这种模式下,主库会将每一行数据的变化记录到 binlog 中,而不是记录 SQL 语句本身。从库会直接应用这些行数据的变化。 优点:可以避免语句模式下由于某些 SQL 语句的不确定性导致的复制不一致问题,适用于复杂的 SQL 操作。 缺点:binlog 文件可能会变得非常大,特别是在进行批量更新或插入操作时。
这种模式是 语句 和 行 的结合。在大部分情况下,MySQL 会使用 语句 模式,但在某些情况下(如无法保证语句在从库上执行结果一致时),会自动切换到 行 模式。 优点:结合了 语句 和 行 的优点,能够在大多数情况下保证复制的一致性和效率。 缺点:复杂性增加,可能需要更多的调试和监控。
binlog_format参数值是MIXED。在这种模式下,MySQL 会在大多数情况下使用基于语句的复制(SBR),但在某些需要更高一致性的情况下(例如,当语句包含不确定性或依赖于环境时),会自动切换到基于行的复制(RBR)。1.3)Reids主从复制
主从复制(全量复制)流程图:
master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。
主从复制初始化流程如图所示。
从上图可以看到,当主服务器判断可以执行部分重同步时向从服务器返回"+CONTINUE";需要执行完整重同步时向从服务器返回"+FULLRESYNC RUN_ID OFFSET",其中RUN_ID为主服务器自己的运行ID,OFFSET为复制偏移量。
2)复制偏移量必须包含在复制缓冲区中。
从服务器重启(复制信息丢失); 主服务器故障导致主从切换(从多个从服务器重新选举出一台机器作为主服务器,主服务器运行ID发生改变)。
这时候无法执行部分重同步的,而这两种情况又很常见,因此Redis 4.0针对主从复制又提出了两点优化,提出了psync2协议。
if(rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
== -1)return-1;
voidshiftReplicationId(void) {
memcpy(server.replid2,server.replid,sizeof(server.replid));
server.second_replid_offset = server.master_repl_offset+1;
changeReplicationId();
{...
gotoneed_full_resync;
假设m为主服务器(运行ID为M_ID),A、B和C为三个从服务器;某一时刻主服务器m发生故障,从服务器A升级为主服务器(同时会记录replid2=M_ID),从服务器B和C重新向主服务器A发送"psync M_ID psync_offset"请求;显然根据上面条件,只要psync_offset满足条件,就可以执行部分重同步。
1.4)延迟复制问题
读写分离策略:在读写分离的系统中,确保关键的读取操作(如需要最新数据的查询)始终从主服务器读取,而非关键的读取操作可以从从服务器读取。 数据一致性要求:根据业务需求,明确哪些操作需要读取最新数据,哪些操作可以容忍一定的延迟。
2)数据分区
面对海量数据或者高并发场景的数据,主从复制技术还不够,还需要将数据拆分为多个分区。分区的目的是为了高可扩展性。
2.1)分区算法
取模
Hash
特点:
散列函数的输出是一个固定长度的哈希值。
相同的输入总是会产生相同的输出。
散列函数的设计目标是使不同的输入尽量均匀地分布到输出范围内,以减少冲突。
数据量和请求量分布均匀:使用散列函数将数据均匀分布到各个节点上,确保每个节点存储的数据量和处理的请求量大致相同,从而避免单个节点成为性能瓶颈。
扩容短板:当需要扩容时,增加或减少节点会导致大量数据需要重新分配,因为散列函数的结果会发生变化。这种情况下,几乎所有的数据都需要迁移到新的节点,导致扩容过程复杂且影响性能。
范围
扩容好:使用范围分片时,每个节点负责一定范围的数据。当需要扩容时,只需将新的范围分配给新节点,旧节点上的数据迁移量较小,扩容过程相对简单。
请求量不均匀:由于数据分布是基于范围的,如果某些范围的数据请求量特别大,会导致部分节点负载过高,而其他节点负载较低,造成请求量的不均匀分布。
一致性Hash
平衡数据分布和扩容问题:一致性Hash通过将数据和节点都映射到一个虚拟的环上,使得每个节点只负责环上特定范围的数据。扩容时,只需将部分数据从现有节点迁移到新节点,迁移量较小,数据分布也相对均匀。
减少数据迁移:当添加或删除节点时,只需重新分配相邻节点的数据,大部分数据不需要移动,极大地减少了数据迁移量。
假设需要增加一台服务器CS4,经过同样的hash运算,该服务器最终落于t1和t2服务器之间,具体如下图所示:
哈希槽:散列和取模的结合
Redis集群(Cluster)并没有选用上面一致性哈希,而是采用了哈希槽(SLOT)的这种概念。主要的原因就是上面所说的,一致性哈希算法对于数据分布、节点位置的控制并不是很友好。
1.散列函数:首先,使用散列函数 CRC16(key) 将键转换为一个哈希值。
2.2)分库分表
个人建议能不分就不分,通过合适的索引,读写分离、冷热数据等方式,可以很好的解决性能问题:避免“过度设计”和“过早优化”
数据量过大,正常运维已经影响到了业务访问的阶段才开始考虑分
分库分表有2种模式,分别如下:
1.CLIENT模式:Apache开源社区的ShardingSphere-JDBC、阿里的TDDL;
mycat架构图:
2.3)扩容理想状态
最好不要数据迁移、无数据热点的问题
范围求模分片案例:
优点可以避免扩容时的数据迁移,又可以一定程度上避免范围分片的热点问题
1.比如数据库刚开始预估是4000W数据量,采用2个库shard0、shard1。里面分别有2张表Table_0,Table_1。通过idhash2分别对应不同shard0、shard1数据库库。里面数据量再通过范围比如0-2000w定位到Table_0,2000-4000w定位到Table_1.
2.扩容(不迁移数据):比如数据量超过了4000万,需要扩容的时候,之前0-4000万数据保持不动,比如扩容到1个亿。则采用上面类似算法,把6000-1个亿的数据再次分布
3.热点:解决数据热点的问题(因为我们局部用了散列)
4.总结:1.多查一次数据库(字典表)2.依赖全局的ID生成
2.4)Redis高可用集群
1)数据库主从模式
采用1主2从模式,业务配置写主库,可延迟的导出或者查询采用读从库。如果对配置一致性比较强,则读主库。从库另外一个作用是大数据抽数,晚上抽数任务运行,但不影响主库
踩坑案例:一条delete语句导致主从延迟问题
前提:mysql-binlog模式是row
现象:在业务验证阶段,发现大数据抽数数据不全(数据库主库数据2800W,大数据抽到1500W数据) alpha_aging_product_info_no_degrade_v2表数据量5700W(保留2天数据)
经过分析操作记录如下:
1.10.8号加的delete语句:delete from alpha_aging_product_info_no_degrade_v2 where create_time <'2022-10-19 00:00:00'; 主从延迟小于30分钟,可接受
2.10.18号添加了一个组合索引(仓+地址)
根本原因:
因为上面的sql增加了索引加剧了主从延迟,如果 delete 的数据是大量的数据,则会:
1.如果不加 limit 由于需要更新大量数据,从而索引失效变成全扫描导致锁表,同时由于修改大量的索引,产生大量的日志,导致这个更新会有很长时间,锁表锁很长时间,期间这个表无法处理线上业务。
2.由于产生了大量 binlog 导致主从同步压力变大。
改进点:
1.由于该数据库只为promise给路由推数不降级数据使用,数据库只有增加操作,故可让大数据抽主库
2.truncate table(VtDriver驱动是不支持truncate语法),truncate操作需要慎用,需要根据业务场景评估。
truncate表都是高危操作,特别是在生产环境要更加小心,下面列出几点注意事项,希望大家使用时可以做下参考。
1.truncate无法通过binlog回滚。
2.truncate会清空所有数据且执行速度很快。
3.truncate不能对有外键约束引用的表使用。
4.执行truncate需要drop权限,不建议给账号drop权限。
思考点:
1. MySQL单表记录数过大,思考是否一定要用MYSQL?比如JDQ数据传输等
2.当MySQL单表记录数过大时,增删改查性能都会急剧下降,任何的sql操作都不能根据常规思维去操作(比如加字段,加索引,删除语句,Select查询语句等)
3.XBP的SQL审批工单添加备注:表记录数,比如同一个sql 表数据10万和1千万是不一样的。
2)大key治理
2.1)扫描大key
严重影响 QPS、TP99 等指标,对大Key进行的慢操作会导致后续的命令被阻塞,从而导致一系列慢查询。 大Key发生热点,大 String,value 大于 20K。当OPS为 10000,流量即为 200M, 达到单实例的流量配额. 导致无法正常提供服务。 集群各分片内存使用不均。某个分片占用内存较高或OOM,发送缓存区增大等,导致该分片其他Key被逐出,同时也会造成其他分片的资源浪费。 集群各分片的带宽使用不均。某个分片被流控,其他分片则没有这种情况,且影响宿主机上的其它应用。
2.2)大key改造
2.2.1)改造list set zset hash元素个数:1000
//首次个人设置清空历史1000+天数据//后面代码自动找到过去7天的fields日期,执行deleallFields.forEach( expireDay -> {deleteCache(logPrefix,storeProductionKey,expireDay);});private void deleteCache(String logPrefix,StoreProductionKey storeProductionKey, String day){String key = storeProductionKey.generateConfigKey();CallerInfo callerInfo = umpService.registerInfo(".XXX.deleteCache");try{redisClient.hDel(logPrefix, key, day);}catch (Exception e){if(log.isErrorEnabled()){log.error(logPrefix +"XXX" + key + day,e);}umpService.functionError(callerInfo);}finally {umpService.registerInfoEnd(callerInfo);}}
2.2.2)把大key变小key
public class WhiteSkuTimeRangeCO {/*** 开始时间*/@JSONField(name = "st")private long effectSt;/*** 结束时间*/@JSONField(name = "ed")private long effectEd;}
刚开始数据量不大,大key不明显,运营N年后,数据量变成1000+条,大key就体现出来了
3)热key治理
案例1:
案例2:
比如promise:xxx:yyy|:zzz这个key,固定前置,hash到固定的某个槽位,流量都打到这个机器。即是热key也是大key
解决方案:1)首先本地缓存是一方面,但没有从根源解决。2)如果某个关键被确认为热点,一个简单方法在关键字开头或者结尾添加一个随机数(比如两位数),这样就可以将关键字分布到100个不同的关键字上,从而分配到不同的分区上。比如上面key对应改造为 promise:xxx:yyy|:zzz:01 ...... promise:xxx:yyy|:zzz:50 ...... promise:xxx:yyy|:zzz:99
1、《数据密集型应用系统设计》
2、《Redis设计与实现 第二版》
3、《Redis5设计与源码分析》
4、Redis之哈希分片原理一致性哈希算法与crc16算法(https://www.cnblogs.com/lemon-flm/p/15205026.html)