开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Android稳定性:Looper兜底框架实现线上容灾(二)

发表于 2023-11-03

背景

此前分享过相关内容,见: Android稳定性:可远程配置化的Looper兜底框架

App Crash对于用户来讲是一种最糟糕的体验,它会导致流程中断、app口碑变差、app卸载、用户流失、订单流失等。相关数据显示,当Android App的崩溃率超过0.4%的时候,活跃用户有明显下降态势。

项目思路来源于一次提问:有没有办法打造一个永不崩溃的app?

继续深挖这个问题后,我们其实有几个问题需要考虑:

  • 如何打造永不崩溃的 app
  • 当这样做了之后,app 还能正常运行吗?
  • 怎么才能在吃掉异常的同事,让主线程继续运行?
  • 异常被吃掉之后会有什么影响?
  • 到底什么异常需要被吃掉或者说可以吃掉?
  • 吃掉异常能给带来什么好处?是否能对线上问题进行容灾?

这些问题在 Android稳定性:可远程配置化的Looper兜底框架 都被一一解答了。

如何实现、如何更好的实现

实现代码参考 demo 项目: scuzoutao/AndroidCrashProtect

主要的核心代码就两部分:

  1. 按配置判断是否需要保护
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
kotlin复制代码fun needBandage(throwable: Throwable): Boolean {                                                                            
if (crashPortrayConfig.isNullOrEmpty()) {
return false
}

val config: List<CrashPortray>? = crashPortrayConfig
if (config.isNullOrEmpty()) {
return false
}
for (i in config.indices) {
val crashPortray = config[i]
if (!crashPortray.valid()) {
continue
}

//1. app 版本号
if (crashPortray.appVersion.isNotEmpty()
&& !crashPortray.appVersion.contains(actionImpl.getVersionName(application))
) {
continue
}

//2. os_version
if (crashPortray.osVersion.isNotEmpty()
&& !crashPortray.osVersion.contains(Build.VERSION.SDK_INT)
) {
continue
}

//3. model
if (crashPortray.model.isNotEmpty()
&& crashPortray.model.firstOrNull { Build.MODEL.equals(it, true) } == null
) {
continue
}

val throwableName = throwable.javaClass.simpleName
val message = throwable.message ?: ""
//4. class_name
if (crashPortray.className.isNotEmpty()
&& crashPortray.className != throwableName
) {
continue
}

//5. message
if (crashPortray.message.isNotEmpty() && !message.contains(crashPortray.message)
) {
continue
}

//6. stack
if (crashPortray.stack.isNotEmpty()) {
var match = false
throwable.stackTrace.forEach { element ->
val str = element.toString()
if (crashPortray.stack.find { str.contains(it) } != null) {
match = true
return@forEach
}
}
if (!match) {
continue
}
}

//7. 相应操作
if (crashPortray.clearCache == 1) {
actionImpl.cleanCache(application)
}
if (crashPortray.finishPage == 1) {
actionImpl.finishCurrentPage()
}
if (crashPortray.toast.isNotEmpty()) {
actionImpl.showToast(application, crashPortray.toast)
}
return true
}
return false
}
  1. 实现保护,looper 兜底:
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
kotlin复制代码override fun uncaughtException(t: Thread, e: Throwable) {       
if (CrashPortrayHelper.needBandage(e)) {
bandage()
return
}

//崩吧
oldHandler?.uncaughtException(t, e)
}

/**
* 让当前线程恢复运行
*/
private fun bandage() {
while (true) {
try {
if (Looper.myLooper() == null) {
Looper.prepare()
}
Looper.loop()
} catch (e: Exception) {
uncaughtException(Thread.currentThread(), e)
break
}
}
}

如何线上容灾

其实思路很简单,问几个问题,答完就知道了。

  1. 崩溃兜底机制可以保护 app,已知我们用配置文件来描述崩溃画像,那配置文件能否远程下发?
  2. 配置文件远程下发后,app 拉下来后能否立即生效?
  3. 假如线上出了个崩溃,崩溃本身涉及代码流程不重要,但是会让 app 直接挂掉,能否线上修改配置文件,将这个崩溃包括进去进行保护,后续在下版本修复之?

崩溃画像实例

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
css复制代码[  {    "class_name": "",    "message": "No space left on device",    "stack": [],
"app_version": [],
"clear_cache": 1,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "BadTokenException",
"message": "",
"stack": [],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "IllegalStateException",
"message": "not running",
"stack": [
"Daemons"
],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "",
"message": "Activity client record must not be null to execute",
"stack": [],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "",
"message": "The previous transaction has not been applied or aborted",
"stack": [],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
}
]

好了,拜拜。

你可能感兴趣

Android QUIC 实践 - 基于 OKHttp 扩展出 Cronet 拦截器 - 掘金 (juejin.cn)

Android启动优化实践 - 秒开率从17%提升至75% - 掘金 (juejin.cn)

如何科学的进行Android包体积优化 - 掘金 (juejin.cn)

Android稳定性:Looper兜底框架实现线上容灾(二) - 掘金 (juejin.cn)

基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)

记 AndroidStudio Tracer工具导致的编译失败 - 掘金 (juejin.cn)

Android 启动优化案例-WebView非预期初始化排查 - 掘金 (juejin.cn)

chromium-net - 跟随 Cronet 的脚步探索大致流程(1) - 掘金 (juejin.cn)

Android稳定性:可远程配置化的Looper兜底框架 - 掘金 (juejin.cn)

一类有趣的无限缓存OOM现象 - 掘金 (juejin.cn)

Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) - 掘金 (juejin.cn)

Android - 彻底消灭OOM的实战经验分享(千分之1.5 -> 万分之0.2) - 掘金 (juejin.cn)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

听说前端出大事儿了

发表于 2023-11-02

公众号「古时的风筝」,专注于后端技术,尤其是 Java 及周边生态。

个人博客:www.moonkite.cn

大家好,我是风筝

最近这两天,在前端圈最火的图片莫过于下面这张了。

这是一段 React 代码,就算你完全没用过 React 也没关系,一眼看过去就能看到其中最敏感的一句代码,就是那句 SQL 。
咱们把这端代码简化一下,大概就是下面这个样子。

1
2
3
4
5
6
javascript复制代码<button formAction={ async() => (
"use server"
await sql`INSERT INTO Bookmarks (slug) VALUES (${slug});`;
)}>
提交
</button>

意思就是在页面上点击一个叫做「提交」的按钮,触发一个 formAction(提交表单)的动作。这有点看到了当年 JSP 和 PHP 的味道了。这还不是最神奇的,最厉害的是提交表单要执行的动作不是一个接口请求,而是直接执行一条 SQL 。使用 use server标签,标示这是一个服务端端执行的方法。

一时间竟分不出这到底是前端还是后端了。

这么发展下去,React 就是妥妥的全栈语言了。此时的 PHP 在旁边笑而不语,还说我不是世界上最好的语言,你们终究还是会活成我的样子。

自从前后端分离以来,前端框架可谓是百花齐放,一片繁荣。最早的是 Angular,然后就是 React 和 Vue,到现在基本都是 Vue 和 React 的天下了。

如果你用过原生的 JavaScript 或者 JQuery,那就能感受到 React 或者 Vue 的出现,完全改变了前端的开发方式。

React 目前的最新版本是 18,支持 ES(ECMAScript) 和TS(TypeScript),除了画界面和写CSS之外,完全可以把它当做一个面向对象的语言工具使用。

这次支持执行执行后端 SQL 的特性是 Next.js 开放的,Next.js 是 在React 框架上再次高度封装的一个框架。有点像 Spring Boot与 Spring 的关系,Spring 好比是 React,Spring Boot 就是 Next.js。

本来好好的前端,为什么要直接支持写 SQL 呢,这也并不是无迹可寻的。前两年,React 就推出了React Server Components 。大致的意思就是说这是一种服务器端组件,为了提高性能,由服务器直接渲染,渲染出来的结果通过元数据的形式发给前端 React,React 拿到元数据后与现有的 UI 树合并,最终由浏览器渲染。

React 官方是大力推荐 Next.js 的,有了官方推荐加上本身已经支持的服务器端组件,Next.js 不知道是出于什么样的目的,竟然直接支持执行服务端方法了。之前要通过 HTTP 请求,现在直接就跳过这一步了。

说实话,站在一个前端框架的视角上,加上我本身是一个后端开发,我是有一点看不懂这个操作了。服务端组件还能理解,毕竟开发效率和性能要兼顾,这无可厚非。

但是直接支持服务端执行,是技术的轮回(照着PHP的方向)还是技术的变革呢,此时的 Next.js 就像是一个站在十字路口的汽车,油都加满了,就看各位开发者驾驶员开着它往哪边走了。

反正依我看来,我是觉得前端框架越简单越好。原因很简单,搞这么复杂,我都快不会用了。

不光是我看不懂,毕竟咱是个后端外行,不是专业的。但是前端同学也是一片调侃,调侃的大致意思就是 React Next.js 啥都能干,既然连后端都能整了,那其他的也能全栈了。

比如有人调侃给 Next.js 赋能 AI,使用 use ai,直接 prompt 编程了。

还有赋能 k8s 的

以及赋能二进制编程的

最厉害的,还有赋能删库跑路的。

调侃归调侃,既然口子已经开了,就会有过来吃螃蟹的人,至于之后会变成什么样子,只能拭目以待了。

推荐阅读

JDK21 有哪些新特性呢

什么时候都用微服务,只会害了你

搞明白什么是零拷贝,就是这么简单

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Usap64 进程莫名死锁问题分析 问题背景 客户端解析 服

发表于 2023-11-01

问题背景

近期我们项目在自动化测试中发生一起奇怪的冻屏现象,它的直接原因是启动某个应用程序时,system_server 进程的 ActivityManager:procStart 线程卡在函数 attemptUsapSendArgsAndGetResult 上等待 socket 数据返回,然而一直没有数据返回。

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
php复制代码"ActivityManager:procStart" prio=5 tid=33 Native
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x13a81920 self=0xb400007cbbd42800
| sysTid=1882 nice=-2 cgrp=foreground sched=1073741824/0 handle=0x7ca6d67cb0
| state=S schedstat=( 87741769 194728696 264 ) utm=2 stm=6 core=3 HZ=100
| stack=0x7ca6c64000-0x7ca6c66000 stackSize=1039KB
| held mutexes=
native: #00 pc 000ed8e4 /apex/com.android.runtime/lib64/bionic/libc.so (__recvmsg+4)
native: #01 pc 000a2f40 /apex/com.android.runtime/lib64/bionic/libc.so (recvmsg+48)
native: #02 pc 00014974 /system/lib64/libbase.so (android::base::ReceiveFileDescriptorVector+372)
native: #03 pc 00195474 /system/lib64/libandroid_runtime.so (android::socket_read_all+84)
native: #04 pc 00194c8c /system/lib64/libandroid_runtime.so (android::socket_readba+332)
at android.net.LocalSocketImpl.readba_native(Native method)
at android.net.LocalSocketImpl.-$$Nest$mreadba_native(unavailable:0)
at android.net.LocalSocketImpl$SocketInputStream.read(LocalSocketImpl.java:110)
- locked <0x000ba91b> (a java.lang.Object)
at java.io.DataInputStream.readFully(DataInputStream.java:203)
at java.io.DataInputStream.readInt(DataInputStream.java:394)
at android.os.ZygoteProcess.attemptUsapSendArgsAndGetResult(ZygoteProcess.java:540)
at android.os.ZygoteProcess.zygoteSendArgsAndGetResult(ZygoteProcess.java:484)
at android.os.ZygoteProcess.startViaZygote(ZygoteProcess.java:828)
- locked <0x085f6ab8> (a java.lang.Object)
at android.os.ZygoteProcess.start(ZygoteProcess.java:405)
at android.os.Process.start(Process.java:755)
at com.android.server.am.ProcessList.startProcess(ProcessList.java:2543)
at com.android.server.am.ProcessList.lambda$handleProcessStart$1(ProcessList.java:2193)
at com.android.server.am.ProcessList.$r8$lambda$swBGAihsWK-ua7Y-9peg-di3_lY(unavailable:0)
at com.android.server.am.ProcessList$$ExternalSyntheticLambda1.run(unavailable:22)
at com.android.server.am.ProcessList.handleProcessStart(ProcessList.java:2219)
at com.android.server.am.ProcessList.lambda$startProcessLocked$0(ProcessList.java:2159)
at com.android.server.am.ProcessList.$r8$lambda$RYfds-QGIevfi1LguTPr20cBG60(unavailable:0)
at com.android.server.am.ProcessList$$ExternalSyntheticLambda5.run(unavailable:22)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:222)
at android.os.Looper.loop(Looper.java:314)
at android.os.HandlerThread.run(HandlerThread.java:67)
at com.android.server.ServiceThread.run(ServiceThread.java:46)

对此问题,我们需要知道此时 system_server 等待的 socket 服务端进程是谁,此时服务端进程又在干吗,熟悉此部分代码,从堆栈上看可以知道等待的服务端进程是 usap64,但实际上后台有多个 usap64 进程,一个个排查当然也是可行的,这里我还是采用内存分析的方式来锁定。将 system_server 内存转储成 Coredump 文件。

客户端解析

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
less复制代码art-parser> bt 1882
"ActivityManager:procStart" prio=5 tid=33 Native
| group="main" sCount=0 ucsCount=0 flags=0 obj=0x13a81920 self=0xb400007cbbd42800
| sysTid=1882 nice=<unknown> cgrp=<unknown> sched=<unknown> handle=0x7ca6d67cb0
| stack=0x7ca6c64000-0x7ca6c66000 stackSize=0x103cb0
| held mutexes=
x0 0x00000000000002c4 x1 0x0000007ca6d66650 x2 0x0000000040004028 x3 0x0000007ca6d66580
x4 0x0000007ca6d66600 x5 0x0000000000000004 x6 0x0000000000000000 x7 0x0000000000000000
x8 0x00000000000000d4 x9 0x0000000000000001 x10 0x0000000000000110 x11 0x0000000000000003
x12 0x0000007cbbd511dc x13 0xb400007cbbf27b40 x14 0x0000007ca6d668f0 x15 0x0000000013c958b8
x16 0x0000007e0b2fd710 x17 0x0000007dec500f10 x18 0x0000007ca6ba6000 x19 0x0000007ca6d66650
x20 0x0000000000000040 x21 0x0000007ca6d68000 x22 0x00000000000002c4 x23 0x0000000000000000
x24 0x0000007ca6d68000 x25 0x0000007ca6d664f0 x26 0x0000007ca6d66600 x27 0x0000007ca6d66718
x28 0x0000000000000000 x29 0x0000007ca6d66490
lr 0x0000007dec500f44 sp 0x0000007ca6d66450 pc 0x0000007dec54b8e4 pst 0x0000000060001000
FP[0x7ca6d66490] PC[0x7dec54b8e4] native: #00 (__recvmsg+0x4) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7ca6d66490] PC[0x7dec500f44] native: #01 (recvmsg+0x34) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7ca6d666a0] PC[0x7e0b2d0978] native: #02 (android::base::ReceiveFileDescriptorVector(android::base::borrowed_fd, void*, unsigned long, unsigned long, std::__1::vector<android::base::unique_fd_impl<android::base::DefaultCloser>, std::__1::allocator<android::base::unique_fd_impl<android::base::DefaultCloser> > >*)+0x178) /system/lib64/libbase.so
FP[0x7ca6d66790] PC[0x7de88c8478] native: #03 (android::register_android_net_LocalSocketImpl(_JNIEnv*)+0x1158) /system/lib64/libandroid_runtime.so
FP[0x7ca6d667f0] PC[0x7de88c7c90] native: #04 (android::register_android_net_LocalSocketImpl(_JNIEnv*)+0x970) /system/lib64/libandroid_runtime.so
>>> analysis 'art::OatHeader::kOatVersion' ...
>>> 'art::OatHeader::kOatVersion' = 238 ?
QF[0x7ca6d66840] PC[0x0000000000] at dex-pc 0x0000000000 android.net.LocalSocketImpl.readba_native(Native method) //AM[0x70954228]
QF[0x7ca6d668f0] PC[0x0071a845ec] at dex-pc 0x7d655c363c android.net.LocalSocketImpl$SocketInputStream.read //AM[0x706be940]
QF[0x7ca6d66960] PC[0x00713a896c] at dex-pc 0x7d664d19c0 java.io.DataInputStream.readInt //AM[0x6fd15fd0]
QF[0x7ca6d669c0] PC[0x7d66d31334] at dex-pc 0x7d64a52fc4 android.os.ZygoteProcess.attemptUsapSendArgsAndGetResult //AM[0x70981b90]
QF[0x7ca6d66ad0] PC[0x0071735b40] at dex-pc 0x7d64a53850 android.os.ZygoteProcess.zygoteSendArgsAndGetResult //AM[0x70981df0]
QF[0x7ca6d66b50] PC[0x00717356b8] at dex-pc 0x7d64a5374a android.os.ZygoteProcess.startViaZygote //AM[0x70981d90]
QF[0x7ca6d66bd0] PC[0x7d66d31a5c] at dex-pc 0x7d64a53170 android.os.ZygoteProcess.start //AM[0x70981f70]
QF[0x7ca6d66e70] PC[0x7d66d319dc] at dex-pc 0x7d64a37090 android.os.Process.start //AM[0x7097ef98]
QF[0x7ca6d670f0] PC[0x7d51d1a288] at dex-pc 0x7d510b1e98 com.android.server.am.ProcessList.startProcess //AM[0x9a9d0718]
QF[0x7ca6d67200] PC[0x7d51d188a0] at dex-pc 0x7d510b9340 com.android.server.am.ProcessList.lambda$handleProcessStart$1 //AM[0x9a9d04f8]
QF[0x7ca6d672c0] PC[0x7d51d15a00] at dex-pc 0x7d510b0140 com.android.server.am.ProcessList$$ExternalSyntheticLambda1.run //AM[0x7d43255648]
QF[0x7ca6d67350] PC[0x7d66d32158] at dex-pc 0x7d510b8b1a com.android.server.am.ProcessList.handleProcessStart //AM[0x9a9d0438]
QF[0x7ca6d67510] PC[0x7d66d319dc] at dex-pc 0x7d510b94c4 com.android.server.am.ProcessList.lambda$startProcessLocked$0 //AM[0x9a9d0538]
QF[0x7ca6d67660] PC[0x7d66d319dc] at dex-pc 0x7d510b758c com.android.server.am.ProcessList.$r8$lambda$RYfds-QGIevfi1LguTPr20cBG60 //AM[0x9a9d0138]
QF[0x7ca6d677b0] PC[0x7d66d30b68] at dex-pc 0x7d510b02a0 com.android.server.am.ProcessList$$ExternalSyntheticLambda5.run //AM[0x7d432553f0]
QF[0x7ca6d67900] PC[0x0071aec7e0] at dex-pc 0x7d649fcdd8 android.os.Handler.dispatchMessage //AM[0x707206c8]
QF[0x7ca6d67930] PC[0x0071aefd10] at dex-pc 0x7d64a241fc android.os.Looper.loopOnce //AM[0x70721830]
QF[0x7ca6d67a00] PC[0x0071aef840] at dex-pc 0x7d64a24926 android.os.Looper.loop //AM[0x70721810]
QF[0x7ca6d67a50] PC[0x0071aee7d4] at dex-pc 0x7d649fc448 android.os.HandlerThread.run //AM[0x70720d58]
QF[0x7ca6d67aa0] PC[0x7d51c7bf3c] at dex-pc 0x7d50f6da60 com.android.server.ServiceThread.run //AM[0x9a9c4370]
方法一 从寄存器上看很容易知道 sock fd = 0x2c4 = 708
方法二 cat /proc/1745/task/1882/syscall系统调用ID 参数0 ~ 参数7212 0x2c4 0x7ca6d66650 0x40004028 0x7ca6d66580 0x7ca6d66600 0x4 0x7ca6d66450 0x7dec54b8e8

从调用栈上看,LocalSocket 创建的 socket 使用的是 AF_UNIX 协议族,因此,我们可以通过 ino 号查询该 socket 状态。

1
2
3
4
5
6
7
arduino复制代码typedef enum {
SS_FREE = 0, /* not allocated */
SS_UNCONNECTED, /* unconnected to any socket */
SS_CONNECTING, /* in process of connecting */
SS_CONNECTED, /* connected to socket */
SS_DISCONNECTING /* in process of disconnecting */
} socket_state;
1
2
3
4
5
6
7
8
9
shell复制代码# ls -lh /proc/1745/fd | grep 708
lrwx------ 1 system system 64 2023-10-25 19:39 708 -> socket:[125175]

# netstat -p 1745 | grep 125175
unix 3 [ ] STREAM CONNECTED 125175 1745/system_server @jdwp-control
或者
# cat /proc/net/unix | grep 125175
Num RefCount Protocol Flags Type St Inode Path
0000000000000000: 00000003 00000000 00000000 0001 03 125175

从 socket 状态上看,已经连上服务端,等待服务端返回数据,我们先查看这个 attemptUsapSendArgsAndGetResult 函数的参数内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码art-parser> disassemble 0x70981df0 -i 0x7d64a53850
android.os.Process$ProcessStartResult android.os.ZygoteProcess.zygoteSendArgsAndGetResult(android.os.ZygoteProcess$ZygoteState, int, java.util.ArrayList) [dex_method_idx=10385]
DEX CODE:
0x7d64a53850: 3070 2871 0054 | invoke-direct {v4, v5, v0}, android.os.Process$ProcessStartResult android.os.ZygoteProcess.attemptUsapSendArgsAndGetResult(android.os.ZygoteProcess$ZygoteState, java.lang.String) // method@10353

QF[0x7ca6d66ad0] PC[0x0071735b40] at dex-pc 0x7d64a53850 android.os.ZygoteProcess.zygoteSendArgsAndGetResult //AM[0x70981df0]
{
StackMap[28] (code_region=[0x71735820-0x71735b58], native_pc=0x320, dex_pc=0x60, register_mask=0x2dc00000)
Virtual registers
{
v0 = r22 v1 = r28 v2 = r29 v4 = r23
v5 = r24 v6 = r25 v7 = r26
}
Physical registers
{
x19 = 0xb400007cbbd42800 x20 = 0x0 x21 = 0xb400007cbbd428c8 x22 = 0x13a81aa8
x23 = 0x728961c0 x24 = 0x13a81a80 x25 = 0x1 x26 = 0x13a825d8
x27 = 0x6fbab3c8 x28 = 0x1 x29 = 0x0 x30 = 0x71735b40
}
}
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
sql复制代码art-parser> p 0x13a81aa8
Size: 0xb30
Object Name: java.lang.String
iFields of java.lang.String
[0x8] int count = 0xb19
[0xc] int hash = 0x0
[16
--runtime-args
--setuid=10092
--setgid=10092
--runtime-flags=18876416
--mount-external-default
--target-sdk-version=29
--setgroups=3003,50092,20092,9997
--nice-name=com.android.statementservice
--seinfo=platform:privapp:targetSdkVersion=29:complete
--app-data-dir=/data/user/0/com.android.statementservice
--package-name=com.android.statementservice
--pkg-data-info-map=com.android.statementservice,null,10472
--allowlisted-data-info-map=com.google.android.gms,null,10735
--disabled-compat-changes=3400644,73144566,78294732,117835097,119147584,123562444,124107808,128611929,130595455,132649864,132742131,133396946,135549675,135634846,135754954,135772972,136069189,136219221,136274596,136293963,140852299,141600225,142191088,142365358,143231523,143539591,143937733,144027538,146211400,147316723,147340954,147798919,148086656,148180766,148534348,148535736,149254050,149391281,149994052,150232615,150776642,150857253,150935354,150939131,151105954,151163173,154726397,156215187,157233955,157629738,158482162,159047832,160794467,161145287,161252188,162547999,163400105,165573442,166236554,167676448,168419799,168782947,168936375,169273070,169887240,169897160,170188668,170233598,170503758,170668199,171032338,171306433,171317480,171979766,172100307,172251878,173031413,174041399,174042936,174042980,174043039,174227820,174228127,174664365,175101461,175319604,175408749,176926741,176926753,176926771,176926829,177438394,178038272,178209446,180326732,180326787,180326845,180523564,181136395,181350407,181658987,182185642,182478738,182734110,182811243,183147249,183155436,183164979,183372781,183407956,183972877,184323934,184838306,185004937,185199076,186082280,189229956,189472651,189969734,189969744,189969749,189969779,189969782,189970036,189970038,189970040,191513214,191911306,192341120,193232191,193247900,194532703,194833441,195579280,196254758,197654537,201522903,201712607,201794303,202110963,202894742,203704822,203800354,205194548,205907456,205919743,206033068,207133734,207557677,208648326,208739934,210856463,210923482,211757425,213289672,214016041,214741472,215066299,215656264,216114297,218393363,218493289,218533173,218865702,218959984,224562872,226439802,227752274,229362273,230590090,234793325,235355681,236283604,236704164,236825255,237508058,237531167,239784307,240273417,240663182,241104082,241766793,242193913,242194868,242716250,243827847,244358506,244637991,247079863,253665015,254631730,254662522,255038118,255039210,255042465,255371817,255659651,255938466,255940284,258236856,258825825,260560985,261055255,261072174,261770108,262645982,263076149,263259275,263959004,264301586,264304459,265103382,265131695,265451093,265452344,265456536,265464455,266124927,266201607,266524688,269165460,269849258,270049379,270306772,271850009,273509367,273564678,277035314,280471513,288845345,293644536
android.app.ActivityThread
seq=48
]
iFields of java.lang.Object
[0x0] java.lang.Class shadow$_klass_ = 0x6fb83df8
[0x4] int shadow$_monitor_ = 0x0

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
ini复制代码art-parser> p 0x13a81a80
Size: 0x28
Padding: 0x7
Object Name: android.os.ZygoteProcess$ZygoteState
iFields of android.os.ZygoteProcess$ZygoteState
[0x8] java.util.List mAbiList = 0x1704f5c8
[0x20] boolean mClosed = 0x0
[0xc] android.net.LocalSocketAddress mUsapSocketAddress = 0x72e38ec8
[0x10] java.io.DataInputStream mZygoteInputStream = 0x1704f5d8
[0x14] java.io.BufferedWriter mZygoteOutputWriter = 0x1704f5f8
[0x18] android.net.LocalSocket mZygoteSessionSocket = 0x1704f618
[0x1c] android.net.LocalSocketAddress mZygoteSocketAddress = 0x72e38ee8
iFields of java.lang.Object
[0x0] java.lang.Class shadow$_klass_ = 0x7009ec28
[0x4] int shadow$_monitor_ = 0x0

art-parser> p 0x72e38ec8
Size: 0x10
Object Name: android.net.LocalSocketAddress
iFields of android.net.LocalSocketAddress
[0x8] java.lang.String name = usap_pool_primary
[0xc] android.net.LocalSocketAddress$Namespace namespace = 0x6ff14ec8
iFields of java.lang.Object
[0x0] java.lang.Class shadow$_klass_ = 0x70052e40
[0x4] int shadow$_monitor_ = 0x20000000

1
2
3
4
5
6
bash复制代码# netstat -a -p | grep usap_pool_primary
unix 2 [ ACC ] STREAM LISTENING 41780 952/zygote64 /dev/socket/usap_pool_primary
unix 2 [ ] DGRAM CONNECTED 62176 1398/shelld /dev/socket/usap_pool_primary
unix 2 [ ] DGRAM CONNECTED 58514 1012/vendor.mediate /dev/socket/usap_pool_primary
unix 3 [ ] STREAM CONNECTED 81802 3690/usap64 /dev/socket/usap_pool_primary
unix 2 [ ] DGRAM CONNECTED 81982 984/android.hardwar /dev/socket/usap_pool_primary

至此,我们可以得出结论是,system_server 在启动应用程序 com.android.statementservice 时发生,并且等待服务端进程 3690/usap64 完成并返回结果集。

服务端解析

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
less复制代码# debuggerd -b 3690                          
----- pid 3690 at 2023-10-25 20:15:07.118260161+0800 -----
Cmd line: usap64
ABI: 'arm64'

"usap64" sysTid=3690
#00 pc 00000000000902fc /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
#01 pc 000000000025ab30 /apex/com.android.art/lib64/libart.so (art::Mutex::ExclusiveLock(art::Thread*)+336) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#02 pc 00000000003b8e24 /apex/com.android.art/lib64/libart.so (art::gc::space::RegionSpace::AllocNewTlab(art::Thread*, unsigned long, unsigned long*)+52) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#03 pc 00000000003814ac /apex/com.android.art/lib64/libart.so (art::gc::Heap::AllocWithNewTLAB(art::Thread*, art::gc::AllocatorType, unsigned long, bool, unsigned long*, unsigned long*, unsigned long*)+1260) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#04 pc 0000000000757240 /apex/com.android.art/lib64/libart.so (artAllocArrayFromCodeResolvedRegionTLAB+224) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#05 pc 0000000000225e24 /apex/com.android.art/lib64/libart.so (art_quick_alloc_array_resolved32_region_tlab+132) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#06 pc 00000000001cbbb0 /system/framework/arm64/boot.oat (java.util.regex.Pattern.fastSplit+560) (BuildId: f2e702c0d8fd389971dd1d32a3ba28e75c08fef8)
#07 pc 0000000000160200 /system/framework/arm64/boot.oat (java.lang.String.split+64) (BuildId: f2e702c0d8fd389971dd1d32a3ba28e75c08fef8)
#08 pc 00000000007ffddc /system/framework/arm64/boot-framework.oat (com.android.internal.os.ZygoteArguments.parseArgs+2844) (BuildId: 1b854c6594c6966f2579f1e4b017c1ab8c92236f)
#09 pc 00000000007ff29c /system/framework/arm64/boot-framework.oat (com.android.internal.os.ZygoteArguments.getInstance+124) (BuildId: 1b854c6594c6966f2579f1e4b017c1ab8c92236f)
#10 pc 0000000000209418 /apex/com.android.art/lib64/libart.so (nterp_helper+152) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#11 pc 000000000053026e /system/framework/framework.jar (com.android.internal.os.Zygote.childMain+126)
#12 pc 00000000002093b4 /apex/com.android.art/lib64/libart.so (nterp_helper+52) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#13 pc 00000000005306f4 /system/framework/framework.jar (com.android.internal.os.Zygote.forkUsap+64)
#14 pc 00000000002093b4 /apex/com.android.art/lib64/libart.so (nterp_helper+52) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#15 pc 000000000052f15e /system/framework/framework.jar (com.android.internal.os.ZygoteServer.fillUsapPool+158)
#16 pc 000000000080c340 /system/framework/arm64/boot-framework.oat (com.android.internal.os.ZygoteServer.runSelectLoop+4080) (BuildId: 1b854c6594c6966f2579f1e4b017c1ab8c92236f)
#17 pc 0000000000807288 /system/framework/arm64/boot-framework.oat (com.android.internal.os.ZygoteInit.main+3976) (BuildId: 1b854c6594c6966f2579f1e4b017c1ab8c92236f)
#18 pc 0000000000210c80 /apex/com.android.art/lib64/libart.so (art_quick_invoke_static_stub+640) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#19 pc 0000000000253b40 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+224) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#20 pc 000000000064bd68 /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeWithVarArgs<art::ArtMethod*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, art::ArtMethod*, std::__va_list)+408) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#21 pc 000000000064c340 /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeWithVarArgs<_jmethodID*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)+80) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#22 pc 00000000005160f4 /apex/com.android.art/lib64/libart.so (art::JNI<true>::CallStaticVoidMethodV(_JNIEnv*, _jclass*, _jmethodID*, std::__va_list)+692) (BuildId: bec5ac60247dc778a314f5b24a093aa5)
#23 pc 00000000000dfca8 /system/lib64/libandroid_runtime.so (_JNIEnv::CallStaticVoidMethod(_jclass*, _jmethodID*, ...)+104)
#24 pc 00000000000ec454 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)+1028)
#25 pc 000000000000258c /system/bin/app_process64 (main+1324) (BuildId: 3177237856445357fb121f4100079921)
#26 pc 000000000008c798 /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+104)

----- end 3690 -----
1
2
3
yaml复制代码# ps -eT 3690
USER PID TID PPID VSZ RSS WCHAN ADDR S CMD
root 3690 3690 952 6306924 426964 futex_wai+ 0 S usap64

从现场通过 debug 手段,确定进程 3690 有且仅有一个线程,并且处于一个等锁的状态,这个锁永远也不可能主动释放了,因此卡死了客户端出现冻屏,我们分析下锁的 owner 是谁,仍然是将进程 3690 内存转储成 Coredump 文件来进行内存分析。

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
rust复制代码(gdb) bt
#0 syscall () at bionic/libc/arch-arm64/bionic/syscall.S:41
#1 0x0000007d66d81b34 in art::futex(int volatile*, int, int, timespec const*, int volatile*, int) [clone .__uniq.55395457626730424248235132913560037531] (op=128, val=0, uaddr2=0x0, val3=0,
uaddr=<optimized out>, timeout=<optimized out>) at art/runtime/base/mutex-inl.h:43
#2 art::Mutex::ExclusiveLock (this=0xb400007d67e22c90, self=0xb400007d67e42c00) at art/runtime/base/mutex.cc:471
#3 0x0000007d66edfe28 in art::MutexLock::MutexLock (self=<optimized out>, mu=..., this=<optimized out>) at art/runtime/base/mutex.h:510
#4 art::gc::space::RegionSpace::AllocNewTlab (this=0xb400007d67e22a80, self=0xb400007d67e42c00, tlab_size=1695, bytes_tl_bulk_allocated=0x7fedceb2f8) at art/runtime/gc/space/region_space.cc:847
#5 0x0000007d66ea84b0 in art::gc::Heap::AllocWithNewTLAB (this=0xb400007d67e29700, self=0xb400007d67e42c00, allocator_type=<optimized out>, alloc_size=<optimized out>, grow=false,
bytes_allocated=0x7fedceb308, usable_size=0x7fedceb300, bytes_tl_bulk_allocated=0x7fedceb2f8) at art/runtime/gc/heap.cc:4567
#6 0x0000007d6727e244 in art::gc::Heap::TryToAllocate<false, false> (this=<optimized out>, self=<optimized out>, allocator_type=<optimized out>, alloc_size=<optimized out>,
bytes_allocated=<optimized out>, usable_size=<optimized out>, bytes_tl_bulk_allocated=<optimized out>) at art/runtime/gc/heap-inl.h:416
#7 art::gc::Heap::AllocObjectWithAllocator<false, true, art::mirror::SetLengthVisitor> (this=0xb400007d67e29700, self=0xb400007d67e42c00, klass=..., byte_count=936,
allocator=art::gc::kAllocatorTypeRegionTLAB, pre_fence_visitor=...) at art/runtime/gc/heap-inl.h:147
#8 art::mirror::Array::Alloc<false, false> (self=<optimized out>, array_class=..., component_count=<optimized out>, allocator_type=art::gc::kAllocatorTypeRegionTLAB,
component_size_shift=<optimized out>) at art/runtime/mirror/array-alloc-inl.h:147
#9 art::AllocArrayFromCodeResolved<false> (component_count=<optimized out>, allocator_type=art::gc::kAllocatorTypeRegionTLAB, klass=..., self=<optimized out>)
at art/runtime/entrypoints/entrypoint_utils-inl.h:369
#10 artAllocArrayFromCodeResolvedRegionTLAB (klass=0x6fb84108, component_count=<optimized out>, self=0xb400007d67e42c00) at art/runtime/entrypoints/quick/quick_alloc_entrypoints.cc:139
#11 0x0000007d66d4ce28 in art_quick_alloc_array_resolved32_region_tlab () at art/runtime/arch/arm64/quick_entrypoints_arm64.S:1641
#12 0x00000000712a2bb4 in java::util::regex::Pattern::fastSplit (re=..., input=..., limit=<optimized out>)
from symbols/system/framework/arm64/boot.oat
#13 0x0000000071237204 in java::lang::String::split (this=..., regex=...)
from symbols/system/framework/arm64/boot.oat
#14 0x0000000071ccfde0 in com::android::internal::os::ZygoteArguments::parseArgs (this=..., args=..., argCount=<optimized out>) at com/android/internal/os/ZygoteArguments.java:286
#15 0x0000000071ccf2a0 in com::android::internal::os::ZygoteArguments::getInstance (args=...)
from symbols/system/framework/arm64/boot-framework.oat
#16 0x0000007d66d3041c in nterp_helper () at out_sys/soong/.intermediates/art/runtime/libart_mterp.arm64ng/gen/mterp_arm64ng.S:11269
#17 0x0000007d66d303b8 in nterp_helper () at out_sys/soong/.intermediates/art/runtime/libart_mterp.arm64ng/gen/mterp_arm64ng.S:11269
#18 0x0000007d66d303b8 in nterp_helper () at out_sys/soong/.intermediates/art/runtime/libart_mterp.arm64ng/gen/mterp_arm64ng.S:11269
#19 0x0000000071cdc344 in com::android::internal::os::ZygoteServer::runSelectLoop (this=..., abiList=...) at com/android/internal/os/ZygoteServer.java:665
#20 0x0000000071cd728c in com::android::internal::os::ZygoteInit::main (argv=...) at com/android/internal/os/ZygoteInit.java:1068
#21 0x0000007d66d37c84 in art_quick_invoke_static_stub () at art/runtime/arch/arm64/quick_entrypoints_arm64.S:688
#22 0x0000007d66d7ab44 in art::ArtMethod::Invoke (this=0x709a91a8, self=0xb400007d67e42c00, args=0x62, args_size=<optimized out>, result=0x7fedcebdb8, shorty=0x7d6464e228 "VL")
at art/runtime/art_method.cc:425
#23 0x0000007d67172d6c in art::(anonymous namespace)::InvokeWithArgArray(art::ScopedObjectAccessAlreadyRunnable const&, art::ArtMethod*, art::(anonymous namespace)::ArgArray*, art::JValue*, char const*) [clone .__uniq.245181933781456475607640333933569312899] (soa=..., method=0x709a91a8, arg_array=0x7fedcebd40, result=0x7fedcebdb8, shorty=0x7d6464e228 "VL") at art/runtime/reflection.cc:458
#24 art::InvokeWithVarArgs<art::ArtMethod*> (soa=..., obj=0x0, method=0x709a91a8, args=...) at art/runtime/reflection.cc:550
#25 0x0000007d67173344 in art::InvokeWithVarArgs<_jmethodID*> (soa=..., obj=0x0, mid=<optimized out>, args=...) at art/runtime/reflection.cc:565
#26 0x0000007d6703d0f8 in art::JNI<true>::CallStaticVoidMethodV (env=<optimized out>, mid=0x709a91a8, args=...) at art/runtime/jni/jni_internal.cc:1963
#27 0x0000007de8812cac in _JNIEnv::CallStaticVoidMethod (this=<optimized out>, clazz=<optimized out>, methodID=<optimized out>) at libnativehelper/include_jni/jni.h:779
#28 0x0000007de881f458 in android::AndroidRuntime::start (this=0x7fedcec190, className=0x5e8d845394 "com.android.internal.os.ZygoteInit", options=..., zygote=true)
at frameworks/base/core/jni/AndroidRuntime.cpp:1358
#29 0x0000005e8d846590 in main (argc=<optimized out>, argv=0x7fedced2e8) at frameworks/base/cmds/app_process/app_main.cpp:372
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
ini复制代码(gdb) p *('art::Mutex' *)0xb400007d67e22c90
$2 = {
<art::BaseMutex> = {
_vptr$BaseMutex = 0x7d67327230 <vtable for art::Mutex+16>,
name_ = 0x7d66be275c "Region lock",
contention_log_data_ = 0xb400007d67e22ca0,
level_ = art::kRegionSpaceRegionLock,
should_respond_to_empty_checkpoint_request_ = false
},
members of art::Mutex:
state_and_contenders_ = {
<std::__1::atomic<int>> = {
<std::__1::__atomic_base<int, true>> = {
<std::__1::__atomic_base<int, false>> = {
__a_ = 3,
static is_always_lock_free = <optimized out>
}, <No data fields>}, <No data fields>}, <No data fields>},
static kHeldMask = 1,
static kContenderShift = 1,
static kContenderIncrement = 2,
exclusive_owner_ = {
<std::__1::atomic<int>> = {
<std::__1::__atomic_base<int, true>> = {
<std::__1::__atomic_base<int, false>> = {
__a_ = 3657,
static is_always_lock_free = <optimized out>
}, <No data fields>}, <No data fields>}, <No data fields>},
recursion_count_ = 1,
recursive_ = false,
enable_monitor_timeout_ = false,
monitor_id_ = 758263346
}

奇怪的事情发生了,线程 3657 到底是谁,即使 ps -eT | grep 3657 搜遍了所有线程号,终究无法得到,因此只有一个原因是该线程已经退出了。

疑问一

为什么3657已退出,锁却没有被释放?\color{red}{为什么 3657 已退出,锁却没有被释放?}为什么3657已退出,锁却没有被释放?

其实这个多线程 Fork 后,子进程发生死锁现象,在 Linux 编写程序发生这种太常见了,由于 Fork 的写时复制机制,因此存在父进程的一个锁变量状态处于上锁状态,此时发生了 Fork,父子进程共享了同一个虚拟内存地址空间,此时子进程在使用该锁,则会出现死锁现象。

Screenshot from 2023-11-01 11-05-27.png

数据恢复

基于前面的疑问,我们可以知道线程 3657 获得锁之后,发生了 Fork,因此 3657 的线程栈也应该在进程 3690 的地址空间下。

1
2
3
shell复制代码# cat /proc/3690/maps | grep 3657
7d5016a000-7d5016b000 ---p 00000000 00:00 0 [anon:stack_and_tls:3657]
7d5016b000-7d50272000 rw-p 00000000 00:00 0 [anon:stack_and_tls:3657]

前面我们保存了进程 3690 的 Coredump 文件,并且它是 Zygote 进程 Fork 而来,共享了 Zygote 复制前的内存,因此我们查看此时被虚拟机管理的线程还有谁。

3690 数据解析

1
2
3
ini复制代码art-parser> thread
[952] "main" tid=1 Unknown
[3657] "FinalizerWatchdogDaemon" tid=5 Unknown

父进程 952 与子进程 3690,在写分离之前,它们是共享地址空间,也共享同一个栈地址,因此我们可以修改一些假数据,让 3690 添加到虚拟机管理下,这样就可以使用 art-parser 来解析 3690 堆栈的 Java 数据。

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
ini复制代码art-parser> bt 952
"main" prio=5 tid=1 Unknown
| group="main" sCount=0 ucsCount=0 flags=0 obj=0x72845b10 self=0xb400007d67e42c00
| sysTid=952 nice=<unknown> cgrp=<unknown> sched=<unknown> handle=0x7e1a07b4f8
| stack=0x7fed4f0000-0x7fed4f2000 stackSize=0x7ff000
| held mutexes= "mutator lock"
x0 0x0000000000000000 x1 0x0000000000000000 x2 0x0000000000000000 x3 0x0000000000000000
x4 0x0000000000000000 x5 0x0000000000000000 x6 0x0000000000000000 x7 0x0000000000000000
x8 0x0000000000000000 x9 0x0000000000000000 x10 0x0000000000000000 x11 0x0000000000000000
x12 0x0000000000000000 x13 0x0000000000000000 x14 0x0000000000000000 x15 0x0000000000000000
x16 0x0000000000000000 x17 0x0000000000000000 x18 0x0000000000000000 x19 0x0000000000000000
x20 0x0000000000000000 x21 0x0000000000000000 x22 0x0000000000000000 x23 0x0000000000000000
x24 0x0000000000000000 x25 0x0000000000000000 x26 0x0000000000000000 x27 0x0000000000000000
x28 0x0000000000000000 x29 0x0000000000000000
lr 0x0000000000000000 sp 0x0000000000000000 pc 0x0000000000000000 pst 0x0000000000000000
0x0 fault sp address!!
>>> analysis 'art::OatHeader::kOatVersion' ...
>>> 'art::OatHeader::kOatVersion' = 238 ?
QF[0x7fedceb450] PC[0x00712a2bb4] at dex-pc 0x7d66667c92 java.util.regex.Pattern.fastSplit //AM[0x6fccbfb0]
QF[0x7fedceb4b0] PC[0x0071237204] at dex-pc 0x7d6650ad72 java.lang.String.split //AM[0x6fd3ec48]
QF[0x7fedceb4f0] PC[0x0071ccfde0] at dex-pc 0x7d64410aba com.android.internal.os.ZygoteArguments.parseArgs //AM[0x708328d8]
QF[0x7fedceb620] PC[0x0071ccf2a0] at dex-pc 0x7d64410398 com.android.internal.os.ZygoteArguments.getInstance //AM[0x708328b8]
QF[0x7fedceb660] PC[0x7d66d3041c] at dex-pc 0x7d6441526e com.android.internal.os.Zygote.childMain //AM[0x709a8698]
QF[0x7fedceb850] PC[0x7d66d303b8] at dex-pc 0x7d644156f4 com.android.internal.os.Zygote.forkUsap //AM[0x709a87f8]
QF[0x7fedceb960] PC[0x7d66d303b8] at dex-pc 0x7d6441415e com.android.internal.os.ZygoteServer.fillUsapPool //AM[0x70832f98]
QF[0x7fedceba80] PC[0x0071cdc344] at dex-pc 0x7d64414632 com.android.internal.os.ZygoteServer.runSelectLoop //AM[0x70833018]
QF[0x7fedcebb80] PC[0x0071cd728c] at dex-pc 0x7d6441307e com.android.internal.os.ZygoteInit.main //AM[0x709a91a8]

堆栈中的 self 则是 art::Thread 的地址,其中 0x3b8 则是该线程的 ID,我们只需要将 0x3b8 修改成 0xe6a 即可。

1
2
makefile复制代码art-parser> rd 0xb400007d67e42c00 -e 0xb400007d67e42c10
0xb400007d67e42c00: 0x0000000000000000 0x000003b800000001
1
2
3
4
5
6
7
sql复制代码art-parser> wd 0xb400007d67e42c08 0x00000e6a00000001
New overlay (0x785f98f000) [0x7d67a00000, 0x7d68200000).
Overlay vaddr(0xb400007d67e42c08) old(0x000003b800000001) new(0x00000e6a00000001)

art-parser> thread
[3690] "main" tid=1 Unknown
[3657] "FinalizerWatchdogDaemon" tid=5 Unknown
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
less复制代码art-parser> bt 3690
"main" prio=5 tid=1 Unknown
| group="main" sCount=0 ucsCount=0 flags=0 obj=0x72845b10 self=0xb400007d67e42c00
| sysTid=3690 nice=<unknown> cgrp=<unknown> sched=<unknown> handle=0x7e1a07b4f8
| stack=0x7fed4f0000-0x7fed4f2000 stackSize=0x7ff000
| held mutexes= "mutator lock"
| wait mutex= "Region lock" held by thread 3657
x0 0xb400007d67e22ca4 x1 0x0000000000000080 x2 0x0000000000000003 x3 0x0000000000000000
x4 0x0000000000000000 x5 0x0000000000000000 x6 0x0000000000000000 x7 0x0000007fedceb2f8
x8 0x0000000000000062 x9 0x0000000000000000 x10 0x0000000000000032 x11 0x0000000000000033
x12 0x0000000000000003 x13 0x0000000000000033 x14 0x000000000000001c x15 0x00000000703013b8
x16 0x0000007d67336918 x17 0x0000007dec4ee2e0 x18 0x0000007e19ada000 x19 0xb400007d67e22c90
x20 0xb400007d67e42c00 x21 0xb400007d67e22ca4 x22 0x0000007d66bde13b x23 0x0000007d66bc4f70
x24 0x0000007d6753d000 x25 0x0000007fedceb150 x26 0x0000000000000001 x27 0x00000000fffffffe
x28 0x0000007e18d58000 x29 0x0000007fedceb190
lr 0x0000007d66d81b34 sp 0x0000007fedceb120 pc 0x0000007dec4ee2fc pst 0x0000000060001000
FP[0x7fedceb190] PC[0x7dec4ee2fc] native: #00 (syscall+0x1c) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7fedceb190] PC[0x7d66d81b34] native: #01 (art::Mutex::ExclusiveLock(art::Thread*)+0x154) /apex/com.android.art/lib64/libart.so
FP[0x7fedceb1f0] PC[0x7d66edfe28] native: #02 (art::gc::space::RegionSpace::AllocNewTlab(art::Thread*, unsigned long, unsigned long*)+0x38) /apex/com.android.art/lib64/libart.so
FP[0x7fedceb280] PC[0x7d66ea84b0] native: #03 (art::gc::Heap::AllocWithNewTLAB(art::Thread*, art::gc::AllocatorType, unsigned long, bool, unsigned long*, unsigned long*, unsigned long*)+0x4f0) /apex/com.android.art/lib64/libart.so
FP[0x7fedceb390] PC[0x7d6727e244] native: #04 (artAllocArrayFromCodeResolvedRegionTLAB+0xe4) /apex/com.android.art/lib64/libart.so
QF[0x7fedceb450] PC[0x00712a2bb4] at dex-pc 0x7d66667c92 java.util.regex.Pattern.fastSplit //AM[0x6fccbfb0]
QF[0x7fedceb4b0] PC[0x0071237204] at dex-pc 0x7d6650ad72 java.lang.String.split //AM[0x6fd3ec48]
QF[0x7fedceb4f0] PC[0x0071ccfde0] at dex-pc 0x7d64410aba com.android.internal.os.ZygoteArguments.parseArgs //AM[0x708328d8]
QF[0x7fedceb620] PC[0x0071ccf2a0] at dex-pc 0x7d64410398 com.android.internal.os.ZygoteArguments.getInstance //AM[0x708328b8]
QF[0x7fedceb660] PC[0x7d66d3041c] at dex-pc 0x7d6441526e com.android.internal.os.Zygote.childMain //AM[0x709a8698]
QF[0x7fedceb850] PC[0x7d66d303b8] at dex-pc 0x7d644156f4 com.android.internal.os.Zygote.forkUsap //AM[0x709a87f8]
QF[0x7fedceb960] PC[0x7d66d303b8] at dex-pc 0x7d6441415e com.android.internal.os.ZygoteServer.fillUsapPool //AM[0x70832f98]
QF[0x7fedceba80] PC[0x0071cdc344] at dex-pc 0x7d64414632 com.android.internal.os.ZygoteServer.runSelectLoop //AM[0x70833018]
QF[0x7fedcebb80] PC[0x0071cd728c] at dex-pc 0x7d6441307e com.android.internal.os.ZygoteInit.main //AM[0x709a91a8]

我们此时也知道解析下从客户端传入的参数,核对下前面的分析是否一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码art-parser> disassemble 0x708328b8 -i 0x7d64410398
com.android.internal.os.ZygoteArguments com.android.internal.os.ZygoteArguments.getInstance(com.android.internal.os.ZygoteCommandBuffer) [dex_method_idx=61801]
DEX CODE:
0x7d64410398: 3070 f166 0021 | invoke-direct {v1, v2, v0}, void com.android.internal.os.ZygoteArguments.<init>(com.android.internal.os.ZygoteCommandBuffer, int) // method@61798

art-parser> bt 3690 -v
QF[0x7fedceb620] PC[0x0071ccf2a0] at dex-pc 0x7d64410398 com.android.internal.os.ZygoteArguments.getInstance //AM[0x708328b8]
{
StackMap[5] (code_region=[0x71ccf220-0x71ccf2b8], native_pc=0x80, dex_pc=0xa, register_mask=0xc00000)
Physical registers
{
x22 = 0x12d7e5f8 x23 = 0x12d7e5e0 x24 = 0xffffffff x25 = 0x10
x26 = 0x7fedceb734 x27 = 0x7fedceb6b8 x28 = 0x7fedceb7b0 x29 = 0x7fedceb734
x30 = 0x71ccf2a0
}
}
1
2
3
4
5
6
7
8
csharp复制代码   0x71ccf280 <+96>:         mov        x2, x23
0x71ccf284 <+100>: mov x3, x25
0x71ccf288 <+104>: mov x1, x0
0x71ccf28c <+108>: mov x22, x1
0x71ccf290 <+112>: adrp x0, 0x70832000
0x71ccf294 <+116>: add x0, x0, #0x8d8
0x71ccf298 <+120>: ldr x30, [x0, #24]
0x71ccf29c <+124>: blr x30
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
ini复制代码art-parser> p 0x12d7e5f8
Size: 0x90
Padding: 0x5
Object Name: com.android.internal.os.ZygoteArguments
iFields of com.android.internal.os.ZygoteArguments
[0x7c] boolean mAbiListQuery = 0x0
[0x8] java.lang.String[] mAllowlistedDataInfoList = 0x12d7ec30
[0xc] java.lang.String[] mApiDenylistExemptions = 0x0
[0x10] java.lang.String mAppDataDir = /data/user/0/com.android.statementservice
[0x7d] boolean mBindMountAppDataDirs = 0x0
[0x7e] boolean mBindMountAppStorageDirs = 0x0
[0x7f] boolean mBootCompleted = 0x0
[0x80] boolean mCapabilitiesSpecified = 0x0
[0x14] long[] mDisabledCompatChanges = 0x0
[0x50] long mEffectiveCapabilities = 0x0
[0x60] int mGid = 0x276c
[0x81] boolean mGidSpecified = 0x1
[0x18] int[] mGids = 0x12d7e8a0
[0x64] int mHiddenApiAccessLogSampleRate = 0xffffffff
[0x68] int mHiddenApiAccessStatslogSampleRate = 0xffffffff
[0x1c] java.lang.String mInstructionSet = 0x0
[0x20] java.lang.String mInvokeWith = 0x0
[0x82] boolean mIsTopApp = 0x0
[0x6c] int mMountExternal = 0x1
[0x24] java.lang.String mNiceName = com.android.statementservice
[0x28] java.lang.String mPackageName = com.android.statementservice
[0x58] long mPermittedCapabilities = 0x0
[0x83] boolean mPidQuery = 0x0
[0x2c] java.lang.String[] mPkgDataInfoList = 0x12d7eb30
[0x30] java.lang.String mPreloadApp = 0x0
[0x84] boolean mPreloadDefault = 0x0
[0x34] java.lang.String mPreloadPackage = 0x0
[0x38] java.lang.String mPreloadPackageCacheKey = 0x0
[0x3c] java.lang.String mPreloadPackageLibFileName = 0x0
[0x40] java.lang.String mPreloadPackageLibs = 0x0
[0x44] java.util.ArrayList mRLimits = 0x0
[0x48] java.lang.String[] mRemainingArgs = 0x0
[0x70] int mRuntimeFlags = 0x1200800
[0x4c] java.lang.String mSeInfo = platform:privapp:targetSdkVersion=29:complete
[0x85] boolean mSeInfoSpecified = 0x1
[0x86] boolean mStartChildZygote = 0x0
[0x74] int mTargetSdkVersion = 0x1d
[0x87] boolean mTargetSdkVersionSpecified = 0x1
[0x78] int mUid = 0x276c
[0x88] boolean mUidSpecified = 0x1
[0x89] boolean mUsapPoolEnabled = 0x0
[0x8a] boolean mUsapPoolStatusSpecified = 0x0
iFields of java.lang.Object
[0x0] java.lang.Class shadow$_klass_ = 0x70177360
[0x4] int shadow$_monitor_ = 0x0

从参数上我们可以核实该 usap64 此时确实是用来启动应用程序 com.android.statementservice,与客户端解析部分吻合。

3657 栈恢复

1
2
css复制代码7d5016b000-7d50272000 rw-p 00000000 00:00 0 [anon:stack_and_tls:3657]
前面我们知道 3657 栈地址范围,于是我们可以根据现有的栈内存进行推导,但这次推导只能之下而上。

1
2
bash复制代码(gdb) x /i 0x0000007dec56126c-0x4
0x7dec561268 <_ZN5NonPIL20MutexLockWithTimeoutEP24pthread_mutex_internal_tbPK8timespec+328>: bl 0x7dec4f3040 <_Z15__futex_wait_exPVvbibPK8timespec>
1
2
3
4
5
6
7
8
9
bash复制代码Dump of assembler code for function _ZN5NonPIL20MutexLockWithTimeoutEP24pthread_mutex_internal_tbPK8timespec:
0x0000007dec561120 <+0>: sub sp, sp, #0x80
0x0000007dec561124 <+4>: stp x29, x30, [sp, #32]
0x0000007dec561128 <+8>: str x27, [sp, #48]
0x0000007dec56112c <+12>: stp x26, x25, [sp, #64]
0x0000007dec561130 <+16>: stp x24, x23, [sp, #80]
0x0000007dec561134 <+20>: stp x22, x21, [sp, #96]
0x0000007dec561138 <+24>: stp x20, x19, [sp, #112]
0x0000007dec56113c <+28>: add x29, sp, #0x20

于是我们可以得到一组 gdb、lldb 以及 art-parser 进行栈回溯所需要的寄存器值。

即:

寄存器 值
PC 0x7dec4f3040
LR 0x7dec56126c
SP 0x7d5026e550
FP 0x7d5026e570

对原本进程 3690 的 Coredump 文件进行修改。

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
erlang复制代码0001B710 05 00 00 00 88 01 00 00 01 00 00 00 43 4F 52 45 ............CORE
0001B720 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B730 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B740 00 00 00 00 6A 0E 00 00 00 00 00 00 00 00 00 00 ....j...........
0001B750 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B760 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B770 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B780 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B790 00 00 00 00 A4 2C E2 67 7D 00 00 B4 80 00 00 00 .....,.g}.......
0001B7A0 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7C0 00 00 00 00 00 00 00 00 00 00 00 00 F8 B2 CE ED ................
0001B7D0 7F 00 00 00 62 00 00 00 00 00 00 00 00 00 00 00 ....b...........
0001B7E0 00 00 00 00 32 00 00 00 00 00 00 00 33 00 00 00 ....2.......3...
0001B7F0 00 00 00 00 03 00 00 00 00 00 00 00 33 00 00 00 ............3...
0001B800 00 00 00 00 1C 00 00 00 00 00 00 00 B8 13 30 70 ..............0p
0001B810 00 00 00 00 18 69 33 67 7D 00 00 00 E0 E2 4E EC .....i3g}.....N.
0001B820 7D 00 00 00 00 A0 AD 19 7E 00 00 00 90 2C E2 67 }.......~....,.g
0001B830 7D 00 00 B4 00 2C E4 67 7D 00 00 B4 A4 2C E2 67 }....,.g}....,.g
0001B840 7D 00 00 B4 3B E1 BD 66 7D 00 00 00 70 4F BC 66 }...;..f}...pO.f
0001B850 7D 00 00 00 00 D0 53 67 7D 00 00 00 50 B1 CE ED }.....Sg}...P...
0001B860 7F 00 00 00 01 00 00 00 00 00 00 00 FE FF FF FF ................
0001B870 00 00 00 00 00 80 D5 18 7E 00 00 00 90 B1 CE ED ........~.......
0001B880 7F 00 00 00 34 1B D8 66 7D 00 00 00 20 B1 CE ED ....4..f}... ...
0001B890 7F 00 00 00 FC E2 4E EC 7D 00 00 00 00 10 00 60 ......N.}......`

对 Coredump 的寄存器以及线程号进行修改后

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
erlang复制代码0001B710 05 00 00 00 88 01 00 00 01 00 00 00 43 4F 52 45 ............CORE
0001B720 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B730 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B740 00 00 00 00 49 0E 00 00 00 00 00 00 00 00 00 00 ....I...........
0001B750 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B760 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B770 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B780 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B790 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B7F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B800 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B820 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B830 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B840 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B850 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B860 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0001B870 00 00 00 00 00 00 00 00 00 00 00 00 70 E5 26 50 ............p.&P
0001B880 7D 00 00 00 6C 12 56 EC 7D 00 00 00 50 E5 26 50 }...l.V.}...P.&P
0001B890 7D 00 00 00 40 30 4F EC 7D 00 00 00 00 10 00 60 }...@0O.}......`
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
less复制代码art-parser> bt 3657
"FinalizerWatchdogDaemon" prio=<unknown> tid=5 Unknown
| group="<unknown>" sCount=0 ucsCount=0 flags=0 obj=0x0 self=0xb400007d67f87c00
| sysTid=3657 nice=<unknown> cgrp=<unknown> sched=<unknown> handle=0x7d5026ecb0
| stack=0x7d5016b000-0x7d5016d000 stackSize=0x103cb0
| held mutexes= "Region lock" "mutator lock"
x0 0x0000000000000000 x1 0x0000000000000000 x2 0x0000000000000000 x3 0x0000000000000000
x4 0x0000000000000000 x5 0x0000000000000000 x6 0x0000000000000000 x7 0x0000000000000000
x8 0x0000000000000000 x9 0x0000000000000000 x10 0x0000000000000000 x11 0x0000000000000000
x12 0x0000000000000000 x13 0x0000000000000000 x14 0x0000000000000000 x15 0x0000000000000000
x16 0x0000000000000000 x17 0x0000000000000000 x18 0x0000000000000000 x19 0x0000000000000000
x20 0x0000000000000000 x21 0x0000000000000000 x22 0x0000000000000000 x23 0x0000000000000000
x24 0x0000000000000000 x25 0x0000000000000000 x26 0x0000000000000000 x27 0x0000000000000000
x28 0x0000000000000000 x29 0x0000007d5026e570
lr 0x0000007dec56126c sp 0x0000007d5026e550 pc 0x0000007dec4f3040 pst 0x0000000060001000
FP[0x7d5026e570] PC[0x7dec4f3040] native: #00 (__futex_wait_ex(void volatile*, bool, int, bool, timespec const*)+0x0) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026e5e0] PC[0x7dec56126c] native: #02 (NonPI::MutexLockWithTimeout(pthread_mutex_internal_t*, bool, timespec const*)+0x14c) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026e670] PC[0x7dec4da500] native: #03 (je_malloc_mutex_lock_slow+0xc0) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026e6c0] PC[0x7dec4ba294] native: #04 (je_arena_tcache_fill_small+0x64) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026e720] PC[0x7dec4e543c] native: #05 (je_tcache_alloc_small_hard+0x1c) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026e780] PC[0x7dec4b1c38] native: #06 (je_malloc+0x2e8) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026e7d0] PC[0x7dec4ad5c8] native: #07 (malloc+0x28) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026e7f0] PC[0x7e12e5401c] native: #08 (operator new(unsigned long)+0x1c) /apex/com.android.art/lib64/libc++.so
FP[0x7d5026e810] PC[0x7d66ee0024] native: #09 (art::gc::space::RegionSpace::RevokeThreadLocalBuffersLocked(art::Thread*, bool)+0x74) /apex/com.android.art/lib64/libart.so
FP[0x7d5026e840] PC[0x7d66ee01f4] native: #10 (art::gc::space::RegionSpace::RevokeThreadLocalBuffers(art::Thread*)+0x54) /apex/com.android.art/lib64/libart.so
FP[0x7d5026e880] PC[0x7d66ea646c] native: #11 (art::gc::Heap::RevokeThreadLocalBuffers(art::Thread*)+0x8c) /apex/com.android.art/lib64/libart.so
FP[0x7d5026e960] PC[0x7d671da368] native: #12 (art::Thread::Destroy(bool)+0xf18) /apex/com.android.art/lib64/libart.so
FP[0x7d5026eb30] PC[0x7d671effe4] native: #13 (art::ThreadList::Unregister(art::Thread*, bool)+0xa4) /apex/com.android.art/lib64/libart.so
FP[0x7d5026ebf0] PC[0x7d671cb16c] native: #14 (art::Thread::CreateCallback(void*)+0x8ac) /apex/com.android.art/lib64/libart.so
FP[0x7d5026ec50] PC[0x7dec55fd60] native: #15 (__pthread_start(void*)+0xd0) /apex/com.android.runtime/lib64/bionic/libc.so
FP[0x7d5026ec80] PC[0x7dec4f3bc4] native: #16 (__start_thread+0x44) /apex/com.android.runtime/lib64/bionic/libc.so


将修改后的 coredump 文件,重载 art-parser 上解析,即可清楚的看到 3657 线程在函数 RevokeThreadLocalBuffers上获得了锁 Region lock,然后发生了 Fork 行为,因此造成子进程死锁了。

疑问二

事实上 Google 也考虑到 Linux 多线程 Fork 易死锁问题,因此在 Zygote Fork 子进程之前会停止所有 Daemon 线程,那为什么 3657 还未完成退出,就发生了 Fork。


细节 1

可能有同学注意到线程都已经 join 了,为什么还有 Daemon 线程没有退出,函数 Daemons.stop() 却已经完成了?

这里涉及到 ART 虚拟机线程设计,在 ART 中 join 线程,会在 art::Thread::Destroy 中释放锁资源,因此线程就不会在等待就结束了,对应代码如下:


然而 3657 线程已经运行到 RevokeThreadLocalBuffers 此处,因此 Java 层最后一个 Daemon.stop() 已经结束了。

细节 2

即使 Daemons.stop() 函数结束了,ART 虚拟机仍可以在做一次保护,只需考虑 ThreadList 列表仅剩主线程即可。我们可以看 nativePreFork 在 ART 虚拟机后续如何处理的,有无等待 ThreadList 清理且等待。



然而我们的机器 unregistering_count_ 并未减到 0,nativePreFork 就已经结束了。
对比我们项目 PreZygoteFork 代码,好家伙并没这一段代码。

目前项目的处理方式,通过 Java API 访问 /proc/self/task 目录下的文件数来判断进程是否仅剩一个。


从 3690 进程的内存中,查询父进程历史线程本地变量

可以从中找到最后一次从 tasks.list() 产生的 java.lang.String[] 对象

1
2
3
4
5
6
7
8
9
10
11
less复制代码art-parser> p 0x12d7e370
Size: 0x18
Array Name: java.lang.String[]
[0] 952
[1] 3655
[2] 3657

art-parser> p 0x12d7e3f0
Size: 0x10
Array Name: java.lang.String[]
[0] 952

结论

至于为什么通过 Java API 访问 /proc/self/task 目录下的文件数会出现不可靠情况仍需进一步调查。

Revert^2 “Wait for thread termination in PreZygoteFork()” android-review.googlesource.com/c/platform/…
Revert^2 “Remove waitUntilAllThreadsStopped()” android-review.googlesource.com/c/platform/…

Google 给出的新的 waitUntilAllThreadsStopped 处理,依旧是存在瑕疵,首先循环条件有限 1000 次采样,不排除一种情况在最后一次线程退出时,结束了 Unregister 函数后,ThreadList 判断是否为 1 条件通过,但线程也可能在内核态中 do_exit 上发生等待,可能就造成 waitUntilAllThreadsStopped 判断 1000 次不可达,从而发生 FATAL 错误。

补充

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
56
57
58
59
60
61
62
63
ini复制代码/*
* Find the next thread in the thread list.
* Return NULL if there is an error or no next thread.
*
* The reference to the input task_struct is released.
*/
static struct task_struct *next_tid(struct task_struct *start)
{
struct task_struct *pos = NULL;
rcu_read_lock();
if (pid_alive(start)) {
pos = next_thread(start);
if (thread_group_leader(pos))
pos = NULL;
else
get_task_struct(pos);
}
rcu_read_unlock();
put_task_struct(start);
return pos;
}

/* for the /proc/TGID/task/ directories */
static int proc_task_readdir(struct file *file, struct dir_context *ctx)
{
struct inode *inode = file_inode(file);
struct task_struct *task;
struct pid_namespace *ns;
int tid;

if (proc_inode_is_dead(inode))
return -ENOENT;

if (!dir_emit_dots(file, ctx))
return 0;

/* f_version caches the tgid value that the last readdir call couldn't
* return. lseek aka telldir automagically resets f_version to 0.
*/
ns = proc_pid_ns(inode->i_sb);
tid = (int)file->f_version;
file->f_version = 0;
for (task = first_tid(proc_pid(inode), tid, ctx->pos - 2, ns);
task;
task = next_tid(task), ctx->pos++) {
char name[10 + 1];
unsigned int len;

tid = task_pid_nr_ns(task, ns);
if (!tid)
continue; /* The task has just exited. */
len = snprintf(name, sizeof(name), "%u", tid);
if (!proc_fill_cache(file, ctx, name, len,
proc_task_instantiate, task, NULL)) {
/* returning this tgid failed, save it as the first
* pid for the next readir call */
file->f_version = (u64)tid;
put_task_struct(task);
break;
}
}

return 0;

UML 图 (8).jpg

验证实验

实验原理:
创建 Daemon 线程前,保存 /proc/self/task 的所有线程号到样本一 (alltask)
启动 5 个 Daemon 线程,分别叫 Daemon-0、Daemon-1、……、Daemon-4
终止 4 个 Daemon 线程,保留 Daemon-4
保存 /proc/self/task 的所有线程号到样本二 (savetask),并循环判断校验 savetask 长度 > alltask 长度,1000次后终止 Daemon-4 重新测试。
当 savetask 长度 <= alltask 长度时,保存现场 Coredump。
使用 art-parser 解析内存,核实 savetask 是否存在 Daemon-4,并校验前后数据。若不存在 Daemon-4, 则 proc_task_readdir 发生了截断。
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
ini复制代码int length = 0;
String[] savetask;
String[] alltask;
void doTest(View view) {

mThreads.add(new Daemon());
mThreads.add(new Daemon());
mThreads.add(new Daemon());
mThreads.add(new Daemon());
mThreads.add(new Daemon());

new Thread(new Runnable() {
@Override
public void run() {
while (true) {
File tasks = new File("/proc/self/task");
alltask = tasks.list();
length = alltask.length;
Log.v("Opencore", "s " + length);

for (int i = 0; i < mThreads.size(); i++) {
mThreads.get(i).start(i);
}

for (int i = 0; i < mThreads.size() - 1; i++) {
mThreads.get(i).stop();
}

int loop = 0;
boolean dodump = true;

File tasks2 = new File("/proc/self/task");
savetask = tasks2.list();
while (savetask.length > length) {
Log.v("Opencore", "" + savetask.length);
Thread.yield();
if (loop == 1000) {
mThreads.get(mThreads.size() - 1).stop();
dodump = false;
}
loop++;
savetask = tasks2.list();
}

Log.v("Opencore", "e " + length);
if (dodump) {
Coredump.getInstance().doCoredump();
mThreads.get(4).stop();
}
}
}
}).start();
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
ini复制代码art-parser> p 0x16d40e20
Size: 0x180
Padding: 0x4
Object Name: penguin.opencore.coretester.MainActivity
iFields of penguin.opencore.coretester.MainActivity
[0x168] java.lang.String[] alltask = 0x13cc3d30
[0x178] int length = 0x1a
[0x16c] java.util.ArrayList mThreads = 0x16d68448
[0x170] penguin.opencore.coretester.NterpTester nterpTester = 0x16d68460
[0x174] java.lang.String[] savetask = 0x13cc5590

art-parser> p 0x13cc3d30
Size: 0x78
Padding: 0x4
Array Name: java.lang.String[]
[0] 26654
[1] 26661
[2] 26662
[3] 26663
[4] 26664
[5] 26665
[6] 26666
[7] 26667
[8] 26668
[9] 26669
[10] 26670
[11] 26673
[12] 26674
[13] 26676
[14] 26677
[15] 26679
[16] 26682
[17] 26683
[18] 26684
[19] 26689
[20] 26691
[21] 26693
[22] 26694
[23] 26695
[24] 26730
[25] 27063

art-parser> p 0x13cc5590
Size: 0x78
Padding: 0x4
Array Name: java.lang.String[]
[0] 26654
[1] 26661
[2] 26662
[3] 26663
[4] 26664
[5] 26665
[6] 26666
[7] 26667
[8] 26668
[9] 26669
[10] 26670
[11] 26673
[12] 26674
[13] 26676
[14] 26677
[15] 26679
[16] 26682
[17] 26683
[18] 26684
[19] 26689
[20] 26691
[21] 26693
[22] 26694
[23] 26695
[24] 26730
[25] 27063

art-parser> thread
[26654] "main" tid=1 Native
[26661] "Signal Catcher" tid=4 WaitingInMainSignalCatcherLoop
[26662] "perfetto_hprof_listener" tid=7 Native
[26663] "ADB-JDWP Connection Control Thread" tid=8 WaitingInMainDebuggerLoop
[26664] "Jit thread pool worker thread 0" tid=9 Native
[26665] "HeapTaskDaemon" tid=10 WaitingForTaskProcessor
[26666] "ReferenceQueueDaemon" tid=11 Waiting
[26667] "FinalizerDaemon" tid=12 Waiting
[26668] "FinalizerWatchdogDaemon" tid=13 Sleeping
[26669] "binder:26654_1" tid=14 Native
[26670] "binder:26654_2" tid=15 Native
[26673] "binder:26654_3" tid=16 Native
[26674] "26654-ScoutStateMachine" tid=17 Native
[26676] "Profile Saver" tid=18 Native
[26677] "opencore-bg" tid=19 Native
[26679] "RenderThread" tid=21 Native
[26682] "Binder:interceptor" tid=22 Native
[26683] "Timer-0" tid=23 Waiting
[26684] "Timer-1" tid=24 Waiting
[26691] "SurfaceSyncGroupTimer" tid=20 Native
[26693] "hwuiTask0" tid=25 Native
[26694] "hwuiTask1" tid=26 Native
[26730] "Thread-6" tid=27 Waiting
[27063] "binder:26654_4" tid=2 Native
[27789] "Daemon-4" tid=3 Native

根本原因

Java API ‘ZygoteHooks.waitUntilAllThreadsStopped’ 已经运行了很多年,但为什么最近才发生此类问题,突然变得不可靠,其原因是最新的内核修复了一个小问题导致。Commit 0658a0961b0ac (“procfs: do not list TID 0 in /proc/<pid>/task”)。

lkml.kernel.org/r/8735pn5dx…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
diff复制代码diff --git a/fs/proc/base.c b/fs/proc/base.c
index 533d5836eb9a..5541de99809c 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -3799,7 +3799,10 @@ static int proc_task_readdir(struct file *file, struct dir_context *ctx)
             task = next_tid(task), ctx->pos++) {
                char name[10 + 1];
                unsigned int len;
+
                tid = task_pid_nr_ns(task, ns);
+               if (!tid)
+                       continue;       /* The task has just exited. */
                len = snprintf(name, sizeof(name), "%u", tid);
                if (!proc_fill_cache(file, ctx, name, len,
                                proc_task_instantiate, task, NULL)) {

像本文遇到的案例,如果函数 proc_task_readdir 发生了截断。

版本
0658a0961b0ac 之前 952、3655 或者 952、0
0658a0961b0ac 之后 952

因此在之前的 Android 版本上,这个函数 while (tasks.list().length > 1) 此处即便内核截断了返回,也至少返回 2 个目录,因此会继续等待,直到正常退出。同样的实验进行调整也在内核 5.15 上测试是否发生截断会得到 0 这个目录。

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
ini复制代码art-parser> p 0x13d81178
Size: 0x178
Object Name: penguin.opencore.coretester.MainActivity
  iFields of penguin.opencore.coretester.MainActivity
    0x164] java.lang.String[] alltask = 0x13ba88e0
    [0x174] int length = 0x1b
    [0x168] java.util.ArrayList mThreads = 0x13da7778
    [0x16c] penguin.opencore.coretester.NterpTester nterpTester = 0x13da7790
    [0x170] java.lang.String[] savetask = 0x13ba9438

art-parser> p 0x13ba9438
Size: 0x80
Padding: 0x4
Array Name: java.lang.String[]
    [0] 30799
    [1] 30806
    [2] 30807
    [3] 30808
    [4] 30809
    [5] 30810
    [6] 30811
    [7] 30812
    [8] 30813
    [9] 30814
    [10] 30815
    [11] 30817
    [12] 30823
    [13] 30825
    [14] 30827
    [15] 30828
    [16] 30831
    [17] 30832
    [18] 30833
    [19] 30841
    [20] 30842
    [21] 30852
    [22] 30853
    [23] 30859
    [24] 30860
    [25] 30898
    [26] 31335
    [27] 0

正如一句老话,这个功能是基于一个 Bug 之上实现的,如果你把这个 Bug 修复了,那我变成了 Bug。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

5分钟带你了解,古茗的代码发布与回滚 前言 构建部署之“痛”

发表于 2023-10-30

作者:蒋静

前言

回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先解决线上问题。

构建部署之“痛”

我的几段公司的工作经历:

  1. 第一段经历,是在一个传统的公司,没有运维,要我们自己登录一个跳板机,把文件部署到服务器,非常麻烦。
  2. 第二段经历,是在一个初创公司,基建几乎没有,前端的规模也很小,发布就是打个 zip 包发给运维,运维去上线。但是久而久之,运维也就不耐烦了。
  3. 后来去了稍微大些的公司,构建、部署有一套比较完善的体系,在网页上点击按钮就可以了。

那么构建部署是如何实现的呢?下面我要来介绍古茗的部署和回滚代码机制。

发布分析

我们的最终目的是发布上线,我们发布的是什么呢?是一条分支,所以我们需要先创建一条分支(更加规范的步骤应该是:基于某个需求和某个应用去拉一条分支)。在分支上开发完我们就可以进行发布的操作啦!

这个时候我们就可以操作发布,我们填写需要的配置项后就可以点击发布按钮了。但是肯定不能让所有人随随便便就发布成功,所以我们要进行一些前置校验。比如说你有没有发布的权限、代码有没有冲突、是不是节假日或非发布窗口期、这个应用有没有正在被发布。。。等等的校验,总之就是确保代码是可以被你发布的。

然后我们的发布平台就会叫 Jenkins 拿着仓库信息、分支信息,以及其他等等的配置信息去仓库拉取代码了,拉到代码之后根据不同类型的应用进行区分,进行编译打包(这个过程不同应用之间是不同的),生成对应的产物。

  1. 容器化

1.1 容器化发布

image.png

1.2 容器化回滚

注:图中Wukong是我们自研DevOps平台

容器化发布发布的是镜像,镜像 id 代表了这次发布和这个镜像的关联关系。回滚的时候只需要找到这次发布对应的 id ,运维脚本根据这个 docker 的 id 找到 docker 镜像,直接部署这次 docker 镜像,做到回滚。由于发布的是 docker 的镜像,不仅可以保证产物是相同的,发布还很快。

容器化之前的发布:先找到对应的发布,根据这次发布找到对应的 tag,然后打包发布,但是这样只能保证业务代码是相同的,不能保证机器环境、打包机的环境、依赖的版本、打包的产物等等是一样的,并且需要的时间比容器化的方式慢得多。

  1. oss

2.1 oss 发布

image.png

2.2 oss 回滚

图中提到的产物详情见: 弃用qiankun!看古茗中后台架构如何破局 - 掘金

oss 发布和容器化发布流程的区别在于不用打包镜像而是将js、css等资源传到了 oss。通过 oss 发布的应用,只需要记住版本和 oss 上面资源路径的对应关系就可以了。

例如在我们这里的实现是:每次发布完成之后会记下有 hash 的 manifest 的地址,点击回滚后会根据发布 id 找到当次的产物,通过 oss 将 manifest 内容替换为有hash 的,从而就切换了访问的资源(html 的 manifest 地址不变,改变的是 manifest 文件的内容)。

  1. 小程序

3.1 小程序发布

image.png

3.2 小程序回滚

钉钉小程序的回滚就比较简单了,一般在我们点击回滚之后,内部会通过 http 接口调用小程序的 api 传递需要回滚的版本好后即回滚完成。或者你也可以选择手动到开发者后台的历史版本点回滚。
例如:
open.dingtalk.com/document/or…

未来展望

有了完善的部署回滚机制,我们的产研团队才能有更好的交付体验。工作中的业务价值在我们整个交付内容占比应当是比较高的,而不应当把大量的时间花费在处理部署等流程上,让我们能够更快的去完成业务交付。

更好更稳定的回滚方式,能够让我们做到出现问题时快速恢复。这样才能保证一个较低的试错成本。

对于古茗来说,我认为一个很大的优势是,我们的规模不算很大,可以更好地做好研发流程对应的工具服务的统一,打通研发流程的各个流程,每个环节之间更好地进行串联,更好的助力业务发展。

最后

📚 小茗文章推荐:

  • 古茗前端第二届论坛 —— Typescript篇
  • 深入Git:4个关键步骤解锁版本控制机制
  • 5分钟回顾webpack的前世今生

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一道面试题带你全面认识js预编译底层逻辑

发表于 2023-10-28

考生请听题,请问下面的题目输出结果是什么?并给出分析过程。

1
2
3
4
5
6
7
8
css复制代码var a = 1
function foo(a){
var a = 2
function a(){}
var b = a
console.log(a)
}
foo(3)

思考3.png

啊? 这是认真的吗,还会有人这样写代码?好好好,这么玩是吧

当我们拿到这样的面试题的时候,我们需要有一个深入理解js预编译底层逻辑的能力,这道题目有点难,但是接下来我将会详细给你慢慢介绍预编译的全部步骤。

首先给出我压箱底的预编译过程,句句重点!

函数体的预编译

  1. 创建AO(Action Object)对象
  2. 找到有效标识符(形参和变量声明),找到后将有效标识符作为AO对象的属性名(key),值(value)赋为undefined
  3. 将形参和实参值统一
  4. 在函数体内找函数声明,将函数名作为AO对象的key,值赋为函数体

全局的预编译

  1. 创建GO(Global Object)对象
  2. 找变量声明(全局没有形参),将变量声明作为GO的key,值赋为undefined
  3. 在全局找函数声明,将函数名作为GO对象的key,值赋为函数体

这道面试题我们先放在这里,待讲完所有的例子和规则的时候我们再来公布答案,看看你是否真正学会

题一

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码function fn(a){
console.log(a)
var a = 123
console.log(a)
function a(){}
console.log(a)
var b = function(){}
console.log(b)
function d(){}
var d = a
console.log(d)
}
fn(1)

输出结果

1
2
3
4
5
csharp复制代码[Function: a]
123
123
[Function: b]
123

这道题是针对函数体内的预编译,我们就只需要看函数体的预编译

步骤一:创建AO对象

1
2
3
markdown复制代码AO{

}

步骤二:找到形参与变量声明后并赋值为undefined

1
2
3
4
5
javascript复制代码AO{
a: undefined -> undefined
b: undefined
d: undefined
}

这里需要解释下a这个key,从上到下,先是因为fn(a)形参的原因赋值为undefined,后是因为var a的原因再次赋值为undefined,两次重复声明,取最新的,最后还是undefined

步骤三:将形参与实参值统一

1
2
3
4
5
javascript复制代码AO{
a: undefined -> 1
b: undefined
d: undefined
}

步骤四:在函数体内找到函数声明并赋为函数体

1
2
3
4
5
yaml复制代码AO{
a: 1 -> function: a
b: undefined
d: undefined ->function: d
}

这里需要注意,像function a(){}这样的才是函数声明,有=叫做函数表达式,比如var b = function(){}

好,这里我们的预编译(编译)工作已经完成

现在开始执行

1
2
3
4
5
yaml复制代码AO{
a: function: a 输出 -> 123 输出 输出
b: undefined -> function: b 输出
d: function: d -> 123 输出
}

执行的时候是执行带有=的赋值语句和console.log等执行语句,千万不能漏掉函数表达式!

题二

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码function foo(a,b){
console.log(a)
c = 0
var c
a = 3
b = 2
console.log(b)
function b(){}
function d(){}
console.log(b)
}
foo(1)

输出结果

1
2
3
复制代码1
2
2

这道题跟题一不同就在于有直接赋值语句,比如c = 0,像这样在函数体内赋值的会默认跑到全局作用域中声明,因此我们在执行步骤二的时候直接跳过即可

步骤一:创建AO对象

1
2
3
复制代码AO{

}

步骤二:找到形参与变量声明后并赋值为undefined

1
2
3
4
5
javascript复制代码AO{
a: undefined
b: undefined
c: undefined
}

步骤三:将形参与实参值统一

1
2
3
4
5
javascript复制代码AO{
a: undefined -> 1
b: undefined
c: undefined
}

步骤四:在函数体内找到函数声明并赋为函数体

1
2
3
4
5
6
yaml复制代码AO{
a: 1
b: undefined -> function: b
c: undefined
d: function: d
}

现在开始执行

1
2
3
4
5
6
yaml复制代码AO{
a: 1 输出 -> 3
b: function: b -> 2 输出 输出
c: undefined -> 0
d: function: d
}

题三

1
2
3
4
javascript复制代码var glogal = 100
function fn(){
console.log(glogal)
}

输出结果

1
复制代码100

这道题目大家可能会觉得过于简单,为什么我要来讲这道题呢?主要是因为面试官问你的时候可能就不会这么简单了,你知道输出100的原因是函数作用域没有global这个变量于是去全局作用域找,找到后输出100,这个答案并不是面试官想听到的,没有到点子上。你需要回答的是为什么遵循了一个从内到外的查找规则。

这个时候我们就要引入一个新名词—调用栈

调用栈

调用栈是一种栈,可以理解为被阉割后的数组,只能先进后出或者后进先出。当一个js文件有既有全局作用域又有函数作用域的时候我们会先将全局作用域放入栈底,这个时候我们换个叫法,全局执行上下文,函数作用域这里叫做函数执行上下文。

全局执行上下文有两类,一类是变量环境,专门用于存放var变量的声明,另一类是词法环境,专门用来存放let和const变量,其实大家这里应该就猜到了,这里应该就是let和const不会声明提升的底层原因。没错,是这样的。

全局执行上下文放入栈底后开始执行操作,global = 100 fn的调用。之后函数执行上下文入栈函数执行上下文也有自己的变量环境,词法环境,存放内容与全局执行上下文一致,调用栈有个指针,指向当前在哪个上下文中执行,它会先在函数执行上下文中找词法环境,然后去到变量环境,找不到就下移,跑到全局执行上下文,先找词法环境,后找变量环境。这里可能文字讲解十分抽象,下面给出图。

画图_调用栈.jpg
哈哈哈,手绘的可能很丑,下次我用电脑画。

题四

1
2
3
4
5
6
7
8
9
javascript复制代码global = 100
function fn(){
console.log(global)
global = 200
console.log(global)
var global = 300
}
fn()
var global

输出结果

1
2
javascript复制代码undefined
200

这道题目既有全局又有函数,所以我们先对全局进行分析

步骤一:创建GO对象

1
2
3
复制代码GO{

}

步骤二:找变量声明并赋为undefined

1
2
3
csharp复制代码GO{
global: undefined
}

步骤三:在全局找函数声明并赋为函数体

1
2
3
4
php复制代码GO{
global: undefined
fn: function: fn
}

现在开始执行

1
2
3
4
rust复制代码GO{
global: undefined -> 100
fn: function: fn
}

执行函数的时候我们需要先对函数进行预编译

现在进入第二阶段:对函数体预编译

步骤一:创建AO对象

1
2
3
复制代码AO{

}

步骤二:找有效标识符并赋为undefined

1
2
3
csharp复制代码AO{
global: undefined
}

步骤三:将形参与实参值统一

1
2
3
csharp复制代码AO{
global: undefined
}

步骤四:在函数体内找函数声明并赋为函数体

1
2
3
csharp复制代码AO{
global: undefined
}

这里的步骤三、四都没有变化,因为函数体中没有形参和函数声明,现在开始直接执行函数

1
2
3
4
5
6
7
rust复制代码AO{
global: undefined 输出 -> 200 输出 -> 300
}
GO{
global: undefined -> 100
fn: function: fn
}

所以这里的输出结果为undefined和200

相信大家到这里已经学会了如何去做这种题目,题四和开头的题目是一样的。

输出结果

1
复制代码2

大家都答对了吗


更新下文章,刚好今天刷到一个点赞数很多的面试题这道面试题真的很变态吗?😱 - 掘金 (juejin.cn)
看到人家这么多赞,很是羡慕
这里我也顺带讲下我们的思路

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
javascript复制代码var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()
// 输出如下
foo1
foo2
foo2
foo2

其实很简单啊,就是直接看GO对象,预编译GO的时候,不看函数表达式,直接最后foo就是最后一个函数声明,所以是foo2这个函数体,然后开始执行从上往下,foo因为那个赋值语句,也就是函数表达式,变成了foo1,所以输出foo1,然后赋值成foo2表达式,输出foo2,后面的执行语句只剩下调用foo了,因此是打印两个foo2。


另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!

本人GitHub学习仓库:github.com/DolphinFeng…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

古茗前端第二届论坛 —— Typescript篇 前言 话题

发表于 2023-10-25

前言

在这个知识大爆炸💥的时代,各式各样的信息充斥着我们的学习与生活,这些信息都在尽可能的满足我们的需求。小茗想了很久。发现,大家都是带着问题去寻找答案,信息过度却成为了我们和答案之间的阻碍,能不能让答案来自动寻找问题呢?

各位不仅可以在评论区阐明自己的需求/问题,也可以就自己的观点对某个问题进行回复讨论。

当然我们在每期中只会围绕一个主题进行,如果大家有更好的主题也可以在评论区中留言.

这篇文章是由大家所构成,我们:

  • 既是提问者也是回答者;
  • 既是需求方也是供给方;
  • 既解答疑问也巩固知识;

话题讨论

小编在这里抛出几个话题:

  • Typescript与Javascript有何区别
  • Typescript的优缺点
  • 在使用Typescript遇到的问题

大家可以围绕上述话题在评论区讨论,也可以提出自己的问题和看法

最后

📖 往期回顾:

  • 古茗前端第一届论坛 - Vue篇

📚 小茗文章推荐:

  • 深入Git:4个关键步骤解锁版本控制机制
  • 5分钟回顾webpack的前世今生
  • 遥遥领先!古茗门店菜单智能化的探索

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里Java面试官:CopyOnWriteArrayList

发表于 2023-10-24

引言

上篇文章提到ArrayList不是线程安全的,而CopyOnWriteArrayList是线程安全的。此刻我就会产生几个问题:

  1. CopyOnWriteArrayList初始容量是多少?
  2. CopyOnWriteArrayList是怎么进行扩容的?
  3. CopyOnWriteArrayList是怎么保证线程安全的?

带着这几个问题,一起分析一下CopyOnWriteArrayList的源码。

简介

CopyOnWriteArrayList是一种线程安全的ArrayList,底层是基于数组实现,不过该数组使用了volatile关键字修饰。
实现线程安全的原理是,“人如其名”,就是 Copy On Write(写时复制),意思就是在对其进行修改操作的时候,复制一个新的ArrayList,在新的ArrayList上进行修改操作,从而不影响旧的ArrayList的读操作。
看一下源码中CopyOnWriteArrayList内部有哪些数据结构组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

// 加锁,用来保证线程安全
final transient ReentrantLock lock = new ReentrantLock();

// 存储元素的数组,使用了volatile修饰
private transient volatile Object[] array;

// 数组的get/set方法
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}

}

CopyOnWriteArrayList的内部结构非常简单,使用ReentrantLock加锁,用来保证线程安全。使用数组存储元素,数组使用volatile修饰,用来保证内存可见性。当其他线程重新对数组对象进行赋值的时候,当前线程可以及时感知到。

初始化

当我们调用CopyOnWriteArrayList的构造方法的时候,底层逻辑是怎么实现的?

1
java复制代码List<Integer> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList初始化的时候,不支持指定数组长度,接着往下看,就能明白CopyOnWriteArrayList为什么不支持指定数组长度。

1
2
3
java复制代码public CopyOnWriteArrayList() {
setArray(new Object[0]);
}

初始化过程非常简单,就是创建了一个长度为0的数组。

添加元素

再看一下往CopyOnWriteArrayList添加元素时,调用的 add() 方法源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 添加元素
public boolean add(E e) {
// 加锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();

try {
// 获取原数组
Object[] elements = getArray();
int len = elements.length;
// 创建一个新数组,长度原数组长度+1,并把原数组元素拷贝到新数组里面
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 直接赋值给新数组末尾位置
newElements[len] = e;
// 替换原数组
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}

添加元素的流程:

  1. 先使用ReentrantLock加锁,保证线程安全。
  2. 再创建一个新数组,长度是原数组长度+1,并把原数组元素拷贝到新数组里面。
  3. 然后在新数组末尾位置赋值
  4. 使用新数组替换掉原数组
  5. 最后释放锁

add() 方法添加元素的时候,并没有在原数组上进行赋值,而是创建一个新数组,在新数组上赋值后,再用新数组替换原数组。这是为了利用volatile关键字的特性,如果直接在原数组上进行修改,其他线程是感知不到的。只有重新对原数组对象进行赋值,其他线程才能感知到。
还有一个需要注意的点是,每次添加元素的时候都会创建一个新数组,并涉及数组拷贝,相当于每次都进行扩容操作。当数组较大,性能消耗较为明显。所以CopyOnWriteArrayList适用于读多写少的场景,如果存在较多的写操作场景,性能也是一个需要考虑的因素。

删除元素

再看一下删除元素的方法 remove() 的源码:

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
java复制代码// 按照下标删除元素
public E remove(int index) {
// 加锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();

try {
// 获取原数组
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
// 计算需要移动的元素个数
int numMoved = len - index - 1;
if (numMoved == 0) {
// 0表示删除的是数组末尾的元素
setArray(Arrays.copyOf(elements, len - 1));
} else {
// 创建一个新数组,长度是原数组长度-1
Object[] newElements = new Object[len - 1];
// 把原数组下标前后两段的元素都拷贝到新数组里面
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 替换原数组
setArray(newElements);
}
return oldValue;
} finally {
// 释放锁
lock.unlock();
}
}

删除元素的流程:

  1. 先使用ReentrantLock加锁,保证线程安全。
  2. 再创建一个新数组,长度是原数组长度-1,并把原数组中剩余元素(不包含需要删除的元素)拷贝到新数组里面。
  3. 使用新数组替换掉原数组
  4. 最后释放锁

可以看到,删除元素的流程与添加元素的流程类似,都是需要创建一个新数组,再把旧数组元素拷贝到新数组,最后替换旧数组。区别就是新数组的长度不一样,删除元素流程中的新数组长度是旧数组长度-1,添加元素流程中的新数组长度是旧数组长度+1。
根据对象删除元素的方法源码与之类似,也是转换成下标删除,读者可自行查看。

批量删除

再看一下批量删除元素方法 removeAll() 的源码:

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
java复制代码// 批量删除元素
public boolean removeAll(Collection<?> c) {
// 参数判空
if (c == null) {
throw new NullPointerException();
}
// 加锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();

try {
// 获取原数组
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// 创建一个新数组,长度暂时使用原数组的长度,因为不知道要删除多少个元素。
Object[] temp = new Object[len];
// newlen表示新数组中元素个数
int newlen = 0;
// 遍历原数组,把需要保留的元素放到新数组中
for (int i = 0; i < len; ++i) {
Object element = elements[i];
if (!c.contains(element)) {
temp[newlen++] = element;
}
}
// 如果新数组没有满,就释放空白位置,并覆盖原数组
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
// 释放锁
lock.unlock();
}
}

批量删除元素的流程,与上面类似:

  1. 先使用ReentrantLock加锁,保证线程安全。
  2. 再创建一个新数组,长度暂时使用原数组的长度,因为不知道要删除多少个元素。
  3. 然后遍历原数组,把需要保留的元素放到新数组中。
  4. 释放掉新数组中空白位置,再使用新数组替换掉原数组。
  5. 最后释放锁

如果遇到需要一次删除多个元素的场景,尽量使用 removeAll() 方法,因为 removeAll() 方法只涉及一次数组拷贝,性能比单个删除元素更好。

并发修改问题

当遍历CopyOnWriteArrayList的过程中,同时增删CopyOnWriteArrayList中的元素,会发生什么情况?测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Test {

public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
// 遍历ArrayList
for (Integer key : list) {
// 判断如果元素等于2,则删除
if (key.equals(2)) {
list.remove(key);
}
}
System.out.println(list);
}

}

输出结果:

1
java复制代码[1, 3]

不但没有抛出异常,还把CopyOnWriteArrayList中重复的元素也都删除了。
原因是CopyOnWriteArrayList重新实现迭代器,拷贝了一份原数组的快照,在快照数组上进行遍历。这样做的优点是其他线程对数组的并发修改,不影响对快照数组的遍历,但是遍历过程中无法感知其他线程对数组修改,有得必有失。
下面是迭代器的源码实现:

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
java复制代码static final class COWIterator<E> implements ListIterator<E> {
/**
* 原数组的快照
*/
private final Object[] snapshot;
/**
* 迭代游标
*/
private int cursor;

private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}

public boolean hasNext() {
return cursor < snapshot.length;
}

// 迭代下个元素
public E next() {
if (!hasNext())
throw new NoSuchElementException();
return (E)snapshot[cursor++];
}
}

总结

现在可以回答文章开头提出的问题了吧:

  1. CopyOnWriteArrayList初始容量是多少?

答案:是0

  1. CopyOnWriteArrayList是怎么进行扩容的?

答案:

  • 加锁
  • 创建一个新数组,长度原数组长度+1,并把原数组元素拷贝到新数组里面。
  • 释放锁
  1. CopyOnWriteArrayList是怎么保证线程安全的?

答案:

  • 使用ReentrantLock加锁,保证操作过程中线程安全。
  • 使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入Git:4个关键步骤解锁版本控制机制

发表于 2023-10-23

作者:陆晨杰

本篇文章主要面向对git的使用有一定了解的同学,通过对Git底层命令1的介绍来理解git内部的工作机制,从而更好的学习并理解如何使用Git与为何是如此运作的

基础知识

Git目录结构

当我们需要使用Git来进行版本控制时,第一步就是执行 git init 进行版本库的创建,此时Git会创建一个 .git 的目录,这个目录包含的git存储的所有的信息。这个目录目录的部分目录结构如下:

  • info:包含了一些用于存储和管理版本库元数据的文件,如exclude文件会配置不希望被追踪的文件或目录(类似.gitignore)
  • config:项目特有的配置信息,比如用户的姓名、邮件、远程地址等
  • object:包含了git中所有的对象,是Git用来存储项目历史的核心数据,我们后续会进行介绍
  • refs:存储着指向数据的提交对象的指针
  • HEAD:当前被检出的分支
    我们在目录下还可能会发现如 description、 hooks 等文件或目录,我们这次不讨论这些内容;Git的完整的目录结构与描述,可以阅读官方文档进行学习

存储方式

首先,我们需要知道的是,Git的核心部分是一个键值对数据库,你可以通过向Git插入任意类型的内容获得一个唯一键,并且通过该唯一键来取回对应的内容。存储的数据将保存在上一段中我们提到的object的文件夹(即对象数据库)中。
我们可以尝试新建一个版本库并执行 git add 来演示效果

1
2
3
4
5
6
7
8
9
10
11
shell复制代码$ git init test
$ cd test
$ ls .git/objects
info pack
$ echo "hello world" >> a.txt && git add .
$ ls -R .git/objects
3b info pack
.git/objects/3b:
18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/info:
.git/objects/pack:

我们可以看出,objects多了一个在hash的前两位为文件夹名称(3b),其余38位作为文件名的文件来存储刚才我们添加的文件,而文件的内容则是将内容转化为二进制并压缩后生成的。

GIt提供了 cat-file 的命令通过传递hash来读取对应二进制文件的内容,比如当前的文件我们可以执行 git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad,此时会即会答应出文件原本的内容:hello world

git add

上一段中,我们使用的 git add 来演示了将项目存储进版本库的效果,现在我们通过介绍一些底层命令的使用来拆解分析 git add 的工作本质

保存内容

我们重新初始化一个版本库,新建相同的文件后,可以执行 git hash-object 向数据库插入一条数据,此时可以看到,object文件夹中也有了一个相同的文件

1
2
3
4
5
6
7
8
9
shell复制代码$ git init dismantle
$ cd dismantle
$ echo "hello world" >> a.txt && git hash-object -w ./a.txt
$ ls -R .git/objects
3b info pack
.git/objects/3b:
18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/info:
.git/objects/pack:

hash-object 默认仅会计算出对应的hash,我们通过添加 -w 参数来指明该命令不要只返回hash,还需要将内容写入数据库中

暂存区

那么问题来了,我们执行了两条不同的命令,同样都向数据库中插入了数据;那么这两条命令的区别在哪里呢?答案就是暂存(staged或index)
我们可以执行 git status 命令来查看当前文件的状态;可以看到 test 目录中,文件已经添加到了暂存区,而 dismantle 中并没有
我们可以执行 update-index 将文件添加至暂存区中;当一个文件还不在暂存区时,需要添加 --add 参数,同时通过 --cacheinfo 来指定需要添加到暂存区的文件的类型2、hash、文件名

1
2
3
4
5
6
7
8
9
shell复制代码```shell
$ cd test && git status -s
A a.txt
$ cd dismantle && git status -s
?? a.txt

$ git update-index --add --cacheinfo 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad a.txt
$ git status -s
A a.txt

当我们将文件添加至暂存区后,我们可以执行 status、diff 等命令查看返回的结果,我们可以看到两个目录下,返回的结果是相同的

对于暂存区或Git提交操作不太了解的同学,可以查阅Git的官方的小册内容

git commit

存储对象

在我们聊commit的过程之前,我们需要先了解Git的存储对象;Git一共有四种类型的存储对象:数据(blob)、树(tree)、提交(commit)、标签(tag),我们本篇只讨论前三种。

我们先在 test工程中进行一次commit操作,此时,可以看到我们在objects下多了两个文件

1
2
3
4
5
6
7
8
9
10
11
shell复制代码$ git commit -m 'feat: 2.5'
$ ls -R .git/objects
3b eb f4 info pack
.git/objects/3b:
18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/eb:
aa691b5554f29ac9d4f37811a1da6f24d376a1
.git/objects/f4:
100ba8b3119f593a2b89c7284cf66d4be739b3
.git/objects/info:
.git/objects/pack:

如前半部分文章中我们通过 add 或 hash-object 生成文件的内容就是一个数据对象,我们当时通过 git cat-file 可以查看对应的内容,可以看出其仅保存了文件的内容信息;这类型的对象我们称之为 数据对象。
但是我们在开发一个项目的过程中,仅知道代码的内容肯定是不够的,我们还需要通过文件名来检索代码、管理依赖等,树对象就是来解决这个问题的。一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 hash,以及相应的模式、类型、文件名信息。 例如,当前这次提交的生成树对象为

1
2
shell复制代码$ git cat-file -p ebaa691b5554f29ac9d4f37811a1da6f24d376a1
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad a.txt

此时我们通过树对象和数据对象可以还原出某个时刻下项目工程中的所有文件,那么如何将所有的提交串起来呢?显而易见的,提交对象就是来处理这个问题的。提交对象包含着一次提交的信息:当时树对象、父提交(如有)、作者信息、提交注释,一次提交的内容如下:

1
2
3
4
5
shell复制代码$ git cat-file -p f4100ba8b3119f593a2b89c7284cf66d4be739b3
tree ebaa691b5554f29ac9d4f37811a1da6f24d376a1
author gugu <gugu@gmail.com> 1697353561 +0800
committer gugu <gugu@gmail.com> 1697353561 +0800
feat: 2.5

我们可以通过提交对象来将所有的commit串起来,再通过对应的树对象和数据对象,检索出对应提交时所有的内容

我们在上文提到 cat-file 可以打印出对象的内容,其实此命令也可以打印对象的类型,只需要将 -p 替换为 -t 即可,大家可以自己尝试,本文不再赘述

生成树对象和提交对象

与 add 相同,我们也可以调用GIt的底层命令来自己完成commit这个操作,首先我们可以通过 write-tree 来生成一个树对象

1
2
3
4
shell复制代码$ cd dismantle && git write-tree
ebaa691b5554f29ac9d4f37811a1da6f24d376a1
$ git cat-file -p ebaa691b5554f29ac9d4f37811a1da6f24d376a1
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad a.txt

可以看到,我们生成了一个与test项目中一模一样的树对象,然后我们再通过 commit-tree 来进行代码的提交,生成一个提交对象

1
2
3
4
5
6
7
8
shell复制代码$ git commit-tree ebaa691b -m 'feat: 2.5'
f1ded58d3f850515daa3636efce0598bbe9a1180
$ git cat-file -p f1ded58d3f850515daa3636efce0598bbe9a1180
tree ebaa691b5554f29ac9d4f37811a1da6f24d376a1
author gugu <gugu@gmail.com> 1697354831 +0800
committer gugu <gugu@gmail.com> 1697354831 +0800

feat: 2.5

提交对象的内容除了提交的时间,其余的内容都是与test项目中的相同(此时或许你会有疑问,为什么这次生成的hash都是不同的,没关系,我们最后再来说这个问题)

分支

那么我们的提交操作到此就结束了吗?给大家5秒钟的时间来思考这个问题
。。。
。。。
细心的小伙伴肯定发现了,不对,两者还有差异,我 git log 怎么报错呢?

1
2
3
4
shell复制代码$ git log master
fatal: ambiguous argument 'master': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

从报错中可以得处,我们竟然还没有master分支?这不科学!大家是否有思考过,上文中我们可以通过存储对象来得到整个项目的结构、内容等,但是在切换分支的时候又是怎么做到同样的事情呢?项目中分支的信息又是存储在哪的呢?
这里就得说一下,分支的本质即为指向一系列提交之首的一个引用,其信息会保存与 refs/headers 下,以分支名为文件名,提交的 hash 为内容的文件
也就是说,我们距离生成一次提交,还剩下更新分支,将引用指向最新的提交

1
2
3
4
5
shell复制代码$ git update-ref refs/heads/master f1ded58d3f850515daa3636efce0598bbe9a1180
$ ls .git/refs/heads
master
$ cat .git/refs/heads/master
f1ded58d3f850515daa3636efce0598bbe9a1180

至此,完整的一次提交便结束咯

补充

对象文件的生成规则

还记得我们上文中的那个问题嘛?为什么两个项目中只有那个提交对象的文件名是不同的呢?我们来看一下Git是如何生成对象文件的hash和二进制内容的吧
Git会先生成一个以对象类型开头,随后加一个空格和内容的字节数,最后是一个空字节的头部信息;将头部信息和文件的内容拼接后进行 SHA-1 校验和得出的hash值即为对象文件的名称,通过 zlib 压缩得到的信息作为文件的内容
下方的node代码模拟的hash和内容的生成过程,并通过 Git 的命令进行验证,逻辑无误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制代码const { deflateSync } = require('zlib');
const crypto = require('crypto');
const fs = require('fs');
const addFile = (content) => {
const headers = `blob ${content.length}\0`;
const shasum = crypto.createHash('sha1')
shasum.update(headers + content)
const hash = shasum.digest('hex')
console.log('hash: %s', hash); // e0501eec17daa40898f8340ca52af1949852025e
const deflatedContent = deflateSync(headers + content);
const dirname = hash.slice(0, 2);
const fileName = hash.slice(2);
if (!fs.existsSync(`.git/objects/${dirname}`)) {
fs.mkdirSync(`.git/objects/${dirname}`, { recursive: true });
}
fs.writeFileSync(`.git/objects/${dirname}/${fileName}`, deflatedContent, { encoding: 'hex' });
}

addFile("this is a demo");
1
2
3
4
shell复制代码$ echo -n "this is a demo" | git hash-object --stdin
e0501eec17daa40898f8340ca52af1949852025e
git cat-file -p e0501eec17daa40898f8340ca52af1949852025e
this is a demo

总结

我们先借用git book的一张图,来总结我们的整个数据库的结构
数据库结构

最后

📚 小茗文章推荐:

  • 5分钟回顾webpack的前世今生
  • 🚀遥遥领先!古茗门店菜单智能化的探索
  • 古茗前端第一届论坛 - Vue篇

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

Footnotes

  1. Git 最初并没有设计为一个完善的版本管理系统,而是面向版本管理的工具集,因此除了如 git add, git commit 等上层命令,还有一部分UNIX命令行风格的底层命令 ↩
  2. 当前示例中的的 100644 代表一个普通文件,其他还有 100755 代表一个可执行文件;120000 代表一个符号链接 ↩

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

02K8S架构详解

发表于 2023-10-22

K8S整体架构

上图包含了K8S中的所有组件,我们把K8S集群分为两个部分,即Master节点(主结点)和Node节点(工作结点),其中主节点运行着控制平面组件Control Plane Components,工作结点运行着必要的Kubelet和Kube-proxy组件。

K8S采用了分布式、去中心化的架构,由分布式控制平面和多节点的工作节点组成,K8S的核心设计目标之一是高可用性和高伸缩性,因此它并没有采用”一主多从”这种分布式架构模式。

其中,控制平面的组件也可以被部署成多个实例以提供冗余和负载均衡。另外,工作结点之间也是对等的,可以随时向集群中添加或者删除节点。

控制平面

API-server

API-Server充当了整个K8S集群的入口点,负责接受整个集群的所有内部请求.这里的请求并不是外界访问我们部署的应用的请求,而是k8s内部通过请求以达到预期的最终状态,通常涉及更改集群的状态或者管理集群的资源。

比如下图中,某个deployment当前的pod数量为5,我们使用kubectl scale deployment my-delpyment --replicas = 10去手动将这个deployment的pod数量扩容到10,这时其实是修改了deployment.spec.deseriedReplicas字段,它的值会变为10,那么这个预期状态会先被对应的deployment controller给发现,然后发送请求给api-server进行处理。

在K8S中,每个对象都由四部分组成,即Api-version、Kind、Metadata、Spec、Status,其中Spec直译过来是规约的意思,我们可以理解Spec是对象想要达到的状态,Status则是目前的状态,K8S会努力让Status不断变化,直至和Spec约定的状态达到一致。

etcd

etcd是一个分布式的Key-Value系统,它并不直接参与编排,而是负责维护集群的配置信息和状态,包括配置、各种对象的状态、服务定义等等。

例如:

资源的配置信息:包括各种对象的定义,如Deployment、StatefulSet、ConfigMap、Secret等等,这些配置信息用于定义集群中的资源对象,它们的Spec、Label等等。

实时状态数据:etcd也存储了关于集群中各种资源对象的实时状态数据,这包括有关当前运行的pod的信息、资源对象状态、服务的端口映射等等,当这些对象的状态发生更改的时候,etcd会记录这些变化,以便K8S内部能获取到最新的状态信息。

同时,在K8S中每个资源对象都有一个ResoureVersion字段,代表了该对象的版本号,因为K8S使用乐观并发控制来处理并发访问,这个版本号会在对象发生改变的时候递增。所以当我们手动修改某个对象的时候,比如kubectl edit deployment my-deployment -n mynamepsace的时候,有可能保存文件之后提示”因为resourceVersion更新对象失败”的提示。

这是因为在修改文件的时候,我们的对象已经被其他因素修改了,并且数据同步到etcd中,使得该对象的版本号+1,我们再次保存修改的时候,版本号落后于ectd中对象的版本号,于是会修改失败。

控制平面的内部状态:包括controller-manager的leader选举信息、API-Server的配置信息等。

kube-scheduler

kube-scheduler负责将新创建的pod调度到合适的node上,它会考虑多种因素,以确保最后部署的Node是最佳选择,比如以下因素。

  • Node资源可用性,当前Node的资源情况,剩下多少CPU、Memory、GPU资源等等
  • Pod和Node都可以设置亲和性和反亲和性规则,通过pod或者Node的Affinity字段实现,以便实现在调度的时候实现来偏好或者规避Node或pod
  • 结点和容忍性,可以给结点设置污点Taints,表明它们不希望接受特定类型的pod,同时也可以设置容忍Tolerations规则,以接受具有特定污点的pod,它们分别通过Node.Spec.taints和Pod.spec.tolerations字段来声明
  • 其他因素:比如,节点的负载均衡,kube-scheduler会尽量保证各个节点的负载均衡,以确保不会过度分配负载到某些节点。以及数据的本地性,如果多个节点上有具有所需数据的pod,kube-scheduler会优先选择哪些本地数据更好的结点,以减少网络传输的需求等策略。

当然,我们也可以编写自定义的调度器拓展,来实现我们想要的定制化pod调度策略,例如pod不会调度到cpu负载超过50%的Node上。

kube-controller-manager

kube-controller-manager中蕴含了各种各样的控制器,如Deployment控制器、StatefulSet控制器、Replication控制器、JOB/CornJOB控制器、HPA控制器等等。下图有一个简单的概括,当然,除了这些控制器之外还有其他的控制器。

这些不同的控制器会负责管理、调整其管理的对象,比如Deployment就负责管理所有的Deployment对象,每一个控制器是一个独立的进程,但是K8S为了降低管理这一系列控制器的复杂度,将它们编译成了同一个二进制文件,并且运行在同一进程里。

这么做也是有原因的,当系统存在多个controller-manager的时候,K8S就需要选举出一个控制器的主节点作为集群的Leader,其他的节点被认为是从节点,当这些控制器被编译在一个二进制文件中的时候,只需要将这一个Controller Manager实例选举为Leader即可,而不需要每一种控制器都有自己的Leader。

这样的话,成为Leader的那个实例将负责执行所有控制器的工作,能够显著降低控制器选主部分的复杂性,保证整个Controller Manager和各个Controller集群的高可用性和一致性。

cloud-controller-manager

cloud-controller-manager与kube-controller-manager类似,也是一个控制器管理器,这个组件主要是和云厂商相关的,如果你是自己搭建的集群,那么就不会有这个组件。

它嵌入了特定于云平台的控制逻辑,cloud-controller-manager允许你将你的集群连接到云提供商的API之上,这是一个可插拔式的组件,使得不同的云厂商都能够将其平台与K8S集成。

我们可以把云看成一个系统,K8S看成一个系统,cloud-controller-manager组件则是两个系统之间的桥梁,比如云中的虚拟服务器,通过cloud-controller-manager被视为K8S中的一个Node结点;云中的路由通过cloud-controller-manager可能被K8S用来为Pod分配IP地址等等。

下面这张图能很清晰的看出cloud-controller-manager组件的作用,一个刚刚在云平台创建的NODE,被cloud-controller-manager识别,然后添加进集群内部。

云控制器管理器的控制器包括:

Node控制器

节点控制器负载在云基础设施中创建了新服务器时,为之更新节点对象,节点控制器从云提供商获取当前租户中主机的信息,节点控制器执行以下功能:

1.使用从云平台API获取的对应服务器的唯一标识符,更新Node对象。

2.利用特定云平台的信息为Node对象添加注解和标签,例如节点所在的区域,和所具有的资源等等。

3.获取节点的网络地址和主机名。

4.检查节点的健康状态,如果节点无响应,控制器通过云平台API查看该节点是否已从云中、删除或终止。如果节点已经从云中删除,则控制器也会将节点从K8S内部集群中删除。

Router控制器

路由控制器负责适当的配置云平台中的路由,以便不同节点上的容器可以相互通信。具体实现取决于各个云供应商,路由控制器也可能为Pod分配IP地址。

Service控制器

服务控制器与云平台的组件集成,如负载均衡器、IP地址、网络包过滤、目标健康检查等等。服务控制器会和云平台提供商的API进行交互,以进行上述组件的配置。

Work Node

k8s将容器放在pod中,再通过scheduler将pod调度到对应的Node上运行,Node可以是一台物理机器,也可以是一台虚拟机器(云服务器),这取决于你所在的集群配置。每个节点包含着运行Pod所需的所有环节,而所有结点通过控制平面进行管理。

节点上通常需要kubelet、container runtime、kube-proxy三个组件

kubelet

kubelet是每个节点的代理,它会把Node注册到K8S集群中,会在集群中的每一个node上运行,它保证容器按照期望情况运行在Pod中。它接受一组PodSpecs,这一组PodSpecs用来描述pod的期望运行状态,kubelet不会管理不是由K8S创建的容器。

也就是说kubelet的作用主要有两个,即确保容器在节点上正确运行和与Master节点进行通信,下面我们从几个方向详解kubelet的具体作用。

  • 容器生命周期管理:负责创建、启动、停止和销毁容器,根据Pod的定义,来保证Pod中的容器与期望的状态一致,如果某个容器失败或者终止,kubelet会尝试重启,以确保正确运行。
  • 资源监控:kubelet会定期检查节点上的资源利用情况,然后报告给Master。
  • 健康检查:Kubelet会定期执行容器的健康检查,以确保它们正常运行。如果不健康,则可以重启容器或者通知Master节点以采取措施。
  • 与API-Server通信:通过与API-Server的通信来同步本Node上的对象状态更新,以及获取其他对象的更新。

例如我们通过下面这个.yaml文件创建一个很简单的Pod

1
2
3
4
5
6
7
8
yaml复制代码apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: container-1
image: nginx

在成功创建这个Pod之后,Kubelet会定期监控container-1容器的健康状态,以确保它能够正常运行;并且会定期向Master报告节点的资源使用情况,如果容器崩溃或者终止,kubelet会尝试重启它,以确保Pod中的容器处于预期状态。

此外,如果想要做到更细粒度的资源监控,比如Deployment、Pod、容器级别,那么单单靠Kubelet是不行的,可以通过访问API-Server,或者使用其他工具进行数据上报,比如Prometheus、Grafana等等。

kube-proxy

kube-proxy是集群每个Node上运行的网络代理,以确保Pod之间的通信,以及Pod和集群外部的通信是正常的。

它会维护节点上的一些网络规则,这些规则会允许从集群内部或者外部的网络与Pod进行通信,以便将流量路由到正确的Pod。

如果操作系统提供了可用的数据包过滤层,则kube-proxy会通过它来实现网络规则。否则,kube-proxy只做流量转发的功能。

  • 服务代理:充当了服务和后端Pod之间的中介,接受流量并传递给对一个的Pod,客户端只需要知道服务的名称或者ClusterIP,而不需要知道后端Pod的详细信息。
  • 负载均衡:根据不同的负载均衡策略将请求分发给对应的后端Pod。
  • 集群外部访问:可以将外部流量路由到集群内部的服务。

Container Runtime

container-Runtime组件使得K8S拥有容器运行的环境和条件,从而能够有效运行容器。它负责管理K8S环境中容器的执行和生命周期。

K8S支持许多容器运行环境,如containerd、CRI-O以及Kuberenetes CRI等等。

K8S早期版本仅仅适用于Docker Engine这一种特定的容器运行时,后来增加了对其他类型的容器运行时的接口,因此设置了CRI标准接口来和各种各样的容器运行时进行交互,它包括了容器运行时的接口和镜像的接口,我们可以看看这些接口的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码service RuntimeService {  //Runtime的grpc接口 
rpc Version(VersionRequest) returns (VersionResponse) {}
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}
...
}
1
2
3
4
5
6
7
scss复制代码service ImageService { //镜像的grpc接口 
rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
}

上面的这些接口都是容器运行时需要暴露给Kubelet的接口,Docker Engine没有实现CRI接口,所以当K8S创建了CRI标准之后,手动创建了部分特殊代码来帮助Docker Engine过度,也就是dockershim。

K8S在v1.20版本宣布移除dockershim,v1.24版本K8S正式移除了dockerShim,为什么要这么做呢?

从可拓展性的角度看,K8S通过引入新的容器运行时接口将容器管理与具体的运行时解耦,不再依赖某个具体的运行时实现。另外,Docker也不打算支持K8S中的CRI接口,需要K8S社区在仓库中维护Dockershim。此外,在较新的 CRI 运行时中实现了与 dockershim 不兼容的功能,例如 cgroups v2 和用户命名空间。 从 Kubernetes 中移除 dockershim 允许在这些领域进行进一步的开发。

如何实现K8S的高可用

工作结点:因为scheduler在调度pod的时候有自动调动策略,通常如果某个node失效,会自动将pod部署在其他node上,所以node结点我们无需额外处理,只要集群拥有足够多的node结点,运行情况就会很稳定

api-server、scheduler:负责接受请求 是一个无状态的服务,所以我们可以起多个api-server实例,然后通过负载均衡器让请求均匀的打到所有的api-server上去,既能提高可用性,也能降低单个api-server的压力,提升请求处理速度

ectd、kube-controller-manager:可部署多个实例,它们内部会选出主节点

结语

《每天十分钟,轻松入门K8S》的第二篇02.K8S架构详解到这里就结束了,感谢您看到这里。

在下一篇中我会带着大家搭建K8S环境,真正开始K8S的具体操作,感兴趣的小伙伴欢迎点赞、评论、收藏,您的支持就是对我最大的鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

工作六年,我学会了用 Arthas 来辅助我的日常工作

发表于 2023-10-21

工作六年,我学会了用 Arthas 来辅助我的日常工作

很久就想写一篇介绍 Arthas 的文章,虽然 Arthas 已有大量文章介绍;但我依然想结合我的实际工作,来聊聊这款我喜爱的 Java 监控诊断产品。

🔊一位 Java 开发者的使用总结,只谈使用经验,不聊原理。

📆 那些辛酸的过往

历历在目的场景🥹(❁´◡❁)(❁´◡❁)

  • 客户线上问题,应该如何复现,让客户再点一下吗?
  • 异常被吃掉,手足无措,看是哪个家伙写的,竟然是自己!
  • 排查别人线上的 bug,不仅代码还没看懂,还没一行日志,捏了一把汗!
  • 预发 debug,稍微时间长点,群里就怨声载道!
  • 加日志重新部署,半个小时就没了,问题还没有找到,头顶的灯却早已照亮了整层楼……
  • 线上机器不能 debug,也不能开 debug 端口,重新部署会不会破坏现场呢?
  • 怀疑入参有问题,怀疑合并代码有问题,怀疑没有部署成功,全是问号……
  • 一个问题排查一天,被 Diss 排查问题慢……

直到我遇到了 Arthas,那些曾经束手无策的问题,都变得轻而易举。于是想把这些遇到的场景和用法做个总结。

📕一、通过命令执行方法–vmtool

vmtool 命令是我最喜欢用的,也是用最多的一个命令。通过这个命令执行方法,检查各种不符合预期的分支逻辑,入参出参,以及各种外部依赖接口,甚至还能修改数据等。

1.1 场景

解决过的场景 具体描述
发布导致线上的缓存 key 错误,需要清理,但过期时间还长,没有删除 key 的远程接口 通过执行 service 方法,删除缓存 key;另外读取 redis 中的 key 也极其方便
缺少日志,不知道上游是否返回数据合理 通过执行方法,确定依赖返回数据不正确
发布应用同时修改分布式配置,导致推送配置到该节点失败 通过执行方法,查询配置信息不是最新
常量值不符合预期,配置在 properties 中的免登 url 失效 通过执行方法,查询当前常量值,判断读取不合理
新增配置信息、删除脏数据 通过接口执行方法,添加了配置、删除了脏数据
集群环境,想要请求打在指定机器上查看日志 需要反复请求多次才能命中特定机器查看日志,通过vmtool 执行方法,快速实现日志查看
出参入参不符合预期 在调用链路上执行所有可疑方法
以前需要写 controller 调用触发的测试方法 直接用这个命令,减少代码,还能测试上下游的各种二方接口,十分方便

案例还有很多很多,因为真的可以拿着入参尽情的 invoke

提升了排查问题、解决问题的效率,也帮助其他人解决他们的问题。不再依赖打印大量日志反复部署服务,也不再申请 debug 端口进行远程 debug ,因为确实方便。

1.2 使用

工欲善其事必先利其器,我在 IDEA 装上一个 Arthas 插件,用它来快速复制命令,想执行哪个方法拷贝即可。
image.png

上图是使用 Arthas 插件生成执行命令。光标放在执行方法上右击选择 vmtool 即可得到可运行命令。

情景一: 执行的方法是对象:需要对参数对象赋值,以下图中的方法为例:

image.png

queryIBCcContactConfig 方法参数是对象,
首先通过 Arthas 工具查找到参数 IbCcContactConfigParam 的classLoaderHash, 如下命令:(sc -d 路径)

image.png

对参数对象进行字段赋值,方式参考下面加粗部分(ognl方式):

vmtool -x 3 -c 76707e36 –action getInstances –className com.xxx.impl.IbCcContactConfigServiceImpl –express ‘instances[0].queryIbCcContactConfig((#demo=new com.xxx.param.IbCcContactConfigParam(), #demo.setContactBizCode(‘12345L’),#demo))’

情景二、基础类型,比如String、Long、Int。直接填充数值即可

举例 语句 描述
基础类型,比如String、Int、Long类型等 vmtool -x 3 –action getInstances –className com.xxl.mq.admin.service.IXxlMqMessageService –express ‘instances[0].delete(0)’ 执行 IXxlMqMessageService#delete 方法,参数为0
参数 解释
-x 3 返回参数展开形式的,默认1,如果返回体是对象,建议设置3,方便观察返回结果
-c xxx 指定classLoaderHash,如果不指定,new 方法报错

1.3 限制

其一、尽量避开 ThreadLocal 。执行线程没有 ThreadLocal 的上下文;

其二、只能有一个端口,只支持一个arthas-server,用完及时关掉。

1.4 扩展

使用 getstatic 命令查看静态变量

场景描述 语句执行 解释
查看静态变量的实际值 getstatic com.xxx.xxx.interceptor.env.EnvIsolationInterceptor FILL_FIELD_NAME -x 3 查看 EnvIsolationInterceptor # FILL_FIELD_NAME 的静态变量值
配置application.properties的免登 uri,发现没有生效 getstatic com.xxx.sso.filter.InitParamContext excludeList -x 3 查看自己的免登 uri 是否在集合里面,从而快速定位问题
修改静态变量值 ognl -x 3 ‘#field=@com.xxl.mq.admin.conf.XxlMqBrokerImpl@class.getDeclaredField(“ENV”),#field.setAccessible(true),#field.set(null,” “)’ image.png

🖥️二、热部署 # redefine && retransform

😭拍桌子拍大腿感叹发布的的代码少写或者漏写;拍脑门惋惜为啥不多打一行日志;口吐芬芳为什么把判断条件写死……,那些只能发布才能调试、部署一次要半小时的应用,真的会让生命变得廉价。

2.1 场景

解决过的场景 描述
加日志语句,入参出参观察 联调查看参数
将判断条件恒定成了 false,目标分支无法执行,阻塞进度 修改判断逻辑
漏写一行赋值代码 对象自己赋值给自己,字段值为NULL
研发、联调阶段,代码验证 需要反复修改代码验证
测试同学提Bug及时修复验证 快速修复问题,不影响测试进度

热部署的优势用过的都说好👍。

2.2 使用

IDEA 集成 ArthasHotSwap 插件,方便快捷:

image.png

很多公司通过 API 方式已经集成了工具,定制化更好用😄。

2.3 限制

  • 不允许新增加 field/method
  • 正在跑的函数,没有退出不能生效
  • redefine 和 watch/trace/jad/tt 等命令冲突

热部署能力,是一个很强大的能力,线上谨慎使用,属于高危操作。

📑三、OGNL && 条件过滤

顶流功能,可以使用 OGNL 解决很多复杂场景,其条件过滤属于绝佳。 适用于 watch、trace、stack、monitor等,有大量请求、 for 场景等。

image.png

3.1 场景

条件过滤的适用场景实在太多,简单举例

解决过的场景 描述
想拦截特定参数值的方法入参出参,过滤其他参数请求 只有特定参数才会被拦截,否则跳过,不影响其他人使用
拦截特定参数,这个参数方法调用耗时长 排查到了参数异常情况,特定账号数据量太大
指定账号登录异常 通过监控指定 userId 的调用栈,排查问题

3.2 使用

条件判断形式:形如 params[0] == “orgIdxxx726” (OGNL)

场景 案例 描述
watch 命令:只监控特定组织的数据信息 watch com.xxx.controller.OrgServiceController getOrgInfo ‘{params,returnObj,throwExp}’ ‘params[0] == “orgIdxxx726”‘ -n 5 -x 3 通过“ ‘params[0] == “orgIdxxx726“‘”命令,判断只有当参数为“orgIdxxx726” watch 才生效
trace 命令:只有特定组织的数据比较消耗时间 trace com.xxx.controller.OrgServiceController getOrgInfo ‘params[0] == “orgIdxxx726”‘ -n 5 –skipJDKMethod false 查询只要特定组织耗时长,忽略其他参数,精准定位
stack 命令:查看调用栈,非常适合代理调用、AOP、Filter等 stack com.xxx.controller.OrgServiceController getOrgInfo ‘params[0] == “orgIdxxx726”‘ -n 5 排查调用链路

更多使用条件判断的案例如下

1
2
3
4
5
6
7
8
java复制代码-- 判空
trace com.xxx.controller.OrgServiceController getOrgInfo'params[0] != null'

-- 等于
trace *com.xxx.controller.OrgServiceController getOrgInfo 'params[0] == 1L'

-- 字符串不等于
trace com.xxx.controller.OrgServiceController getOrgInfo 'params[1] != "AA"'

OGNL 可以组合各种形式的条件判断。非常适合 watch、trace、stack 等场景。

3.3 扩展

更多灵活的用法

ognl | arthas

🔖 四、其他命令汇总

4.1 logger

在写代码的时候,也可以刻意加 log.debug 级别的日志。日志级别一般为 info ,当需要排查问题的时候,可以修改日志级别为 debug。

解决过的场景 描述
自定义日志失效,排查日志的实现类由哪个包引入或者提供 排查间接引入的日志依赖包
改变当前类的日志级别,查看日志 将 info 级别修改成 debug 级别
1
2
3
4
5
css复制代码logger -n com.xxx.controller.OrgServiceControlle

通过sc 查看这个类的claasLoaderHash;

logger --name ROOT --level debug -c 4839ebd

4.2 monitor

监控某个方法的调用次数。包括调用次数,平均RT、成功率等信息。在性能调优使用:

1
arduino复制代码monitor com.XXXX.handler.HandlerManager process  -n 10  --cycle 10

4.3 thread

排查线程死锁,以及线程状态:

语句 详细
thread -b 排查死锁情况,注意,当 Arthas 不能加载的时候,还是继续使用原来的 top 命令那一套排查问题
thread -n 3 查询当前最忙的 N 个线程

4.4 jad 命令、反编译

场景 描述
排查 jar 中的 class 文件加载是否符合预期。比如:突然发现某一台机器上的执行结果和其他的机器的结果不一致。 怀疑机器部署异常
依赖包冲突,加载类不符合预期或者支合并的时候出错 检查运行的代码

案例:springBoot 应用遇到 NoSuchMethodError 等问题,可以使用 Jad 反编译确认,看一下加载的类是否有问题。

4.5 watch

watch 用来查看入参出参,配合 OGNL 条件过滤非常实用:

1
arduino复制代码watch com.xxl.mq.admin.service.IXxlMqMessageService pageList '{params,returnObj,throwExp}'  -n 5  -x 4

image.png

条件判断 #cost>200(单位ms) 表示只有当耗时大于200ms才输出:

1
arduino复制代码watch demo... primeFactors '{params, returnObj}' '#cost>200' -x 2

如果 x 设置超过 4 也就只展示4。使用 x=4 的情况比较常见,因为展开的信息最多。但需要注意线上数据量太大情况。

4.6 tt (Time Tunnel)

tt 可以实现重做,实现方法调用;个人比较喜欢 vmtool,不过多介绍, tt 功能也十分强大。

4.7 用得不多的命令

下面几个命令个人用得少,但很重要:

命令 描述
memory 查看内存信息
options 全局开关,jvm 比较高级少用的命令
sysprop / sysenv 当前 JVM 的系统属性,环境属性
profiler 火焰图
dashboard 实时数据面板

其他命令就不再赘述了📎。

4.8 Arthas 插件功能

Arthas 插件生成的命令如下:

image.png

注:图片来源于 arthas 插件作者,插件和文章都很好🌺

针对插件的热部署配置详见: (Hot Swap) Redefine 热更新支持); 个人更推荐热部署用 ArthasHotSwap 插件。

📌五、一些限制 && 注意事项

  • 执行trace / tt 等命令时,本质上是在 method 的前后插入代码,会影响原来 JVM 里面 JIT 编译生成的代码。可能执行并发高的函数有抖动;
  • 只能有一个端口,只支持一个 arthas-server;
  • 热部署有限制且不一定能成功,线上属于高危操作;
  • 如果服务不能响应,可能 Arthas 不能启动使用,需要使用 Linux 相关命令排查问题。

📇六、好文推荐

  1. 官网文档
  2. 个人最推荐的学习资料: arthas idea plugin手册

🧣七、最后的话

🖲要成为 Arthas 使用的好手,一定多多练习:纸上得来终觉浅,绝知此事要躬行。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…717273…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%