nginx-0x0C-系统时间

系统时间缓存的必要性以及整套配合机制在nginx-0x0A-定时器已经提过。这篇介绍的是系统时间到底是咋缓存的。

首先年月日时分秒结构化的时间肯定是通用的,但是有些场景对时间单调性有严格要求。

1 获取时间的系统调用

系统调用\比较 gettiemofday clock_gettime
格式 结构化绝对时间,系统时间,墙上时间 相对时间,相对系统开机时长
精度 微秒 纳秒

2 关于时间格式

上面已经介绍了两种时间格式

  • 结构化时间
  • 单调相对时间

因此在nginx中就维护了这两种格式的全局变量

  • static ngx_time_t cached_time[NGX_TIME_SLOTS];
  • volatile ngx_msec_t ngx_current_msec;

3 关于性能的考量

对于缓存,有更新的地方,有读取使用的地方。为了保障公共资源的安全性,采用锁保护是最简单的方式,关于怎么加锁

  • 读写都要加锁,共用一把锁,这种方式实现最简单,性能最差
  • 读写都要加锁,读写锁分离,这种方式稍微复杂,但是对于高性能服务器上高吞吐的读是接受不了的
  • 只要求写加锁,读无锁化,这种方式实现起来最难,性能最高

4 为什么同样是缓存一个是单值变量一个是数组

读写解耦的情况下,想象有个无限长度的数组,写有写指针,读有读指针,写就永远不用担心读的数据被污染和安全问题。
但是不可能有无限空间这一说,环形数组就是有有限空间模拟无限存储的过程,既然是数组就要考虑数组长度,过短和过长都不行,nginx默认用长度64的数组来缓存时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 系统时间缓冲区
* 缓存时间 减少系统调用 数组长度通过宏NGX_TIME_SLOTS控制 长度64
* 首先为什么把缓冲区设计成环形 这样设计的目的是为了保证有锁单线程写 无锁多进程读
* 既然环形的设计目的是为了解耦读写操作 那么是不是只要保证缓冲区大小是2就行 写操作一直在两个位置轮流交替
* 长度2不是不可以但是存在的隐患是读时被写覆盖
* 假设A在读系统时间 拿到的指针是1
* 在A读期间B发生了多次对系统时间的更新
* - B更新系统时间 指针1
* - B更新系统时间 指针2
* - B更新系统时间 指针1
* - ...
* 也就是意味着A使用的系统时间已经发生了更新 原来的值被覆盖了 这种场景可能导致误判
* 所以本质问题就是最好留给更新操作多点时间缓冲 用空间来换 只要环形缓冲区大一点 就足够避免上面的事情发生
* 可能这就是作者设计缓冲区默认长度64的原因
*/
static ngx_time_t cached_time[NGX_TIME_SLOTS];

4.1 结构化时间格式

1
2
3
4
5
6
7
8
// 结构化时间格式
typedef struct {
// 秒
time_t sec;
// 毫秒
ngx_uint_t msec;
ngx_int_t gmtoff;
} ngx_time_t;

4.2 单调时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 缓存的系统时间 格式是毫秒 语义是这个时间表达的是系统启动后x毫秒 是个相对系统启动的相对时间
* 是通过clock_gettime得到的单调时间
* 为什么下面的ngx_cached_time要用环形设计 而这个系统时间不需要环形设计而只要一个变量
* <ul>
* <li>首先ngx_current_msec就是个整数 而ngx_cached_time是结构化的时间 有多个结构体成员</li>
* <li>其次系统对于long读写是原子的 不存在并发不安全问题</li>
* </ul>
* 这个时间的唯一作用就是判断定时任务是不是到期该执行了 这个使用场景决定了
* 必须单调 不能出现时间倒退导致对定时任务的误判
* 为什么结构化时间缓存用的是环形数组形式 而单调时间用的就是一个变量
* <ul>
* <li>首先 语义是就不同 单调时间就是一个很明确的数字</li>
* <li>第二 类型 它就是一个整型 不存在更新期间读到一半新数据 一半老数据</li>
* </ul>
*/
volatile ngx_msec_t ngx_current_msec;

这就是结构化时间用环形数组缓存,而单调时间用整数缓存的原因。

5 关于读写解耦

在环形数组上用读写指针方式保证读写操作分离,写用锁保证资源互斥,读用无锁保证性能。

5.1 读

1
2
3
4
5
6
7
8
9
/*
* 下面这几个都是给读线程直接用的 目的就直接读到缓存的最新的系统时间
* ngx_cached_time比较特殊 它是一个指针 本质就是cached_time环形数组的读指针
* <ul>
* <li>读操作的是ngx_cached_time 无锁</li>
* <li>写操作的是slot 需要竞争锁 互斥操作 写完移动读指针到最新位置</li>
* </ul>
*/
volatile ngx_time_t *ngx_cached_time;

5.2 写

写指针,这个数组指针只留给写操作使用的。

1
2
3
4
5
6
/*
* cached_time数组 指向的是当前缓存的最新的系统时间 数组脚标移动实现环形数组
* 这个slot可以理解成只给写线程用的写指针
* 读线程不会直接用这个指针 读线程用的是ngx_cached_time
*/
static ngx_uint_t slot;

在更新时间内部有个小细节,我觉得值得学习,高性能软件开发中总能看到很多小技巧小细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 正常情况下更新时间的步骤是
* <ul>
* <li>数组维护的最新的时间在slot上 slot是直接给写线程使用的 对应这个slot位置的是ngx_cached_time给读线程使用的</li>
* <li>拿到逻辑上的下一个位置 slot等于0或slot+1</li>
* <li>将新时间写到缓存上</li>
* <li>写完后更新读指针ngx_cached_time</li>
* <li>释放写锁</li>
* </ul>
* 为什么要上写锁 为了保护写这个资源的原子性 为什么呢 因为要这是个结构体要写秒和毫秒两个成员
* 那是不是如果只写一个long型数字就可以不用锁 天然原子性
* 所以并没有直接去更新到下一个位置上 而是看下秒级没变 那就只用更新毫秒 只更新毫秒就是只更新一个long字段 不怕无锁读的地方读到更新一半这种情况
*/
tp = &cached_time[slot];

if (tp->sec == sec) {
// 只更新毫秒这个字段 这样做的的目的是为了快速返回 减少持锁时长 减少写并发的锁竞争
tp->msec = msec;
ngx_unlock(&ngx_time_lock);
return;
}

nginx-0x0C-系统时间
https://bannirui.github.io/2025/04/09/nginx/nginx-0x0C-系统时间/
作者
dingrui
发布于
2025年4月9日
许可协议