分析一下我司ZooKeeper客户端封装思路

  为了提高系统的可用性,我司现在不少项目已经采用ZooKeeper进行服务发布和服务发现了。
  之前已经对这块内容预学习了很久,虽说ZooKeeper的C API使用起来很简单,核心接口不超过十个左右的函数,但是多线程的库中接口涉及到大量的函数回调,要想在C++中做到傻瓜式的使用还是有点麻烦的,因此同往常一样C++中也有大量针对ZooKeeper Client封装的轮子。当前我们软件研运部使用客户端库的是我们老大传说花了一周心血封装完成的,已经被大量使用并接受了考验,这边文章是对其封装设计思路的总结,所有Idea及版权归原作者所有,抱歉源码无法分享。

  对于服务提供者来说,最为常见的情况就是把服务自身创建注册为一个临时节点,当自己挂掉之后该节点自动消失,监听服务目录的服务调用者感知变化并作出反应。这比较的直观的设计方式,基本ZooKeeper的教科书都会这么举例子,但是也带来一个问题:因为临时节点不支持创建子节点,所以这个服务提供者的其他配置信息就必须丢到node data域里面了,而如果配置信息比较多的时候就需要使用json或者乱七八糟的编码方式,这同时也意味着更新一个配置的话需要将数据全部取出来解码,修改某些字段后再编码打包,最后再将这一坨东西写回去。如果是程序自动协助完成还好,但是要在zkCli.sh的方式临时手动更新的话,这种困难可想而知。
  因此,我们不将服务提供者实现为一个临时节点,而将其创建为一个持久节点,然后在其下面建立属性子节点方便配置和更新,下面就是一个服务提供者所需要考虑的常见属性信息,当然还可以按照需求扩充,下面这些节点容我描述过来。
eink-pdf
  图中的service_name族的节点表示某个具体的服务,服务消费者可以Watch这个节点,而以host:port命令的子节点代表一个个实际的服务提供者,在服务提供者启动的时候会尝试创建或者更新这个永久节点,这个服务提供者节点的子节点包括:

C++中多读少写共享数据的保护操作

  开发过程中读多写少的情况还是很常见的,比如很多服务运行参数或者业务配置参数,这些内容只有在更改配置文件或者数据库的时候才需要重新加载,执行写操作,平常大多数时候都是只读的。对于这些配置,我暂时的处理方式(比较low)是手动向服务发送一个信号(SIGHUP,跟nginx学的),然后在服务的信号处理中进行加载和更新替换操作,虽然多线程情况下信号机制复杂的要死,大神建议的情况就是能不用就不用,但是因为服务端没有HTTP的支持,所以那些RESTful优雅的更新操作也没法使用,实乃无奈之举。曾经也在网上尝试找一些配置管理工具,但是都觉得用起来太重,因此也一直这么将就着了。
  话题转回来,对于读写数据,最常用的方式就是使用一个读写锁来进行保护,其使用基本都是下面的套路来完成的:

1
2
mutable boost::shared_mutex rwlock_;
std::map<std::string, channel_health_t> cached_health_;

  然后,读写访问起来也很清晰,使用shared_mutex和unique_mutex就可以解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool PbiChannelRoute::is_channel_health(std::string channel_name) const {
boost::shared_lock<boost::shared_mutex> rlock(rwlock_);
auto iter = cached_health_.find(channel_name);
if ( iter != cached_health_.cend()){
return it->second.is_health;
}
return true;
}

void PbiChannelRoute::update_channel_health() {
....
{
boost::unique_lock<boost::shared_mutex> wlock(rwlock_);
cached_health_ = channel_health;
}
...
}

  在读访问的时候,我们使用shared_lock来用作为读锁,而写的时候使用unique_lock排他锁作为写锁,同时在更新的时候我们注意到需要尽量把这个操作的临界区设置的最小,更新前的数据准备工作以及更新完的善后工作都丢到临界区之外执行。这个模型的优势是简单直白,在读操作远远大于写操作的时候没有什么问题,不过其中的lock contention点我们也必须心里有数:整个读操作都是带锁访问的;更新操作是赋值操作,在临界区就有一个析构和拷贝的成本(用智能指针能够优化掉)。

使用Lua Script简化Redis的访问

  之前跟公司的总监聊过一段,他的一席话还是挺中肯的:其实我们大多数的工程师,所要攻克的内容也不是什么高深的算法,难以攻克的难题,而只是完成特定功能的普通服务、普通工具,我们要做的就是把服务做稳定,性能上满足公司发展的需求,不要因为我们技术的原因被客户投诉,被老板指责就好;即使是因为合作方的原因导致的事故,也要有理有据,不要因为技术人员老实就专做其他部门的背锅侠;如果在合作方出现问题的时候,我们通过各种途径去减少甚至避免别人的故障带给我们系统和业务的影响,那就比完成工作更上了一个台阶,是把事情做好的境界了。在无法避免合作方事故的时候,我们系统受影响的投诉率趋于不断减少的收敛范围,这才是领导最喜欢看到的结果。
  其实,支付行业中的打款服务,也算是公司所有服务中业务逻辑最简单清晰的部分,但因为需要同外网各个结算通道方交互,所以也较容易受到对端不可控因素的影响。路由服务算算是整个系统中十分重要的部分,其不仅关系到费率成本,也关系到速率、成功率、稳定性等各项指标,并最终影响结算服务的质量。而且,业务逻辑是相对死板的,但是路由服务算是可玩性最强的环节,所以最近个人一直在做打款系统路由服务的优化,主要是性能的提升,以及根据服务运行指标进行自动化的流控机制。
  通过监控系统,发现一条路由请求的处理响应时间是12ms-15ms。因为最初设计实现的时候各项统计数据是完全放到Redis中的,这样就可以实现路由服务的无状态性,方便了后续服务更新和分布式部署,但是放到Redis中的缺点就是每次路由都需要进行数据的获取和更新,而且中间环节必须串行化执行,所以上面的响应指标意味着最高只能支持70~80TPS,业务量再增长一段时间打款服务就会成为整个系统的瓶颈所在,优化工作还是很有必要的。当前的一个着眼点就是通过Redis的访问优化获得路由性能的提升。

Shell脚本开发基础

  虽然不是运维狗,不需要使用shell来写主程维持生计,但是还是感觉平时用shell比较多的,其中命令行和脚本方式都用。虽然对于C++程序员来说Python的语法更为的亲切上手,而且对应的库也聆郎满目,shell可以直接方便的调用命令行工具,比如date、awk、sed,从而做些简单的处理或者控制系统更为的直接,信手拈来增加生产率也实觉容易,还是印证了那句话,没有绝对的熟优熟劣,存在的就是合理的。

一、背景

1.1 脚本执行方式

  当一个shell启动的时候,会依次按照/etc/profile、~/.bash_profile、~/.bashrc、/etc/bashrc的顺序加载配置文件,后面对环境变量的重命名可以覆盖之前的设置值。执行脚本通常有以下格式:
  (1) bash script-name.sh: 当脚本本身没有可执行权限的时候可以这么执行,或者脚本的开头没有执行脚本执行解释器的时候。
  (2) path/script-name.sh或./script-name.sh: 当脚本本身有可执行权限的时候可以这么运行。
  (3) source script-name.sh或. script-name.sh: 这会读取脚本内容并执行脚本中的所有语句,其区别是该脚本是在当前shell中执行的,而不是像其他方法那样会开启一个子shell进程去执行,其通常用作加载shell脚本库,将脚本中的环境变量、函数等导入到当前的shell中来。
  通过bash -x script-name.sh是调试脚本的一种很好的习惯,它会把执行的脚本内容输出到显示器上,在输出的内容中以+开头的表示是程序的代码,其他部分的是正常输出,这样脚本出问题的时候就可以快速定位到是哪一行异常。有时候,如果整个脚本都输出可能内容过多会分散注意力,就可以使用set -x和set +x的方式只针对某一局部内容进行输出调试,其他部分的脚本正常执行即可。

1.2 环境和配置加载

  shell可以分成interactive login、interactive non-login、non-interactive这几种类型:
  interactive login, 启动首先会读取加载/etc/profile,然后依次执行 ~/.bash_profile(这个文件会调用加载~/.bashrc)、~/.bash_login、~/.profile,而当其退出的时候,会执行~/.bash_logout。interactive shell是标准输入和标准错误输出都被绑定到了终端terminal上,因为启动脚本会加载设置\$PS1,所以可以通过[ -z “\$PS1” ]可以检查当前shell是否是交互式shell;需要在字符模式输入密码的情况,比如在终端上或者ssh远程到远程主机的情况,都是login shell。
  interactive non-login,启动时候会读取加载~/.bashrc文件。
  non-interactive,通常是执行脚本所用的shell,其会检查环境变量\$BASH_ENV,如果其指定了某个文件就会加载该文件。
  remote shell,当shell是被rshd、sshd启动的时候,它也会尝试读取和加载~/.bashrc文件。
  需要区分他们的主要原因是他们自动加载的配置文件会有所差异,interactive loginshell的功能自然是最齐全的,而如果想要配置在大多情况下生效,最好还是能被~/.bashrc直接或者间接加载到,他才是大多数情况下都会被自动加载的配置文件。

C++中的那些拷贝控制函数

  构造、析构、拷贝、移动是C++中的那与众不同的几大件,他们有些时候会自动被编译器生成、有些时候又不会,有些时候会被自动调用、有些时候又不会自动被调用,有时候编译器自动调用还是错误的版本,所以这些东西总体是比较繁琐的。依稀记得当初被面试的时候被问道:派生类拷贝构造的时候基类的拷贝构造函数会不会被调用,现在想想当时自己的回答就倍感羞愧,所以决定痛定思痛想要把这些东西再行梳理一下。
  其实,想要深入了解这个东西,还真的要去啃《深度探索C++对象模型》这本古籍才行,不然也只是人云亦云不能真正深入知其所以然。现在没时间,但是对于每一个学习C++的人来说,以后肯定是要钻研的。

一、构造函数

  当类没有声明任何构造函数的时候,编译器会自动生成默认构造函数,默认构造函数的行为是:如果存在类内初始化值,则使用这个初始值来初始化类成员;否则,执行默认初始化操作。
  C++11新标准规定,可以在定义类的时候给数据成员提供一个类内初始值,创建对象的时候该类内初始值将用于初始化成员,对于没有初始值的成员将会被默认初始化。默认初始化行为跟成员类型和对象定义位置相关:如果成员是内置数据类型且没有被显式初始化,那么当定义于任何函数之外的位置都会被初始化为0,而定义在函数体内部的内置类型将不会被初始化,其内容是未被定义的;对于类类型的对象如果没有进行显式初始化,其值由类本身所决定,直白来说就是如果没有提供类内初始化值,且该对象定义在函数中,则其内置成员变量的初始值是未定义的,其他情况下都有良好的初始值。

1
2
3
class Date {
int year {1970}; int month {1}; int day{1}; ...
}

  因为类内初始化值只有新标准编译器才支持,所以依赖编译器自动生成构造函数必须特别的小心,尤其当类含有内置类型成员变量的时候,就需要手动定义默认构造函数为这些内置类型变量进行列表初始化,而且递归看待该类的其他非内置类型成员变量,他们也需要有良好的默认初始化行为。
  如果类含有对引用、const成员以及没有默认构造行为的成员的时候,就必须在初始化列表中对这些成员进行初始化操作。同时新标准C++11还扩充了构造函数初始化列表的功能,可以执行委托构造函数的行为:委托构造函数就是使用该类的其他构造函数执行它自己的初始化过程,受委托的构造函数初始化列表、函数体被依次执行,然后委托者构造函数的函数体才会接着被执行。
  static成员是静态分配的而不属于任何一个对象,需要在类外进行定义初始化。例外是当成员是const整形、枚举类型、constexpr字面常量类型的情况,且初始化器是一个常量表达式的时候,就不需要额外在类外部进行定义和初始化,这主要是把这类成员方便当做常量符号使用,所以使用的时候不能对这类变量取地址操作。

说说HTTP的缓存

  前两天吐槽了自己的博客加载太慢,其中最主要是因为有一个4M大小的search.xml索引文件,而每次访问网站的时候都完全需要下载这个文件,这样一个1M带宽的小水管的确是扛不住,十几秒的加载时间严重影响了用户的体验
  虽然使用CDN可以解决这个问题,毕竟一般的CDN是按照流量而不是带宽收费的,但是仔细想想觉得,我的博客内容都是静态文件,只有每次更新博客的时候网页和资源文件才可能部分更新,所以完全可以告知浏览器缓存文件一段时间,或者需要该文件的时候向服务器确认该文件是否Modified,依照结果决定是否真正下载该文件即可。呵呵,这就牵扯出了HTTP中缓存的概念了。
  随着互联网的不断发展,用户体验和运营成本显得越来越重要,合适地使用HTTP缓存可以:
  (1) 降低访问延迟:通过客户端缓存可以让客户端立即从本地加载数据,或者同客户端网络最接近的服务器上加载缓存副本,而不用向原始服务器发起请求,客户访问响应资源的速度将会有很大的提升,这对于国内电信、联通骨干网络划江而治的清晰有奇效;
  (2) 减少网络带宽:国内服务器的租赁费用绝大多数成本来源于带宽,合理利用缓存可以重用缓存副本,减少实际数据的网络传输量,从而节省带宽或者流量费用。而且如果使用云计算成熟的CDN解决方案,按照流量计费则可以获得很大的访问带宽,而且这些云计算厂商同电信运营商的议价能力更强,相对用户来说也更为的合算;
  (3) 提高服务的可用性:缓存可以降低后台服务端的压力,对服务端来说本身就起到一定的保护作用,比如现在的CDN可以设置访问带宽达到某个限额回源或者拒绝访问。在某些情况下后台服务端不可用的时候,还可以设置缓存服务器返回陈旧的缓存信息,不至于让整个网站不可用。
  整个网络的访问过程,从服务端到客户端之间任何环节都可能有缓存的存在,比如:
  (1) 浏览器缓存:记得当初网上列举优化Windows的一项内容,就是设置浏览器的缓存大小和位置(Ramdisk内存盘中),浏览器可以把认为可以缓存的东西保存到本地磁盘,下次再需要对应内容的时候只要检查本地缓存合法后直接加载就可以了,对于频繁访问的网站,以及浏览器返回这种情形十分有效。
  (2) 代理缓存:一般存在于大型公司或者ISP等机构,为其所服务的客户提供可配置(比如浏览器设置代理地址)的或者无感知的缓存服务,他们扮演者一种中间人的角色,其提供的缓存是一种shared cache。比如我们下载大尺寸的软件或者视频,看似国外的地址但是却急速下载完成,就是运营商把一些热点资源缓存来下的结果,而天威、长城这类宽带浏览网页慢但是看视频飞快,也是这么个原因。
  (3) 网关缓存:也被叫做反向代理缓存,通常是服务提供着向外部署的一套缓存服务,用以提高自身服务的可伸缩性、可靠性和响应性能。服务的请求通过负载均衡等技术路由到缓存服务器上,缓存服务器将本地有效的缓存直接返回给客户端,或者将无效、不存在的缓存向原始服务器更新后,再回传给客户端,我们常说的CDN就是这么个角色。其实上面一点就是我们常说的正向代理,这里说的是反向代理,他们都是加载在客户端和真实服务端之间的网关,只是某些角度不同罢了。
  当然缓存是一个思路,各个行业各个服务都可以借鉴,这里我们就着重关注HTTP中的缓存了。

咱又败了个电纸书阅读器

  前两天公司组织年度体检,然后发现右眼视力下降了许多,其实毕业上班后视力都没有下降过了,还让我误以为成年了视力就不会下降了,然后套餐内又做了个脑部CT检查,报告显示眼睛周围温度过高,提示用眼过度,注意用眼卫生和休息。
  毕业后连续在TP-LINK和智科网络就职后,感觉这大半年在移卡工作还确实挺累的。虽然公司没有强制或者潜规则的加班制度,但是相对来说压力要大很多:自己负责的是打款业务,是实实在在的资金转账,所以任何一个操作都绷紧着神经反复确认的;公司合作的几个出款通道时长都会出点问题,即便是老大哥银联的系统也有过抽风,一旦出款异常就会有大量商户咨询或投诉,客服的压力倒灌到运营、财务,最后击穿到我们技术结算组,所以系统部署了大量的监控系统让我们时时处于应急状态;入职公司后就一直着手着打款系统的重构工作,借助MQ、RPC、Redis等技术手段将原先一个传统的巨无霸程序解耦成若干单独的服务和模块,系统的稳定性和可维护性得到了很大的提升,不过每天的清算资金也从我入职的3~4亿激增到现在的15亿+,这对现有系统的性能和可用性提出了更高的挑战。这大半年觉得自己的工作内容充实了很多,技术在某些程度上也增长了不少,但是随着自己眼界的开阔、业务系统的急剧增长,对现有系统的要求也越来多,感觉自己需要学习的东西更多……
  上面这一段说了半天,就是觉着自己之前学习的一些皮毛,很多都需要再深入、再系统的去学习,尤其是数据库、计算机网络这类的东西,使用得当会收益很大,于是自己的网盘中屯了好多后台开发、分布式系统相关的技术书籍,想要在工作之余多充充电。但是自己不是很喜欢看纸质版的技术书籍,除非是像《C++ Primer》这种需要反复参阅、圣经级别书才会买,普通的技术书籍没有收藏价值、搬家麻烦,而且不能做到轻巧携带随时可以查阅,扫描格式的PDF成了我的首选。忙乎了一天之后回家眼睛无比的干涩,完全没有打开电脑、打开iPad的欲望,手机又太小无法使用零碎时间,如果有一个可以满足各项需求的电纸书该有多好:

超好用的会话管理工具Tmux

  作为Linux服务端程序资深开发者,归根结底不得不承认终端命令行的工作环境反而是最具有效率的。在Unix-like阵营中会话终端工具有老牌的screen和新秀tmux,而通常来说99%的Linux用户都是折腾帝(本人之前也不例外),而tmux通过配置可以得到十分酷炫的界面效果,因此普及率上tmux要比screen流行的多。
  现在的情况是,基本都用自己的rmbp远程连接多个服务器工作,iTerm标签一开多有时候自己都凌乱了,所以觉得配置完善一个tmux工作环境还是会很受用的。通过tmux,你可以在一个terminal window的环境下创建多个独立的window,而每一个widow中又可以有一个或者通过split切分成多个pane,在每个pane中运行着一个独立的terminal实例,里面躺着一个独立的shell,这样就可以让你在这个框架下各个pane中独立做对应的任何工作了。在实现上,tmux会将这些window和pane归纳到一个session当中,用户可以在任意时刻detach这个会话,而这个detach无论是你主动操作的,还是因为网络等因素被动触发的,总之只要这个tmux进程还活着,那么其上面所有的window、pane及其管理运行的任务都会被保留,而在后续任何时刻都可以随时再attach到这个会话上面去,而所有的工作环境立马都会立马全部恢复。
  实际中,tmux工具帮我们解决的痛点有:(1) 在终端关闭的时候session仍然安全的被保留着,那么session中的任务得以持续运行,就可以让程序保持终端而不需要通过&将服务作为后台进程去执行了;(2) tmux也可以看做是一个会话服务端,然后你在任何地方只要登录到对应的机器,再attach到对应的会话就立马恢复之前的工作环境,这样就可以安心的在家加班了;(3) 管理多个会session可以方便的切换,就像生活一个号工作一个号一样的道理,同样可以把工作内容组织成多个逻辑的session,然后方便的切换工作角色。

  tmux工具需要熟悉的东西有两个:操作快捷键配置文件,主题也就围绕着tmux的三个重要组件展开:Pane、Window、Session。
  tmux的所有操作都需要一个prefix key,默认是Ctrl-b,而几乎所有定制化tmux的都介绍将它修改成了Ctrl-a,因为Ctrl和b两个靠的太远了,基本没法一只手去独立完成,但是Ctrl-a又和shell的快捷键冲突……。既然Ctrl-a已经熟悉了,也就不跟风修改prefix key了,对现有工作影响越小越好。

搞个CDN加速却又吃了苍蝇

  细心的朋友应该发现目前小站的域名已经更换了,一方面从美学角度考虑看来这个.net域名配合taozj在形态上显得更为对称,二来是.org已经绑定邮箱域名了,而裸域和MX记录之间协作有些问题(主要是CNAME和MX不兼容,很多域名解析商不允许裸域指定CNAME记录)。在观察到这个域名到期之后就立马就从狗爹家一口价买了下来,然后考虑到接下来快到期的时候再转到Namesilo名下,因为后者价格便宜、操作界面简洁,而且还免费送隐私保护。之前已在阿里云备案了.org域名,这次只需在原备案号下增加个域名就可以了,而且在广东的话阿里云支持手机上传资料,整个备案环节就十分的快捷方便,虽然中间一些资料的审核修改折腾了几次,但阿里云的客服妹纸还是挺耐性的帮助解决,没过几天管局就通过了。
  接着在腾讯云申请了SSL证书后,就将.org所有请求都导到了taozj.net下面,到此为止顺风顺水,总体感觉还是不错。但是因为托管的主机带宽不足,而本站还挂着个几兆大的search.xml索引文件,所以细细的小水管拖慢了页面加载完成的速度,打开Chrome开发者工具发现那个文件下载了近10秒钟,云测速也是红红的一大片,因此严重影响了用户的体验。对此,除了打开Nginx的压缩配置外,解决的方式也只能祭出CDN大旗了,CDN按照流量计费带宽很高,整体效果应该会好很多的。
  国内除了几大巨无霸云厂商之外,做CDN比较有名的就数七牛云和又拍云了(而实际上两者有着紧密的合作关系,导致选啥效果也没多大区别),其实自己之前对七牛云的印象还是不错的,因为经常追“神秘的程序员们”系列漫画,而他们又在上面投了不少软硬兼施的广告,即使七牛云操作界面的配置更新和生效操作被吐槽了许久,但反正这东西又不是天天配置,所以想来也觉得没啥。直到今天的一件事,确实把我给恶心到了。

C++新标准中基于任务的异步计算模型

  C++中的任务(task-based)是区别于传统的异步事件开发和多线程开发的一种更高级的开发手法,为更高级于线程的一种同步并发编程模型的抽象,他让使用者只需要关注通过特定的参数和逻辑完成特定的工作,并最终返回相应的结果,而不必关注线程的创建管理、数据传输、锁机制等底层细节性的东西,让开发者的精力可以更加集中与应用程序逻辑的实现。
  C++的异步计算模型由future、promise、package_task和async这几个组件组成,他们实现的关键点是允许两个任务之间传输值,而无需显式的使用锁机制:任务执行的结果放入到一个promise中,关心需要此任务结果的角色则可以从future中提取结果,联系promise和future的是一个称之为共享状态(shared state)的对象,其构成除了任务正常执行完成通过set_value保存值、或者执行发生异常后通过set_exception保存异常信息之外,他还应该包含两个thread之间安全交换数据所需的信息、一个就绪信息表示是否可供future提取结果等部分构成。
future-promise
  上述结构实现了方便的进程间交换执行结果,如果使用std::thread自创线程,这种交换数据就必须使用一个共享变量来实现。
  当然,Facebook的Folly库也有一个改良版的future实现,现在还没能力高攀这个传说中不存在公司的高大上组件,话说Facebook的Call Chain风格还真显特色,不过其对美国联邦政府的态度真的让我唏嘘。

一、C++任务中组成部分解析

1.1 promise

  promise为一个共享状态的句柄,其最有用的两个成员函数就是set_value和set_exception。promise没有拷贝操作,只支持移动操作,并且调用set_value/exception只能执行一次,否则会抛出future_error异常。
  相比于传统的线程方式执行任务,一旦任务发生异常,默认情况下线程将会终止执行,从而默认导致整个程序的挂起,这里通过传递异常的方式,不仅让程序更加的稳健,而且调用者可以据此做相应的处理。