Senlin's Blog


  • 分类

  • 归档

  • 标签

  • 关于

深入 Linux 多线程编程

发表于 2017-06-10   |   分类于 并发编程   |   阅读次数

NPTL 线程模型

  NPTL,也即 Native POSIX Thread Library,是 Linux 2.6 引入的新的线程库实现,用来替代旧的 LinuxThreads 线程库。在 NPTL 实现中,用户创建的每个线程都对应着一个内核态的线程,内核态线程也是 Linux 的最小调度单元。
  在 NPTL 实现中,线程的创建相当于调用clone(),并指定下面的参数:

1
2
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS |
CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM

  下面解释一下这些参数的含义:

  • CLONE_VM 所有线程都共享同一个进程地址空间。
  • CLONE_FILES 所有线程都共享进程的文件描述符列表 (file descriptor table)。
  • CLONE_FS 所有线程都共享同一个文件系统的信息。
  • CLONE_SIGHAND 所有线程都共享同一个信号 handler 列表。
  • CLONE_THREAD 所有线程都共享同一个进程 ID 以及 父进程 ID。

  在 Linux 可以通过下面命令查看线程库的实现方式:

1
2
$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.23

线程的栈

  在 Linux 中,一个进程可以包含多个线程,这些线程将共享进程的全局变量,以及进程的堆,但每个线程都拥有它自己的栈。正如下图所示:


  在 64 位系统中,除了主线程之外,其它线程的栈默认大小为 8M,而主线程的栈则没有这个限制,因为主线程的栈可以动态增长。可以用下面的命令查看线程栈的大小:
1
2
$ ulimit -s
8192

阅读全文 »

Linux TCP/IP 性能调优

发表于 2017-05-28   |   分类于 网络编程   |   阅读次数

再谈三次握手

  Linux 提供了listen()系统调用,它的作用是在 server socket 上监听新的连接请求:

1
int listen(int sockfd, int backlog);

  那么listen()的第二个参数backlog的作用是什么呢?让我们先回顾一下 TCP 的三次握手:


  对于 server 来说,一个新的连接首先会经过 SYN_RECV 状态,然后才会进入 ESTABLISHED 状态。那么对于每个 server socket 来说,内核需要两个队列来保存所有连接的信息:

  • 一个是半连接队列 (incompletely queue),用来保存所有处在 SYN_RECV 状态的连接。
  • 另一个是连接队列 (completely queue),用来保存所有处在 ESTABLISHED 状态的连接。

  也就是说,当一个新的连接进入 SYNC_RECV 状态时,内核会将它放到半连接队列中。而当这个连接从 SYNC_RECV 状态进入到 ESTABLISHED 状态时,内核会将它从半连接队列中移动到连接队列中。最后当accept()返回时,这个连接也会从连接队列中移走。
  listen()的backlog参数其实是用来指定连接队列的长度。而如果要改变半连接队列的长度,则需要改变内核参数:

1
2
3
$ sudo vi /etc/sysctl.conf
net.ipv4.tcp_max_syn_backlog = 1024 # 半连接队列的长度
$ sudo sysctl -p # 使改变生效

  在实际情况中,如果 server 的负载比较高,无法迅速地accept()新的连接,就可能导致连接队列满了,这时候就需要调高listen()的backlog参数。然而注意到,backlog参数有一个上限,这个上限由内核参数net.core.somaxconn决定,这个参数的默认值是128。也就是说,如果我们要让backlog的上限大于 128,就需要调高net.core.somaxconn这个内核参数的值:

1
2
3
$ sudo vi /etc/sysctl.conf
net.core.somaxconn = 1024 # listen backlog 的上限,默认值是 128
$ sudo sysctl -p # 使改变生效


  让我们思考一个问题,对于 server 的一个连接来说,当它接收到 client 发送的 ACK segment 之后,就会从 SYNC_RECV 进入到 ESTABLISHED 状态,但如果这时连接队列满了,那么会发生什么事情呢?
  默认情况下,server的 TCP 协议栈会丢弃这个 ACK segment。但此时这个连接仍然处在 SYNC_RECV 状态,所以 server 的 TCP 协议栈会重新发送 SYN/ACK 给 client。下图显示了进行一次重试的过程:


  最大的重传次数由内核参数net.ipv4.tcp_synack_retries规定,默认情况下,这个值是 5,重传 5 次的总耗时约为 180 秒。在某些情况下,我们可以调小这个值,以减小重传的超时时间。
阅读全文 »

深入 Redis 持久化

发表于 2017-05-11   |   分类于 数据库   |   阅读次数

  Redis 提供了两种数据持久化的方式,一种是 RDB,另一种是 AOF。默认情况下,Redis 使用的是 RDB 持久化。

RDB 持久化

  当 Redis 执行 RDB 持久化时,它会怎么做呢?

  • Redis 进程会 fork 出一个子进程。
  • 由子进程将内存中的所有数据写入到一个临时的 RDB 文件中。
  • 完成写入操作之后,旧的 RDB 文件会被新的 RDB 文件替换掉。

  下面是一些和 RDB 持久化相关的配置:

  • save 60 10000:如果在 60 秒内有 10000 个 key 发生改变,那就执行 RDB 持久化。
  • stop-writes-on-bgsave-error yes:如果 Redis 执行 RDB 持久化失败(常见于操作系统内存不足),那么 Redis 将不再接受 client 写入数据的请求。
  • rdbcompression yes:当生成 RDB 文件时,同时进行压缩。
  • dbfilename dump.rdb:将 RDB 文件命名为 dump.rdb。
  • dir /var/lib/redis:将 RDB 文件保存在/var/lib/redis目录下。

  当然在实践中,我们通常会将stop-writes-on-bgsave-error设置为false,同时让监控系统在 Redis 执行 RDB 持久化失败时发送告警,以便人工介入解决,而不是粗暴地拒绝 client 的写入请求。


  在考虑是否采用 RDB 持久化之前,要先了解 RDB 持久化的缺点:

  • 在 Linux 系统中,fork 会拷贝进程的 page table。随着进程占用的内存越大,进程的 page table 也会越大,那么 fork 也会占用更多的时间。 如果 Redis 占用的内存很大 (例如 20 GB),那么在 fork 子进程时,会出现明显的停顿现象(无法处理 client 的请求)。另外,在不同机器上,fork 的性能是不同的,可以参见 Fork time in different systems。
  • Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的两倍。
  • 如果业务场景很看重数据的持久性 (durability),那么不应该采用 RDB 持久化。譬如说,如果 Redis 每 5 分钟执行一次 RDB 持久化,要是 Redis 意外奔溃了,那么最多会丢失 5 分钟的数据。

AOF 持久化

  可以使用appendonly yes配置项来开启 AOF 持久化。Redis 执行 AOF 持久化时,会将接收到的写命令追加到 AOF 文件的末尾,因此 Redis 只要对 AOF 文件中的命令进行回放,就可以将数据库还原到原先的状态。
  与 RDB 持久化相比,AOF 持久化的一个明显优势就是,它可以提高数据的持久性 (durability)。因为在 AOF 模式下,Redis 每次接收到 client 的写命令,就会将命令write()到 AOF 文件末尾。
  然而,在 Linux 中,将数据write()到文件后,数据并不会立即刷新到磁盘,而会先暂存在 OS 的文件系统缓冲区。在合适的时机,OS 才会将缓冲区的数据刷新到磁盘(如果需要将文件内容刷新到磁盘,可以调用fsync()或fdatasync())。
  通过appendfsync配置项,可以控制 Redis 将命令同步到磁盘的频率:

  • always:每次 Redis 将命令write()到 AOF 文件时,都会调用fsync(),将命令刷新到磁盘。这可以保证最好的数据持久性,但却会给系统带来极大的开销。
  • no:Redis 只将命令write()到 AOF 文件。这会让 OS 决定何时将命令刷新到磁盘。
  • everysec:除了将命令write()到 AOF 文件,Redis 还会每秒执行一次fsync()。在实践中,推荐使用这种设置,一定程度上可以保证数据持久性,又不会明显降低 Redis 性能。

  然而,AOF 持久化并不是没有缺点的:Redis 会不断将接收到的写命令追加到 AOF 文件中,导致 AOF 文件越来越大。过大的 AOF 文件会消耗磁盘空间,并且导致 Redis 重启时更加缓慢。为了解决这个问题,在适当情况下,Redis 会对 AOF 文件进行重写,去除文件中冗余的命令,以减小 AOF 文件的体积。在重写 AOF 文件期间, Redis 会启动一个子进程,由子进程负责对 AOF 文件进行重写。
  可以通过下面两个配置项,控制 Redis 重写 AOF 文件的频率:

  • auto-aof-rewrite-min-size 64mb
  • auto-aof-rewrite-percentage 100

  上面两个配置的作用:当 AOF 文件的体积大于 64MB,并且 AOF 文件的体积比上一次重写之后的体积大了至少一倍,那么 Redis 就会执行 AOF 重写。

参考资料

  • Redis Persistence
  • Redis persistence demystified
  • Redis persistence in practice
  • Learn Redis the hard way (in production)
  • Everything You Always Wanted to Know About Fsync()

Nginx 内存池源码分析

发表于 2017-04-24   |   分类于 网络编程   |   阅读次数

内存池介绍

  在C语言中,程序员需要手动管理动态分配的内存,这带来了不少麻烦的事情。譬如说,当某块内存使用完毕之后,程序员需要记得释放这块内存,否则就会发生内存泄漏。另一方面,如果程序中需要频繁地分配和释放小块的内存,倘若每次都是直接通过malloc()去分配内存,这将带来额外的调用开销,同时也会产生内存碎片。
  Nginx 使用内存池ngx_pool_t来管理内存,这带来了不少的好处:

  • 当程序需要内存时,只管向内存池申请,申请而来的内存也无需手动释放,因为内存池销毁时会负责释放所有的内存。
  • 对于那些需要频繁分配和释放小块内存的程序来说,使用内存池可以减少malloc()的次数,除了降低了调用开销之外,还减少了内存碎片的产生。

数据结构

  下面是内存池的数据结构,ngx_pool_t代表内存池,ngx_pool_data_t代表小块内存,而ngx_pool_large_t则代表大块内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 大块内存
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 所有大块内存都通过链表串联起来
void *alloc; // 指向实际分配的大块内存
};
// 小块内存
typedef struct {
u_char *last;
u_char *end;
ngx_pool_t *next;
ngx_uint_t failed;
} ngx_pool_data_t;
// 内存池
typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s {
ngx_pool_data_t d;
size_t max;
ngx_pool_t *current;
ngx_chain_t *chain;
ngx_pool_large_t *large; // 大块内存链表
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;
};

内存池构造

  内存池处理大块内存和小块内存的策略是不同的,譬如说,当用户向内存池申请内存时,内存池会判断申请的是大块内存还是小块内存,如果是小块内存,那么直接在内存池里面分配,但如果是大块内存,就直接向 OS 申请。ngx_pool_t的max成员则作为判断的标准,如果用户申请的内存大于max,则认为是大块内存,否则就是小块内存。下面的ngx_create_pool()函数用于创建和初始化内存池:

阅读全文 »

gRPC 编程指南

发表于 2017-04-20   |   分类于 网络编程   |   阅读次数

gRPC 介绍

  gRPC 是谷歌开源的高性能 RPC 框架。RPC 也即远程方法调用,对于 RPC client 来说,它可以调用远程 server 上的某个方法,看起来就像是在调用本地方法一样。区别就在于,通过 RPC 调用远程方法时,数据经过序列化之后会通过网络发送给远程 server,远程 server 执行方法之后,同样会将返回结果序列化之后发送回 client。在分布式系统中,gRPC 可以用来解耦程序的逻辑,不同组件之间通过 gRPC 进行通信。
  gRPC 使用 Protobuf 作为它的数据序列化的工具,Protobuf 会将数据序列化成二进制的数据流。与 JSON 这类文本形式的数据相比,二进制数据显得更加紧凑和便于解析,在网络传输中,二进制数据由于体积更小,传输也更快。另一方面,gRPC 也是跨多种编程语言的,譬如说,一个 Java 的 client 可以与一个 C++ 的 server 通信。

阅读全文 »
1…678…13

高性能

61 日志
13 分类
14 标签
GitHub 知乎
© 2015 - 2022
由 Hexo 强力驱动
主题 - NexT.Mist
  |   总访问量: