zoukankan      html  css  js  c++  java
  • 3千字了解微服务持续演进

    第一章、微服务架构

    1.1 为什么采用微服务架构?

    1.1.1 单体架构和微服务架构

    很难用一个绝对的方式去判断架构的好坏,在大多数情况下,我们很难从一个外部的视角去判断服务拆分的合理性,需要对上下文非常了解才能做出好的决策。

    可以综合下面表中的多个维度进行分析


    1.1.2 什么时候开始微服务架构

    产品初期优先使用单体架构,面对一个新的领域,对业务的理解很难在开始阶段就比较清晰,往往是经过一段时间后,才能逐步弄清楚。

    在服务划分之前,应该保证基础设施及公共基础服务已经准备好。如:监控服务、自动化运维工具、服务化框架等(灰度发布、资源调度)。

    1.1.3 拆分粒度

    微服务里面的微应该解释为“合适”,但是这个词比较含糊。对业务不够理解,对团队情况不够理解,都无权协助确定服务的粒度。随着业务发展,粒度可能还会发生变化。

    每个人情况不一样,有的公司认为团队规模是决定性的,有的认为交付速度是决定性的,找到你的决定性因素,来做拆分即可。实在找不到合适的依据,可以参考此表。

    1.2 微服务设计原则

    • 垂直划分优先原则

    应该根据业务领域对服务进行垂直划分。

    • 持续演进原则

    服务数量快速增长带来的架构复杂度急剧升高,开发、测试、运维等环节很难快速适应。非必要情况,应逐步划分,持续演进。

    • 服务自治、接口隔离

    尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。

    • 自动化驱动原则

    服务的增多,部署和运维的成本就会呈指数型增长,应该首先构建自动化的工具和环境。

    1.3 微服务架构实施的先决条件

    1.3.1 研发环境和流程上的转变

    在微服务架构实施之前,要准备相关的环境和流程。

    • 自动化工具链

    微服务可以基于自动化工具链,以流水线的交付方式串联整个DevOps流程。

    • 微服务框架

    先进行微服务框架的选型和试用

    • 快速申请资源
    • 故障反馈机制

    需要全面的监控故障,及时处理并发出报警。

    • 研发流程转变

    需要重建团队,以服务为核心,按照业务领域划分全功能团队。

    1.3.2 拆分前先做好解耦
    • 状态外置

      1. 定时任务:有很多任务不能重复触发,所以需要把定时任务从业务服务中提取出来,通过分布式任务系统调度。
      2. 本地存储:在本地存储文件的方式比较常见,但是当有多个实例的时候,要么需要全部同步,要么需要路由到一个实例。
      3. 本地缓存:如session数据,可以通过分布式缓存解决
    • 去触发器,存储过程

      1. 当有触发器,存储过程时,整体伸缩难以扩展
      2. 当存在水平分表时,可能无法满足需求
      3. 如果触发器,存储过程过多,则会导致运维复杂度增高
    • 通过接口隔离

      1. 如果直接去连接其他服务的数据库,当其他服务的数据结构变化,自己的服务也要跟着调整。
      2. 限流某个接口或者缓存也做不成,因为别人直接访问你的数据库

    1.4 微服务划分模式

    1.4.1 基于业务复杂度划分

    当业务复杂度较低时,可以选择基于数据驱动划分服务。数据驱动更容易理解和上手。

    1.4.2 基于数据驱动划分服务

    数据驱动是自下而上的架构设计方法,强调的是数据结构。也就是分析需求确定数据结构,然后根据表之间的关系划分服务。

    (1)需求分析,总结用户故事
    (2)抽象数据结构
    (3)划分服务
    (4)确定服务调用关系,根据流程图来确定
    (5)业务流程验证,验证划分的服务是否合适,可以根据以下几个问题来考虑

    • 一次更新操作如需跨越几个服务,一致性要求是什么
    • 跨服务查询时,是否要做关联查询
    • 性能是否满足要求
    • 成本是否满足要求

    (6)持续优化

    1.4.3 基于领域驱动划分服务

    领域驱动是自上而下的设计方法,确定关键业务场景,确定业务边界。领域驱动更注重业务实现效果,认为自下而上的设计会让技术人员不能很好的理解业务方向,进而偏离业务目标。

    1.4.4 从已有的单体架构中划分服务

    (1) 通常前后端分离是第一步
    (2) 提取公共基础服务
    (3) 不断地从老系统中抽象出服务,垂直划分优先

    1.4.5 微服务拆分策略
    • 比较独立的新业务
    • 优先抽取通用服务
    • 优先抽取比较容易识别的,边界比较明显的服务。包结构比较清晰的,比较好操作。
    • 优先抽象核心服务。边缘服务抽出去还要增加维护成本没有必要
    • 优先抽象具有独立属性的服务

    第二章:敏捷基础设施

    2.1 传统基础设施面临的挑战
    • 资源利用率低,服务器没有被完全利用
    • 服务器数量呈爆炸性增长
    • 没有标准化,因为一些事故,导致运维操作了某台服务器,那么这台与其他服务器有差异
    • 脆弱的基础设施。传统运维方式花费大量时间
    2.2 敏捷基础设施

    第一阶段:运维“全人肉”操作

    第二阶段:脚本阶段

    第三阶段:工具阶段。通过私有云管理虚拟机,通过CI工具实现持续部署。运维人员通过虚拟机镜像来封装常用依赖环境。但是开发环境、测试环境、生产环境差距很大。可能开发环境可以的,上了测试环境不行

    第四阶段:敏捷基础设施阶段,无须运维人员,全部自动化,通过容器封装环境,实现开发、测试、生产环境的一致。敏捷基础设施也可称为基础设施既代码,基础设施配置可以当作编写代码进行。整个过程只需要开发人员负责,无需运维人员参与。

    2.3 基于容器的敏捷基础设施

    敏捷基础设施的目标如下:

    • 标准化
    • 可替换。任意节点能够被轻易的创建,销毁,替换
    • 自动化。
    • 可视化。当前环境要做到可控,就需要对当前环境情况可视
    • 可追溯。所有的配置统一作为代码进行版本化管理
    • 快速。资源申请和释放要求秒级

    目前比较常用的基础设施自动化工具有:Ansible、Chef、SaltStack、Terraform等。

    2.3.1 容器 VS 虚拟机

    容器和虚拟机的区别:虚拟机是在硬件的基础上进行虚拟化,隔离性更高,而容器是在操作系统上进行的虚拟化。严格意义上来说,容器并不是虚拟化,因为所有容器是共享内核的。区别如图所示:

    • 资源利用率。容器更轻量级,比虚拟机资源利用率更高
    • 创建速度。Docker的启动速度是秒级的,虚拟机启动是分钟级的
    • 性能。虚拟机需要运行完整的Guest OS,不可避免的会出现性能损失。而容器相当于一个进程,性能相当于物理机
    • 隔离性。虚拟机隔离性更高。
    2.3.2 Docker

    安装及使用

    2.4 监控告警服务

    传统的监控更强调以资源为中心,关注CPU、内存、宽带等。这种方式不够精确。现在更偏向于应用,如订单量下降一半是否存在问题,吞吐量到达阈值后先关注依赖的其他系统是否正常在弹性伸缩。

    2.4.1 监控数据采集

    (1)直接上报
    (2)通过打印的日志上报
    (3)通过agent上报

    2.4.2 通过Prometheus和Grafana监控

    2.5 分布式消息中间件

    2.5.1 常见的消息中间件

    ActiveMQ

    优点:历史悠久,功能丰富,能够适配各种协议,文档多,有鉴权机制,多语言客户端

    缺点:性能差、只支持主从,扩展性差

    RabbitMQ

    优点:可以看作ActiveMQ的改进版,用Erlang语言实现,性能比ActiveMQ高

    缺点:虽然性能有所提高,但是对比Kafaka、RocketMQ还有差距。只支持主从,扩展性差

    Kafka

    优点:性能非常高,0.8版本后可靠性得到了保障,分布式能力强大。

    缺点:支持的协议少,工具少

    RocketMQ

    优点:基于Java语言开发,模仿了Kafka的设计理念,继承了高性能,分布式能力强的优点。同时有一些对企业比较好的功能,如:消息服务端过滤,定时消息等。

    2.5.2 Kafka的设计原理

    • Broker:Kafka的服务端,负责接收数据,并持久化数据,Broker可以有多个,每个Broker可以包含多个Topic,Broker并不保存Offset,由Consumer自己保存,默认保存在Zookeeper中
    • Producer:生产者。生产数据发送到Broker,Producer直连Broker,不经过任何代理。Producer还有异步发送功能,也就是说多条消息缓冲到客户端,达到一定数量或时间后,批量发送给Broker。通常Producer是一个包含Kafka客户端的业务服务。
    • Consumer:消费者。业务服务从Broker订阅Topic。每个消费组属于某个消费者组,一个组里的消费者订阅的是同一个Topic,同一个组的消费者分别订阅同一个Topic下面的不同的Partition的数据。每个Partition只能被一个消费者订阅,
    • Topic:主题。Topic更像一个逻辑概念,每个Topic下包含了多个Partition,所有的元数据都存在Zookeeper中。
    • Partition:分区。Kafka为了扩展性,提升性能,可以将一个Topic拆分为多个分区,每个分区可以独立放到一个Broker上。
    2.5.3 为什么Kafka性能高
    • 顺序访问磁盘,访问速度比随机访问磁盘快了非常多
    • 零拷贝
    2.5.4 Kafka的数据存储结构

    Kafka的存储设计基于日志实现,非常简单。这里的日志是按时间序列排列的追加记录序列,只能在末尾添加,不能修改,以此来利用磁盘顺序写的能力。

    一个Partition是一个文件夹,每个Partition下包含多个Segment。物理上每个Segment包含两种文件,一个是数据文件,以log结尾,一个是索引文件,以index结尾。

    2.5.5 如何保证Kafka不丢消息

    1.ACK

    生产消息投递到Broker时,可以通过ACK保证消息投递

    • 最多一次。消息可能会丢失
    • 最少一次。消息绝对不丢,但是会重复
    • 有且只有一次

    2.复制机制

    谈到复制机制,必须先从消息的持久化谈起。Linux写文件有如下三种方式。

    • 直接持久化到磁盘
    • 写到内核态Buffer,间隔一段时间刷新一次磁盘
    • 数据直接持久化,元数据间隔一段时间持久化

    RocketMQ可以在配置文件上设置前两种写文件的格式,Kafka当前版本还不支持。因此单机的可靠性上RocketMQ优于Kafka。Kafka保证可靠性的依赖是复制机制。

    举例说明,假如Topic1复制因子设置为3,分区数为2。当生产者发送消息时,先计算属于哪个Partition,如果属于part1,则会发送至Broker0,写入Topic1-part1-leader,Broker1和Broker2下面的Topic1-part1-follower会去拉取消息并复制到本地,一旦副本数够了,leader就会提交。

    3.消息删除机制
    Broker默认7天才删除消息

    4.发送消息
    Kafka支持在生产者一侧进行本地buffer,也就是累积到一定条数才发送。生产端可以设置producer.type=asyncsync,默认为sync。当然设置为async会提高性能,但是如果消息缓存到了本地,还没发出去就挂了,就会丢消息。

    5.消费消息
    消费消息的时候,如果更注重可靠性,可以显示提交Offset,也就是当业务都处理完了再提交Offset,当然这可能会导致重复消费,需要提供幂等性接口。

    2.6 分布式缓存服务

    2.6.1 分布式缓存的应用场景

    按照缓存的位置分类,可以分为本地缓存和分布式缓存。相较于本地缓存,分布式缓存有具有如下优点:

    • 不需要各个业务节点同步数据
    • 能够做到业务服务无状态
    • 不需要管理数据。例如Java,本地内存多了会导致FullGC

    当然本地缓存也有优点,性能更高,不用维护额外的缓存服务。

    那么什么样得数据可以放进缓存:

    • 数据量比较小
    • 不经常变化
    • 计算代价比较高的
    • 核心热点数据

    相反,什么样的数据不应该被缓存呢?

    • 变化比较快的数据
    • 要求强一致性的数据
    2.6.2 常用的分布式缓存Memcached

    Memcached是一款开源、高性能的分布式内存对象缓存系统。

    2.6.3 常用的分布式缓存Redis

    可以基于Codis实现Redis分布式缓存集群

    Codis是一个基于代理的分布式Redis解决方案,业务应用可以像使用单机Redis那样使用它。

    2.7 分布式任务调度服务

    一般对可用性和性能要求不高的任务,采用单点即可,例如Spring的Quarz。但是对可用性的要求更高的话,上面的方案就不适用了。

    分布式调度至少满足两个要求:

    • 不重复的执行任务
    • 不遗漏的执行任务
    2.7.1 通过Tbschedule实现分布式调度

    Tbschedule是阿里开源的分布式任务调度系统,后期维护较少,但是代码简单,可塑性好。

    2.7.2 通过Elastic-Job实现分布式调度

    由当当开源的,国内应用十分广泛。原理是通过Zookeeper作为分布式协调服务实现任务调度的。

    2.8 如何生成分布式ID

    1. UUID

    UUID由以下几部分组成:

    • 当前日期和时间
    • 时钟序列
    • 全局唯一的IEEE机器识别号,如果有网卡,则从网卡MAC地址获得。
    1. SnowFlake

    核心算法是毫秒级时间41位+10位机器ID+毫秒级内序列12位。
    3. Ticket Server

    利用Mysql自增长ID实现。可以利用多台Mysql实现高扩展性和高可用性。

    首先要保证ID是全局唯一的,另外,由于无法统一分布式环境的每台服务器的时钟,无法做到全局递增,因此A服务可能取ID早,但是入库晚。因此分布式ID无法保证全局顺序性。

    第三章、可用性设计

    3.1 可用性概述

    3.1.1 可用性描述

    可用性:是关于系统可以被使用的时间的描述,以丢失的时间为驱动。

    可用性的衡量标准通常是以N个9来量化的。

    可用性等级表:

    3.1.2 是什么降低了可用性
    • 发布。当应用需要升级的时候,为了更好的用户体验,应用不能中断,如果需要迁移数据,会导致整个流程非常复杂。为了降低复杂度和成本,我们通常会暂时中断服务。
    • 故障。如内存溢出。
    • 压力。流量突然增大会造成系统宕机。
    • 外部强依赖。如果外部依赖的服务发生故障,则会导致调用异常。

    3.2 逐步切换

    3.2.1 影子测试

    影子测试是一种常用的生产环境中通过流量复制、回放、对比的测试方法。先同步新老数据库内的数据,在不影响老服务的情况下,在负载均衡的位置记录请求日志,通过日志回放服务向新服务发送请求,新服务正常处理业务逻辑后入库,最后对比验证服务、两个库之间的数据差异。如果比对都正确,说明新服务和老服务逻辑上是等价的。

    3.2.2 蓝绿部署

    在生产环境中,除了正在运行的环境(蓝色环境),还需要冗余一份相同的环境(绿色环境)。如果要将服务由v1升级到v2,则先在绿色环境上部署,测试通过后将流量、路由指向绿色环境,一旦发生故障需要回退时,只需要切换到蓝色环境即可。

    虽然听起来不错,但是需要注意以下细节:

    • 最好有自动化的基础设施支持
    • 全面的监控
    • 两套环境隔离风险,有互相影响的风险
    • 难点是数据结构发生变化时,如何同步数据,故障后如何回滚
    • 切换时需要优雅的终止,禁止直接kill进程
    3.2.3 灰度发布、金丝雀发布

    金丝雀发布和灰度发布很像。

    金丝雀发布:
    (1)在负载均衡列表中摘掉一个节点,作为“金丝雀”服务器。
    (2)在“金丝雀”服务器上部署新版本
    (3)进行自动化测试
    (4)将“金丝雀”节点添加到负载均衡列表上
    (5)如果发生故障,则回滚
    (6)如果没有问题,则逐步升级剩余的其他节点。

    灰度发布的意义在于:

    • 减少波及的范围
    • 尽早得到用户的反馈

    一般互联网公司还会有独立的灰度发布引擎,由运维人员设置规则,可以使内部员工用户先进入新版本进行测试。

    流程如下:
    内部员工-》外部1%用户-》5%用户-10%用户-》全网发布

    3.3 容错设计

    3.3.1 消除单点

    多节点部署

    3.3.2 特性开关

    当某个功能有问题时,可以通过开关关掉。

    3.3.3 服务分级

    服务分级实际上就是服务的标签,表示服务的关键程度。

    3.3.4 服务降级

    降级是指为了保障核心功能,利用目前有限的资源,通过开关关闭非核心服务。

    通常有哪些方式呢?

    • 关闭某个功能
    • 请求短路,直接返回缓存结果
    • 简化流程,直接放弃某个操作。如给用户发注册成功短信
    • 延迟执行,停止定时任务,如某些结算

    降级的前提是进行分级,需要在设计阶段明确降级的条件,是吞吐量太大了,还是响应时间太长了,或者是某个依赖服务不可用了。

    降级的方法如下:

    • 页面加开关,通过js控制是否隐藏
    • 关闭低级别服务前端页面。例如一些运营系统
    • 关闭定时任务。有一些非核心的定时任务可以延迟在跑
    • 预先定义降级逻辑。在配置中心配置一个变量,预先定义好变量的含义,例如变量值为3,则控制3级以下的服务不可调用
    • 降低精确度。例如在电商中库存可以显示为无货、有货,而不是具体的数量。

    总之,降级是不得已而为之的,至少比宕机的用户体验好。

    3.3.5 超时重试

    需要考虑如下参数:

    • 超时时间
    • 重试总次数
    • 重试的间隔时间
    • 重试间隔时间的衰减度

    方案:基于spring-retry进行重试

    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
        <version>1.3.0</version>
    </dependency>
    

    方案:基于Guava-retrying进行重试

    <dependency>
        <groupId>com.github.rholder</groupId>
        <artifactId>guava-retrying</artifactId>
        <version>2.0.0</version>
    </dependency>
    
    3.3.6 隔离策略

    隔离是为了发生故障时,限制传播范围。

    • 线程池隔离
    • 进程隔离
    • 集群隔离
    • 用户隔离
    • 租户隔离

    通过Hystrix实现隔离。

    3.3.7 熔断器

    可以配置超时时间、一个统计窗口内失败多少次熔断、熔断多少秒后去重新尝试、失败率达到多少熔断等。

    3.4 流控设计

    3.4.1 限流算法

    固定窗口算法

    对于限流来说,最简单的方法就是通过一个变量记录单位时间内的访问次数。例:如果一分钟内请求次数超过1000,那么拒绝1000以后发过来的请求,1分钟后次数归零。

    但是这个算法有个漏洞,就是在次数清零的前后,是可能突然各涌入1000的请求的,就会有2000的请求并发。

    漏桶算法

    漏桶算法是控制数据注入网络的速率,平滑网络上的突发流量。

    漏桶算法简单描述如下:

    • 水(请求)先进入漏桶(队列)
    • 漏桶(队列)以一定的速度出水(请求)
    • 水(请求)过大会直接溢出(丢弃数据包)

    漏桶算法可以看作是一个先进先出的队列。当队列填满的时候,抛弃新请求,队列不保证某个时间点内请求一定会得到处理。往往当消费端处理能力有限时,通过消息队列削峰。例如秒杀场景:如果最后只秒杀一个商品,那么队列保留适当的请求,就能保证结果成功,不必处理所有请求。

    令牌桶算法

    令牌桶控制的是一个时间窗口内通过的数据量,通常以QPS、TPS衡量。

    令牌桶简单描述如下:

    • 每秒会有x各令牌放入桶中
    • 桶中最多放n各令牌,如果桶满了,则新放入的令牌丢弃
    • 当一个m字节的数据包到达时,消耗m个令牌,然后发送数据包。
    • 如果桶中可用令牌小于m个,则该数据包将被缓存或丢弃。

    令牌算法和漏桶算法不一样,令牌桶允许突发流量,只要有令牌就可以执行。

    3.4.2 流控策略

    在分布式系统中,每个环节都要考虑流控。限流一般是根据压测结果、生产环境上的表现对各个服务进行设定。

    通常,下面几个重要的节点需要考虑。

    • 请求入口处。如Nginx
    • 业务服务入口处
    • 公共基础服务处
    3.4.3 基于Guava限流
    3.4.4 基于Nginx限流

    连接数限流模块:ngx_http_limit_conn_module

    请求限制模块:ngx_http_limit_req_module

    3.5 容量预估

    传统的压测方式是在测试环境下进行的,针对场景进行数据模拟,需要开发、测试人员根据线上的场景,评估可能出现的情况。但是这种做法非常不准确,很难模拟出接近生产环境的场景和数据。

    互联网公司普通采用全链路压测的方式。全链路压测平台在请求入口进行真实的流量复制,为了加大压力,可以通过TCPCopy的参数调节。在数据库一侧通过影子表进行隔离,影子表和生产表建立相同的数据结构,通过后缀区分。

    全链路压测需要注意以下几点:

    • 找到核心流程,全链路压测成本巨大,不可能全做,一个系统中核心的20%才是压测的目标
    • 选择隔离方式。一种是独立的环境进行压测,隔离效果好。另一种是和生产环境混合,通过参数识别,在框架里进行特殊处理
    • 缩小依赖服务范围。如果对A服务压测,那么A服务依赖的服务如何配合。

    3.6 故障演练

    我们害怕故障,做了很多应对策略,但是这些策略在没有测试的情况下谁也不敢轻易启用。

    阿里也进行故障演练,他的工具叫MonkeyKing,可以模拟硬件故障、API故障、分布式故障、数据库故障。

    3.7 数据迁移

    因为大多数服务是从单体架构开始,伴随业务的发展开始拆分,所以会涉及到数据迁移。

    3.7.1 逻辑分离,物理不分离

    是指老服务和新服务放在同一个库里,建立不同的表名,从代码层面实现隔离、解耦。数据迁移可以通过触发器实现,或者双写的方式实现。

    3.7.2 物理分离

    新服务和老服务的数据通过不同的数据库物理隔离。可以使用相同的表名,数据同步需要额外的方案实现。

    • 利用数据库同步工具读取binlog
    • 业务应用上写两个库
    • 老系统在写数据库的同时,发消息到中间件实现同步

    第四章、可扩展性设计

    4.1 横向扩展

    横向扩展:指用更多的节点支撑更大量的请求。
    纵向扩展:扩展一个点的能力支撑更大的请求。例如将磁盘升级为SSD。

    4.2 扩展数据库后查询

    4.2.1 带拆分键

    拆分的时候携带拆分键,通过对拆分键进行路由查询,就知道查哪张表了,不然并行的去查所有表导致性能问题。

    4.2.2 拆分库之后的关联查询

    订单表和用户表是分别在不同的数据库,那么要想查询“订单金额大于100的用户”就比较麻烦。

    方案1:建立多维度数据库

    尝试建议一个综合数据库,相当于为了进行关联查询多冗余了一份数据。电商系统中,商品、价格、库存划分为了多个数据库。

    可以建立一个综合数据库更新时,通过消息中间件更新到综合数据库内。
    查询时,直接从综合数据库查询。

    方案2:建立外部搜索引擎

    通过分布式搜索引擎进行全文检索

    方案3:通过分布式缓存

    通过分布式缓存冗余数据。如果数据量比较少,可以采用这种方式。

    4.2.3 数据库分表经典案例

    案例1: 活动平台数据表水平切分

    假设有一个活动平台,管理员可以创建活动,为活动添加用户,针对一个活动给用户发送促销短信或邮件提醒。

    通常用户数据大的时候,会把用户单独在一个数据库中。活动和活动关系表放在另一个数据库中。

    但是如果活动用户关系表数据量很大时,如果按活动进行分片,就会导致热点数据,因为有的活动关注的用户多,有的活动关注的用户少。
    如果根据用户分片的话,那么要查询关注某活动的所有用户,就需要遍历所有分片查询。

    实际上,关系数据就是两个id而已,存储空间不大。可以优先考虑做缓存,不做分区。

    案例2:SNS数据表水平切分

    比如微博、微信。他们主要结构包括用户表,用户关系表(谁关注了谁)、消息表(发的微博、朋友圈)。在微博中,首页通常是timeline(指你可以看到你关注的人的所有消息,通常按时间排序),还有一个页面是profile(自己或单个人发布的所有消息)。当消息的量很大时,要进行水平切片,那么如何保证查询不去遍历所有分片呢?

    如果按照发布消息的用户id进行分片,那么查询自己或某个人的消息还好,只用一个分片内能查询到。但是要查所有关注者的消息,那就得遍历所有分片了。

    可以再增加一个消息内容表,让所有timeline的数据在一个分片内取到。他带来的问题就是浪费资源。这也是Twitter采用的方案,由于国内的SNS存在大量僵尸用户,一般采用推拉结合的方式兼顾。

    案例3:电商数据表水平切分

    电商中以订单为典型,一个订单包含id,卖家id,卖家id三个重要的查询关键字。为了简化,先不考虑订单与子订单相关的内容。该如何选择水平切分呢?

    方法1:外置搜索引擎查询

    方法2:让订单id和买家id建立联系。在水平分表的时候可以截取买家id的后几位加在生成订单id的末尾。这样订单id和买家id就建立了联系。通过这两个条件查询都不用遍历所有表。

    第五章、性能设计

    5.1 性能指标

    • 响应时间
    • 吞吐量。单位时间内的响应次数

    在资源一定的情况下,性能优化的本质就是榨取资源,利用一切可利用的资源。CPU消耗少的时候,可以考虑增加线程榨取CPU的能力。

    5.2 定位瓶颈点

    1. 压力测试
    2. 日志分析
    3. 监控工具
    • dstat是一个全能系统信息统计工具
    • sar是目前linux是最为全面的性能分析工具之一,可以查看文件读写情况、系统调用、磁盘IO、CPU、内存、进程活动等。
    • netstat 可以用来查看网络系统的状态信息
    • tcpdump可以用来查看网络连接的封包内容。

    5.3 服务通信优化

    5.3.1 同步转异步

    可以利用ajax技术实现异步

    5.3.2 阻塞转非阻塞

    阻塞调用是指调用结果返回之前,当前线程被挂起,调用线程只有得到结果才会被返回。当异步调用的时候,如需返回结果,有如下三种方式:

    • 状态:通过变量实现,需要主线程不断轮询变量结果
    • 通知:通过消息的方式,比状态更高效。
    • 回调:本质上和通知类似,如ajax中的回调函数。

    在Java中,如果需要返回结果可以采用future模式。因为是异步,调用future.get()的时候不一定能获得结果,如果还没有结果,则会被阻塞。Future是通过轮询或阻塞等待的方式,才能得到结果。更好的方式是通过callback,也就是执行结束的时候异步通知完成状态,然后去future中取执行结果。JDK中有future,但是不支持callback模式。还好Guava给我们提供了ListenableFuture。

    5.3.3 序列化

    从左往右分别是Avro、Thrift、Protobuf、gRPC、HTTP+json。可以看出前三个是一个级别的,相差不大。gRPC响应时间稍多一点,HTTP+json最慢。

    5.4 通过消息中间件提升写性能

    当规模不断变大后,数据库通常成为限制系统性能的主要因素。Mysql5.7单表字段个数为8,数据量为500万,写的吞吐量大约在1000TPS左右。而Kafka三节点集群的吞吐量能达到10万TPS。当然水平分表也可以提升吞吐量,但是水平分表带来的复杂度非常难解决。

    5.5 通过缓存提升读性能

    常见的缓存包括客户端缓存、HTTP缓存、操作系统缓存、CDN、代理缓存、数据库缓存等。

    5.5.1 Guava Cache本地缓存

    他比HashMap做缓存好的地方在于,他可以设置过期时间,回收空间。

    • maximumSize定义了缓存的容量大小,达到了容量大小就会进行LRU缓存回收
    • concurrencyLevel定义了Segment的数量,因为Guava Cache重写了ConcurrentHashMap,concurrencyLevel越大,并发能力越强
    • expireAfterWrite定义了缓存过期时间
    • refreshAfterWrite定义了缓存定时刷新时间
    5.5.2 使用缓存常见问题
    1. 缓存数据需要设置合理的过期时间
    2. 为缓存设置回收策略

    常见回收算法:FIFO(先进先出)、LRU(最近最少使用)、LFU(最不常用)
    3. 先预热数据

    5.6 数据库优化

    数据库通常是各个系统中最难以扩展的点。一般有以下可以优化的地方:

    • 索引、冗余、批量写入
    • 减小锁粒度
    • 减少复杂查询
    • 适当转移事务处理
    • 提升硬件性能
    • 读写分离
    • 分库
    • 垂直分表
    • 水平分表
    • 根据业务情况选择其他数据库
    5.6.1 优化慢sql
    1. 通过Explain 分析sql语句
    2. 通过慢日志分析瓶颈点

    5.7 简化设计

    架构是需要全方位考虑的,不可能完全从技术角度去解决问题。除了高性能,还需要考虑架构的复杂度、成本、代码可读性、维护性。

    5.7.1 转移复杂度

    如让我们设计一个抢红包的功能。抢红包的时候如果通过加锁的方式实现,那么数据库压力会很大。单纯从数据库优化的角度去实现,问题会变得越来越复杂。如果把这些抢红包的请求排序,串行修改数据库,那么数据库就不需要加锁了。

    另外拆红包的时候的请求数量肯定比发红包的请求数量大,我们可以在发红包的时候就算好随机金额,拆的时候直接返回,这样并不会降低用户体验。

    5.7.2 从业务角度优化

    大家通常认为12306网站买票像秒杀,实际上比秒杀复杂的多。秒杀无论前端放出多少流量,后端可以根据库存去抛弃多余的量。12306网站并非如此,除了固定时间放出所有的票,其次用户买票的起始站点都不一样,排列组合有很多。

    从业务角度可以这么优化:1.分时段放票。2.分地区放票。3.票的剩余数量不具体显示,只展示有票、无票等文字。4.最终采用排队的方式、满足最终一致性即可。

    第六章、一致性设计

    6.1 基础理论

    6.1.1 单机事务

    事务的四大特性:

    • 原子性

    指通过事务保证所有操作是不可分割的,要么全成功、要么全失败

    • 一致性

    指通过事务保证数据从一种状态变化到另一种状态。至少事务结束前,所有数据处于有效状态。一致性的核心是事务处理的中见状态不可见。

    • 隔离性

    是指事务内的操作不受其他操作的影响,多个事务同时处理同一个数据的时候,多个事务是互不影响的。

    • 持久性

    指事务被提交后,应该持久化,永久保存下来。

    6.1.2 CAP定理
    • 一致性
    • 可用性
    • 分区容错性
    6.1.3 BASE理论
    • BA:Basically Available 基本可用
    • S:Soft state,软状态
    • E:Eventually consistent,最终一致

    BASE理论的核心思想是:如果做强一致性无法做到,或者要付出很大的代价,那么可以根据自身业务特点,采用适当的方式使系统达到最终一致性,只要最终对用户没有影响或者影响是可接受的即可。

    6.2 如何实现强一致性

    两阶段提交
    三阶段提交

    6.3 如何实现最终一致性

    • 重试机制
    • 本地记录日志
    • 可靠事件模式
    • TCC事务模型

    TCC是下面三个单词的简写:Try、Confirm、Cancel

    TCC的优点是:是在业务层处理,平衡数据库压力。比2PC性能好很多,没有真正在数据库加锁。

    缺点是:增加业务复杂度,需要提供Try、Confirm、Cancel接口。需要提供幂等性接口

    支付宝目前的XTS框架就是采用的TCC模式。

    6.4 分布式锁

    6.4.1 分布式锁的实现方式
    1. 基于数据库实现悲观锁和乐观锁

    2. 基于Zookeeper实现分布式锁

    • 客户端连接zookeeper,并在/lock(自己定)目录下创建一个临时有序节点。第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推;
    • 查询/lock下的所有子节点列表,判断自己创建的子节点是否为序号最小的,如果是就代表拿到了锁,否则监听刚好在自己之前一位的子节点的删除事件,获得变更通知之后重复此步骤。
    • 拿到锁后执行业务代码
    • 完成业务流程后,删除对应的子节点释放锁
    1. 基于Redis实现分布式锁

    可以根据SETNX实现。但是存在很多问题。

    问题1:超时问题?

    如果线程A拿到锁,执行的比较慢,那么你超时时间就需要设置的比较长,那么就会阻塞很久。

    问题2:如何释放锁?

    能直接删除吗?不能。如果线程A拿到锁,业务执行完了,去查询看锁超时没有,查询到没有超时,这时他准备去删除锁。刚要删还没删的时候这个锁超时了,线程B进来拿到锁,那么线程A的删除操作就把B的锁删了。

    解决方案是,SETNX的时候,value值可以在客户端生成一个随机值,删除的时候判断是自己生成的再删。

    问题3:单点问题如何解决?

    redis是单点的,如果宕机了,那么整个系统就会崩溃。如果是主从结构,那么master宕机了,存储的key还没同步到slave,此时slave升级为新的master,客户端2从新的master上就能拿到同一个资源的锁。这样客户端1和客户端2都拿到锁,就不安全了。

    解决方案:RedLock算法。简单说就是N个(通常是5)独立的redis节点同时执行SETNX,如果大多数成功了,就拿到了锁。这样就允许少数节点不可用。

    6.5 如何保证幂等

    1. 利用redis
    2. 利用数据库的唯一约束
  • 相关阅读:
    剑指offer系列——41.和为S的连续正数序列
    剑指offer系列——40.数组中只出现一次的数字i-ii
    指针初始化
    剑指offer系列——39.平衡二叉树
    剑指offer系列——38.二叉树的深度
    剑指offer系列——37.数字在排序数组中出现的次数/在排序数组中查找元素的第一个和最后一个位置
    剑指offer系列——36.两个链表的第一个公共结点?
    剑指offer系列——35.数组中的逆序对**
    查看机器上GPU情况
    Linux下fork()、vfork()、clone()和exec()的区别
  • 原文地址:https://www.cnblogs.com/javammc/p/14159134.html
Copyright © 2011-2022 走看看