有很多场景需要我们的代码检测一个进程是否存在,常用的一种方式是通过调用脚本通过ps -ef的方式查看,然而其实这种做法并不怎么高效,会fork一个进程出来,还会影响go协程的调度

一种更好的方式是可以通过解析/proc文件夹来得到想要的信息,其实可以通过strace命令查看,ps -ef也是读取了这个路径下的信息

linux-ps-ef-strace

下面分别是java和go的轮子示例

使用正则表达式[0-9]+的原因是/proc路径下还有一些其他文件,其中pid都是数字。

java

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
private static final Pattern numberPattern = Pattern.compile("[0-9]+");

public static boolean processExists(String processName) throws Exception {
final File procFile = new File("/proc");
if (!procFile.isDirectory()) {
throw new Exception("why proc dir is not directory");
}
final File[] listFiles = procFile.listFiles();
if (listFiles == null) {
return false;
}
final List<File> procDir = Arrays.stream(listFiles).filter(f -> numberPattern.matcher(f.getName()).matches()).collect(Collectors.toList());
// find the proc cmdline
for (File file : procDir) {
try {
final byte[] byteArray = FileUtils.readFileToByteArray(new File(file.getCanonicalPath() + File.separator + "cmdline"));
final byte[] bytes = new byte[byteArray.length];
for (int i = 0; i < byteArray.length; i++) {
if (byteArray[i] != 0x00) {
bytes[i] = byteArray[i];
} else {
bytes[i] = (byte) 0x20;
}
}
final String cmdLine = new String(bytes, StandardCharsets.UTF_8);
if (cmdLine.contains(processName)) {
return true;
}
} catch (IOException e) {
// the proc may end during the loop, ignore it
log.error("read file exception ", e);
}
}
return false;
}

go

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
func ProcessExists(processName string) (bool, error) {
result := false
fileInfos, err := ioutil.ReadDir("/proc")
if err != nil {
return false, err
}
for _, info := range fileInfos {
name := info.Name()
matched, err := regexp.MatchString("[0-9]+", name)
if err != nil {
return false, err
}
if !matched {
continue
}
cmdLine, err := parseCmdLine("/proc/" + info.Name() + "/cmdline")
if err != nil {
glog.Error("read cmd line failed ", err)
// the proc may end during the loop, ignore it
continue
}
if strings.Contains(cmdLine, processName) {
result = true
}
}
return result, err
}

func parseCmdLine(path string) (string, error) {
cmdData, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
if len(cmdData) < 1 {
return "", nil
}

split := strings.Split(string(bytes.TrimRight(cmdData, string("\x00"))), string(byte(0)))
return strings.Join(split, " "), nil
}

java 根据线程统计CPU

设计思路

java的ThreadMXBean可以获取每个线程CPU执行的nanoTime,那么可以以这个为基础,除以中间系统经过的纳秒数,就获得了该线程的CPU占比

编码

首先,我们定义一个结构体,用来存放一个线程上次统计时的纳秒数和当时的系统纳秒数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;

@Data
public class ThreadMetricsAux {

private long usedNanoTime;

private long lastNanoTime;

public ThreadMetricsAux() {
}

public ThreadMetricsAux(long usedNanoTime, long lastNanoTime) {
this.usedNanoTime = usedNanoTime;
this.lastNanoTime = lastNanoTime;
}

}

然后我们在SpringBoot中定义一个定时任务,它将定时地统计计算每个线程的CPU信息,并输出到MeterRegistry,当你调用SpringActuator的接口时,你将能获取到这个指标。

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
import com.google.common.util.concurrent.AtomicDouble;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.HashMap;

@Slf4j
@Service
public class ThreadMetricService {

@Autowired
private MeterRegistry meterRegistry;

private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();

private final HashMap<Long, ThreadMetricsAux> map = new HashMap<>();

private final HashMap<Meter.Id, AtomicDouble> dynamicGauges = new HashMap<>();

/**
* one minutes
*/
@Scheduled(cron = "0 * * * * ?")
public void schedule() {
final long[] allThreadIds = threadBean.getAllThreadIds();
for (long threadId : allThreadIds) {
final ThreadInfo threadInfo = threadBean.getThreadInfo(threadId);
if (threadInfo == null) {
continue;
}
final long threadNanoTime = getThreadCPUTime(threadId);
if (threadNanoTime == 0) {
// 如果threadNanoTime为0,则识别为异常数据,不处理,并清理历史数据
map.remove(threadId);
}
final long nanoTime = System.nanoTime();
ThreadMetricsAux oldMetrics = map.get(threadId);
// 判断是否有历史的metrics信息
if (oldMetrics != null) {
// 如果有,则计算CPU信息并上报
double percent = (double) (threadNanoTime - oldMetrics.getUsedNanoTime()) / (double) (nanoTime - oldMetrics.getLastNanoTime());
handleDynamicGauge("jvm.threads.cpu", "threadName", threadInfo.getThreadName(), percent);
}
map.put(threadId, new ThreadMetricsAux(threadNanoTime, nanoTime));
}
}

// meter Gauge相关代码
private void handleDynamicGauge(String meterName, String labelKey, String labelValue, double snapshot) {
Meter.Id id = new Meter.Id(meterName, Tags.of(labelKey, labelValue), null, null, Meter.Type.GAUGE);

dynamicGauges.compute(id, (key, current) -> {
if (current == null) {
AtomicDouble initialValue = new AtomicDouble(snapshot);
meterRegistry.gauge(key.getName(), key.getTags(), initialValue);
return initialValue;
} else {
current.set(snapshot);
return current;
}
});
}

long getThreadCPUTime(long threadId) {
long time = threadBean.getThreadCpuTime(threadId);
/* thread of the specified ID is not alive or does not exist */
return time == -1 ? 0 : time;
}

}

其他配置

依赖配置

pom文件中

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

Prometheus接口配置

application.yaml

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: health,info,prometheus

效果

通过curl命令调用curl localhost:20001/actuator/prometheus|grep cpu

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
jvm_threads_cpu{threadName="RMI Scheduler(0)",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-10",} 0.0
jvm_threads_cpu{threadName="Signal Dispatcher",} 0.0
jvm_threads_cpu{threadName="Common-Cleaner",} 3.1664628758074733E-7
jvm_threads_cpu{threadName="http-nio-20001-Poller",} 7.772143763853949E-5
jvm_threads_cpu{threadName="http-nio-20001-Acceptor",} 8.586978352515361E-5
jvm_threads_cpu{threadName="DestroyJavaVM",} 0.0
jvm_threads_cpu{threadName="Monitor Ctrl-Break",} 0.0
jvm_threads_cpu{threadName="AsyncHttpClient-timer-8-1",} 2.524386571545477E-4
jvm_threads_cpu{threadName="Attach Listener",} 0.0
jvm_threads_cpu{threadName="scheduling-1",} 1.2269694160981585E-4
jvm_threads_cpu{threadName="container-0",} 1.999795692406262E-6
jvm_threads_cpu{threadName="http-nio-20001-exec-9",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-7",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-8",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-5",} 0.0
jvm_threads_cpu{threadName="Notification Thread",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-6",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-3",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-4",} 0.0
jvm_threads_cpu{threadName="Reference Handler",} 0.0
jvm_threads_cpu{threadName="http-nio-20001-exec-1",} 0.0012674719289349648
jvm_threads_cpu{threadName="http-nio-20001-exec-2",} 6.542541277148053E-5
jvm_threads_cpu{threadName="RMI TCP Connection(idle)",} 1.3998786340454562E-6
jvm_threads_cpu{threadName="Finalizer",} 0.0
jvm_threads_cpu{threadName="Catalina-utility-2",} 7.920883054498174E-5
jvm_threads_cpu{threadName="RMI TCP Accept-0",} 0.0
jvm_threads_cpu{threadName="Catalina-utility-1",} 6.80101662787773E-5

Java计算磁盘使用率

https://support.huaweicloud.com/bestpractice-bms/bms_bp_2009.html

华为云文档上的材料值得学习。

翻阅资料

1
2
3
https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats

13 - time spent doing I/Os (ms)

这就意味着如果我想统计一个磁盘在一定周期内的利用率,只需要对这两个数字做差,除以统计的间隔,即就是这段时间内磁盘的利用率

1
2
3
cat /proc/diskstats
253 0 vda 24046 771 2042174 180187 20689748 21411881 527517532 18028256 0 14610513 18201352
253 1 vda1 23959 771 2038022 180153 20683957 21411881 527517532 18028066 0 14610312 18201129

样例代码

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
package com.github.hezhangjian.demo.metrics;

import com.github.hezhangjian.demo.base.module.ShellResult;
import com.github.hezhangjian.demo.base.util.LogUtil;
import com.github.hezhangjian.demo.base.util.ShellUtil;
import com.github.hezhangjian.demo.base.util.StringUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* @author hezhangjian
*/
@Slf4j
public class DiskUtilizationMetrics {

private static final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();

private static long lastTime = -1;

public static void main(String[] args) {
LogUtil.configureLog();
String diskName = "vda1";
scheduledExecutor.scheduleAtFixedRate(() -> metrics(diskName), 0, 10, TimeUnit.SECONDS);
}

private static void metrics(String diskName) {
//假设统计vda磁盘
String[] cmd = {
"/bin/bash",
"-c",
"cat /proc/diskstats |grep " + diskName + "|awk '{print $13}'"
};
ShellResult shellResult = ShellUtil.executeCmd(cmd);
String timeStr = shellResult.getInputContent().substring(0, shellResult.getInputContent().length() - 1);
long time = Long.parseLong(timeStr);
if (lastTime == -1) {
log.info("first time cal, usage time is [{}]", time);
} else {
double usage = (time - lastTime) / (double) 10_000;
log.info("usage time is [{}]", usage);
}
lastTime = time;
}

}

打印CPU使用

1
2
3
4
5
private static void printCpuUsage() {
final com.sun.management.OperatingSystemMXBean platformMXBean = ManagementFactory.getPlatformMXBean(com.sun.management.OperatingSystemMXBean.class);
double cpuLoad = platformMXBean.getProcessCpuLoad();
System.out.println(cpuLoad);
}

打印线程堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void printThreadDump() {
final StringBuilder dump = new StringBuilder();
final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 100代表线程堆栈的层级
final ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds(), 100);
for (ThreadInfo threadInfo : threadInfos) {
dump.append('"');
dump.append(threadInfo.getThreadName());
dump.append("\" ");
final Thread.State state = threadInfo.getThreadState();
dump.append("\n java.lang.Thread.State: ");
dump.append(state);
final StackTraceElement[] stackTraceElements = threadInfo.getStackTrace();
for (final StackTraceElement stackTraceElement : stackTraceElements) {
dump.append("\n at ");
dump.append(stackTraceElement);
}
dump.append("\n\n");
}
System.out.println(dump);
}

打印内存统计信息

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.jerolba</groupId>
<artifactId>jmnemohistosyne</artifactId>
<version>0.2.3</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
private static void printClassHisto() {
Histogramer histogramer = new Histogramer();
MemoryHistogram histogram = histogramer.createHistogram();

HistogramEntry arrayList = histogram.get("java.util.ArrayList");
System.out.println(arrayList.getInstances());
System.out.println(arrayList.getSize());

for (HistogramEntry entry : histogram) {
System.out.println(entry);
}
}

打印死锁

javadoc中指出,这是一个开销较大的操作

1
2
3
4
5
6
7
8
private static void printDeadLock() {
final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
final long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
for (long deadlockedThread : deadlockedThreads) {
final ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);
System.out.println(threadInfo + "deadLocked");
}
}

中间件在很多系统中都存在

在一个系统里面,或多或少地都会有中间件的存在,总会有数据库吧,其他的如消息队列,缓存,大数据组件。即使是基于公有云构筑的系统,公有云厂商只提供广泛使用的中间件,假如你的系统里面有很多组件没那么泛用,那么就只能自己维护,如ZooKeeperEtcdPulsarPrometheusLvs

什么是中间件adapter

中间件adapter指的是和中间件运行在一起(同一个物理机或同一个容器),使得中间件和商用系统中已有的组件进行对接,最终使得该中间件达到在该系统商用的标准。像Prometheus的众多exporter,就是将中间件和已有的监控系统(Prometheus)进行对接的adpater

为什么不修改中间件源码直接集成

原因可以有很多,这里我列出几点

源码修改容易,维护困难

很多时候不是社区通用需求,无法合并到社区主干。后续每次中间件版本升级,源码的修改就要重新进行一次。社区大版本代码重构,有的甚至不知道如何修改下去。并且对研发人员的技能要求高。

源码与团队技术栈不同,修改困难

这是最常见的,像java团队维护erlang写的rabbitmq

和其他系统对接,有语言要求

XX监控系统,只能使用X语言接入,但中间件使用Y语言写的,怎么办?adapter的能力就体现出来了。

为什么在商用系统中中间件做不到开箱即用

在商用系统中,对一个新引入的中间件,往往有如下能力上的诉求,原生的中间件很难满足

  • 适配原有的监控系统
  • 适配原有的告警系统
  • 适配原有的证书系统
  • 适配原有的备份系统(如果该中间件有状态)
  • 适配原有的容灾系统(如果该中间件有状态)
  • 自动化能力(适配部署、账号创建、权限策略创建)
  • 对外暴露时封装一层接口
  • 应用程序和中间件的服务发现

有时候,业务也会根据业务的需求对中间件做一些能力增强,这部分需求比较定制,这里无法展开讨论了。

我们来逐一讨论上面列出的能力诉求,凡是adapter能实现的功能,对中间件做修改也能实现,只不过因为上一节列出的原因,选择不在中间件处侵入式修改。

适配原有的监控系统

监控系统获取数据,往往是推拉两种模式,如果该中间件原生不支持和该监控系统对接。我们就可以让adapter先从中间件处取得监控数据,再和监控系统对接

适配原有的告警系统

如果中间件发生了不可恢复的错误,如写事务文件失败,操作ZooKeeper元数据失败,可以通过adapter来识别中间件是否发生了上述不可恢复的错误,并和告警系统对接,发出告警。

适配原有的证书系统

这一点也很关键,开源的中间件,根据我的了解,几乎没有项目做了动态证书轮换的方案,证书基本都不支持变更。而出色的商用系统是一定要支持证书轮换的。不过很遗憾的是,这些涉及到TLS握手的关键流程,adapter无法干涉这个流程,只能对中间件进行侵入式修改。

适配原有的备份系统

通过adapter对中间件进行定期备份、按照配置中心的策略备份、备份文件自动上传到文件服务器等。

适配原有的容灾系统

这个视中间件而定,有些中间件如Pulsar原生支持跨地域容灾的话,我们可能做一做配置就好了。另外一些,像mysqlmongo这种,可能我们还需要通过adapter来进行数据同步。不过这个时候adapter负责的职责就大了,还包括了容灾能力。

自动化能力

自动化部署

比如ZooKeeperKafkafilebeat在安装的时候,要求填写配置文件,我们就可以让adapter来自动化生成配置或更新配置

账号和策略的创建更新

kubernetesmysqlmongo,我们可以在安装的时候通过adapter来自动化创建或更新

对外暴露时封装一层接口

封装接口常用于中间件的提供者,出于种种原因,如中间件原本接口能力太大、中间件原本接口未做权限控制、中间件原本接口未适配期望的权限框架等。我们可以用adapter封装实现一层新的接口对外暴露。

应用程序和中间件的服务发现

应用程序发现中间件

应用程序与中间件的连接,说的简单一点就是如何获取Ip,如果是基于kubernetes的部署,那么不推荐配置Ip,最好是配置域名,因为Ip会跟着容器的生命周期变化。首先,你的应用程序并不会因为中间件的一个容器重启了来重建客户端,往往是通过一个简单重连的方式连接到新的中间件容器继续工作。其次,我们的运维人员也不会每时每刻盯着容器Ip是否变化来进行配置吧。以下图为例,域名的配置要优于Ip的配置。

application-discover-middleware

截止到目前,我们只需要一个静态配置,使得应用程序可以连接到中间件。最好这个配置是可以修改的,这样我们还可以继承蓝绿、灰度发布的能力。

中间件到业务程序的发现

这个模式常用于负载均衡中间件如LvsNginx自动维护后端列表,我们可以通过adapter来从注册中心获取后端服务的实例信息,并实时更新。

总结

在商用系统中,中间件并没有想象中的那么开箱即用,本文讲述了一些中间件集成到商用系统中需要具备的能力。在对中间件侵入式修改没有技术能力或不想对中间件进行侵入式修改的场景。选用团队常用的、占用资源少的语言来开发中间件adapter应该是更好的选择。

翻译自 https://medium.com/flutter-community/flutter-best-practices-and-tips-7c2782c9ebb5

最佳实践是一个领域内可接受的专业标准,对于任何编程语言来说,提高代码质量、可读性、可维护性和健壮性都非常重要。
这是一些设计和开发 Flutter 应用程序的最佳实践。

命名规范

类名、枚举、typedef、扩展名应该使用大驼峰命名方式

1
2
3
4
class MainScreen { ... }
enum MainItem { .. }
typedef Predicate<T> = bool Function(T value);
extension MyList<T> on List<T> { ... }

libraries、包、目录、源文件名采用蛇形命名。

1
2
library firebase_dynamic_links;
import 'socket/socket_manager.dart';

变量、参数等使用蛇形命名

1
2
3
4
5
6
var item;
const bookPrice = 3.14;
final urlScheme = RegExp('^([a-z]+):');
void sum(int bookPrice) {
// ...
}

总是指定变量类型

如果已经知道变量值的类型,那么应该指定变量的类型,尽量避免使用var

1
2
3
4
5
6
7
8
9
10
11
//Don't
var item = 10;
final car = Car();
const timeOut = 2000;


//Do
int item = 10;
final Car bar = Car();
String name = 'john';
const int timeOut = 20;

尽量用is替代as

as运算符会在无法进行类型转换的时候抛出异常,为了避免抛出异常,使用is操作符

1
2
3
4
5
6
7
//Don't
(item as Animal).name = 'Lion';


//Do
if (item is Animal)
item.name = 'Lion';

使用???.操作符

尽量使用???.操作符替代if-null检测

1
2
3
4
5
6
7
8
9
10
11
12
//Don't
v = a == null ? b : a;

//Do
v = a ?? b;


//Don't
v = a == null ? null : a.b;

//Do
v = a?.b;

使用扩散集合(Spread Collections)

当items已经存在另外的集合中时,扩展集合语法可以简化代码

1
2
3
4
5
6
7
8
9
//Don't
var y = [4,5,6];
var x = [1,2];
x.addAll(y);


//Do
var y = [4,5,6];
var x = [1,2,...y];

使用级联操作符

在同一Object上的连续操作可以使用级联操作符..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Don't
var path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
path.close();


// Do
var path = Path()
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, 0)
..close();

使用原生字符串(Raw Strings)

原生字符串可以不对$\进行转义

1
2
3
4
5
6
//Don't
var s = 'This is demo string \\ and \$';


//Do
var s = r'This is demo string \ and $';

不要使用null显示初始化变量

dart中的变量默认会被初始化为null,设置初始值为null是多余且不必要的。

1
2
3
4
5
6
//Don't
int _item = null;


//Do
int _item;

使用表达式函数体

对于只有一个表达式的函数,你可以使用表达式函数。=>标记着表达式函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Don't
get width {
return right - left;
}
Widget getProgressBar() {
return CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
);
}


//Do
get width => right - left;
Widget getProgressBar() => CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
);

避免调用print()函数

print()debugPrint()都用来向控制台打印消息。在print()函数中,如果输出过大,那么Android系统可能会丢弃一些日志。这种情况下,你可以使用debugPrint

使用插值法来组织字符串

使用插值法组织字符串相对于用+拼接会让字符串更整洁、更短。

1
2
3
4
5
6
//Don’t
var description = 'Hello, ' + name + '! You are ' + (year - birth).toString() + ' years old.';


// Do
var description = 'Hello, $name! You are ${year - birth} years old.';

使用async/await而不是滥用future callback

异步代码难以阅读和调试,asyncawait语法增强了可读性

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
// Don’t
Future<int> countActiveUser() {
return getActiveUser().then((users) {

return users?.length ?? 0;

}).catchError((e) {
log.error(e);
return 0;
});
}


// Do
Future<int> countActiveUser() async {
try {
var users = await getActiveUser();

return users?.length ?? 0;

} catch (e) {
log.error(e);
return 0;
}
}

widget中使用const

如果我们把widget定义为const,那么该widgetsetState的时候不会重建。通过避免重建来提升性能

1
2
3
4
5
6
7
8
9
10
Container(
padding: const EdgeInsets.only(top: 10),
color: Colors.black,
child: const Center(
child: const Text(
"No Data found",
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.w800),
),
),
);

Go符号表简介

参考资料

https://medium.com/a-journey-with-go/go-how-to-take-advantage-of-the-symbols-table-360dd52269e5

在参考资料的基础上,尝试复写已存在的变量,并更新Go版本到1.16。

符号表由编译器创建维护,用于存储程序相关的信息,比如函数或全局变量。

符号表

每个由Go编译的二进制程序默认包含符号表。举个例子

1
2
3
4
5
6
7
8
9
package main

import "fmt"

var AppVersion string

func main() {
fmt.Println(`Version: ` + AppVersion)
}
1
2
go build . -o main
nm main

符号表输出非常大,截取其中几行,如下

1
2
3
4
000000000114b000 s _main..inittask
000000000115fc10 b _main.AppVersion
00000000010a3120 t _main.main
0000000001158d10 d _runtime.buildVersion

符号前面用b标识是未初始化的数据。我们之前的变量AppVersion的确也没有初始化。d代表已初始化的数据。t代表文字符号,由这两者组成函数。

go也包装了nm命令,使用go tool nm可以得到相同的结果。

自定义变量

运行命令go build时,将会执行两个阶段:编译和链接。链接阶段从编译阶段的产物中派生出二进制文件。为了实现这一目标,链接器使用符号表将符号重新定位到最终的二进制文件。

Go允许我们通过-X命令来复写符号表。这是例子

1
go build -o ex -ldflags="-X main.AppVersion=v1.0.0"

重新运行程序,程序打印

1
Version: v1.0.0

重新运行nm命令,发现变量已经是初始化状态了

1
0000000001158c80 d _main.AppVersion

该方式不仅可以复写未初始化的变量,同时也可以复现已初始化的变量

1
go build -o ex -ldflags="-X main.AppVersion=v1.0.0,main.AppName=k8s"

感兴趣的朋友可以用这个程序试一下输出是什么

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

var AppVersion string
var AppName = "app"

func main()
fmt.Println(`appName: ` + AppName)
fmt.Println(`Version: ` + AppVersion)
}

Debug

符号表的目的是确保使用的标识符在使用之前已经得到了很好地声明。这意味着一旦程序构建完成,它就不再需要该表了。但是,默认情况下,出于调试目的,符号表嵌入在Go二进制文件中。在了解如何从二进制文件中删除它之前,让我们了解如何利用它。使用go build构建应用程序

1
go build -gcflags "-N -l" -ldflags="-compressdwarf=false -X main.AppVersion=v1.0.0 -X main.AppName=k8s" main.go

上述参数解释

  • Go 1.11 之后,为了压缩二进制文件的大小,debug信息被压缩了,OSX的gdb不能识别压缩的DWARF信息
  • -N代表禁止优化,不要在生产环境上开启,此处仅为演示使用
  • -l参数代表禁止内联,也建议不要在生产环境上开启,此处仅为演示使用
1
gdb main

image-20210715104937454

我们可以通过-s去掉符号表信息

1
go build -gcflags "-N -l" -ldflags="-s -X main.AppVersion=v1.0.0 -X main.AppName=k8s" main.go

gdb就看不到其他信息了

image-20210715105230494

二进制文件大小

去掉符号表,会使debug变得更加困难,但可以有效降低文件大小。MAC上两个文件的差异是

image-20210715105637847

1
2
go build -gcflags "-N -l" -ldflags="-compressdwarf=false -X main.AppVersion=v1.0.0 -X main.AppName=k8s" main.go
go build -gcflags "-N -l" -ldflags="-s -X main.AppVersion=v1.0.0 -X main.AppName=k8s" -o main-s main.go

如果在linux上,可以开启压缩dwarf特性,差距差不多在

image-20210715110105760

百分之20~百分之30之间

Varint编码

一个简单的消息

假设你有如下的protobuf定义

1
2
3
4
5
6
7
syntax = "proto3";
package test;
option go_package = "test";

message Test1 {
optional int32 a = 1;
}

在应用中,你创建了一个TestMessage实例,将a设置为150。接下来将消息序列化。检查序列化后的消息,你会发现是这么一个字节数组

1
0x08 0x96 0x01

Base 128 Varints

这是一个varints编码。varints是一种使用1个或多个字节来序列化整数的方式。更小地数字占更少的字节。

varint 中的每个字节,除了最后一个字节,都设置了最高有效位 (msb)——这表明还有更多的字节需要处理。 每个字节的低 7 位用于以 7 位为一组存储数字的二进制补码表示,采用小端字节存储。

举个例子,对于1来说,由于只有1个字节,所以没有设置msb

1
0000 0001

这是300,有一些复杂

1
1010 1100 0000 0010

怎么发现这是300的?首先,先把每个字节的msb去掉,变换为

1
010 1100 000 0010

再进行小端转换,变为

1
000 0010 010 1100

组合为

1
100101100

即就是300

使用Delve对Go程序进行Debug

翻译自

https://medium.com/a-journey-with-go/go-debugging-with-delve-core-dumps-384145b2e8d9

CoreDump是异常退出程序的内存快照。可以用来死后debug来找出crash发生的原因以及牵连的变量。通过GOTRACEBACKGo提供了控制程序崩溃时的输出。变量还可以强制生成CoreDump,使得debug成为可能。

GOTRACEBACK

GOTRACEBACK控制着程序崩溃时的输出,可以取以下的值

  • none 不展示任何协程堆栈追踪
  • single 默认选项,打印当前协程堆栈
  • all 展示所有用户创建的协程堆栈
  • system 展示所有协程堆栈,包括go运行时
  • crashsystem相似,不过同时会生成core dump文件

最后一个选项给了我们在crash的时候,debug程序的能力。如果您没有获得足够的日志,或者崩溃不可重现,这可能是一个不错的选择。 让我们以下面的程序为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "math/rand"

func main() {
for {
var sum int
n := rand.Intn(1e6)
sum += n
if sum % 42 == 0 {
panic(":(")
}
}
}

程序将很快crash

image-20210712180555586

我们不能从堆栈看出来什么值导致了crash。添加日志可能是一个解决方案,但是我们不是所有情况都知道在哪里添加日志。当一个问题无法重现时,添加日志只是一个后知后觉,不断寻找的思路。

让我们加上环境变量GOTRACEBACK=crash。由于所有协程和runtime堆栈都会输出,输出更加详细。并且我们还有了coredump文件

1
2
3
4
5
go build -gcflags=all="-N -l" .
export GOTRACEBACK=crash
./core_dump
# 如果您的core_dump没有生成,可能是coredump size配置为0,如下命令将coredump配置为1MB大小
ulimit -c 1048576

我们有

image-20210712183024428

Core-dump文件可被Go delve或GDB分析

Delve

安装Delve

1
go install github.com/go-delve/delve/cmd/dlv@latest

Delve 是用 Go 编写的 Go 程序的调试器。 它允许通过在用户代码和运行时的任何位置添加断点来逐步调试,甚至可以使用将二进制文件和CoreDump作为参数的命令 dlv core 调试CoreDump。

通过dlv core core_dump core.2716来调试coredump。然后通过bt命令打印堆栈,并且展示程序造成的panic

image-20210712183456364

我们看到7发生了panic,然后通过frame 9查看第9个堆栈

image-20210712185150712

这展示了panic时的代码,然后通过命令locals打印本地变量,帮助我们搞明白什么变量参与了crash

image-20210712185206700

随机生成的n的值是203300。sum呢?通过vars main来查看main包里的sum的值

image-20210712185307402

通过这个就可以推理出,sum的和刚好整除42,导致程序panic

Go中的指针

翻译自

https://go101.org/article/pointer.html

尽管Go吸收了很多其他语言的特性,但Go总体来说是一个C家族语言。其中一个证据就是Go也支持指针。Go指针和C指针在许多方面非常相似,但其中也有一些不同。本文将会列举Go指针的概念和细节。

Memory Address 内存地址

内存地址指的是整个系统管理(通常由操作系统管理)的内存空间中的偏移量(byte的个数)。

通常,内存地址被存储成一个无符号(整型)字。字长在32位机器上是4字节,在64位机器上是8字节。所以理论最大寻址范围,在32位机器上是
2^32,也就是4GB。在64位机器上是2^64,也就是16EB(1EB=1024PB,1PB=1024TB,1TB=1024GB)。

内存地址通常用Hex表达模式书写,如0x1234CDEF

Value Address 值地址

值的地址代表这个值在内存段中的起始地址

什么是指针?

指针是Go的一种类型。指针用来存储内存地址。跟C语言不同,为了安全的原因,Go指针上有一些限制。

Go指针类型和值

在Go中,未定义的指针类型可以被写成*TT可以是任意类型。T*T的基础类型。

我们也可以自定义指针类型,通常由于未定义指针类型拥有更好的可读性,不推荐使用自定义指针类型。

如果一个定义的指针类型的底层类型(underlying type)是*T,那么该指针的基础类型是T。这个基础类型相同的指针也是同一种类型。样例

1
2
3
4
5
6
7
8
9
// 未定义的指针类型,基础类型是int
*int
// 未定义的指针类型,基础类型是*int
**int

// Ptr是一个定义指针类型,基础类型是int
type Ptr *int
// PP是一个定义指针类型,基础类型是Ptr
type PP *Ptr

指针的零值是nil,不存储任何地址。

基础类型为T的指针仅能存储T类型值的地址

如何获得指针值、什么是可寻址值

有两种方式可用于获取非nil的指针值

  • go内置的new函数,可以分配任何类型的内存。new(T)在内存中分配一个T值的空间,然后返回T值的地址。分配的值是T
    类型的零值。返回的地址就是一个T类型的指针
  • 我们还可以直接获取可寻址值的地址。对于一个可寻址类型T的值t,我们可以使用&t来获取t的地址,`&操作符用来获取值地址。

通常上来说,可寻址值意味着该值存放在内存中的某处。现在,我们只需要知道任何变量都是可寻址的,同时常数、函数调用和显示转换的结果是不可寻址的。变量声明的时候,Go运行时将会为这个变量分配一片内存,这片内存的开始地址就是这个变量的地址。

Pointer Derefernece 指针解引用

对于一个基础类型为T的指针类型p,你如何获取指针中存储的值(或者说,被指针指向的值)?只需要使用表达式*p*
被称为解引用操作符。指针的解引用是取地址的逆运算。*p的返回值是T类型,也就是p的基础类型。

nil指针进行解引用会导致运行时异常。

这个程序展示了地址获取和解引用的例子:

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
package main

import "fmt"

func main() {
// p0指向int的零值
p0 := new(int)
// hex表达的地址
fmt.Println(p0)
// 0
fmt.Println(*p0)

// x是p0指向值的拷贝
x := *p0
// 都取得x的地址
// x, *p1, *p2 的值相同
p1, p2 := &x, &x
// true
fmt.Println(p1 == p2)
// false
fmt.Println(p0 == p1)
// p3 和 p0 也存储相同的地址
p3 := &*p0
fmt.Println(p0 == p3)
*p0, *p1 = 123, 789
// 789 789 123
fmt.Println(*p2, x, *p3)

// int, int
fmt.Printf("%T, %T \n", *p0, x)
// *int, *int
fmt.Printf("%T, %T \n", p0, p1)
}

下图揭示了上面程序存储的值间关系

image-20210712093648121

为什么我们需要指针

让我们先来看一个样例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func double(x int) {
x += x
}

func main() {
var a = 3
double(a)
fmt.Println(a) // 3
}

上例中的double函数预期对输入值进行双倍处理。但是它失败了。为什么?因为所有值的分配,包括函数参数的传递,都是值拷贝。double
函数操作的x只是a变量的拷贝,而不是a变量。

修复上例的一种方式是让double函数返回一个新值。但这并不是所有场景都适用。下例展示了另一种使用指针的方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func double(x *int) {
*x += *x
// 这行只为了解释用途
x = nil
}

func main() {
var a = 3
double(&a)
fmt.Println(a) // 6
p := &a
double(p)
fmt.Println(a, p == nil) // 12 false
}

我们可以发现,通过将参数改为指针类型,传递的指针参数&a和它的拷贝x都指向相同的值,所以在*x上进行的修改,在a
上也体现了出来。同时,因为参数传递都是值拷贝,上面将x赋值为nil,在p上也不生效。

简而言之,指针提供了操作值的间接方式。大部分语言没有指针的概念。然而,指针的概念只是隐藏在了语言的其他概念中。

返回局部变量指针在Go是安全的

和C语言不通,Go支持垃圾回收,所以返回局部变量的指针在Go中是绝对安全的

1
2
3
4
func newInt() *int {
a := 3
return &a
}

Go指针的限制

为了安全原因,相比C语言来说,Go在指针上做了一些限制。通过这些限制,Go保持了指针带来的收益,并且避免了危险的指针使用。

Go指针不支持算术操作

在Go中,指针不能进行算术运算。对于指针pp++p-2都是非法的。

如果指针p指向一个数值,编译器会将*p++识别为一个合法的语句,并解析为(*p)++。换句话说,*p操作的优先级高于++--操作符。样例

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

import "fmt"

func main() {
a := int64(5)
p := &a

// 下面的语句无法编译
/*
p++
p = (&a) + 8
*/

*p++
fmt.Println(*p, a) // 6 6
fmt.Println(p == &a) // true

*&a++
*&*&a++
**&p++
*&*p++
fmt.Println(*p, a) // 10 10
}

指针值不能转换为任意的指针类型

在Go中,T1的指针值可以被隐式或是显示地转换为T2,需要满足如下两个条件

  • T1T2的底层类型相同(忽略结构体Tag)。特别地,如果T1T2都是未定义类型并且它们的底层类型相同(考虑结构体Tag),可以进行隐式转换。
  • T1T2都是未定义指针类型,并且它们的基础类型的底层类型相同(忽略结构体Tag)

举个例子,有如下类型

1
2
3
type MyInt int64
type Ta *int64
type Tb *MyInt

有如下事实

  • *int64类型的值可以被隐式转换为Ta类型,反过来也是可以的。因为它们的底层类型都是*int64
  • *MyInt类型的值可以被隐式转换为Tb类型,反过来也行。因为它们的底层类型都是*MyInt
  • *MyInt类型的值可以被限制转换为*int64,反之亦然。因为它们的基础类型的底层类型都是int64
  • 即使是显示转换,Ta类型的值也不能直接转换为Tb。因为Ta和底层类型和Tb不同,并且都是定义指针类型。不过可以进行连续几次转换,将
    Ta类型的pa间接转化为Tb类型。 先将pa转化为*int64类型(因为基础类型的底层类型都是int64),再将*int64类型转换为
    *MyInt类型,再将*MyInt类型转化为*Tb类型。Tb((*MyInt)((*int64)(pa)))

上面的值通过任何安全的手段,都不能转化为类型*uint64

任意两个指针的值不能比较

在Go中,指针可以通过==!=符号比较。如果满足如下任意一个条件,那么两个Go指针的值可以进行比较

  • 两个Go指针类型一致
  • 指针值可以隐式转换为另一类型
  • 两个指针中的一个且仅一个用无类型 nil 标识符表示。
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
package main

func main() {
type MyInt int64
type Ta *int64
type Tb *MyInt

// 4个不同类型的指针零值
var pa0 Ta
var pa1 *int64
var pb0 Tb
var pb1 *MyInt

// 下面这6行都可以正常编译
// 比较结果都为true
// 指针可以隐式转换
_ = pa0 == pa1
// 指针类型一致
_ = pb0 == pb1
_ = pa0 == nil
_ = pa1 == nil
_ = pb0 == nil
_ = pb1 == nil

// 这三行都不能正常编译
/*
_ = pa0 == pb0
_ = pa1 == pb1
_ = pa0 == Tb(nil)
*/
}

指针值不能赋值给其他指针类型

指针值互相赋值的条件和比较的条件一样

有手段打破Go对指针的限制

unsafe 标准包提供的机制(特别是 unsafe.Pointer 类型)可以用来打破 Go 中对指针的限制。 unsafe.Pointer 类型类似于 C 中的
void*。一般不推荐使用unsafe

翻译自

https://medium.com/a-journey-with-go/go-what-is-the-unsafe-package-d2443da36350

正文

包的名称指引着我们的使用。了解此包可能不安全的原因,让我们首先查看如下文档

1
2
unsafe包包含可以绕过Go程序类型安全的操作
引用了unsafe的包,可能会无法跨平台、跨设备,并且Go1 兼容性指南不保证这些包的兼容

因此,该名称被用作对为 Go 提供类型的安全性的反面。 现在让我们深入研究文档中提到的这两点。

类型安全

在Go中,每个变量都拥有一个类型,在赋值给另外一个变量之前,可以转换为另外的类型。在此转换期间,Go 执行此数据的转换以适应所请求的类型。 这是一个例子

1
2
3
4
5
6
7
8
9
10
11
package unsafe_test

import "testing"

func TestConvert(t *testing.T) {
// -1 二进制表达 11111111
var i int8 = -1
// -1 二进制表达 11111111 11111111
var j = int16(i)
println(i, j)
}

输出

1
-1 -1

unsafe包让我们直接操作这个变量的内存地址,直接获取里面的二进制值。在绕过类型约束时,我们可以随意使用它。

1
2
3
var k uint8 = *(*uint8)(unsafe.Pointer(&i))
println(k)
// 255 is the uint8 value for the binary 11111111

Go1 兼容性指南

unsafe包可能会依赖于Go的底层实现。Go可能会做出破坏性的改动。

Go 反射包中使用Unsafe

reflect包是使用最多的一个包。反射基于空interface包含的数据。为了读取数据,Go只是将我们的变量转化为一个空接口,并通过映射一个结构体来读取它们,该结构体匹配空接口的内部表示与指针地址处的内存。

1
2
3
4
5
6
7
8
9
func ValueOf(i interface{}) Value {
[...]
return unpackEface(i)
}
// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
[...]
}

变量 e 现在包含有关该值的所有信息,例如类型或该值是否已导出。 反射还使用 unsafe 包通过直接在内存中更新值来修改反射变量的值。

Go Sync包使用Unsafe

sync.Pool 这些池通过一段go协程都能访问的内存片段来共享数据。这访问模式和C语言数组的访问方式很像

1
2
3
4
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
return (*poolLocal)(lp)
}

l是内存片段,i是数字。函数 indexLocal 只是读取这个内存段——它包含 X个poolLocal 结构——与它读取的索引相关的偏移量。 存储指向完整内存段的单个指针是实现共享池的一种非常轻量的方式。

在Go runtime包中的使用

Go 在runtime包也大量使用 unsafe 包,因为它必须处理内存操作,如栈分配或释放栈内存。 栈由其结构中的两个边界表示:

1
2
3
4
type stack struct {
lo uintptr
hi uintptr
}

unsafe包可以完成这个操作

1
2
3
4
5
6
7
func stackfree(stk stack) {
[...]
v := unsafe.Pointer(stk.lo)
n := stk.hi - stk.lo
// 内存基于栈上的指针释放
[...]
}

开发平时使用

unsafe包的一个很好的用法是转换具有相同底层数据结构的不同结构体,这是转换器无法实现的

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

import (
"testing"
"unsafe"
)

type A struct {
A int8
B string
C float32
}

type B struct {
D int8
E string
F float32
}

func TestStructConvert(t *testing.T) {
a := A{A: 1, B: `foo`, C: 1.23}
b := *(*B)(unsafe.Pointer(&a))
println(b.D, b.E, b.F) // 1 foo 1.23
}

不使用三方库

协程Sleep方式

1
2
3
4
5
6
go func() {
for true {
fmt.Println("Hello !!")
time.Sleep(1 * time.Second)
}
}()

使用ticker方式1

1
2
3
4
5
6
7
8
9
10
ticker := time.NewTicker(1 * time.Second)
go func() {
for range ticker.C {
fmt.Println("Hello !!")
}
}()

// wait for 10 seconds
time.Sleep(10 *time.Second)
ticker.Stop()

使用ticker方式2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
done := make(chan bool)
ticker := time.NewTicker(1 * time.Second)

go func() {
for {
select {
case <-done:
ticker.Stop()
return
case <-ticker.C:
fmt.Println("Hello !!")
}
}
}()

// wait for 10 seconds
time.Sleep(10 *time.Second)
done <- true

参考

https://stackoverflow.com/questions/53057237/how-to-schedule-a-task-at-go-periodically

0%