关于自己写的两个小项目tzhttpd和tzmonitor

  前面的文章提到了自己开发的tzhttpdtzmonitor,虽然不是什么厉害的东西,但是个人感觉对于后台开发者来说算是比较有用,同时也是自己认真开发和维护的小项目。在这篇文章中,我就将这两个项目实现中的小细节和大家分享一下。

一、tzhttpd - 一个通用的HTTP开发框架


  在程序员界有一个说法:每个后台开发工程师都有一个自己的Web Server。这个说法也非空穴来风,尤其是对于做C++后台开发的,虽然C++程序酷爱造轮子自然是一个原因,但究其根本是C++语言标准本身没有集成一个HTTP框架,甚至C++标准还没有敲定一个标准的网络库,而大家风风火火的造轮子也没有形成一个事实上工业标准的HTTP框架,所以这种尴尬的境地就这么一直持续着。
  自己在做后台开发以来,就使用了boost.asio开发了一个简易的HTTP框架,主要目的是用来做应用程序网关相关的东西,不过那个时候自己的经验和能力有限,而最近工作中越发感觉对一个稳定好用的HTTP框架的需求,在对生产服务端系统的各项要求有了更深入的理解之后,将之前的项目进行了再整理和优化,就得到了上面提到的tzhttpd了。

1.1 特性

  a. 他是基于boost.asio高性能异步框架开发的,所以理论上可以支持很高的并发度支持。因为我的工作环境迫切要求是吞吐量而不是并发度,所以也没有对他做更深入的并发测试和验证,不过框架本身的性能还是不错的;
  b. 虽然只支持HTTP协议的GET/POST方法,但是也可以应对绝大多数应用场景了。HTTP请求的报文、数据都进行了解析和存储,业务层可以很方便的取用;支持Keep-alive长连接;支持VHost虚拟主机。
  c. 支持表达式描述的请求URI匹配,将请求转发到匹配的handler中去。除了将handler写死在项目中之外,还可以通过配置文件和so动态链接库的方式,动态增加URI和对应的handler,这个过程可以方便的部署、更新业务,其他业务不停机不受影响;
  d. 虽然最初是用来做应用程序网关使用的,但是在后期我也增加了VHost、Cache-Control、Content-Type、Redirect的支持,而且还计划支持FastCGI协议,在一定的程度上做一个可用的WebServer;
  e. 附带一个internal_manage的管理界面,方便不停机对服务进行某些更新和维护;

1.2 可以用来做什么

  a. 虽然tzhttpd不是立志做一个WebServer,但是一定程度上他确实可以支撑一个网站,比如你正在看的这篇文章的网站就是tzhttpd的example编译出来的例程然后运行支撑的;
  b. 一个灵活、可维护的HTTP应用程序网关,这算是tzhttpd该干的正事了。你可以使用该框架快速实现一个支持HTTP协议的测试服务端,也可以通过so将其变成一个方便维护的应用程序网络服务器,这个库因为简单,所以也比较稳定;
  c. 作为一个HTTP协议的适配器,可以在handler中将请求转换成后端支持的通信协议(比如RPC、Socket),并实现标准的HTTP响应,实现一种代理的功能;
  d. 可以集成到你现有的系统中,提供一个系统运行状态的展示支持,或者对系统进行控制、维护的操作接口;

1.3 其他

  其实完善这个东西的动力是用来淘汰CGI调用的,作为上个世纪的产物,CGI实在太耗资源难以支撑现在互联网高并发大流量的访问需求。很多业务接口会在CGI中创建数据库连接,做一个查询然后返回,这么简单的操作需要创建新的进程、和数据库建立连接、访问数据库、断开数据库连接、销毁进程,在一个线程都被认为过于重量的时代,如此挥霍进程简直惨不忍睹,而且CGI会占用整个系统的资源进而影响同机器其他服务的性能。
  针对这个问题,最初提出的方案是使用Golang进行改造,虽然Golang简单,不过自己觉得更换语言来解决问题还是有一定的风险的。虽然C++比较的复杂,但是大家对语言特性、常用库的选型和封装都是有积累的,使用Golang意味着所有的代码需要翻译重写,对语言特性和选型也需要时间去熟悉和验证,而且“Golang写时一时爽,优化火葬场”,当需要和内存、GC做斗争的时候,可就没那么轻松了。
  这里对动态链接库的使用感觉还是挺有意义的。首先从开发层面上说,程序员开发一个CGI现在就等同于开发一个动态链接库,我们可以在cgi-handler目录中开发任意数目的动态链接库,像之前CGI一样进行管理;虽然动态链接库向主程序暴露的是有限的C语言接口,但是在程序链接的时候我们使用了rdynamic选项,这样主程序的所有符号就向动态链接库暴露了,也就是动态链接库程序的编写可以复用主程序的绝大多数代码,迁移陈本非常低;我们通过智能指针和管理接口相配合,可以支持动态链接库的动态加载,所以部署和更新不需要重启主服务,接近和CGI的部署维护成本了;而且可以认为可执行程序大家都是一样的,然后配合不同的so可以实现不同的逻辑,更加降低了主服务迭代风险。
  不过这个库也有一定的缺陷,就是handler是在io_service线程组中直接执行的,所以如果你的handler如果比较耗时的话,整个服务的效率会很低,而且各个handler之间也没有做资源隔离,慢的handler会影响到其他handler的调度和执行。如果要将handler做成异步的,整个框架就会变得十分复杂,我想后面通过协程hook阻塞函数的方式或许可以根本解决这个问题,但是目前而言需要将关键服务和非关键服务分开部署比较的妥当。

二、tzmonitor - 一个简易的高性能信息采集系统

2.1 项目背景

  这个项目的初衷,是我在做打款路由流控机制的时候,很多服务运行时刻的参数需要收集,而这些数据除了少部分可以直接从数据库获取之外,很多都是需要手动收集和计算整理的。虽然我们已经有了强大成熟的监控系统,但是没有找到接口可以让我访问监控数据的方法,所以最开始实现这个功能的时候,都是将数据放到Redis当中,然后周期性获取的时候直接做计算然后返回。虽然这种实现在线上运行了大半年,功能上也基本能够胜任,达到当初的需求,但是:
  a. 每有一个事件需要上报都会访问Redis,这给Redis带来了很重的负担,而且对业务系统的性能也有很大的影响。
  b. 每次取数据的时候做实时计算,所以访问效率也不高。而且数据存储不连续、纬度也不规律,也没有统一的整理入库操作。为了性能起见每隔一段时间就把Redis上历史数据删除掉,不过数据也就永久消失了。
  c. 当初为了方便起见,很多数据都是按照“日期时间”组合建立一个slot,导致统计的数据只能离散地以分钟的粒度进行访问,在高峰的时候一分钟会影响很多交易,而在低谷的时候每分钟触发的问题也很少,触发不了我们设定的响应阈值。

2.2 设计概述

  为了解决上面的问题,我开发了这套系统,从主机名、服务名、服务实例编号、事件名、事件标签和整型值的维度来标记每一个事件,尤其是事件标签这个域,可以用来定义成功、失败,或者该事件可能的任何离散取值。
  (a) 客户端
  客户端可以在任何需要的时刻创建一条事件,然后该事件会缓存在本地的队列中,服务端库会等待直到本地堆积了一定数目的消息或者定时器超时后,自动向服务端批量提交这些事件包。其中每个事件在当前的维度会有一个唯一的msg_id,客户端会尽可能的提交事件,而不用担心重复提交的问题。
  (b) 服务端
  服务端会在一个约定的linger时间外,对之前的每秒钟的事件消息做归并处理,计算sum count avg std值,然后入库持久化。为了简化处理,服务端不会尝试解析具体的事件类型,对任何的事件都做上述统一处理。
  (c) 查询接口
  封装了客户端的查询接口,客户端可以以任意的条件查询任意的字段,客户端作为业务调用知道自己感兴趣的信息。
  上面所有涉及到网络操作的部分,都同时提供了Thrift和HTTP两种接口,这两种协议出现这么久了,在性能、兼容性、可维护性等方面都各有千秋,谁也取代不了谁。同时客户端和服务端都会监测队列长度,如果超过了阈值会自动启动线程增加并发处理,现在是用裸thread实现的,后面会考虑修改成boost::future来实现。

2.3 其他

  虽然这是一个具有完整功能的系统,但是他的价值还在于:其集成了大量的开箱可用的封装库:RabbitMQ连接池、Redis连接池、数据库连接池、Thrift RPC客户端和服务端、TimerService、日志支持、单元测试框架等。他可以作为一个完整的服务端系统框架来做进一步的业务开发,也可以截取其中感兴趣的组件用到自己的项目当中去。
  这个项目不仅仅有服务端,还为客户端提供了便捷的开发库,客户端和服务端会共用部分代码,这里在服务端和客服端的代码复用和降低耦合还是花了一定心思的。最终的客户端库以两个库文件和两个头文件提供,实现上采用PImpl实现编译防火墙隔绝依赖,同时对于日志这类个性化的组件,提供接口允许客户端提供自己的日志底层实现函数。
  现在发现这个工具除了可以考虑用在之前的情景下,还可以做一个通用的收集器,比如要调优程序又不知道瓶颈出在哪边,可以将关键函数调用时间上报到这个服务中去,这个服务会自动汇聚计算出次数、调用结果、均值等参考信息。而且在垃圾虚拟机上,客户端可以轻松每秒上报十万条消息,所以不会对整体系统性能带来多大的影响的。
  另外,这个服务也自带了一个简单的展示页面,比如下图就是这个网站在过去1小时的访问量。很丑啦,当然数据就在那儿,你们可以用更专业的看图软件进行展示。
tzmonitor

本文完!