又双叒一个HTTP服务端轮子

  现在看来,自己已经撸了好几个HTTP服务端的轮子了,就像是做前端的都爱做博客主题一样,估计做HTTP服务端也是很多服务端开发最爱干的事情了。事由是公司测试环境需要一个虚拟银行端来匹配打款系统做测试使用,而且后面可能也会用来匹配做压力测试和系统调优工作,虽说主要达到需求为目的,这东西可做简单可做复杂,但是本着进益求精(其实是不折腾不死心)的态度,自己还是想把这个功能能够模拟的真实一点。刚好自己之前做了很多的小组件,然后发现很多可以整理一下拿来直接使用,由此不禁感叹:工作年限多了,虽说感到技术和能力没啥长进,手头倒是积累了一大批的轮子、工具和脚手架,也能算得上是一笔小积累和小财富吧……
  总体来说,东西还是向着更好的、更成熟的方向发展的。
  经过前几次HTTP服务端的尝试,现在总算感觉把boost.asio和HTTP/1.x的GET/POST较好的融合起来了。通过GET方式返回文件系统内容基本很容易就能做一个静态Web服务器,而且之前用过FastCGI接口让其支持php动态语言也不是难事,但是总体开发HTTP服务端的应该都是作为接口开发的,将特定uri路由到特定接口的处理函数中去,所以开发中提供向指定uri注册回调函数的机制,将解析后的HTTP头部、uri和参数、POST数据体透传给这些函数,并约定处理结果的返回内容和格式,就算是形成了一个通用的HTTP服务器框架了,后面各种服务都可以注册进来,将请求路由到各个响应函数中,这个库把HTTP服务需要考虑的大多问题都帮忙解决了。后续的工作可以对更多的头属性做支持,HTTP/2暂时就不考虑了,不得不说这玩意儿实在是太复杂了……

Boost.Chrono时间库的使用

  时钟这个东西在程序中扮演者重要的角色,在系统编程的时候睡眠、带超时的等待、带超时的条件变量、带超时的锁都会用到,但是往往对特定系统依赖性很大,感觉即使不考虑系统的跨平台性,如果能使用一个稳定的接口,同时如果能够方便的对时刻、时段等进行相关的操作和运算,将是再好不过的了。
  在boost库中和时间相关的库有Boost.DateTime和Boost.Chrono,前者专注于时间时刻以及本地化相关的内容,而后者主要是时刻、时长和时间的计算等内容。当然,C++11标准已经支持std::chrono了,但是为了兼容老编译系统现在很多C++库和程序都使用boost.chrono作为时间类库(还有的原因就是std::chrono没有收录boost.chrono的所有功能,比如统计CPU使用时间、自定义时间输出格式等),不过比较可惜的是即便使用boost::chrono作为权宜之计,也需要boost-1.47版本之上才行,而现在比较旧的发行版需要升级boost库才可以使用。想想现在RHEL-6.x仍然被大规模的部署,而且RedHat要为这货提供长达十年的技术支持,真不知道啥时候才能顺顺利利的享受C++11……
  Boost.Chrono的时间类型分为duration和time_point,也就是时长和时刻两类,很多概念和接口都是围绕这两个维度去定义和实现的。

一、Clock

  clock是Boost.Chrono中的重要概念,而且这些clock都包含一个now()的成员函数,用于返回当前的time_point。Boost.Chrono包含的clock类型有:
  (1) chrono::system_clock 代表系统时间,比如电脑上显示的当前时间,其特点是这个时间可以被用户手动设置更新,所以这个时钟是可以和外部时钟源同步的。这个时钟还有一个to_time_t()成员函数,用于返回自1970.1.1开始到某个时间点所经过的秒数,数据类型是std::time_t。这种时钟通常用来转换成日历时间使用。
  (2) chrono::steady_clock 其特点是时间是单调增长的,后一个时刻访问得到的时间点肯定比之前时刻得到的时间点要晚,即使我们手动将系统时间向前调整了也不会改变这个时钟稳步向前推行累计,其也被称为monotonic time,该时钟是均匀增长且不能被调整,其特性对于很多不允许时间错乱的系统是十分重要的。chrono::steady_clock通常是基于系统启动时间来计时的,而且常常用来进行耗时、等待等工作使用。
  (3) chrono::high_resolution_clock 依赖于系统实现,通常是上面两种时钟的某个宏定义,取决于哪个时钟源更为的精确,所以其输出也决定于取决于上面哪个clock来实现的。
  (4) chrono::process_real_cpu_clock 表示自进程启动以来使用的CPU时间,而这个数据也可以通过使用std::clock()来获得。chrono::process_user_cpu_clockboost::chrono::process_system_cpu_clock表示自进程启动以来,在用户态、内核态所花费的时间,而所有的这些事件可以通过chrono::process_cpu_clock来获得,他返回上面所有时间组成的一个tuple结构。
  (5) chrono::thread_clock 返回基于线程统计的花费时间,而且不区分用户态、内核态的时间。

C++之虚函数的访问性

  在上次的一篇文章中,提到了private virtual函数,说实话直到当前自己所写的所有的虚函数都是public的,毕竟成员数据总应当被设置为private已经深入人心了,但是对成员函数的访问性貌似强调的不够多。后面网上搜了一下,虚函数的访问性还是挺有讲究的,顿时Sutter的两篇历史博文让自己醍醐灌顶,可见经典永流传啊。
  总体而言,涉及到虚函数应该秉持Non-Virtual Interface Idiom,相似的说法是Template Method涉及模式。

一、接口类型是non-virtual public,实现类型是virtual private

  如果一个成员函数是virtual public,那么这个函数就需要完成两个任务:定义调用接口、提供实现细节,而很多请看下这两个目标是相互对立的制约关系,因为接口要尽可能保持稳定,而实现要尽可能的方便修改更新。
  模板方法就是将接口定义为稳定的non-virtual,然后将实现和定制化的工作代理给private virtual成员函数,这样继承类就可以直接继承public函数作为稳定接口,同时override基类的private virtual进行定制化的实现。这样去做的话其好处有:
  (1) 在基类的公有接口中可以做很多pre-conditions和post-conditions的工作、插入度量性代码、写入调试跟踪日志等,跟一般的说是在调用之前设定好相关场景,而在调用之后清理相关场景,而不需要在每个派生类override的时候重复这一任务。
  (2) 接口和实现分类后,两者就不用像原本public virtual要实现一一对应的关系,比如在一个公共接口中可以按照一定的顺序、一定的条件可选择性的调用多个private virtual实现函数,派生类选择性的override某些或者全部虚函数,处理起来就更加灵活了。
  (3) 这样实现后的类后续修改和维护更加的方便,可以快捷的在public non-virtual接口中添加检查、调试等任何操作,派生类也可以按需独立的override业务部分,接口的使用者不受任何影响。
  (4) 关键的是这种手法几乎没有副作用,即使公有接口类没有额外的工作而仅仅当做一个函数wrapper,也可以使用inline进行可能的调用开销的优化。

C++面向对象设计的访问性问题

  最近在看Scott Meyers大神的《Effective C++》和《More Effective C++》,虽然这两本书都是古董级的教参了(当然针对C++11/C++14作者所更新的《Modern Effective C++》英文已经发售了,不过还没中文翻译版本),但是现在看来仍然收益匪浅,而且随着对这个复杂语言了解的深入和实践项目经验的增加,很多东西和作者产生了一种共鸣,以前种种疑惑突然有种拨云雾而见天日、豁然开朗的感觉,也难怪被列为合格C++程序员之必读书目。其实C++确实是个可怕的语言,于是市面上针对这个语言的教参也是聆郎满目层出不穷,当然水平也是参差不齐,像上面所说的Meyers三部曲能够历久弥新,也凸显了这些经典教参的真正价值。
  至于最近回归C++本质,主要是觉得现在后台开发的RPC、MQ、分布式系统虽然被称的神乎其神的,但是作为成熟的组件绝大多数公司都可以是直接拿来主义,当然也不可否认其使用经验的可贵,因为最近线上使用这些组件还是遇到或多或少不少问题的,以后可以少走些坑,然而这种东西也是可遇难求的;反而C++语言本身的使用占用了程序员绝大多数的工作内容,从而直接影响到项目的质量和后续的可维护性。在此,侯捷老师的 勿在浮沙筑高台 仍如警世名言响彻在耳,一个合格的程序员其扎实的基本功是多么重要。
  C++面向对象的东西太多了:public、protected、private访问和继承,virtual和多态、多继承,外加const、缺省参数、名字查找等,光这些元素的排列组合就可以导出很多种情况,看似灵活多变,但不是每种情况都值得去尝试的。

RabbitMQ的Mirror Queue集群高可用性

  RabbitMQ算是历史悠久的消息队列了,所以也算得上是在工业界久经考验的长者了。这些东西平常让他跑着一般不会出问题,但是生产环境真是不怕一万就怕万一,为了高可用高可靠而言最好还是使用其mirror特性建立集群做备份。其实RabbitMQ用起来在我们公司运维也没有什么经验,虽然数据库我们玩的很溜了,但是RabbitMQ遇到问题还是一脸懵逼,主要这东西是非主流Erlang所做,而且也涉及到什么Node的概念,上一次血泪的教训就是没事千万不要改节点的名字。这段时间闲着把RabbitMQ官方的文档看了一遍,对其mirror集群的知识翻译下来。
  我们说RabbitMQ的服务端通常都沿用broker术语,其表示一个或者多个Erlang Node,每个都运行着RabbitMQ的程序,并且他们之间共享users、virtual hosts、queues、exchanges、bindings和运行时参数,不过默认情况下queue是不会再每个节点上都存在的(这也是后面要说的mirror queue之所在),通常也把他们打包叫做cluster。
  当多态RabbitMQ运行的时候,他们之间默认没有联系,都运行在以自己为中心的小集群当中,创建一个大集群的时候可以选取其中的一个小集群,然后让其他的主机加入到这个集群当中。

1
2
3
rabbit2$ rabbitmqctl stop_app
rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1
rabbit2$ rabbitmqctl start_app

  在集群运行的过程中,node可以随时加入和撤离开集群,不过当整个集群都停止之后,则必须由最后那个关闭的node开始重启集群,而此时如果先启其他的节点,则该节点将会等待30s那个最后关闭的节点启动,否则的话将会启动失败。这样设计是考虑到最后关闭的node可能保留最多的消息,如果想要强制启动非最后关闭的node,则需要添加forget_cluster_node参数才可以,此时新启动的node将直接被提升为master。

C++的pimpl用法

  C++的pImpl可以说是最常见的惯用手法了,在很多的C++项目和C++开发库中都有所见。plmp的缩写就是Pointer to Implementor,顾名思义就是将真正的实现细节的Implementor从类定义的头文件中分离出去,公有类通过一个私有指针指向隐藏的实现类,是促进接口和实现分离的重要机制。
  在C++语言中,要定义某个类型的变量或者使用类型的某个成员,就必须知道这个类的完整定义,其例外情况是:如果定义这个类型的指针,或者该类型是函数的参数或者返回类型(即使是传值类型的),那么就可以通过前置声明引入这个类型的名字,而不需要提供暴露其完整的类型定义,从而类型的完整定义可以被隐藏在其他hpp头文件或者cpp实现文件中,而这个指针也被称为不透明指针(opaque pointer)。通常的pImp的手法是在API的头文件中提供接口类的定义以及实现类的前置声明,实现类的本身定义和成员函数的实现都隐藏在cpp文件中去,同时为了避免实现类的符号污染外部名字空间,实现类大多作为接口类的内部嵌套类的形式。

一、pImpl手法的优势和目的

1.1 信息隐蔽

  私有成员完全可以隐藏在共有接口之外,尤其对于闭源API的设计尤其的适合。同时,很多代码会应用平台依赖相关的宏控制,这些琐碎的东西也完全可以隐藏在实现类当中,给用户一个间接明了的使用接口再好不过了。

1.2 加速编译

  这通常是用pImpl手法的最重要的收益,称之为编译防火墙(compilation firewall),主要是阻断了类的实现和类的实现两者的编译依赖性。这样,类用户不需要额外include不必要的头文件,同时实现类的成员可以随意变更,而公有类的使用者不需要重新编译。
compilation firewall

1.3 更好的二进制兼容性

  承接上面说的,通常对一个类的修改,会影响到类的大小、对象的表示和布局等信息,那么任何该类的用户都需要重新编译才行。而且即使更新的是外部不可访问的private部分,虽然从访问性来说此时只有类成员和友元能否访问类的私有部分,但是由于C++的特性是名字查找先于名字查找和重载解析的(即使不可访问也会返回调用失败,而不是视而不见),私有部分的修改也会影响到类使用者的行为,这也迫使类的使用者需要重新编译。而对于使用pImpl手法,如果实现变更被限制在实现类中,那公有类只持有一个实现类的指针,所以实现做出重大变更的情况下,pImpl也能够保证良好的二进制兼容性。
  因此,独立和自由是pImpl的精髓所在。

1.4 惰性分配

  实现类可以做到按需分配或者实际使用时候再分配,从而节省资源提高响应。如果你意识到这点了,那是很不错的。

服务重构过程阶段性小结

  首先呢,看到这篇文章的时候,重构工作说明已经进行的差不多了,不然也没心情出来跟大家扯淡了,而前几天真是够自己忙活惨了的。总体来说业务过程不是很复杂,所以项目规模也不是很大,不过经过这个蜕变的过程,还是有些东西想说说总结一下,而且这个过程中也不可避免的造了点轮子。其实原本项目中也有很多的轮子,有些不好用、有些晦涩难懂也不敢再用,所以现在这个项目中除了将有些轮子使用boost等一些成熟库来替换,有些不好用的轮子自己也再造了一次。
refactor

一、定时任务服务

  原来的定时任务服务,是采用了类似堆结构(内部用std::set然后重载compare比较器)的方法来实现的。其实堆结构实现定时器算是业内习惯性的解决方案:Libevent内部定时器就是通过堆数据结构实现的,而Nginx中对连接计时也是通过堆结构实现的,这样当各个定时任务按照到期时间进行有序排列的时候,管理器就只需要睡眠到下一个最早任务到期时间就可以了,这对于简单和类型不多的定时任务是比较合适的,但是要作为一个通用的定时任务服务,难以预估定时任务执行所需时间,对单个线程的处理能力和响应灵敏度也不好控制。
  新的TimerService没有沿用之前的设计,新的方式是基于Libevent异步事件框架,采用标准接口创建定时器事件,由于Libevent是C语言实现的事件库,所以采用一个简单标准C签名的函数进行wrapper,而在wrapper函数中调用C++的Callable对象,以应对各种各样的定时任务。同时,这个服务中还创建一个事件线程和N个普通工作线程组,前者负责Event Loop,同时对于简单的任务就直接内联执行;而对于重量级、实时性要求不高的任务就放到一个队列中丢给工作线程组慢慢消耗,当然这也就是借鉴了Linux内核中断上下部分的模式。

Apache ZooKeeper进一步学习

  最然之前自己搞过一段时间的分布式理论知识,对Paxos、ZAB、Raft一致性协议也或深或浅地有些了解认识,所以对这种最终一致性分布式系统的使用场景也有一定的概念(感觉和别人侃起来也更有谈资了),但是作为一个程序员来说,一旦要把他们落实到代码上来,下起手来还真是有些困难。前几天偶遇一本英文的《Apache ZooKeeper Essentials》,其中对ZooKeeper实现分布式系统常用组件的构建过程讲的相对具体一些,这里不敢独享也和大家分享一下。
  额外的在此感叹一下:一方面觉得Java的程序员好享福,Apache全家桶好多项目都是Java实现的,所以在企业项目开发中Java可选用的成熟组件非常的多,自然使用资料和经验也遍地都是;虽然很多库也提供了C语言访问库的绑定,而且通常这是必须的,因为很多脚本语言(比如Python、PHP)的绑定,受限于语言的操作能力和性能方面的考虑,大多也是基于C语言绑定之上再进行一层特定语言的封装。这个过程中,C++的地位感觉有些尴尬了,除非原生使用C++开发的,绝大多数的组件感觉都没有原生C++绑定,而C++程序员想要用的Happy顺手(比如自动构造中初始化,自动析构释放资源),就必须自己基于C语言绑定再进行进一步的封装,因此C++的世界中,这种简单封装的轮子在我们的项目中非常之普遍,由此可见C++对C语言封装技能将会是C++程序员必备重要技能之一啊!

一、ZooKeeper的启动配置

  为了兼顾于测试和生产环境的需要,ZooKeeper具有单机模式和集群模式的部署形式可供选择。

1.1 standalone模式

  将conf/zoo_sample.cfg拷贝成conf/zoo.cfg,在该配置中有几个必须的参数需要说明:tickTime表示每个tick所代表的时长单元,以ms为单位,后续很多的配置都是基于这个tickTime来计数的,比如心跳间隔;dataDir表示数据存储目录,ZooKeeper服务会有一个in-memory状态数据库,而这个目录就是用于存储数据库的快照内容和修改事务日志信息使用的,这是一个类似的append only的记录文件;clientPort是接收客户端连接请求的侦听端口,默认是2181。
  使用上面的配置信息,使用bin/zkServer.sh start就可以启动ZooKeeper服务端了,通过使用bin/zkServer.sh status命令可以看见,当前服务工作在standalone模式下,因为该模式存在单点故障的风险,所以只可以用于测试使用。

坑爹的RabbitMQ

  RabbitMQ本身是个好东西:其可靠性好不容易丢消息,性能也不算差,准照AMQP协议,而且历史悠久算是被实践检验过,但是说他坑爹,主要是这个中间件使用非主流的Erlang语言开发,相比其他语言都还好,而唯独对C/C++没有那种官方完全支持、简单好用的客户端开发库可用。现有来说C/C++的开发库只有alanxz维护的librabbitmq-c,这个库虽然目前被收录在EPEL库,但是没有Apache、GNU这类大组织或者公司维护的话总让人心里有些忐忑和不安,而且这个库的接口比较的底层,如果用户对AMQP没有一些了解的话很难开箱即用 ;然后在GitHub上面搜索其他的C++客户端库,相同作者的SimpleRabbitClient在低版本boost上面用不了,而其他库也基本是librabbitmq-c的再封装,看了下代码质量感觉也是一般般。
  其实这几天折腾看来,AMQP的协议也不是很复杂,但是要从头写一个C++客户端库还是有点折腾。既然大家都用rabbitmq-c,就假定其被实践检验了稳定了(话说作者还是挺热心的,有问题提Issue回复很快),于是花了几天对AMQP协议、Python pika、SimpleRabbitClient的东西进行一些深入的梳理,自己模仿做出来了个rabbitmq_cpp_wrapper的对rabbitmq-c库上层封装的轮子。
  这个库的封装主要是使用RabbitMQHelper管理一个connnection,同时管理其中的各个channel,并且利用C++的析构机制自动进行一些资源的释放清理工作,而且对AMQP的Publish Confirm和Consume ACK/Reject机制提供可配置的支持操作。这个封装的作用就是想让客户端的操作尽可能的简单,尤其在发送和接收消息的时候不需要考虑各种异常处理、不需要考虑各种资源的释放等,最好是会用pika的人用起这个封装来也一样顺手就好。

Apache Thrift使用解析

  之前的一篇文章中,读摘了Apache Thrift技术白皮书的中的一些内容。Apache Thrift当前已经在公司的很多业务中使用,而自己负责的重构的服务,也使用Thrift作为通信手段对大而全的巨无霸服务进行拆分解耦。理论归理论,现实使用起来还是有些东西需要休息的,这次又借着另外一本书,对Thrift再进行一次接地气的学习和实践。
thrift

一、Thrift IDL的类型

1.1 数据类型

  Thrift提供的基本数据类型有:bool、byte、i16、i32、i64、double、string,今后可能会提供binary数据类型,作为string类型的一种特例化,提供更好的操作性和正确性。Thrift不支持无符号整形。
  可以使用struct自定义复合数据类型,每个字段都有一个唯一性标识符,这些标识符不需要连续;字段可以是required和optional的,required字段如果在新版本上删除的话,老版本和新版本交互时候可能会产生问题,所以有些人推荐所有的字段都设置成optional的;可以给struct的字段设置默认值,那么如果没有显式设置字段值的话变量就会使用这些默认值。此外,struct不支持继承。
  联合类型使用union定义,由于任何时候只有一个值能被使用,所以其字段不能是required的。
  Thrift还支持三种基本的容器类型:(1) list类型,其在C++中被映射成std::vector类型;(2) set类型,其在C++中被映射成std::set类型;(3) map类型,其在C++中被映射成std::map类型。
  枚举类型使用enum关键字定义,其成员的枚举值从0开始标号,通常使用全大写字母命名。

1.2 服务类型

  服务使用service关键字定义,声明一个个的函数接口。
  函数接口还可以使用oneway关键字修饰,这类函数的返回类型必须是void,意在告诉该接口客户端不需要等待服务器的响应结果。比如在将日志信息传递给服务端存储的时候,客户端不需要阻塞等待响应结果,从而增强客户端的效能。