分布式架构心得
1. 为什么要分布式
2. 分布式架构带来的挑战
3. 提高可靠性的设计
3.1 监控设计
3.2 一致性设计
3.3 重试设计
3.4 熔断设计
3.5 限流设计
3.6 降级设计
4. 提高性能的设计
4.1 缓存设计
4.2 异步设计
4.3 数据库设计
5. 分布式架构的部署
1. 为什么要分布式
随着信息化的推进, 不论是 PC 还是 mobile 的用户量都在激增, 单机的性能和可靠性是不可能满足用户的增长需求的. 为什么分布式? 简单来说, 不用分布式, 没有更好的方法来解决不断增长的用户访问需求.
分布式是趋势, 对分布式架构中的机制有所了解, 是实施分布式架构的前提. 本文先简要介绍分布式架构中一些的重要的概念和机制, 后续会对其中的机制进行更细致的讲解, 并给出实现示例.
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 测试是对新版的功能没信心