Gitlab升级迁移小记

由于历史操作原因,Gitlab服务器与Jenkins服务器位于不同大区的ECS服务器中,拉取代码较慢,新服务器采购时正好做个迁移,将2个服务器配置在一个内网中。

原服务器系统CentOS,Gitlab版本 10.2.4,新服务器系统Ubuntu,Gitlab版本。

登录新服务器配置SSH,方便以后的登录。

可参考 SSH登录配置

在新服务器上安装Gitlab

参考 官网命令进行 新服务器Gitlab安装 https://about.gitlab.com/installation/#ubuntu?version=ce

1
2
3
4
5
6
7
8
9
apt update

sudo apt-get install -y curl openssh-server ca-certificates

sudo apt-get install -y postfix

curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash

sudo EXTERNAL_URL="https://xxx.xxxx.com" apt-get install gitlab-ce

升级原服务器Gitlab版本。

由于Gitlab迁移要求 版本号相同,因此对于原服务器Gitlab需要做个升级。

下载新包

https://packages.gitlab.com/gitlab/gitlab-ce
https://packages.gitlab.com/gitlab/gitlab-ce/packages/el/7/gitlab-ce-11.0.3-ce.0.el7.x86_64.rpm

关闭部分gitlab服务

1
2
3
gitlab-ctl stop unicorn
gitlab-ctl stop sidekiq
gitlab-ctl stop nginx

Gitlab备份

1
gitlab-rake gitlab:backup:create

升级

1
2
3
4
5
6
7
8
rpm -Uvh gitlab-ce-11.0.3-ce.0.el7.x86_64.rpm

# 参考
# Ubuntu/Debian系统:
# sudo dpkg -i gitlab_x.x.x-omnibus.xxx.deb

# CentOS系统:
# sudo rpm -Uvh gitlab-x.x.x_xxx.rpm

gitlab跨版本升级错误

提示版本跨度过大无法升级,需按路径依次升级。

原Gitlab版本10.x,找了10版本下最大版本10.8.4,按照上面的升级流程重新走了一遍。

升级10成功。

Gitlab升级10.8.4成功

重新配置gitlab

1
gitlab-ctl reconfigure

重启gitlab

1
gitlab-ctl restart

启动以后500错误。

10.8.4 500错误

查看 日志 /var/log/gitlab/gitlab-rails/production.log

500日志错误

这个错误没有找到好的解决办法。

1
2
3
4
5
6
7
8
# 尝试直接修改表结构
$ gitlab-rails dbconsole

$ alter table issues add column deleted_at timestamp;

# 后续错误依次修改
$ alter table merge_requests add column deleted_at timestamp;
$ alter table namespaces add column deleted_at timestamp;

然后一般页面500错误消失,但退出登录会出现500错误,简直奔溃。

迁移还原

原服务器弄的不完美,直接将升完级的后再备份的文件转移至新服务器备份文件夹下,Gitlab备份文件夹目录/var/opt/gitlab/backups/,还原成功。

1
2
3
4
5
6
7
8
# 备份文件名 1531211711_2018_07_10_11.0.3_gitlab_backup.tar
$ cd /var/opt/gitlab/backups/

# 还原时出现 这个错误 tar: Error is not recoverable: exiting now,权限不足
$ chown git 1531211711_2018_07_10_11.0.3_gitlab_backup.tar

# 还原,还原成功。
$ sudo gitlab-rake gitlab:backup:restore BACKUP=1531211711_2018_07_10_11.0.3

建议谨慎升级,做好备份,实在不行还原到原版本,迁移的话也可以到同版本升级。

本次迁移的下下策是重新创建仓库、用户。因为用户及仓库数量也就十来个,不算多,也算有恃无恐。

Thymeleaf List指定数量循环

问题

项目使用Thymeleaf模板,后端查询10条数据,但根据不同页面可能会显示其中3条或5条。因此有了List指定数量循环的需求,数量由模板决定,而非后端。

解决

不知道是不是搜索关键字的问题还是问题太简单的原因,网上找到一些都是这种

1
${#numbers.sequence(0,3)}

通过Thymeleaf numbers函数进行截取的解决方案。此方案对本项目不适用,因为它要求传入的数据模型就是List,函数会直接截取该List,而项目使用的是更复杂的Map对象。
看了一下Thymeleaf文档,可以使用List的状态变量加以判断来解决这种问题。

1
2
3
4
<div th:text="${title}">标题</div>
<div class="column" th:each="item,itemStat : ${testList}" th:if="${itemStat.count}<=3">
<div th:text="${item.name}"></div>
</div>

以上代码使得无论testList实际有多长,最多只有前3个会被使用到。

其中th:each第二个变量就是迭代的状态变量,本文命名的是itemStat。状态变量有如下属性:
index: 当前迭代索引,从0开始。
count: 当前迭代索引,从1开始。
size: 迭代变量中的元素总数。
current: 每个迭代的iter变量, 和本文中的item一样。
even/odd: 当前迭代是偶数还是奇数,布尔值。
first: 在当前的迭代中是否是第一个,布尔值。
last: 在当前的迭代中是否是最后一个,布尔值。

使用 th:if 加 count比较 即可约束后续内容显示。

Thymeleaf相关链接:

Thymeleaf SAXParseException
Thymeleaf实现页面静态化
Thymeleaf TemplateProcessingException
Thymeleaf比较判断枚举类型

MyBatis查询无数据

问题

项目使用了Mybatis-PageHelper,项目有业务需要在指定范围(指定时间范围指定条数)内的数据里做查询,因此有了下面的Mybatis代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="list" resultMap="TotalResultMap" parameterType="com.yl.dao.query.YlCaseListQuery">
select allData.* from (
select yce.*,yc.patient_id
from yl_a yce left join yl_c yc on yc.id = yce.case_id where 1=1
<if test="ctimeStart!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{ctimeStart}, '%Y-%m-%d')
</if>
<if test="ctimeEnd!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{ctimeEnd}, '%Y-%m-%d')
</if>
order by yce.ctime desc limit 200) as allData
<where>
<if test="patientId!=null">
and allData.patient_id = #{patientId}
</if>
</where>
</select>

当不使用patientId查询条件时没有问题,查询能正常查询出200条记录。
但添加查询条件后数据可能查询不出来。当时的情况是测试环境测试了一下可以查询,生产环境没有结果。

解决

当前看了好久没找到问题,后来打开生产环境的sql打印。发现mybatis-pagehelper在select count(0)查询时会默认将sql语句中的order by去掉,也就是count查询时上面的子查询没有order by排序但有limit 200, 有无排序的数据集是不一样的,此时查询出来的新的数据集确实没有包含对应patientId记录的话,count(0)值就是0。

因为查询前会count查询一下判断是否存在数据,再决定是否进行实际查询,count结果为0就直接导致了查询无数据。

查看Mybatis-PageHelper文档,在SQL中包含

1
/*keep orderby*/

注释时,count查询时不会移出order by。

因此更新代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="list" resultMap="TotalResultMap" parameterType="com.yl.dao.query.YlCaseListQuery">
select allData.* from (
select yce.*,yc.patient_id
from yl_a yce left join yl_c yc on yc.id = yce.case_id where 1=1
<if test="ctimeStart!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{ctimeStart}, '%Y-%m-%d')
</if>
<if test="ctimeEnd!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{ctimeEnd}, '%Y-%m-%d')
</if>
order by yce.ctime desc limit 200) as allData /*keep orderby*/
<where>
<if test="patientId!=null">
and allData.patient_id = #{patientId}
</if>
</where>
</select>

此时,count查询是order by没被删除,根据patientId查询成功。

Thymeleaf异常-TemplateProcessingException Exception evaluating SpringEL expression - Property or field 'x' cannot be found on object of type 'java.util.HashMap'

问题

1
2
3
<div th:each="item:${list}">
<div th:text="${item.show}"></div>
</div>

thymeleaf 循环list时,若item里存在空值,即show为空时,可能会出现如下错误。

1
2
3
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "item.show" (template: "index" - line 37, col 82)
...
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'show' cannot be found on object of type 'java.util.HashMap' - maybe not public or not valid?

解决

使用map的方式获取值,即使用 ${item.get(‘show’)} 替换 ${item.show}。

1
2
3
<div th:each="item:${list}">
<div th:text="${item.get('show')}"></div>
</div>

成功渲染。

Thymeleaf相关链接:

Thymeleaf List指定数量循环
Thymeleaf SAXParseException
Thymeleaf实现页面静态化
Thymeleaf比较判断枚举类型

IOS The OSS Access Key Id you provided does not exist in our records

问题

IOS使用OSS上传提示403, InvalidAccessKeyId, The OSS Access Key Id you provided does not exist in our records。

解决

首先排除AccessKeyId出错的情况,Android及前端均能上传成功。

原因可能是服务端使用STS签名,返回相关凭证,如:

1
2
3
4
5
"StatusCode": "200",
"AccessKeyId": "STS.NJu2d79fYixPT5KmX8hF3GQ2A",
"AccessKeySecret": "FzskGbUNkiJn7BqMwfSPv1jkmAfLDyaEMiSAVnrnCLbn",
"SecurityToken": "CAISjQJ1q6Ft5B2yfSjIr4nAed6D1Ll43rq7VhP6iVhtZMkfqJSZozz2IH5Lf3RqAuEctP4xmmpQ6PoZlqp6U4cd7u9dwGY0vPpt6gqET9fria7ctM456vCMHWyUFGSMvqv7aPn4S9XwY+qkb0u++AZ43br9c0fNPTGiKobby+QkDLItUxK/cCBNCfpPOwJms7V6D3bKMuu3OROY5Qi1BUFz6A1nkjE9u+btgO/ks0OE0wSqmrJE+dmgesf1NPMBZskvD42Hu8VtbbfE3SJq7BxHybx7lqQs+02c4I7GXgMKsk/cabSKqIUxcFRjFaE+Gr9Zqv/njuF/ueHVmInxxgxEIeZPSSPbSZABOTdzsC5BXBqAAWOe5SUr5pZUDVrYtTLfpFnv030/ru2vxmINy3T8/ERQpnp92xDMXC/o1uozmN9GHBjh1qOvE6RRwjD0zewZYy9dHDbMSna43nJg988w0+vhciI1aswBrjwUlIqCmRosIVngmgIYe4zqqrYuTq1KF7jLmkciIORF8ZauwFakrln/",
"Expiration": "2018-07-02T02:04:59Z"

而IOS端当时的写法需要使用的是主AccessKey、Security,而非STS生成的凭证,因此也没有使用SecurityToken,因此导致上传出错。

如果仍然打算参数STS签名方式,可参考 IOS访问控制 https://help.aliyun.com/document_detail/32046.html 使用 OSSStsTokenCredentialProvider。

Runtime.getRuntime().exec异常

问题

Process process = Runtime.getRuntime().exec(“ifconfig | grep inet”); 此时会报如下异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java.io.IOException: Cannot run program "ifconfig | grep inet": error=2, No such file or directory

at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048)
at java.lang.Runtime.exec(Runtime.java:620)
at java.lang.Runtime.exec(Runtime.java:485)
at com.uploader.util.DeviceUtil.getNetInfo(DeviceUtil.java:73)
at AddressAPITest.ip(AddressAPITest.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.io.IOException: error=2, No such file or directory
at java.lang.UNIXProcess.forkAndExec(Native Method)
at java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
at java.lang.ProcessImpl.start(ProcessImpl.java:134)
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
... 26 more

解决

使用sh -c

1
2
String[] command = {"/bin/sh", "-c", "ifconfig | grep inet"};
Process process = Runtime.getRuntime().exec(command);

成功执行。

1
2
3
4
$ man sh

...
-c Read commands from the command_string operand instead of from the standard input. Special param‐eter 0 will be set from the command_name operand and the positional parameters ($1, $2, etc.)set from the remaining argument operands.

获取阿里云STS凭证报错SDK.ServerUnreachable

问题

最近做捣鼓Web端OSS直传,比较安全的直传方式有两种,使用后端签名直传 和 使用STS凭证直传, OSS官方Web端直传例子中使用的是后端签名直传,移动端直传例子使用的STS凭证,相关SDK例子使用的是STS凭证。项目前端使用的是react + antd,因此打算参考使用STS凭证 + ali-oss 进行直传,以后也能复用在移动端上传。

完成相关代码后获取凭证报错。
获取阿里云STS凭证失败

1
2
3
4
5
org.apache.http.impl.execchain.RetryExec - I/O exception (org.apache.http.conn.UnsupportedSchemeException) caught when processing request to {s}->https://sts.aliyuncs.com:443: https protocol is not supported

org.apache.http.impl.execchain.RetryExec - Retrying request to {s}->https://sts.aliyuncs.com:443

me.gelu.base.framework.aliyun.oss.OSSService - {StatusCode=500, ErrorCode=SDK.ServerUnreachable, ErrorMessage=Server unreachable: org.apache.http.conn.UnsupportedSchemeException: https protocol is not supported}

解决

参考文档,对比POM文件,发现core依赖版本可能过高,最近正好升级过依赖版本。

阿里云STS POM版本配置

当前依赖版本

1
2
3
4
5
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.2</version>
</dependency>

降级为截图中的3.5.0即可。

1
2
3
4
5
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.5.0</version>
</dependency>

这应该不是唯一能用的版本,尝试降到3.7.1,依旧报错,降到升级前版本3.2.8则新编写的STS凭证代码无法使用,需要改写。因此直接参考截图进行配置,获取凭证成功。

阿里云STS凭证获取成功

Configuration property name 'xxx' is not valid

问题

程序出错:Configuration property name ‘xxx’ is not valid, Canonical names should be kebab-case (‘-‘ separated), lowercase alpha-numeric characters and must start with a letter。

1
2
3
4
5
6
7
8
9
Configuration property name 'cmsOss' is not valid:

Invalid characters: 'O'
Bean: CMSImageController
Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter

Action:

Modify 'cmsOss' so that it conforms to the canonical names requirements.

解决

原因是命名不规范,不要大写字母,使用小写字母,可加中划线-。

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Component
@ConfigurationProperties(prefix = "cmsOss")
public class CMSOSSConfig {
private String accessId;

private String accessKey;

private String endpoint;

private String bucket;
}

将cmsOss改成cms-oss, 对应配置文件也做相应调整即可。

Thymeleaf实现页面静态化

最近业余时间在折腾CMS框架,需要完成页面静态化。采用的方案是Thymeleaf。
大致流程是:首先需要编辑一些Thymeleaf模板,然后调用接口来向模板里渲染数据,同时输出到指定文件。

添加依赖

项目使用的是Spring Boot,所以直接使用spring-boot-starter-thymeleaf。

1
2
3
4
5
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>

配置测试模板 test.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello <span th:text="${title}">test</span></title>
</head>
<script th:remove="all" type="text/javascript" src="http://cdn.jsdelivr.net/thymol/latest/thymol.min.js"></script>
<body>
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
My first website with <span th:text="${content}">test</span>!
</p>
</div>
</section>
</body>
</html>

将test.html放入resources/templates下即可。

配置Bean

生成Bean供业务使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AppConfig {

@Bean
public TemplateEngine emailTemplateEngine() {
final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(springTemplateResolver());
return templateEngine;
}

private ITemplateResolver springTemplateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setOrder(1);
templateResolver.setPrefix("classpath:/templates");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
}

在对应的业务类中注入即可。

配置相关工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ThymeleafUtil {

// 隐藏Thymeleaf细节, 直到在渲染数据前都使用Map存储数据
public static Context getContext(Map<String, Object> dataMap) {
Context ctx = new Context();
for (Map.Entry<String, Object> dataEntry : dataMap.entrySet()) {
ctx.setVariable(dataEntry.getKey(), dataEntry.getValue());
}
return ctx;
}

}


import org.apache.commons.io.FileUtils;
public class FileUtil {

// 输出内容到文件
public static void writeStringToFile(String content, String filePath) {
File file = new File(filePath);
try {
FileUtils.writeStringToFile(file, content, Charset.defaultCharset());
} catch (IOException e) {
e.printStackTrace(); // TODO
}
}

}

业务编写

注入TemplateEngine

1
2
@Autowired
private TemplateEngine templateEngine;
1
2
3
4
5
Map<String, Object> dataMap = Maps.newHashMap();
dataMap.put("title", "test");
dataMap.put("content", "test");
Context ctx = ThymeleafUtil.getContext(dataMap);
FileUtil.writeStringToFile(templateEngine.process("test", ctx), "result.html");

完成。

Thymeleaf相关链接:

Thymeleaf List指定数量循环
Thymeleaf SAXParseException
Thymeleaf TemplateProcessingException
Thymeleaf比较判断枚举类型

freemarker出现NonNumericalException异常

问题

使用Freemarker作为模板引擎出现了NonNumericalException异常。错误如下:

1
2
ERROR freemarker.runtime - Error executing FreeMarker template  
freemarker.core.NonNumericalException: For "#{...}" content: Expected a number, but this has evaluated to a string (wrapper: f.t.SimpleScalar):

确认了模板数据没有问题,模板及数据都是字符串类型。

解决

不应该使用 #{…}, 应该要使用 ${…}

平常使用较多的表示式有些混淆,这个问题当时找了好久,遂记之。