说说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异常。
  相比于传统的线程方式执行任务,一旦任务发生异常,默认情况下线程将会终止执行,从而默认导致整个程序的挂起,这里通过传递异常的方式,不仅让程序更加的稳健,而且调用者可以据此做相应的处理。

再说C++的lambda表达式

  一直以来,lambda仿佛都是像是脚本语言的专利,对于C/C++这类系统级强类型的编译语言来说,实现匿名函数几乎是不可想象的,不过现在C++11已经支持lambda创建匿名函数了。对于一些小而简单的代码,创建匿名函数再方便不过了,因为围绕程序员最头疼的难题就是吃饭吃什么,变量、函数该取什么名,所以Lambda支持必然深受大家的喜爱。
  传统上,大家都是系统先定义一个含有operator()的命名类,然后在创建该类的一个对象,最终在合适的位置通过该对象调用函数。其实,这个步骤就算是lambda的前身了,lambda语法自动创建匿名类和匿名对象,像是上面传统可调用类实现繁琐步骤的快速实现,一处定义且只使用一次,可以方便的结合各种标准算法库作为谓词使用,或者智能指针的deleter定义,同时也可以创建闭包任务执行各种回调等任务。总之结合C++可调用对象的概念,可以大大简化了项目的设计和实现风格。

1
std::find_if(container.begin(), container.end(), [](int val) { return val > 0 && val < 10; });

  本来以为lambda把捕获搞清楚就可以了,但是细究下去还是需要梳理一下。
  lambda的组件包括:一个可能为空的捕获列表、一个可选的参数列表、一个可选的mutable修饰符、一个可选的noexcept修饰符、一个可选的->返回类型、一个执行表达式体。

1
[ capture ] ( params ) opt -> ret { body; }

一、捕获

1.1 lambda的捕获类型

  有些时候,我们需要控制lambda是否允许和如何访问局部名字,这时候就需要指明捕获信息。在lambda中,我们可以选择的捕获类型有下面这几种:
  []: 空捕获列表,意味着lambda无法使用期外层上下文中的任何局部名字,其内部执行体所需的符号只能从实参或者非局部变量中获取;
  [&]: 引用隐式捕获,其所有的局部名字都能使用,所有的局部变量都用引用访问;
  [=]: 按值隐式捕获,所有的局部名字都能使用,所有名字都是指向局部变量的副本,这些副本是在lambda表达式的调用点获得的;
  [捕获列表]: 只捕获列表中的变量,不捕获其他变量,捕获列表中可以出现this。
  [&, 捕获列表]、[=, 捕获列表]: 某些变量进行特殊的捕获方式,其他变量采用默认捕获方式,捕获列表中可以出现this。
  在使用中,当我们考虑选择捕获类型的时候,使用捕获列表可以具有更细粒度的捕获控制,而如果希望局部对象可以写入修改,或者捕获的对象很大会有拷贝负担,则可以考虑使用引用捕获;但是lambda用于闭包执行的话,其有效期可能会超过其调用者,而且如果lambda的创建和执行在不同线程的话,一般通过按值捕获会更加安全。

右值引用、移动语义和完美转发

  C++作为一门极为讲求运行效率的编程语言,在新标准中引入了右值引用和移动语义,同时通过右值引用还可以实现参数的完美转发,方便进行通用模板编程。

一、右值引用

1.1 左值和右值

  在C++中,对象具有两种属性:(1)有身份:在程序有有对象的名字,或者指向该对象的指针,或者指向该对象的引用;(2)可移动:能否把对象的内容移动出来,比如把对象的值移动到其他对象中去,该对象移动后处于合法但是未指定的状态。
  所以,根据上面两种属性总共有四种组合出现,不过没有身份又无法移动的对象不重要也不需要讨论,因此可以看到:
right-val
  从上图看到,一个经典的左值有身份但是不能移动,一个经典的右值允许执行移出操作,而其中的特殊值常常是用std::move()方式得到的,他本身具有身份,但是通过上面的操作也允许其执行移出的许可。纯右值一般是函数以非引用方式返回的临时变量、运算表达式(比如运算、关系、位、后置递增递减等)产生的临时变量、原始字面量、lambda表达式等,都是纯右值的概念,这些对象即将被消亡且没有其他的用户;特殊值是C++11中新产生的,比如将要被移动的对象、T&&函数返回值、std::move()返回值和转换为T&&类型的转换函数的返回值。

1.2 右值引用

  在C++中,有左值引用、const左值引用、右值引用三种类型,他们的使用目的各不相同:普通左值引用所引用的对象表明用户可以执行写入、修改操作;const左值引用的对象对用户看来是不可修改的;右值引用对应于一个临时对象,用户可以修改这个对象,并且确认这个对象以后不会再用了。

C++之参数类型推导

  参数类型推演算是Modern C++中的重点内容,他们会在函数模板、auto、decltype中被用到,不过这东西确实很隐晦,想要搞透他们也的确不容易,经常被弄得头晕晕的。但是这种硬骨头不啃也不得行,绕过他想在Modern C++中进行简洁、高效编程几乎是不可能的,尤其当你想偷懒进行某些通用编程的时候。

一、参数推演概述

  参数推演指的是编译器根据实参推导出相关模板参数的行为,这种行为不同于其他脚本类动态语言发生在运行时执行推演,而是在编译器编译的期间发生的。例如我们常见的对于函数模板的情形:

1
2
3
4
template<typename T>
void foo(ParamType param);
foo(expr);

  上面为模板函数定义形式,下面为实际调用表达式,在编译时候编译器需要推导出TParamType的具体类型,这里用两个名字表示是因为两者的类型通常是不相同的,因为ParamType通常还会带有一些额外的修饰符,比如:const、volatile、reference、pointer等。

1.1 模板参数推演规则

  (1) ParamType是指针或者引用类型,但是不是universe reference类型

此时如果expr是引用类型,则将引用的部分忽略掉;然后将上一步去除引用的剩余部分和ParamType进行匹配,以确定T的类型。
这里也就是意味着底层的const、volatile修饰符将会被保留。

  例如对于如下使用:

1
2
3
4
5
6
template<typename T>
void f(T& param){}
int x = 27; f(x); // T -> int, param -> int&
const int cx = x; f(cx); // T -> const int, param -> const int&
const int& crx = x; f(crx); // T -> const int, param -> const int&

  在面的例子里,即使模板参数的类型是T&,也可以传递给他一个const对象来调用,此时const的属性被推导至T参数中;最后一个调用在推导的时候首先会将引用剔除掉,所以得到的T类型和第二个调用是一样的。

C++11之统一初始化语法

  在当前新标准C++11的语法看来,变量合法的初始化器有如下形式:

1
2
3
4
X a1 {v};
X a2 = {v};
X a3 = v;
X a4(v);

  其实,上面第一种和第二种初始化方式在本质上没有任何差别,添加=则是一种习惯上的行为。使用花括号进行的列表初始化语法,其实早在C++98时代就有了,只不过历史上他们只是被用来对数组元素进行初始化操作,以及初始化自定义POD类型的数据(简单理解就是可以memcpy复制对象的类型)。比如:

1
2
3
int v1[] = {1, 2, 3, 4};
int v2[5] = {1,2,3};
char msg = "hello, world!";

  在使用列表来初始化数组的时候,如果声明数组的时候没有指定数组尺寸大小,则编译器就使用其列表包含的元素个数自动计算数组的尺寸;如果提供了数组尺寸,但是列表的元素数目小于数组尺寸,则系统会将剩余的元素全部赋值为0。如果是字符数组的话,C++还支持使用字符串常亮来进行初始化。

一、C++11的统一初始化器

  在新标准C++11中这个东西使用范围和特性被大大的扩展了,而且已经成为了一个基础而又重要的利器,几乎可以执行任何的初始化操作,所以也被称为”Uniform initialization”,尽管国内还是习惯上称为列表初始化。因为他可以避免传统初始化中的诸多问题和缺陷,所以从Bjarne Stroustrup爷爷的《C++ 程序设计语言》描述口吻看来,列表初始化是被大力推荐使用的,即便用惯旧式初始化的C++程序员初看起来会很不习惯,但C++强烈建议使用上述第一种方式进行统一初始化操作。

C++之CRTP手法

  初次相识CRTP就是boost::enable_shared_from_this,这种以派生类型作为模板参数生成基类的形式,让人看着很新奇。
  参阅了很多的文献,大多是讲使用CRTP可以实现编译器静态多态的效果,并与之同传统的虚函数实现多态进行对比,这里也类似的探究一下多态的特性。其实CRTP的主要功能是用模板类实现通用功能,然后其他类将自己作为模板类的模板参数并继承这个实例化的类,以获取模板类提供的额外功能的方法。

一、C++的多态实现

1.1 传统的虚函数实现多态的原理

  我们通常讨论的C++之多态是通过虚函数的方式来实现的,其被称为运行时刻的多态(dynamic polymorphism, runtime polymorphism),因为程序运行时刻具体调用的函数实体是根据指针或者引用实际指向的实际对象类型来决定的,这就是C++的延迟绑定或者称为动态绑定的特性。
  在C++中,上述特性是通过vtable和vptr来实现的,比如某个基类Base具有function1和function2两个虚函数,并以其为基类得到两个派生类D1和D2,两个派生类D1和D2分别override了基类的function1和function2虚函数,这种关系下的vtable则形如下图所示:
vtable
  在C++中,每一个使用了虚函数的类(或者从含有虚函数的类派生出来的子类)都会拥有一个vtable,该vtable是在编译期间由编辑器产生的一个静态数组,该数组中的每个条目则指向了该类型可以访问的最接近的虚函数。这里最近可访问的函数就意味着,如果派生类override了基类的虚函数,则该vtable中的对应条目就指向派生类的虚函数,如果派生类没有override基类的虚函数,则该条目就直接指向其基类的虚函数版本。
  这里的vtable是与类类型相关的,而不针对于任何创建出来的具体对象,所有该类型的对象都共用该vtable,但是程序的执行依赖于创建的对象来运行的,所以编译器在创建vtable的同时还创建了vptr成员变量,该变量还会被所有的派生类所继承,当使用一个具有vtable的类型创建一个对象的时候,该vptr成员就指向了其具体对象类型的vtable。
  通过上面的这些操作,C++通过虚函数实现运行时绑定就显而易见了:程序在执行一个函数调用的时候,如果发现该函数是一个虚函数,则用其vptr成员访问其动态类型关联的vtable,然后在vtable中找到合适的虚函数版本进行实际的函数调用。不过经过上面的步骤,我们会发现使用虚函数机制,会让每个对象增加一个额外的vptr大小的开销(比如64位系统上就是8字节);同时在调用虚函数的时候还需要查找虚函数表去寻求正确的函数版本,所以会有人诟病虚函数机制会对函数调用性能产生负面影响;还有就是虚函数通常不能被inline,对于一些尺寸小的高频调用函数将无法得到内联的优化。