国庆假日回家所感

  因为需要回家办点事情,所以国庆法定休假日之前就请了三天假回去了。回家进门第一感觉就是爸妈长得没有过年时候好了,人明显消瘦了同时也变黑了,估计是爸爸在外打散工,妈妈在附近工艺厂熬夜做针织太辛苦了所致。其实现在他们的头发早已花白过半,但是还是每一段时间就回去把头发染黑,估计他们也知道这样的发色显然和自己的年龄不相匹配,也觉得白发出门显得很难为情吧。俗话说相由心生,爸妈心中一定也怀着满满的不安、承受着极大的压力。唉,整个社会除了那少部分先富起来的人之外,谁又能活得有多滋润呢?
  其实,自己基本每次打电话都会跟爸妈一而再再而三地嘱咐,他们已经上了年纪了,国家认定60岁法定应该退休了,所以他们还在勉强还在做体力活实在不适当的,尤其爸爸过年的时候说腰疼,去医院拍片后说是脊柱退化所致——毕竟机体开始衰老了,也上了年纪了,所以不应当再做体力活了。但是他们就像千千万万农村普通农民一样,一直都是那么的勤劳朴实,也一直不听我的劝告多停下来休息休息,因为在农村人看来没有子女相伴、儿孙绕膝都还没到享福的阶段,况且我自己现在工作生活还漂泊在外,工作性质和房价种种因素交错的原因,也无法在家乡安居陪伴在他们的身边,所以他们也总是想着在自己还能动一点的时候,尽量的多挣一点钱给我减轻一点压力。
  假期在家跟妈妈聊天的时候,发现老家很多看着我长大的长辈们一个个都不在了,因为自己常年在外读书和工作的原因,这些长辈有的很多年都没碰过面了,虽然年隔已久,但是只要想到他们的话,小时候和他们接触的印象还能活伶伶的在浮现在自己的脑袋中,只可惜物是人非,在之后的岁月中再也看不到他们了,虽说即便同我非亲非故,但向来多愁善感的我心中却不禁莫名难受起来。不过近年的境像就是,近年来家乡的人,很多都是六十多岁、甚至五十多岁就容易患上各种恶疾,比如中风、糖尿病、尤其是各种癌症,特别是在城镇化建设的进程中很多村民搬迁到集镇上面来开始的。更有一个好朋友告诉我,她们村的男丁很多都是年纪不大就走了,村子俨然都快成寡妇村了。当然,这种问题的原因也是总说纷纭,村里是各说各的:有人说是农村人搬到城镇上,没有活干了,生活习惯和生活方式的骤变带来各种不适应所致;有人说是现在的环境大不如前了,虽然城里的环境自然不如乡下山清水秀,尤其时常看电视听广播也知道现在的工业污染严重,大气污染、水污染已经严重威胁到大众的生活健康了;再有就是现在的城乡建设大兴土木,破坏了本地人的风水所致。

再说智能指针

  在之前的文章已经介绍过了现代C++最具价值的开发组件——智能指针了,这东西使用起来比较简单,而且大多数情况下也不容易用错,所以也不会去深究他。但是,最近看了Sutter和Meyer的相关文章和书籍后,对C++智能指针的使用有了更进一步的了解和认识。C++是一个讲求效能和务实的语言,如果本着这个观念去做事,那么C++的很多特性和组件都很有讲究,所以看见这些大神们的经验、总结和套路,虽然也有看不懂焦头烂额得时候,但一旦领略其中的奥妙,那种拨云见日,豁然开朗的感觉真的不可言喻。虽然相比于Python以及新秀Go,C++的复杂和深奥是出了名的,不过也正因为此,先比其他语言而言更具有折腾的潜质,其中的乐趣也只有体验过才会真正有所感受。

一、智能指针解析

  智能指针是用来管理堆上动态分配对象的利器,当然通过定制化析构的技巧,也可以管理其他类型的资源。

1.1 unique_ptr

  unique_ptr是一个独占类型的智能指针,其不允许其他的智能指针共享其内部管理的指针,所以该指针对象不允许被拷贝,但是允许通过函数被返回、通过std::move进行移动来转移其控制权。比较可惜的是C++11不支持std::make_unique方法,也可以自己简单的先实现一个:

1
2
3
4
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params) {
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

  unique_ptr在指定删除器的时候需要在创建unique_ptr对象的时候,指定删除器的类型信息作为模板参数(所以删除器类型是整个unique_ptr类型的一部分),而不能像shared_ptr那样直接指定删除器对象而不用指定删除器类型信息,甚至推迟在reset()的时候指定删除器,这是为了unique_ptr的高效而这样做的,可以产生更快的运行时代码。
  unique_ptr支持数组形式,但是应当避免使用它们,用容器来存储智能指针,而不要让智能指针指向数组。

生产环境软件的调试信息

  虽然把系统做的坚若磐石是每个程序员的理想和坚持不懈的目标,但往往现实就是现实,就是必须面对的东西。每当服务被引入新的组件或者特性的时候,即使在本地环境下各种测、各种压,但是上线后还是可能会遇到各种问题导致服务挂掉。这不,前两天刚引入的两个更改(syslog日志以及Redis队列服务解耦),导致核心服务crash了两次,当然在每天几十万笔交易中出现两次问题(分属两个特性),说明不是普遍性Bug,很有可能是在特定的条件、特定数据情况下才被触发的。
  线上的服务器打开了core dump的功能,这点还是要表扬的。出现任何问题都要保留尸体,只有这样才能追踪问题,不在相同的位置再跌倒第二次。不过线上运行的程序都是发行版,缺乏调试信息,所以用发行版的软件虽然能加载调用栈信息,知道是哪里出了问题,但是具体的调用参数就找不到了,而这种偶发性的问题大多都是因为调用参数的异常导致的,所以这种情况下的尸体是没法用的。
  使用-g编译过的程序,在可执行程序的体积和占用内存方面会耗费比较大,至于执行速度方面则不得而知了。
  网上搜了一下,针对生产版本软件调试符号的处理,主要有两个方法:(1) 编译带调试版本的软件,然后将调试符号strip调后,用于生产环境;(2) 编译不带调试版本的软件,然后记录其软件版本号,出了问题后现编带调试版本的软件进行调试。

一、记录版本号法

  主要是记录线上运行软件对应的软件分支和版本号,出了问题后将代码树checkout到指定的版本信息,然后编译出一个带调试符号的版本用于问题的跟踪调试。我们知道在Linux下,对于常亮字符串可执行区域有一个专门的区域用于保存这些字符串常量,通过strings工具可以取出这些字符串常量。

ZooKeeper C语言API简单试用

  ZooKeeper有官方标准C语言绑定客户端却没有C++版本的,反正身为一个见多识广的C++程序员对此已经习以为常、生无可恋了。其实,ZooKeeper最流行的客户端绑定是Curator,不过它是Java语言的,这个库极为的流行好用,于是乎GitHub上面有很多模仿Curator的接口,然后基于C语言绑定库,封装实现出C++语言的ZooKeeper客户端,不过貌似都是小作坊之手笔(没有太多的Star),所以也不敢直接上。
  C语言的客户端库咋看之下会比较的复杂,因为异步的方式将操作和回调割裂开来,代码会比较碎片化。通过XMind将这些接口梳理一下,其实也挺容易的。

一、ZooKeepr C语言客户端基本使用

  在之前介绍编译C客户端的时候,会同时生成C语言开发所需的库文件。我们通常会使用其多线程异步版本的库,该版本库在使用的时候,会自动创建单独的IO线程、事件线程用于处理连接和事件回调操作。
  下面,首先将zookeeper.h头文件中的重要函数接口罗列出来,其实对照前面一篇文章看来,这些函数的意义也是清晰可辨的,而且接口的风格比较统一。基本有些函数名会有一个额外添加w标记的版本(没办法,C语言不支持重载),可以用于设置watch event,当侦听的事件发生后收到notification时,就会使用事先设置参数去调用指定的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ZOOAPI zhandle_t *zookeeper_init(...);
ZOOAPI int zookeeper_close(zhandle_t *zh);
ZOOAPI const clientid_t *zoo_client_id(zhandle_t *zh);
ZOOAPI int zoo_acreate(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_adelete(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_aexists(zhandle_t *zh, const char *path, int watch, ... );
ZOOAPI int zoo_awexists(zhandle_t *zh, const char *path, watcher_fn watcher, ...);
ZOOAPI int zoo_aget(zhandle_t *zh, const char *path, ...
ZOOAPI int zoo_awget(zhandle_t *zh, const char *path, watcher_fn watcher, ...);
ZOOAPI int zoo_aset(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_aget_children(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_awget_children(zhandle_t *zh, const char *path, watcher_fn watcher, ...);
ZOOAPI int zoo_async(zhandle_t *zh, const char *path, string_completion_t completion, ...);

  为了看起来更加的直观,同时也方便查找和备忘,我重要的五个函数整理到一个xmind的文件中了:
zookeeper

C++设计中的Handle处理类

  Handle这个内容是在看Andrew和Barbara夫妇的《C++沉思录》中接触到的,被广为翻译为句柄,用他来控制其所管理的类。刚开始看瞄的时候就觉得:握草,这不就是智能指针的原型么,难道是没有智能指针时代的轮子?但是耐着性子看完后,觉得还是收获不少的,起码的话算是对智能指针中引用计数、写时复制的设计实现写的比较清楚了。
  题外话,其实智能指正在我司也早有轮子,并且一直被使用至今了。今天过细看了下代码,发现都是最简单的Scoped非拷贝使用方式,而在需要外传的时候都是使用引用的方式传递出去,所以算是挂羊头卖狗肉吧,Shared类其实根本就没做到Shared的事儿,就是个简单的RAII做的事儿。不过,因为我们的业务逻辑比较简单,所以长久使用起来也没有什么问题……

一、简单实现

1.1 准备工作

  作者过于循循善诱的细节东西就不细说了,Point这个类是我们实际的用户类,跟业务相关的我们不管;Handle类是我们要实现的句柄类,我们的目的是要将Point的对象绑定到Handle对象上,让handle控制他所绑定的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
public:
Point(int x, int y): x_(x), y_(y) {}
private:
int x_; int y_;
};
class Handle {
public:
Handle(int, int);
Handle(const Point& p);
private:
UPoint* up_; // TODO...
};

  为了用户使用的舒适傻瓜,那么Handle就应该自动接手用户类Point资源的创建和销毁,通过Handle的构造函数我们得知申请资源的时候就有两种方式:(1)直接让Handle类的构造函数参数和Point类构造函数签名一致,然后做一个参数转发;(2)用拷贝的方式,创建一个现有Point的对象的副本,而原始对象的资源我们不关心。

Linux下的日志服务Rsyslog

  当前公司的业务系统是自己搞了个日志服务,其实也就是对格式化后的消息使用UDP发送做了一个封装,然后日志服务端根据执行进程名字创建对应的日志文件后,对相同服务的日志内容的聚集和落盘操作,并且日志文件每日进行截断保存。由于一些异常交易的跟踪排查都通过日志文件进行跟踪,所以平时对日志文件的依赖还是比较大的,但使用过程中发现这个自研的日志系统问题多多:
  (1). 日志文件的内容经常会乱序,虽然根据时间戳可以判断日志的先后顺序,但人为跳来跳去总有些别扭。在多线程环境下每发一条日志都会创建一个UDP socket,所以预想在今后随着业务增长和线程数目增加,日志量攀升的情况下这种乱序现象会更为的严重;
  (2). 有时候本该有的日志没有发现,不知道是执行流程改了还是怀疑是日志本身丢失了,毕竟是UDP协议发送的,可靠性是得不到保障的;
  (3). 采用自定义的日志格式,虽然zabbix也能进行一些正则匹配,但是我想后续接入专业的日志分析处理系统可能会比较困难,这里把所有的日志元数据都格式化死到消息体里面的;
  (4). 日志消息没法按照level灵活的分文件存放,查阅日志需要在一大堆的调试信息中寻找,费神费力,所以效率真心堪忧。

  一句话三个字儿:坑爹啊!所以在此强烈建议规模不大研发能力又有限的公司,还是应该选择成熟、大众、主流的后台开发组件:一方面这些开源组件对你遇到的和将要遇到的坑,人家都遇到帮你填了;二来他们通常构架灵活,扩充和修改方便,而且围绕他们做的配套工具和组件周边也比较丰富,很多事情都不必自己再做了。虽然,对程序员来说造一个轮子很简单,通过造轮子对比也是一个很好的学习进步方式,更厉害的是轮子对公来说也是KPI亮点,但是没有严格论证测试就将其推向生产环境是一个极度不负责任的行为,情况严重的话会给后续的开发维护带来不小的负担。

又双叒一个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、缺省参数、名字查找等,光这些元素的排列组合就可以导出很多种情况,看似灵活多变,但不是每种情况都值得去尝试的。