Eric 发布的文章

http keep-alive 实验

之前一篇讲道客户端和服务端是如何处理 http keep-alive 的, 其中很多都是一笔带过. 本篇补充一些细节.

版本

对于 http keep-alive 的概念, 这里的讨论只局限于 http 1.0, http 1.1. 对于 HTTP/2, HTTP/3 这里的讨论不适用.

http header - Connection & Keep-Alive

http 1.1 里面默认是持久连接. 但是我们可以看到下面的默认情况:
Chrome:

  1. 默认发送 Connection: keep-alive 头, 但是不发 Keep-Alive.

curl:

  1. Ubuntu 上的 curl 7.81.0 默认连 Connection 都没发.
  2. Mac 上的 curl 8.6.0 也没发 Connection header.

python requests:

GET / HTTP/1.1
Host: www.henu.edu.cn
User-Agent: python-requests/2.31.0
Connection: keep-alive

Java:

GET / HTTP/1.1
User-Agent: Java/17.0.4.1
Host: www.henu.edu.cn
Connection: keep-alive

JDK 里面的 Java client 默认的处理细节

Keep-Alive header 里面的 timeout 和 max 分别对应 JDK HttpClient 里面的字段:
timeout - keepAliveTimeout
max - keepAliveConnections.

keepAliveTimeout

首先, 在 HttpClient 里面定义了一个 keepAliveTimeout 字段. JDK 21 到链接:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L136C9-L136C25.

这个字段有4种取值可能:

  1. 正值 - timeout 的秒数, 对应 Keep-Alive header 里面的 timeout 值.
  2. 0 - 对方明确不需要 Keep-Alive.
  3. -1: 需要保持连接, http 1.1 设置或者不设 Connection: keep-alive, 但是没有设置 timeout 值.
  4. -2: 明确在 Keep-alive header 里面设置了 timeout: 0 这个值.

如果把我们上面实验的客户端的结果反过来看成对方发来的response 来看, 都属于上面的第3种: -1 类型.

真实的JDK java 客户端的解析过程在这些代码中:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L907-L917

真正的使用这个值的地方

真正使用这个值的地方在:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/KeepAliveCache.java#L162-L177

keepAliveConnections

这个值全部都是在 HttpClient 里面使用的.

parse:

  1. 若头部带来了 Keep-Alivemax 则使用这个值.
  2. 若没带来 max, 则如果使用代理, 则是50, 否则是5.
    细节代码: https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L902-L904

由http header Keep-Alive 引出的问题

由于建立一个网络连接相对于本地操作十分昂贵, 所以系统应该尽量重用一个已经建立的连接.

因此, 在http 1.0的规范中定义了 Keep-Alive header, 一个典型的例子:

Keep-Alive: timeout=5, max=1000

这里的 timeout 表示如果没有后续的重用, 让这个连接最短逗留(idle) 5秒才能关闭它. 若有重用, 这从最后一个重用重新开始计时.
这里的 max 表示如果这个连接被一直重用, 但是这个连接最多只能发送1000个请求(或响应), 一旦超过这个数目, 即使能还没等idle 5秒, 也要关闭它. 所以它表示最多可以通过这个连接发送多少请求(或响应).

http 1.1

http 1.1 默认是持久化连接, 所以默认暗含是 Keep-Alive 的, 即便不发送这个头字段. 如果不在 http 的请求和响应中发送这个头字段, 那么连接的两端又该如何控制 idle 的timeout 和 一个连接最多可以发送的请求(或响应)的数目呢?

既然是为了重用, 那就最大化重用. 是不是一直重用某个连接, 一直不关闭就好了呢?

timeout 的合理性

  1. 从资源管理的角度. 若服务器端一直保持连接, 那么每个新用户都保持一个连接, 即便用户几分钟没数据传输, 这会导致服务器资源(连接, 内存等)很快被耗尽. 若客户端程序(浏览器, 代码客户端)也会有一样的问题.
  2. 从公平的角度. 若一个连接一直保持, 那么后来的新用户肯定无法再连接上.
  3. 从安全和性能的角度. 若保持很多没有数据的连接, 可能会有潜在的性能降级或者内存泄漏问题.

设置一个合理的 timeout 值, 保证一旦一段时间没数据传输, 就关闭这个连接.

max 请求的合理性

既然有了 timeout 可以保证 idle 了就关闭, 同时为了最大化重用, 那么是不是不需要 max 了呢?
考虑一种情况: timeout 是5秒, 但是每当 idle 4秒的时候, 就有一个新的请求(或响应), 那么这个连接将一直会被重用. 那么一直重用又有什么不好呢?

在理想的情况一下, 一直有数据固定频率的传输, 永久的使用这个连接其实是非常美好的一种状态.

但是 max 也有其它合理的地方:

  1. 避免某些写的不好的代码把某些请求(request)资源绑定在这个连接之上, 导致内存泄漏, 若关掉连接则全部释放.
  2. 公平性. 比如某些抢票的网站, 避免某些人一直先保持一个连接, 导致后面的人根本连不上. max 保证多少次请求之后, 一定关闭.
  3. 对于某些负载均衡器(Loader Balancer), 如果某个特定IP上一直保持特别多的连接, 会导致不再均衡.

服务端的 timeout 和 max

虽然 http 1.1 不再设置 Keep-Alive 头字段, 默认是持久连接. 但是连接的2端仍然要对合理的这2种机制做处理. 下面列举一些常见的服务端的处理和配置.

Tomcat web 服务器

Tomcat 是一个常用的 Java web 服务器. 在今天(20240605)最新的 10.1 版本里面的 http Connector 的配置里, 就能看到关于 timeout 和 max 的配置:
keepAliveTimeout: 默认60秒.
maxKeepAliveRequests : 默认100.

nginx 服务器

nginx 的配置分别是:
keepalive_timeout: 默认75秒.
keepalive_requests: 默认 1000.
不过, nginx 还有一个 keepalive_time: 类似max, 它从时间角度约束一个连接从开始建立最长存活多久, 区别于idle timeout.

envoy proxy

对于 Envoy proxy 没有对应的完全一致的概念, 不过它有另外2个参数:
common_http_protocol_options.idle_timeout:
stream_idle_timeout:
更多内容参考: https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts
https://github.com/envoyproxy/envoy/issues/8652

客户端的 timeout 和 max

既然对于 http 1.1 默认是持久的, 那么客户端也是暗含的, 不发送 Keep-Alive 的, 那么客户端是怎么处理的呢?

chrome 浏览器

没有找到相关文档.

Apache httpClient

在 4.5 版本 2.6 节 连接管理部份(https://hc.apache.org/httpcomponents-client-4.5.x/current/tutorial/html/connmgmt.html), 有下面一段描述:
“If the Keep-Alive header is not present in the response, HttpClient assumes the connection can be kept alive indefinitely”

JDK HttpClient

对于取值的大概处理:
https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L892-L937

max 的取值:

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L902C25-L902C82

keepAliveConnections = p.findInt("max", usingProxy?50:5);

timeout 的取值

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L906C37-L918

    /*
     * The timeout if specified by the server. Following values possible
     *  0: the server specified no keep alive headers
     * -1: the server provided "Connection: keep-alive" but did not specify a
     *     a particular time in a "Keep-Alive:" headers
     * -2: the server provided "Connection: keep-alive" and timeout=0
     * Positive values are the number of seconds specified by the server
     * in a "Keep-Alive" header
     */
OptionalInt timeout = p.findInt("timeout");
if (timeout.isEmpty()) {
    keepAliveTimeout = -1;
} else {
    keepAliveTimeout = timeout.getAsInt();
    if (keepAliveTimeout < 0) {
        // if the server specified a negative (invalid) value
        // then we set to -1, which is equivalent to no value
        keepAliveTimeout = -1;
    } else if (keepAliveTimeout == 0) {
        // handled specially to mean close connection immediately
        keepAliveTimeout = -2;
    }
}                          

max 用完之后, 关闭连接:

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L443-L453

客户端总结

客户端也会考虑 连接idle 的timeout 和 max 请求数, 但是很多情况下, 它没有服务端那么紧迫, 但是客户端也有这些机制, 只是没有那么透明.

当 CPU 空闲(idle)的时候在干什么

今天学习 Linux schedule 框架的时候, 看到如下的图. 这个图列出了 Linux 内核里面的几个 schedule classes, 以及它们之间的关系.
图片源自于: https://deepdives.medium.com/digging-into-linux-scheduler-47a32ad5a0a8
1_IfRhjQssWLhgJqeItlUAdw.webp

图中各个方块分别表示 Linux 内核的几个 schedule class, 他们按照顺序通过链表的形式连在一起, 当前面的 schedule class 没有需要执行的task的时候, 就去下一个 schedule class 去查找. 当前面所有 schedule class 都没有 task 可以执行的时候, 就去执行 idle 这个虚拟的task.

各个 schedule class 的描述:

  1. Stop Scheduler (SCHED_STOP) - 停止调度器 这个调度类是用于管理特殊的停止任务,这些任务用于在CPU上停止所有其他任务的执行,通常用于实现CPU热插拔和一些系统级别的调试操作。这个调度类的任务具有最高的优先级,可以抢占系统中的任何其他任务。
  1. Deadline Scheduler (SCHED_DEADLINE) - 截止时间调度器 这是一种较新的调度类,它为任务提供了严格的截止时间保证。SCHED_DEADLINE使用了称为“恒定带宽算法”的调度策略,允许任务指定它们的运行周期、运行时间和截止时间。这个调度器试图保证每个任务在其截止时间之前都能完成指定的运行时间,适用于对时间敏感度非常高的任务。
  1. Real-Time Scheduler (RT) - 实时调度器 实时调度类分为两种策略:SCHED_FIFO(先进先出)和SCHED_RR(轮转)。这些策略用于实时任务,这些任务需要快速且可预测的响应时间。在SCHED_FIFO中,只有当运行的实时任务主动放弃CPU或者被更高优先级的实时任务抢占时,它才会停止运行。SCHED_RR与SCHED_FIFO类似,但是它为每个实时任务提供了时间片,使得同一优先级的实时任务可以轮流运行。
  1. Completely Fair Scheduler (CFS) - 完全公平调度器 CFS是Linux内核的默认调度类,用于普通的非实时任务。它的目标是确保所有运行的进程都能获得公平的CPU时间。CFS使用红黑树来跟踪所有可运行的进程,并尽量平等地分配CPU时间。它基于虚拟运行时间的概念,该时间考虑了进程的权重。权重越高的进程获得的CPU时间越多。
  1. Idle Scheduler (SCHED_IDLE) - 空闲调度器 这个调度类用于非常低优先级的任务,只有当系统中没有其他可运行的任务时,这些任务才会被调度执行。它通常用于那些可以在系统空闲时运行的后台任务,例如内存压缩守护进程(kcompactd)。

每个调度类都实现了一组调度器框架定义的通用接口,这些接口包括任务入队、出队、选择下一个运行任务等操作。调度器通过这些接口与调度类交互,以实现不同类型的任务调度需求。

Idle 调度器是实现的?

参考这篇文章: https://manybutfinite.com/post/what-does-an-idle-cpu-do/
最终是通过 IA 32 指令集的 HTL(halt) 指令来完成的.
hlt: Halt,使CPU进入halt状态,这是一种低功耗模式。在这种模式下,CPU会停止执行指令,直到下一个中断到来。

查询最新代码:
https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/irqflags.h#L48
asm volatile("sti": : :"memory");
这段代码的作用是使CPU开始响应外部中断,并且立即进入低功耗的halt状态,直到下一个中断到来。这通常用在系统空闲循环或者其他需要CPU等待事件的场合。由于这段代码会改变CPU的中断使能状态,并且可能影响内存的一致性.

为什么一个简单的 Java 进程的 core dump 那么大

写了一个非常简单的 java main 函数, 然后运行, 然后做 core dump, 竟然发现 core dump 竟然有 6.6G.

Java 程序:

public class ErrorExample {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(600000);
    }
}

做 core dump 和 2种不同的 heap dump.

gcore <pid>

$ ls -lah 
-rw-rw-r-- 1 supra supra 6.6G Apr 30 00:26 core.276268
-rw-rw-r-- 1 supra supra  67M Apr 30 00:26 core.276268.gz
-rw-rw-r-- 1 supra supra 1.3M Apr 30 01:02 heap.bin
-rw-rw-r-- 1 supra supra 334K Apr 30 01:02 heap.bin.gz
-rw------- 1 supra supra 2.8M Apr 30 01:01 heap.hprof
-rw------- 1 supra supra 855K Apr 30 01:01 heap.hprof.gz

上面分别是使用不同工具得到的不同 dump 及进一步压缩过后的:

  1. gcore 276268 得到 core.276268
  2. jhsdb jmap --binaryheap --pid 280038 得到 heap.bin
  3. jcmd 280038 GC.heap_dump heap.hprof 得到 heap.hprof.

可以看到 jhsdb 做出的最小, core dump 最大. 这很有可能是 JVM 保留了很多想用但是还没用到的内存.

查看JVM 当前使用的内存, 发现其实使用的很小:

$ jcmd 281746 GC.heap_info
281746:
 garbage-first heap   total 258048K, used 1744K [0x0000000707e00000, 0x0000000800000000)
  region size 1024K, 2 young (2048K), 0 survivors (0K)
 Metaspace       used 155K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 6K, capacity 386K, committed 512K, reserved 1048576K

然后查看使用的启动参数, 可以看到保留的最大的堆是将近4G(MaxHeapSize=4162846720), 保留的代码缓存区是240M(ReservedCodeCacheSize=251658240), 再加上其它, 比如 JDK 库, 元数据区等, 可能达到6G多.

$ jcmd 281746 VM.flags
281746:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=262144000 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4162846720 -XX:MaxNewSize=2497708032 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5836300 -XX:NonProfiledCodeHeapSize=122910970 -XX:ProfiledCodeHeapSize=122910970 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC

如果使用 pmap 查看这个进程的虚拟内存使用情况, 可以发现确实使用了6.6G(最后一行):

$ pmap -x 280038
280038:   java ErrorExample
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000707e00000  256000     328     328 rw---   [ anon ]
0000000717800000 2758656       0       0 -----   [ anon ]
00000007c0000000 1048576       0       0 -----   [ anon ]
............ 省略 ....................
00007f39c513e000       8       8       0 r---- ld-linux-x86-64.so.2
00007f39c5178000       8       8       8 rw--- ld-linux-x86-64.so.2
00007ffc854ed000     132      32      32 rw---   [ stack ]
ffffffffff600000       4       0       0 --x--   [ anon ]
---------------- ------- ------- -------
total kB         6821276   37464   10660

Linux 文件特殊权限

最简单的文件看起:
例如:

$ ls -lah
total 948K
drwxrwxr-x  4 supra supra 4.0K Apr 19 21:02 .
drwxrwxr-x 14 supra supra 4.0K Dec  6 21:16 ..
-rw-rw-r--  1 supra supra   69 Nov  6  2022 Dockerfile
-rwxrwxr-x  1 supra supra  16K Apr 19 21:02 a.out
-rwxrwxr-x  1 supra supra  11K Sep 18  2021 hello
-rw-rw-r--  1 supra supra  217 Jun 14  2022 hello.c

读写执行权限

  1. 第一个字符表示文件类型:

    1. -:普通文件
    2. d:目录
    3. l:符号链接

    其他字符表示其他特殊类型的文件,如管道:p、套接字p等。

  2. 接下来的三个字符表示文件所有者(owner)的权限:

    1. r:读权限(可以查看文件内容)
    2. w:写权限(可以修改文件内容)
    3. x:执行权限(可以运行文件作为程序)
  3. 紧接着的三个字符表示文件所属组(group)的权限,含义同上。
  4. 最后三个字符表示其他用户(others)的权限,含义同上。
  5. 如果权限字符被替换为特殊字符,如sS,则表示设置了SUID或SGID位。如果是tT,则表示设置了粘滞位(sticky bit).

在Linux系统中,SUID(Set User ID)是一种特殊的文件权限设置,当应用于可执行文件时,它允许用户以文件所有者的身份运行该文件。通常,当一个程序运行时,它继承了启动它的用户的权限。但是,如果该程序具有SUID权限,那么不管是谁运行它,它都会以文件所有者的权限来执行。

然而,SUID也带来了潜在的安全风险。如果SUID程序存在漏洞,那么它可能被利用来提升权限,因此只有在确实需要时才应该设置SUID权限,并且需要确保相关程序是安全的。系统管理员应该定期审查具有SUID权限的文件,并确保它们来自可信的源并且是最新的。

SUID & SGID

SUID权限通常用八进制代码 4000 表示,或者在 ls -l 命令的输出中,可以看到在文件所有者的执行权限位上有一个 s 字符(例如 -rwsr-xr-x)。

SUID的意义在于它允许用户执行一些通常需要更高权限的操作,而不必给予用户那些权限。这是通过让特定的程序以更高权限(通常是root权限)运行来实现的。

为什么要这么做:

1. 安全性:SUID允许系统管理员授予用户执行特定高权限任务的能力,而不必给用户更广泛的权限。这有助于遵循最小权限原则,减少安全风险。

2. 功能性:某些程序需要访问系统资源或执行任务,这些资源或任务通常只能由系统管理员(root用户)访问或执行。例如,passwd 程序需要修改shadow 文件,该文件包含加密的用户密码,通常只有root用户才有权限修改它。

3. 用户便利性:SUID使得普通用户可以方便地执行某些需要特殊权限的操作,而无需切换到root用户。

Sticky权限

Sticky权限(也称为粘滞位或粘着位)是Linux和类Unix操作系统中的一种特殊权限设置,它可以应用于目录。当一个目录设置了sticky权限,这意味着只有文件的所有者、目录的所有者或者超级用户(root)才能删除或重命名目录中的文件。其他用户即使有该目录的写权限也不能删除或重命名其中不属于他们的文件。

Sticky权限通常用于共享目录,如/tmp,这个目录是所有用户都可以写入的。由于任何用户都可以创建临时文件,没有sticky权限的话,就可能出现一个用户删除或修改了另一个用户的文件的情况。通过设置sticky权限,系统管理员可以防止这种情况发生,确保用户只能管理自己的文件。

设置

有2种: 符号方式和数字方式

符号方式

chmod WhoWhatWhich file | directory
  1. Who - represents identities: u,g,o,a (user, group, other, all)
  2. What - represents actions: +, -, = (add, remove, set exact)
  3. Which - represents access levels: r, w, x, s, t(read, write, execute, SUID/SGID, sticky)

ex:

chmod ug+rw test.txt
chmod g+s 

数字方式

通常使用3位数字分别表示 owner, group, others.

chmod ### file | directory
  1. If the read permission should be set, add 4
  2. If the write permission should be set, add 2
  3. If the execute permission should be set, add 1
特殊权限

使用4位, 第一位代表特殊权限, 后面3位表示 owner, group, others.
SUID = 4
SGID = 2
Sticky = 1

chmod 2770 test.txt