我一直有一种自建基础服务的兴趣,自从当初机缘巧合混到一个开发者邮箱之后,就一直在尝试自建 NTP 服务,海外已经放了好几台 VPS 到 pool.ntp.org 里面了,但国内条件有限,并没有什么好办法。于是就想到了自建一个。
1. 背景知识
整个 NTP 分发网络的结构是一种近似树状的分层结构,每一级叫做一个 “Stratum”, 其中 Stratum-0 是可靠的参考时间源,比如原子钟、GPS或无线电授时信号源等,所有0级时间源构成了整个 NTP 网络中的参考时钟;在逻辑上,所有0级时间源作为一个整体提供了全世界的标准时钟。不过因为0级时间源本身实际上只是某种形式的时钟信号发生装置,所以它们并不直接对外提供授时服务,而是连接到一些运行着特定软件的服务器上,这些服务器将0级时间源作为参考时钟并与其同步时间,再把自己的系统时间作为参考时钟提供给其它前来查询时间戳的客户端,以此提供授时服务。这些直接以0级时间源为参考时钟的服务器被称作1级时间源(Stratum-1),它们是 NTP 网络中最可信的时间信号来源。1级时间源之间也可能互相作为上游,作为单个0级时间源不可用时候的备份。
按照标准的 NTPd 实现,每台运行 NTPd 的入网设备都同时充当客户端和服务器的角色(当然是可配置的),从上游同步时间的同时,也可供其他客户端用作参考时钟。设备的 Stratum 为其参考时钟的 Stratum 加一,整个网络中 Stratum 的最大值为16. 因为1级时间源有较高的硬件要求,通常网络上提供授时服务的都是2级时间源,也有一部分3级时间源,1级和大于3级的时间源都相对少见。
(图自维基百科)
所以网络中能对外提供服务的最高一级服务器就是 Stratum-1 了,在1级时间源上面需要连接一个0级的参考时钟,这个时钟最简单的来源就是 GPS 信号。然而,一般的民用电子设备(这里特指计算机)在运行中由于节能策略和物理限制等等原因,系统时间的流逝并不是严格匀速的,也就是说系统时间的每一秒对应到现实时间并不严格均匀,这就造成系统在运行时间足够长之后会积累相当可观的误差。运行 NTPd 的系统虽然可以周期性地和上级时间源同步来校正系统时间,但从较长的时间尺度上看依然是一个“抖动”的时间轴,单纯供系统自己使用问题不大,而就1级时间源来说,要对外授时质量就不太够了。
主流的解决方案是在系统上引入一个高可靠的周期性信号,作为基础频率让内核保持稳定的时间流逝速率。比如阿里云的公共 NTP 服务就使用的是 GPS (提供时间戳)加上原子钟(保持时间流逝速率)来维持高精度的授时信号。自家用当然不需要那么夸张,市面上的很多 GPS 模块都能输出 PPS (Pulse Per Second) 信号,其稳定性远远高于一般主板上的晶振,一些型号的 GPS 芯片能提供误差小于 50ns 的信号输出,用作大多数授时场景都绰绰有余。
顺便一提,USB 接口的成型 GPS 模块好像基本都不能输出 PPS.
2. 硬件准备
网上有很多用 Respberry Pi 搭建1级时间源的文章,但树莓派裸机加上一些基础毕竟还是要好几百,作为多数时候只能吃灰的玩具来说还是略贵了点。正好去年年中的时候看到了 Orange Pi 的价格和配置,发现实在是白菜,就顺手入了几只。后来更便宜的 NanoPi M1 和 NanoPi Neo 出现之后也分别入手了一两个。这几个东西都是基于全智 H3 CPU 的 ARM 小板子,性能比树莓派要差一些,社区资源也更有限,好在胜在价格便宜。这篇文档是基于 Orange Pi One 来的,后面会简称为 opi.
至于 GPS 模块,我是在淘宝上找了一款有 PPS 输出的裸模块,自己动手把店家附送的引线转接到杜邦线头上,然后就可以插到 opi 的 GPIO 针脚上了。
3. 软件准备
系统我用的是 Armbian Orange Pi One 的 Debian Jessie 镜像,搭配 Linux 3.4.y 内核(用主线内核 4.9 已经可以正常开机,但配置方法和 3.4 区别比较大所以还没去折腾),系统本身的安装和配置有文档说明略去不提,主要的准备工作是:自己编译内核。
具体编译方法可以看官方的说明,只编译内核的话在完成后把生成的若干个 .deb 包放到 opi 上安装然后重启就行了,当然直接自己编译整个镜像也是可以的。这里需要自己编译的主要原因是为了引入 pps-gpio 支持。
方法也很简单,把这里的 .patch 文件放到编译工程对应的 userpatches 目录( userpatches/kernel/sun8i-default/)下面就行。
另一项软件准备是编译 NTPd, 因为 Debian 官方源里面的 ntp 包禁用了 PPS 驱动,只能自己重新编译来开启。
1 2 3 4 |
mkdir -p ~/debs/ntp && cd ~/debs/ntp sudo apt-get source ntp cd ntp-4.2.6.p5+dfsg vim debian/rules |
然后在 configure 参数中加上 --enable-linuxcaps 选项:
1 2 3 4 5 6 7 8 9 10 11 12 |
./configure CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' LDFLAGS='$(LDFLAGS)' \ --prefix=/usr \ --enable-all-clocks --enable-parse-clocks --enable-SHM \ --enable-linuxcaps \ --disable-debugging --sysconfdir=/var/lib/ntp \ --with-sntp=no \ --with-lineeditlibs=edit \ --without-ntpsnmpd \ --disable-local-libopts \ --enable-ntp-signd \ --disable-dependency-tracking \ --with-openssl-libdir=/usr/lib/$(DEB_HOST_MULTIARCH) |
可选编辑 debian/changelog 更新版本号和说明。
之后使用 export DEB_BUILD_OPTIONS=nocheck; dpkg-buildpackage -rfakeroot -uc -b命令编译并打包,依赖提示缺什么就装什么就行了。编译过程在 opi 上会非常缓慢而持久,如果有办法在更好的机器上做交叉编译的话值得一试。
编译完成后会在上层目录生成若干个 .deb 文件,使用 dpkg -i 命令安装之即可。
另外我们还需要 gpsd 来读取和解析 GPS 数据,这个包建议使用 jessie-backports 源,版本足够新,按官方说明添加源后直接 sudo apt-get install gpsd gpsd-clients 搞定。
4. Do Magic!
现在假定所有准备工作已经完成:opi 系统安装完成、联网并运行在正确的内核上,ntp 和 gpsd 包安装完毕,GPS 模块连线引出完成并且有良好信号。
首先到 /boot下,编辑 scrit.fex 文件,搜索 uart0 字段,确保所有 uart 端口都是启用状态(opi 一共有4个 uart 接口,其中 uart0 供默认控制终端,剩下3个在 GPIO 上):
1 |
sudo bin2fex /boot/script.bin /boot/script.fex |
1 2 3 4 5 6 7 8 |
[uart1] uart_used = 1 uart_port = 1 uart_type = 4 uart_tx = port:PG06<2><1><default><default> uart_rx = port:PG07<2><1><default><default> uart_rts = port:PG08<2><1><default><default> uart_cts = port:PG09<2><1><default><default> |
完成后保存退出,执行:
1 |
sudo fex2bin /boot/script.fex /boot/script.bin |
然后编辑 /etc/modules文件,向其中加入一行 pps-gpio 并保存;然后再编辑(新建) /etc/modprobe.d/pps-gpio.conf文件,其内容为:
1 |
options pps-gpio gpio_pin=11 |
关于 GPIO 端口号计算的方法可以参考这篇帖子下面的回复,而具体的引脚定义可以在 Sunxi Wiki 找到。
完成上面两处编辑过后重启系统。如果正常应当有 /dev/ttyS[1-3]几个串口设备出现;另外可以找到 /dev/pps0设备并且在 dmesg ( /var/log/messages) 里面看到类似下面的日志:
1 2 |
Feb 2 02:40:21 pallas kernel: [ 3.090904] pps pps0: new PPS source pps-gpio.-1 Feb 2 02:40:21 pallas kernel: [ 3.091024] pps pps0: Registered IRQ 11 as PPS source |
然后我们来接线:
1 2 3 4 5 6 7 8 9 |
+-----+----------+----------+---------------+ | GPS | Physical | GPIO | Pin Name | +-----+----------+----------+---------------+ | VCC | 1 | | +3.3V | | PPS | 5/29 | PA11/PA7 | SCL.0/SIM_CLK | | GND | 6 | | 0V | | RX | 8 | PA13 | UART3_TX | | TX | 10 | PA14 | UART3_RX | +-----+----------+----------+---------------+ |
其中 PPS 线的接法,按照网上各种版本各异的基于树莓派的文章,有 GPIO 4/5/18 和物理针脚 18/23 等多种说法,但我全部试过一遍都不行…最后照着 GPIO 针脚定义找相关的说明试出来 opi 的物理针脚 5(对应 GPIO 11)和物理针脚 29(对应 GPIO 7)可用,我也有一块 GPS 模块可以使用物理端口 7(对应GPIO 6)但别的型号都不行。
如果买到的 GPS 模块是 5V 的,把 VCC 接到物理针脚 2 上面就行了。如果需要外接 RTC 建议用 PA7, 避免和 PPS 抢针脚。
接线完成后观察 GPS 是否正常工作,然后在系统中进行相关测试:
1 2 3 4 5 6 7 8 9 10 11 12 |
allen@pallas:~$ sudo cat /dev/ttyS3 $GPGSV,3,1,12,01,42,178,16,07,63,294,29,08,63,007,25,09,15,234,18*7C $GPGSV,3,2,12,10,02,066,,11,67,198,,16,20,091,,22,03,158,*7B $GPGSV,3,3,12,23,05,196,14,27,29,044,18,28,09,298,29,30,29,316,23*74 allen@pallas:~$ sudo ppstest /dev/pps0 trying PPS source "/dev/pps0" found PPS source "/dev/pps0" ok, found 1 source(s), now start fetching data... source 0 - assert 1485009545.999994434, sequence: 13396 - clear 0.000000000, sequence: 0 source 0 - assert 1485009546.999994875, sequence: 13397 - clear 0.000000000, sequence: 0 source 0 - assert 1485009548.000002813, sequence: 13398 - clear 0.000000000, sequence: 0 |
按照我上面的接线方式,GPS 的数据传输对应在 /dev/ttyS3上,设置正确的波特率后直接 cat 就能看到输出;后一个命令是测试 PPS 信号是否获取正确,注意如果 GPS 没有定位成功是没有 PPS 输出的。
测试成功后先配置 gpsd, 编辑 /etc/default/gpsd文件,设置设备为 /dev/ttyS3并添加 “-n” 参数,具体可参考 AlpineLinux Wiki.
完成后启动 gpsd ( sudo systemctl start gpsd) 并使用 gpsmon 命令观察是否能正确获取数据。
5. 配置 NTP
编辑 /etc/ntp.conf 文件,将其中的 “server xxx” 保留2-3个,其余全部注释掉,然后添加如下内容:
1 2 3 4 |
server 127.127.28.0 #prefer server 127.127.22.0 #prefer fudge 127.127.28.0 refid GPS stratum 9 fudge 127.127.22.0 flag3 1 refid PPS stratum 9 |
这样配置的具体文档可以参考 NTP 文档。
然后启动 ntp ( sudo systemctl start ntp) 等待几分钟后使用 ntpq -p 查看工作状态,正常情况下可以看到类似下面的输出:
1 2 3 4 5 6 7 8 |
remote refid st t when poll reach delay offset jitter ============================================================================== +li461-162.membe 10.84.87.146 2 u 10 16 371 72.431 -14.973 5.654 *2400:8900::f03c 10.84.87.146 2 u 1 16 137 249.315 18.702 4.915 +118.143.17.82 .MRS. 1 u 3 64 51 36.991 -0.460 1.800 +61-216-153-105. 118.163.81.63 3 u 161 64 274 72.461 1.635 2.845 xSHM(0) .GPS. 9 l 13 16 377 0.000 -125.67 2.718 oPPS(0) .PPS. 9 l 12 16 377 0.000 -0.011 0.012 |
对于各列的具体含义见这里的说明,总之结论是从这一输出中获取到 GPS 时间戳和准确时间的偏移值(这个偏移值来自线缆传输、gpsd进程处理等地方)。这个步骤最好等10分钟以上,保证系统已经和一个公网服务器同步完成,延迟和 jitter 都足够小的时候再确定偏移值。
之后我们再次编辑配置文件,改成类似下面这样(设置时钟偏移量、去掉 stratum 指定、设置本机0级时间源为首选):
1 2 3 4 |
server 127.127.28.0 prefer server 127.127.22.0 prefer fudge 127.127.28.0 time1 +0.126 refid GPS #stratum 9 fudge 127.127.22.0 flag3 1 refid PPS #stratum 9 |
重启 ntp ( sudo systemctl restart ntp) 后等待几分钟,再查看和其它服务器的时钟偏移是否正常即可。
1 2 3 4 5 6 7 |
remote refid st t when poll reach delay offset jitter ============================================================================== xli461-162.membe 103.1.106.69 2 u 15 16 345 65.665 -11.538 4.612 +hygiea.home.atr .PPS. 1 u 15 16 376 0.940 0.452 0.802 *SHM(0) .GPS. 0 l 8 16 377 0.000 -0.209 2.926 oPPS(0) .PPS. 0 l 7 16 377 0.000 0.773 0.077 LOCAL(0) .LOCL. 10 l - 64 0 0.000 0.000 0.000 |
就感觉很专业
并不,搜一下会发现网上各种树莓派的…
博主现在还有在运行这个项目么?不知道是否稳定?
看完这篇文章,我也想架一个服务器给内网的机器同步时间用了,想听听博主的意见,谢谢。
在继续跑(放窗台上),opi本身稳定性感觉还行,尚未经过夏季洗礼目前只要不搞事都没问题,除了断电暂时没遇到自动重启之类的;主要是要尽量保证GPS信号足够好,让模块持续处在fix状态,否则在无定位期间时间会出现(相对有定位和有PPS)明显的漂移,但误差调整好了的话基本上可以长期稳定在10ms之内,自用我觉得精度足够了