Nicksxs's Blog

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

最近在研究headscale的多局域网互通,但是因为设备和网络知识不过关,所以还没完全走通,不过对于mac设备的路由和linux的路由这些知识稍微多了一点
在mac上可以通过

1
netstat -nr

查看路由表
命令参数的解析是

1
2
3
4
5
 -n    Show network addresses as numbers (normally netstat interprets addresses and attempts to display them symbolically).  This option may be used with any
of the display formats.
-r Show the routing tables. Use with -a to show protocol-cloned routes. When -s is also present, show routing statistics instead. When -l is also
present, netstat assumes more columns are there and the maximum transmission unit. More detailed information about the route metrics are displayed with
-ll for TCP round trip times -lll for all metrics. Use the -z flags to display only entries with non-zero RTT values. (“mtu”) are also displayed.

比如我的路由表里

1
2
3
4
5
Routing tables

Internet:
Destination Gateway Flags Netif Expire
default 192.168.xx.1 UGScg en0

这个表示是默认路由,走的网关转发,
这里的Gateway就是网关地址,一般是我们的路由器地址
Flags的意思是
U表示Up,表示路由是可用的,
G表示是Gateway,表示通过网关转发
S表示是Static,是静态路由,非动态生成,
c表示是Cloned,由父路由自动克隆生成
g表示是Global,全局的
比较全的

1
2
3
4
5
6
7
8
U = Up
G = Gateway
S = Static, e.g., default route added at boot time
H = Host-specific
C = Generate new (host-specific) routes on use
L = Valid link-layer (MAC) address
c = Cloned route
R = Reject route, known but unreachable route

表示我们的请求都是经过这个网关转发的,通过它把数据往外网或者局域网内其他设备发
至于这个Netif表示 network interface
就是网络接口也可以理解为网卡
比如我这里的en0,一般指物理网卡(通常是 Wi-Fi)
另外还可以通过

1
route get default

查看默认路由设置
可以这么理解网络连接过程

1
2
3
4
5
6
7
8
9
应用程序

路由表

netif(网络接口)

物理网卡 / 虚拟隧道

网络

原来的设置就是比如我可以在网关设备配置好tailscale节点,
然后让局域网内的设备都通过这个网关设备连接tailscale的网络,
对于非自建headscale应该就是subnet互联这样的逻辑
但是对于headscale中,可能因为我的网关设备是n1刷的openwrt,
对应的tailscale版本比较新,一直出现比较奇怪的问题
比如获取不到ipv4的地址,
启动tailscale就直接把本地局域网的地址全都走tailscale0虚拟网卡路由了
导致ipv4地址直接都不通了,
还得找下原因,可能是tailscale的客户端版本太高了,
一开始连接不上headscale的http地址,
高版本必须得用https才行,
结果就是用caddy套了一层https的壳
能连接上了又发现ipv4地址没了
而且还会出现一些很奇怪的问题
比如会出现局域网内的wifi也断连
是否是出现了局域网内的二层广播风暴或者arp冲突
应该是二层环路问题比较大
这一块还有待后续的研究

我们在使用mysql的索引的时候一般会使用explain来查看执行计划,用来分析索引使用情况等
但是经常我们也会质疑,为啥没有用预期的索引,反而使用了另一个或者甚至没使用
这样我们就可以开启optimizer trace来看看具体优化器是怎么处理的

1
2
3
4
SET optimizer_trace='enabled=on';
SET optimizer_trace_limit=10;
set optimizer_trace_offset=-10;
set end_markers_in_json=on;

我们可以用这几个命令来开启
比如我们执行一个最简单的 do 1+1;
通过查询

1
SELECT query,trace FROM information_schema.OPTIMIZER_TRACE;

就可以看到优化器的trace

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
{
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select (1 + 1) AS `1+1`"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": {
"select#": 1,
"steps": [
] /* steps */
} /* join_optimization */
},
{
"join_execution": {
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}

我们可以再查一个真实点的
比如

1
SELECT * from students WHERE class = 10;

trace 就长这样

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
{
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `students`.`id` AS `id`,`students`.`name` AS `name`,`students`.`age` AS `age`,`students`.`class` AS `class`,`students`.`created_at` AS `created_at`,`students`.`updated_at` AS `updated_at` from `students` where (`students`.`class` = 10)"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`students`.`class` = 10)",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "multiple equal(10, `students`.`class`)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "multiple equal(10, `students`.`class`)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "multiple equal(10, `students`.`class`)"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": {
} /* substitute_generated_columns */
},
{
"table_dependencies": [
{
"table": "`students`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [
{
"table": "`students`",
"field": "class",
"equals": "10",
"null_rejecting": false
}
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [
{
"table": "`students`",
"range_analysis": {
"table_scan": {
"rows": 107,
"cost": 24.5
} /* table_scan */,
"potential_range_indexes": [
{
"index": "PRIMARY",
"usable": false,
"cause": "not_applicable"
},
{
"index": "idx_class",
"usable": true,
"key_parts": [
"class",
"id"
] /* key_parts */
}
] /* potential_range_indexes */,
"setup_range_conditions": [
] /* setup_range_conditions */,
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
} /* group_index_range */,
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "idx_class",
"ranges": [
"10 <= class <= 10"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": true,
"using_mrr": false,
"index_only": false,
"rows": 5,
"cost": 7.01,
"chosen": true
}
] /* range_scan_alternatives */,
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */,
"chosen_range_access_summary": {
"range_access_plan": {
"type": "range_scan",
"index": "idx_class",
"rows": 5,
"ranges": [
"10 <= class <= 10"
] /* ranges */
} /* range_access_plan */,
"rows_for_plan": 5,
"cost_for_plan": 7.01,
"chosen": true
} /* chosen_range_access_summary */
} /* range_analysis */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`students`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "ref",
"index": "idx_class",
"rows": 5,
"cost": 4,
"chosen": true
},
{
"access_type": "range",
"range_details": {
"used_index": "idx_class"
} /* range_details */,
"chosen": false,
"cause": "heuristic_index_cheaper"
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 5,
"cost_for_plan": 4,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {
"original_condition": "(`students`.`class` = 10)",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`students`",
"attached": null
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"refine_plan": [
{
"table": "`students`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": {
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}

比如这里的 potential_range_indexes 提示primary主键就是不适用的,当然具体分析还需要更深入的学习。

最近也看到了一篇文章,结合一些实际的经验来看下索引的基数和可选择性,
这个基数指的是啥呢,就是索引我们一直在讲的是要字段值的差异度比较大的那种,因为假如这个字段的所有值都是比如0和1的话,那索引的结构BTree就没办法高效的找到所查询的值,这个基数就是可以作为它差异度大小的一个参考,当然这是一种反过来的说法,理论上应该从写入这个表的数据的逻辑去看,这些字段会出现哪些值以及它的取值范围是怎么样的
那么这个会在mysql中存起来的基数值可以怎么看呢
还是用我之前建的一个简单的表

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `students` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) NOT NULL COMMENT '姓名',
`age` int(11) NOT NULL COMMENT '年纪',
`class` int(11) NOT NULL COMMENT '班级',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_class` (`class`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=608 DEFAULT CHARSET=utf8mb4 COMMENT='学生表';

表里的数据也比较简单

因为升成name和class的时候我都用了一定的规律取余,所以会有一些重复的,
这是我们就可以在information库的查一下这个基数

1
select table_schema,table_name,index_name,cardinality from information_schema.statistics where table_schema='database' and table_name = 'students' order by cardinality desc

database是你的库,可以来这么查询

我的这个表差不多是这样,主键刚好对应的就是数据量,另一个班级的索引就是这个class的数量是一致的
所以这就是个明确的区分度的值,而之前也提到过,如果一个表的某个索引字段他的区分度低,跟全表扫描差不多的话
优化器当然也可能会选择不使用索引,直接使用全表扫描,这样就不用质疑说为啥没用起来索引等等

我们日常在使用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;
}
0%