Nicksxs's Blog

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

我们日常在使用github的时候经常会碰到访问比较慢的问题,一方面是github的打开慢,还有就是相关的数据传输很慢,比如从github上clone代码,有时候仓库比较大,经常是clone到一半就卡住了,或者速度几乎跌0了,还有比如是下载一些软件的软件包,特别是这个让人特别眼熟的地址raw.githubusercontent.com,经常是龟速下载,时间一长还经常断了就没法继续下载了
最近看到了这个项目,https://github.com/521xueweihan/GitHub520
我们可以把项目的内容复制下来,追加写入到我们的hosts文件里,但是这种方式不够一劳永逸,而且如果出现了访问缓慢,又去这个仓库下载的话,就变成先有鸡还是先有蛋的问题了,
所以刚好有switchhosts这款软件,它可以新建一个远程模式的hosts配置文件

同时可以设置更新频率,仓库维护着推荐是1小时更新,我觉得其实1天问题也不大,这样也能减轻维护者服务器的压力
还有就是通过crontab任务,比如

1
sudo sed -i "" "/# GitHub520 Host Start/,/# Github520 Host End/d" /etc/hosts && curl https://raw.hellogithub.com/hosts | sudo tee -a /etc/hosts

但是相对来说不太推荐,crontab维护比较原始,出现问题也不好恢复,switchhosts是个不错的选择

上次简单体验了下koupleless框架,可以发现ark模块的加载运行是非常有优势的,
这次就来看下这个模块式怎么加载的
我们可以通过启动基座使用调试模式,然后打下断点在ArkClient这个类的几个核心方法
比如com.alipay.sofa.ark.api.ArkClient#installBiz(java.io.File)
核心方法是在这

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
private static ClientResponse doInstallBiz(File bizFile, String[] args, Map<String, String> envs) throws Throwable {
AssertUtils.assertNotNull(bizFactoryService, "bizFactoryService must not be null!");
AssertUtils.assertNotNull(bizManagerService, "bizManagerService must not be null!");
AssertUtils.assertNotNull(bizFile, "bizFile must not be null!");
long start = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss,SSS");
String startDate = sdf.format(new Date(start));
Biz biz = bizFactoryService.createBiz(bizFile);
ClientResponse response = new ClientResponse();
if (bizManagerService.getBizByIdentity(biz.getIdentity()) == null && bizManagerService.registerBiz(biz)) {
try {
biz.start(args, envs);
long end = System.currentTimeMillis();
response.setCode(ResponseCode.SUCCESS).setMessage(String.format("Install Biz: %s success, cost: %s ms, started at: %s", biz.getIdentity(), end - start, startDate)).setBizInfos(Collections.singleton(biz));
getLogger().info(response.getMessage());
return response;
} catch (Throwable var15) {
long end = System.currentTimeMillis();
response.setCode(ResponseCode.FAILED).setMessage(String.format("Install Biz: %s fail,cost: %s ms, started at: %s", biz.getIdentity(), end - start, startDate));
getLogger().error(response.getMessage(), var15);
boolean autoUninstall = Boolean.parseBoolean(ArkConfigs.getStringValue("sofa.ark.auto.uninstall.when.failed.enable", "true"));
if (autoUninstall) {
try {
getLogger().error(String.format("Start Biz: %s failed, try to unInstall this biz.", biz.getIdentity()));
biz.stop();
} catch (Throwable var14) {
getLogger().error(String.format("UnInstall Biz: %s fail.", biz.getIdentity()), var14);
}
}

throw var15;
}
} else {
return response.setCode(ResponseCode.REPEAT_BIZ).setMessage(String.format("Biz: %s has been installed or registered.", biz.getIdentity()));
}
}

具体的代码在

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
public Biz createBiz(File file) throws IOException {
boolean unpackBizWhenInstall = Boolean.parseBoolean(ArkConfigs.getStringValue("sofa.ark.unpack.biz.when.install", "true"));
Object bizArchive;
if (ArkConfigs.isEmbedEnable() && unpackBizWhenInstall) {
File unpackFile = FileUtils.file(file.getAbsolutePath() + "-unpack");
if (!unpackFile.exists()) {
unpackFile = FileUtils.unzip(file, file.getAbsolutePath() + "-unpack");
}

if (file.exists()) {
file.delete();
}

file = unpackFile;
bizArchive = new ExplodedBizArchive(unpackFile);
} else {
JarFile bizFile = new JarFile(file);
JarFileArchive jarFileArchive = new JarFileArchive(bizFile);
bizArchive = new JarBizArchive(jarFileArchive);
}

BizModel biz = (BizModel)this.createBiz((BizArchive)bizArchive);
biz.setBizTempWorkDir(file);
return biz;
}

接下来是创建biz模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Biz createBiz(BizArchive bizArchive) throws IOException {
AssertUtils.isTrue(this.isArkBiz(bizArchive), "Archive must be a ark biz!", new Object[0]);
BizModel bizModel = new BizModel();
Attributes manifestMainAttributes = bizArchive.getManifest().getMainAttributes();
String mainClass = manifestMainAttributes.getValue("Main-Class");
String startClass = manifestMainAttributes.getValue("Start-Class");
bizModel.setBizState(BizState.RESOLVED, StateChangeReason.CREATED).setBizName(manifestMainAttributes.getValue("Ark-Biz-Name")).setBizVersion(manifestMainAttributes.getValue("Ark-Biz-Version")).setMainClass(!StringUtils.isEmpty(startClass) ? startClass : mainClass).setPriority(manifestMainAttributes.getValue("priority")).setWebContextPath(manifestMainAttributes.getValue("web-context-path")).setDenyImportPackages(manifestMainAttributes.getValue("deny-import-packages")).setDenyImportClasses(manifestMainAttributes.getValue("deny-import-classes")).setDenyImportResources(manifestMainAttributes.getValue("deny-import-resources")).setInjectPluginDependencies(this.getInjectDependencies(manifestMainAttributes.getValue("inject-plugin-dependencies"))).setInjectExportPackages(manifestMainAttributes.getValue("inject-export-packages")).setDeclaredLibraries(manifestMainAttributes.getValue("declared-libraries")).setClassPath(bizArchive.getUrls()).setPluginClassPath(this.getPluginURLs());
if (!(bizArchive instanceof DirectoryBizArchive)) {
bizModel.setBizUrl(bizArchive.getUrl());
}

BizClassLoader bizClassLoader = new BizClassLoader(bizModel.getIdentity(), this.getBizUcp(bizModel.getClassPath()), bizArchive instanceof ExplodedBizArchive || bizArchive instanceof DirectoryBizArchive);
bizClassLoader.setBizModel(bizModel);
bizModel.setClassLoader(bizClassLoader);
return bizModel;
}

这里就会读取对应biz包的启动类等,以及创建对应的biz模块的类加载器 BizClassLoader
接下去回到上一层

1
biz.start(args, envs);

接下来就是BizModel的start方法

1
2
3
public void start(String[] args, Map<String, String> envs) throws Throwable {
this.doStart(args, envs);
}

实际的就是doStart方法

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
private void doStart(String[] args, Map<String, String> envs) throws Throwable {
AssertUtils.isTrue(this.bizState == BizState.RESOLVED, "BizState must be RESOLVED", new Object[0]);
if (envs != null) {
String mainClassFromEnv = (String)envs.get("sofa.ark.biz.main.class");
if (mainClassFromEnv != null) {
this.mainClass = mainClassFromEnv;
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} will start with main class {} from envs", this.getIdentity(), mainClassFromEnv);
}
}

if (this.mainClass == null) {
throw new ArkRuntimeException(String.format("biz: %s has no main method", this.getBizName()));
} else {
ClassLoader oldClassLoader = ClassLoaderUtils.pushContextClassLoader(this.classLoader);
EventAdminService eventAdminService = (EventAdminService)ArkServiceContainerHolder.getContainer().getService(EventAdminService.class);

try {
eventAdminService.sendEvent(new BeforeBizStartupEvent(this));
this.resetProperties();
if (!this.isMasterBizAndEmbedEnable()) {
long start = System.currentTimeMillis();
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} start.", this.getIdentity());
MainMethodRunner mainMethodRunner = new MainMethodRunner(this.mainClass, args, envs);
mainMethodRunner.run();
eventAdminService.sendEvent(new AfterBizStartupEvent(this));
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} started in {} ms", this.getIdentity(), System.currentTimeMillis() - start);
}
} catch (Throwable var11) {
this.setBizState(BizState.BROKEN, StateChangeReason.INSTALL_FAILED, getStackTraceAsString(var11));
eventAdminService.sendEvent(new AfterBizStartupFailedEvent(this, var11));
throw var11;
} finally {
ClassLoaderUtils.popContextClassLoader(oldClassLoader);
}

BizManagerService bizManagerService = (BizManagerService)ArkServiceContainerHolder.getContainer().getService(BizManagerService.class);
if (bizManagerService.getActiveBiz(this.bizName) == null) {
this.setBizState(BizState.ACTIVATED, StateChangeReason.STARTED);
} else {
boolean activateMultiBizVersion = Boolean.parseBoolean(ArkConfigs.getStringValue("sofa.ark.activate.multi.biz.version.enable", "false"));
if (activateMultiBizVersion) {
this.setBizState(BizState.ACTIVATED, StateChangeReason.STARTED);
} else {
if (Boolean.getBoolean("activate.new.module")) {
Biz currentActiveBiz = bizManagerService.getActiveBiz(this.bizName);
((BizModel)currentActiveBiz).setBizState(BizState.DEACTIVATED, StateChangeReason.SWITCHED, String.format("switch to new biz %s", this.getIdentity()));
this.setBizState(BizState.ACTIVATED, StateChangeReason.STARTED, String.format("switch from old biz: %s", currentActiveBiz.getIdentity()));
} else {
this.setBizState(BizState.DEACTIVATED, StateChangeReason.STARTED, "start but is deactivated");
}

}
}
}
}

主要是看这个方法

1
2
3
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} start.", this.getIdentity());
MainMethodRunner mainMethodRunner = new MainMethodRunner(this.mainClass, args, envs);
mainMethodRunner.run();

看到这个run方法

1
2
3
4
5
public Object run() throws Exception {
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
return mainMethod.invoke((Object)null, this.args);
}

最终就是到这个,它就用了这个上下文的类加载器,而这个类加载器就是用的前面设置的bizClassLoader

1
2
3
4
5
public static ClassLoader pushContextClassLoader(ClassLoader newClassLoader) {
ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(newClassLoader);
return oldClassLoader;
}

在挺久之前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中再叠加树形结构,去细化一个链路中的每次子调用
后面可以再对这些内容进行展开

0%