分布式服务架构笔记¶
第1章 分布式微服务架构设计原理
1.1 从传统单体架构到服务化架构¶
- JEE架构:JEE平台是典型的二八原则的一个应用场景,它将 80%通用的与业务无关的逻辑和流程封装在应用服务器的模块化组件里,通过配置的模式提供给应用程序访问,应用程序实现 20%的专用逻辑,并通过配置的形式来访问应用服务器提供的模块化组件
- SSH架构:每个层级的实现比JEE对应的层次更简单、更轻量级,不需要开启整个应用服务器即可测试和验证
- 服务化架构:SOA代表面向服务的架构
- Web Service:通过某种协议交换数据
- ESB:企业服务总线的简称
图1-5 Web Service工作原理图
图1-6 ESB架构
1.2 从服务化到微服务¶
微服务架构致力于松耦合和高内聚的效果,与SOA和ESB相比,不再强调服务总线和通信机制的多样性,常常通过RESTful风格的API和轻量级的消息通信协议来完成。
1.3 微服务架构的核心要点和实现原理¶
微服务的交互模式
- 读者容错模式:指微服务化中服务提供者和消费者之间如何对接口的改变进行容错
- 消费者驱动契约模式:用来定义服务化中服务之间交互接口改变的最佳规则
- 去数据共享模式:与SOA服务化对比,微服务是去ESB总线、去中心化及分布式的;而SOA还是以ESB为核心实现遗留系统的集成,以及基于Web Service为标准实现的通用的面向服务的架构
微服务的分解和组合模式
- 服务代理模式:根据业务的需求选择调用后端的某个服务
- 服务聚合模式:根据业务流程处理的需要,以一定的顺序调用依赖的多个微服务,对依赖的微服务返回的数据进行组合、加工和转换,最后以一定的形式返回给使用方
- 服务串联模式:类似于一个工作流
- 服务分支模式:是服务代理模式、服务聚合模式和服务串联模式相结合的产物
- 服务异步消息模式
- 服务共享数据模式
- 单元化架构
- 遗留的整体服务
图1-12 服务代理模式的架构
图1-14 服务聚合模式的架构
图1-19 服务串联模式的架构
图1-20 服务分支模式的架构
图1-23 服务异步消息模式的架构
图1-25 服务共享数据模式的架构
微服务的容错模式
- 舱壁隔离模式
- 微服务容器分组
- 线程池隔离
- 熔断模式
- 限流模式
- 计数器
- 令牌筒
- 信号量
- 失效转移模式
1.4 Java平台微服务架构的项目组织形式¶
在微服务领域,Jar包分为
- 一方库:本服务在JVM进程内依赖的Jar包
- 二方库:在服务外通过网络通信或者RPC调用的服务的Jar包
- 三方库:所依赖的其他公司或者组织提供的服务或者模块
切记永远不要在本地事务中调用远程服务,在这种场景下如果远程服务出现了问题,则会拖长事务,导致应用服务器占用太多的数据库连接,让服务器负载迅速攀升,在严重情况下会压垮数据库。
1.5 服务化管理和治理框架的技术选型¶
RPC
- JDK RMI
- Hessian及Burlap
- Spring HTTP Invoker
服务化
- Dubbo
- HSF
- Thrift
- AXIS
- Mule ESB
第2章 彻底解决分布式系统一致性的问题
2.1 什么是一致性¶
一致性指分布式服务化系统之间的弱一致性,包括应用系统的一致性和数据的一致性
2.2 一致性问题¶
- 案例1:下订单和扣库存
- 案例2:同步调用超时
- 案例3:异步回调超时
- 案例4:掉单
- 案例5:系统间状态不一致
- 案例6:缓存和数据库不一致
- 案例7:本地缓存节点间不一致
- 案例8:缓存数据结构不一致
2.3 解决一致性问题的模式和思路¶
酸碱平衡理论
- ACID(酸)
- CAP(帽子原理)
- BASE(碱)
DTS(分布式事务处理模型),模型中包含4种角色:应用程序、事务管理器、资源管理器和通信资源管理器。
两阶段提交协议
- 准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,则会写redo或者undo日志(Write-Ahead Log的一种),然后锁定资源,执行操作,但是并不提交。
- 提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,则协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,则协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源。
图2-1 两阶段提交协议的成功场景
三阶段提交协议是两阶段提交协议的改进版本,通过超时机制解决了阻塞的问题
- 询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是或不是,而不需要做真正的操作,这个阶段超时会导致中止。
- 准备阶段:如果在询问阶段所有参与者都返回可以执行操作,则协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作但是不提交操作;如果在询问阶段任意参与者返回不能执行操作的结果,则协调者向参与者发送中止请求
- 提交阶段:如果每个参与者在准备阶段返回准备成功,也就是说预留资源和执行操作成功,则协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何参与者返回准备失败,也就是说预留资源或者执行操作失败,则协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源
图2-2 三阶段提交协议的成功场景
一个使用TCC的实际案例,在秒杀的场景中,用户发起下订单请求,应用层先查询库存,确认商品库存还有余量,则锁定库存,此时订单状态为待支付,然后指引用户去支付,由于某种原因用户支付失败或者支付超时,则系统会自动将锁定的库存解锁以供其他用户秒杀。
图2-3 TCC协议的使用场景
保证最终一致性的模式
- 查询模式
- 对于2.2节的案例2~案例5,使用查询模式来决定下一步做什么,例如是补偿未完成的操作还是回滚已经完成的操作
- 补偿模式
- 异步确保模式
- 最大好处是能够对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入账等
- 对于2.2节的案例3,若对某个操作迟迟没有收到响应,则通过查询模式、补偿模式和异步确保模式来继续未完成的操作
- 定期校对模式
- 实现定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID
- 对于2.2节的案例4和案例5,通常通过定期校对模式发现问题,并通过补偿模式来修复
- 可靠消息模式
- 缓存一致性模式
- 避免2.2节案例6~案例8中的问题
图2-4~图2-11
2.4 超时处理模式¶
微服务的交互模式
- 同步调用模式
- 适用于大规模、高并发的短小操作,而不适用于后端负载较高的场景
- 例如:几乎所有JDBC的实现完全使用BIO同步阻塞模式
- 接口异步调用模式
- 适用于非核心链路上负载较高的处理环节,这个环节经常耗时较长,并且对时效性要求不高
- 例如:在B2C电商系统中,一件商品售卖成功后,需要给相应的商户入账收入,这个过程对时效性要求不高
- 消息队列异步处理模式
- 与接口异步调用模式类似,多应用于非核心链路上负载较高的处理环节中,并且服务的上游不关心下游的处理结果,下游也不需要向上游返回处理结果
- 例如:在电商系统中,用户下订单支付且交易成功后,后续的物流处理适合使用消息队列异步处理模式
同步与异步的抉择
- 尽量使用异步来替换同步操作
- 是从业务功能的角度出发的,也就是从与用户或者使用方的交互模式出发的
- 一些耗时较长的、用户对响应没有特别要求的操作异步化
- 能用同步解决的问题,不要引入异步
- 是从技术和架构的角度出发的
- 如果性能不是问题,或者所处理的操作是短小的轻量级处理逻辑
超时补偿的原则
- 服务1调用服务2,如果服务2处理失败,那么服务2应该负责重试或者补偿
- 服务1调用服务2,如果服务2没有给出明确的接收响应,那么服务1应该持续进行重试,直到服务2明确表示已经接收消息
2.5 迁移开关的设计¶
第3章 服务化系统容量评估和性能保障
3.1 架构设计与非功能质量¶
在互联网时代,软件架构设计使用服务化或者微服务架构对系统进行拆分,拆分后的系统职责单一,每个服务的业务逻辑简单,但是对非功能质量尤其是性能和容量需求的要求非常高
互联网架构权衡分析方法(Architecture Tradeoff Analysis Method,ATAM)是评价软件构架的一种综合且全面的方法
3.2 全面的非功能质量需求¶
表3-1 核心非功能质量指标
核心非功能质量指标 | 描述 |
---|---|
高性能 | 运行效率高、性价比高 |
可用性 | 持续可用性、缩短宕机时间、出错恢复、可靠性 |
可伸缩性 | 垂直伸缩、水平伸缩 |
可扩展性 | 可插拔、组件重用 |
安全性 | 数据安全、加密、熔断、防攻击 |
表3-2 其他非功能质量指标
其他非功能质量指标 | 描述 |
---|---|
可监控性 | 快速发现、定位和解决 |
可测试性 | 可灰度、可预览、可Mock、可拆解 |
鲁棒性 | 容错性、可恢复性 |
可维护性 | 易于维护、监控、运营和扩展 |
可重用性 | 可移植性、解耦 |
易用性 | 可操作性 |
3.3 典型的技术评审提纲¶
- 现状
- 需求
- 方案描述
- 方案对比
- 风险评估
- 工作量评估
3.4 性能和容量评估经典案例¶
3.4.1 背景¶
物流系统包含如下两个质量优先需求
- 维护用户的常用地址,在下单时提供其地址列表
- 下单时异步产生物流订单,物流系统后台通过第三方物流轮询拉取物流状态,已下单用户可查询订单的物流订单和物流记录
3.4.2 目标数据量级¶
选取行业内一线电商平台的量级作为目标
- 用户量达两亿,平均每天增长5万个
- 平时每天的订单量为400万个,下单时段集中在9:00~23:00,促销时日订单量为1400万个,50%的下单时段集中在19:30~20:30和22:00~23:00
3.4.3 量级评估标准¶
- 通用标准
- 容量:按照峰值的5倍进行冗余计算
- 用户的常用地址容量:按照30年计算
- 数据物流订单的容量:时效性较强,按照3年计算
- 第三方物流查询接口:吞吐量为5000/s
- MySQL
- 单端口读:1000/s
- 单端口写:700/s
- 单表容量:5000万条
- Redis
- 单端口读:40000/s
- 单端口写:40000/s
- 单端口内存容量:32GB
- Kafka
- 单机读:30000/s
- 单机写:5000/s
- 应用服务器
- 请求量的峰值:5000/s
3.4.4 方案¶
方案1.最大性能方案
需求1.用户常用地址
- 整体流程
- 提供RESTful服务来增加用户的常用地址
- 提供RESTful服务来获取用户的常用地址列表
- 数据库资源评估
- 读操作吞吐量
- 在用户每次下单时拉取一次用户的地址列表,按照促销时日订单量1400万且50%的下单时段集中在两个小时内计算:
(1400万×0.5)/(2×60×60)=1000/s
- 容量评估按照5倍冗余计算,读操作吞吐量峰值为
1000/s×5=5000/s
,需要5端口数据库服务读
- 在用户每次下单时拉取一次用户的地址列表,按照促销时日订单量1400万且50%的下单时段集中在两个小时内计算:
- 写操作吞吐量
- 假设每天新增的用户全部增加一次常用地址,并且在高峰期有20%的用户在下单时会增加一条常用地址:
(1400万×0.2+5万)/(2×60×60)=400/s
- 容量评估按照5倍冗余计算:
400/s×5=2000/s
,则需要3端口数据库服务写
- 假设每天新增的用户全部增加一次常用地址,并且在高峰期有20%的用户在下单时会增加一条常用地址:
- 数据容量
- 当前有两亿用户,每天增长5万用户,平均每个用户有5个常用地址,30年后用户的常用地址表数量计算如下:
(2亿+5万×365×30年)×5=35亿
- 容量评估按照5倍冗余计算:
35亿×5=175亿
,则需要350张表即可容纳 - 设计结果:4端口×32库×4表,4主8从
- 当前有两亿用户,每天增长5万用户,平均每个用户有5个常用地址,30年后用户的常用地址表数量计算如下:
- 读操作吞吐量
- 缓存资源评估
- 定义当天下单的用户为活跃用户,将活跃用户的地址缓存24小时,假定每天下单的用户均为不同的用户,每个用户有5个常用地址,每个地址大小为1KB左右,则缓存大小的计算如下:
1400万×5×1KB=70GB
- 容量评估按照5倍冗余计算:
70GB×5=350GB
- 设计结果:11台,主从
- 定义当天下单的用户为活跃用户,将活跃用户的地址缓存24小时,假定每天下单的用户均为不同的用户,每个用户有5个常用地址,每个地址大小为1KB左右,则缓存大小的计算如下:
- 应用服务器资源评估
- 根据数据库的读操作吞吐量(5000/s)峰值和写操作吞吐量(2000/s)峰值计算,选择单台应用服务器即可,选择两台可避免单点
- 设计结果:2台
需求2.物流订单和物流记录
- 整体流程
- 订单提交后,通过消息队列产生物流订单,物流订单消息稍后传入物流系统,物流系统消费物流订单消息然后入库
- 后台任务轮询未完成的物流订单,查询第三方物流接口状态,填写物流记录信息。按照每天产生1400万个订单,订单平均3天到货,第三方查询接口提供5000/s的吞吐量,则每次进行状态查询需要的时间计算如下:
1400万×3天/5000=2小时
,即将任务定为两小时查一次 - 提供RESTful服务获取物流订单信息
- 提供RESTful服务获取物流记录信息
- 数据库资源评估
- 读操作吞吐量
- 在用户下单后三天到货,三天内50%的用户每天会查询一次物流订单和一次物流记录,计算如下:
(1400万×3×0.5)/(24×60×60)=250/s
- 容量评估按照5倍冗余计算:
2×250/s×5=2500/s
,即需要3端口数据库服务读操作
- 在用户下单后三天到货,三天内50%的用户每天会查询一次物流订单和一次物流记录,计算如下:
- 写操作吞吐量
- 用户每次下单时产生一次物流订单,按照促销时日订单量为1400万,且50%的下单时段集中在两个小时内计算:
(1400万×0.5)/(2×60×60)=1000/s
- 按照每天产生1400万个订单,每个订单平均3天到货,每条物流订单产生8条物流记录,并且8条物流记录在3天内均匀产生,则物流记录写操作的吞吐量计算如下:
1400万×3×8/3/(24×60×60)=1200/s
- 容量评估按照5倍冗余计算:
(1000/s+1200/s)×5=11000/s
,则需要16端口数据库服务写
- 用户每次下单时产生一次物流订单,按照促销时日订单量为1400万,且50%的下单时段集中在两个小时内计算:
- 数据容量
- 当前有两亿条物流订单积累,每天增加400万订单,则3年订单量计算如下:
2亿+400万×365天×3年=46亿
- 容量评估按照5倍冗余计算:
46亿×5=230亿
则需要460张表即可容纳。物流记录表是物流订单的8倍,为460×8=3680张表
- 设计结果:16端口×32库×8表,16主16从
- 当前有两亿条物流订单积累,每天增加400万订单,则3年订单量计算如下:
- 读操作吞吐量
- 消息队列资源评估
- 设计结果:两台Kafka,两台处理机
- 应用服务器资源评估
- 设计结果:2台
方案2.最小资源方案
- 用户常用地址
- 设计结果:1端口×128库×4表,1主1从
- 物流订单和物流记录
- 设计结果:1端口×512库×8表,1主1从
3.5 性能评估参考标准¶
3.5.2 常用的系统层性能指标参考标准¶
- 寄存器和内存
- 寄存器、L2、L3、内存、分支预测失败、互斥量加锁和解锁等耗时为纳秒级别
- 内存随机读取可达30万次/s,顺序读取可达500万次/s
- 内存每秒可以读取GB级别的数据
- 读取内存中1MB的数据为250ns,为亚毫秒级
- 硬盘I/O
- 普通的SATA机械硬盘IOPS能达到120次/s
- 普通的SATA机械硬盘顺序读取数据可达100MB/s
- 普通的SATA机械硬盘随机读取数据可达2MB/s
- 普通的SATA机械硬盘旋转半圈需要3ms
- 普通的SATA机械硬盘寻道需要3ms
- 普通的SATA机械硬盘在已经寻道后(找到了要读取的磁道,也找到了要读取的扇区)开始读取数据,读取一次数据真正的耗时为2ms
- FusionIO卡(一种高的SSD硬盘套件)可达到百万级别的IOPS
- 高端机器如IBM、华为等的服务器配上高端的存储设备,可以达到每秒GB级别的数据读取,相当于普通内存的读取速度
- 固态硬盘访问延迟:0.1~0.2ms,为亚毫秒级别,和内存速度差不多
- 网络I/O
- 常见的千兆网卡的传输速度为1000Mbit/s,即128Mbit/s
- 千兆网卡读取1MB数据:10ms
- 数据库
- 读写数据库中的一条记录在毫秒级别,短则几毫秒,多则几百毫秒,大于500ms一般认为超时
- MySQL在4核心、256GB内存的CPU中性价比最好,继续垂直扩展时由于体系结构的限制,成本开始增加,提升的性能开始减少,性价比开始降低
- IDC
- 同一机房网络来回:0.5ms
- 异地机房来回:30~100ms
- 同一机房的RPC服务调用为几个毫秒,有的为几十毫秒或者几百毫秒,一般设置500ms以上为超时
- 网站
- 网页加载为秒级别
- UV: 每日一共有多少用户来访,用Cookie Session跟踪
- 独立IP访问:每日有多少独立IP来访,同一个局域网可看到同一个IP
- PV:每日单独用户的所有页面访问量。如果每日UV为50 000 000,那么每秒的平均在线人数为
50 000 000/24/60/60=578人
,还要知道这一秒内每个用户都在做什么,如果每秒内都在做一次查询操作,那么需要有一个能承受578/s吞吐量的机器 - 某社交媒体平台每秒的写入量上万,每秒的请求量上百万,每天登录的用户上亿,每天产生的数据量上千亿
- 组合计算和估算
- 普通的SATA机器硬盘一次随机读取的时间为:
3ms(磁盘旋转)+3ms(寻道)+2ms(存取数据延迟)= 8ms
- 普通的SATA机器硬盘每秒随机读取:
1000ms/8ms=125次IOPS
- IOPS代表磁盘每秒可随机寻址多少次,随机读取速度取决于数据是如何存放的,如果数据按照块存放,每块4KB,每次读取10块,那么随机读取速度为:
10×4KB×125次/s=5MB/s
- 一次读取内存的时间:
1000ms/30万次/s=3ns
CPU速度=10倍×内存速度=100倍×I/O速度
- 顺序读取普通SATA机械硬盘1MB的数据:20ms
- 请记住:210=1KB,220=1MB,230=1GB,232=4GB
- 普通的SATA机器硬盘一次随机读取的时间为:
3.6 性能测试方案的设计和最佳实践¶
- 单线程响应时间和吞吐量的计算公式如下:
吞吐量=1s/响应时间
- 多线程响应时间和吞吐量的计算公式如下:
吞吐量=(1s/响应时间)×并发数
测试类型
- 基准测试
- 容量测试
- 负载测试
- 混合业务测试
- 稳定性测试
- 异常测试
加压方式
- 瞬间加压
- 逐渐加压
- 梯度加压
4.1 开源日志框架的原理分析与应用实践¶
- JDK Logger
- Apache Commons Logging
- Apache Log4j
- Slf4j
- Logback
- Apache Log4j 2
4.4 ELK系统的构建与使用¶
ELK项目是开源项目Elasticsearch、Logstash和Kibana的集合
- Elasticsearch是基于Lucene搜索引擎的NoSQL数据库
- Logstash是一个基于管道的处理工具,它从不同的数据源接收数据,执行不同的转换,然后发送数据到不同的目标系统
- Kibana工作在Elasticsearch上,是数据的展示层系统
第5章 基于调用链的服务治理系统的设计与实现
5.3 调用链跟踪系统的设计与实现¶
5.3.1 整体架构¶
图5-9 整体的调用链跟踪系统的通用实现架构
- 采集器:负责把业务系统的远程服务调用信息从业务系统中传递给处理器
- 处理器:负责从业务系统的采集器中接收服务调用信息并聚合调用链,将其存储在分布式数据存储中,以及对调用链进行分析,并输出给监控和报警系统
- 分布式存储系统:存储海量的调用链数据,并支持灵活的查询和搜索功能
- 调用链展示系统:支持查询调用链、业务链等功能
第6章 Java服务的线上应急和技术攻关
6.1 海恩法则和墨菲定律¶
海恩法则:每一起严重事故的背后,必然有29次轻微事故和300起未遂先兆及1000起事故隐患。
- 事故的发生是量的积累的结果
- 再好的技术、再完美的规章,在实际操作层面也无法取代人自身的素质和责任心
墨菲定律:如果有两种或两种以上方式去做某件事情,而选择其中一种方式将导致灾难,则必定有人会做出这种选择。
- 任何事情都没有表面看起来那么简单。
- 所有事情的发展都会比你预计的时间长。
- 会出错的事总会出错。
- 如果你担心某种情况发生,那么它更有可能发生
6.2 线上应急的目标、原则和方法¶
图6-1 应急的整体流程
6.3 技术攻关的方法论¶
图6-2 技术攻关的流程
6.4 环境搭建和示例服务启动¶
6.5 高效的服务化治理脚本¶
6.5.1 show-busiest-java-threads¶
通过结合Linux操作系统的ps命令和JVM自带的jstack命令,来查找Java进程内CPU利用率最高的线程,一般适用于服务器负载较高的场景,并需要快速定位负载高的成因
- 命令格式:
./show-busiest-java-threads -p 进程号 -c 显示条数
./show-busiest-java-threads -h
- 使用示例:
./show-busiest-java-threads -p 8244 -c 3
6.5.2 find-in-jar¶
用于在 Jar包的包名和类名中查找某一关键字,并高亮显示匹配的包名、类名和路径,多用于定位java.lang.NoClassDefFoundError
和java.lang.ClassNotFoundException
的问题,以及类版本重复或者冲突的问题等
- 命令格式:
find-in-jar 关键字类名 根路径
- 使用示例:
find-in-jar ByteBufferHolder .
6.5.3 grep-in-jar¶
在Jar包中进行二进制内容查找,通常会解决线上出现的一些“不可思议”的问题,例如:某些功能上线后没有生效、某些日志没有打印等,通常是上线工具或者上线过程出现了问题,可以把线上的二进制包拉下来并查找特定的关键字来定位问题
- 命令格式:
grep-in-jar 关键字 路径
- 使用示例:
grep-in-jar "vesta" .
6.5.4 jar-conflict-detect¶
用于识别冲突的Jar包,可以在一个根目录下找到包含相同类的所有Jar包,并且根据相同类的多少来判断Jar包的相似度,常常用于某些功能上线却不可用或者没有按照预期起到作用的情况,或者使用此脚本分析是否存在两个版本的类,而老版本的类被Java虚拟机加载
- 命令格式:
jar-conflict-detect 路径
- 使用示例:
jar-conflict-detect .
6.5.5 http-spy¶
6.5.6 show-mysql-qps¶
6.5.7 小结¶
场景 | 命令 |
---|---|
服务器负载高、服务超时、CPU利用率高 | show-busiest-iava-thread |
java.lang Noclassdeffounderror , java.lang Classnotfoundexception , 程序未按照预期运行 |
find-in-jar |
程序未按照预期运行、上线后未执行新逻辑、查找某些关键字 | grep-in-jar |
Jar包版本冲突、程序未按照预期运行 | jar-conflict-detect |
HTP调用后发现未按照预期输出结果 | http-spy |
数据库负载高、SOQL超时 | show-mysql-qps |
6.6 JVM提供的监控命令¶
6.6.2 btrace¶
btrace可以动态地跟踪Java运行时程序,将定制化的跟踪字节码切面注入运行类中,对运行代码无侵入,对性能的影响也可忽略不计
- 命令格式:
btrace [-p port] [-cp classpath] pid btrace-script
port
:指定btrace agent的服务端监听端口号,供客户端连接classpath
:用来指定依赖的类加载路径pid
:表示进程号,可通过jps或者ps命令获取btrace-script
:btrace跟踪切面脚本
6.6.3 jmap¶
用来定位OutOfMemoryError
- 按照占用空间的大小打印程序中类的列表:
jmap -histo:live 2743
- 按照占用空间的大小打印程序中加载的动态链接库的列表:
jmap 2743
- 查看堆的概要信息:
jmap -heap 38574
- 对Java堆的内部结构进行剖析:
jmap -dump:format=b,file=./heap.hprof 2743
6.6.4 jstat¶
利用了JVM内建的指令对Java应用程序的资源和性能进行实时的命令行监控,包括对堆大小和垃圾回收状况的监控等。与jmap对比,jstat更倾向于输出累积的信息与打印GC等的统计信息等
- 使用示例:
jstat -gcutil 2743 5000 10
6.6.5 jstack¶
用于打印给定的Java进程ID的线程堆栈快照信息,从而可以看到Java进程内线程的执行状态、正在执行的任务等,可以据此分析线程等待、死锁等问题
- 使用示例:
jstack 2743
6.6.7 其他命令¶
- javah:生成Java类中本地方法的C头文件,一般用于开发JNI库
- jps:用于查找Java进程,通常使用ps命令代替
- jhat:用于分析内存堆的快照文件
- jdb:远程调试,用于线上定位问题
- jstatd:jstat的服务器版本
6.6.8 小结¶
场景 | 命令 |
---|---|
没有源码的Jar包出了问题、破解别人的代码、新上线的代码不符合预期 | jad |
线上出问题,无法增加日志、无法线上调试,需要实现切面功能 | btrace |
内存不足、OutOfMemoryError | jmap |
内存不足、OutOfMemoryError、GC频繁、服务超时、出现长尾响应现象 | jstat |
服务超时、线程卡死、线程死锁、服务器负载高 | jstack |
査看或者修改Java进程的环境变量和Java虚拟机变量 | jinfo |
使用JNI开发Java本地程序库 | javah |
査找Java进程ID | jps |
分析jmap产生的Java堆的快照 | jhat |
QA环境无法重现,需要在准生产线上远程调试 | jdb |
与jstat相同,是jstat的服务器版本,但是可以在线下用客户端连接,可线下操作 | jstatd |
简单的有界面的内存分析工具,是JDK自带的,已被JVisualVM取代 | JConsole |
全面的有界面的内存分析工具,功能丰富,JDK自带 | JVisualVM |
专业的Java进程性能分析和跟踪工具 | JMAT |
商业化的Java进程性能分析和跟踪工具 | JProfiler |
6.7 重要的Linux基础命令¶
6.7.1 必不可少的基础命令和工具¶
- grep: 通用的文本内容查找命令
- find: 通过文件名查找文件的所在位置
- uptime: 查看机器的启动时间、登录用户、平均负载等
- lsof: 列出系统当前打开的文件句柄
- ulimit: 显示当前的各种系统对用户使用资源的限制
- curl: 集成测试
- scp: 实现从本地到远程,以及从远程到本地的双向文件传输
- vi和vim
- dos2unix和unix2dos: 用于转换Windows和UNIX的换行符
- awk: 强大的文本分析工具
- sed:文本编辑和替换
- tr: 字符替换
- cut: 选取命令,分析一段数据并取出我们想要的部分
- wc: 统计字数和行数等
- sort: 排序
- uniq: 去重或者分组统计
- zip: 压缩成zip格式的压缩包或者解压
- tar: 创建或者解压tar格式的包
6.7.2 查看活动进程的命令¶
6.7.3 窥探内存的命令¶
6.7.4 针对CPU使用情况的监控命令¶
6.7.5 监控磁盘I/O的命令¶
6.7.6 查看网络信息和网络监控命令¶
6.7.7 Linux系统的高级工具¶
6.7.8 /proc文件系统¶
6.7.9 摘要命令¶
6.7.10 小结¶
命令 | 场景 |
---|---|
grep | 超级强大的文本査找命令,常用于在大量文件中查找相关的关键词 |
find | 查找某些文件,常用于在众多项目中根据文件名查找某些文件 |
uptime | 查看操作系统启动的时间、登录的用户、系统的负载等 |
lsof | 查看某个进程打开的文件句柄 |
ulimit | 查看用户对资源使用的限制,例如:打开的最大文件句柄、创建的最大线程数等 |
curl | 模拟HTTP调用,常用于RESTful服务的简单测试 |
scp | 从服务器上下载文件或者上传文件到服务器上 |
vi/vim | 在服务器上编辑文件,或者作为开发脚本程序的编辑环境 |
dos2unix & unix2dos | 转换 Windows和 UNIX/Linux的换行符 |
awk | 一款强大的按照行进行文本处理和分割的工具 |
ps | 査看系统内的进程列表,可以看到内存、CPU等信息 |
top、htop | 按照资源的使用情况排序显示系统内的进程列表 |
pidstat | 针对某一进程输出系统资源的使用情况,包括:CPU、内存、IO等 |
free | 查看系统的内存使用情况 |
pmap | 查看进程的详细的内存分配情况 |
vmstat | 査看系统的CPU利用率、负载、内存等信息 |
mpstat | 査看系统的CPU利用率、负载,并且按照CPU核心分别显示相关信息 |
iostat | 查看磁盘IO的信息及传输速度 |
swapon | 查看系统交换区的使用情况 |
df | 显示磁盐挂载的信息 |
ifconfig、ip | 显示网卡挂载的信息 |
ping | 检测某服务器到其他服务器的网络连接情况 |
telnet | 检测某服务器的端口是否正常对外服务 |
nc | 模拟开启TCP/IP的服务器,通常用于拦截HTTP传递的参数,帮助定位RESTful服务的问题 |
mtr | 检测网连通性问题,并可以获取某一个域名或若P的丢包率 |
nslookup | 判断DNS能否正确解析域名,以及将域名解析到哪个IP地址 |
traceroute | 跟踪网络传输的详细路径,显示每一级网关的信息 |
sar | 为全面监控网络、磁盘、CPU、内存等信息的轻量级工具 |
netstat(ss) | 通常用于查看网络端口的连接情况 |
intra | 用于获取网络1O的传输速度及其他网络状态信息 |
tcpdump | 可以拦截本机网卡上任何协议的通信内容,用于调试网络问题 |
nmap | 扫描某一服务器打开的端口 |
ethtool | 査看网卡的配置或者配置网卡 |
pstack | 打印进程内的调用堆栈 |
strace | 跟踪进程内的工作机制 |
/proc文件系统 | 实时査看系统的CPU、内存、IO等信息 |
md5sum | 生成md5摘要 |
sha256 | 生成sha256摘要 |
base64 | 生成basc64编码 |
6.8 现实中的应急和攻关案例¶
6.8.1 一次OOM事故的分析和定位¶
产生OutOfMemoryError错误的原因如下
java.lang.OutOfMemoryError
: Java heap space,表示Java堆空间不足java.lang.OutOfMemoryError
: PermGen space,表示Java永久代(方法区)的空间不足java.lang.OutOfMemoryError
: unable to create new native thread,本质原因是创建了太多的线程,而系统允许创建的线程数是有限制的java.lang.OutOfMemoryError
: GC overhead limit exceeded,是并行(或者并发)垃圾回收器的GC回收时间过长、超过98%的时间用来做GC并且回收了不到2%的堆内存时抛出的异常,用来提前预警,避免内存过小导致应用不能正常工作
两个异常与OOM有关系,却又没有绝对关系
java.lang.StackOverflowError
,是JVM的线程由于递归或者方法调用的层次太多,占满了线程堆栈而导致的,线程堆栈的默认大小为1MBjava.net.SocketException
: too many open files,是由于系统对文件句柄的使用有限制,而某个应用程序使用的文件句柄超过了这个限制而导致的
如下公式可以用来从内存的角度计算允许创建的最大线程数:
最大线程数=(操作系统最大可用内存 - JVM内存 - 操作系统预留内存)/线程栈大小
由于对Dubbo服务框架进行定制化时,设计了自动降级原则,如果Dubbo服务负载增加或者注册中心宕机,则会自动切换到点对点的RPC框架,这也符合微服务的失效转移原则,但是在设计中没有进行全面考虑,一旦一部分服务切换到了点对点的RPC,而另一部分服务没有切换,就会导致两个线程池都被撑满,于是超过了1024的限制(800+800=1600
),出现问题。
问题的根源是日志切割导致I/O负载增加,然后阻塞线程池,最后发生OOM
解决方案如下。
- 将全部应用改为直接使用Log4j的日志滚动功能
- Tomcat线程池的线程数设置与操作系统的线程数设置不合理,适当减少Tomcat线程池的线程数
- 升级Log4j日志,使用Logback或者Log4j2
6.8.2 一次CPU 100%的线上事故排查¶
concurrent系列的集合类的size
方法并不是常量时间内返回的,这是因为concurrent系列的集合类使用分桶的策略减少集合的线程竞争,在获取其整体大小时需要进行统计,而不是直接返回一个预先存储的值。
concurrent系列的集合类的size
方法和其他集合类的size
方法不一样,获取的时间复杂度是o(n)
改进方案
- 使用环形队列来解决问题,生产者通过环形队列的写入指针存储数据,消费者通过队列的读取指针来读取数据,如果生产者的进度快于消费者,则生产者可抛弃事件
- 可参考或者使用开源的Disruptor Ring Buffer来实现
- 可以采用其他有界队列来实现
- 可以采用专业的日志收集器来实现,如Fluentd、Flume、Logstash等
第7章 服务的容器化过程
7.2 Docker实战¶
7.2.1 Docker的架构¶
Docker引擎主要包括三大组件
- Docker后台服务(Docker Daemon):是长时间运行在后台的守护进程,是Docker的核心服务,可以通过命令dockerd与它交互通信
- REST接口(REST API):程序可以通过REST的接口来访问后台服务,或向它发送操作指令
- 交互式命令行界面(Docker CLI):大多数时间都在使用命令行界面与Docker进行交互,例如以docker为开头的所有命令的操作
7.2.3 Docker初体验¶
- 查看版本信息
- 查看Docker版本:
$ docker --version
- 查看docker-compose版本:
$ docker-compose --version
- 查看docker-machine版本:
$ docker-machine --version
- 查看Docker的更多信息:
$ docker version
- 查看Docker版本:
- 运行简单的Docker命令
- 查看本地镜像:
$ docker images
- 查看运行的容器:
$ docker ps
- 运行Docker自带的hello world程序:
$ docker run hello-world
3.启动简单的Web服务 - 以后台模式在80端口启动nginx容器:
$ docker run -d -p 80:80 --name webserver nginx
- 查看本地镜像:
7.2.5 Docker的客户端命令¶
Docker 中最常用的命令有:镜像(image)、容器(container)、磁盘卷(volume)、网络(network)、服务(service)和集群(swarm)等
7.2.6 Docker Compose编排工具的使用¶
Docker Compose(简称Compose)是一个用于定义和运行多个Docker应用程序的工具,我们可以在Compose文件中定义多个应用服务(service),然后使用一个简单的命令就能创建和启动所有服务
7.3 容器化项目¶
7.3.3 容器化部署应用¶
容器的管理工具
- Swarm是Docker的原生集群工具,它使用标准的Docker API
- Kubernetes(缩写K8s)是Google开源的一套自动化容器管理平台,前身是Borg,用于容器的部署、自动化调度和集群管理
- Apache Mesos是一款开源集群管理软件
第8章 敏捷开发2.0的自动化工具
8.1 什么是敏捷开发2.0¶
8.1.1 常用的4种开发模式¶
图8-1~图8-4
- 瀑布式开发:在从需求到设计、从设计到编码、从编码到测试、从测试到提交的每个开发阶段都要做到最好,特别是在前期阶段设计得越完美,提交后的损失就越少。然而现在的系统很复杂且多变,所以很难在现实中应用瀑布式开发
- 迭代式开发:不要求每个阶段的任务都做到最好,可以容忍一些不足,先不去完善它,将主要功能先搭建起来,以最短的时间及最少的损失完成一个不完美的成果直至提交,然后通过客户或用户的反馈信息,在这个不完美的成果上逐步进行完善
- 螺旋式开发:在很大程度上是一种风险驱动的方法体系,因为在每个阶段及经常发生的循环之前,都必须先进行风险评估
- 敏捷软件开发:和迭代式开发相比,两者都强调在较短的开发周期内提交软件,但是,敏捷开发的周期可能更短且更强调队伍中的高度协作。敏捷方法有时被误认为是无计划性和纪律性的方法,实际上更确切的说法是敏捷方法强调适应性而非预见性,适应性的方法主要用于快速适应需求的变化。当项目的需求有变化时,团队能够迅速应对新的需求
在一般的公司里,采用敏捷开发和不断迭代开发的方式较多,而且效率高、效果明显
8.1.2 什么是DevOps¶
有一个共同的思想:解决开发者与运维者之间曾经不可逾越的鸿沟,增强开发者与运维者之间的沟通和交流
关键有两点:
- 全局观,要从软件交付的全局出发,加强各角色之间的合作
- 自动化,人机交互就意味着手工操作,应选择那些支持脚本化、无须人机交互界面的强大管理工具
精益管理的7个原则如下
- 消除浪费
- 增强学习
- 延迟决策
- 快速交付
- 团队授权
- 内置完整性(完整性是为了让客户对产品的体验具备平滑性和一致性)
- 考虑全局
DevOps可以用一个公式表达:文化观念的改变+自动化工具=不断适应快速变化的市场
DevOps的开发流程如下
- 提交:工程师将程序在本地测试后,提交到版本控制系统如Git等
- 编译:持续整合系统(如Jenkins CI),在检测到版本更新时,便自动从Git仓库里拉取最新的程序,进行编译、构建
- 单元测试:Jenkins完成编译构建后,会自动执行指定的单元测试代码
- 部署到测试环境中:在完成单元测试后,Jenkins 可将程序部署到与生产环境相近的测试环境中进行测试
- 预生产测试:在预生产测试环境里,可以进行一些最后的自动化测试,例如 Selenium测试及与实际情况类似的测试,可由开发人员或客户手动进行
- 部署到生产环境:通过所有测试后,便可将最新的版本部署到实际生产环境里
图8-5 DevOps的开发流程
SaltStack是一个架构管理系统,可以批量地修改Server的设定或执行指令,也可以通过配置管理使得开发环境、测试环境及应用环境最大可能地保持一致
8.1.3 敏捷开发2.0解决的问题¶
为了更加快速地发现问题和得到市场的快速反馈,引入了持续集成(Continuous Integration,CI)和持续交付(Continuous Delivery,CD),来更加高效地进行敏捷开发,即敏捷开发2.0
8.2 敏捷开发的自动化流程¶
8.3 敏捷开发的常用自动化工具¶
8.3.1 分布式版本控制工具Git¶
- 工作区(Workspace):在计算机中能看到的目录,它持有实际文件
- 缓存区(Index/Stage):临时保存我们的改动
- 版本库(Repository):工作区有一个隐藏目录.git,是Git的版本库
- 远程仓库(Remote):托管在因特网或其他网络中的项目的版本库,可供多人分布式开发
图8-14
8.3.2 持续集成和持续交付工具Jenkins¶
Pipeline是Jenkins的一套插件,实现了持续集成和持续交付的功能
- 自动为所有分支创建Pipeline
- 可以用代码评审Pipeline
- 可以跟踪Pipeline的改变记录
- 同一团队的成员可以共享一份Pipeline文件
图8-22
Pipeline常用的指令
- Agent:指示Jenkins需要为Pipeline分配执行器和工作空间
- Step:是Jenkins里Job中的最小单位,可以认为是一个脚本的调用和一个插件的调用。通常Steps告诉Jenkins要做什么,例如:sh ’make’ 指在命令行下执行make命令
- Node:可以给定参数来选择Agent,Node里的Step将会运行在Node选择的Agent上,Job里可以有多个Node,将Job的Step按照需求运行在不同的机器上。例如一个Job里有多个测试集合需要同时运行在不同的机器上
- Stage:是一些Step的集合,我们通过stage可以将Job的所有Step划分为不同的Stage,使得整个Job像管道一样容易维护
8.3.3 基础平台管理工具SaltStack¶
SaltStack采用C/S模式,其Server端是Master,Client端是Minion,Minion与Master之间通过ZeroMQ消息队列通信,是一个同时对一组服务器远程执行命令和状态管理的工具
8.3.4 Docker容器化工具¶
图8-29 Docker的基本组件
- Docker镜像(Image)是一个运行容器的只读模板
- Docker容器(Container)是一个运行应用的标准化单元
- Docker注册服务器(Registry)用于存放镜像
- Docker引擎(Docker Engine)用于在主机上创建、运行和管理容器
References¶
- 《分布式服务架构——原理、设计与实战》