最近阅读了一本架构方面的入门图书叫《从零开始学架构:照着做,你也能成为架构师》,部分内容比较不错,先做书摘总结,以便加深印象与未来回顾学习。
本文是该书第二部分,是书中第四、五章,主要介绍存储高性能、计算高性能,涉及到关系型数据库分库分表与读写分离、NoSQL类型、缓存穿透与热点、单服高性能、集群高性能等内容。
连续阅读,请点击如下链接:
第四章 存储高性能
-
关系型数据库
-
读写分离
-
本质:将访问压力分散到集群中的多个节点,但是没有分散存储压力。
-
读写分离的基本实现:
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
-
读写分离在实际应用过程中需要应对复制延迟带来的复杂性。
-
解决主从复制延迟的方法:
- 写操作后的读操作指定发给数据库主服务器。
- 读从机失败后再读一次主机。
- 关键业务读写操作全部指向主机,非关键业务采用读写分离。
-
-
分库分表
- 本质:既可以分散访问压力,又可以分散存储压力。
- 为了满足业务数据存储的要求,就需要将存储分散到多台数据库服务器上。
- 常见的分散存储的方法有**“分库”和“分表”**两大类。
- 业务分库:按照业务模块将数据分散到不同的数据库服务器。
- 业务分库带来的问题:join操作问题、事务问题、成本问题。
- 分表:同一业务的单表数据会达到单台数据库服务器的处理瓶颈,此时需要单表数据进行拆分。单表数据拆分有两种形式:垂直分表和水平分表。
- 单表进行切分以后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定。原因为新的切分表即使在同一个数据库服务器中,也可能带来可观的性能提升。如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。
- 垂直分表引入的复杂性主要体现在表操作的数量要增加。
- 如果单表行数超过5000万行就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。
- 水平分表复杂性:增加路由算法、join操作、count()操作、order by操作、
-
实现方法
- 读写分离需要将读/写操作区分开来,然后访问不同的数据库服务器;分库分表需要根据不同的数据访问不同的数据库服务器,两者本质上都是一种分配机制,即将不同的SQL语句发送到不同的数据库服务器。常见的分配实现方式有两种:程序代码封装和中间件封装。
- 程序代码封装:指在代码中抽象一个数据访问层来实现读写分离、分库分表。
- 中间件封装:指的是独立一套系统出来,实现读写分离和分库分表操作。中间件对业务服务器提供SQL兼容的协议,对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器来看,中间件就是一个数据库服务器。
-
-
NoSQL
NoSQL!=No SQL,而是NoSQL = Not Only SQL
-
K-V存储:Key-Value存储,Key是数据的标识,Value是具体数据。典型Redis。
- Redis缺点:并不支持完整的ACID事务。Redis的事务只能保证隔离性和一致性(I和C),无法保证原子性和持久性(A和D)。
-
文档数据库:未来解决关系数据库schema带来的问题,其最大的特点就是no-schema,可以存储和读取任意的数据。
- 绝大部分文档数据库的数据格式为JSON。因为JSON数据是自描述的,无需在使用前定义字段,读取一个JSON中不存在的字段也不会导致SQL那样的语法错误。
- no-schema特性优势:
- 新增字段简单。
- 历史数据不会出错。
- 可以很容易存储复杂数据。
- 文档数据库特点特别适合电商和游戏的业务场景,例如商品属性。
- 文档数据库缺点:不支持事务、无法实现关系数据库join操作。
-
列式数据库:按照列来存储数据的数据库。
-
优势:
- 业务同时都区多个列是效率高。
- 能够一次性完成对一行中的多格列的写操作。
- 节省I/O,具备更高的存储压缩比。
-
海量数据统计,行式存储是劣势。
-
在需要频繁的更新多个列时,列式数据库为劣势。
-
一般将列式存储应用在离线的大数据分析和统计场景中,这种场景主要针对部分列进行操作,且数据写入后就无需再更新删除。
-
-
全文搜索引擎:传统关系型数据库在支撑全文搜索业务时缺陷,可以通过引入全文搜索引擎来弥补关系型数据库的缺陷。
- 全文搜索引擎的技术原理被称为“倒排索引”(Inverted index),也常被称为反向索引。
- 全文搜索引擎能够基于JSON文档建立全文索引,然后快速进行全文搜索。
-
-
缓存
缓存就是为了弥补存储系统在上述复杂业务场景下的不足。缓存的基本原理是将可能重复使用的数据放到内存中,一次生成,多次使用。避免每次使用都去访问存储系统。
-
单纯依靠存储系统性能提升不够的情况:
- 需要经过复杂运算后得出的数据,存储系统无能为力。
- 读多写少的数据,存储系统有心无力。
-
缓存穿透:指缓存没有发挥作用,业务系统虽然去缓存中查询数据,但缓存中没有数据,业务系统需要再次去存储系统中查询数据。通常情况下有两种情况:存储数据不存在、生成缓存数据需要耗费大量时间和资源。
- 存储数据不存在:缓存在这个场景中并没有起到分担存储系统访问压力的作用。解决方法:数据为找到直接设置一个默认值(可以是空值,也可以是具体的值)并存到缓存中。
- 生成缓存数据需要耗费大量时间和资源:典型的就是电商的商品分页,然后被爬虫访问。这种情况没有太好的解决方案,要么识别爬虫拒绝访问但有可能降低SEO,要么做好监控发现问题及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。
-
缓存雪崩:由于旧的缓存已经被清楚,新的缓存还未生成,并且处理这些请求的县城都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统雪崩。
-
常见解决方法:更新锁机制和后台更新机制。
- 更新锁机制:对缓存更新操作进行加锁保护。而分布式集群的业务系统要完美实现更新锁机制,需要用到分布式锁,如ZooKeeper。
- 后台更新:由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
-
-
缓存热点:解决方案是复制多份缓存,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
-
第五章 计算高性能
-
高性能架构设计主要集中在几个方面:
- 尽量提升单服务器性能,将单服务器的性能发挥到极致。
- 如果单服务器无法支撑性能,设计服务器集群方案。
- 最终实现高性能,还和具体的实现及编码有关。
-
架构设计决定了系统性能的上限,实现细节决定了系统性能的下限。
-
单服务器高性能:关键之一是服务器采取的网络编程模型
-
网络编程模型的两个关键设计点:
- 服务器如何管理连接。
- 服务器如何处理请求。
-
上面两个设计点最终都和操作系统的I/O模型及进程模型相关:
- I/O模型:阻塞、非阻塞、同步、异步。
- 进程模型:单进程、多进程、多线程。
-
PPC(Process per Connection):指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统UNIX网络服务器所采用的模型。
-
PPC模式实现简单,比较适合服务器的连接数没那么多的情况。而互联网兴起后,服务器的并发和访问量从几十剧增到成千上万,这种模式弊端凸显,主要体现在:
- fork代价高。
- 父子进程通信复杂。
- 进程数量增大后对操作系统压力较大。
-
prefork:提前创建进程,系统在启动时就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去“fork”进程的操作,让用户的访问更快、体验更好。
-
prefork的实现关键就是多个子进程都accept同一个socket,当有洗呢连接进入的时,操作系统保证只有一个进程能最后accept成功。
-
但prefork有“惊群”现象,即虽然只有一个子进程能accept成功,但所有阻塞在accept上的子进程都会被唤醒,这样就导致了不必要的进程调度和上下文切换。幸运的是,操作系统可以解决这个问题,例如Linux 2.6版本后内核已经解决了accept惊群问题。
-
TPC(Thread per Connection):指每次有新的连接就新建一个线程去专门处理这个连接的请求。
-
线程比进程更轻量级,同时线程通信比进程通信更简单。因此TPC实际上解决或弱化PPC的fork代价高和父子进程通信复杂两个问题。
-
TPC在高并发时(例如每秒上万连接)还是有性能问题,线程间的互斥和共享又引入了复杂度可能导致死锁问题,多线程会出现互相影响例如内存越界,另外还有CPU线程调度和切换代价问题。
-
在并发几百连接的场景,一般采用PPC方案,因为PPC不会有死锁,也不会有多进程互相影响问题,稳定性最高。
-
prethread:类似prefork,其会预先创建线程,然后才开始接收用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作,让用户的访问更快、体验更好。
-
Apache MPM Worker模式本质是prethread,但稍有改进。prethread理论上可以比prefork支持更多的并发连接,Apache服务器MPM worker模式默认支持16 x 25=400个并发处理线程。
-
Reactor:又叫Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。
-
Reactor模式的核心组成部分包括Reactor和处理资源池(进程池或线程池),其中Reactor负责监听和分配事件,处理资源池负责处理事件。
-
Reactor典型实现方案:
- 单Reactor单进程/单线程
- 缺点:
- 只有一个进程,无法发挥多核CPU的性能。
- Handler在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈。
- Handler在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈。
- 单Reactor单进程的方案应用场景不多,只适用于业务处理非常快速的场景,如Redis。
- 缺点:
- 单Reactor多线程
- 此方案能充分利用多核多CPU的处理能力。
- 缺点:
- 多线程数据共享和访问比较复杂。
- Reactor承担所有的事件监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。
- 多Reactor多进程/多线程
- 多Reactor多进程/多线程的方案看起来比单Reactor多线程要复杂,但实际实现时反而更加简单。
- 目前采用多Reactor多进程实现的开源程序是Nginx,采用多Reactor多线程实现的有Memcache和Netty。
以上方案具体选择进程还是线程,和编程语言及平台有关。
- 单Reactor单进程/单线程
-
Proactor:把I/O操作改为异步就能够进一步提升性能,这就是异步网络模型Proactor。
-
理论上Proactor比Reactor效率要高一些,异步I/O能够充分利用DMA性能,让I/O操作与计算重叠。但实现真正的异步I/O,操作系统需要做大量的工作,目前Windows下通过IOCP实现了真正的异步I/O,而在Linux下的AIO并不完善,因此在Linux下实现高并发网络编程时都是以Reactor模式为主。所以即使boost asio号称实现了Proactor模型,其实它在Windows下采用IOCP,而在Linux下是用Reactor模式(采用epoll)模拟出来的异步模型。
-
-
集群高性能
-
单服务器无论如何优化,无论采用多好的硬件,总会有一个性能天花板,当单服务器的性能无法满足业务需求时,就需要设计高性能集群来提升系统整体的处理性能。
-
高性能集群本质很简单,通过增加更多的服务器来提升系统整体的计算能力。
-
高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。
-
对于任务分配器,现在更通用的叫法是“负载均衡器”。但请时刻记住,负载均衡不止是未来计算单元的负载达到均衡状态。
-
常见的负载均衡系统包括三种:DNS负载均衡、硬件负载均衡和软件负载均衡。
-
DNS负载均衡:
- 优点:简单、成本低;就近访问,提升访问速度。
- 缺点:更新不及时;扩展性差;分配策略比较简单。
-
硬件负载均衡:
- 优点:功能强大;性能强大(100w+高并发);稳定性高;支持安全防护。
- 缺点:价格昂贵;扩展能力差。
-
软件负载均衡:
- 常见的有Nginx和LVS,其中Nginx是软件的7层负载均衡,LVS是Linux内核的4层负载均衡。
- 上述4层和7层的区别在于协议灵活性。Nginx支持HTTP、E-mail协议,而LVS与协议无关,几乎所有应用都可以做,例如聊天、数据库等。
- 优点:简单;便宜;灵活。
- 缺点:性能一般;一般不具备防火墙和防DDoS攻击等安全功能;功能没有硬件负载均衡那么强大。
-
负载均衡架构:
- 地理级别负载均衡:DNS。
- 集群级别负载均衡:F5等硬件。
- 机器级别的负载均衡:Nginx或LVS。
-
负载均衡算法:
- 任务平分类:轮询、加权轮询
- 轮询:负载均衡系统收到请求后,按照顺序轮流分配到服务器上。轮询是最简单的一个策略,无需关注服务器本身的状态。“简单”是轮询算法的优点,也是它的缺点。
- 加权轮询:负载均衡系统根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。
- 负载均衡类:负载最低优先Nginx、LVS
- 这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。例如LVS的连接数,Nginx的HTTP请求数,系统的CPU负载、I/O负载等。
- 其应用场景仅限于负载均衡接收的任何连接请求都会转发给服务器进行处理,否则如果负载均衡系统和服务器之间是固定连接池方式,就不适合采取这种算法。
- 性能最优类:站在客户端的角度进行分配,优先将任务分配给处理速度最快的服务器。
- Hash类:源地址Hash、ID Hash
- 任务平分类:轮询、加权轮询
-
其他相关摘要
- 常见路由算法:范围路由、Hash路由、配置路由。
- 水平分表后实现count()方法:count()相加、记录数表。
- Reactor中,Java语言一般使用线程(如Netty),C语言使用进程或线程皆可(如Nginx使用进程,Memcache使用线程)。