--- title: 大型系统核心技术 date: 2018-07-09 00:00:00 order: 08 categories: - 设计 - 架构 - 综合 tags: - 架构 - 分布式 permalink: /pages/53415678/ --- # 大型系统核心技术 > 大型系统的设计目标就是为了快速、高效、稳定的处理海量的数据以及高并发的请求。 > > 单机服务受限于硬件,客观存在着资源瓶颈,难以应对不断增长的数据量和请求量,为了打破瓶颈,大型系统基本上都被设计为分布式系统。 > > 分布式系统由于其面临的共性问题,在很多场景下的解决方案往往也存在着共性。因此,我们会发现,很多优秀的大型系统在设计方案上存在着很多的共同点。 > > 本文主要讨论应对分布式系统共性问题的解决方案,这既可以加深对分布式系统运作原理的理解,也可以作为设计大型分布式系统时的借鉴。 ## 分布式事务 ## 分布式锁 Java 原生 API 虽然有并发锁,但并没有提供分布式锁的能力,所以针对分布式场景中的锁需要解决的方案。 分布式锁的解决方案大致有以下几种: - 基于数据库实现 - 基于缓存(redis,memcached 等)实现 - 基于 Zookeeper 实现 ### 基于数据库实现分布式锁 #### 实现 ##### 创建表 ```sql CREATE TABLE `methodLock` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', `desc` varchar(1024) NOT NULL DEFAULT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; ``` ##### 获取锁 想要锁住某个方法时,执行以下 SQL: ```sql insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) ``` 因为我们对 `method_name` 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 成功插入则获取锁。 ##### 释放锁 当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql: ```sql delete from methodLock where method_name ='method_name' ``` #### 问题 1. 这把锁强依赖数据库的可用性。如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 3. 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 #### 解决办法 1. 单点问题可以用多数据库实例,同时塞 N 个表,N/2+1 个成功就任务锁定成功 2. 写一个定时任务,隔一段时间清除一次过期的数据。 3. 写一个 while 循环,不断的重试插入,直到成功。 4. 在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 #### 小结 - 优点: 直接借助数据库,容易理解。 - 缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑。 ### 基于 Redis 实现分布式锁 相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好一些。目前有很多成熟的分布式产品,包括 Redis、memcache、Tair 等。这里以 Redis 举例。 #### Redis 命令 - setnx - setnx key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0。 - expire - expire key timeout:为 key 设置一个超时时间,单位为 second,超过这个时间锁会自动释放,避免死锁。 - delete - delete key:删除 key #### 实现 单点实现步骤: 1. 获取锁的使用,使用 setnx 加锁,锁的 value 值为一个随机生成的 UUID,再使用 expire 设置一个过期值。 2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。 #### 问题 - 单点问题。如果单机 redis 挂掉了,那么程序会跟着出错。 - 如果转移使用 slave 节点,复制不是同步复制,会出现多个程序获取锁的情况 #### 小结 可以考虑使用 [redisson 的解决方案](https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8)。 ### 基于 ZooKeeper 实现分布式锁 #### 实现 这也是 ZooKeeper 客户端 curator 的分布式锁实现。 1. 创建一个目录 mylock; 2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点; 3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; 5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 #### 小结 ZooKeeper 版本的分布式锁问题相对比较来说少。 - 锁的占用时间限制:redis 就有占用时间限制,而 ZooKeeper 则没有,最主要的原因是 redis 目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而 ZooKeeper 通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来 redis 如果能像 ZooKeeper 一样添加一些与客户端绑定的临时键,也是一大好事。 - 是否单点故障:redis 本身有很多中玩法,如客户端一致性 hash,服务器端 sentinel 方案或者 cluster 方案,很难做到一种分布式锁方式能应对所有这些方案。而 ZooKeeper 只有一种玩法,多台机器的节点数据是一致的,没有 redis 的那么多的麻烦因素要考虑。 总体上来说 ZooKeeper 实现分布式锁更加的简单,可靠性更高。但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。 ## 分布式 Session 在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。 分布式 Session 的几种实现策略: 1. 粘性 session 2. 应用服务器间的 session 复制共享 3. 基于 cache DB 缓存的 session 共享 ### Sticky Sessions 需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。 缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
### Session Replication 在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。 缺点:占用过多内存;同步过程占用网络带宽以及服务器处理器时间。
### Session Server 使用一个单独的服务器存储 Session 数据,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。 缺点:需要去实现存取 Session 的代码。
## 分布式存储 通常有两种解决方案: 1. 数据分布:就是把数据分块存在不同的服务器上(分库分表)。 2. 数据复制:让所有的服务器都有相同的数据,提供相当的服务。 ## 分布式缓存 使用缓存的好处: - 提升数据读取速度 - 提升系统扩展能力,通过扩展缓存,提升系统承载能力 - 降低存储成本,Cache+DB 的方式可以承担原有需要多台 DB 才能承担的请求量,节省机器成本 根据业务场景,通常缓存有以下几种使用方式 - 懒汉式(读时触发):写入 DB 后, 然后把相关的数据也写入 Cache - 饥饿式(写时触发):先查询 DB 里的数据, 然后把相关的数据写入 Cache - 定期刷新:适合周期性的跑数据的任务,或者列表型的数据,而且不要求绝对实时性 缓存分类: - 应用内缓存:如:EHCache - 分布式缓存:如:Memached、Redis ## 分布式计算 ## 负载均衡 ### 算法 #### 轮询(Round Robin) 轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。
#### 加权轮询(Weighted Round Robbin) 加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。
#### 最少连接(least Connections) 由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。
最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
#### 加权最少连接(Weighted Least Connection) 在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。
#### 随机算法(Random) 把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
#### 源地址哈希法 (IP Hash) 源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。 - 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。 - 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。
### 实现 #### HTTP 重定向 HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。 缺点: - 用户访问的延迟会增加; - 如果负载均衡器宕机,就无法访问该站点。
#### DNS 重定向 使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。 缺点: - DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。
#### 修改 MAC 地址 使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。
#### 修改 IP 地址 在网络层修改请求的目的 IP 地址。
#### 代理自动配置 正向代理与反向代理的区别: - 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。 - 反向代理:发生在服务器端,用户不知道代理的存在。 PAC 服务器是用来判断一个请求是否要经过代理。
## 资料 - https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html - https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E5%88%86%E5%B8%83%E5%BC%8F%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90.md - https://www.jianshu.com/p/453c6e7ff81c - https://juejin.im/post/5a20cd8bf265da43163cdd9a - https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8 - https://github.com/L316476844/distributed-session - [分布式缓存架构基础](https://juejin.im/entry/57e39e320e3dd90058021bff) - [阿里 P8 技术专家细究分布式缓存问题](https://www.toutiao.com/i6533812974807679495/?tt_from=weixin&utm_campaign=client_share&from=singlemessage×tamp=1521281305&app=news_article&utm_source=weixin&iid=28128279343&utm_medium=toutiao_android&weixin_list=1&wxshare_count=2&pbid=6517746516513195523)