《从0开始学架构》里讲述了常用的高性能架构模式,这里面很多大家可能也都用过了,我应该也写过相关的技术文章。正好按照书里的思路重新梳理一次。
一、读写分离
读写分离的基本原理是将数据库读写操作分散到不同的节点上
感想:
读写分离应该是IT公司基建的一部分,绝大部分时候,研发人员没有感知。字节在读写分离上用的事dbatman,使用的事数据库中间件方式。
读写分离主要问题是主从复制延迟和分配机制。分配机制一般也由基建团队搞定了。比较麻烦的是主从复制延迟,因为无论如何延迟都是存在的,只是延迟多久的问题。以前遇到主从问题找DBA,给的回复是保证1s,1s内的都算正常。一般的主从问题可以通过主从延迟如何解决,但是还有一种复杂的边界场景,如sg和va机房都部署了服务,而且分别部署了各自的数据库,数据库之间有同步,这种类似的延迟问题就比较难解决。好在流量一般都会固定的打到某个机房。
二、分库分表
- 业务分库指的是按照业务模块将数据分散到不同的数据库服务器。
- join 操作问题
- 事务问题
- 成本问题
- 单表数据拆分有两种方式:垂直分表和水平分表。
-
垂直分表引入的复杂性主要体现在表操作的数量要增加
-
水平分表有路由、join 操作、count() 操作等问题
感想:
以前写过一文搞懂MySQL数据库分库分表.分库分表我设计中用的比较少,但是团队的项目中还是有一些的。以前做海外电商的时候,订单就是做的水平分表;做跨境电商的时候,根据店铺做的水平分表,但为了做查询,异构了ES。做溯源系统的时候,预计溯源码的数量可能好多亿,需要几T数据,最初也打算按照1个亿分张表,结果公司底层存储升级了,单表能存3个多T的数据,我要解决的场景性能上问题也不大,就没分表。
三、高性能NoSQL
关系数据库的问题
-
关系数据库存储的是行记录,无法存储数据结构
-
关系数据库的 schema 扩展很不方便,如DDL操作
-
关系数据库在大数据场景下 I/O 较高
-
关系数据库的全文搜索功能比较弱
常见的 NoSQL 方案分为 4 类。
-
K-V 存储:解决关系数据库无法存储数据结构的问题,以 Redis 为代表。
-
文档数据库:解决关系数据库强 schema 约束的问题,以 MongoDB 为代表。
- 资料:
- 优点:新增字段简单、历史数据不会出错、可以很容易存储复杂数据
- 缺点:最主要的代价就是不支持事务、无法实现关系数据库的 join 操作
- 列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表。
- 说明:列式数据库就是按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”
- 场景:典型的场景就是海量数据进行统计。例如,计算某个城市体重超重的人员数据,实际上只需要读取每个人的体重这一列并进行统计即可,而行式存储即使最终只使用一列,也会将所有行数据都读取出来。一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除。
- 优点:业务同时读取多个列时效率高,因为这些列都是按行存储在一起的;能够一次性完成对一行中的多个列的写操作;列式存储还具备更高的存储压缩比
- 缺点:典型的场景是需要频繁地更新多个列,因为列式存储将不同列存储在磁盘上不连续的空间;列式存储的随机写效率要远远低于行式存储的写效率;列式存储高压缩率在更新场景下也会成为劣势,因为更新时需要将存储数据解压后更新,然后再压缩,最后写入磁盘
- 资料:
- 全文搜索引擎:解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表。
-
说明:全文搜索引擎的技术原理被称为“倒排索引”(Inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,其基本原理是建立单词到文档的索引。Elastcisearch 是分布式的文档存储方式。它能存储和检索复杂的数据结构——序列化成为 JSON 文档——以实时的方式。
-
使用:目前常用的转换方式是将关系型数据按照对象的形式转换为 JSON 文档,然后将 JSON 文档输入全文搜索引擎进行索引。在 Elasticsearch 中,每个字段的所有数据都是默认被索引的。即每个字段都有为了快速检索设置的专用倒排索引。而且,不像其他多数的数据库,它能在相同的查询中使用所有倒排索引,并以惊人的速度返回结果
感想:
- KV存储用的还是相当多的,但得用好,Redis相关的文章有常用缓存技巧、Redis的事务,Go+lua用起来真香、Redis实现分布式锁、秒杀系统。
- MongoDB其实用的比较少,只用过两次吧,最近的一次是给用户提供数据库功能,而且允许用户随便修改字段。另一个原因是因为面向用户的,所以表会特别多,mysql团队不支持这么多的表,但mongodb支持,虽然一个库支持1W,但可以多开库啊。
- 列示数据库就完全没有接触过了
- 全文搜索引擎还是比较需要的,mysql分表后很多时候得依仗ES做搜索,另外数据量较多,有count、like这种需求的话,得靠ES了。不过用ES的时候有几个问题需要解决,一个是如何保证同步数据的稳定,这个还好搞,一般有基建,二是从Mysql异构到ES需要时间,一般*s以内,得看在这个前提下能否满足业务需求
四、高性能缓存架构
基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。
- 缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。原因
- 存储数据不存在:可以赋个默认值
- 缓存数据生成耗费大量时间或者资源
- 缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。解决方案
- 更新锁:对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 后台更新:由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存
- 缓存热点虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。解决方案
- 缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。以微博为例,对于粉丝数超过 100 万的明星,每条微博都可以生成 100 份缓存,缓存的数据是一样的,通过在缓存的 key 里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。
感想:
这个主要是常用缓存技巧,大部分的业务达不到Redis的极限的,一般得国民级应用。有时候得用Redis的事务,Go+lua用起来真香自己创建些命令出来。
五、单服务器高性能模式:PPC与TPC
单服务器高性能的关键之一就是服务器采取的并发模型,并发模型有如下两个关键设计点:
-
服务器如何管理连接。
-
服务器如何处理请求。
以上两个设计点最终都和操作系统的 I/O 模型及进程模型相关。
-
I/O 模型:阻塞、非阻塞、同步、异步。
-
进程模型:单进程、多进程、多线程
- PPC 是 Process Per Connection 的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的 UNIX 网络服务器所采用的模型。
- 优点:实现简单
- 缺点:fork 代价高;父子进程通信复杂;支持的并发连接数量有限,最多就几百个
- TPC 是 Thread Per Connection 的缩写,其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。与进程相比,线程更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。因此,TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题。
- 缺点:创建线程虽然比创建进程代价低,但高并发时(例如每秒上万连接)还是有性能问题;无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题;多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出
感想:
这个就没用过,太偏底层了。不过还是要有了解的,因为有些时候面临技术选型,能清晰的知道原理是比较重要的。
六、单服务器高性能模式:Reactor与Proactor
PCC和TPC都无法支撑高并发的场景
- Reactor:Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池),其中Reactor 负责监听和分配事件,处理资源池负责处理事件。最终 Reactor 模式有这三种典型的实现方案:
-
单 Reactor 单进程 / 线程。
-
单 Reactor 多线程。
-
多 Reactor 多进程 / 线程。
- Proactor:Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作,这里的“同步”指用户进程在执行 read 和 send 这类 I/O 操作的时候是同步的。Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”,这里的“我”就是操作系统内核。
感想:
这个就没用过,太偏底层了。不过还是要有了解的,因为有些时候面临技术选型,能清晰的知道原理是比较重要的。而且很多时候面试题里会问到。
七、高性能负载均衡:分类及架构
常见的负载均衡系统包括 3 种:DNS 负载均衡、硬件负载均衡和软件负载均衡。
- DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。例如,北方的用户访问北京的机房,南方的用户访问深圳的机房。
- 优点:简单、成本低;就近访问,提升访问速度
- 缺点:更新不及时;扩展性差;分配策略比较简单
- 硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。目前业界典型的硬件负载均衡设备有两款:F5 和 A10。
- 优点:功能强大;性能强大;稳定性高;支持安全防护
- 缺点:价格昂贵;扩展能力差
- 软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS,其中Nginx 是软件的 7 层负载均衡,LVS 是 Linux 内核的 4 层负载均衡。
- 优点:简单;灵活;方便扩展
- 缺点:性能一般,一个 Nginx 大约能支撑 5 万并发;功能没有硬件负载均衡那么强大;一般不具备防火墙和防 DDoS 攻击等安全功能
组合的基本原则为:DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。
感想:
这个也一般用不到,偏基建的一部分。相关的一些操作可以看CDN请求过程详解、常用缓存技巧。
八、高性能负载均衡:算法
任务平分类:负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或者权重上的平均。
- 轮询
- 加权轮询
负载均衡类:负载均衡系统根据服务器的负载来进行分配,这里的负载并不一定是通常意义上我们说的“CPU 负载”,而是系统当前的压力,可以用 CPU 负载来衡量,也可以用连接数、I/O 使用率、网卡吞吐量等来衡量系统的压力。
- 通过连接数、Http请求数等判断
性能最优类:负载均衡系统根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器。
- 通过响应时间
Hash 类:负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上。常见的有源地址 Hash、目标地址 Hash、session idhash、用户 ID Hash 等。
- 源地址hash
- ID hash
感想:
这个也很少用,偏运维侧。刚开始工作的时候,那时候大家主要还是用物理机,不同机器性能不一样,偶尔有机器负载过高,需要和运维一起查看,主要看一下加权轮询的值配置的是否合理。