Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

在挺久之前serverless架构还是很火的,大概是在这一波人工智能热潮之前,结合云原生,但似乎这个框架似乎没有一个大一统的,类似于Java中的spring,springboot这种框架
前阵子体验到了这款Koupleless框架,前身是sofa serverless,总结下来主要是对于它的模块级开发和部署的机制觉得非常不错
首先就是我们可以下一个samples来体验
可以通过git也可以去下载zip包(因为实在有点大,网络不好可能还是zip包比较快)

1
git clone git@github.com:koupleless/samples.git

比较方便的是基于仓库下的springboot-samples/web/tomcat/样例来体验

目录下的 base-web-single-host 是基座应用
biz1-web-single-hostbiz2-web-single-host 是业务模块的demo
代码clone下来以后首先要构建这几个模块

1
mvn -pl web/tomcat/biz1-web-single-host,web/tomcat/biz2-web-single-host -am clean package -DskipTests

这里会碰到几个问题,一个是需要maven版本 >= 3.9.x,于是我就单独下了个,另外就是会碰到

1
2
 Plugin com.alipay.sofa.koupleless:koupleless-base-build-plugin:1.4.2-SNAPSHOT or one of its dependencies could not be resolved:
[ERROR] Could not find artifact com.alipay.sofa.koupleless:koupleless-base-build-plugin:jar:1.4.2-SNAPSHOT in ark-snapshot (https://oss.sonatype.org/content/repositories/snapshots)

这个问题,然后就去这个仓库看了下缔约没这个版本的包,已经是正式包了,
找到这个版本是在根pom定义的

1
<koupleless.runtime.version>1.4.2</koupleless.runtime.version>

把它改成正式版本构建就好了,当然也是对国内网络不太友好,需要等一会
还需要准备好模块包的构建工具,arkctl v0.2.1+
可以用go来安装

1
go install github.com/koupleless/arkctl@v0.2.1

或者直接在这下载 https://github.com/koupleless/arkctl/releases
安装后需要设置下PATH,也可以直接用全路径,比如mac下安装完了是在用户目录下的/go/bin/arkctl
对了,还需要maven拉下依赖包,也可以用命令行的
然后就可以启动基座了,基座启动就类似于普通的springboot应用,
接下去启动biz模块

1
~/go/bin/arkctl deploy web/tomcat/biz1-web-single-host/target/biz1-web-single-host-0.0.1-SNAPSHOT-ark-biz.jar

1
~/go/bin/arkctl deploy web/tomcat/biz2-web-single-host/target/biz2-web-single-host-0.0.1-SNAPSHOT-ark-biz.jar 

启动之后就可以用curl或者浏览器访问来验证,是否两个模块都被加载了
使用curl查看biz1

1
curl http://localhost:8080/biz1/

可以看到输出

1
hello to biz1 deploy, run in com.alipay.sofa.ark.container.service.classloader.BizClassLoader@4850d96f%

再看下biz2

1
curl http://localhost:8080/biz2/ 

可以看到输出

1
hello to biz2 deploy, run in com.alipay.sofa.ark.container.service.classloader.BizClassLoader@6c5136fe

结合代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.alipay.sofa.web.biz1.rest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.time.LocalDateTime;

@RestController
public class SampleController {

@Autowired
private ApplicationContext applicationContext;

@RequestMapping(value = "/", method = RequestMethod.GET)
public String hello() {
String appName = applicationContext.getId();
return String.format("hello to %s deploy, run in %s", appName, Thread.currentThread()
.getContextClassLoader());
}

可以看到这是biz1的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.alipay.sofa.web.biz2.rest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.time.LocalDateTime;

@RestController
public class SampleController {

@Autowired
private ApplicationContext applicationContext;

@RequestMapping(value = "/", method = RequestMethod.GET)
public String hello() {
String appName = applicationContext.getId();
return String.format("hello to %s deploy, run in %s", appName, Thread.currentThread()
.getContextClassLoader());
}

biz2也会答应对应的
这里其实主体就是启动一个应用基座,模块都是直接在基座上热加载的,不用重新启动应用,对于需要快捷开发并行开发的还是很方便的,也是种变相的serverless下,模块的代码结构模式也跟普通springboot应用很类似,就常规的开发,但是部署启动都变成了秒级的,非常方便

前阵子在微博看到一个说nocodb这个工具,一开始以为是类似于飞书的多维表格,那个对于很多办公党来说真的很好用,但是对于我好像没啥吸引力,但是这次看到的介绍是能替代很多db工具,就相当于是个后台系统,可以查看表的数据以及编辑等
首先就是很简单的安装,不过需要再支持docker且安装了的环境,

1
bash <(curl -sSL http://install.nocodb.com/noco.sh) <(mktemp)

在高级选项中我选择了

1
2
3
4
1. db (1 replicas)
2. nocodb (1 replicas)
3. redis (1 replicas)
4. traefik (1 replicas)

这几个,第一个是db,可以选pgsql,2是本体,3是作为缓存,4是作为网络路由组件

可以在左边菜单中添加外部数据源集成

添加以后
就可以在这里

连接外部数据源,选择刚才新增的数据源

接下来就能在tables里查看数据表里的数据了

比如像新增数据,编辑数据都是支持

还能搜索过滤数据

真的是只要有了表,里面的数据查看管理都直接支持了,非常的方便,省去了我们自己搞个后台系统,并且也是非常美观的展示样式,对比起phpmyadmin这种,真的优秀太多了
如果有外键关联还可以查看表结构关系,类似于er图的功能了,还有webhook,数据出现变动就会发送hook通知

甚至还能直接升成swagger的rest api文档,真的强的有点离谱

感觉有了这种工具,一般的crud工作基本就完全省掉了,建好表,直接连接nocodb,然后导出api,前端就可以干活了,
如果是基本的数据crud就完全不用开发了,需要前端定制的就直接对接前端
感觉对于小工作室或者个人开发者来说还是很方便了的,当然结合最新的人工智能技术编写代码也能做得很好
现在这样子相对简单的功能是变得越来越方便

之前在前司的时候研究过一些链路日志相关的,有基于pinpoint,skywalking等中间件的,也有一些基于rpc框架的自研的,不过其中很多相通的逻辑应该是基于MDC来实现的,MDC 全称是 Mapped Diagnostic Context , 可以比较粗略的想成是一个存放诊断日志的工具
可以基于简单的代码来看一下

1
2
3
4
5
6
7
public static void main(String[] args) {
MDC.put(TRACE_ID, UUID.randomUUID().toString());
logger.info("开始业务逻辑处理");
logger.info("业务逻辑处理结束");
MDC.remove(TRACE_ID);
logger.info("TRACE_ID 还有吗?{}", MDC.get(TRACE_ID) != null);
}

另外再配置下

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>[%t] [%X{TRACE_ID}] - %m%n</Pattern>
</layout>
</appender>
<root level="debug">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

一般springboot这种框架都是自带logback了的,没有的话可以引一下
打印下看

1
2
3
[main] [d8610b0d-7db8-4926-8de0-0e14e8342018] - 开始业务逻辑处理
[main] [d8610b0d-7db8-4926-8de0-0e14e8342018] - 业务逻辑处理结束
[main] [] - TRACE_ID 还有吗?false

那么MDC里究竟是啥呢
我们可以顺着 org.slf4j.MDC#put 的逻辑看下去,里面的是调用了个接口的

1
2
3
4
5
6
7
8
9
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
} else if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
mdcAdapter.put(key, val);
}
}

这个 org.slf4j.spi.MDCAdapter 接口我们再看下实现类
我们用的是logback,就看下这个实现 ch.qos.logback.classic.util.LogbackMDCAdapter
这里的put呢,就是基于ThreadLocal的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}

Map<String, String> oldMap = copyOnThreadLocal.get();
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
} else {
oldMap.put(key, val);
}
}

至于这些map的操作主要是为了父子线程之间来传递MDC信息用的
主要还是基于

1
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();

的操作,这样也就能保持线程安全
基于这个逻辑,我们在做链路日志串联的时候就有基础功能了
比如web请求,就在拦截器里可以先给MDC里设置好当前请求的请求traceId
当然这个traceId的唯一性就需要进行一步讨论比如雪花算法这种,不过一般这种情况带上机器id就可以简化点
在微服务之间进行rpc调用的时候可以基于类似于Dubbo的filter机制,在RpcContext中写入traceId,保证服务之间调用能够带上这个traceId
接下去就是更复杂的,比如定时任务,消息队列,
还有涉及到其他中间件,以及最重要的数据库的操作,需要研究类似于数据库连接池或者分库分表中间件的能力
只是总结下来,对于这类基础设施的实现,还是离不开MDC这个日志的基础能力
否则比如我们在整个应用写入日志的时候都需要处理这个traceId
有的说可以用切面,切面其实也是要基于MDC这种存储,另外就是整个应用的切面其实用起来也没那么方便
再说回来,traceId来串联日志的重要性在一般业务排查中还是起到非常大作用的,这个还有业界的dapper这种示范理念
再细化的还会在这个traceId中再叠加树形结构,去细化一个链路中的每次子调用
后面可以再对这些内容进行展开

之前比较粗浅的写过一点类加载器的相关知识,最近因为在看相关的内容,所以打算来复习也简单分享下
首先是我们常规用户自定义类的类加载器,

1
2
3
4
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println(classLoader);

输出的是

1
sun.misc.Launcher$AppClassLoader@18b4aac2

是这个AppClassLoader
之前也讲到过,常规的层级就是
用户自定义类加载器 -> AppClassLoader -> ExtClassLoader -> BootstrapClassLoader
可以用一段简单的代码来看下

1
2
3
4
5
6
7
8
9
10
11
StringBuilder sb = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue) {
System.out.println(sb.toString() + classLoader);
if (classLoader == null) {
needContinue = false;
} else {
classLoader = classLoader.getParent();
sb.insert(0, "\t");
}
}

可以看到

1
2
3
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@555590
|--null

最后的就是 BootstrapClassLoader,因为是native实现的,所以没有类名
那么到这呢,就是想接着说下双亲委派,这个翻译可能跟实际的理解不一定一样属于见仁见智
个人的观点主要是第一是保证了一致性,不会随意的使用classloader,对于比如jdk提供的类,应该由什么类加载器来加载是稳定可预期的,另一个就是各司其职,
我们可以看下BootstrapClassLoader类加载器主要负责加载哪些类

1
2
3
4
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}

主要是核心的这些包

1
2
3
4
5
6
7
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/classes

而ExtClassLoader
则是负责 /jre/lib/ext 下面的类
因为没有公开方法
我们捞一下
其实代码都在 sun.misc.Launcher 里面
创建的时候

1
2
3
4
5
private static ExtClassLoader createExtClassLoader() throws IOException {
try {
return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();

而这个 getExtDirs() 就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];

for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}

return var1;
}

打印出来就是
/Users/user/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
在这些目录下的

AppClassLoader
则是当前应用程序的classpath下的类
这里就有很多了,不过classpath好像也是个知识点,可以后面讲讲,这块主要还是之前也提过,首先是找这个类的实例化对象,找到了就可以没有就调用父级的loadClass,实现逐层找,找不到在通过本加载器的findClass去加载。这里可以稍微想一下,如果有父类加载器就让parent加载,parent也是再往上,当这个parent是比如Ext时,委托它的parent就是Bootstrap,加载为空时,就会走到第二个if (c==null)

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

然后调用当前类加载器的findClass进行处理,这里还是异常就是对下层来说的 c == null 会往下再调用 findClass,这样就完成了往上找再往下逐层尝试加载

偶尔会用工具下载网上视频,然后有些下载会是视音频分离的模式,当文件名的开头是”-“的时候,在文件合并的时候就会出现异常
因为ffmpeg会将”-“视为命令参数的一部分,所以需要把文件重命名后,手动进行合并
命令也比较简单

1
ffmpeg –i video_file –i audio_file –vcodec copy –acodec copy output_file

主要是识别到,它是通过两个输入文件,一个是视频,一个是音频,然后都是直接拷贝合并进目标文件
当然还有方法是比如加上当前目录
比如从-audio.m3a改成./-audio.m3a 或者全路径
另一个是在文件名和参数名中间用-- 来分割,这样都能解决问题

0%