总有刁民想害朕

总有刁民想害朕 查看完整档案

苏州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

总有刁民想害朕 发布了文章 · 2018-12-29

【从零开始学架构】学习笔记(一)

1.1 什么是架构

1.1.1 架构简述

clipboard.png

【优秀架构具备的特点】

  • 优秀的 TPS 承载力
  • 优秀的性能
  • 故障影响降到最小
  • 投入产出最优方案

1.1.2 架构师职责

  • 明确需求
  • 系统能力分解
  • 技术选型
  • 制定架构说明书及主导执行落地

1.2 架构设计分层

1.2.1 为什么要分层

  • 分而治之
  • 各司其职
  • 有条不紊的结合

1.2.2 常见的分层设计

  • 计算机网络 OSI 七层模型
  • Web 系统 MVC 模型分层
  • 基于领域模型的分层

1.2.3 分层模型演进

一、Servlet JSP 时代(V0.1)

  1. Servlet + Tomcat 容器完成 Web 接入
  2. 使用 JavaBean + JDBC 完成数据层接入
  3. 使用 JSP 完成页面展示

二、MVC(V1.0)
clipboard.png

【V 1.0 时代 典型代表 SSH】
clipboard.png

  1. Structs 解决接入及表示层。(ActionServlet 重)
  2. Spring 解决业务服务、事务处理、会话管理。
  3. Hibernate 解决数据存储接入问题。(特殊的SQL处理繁琐;SET 联动数据库问题)

三、SSM 时代(V1.5)
clipboard.png

  1. SpringMVC 解决接入及表示层
  2. Spring 解决业务服务、事务处理、会话管理等问题
  3. MyBatis 解决数据接入层

四、SpringBoot all in one(V2.0)
clipboard.png

  1. 整合了所有 Spring 的框架功能
  2. 提供了简单的配置及注解的接入方式
  3. 提供 All in one 的服务

【V2.0 存在的问题】:

  • 解决了单一应用内的软件分层,却没有解决整体应用的分层
  • 单一应用的性能瓶颈,无法支撑亿级流量
  • 团队协作问题

五、分布式分层(V3.0)
clipboard.png

1、WEB概念层
clipboard.png

2、业务概念层
clipboard.png

3、数据访问记存储层
clipboard.png

查看原文

赞 2 收藏 1 评论 0

总有刁民想害朕 发布了文章 · 2018-12-28

【分布式服务架构原理、设计与实战】读书笔记 (一)分布式微服务架构设计原理

1.1 从传统单体架构到服务化架构

1.1.1 JEE 架构

JEE 将企业级软件架构分为三个层级:

  • Web 层:负责与用户交互或者对外提供接口。
  • 业务逻辑层:为实现业务逻辑而设计的流程处理和计算处理模块。
  • 数据存取层:将业务逻辑层处理的结果持久化以待后续查询,并维护领域模型中对象的生命周期。

JEE 平台将不同的模块化组件聚合后运行在通用的应用服务器上,例如:WebLogic、WebSphere、JBoss、Tomcat

图1-1 JEE 时代的典型架构
clipboard.png

1.1.2 SSH 架构

  • Struts、Spring、Hibernate
  • 视图、模型、控制器(Struts MVC)

图1-3 MVC 模型
clipboard.png

图1-4 SSH 时代的架构
clipboard.png

1.1.3 服务化架构

从 JEE 到 SSH ,服务的特点仍然是单体化、服务的粒度抽象为模块化组件,所有组件耦合在一个项目中,并配置和运行在一个 JVM 进程中。
为解决上述问题,SOA 出现了

  • SOA 代表面向服务的架构,俗称服务化。
  • SOA 将应用程序的模块化组件通过【定义明确的接口和契约】联系起来,接口采用中立的方式进行定义,独立于某种语言、硬件和操作系统。
  • SOA 通常通过网络通信来完成,但不局限于某种网络协议,可以是底层的 TCP/IP,也可以是应用层的 HTTP,也可以是消息队列协议,甚至可以是约定的某种数据库存储形式。

SOA 服务化的发展

一、Web Service
SOA 服务化的一种实现方式,使得运行在不同机器及操作系统上的服务互相发现和调用成为可能,并可以通过某种协议交换数据。
图1-5 Web Service 的工作原理图
clipboard.png

  • 服务提供者 Web Service 2 和 Web Service 3 通过 UDDI 协议将服务注册到 Web Service 目录服务中。
  • 服务消费者 Web Service 1 通过 UDDI 协议从 Web Service 目录中查询服务,并获得服务的 WSDL 服务描述文件。
  • 服务消费者 Web Service 1 通过 WSDL 语言远程调用和消费 Web Service 2 和 Web Service 3 提供的服务。

二、ESB
ESB 是企业服务总线的简称,用户设计和实现网络化服务交互和通信的软件模型,是 SOA 的另外一种实现方式,主要用于企业信息化系统的集成服务场景中。
ESB 也适用于事件处理、数据转换、映射、消息和事件异步队列顺序处理、安全和异常处理、协议转换、保证通信服务的质量等场景。
图1-6 ESB 架构图
clipboard.png
ESB 服务密友中心化的服务节点,每个服务提供者都是通过总线的模式插入系统,总线根据流程的编排负责将服务的输出进行转换并发送给流程要求的下一个服务进行处理。

ESB 的核心在于企业服务总线的功能和职责

  • 监控和控制服务之间的消息路由。
  • 控制可拔插的服务化的功能和版本。
  • 解析服务之间交互和通信的内容和格式。
  • 通过组合服务、资源和消息处理器来统一编排业务需要的信息处理流程。
  • 使用冗余来提供服务的备份能力。

1.2 从服务化到微服务

图1-7 微服务架构
clipboard.png

1.3 微服务架构的核心要点和实现原理

1.3.1 微服务架构中职能团队划分

【康威定律】

1.3.2 微服务的去中心化治理

1.3.3 微服务的交互模式

1、读者容错模式(Tolerant Reader)

微服务中服务提供者和消费者之间如何对接口的改变进行容错。

在服务消费者处理服务提供者返回的消息过程中,需要对服务返回的消息进行过滤,提取自己需要的内容,对多余或位置内容抛弃,不是硬生生的抛错。

推荐宽松的校验策略,只有无法识别信息,继续处理流程时,才能抛错。

2、消费者驱动契约模式

消费者驱动契约模式用来定义服务化中服务之间交互接口改变的最佳规则。
服务契约分为:提供者契约、消费者契约、消费者驱动契约,它从期望与约束的角度描述了服务提供者与消费者之间的联动关系。

  • 提供者契约:以提供者为中心,消费者无条件遵守
  • 消费者契约:对某个消费者的需求进行更为精确的描述,可以用来标识现有的提供者契约,也可以用来发现一个尚未明确的提供者契约。
  • 消费者驱动的契约:代表服务提供者向其所有当前消费者承诺遵守的约束。一旦各消费者把具体的期望告知提供者,则提供者无论在什么时间和场景下,都不应该打破契约。

图1-10 服务之间的交互需要使用的三种服务契约
clipboard.png

3、去数据共享模式

1.3.4 微服务的分解和组合模式

1、服务代理模式

根据业务的需求选择调用后端的某个服务,在返回给使用端前,可以对后端服务的输出进行加工,也可以直接把后端服务的返回结果返回给使用端。
图1-12 服务代理模式
clipboard.png

【典型案例:平滑的系统迁移】

  1. 在新老系统上双写。
  2. 迁移双写之前的历史遗留数据。
  3. 将读请求切换到新系统『服务代理模式』
  4. 下调双写逻辑,只写新系统。

第 3 步,一般只会对读请求切换设计一个开关,开关打开时查询新系统,开关关闭时查询老系统。

图1-13 迁移案例中开关的逻辑
clipboard.png

2、聚合服务模式模式

最常用的服务组合模式,根据业务流程处理的需要,以一定的顺序调用依赖的多个微服务,对依赖的微服务返回的数据进行组合、加工和转换,返回给使用方。
图1-14 服务聚合模式的架构
clipboard.png

3、服务串联模式

类似工作流,最前面的服务1负责接收请求和相应使用方,串联服务后再与服务1交互,随后服务 1与服务2交互,最后,从服务2产生的结果经过服务1和串联服务逐个处理后返回给使用方。

图1-17 服务串联模式的架构
clipboard.png

● 使用 RESTful 风格的远程调用实现;
● 采用同步调用模式,在串联服务没有完成返回之前,所有服务都会阻塞和等待;
● 一个请求会占用一个线程来处理;
● 不建议层级太多,如果能用服务聚合模式,优先使用服务聚合模式;
● 串联链路上增加节点,只要不是在正后方,串联服务无感知

图1-18 在串联服务中调用链的最后增加无感知的架构
clipboard.png

图1-19 服务串联模式案例的架构图
clipboard.png

4、服务分支模式

● 服务分支模式是服务代理模式、服务聚合模式和服务串联模式相结合的产物。
● 分支服务可以拥有自己的存储,调用多个后端的服务或者服务串联链,然后将结果进行组合处理再返回给客户端。
● 分支服务也可以使用代理模式,简单地调用后端的某个服务或者服务链,然后将返回的数值直接返回给使用方。

图1-20 服务分支模式的架构图
clipboard.png

调用链上有多个层次重复调用某基础服务,导致基础服务挂掉时影响的流量有累加效果:
假设基础服务资源池中的机器个数为 i,一次挂掉的机器个数为 j,一个调用链中调用 x 次基础服务,那么正确处理的流量的计算公式为:

   成功率 =  ((i-j) / i) x方

分支模式放大了服务的依赖关系,在现实设计中尽量保持服务调用级别的简单,在使用服务组合和服务代理模式时,不要使用服务串联模式和服务分支模式,以保持服务依赖关系的清晰明了。

5、服务异步消息模式

核心的系统服务使用同步调用,核心链路以外的服务可以使用异步消息队列进行异步化。

图1-20 服务异步消息模式的架构
clipboard.png

5、服务共享数据模式

其实是反模式

图1-25 服务共享数据模式
clipboard.png

【在下面两种场景中,仍然需要数据共享模式】:

一、单元化架构

对性能要求高。
图1-26 单元化架构的示意图
clipboard.png

二、遗留的整体服务

在重构微服务的过程中,发现单体服务依赖的数据库表耦合在一起,对其拆分需要进行反规范化的处理,可能造成数据一致性问题。

1.3.4 微服务的容错模式

1、舱壁隔离模式
1)微服务容器分组
将微服务的每个节点服务池分为三组:

  • 准生产环境;
  • 灰度环境;
  • 生产环境。

图1-27 服务分组
clipboard.png

2)线程池隔离
图1-28 线程池隔离
clipboard.png

2、熔断模式

  • 用电路保险开关来比如熔断模式。
  • 对微服务系统,当服务的输入负载迅速增加,如果没有有效的措施对负载进行熔断,则会使服务迅速压垮。

图1-29 熔断模式
clipboard.png

3、限流模式
【有如下几种主流的方法实现限流】:
1)计数器

2)令牌桶
图1-32 令牌桶结构
clipboard.png

3)信号量

类似于生活中的漏洞
示例:
public class SemaphoreExample {
    private ExecutorService exec = Executors.newCachedThreadPool();
        public static void main(String[] args) {
            final Semaphore sem = new Semaphore(5);
            for (int index = 0; index < 20;  index++) {
                Runnable run = new Runnable() {
                    public void run() {
                    try {
                        // 获得许可
                        sem.acquire();
                        // 同时只有 5 个请求可以到达这里
                        Thread.sleep((long) (Math.random()));
                        // 释放许可
                        sem.release()
                        System.out.println("剩余许可:" + sem.availablePermits());
                    } catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            exec.execute(run);
        }
        exec.shutdown();
    }
}

4、实现转移模式
【如果微服务架构发生了熔断和限流,则该如何处理被拒绝的请求】

  • 采用【快速失败】,直接返回使用方错误
  • 是否有【备份】,如果有,则迅速切换到备份服务
  • 【failover策略】,采用重试的方法来解决,这种方法要求服务提供者的服务实现幂等性

1.4 Java 平台微服务架构的项目组织形式

1.4.1 微服务项目的依赖关系

在微服务领域,jar 包被分为:

  • 一方库:本服务在 JVM 进程内依赖的 Jar 包。
  • 二房库:在服务外通过网络通信或 RPC 调用的服务的 Jar 包。
  • 三方库:所依赖的其他公司或者组织提

1.4.2 微服务项目的层级结构

Java 微服务项目一般分为:服务导出层、接口层、逻辑实现层
clipboard.png

  • 服务导出层 : 最后会打包成一个 War 包,包含服务的实现 Jar 包、接口 Jar 包,以及 Web 项目导出 RPC 服务所需的配置文件等。
  • 服务接口层 : 包含业务接口、依赖的 DTO 及需要的枚举类等,最后打包成 Jar 包,发布到 Maven 服务器,也包含在服务导出层的 War 包中。
  • 服务实现层 : 包含业务逻辑实现类、依赖的第三方服务的包装类,以及下层数据库访问的 DAO 类,最后打包成 Jar。

【服务实现层的架构图】
clipboard.png

【服务实现层的反模式架构图】
clipboard.png

1.4.3 微服务项目的持续发布

微服务项目需要实现自动化的CI/CD,包括:

  • 代码管理
  • 自动编译
  • 发布 OA
  • 自动化测试
  • 性能测试
  • 准生产部署和测试
  • 生成环境发布

1.5 服务化管理和治理框架的技术选型

1.5.1 RPC

1、JDK RMI

Java 到 Java 的分布式调用框架,一个 Java 进程内的服务可以调用其他 Java 进程内的服务,使用 JDK 内置的序列化和反序列化协议。

  • 序列化协议:JDK自带的专用序列化协议,不能跨语言
  • 网络传输协议:底层网络协议

2、Hessian & Burlap

  • 序列化协议:Hessian 序列化为二进制协议;Burlap 序列化为 XML 数据
  • 网络传输协议:HTTP 协议

3、Spring HTTP Invoker

  • 序列化协议:Hessian 序列化为二进制协议;Burlap 序列化为 XML 数据
  • 网络传输协议:HTTP 协议

1.5.2

1、Dubbo

  • 提供高性能、透明化的 RPC 远程服务调用,还提供了基本的服务监控、服务质量和服务调度
  • 支持多种序列化协议和通信编码协议,默认使用 Dubbo 协议传输 Hessian 序列化的数据
  • 使用 ZooKeeper 作为注册中心来注册和发现服务
  • 通过客户端负载均衡来路由请求,负载均衡算法包括:随机、轮询、最少活跃调用数、一致性哈希等。

2、HSF

High Speed Framework

3、Thrift

  • 采用中间的接口描述语言定义并创建服务
  • 支持跨语言服务开发和调用,并包含中间的接口描述语言与代码生成和转换工具
  • 采用二进制序列化传输数据

4、AXIS

源于 IBM "SOAP4J",使用 SOAP 协议

5、Mule ESB

基于 Java 语言的企业服务总线产品,可以把多个异构系统通过总线模式集成在一起

1.5.3 微服务

1、Spring Boot
【图1-37 JEE时代,应用包含在容器内的架构图】
clipboard.png

Spring Boot 相反,它将容器嵌入自启动的 Jar 包中,在 Spring Boot 应用启动时,内部启动嵌入的容器
【Spring Boot 的容器包含在应用内的架构图】
clipboard.png

Spring Boot 这种设计在微服务架构下有如下明显有点:

  • 可以创建独立、自启动的应用程序
  • 无需构建 War 包并发布到容器中,构建和维护 War 包、容器的配置和管理也需要成本
  • 通过 Maven 的定制化标签,可以快速构建 Spring Boot 的应用程序
  • 可以最大化的自动化配置 Spring,而无需人工配置各项参数
  • 提供了产品话特点,如:性能分析、健康检查和外部化配置
  • 无 XML 配置
  • 是 Spring Cloud 构建微服务架构的重要基础

2、Netflix

提供服务发现、断路器和监控、智能路由、客户端负载均衡、易用的 REST 客户端等服务化必须的功能

3、Spring cloud Netflix

  • 服务发现组件 Eureka
  • 容错性组件 Hystrix
  • 智能路由组件 Zuul
  • 客户端负载均衡组件 Ribbon

【图 1-39 Spring Cloud Netfix 架构图】
clipboard.png

【Netflix 交互流程】

  1. 服务在 Eureka 服务器实例注册
  2. Zuul 作为一个特殊的服务在 Eureka 上注册并发现服务
  3. Zuul 作为网关,将发现的服务导出给客户端
  4. RestTemplate 和 FeignClient 使用简单的服务调用方法调用服务1、服务2

【Netflix 特点】

  • 服务在 Eureka 注册,由 Spring 管理的 Bean 来发现和调用
  • 通过配置的方式可以启动嵌入式的 Eureka 服务器
  • Feign 客户端通过声明的方式即可导入服务代理
  • Zuul 使用 Ribbon 服务实现客户端负载均衡
  • 通过声明的方式即可插入 Hystrix 的客户端
  • 通过配置的方式即可启动 Hystrix 面板服务器
  • 在 Spring 环境中可以直接配置 Netflix 组件
  • Zuul 可以自动注册过滤器和路由器,形成一个反向代理服务器
  • Hystrix 面板可以对服务的状态进行监控,并提供容错机制
查看原文

赞 2 收藏 2 评论 0

总有刁民想害朕 发布了文章 · 2018-12-18

Git常用命令速查表

【名词】

  • master :默认开发分区
  • origin :默认远程版本库
  • Index / Stage :暂存区
  • Workspace :工作区
  • Repository :仓库区(本地仓库)
  • Remote :远程仓库

一、新建代码库

# 在当前目录新建一个 Git 代码库
$ git init
# 新建一个目录,将其初始化为 Git 代码库
$ git init [project-name]
# 下载一个项目和它的整个代码历史
$ git clone [url]

二、配置

Git 的配置文件为 .gitconfig,它可以自用户主目录(全局配置),也可以在项目目录下(项目配置)。

# 显示当前的 Git 配置
$ git config --list
# 编辑 Git 配置文件
$ git config -e [--global]
# 设置提交代码时的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"

三、增加、删除、修改文件

# 查看状态
$ git status
# 查看变更内容
$ git diff
# 添加指定文件到暂存区
$ git add [file1] [file2] ...
# 添加指定目录到暂存区,包括子目录
$ git add [dir]
# 添加当前目录的所有文件到暂存区
$ git add .
# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
$ git add -p
# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...
# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]
# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]

四、代码提交

# 提交暂存区到仓库区
$ git commit -m [message]
# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]
# 提交工作区自上次 commit 之后的变化,直接到仓库区
$ git commit -a
# 提交时显示所有 diff 信息
$ git commit -v
# 使用一次新的 commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次 commit 的提交信息
$ git commit --amend -m [message]
# 重做上一次 commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2]

五、分支

# 显示所有本地分支
$ git branch
# 列出所有远程分支
$ git branch -r
# 列出所有本地、远程分支
$ git branch -a
# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]
# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]
# 删除分支
$ git branch -d [branch-name]
# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]
# 新建一个分支,并切换到该分支
$ git checkout -b [branch]
# 切换到指定分支,并更新工作区
$ git checkout [branch-name]
# 切换到上一个分支
$ git checkout -
# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]
# 合并指定分支到当前分支
$ git merge [branch]
# 衍[yǎn] 合指定分支到当前分支
$ git rebase [branch]
# 选择一个 commit,合并进当前分支
$ git rebase [branch]

【 merge 与 rebase 区别】

作者:Michaelliu_dev
来源:CSDN
原文:https://blog.csdn.net/liuxiao...
版权声明:本文为博主原创文章,转载请附上博文链接!

假设有如下图所示仓库,该仓库有master和develop两个分支,且develop是在(3.added merge.txt file)commit处从master拉出来的分支。
clipboard.png
merge,假设现在HEAD在(6.added hello.txt file)处,也就是在master分支最近的一次提交处,此时执行git merge develop, 结果如下图所示。
clipboard.png
工作原理就是:git 会自动根据两个分支的共同祖先即 (3.added merge.txt file)这个 commit 和两个分支的最新提交即 (6.added hello.txt file) 和 (5.added test.txt file) 进行一个三方合并,然后将合并中修改的内容生成一个新的 commit,即图二的(7.Merge branch ‘develop’)。
这是merge的效果,简单来说就合并两个分支并生成一个新的提交。
rebase,HEAD在(6.added hello.txt file)处,现在执行git rebase develop,结果如下图所示。
clipboard.png
可以看见develop分支分出来分叉不见了。
下面来解释一下它的工作原理:
在执行git rebase develop之前,HEAD在(6.added hello.txt file)处,当执行rebase操作时,git 会从两个分支的共同祖先 (3.added merge.txt file)开始提取 当前分支(此时是master分支)上的修改,即 (6.added hello.txt file)这个commit,再将 master 分支指向 目标分支的最新提交(此时是develop分支)即(5.added test.txt file) 处,然后将刚刚提取的修改应用到这个最新提交后面。如果提取的修改有多个,那git将依次应用到最新的提交后面,如下两图所示。

图四为初始状态
clipboard.png

图五为执行rebase后的状态
clipboard.png
简单来说,git rebase提取操作有点像git cherry-pick一样,执行rebase后依次将当前的提交cherry-pick到目标分支上,然后将在原始分支上的已提取的commit删除。

六、标签

# 列出所有本地标签
$ git tag
# 基于最新提交创建标签
$ git tag <tagname>
# 删除标签
$ git tag -d <tagname>
# 删除远程tag
$ git push origin :refs/tags/[tagName]
# 查看标签信息
$ git show [tag]
# 提交指定tag
$ git push [remote] [tag]
# 提交所有tag
$ git push [remote] --tags
# 新建一个分支,指向某个tag
$ git checkout -b [branch] [tag]

七、查看信息

# 显示所有变更的文件
$ git status
# 显示当前分支的版本历史
$ git log
# 显示 commit 历史,以及每次 commit 发生变更的文件
$ git log --stat
# 搜索提交历史
$ git log -S [keyword]
# 显示某个 commit 之后的所有变动,每个 commit 占据一行
$ git log [tag] HEAD --pretty=format:%s
# 显示某个 commit 之后的所有变动,其“提交说明”必须符合搜索条件
$ git log [tag] HEAD --grep feature
# 显示某个文件的历史版本,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]
# 显示指定文件相关的每一次diff
$ git log -p [file]
# 显示过去5次提交
$ git log -5 --pretty --oneline
# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn
# 显示指定文件是什么人在什么时间修改过
$ git blame [file]
# 显示暂存区和工作区的差异
$ git diff
# 显示暂存区和上一个 commit 的差异
$ git diff -cached [file]
# 显示工作区与当前分支最新 commit 之间的差异
$ git diff HEAD
# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]
# 显示今天写了多少行代码
$ git diff --shortstat "@{0 day ago}"
# 显示某次提交的元数据和内容变化
$ git show [commit]
# 显示某次提交发生变化的文件
$ git show --name-only [commit]
# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]
# 显示当前分支的最近几次提交
$ git reflog

八、远程操作

# 下载远程仓库的所有变动
$ git fetch [remote]
# 取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]
# 显示所有远程仓库
$ git remote -v
# 显示某个远程仓库的信息
$ git remote show [remote]
# 显示增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]
# 上传本地指定分支到远程仓库
$ git push [remote] [branch]
# 前行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force
# 推送所有分支到远程仓库
$ git push [remote] --all
# 删除远程分支或标签
$ git push <remote> :<branch/tag-name>
# 上传所有标签
$ git push --tags

九、撤销

# 撤销工作目录中所有未提交文件的修改内容
$ git reset --hard HEAD
# 撤销指定的未提交文件的修改内容
$ git checkout HEAD <file>
# 撤销指定的提交
$ git revert <commit>
# 退回到之前 1 天的版本
$ git log --before="1 days"
# 恢复暂存区的指定文件到工作区
$ git checkout [file]
# 恢复某个 commit 的指定文件到暂存区和工作区
$ git checkout [commit] [file]
# 恢复暂存区的所有文件到工作区
$ git checkout .
# 重置暂存区的指定文件,与上一次 commit 保持一致,但工作区不变
$ git reset [file]
# 重置暂存区与工作区,与上一次 commit 保持一致
$ git reset --hard
# 重置当前分支的指针为指定commit,同时重置暂存区,当工作区不变
$ git reset [commit]

# 重置当前分支的 HEAD 为指定 commit,同时重置暂存区和工作区,与指定 commit 一致
$ git reset --hard [commit]

# 重置当前分支的 HEAD 为指定 commit,但保持暂存区和工作区不变
$ git reset --keep [commit]
# 新建一个 commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]
# 暂时将未提交的变化移除,随后再移入
$ git stash
$ git stash pop

十、其他

# 生成一个可供发布的压缩包
$ git archive
查看原文

赞 2 收藏 2 评论 0

总有刁民想害朕 发布了文章 · 2018-12-17

Spring思维导图,让Spring不再难懂(AOP 篇)

转载自:
1、微信公众号【Java思维导图
2、若谷先生

一、什么是 AOP ?

AOP(Aspect-Oriented Programming,面向方面编程),对 OOP(Object-Oriented Programming,面向对象编程)
clipboard.png
  • 【OOP与AOP】

    • 概念

      • AOP(Aspect-Oriented Programming,面向方面编程)
      • OOP(Object-Oriented Programming,面向对象编程)
    • 方向

      • OOP 定义从上到下的关系
      • AOP 定义从左到右的关系
  • 【两个部分】

    • 核心关注点

      • 业务处理的主要流程
    • 横切关注点

      • 与业务主要流程关系不大的部分
      • 经常发生在核心关注点的多处,而各处都是基本相似的功能
      • 如权限认证、日志、事务处理

二、AOP 使用场景

1、AOP框架种类

  • AspectJ
  • JBoss AOP
  • Spring AOP

2、使用 AOP 场景

  • 性能监控:在方法调用前后记录调用事件,方法执行太长或超时报警。
  • 缓存代理:缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
  • 软件破解:使用AOP修改软件的验证类的判断逻辑。
  • 记录日志:在方法执行前后记录系统日志。
  • 工作流系统:工作流系统需要将业务代码和流畅引擎代码混合在一起执行,那么可以使用 AOP 将其分离,并动态挂载业务。
  • 权限验证:方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕获。

3、传统编码与AOP区别

clipboard.png

三、核心概念

clipboard.png
【术语】

  • 切面(Aspect):
    一个关注点的模块化,这个关注点可能会横切多个对象。
    事务管理是 J2EE 应用中一个关于横切关注点的很好列子。
    在 Spring AOP 中,切面可以使用基于模式或者基于 @Aspect 注解的方式来实现。
  • 连接点(Joinpoint):
    在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。
    在 Spring AOP 中,一个连接点总是表示一个方法的执行。
  • 切入点(Pointcut):
    匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如:当执行某个特定名称的方法时)。
    切入点表达式如何和连接点匹配是 AOP 的核心:Spring 缺省使用 Aspect 切入点语法。
  • 引入(Introduction):
    用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))
    Spring 允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,可以使用引入来使一个 bean 实现 isModified 接口,以便简化缓存机制。
  • 目标对象(Target Object):
    被一个或者多个切面所通知的对象,也被称为通知(advised)对象。
    既然 Spring AOP 是通过运行时代理实现的。这个对象永远是一个被代理(proxied)对象。
  • AOP 代理(AOP Proxy):
    AOP 框架创建的对象,用来实现切面契约(例如,通知方法执行等等)。
    在 Spring 中,AOP 代理可以是 JDK 动态代理或者 CGLIB 代理。
  • 织入(Weaving):
    把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用 AspectJ 编译器),类加载时和运行时完成。Spring 和其他纯 Java AOP 框架一样,在运行时完成织入。
  • 通知(Advice):
    在切面的某个特定的连接点上执行的动作。其中包括“around”、“before” 和 “after” 等不同类型的通知。
    许多 AOP 框架(包括 Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。

【通知类型】

  • 前置通知(Before advice):
    @Before
    在某连接点之前执行的通知,但这个通知不能阻止了连接点之前的执行流程(除非它抛出一个异常)。
  • 后置通知(After returning advice):
    @After
    在某连接点正常完成后执行的通知,例如,一个方法么有抛出任何异常,正常返回。
  • 异常通知(After throwing advice):
    @After-returning
    在方法抛出异常退出时执行的通知。
  • 最终通知(After(finally)advice):
    @After-throwing
    当某连接点退出的时候执行的通知(不论是正常退出还是异常退出);
  • 环绕通知(Around advice):
    @Around
    包围一个连接点的通知,如方法调用,这是最强大的一种通知类型。

三、简单例子

1、基于注解的方式

@Aspectj 
public class TransactionDemo {
    @Pointcut(value="execution(* com.yangxin.core.service.*.*.*(..))")
    public void point() { //... }
    
    @Before(value="point()")
    public void before() {
        System.out.println("transaction begin");
    }
    
    @AfterReturning(value = "point()")
    public void after() {
        System.out.println("transaction commit");
    }
    
    @Around("point()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("transaction begin");
        joinPoint.proceed();
        System.out.println("transaction commit");
    }
} 

在 applicationContext.xml 中配置:

<aop:aspectj-autoproxy />
    <bean id="transactionDemo" class="com.yangxin.core.transaction.TransactionDemo" />   

2、基于 XML 的方式

<aop:config>
    <aop:aspect ref="log">
        <aop:pointcut
                expression="(execution(* spring.ch3.topic1.Chief.*(..)))"
                id="chiefPointCut" />
        <aop:before method="washOven" pointcut-ref="chiefPointCut" />
        <aop:before method="prepare" pointcut-ref="chiefPointCut" />
        <aop:after method="after" pointcut-ref="chiefPointCut" />
    </aop:aspect>
</aop:config>
    

四、Spring AOP 原理

AOP 代理其实是由 AOP 框架动态生成的一个对象,该对象可作为目标对象使用。
AOP 代理包含了目标对象的全部方法,但 AOP 代理中的方法与目标对象的方法存在差异:AOP 方法在特定切入点添加了增强处理,并回调了目标对象的方法。
clipboard.png

Spring 的 AOP 代理由 Spring 的 IoC 容器负责生成、管理,其依赖关系也由 IoC 容器负责管理。因此,AOP 代理可以直接使用容器中的其他 Bean 实例作为目标,这种关系可由 IoC 容器的依赖注入提供。

aop开发时,其中需要参与开发的只有 3 个部分:

  • 定义普通业务组件。
  • 定义切入点,一个切入点可能横切多个业务组件。
  • 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作。

五、两种动态代理方式

Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。

Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。

1、JDK 动态代理

  • JDK动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。InvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。
  • Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。

2、CGLib动态代理

  • CGLib全称为Code Generation Library,是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展Java类与实现Java接口,CGLib封装了asm,可以再运行期动态生成新的class。和JDK动态代理相比较:JDK创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过CGLib创建动态代理。

六、补充实例

clipboard.png

package springMVCmybatis.com.my.aop;
import org.aspectj.lang.annotation.Aspect; 
import org.aspectj.lang.annotation.Around; 
import org.aspectj.lang.annotation.Before; 
import org.aspectj.lang.annotation.Pointcut; 
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint; 
import org.aspectj.lang.annotation.After; 
import org.aspectj.lang.annotation.AfterReturning; 
import org.aspectj.lang.annotation.AfterThrowing; 
import org.springframework.core.annotation.Order;

@Aspect
@Order(3)
public class MyAopTest {
    @Pointcut("execution(* springMVCmybatis.addController.addEmp(..))")
    private void pointCutMethod() { //... }
    
    @Pointcut("execution(* springMVCmybatis.com.my.aop.UserServiceImp.*(..))") 
    private void testAOP() { //... }
    
    /**
    * 声明前置通知 ,JoinPont是srpring提供的静态变量,
    * 通过joinPoint参数可以获得目标方法的类名,方法参数,方法名等信息,这个参数可有可无。
    */
    @Before("pointCutMethod() || testAOP()") 
    public void doBefore(JoinPoint joinPoint) { 
        System.out.println("@Before:开始添加--order=3"); 
    } 
    
    /**
    * 声明后置通知 ,如果result的类型与proceed执行的方法返回的参数类型不匹配那么就不会执行这个方法
    */
    @AfterReturning(pointcut = "pointCutMethod()  || testAOP()", returning = "result") 
    public void doAfterReturning(String result) { 
        System.out.println("@AfterReturning:后置通知--order=3"); 
        System.out.println("---" + result + "---"); 
    } 
    
    /**
    * 声明例外通知 
    */
    @AfterThrowing(pointcut = "pointCutMethod() || testAOP()", throwing = "e") 
    public void doAfterThrowing(Exception e) { 
        System.out.println("@AfterThrowing:例外通知--order=3"); 
        System.out.println(e.getMessage()); 
    }
    
    /**
    * 声明最终通知
    */
    @After("pointCutMethod() || testAOP()") 
    public void doAfter() { 
        System.out.println("@After:最终通知--order=3"); 
    } 

    /**
    * 声明环绕通知
    * 参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数,
    * proceed()的返回值就是环绕通知的返回值,proceedingJoinPoint是个接口,
    * implement JoinPoint,所以也可以获得目标函数的类名,方法名等参数。
    */
    @Around("pointCutMethod() || testAOP()") 
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable { 
        System.out.println("@Around:进入方法---环绕通知--order=3"); 
        Object o = pjp.proceed(); 
        System.out.println("@Around:退出方法---环绕通知--order=3"); 
        return o; 
    }
}

七、切点表达式

1、方法签名表达式

execution(<修饰符模式>?<返回类型模式><方法所在类的完全限定名称模式>(<参数模式>)<异常模式>?)
execution(modifiers-pattern? ret-type-pattern fully-qualified-class-name (param-pattern) throws-pattern?)   
对比方法的定义来记忆,一个java方法的全部定义方式可以表示成下面的方式:
public String springMVCmybatic.com.my.aop.UserServiceImp(String a, int b) throw Exception{}
  • modifier-pattern?:表示方法的修饰符,可有可无;对应的就是 public
  • ret-type-pattern:表示方法的返回值;对应的就是 String
  • fully-qualified-class-name 方法所在类的完全限定名称;对应的就是 springMVCmybatic.com.my.aop.UserServiceImp
  • param-pattern:表示方法的参数;对应的就是 String a, int b
  • throws-pattern:表示方法抛出的异常,可有可无;对应的就是 throw Exception

2、&&,||,!(表达式之间可以采用与,或,非的方式来过滤。)

@Around("pointCutMethod() || testAOP()") 
public Object doAround(ProceedingJoinPoint pjp) throws Throwable { 
    System.out.println("@Around:进入方法---环绕通知"); 
    Object o = pjp.proceed(); 
    System.out.println("@Around:退出方法---环绕通知"); 
    return o; 
}

八、多个切点的执行顺序

上面的例子中,定义了order=3,重新创建一个切面,定义order=6,执行的结果是:
@Around:进入方法---环绕通知--order=3
@Before:开始添加--order=3
@Around:进入方法---环绕通知--order=6
@Before:开始添加--order=6
============执行业务方法findUser,查找的用户是:张三=============
@Around:退出方法---环绕通知--order=6
@After:最终通知--order=6
@AfterReturning:后置通知--order=6
---张三---
@Around:退出方法---环绕通知--order=3
@After:最终通知--order=3
@AfterReturning:后置通知--order=3
---张三---
 
 
@Around:进入方法---环绕通知--order=3
@Before:开始添加--order=3
@Around:进入方法---环绕通知--order=6
@Before:开始添加--order=6
============执行业务方法addUser=============
@After:最终通知--order=6
@AfterThrowing:例外通知--order=6
null
@After:最终通知--order=3
@AfterThrowing:例外通知--order=3
null
 从结果中可以看出order越小越先执行,执行完了之后就order越小就越后推出。总结为下面的图:
clipboard.png
查看原文

赞 2 收藏 2 评论 0

总有刁民想害朕 发布了文章 · 2018-12-12

Spring思维导图,让Spring不再难懂(ioc篇)

  • java垃圾回收机制帮助我们实现了这个过程;
  • 而ioc则是让我们无需考虑对象的创建过程,由ioc容器帮我们实现对象的创建、注入等过程。

clipboard.png
【说明】:

  • Context

    • 对于IoC和DI的理解
    • BeanFactory
    • ApplicationContext
  • Bean

    • SpEL(#)
    • 引入外部的属性文件($)
    • 基于XML的Bean配置

      • 配置Bean

        • 通过全类名
        • 通过工厂方法
        • 通过 FactoryBean
      • 装配Bean的属性

        • 通过属性
        • 通过构造器
    • 基于注解的Bean配置

      • 装配Bean的属性

        • Autowired等注解
      • 配置Bean

        • Context:componentScan节点

一、控制反转(Spring IoC)

图片描述
【说明】:

  • IoC 概念

    • 应用控制反转,对象在创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用,传递给它。也可以说,依赖注入到对象中。所以,控制反转是,关于一个对象如何获取他所依赖的对象的引用,这个责任的反转。
    • 控制反转(Inversion of Controll)是一个重要的面向对象编程的法则,来削弱计算机程序的耦合问题,也是轻量级 Spring 框架的核心。
    • 控制反转一般分为两种类型,依赖注入(Dependency Injection,DI)和依赖查找(Dependency Lookup),依赖注入应用广泛。
  • 深入分析

    • 依赖于谁?应用程序依赖于 IoC 容器
    • 为什么要依赖?应用程序需要 IoC 容器来提供对象需要的外部资源
    • 谁注入谁?IoC容器注入应用程序某个对象,应用程序依赖的对象
    • 注入了社么?注入某个对象所需要的外部资源(包括对象、资源、常量对象)
  • 与new对象的区别

    • 正转与反转,传统应用程序是由我们将自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮助创建及注入依赖对象。
  • IoC优缺点

    • 优点:实现组件之间的解耦,提供程序的灵活性和可维护性
    • 缺点:对象生成因为是使用反射编程,在效率上有损耗

二、Spring IoC 容器

【Spring核心组件】:它们构建起了整个Spring的骨骼架构,没有它们就不可能有AOP、Web等特性功能。

  • Core
  • Context
  • Bean

clipboard.png

Spring解决了一个非常关键的问题,它可以让你对对象之间的关系转而用配置文件来管理,或者注解,也就是它的依赖注入机制。而这个注入关系在一个叫Ioc的容器中管理。Ioc容器就是被Bean包裹的对象。Spring正是通过把对象包装在Bean中从而达到管理这些对象及做一些列额外操作的目的。

1、IoC 容器

clipboard.png

  • 概念:
    在每个框架中都有一个容器的概念,所谓的容器就是将常用的服务封装起来,然后,用户只需要遵循一定的规则,就可以达到统一、灵活、安全、方便、快速的目的
    具有依赖注入功能的容器,负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖
  • Bean的概念:
    由IoC容器管理的那些组成应用程序的对象就叫Bean
    Bean就是由Spring容器初始化、装配及管理的对象,除此之外,Bean就与应用程序中的其他对象没什么区别了
  • 元数据 BeanDefinition
    确定如何实例化Bean、管理Bean之间的依赖关系及管理Bean,就需要配置元数据,在Spring中由BeanDefintion代表
  • 如何工作(以XML配置方式解释)

    • 准备配置文件:配置文件中声明 Bean 定义也就是 Bean 配置元数据。
    • 由 IoC 容器进行解析元数据:IoC 容器的 Bean Reader 读取并解析配置文件,根据定义生成 BeanDefintion 配置元数据对象,IoC 容器根据 BeanDefintion 进行实例化、配置及组装 Bean。
    • 实例化 IoC 容器:由客户端实例化容器,获取需要的 Bean。
  • hello world
@Test
public void testHelloWorld() {
    // 1、读取配置文件实例化一个 IoC 容器
    ApplicationContext context = new ClassPathXmlApplicationContext("helloWorld.xml");
    // 2、从容器获取 Bean,主次此处完全“面向接口编程”
    HelloApi helloApi = context.getBean("hello", HelloApi.class);
    // 3、执行业务逻辑
    helloApi.sayHello();
}

2、Spring IoC 初始化

clipboard.png

3、Spring IoC 容器工作原理
clipboard.png

三、核心组件协同工作

clipboard.png

  • Bean : 【演员】Spring 的 bean 实例
  • Context :【导演】Spring 的上下文
Bean 包装的是 Object,而 Object 必然有数据,如何给这些数据体提供生存环境就是 Context 要解决的问题,对 Context 来说就是要发现每个 Bean 之间的关系,为它们建立这种关系并维护关系,所以 Context 就是 Bean 关系的集合,这个关系机构叫做 IoC 容器。
  • Core :【道具】Spring 的核心工具包
建立和维护每个 Bean 之间的关系所需要的一些核心工具包。相当于 Util。

四、BeanFactory 和 ApplicationContext 的区别

IoC 中最核心的接口是 BeanFactory 提供 IoC 的高级服务,而 ApplicationContext 是建立在 BeanFactory 基础之上提供抽象的面向应用的服务。
clipboard.png

  • BeanFactory

提供了一种先进的配置机制来管理任何种类 Bean 对象,这种配置机制考虑到任何一种可能的存储方式。

  • ApplicationContext

建立在 BeanFactory,具有 BeanFactory 的所有功能和行为

  • MessageSource,提供国际化的消息服务,扩展类 MessageResource 接口
  • 资源访问,如 URL 和文件
  • 事件传播
  • 载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的 web 层。
  • 不同点

    • 选用哪个?

      • BeanFactory:是延迟加载,如果 Bean 的某一个属性没有注入,BeanFactory 加载后,直至第一次使用调用 getBean 方法才会抛出异常
      • ApplicationContext:则在初始化自身时检验,这样有利于检查所依赖属性是否注入;所以通常情况下使用 ApplicationContext。
  • 特性表 BeanFatory / ApplicationContext

    * Bean 实例化/装配                    YES     YES
    * 启动 BeanPostProcessor注册          NO      YES
    * 启动 BeanFactoryPostProcessor注册   NO     YES
    * 便捷的 MessageSource反问(i18n)     NO     YES
    * ApplicationEvent 发送               NO     YES
  • 关系

    * BeanFactory 提供了配置框架和基本的功能
    * ApplicationContext 建立在 BeanFactory 之上,并增加了其他功能
    * 一般来说,ApplicationContext 是 BeanFactory 的完全超集,任何 BeanFactory 功能和行为的描述也同样被认为适用于 ApplicationContext。

五、三种注入方式

在Spring框架中,依赖注入(DI)的设计模式是用来定义对象彼此间的依赖。使用xml配置bean的情况下,它主要有两种类型:

  • Setter 方法注入
  • 构造器注入
  • 注解 @autowired
当然,有了注解之后,使用注解的方式更加方便快捷。即自动装配功能实现属性自动注入(@autowire)。

六、原理解析

1、Contxt 的初始化过程

当运行 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
构造方法 ClassPathXmlApplicationContext(String configLocation), 调用了 this(new String[] {configLocation}, true, null); 该构造方法具体代码:
【ClassPathXmlApplicationContext构造方法】:
/**
* 在 parent 下创建 ClassPathXmlApplicationContext,
* 从 XML 中读取素有 Bean 定义。
* @param configLocations 配置文件路径如 c:\simpleContext.xml
* @param refresh 是否需要自动刷新 context,refrest--> 重新加载
* 加载所有的 bean 定义,创建所有单例。
* refresh 为 true 的时候,根据 context 来手工刷新
* @param parent the parent context
* @throws BeansException if context creation failed
* @see #refresh()
*/
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)     
    throws BeansException{
    // 初始化XmlApplicationContext
    super(parent);
    // 转换配置文件的路径
    setConfigLocations(configLocations);
    if(refresh) {
        // 重新刷新所有的 context,这一篇的重点
        refresh();
    }
}
【refresh方法】:
/**
* 加载或刷新持久的配置,可能是xml文件,properties文件,或者关系型数据库的概要。
* 作为一启动方法,如果初始化失败将会销毁已经创建好的单例,避免重复加载配置文件。
* 换句话说,在执行这个方法之后,要不全部加载单例,要不都不加载。
public void refresh() throws BeansException, IllegalStateException {
    synchronized(this.startupShutdownMonitor) {
        // 初始化配置准备刷新,验证环境中的一些必选参数
        prepareRefresh();
        
        // 告诉继承销毁内部的 factory创建新的 factory 实例
        // 初始化 Bean 实例
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        
        // 初始化 beanFactory 的基本信息,包括 classLoader、environment、忽略的注解等。
        prepareBeanFactory(beanFactory);
        
        try {
            // beanFactory 内部的 postProces,可以理解为 context 中的 PostProcess 的补充
            beanFactory.postProcessBeanFactory(beanFactory)p;
            
            
            // 执行 BeanFactoryPostProcessor(在beanFactory)初始化过程中,bean 初始化之前,修改BeanFactory 参数
            // BeanDefinitionRegistryPostProcessor 其实也是继承自 BeanFactoryPostProcessor,
            // 多个对 BeanDefinitionRegistry 的支持 invokeBeanFactoryPostProcessors(beanFactory)
            // 执行 postProcess,那 BeanPoseProcessor 是什么呢,是为了在 bean 加载过程中修改 bean 的内容,
            // 使用Before、After分别对应初始化前和初始化后
            registerBeanPostProcessors(beanFactory);
            
            // 初始化 MessageSource,主要用于 I18N 本地化的内容
            initMessageSource();
            
            // 初始化事件广播 ApplicationEventMulticaster,使用观察者模式,对注册的 ApplicationEvent 时间进行捕捉
            initApplicationEventMulticaster();
            
            // 初始化特殊的 bean 方法
            onRefresh();
            
            // 将所有 ApplicationEventListener 注册到 ApplicationEventMulticaster 中
            registerListeners();
            
            // 初始化所有部位 lazy-init 的bean,singleton 实例
            finishBeanFactoryInitialization(beanFactory);
            
            // 初始化 lifeCycle 的bean启动(例如 quartz 的定时器),如果开启 JMX 则将 ApplicationContext 注册到上面
            finishRefresh();
        } 
        catch(BeansException ex) {
            // 销毁已经创建单例
            resources.destroyBeans();
            
            // 将 context 的状态转换为无效,标示初始化失败
            flag.cancelRefresh(ex);
        }
    }
}

2、从时序图看上述启动初始化

【容器初始化时序图】
clipboard.png

查看原文

赞 22 收藏 22 评论 0

总有刁民想害朕 发布了文章 · 2018-12-11

Chapter 10 运行 ZooKeeper

一、配置 ZooKeeper 服务器

ZooKeeper 服务器在启动时从一个名为 zoo.cfg 的配置文件读取所有选项,多个服务器如果角色相似,同时基本配置信息一样,就可以共享一个文件。
data 目录下的 myid 文件用于区分各个服务器,对每个服务器来说,data 目录是唯一的,因此这个目录可以更加方便地保持一些差异化文件。服务器ID 将 myid 文件作为一个索引引入到配置文件中,一个特定的 ZooKeeper 服务器可以知道如何配置自己参数。
配置参数常常通过配置文件的方式进行设置,本节后续部分,通过列表方式列出了这些参数。很多参数也可以通过Java的系统属性传递,其形式通常为zookeeper.propertyName,在启动服务器时,通过-D选项设置这些属性。不过,系统属性所对应的一个特定参数对服务来说是插入的配置,配置文件中的配置参数优先于系统属性中的配置。

1、基本配置

  • clientPort
    客户端所连接的服务器所监听的TCP端口,默认情况下,服务端会监听在所有的网络连接接口的这个端口上,除非设置了clientPortAddress参数。客户端端口可以设置为任何值,不同的服务器也可以监听在不同的端口上。默认端口号为2181。
  • dataDir 和 dataLogDir

    • dataDir:用于配置内存数据库保存的模糊快照的目录,如果某个服务器为集群中的一台,id文件也保存在该目录下。dataDir并不需要配置到一个专用存储设备上,快照将会以后台线程的方式写入,且并不会锁定数据库,而且快照的写入方式并不是同步方式,直到写完整快照为止。
    • dataLogDir:事务日志对该目录所处的存储设备上的其他活动更加敏感,服务端会尝试进行顺序写入事务日志,以为服务端在确认一个事务前必须将数据同步到存储中,该设备的其他活动(尤其是快照的写入)可能导致同步时磁盘过于忙碌,从而影响写入的吞吐能力。因此,最佳实践是使用专用的日志存储设备,将dataLogDir的目录配置指向该设备。
  • tickTime
    tick的时长单位为毫秒,tick为ZooKeeper使用的基本的时间度量单位,在9.7节已经介绍过,该值还决定了会话超时的存储器大小。Zookeeper集群中使用的超时时间单位通过tickTime指定,也就说,实际上tickTime设置了超时时间的下限值,因为最小的超时时间为一个tick时间,客户端最小会话超时事件为两个tick时间。
    tickTime的默认值为3000毫秒,更低的tickTime值可以更快地发现超时问题,但也会导致更高的网络流量(心跳消息)和更高CPU使用率(会话存储器的处理)。

2、存储配置

  • preAllocSize
    用于设置预分配的事务日志文件(zookeeper.preAllocSize)的大小值,以KB为单位。
    当写入事务日志文件时,服务端每次会分配preAllocSize值的KB的存储大小,通过这种方式可以分摊文件系统将磁盘分配存储空间和更新元数据的开销,更重要的是,该方式也减少了文件寻址操作的次数。

    默认情况下preAllocSize的值为64MB,缩小该值的一个原因是事务日志永远不会达到这么大,因为每次快照后都会重新启动一个新的事务日志,如果每次快照之间的日志数量很小,而且每个事务本身也很小,64MB的默认值显然就太大了。例如,如果我们每1000个事务进行一次快照,每个事务的平均大小为100字节,那么100KB的preAllocSize值则更加合适。默认的preAllocSize值的设置适用于默认的snapCount值和平均事务超过512字节的情况。

  • snapCount
    指定每次快照之间的事务数(zookeeper.snapCount)。
    当Zookeeper服务器重启后需要恢复其状态,恢复时两大时间因素分别是为恢复状态而读取快照的时间以及快照启动后所发生的事务的执行时间。执行快照可以减少读入快照文件后需要应用的事务数量,但是进行快照时也会影响服务器性能,即便是通过后台线程的方式进行写入操作。
    snapCount的默认值为100000,因为进行快照时会影响性能,所以集群中所有服务器最好不要在同一时间进行快照操作,只要仲裁服务器不会一同进行快照,处理时间就不会受影响,因此每次快照中实际的事务数为一个接近snapCount值的随机数。
    注意,如果snapCount数已经达到,但前一个快照正在进行中,新的快照将不会开始,服务器也将继续等到下一个snapCount数量的事务后再开启一个新的快照。
  • autopurge.snapRetainCount
    当进行清理数据操作时,需要保留在快照数量和对应的事务日志文件数量。
    ZooKeeper将会定期对快照和事务日志进行垃圾回收操作,autopurge.snapRetainCount值指定了垃圾回收时需要保留的快照数,显然,并不是所有的快照都可以被删除,因为那样就不可能进行服务器的恢复操作。autopurge.snapRetainCount的最小值为3,也是默认值的大小。
  • autopurge.purgeInterval
    对快照和日志进行垃圾回收(清理)操作的时间间隔的小时数。如果设置为一个非0的数字,autopurge.purgeInterval指定了垃圾回收周期的时间间隔,如果设置为0,默认情况下,垃圾回收不会自动执行,而需要通过ZooKeeper发行包中的zkCleanup.sh脚本手动运行。
  • fsync.warningthresholdms
    触发警告的存储同步时间阀值(fsync.warningthresholdms),以毫秒为单位。
    ZooKeeper服务器在应答变化消息前会同步变化情况到存储中。如果同步系统调用消耗了太长时间,系统性能就会受到严重影响,服务器会跟踪同步调用的持续时间,如果超过fsync.warningthresholdms只就会产生一个警告消息。默认情况下,该值为1000毫秒。
  • weight.x=n
    该选项常常以一组参数进行配置,该选项指定组成一个仲裁机构的某个服务器的权重为n,其权重n值指示了该服务器在进行投票时的权重值。在ZooKeeper中一些部件需要投票值,比如群首选举中和原子广播协议中。默认情况下,一个服务器的权重值为1,如果定义的一组服务器没有指定权重,所有服务器的权重值将默认分配为1。
  • traceFile
    持续跟踪ZooKeeper的操作,并将操作记录到跟踪日志中,跟踪日志的文件名为traceFile.year.month.day。除非设置了该选项(requestTraceFile),否则跟踪功能将不会启用。
    该选项用来提供ZooKeeper所进行的操作的详细视图。不过,要想记录这些日志,ZooKeeper服务器必须序列化操作,并将操作写入磁盘,这将争用CPU和磁盘的时间。如果你使用了该选项,请确保不要将跟踪文件放到日志文件的存储设备中。还需要知道,跟踪选项还可能影响系统运行,甚至可能会很难重现跟踪选项关闭时发生的问题。另外还有个有趣的问题,traceFile选项的Java系统属性配置中不含有zookeeper前缀,而且系统属性的名称也与配置选项名称不同,这一点请小心。

3、网络配置

这些配置参数可以限制服务器和客户端之间的通信,超时选项也在该节进行讨论:
  • globalOutstandingLimit
    ZooKeeper中待处理请求的最大值(zookeeper.globalOutstandingLimit)。
    ZooKeeper客户端提交请求比ZooKeeper服务端处理请求要快很多,服务端将会对接收到的请求队列化,最终(也许几秒之内)可能导致服务端的内存溢出。为了防止发生这个问题,ZooKeeper服务端中如果待处理请求达到globalOutstandingLimit值就会限制客户端的请求。但globalOutstandingLimit值并不是硬限制,因为每个客户端至少有一个待处理请求,否则会导致客户端超时,因此,当达到globalOutstandingLimit值后,服务端还会继续接收客户端连接中的请求,条件是这个客户端在服务器中没有任何待处理的请求。
    为了确定某个服务器的全局限制值,我们只是简单地将该参数值除以服务器的数量,目前还没有更智能的方式去实现全局待处理操作数量的计算,并强制采用该参数所指定的限制值,因此,该限制值为待处理请求的上限值,事实上,服务器之间完美的负载均衡解决方案还无法实现,所以某些服务器运行得稍缓慢一点,或者处于更高的负载中,即使最终没有达到全局限制值也可能被限制住吞吐量。
    该参数的默认值为1000个请求,你可能并不会修改该参数值,但如果你有很多客户端发送大数据包请求可能就需要降低这个参数值,但我们在实践中还未遇到需要修改这个参数的情况。
  • maxClientCnxns
    允许每个IP地址的并发socket连接的最大数量。Zookeeper通过流量控制和限制值来避免过载情况的发生。一个连接的建立所使用的资源远远高于正常操作请求所使用的资源。我们曾看到过某些错误的客户端每秒创建很多ZooKeeper连接,最后导致拒绝服务(DoS),为了解决这个问题,我们添加了这个选项,通过设置该值,可以在某个IP地址已经有maxClientCnxns个连接时拒绝该IP地址新的连接。该选项的默认值为60个并发连接。
    注意,每个服务器维护着这个连接的数量,如果有一个5个服务器的集群,并且使用默认的并发连接数60,一个欺诈性的客户端会随机连接到这5个不同的服务器,正常情况下,该客户端几乎可以从单个IP地址上建立300个连接,之后才会触发某个服务器的限制。
  • clientPortAddress
    限制客户端连接到指定的接收信息的地址上。默认情况下,一个ZooKeeper服务器会监听在所有的网络接口地址上等待客户端的连接。
    有些服务器配置了多个网络接口,其中一个网络接口用于内网通信,另一个网络接口用于公网通信,如果你并不希望服务器在公网接口接受客户端的连接,只需要设置clientPortAddress选项为内网接口的地址。
  • minSessionTimeout
    最小会话超时时间,单位为毫秒。当客户端建立一个连接后就会请求一个明确的超时值,而客户端实际获得的超时值不会低于minSessionTimeout的值。
    minSessionTimeout的默认值为tickTime值的两倍。配置该参数值过低可能会导致错误的客户端故障检测,配置该参数值过高会延迟客户端故障的检测时间。
  • maxSessionTimeout
    会话的最大超时时间值,单位为毫秒。当客户端建立一个连接后就会请求一个明确的超时值,而客户端实际获得的超时值不会高于maxSessionTimeout的值。
    虽然该参数并不会影响系统的性能,但却可以限制一个客户端消耗系统资源的时间,默认情况下maxSessionTimeout的时间为tickTime的20倍。

4、集群配置

当以一个集群来构建ZooKeeper服务时,需要为每台服务器配置正确的时间和服务器列表信息,以便服务器之间可以互相建立连接并进行故障监测,在ZooKeeper的集群中,这些参数的配置必须一致:
  • initLimit
    对于追随者最初连接到群首时的超时值,单位为tick值的倍数。
    当某个追随者最初与群首建立连接时,它们之间会传输相当多的数据,尤其是追随者落后整体很多时。配置initLimit参数值取决于群首与追随者之间的网络传输速度情况,以及传输的数据量大小,如果ZooKeeper中保存的数据量特别大(即存在大量的znode节点或大数据集)或者网络非常缓慢,就需要增大initLimit值,因为该值取决于环境问题,所有没有默认值。需要为该参数配置适当的值,以便可以传输所期望的最大快照,也许有时你需要多次传输,你可以配置initLimit值为两倍你所期望的值。如果配置initLimit值过高,那么首次连接到故障的服务器就会消耗更多的时间,同时还会消耗更多的恢复时间,因此最好在你的网络中进行追随者与群首之间的网络基准测试,以你规划所使用的数据量来测试出你所期望的时间。
  • syncLimit
    对于追随者与群首进行sync操作时的超时值,单位为tick值的倍数。
    追随者总是会稍稍落后于群首,但是如果因为服务器负载或网络问题,就会导致追随者落后群首太多,甚至需要放弃该追随者,如果群首与追随者无法进行sync操作,而且超过了syncLimit的tick时间,就会放弃该追随者。与initLimit参数类似,syncLimit也没有默认值,与initLimit不同的是,syncLimit并不依赖于ZooKeeper中保存的数据量大小,而是依赖于网络的延迟和吞吐量。在高延迟网络环境中,发送数据和接收响应包会耗费更多时间,此时就需要调高syncLimit值。即使在相对低延迟的网络中,如果某些相对较大的事务传输给追随者需要一定的时间,你也需要提高syncLimit值。
  • leaderServes
    配置值为“yes”或“no”标志,指示群首服务器是否为客户端提供服务(zookeeper.leaderServes)。
    担任群首的ZooKeeper服务器需要做很多工作,它需要与所有追随者进行通信并会执行所有的变更操作,也就意味着群首的负载会比追随者的负载高,如果群首过载,整个系统可能都会受到影响。
    该标志位如果设置为“no”就可以使群首除去服务客户端连接的负担,使群首将所有资源用于处理追随者发送给它的变更操作请求,这样可以提高系统状态变更操作的吞吐能力。换句话说,如果群首不处理任何与其直连的客户端连接,追随者就会有更多的客户端,因为连接到群首的客户端将会分散到追随者上,尤其注意在集群中服务器数量比较少的时候。默认情况下,leaderServes的值为“yes”。
  • server.x=[hostname]:n:n[:observer]
    服务器x的配置参数。
    ZooKeeper服务器需要知道它们如何通信,配置文件中该形式的配置项就指定了服务器x的配置信息,其中x为服务器的ID值(一个整数)。当一个服务器启动后,就会读取data目录下myid文件中的值,之后服务器就会使用这个值作为查找server.x项,通过该项中的数据配置服务器自己。如果需要连接到另一个服务器y,就会使用server.y项的配置信息来与这个服务器进行通信。
    其中hostname为服务器在网络n中的名称,同时后面跟了两个TCP的端口号,第一个端口用于事务的发送,第二个端口用于群首选举,典型的端口号配置为2888:3888。如果最后一个字段标记了observer属性,服务器就会进入观察者模式。
    注意,所有的服务器使用相同的server.x配置信息,这一点非常重要,否则的话,因服务器之间可能无法正确建立连接而导致整个集群无法正常工作。
  • cnxTimeout
    在群首选举打开一个新的连接的超时值(zookeeper.cnxTimeout)。
    ZooKeeper服务器在进行群首选举时互相之间会建立连接,该选项值确定了一个服务器在进行重试前会等待连接成功建立的时间为多久,9.2节介绍了该超时的用途。默认的超时时间为5秒,该值足够大,也许你并不需要修改。
  • electionAlg
    选举算法的配置选项。
    为了整个配置的完整性,我们也列入了该选项。该选项用于选择不同的群首选举算法,但除了默认的配置外,其他算法都已经弃用了,所以并不需要配置这个选项。

5、认证和授权选项

该节中包括认证和授权相关的选型配置。对于Kerberos相关的配置选项信息,请参考6.1.2节:
zookeeper.DigestAuthenticationProvider.superDigest(只适用于Java系统属性)该系统属性指定了“super”用户的密码摘要信息(该功能默认不启用),以super用户认证的客户端会跳过所有ACL检查。该系统属性的值形式为super:encoded_digest。为了生成加密的摘要,可以使用org.apache.zookeeper.server.auth.DigestAuthenticationProvider工具,使用方式如下:
java -cp $ZK_CLASSPATH \ org.apache.zookeeper.server.auth.DigestAuthenticationProvider super:asdf

通过命令行工具生成一个 asdf 密码的加密摘要信息:
super:asdf->super:T+4Qoey4ZZ8Fnni1Yl2GZtbH2W4=

为了在服务器启动中使用该摘要,可以通过以下命令实现:
export SERVER_JVMFLAGS
SERVER_JVMFLAGS=-Dzookeeper.DigestAuthenticationProvicder.superDigest=
    suer:T+4Qoey4ZZ8Fnni1Yl2GZtbH2W4=
./bin/zkServer.sh start

现在通过 zkCli 进行连接:
[zk: localhost:2181(CONNECTED) 0] addauth digest super:asdf
[zk: localhost:2181(CONNECTED) 1]
此时,已经以super用户的身份被认证,现在不会被任何ACL所限制。

【注意:不安全连接】

ZooKeeper客户端与服务器之间的连接并未加密,因此不要在不可信的链接中使用super的密码,使用super密码的安全方式是在ZooKeeper服务器本机上使用super密码运行客户端。

6、非安全配置

  • forceSync
    通过“yes”或“no”选项可以控制是否将数据信息同步到存储设备上(zookeeper.forceSync)。
    默认情况下,forceSync配置yes时,事务只有在同步到存储设备后才会被应答,同步系统调用的消耗很大,而且也是事务处理中最大的延迟原因之一。如果forceSync配置为no,事务会在写入到操作系统后就立刻被应答,在将事务写入磁盘之前,这些事务常常缓存于内存之中,配置forceSync为no可以提高性能,但代价是服务器崩溃或停电故障时可恢复性。
  • jute.maxbuffer(仅适用于Java系统属性)
    一个请求或响应的最大值,以字节为单位。该选项只能通过Java的系统属性进行配置,并且选项名称没有zookeeper前缀。
    ZooKeeper中内置了一些健康检查,其中之一就是对可传输的znode节点数据的大小的检查,ZooKeeper被设计用于保存配置数据,配置数据一般由少量的元数据信息(大约几百字节)所组成。默认情况下,一个请求或响应消息如果大于1M字节,就会被系统拒绝,可以使用该属性来修改健康检查值,调小检查值,或者真的确认要调大检查值。

【注意:修改健康检查值】
虽然通过jute.maxbuffer指定的限制值可以进行大块数据的写入操作,但获取一个znode节点的子节点,而同时该节点有很多子节点时就会出现问题。如果一个znode节点含有几十万个子节点,每个子节点的名字长度平均为10个字符,在试着返回子节点列表时就会命中默认最大缓冲大小检查,此时就会导致连接被重置。

  • skipACL
    跳过所有ACL检查(zookeeper.skipACL)。
    处理ACL检查会有一定的开销,通过该选项可以关闭ACL检查功能,这样做可以提高性能,但也会将数据完全暴露给任何一个可以连接到服务器的客户端。
  • readonlymode.enabled(仅适用于Java系统属性)
    将该配置设置为true可以启用服务器只读模式功能,客户端可以以只读模式的请求连接服务器并读取信息(可能是已过期的信息),即使该服务器在仲裁中因分区问题而被分隔。为了启用只读模式,客户端需要配置canBeReadOnly为true。
    该功能可以使客户端即使在网络分区发生时也能读取(不能写入)ZooKeeper的状态,在这种情况下,被分区而分离的客户端依然可以继续取得进展,并不需要等待分区问题被修复。特别注意,一个与集群中其他服务器失去连接ZooKeeper也许会终止以只读模式提供过期的数据服务。

7、日志

ZooKeeper采用SLF4J库(JAVA简易日志门面)作为日志的抽象层,默认使用Log4J进行实际的日志记录功能。
Log4J的配置文件为log4j.properties,系统会从classpath中加载这个文件,对于Log4J比较失望的是,如果对应路径不存在log4j.properties文件,我们会看到以下输出信息:
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.serv ...
log4j:WARN Please initialize the log4j system properly.

一般,log4j.properties会保存到classpath中的conf目录下,在ZooKeeper中的log4j.properties文件的主要部分:
zookeeper.root.logger=INFO, CONSOLE ①
zookeeper.console.threshold=INFO
zookeeper.log.dir=.
zookeeper.log.file=zookeeper.log
zookeeper.log.threshold=DEBUG
zookeeper.tracelog.dir=.
zookeeper.tracelog.file=zookeeper_trace.log
log4j.rootLogger=${zookeeper.root.logger} ②
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender ③
log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold} ④
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout ⑤
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] -
...
log4j.appender.ROLLINGFILE=org.apache.log4j.RollingFileAppender ⑥
log4j.appender.ROLLINGFILE.Threshold=${zookeeper.log.threshold} ⑦
log4j.appender.ROLLINGFILE.File=${zookeeper.log.dir}/${zookeeper.log.file}
log4j.appender.ROLLINGFILE.MaxFileSize=10MB
log4j.appender.ROLLINGFILE.MaxBackupIndex=10
log4j.appender.ROLLINGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.ROLLINGFILE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] -
①第一组配置中,所有配置项均以zookeeper.开头,配置了该文件的默认值,这些配置项实际上是系统属性配置,可以通过java命令行指定-D参数来覆盖JVM的配置。第一行的日志配置中,默认配置了日志消息的级别为INFO,即所有低于INFO级别的日志消息都会被丢弃,使用的appender为CONSOLE。你可以指定多个appender,例如,你如果想将日志信息同时输出到CONSOLE和ROLLINGFILE时,可以配置zookeeper.root.logger项为INFO,CONSOLE,ROLLINGFILE。
②rootLogger指定了处理所有日志消息的日志处理器,因为我们并不需要其他日志处理器。
③该行配置以CONSOLE名称定义了一个类,该类会处理消息的输出。在这里使用的是ConsoleAppender类。
④在appender的定义中也可以过滤消息,该行配置了这个appender会忽略所有低于INFO级别的消息,因为zookeeper.root.logger中定义了全局阀值为INFO。
⑤appender使用的布局类对输出日志在输出前进行格式化操作。我们通过布局模式定义了输出日志消息外还输出日志级别、时间、线程信息和调用位置等信息。
⑥RollingFileAppender实现了滚动日志文件的输出,而不是不断地输出到一个单个日志文件或控制台。除非ROLLINGFILE被rootLogger引用,否则该appender会被忽略。
⑦定义ROLLINGFILE的输出级别为DEBUG,因为rootLogger过滤了所有低于INFO级别的日志,所以,你如果你想看DEBUG消息,就必须将zookeeper.root.logger项的配置从INFO修改为DEBUG。

8、专用资源

当考虑在服务器上运行ZooKeeper如何配置时,服务器本身的配置也很重要。为了达到所期望的性能,可以考虑使用专用的日志存储设备,就是说日志目录处于专属的硬盘上,没有其他进程使用该硬盘资源,甚至周期性的模糊快照也不会使用该硬盘。

二、配置 ZooKeeper 集群

仲裁(quorum)的概念:该概念深深贯穿于ZooKeeper的设计之中。在复制模式下处理请求时以及选举群首时都与仲裁的概念有关,如果ZooKeeper集群中存在法定人数的服务器已经启动,整个集群就可以继续工作。
观察者(observer):与集群一同工作,接收客户端请求并处理服务器上的状态变更,但是群首并不会等待观察者处理请求的响应包,同时集群在进行群首选举时也不会考虑观察者的通知消息。

1、多数原则

当集群中拥有足够的 ZooKeeper 服务器来处理请求时,称这组服务器的集合为仲裁法定人数。
当配置多个服务器来组成ZooKeeper集群时,我们默认使用多数原则作为仲裁法定人数。ZooKeeper会自动监测是否运行于复制模式,从配置文件读取时确定是否拥有多个服务器的配置信息,并默认使用多数原则的仲裁法定人数。

2、法定人数的可配置性

关于法定人数的一个重要属性:
如果一个法定人数解散了,集群中另一个法定人数形成,这两个法定人数中至少有一个服务器必须交集。
ZooKeeper也允许灵活的法定人数配置,这种特殊方案就是对服务器进行分组配置时,会将服务器分组成不相交的集合并分配服务器的权重,通过这种方案来组成法定人数,需要使多数组中的服务器形成多数投票原则。
例如,有三个组,每个组中有三个服务器,每个服务器的权重值为1,在这种情况下,需要四个服务器来组成法定人数:某个组中的两个服务器,另一组中的两台服务器。
总之,其数学逻辑归结为:如果有G个组,所需要的服务器为一个G组的服务器,满足|G|>|G|/2,同时对于G组中的服务器集合g,还需要集合g中的集合g满足集合g的所有权重值之和W'不小于集合g的权重值之和(如:W>W/2)。
通过以下配置选项可以创建一个组:
group.x=n[:n]
启用法定人数的分层构建方式。x为组的标识符,等号后面的数字对应服务器的标识符,赋值操作符右侧为冒号分隔的服务器标识符的列表。注意,组与组之间不能存在交集,所有组的并集组成了整个ZooKeeper集群,换句话说,集群中的每个服务器必须在某个组中被列出一次。

【下面的示例说明了9个服务器被分为3组的情况:】

group.1=1:2:3
group.2=4:5:6
group.3=7:8:9
这个例子中,每个服务器的权重都一样,为了构成法定人数,需要两个组及这两个组中各取两个服务器,也就是总共4个服务器。但根据法定人数多数原则,至少需要5个服务器来构成一个法定人数。注意,不能从任何子集形成法定人数的4个服务器,不过,一个组全部服务器加上另一个组的一个单独的服务器并不能构成法定人数。
当在跨多个数据中心部署ZooKeeper服务时,这种配置方式有很多优点。例如,一个组可能表示运行于不同数据中心的一组服务器,即使这个数据中心崩溃,ZooKeeper服务也可以继续提供服务。
在跨三个数据中心部署的这种方式可以容忍某个数据中心的故障问题,可以在两个数据中心中每一个部署三个服务器,而只在第三个数据中心部署一个服务器,通过这种方式来使用多数原则,这样,如果某个数据中心不可用,其他两个数据中心还能组成法定人数。这种配置方式的优点是这七个服务器中的任何四个都构成一个法定人数,而缺点是一旦某个数据中心不可用,其他数据中心中任何服务器的崩溃都无法容忍。
如果只有两个数据中心可用,可以使用【权重值】来表示优先权,例如,基于每个数据中心中的客户端数量来配置权重值。只有两个数据中心时,如果每个服务器的权重值都一样,就无法容忍任何一个数据中心的失效,但是如果对某个服务器分配了更高的权重值,就可以容忍这两个数据中心中某个数据中心的失效。假设,在每个数据中心中分配三个服务器,并且将这些服务器均放到同一组中:
group.1=1:2:3:4:5:6
因为所有的服务器默认情况下权重值都一样,因此只要6个服务器中有4个服务器有效时就可以构成法定人数的服务器。当然,这也意味着如果某个数据中心失效,就不能形成法定人数,即使另一个数据中心的三个服务器均有效。

【为了给服务器分配不同的权重值,可以通过以下选型进行配置:】

weight.x=n
与group选项一起配合使用,通过该选项可以为某个服务器形成法定人数时分配一个权重值为n。其值n为服务器投票时的权重,ZooKeeper中群首选举和原子广播协议中均需要投票。默认情况下,服务器的权重值为1,如果配置文件中定义了组的选项,但为指定权重值,所有的服务器均会被分配权重值1。

假设,某个数据中心只要其所有服务器均可用,即使在其他数据中心失效时,这个数据中心也可以提供服务,我们暂且称该数据中心为D1,此时,可以为D1中的某个服务器分配更高权重值,以便可以更容易与其他服务器组成法定人数。

【假设D1中有服务器1、2和3,通过以下方式为服务器1分配更高的权重值:】

weight.1=2
通过以上配置,就有了7个投票,在构成法定人数时,只需要4个投票。如果没有weight.1=2参数,任何服务器都需要与其他三个服务器来构成法定人数,但有了这个参数配置,服务器1与两个服务器就可以构成法定人数。因此,只要D1可用,即使其他数据中心发生故障,服务器1、2和3也能构成法定人数并继续提供服务。

通过以上不同的法定人数配置的若干示例,看到该配置对部署的影响。提供的分层方案非常灵活,通过不同的权重值和组的管理可以提供不同的分层配置。

3、观察者

观察者(observer)为ZooKeeper服务器中不参与投票但保障状态更新顺序的特殊服务器。

【配置ZooKeeper集群使用观察者,需要在观察者服务器的配置文件中添加以下行】:

peerType=observer

【同时,还需要在所有服务器的配置文件中添加该服务器的:observer定义。如下】:

server.1:localhost:2181:3181:observer

三、重配置

图10-1的场景,三个服务器(A、B、C)组成了整个集群,服务器C因某些网络拥塞问题稍稍落后于整个集群,因此服务器C刚刚了解事务到<1,3>(其中1为时间戳,3为对应该时间戳的事务标识,但因为服务器A和B的通信良好,所以服务器C稍稍落后并不会导致整个系统变慢,服务器A和B可以提交事务到<1,6>)。

【图10-1:含有3个服务器的集群将要扩展到5个】
clipboard.png

现在,假设将所有服务停止,添加服务器D和E到集群中,当然这两个新的服务器并不存在任何状态信息,重新配置了服务器A、B、C、D、E成为更大的集群并启动集群恢复服务,因为现在有了五个服务器,至少需要三个服务器组成一个法定人数,而服务器C、D、E足够构成法定人数,因此在图10-2中看到当这些服务器构成法定人数并开始同步时都发生了什么。这个场景可以简单重现,如果服务器A和B的启动慢一些,比如服务器A和B比其他三个服务器的启动晚一些。一旦新的法定人数开始同步,服务器A和B就会与服务器C进行同步,因为法定人数中服务器C的状态为最新状态,法定人数的三个成员服务器会同步到最后的事务<1,3>,而不会同步<1,4>、<1,5>和<1,6>这三个事务,因为服务器A和B并未构成法定人数的成员。

【图10-2:5个服务器的集群的法定人数为3】
clipboard.png

因为已经构成一个活跃的法定人数,这些服务器可以开始提交新的事务,假设有两个事务:<2,1>和<2,2>,如图10-3所示,当服务器A和B启动后连接到服务器C后,服务器C作为群首欢迎其加入到集群之中,并在收到事务<2,1>和<2,2>后立即告知服务器A和B删除事务<1,4>、<1,5>和<1,6>。
图10-3:5个服务器的集群丢失数据
clipboard.png

这个结果非常糟糕,丢失了某些状态信息,而且状态副本与客户端所看到的<1,4>、<1,5>、<1,6>也不再一致。为了避免这个问题,ZooKeeper提供了重配置操作,这意味着运维人员并不需要手工进行重配置操作而导致状态信息的破坏,而且,也不需要停止任何服务。

【重配置】不仅可以改变集群成员配置,还可以修改网络参数配
置,因为ZooKeeper中配置信息的变化,需要将重配置参数与静态的配置文件分离,单独保存为一个配置文件并自动更新该文件。dynamicConfigFile参数和链接这两个配置文件。

【使用动态配置之前,回顾一下之前的配置文件:】

tickTime=2000
initLimit=10
syncLimit=5
dataDir=./data
dataLogDir=./txnlog
clientPort=2182
server.1=127.0.0.1:2222:2223
server.2=127.0.0.1:3333:3334
server.3=127.0.0.1:4444:4445

【现在,将配置文件修改为动态配置方式:】

tickTime=2000
initLimit=10
syncLimit=5
dataDir=./data
dataLogDir=./txnlog
dynamicConfigFile=./dyn.cfg

【注意】,甚至从配置文件中删除了clientPort参数配置,在dyn.cfg文件由服务器项的配置组成,同时还多了一些配置,服务器项的配置形式如下:

server.id=host:n:n[:role];[client_address:]client_port

与正常的配置文件一样,列出了每个服务器的主机名和端口号用于法定人数和群首选举消息。

  • role选项:必须为participant或observer,如果忽略role选项,默认为participant
  • client_port:还指定了client_port(用于客户端连接的服务器端口号),以及该服务器需要绑定的特定网络接口地址,因为从静态配置文件中删除了clientPort参数,所以在这里添加该配置。

【最终的dyn.cfg配置文件如下所示】:

server.1=127.0.0.1:2222:2223:participant;2181
server.2=127.0.0.1:3333:3334:participant;2182
server.3=127.0.0.1:4444:4445:participant;2183

使用重配置之前必须先创建这些文件,一旦这些文件就绪,就可以通过reconfig操作来重新配置一个集群,该操作可以增量或全量(整体)地进行更新操作。

增量的重配置操作将会形成两个列表:

  • 待删除的服务器列表
    待删除的服务器列表仅仅是一个逗号分隔服务器ID列表
  • 待添加的服务器项的列表
    待添加的服务器项列表为逗号分隔的服务器项列表,每个服务器项的形式为动态配置文件中所定义的形式。例如:
reconfig -remove 2,3 -add \
server.4=127.0.0.1:5555:5556:participant;2184,\
server.5=127.0.0.1:6666:6667:participant;2185

该命令将会删除服务器2和3,添加服务器4和5。该操作成功执行还需要满足某些条件:

  • 首先,与其他ZooKeeper操作一样,原配置中法定人数必须处于活动状态;
  • 其次,新的配置文件中构成的法定人数也必须处于活动状态。

【注意:通过重配置从一个服务器到多个服务器】

当只有一个单独的ZooKeeper服务器,该服务器以独立模式运行,这种情况稍微复杂一些,因为重配置不仅改变了法定人数组成的元素,同时还会切换原来的服务器模式从独立模式到仲裁模式,所以,不允许以独立模式运行重配置操作,只有在仲裁模式时才可以使用重配置功能。
ZooKeeper一次允许一个配置的变更操作请求,当然,配置操作会非常快地被处理,而且重新配置也很少发生,所以并发的重配置操作应该不是什么问题。

还可以使用-file参数来指定一个新的成员配置文件来进行一次全量更新。例如:reconfig-file newconf命令会产生如上面命令一样的增量操作结果,newconf文件为:

server.1=127.0.0.1:2222:2223:participant;2181
server.4=127.0.0.1:5555:5556:participant;2184
server.5=127.0.0.1:6666:6667:participant;2185
通过-members参数,后跟服务器项的列表信息,可以代替-file参数进行全量更新配置操作。
最后【-v】,所有形式的reconfig的为重新配置提供了条件,如果通过-v参数提供了配置版本号,reconfig命令会在执行前确认配置文件当前的版本号是否匹配,只有匹配才会成功执行。可以通过读取zookeeper/config节点来获取当前配置的版本号,或通过zkCli工具来调用config获取配置版本号信息。

客户端连接串的管理

客户端也涉及一些相关的配置问题:连接串。
客户端连接串常常表示为逗号分隔的host:port对,其中host为主机名或IP地址,通过主机名可以提供服务器实际IP与所访问的服务器的标识符之间的间接层的对应关系。
例如,运维人员可以替换ZooKeeper服务为另一个,而不需要改变客户端的配置。
不过,该灵活性有一定限制,运维人员可以改变组成集群的服务器机器,但不能改变客户端所使用的服务器。
例如,如图10-4所示,ZooKeeper可以通过重配置很简单地将集群从三个服务器扩展到五个服务器,但客户端仍然使用三个服务器,而不是五个。

【图10-4:集群从三个到五个服务器时,客户端的重配置】
clipboard.png

另一种方式可以使ZooKeeper的服务器数量更具弹性,而不需要改变客户端的配置。对主机名很自然地想到可以解析为一个IP地址,但实际上,一个主机名可以解析为多个地址,如果主机名解析为多个IP地址,ZooKeeper就可以连接到其中的任何地址,在图10-4中,假设服务器zk-a、zk-b和zk-c,解析为三个独立的IP地址:10.0.0.1、10.0.0.2和10.0.0.3,现在假设通过DNS配置了一个单独的主机名:zk,解析为这三个IP地址,只需要修改DNS的解析地址数量,之后启动的任何客户端都可以访问这五个服务器,如图10-5所示。

【图10-5:集群从三个到五个服务器时,使用DNS对客户端的重配置】
clipboard.png

在使用主机名解析为多个地址方式时,还有一些注意事项:

  • 首先,所有的服务器必须使用相同的客户端端口号;
  • 其次,主机名解析只有在创建连接时才会发生,所以已经连接的客户端无法知道最新的名称解析,只能对新创建的ZooKeeper客户端生效。
【路径信息】:
客户端的连接还可以包含路径信息,该路径指示了解析路径名称时的根路径,其行为与UNIX系统中的chroot命令相似,而且在ZooKeeper社区中也会经常听到人们以“chroot”来称呼这个功能。
例如,如果客户端的连接串为zk:2222/app/superApp,当客户端连接并执行getData("/a.dat",...)操作时,实际客户端会得到/app/superApp/a.dat节点的数据信息(注意,连接串中指示的路径必须存在,而不会创建连接串中所指示的路径)。

在连接串中添加路径信息的动机在于一个ZooKeeper集群为多个应用程序提供服务,这样不需要要求每个应用程序添加其路径的前缀信息。每个应用程序可以类似名称独享似的使用ZooKeeper集群,运维人员可以按他们的期望来划分命名空间。
图10-6的示例展示了不同的连接串可以为客户端应用程序提供不同的根入口点。

【图10-6:通过连接串指定ZooKeeper客户端的根节点】
clipboard.png

【注意:连接串的重叠】
当管理客户端连接串时,注意一个客户端的连接串永远不要包含两个不同的ZooKeeper集群的主机名,这是最快速也是最简单导致脑裂问题的方式。

四、配额管理

ZooKeeper的另一个可配置项为配额,ZooKeeper初步提供了znode节点数量和节点数据大小的配额管理的支持。可以通过配置来指定某个子树的配额,该子树就会被跟踪,如果该子树超过了配额限制,就会记录一条警告日志,但操作请求还是可以继续执行。此时,ZooKeeper会检测是否超过了某个配额限制,但不会阻止处理流程。

配额管理的跟踪功能通过/zookeeper子树完成,所以应用程序不能在这个子树中存储自己的数据,这个子树只应该保留给ZooKeeper使用,而 /zookeeper/quota 节点就是ZooKeeper管理配额的节点。为了对应用程序/application/superApp创建一个配额项,需要在application/superApp节点下创建两个子节点zookeeper_limits和zookeeper_stats。

对于znode节点数量的限制称之为count,而对于节点数据大小的限制则为bytes。在zookeeper_limits和zookeeper_stats节点中通过count=n,bytes=m来指定配额,其中n和m均为整数,在zookeeper_limits节点中,n和m表示将会触发警告的级别(如果配置为-1就不会触发警告信息),在zookeeper_stats借点中,n和m分别表示当前子树中的节点数量和子树节点的数据信息的当前大小。

【注意:对元数据的配额跟踪】

对于子树节点数据的字节数配额跟踪功能,并不会包含每个znode节点的元数据的开销,元数据的大小大约100字节,所以如果每个节点的数据大小都比较小,跟踪znode节点的数量比跟踪znode数据的大小更加实用。

可以使用zkCli来创建/application/superApp节点,并配置配额限制:

[zk: localhost:2181(CONNECTED) 2] create /application ""
Created /application
[zk: localhost:2181(CONNECTED) 3] create /application/superApp super
Created /application/superApp
[zk: localhost:2181(CONNECTED) 4] setquota -b 10 /application/superApp
Comment: the parts are option -b val 10 path /application/superApp
[zk: localhost:2181(CONNECTED) 5] listquota /application/superApp
absolute path is /zookeeper/quota/application/superApp/zookeeper_limits
Output quota for /application/superApp count=-1,bytes=10
Output stat for /application/superApp count=1,bytes=5
创建了/application/superApp节点,且该节点的数据为5个字节(一个单词“super”),之后为/application/superApp节点设置了配额限制为10个字节,当列出/application/superApp节点配置限制是,发现数据大小的配额还有5个字节的余量,而并未对这个子树设置znode节点数量的配额限制,因为配额中count的值为-1。

如果发送命令get/zookeeper/quota/application/superApp/zookeeper_stats,可以直接访问该节点数据,而不需要使用zkCli工具,事实上,可以通过创建或删除这些节点来创建或删除配额配置。如果运行以下命令:

create /application/superApp/lotsOfData ThisIsALotOfData
就会在日志中看到如下信息:
Quota exceeded: /application/superApp bytes=21 limit=10

五、多租赁配置

配额,提供了配置选项中的某些限制措施,而ACL策略更值得我们考虑如何使用ZooKeeper来服务于多租赁(multitenancy)情况。满足多租赁的一些令人信服的原因如下:

  • 为了提供可靠的服务器,ZooKeeper服务器需要运行于专用的硬件设备之上,跨多个应用程序共享这些硬件设备更容易符合资本投资的期望。
  • 发现,在大多数情况下,ZooKeeper的流量非常具有突发性:配置或状态的变化的突发操作会导致大量的负载,从而导致服务长时间的不可用。如果是没有什么关联的应用程序的突发操作,将这些应用程序共享这个服务器更能有效利用硬件资源。不过还是要注意失联事件发生时所产生的峰值,某些写得不太规范的应用程序在处理Disconnected事件时,产生的负载高于其所需要的资源。
  • 对于硬件资源的分摊,可以获得更好的故障容错性:如果两个应用程序,从之前各自三个服务器的集群中转移到一个由5台服务器组成的集群,总量上所使用的服务器更少了,对ZooKeeper也可以容忍两台服务器的故障,而不是之前的只能容忍一个服务器故障。

当服务于多租赁的情况下时,运维人员一般会将数据树分割为不同的子树,每个子树为某个应用程序所专用。开发人员在设计应用程序时可以考虑在其所用的znode节点前添加前缀,但还有一个更简单的方法来隔离各个应用程序:在连接串中指定路径部分,10.3节中介绍了这方式。每个应用程序的开发人员在进行应用程序的开发时,就像使用专用的ZooKeeper服务一样。如果运维人员决定将应用程序部署到根路径/application/newapp之下,应用程序可以使用host:port/application/newapp连接串,而不仅仅是host:port,通过这种方式,对应用程序所呈现的犹如使用专用服务一样,与此同时,运维人员还可以为/application/newapp节点配置配额限制,以便跟踪应用程序的空间使用情况。

六、文件系统布局和格式

数据存储有两类:事务日志文件和快照文件。
这些文件均以普通文件的形式保存到本地文件系统中,在进行关键路径的事务处理时就会写入事务日志文件,所以强烈建议将这些文件保存到一个专用存储设备上

快照文件将会被写入到DataDir参数所指定的目录中,而事务日志文件将会被写入到DataLogDir参数所指定的目录中。

首先,看一下事务日志目录中的文件,如果列出该目录的信息,会发现只有一个子目录,名为version-2,对日志和快照的格式只做出了一次重大改进,当改变其格式后,发现,将数据通过文件版本进行分离,对于处理版本间的数据迁移是非常有用的。

1、事务日志

在运行一些小测试后的目录内容,有两个事务日志文件:

-rw-r--r-- 1 breed 67108880 Jun 5 22:12 log.100000001
-rw-r--r-- 1 breed 67108880 Jul 15 21:37 log.200000001
可以仔细观察这些文件信息。首先,考虑到测试很少,而这些文件却非常大(每个都超过6MB);其次这些文件名的后缀中均有一个很大数字。

ZooKeeper为文件预分配大的数据块,来避免每次写入所带来的文件增长的元数据管理开销,如果通过对这些文件进行十六进制转储打印,会发现这些文件中全部以null字符(0)填充,只有在最开始部分有少量的二进制数据,服务器运行一段时间后,其中的null字符逐渐被日志数据替换。

日志文件中包含事务标签zxid,但为了减轻恢复负载,而且为了快速查找,每个日志文件的后缀为该日志文件中第一个zxid的十六进制形式。通过十六进制表示zxid的一个好处就是可以快速区分zxid中时间戳部分和计数器部分,所以在之前例子中的第一个文件的时间戳为1,而第二个文件的时间戳为2。

不过,还想继续看一看文件中保存了什么内容,对于问题诊断也非常有帮助。有时,开发人员宣称ZooKeeper丢失了某些znode节点信息,此时只有通过查找事务日志文件才可以知道客户端具体删除过哪些节点。

可以通过一下命令来查看第二个日志文件:
java -cp $ZK_LIBS org.apache.zookeeper.server.LogFormatter version-2 log.200000001
这个命令的输出信息如下:
7/15/13... session 0x13...00 cxid 0x0 zxid 0x200000001 createSession 30000
7/15/13... session 0x13...00 cxid 0x2 zxid 0x200000002 create
'/test,#22746573746 ...
7/15/13... session 0x13...00 cxid 0x3 zxid 0x200000003 create
'/test/c1,#6368696c ...
7/15/13... session 0x13...00 cxid 0x4 zxid 0x200000004 create
'/test/c2,#6368696c ...
7/15/13... session 0x13...00 cxid 0x5 zxid 0x200000005 create
'/test/c3,#6368696c ...
7/15/13... session 0x13...00 cxid 0x0 zxid 0x200000006 closeSession null
每个日志文件中的事务均以可读形式一行行地展示出来。因为只有变更操作才会被记录到事务日志,所以在事务日志中不会看到任何读事务操作。

2、快照

快照文件的命名规则与事务日志文件的命名规则相似,以下为之前例子中的服务器的快照列表信息:
-rw-r--r-- 1 br33d 296 Jun 5 07:49 snapshot.0
-rw-r--r-- 1 br33d 415 Jul 15 21:33 snapshot.100000009
快照文件并不会被预分配空间,所以文件大小也更加准确地反映了其中包含的数据大小。其中后缀表示快照开始时当时的zxid值,之前已经介绍过,快照文件实际上为一个模糊快照,直到事务日志重现之后才会成为一个有效的快照文件。因此在恢复系统时,必须从快照后缀的zxid开始重现事务日志文件,甚至更早的zxid开始重现事务。

快照文件中保存的模糊快照信息同样为二进制格式,因此,可以通过另一个工具类来检查快照文件的内容:

java -cp ZK_LIBS org.apache.zookeeper.server.SnapshotFormatter version-2 /snapshot.100000009
这个命令的输出信息如下:
----
/
cZxid = 0x00000000000000
ctime = Wed Dec 31 16:00:00 PST 1969
mZxid = 0x00000000000000
mtime = Wed Dec 31 16:00:00 PST 1969
pZxid = 0x00000100000002
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x00000000000000
dataLength = 0
----
/sasd
cZxid = 0x00000100000002
ctime = Wed Jun 05 07:50:56 PDT 2013
mZxid = 0x00000100000002
mtime = Wed Jun 05 07:50:56 PDT 2013
pZxid = 0x00000100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x00000000000000
dataLength = 3
----
....
只有每个节点的元数据被转储打印出来,这样,运维人员就可以知道一个znode节点何时发生了变化,以及哪个znode节点占用了大量内存。很遗憾,数据信息和ACL策略并没有出现在输出中,因此,在进行问题诊断时,记住将快照中的信息与日志文件的信息结合起来分析问题所在。

3、时间戳文件

ZooKeeper的持久状态由两个小文件构成,它们是两个时间戳文件,其文件名为acceptedEpoch和currentEpoch,之前已经讨论过时间戳的概念,而这两个文件则反映了某个服务器进程已接受的和正在处理的信息。虽然这两个文件并不包含任何应用数据信息,但对于数据一致性却至关重要,所以如果你在备份一个ZooKeeper服务器的原始数据文件时,不要忘了这两个文件。

4、已保存的ZooKeeper数据的应用

ZooKeeper数据存储的一个优点是,不管独立模式的服务器还是集群方式的服务器,数据的存储方式都一样。之前已经介绍过通过事务日志和快照的合并可以得到准确的数据视图,可以将事务日志文件和快照文件拷贝到另一台设备上进行这些操作(例如在你的便携电脑中),将这些文件放到一个独立模式的服务器下空白的data目录下,然后启动服务,该服务就会真实反映出你所拷贝的那个服务器上的状态信息。这项技术可以从生产环境拷贝服务器的状态信息,用于稍后的复查等用途。

同时也意味着,只需要简单地将这些数据文件进行备份,就可以轻易地完成ZooKeeper服务器的备份,如果采用这种方式进行备份,还需要注意一些问题。首先,ZooKeeper为复制服务,所以系统中存在冗余信息,如果进行备份操作,只需要备份其中一台服务器的数据信息。

当ZooKeeper服务器认可了一个事务,从这时起它就会承诺记录下该状态信息,一定要记住这一点,这一点非常重要。因此如果使用旧的备份文件恢复一个服务器,就会导致服务器违反其承诺。如果刚刚遭遇了所有服务器的数据丢失的情况,这可能不是什么大问题,但如果你的集群在正常工作中,而你将某个服务器还原为旧的状态,你的行为可能会导致其他服务器也丢失了某些信息。

如果要对全部或大多数服务器进行数据丢失的恢复操作,最好的办法是使用最新抓取的状态信息(从最新的存活服务器中获取的备份文件),并在启动服务器之前将状态信息拷贝到其他所有服务器上。

七、四字母命令

四字母命令提供的这一简单方法,可以让对系统进行各种各样的检查。四字母命令的主要目标就是提供一个非常简单的协议,使使用简单的工具,如telnet或nc,就可以完成系统健康状况检查和问题的诊断。为简单起见,四字母命令的输出也是可读形式,使得更容易使用这些命令。

对服务器添加一个新的命令也很容易,命令列表也就会增长。本节中将会介绍一些常用的命令,对于最新的全部命令列表信息,请参考ZooKeeper文档。

  • ruok
    提供(有限的)服务器的状态信息。如果服务器正在运行,就会返回imok响应信息。事实上“OK”状态只是一个相对的概念,例如,服务器运行中,虽无法与集群中其他服务器进行通信,然而该服务器返回的状态仍然是“OK”。对于更详细信息及可靠的健康状态检查,需要使用stat命令。
  • stat
    提提供了服务器的状态信息和当前活动的连接情况,状态信息包括一些基本的统计信息,还包括该服务器当前是否处于活动状态,即作为群首或追随者,该服务器所知的最后的zxid信息。某些统计信息为累计值,可以使用srst命令进行重置。
  • srvr
    提供的信息与stat一样,只是忽略了连接情况的信息。
  • dump
    提供会话信息,列出当前活动的会话信息以及这些会话的过期时间。该命令只能在群首服务器上运行。
  • conf
    列出该服务器启动运行所使用的基本配置参数。
  • envi
    列出各种各样的Java环境参数。
  • mntr
    提供了比stat命令更加详细的服务器统计数据。每行输出的格式为key<tab>value。(群首服务器还将列出只用于群首的额外参数信息)。
  • wchs
    列出该服务器所跟踪的监视点的简短摘要信息。
  • wchc
    列出该服务器所跟踪的监视点的详细信息,根据会话进行分组。
  • wchp
    列出该服务器所跟踪的监视点的详细信息,根据被设置监视点的

znode节点路径进行分组。

  • cons,crst
    cons命令列出该服务器上每个连接的详细统计信息,crst重置这些连接信息中的计数器为0。

八、通过JMX进行监控

四字母命令可以用于系统的监控,但却没有提供系统控制和修改的方法,ZooKeeper通过标准Java管理协议,JMX(Java管理扩展),提供了更强大的监控和管理功能。

本节中,将会使用一个简单的管理控制台工具jconsole来探索通过JMX管理ZooKeeper功能。

jconsole为Java中自带的工具,实际上,类似jconsole这样的JMX工具常常用于监控远程的ZooKeeper服务器,但出于说明的目的,将会在ZooKeeper服务所运行的设备运行该工具。

首先、启动第二个ZooKeeper服务器(即ID为2的服务器),之后,只需要在命令行中简单的输入jconsole命令就可以启动jconsole工具,jconsole启动后,就会看到类似图10-7中所示的窗口。

注意到其中带有“zookeeper”名称的进程,对于本地进程,jconsole会自动发现可连接的进程。
【图10-7:jconsole启动界面】
clipboard.png

现在,只需要在列表中双击该进程就可以连接到ZooKeeper进程上,因为没有启用SSL,此时会提示关于非安全连接的选项,单击非安全连接按钮,之后屏幕中就会出现图10-8所示的窗口。
【图10-8:进程管理的第一个窗口】
clipboard.png

从这个界面中看到,可以通过该工具获取关于ZooKeeper服务器的各种各样有趣的统计信息。JMX支持通过MBean(托管Bean)来将自定义信息暴露给远程管理者,虽然名字听起来比较笨拙,但却是暴露信息和操作的一个非常灵活的方式。jconsole会在最右侧的信息标签中列出进程暴露的所有MBean信息,如图10-9所示。
【图10-9:jconsole中MBean信息】
clipboard.png

在MBean列表中可以看到ZooKeeper所使用且暴露出来的组件信息,比较关心ZooKeeperService的信息,因此双击该列表项,将会看到一个分级的列表副本以及这些副本的信息,如果打开某些列表中的子项,会看到图10-10所示的信息。
【图10-10:jconsole中关于服务器2的信息】
clipboard.png

通过浏览replica.2的信息,注意到这些信息中还包括其他副本
的信息,但只是一些通信信息,因为服务器2对其他副本所知信息并不多,所以服务器2无法展示更多其他副本信息,而服务器2很了解自己的信息,所以它可以暴露更多的信息。

当启动服务器1,服务器2就可以与服务器1构成一个法定人数,此时就可以看到服务器2的更多信息。启动服务器1,之后再次通过jconsole检查服务器2信息。图10-11展示了通过JMX暴露的一些额外信息,可以看到服务器2当前角色为追随者,还可以看到数据数的信息。

图10-11还展示了服务器1的一些信息,看到,服务器1的角色为群首角色,同时还有一些额外信息,仅在群首服务器中,FollowerInfo中还会展示追随者的列表。当点击该按钮,会看到连接到服务器1的其他ZooKeeper服务器的原始列表信息。
【图10-11:jconsole中关于服务器1的信息】
clipboard.png

到目前为止,看到相比四字母命令,通过JMX所看到的信息更加优美直观,但是还没有看到有什么新功能,现在看看JMX可以做到而四字母命令无法做到的功能。启动zkCli脚本工具,连接到服务器1,之后运行以下命令:

create -e /me "foo"

通过该命令,会创建一个临时性的znode节点,图10-11所示的服务器1的JMX信息中,可以看到出现了一个关于连接的新信息项,连接的属性中列出了各种各样的信息,这些信息对于调试运行问题非常有用。这个视图中,还看到两个有意思的操作:terminateSession和terminateConnection。

terminateConnection操作会关闭ZooKeeper客户端到服务器之间的连接,而会话依然处于活跃状态,所以此时客户端还可以重新连接到另一个服务器,客户端会收到失去连接的事件,但可以轻易恢复。

与之相反,terminateSession操作会声明会话已经死亡,客户端与服务器之间的连接将会被关闭,且会话也将因过期而中止,客户端也不能再使用这个会话与其他服务器建立连接。因此,使用terminateSession操作时需要小心,因为该操作会在会话超时之前就导致会话过期,所以在该进程自己发现过期前,其他进程可能已经发现了该进程的会话死亡的情况。

远程连接

JMX代理器运行于ZooKeeper服务器的JVM之中,如果连接远程的ZooKeeper服务器,需要配置好JMX代理器。对与远程连接的JMX协议有若干参数需要配置,本节中,展示了一种JMX的配置方式,来看看JMX提供了什么样功能。如果在生产环境使用JMX,可能需要使用另一种JMX配置——具体参考如何配置更高级的安全功能。

对JMX的配置可以通过系统属性的方式进行配置,在用于启动ZooKeeper服务器的zkServer.sh脚本中,使用SERVER_JVMFLAGS环境变量来配置这些系统属性。

例如,以下面的配置启动服务,就可以远程连接服务器3的55555端口。

SERVER_JVMFLAGS="-Dcom.sun.management.jmxremote.password.file=passwd \
-Dcom.sun.management.jmxremote.port=55555 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.access.file=access"
 _path_to_zookeeper_/bin/zkServer.sh start   _path_to_server3.cfg_

系统属性参数中用到了密码文件和访问控制文件,这些文件的格式非常简单。首先,创建passwd文件,方式如下:

# user password
admin <password>

注意,密码文件中的密码信息为明文保存,因此只能给密码文件的所有者分配读写权限,如果不这样做,Java就无法启动服务。同时,关闭了SSL功能,这意味着密码信息在网络上会以明文的方式进行传输,如果需要更强的安全级别,JMX也提供了更强有力的选项,但这些已经超出本书所涉及的范围。

对于访问控制文件,将会给admin用户赋予readwrite权限,创建该文件的方式如下:

admin readwrite

九、工具

在ZooKeeper发行包的contrib目录中,可以找到一些软件,这些软件可以帮你集成ZooKeeper到其他的检测系统中去。列出了一部分最受欢迎的软件或工具:

  • 通过C绑定实现的Perl和Python语言的绑定库。
  • ZooKeeper日志可视化的软件。
  • 一个基于网页的集群节点浏览和ZooKeeper数据修改功能的软件。
  • ZooKeeper中自带的zktreeutil和guano均可以从GitHub下载。这些软件可以对ZooKeeper的数据进行导入和导出操作。
  • zktop,也可以从GitHub下载,该软件监控ZooKeeper的负载,并提供Unix的top命令类似的输出。
  • ZooKeeper冒烟测试,可以从GitHub上下载。该软件对ZooKeeper集群提供了一个简单的冒烟测试客户端,这个工具对于开发人员熟悉ZooKeeper非常不错。
查看原文

赞 0 收藏 0 评论 0

总有刁民想害朕 发布了文章 · 2018-12-11

Chapter 9 ZooKeeper 内部原理

  • 群首(leader):作为中心点处理所有对ZooKeeper系统变更的请求,它就像一个定序器,建立了所有对ZooKeeper状态的更新的顺序。
  • 追随者(follower):接收群首所发出更新操作请求,并对这些请求进行处理,以此来保障状态更新操作不会发生碰撞。
  • 观察者(observer):不会参与决策哪些请求可被接受的过程,只是观察决策的结果,观察者的设计只是为了系统的可扩展性。

一、请求、事务和标识符

  • 读请求(exists、getData和getChildren):本地处理
  • 写请求(create、delete和setData):被转发给群首,群首执行相应的请求,并形成状态的更新,称为事务(transaction)

    • 一个事务为一个单位,也就是说所有的变更处理需要以原子方式执行
    • ZooKeeper集群以事务方式运行,并确保所有的变更操作以原子方式被执行,同时不会被其他事务所干扰
    • 同时一个事务还具有幂等性
    • 当群首产生了一个事务,就会为该事务分配一个标识符,称之为ZooKeeper会话ID(zxid),通过Zxid对事务进行标识,就可以按照群首所指定的顺序在各个服务器中按序执行。
    • 服务器之间在进行新的群首选举时也会交换zxid信息,这样就可以知道哪个无故障服务器接收了更多的事务,并可以同步他们之间的状态信息。
    • zxid为一个long型(64位)整数,分为两部分:时间戳(epoch)部分和计数器(counter)部分,每个部分为32位。
    • zab协议,通过该协议来广播各个服务器的状态变更信息

二、群首选举

设置群首的目的是为了对客户端所发起的ZooKeeper状态变更请求进行排序,包括:create、setData和delete操作。

群首将每一个请求转换为一个事务,将这些事务发送给追随者,确保集群按照群首确定的顺序接受并处理这些事务。

LOOKING --> LEADING --> FOLLOWING

当一个服务器收到一个投票信息,该服务器将会根据以下规则修改自己的投票信息:

  • 1、将接收的voteId和voteZxid作为一个标识符,并获取接收方当前的投票中的zxid,用myZxid和mySid表示接收方服务器自己的值。
  • 2、如果(voteZxid > myZxid)或者(voteZxid = myZxid且voteId > mySid),保留当前的投票信息。
  • 3、否则,修改自己的投票信息,将voteZxid赋值给myZxid,将voteId赋值给mySid。
简而言之,只有最新的服务器将赢得选举,因为其拥有最近一次的zxid。

【图9-1:群首选举过程的示例】
clipboard.png

图9-1展示了三个服务器,这三个服务器分别以不同的初始投票值开始,其投票值取决于该服务器的标识符和其最新的zxid。每个服务器会收到另外两个服务器发送的投票信息,在第一轮之后,服务器s2和服务器s3将会改变其投票值为(1,6),之后服务器服务器s2和服务器s3在改变投票值之后会发送新的通知消息,在接收到这些新的通知消息后,每个服务器收到的仲裁数量的通知消息拥有一样的投票值,最后选举出服务器s1为群首。

【图9-2:消息交错导致一个服务器选择了另一个群首】
clipboard.png

在图9-2中,展示了另一种情况的例子。服务器 s2 做出了错误判断,选举了另一个服务器 s3 而不是服务器 s1 ,虽然 s1 的zxid值更高,但在从服务器 s1 向服务器 s2 传送消息时发生了网络故障导致长时间延迟,与此同时,服务器 s2 选择了服务器s 3 作为群首,最终,服务器 s1 和服务器 s3 组成了仲裁数量(quorum),并将忽略服务器 s2 。

【图9-3:群首选举时的长延迟】
clipboard.png

如果想实现一个新的群首选举的算法,需要实现一个quorum 包中的Election接口。为了可以让用户自己选择群首选举的实现,代码中使用了简单的整数标识符(请查看代码中QuorumPeer.createElectionAlgorithm()),另外两种可选的实现方式为LeaderElection类和AuthFastLeaderElection类

三、Zab:状态更新的广播协议

在接收到一个写请求操作后,追随者会将请求转发给群首,群首将探索性地执行该请求,并将执行结果以事务的方式对状态更新进行广播。

一个事务中包含服务器需要执行变更的确切操作,当事务提交时,服务器就会将这些变更反馈到数据树上,其中数据树为ZooKeeper用于保存状态信息的数据结构(请参考DataTree类)。

之后需要面对的问题便是服务器如何确认一个事务是否已经提交,由此引入了所采用的协议:
Zab:ZooKeeper原子广播协议(ZooKeeper Atomic Broadcast protocol)。

假设现在有一个活动的群首服务器,并拥有仲裁数量的追随者支持该群首的管理权,通过该协议提交一个事务非常简单,类似于一个两阶段提交。

【图9-4:提交提案的常规消息模式】
clipboard.png

  • 1、群首向所有追随者发送一个PROPOSAL消息p。
  • 2、当一个追随者接收到消息p后,会响应群首一个ACK消息,通知群首其已接受该提案(proposal)。
  • 3、当收到仲裁数量的服务器发送的确认消息后(该仲裁数包括群首自己),群首就会发送消息通知追随者进行提交(COMMIT)操作。
在应答提案消息之前,追随者还需要执行一些检查操作。追随者将会检查所发送的提案消息是否属于其所追随的群首,并确认群首所广播的提案消息和提交事务消失的顺序正确。

【Zab保障了以下几个重要属性:】

  • 如果群首按顺序广播了事务T和事务T,那么每个服务器在提交T事务前保证事务T已经提交完成。
  • 如果某个服务器按照事务T、事务T的顺序提交事务,所有其他服务器也必然会在提交事务T前提交事务T。

第一个属性保证事务在服务器之间的传送顺序的一致,而第二个竖向地保证服务器不会跳过任何事务。

多个并发的群首可能会导致服务器提交事务的顺序发生错误,或者直接跳过了某些事务。为了阻止系统中同时出现两个服务器自认为自己是群首的情况是非常困难的,时间问题或消息丢失都可能导致这种情况,因此广播协议并不能基于以上假设。

【为了解决这个问题,Zab协议提供了以下保障】:

  • 一个被选举的群首确保在提交完所有之前的时间戳内需要提交的事务,之后才开始广播新的事务。
  • 在任何时间点,都不会出现两个被仲裁支持的群首。

第一个需求,群首并不会马上处于活动状态,直到确保仲裁数量的服务器认可这个群首新的时间戳值。一个时间戳的最初状态必
须包含所有的之前已经提交的事务,或者某些已经被其他服务器接受,但尚未提交完成的事务。这一点非常重要,在群首进行时间戳e的任何新的提案前,必须保证自时间戳开始值到时间戳e-1内的所有提案被提交。如果一个提案消息处于时间戳e'<e,在群首处理时间戳e的第一个提案消息前没有提交之前的这个提案,那么旧的提案将永远不会被提交。

第二个需求有些棘手,因为并不能完全阻止两个群首独立地运行。假如一个群首l管理并广播事务,在此时,仲裁数量的服务器Q判断群首l已经退出,并开始选举了一个新的群首l',假设在仲裁机构Q放弃群首l时有一个事务T正在广播,而且仲裁机构Q的一个严格的子集记录了这个事务T,在群首l'被选举完成后,在仲裁机构Q之外服务器也记录了这个事务T,为事务T形成一个仲裁数量,在这种情况下,事务T在群首l'被选举后会进行提交。不用担心这种情况,这并不是个bug,Zab协议保证T作为事务的一部分被群首l'提交,确保群首l'的仲裁数量的支持者中至少有一个追随者确认了该事务T,其中的关键点在于群首l'和l在同一时刻并未获得足够的仲裁数量的支持者。

【图9-5:群首发生重叠的情况】
clipboard.png

在图中,群首l为服务器s 5 ,l'为服务器s 3 ,仲裁机构由s 1 到s 3 组成,事务T的zxid为(1,1)。在收到第二个确认消息之后,服务器s 5 成功向服务器s 4 发送了提交消息来通知提交事务。

之前提到Zab保证新群首l'不会缺失(1,1),现在来看看其中的细节。在新群首l'生效前,它必须学习旧的仲裁数量服务器之前
接受的所有提议,并且保证这些服务器不会继续接受来自旧群首的提议。此时,如果群首l还能继续提交提议,比如(1,1),这条提议必须已经被一个以上的认可了新群首的仲裁数量服务器所接受。我们知道仲裁数量必须在一台以上的服务器之上有所重叠,这样群首l'用来提交的仲裁数量和新群首l使用的仲裁数量必定在一台以上的服务器上是一致的。因此,l'将(1,1)加入自身的状态并传播给其跟随者。

在群首选举时,选择zxid最大的服务器作为群首。这使得ZooKeeper不需要将提议从追随者传到群首,而只需要将状态从群首传
播到追随者。假设有一个追随者接受了一条群首没有接受的提议。群首必须确保在和其他追随者同步之前已经收到并接受了这条提议。但是,如果选择zxid最大的服务器,将可以完完全全跳过这一步,可以直接发送更新到追随者。

在时间戳发生转换时,Zookeeper使用两种不同的方式来更新追随者来优化这个过程:

  • DIFF :追随者滞后于群首不多,群首只需要发送缺失的事务点。因为追随者按照严格的顺序接收事务点,这些缺失的事务点

永远是最近的。

  • SNAP:追随者滞后很久,因为发送完整的快照会增大系统恢复的延时,发送缺失的事务点是更优的选择。可是当追随者滞后太远的情况下,只能选择发送完整快照。

四、观察者

观察者和追随者之间有一些共同点。具体说来,他们提交来自群首的提议。不同于追随者的是,观察者不参与我们之前介绍过的选举过程。他们仅仅学习经由INFORM消息提交的提议。由于群首将状态变化发送给追随者和观察者,这两种服务器也都被称为学习者。

【注意:深入INFORM消息】

因为观察者不参与决定提议接受与否的投票,群首不需要发送提议到观察者,群首发送给追随者的提交消息只包含zxid而不包含提议本身。因此,仅仅发送提交消息给观察者并不能使其实施提议。这是我们使用INFORM消息的原因。INFORM消息本质上是包含了正在被提交的提议信息的提交消息。

简单来说,追随者接受两种消息而观察者只接受一种消息。追随者从一次广播中获取提议的内容,并从接下来的一条提交消息中获取
zxid。相比之下,观察者只获取一条包含已提交提议的内容的INFORM消息。

参与决定那条提议被提交的投票的服务器被称为PARTICIPANT服务器。一个PARTICIPANT服务器可以是群首也可以是追随者。而观察者则被称为OBSERVER服务器。

  • 引入观察者的一个主要原因是提高读请求的可扩展性。
    通过加入多个观察者,可以在不牺牲写操作的吞吐率的前提下服务更多的读操作。写操作的吞吐率取决于仲裁数量的大小。如果加入更多的参与投票的服务器,将需要更大的仲裁数量,而这将减少写操作的吞吐率。增加观察者也不是完全没有开销的。每一个新加入的观察者将对应于每一个已提交事务点引入的一条额外消息。然而,这个开销相对于增加参与投票的服务器来说小很多。
  • 采用观察者的另外一个原因是进行跨多个数据中心的部署。
    由于数据中心之间的网络链接延时,将服务器分散于多个数据中心将明显地降低系统的速度。引入观察者后,更新请求能够先以高吞吐率和低延迟的方式在一个数据中心内执行,接下来再传播到异地的其他数据中心得到执行。值得注意的是,观察者并不能消除数据中心之间的网络消息,因为观察者必须转发更新请求给群首并且处理INFORM消息。不同的是,当参与的服务器处于同一个数据中心时,观察者保证提交更新必需的消息在数据中心内部得到交换。

五、服务器的构成

群首、追随者和观察者根本上都是服务器。
在实现服务器时使用的主要抽象概念是请求处理器。请求处理器是对处理流水线上不同阶段的抽象。每一个服务器实现了一个请求处理器的序列。可以把一个处理器想象成添加到请求处理的一个元素。一条请求经过服务器流水线上所有处理器的处理后被称为得到完全处理。

【注意:请求处理器】
ZooKeeper代码里有一个叫RequestProcessor的接口。
这个接口的主要方法是processRequest,它接受一个Request参数。在一条请求处理器的流水线上,对相邻处理器的请求的处理通常通过队列现实解耦合。当一个处理器有一条请求需要下一个处理器进行处理时,它将这条请求加入队列。然后,它将处于等待状态直到下一个处理器处理完此消息。

1、独立服务器

Zookeeper中最简单的流水线是独立服务器(ZeeKeeperServer类)。图9-6描述了此类服务器的流水线。它包含三种请求处理器:PrepRequestProcessor、SyncRequestProcessor和FinalRequestProcessor。
【图9-6:一个独立服务器的流水线】:
clipboard.png

  • PrepRequestProcessor:接受客户端的请求并执行这个请求,处理结果则是生成一个事务。我们知道事务是执行一个操作的结果,该操作会反映到ZooKeeper的数据树上。事务信息将会以头部记录和事务记录的方式添加到Request对象中。同时还要注意,只有改变ZooKeeper状态的操作才会产生事务,对于读操作并不会产生任何事务。因此,对于读请求的Request对象中,事务的成员属性的引用值则为null。
  • SyncRequestProcessor:负责将事务持久化到磁盘上。实际上就是将事务数据按顺序追加到事务日志中,并生成快照数据。
  • FinalRequestProcessor:如果Request对象包含事务数据,该处理器将会接受对ZooKeeper数据树的修改,否则,该处理器会从数据树中读取数据并返回给客户端。

2、群首服务器

当切换到仲裁模式时,服务器的流水线则有一些变化,首先我们群首的操作流水线(类LeaderZooKeeper),如图9-7所示。
【图9-7:群首服务器的流水线】
clipboard.png

  • PrepRequestProcessor:
  • ProposalRequestProcessor:该处理器会准备一个提议,并将该提议发送给跟随者。ProposalRequestProcessor将会把所有请求都转发给CommitRequestProcessor,而且,对于写操作请求,还会将请求转发给SyncRequestProcessor处理器。
  • SyncRequestProcessor:处理器所执行的操作与独立服务器中的一样,即持久化事务到磁盘上。执行完之后会触发AckRequestProcessor处理器。
  • AckRequestProcessor:这个处理器是一个简单请求处理器,它仅仅生成确认消息并返回给自己。之前曾提到过,在仲裁模式下,群首需要收到每个服务器的确认消息,也包括群首自己,而AckRequestProcessor处理器就负责这个。
  • CommitRequestProcessor:CommitRequestProcessor会将收到足够多的确认消息的提议进行提交。实际上,确认消息是由Leader类处理的(Leader.processAck()方法),这个方法会将提交的请求加入到CommitRequestProcessor类中的一个队列中。这个队列会由请求处理器线程进行处理。
  • FinalRequestProcessor:它的作用与独立服务器一样。FinalRequestProcessor处理更新类型的请求,并执行读取请求。在FinalRequestProcessor处理器之前还有一个简单的请求处理器,这个处理器会从提议列表中删除那些待接受的提议,这个处理器的名字叫ToBeAppliedRequestProcessor。待接受请求列表包括那些已经被仲裁法定人数所确认的请求,并等待被执行。群首使用这个列表与追随者之间进行同步,并将收到确认消息的请求加入到这个列表中。之后ToBeAppliedRequestProcessor处理器就会在FinalRequestProcessor处理器执行后删除这个列表中的元素。
    注意,只有更新请求才会加入到待接受请求列表中,然后由ToBeAppliedRequest-Processor处理器从该列表移除。ToBeAppliedRequestProcessor处理器并不会对读取请求进行任何额外的处理操作,而是由FinalRequestProcessor处理器进行操作。

3、追随者和观察者服务器

【图9-8:追随者服务器的流水线】
clipboard.png

  • FollowerRequestProcessor:首先从FollowerRequestProcessor处理器开始,该处理器接收并处理客户端请求。FollowerRequestProcessor处理器之后转发请求给CommitRequestProcessor,同时也会转发写请求到群首服务器。
  • CommitRequestProcessor:会直接转发读取请求到FinalRequestProcessor处理器,而且对于写请求,CommitRequestProcessor在转发给FinalRequestProcessor处理器之前会等待提交事务。
为了保证执行的顺序,CommitRequestProcessor处理器会在收到一个写请求处理器时暂停后续的请求处理。这就意味着,在一个写请求之后接收到的任何读取请求都将被阻塞,直到读取请求转给CommitRequestProcessor处理器。通过等待的方式,请求可以被保证按照接收的顺序来被执行。
  • SyncRequestProcessor:当群首接收到一个新的写请求操作时,直接地或通过其他追随者服务器来生成一个提议,之后转发到追随者服务器。当收到一个提议,追随者服务器会发送这个提议到SyncRequestProcessor处理器。
  • SendRequestProcessor:会向群首发送确认消息。当群首服务器接收到足够确认消息来提交这个提议时,群首就会发送提交事务消息给追随者(同时也会发送INFORM消息给观察者服务器)。当接收到提交事务消息时,追随者就通过CommitRequestProcessor处理器进行处理。

六、本地存储

1、日志和磁盘的使用

服务器通过事务日志来持久化事务。在接受一个提议时,一个服务器(追随者或群首服务器)就会将提议的事务持久化到事物日志中,该事务日志保存在服务器的本地磁盘中,而事务将会按照顺序追加其后。服务器会时不时地滚动日志,即关闭当前文件并打开一个
新的文件。

【组提交和补白】:因为写事务日志是写请求操作的关键路径,因此ZooKeeper必须有效处理写日志问题。一般情况下追加文件到磁盘都会有效完成,但还有一些情况可以使ZooKeeper运行的更快,组提交和补白。组提交(GroupCommits)是指在一次磁盘写入时追加多个事务。这将使持久化多个事物只需要一次磁道寻址的开销。

【冲刷(Flush)事务到磁盘介质】:冲刷在这里就是指告诉操作系统将脏页写入磁盘,并在操作完成后返回。因为在SyncRequestProcessor处理器中持久化事务,所以这个处理器同时也会负责冲刷。在SyncRequestProcessor处理器中当需要冲刷事务到磁盘时,事实上是冲刷的是所有队列中的事务,以实现组提交的优化。如果队列中只有一个事务,这个处理器依然会执行冲刷。该处理器并不会等待更多的事务进入队列,因为这样做会增加执行操作的延时。

【补白(padding)】:指在文件中预分配磁盘存储块。这样做,对于涉及存储块分配的文件系统元数据的更新,就不会显著影响文件的顺序写入操作。假如需要高速向日志中追加事务,而文件中并没有原先分配存储块,那么无论何时在写入操作到达文件的结尾,文件系统都需要分配一个新存储块。而通过补白至少可以减少两次额外的磁盘寻址开销:一次是更新元数据;另一次是返回文件。

2、快照

快照是ZooKeeper数据树的拷贝副本,每一个服务器会经常以序列化整个数据树的方式来提取快照,并将这个提取的快照保存到文件中。服务器在进行快照时不需要进行协作,也不需要暂停处理请求。因为服务器在进行快照时还会继续处理请求,所以当快照完成时,数据树可能又发生了变化,称这样的快照是模糊的(fuzzy),因为它们不能反映出在任意给点的时间点数据树的准确状态。

【举例说明】

一个数据树中只有2个znode节点:/z和/z'。一开始,两个znode节点的数据都是1。
现在有以下操作步骤:
1、开始一个快照。
2、序列化并将/z=1到到快照。
3、使/z的数据为2(事务T)。
4、使/z'的数据为2(事务T')。
5、序列化并将/z'=2写入到快照。

这个快照包含了/z=1和/z'=2。然而,数据树中这两个znode节点在任意的时间点上都不是这个值。这并不是问题,因为服务器会重播(replay)事务。每一个快照文件都会以快照开始时最后一个被提交的事务作为标记(tag),将这个时间戳记为TS。如果服务器最后加载快照,它会重播在TS之后的所有事务日志中的事务。在这个例子中,它们就是T和T。在快照的基础上重放T和T'后,服务器最终得到/z=2和/z'=2,即一个合理的状态。

接下来还需要考虑一个重要的问题,就是再次执行事务T'是会有问题,因为这个事务在开始快照开始之后已经被接受,而结果也被
快照中保存下来。就像之前所说的,事务是幂等的(idempotent),所以即使按照相同的顺序再次执行相同的事务,也会得到相同的结果,即便其结果已经保存到快照中。

为了理解这个过程,假设重复执行一个已经被执行过的事务。如上例中所描述,一个操作设置某个znode节点的数据为一个特定的值,这个值并不依赖于任何其他东西,无条件(unconditionly)地设置/z'的值(setData请求中的版本号为-1),重新执行操作成功,但因为递增了两次,所以最后以错误的版本号结束。
如以下方式就会导致问题出现,假设有如下3个操作并成功执行:

setData /z', 2, -1
setData /z', 3, 2
setData /a, 0, -1
第一个setData操作跟之前描述的一样,而后又加上了2个setData操作,以此来展示在重放中第二个操作因为错误的版本号而未能
成功的情况。假设这3个操作在提交时被正确执行。此时如果服务器加载最新的快照,即该快照已包含第一个setData操作。服务器仍然会重放第一个setData操作,因为快照被一个更早的zxid所标记。因为重新执行了第一个setData操作。而第二个setData操作的版本号又与期望不符,那么这个操作将无法完成。而第三个setData操作可以正常完成,因为它也是无条件的。

在加载完快照并重放日志后,此时服务器的状态是不正确的,因为它没有包括第二个setData请求。这个操作违反了持久性和正确性,以及请求的序列应该是无缺口(no gap)的属性。

这个重放请求的问题可以通过把事务转换为群首服务器所生成的state delta来解决。当群首服务器为一个请求产生事务时,作为事务生成的一部分,包括了一些在这个请求中znode节点或它的数据变化的值(delta值),并指定一个特定的版本号。最后重新执行一个事务就不会导致不一致的版本号。

七、服务器与会话

会话(Session)是Zookeeper的一个重要的抽象。保证请求有序、临时znode节点、监事点都与会话密切相关。因此会话的跟踪机制对ZooKeeper来说也非常重要。

ZooKeeper服务器的一个重要任务就是跟踪并维护这些会话。

  • 在独立模式下,单个服务器会跟踪所有的会话。
  • 在仲裁模式下,则由群首服务器来跟踪和维护。

群首服务器和独立模式的服务器实际上运行相同的会话跟踪器(参考SessionTracker类和SessionTrackerImpl类)。而追随
者服务器仅仅是简单地把客户端连接的会话信息转发给群首服务器(参考LearnerSessionTracker类)。

为了保证会话的存活,服务器需要接收会话的心跳信息。心跳的形式可以是一个新的请求或者显式的ping消息(参考LearnerHandler.run())。两种情况下,服务器通过更新会话的过期时间来触发(touch)会话活跃(参考SessionTrackerImpl.touchSession()方法)。
在仲裁模式下,群首服务器发送一个PING消息给它的追随者们,追随者们返回自从最新一次PING消息之后的一个session列表。群首服务器每半个tick(参考10.1.1节的介绍)就会发送一个ping消息给追随者们。所以,如果一个tick被设置成2秒,那么群首服务器就会每一秒发送一个ping消息。

对于管理会话的过期有两个重要的要点。一个称为过期队列(expiry queue)的数据结构(参考ExpiryQueue类),用于维护会话的过期。这个数据结构使用 bucket 来维护会话,每一个bucket对应一个某时间范围内过期的会话,群首服务器每次会让一个bucket的会话过期。为了确定哪一个bucket的会话过期,如果有的话,当下一个底限到来时,一个线程会检查这个expiry queue来找出要过期的bucket。这个线程在底限时间到来之前处于睡眠状态,当它被唤醒时,它会取出过期队列的一批session,让它们过期。当然取出的这批数据也可能是空的。
为了维护这些bucket,群首服务器把时间分成一些片段,以expirationInterval为单位进行分割,并把每个会话分配到它的过期时间对应的bucket里,其功能就是有效地计算出一个会话的过期时间,以向上取正的方式获得具体时间间隔。更具体来说,就是对下面的表达式进行计算,当会话的过期时间更新时,根据结果来决定它属于哪一个bucket。
(expirationTime / expirationInterval + 1) * expirationInterval
举例说明,比如expirationInterval为2,会话的超时时间为10。那么这个会话分配到bucket为12((10/2+1)*2的结果)。注意当触发(touch)这个会话时expirationTime会增加,所以随后需要根据之后的计算会话移动到其他的bucket中。
使用bucket的模式来管理的一个主要原因是为了减少让会话过期这项工作的系统开销。在一个ZooKeeper的部署环境中,可能其客户端就有数千个,因此也就有数千个会话。在这种场景下要细粒度地检查会话过期是不合适的。如果expirationInterval短的话,那么ZooKeeper就会以这种细粒度的方式完成检查。目前expirationInterval是一个tick,通常以秒为单位。

八、服务器与监视点

为了在服务端管理监视点,ZooKeeper的服务端实现了监视点管理器(watch manager)。一个WatchManager类的实例负责管理当前已被注册的监视点列表,并负责触发它们。所有类型的服务器(包括独立服务器,群首服务器,追随者服务器和观察者服务器)都使用同样的方式处理监视点。
DataTree类中持有一个监视点管理器来负责子节点监控和数据的监控,对于这两类监控,请参考4.2节,当处理一个设置监视点的读请求时,该类就会把这个监视点加入manager的监视点列表。类似的,当处理一个事务时,该类也会查找是否需要触发相应的监视点。如果发现有监视点需要触发,该类就会调用manager的触发方法。添加一个监视点和触发一个监视点都会以一个read请求或者FinalRequestProcessor类的一个事务开始。
在服务端触发了一个监视点,最终会传播到客户端。负责处理传播的为服务端的cnxn对象(参见ServerCnxn类),此对象表示客户端和服务端的连接并实现了Watcher接口。Watch.process方法序列化了监视点事件为一定格式,以便用于网络传送。ZooKeeper客户端接收序列化的监视点事件,并将其反序列化为监视点事件的对象,并传递给应用程序。
监视点只会保存在内存,而不会持久化到硬盘。当客户端与服务端的连接断开时,它的所有监视点会从内存中清除。因为客户端库也会维护一份监视点的数据,在重连之后监视点数据会再次被同步到服务端。

九、客户端

在客户端库中有2个主要的类:ZooKeeperClientCnxn

  • ZooKeeper类:实现了大部分API,写客户端应用程序时必须实例化这个类来建立一个会话。一旦建立起一个会话,ZooKeeper就会使用一个会话标识符来关联这个会话。这个会话标识符实际上是由服务端所生成的(参考SessionTrackerImpl类)。
  • ClientCnxn类管理连接到server的Socket连接。该类维护了一个可连接的ZooKeeper的服务器列表,并当连接断掉的时候无缝地切换到其他的服务器。当重连到一个其他的服务器时会使用同一个会话(如果没有过期的话),客户端也会重置所有的监视点到刚连接的服务器上(参考ClientCnxn.SendThread.primeConnection())。重置默认是开启的,可

以通过设置disableAutoWatchReset来禁用。

十、序列化

对于网络传输和磁盘保存的序列化消息和事务,ZooKeeper使用了Hadoop中的Jute来做序列化。如今,该库以独立包的方式被引入,在ZooKeeper代码库中,org.apache.jute就是Jute库。

对于Jute最主要的定义文件为zookeeper.jute。它包含了所有的消息定义和文件记录。下面是一个Jute定义的例子:

module org.apache.zookeeper.txn {
...
class CreateTxn {
    ustring path;
    buffer data;
    vector<org.apache.zookeeper.data.ACL> acl;
    boolean ephemeral;
    int parentCVersion;
    }
...
}
这个例子定义模块,该模块包括一个create事务的定义。同时。这个模块映射到了一个ZooKeeper的包中
查看原文

赞 0 收藏 0 评论 0

总有刁民想害朕 发布了文章 · 2018-12-10

Chapter 8 Curator:ZooKeeper API 的高级封装库 —— 笔记

Curator的核心目标就是管理ZooKeeper的相关操作,将连接管理的复杂操作部分隐藏起来。

  • 例如,Curator实现了以下原语的菜谱:

    • 锁(lock)
    • 屏障(barrier)
    • 缓存(cache)
  • 还实现了流畅(fluent)式的开发风格的接口:能够将ZooKeeper 中的create、delete、getData 等操作以流水线式的编程方式链式执行。
  • 提供了命名空间(namespace)
  • 自动重连和其他组件

一、Curator 客户端程序

  • 首先、创建一个客户端实例,实例为 CuratorFramework 类的实力,通过调用 Curator 提供的工厂方法来获得该实例:
  CuratorFramework zkc = 
    CuratorFrameworkFactory.newClient(connectString, retryPolicy);
    • connectString:连接的ZooKeeper服务器的列表
    • retryPolicy:指定对于失去连接事件重试操作的处理策略
    • 注意:在工厂类中还提供了其他方法来创建实例,其中有一个CuratorZooKeeperClient类,该类在ZooKeeper客户端实例上提供了某些附加功能,如保证请求操作在不可预见的连接断开情况下也能够安全执行,与CuratorFramework类不同,CuratorZooKeeperClient类中的操作执行与ZooKeeper客户端句柄直接相对应。

    二、流畅式 API

    【标准 ZooKeeper API】

    zk.create("/mypath",
        new byte[0],
        ZooDefs.Ids.OPEN_ACL_UNSAFE,
        CreateMode.PERSISTENT);
    

    【Curator 流程 API】

    zkc.create().withMode(CreateMode.PERSISTENT).forPath("/mypath", new byte[0]);

    create 调用返回一个 CreateBuilder 类的实力,随后调用的返回均为 CreateBuilder 类所继承的对象,如:

    • CreateBuilder 继承了 CreateModable<ACLBackgroundPathAndBytesable<String>> 类
    • withMode 方法声明了泛型接口 CreateModable<T>

    【异步执行方法,只需增加 inBackground】

    zkc.create().inBackground().withMode(CreateMode.PERSISTENT).forPath("/mypath", new byte[0]);
    inBackground调用可以传入一个上下文对象,通过该参数可以传入一个具体的回调方法的实现,或是一个执行回调的执行器(java.util.concurrent.Executor)。在Java中,执行器(executor)对象可以执行可运行的对象,我们可以通过执行器将回调方法的执行与ZooKeeper客户端线程的运行解耦,采用执行器常常比为每个任务新建一个线程更好。

    【设置监视点,只需调用链中增加 watched 方法】

    zkc.getData().inBackground().watched().forPath("/mypath")
    • 上面设置的监视点将会通过监听器触发通知,这些通知将会以WATCHED事件传递给指定的监听器
    • 还可以使用usingWathcer方法替换watched方法,usingWathcer方法接受一个普通的ZooKeeper的Wathcer对象,并在接收到通知后调用该监视点方法
    • 第三种选择就是传入一个CuratorWatcher对象,CuratorWatcher的process方法与ZooKeeper的Watcher不同的是,它可能会抛出异常。

    三、监听器

    1、【实现一个 CuratorListener 接口】

    CuratorListener masterListener = new CuratorListener() {
        public void eventRecived(CuratorFramework client, CuratorEvent event) {
            try {
                switch(event.getType()) {
                case CHILDREN:
                    ...
                    break;
                case CREATE:
                    ...
                    break;
                case DELETE:
                    ...
                    break;
                case WATCHED:
                    ...
                    break;
                }
            } catch(Exception e) {
                LOG.error("Exception while processing event.", e);
                try {
                    close();
                } catch(IOException ioe) {
                    LOG.error(:IOException while closing.", ioe);
                }
            }
        }
    };
    

    2、【注册这个监听器】

    client = CuratorFrameworkFactory.newClient(hostPost, retryPolicy);
    client.getCuratorListenable().addListener(masterListener);
    

    3、【特殊的监听器,负责处理后台工作线程捕获的异常时的错误报告,提供了底层细节的处理】

    UnhandledErrorListener errorsListener = new UnhandledErrorListener() {
        public void UnhandleError(String message, Throwable e) {
            LOG.error("Unrecoverable error:", e);
            try {
                close();
            } catch (IOException ioe) {
                LOG.warn( "Exception when closing.", ioe );
            }
        }
    }
    

    4、【将该监听器注册到客户端实例中】

    client.getUnhandledErrorListeable().addListener(errorsListener);
    

    四、Curator 中状态的转换

    在Curator中暴露了与ZooKeeper不同的一组状态,比如SUSPENDED状态,还有Curator使用LOST来表示会话过期的状态。
    图8-1中展示了连接状态的状态机模型,当处理状态的转换时,建议将所有主节点操作请求暂停,因为并不知道ZooKeeper客户端能否在会话过期前重新连接,即使ZooKeeper客户端重新连接成功,也可能不再是主要主节点的角色,因此谨慎处理连接丢失的情况,对应用程序更加安全。
    【图8-1:Curator连接状态机模型】
    clipboard.png
    【READ_ONLY状态】:当ZooKeeper集群启用了只读模式,客户端所连接的服务器就会进入只读模式中,此时的连接状态也将进入只读模式。服务器转换到只读模式后,该服务器就会因隔离问题而无法与其他服务器共同形成仲裁的最低法定数量,当连接状态为制度模式,客户端也将漏掉此时发生的任何更新操作,因为如果集群中存在一个子集的服务器数量,可以满足仲裁最低法定数量,并可以接收到客户端的对ZooKeeper的更新操作,还是会发生ZooKeeper的更新,也许这个子集的服务器会持续运行很久(ZooKeeper无法控制这种情况),那么漏掉的更新操作可能会无限多。漏掉更新操作的结果可能会导致应用程序的不正确的操作行为,所以,强烈建议启用该模式前仔细考虑其后果。

    五、两种边界情况

    有两种有趣的错误场景,在Curator中都可以处理得很好,第一种是在有序节点的创建过程中发生的错误情况的处理,第二种为删除一个节点时的错误处理。

    • 【有序节点的情况】
      如果客户端所连接的服务器崩溃了,但还没来得及返回客户端所创建的有序节点的节点名称(即节点序列号),或者客户端只是连接丢失,客户端没接收到所请求操作的响应信息,结果,客户端并不知道所创建的znode节点路径名称。回忆对于有序节点的应用场景,例如,建立一个有序的所有客户端列表。为了解决这个问题,CreateBuilder提供了一个 withProtection 方法来通知Curator客户端,在创建的有序节点前添加一个唯一标识符,如果create操作失败了,客户端就会开始重试操作,而重试操作的一个步骤就是验证是否存在一个节点包含这个唯一标识符。
    • 【删除节点的保障】
      在进行delete操作时也可能发生类似情况,如果客户端在执行delete操作时,与服务器之间的连接丢失,客户端并不知道delete操作是否成功执行。如果一个znode节点删除与否表示某些特殊情况,例如,表示一个资源处于锁定状态,因此确保该节点删除才能确保资源的锁定被释放,以便可以再次使用。Curator客户端中提供了一个方法,对应用程序的delete操作的执行提供了保障,Curator客户端会重新执行操作,直到成功为止,或Curator客户端实例不可用时。使用该功能,只需要使用DeleteBuilder接口中定义的 guaranteed 方法。

    六、菜谱

    1、群首闩(leader latch)

    【创建一个LeaderLatch的实例】

    leaderLatch = new LeaderLatch(client, "/master", myId);
    • 传入一个Curator框架客户端的实例
    • 一个用于表示集群管理节点的群组的ZooKeeper路径
    • 以及一个表示当前主节点的标识符

    【注册一个LeaderLatchListener接口的实现,该接口中有两个方法:isLeader和notLeader。以下为isLeader实现的代码:】

    @Override
    public void isLeader()
    {
        ...
        /*
        * Start workersCache  ①
        */
        workersCache.getListeable().addListener(workersCacheListener);
        workersCache.start();
        (new RecoveredAssignments(
            client.getZooKeeperClient().getZooKeeper())).recover(
                new RecoveryCallback() {
                    public void recoveryComplete(int rc, List<String> tasks) {
                        try {
                            if(rc == RecoveryCallback.FAILED) {
                                LOG.warn("Recovery of assigned tasks failed.");
                            } else {
                                LOG.info( "Assigning recovered tasks" );
                                recoveryLatch = new CountDownLatch(tasks.size());
                                assignTasks(tasks); ② 
                            }
                            new Thread( new Runnable() {③
                                public void run() {
                                    try {
                                        /*
                                        * Wait until recovery is complete
                                        */
                                            recoveryLatch.await();
                                        /*
                                        * Start tasks cache
                                        */
                                        tasksCache.getListenable().
                                            addListener(tasksCacheListener); ④
                                        tasksCache.start();
                                    } catch (Exception e) {
                                        LOG.warn("Exception while assigning and getting tasks.", e );
                                    }
                                }
                            }).start();
                        } catch(Exception e) {
                        LOG.error("Exception while executing the recovery callback", e);
                        }    
                    }
                });
            }
    /* ① 首先初始化一个从节点缓存列表的实例,以确保有可以分配任务的从节点。
     * ② 一旦发现存在之前的主节点没有分配完的任务需要分配,将继续进行任务分配。
     * ③ 实现了一个任务分配的屏障,这样就可以在开始分配新任务前,等待已恢复的任务的分配完成,如果不这样做,新的主节点会再次分配所有已恢复的任务。启动了一个单独的线程进行处理,以便不会锁住ZooKeeper客户端回调线程的运行。
     * ④ 当主节点完成恢复任务的分配操作,开始进行新任务的分配操作。 */
    

    【需要在具体流程开始前注册监听器。在runForMaster方法中进行这两步操作,同时,还将注册另外两个监听器,来处理事件的监听和错误:】

    pubic void runForMaster() {
        client.getCuratorListenable().adddListener(masterLisener);
        client.getUnhandledErrorListenable().addListener(errorsListener);
        leaderLatch.start();
    }
    

    **【对于notLeader方法,会在主节点失去管理权时进行调用,在本例中,只是简单地关闭了所有对象实例,对这个例子来说,这些操
    作已经足够了。在实际的应用程序中,也许还需要进行某些状态的清理操作并等待再次成为主节点。如果LeaderLatch对象没有关闭,Curator客户端有可能再次获得管理权】**

    2、群首选举器

    选举主节点时还可以使用的另一个菜谱为LeaderSelector。
    LeaderSelector和LeaderLatch之间主要区别在于使用的监听器接口不同:

    • LeaderSelector使用了LeaderSelectorListener接口,该接口中定义了takeLeadership方法,并继承了stateChanged方法,可以在应用程序中使用群首闩原语来进行一个主节点的选举操作

    【首先需要创建一个LeaderSelector实例】

    leaderSelector = new LeaderSelector(client, "/master", this);
    

    【takeLeadership方法用于获取管理权,该代码实现与isLeader类似】

    CountDownLatch leaderLatch = new CountDownLatch(1);
    CountDownLatch closeLatch = new CountDownLatch(1); 
    @Override
    public void takeLeadership(CuratorFramework client) throws Exception
    {
        ...
        /*
        * Start workersCache
        */
        workersCache.getListenable().addListener(workersCacheListener);
        workersCache.start();
        (new RecoveredAssignments(
            client.getZooKeeperClient().getZooKeeper())).recover(
                new RecoveryCallback() {
                    public void recoveryComplete (int rc, List<String> tasks) {
                    try {
                        if(rc == RecoveryCallback.FAILED) {
                            LOG.warn("Recovery of assigned tasks failed.");
                        } else {
                            LOG.info( "Assigning recovered tasks" );
                            recoveryLatch = new CountDownLatch(tasks.size());
                            assignTasks(tasks);
                        }
                        new Thread( new Runnable() {
                            public void run() {
                                try {
                                    /*
                                    * Wait until recovery is complete
                                    */
                                    recoveryLatch.await();
                                    /*
                                    * Start tasks cache
                                    */
                                    tasksCache.getListenable().
                                    addListener(tasksCacheListener);
                                    tasksCache.start();
                                } catch (Exception e) {
                                    LOG.warn("Exception while assigning and getting tasks.", e);
                                }
                            }
                        }).start();
                    /*
                    * Decrement latch
                    */
                    leaderLatch.countDown(); ①
                    } catch (Exception e) {
                        LOG.error("Exception while executing the recovery callback", e);
                    }
                }
        });
        /*
        * This latch is to prevent this call from exiting. If we exit, then
        * we release mastership.
        */
        closeLatch.await(); ②
    }
    /* ① 通过一个单独的CountDownLatch原语来等待该Curator客户端获取管理权。
     * ② 如果主节点退出了takeLeadership方法,也就放弃了管理权,通过CountDownLatch来阻止退出该方法,直到主节点关闭为止。
     */
    实现的这个方法为CuratorMaster类的一部分,而CuratorMaster类实现了LeaderSelectorListener接口。对于主节点来说,如果想要释放管理权只能退出takeLeadership方法,所以需要通过某些锁等机制来阻止该方法的退出,在实现中,在退出主节点时通过递减闩(latch)值来实现。

    【依然在runForMaster方法中启动我们的主节点选择器】

    public void runForMaster() {
        client.getCuratorListenable().addListener(masterListener);
        client.getUnhandledErrorListenable().addListener(errorsListener);
        leaderSelector.setId(myId);
        leaderSelector.start();
    }
    
    另外还需要给这个主节点一个任意的标识符,虽然在本例中并未实现,但可以设置群首选择器在失去管理权后自动重新排队(LeaderSelector.autoRequeue)。重新排队意味着该客户端会一直尝试获取管理权,并在获得管理权后执行takeLeadership方法。

    【作为LeaderSelectorListener接口实现的一部分,还实现了一个处理连接状态变化的方法:】

    @Override
    public void stateChanged(CuratorFramework client, ConnectionState newState)
    {
        switch(newState) {
        case CONNECTED:
            // Nothing to do in this case.
            break;
        case RECONNECTED:
            // Reconnected, so I should ①
            // still be the leader.
            break;
        case SUSPENDED:
            LOG.warn("Session suspended");
            break;
        case LOST:
            try {
                close(); ②
            } catch (IOException e) {
                LOG.warn( "Exception while closing", e );
            }
            break;
        case READ_ONLY:
            // We ignore this case.
            break;
        }
    }
    /* ① 所有操作均需要通过ZooKeeper集群实现,因此,如果连接丢失,主节点也就无法先进行任何操作请求,因此在这里最好什么都不做。
     * ② 如果会话丢失,只是关闭这个主节点程序。
     */
     

    3、子节点缓存器

    最后一个菜谱是子节点缓存器(PathChildrenCached类)

    为了处理每一个缓存器实例的变化情况,需要一个 PathChildrenCacheListener 接口的实现类,该接口中只有一个方法
    childEvent 。对于从节点信息的列表,只关心从节点离开的情况,因为需要重新分配已经分给这些节点的任务,而列表中添加信息对于分配新任务更加重要:
    PathChildrenCacheListener workersCacheListener = new PathChildrenCacheListener()
    {
        public void childEvent(CuratorFramework client, PathChildrenCacheEvent event)
        {
            if(event.getType() == PathChildrenCacheEvent.Type.CHILD_REMOVED) {
                /*
                * Obtain just the worker's name
                */
                try {
                    getAbsentWorkerTasks(event.getData().getPath().replaceFirst("/workers/", ""));
                } catch (Exception e) {
                    LOG.error("Exception while trying to re-assign tasks.", e);
                }
            }
        }
    };
    
    对于任务列表,通过列表增加的情况来触发任务分配的过程:
    PathChildrenCacheListener tasksCacheListener = new PathChildrenCacheListener() {
        public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) {
            if(event.getType() == PathChildrenCacheEvent.Type.CHILD_ADDED) {
                try {
                    assignTask(event.getData().getPath().replaceFirst("/tasks/",""));
                } catch (Exception e) {
                    LOG.error("Exception when assigning task.", e);
                }
            }
        }
    };
    查看原文

    赞 0 收藏 0 评论 0

    总有刁民想害朕 发布了文章 · 2018-12-07

    Chapter 6 ZooKeeper 注意事项 —— 笔记

    一、使用 ACL

    每次创建znode节点时,必须设置访问权限,而且子节点并不会继承父节点的访问权限。访问权限的检查也是基于每一个 znode 节点的,如果一个客户端可以访问一个 znode 节点,即使这个客户端无权访问该节点的父节点,仍然可以访问这个 znode 节点。

    ZooKeeper 通过访问控制表(ACL)来控制访问权限。一个ACL包括以下形式的记录:scheme:auth-info

    • scheme:对应了一组内置的鉴权模式
    • auth-info:为对于特定模式所对应的方式进行编码的鉴权信息
    ZooKeeper通过检查客户端进程访问每个节点时提交上来的授权信息来保证安全性。如果一个进程没有提供鉴权信息,或者鉴权信息与要请求的znode节点的信息不匹配,进程就会收到一个权限错误。

    为了给一个ZooKeeper增加鉴权信息,需要调用 addAuthInfo 方法,形式如下:

    void addAuthInfo(
        String scheme,
        byte auth[]
    )
    • scheme:表示所采用的鉴权模式。
    • auth:表示发送给服务器的鉴权信息。该参数的类型为byte[]类型,不过大部分的鉴权模式需要一个String类型的信息,所以你可以通过String.getBytes()来将String转换为byte[]。
    一个进程可以在任何时候调用addAuthInfo来添加鉴权信息。一般情况下,在ZooKeeper句柄创建后就会调用该方法来添加鉴权信息。进程中可以多次调用该方法,为一个ZooKeeper句柄添加多个权限的身份。

    1、内置的鉴权模式

    ZooKeeper提供了4种内置模式进行ACL的处理:

    • OPEN_ACL_UNSAFE常量

      • 使用world作为鉴权模式
      • 使用anyone作为auth-info
    • 管理员所使用的super模式

      • 该模式不会列入到 ACL 中
      • 但可以用于ZooKeeper的鉴权
      • 一个客户端通过super鉴权模式连接到ZooKeeper后,不会被任何节点的ACL所限制
    • digest为内置鉴权模式

      • auth-info格式为userid:passwd_digest(当调用addAuthInfo时需要设置ACL和userid:password信息。)
    当ZooKeeper以一个空树开始,只有一个znode节点:/,这个节点对所有人开放,我们假设管理员Amy负责配置ZooKeeper服务,Amy创建/apps节点,用于所有使用服务的应用需要创建节点的父节点,她现在需要锁定服务,所以她设置为/和/apps节点设置的ACL为:digest:amy:Iq0onHjzb4KyxPAp8YWOIC8zzwY=, READ | WRITE | CREATE | DELETE | ADMIN
    其中passwd_digest为用户密码的加密摘要。在这个ACL例子中,Iq0onHjzb4KyxPAp8YWOIC8zzwY=为passwd_digest,因此当Amy调用addAuthInfo方法,auth参数传入的为amy:secret字符串的字节数组,Amy使用下面的DigestAuthenticationProvider来为她的账户amy生成摘要信息。
    java -cp $ZK_CLASSPATH \org.apache.zookeeper.server.auth.DigestAuthenticationProvider amy:secret.... amy:secret->amy:Iq0onHjzb4KyxPAp8YWOIC8zzwY= 
    amy:后面生成的字符串为密码摘要信息,也就是我们在ACL记录总使用的信息。当Amy需要向ZooKeeper提供鉴权信息时,她就要使用digest amy:secret。例如,当Amy使用zkCli.sh连接到ZooKeeper,她可以通过以下方式提供鉴权信息:
    [zk: localhost:2181(CONNECTED) 1] addauth digest amy:secret
    为了避免在后面的例子中写出所有的摘要信息,将使用XXXXX作为占位符来简单的表示摘要信息。Amy想要设置一个子树,用于一个名为SuperApp的应用,该应用由开发人员Dom所开发,因此她创建了/apps/SuperApp节点,设置ACL如下:
    digest:dom:XXXXX, READ | WRITE | CREATE | DELETE | ADMIN
    digest:amy:XXXXX, READ | WRITE | CREATE | DELETE | ADMIN
    该ACL由两条记录组成,一个由Dom使用,一个由Amy使用。这些记录对所有以dom或amy密码信息认证的客户端提供了全部权限。注意,根据ACL中的Dom的记录,他对/apps/SuperApp节点具有ADMIN权限,他有权限修改ACL,这就意味着Dom可以删除Amy访问/apps/SuperApp节点的权限。当然Amy具有super的访问权限,所以她可以随时访问任何znode节点,即使Dom删除了她的访问权限。

    Dom使用ZooKeeper来保存其应用的配置信息,因此他创建了/apps/SuperApp/config节点来保存配置信息。之后他使用我们在之前例子中介绍的模式OPEN_ACL_UNSAFE来创建znode节点,因为Dom认为/apps和/apps/SuperApp的访问是受限制的,所以也能保护/apps/SuperApp/config节点的访问。我们后面就会看到,这样做称为UNSAFE。

    我们假设一个名为Gabe的人具有ZooKeeper服务的网络访问权限。因为ACL的策略设置,Gabe无法访问/app或/apps/SuperApp节点,Gabe也无法获取/apps/SuperApp节点的子节点列表。但是,也许Gabe猜测Dom使用ZooKeeper保存配置信息,config这个名字对于配置文件信息也非常显而易见,因此他连接到ZooKeeper服务,调用getData方法获取/apps/SuperApp/config节点的信息。因为该znode节点采用了开放的ACL策略,Gabe可以获取该节点信息。还不止这些,Gabe可以修改、删除该节点,甚至限制/apps/SuperApp/config节点的访问权限。

    假设Dom意识到这个问题,修改/apps/SuperApp/config节点的ACL策略为:

    digest:dom:XXXXX, READ | WRITE | CREATE | DELETE | ADMIN
    
    随着事情的发展,Dom得到一个新的开发人员Nico的帮助,来一同完善SuperApp。Nico需要访问SuperApp的子树,因此Dom修改了子树的ACL策略,将Nico添加进来。新的ACL策略为:
    digest:dom:XXXXX, READ | WRITE | CREATE | DELETE | ADMIN
    digest:nico:XXXXX, READ | WRITE | CREATE | DELETE | ADMIN
    

    注意:用户名和密码的摘要信息从何而来?

    你也许注意到我们用于摘要的用户名和密码似乎凭空而来。实际上确实如此。这些用户名或密码不用对应任何真实系统的标识,甚至用户名也可以重复。也许有另一个开发人员叫Amy,并且开始和Dom和Nico一同工作,Dom可以使用amy:XXXXX来添加她的ACL策略,只是在这两个Amy的密码一样时会发生冲突,因为这样就导致她们俩可以互相访问对方的信息。

    现在Dom和Nico具有了他们需要完成的SuperApp的所需的访问权限。应用部署到生产环境,然而Dom和Nico并不想提供进程访问ZooKeeper数据时所使用的密码信息,因此他们决定通过SuperApp所运行的服务器的网络地址来限制数据的访问权限。例如所有10.11.12.0/24网络中服务器,因此他们修改了SuperApp子树的znode节点的ACL为:

    digest:dom:XXXXX, READ | WRITE | CREATE | DELETE | ADMIN
    digest:nico:XXXXX, READ | WRITE | CREATE | DELETE | ADMIN
    ip:10.11.12.0/24, READ
    
    
    • ip鉴权模式
    ip鉴权模式需要提供网络的地址和掩码,因为需要通过客户端的地址来进行ACL策略的检查,客户端在使用ip模式的ACL策略访问znode节
    点时,不需要调用addAuthInfo方法。
    现在,任何在10.11.12.0/24网段中运行的ZooKeeper客户端都具有SuperApp子树的znode节点的读取权限。该鉴权模式假设IP地址无法被伪造,这个假设也许并不能适合于所有环境中。

    2、SASL和Kerberos

    SASL表示简单认证与安全层(Simple Authentication and SecurityLayer)。SASL将底层系统的鉴权模型抽象为一个框架,因此应用程序可以使用SASL框架,并使用SASL支持多各种协议。在ZooKeeper中,SASL常常使用Kerberos协议,该鉴权协议提供之前我们提到的那些缺失的功能。在使用SASL模式时,使用sasl作为模式名,id则使用客户端的 Kerberos的ID。
    SASL是ZooKeeper的扩展鉴权模式,因此,需要通过配置参数或Java系统中参数激活该模式。如果你采用ZooKeeper的配置文件方式,需要使用authProvider.XXX配置参数,如果你想要通过系统参数方式,需要使用zookeeper.authProvider.XXX作为参数名。这两种情况,XXX
    可以为任意值,只要没有任何重名的authProvider,一般XXX采用以0开始的一个数字。配置项的参数值为org.apache.zookeeper.server.auth.SASLAuthenticationProvider,这样就可以激活SASL模式。

    3、增加新鉴权模式

    ZooKeeper中还可以使用其他的任何鉴权模式。对于激活新的鉴权模式来说只是简单的编码问题。在org.apache.zookeeper.server.auth包中提供了一个名为AuthenticationProvider的接口类,如果你实现你自己的鉴权模式,你可以将你的类发布到服务器的classpath下,创建zookeeper.authProvider名称前缀的Java系统参数,并将参数值设置为你实现AuthenticationProvider接口的实际的类名。

    二、恢复会话

    • 首先,应用程序的ZooKeeper状态还处于客户端崩溃时的状态,其他客户端进程还在继续运行,也许已经修改了ZooKeeper的状态,因此,建议客户端不要使用任何之前从ZooKeeper获取的缓存状态,而是使用ZooKeeper作为协作状态的可信来源。
    • 第二个重要问题是客户端崩溃时,已经提交给ZooKeeper的待处理操作也许已经完成了,由于客户端崩溃导致无法收到确认消息,ZooKeeper无法保证这些操作肯定会成功执行,因此,客户端在恢复时也许需要进行一些ZooKeeper状态的清理操作,以便完成某些未完成的任务。

    三、当znode节点重新创建时,重置版本号

    znode节点被删除并重建后,其版本号将会被重置。如果应用程序在一个znode节点重建后,进行版本号检查会导致错误的发生。

    四、sync方法

    因为与ZooKeeper的带外通信可能会导致某些问题,这种通信常常称为隐蔽通道(hidden channel)
    sync方法可以用于处理这种情况。sync为异步调用的方法,客户端在读操作前调用该方法,假如客户端从某些直接通道收到了某个节点变化的通知,并要读取这个znode节点,客户端就可以通过sync方法,然后再调用getData方法:
    ...
    zk.sync(path, voidCb, ctx); ①
    zk.getData(path, watcher, dataCb, ctx); ②
    ...
    /** ①sync方法接受一个path参数,一个void返回类型的回调方法的示例,一个上下文对象实例。
        ②getData方法与之前介绍的调用方式一样。 */
    sync方法的path参数指示需要进行操作的路径。在系统内部,sync方法实际上并不会影响ZooKeeper,当服务端处理sync调用时,服务端会刷新群首与调用sync操作的客户端c所连接的服务端之间的通道,刷新的意思就是说在调用getData的返回数据的时候,服务端确保返回所有客户端c调用sync方法时所有可能的变化情况。在上面的隐蔽通道的情况中,变化情况的通信会先于sync操作的调用而发生,因此当c收到getData调用的响应,响应中必然会包含c'所通知的变化情况。注意,在此时该节点也可能发生了其他变化,因此在调用getData时,ZooKeeper只保证所有变化情况能够返回。
    使用sync还有一个注意事项,这个需要深入ZooKeeper内部的技术问题(你可以选择跳过此部分)。因为ZooKeeper的设计初衷是用于快速读取以及以读为主要负载的扩展性考虑,所以简化了sync的实现,同时与其他常规的更新操作(如create、setData或delete)不同,sync操作并不会进入执行管道之中。sync操作只是简单地传递到群首,之后群首会将响应包队列化,传递给群组成员,之后发送响应包。不过还有另外一种可能,仲裁机制确定的群首l',现在已经不被仲裁组成员所认可,仲裁组成员现在选举了另个群首l',在这种情况下,群首l可能无法处理所有的更新操作的同步,而sync调用也就可能无法履行其保障。
    ZooKeeper的实现中,通过以下方式处理上面的问题,ZooKeeper中的仲裁组成员在放弃一个群首时会通知该群首,通过群首与群组成员之间的tickTime来控制超时时间,当它们之间的TCP连接丢失,群组成员在收到socket的异常后就会确定群首是否已经消失。群首与群组成员之间的超时会快于TCP连接的中止,虽然的确存在这种极端情况导致错误的可能,但是在我们现有经验中还未曾遇到过。

    五、顺序性保障

    虽然ZooKeeper声明对一个会话中所有客户端操作提供顺序性的保障,但还是会存在ZooKeeper控制之外某些情况,可能会改变客户端操作的顺序。

    1、连接丢失时的顺序性

    对于连接丢失事件,ZooKeeper会取消等待中的请求:

    • 对于同步方法的调用客户端库会抛出异常
    • 对于异步请求调用,客户端调用的回调函数会返回结果码来标识连接丢失

    2、同步 API 和多线程的顺序性

    如果在多线程环境中使用同步API,需要特别注意顺序性问题,一个同步ZooKeeper调用会阻塞运行,直到收到响应信息,如果两个或更多线程向ZooKeeper同时提交了同步操作,这些线程中将会被阻塞,直到收到响应信息,ZooKeeper会顺序返回响应信息,但操作结果可能因线程调度等原因导致后提交的操作而先被执行。

    3、同步和异步混合使用的顺序性

    六、数据字段和子节点的限制

    ZooKeeper默认情况下对数据字段的传输限制为1MB,该限制为任何节点数据字段的最大可存储字节数,同时也限制了任何父节点可以拥有的子节点数。

    七、嵌入式 Zookeeper 服务器

    缺点:

    • Zookeeper 不透明
    • 应用可用性和 ZooKeeper 可用性耦合
    • ZooKeeper常常被用来提供高可用服务,但对于应用中嵌入ZooKeeper的方式却降低了其最强的优势。
    查看原文

    赞 0 收藏 0 评论 0

    总有刁民想害朕 发布了文章 · 2018-12-07

    Spring思维导图,让Spring不再难懂(mvc篇)

    Spring的模型-视图-控制器(MVC)框架是围绕一个DispatcherServlet来设计的,这个Servlet会把请求分发给各个处理器,并支持可配置的处理器映射、视图渲染、本地化、时区与主题渲染等,甚至还能支持文件上传。

    clipboard.png

    • 1、HTTP 请求:客户端请求提交到 DispatcherServlet。
    • 2、HandlerMapping 寻找路由器:由 DispatcherServlet 查询一个或多个 HandlerMapping,找到处理请求的 Controller。
    • 3、Controller 调用处理器:DispatcherServlet 将请求提交到 Controller。
    • 4-5、Service & ModelAndView 调用业务处理并返回模型:Controller 调用业务逻辑 Service 处理后,返回 ModelAndView。
    • 6-7、ViewResolver & Model 处理视图映射并返回模型:DispatcherServlet 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 指定的视图。
    • 8、Http 响应:视图负责将结果显示到客户端。

    一、主要注解

    clipboard.png

    • @Controller:

      1. 用于标注控制层组件
      2. 用于标记在一个类上,SpringMVC Controller 对象
      3. HandlerMapping 分发处理器会扫描使用了该注解的类方法,并检测是否使用了 @RequestMapping 注解。
      4. 可以把 Request 请求 header 部分的值绑定到方法的参数上。
    • @RestController:
      等于 @Controller 和 @ResponseBody 的组合效果
    • @Component
      泛指组件,当组件不好归类时,可以是用这个注解进行标注。
    • @Repository
      用于注解 DAO 层,在 daoImpl 类上注解。
    • @Service
      用于标注业务层组件
    • @ResponseBody

      1. 异步请求
      2. 用于将 Controller 的方法返回的对象,通过适当的 HttpMessageConverter 转换为指定格式后,写入到 Response 对象的 body 数据区。
      3. 返回的数据不是 HTML 标签的页面,而是其他某种格式的数据时(JSON、XML)使用
    • @RequestMapping
      一个处理请求地址映射的注解,可用于类或方法上。类上:表示类中的所有响应请求的方法都是以该地址作为父路径。
    • @Autowired
      它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。
      通过 @Autowired 的使用来消除 set、get 方法
    • @PathVariable
      用于将请求 URL 中的模板变量映射到功能处理方法的参数上,即取出 uri 模板中的变量作为参数
    • @RequestParam
      主要用于在 SpringMVC 控制层获取参数,类似 request.getParameter("name")
    • @RequestHeader
      可以把 Request 请求 header 部分的值绑定到方法的参数上。

    二、ContextLoaderListener

    1、首先了解 web.xml 的作用

    • 一个 web 工程中可以没有 web.xml。
    • web.xml 文件是用来初始化配置信息

      • Welcome 页面
      • servlet
      • servlet-mapping
      • filter
      • listener
      • 启动加载级别等。
    • 当要启动web项目,服务器软件或容器(tomcat)会第一步加载项目中的web.xml文件,通过其中的各种配置类启动项目,只有其中配置的各项均无误时,项目才能正确启动。

      • 记载过程的顺序依次为:context-param >> listener >> filter >> servlet(同类多个节点以出现顺序加载)

    【web.xml 加载过程】
    clipboard.png

    1、启动 web 项目的时候,容器首先会去找它的配置文件 web.xml 读取两个节点:<listener></listener> 和 <context-param></context-param>
    2、接着,容器创建一个 ServletContext(application),这个 web 项目所有部分都将共享这个上下文。
    3、容器以 <context-param></context-param> 的 name 作为键,value 作为值,将其转化为键值对,存入 ServletContext。
    4、容器创建 <listener></listener> 中的类实例,根据配置的 class 类路径 <listener-class> 来创建监听,在监听中会有 contextInitialzed(ServletContextEvent args) 初始化方法,启动 web 应用时,系统调用 Listener 的该方法,在这个方法获得:

       * ServletContext application = ServletContextEvent.getServletContext(); 
       * context-param 的值 = application.getInitParameter("context-param 的值"); 
       * 得到这个 context-param 的值之后,就可以做一些操作了。
    

    举例:在项目启动之前就打开database:

    * 在 <context-param> 中设置 database 的连接方式(驱动、url、user、password)
    * 在监听类中初始化database 的连接
    * 这个监听是自己写的一个类,除了初始化方法,还有销毁方法,用于关闭应用前释放资源,如 database连接的关闭
    * 此时,调用 contextDestroyed(ServletContextEvent args),关闭 web 应用时,系统调用listener 的该方法。

    5、容器读取 <filter></filter>,根据指定的类路径类实例化过滤器。

    2、SpringMVC 的启动过程分为两个过程

    • ContextLoaderListener 初始化,实例化 IoC 容器,并将此容器实例注册到 ServletContext。
    • DispatcherServlet 初始化

    clipboard.png

    【web.xml 配置】

    • contextConfigLocation

      * 指定 Spring IoC 容器需要读取的 XML 文件路径
      * 默认会去 /WEB-INF/ 下加载 applicationContext.xml
    • contextLoaderListener

      * Spring 监听器
      * Spring MVC 在 web 容器中的启动类,读取 applicationContext.xml,负责 Spring IoC 容器在 web 上下文中的初始化
    • DispatcherServlet

      * 前端处理器,接收 HTTP 请求和转发请求的类。
    • CharacterEncodingFilter

      * 字符集过滤器
    • IntrospectorCleanupListener

      * 防止 Spring 内存溢出监听器

    其中 ContextLoadListener 监听器实现了 ServletContextListener 接口,在 web.xml 配置这个监听器,启动容器时,就会默认执行它实现的方法。在 ContextLoaderListener 中关联了 ContextLoader 类,所以整个加载配置过程由 ContextLoader 来完成。

    • ContextLoaderListener 在 web.xml 中的配置
    <!-- 配置 contextConfigLocation 初始化参数 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationServlet.xml</param-value>
    </context-param>
    <!-- 配置 ContextLoadListener -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    

    ServletContextListener 接口有两个方法:contextInitialized、contextDestoryed

    三、DispatcherServlet

    • DispatcherServlet 在 web.xml 中的配置
    <!-- servlet 定义 -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    

    其中:

    • load-on-startup:表示启动容器时初始化该 servlet
    • url-pattern:表示哪些请求交给 Spring Web MVC 处理,"/" 是用来定义默认 servlet 映射的。也可以如 "*.html" 表示拦截所有以 .html 为扩展名的请求。
    在 Spring MVC 中,每个 DispatcherServlet 都持有一个自己的上下文对象 WebApplicationContext,它又继承了根(root)WebApplicationContext 对象中已经定义的所有 bean。这些继承的 bean 可以在具体的 Servlet 实例中被重载,在每个 Servlet 实例中也可以定义其 scope 下的新 bean。
    WebApplicationContext 继承自 ApplicationContext,它提供了一些 web 应用经常需要用到的特性,与普通的 ApplicationContext 不同之处在于,它支持主题的解析,并且知道它关联到的是哪个 servlet(它持有一个该 ServletContext 的引用)

    【DispatcherServlet 的继承结构】

    clipboard.png

    spring mvc同时提供了很多特殊的注解,用于处理请求和渲染视图等。DispatcherServlet初始化的过程中会默认使用这些特殊bean进行配置。如果你想指定使用哪个特定的bean,你可以在web应用上下文WebApplicationContext中简单地配置它们。

    clipboard.png

    【特殊 bean】

    • HandlerMapping

      * 处理映射,会根据某些规则将进入容器的请求映射到具体的处理器以及一系列前处理器和后处理器(即处理器拦截)上,具体的规则视 HandlerMapping 类的实现不同而有所不同。其最常用的一个实现支持在控制器上添加注解,配置请求路径。也存在其他实现。
    • HandlerAdapter

      * 处理器适配器,拿到请求所对应的处理器后,适配器将负责去调用该处理器,使得 DispatcherServlet 无需关心具体的调用细节。比如,要调用的是一个基于注解配置的控制器,那么调用前还学要从许多注解中解析出一些相应的信息,因此,HandlerAdapter 的主要任务就是对 DispatcherServlet 屏蔽这些具体的细节。
    • HandlerExceptionResolver

      * 处理器异常解析器,负责将捕获的异常映射到不同的视图上去,此外还支持更复杂的异常处理代码。
    • ViewResolver

      * 视图解析器,负责将一个代表逻辑视图的字符串(String)映射到实际的视图类型 View 上。
    • LocaleResolver & LocaleContextResolver

      * 地区解析器和地区上下文解析器。负责解析客户端所在的地区信息甚至时区信息,为国际化的视图定制提供支持。
    • ThemeResolver

      * 主题解析器,负责解析 web 应用中可用的主题,如,提供一些个性化定制的布局等。
    • MultipartResolver

      * 解析 multi-part 的传输请求,如,支持通过 HTML 表单进行文件上传等。
    • FlashMapManager

      * FlashMap 管理器,能够存储并取回两次请求之间的 FlashMap 对象,后者可用于在请求之间传递数据,通常是在请求重定向的情景下使用。

    其中,常用的ViewResolver的配置。以jsp作为视图为例

    <!-- 对模型视图名称的解析,即在模型视图名称添加前后缀 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
    

    配置上传文件限制MultipartResolver

    <!-- 上传限制 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
         <!-- 上传文件大小限制为31M,31*1024*1024 -->
         <property name="maxUploadSize" value="32505856"/>
    </bean>
    

    四、applicationContext.xml 中的标签

    clipboard.png

    文件上传
    前面说到DispatcherServlet中有个特殊的Bean叫MultipartResolver,可用于限制文件的上传大小等。当解析器MultipartResolver完成处理时,请求便会像其他请求一样被正常流程处理。

    * 表单

    <form method="post" action="/form" enctype="multipart/form-data">
         <input type="text" name="name"/>
         <input type="file" name="file"/>
         <input type="submit"/>
    </form>

    * 控制器

    @RequestMapping(path = "/form", method = RequestMethod.POST) public String handleFormUpload(@RequestParam("name") String name, 
            @RequestParam("file") MultipartFile file) {
       if (!file.isEmpty()) {
              byte[] bytes = file.getBytes();
              // store the bytes somewhere
              return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
    

    异常处理

    • HandlerExceptionResolver

      * 可以实现全局异常控制
      * HandlerExceptionResolver 接口中定义了一个ieresolveException 方法,用于处理 Controller 中的异常,Exception ex 参数即 Controller 抛出的异常,返回值类型是 ModelAndView,可以通过这个返回值设置异常时显示的页面。
    • SimpleMappingExceptionResolver

      * Spring 提供的一个默认的异常实现类 SimpleMappingExceptionResolver
    • ExceptionHandler

      * 可是实现局部异常控制
      * 如果 @ExceptionHandler 方法是在控制器内部定义的,那么它会接收并处理控制前(或其他任何子类)中的 @RequestMapping 方法抛出的异常。
      * 如果将 @ExceptionHandler 方法定义在 @ContollerAdvice 类中,那么它会处理相关控制器抛出的异常。
    • web.xml 中的 error-page 标签

      * 简单处理异常和跳转,灵活程序不及 HandlerExceptionResolver 的异常处理

    clipboard.png

    Spring的处理器异常解析器HandlerExceptionResolver接口的实现负责处理各类控制器执行过程中出现的异常。也是上面提到的,是DispatcherServlet中的特殊bean,可以自定义配置处理。

    某种程度上讲,HandlerExceptionResolver与你在web应用描述符web.xml文件中能定义的异常映射(exception mapping)很相像,不过它比后者提供了更灵活的方式。比如它能提供异常被抛出时正在执行的是哪个处理器这样的信息。

    * HandlerExceptionResolver 提供resolveException接口

    public interface HandlerExceptionResolver {  
        ModelAndView resolveException(  
                HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);  
    }
    

    * 在BaseController中使用 @ExceptionHandler注解处理异常

    @ExceptionHandler(Exception.class)    
    public Object exceptionHandler(Exception ex, HttpServletResponse response, 
                  HttpServletRequest request) throws IOException {
            String url = "";
            String msg = ex.getMessage();
            Object resultModel = null;        try {            if (ex.getClass() == HttpRequestMethodNotSupportedException.class) {
                    url = "admin/common/500";
                    System.out.println("--------毛有找到对应方法---------");
                } else if (ex.getClass() == ParameterException.class) {//自定义的异常
    
                } else if (ex.getClass() == UnauthorizedException.class) {
                    url = "admin/common/unauth";
                    System.out.println("--------毛有权限---------");
                }
    
                String header = req.getHeader("X-Requested-With");
                boolean isAjax = "XMLHttpRequest".equalsIgnoreCase(header);
                String method = req.getMethod();
                boolean isPost = "POST".equalsIgnoreCase(method);            if (isAjax || isPost) {                return Message.error(msg);
                } else {
                    ModelAndView view = new ModelAndView(url);
                    view.addObject("error", msg);
                    view.addObject("class", ex.getClass());
                    view.addObject("method", request.getRequestURI());                return view;
                }
            } catch (Exception exception) {
                logger.error(exception.getMessage(), exception);            return resultModel;
            } finally {
                logger.error(msg, ex);
                ex.printStackTrace();
            }
        }
        

    * 在web.xml中处理异常

    <!-- 默认的错误处理页面 -->
    <error-page>
        <error-code>403</error-code>
        <location>/403.html</location>
    </error-page>
    <error-page>
        <error-code>404</error-code>
        <location>/404.html</location>
    </error-page>
    <!-- 仅仅在调试的时候注视掉,在正式部署的时候不能注释 --><!-- 这样配置也是可以的,表示发生500错误的时候,转到500.jsp页面处理。 -->
    <error-page> 
        <error-code>500</error-code> 
        <location>/500.html</location> 
    </error-page> 
    <!-- 这样的配置表示如果jsp页面或者servlet发生java.lang.Exception类型(当然包含子类)的异常就会转到500.jsp页面处理。 -->
    <error-page> 
        <exception-type>java.lang.Exception</exception-type> 
        <location>/500.jsp</location> 
    </error-page> 
    <error-page> 
        <exception-type>java.lang.Throwable</exception-type> 
        <location>/500.jsp</location> 
    </error-page>
    <!-- 当error-code和exception-type都配置时,exception-type配置的页面优先级高及出现500错误,发生异常Exception时会跳转到500.jsp-->
    查看原文

    赞 0 收藏 0 评论 0

    认证与成就

    • 获得 30 次点赞
    • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2018-11-27
    个人主页被 509 人浏览