分类 Troubleshooting 相关 下的文章

Java 如何简单的诊断 https (SSL/TLS) 请求

如果你能控制整个 Java 应用的启动, 那么你可以很简单的加SSL/TLS 的 debug 参数去打印等多的信息. 比如:

java -Djavax.net.debug=all myApp
java -Djavax.net.debug=ss:plaintext myApp 

想查看更多可以用的参数: 必须找一个 App, 不能使用常见的 java -Djavax.net.debug=help -version

java -Djavax.net.debug=help myApp

一个访问百度首页的 (-Djavax.net.debug=ss:plaintext) 的输出:

javax.net.ssl|DEBUG|01|main|2021-08-06 07:43:20.943 PDT|SSLCipher.java:1769|Plaintext before ENCRYPTION (
  0000: 47 45 54 20 2F 20 48 54   54 50 2F 31 2E 30 0A 0A  GET / HTTP/1.0..
)
javax.net.ssl|DEBUG|01|main|2021-08-06 07:43:21.233 PDT|SSLCipher.java:1671|Plaintext after DECRYPTION (
  0000: 48 54 54 50 2F 31 2E 30   20 35 30 30 20 49 6E 74  HTTP/1.0 500 Int
  0010: 65 72 6E 61 6C 20 53 65   72 76 65 72 20 45 72 72  ernal Server Err
  0020: 6F 72 0D 0A 43 6F 6E 74   65 6E 74 2D 4C 65 6E 67  or..Content-Leng
  0030: 74 68 3A 20 30 0D 0A 43   6F 6E 74 65 6E 74 2D 54  th: 0..Content-T
  0040: 79 70 65 3A 20 74 65 78   74 2F 70 6C 61 69 6E 3B  ype: text/plain;
  0050: 20 63 68 61 72 73 65 74   3D 75 74 66 2D 38 0D 0A   charset=utf-8..
  0060: 44 61 74 65 3A 20 46 72   69 2C 20 30 36 20 41 75  Date: Fri, 06 Au
  0070: 67 20 32 30 32 31 20 31   34 3A 34 33 3A 32 31 20  g 2021 14:43:21 
  0080: 47 4D 54 0D 0A 53 65 72   76 65 72 3A 20 62 66 65  GMT..Server: bfe
  0090: 0D 0A 0D 0A                                        ....
)

更多信息参考:

  1. https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/ReadDebug.html
  2. https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#Debug
  3. https://access.redhat.com/solutions/973783

记录一次奇怪的 CPU profiling 问题

本文记录了一次奇怪的线上线程相关问题的案例.

  1. 先是看到 CPU 稍微有点高
    cpu.png
  2. 大概看了下, 发现这个机器没做啥耗 CPU 的操作, 大多数都是网络请求, 于是做 CPU profiling, 于是出现了下面奇怪的方法调用
    sched_yield.png
    首先, 看上去这个不像 Java 的方法, 于是推断是 JVM 里面调用的系统 native 方法. 其次, 去 google 了一把, 这是系统的一个方法, 它的意图就是让出 CPU, 让其它 CPU 去执行. 可是为什么要频繁让出呢? 为什么占用这么多 CPU 资源呢? 另外, 搜了一下有没有人遇到过类似问题, 并没有发现.
  3. 是不是 Native 代码调用, 那些 native 代码调用了它? 于是从系统层面针对这个 Java 进程的这个方法调用, 又专门做了一次 CPU profiling, 看到下面的火焰图:
    native.png
  4. 从上的火焰图看, 貌似有个叫 WrapperJarAppMa 的方法调用了 __sched_yield 方法. 于是去 google 了一下这个关键字, 相当失望:
    google.png
    不过注意 google 给的提示! 后面再说.
  5. 正常的推理已经不能解决问题, 于是开始归纳总结. 发现如下模式:

    1. 这个应用分配了 174G 内存, 分配了 24 个 CPU, 资源相当丰富
      cpuMem.png
    2. 这个应用的 heap 给了 154G, heap 常规占用在 75G 左右, 空闲也非常多;
    3. 这个应用占用这么多 heap 是因为内存加载了非常多的缓存, 系统启动就加载, 加载完要十几分钟, 然后再接收流量;
      可是从上面的这些模式, 并么有推断出什么.
  6. 于是又回到已知的关键字 "__sched_yeild" 和 "WrapperJarAppMa". 想到 AsyncProfiler 其实可以根据线程做火焰图, 于是又根据线程做了火焰图, 看到如下:
    flameWithThread.png
    两边都是业务线程, 中间只有 2 层方法的都是我们看到的上面 "WrapperJarAppMa" 调用 "__sched_yeild", 所以, 中间从下数第二层红色的都是"WrapperJarAppMa", 第三层都是"__sched_yeild". 虽然这次也只能看到上面已经看到的 2 个关键字, 可是从于业务线程对比, 可以明显看出来: "WrapperJarAppMa" 是线程名字, 因为绿色的业务线程那边, 第二层也是线程名字.
  7. 于是通过 pstree 去查看线程的结构关系, 发现确实如此:
    wrappThread.png
  8. 可是代码里面从来没有看到这个线程的关键字, 这个线程从那里来的? 为什么这么多? 于是打算到 log 文件里面查一吧, 确实发现了这个线程(原来真正的线程名字叫 "WrapperJarAppMain" 其它地方只是字长的省略):
    log.png
  9. 这个时候在拿这个线程名字去 google, 就发现了线程出处, 原来在我们付费使用的商业软件 Java Service Wrapper 里面. 因为是付费软件, 在 maven 里面没有发现它的踪影, 于是从生产环境下载一个正在使用的 wrapper.jar. 反编译后重要找到了这个线程的出处:
    wrapperJarApp.png
  10. 为什么会有这么多 "WrapperJarAppMa" 线程一直存活, 并且一直 sched_yeild 呢? 想看看这些线程到底在做什么? 是不是在上面的 while 循环里面出不来了? 于是做了一个 54G 的 heap dump, 然后压缩到38G 下载到本地去做 heap 分析, 竟然没有看到这些线程:
    wrapperThreadHeap.png
  11. 于是怀疑在做 heap dump 过程中这些线程已经死掉了, 于是重新去查看了这些线程, 发现线程还好好的活着:
    threadAgain.png
  12. 这次还注意到, 在 heap dump 里面线程共计277个, 可是使用 pstree 能看到383个, 考虑到系统正在运行中, 有好多线程可能在变动中, 但是也不能差别这么多. 仔细观察, 发现类似这样的线程都没有出现在 heap dump 中
    g1.png
  13. 类似这样的线程有:

    1. {C2 CompilerThre}(123318)
    2. {G1 Main Marker}(123296)
    3. {G1 Marker#X}(123297)
    4. {G1 Refine#XX}(123295)
    5. {GC Thread#XX}(123247)
    6. {VM Periodic Tas}(123322)
    7. {VM Thread}(123313)
    8. {WrapperJarAppMa}(130114)
    9. {default-microme}(123384)
    10. {java}(123246)
  14. 可以看到这些线程的数量也不少:
    total.png
  15. 在生产环境看到这个问题之后, 去测试环境看了一下, 有如下总结:

    1. 测试环境在 CPU profiling 中并没有看到 __sched_yeild 被调用;
    2. 测试环境 也有 WrapperJarAppMa 线程;
    3. WrapperJarAppMa 总数与当前物理机的 CPU 合数一致, 除了一个正常结束的 WrapperJarAppMa 线程, 剩下 (CPU 核总数 - 1) 个这样的线程;
    4. 这些后来不死的线程, 在 heap 中看不到, 类似JDK 某些自带的线程;
    5. 这些线程的名字完全复制第一个线程的名字, 可是看不到创建的地方.
  16. 改动 WrapperJarApp 的源码, 在生产新线程的地方加了日志, 改了线程的名字, 发现日志中该方法只有一次被调用

    long i = System.currentTimeMillis() % 10;
    Thread mainThread = new Thread(this, i + "-WrapperJarAM");
    Thread.dumpStack();
  17. 如今的问题:

    1. 这些后来是怎么创建出来的?
    2. 为什么生产环境他们一直在运行, 可是在测试环境 profiling 中看不到运行;
    3. 另外发现 Wrapper-Connection 线程的优先级是 10, 怀疑这些不死的线程优先级也是 10
  18. 终于看到这些线程在干嘛

    1. 怀疑这些线程不是 Java 代码创建 (拦截Thread.start() 不到, 并且没有出现在 thread dump), 于是做了一个 core dump;
    2. 使用 gdb 从 core dump 可以看到线程正在运行的栈
      coreBt.png
    3. 果然在对应的目录找到了对应的共享库文件, 于是去查该项目的git 工程, 也找到了对应的共享库;
  19. 找到调用 JNI 共享库的代码 - 从项目源代码中找到了相应的代码, 如下:

    private static final String PATH = "/home/supra/work/test/blas/netlib-blas-dist/src/main/resources/linux-x86_64";
    private static final String[] NETLIB_LIBS = {
              "libquadmath.so.0", "libgfortran.so.3", "libopenblas.so.0", "libblas.so.3", "liblapack.so.3" };
    
    public static void main(String[] args) throws IOException {
      for (String lib : NETLIB_LIBS) {
        System.load(new File(PATH + "/" + lib).getAbsolutePath());
        NativeLibrary.getInstance(lib);
      }
    
      NativeSystemBLAS blas = new NativeSystemBLAS();
      double prod = blas.ddot(4, new double[] { 1.0, 1.0, 1.0, 1.0 }, 1, new double[] { 1.0, 0.0, 1.0, 0.0 }, 1);
      if (prod != 2.0) {
        throw new IllegalArgumentException("BLAS sanity test failed. Expected 2.0. Got: " + prod);
      }
    
      System.out.println("done");
      try {
        Thread.currentThread().sleep(1000000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  20. 为什么会创建 (cpu count - 1) 个线程?

    1. 我们找到了这个共享库的源码, 结合 core dump 中看到的栈, 找出了创建这些线程的源代码行. 这里使用了 CPU 个数去创建 blas server 的服务线程数.
      https://github.com/xianyi/OpenBLAS/blob/974acb39ff86121a5a94be4853f58bd728b56b81/driver/others/blas_server.c#L639
    2. 下面的 blas_thread_server 方法就是 blas server 要执行的 routine. 我们通过 gdb 看到的代码, 就是这个里面的代码
  21. 为了进一步验证我们的想法, 下载了 https://github.com/xianyi/OpenBLAS 的代码, 在本地 Linux 上面重新编译包含符号的共享库, 这次, 我们看到这些线程在哪行代码运行:
    bt_thread.png

到此为止, 我们终于搞明白了这些线程哪来的?这些线程数目和 CPU 个数的关系. 其它一些相关的话题:

  1. blas 和 openBlas 应该是人工智能相关的算法类库;
  2. 其实 WrapperJarApp 里面设置的线程优先级在 Linux 上面其实没啥用.
  3. C 代码 (通过 pthread_create 创建的线程, 在Java 的 thread dump 看不到)

JDBC 无法建立到 Oracle 数据库的连接

A 程序使用和 B 程序一样的数据库访问用户名, 密码, 主机名, 端口等配置, 可是 A 程序无法建立到数据库的连接, B 程序却可以.

A 程序的代码在建立连接的时候, 只是打印了一句: 无法建立连接, 再也没任其它信息.

首先, 怀疑是不是防火墙问题 -> 使用 telnet 和 curl 测试了一下端口, 发现端口完全可以建立正常的 tcp 连接;

虽然可以建立 tcp 连接, 可是并不一定不是防火墙问题, 因为防火墙可能根据协议规则等建立, 允许基本的 tcp 端口连接;

下一步, 在建立连接的时候, 进行 tcp 抓包, 重要找到了问题所在: 原来在建立连接之后的协商阶段, 发生了错误->

...AUTH_PID...1234....AUTH_SID.  /ORA-28040: No matching authentication protocol

原来是认证协议不匹配, 还给出了 Oracle 官方的错误 ID.

根据这个错误 ID, 我们搜索一下, 发现基本是客户端JDBC 版本和服务器版本不一致造成的.

最近Oracle 服务器升级到 19c, 可是客户端还是用的适配 11c 的 JDBC 连接, 所以导致出现了这个错误.

为什么程序 B 没问题呢? 程序 B 使用的是 JDK 8, 它的 jdbc 驱动也升级了, 所以完全没问题. 然而程序 A 是一个基于 JDK 7 的程序, 它无法升级到最新的 JDBC 驱动, 因为 Oracle 最新的 JDBC 驱动只支持最低 JDK 8.

所以, 为了解决这个问题, 程序 A 应该升级 JDK 最低为 JDK8.

关于 Java 应用的 线程栈内存大小

对于 Java 应用, 对于内存的占用, 主要是Java堆, 通常我们为了性能考虑, 会对堆设置最大值, 否则 JVM 就会根据它锁了解的系统参数, 设置一个可能的最大值. 除了堆之外, 其实还有好几部分, 也可能占用不少内存. 比如元数据区(老年代), 线程栈, 编译后的代码, GC 程序, 编译器, JVM符号表之类的. 这里可以通过添加启动参数: -XX:NativeMemoryTracking=summary or -XX:NativeMemoryTracking=detail 来追踪原生内存的占用. 比如:
nmt.png

比如上面的截图中, kiyomi 看到 Thread 部分 reserve 了 1052M, 已经提交占用了 1052M, 查看 thread dump, 发现现在大概有 1003 个线程, 基本每个线程占用 1M.

另外我们知道, 可以使用 Xss 参数设置每个线程栈的大小, 比如:
-Xss1m
-Xss1024k
-Xss1048576
上面的参数等同于:
-XX:ThreadStackSize

在 Linux 64 bit 上, 默认是 1MB. 从 Oracle 的官方文档: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
可以看到具体的说明.

从上面的文档可以看出, 每个栈在 64bit 的 Linux 上使用 1M, 可是我从另外一个机器上看到大概有 20K 个线程, 可是占用的内存并不是 20G. 于是我们看到 对于 IBM JDK 和 OpenJ9 JDK 它们都用 3 个参数:

  1. -Xiss<size> Set initial Java thread stack size 2 KB
  2. -Xss<size> Set maximum Java thread stack size 320 KB (31/32-bit); 1024 KB (64-bit)
  3. -Xssi<size> Set Java thread stack size increment 16 KB
    这里它们对 Xss 的解读是最大的可用栈大小. 所以, 就有个疑问, 对于 HotSpot JDK, 是每个栈必须占用 1M, 还是这也是最大值???

更新:
根据这个 Bug 描述: https://bugs.openjdk.java.net/browse/JDK-8191369
应该是最大值, 之前的 NMT 简单的把 reserved 和 commited 置成 线程数 * Xss. 其实真正的 committed 可能并么有这么多, 虽然声明使用 1M 虚拟内存, 可能实际使用的物理内存页比较少. 所以 commited 比较少.

比如下面这个例子, 该 JVM 在一个 container 里面, 设置的最大可用内存是 16G, 已经使用 13.58G. 当前线程数是 11266 个, 堆大小设置的是 8G, 可以看到 Thread 通过 NMT 显示的是大概 10G, 总的 commit 已经大约 20G. 其实这个 Thread 占用的都是根据线程数 * Xss 算出来的, 都是根据最大值算出来的, 所以其实最后算出的 commit 是不准的. 根据 top 可得到该进程的 RSS 大概是 10G. 所以可以推算 Thread 占用少于 2 个 G.

nmt1.png

修复之后的版本:
newNMT.png