【置顶】个人阶段性学习和规划总结(技能树)

  本人专注后台服务端开发一晃已经好久了,自我感觉上不仅经验增加了很多,同时接触到的东西确实不少,在增加见识的同时博文也批量更新了很多。大多人看来内容很多很杂,此处做一阶段性整理和总结吧,给自己也梳理梳理一下。
  其实不仅仅是后台服务端开发这个方向,就整个软件开发来说,其知识构成也是有所层次的。个人毕竟不是正规计算机科班出身,也就是大家所说的半路出道、自学成才的野程序员,优点就是不会被那些所谓正规计算机教育的所束缚禁锢住,但是很多时候感觉自己的知识构成还是有所缺陷。既然励志要靠撸代码吃饭养家,那么长痛不如短痛,晚补不如早补,该学的终究跑不掉!

【置顶】博客资源收录大全

  虽然当下微信公众号席卷自媒体市场之势如火如荼,但是针对文章展示的话个人还是偏向于独立博客的形式,主要是因为个人可控制化的东西比较多。我想,在被条条框框束缚的无以喘息的情况下,这个时代没有什么比个性和自由更为重要的了吧。
  下面是一些网络知名人士的博客,以及平时在搜索资料过程中遇到的好站点,都被一一记录下来了,真心喜欢的话可以用RSS订阅这些站点,其中很多都是全文输出的。因为个人的兴趣取向问题,除了将不感兴趣的前端、移动端外,过于偏向于Java/Nodejs等语言化的博客也被KO了(即使编程思想是独立于语言而存在的),希望平时没事多看看吧!

服务重构过程阶段性小结

  首先呢,看到这篇文章的时候,重构工作说明已经进行的差不多了,不然也没心情出来跟大家扯淡了,而前几天真是够自己忙活惨了的。总体来说业务过程不是很复杂,所以项目规模也不是很大,不过经过这个蜕变的过程,还是有些东西想说说总结一下,而且这个过程中也不可避免的造了点轮子。其实原本项目中也有很多的轮子,有些不好用、有些晦涩难懂也不敢再用,所以现在这个项目中除了将有些轮子使用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,意在告诉该接口客户端不需要等待服务器的响应结果。比如在将日志信息传递给服务端存储的时候,客户端不需要阻塞等待响应结果,从而增强客户端的效能。

Redis基础学习笔记

  Memcached和Redis都是非常有名的旁路缓存,但显然这些年Redis的风头大大盖过了Memcached,Memcached作为一个单纯的键值缓存或许够用,但是相比而言Redis提供了丰富的数据结构、主从复制、持久化的功能,俨然就是个高性能的精简数据库(所以他也被认为NoSQL的一种),所以如果两者性能没有大的差异的话估计正常人都会选择Redis。
  缓存用于降低IO延迟,避免重复存取、重复计算等效果显著;而且在运营技术越来越讲究的情况下,无状态服务的弹性计算和容灾也越来越流行,无状态的服务器可以快速添加到负载均衡中,失效的服务器也可以快速被替换掉,不过这些无状态服务也通常也会采用缓存来共享数据(虽然数据库的SELECT FOR UPDATE也可以实现一定的同步功能,通常用在对数据完整性要求高、性能求其次的情况下);而且Redis本身是单线程的,且提供简单的流水(事务)操作机制,所以基于Redis的简易分布式应用案例也数见不鲜,比如分布式锁、分布式队列、分布式ID生成器等案例。
  本文就对Redis的基本数据类型进行整理,同时对其流水化、持久化等特性进行整理。

一、数据结构和基本命令

  Memcached只支持简单的键值存储,而Redis除了支持字符串值外,还支持列表、集合、散列表、有序集合这四种数据类型,这和很多编程语言内置的数据集合类型极为相似,更为神奇的是根据不同的数据类型还能进行一些逻辑、集合运算,使用这些Redis内置的数据结构和操作支持很容易组建出简单逻辑事务。
redis

1.1 Redis基本数据结构

1.1.1 STRING 字符串

  字符串类型可以用来存储字符串、整数或者浮点数,支持对整个或者部分字符串的字符串常用操作,而对数字类型提供自增、自减操作。
  SET、GET、DEL是常见的键值操作命令。
  INCR | DECR 将键存储的值自增、自减1
  INCRBY | DECRBY 将键存储的值自增、自减整数值
  INCRBYFLOAT 将键存储的值加上浮点数
  需要注意的是,将对存储在Redis中的字符串进行自增等计算时,如果该字符串可以被解释为十进制整数或者浮点数,则在原来的基础上进行正常的计算,而如果对一个不存在的键,或者其值为空的键进行上述操作,其初始值被假定为0,而如果其值无法被解释为数字的时候,Redis将返回一个错误。

再说socket的SO_REUSEPORT选项

  上次扯epoll的时候连带提到SO_REUSEPORT这个socket标志,虽说标志比较的简单,说来就是允许在一台主机上的一个IP:Port上可以创建多个socket,而且这些socket可以分布在相同主机的同一个线程、多个线程、乃至多个进程中去,内核会自动把这个端口的请求自动分派到各个socket上面去,而且这个过程没有用户惊群、互斥等问题。正如前面一篇文章所描述的那样,事件驱动框架epoll为利用多核优势运行在多线程、多进程环境下,伴随而来的各种毛病问题多多,而这个标志看似就是解决这些毛病的良药。
reuseport

1
2
3
int optval = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
bind(sfd, (struct sockaddr *) &addr, addrlen);

  在使用的时候,只要第一个服务设置了这个选项(为了保护端口,否则恶意程序可以偷取任意端口的数据),那么后续的服务设置这个选项然后进行bind(为了安全,后续程序必须和第一个程序具有相同的effective-uid)。除了常用的TCP服务端外,UDP也可以使用该选项,比如常见的DNS服务器,传统上是多个线程竞争执行recv()操作接收数据报,而使用SO_REUSEPORT选项后,内核负责将请求分配在这些socket上面。
  之所以要把这个东西单独拿出来研究一下,是这个特性可能会影响甚至改变高性能服务器的设计和实现,以后的服务不叫多线程服务、多进程服务,而称之为多socket服务了。
  在传统模式上开发高性能服务器,主要的方式有:

epoll事件驱动框架使用注意事项

  自己一直订阅云风大哥的blog,今天看到期博文《epoll 的一个设计问题》,再追踪其连接看下去,着实让自己惊出一阵冷汗。真可谓不知者无畏,epoll在多线程、多进程环境下想要用好,需要避过的坑点还是挺多的。
  这篇博文主要是根据Marek的博客内容进行翻译整理的。
epoll
  epoll的坑点主要是其最初设计和实现的时候,没有对多线程、多进程这种scale-up和load-balance问题进行考虑,所以随着互联网并发和流量越来越大,越来越多的epoll flag和kernel flag被引入来修补相关问题;而来epoll的用户态空间操作接口是file descriptor,内核态管理接口是file descripton,有些情况下两者不是对应关系,会导致程序的行为很奇怪。

基于rsync的开发环境代码传输

  现在很多的后台服务端开发者都不能享受本地电脑编码、本地编译调试了,一来为了兼顾沟通、办公等需求电脑都会装Windows系统,如果不是一个Geek氛围浓厚的开源公司很难Linux单系统走天下;二来现在的服务越来的越复杂,使得开发的模块很难在本地单独运行和调试;再则就是企业处于安全和管理的需要,办公网络和开发测试网络、生产网络会进行隔离,那么很有可能你的代码就必须传输到目标机器上进行编译和调试了。
  如果环境的网络拓扑比较简单的话,使用CIFS、NFS、SSHFS等网络文件系统就可以解决这个问题,但是如果网络环境比较复杂的话,比如中途给你设置一个跳板机,那上面的方法基本没辙了。之前了解到有很多同事当前都是ssh到目标机器上,然后直接在目标机器上用vim进行编码,如果这类开发者要么之前就习惯了使用vim用作IDE来开发,并且有一套压箱底的编辑器配置文件共享上去是很赞的,否则想想这种方式阅读和编码就格外蛋疼。
  我们要相信,程序员是最不喜欢做重复性体力劳动的。一问其他的同僚,大家给出的方案是inotify+rsync,自己一搜看来这个组合的确强大,其可以用于企业级平台下的文件备份、资源分发和更新等各种需要“同步”的需求。然后作为macOS工作机的我来说,自然要变通一下才可以。
  说到rsync这个工具使用起来比较简单的,但是前几天遇到一篇美团的博客,才得知这东西还是大有讲头的。rsync最求的是极致效率,为此设计出快速的差异监测和增量更新算法,从而减少网络占用并增加同步速度。由于需要差异比对,因此除了发起同步的源端外,还需要在同步目的端创建rsync服务:当同一个主机相同或者不同文件系统中使用rsync同步的话,操作系统会fork出目的端rsync进程,和源端进程使用管道进行通信;当需要跨主机同步的时候,源端通常通过SSH和目的端连接,然后源端向目的端发送命令创建一个rsync的进程,当然目的端也可以采用rsync服务守候进程的方式,自动侦听873端口。