记一次 splice 导致 io.Copy 阻塞的排查过程

简而言之,net.TCPConn 的 ReadFrom 零拷贝实现 splice1.21.0 - 1.21.4 删除了 SPLICE_F_NONBLOCK 参数,导致在 CentOS7.2(内核版本 3.10.0) 上 splice 被阻塞。

相关的 issuehttps://github.com/golang/go/issues/59041

这个问题在 1.21.5 中被修复,commithttps://github.com/yunginnanet/go/commit/35afad885d5e046a4a14643b5b530b128ca953de

背景

由于环境的问题,需要有一个 TCP 的代理,之前一直用 ncat -vl 10022 -k -c 'ncat -nv 127.0.0.1 22' 方式将 10022 端口的流量代理至 127.0.0.1:22,但是 ncat 是一个连接一个进程,如果要做短连接压测的,代理会成为瓶颈。

所以决定换个代理的软件,因为 Go 写一个代理特别简单,十行代码就能实现一个性能不错的服务,那就直接自己写一个。

package main

import (
"flag"
"fmt"
"io"
"net"
"sync" "github.com/sirupsen/logrus"
) func main() {
f := flag.String("from", "", "source addr")
t := flag.String("to", "", "dest addr")
flag.Parse() if *f == "" || *t == "" {
fmt.Println("Invalid from/to address")
return
}
logrus.WithFields(logrus.Fields{"from": *f, "to": *t}).Info("Setup proxy server") lis, err := net.Listen("tcp", *f)
if err != nil {
panic(err)
}
logrus.WithField("addr", lis.Addr()).Info("Listen on") for {
conn, err := lis.Accept()
if err != nil {
panic(err)
}
go handleConn(conn, *t)
}
} func handleConn(uConn net.Conn, to string) {
logrus.WithField("addr", uConn.RemoteAddr()).Info("New conn")
defer uConn.Close() rConn, err := net.Dial("tcp", to)
if err != nil {
logrus.WithError(err).Error("Fail to net.DialTCP")
return
}
logrus.WithField("local", rConn.LocalAddr()).Info("Start proxy conn") wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(uConn, rConn)
rConn.Close()
uConn.Close()
}()
go func() {
defer wg.Done()
io.Copy(rConn, uConn)
uConn.Close()
rConn.Close()
}()
wg.Wait()
}

编译操作系统为 Debian12,Go 版本为 1.21.1

因为默认路由的原因,我把这个服务部署在了一个 CentOS7.2 的虚拟机里面,压测发现QPS总是上不去。

用 tcpdump 抓包定位到是这边的代理程序有问题,流量没有被正确的进行转发。

为避免出现敏感数据,用下面的图来做模拟,在 A 使用 scpB 发送文件,中间经过了个我们写的服务 PROXY

+----------------+      +-------------------------------+
| (A) Debian12 | | (B) CentOS7.2 |
| | <--> | 192.168.32.251:10022 |
| 192.168.32.251 | | └─> PROXY |
| | | └─> 127.0.0.1:22 |
+----------------+ +-------------------------------+ # 生成一个大的文件
# dd if=/dev/zero of=/tmp/1.txt bs=1M count=1024
# 使用命令模拟压测
# scp -P 10022 /tmp/1.txt root@192.168.32.245:/tmp/

排查

ps 看到这个进程还在运行,所以不是进程退出导致的。

top 观察进程 CPU 占用也不高,所以不是代码写出死循环来了。

由于程序没有加日志,通过 strace -p $(pidof PROXY) 来分析一下当前哪些系统调用在执行,看起来是 epoll_pwait 没有就绪事件返回。

[pid 26877] splice(14, NULL, 18, NULL, 1048576, 0) = -1 EAGAIN (Resource temporarily unavailable)
[pid 26790] epoll_pwait(5, <unfinished ...>
[pid 26788] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
[pid 26790] <... epoll_pwait resumed>[], 128, 0, NULL, 0) = 0
[pid 26877] epoll_pwait(5, <unfinished ...>
[pid 26790] epoll_pwait(5, <unfinished ...>
[pid 26877] <... epoll_pwait resumed>[], 128, 0, NULL, 0) = 0
[pid 26877] futex(0xc000040d48, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>
[pid 26788] <... nanosleep resumed>NULL) = 0
[pid 26788] futex(0x5ea8a0, FUTEX_WAIT_PRIVATE, 0, {tv_sec=60, tv_nsec=0} <unfinished ...>
[pid 26790] <... epoll_pwait resumed>[{EPOLLIN|EPOLLOUT, {u32=2345140225, u64=9221451948300435457}}], 128, -1, NULL, 0) = 1
[pid 26790] epoll_pwait(5, [], 128, 0, NULL, 0) = 0
[pid 26790] epoll_pwait(5, [{EPOLLIN|EPOLLOUT, {u32=2345140225, u64=9221451948300435457}}], 128, -1, NULL, 0) = 1
多条 epoll_pwait 省略

看看连接缓冲区里面有没有数据 netstat -ntp | grep 10022,在接受缓冲区内还有 1666120 个字节的数据没有被读出

tcp6  1666120      0 192.168.32.245:10022    192.168.32.251:49440    ESTABLISHED 26787/PROXY

当时想着看看重启能不能复现,在重启之前先 kill -3 把堆栈打印出来,拿到了一个关键的栈信息。

goroutine 19 [syscall]:
syscall.Syscall6(0x7ff92db08be8?, 0xc000068c88?, 0x45fca5?, 0xc000068c98?, 0x48ed3c?, 0xc000068cb0?, 0x48eea7?)
/usr/local/go1.21/src/syscall/syscall_linux.go:91 +0x30 fp=0xc000068c60 sp=0xc000068bd8 pc=0x481b50
syscall.Splice(0xc000102000?, 0xc000068d08?, 0x0?, 0x4e70c0?, 0x4e70c0?, 0xc000068d20?)
/usr/local/go1.21/src/syscall/zsyscall_linux_amd64.go:1356 +0x45 fp=0xc000068cc0 sp=0xc000068c60 pc=0x480d05
internal/poll.splice(...)
/usr/local/go1.21/src/internal/poll/splice_linux.go:155
internal/poll.spliceDrain(0xc000102100?, 0xc000102000, 0x5a800?)
/usr/local/go1.21/src/internal/poll/splice_linux.go:92 +0x185 fp=0xc000068d68 sp=0xc000068cc0 pc=0x4917c5
internal/poll.Splice(0x0?, 0x0?, 0x7fffffffffffffff)
/usr/local/go1.21/src/internal/poll/splice_linux.go:42 +0x173 fp=0xc000068e00 sp=0xc000068d68 pc=0x491413
net.splice(0x0?, {0x53bca8?, 0xc000106000?})
/usr/local/go1.21/src/net/splice_linux.go:39 +0xdf fp=0xc000068e60 sp=0xc000068e00 pc=0x4cc29f
net.(*TCPConn).readFrom(0xc000106008, {0x53bca8, 0xc000106000})
/usr/local/go1.21/src/net/tcpsock_posix.go:48 +0x28 fp=0xc000068e90 sp=0xc000068e60 pc=0x4cd0c8
net.(*TCPConn).ReadFrom(0xc000106008, {0x53bca8?, 0xc000106000?})
/usr/local/go1.21/src/net/tcpsock.go:130 +0x30 fp=0xc000068ed0 sp=0xc000068e90 pc=0x4cc770
io.copyBuffer({0x53bd68, 0xc000106008}, {0x53bca8, 0xc000106000}, {0x0, 0x0, 0x0})
/usr/local/go1.21/src/io/io.go:416 +0x147 fp=0xc000068f50 sp=0xc000068ed0 pc=0x47d587
io.Copy(...)
/usr/local/go1.21/src/io/io.go:389
main.handleConn.func2()
/home/devel/demo/app/demo/main.go:73 +0xb2 fp=0xc000068fe0 sp=0xc000068f50 pc=0x4db672
runtime.goexit()
/usr/local/go1.21/src/runtime/asm_amd64.s:1650 +0x1 fp=0xc000068fe8 sp=0xc000068fe0 pc=0x464641
created by main.handleConn in goroutine 17
/home/devel/demo/app/demo/main.go:71 +0x368

分析看到在 io.Copy 这条路线有问题,先看看 io.Copy 的源码

分析 io.Copy

io.Copy 内部有这么一段代码,优先于 read/write 调用,上面的堆栈打印看起来也是这个 ReadFrom 里面有问题。

if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(src)
}

OK 先跳过这个 ReadFrom 看看能不能行呢,于是把 io.Copy 里面的 WriteTo/ReadFrom 注释,并且直接放到外面来,使用一般的 read/write 调用。

编译运行,可行!!!

那么问题就只能在这个 ReadFrom 里面了,照着上面的堆栈,一路追到了 poll.Splice 内,但是之前没有用过 splice 这个函数,只知道是一个零拷贝相关的函数。好吧,Go 在这里还做了一些优化。

那看来还是得研究一下,这个 splice 系统调用。

分析 poll.Splice

在这之前先搜索了一些文档看了一下,这个 splice文档写的相当好,很快就能够理解。

文章里面的的这张图清晰的描述了两次 splice 就能通过 pipe 在内核就将数据发送出去,没有把数据从内核空间拷贝至用户空间。

为了减少语言的干扰,使用 C 照着 poll.Splice 重写了一遍,代码如下。在 splice_readfrom 内部,每次循环调用两次 splice,一次将源 sockfd 的数据放至 pipe 中,一次将 pipe 中的数据写入目的 sockfd 中。

#define _GNU_SOURCE 1
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <netinet/in.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h> #include <thread> static ssize_t splice_drain(int fd, int pipefd, size_t max) {
while (1) {
ssize_t n = splice(fd, NULL, pipefd, NULL, max, 0);
if (n >= 0)
return n; // error handle
if (errno == EINTR)
continue;
else if (errno != EAGAIN)
return -1;
}
} static ssize_t splice_pump(int pipefd, int fd, size_t in_pipe) {
ssize_t written = 0;
while (in_pipe > 0) {
ssize_t n = splice(pipefd, NULL, fd, NULL, in_pipe, 0);
if (n >= 0) {
in_pipe -= n;
written += n;
continue;
} if (errno != EAGAIN)
return -1;
}
return written;
} static const size_t kMaxSpliceSize = 1 << 20; ssize_t splice_readfrom(int dstfd, int srcfd) {
int pipefd[2];
if (pipe2(pipefd, 0) < 0)
return -1; ssize_t written = 0;
ssize_t remain = INT64_MAX;
while (remain > 0) {
size_t max = kMaxSpliceSize;
if (max > (size_t)remain)
max = remain; ssize_t in_pipe = splice_drain(srcfd, pipefd[1], max);
if (in_pipe < 0)
return -1;
else if (in_pipe == 0)
break; ssize_t n = splice_pump(pipefd[0], dstfd, in_pipe);
if (n > 0) {
remain -= n;
written += n;
}
}
close(pipefd[0]);
close(pipefd[1]);
return written;
} int main(int argc, char **argv) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Fail to socket");
return -1;
} int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt)); fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, NULL) | O_NONBLOCK); struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10022);
addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("Fail to bind");
return -1;
} if (listen(sockfd, 10) < 0) {
perror("Fail to listen");
return -1;
}
printf("listen on\n"); int timeout = 3000;
struct pollfd fds = {sockfd};
fds.events |= POLLIN; while (1) {
int ret = poll(&fds, 1, timeout);
if (ret > 0) {
struct sockaddr_in in;
socklen_t len = sizeof(in);
int connfd = accept(sockfd, (struct sockaddr *)&in, &len);
if (connfd < 0) {
perror("Fail to accept");
return -1;
} fcntl(connfd, F_SETFL, fcntl(connfd, F_GETFL, NULL) | O_NONBLOCK); std::thread t(
[](struct sockaddr_in addr, int u_connfd) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Fail to socket");
return;
} struct sockaddr_in dst_addr;
dst_addr.sin_family = AF_INET;
dst_addr.sin_port = htons(10022);
dst_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); if (connect(sockfd, (struct sockaddr *)&dst_addr,
sizeof(dst_addr)) < 0) {
perror("Fail to connect");
return;
} char dst_txt[INET_ADDRSTRLEN];
char src_txt[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &addr.sin_addr, src_txt, sizeof(src_txt));
printf("New conn from %s:%d\n", src_txt, ntohs(addr.sin_port)); std::thread t1([&]() { splice_readfrom(sockfd, u_connfd); });
std::thread t2([&]() { splice_readfrom(u_connfd, sockfd); });
t1.join();
t2.join(); close(sockfd);
close(u_connfd);
},
in, connfd);
t.detach();
}
} return 0;
}

测试下来,和 Go 版本表现一样,也是被阻塞,不过现在问题就更清晰一些,splice 的使用有问题。

于是仔细看了一下文档,里面有一个参数 SPLICE_F_NONBLOCK,要不加上试一下看看,加上之后程序是正常运行的。

所以会是这个参数的问题?在 Go 的实现里面,spliceflags 参数是为 0 的,也就是意味着是没有设置为非阻塞状态的。

想到我们之前的代理程序都没有出现这个情况,难道是 Go 版本的原因?于是使用 Go1.18 对 PROXY 进行编译运行,正常运行!

看了两个版本的实现,果然 Go1.18 是含有这个 SPLICE_F_NONBLOCK 参数的,在之后的版本内被删除了;继续搜索,发现了有人提了个上面的 issue。

对代码追踪发现 受影响版本为 1.21.0 - 1.21.4

扩展分析

issue 里面 Go 的开发者说所有的 case 都正常能跑过,所以把这个参数删除了。既然开发者测试没有问题,但是实际使用又有问题,那就有可能是环境不一致导致的。

分析未在不同内核上splice表现不一致

在上面的排查过程中,我还把 PROXY(Go1.21.1) 放到 A 中运行,代理至 192.168.32.245:22 上,表现也是正常的。经过测试,io.Copy 在不同的系统上的影响如下:

Kernel\Go 1.18.0 1.21.1
3.10 正常 不正常
6.1 正常 正常

1.18 是没有 BUG 的版本,也就是增加了 SPLICE_F_NONBLOCK 参数。那为何 1.21.1 版本没有增加这个参数的可以在 6.1 的内核上运行呢。

没有很好的头绪,难道是 pipe 导致的问题吗,pipe 太小了?于是调整 pipe 大小

fcntl(pipefd[0], F_SETPIPE_SZ, 1 << 20);
fcntl(pipefd[1], F_SETPIPE_SZ, 1 << 20);

使用 Go1.21.1 版本进行编译,并且进行测试,结果如下:

Kernel\Go 1.21.1
3.10 正常
6.1 正常

pipe 太小,那测试数据小于默认大小 65536 的看看会不会有问题

dd if=/dev/zero of=/tmp/1.txt bs=1 count=65536

测试结果如下:

测试数据大小 测试结果
65536 不正常
32768 不正常
25000 不正常
16384 正常

splice 还有一个参数 len,为从 fd_in 到 pipe_w 中的字节数,如果我减少这个大小,那么结果会如何。测试下来 和调整 pipe 大小带来的结果相同

splice 在不同内核上表现的结果不同这个问题,可以缩小一些排查的范围了:和 pipe 相关

不同内核的 splice 实现

看代码之前确认要关注的点:在哪里存在阻塞的动作

splice 实现位于 fs/splice.c 中,下面的代码取自 kernel-6.1(3.10 的内核代码也相似,主体逻辑没有变化)

SYSCALL_DEFINE6(splice, ...) // fs/splice.c
-> __do_splice
-> do_splice
-> splice_file_to_pipe // 将 sockfd 的数据传输至 pipe 中,走这条路径
-> do_splice_to
-> tcp_splice_read (in->f_op->splice_read) // net/ipv4/tcp.c
-> __tcp_splice_read
-> tcp_read_sock
-> tcp_splice_data_recv
-> skb_splice_bits // net/core/ipv4/skbuff.c
-> splice_to_pipe // fs/splice.c

经过 TCP 的读取,兜兜转转又回到 fs/splice.c 中。

kernel-6.1 的实现

在 kernel-6.1 的实现中,spclie_to_pipe 的实现没有阻塞

ssize_t splice_to_pipe(struct pipe_inode_info *pipe,
struct splice_pipe_desc *spd)
{
// ....
while (!pipe_full(head, tail, pipe->max_usage)) {
struct pipe_buffer *buf = &pipe->bufs[head & mask]; buf->page = spd->pages[page_nr];
buf->offset = spd->partial[page_nr].offset;
buf->len = spd->partial[page_nr].len;
buf->private = spd->partial[page_nr].private;
buf->ops = spd->ops;
buf->flags = 0; head++;
pipe->head = head;
page_nr++;
ret += buf->len; if (!--spd->nr_pages)
break;
}
if (!ret)
ret = -EAGAIN; out:
while (page_nr < spd_pages)
spd->spd_release(spd, page_nr++); return ret;
}

向上回溯,在 splice_file_to_pipe 中,wait_for_space 中如果 pipe 满了则进行等待 pipe_wait_writable(pipe)

long splice_file_to_pipe(struct file *in,
struct pipe_inode_info *opipe,
loff_t *offset,
size_t len, unsigned int flags)
{
long ret; pipe_lock(opipe);
ret = wait_for_space(opipe, flags);
if (!ret)
ret = do_splice_to(in, offset, opipe, len, flags);
pipe_unlock(opipe);
if (ret > 0)
wakeup_pipe_readers(opipe);
return ret;
} static int wait_for_space(struct pipe_inode_info *pipe, unsigned flags)
{
for (;;) {
if (unlikely(!pipe->readers)) {
send_sig(SIGPIPE, current, 0);
return -EPIPE;
}
if (!pipe_full(pipe->head, pipe->tail, pipe->max_usage))
return 0;
if (flags & SPLICE_F_NONBLOCK)
return -EAGAIN;
if (signal_pending(current))
return -ERESTARTSYS;
pipe_wait_writable(pipe);
}
}

调整测试代码,对 pipe 只生产而不不消费数据。

ssize_t splice_readfrom(int dstfd, int srcfd) {
...
ssize_t in_pipe = splice_drain(srcfd, pipefd[1], max); sleep(1);
written += in_pipe;
printf("+%ld written=%ld\n", in_pipe, written);
continue; ssize_t n = splice_pump(pipefd[0], dstfd, in_pipe);
}

为避免 ssh 的元数据干扰,不再使用 sshd 127.0.0.1:22 作为最后点,转而写了一个 io.Discard 的 Go 服务。

测试客户端为 ncat -nv 192.168.32.251 10022 < /tmp/1.txt

在 Debian12(kernel-6.1) 上进行测试,结果如下

+65509 written=65509
+57344 written=122853
+49152 written=172005
+36864 written=208869
+28672 written=237541
+20480 written=258021
+16384 written=274405
+8192 written=282597
+4096 written=286693
// 之后阻塞

pipe 的大小为 65536(PAGE_SIZE * 16),但是写入的数据大于了 pipe 的缓冲区后,还能够继续写入,这点和可能和 skbuff/pipe 的 PAGE 有关,这里先跳过,直接测试一下在 CentOS7.2 上表现如何,结果直接阻塞,第一个 splice 都没有返回,好吧看看代码。

kernel-3.10 的实现

同样找到关键的 splice_to_pipe 函数

ssize_t splice_to_pipe(struct pipe_inode_info *pipe, struct splice_pipe_desc *spd)
{
// ...
for (;;) {
if (pipe->nrbufs < pipe->buffers) {
int newbuf = (pipe->curbuf + pipe->nrbufs) & (pipe->buffers - 1);
struct pipe_buffer *buf = pipe->bufs + newbuf; buf->page = spd->pages[page_nr];
buf->offset = spd->partial[page_nr].offset;
buf->len = spd->partial[page_nr].len;
buf->private = spd->partial[page_nr].private;
buf->ops = spd->ops;
if (spd->flags & SPLICE_F_GIFT)
buf->flags |= PIPE_BUF_FLAG_GIFT; pipe->nrbufs++;
page_nr++;
ret += buf->len; if (!--spd->nr_pages)
break;
if (pipe->nrbufs < pipe->buffers)
continue; break;
} if (spd->flags & SPLICE_F_NONBLOCK) {
if (!ret)
ret = -EAGAIN;
break;
} pipe->waiting_writers++;
pipe_wait(pipe);
pipe->waiting_writers--;
}
return ret;
}

代码删除了和信号相关的逻辑,整个循环内的关键路径

  • if (!--spd->nr_pages) 为数据页都被挂在 pipe 后退出循环
  • if (pipe->nrbufs < pipe->buffers) 为 pipe 中还有空间则继续运行
  • if (spd->flags & SPLICE_F_NONBLOCK) 为 pipe 没有空间但是设置了非阻塞,则直接返回
  • pipe_wait 为数据没有读完,但是 pipe 已经没有空间则直接被挂起

在上面分析未在不同内核上splice表现不一致的结果中,可以看到 16K 的数据是能够返回的,数据的大小大一些就被阻塞了。

对比分析

kernel-6.1 对 splice 的实现相较 kernel-3.10 做了关键的两点变化:

  1. 提前做了 pipe 的空判断,这样数据挂载函数 splice_to_pipe 内部就不用进行阻塞了,而 3.10 将空判断和数据的转移放在一起做了
    static int wait_for_space(struct pipe_inode_info *pipe, unsigned flags)
    {
    for (;;) {
    if (unlikely(!pipe->readers)) {
    send_sig(SIGPIPE, current, 0);
    return -EPIPE;
    }
    if (!pipe_full(pipe->head, pipe->tail, pipe->max_usage))
    return 0;
    if (flags & SPLICE_F_NONBLOCK)
    return -EAGAIN;
    if (signal_pending(current))
    return -ERESTARTSYS;
    pipe_wait_writable(pipe);
    }
    }
  2. 限制了单次 splice 读取的大小
    static long do_splice_to(struct file *in, loff_t *ppos,
    struct pipe_inode_info *pipe, size_t len,
    unsigned int flags)
    {
    /* Don't try to read more the pipe has space for. */
    p_space = pipe->max_usage - pipe_occupancy(pipe->head, pipe->tail);
    len = min_t(size_t, len, p_space << PAGE_SHIFT); return in->f_op->splice_read(in, ppos, pipe, len, flags);
    }

结合代码和测试程序进行分析一下

kernel-3.10 里面的实现可能在 splice_to_pipe 中就被阻塞了,pipe 可容纳的空间小于 skbuff 中的数据

kernel-6.1 由于每次都会判断是否为空,只向 pipe 中写入可容纳的数据,所以只要有空间就不会被阻塞。

那么就遗留另外一个问题,pipe 的可容纳大小在不同版本内核上的不一样,和文档里面的 65536 都有一些明显出入,但是测试 pipe 的 write,则是准确的 65536. 据查资料得到的结论,fd -> pipe -> fd 这个过程只是 skbuff 的 PAGE 变化,内核不会再进行额外的内存分配。

上面的分析还需要通过调试来进行证明,那可以再写一篇文章通过kprobe分析 splice 了,这里再挖一个坑。

结论

这个问题只在低版本的内核上有问题,在高版本 Debian12 是正常的,在 Go1.21.5 中已经修复,建议使用 Go1.21.5 及以上的版本。

TODO

通过 kprobe 来分析 splice 下的 pipe 空间变化

参考

记一次 splice 导致 io.Copy 阻塞的排查过程的更多相关文章

  1. 一次FGC导致CPU飙高的排查过程

    今天测试团队反馈说,服务A的响应很慢,我在想,测试环境也会慢?于是我自己用postman请求了一下接口,真的很慢,竟然要2s左右,正常就50ms左右的. 于是去测试服务器看了一下,发现服务器负载很高, ...

  2. 记一次mq无法正常生产消息的事故排查过程

    早上上班后得知,服务费未同步到代理商系统.查看draft_server系统生产环境的log,显示在往RabbitMQ推数据时出现异常:no route to host. 2019-07-29 01:3 ...

  3. IO模型--阻塞IO,非阻塞IO,IO多路复用,异步IO

    IO模型介绍: * blocking IO 阻塞IO * nonblocking IO 非阻塞IO * IO multiplexing IO多路复用 * signal driven IO 信号驱动IO ...

  4. python 全栈开发,Day44(IO模型介绍,阻塞IO,非阻塞IO,多路复用IO,异步IO,IO模型比较分析,selectors模块,垃圾回收机制)

    昨日内容回顾 协程实际上是一个线程,执行了多个任务,遇到IO就切换 切换,可以使用yield,greenlet 遇到IO gevent: 检测到IO,能够使用greenlet实现自动切换,规避了IO阻 ...

  5. {python之IO多路复用} IO模型介绍 阻塞IO(blocking IO) 非阻塞IO(non-blocking IO) 多路复用IO(IO multiplexing) 异步IO(Asynchronous I/O) IO模型比较分析 selectors模块

    python之IO多路复用 阅读目录 一 IO模型介绍 二 阻塞IO(blocking IO) 三 非阻塞IO(non-blocking IO) 四 多路复用IO(IO multiplexing) 五 ...

  6. python网络编程-同步IO和异步IO,阻塞IO和非阻塞IO

    同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的.所以先限定一下本文的上下文. 本文讨论的背景是Linux环境下的network IO. ...

  7. python开发IO模型:阻塞&非阻塞&异步IO&多路复用&selectors

    一 IO模型介绍 为了更好地了解IO模型,我们需要事先回顾下:同步.异步.阻塞.非阻塞 同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非 ...

  8. Python学习-day10(番外篇) 阻塞IO 非阻塞IO 同步IO 异步IO

    这个章节的内容是关于IO的概念,谈一谈什么是 阻塞IO 非阻塞IO 同步IO 异步IO.以下摘要是我对这四种IO的一个形象理解. 场景是去去银行办理业务.节点有三个,1)到银行提交申请:2)取号:3) ...

  9. (IO模型介绍,阻塞IO,非阻塞IO,多路复用IO,异步IO,IO模型比较分析,selectors模块,垃圾回收机制)

    参考博客: https://www.cnblogs.com/xiao987334176/p/9056511.html 内容回顾 协程实际上是一个线程,执行了多个任务,遇到IO就切换 切换,可以使用yi ...

  10. 网络IO模型:同步IO和异步IO,阻塞IO和非阻塞IO

    同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不同的人给出 ...

随机推荐

  1. [转帖] 记一次使用gdb诊断gc问题全过程

    记一次使用gdb诊断gc问题全过程   原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 上次解决了GC长耗时问题后,系统果然平稳了许多,这是之前的文章<G ...

  2. 【转帖】如何使用route管理路由表

    这里是引用 route快捷使用方法 我们一般管理路由有使用route命令 本身route使用大致有两种方法:但其实 在实际操作中,我们熟练掌握一种方法就可以了. route 有以下6种操作方法: 1) ...

  3. [转帖]传输层安全协议真(TLS)的安全吗?

    https://zhuanlan.zhihu.com/p/305161227 随着数字通信,计算机网络,公钥密码体制等技术的迅速发展,安全网络通信已经成为了人们的日常需求.TLS 作为目前被广泛应用的 ...

  4. 【转贴】西数全新推出企业级金盘SSD:2.5寸U.2接口、最大7.68TB、96层TLC

    西数全新推出企业级金盘SSD:2.5寸U.2接口.最大7.68TB.96层TLC https://www.cnbeta.com/articles/tech/951353.htm 硬件发展日新月异 &q ...

  5. 使用rpm打包nacos然后部署为systemd服务开机自动启动的方法

    背景 Nacos是阿里开源的服务注册组件,能够简单的实现微服务的注册与发现机制. 但是官方并没有提供 sytemd的服务脚本, 也没有提供rpm包的方式. 公司里面使用 nacos的场景越来越多, 部 ...

  6. C# WPF 开发一个 Emoji 表情查看软件

    微软在发布 Windows 11 系统的时候,发布过一个开源的 Emoji 表情 fluentui-emoji .因为我经常需要里面的一些表情图片,在仓库一个个查找特别的不方便,所以我做了一个表情查看 ...

  7. 兄弟组件互相传递值-this.$bus.$emit与this.$bus.$on

    B组件向C组件传递一个值. 一种组件间通信的方式, 适用于任意的组件间通信. 适用于任意的组件间通信. 适用于任意的组件间通信. 通过this.$bus.$emit('事件名',数据)进行提供数据 通 ...

  8. git创建分支出现 fatal: Not a valid object name: 'master'.

    今天使用git 创建新分支的时候 出现了一个错误 fatal: Not a valid object name: 'master'. 问题描述:一个非法的master,原因:本地还没有创建master ...

  9. 【VictoriaMetrics】一个小优化:循环改查表,性能提升56.48 倍

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 做了一个 vm-storage 数据文件 merge 的工 ...

  10. 如何控制Tomcat的catalina.out的大小

    catalina.out文件,数据主要来源为:System.out 和 System.err 在控制台上直接输出的信息. 编码时应避免使用System.out.println()和e.printSta ...