[DDIA]Chap1可靠、可扩展且易维护系统

当前很多应用都是data-intensive型,而非compute-intensive。如

  1. 存储数据后供其他应用访问(Database)
  2. 记录一些比较重的操作的结果用以加速获取操作(Cache)
  3. 支持用户通过关键词或者过滤器检索数据(search index)
  4. 通过发消息到进程以异步化(stream processing)
  5. 定期获取大量数据(batch processing)

这些(data-intensive)应用各自拥有各自的特性,我们需要根据他们提供的能力来设计我们的应用,甚至在当前没有符合我们使用场景的应用组件时,构建相应的组件。

关于数据系统的思考

我们是做业务系统开发的,为什么我们需要思考data-intensive应用?
在过去,一个业务系统使用的数据应用可能比较单一,一个数据库就可以完成全部数据的处理,表的join、唯一性控制、存储优化、事务等等。现在来看,各种数据应用所承担的责任越来越精专:rdbms、nosql、mq、cache、es,我们选择每个数据应用时都要考虑这些应用的专长与缺陷。以至于业务系统开发时我们不得不去思考:如何保证缓存与主存的一致性、分布式事务方案选型、使用MQ异步化、高可用及降级。
很多方面都会影响一个数据系统的设计,包括但不限于:开发者经验、依赖的遗留系统、交付周期、组织对各种风险的忍受力及其他限制。

这里主要关注三个层面:

  • 可靠性
  • 伸缩性
  • 可维护性

可靠性

软件系统关于可靠性的期望包括:

  • 符合用户预期的表现
  • 是否能支持用户的异常行为(输入、操作等)
  • 高负载、大数据量的情况下对要求的用户场景是否能表现良好
  • 系统可以防止越权(unauthorized access)和滥用(abuse)
    简言之:无论如何都能正常表现(continuing to work correctly, even when things go wrong)
    正常情况下的符合预期是软件系统最基本保证的。当系统出现问题时,我们称之fault,对于能够抵御错误的系统我们称之容错能力。但是并非所有的问题都可以被抵销或避免,比如说地球毁灭。所以我们所说的容错也指特定类型。这里还要区分faultfailure
  • fault:也是在spec中描述的。fault不能百分之百避免,所以需要系统能够处理可预知的fault,防止fault导致failure
  • failure:在意料之外的,当failure出现时,系统将不再对外提供服务

和直觉相反的是,对于容错能力比较强的系统是经常触发一些fault(断网演练、随机切断进程等)的系统,只有通过触发fault才能够检验容错机制是否可靠,很多时候系统问题来自于对于异常的处理之上。就像Netflix引入chaos monkey随机产生问题来验证系统的容错能力。

硬件错误

据报告称一个硬件发生问题的平均时长为10-50年,对于一个10000块磁盘的集群来说,按概率平均一天就会有一块磁盘发生问题。
对于这类问题,常规解决方案就是冗余,比如说通过RAID卡、备用电源、可热替换CPU等方式,这些方法不能避免硬件发生问题,而是在发生问题时能够让运行其上的系统无感知硬件问题以长时间运行。
随着集群规模的扩大,硬件故障发生的越来越频繁,也越来越倾向使用软件或系统设计方式来解决硬件故障,如数据中心、多机房、软负载等。

软件错误

硬件问题大多随机发生,但是大面积的硬件问题同时爆发确实小概率事件。软件系统异常则更难预测,且通常一个软件问题可能会关联其他系统、节点服务,导致大面积不可用。如:

  • 由于软件bug某个特定输入导致所有节点崩溃(如之前Stack Overflow发生的regex解析cpu耗时过高)。
  • 失控的线程消耗过多的系统资源
  • 依赖服务耗时过多拖垮当前应用,或者返回异常信息对当前应用造成影响
  • 雪崩。一个小的问题引发一连串的故障,从而导致整个服务不可用

通常导致这些问题的bug在系统中隐藏很久,当特定的实际时被触发。一个系统运行正常是基于一些假设,通常在这些假定的环境下系统是不会发生问题的,而且在一段时间内这些假定也是成立的,不过当这些假定不成立时,就可能发生一些不可知的情况。
没有一个好的方案避免上述所有问题,不过一些小细节可以带来些助益:

  1. 仔细思考与系统交互的环境与场景
  2. 完备的测试
  3. process isolation
  4. 进程能够崩溃及重启 #对于不允许崩溃的系统,就要保证足够健壮,否则墨菲定律教你做人
  5. 生产环境下监控、测量、分析

如果一个系统确保其行为可靠,则需要时常进行自检,且当出现与预期不符是能够及时告警。

人为错误

系统的问题归根结底是人的问题。
人设计、搭建系统,维护系统运转的也是人。
基于不可靠的「人」如何设计、构建可靠系统:

  • 设计系统以尽可能低的可能发生错误。如:设计良好的抽象、API能够让使用者容易选择正确的姿势而非错误的方式。但是如果接口设计太死,用户则会尝试使用他们更舒服的姿势包装(work around)再使用,这点是需要权衡的。
  • 将最可能发生问题的行为与最可能导致故障环境隔离。比如构建预发环境,避免生产环境测试。
  • 完备的测试。从单元测试到系统集成测试以及人工测试。自动化测试不仅覆盖大部分场景,也要覆盖那些比较少触发的边缘场景。
  • 能够简单、快速地从人为异常中恢复,将异常所造成的影响降低到最小。如提供配置回滚机制、灰度发布、一些工具能够对消息进行重演来修复数据计算异常等。
  • 构建清晰详尽异常监控,包括但不限性能指标、错误率等。监控可以在异常发生前进行预警,在异常发生后帮助分析
  • 项目管理

为什么可靠性很重要:

伸缩性

一个当前稳定的系统在将来不一定仍旧稳定,除非是一个非核心应用或者一个不再有新需求或用户增长的产品。当一个产品承载的请求量从1kw增长到1b,原有应用服务一定会遇到挑战。
伸缩性指当应用请求增长时,可以通过一定手段使应用能够继续承载新的流量压力。我们需要常常问自己:当系统扩张、用户请求增长、数据增多,我们该如何应对?

负载

QPS?IOPS?并发请求数?缓存命中?这些指标都是单一层面,对于一个系统而言需要从更全局的角度来度量。以twitter为例,主要操作包括:

  • 发推:用户发送一个推文到其跟随者(followers)(4.6k均值QPS,12k峰值QPS)
  • 时间线:用户可以看到他关注者的推文列表(300k QPS)

如果单纯处理12kQPS写入操作其实很简单,但是Twitter面临的伸缩性挑战不是推文容量,而是fan-out:每个用户关注多个用户,每个用户也被很多用户所关注。上述两个操作真正实现有两种:

  1. 用户发推文,将其存储到集中存储中。当用户拉取自己的时间线时,需要将自己关注的所有用户的推文拉取出来,并且按照一定的规则进行排序。
  2. 维护一个缓存来存储用户时间限,当用户发表新推文,则查找所有关注该用户跟随者的时间线缓存,并更新该推文。
    当然实际Twitter并不是这样实现,但是可以作为一个典型的场景来说明负载不是单纯的来自于入口侧的流量,与具体的业务场景及实现方式有关。

性能

负载是外部因素,性能是具体表现。当负载发生变化时,可以用以下两种方式表述系统的:

  • 负载升高系统资源不调整,此时系统性能如何?
  • 负载升高,通过增加多少资源可以保持系统性能保持恒定?

性能也是一系列指标,如web应用为平均请求耗时、数据库为读写速率、大数据应用为吞吐量。
通常有一系列数值可以用来衡量性能,如:

  • 平均数:最常用指标,但是并不能完全反映实际情况
  • 中位数:用以反映半数用户实际获得的性能
  • 99/95百分位数:绝大部分占比用户实际获得性能。可以避免一些极端情况将整体均值拉大
  • 高位:通常被称为尾部延迟(tail lantencies)。虽然这些请求占比很低,但是通常是一些临界情况导致,如一些大商家数据、大批量请求、访问部分问题机器等。对于大商家用户的请求需要格外关注,如果忽略尾部延迟的话可能会造成大商家用户体验丧失。客户端通常都会使用重试作为failover机制,当请求超时时可能会触发重试,对于尾部延迟由于客户端重试可能会进一步增加超时请求的触发,此现象被称为:尾部延迟放大(tail lantency amplification)

大规模集群的数值统计一定要有各自服务的统计数据,全局平均会掩盖部分机器的异常。
通常会使用SLO/SLA来约定服务性能,因为一些随机事件就是可能会导致系统出现各种各样的问题。对于性能的追求也要考虑投入产出比。
注意响应时间测量不能只测量server端的处理时间,因为有些慢请求可能会导致请求堆积,对于服务端而言小部分请求处理时间长,后续请求处理时间很短,但是对于客户端而言大部分的请求的耗时因为堆积而变长。因而可以考虑记录客户端请求的时延,可以保证最真实的服务质量。
压测时也要注意各自压测请求的独立,等待前次响应返回后再触发后续可能会导致压测流量下降。

如何处理负载

对于处理不同负载指标的系统其架构设计、技术选型、部署方式以及后续的扩展方式(scale-out/scale-up)都会有所不同。一个系统是否能够适于拓展,来自于对负载场景的分析,如果基于一个错误的假设(如后续流量瓶颈在本机CPU算力,但是实际在磁盘IO),好一点来说整个扩展方案仅仅是浪费资源,糟糕的则会适得其反。

可维护性

一个软件最愉悦的时间莫过于在起初开发的过程中,但是最最耗费精力阶段是在上线后的维护,问题处理、保证运行、适配新平台、添加新特性、偿还技术债务等。
几乎大部分人都不喜欢与遗留系统打交道,修改其他人的bug、维护陈年代码、使用了一个几乎没人了解过往技术栈。但是转念想想,一个系统一旦开发发布上线之后,立马就变成了别人的遗留系统。如果没有良好设计,那么在别人接手之前,你需要维护你这坨狗屎(如果确实是狗屎)。所以在软件系统实际是,需要对三个设计原则格外关注:

  • 可操作性(Operability):便于运维同学方便保证系统平滑运行
  • 简单:可以让新的工程师很好的了解该系统,尽可能的将所有复杂逻辑移除。(与接口的简单性是不同的)
  • 可进化(Evolvability):新增功能、系统变更、适配未知需求等都很方便。也称为:可扩展性、可适配变化性、柔性等

可操作性:让运维更美好

Good operations can often work around the limitations of bad software, but good software cannot run reliably with bad operations

简言之:有时候良好的操作性比好的软件(设计)更重要。

运维团队(不仅仅指ops,还包含devs)对于系统是否能良好执行也起到至关重要的作用,其职责通常包括:

  • 监控系统健康情况,在系统异常时能够迅速反应
  • 当系统异常或者性能下降时,能够追根溯源
  • 保持系统、平台更新,包括最新安全补丁
  • 了解系统间关系,当可能有问题变更发生时,能够在真正发生问题前避免。
  • 预防可能发生的问题(容量预估)
  • 良好的发布、配置变更等工具
  • 执行复杂的维护任务,如将应用从一个平台迁移到另一个平台
  • 流程定义,以使操作可期,保证环境稳定
  • 维护组织信息,以便人员流动时仍能够传承

高可操作性意味着日常工作处理顺畅,可以让运维团队专注于更有价值的工作。同时数据系统也能提供相应的支持,如:

  • 提供运行时行为、系统内运行态可视化,以及相应的监控服务
  • 避免单点依赖,允许节点动态摘除仍保证系统运行稳定
  • 使用标准工具提供自动化及集成支持
  • 良好文档及易懂的运维模型(如果操作X,那么Y将发生)
  • 提供可靠的默认行为,同时提供管理员足够权限
  • 可期的行为可以自愈,当系统异常时(给予足够权限)系统管理员能够介入处理
  • 行为可期,避免意外(「惊喜」)

简单:处理复杂

当需求不断扩充、系统变大时,必然会由简单变得复杂。一个系统变得复杂的表现通常为:
状态的膨胀、模型紧耦合、纠缠不清的依赖、不一致的命名和术语、为了解决性能问题而做的hack、特殊场景的兼容方案等等。
系统变得复杂以后,新的变更将会更易引入问题。当系统让开发者难以理解,一些潜规则、未预料的后果、不可预期交互更容易被忽视。因而让系统变得简单应当是我们很关键的目标。
Moseley和Marks如下定义复杂:

Complex as accidental if it is not inherent in the problem that the software solves(as seen by the users) but arise only from the implementation.
当一个软件产生了或解决了原本不是这个软件应该提供给用户的功能或问题解决方案时,那么这些问题就是复杂度所引起的。

解决意外复杂度的一个利器是抽象。抽象隐藏了实现细节,提供了简单易懂的接口,同时可以在很多场合进行复用。不仅仅是因为复用避免了重复开发所引入的新问题,更可以通过对同一抽象的优化可以实现使用该抽象的全部优化。抽象也可以让我们避免去理解那些不需要理解的细节内容,如高级语言将机器语言进行封装,对于一般的软件开发者而言,使用Java并不需要关心其机器码,可以让开发人员更聚焦。

可扩展性(Evolvability):让变更更容易

系统不可能一成不变:过去为预料的场景发生了、新的业务需求、平台更替、架构升级等等。
通过引入敏捷可以协助完成变更,同时进可能避免问题发生。如引入TDD(Test-Driven Development)及重构。
如上所说,简单及抽象可以保证变更的可预期,所以一个扩展性良好的系统也必然是足够抽象简单的。

小结

  • 可靠性:即便当问题发生时,系统也能正常运作。异常可能发生自硬件、软件甚至更多来自人为。容错技术可以在问题发生时让用户无感知
  • 可扩展性:当负载升高,系统也能通过相应策略保证性能稳定。讨论可扩展性之前,需要先明确负载性能分表代表什么。
  • 可维护性:简言之就是让运维及开发在处理系统时能更从容。好的抽象能够降低复杂度,让系统更易于变更及添加新功能。系统指标数据可视化也便于系统的维护。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容