环境准备

需要有一个运行的java程序,如果你已经有了运行中的java程序,请跳过这一节,示例,我启动自制的kafka镜像

1
docker run ttbb/kafka:mate

找到java程序的pid

ps -ef或者jps均可,其中jps需要安装jdk

image-20210929143232887

安装arthas

1
2
3
wget https://github.com/alibaba/arthas/releases/download/arthas-all-3.5.4/arthas-bin.zip
mkdir -p arthas
unzip arthas-bin.zip -d arthas

使用arthas连接到目标程序

image-20210929143537663

开始profiler

1
profiler start

如果出现Perf events unavailable. See stderr of the target process.如图所示

image-20210929143621015

需要在docker所在虚拟机上执行如下命令

1
2
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo 0 > /proc/sys/kernel/kptr_restrict

如果是mac用户

1
docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh

执行上述命令进入docker所在的虚拟机操作即可

注意,在部分docker版本中,有可能还无法进行profiler采集,您可能需要以特权方式启动容器,不过,为了定位性能问题,这总是值得付出的,不是吗?

image-20210929151106090

等待profiler一段时间

一般等待一分钟即可

结束profiler

1
profiler stop

image-20210929151209352

结束

Congratulations,完成了火焰图的输出,现在你可以使用火焰图来分析执行时间较长的方法啦

image-20210929151506122

今天看到了https://mp.weixin.qq.com/s/tTipcU8MKxTpFurWNEUIww 中热迁移网络句柄的功能,忍不住自己在机器上实验了一下,确实可以实现网络句柄的迁移。实验代码如下

old_server

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 (
"net"
"syscall"
)

func main() {
tcpAddr := net.TCPAddr{Port: 8001}
tcpLn, err := net.ListenTCP("tcp4", &tcpAddr)
if err != nil {
panic(err)
}
f, _ := tcpLn.File()
fdNum := f.Fd()
data := syscall.UnixRights(int(fdNum))
// 与新版本sidecar通过Unix Domain Socket建立链接
raddr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
uds, _ := net.DialUnix("unix", nil, raddr)
// 通过UDS,发送ListenFD到新版本sidecar容器
_, _, _ = uds.WriteMsgUnix(nil, data, nil)
// 停止接收新的request,并且开始排水阶段
tcpLn.Close()
}

new_server

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

import (
"fmt"
"net"
"net/http"
"os"
"syscall"
"time"
)

func main() {
// 新版本sidecar 接收ListenFD,并且开始对外服务
// 监听UDS
addr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
unixLn, _ := net.ListenUnix("unix", addr)
conn, _ := unixLn.AcceptUnix()
buf := make([]byte, 32)
oob := make([]byte, 32)
time.Sleep(5 * time.Second)
// 接收 ListenFD
_, oobn, _, _, _ := conn.ReadMsgUnix(buf, oob)
scms, _ := syscall.ParseSocketControlMessage(oob[:oobn])
if len(scms) > 0 {
// 解析FD,并转化为 *net.TCPListener
fds, _ := syscall.ParseUnixRights(&scms[0])
f := os.NewFile(uintptr(fds[0]), "")
ln, _ := net.FileListener(f)
tcpLn, _ := ln.(*net.TCPListener)
http.Serve(tcpLn, &MyHttp{})
}
}

type MyHttp struct {
}

func (*MyHttp) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello world")
}

实验结果

  • 先启动new_server
  • 再启动old_server
  • 然后访问localhost:8001,返回hello world,这是new_server返回的结果

代码地址

https://github.com/hezhangjian/go_demo/tree/main/demo_base/fd_migrate

本篇文章探讨镜像仓库registry的高可用

镜像仓库高可用无单点故障涉及那些场景

镜像仓库对外提供访问无单点故障

镜像仓库对外提供的访问点保持高可用

镜像仓库的数据存储高可用

存储在镜像仓库中的数据都得是高可用的

镜像仓库无单点故障技术关键点

镜像仓库对外提供访问无单点故障

和上一篇文章一样,如果IaaS能提供ELB,我们最好是使用ELB,或者使用浮动IP的方式替换

镜像仓库的数据存储高可用

  • 配置镜像仓库使用IaaS的S3存储
  • 配置镜像仓库使用本地存储,通过共享文件路径存储来实现高可用,如Glusterfs
  • 配置镜像仓库使用S3存储,自建兼容S3 API的存储Server

通常会使用共享存储来做到镜像仓库存储的高可用

方案概述

那么其实镜像仓库的高可用方案就是对上面方案的组合,下面我们举几个例子

镜像仓库依赖组件部署方式

MinIoKeepAlivedregistry都推荐使用容器部署,方便运维管理,但是镜像推荐内置到虚拟机中,不依赖镜像仓库或其他组件,避免循环依赖

使用IaaS的S3存储 + 负载均衡组件

这是最简单的方案,得益于云厂商提供的S3存储和负载均衡组件,我们可以进行很简单的配置,并部署一台以上的registry,如下图所示

kubernetes-registry-ha-s3

自建兼容S3存储 + KeepAlived浮动Ip

我们可以自己搭建MinIo集群来作为兼容S3存储,由于MINIO最低部署4个节点,我们需要根据故障域机器来选择部署MINIO的数目,比如,故障域是三台物理机,我们部署4节点就不妥。原因是,4节点,总会有一台物理机上会部署2个minio节点,如果这台物理机挂掉,就会导致单点故障。所以,如果故障域为三台物理机,我们最好部署6节点,可容忍一台物理机宕机。其他的节点,读者也可以自行测算。

下图是假设三台物理机,minio6副本场景下的部署示意图

kubernetes-registry-ha-minio

注,为了图的美观,并未画出所有的连线

MINIO关键配置

  • 节点数目6个
  • EC2

k8s高可用无单点故障涉及那些场景

k8s 节点添加、pod添加等增删查改无单点故障

需要元数据的存储和处理能力高可用

k8s对外的apiServer(如worker)无单点故障

worker node和其他组件访问apiServer路径高可用

k8s无单点故障技术关键点

元数据存储

通过etcd存储元数据,etcd三节点集群保证高可用

元数据处理

通过多个kube-controllerkube-scheduler节点来保证高可用

worker节点请求数据通过多ip或负载均衡来保证

节点请求通信通过多Ip或负载均衡来保证高可用,这里也有几种方式

IaaS厂商可提供负载均衡的场景下

如下图所示,可将worker node的访问地址指向负载均衡的地址

kubernetes-ha-iaas-lb

私有化部署KeepAlived

私有化部署场景常用keepAlived提供浮动IP来给worker node或其他组件访问,如下图所示

kubernetes-ha-keepalived

私有化部署加上负载均衡组件

如果你觉得同一时刻只有单个apiServer工作会成瓶颈,也可以使用KeepAlivedNginxHaProxy来对ApiServer做负载均衡

kubernetes-ha-keepalived-nginx

为了简化图像,只画出了master1上的Nginx向后转发的场景。

至于Nginx和KeepAlived如何部署,推荐采用容器化的部署模式,方便进行监控和运维;但是镜像不从镜像仓库拉取,而是保存在master节点上,这样虽然升级复杂一点,但是这样子kubernetes的高可用就不依赖镜像仓库了,不会和镜像仓库形成循环依赖,更不会影响镜像仓库的高可用方案,大大简化了后续的技术方案。(因为镜像仓库可能会占据较大的存储空间,可能会和master节点分离部署,这时会作为worker节点连接master节点)。

前几天,我的同事碰到了一个问题,是关于GoFrame 框架中数据字段的更新问题,数据中有一个status字段,他本来不想更新,但是却更新成了0。

相信看到描述,已经有经验丰富的专家可以猜到是数据部分更新导致的问题。

没错,就是因为数据库部分更新,把0这个值当成了需要更新的值刷新到了数据库中。

中间是复现问题的流程及代码细节,不感兴趣的可以直接拉到最后

复现问题

创建一个数据表

1
2
3
4
5
6
7
8
CREATE TABLE health_check
(
id VARCHAR(50) PRIMARY KEY,
str_null VARCHAR(50) NULL,
str_not_null VARCHAR(50) NOT NULL,
int_null INT NULL,
int_not_null INT NOT NULL
)

插入一条数据

image-20210819104835758

数据库module的声明

1
2
3
4
5
6
7
type HealthCheck struct {
Id string `orm:"id"`
StrNull *string `orm:"str_null"`
StrNotNull string `orm:"str_not_null"`
IntNull *int `orm:"int_null"`
IntNotNull int `orm:"int_not_null"`
}

更新数据

这时,只想把IntNotNull字段更新成0

1
2
3
4
5
6
7
8
9
10
11
func UpdateHealth() error {
table := g.DB().Schema(healthDb).Table(healthTable)
healthCheck := &module.HealthCheck{}
healthCheck.Id = config.HealthCheckId
i := 6
healthCheck.IntNull = &i
table.Data(healthCheck).Where("id", config.HealthCheckId)
result, err := table.Update()
glog.Info(result)
return err
}

执行完毕之后

除了我要更新的int_not_null字段,其他值都被修改了

image-20210819111903460

这肯定不是我们想要的效果,我们只想更新一个字段。

这个时候GoFrame框架提供了OmitEmpty方法,可以忽略0值,也就是达到没传值不修改的效果,让我们加上OmitEmpty试试

1
2
3
4
5
6
7
8
9
table := g.DB().Schema(healthDb).Table(healthTable).OmitEmpty()
healthCheck := &module.HealthCheck{}
healthCheck.Id = config.HealthCheckId
i := 0
healthCheck.IntNotNull = i
table.Data(healthCheck).Where("id", config.HealthCheckId)
result, err := table.Update()
glog.Info(result)
return err

重新导入数据

image-20210819110235646

操作update

image-20210819111954946

可是什么都没有更新,原来OmitEmpty把数字0当做是0值,没有进行更新,那么要怎么写呢,答案是使用*int类型

让我们使用定位为*int类型的字段来更新一下int_not_null,代码如下

1
2
3
4
5
6
7
8
9
10
11
func UpdateHealth() error {
table := g.DB().Schema(healthDb).Table(healthTable).OmitEmpty()
healthCheck := &module.HealthCheck{}
healthCheck.Id = config.HealthCheckId
i := 0
healthCheck.IntNull = &i
table.Data(healthCheck).Where("id", config.HealthCheckId)
result, err := table.Update()
glog.Info(result)
return err
}

再次操作Update

image-20210819112254854

效果达成.

总结

  • GoFrame的框架默认会对全量字段进行更新,无论你的字段有没有复制
  • OmitEmpty可以让框架跳过空值,但是int类型的0,也会跳过
  • 如果你还是想用OmitEmpty跳过空值的情况下,写入0,请使用*int类型

当代码只在当地(example:中国)部署的时候,可能不需要太考虑时区问题,很大可能你的部署机器、容器都是在当地时间。
这种情况下,代码里面统一用当地时间做处理没有任何问题。

但如果代码需要多地域部署,或者是部署的机器时区不一,比如XX单位机器统一采用UTC时间,YY单位机器统一采用当地时间,这么做不利于数据的导入导出,也不利于开发人员的维护

一个良好的处理方式可以是这样子的

image-20210814094346086

  • 存储数据都使用UTC格式存储
  • 业务层跟不同的当地系统对接

有很多场景需要我们的代码检测一个进程是否存在,常用的一种方式是通过调用脚本通过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),
),
),
);
0%