"phaul热迁移过程总结"

  "docker容器热迁移实现阶段一"

Posted by Xu on July 17, 2017

P.Haul热迁移实现总结

p.haul是一个以criu为工具实现进程热迁移及,docker容器外部迁移的项目,所谓docker容器外部迁移就是当容器从源主机迁移到目的机后,目的机上的docker无法对该容器发出控制命令,这是由于传统的criu在目的机上恢复会默认以操作系统的init初始化进程为父进程来恢复所迁移的进程树,然而在源主机上的docker daemon进程和容器init初始化进程为父子关系,所以可以通过管道通信发送控制消息,但当这层父子关系不能在目的机上恢复之后,daemon与容器之间的通信方式就被破坏掉了,所以目的机上的docker再也无法通过docker start,docker stop,docker attach等来控制容器的生命周期,但是一些不依赖于进程通信的命令如docker ps -a等还可以起作用。然而目前docker已经可以原生支持对docker容器的checkpoint和restore,所以目的机上的docker daemon和容器之间的父子关系是可以恢复的,所以在p.haul项目的基础上通过使用docker checkpoint/restore理论上是可以实现对docker容器的热迁移的,而我的工作就在于对p.haul的实现原理的研究做适当的修改来实现对docker容器的热迁移

由于p.haul项目是用python语言完成的,所以在研究p.haul项目的同时我们可以熟悉python语言的使用

phaul 首先我们了解到phaul对docker容器外部迁移的命令为:

./p.haul-wrap client 192.168.11.106 docker d78

./p.haul-wrap为python脚本,client为子命令,192.168.11.106为目的端,docker为迁移目标类型,d78为容器id

parser = argparse.ArgumentParser("Process HAULer wrap")
subparsers = parser.add_subparsers(title="Subcommands")
//以上是python语言的argparse.ArgumentParser模块处理参数的实例,第一步先得到一个参数解析器,第二步是为该参数解析器添加一个子命令解析器

//Initialize client mode arguments parser
client_parser = subparsers.add_parser("client", help="Client mode")
client_parser.set_defaults(func=run_phaul_client)
client_parser.add_argument("to", help="IP where to haul")
client_parser.add_argument("--port", help="Port where to haul", type=int,
	default=default_rpc_port)
client_parser.add_argument("--path", help="Path to p.haul script",
	default=os.path.join(os.path.dirname(__file__), "p.haul"))
//以上是对子命令解析器进行设置,首先添加一个子命令为client,再设置了该子命令默认的处理函数为run_phaul_client,然后为该子命令添加三个参数:to,--port,--path并设置好了默认值
//所以当我们执行命令./p.haul-wrap client 192.168.11.106 docker d78时,会默认调用函数run_phaul_client

参数解析完毕后,进入run_phaul_client函数调用部分

//python对函数的声明就是简单以def来声明
def run_phaul_client(args, unknown_args):
	"""Run p.haul"""

	print "Establish connection..."

	# Establish connection
	dest_host = args.to, args.port//定义目的端口数组

	connection_sks = [None, None]
	for i in range(len(connection_sks)):
		connection_sks[i] = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		connection_sks[i].connect(dest_host)

	# Organize p.haul args,组织p.haul命令参数list
	
	target_args = [args.path]
	target_args.extend(unknown_args)//list.extend向链表末位追加数据,这里unknown参数为[docker,d78]
	target_args.extend(["--to", args.to,
		"--fdrpc", str(connection_sks[0].fileno()),
		"--fdmem", str(connection_sks[1].fileno())])//追加rpc socket参数和内存传输socket参数

	# Call p.haul
	print "Exec p.haul: {0}".format(" ".join(target_args))
	os.system(" ".join(target_args))//执行p.haul命令,指定了默认的rpc sokcet和内存传输socket

执行p.haul命令后会执行对应的python脚本p.haul:p.haul命令解析方式:p.haul --fdrpc --fdmem ,由于unknown args为前两个参数,所以type为docker,id为d78

# Parse arguments
args = phaul.args_parser.parse_client_args()//参数的解析放在args_parser.py中,设置typedockeridd78--to192.168.11.106

# Configure logging
//logging模块对日志系统进行基本设置:日志文件名,日志文件打开模式(wa),日志级别,指定日志格式
logging.basicConfig(filename=args.log_file, filemode="a", level=logging.INFO,
	format="%(asctime)s.%(msecs)03d: %(process)d: %(message)s",
	datefmt="%H:%M:%S")

# Setup hook to log uncaught exceptions
sys.excepthook = phaul.util.log_uncaught_exception//追踪异常的函数调用过程,这个我觉得非常有用,对于分析出错信息

phaul.util.log_header()//打印日志头:三个空格
logging.info("Starting p.haul")

# Establish connection
connection = phaul.connection.establish(args.fdrpc, args.fdmem, args.fdfs)//根据已有的rpc socket和内存传输socket建立连接

# Start the migration
ph_type = args.type, args.id
worker = phaul.iters.phaul_iter_worker(ph_type, args.dst_id, args.mode,
	connection, args.nostart)//新建一个迭代迁移器对象,该对象会自动初始化 _init_
worker.set_options(vars(args))//设置worker的配置参数
worker.start_migration()//开始迁移

# Close connection
connection.close()//迁移完成后,关闭连接

首先介绍worker初始化过程

//python对象的定义非常简单,class关键字及:即可,通过缩进来确定范围
class phaul_iter_worker(object):
def __init__(self, p_type, dst_id, mode, connection, nostart):
//每一个对象初始化都会自动执行__init__函数,相当于java中的构造函数
		self.__mode = mode
		self.connection = connection
		self.target_host = xem_rpc_client.rpc_proxy(self.connection.rpc_sk)
		self.nostart = nostart
/*
*根据构造函数传进来的参数设置迁移模式,rpc连接,目的机对象和nostart,这里的xem_rpc_client.rpc_proxy对象初始化:
*class rpc_proxy(object):
*  def __init__(self, sk, *args):
*      	self._rpc_sk = sk
*		c = _rpc_proxy_caller(self._rpc_sk, xem_rpc.RPC_CMD, "init_rpc")
*		c(args)
*  def __getattr__(self, attr):
*		return _rpc_proxy_caller(self._rpc_sk, xem_rpc.RPC_CALL, attr)
*		这里_getattr_就是当执行rpc_proxy.attr()时会调用__getattr__self,attr),然后通过_rpc_proxy_caller对象中的_call_函数来对rpc——socket
*		发送请求,当执行_rpc_proxy_caller(args)对象本身相当于执行_call_(self,*args)本身,对应代码中的cargs
*/
	
	
		logging.info("Setting up local")
		self.htype = htype.get_src(p_type)//迁移类型
		if not self.htype:
			raise Exception("No htype driver found")

		self.fs = self.htype.get_fs(self.connection.fdfs)//设置文件系统
		if not self.fs:
			raise Exception("No FS driver found")

		self.img = None
		self.criu_connection = None
		if is_live_mode(self.__mode):
		//这里的phaul命令中mode的默认值为MIGRATION_MODE_LIVE,还有许多参数均在args_parser.py设为默认值
			self.img = images.phaul_images("dmp")//新建一个phaul_image对象,初始化的时候设置img的类型			     
			self.criu_connection = criu_api.criu_conn(self.connection.mem_sk)//新建一个连接criu服务的对象,用于使用criu工具

		logging.info("Setting up remote")
		p_dst_type = (p_type[0], dst_id if dst_id else p_type[1])//新建tuple元组数据
		self.target_host.setup(p_dst_type, mode)//向目的机发送rpc请求,调用_call_命令,使得目的机执行setup(p_dst_type, mode)命令

这一章节我们了解到了 ./p.haul-wrap client 192.168.11.106 docker d78命令的解析及相应初始化过程,构建了与目的机通信及criu service通信的体系,通过目的机上的相应操作及criu相关命令的技术支持,为docker容器的热迁移做好准备,下一章节我们并不会研究开始迁移之后的步骤,而是研究源主机与目的机的通信及远程命令调用方式

在python语言的学习过程中,我们也了解到_init_,call,_getattr_的含义,以及参数解析的模块,日志模块,异常追踪实现,等内容