Git工作流的使用

  最近好久都没有更新博客了。
  一方面是自己“三十 而未立”也当爸爸了,在幸福洋溢之余也还有很多的事情需要去做,很多时候下班和周末没有时间开电脑了;另外就是工作上面的事情比较繁琐,而且与此同时挤出零碎的时间完善了一个HTTP服务端框架tzhttp和一个简易的信息收集系统tzmonitor,关于这两个系统的细节我后面会单独描述,此处暂且不表了。不过在写这两个项目的时候,我使用了Git Workflow和Git Submodule特性,感觉确实很方便实用——如果说Git是一个强大的版本控制工具的话,那么Git Workflow就是在现实工作中活用这个工具的“术”了,Git Workflow就像是一个Git在团队实践项目中的工作模式,套用这个模式可以清晰完美解决工作中的版本控制、团队合作、迭代发布的需求,其本质提现的是有效的项目流程管理和各个参与者对于开发维护工作的协同约定。
  说到Git Workflow就不得不谈大名鼎鼎的A successful Git branching model了,这里祭出他的Workflow框图:
git-workflow

人到中年,是该给自己配份保险了

  随着爸妈年纪越来越大、新的生命即将诞生,从来没有感觉自己的责任越来越重,而且“享受”独身子女政策光芒照耀的人,会这种无他可依的孤独感尤为的强烈。太太很久之前就说到保险的事情,现在看来确实是很有必要——倒不是说怕死,而是作为家庭的顶梁柱,已经突破个人的价值,而是全家的依托之所在,虽然中国人从心底里有一种抵触保险的情节(而他的产生感觉毫无根据可言),除了强制的社保、医保、车险之外几乎不会涉及其他的商业保险,身边买其他商业保险的人主要也是身边的朋友是干买保险这一行的,不过在中国这种国情和大环境下,也只有通过保险才是最稳妥的方式手段,借用杠杆提升家庭的抗风险能力。
  年底前也话了不少时间,从一个保险文盲小白,成长为一个对人寿保险略知一二的人,下面就是个人对保险的认识和理解,以及和我相似处境同伴的一些建议,希望能对大家提高保险意识、选择保险产品尽一份绵薄之力。

  言归正传,除了央行、证监会和银监会,中国还有一个相对低调的保监会。因为央行和银监会和大家手里的资金以及社会生活息息相关,所以大家会很熟悉;中国股市跟中国足球一个德行,正所谓恶名传千里,证监会几乎天天被大家问候,曝光率自然很高;中国家庭绝大多数都是保守的储蓄理财,保险除了我们这一代年轻人才有所关注之外,老一代人除非银行渠道推销,受众范围小了自然也显得低调很多。保险按照类型也分为财产保险、养老保险、人身保险,财产保险只有家产万贯的大佬才会考虑,作为一个都快活不下去的穷屌丝自然无须关注;养老保险基本也是钱多的人烧的慌,随着货币不断贬值,感觉还是买房养老更可靠些;人身保险主要以人的健康为目标的保险,每个人都不能保证自己永远健康无疾而终,这个是涉及到人生存本能的需求,也是这篇文章所关注的险种。此外还需要说一句,就像支付公司需要央行的支付业务许可证牌照一样,保险公司也需要保监会的保险牌照才行,不是你有钱就能玩的,而且保险牌照还有地区、险种的限制。

春节长假中的学习小结

  今天没有像往年一样做一个年终总结贴出来,主要是技术方面在之前的文章中都慢慢透露出来了,而且年底也公司也填了各种绩效考评,总结性的东西都写的想吐了,所以也不再流水账了。今年放假没回老家,老爸老妈都来深圳照顾我们了,虽然过年少了些热闹和气氛,但是家人围绕一起的感觉也没显孤单,而且少了拜年胡吃海喝那些形式,倒是七天长假有了些许空闲的事件。
  这几天读了几本书:《Rust 程序设计语言(第一版)》、《Go语言实战》、《RabbitMQ实战指南》、《TCP/IP高效编程:改善网络程序的44个技巧》、《MySQL技术内幕 : SQL编程》,有些想说的,就码成一文吧。
dog-year
  前面两个是编程语言类的图书,读他们倒不是想折腾新开发语言的意思:首先C++已经很复杂了,新标准刷存在感似的层出不穷,足够自己喝几壶慢慢折腾的了,虽然C++使用经验也挺多的了,但自己还不敢冒言“精通”之;而且随着年龄增大,对自己的方向和目标也更显的清晰,集中精力在重要、感兴趣、本质性的领域,不爱折腾这么多现象层面的东西了。了解Rust主要是最近区块链越来越热了,据说绝大多数的区块链框架使用Rust开发,所以这个语言是不是很独特?Go作为含着金汤匙出生的幸运儿,在中国搞的风生水起,各种资料、框架、Repo排山倒海般的袭来,所以也想看看这个语言为啥这么快风靡世界!

Lua脚本语言语法速览

  之前先上了Redis Lua的优化,这里把Lua的基本语法也顺便整理一下,后面回顾查阅会比较方便。
  其实关于Lua这个脚本语言,之前在TP-LINK工作的时候就与之擦肩而过,那时候别的小组在用他做路由器软件(基于openWrt)。后面在逛风云大大的博客时候也时常遇到过他的身影,另外在拜读开涛《亿级流量网站架构核心技术》的时候,书中描述针对大流量网站的开发、运维、管理等很多地方也是用Lua的。虽然此时对Lua的使用场景接触有限,但是看过Lua教程后发现他是一个精炼小巧的语言,现在开发圈子系统语言、脚本语言纷繁丛杂,在他们争来争去不可开交的时候,Lua反而在夹缝中找到了适合自己生存的土壤:他不求接触系统底层获取超凡执行性能;也不求建立丰富的数据结构和程序库,培养封闭自己的生态圈;而他把自己变得极为的小巧精简,用ANSI C实现因此可以运行在几乎所有的架构平台上面,也可以寄存在很多成熟的开发组件当中,当别人觉得蹩脚不方便的时候,我能够见缝插针迎难而上,真是个不错的策略啊。
  Lua程序设计这本书通篇200多页,相比于其他语言的教程可以说是很简短了,其中由易到难描述了Lua语言的方方面面。Lua要是深挖的还是有很多东西可以学的,因为我们只是用他来做插件,当前看来不会做很复杂的大规模的开发,就先看看数据结构、语句、函数这类语言的基本知识吧!

一、Lua概述

  Lua的语句不需要分隔符,所以不用纠结换行、分号等这些东西,可以按照自己的习惯写出工整方便理解的句式。Lua解释器会不断的尝试解析输入的每行内容,如果发现不能够形式一个完整的语句块,就会等到更多的输入内容。在交互式模式下,通过EOF或者os.exit()可以退出交互式Lua shell;如果将执行的语句放入文件中作为脚本,在Lua shell中可以通过dofile(“file.lua”)就可以加载这个脚本;如果只想测试某些Lua语句,可以运行lua -e “print(math.sin(12))”的形式,这样就直接运行语句,而不会启动生成交互式的Lua shell。

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

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

  对于服务提供者来说,最为常见的情况就是把服务自身创建注册为一个临时节点,当服务自己挂掉、或者网络异常之后该节点自动消失,监听服务目录的服务调用者会感知到该变化并作出反应。这是一种比较直观的设计方式,而且基本所有的ZooKeeper教科书、手册也都会这么举例子,但是也带来一个问题:因为临时节点不支持创建子节点,所以这个服务提供者的所有配置信息就必须全部丢到node data域里面了,如果配置信息比较多的时候就需要使用json、xml或者类似的方式组织编码,也就意味着更新一个配置的话需要将数据全部取出来解码,修改某些字段后再编码打包,最后再将这一坨东西写回去。如果是程序自动协助完成还好,但要是想在以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中的缓存了。