树莓派(raspberrypi)、saltstack 在线下自助机运维上的应用

目前每家院区都分布了从数台到近百台规模不等的自助机,覆盖了北京市属 22 家医院的三十多个院区,一千多台的日常变更、升级管理、甚至常人看来很简单的开关机成了摆在眼前的一大问题。下面这篇博客会抛出 4 个问题并且分享下我们线上的实战经验。

1. 开关机,一个看似很幼稚的问题
你去市面上问一个的有点经验的运维,「服务器怎么开关机」,他可能第一反应是,「服务器要关机吗?」如果你接着问,「如果现在就有这么一个需要,需要先关机过段时间再开机,怎么办?」,他很可能会告诉你,「远程卡啊,这不是标配吗,再不行,电话个驻场的帮忙开下呗」。 以上的情况对于机房的服务器是适用的,但是对于自助机,放在院内各个大厅角落,物理环境恶劣,随时断网断电,尘埃飞扬,更别谈恒温空调 UPS 之类的,要实现开关机,真不是一件容易的事情。我们在院内用的自助机,说白了就是一台组装的 PC,仅仅是通过串口并口连接了一些读卡器扫码器等外置设备。既然是消费级别的 PC,就不会有远程卡这么「高端」设备,当然,对于 PC 级别来说,市面上是有跟企业级别 iDRAC/iLO/iMana 类似技术的设备的:Intel 的 AMT,最终的实现效果跟远程卡几乎一样,另外一种能实现开机的方式就是通过 Wake-on-LAN 这种古老的协议。

有开就有关,AMT 能开能关,Wake-on-LAN 就没办法了。但是基本思想很简单,需要在每台机器上安装一个 agent,通过此 agent 来下发关机以及后续的变更操作。

有了上面的思路,我们来对比下 AMT 以及 Wake-on-LAN 这两种方案的优劣,如下图:

对于目前的业务来说,我们最看重的是经济成本以及可扩展性,很明显,Wake-on-LAN 完爆 AMT。

有了大方向,下面是具体的实现,首先是至少有一台常年开机的机器用来唤醒其他的机器,选自助机肯定不行,这些机器本身就会出现白天开机晚上关机的状态,后来想到了树莓派,设备小,可以完全放进一台自助机里面,只要该自助机的电源线不拔,就能保证树莓派的常年开机状态;管理简单,默认使用的 Raspbian 系统,源的同步,账户的管理,监控、变更等等跟我们线上完全打通;经济成本低,一台树莓派配上 SD 卡电源线插座传感器(这个后面会单独提)不到 400。有点需要注意的是,很多情况下院内会有多个 vlan,正常情况下 Wake-on-LAN 不支持跨网段的唤醒,一般来说,放在不同位置的自助机网段会不大一样,加上我们需要监控不同位置的温湿度烟雾的状态,所以最终决定出单家院区 4 台树莓派的标准配置。下图是一套树莓派的配置图:

搞明白了上面的核心问题,接下来要解决的是流程类的问题,每上一个新院区,我们会直接格式化 SD 卡灌装定制的 raspbian 系统,salt 初始化,zabbix/freeipa 注册,实施人员到现场之后插电接网即可。

2. 如何优雅的开关机
上面的话题仅仅覆盖了基本的原理,在我们上线的早期,每个院区仅仅上线了个位数的机器,同时运行的院区在三四个左右。这里需要提到一个背景,每个院区的部分自助机,同一个院区的不同自助机,开关机时间都不同,比如友谊位于门诊大厅的自助机是每天早上 7:00 准时开机对外使用,但是二三楼的自助机则是每天早上 07:10 启用,还有一台位于急诊大厅的机器则是全年 24 小时开机,而对于同仁医院来说,所有的机器都是早上 06:45 开机。

早期,我们通过 saltstack/jenkins/git 在树莓派上维护对应的院区的 crontab 列表来实现自助机的开关机。

另外一个频繁出现的问题是,院方会经常性的提出修改自助机开关机服务时间的需求,每次运营转达给我们再经过 git/jenkins 提交的效率异常的低效。为了彻底的将这部分重复低效的工作从运维手里解放出来,我们为院方开发了一套开放平台(open),除了日常的开发者文档更新、开发接口 API 调用,接口测试用例等之外,一块重要的功能就是为院方开放了自助机服务器时间的自主修改查看权限,这样将日常繁杂的自助机时间管理下移到了院方手里。

目前该平台支持全局、个别自助机的开关机时间:

bangkok 上面的 KioskScheduler 进程定期会向 open 平台同步各个院区所有自助机的开关机时间,将其写入到 MySQL,底层通过 APScheduler 这个任务框架来调度,使用 BackgroundScheduler 调度器,默认的 MemoryJobStore 作业存储。每家院区的规则都分为全局跟单独两种,全局规则针对该院区的所有自助机有效,优先级低;单台自助机规则仅针对单台自助机规则生效,优先级高。KioskScheduler 根据以上优先级的不同,优先执行高优先级的规则。这样「积水潭 2017 年春节期间,除了 10、11 号自助机在每天的 08:00 ~ 16:00 开机之外,其余所有的自助机均关机」的规则就能顺利实现了。

同时,为了确保 KioskScheduler 运行正常,应用层面通过 monit 实现进程的监控,业务层面的规则执行与否以及是否达到预计,我们通过 python-nmap 实现了一个批量扫描的脚本,每次开关机时间点触发后的 5min/10min/15min 三个阶段,对命中规则的自助机进行批量的存活性扫描,对于未达到期望的自助机会触发报警到我们的自助机运维同事那边进行人工处理。

对于开机的方式,部署在院内的 4 台树莓派通过 syndic 机制收到请求后同时向目标机器发起唤醒的请求,根据之前的经验,单台树莓派的唤醒偶尔会出现唤醒失败的情况,另外一点 4 台里面只有保证至少有 1 台存活,那么开机就能成功触发。

3. 如何低成本实现 OOB 的核心功能?
先来看下行业标杆的 Dell 远程卡(OOB)提供的一些核心功能,如下图:

根据我们日常使用频率的统计,80% 以上的操作花在了如下的操作上:

1. 服务器的开关机,尤其是机器 hang 住了/kernel panic 之后的开关机
2. 由于各种原因无法 ssh 之后需要通过虚拟终端进入查看当前机器屏幕的状态
3. 机器主要零部件的硬件状态,健康状况

对于一台自助机来说,其核心是简化后的服务器,没有 raid 等企业功能,除此之外,提供的服务跟服务器并无二异,仅仅是加上了 GUI 而已。对于第一、二两点,上面的话题已经阐述过,比较遗憾的是目前并不能对机器的死机、蓝屏实现脱离系统的开关机重启以及实时屏幕的查看,但是对于日常的开关机,VNC 远程操控,绰绰有余;对于第三点,目前诸如磁盘等底层硬件的监控是放在应用层来监控,对于我们来说,个别自助机硬件层面的宕机并不会影响整个院区的使用,我们及时报修返厂更换即可解决此类问题,众所周知的是,对于绝大多数的医院,其院内的物理环境,比如尘埃浓度(PM10)、温湿度等跟正规的机房比相距甚远,早期,我们发现某院区的部分的自助机经常性的宕机蓝屏,经过若干天系统级别应用级别的排查,都无所发现,后来现场的志愿者提醒我们是不是温度太高导致的,现场勘查后发现,西区的部分自助机直接放在了仅有一顶透明帐篷遮蔽的半室外空间内,6 月市内温度 30 度的情况下,这部分自助机箱体的温度已经接近 40 度了,蓝屏在所难免了。

为了及时发现这类问题,我们后续在所有树莓派上全部引进了温度、湿度以及烟雾的传感器,成本非常低,一个 DHT11 的温湿度传感器加上一个 MQ-2 的烟雾传感器成本在 20RMB 以内,加上现成的 Adafruit_DHT/RPi.GPIO 的库直接调用,完美解决此类问题。通过分散在不同地点的四台树莓派,我们能够推断出当前院内的物理环境,这对改进目前自助机的物理位置有非常重要的意义。

下图是某院区蓝屏故障增大期间周边温度的变化,观察可以发现具备一致性关系:

类似的,我们在院方机房放置的连接专线的路由设备,同样发现多次类似的问题,下图是某院区机房空调设备故障温度升高导致设备丢包增大中断服务的监控:

很多看似简单的问题,一旦场景转变了,需要应对的措施也就随之改变了。

4. 如何有效实现自助机的变更操作
自助机上的变更,主要分为两类,一种是最为频繁的版本升级,包括自助机上挂号缴费程序的版本、第三方依赖的程序升级,这类变更的特点是,每次的程序升级,其升级包都相对较大,小的几兆,大的有上百兆;第二种变更更多的集中在日常的系统变更,比如修改 hosts 以对应不同的 nat 影射、增删改查注册表等系,对于这类变更,最大的特点是占用的带宽小,但是操作频繁。

针对这两种变更,分别设计了两种不完全一样的变更方式,但是底层都是通过 saltstack master/syndic/minion 的架构实现文件的传输。中心端一台 master(存在单点的问题),每家院区 4 台部署了 syndic 的树莓派,同时跑 master/syndic,向中心端注册,底层的自助机部署有 minion 向其中一台 syndic 注册,目前 minion 到 syndic 同样存在单点问题,后续考虑将 syndic 升级成 MultiSyndic。

对于第一种变更方式,我们将要升级的版本库文件打包上传至 git,通过 jenkins 将数据从打包机 git pull 下来之后,在打包机通过 salt 将升级的文件分发到 salt syndic 上,syndic 上会起 http 服务,自助机每次开机的时候会自动的向 http 服务检查是否有最新的版本,如果有的话则会升级,对于回滚,我们将版本号递增内容回滚至上一版本重启机器即可。
对于第二种变更方式,如果是修改注册表之类的变更,我们使用 cmd.run 直接将需要跑的 key 执行结束,类似下面这样:
cmd.run 'reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" /v "{5399-XXX-CBA0}" /t reg_dword /d 1  /f'

如果是传输文件的变更,跟第一种方式类似,不同的是,文件落地到 syndic 之后,我们会直接通过 master/minion 的方式 push 到每台自助机,而不是主动 pull 的方式。同时为了确认文件的完整,每次从 master 到 syndic,从 syndic 到 minion 的两个关键步骤都会做一次 md5 校验,最终实现的效果可以参考早期的 saltpad 版本,下图是 bangkok 中变更的页面展示:

除了最基本的单次变更外,增加了多次执行的执行步骤以及执行步骤的模版功能,前者对于多合一的操作步骤很适用,后者对于经常使用的诸如修改 hosts 文件等很适用。

在这之前,我们尝试直接通过 salt master/minion 的方式进行日常的变更管理,这种方式对于线下的实施同事来说非常的不友好,其灵活性以及方便性远低于目前的方案,遂未在团队内推广使用。上面介绍的系统是目前在线的稳定系统,使用了一段时间整体反馈还不错,后续会优化版本控制以及更加自动的回滚等操作。

resolv.conf 的超时(timeout)与重试(attempts)机制

/etc/resolv.conf 有两个默认的值至关重要,一个是超时的 timeout,一个是重试的 attempts,默认情况下,前者是 5s 后者是 2 次。
这个估计很多工程师都不是很在意,一般情况下,使用默认的值倒没什么大问题,特殊情况我会在最后说明。

要测试,不要使用 dig, host, nslook 这类工具,因为他们并没有调用 resolver 的库,可以使用 getent 来测试。上面提到的只是一些诊断的工具,对于日常的应用来说,包括 web server、mail client、db 以及各种 app server 等等,任何使用 glibc resolver 都需要经过 resolv.conf 文件。

对于 libresolv 来说,只认 resolv.conf 的前三个 nameserver,所以写的再多也没什么意义。正常情况下,resolver 会从上至下进行解析,每个 nameserver 等待 timeout 的时间,如果一直到第三个都没结果,resolver 会重复上面的步骤 (attempts – 1) 次。

为了真正理解,例如下面这个场景:
search test.example.com example.com  
nameserver 192.168.0.1  
nameserver 192.168.0.2

假设 192.168.0.1 不返回结果(可能根本就不是台 DNS),我们假设需要解析 "www.devel",而这个仅仅在 www.devel.example.com 里面有记录,下面是整个执行的步骤:
1. "www.devel" on 192.168.0.1, wait until timeout (default 5 secs)
2. "www.devel" on 192.168.0.2, get reply: no such hostname
3. "www.devel" on 192.168.0.1, wait until timeout (default 5 secs)
4. "www.devel" on 192.168.0.2, get reply: no such hostname
5. "www.devel.test.example.com" on 192.168.0.1, wait until timeout (default 5 secs)
6. "www.devel.test.example.com" on 192.168.0.2, reply no such hostname
7. "www.devel.test.example.com" on 192.168.0.1, wait until timeout (default 5 secs)
8. "www.devel.test.example.com" on 192.168.0.2, reply no such hostname
9. "www.devel.example.com" on 192.168.0.1, wait until timeout (default 5 secs)
10. "www.devel.example.com" on 192.168.0.2, reply with IP address

默认情况下是 5s 超时,我做了两个简单的测试,把 resolv.conf 的前三个 nameserver 全部换成不存在的 1.1.1.1, 2.2.2.2, 3.3.3.3,然后可以观察下面 strace 跟踪的结果,对于 ping 以及 getent 来说,已经算是上层的应用结果了。
1. strace -t getent hosts baidu.com
2. strace ping baidu.com

把 timeout 设置为 1s 的结果可以看下面这个测试结果:
strace -t ping baidu.com(options timeout:1)

对于生产有什么意义了?
对于任何的代码,不管何种形式的(HTTP 的也好,DB 的连接也罢),只要是一端对另外一端的连接,都应该加上超时重试以及异常处理的。上面其实一共涉及三个点:
1. 超时,目前绝大多数的 API 都支持这类参数,包括我们线上用的 grequestsMySQLdb 等等
2. 重试,对于代码层面同样重要,跟上面类似
3. 异常处理,好的代码对于无法连接或者连接超时等问题应该及时的抛出异常打 log,而不是什么都没有,否则会加大问题定位的工作量,后期维护起来也会异常的麻烦。

这里有个有意思的脚本能检测最佳的 DNS。

最后,记得 dig, nslook 只会解析 resolv.conf 的内容,而不会解析 hosts 里面内容,所以如果想让 dig 解析 hosts 里面的内容,可以通过 dnsmasq 实现。

ref:
https://access.redhat.com/solutions/21420
http://serverfault.com/questions/562079/adjusting-how-long-linux-takes-to-fail-over-to-backup-dns-server-listed-in-resol

如何部署一个稳定高效可扩展的前端

一切的工程都要从实用的角度出发,排除 GLSB 在外,目前主流的开放的前端无非就那几种:
1. 最简单的就是 DNS RR,上手很快,缺点也很明显
2. web server + keepalived,相对智能些,依然有缺点,没有 health checker
3. 目前主流的方案 LVS(ipvs) + keepalived + web server(Nginx/Tengine),如果规模比较大,可能还会涉及到 OSPF

前端的地位不言而喻,出问题大家都只能大眼瞪小眼,保证稳定是其首要目标。这里总结一些我们线上的通用问题(前期的 benchmark 就不说了),其基本原理不仅仅适用于前端,中间件、DB 同样适用,举一反三非常重要。

Tengine
在调研比较了 Tengine, Nginx 之后,我们还是决定使用 Tengine,主要原因之一是其添加了一些非常诱人的功能,再加上我们后面有大量的技术支持,所以不是很需要担心 Tengine 本身的问题。后来偶然发现,@chobits 同学在全职开发 Tengine,就更不要担心了,全程也拉他帮我们 debug 了不少问题,提供了不少非常实用的建议,再次感谢。
1. 其支持的 reqstat 模块可以实时的针对不同的 location、server 统计到当前的 return code,这个对于整个 web server 的监控是最重要的一环。
2. 其次,对 log 部分做了改进,支持 syslog/rsyslog,这样可以直接打到后端,方便后续的归档分析。
3. upstream 几个对应的模块可以更好的检测后端机器的健康状况,不过我们没实用这个 module,而是自己编译的 fair module。
4. 另外,还支持一致性 hash、session sticky 以及灰度发布类似 tcpcopy(不推荐使用)/gor(推荐使用) 的 httpcopy 等特性,总的来说,确实比较实用。

LVS/keepalived
这个没什么特别要说的,首先是模式的选择了,目前主流就是 NAT/FNAT,DR,前者更安全,后者在出现某些问题的是时候切换 DNS 比较方便。有点要特别强调的是,keepalived 的 syntax 问题,默认没有任何的检查,我们之前少写了一个配对的括号竟然也能起起来,不禁让人感叹一下,后来找到了一个叫 keepalived-check 的工具,小巧方便,真应该直接编译到 keepalived 里面。除此之外,还有针对 vim  syntax 高亮插件。

debug
该开 debug 的初期最好开起 debug,这样能发现很多的问题。
keepalived,最主要就是看 messages 文件
tegine,把 debug 的 module 编译进去就可以看到更详细的信息了,包括一些非常重要的系统调用,recv() 在哪里出问题等等,我们通过此信息发现了不少的 upstream 问题;其次就是 log format 的定义,尽量定义的详细些,同样的能发现不少问题,多看看 error log 以及 access log,通过实时的 reqstat 就能一步一步的细化。

网络
直接上的万兆,涉及的方面还比较多,可以看我之写的一系列文档,概括一下包含下面的一些内容:
1. 路由的添加,/etc/sysconfig/network-scripts/route-INTERFACE,别在 rc.local 里面加了,太土鳖了
2. 中断的绑定,万兆的都是多队列,这个不做其他的没任何意义
3. ring buffer 调整,GRO 关闭等等
4. kernel 方面的,下面会提到

版本&打包
1. 自己编译,添加一些需要的 module,比如 fair,记得开启 –debug 模式
2. 实用 SysV 来管理进程,统一由 /etc/ini.d/ 下面的脚本来管理

内核
肯定是要重新编译的(CONFIG_IP_VS_TAB_BITS=20),下面包含一些基本的 TCP 优化
1. rmem_max/wmem_max
2. rmem_default/wmem_default
3. tcp_rmem/tcp_wmem/tcp_mem
4. netdev_max_backlpg
5. somaxconn/nginx backlog
6. timestamp/tcp_reuse/tcp_recycle

监控到位
zabbix 的 item,要么通过 trigger 来体现,要么通过 graph 来体现,如果存在于第三者,就没有存在的必要了,很少会到 "Latest data" 里面去挖来挖去,你自己的写的说不定都会忘记。
1. 系统层面的不细说,但是像 DNS resolver, NTP, fork rate, CPU freq(BIOS), 10000M 速率双工, ethtool -S 下面的一些 discard/dropped 很可能会忽略掉,这些 item 其实是非常重要的,关键的时候能派上大用途
2. 基本的 ping、curl 监控,为了确保 LVS 到 RS 的基本连通行,ping、curl 必不可少,这个在出问题的时候能第一时间确定是否是小范围或者大面积的网络故障
3. LVS 相关的,包括进程,链接情况(ipvsadm),rs 的情况等等,这个通过 zbx LLD 还是很方便的
4. Nginx 基本的 req, conn,以及 return code,rt 等等,不少都是直接从 Tengine 获取到的
5. proc 下面的 fd, open file, proc,Nginx 的比较诡异,目前已经遇到至少三次 open file 由系统值变为 1024 的,这方面监控必不可少
6. 全国各地的请求监控,这个一般是第三方的服务来帮助发现问题了

log 处理
1. 目前是本地 logrotate 一份,关闭了 compress,避免短时间内 IO 过高 LVS 以为 RS 出问题了
2. 通过 syslog 远端到另外一台机器上做压缩存档
3. 除此之外,Tengine 可以抽样获取 log,这样对服务器的压力可以大大减小

整个过程就是不断发现问题,解决问题的过程,及时的记录 post-mortem,及时的 review 当前完成的状况,这样整个系统才会朝更好的方向发展。之前发现有同学写完 post-mortem 就扔在一边不管了,虽然写了改进措施,但是由于没有及时的更进,导致的没有真正的完成需要改进的工作,结果是相同的问题继续出现,这就失去了本身的意义了。题目的「稳定高效」全篇都在提。可扩展如果是 10G 或者 20G 以下,可以通过加 RS 的方式完成,当然在这之前需要确定前端网卡能否以 wire rate 抗过 10G 或者 20G 甚至 40G(bonding) 的量,尤其全是小包的极端情况。当然,一般这么大的量已经很少只通过一个出口完成了,一般 OSPF 结合多 uplink 或许能够支撑住这么大的量。
任何一个子系统子模块都可以遵循上面提到的的一些方面以及思路来完善。

NX-OS(N7K) 也能跑崩(Nexus 7000 Stuck Sending TCNs Every 2 Seconds)

题目有点夸张,但是确实对我们的生产环境造成了很大的影响,越是基础的部件出现问题造成的损失越大。这个是触发该 bug 的两个时间段内,一台前端机器到部分后端机器的丢包情况监控。

这个 bug 跟我两年前碰到的 2.6.32 内核的 208.5 bug(1, 2) 倒是很像,弄不好又是哪个无证码农犯了除以 0 了。

发现这个 unicast flooding 的特征还是蛮明显的,比如 iftop 发现竟然出现了其他主机之间交互的流量,tcpdump 抓包也能观察到类似的现象,除了一些 ARP、HSRP 的请求回应之外不应该有大量的其他的包了。这篇文档总结了出现了该情况的几种可能。

总的来说,NX-OS 虽然已经上市好几年了,但还是不够成熟,无意中还发现可能存在 kernel panic 的风险。考虑到可能有人因为权限的问题无法打开上面的链接,我这里附一份完整的存档:

Nexus 7000 Stuck Sending TCNs Every 2 Seconds
CSCuo80937

Symptom:
If there is any TC after upgrade to 6.2.(6), 6.2.(6a) or 6.2.(8), then after
approximately 90 days of active supervisor uptime, STP TC BPDUs are sent out
every 2 seconds for a long period of time.

Conditions:
Nexus 7000 or 7700 running 6.2(6), 6.2.(6a), or 6.2(8).

Workaround:
In order to circumvent this issue until an upgrade to fixed code can be
performed,
execute the appropriate workaround depending on whether you have a
dual-supervisor or single-supervisor
configuration before each 90 days of Active supervisor uptime.

For dual-supervisor setups:
1. Reload the standby supervisor using cli "reload module x" where x is
standby supervisor slot number.
2. Use the 'show module' command to confirm that the standby supervisor is up
and in the ha-standby mode.
3. Use the system 'switchover command' to switch to the standby supervisor.

For single-supervisor setups:
1. Upgrade to 6.2.6b or 6.2.8a, depending on your business requirements.
2. Reload the switch.

Further Problem Description:
Active Supervisor Uptime can be found from "show system uptime":
N7K-7009-3# show system uptime
System start time: Fri Oct 25 09:40:58 2013
System uptime: 236 days, 8 hours, 56 minutes, 59 seconds
Kernel uptime: 110 days, 23 hours, 7 minutes, 49 seconds
Active supervisor uptime: 110 days, 23 hours, 2 minutes, 23 seconds <<<<<<<<<<<