Harbor折腾升级记

Harbor是vmware中国开发的一款企业级的DockerRegistry服务器,我们内部也是有搭建了一个Harbor,但是版本是0.5,对于当前最新的release版本1.5.2而言已经太老了,确实也有一些问题,比如不支持多级的镜像名称,某些情况下会触发bug导致panic。

所以需要升级一下,既然考虑升级了,就干脆升级到最新的版本1.5.2了。首先说一下目前的Harbor,官方提供的离线安装包里,默认是本地启动一个MySQL,将Harbor需要的一些数据存储在本地的MySQL中的,这个是不能接受的,所以在之前的部署中,是使用了外部的一个MySQL,同样,registry的存储在线上也是使用了共享存储,保证可用性。

不过Harbor的1.5.2版本对于0.5版本变化还是比较大的,首先是增加了adminserver这个角色,将所有的配置都拿到adminserver中存储,ui组件通过http请求定期向adminserver请求当前最新的配置信息,其次是数据库结构,新版本和旧版本相比数据库结构发生了很大的变化。

对于升级操作,官方也提供了解决方案,可以参考migration_guide进行升级,升级工具的镜像官方也是提供了,但是这其中存在一个问题,就是升级工具依赖本地的MySQL,也就是说,这个工具只能工作在MySQL是Harbor离线安装包启动的情况下,如果使用了外部的MySQL,这个升级工具就无法直接使用了。

所以呢,最终还是需要去看一下官方的升级工具是如何实现的,看能否通过其他办法手动升级,于是就花了点时间看了一下代码,找到了最后的实现方式,具体的代码在alembic/mysql这个目录下,原理也很简单,官方使用了一个Python的工具alembic实现了数据库结构的版本管理。

手动运行数据库升级,首先需要安装alembic工具,可以通过pip安装,或者针对不同的发行版找对应的软件包。

下面开始操作:

目录下有一个alembic.tpl文件,这个文件是alembic运行所需要配置文件的模板,我们可以通过source alembic.tpl > alembic.ini实例化一个配置文件,然后打开配置文件,可以看到最关键的两个配置:

script_location = /harbor-migration/db/alembic/mysql/migration_harbor
sqlalchemy.url = mysql://:@localhost:3306/registry?unix_socket=/var/run/mysqld/mysqld.sock

分别是脚本路径和连接MySQL的地址,默认的在工具镜像中脚本路径是放在/harbor-migration/这个目录下,这个需要根据机器上的情况修改到对应目录,然后sqlalchemy.url配置也很明显,需要修改为实际线上数据库的连接地址。

修改完成后,就可以调用alembic工具升级数据库了:

export PYTHONPATH=`pwd`    # 添加PYTHONPATH
alembic -c alembic.ini current # 查看当前数据库版本
alembic -c alembic.ini upgrade 1.5  # 升级到1.5版本
alembic -c alembic.ini current # 查看升级后数据库版本

脚本运行完成后,数据库结构就能升级成功了。需要提醒的是,建议先导出线上数据到单独的一个数据库中做一下测试,确认没有问题后再进行线上操作,另外,做好备份!

数据库升级完成,该升级各个组建了,新版Harbor的配置也有比较大的变化,默认安装时会有个harbor.cfg,官方也提供了一个工具去做harbor.cfg的版本迁移,但是我们也不需要通过这个工具了,在安装时,prepare脚本会根据harbor.cfg文件生成对应组件所需要的配置和env文件,所以我们直接使用新版的默认harbor.cfg生成配置文件,再对比老的配置文件,然后直接修改最后的配置文件保持一致。

需要注意的是,新版Harbor大部分的配置都集中在common/config/adminserver/env文件中,而adminserver是老版本没有的角色,老版本大部分配置都集中在common/config/ui/app.confcommon/config/ui/env中。
最终我们采取的办法是,先用默认配置生成一份实际的配置文件,特别是common/config/adminserver/env文件,再根据老版本的线上配置文件比对,直接修改生成后的配置文件而不是harbor.cfg,相当于新旧配置文件做一次merge。等配置文件准备完成,直接调用docker-compose up -d以新配置文件启动Harbor容器。

总体来说,升级还是很顺利的,具体的步骤如下:
1. 备份数据库
2. 停止并删除老Harbor容器
3. 通过alembic升级数据库版本
4. 通过docker-compose up -d启动新版本Harbor容器

升级过程很顺利,容器启动后工作正常,但是遇到一个小问题,就是Harbor管理平台里显示的日志的时间差了8个小时,这个其实是在容器化中很容易遇到的一个情况,容器中的时区和宿主机不一致,毕竟我们在中国嘛,还是应该用北京时间,虽然问题挺常见,但是在Harbor解决这个问题还是费了一些力气。
正常的思路,就是修改一下docker-compose.yaml/usr/share/zoneinfo/Asia/Shanghai挂载到/etc/localtime,这样容器内部时区就正确了。测试了一下,确实如此,在容器中执行date命令已经可以正常返回正确的时间。
但是日志的时间并没有变化,这个确实令人费解,由于Harbor的镜像相关日志都是写到数据库中的,所以还是需要看一下具体插入日志的代码,看是否能够发现一些问题:

// 需要记录日志的地方调用dao.AddAccessLog插入日志,可以看到OpTime就是time.Now()。
go func() {
	if err = dao.AddAccessLog(
		models.AccessLog{
			Username:  p.SecurityCtx.GetUsername(),
			ProjectID: projectID,
			RepoName:  pro.Name + "/",
			RepoTag:   "N/A",
			Operation: "create",
			OpTime:    time.Now(),
		}); err != nil {
		log.Errorf("failed to add access log: %v", err)
	}
}()

// AddAccessLog具体实现
// AddAccessLog persists the access logs
func AddAccessLog(accessLog models.AccessLog) error {
	// the max length of username in database is 255, replace the last
	// three charaters with "..." if the length is greater than 256
	if len(accessLog.Username) > 255 {
		accessLog.Username = accessLog.Username[:252] + "..."
	}

	o := GetOrmer()
	_, err := o.Insert(&accessLog)
	return err
}

看到这边,基本可以确定和ORM实现没有关系了,因为用的mysql驱动是go-sql-driver/mysql,而这个驱动可以单独设置时区,而且驱动默认的时区是UTC,所以需要修改,具体可以参考mysql#loc,于是问题就变成了如何将这个参数传到驱动了,继续看代码:

// NewMySQL returns an instance of mysql
func NewMySQL(host, port, usr, pwd, database string) Database {
	return &mysql{
		host:     host,
		port:     port,
		usr:      usr,
		pwd:      pwd,
		database: database,
	}
}

// Register registers MySQL as the underlying database used
func (m *mysql) Register(alias ...string) error {

	if err := utils.TestTCPConn(m.host+":"+m.port, 60, 2); err != nil {
		return err
	}

	if err := orm.RegisterDriver("mysql", orm.DRMySQL); err != nil {
		return err
	}

	an := "default"
	if len(alias) != 0 {
		an = alias[0]
	}
    // 关键代码!根据配置组合一个MySQL URI,初始化驱动。
	conn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", m.usr,
		m.pwd, m.host, m.port, m.database)
	return orm.RegisterDataBase(an, "mysql", conn)
}

也就是说,可以在database名字后面加上?loc=Local来让MySQL驱动使用本地的时区设置。于是就简单了,直接修改common/config/adminserver/env文件,将其中的MYSQL_DATABASE=registry改成MYSQL_DATABASE=registry?loc=Local就行了,虽然有点hack,但是,起码不用修改代码再编译打镜像了吧~
一波hack操作,重启对应组建,启动完成,果然时区也正常了~

PS:Harbor 1.6为了保持和Clair数据库的一致性,将MySQL迁移到PostgreSQL了,所以上面的做法可能就不适用于1.6了,由于暂时内部还没有PostgreSQL的支持,也不需要Clair,所以短期内没有考虑升级1.6,等哪天需要升级了,再看相应的解决方案吧。