admin管理员组

文章数量:1600301

目录

  • 00 高并发、高流量、分布式
    • 00 三高定义
        • 高并发
        • 高性能
        • 高可用
    • 01 三高特点:分层、冗余、分隔、异步、分布式、安全、自动化、集群、缓存
    • 02 三高实际设计方案简介
      • 高并发-负载均衡、池化技术、流量漏斗
      • 高性能-高性能缓存、日志优化,避免IO瓶颈
      • 高可用-主备切换,缩减故障时、熔断,提供过载保护、限流,提供过载保护、降级
    • 03 分布式token
    • 04 分布式数据库的概述
      • 1. 分布式数据库的架构模式主要包括两种:单主模式和多主模式
      • 2. 高并发、高可用架构设计
      • 3. 大数据量高并发情况下如何更新缓存
      • 4. 分布式数据库的性能优化
    • 05 消息队列
      • 对比
      • kafak定义
      • 发布订阅模式:一对多,消费者消费数据之后不会清除消息
      • 存储机制:Kafka的消息是存储在磁盘的,所以数据不易丢失。
      • 生产流程
      • 消费流程
      • FAq
        • 1.Kafka的多分区多副本机制有什么好处?
        • 2.Kafka如何实现高效读写?
        • 4.Kafka如何保证消息的顺序消费呢?
        • 5.Kafka如何保证消息不丢失?
        • 6.Kafka如何保证消息不重复消费?
  • 一、设计模式
    • 1.1 设计模式对比
      • 创建型对比-单例、工厂、抽象工厂、原型、建造者
      • 结构型对比-代理、适配器、装饰器
      • 行为型模式-策略、迭代器、模板、过滤器
    • 1.2 六大设计原则
    • 1.3 组合模式
      • 1) 工厂模式 + 单例模式
      • 2) 模板方法模式+策略模式
      • 3) 策略模式+工厂模式
      • 4) 适配器模式+装饰器模式
      • 5)观察者模式 + 命令模式
  • 二、DDD领域驱动设计
    • 2.1 FAQ
      • DDD 软件思想
      • 基本解释
      • DDD战略设计
      • 六边形架构
      • 实体值对象
    • 2.2. MVC和DDD
      • MVC
    • 2.3. DDD 四大核心特点
      • 1 以通用语言建设为核心,比如python的包也算是某种意义上的通用
      • 2 以一系列抽象概念为开发模式
      • 3 以四层架构为基本思想
        • ==以聚合解决类爆炸问题==
      • 4 解决系统老化问题
    • 2.4. 三大设计原则
    • 2.5. 充血模型、贫血模型
      • ==贫血模型==
      • ==充血模型==
      • ==总结==
    • 2.6. DDD 视角下的微服务建设过程
      • 2.7 搭建服务开放平台
      • 2.8 微服务架构落地难点
  • 三、架构
    • 3.1 基本概念
    • 3.2 互联网架构
    • 1 业务层
      • 1.1 单体模式
      • 1.2 中台战略
    • 2、数据层
      • 2.1 主从读写
      • 2.2 分库分表
      • 2.3 高速缓存
      • 2.4 数据多样化
    • 3、应用层
      • 3.1 动静分离
      • 3.2 SOA
      • 3.3 微服务
    • 4、部署层
      • 4.1 角色划分
      • 4.2 应用集群
      • 4.3 多层代理
      • 4.4 异地访问
      • 4.4 云平台
  • 四、容器化概述
    • 4.1 容器技术
    • 4.2 容器化与虚拟机的区别
      • 虚拟机容易遭受攻击
      • 不是所有场合都适用容器技术
      • 存储方案
    • 4.3 Docker 引擎
      • c/s架构
      • 平台组成
      • 生命周期
      • docker 镜像
    • 4.4 Docker的应用场景
    • 4.5基于centos7 部署 docker
      • 1 linux文件系统
      • .2 Docker的三个基本概念:镜像、容器、仓库
    • 4.6 k8s
      • 1docker容器问题
      • 2 k8s功能
      • 3k8s架构
      • 4 master结构
      • 5 node结构
      • 举例-协调来完成一次Pod的调度执行操作
    • 4.7 k8s 核心概念
      • Pod
      • Volume
      • Deployment
      • service
      • namespace
      • k8s中的api
  • 五、Zookeeper
    • 5.1 是什么
    • 5.2 数据模型
    • 5.3 zookeeper应用场景
    • 5.4 zookeeper选举策略
    • 5.5 zookeeper集群、ZAB协议
  • 六、全链路压测
    • 背景
      • QPS等概念
      • 最佳线程数
    • 6.1 什么是全链路压测?
    • 6.2 与传统方式的对比
    • 6.3 如何展开全链路压测
      • 业务模型梳理
      • 数据模型构建
      • 压测工具选型
    • 6.4 全链路整体架构
      • 核心技术
      • 涉及的业务问题
      • 框架实现
      • 流量染色方案
        • 流量识别
        • tomcat线程池复用问题
        • fegin传递染色标识
        • Hystrix传递染色体标识
      • RabbitMQ 数据隔离
  • 七、RPC
    • 7.1 基础
      • 定义&特点
      • 具体实现框架
      • 应用场景
    • 7. 2. RPC的关键技术点&一次调用rpc流程
      • 1 RPC流程
        • 两个网络模块如何连接的呢?
        • 其它特性
        • RPC优势
    • 7.3 序列化技术
      • 序列化方式
      • PRC如何选择序列化框架
      • 考虑因素
    • 7.4 应用层的通信协议-http
      • 基础概念
        • 大多数RPC大多自研http,也支持合http1.1
        • 什么是IO
        • 边缘触发
        • 水平触发
        • 事件驱动 IO
        • 异步 IO
      • 操作系统的IO模型有哪些
        • 同步阻塞IO
        • 非阻塞式 IO
        • IO多路复用
        • select
        • poll
        • epoll
      • IO总结
      • 线程模型
    • 7.5 动态代理
      • 其它动态代理方案
    • 7.6 基于ZK注册的原理
      • 基于zk注册数据的存储结构-以dubbo为例
    • 7.7 容错策略之超时重试
      • 什么是超时重试?
      • 如何检测请求是否超时?
      • 时间轮算法
    • 7.8 熔断限流
      • 熔断降级的概念
      • 如何判断熔断
      • 熔断工作合适开始?又合适结束
      • 有了熔断降级为何还要限流?
      • 常见的限流算法
    • 7.9 实现一个自己定义的RPC框架-mini版
      • 1 PRC框架
      • 2 Netty
      • 3Dubbo

00 高并发、高流量、分布式

00 三高定义

高并发
  1. 指标
    • 响应时间:系统对进来的请求反应的时间,比如你打开一个页面需要1秒,那么这1秒就是响应时间。
    • 吞吐量:吞吐量是指每秒能处理多少请求数量,好比你吃饭,每秒能吃下多少颗米饭。
    • 秒查询率:秒查询率是指每秒响应请求数,和吞吐量差不多。
    • 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
高性能
  1. 定义
    • 高性能是指程序处理速度非常快,所占内存少,cpu占用率低
    • 应用性能优化的时候,对于计算密集型和IO密集型还是有很大差别,需要分开来考虑。还有可以增加服务器的数量,内存,IO等参数提升系统的并发能力和性能,但不要浪费资源,要考虑硬件的使用率最高才能发挥到极致
  2. 方法
    • ① 避免因为IO阻塞让CPU闲置,导致CPU的浪费
    • ② 避免多线程间增加锁来保证同步,导致并行系统串行化
    • ③ 免创建、销毁、维护太多进程、线程,导致操作系统浪费资源在调度上
高可用
  1. 定义
    • 高可用通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性
    • 比如现在redis的高可用的集群方案有: Redis单副本,Redis多副本(主从),Redis Sentinel(哨兵),Redis Cluster,Redis自研

01 三高特点:分层、冗余、分隔、异步、分布式、安全、自动化、集群、缓存

  1. 分层

    • 常见的为3层,即应用层、服务层、数据层。应用层具体负责业务和视图的展示;服务层为应用层提供服务支持;数据库提供数据存储访问服务,如数据库、缓存、文件、搜索引擎等
  2. 冗余

    • 网站需要7×24小时连续运行,那么就得有相应的冗余机制,以防某台机器宕掉时无法访问,而冗余则可以通过部署至少两台服务器构成一个集群实现服务高可用。数据库除了定期备份还需要实现冷热备份。甚至可以在全球范围内部署灾备数据中心
  3. 分隔

    • 如果说分层是将软件在横向方面进行切分,那么分隔就是在纵向方面对软件进行切分。
    • 比如在应用层,将不同业务进行分隔,例如将购物、论坛、搜索、广告分隔成不同的应用
  4. 异步

    • 使用异步,业务之间的消息传递不是同步调用,而是将一个业务操作分成多个阶段,每个阶段之间通过共享数据的方法异步执行进行协作。具体实现则在单一服务器内部可用通过多线程共享内存对了的方式处理;在分布式系统中可用通过分布式消息队列来实现异步。异步架构的典型就是生产者消费者方式,两者不存在直接调用。
  5. 分布式

    • 对于大型网站,分层和分隔的一个主要目的是为了切分后的模块便于分布式部署,即将不同模块部署在不同的服务器上,通过远程调用协同工作
    • 在网站应用中,常用的分布式方案有一下几种.分布式应用和服务:将分层和分隔后的应用和服务模块分布式部署,可以改善网站性能和并发性、加快开发和发布速度、减少数据库连接资源消耗
    • 分布式静态资源:网站的静态资源如JS、CSS、Logo图片等资源对立分布式部署,并采用独立的域名,即人们常说的动静分
    • 分布式数据和存储:大型网站需要处理以P为单位的海量数据,单台计算机无法提供如此大的存储空间,这些数据库需要分布式存储
    • 分布式计算:目前网站普遍使用Hadoop和MapReduce分布式计算框架进行此类批处理计算,其特点是移动计算而不是移动数据,将计算程序分发到数据所在的位置以加速计算和分布式计算
  6. 安全

    • 网站在安全架构方面有许多模式:通过密码和手机校验码进行身份认证;登录、交易需要对网络通信进行加密;为了防止机器人程序滥用资源,需要使用验证码进行识别;对常见的XSS攻击、SQL注入需要编码转换;垃圾信息需要过滤等
  7. 自动化

    • 具体有自动化发布过程,自动化代码管理、自动化测试、自动化安全检测、自动化部署、自动化监控、自动化报警、自动化失效转移、自动化失效恢复等
  8. 集群

    • 对于用户访问集中的模块需要将独立部署的服务器集群化,即多台服务器部署相同的应用构成一个集群,通过负载均衡设备共同对外提供服务
  9. 缓存

    • 缓存目的就是减轻服务器的计算,使数据直接返回给用户。在现在的软件设计中,缓存已经无处不在。具体实现有CDN、反向代理、本地缓存、分布式缓存等。
    • 使用缓存有两个条件:访问数据热点不均衡,即某些频繁访问的数据需要放在缓存中;数据在某个时间段内有效,不过很快过期,否在会因为数据过期而脏读,影响数据的正确性。

02 三高实际设计方案简介

高并发-负载均衡、池化技术、流量漏斗

  1. 将流量转发到服务器集群,这里面就要用到负载均衡,比如:LVS 和 Nginx
    • 常用的负载算法有轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接数法等业务实战:对于千万级流量的秒杀业务,一台LVS扛不住流量洪峰,通常需要 10 台左右,其上面用DDNS(Dynamic DNS)做域名解析负载均衡。搭配高性能网卡,单台LVS能够提供百万以上并发能力。
    • 注意, LVS 负责网络四层协议转发,无法按 HTTP 协议中的请求路径做负载均衡,所以还需要 Nginx
  2. 池化技术:
    • 复用单个连接无法承载高并发,如果每次请求都新建连接、关闭连接,考虑到TCP的三次握手、四次挥手,有时间开销浪费。池化技术的核心是资源的“预分配”和“循环使用”,常用的池化技术有线程池、进程池、对象池、内存池、连接池、协程池
    • 连接池的几个重要参数:最小连接数、空闲连接数、最大连接数
    • Linux 内核中是以进程为单元来调度资源的,线程也是轻量级进程。所以说,进程、线程都是由内核来创建并调度。协程是由应用程序创建出来的任务执行单元,比如 Go 语言中的协程“goroutine”。协程本身是运行在线程上,由应用程序自己调度,它是比线程更轻量的执行单元
  3. 流量漏斗
    • 逆向思维,做减法,拦截非法请求,将核心能力留给正常业务
    • 拦截器分层:
      • 网关和 WAF(Web Application Firewall,Web 应用防火墙)
      • 采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法
      • 风控分析。借助大数据能力分析订单等历史业务数据,对同ip多个账号下单、或者下单后支付时间过快等行为有效识别,并给账号打标记,提供给业务团队使用。
      • 下游的每个tomcat实例应用本地内存缓存化,将一些库存存储在本地一份,做前置校验。当然,为了尽量保持数据的一致性,有定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中。

高性能-高性能缓存、日志优化,避免IO瓶颈

  1. 缓存根据性能由高到低分为:寄存器、L1缓存、L2缓存、L3缓存、本地内存、分布式缓存
    • 上层的寄存器、L1 缓存、L2 缓存是位于 CPU 核内的高速缓存,访问延迟通常在 10 纳秒以下。L3 缓存是位于 CPU 核外部但在芯片内部的共享高速缓存,访问延迟通常在十纳秒左右。高速缓存具有成本高、容量小的特点,容量最大的 L3 缓存通常也只有几十MB。
    • 本地内存是计算机内的主存储器,相比 CPU 芯片内部的高速缓存,内存的成本要低很多,容量通常是 GB 级别,访问延迟通常在几十到几百纳秒。
    • 内存和高速缓存都属于掉电易失的存储器,如果机器断电了,这类存储器中的数据就丢失了。
  2. 日志优化,避免IO瓶颈
    • 当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 “iowait”
    • ‘在IO中断过程中,如果此时有其他任务线程可调度,系统会直接调度其他线程,这样 CPU 就相应显示为 Usr 或 Sys;但是如果此时系统较空闲,无其他任务可以调度,CPU 就会显示为 iowait(实际上与 idle 无本质区别)。
    • 磁盘有个性能指标:IOPS,即每秒读写次数,性能较好的固态硬盘,IOPS 大概在 3 万左右。对于秒杀系统,如果单节点QPS在10万,每次请求产生3条日志,那么日志的写入QPS在 30W/s,磁盘根本扛不住。
    • Linux 有一种特殊的文件系统:tmpfs(临时文件系统),它是一种基于内存的文件系统,由操作系统管理。当我们写磁盘的时候实际是写到内存中,当日志文件达到我们的设置阈值,操作系统会将日志写到磁盘中,并将tmpfs中的日志文件删除。

高可用-主备切换,缩减故障时、熔断,提供过载保护、限流,提供过载保护、降级

  1. 高可用指标是指用来衡量一个系统可用性有多高。MTBF(Mean Time Between Failure),系统可用时长。MTTR(Mean Time To Repair),系统从故障后到恢复正常所耗费的时间SLA(Service-Level Agreement),服务等级协议,用于评估服务可用性等级。计算公式是 MTBF/(MTBF+MTTR)。一般我们所说的可用性高于 99.99%,是指 SLA 高于 99.99%。
  2. 方法
    • ①多云架构、异地多活、异地备份
    • ②主备切换,如redis缓存、mysql数据库,主备节点会实时数据同步、备份。如果主节点不可用,自动切换到备用节点
      • 第一步故障自动侦测(Auto-detect),采用健康检查、心跳等技术手段自动侦测故障节点;
      • 第二步自动转移(FailOver),当侦测到故障节点后,采用摘除流量、脱离集群等方式隔离故障节点,将流量转移到正常节点;
      • 第三步自动恢复(FailBack),当故障节点恢复正常后,自动将其加入集群中,确保集群资源与故障前一致。
    • ③微服务,无状态化架构,业务集群化部署,有心跳检测,能最短时间检测到不可用的服务。
    • ④通过熔断、限流,解决流量过载问题,提供过载保护
      • 例子:熔断触发条件往往跟系统节点的承载能力和服务质量有关,比如 CPU 的使用率超过 90%,请求错误率超过 5%,请求延迟超过 500ms, 它们中的任意一个满足条件就会出现熔断
      • 限流算法主要有:计数器限流、滑动窗口限流、令牌桶限流、漏桶限流
    • ⑤重视web安全,解决攻击和XSS问题

03 分布式token

  • 在涉及到统一网关入口的时候,难免会涉及到多系统的鉴权机制,如果在在集群方案中,会有一个问题,当用户第一次登录在tomcat1上了,接口回去请求其他的接口,由于负载均衡的原因,下一个请求可能会打在了tomcat2上,此时是校验不通过的,为了解决这一问题,于是分布式token来了
  • 第一步:用户登录,验证账号密码成功将token写入到cookie中,并同时将用户信息写入redis
  • 第二步:用户请求其他的接口,携带着登录成功返回的cookie
  • 第三步:服务器从request中获取到cookie,解析,查询token是否存在
  • 第四步:存在,继续业务,不存在,返回错误

04 分布式数据库的概述

1. 分布式数据库的架构模式主要包括两种:单主模式和多主模式

  • 单主模式:所有的数据都存储在一个主节点上,其他从节点可以读取数据,但只有主节点才能进行写操作。这种模式下,主节点可能成为系统的瓶颈,同时也存在单点故障的风险
  • 多主模式:数据被划分为多个分片,每个分片都由多台服务器承载,各个分片之间相互独立。这种模式下,数据可以在多个节点上同时进行读写操作,系统具备更好的可扩展性和容错性

2. 高并发、高可用架构设计

  1. 分库分表
    在实现高并发、高可用架构时,分库分表是一种常见的优化手段。将数据按照一定规则进行拆分,可以将单个数据库承载的数据量降低到一个可接受的范围内,从而提高系统的性能和可扩展性。
  2. 读写分离
    读写分离是指将读和写操作分开处理,可以有效地减轻数据库的压力,并且提高系统的性能和可用性。具体来说,可以通过主从复制或者分布式事务来实现读写分
  3. 主从复制是指将一个数据库服务器作为主库,其他服务器作为从库。主库负责处理所有的写操作,并将数据同步到从库中,而从库只负责读取数据。这样可以有效地降低主库的压力,同时也能够提高系统的可用性
  4. 缓存优化
    缓存技术是一种常见的优化手段,可以将热点数据缓存在内存中,提高数据的访问速度和响应时间。常见的缓存技术有Redis、Memcached等
  5. 数据库性能优化
    数据库性能优化是一个非常复杂的问题,需要从多个方面入手,包括SQL语句的优化、索引的设计、数据存储结构的选择、连接池的设置等。
    • (1)避免全表扫描和索引失效:可以通过合理的索引设计和SQL语句优化来解决这个问题。

    • (2)选择合适的存储结构:可以考虑使用B+树、哈希表等数据结构来提高数据的查询效率。

    • (3)控制事务数量和并发度:过多的事务和并发操作可能会导致数据库性能下降,因此需要合理控制事务数量和并发度。

    • (4)定期清理无用数据:及时清理无用数据可以有效地减轻数据库压力,并且提高系统性能。

3. 大数据量高并发情况下如何更新缓存

  1. 首先是查询的时候,一般先查询缓存,在查询数据库,同步的去更新缓存
  2. 但是都是异步去更新,引入消息队列MQ

    本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务

  3. 异步修改完数据库之后,去更新缓存,可能出现数据不一致的情况,如果缓存设置的实践比较短,是可以接受的
  4. 查询缓存,没有数据, 查询数据库,写一个MQ消息,异步的去更新(也可以用多线程异步去代替),可以加一个分布式锁来实现,但是锁效率很低。所以可以对数据库做限流,不能加锁
  5. 可以对数据库分库分表,主从,数据库层面限流
  6. 防止重复更新。可以做一个分布式锁,给mq消息加一个唯一ID,防止重复消费
  7. 还有其它方法:监听数据库的binlog日志,有一个单独的程序监听,监听到了之后修改缓存,但是binlog流程是比较长的,代码量比较多
  8. 查询查不到,然后查询数据库,写入缓存这个情况。不是每个公司都把数据写入到缓存中,只把活跃的用户的数据写入到缓存中

4. 分布式数据库的性能优化

  1. 多级缓存技术
    多级缓存技术是指将数据缓存在不同的层次中,如内存、磁盘和网络中,以提高数据的读写效率。具体来说,可以采用如下几种缓存技术:
    • (1)本地缓存:将热点数据缓存在内存中,以提高数据的访问速度和响应时间。

    • (2)分布式缓存:将数据缓存在多台服务器上,通过网络进行通信和协作,以提高系统的可扩展性和容错性。

    • (3)CDN缓存:将静态资源缓存在CDN节点上,以提高用户访问速度和网站性能。

  2. 异步I/O技术
    • 异步I/O技术是指将I/O操作交给系统内核处理,而不是由应用程序自己处理,从而减少系统的响应时间和CPU负载。这种技术在高并发、高负载场景下非常有效。
  3. 并发控制和事务管理
    • (1)锁机制:可以采用悲观锁或乐观锁来控制数据的并发访问
    • (2)分布式事务:需要选择合适的分布式事务协议来保证数据的一致性和原子性。
    • (3)读写分离:通过实现读写分离来减轻主库的压力,并提高系统的性能和可用性。

05 消息队列

对比

RabbitMQActiveMQRocketMQKafka
单机吞吐量万级万级十万级(单机万级)十万级
topic数量对吞吐量的影响\\topic可以达到千的级别,吞吐量会有较小幅度的下降这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topictopic从几十个到几百个的时候,吞吐量会大幅度下降同等机器下,topic数量不要过多,如果topic一定要大,那么需要增加更多的机器资源
消息写入性能RAM约为RocketMQ的1/2Disk的性能约为RAM性能的1/3\ 很好每条10字节测试:单机单broker约7w/s非常好每条10字节测试:百万条/s
可用性高(主从架构)master提供服务,slave仅做备份。采用镜像模式,数据量大时可能产生性能瓶颈高(主从架构)基于ZooKeeper+LevelDB的主从实现方式高(分布式架构)支持多master、多master多slave模式,异步复制模式,同步双写非常高(分布式架构)支持replica机制,leader宕机后,备份自动顶替,并重新选举leader(基于Zookeeper)
消息延迟微秒级毫秒级毫秒级毫秒级以内
消息可靠性可以保证数据不丢失,有slave节点做备份有较低的概率丢失数据经过参数优化配置,理论上可以做到0丢失经过参数优化,理论上可以做到0丢失
持久化能力内存、文件支持数据堆积,但数据堆积反过来影响生产速率内存、文件、数据库磁盘文件磁盘文件只要磁盘容量够,就可以做到无限消息堆积
是否有序若想有序,只能使用1个Client可以支持有序有序多Client保证有序
消息批量操作不支持支持支持支持
集群支持支持支持支持
事务不支持支持支持不支持,但可以通过Low Level API保证仅消费一次

链接:kafka

kafak定义

  1. Kafka是一个分布式的基于发布/订阅模式的消息队列。分布式消息队列可以看成是将这种先进先出的数据结构独立部署在服务器上,应用程序可以通过远程访问接口使用它

发布订阅模式:一对多,消费者消费数据之后不会清除消息

存储机制:Kafka的消息是存储在磁盘的,所以数据不易丢失。

生产流程

  • 1)主线程首先将业务数据封装成ProducerRecord对象
  • 2)调用send方法将消息放入消息收集器RecordAccumlator中
  • 3)Sender线程将消息信息构成请求
  • 4)执行网络IO的线程从RecordAccumlator中将消息取出并批量发送出去

消费流程

  • Kafka消费者从属于消费者组。消费者组内的消费者订阅的是相同主题,每个消费者接收主题的一部分分区的消息

FAq

1.Kafka的多分区多副本机制有什么好处?
  • 1)Kafka通过将特定topic指定到多个partition,各个partition分布到不同的Broker上,这样能够提供比较好的并发能力。
  • 2)Partition可以指定对应的replica数,这也极大地提高了消息存储的安全性和容灾能力。
2.Kafka如何实现高效读写?
  • 1)顺序写入磁盘:在日志文件尾部追加,顺序写入且不允许修改。

  • 2)页缓存:每次从磁盘中加载一页的数据到内存中这样可以减少IO次数。

  • 3)零拷贝技术:只用将磁盘中的数据复制到页面缓存中一次,然后将数据从页面缓存中发送到网络中,避免了重复复制操作。

    • 零CPU拷贝模式:splice系统调用可以在内核缓冲区和socket缓冲区之间建立管道来传输数据,避免了两者之间的CPU拷贝操作
4.Kafka如何保证消息的顺序消费呢?
  • 1)当partition只有一个时可以做到全局有序,Kafka只能保证分区内部消息消费的有序性。
  • 2)在发送消息时指定key和postion,从而可以保证间隔有序。
5.Kafka如何保证消息不丢失?

1)生产者确认

  • topic的每个partition收到producer发送的数据后,都需要向producer发送ack,如果producer收到ack,就会进行下一轮发送,否则重新发送数据。

  • Leader维护了一个动态的ISR(in-sync replica),它是一个保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会给follower发送一个ack。如果follower长时间未从leader同步数据,则该follower将被踢出ISR,该时间由阈值replica.lag.max.ms参数设定。Leader发生故障后,ISR将会重新选举新的Leader。

Kafka为用户提供了三种可靠性级别,用户可根据对可靠性和延迟的要求权衡。

  • ack=0,生产者在成功写入消息之前不会等待任何来自服务器的响应,如果出现问题生产者感知不到,但能够以网络支持的最大速度发送消息。

  • ack=1,默认值,只要集群的首领节点leader收到消息,生产者就会收到一个来自服务器的成功响应。如果消息无法到达首领节点,生产者会收到一个错误响应,为了避免数据丢失,生产者将重发消息。如果收到写成功通知,但首领节点还没来的及同步follower节点就崩溃了,也会造成数据丢失。

  • ack=-1,只有当所有所有参与复制的节点收到消息后,生产者会收到一个来自服务器的成功确认。如果在follower同步完成之后,broker返回ack之前,leader发生故障,那么会造成数据重复。

2)消费者确认

  • 一次poll会拉取一批消息,对应的消费位移是一个区间,如果是拉取信息之后进行位移提交,在消费中间中间发生了故障,会造成消息丢失现象。如果是消费完成之后进行位移提交,在消费中间发生了故障,会造成重复消费现象。

  • 将位移提交方式改为手动提交,即每次消费完成之后提交,可以避免因为消费未完成出现异常导致的消息丢失。

6.Kafka如何保证消息不重复消费?
  • 自动提交offset,在下一次提交位移之前消费者崩溃了,那么又会从上一次位移提交的地方重新开始消费,这样便造成了重复消费。

  • 使用异步提交方式,此时可以设置一个递增的序号来维护异步提交的顺序,每次位移提交之后就增加对应的序号值。在遇到位移提交失败需要重试的时候,可以检查所需要提交的位移和序号值的大小,如果前者的值大于后者,则说明有更大的位移已经提交了,不需要进行本次重试;如果前者等于后者,则进行重试。除非编码错误,否则不会出现前者大于后者的情况。

一、设计模式

1.1 设计模式对比

创建型对比-单例、工厂、抽象工厂、原型、建造者

定义:创建型提供创建对象的机制,提升已有代码的灵活性和可复用性

模式定义优点缺点应用场景
单例模式确保一个类仅有一个实例,并提供一个全局访问点节约系统资源,提高系统性能;提供对唯一实例的受控访问不易扩展,特别是当需要不同实例时;滥用单例可能会导致资源溢出或状态丢失系统只需一个实例对象,如配置文件的读取、数据库连接池等
工厂方法模式定义一个用于创建对象的接口,让子类决定实例化哪一个类隐藏了对象的创建细节,降低了耦合度;易于扩展,增加新产品时只需添加新的具体工厂和具体产品增加了系统的复杂度,因为每增加一个产品,就需要增加一个具体工厂和具体产品客户端不知道它所需要的对象的类
抽象工厂模式供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类易于交换产品系列,分离了具体类的创建和使用增加了系统的抽象性和理解难度,需要添加新产品族时,需要修改抽象工厂的接口系统功能结构稳定,不需要频繁新增功能,但需要支持多种产品族
原型模式(Prototype)通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象创建通过复制,提高了新建的效率每个类都需要实现克隆方法,增加了实现的复杂度对象相似,可以通过复制加修改实现的对象创建
建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示封装了复杂对象的创建过程,易于扩展,增加新的构建部分不需要修改现有的类如果产品内部变化复杂,会增加系统的难度和运行成本创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式

结构型对比-代理、适配器、装饰器

结构型模式用于描述如何将类或对象结合在一起形成更大的结构

模式定义优点缺点应用场景
代理模式(Proxy)为其他对象提供一种代理以控制对这个对象的访问降低耦合度,扩展性好处理速度可能变慢,因为需要通过代理访问实际对象远程代理,虚拟代理,安全代理等
适配器模式(Adapter)将一个类的接口转换成客户端所期待的另一种接口形式,使因接口不匹配而不能在一起工作的类可以一起工作灵活性、扩展性好不支持多重继承系统的数据和行为都正确,但接口不符
装饰模式(Decorator)动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活装饰类和被装饰类可以独立发展,不会相互耦合会产生较多的装饰类封装了复杂对象的创建过程,易于扩展,增加新的构建部分不需要修改现有的类

行为型模式-策略、迭代器、模板、过滤器

负责对象间的高效沟通和职责传递委派

模式定义优点
策略模式(Strategy)定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户算法可以独立变化,不影响客户端
迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示
模板方法模式定义一个操作中的算法骨架,将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤
过滤器设计模式允许在不改变原始对象的情况下,动态地添加或删除对象的行为

1.2 六大设计原则

原则定义
单一职责原则一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一项职责。这样可以保证类的内聚性,并且降低类之间的耦合性。
开闭原则一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。这意味着当需要添加新功能时,应该尽量通过扩展已有代码来实现,而不是修改已有代码。
里氏替换原则子类应该能够替换父类并且不影响程序的正确性。这意味着在使用继承时,子类不能修改父类已有的行为,而只能扩展父类的功能。
接口隔离原则客户端不应该依赖于它不需要的接口。一个类应该只提供它需要的接口,而不应该强迫客户端依赖于它不需要的接口。
依赖倒置原则高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现,而具体实现应该依赖于抽象。
迪米特法则一个对象应该对其他对象保持最少的了解。换句话说,一个对象只应该与它直接相互作用的对象发生交互,而不应该与其它任何对象发生直接的交互。这样可以降低类之间的耦合性,提高系统的灵活性和可维护性。

1.3 组合模式

1) 工厂模式 + 单例模式

  1. 使用工厂模式来创建对象,通过单例模式保证该工厂只有一个实例,从而减少创建对象时的开销
  2. 首先,创建一个工厂类,该类使用单例模式来保证只有一个实例,该实例负责创建对象。然后,根据需要创建多个工厂方法,每个方法用于创建不同的对象。
public class SingletonFactory {
    private static volatile SingletonFactory instance;

    private SingletonFactory() {
        // 私有构造方法
    }

    public static SingletonFactory getInstance() {
        if (instance == null) {
            synchronized (SingletonFactory.class) {
                if (instance == null) {
                    instance = new SingletonFactory();
                }
            }
        }
        return instance;
    }

    public Object createObject(String type) {
        if ("type1".equals(type)) {
            return new Type1();
        } else if ("type2".equals(type)) {
            return new Type2();
        } else {
            throw new IllegalArgumentException("Unsupported type: " + type);
        }
    }
}

public class Type1 {
    // 类型1实现逻辑
}

public class Type2 {
    // 类型2实现逻辑
}

2) 模板方法模式+策略模式

  1. 使用模板方法名模式来定义算法的框架,同时使用策略模式定义算法的不同实现方法,以实现更高的活性
  2. 假设我们要实现一个图片处理程序,可以对不同类型的图片进行处理,包括缩放、旋转和裁剪等操作。具体的处理算法可以根据不同类型的图片而异
  3. 首先,我们定义一个抽象类 ImageProcessor,它包含一个模板方法 processImage(),该方法定义了一系列的处理步骤,包括打开图片、执行具体的处理算法和保存图片等。其中,具体的处理算法是由策略模式来实现的,我们使用一个抽象策略接口 ImageProcessingStrategy 来定义不同的处理算法。
public abstract class ImageProcessor {

    public void processImage() {
        BufferedImage image = openImage();
        ImageProcessingStrategy strategy = createImageProcessingStrategy();
        BufferedImage processedImage = strategy.processImage(image);
        saveImage(processedImage);
    }

    protected BufferedImage openImage() {
        // 打开图片的具体实现
    }

    protected abstract ImageProcessingStrategy createImageProcessingStrategy();

    protected void saveImage(BufferedImage image) {
        // 保存图片的具体实现
    }
}

public interface ImageProcessingStrategy {

    BufferedImage processImage(BufferedImage image);
}

首先,我们定义一个抽象类 ImageProcessor,它包含一个模板方法 processImage(),
该方法定义了一系列的处理步骤,包括打开图片、执行具体的处理算法和保存图片等。
其中,具体的处理算法是由策略模式来实现的,
我们使用一个抽象策略接口 ImageProcessingStrategy 来定义不同的处理算法

 然后,定义具体的图片处理类 JpegProcessorPngProcessor,
 它们分别继承自 ImageProcessor,并实现 createImageProcessingStrategy() 方法,
 返回不同的处理算法策略。
public class JpegProcessor extends ImageProcessor {

    @Override
    protected ImageProcessingStrategy createImageProcessingStrategy() {
        return new JpegProcessingStrategy();
    }
}

public class PngProcessor extends ImageProcessor {

    @Override
    protected ImageProcessingStrategy createImageProcessingStrategy() {
        return new PngProcessingStrategy();
    }
}

最后,定义不同的处理算法策略,例如 JpegProcessingStrategyPngProcessingStrategy,它们实现了 ImageProcessingStrategy 接口,
提供了具体的处理算法实现。
public class JpegProcessingStrategy implements ImageProcessingStrategy {

    @Override
    public BufferedImage processImage(BufferedImage image) {
        // Jpeg 图片处理算法
    }
}

public class PngProcessingStrategy implements ImageProcessingStrategy {

    @Override
    public BufferedImage processImage(BufferedImage image) {
        // Png 图片处理算法
    }
}


3) 策略模式+工厂模式

  1. 使用工厂模式来创建不同的策略对象,然后使用策略模式来选择不同的策略,以实现不同的功能。我们实现一个简单的计算器。
首先,我们定义一个 CalculatorStrategy 接口,其中包含了两个方法:
calculate 用于计算两个数的结果,getDescription 用于获取当前策略的描述信息。
public interface CalculatorStrategy {
    double calculate(double num1, double num2);
    String getDescription();
}

然后,我们实现几个具体的计算策略类,如加法、减法、乘法和除法:
public class AddStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 + num2;
    }

    @Override
    public String getDescription() {
        return "加法";
    }
}

public class SubtractStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 - num2;
    }

    @Override
    public String getDescription() {
        return "减法";
    }
}

public class MultiplyStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 * num2;
    }

    @Override
    public String getDescription() {
        return "乘法";
    }
}

public class DivideStrategy implements CalculatorStrategy {
    @Override
    public double calculate(double num1, double num2) {
        return num1 / num2;
    }

    @Override
    public String getDescription() {
        return "除法";
    }
}

 接下来,我们使用工厂模式来创建具体的策略对象。
我们创建一个 CalculatorStrategyFactory 工厂类,
其中定义了一个 getCalculatorStrategy 方法,根据传入的操作符,返回相应的计算策略对象。
public class CalculatorStrategyFactory {
    public static CalculatorStrategy getCalculatorStrategy(String operator) {
        switch (operator) {
            case "+":
                return new AddStrategy();
            case "-":
                return new SubtractStrategy();
            case "*":
                return new MultiplyStrategy();
            case "/":
                return new DivideStrategy();
            default:
                throw new IllegalArgumentException("无效的操作符:" + operator);
        }
    }
} 

4) 适配器模式+装饰器模式

  1. 适配器模式用于将一个接口转换成另一个接口,而装饰器模式则用于动态地给对象添加一些额外的职责。
  2. 当我们需要将一个已有的接口转换成新的接口,并且还需要给对象添加一些额外的职责时,可以使用这两个模式混合使用。

5)观察者模式 + 命令模式

  1. 观察者模式用于观察对象的状态变化,并及时通知观察者。
  2. 而命令模式则用于将一个请求封装成一个对象,可以在运行时动态地切换命令的接收者。当我们需要观察对象的状态变化,并在状态变化时执行一些命令时,可以使用这两个模式混合使用。
首先,我们创建一个接口 Observer 来表示观察者对象
其中包含一个 update() 方法用于更新观察者状态
public interface Observer {
    void update();
}

接着,我们创建一个类 Subject 来表示被观察者对象,
其中包含一些观察者对象的引用和一些方法用于注册、注销和通知观察者
import java.util.ArrayList;
import java.util.List;

public class Subject {
    private List<Observer> observers = new ArrayList<>();

    public void register(Observer observer) {
        observers.add(observer);
    }

    public void unregister(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}

 接下来,我们创建一个接口 Command 来表示命令对象,其中包含一个 execute() 方法用于执行命令
public interface Command {
    void execute();
}
 然后,我们创建一个具体命令类 ConcreteCommand,该类实现了 Command 接口,其中包含一个 Subject 对象的引用和一个 execute() 方法,该方法会调用 Subject 对象的 notifyObservers() 方法通知观察者对象。



二、DDD领域驱动设计

2.1 FAQ

DDD 软件思想

  • 技术主动理解业务,业务如何运行,软件如何构建。别没完没了的重构了,让复杂系统保持年轻,项目越复杂。使用DDD的收益越大。学会使用动态的角度看待技术!
  • 目的就是对业务做拆分
  • DDD 核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性
  • DDD是从设计到开发到落地贯穿全部的功能
  • 使用充血模型的实体对象:实体能做什么事情,一了然
  • 使用仓库与工厂封装实体持久化操作,摆脱数据库限制
  • DDD妙招:防腐层隔离第三方软件

基本解释

DDD将一个软件系统的核心业务集中在一个核心域里面,其中包含了实体、值对象、领域服务、资源库和聚合等概念,不是面向对象那么简单,而是演变成了一个工程

DDD战略设计

  • 包括领域、子领域、通用语言、限界上下文和架构风格等概念。
    DDD对系统的划分是基于领域的,也就是基于业务的。问题1:那些概念应该建模在那些子系统里;问题2:各子系统之间该如何集成(不同系统之间涉及到基础设施和不同领域概念在两个系统之间的翻译)
    • (1)限界上下文(bounded context):在一个领域/子域中,我们会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。技术本身并不应该用来界分限界上下文,为该限界上下文创建了一套通用语言。通用语言是一个团队所有成员交流时所使用的语言。
    • (2)各个子域之间的集成问题,其实也是限界上下文之间的集成问题。在集成时,主要关心的是领域模型和集成手段之间的关系。比如需要一个资源集成,需要提供基础设施,但这些设施并不是核心领域模型的一部分,方法就是防腐层,该层负责与外部服务提供方打交道,还负责将外部概念翻译成自己的核心领域能够理解的概念
      设计步骤

六边形架构

DD的架构风格可以采用传统的三层式架构,比较推荐的是时间驱动架构和六边形架构。六边形架构:在六边形架构中,已经不存在分层的概念,所有组件都是平等的。这主要得益于软件抽象的好处,即各个组件的之间的交互完全通过接口完成,而不是具体的实现细节。

  • 领域如何与端口进行交互?
    • 软件系统的真正价值在于提供业务功能,我们会将所有的业务功能分解为若干个业务用例,每一次业务用例都表示对软件系统的一次原子操作,以业务用例为单位向外界暴露该系统的业务功能。这样的组建称为应用层。所以软件系统和外界的交互变成了适配器和应用层之间的交互。应用层虽然很薄,但却非常重要,因为软件系统的领域逻辑都是通过它暴露出去的,此时的应用层扮演了系统门面(Facade)的角色

实体值对象

实体表示那些具有生命周期并且会在其生命周期中发生改变的东西;而值对象则表示起描述性作用的并且可以相互替换的概念。多数领域概念都可以建模成值对象,而非实体。值对象就像软件系统中的过客一样,具有“创建后不管”的特征,因此,我们不需要像关心实体那样去关心诸如生命周期和持久化等问题。

2.2. MVC和DDD

MVC

  • 什么是MVC,model-view-controller
    • 模型(model):后端,包含了所有的数据逻辑
    • 视图(view):前端界面或GUI
    • 控制器(controller): 应用大脑,控制数据如何展示
  • 为什么使用MVC
    • 关注点分离(separation of concerns,Soc)
    • 助于将前端和后端代码拆分为独立的组件
  • 传统MVC带来的大泥球结构严重影响系统的灵活性

2.3. DDD 四大核心特点

1 以通用语言建设为核心,比如python的包也算是某种意义上的通用

2 以一系列抽象概念为开发模式

3 以四层架构为基本思想

  • 领域层:Domain Layer:放之四海而皆准的理想。系统的核心,纯粹表达业务能力,不需要任何外部依赖。表示业务逻辑、业务场景和规则。该层次会控制和使用业务状态,即使这些状态最终会交由持久化层来存储。总之,该层是软件核心
  • 应用层:Application Laver:理想与现实。协调领域对象,组织形成业务场景。只依赖于领域层,描述程序所要做的工作,并调度丰富的领域模型来完成,这个层次的任务是描述业务逻辑,或和其它项目的应用层做交互,这层很薄,不包含任何业务规则或知识,仅用于调度和派发任务给下一层的领域模型。这层没有业务状态,但可以为用户或程序提交任务状态
  • 用户层:UserInterface:护城河。负贵与用户进行交互。解释用户请求,返回用户响应。只依赖于应用层
  • 基础层:Infrastructure Layer:业务与数据分离。为领域层提供持久化机制,为其他层提供通用技术能力
以聚合解决类爆炸问题
  • 聚合是用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性

如何判断对象之间是否有聚合关系

  • 每个聚合内部有一个外部访问聚合的唯一入口,称为聚合根
  • 每个聚合中应确定唯一的聚合根实体
  • 通过聚合与聚合根的设计,极大简化整个系统内的对象关系图

4 解决系统老化问题

  • 新需求越来越难:不懂原来的代码如何实现的
  • 开发越来越难
  • 技术创新越来越难:外面技术很多,老系统没时间重构
  • 测试越来越难:没办法单元测试,一个小需求又要回归测试,累死了
    需要高内聚,低耦合

2.4. 三大设计原则

  • 单一职责:一个类只需要负责一个单一的职责,也就是一个类也只有一个引起它变化的原因
  • 开放封闭原则:对外扩展开放,对修改封闭
  • 依赖反转原则:程序之间应该只依赖于抽象接口,而不要依赖于具体实现

2.5. 充血模型、贫血模型

贫血模型

  • 【行为-逻辑、过程】状态-数据及对应到云烟就是对象成员变量,分离到不同的对象中
  • 只有状态的对象就是贫血对象
  • 只有行为的对象就是,我们常见的N层结构中的logic/service/mannger层

贫血模型

  • 但当你检视这些对象的行为时,会发现它们基本上没有任何行为,仅仅是一堆getter/setter
  • 这些对象在设计之初就被定义为只能包含数据,不能加入领域逻辑;逻辑要全部写入一组叫Service的对象中;而Service则构建在领域模型之上,需要使用这些模型来传递数据
    反模式的恐怖之处在于:它完全和面向对象设计背道而驰,面向对象设计主张将数据和行为绑定在一起,而贫血领域模型则更像是一种面向过程设计
  • 缺点
    • 比如收入确认规则发生变化,例如在4月1号之前签订的合同要使用某规则
    • 和欧洲签订的合同使用另外一个规则

充血模型

  • 面向对象设计的本质是:“一个对象是拥有状态和行为的”

  • 传统的设计一般是:

    • 类:User+UserManager;
    • 保存用户调用:userManager.save(User user)。
  • 充血的设计则可能会是:

    • 类:User;
    • 保存用户调用:user.save();
    • User有一个行为是:保存它自己。

总结

  • 贫血模型:
    • 定义对象的简单的属性值,没有业务逻辑上的方法
  • 充血模型
    • 充血模型也就是我们在定义属性的同时也会定义方法,我们的属性是可以通过某些方式直接得到属性值,那我们也就可以在对象中嵌入方法直接创建出一个具有属性值的对象。也就是说这个对象不再需要我们在进行进一步的操作,这也就复合了OOP的三大特性之一的封装
      • 定义
    • 贫血模型即事物脚本模式
    • 充血模型即领域模型模式

2.6. DDD 视角下的微服务建设过程

    1. 衔接上下文
    • 有了领域划分后,就需要保证领域之间的边界,这个边界就是界限上下文
    • 有了界限上下文的划分,单体,微服务,事件驱动这些架构都只是领域之间不同的协作方式,而领域本身是保持稳定的。

    1. 领域的落地方式-棱形编程模型
    1. DDD的宏观发展

      当领域足够丰富时,企业的业务能力就能够逐渐沉淀,形成领域仓库。未来构建新业务时。开发人员只需要从领域仓库中抽取对应的能力,组合拼凑即可,这时DDD有点像中台

2.7 搭建服务开放平台

2.8 微服务架构落地难点

  • 微服务边界如何划分:高内聚,低耦合
  • 微服务设计如何坚守:后期的各个牛鬼蛇神足够摧毁任何前期的优秀设计
  • 微服务项目如何推进:不是每个团队都有完整的业务、测试、开发、运维团队。

三、架构

3.1 基本概念

  1. 业务架构
  • 定义:业务战略,治理,组织和关键业务流程。从企业视角来看,重在价值、信息、协作,关联多部门。
  • 从业务角度描述系统承载的功能集合、领域边界、各组成部分的逻辑关系。区别于技术架构,业务架构图里避免出现技术类的术语,如 DB、MySQL、CMQ、同步、异步、并发等。
  1. 应用架构
  • 定义:要部署的各个应用程序的蓝图,其交互以及与组织核心业务流程的关系。
  1. 数据架构
  • 定义:一个组织的逻辑和物理数据资产和数据管理资源的结构。
  1. 技术架构
  • 定义:支持部署业务,数据和应用程序服务所需的逻辑软件和硬件功能。这包括IT基础设施,中间件,网络,通信,处理和标准。
  • 技术架构体现的要具有技术特色,例如同步、异步、消息等

3.2 互联网架构

  • 业务层:与访问连接的桥梁
  • 应用层:代码、项目
  • 数据层:数据
  • 部署层:环境(硬件、云平台)

1 业务层

1.1 单体模式

单体模式就是以单业务为主,逐个业务线扩张,系统也呈现为多个MVC独立运行状态,每个业务一套系统,会带来很多问题:

技术架构上

  • 相同的功能需要重复性的投资
  • 业务系统间的集成和协作成本高
  • 不利于基础性业务的沉淀和持续发展

组织架构上

  • 部门在单体模式往往每一个项目团队跟随项目疯狂扩展,项目利用率低

1.2 中台战略

中台是一种企业架构,实际上是共享理念在业务、系统、组织架构上的落地实施

2、数据层

2.1 主从读写

  • 连接多个数据库,数据之间形成主从关系,从库上读,读写压力被分散。
  • 数据库集群:一主多从、双主单写
  • 但是这个不能解决一个表过大的问题,就要考虑拆表的问题

2.2 分库分表

  • 主从库的写入依然是有一个统一的主库入口。随着业务量的提升,继续细粒度化拆分业务分库:订单库,产品库,活动库,会员库
  • 横向分表:(拆记录)3个月内订单,半年内订单,更多订单
  • 纵向分表:(拆字段)name、phone-张表,info、address一张表,俩表id一致(根据活跃度拆表,通过字段关联起来)

2.3 高速缓存

  • 一些热点数据放到redis中
  • redis性能可靠,纯内存,子带分片,集群,哨兵,支持持久化
    存在的问题
  • 缓存策略:冷热数据的存放,缓存与db的边界需要架构师去把控,重度依赖可能引发问题8XU(memcache造成db高压案例; redis短信平台故障案例)
  • 缓存陷阱: 击穿(单- key过期),穿透(不存在的 key),雪崩(多个 key 同时过期)
  • 数据一致性:缓存和 db 之间因为同一份数据保存了两份,自然带来了一致性问题

2.4 数据多样化

  • 分类
    搜索引擎(ES)、缓存集群(mysql)、mysql集群、分布式文件、nosql

  • 开发框架支持:
    存储的数据多样化,要求开发框架架构层面要提供多样化的支撑,并确保访问易用性
    数据运维:多种数据服务器对运维的要求提升,机器的数据维护与灾备工作量加大
    数据安全:多种数据存储的权限,授权与访问隔离需要注意

3、应用层

3.1 动静分离

3.2 SOA

解决服务之间的通信问题

3.3 微服务

  • 方案
    微服务是基于SOA的思想,将系统粒度进一步细化而诞生的一种手段,中台化得以实现,各个中心以及前端业务拆解为多个小的服务单元
  • 技术
    微服务经历了从1.0(cloud)到2.0的演化(service mesh),目前企业中主流的解决方案依然是cloud全家桶Spring Cloud(课题:Spring Cloud微服务前沿技术栈,Spring、Spring Boot源码剖析)
  • 特点
    服务拆分:粒度并非越小越好。太小会带来部署维护等一系列成本的上升。
    接口约束:系统增多,各个服务接口的规范化日益重要,要求有统一的服务接口规范,推动企业消息总线的建设
    权限约束:接口不是任意想调就可以调的,做好权限控制,借助oauth2等手段,实现服务之间的权限认证

4、部署层

4.1 角色划分

把数据据、缓存、消息等中间件剥离出去,单独机器部署

4.2 应用集群

  • 方案
    • apache:早期负载均衡方案,性能一般
    • Nginx:7层代理,性能强悍,配置简洁,当前不二之选
    • haproxy:性能同样可靠,可做7层或4层代理
    • Ivs:4层代理,性能最强,linux集成,配置麻烦
    • f5:4层,硬件负载,财大气粗的不二选择
  • 特点
    • session保持:集群环境下,用户登陆需要分布式session做支撑
    • 分布式协同:分布式环境下对资源的加锁要超出线程锁的范畴,上升为分布式锁
    • 调度问题:调度程序不能多台部署,容易跑重复,除非使用分布式调度,如elastic-job
    • 机器状态管理:多台应用机的状态检测与替换需要做到及时性,一般niginx层做故障转移
    • 服务升级:滚动升级成为可能,灰度发布
    • 日志管理:日志文件分散在各个机器,促进集中式日志平台的产生

4.3 多层代理

常见的代理工具:52528

4.4 异地访问

将相同的系统部署多份,分散到异地多个机房,或者电信、移动多个网络中
不同地点,不同网络接入的用户,有了不同的访问入口和选择

4.4 云平台

当部署数量增加时,出现了云平台,

  • 方案

    • 虚拟化:Vmware
    • 容器化:docker
    • 容器编排工具:k8s
    • 云化解决了资源的快速伸缩,但仍需要企业自备大量机器资源,推动私有云到企业云进化
  • 特点

    • 注意资源的回收,降低资源闲置和浪费,例如大促结束后要及时回收
    • 运维要求:需要运维层面的高度支撑,门槛比较高
    • 预估风险:云瘫痪的故障造成的损失不可估量

四、容器化概述

4.1 容器技术

Docker是基于google突出的golang语言开发,基于linux内核的cgroups、namespace以及union FS等技术,
作用:对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主机和其他进程,也被称为容器,使用docker就是对其创建管理删除使用

4.2 容器化与虚拟机的区别

虚拟机将操作系统和硬件都虚拟化,但是容器是将app、环境、操作系统环境依赖打包,是更轻量级的打包

虚拟机容易遭受攻击

基于hypervisor的虚机技术,在隔离性上比容器技术要更好,它们的系统硬件资源完全是虚拟化的,当一台虚机出现系统级别的问题,往往不会蔓延到同一宿主机上的其他虚机,但是容器就不一样了,容器之间共享同一个操作系统内核以及其他组件,所以在收到攻击之类的情况发生时,更容易通过底层操作系统影响到其他容器

不是所有场合都适用容器技术

不管是虚机还是容器,都是运用不同的技术,对应用本身进行了一定程度的封装和隔离,在降低应用和应用之间以及应用和环境之间的耦合性上做了很多努力,但是随机而来的,就会产生更多的网络连接转发以及数据交互,这在低并发系统上表现不会太明显,而且往往不会成为一个应用的瓶颈(可能会分散于不同的虚机或者服务器上),但是当同一虚机或者服务器下面的容器需要更高并发量支撑的时候,也就是并发问题成为应用瓶颈的时候容器会将这个问题放大,所以,并不是所有的应用场景都是适用于容器技术的。

存储方案

  • 容器并不是为0S抽象服务的,这是它和虚机最大的区别,这样的基因意味着容器天生是为应用环境做更多的努力,容器的伸缩也是基于容器的这-disposable特性,而与之相对的,需要持久化存储方案恰恰相反。
  • 这一点docker容器提供的解决方案是利用volume接口形成数据的映射和转移,以实现数据持久化的目的,但是这样同样也会造成一部分资源的浪费和更多交互的发生,不管是映射到宿主机上还是到网络磁盘,都是退而求其次的解决方案。

4.3 Docker 引擎

c/s架构

平台组成


docker核心组建,就是
(1)image镜像,构建容器(应用程序所处的环境,打包为镜像文件)
(2)container,容器(你的应用程序,就跑在容器中)
(3)镜像仓库(保存镜像文件、提供上传、下载镜像)作用好比github
(4)dockerfile

生命周期

docker 镜像

本质是基于unionFS管理的分层文件系统

4.4 Docker的应用场景

容器变得越来越重要,尤其是在云环境中,许多企业甚至在考虑将容器替代 VM 作为其应用程序和工作负的通用计算平台

  • 微服务:容器小巧轻便,非常适合微服务体系结构,在微体系结构中,应用程序可以由许多松散耦合且i巴鱼区对部署的较小服务构成。
  • DevOps:微服务作为架构和容器作为平台的结合,是许多团队将 DevOps 视为构建,交付和运行软件的的共同基础。
  • 多云:由于容器可以在笔记本电脑,本地和云环境中的任何地方连续运行,因此它们是混合云和多云方案的理想基础架构,在这种情况下,组织发现自己跨多个公共云运行与自己的数据中心结合,
  • 应用程序现代化和迁移:使应用程序现代化的最常见方法之一是将它们容器化,以便可以将它们迁移到云中。

4.5基于centos7 部署 docker

1 linux文件系统

在部署docker之前先了解一下linux的文件系统

  • 文件系统的特点
    • Linux文件系统采用树形结构,从根目录root(/)开始。
    • Linux的虚拟文件系统允许众多不同类型的文件系统共存,并支持跨文件系统的操作
    • Linux的文件是无结构字符流式文件,不考虑文件内部的逻辑结构,只把文件简单地看作是一系列字符的序列。
    • Linux的文件可由文件拥有者或超级用户设置相应的访问权限而收到保护。
    • Linux把所有的外部设备都看作文件,可以使用与文件系统相同的系统调用来读写外部设备。
  • /:是所有文件的根目录;
  • /bin:存放二进制可执行命令目录;
  • /home:用户主目录的基点目录,默认情况每个用户主目录都设在该目录下,如默认情况下用户user01的主目录是/home/user01;
  • /lib:存放标准程序设计库目录,又叫动态链接共享库目录,目录中文件类似windows里的后缀名为dll的文件;
  • /etc:存放系统管理和配置文件目录;
  • /dev:存放设备特殊文件目录,如声卡文件,磁盘文件等;
  • /usr:最庞大的目录,存放应用程序和文件目录;
  • /proc:虚拟目录,是系统内存的映射,可直接访问这个目录来获取系统信息;
  • /root:系统管理员的主目录;
  • /var:存放系统产生的经常变化文件的目录,例如打印机、邮件等假脱机目录、日志文件、格式化后的手册页以及一些应用程序的数据文件等;
  • /tmp:存放公用临时文件目录。

.2 Docker的三个基本概念:镜像、容器、仓库

  • 镜像
    镜像就是一个只读的模板,可以用来创建docker容器
  • 容器
    • docker 利用容器来运行应用,容器时从镜像创建的运行实例,可以被启动、开始停止和删除。 每个容器都是相互隔离的,保证安全的平台
    • 可以把容器看作一个简易板的linux系统,包括root用户权限,进程空间,用户空间和网络空间和运行在其中的应用程序
    • 镜像只是读的,容器在启动的时候创建一层可写层作为最上层
  • 仓库
    仓库是集中存放镜像文件的场所。有时候会把仓库和仓库注册服务器(Registry)混为一谈,并不严格区分。实际上,仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)

4.6 k8s

1docker容器问题

  • 业务器数量庞大,哪些容器部署了哪些节点,使用了哪些端口,如何记录管理
  • 跨主机通信
  • 跨主机容器之间相互调用,如何写

2 k8s功能

  1. 服务的发现与负载的均衡;
  2. 容器的自动装箱,我们也会把它叫做 scheduling,就是“调度”,把一个容器放到一个集群的某一个机器上,Kubernetes 会帮助我们去做存储的编排,让存储的声明周期与容器的生命周期能有一个连接;
  3. Kubernetes 会帮助我们去做自动化的容器的恢复。在一个集群中,经常会出现宿主机的问题或者说是 OS 的问题,导致容器本身的不可用,Kubernetes 会自动地对这些不可用的容器进行恢复;
  4. Kubernetes 会帮助我们去做应用的自动发布与应用的回滚,以及与应用相关的配置密文的管理;
  5. 对于 job 类型任务,Kubernetes 可以去做批量的执行;
  6. 为了让这个集群、这个应用更富有弹性,Kubernetes 也支持水平的伸缩。
  • 调度
    • Kubernetes 可以把用户提交的容器放到 Kubernetes 管理的集群的某一台节点上去。Kubernetes 的调度器是执行这项能力的组件,它会观察正在被调度的这个容器的大小、规格。
    • 比如说它所需要的 CPU以及它所需要的 memory,然后在集群中找一台相对比较空闲的机器来进行一次 placement,也就是一次放置的操作。在这个例子中,它可能会把红颜色的这个容器放置到第二个空闲的机器上,来完成一次调度的工作。
  • 自动修复
    • Kubernetes 有一个节点健康检查的功能,它会监测这个集群中所有的宿主机,当宿主机本身出现故障,或者软件出现故障的时候,这个节点健康检查会自动对它进行发现。

3k8s架构

Kubernetes 架构是一个比较典型的二层架构和 server-client 架构。Master 作为中央的管控节点,会去与 Node 进行一个连接。
所有 UI 的、clients、这些 user 侧的组件,只会和 Master 进行连接,把希望的状态或者想执行的命令下发给 Master,Master 会把这些命令或者状态下发给相应的节点,进行最终的执行

4 master结构

  • API Server:api-server提供了资源操作的唯一入口,提供认证、授权、访问控制、API注册等:顾名思义是用来处理 API 操作的,Kubernetes 中所有的组件都会和 API Server 进行连接,组件与组件之间一般不进行独立的连接,都依赖于 API Server 进行消息的传送;
  • Controller:责维护集群的状态,比如故障检测、资源扩展、滚动更新,它用来完成对集群状态的一些管理。比如刚刚我们提到的两个例子之中,第一个自动对容器进行修复、第二个自动进行水平扩张,都是由 Kubernetes 中的 Controller 来进行完成的;
  • Scheduler:负责资源的调度,将pod部署都相应的node机器上,“调度器”顾名思义就是完成调度的操作,就是我们刚才介绍的第一个例子中,把一个用户提交的 Container,依据它对 CPU、对 memory 请求大小,找一台合适的节点,进行放置;
  • etcd:是一个分布式的一个存储系统,API Server 中所需要的这些原信息都被放置在 etcd 中,etcd 本身是一个高可用系统,通过 etcd 保证整个 Kubernetes 的 Master 组件的高可用性。

5 node结构


Kubernetes 的 Node 是真正运行业务负载的,每个业务负载会以 Pod 的形式运行。一个 Pod 中运行的一个或者多个容器,真正去运行这些 Pod 的组件的是叫做 kubelet,也就是 Node 上最为关键的组件,它通过 API Server 接收到所需要 Pod 运行的状态,然后提交到我们下面画的这个 Container Runtime 组件中

  • 在 OS 上去创建容器所需要运行的环境,最终把容器或者 Pod 运行起来,也需要对存储跟网络进行管理。Kubernetes 并不会直接进行网络存储的操作,他们会靠 Storage Plugin 或者是网络的 Plugin 来进行操作。用户自己或者云厂商都会去写相应的 Storage Plugin 或者 Network Plugin,去完成存储操作或网络操作。
  • 在 Kubernetes 自己的环境中,也会有 Kubernetes 的 Network,它是为了提供 Service network 来进行搭网组网的。真正完成 service 组网的组件的是 Kube-proxy,它是利用了 iptable 的能力来进行组建 Kubernetes 的 Network,就是 cluster network,以上就是 Node 上面的四个组件。
  • Kubernetes 的 Node 并不会直接和 user 进行 interaction,它的 interaction 只会通过 Master。而 User 是通过 Master 向节点下发这些信息的。Kubernetes 每个 Node 上,都会运行我们刚才提到的这几个组件。

举例-协调来完成一次Pod的调度执行操作

  • 用户可以通过 UI 或者 CLI 提交一个 Pod 给 Kubernetes 进行部署
  • 这个 Pod 请求首先会通过 CLI 或者 UI 提交给 Kubernetes API Server
  • 下一步 API Server 会把这个信息写入到它的存储系统 etcd
  • 之后 Scheduler 会通过 API Server 的 watch 或者叫做 notification 机制得到这个信息:有一个 Pod 需要被调度
  • 这个时候 Scheduler 会根据它的内存状态进行一次调度决策,在完成这次调度之后,它会向 API Server report 说:“OK!这个 Pod 需要被调度到某一个节点上
  • API Server 接收到这次操作之后,会把这次的结果再次写到 etcd 中,然后 API Server 会通知相应的节点进行这次 Pod 真正的执行启动
  • 相应节点的 kubelet 会得到这个通知,kubelet 就会去调 Container runtime 来真正去启动配置这个容器和这个容器的运行环境,去调度 Storage Plugin 来去配置存储,network Plugin 去配置网络

4.7 k8s 核心概念

Pod

  • 是 Kubernetes 的一个最小调度以及资源单元。
    -定义容器运行的方式,用户可以通过 Kubernetes 的 Pod API 生产一个 Pod,让 Kubernetes 对这个 Pod 进行调度,也就是把它放在某一个 Kubernetes 管理的节点上运行起来
  • 一个 Pod 简单来说是对一组容器的抽象,它里面会包含一个或多个容器

Volume

  • 声明在pod中的容器可以访问的文件目录:Volume 就是卷的概念,它是用来管理 Kubernetes 存储的,是用来声明在 Pod 中的容器可以访问文件目录的,一个卷可以被挂载在 Pod 中一个或者多个容器的指定路径下面
  • 可以被挂载在pod中一个或多个容器的制定路径下
  • 支持多种后端存储的抽象:本地存储,分布式存储,云存储

Deployment

  • Deployment 是在 Pod 这个抽象上更为上层的一个抽象,它可以定义一组 Pod 的副本数目、以及这个 Pod 的版本。一般大家用 Deployment 这个抽象来做应用的真正的管理,而 Pod 是组成 Deployment 最小的单元
    • 定义一组pod的副本数目,版本等
    • 通过控制器维持pod的数目
      • 自动恢复失败的pod
    • 通过控制器以指定的策略控制版本:滚动升级、重新生成、回滚等

service

Service 提供了一个或者多个 Pod 实例的稳定访问地址

  • 比如在上面的例子中,我们看到:一个 Deployment 可能有两个甚至更多个完全相同的 Pod。对于一个外部的用户来讲,访问哪个 Pod 其实都是一样的,所以它希望做一次负载均衡,在做负载均衡的同时,我只想访问某一个固定的 VIP,也就是 Virtual IP 地址,而不希望得知每一个具体的 Pod 的 IP 地址。
  • 这个 pod 本身可能 terminal go(终止),如果一个 Pod 失败了,可能会换成另外一个新的
  • 对一个外部用户来讲,提供了多个具体的 Pod 地址,这个用户要不停地去更新 Pod 地址,当这个 Pod 再失败重启之后,我们希望有一个抽象,把所有 Pod 的访问能力抽象成一个第三方的一个 IP 地址,实现这个的 Kubernetes 的抽象就叫 Service
  • 实现 Service 有多种方式,Kubernetes 支持 Cluster IP,上面我们讲过的 kuber-proxy 的组网,它也支持 nodePort、 LoadBalancer 等其他的一些访问的能力

namespace

  • 一个集群内部的逻辑隔离机制
  • 每个资源都属于一个namespace
  • 同一个namespace中的资源命名唯一
  • 不同的namespace中的资源可重名

k8s中的api

Kubernetes API 是由 HTTP+JSON 组成的:用户访问的方式是 HTTP,访问的 API 中 content 的内容是 JSON 格式的

五、Zookeeper

持续更新中…

5.1 是什么

  • 提供开源的分布式配置服务,同步服务和命名注册
  • 主要解决分布式应用中常遇到的数据管理问题
  • 本质是小型的文件存储系统+监听机制
  • 一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式
    协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能

5.2 数据模型

本身是一个树形目录结构

5.3 zookeeper应用场景

5.4 zookeeper选举策略

5.5 zookeeper集群、ZAB协议

六、全链路压测

背景

最早是阿里提出来的,天猫双十一…

QPS等概念

  • QPS:Queries Per Second意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数
  • TPS:是TransactionsPerSecond的缩写,也就是事务数/秒。它是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。
  • RT(Response-time):响应时间:执行一个请求从开始到最后收到响应数据所花费的总体时间,即从客户端发起请求到收到服务器响应结果的时间
  • 并发数是指系统同时能处理的请求数量,这个也是反应了系统的负载能力。
  • 系统的吞吐量(承压能力)与request对CPU的消耗、外部接口、IO等等紧密关联。单个request 对CPU消耗越高,外部系统接口、IO速度越慢,系统吞吐能力越低,反之越高。系统吞吐量几个重要参数:QPS(TPS)、并发数、响应时间。
    • QPS(TPS):(Query Per Second)每秒钟request/事务 数量
    • 并发数:系统同时处理的request/事务数
    • 响应时间:一般取平均响应时间
      QPS = 并发数/平均响应时间
  • 实际举例
    1. 如果每天80%的访问集中在20%的时间里,那么这20%的时间就叫做峰值时间
    2. 公式:峰值时间每秒请求数QPS=总PV数0.8 / 每天秒数0.2
    3. 每天300w PV 的在单台机器上,这台机器需要多少QPS?
      • ( 3000000 * 0.8 ) / (86400 * 0.2 ) = 139 (QPS)
    4. 如果一台机器的QPS是58,需要几台机器来支持?
      • 139 / 58 = 3

最佳线程数

  1. 单线程QPS公式:QPS=1000ms/RT
    • 对同一个系统而言,支持的线程数越多,QPS越高。假设一个RT是80ms,则可以很容易的计算出QPS,QPS = 1000/80 = 12.5
  2. 多线程场景,如果把服务端的线程数提升到2,那么整个系统的QPS则为 2*(1000/80) = 2
  3. 实际的QPS、RT关系如下

    最佳线程数量,刚好消耗完服务器的瓶颈资源的临界线程数,公式如下

最佳线程数量=((线程等待时间+线程cpu时间)/线程cpu时间)* cpu数量

6.1 什么是全链路压测?

基于实际的生产业务场景和系统环境,模拟海量的用户请求和数据,对整个业务链路进行各种场景的测试验证,持续发现并进行瓶颈调优,保障系统稳定性的一个技术工程。

  • 全链路压测解决了什么问题?

针对业务场景越发复杂化、海量数据冲击,发现并解决整个业务系统的可用性、扩展性以及容错性的过程。

  • 全链路压测创造了什么价值?
    技术角度:降低成本、提高服务可用性、技术练兵&团队协作&快速响应;
    业务角度:提升用户体验、技术更好的服务业务、创造更多业务价值。
  1. 保证系统稳定性:可能提前预估系统存在的各种问题,提前拟高并发场景,有备无患。
  2. 请求链路追踪,故障快速定位:可以通过调用链结合业务日志快速定位错误信息。
  3. 精准的容量评估:能够定位到最需要扩容的服务,帮助公司用最低的成本满足业务的性能要求真实的性能验证:能够在生成环境以最真实的环境来验证系统的真实性能。
  4. 数据分析,优化链路:可以得到用户的行为路径,汇总分析应用在很多业务场景。

6.2 与传统方式的对比

压测类型传统压测全链路压测
压测方式Jmeter、Locust、Loadrunner压测集群、流量引擎、录制回放
承接方式需求响应式,被动发现系统所有链路存在的瓶颈点,主动
投入成本需要搭建单独的压测环境完全线上生产环境进行,无须单独搭建环境
压测环境测试环境/性能环境生产环境
压测场景单机单接口、单机单链路、单机混合链路包含覆盖范围内的所有核心链路及场景
压测过程可观测性较低,延时较高实时可视化观测

6.3 如何展开全链路压测

业务模型梳理

  1. 首先应该将核心业务和非核心业务进行拆分,确认流量高峰针对的是哪些业务
  2. 梳理出对外的接口:使用MOCK(模拟)方式做挡板。千万不要污染正常数据:认真梳理数据处理的每一个环节

数据模型构建

  1. 数据的真实性和可用性:可以从生产环境完全移植一份当量的数据包,作为压据,通过分析历史数据增长趋势,预估当前可能的数据量
  2. 数据隔离:千万千万不要污染正常数据:认真梳理数据处理的每一个环节,可落入影子库,mock 对象等手段,来防止数据污染

压测工具选型

使用分布式压测的手段来进行用户请求模拟,目前有很多的开源工具可以提供IMeter、nGrinder、Locust等。

6.4 全链路整体架构

核心技术

  • 流量染色
    1. 染色标志穿透微服务
    2. tomcat线程池复用问题
    3. 染色标志穿透线程池
  • 接口mock
  • 数据库隔离
    1. 影子表:比如在表中加入一个字段,0代表是压测数据,1代表正常数据
    2. 影子库:放到不同的库下
  • 消息队列隔离
    1. 当生产消息扔到MQ中,接着让消费者消费,这个没有问题,压测的数据不能够直接扔到MQ中
  • redis隔离
    1. 通过key值来区分,压测流量的key值加统一后缀,通过改造RedisTemplate来实现key路由
  1. rabbitmg隔离
  2. 全链路服务监控
  3. 全链路日志隔离:生产日志和压测日志隔离
  4. 全链路风险熔断

涉及的业务问题

  1. 涉及的系统太多,牵扯的开发人员太多
  2. 模拟的测试数据和访问流量不真实
  3. 压测生产数据未隔离,影响生产环境

框架实现

流量染色方案

流量识别
  1. 全链路压测发起的都是HTTP请求,只需要请求头上添加统一的压测请求头
  2. 要想压测的流量和数据不影响线上真实的生产数据,就需要线上的集群能够识别出压测数据,只要能识别压测请求的流量,那么流量触发的读写操作就很好的统一去做隔离了
  3. 通过在请求协议中添加压测请求的标识,在不同服务的相互调用时,一路透传下去,这样每一个服务都能识别处压测的请求流量,这样做的好处就是与业务完全的解耦,只需要应用框架进行感知,对业务方代码无侵入

那么服务怎么识别这个请求的标识呢

tomcat线程池复用问题

tomcat默认使用线程池来管理线程,一个请求过来,如果线程池里面有空闲的线程,那么会在线程池里面取个线程来处理该请求,一旦该线程当前在处理请求,其他请求就不会被分配到该线程上,直到该请求处理完成。请求处理完成后,会将该线程重新加入线程池,因为是通过线程池复用线程,就会如果线程内部的ThreadLocal没有清除就会出现问题,需要新的请求进来的时候,清除ThreadLocal

fegin传递染色标识

使用fegin来实现远程调用,跨微服务传染色体标识是通过MVC拦截器获取到请求header的染色体标识,并放进Threadlocal中,然后交给Fegin拦截器在发送请求之前从Threadlocal中获取到染色体标识,并放进Fegin构建请求的header中,实现微服务之间的火炬传递

Hystrix传递染色体标识

Hystrix隔离技术主要有两个

  • 信号量:信号量的资源隔离只起到一个开关的作用,比如服务A的信号量大小为10,那么就说它同时只允许有10个tomcat线程来访问服务A,其它请求都会等待,从而达到资源隔离和限流保护的作用
  • 线程池
    线程池隔离技术,用hystrix自己的线程去执行调用,而信号量隔离技术,是直接让tomcat线程去调用依赖服务,信号量隔离,只是一道管卡,信号量有多少,就允许多少个tomcat线程通过它,然后去执行

RabbitMQ 数据隔离

七、RPC

7.1 基础

定义&特点

RPC,remote procedure call,远程过程调用,它定义了一台机器上的程序去调用另一台机器上子程序的这一行为
特点:

  • 把远程实现搬到了本地,效果上远程调用和本地调用没有差别
  • 使用cs模式,客户端发起请求,服务端接收请求参数后执行
  • 屏蔽跨进程跨网络调用底层复杂性让我们更专注于业务逻辑

具体实现框架

  1. dubbo(apache alibaba java)
  2. motan(微博)
  3. tars(腾讯内部)
  4. grpc
  5. thrift
  6. spring cloud openfeign

应用场景

跨网络通信都可以用

7. 2. RPC的关键技术点&一次调用rpc流程

1 RPC流程


  1. 客户端调接口走到代理类,组装请求并序列化,然后协议编码并发送
  2. 服务端收到请求,进行协议解析以及反序列化拿到请求参数
  3. 服务端根据请求参数调用接口实现,然后组装响应
  4. 响应按同样的方式返回
两个网络模块如何连接的呢?

注册中心就是一个存数据的地方,最好可以提供监听功能。注册中心与rpc框架是分开的
常见的注册中心:zookeeper、nacos、etcd

其它特性

  • 路由筛选可用的提供者
  • 负载均衡:从可用的提供者种,选择具体用哪个
  • 熔断限流:流量控制
  • 网络处理
  • 协议处理
RPC优势
  1. 让构建分布式应用更容易,解耦服务,容易扩展
  2. RPC一般使用长连接,不必每次通信都要建立连接,减少网络开销
  3. RPC需要有注册中心,可以动态感知服务变化并可视化
  4. 丰富的后台管理功能,可统一管理接口服务,对调用方来说是无感知的,统一化的操作
  5. 协议精简,效率更高,私密安全性高
  6. 具有负载均衡,熔断限流等功能

7.3 序列化技术

  • 任何一种序列化框架:核心思想就是设计一种序列化协议将对象的类型、属性类型属性值–按照固定的格式写到二进制字节流中来完成序列化,再按照固定的格式一一读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化

序列化方式

  • JDK原生序列化
  • 轻量级文本数据交换格式-json/XML
    • 可读性好,方便阅读和调试,多语言支持序列化以后的字节文件相对较大,效率相对不高,但对比XML序列化后的字节流更小,在企业运用普遍,特别是对前端和三方提供api。
  • Hessian是一个动态类型,二进制,并支持跨语言的徐丽华框架
    • Hessian 性能上要比JDK、JSON 序列化高效很多,并且生成的字节数也更小。有非常好的兼容性和稳定性所以Hessian 更加适合作为 RPC框架远程通信的序列化协议
  • protobuf
    • Google 推出的开源序列库,它是一种轻便,高效的结构化数据存储格式,多语言支持。
    • 速度快,压缩比高,体积小,序列化后体积相比JSON、Hessian 小很多课彩盘格式的扩展、升级和兼容性都不错,可以做到向后兼容

PRC如何选择序列化框架

  • 选型因素
    • 安全性: 首要考虑,如果序列化存在安全漏洞,那么线上的服务就很可能被入侵(JDK原生序列化存在漏洞
    • 兼容性: 序列化协议在版本升级后的兼容性是否很好,是否是跨平台、跨语言等
    • 通用性:能够对任意类型进行序列化和反序列化,不会出现服务接口方法加一个某种类型的参数后服务器突然不能调用
    • 性能、效率:序列化与反序列化过程是 RPC调用的一个必须过程,性能和效率势必将直接关系到 RPC 框架整体的性能和效率
    • 空间开销:序列化之后的二进制数据的体积大小,序列化后的字节数据体积越小,网络传输的数据量就越小,传输的数据的速度也就越快,在RPC调用中直接关系到请求响应的耗时

考虑因素

  1. 避免对象构造得过于复杂,属性很多,并且存在多层的嵌套
  2. 避免对象过于庞大:大字符串,超大数组等
  3. 避免使用序列化框架不支持的类型作为参数传递
  4. 防止对象有复杂的继承关系

7.4 应用层的通信协议-http

基础概念

大多数RPC大多自研http,也支持合http1.1

什么是IO

IO就是计算机内部与外部设备之间拷贝数据的过程
网络数据到来后先存储到操作系统的内核缓存区,在等待应用程序收走

边缘触发

使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完。

水平触发

使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取。

事件驱动 IO

发起读请求后,等待读就绪事件通知再进行数据读取。

异步 IO

发起读请求后,等待操作系统读取完成后通知,完全将功能交给操作系统实现。

操作系统的IO模型有哪些

同步阻塞IO
同步非阻塞IO
IO多路复用
信号驱动IO
异步IO

同步阻塞IO

read 的第一个阶段阻塞的,这就是我们常说的阻塞式 IO,即如果read 第一个阶段等待读就绪是阻塞的,我们就称为阻塞式IO

listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  int n = read(connfd, buf);  // 阻塞 读数据
  doSomeThing(buf);  // 处理数据
  close(connfd);     // 关闭连接
}

非阻塞式 IO

为了让上面操作中的读操作 read 不再主线程中阻塞,我们可以使用多线程实现非阻塞

listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  newThreadDeal(connfd)       // 当有新连接建立时创建一个新线程处理连接
}
newThreadDeal(connfd){
  int n = read(connfd, buf);  // 阻塞 读数据
  doSomeThing(buf);  // 处理数据
  close(connfd);     // 关闭连接 
}

非阻塞IO就是将第一阶段的等待读就绪改为非阻塞,但第二阶段的数据读取还是阻塞的

IO多路复用

  • 上面服务端通过多线程的方式处理客户端请求实现了主线程的非阻塞,使用不同线程处理不同的连接请求,但是我们并没有那么多的线程资源,并且等待读就绪的过程是耗时最多的,那么有没有什么办法可以将连接保存起来,等读已就绪时我们再进行处理。
  • 这样实现
arr = new Arr[];
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  arr.add(connfd);
}

// 异步线程检测 连接是否可读
new Tread(){
  for(connfd : arr){
    // 还有一个弊端:可读 connfd 只能串行处理
    // 获取直接开多线程处理连接 但线程资源有限
    int n = read(connfd, buf);  // 检测 connfd 是否可读
    if(n != -1){
       newThreadDeal(buf);   // 创建新线程处理
       close(connfd);        // 关闭连接 
       arr.remove(connfd);   // 移除已处理的连接
    }
  }
}

newTheadDeal(buf){
  doSomeThing(buf);  // 处理数据
}
  • 上面的实现看着很不错,但是却存在一个很大的问题,我们需要不断的调用 read() 进行系统调用,这里的系统调用我们可以理解为分布式系统的 RPC 调用,性能损耗十分严重,因为这依然是用户层的一些小把戏
  • 这时我们自然而然就会想到把上述循环检测连接(文件描述符)可读的过程交给操作系统去做,从而避免频繁的进行系统调用。当然操作系统给我们提供了这样的函数:select、poll、epoll
select

select 是操作系统提供的系统函数,通过它我们可以将文件描述符发送给系统,让系统内核帮我们遍历检测是否可读,并告诉我们进行读取数据。

arr = new Arr[];
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  arr.add(connfd);
}

// 异步线程检测 通过 select 判断是否有连接可读
new Tread(){
  while(select(arr) > 0){
    for(connfd : arr){
      if(connfd can read){
        // 如果套接字可读 创建新线程处理
        newTheadDeal(connfd);
        arr.remove(connfd);   // 移除已处理的连接
      }
    }
  }
}

newTheadDeal(connfd){
    int n = read(connfd, buf);  // 阻塞读取数据
    doSomeThing(buf);  // 处理数据
    close(connfd);        // 关闭连接 
}

  • 减少大量系统调用但也存在一些问题
    • 每次调用需要在用户态和内核态之间拷贝文件描述符数组,在高并发场景下这个拷贝的消耗是很大的。
    • 内核检测文件描述符可读还是通过遍历实现,当文件描述符数组很长时,遍历操作耗时也很长。
    • 内核检测完文件描述符数组后,当存在可读的文件描述符数组时,用户态需要再遍历检测一遍。
poll
  • poll 和 select 原理基本一致,最大的区别是去掉了最大 1024 个文件描述符的限制。
  • select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
  • poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
epoll
  • 每次调用需要在用户态和内核态之间拷贝文件描述符数组,但高并发场景下这个拷贝的消耗是很大的。
    方案:内核中保存一份文件描述符,无需用户每次传入,而是仅同步修改部分。
  • 内核检测文件描述符可读还是通过遍历实现,当文件描述符数组很长时,遍历操作耗时也很长。
    方案:通过事件唤醒机制唤醒替代遍历。
  • 内核检测完文件描述符数组后,当存在可读的文件描述符数组时,用户态需要再遍历检测一遍。
    方案:仅将可读部分文件描述符同步给用户态,不需要用户态再次遍历。

epoll 基于高效的红黑树结构,提供了三个核心操作,主要流程如下所示:

listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
int epfd = epoll_create(...); // 创建 epoll 对象
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  epoll_ctl(connfd, ...);  // 将新连接加入到 epoll 对象
}

// 异步线程检测 通过 epoll_wait 阻塞获取可读的套接字
new Tread(){
  while(arr = epoll_wait()){
    for(connfd : arr){
        // 仅返回可读套接字
        newTheadDeal(connfd);
    }
  }
}

newTheadDeal(connfd){
    int n = read(connfd, buf);  // 阻塞读取数据
    doSomeThing(buf);  // 处理数据
    close(connfd);        // 关闭连接 
}

IO总结

  1. IO 分为等待读就绪和读取数据两个阶段,阻塞和非阻塞指的是等待读就绪阶段
  2. IO 模型发展从阻塞 read 函数开始,它整个过程都是阻塞的,为了解决这个问题**,我们在用户态通过异步线程实现主线程的非阻塞**,但是子线程的 read 过程还是阻塞的,但是线程资源是有限的,且等待读就绪的过程是耗时最多的环节,因此我们在一个线程内通过 while 和非阻塞 read 的能力避免等待读就绪过程中线程资源占用,后来操作系统发现这个场景比较多,便提供了 select、epoll、epoll 函数实现上述功能来减少系统调用。我们会发现,包括后面的异步 IO 我们其实是把更多的功能交给了操作系统实现
  3. 比如大家常说的 IO 多路复用效率之所以高是因为可以通过一个线程管理多个文件描述符,当然这也是其中一个原因,另外一个原因是因为减少了大量的系统调用

线程模型

reactor:reactor单线程,reactor多线程,主从reactor多线程

7.5 动态代理

  • 代理的作用
    • RPC框架会自动给接口生成一个代理类,当我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理类
    • 接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样我们就可以在生成的代理类里,加入远程调用逻辑

举例
IDK Proxy

其它动态代理方案

  1. ASM:可直接对字节码文件进行修改,也可生成字节码,开发者要掌握字节码结构以及更为底层的指令https://asm.ow2.io/
  2. cglib:基于ASM,Spring AOP也可以使用它,其原理是动态生成一个要代理类的子类github:https://github/cglib/cglib
  3. bytebuddy:也是基于 ASM API实现的,是一个较高层级的抽象的字节码操作工具,通过使用Byte Buddy,任何熟悉Java 编程语言的人都有望非常容易地进行字节码操作站点:https://bytebuddy/#/
  4. lavassist:增强字节码时直接使用iava编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类github:https://github/jboss-javassist/javassist

7.6 基于ZK注册的原理

注册中心满足什么条件

  • 存储介质
  • 变更通知

zookeeper介绍:

  • 本质是一个树形目录服务,非常类似于标准文件系统
  • 路径名+节点构成唯一标识
  • 节点中也可以存储数据
  • watch机制可监听节点的变更

基于zk注册数据的存储结构-以dubbo为例

7.7 容错策略之超时重试

什么是超时重试?

其实就是容错策略,超时后重新请求其它提供者,做到故障转移

如何检测请求是否超时?

  • 方案一
    • 每隔1s检测一下
    • 浪费CPU资源
  • 方案二
    • 基于时间论算法,核心是将任务放到对应的时间槽位

时间轮算法

在时钟轮机制中,有时间槽,每个槽位代表一个时间单位,时间指针按照时间单位走动,将任务放到对应的时间槽位上,当时间指针走到该槽位时就可以拿出来执行

实现的方式多样,比如使用数组实现

  1. 每个任务会按要求只扫描执行一次能很好的解决CPU浪费的问题
  2. 秒级轮,分钟轮,小时轮

7.8 熔断限流

熔断降级的概念

消费者端:业务代码→PRC框架
→提供者端:PRC框架→业务代码
但是如果调用提供者端的业务代码失败了,那么提供者的业务代码不执行返回,这就是降级

  • 降级:正常返回,备选结构正常但不一定正确
  • 熔断:对目标有一个冷静期,冷静期内不对目标发起直接调用

如何判断熔断

基于服务的可用率/失败率

熔断工作合适开始?又合适结束

有了熔断降级为何还要限流?

  • 实际生产环境中,服务发生的一系列问题很大可能是由于访问量过大而引起的,这就需要业务提供方能够进行自我保护,从而保证在高访问量、高并发场景下,依然能能够稳定运行
  • 限流的作用是用来限制其请求的速率,保护后相应服务,以免服务过载导致服务不可用现象出现。

常见的限流算法

  • 令牌桶算法
  • 漏斗算法
  • 滑动窗口算法

7.9 实现一个自己定义的RPC框架-mini版

1 PRC框架

2 Netty

Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目Netty提供非阻塞的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序

3Dubbo

  • Apache Dubbo是一款高性能的Java RPC框架。其前身是阿里巴巴公司开源的一个高性能、轻量级的开源Java RPC框架,可以和Spring框架无缝集成。
  • Dubbo提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • Dubbo作为一个RPC框架,其最核心的功能就是要实现跨网络的远程调用。一个作为服务的提供方,一个作为服务的消费方。通过Dubbo来实现服务消费方远程调用服务提供方的方法。

未完待续…

本文标签: 互联网读懂架构