《数据密集型应用的设计》读书笔记──第一章:可靠性,可扩展性,可维护性

第一章:可靠性,可扩展性,可维护性

数据密集型(data-intensive)而非计算密集型(compute-intensive):

  • 数据量、数据复杂性、以及数据的变更速度
  • 数据密集型应用基本组件
    • 存储数据,以便自己或其他应用程序之后能再次找到 (数据库(database)
    • 记住开销昂贵操作的结果,加快读取速度(缓存(cache)
    • 允许用户按关键字搜索数据,或以各种方式对数据进行过滤(搜索索引(search indexes)
    • 向其他进程发送消息,进行异步处理(流处理(stream processing)
    • 定期处理累积的大批量数据(批处理(batch processing)
  • 一个可能的组合使用多个组件的数据系统架构:

Untitled

影响数据系统设计的因素很多,包括参与人员的技能和经验、历史遗留问题、系统路径依赖、交付时限、公司的风险容忍度、监管约束等。

重要的三个问题;

  • 可靠性(Reliability)

    系统在困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)。

  • 可扩展性(Scalability)

    有合理的办法应对系统的增长(数据量、流量、复杂性)。

  • 可维护性(Maintainability)

    随着时间的推移,许多新的人员参与到系统开发和运维, 以维护现有功能或适配 新场景等,系统都应高效运转。

可靠性

期望:

  • 应用程序执行用户所期望的功能。
  • 可以容忍用户出现错误或者不正确的软件使用方怯 。
  • 性能可以应对典型场 景 、 合 理负载压力和数据 量。
  • 系统可防止任何未经授权的访问和滥用。

故障(fault)和失效(failure):

  • 故障:系统的一部分状态偏离其标准;
  • 失效:系统作为一个整体停止向用户提供服务。
  • 由于出现fault的几率不可能为0,因此倾向于设计fault-tolerant而不是fault-preventing的系统。

硬件故障

  • 随机的、相互独立的
  • 计算机系统中的硬盘、内存、power grid等零件都可能出问题,这可以通过增加单个硬件的冗余度(redundancy)来减少整个系统宕机的概率;
  • 随着数据量和应用计算需求的增加,增加设备冗余也无法解决,就需要考虑如何在某个机器宕机的情况下通过软件调度来防止整个服务崩溃。

软件故障

  • 系统性错误的BUG难以预料,通常在异常情况被触发,例如Linux内核的润秒BUG;
  • 没有快速解决办法,只能在实现时:
    • 认真检查依赖的假设条件与系统之间交互
    • 进行全面的测试
    • 进程隔离
    • 允许进程崩愤并自动重启
    • 反复评估
    • 监控井分析生产环节的行为表现

人为故障

  • 人类是不可靠的,操作人员的不当操作占系统崩溃的75%~90%
  • 如何降低:
    • 以最小出错的方式来设计系统;
    • 想办住分离最容易出错的地方、容易引发故障的接口,提供sandbox让他们熟悉使用方法,随意犯错而不会影响生产;
    • 充分的测试:unit/integration/manual;
    • 当出现人为失误时,提供快速的恢复机制以尽量减少故障影响;
    • 设置详细而清晰的监控子系统,包括性能指标和错误率;
    • 推行管理流程井加以培训。

可扩展性

系统不见得一直可靠,比如负载增加,系统需要持续变化。

衡量负载(Load)

  • 根据系统特性简洁描述,例如服务器的每秒请求处理次数,数据库读写比例,用户数量, 缓存命中率
  • Twitter例子
    • 需求
      • 用户发推(avg 4.6k RPS,max 12k RPS)
      • 查看个人主页时间线(300k RPS)
    • 挑战:如何快速为用户提供timeline。
      1. 直接从数据库读查询,涉及join,在请求多的时候数据库无法处理;
      2. 考虑为每个用户维护时间线的缓存,只是发推的时候就需要更新对应用户的缓存;
      3. 混合:由于发推的RPS远小于查询,缓存的方法对读更有优势,但是对于follower众多的大号,发推需要写入的缓存可能非常多,因此可以考虑针对这种大号直接写入数据库,在时间线单独从数据库读大号的推。

衡量性能(Performance)

  • 两个问题:
    • 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将受到什么影响?
    • 增加负载参数并希望保持性能不变时,需要增加多少系统资源?
  • 吞吐量(throughput):每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间;
  • 响应时间(response time)和延迟(latency)
    • 响应时间:网络延迟 + queuing + service time
    • 延迟:请求花费在处理上的时间。
  • 百分位数(Percentile):
    • 响应时间如果直接数学平均,无法反映大致有多少请求受到影响
    • p95, p99, p999表示有95%, 99%, 99.9%的请求的响应时间快于某个值。
  • 尾部延迟(tail latencies):响应时间的高百分位点有时也很重要
    • 对于Amazon,大客户请求的数据量大,响应时间也就更长,不能不关注。
  • SLO/SLA: Service Level Objective/Agreements, 用百分位数来判断系统是否可用。
  • 测试时负载生成独立于响应时间来持续发送请求,不能等到收到响应才轰下一个请求。

应对负载增加的方法

  • 纵向扩展(scaling up)(垂直扩展(vertical scaling),转向更强大的机器)
  • 横向扩展(scaling out) (水平扩展(horizontal scaling),将负载分布到多台小机器上)

可维护性

软件开发本身开销并不算大,日后的维护升级需要花费更多。

软件系统的三个设计原则:

  • 可运维性(Operability)

    方便运营团队来保持系统平稳运行。

  • 简单性(Simplicity) 简化系统复杂性,使新工程师能够轻松理解系统。注意这与用户界面的简单性并 不一样。

  • 可演化性(Evolvability) 后续工程师能够轻松地对系统进行改进,井根据需求变化将其适配到非典型场 景,也称为可延伸性、易修改性或可塑性。

可运维性

  • 良好的监控
  • 自动化
  • 避免依赖单台机器
  • 良好的文档和易于理解的操作模型
  • 默认行为和可覆盖
  • 自我修复
  • 行为可预测
  • 其他
    • 适当的日志来跟踪出问题(系统故障或性能下降)的根源
    • 保证系统能升级,尤其是安全补丁
    • 正确的配置
    • 开发时遵循最佳实践

简单性

保证代码简洁,让新加入的码农也能理解代码。一个很有效的方法是抽象,用高级类来描述统一的行为。

  • 防止状态空间爆炸
  • 让系统中的模块尽量解耦,减少依赖
  • 保证一致的命名
  • 减少短视的hacks

可演化性

方便后续重构、加入新功能那个,与前面的简单性息息相关。

  • 敏捷工作模式比较方便改变
  • TDD:测试驱动开发

参考

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

View on GitHub