谈谈Linux下的时间机制

最近同事在解决一个时区问题时花费了较多时间,但仍是一头雾水,通过查询Linux相关的文档,我给出了他一些修改建议,经过验证后问题得以修复,事后我总结来看,主要是该同事对于Linux下的时间机制的理解不够深入,因此这里我将相关的知识给记录下来,以解释相关的疑惑。

Linux下目前有三种主要的时钟,其名称及作用如下:

硬件时钟

此时钟一般由主板上的单独电子器件提供,它与CPU核心是独立的运行的,往往在电脑关机时仍能继续运行(想想我们的PC在装机重启后,时间仍然还保持着正确性就是因为这么个东西存在)。

此时钟还有一些其他名称,如RTC(实时时钟)、BIOS时钟或CMOS时钟,此时钟可以通过hwclock这样的小工具来设置。

需要注意的是,硬件时钟中是不感知时区的,其硬件实现记录了个初始值(如我司设备经常采用的1970.1.1等),然后用户可以通过hwclock写入一个时间来更新它,我们可以认为它是一个有初始值的计数器,用户可以更新当前的值,之后它将按照其计时精度来更新其值,供系统通过硬件接口读取。

系统时钟

此时钟是由Linux内核所维护的一个软件时钟,由时钟中断触发更新,其时间是相对于1970.1.1-00:00:00 UTC这个时间的秒数(UTC与GMT即格林威治时间意义相同,为世界标准时间),为方便这里的讲述,我们可以假定我们在格林威治这个地方讨论时间,这样我们可以先把时区放到一边不管了。

在Linux启动后,系统启动脚本(不管是sysvinit还是systemd都将有这样的处理)将通过hwclock读取硬件时钟,并将时间设置到内核中,此后用户获取时间均通过内核获取。

单调时钟

在Linux系统上,用户可以通过date命令更新系统时间,从而导致系统时钟出现跳变,这对于一些涉及到对调度单元(如协程)运行时间来记时以便切出去让其他进程执行的程序来说则会产生计时不准的问题,为此,Linux上支持了一个所谓的单调时钟的接口,此接口除支持单调时钟外,还支持对于系统时钟的获取,此接口属于较新的POSIX标准,如果Linux版本较老的话,可能将无法支持

此接口的定义如下:

int clock_gettime(clockid_t clk_id, struct timespec *tp); // 采用CLOCK_MONOTONIC这个clk id可以获取到单调时钟

上面讨论时,我刻意互略了关于时区的处理,为简单起见,我们这里可以认为Linux内核是不感知时区的(实际上由于种种原因比如VFAT文件系统的时间戳处理就需要内核需要感知到时区)。

在Linux下,由于大量程序均需要依赖于glibc,因此glibc会读取TZ这个环境变量来设置时区,下面是一些C库函数的简单介绍:

struct tm *gmtime(const time_t *timep); // 获取UTC时间,也就是说怎么改TZ变量或者通过tzset更改时区都不会影响此值
struct tm *localtime(const time_t *timep); // 获取本地时间,更改TZ变量或者通过tzset更改时区都会影响此值

当然当于时区还需要补充一点的是,环境变量这个东西只影响所属的进程,在子进程中修改并不会影响父进程,libc.so甚至提供了一个全局变量用来保存当前进程的时区设置(注意,虽然libc.so为动态库,但其在mmap映射是private的,所以会导致其中定义的全局变量每个进程都复制了一份),如果修改的话,需要通过signal类似的IPC机制通知相关进程予以刷新,这也是我同事改时区反复出问题的一个原因。

从一个编程者的角度来看,如果是涉及到对调度单元(如协程)运行时间记时的话,要尽可能采用单调时钟,下面是syslog-ng项目所采用的异步io库ivykis关于时间处理的代码片段,可以看出,它将尽可能去使用单调时钟或者clock_gettime接口,在最后才尝试使用gettimeofday来获取时间。

#ifdef HAVE_CLOCK_GETTIME
static int clock_source;
#endif

void iv_time_get(struct timespec *time)
{
struct timeval tv;

#if defined(HAVE_CLOCK_GETTIME) && defined(HAVE_CLOCK_MONOTONIC_FAST)
if (clock_source < 1) {
if (clock_gettime(CLOCK_MONOTONIC_FAST, time) >= 0)
return;
clock_source = 1;
}
#endif

#if defined(HAVE_CLOCK_GETTIME) && defined(HAVE_CLOCK_MONOTONIC)
if (clock_source < 2) {
if (clock_gettime(CLOCK_MONOTONIC, time) >= 0)
return;
clock_source = 2;
}
#endif

#if defined(HAVE_CLOCK_GETTIME) && defined(HAVE_CLOCK_REALTIME)
if (clock_source < 3) {
if (clock_gettime(CLOCK_REALTIME, time) >= 0)
return;
clock_source = 3;
}
#endif

gettimeofday(&tv, NULL);
time->tv_sec = tv.tv_sec;
time->tv_nsec = 1000L * tv.tv_usec;
}

精神自由

昨晚晚饭时喝了一杯咖啡,于是在夜半时一如往常一样的失眠了,而关于自己此生到底要追求什么这样一个问题,其实在我心里已经盘旋了很久,于是借着昨晚的时间,我好好思考了这个问题,而结果既出于意料之外,又在意料之中,仔细推敲来,我其实一直所追求的是一种自由,但这种自由并非是消费的自由,也并非旅行的自由,虽然这种自由也建构在金钱的基础之上,但这种自由则是思考的自由,工作方式的自由,睡眠的自由,读书的自由,写作的自由等等,而如果要从这些这些自由中抽象出共性出来,那就是精神的自由,或者说是精神层面的自由。

今年是二零一五年,也是我进入三十岁奔向四十岁的关键一年,对我来说,工作、结婚、生子、逐渐年迈的父母、偿还房贷等等事务,都会成为我要面对的现实问题,而我能拿出来安慰自己的,则是我相信自己还能够抽出额外的时间,进行思考、阅读甚至这里的写作这样片刻属于我的时间,而我相信我所追求的,就是我能够在这些事务与自己的内心世界之间寻找到一种平衡,我能够坚守这这块属于我的土地,但却又能维持好前述种种。

从目前来看,实现这种自由的前提很世俗,就是资金,但相比财务自由来说,我的要求却低的很,或许是因为成长于农村的原因,我生活并不奢华,也不想追求奢华,于我而言,我只需要简单而基本的生活,于是我有理由相信自己实现这个过程较为简单,它应该是两部分,首先我可以拥有一个覆盖日常生活开支的固定收入来源,比方说一套用于出租的房产或者其他较为稳健的理财产品,之后是我将从事相对灵活自由的工作,像我这样的职业,在互联网这样的时代,想做自由职业者应该容易的多;通过这两部分的结合,我希望再经过十年,也即我四十岁的时候,我能够实现自己这里所谈到的精神自由的资金基础。

想清楚自己追求之后,对于我而言,最大的作用是能够理性正视生活中所遇到的事情,哪些事情我需要隐忍,哪些事情我需要放手一博,哪些事情我可以做,哪些事情我不可以做,也有助于我更好的做好人生规划,不偏离人生方向的持续前行。

我很开心在自己即将在而立之年能想通这样一件事,于是就记录在网络上的这片属于自己的地方,期待将来我回过头来再来看看。

附:

当我发布这篇文章到网站上后,才发现原来我网站的标题就是通往成熟与自由的旅程,看来这并不是巧合,而是我潜意识中故有的所在了。

Midonet聪明的Tunnel Key分配策略

拥抱开源如今成了一种潮流,既Juniper家的OpenContrail成为开源一揽子(之所以称为一揽子是用于区别现有OpenStack中由社区所维护的基于分散组件的松耦合方式)OpenStack网络虚拟化方案的首发明星后,Midukura这家公司也将自家的OpenStack网络虚拟化方案以开源方式来运作了,相比于OpenContrail的工程师的设备商研发背景,Midukura更具有IT运维背景(这点可以从其技术堆栈如scala编程语言、zookeeper/cassandra数据库、分布式架构推测出来,关于更细节的内容,后面我会陆续补充相关的分析),因此我个人还是更看好Midokura的方案,而从自己实际部署及验证的情况来看,Midokura要靠谱的多一点,当然这只是一家之言,也并非本文的重点,就不再展开。

在本文中我想谈的是,Midonet Tunnel Key的使用策略,与我们通常在基于OVS的方案中看到Tunnel Key(不论是NVGRE还是VXLAN)一般用做租户标识(即Virtual Network Identifier)不同,Midonet Tunnel Key则可以理解为按VIF分配的(实际上是基于Midonet的外部虚拟端口分配的,这里为了简化理解,可以简单认为就是按VIF分配的),而前者其实也是IETF相关标准文档所描述的用法。

Midonet之所以这样做的原因,与其架构实现有较大的关系,Midonet的架构总结起来,有如下的这几种特色:

  • 全分布式架构,每个节点上运行一个Midolman进程(跑在JVM上),用于负责从分布式数据库(zookeeper)同步并发布虚拟网络配置信息,因此实现了去中心化,每个节点可以独立计算出转发结果
  • 虚拟拓朴仿真机制,Midolman从VIF上收到报文后,通过拓朴转发仿真(与我们常规的Bridge->Router->Router->Bridge转发类似),就可以计算出目的VIF,从而得知目的虚拟机所在的主机,并在通过Underlay网络的IP以NVGRE/VXLAN的方式将报文Overlay传送给目的主机前就将报文编辑好
  • 内核转发采用OVS datapath,即采用exact match的flow转发(megaflow暂不支持),以实现次包(即首包之后的包)的内核转发,提高转发性能

结合上面的介绍,Midonet采用基于VIF分配的原因就比较清楚的:

  • 即然源端可以直接仿真出目的VIF且完成报文编辑,因此到对端唯一需要做的就是知道对端的VIF对应的OVS datapath接口是什么,NVGRE/VXLAN封装中的Tunnel Key则提供了一个简单方便的编码点,于是midonet就这么用了,另外一个Host上通过Zookeeper分配的Tunnel Key空间有10位数(非标准限制,而是Zookeeper的限制),对于一个cpu有限、memory有限的主机来说,10位数的VM已经远超能力极限的(咱们又不是天河一号,天河一号估计也不行),已经足足够用了
  • Midonet采用与OVS相似的机制,转件层面采用wildcard match流表,内核采用exact match流表,因此在主机上的Midolman扫描到VIF对应的TAP接口时,Midolman就可以为这个VIF生成一条匹配域为该VIF在该主机上所申请到的Tunnel Key为匹配项的wildcard match流表,其出接口即为该VIF所对应的OVS datapath接口,当收到含有该Tunnel Key的报文时,可以直接命中该wildcard match流表,提取报文字段生成exact match流表出接口继承该wildcard流表向OVS datapath下发即可,Wildcard match流表查询及exact match流表下发会非常迅速,也弥补了Java/Scala代码在未被JIT编译器优化时的性能损失,是一个非常优雅的方案

以上便是我个人对于Midonet Tunnel Key分配的理解,在软件定义网络这股风潮日近的今天,来自于互联网IT企业所带来的新思维,新用法,也许会继续改变设备商的开发模式,设备商要想办法适应并拥抱这种变化了。