3千字了解微服务持续演进
第一章、微服务架构
1.1 为什么采用微服务架构?
1.1.1 单体架构和微服务架构
很难用一个绝对的方式去判断架构的好坏,在大多数情况下,我们很难从一个外部的视角去判断服务拆分的合理性,需要对上下文非常了解才能做出好的决策。
可以综合下面表中的多个维度进行分析
1.1.2 什么时候开始微服务架构
产品初期优先使用单体架构,面对一个新的领域,对业务的理解很难在开始阶段就比较清晰,往往是经过一段时间后,才能逐步弄清楚。
在服务划分之前,应该保证基础设施及公共基础服务已经准备好。如:监控服务、自动化运维工具、服务化框架等(灰度发布、资源调度)。
1.1.3 拆分粒度
微服务里面的微应该解释为“合适”,但是这个词比较含糊。对业务不够理解,对团队情况不够理解,都无权协助确定服务的粒度。随着业务发展,粒度可能还会发生变化。
每个人情况不一样,有的公司认为团队规模是决定性的,有的认为交付速度是决定性的,找到你的决定性因素,来做拆分即可。实在找不到合适的依据,可以参考此表。
1.2 微服务设计原则
- 垂直划分优先原则
应该根据业务领域对服务进行垂直划分。
- 持续演进原则
服务数量快速增长带来的架构复杂度急剧升高,开发、测试、运维等环节很难快速适应。非必要情况,应逐步划分,持续演进。
- 服务自治、接口隔离
尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。
- 自动化驱动原则
服务的增多,部署和运维的成本就会呈指数型增长,应该首先构建自动化的工具和环境。
1.3 微服务架构实施的先决条件
1.3.1 研发环境和流程上的转变
在微服务架构实施之前,要准备相关的环境和流程。
- 自动化工具链
微服务可以基于自动化工具链,以流水线的交付方式串联整个DevOps流程。
- 微服务框架
先进行微服务框架的选型和试用
- 快速申请资源
- 故障反馈机制
需要全面的监控故障,及时处理并发出报警。
- 研发流程转变
需要重建团队,以服务为核心,按照业务领域划分全功能团队。
1.3.2 拆分前先做好解耦
-
状态外置
- 定时任务:有很多任务不能重复触发,所以需要把定时任务从业务服务中提取出来,通过分布式任务系统调度。
- 本地存储:在本地存储文件的方式比较常见,但是当有多个实例的时候,要么需要全部同步,要么需要路由到一个实例。
- 本地缓存:如session数据,可以通过分布式缓存解决
-
去触发器,存储过程
- 当有触发器,存储过程时,整体伸缩难以扩展
- 当存在水平分表时,可能无法满足需求
- 如果触发器,存储过程过多,则会导致运维复杂度增高
-
通过接口隔离
- 如果直接去连接其他服务的数据库,当其他服务的数据结构变化,自己的服务也要跟着调整。
- 限流某个接口或者缓存也做不成,因为别人直接访问你的数据库
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=async\sync,默认为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
- UUID
UUID由以下几部分组成:
- 当前日期和时间
- 时钟序列
- 全局唯一的IEEE机器识别号,如果有网卡,则从网卡MAC地址获得。
- 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 定位瓶颈点
- 压力测试
- 日志分析
- 监控工具
- 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 使用缓存常见问题
- 缓存数据需要设置合理的过期时间
- 为缓存设置回收策略
常见回收算法:FIFO(先进先出)、LRU(最近最少使用)、LFU(最不常用)
3. 先预热数据
5.6 数据库优化
数据库通常是各个系统中最难以扩展的点。一般有以下可以优化的地方:
- 索引、冗余、批量写入
- 减小锁粒度
- 减少复杂查询
- 适当转移事务处理
- 提升硬件性能
- 读写分离
- 分库
- 垂直分表
- 水平分表
- 根据业务情况选择其他数据库
5.6.1 优化慢sql
- 通过Explain 分析sql语句
- 通过慢日志分析瓶颈点
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 分布式锁的实现方式
-
基于数据库实现悲观锁和乐观锁
-
基于Zookeeper实现分布式锁
- 客户端连接zookeeper,并在/lock(自己定)目录下创建一个临时有序节点。第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推;
- 查询/lock下的所有子节点列表,判断自己创建的子节点是否为序号最小的,如果是就代表拿到了锁,否则监听刚好在自己之前一位的子节点的删除事件,获得变更通知之后重复此步骤。
- 拿到锁后执行业务代码
- 完成业务流程后,删除对应的子节点释放锁
- 基于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 如何保证幂等
- 利用redis
- 利用数据库的唯一约束