Eric 发布的文章

Linux 文件系统学习摘要

这是公司内学习 Linux Kernel 的第13章, 关于文件系统的部份.

inode

In Linux and other Unix-like operating systems, an inode (index node) is a data structure that stores information about a file or directory except its name and its actual data. Each file or directory has an inode that contains important metadata about the file or directory.

The information stored in an inode includes:

  1. File size
  2. Device ID
  3. User ID (owner)
  4. Group ID
  5. File permissions
  6. File creation, modification, and access times
  7. Number of links (how many file names point to this inode)
  8. Pointers to the disk blocks that store the file's data

The inode number is a unique identifier for the inode within the filesystem. You can view the inode information of a file or directory using the ls -i or stat command in the terminal.

dentry

In the Linux kernel, a dentry (directory entry) is a data structure that represents a specific inode in the cache. It's a key component of the Virtual File System (VFS) layer, which provides a common interface for all file systems.

The dentry object contains information about the inode, the file name, and pointers to the parent and child dentries, forming a dentry tree that represents the directory hierarchy. This structure allows the kernel to quickly look up files and directories, improving the efficiency of file system operations.

Dentries are stored in a dentry cache (dcache), which keeps track of recently accessed dentries to speed up subsequent file and directory lookups. When a file is accessed, the kernel first checks the dcache. If the dentry is found, the kernel can access the file directly without having to traverse the entire file system, which can significantly improve performance.

stat 命令

stat 命令显示文件, 路径, 文件系统信息. 具体看下面例子.

# 显示 /tmp 目录所在的文件系统信息
$ stat -f /tmp/
File: "/tmp/"
    ID: 7ae4c0a51947813 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 122221576  Free: 73122076   Available: 66895184
Inodes: Total: 31121408   Free: 28709496

例子

当我们新建一个空文件和一个空目录的时候, 可以看到如下:

supra@suprabox:~/Downloads$ touch x
supra@suprabox:~/Downloads$ stat x
  File: x
  Size: 0             Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d    Inode: 10748017    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/   supra)   Gid: ( 1000/   supra)
Access: 2023-12-01 21:05:46.177415933 -0800
Modify: 2023-12-01 21:05:46.177415933 -0800
Change: 2023-12-01 21:05:46.177415933 -0800
 Birth: 2023-12-01 21:05:46.177415933 -0800
supra@suprabox:~/Downloads$ mkdir y
supra@suprabox:~/Downloads$ stat y
  File: y
  Size: 4096          Blocks: 8          IO Block: 4096   directory
Device: fd01h/64769d    Inode: 11153180    Links: 2
Access: (0775/drwxrwxr-x)  Uid: ( 1000/   supra)   Gid: ( 1000/   supra)
Access: 2023-12-01 21:06:28.061716185 -0800
Modify: 2023-12-01 21:06:28.061716185 -0800
Change: 2023-12-01 21:06:28.061716185 -0800
 Birth: 2023-12-01 21:06:28.061716185 -0800

下面是对x这个空文件输出的详细解释:

  1. File: x:文件名是x。
  2. Size: 0:文件大小是0字节,因为你刚创建了这个文件但还没有写入任何内容。
  3. Blocks: 0:文件占用的块数是0,这与文件大小为0是一致的。
  4. IO Block: 4096:文件系统的I/O块大小是4096字节。
  5. regular empty file:这是一个常规的空文件。
  6. Device: fd01h/64769d:文件所在的设备的设备号。
  7. Inode: 10748017:文件的inode号是10748017。
  8. Links: 1:硬链接数是1,表示只有一个文件名指向这个inode。
  9. Access: (0664/-rw-rw-r--):文件的权限是0664,也就是用户(owner)和组(group)有读写权限,其他人(other)只有读权限。
  10. Uid: ( 1000/ supra):文件的所有者的用户ID是1000,用户名是supra。
  11. Gid: ( 1000/ supra):文件的所有者的组ID是1000,组名是supra。
  12. Access: 2023-12-01 21:05:46.177415933 -0800:文件最后一次被访问的时间。
  13. Modify: 2023-12-01 21:05:46.177415933 -0800:文件最后一次被修改的时间。
  14. Change: 2023-12-01 21:05:46.177415933 -0800:文件状态最后一次被改变的时间。
  15. Birth: 2023-12-01 21:05:46.177415933 -0800:文件的创建时间。

Java 为什么我要的锁不见了

线上遇到问题: 有些 Tomcat 线程卡住了, 卡住的越来越多, 重启虽然能暂时解决, 但不是长期解决办法, 如下图:
TomcatBusyThread.png

确定卡住的线程

随机找一个症状还在的服务器, 获取 thread dump, 看到如下卡住的线程 (截取部分):

"MyTaskExecutor-127" #407 daemon prio=5 os_prio=0 tid=0x00007ff6d0019000 nid=0x1da waiting on condition [0x00007ff4159d7000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x000000075e438328> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:837)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:872)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1202)
    at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:213)
    at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:290)
    at sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:951)
    at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
    at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)
    at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
    - locked <0x000000075e5b2488> (a java.io.BufferedInputStream)
    at sun.net.www.MeteredStream.read(MeteredStream.java:134)
    - locked <0x000000075e5b4400> (a sun.net.www.http.KeepAliveStream)
    at java.io.FilterInputStream.read(FilterInputStream.java:133)
    at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(HttpURLConnection.java:3471)

可以看出这是一个使用 HTTPS 访问外部请求的操作, 现在卡在了 SSLSocketImpl$AppInputStream.read() 上面, 现在它需要一把锁.

环境信息

Open JDK 1.8.362. 为什么要强调 JDK 版本, 后面会看到JDK涉及到这块的代码改动量非常大, 每个版本代码都不一样.

初步分析

一开始认为是没有设置 read timeout, 导致一直死等. 但是看了应用程序配置, 发现是设置的, 查看heapd dump 里面, 却是也是设置的. 如下图:
timeout.png

为什么设置了 connect 和 read timeout 还死等

根据这个栈可以看出, 连接已经建立(新建或者使用KeepLiveCache里面的), 所以, connect timeout 阶段已经过了, 不管用了.
同时, read time 是在使用 poll() 或者在它的外层 c代码 Java_java_net_SocketInputStream 里面才会计算 read timeout, 所以这里还没到.

这个线程等的锁被谁占用

通过 thread dump 很容易可以看到, 这个锁被下面的线程占着.

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007ff8508d3000 nid=0x26 in Object.wait() [0x00007ff80f427000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007570016f8> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
    - locked <0x00000007570016f8> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:188)

   Locked ownable synchronizers:
    - <0x000000075e438328> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075e438410> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075e613df8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075e6682c0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075e6a3e90> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075e6b4070> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075e6c51f8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075ee84098> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000075f636998> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x00000007610a3e08> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x0000000766af21d0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x00000007759c8fd8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000078c1bed50> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000078d6fb888> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000078e2b8ff0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000078f448fc8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000078f592e50> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000078f5a9430> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    - <0x000000078f5bed40> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

从上面的信息中看到, 这个Finalizer 线程的 Locked ownable synchronizers部分看, 它占有了很多锁. 其中第一个就是我们之前线程正想要获取的锁.

那么这个 Finalizer 在干嘛?

从上面的栈信息, 结合具体的源代码, 可以看到这个 Finalizer 线程其实在等下一个需要 finalize() 方法的对象. 并且当前没有在排队的对象(从heap dump)可以看到:
lock.png

矛盾的现象

这个线程拥有了这把锁(其实它拥有很多把锁, 从上面Locked ownable synchronizers可以看到), 却没有在使用这把锁做事情, 反而现在没有任何事情在做. 那么它就没有可能释放这把锁. 也就是说, 它曾经获得了这把锁, 但是没办法释放这把锁.

那么任何在等这把锁的对象, 都面临着永远等不来的情况.

为什么会造成这种情况

从现在的数据来看, 这种情况发生的几率很小, 没几天才能发生一次. 从已有的数据看, 很有可能是 Finalizer 线程在执行 sun.security.ssl.SSLSocketImplfinalize() 方法的时候, 获得了这把锁, 然而却没释放.

于是去查看这个版本的sun.security.ssl.SSLSocketImpl的源代码, 发现几乎每处使用这个锁的地方都是 try{}finally{} 方式, 在 finally{} 代码块里去释放的锁. 所以正常执行完不可能不释放.

唯一的可能性就是: finalize() 方法没有正常执行完. 在获得锁还没有释放锁的位置, finalize() 方法被中断了. 在JDK 里面, 根本不保证 finalize() 一定被执行, 什么时候被执行, 以及是不是执行完. 所以在 JDK 9 之后 finalize() 就被 deprecated 了.

思考

如果这个 sun.security.ssl.SSLSocketImpl 已经被开始执行 finalize() 方法, 那么它在某个时间点, 已经被 JVM GC 判定为不可达. 那么肯定有一种神秘的力量把它从死神哪里拉回来了. 并且现在正在被另外一个线程使用.
当一个 AbstractOwnableSynchronizer 的锁被一个线程使用的时候, 它会记录拥有锁的线程名字到它的 exclusiveOwnerThread 字段. 从heap dump, 我们可以证实这个锁也是被 Finalizer 拥有.
lock2.png
这里的线程是 Finalizer, state 是1, 表示这个 ReentrantLock$NonfairSync 被进入一次.

是哪个神秘的力量救活了它?

探索 Java URL 连接的 read timeout 如何实现的

使用最朴实的 java.net.HttpURLConnection 来获取网页数据, 设置 timeout 然后通过OS层面的 tracing 工具去获取代码栈. 运行于 Ubuntu 22.04 和 OpenJDK 11. 由于JDK 11 使用了系统调用 poll 去获取网络事件, 然后使用了系统调用 recvfrom 来取的数据, 所以会拦截这2个系统调用. 另外可以看到下面使用perf 注入符号表, perf 和 bpftrace 都能看到 Java 编译的方法. 但是 strace 和 gdb 不行.
依次使用的工具:

  1. strace
  2. perf
  3. bpftrace
  4. gdb

java 代码

还是使用之前 chatGPT 给的这段代码. 注意这里设置的 connect timeoutread timeout 参数, 后面会看到.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class URLTest {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000; i++) {
            try {
                URL url = new URL("http://www.tianxiaohui.com");
                HttpURLConnection con = (HttpURLConnection) url.openConnection();
                con.setRequestMethod("GET");
                con.setConnectTimeout(997); // 连接超时时间 997ms
                con.setReadTimeout(719); // 读取超时时间 719ms
                System.out.println(i + "Response code: " + con.getResponseCode());
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String line;
                StringBuilder response = new StringBuilder();
    
                while ((line = in.readLine()) != null) {
                    response.append(line);
                }
                in.close();
                //System.out.println(response.toString());
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println(e.getMessage());
            }
            Thread.sleep(2000);
        }
    }
}

准备工作

运行程序使用以下代码:

$ javac URLTest.java
$ java -XX:+PreserveFramePointer -XX:-TieredCompilation -XX:CompileThreshold=1 URLTest

生成JIT 编译的代码符号表. 参看: bpftrace 探测 Java 运行时栈-实践

strace

直接对这个java程序使用 trace pollrecvfrom event. 可以看到 poll 系统调用的timeout参数是997和719. 分别来自于JDK里面的C 代码 Java_java_net_PlainSocketImpl_socketConnectJava_java_net_SocketInputStream_socketRead0

$ sudo strace --stack-trace -f -e trace=poll  -p 1097556
strace: Process 1097556 attached with 18 threads
[pid 1097557] poll([{fd=5, events=POLLOUT}], 1, 997 <unfinished ...>
[pid 1097670] +++ exited with 0 +++
[pid 1097557] <... poll resumed>)       = 1 ([{fd=5, revents=POLLOUT}])
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__poll+0x4f) [0x118dbf]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(NET_Poll+0xab) [0xff2b]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(Java_java_net_PlainSocketImpl_socketConnect+0x1ec) [0xd24c]
 > unexpected_backtracing_error [0x7f93742cbd95]
[pid 1097557] poll([{fd=5, events=POLLIN|POLLERR}], 1, 719) = 1 ([{fd=5, revents=POLLIN}])
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__poll+0x4f) [0x118dbf]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(NET_Timeout+0xed) [0x1019d]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(Java_java_net_SocketInputStream_socketRead0+0xdf) [0xe75f]
 > unexpected_backtracing_error [0x7f93742daa34]


$ sudo strace --stack-trace -f -e trace=recvfrom  -p 1097556
strace: Process 1097556 attached with 18 threads
[pid 1097557] recvfrom(5, "HTTP/1.1 301 Moved Permanently\r\n"..., 8192, MSG_DONTWAIT, NULL, NULL) = 242
 > /usr/lib/x86_64-linux-gnu/libc.so.6(recv+0x6e) [0x1278ae]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(NET_NonBlockingRead+0xb0) [0xf1e0]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(Java_java_net_SocketInputStream_socketRead0+0xf9) [0xe779]
 > unexpected_backtracing_error [0x7f93742daa34]

perf

使用 perf 要先记录数据, 然后生成命令行report, 进入单个结果, 就能看到完整的栈. 这个栈是从上往下的, 最新的在下面.

$ sudo perf record -p 1097556  -e "syscalls:sys_enter_poll" -ag
$ ls -lah perf.data
$ sudo perf report 
# 选择一个sample, enter 键进入detail 如下
Samples: 8  of event 'syscalls:sys_enter_poll', Event count (approx.): 8
  Children      Self  Trace output
-   37.50%    37.50%  ufds: 0x7f937753c438, nfds: 0x00000001, timeout_msecs: 0x000002cf                                                                                                 ▒
     start_thread                                                                                                                                                                       ▒
     ThreadJavaMain                                                                                                                                                                     ▒
     JavaMain                                                                                                                                                                           ▒
     jni_CallStaticVoidMethod                                                                                                                                                           ▒
     jni_invoke_static                                                                                                                                                                  ▒
     JavaCalls::call_helper                                                                                                                                                             ▒
     call_stub                                                                                                                                                                          ▒
     Interpreter                                                                                                                                                                        ▒
     Ljava/net/HttpURLConnection;::getResponseCode                                                                                                                                      ▒
     Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream                                                                                                                      ▒
     Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0                                                                                                                     ▒
     Lsun/net/www/http/HttpClient;::parseHTTP                                                                                                                                           ▒
     Lsun/net/www/http/HttpClient;::parseHTTPHeader                                                                                                                                     ▒
     Ljava/io/BufferedInputStream;::read                                                                                                                                                ▒
     Ljava/io/BufferedInputStream;::read1                                                                                                                                               ▒
     Ljava/io/BufferedInputStream;::fill                                                                                                                                                ▒
     Ljava/net/SocketInputStream;::read                                                                                                                                                 ▒
     Ljava/net/SocketInputStream;::socketRead0                                                                                                                                          ▒
     Java_java_net_SocketInputStream_socketRead0                                                                                                                                        ▒
     __poll                                                                                                                                                                             ▒
+   25.00%    25.00%  ufds: 0x7f937754c378, nfds: 0x00000001, timeout_msecs: 0x000003e5                                                                                                 ▒
+   12.50%    12.50%  ufds: 0x7f937754a888, nfds: 0x00000001, timeout_msecs: 0x00000000                                                                                                 ▒
+   12.50%    12.50%  ufds: 0x7f937754a888, nfds: 0x00000001, timeout_msecs: 0x00001385                                                                                                 ▒
+   12.50%    12.50%  ufds: 0x7f937754a888, nfds: 0x00000001, timeout_msecs: 0x00001388
$ sudo perf record -p 1097556  -e "syscalls:sys_enter_recvfrom" -ag
$ sudo perf report 
Samples: 2  of event 'syscalls:sys_enter_recvfrom', Event count (approx.): 2
  Children      Self  Trace output
-  100.00%   100.00%  fd: 0x00000005, ubuf: 0x7f937753c4c0, size: 0x00002000, flags: 0x00000040, addr: 0x00000000, addr_len: 0x00000000                                                 ◆
     start_thread                                                                                                                                                                       ▒
     ThreadJavaMain                                                                                                                                                                     ▒
     JavaMain                                                                                                                                                                           ▒
     jni_CallStaticVoidMethod                                                                                                                                                           ▒
     jni_invoke_static                                                                                                                                                                  ▒
     JavaCalls::call_helper                                                                                                                                                             ▒
     call_stub                                                                                                                                                                          ▒
     Interpreter                                                                                                                                                                        ▒
     Ljava/net/HttpURLConnection;::getResponseCode                                                                                                                                      ▒
     Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream                                                                                                                      ▒
     Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0                                                                                                                     ▒
     Lsun/net/www/http/HttpClient;::parseHTTP                                                                                                                                           ▒
     Lsun/net/www/http/HttpClient;::parseHTTPHeader                                                                                                                                     ▒
     Ljava/io/BufferedInputStream;::read                                                                                                                                                ▒
     Ljava/io/BufferedInputStream;::read1                                                                                                                                               ▒
     Ljava/io/BufferedInputStream;::fill                                                                                                                                                ▒
     Ljava/net/SocketInputStream;::read                                                                                                                                                 ▒
     Ljava/net/SocketInputStream;::socketRead0                                                                                                                                          ▒
     Java_java_net_SocketInputStream_socketRead0                                                                                                                                        ▒
     __libc_recv

bpftrace

使用 bpftrace 给出的 event 获取用户态的栈.

$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_poll /pid==1097556/{ @[ustack(20)] = count(); }'
Attaching 1 probe...
^C

@[
    poll+79
    Java_java_net_SocketInputStream_socketRead0+223
    Ljava/net/SocketInputStream;::socketRead0+244
    Ljava/net/SocketInputStream;::read+224
    Ljava/io/BufferedInputStream;::fill+784
    Ljava/io/BufferedInputStream;::read1+176
    Ljava/io/BufferedInputStream;::read+252
    Lsun/net/www/http/HttpClient;::parseHTTPHeader+444
    Lsun/net/www/http/HttpClient;::parseHTTP+1004
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+1420
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+196
    Ljava/net/HttpURLConnection;::getResponseCode+96
    Interpreter+4352
    call_stub+138
    JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+883
    jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*) [clone .constprop.1]+682
    jni_CallStaticVoidMethod+352
    JavaMain+3441
    ThreadJavaMain+13
    start_thread+755
]: 1
$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_recvfrom /pid==1097556/{ @[ustack(20)] = count(); }'
Attaching 1 probe...
^C

@[
    recvfrom+116
]: 2
@[
    __GI___recv+110
    Java_java_net_SocketInputStream_socketRead0+249
    Ljava/net/SocketInputStream;::socketRead0+244
    Ljava/net/SocketInputStream;::read+224
    Ljava/io/BufferedInputStream;::fill+784
    Ljava/io/BufferedInputStream;::read1+176
    Ljava/io/BufferedInputStream;::read+252
    Lsun/net/www/http/HttpClient;::parseHTTPHeader+444
    Lsun/net/www/http/HttpClient;::parseHTTP+1004
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+1420
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+196
    Ljava/net/HttpURLConnection;::getResponseCode+96
    Interpreter+4352
    call_stub+138
    JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+883
    jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*) [clone .constprop.1]+682
    jni_CallStaticVoidMethod+352
    JavaMain+3441
    ThreadJavaMain+13
    start_thread+755
]: 4

gdb

使用gdb连接, 然后通过设置段点来找到我们期望的栈. 这个看到的更多一些, 包括从 Java_java_net_SocketInputStream_socketRead0 到 glibc 的 __GI___poll 中间的2步: NET_ReadWithTimeoutNET_Timeout.

# gdb 连接
$ sudo gdb --pid=1097556
# 找到我们需要的线程
$ thread 2
#设置断点, 先设置一个 socketRead的, 如果直接设置
$ break Java_java_net_SocketInputStream_socketRead0
$ cont # 到上面这个断点停止
$ bt # 查看是不是我们想要的栈
$ break __GI___poll
$ cont # 这样就会到我们想要的点
(gdb) break __GI___poll
Breakpoint 5 at 0x7f9378c6dd70: file ../sysdeps/unix/sysv/linux/poll.c, line 27.
(gdb) cont
Continuing.

Thread 2 "java" hit Breakpoint 5, __GI___poll (fds=fds@entry=0x7f937753c438, nfds=nfds@entry=1, timeout=719, timeout@entry=<error reading variable: That operation is not available on integers of more than 8 bytes.>) at ../sysdeps/unix/sysv/linux/poll.c:27
27    ../sysdeps/unix/sysv/linux/poll.c: No such file or directory.
(gdb) bt
#0  __GI___poll (fds=fds@entry=0x7f937753c438, nfds=nfds@entry=1, timeout=719,
    timeout@entry=<error reading variable: That operation is not available on integers of more than 8 bytes.>) at ../sysdeps/unix/sysv/linux/poll.c:27
#1  0x00007f937402419d in poll (__timeout=719, __nfds=1, __fds=0x7f937753c438) at /usr/include/x86_64-linux-gnu/bits/poll2.h:39
#2  NET_Timeout (env=env@entry=0x7f937001bb48, s=s@entry=5, timeout=<error reading variable: That operation is not available on integers of more than 8 bytes.>,
    nanoTimeStamp=nanoTimeStamp@entry=1643674031468882) at ./src/java.base/linux/native/libnet/linux_close.c:433
#3  0x00007f937402275f in NET_ReadWithTimeout (timeout=<optimized out>, len=8192, bufP=0x7f937753c4c0 "", fd=5, env=0x7f937001bb48)
    at ./src/java.base/unix/native/libnet/SocketInputStream.c:55
#4  Java_java_net_SocketInputStream_socketRead0 (env=0x7f937001bb48, this=<optimized out>, fdObj=<optimized out>, data=0x7f937754c518, off=0, len=8192, timeout=719)
    at ./src/java.base/unix/native/libnet/SocketInputStream.c:127
#5  0x00007f93742daa34 in ?? ()
#6  0x00000000000002cf in ?? ()

timeout 的实现

根据上面的栈信息, 我们很容易就知道了我们期望的 timeout 如何使用的:

  1. Java_java_net_SocketInputStream_socketRead0 - https://github.com/openjdk/jdk11/blob/37115c8ea4aff13a8148ee2b8832b20888a5d880/src/java.base/unix/native/libnet/SocketInputStream.c#L91C1-L91C44
  2. NET_ReadWithTimeout - https://github.com/openjdk/jdk11/blob/37115c8ea4aff13a8148ee2b8832b20888a5d880/src/java.base/unix/native/libnet/SocketInputStream.c#L50
  3. NET_Timeout - https://github.com/openjdk/jdk11/blob/master/src/java.base/linux/native/libnet/linux_close.c#L415
  4. poll - https://github.com/openjdk/jdk11/blob/master/src/java.base/linux/native/libnet/linux_close.c#L441

其中真正使用 timeout 地方在于 poll 和 Java_java_net_SocketInputStream_socketRead0 里面的循环.

如果改成 https

改成 https 连接 connect 的栈:

    poll+79
    Java_java_net_PlainSocketImpl_socketConnect+492
    Ljava/net/PlainSocketImpl;::socketConnect+213
    Ljava/net/AbstractPlainSocketImpl;::doConnect+328
    Ljava/net/AbstractPlainSocketImpl;::connect+444
    Ljava/net/SocksSocketImpl;::connect+2528
    Ljava/net/Socket;::connect+592
    Lsun/security/ssl/SSLSocketImpl;::connect+96
    Lsun/net/NetworkClient;::doConnect+432
    Lsun/net/www/http/HttpClient;::openServer+72
    Lsun/net/www/http/HttpClient;::openServer+348
    Lsun/net/www/protocol/https/HttpsClient;::<init>+972
    Lsun/net/www/protocol/https/HttpsClient;::New+1444
    Lsun/net/www/protocol/http/HttpURLConnection;::plainConnect0+3272
    Lsun/net/www/protocol/http/HttpURLConnection;::plainConnect+256
    Lsun/net/www/protocol/https/AbstractDelegateHttpsURLConnection;::connect+100
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+1996
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+192
    Ljava/net/HttpURLConnection;::getResponseCode+116
    LURLTest;::main+660

改成 https 连接 read 的栈:

    poll+79
    Java_java_net_SocketInputStream_socketRead0+223
    Ljava/net/SocketInputStream;::socketRead0+244
    Ljava/net/SocketInputStream;::read+204
    Lsun/security/ssl/SSLSocketInputRecord;::read+52
    Lsun/security/ssl/SSLSocketInputRecord;::readHeader+144
    Lsun/security/ssl/SSLSocketInputRecord;::bytesInCompletePacket+72
    Lsun/security/ssl/SSLSocketImpl;::readApplicationRecord+348
    Lsun/security/ssl/SSLSocketImpl$AppInputStream;::read+552
    Ljava/io/BufferedInputStream;::fill+760
    Ljava/io/BufferedInputStream;::read+676
    Lsun/net/www/http/ChunkedInputStream;::fastRead+116
    Lsun/net/www/http/ChunkedInputStream;::read+584
    Lsun/net/www/protocol/http/HttpURLConnection$HttpInputStream;::read+116
    Lsun/nio/cs/StreamDecoder;::readBytes+444
    Lsun/nio/cs/StreamDecoder;::implRead+392
    Lsun/nio/cs/StreamDecoder;::read+432
    Ljava/io/BufferedReader;::fill+724
    Ljava/io/BufferedReader;::readLine+1132
    LURLTest;::main+304

Java 里面的 connect timeout 的实现

在如今的微服务应用中, 有很多的服务之间相互调用. 在服务调用的时候, 我们都会设置 connect timeoutread timeout, 那么这个 connect timeout 的逻辑是什么样子的? JDK 里面到底是怎么实现的?

下面我们以一个简单的URL连接的例子, 来看这个 connect timeout 是如何工作的.

连接例子

下面是我们用来演示的代码, 它的意图就是不断的循环去获得某个网页. 不断循环的目的是为了给我们的手工操作留有足够的时间. 这段代码有网络操作, 所以会调用系统调用 connect 来拿到响应(response), 对应 bpftrace 里面的 tracepoint:syscalls:sys_enter_connect.(感谢 chatGPT 给我们演示代码)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class URLTest {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000; i++) {
            try {
                URL url = new URL("http://www.tianxiaohui.com");
                HttpURLConnection con = (HttpURLConnection) url.openConnection();
                con.setRequestMethod("GET");
                con.setConnectTimeout(5000); // 连接超时时间 5000ms
                con.setReadTimeout(10000); // 读取超时时间 10000ms
                System.out.println(i + "Response code: " + con.getResponseCode());
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String line;
                StringBuilder response = new StringBuilder();
    
                while ((line = in.readLine()) != null) {
                    response.append(line);
                }
                in.close();
                //System.out.println(response.toString());
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println(e.getMessage());
            }
            Thread.sleep(2000);
        }
    }
}

获取调用栈

我们可以通过拦截系统调用 connect 的时候, 去获取调用栈, 然后依次查看调用栈里面的方法, 来确认哪里使用了 timeout, 啥如何使用的.

通过 bpftrace 获取的 connect 调用栈

$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_connect /pid==288407/ { @[ustack(20)] = count(); }'
Attaching 1 probe...
^C

@[
    __connect+75
    Java_java_net_PlainSocketImpl_socketConnect+687
    Ljava/net/PlainSocketImpl;::socketConnect+213
    Ljava/net/AbstractPlainSocketImpl;::doConnect+332
    Ljava/net/AbstractPlainSocketImpl;::connect+444
    Ljava/net/Socket;::connect+596
    Lsun/net/NetworkClient;::doConnect+1208
    Lsun/net/www/http/HttpClient;::openServer+72
    Lsun/net/www/http/HttpClient;::openServer+332
    Lsun/net/www/http/HttpClient;::<init>+924
    Lsun/net/www/http/HttpClient;::New+1284
    Lsun/net/www/protocol/http/HttpURLConnection;::plainConnect0+3156
    Lsun/net/www/protocol/http/HttpURLConnection;::plainConnect+260
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+2000
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+196
    Ljava/net/HttpURLConnection;::getResponseCode+96
    Interpreter+4352
    call_stub+138
    JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+883
    jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*) [clone .constprop.1]+682
]: 1

通过 strace 获取的调用栈

$ sudo strace --stack-trace  -f -e trace=connect  -p  288407
[pid 288408] connect(5, {sa_family=AF_INET6, sin6_port=htons(80), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:23.222.245.123", &sin6_addr), sin6_scope_id=0}, 28) = 0
 > /usr/lib/x86_64-linux-gnu/libc.so.6(__connect+0x4b) [0x12771b]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(NET_Connect+0xaa) [0xfd0a]
 > /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(Java_java_net_PlainSocketImpl_socketConnect+0x2af) [0xd30f]
 > unexpected_backtracing_error [0x7fd780780995]

哪里使用了 timeout

根据 bpftrace 给出的调用栈, 我们依次排查每个方法, 发现从 Ljava/net/Socket;::connect 开始, 就获取了当前设置的 connect timeout 值, 然后依次向下传递, 直到 JDK 里面的 C 代码 Java_java_net_PlainSocketImpl_socketConnect. 真正使用 timeout 的地方, 就在这个方法里面. 代码如下 (JDK11) https://github.com/openjdk/jdk11/blob/37115c8ea4aff13a8148ee2b8832b20888a5d880/src/java.base/unix/native/libnet/PlainSocketImpl.c#L344-L372

while (1) {
    jlong newNanoTime;
    struct pollfd pfd;
    pfd.fd = fd;
    pfd.events = POLLOUT;

    errno = 0;
    connect_rv = NET_Poll(&pfd, 1, nanoTimeout / NET_NSEC_PER_MSEC);

    if (connect_rv >= 0) {
        break;
    }
    if (errno != EINTR) {
        break;
    }

    /*
        * The poll was interrupted so adjust timeout and
        * restart
        */
    newNanoTime = JVM_NanoTime(env, 0);
    nanoTimeout -= (newNanoTime - prevNanoTime);
    if (nanoTimeout < NET_NSEC_PER_MSEC) {
        connect_rv = 0;
        break;
    }
    prevNanoTime = newNanoTime;

} /* while */

这是一个循环, 只有满足某些条件, 才会跳出. 涉及到 timeout 的地方, 有2处, 一处是 NET_Poll, 它的定义在: https://github.com/openjdk/jdk11/blob/37115c8ea4aff13a8148ee2b8832b20888a5d880/src/java.base/linux/native/libnet/linux_close.c#L407, 可以看到, 它其实是调用了 系统调用 poll, 这个 poll 可以传入一个timeout, 当timeout的时候, 返回0. 另外一处是下面的每次 nanoTimeout -= (newNanoTime - prevNanoTime), 相当于把已经过去的时间剪掉, 看 timeout 还剩余多少. 但是不论那种方式timeout, connect_rv 的值都会是0.

所以, 我们可以看到接着就有了下面的代码:

if (connect_rv == 0) {
   JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
                            "connect timed out");
   ...省略 ...
}

所以, 真正的 timeout 的处理, 要么是在 poll 的系统调用, 要么是在一个循环里面每次循环递减, 直到减没.

在结尾时候, 我们可以思考这吗一个问题: 当不设置timeout, 真的就永远等下去吗?

bpftrace 探测 Java 运行时栈-实践

开发Java应用的时候, 有时候我们想知道某个函数到底在哪里被调用的. 我们可以采取的方法有:

  1. 若是本地开发, 可以在函数体上加断点, 每当函数被调用, 都会暂停.
  2. 若是我们可以改动的代码, 我们可以加日志打印栈, 这样就能发现在哪里被调用.
  3. 不论是不是我们自己的Java代码, 我们都可以通过 Btrace 进行注入脚本, 在脚本打印运行时栈.

但是有时候, 我们想知道我们的 Java 代码是哪里调用了系统的 native 代码, 比如某些系统调用(syscall), 那么该如何获取这些栈呢?

这时候, 使用Java 提供的这些方法, 都无法达到目的, 所以, 我们要借用系统层级的 tracing 方式. 如今最流行且最简单的方式就是使用 bpftrace. 本文接下来将用一个例子来说明, 如何使用 bpftrace 来查找我们的Java应用是如何调用 recvfrom 这个系统调用的.

Java 代码

下面是我们用来演示的代码, 它的意图就是不断的循环去获得某个网页. 不断循环的目的是为了给我们的手工操作留有足够的时间. 这段代码有网络操作, 所以会调用系统调用 recvfrom 来拿到响应(response).(感谢 chatGPT 给我们演示代码)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class URLTest {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000; i++) {
            try {
                URL url = new URL("http://www.tianxiaohui.com");
                HttpURLConnection con = (HttpURLConnection) url.openConnection();
                con.setRequestMethod("GET");
                System.out.println(i + "Response code: " + con.getResponseCode());
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String line;
                StringBuilder response = new StringBuilder();
    
                while ((line = in.readLine()) != null) {
                    response.append(line);
                }
                in.close();
                //System.out.println(response.toString());
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println(e.getMessage());
            }
            Thread.sleep(2000);
        }
    }
}

本地编译并运行

$ javac URLTest.java
$ java -XX:+PreserveFramePointer URLTest
Response code: 301

安装编译 perf-map-agent

下载

可以直接克隆这个 git repo:

$ git clone https://github.com/jvm-profiling-tools/perf-map-agent.git

或者直接下载最新的代码

$ curl https://github.com/jvm-profiling-tools/perf-map-agent/archive/refs/heads/master.zip --output perf-map-agent.zip
$ unzip perf-map-agent.zip

编译

$ cd perf-map-agent
$ cmake .
$ make 

生成JIT编译后符号表

# 获取 java 进程号
$ jcmd 
3408161 jdk.jcmd/sun.tools.jcmd.JCmd
3406489 URLTest

# 设置 JAVA_HOME
$ export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64

# 生成符号表
$ bin/create-java-perf-map.sh 3406489

# 查看符号表
ls -lah /tmp/perf-3406489.map
-rw-rw-r-- 1 root root 42K Nov 15 05:08 /tmp/perf-3406489.map

bpftrace 获取调用栈

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_recvfrom /pid==3406489/{ @[ustack(20)] = count(); }'
Attaching 1 probe...
^C

@[
    recvfrom+116
]: 2
@[
    __GI___recv+110
    Java_java_net_SocketInputStream_socketRead0+434
    Interpreter+28336
    Interpreter+4352
    Interpreter+4352
    Interpreter+4352
    Interpreter+4352
    Interpreter+5875
    Interpreter+4352
    Interpreter+4352
    Interpreter+3728
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+3828
    call_stub+138
    JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+883
    jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*) [clone .constprop.1]+682
    jni_CallStaticVoidMethod+352
    JavaMain+3441
    ThreadJavaMain+13
    start_thread+755
]: 3

把翻译代码变成编译代码

java -XX:+PreserveFramePointer -XX:-TieredCompilation -XX:CompileThreshold=1 URLTest

    __GI___recv+110
    Java_java_net_SocketInputStream_socketRead0+434
    Ljava/net/SocketInputStream;::socketRead0+244
    Ljava/net/SocketInputStream;::read+224
    Ljava/io/BufferedInputStream;::fill+784
    Ljava/io/BufferedInputStream;::read1+176
    Ljava/io/BufferedInputStream;::read+252
    Lsun/net/www/http/HttpClient;::parseHTTPHeader+444
    Lsun/net/www/http/HttpClient;::parseHTTP+1004
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+1420
    Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+196
    Ljava/net/HttpURLConnection;::getResponseCode+96

调大code cache

在Java中,您可以通过设置-XX:ReservedCodeCacheSize标志来调整JIT代码缓存的大小。该标志控制JIT编译器可用来存储生成的代码的本机内存的最大数量。默认情况下,代码高速缓存的大小由平台确定 - 32位JVM通常为240MB,而64位JVM通常为480MB。

要调整JIT代码缓存区的大小,可以将-XX:ReservedCodeCacheSize标志设置为自定义值。该值应以字节为单位指定,并且可以是2的幂次方或2048(2KB)的倍数。以下是将标志设置为512MB的示例:

java -XX:ReservedCodeCacheSize=536870912 <your_program>
这将将JIT代码缓存的最大大小设置为512MB。

请注意,增加JIT代码缓存的大小可以通过允许JIT编译器在内存中存储更多的生成代码来提高性能。但是,这也会增加应用程序的内存使用量。您应仔细调整代码缓存的大小,并监视应用程序的内存使用情况,以确保它不超过系统上的可用内存。