写在前面

之前也是写了一篇这周搞的一些新东西,其实当时就是看了V2站长开始写周报也有了点想法但是想想似乎每周也没有那么多可写的内容,就先随便写了一篇.但是最近也算是有点清闲又骚了很多新的东西就不免想要记录一下,正好趁着给博客重新进行了一波整理分类之后也是下了开始写周报的决心吧,但是频率可能也就是2周或者3周记录和总结一下.

ps. 目前格式就按照事情来排列然后加以描述

v2ray

在经历了一系列不可描述的会议之后,在2个星期前我的hk服务器算是终于又通了$$了,但是鉴于封杀时的效率还是有点怕自己的ip被封了的,所以就趁着摸鱼的时光研究了一下v2ray.当然不是深入研究,最开始是找了一个一键部署的脚本,也就是填一下配置就能直接用了感觉还是很不错的.

V2Ray一键安装脚本

HTTP/2

在后来看了这个v2ray-HTTP/2之后发现HTTP/2似乎是很”香”就打算尝试一下,但是:

与其它的传输层协议一样在 streamSettings 中配置,不过要注意的是使用 HTTP/2 要开启 TLS。

似乎是要上HTTPS了,鉴于现在的域名还用在博客上就又重新申请了一个域名用来申请一个免费的HTTPS证书然后用来配置HTTP/2.

CDN

Cloudflare 还是太慢了,用国内的吧,绝对能体验飞一般的速度,正好 V2Ray 已经支持 HTTP/2 了,又拍云、七牛、阿里都不错,腾讯的 h2 还在内测,百度不清楚。不过说真的,CDN 的话国外的还是有些水土不服,强烈建议使用国内的,速度提升非常大,也非常稳定,高峰期毫无压力,在重点 IP 段也无所畏惧。

就是跟着上面的HTTP/2又顺势看了CDN,想的是如果能使用国内CDN的话岂不是速度能快的飞起(毕竟现在延迟有200ms左右),但是在看了国内免费CDN对比与申请部署教程之后尝试了一下又拍云才发现:国内的备案太痛苦了!!!.并且阿里云的备案要求购买轻量应用服务器续费3个月以上,但是在我续费之后仍然不能申请备案服务号也就放弃了,还是选择了cloudflare.

v2-ui

是在看拯救被墙的IP,CDN + v2ray,安全的科学上网方法这篇博客的时候发现的,发现比一键部署脚本还是挺不错的毕竟有GUI可以用来动态配置.

现在的v2ray配置

大致的流程:

  1. 使用cloudflare来解析域名(修改DNS)
  2. 添加一个A解析指向v2ray的ip,Name自己填写,我使用了www
  3. 在SSL/TLS选项卡中选择Full(Encrypts end-to-end, using a self signed certificate on the server)
  4. 在v2-ui中新建一个账号,端口使用了80(玄学:因为大部分网站都使用80端口做web服务所以不容易被侦测),配置http(就是http/2),host和域名都填了上面解析的域名,然后选择申请的免费ssl证书
  5. 导出配置

下面是我的配置截图:
HTTP/2+TLS

Aria2+Filebrowser

有了服务器就会开始折腾,因为自己电脑挂bt有点慢而且AMD出名的功耗高也不想因为下个视频就一直开着于是就想到在服务器上在整一个Aria来挂bt然后再来一个远程播放岂不美哉?

当然有轮子是最好的,于是就找到了:使用Docker安装Aria2+AriaNg+Filebrowser,可离线BT下载/在线播放,直接docker run 非常不错:

1
docker run --name ccaa -d -p 6800:6800 -p 6080:6080 -v /home/root/aria-download:/Down moerats/ccaa:latest

当然-v不是必要的,这是为了后面做铺垫

NextCloud

Nextcloud是一套用于创建网络硬盘的客户端-服务器软件。其功能与Dropbox相近,但Nextcloud是自由及开放源代码软件,每个人都可以在私人服务器上安装并运行它。

与Dropbox等专有服务相比,Nextcloud的开放架构让用户可以利用应用程序的方式在服务器上新增额外的功能,并让用户可以完全掌控自己的数据。
Nextcloud的文件存储在一般的目录结构中,并可透过WebDAV访问。用户的文件会在传输时加密。Nextcloud可与在Windows(Windows XP、Vista、7与8)、macOS(10.6或更新版本)或是多种Linux散布版上运行的客户端同步。

Nextcloud用户可以管理日历(使用CalDAV)、联系人(CardDAV)、计划工作与流媒体(Ampache)。

从管理的角度来看,Nextcloud允许用户与组群管理(透过OpenID或LDAP)。透过用户间与/或组群间的读/写权限调整达到分享文件的目的。另外,Nextcloud的用户可以创建公开的URL来分享文件。也可以记录与文件相关的动作,以及利用文件访问规则来禁止对特定文件的访问[2]。

此外,用户也可以透过浏览器使用Nextcloud的文本编辑器、书签服务、缩略网址服务、相册、RSS阅读器与文件查看器。因为有良好的扩展性,Nextcloud可以透过鼠标点一下即可完成安装的应用程序强化其功能,并可连线至Dropbox、Google云端硬盘与Amazon S3。 ————来自维基百科

上面的-v当然是这里用到了,原因是有些视频格式对于Filebrowser来说太难了,并不能正确的解析,在线观看也就无从说起了.所以在一番调研之后决定在为了能看视频的同时顺便尝试一下这私人网盘是什么感觉的.

当然还是方便的docker部署了:

1
docker run -d --restart=always --name nextcloud -p 8888:80 -v /root/nextcloud:/data -v /home/root/aria-download:/aria docker.io/nextcloud

然后在NextCloud挂载外部存储就可以了.

JetBrains 免费License

在毕业几个月之后终于意识到:我的学生邮箱没了,也就是说不能再白嫖好用的idea了(才怪),在琢磨了一番之后发现了官方的面向开发者的免费许可:Open Source License Request,于是就用这个博客项目尝试了一波,一开始也是没有抱多大希望的,毕竟项目没人star也没人fork,但是在几天之后收到了:

于是就快乐的激活了许可:

虽然感觉还是受之有愧,但是毕竟JB对开发者这么好也就只能收下这份许可来继续激励我写博客吧.

咖啡

不知道是什么时候养成的习惯吧,但是现在似乎是已经离不开咖啡了,之前的出租房地方比较小的时候还是喝一喝挂耳咖啡,在这次搬家之后也是空间Dark♂了很多然后就趁着双11入了一个法压壶+咖啡豆,一是挂耳确实有点贵,买咖啡豆的话买些意式拼配的豆子也是比挂耳便宜了许多,再者也是想尝试一下是不是法压壶能够提升一下鉴赏咖啡的水平.

在尝试了两次之后发现还是挺不错的,也是开始进行变量控制来尝试自己最喜欢的配比跟步骤吧:

博客分类

空闲的时候重新回来看了看博客的分类着实有些杂乱,后面打算写文章也不知道往哪里分比较好就重构了一下自己博客的分类方式,现在也算是比较满意了吧(最起码新写博客的时候不再为了这个分类头疼了).

骑行

鉴于不断增加的体重和国庆回家期间被家里人的不断叮嘱,还是觉得要增加一下运动量的嘛,正好拿到了人才补助的钱就想着整了一辆自行车,一来可以代步二来附近也没什么可以跑步的场地正好尝试一下骑行的快乐⑧.

若饭

这是之前看小众软件的时候被推荐了一波,结合上面打算运动一下减减肥的理念,也就尝试了一下新手套餐发现液体的饮料还是比较能接受的(喝上去感觉和豆奶很像),就整了一罐粉末来当晚饭.个人感觉还是可以接受的,饱腹感也还可以.就先体验一下,说不定也会有回购和体验的测评.

总结

周报周报,可能对我来说更像是多了一个文字输出的渠道吧,毕竟网络社交渠道基本都已经是断绝了,有一个输出口也不见得是一件坏事.

当然有时候似乎很坚信自己现在清晰的知道自己究竟想要什么,在做什么,但是有时在陌生的边缘徘徊的时候又觉得自己似乎几年来还是一个dio样子又不免让人觉得蕉躁.

当然,多想多看,多写写日志吧.

吾尝终日而思矣 不如须臾之所学也

写在前面

在看了许多CI的内容之后,正好碰上公司尝试把应用容器化以及进行持续集成(之前还是需要手动执行启停脚本),然后我便成了第一个吃螃蟹的人,在一阵折腾之后也勉强算是把整个流程给走通了便在这里记录一下大致的流程和踩到的坑,也是对最近许久不更新的博客增加一些新东西吧.

部署流程

首先描述一下整个部署的流程来理清一下整个逻辑,这里仅仅是给出我自己的暂时的解决方案,由于业务和环境的不同必定没有完全一样的流程,并且这也只是暂时的方案还是有很多可以优化的地方。

1
2
更新代码 ==> 触发Jenkins流水线的构建 ==> Maven打包项目 ==> 制作docker image ==> 
上传image(这里使用阿里的镜像服务) ==> 远程服务器执行部署脚本 ==> 判断部署状态(nacos api)

需要注意的是:

  1. 项目的仓库中包含了Jenkinsfile和部署的脚本(全部交由版本控制)
  2. 由于线上服务器为阿里ECS并且外网无法访问,所以使用了部署服务器(外网IP)来做跳转和部署
  3. 因为项目使用了SpringCloud的微服务框架并且使用了NACOS注册中心,所以在服务的优雅下线重启阶段使用了nacos的服务调度API并没有使用SpringCloud自带的端点,当然本质都是通过接口调用告知注册中心下线服务
  4. 由于线上服务器均没有使用root用户来发布应用所以在这里踩了小坑,暂时的解决方案可能并不完美

Jenkins

安装

官方教程的链接:Installing Jenkins

当然这里还是用了docker安装的方式,官方的命令如下:

1
2
3
4
5
6
7
8
9
docker run \
-u root \
--rm \
-d \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins-data:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkinsci/blueocean

最主要的命令还是两个-v的参数了,一个是指定Jenkins文件的存放位置,第二个则是docker in docker的关键命令了,这个挂载能够让安装在docker里的Jenkins使用docker命令。

not work?

没错,这个命令在我司的服务器上并没有生效,可能是因为安装的docker版本过低的原因,在一番面向Google编程之后找到了如下的方式:How to build docker images inside a Jenkins container.,本质还是docker in docker,不过需要自己制作镜像,Dockerfile文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM docker.io/jenkins/jenkins:latest

USER root
RUN apt-get update -qq \
&& apt-get install -qqy apt-transport-https ca-certificates curl gnupg2 software-properties-common
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
RUN add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/debian \
$(lsb_release -cs) \
stable"
RUN apt-get update -qq \
&& apt-get install docker-ce=17.12.1~ce-0~debian -y
RUN usermod -aG docker jenkins

构建镜像命令:

1
docker image build -t jenkins-docker .

docker run 命令和上面一样

pipeline(流水线)

官方的文档:流水线语法

在自己实践的过程中踩了不少的坑,到最后还是发现官方文档最靠谱,所以有什么解决不了的问题还是先求助官方文档为上。

这里给出测试和生产两个环境的Jenkinsfile用于参考:

测试环境

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/*
在接触Jenkins流水线的时候最迷惑的就是语法中的pipelinee和node两种组织形式,
一开始使用的是pipeline,但是在看文档的时候又有很多node的写法就感到非常的难受,
总体感觉上node的写法似乎支持更多的插件并且语法更友好但是发现的时候已经差不多写完了也就没有更改
*/
def host() {
def remote = [:]
remote.name = 'test'
remote.host = 'xxxx'
remote.user = 'xxx'
remote.password = 'xxxx'
remote.allowAnyHosts = true
return remote
}

pipeline {

agent any

environment {
// 这里主要是配置整个流水线所需要的变量,核心思想是要复用流水线和部署脚本的代码而只更改参数
ENV = 'xx'
SERVICE_NAME = 'xx'
SERVER_IP = 'xx'
SERVER_PORT = 'xx'
SERVER_USER = 'xx'
SERVICE_VERSION = 'xx'
DOCKER_REPO = 'xx'
DOCKER_NAMESPACE = 'xx'
DOCKER_USERNAME = 'xx'
DOCKER_PASSWORD = 'xx'
NACOS_IP = 'xx'
NAMESPACE_ID = 'xx'
// jvm参数
JAVA_OPTS = 'xx'
}

stages {
stage('构建MAVEN项目') {
agent {
// docker in docker就是应用在这里了,使用maven的docker镜像来执行构建
docker {
image 'maven:3-alpine'
// 映射本地的maven仓库到docker里防止每次都去远程仓库下载jar
args '-v /var/jenkins_home/.m2:/root/.m2'
}
}
steps {
// maven package
sh 'mvn clean install -Dmaven.test.skip=true -Dproject.type=jar -Ptest package'
}
}

stage('构建docker镜像') {
agent {
docker {
image 'docker'
// 这里的挂载比较奇怪,因为在上一步maven构建完成之后并没有生成target(现在想来可能是docker的缘故),
// 所以把本地的maven仓库给挂载进来获取jar包使用了
args '-v /var/jenkins_home/.m2/repository/com/ghaoqi:/root/jar'
}
}
steps {
sh '''
rm -f docker/*.jar
cp /root/jar/${SERVICE_NAME}/${SERVICE_VERSION}-${ENV}/${SERVICE_NAME}-${SERVICE_VERSION}-${ENV}.jar ./docker/${SERVICE_NAME}.jar
cd docker
docker build -t ${SERVICE_NAME}:${SERVICE_VERSION} .
'''
}
}

stage('推送构建的镜像') {
steps {
// 这里使用了阿里云的容器镜像服务
sh '''
docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} ${DOCKER_REPO}
docker tag ${SERVICE_NAME}:${SERVICE_VERSION} ${DOCKER_REPO}/${DOCKER_NAMESPACE}/${SERVICE_NAME}:${SERVICE_VERSION}
docker push ${DOCKER_REPO}/${DOCKER_NAMESPACE}/${SERVICE_NAME}:${SERVICE_VERSION}
docker image rm ${SERVICE_NAME}:${SERVICE_VERSION}
docker image rm ${DOCKER_REPO}/${DOCKER_NAMESPACE}/${SERVICE_NAME}:${SERVICE_VERSION}
'''
}
}

stage('部署到远程机器') {
steps {
// 使用sed的核心思路还是保证脚本能够复用而只需要修改参数
sh """
sed -e 's/__nacos_ip__/${NACOS_IP}/' \
-e 's/__serviceName__/${SERVICE_NAME}/' \
-e 's/__ip__/${SERVER_IP}/' \
-e 's/__port__/${SERVER_PORT}/' \
-e 's/__docker_repo__/${DOCKER_REPO}/' \
-e 's/__docker_namespace__/${DOCKER_NAMESPACE}/' \
-e 's/__docker_username__/${DOCKER_USERNAME}/' \
-e 's/__docker_password__/${DOCKER_PASSWORD}/' \
-e 's/__version__/${SERVICE_VERSION}/' \
-e 's/__user__/${SERVER_USER}/' \
-e 's#__JAVA_OPTS__#${JAVA_OPTS}#' \
-e 's/__namespaceId__/${NAMESPACE_ID}/' deploy.sh > replace.sh;
"""

// Jenkins自带的ssh插件
sshScript remote: host(), script: "replace.sh"
}
}
}
}

生产环境:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
pipeline {

agent any

environment {
// 环境
ENV = 'PRO'
// 服务名 ip 端口 版本
SERVICE_NAME = 'xx'
SERVER_IP = 'xx'
SERVER_PORT = 'xx'
SERVER_USER = 'xx'
SERVICE_VERSION = 'xx'
// docker repo 内网ip
DOCKER_INTERNAL_REPO = 'xx'
// docker repo 外网ip(上传镜像用)
DOCKER_PUBLIC_REPO = 'xx'
// docker repo 命令空间 帐号密码
DOCKER_NAMESPACE = 'xx'
DOCKER_USERNAME = 'xx'
DOCKER_PASSWORD = 'xx'
// 注册中心地址 命名空间
NACOS_IP = 'xx'
NAMESPACE_ID = 'xx'
// ssh 密钥文件
SECRET_FILE = credentials('xx')
// jvm参数
JAVA_OPTS = 'xx'
}

stages {

stage('构建MAVEN项目') {
agent {
docker {
image 'maven:3-alpine'
args '-v /var/jenkins_home/.m2:/root/.m2'
}
}
steps {
sh 'mvn clean install -Dmaven.test.skip=true -Dproject.type=jar -Ppro package'
}
}

stage('构建docker镜像') {
agent {
docker {
image 'docker'
args '-v /var/jenkins_home/.m2/repository/com/ghaoqi:/root/jar'
}
}
steps {
sh '''
rm -f docker/*.jar
cp /root/jar/${SERVICE_NAME}/${SERVICE_VERSION}-${ENV}/${SERVICE_NAME}-${SERVICE_VERSION}-${ENV}.jar ./docker/${SERVICE_NAME}.jar
cd docker
docker build -t ${SERVICE_NAME}:${SERVICE_VERSION} .
'''
}
}

stage('推送构建的镜像') {
steps {
sh '''
docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} ${DOCKER_PUBLIC_REPO}
docker tag ${SERVICE_NAME}:${SERVICE_VERSION} ${DOCKER_PUBLIC_REPO}/${DOCKER_NAMESPACE}/${SERVICE_NAME}:${SERVICE_VERSION}
docker push ${DOCKER_PUBLIC_REPO}/${DOCKER_NAMESPACE}/${SERVICE_NAME}:${SERVICE_VERSION}
docker image rm ${SERVICE_NAME}:${SERVICE_VERSION}
docker image rm ${DOCKER_PUBLIC_REPO}/${DOCKER_NAMESPACE}/${SERVICE_NAME}:${SERVICE_VERSION}
'''
}
}

stage('部署到远程机器') {
steps {
sh """
sed -e 's/__nacos_ip__/${NACOS_IP}/' \
-e 's/__serviceName__/${SERVICE_NAME}/' \
-e 's/__ip__/${SERVER_IP}/' \
-e 's/__port__/${SERVER_PORT}/' \
-e 's/__docker_repo__/${DOCKER_INTERNAL_REPO}/' \
-e 's/__docker_namespace__/${DOCKER_NAMESPACE}/' \
-e 's/__docker_username__/${DOCKER_USERNAME}/' \
-e 's/__docker_password__/${DOCKER_PASSWORD}/' \
-e 's/__version__/${SERVICE_VERSION}/' \
-e 's/__user__/${SERVER_USER}/' \
-e 's#__JAVA_OPTS__#${JAVA_OPTS}#' \
-e 's/__namespaceId__/${NAMESPACE_ID}/' deploy.sh > replace.sh;
"""
// 和测试唯一的区别就是在这里,主要是需要跳板服务器去部署线上的服务器(没有外网IP)
// $SECRET_FILE是在Jenkins中保存的凭证,原本是可以直接保持ssh凭证然后使用对应的函数来获取的
// 但是无奈不知为何不能获取,就直接使用了这种简单暴力的文件的形式来读取了
// -oStrictHostKeyChecking=no 这个参数比较重要因为脚本执行的时候可不会自动输入yes
// ssh xxx@xxx -C "/bin/bash" < xx.sh 可以用来在远程服务器上执行脚本
// 在写sh脚本的时候选择使用 “” 还是 '' 来包含参数是很重要的,'' 的形式是完全不转义的也就是说不能使用sh的变量
// 注意: 需要在服务器上配置ssh免密
sh """
cat $SECRET_FILE > pc-libs
chmod 400 pc-libs
scp -i pc-libs -oStrictHostKeyChecking=no replace.sh libs@47.110.85.172:/home/libs/${SERVICE_NAME}.sh
ssh -oStrictHostKeyChecking=no -i pc-libs libs@47.110.85.172 \
'ssh -oStrictHostKeyChecking=no webs@${SERVER_IP} -C "/bin/bash" < /home/libs/${SERVICE_NAME}.sh';
"""
}
}
}
}

docker

下面命令在需要部署的服务器上执行:

1
2
3
4
$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh --mirror Aliyun
# 将当前用户加入 docker 组
$ sudo usermod -aG docker $USER

常用命令

  • docker ps : 查看运行中的容器
  • docker ps -a: 查看全部容器
  • docker start [容器id]: 启动容器
  • docker stop [容器id]: 停止容器
  • docker rm [容器id]: 删除容器
  • docker iamge ls: 查看本地iamge
  • docker rmi [镜像id]: 删除本地image

注意: 所有 [容器id] 的地方都可以用空格分割来对多个容器进行操作

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FROM java:8

ENV TimeZone=Asia/Shanghai

USER root
# 这里添加组和用户主要是线上服务是不用root启动的
# 如果不修改用户的话应用生成的日志都是root权限的无法操作
# 注意:简单来说,容器内外的所有用户是一样的,容器内的root可以看作就是服务器上的root
RUN mkdir /data \
&& groupadd -r webs \
&& useradd -d /data/webs -r -g webs webs \
&& mkdir -p /data/webs/common-data/logs \
&& mkdir -p /data/webs/common-data/logs/jvm-log \
&& chown -R webs:webs /data/webs \
# 应用的时区修改,否则日志时间相差8小时
&& ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime \
&& echo $TimeZone > /etc/timezone

USER webs
ADD common-data.jar common-data.jar
# 启动应用的命令, JAVA_OPTS 在启动脚本中配置,在上面的pipeline变量中应该已经看到
# 目的是为了将全部变量都放置在一起配置保证复用
ENTRYPOINT java ${JAVA_OPTS} -jar common-data.jar

部署脚本

deploy.sh

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 准备变量
nacos_ip='__nacos_ip__'
serviceName='__serviceName__'
ip='__ip__'
port='__port__'
namespaceId='__namespaceId__'
docker_repo='__docker_repo__'
docker_namespace='__docker_namespace__'
docker_username='__docker_username__'
docker_password='__docker_password__'
version='__version__'
JAVA_OPTS='__JAVA_OPTS__'
user='__user__'

# 停止spring应用
url='http://'$nacos_ip'/nacos/v1/ns/instance'
data='serviceName='$serviceName'&ip='$ip'&port='$port'&namespaceId='$namespaceId'&enabled=false'
echo "下线正在运行的服务"
curl $url -X PUT --data $data

# 等待10s确保没有流量
sleep 10

# 停止并删除docker运行的服务容器
containerId=$(docker ps -a | grep $serviceName | awk '{print $1}')
if [[ $containerId ]]
then
echo "删除正在运行的容器服务"
docker stop -t 60 "$containerId"
docker rm "$containerId"
fi

echo "删除本地服务镜像"
localimage=$(docker image ls | grep $serviceName | awk '{print $1":"$2}')
if [[ $localimage ]]
then
docker image rm "$localimage"
fi

echo "从镜像仓库拉取最新镜像"
docker login -u $docker_username -p $docker_password $docker_repo
image_name=$docker_repo/$docker_namespace/$serviceName:$version
docker pull $image_name
echo "运行docker服务"
mkdir -p /data/webs/$serviceName/logs
mkdir -p /data/webs/$serviceName/logs/jvm-log/
# 获取webs用户uid和gid
uid=$(id $user | awk '{print $1}' | grep -o "[[:digit:]]\{4\}")
gid=$(id $user | awk '{print $2}' | grep -o "[[:digit:]]\{4\}")
docker_id=$(docker run -v /data/webs/$serviceName/logs:/data/webs/$serviceName/logs \
-e JAVA_OPTS="$JAVA_OPTS" \
-p $port:$port -u "$uid:$gid" -d \
--name $serviceName --net host $image_name)

# 等待docker中java服务正式启动
sleep 60
# 查看服务是否启动
url='http://'$nacos_ip'/nacos/v1/ns/catalog/instances?'
data='serviceName='$serviceName'&clusterName=DEFAULT&groupName=DEFAULT_GROUP&pageSize=10&pageNo=1&namespaceId='$namespaceId
server_list=$(curl "$url$data" | python2 -c "import sys, json; res = [(str(d['ip'])+':'+str(d['port'])) for d in json.load(sys.stdin)['list']]; print res" | grep $ip | grep $port)
if [[ $server_list ]]
then
echo "--------服务启动成功-------"
else
echo "没有在注册中心获取到上线的服务,查看docker进程:$docker_id"
res=$(docker ps | grep $serviceName)
if [[ $res ]]
then
echo "docker进程包含服务容器,请重新检查服务是否上线"
echo "$res"
exit 1;
else
echo "--------服务启动失败!-------"
exit 1;
fi
fi

停止spring应用查看服务是否启动并不是必需的,但是也算是为了服务能够优雅下线做的尝试吧,也可以看到大部分的参数也是这两个步骤带进来的,如果只对docker容器进行操作的话整个脚本会简单很多

这也可以说是第一次写这么多行的脚本,但是实质还是不是很难的,主要用到的是awk,grep,if和管道|,还有一个一行处理json的python(还挺不错)

总体使用流程

  1. 在项目中编写Jenkinsfile和deploy.sh
  2. 在Jenkins中新建流水线项目,填写项目的地址并选择Jenkinsfile
  3. 需要部署项目的服务器安装docker并配置ssh免密登陆
  4. Jenkins中点击构建

总结

这次项目向docker化的持续集成的迁移改造,前前后后也大致是花费了一个多星期了,但是整个流程下来也算是能够对Jenkins和docker更加的熟悉一点了吧

不足

当然由于每个组的技术不可能完全一致,CI的方式也肯定不尽相同,上面的方案也是草草交了的答卷,肯定是有很多优化的地方的。

比如像构建并不是在监测到分支改动之后自动执行而是需要手动执行,代码并没有进行从单测到集成测试的任何测试,也没有经过代码质量审查,当然还有一些构建完成之后的工作也没有进行(比如没有通知构建),等等….

并且对上面的解决方案自我感觉也并不是特别良好,或者说感觉不够优雅,也是局限于业务规模,并不需要深入使用compose或者容器编排之类的技术,毕竟ci也是代码,是代码就决定它肯定不是一成不变的,也只能说多多自勉吧

吾尝终日而思矣 不如须臾之所学也

chp2.

lambda表达式的几种形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// () -> 表示没有参数且Runnable接口只有一个run方法
Runnable noArguments = () -> System.out.println("Hello World");

// 只有一个参数的表达式可以省略括号
ActionListener oneArgument = event -> System.out.println("button clicked");

// 表达式可以为代码块,用{}括起来
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};

// 包含多个参数,返回的add变量是代码 BinaryOperator而不是相加之后的结果(有点类似于将函数作为变量)
BinaryOperator<Long> add = (x, y) -> x + y;

// 括号内的参数也可以指定类型而不是让编译器来推断
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。

变量引用:

在lambda表达式中可以引用非final变量但该变量在既成事实上必须是final。也就是只能给该变量赋值一次,也就是值引用。

函数接口:

使用只有一个方法的接口来表示某特定方法并反复使用。

Java中重要的函数接口:

chp3.

外部迭代

内部迭代

lambda表达式采用惰性求值,在没有进行及早求知(reduce)之前不会进行前面的求值操作。 ex:

1
2
3
4
5
6
// 由于采用惰性求值,这个表达式并不会有输出
allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
});

常用流操作

  • collect(toList()) : 由Stream里的值生成一个列表,是一个及早求值操作。

  • map : 将一个流中的值转换成一个新的流。

  • filter :

    1
    2
    3
    4
    // ex
    List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1")
    .filter(value -> isDigit(value.charAt(0)))
    .collect(toList());
  • flatMap : 用Stream替换值,然后将多个Stream连接成一个Stream

  • max,min

    1
    2
    3
    4
    // ex:
    Track shortestTrack = tracks.stream()
    .min(Comparator.comparing(track -> track.getLength()))
    .get();
  • reduce

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 使用reduce求和
    int count = Stream.of(1, 2, 3)
    .reduce(0, (acc, element) -> acc + element);
    // 展开reduce
    BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
    int count = accumulator.apply(
    accumulator.apply(
    accumulator.apply(0, 1),
    2),
    3);

chp4.

labmda类型推导

默认方法

三定律
如果对默认方法的工作原理, 特别是在多重继承下的行为还没有把握, 如下三条简单的定律可以帮助大家。

  1. 类胜于接口。 如果在继承链中有方法体或抽象的方法声明, 那么就可以忽略接口中定义的方法。
  2. 子类胜于父类。 如果一个接口继承了另一个接口, 且两个接口都定义了一个默认方法,
    那么子类中定义的方法胜出。
  3. 没有规则三。 如果上面两条规则不适用, 子类要么需要实现该方法, 要么将该方法声明为抽象方法。

其中第一条规则是为了让代码向后兼容。

Optional

(这里的介绍有点简略)

chp5.

方法引用

1
2
3
4
// idea会在写出前一种形式的时候给出修改建议
artist -> artist.getName() ==> Artist::getName

(name, nationality) -> new Artist(name, nationality) ==> Artist::new

元素顺序

  • 在一个有序集合中创建一个流时, 流中的元素就按出现顺序排列
  • 如果集合本身就是无序的, 由此生成的流也是无序的

收集器

  • 使用 toCollection, 用定制的集合收集元素 -> stream.collect(toCollection(TreeSet::new));
  • 生成值
    • maxBy -> artists.collect(maxBy(comparing(getCount)));
    • averagingInt -> .collect(averagingInt(album -> album.getTrackList().size()));
  • 分块(partition)-> return artists.collect(partitioningBy(Artist::isSolo));
  • 分组(groupby)
    1
    2
    3
    4
    // 返回Map k,v value为对象的List
    public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
    return albums.collect(groupingBy(album -> album.getMainMusician()));
    }
  • 字符串 -> .collect(Collectors.joining(", ", "[", "]")); joining(分割符,前缀,后缀)
  • 组合收集器(下游收集器)
    1
    2
    3
    4
    5
    6
    // counting
    albums.collect(groupingBy(album -> album.getMainMusician(),
    counting()));
    // mapping
    return albums.collect(groupingBy(Album::getMainMusician,
    mapping(Album::getName, toList())));
0%