互联网应用服务端的常用技术思想与机制纲要

拓展技术边界,提升业务创新能力。


不患不知,而患不精。精益求精,技术过硬是必要的。理解互联网技术的底层原理与实现,对提升设计能力非常有益处,亦能为业务创新提供重要的技术保障。技术深度不够会限制业务创新能力。

本文旨在聚合互联网应用的服务端所用到的主要且重要的技术思想、原理、机制、技巧。

综述

软件的使命是处理数据。人赋予数据以意义。

数据处理的要求:灵活、高性能、高可靠、高可用、一致、海量、安全、自动化、智能化。

  • 灵活:数据建模与存储、架构与编程、微服务、框架、代理、组件与配置化。
  • 高性能:数据结构与算法、并发、缓存、精简开销、参数调优、编译优化、服务器性能、服务指标测量。
  • 高可用:限流、降级、冗余。架构上要支持水平扩展,运维上多节点部署、负载均衡、弹性伸缩容(容器化)。
  • 高可靠:分布式 + 容错。
  • 一致: 复制、状态机、一致协议、重试补偿对账。
  • 海量:分布式 + 大数据存储 + 实时计算 + 离线分析
  • 安全:机密性、完整性、可用性。机密性 - 数据内容不泄露;完整性 - 数据内容不被篡改;可用性 - 保护资源随需所得。
  • 智能化:让机器从大量已有数据中自动学习和分析模式,增加反馈环节,来改善产品、业务的前置环节。

视角

解决问题视角

  • 解决什么问题,有哪些情形,要考虑什么错误和异常 ?What - Problem - Circumstances - Errors - Exceptions
  • 为什么要解决这个问题 ? 造成了什么麻烦,有哪些应用场景 ? Why - Troublesome - Applications
  • 如何解决:理念、方法、实践如何 ? How - Thoughts - Methods - Practices
  • 方案对比:优点和缺点,适用场合 ? Evaluation - Benefits - Drawbacks

系统设计视角

  • 核心抽象、概念及类。 Abstraction - Conception
  • 主流程、重要环节、核心类的串联。 Flow - Segments - Links
  • 问题分解、技术难点与方案、可行性论证。Decomposition - Keypoints - Solutions - Feasibility
  • 关键数据集及算法。 Core Dateset - Algorithm

技术决策视角

  • 场景考量:通用场景,还是特定场景? 场景是否有特殊性,可以用更高效的方式有针对性地解决?场景下的语境上下文是怎样的 ? Scene - Context
  • 方案权衡:要达成的目标是怎样的? 哪些质量因素是尤为重要的?要兼顾考虑哪些质量因素?可用资源如何? Goal - Quality Requirements - Available Resources

思想

纵观各种技术:基本思想是在不同语境里反复使用着的。

  • 功能与灵活: 标识、标记/状态位、元数据;分离与解耦。
  • 时间复杂度: 二分、映射、分治与组合、分摊、缓存、缓冲;动态规划;预处理;空间换时间。
  • 空间复杂度: 位图、冗余编码、虚拟化;时间换空间。
  • 高效读:缓存优先于主存,主存优先于磁盘,缓冲优先于直接读写;减少磁盘读次数,减少更慢存储介质的读次数。
  • 可靠写:主存优先于缓存、磁盘优先于主存;提交/确认/回滚机制。
  • 提升响应:批量、异步、并发。
  • 可维护性:抽象(问题本质)、封装(实现隐藏)、复用(提升效率)、多态(增强灵活性)、组合(功能集成)。

灵活

灵活是指能够低成本、高质量地添加新功能和优化现有功能。

架构与编程

对于单个微服务而言,可以通过如下手段来增强灵活性:

  • 建模存储:数据模型、数据 Schema 、完整性和一致性约束定义。
  • 领域驱动:DDD 设计,稳定的精炼的可持续演进的领域模型,六边形架构,聚合根,充血模型。
  • 架构模式:分层、微内核+插件、PipeLine 、事件驱动、订阅-推送、Actor 等。
  • 柔性编程:应用设计模式、设计原则(SOLID, KISS)、组件化、持续小幅重构。

微服务

  • 单体应用的问题:直接通过访问 DB 来共用数据库,数据管理容易缺乏明确 owner 而混乱,DB 设计变更难度高;缺乏明确的域的划分,功能边界不清晰,重复功能实现和重复代码难以维护,业务耦合,依赖关系混乱;协作成本高,系统整体稳定性差。

  • 微服务:将单体应用分解为多个具有明确领域定义的业务子域,将每个相对独立的业务子域实现成单独的微服务,微服务独立管理各自子域的问题,采用不同的架构和方案来适配自身领域的问题,最终所有微服务集成起来完成整体应用功能。实现独立自治和发展、模块化、分工协作等。

  • 微服务要解决的问题是服务治理。主要包括:限流/熔断降级、配置管理、日志中心、监控预警、链路跟踪、故障隔离、动态扩容、分流发布、全链路压测、中间件支撑、团队组织架构适配与管理。基本解决方案: RPC 框架和统一应用框架接入。

  • 一个较为棘手的问题:应用一致性。解决方案有:分布式事务、消息补偿、对账和自动修复。

  • 分布式事务解决方案:2PC,TCC,本地消息表(消息解耦)、消息事务。


RPC:Dubbo

  • Dubbo 官方文档已经给出了详细的解释和引读,推荐阅读。Dubbo 官网地址在文末参考文献中给出。
  • 核心抽象是服务。Dubbo 服务是通过 URL, ServiceConfig 来表示的。 URL 是服务的表示,作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息;而 ServiceConfig 是基于 URL 的服务的配置,包括与服务导出有关的所有信息。服务的导出入口在 ServiceBean.onApplicationEvent 方法里,即 Spring 容器初始化完成之后。
  • Dubbo 十层架构理解:总体可分为 RPC 服务和 RPC 通信两部分。在 RPC 服务部分,首先是最接近于业务方的服务接口和实现层(Service),有了服务就需要服务的生产者和消费者配置(Config - XML 或注解),还需要服务的注册与订阅(Register - Dubbo, ZK, redis), 与之相配套的服务监控(Monitor)。通常裸服务是不够的,往往会有代理(Proxy - javassist,JDK)、负载均衡(LoadBalance - ConstentHash, LeastActive, Random, RoundRobin)和容错(Cluster)。在 RPC 通信部分,需要协议层(Protocol - Dubbo, HTTP, Rest, Hessian, InJVM, Tether, Thrift Nova, RMI, WebService, Redis, 直连)、信息交换层(Exchange - 封装 Request-Response 语义)、传输层(Transport - netty, mina), 以及数据在网络和内存中的序列化和反序列化(Serialize - Hession2, Dubbo, FastJson, JDK)。
  • 扩展机制:Dubbo SPI ,核心类是 ExtensionLoader , 核心成员是 EXTENSION_LOADERS[Class, ExtensionLoader], EXTENSION_INSTANCES[Class, Object] 。带缓存的实现。
  • 序列化与反序列化:考虑跨平台、性能(占用空间及网络带宽传输)和兼容性。Hession2 -- 自描述序列化,跨语言、占用空间小,要注意子类和父类的同名变量在反序列化时有覆盖问题。 FastJson -- 序列化行为可定制、序列化后可读性好、容易调试、丢失类型信息。反序列化时容易有安全漏洞,敏感信息要么不序列化,要么加密后再序列化。

统一应用框架:Spring

  • IoC: Bean 生命周期定义和操作定义。定义一个包含多个阶段的生命周期,在每个阶段可以设置一些钩子方法作为扩展点。
  • AOP: Pointcut - Advice - Joint - Aspect 。 接口用 JDK 代理,其他用 CGLIB 代理。JDK 代理将方法委托给目标对象,CGLIB 代理则通过加载并修改目标对象的字节码、继承并覆写目标对象的方法而实现。

分布式Trace

  • TraceId + Span + javaagent + 字节码修改。

消息系统

代理

  • 代理的目标是性能、路由、安全、透明、迟加载、隐藏复杂实现细节。
  • 代理技术:四层代理(IP+Port,LVS)、七层代理(IP+Port+Application,Ngnix)。四层代理性能更高,七层代理更灵活。
  • 代理模式:静态代理、动态代理。

代理模式

  • 参与者: 目标实例、代理实例、代理逻辑。 目标实例是已知的,需要代理的逻辑需要指定,代理实例需要生成。
  • 静态代理: 编写和目标实例具有相同行为的代理实例,并将对目标实例的请求转发给这个代理实例上。由于总是需要为目标对象手动编写代理实例,因此称为静态代理。静态代理容易理解,但不够灵活。
  • 动态代理:动态生成和目标实例具有相同行为的代理实例,并将对目标实例的请求转发给这个代理实例上。动态体现在可以为不同行为的目标对象生成相应的代理实例,而不是手动去编写代理实现。常用动态代理有 JDK 代理和 CGBLIB 代理。
  • JDK 代理:通过 java.lang.reflect.Proxy.newProxyInstance + 反射机制实现。适合对接口代理。代理逻辑通过 InvocationHandler 接口定义,Proxy 将实现 InvocationHandler 的实例传入构造器,生成动态代理实例。代理实例的类继承自 Proxy 。Proxy 通过proxyClassCache 来管理 ProxyClass 和 ProxyFactory ,并在 getProxyClass0 的时候去缓存 ProxyClass 的信息。

HTTP代理

  • HTTP 代理就像客户端与服务器之间的拦截器。既充当客户端的角色,又充当服务器的角色。代理可以级联,组合使用。可以通过 Trace 方法和 响应头的 Via 首部来追踪报文途径的网关和代理(Via 有安全与隐私问题)。
  • HTTP 代理的作用:过滤(不宜内容)、访问控制与审计追踪(安全)、安全防火墙(安全)、流量监控(安全)、缓存(性能,降低网络开销和拥塞)、反向代理(性能)、内容路由器(增值服务)、转码与压缩(国际化与性能)、匿名(安全与隐私)、路由与负载均衡(稳定性)。
  • HTTP 代理的部署: 出口(LAN 出网点,过滤、安全)、入口(缓存与性能)、边缘(反向代理)、对等交换点(缓存与安全)。
  • 使用 HTTP 代理的方式: 浏览器配置、交换或路由设备拦截、修改 DNS 、重定向。客户端代理配置 -- PAC 文件(提供一个URI, 指向用 JS 写的代理自动配置文件,会动态计算适合的代理配置);自动代理发现 -- WPAD ,按顺序尝试 DHCP(动态主机配置协议)、 SLP(服务定位协议)、DNS Known Hosts、DNS SRV 等技术,自动发现合适的 PAC 文件。
  • HTTP 代理的问题及方案:客户端发给代理的 HTTP 请求报文里应当是包含主机名的完整 URI。但客户端并不总是知道对方是代理,或者并不知道代理的存在。因此通用代理需要进行“缺失主机名的部分 URI 补全”,拿到主机名拼成完整的 URI(没有代理时浏览器也会做类似的事情)。某些代理会对 URI 做细微修改,影响互操作性。代理的容错机制(解析到的主机是已停用服务器时)。
  • 代理认证:客户端发送请求,代理发现没有认证,会返回 407 响应码,客户端拿到 407 后搜索和拿到证书,重发请求,代理认证通过。

组件和配置化

  • 组件化: 配置化的基本前提。组件需要定义良好的行为规范和接口规范。
  • 流程编排:需要将整个流程划分为若干阶段,定义每个阶段的行为和目标、阶段之间的连接。
  • 动态语言脚本。比如订单导出使用 Groovy 脚本配置报表字段逻辑。 脚本注意做成缓存对象,避免可能的内存泄漏。
  • 选项参数。选项参数的原型是命令行参数。用户可以通过选项参数来选择策略、调节性能等。
  • 规则引擎。 将业务逻辑表达为若干条规则,然后用工作流将规则集合串联起来。

发布

  • 分流发布:灰度发布、蓝绿发布。小批量验证。分流系数可动态配置和生效。
  • 容器化部署。

高性能

提升性能,即是用更少的资源做更多的事情。资源类型包括:CPU 时间片、内存空间、磁盘空间、网络带宽、连接池、缓存等。弄清楚应用程序依赖哪些资源类型以及依赖程度如何。

性能通常用 RT 和 吞吐量来衡量。 RT 是单个请求的处理时间, 吞吐量是指定时间内处理的请求数。应用也分为响应敏感型和吞吐量敏感型。比如订单详情,就是响应敏感型,RT 过高会导致超时,表现为服务不稳定; 而订单导出,则是吞吐量敏感型,单个请求处理长一点不影响体验,单位时间的吞吐量越高越好。

数据结构和算法的设计是性能提升的基本层面。并发是通过多个 Woker 并发或并行处理的思路。缓存是不同读写速度的存储介质及淘汰算法的权衡。精简开销和参数调优更多是针对热点和耗时进行细节上的调优。编译优化是语言层面的优化。高配服务器能直接提升性能,但成本较高。最后,性能测量必不可少。只有理论值是不够的。

高性能的核心:更少的 CPU 周期(数据结构、算法、内存计算),更少的等待时间(IO 读写、加锁与解锁、内核与用户态的切换)。

常见硬件性能:L1 cache (0.5ns) > 分支预测失败(5ns) OR L2 cache (7ns) > Mutex 加锁与解锁 (25ns) > 内存访问 (100ns) > 固态盘 SSD 访问延迟(0.1ms) > 机房内网络来回 (0.5ms) > 千兆网络发送 1MB (10ms) OR SATA 磁盘寻道 (10ms) OR SATA 顺序读取 1MB 数据 (20ms) > 异地机房网络来回 (30-100ms) 【来源:《大规模分布式存储系统》2.1.4 】

数据结构与算法

时间复杂度和空间复杂度。 在空间存储充足的情况下,通常考虑时间复杂度。当然,在移动端以及大数据存储方面,空间复杂度也需要仔细考虑。

  • 基本数据结构:数组、位图、链表、栈、队列、堆、表、哈希散列、二叉查找树、红黑树、DAG、JSON 。
  • 基本算法:分治、递归、动态规划、贪心算法;排序、查找;深度优先遍历、广度优先遍历。
  • 预处理思想:通过预排序、预索引、拓扑结构等构造特定的数据结构,以支持高效查找,比如 KMP, RETE ,倒排索引,都运用了这种思想。
  • 高效查找:有序查找、哈希查找。有序查找 -- 构建有序结构,比如有序链表、跳表、二叉查找树、红黑树,B+ 树,从而使用二分查找来提升查找效率,减少比较次数,查找时间复杂度 O(logn) ; 哈希查找 -- 构建哈希 key-value 映射结构,解决哈希冲突,查找时间复杂度 O(1) 。哈希查找需要仔细选择哈希函数(通过参数调优来提升性能),不支持 rank 和 select 操作(第 K 大元素),通常用于 K-V 结构的存储系统; 顺序查找的查找效率与哈希接近,支持 rank 和 select 操作,通常用于有序表、关系型数据库等。可以结合两种查找结构使用。比如 java8 的 HashMap 是哈希查找与顺序查找的结合。
  • 最优排序:快速排序、合并排序,时间复杂度 O(nlogn) , 根据特殊情形可以时间换空间或空间换时间策略获得更好性能
  • 空间效率:压缩算法(RLE、增量编码、哈夫曼编码、Rice编码、LZ77编码、位表示、Trie前缀树); snappy, gzip, lz4; 图像视频压缩(MLP、CNN、GAN)。
  • 生成全局唯一ID: snowflake 算法,生成 64 bit 的 long 型数值作为唯一ID。41bit (毫秒数) + 10 bit (5bit 数据中心 + 5bit 机器号) + 12bit (毫秒内的偏移量) + 1bit (=0)。
  • 判断 key 存在性 : 位图(稠密、不重复)、布隆过滤器,O(logn)。位图可用于稠密不重复数组的排序。
  • 动态变化的查找: 一致性哈希。环状队列 + 多哈希 + 虚拟节点。构建一个 serverMap = TreeMap[VirtualNodeHash, Server] 的有序映射。对数据进行哈希 h 后,在 serverMap 找到第一个不小于 h 的键 S,将该数据分布到服务器 serverMap[S] 上。哈希算法可采用 Fowler-Noll-Vo 哈希算法。一致性哈希的性质:平衡性、单调性、分散性、负载、平滑性。
  • 全文检索: 倒排索引。关键词分割、关键词所在的文档及位置的存储、词典索引构建与优化。
  • 动态规划:子问题求解、计算结果缓存与复用。要学会划分可复用的子问题,并理清楚子问题与原问题之间的关联。
  • 堆:排序 O(nlogn)、前 K 最大或最小值 O(n) 。可用于实现优先级队列。
  • 字符串匹配:朴素字符串匹配 - 双重循环,简单,低频场景;有限自动机 - 根据模式字符串构造有限自动机,再匹配文本,适用模式串的不同字符数很少的情形; KMP - 根据模式字符串及构造后缀匹配数组(PMT,Partial Match Table,前缀集合与后缀集合的交集字符串的最长长度),避免无用位移测试,适用生产环境。
  • SkipList: 有序链表,空间换时间,通过在每个节点上新增多级索引指向后继节点,实现跳跃查找,平均 O(logN) ,最坏 O(N) 查找效率;批量顺序操作;比平衡树实现简单。Redis 使用 SkipList 实现有序集合键。
  • 布隆过滤器: 使用多个哈希函数将一个值映射成多个哈希值,并投影到位图上,并置为 1。如果指定 key 通过哈希函数映射到位图上,有一个位为 0, 则一定不存在这个 key 。利用布隆过滤器减少磁盘 IO 或者网络请求。
  • 红黑树:平衡二叉树。最坏情况下查找效率 O(logn)。 2-3 树的变体。指向左孩子的指针为红色的节点标识 3-节点。左旋与右旋。
  • 哈希计算:主要基于整数或二进制位来计算。如果是浮点、字符串、IP地址、对象,先将其转换成整数型,再进行计算。最好能用到 key 里面所有相异的部分。常见的哈希函数有取模(模最好是素数)、MurmurHash2 算法(源码可以在 GitHub 上搜到)。可以编写一个程序,来检测哈希值的分布均匀度。如果哈希计算代价比较昂贵,需要做哈希值缓存。
  • 位操作技巧:一般用于求哈希值、高效实现特殊运算(比如 m mod 2^n == m & (n-1) )、状态位设置、节省空间等。
  • 字符串处理技巧:从尾部开始编辑,可以避免覆盖问题。
  • 链表技巧:快指针与慢指针。

容器

  • HashMap: 数组 + 链表(在必要时会变成红黑树)。 核心操作是 put, resize, get。线程不安全。并发下会发生数据丢失( put 更新 value 时)、死循环( resize 复制数据时)。
  • LinkedHashMap: 保持插入序的 Map 。节点采用双向链表。新插入节点或已访问节点移至链表尾部。线程不安全。
  • TreeMap: 红黑树实现。可顺序查找的 Map 。线程不安全。

分区

“【总结系列】互联网服务端技术体系:可扩展之数据分区”

数据库索引

“【总结系列】互联网服务端技术体系:高性能之数据库索引”

并发

“【总结系列】互联网服务端技术体系:高性能之并发”

缓存

“【总结系列】互联网服务端技术体系:高性能之缓存面面观”

精简开销

  • 热点分析,定位开销大的地方。比如订单导出的热点耗时区域在批量获取订单的详情内容上。
  • 移除不必要:去掉不必要的访问、去掉重复开销。比如获取订单详情时不需要的字段就不去访问相应的 API。
  • 精简链路:去掉重复调用,合并调用,调用结果缓存与传递。比如交易可以拿到商品信息并缓存、传给营销中心。
  • 无锁化:CAS 机制。去掉加锁和释放锁的操作耗时,增加了 CPU 轮询开销(两害权衡取其轻)。
  • 非阻塞IO:Select 和 Epoll 机制。减少不必要的线程切换和数据拷贝。

非阻塞IO

  • 阻塞IO:需要为每个 Socket 建立线程(线程数量有限),创建大量线程(空间开销大),线程容易被IO阻塞等待(利用率低),大量的线程切换开销(时间耗费在与业务无关的事情上)。
  • 非阻塞IO:IO 多路复用思想;Select机制、Epoll 机制。
  • Select 机制 : Buffer - Channel - Selector 。 一个线程可以管理多个 selector ,每个 selector 可以轮询监听多个 Channel 的事件并读取或写入数据,每个 channel 与一个 Buffer 相连,提升 IO 读写的效率。select 相对于阻塞IO有进步,但仍然有缺点:1. 每次 select 都需要将 fd 集合从用户态拷贝到内核态,并在内核态遍历所有的 fd ;fd 越多开销越大; 2. 一个进程支持的 fd 集合大小受限,默认是 1024 ; 3. 如果没有 fd 处于就绪状态,select 会阻塞。
  • Epoll 机制:三个函数 --- epoll_create ,epoll_ctl,epoll_wait 。epoll_create 会创建一个 epoll 实例, 其中包含一棵红黑树 rbr 存储需要监控的 fd 集合,双链表 rdlist 存储返回给用户的满足条件的事件; epoll_ctl 向 epoll 实例注册给 fd 要监听的事件;epoll_wait 等待内核返回监听 fd 的事件发生,返回已就绪的事件及数量。Epoll 有 LT 和 ET 两种工作模式。LT --- 检测到 fd 的事件就绪通知应用程序后,可以暂时不处理,事件放回就绪链表中,待下次调用 epoll_wait 时再通知;ET --- 必须立即处理。 Epoll 的优点: fd是共享在用户态和内核态之间的(mmap技术),不需要将 fd 在内核态和用户态之间拷贝,不需要遍历就可以获得就绪的 IO 事件。一个进程支持的 fd 集合大小只受操作系统限制。
  • Netty: ByteBuf - Channel - ChannelEvent - EventLoop - ChannelPipeline - Channel Handler - Callback - Channel Handler Context - ChannelFuture 。Channel 是数据流 ByteBuf 的出入通道,网络连接和数据事件发生的起点。当 Channel 的某个事件 ChannelEvent 发生时,EventLoop 会轮询 ChannelEvent 已经注册的 Channel Handler ,并执行 Channel Handler 对应的回调函数 Callback。多个 Channel Handler 可以构成一个 Channel Pipeline。 Future 提供了异步返回结果和通知的方式。Future 可以注册 FutureListener ,从而在 Future 执行完成时回调 FutureListener 的方法 operationComplete。一个 Channel 在其整个生命周期里只关联一个 EventLoop 且它的所有 ChannelEvent 都由这个 EventLoop 处理。EventLoop 是 netty 的事件并发执行模型。
  • Redis 多路复用: evport, epoll , kqueue , select

HTTP2优化

  • HPack 压缩首部算法,常用首部做成静态表映射获取, 哈夫曼编码。
  • 借鉴 TCP ,数据划分为更小的二进制帧进行传输;
  • 多路复用,单连接多流并行;
  • 服务器主动推送相关资源,而非浏览器发送多次请求,减少 TCP 连接耗时;
  • 应用层重置连接和请求优先级;
  • 流量控制。

参数调优

  • JVM 调优
  • 应用配置参数调优。首先提取影响系统功能、性能、稳定性、可用性等的重要因子,然后通过配置平台来管理。参数配置可以包括策略选择、超时设置、重试次数、批次处理数、限流因子等。比如订单导出的策略选择有报表维度(商品/订单/商品订单均要)、文件上传下载维度(本地服务器或云存储); 订单导出并发批量拉取订单详情,可以配置分批处理的订单数,并发拉取的订单数,每批次订单处理的时间间隔、超时重试次数等。

服务指标测量

在工程上,测量是尤为重要的。理论只是给出了定性值,而测量将给出更精确的量化值。

  • 基本指标: 单机 RT 和 QPS。运行多次单个请求,取请求响应时间的平均、峰值、最小值、百分位数。平均值即为 RT 值。QPS = 1 / RT(ms) 。
  • 并发指标: 并发数。通过压测来测试并发负载能力。

高可靠

分布式

分布式通过异构、冗余实现容错和扩展能力。

  • CAP:一致性、可用性、分区容错。 一致性要求不高的,通常采用 AP; 资金敏感的,通常采用 CP。
  • BASE: 基本可用,软状态、最终一致性。牺牲强一致性来获得可用性。
  • 协议:Paxos,Raft

选举算法

选举算法是分布式系统的基础。许多分布式都是基于 Master-Slaves 或 Leader - Followers 模式。一般 Leader 负责写数据,而 Followers 负责同步写入的数据到自己的副本上保持与主一致。

  • 选举算法:Bully 算法、FastLeader 算法(ZK)。Bully -- ID 最大的作为 Leader ,有频繁换主的风险。

选举机制的组成部分:

  • 选举轮次标识:时间戳或编号,防止过期选举消息干扰;
  • 选举检查项:比如服务器状态;
  • 选举依据:根据什么来比较,判断谁应当成为 Leader ;
  • 投票统计:每个服务器都投出自己的一票,并接收别人的投票做某种处理,和自己的投票来做统计;
  • 选出 Leader : 根据统计结果,大部分服务器都能达到共识,Leader 从共识中产出。

ZK

  • 实现分布式协同服务。CP 模型、ZAB 协议、ZNode、树形结构、Watch 通知。 ZK 只能存少量元数据,不适合做分布式存储系统。
  • ZK 的数据结构:节点以树的形式组织起来。节点分 持久/ 临时节点,有序/无序节点。
  • ZK 服务器数最好为奇数个:1. 偶数个只允许更少服务器崩溃,更脆弱; 2. 防止脑裂现象。
  • ZK 以事务的方式执行所有操作,并确保所有事务以原子方式执行,不受其它事务的干扰。ZK 事务具有原子性和幂等性。zxId 作为事务的标识。zxId 是 64 位的值,分为 32 位时间戳和32位的计数器。32位时间戳可以用于识别当前时段的 Leader 和事务时间戳。
  • 选举算法: 每个服务拥有一个 Eo (sId, zxId) ,并向其它服务器发送自己的 Eo (sId, zxId) 。 收到 Er 的每个服务器将 Er 与 Eo 进行比较。如果 Er.zxId > Eo.zxId 或者 Er.zxId = Eo.zxId AND Er.sId > Eo.sId ,则用 Er 替换 Eo 。最终大部分服务器都会拥有相同的 Er。从而选举出具有 Er 的那个服务作为 Leader 。
  • ZAB:S1- Leader 向所有 Follower 发送一个事务的 Proposal 消息 P; S2- 每个 Follower 收到消息 P 后响应一个 ACK ,表示同意该事务; S3- 当仲裁数量的服务器发送 ACK 消息后,Leader 将通知 Follower 进行提交事务。

容错

“【整理】互联网服务端技术体系:可靠性与容错”

故障检测与恢复

  • 故障检测主要靠监控。服务器监控(CPU、内存利用率、Load、IO RW、Net RW)、服务监控(RT、QPS、消费速率
    、延迟、网络连接;来源、TOPN)、Java 监控(ThreadPool、Heap、GC 等)、异常监控(失败次数、失败比例;超时,消费堆积或不均)、业务监控(瞬时峰值、瞬时下跌、同比上涨或下跌、大数据对象)、对账监控(数据一致性检测,尤其资金相关)。
  • 心跳机制:心跳机制用于故障检测。每台工作机每隔指定时间将自己的CPU、内存、磁盘、网络、IO读写、负载等情况上报给 Master, Master 会判断服务器是否正常,以决定是否分发流量给该服务器。每台服务器与 Master 有个超时设置,如果达到超时没有收到该服务器的心跳信息,则会判断服务器出现问题。心跳机制需要 KeepAlive (连接最大空闲时间) 属性,使用 MQTT 协议。
  • 租约机制:每台工作机向 Master 申请具有一定租约期限的服务时间。当服务时间快到时,再向 Master 继续申请延长租约的有效期。如果因为网络或工作机故障导致租约无法响应,则会将该工作机隔离,不再提供服务。
  • WAL(Write Ahead Log): 预写日志,恢复日志。基本机理是复制状态机、备忘录、备份。关系数据库系统中用于提供原子性和持久性。in-place 和 shadow paging 。提升磁盘写性能:随机读写改顺序读写、缓冲单条读写改批量读写、单线程读写改并发读写。同步 WAL 文件和数据库文件的行为被称为 checkpoint(检查点)。实现方法:DB - undo, redo 机制;ES - fsync 机制;ZK - 先写 WAL,再更新内存,最后通知客户端;定期将内存中的目录树进行 Snapshot,落磁盘; ETCD - wal 和 snap 目录; HBase - 更新数据前写 WAL, 且写 WAL 与数据更新在同一事务。
  • 检查点(CheckPoint):系统定期将内存状态以检查点文件的形式dump到磁盘中,并记录检查点时刻对应的操作日志回放点。 检查点可以快速提升故障恢复的速度。
  • 备份。备份是应对数据故障(丢失、不一致)的重要保障。备份只是实现数据的恢复,并不能完全实现故障恢复。故障恢复是指服务或服务器回到正常可用状态。

MySQL备份

  • 逻辑备份和物理备份。逻辑备份是可读文件、恢复简单灵活(恢复工具及选项)、在不同机器上运行、与存储引擎无关、避免物理环境导致的数据损坏,但恢复时间较长,需要 MySQL 来完成、需要测试恢复;物理备份通常是不可读的二进制文件,恢复更简单(只要拷贝文件到目的路径)、恢复时间很短,但 InnoDB 的原始文件通常比逻辑备份的文件更大。两种方式可混合。

高可用

可用性策略

  • 可用性评估:平均失效时间(MTBF)和平均恢复时间(MTTR)。不可用时间 -- 99%-5256m,99.9%-526m,99.99%-53m, 99.999%-5m。获得可用性与投入成本是非线性的。越高的可用性需要越高的成本,因此评估应用所需的可用性是必要的。提炼应用的核心模块,对更小的系统提升可用性成本会更小。使用风险敞口来评估优先考虑的可用性需求。风险敞口 = 失效概率 * 失效损失。
  • 宕机原因分析: 1. 运行环境,磁盘故障居多; 2. 性能问题,大流量杀手;3. 软件 bug; 4. 变更管理操作出错。
  • 高可用策略:思路 - 避免导致宕机的主要原因、快速恢复故障。三大措施 - 限流、降级、冗余。方法层面 - 避免单点失效、冗余和故障转移。执行层面 - 主备、对称布局;评估组件切换时间和故障恢复时长(估算和演练);备库演练(记录切换时的工作负载);虚拟IP、代理、端口转发、NAT;运维操作安全准则、故障演练。

限流

熔断降级


冗余

冗余是保证可用性的基础。

  • 冗余是指一份数据有多个副本存储在不同的节点上。冗余是应对容错和提升可用性的重要机制,同时又会带来一致性问题。
  • 副本协议:中心化副本协议,primary-backup 协议、去中心化副本协议。主节点写入成功后,发送操作日志给指定节点或全部节点的副本,指定节点或全部节点根据操作日志写入成功后回复主节点,主节点回复写入成功。
  • 一致与可用:强一致复制 -- 如果所有副本写入成功,主节点才返回客户端成功,则是强一致同步复制。但如果某个副本失败,则整体写入失败,此时无法达成可用性。弱一致复制 -- 只要主副本写入成功,就返回客户端成功,通过线程异步发送给其他节点去更新副本,则是弱一致同步复制。某个副本写入失败,不影响整体写入的结果。此时,可以达成可用性,但无法达成强一致性。
  • 多个副本不能全部放在同一个节点或机架上。否则,节点或机架故障时,数据会丢失,无法达成可用性。

复制

“【总结系列】互联网服务端技术体系:高可用之复制技术”


一致性

数据的多副本在一定时刻点之后保持一致性的要求。

分类

要保证数据一致性,根据应用实际需求,有多种一致性的定义。

  • 时间点一致性:如果所有相关的数据组件在任意时刻都是一致的,那么可以称作为时间点一致性。
  • 事务一致性:事务的一致性指的是数据库的数据从事务执行之前的有效状态变更为事务执行之后的另一种有效状态。数据有效状态是指必须满足数据的约束与完整性(事先在数据库里定义的数据规则集合)。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于新的有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始有效状态。
  • 应用一致性:在应用程序中涉及多个不同的单机事务,在所有的单机事务完成之前和完成之后,多个应用的数据总是符合指定的规则集合。
  • 区别与联系:事务一致性和应用一致性,可以看成数据约束和完整的有效性,这些数据不一定是同一个数据项的定义(比如支付成功后送优惠与送积分,退款后退券与退积分);时间点的一致性,主要是多副本的数据内容要相同,通常属于同一个数据项定义(比如DB、ES、HBase 上的订单状态是同一个值)。

程度

一致性程度的观察角度:从所有的复制节点来看。

  • 一致性程度: 线性一致性 > 顺序一致性 > 因果一致性 > 最终一致性
  • 线性一致性:亦称原子一致性。任何进程的写操作,如果一个节点进程的读操作读到了最新值,则所有后续的读操作都应当读到最新值。反之,则不满足线性一致性。难点:分布式下的统一的全局时钟。
  • 顺序一致性:顺序一致性是指所有的进程以相同的顺序看到所有的修改。读操作未必能及时得到此前其他进程对同一数据的写更新。但是每个进程读到的该数据的不同值的顺序是一致的。
  • 因果一致性: 有因果关系的操作满足顺序一致性。比如一个线程先写一个变量,再读一个变量,那么它读到的一定是写之后的值。就满足因果一致性。
  • 最终一致性:不需要关注中间变化的顺序,只需要保证在某个时间点一致即可。

手段

  • 协议算法
  • 异步复制
  • 实时计算与同步
  • 重试、补偿

Raft协议

  • Raft 是一个基于复制状态机的一致性算法,用于分布式存储系统。Raft 将一致性保证分解如下子问题:Leader election, Log replication, safety, and membership changes。Raft 保证一个强有力的 Leader ,只允许 Leader 发送复制日志 ;复制状态机通常使用复制日志实现,每个服务器存储一个包含一系列命令的日志,其状态机按顺序执行日志中的命令。 每个日志中命令都相同并且顺序也一样,因此每个状态机处理相同的命令序列。 这样就能得到相同的状态和相同的输出序列;Safety 机制用来保证在 Leader, Follower 崩溃的情况下,依然能够保证 Leader 总有所有已提交的日志。一致性算法的工作就是保证复制日志的一致性。
  • Leader election:时间被随机划分为多个时间片。每个时间片作为一个“任期”,在任期内进行选举,选举出来的 Leader 负责管理 log replication。Leader 周期性地向所有 follower 发送心跳(不包含日志条目的 AppendEntries RPC)来维持自己的地位。每个服务器都有一个任期号。如果 RequestVote RPC 中的任期号比自己小,那么 candidate 就会拒绝这次的 RPC 并且继续保持 candidate 状态。Raft 算法使用随机选举超时时间的方法来确保很少发生选票瓜分的情况,就算发生也能很快地解决。
  • Log replication:Leader 一旦被选举出来,就开始为客户端请求提供服务。客户端的每一个请求都包含一条将被复制状态机执行的指令。Leader 把该指令作为一个新的条目追加到日志中去,然后并行的发起 AppendEntries RPC 给其他的服务器,让它们复制该条目。当该条目被安全地复制,leader 会应用该条目到它的状态机中(状态机执行该指令)然后把执行的结果返回给客户端。如果 follower 崩溃或者运行缓慢,或者网络丢包,leader 会不断地重试 AppendEntries RPC(即使已经回复了客户端)直到所有的 follower 最终都存储了所有的日志条目。复制状态机用于解决分布式系统中的各种容错问题。
  • Safety:Raft 使用投票的方式来阻止 candidate 赢得选举除非该 candidate 包含了所有已经提交的日志条目。候选人为了赢得选举必须与集群中的过半节点通信,这意味着至少其中一个服务器节点包含了所有已提交的日志条目。如果 candidate 的日志至少和过半的服务器节点一样新(接下来会精确地定义“新”),那么他一定包含了所有已经提交的日志条目。RequestVote RPC 执行了这样的限制: RPC 中包含了 candidate 的日志信息,如果投票者自己的日志比 candidate 的还新,它会拒绝掉该投票请求。Raft 通过比较两份日志中最后一条日志条目的索引值和任期号来定义谁的日志比较新。如果两份日志最后条目的任期号不同,那么任期号大的日志更新。如果两份日志最后条目的任期号相同,那么日志较长的那个更新。Safety 五法则:1. 至多只有一个 Leader 被选举; 2. Leader 只能添加新日志条目,不能修改或删除日志条目; 3. 两条日志条目具有相同的 index 和 任期,则视为等同; 4. 如果一个日志条目被提交,那么它一定出现在最高任期的 Leader 的提交日志条目里; 5. 如果一个节点服务器应用了某个 index 在状态机上,那么没有任何一个其他节点服务器会在相同的 index 具有不同的日志条目。

海量

存储

数据存储及索引结构

事务

文件系统

  • 音视频存储

分析

  • Hive: Hive SQL

实时计算

  • Storm, Spark , Flink

安全

  • 常见安全问题:缓冲区溢出、XSS、SQL注入、DDOS
  • 安全评估:资产(数据)等级分析、威胁分析、风险分析、解决方案确认
  • 机密性保证:加密(对称加密和非对称加密)、脱敏(敏感信息避免模糊化处理)、代理隐藏、权限管控
  • 完整性保证:数字签名
  • 可用性保证:黑名单、限流、防火墙、清洗流量
  • 威胁建模:STRIDE 模型。伪装、篡改、抵赖、信息泄露、拒绝服务、提升权限。
  • 风险分析:DREAD 模型。Damage Potential(完整验证权限、执行管理员操作、非法上传文件)、Reproducibility (随意再次攻击)、Exploitability(易学性,初学者可以很快学会)、Affected Users(所有用户、默认配置、关键用户)、Discoverability(漏洞很显眼,攻击条件易得)
  • Secure by Default: 白名单原则 , 最小权限原则。 采用黑名单容易绕过或遗漏。白名单要避免范围过大的通配符。
  • 纵深防御原则:从不同层次、不同方面实施安全方案,构成防御整体。避免薄弱环节与短板。
  • 数据与代码分离原则:防止 缓冲区溢出、SQL 注入、XSS。
  • 不可预测原则:让系统的关键运行区域显示出随机性,从而大幅提升恶意构造请求来攻击的难度和成本。请求 token 机制。
  • 源头堵截原则:在源头上堵截漏洞,而不要依靠很多人通过学习来掌握防御技术。让防御变得易学易用。

智能化

  • 机器学习

平台

操作系统

内存管理

  • 虚拟内存。建立从虚拟地址空间到物理内存地址的映射。由 MMU 中的地址翻译硬件、页表(PT,常驻物理内存,进程专享)、操作系统共同实现。虚拟内存的用途:1. 磁盘空间的缓存工具; 2. 内存管理工具,为进程提供独立的地址空间,互不影响; 3. 灵活的内存分配,虚拟地址可以连续,而对应的物理地址可以不连续;4. 保护内存不受进程破坏,不允许修改只读代码段和内核的代码与数据,不允许修改其他进程的地址空间的数据,不允许修改共享页面。可以为 PT 添加一些许可位来控制进程的读写权限。
  • 内存布局。指各种元数据、数据、代码段在内存中的有序布置。设计良好的内存布局,更有利于访问数据和代码。具体示例可参考 ELF 可重定向目标文件格式。JVM 也有自己的内存布局。
  • 内存分配。内存分配策略 - 首次适配(简单)、最佳适配(碎片小,但很难再次分配,需要合并)、最坏适配(分配后仍然可再次分配,大块内存难以满足)。算法 - Buddy, CMA, slab。Bump the Pointer(如果未使用内存与已使用内存分离了,Serial, ParNew)、Free List(找适配大小的空闲块,Mark-Sweep, CMS)。分配内存的并发问题解决方案:CAS 机制和 预先分配的 TLAB(本地线程缓冲分配)。指向对象的指针或引用在虚拟机栈上分配。

程序执行

  • 链接与加载。将各种代码片段、数据片段进行收集并组合起来,构成完整的可执行目标文件,并加载到内存中执行的过程。链接与加载涉及模块和库依赖、符号引用、作用域、动态共享库之类的话题。链接器的主要任务:符号引用和重定向。每个可重定向目标模块都有一个符号表,用来做符号解析。

JVM

内存管理

  • 内存布局:PC寄存器(指令地址)、虚拟机栈(方法执行)、本地方法栈(Native方法执行)、堆(对象内存分配)、方法区(类加载信息、运行时常量池、静态变量、即时编译信息等)、直接内存(NewIO)。每个线程都有专属的【虚拟机栈、PC寄存器 、本地方法栈】。
  • 对象创建和初始化:new 参数 -> 定位类引用 -> 检查类是否加载、解析、初始化 -> 如果没有则执行类加载过程 -> 为对象分配内存 -> 分配空间清零 -> 对象头内容设置 -> 执行 _init 方法初始化对象。
  • 对象内存布局:对象头(MarkWord)、实例数据、对齐填充。 对象头 -- 对象哈希码、分代年龄、轻量锁、重量锁、偏向、类元数据指针。实例数据 -- 虚拟机字段分配策略和程序指定顺序。字节数相同的类型的字段放在一起分配。对齐填充 -- 对象起始地址必须是 8 的整数倍。访问对象的方式:对象句柄池和直接指针。使用对象句柄的好处是引用的地址是稳定的,只要改变句柄的值(对象移动时),使用直接指针的好处是效率更高,减少一次指向过程。
  • 对象引用分析:对象可达性遍历判断对象是否活跃。强、软、弱、虚引用。强 - 只要有引用就不回收;软 - 在抛出 OOM 之前进行回收;弱 - 下一次 GC 时回收;虚 - 持有虚引用的对象在GC时可以收到系统通知。
  • GC算法:复制算法(新生代,优化:迭代避免溢出、多空间提升空间利用率);标记-清除算法(老年代,优化:多空闲链表、位图标记、延迟清除)。 GC 收集器:三个维度,单线程与多线程,Client 与 Server,新生代和老年代。Serial- 单线程,Client, Par- 多线程。CMS 更好的响应,Parallel Scavenge 更好的吞吐。G1 :可预测停顿时间。 GC 日志分析。
  • 内存问题:OOM, Memory Leak , Memory Overflow,Stack Overflow ,Method Area Overflow。 OOM -- 线程转储,定位原因;Memory Overflow -- 堆内存空间不足,死循环; Memory Leak 通常是资源没有正确释放,无法及时 GC ,使用 MAT 等工具分析从 GC ROOT 的引用链;Stack Overflow -- 递归过深,分配数组大小过大;Method Area Overflow -- 字节码技术动态生成或增强的大量类载入方法区可能导致方法区溢出(CGLib, JSP, OSGi)。
  • FullGC 原因及方案:1. 大对象列表,很大的列表,拆分小对象/小列表,及时释放; 2. Old 空间小,可以调大 Old 区; 3. 不要轻易手动调用 System.gc 方法。

类加载

  • 平台无关性和语言无关性:字节码与虚拟机。将高级语言编译成符合虚拟机规范的字节码,从而能够在符合规范的虚拟机上执行字节码,最终映射到本地机器指令集。操作系统平台和编程语言对应用程序是透明的。
  • 重要元素:Class 类文件 - 字节码指令集 - 类加载器 - 加载机制。Class 类文件是 Java 类或接口的字节码表示,字节码指令集是字节码的主要元素。类加载器根据 Class 类文件在虚拟机中创建类对应的 Class 对象 。Java 类加载器包含 Bootstrap 加载器、Ext 加载器、App 加载器、Customized 加载器。类加载的顺序是先用前面的加载器进行加载,找不到则用下一个。类或接口的唯一标识:类或接口的名称 + 定义类加载器。不同类加载器加载同一个类或接口在 JVM 是不同的。
  • 类加载过程:加载-链接-与初始化:加载 -- 查找类或接口的二进制表示,并据此来创建类或接口的 Class 对象; 链接 -- 将类或接口链接到虚拟机运行时状态,链接包括验证、准备、解析; 初始化 -- 执行初始化方法
  • 加载:运行时常量池存储着类、类的成员和方法的各种信息的二进制表示,符号引用来自于 CONSTANT_XXX_info 结构。
  • 初始化:调用 new, getstatic, putstatic, invokestatic 指令时触发。初始化需要保证多线程环境的安全性,由虚拟机负责实现。每个类或接口 C 都有一个对应的唯一初始化锁 LC。初始化之前需要先获取这个锁 LC ,分情况进行处理并释放锁。

安全机制

  • 安全模型:沙箱模型、限定于 JVM 范围。主要是对本地系统资源的访问限制。类加载验证。安全策略。域、代理、权限组。doPrivileged。

小结

数据结构与算法是构建互联网的基本材料,网络、存储、操作系统、分布式是现代互联网应用的服务端的基石,而语言、平台、框架、中间件则是在基石之上的基础设施,它们共同构建了稳固的互联网大厦。

体系结构是一种强大的方法论。打造一个知识体系,构建底层逻辑,理解上层变化,自下而上打通,持续融入所学。

PS:以前,我常觉得业界写具体技术的书比较多,而总结软件设计思想和案例的书太少,不利于开发者的设计能力的快速提升。现在看来,业界处处都有优秀的框架、中间件、平台设计,简直是宝山在眼前,而我却视若无睹。不过,要能从这宝山里挖掘财富,其实也不容易。

参考文献

posted @ 2020-09-08 16:33  琴水玉  阅读(1638)  评论(3编辑  收藏  举报