sockfwd 一个数据转发的小工具

最近在看containerd的代码,上手试的时候才发现它监听的是unix socket,没法从外部访问containerd。
而我要验证的是从远端能不能访问containerd、管理containerd的容器,所以需要一个从远端访问unix socket的工具。

网上搜了一圈,没有现成的实现,就自己写了 sockfwd

用法

1
2
3
4
5
6
7
Usage:
sockfwd [flags]

Flags:
-d, --destination string 目的地址,即要转发到的地址
-s, --source string 源地址,即接收请求的地址
-q, --quiet 静默模式

例子

将本地的containerd实例暴露到网络上:

./sockfwd -s tcp://127.0.0.1:8090 -d unix:///var/run/containerd.sock

将本地的127.0.0.1:8080端口暴露到0.0.0.0:8090端口上:

./sockfwd -s tcp://127.0.0.1:8090 -d unix://127.0.0.1:8090

将本地的服务暴露到网络上,需要格外注意是否有安全隐患!

阅读更多

containerd/nerdctl - 一个开源、免费的Docker的替代品

2021年8月31日,Docker公司更改了收费策略,简而言之就是对个人和小公司不收费,对大公司收费。

那么,是时候开始试一下 containerd + lima这个开源、免费的Docker替代品。

什么是containerd?

containerd是一个标准化的容器运行时,可以被其他系统很方便的集成。正是由于这个原因,containerd也成为了Kubernetes的默认容器运行时。

但是在containerd的目标聚焦在和其他系统集成,所以它的默认命令行工具(crictl)也不是很好用,和docker也不兼容。

后来,nttlabs贡献了一个名为nerdctl的containerd客户端,可以兼容docker命令行工具。于是我们就可以使用nerdctl来作为docker的替代品了。

nerdctl不仅与docker兼容,而且还支持了更多的功能:

  • 支持containerd的命名空间查看,nerdctl不仅可以管理Docker容器,也可以直接管理本地的的Kubernetes pod
  • 支持将Docker Image Manifest镜像转换为OCI镜像、estargz镜像
  • 支持OCIcrypt(镜像加密)

什么是lima?

containerd要在macOS上使用,需要安装虚拟机、然后在虚拟机配置containerd,这个过程很浪费时间。

阅读更多

如何优雅关闭maven-default-http-blocker?

最近升级Maven到3.8.1后,mvn编译的时候总是提示拉不到依赖,报错如下:

Could not validate integrity of download from http://0.0.0.0/...

全部报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[WARNING] Could not validate integrity of download from http://0.0.0.0/com/alibaba/nacos/nacos-client-mse-extension/1.4.2-SNAPSHOT/maven-metadata.xml
org.eclipse.aether.transfer.ChecksumFailureException: Checksum validation failed, expected <!doctype but is 18420d7f1430a348837b97a31a80e374e3b00254
at org.eclipse.aether.connector.basic.ChecksumValidator.validateExternalChecksums (ChecksumValidator.java:174)
at org.eclipse.aether.connector.basic.ChecksumValidator.validate (ChecksumValidator.java:103)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector$GetTaskRunner.runTask (BasicRepositoryConnector.java:460)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector$TaskRunner.run (BasicRepositoryConnector.java:364)
at org.eclipse.aether.util.concurrency.RunnableErrorForwarder$1.run (RunnableErrorForwarder.java:75)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector$DirectExecutor.execute (BasicRepositoryConnector.java:628)
at org.eclipse.aether.connector.basic.BasicRepositoryConnector.get (BasicRepositoryConnector.java:235)
at org.eclipse.aether.internal.impl.DefaultMetadataResolver$ResolveTask.run (DefaultMetadataResolver.java:573)
at org.eclipse.aether.util.concurrency.RunnableErrorForwarder$1.run (RunnableErrorForwarder.java:75)
at java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1130)
at java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:630)
at java.lang.Thread.run (Thread.java:832)
[WARNING] Checksum validation failed, expected <!doctype but is 18420d7f1430a348837b97a31a80e374e3b00254 from maven-default-http-blocker for http://0.0.0.0/com/alibaba/nacos/nacos-client-mse-extension/1.4.2-SNAPSHOT/maven-metadata.xml
Downloaded from maven-default-http-blocker: http://0.0.0.0/com/alibaba/nacos/nacos-client-mse-extension/1.4.2-SNAPSHOT/maven-metadata.xml (63 kB at 19 kB/s)

从关键字maven-default-http-blocker可以找到相关资料。

简而言之,如果使用HTTP协议下载依赖,可能会导致中间人攻击。比如,本来想下载一个nacos-client的,结果下载的结果中被插入了恶意代码,然后开发人员运行了一下,黑客就能获得开发人员的计算机控制权了。

所以Maven 3.8.1就禁止了所有HTTP协议的Maven仓库。

详情见Maven 3.8.1的发布日志

问题是在日常开发中,我们经常会用到公司内部的maven仓库。这些仓库一般都是http协议,Maven 3.8.1禁止了http协议,那么就会导致开头的报错。

于是查了下,可以按照如下方式关闭:

阅读更多

Golang小技巧-在单个仓库中支持多个 go mod 模块

背景

最近在写Go,有一个项目是多模块的,版本的发布都是在一起的,为了其他项目使用这些模块,所以需要在一个仓库中实现多个模块的发布。

仓库结构

仓库结构如下:

1
2
3
4
5
6
7
8
.
├── README.md
├── a
│   ├── a.go
│   └── go.mod
└── b
├── b.go
└── go.mod

其中a/go.mod使用如下命令生成:

1
go mod init github.com/robberphex/go-test-multi-module/a

版本发布

由于是多模块,所以使用不同的tag。tag名字统一为<模块名>/<版本号>

比如现在的tag有:

阅读更多

Linux下的环境变量到底有什么限制?

前言

最近在排查一个eureka问题,发现客户设置了环境变量eureka.client.serviceUrl.defaultZone
于是在本地尝试复现这个现场,发现我设置不了这个环境变量,提示:

1
2
# export eureka.client.serviceUrl.defaultZone=http://localhost:8761/
sh: export: eureka.client.serviceUrl.defaultZone: bad variable name

但是在k8s中,可以给pod设置带点的环境变量。

于是看了下Linux中环境变量的限制。

首先看一下为什么export会报错

以busybox为例,export是一个内置命令,所以执行这个命令的时候,直接调用了 exportcmd 函数

这个函数的核心逻辑就是:

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
// 函数setvar
static struct var *
setvar(const char *name, const char *val, int flags)
{
//……
q = endofname(name);
p = strchrnul(q, '=');
namelen = p - name;
if (!namelen || p != q)
ash_msg_and_raise_error("%.*s: bad variable name", namelen, name);
//……
}

// endofname.c
#define is_name(c) ((c) == '_' || isalpha((unsigned char)(c)))
#define is_in_name(c) ((c) == '_' || isalnum((unsigned char)(c)))

const char* FAST_FUNC
endofname(const char *name)
{
if (!is_name(*name))
return name;
while (*++name) {
if (!is_in_name(*name))
break;
}
return name;
}

仔细观察endofname的逻辑就会发现,shell对于环境变量名字的限制是:

阅读更多

虚引用真的不影响对象的生命周期吗?

Java的四大引用,大家都很熟悉吧:

  • 强应用:正常代码中的引用。一个对象能通过强应用访问到,那它就永远不会被回收
  • 软引用:比强引用弱一级的引用,内存不足时引用指向的对象会被回收
  • 弱引用:比软引用弱一级的引用,下一次GC时指向对象会被回收
  • 虚引用

最后一个虚应用是今天要讨论的。很多文章都是这么写的:

一个对象是否有虚引用存在,对其生存不会产生任何影响。

事实上,这个是错的。正确的表述是:

在Java 8以及之前的版本中,在虚引用回收后,虚引用指向的对象才会回收。在Java 9以及更新的版本中,虚引用不会对对象的生存产生任何影响。

一个示例

首先用Java 8,带上-Xmx10m -XX:+HeapDumpOnOutOfMemoryError参数运行如下代码:

Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public final class Main {

public static void main(String[] args) throws InterruptedException {
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
PhantomReference<byte[]> ref = new PhantomReference<>(new byte[1024 * 1024 * 5], queue);

System.out.println(queue.poll());
System.out.println("第一次gc");
System.gc();
Thread.sleep(300L);
System.out.println(queue.poll());
System.out.println("第二次gc");
System.gc();
byte[] bytes1 = new byte[1024 * 1024 * 6];
System.out.println("ending");
}
}

你猜猜结果是什么?

阅读更多

如何从Spring Cloud Config迁移到阿里云ACM

从零开始使用Spring Cloud Config中,我们简单了解了下Spring Cloud Config,它提供了一套配置管理的解决方案。

但是Spring Cloud Config需要自己搭建config-server,还需要结合eureka来实现高可用;如果需要实时更新配置,还需要Spring Cloud Bus。实在是过于繁琐。

而阿里云ACM(应用配置管理)则提供了一整套完整的解决方案:

  1. 与Spring生态紧密集成
    和Spring Cloud Config一样,ACM通过Envirment和PropertySource与Spring结合。
  2. 配置简单
    只需要在ACM上开一个namespace,然后配置client,就能享受到获取配置、自动更新配置功能。
  3. 免费

阿里云ACM相比与Spring Cloud Config,提供了一揽子配置解决方案,不需要在借助Spring Cloud Bus和eureka了。

我们以上一篇文章中的spring-cloud-config-client作为示例。

迁移配置文件

在使用ACM之前,我们需要将现有的配置迁移到ACM中,要不然直接切换过去没法获取任何配置,会导致应用无法正常运行。

为每个环境(此处的环境就是Spring中的profile)创建一个ACM的命名空间。

比如dev对应namespace dev,test对应test命名空间。

阅读更多

从零开始使用Spring Cloud Config

在传统的Java应用中,应用配置都是直接存储在代码库中(比如放在application.yml中),配置的更新需要修改发布包并重新发布。

而配置的审计也会和代码夹杂,需要从众多提交中中找到配置变更的提交,很难独立完成,也很难追溯何时修改了某一项配置。

随着微服务的发展,服务越来越多、配置也越来越多,这种原始的配置管理方式,很难适应分布式环境。作为开发者,还是需要配置能单独地管理和审计。

Spring Cloud Config就提供了配置管理和审计的解决方案。

Spring Cloud Config有如下特点:

  1. 与Spring生态紧密集成
    提供了和Spring中的EnvironmentPropertySource一致的抽象,能够和原有的配置文件很好的对应起来。
  2. 可以动态更新
    只需要添加注解,不需要重启就能更新配置项
  3. 支持多种provider
    配置仓库可以是git仓库、本地文件、数据库等等。

开始使用Spring Cloud config

Spring Cloud Config 分为两部分:

Spring Cloud Config Server负责从配置仓库中获取配置,并给client提供获取配置的HTTP接口

Spring Cloud Config Client负责给目标应用提供从config-server获取配置、更新配置的能力。

阅读更多

Jackson如何反序列化Java14中的record类型?

Java14近日发布,其中引入了新的record类型,虽然这是个预览特性,但是也不妨碍我们尝试下。

record类型

record类型和普通的类相比,有几个特点

  1. 每个字段都是private final的,类本身是final
  2. 类只有一个所有传递所有参数的构造函数
  3. 每个字段会自动会有一个get方法,但是方法名和字段名一致(没有get前缀
  4. 可以给字段添加注解,支持FIELD、METHOD和PARAMETER(如果一个注解是FIELD、METHOD和PARAMETER的,那么注解会在字段、get方法和构造函数参数上同时出现)
  5. toStringequalshashCode方法都是自带的

这几个特点决定了这个非常适合做DO(Model),DTO(比如RPC中定义的各种entity,http返回的json)。

由于业务中经常用到jackson来序列化反序列化对象,今天我们就来试一试jackson对record类型的支持。

如何使用jackson反序列化record对象

如果我们按照原来的方式来反序列化record(如果字段上没有@JsonProperty注解的话),会直接报错:
InvalidDefinitionException: Cannot construct instance of Person (no Creators, like default construct, exist)

这是因为jackson没法找到构造函数和字段的映射,所以我们自己指定默认的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
// from https://gist.github.com/youribonnaffe/03176be516c0ed06828ccc7d6c1724ce
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
@Override
public String findImplicitPropertyName(AnnotatedMember m) {
if (m.getDeclaringClass().isRecord()) {
if (m instanceof AnnotatedParameter parameter) {
return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
}
}
return super.findImplicitPropertyName(m);
}
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);
阅读更多

Java 14中的 JEP 358: Helpful NullPointerExceptions是如何实现的

Java 14在2020年3月17号正式发布,不久AdoptOpenJDK也跟进发布了新版。

JEP 358: Helpful NullPointerExceptions

其中对使用者比较有用的功能是 JEP 358: Helpful NullPointerExceptions,即在NullPointerException的message中指明为什么产生了这个NPE。

示例代码如下(需要Java14,并配置参数-XX:+ShowCodeDetailsInExceptionMessages):

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
public class Main {
static class Class1 {
Object object;
}
static class Class2 {
Class1 class1;
public Class1 getClass1() {
return class1;
}
}
static Class2 class2;
private static Object get(int i) {
return null;
}
// 打印NPE的message
private static void printNPE(Runnable runnable) {
try {
runnable.run();
} catch (NullPointerException e) {
System.err.println(e.getMessage());
}
}
public static void main(String[] args) {
Class2 class21 = null;
printNPE(() -> {
// 1. getstatic为null
// Cannot invoke "Main$Class2.getClass1()" because "Main.class2" is null
System.out.println(class2.getClass1().object.toString());
});
Class2 class22 = new Class2();
printNPE(() -> {
// 2. 调用函数结果为null
// Cannot read field "object" because the return value of "Main$Class2.getClass1()" is null
System.out.println(class22.getClass1().object.toString());
});
class22.class1 = new Class1();
printNPE(() -> {
// 3. 函数调用后,getfield为null
// Cannot invoke "Object.toString()" because "Main$Class2.getClass1().object" is null
System.out.println(class22.getClass1().object.toString());
});
printNPE(() -> {
// 4. 链式getfield
// Cannot invoke "Object.toString()" because "class22.class1.object" is null
System.out.println(class22.class1.object.toString());
});
Integer[] ary1 = null;
printNPE(() -> {
// 5. 对null执行aaload
// Cannot load from object array because "ary1" is null
System.out.println(ary1[0].intValue());
});
Integer[] ary2 = new Integer[1];
printNPE(() -> {
// 6. iconst_0+aaload后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[0]" is null
System.out.println(ary2[0].intValue());
});
printNPE(() -> {
int i = 0;
// 7. iconst_0+iload_1后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[i]" is null
System.out.println(ary2[i].intValue());
});
printNPE(() -> {
int i = 0;
// 8. iconst_0+iadd后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[...]" is null
System.out.println(ary2[i + 0].intValue());
});
printNPE(() -> {
// 9. invokevirtual结果为null
// Cannot invoke "Object.toString()" because the return value of "Main.get(int)" is null
System.out.println(Main.get(1).toString());
});
}
}

令我比较意外的是情况4和情况7,Java能够追溯链式回调,而且连变量名都带上了!

JEP 358实现细节

那我们就看下这么神奇的特性是如何实现的吧。

首先,这个功能需要添加参数-XX:+ShowCodeDetailsInExceptionMessages才能开启,那我们只要在JDK的代码中搜索这个字符串就行。于是找到给NPE填充Message的代码:

阅读更多