Eric 发布的文章

查看一个socket的开始时间

问题起源

有一个服务分布在3个数据中心, 有一个统一的FQDN(svc1.vip.tianxioahui.com)让别人去访问它, 同时每个数据中心都有一个单独的IP来访问该服务, 也就是说这个域名能解析到3个不同的IP. 有一次某个数据中心的该服务出现了点问题, 于是对应的IP被从DNS服务器上临时去掉了. 当问题修复之后, DNS服务器上恢复对应的IP, 该数据中心的流量却没有恢复, 还是流向了其它2个数据中心. 问题到底出现在哪呢?

初步调查

该服务的使用方是我们内部的一个服务, 于是我们首先检查客户端的DNS解析. 同时, 我们的DNS服务器会根据查询者所在的数据中心首先返回本数据中心的IP, 当没有对应数据中心的IP之后, 才会返回不同的数据中心的IP.

正常情况下的服务访问情况:
Screen Shot 2023-02-10 at 20.47.20.png

当 DC-C 的服务出问题的时候:
Screen Shot 2023-02-10 at 20.49.26.png

于是我们登录之前出问题数据中心某个客户端, 查询 DNS 解析情况, 发现查询的结果就是已经恢复的数据中心的IP, 也就是DNS解析已经完全恢复.

于是通过 ss 去查看现存的tcp连接, 发现确实是连接到其它数据中心的.

为什么恢复后还连接到其它数据中心?

首先经过连续多次测试, DNS解析的结果仍然是当前数据中心的IP, 所以DNS解析完全是正常的. 所以怀疑这个连接是之前早就建立好的, 并且一直 keep-alive, 所以一直还是正常在用的.

如何确定这个连接已经存活多久了?

我们想通过最简单的方式: ss 命令, 看看它是否提供了一个tcp连接的开始时间man ss答案是, 并没有.

于是, 我们找到这篇: https://superuser.com/questions/565991/how-to-determine-the-socket-connection-up-time-on-linux

这个问题的最高评答案里面给的过程是:

  1. 找到这个tcp连接所在的进程和proc文件系统的inode
  2. 然后使用stat命令去查看文件的最后访问时间(回答里用的是: query the file access time of the symbolic link)

然而这个并不是socket uptime, 而是socket最后的访问时间, 可是那个shell函数又命名为suptime, 真是对不上.

但是, 他使用stat访问文件的创建时间是对的

文件创建时间

通常情况下,使用stat都能查看文件的创建时间, 比如:

supra@suprabox:~$ stat ./bootstrap.sh
  File: ./bootstrap.sh
  Size: 4162          Blocks: 16         IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 10763859    Links: 1
Access: (0775/-rwxrwxr-x)  Uid: ( 1000/   supra)   Gid: ( 1000/   supra)
Access: 2022-11-22 19:06:03.507882931 -0800
Modify: 2022-11-22 19:05:59.487695031 -0800
Change: 2022-11-22 19:05:59.491695212 -0800
 Birth: 2022-11-22 19:05:59.487695031 -0800

上面最后一行 Birth 就是它的创建时间.

查看socket inode的创建时间

其实上面输出中的 Birth 是在 inode 的元数据里的, 所以, 如果我们按照那个答案的方法去访问socket虚拟文件的inode是不是就找到了socket 的创建时间?

sudo ss -t -p -e
State    Recv-Q  Send-Q   Local Address:Port     Peer Address:Port      Process
ESTAB    0       0        10.249.64.103:ssh      10.226.199.52:61340    users:(("sshd",pid=812231,fd=4),("sshd",pid=812101,fd=4)) timer:(keepalive,54min,0) ino:2211652 sk:5063 cgroup:/system.slice/ssh.service <->

ss命令通过制定 -p 参数, 现实进程号和fd信息(pid=812101,fd=4), 通过-e参数, 显示inode 信息(ino:2211652). 于是, 我们可以查看这个虚拟文件的inode 信息了

supra@suprabox:~$ sudo stat /proc/812101/fd/4
  File: /proc/812101/fd/4 -> socket:[2211652]
  Size: 64            Blocks: 0          IO Block: 1024   symbolic link
Device: 17h/23d    Inode: 2228948     Links: 1
Access: (0700/lrwx------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2023-02-11 01:51:35.510079439 -0800
Modify: 2023-02-11 01:51:26.442196746 -0800
Change: 2023-02-11 01:51:26.442196746 -0800
 Birth: -

可是Birth 这行是空.
文件的符号链接指向 socket:[2211652], 这个方括号里面的值就是我们通过ss查看得到的 inode id.

同时, 我们去查看 cat /proc/net/tcp 也能看到一样的行

最终, 也没找到一个可以查看socket创建时间的命令.

导出 docker container 运行时快照并从快照运行

之前讲 docker container 生命周期的时候, 看到 pause container 命令, 突然想到是否能够对一个正在运行的container 做个快照, 把全部文件和内存运行数据都dump出来, 之后根据快照随时继续运行呢?

于是 Google 了一把, 发现也有不少人有同样的疑问, 很多人的回答都提到了 docker commit, 不过这不是我想要的. docker commit 只是把改动的文件内容覆盖到原来的 image, 重新做了一个 image, 然而丢失了正在运行的进程的全部数据, 比如内存和寄存器里面的数据. 而我期望的是能保存这些进程的运行时信息, 等继续运行的时候, 能在进程应该继续执行的下一个指令的地方继续运行.

那么到底有没有神奇的方法, 能做到期望的行为呢? 其实 Docker 已经考虑了这个需求, 做为实验命令, 只是还没有正式发布, 那就是通过docker checkpoint 子命令. 官方链接.

它的主要过程就是: 当一个container 正在运行时, 通过 docker checkpoint create dump一个包含container文件系统和运行时内存数据的快照, 并把它保存到磁盘. 之后通过docker start --checkpoint checkpoint_id container_name 来继续运行. 由于我们保存快照到文件系统, 所以可以复制很多份, 之后可以随时启动它, 随时查看当时的状态.

下面是官方的一个例子:

  1. 安装 CRIU 依赖, Docker 其实是依赖这个包才能完成container冻结和恢复的.

    supra@suprabox:~$ sudo apt install criu -y
  2. 首先启动这个每隔一秒钟打印一个数字的container, 名字是cr

    supra@suprabox:~$ docker run --security-opt=seccomp:unconfined --name cr -d busybox /bin/sh -c 'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done'
    0e09ac6f369f965af18e429ec78fcab8875610fdfd69313d6c7ecb0b86e61d7
  3. 查看打印的数据

    supra@suprabox:~$ docker logs -f cr
    0
    1
    2
  4. 生成快照 --leave-running 表示快照生成完继续运行 --checkpoint-dir表示要保存的文件夹

    supra@suprabox:~$ docker checkpoint create cr checkpoint1 --leave-running --checkpoint-dir /tmp/
    checkpoint1
  5. 查看之前的container 还在运行, 查看我们保存快照的文件夹, 发现里面各种以 .img 结尾的文件

    supra@suprabox:~$ docker ps -l
    CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS     NAMES
    ecb0b86e61d7   busybox   "/bin/sh -c 'i=0; wh…"   10 minutes ago   Up 10 minutes             cr
    
    supra@suprabox:~$ sudo ls -alh /tmp/checkpoint1/
    total 224K
    drwx------  2 root root 4.0K Jan 28 18:40 .
    drwxrwxrwt 22 root root  12K Jan 28 18:42 ..
    -rw-r--r--  1 root root  356 Jan 28 18:40 cgroup.img
    -rw-r--r--  1 root root 1.8K Jan 28 18:40 core-1.img
    -rw-r--r--  1 root root 1.8K Jan 28 18:40 core-1034.img
    -rw-------  1 root root   45 Jan 28 18:40 descriptors.json
    -rw-r--r--  1 root root   44 Jan 28 18:40 fdinfo-2.img
    -rw-r--r--  1 root root   44 Jan 28 18:40 fdinfo-3.img
  6. 恢复运行 - 各种失败 - 没有达到预期
    去掉各种参数, 按照官方方法, 在 Ubuntu 22.04 上照样不能成功. 失败

    supra@suprabox:~$ docker start --checkpoint  checkpoint1 cr
    Error response from daemon: OCI runtime restore failed: criu failed: type NOTIFY errno 0
    log file: 
     
     /run/containerd/io.containerd.runtime.v2.task/moby/63263d8b31317dbe1f6cf19eac2198f3e078a12f9d872e4e134eae1a4f465d49/work/restore.log: unknown
    
    supra@suprabox:~$ sudo tail -f 
     
     /run/containerd/io.containerd.runtime.v2.task/moby/63263d8b31317dbe1f6cf19eac2198f3e078a12f9d872e4e134eae1a4f465d49/work/restore.log
    (00.274155) pie: 52:     mmap(0x7fffb69f7000 -> 0x7fffb69fb000, 0x3 0x32 -1)
    (00.274162) pie: 52:     mmap(0x7fffb69fb000 -> 0x7fffb69fd000, 0x7 0x32 -1)
    (00.274170) pie: 52: Preadv 0x1612000:8192... (3 iovs)
    (00.274193) pie: 52: `- returned 20480
    (00.274198) pie: 52:    `- skip pagemap
    (00.274203) pie: 52:    `- skip pagemap
    (00.274208) pie: 52:    `- skip pagemap
    (00.488504)      1: Error (criu/cr-restore.c:1480): 52 killed by signal 11: Segmentation fault
    (00.488867) mnt: Switching to new ns to clean ghosts
    (00.489104) Error (criu/cr-restore.c:2447): Restoring FAILED.

官方 CRIU 最新版本是 3.17.1, 我本地版本是 3.16.1-2 不知道是不是有问题.

upra@suprabox:~$ apt show criu
Package: criu
Version: 3.16.1-2

但是这篇 blog 在2015年当时已经成功了: https://kubernetes.io/blog/2015/07/how-did-quake-demo-from-dockercon-work/

不论如何, 想要做快照, 并且从快照恢复一个进程, 已经有很多研究了, 更多看这里: https://criu.org/

http 客户端常见的三种 timeout

微服务的开发部署方式已经在业界广泛被采用, 许多微小的服务共同组成一个完整的软件系统, 对外提供有价值的商业服务. 一个完整的系统组合起来, 离不开内部各个微服务之间的依赖调用, 于是微服务之间形成了一个密集的调用网. 因此我们在微服务的代码中看到许多的 http 客户端到服务端的调用, 而且当应用出问题时候, 我们经常看到各种 http client 相关的各种问题. 这些问题当中, 尤其与 timeout 相关的问题特别多, 本系列的几篇文章将以典型的 Liunx 上 Java 的 http client 为例, 来详细讨论常见的 http 客户端的3种timeout 问题.

connect timeout 和 read timeout

首先, 我们看一个最简单的服务间的调用过程:

  1. 客户端发起 tcp 连接, 服务端应答, tcp 连接建立;
  2. 客户端发出 http request;
  3. 服务端收到请求, 处理请求, 发回 http response;
  4. 客户端收到应答, 关闭 tcp 连接(open issue)

在上面的过程中, 客户端可能会面临2种timeout.

  1. 在上面的第(1)步建立 tcp 连接的过程中, 如果客户端发出 syn, 迟迟收不到服务端的应答, 会产生 connect timeout;
  2. 在上面的过程中, 如果客户端发出了请求, 迟迟收不到 http response, 或收不到完整的response, 则可能产生 read timeout;

从连接池租借连接 timeout

如果某个服务调用的频率非常高, 那么按照上面的流程, 每次都新建一个 tcp 连接就显得非常消耗系统资源和时间. 于是一个可以优化的选项就是在客户端建立连接池, 每次需要发送请求的时候, 从连接池租借出一个连接. 若连接池有现成的连接, 则可以立即借来用. 若没有, 才去新建. 当连接用完之后, 不是关闭连接, 而是返还给连接池以备下次使用. 一般情况下, 连接池不可能有无限的连接, 通常设置一个最大值, 当被借出的连接达到最大值之后, 后续的请求通常要等待空闲的连接可用 (这里介绍的连接池是一个比较简单的模型, 查看专门的书籍去查看更多关于连接的模型和策略).
所以, 从连接池去租借一个可用的连接, 并不一定每次都能立即拿到, 所以有可能迟迟拿不到连接, 所以这里会出现从连接池租借连接的等待 timeout.

所以, 一个客户端的请求会出现3种可能的timeout. 如下图:
timeouts.png

针对上面的3种timeout, 在我们常见的代码中, 它们的默认值是什么呢? 我们应该如何设置这些值呢? 如果设置不合理, 又可能出现那些问题呢?

docker container 的生命周期

当我们创建一个 container 的时候, 它的生命过程中都会经历哪几个阶段呢? 有时候明明一个 container 已经死了, 我们去创建一个同名 container 的时候, 它竟然说有一个同名的 container 存在? 明白了 container 的生命周期, 就能理解了.

container 的生命阶段

一个 docker container 从创建到彻底消亡, 可能会经过的阶段:
docker_container_lifecycle.png

  1. created: container 被创建, 但是没有被启动
  2. running: container 被启动, 里面的进程在运行
  3. paused: container 里的所有进程被挂起, 暂停运行
  4. stopped: container 里面的进程被杀死, container 还存在
  5. deleted: container 被彻底删除

下面我们就以 nginx 做container 为例来说明这个过程.

create 创建 container

下面创建一个 nginx container, 并使用 ps -l (--latest) 来查看, 状态是 Created.

supra@suprabox:~$ docker create --name nginxServer nginx
ac301e1f4f3f3ba5631ff5d7f3276d1544610229a9e3727dac6d5f66181bfe05

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS    PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   1 minutes ago   Created             nginxServer

start 启动 container

通过start子命令启动 container, 然后通过ps子命令查看状态. 通过exec子命令写一句话到/text.txt文件, 并且查看写入内容.

supra@suprabox:~$ docker start nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS         PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   25 minutes ago   Up 5 seconds   80/tcp    nginxServer

supra@suprabox:~$ docker exec nginxServer bash -c 'echo "this is a test" > /test.txt'
supra@suprabox:~$ docker exec nginxServer cat  /test.txt
this is a test

pause 暂停 container

可以看到经过 pause 之后, 状态变为 Paused. 如果尝试进入这个 container exec, 这个时候会报错.

supra@suprabox:~$ docker pause nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                  PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   29 minutes ago   Up 4 minutes (Paused)   80/tcp    nginxServer

supra@suprabox:~$ docker exec -it nginxServer bash
Error response from daemon: Container nginxServer is paused, unpause the container before exec

unpause 继续 container

supra@suprabox:~$ docker unpause nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED             STATUS          PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   About an hour ago   Up 44 minutes   80/tcp    nginxServer

stop 停止 container

stop 之后, 通过docker ps -l 可以看到状态变为 Exited, 这个时候通过docker logs 仍旧能看到之前的log, 并且可以看到主进程被杀死的日志. 但是通过docker exec 已经无法进入 container.

supra@suprabox:~$ docker stop nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED             STATUS                     PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   About an hour ago   Exited (0) 8 seconds ago             nginxServer

supra@suprabox:~$ docker logs -f nginxServer
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
...
2023/01/27 13:35:52 [notice] 1#1: signal 17 (SIGCHLD) received from 32
2023/01/27 13:35:52 [notice] 1#1: worker process 32 exited with code 0
2023/01/27 13:35:52 [notice] 1#1: worker process 37 exited with code 0
2023/01/27 13:35:52 [notice] 1#1: exit

supra@suprabox:~$ docker exec -it nginxServer bash
Error response from daemon: Container ac301e1f4f3f3ba5631ff5d7f3276d1544610229a9e3727dac6d5f66181bfe05 is not running

start 重新启动 stop 的 container

通过 start 子命令, 可以重新启动 stop 的container, 通过docker ps -l 可以看到它的状态是刚起来4秒. 这时候去查看之前写入的 /test.txt 文件, 内容还在. 但是这时候里面的进程都是新启动的, 不说 stop 之前的那个(批)进程了.

supra@suprabox:~$ docker start nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED             STATUS         PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   About an hour ago   Up 4 seconds   80/tcp    nginxServer

supra@suprabox:~$ docker exec nginxServer cat  /test.txt
this is a test

rm 彻底删除container

可以通过rm子命令删除任何状态的container. 如果一个container 是 stopped 或 created 状态的, 可以直接rm, 否则就必须通过 -f 强制删除.

docker rm nginxServer
docker rm -f nginxServer

run 创建同时启动container

可以通过 run 子命令, 在创建container的同时, 启动container, 相当于同时执行了create和start.

supra@suprabox:~$ docker run --name nginxServer1 --rm  nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/

kill 杀掉一个运行的container

通过kill 杀掉一个运行的container 之后, container 进入 stopped 的状态.

supra@suprabox:~$ docker kill nginxServer
nginxServer

supra@suprabox:~$ docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED       STATUS                        PORTS     NAMES
ac301e1f4f3f   nginx     "/docker-entrypoint.…"   2 hours ago   Exited (137) 18 seconds ago             nginxServer

虚拟以太网卡 veth

这是一块真实的台式机网卡, 左边是一个插网线的接口, 右边是电路板, 电路板上哪些金色的地方就插入主板的接口. 于是外部流量就通过网线, 进入网卡, 然后流入计算机, 计算机内部数据则通过相反的方向流出到外部.
网卡.png

虚拟以太网卡, 就是一个通过软件模拟的网卡设备, 它能模拟网卡的功能把数据传出去, 接收进来的数据, 在操作系统看来, 它就是一块功能完备的网卡. 这个虚拟的网卡一头连着操作系统的kernel, 另外一头呢? 它有没有网线可以插拔, 所以它只能连向另外一个网卡, 所以虚拟出来的网卡都是成对出现的, 它们不能插网线, 所以只能2个网卡之间相互通信, 就好比2个机器之间通过一条直连的网线连在了一起. 就好比下图这样:
vethPair.png

但是这个 veth0 和 veth1 对必然在同一台机器上, 不可能跨域两台机器还能连着. 不过 veth0 和 veth1 可以属于同一台机器上的不同VM, container 或者不同的 netns.

下面演示如何创建虚拟网卡:

# 新建之前查看已有的
$ ip link show

$ sudo ip link add  name veth1 type veth #新建link

$ ip link show # 再次查看
1994: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether fe:99:a3:6e:f7:b0 brd ff:ff:ff:ff:ff:ff
1995: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 56:64:02:ac:de:26 brd ff:ff:ff:ff:ff:ff

可以看到新加出来的 veth0 和 veth1 这对. 我们给的veth 的名字是 veth1, 而系统默认给出了配对的名字是 veth0. 最前面的数字 1994 和 1995 是系统的一个索引值, 称之为 IDX. @后面的名字是成对的另外一边点名字. <> 里面是支持的一些功能, 后边是一些相关参数, 这些参数可以在新建时设置, 也可以使用默认值. 下面一行代表 link 类型: ether(以太网), link 层地址(MAC 地址) 和 广播地址.

当然, 我们可以新建的时候, 直接给出这一对的名字, 比如:

$ sudo ip link add  name veth2 type veth peer name veth3

$ ip link show
1996: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:39:5b:12:84:49 brd ff:ff:ff:ff:ff:ff
1997: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 8a:f0:21:23:20:52 brd ff:ff:ff:ff:ff:ff

因为它们是默认一对出现的, 如果一方下线(down) 则另外一方默认就下线了:

$ sudo ip link set veth3  down

$ ip link
1996: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:39:5b:12:84:49 brd ff:ff:ff:ff:ff:ff
1997: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 8a:f0:21:23:20:52 brd ff:ff:ff:ff:ff:ff

可以看到, 我们这里新建的 veth 都只有link 层地址, 没有 IP 地址, 我们可以给它们赋予IP 地址.

$ ip addr show #设置 IP 地址之前先查看一下

$ sudo ip address add 192.168.88.1/16 dev veth3
$ sudo ip address add 192.168.88.2/16 dev veth2

$ ip addr show #设置完再次查看 
1996: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 12:39:5b:12:84:49 brd ff:ff:ff:ff:ff:ff
    inet 192.168.88.1/16 scope global veth3
       valid_lft forever preferred_lft forever
1997: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 8a:f0:21:23:20:52 brd ff:ff:ff:ff:ff:ff
    inet 192.168.88.2/16 scope global veth2
       valid_lft forever preferred_lft forever

$ sudo ip link set veth3 up #up