Docker集群性能数据监控与简单选型

Docker要在生产环境上使用,监控工具必不可少。传统物理机或着虚拟机通常可以通过部署一些agent收集数据,上报到中心节点进行数据聚合和存储,再通过一些UI工具进行可视化展现。与之相比,容器因为它的轻量级特性,一个容器内部通常只包含一个服务进程,通过在容器内部部署额外的性能数据采集agent非常不优雅;另一个原因,top、free、sysstat等常用性能诊断命令在容器中往往拿到的是宿主机的数据,无法准确获取容器数据。因此各种在宿主机上采集容器数据的工具(链)冒了出来。

市面上关于Docker的监控工具五花八门,但是相对成熟的却又很少。Google的cAdvisor已经是属于较成熟的一类,它会在每个宿主机节点上部署cAdvisor进程,默认对外暴露8080端口,用户可通过其提供的Web服务访问当前节点和各容器的性能数据(CPU、内存、网络、磁盘、文件系统等等),非常详细。展现方式有两种,一种是API,返回标准JSON格式数据,另一种是友好的图表方式,如下图:
cAdvisor

默认cAdvisor是将数据缓存在内存中,因此数据展示能力有限,当然它也提供不同的持久化存储后端:云端的Google BigQuery或者本地端的InfluxDB,通过-storage_driver启动参数指定。

当使用InfluxDB作为cAdvisor的存储后端时,cAdvisor会在InfluxDB中建立一个名为stats的series,所有容器性能数据都存储到这张表里,主要记录数据包括time、tx_errors、rx_bytes、container_name、rx_errors、fs_limit、memory_usage、memory_working_set、fs_device、cpu_cumulative_usage、machine、tx_bytes、fs_usage等等。

在一个大规模部署的Docker集群中,往往会选用各种集群管理和Docker编配工具,比如KubernetesFigSwarm等。网上各种Docker集群工具的选型文章也多得很,可以参考阅读。只能说,由Google在主推,并且有其闭源版本Borg在Google内部稳定运行10多年的口碑,Kubernetes(k8s)虽然出来得晚,但是社区发展得相当快,大有后来居上的势头。

那么假设选择了k8s作为Docker集群管理工具,容器性能数据监控如何选型?

k8s会在每个node(minor)节点上部署一个Kubelet,默认暴露10250端口,Kubelet主要负责容器的生命周期管理和状态维护,以及监控数据采集。实际上,Kubelet也是通过cAdvisor来采集容器性能数据的,所以需要在Kubelet的启动参数中增加--cadvisor_port参数,它表示本地的cAdvisor服务端口号。Google同时也提供了开源组件heapster,用作对Docker集群的监控,heapster原生支持k8s和CoreOS中容器的性能数据采集。当heapster配合k8s运行时,需要指定kubernetes_master的地址,heapster通过k8s得到所有node节点地址,然后通过访问对应的node ip和端口号(10250)来调用目标节点Kubelet的HTTP接口,再由Kubelet调用cAdvisor服务获取该节点上所有容器的性能数据,并依次返回到heapster进行数据聚合。heapster聚合的metric可分为以下几类:

uptime
cpu/usage
memory/usage
memory/page_faults
memory/working_set
network/rx
network/rx_errors
network/tx
network/tx_errors
filesystem/usage

同cAdvisor类似,heaspter也支持多种存储后端,比如默认的memory,表示存内存,另外还有influxdb、bigquery、gcm,可由-sink启动参数指定。如果持久化到InfluxDB,那么根据metric的分类,InfluxDB会生成以下series:

cpu/usage_ns_cumulative
filesystem/usage
memory/page_faults_gauge
memory/usage_bytes_gauge
memory/working_set_bytes_gauge
network/rx_bytes_cumulative
network/rx_errors_cumulative
network/tx_bytes_cumulative
network/tx_errors_cumulative
uptime_ms_cumulative

这里需要注意的是,metric的分类有两种,一种是类似cpu使用时间、网络流入流出量,聚合的是累计值,在serie名称中用cumulative表示,另一种是类似内存使用量,聚合的是瞬时值,在serie名称中用gauge表示。

有了InfluxDB的数据持久化存储之后,剩下的就是数据的展现功能,可以说,任何支持InfluxDB作为存储后端的UI系统(dashboard)都可以用来作为展现层,比如InfluxDB官方自带的图形界面,也可以对接Grafana,页面相当酷炫。作为一个平台管理人员,用heapster(cAdvisor) + InfluxDB + Grafana应该可以取得不错的用户体验,但是如果把k8s用在多租户的场景下,由于Grafana的可编程能力有限,对接起来不是很合适,较好地方式还是基于InfluxDB的原始数据(支持类SQL查询)自己实现UI,这样与容器集群管理平台的契合度也高一些。

--EOF--

RabbitMQ不同Confirm模式下的性能对比

前几天看到一篇文章『Evaluating persistent, replicated message queues』,作者比较客观地分析了多种分布式消息服务器间集群和消息可靠传输机制,比对了各自的性能情况,他的测试场景为:

1. 分布式队列,节点间数据复制(同步、异步)
2. 消息可靠性等级最高(持久化、ack等)

由于不同的消息服务器实现原理不同,会造成集群节点间数据复制代价和消息可靠性上的差异,最终文章给出的基准性能测试数据(消息吞吐量维度)总结如下图:
benchmark-summary

从上图来看,Kafka毫无争议的拥有最大的消息吞吐量。但是RabbitMQ的数据却是有点反直觉,因为之前给人的感觉RabbitMQ作为一款工业级的消息队列服务器,虽说不是靠高性能扬名,但也不至于让性能问题成为累赘。

网上的基准测试结果只能作为参考,不能作为技术选型的依据。当我们看到某类产品(Web服务器、缓存、队列、数据库等)的一组性能测试数据时,首先要了解以下三点:

1. 作者是否利益相关,利益相关往往导致给出的数据和结论偏向自家产品。
2. 作者是否能hold得住不同产品的技术细节,有时候一个参数值的优化会影响到产品的表现。
3. 具体的测试场景和参数。这个不用多解释了,每个产品都有自己擅长和不擅长的使用场景。

对于上述的第2点和第3点,本质上是由于信息不对称造成的。本文的目的不是为了质疑那篇文章的结论,而是借此分析一下是否可以通过一些客户端程序优化来提升RabbitMQ性能。

之前自己也做过RabbitMQ的性能测试,对于固定消息体大小和线程数,如果消息持久化、生产者confirm、消费者ack三个参数中开启消息持久化和生产者confirm,那么对性能影响相当致命,能够衰减一个数量级,吞吐量甚至会退化到几百msg/s。

消息持久化的优化没太好方法,用更好更快的物理存储(SAS,SSD,RAID卡)总会带来改善的。生产者confirm这一环节的优化则主要在于客户端程序的优化上。归纳起来,客户端实现生产者confirm有三种编程方式:

1. 普通confirm模式。每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm了。
2. 批量confirm模式。每次发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm。
3. 异步confirm模式。提供一个回调方法,服务器端confirm了一条(或多条)消息后SDK会回调这个方法。

从编程实现的复杂度上来看:

第1种普通confirm模式最简单,publish一条消息后,等待服务器端confirm,如果服务器端返回false或者超时时间内未返回,客户端进行消息重传。

第2种批量confirm模式稍微复杂一点,客户端程序需要定期(每x秒)或定量(每x条)或者两者结合来pubish消息,然后等待服务器端confirm。相比普通confirm模式,批量可以极大提升confirm效率,但是问题在于一旦出现confirm返回false或者超时的情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且,当消息经常丢失时,批量confirm性能应该是不升反降的。

第3种异步confirm模式的编程实现最复杂,Channel对象提供的ConfirmListener()回调方法只包含deliveryTag(当前Channel发出的消息序号),我们需要自己为每个Channel维护一个unconfirm的消息序号集合,每publish一条数据,集合中元素加1,每回调一次handleAck方法,unconfirm集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。从程序运行效率角度上看,这个unconfirm集合最好采用有序集合SortedSet存储结构。实际上,SDK里的waitForConfirms()方法也是通过SortedSet维护消息序号的。

我写了一个简单的RabbitMQ生产者confirm环节性能测试程序放在了github上,它实现了上述三种confirm模式,并且有丰富的参数可以配置,比如生产者数量、消费者数量、消息体大小、消息持久化、生产者confirm、消费者ack等等,可以根据使用场景组合。以下的讨论都是基于这个测试程序跑出来的结果。

首先是测试环境:

OS: OSX 10.10, Darwin 14.0.0, x86_64
Erlang: R16B03-1 (erts-5.10.4)
RabbitMQ: 3.5.1
CPU: 2.5 GHz Intel Core i5
Disk: SATA
Message Size: 1000 Bytes

一、单线程,未开启消息持久化和消费者ack。

普通 批量,50 msg/批 批量,100 msg/批 批量,200 msg/批 异步
2931 msg/s 6581 msg/s 7019 msg/s 7563 msg/s 8550 msg/s

可见,单线程跑时批量和异步confirm甩开普通confirm一大截了。严格来讲,异步confirm不存在单线程模式,因为回调handleAck()方法的线程和publish消息的线程不是同一个。

二、多线程,开启消息持久化和消费者ack。

多线程下普通confirm模式:

100线程 500线程 800线程 1000线程
659 msg/s 2110 msg/s 2353 msg/s 2477 msg/s

多线程下批量confirm模式:

100线程,50 msg/批 100线程,100 msg/批 100线程,500 msg/批 500线程,100 msg/批
3828 msg/s 3551 msg/s 3567 msg/s 3829 msg/s

多线程下异步confirm模式:

50线程 100线程 200线程
3621 msg/s 3378 msg/s 2842 msg/s

以上是不同线程数量的维度下,相同confirm模式的性能数据,大致来看,遵循线程数越大,吞吐量越大的规律。当然,当线程数量达到一个阈值之后,吞吐量会下降。通过这些数据还能得到一个隐式的结论:不论哪种confirm模式,通过调整客户端线程数量,都可以达到一个最大吞吐量值。无非是达到这个最大值的代价不同,比如异步模式需要少量线程数就能达到,而普通模式需要大量线程数才能达到。

最后再从相同线程数量(100线程数)的维度下,分析下不同confirm模式的性能数据:

普通 批量,50 msg/批 批量,100 msg/批 批量,500 msg/批 异步
659 msg/s 3828 msg/s 3551 msg/s 3567 msg/s 3378 msg/s

由此可见,选取了一个典型的线程数量(100)后,普通confirm模式性能相比批量和异步模式,差了一个数量级。

从以上所有的数据分析来看,异步和批量confirm模式两者没有明显的性能差距,实际上他们的实现原理是一样,无非是客户端SDK进行了不同的封装而已。所以,只需从可编程性的角度选择异步或批量或者两者结合的模式即可。相比而言,选择普通confirm模式只剩编程简单这个理由了。

回到本文开头提到的不同队列服务之间的性能对比,实际上,我认为RabbitMQ最大的优势在于它提供了最灵活的消息路由策略、高可用和可靠性,可靠性又分为两部分(消息可靠性和软件可靠性),以及丰富的插件、平台支持和完善的文档。然而,由于AMQP协议本身的灵活性导致了它比较重量,所以造成了它相比某些队列服务(如Kafka)吞吐量处于下风。因此,当选择一个消息队列服务时,关键还是看需求上更看重消息吞吐量、消息堆积能力还是路由灵活性、高可用、可靠传输这些方面,只有先确定使用场景,根据使用场景对不同服务进行针对性的测试和分析,最终得到的结论才能成为技术选型的依据。

--EOF--

单机磁盘故障引发RabbitMQ镜像队列数据丢失

某分布式消息推送系统,用RabbitMQ做消息分发。RabbitM采用镜像队列模式、双机HA部署,前端使用负载均衡,因此RabbitMQ主从节点上都会有生产者和消费者连接。此为应用背景。

版本:
OS: Debian 7, 3.2.39-2 x86_64 GNU/Linux
Erlang: R15B01 (erts-5.9.1)
RabbitMQ: 3.0.2 (ha policy, ha-mode:all)

故障现象还原:

1. 节点A的数据盘故障(磁盘控制器故障、无法读写),所有原本A上的生产者消费者failover到B节点。
2. 从节点B的WebUI上看,所有队列信息(包括队列元信息和数据)丢失,但是exchange、binding、vhost等依旧存在。
3. 节点B的日志中出现大量关于消费请求的错误日志:

=ERROR REPORT==== 23-Apr-2015::19:31:26 ===
connection <0.1060.0>, channel 1 - soft error:
{amqp_error,not_found,
"home node 'rabbit@192.168.156.210' of durable queue 'test' in vhost 
'2891f31d' is down or inaccessible", 'basic.consume'}

4. 从生产者端看来一切正常,依旧会收到来自节点B的confirm消息(basic.ack amqp方法)。

经过一番排查定位,上述现象实际上有两个坑在里面:

一、 在数据可靠性方面,镜像队列也不完全可靠。
二、 要保证消息可靠性,生产者端仅仅采用confirm机制还不够。

对于第一个坑:

第一感觉应该是bug,其他人也出现过。在云环境下要重现这个Bug很简单,申请两台云主机做RabbitMQ HA,部署镜像队列。另外申请两块云硬盘,分别挂载到两台云主机,作为数据盘为RabbitMQ提供元信息和队列数据提供存储(${RABBITMQ_HOME}/var/lib),模拟生产者和消费者对主节点(假设为节点A)进行生产消费,此时可以从云平台管理界面上卸载节点A上的云硬盘,观察节点B RabbitMQ WebUI,可以发现队列不见了。:)

换上最新版的3.5.1就没这个问题,但是要确认是哪个版本对它进行修复的却耗费了很久。初步翻阅了3.0.3 ~ 3.5.1之间大概20多个版本的Changelog,没有看到哪个bugfix的描述符合。只好用最笨的办法,对所有版本号采用二分法,挨个版本测试,最终将修复版本确定为3.4.0 (Changelog)。但是即便如此,也还是没有看到具体是哪个bugfix修复了此问题。

对于第二个坑:

对于队列不存在了,RabbitMQ依然向生产者返回confirm消息的情况,实际上通过生产者端的正确编程姿势可以避免。RabbitMQ官方有过说明:

For unroutable messages, the broker will issue a confirm once the exchange verifies a message won't route to any queue (returns an empty list of queues). If the message is also published as mandatory, the basic.return is sent to the client before basic.ack.

也就是说,对于那些路由不可达的消息(根据routerkey,找不到相应队列),如果basic.publish方法没有设置mandatory参数(关于mandatory参数,可以参考『AMQP协议mandatory和immediate标志位区别』),那么RabbitMQ会直接丢弃消息,并且向客户端返回basic.ack消息;如果basic.publish方法设置了mandatory参数,那么RabbitMQ会通过basic.return消息返回消息内容,然后再发送basic.ack消息进行确认。

因为节点上的队列元信息在第一个踩的坑里已经丢失了,所以所有发送到此节点上的消息必然是不可到达的,加上未在basic.publish方法上加mandatory参数,因此消息直接被RabbitMQ丢弃,并被正常confirm掉。

要避免踩这个坑,Java SDK的Channel对象提供了addReturnListener()回调方法,当收到服务器端的basic.return消息时,ReturnListener方法被调用,生产者端可以进行消息重传。当然,前提是basicPublish()方法的mandatory参数已经设置为true。

--EOF--

小内存VPS站点内存使用调优(续)

昨天在『小内存VPS站点内存使用调优』中介绍了在VPS中打开swap分区,解决物理内存不足导致的OOM问题。本以为有了1G swap分区(SSD)做后盾,php-fpm的pm.*参数可以调大一点:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 10
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 5
pm.max_requests = 1000

结果今天DNSPod继续发来报警邮件,网站不可用,不过这次不可用的原因不是内存不足,而是IO太忙,Nginx提示504 Time-out。

先SSH到VPS上查看系统状态:
top-wa

上图是top命令的执行结果,异常表现在load很高(单核,10+),CPU使用率很低12.7%,大部分时间在等待IO,71.8% wa。

top命令看不出哪个进程在io wait。于是用apt-get安装iotop命令,它可以用来查看每个进程的IO情况:
iotop

通过左右箭头可以指定按某一列排序,按DISK READ列排序后,问题很直观了,清一色的php-fpm worker进程,并且都是99%左右的时间在swapin,所以问题定位在swap分区的使用不合理上。

swap分区本质是用磁盘模拟内存,它的IO特点是随机读写,即便是号称SSD,相比HDD延时低了不少,看来还是达不到系统使用要求。不知道DigitalOcean有没有对磁盘的IOPS进行Qos限制,从iotop上显示读速度62.85M/s来看,跟基准数据相比,处在偏下的水平。

定位出问题的原因之后,只能继续折腾php-fpm配置参数:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 5
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 3
pm.max_requests = 1000

另外,调整内核vm.swappiness参数(取值0-100,默认60)。swappiness数值越大,表示越优先使用swap;swappiness数值越低,表示越优先使用物理内存。

1
2
# echo 5 > /proc/sys/vm/swappiness
# echo vm.swappiness=5 >> /etc/sysctl.conf

把swappiness参数设置为5,告诉系统大部分时间优先分配物理内存,这可以解决极端情况下物理内存耗尽时,用swap分区做下缓冲。

初步看来,Nginx提示504 Timeout的问题解决了。

--EOF--

小内存VPS站点内存使用调优

本站托管在DigitalOcean,VPS规格是最低的1VCPU,512M内存,20G SSD存储。即便是对于这种日均PV不过百余的小站点,LNMP架构再加上顺便在主机里装上的一系列Shadowsocks、squid、l2tp等代理和VPN软件,内存逐渐成为最大瓶颈。

终于这两天DNSPod不断发邮件报警网站无法访问,打开网站提示“数据库连接错误”,ssh到主机里,dmesg提示各种OOM Killer:

# dmesg | grep oom
[.] php5-fpm invoked oom-killer: gfp_mask=0x201da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name
[.] mysqld invoked oom-killer: gfp_mask=0x201da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name
[.] php5-fpm invoked oom-killer: gfp_mask=0x280da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name
[.] php5-fpm invoked oom-killer: gfp_mask=0x201da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name

php5-fpm和mysqld进程频繁被杀,php5-fpm进程实际上是fastcgi worker进程,即使被杀,也能被php-fpm master进程重启,因此不影响网站可用性,但是mysqld进程一旦被杀了,数据库就挂了,导致首页出现“数据库连接错误”提示。

低层次的内存使用优化可以从两方面入手:

1. 降低mysql的oom score权重,降低被OOM Killer选中的几率。

这点可参考『Linux OOM Killer问题』,将mysqld进程的oom_score_adj调整成最小值-1000:

1
2
# pid=$(cat /var/run/mysqld/mysqld.pid)
# echo -1000 > /proc/$pid/oom_score_adj

2. 减少php-fpm的内存使用量。

Nginx作为Web Server,本身不具备解释执行php的能力,我们能通过GET /index.php获得HTML响应的原因是Nginx后端对接了php解释程序,这个对接的协议就是fastcgi(早期使用cgi,因为fork-exec的请求分发执行方式效率太低,后来有了fastcgi,采用对象池模型,避免每次解析配置文件、初始化执行环境等),fastcgi与语言无关,php-fpm就是php语言环境下的一个fastcgi实现。关于cgi、fastcgi、php-fpm之类的讨论可以参考这两篇文章: 『FastCgi与PHP-fpm之间的关系』,『概念了解:CGI,FastCGI,PHP-CGI与PHP-FPM』。

默认情况下,php-fpm中关于进程池的配置(/etc/php5/fpm/pool.d/www.conf)如下:

1
2
3
pm = dynamic ;;表示worker进程动态调整。
pm.max_children = 20 ;;表示最大worker进程数量。
pm.max_requests = 0 ;;表示每个worker进程最多处理多少次请求后重启,0表示永不重启。

从中可以看出默认配置对于小内存(512M)的VPS的不合理之处,每个cgi worker进程的初始内存占用量约为10M,并且很容易就涨至50+M,当并发数稍稍上去一点,可能就会产生 20(workers) * 50M ~= 1G 的内存使用量。这还未考虑某些php第三方库存在内存泄露问题,当pm.max_requests参数为0,内存泄露会造成worker进程内存使用量越用越大,最终失控。

权衡之后,我把配置调整为:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 3
pm.start_servers = 2 ;;启动worker数量
pm.min_spare_servers = 2 ;;最小空闲worker数量
pm.max_spare_servers = 3 ;;最大空闲worker数量
pm.max_requests = 500 ;;每个worker处理500次请求后重启

虽然保守了一点,但至少比较保险。

调整pm.*参数时建议taif syslog和php5-fpm.log,实时监控站点运行状况。tailf syslog目的是为了监控还有没有php-fpm进程被oom kill;tailf php5-fpm.log目的是为了查看fpm worker进程的启动和异常关闭情况。如果调整后出现Nginx “502 bad gateway”错误,或者php-fpm频繁的“WARNING: [pool www] child 10595 exited on signal 9 (SIGKILL) after 78.871109 seconds from start”日志,那肯定是pm.*参数调整不合理无疑了。

以上2种手段毕竟治标不治本,是以牺牲性能为前提的。实际上当内存不足时,最直观也是最简单的方法当然是加内存,既然物理内存没法加,那就加虚拟内存(swap)吧,DigtalOcean上的VPS主机默认没有开swap分区:

1
2
3
4
5
6
7
# free -h
             total       used       free     shared    buffers     cached
Mem:          497M       408M        88M         0B       456K        15M
-/+ buffers/cache:       393M       103M
Swap:            0          0          0
# swapon -s
Filename                                Type            Size    Used    Priority

DigitalOcean提供了SSD硬盘,所以开个2倍物理内存大小的swap分区也没什么问题,以下是使用磁盘文件作为swap分区的步骤:

1. 生成1G大小的磁盘文件

1
# dd if=/dev/zero of=/swapfile bs=1M count=1024

2. 将文件格式化为swap分区

1
# mkswap /swapfile

3. 启动swap分区

1
2
3
4
5
6
7
8
9
# swapon /swapfile
# free -h
             total       used       free     shared    buffers     cached
Mem:          497M       462M        34M         0B       252K        21M
-/+ buffers/cache:       440M        56M
Swap:         1.0G          0       1.0G
# swapon -s
Filename                                Type            Size    Used    Priority
/swapfile                               file            1048572 645840  -1

4. 将swap分区加到/etc/fstab,使重启后仍生效

1
# echo /swapfile swap swap defaults 0 0 >> /etc/fstab

有了1G的基于SSD的swap分区后,就可以把php-fpm参数调得任性一点了:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 10
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 5
pm.max_requests = 1000

初步看来,小内存VPS站点内存耗尽问题解决了。

--EOF--