架构设计

18 架构设计思维导图 阅读更多

17 架构设计文档模板 阅读更多

0.1. 备选方案模板 0.1.1. 需求介绍 0.1.2. 需求分析 0.1.2.1. W 0.1.2.2. H 0.1.2.3. C 0.1.3. 复杂度分析 0.1.3.1. 高可用 0.1.3.2. 高性能 0.1.3.3. 可扩展 0.1.4. 备选方案 0.1.5. 备选方案评估 0.2. 架构设计模板 0.2.1. 总体方案 0.2.2. 架构总览 0.2.3. 核心流程 0.2.4. 详细设计 0.2.4.1. 高可用设计 0.2.4.2. 高性能设计 0.2.4.3. 可扩展设计 0.2.4.4. 安全设计 0.2.4.5. 其他设计 0.2.4.6. 部署方案 0.2.5. 架构演进规则 0.1. 备选方案模板 0.1.1. 需求介绍 [需求介绍主要描述需求的背景、目标、范围等] 0.1.2. 需求分析 [需求分析主要全方位地描述需求相关的信息] 0.1.2.1. W 5W 指 Who、When、What、Why、Where Who:需求利益干系人,包括开发者、使用者、购买者、决策者等 When:需求使用时间,包括季节、时间、里程碑等 What:需求的产出是什么,包括系统、数据、文件、开发库、平台等 Where:需求的应用场景,包括国家、地点、环境等,例如测试平台只会在测试环境使用 Why:需求需要解决的问题,通常和需求背景相关 0.1.2.2. H [这里的 How 不是设计方案也不是架构方案,而是关键业务流程。消息队列系统这部分内容很简单,但有的业务系统 1H 就是具体的用例了,有兴趣的同学可以尝试写写 ATM 机取款的业务流程。如果是复杂的业务系统,这部分也可以独立成“用例文档”] 0.1.2.3. C 8C 指的是 8 个约束和限制即 Constraints,包括 性能 Performance 成本 Cost 时间 Time 可靠性 Reliability 安全性 Security 合规性 Compliance 技术性 Technology 兼容性 Compatibility 注:需求中涉及的性能、成本、可靠性等仅仅是利益关联方提出的诉求,不一定准确;如果经过分析有的约束没有必要,或成本太高、难度太大,这些约束是可以调整的。 0.1.3. 复杂度分析 [分析需求的复杂度,复杂度常见的有高可用、高性能、可扩展等,具体分析方法请参考专栏前面的内容] 注:文档的内容省略了分析过程,实际操作的时候每个约束和限制都要有详细的逻辑推导,避免完全拍脑袋式决策。 0.1.3.1. 高可用 0.1.3.2. 高性能 0.1.3.3. 可扩展 0.1.4. 备选方案 [备选方案设计,至少 3 个备选方案,每个备选方案需要描述关键的实现,无须描述具体的实现细节] 0.1.5. 备选方案评估 [备选方案 360 度环评。注意备选方案评估的内容会根据评估会议的结果进行修改,也就是说架构师首先给出自己的备选方案评估,然后举行备选方案评估会议,再根据会议结论修改备选方案文档] 0.2. 架构设计模板 [备选方案评估后会选择一个方案落地实施,架构设计文档就是用来详细描述细化方案的] 0.2.1. 总体方案 [总体方案需要从整体上描述方案的结构,其核心内容就是架构图,以及针对架构图的描述,包括模块或者子系统的职责描述、核心流程] 0.2.2. 架构总览 [架构总览给出架构图以及架构的描述] 0.2.3. 核心流程 0.2.4. 详细设计 [详细设计需要描述具体的实现细节] 0.2.4.1. 高可用设计 0.2.4.2. 高性能设计 0.2.4.3. 可扩展设计 [此处省略具体设计。如果方案不涉及,可以简单写上“无”,表示设计者有考虑但不需要设计;否则如果完全不写的话,方案评审的时候可能会被认为是遗漏了设计点] 0.2.4.4. 安全设计 0.2.4.5. 其他设计 0.2.4.6. 部署方案 [部署方案主要包括硬件要求、服务器部署方式、组网方式等] 0.2.5. 架构演进规则 [通常情况下,规划和设计的需求比较完善,但如果一次性全部做完,项目周期可能会很长,因此可以采取分阶段实施,即:第一期做什么、第二期做什么,以此类推]

16 开源项目 阅读更多

0.1. 选:如何选择一个开源项目 0.1.1. 聚焦是否满足业务 0.1.2. 聚焦是否成熟 0.1.3. 聚焦运维能力 0.2. 用:如何使用开源项目 0.2.1. 深入研究,仔细测试 0.2.2. 小心应用,灰度发布 0.2.3. 做好应急,以防万一 0.3. 改:如何基于开源项目做二次开发 0.3.1. 保持纯洁,加以包装 0.3.2. 发明你要的轮子 软件开发领域有一个流行的原则:DRY,Don’t repeat yourself,即不要重复造轮子。开源项目的主要目的是共享,就是为了让大家不要重复造轮子,尤其是在互联网这样一个快速发展的领域,速度就是生命,引入开源项目可以节省大量的人力和时间,大大加快业务的发展速度。 开源项目虽然节省了大量的人力和时间,但带来的问题也不少,相信绝大部分技术人员都踩过开源软件的坑,小的影响可能是宕机半小时,大的问题可能是丢失几十万条数据,甚至灾难性的事故是全部数据都丢失。 不要重复发明轮子,但要找到合适的轮子。 0.1. 选:如何选择一个开源项目 0.1.1. 聚焦是否满足业务 聚焦于是否满足业务,而不需要过于关注开源项目是否优秀。 如果你的业务要求 1000 TPS,那么一个 20000 TPS 和 50000 TPS 的项目是没有区别的。架构是可以不断演进的,等到真的需要这么高的时候再来架构重构,这里的设计决策遵循架构设计原则中的“合适原则”和”演化原则”。 0.1.2. 聚焦是否成熟 很多新的开源项目往往都会声称自己比以前的项目更加优秀:性能更高、功能更强、引入更多新概念,但实际上都有意无意地隐藏了一个负面的问题:更加不成熟! 不成熟的开源项目应用到生产环境,风险极大:轻则宕机,重则宕机后重启都恢复不了,更严重的是数据丢失都找不回来。 在选择开源项目时,尽量选择成熟的开源项目,降低风险。 从这几个方面考察开源项目是否毕竟,很多财大气粗的公司(BAT 等)都是这样做的,否则我们也就没有那么多好用的开源项目了。成熟: 版本号:除非特殊情况,否则不要选 0.X 版本的,至少选 1.X 版本的,版本号越高越好。 使用的公司数量:一般开源项目都会把采用了自己项目的公司列在主页上,公司越大越好,数量越多越好。 社区活跃度:看看社区是否活跃,发帖数、回复数、问题处理速度等。 0.1.3. 聚焦运维能力 大部分架构师在选择开源项目时,基本上都是聚焦于技术指标,例如性能、可用性、功能这些评估点,而几乎不会去关注运维方面的能力。 如果要将项目应用到线上生产环境,则运维能力是必不可少的一环,否则一旦出问题,运维、研发、测试都只能干瞪眼! 从这几个方面去考察运维能力: 开源项目日志是否齐全:有的开源项目日志只有寥寥启动停止几行,出了问题根本无法排查。 开源项目是否有命令行、管理控制台等维护工具,能够看到系统运行时的情况。 开源项目是否有故障检测和恢复的能力,例如告警、切换等。 如果是开源库,例如 Netty 这种网络库,本身是不具备运维能力的,就需要在使用库的时候将一些关键信息通过日志记录下来,例如在 Netty 的 Handler 里面打印一些关键日志。 0.2. 用:如何使用开源项目 0.2.1. 深入研究,仔细测试 从这几方面进行研究和测试: 毕竟,很多财大气粗的公司(BAT 等)都是这样做的,否则我们也就没有那么多好用的开源项目了。 通读开源项目的设计文档或者白皮书,了解其设计原理。 核对每个配置项的作用和影响,识别出关键配置项。 进行多种场景的性能测试。 进行压力测试,连续跑几天,观察 CPU、内存、磁盘 I/O 等指标波动。 进行故障测试:kill、断电、拔网线、重启 100 次以上、切换等。 0.2.2. 小心应用,灰度发布 再怎么深入地研究,再怎么仔细地测试,都只能降低风险,但不可能完全覆盖所有线上场景。先在非核心的业务上用,然后有经验后慢慢扩展。 0.2.3. 做好应急,以防万一 即使前面的工作做得非常完善和充分,也不能认为万事大吉,尤其是刚开始使用一个开源项目,运气不好可能遇到一个之前全世界的使用者从来没遇到的 bug,导致业务都无法恢复,尤其是存储方面,一旦出现问题无法恢复,可能就是致命的打击。 对于重要的业务或者数据,使用开源项目时,最好有另外一个比较成熟的方案做备份,尤其是数据存储。 0.3. 改:如何基于开源项目做二次开发 0.3.1. 保持纯洁,加以包装 当发现开源项目有的地方不满足需求时,自然会有一种去改改的冲动。一种方式是投入几个人从内到外全部改一遍,将其改造成完全符合业务需求。但这样做有几个比较严重的问题: 投入太大 失去了跟随原项目演进的能力 所以不要改动原系统,而是要开发辅助系统:监控、报警、负载均衡、管理等。 以 Redis 为例,如果我们想增加集群功能,则不要去改动 Redis 本身的实现,而是增加一个 proxy 层来实现。 如果实在想改到原有系统,建议直接给开源项目提需求或者 bug,但弊端就是响应比较缓慢,这个就要看业务紧急程度了,如果实在太急那就只能自己改了;如果不是太急,建议做好备份或者应急手段即可。 0.3.2. 发明你要的轮子 选与不选开源项目,核心还是成本和收益的问题,并不是说选择开源项目就一定是最优的项目,最主要的问题是:没有完全适合你的轮子! 软件领域和硬件领域最大的不同就是软件领域没有绝对的工业标准。 开源项目为了能够大规模应用,考虑的是通用的处理方案,而不同的业务其实差异较大,通用方案并不一定完美适合具体的某个业务。 如果有钱有人有时间,投入人力去重复发明完美符合自己业务特点的轮子也是很好的选择!

15 架构重构 阅读更多

0.1. 有的放矢 0.2. 合纵连横 0.2.1. 合纵 0.2.2. 连横 0.3. 运筹帷幄 “架构设计三原则”中的演化原则,提到了系统的架构是不断演化的,少部分架构演化可能需要推倒重来进行重写,但绝大部分的架构演化都是通过架构重构来实现的。相比全新的架构设计来说,架构重构对架构师的要求更高,主要体现在: 业务已经上线,不能停下来:既需要尽量保证业务继续往前发展,又要完成架构调整 关联方众多,牵一发动全身:尽量减少对关联方的影响,或者协调关联方统一行动,是一项很大的挑战 旧架构的约束:需要在旧的架构基础上进行,这是一个很强的约束,会限制技术选择范围 即使推倒重来,完全抛弃旧的架构而去设计新的架构,新架构也会受到旧架构的约束和影响,因为业务在旧架构上产生的数据是不能推倒重来的,新架构必须考虑如何将旧架构产生的数据转换过来。 架构重构对架构师的综合能力要求非常高: 业务上需要架构师能够说服产品经理暂缓甚至暂停业务来进行架构重构; 团队上需要架构师能够与其他团队达成一致的架构重构计划和步骤; 技术上需要架构师给出让技术团队认可的架构重构方案。 0.1. 有的放矢 通常情况下,当系统架构不满足业务的发展时,其表现形式是系统不断出现各种问题: 轻微的如系统响应慢、数据错误、某些用户访问失败等, 严重的可能是宕机、数据库瘫痪、数据丢失等, 或者系统的开发效率很低。 期望通过架构重构来解决所有问题当然是不现实的,所以首要任务是从一大堆纷繁复杂的问题中识别出真正要通过架构重构来解决的问题,集中力量快速解决,而不是想着通过架构重构来解决所有的问题。 需要透过问题表象看到问题本质,找出真正需要通过架构重构解决的核心问题,从而做到有的放矢,既不会耗费大量的人力和时间投入,又能够解决核心问题。 一个简单的做法判断到底是采取架构重构还是采取系统优化:假设现在需要从 0 开始设计当前系统,新架构和老架构是否类似?如果差异不大,说明采取系统优化即可;如果差异很大,那可能就要进行系统重构了。 0.2. 合纵连横 0.2.1. 合纵 架构重构是大动作,持续时间长,且会占用一定的研发资源(包括,开发和测试),因此不可避免地会影响业务功能的开发。因此,要想真正推动一个架构重构项目启动,需要花费大量的精力进行游说和沟通,要和利益相关方沟通好,让大家对于重构能够达成一致共识,避免重构过程中不必要的反复和争执。 在沟通时经常遇到的一个问题是凭感觉而不是凭数据说话。所以在沟通协调时,将技术语言转换为通俗语言,以事实说话,以数据说话,是沟通的关键! 0.2.2. 连横 除了和上下游沟通协调,有的重构还需要和其他相关或者配合的系统的沟通协调。由于大家都是做技术的,有比较多的共同语言,所以这部分的沟通协调其实相对来说要容易一些,但也不是说想推动就能推动的,主要的阻力来自“这对我有什么好处”和“这部分我这边现在不急”。 有效推动的策略是“换位思考、合作双赢、关注长期”。就是站在对方的角度思考,重构对他有什么好处,能够帮他解决什么问题,带来什么收益。 如果真的出现了对公司或者部门有利,对某个小组不利的情况,那可能需要协调更高层级的管理者才能够推动,平级推动是比较难的。 因为大部分重构的系统并不是到了火烧眉毛非常紧急的时候才开始启动的,而是有一定前瞻性的规划,采取等待的策略也未尝不可,但要明确正式启动的时间。 除了计划上灵活一点,方案上也可以灵活一点:可以先不做这个系统相关的重构,先把其他需要重构的做完。因为大部分需要重构的系统,需要做的事情很多,分阶段处理,在风险规避、计划安排等方面更加灵活可控。 0.3. 运筹帷幄 区分问题的优先级:集中有限资源去解决最重要或者最关键的问题 将问题分类:相似问题统筹考虑 不能迫于业务压力,专门挑容易做的实施:到了稍微难一点的问题时,因为复杂度和投入等原因被搁置,达不到重构的真正目的 集中有限的资源,某个阶段集中解决某一类问题。 首先这样做效率高,因为阶段目标比较明确,做决策和方案的时候无须进行太多选择; 其次每个阶段都能看到明显的成果,给团队很大的信心。比如说第一阶段做完后,系统很少有因为机器过载、缓存响应慢、虚拟机挂死等问题导致的故障;完成第二阶段的事项后,因为组件、外部系统故障导致系统故障的问题也很少了。 完成前两个阶段后,可以安心地做第三阶段的“服务化”工作了。 总结一下重构的做法,其实就是“分段实施”,将要解决的问题根据优先级、重要性、实施难度等划分为不同的阶段,每个阶段聚焦于一个整体的目标,集中精力和资源解决一类问题。这样做有几个好处: 每个阶段都有明确目标,做完之后效果明显,团队信心足,后续推进更加容易。 每个阶段的工作量不会太大,可以和业务并行。 每个阶段的改动不会太大,降低了总体风险。 具体如何制定“分段实施”的策略呢? 优先级排序:将明显且又比较紧急的事项优先落地,解决目前遇到的主要问题。 问题分类:将问题按照性质分类,每个阶段集中解决一类问题。 先易后难: 首先,一开始就做最难的部分,会发现想要解决这个最难的问题,要先解决其他容易的问题。 其次,最难的问题解决起来耗时都比较长,占用资源比较多,如果一开始做最难的,可能做了一两个月还没有什么进展和成果,会影响相关人员对项目的评价和看法,也可能影响团队士气。 第三,刚开始的分析并不一定全面,所以一开始对最难的或者最关键的事项的判断可能会出错。 采取“先易后难”的策略,能够很大程度上避免“先难后易”策略的问题。 首先,随着项目的推进,一些相对简单的问题逐渐解决,会发现原来看起来很难的问题已经不那么难了,甚至有的问题可能都消失了。 其次,先易后难能够比较快地看到成果,虽然成果可能不大,但至少能看到一些成效了,对后续的项目推进和提升团队士气有很大好处。 第三,随着项目的进行,原来遗漏的一些点,或者分析和判断错误的点,会逐渐显示出来,及时根据实际情况进行调整,能够有效地保证整个重构的效果。 循序渐进:按照前 3 个步骤划分了架构重构的实施阶段后,就需要评估每个阶段所需要耗费的时间,很可能会出现有的阶段耗时可能只要 1 个月,而有的却需要 6 个月,虽然这可能确实是客观事实,但通常情况下,按照固定的步骤和节奏,更有利于项目推进。 每个阶段最少 1 个月,最长不要超过 3 个月,如果评估超过 3 个月的,那就再拆分为更多阶段。先划分了阶段,每个阶段又分了任务子集,当任务子集比较小的时候,多个任务子集可以并行;当任务子集比较大的时候,就当成一个独立的里程碑推进。

14 互联网架构模板 阅读更多

0.1. 存储层技术 0.1.1. SQL 0.1.2. NoSQL 0.1.3. 小文件存储 0.1.4. 大文件存储 0.2. 开发层技术 0.2.1. 开发框架 0.2.2. Web服务器 0.2.3. 容器 0.3. 服务层技术 0.3.1. 配置中心 0.3.2. 服务中心 0.3.2.1. 服务名字系统 0.3.2.2. 服务总线系统 0.3.3. 消息队列 0.4. 网络层技术 0.4.1. 负载均衡 0.4.1.1. DNS 0.4.1.2. Nginx、LVS、F5 0.4.2. CDN 0.4.3. 多机房 0.4.4. 多中心 0.5. 用户层技术 0.5.1. 用户管理 0.5.2. 消息推送 0.5.3. 存储云、图片云 0.6. 业务层技术 0.7. 平台技术 0.7.1. 运维平台 0.7.1.1. 标准化 0.7.1.2. 平台化 0.7.2. 自动化 0.7.3. 可视化 0.7.4. 测试平台 0.7.5. 数据平台 0.7.5.1. 数据管理 0.7.5.2. 数据分析 0.7.5.3. 数据应用 0.7.6. 管理平台 互联网的标准技术架构如下图所示: 0.1. 存储层技术 0.1.1. SQL SQL 即通常所说的关系数据,关系数据不可能完全被抛弃,NoSQL是Not Only SQL,即 NoSQL 是 SQL 的补充。 一般情况下互联网行业都是用 MySQL、PostgreSQL ,这类开源数据库的特点是开源免费;缺点是性能比商业数据库要差一些。随着业务发展,性能要求越来越高,必然要将数据拆分到多个数据库实例才能满足业务的性能需求。 数据库拆分满足了性能的要求,但带来了复杂度的问题:数据如何拆分、数据如何组合?这个复杂度的问题解决起来并不容易,所以流行的做法是将这部分功能独立成中间件,将分库分表做到自动化和平台化,如MySQL官方推荐的MySQL Router。 业务继续发展,规模继续扩大,SQL 服务器越来越多,导致新的复杂度问题:数据库资源使用率不高,各 SQL 集群分开维护成本越来越高。因此,一般都会在 SQL 集群上构建 SQL 存储平台,以对业务透明的形式提供资源分配、数据备份、迁移、容灾、读写分离、分库分表等一系列服务。 0.1.2. NoSQL NoSQL 在数据结构上与传统的 SQL 的不同,例如: Memcache 的 key-value 结构 Redis 的复杂数据结构 MongoDB 的文档数据结构 NoSQL 将性能作为自己的一大卖点。 NoSQL 的这两个特点很好地弥补了关系数据库的不足,因此在互联网行业 NoSQL 的应用基本上是基础要求。 NoSQL 方案一般自身就提供集群的功能,因此在刚开始应用时很方便。 一般不会在开始时就考虑将 NoSQL 包装成存储平台,但如果公司发展很快,例如 Memcache 的节点有上千甚至几千时,NoSQL 存储平台就很有意义了。 存储平台通过集中管理能够大大提升运维效率 存储平台可以大大提升资源利用效率 NoSQL 发展到一定规模后,在集群的基础之上再实现统一存储平台,主要实现如下功能: 资源动态按需动态分配 资源自动化管理 故障自动化处理 一般几十台 NoSQL 服务器,做存储平台收益不大;有几千台 NoSQL 服务器,NoSQL 存储平台就能够产生很大的收益。 0.1.3. 小文件存储 除了关系型的业务数据,还有很多用于展示的数据。这些数据具有三个典型特征: 数据小,一般在 1MB 以下 数量巨大 访问量巨大 基本上业务在起步阶段就可以考虑做小文件统一存储,通常将小文件存储做成统一的和业务无关的平台,避免重复造轮子。 在开源方案的基础上封装一个小文件存储平台并不是太难的事情。例如,HBase、Hadoop、Hypertable、FastDFS 等都可以作为小文件存储的底层平台,只需要将这些开源方案再包装一下基本上就可以用了。 0.1.4. 大文件存储 大文件主要分为两类: 业务大数据,例如 Youtube 的视频、电影网站的电影; 海量日志数据,例如各种访问日志、操作日志、用户轨迹日志等。 在存储上和小文件有较大差别,不能直接将小文件存储系统拿来存储大文件。开源方案现在很成熟了,所以大数据存储和处理相对简单,在几个流行的开源方案中选择,例如,Hadoop、HBase、Storm、Hive 等。 0.2. 开发层技术 0.2.1. 开发框架 随着业务发展,复杂度越来越高,系统拆分的越来越多,不同的系统由不同的小组开发。如果每个小组用不同的开发框架和技术,则会带来很多问题: 技术人员之间没有共同的技术语言,交流合作少。 每类技术都需要投入大量的人力和资源并熟练精通。 不同团队之间人员无法快速流动,人力资源不能高效的利用。 对于框架的选择,有一个总的原则:优选成熟的框架,避免盲目追逐新技术! 成熟的框架资料文档齐备,遇到问题很容易通过搜索来解决。 成熟的框架受众更广,招聘时更加容易招到合适的人才。 成熟的框架更加稳定,不会出现大的变动,适合长期发展。 0.2.2. Web服务器 开发框架只是负责完成业务功能的开发,真正能够运行起来给用户提供服务,还需要服务器配合。 选择一个服务器主要和开发语言相关,例如: Java 的有 Tomcat、JBoss、Resin 等 PHP/Python 的用 Nginx Apache什么语言都支持 0.2.3. 容器 传统的虚拟化技术是虚拟机,解决了跨平台的问题,但由于虚拟机太庞大,启动又慢,运行时太占资源,在互联网行业并没有大规模应用; Docker 的容器技术,虽然没有跨平台,但启动快,几乎不占资源,Docker 类的容器技术将是技术发展的主流方向。 Docker 不只是一个虚拟化或者容器技术,它将在很大程度上改变目前的技术形势: 运维方式会发生革命性的变化:基于 Docker 打造自动化运维、智能化运维将成为主流方式。 设计模式会发生本质上的变化:设计思路朝“微服务”的方向发展。 0.3. 服务层技术 服务层的主要目标是降低系统间相互关联的复杂度。 0.3.1. 配置中心 当系统数量不多的时候,一般是各系统自己管理自己的配置,但系统数量多了以后,这样的处理方式会有问题: 某个功能上线时,需要多个系统配合一起上线,分散配置时,配置检查、沟通协调需要耗费较多时间。 处理线上问题时,需要多个系统配合查询相关信息,分散配置时,操作效率很低,沟通协调也需要耗费较多时间。 各系统自己管理配置时,一般是通过文本编辑的方式修改的,没有自动的校验机制,容易配置错误,而且很难发现。 实现配置中心主要就是为了解决上面这些问题,将配置中心做成通用系统的好处: 集中配置多个系统,操作效率高。 所有配置都在一个集中的地方,检查方便,协作效率高。 配置中心可以实现程序化的规则检查(正则表达式),避免常见的错误。 配置中心相当于备份了系统的配置,当某些情况下需要搭建新的环境时,能够快速搭建环境和恢复业务。 0.3.2. 服务中心 当系统数量不多的时候,系统间的调用一般都是直接通过配置文件记录在各系统内部的,但当系统数量多了以后,这种方式就存在问题了。 服务中心就是为了解决跨系统依赖的“配置”和“调度”问题。服务中心的实现一般来说有两种方式:服务名字系统和服务总线系统。 0.3.2.1. 服务名字系统 DNS 的作用将域名解析为 IP 地址 服务名字系统是为了将 Service 名称解析为“host + port + 接口名称”,真正发起请求的还是请求方 0.3.2.2. 服务总线系统 相比服务名字系统,服务总线系统更进一步了:由总线系统完成调用,服务请求方都不需要直接和服务提供方交互了。 0.3.3. 消息队列 互联网业务的一个特点是“快”,这就要求很多业务处理采用异步的方式。 传统的异步通知方式是由消息生产者直接调用消息消费者提供的接口进行通知的,但当业务变得庞大,子系统数量增多时,这样做会导致系统间交互非常复杂和难以管理,因为系统间互相依赖和调用,整个系统的结构就像一张蜘蛛网。 消息队列就是为了实现这种跨系统异步通知的中间件系统。消息队列既可以“一对一”通知,也可以“一对多”广播。 对比蜘蛛网架构,可以清晰地看出引入消息队列系统后的效果: 整体结构从网状结构变为线性结构,结构清晰。 消息生产和消息消费解耦,实现简单。 增加新的消息消费者,消息生产者完全不需要任何改动,扩展方便。 消息队列系统可以做高可用、高性能,避免各业务子系统各自独立做一套,减轻工作量。 业务子系统只需要聚焦业务即可,实现简单。 消息队列系统基本功能的实现比较简单,但要做到高性能、高可用、消息时序性、消息事务性则比较难。 业界已经有很多成熟的开源实现方案: 如果要求不高,基本上拿来用即可,例如,RocketMQ、Kafka、ActiveMQ 等。 如果业务对消息的可靠性、时序、事务性要求较高时,则要深入研究这些开源方案,否则很容易踩坑。 开源的用起来方便,但要改就很麻烦了。由于其相对比较简单,花费人力和时间重复造一个轮子,这样可以根据自己的业务特点做快速的适配开发。 0.4. 网络层技术 除了复杂度,互联网业务发展的另外两个关键特点是“高性能”和“高可用”。设计高可用和高性能系统的时候,主要关注点在系统本身的复杂度,但是,单个系统的高可用和高性能并不等于整体业务的高可用和高性能,要从更高的角度去设计,这就是“网络”,这里强调的是站在网络层的角度整体设计架构,而不是某个具体网络的搭建。 0.4.1. 负载均衡 将请求均衡地分配到多个系统上,使用负载均衡的原因:每个系统的处理能力是有限的,为了应对大容量的访问,必须使用多个系统。 例如,一台 32 核 64GB 内存的机器,性能测试数据显示每秒处理 Hello World 的 HTTP 请求不超过 2 万,实际业务机器处理 HTTP 请求每秒可能才几百 QPS,而互联网业务并发超过 1 万是比较常见的,遇到双十一、过年发红包这些极端场景,每秒可以达到几十万的请求。 0.4.1.1. DNS DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。一般不会使用 DNS 来做机器级别的负载均衡,因为太耗费 IP 资源了。 DNS 负载均衡: 优点:通用(全球通用)、成本低(申请域名,注册 DNS 即可) 缺点: DNS 缓存的时间比较长,即使将某台业务机器从 DNS 服务器上删除,由于缓存的原因,还是有很多用户会继续访问已经被删除的机器。 DNS 不够灵活。DNS 不能感知后端服务器的状态,只能根据配置策略进行负载均衡,无法做到更加灵活的负载均衡策略。 所以对于时延和故障敏感的业务,可以尝试实现 HTTP-DNS 的功能,即使用 HTTP 协议实现一个私有的 DNS 系统。 HTTP-DNS 主要应用在通过 App 提供服务的业务上,因为在 App 端可以实现灵活的服务器访问策略,如果是 Web 业务,实现起来就比较麻烦一些,因为 URL 的解析是由浏览器来完成的,只有 Javascript 的访问可以像 App 那样实现比较灵活的控制。 HTTP-DNS 的优缺点有: 灵活:可以根据业务需求灵活的设置各种策略。 可控:自己开发的系统,IP 更新、策略更新等无需依赖外部服务商。 及时:不受传统 DNS 缓存的影响,可以非常快地更新数据、隔离故障。 开发成本高:没有通用的解决方案,需要自己开发。 侵入性:需要 App 基于 HTTP-DNS 进行改造。 0.4.1.2. Nginx、LVS、F5 Nginx、LVS、F5 用于同一地点内机器级别的负载均衡。 Nginx 是软件的 7 层负载均衡,性能是万级;一般的 Linux 服务器上装个 Nginx 大概能到 5万/秒;支持 HTTP、E-mail 协议 LVS 是内核的 4 层负载均衡,性能是十万级;达到 80万/秒;和协议无关 F5 是硬件的 4 层负载均衡,性能是百万级;从 200万/秒到 800万/秒都有;和协议无关 软件和硬件的区别就在于性能,硬件远远高于软件。 如果按照同等请求量级来计算成本的话,实际上硬件负载均衡设备可能会更便宜,例如假设每秒处理 100 万请求,用一台 F5 就够了,但用 Nginx,可能要 20 台,这样折算下来用 F5 的成本反而低。 4 层和 7 层的区别就在于协议和灵活性。 0.4.2. CDN 为了解决用户网络访问时的“最后一公里”效应,本质上是一种“以空间换时间”的加速策略,即将内容缓存在离用户最近的地方,用户访问的是缓存的内容,而不是站点实时的内容。 CDN 经过多年的发展,已经变成了一个很庞大的体系:分布式存储、全局负载均衡、网络重定向、流量控制等都属于 CDN 的范畴,尤其是在视频、直播等领域,如果没有 CDN,用户是不可能实现流畅观看内容的。CDN 作为网络的基础服务,独立搭建的成本巨大。 0.4.3. 多机房 从架构上来说,单机房就是一个全局的网络单点,在发生比较大的故障或者灾害时,单机房难以保证业务的高可用。 多机房设计最核心的因素就是如何处理时延带来的影响,常见的策略有: 同城多机房:搭建私有的高速网络,基本上能够做到和同机房一样的效果。 跨城多机房:机房间通过网络进行数据复制,但由于网络时延的问题,业务上需要做一定的妥协和兼容,且与业务有很强的关联性。 跨国多机房:一般仅用于备份和服务本国用户。 0.4.4. 多中心 多中心必须以多机房为前提,但从设计的角度来看,多中心相比多机房是本质上的飞越,难度也高出一个等级。 多机房的主要目标是灾备,多中心要求每个中心都同时对外提供服务,且业务能够自动在多中心之间切换,故障后不需人工干预或者很少的人工干预就能自动恢复。 多中心设计的关键就在于“数据一致性”和“数据事务性”,这两个难点都和业务紧密相关。 0.5. 用户层技术 0.5.1. 用户管理 互联网业务的一个典型特征就是通过互联网将众多分散的用户连接起来,因此用户管理是互联网业务必不可少的一部分。 互联网业务拆分为多个子系统,管理用户时需要要实现单点登录(SSO),实现手段较多,例如 cookie、JSONP、token 等,目前最成熟的开源方案是 CAS,架构如下: 当业务做大后,开放成为了促进业务进一步发展的手段,需要允许第三方应用接入:授权登录。现在最流行的授权登录就是 OAuth 2.0 协议,基本上已经成为了事实上的标准。 0.5.2. 消息推送 消息推送根据不同的途径,分为: 短信:运营商的短信接口 邮件:邮件服务商的邮件接口 站内信:系统提供的消息通知功能 App 推送:iOS(APNS) 和 Android(国外GCM,国内各家自己定制或阿里云移动推送、腾讯信鸽推送、百度云推送或第三方友盟推送、极光推送) 推送 通常情况下: 如果不涉及敏感数据,Android 系统上推荐使用第三方推送服务,毕竟专业,消息到达率是有一定保证的。 如果涉及敏感数据,需要自己实现消息推送,主要包含 3 个功能: 设备管理(唯一标识、注册、注销) 连接管理 消息管理 主要挑战: 海量设备和用户管理:消息推送的设备数量众多,存储和管理这些设备是比较复杂的;同时,为了针对不同用户进行不同的业务推广,还需要收集用户的一些信息,简单来说就是将用户和设备关联起来,需要提取用户特征对用户进行分类或者打标签等。 连接保活:要想推送消息必须有连接通道,但是应用又不可能一直在前台运行,大部分设备为了省电省流量等原因都会限制应用后台运行,限制应用后台运行后连接通道可能就被中断了,导致消息无法及时的送达。连接保活是整个消息推送设计中细节和黑科技最多的地方,例如应用互相拉起、找手机厂商开白名单等。 消息管理:实际业务运营过程中,并不是每个消息都需要发送给每个用户,而是可能根据用户的特征,选择一些用户进行消息推送。由于用户特征变化很大,各种排列组合都有可能,将消息推送给哪些用户这部分的逻辑要设计得非常灵活,才能支撑花样繁多的业务需求,可以采取规则引擎之类的微内核架构技术。 0.5.3. 存储云、图片云 互联网业务场景中,用户会上传多种类型的文件数据: 数据量大 文件体积小 访问有时效性 为了满足用户的文件上传和存储需求,需要对用户提供文件存储和访问功能,简单来说,存储云和图片云通常的实现都是“CDN + 小文件存储”,现在有了“云”之后,直接买云服务可能是最快也是最经济的方式。 普通的文件基本上提供存储和访问就够了 图片涉及裁剪、压缩、美化、审核、水印等处理,通常图片云会拆分为独立的系统对用户提供服务 0.6. 业务层技术 业务层面对的主要技术挑战是“复杂度”,复杂度越来越高的一个主要原因就是系统越来越庞大,业务越来越多。通过拆分,化整为零,分而治之,将整体复杂性分散到多个子业务或子系统中。 子系统太多出现另一个问题,业务的调用流程复杂,出了问题排查也会特别复杂。通过合并,高内据低耦原则,将职责关联比较强的子系统合成一个虚拟业务域,然后通过网关对外统一呈现。 0.7. 平台技术 0.7.1. 运维平台 运维平台核心的职责分为四大块:配置、部署、监控、应急,每个职责对应系统生命周期的一个阶段。 配置:主要负责资源的管理。例如,机器管理、IP 地址管理、虚拟机管理等。 部署:主要负责将系统发布到线上。例如,包管理、灰度发布管理、回滚等。 监控:主要负责收集系统上线运行后的相关数据并进行监控,以便及时发现问题。 应急:主要负责系统出故障后的处理。例如,停止程序、下线故障机器、切换 IP 等。 运维平台的核心设计要素是“四化”:标准化、平台化、自动化、可视化。 0.7.1.1. 标准化 需要制定运维标准,规范配置管理、部署流程、监控指标、应急能力等,各系统按照运维标准来实现,避免不同的系统不同的处理方式。标准化是运维平台的基础,没有标准化就没有运维平台。 如果某个系统就是无法改造自己来满足运维标准,常见的做法是不改造系统,由中间方来完成规范适配。 例如,某个系统对外提供了 RESTful 接口的方式来查询当前的性能指标,而运维标准是性能数据通过日志定时上报,那么就可以写一个定时程序访问 RESTful 接口获取性能数据,然后转换为日志上报到运维平台。 0.7.1.2. 平台化 传统的手工运维方式需要投入大量人力,效率低,容易出错,因此需要在运维标准化的基础上,将运维的相关操作都集成到运维平台中,通过运维平台来完成运维工作。 运维平台的好处有: 将运维标准固化到平台中,无须运维人员死记硬背运维标准。 提供简单方便的操作,相比之下人工操作低效且容易出错。 可复用,一套运维平台可以支撑几百上千个业务系统。 0.7.2. 自动化 传统手工运维方式效率低下的一个主要原因就是要执行大量重复的操作,运维平台可以将这些重复操作固化下来,由系统自动完成。 例如,一次手工部署需要登录机器、上传包、解压包、备份旧系统、覆盖旧系统、启动新系统,这个过程中需要执行大量的重复或者类似的操作。 有了运维平台后,平台需要提供自动化的能力,完成上述操作,部署人员只需要在最开始单击“开始部署”按钮,系统部署完成后通知部署人员即可。 有了运维平台后,运维平台可以实时收集数据并进行初步分析,当发现数据异常时自动发出告警,无须运维人员盯着数据看,或者写一大堆“grep + awk + sed”来分析日志才能发现问题。 0.7.3. 可视化 运维平台有非常多的数据,如果全部通过人工去查询数据再来判断,则效率很低。 尤其是在故障应急时,时间就是生命,处理问题都是争分夺秒,能减少 1 分钟的时间就可能挽回几十万元的损失,可视化的主要目的就是为了提升数据查看效率。 可视化相比简单的数据罗列,具备下面这些优点: 能够直观地看到数据的相关属性 能够将数据的含义展示出来 能够将关联数据整合一起展示 0.7.4. 测试平台 测试平台核心的职责就是测试,包括单元测试、集成测试、接口测试、性能测试等,都可以在测试平台完成。 测试平台的核心目的是提升测试效率,从而提升产品质量,其设计关键就是自动化。 传统的测试方式是测试人员手工执行测试用例,测试效率低,重复的工作多。通过测试平台提供的自动化能力,测试用例能够重复执行,无须人工参与,大大提升了测试效率。 为了达到“自动化”的目标,测试平台的基本架构如下图所示。 用例管理:测试自动化的主要手段是通过脚本或者代码来进行测试。为了能够重复执行这些测试用例,测试平台需要将用例管理起来,管理的维度包括业务、系统、测试类型、用例代码。 资源管理:测试用例要放到具体的运行环境中才能真正执行,运行环境包括硬件(服务器、手机、平板电脑等)、软件(操作系统、数据库、Java 虚拟机等)、业务系统(被测试的系统)。除了性能测试,一般的自动化测试对性能要求不高,所以为了提升资源利用率,大部分的测试平台都会使用虚拟技术来充分利用硬件资源,如虚拟机、Docker 等技术。 任务管理:将测试用例分配到具体的资源上执行,跟踪任务的执行情况。任务管理是测试平台设计的核心,它将测试平台的各个部分串联起来从而完成自动化测试。 数据管理:测试任务执行完成后,需要记录各种相关的数据(例如,执行时间、执行结果、用例执行期间的 CPU、内存占用情况等),这些数据具备下面这些作用: 展现当前用例的执行情况。 作为历史数据,方便后续的测试与历史数据进行对比,从而发现明显的变化趋势。 作为大数据的一部分,可以基于测试的任务数据进行一些数据挖掘。 0.7.5. 数据平台 数据平台的核心职责主要包括三部分:数据管理、数据分析和数据应用。每一部分又包含更多的细分领域,详细的数据平台架构如下图所示。 0.7.5.1. 数据管理 数据管理包含数据采集、数据存储、数据访问和数据安全四个核心职责,是数据平台的基础功能。 数据采集:从业务系统搜集各类数据。例如,日志、用户行为、业务数据等,将这些数据传送到数据平台。 数据存储:从业务系统采集的数据存储到数据平台,用于后续数据分析。 数据访问:对外提供各种协议用于读写数据。例如,SQL、Hive、Key-Value 等读写协议。 数据安全:通常数据平台都是多个业务共享的,部分业务敏感数据需要加以保护,防止被其他业务读取甚至修改,因此需要设计数据安全策略来保护数据。 0.7.5.2. 数据分析 数据分析包括数据统计、数据挖掘、机器学习、深度学习等几个细分领域。 数据统计:根据原始数据统计出相关的总览数据。例如,PV、UV、交易额等。 数据挖掘:为了与机器学习和深度学习区分开,这里的数据挖掘主要是指传统的数据挖掘方式。例如,有经验的数据分析人员基于数据仓库构建一系列规则来对数据进行分析从而发现一些隐含的规律、现象、问题等。 机器学习、深度学习:属于数据挖掘的一种具体实现方式,其实现方式与传统的数据挖掘方式差异较大,因此数据平台在实现机器学习和深度学习时,需要针对机器学习和深度学习独立进行设计。 0.7.5.3. 数据应用 数据应用很广泛,既包括在线业务,也包括离线业务。例如,推荐、广告等属于在线应用,报表、欺诈检测、异常检测等属于离线应用。 数据应用能够发挥价值的前提是需要有“大数据”,只有当数据的规模达到一定程度,基于数据的分析、挖掘才能发现有价值的规律、现象、问题等。如果数据没有达到一定规模,通常情况下做好数据统计就足够了。 0.7.6. 管理平台 管理平台的核心职责就是权限管理,无论是业务系统(例如,淘宝网)、中间件系统(例如,消息队列 Kafka),还是平台系统(例如,运维平台),都需要进行管理。如果每个系统都自己来实现权限管理,效率太低,重复工作很多,因此需要统一的管理平台来管理所有的系统的权限。 权限管理主要分为两部分:身份认证、权限控制,其基本架构如下图所示。 身份认证:确定当前的操作人员身份,防止非法人员进入系统。为了避免每个系统都自己来管理用户,通常情况下都会使用企业账号来做统一认证和登录。 权限控制:根据操作人员的身份确定操作权限,防止未经授权的人员进行操作。

13 技术演进方向与模式 阅读更多

0.1. 技术演进的动力 0.2. 技术演进的模式 0.2.1. 互联网行业 0.2.1.1. 业务复杂性 0.2.1.2. 用户规模 0.2.1.3. 量变到质变 潮流派的价值观是:新技术肯定能带来很大收益; 稳定派的价值观是:稳定压倒一切; 跟风派的价值观是:别人用了我就用。 这些价值观本身都有一定的道理,但如果不考虑实际情况生搬硬套,就会出现“橘生淮南则为橘,生于淮北则为枳”的情况。 不同派别存在的问题: 潮流派:新技术需要时间成熟,刚出来就用,实际应用中很可能遇到各种“坑”。新技术需要学习成本,掌握后又发现不适用,则是一种较大的人力浪费。 保守派:无视技术的发展,不能享受新技术带来的收益,因为新技术很多都是为了解决以前技术存在的固有缺陷。 跟风派:既不会承担“潮流派”的风险,也不会遭受“保守派”的损失,花费的资源也少,简直就是一举多得。如果没有风可跟的时候怎么办。竞争对手的信息并不那么容易获取,即使获取到了也是不全面的,一不小心可能就变成邯郸学步了。即使有风可跟,盲目模仿可能带来相反的效果。 0.1. 技术演进的动力 站在技术本身的角度来考虑问题的,陷入“不识庐山真面,只缘身在此山中”。要想看到“庐山真面目”,只有跳出技术的范畴,从一个更广更高的角度来考虑这个问题,这个角度就是企业的业务发展。 影响企业业务的发展主要有 3 个因素:市场、技术、管理,这三者构成支撑业务发展的铁三角,任何一个因素的不足,都可能导致企业的业务停滞不前。 在这个铁三角中,业务处于三角形的中心,毫不夸张地说,市场、技术、管理都是为了支撑企业的业务发展。 企业的业务分为两类:一类是产品类,一类是服务类。 产品类:360 的杀毒软件、苹果的 iPhone、UC 的浏览器等都属于这个范畴,这些产品本质上和传统的制造业产品类似,都是具备了某种“功能”,单个用户通过购买或者免费使用这些产品来完成自己相关的某些任务,用户对这些产品是独占的。 服务类:百度的搜索、淘宝的购物、新浪的微博、腾讯的 IM 等都属于这个范畴,大量用户使用这些服务来完成需要与其他人交互的任务,单个用户“使用”但不“独占”某个服务。事实上,服务的用户越多,服务的价值就越大。服务类的业务符合互联网的特征和本质:“互联”+“网”。 对于产品类业务,技术创新推动业务发展!因为,用户选择一个产品的根本驱动力在于产品的功能是否能够更好地帮助自己完成任务。 对于服务类业务,业务发展推动技术发展!原因,用户选择服务的根本驱动力不是功能,而是“规模”。 当“规模”成为业务的决定因素后,服务模式的创新成为业务发展的核心驱动力,而产品只是为了完成服务而提供给用户使用的一个载体。 服务类的业务发展路径是这样的: 提出一种创新的服务模式 吸引了一批用户 业务开始发展 吸引了更多用户 服务模式不断完善和创新 吸引越来越多的用户 如此循环往复 综合这些分析,除非是开创新的技术能够推动或者创造一种新的业务,其他情况下,都是业务的发展推动了技术的发展。 0.2. 技术演进的模式 业务模式千差万别: 有互联网的业务(淘宝、微信等), 有金融的业务(中国平安、招商银行等), 有传统企业的业务(各色 ERP 对应的业务)等, 但无论什么模式的业务,如果业务的发展需要技术同步发展进行支撑,无一例外是因为业务“复杂度”的上升,导致原有的技术无法支撑。 复杂度要么来源于功能不断叠加,要么来源于规模扩大,从而对性能和可用性有了更高的要求。判断到底是什么复杂度发生了变化至关重要。 在架构设计时,判断业务当前和接下来一段时间的主要复杂度是什么就非常关键。 判断不准确就会导致投入大量的人力和时间做了对业务没有作用的事情, 判断准确就能够做到技术推动业务更加快速发展。 判断的标准就是基于业务发展阶段。这也是为什么架构师必须具备业务理解能力的原因。不同的行业业务发展路径、轨迹、模式不一样,架构师必须能够基于行业发展和企业自身情况做出准确判断。 0.2.1. 互联网行业 互联网业务千差万别,但由于它们具有“规模决定一切”的相同点,其发展路径也基本上是一致的。互联网业务发展一般分为几个时期:初创期、发展期、竞争期、成熟期。不同时期的差别主要体现在两个方面:复杂性、用户规模。 0.2.1.1. 业务复杂性 互联网业务发展第一个主要方向就是“业务越来越复杂”。 发展时期 业务发展特点 技术要求 初创期 创新 快 发展期(堆功能期/优化期/架构期) 逐渐完善 快速实现需求/系统优化/调整架构(拆分) 竞争期(重复造轮子/系统交互混乱) 形成规模 平台化/服务化 成熟期 行业成熟、市场地位牢固,业务求精 技术进入成熟期,继续优化 0.2.1.2. 用户规模 互联网业务的发展第二个主要方向就是“用户量越来越大”,随着业务阶段的发展,用户量也随着变大。 用户量增大对技术的影响主要体现在两个方面:性能要求越来越高、可用性要求越来越高。 性能:单台 MySQL 机器支撑的 TPS 和 QPS 最高也就是万级,低的可能是几千,高的也不过几万。需要从集中式变成分布式存储,复杂度大幅提升。 可用性:系统宕机造成的口碑影响以及可用性对收入的影响随着用户量增大而增大。 0.2.1.3. 量变到质变 互联网业务驱动技术发展的两大主要因素是复杂性和用户规模,而这两个因素的本质其实都是“量变带来质变”。 阶段 用户规模 业务阶段 技术影响 婴儿期 0~1万 初创期 用户规模对性能和可用性没有压力 幼儿期 1万~10万 初创期 用户规模对性能和可用性略有压力,单台服务器可能撑不住,需要多台机器 少年期 10~100万 发展期 用户规模对性能和可用性有较大压力,系统优化同时将大业务拆分为多个子业务 青年期 100万~1000万 竞争期 用户规模对性能和可用性有很大压力,集群,多机房,平台化,服务化 壮年期 1000万~1亿 竞争&成熟期 用户规模对性能和可用性有巨大压力,架构方案可能要推倒重来,机会难得非常锻炼人 巨人期 1亿+ 成熟期 行业垄断地位,稳妥 应对业务质变带来的技术压力,不同时期有不同的处理方式,但其核心目标都是为了满足业务“快”的要求,当发现业务快不起来的时候,其实就是技术的水平已经跟不上业务发展的需要了,技术变革和发展的时候就到了。 更好的做法是在问题还没有真正暴露出来就能够根据趋势预测下一个转折点,提前做好技术上的准备,这对技术人员的要求是非常高的。

12 微内核架构 阅读更多

0.1. 基础架构 0.2. 设计关键点 0.2.1. 插件管理 0.2.2. 插件连接 0.2.3. 插件通信 0.2.4. OSGi 架构简析 微内核架构(Microkernel Architecture),也称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品(原文为 product-based,指存在多个版本、需要下载安装才能使用,与 web-based 相对应)的应用。 例如 Eclipse 这类 IDE 软件、UNIX 这类操作系统、淘宝 App 这类客户端软件等,也有一些企业将自己的业务系统设计成微内核的架构,例如保险公司的保险核算逻辑系统,不同的保险品种可以将逻辑封装成插件。 0.1. 基础架构 微内核架构包含两类组件:核心系统(core system)和插件模块(plug-in modules)。 核心系统:负责与具体业务功能无关的通用功能,例如模块加载、模块间通信等; 插件模块:负责实现具体的业务逻辑,例如“学生信息管理”系统中的“手机号注册”功能。 微内核的基本架构示意图如下: 上面这张图中核心系统功能比较稳定,不会因为业务功能扩展而不断修改,插件模块可以根据业务功能的需要不断地扩展。微内核的架构本质是将变化部分封装在插件里面,从而达到快速灵活扩展的目的,而又不影响整体系统的稳定。 0.2. 设计关键点 微内核的核心系统设计的关键技术有:插件管理、插件连接和插件通信。 0.2.1. 插件管理 核心系统需要知道当前有哪些插件可用,如何加载这些插件,什么时候加载插件。 常见的实现方法是插件注册表机制。 核心系统提供插件注册表(配置文件/代码/数据库),插件注册表含有每个插件模块的信息,包括它的名字、位置、加载时机(启动就加载/按需加载)等。 0.2.2. 插件连接 插件如何连接到核心系统,通常,核心系统必须制定插件和核心系统的连接规范,然后插件按照规范实现,核心系统按照规范加载即可。 常见的连接机制有 OSGi(Eclipse 使用)、消息模式、依赖注入(Spring 使用),甚至使用分布式的协议都是可以的,比如 RPC 或者 HTTP Web 的方式。 0.2.3. 插件通信 插件间的通信。虽然设计的时候插件间是完全解耦的,但实际业务运行过程中,必然会出现某个业务流程需要多个插件协作,这就要求两个插件间进行通信。 由于插件之间没有直接联系,通信必须通过核心系统,因此核心系统需要提供插件通信机制。 这种情况和计算机类似,计算机的 CPU、硬盘、内存、网卡是独立设计的配件,但计算机运行过程中,CPU 和内存、内存和硬盘肯定是有通信的,计算机通过主板上的总线提供了这些组件之间的通信功能。 微内核的核心系统也必须提供类似的通信机制,各个插件之间才能进行正常的通信。 0.2.4. OSGi 架构简析 OSGi 的全称是 Open Services Gateway initiative,本身其实是指 OSGi Alliance。这个联盟是 Sun Microsystems、IBM、爱立信等公司于 1999 年 3 月成立的开放的标准化组织,最初名为 Connected Alliance。它是一个非盈利的国际组织,旨在建立一个开放的服务规范,为通过网络向设备提供服务建立开放的标准,这个标准就是 OSGi specification。 谈到 OSGi,如果没有特别说明,一般都是指 OSGi 的规范。 OSGi 联盟的初始目标是构建一个在广域网和局域网或设备上展开业务的基础平台,所以 OSGi 的最早设计也是针对嵌入式应用的,诸如机顶盒、服务网关、手机、汽车等都是其应用的主要环境。 由于 OSGi 具备动态化、热插拔、高可复用性、高效性、扩展方便等优点,它被应用到了 PC 上的应用开发。 尤其是 Eclipse 这个流行软件采用 OSGi 标准后,OSGi 更是成为了首选的插件化标准。 现在谈论 OSGi,已经和嵌入式应用关联不大了,更多是将 OSGi 当作一个微内核的架构模式。 Eclipse 从 3.0 版本开始,抛弃了原来自己实现的插件化框架,改用了 OSGi 框架。 注意:OSGi 是一个插件化的标准,而不是一个可运行的框架,Eclipse 采用的 OSGi 框架称为 Equinox,类似的实现还有 Apache 的 Felix、Spring 的 Spring DM。 OSGi 框架的逻辑架构图如下: 模块层(Module 层)实现插件管理功能。OSGi 中,插件被称为 Bundle,每个 Bundle 是一个 Java 的 JAR 文件,每个 Bundle 里面都包含一个元数据文件 MANIFEST.MF,这个文件包含了 Bundle 的基本信息。例如,Bundle 的名称、描述、开发商、classpath,以及需要导入的包和输出的包等,OSGi 核心系统会将这些信息加载到系统中用于后续使用。 生命周期层(Lifecycle 层)实现插件连接功能,提供了执行时模块管理、模块对底层 OSGi 框架的访问。生命周期层精确地定义了 Bundle 生命周期的操作(安装、更新、启动、停止、卸载),Bundle 必须按照规范实现各个操作。 服务层(Service 层)实现插件通信的功能。OSGi 提供了一个服务注册的功能,用于各个插件将自己能提供的服务注册到 OSGi 核心的服务注册中心,如果某个服务想用其他服务,则直接在服务注册中心搜索可用服务中心就可以。 注意:这里的服务注册不是插件管理功能中的插件注册,实际上是插件间通信的机制。

11 微服务架构 阅读更多

0.1. 微服务与SOA的关系 0.1.1. 对比微服务与SOA的实现 0.2. 微服务的陷阱 0.3. 最佳实践——方法 0.3.1. 服务粒度 0.3.2. 拆分方法 0.3.2.1. 基于业务逻辑拆分 0.3.2.2. 基于可扩展拆分 0.3.2.3. 基于可靠性拆分 0.3.2.4. 基于性能拆分 0.3.3. 基础设施 0.4. 最佳实践——基础设施 0.4.1. 自动化测试 0.4.2. 自动化部署 0.4.3. 配置中心 0.4.4. 接口框架 0.4.5. API网关 0.4.6. 服务发现 0.4.6.1. 自理式 0.4.6.2. 代理式 0.4.7. 服务路由 0.4.8. 服务容错 0.4.9. 服务监控 0.4.10. 服务跟踪 0.4.11. 服务安全 0.1. 微服务与SOA的关系 关于 SOA 和微服务的关系和区别,大概分为下面几个典型的观点。 微服务是SOA的实现方式:SOA 是一种架构理念,而微服务是 SOA 理念的一种具体实现方法: 微服务就是使用 HTTP RESTful 协议来实现 ESB 的 SOA 使用 SOA 来构建单个系统就是微服务 微服务就是更细粒度的 SOA 微服务是去掉ESB后的SOA:传统 SOA 架构最广为人诟病的就是庞大、复杂、低效的 ESB,因此将 ESB 去掉,改为轻量级的 HTTP 实现,就是微服务。 微服务是一种和 SOA 相似但本质上不同的架构理念:微服务和 SOA 只是类似,本质是不同的架构设计理念。 相似点在于两者都关注“服务”,通过服务的拆分来解决可扩展性问题。 本质上不同的地方在于几个核心理念的差异:是否有 ESB、服务的粒度、架构设计的目标等。 0.1.1. 对比微服务与SOA的实现 服务粒度:SOA 的服务粒度要粗一些,而微服务的服务粒度要细一些。对一个大型企业来说: “员工管理系统”就是一个 SOA 架构中的服务; 而采用微服务架构,则“员工管理系统”会被拆分为更多的服务,比如“员工信息管理”“员工考勤管理”“员工假期管理”和“员工福利管理”等更多服务。 服务通信: SOA 采用了 ESB 作为服务间通信的关键组件,负责服务定义、服务路由、消息转换、消息传递,总体上是重量级的实现。 微服务使用统一的协议和格式,例如,RESTful 协议、RPC 协议,无须 ESB 这样的重量级实现。 服务交付: SOA 对服务的交付并没有特殊要求,因为 SOA 更多考虑的是兼容已有的系统; 微服务的架构理念要求“快速交付”,相应地要求采取自动化测试、持续集成、自动化部署等敏捷开发相关的最佳实践。如果没有这些基础能力支撑,微服务规模一旦变大(例如,超过 20 个微服务),整体就难以达到快速交付的要求,这也是很多企业在实行微服务时踩过的一个明显的坑,就是系统拆分为微服务后,部署的成本呈指数上升。 应用场景: SOA 更加适合于庞大、复杂、异构的企业级系统,因为成本和影响太大,只能采用兼容的方式进行处理,而承担兼容任务的就是 ESB。 微服务更加适合于快速、轻量级、基于 Web 的互联网系统,这类系统业务变化快,需要快速尝试、快速交付;同时基本都是基于 Web,虽然开发技术可能差异很大(例如,Java、C++、.NET 等),但对外接口基本都是提供 HTTP RESTful 风格的接口,无须考虑在接口层进行类似 SOA 的 ESB 那样的处理。 SOA 和微服务本质上是两种不同的架构设计理念,只是在“服务”这个点上有交集而已,因此两者的关系应该是上面第三种观点。 small、lightweight、automated,基本上浓缩了微服务的精华,也是微服务与 SOA 的本质区别所在。SOA 和微服务是两种不同理念的架构模式,并不存在孰优孰劣,只是应用场景不同而已。 0.2. 微服务的陷阱 似乎微服务大大优于 SOA,导致在实践时不加思考地采用微服务: 既不考虑团队的规模, 也不考虑业务的发展, 也没有考虑基础技术的支撑, 只是觉得微服务很牛就赶紧来实施,以为实施了微服务后就什么问题都解决了,而一旦真正实施后才发现掉到微服务的坑里面去了。 微服务具体有哪些坑: 服务划分过细,服务间关系复杂:虽然单个服务的复杂度下降了,但整个系统的复杂度却上升了,微服务将系统内的复杂度转移为系统间的复杂度。从理论的角度来计算,n 个服务的复杂度是 n×(n-1)/2,整体系统的复杂度是随着微服务数量的增加呈指数级增加的。 服务数量太多,团队效率急剧下降:一个简单的需求开发就需要涉及多个微服务,相互之间的接口就有 6 ~ 7 个,无论是设计、开发、测试、部署,都需要不停地在不同的服务间切换。 调用链太长,性能下降:微服务之间都是通过 HTTP 或者 RPC 调用的,每次调用必须经过网络。一般线上的业务接口之间的调用,平均响应时间大约为 50 毫秒,如果一个请求需要经过 6 次微服务调用,则性能消耗就是 300 毫秒,这在很多高性能业务场景下是难以满足需求的。为了支撑业务请求,可能需要大幅增加硬件,这就导致了硬件成本的大幅上升。 调用链太长,问题定位困难:一次用户请求需要多个微服务协同处理,任意微服务的故障都将导致整个业务失败。微服务数量较多,且故障存在扩散现象,快速定位到底是哪个微服务故障是一件复杂的事情。 没有自动化支撑,无法快速交付:靠人工操作,微服务不但达不到快速交付的目的,甚至还不如一个大而全的系统效率高,需要自动化测试、部署、监控。 没有服务治理,微服务数量多了后管理混乱:随着微服务种类和数量越来越多,如果没有服务治理系统进行支撑,微服务提倡的 lightweight 就会变成问题。,如:服务路由、服务故障隔离、服务注册与发现。 0.3. 最佳实践——方法 0.3.1. 服务粒度 针对微服务拆分过细导致的问题,建议基于团队规模进行拆分,类似贝索斯在定义团队规模时提出的“两个披萨”理论(每个团队的人数不能多到两张披萨都不够吃的地步),或者微服务拆分粒度的“三个火枪手”原则,即一个微服务三个人负责开发。 在实施微服务架构时,根据团队规模来划分微服务数量,如果业务规继续发展,团队规模扩大,再将已有的微服务进行拆分。 从系统规模来讲,3 个人负责开发一个系统,系统的复杂度刚好达到每个人都能全面理解整个系统,又能够进行分工的粒度。 从团队管理来讲,3 个人可以形成一个稳定的备份,即使 1 个人休假或者调配到其他系统,剩余 2 个人还可以支撑。 从技术提升的角度来讲,3 个人的技术小组既能够形成有效的讨论,又能够快速达成一致意见。 “三个火枪手”的原则主要应用于微服务设计和开发阶段,如果微服务经过一段时间发展后已经比较稳定,处于维护期了,无须太多的开发,那么平均 1 个人维护 1 个微服务甚至几个微服务都可以。当然考虑到人员备份问题,每个微服务最好都安排 2 个人维护,每个人都可以维护多个微服务。 0.3.2. 拆分方法 基于“三个火枪手”的理论,可以计算出拆分后合适的服务数量,但具体怎么拆也是有技巧的,不是只能按照业务来进行拆分,而是可以根据目的的不同灵活地选取不同的拆分方式。 0.3.2.1. 基于业务逻辑拆分 这是最常见的一种拆分方式,将系统中的业务模块按照职责范围识别出来,每个单独的业务模块拆分为一个独立的服务。 基于业务逻辑拆分虽然看起来很直观,但在实践过程中最常见的一个问题就是团队成员对于“职责范围”的理解差异很大,经常会出现争论,难以达成一致意见。 从业务的角度来拆分的话,规模粗和规模细都没有问题,因为拆分基础都是业务逻辑,要判断拆分粒度,不能从业务逻辑角度,而要根据“三个火枪手”的原则,计算一下大概的服务数量范围,然后再确定合适的“职责范围”,否则就可能出现划分过粗或者过细的情况,而且大部分情况下会出现过细的情况。 例如: 如果团队规模是 10 个人支撑业务,按照“三个火枪手”规则计算,大约需要划分为 4 个服务,那么“登录、注册、用户信息管理”都可以划到“用户服务”职责范围内; 如果团队规模是 100 人支撑业务,服务数量可以达到 40 个,那么“用户登录“就是一个服务了; 如果团队规模达到 1000 人支撑业务,那“用户连接管理”可能就是一个独立的服务了。 0.3.2.2. 基于可扩展拆分 将系统中的业务模块按照稳定性排序,将已经成熟和改动不大的服务拆分为稳定服务,将经常变化和迭代的服务拆分为变动服务。 稳定的服务粒度可以粗一些,即使逻辑上没有强关联的服务,也可以放在同一个子系统中; 不稳定的服务粒度可以细一些,但也不要太细,始终记住要控制服务的总数量。 这样拆分主要是为了提升项目快速迭代的效率,避免在开发的时候,不小心影响了已有的成熟功能导致线上问题。 0.3.2.3. 基于可靠性拆分 将系统中的业务模块按照优先级排序,将可靠性要求高的核心服务和可靠性要求低的非核心服务拆分开来,然后重点保证核心服务的高可用。 具体拆分的时候,核心服务可以是一个也可以是多个,只要最终的服务数量满足“三个火枪手”的原则就可以。 这样拆分带来下面几个好处: 避免非核心服务故障影响核心服务 核心服务高可用方案可以更简单:核心服务的功能逻辑更加简单,存储的数据可能更少,用到的组件也会更少,设计高可用方案大部分情况下要比不拆分简单很多。 能够降低高可用成本:将核心服务拆分出来后,核心服务占用的机器、带宽等资源比不拆分要少很多。因此,只针对核心服务做高可用方案,机器、带宽等成本比不拆分要节省较多。 0.3.2.4. 基于性能拆分 基于性能拆分和基于可靠性拆分类似,将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其他服务。 常见的拆分方式和具体的性能瓶颈有关,可以拆分 Web 服务、数据库、缓存等。例如电商的抢购,性能压力最大的是入口的排队功能,可以将排队功能独立为一个服务。 以上几种拆分方式不是多选一,而是可以根据实际情况自由排列组合,例如可以基于可靠性拆分出服务 A,基于性能拆分出服务 B,基于可扩展拆分出 C/D/F 三个服务,加上原有的服务 X,最后总共拆分出 6 个服务(A/B/C/D/F/X)。 0.3.3. 基础设施 大部分人主要关注的是微服务的“small”和“lightweight”特性,但实际上真正决定微服务成败的,恰恰是那个被大部分人都忽略的“automated”。 因为服务粒度即使划分不合理,实际落地后如果团队遇到麻烦,自然会想到拆服务或者合服务;如果“ mated”相关的基础设施不健全,那微服务就是焦油坑,让研发、测试、运维陷入各种微服务陷阱中。 微服务基础设施如下图所示: 微服务并不是很多人认为的那样又简单又轻量级。要做好微服务,这些基础设施都是必不可少的,否则微服务就会变成一个焦油坑,让业务和团队在里面不断挣扎且无法自拔。 微服务并没有减少复杂度,而只是将复杂度从 ESB 转移到了基础设施。 虽然建设完善的微服务基础设施是一项庞大的工程,但也不用太过灰心。 第一个原因是:已经有开源的微服务基础设施全家桶了,例如大名鼎鼎的 Spring Cloud 项目,涵盖了服务发现、服务路由、网关、配置中心等功能; 第二个原因是:如果微服务的数量并不是很多的话,并不是每个基础设施都是必须的。通常情况下,建议按照下面优先级来搭建基础设施: 服务发现、服务路由、服务容错:这是最基本的微服务基础设施。 接口框架、API 网关:主要是为了提升开发效率,接口框架是提升内部服务的开发效率,API 网关是为了提升与外部服务对接的效率。 自动化部署、自动化测试、配置中心:主要是为了提升测试和运维效率。 服务监控、服务跟踪、服务安全:主要是为了进一步提升运维效率。 以上 3 和 4 两类基础设施,其重要性会随着微服务节点数量增加而越来越重要,但在微服务节点数量较少的时候,可以通过人工的方式支撑,虽然效率不高,但也基本能够顶住。 0.4. 最佳实践——基础设施 每项微服务基础设施都是一个平台、一个系统、一个解决方案,如果要自己实现,其过程和做业务系统类似,都需要经过需求分析、架构设计、开发、测试、部署上线等步骤。 0.4.1. 自动化测试 微服务将原本大一统的系统拆分为多个独立运行的“微”服务,微服务之间的接口数量大大增加,并且微服务提倡快速交付,版本周期短,版本更新频繁。如果每次更新都靠人工回归整个系统,则工作量大,效率低下,达不到“快速交付”的目的,因此必须通过自动化测试系统来完成绝大部分测试回归的工作。 自动化测试涵盖的范围包括代码级的单元测试、单个系统级的集成测试、系统间的接口测试,理想情况是每类测试都自动化。如果因为团队规模和人力的原因无法全面覆盖,至少要做到接口测试自动化。 0.4.2. 自动化部署 相比大一统的系统,微服务需要部署的节点增加了几倍甚至十几倍,微服务部署的频率也会大幅提升,综合计算下来,微服务部署的次数是大一统系统部署次数的几十倍。 这么大量的部署操作,如果继续采用人工手工处理,需要投入大量的人力,且容易出错,因此需要自动化部署的系统来完成部署操作。自动化部署系统包括版本管理、资源管理(例如,机器管理、虚拟机管理)、部署操作、回退操作等功能。 0.4.3. 配置中心 微服务的节点数量非常多,通过人工登录每台机器手工修改,效率低,容易出错。特别是在部署或者排障时,需要快速增删改查配置,人工操作的方式显然是不行的。 除此以外,有的运行期配置需要动态修改并且所有节点即时生效,人工操作是无法做到的。综合上面的分析,微服务需要一个统一的配置中心来管理所有微服务节点的配置。 配置中心包括配置版本管理(例如,同样的微服务,有 10 个节点是给移动用户服务的,有 20 个节点给联通用户服务的,配置项都一样,配置值不一样)、增删改查配置、节点管理、配置同步、配置推送等功能。 0.4.4. 接口框架 微服务提倡轻量级的通信方式,一般采用 HTTP/REST 或者 RPC 方式统一接口协议。但在实践过程中: 统一接口协议, 统一接口传递的数据格式。 因此需要统一接口框架,它不是一个可运行的系统,一般以库或者包的形式提供给所有微服务调用。 0.4.5. API网关 系统拆分为微服务后,内部的微服务之间是互联互通的,相互之间的访问都是点对点的。如果外部系统想调用系统的某个功能,也采取点对点的方式,则外部系统会非常“头大”。因为在外部系统看来,它不需要也没办法理解这么多微服务的职责分工和边界,它只会关注它需要的能力,而不会关注这个能力应该由哪个微服务提供。 除此以外,外部系统访问系统还涉及安全和权限相关的限制,如果外部系统直接访问某个微服务,则意味着每个微服务都要自己实现安全和权限的功能,这样做不但工作量大,而且都是重复工作。 综合上面的分析,微服务需要一个统一的 API 网关,负责外部系统的访问操作。 API 网关是外部系统访问的接口,所有的外部系统接⼊系统都需要通过 API 网关,主要包括接入鉴权(是否允许接入)、权限控制(可以访问哪些功能)、传输加密、请求路由、流量控制等功能。 0.4.6. 服务发现 微服务种类和数量很多,如果这些信息全部通过手工配置的方式写入各个微服务节点, 首先配置工作量很大,配置文件可能要配几百上千行,几十个节点加起来后配置项就是几万几十万行了,人工维护这么大数量的配置项是一项灾难; 其次是微服务节点经常变化,可能是由于扩容导致节点增加,也可能是故障处理时隔离掉一部分节点,还可能是采用灰度升级,先将一部分节点升级到新版本,然后让新老版本同时运行。不管哪种情况,我们都希望节点的变化能够及时同步到所有其他依赖的微服务。如果采用手工配置,是不可能做到实时更改生效的。 因此,需要一套服务发现的系统来支撑微服务的自动注册和发现。服务发现主要有两种实现方式:自理式和代理式。 0.4.6.1. 自理式 自理式结构就是指每个微服务自己完成服务发现。 自理式服务发现实现比较简单,因为这部分的功能一般通过统一的程序库或者程序包提供给各个微服务调用,而不会每个微服务都自己来重复实现一遍;并且由于每个微服务都承担了服务发现的功能,访问压力分散到了各个微服务节点,性能和可用性上不存在明显的压力和风险。 0.4.6.2. 代理式 代理式结构就是指微服务之间有一个负载均衡系统,由负载均衡系统来完成微服务之间的服务发现。 代理式的方式看起来更加清晰,微服务本身的实现也简单了很多,但实际上这个方案风险较大。 第一个风险是可用性风险,一旦 LOAD BALANCER 系统故障,就会影响所有微服务之间的调用; 第二个风险是性能风险,所有的微服务之间的调用流量都要经过 LOAD BALANCER 系统,性能压力会随着微服务数量和流量增加而不断增加,最后成为性能瓶颈。 因此 LOAD BALANCER 系统需要设计成集群的模式,但 LOAD BALANCER 集群的实现本身又增加了复杂性。 不管是自理式还是代理式,服务发现的核心功能就是服务注册表,注册表记录了所有的服务节点的配置和状态,每个微服务启动后都需要将自己的信息注册到服务注册表,然后由微服务或者 LOAD BALANCER 系统到服务注册表查询可用服务。 0.4.7. 服务路由 有了服务发现后,微服务之间能够方便地获取相关配置信息,但具体进行某次调用请求时,还需要从所有符合条件的可用微服务节点中挑选出一个具体的节点发起请求,这就是服务路由需要完成的功能。 服务路由和服务发现紧密相关,服务路由一般不会设计成一个独立运行的系统,通常情况下是和服务发现放在一起实现的。 对于自理式服务发现,服务路由是微服务内部实现的; 对于代理式服务发现,服务路由是由 LOAD BALANCER 系统实现的。 无论放在哪里实现,服务路由核心的功能就是路由算法。常见的路由算法有: 随机路由、 轮询路由、 最小压力路由、 最小连接数路由等。 0.4.8. 服务容错 系统拆分为微服务后,单个微服务故障的概率变小,故障影响范围也减少,但是微服务的节点数量大大增加。从整体上来看,系统中某个微服务出故障的概率会大大增加。 微服务具有故障扩散的特点,如果不及时处理故障,故障扩散开来就会导致看起来系统中很多服务节点都故障了,因此需要微服务能够自动应对这种出错场景,及时进行处理。否则,如果节点一故障就需要人工处理,投入人力大,处理速度慢;而一旦处理速度慢,则故障就很快扩散,所以需要服务容错的能力。 常见的服务容错包括请求重试、流控和服务隔离。通常情况下,服务容错会集成在服务发现和服务路由系统中。 0.4.9. 服务监控 系统拆分为微服务后,节点数量大大增加,导致需要监控的机器、网络、进程、接口调用数等监控对象的数量大大增加;同时,一旦发生故障,需要快速根据各类信息来定位故障。这两个目标如果靠人力去完成是不现实的,因此需要服务监控系统来完成微服务节点的监控。 服务监控的主要作用有:实时搜集信息并进行分析,避免故障后再来分析,减少了处理时间。 服务监控可以在实时分析的基础上进行预警,在问题萌芽的阶段发觉并预警,降低了问题影响的范围和时间。 通常情况下,服务监控需要搜集并分析大量的数据,因此建议做成独立的系统,而不要集成到服务发现、API 网关等系统中。 0.4.10. 服务跟踪 服务监控可以做到微服务节点级的监控和信息收集,但如果需要跟踪某一个请求在微服务中的完整路径,服务监控是难以实现的。因为如果每个服务的完整请求链信息都实时发送给服务监控系统,数据量会大到无法处理。 服务监控和服务跟踪的区别可以简单概括为宏观和微观的区别。 例如,A 服务通过 HTTP 协议请求 B 服务 10 次,B 通过 HTTP 返回 JSON 对象: 服务监控会记录请求次数、响应时间平均值、响应时间最高值、错误码分布这些信息; 服务跟踪会记录其中某次请求的发起时间、响应时间、响应错误码、请求参数、返回的 JSON 对象等信息。 目前无论是分布式跟踪还是微服务的服务跟踪,绝大部分请求跟踪的实现技术都基于 Google 的 Dapper 论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》。 0.4.11. 服务安全 系统拆分为微服务后,数据分散在各个微服务节点上。 从系统连接的角度来说,任意微服务都可以访问所有其他微服务节点; 从业务的角度来说,部分敏感数据或者操作,只能部分微服务可以访问,而不是所有的微服务都可以访问,因此需要设计服务安全机制来保证业务和数据的安全性。 服务安全主要分为三部分:接入安全、数据安全、传输安全。 通常情况下,服务安全可以集成到配置中心系统中进行实现,即配置中心配置微服务的接入安全策略和数据安全策略,微服务节点从配置中心获取这些配置信息,然后在处理具体的微服务调用请求时根据安全策略进行处理。由于这些策略是通用的,一般会把策略封装成通用的库提供给各个微服务调用。

10 可扩展架构 阅读更多

0.1. 可扩展的基本思想 0.2. 可扩展方式 0.3. 分层架构 0.4. SOA 软件系统与硬件和建筑系统最大的差异在于软件是可扩展的。架构可扩展模式主要包括分层架构、SOA 架构、微服务和微内核等。 0.1. 可扩展的基本思想 所有的可扩展性架构设计,基本思想都可以总结为一个字:拆!将原本大一统的系统拆分成多个规模小的部分,扩展时只修改其中一部分即可,无须整个系统到处都改,通过这种方式来减少改动范围,降低改动风险。 按照不同的思路来拆分软件系统,就会得到不同的架构。常见的拆分思路有如下三种: 面向流程拆分:将整个业务流程拆分为多个阶段,每个阶段作为一部分。 面向服务拆分:将系统提供的服务拆分,每个服务作为一部分。 面向功能拆分:将系统提供的功能拆分,每个功能作为一部分。 理解这三种思路的关键就在于如何理解“流程”“服务”“功能”三者的联系和区别。 从范围上来看,从大到小依次为:流程 > 服务 > 功能,。以 TCP/IP 协议栈为例,来说明“流程”“服务”“功能”的区别和联系,TCP/IP 协议栈和模型图如下图所示。 流程:对应 TCP/IP 四层模型,因为 TCP/IP 网络通信流程是:应用层 → 传输层 → 网络层 → 物理 + 数据链路层。 服务:对应应用层的 HTTP、FTP、SMTP 等服务,例如: HTTP 提供 Web 服务, FTP 提供文件服务, SMTP 提供邮件服务,以此类推。 功能:每个服务都会提供相应的功能。例如: HTTP 服务提供 GET、POST 功能, FTP 提供上传下载功能, SMTP 提供邮件发送和收取功能。 以学生信息管理系统为例,拆分方式是: 面向流程拆分 展示层 → 业务层 → 数据层 → 存储层,各层含义是: 展示层:负责用户页面设计,不同业务有不同的页面。例如,登录页面、注册页面、信息管理页面、安全设置页面等。 业务层:负责具体业务逻辑的处理。例如,登录、注册、信息管理、修改密码等业务。 数据层:负责完成数据访问。例如,增删改查数据库中的数据、记录事件到日志文件等。 存储层:负责数据的存储。例如,关系型数据库 MySQL、缓存系统 Memcache 等。 最终的架构如下: 面向服务拆分 将系统拆分为注册、登录、信息管理、安全设置等服务,最终架构示意图如下: 面向功能拆分 每个服务都可以拆分为更多细粒度的功能,例如: 注册服务:提供多种方式进行注册,包括手机号注册、身份证注册、学生邮箱注册三个功能。 登录服务:包括手机号登录、身份证登录、邮箱登录三个功能。 信息管理服务:包括基本信息管理、课程信息管理、成绩信息管理等功能。 安全设置服务:包括修改密码、安全手机、找回密码等功能。 最终架构图如下: 通过学生信息管理系统的案例可以发现,不同的拆分方式,架构图差异很大。但无论哪种方式,最终都是可以实现的。不同的拆分方式,本质上决定了系统的扩展方式。 0.2. 可扩展方式 合理的拆分,能够强制保证即使程序员出错,出错的范围也不会太广,影响也不会太大。下面是不同拆分方式应对扩展时的优势。 面向流程拆分:扩展时大部分情况只需要修改某一层,少部分情况可能修改关联的两层,不会出现所有层都同时要修改。 面向服务拆分:对某个服务扩展,或者要增加新的服务时,只需要扩展相关服务即可,无须修改所有的服务。 面向功能拆分:对某个功能扩展,或者要增加新的功能时,只需要扩展相关功能即可,无须修改所有的服务。 不同的拆分方式,将得到不同的系统架构,典型的可扩展系统架构有: 面向流程拆分:分层架构。 面向服务拆分:SOA、微服务。 面向功能拆分:微内核架构。 这些系统架构并不是非此即彼的,而是可以在系统架构设计中进行组合使用的。 以学生管理系统为例,最终可以这样设计架构: 整体系统采用面向服务拆分中的“微服务”架构,拆分为“注册服务”“登录服务”“信息管理服务”“安全服务”,每个服务是一个独立运行的子系统。 其中的“注册服务”子系统本身又是采用面向流程拆分的分层架构。 “登录服务”子系统采用的是面向功能拆分的“微内核”架构。 0.3. 分层架构 分层架构(也称为 N 层架构,至少2层)是很常见的架构模式,通常情况下: 2 层架构:例如,C/S 架构、B/S 架构 3 层架构:例如,MVC、MVP 架构 一般是比较复杂的系统才会达到或者超过 5 层,比如操作系统内核架构 按照分层架构进行设计时,根据不同的划分维度和对象,可以得到多种不同的分层架构。 C/S 架构、B/S 架构:划分的对象是整个业务系统,划分的维度是用户交互,即将和用户交互的部分独立为一层,支撑用户交互的后台作为另外一层。 MVC 架构、MVP 架构:划分的对象是单个业务子系统,划分的维度是职责,将不同的职责划分到独立层,但各层的依赖关系比较灵活。 逻辑分层架构:划分的对象可以是单个业务子系统,也可以是整个业务系统,划分的维度也是职责。虽然都是基于职责划分,但逻辑分层架构和 MVC 架构、MVP 架构的不同点在于,逻辑分层架构中的层是自顶向下依赖的。典型的有操作系统内核架构、TCP/IP 架构。 无论采取何种分层维度,分层架构设计最核心的是需要保证各层之间的差异足够清晰,边界足够明显,让人看到架构图后就能看懂整个架构,这也是分层不能分太多层的原因。 分层架构能够较好地支撑系统扩展,本质在于隔离关注点(separation of concerns),即每个层中的组件只会处理本层的逻辑。 展示层只需要处理展示逻辑, 业务层只需要处理业务逻辑, 这样在扩展某层时,其他层是不受影响的,通过这种方式可以支撑系统在某层上快速扩展。并不是简单地分层就一定能够实现隔离关注点从而支撑快速扩展,分层时要保证层与层之间的依赖是稳定的,才能真正支撑快速扩展。 分层结构的另外一个特点就是层层传递,一旦分层确定,整个业务流程是按照层进行依次传递的,不能在层之间进行跳跃。最简单的 C/S 结构,用户必须先使用 C 层,然后 C 层再传递到 S 层,用户是不能直接访问 S 层的。 分层结构的这种约束,好处在于强制将分层依赖限定为两两依赖,降低了整体系统复杂度。 分层结构的代价就是冗余,不管这个业务有多么简单,每层都必须要参与处理,甚至可能每层都写了一个简单的包装函数。 分层架构另外一个典型的缺点就是性能,因为每一次业务请求都需要穿越所有的架构分层,有一些事情是多余的,多少都会有一些性能的浪费。这里的性能缺点只是理论的,现在的硬件和网络的性能,这些理论的损失已经可以忽略不计。 0.4. SOA SOA 的全称是 Service Oriented Architecture,翻译为“面向服务的架构”。 SOA 出现的背景是企业内部的 IT 系统重复建设且效率低下,主要体现在: 企业各部门有独立的 IT 系统,比如人力资源系统、财务系统、销售系统,这些系统可能都涉及人员管理,各 IT 系统都需要重复开发人员管理的功能。 各个独立的 IT 系统可能采购于不同的供应商,实现技术不同,企业自己也不太可能基于这些系统进行重构。 随着业务的发展,复杂度越来越高,更多的流程和业务需要多个 IT 系统合作完成。由于各个独立的 IT 系统没有标准的实现方式(例如,人力资源系统用 Java 开发,对外提供 RPC;而财务系统用 C# 开发,对外提供 SOAP 协议),每次开发新的流程和业务,都需要协调大量的 IT 系统,同时定制开发,效率很低。 为了应对传统 IT 系统存在的问题,SOA 提出了 3 个关键概念。 服务 所有业务功能都是一项服务,服务就意味着要对外提供开放的能力,当其他系统需要使用这项功能时,无须定制化开发。服务可大可小,可简单也可复杂。服务的粒度根据企业实际情况进行判断。 ESB 全称是 Enterprise Service Bus,翻译为“企业服务总线”。ESB 参考了计算机总线的概念。计算机中的总线将各个不同的设备连接在一起,ESB 将企业中各个不同的服务连接在一起。 因为各个独立的服务是异构的,如果没有统一的标准,则各个异构系统对外提供的接口是各式各样的。SOA 使用 ESB 来屏蔽异构系统对外提供各种不同的接口方式,以此来达到服务间高效的互联互通。 松耦合 目的是减少各个服务间的依赖和互相影响。因为采用 SOA 架构后,各个服务是相互独立运行的,甚至都不清楚某个服务到底有多少对其他服务的依赖。 如果做不到松耦合,某个服务一升级,依赖它的其他服务全部故障,这样肯定是无法满足业务需求的。但实际上真正做到松耦合并没有那么容易,要做到完全后向兼容,是一项复杂的任务。 典型的 SOA 架构样例如下: SOA 架构是比较高层级的架构设计理念,一般情况下可以说某个企业采用了 SOA 的架构来构建 IT 系统,但不会说某个独立的系统采用了 SOA 架构。 SOA 解决了传统 IT 系统重复建设和扩展效率低的问题,但其本身也引入了更多的复杂性。 SOA 最广为人诟病的就是 ESB,ESB 需要实现与各种系统间的协议转换、数据转换、透明的动态路由等功能。 ESB 虽然功能强大,但现实中的协议有很多种,如 JMS、WS、HTTP、RPC 等,数据格式也有很多种,如 XML、JSON、二进制、HTML 等。 ESB 要完成这么多协议和数据格式的互相转换,工作量和复杂度都很大,而且这种转换是需要耗费大量计算性能的,当 ESB 承载的消息太多时,ESB 本身会成为整个系统的性能瓶颈。 SOA 的 ESB 设计也是无奈之举。SOA 的提出背景是各种异构的 IT 系统都已经存在很多年了,完全重写或者按照统一标准进行改造的成本是非常大的,只能通过 ESB 方式去适配已经存在的各种异构系统。

09 异地多活架构 阅读更多

0.1. 应用场景 0.2. 架构模式 0.2.1. 同城异地 0.2.2. 跨城异地 0.2.3. 跨国异地 0.3. 跨城异地设计技巧 0.3.1. 技巧 1:保证核心业务的异地多活 0.3.2. 技巧 2:保证核心数据最终一致性 0.3.3. 技巧 3:采用多种手段同步数据 0.3.4. 技巧 4:只保证绝大部分用户的异地多活 0.3.5. 核心思想 0.4. 跨城异地设计步骤 0.4.1. 业务分级 0.4.2. 数据分类 0.4.3. 数据同步 0.4.4. 异常处理 0.5. 应对接口级故障 0.5.1. 降级 0.5.2. 熔断 0.5.3. 限流 0.5.3.1. 基于请求限流 0.5.3.2. 基于资源限流 0.5.4. 排队 高可用计算架构和高可用存储架构的设计目的都是为了解决部分服务器故障时,如何保证系统能够继续提供服务。 在一些极端场景下,可能所有服务器都故障(例如,机房断电、机房火灾、地震、水灾等)导致业务整体瘫痪,即使有其他地区的备份,把备份业务系统全部恢复到能够正常提供业务,花费的时间也比较长。因为备份系统平时不对外提供服务,可能会存在很多隐藏的问题没有发现。如果期望达到即使在此类灾难性故障时,业务也不受影响,或者能够很快恢复,就需要设计异地多活架构。 0.1. 应用场景 判断一个系统是否符合异地多活,需要满足两个标准: 正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务。 某个地方业务异常的时候,用户访问其他地方正常的业务系统,能够得到正确的业务服务。 实现异地多活架构代价很高,具体表现为: 系统复杂度会发生质的变化,需要设计复杂的异地多活架构。 成本会上升,毕竟要多在一个或者多个机房搭建独立的一套业务系统。 常见的新闻网站、企业内部的 IT 系统、游戏、博客站点等,可以不做异地多活的;因为这类业务系统即使中断,对用户的影响并不会很大。 共享单车、滴滴出行、支付宝、微信这类业务,就需要做异地多活;这类业务系统中断后,对用户的影响很大。 如果业务规模很大,尽量做异地多活: 首先,能够在异常的场景下给用户提供更好的体验; 其次,业务规模很大肯定会伴随衍生的收入(例如,广告收入),异地多活能够减少异常场景带来的收入损失。 0.2. 架构模式 根据地理位置上的距离来划分,异地多活架构可以分为同城异区、跨城异地、跨国异地。 0.2.1. 同城异地 将业务部署在同一个城市不同区的多个机房,然后将两个机房用专用的高速网络连接在一起。如果考虑一些极端场景(例如,停电、水灾),同城异地似乎没什么作用。 但同城的两个机房,距离上一般大约是几十千米,通过搭建高速的网络,两个机房能够实现和同一个机房内几乎一样的网络传输速度。这意味着虽然是两个不同地理位置上的机房,但逻辑上可以看作同一个机房,这样的设计大大降低了复杂度,减少了异地多活的设计和实现复杂度及成本。 极端灾难发生概率是比较低的,但机房火灾、机房停电、机房空调故障这类问题发生的概率更高,而且破坏力一样很大。而这些故障场景,同城异地架构都可以很好地解决。 因此,结合复杂度、成本、故障发生概率来综合考虑,同城异地是应对机房级别故障的最优架构。 0.2.2. 跨城异地 将业务部署在不同城市的多个机房,而且距离最好要远一些,这样才能有效应对某一地区的极端灾难事件。但“距离较远”这点并不只是一个距离数字上的变化,而是量变引起了质变,导致了跨城异地的架构复杂度大大上升。 距离增加带来的最主要问题是两个机房的网络传输速度会降低,这是物理定律的限制: 光速真空传播大约是每秒 30 万千米, 光纤中传输的速度大约是每秒 20 万千米, 传输中的各种网络设备的处理,实际还远远达不到理论上的速度。 除了距离上的限制,中间传输各种不可控的因素也非常多。 例如,挖掘机把光纤挖断、中美海底电缆被拖船扯断、骨干网故障等,这些线路很多是第三方维护,针对故障我们根本无能为力也无法预知。 例如,广州机房到北京机房,正常情况下 RTT 大约是 50 毫秒左右,遇到网络波动之类的情况,RTT 可能飙升到 500 毫秒甚至 1 秒,更不用说经常发生的线路丢包问题,那延迟可能就是几秒几十秒了。 以上描述的问题,虽然同城异地理论上也会遇到,但由于距离较短,中间经过的线路和设备较少,问题发生的概率会低很多。 而且同城异地距离短,即使是搭建多条互联通道,成本也不会太高,而跨城异地距离太远,搭建或者使用多通道的成本会高不少。 跨城异地距离较远带来的网络传输延迟问题,给异地多活架构设计带来了复杂性,如果要做到真正意义上的多活,业务系统需要考虑部署在不同地点的两个机房,在数据短时间不一致的情况下,还能够正常提供业务。 这就引入了一个看似矛盾的地方:数据不一致业务肯定不会正常,但跨城异地肯定会导致数据不一致。 如何解决这个问题呢?重点还是在“数据”上,即根据数据的特性来做不同的架构。 如果数据要求强一致性,例如银行存款余额、支付宝余额等,这类数据是无法做到跨城异地多活,只能采用同城异地架构。 如果数据一致性要求不高,或者数据不怎么改变,或者即使数据丢失影响也不大的业务,跨城异地多活就能够派上用场了。例如: 用户登录(数据不一致时用户重新登录即可)、 新闻类网站(一天内的新闻数据变化较少)、 微博类网站(丢失用户发布的微博或者评论影响不大),这些业务采用跨城异地多活,能够很好地应对极端灾难的场景。 0.2.3. 跨国异地 将业务部署在不同国家的多个机房。相比跨城异地,跨国异地的距离更远,数据同步延迟更长,正常情况下可能就有几秒钟。这种程度的延迟已经无法满足异地多活标准的第一条:“正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务”。 虽然跨城异地也会有此类同步延迟问题,但几十毫秒的延迟对用户来说基本无感知的;而延迟达到几秒钟就感觉比较明显了。 跨国异地多活的主要应用场景一般有这几种情况: 为不同地区用户提供服务:例如,亚马逊中国是为中国用户服务的,而亚马逊美国是为美国用户服务的,亚马逊中国的用户如果访问美国亚马逊,是无法用亚马逊中国的账号登录美国亚马逊的。 只读类业务做多活:例如,谷歌的搜索业务,由于用户搜索资料时,这些资料都已经存在于谷歌的搜索引擎上面,无论是访问英国谷歌,还是访问美国谷歌,搜索结果基本相同,并且对用户来说,也不需要搜索到最新的实时资料,跨国异地的几秒钟网络延迟,对搜索结果是没有什么影响的。 0.3. 跨城异地设计技巧 同城异地:关键在于搭建高速网络将两个机房连接起来,达到近似一个本地机房的效果。架构设计上可以将两个机房当作本地机房来设计,无须额外考虑。 跨城异地:关键在于数据不一致的情况下,业务不受影响或者影响很小,从逻辑的角度上来说其实是矛盾的,架构设计的主要目的就是为了解决这个矛盾。 跨国异地:主要是面向不同地区用户提供业务,或者提供只读业务,对架构设计要求不高。 因此,跨城异地多活是架构设计复杂度最高的一种,下面是跨城异地多活架构设计的一些技巧和步骤。 0.3.1. 技巧 1:保证核心业务的异地多活 “异地多活”是为了保证业务的高可用,但并不是要保证所有业务都能“异地多活”! 为了架构设计去改变业务规则(特别是核心的业务规则)是得不偿失的。优先实现核心业务的异地多活架构! 0.3.2. 技巧 2:保证核心数据最终一致性 异地多活本质上是通过异地的数据冗余,来保证在极端异常的情况下业务也能够正常提供给用户,因此数据同步是异地多活架构设计的核心。但并不是要所有数据都实时同步! 异地多活架构面临一个无法彻底解决的矛盾:业务上要求数据快速同步,物理上正好做不到数据快速同步,因此所有数据都实时同步,实际上是一个无法达到的目标。 既然是无法彻底解决的矛盾,那就只能想办法尽量减少影响。有几种方法可以参考: 尽量减少异地多活机房的距离,搭建高速网络 尽量减少数据同步,只同步核心业务相关的数据(不重要的数据不同步,同步后没用的数据不同步,只同步核心业务相关的数据) 保证最终一致性,不保证实时一致性;最终一致性在具体实现时,根据不同的数据特征,进行差异化的处理,以满足业务需要。 0.3.3. 技巧 3:采用多种手段同步数据 数据同步是异地多活架构设计的核心,幸运的是基本上存储系统本身都会有同步的功能。虽然绝大部分场景下,存储系统本身的同步功能基本上也够用了,但在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求。 尤其是异地多机房这种部署,各种各样的异常情况都可能出现,只考虑存储系统本身的同步功能时,就会发现无法做到真正的异地多活。 解决的方案是将多种手段配合存储系统的同步来使用,甚至可以不采用存储系统的同步方案,改用自己的同步方案: 消息队列方式 二次读取方式:所谓的二次读取,第一次读取本地,本地失败后第二次读取对端,这样就能够解决异常情况下同步延迟的问题 回源读取方式:对于登录的 session 数据,由于数据量很大,可以不同步数据;但当用户在 A 中心登录后又在 B 中心登录,B 中心拿到用户上传的 session id 后,根据路由判断 session 属于 A 中心,直接去 A 中心请求 session 数据即可,反之亦然。 重新生成数据方式:对于“回源读取”场景,如果异常情况下,A 中心宕机了,B 中心请求 session 数据失败,此时就只能登录失败,让用户重新在 B 中心登录,生成新的 session 数据。 0.3.4. 技巧 4:只保证绝大部分用户的异地多活 某些场景下无法保证 100% 的业务可用性,总是会有一定的损失。因此,异地多活也无法保证 100% 的业务可用,例如: 密码不同步导致无法登录 用户信息不同步导致用户看到旧的信息等 这是由物理规律决定的,光速和网络的传播速度、硬盘的读写速度、极端异常情况的不可控等,都是无法 100% 解决的。 所以要忍受这一小部分用户或者业务上的损失,否则本来想为了保证最后的 0.01% 的用户的可用性,做一个完美方案,结果却发现 99.99% 的用户都保证不了了。 对于某些实时强一致性的业务,实际上受影响的用户会更多,甚至可能达到 1/3 的用户。 以银行转账这个业务为例,假设小明在北京 XX 银行开了账号,如果小明要转账,一定要北京的银行业务中心才可用,否则就不允许小明自己转账。 针对银行转账这个业务,虽然无法做到“实时转账”的异地多活,但可以通过特殊的业务手段让转账业务也能实现异地多活。例如,转账业务除了“实时转账”外,还提供“转账申请”业务,即小明在上海业务中心提交转账请求,但上海的业务中心并不立即转账,而是记录这个转账请求,然后后台异步发起真正的转账操作,如果此时北京业务中心不可用,转账请求就可以继续等待重试;假设等待 2 个小时后北京业务中心恢复了,此时上海业务中心去请求转账,发现余额不够,这个转账请求就失败了。 “转账申请”的这种方式虽然有助于实现异地多活,但其实还是牺牲了用户体验的,本来一次操作的事情,分为两次:一次提交转账申请,另外一次是要确认是否转账成功。 虽然无法做到 100% 可用性,为了让用户心里更好受一些,可以采取一些措施进行安抚或者补偿,例如: 挂公告:说明现在有问题和基本的问题原因,如果不明确原因或者不方便说出原因,可以发布“技术哥哥正在紧急处理”这类比较轻松和有趣的公告。 事后对用户进行补偿:送一些业务上可用的代金券、小礼包等,减少用户的抱怨。 补充体验:对于为了做异地多活而带来的体验损失,可以想一些方法减少或者规避。以“转账申请”为例,为了让用户不用确认转账申请是否成功,可以在转账成功或者失败后直接给用户发个短信,告诉他转账结果,这样用户就不用时不时地登录系统来确认转账是否成功了。 0.3.5. 核心思想 采用多种手段,保证绝大部分用户的核心业务异地多活! 0.4. 跨城异地设计步骤 0.4.1. 业务分级 按照一定的标准将业务进行分级,挑选出核心的业务,只为核心业务设计异地多活,降低方案整体复杂度和实现成本。 常见的分级标准有下面几种: 访问量大的业务:以用户管理系统为例,业务包括登录、注册、用户信息管理,其中登录的访问量肯定是最大的。 核心业务:以 QQ 为例,QQ 的主场景是聊天,QQ 空间虽然也是重要业务,但和聊天相比,重要性就会低一些。 产生大量收入的业务:以 QQ 为例,聊天可能很难为腾讯带来收益,因为聊天没法插入广告;而 QQ 空间可以插入很多广告,如果从收入的角度,QQ 空间做异地多活的优先级反而高于聊天。 以用户管理系统为例,“登录”业务符合“访问量大的业务”和“核心业务”这两条标准,因此登录是核心业务。 0.4.2. 数据分类 挑选出核心业务后,需要对核心业务相关的数据进一步分析,目的在于识别所有的数据及数据特征,这些数据特征会影响后面的方案设计。 常见的数据特征分析维度有: 数据量:包括总的数据量和新增、修改、删除的量。对异地多活架构来说,新增、修改、删除的数据就是可能要同步的数据,数据量越大,同步延迟的几率越高,同步方案需要考虑相应的解决方案。 唯一性:是否要求多个异地机房产生的同类数据(例如,用户 ID)必须保证唯一。 如果数据不需要唯一,那就说明两个地方都产生同类数据是可能的; 如果数据要求必须唯一,那只能一个中心产生数据或设计一个数据唯一生成的算法。 实时性:在一个中心修改了数据,要求多长时间必须同步到另一个中心,实时性要求越高,对同步的要求越高,方案越复杂。 可丢失性:数据是否可以丢失,部分丢失的数据是否会对业务产生重大影响。 可恢复性:数据丢失后,是否可以通过某种手段进行恢复,如果数据可以恢复,至少说明对业务的影响不会那么大,这样可以相应地降低异地多活架构设计的复杂度。 0.4.3. 数据同步 确定数据的特点后,可以根据不同的数据设计不同的同步方案。常见的数据同步方案有: 存储系统同步:最常用最简单的同步方式(如,MySQL主从/主主数据同步),主流存储系统都支持,但是不能针对业务数据特点做定制化的控制。 消息队列同步:采用独立消息队列进行数据同步(如,Kafka、ActiveMQ、RocketMQ 等),适合无事务性或者无时序性要求的数据。 重复生成:数据不同步到异地机房,每个机房都可以生成数据,适合于可以重复生成的数据。例如,登录产生的 cookie、session 数据、缓存数据等。 0.4.4. 异常处理 无论数据同步方案如何设计,一旦出现极端异常的情况,总是会有部分数据出现异常的。例如,同步延迟、数据丢失、数据不一致等。 异常处理就是假设在出现这些问题时,系统采取的应对措施。异常处理主要有以下几个目的: 问题发生时,避免少量数据异常导致整体业务不可用。 问题恢复后,将异常的数据进行修正。 对用户进行安抚,弥补用户损失。 常见的异常处理措施有这几类: 多通道同步:采取多种方式来进行数据同步,可以应对同步通道故障的情况。从成本与风险的角度考虑一般采取两个通道,数据库同步通道和消息队列同步通道不能采用相同的网络连接(例如,一公一内),需要数据是可以重复覆盖的(如,新建的帐号数据),无论哪个通道的数据先到,最终结果是一样的。 同步和访问结合:访问指异地机房通过系统的接口来进行数据访问,接口访问通道和数据库同步通道不能采用相同的网络连接(例如,一公一内),数据有路由规则,可以根据数据来推断应该访问哪个机房的接口来读取数据,由于有同步通道,优先读取本地数据,本地数据无法读取到再通过接口去访问,这样可以降低跨机房的异地接口访问数量,适合于实时性要求非常高的数据。 日志记录:在每个关键操作前后都记录一条相关日志并独立保持,故障恢复后将数据与日志比对进行数据修复。应对不同级别的故障,日志保存要求不同,应对的故障程度不同,复杂度、成本和收益也不同,常见的日志保存方式有: 服务器上保存日志,数据库中保存数据,可应对单台数据库服务器故障或者宕机的情况。 本地独立系统保存日志,可应对某业务服务器和数据库同时宕机的情况。 日志异地保存,可应对机房宕机的情况。 用户补偿:无论何种异常处理措施,都只能最大限度地降低受到影响的范围和程度,无法完全做到没有任何影响。采用人工的方式对用户进行补偿,弥补用户损失,培养用户的忠诚度。 0.5. 应对接口级故障 异地多活方案主要应对系统级的故障,例如,机器宕机、机房故障、网络故障等问题,虽然影响很大,但发生概率较小,而接口级别的故障虽然影响没有那么大,但是发生的概率比较高。 接口级故障的典型表现就是系统并没有宕机,网络也没有中断,但业务却出现问题了: 例如,业务响应缓慢、大量访问超时、大量访问出现异常(给用户弹出提示“无法连接数据库”),这类问题的主要原因在于系统压力太大、负载太高,导致无法快速处理业务请求,由此引发更多的后续问题。 例如,最常见的数据库慢查询将数据库的服务器资源耗尽,导致读写超时,业务读写数据库时要么无法连接数据库、要么超时,最终用户看到的现象就是访问很慢,一会访问抛出异常,一会访问又是正常结果。 导致接口级故障的原因一般有下面几种: 内部原因:程序 bug 导致死循环,某个接口导致数据库慢查询,程序逻辑不完善导致耗尽内存等。 外部原因:黑客攻击、促销或者抢购引入了超出平时几倍甚至几十倍的用户,第三方系统大量请求,第三方系统响应缓慢等。 解决接口级故障的核心思想和异地多活基本类似:优先保证核心业务和优先保证绝大部分用户。 0.5.1. 降级 系统将某些业务或者接口的功能降低,只提供部分功能或完全停止所有功能。降级的核心思想就是丢车保帅,优先保证核心业务。 常见的实现降级的方式有: 系统后门降级:系统预留后门用于降级操作,实现成本低,但需要每台服务器操作效率低。例如,系统提供一个降级 URL,访问 URL 传入参数实现降级,但是存在安全隐患需要加入密码之类的安全措施。 独立降级系统:为了解决系统后门降级方式的缺点,将降级操作独立到一个单独的系统中,可以实现复杂的权限管理、批量操作等功能。 0.5.2. 熔断 降级的目的是应对系统自身的故障,降级是服务端操作 熔断的目的是应对依赖的外部系统故障的情况,熔断是客户端操作。 熔断机制实现的关键是需要有一个统一的 API 调用层,由 API 调用层来进行采样或者统计,如果接口调用散落在代码各处就没法进行统一处理了。 熔断机制实现的另外一个关键是阈值的设计,例如 1 分钟内 30% 的请求响应时间超过 1 秒就熔断,这个策略中的“1 分钟”“30%”“1 秒”都对最终的熔断效果有影响。实践中一般都是先根据分析确定阈值,然后上线观察效果,再进行调优。 0.5.3. 限流 降级是从系统功能优先级的角度考虑如何应对故障, 限流是从用户访问压力的角度来考虑如何应对故障。 限流只允许系统能够承受的访问量进来,超出系统访问能力的请求将被丢弃,保证一部分请求能够正常响应。限流一般都是系统内实现的,常见的限流方式可以分为两类:基于请求限流和基于资源限流。 0.5.3.1. 基于请求限流 从外部访问的请求角度考虑限流,常见的方式有:限制总量、限制时间量。 限制总量的方式是限制某个指标的累积上限,常见的是限制当前系统服务的用户总量: 某个直播间限制总用户数上限为 100 万,超过 100 万后新的用户无法进入; 某个抢购活动商品数量只有 100 个,限制参与抢购的用户上限为 1 万个,1 万以后的用户直接拒绝。 限制时间量指限制一段时间内某个指标的上限: 1 分钟内只允许 10000 个用户访问, 每秒请求峰值最高为 10 万。 无论是限制总量还是限制时间量,共同的特点都是实现简单,但在实践中面临的主要问题是比较难以找到合适的阈值。 例如系统设定了 1 分钟 10000 个用户,但实际上 6000 个用户的时候系统就扛不住了;也可能达到 1 分钟 10000 用户后,其实系统压力还不大,但此时已经开始丢弃用户访问了。 即使找到了合适的阈值,基于请求限流还面临硬件相关的问题。 例如一台 32 核的机器和 64 核的机器处理能力差别很大,阈值是不同的,可能有的技术人员以为简单根据硬件指标进行数学运算就可以得出来,实际上这样是不可行的,64 核的机器比 32 核的机器,业务处理性能并不是 2 倍的关系,可能是 1.5 倍,甚至可能是 1.1 倍。 为了找到合理的阈值,通常情况下可以采用性能压测来确定阈值,但性能压测也存在覆盖场景有限的问题,可能出现某个性能压测没有覆盖的功能导致系统压力很大;另外一种方式是逐步优化,即:先设定一个阈值然后上线观察运行情况,发现不合理就调整阈值。 基于上述的分析,根据阈值来限制访问量的方式更多的适应于业务功能比较简单的系统,例如负载均衡系统、网关系统、抢购系统等。 0.5.3.2. 基于资源限流 基于请求限流是从系统外部考虑的, 基于资源限流是从系统内部考虑的,即:找到系统内部影响性能的关键资源,对其使用上限进行限制。 常见的内部资源有:连接数、文件句柄、线程数、请求队列等。 例如,采用 Netty 来实现服务器,每个请求都先放入一个队列,业务线程再从队列读取请求进行处理,队列长度最大值为 10000,队列满了就拒绝后面的请求;也可以根据 CPU 的负载或者占用率进行限流,当 CPU 的占用率超过 80% 的时候就开始拒绝新的请求。 基于资源限流相比基于请求限流能够更加有效地反映当前系统的压力,但实践中设计也面临两个主要的难点: 如何确定关键资源, 如何确定关键资源的阈值。 通常情况下,这也是一个逐步调优的过程,即:设计的时候先根据推断选择某个关键资源和阈值,然后测试验证,再上线观察,如果发现不合理,再进行优化。 0.5.4. 排队 排队实际上是限流的一个变种,限流是直接拒绝用户,排队是让用户等待一段时间。排队虽然没有直接拒绝用户,但用户等了很长时间后进入系统,体验并不一定比限流好。 由于排队需要临时缓存大量的业务请求,单个系统内部无法缓存这么多数据,一般情况下,排队需要用独立的系统去实现,例如使用 Kafka 这类消息队列来缓存用户请求。

08 计算高可用架构 阅读更多

0.1. 主备 0.2. 主从 0.3. 集群 0.3.1. 对称集群 0.3.2. 非对称集群 计算高可用: 设计目标:当出现部分硬件损坏时,计算任务能够继续正常运行。 本质:通过冗余来规避部分故障的风险,单台服务器是无论如何都达不到这个目标的。 设计复杂度:在任务管理方面,即当任务在某台服务器上执行失败后,如何将任务重新分配到新的服务器进行执行。 所以计算高可用的设计思想很简单:通过增加更多服务器来达到计算高可用。 计算高可用架构设计的关键点有下面两点: 哪些服务器可以执行任务: 与高性能架构类似,每个服务器都可以执行任务 与高可用存储架构类似,只有特定服务器(主)可以执行任务,当执行任务的服务器故障后,系统需要挑选新的服务器来执行任务。 任务如何重新执行: 对于已经分配的任务即使执行失败也不做任务处理,系统只要保证新的任务能够分配到非故障服务器上执行即可 设计一个任务管理器来管理需要执行的计算任务,服务器执行完任务后,向任务管理器反馈结果,任务管理器根据任务结果来决定是否需要将任务重新分配到另外的服务器上执行。 “任务分配器”是一个逻辑的概念,并不一定要求系统存在一个独立的任务分配器模块。例如: Nginx 将页面请求发送给 Web 服务器,而 CSS/JS 等静态文件直接读取本地缓存。此时 Nginx 是反向代理又承担任务分配器的职责。 后台批量运算的任务,可以设计独立的任务分配器来管理这些批处理任务的执行和分配。 ZooKeeper 中的 Follower 节点,当接收到写请求时会将请求转发给 Leader 节点处理,当接收到读请求时就自己处理,此时 Follower 就相当于一个逻辑上的任务分配器。 0.1. 主备 主备架构是计算高可用最简单的架构,和存储高可用的主备复制架构类似,但更简单无须数据复制。 主机执行所有计算任务。例如,读写数据、执行操作等。 当主机故障(例如,主机宕机)时,任务分配器不会自动将计算任务发送给备机,此时系统处于不可用状态。 如果主机能够恢复(人工/自动),任务分配器继续将任务发送给主机。 如果主机不能够恢复(例如,硬件损坏),则需要人工操作,将备机升为主机,然后让任务分配器将任务发送给新的主机;同时,为了继续保持主备架构,需要人工增加新的机器作为备机。 根据备机状态的不同,主备架构又可以细分为冷备架构和温备架构。 冷备:备机上的程序包和配置文件都准备好,但业务系统没有启动(注意:备机的服务器是启动的),主机故障后,需要人工将备机的业务系统启动,并将任务分配器的任务请求切换发送给备机。 温备:备机上的业务系统已经启动,只是不对外提供服务,主机故障后,人工将任务分配器的任务请求切换发送到备机即可。 冷备可以节省一定的能源,但温备能够大大减少手工操作时间,因此一般情况下推荐用温备的方式。 主备架构: 优点:简单,主备之间不需进行交互,状态判断和切换操作由人工执行,系统实现很简单。 缺点:人工操作的时间不可控,人工操作的效率低,人工操作过程容易出错。 和存储高可用中的主备复制架构类似,计算高可用的主备架构也比较适合与内部管理系统、后台管理系统这类使用人数不多、使用频率不高的业务,不太适合在线的业务。 0.2. 主从 和存储高可用中的主从复制架构类似,计算高可用的主从架构中的从机也是要执行任务的。任务分配器需要将任务进行分类,确定哪些任务可以发送给主机执行,哪些任务可以发送给备机执行。 正常情况下,主机执行部分计算任务,备机执行部分计算任务。 当主机故障(例如,主机宕机)时,任务分配器不会自动将原本发送给主机的任务发送给从机,而是继续发送给主机,不管这些任务执行是否成功。 如果主机能够恢复(人工/自动),任务分配器继续按照原有的设计策略分配任务。 如果主机不能够恢复(例如,硬件损坏),则需要人工操作,将原来的从机升级为主机(一般只是修改配置即可),增加新的机器作为从机,新的从机准备就绪后,任务分配器继续按照原有的设计策略分配任务。 主从架构与主备架构相比: 优点:主从架构中从机也执行任务,发挥了从机的硬件性能。 缺点:主从架构中需要将任务分类,任务分配器会复杂一些。 0.3. 集群 主备架构和主从架构通过冗余一台服务器来提升可用性,且需要人工切换主备或主从,架构虽简单,但存在人工操作效率低、容易出错、不能及时处理故障等问题。 在可用性要求更加严格的场景中,需要系统能够自动完成切换操作,这就是高可用集群方案。 根据集群中服务器节点角色的不同,可以分为两类: 对称集群,集群中每个服务器的角色都是一样的,都可以执行所有任务; 非对称集群,集群中的服务器分为多个不同的角色(例如,主从角色),不同的角色执行不同的任务。 注意: 计算高可用集群:包含 2 台服务器的集群,这与多台服务器的集群,在设计上没有本质区别 存储高可用集群:把双机架构和集群架构进行了区分 0.3.1. 对称集群 也称为负载均衡集群。 正常情况下,任务分配器采取某种策略(随机、轮询等)将计算任务分配给集群中的不同服务器。 当集群中的某台服务器故障后,任务分配器不再将任务分配给它,而是将任务分配给其他服务器执行。 当故障的服务器恢复后,任务分配器重新将任务分配给它执行。 负载均衡集群的设计关键点在于两点: 任务分配器需要选取分配策略,轮询和随机基本就够了。 任务分配器需要检测服务器状态。既要检测服务器的状态(例如,服务器是否宕机、网络是否正常等);同时要检测任务的执行状态(例如,任务是否卡死、是否执行时间过长等)。常用的做法是任务分配器和服务器之间通过心跳来传递信息,包括服务器信息和任务信息,然后根据实际情况来确定状态判断条件。 例如,一个在线页面访问系统,正常情况下页面平均会在 500 毫秒内返回,那么状态判断条件可以设计为:1 分钟内响应时间超过 1 秒(包括超时)的页面数量占了 80% 时,就认为服务器有故障。 例如,一个后台统计任务系统,正常情况下任务会在 5 分钟内执行完成,那么状态判断条件可以设计为:单个任务执行时间超过 10 分钟还没有结束,就认为服务器有故障。 不同业务场景的状态判断条件差异很大,实际设计时要根据业务需求来进行设计和调优。 0.3.2. 非对称集群 非对称集群中不同服务器的角色是不同的,不同角色的服务器承担不同的职责。 以 Master-Slave 为例,部分任务是 Master 服务器才能执行,部分任务是 Slave 服务器才能执行。 集群会通过某种方式来区分不同服务器的角色。例如,通过 ZAB 算法选举,或者简单地取当前存活服务器中节点 ID 最小的服务器作为 Master 服务器。 任务分配器将不同任务(对任务进行分类)发送给不同服务器。 当指定类型的服务器故障时,需要重新分配角色。 如果是 Master 服务器故障,需要将剩余的 Slave 服务器中的一个重新指定为 Master 服务器; 如果是 Slave 服务器故障,不需要重新分配角色,只需将故障服务器从集群剔除即可。 非对称集群相比负载均衡集群,设计复杂度主要体现在两个方面: 任务分配策略复杂:将任务划分为不同类型并分配给不同角色的集群节点。 角色分配策略复杂:使用 ZAB、Raft 这类复杂的算法来实现 Leader 的选举。 以 ZooKeeper 为例: 任务分配:ZooKeeper 中不存在独立的任务分配器节点,每个节点都是任务分配器,Follower 收到请求后会进行判断,如果是写请求就转发给 Leader,如果是读请求就自己处理。 角色指定:ZooKeeper 通过 ZAB 算法来选举 Leader,当 Leader 故障后,所有的 Follower 节点会暂停读写操作,开始进行选举,直到新的 Leader 选举出来后才继续对 Client 提供服务。

07 高可用存储架构 阅读更多

0.1. 主备复制 0.1.1. 优点 0.1.2. 缺点 0.2. 主从复制 0.2.1. 优点 0.2.2. 缺点 0.3. 双机切换 0.3.1. 常见架构 0.3.1.1. 互连式 0.3.1.2. 中介式 0.3.1.3. 模拟式 0.4. 主主复制 0.5. 数据集群 0.5.1. 数据集中集群 0.5.2. 数据分散集群 0.6. 数据分区 0.6.1. 集中式 0.6.2. 互备式 0.6.3. 独立式 存储高可用方案的本质是将数据复制到多个存储设备,通过数据冗余来实现高可用,复杂性主要体现在如何应对复制延迟和中断导致的数据不一致问题。 对任何一个高可用存储方案,从以下几个方面分析: 数据如何复制? 各个节点的职责是什么? 如何应对复制延迟? 如何应对复制中断? 常见的高可用存储架构有主备、主从、主主、集群、分区,每一种又可以根据业务的需求进行一些特殊的定制化功能,由此衍生出更多的变种。 常见的双机高可用架构为:主备、主从、主备/主从切换和主主。 0.1. 主备复制 最常见、最简单的一种存储高可用方案,几乎所有的存储系统都提供了主备复制的功能,例如 MySQL、Redis、MongoDB 等。 主备架构中的“备机”主要起到备份作用,并不承担实际的业务读写操作,如果要把备机改为主机,需要人工操作。 0.1.1. 优点 主备复制架构的优点就是简单,表现有: 对于客户端来说,不需要感知备机的存在,即使灾难恢复后,原来的备机被人工修改为主机后,对于客户端来说,只是认为主机的地址换了而已,无须知道是原来的备机升级为主机。 对于主机和备机来说,双方只需要进行数据复制即可,无须进行状态判断和主备切换这类复杂的操作。 0.1.2. 缺点 备机只做备份,不提供读写,硬件浪费。 故障后需要人工干预(效率低下且容易出错),无法自动恢复。 一般系统的数据变更频率低,即使在某些场景下丢失数据,也可以通过人工的方式补全。这样可以采用主备架构。 0.2. 主从复制 主从复制和主备复制只有一字之差,“从”意思是“随从”(需要干活,提供读取功能),“备”的意思是备份。也就是说,主机负责读写操作,从机只负责读操作,不负责写操作。 0.2.1. 优点 主从复制与主备复制相比,优点有: 主从复制在主机故障时,读操作相关的业务可以继续运行。 主从复制架构的从机提供读操作,发挥了硬件的性能。 0.2.2. 缺点 主从复制架构中,客户端需要感知主从关系,并将不同的操作发给不同的机器进行处理,复杂度比主备复制要高。 主从复制架构中,从机提供读业务,如果主从复制延迟比较大,业务会因为数据不一致出现问题。 故障时需要人工干预。 一般情况下,写少读多的业务使用主从复制的存储架构比较多。 0.3. 双机切换 主备复制和主从复制方案存在两个共性的问题: 主机故障后,无法进行写操作。 如果主机无法恢复,需要人工指定新的主机角色。 双机切换就是为了解决这两个问题而产生的,包括主备切换和主从切换两种方案。 在原有方案的基础上增加“切换”功能,即系统自动决定主机角色,并完成角色切换(主备切换和主从切换在切换的设计上没有差别)。 要实现一个完善的切换方案,必须考虑这几个关键的设计点: 主备间状态判断: 状态传递的渠道:相互连接/第三方仲裁 状态检测的内容:机器掉电/进程状态/响应状态 切换决策: 切换时机:备机升级为主机的判断标准(机器掉电/主进程退出/响应超时) 切换策略:原主机故障恢复后,重新升级为主机还是继续做备机 自动程度:全自动切换/人工判断后切换 数据冲突解决:原主机故障恢复后,主备之间数据冲突 0.3.1. 常见架构 0.3.1.1. 互连式 主备机直接建立状态传递的渠道。 在主备复制的架构基础上,主机和备机多了一个“状态传递”的通道,这个通道就是用来传递状态信息的。 这个通道的具体实现可以有很多方式: 网络连接(例如,各开一个端口)或者非网络连接(用串口线连接)。 主机发送状态给备机,或者备机到主机来获取状态信息。 与数据复制通道共用,或者独立一条通道。 状态传递通道可以是一条或多条,还可以是不同类型的通道混合(例如,网络 + 串口)。 为了充分利用切换方案能够自动决定主机这个优势,客户端会有一些相应的改变,常见的方式有: 为了切换后不影响客户端的访问,主机和备机之间共享一个对客户端来说唯一的地址。例如虚拟 IP,主机需要绑定这个虚拟的 IP。 客户端同时记录主备机的地址,哪个能访问就访问哪个;备机虽然能收到客户端的操作请求,但是会直接拒绝,拒绝的原因就是“备机不对外提供服务”。 互连式主备切换的缺点: 如果状态传递的通道本身有故障(例如,网线断了),那么备机也会认为主机故障了从而将自己升级为主机,而此时主机并没有故障,最终就可能出现两个主机。 通过增加多个通道来增强状态传递的可靠性,但这样只降低了通道故障概率,不能从根本上解决这个缺点,而且通道越多状态决策越复杂,对备机来说,可能从不同的通道收到不同甚至矛盾的状态信息。 0.3.1.2. 中介式 中介式指的是在主备两者之外引入第三方中介,主备机之间不直接连接,而都去连接中介,并且通过中介来传递状态信息。 虽然引入了第三方中介,但是在状态传递和决策上更简单: 连接管理更简单:主备机无须建立和管理多种类型的状态传递连接通道,只要连接到中介即可,降低了主备机的连接管理复杂度。 状态决策更简单:主备机无须考虑多种类型的连接通道获取的状态信息如何决策的问题,只需要按照如下算法完成状态决策。 初始状态都为备机,且只要与中介断开连接,就将自己降级为备机,可能出现双备机的情况。 主机与中介断连后,中介能够立刻告知备机,备机将自己升级为主机。 旧主机恢复后以新备机身份向中介上报自己的状态。 掉电重启或进程重启后,旧主机初始状态为备机,与中介恢复连接后,保持备机状态不变。 主备机与中介连接都正常时,按照实际的状态决定(如主机响应超时)是否进行切换。 中介式架构在状态传递和状态决策上更加简单,其关键代价就在于如何实现中介本身的高可用。 如果中介自己宕机了,整个系统就进入了双备的状态,写操作相关的业务就不可用了。这就陷入了一个递归陷阱:为了实现高可用,我们引入中介,但中介本身又要求高可用,于是又要设计中介的高可用方案……如此递归下去就无穷无尽了。 开源方案有比较成熟的中介式解决方案,例如 ZooKeeper 和 Keepalived。 ZooKeeper 本身已经实现了高可用集群架构,因此已经解决了中介本身的可靠性问题,在工程实践中推荐基于 ZooKeeper 搭建中介式切换架构。 0.3.1.3. 模拟式 主备机之间不传递任何状态数据,备机模拟客户端,向主机发起模拟的读写操作,根据响应情况来判断主机的状态。 对比互连式切换架构,主备机之间只有数据复制通道,而没有状态传递通道,备机通过模拟的读写操作来探测主机的状态,然后根据读写操作的响应情况来进行状态决策。 模拟式切换与互连式切换相比,优点是实现更加简单,省去状态传递通道的建立和管理工作。 简单既是优点,同时也是缺点。因为模拟式读写操作获取的状态信息只有响应信息(例如,HTTP 404,超时、响应时间超过 3 秒等),没有互连式那样多样(除了响应信息,还可以包含 CPU 负载、I/O 负载、吞吐量、响应时间等),基于有限的状态来做状态决策,可能出现偏差。 0.4. 主主复制 两台机器都是主机,互相将数据复制给对方,客户端可以任意挑选其中一台机器进行读写操作。 相比主备切换架构,主主复制架构具有如下特点: 两台都是主机,不存在切换的概念。 客户端无须区分不同角色的主机,随便将读写操作发送给哪台主机都可以。 主主复制架构从总体上来看要简单很多,无须状态信息传递,也无须状态决策和状态切换。 主主复制架构有其独特的复杂性,主主复制架构,必须保证数据能够双向复制,而很多数据(用户账户、库存数据、余额数据)是不能双向复制的。 因此,主主复制架构对数据的设计有严格的要求,一般适合于那些临时性、可丢失、可覆盖的数据场景。例如: 用户登录产生的 session 数据(可以重新登录生成) 用户行为的日志数据(可以丢失) 论坛的草稿数据(可以丢失) 0.5. 数据集群 主备、主从、主主架构本质上都有一个隐含的假设:主机能够存储所有数据,但主机本身的存储和处理能力肯定是有极限的,必须使用多台服务器来存储数据,这就是数据集群架构。 集群是多台(数量上至少是 3 台)机器组合在一起形成一个统一的系统;相比而言,主备、主从都是 2 台机器。 根据集群中机器承担的不同角色来划分,集群可以分为两类: 数据集中集群 数据分散集群 0.5.1. 数据集中集群 数据集中集群与主备、主从类架构相似,也称为 1 主多备或者 1 主多从。无论是 1 主 1 从、1 主 1 备,还是 1 主多备、1 主多从,数据都只能往主机中写,而读操作可以参考主备、主从架构进行灵活多变。 由于集群里面的服务器数量更多,导致复杂度整体更高一些,具体体现在: 主机如何将数据复制给备机:主备和主从架构中,只有一条复制通道,而数据集中集群架构中,存在多条复制通道。 首先会增大主机复制的压力,某些场景下需要考虑如何降低主机复制压力,或者降低主机复制给正常读写带来的压力。 其次,多条复制通道可能会导致多个备机之间数据不一致,某些场景下需要对备机之间的数据一致性进行检查和修正。 备机如何检测主机状态:主备和主从架构中,只有一台备机需要进行主机状态判断。在数据集中集群架构中,多台备机都需要对主机状态进行判断,而不同的备机判断的结果可能是不同的,如何处理不同备机对主机状态的不同判断,是一个复杂的问题。 主机故障后,如何决定新的主机:主从架构中,如果主机故障,将备机升级为主机即可;而在数据集中集群架构中,如何从多台备机中协商选择一台升级为主机,这也是一个复杂的问题。 目前开源的数据集中集群以 ZooKeeper 为典型,ZooKeeper 通过 ZAB 算法来解决上述提到的几个问题,但 ZAB 算法的复杂度是很高的。 0.5.2. 数据分散集群 多个服务器组成一个集群,每台服务器都会负责存储一部分数据;同时,为了提升硬件利用率,每台服务器又会备份一部分数据。 数据分散集群的复杂点在于如何将数据分配到不同的服务器上,算法需要考虑这些设计点: 均衡性:保证服务器上的数据分区基本是均衡的,不能存在某台服务器上的分区数量是另外一台服务器的几倍的情况。 容错性:当出现部分服务器故障时,需要将原来分配给故障服务器的数据分区重新分配给其他服务器。 可伸缩性:当集群容量不够,扩充新的服务器后,能够自动将部分数据分区迁移到新服务器,并保证扩容后所有服务器的均衡性。 数据分散集群和数据集中集群的不同点在于: 数据分散集群中,每台服务器都可以处理读写请求,因此不存在数据集中集群中负责写的主机那样的角色。 数据分散集群中,必须有一个角色来负责执行数据分配算法,这个角色可以是独立的一台服务器,也可以是集群自己选举出的一台服务器。如果是集群服务器选举出来一台机器承担数据分区分配的职责,则这台服务器一般也会叫作主机,但这里的“主机”和数据集中集群中的“主机”,其职责是有差异的。 Hadoop 的实现就是独立的服务器(Namenode)负责数据分区的分配。 与 Hadoop 不同的是,Elasticsearch 集群通过选举一台服务器来做数据分区的分配,叫作 master node。 数据集中集群架构中,客户端只能将数据写到主机; 数据分散集群架构中,客户端向任意服务器读写数据。 正是因为这个关键的差异,决定了两种集群的应用场景不同。 数据集中集群适合数据量不大,集群机器数量不多的场景。例如,ZooKeeper 集群,一般推荐 5 台机器左右,数据量是单台服务器就能够支撑; 数据分散集群由于良好的可伸缩性,适合业务数据量巨大、集群机器数量庞大的业务场景。例如,Hadoop 集群、HBase 集群,大规模的集群可以达到上百台甚至上千台服务器 0.6. 数据分区 上述存储高可用架构都是基于硬件故障的场景去考虑和设计的,主要考虑当部分硬件可能损坏的情况下系统应该如何处理,但对于一些影响非常大的灾难或者事故来说,有可能所有的硬件全部故障,这种情况下基于硬件故障而设计的高可用架构不再适用,需要基于地理级别的故障来设计高可用架构,这就是数据分区架构产生的背景。 将数据按照一定的规则进行分区,不同分区分布在不同的地理位置上,每个分区存储一部分数据,通过这种方式来规避地理级别的故障所造成的巨大影响。 采用数据分区架构后,即使某个地区发生严重的自然灾害或者事故,受影响的也只是一部分数据,而不是全部数据都不可用;当故障恢复后,其他地区备份的数据也可以帮助故障地区快速恢复业务。 设计一个良好的数据分区架构,需要从多方面去考虑: 数据量:数据量的大小直接决定了分区的规则复杂度 分区规则:地理位置有近有远,因此可以得到不同的分区规则,包括洲际分区、国家分区、城市分区。具体采取哪种或者哪几种规则,需要综合考虑业务范围、成本等因素。 复制规则:每个分区本身的数据量虽然只是整体数据的一部分,但还是很大,这部分数据如果损坏或者丢失,损失同样难以接受。因此即使是分区架构,同样需要考虑复制方案。常见的分区复制规则有三种:集中式、互备式和独立式。 0.6.1. 集中式 集中式备份指存在一个总的备份中心,所有的分区都将数据备份到备份中心,集中式备份架构的优缺点是: 设计简单,各分区之间并无直接联系,可以做到互不影响。 扩展容易,如果要增加第新的分区,只需要将新分区的数据复制到备份中心即可,其他分区不受影响。 成本较高,需要建设一个独立的备份中心。 0.6.2. 互备式 互备式备份指每个分区备份另外一个分区的数据,互备式备份架构的优缺点是: 设计比较复杂,各个分区除了要承担业务数据存储,还需要承担备份功能,相互之间互相关联和影响。 扩展麻烦,如果增加一个新分区,则需要修改依赖和被依赖的两个分区的备份关系。 成本低,直接利用已有的设备。 0.6.3. 独立式 独立式备份指每个分区自己有独立的备份中心。 注意,各个分区的备份并不和原来的分区在一个地方,这样做的主要目的是规避同城或者相同地理位置同时发生灾难性故障的极端情况。 独立式备份架构的优缺点是: 设计简单,各分区互不影响。 扩展容易,新增加的分区只需要搭建自己的备份中心即可。 成本高,每个分区需要独立的备份中心,备份中心的场地成本是主要成本,因此独立式比集中式成本要高很多。

06 高可用架构 阅读更多

0.1. CAP 0.1.1. 定义 0.1.2. 应用 0.1.2.1. CP-Consistency/Partition Tolerance 0.1.2.2. AP-Availability/Partition Tolerance 0.1.3. CAP关键细节点 0.1.3.1. CAP关注的粒度是数据,而不是整个系统 0.1.3.2. CAP是忽略网络延迟的 0.1.3.3. 正常运行情况下,不存在 CP 和 AP 的选择,可以同时满足 CA 0.1.3.4. 放弃并不等于什么都不做,需要为分区恢复后做准备 0.1.4. 与ACID和BASE的对比 0.1.4.1. ACID 0.1.4.2. BASE 0.2. 排除架构可用性隐患 0.2.1. 功能点 0.2.2. 故障模式 0.2.3. 故障影响 0.2.4. 严重程度 0.2.5. 故障原因 0.2.6. 故障概率 0.2.7. 风险程度 0.2.8. 已有措施 0.2.9. 规避措施 0.2.10. 解决措施 0.2.11. 后续规划 0.1. CAP 对于设计分布式系统的架构师来说,CAP 是必须掌握的理论。 在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。 分布式并不一定会互联和共享数据。 CAP探讨的对象是interconnected和share data的分布式系统。 CAP关注的是对数据的读写操作,而不是分布式系统所有功能。 0.1.1. 定义 特性 原始解释 升级版解释 一致性(Consisitency) 所有节点在同一时刻都能看到相同的数据 对某个指定的客户端来说,读操作保证能够返回最新的写操作结果 可用性(Availability) 每个请求都能得到成功或者失败的响应 非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应) 分区容错性(Partition Tolerance) 出现消息丢失或者分区错误时系统能够继续运行 当出现网络分区后,系统能够继续“履行职责” 一致性:,对于系统执行事务来说,在事务执行过程中,系统其实处于一个不一致的状态,不同的节点的数据并不完全一致,也就是出现same time + different data的情况,而不是每时每刻都是same time + same data。 网络分区:在分布式集群中,节点之间由于网络不通,导致集群中节点形成不同的子集,子集中节点间的网络相通,而子集和子集间网络不通。 0.1.2. 应用 虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是必然的现象。 假设实现 CA 架构,当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因为 A 要求返回 no error 和 no timeout。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 0.1.2.1. CP-Consistency/Partition Tolerance 如下图所示,为了保证一致性,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。 这时客户端访问 N2 时,N2 需要返回 Error,提示客户端“系统现在发生了错误”,这种处理方式违背了可用性(Availability)的要求,因此 CAP 三者只能满足 CP。 0.1.2.2. AP-Availability/Partition Tolerance 如下图所示,为了保证可用性,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。 这时客户端访问 N2 时,N2 将当前自己拥有的数据 x 返回给客户端,而实际上当前最新的数据已经是 y,这就不满足一致性(Consistency)的要求,因此 CAP 三者只能满足 AP。 注意:这里 N2 节点返回 x,虽然不是一个“正确”的结果,但是一个“合理”的结果,因为 x 是旧的数据,并不是一个错乱的值,只是不是最新的数据而已。 0.1.3. CAP关键细节点 0.1.3.1. CAP关注的粒度是数据,而不是整个系统 C 与 A 之间的取舍可以在同一系统内以非常细小的粒度反复发生,而每一次的决策可能因为具体的操作,乃至因为牵涉到特定的数据或用户而有所不同。 CAP 理论的定义和解释中,都是 system、node 这类系统级的概念,这造成了误导,认为在进行架构设计时,整个系统要么选择 CP,要么选择 AP。 在实际设计过程中,每个系统不可能只处理一种数据,而是包含多种类型的数据: 有的数据必须选择 CP 有的数据必须选择 AP 架构设计时,从整个系统的角度去选择 CP 还是 AP,就会发现顾此失彼。所以在 CAP 理论落地实践时,需要将系统内的数据按照不同的应用场景和要求进行分类,每类数据选择不同的策略(CP 还是 AP),而不是直接限定整个系统所有数据都是同一策略。 0.1.3.2. CAP是忽略网络延迟的 CAP 理论中的 C 在实践中是不可能完美实现的,在数据复制的过程中,总是需要花费一定的时间(几毫秒到几十毫秒不等),节点 A 和节点 B 的数据并不一致。 技术上是无法做到在分布式场景下完美的一致性的,但是在条件严格的业务场景下,必须要求一致性(如银行账户存取钱、商品抢购库存更新等),因此在实际中会选择CA,也就是单点写入,其他节点做备份,无法做到分布式情况下的多点写入。 例如,根据用户ID将用户的读写操作限定在某一个节点,这样单点故障只会影响部分用户,从整体上看依然是分布式架构。 0.1.3.3. 正常运行情况下,不存在 CP 和 AP 的选择,可以同时满足 CA CAP 理论中分布式系统只能选择 CP 或者 AP的前提是系统发生了“分区”现象。 正常运行情况下,系统中不存在网络分区,节点间的网络连接一起正常,此时应该同时保证C和A。因此,在架构设计的时候既要考虑分区发生时选择 CP 还是 AP,也要考虑分区没有发生时如何保证 CA。 0.1.3.4. 放弃并不等于什么都不做,需要为分区恢复后做准备 CAP 理论中三者只能取两个,需要“牺牲”(sacrificed)另外一个,这里的“牺牲”是有一定误导作用的,因为“牺牲”让很多人理解成什么都不做。 实际上,CAP 理论的“牺牲”只是说在网络分区过程中无法保证 C 或者 A,但并不意味着什么都不做。因为在系统整个运行周期中,大部分时间都是正常的,发生分区现象的时间并不长。分区期间放弃 C 或者 A,并不意味着永远放弃 C 和 A,可以在分区期间进行一些操作,从而让分区故障解决后,系统能够重新达到 CA 的状态。 最典型的做法就是在分区期间记录日志,当分区故障解决后,系统根据日志进行数据恢复,使得重新达到 CA 状态。 0.1.4. 与ACID和BASE的对比 0.1.4.1. ACID ACID 是数据库管理系统为了保证事务的正确性而提出来的一个理论,ACID 包含四个约束。 Atomicity(原子性):一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某个环节结束。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。 Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。 Isolation(隔离性):数据库允许多个并发事务同时对数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括: 读未提交(Read uncommitted) 读提交(read committed) 可重复读(repeatable read) 串行化(Serializable) Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。 ACID 中的 A(Atomicity)和 CAP 中的 A(Availability)意义完全不同 ACID 中的 C 和 CAP 中的 C 名称虽然都是一致性,但含义也完全不一样 ACID 中的 C 是指数据库的数据完整性 CAP 中的 C 是指分布式节点中的数据一致性 ACID 的应用场景是数据库事务 CAP 关注的是分布式系统数据读写 0.1.4.2. BASE BASE : 基本可用(Basically Available) 软状态(Soft State) 最终一致性(Eventual Consistency) 核心思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。 基本可用(Basically Available):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。具体选择哪些作为可以损失的业务,哪些是必须保证的业务,是一项有挑战的工作。 软状态(Soft State):允许系统存在中间状态(CAP 理论中的数据不一致),而该中间状态不会影响系统整体可用性。 最终一致性(Eventual Consistency):系统中的所有数据副本经过一定时间(这里的“一定时间”和数据的特性是强关联的,不同的数据能够容忍的不一致时间是不同的)后,最终(不管多长时间)能够达到一致的状态。 BASE 理论本质上是对 CAP 中 AP 方案的一个补充: CAP 理论是忽略延时的,而实际应用中延时是无法避免的。这意味着完美的 CP 场景是不存在的,即使是几毫秒的数据复制延迟,在这几毫秒时间间隔内,系统是不符合 CP 要求的。因此 CAP 中的 CP 方案,实际上也是实现了最终一致性,只是“一定时间”是指几毫秒。 AP 方案中牺牲一致性只是指分区期间,而不是永远放弃一致性。这其实就是 BASE 理论延伸的地方,分区期间牺牲一致性,但分区故障恢复后,系统应该达到最终一致性。 综上: ACID 是数据库事务完整性的理论 CAP 是分布式系统设计理论 BASE 是 CAP 理论中 AP 方案的延伸 0.2. 排除架构可用性隐患 FMEA(Failure mode and effects analysis,故障模式与影响分析)又称为失效模式与后果分析、失效模式与效应分析、故障模式与后果分析等,FMEA 是一种在各行各业都有广泛应用的可用性分析方法,通过对系统范围内潜在的故障模式加以分析,并按照严重程度进行分类,以确定失效对于系统的最终影响。 在架构设计领域,FMEA 的具体分析方法是: 给出初始的架构设计图。 假设架构中某个部件发生故障。 分析此故障对系统功能造成的影响。 根据分析结果,判断架构是否需要进行优化。 FMEA 分析的方法是分析表,常见的 FMEA 分析表格包含下面部分。 0.2.1. 功能点 注意这里的“功能点”指的是从用户角度来看的,而不是从系统各个模块功能点划分来看的。 0.2.2. 故障模式 系统会出现什么样的故障,包括故障点和故障形式。 注意,这里的故障模式并不需要给出真正的故障原因,只需要假设出现某种故障现象即可,例如 MySQL 响应时间达到 3 秒。造成这个现象可能的原因很多:磁盘坏道、慢查询、服务器到 MySQL 的连接网络故障、MySQL bug 等,不需要在故障模式中一一列出来,而是在“故障原因”一节中列出来。 因为在实际应用过程中,不管哪种原因,只要现象是一样的,对业务的影响就是一样的。 此外,故障模式的描述要尽量精确,多使用量化描述,避免使用泛化的描述。例如,推荐使用“MySQL 响应时间达到 3 秒”,而不是“MySQL 响应慢”。 0.2.3. 故障影响 当发生故障模式中描述的故障时,功能点具体会受到什么影响。常见的影响有: 功能点偶尔不可用、 功能点完全不可用、 部分用户功能点不可用、 功能点响应缓慢、 功能点出错等。 故障影响需要准确描述。例如,推荐使用“20% 的用户无法登录”,而不是“大部分用户无法登录”。要注意这里的数字不需要完全精确,比如 21.25% 这样的数据其实是没有必要的,只需要预估影响是 20% 还是 40%。 0.2.4. 严重程度 站在业务角度故障的影响程度,一般分为“致命 / 高 / 中 / 低 / 无”五个档次。 严重程度按照这个公式进行评估:严重程度 = 功能点重要程度 × 故障影响范围 × 功能点受损程度。 以用户管理系统为例:登录功能比修改用户资料要重要得多,80% 的用户比 20% 的用户范围更大,完全无法登录比登录缓慢要更严重,得出如下故障模式的严重程度。 致命:超过 70% 用户无法登录。 高:超过 30% 的用户无法登录。 中:所有用户登录时间超过 5 秒。 低:10% 的用户登录时间超过 5 秒。 中:所有用户都无法修改资料。 低:20% 的用户无法修改头像。 对于某个故障的影响到底属于哪个档次,有时会出现一些争议。 0.2.5. 故障原因 “故障模式”中只描述了故障的现象,不管什么故障原因,故障现象相同,对功能点的影响就相同。 单独将故障原因列出来是原因: 不同的故障原因发生概率不同,而不同的概率又会影响具体如何应对这个故障。 不同的故障原因检测手段不同 不同的故障原因处理措施不同 0.2.6. 故障概率 指某个具体故障原因发生的概率。例如,磁盘坏道的概率、MySQL bug 的概率、没有索引的概率。 一般分为“高 / 中 / 低”三档即可,具体评估时有以下几点要重点关注。 硬件:硬件随着使用时间推移,故障概率会越来越高。 开源系统: 成熟的开源系统 bug 率低,刚发布的开源系统 bug 率相比会高一些; 已经有使用经验的开源系统 bug 率会低,刚开始尝试使用的开源系统 bug 率会高。 自研系统:成熟的自研系统故障概率会低,而新开发的系统故障概率会高。 高中低是相对的,只是为了确定优先级以决定后续的资源投入,没有必要绝对量化,因为绝对量化是需要成本的,而且很多时候都没法量化。 0.2.7. 风险程度 综合严重程度和故障概率判断某个故障的最终等级,风险程度 = 严重程度 × 故障概率。可能出现某个故障影响非常严重,但其概率很低,最终来看风险程度就低。 “某个机房业务瘫痪”对业务影响是致命的: 如果故障原因是“地震”,那概率就很低,例如广州5 级以上地震的 20 世纪才 1 次(1940 年); 如果故障的原因是“机房空调烧坏”,则概率就比地震高很多了,可能是 2 年 1 次; 如果故障的原因是“系统所在机架掉电”,这个概率比机房空调又要高了,可能是 1 年 1 次。 同样的故障影响,不同的故障原因有不同的概率,最终得到的风险级别就是不同的。 0.2.8. 已有措施 针对具体的故障原因,系统现在是否提供了某些措施来应对,包括:检测告警、容错、自恢复等。 检测告警:最简单的措施就是检测故障,然后告警,系统自己不针对故障进行处理,需要人工干预。 容错:检测到故障后,系统能够通过备份手段应对。例如,MySQL 主备机,当业务服务器检测到主机无法连接后,自动连接备机读取数据。 自恢复:检测到故障后,系统能够自己恢复。主要指“业务”上的恢复,一般不太可能将真正的故障恢复。 0.2.9. 规避措施 为了降低故障发生概率而做的一些事情,可以是技术手段,也可以是管理手段。 技术手段:为了避免新引入的 MongoDB 丢失数据,在 MySQL 中冗余一份。 管理手段:为了降低磁盘坏道的概率,强制统一更换服务时间超过 2 年的磁盘。 0.2.10. 解决措施 为了能够解决问题而做的一些事情,一般都是技术手段。例如: 为了解决密码暴力破解,增加密码重试次数限制。 为了解决拖库导致数据泄露,将数据库中的敏感数据加密保存。 为了解决非法访问,增加白名单控制。 如果某个故障既可以采取规避措施,又可以采取解决措施,那么优先选择解决措施。 如果问题是系统自己无法解决的,例如磁盘坏道、开源系统 bug,这类故障只能采取规避措施;系统能够自己解决的故障,大部分是和系统本身功能相关的。 0.2.11. 后续规划 综合前面的分析: 看出哪些故障目前还缺乏对应的措施, 哪些已有措施还不够,针对这些不足的地方,再结合风险程度进行排序,给出后续的改进规划。 这些规划既可以是技术手段,也可以是管理手段;可以是规避措施,也可以是解决措施。需要考虑资源的投入情况,优先将风险程度高的系统隐患解决。 例如: 地震导致机房业务中断:这个故障模式就无法解决,只能通过备份中心规避,尽量减少影响;而机柜断电导致机房业务中断:可以通过将业务机器分散在不同机柜来规避。 敏感数据泄露:这个故障模式可以通过数据库加密的技术手段来解决。 MongoDB 断电丢数据:这个故障模式可以通过将数据冗余一份在 MySQL 中,在故障情况下重建数据来规避影响。

05 高性能负载均衡 阅读更多

0.1. 负载均衡分类 0.1.1. DNS负载均衡 0.1.1.1. 优点 0.1.1.2. 缺点 0.1.2. 硬件负载均衡 0.1.2.1. 优点 0.1.2.2. 缺点 0.1.3. 软件负载均衡 0.1.3.1. 优点 0.1.3.2. 缺点 0.2. 负载均衡典型架构 0.3. 负载均衡算法 0.3.1. 轮询 0.3.2. 加权轮询 0.3.3. 负载最低优先 0.3.4. 性能最优类 0.3.5. Hash类 单服务器无论如何优化和升级硬件,总会性能天花板,当无法满足业务需求时,就需要设计高性能集群来提升系统整体的处理性能。 高性能集群的本质就是通过增加更多的服务器来提升系统整体的计算能力。 计算本身的特点:同样的输入数据和逻辑,无论在哪台服务器上执行,都应该得到相同的输出。 高性能集群设计的复杂度主要体现在增加一个任务分配器(负载均衡器),以及为任务选择一个合适的任务分配算法。 任务分配并不只是考虑计算单元的负载均衡,不同的任务分配算法目标是不一样的: 有的基于负载考虑 有的基于性能(吞吐量、响应时间)考虑 有的基于业务考虑 0.1. 负载均衡分类 0.1.1. DNS负载均衡 DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。DNS 负载均衡的本质是DNS 解析同一个域名可以返回不同的 IP 地址。 0.1.1.1. 优点 简单、成本低:负载均衡工作交给 DNS 服务器处理,无须自己开发或者维护负载均衡设备。 就近访问,提升访问速度:DNS 解析时可以根据请求来源 IP,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能。 0.1.1.2. 缺点 更新不及时:DNS 缓存的时间比较长,修改 DNS 配置后,由于缓存的原因,还是有很多用户会继续访问修改前的 IP,这样的访问会失败,达不到负载均衡的目的,并且也影响用户正常使用业务。 扩展性差:DNS 负载均衡的控制权在域名商那里,无法根据业务特点针对其做更多的定制化功能和扩展特性。 分配策略比较简单:DNS 负载均衡支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载);也无法感知后端服务器的状态。 针对 DNS 负载均衡的一些缺点,对于时延和故障敏感的业务,有一些公司自己实现了 HTTP-DNS 的功能,也就是使用 HTTP 协议实现一个私有的 DNS 系统。这样的方案和通用的 DNS 优缺点正好相反。 0.1.2. 硬件负载均衡 硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。 目前业界典型的硬件负载均衡设备有两款:F5 和 A10。 这类设备性能强劲、功能强大,但价格都不便宜,一般只有“土豪”公司才会考虑使用此类设备。普通业务量级的公司一是负担不起,二是业务量没那么大,用这些设备也是浪费。 0.1.2.1. 优点 功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。 性能强大:软件负载均衡支持到 10 万级并发已经很厉害了,硬件负载均衡可以支持 100 万以上的并发。 稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。 支持安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙、防 DDoS 攻击等安全功能。 0.1.2.2. 缺点 价格昂贵:最普通的一台 F5 就是一台“马6”(20万),好一点的就是“Q7”(80万)。 扩展能力差:硬件设备,可以根据业务进行配置,但无法进行扩展和定制。 0.1.3. 软件负载均衡 软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有: Nginx 是软件的 7 层负载均衡 LVS 是 Linux 内核的 4 层负载均衡 4 层和 7 层的区别就在于协议和灵活性,Nginx 支持 HTTP、E-mail 协议;而 LVS 是 4 层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等。 软件和硬件的最主要区别就在于性能,硬件负载均衡性能远远高于软件负载均衡性能。 Ngxin 的性能是万级,一般的 Linux 服务器上装一个 Nginx 大概能到 5万/秒 LVS 的性能是十万级,据说可达到 80万/秒 F5 性能是百万级,从 200万/秒到 800万/秒都有 软件负载均衡的最大优势是便宜,除了使用开源的系统进行负载均衡,如果业务比较特殊,也可能基于开源系统进行定制(例如,Nginx 插件),甚至进行自研。 0.1.3.1. 优点 简单:部署、维护简单 便宜:只要 Linux 服务器,装上软件即可 灵活:4 层和 7 层负载均衡可以根据业务进行选择;也可以根据业务进行比较方便的扩展,例如,可以通过 Nginx 的插件来实现业务的定制化功能。 0.1.3.2. 缺点 与硬件负载均衡相比: 性能一般 功能没有硬件负载均衡那么强大 一般不具备防火墙和防 DDoS 攻击等安全功能 0.2. 负载均衡典型架构 在架构设计中,基于上述方式的优缺点进行组合使用,基本原则是: DNS负载均衡用于实现地理级别的负载均衡; 硬件负载均衡用于实现集群级别的负载均衡; 软件负载均衡用于实现机器级别的负载均衡。 0.3. 负载均衡算法 负载均衡算法数量较多,且可根据业务特性进行定制开发,抛开细节差异,根据算法期望达到的目的,大体上可以分为下面几类。 任务平分类:负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或者权重上的平均。 负载均衡类:负载均衡系统根据服务器的负载来进行分配,这里的负载并不一定是通常意义上说的“CPU负载”,而是系统当前的压力,可以用 CPU 负载来衡量,也可以用连接数、I/O 使用率、网卡吞吐量等来衡量系统的压力。 性能最优类:负载均衡系统根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器。 Hash 类:负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上。常见的有源地址 Hash、目标地址 Hash、session id hash、用户 ID Hash 等。 0.3.1. 轮询 负载均衡系统收到请求后,按照顺序轮流分配到服务器上,无需关注服务器本身的状态。 只要服务器在运行,运行状态是不关注的。但如果服务器直接宕机了,或者服务器和负载均衡系统断连了,这时负载均衡系统是能够感知的,也需要做出相应的处理。 简单”是轮询算法的优点,也是它的缺点。 0.3.2. 加权轮询 负载均衡系统根据服务器权重(根据硬件配置进行静态配置)进行任务分配,如果采用动态的方式计算会更加契合业务,但复杂度也会更高。 加权轮询是轮询的一种特殊形式,其主要目的就是为了解决不同服务器处理能力有差异的问题。 加权轮询解决了轮询算法中无法根据服务器的配置差异进行任务分配的问题,但同样存在无法根据服务器的状态差异进行任务分配的问题。 0.3.3. 负载最低优先 负载均衡系统将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。例如: LVS 这种 4 层网络负载均衡设备,可以以“连接数”来判断服务器的状态,服务器连接数越大,表明服务器压力越大。 Nginx 这种 7 层网络负载系统,可以以“HTTP 请求数”来判断服务器状态(Nginx 内置的负载均衡算法不支持这种方式,需要进行扩展)。 自研负载均衡系统,可以根据业务特点来选择指标衡量系统压力。如果是 CPU 密集型,可以以“CPU 负载”来衡量系统压力;如果是 I/O 密集型,可以以“I/O 负载”来衡量系统压力。 负载最低优先的算法解决了轮询算法中无法感知服务器状态的问题,由此带来的代价是复杂度要增加很多。例如: 最少连接数优先的算法:要求负载均衡系统统计每个服务器当前建立的连接,其应用场景仅限于负载均衡接收的任何连接请求都会转发给服务器进行处理,否则如果负载均衡系统和服务器之间是固定的连接池方式,就不适合采取这种算法。 CPU 负载最低优先的算法:要求负载均衡系统以某种方式收集每个服务器的 CPU 负载,而且要确定是以 1 分钟的负载为标准,还是以 15 分钟的负载为标准,不存在 1 分钟肯定比 15 分钟要好或者差。不同业务最优的时间间隔是不一样的,时间间隔太短容易造成频繁波动,时间间隔太长又可能造成峰值来临时响应缓慢。 负载最低优先算法基本上能够比较完美地解决轮询算法的缺点,因为采用这种算法后,负载均衡系统需要感知服务器当前的运行状态。其代价是复杂度大幅上升。 负载最低优先算法如果本身没有设计好,或者不适合业务的运行特点,算法本身就可能成为性能的瓶颈,或者引发很多莫名其妙的问题。所以负载最低优先算法虽然效果看起来很美好,但实际上真正应用的场景反而没有轮询(包括加权轮询)那么多。 0.3.4. 性能最优类 负载最低优先类算法是站在服务器的角度来进行分配的, 性能最优优先类算法是站在客户端的角度来进行分配的,优先将任务分配给处理速度最快的服务器,达到最快响应客户端的目的。 性能最优优先类算法本质上也是感知服务器的状态,只是通过响应时间这个外部标准来衡量服务器状态而已。 因此性能最优优先类算法的复杂度都很高,主要体现在: 负载均衡系统需要收集和分析每个服务器每个任务的响应时间,在大量任务处理的场景下,这种收集和统计本身也会消耗较多的性能。 为了减少这种统计上的消耗,可以采取采样的方式来统计,即不统计所有任务的响应时间,而是抽样统计部分任务的响应时间来估算整体任务的响应时间。采样统计虽然能够减少性能消耗,但使得复杂度进一步上升,因为要确定合适的采样率,采样率太低会导致结果不准确,采样率太高会导致性能消耗较大,找到合适的采样率也是一件复杂的事情。 无论是全部统计还是采样统计,都需要选择合适的周期:是 10 秒内性能最优,还是 1 分钟内性能最优,还是 5 分钟内性能最优,需要根据实际业务进行判断和选择,这也是一件比较复杂的事情,甚至出现系统上线后需要不断地调优才能达到最优设计。 0.3.5. Hash类 负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上,这样做的目的主要是为了满足特定的业务需求。例如: 源地址 Hash:将来源于同一个源 IP 地址的任务分配给同一个服务器进行处理,适合于存在事务、会话的业务 ID Hash:将某个 ID 标识的业务分配到同一个服务器中进行处理,这里的 ID 一般是临时性数据的 ID(如 session id)

04 单服务器高性能 阅读更多

0.1. PPC(Process Per Connection) 0.1.1. Prefork 0.2. TPC(Thread Per Connection) 0.2.1. prethread 0.3. 并发模式的选择 0.4. Reactor 0.4.1. 单 Reactor 单进程/线程 0.4.2. 单 Reactor 多线程 0.4.3. 多 Reactor 多进程/线程 0.5. Proactor 磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构等,每个都有可能影响系统达到高性能。 一行不恰当的 debug 日志,就可能将服务器的性能从 TPS 30000 降低到 8000 一个 tcp_nodelay 参数,就可能将响应时间从 2 毫秒延长到 40 毫秒 高性能架构设计主要集中在两方面: 将单服务器的性能发挥到极致 单服务器无法支撑性能,则设计服务器集群方案 除了以上两点,最终系统能否实现高性能,还和具体的实现及编码相关。 架构设计决定了系统性能的上限 实现细节决定了系统性能的下限 单服务器高性能的关键之一就是服务器采取的并发模型,并发模型有如下两个关键设计点: 服务器如何管理连接。 服务器如何处理请求。 以上两个设计点最终都和操作系统的 I/O 模型及进程模型相关。 I/O 模型:阻塞、非阻塞、同步、异步。 进程模型:单进程、多进程、多线程。 0.1. PPC(Process Per Connection) PPC的含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的 UNIX 网络服务器所采用的模型。基本流程如下所示: 父进程接受连接(accept) 父进程fork子进程 子进程处理连接的读写请求(read、业务处理、write) 子进程关闭连接(close) 注意,图中父进程fork子进程后,直接调用了close,只是将连接的文件描述符引用计数减1(因为子进程复制了文件描述符),真正的关闭连接是等子进程也调用close后,连接对应的文件描述符引用计数变为0后,操作系统才会真正关闭连接。 PPC 模式实现简单,比较适合服务器的连接数没那么多的情况,例如数据库服务器。 互联网兴起后,服务器的并发和访问量剧增成千上万,PPC的弊端就凸显出来了: fork代价高:操作系统创建一个进程的代价是很高的,需要分配很多内核资源,需要将内存映像从父进程复制到子进程。(即使现在的操作系统在复制内存映像时用到了 Copy on Write(写时复制)技术,总体来说创建进程的代价还是很大的)。 父子进程通信复杂:父进程“fork”子进程时,文件描述符可以通过内存映像复制从父进程传到子进程,但“fork”完成后,父子进程通信就需要采用 IPC(Interprocess Communication)之类的进程通信方案。 支持的并发连接数量有限:如果每个连接存活时间比较长,而且新的连接又源源不断的进来,则进程数量会越来越多,操作系统进程调度和切换的频率也越来越高,系统的压力也会越来越大。 在架构设计时,PPC方案能处理的并发连接数量最大也就几百。 0.1.1. Prefork PPC 模式中,当连接进来时才 fork 新进程来处理连接请求,由于 fork 进程代价高,用户访问时可能感觉比较慢,prefork 模式的出现就是为了解决这个问题。 系统在启动时预先创建好进程,然后开始接受用户的请求,当有新的连接进来时,可以省去 fork 进程的操作,让用户访问更快、体验更好。prefork 的基本示意图是: prefork 的实现关键就是多个子进程都 accept 同一个 socket,当有新的连接进入时,操作系统保证只有一个进程能最后 accept 成功。 注意:一个小问题“惊群”现象,虽然只有一个子进程能 accept 成功,但所有阻塞在 accept 上的子进程都会被唤醒,这样就导致了不必要的进程调度和上下文切换了。幸运的是,操作系统可以解决这个问题,例如 Linux 2.6 版本后内核已经解决了 accept 惊群问题。 prefork 模式和 PPC 一样,还是存在: 父子进程通信复杂 支持的并发连接数量有限的问题 因此目前实际应用也不多。 Apache 服务器提供了 MPM prefork 模式,推荐在需要可靠性或者与旧软件兼容的站点时采用这种模式,默认情况下最大支持 256 个并发连接。 0.2. TPC(Thread Per Connection) TPC的含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。 与进程相比,线程更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。因此,TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题。 TPC 的基本流程是: 父进程接受连接(accept) 父进程创建子线程(pthread) 子线程处理连接的读写请求(read、业务处理、write) 子线程关闭连接(close) 注意:和 PPC 相比,主进程不用“close”连接了。原因是在于子线程是共享主进程的进程空间的,连接的文件描述符并没有被复制,因此只需要一次 close 即可。 TPC 虽然解决了 fork 代价高和进程通信复杂的问题,但是也引入了新的问题: 创建线程依然有代价,高并发时(每秒上万连接)还是有性能问题 无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题 多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界) 除了上述问题,TPC 还存在 CPU 线程调度和切换代价的问题。 因此,TPC 方案本质上和 PPC 方案基本类似,在并发几百连接的场景下,反而更多地是采用 PPC 的方案,因为 PPC 方案不会有死锁的风险,也不会多进程互相影响,稳定性更高。 0.2.1. prethread TPC 模式中,当连接进来时才创建新的线程来处理连接请求,虽然创建线程更加轻量级,但还是有一定的代价,prethread 模式就是为了解决这个问题。 prethread 模式预先创建线程,然后开始接受用户请求,当有新的连接进来时,省去创建线程的操作,让用户感觉更快、体验更好。 由于多线程之间数据共享和通信比较方便,因此实际上 prethread 的实现方式相比 prefork 要灵活一些,常见的实现方式有下面几种: 主进程 accept,然后将连接交给某个线程处理 子线程都尝试 accept,最终只有一个线程 accept 成功,方案的基本示意图如下 Apache 服务器会首先创建多个进程,每个进程里面再创建多个线程,这样做主要考虑稳定性,即使某个子进程里面的某个线程异常导致整个子进程退出,还会有其他子进程继续提供服务,不会导致整个服务器全部挂掉。prethread 理论上可以比 prefork 支持更多的并发连接,Apache 服务器 MPM worker 模式默认支持 16×25=400 个并发处理线程。 0.3. 并发模式的选择 不同并发模式的选择,还要考察三个指标: 响应时间(RT) 并发数(Concurrency) 吞吐量(TPS) 三者关系,吞吐量=并发数/平均响应时间。 不同类型的系统,对这三个指标的要求不一样。 三高系统,比如秒杀、即时通信,不能使用并发模式 三低系统,比如ToB系统,运营类、管理类系统,一般可以使用并发模式 高吞吐系统: 如果是内存计算为主的,一般可以使用并发模式 如果是网络I/O为主的,一般不能使用并发模式 高并发需要根据两个条件划分:连接数量,请求数量。 海量连接海量请求:例如抢购,双十一等(成千上万连接) 常量连接海量请求:例如中间件(几十上百连接) 海量连接常量请求:例如门户网站 常量连接常量请求:例如内部运营系统,管理系统 因此,常量连接海量请求和常量连接常量请求比较适合使用并发模式,因为PPC和TPC能够支持的最大连接数差不多是几百个。 单服务器高性能的 PPC 和 TPC 模式的优点是实现简单,缺点是无法支撑高并发的场景,尤其是互联网发展到现在,各种海量用户业务的出现,PPC 和 TPC 完全无能为力。 应对高并发场景的单服务器高性能架构模式:Reactor 和 Proactor。 0.4. Reactor PPC/TPC 模式最主要的问题就是每个连接都要创建进程/线程,连接结束后进程/线程就销毁了,这样做其实是很大的浪费。 为了解决这个问题,自然就想到资源复用,不再单独为每个连接创建进程,而是创建一个进程池,将连接分配给进程,一个进程可以处理多个连接的业务。 引入资源池的处理方式后,会引出一个新的问题:进程如何才能高效地处理多个连接的业务? 当一个连接一个进程时,进程可以采用“read -> 业务处理 -> write”的处理流程,如果当前连接没有数据可以读,则进程就阻塞在 read 操作上。 这种阻塞的方式在一个连接一个进程的场景下没有问题,但如果一个进程处理多个连接,进程阻塞在某个连接的 read 操作上,此时即使其他连接有数据可读,进程也无法去处理,很显然这样是无法做到高性能的。 解决这个问题: 最简单的方式是将 read 操作改为非阻塞,然后进程不断地轮询多个连接。这种方式能够解决阻塞的问题,但解决的方式并不优雅。 首先,轮询是要消耗 CPU 的; 其次,如果一个进程处理几千上万的连接,则轮询的效率是很低的。 只有当连接上有数据的时候进程才去处理,这就是 I/O 多路复用技术的来源。I/O 多路复用技术归纳起来有两个关键实现点: 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的实现方式有 select、epoll、kqueue 等。 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。 I/O 多路复用结合线程池,完美地解决了 PPC 和 TPC 的问题,称为Reactor,中文是“反应堆”。来了一个事件Reactor就根据事件类型来调用相应的代码进行处理。也叫Dispatcher,即 I/O 多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。 Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池): 其中 Reactor 负责监听和分配事件 处理资源池负责处理事件 初看 Reactor 的实现是比较简单的,但实际上结合不同的业务场景,Reactor 模式的具体实现方案灵活多变,主要体现在: Reactor 的数量可以变化:一个或多个 Reactor 资源池的数量可以变化:以进程为例,可以是单个进程,也可以是多个进程(线程类似)。 因此,Reactor 模式有这三种典型的实现方案: 单 Reactor 单进程/线程 单 Reactor 多线程 多 Reactor 多进程/线程 以上方案具体选择进程还是线程,更多地是和编程语言及平台相关。 Java 语言一般使用线程(例如,Netty) C 语言使用进程和线程都可以 Nginx(C语言实现) 使用进程 Memcache(C语言实现) 使用线程 0.4.1. 单 Reactor 单进程/线程 以进程为例: 注意,select、accept、read、send 是标准的网络编程 API,dispatch 和“业务处理”是需要完成的操作,其他方案示意图类似。 Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。 Handler 会完成 read -> 业务处理 -> send 的完整业务流程。 单 Reactor 单进程的模式优点: 简单, 没有进程间通信,没有进程竞争,全部都在同一个进程内完成。 缺点: 只有一个进程,无法发挥多核 CPU 的性能;只能采取部署多个系统来利用多核 CPU,但这样会带来运维复杂度,本来只要维护一个系统,用这种方式需要在一台机器上维护多套系统。 Handler 在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈。 因此,单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快速的场景,目前比较著名的开源软件中使用单 Reactor 单进程的是 Redis。 注意:C 语言编写系统的一般使用单 Reactor 单进程,因为没有必要在进程中再创建线程;而 Java 语言编写的一般使用单 Reactor 单线程,因为 Java 虚拟机是一个进程,虚拟机中有很多线程,业务线程只是其中的一个线程而已。 0.4.2. 单 Reactor 多线程 为了克服单 Reactor 单进程/线程方案的缺点,引入多进程/多线程是显而易见的。 主线程中,Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。 Handler 只负责响应事件,不进行业务处理;Handler 通过 read 读取到数据后,会发给 Processor 进行业务处理。 Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的 Handler 处理;Handler 收到响应后通过 send 将响应结果返回给 client。 单 Reator 多线程方案能够充分利用多核多 CPU 的处理能力,但同时也存在下面的问题: 多线程数据共享和访问比较复杂。例如,子线程完成业务处理后,要把结果传递给主线程的 Reactor 进行发送,这里涉及共享数据的互斥和保护机制。 Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。 如果采用多进程,子进程完成业务处理后,将结果返回给父进程,并通知父进程发送给哪个 client,这是很麻烦的事情。因为父进程只是通过 Reactor 监听各个连接上的事件然后进行分配,子进程与父进程通信时并不是一个连接。如果要将父进程和子进程之间的通信模拟为一个连接,并加入 Reactor 进行监听,则是比较复杂的。 采用多线程时,因为多线程是共享数据的,因此线程间通信是非常方便的。虽然要额外考虑线程间共享数据时的同步问题,但这个复杂度比进程间通信的复杂度要低很多。 0.4.3. 多 Reactor 多进程/线程 为了解决单 Reactor 多线程的问题,最直观的方法就是将单 Reactor 改为多 Reactor。 以进程为例: 父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 接收,将新的连接分配给某个子进程。 子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个 Handler 用于处理连接的各种事件。 当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的 Handler)来进行响应。 Handler 完成 read -> 业务处理 -> send 的完整业务流程。 多 Reactor 多进程/线程的方案看起来比单 Reactor 多线程要复杂,但实际实现时反而更加简单,主要原因是: 父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理。 父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。 子进程之间是互相独立的,无须同步共享之类的处理(这里仅限于网络模型相关的 select、read、send 等无须同步共享,“业务处理”还是有可能需要同步共享的)。 开源项目 使用模型 Nginx 多Reactor多进程 Memcache、Netty 多Reactor多线程 Nginx 采用的是多 Reactor 多进程的模式,与标准的多 Reactor 多进程有差异,表现为主进程中仅仅创建了监听端口,并没有创建 mainReactor 来“accept”连接,而是由子进程的 Reactor 来“accept”连接,通过锁来控制一次只有一个子进程进行“accept”,子进程“accept”新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程,更多细节请查阅相关资料或阅读 Nginx 源码。 0.5. Proactor Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作。这里的“同步”指用户进程在执行 read 和 send 这类 I/O 操作的时候是同步的,如果把 I/O 操作改为异步就能够进一步提升性能,这就是异步网络模型 Proactor。 Reactor 可以理解为“来了事件操作系内核通知业务程序来处理”, Proactor 可以理解为“来了事件操作系统内核来处理,处理完了操作系统内核通知业务程序”。 这里的“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件。 Proactor Initiator 负责创建 Proactor 和 Handler,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核。 Asynchronous Operation Processor 负责处理注册请求,并完成 I/O 操作。 Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor。Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理。 Handler 完成业务处理,Handler 也可以注册新的 Handler 到内核进程。 理论上 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)模拟出来的异步模型。

03 高性能架构模式 阅读更多

0.1. 高性能数据库集群 0.1.1. 读写分离 0.1.1.1. 原理 0.1.1.2. 复制延迟 0.1.1.3. 分配机制 0.1.2. 分库分表 0.1.2.1. 业务分库 0.1.2.2. 分表 0.1.2.3. 实现方法 0.2. 高性能NoSQL 0.2.1. K-V存储 0.2.2. 文档数据库 0.2.3. 列式数据库 0.2.4. 全文搜索引擎 0.2.4.1. 全文搜索基本原理 0.2.4.2. 全文搜索使用方式 0.2.5. 技术选型 0.3. 高性能缓存架构 0.3.1. 缓存穿透 0.3.2. 缓存雪崩 0.3.2.1. 更新锁 0.3.2.2. 后台更新 0.3.3. 缓存热点 0.3.4. 缓存方案实现 大部分情况下,做架构设计主要都是基于已有的成熟模式,结合业务和团队的具体情况,进行一定的优化或者调整;即使少部分情况需要进行较大的创新,前提也是需要对已有的各种架构模式和技术非常熟悉。 0.1. 高性能数据库集群 虽然近十年来各种存储技术飞速发展,但关系数据库由于其 ACID 的特性和功能强大的 SQL 查询,目前还是各种业务系统中关键和核心的存储系统,很多场景下高性能的设计最核心的部分就是关系数据库的设计。 海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需要,必须考虑数据库集群的方式来提升性能。 高性能数据库集群: 第一种方式是“读写分离”,其本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力; 第二种方式是“分库分表”,既可以分散访问压力,又可以分散存储压力。 0.1.1. 读写分离 0.1.1.1. 原理 读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是其基本架构图。 读写分离的基本实现是: 数据库服务器搭建主从集群,一主一从、一主多从都可以。 数据库主机负责读写操作,从机只负责读操作。 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。 读写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度:主从复制延迟和分配机制。 0.1.1.2. 复制延迟 以 MySQL 为例,主从复制延迟可能达到 1 秒,如果有大量数据同步,延迟 1 分钟也是有可能的。 主从复制延迟会带来一个问题:如果业务服务器将数据写入到数据库主服务器后立刻(1 秒内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。 解决主从复制延迟有几种常见的方法: 写操作后的读操作指定发给数据库主服务器 读从机失败后再读一次主机 关键业务读写操作全部指向主机,非关键业务采用读写分离 0.1.1.3. 分配机制 将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。 程序代码封装指在代码中抽象一个数据访问层(也称为“中间层封装”),实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离,基本架构是: 程序代码封装的方式具备几个特点: 实现简单,可以根据业务做较多定制化的功能。 每个编程语言都需要自己实现一次,无法通用。 故障情况下,如果主从发生切换,则可能需要所有系统都修改配置并重启。 中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。其基本架构是: 数据库中间件的方式具备的特点是: 能够支持多种编程语言,对业务服务器提供标准 SQL 接口。 数据库中间件要支持完整的 SQL 语法和数据库服务器的协议。 数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求很高。 数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态(向某个测试表写入一条数据,成功的就是主机,失败的就是从机)。 目前开源数据库中间件方案: MySQL Proxy没有GA MySQL Router 奇虎360 Atlas,基于MySQL Proxy实现 0.1.2. 分库分表 读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面: 数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。 数据文件会变得很大,数据库备份和恢复需要耗费很长时间。 数据文件越大,极端情况下丢失数据的风险越高。 基于上述原因,单个数据库服务器存储的数据量不能太大,需要控制在一定的范围内。为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上。 0.1.2.1. 业务分库 业务分库指的是按照业务模块将数据分散到不同的数据库服务器。 虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题: 原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。 原本在同一个数据库中不同的表可以在同一个事务中修改,表分散到不同的数据库中,无法通过事务统一修改。 成本的代价,本来 1 台服务器搞定的事情,现在要 3 台,如果考虑备份,那就是 2 台变成了 6 台。 0.1.2.2. 分表 将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。 单表数据拆分有两种方式:垂直分表和水平分表。示意图如下: 实际架构设计过程中并不局限切分的次数。单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定,并不强制要求单表切分为多表后一定要分散到不同数据库中。原因在于单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升。 分表能够有效地分散存储压力且带来性能提升,但是也会引入复杂性: 垂直分表:适合将表中某些不常用且占了大量空间的列拆分出;垂直分表引入的复杂性主要体现在表操作的数量要增加。 水平分表:适合表行数特别大的表,单表行数超过 5000 万这个数字可以作为参考,关键看表的访问性能。当表的数据量达到千万级别时,就要警觉起来,这很可能是架构的性能瓶颈或者隐患。 水平分表引入更多的复杂性,主要表现在下面几个方面: 路由:某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,常见算法: 范围路由:选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中。范围路由设计的复杂点主要体现在分段大小的选取上,太小则子表多维护负责,太大则单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间。范围路由的优点是可以随着数据的增加平滑地扩充新的表。范围路由的一个比较隐含的缺点是分布不均匀。 Hash路由:选取某个列(或者某几个列组合)的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中。Hash 路由设计的复杂点主要体现在初始表数量的选取上,太多则子表多维护负责,太少则单表依然存在性能问题,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。 配置路由:用一张独立的表来记录路由信息,设计简单,使用灵活,扩充表时,只需要迁移指定的数据,然后修改路由表就可以了。配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大,性能同样可能成为瓶颈。 join操作:数据分散在多个表中,如果需要与其他表进行 join 查询,需要在业务代码或者数据库中间件中进行多次 join 查询,然后将结果合并。 count()操作:物理上数据分散到多个表中,但某些业务逻辑上还是将这些表当作一个表来处理。常用的处理方式: count()相加:在业务代码或者数据库中间件中对每个表进行 count() 操作,然后将结果相加。实现简单但是性能低下。 记录数表:新建一张表,如表名为“记录数表”,包含 table_name、row_count 两个字段,每次插入或者删除子表数据成功后,都更新“记录数表”。性能大大优于上一个方法,但是复杂度增加,要操作多个表,且不能在一个事物中完成处理,最终导致数据不一致,记录数据表也会增加写的压力。 定时更新记录数表:count()相加与记录数表的结合,定时通过 count() 相加计算表的记录数,然后更新记录数表中的数据。 order by操作:数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。 0.1.2.3. 实现方法 分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。 读写分离实现时只要识别 SQL 操作是读操作还是写操作,通过简单的判断 SELECT、UPDATE、INSERT、DELETE 几个关键字就可以做到 分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。 0.2. 高性能NoSQL 关系数据库非常成熟,强大的 SQL 功能和 ACID 的属性,但关系数据库存在如下缺点: 关系数据库存储行记录,无法存储数据结构 关系数据库的schema扩展不方便:因为表结构 schema 是强约束,操作不存在的列会报错,业务变化时扩充列也比较麻烦,需要执行 DDL语句修改,可能会长时间锁表。 关系数据库在大数据场景下I/O较高:因为即使只针对其中某一列进行运算,也会将整行数据从存储设备读入内存。 关系数据库的全文搜索功能比较弱:只能使用like全表扫描 针对上述问题,诞生了不同的 NoSQL 解决方案,它们在某些应用场景下比关系数据库表现好。 但 NoSQL 方案带来的优势,本质上是牺牲 ACID 中的某个或者某几个特性,因此 NoSQL 也不是银弹,而是 SQL 的补充,NoSQL != No SQL,而是 NoSQL = Not Only SQL。 常见的 NoSQL 方案分为 4 类: K-V存储:解决关系数据库无法存储数据结构的问题,以 Redis 为代表。 文档数据库:解决关系数据库强 schema 约束的问题,以 MongoDB 为代表。 列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表。 全文搜索引擎:解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表。 0.2.1. K-V存储 Key-Value 存储,其中 Key 是数据的标识,和关系数据库中的主键含义一样,Value 就是具体的数据。 Redis 是 K-V 存储的典型代表,一款开源的高性能 K-V 缓存和存储系统。Redis 的 Value 是具体的数据结构,包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog,常被称为数据结构服务器。Redis 的缺点不支持完整的 ACID 事务,Redis 虽然提供事务功能,但 Redis 的事务和关系数据库的事务不可同日而语,Redis 的事务只能保证隔离性和一致性(I 和 C),无法保证原子性和持久性(A 和 D)。 在设计方案时,需要根据业务特性和要求来确定是否可以用 Redis,而不能因为 Redis 不遵循 ACID 原则就直接放弃。 0.2.2. 文档数据库 文档数据库最大的特点就是 no-schema,可以存储和读取任意的数据。 目前绝大部分文档数据库存储的数据格式是 JSON(或者 BSON),因为 JSON 数据是自描述的,无须在使用前定义字段,读取一个 JSON 中不存在的字段也不会导致 SQL 那样的语法错误。 文档数据库no-schema特性带来的优势: 新增字段简单:业务上新增字段,程序代码直接读写即可 历史数据不会出错:历史数据无新增字段直接返回空值,代码进行兼容处理即可 可以很容易存储复杂数据:JSON能够描述复杂的数据结构 文档数据库的这个特点,特别适合电商和游戏这类的业务场景。因为不同商品的属性差别很大,即使同类商品也有不同的属性。 文档数据库 no-schema 的特性带来的这些优势也是有代价的,最主要的代价就是不支持事务。另外一个缺点就是无法实现关系数据库的 join 操作。 在设计方案时,某些对事务要求严格的业务场景是不能使用文档数据库的。 0.2.3. 列式数据库 列式数据库就是按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”,因为关系数据库是按照行来存储数据的。 行式存储的优势: 业务同时读取多个列时效率高,因为这些列都是按行存储在一起的,一次磁盘操作就能够把一行数据中的各个列都读取到内存中。 能够一次性完成对一行中的多个列的写操作,保证了针对行数据写操作的原子性和一致性;否则如果采用列存储,可能会出现某次写操作,有的列成功了,有的列失败了,导致数据不一致。 行式存储的优势是在特定的业务场景下才能体现,如果不存在这样的业务场景,那么行式存储的优势也将不复存在,甚至成为劣势,典型的场景就是海量数据进行统计。 列式存储的优势: 节省I/O,只需要某一列时只读取该列 更高的存储压缩比,能够节省更多的存储空间(行式存储3:1到5:1;列式存储8:1到30:1),因为单列的数据相似度比行来说更高,能够达到更高的压缩率。 需要频繁地更新多个列的场景下,列式存储的优势就变成了劣势,因为列式存储将不同列存储在磁盘上不连续的空间,导致更新多个列时磁盘是随机写操作;而行式存储时同一行多个列都存储在连续的空间,一次磁盘写操作就可以完成,列式存储的随机写效率要远远低于行式存储的写效率。列式存储高压缩率在更新场景下也会成为劣势,因为更新时需要将存储数据解压后更新,然后再压缩,最后写入磁盘。 在设计方案时,一般将列式存储应用在离线的大数据分析和统计场景中,这种场景主要针对部分列、单列进行操作,且数据写入后无需更新或删除。 0.2.4. 全文搜索引擎 关系数据库通过索引实现快速查询,但在全文搜索场景下,索引无能为力,主要体现在: 全文搜索的条件随意排列组合,如果通过索引来满足,则索引的数量会非常多。 全文搜索的模糊匹配方式,索引无法满足,只能用 like 查询,而 like 查询是整表扫描,效率非常低。 0.2.4.1. 全文搜索基本原理 全文搜索引擎的技术原理被称为“倒排索引”(Inverted index),是一种索引方法,其基本原理是建立单词到文档的索引。 被称为“倒排”索引,是和“正排“索引相对的,“正排索引”的基本原理是建立文档到单词的索引。 正排索引: 文章ID 文章名称 文章内容 1 敏捷架构设计原则 架构、设计、架构师 2 Java编程必知比会 Java、编程、面向对象、类、架构、设计 3 面向对象葵花宝典是什么 设计、模式、对象、类、Java 正排索引适用于根据文档名称来查询文档内容。(注:文章内容仅为示范,文章内容实际上存储的是几千字的内容。) 倒排索引: 单词|文档ID列表| 架构|1,2 设计|1,2,3 Java|2,3 倒排索引适用于根据关键词来查询文档内容。(注:表格仅为示范,不是完整的倒排索引表格,实际上的倒排索引有成千上万行,因为每个单词就是一个索引。) 0.2.4.2. 全文搜索使用方式 全文搜索引擎的索引对象是单词和文档,而关系数据库的索引对象是键和行。 为了让全文搜索引擎支持关系型数据的全文搜索,需要将关系型数据转换为文档数据。目前常用的转换方式是将关系型数据按照对象的形式转换为 JSON 文档,然后将 JSON 文档输入全文搜索引擎进行索引。 全文搜索引擎能够基于 JSON 文档建立全文索引,然后快速进行全文搜索。 Elastcisearch 是分布式的文档存储方式。它能存储和检索复杂的数据结构——序列化成为 JSON 文档——以实时的方式。在 Elasticsearch 中,每个字段的所有数据都是默认被索引的。即每个字段都有为了快速检索设置的专用倒排索引。而且,不像其他多数的数据库,它能在相同的查询中使用所有倒排索引,并以惊人的速度返回结果。 0.2.5. 技术选型 关系型和NoSQL数据库的选型。考虑如下指标: 数据量 并发量 实时性 一致性要求 读写分布和类型 安全运维性等 根据这些指标,软件系统可分成如下类别: 管理型系统,如运营类系统,首选关系型。 大流量系统,如电商单品页的某个服务,后台选关系型,前台选内存型。 日志型系统,原始数据选列式,日志搜索选倒排索引。 搜索型系统,指站内搜索,非通用搜索,如商品搜索,后台选关系型,前台选倒排索引。 事务型系统,如库存、交易、记账,选关系型+缓存+一致性协议,或新型关系数据库。 离线计算,如大量数据分析,首选列式,关系型也可以。 实时计算,如实时监控,可以选时序数据库,或列式数据库。 0.3. 高性能缓存架构 通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的,典型的场景有: 需要经过复杂运算后得出的数据,存储系统无能为力 读多写少的数据,存储系统有心无力 缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。 缓存能够带来性能的大幅提升,以 Memcache 为例,单台 Memcache 服务器简单的 key-value 查询能够达到 TPS 50000 以上。 缓存虽然能够大大减轻存储系统的压力,但同时也给架构引入了更多复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃。 0.3.1. 缓存穿透 缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况: 存储数据不存在:当数据确实不存在时,每次都要先查缓存再查存储,通常,业务上读取不存在的数据的请求量并不会太大,但出现异常攻击会拖跨存储系统,解决方案是如果数据确实不存在,在缓存中设置一个默认值,这样后续查询都读取默认值。 缓存数据生成耗费大量时间或者资源:数据在存储系统中存在,但是生成换成数据需要消耗较长时间或者消耗大量资源。如果在业务访问时缓存失效,那么访问压力都集中在存储系统上。 具体的场景有: 分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据。 通常情况下,用户不会从第 1 页到最后 1 页全部看完,一般用户访问集中在前 10 页,因此第 10 页以后的缓存过期失效的可能性很大。 竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历,从第 1 页到最后 1 页全部都会读取,此时很多分页缓存可能都失效了。 由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(order by limit 操作),因此爬虫会将整个数据库全部拖慢。 通常的应对方案: 识别爬虫然后禁止访问,但这可能会影响 SEO 和推广; 做好监控,发现问题后及时处理,爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。 0.3.2. 缓存雪崩 缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。 当缓存过期被清除后,业务系统需要重新生成缓存, 因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。 而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。 由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。 这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。 缓存雪崩的常见解决方法有两种:更新锁机制和后台更新机制。 0.3.2.1. 更新锁 对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 对于采用分布式集群的业务系统,由于存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。 0.3.2.2. 后台更新 由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。 后台定时机制需要考虑一种特殊的次使用mysql的这个缓冲来做查询使用的话场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。 解决的方式有两种: 后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。 适用场景: 相比更新锁机制要简单一些。 单机多线程的场景 分布式集群的场景 业务刚上线的时候进行缓存预热 缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。 0.3.3. 缓存热点 虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。 缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。缓存的数据是一样的,通过在缓存的 key 里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。 缓存副本设计有一个细节需要注意,不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。 0.3.4. 缓存方案实现 由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式或者独立的中间件来实现。

02 架构设计流程 阅读更多

0.1. 识别复杂度 0.2. 设计备选方案 0.3. 评估和选择备选方案 0.4. 详细方案设计 0.1. 识别复杂度 架构设计的本质目的是为了解决软件系统的复杂性,所以在设计架构时,首先就要分析系统的复杂性。 架构的复杂度主要来源于“高性能”“高可用”“可扩展”等几个方面,在具体判断复杂性的时候,不能生搬硬套,认为任何时候架构都必须同时满足这三方面的要求。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个,出现同时需要解决三个或者三个以上的复杂度,要么说明这个系统之前设计的有问题,要么可能就是架构师的判断出现了失误,即使真的认为要同时满足这三方面的要求,也必须要进行优先级排序。 将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。 对于同一个复杂度问题,软件系统的方案可以有多个,总是可以挑出综合来看性价比最高的方案。如果决定要推倒重来,这个新的方案也必须能够同时解决已经被解决的复杂度问题,一般来说能够达到这种理想状态的方案基本都是依靠新技术的引入。 识别复杂度对架构师来说是一项挑战,因为原始的需求中并没有哪个地方会明确地说明复杂度在哪里,需要在理解需求的基础上进行分析,如果经验不足,那只能采取“排查法”,从不同的角度逐一进行分析。 0.2. 设计备选方案 成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整。 常见系统性能量级: Nginx负载均衡性能是3万左右 Memcached读取性能是5万左右 Kafka号称百万级别 Zookeeper读写2万左右 HTTP请求2万左右 新技术层出不穷,经过时间考验,已经被各种场景验证过的成熟技术更多。例如: 高可用:主备方案、集群方案, 高性能:负载均衡、多路复用, 可扩展:分层、插件化, 有了明确的目标后,按图索骥就能够找到可选的解决方案。只有当这种方式完全无法满足需求的时候,才会考虑进行方案的创新。 事实上方案的创新绝大部分情况下也都是基于已有的成熟技术 NoSQL:KV存储与数据库索引是类似的,Memcache 只是把数据库的索引独立出来做成了一个缓存系统 Hadoop 大文件存储方案,基础是集群方案 + 数据复制方案 Docker 虚拟化,基础是 LXC(Linux Containers) LevelDB 的文件存储结构是 Skip List 新技术都是在现有技术的基础上发展起来的,现有技术又来源于先前的技术。 基于已有的技术或者架构模式进行组合调整,大部分情况下能得到需要的方案,并不意味着架构设计很简单。 可选的模式很多,组合的方案更多,一个问题的解决方案就有多个; 在组合的方案上进行一些创新,解决方案会更多。 因此,设计最终的方案,并不容易,这个阶段容易犯错: 第一种常见错误:设计最优秀的方案(根据简单合适原则挑选适合业务、团队和技术的方案) 第二种常见错误:只做一个方案(备选方案3个为宜,相互差异明显,且不局限在熟悉的技术中) 第三种常见错误:备选方案过于详细(备选阶段关注技术选型而不是技术细节,选型差异要比较明显) 0.3. 评估和选择备选方案 挑选出最终的方案也是一个很大的挑战,主要原因有: 每个方案都是可行的 没有哪个方案是完美的 评价标准主观性比较强 实践中采取下面几种指导思想: 最简派 最牛派 最熟派 领导派 最合适的做法:360度环评,列出需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。 常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。 在评估这些质量属性时,需要遵循合适简单原则,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。考虑小概率事件时,需要遵循演化原则,避免过度设计一步到位。 完成方案的 360 度环评后,基于评估结果整理出 360 度环评表,一目了然地看到各个方案的优劣点。此时不要采取错误的选择法: 数量对比法:简单地看哪个方案的优点多就选哪个。 加权法:每个质量属性给一个权重。 正确做法是按优选择,综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。 0.4. 详细方案设计 详细方案设计就是将方案涉及的关键技术细节给确定下来,有一些技术点和备选方案类似。 详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。这种情况可以通过下面方式有效地避免: 不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。 通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度,方案本身的复杂度越高,某个细节推翻整个方案的可能性就越高,适当降低复杂性,可以减少这种风险。 如果方案本身就很复杂,那就采取设计团队的方式来进行设计,博采众长,汇集大家的智慧和经验,防止只有 1~2 个架构师可能出现的思维盲点或者经验盲区。

01 架构、复杂度与三原则 阅读更多

0.1. 架构定义 0.1.1. 系统与子系统 0.1.2. 模块与组件 0.1.3. 框架与架构 0.1.4. 架构 0.2. 架构的目的 0.2.1. 架构设计的误区 0.2.2. 架构设计的真正目的 0.3. 高性能 0.3.1. 单机复杂度 0.3.2. 集群复杂度 0.3.2.1. 任务分配 0.3.2.2. 任务分解 0.4. 高可用 0.4.1. 计算高可用 0.4.2. 存储高可用 0.4.3. 高可用状态决策 0.4.3.1. 独裁式 0.4.3.2. 协商式 0.4.3.3. 民主式 0.5. 可扩展性 0.5.1. 预测变化 0.5.2. 应对变化 0.5.2.1. 变化层VS稳定层 0.5.2.2. 抽象层VS实现层 0.6. 低成本、安全、规模 0.6.1. 低成本 0.6.2. 安全 0.6.2.1. 功能安全 0.6.2.2. 架构安全 0.6.3. 规模 0.7. 架构设计三原则 0.7.1. 合适原则 0.7.2. 简单原则 0.7.3. 演化原则 架构设计相关的特性: 架构设计的思维和程序设计的思维差异很大。架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现。 架构设计没有体系化的培训和训练机制。 程序员对架构设计的理解存在很多误区。 0.1. 架构定义 0.1.1. 系统与子系统 系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。 关联:系统是由一群有关联的个体组成的,没有关联的个体堆在一起不能成为一个系统。 规则:系统内的个体需要按照指定的规则运作,而不是单个个体各自为政。规则规定了系统内个体分工和协作的方式。 能力:系统能力与个体能力有本质的差别,系统能力不是个体能力之和,而是产生了新的能力。 子系统是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。 0.1.2. 模块与组件 软件模块(Module)是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。 模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已。 从逻辑的角度来拆分系统后,得到的单元就是“模块”;划分模块的主要目的是职责分离。 从物理的角度来拆分系统后,得到的单元就是“组件”;划分组件的主要目的是单元复用,组件是独立独立可替换的。 0.1.3. 框架与架构 软件框架(Software framework)通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。 框架是组件规范 框架提供基础功能的产品 软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。 框架(Framework)关注的是“规范”,架构(Architecture)关注的是“结构”。 0.1.4. 架构 软件架构指软件系统的顶层结构。 架构是顶层设计; 框架是面向编程或配置的半成品; 组件是从技术维度上的复用; 模块是从业务维度上职责的划分; 系统是相互协同可运行的实体。 0.2. 架构的目的 0.2.1. 架构设计的误区 因为架构很重要,所以要做架构设计 不是每个系统都要做架构设计吗 公司流程要求系统开发过程中必须有架构设计 为了高性能、高可用、可扩展,所以要做架构设计 0.2.2. 架构设计的真正目的 整个软件技术发展的历史,其实就是一部与“复杂度”斗争的历史。架构也是为了应对软件系统复杂度而提出的一个解决方案,架构设计的主要目的是为了解决软件系统复杂度带来的问题。 架构过程: 通过熟悉和理解需求,识别系统复杂性所在的地方,然后针对这些复杂点进行架构设计。 架构设计并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特点,而是要识别出复杂点然后有针对性地解决问题。 理解每个架构方案背后所需要解决的复杂点,然后才能对比自己的业务复杂点,参考复杂点相似的方案。 复杂度的六个来源: 高性能 高可用 可扩展性 低成本 安全 规模 0.3. 高性能 软件系统中高性能带来的复杂度主要体现在两方面: 一方面是单台计算机内部为了高性能带来的复杂度; 另一方面是多台计算机集群为了高性能带来的复杂度。 0.3.1. 单机复杂度 计算机内部复杂度最关键的地方就是操作系统,计算机性能的发展本质上是由硬件发展驱动的,尤其是 CPU 的性能发展。而将硬件性能充分发挥出来的关键就是操作系统,所以操作系统本身也是随硬件的发展而发展的,操作系统是软件系统的运行环境,操作系统的复杂度直接决定了软件系统的复杂度。 操作系统和性能最相关的就是进程和线程。 进程:用进程来对应一个操作系统执行的任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。 多进程:为了达到多进程并行运行的目的,采取了分时的方式,即把 CPU 的时间分成很多片段,每个片段只能执行某个进程中的指令。 进程间通信:为了解决进程在运行时相互通信的问题,进程间通信的各种方式被设计出来了,包括管道、消息队列、信号量、共享存储等。 多线程:多进程让多任务能够并行处理任务,但本身还有缺点,单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求是严格按照时间顺序来执行的,也需要并行处理。为了解决这个问题发明了线程,线程是进程内部的子任务,但这些子任务都共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。 多核处理器:多进程多线程虽然让多任务并行处理的性能大大提升,但本质上还是分时系统,并不能做到时间上真正的并行。解决这个问题的方式显而易见,就是让多个 CPU 能够同时执行计算任务,从而实现真正意义上的多任务并行。 目前这样的解决方案有 3 种: SMP(Symmetric Multi-Processor,对称多处理器结构),最常见,主流的多核处理器方案 NUMA(Non-Uniform Memory Access,非一致存储访问结构) MPP(Massive Parallel Processing,海量并行处理结构) 操作系统发展到现在,如果要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。 在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合,这个过程同样很复杂。例如,下面的系统都实现了高性能,但是内部实现差异很大: Nginx 可以用多进程也可以用多线程 JBoss 采用的是多线程 Redis 采用的是单进程 Memcache 采用的是多线程 0.3.2. 集群复杂度 让多台机器配合起来达到高性能的目的,是一个复杂的任务,常见的方式有: 任务可以指完整的业务处理,也可以指某个具体的任务。 任务分配:每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。 任务分解:业务越来越复杂,单台机器处理的性能会越来越低。为了能够继续提升性能,采用任务分解。 0.3.2.1. 任务分配 增加一个任务分配器,可以是硬件(F5、交换机)、软件(LVS)、负载均衡软件(Nginx、HAProxy)、自己开发的系统。 任务分配器与业务服务器之间的连接和交互。 任务分配器增加分配算法(轮询、权重、负载)。 业务量继续提升,需要增加任务分配器的数量。 任务分配器增加为多台,这样需要将不同的用户请求分配到不同的任务分配器上(DNS轮询、智能DNS、CDN、GSLB全局负载均衡)。 任务分配器和业务服务器之间从一对多变成多对多的网状结构。 业务服务器继续扩增,状态管理和故障处理复杂度更大。 0.3.2.2. 任务分解 微服务架构。 通过任务分配的方式,能够突破单台机器处理性能的瓶颈,通过增加更多的机器来满足业务的性能需求,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。 通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),任务分解能够提升性能的主要原因是: 简单的系统更容易做到高性能:系统的功能越简单,影响性能的点就越少,就更加容易进行有针对性的优化。 可以针对单个任务进行扩展:当各个逻辑任务分解到独立的子系统后,整个系统的性能瓶颈更加容易发现,而且发现后只需要针对有瓶颈的子系统进行性能优化或者提升,不需要改动整个系统,风险会小很多。 最终决定业务处理性能的还是业务逻辑本身,业务逻辑本身没有发生大的变化下,理论上的性能是有一个上限的,系统拆分能够让性能逼近这个极限,但无法突破这个极限。因此,任务分解带来的性能收益是有一个度的,并不是任务分解越细越好,而对于架构设计来说,如何把握这个粒度就非常关键了。 0.4. 高可用 系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。 本质上都是通过“冗余”来实现高可用。高可用的“冗余”解决方案,单纯从形式上来看,和高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。 通过冗余增强了可用性,但同时也带来了复杂性。 0.4.1. 计算高可用 计算的特点是无论从哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的,所以将计算从一台机器迁移到另一台对业务没有影响。 需要增加一个任务分配器 任务分配器和真正的业务服务器之间有连接和交互 任务分配器需要增加分配算法(主备【冷备、温备、热备】、主主、多主多倍【2主2备、4主0备】) 0.4.2. 存储高可用 对于需要存储数据的系统来说,整个系统的高可用设计关键点和难点就在于“存储高可用”。存储与计算相比,有一个本质上的区别:将数据从一台机器搬到到另一台机器,需要经过线路进行传输。 线路的传输是有延迟的,速度在毫秒级别,距离越长延迟越高,各种异常情况(传输中断、丢包、拥塞)会导致延迟更高,对于高可用系统来说,意味着整个系统在某个时间点撒谎嗯,数据肯定是不一致的。。按照“数据 + 逻辑 = 业务”这个公式来套的话,数据不一致,即使逻辑一致,最后的业务表现就不一样了。 如果完全不做冗余,系统的整体高可用又无法保证,所以存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。 分布式领域中著名的 CAP 定理,从理论上论证了存储高可用的复杂度。存储高可用不可能同时满足“一致性、可用性、分区容错性”,最多满足其中两个,这就要求我们在做架构设计时结合业务进行取舍。 0.4.3. 高可用状态决策 无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。 如果状态决策本身都有错误或有偏差,那么后续的任何行动和处理都没有意义,但是通过冗余来实现的高可用系统,状态决策本质上不可能做到完全正确。 常见的几种决策方式包括: 独裁式 协商式 民主式 0.4.3.1. 独裁式 存在一个独立的决策主体,称为“决策者”,负责收集信息然后进行决策;所有冗余的个体,称为“上报者”,都将状态信息发送给决策者。 独裁式的决策方式不会出现决策混乱的问题,因为只有一个决策者,但是当决策者本身故障时,整个系统就无法实现准确的状态决策。如果决策者本身又做一套状态决策,那就陷入一个递归的死循环了。 0.4.3.2. 协商式 两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策。这个架构的基本协商规则可以设计成: 2 台服务器启动时都是备机。 2 台服务器建立连接。 2 台服务器交换状态信息。 某 1 台服务器做出决策,成为主机; 另一台服务器继续保持备机身份。 协商式决策的架构不复杂,规则也不复杂,其难点在于,如果两者的信息交换出现问题(比如主备连接中断),此时状态决策应该怎么做。 如果备机在连接中断的情况下认为主机故障,则备机需要升级为主机,但实际上此时主机并没有故障,那么系统就出现了两个主机 如果备机在连接中断的情况下不认为主机故障,则此时如果主机真的发生故障,那么系统就没有主机了 如果为了规避连接中断对状态决策带来的影响,可以增加更多的连接(双连接、三连接),这样可以降低连接中断带来的影响,但不能彻底解决,同时会引入多条连接信息(连接之间传递的信息不同)之间的取舍问题,这个问题无解 协商式状态决策在某些场景总是存在一些问题的。 0.4.3.3. 民主式 多个独立的个体通过投票的方式来进行状态决策。 民主式决策和协商式决策比较类似,其基础都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“多数取胜”的规则来确定最终的状态。不同点在于民主式决策比协商式决策要复杂得多,ZooKeeper 的选举算法 Paxos,绝大部分人都看得云里雾里,更不用说用代码来实现这套算法了。除了算法复杂,民主式决策还有一个固有的缺陷:脑裂。 脑裂的根本原因是,原来统一的集群因为连接中断,造成了两个独立分隔的子集群,每个子集群单独进行选举,于是选出了 2 个主机。 为了解决脑裂问题,民主式决策的系统一般都采用“投票节点数必须超过系统总节点数一半”规则来处理。这种方式虽然解决了脑裂问题,但同时降低了系统整体的可用性,系统因为节点故障导致存活节点数少于一半,此时系统不会选出主节点,整个系统就相当于宕机了,尽管此时仍然有正常运行的节点。 0.5. 可扩展性 可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。 在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;设计模式更是将可扩展性做到了极致。 设计具备良好可扩展性的系统,有两个基本条件: 正确预测变化 完美封装变化 0.5.1. 预测变化 “唯一不变的是变化”,按照这个标准衡量,架构师每个设计方案都要考虑可扩展性。预测变化的复杂性在于: 不能每个设计点都考虑可扩展性 不能完全不考虑扩展性 所有的预测都存在出错的可能性 如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准,更多是靠经验、直觉。 0.5.2. 应对变化 预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦。 微服务架构中的各层进行封装和隔离也是一种应对变化的解决方式。 0.5.2.1. 变化层VS稳定层 第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。 无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。 无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。 系统需要拆分出变化层和稳定层(如何拆分) 需要设计变化层和稳定层之间的接口(稳定层接口越稳定越好,变化层接口从差异中找到共同点) 0.5.2.2. 抽象层VS实现层 第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。 抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。 以设计模式的“装饰者”模式来分析,下面是装饰者模式的类关系图。 图中的 Component 和 Decorator 就是抽象出来的规则,这个规则包括几部分: Component 和 Decorator 类。 Decorator 类继承 Component 类。 Decorator 类聚合了 Component 类。 这个规则一旦抽象出来后就固定了,不能轻易修改。 规则引擎和设计模式类似,都是通过灵活的设计来达到可扩展的目的,但“灵活的设计”本身就是一件复杂的事情。 0.6. 低成本、安全、规模 0.6.1. 低成本 当架构方案涉及几台或者十几台服务器时,一般情况下成本并不是重点关注的目标, 当架构方案涉及几百上千甚至上万台服务器时,成本就会变成一个非常重要的架构设计考虑点。 设计“高性能”“高可用”的架构时,通用的手段都是增加更多服务器;而低成本则需要减少服务器的数量才能达成低成本的目标。 低成本本质上是与高性能和高可用冲突的,低成本很多时候不是架构设计的首要目标,而是架构设计的附加约束。 设定一个成本目标, 根据高性能、高可用的要求设计方案, 评估方案是否能满足成本目标, 如果不行,重新设计架构; 如果无论如何都无法设计出满足成本要求的方案,那只能找老板调整成本目标了。 低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标。 这里的“创新”既包括开创一个全新的技术领域,也包括引入新技术,如果没有找到能够解决自己问题的新技术,那么就真的需要自己创造新技术了。 新技术的例子: NoSQL(Memcache、Redis 等)为了解决关系型数据库无法应对高并发访问带来的访问压力。 全文搜索引擎(Sphinx、Elasticsearch、Solr)为了解决关系型数据库 like 搜索的低效的问题。 Hadoop 为了解决传统文件系统无法应对海量数据存储和计算的问题。 业界的例子: Facebook解决PHP低效问题,研发HipHop PHP(将 PHP 语言翻译为 C++ 语言执行)、HHVM(将 PHP 翻译为字节码然后由虚拟机执行)。 新浪微博将传统的 Redis/MC + MySQL 方式,扩展为 Redis/MC + SSD Cache + MySQL 方式,SSD Cache 作为 L2 缓存使用,既解决了 MC/Redis 成本过高,容量小的问题,也解决了穿透 DB 带来的数据库访问压力。 Linkedin 为了处理每天 5 千亿的事件,开发了高效的 Kafka 消息系统。 Ruby on Rails 改为 Java。 Lua + redis 改为 Go 语言实现。 引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来(小公司引入新技术达到低成本) 创造新技术的主要复杂度在于需要自己去创造全新的理念和技术,并且新技术跟旧技术相比,需要有质的飞跃(大公司创造新技术达到低成本) 0.6.2. 安全 从技术的角度来讲,安全可以分为两类: 一类是功能上的安全, 一类是架构上的安全。 0.6.2.1. 功能安全 常见的 XSS 攻击、CSRF 攻击、SQL 注入、Windows 漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机,功能安全其实就是“防小偷”。 从实现的角度来看,功能安全更多地是和具体的编码相关,与架构关系不大。开发框架会内嵌常见的安全功能,但是开发框架本身也可能存在安全漏洞和风险。 所以功能安全是一个逐步完善的过程,而且往往都是在问题出现后才能有针对性的提出解决方案,我们永远无法预测系统下一个漏洞在哪里,也不敢说自己的系统肯定没有任何问题。 换句话讲,功能安全其实也是一个“攻”与“防”的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计的时候一劳永逸地解决。 0.6.2.2. 架构安全 如果说功能安全是“防小偷”,那么架构安全就是“防强盗”。 架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。 传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。 防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。 互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。 0.6.3. 规模 规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。常见的规模带来的复杂度有: 功能越来越多,系统复杂度指数级上升 数据越来越多,系统复杂度发生质变(以 MySQL 为例,单表的数据因不同的业务和应用场景有不同的最优值,但都有一定的限度,一般推荐在 5000 万行左右。) 0.7. 架构设计三原则 对于编程来说,本质上是确定的,对于架构来说,本质上是不确定的。相比编程来说,架构设计并没有像编程语言那样的语法来进行约束,更多的时候是面对多种可能性时进行选择。 合适原则、简单原则、演化原则,架构设计时遵循这几个原则,有助于做出最好的选择。 0.7.1. 合适原则 合适优于业界领先。 没那么多人,却想干那么多活,是失败的第一个主要原因。 没有那么多积累,却想一步登天,是失败的第二个主要原因。 没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因。 真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。 0.7.2. 简单原则 简单优于复杂。 “复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”。软件领域的复杂性体现在两个方面: 结构的复杂性:组件数量更多,同时组件之间的关系更加复杂。组件越多,就越有可能其中某个组件出现故障,某个组件改动,会影响关联的所有组件,定位一个复杂系统中的问题总是比简单系统更加困难。 逻辑的复杂性:组件虽少,但是单个组件承担了太多的功能,采用了复杂的算法,难以理解,进而导致难以实现、难以修改,并且出了问题难以快速解决。 架构设计时如果简单的方案和复杂的方案都可以满足需求,最好选择简单的方案,遵循KISS(Keep It Simple,Stupid!)原则进行架构设计。 0.7.3. 演化原则 演化优于一步到位。 从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。然而,字面意思上的相似性却掩盖了一个本质上的差异:建筑一旦完成(甚至一旦开建)就不可再变,而软件却需要根据业务的发展不断地变化! 对于建筑来说,永恒是主题;而对于软件来说,变化才是主题,软件架构需要根据业务发展不断变化。软件架构设计更类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大: 首先,生物要适应当时的环境。 其次,生物需要不断地繁殖,将有利的基因传递下去,将不利的基因剔除或者修复。 再次,当环境变化时,生物要能够快速改变以适应环境变化;如果生物无法调整就被自然淘汰;新的生物会保留一部分原来被淘汰生物的基因。 软件架构设计同样是类似的过程: 首先,设计出来的架构满足当时的业务需要。 其次,架构不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。 再次,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因)却可以在新架构中延续。 在进行架构设计时应该: 认真分析当前业务的特点, 明确业务面临的主要问题, 设计合理的架构, 快速落地以满足业务需要, 在运行过程中不断完善架构,不断随着业务演化架构。