普通视图

发现新文章,点击刷新页面。
昨天以前犀利豆的博客

《SRE google 运维解密》读书笔记 (六)

作者 Zhengxin Diao
2022年5月10日 06:55

负载均衡

前端

使用 DNS 进行负载均衡。在 DNS 回复中提供多个 A 记录或者 AAAA 记录。
虽然 DNS 看起来简单,但是存在不少问题。

  1. DNS 对客户端行为的约束很弱:记录是随机选择的。
  2. 客户端无法识别“最近”的地址
  3. 权威服务器不能主动清楚某个解析器的缓存,DNS 记录需要保持一个相对低的失效值(TTL)。

需要在 DNS 负载后面增加一层虚拟 IP 地址,我们常说的 VIP。

使用 VIP 进行负载均衡
虚拟 IP(VIP) 不是绑定在某一个特定的网络接口上的。很多设备共享。外界看 VIP 是一个独立的普通 IP。VIP 是网络负载均衡器。负载均衡器接收网络数据包,转发给 背后的某个服务器。

负载的方案:

  • 对于无状态的服务,理论上说永远优先负载最小的后端服务
  • 对于有转态的服务
    • 某个连接标识取模
    • 一致性哈希

后端

理想情况

某个服务的负载会完全均匀的分发给所有的后端服务。任何时间点,最忙和最不忙的任务消耗相同数量的 CPU。

识别异常任务

  • 限流
    • 客户端限流
    • 某个后端的活跃请求达到一定数量,客户端将后端标记为异常转态,不再发送请求。
    • 正常情况下,后端请求很快完成,限流几乎不会触发
    • 后端过载了,请求响应慢,客户端就会自动避开这个后端
    • 缺点就是,不精确。后端很可能达到限额之前就过载了。反之亦然。
  • 坡脚鸭任务
    • 客户端视角来看,后端任务有以下几个状态
      • 健康
      • 拒绝连接
      • 坡脚鸭状态
        • 后端服务正常,也能服务请求。但是明确要求客户端停止发送请求。
        • 某个请求进入坡脚鸭状态,需要广播给客户端
        • 处于停止过程中的服务不会给正在处理的请求返回错误
        • 可以实现优雅的下线服务

利用划分子集限制连接池大小

子集划分:限制某个客户端任务需要连接的后端数量。

Google 的 RPC 框架对于每个客户端都会维持一个长连接。如果一个集群的规模过大,客户端就要维护很多长连接。

子集选择算法

  • 随机选择
  • 确定性算法

负载均衡策略

简单轮询

造成效果差的因素如下:

  1. 子集过小
  2. 请求处理的成本不同
  3. 物理服务器的差异
  4. 无法预支的性能因素
    1. 怀邻居(物理服务器上的其他进程)
    2. 任务重启

最闲轮询策略

客户端追踪子集中每个后端任务的活跃请求数量,在活跃请求最小的任务中进行轮询。

最危险的坑:如果一个任务不健康,可能 100% 返回错误。取决于错误的类型,错误回复可能延迟非常低。从而给异常任务分配的大量的请求。
需要将错误信息计算为活跃请求,剔除异常任务。

限制:

  • 活跃的请求数量不一定是后端容量的代表
  • 每个客户端的活跃请求不包括其他客户端发往同一个后端的请求

实践中发现,效果很差。

加权轮询

每个客户端为子集中的每个后端任务保持一个“能力”值。请求仍以轮询方式分发,客户端按照能力值权重比例调节。

实践中效果较好。

《SRE google 运维解密》读书笔记 (三)

作者 Zhengxin Diao
2022年5月2日 06:27

应急事件响应

测试导致的事故

SRE 故意破坏系统,利用这些测试发现系统的薄弱地方。

在某次测试中发现了额外的系统依赖。

响应

  • 终止测试
  • 用以前 测试过的方法 回滚了数据
  • 找到开发者修复了相关问题
  • 制定了周期性测试机制来保证问题不重现

事后总结

好的方面:
事先沟通,有足够信息推测是测试造成的问题。
快速恢复了系统。
遗留一个代办,彻底修复问题。制定了周期性的测试流程。

不好的方面:
虽然评估了,但是还是发生了问题
没有正确遵守响应流程
没有测试“回滚机制”,发生问题后回滚机制失效。

变更导致的事故

某个周五,某个配置文件推送到所有的服务器。触发了 bug。

响应

  • 各个系统开始报警
  • on-call 工程师前往灾难安全屋。(有google 生产环境专线)
  • 5 分钟以后发布这个配置的工程师发现问题,回滚发布
  • 某些服务由于这次发布,触发了别的 bug 一小时后才回复

事后总结

好的方面:

  • 监控系统及时报告问题
  • 问题被检测后,应急流程处理得当。SRE 要保持一些可靠的,低成本的访问系统
  • Google 还有命令行工具和其他访问方式确保能够在其他条件无法访问的时候进行更新和变更回滚。且要频繁测试,让工程师熟悉他们。
  • 限速机制,限制了错误的扩散。抑制了崩溃的速度。

从中学到的:

  • 变更经过了完整的部署测试没有触发 bug,评估并不危险,但是在全球部署的时候触发了 bug
  • 不管风险看起来有多小,都需要严格测试
  • 监控系统在灾难中,发出了很多报警,干扰了 on-call 工程师的工作

流程导致的严重事故

常规自动化测试,对一个集群发送了两次下线请求,触发 bug将全球所有数据中心的的所有机器加入到了磁盘销毁的队列

响应

  • on-call工程师收到报警,将流量导入其他地区
  • 停止了自动化工具
  • 用户导入其他地方,响应时间变长,但是还是可以正常使用
  • 恢复数据。

总结

好的地方

  • 反向代理可以迅速的切换用户流量。
  • 自动化下线虽然一起下线了监控系统,on-call 工程师快速恢复系统。
  • 工程师训练有素,多亏了应急事故管理系统和平时的训练。

从中学到的:

  • 事故的根源在于自动化系统对发出的指令缺乏合适的合理性校验。

所有的问题都有解决方案

系统不但一定会出问题,而且会以没有人能够想到的方式出现问题。但是所有的问题都有对应的解决方案。如果想不到解决问题的方法,那就再更大的范围里面寻求帮助。很多时候触发事故的人对事故最了解。

一旦紧急事故过去之后,一定要留出时间书写事后报告。

向过去学习,而不是重复它

  • 为事故保留记录
  • 提出那些大的,甚至不可能的问题:加入…
  • 鼓励主动测

小结

遇到事故,不要惊慌失措,必要时引入其他人帮助,事后需要记录。把系统改善为能够更好的处理同类故障。

紧急事故管理

无流程管理事故的剖析

  • 过于关注技术问题
  • 沟通不畅
  • 不请自来

事故管理的要素

嵌套式责任分离

事故处理中,让每个人清楚自己的职责。如果一个人处理的事务过多,就应该申请更多人力资源。把一部分任务交给别人。
事故中的角色:

  • 事故总控
    负责组建事故处理团队,负责协调工作
  • 事务处理团队
    具体处理事故的团队,唯一能够对系统进行修改的团队
  • 发言人
    向事务处理团队和关心事故的人发送周期性的通知。维护事故文档。
  • 规划负责人
    为团队提供支持。如填写事故报告,定晚餐,安排交接。

控制中心

很多时候可以设立一个”作战室“。

IRC 处理事故很有帮助。 IRC 很可靠,记录下所有沟通记录。

实时的事故状态文档

事故总控人最重要的职责就是维护事故的实时文档。最好可以多人同时编辑。

明确公开的职责交接

事故总控人的职责能够明确,公开的进行交接很重要。交接结果要宣布给正在处理事故的其他人。

什么时候对外宣布事故

  • 是否需要引入第二个团队来帮助处理问题
  • 是否时候正在影响最终客户
  • 集中分析一个小时后,这个问题是不是依然没有得到解决

最佳实践

  • 划分优先级
  • 事前准备
  • 信任
  • 反思
  • 考虑替代方案
  • 练习
  • 换位思考

Vertx入门到实战—实现钉钉机器人内网穿透代理

作者 Zhengxin Diao
2020年4月13日 00:39

最近研究 Vetrx 简直爱不释手。迫不及待的想给大家介绍一下。

carbon

Vertx 是什么

  • Vertx 是一个运行在 JVM 上,用来构建响应式应用的工具集。
  • 基于 netty 的高性能的,异步的网络库。
  • 对 netty 进行了封装,提供更加友好的 API。
  • 同时实现了一些基于异步调用的库,包括database connection, monitoring, authentication, logging, service discovery, clustering support, etc。

为什么我推荐学习

其实随着技术的发展。异步调用其实越来越普及了。

1、现在随着 RPC 的普及。类似 Dubbo 这样的框架都是基于 NIO 的概念带来的,了解异步编程有助于学习理解框架。

2、响应式编程逐渐由客户端,前端向后端渗透。

3、更容易的编写出高性能的异步服务。

Vertx 的几个重要概念

Event Loop

Event Loop 顾名思义,就是事件循环的。在 Vertx 的生命周期内,会不断的轮询查询事件。

传统的多线程编程模型,每个请求就 fork 一个新的线程对请求进行处理。这样的编程模型有实现起来比较简单,一个连接对应一个线程,如果有大量的请求需要处理,就需要 fork 出大量的线程进行处理,对于操作系统来说调度大量线程造成系统 load 升高。

所以为了能够处理大量请求,就需要过渡到基于 Roactor 模型的 Event Loop上。

/images//event-loop.png

官网的这个图就很形象了。Eventloop 不断的轮训,获取事件然后安排上不同的 Handler 处理对应的Event。

这里要注意的是为了保证程序的正常运行,event 必须是非阻塞的。否则就会造成 eventloop 的阻塞,影响Vertx 的表现。但是现实中的程序肯定不能保证都是非阻塞的,Vertx 也提供了相应的处理阻塞的方法的机制。我们在下面会继续介绍。

Verticle

在 Vertx 中我们经常可以看见 Vertical 组件。

verticle

Verticle 是由 Vert.x 部署和运行的代码块。默认情况一个 Vert.x 实例维护了N(默认情况下N = CPU核数 x 2)个 Event Loop 线程。Verticle 实例可使用任意 Vert.x 支持的编程语言编写,而且一个简单的应用程序也可以包含多种语言编写的 Verticle。

您可以将 Verticle 想成 Actor Model 中的 Actor。

一个应用程序通常是由在同一个 Vert.x 实例中同时运行的许多 Verticle 实例组合而成。不同的 Verticle 实例通过向 Event Bus 收发送消息来相互通信。

Event bus

Vertx 中的 Event bus 如果类比后端常用的 MQ 就更加容易理解了。实际上 Event Bus 就是 Verticle 之间传递 信息的桥梁。

换句话说,就是 Java 通用设计模式中的监听模式,或者是我们常说的 基于 MQ 消息开发模式。

Event bus

回到 Vertx

上文我们讨论了 vertx 的模型和机制,现在人们就看看怎么使用 vertx 开发一个程序。

我会结合之前写的 暴打钉三多的来进行讲解,一切从 Vertx 开始。

1
val vertx = Vertx.vertx()

vertx 是整个 vert.x 框架的核心。通常来说 Vertx 所有的行为就是从 vertx 这个类中产生的。

Don’t call us, we’ll call you

Vert.x 是一个事件驱动框架。所谓事件驱动是指当某件事情发生以后,就做这个动作。

我们再回到标题, “Don’t call us, we’ll call you” 这个原则,其实就是当我们 发现你能完成这项工的时候,我们会找你的。你不需要主动来联系我。

我们通过代码来理解一下 Vertx 是怎么实现这个原则的 :

1
2
3
server.requestHandler(request -> {
request.response().end("hello world!");
});

这个代码块的意思是,每当 server 的 request 被调用的时候,就返回一个 hello world

所以 Vertx 中的 ‘you’ j就是各种各样的 Handler 。大多数时候我们编写 Vertx 的程序,实际上就是在编写Handler 的行为。然后再告诉 Vertx ,每当 XXX 事件触发以后,你就调用 XXX Handler。

Don’t block me

Vertx 是基于事件的,上文我们提到了 Event Loop ,在 Vertx 中,EventLoop 就是一个勤劳的小蜜蜂,不断的去寻找,到底有哪些事件被触发了。然后再执行对应的 Handler。假如执行 Hanlder 的线程,就是 Event Loop 线程。如过 Handler 执行的时间过长。就会阻塞 Event Loop 。造成别的事件触发的时候。Event Loop 还在处理时间花费较长的 Handler。Event loop就不及时的响应其他的事。

但是现实中,不可能所有的事件 都是非阻塞的。比如查询数据库,调用远程接口等等,那怎么办呢?

在事件驱动模型中,大概有两种套路解决,这个问题,比如在 Redis 中,Redis 会十分小心的维护一个时间分片。当某个人物执行事件过长的话,就保存当前事件的状态,然后暂停当前事件,重新由 Event loop 进行调度。防止 Event Loop 被事件阻塞。

还有一种套路,就是把阻塞的事件,交给别的线程来来执行。Event Loop 就可以继续进行事件的循环,防止被阻塞。事实上 Vertx 就是这么操作的。

1
2
3
4
5
6
7
vertx.executeBlocking(promise -> {
// Call some blocking API that takes a significant amount of time to return
String result = someAPI.blockingMethod("hello");
promise.complete(result);
}, res -> {
System.out.println("The result is: " + res.result());
});

如果我们开发的时候意识到这个 Handler 是一个阻塞的,就需要告诉 vertx 这是是一个 Blocking 的需要交给别的线程来处理。

协调异步处理

上文提到. Vertx 是通过 Handler 来处理事件的,但是,很多时候,某个操作,通常需要不止一个 Handler 来对数据进行处理。如果一直使用 callback 的写法,就会形成箭头代码。产生地狱回调的问题。

作为一个异步框架,Vertx 一般使用 Future 来解决回调地狱的问题。理解 Vertx 中的 Future 是编写好的代码的核心。

通常我们理解 Future 只是一个占位符,代表某个操作未来某个时候的结果。不太清楚的可以看我以前写文章。

这里需要特别指出的是 Vertx 的 Future 和 Jdk 里面的 CompletableFuture 原理和理念类似,但是使用起来有很大的区别的。

Jdk 里面的 CompletableFuture 是可以直接使用 result() 阻塞的等待结果,但是 Vertx 中的 Future 如果直接使用 result() ,就会立刻从 Future 中取出结果,而不是阻塞的等待结果,就很容易收获一个 Null。

明确这个区别以后,写起代码就不会出错了。

Event Bus

如果在日常开发中使用过消息系统,就很容易理解 Vertx 中的 Event bus 了。官方文档把 Event bus 比作 Vertx 的神经系统,其实我们就认为,Event bus是 Vertx 的消息系统,就好了。

钉钉内网穿透代理的的开发

这个小 Demo 麻雀虽小但是包含了 Vertx 几个关键组件的使用。写这个 Demo 的时候,正好在学习 Kotlin 所以顺手就用 kotlin 写了。如果写过 Java 或者 Typescript 那你也能很容易的看懂。

项目包含了

  • Http Service 用于接收钉钉的回调
  • WebSocket Service 用于向 Client 推送收到的回调,达到内网穿透的目的。
  • Vertx Config 用于配置项目相关参数,便于使用
  • Event Bus 的使用,用于 Http Service 和 WebSocket 之间传递消息。

先来一个 Verticle

Gradle 配置文件如下先引入包:

1
2
3
implementation ("io.vertx:vertx-core:3.8.5")
implementation ("io.vertx:vertx-web:3.8.5")
implementation ("io.vertx:vertx-lang-kotlin:3.8.5")

上文我我们已经介绍了 Verticle 是什么了,为了方便开发,Vertx 给我们提供了一个 AbstractVerticle 抽象类。直接继承:

1
2
class DingVerticle : AbstractVerticle() {
}

AbstractVerticle 中包含了 Vericle 常用的一些方法。

我们可以重写 start() 方法,来初始化我们 Verticle 的行为。

HttpService 的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun start() {
val httpServer = vertx.createHttpServer()
val router = Router.router(vertx)
router.post("/ding/api").handler{event ->
val request = event.request()
request.bodyHandler { t ->
println(t)
}
event.response().end();
}
httpServer.requestHandler(router);
httpServer.listen(8080);
}

代码比较简单:

  1. 创建一个 httpService
  2. 设置一个 Router,如果写过 Spring Mvc 相关的代码。这里的 Router 就类似 Controller 里面的 RequestMapping 。用于指定一个 Http 请求 URI 和 Method 对应的 Handler。这里的 Handler 是一个 lambda 表达式。只是简单的把请求的 body 打印出来。
  3. 将 Router 加入到 httpService 中,并监听 8080 端口。

WebSocketService

webSocket协议是这个 proxy 的关键,因为 WebSocket 不同于 Http,是双向通通信的。依赖这个特性我们可以把消息“推到”内网。达到内网“穿透”的目的。

1
2
3
4
5
6
7
8
httpServer.webSocketHandler { webSocket: ServerWebSocket ->
val binaryHandlerID = webSocket.binaryHandlerID()
webSocket.endHandler() {
log.info("end", binaryHandlerID)
}
webSocket.writeTextMessage("欢迎使用 xilidou 钉钉 代理")
webSocket.writeTextMessage("连接成功")
}

代码也比较简单,就是向 Vertx 注册一个处理 WebSocket 的 Handler。

Event Bus 的使用

作为代理最核心的功能就是转发钉钉的回调消息,前面我说到,Event Bus 在 Vertx 中起到了“神经系统的作用”实际上 ,换句话说,就是http 服务收到回调的时候,可以通过 Event Bus 发出消息。WebSocket 在收到 Event Bus 发来的消息的时候,推送给客户端。如下图看图:

为了方便理解,我们就使用 MQ 里面通常的概念生产者和消费者。

所以我们使用在 HttpService 中注册一个生产者,收到钉钉的回调以后,把消息转发出来。

为了便于编写,我们可以单独写一个 HttpHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1
class HttpHandler(private val eventBus: EventBus) : Handler<RoutingContext> {

private val log = LoggerFactory.getLogger(this.javaClass);

override fun handle(event: RoutingContext) {
val request = event.request()
request.bodyHandler { t->
val jsonObject = JsonObject(t)
val toString = jsonObject.toString()
log.info("request is {}",toString);
// 2
eventBus.publish("callback", toString)
}
event.response().end("ok")
}
}

这里需要注意几个问题:

  1. 我们需要使用 Event Bus 发送消息,所以需要在构造函数里面传入一个 Event Bus
  2. 我们在收到消息以后,可以先将数据转换为 Json 字符串,然后发送消息,注意这里使用的是 publish() 是广播的意思,这样所有订阅的客户端都能收到新消息。

有了生产者,并发出了数据,我们就可以,在 WebSocket 里面消费这个消息,然后推送给客户端了

再来写一个 WebSocket 的 Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//1
class WebSocketHandler(private val eventBus: EventBus) : Handler<ServerWebSocket> {

private val log = LoggerFactory.getLogger(this.javaClass)

override fun handle(webSocket: ServerWebSocket) {
val binaryHandlerID = webSocket.binaryHandlerID()

//2
val consumer = eventBus.consumer<String>("callback") { message ->
val body = message.body()
log.info("send message {}", body)
//3
webSocket.writeTextMessage(body)
}
webSocket.endHandler() {
log.info("end", binaryHandlerID)
//4
consumer.unregister();
}
webSocket.writeTextMessage("欢迎使用 xilidou 钉钉 代理")
webSocket.writeTextMessage("连接成功")
}
}

这里需要注意几个问题:

  1. 初始化的时候需要注入 eventBus
  2. 写一个 consumer() 消费 HttpHandler 发来的消息
  3. 将消息写入到 webSocket 中,发送给 Client
  4. WebSocket 断开后需要回收 consumer

初始化 Vertx

做了那么多准备终于可以初始化我们的 Vertx 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DingVerticleV2: AbstractVerticle(){
override fun start() {

//2
val eventBus = vertx.eventBus()
val httpServer = vertx.createHttpServer()

val router = Router.router(vertx);
//3
router.post("/api/ding").handler(HttpHandler(eventBus));
httpServer.requestHandler(router);

//4
httpServer.webSocketHandler(WebSocketHandler(eventBus));
httpServer.listen(8080);
}
}

//1
fun main() {
val vertx = Vertx.vertx()
vertx.deployVerticle(DingVerticleV2())
}

这里需要注意几个问题:

  1. 初始化 Vertx 并部署他
  2. 初始化 eventBus
  3. 注册 HttpHandler
  4. 注册 WebSocketHandler

总结

  • Vertx 是一个工具,不是框架,所以可以很方便的与其他框架组合。
  • Vertx 是一个基于 Netty 的异步框架。我们可以向编写同步代码一样,编写异步代码。
  • vertx 在代码中主要有两个作用,一个是初始化组件,比如 :
1
2
val eventBus = vertx.eventBus()
val httpServer = vertx.createHttpServer()

还有一个是注册 Handler:

1
httpServer.webSocketHandler(WebSocketHandler(eventBus));
  • Event Bus 是一个消息系统。用于不同的 Handler 直接传递数据,简化开发。

相关连接

  1. 使用教程 钉钉机器人回调内网穿透代理–使用篇
  2. Github 地址: Github
  3. 官网教程:A gentle guide to asynchronous programming with Eclipse Vert.x for Java developers;
  4. Vert.x Core Manual

欢迎关注我的微信公众号:

二维码

钉钉机器人回调内网穿透代理--使用篇

作者 Zhengxin Diao
2020年3月26日 07:26

“山川异域,风月同钉”,被钉钉暴打的你,是不是已经想写一个机器人调戏一下钉钉了。在写机器人的时候,钉钉机器人的回调需要填写一个公网 http 地址。

这还没开发机器人,就没有 http 服务,没有 http 服务就收不到钉钉的回调,没有回调就不能调试机器人。不能调试机器人,就不能上线。

black

又一次陷入了被钉钉暴打的死循环,办法总比问题多,所以为了解决这个问题。我们就需要一个公网代理。所以我们就来撸一个。

这里注意一下,由于一般开发人员都处在内网环境。要想让代理做内网穿透,技术比较复杂。所以我们就换个思路。我们可以利用 Websocket 的双工的特性。接入代理,当代理收到钉钉的回调的时候,把消息推到我们本地开发环境。提升我们开发的效率。见下图:

dingproxy.jpg

使用方法

1
2
3
4
5
6
git clone https://github.com/diaozxin007/DingTalkProxy
cd DingProxyServer
./gradlew build
java -jar build/libs/dingWs-all.jar
# 如果需要在后台运行
nohup java -jar build/libs/dingWs-1.0.0-all.jar &>> nohup.out & tailf nohup.out

可以修改 resources 下的 server.properties

1
2
3
4
# 监听端口
server.port=8080
# 钉钉回调的 uri
server.api=/ding/api

然后重新运行:

1
./gradlew build

这个时候,proxy 已经开始正常运行了。

如果只是想看看一看钉钉回调的报文,那就可以直接使用 [websock-test] (http://www.websocket-test.com/) GUI 调试工具。

如果想在代码里面使用可以参考 DingProxyClinet 里面的代码。

注意事项

Q:1、为什么我连不上服务?

A:确认服务是否只开启了 https,如果开启了 https, 需要把协议头修改为 wss。

Q:2、我还是连不上?

A:需要确认 nginx 的配置,是否支持 WebSocket。

可以在 nginx 的配置中增加

1
2
3
4
5
6
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# 如果频繁超时断开可以配置
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;

Q:3、除了做钉钉的代理,还能干什么?

A: 理论上可以代理一切请求,然后转换为 String 通过 WebSocket 推送到客户端。

Q:4、我懒得部署服务了

A:可以使用我提供的公益服务

在回调接口中填写:

WebSocket 地址为:

  • wss://api.xilidou.com

为了防止滥用,每个客户端每次连接只能接收 10 条消息,然后会被断开。

Github 传送门

下一篇文章将会具体讲解,如何使用 vertx 实现这个代理。敬请期待。

那些有趣的代码(二)--偏不听父母话的 Tomcat 类加载器

作者 Zhengxin Diao
2019年10月28日 07:32

看 Tomcat 的源码越看越有趣。Tomcat 的代码总有一种处处都有那么一点调皮的感觉。今天就聊一聊 Tomcat 的类加载机制。

了解过 JVM 的类加载一定知道,JVM 类加载的双亲委派机制。但是 Tomcat 却打破了 JVM 固有的双亲委派加载机制。

JVM 的类加载

首先需要明确一下类加载是什么?

  • Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。

JVM 预定义的三个加载器:

  • 启动类加载器(Bootstrap ClassLoader):是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
  • 标准扩展类加载器(Extension ClassLoader):是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

双亲委派机制:

所谓双亲委派机制,这里要指出的是,其实双亲委派来源于英文的 ”parents delegate“,仅仅表示的只是”父辈“,可见翻译的人不但英文是半吊子,而且也不了解 JVM 的类加载策略,造成了很大的误解。尤其是这个”双“字在初学的时候给我造成了极大的干扰。所以换个说法,应该是”父辈代理“。

类加载的时候,把加载的这个动作递归的委托给父辈,由父辈代劳,只有父辈无法加载时,才会由自己加载。

双亲委派加载模型:

parents

这里需要特别注意的是加载器的关系并非是继承的关系。我们看代码:

1
2
3
4
5
6
static class ExtClassLoader extends URLClassLoader{
... ...
}
static class AppClassLoader extends URLClassLoader{
... ...
}

二者同时继承了 URLClassLoader ,继承关系如下:

appClassLoader

怎么实现委托机制呢?在 ClassLoader 里面有几处比较重要的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 尝试使用 父辈的 loadClass 方法
c = parent.loadClass(name, false);
} else {
// 如果没有 父辈的 classLoader 就使用 bootstrap classLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父辈没法加载这个 class,就自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

// 根据类名 寻找 class。我们在之前我们讲过,不通过的 classLoader 加载的 class 的位置不同。
protected Class<?> findClass(String name) throws ClassNotFoundException {
return defineClass(name, res);
}

}
  1. 首先在初始化 ClassLoader 的时候需要指定自己的 parent 是谁?(这很重要)
  2. 先检查类有没被加载,如果类已经被加载了,直接返回。
  3. 如果没有被加载,则通过 parent 的 loadClass 来尝试加载类。(双亲委派的核心逻辑)
  4. 找不到 parent 的时候使用 bootstrap ClassLoader 进行加载。
  5. 如果委托的 parent 没法加载类,那就自己加载。

Tomcat 的类加载

Tomcat 自己实现了自己的类加载器 WebAppClassLoader。类图关系图如下:

WebAppClassLoader

我们就来看看 Tomcat 的类加载器是怎么打破双亲委派的机制的。我们先看代码:

findClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;

// 先在自己的 Web 应用目录下查找 class
clazz = findClassInternal(name);

// 找不到 在交由父类来处理
if ((clazz == null) && hasExternalRepositories) {
clazz = super.findClass(name);
}
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}

对于 Tomcat 的类加载的 findClass 方法:

  • 首先在 web 目录下查找。(重要)
  • 找不到再交由父类的 findClass 来处理。
  • 都找不到,那就抛出 ClassNotFoundException。

loadClass 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1. 先在本地cache查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//2. 从系统类加载器的cache中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 3. 尝试用ExtClassLoader类加载器类加载
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 尝试在本地目录搜索class并加载
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}

总结一下加载的步骤:

  1. 先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。
  2. 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。
  3. 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加载。
  4. 如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。
  5. 加载依然失败,才使用 AppClassLoader 继续加载。
  6. 都没有加载成功的话,抛出异常。

总结一下以上步骤,WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。

  • 保证了基础类不会被同时加载。
  • 由保证了在同一个 Tomcat 下不同 web 之间的 class 是相互隔离的。

more

准备把有趣的代码这个系列慢慢写下去,发现编程的乐趣:

那些有趣的代码(一)–有点萌的 Tomcat 的线程池

❌
❌