写在前面

在看了许多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())));

话到嘴边又不知道该怎么描述,也算是拾起了很久没用的博客吧,这也算是自己少见的文字输出方式了所以还是来写一写.

之所以会写这周搞的一些新东西这样的博客,主要是这周摸🐟的时光似乎有些多了,也是四处搞了很多新东西和一些之前遗留的事,就来记录一下吧.

敏捷学习仓库合并

在许久之前看了基于GitHub的敏捷学习方法之道与术之后就深受影响然后搞了一个仓库Agile-Learning也坚持搞了一段时间(MySQL文档翻译就是这样促成的),但是过年回来之后惰性发作也就许久没有再去动这个仓库也就闲置了许久,最近打算重新使用之后发现private的仓库不能无偿使用wiki,也就借着public的机会把这个仓库和github.io的仓库给合并了顺便整理了一下吧.

因为写博客的分支是在backup上的,所以对博客访问也没有什么影响,也是方便自己管理,之前没有public敏捷学习的仓库也是因为打算实践一段时间找到自己的使用方式之后再public出来,现在也是想用wiki就用public的方式来督促一下自己吧算是.

用wiki整合知识

承接上文现在也是打算吧一些开发和日常过程中自己Google过的不熟悉的东西整理到wiki里面去了(毕竟记性不好,还是写下来靠谱):
个人知识点整理用wiki

对于wiki的话其实也就是一个MarkDown文件的文件夹,大致的目录如下:

主要是*_Footer.md*,_Sidebar.md,Home.md三个文件,需要注意的是没有Home.md这个文件的话进入wiki看到的就是文件列表了.还有一点需要吐槽的是wiki默认的git方式是https的,需要自己手动改成ssh方式的不然就还是需要账号密码而不能用密钥的方式了.

博客HTTPS

关于https,很早之前就已经用上了,当时使用的是cloudflare的免费HTTPS方式,但是现在发现GitHub Pages已经自己支持HTTPS了,就改用GitHub自带的了:

VSCode remote

因为现在一直在用VSCode写MarkDown而在经历了整合仓库之后在clone的时候发现网速巨慢,大概需要10-30min才能把仓库clone下来,实在是无法忍受之后想到了用阿里云的服务器来写博客,之前也是对VSCode remote略有耳闻就Google并且尝试了一下,目前来看在写Markdown这一块还是很不错的并且由于文件保存在服务器上所以哪怕没有保存的修改也不会丢失.

步骤

  1. 下载openssh:OpenSSH-Win64.zip
  2. 添加到Path
  3. PowerShell使用ssh xxx@xxx.xxx.xxx尝试连接服务器
  4. VSCode安装Remote的插件
  5. 在本地的.ssh/config文件下进行服务器的配置:
  6. 完成之后如下就已经可以了:

Windwos Terminal && WSL

先来一个效果图吧(想自定义主题的但是目前好像文章不是很多还得好好研究一下):

Win10下的安装也很简单就不在赘述了,下面就放一下配置文件好了:

1
2
3
4
5
6
7
8
9
{
"globals" :
{
"alwaysShowTabs" : true,
"copyOnSelect" : false,
"defaultProfile" : "{9acb9455-ca41-5af7-950f-6bca1bc9722f}",
"initialCols" : 120,
"initialRows" : 30,
"keybindings" :

这里主要是defaultProfile这个字段,对应下面每个terminal类型的guid,我改成了默认启动WSL的窗口.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"profiles" : 
[
{
"acrylicOpacity" : 0.75,
"closeOnExit" : true,
"colorScheme" : "Vintage",
"commandline" : "wsl.exe",
"cursorColor" : "#FFFFFF",
"cursorShape" : "bar",
"fontFace" : "Fira Code",
"fontSize" : 10,
"guid" : "{9acb9455-ca41-5af7-950f-6bca1bc9722f}",
"historySize" : 9001,
"icon" : "ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png",
"name" : "WSL",
"padding" : "0, 0, 0, 0",
"snapOnInput" : true,
"startingDirectory" : "//wsl$/Ubuntu/home/tang",
"useAcrylic" : true
},

这是WSL的配置文件,icon这个是需要更换的,目录是在(我的文件夹权限有点问题就用命令行来查看了):

guidicon一起改掉就ok了.

我还更换了一下字体:"fontFace" : "Fira Code", 关于字体可以自行Google.

终端主题在下面的"schemes" : 里有自带默认的几个风格.修改到"colorScheme" : "Vintage",这个字段就可以应用.

可以看出这一次巨硬还是很良心的,而且配置文件也很容易修改,可以说是Win开发的一大福音了.

id_rsa

以前的我只知道莽,无脑生成密钥就完事了.

但是在经历了许多环境和服务器的摧残之后,还是认识到了使用一套自己的密钥的重要性了,于是在生成了一套之后把GitHub和服务器都替换了一下,现在就very方便了(顺便保存在了1password里).

Pi_hole

可以查看示例:Pi_hole Admin

知道这东西是在老莱的频道发现的:【官方双语】世界清净了…用树莓派屏蔽所有在线广告!#linus谈科技

感觉还挺有意思的但是介于没有树莓派然后又想到服务器不是有公网ip吗,于是就在阿里服务器上搭了一个,一开始感觉还非常不错但是在经历了更多网页的洗礼之后发现还是不太行,现在用的是adblock+Pi hole的双重过滤方式.

需要注意的是如果在服务器上部署需要在安全组里开放53的UDP端口,是DNS需要的端口.

安装的话由于有一键脚本,直接跟着做就成了所以也就不在赘述了.

1
curl -sSL https://install.pi-hole.net | bash

(后期补充:docker脚本)

1
2
3
4
5
6
7
8
9
10
11
docker run -d \
--name pihole \
-p 53:53/tcp -p 53:53/udp \
-p 8081:80 \
-p 443:443 \
-e TZ="Asia/Shanghai" \
-v "$(pwd)/etc-pihole/:/etc/pihole/" \
-v "$(pwd)/etc-dnsmasq.d/:/etc/dnsmasq.d/" \
--dns=127.0.0.1 --dns=1.1.1.1 \
--restart=unless-stopped \
pihole/pihole:latest

自建RSS

我也算是个轻度的RSS使用者了吧,之前一直在使用Inoreader但是最近发现广告变多了而且免费账户着实有些弟弟,就想着反正有个服务器就克服一下懒癌尝试一下吧.

然后Google了一顿还是发现之前看过的:如何搭建属于自己的 RSS 服务,高效精准获取信息感觉还不错就跟着做了,不得不说:真香. 在docker的加持之下安装是非常之简单了,唯一的坑是Mercury 的全文插件,现在已经需要自建了, 然后参考了:通过Docker安装Tiny RSS,mercury全文插件及mercury-parser-api;申请域名通配符证书,安装nginx并配置ssl 使用docker安装了Mercury Parser API.

直接使用

1
docker run -p 3000:3000 -d --restart=always  wangqiru/mercury-parser-api

然后配置API地址就可以了,当然之前是使用API KEY的形式.

然后把Inoreader的订阅源导入过来就成了,有一点值得吐槽的是Inoreader导出的opml文件竟然是xml格式的,然后直接导入ttrss就会失败,但是改一下后缀就ok了.

TabNine

据说是杀手级AI代码补全工具,毕竟现在是”AI大数据”的时代就下载下来蹭一下热度呗,实际的使用体验还算是不错,写Markdown的时候竟然也会提示单词的补全,好评.

在idea也装了插件,补全智能程度还算是非常的不错,在填参数的时候甚至会帮你补全后面的’)’.

介绍的文章也很多我也就不再唠叨用就完事了(毕竟我只是个记录贴 :)

0%