小不的笔记

时间之外的往事

查看Linux发行版

TL;DR

1
2
3
cat /etc/*-release
lsb_release -a
hostnamectl

我们在讲Linux的时候,我们通常是在说Linux 发行版. 严格来说,Linux只是一个内核, 操作系统最核心的部分,是软件应用与硬件的桥梁。一个Linux发行版是由Linux内核、GNU工具及库和软件集合构成。通常Linux发行版包括桌面环境、包管理系统还有一系列的预安装的软件。 比较流行的Linux发行版有: Debian、Red Hat、Ubuntu、Arch Linux、Fedora、CentOS、Kali Linux、OpenSUSE、Linux Mint、Alpine等等。 当你第一次登入一个Linux系统时,最好在做事之前先看看系统的Linux的版本。至少知道了是什么发行版,我们才知道应该用什么包管理工具。 以下就是演示如何通过命令行来获取Linux发行版及版本信息。

/etc/os-release 文件

1
cat /etc/os-release

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

lsb_release 命令

1
lsb_release -a

输出示例:

1
2
3
4
5
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.1 LTS
Release: 20.04
Codename: focal

/etc/lsb_release 文件是一些Linux发行版为了兼容旧程序而创建的。lsb是Linux Standard Base的缩写, 它叫做Linux基础标准,但实际它并不是标准。只有部分发行版在使用它。 然而/etc/os-release 文件是标准,一种基于systemd的标准。任何一个基于systemd的Linux发行版都有这个文件。 所以CentOS6上有/etc/lsb_release, CentOS7上有/etc/os-release,所以在不确定是什么系统时可以直接

1
cat /etc/*-release

如果使用了什么小众的发行版,上面的命令都不好用,还可以尝试这个命令:

1
echo /etc/*_ver* /etc/*-rel*; cat /etc/*_ver* /etc/*-rel*

release-files

hostnamectl 命令

1
hostnamectl

输出示例:

1
2
3
4
5
6
7
8
9
10
 Static hostname: localhost.localdomain
Icon name: computer-vm
Chassis: vm
Machine ID: 7c5b5acceb4844b1be6d38ffe8701c30
Boot ID: 8e3a6b1d077e46759b094f0c610222a2
Virtualization: vmware
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-1062.el7.x86_64
Architecture: x86-64

参考链接: https://linuxize.com/post/how-to-check-linux-version/ https://stackoverflow.com/questions/47838800/etc-lsb-release-vs-etc-os-release

Flyway migrate晚于JPA建表语句

Spring Boot下通过EntityManagerFactoryDependsOnPostProcessor来确保Flyway的初始化执行晚于JPA。但是有时候我们会希望由JPA完成表结构的维护,Flyway用来修数据、基础数据的维护。这个时候如果flyway执行早于JPA的表结构维护,可能会导致表或字段不存在的异常。 所以我们重新实现FlywayMigrationStrategy的逻辑,在正常migrate时,不做具体事情,把flyway保存下来,在Spring完成初始化后ContextRefreshedEvent再执行migrate

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
package org.xobo.configuration;

import org.flywaydb.core.Flyway;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;

@Configuration
public class MigrationConfiguration {

public static class MyFlywayMigrationStrategy implements FlywayMigrationStrategy {
private Flyway flyway;
private boolean hasMigrated;

public MyFlywayMigrationStrategy() {

}

@Override
public void migrate(Flyway flyway) {
this.flyway = flyway;
}

@EventListener
public void handleContextRefresh(ContextRefreshedEvent event) {
if (!hasMigrated && flyway != null) {
flyway.migrate();
hasMigrated = true;
}
}
}

@Bean
FlywayMigrationStrategy FlywayMigrationStrategy() {
return new MyFlywayMigrationStrategy();
}
}

Spring Boot 构建docker镜像

Spring官网提供了一个简明的基于SpringBoot项目构建docker镜像的教程(Getting Started Spring Boot with Docker)。这个教程给我了很大的启发,结合我现在实际项目,又进行一些改造。

引入多阶段构建

原始教程都是基于构建好的jar包做操作,这样就要求宿主机配置好JDK和Maven,这样就存在在不同的宿主机下,可能构建出不同的镜像,不够The Docker Way。 通过多阶段构建我们一个在一个 Dockerfile 里使用多个FROM。每一个FROM都可以使用不同的基础镜像,没一个都开启一个构建的新阶段。 我们可以选择性的复制构建物从一个阶段到另一个,这样在最终的镜像里就不会有我们不需要的中间产物了。

1
2
3
4
5
6
7
8
9
10
FROM maven:3.8.2-adoptopenjdk-8 AS builder
WORKDIR /build
COPY . .
RUN mvn clean package

FROM adoptopenjdk:8-jdk-hotspot
COPY --from=builder /build/target/*.jar /app/
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

减少文件复制

构建时.git,target等目录是不需要的,创建.dockerignore文件:

1
2
3
4
.git/
target/
.idea/
Dockerfile

减少与Maven服务器交互

如果项目严格按照每次部署jar包时版本号都改变,可以认为pom文件没发生变化时maven不需要向服务器更新依赖. mvn dependency:go-offline 可以下载所有的项目依赖,后续的所有mvn操作我们都可以添加-o参数,maven就工作的离线模式,不再需要和maven服务器交互,提高构建速度。

1
2
3
4
5
6
7
8
9
10
11
FROM maven:3.8.2-adoptopenjdk-8 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY . .
RUN mvn -o clean package

FROM adoptopenjdk:8-jdk-hotspot
ARG JAR_FILE=target/*.jar
COPY --from=builder ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

缓存maven repository

现在每个项目的构建都有一个独立的本地maven repository,极大的浪费磁盘空间,同时也浪费网络带宽。共享maven repository有两个选择: 一个是通过挂载VOLUME,或者利用BuildKit(>=18.09)的Cache. VOLUME:

1
2
VOLUME "/root/.m2"
RUN mvn xxx

BuildKit:

1
RUN --mount=type=cache,id=mvnrepo,target=/root/.m2 mvn xxx

启用BuildKit

docker默认是不启用BuildKit的。可以在执行docker build之前设置环境变量DOCKER_BUILDKIT=1,例如:

1
DOCKER_BUILDKIT=1 docker build .

如果想默认启用BuildKit,可以通过配置/etc/docker/daemon.json,然后重启daemon。

1
{ "features": { "buildkit": true } }

更精细的镜像分层

原始教程镜像分为/BOOT-INF/lib,/META-INF,/BOOT-INF/classes 三层。对于普通项目来说是够用的,但是我司有一些自有jar包,更新频率高于三方的外部库,同时低于业务代码。为最大程序利用 docker 构建缓存及镜像层,我把/BOOT-INF/lib分为两层,一层是外部依赖的jars,一层是公司的jars。 jar包的分层我是通过maven的dependency:copy-dependencies并指定excludeGroupIdsincludeGroupIds来实现的。

1
2
mvn dependency:copy-dependencies -DexcludeGroupIds=org.xobo -DoutputDirectory=./target/lib/3rd
mvn dependency:copy-dependencies -DincludeGroupIds=org.xobo -DoutputDirectory=./target/lib/xobo

额外要注意: 1. 如果项目有更精细的分层需求,有一点要注意,COPY 虽然支持模糊匹配,但是在复制目录的时候会把匹配上的_目录内的内容_复制到目的文件夹下。这样文件夹目录少了一层。 2. Maven 3.8.1禁止了HTTP repository, 必须使用HTTPS协议。 可以升级HTTP为HTTPS,或者添加一个mirror,或者版本降到3.6.3。 3. 不要追求镜像的绝对最小,像alpine这种镜像很多命令、基础库都和我们日常使用的Linux不一样,虽然一共能省了不到100M空间(docker基础层是共享的)但留下很多隐患。

最终的Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM maven:3.8.2-adoptopenjdk-8 AS builder
WORKDIR /build
COPY pom.xml .
RUN --mount=type=cache,id=mvnrepo,target=/root/.m2 mvn dependency:go-offline
COPY . .
RUN --mount=type=cache,id=mvnrepo,target=/root/.m2 mvn -B -o clean package && \
mvn -o dependency:copy-dependencies -DexcludeGroupIds=org.xobo -DoutputDirectory=./target/lib/3rd && \
mvn -o dependency:copy-dependencies -DincludeGroupIds=org.xobo -DoutputDirectory=./target/lib/my && \
jar xf ./target/demo.jar

FROM adoptopenjdk:8-jdk-hotspot
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN adduser --system --no-create-home --group app
USER app:app
COPY --from=builder --chown app:app /build/target/lib/3rd /app/lib
COPY --from=builder --chown app:app /build/target/lib/my /app/lib
COPY --from=builder --chown app:app /build/BOOT-INF/classes /app
COPY --from=builder --chown app:app /build/META-INF /app/META-INF

EXPOSE 8080
ENTRYPOINT ["java","-cp","app:app/lib/*","org.xobo.DemoApplication"]

Nginx的if

Nginx的配置是一种声明式的配置,虽然有if指令,但是跟传统意义上的编程语言是完全不同的。在实际使用的过程中,使用if指令会产生有很多很奇怪的事情,由其是在对它没有充分了解之前,经常会误用。Nginx官方也提醒在location里使用if是灾难,如果一定要使用if,需要测试测试再测试. If is Evil… when used in location context 对于声明式的配置来说: 1. Nginx的if指令条件满足时,会在if块里创建一个嵌套的子location块,然后再执行if块内的声明和内容处理器(content handler)。 2. 在一个配置块里只有一个if语句和一些其他的声明,由于if的存在,会导致有些声明不生效。为了保证这些声明都生效,最好在if块内,重新声明一次。 3. 在一个配置块里,如果有两个if指令都满足条件,只有第二个if会被执行。 Case 1

1
2
3
4
5
6
7
8
9
10
11
12
location /case1 {
set $a 32;
if ($a = 32) {
set $a 56;
}
set $a 76;
proxy_pass http://127.0.0.1:$server_port/$a;
}

location ~ /(\d+) {
echo $1;
}

访问/case1时会返回76.

1
2
~ curl localhost/case1
76

Nginx 是这样工作的: 1. Nginx先按照配置顺序执行所有的rewrite指令:

1
2
3
4
5
set $a 32;
if ($a = 32) {
set $a 56;
}
set $a 76;

$a最后值是76

  1. 因为条件$a = 32成立,所以Nginx就进入if的内部块。
  2. 这个内部块没有任何内容处理器(content handler),ngx_proxy 就会继承外部块的内容处理器(content handler)。(see src/http/modules/ngx_http_proxy_module.c:2025).
  3. 同时配置指定的proxy_pass也会被if内部块继承。(see src/http/modules/ngx_http_proxy_module.c:2015)
  4. 请求结束。(其实流程进了if块再也没出来)

所以,外面的proxy_pass指令并没有执行,真正提供服务的是if内部块。 如果我们的if块声明了内容处理器(content handler)会发生什么 Case2

1
2
3
4
5
6
7
8
9
10
11
12
13
location /case2 {
set $a 32;
if ($a = 32) {
set $a 56;
echo "a = $a";
}
set $a 76;
proxy_pass http://127.0.0.1:$server_port/$a;
}

location ~ /(\d+) {
echo $1;
}

当我们访问/case2时,我们会得到

1
2
~ curl localhost/case2
a = 76

这中间都发生了什么: 1. Nginx先按照配置顺序执行所有的rewrite指令:

1
2
3
4
5
set $a 32;
if ($a = 32) {
set $a 56;
}
set $a 76;

$a最后值是76

  1. 因为条件$a = 32成立,所以Nginx就进入if的内部块。
  2. 这个内部块有内容处理器(content handler)echo,所以$a的值(a = 76)就返回给了客户端。
  3. 请求结束。(流程进了if块再也没出来,和Case1一样)

我们再调整一下: Case 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
location /case3 {
set $a 32;
if ($a = 32) {
set $a 56;
break;

echo "a = $a";
}
set $a 76;
proxy_pass http://127.0.0.1:$server_port/$a;
}

location ~ /(\d+) {
echo $1;
}

这一次我们在if块内添加了break指令. 它会阻止nginx的ngx_rewrite指令的执行。因此我们得到

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

~ curl localhost/case3 a = 56

````
这一次Nginx是这样工作的:
1. Nginx先按照配置顺序执行所有的rewrite指令:
```Shell
set $a 32;
if ($a = 32) {
set $a 56;
break;
}
````

$a最后值是56

1. 因为条件`$a = 32`成立,所以Nginx就进入`if`的内部块。
2. 这个内部块有内容处理器(content handler)`echo`,所以$a的值(a = 56)就返回给了客户端。
3. 请求结束。(流程进了`if`块再也没出来,和Case1一样)

还有一个`if`块继承配置的场景: case 4:

location /case4 {
set $a 32;
if ($a = 32) {
return 404;
}
set $a 76;
proxy_pass http://127.0.0.1:$server_port/$a;
more_set_headers “X-Foo: $a”;
add_header X-Bar $a;
}

location ~ /(\d+) {
echo $1;
}

1
2
3

访问的时候需要添加`-i`参数,这样就可以打印出HTTP响应头。

~ curl -i localhost/case4
HTTP/1.1 404 Not Found
Server: openresty/1.19.9.1
Date: Thu, 09 Sep 2021 07:28:48 GMT
Content-Type: text/html
Content-Length: 159
Connection: keep-alive
X-Foo: 32

404 Not Found

404 Not Found


openresty/1.19.9.1
1
2
3

只有`X-Foo` Header被添加了,`X-Bar` Header却没有 这个可能是你想要的或者不是 这里`ngx_header_more`的`more_set_headers`也会被继承 `add_header`指令虽然忽略X-Foo header,但是它依旧被继承了,是因为`add_header`的header filter会忽略4XX响应。 :-p 注: `echo`,`ngx_header_more`模块都是三方模块,如果需要测试以上示例,建议使用openresty. 或者直接使用 docker 测试

git clone https://github.com/cnxobo/nginxifisevil.git docker build -t nginxifisevil . docker run --rm -d -p 80:80 --name nginxifisevil nginxifisevil curl localhost/case1 curl localhost/case2 curl localhost/case3 curl localhost/case4 curl -i localhost/case4 ```

相关博客: Nginx proxy_pass 的URL Mapping 参考资料: How nginx “location if” works Human & Machine How does if condition work inside location block in nginx conf? - Stack Overflow

dorado 属性配置

dorado有一套自身的属性加载机制,dorado-home下的configure.propertes和configure-debug.properties及通过dorado插件机制加载的配置文件) dorado的el表达式取属性值${configure["core.runMode"]}只能取ConfigureStore内的值。 >= 7.6.1-SNAPSHOT com.bstek.dorado.core.ConfigurePropertiesConfigurer

com.bstek.dorado.core.ConfigureProper[t]iesConfigurer 7.6.1-SNAPSHOT之后修正了拼写错误,把丢失的t补上了。

属性的加载顺序

  1. 先把dorado ConfigureStore的属性值存入Properties。 2) 遍历所有Spring加载到的属性,找到 dorado. 开头的,移除dorado.前缀然后重新存入ConfigureStore和步骤1创建的Properties内。 3) 然后基于Properties创建PropertiesPropertySource然后加入propertySources集合的最后。 简单的理解为:
  1. dorado的配置文件优先级最低。

  2. 如果希望在Spring Boot 的application.properties里创建一个可以被dorado el表达式读取的的配置。就需要创建一个以dorado.开头的配置。比如配置属性dorado.name=xobo, 那么ConfigureStore会新增一个配置(name=xobo),Spring Boot会新增两个配置 dorado.name=xobo 和 name=xobo.

<7.6.1-SNAPSHOT com.bstek.dorado.core.ConfigureProperiesConfigurer 该版本省略了第二步,也就是只能dorado的配置文件导入Spring项目,但是Spring项目的配置无法导入dorado的,而且改类继承自PropertyPlaceholderConfigurer,Spring3.1之后就不推荐使用了,不多做分析。

dorado ide 更新规则

dorado eclipse 插件 百度网盘: [https://pan.baidu.com/s/1chalW5ebFOC3cKkYJLvkig](https://pan.baidu.com/s/1chalW5ebFOC3cKkYJLvkig  “百度网盘” target=”_blank”) 提取码: xobo

dorado可以自定义控件,所以dorado ide 在设计的时候并没有把控件写死,而是采用基于配置的方式来加载控件及控件对应的事件、属性。 dorado配置规则是基于当前项目动态生成的,然后保存在项目根目录的.rules文件里。控件对应的图标则保存在.setting目录里。这个设计也带来了弊端–每个新项目都要手动更新一下dorado配置规则。 dorado的配置规则更新有两方式:离线、在线。

离线模式

IDE的默认方式,需要指定一个dorado home目录,然后IDE通过加载Spring配置文件,解析出dorado控件信息。该模式适合简单项目,我经常会遇到有自定义控件加载不到的情况。 配置方式:

  1. 在项目名上点右键,然后选择”Update Dorado Config Rules”(更新dorado配置规则)。

  2. 在弹出框里选择Dorado Home目录。BDF2项目通常在\web\WEB-INF\dorado-home,BDF3项目通常在\src\main\resources\dorado-home位置。

在线模式

个人推荐。需要先启动dorado项目。dorado ide会访问项目的/dorado/ide/config-rules.xml路径,获取dorado的配置规则,然后保存到本地。这个模式是加载最完整的。 配置方式:

  1. 启动项目

  2. 先在Eclipse的Preference的Dorado7 Studio,里把更新规则的模式切换成Online (在线模式).

Windows下菜单Window -> Preferences MacOS下,快捷键⌘+,

  1. 在项目名上点右键,然后选择”Update Dorado Config Rules”(更新dorado配置规则)。

  2. 在弹出框里填写服务器的地址。

    Server Name 填域名或者IP,不带http/https前缀 Port 填端口号 AppName 填context path,如果是直接访问可以留空。

在线模式下 ServerName对应着域名或ip, Port就是端口号, App Name就是contextpath,如果为空可以不填。图片中是示例访问地址为: http://localhost:8080/waterdrop 。如果 项目访问地址是: http://localhost:8080

Server Name: localhost Port: 8080 App Name:

但是也是最需要容易踩坑的:

  • 系统必须可以匿名访问/dorado/ide/config-rules.xml路径

  • 由于dorado IDE的bug,其更新规则时真实的请求是POST //dorado/ide/config-rules.xml,路径中出现双斜杠。双斜杠这种非标准的路径可能会被防火墙拦截,比如Spring Security的HttpFirewall。这个时候需要注册一个我们自己的HttpFireWall,然后允许路径中出现双斜杠。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Bean
    public HttpFirewall looseHttpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    // 允许路径中带双斜杠("//")
    firewall.setAllowUrlEncodedDoubleSlash(true);

    // 清空所有策略,不建议使用。
    // firewall.getEncodedUrlBlacklist().clear();
    return firewall;
    }

附录: HttpFirewall 拦截的异常信息

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
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String "//"
at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlacklistedUrls(StrictHttpFirewall.java:369)
at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:336)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:194)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:367)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1639)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)

Nginx proxy_pass 的URL Mapping

Nginx 从Web服务器起步,现在越来越多的承担起反向代理服务器和负载均衡器的角色。proxy_pass 也就成了很最常用的指令之一,但是proxy_pass中,请求URI与传递给后端服务器的URI的转化是很容易弄错的地方,本文通过各种示例展示这个URI的转化规则。

一个简单的例子

假设我们有一个前后分离的应用,前端静态文件存放在/srv/www/myapp目录,后端API访问地址是http://127.0.0.1:8080。后端提供API接口/hello,同时前端访问后端时,统一加/api前缀,那这个时候,我们就可以配置为:

1
2
3
4
5
6
location / {
root /srv/www/myapp;
}
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}

现在,就通过 http://127.0.0.1 访问到/srv/www/myapp里的静态文件(假设Nginx监听端口80),访问http://127.0.0.1/api/hello时,请求都会被转发给http://127.0.0.1:8080/hello。 如果我们希望保留/api/路径那么只需要把proxy_pass配置的URI的path删掉就可以了。

1
proxy_pass http://127.0.0.1:8080;

URI的在与不在

URI在这里不是指完整的URL,而是proxy_pass 指定的URL内服务器地址或端口(如果存在)后面的地址,也就是标准URL定义中的path。 URI = scheme:[//[userinfo@]host[:port]]path[?query][#fragment] uri.png 例如:

proxy_pass http://127.0.0.1:8080/,URI是/; proxy_pass http://127.0.0.1:8080/api, URI是/api; proxy_pass http://127.0.0.1:8080, URI不存在。 请求URI发起请求的URL内服务器地址或端口(如果存在)后面的地址。例如: http://127.0.0.1/api/hello, 请求URI是/api/hello上游服务器,被代理服务器,或者说是后端服务器。

指定了URI

  1. 如果proxy_pass指令指定了URI请求URI中匹配location的部分在传递给上游服务器时被URI替换掉。 Case 1
1
2
3
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}

请求http://127.0.0.1/api/hello, 上游服务器就会收到http://127.0.0.1:8080/hello。_(/api/ -> /)_ Case 2

1
2
3
location /api/ {
proxy_pass http://127.0.0.1/remote/;
}

请求http://127.0.0.1/api/hello, 上游服务器就会收到http://127.0.0.1:8080/remote/hello(/api/ -> /remote/) Case 3

1
2
3
location /api/ {
proxy_pass http://127.0.0.1/remote;
}

请求http://127.0.0.1/api/hello, 上游服务器就会收到http://127.0.0.1:8080/remotehello。_(/api/ -> /remote)_

没有指定URI

  1. 如果proxy_pass指令没有指定URI,那么请求URI就会原样的传递给上游服务器。
1
2
3
location /api/ {
proxy_pass http://127.0.0.1:8080;
}

请求/api/hello, 上游服务器就会收到/api/hello

结尾的/

location的值是否以/结尾,proxy_pass的值是否以/结尾可以组合出成N多种可能。但是不管怎么组合,只有一个会影响匹配规则: URI的在与不在。 location的值最好以/结尾。如果location的值如果没有以/结尾,例如/api,那么URL/apixxx也会匹配上。除非明确需要匹配这种/apixxx场景。从最小配置的原则上,建议location的值都是以/结尾。 proxy_pass的URI,根据项目实际需要赋值或留空。如果有值,那么结尾通常都需要与location的值保持一致–要么都有/,要么都没有,不然会拼接出黏连在一起的路径(/remotehello)或者出现两个/的路径(/api//hello)。

更多的Case

Case #

Nginx location

proxy_pass URL

Test URL

Path received

1

/api1

http://127.0.0.1:8080

/api1/abc/api

/api1/abc/api

2

/api2

http://127.0.0.1:8080/

/api2/abc/api

//abc/api

3

/api3/

http://127.0.0.1:8080

/api3/abc/api

/api3/abc/api

4

/api4/

http://127.0.0.1:8080/

/api4/abc/api

/abc/api

5

/api5

http://127.0.0.1:8080/app1

/api5/abc/api

/app1/abc/api

6

/api6

http://127.0.0.1:8080/app1/

/api6/abc/api

/app1//abc/api

7

/api7/

http://127.0.0.1:8080/app1

/api7/abc/api

/app1abc/api

8

/api8/

http://127.0.0.1:8080/app1/

/api8/abc/api

/app1/abc/api

9

/

http://127.0.0.1:8080

/api9/abc/api

/api9/abc/api

10

/

http://127.0.0.1:8080/

/api10/abc/api

/api10/abc/api

11

/

http://127.0.0.1:8080/app1

/api11/abc/api

/app1test11/abc/api

12

/

http://127.0.0.1:8080/app2/

/api12/abc/api

/app2/api12/abc/api

13

/api

http://127.0.0.1:8080

/api13/abc/api

/api13/abc/api

14

/api

http://127.0.0.1:8080/remote

/api14/abc/api

/remote14/abc/api

更复杂的路径映射

正则表达式

使用正则表达式可以很方便的重组请求URI。例如: 对外URI是http://localhost:8080/api/cart/items/123,而上游的API处理的格式是http://localhost:5000/cart_api?items=123。这个时候就可以使用正则表达式去捕获需要的部分,然后转化成希望的格式。

1
2
3
location ~ ^/api/cart/([a-z]*)/(.*)$ {
proxy_pass http://127.0.0.1:5000/cart_api?$1=$2;
}

rewrite

也可以在location里如果使用rewrite修改了URI:

1
2
3
4
location /name/ {
rewrite /name/([^/]+) /users?name=$1 break;
proxy_pass http://127.0.0.1;
}

在这个场景下,指定的URI会被忽略,完整的修改后的URI会传递给被代理的服务器。

使用变量

也可以指定Nginx变量来

1
2
3
location /name/ {
proxy_pass http://127.0.0.1$request_uri;
}

如果你想把服务器地址定义为变量的话,还需要额外指定一个resolver也就是DNS服务器,为了安全不建议使用公共的DNS例如:8.8.8.8, 223.5.5.5等等。

1
2
3
4
5
location /proxy {
resolver 127.0.0.1;
set $target http://proxytarget.example.com;
proxy_pass $target;
}

If 引发的bug

nginx < 1.7.9 有个bug。如果在location里使用if,会导致proxy_pass不按照预期工作。proxy_pass 的URI叠加request_uri。 就像最开始的例子访问http://127.0.0.1/api/hello时,请求都会被转发给http://127.0.0.1:8080//api/hello

在docker内设置内存与CPU限制

0x01 总览

在生产环境中,为了保证服务器不因某一个软件导致服务器资源耗尽,我们会限制软件的资源使用。同样的在使用docker的时候,我们可以对docker镜像限制内存与CPU资源限制。

0x02 使用docker run 时设置资源限制

我们在使用docker run的时候可以直接设置资源限制。

2.1 内存

假如我们希望容器的内存最多只使用512M。 我们通过 --memory-m 参数设置内存限制。 参数后面跟一个数字,单位可以是b、 k、 m 或者 g, 最小值是 4M。

1
docker run -m 512m nginx

我们也可以设置一个保留内存(memory reservation)。 这个值要小于--memory值。当docker检测到宿主机上内存较少的或存在内存竞争(memory contention)时才会启用该限制。但是因为是软限制,所以无法保证docker容器不超出这个限制。

1
docker run -m 512m --memory-reservation=256m nginx

2.2 CPU

默认情况下,CPU的算力是不受限制的。我们可以通过cpus参数设置CPU限制。例如,我们限制容器最多使用两个CPU:

1
docker run --cpus=2 nginx 

我们也可以指定CPU分配的优先级。默认优先级是1024, 更高的数值的优先级更高:

1
docker run --cpus=2 --cpu-shares=2000 nginx

0x03 通过docker-compose 文件设置内存限制

我们也可以通过docker-compose配置文件实现类似的效果。要注意的是,对于不同版本的docker-compose格式会有不同的。

3.1 docker-compose API v3+ with docker swarm

给nginx服务限制0.5个CPU和512M内存,并且设置保留CPU为四分之一,保留内存为128M。我们需要在配置文件里创建 deployresources 配置段:

1
2
3
4
5
6
7
8
9
10
11
services:
service:
image: nginx
deploy:
resources:
limits:
cpus: 0.50
memory: 512M
reservations:
cpus: 0.25
memory: 128M

deploy 配置是API v3新加的,而且只有通过docker stack deploy在使用Swarm时环境下使用才生效。

1
docker stack deploy --compose-file docker-compose.yml bael_stack

如果希望使用API v3又不想使用Swarm的话,docker-compose在1.20.0+提供了一个--compatible参数。开启该参数Docker Compose就会尝试把API v3配置里deploy相关配置转化为等价的非Swarm配置API v2

1
docker-compose --compatibility up

同时可以通过config命令查看转化后的配置文件。(根据我的测试转化deploy配置需要docker-compose <=1.26.0, 新的版本取消了该功能)

1
docker-compose --compatibility config

3.2 docker-compose API v2

在 docker-compose里,我们可以把资源限制放在service的主属性上.他们名字也略有差异:

1
2
3
4
5
6
7
service:
image: nginx
mem_limit: 512M
mem_reservation: 128M
cpus: 0.5
ports:
- "80:80"

这样我们就可以直接通过docker-compose up启动了。

0x04 验证资源使用

在我们设置过资源限制之后,我们可以通过docker stats 命令来验证设置是否生效。

1
2
3
$ docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
8ad2f2c17078 bael_st 0.00% 2.578MiB / 512MiB 0.50% 936B / 0B 0B / 0B 2

0x05 总结

docker-compose的两个版本v2和v3并不是替代关系。v3是为了兼容Compose和Swarm设计的,所以v3版本里的deploy和容器的配置并不是1-1的匹配的。而且v2并不比v3老(v2.3 和 v3.4 发布时间差不多。),所以这两个版本的API是基于不同目的来实现的。 如果没有使用Swarm的计划,建议还是使用v2。

0x06 参考链接

Runtime options with Memory, CPUs, and GPUs https://docs.docker.com/config/containers/resource\_constraints/ Runtime metrics https://docs.docker.com/config/containers/runmetrics/ Compose file version 3 reference https://docs.docker.com/compose/compose-file/compose-file-v3/#deploy docker-compose Command options overview and help https://docs.docker.com/compose/reference/overview/#command-options-overview-and-help Setting Memory And CPU Limits In Docker https://www.baeldung.com/ops/docker-memory-limit How to specify Memory & CPU limit in version 3 https://github.com/docker/compose/issues/4513 Memory Resource Controller https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt

Excel公式插入双引号

通用公式

```=”””” & A1 & “”””``` ```=CHAR(34) & A1 & CHAR(34)```

解释

在公式里插入双引号,需要一个额外的双引号作为转义字符。通过转义字符,就可以告诉Excel把”字符当做文本。我们同时我们在公式中使用的时候还需要使用一对双引号。 “””” 最外面的两个引号(1&4)告诉Excel这是文本,第二个引号告诉Excel去转义下一个字符,然后第三个字符作为原始的字符展示出来呢。 如果觉得四个**”来标识一个双引号比较困惑,还可以通过CHAR**函数实现相同的功能。”的ASCII值是34所以 CHAR(34) 返回的是”.

Java多线程并发操作ArrayList

给公司一个业务系统做性能优化时,有个地方需要在循环内实现对外交互。有网络IO的地方很容易出现性能瓶颈,就打算通过parallelStream实现并发操作,

1
2
3
4
5
6
List resultList = new ArrayList();
xxxList.forEach(item -> {
result = doSomethingWithRemoteServer()
resultList.add(result)
});
return resultList;

如果直接把forEach改为parallelStream().forEach,就会引发新的问题,因为业务代码里使用了 arraylist.add 方法收集计算结果,ArrayList 是非线程安全的使用一个线程安全的容器。 Java里线程安全的集合容器,可以通过如下方法: Vector * 古老且线程安全的List, 每次扩容一倍空间,而ArrayList扩容50%。 * 在这个场景下因为集合需要返回上层做额外操作,如果使用Vector会有不必要的锁开销,当然这点儿性能影响可以忽略不计,如果不想有额外的锁开销就需要在返回时多了一层转换,把Vector转化为ArrayList。 CopyOnWriteArrayList * 每次添加新元素时创建一个新的List,适合读多写少的场景。该场景基本没并发读的场景,完全没必要使用。 Collections.synchronizedList * 返回一个包装类SynchronizedList,对被包装的真实List的所有场景加锁。 其他 * 因为我这里这个场景比较简单也可以使用ConcurrentHashMap等集合容器来实现线程安全。 我现在的这个场景挺适合Collections.synchronizedList

1
2
3
4
5
6
7
8
9
List resultList = new ArrayList();
List syncResultList = Collections.synchronizedList(resultList)

List resultList = new ArrayList();
xxxList.parallelStream().forEach(item -> {
result = doSomethingWithRemoteServer()
syncResultList.add(result)
});
return resultList;

全部改完之后,又想到我只用到了 arraylist.add 其实只需要同步这一方法就行了。

1
2
3
4
5
6
7
8
List resultList = new ArrayList();
xxxList.parallelStream().forEach(item -> {
result = doSomethingWithRemoteServer()
synchronized(resultList){
resultList.add(result)
}
});
return resultList;

这样没有一句废话的实现了多线程环境下,给ArrayList下添加元素。如果还在学生时代,估计我会直接写出最后这种代码,但是随着工作时间久了,习惯于使用各种工具包来实现各种功能,渐渐的忘记最初的样子, 习惯于把简单的问题复杂化。 Keep it simple and stupid.