1. 为何要分布式
随着信息化的推动, 不管是 PC 仍是 mobile 的用户量都在激增, 单机的性能和可靠性是不可能知足用户的增加需求的. 为何分布式? 简单来讲, 不用分布式, 没有更好的方法来解决不断增加的用户访问需求.nginx
分布式是趋势, 对分布式架构中的机制有所了解, 是实施分布式架构的前提. 本文先简要介绍分布式架构中一些的重要的概念和机制, 后续会对其中的机制进行更细致的讲解, 并给出实现示例.redis
2. 分布式架构带来的挑战
实施分布式以后, 首先面对的就是服务数量的增长, 由此给 监控 和 调度 带来了新的挑战. 分布式的复杂性确定比单机要高, 由此又带来 可靠性 和 性能 上的挑战.算法
3. 提升可靠性的设计
提升可靠性, 先得知道可靠性是怎么计算的, 通常有 2 个指标 MTTF 和 MTTR数据库
- MTTF: mean time to failure 平均故障前时间, 注意, 这里是 故障前 时间, 不是 故障 时间
- MTTR: mean time to recovery 平均修复时间
系统可用性计算方法 MTTF/(MTTF+MTTR)缓存
提升系统的可靠性, 有 2 种方式, 一种是作到没有故障, 一种作到故障发生时, 系统依然可以工做. 显然第一种方式就像写出没有 BUG 的程序同样不可能作到, 因此, 应该把故障也当成正常的业务逻辑来处理, 只要故障有了应对之策, 那么对系统的损害就微乎其微, 至少损害是可控的.网络
3.1 监控设计
监控是可靠性的前提, 没有监控, 没法在第一时间发现问题, 更别说预防问题的发生了. 监控也是分层的:架构
- 基础层: CPU, 内存, 网络吞吐, 磁盘 等
- 中间层: nginx, redis, 消息队列, 数据库 等
- 应用层: HTTP 响应时间, 返回码, API 调用链路, 客户端访问信息 等
3.2 一致性设计
一致性是单机应用改形成分布式以后, 首先面对的问题. 一致性有强一致性(ACID)和最终一致性(BASE) 2 种. 现实中, 要求强一致性的场景其实远没有咱们想象的那么多, 在分布式系统中, 不少时候只须要最终一致性(BASE)便可.并发
- ACID: 原子性(Atomcity), 一致性(Consistency), 隔离性(Isolation,又称独立性), 持久性(Durability)
- BASE: 基本可用(Basic Availability), 软状态(Soft-state), 最终一致性(Eventual Consistency)
ACID 是真正的强调一致性, BASE 实际上是强调可用性异步
3.3 重试设计
分布式系统中, 存在不少服务之间的调用, 频繁的互相调用中, 常常会发生些意料以前的间歇性错误. 有些错误在响应端是会自动恢复的, 因此请求端不用对每一个错误都直接返回给客户端, 有时须要再重试几回, 等待响应端的恢复.数据库设计
固然, 重试要看状况, 调用返回超时, 或者返回能够重试的错误(好比, 繁忙中, 维护中, 资源不足等), 那么就能够重试几回. 若是返回 http 503 等, 这些可能触发了响应端的 BUG, 或者响应端服务已经不正常了, 就没有必要重试了.
重试也有策略, 不是一味不停的反复调用, 这样可能会给响应端带来更大的压力, 让其更难自动恢复. 重试的策略有多种, 根据实际状况, 可让重试的时间间隔愈来愈大, 或者对重试次数作限制.
3.4 熔断设计
重试是请求端的机制, 熔断是响应端的机制, 目的是为了防止响应端的进一步恶化. 熔断是响应端保护本身的一种机制, 也就是在不堪重负的时候, 断开本身和外部的联系, 防止进一步恶化(就像家里保护电器的保险丝)
熔断相似保险丝, 但也有不一样, 它不只仅只有 断开 和 连通 2 种状态, 还能够有 半开 的状态, 也就是限制请求的流量, 只处理有限的请求, 直至服务恢复. 当响应端的熔断断开的时候, 应该通知请求端, 中止重试
3.5 限流设计
限流的目的是经过对并发访问进行限速, 让服务端可以响应更多的请求, 不至于在峰值被压死 限流的算法有: 计数器方式, 队列算法(请求速度波动, 处理速度匀速), 漏斗算法, 令牌桶算法
3.6 降级设计
降级是在系统应对突发状况时, 下降损失的一个方案 主要的降级方式有:
- 下降一致性: 从强一致性变为最终一致性
- 中止次要功能: 中止访问不重要的功能, 从而释放出更多的资源
- 简化功能: 把一些功能简化掉. 好比, 简化业务流程, 或是再也不返回全量数据, 只返回部分数据
4. 提升性能的设计
采用分布式设计以后, 不只不会下降性能, 反而在分布式环境下, 有了更多的手段来提升性能.
4.1 缓存设计
缓存能够有效的提升 I/O, 是提升性能的有效手段之一. 在分布式环境下, 除了性能, 缓存的策略还要考虑一致性的问题, 通常有 3 种经常使用策略:
- Cache Aside 更新模式 失效 先从 Cache 取数据, 没有则从数据库获取, 成功后, 放入 Cache 命中 从 Cache 中取数据, 而后返回 更新 先把数据存入数据库中, 成功后, 让缓存失效
- Read/Write Through 更新模式, 与 Cache Aside 相比, 应用不用再关心数据放在缓存仍是数据库中了 Read Through 查询时若是未命中, 则更新缓存, 可是更新缓存的动做由缓存服务来完成, 应用自己不用关心 Write Through 若是未命中, 直接更新数据库, 而后返回. 若是命中, 则更新缓存, 而后由缓存服务本身去更新数据库
- Write Behind Caching 更新模式 在更新数据的时候,只更新缓存,不更新数据库,而咱们的缓存会异步地批量更新数据库 由于 I/O 是异步处理的, 全部更新操做会很是快, 可是带来的问题是, 数据不是强一致性的, 有数据丢失的风险
缓存的设计中, 要考虑爬虫的影响, 由于爬虫有可能会改变缓存数据的优先级, 让真正有用的数据被缓存清除.
4.2 异步设计
同步调用 影响吞吐量, 消耗系统资源, 只能一对一, 有多米诺骨牌效应, 而 异步调用 能有效提升系统的吞吐量. 采用异步设计, 服务解耦以后, 必定要有 监控, 不然出了问题没法及时对应.
异步调用的方式主要有:
- 请求响应方式: 给请求中加入回调, 响应结束后出发回调
- 订阅方式: 接收方订阅发送方的消息, 收到消息就放入处理队列中, 逐步处理
- Broker 方式: 接收方和发送方互相看不到对方, 发送方发送消息到 broker, 接受方从 broker 中获取消息来处理, 这种方式的好处是将服务完全解耦
异步处理的事务一致性通常不是强一致性, 而是最终一致性, 异步处理能够和事件溯源(Event Sourcing)结合, 异步处理 + 事件溯源的方式,能够很好地让咱们的整个系统进行任务的统筹安排、批量处理,可让总体处理过程达到性能和资源的最大化利用
4.3 数据库设计
分布式环境中, 不管是 缓存, 仍是 异步, 最终数据仍是要持久化到数据库中, 若是数据库慢的话, 前面作的再好也效果有限. 提升数据库性能的方式主要有:
- 读写分离(CQRS): 将 Command 和 Query 分开, 若是 Command 操做变为 Event Sourcing, 就能够把写操做也简化掉, 也变成无状态的, 大幅下降写操做的反作用, 以获得更大的并发和性能
- 分库分表(Sharding): 通常来讲, 数据库最大的性能问题有 2 个, 一个是对数据库的操做, 一个是数据库中数据的大小 分库的策略能够按地理位置, 按日期或者某个范围分, 或是按一种哈希散列算法. 数据库分片必须考虑业务, 从业务的角度入手, 而不是从技术的角度入手 只考虑业务分片, 不要考虑哈希散列的方式分片
- 数据分片有水平分片和垂直分片 2 种, 水平分片就是分库, 垂直分片则是分表, 把一张表中的一些字段放到一张表中,另外一些字段放到另外一张表中
5. 分布式架构的部署
分布式环境下, 部署升级的方式也和单机应用不同, 更加灵活, 主要有:
- 停机部署: 现有服务停机, 部署新版本以后再启动
- 蓝绿部署: 分别有 stage/prod 2 套环境, 升级 stage 以后, 将流量切换到 stage, stage 变为 prod, prod 做为下一次升级的 stage 使用物理机的话, 2 套环境会有资源浪费, 虚拟机的话随时回收会好些. 若是服务中有状态的话, 好比缓存之类的, 那么停机部署和蓝绿部署都会有问题
- 滚动部署: 逐步替换应用的全部实例, 来缓慢发布一个新版本, 这种方式对于有状态的服务也是比较友好的 这种方式的问题是新旧版本同时在线, 若是有问题, 回滚麻烦, 并且 2 个版本同时在会带来兼容性的问题
- 灰度部署(金丝雀部署): 将生产环境的流量逐步切换到新环境, 能够按流量分配, 先切 10%, 没有问题, 再加, 有问题回滚. 在多租户环境, 也能够按租户切换流量.
- A/B 测试: 这种方式同时上线 2 个版本, 而后比较可用性, 受欢迎程度, 可见性等 蓝绿部署是为了避免停机, 灰度部署是对新版本的质量没信心. 而 A/B 测试是对新版的功能没信心