再说Go语言

  当前如火如荼的Raft最成功得意之作莫属基于Go语言实现的etcd了,以目前形式看来大有挑战ZooKeeper作为企业级通用分布式协调组件之势。从社区状态看来etcd的更新也十分活跃,而且不少公司的分布式组件也是基于etcd中实现的Raft的基础之上再开发,所以说应该算是久经磨练考验了吧。etcd项目在其README.md中还描述到其Raft实现除了微小的变更优化之外,基本是完全按照原作者的论文设计来实现的,所以除了logcabin之外,从etcd的实现开始接触企业级的Raft实现也是一个门路(主要是logcabin好久没更新了,折腾的人少让人感觉好孤单……)
  背这次算是真刀实枪、认真系统性地学习了一下Go语言,同之前蜻蜓点水不同,随着对这个语言了解的更加深入,结合同C++相对比还是有一些心得和想法的,下面就简单聊聊自己的感受吧。

a. 数据类型表面形式上宽松

  Go语言提供了短变量声明语法,形式上看就感觉跟Python这类脚本语言一样弱化了数据类型的概念,但同时它确实是个静态编译语言,会有严格的检查机制确保强类型安全性。大家可能会说这同C++的auto自动类型推导没啥区别嘛!其实我觉得也是这样的,简短变量句法也就是个语法糖而已。
  另一方面Go语言的多重赋值确实是一个很好用的特性,可以使函数同时返回多个值,也从语法上让Go有使用error代替exception成为了可能。在做C++开发的时候,我们经常需要使用一个int或bool返回类型表明函数调用是否成功,确保调用处理成功后再通过引用或者指针来获取有意义的数据,这种写法相当的蹩脚,即便后来在开发的时候使用boost::optional部分缓解上述问题,但还是限制在只能返回一个值的情况。相比之下Go语言这种多重赋值的功能,才是真正满足编程习惯的设计。至于程序出错是使用错误标识还是使用异常机制就可谓仁者见仁了,没有绝对的好与坏,不然的话也不会出现很多boost库同时提供跑出异常和返回错误码两种API了,也有可能我个人对异常了解的还不够深刻,在习惯上基本倾向于使用错误码返回,所以Go的错误标识机制还是挺喜欢用的,只不过Go对于某些比较严重的错误直接宕机的做法表示惊讶。
  默认行为下Go语言为各种类型的变量都提供了定义良好的初始化值,防止变量随机初始化给程序带来不确定的行为。当初C语言设计出来的时候,为了性能考虑某些情况下定义的变量是未初始化的,C++也延续了这个传统,不过当前的计算能力下这种省略初始化带来的性能增益可能微乎其微,所以个人对这些语言的新标准中迟迟不提供变量的默认初始化表示比较费解。对于这个问题,目前我写C++的习惯是将所有的变量定义都附加一个统一初始化语法{},以确保变量是良好初始化的。

一个简单的互联网架构样例图

  咋看标题感觉像是一个伪命题,因为在之前我就描述过:架构永远是服务于业务的,不同的业务类型、不同的业务体量、甚至不同的管理层价值观和公司文化都会对最终的架构产生重要影响。但是从另外一方面说,互联网行业是开放、发展迅速同时在技术上同质化较强的行业,所以相当多的公司可能最初都像传统行业一样对工程进行功能确定和架构的详细设计,而是以近似野蛮的方式快速开发和部署业务系统,待到业务量快速上升的时候再借鉴类似行业的解决方案去做演化更新和扩容,因此来说大部分互联网企业的架构还是有章法可循的,只是会根据具体的业务逻辑做一些变更,或者组件集群化,甚至对某些组件进行二次定制开发等,也就是随着业务规模的越庞大、运营的越精细,架构才会演化的越来越复杂。
  最近在看一本书《Designing Data-Intensive Applications》,因为目前只有英文原版所以啃起来还是有点慢。这本书是一个英国人写的,因此书的内容偏重于从理论、宏观概念上对现代Data-Intensive类服务开发面临的挑战进行描述,同时对现在各个企业、开源服务、学术论文等给出的解决方案进行介绍。虽然这本书可能不是那种看了就立马技能提升并在实践中操刀用起来的那种,不过有一定工作经验的人看的过程中能获得很大的共鸣感,可以说是对工作中一些遇到的问题进行了更高层次的一种总结归纳。之前有人说,计算机科班出身和野路子出身的程序员很大的区别就是:前者会从理论慢慢走向实践,而后者会从实践再慢慢回归理论。现在仔细想来,盖莫如此啊……
arch
  上面的这张图总结的是目前我所认识到的互联网系统的常用架构,就当是一个系统架构设计的起点吧,原设计我是使用Draw.io工具进行绘制的,源文件可以从这里下载。可能现在看起来还算十分简单,有些东西可能还不够完善,但希望随着自己知识和经验的积累,其内容也能更加的丰满精细。

本文完!

自己拼凑的一个分布式存储服务

  现代互联网的发展对服务质量的要求越来越高,后台开发中对基础组件和服务的可靠性要求也越来越严格,在这类需求下通过多副本的分布式协议提高服务整体可靠性也越来越被证实为行之有效的解决手段。虽然Lamport早在上个世纪就提出了Paxos算法,但一直被觉得因为难以理解,所以工业上基本也就几个技术实力雄厚的大厂将其实现并应用在生产环境中,不过随着Stanford的Diego Ongaro提出Raft一致性算法后,因为其在Paxos的基础上做出了很多的假设和约束,使得该协议更容易被理解和工程实现,因此近年来基于Raft实现的分布式系统突如百花齐放,使人感觉上高端无比的分布式系统也可以被我们这技术菜鸟们学习、研究甚至实现了。
  之前通读过不少分布式系统的设计和实现论文,其实可以发现处于稳定状态的分布式系统工作原理很简单明了,通常整个论文的绝大部分则是用于解释和证明系统运行过程中各种异常、突发情况下的正确性,以及可以部署在工业级生产环境下一个可靠性强、便于实现和维护的分布式系统的各种细节性问题。其实在仔细研读作者的博士论文之后,同时参照一些开源实现代码,觉得自己实现Raft协议还是没啥问题的,不过对于分布式系统除了实现算法,后续正确性验证还需要更多的工作量去完成。这里我就投了一个巧:使用Diego Ongaro的logcabin这个样例项目工程,剥离出其中Raft协议之后,再将自己的业务代码添加进入,就可以快速得到一个具备分布式能力的服务了,而且我想作者自己操刀实现的Raft协议库应当比绝大多数人的实现都更加完整可靠吧。
  logcabin是基于Raft协议实现的一套驻留于内存、树状层次结构的存储系统,类似于UNIX文件系统中的目录和文件层次结构,通过自带工具可以实现目录和文件的操作管理。为了将这个项目折腾的更加好玩,于是在原作者项目基础上做了一些更改操作,并命名为RaftStore,立志将其实现为一个可靠的静态资源存储服务。当然折腾不是最终目的,自己的初衷就是在折腾中加深对Raft协议以及分布式系统的理解,能够在折腾中有所习得吧。


再学设计模式

  这篇打算从Uncle Bob的大作中再学习整理一下设计模式,主要是感觉这本书相比起GoF的《设计模式》更容易入口一些,书中会使用简短精悍的例子告诉我们带有腐臭味的设计有什么样的毛病,然后通过之前的SOLID原则怎样演化改进这些问题设计。之前自己总是贪心着想把23种设计模式一下子学全,但是这种囫囵吞枣式地大满贯感觉收效甚微,毕竟设计模式具有极强的实践操作性,是需要不断地在学习中思考、在使用中感受的东西,所以与其贪多还不如找几个实用的模式研究透了更实在。任何时候我们都需要记住:我们需要的不是最初承诺的豪言壮语,而是在今后的行动中不忘初心,践行当初自己一点一滴的承诺。
  其实现在再仔细审视一下这些设计模式,也会发现一些有趣的现象:不少模式在项目中感觉也不会太多用到了,尤其现在微服务化浪潮下每个服务都设计的尽可能轻巧简洁,每一个服务的整体复杂度与上个世纪的巨无霸服务不可同日而语了;还有一些在实践上已经被用烂了,只是一直没有意识到它们的名字而已,比如FACADE、PROXY;此外设计模式提出来的时间也比较久了,而现代C++增加了许多新的特性,所以一些设计模式真的需要重新REVISE一下,比如最常见的OBSERVER有更完美的替代方案。

1. TEMPLATE METHOD

  根据DIP原则,我们希望通用的算法不要依赖于具体的实现,而TEMPLATE METHOD和STRATEGY都可以用来分离算法和具体上下文的耦合,使得通用算法和具体实现都依赖于抽象。不同的是TEMPLATE METHOD使用继承机制来解决该问题,而STRATEGY则是使用委托机制。
  在TEMPLATE METHOD中,可以将程序的通用结构分离出来,把他们放到一个抽象基类的普通实方法中,然后该实方法可以按应需调用抽象方法,所有的实现细节都由这些抽象方法在派生类来实现。通常上面的实方法是public的,而虚接口则是private的,成员方法的访问控制和虚特性是正交的,private virtual接口也可以在派生类中被override。

InnoDB存储引擎之数据库锁

  锁的目的就是为了支持对共享资源的并发访问,并提供数据的完整性和一致性,InnoDB具有两种标准的行级锁:共享锁(S Lock)和排他锁(X Lock)。
  不同的数据库,甚至MySQL不同的存储引擎,对锁的实现都是完全不同的。MyISAM是表级锁设计,在并发修改的时候性能较差;SQL Server的老版本使用页锁,新版本开始支持乐观并发和悲观并发,而且在乐观并发下开始支持行级锁了;InnoDB是通过在每个页中采用位图的形式进行锁管理的,所以InnoDB中锁是一种很轻量级的实现,即使同时锁住再多的记录其开销几乎也不会发生什么变化。

1. 一致性非锁定读

  一致性非锁定读是指InnoDB通过行多版本控制(MVCC)的方式来读取当前执行时间下数据库中的行数据,比如读取的行正进行DELETE或UPDATE操作,此时的读操作不会等待该行上X锁释放,而是会去读取行的某个快照数据。快照数据是通过undo段来完成的,undo数据是事务中用来回滚时候使用的,所以只要执行事务产生就肯定会产生undo数据,因而使用快照数据是没有额外开销的,同时快照是不会被事务修改的,所以访问这些数据也不用进行加锁操作。
  InnoDB对每一行记录可能会有多个版本的快照,因此称之为行多版本技术,这种技术下的并发控制成为多版本并发控制(MVCC)。在READ COMMITTED和REPEATABLE READ事务隔离级别下,InnoDB会使用一致性非锁定读,不过不同的是在READ COMMITTED事务隔离级别下,对快照数据一致性非锁定读总是读取被锁定数据的最新一份快照数据;而在REPEATABLE READ事务隔离级别下,对于快照数据非锁定一致性读总是读取事务开始时候行数据版本。

InnoDB存储引擎之索引和优化

  数据库优化可以说是后台开发中永恒的话题,数据库的性能通常是整个服务吞吐量的瓶颈之所在。

1. 索引概述

  InnoDB中的表都是按照主键顺序组织存放的,这种组织方式称之为索引组织表,对比于MyISAM的表组织方式。在InnoDB中每张表都必须有一个主键,如果在创建表的时候没有显式定义主键,则InnoDB首先会判断表中是否有非空的唯一索引,如果有则将该列作为主键;否则InnoDB会自动创建一个6字节大小的指针作为主键。除主键之外,InnoDB还可以有辅助索引,而辅助索引页中仅仅存放键值和指向数据页的偏移量,而不像主键数据页存储的是一个完整的行记录。
  InnoDB存储引擎中,所有的数据都被逻辑地存放在一个表空间中,表空间又被分为段(Segment)、区(Extent)、页(Page)组成,其中段由存储引擎自动管理,区的大小固定为1M,然后默认情况下页的大小为16KB,也就是一个区总共有64个连续的页组成。不过在MySQL5.6开始,页的大小可以设置为4K、8K了,设置成4K除了可以提高磁盘的利用率之外,对于现代SSD硬盘将更加合适,不过这中更新比较的麻烦,需要将输入导出后再重新导入,一般的备份恢复工具都是原样复制数据,没有办法支持变更页大小。
  默认的B+树索引其查找次数(效率)取决于B+树的高度,生产环境下一般树高为3~4层,即查询一条记录需要经过3~4个索引页,而且B+树索引并不能找到一个给定键值的具体行,其只能根据键和索引找到数据行所在的页,然后数据库把对应的页读取到内存,再在内存中执行查找,并最后得到需要查询的数据。InnoDB还会监控对表上各索引页的查询操作,如果观察到通过建立hash索引可以带来速度提升,则会根据访问频率和访问模式自动为部分热点页建立hash索引,这个过程称之为自适应哈希索引,而且该过程是人为无法干预、存储引擎自动实现的。
  使用索引的一大禁忌是不要在引用索引列的时候使用函数,比如max(id)、id+3>5等,或者隐式的数据类型转换操作,这样会导致索引失效导致全扫描。

InnoDB存储引擎之事务和可靠性

  最近读了姜承尧的《MySQL技术内幕 InnoDB存储引擎》,感觉此书乃是为数不多的国产数据库经典教材。虽然说到数据库大家首推《高性能MySQL》,不过感觉这本书的知识体系被梳理的更加系统条理一些,而且作者的数据库知识渊博、功力深厚,读完确实让人有一种欲罢不能的快感。接下来的几篇文章,将是结合这本书对InnoDB存储引擎之事务、锁、索引等知识进行相关的梳理,虽然不会去做存储引擎,但是InnoDB是事实上被用的最多的事务级存储引擎,了解一下知其所以然才能对网上那些天花乱醉的教程指南做到心中有数,才能更好的去使用MySQL,而不是把MySQL当做一个黑盒一样把玩。
  总体来说,感觉MySQL从5.5开始,在可靠性、性能、维护性这些方面做了相当大的改进,虽然人家说新版本的数据库资源消耗比之前的版本要增加很多,但是在硬件技术突飞猛进的今天这些消耗都基本可以被忽略的。书中的内容在后面翻阅资料发现,一些部分和MySQL手册雷同,But who cares … ^_^

1. 事务概述

  数据库事务的意义在于保证把数据库从一种一致状态迁移到另外一种一致状态,InnoDB中的事务是完全支持A(Atomicity)、C(Consistency)、I(Isolation)、D(Durability)特性的:事务隔离性采用锁机制来实现,原子性和持久性通过数据库的Redo log来实现,Undo log则用来保证事务的一致性。
  Redo log的内容是页物理级别的日志,属于InnoDB存储引擎专用的日志格式,记录的内容形如在某个数据页的偏移某处的值为多少,通过这种日志格式可以满足恢复操作幂等性要求。Redo log基本都是顺序写的,在数据库正常运行的时候不需要对Redo log文件进行读取操作,只有在数据库启动或者特殊的情况下才需要用到该记录来执行重做恢复操作。
  Undo log可以用来使行记录回滚到某个特定的版本,通常是行记录的逻辑日志,除了用来执行事务回滚外,还可以用来支持MVCC实现的非锁定一致性读功能,而非锁定一致性读是InnoDB高性能读取的秘密武器。Undo log在数据库运行时候是会进行大量随机性读写操作的。
  MySQL5.6提供了只读事务优化的机制,这种情况下对于使用START TRANSACTION READ ONLY或者在AutoCommit激活时候只进行SELECT查询的事务,InnoDB存储引擎会将其识别为只读事务,不会派发事务ID,也不会分配变更数据所需的内存结构体,整体优化后可以得到更快的执行性能。

SQL解析和执行过程

1. 简介

  MySQL整体的架构是分层设计的,各个模块都有自己独立的功能。通常,在执行一个查询的时候,会大致经过三个阶段的处理,还有就是在MySQL中有一个查询缓存管理,如果在接受到请求的开始能够在查询缓存中直接找到对应的查询,就可以直接返回查询缓存中的结果集,而不用执行以下的查询解析、优化和执行这些步骤了。
  (1) SQL解析操作
  从用户接受SQL语句,切分语句并进行词法语法分析,最后生成MySQL可以理解的解析树这种内部数据结构,该过程是SQL解析器负责处理的。
  (2) 进行优化处理并确定最终执行计划
  这个阶段通过优化器的工作,会对上面生成的解析树作出如下处理:a.删除不必要的条件,将复合运算简单化,甚至必要时候重写某些查询操作;b.若存在多表连接,则确定读表的顺序;c.根据用于各数据表的条件和索引统计信息,确定要使用的索引;d.将获取的记录放入临时表,确定是否需要再次加工;
  这个过程用户也可以使用提示器来影响执行计划的决策过程。同时,虽然优化器并不关心表使用的是什么存储引擎,但是这个过程优化器也会请求存储引擎提供数据表容量、某个操作的开销等信息,以及数据表的统计信息等,来帮助优化决策。
  (3) 执行查询计划,获取数据
  依据最终执行计划向存储引擎请求读取记录,此时MySQL可能会将获得的记录执行连接、排序等处理。
  这边祭上MySQL框架图镇楼!
mysql

MySQL查询语句EXPLAIN解析

  从MySQL服务器获取正确结果可能会有不止一种方法,但是这些方法所花费的代价很可能是不同的,在MySQL中为了找出这种花费最少、效率最高的方法肯定是大家最希望的。相比于传统基于规则的优化,MySQL和现代绝大多数数据库都是实现基于代价的优化方式,他们在处理查询的时候会创建多种可用方法,然后参考各种处理的消耗信息以及目标数据库表的统计分布信息,计算各个执行计划的代价,从中选择代价最小的计划去执行,所以找到最优的执行计划就是优化器所要负责的功能。
  在以前的版本中,数据库相关统计信息是由各存储引擎在内存中进行管理的,由于这些信息是动态的,很容易造成主从服务器对相同的查询选择不用执行计划的情况。从MySQL5.6开始,InnoDB采用表的形式管理相关统计信息,这样就可以将统计信息同步给从服务器,防止主、从服务器的执行计划出现差异。
  通过EXPLAIN命令可以查看SQL语句的执行计划,后面直接跟SELECT语句即可,如果想看DML语句的执行计划,则可以先将其转换成带WHERE条件的SELECT语句,再使用EXPLAIN分析即可,然后执行完EXPLAIN后会返回一个包含执行计划信息的相关表格,不过据称新版本的MySQL已经提供了直接通过EXPLAIN分析INSERT、UPDATE等操作的执行计划了。

1. id (query id)

  如果一个SELECT查询包含有多个子查询语句,则EXPLAIN返回的信息表会将每个子查询都赋予不同的ID列并显示出来,同时还包括子查询需要创建临时表的情况。如果一个SELECT语句需要连接多个数据表,执行计划把连接的表都进行显示,并且把他们都赋予相同的ID列值,即ID值不会增加。

1
2
SELECT e.emp_no, e.first_name, s.from_date, s.salary FROM employees e, salaries s
WHERE e.emp_no = s.emp_no LIMIT 10;

  信息表越显示在上方的结果,也就是ID值越小的结果,表示越为查询的外部、或越先访问的数据表;相反越显示在下方的记录,即ID值越大的结果,则为越是查询的内部、或越是后访问的数据表。

软件设计模式中应该遵守的SOLID原则

  《敏捷软件开发:原则、模式与实践》和可能又是一本极易被埋没的面向对象软件设计的经典之作,本人也是在闲暇时候逛论坛偶然看见有人倾力推荐后才主动了解到它的,而之所以说容易被埋没,是因为这本书的名字很容易让别人感觉是一本专门讲授敏捷开发的知识,而大家心里其实都很清楚:敏捷开发在中国这种大量民工级别的程序员参与开发的形式下是很难实施的。曾记得在2012年我在公司实习的时候,公司总部就部署各个分舵组织学习Agile和看板这些开发和管理技术,不过形式上走过场之后就没有然后了,根本无法实施。
  言归正题,这本书还是说了不少敏捷开发、测试驱动开发的相关知识和案例演示,但目前为止最吸引我的是里面罗列的面向对象软件设计原则和各种设计模式的灵活运用,总体来说比GoF的经典《设计模式》要容易消化接收一些,书中的样例很多都是使用C++实现的,尽管也有一些是使用Java实现的,不过这两种语言基本同根,所以理解起来也不是特别的费劲。总体评价是:这是一本很值得慢慢研磨品味的书。
  下面的内容是介绍面向对象软件设计中所需要遵守的五个原则,他们有时候也被统称为S.O.L.I.D原则,分别是:单一功能原则、开闭原则、里氏替换原则、接口隔离原则和依赖翻转原则。如果说设计模式是软件开发中小巧灵活、功能实用的瑞士军刀,那么这里的软件设计原则就是军刀的灵魂。而且Uncle Bob也在书中说到,经验丰富的开发者遇到一个场景的时候很可能立马熟悉的设计模式就复现在脑海中了,其实有时候也不必一定要寻找某个设计模式来套当前的项目,而是朝着软件的需求方向,按照面向对象软件设计的原则去不断重构、演化项目的代码,那么最后的可能会发现模块自然而然地就会演化接近某一个设计模式了,毕竟那些设计模式也是从实践中提炼出来的,实践开发中也应该回归设计模式。

一、单一职责原则 (Single Responsibility Principle, SRP)

  就一个类而言应该仅有一个引起它变化的原因。
  因为类的职责会随着需求的变化而发生变化,同时每一个职责都是变化的一个轴线,如果一个类承担的职责过多,那么这些职责就会被耦合在一起,一个职责的变化可能会削弱甚至抑制这个类完成其他职责的能力。
  在SRP中将“引起变化的原因”定义为职责。在现实中,我们通常习惯于以组的形式(而不是变化的原因)去考虑职责进行归类,比如通常会将Modem的所有接口组合起来放在一起,因为他们都和Modem相关,不过这个Modem具有连接管理、数据通信两个职责,后续会因为两者中的任意一个发生变化都,则所有依赖这两个职责中的任何一个都迫使我们需要进行代码审查、测试验证、部署等操作,在用户的角度看来这两个职责被耦合在了一起。不过该原则需要预先推断出变化的情况,如果不会发生变化,或者应用程序的变化总是导致两个职责同时发生变化,那么就没有必要分离他们了。
  还有一种违反SRP的是经常变动的职责和不会频繁(甚至不会)变动的模块混合在一起的情形,比如业务模块和持久化子系统就是典型的变化频率和原因不相同的模块,这个时候就推荐使用FACADE或者PROXY模式重构分离两个职责。