IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

我自己研究开源项目源代码的两个重要习惯

游啊游,淡定、淡定 (微薄@zhh-2009) 2012-11-02 13:11:33 累计浏览 5,970 次
本机暂存

   这两个习惯应该很大众化很普通,就是:

   1. 写代码流程分析文档

   2. 写不同场景的测试用例

   不过我做得比较细:

1. 写代码流程分析文档

   把一些难理解的、重要的代码流程写成分析文档,

   按方法调用顺序排好,通常第一次分析时不可能完全理解透的,无法理解透的可以加TODO,

   也有可能会理解错的,这都是没关系的,

   代码分析通常看一遍是不能全理解透的,需要反复几次,一步步把分析文档完善。

   最好是把分析文档提交到svn中,免得丢失,还可以从这些历史记录中观察自己的整个分析过程。

   如果你好几个月不关注某个开源项目的源代码了,当再次需要研究代码时,

   分析文档是非常有用的,对照代码和分析文档可以让你快速恢复到当初对代码的理解水平,

   如果没有分析文档,通常又会浪费大量时间再重做一次。

   以下两个例子就是我在分析HBase的HMaster和HRegionServer的启动流程时写的原始分析文档片断,

   会有一些TODO或错误或过时的地方,因为这是个在研究过程中不断完善的文档,

   并不是最终的,也没有终极文档,因为开源项目的代码一直在变动。

1 HMaster构造函数

  HRegionServer构造函数, HRegionServer的RPC端口默认是60020,master的RPC端口默认是60000
  HRegionServer的Jetty(InfoServer)端口默认是60030,master的Jetty(InfoServer)端口默认是60010

  1.1 获取当前运行HMaster的机器的地址
  1.2 生成HBaseServer对象用于接收RPC请求,并启动HBaseServer的相关线程
  1.3 生成ZooKeeperWatcher对象
      在构造函数中生成这些持久结点: /hbase, /hbase/unassigned, /hbase/rs, /hbase/table, /hbase/splitlog

	ZooKeeperWatcher管理下面10个结点:

	baseZNode              "/hbase"
	rootServerZNode        "/hbase/root-region-server"
	rsZNode                "/hbase/rs"
	drainingZNode          "/hbase/draining"
	masterAddressZNode     "/hbase/master"
	clusterStateZNode      "/hbase/shutdown"
	assignmentZNode        "/hbase/unassigned"
	tableZNode             "/hbase/table"
	clusterIdZNode         "/hbase/hbaseid"
	splitLogZNode          "/hbase/splitlog"
	schemaZNode            "/hbase/schema"

	这6个结点在ZooKeeperWatcher构造函数中生成
	baseZNode              "/hbase"
	rsZNode                "/hbase/rs"
	drainingZNode          "/hbase/draining"
	assignmentZNode        "/hbase/unassigned"
	tableZNode             "/hbase/table"
	splitLogZNode          "/hbase/splitlog"
	schemaZNode            "/hbase/schema"

	这4个在不同地方生成
	rootServerZNode        "/hbase/root-region-server"
	masterAddressZNode     "/hbase/master" //在HMaster中建立,并且是一个短暂结点,结点的值是HMaster的ServerName
	                                       //见org.apache.hadoop.hbase.master.ActiveMasterManager.blockUntilBecomingActiveMaster
	clusterStateZNode      "/hbase/shutdown"
	clusterIdZNode         "/hbase/hbaseid" //在HMaster.finishInitialization方法中调用ClusterId.setClusterId建立,结点值是UUID

  1.4 生成MasterMetrics对象

2 HMaster.run
  
  2.1
  生成ActiveMasterManager对象,如果此HMaster作为一个备份(backup),
  那么需要等到集群中有Active Master时才往下调用blockUntilBecomingActiveMaster,
  并且调用blockUntilBecomingActiveMaster也会阻塞,直到它变成ActiveMaster。

  与此同时,在blockUntilBecomingActiveMaster中会创建短暂结点"/hbase/master",
  此节点的值是HMaster的版本化ServerName(也就是version+ServerName),
  此结点用于协调region server的启动,只有"/hbase/master"创建好后,region server才能往下进行。
 
  2.2
  调用HMaster.finishInitialization

    2.2.1
	生成MasterFileSystem对象
	建立由hbase-site.xml的hbase.rootdir属性指定的目录(如:file:/E:/hbase-0.90.4/tmp/hbase-db/hbase)
	调用FSUtils.setVersion在hbase.rootdir目录中建立一个hbase.version文件,并写入版本号(HConstants.FILE_SYSTEM_VERSION=7)
	判断-ROOT-分区是否存在,不存在则调用MasterFileSystem.bootstrap来创新-ROOT-和.META.

	最后创建file:/E:/hbase-0.90.4/tmp/hbase-db/hbase/.oldlogs目录

	2.2.2
	如果持久结点"/hbase/hbaseid"不存在则创建它,否则不创建,同时每次master启动时都会把此节点的值设为hbase.id文件中的值

	2.2.3
	生成ExecutorService (TODO)

	2.2.4
	生成ServerManager (TODO)

	2.2.5
	initializeZKBasedSystemTrackers

	  2.2.5.1
	  生成CatalogTracker, 它包含两个ZooKeeperNodeTracker,分别是RootRegionTracker和MetaNodeTracker,
	  对应/hbase/root-region-server和/hbase/unassigned/1028785192这两个结点(1028785192是.META.的分区名)
	  如果之前从未启动过hbase,那么在start CatalogTracker时这两个结点不存在。
	  /hbase/root-region-server是一个持久结点,在RootLocationEditor中建立

	  2.2.5.2
	  生成AssignmentManager 

	  2.2.5.3
	  生成 LoadBalancer

	  2.2.5.4
	  生成 RegionServerTracker: 监控"/hbase/rs"结点

	  2.2.5.5
	  生成 DrainingServerTracker: 监控"/hbase/draining"结点

	  2.2.5.6
	  生成 ClusterStatusTracker,通过它的setClusterUp方法创建持久结点"/hbase/shutdown",结点值是当前时间,
	  如果结点已存在(master可能未正常关闭),那么此结点的值不更新。

	
	2.2.6
	生成 MasterCoprocessorHost

	2.2.7
	startServiceThreads()

	启动服务线程
	(MASTER_OPEN_REGION、MASTER_CLOSE_REGION、MASTER_SERVER_OPERATIONS、MASTER_META_SERVER_OPERATIONS、MASTER_TABLE_OPERATIONS
	这几个只是生成Executor,并未正式启动, 正式启动的有LogCleaner,和基于Jetty的InfoServer(端口号默认是60010))
	
	2.2.8
	等待RegionServer注册

	2.2.9
	splitLogAfterStartup (TODO)

	2.2.10
	assignRootAndMeta
		
		2.2.10.1
		processRegionInTransitionAndBlockUntilAssigned
		先看一下分区正在转换状态当中,如果处于转换状态当中则先处理相关的状态,并等待体处理结束后再往下进行。

		2.2.10.2
		verifyRootRegionLocation

		2.2.10.3
		getRootLocation

		2.2.10.4.A
		expireIfOnline

		2.2.10.4.B
		assignRoot

		先删掉"/hbase/root-region-server",不管它存不存在,KeeperException.NoNodeException被忽略了

		写入EventType.M_ZK_REGION_OFFLINE、当前时间戳、跟分区名(-ROOT-,,0)、master的版本化ServerName
		到/hbase/unassigned/70236052, payload为null,所以不写入

		RegionServer修改/hbase/unassigned/70236052的值,
		写入EventType.RS_ZK_REGION_OPENING、当前时间戳、跟分区名(-ROOT-,,0)、RegionServer的版本化ServerName

	
	2.2.11
	MetaMigrationRemovingHTD.updateMetaWithNewHRI

	2.2.12
	assignmentManager.joinCluster()
	把meta表中的分区读出来,然后分配到Region Server,
	meta表只有一个列族:info,存入meta的行有三列: 
	regioninfo、server、serverstartcode,
	其中regioninfo对应HRegionInfo,
	server对应ServerName的host和port(例如:myhost:60070)
	serverstartcode对应ServerName的startcode(一般是时间戳)。

		2.2.12.1
		rebuildUserRegions()
			
			2.2.12.1.1
			调用MetaReader.fullScan 从meta表中取出所有的分区,得到一个List<Result>,
			调用MetaReader.parseCatalogResult,解析每个result得到Pair<HRegionInfo, ServerName>,
			其中HRegionInfo由regioninfo列的值反序列化得来,ServerName由server、serverstartcode两列的值反序列化后组合而成。

		2.2.12.2
		processDeadServersAndRegionsInTransition
HRegionServer在reportForDuty()中向HMaster报告自己启起来后,
接着调用handleReportForDutyResponse把自己挂到zookeeper的/hbase/rs节点下(是个短暂节点),

之后HMaster就可以从/hbase/rs节点中得知有多少个HRegionServer了。


0 HRegionServer构造函数

HRegionServer构造函数, HRegionServer的RPC端口默认是60020,master的RPC端口默认是60000
HRegionServer的Jetty(InfoServer)端口默认是60030,master的Jetty(InfoServer)端口默认是60010

1 run

	1.1
	preRegistrationInitialization

	1.1.1
		initializeZooKeeper() 此方法不会创建任何节点

		. 生成ZooKeeperWatcher
		. 生成MasterAddressTracker 并等到"/hbase/master"节点有数据为止
		. 生成ClusterStatusTracker 并等到"/hbase/shutdown"节点有数据为止
		. 生成CatalogTracker 不做任何等待

	1.1.2
		initializeThreads()

		. 生成 MemStoreFlusher
		. 生成 CompactSplitThread
		. 生成 CompactionChecker
		. 生成 Leases
	
		1.1.3
	参数hbase.regionserver.nbreservationblocks默认为4,默认会预留20M(每个5M,20M = 4*5M)的内存防止OOM

	1.2
	reportForDuty

		1.2.1 getMaster()
			取出"/hbase/master"节点中的数据,构造一个master的ServerName,然后基于此生成一个HMasterRegionInterface接口的代理,
			此代理用于调用master的方法

		1.2.2 regionServerStartup

			1.2.3.1
			用rs的端口(默认是60020)、startcode(rs构造函数被调用时的时间戳),now(当前时间)这三个参数调用master的regionServerStartup.

		1.2.3 handleReportForDutyResponse
			1.2.3.2
			regionServerStartup会返回来一个MapWritable,
			这个MapWritable有三个值:
			"hbase.regionserver.hostname.seen.by.master" = master为rs重新定义的hostname(通常跟rs的InetSocketAddress.getHostName一样)
															 rs会用它重新得到serverNameFromMasterPOV
			"fs.default.name" = "file:///"
			"hbase.rootdir"	= "file:///E:/hbase/tmp"

			这三个key的值会覆盖rs原有的conf

			1.2.3.3
			查看conf中是否有"mapred.task.id",没有就自动设一个(格式: "hb_rs_"+serverNameFromMasterPOV)
			例如: hb_rs_myhost,60050,1323525314060

			1.2.3.4
			createMyEphemeralNode

			在zk中建立 短暂节点"/hbase/rs/myhost,60050,1323525314060",
			也就是把当前rs的serverNameFromMasterPOV(为null的话用rs的InetSocketAddress、port、startcode构建新的ServerName)
			放到/hbase/rs节点下,"/hbase/rs/myhost,60050,1323525314060"节点没有数据。

			1.2.3.5
			设置conf中的"fs.defaultFS"为"hbase.rootdir"的值(conf之前可能没有"fs.defaultFS"属性)

			1.2.3.6
			把"hbase.rootdir"的值保存到rootDir字段,
			生成一个只读的FSTableDescriptors

			1.2.3.7
			setupWALAndReplication

				1.2.3.7.1
				得到oldLogDir = "hbase.rootdir"的值+".oldlogs",例如: file:/E:/hbase/tmp/.oldlogs
				得到logdir		= "hbase.rootdir"的值+".logs"+serverNameFromMasterPOV,
											例如: file:/E:/hbase/tmp/.logs/myhost,60050,1323525314060

				(备注: 假设"hbase.rootdir" = "file:/E:/hbase/tmp/")

				如果logdir已存在,抛出RegionServerRunningException


				1.2.3.7.2 (TODO)
				判断是否使用Replication,默认为false,可通过"hbase.replication"参数设置

				1.2.3.7.3
				instantiateHLog
					
					1.2.3.7.3.1
					getWALActionListeners

					只有下面两个数实现了org.apache.hadoop.hbase.regionserver.wal.WALActionsListener
					org.apache.hadoop.hbase.regionserver.LogRoller
					org.apache.hadoop.hbase.replication.regionserver.Replication

					LogRoller会加入WALActionsListener列表中
					"hbase.replication"参数的值是true时,Replication也被加入


					1.2.3.7.3.2 (TODO)
					生成一个新的HLog
					在他的构造函数中会建立oldLogDir和logdir两个目录,
					prefix字段的值是serverNameFromMasterPOV经过URLEncoder.encode(prefix, "UTF8")后的值,
					然后在logdir目录中生成一个新的日志文件,
					日志文件名是prefix+当前时间戳,比如: myhost%2C60050%2C1323525314060.1323527965276

			1.2.3.8
			生成RegionServerMetrics

			1.2.3.9
			startServiceThreads

			RS_OPEN_REGION RS_OPEN_ROOT RS_OPEN_META
			RS_CLOSE_REGION RS_CLOSE_ROOT RS_CLOSE_META
			这6个并未正真启动,只是生成Executor。

			启动的线程有:
			LogRoller
			MemStoreFlusher
			CompactionChecker
			Leases
			Jetty InfoServer (可通过"/rs-status"和"/dump"这两个url来访问rs的相关信息)
			Replication(待确定 TODO)
			SplitLogWorker

		1.2.4 周期性(msgInterval默认3妙)调用doMetrics,tryRegionServerReport

			1.2.4.1
			isHealthy健康检查,只要Leases、MemStoreFlusher、LogRoller、CompactionChecker有一个线程退出,rs就停止
			
			1.2.4.2
			doMetrics

			1.2.4.3
			tryRegionServerReport
			向master汇报rs的负载HServerLoad

2. 写不同场景的测试用例

   例子一般是验证某个方法中的代码的各种分枝,

   还有些是验证几个类之间的调用关系的,

   这种测试用例有可能很多,最好还是自己写一写,

   去看开源项目自带的一般也没那么详细而且你还要了解它的测试代码,

   还不如自己写来得快,通常这些测试例子不是一次性完成的,

   一般是分析方档写到哪,你就可以写些例子来测试一下。

   下面这个例子就是我在分析H2数据库在解析Insert SQL语法时写的例子,

   几乎含盖了所有的场景,同时顺便测试记录是怎么存入表中的,会触发哪些触发器。

   (在Eclipse中可以按CTRL+/来注释掉一些已经测过的,例子通常是按开源项目的代码运行流程的先后顺序来写的)

package my.test.sql;

import java.sql.Connection;
import java.sql.SQLException;

import my.test.TestBase;

//找断点条件
//table.getName().equalsIgnoreCase("InsertTest");
public class InsertTest extends TestBase {
	public static void main(String[] args) throws Exception {
		new InsertTest().start();
	}

	public static class MyInsertTrigger implements org.h2.api.Trigger {

		@Override
		public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type)
				throws SQLException {
			System.out.println("schemaName=" + schemaName + " tableName=" + tableName);

		}

		@Override
		public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException {
			System.out.println("oldRow=" + oldRow + " newRow=" + newRow);
		}

		@Override
		public void close() throws SQLException {
			System.out.println("my.test.sql.InsertTest.MyInsertTrigger.close()");
		}

		@Override
		public void remove() throws SQLException {
			System.out.println("my.test.sql.InsertTest.MyInsertTrigger.remove()");
		}

	}

	//测试org.h2.command.Parser.parseInsert()和org.h2.command.dml.Insert
	@Override
	public void startInternal() throws Exception {
		conn.setAutoCommit(false);

		stmt.executeUpdate("DROP TABLE IF EXISTS InsertTest");
		//stmt.executeUpdate("CREATE TABLE IF NOT EXISTS InsertTest(id int not null, name varchar(500) not null)");
		stmt.executeUpdate("CREATE TABLE IF NOT EXISTS InsertTest(id int, name varchar(500))");

		stmt.executeUpdate("CREATE TRIGGER IF NOT EXISTS TriggerInsertTest BEFORE INSERT ON InsertTest "
				//+ "FOR EACH ROW CALL \\"my.test.sql.InsertTest$MyInsertTrigger\\"");
				+ "CALL \\"my.test.sql.InsertTest$MyInsertTrigger\\"");

		stmt.executeUpdate("DROP TABLE IF EXISTS tmpSelectTest");
		stmt.executeUpdate("CREATE TABLE IF NOT EXISTS tmpSelectTest(id int, name varchar(500))");
		stmt.executeUpdate("INSERT INTO tmpSelectTest VALUES(DEFAULT, DEFAULT),(10, \'a\'),(20, \'b\')");

		//从另一表查数据,然后插入此表
		stmt.executeUpdate("INSERT INTO InsertTest(SELECT * FROM tmpSelectTest)");
		stmt.executeUpdate("INSERT INTO InsertTest(FROM tmpSelectTest SELECT *)"); //FROM开头先也是支持的

		//DEFAULT VALUES这种语法不适合用于not null字段
		stmt.executeUpdate("INSERT INTO InsertTest DIRECT SORTED DEFAULT VALUES");
		//DEFAULT VALUES这种语法不能在表名之后又指定字段列表
		//stmt.executeUpdate("INSERT INTO InsertTest(name) DIRECT SORTED DEFAULT VALUES");

		//这种语法可查入多条记录
		//null null
		//10 a
		//20 b
		//stmt.executeUpdate("INSERT INTO InsertTest VALUES(DEFAULT, DEFAULT),(10, \'a\'),(20, \'b\')");

		//SET语法不能在表名之后又指定字段列表
		//stmt.executeUpdate("INSERT INTO InsertTest(name) SET name=\'xyz\')");
		//虽然在语法上可以重复相同的字段,本意是想插入多条记录,但是实际上只有一条,就是最后一个id和name
		//stmt.executeUpdate("INSERT INTO InsertTest SET id=DEFAULT, name=DEFAULT, id=10, name=\'a\', id=20, name=\'b\'");

		//列必须一样多,否则:
		//Exception in thread "main" org.h2.jdbc.JdbcSQLException: Column count does not match; SQL statement:
		//INSERT INTO InsertTest(name) (SELECT * FROM tmpSelectTest) [21002-169]
		//stmt.executeUpdate("INSERT INTO InsertTest(name) (SELECT * FROM tmpSelectTest)");
		//stmt.executeUpdate("INSERT INTO InsertTest(name) (FROM tmpSelectTest SELECT *)"); //FROM开头先也是支持的

		stmt.executeUpdate("INSERT INTO InsertTest(name) (SELECT name FROM tmpSelectTest)");
		stmt.executeUpdate("INSERT INTO InsertTest(name) (FROM tmpSelectTest SELECT name)"); //FROM开头先也是支持的

		//SELECT语句不带括号也是允许的
		stmt.executeUpdate("INSERT INTO InsertTest(name) SELECT name FROM tmpSelectTest");
		stmt.executeUpdate("INSERT INTO InsertTest(name) FROM tmpSelectTest SELECT name"); //FROM开头先也是支持的

		stmt.executeUpdate("INSERT INTO InsertTest(name) DIRECT FROM tmpSelectTest SELECT name"); //FROM开头先也是支持的

		ps = conn.prepareStatement("INSERT INTO InsertTest(id, name) VALUES(?, ?)");
		ps.setInt(1, 30);
		ps.setString(2, "c");
		ps.executeUpdate();

		stmt.executeQuery("EXPLAIN INSERT INTO InsertTest(name) DIRECT FROM tmpSelectTest SELECT name");

		sql = "select id,name from InsertTest";
		rs = stmt.executeQuery(sql);
		while (rs.next()) {
			System.out.println(rs.getString(1) + " " + rs.getString(2));
		}

		conn.commit();
		//conn.rollback();
	}
}

同分类推荐文章

  1. 科技爱好者周刊(第 401 期):如何赚到10亿美元 (2026-06-26 08:05:38)
  2. 如何做决策 - 从 Go 的一个 issue 说起 (2026-06-26 08:00:00)
  3. Seven Player:Windows上播放115网盘视频的增强工具 (2026-06-09 00:06:47)

查看更多 开发者 文章 →

建议继续学习

  1. Git常用命令备忘 (累计阅读 54,693)
  2. Git log diff config高级进阶 (累计阅读 24,842)
  3. Git subtree 要不要使用 –squash 参数 (累计阅读 23,395)
  4. 我的git笔记 (累计阅读 20,259)
  5. 公司倒了,请让领导先走 (累计阅读 13,407)
  6. 别为大公司拼命(译文) (累计阅读 10,295)
  7. Zend Studio集成Git使用 (累计阅读 8,978)
  8. 学你妹的计算机! (累计阅读 8,135)
  9. 个人开公司的流程,以后用得着 (累计阅读 7,921)
  10. 存储基础知识之——硬盘接口简述 (累计阅读 7,545)