手写 sql

if 条件的字段为空则不应该拼接条件,是一个很容易被忽略的编程错误。如果线上发生了这个问题,则可能导致数据同步出错。

熔断和降级仍然导致 cpu 过高

熔断和降级会导致大量的日志打印。

日志打印在高并发时可能遇到问题:

ThrowableProxy.toExtendedStackTrace 内部会进行loadClass操作。

并且可以看到 ClassLoader 的 loadClass 在加载类时

1)首先会持有锁。

2)调用 findLoadedClass 看下是否类已经被加载过了

3)如果类没被加载过,根据双亲委派模型去加载类。

可以看到当某个类被加载过了,调用 findLoadedClass 会直接返回,锁也会被很快释放掉,无需经过双亲委派等后面的一系列步骤。

但是,在进行反射调用时,JVM 会进行优化,会动态生成名为 sun.reflect.GeneratedMethodAccessor 的类,这个类无法通过 ClassLoader.loadClass 方法加载。

导致每次解析异常栈进行类加载时,锁占有的时间很长,最终导致阻塞。

Java中对反射的优化

使用反射调用某个类的方法,jvm内部有两种方式

JNI:使用native方法进行反射操作。

pure-Java:生成bytecode进行反射操作,即生成类sun.reflect.GeneratedMethodAccessor,它是一个被反射调用方法的包装类,代理不同的方法,类后缀序号会递增。这种方式第一次调用速度较慢,较之第一种会慢3-4倍,但是多次调用后速度会提升20倍

对于使用JNI的方式,因为每次都要调用native方法再返回,速度会比较慢。所以,当一个方法被反射调用的次数超过一定次数(默认15次)时,JVM内部会进行优化,使用第2种方法,来加快运行速度。

JVM有两个参数来控制这种优化

IDEA 里的 VM options:

-Dsun.reflect.inflationThreshold=
value默认为15,即反射调用某个方法15次后,会由JNI的方式变为pure-java的方式

-Dsun.reflect.noInflation=true

默认为false。当设置为true时,表示在第一次反射调用时,就转为pure-java的方式

关于如何验证上面所说的反射优化以及两个参数的具体作用,可以参考R大的这篇博客https://rednaxelafx.iteye.com/blog/548536

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
public class TestMethodInvoke {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("A");
Object o = clz.newInstance();
Method m = clz.getMethod("foo", String.class);
for (int i = 0; i < 100; i++) {
m.invoke(o, Integer.toString(i));
}
}
}
public class A {
public void foo(String name) {
System.out.println("Hello, " + name);
}
}

private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}

return tmp;
}

public MethodAccessor newMethodAccessor(Method var1) {
checkInitted();
if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
} else {
NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
var2.setParent(var3);
return var3;
}
}

这个方法不一定能够奏效。

如何关闭JVM对反射调用的优化?
想关闭JVM对反射优化怎么办?

JVM中只提供了两个参数,因此,没有办法完全关闭反射优化。

一种能想到的接近于关闭反射优化的方法就是将inflationThreshold设为的一个特别大的数。

inflationThreshold是java中的int型值,可以考虑把其设置为Integer.MAX_VALUE ((2^31)-1)。

$ java -Dsun.reflect.inflationThreshold=2147483647 MyApp

两类触发条件:

  1. 高并发打印异常栈日志(QPS>50);
  2. 异常栈中包含反射相关的类(RPC中间件、aop);

解决方案,绕开 log4j里面的 ThrowableProxy.toExtendedStackTrace 对异常的处理流程:

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
 public static void logError(Logger logger, String message, Object... args) {
if (ArrayUtils.isEmpty(args)) {
logger.error(message);
Monitor.logError(message, new BizException());
return;
}
int length = ArrayUtils.getLength(args);
Object last = args[length - 1];
String builder = message;
for (int i = 0; i < length - 1; i++) {
builder = StringUtils.replaceOnce(builder, replaceStr, String.valueOf(args[i]));
}
// 传入参数以异常结尾
if (last instanceof Exception) {
logger.error(builder + " exception msg={}", getStackTrace((Throwable) last));
Monitor.logError(builder, (Throwable) last);
} else {
logger.error(message, args);
builder = StringUtils.replaceOnce(builder, replaceStr, String.valueOf(last));
Monitor.logError(builder, new BizException());
}
}
}

public static String getStackTrace(Throwable throwable) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
throwable.printStackTrace(pw);
return sw.getBuffer().toString();
} catch (Exception ex) {
return "";
}
}

ExceptionUtil.logError(log, "warden上传图片节点,解析图片内容为空, param:{}", riskClaimParam, wardenRiskException);


// 第二种方法:去掉了 GeneratedMethodAccessor,不够好
public class ExceptionUtils {
/**
* 过滤反射相关类,JVM反射优化后 log4j输出异常栈会因反复加载这些类block线程,尤其GeneratedMethodAccessor
*
* @param e
* @return
*/
public static Throwable filterReflectTrace(Throwable e) {
Throwable cause = e;

while (cause != null) {
StackTraceElement[] traces = cause.getStackTrace();
List<StackTraceElement> list = new ArrayList<>();
for (StackTraceElement element : traces) {
String className = element.getClassName();
if (className.contains("GeneratedMethodAccessor") || className.contains("DelegatingMethodAccessorImpl")
|| className.contains("NativeMethodAccessorImpl")) {
continue;
}
list.add(element);
}
StackTraceElement[] newTraces = new StackTraceElement[list.size()];
cause.setStackTrace(list.toArray(newTraces));

cause = cause.getCause();
}

return e;
}
}

参考:

  1. 《一个关于log4j2的高并发问题》
  2. apache的官方jira

老的表如果预先没有加好查询索引,则后续维护的 orm 用户更加容易忘记加

缺慢查询告警则无法在性能恶化以前发现这个问题。

nginx 集群的 tps 只有 18 万,不容易动态扩容。因为没有打招呼,所以没有扩容。前后端没有做过针对活动的限流调整。

要针对活动对 PaaS、IaaS 层做预案设计,这种偏物理层面的非逻辑节点隐藏得特别深,容易被忽略。

改配置无前置正确性校验和运营审核卡点

要配上前置校验工具和卡点平台。

不同业务没有做数据层面的隔离,而只做了应用层面的隔离

错误雪崩无边界。

MySQL 官方bug

相互矛盾的 dml 会使用同一个 commitid,因而在从库被并行执行,被并行执行时触发死锁。:FLUSH PRIVILEGES may cause MTS deadlock

平台有两种工作模式时,无缓存的工作模式没有经过深入测试

无缓存的测试数据集比较小,分布不够散列,导致测试的结果不能匹配线上真实的大流量。

动态配置服务

广播失败,播放了缺省值。

Java 7 的 code cache 问题

根因:

  1. codecache打满且发生flush;
  2. flush之后未操作过重启;
  3. 热点代码已从codecache中卸载。

Code cache flushing causes stop in compilation, contention on
codecache lock and lots of wasted CPU cycles when the code cache gets
full.

When uncommited codecache memory is less than the the size of the
total memory on the freelist, we iterate the entire freelist for the
largest block. This is done from ~8 places in the compiler broker. As
long as the largest block is less than CodeCacheMinimumFreeSpace
(1,5M) all compilation is halted and flushing is invoked. Since
gathering a 1,5M continous freelist block will take some time,
compilations is delayed, and regular flushing makes the freelist
longer and longer. After a while it is very long, but still far from
being continous. More and more time is spent iterating the freelist.
All profile counters that overflow will end up checking the freelist.
All compiler threads will check the freelist a few times before
delaying compilation. In addition the freelist is accessed holding the
codecache lock making the excessive iterating fully serilized. After a
day or so a CPU core may spend 100% of its cycles banging the
codecache locka and iterating the freelist. Also the application slows
down when more and more code is flushed from the cache.

This problem is mostly mainfested with tiered compilation since we
compile and reclaim a lot more code. A clear symptom is when the VM
has stopped compiling even though it reports it has enough free code
cache. The problem is worse with big codecaches since the freelist
will be more fragmented and get much longer before finding a continous
block.

Workaround: Turn of code cache flushing.

Solution is probably not to require continous free memory. The cause
for the threshold is to gaurantee space for adapters, and they are
usually small and will fit anyway.

这个结论大致上认为:
如果 uncommited codecache memory(未提交 codecache 内存)小于 the the size of the
total memory on the freelist(这个设计很像 InnoDB 的 free list),JVM会去查找 freelist 寻找 largest block。姑且认为,这是一个线性数据结构。如果查找的结果,largest block 小于一个法定配置 CodeCacheMinimumFreeSpace (1,5M,comment 里的说法是 500k) ,JVM就会不安地 halt 住 all compilation,并且开始 flushing code cache。这个 flushing 第一持有锁,第二目标是构造一块连续的内存,大于等于 CodeCacheMinimumFreeSpace。因为 complier 是多线程的,但锁的存在让这个iterating fully serialized,所以 lock centention 出现了,因为 code cache 被清理了,所以 cpu utilization 会飙得非常高。这时候光看 JVM的内存状态诊断会发现,其实还有 enough free code cache。大的code cache 碎片化更严重,所以并不一定能解决这个问题。

一个潜在的解法是:允许 code cache 在非连续内存上工作。

这类问题不易复现的原因之一是:

A DESCRIPTION OF THE PROBLEM :
1.7 JVM’s (starting with Java 1.7.0_4) set ReservedCodeCacheSize option to the default value of 48MB. Once the Code Cache size reaches
this limit, JVM switches the hotspot off forever.

This results in the immediate performance decrease for all not yet
“compiled” code and gradual “slow” performance decrease for already
“compiled” code which was “too long” time in the cache and is removed
from cache later.

Unfortunately there is no way back - once the JVM decides to switch to
the interpreted mode, it never switches back even if the CodeCache
memory is freed again.

如果 code cache “满了”,JVM 会关闭 JIT,所以这个 flushing 在折磨完人以后不会再出现。而应该被放在 code cache 里的代码分支,突然会变得很慢很慢。

只有重启这台机器,然后通过压测能够复现这个问题。

出现这个问题的时候,cpu 的监控和 code cache 的监控的抖动是一致的。

code cache 的简介,注意 jcmd、nmt 对 code、code heap、profiled method、non-profiled method 等方法的描述。

线程池的父子任务互相等待,导致线程池耗尽,从 worker 线程池一直阻塞到

这篇文章的结论要辩证地接受,ThreadPerTaskExecutor 是个很危险的线程池。如果使用 commonPool则 CompletableFuture#join方法在进入阻塞之前,判断当前线程是 ForkJoinWorkerThread线程则会在满足条件时先尝试补偿线程,确保有足够的线程去保证任务可以正常执行,这个知识点很重要。

不过有意思的是,父任务取走了所有线程,而子任务取不到线程,且父任务阻塞调用子任务,就有可能会产生死锁。

一次线程池引发的线上故障分析

ES 扩容到错误的机器,引发频发降级

  1. 在 SSD 机器上得到的经验不一定适用于 SATA 机器,SATA 机器的存在最终会导致误申请。
  2. 在业务高峰时迁移大分片可能导致业务的平响上升。自动迁移要有高低峰的限制。

nginx 的配置模块 lru cache不够用,导致部分配置丢失,导致流量偏移,拖垮中心单元

敏感配置如果不可降级,怎么做好冗余?

MySQL 里使用 bigint 表达自增 id,但 Java 代码里使用 Integer,导致 ORM 映射失败

所有的数据库字段,最好统一生成,不要自己手写,很容易出错。

beandefinition 里面依赖 bean

循环依赖导致 Spring 启动失败,或者出现未正确初始化的 bean(某些 xml 的占位符不能被正确替换)。

高流量的时候,大量的 IO 线程在线程池里等待任务

这个问题可以用 jstack 定位:

1
2
3
4
5
6
7
8
9
10
com.magicliang.Service-5-thread-95  WAITING waiting on java.util.concurrent.SynchronousQueue$TransferStack@66c0a24a
at sun.misc.Unsafe.park (Native Method)
at java.util.concurrent.locks.LockSupport.park (LockSupport.java:175)
at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill (SynchronousQueue.java:458)
at java.util.concurrent.SynchronousQueue$TransferStack.transfer (SynchronousQueue.java:362)
at java.util.concurrent.SynchronousQueue.take (SynchronousQueue.java:924)
at java.util.concurrent.ThreadPoolExecutor.getTask (ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:617)
at java.lang.Thread.run

线程池最好可伸可缩。

Why does park/unpark have 60% CPU usage?

读取过大的包

这个问题可以用 jstack 定位:

1
java.net.SocketInputStream.socketRead0(Native Method)

线下的自动化测试用例数据失效

导致不断地重试引发流量毛刺,这个问题可以从业务接口的监控提前检测到。

hashset 需要 hashcode 才能set,但一个空对象不能放进 hashcode 里面

所以一个不能正常调用 hashcode 的数据结构不一定能够构造出散列类型的 value,所以无法形成 entry。

Spring 代理问题

jacocoInit 会导致某些 config 被代理,代理的结果就是其 public 成员只能用 getter 来访问。

解法是改切点表达式:and !execution(* com.magicliang..*jacocoInit(..))

AspectJBeanFactoryPostProcessor 对切点表达式的处理出错也可能导致问题。

mvn deploy release 仓库

污染了版本号,导致 jar 特定版本被污染。

类路径冲突

  1. maven 里多了引用,导致低版本的依赖顶掉了高版本的依赖。导致高版本的依赖 classnotfound,很不符合直觉。
  2. 某些错误的配置文件也顶掉了特定的配置文件(只有 Spring 才能 merge config,log4j2 不可以)。

性能优化

串行查询变并行查询-阿姆达尔定律生效中。

压力测试会让我们知道我们以前理解不了的性能瓶颈。到底发生在客户端,还是缓存,还是数据库?随机流量会制造随机热点。了解流量特性才能做高可用设计。

Circular Dependencies in Spring

constructor injection会导致问题:

BeanCurrentlyInCreationException: Error creating bean with name
‘circularDependencyA’: Requested bean is currently in creation: Is
there an unresolvable circular reference?

解法用setter注入:

  • 单例作用域的setter循环依赖,能够解决
  • 单例作用域的构造器循环依赖,不能解决
  • prototype作用域的循环依赖,不能解决

有时候 g1 会做进行若干个无用的 eden ygc,stw得毫无意义

After an evacuation failure, G1 sometimes issues young-only gcs (maybe more than one) with zero sized eden (which accomplish nothing) before doing a full gc.

JDK-8165150

IEEE 754 浮点数问题

1
2
3
4
5
6
7
8
public static void main(String[] args) {
// 2 的 53 次方加 1
long l = 9007199254740992L +1;
Map<String, Object> map = new HashMap<>();
map.put("1", l);
System.out.println( JsonUtils.toJson(map));
}
// 原始输出:{"1":9007199254740993}

只要使用标准 JsonParser 就会观察到 Json 的数据精度被截断。

16g的结算服务的gc时间比8g的账单服务的gc时间少

因为对象分代状况不一样。

JacksonCache 导致的 ygc 频繁和线程 blocked

  1. DeSerializerCache 的存在在单例 mapper 里可能有用,但如果 mapper 不是单例的就会有巨大的问题。
  2. 初始化缓存需要调用: java.lang.reflect.Executable.java的方法declaredAnnotation() 是 synchronized 的。

CMS 频繁 Major GC

cms 频繁 gc 不一定是老年代达到了 CMSInitiatingOccupancyFraction,也可能是 ygc 产生的 promotion 本身不足以被老年代容纳。

一个有意思的CMS问题

MetaSpace 频繁超过高位水位线

  1. MetaspaceSize 意味着开始 gc。可以查看监控里的 loadingclass 的数量,来确认有没有问题。
  2. NativeMemory = direct buffer + metaspace。
  3. oom 有三种:heap error、metaspace error、gc overhead。

Heap PSYoungGen total 10752K, used 4419K
[0xffffffff6ac00000, 0xffffffff6b800000, 0xffffffff6b800000)
eden space 9216K, 47% used
[0xffffffff6ac00000,0xffffffff6b050d68,0xffffffff6b500000)
from space 1536K, 0% used
[0xffffffff6b680000,0xffffffff6b680000,0xffffffff6b800000)
to space 1536K, 0% used
[0xffffffff6b500000,0xffffffff6b500000,0xffffffff6b680000) ParOldGen total 20480K, used 20011K
[0xffffffff69800000, 0xffffffff6ac00000, 0xffffffff6ac00000)
object space 20480K, 97% used
[0xffffffff69800000,0xffffffff6ab8add8,0xffffffff6ac00000) Metaspace used 2425K, capacity 4498K, committed 4864K, reserved
1056768K
class space used 262K, capacity 386K, committed 512K, reserved 1048576K

解释

In the line beginning with Metaspace, the used value is the amount of
space used for loaded classes. The capacity value is the space
available for metadata in currently allocated chunks. The committed
value is the amount of space available for chunks. The reserved value
is the amount of space reserved (but not necessarily committed) for
metadata. The line beginning with class space line contains the
corresponding values for the metadata for compressed class pointers.

并发优化失败,回退问题

用大漏斗代替小漏斗,用第三个线程池代替第二层 eventloop 的线程池。

但第三层的线程池的 blockingqueue 太长,线程放大过高的时候会导任务分配不均衡,先到达的任务占据了大多数的线程池,后到达的任务拆解出来的子线程进入了 blockingqueue(所以子任务和父任务公用一个线程池是很危险的,只有 ForkJoinPool 能够妥善地解决这个问题)。

对于子任务本身倾斜度极高的任务而言,阿姆达尔定律决定并发优化微乎其微。

并发的转置大漏斗的容量规划一定要设计好。

不得已而为之的时候,应该使用聚合查询。

Jedis 使用单调钟却发现超时

BinaryJedisClusterMultiKeyCommand 单线程检查调用是否超时的方法:

1
2
3
4
5
6
7
8
9
 // Interleave time checks and calls to execute in case
// executor doesn't have any/much parallelism.
for (Future<Map<byte[], T>> task : tasks) {
this.multiCommandExecutor.execute((Runnable) task);
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
throw new JedisException(CLIENT_STATUS_BUSY_MESSAGE);
}
}

高吞吐导致大量 fgc

mq 不断堆积,导致 ygc 和 fgc 次数非常多。
而且产生了次生灾害,连接全死掉,必须通过重启解决。
这其实暴露了 mq 客户端对连接的管理能力不够强。

里找不到版本

可以考虑引入某些 bom。

mvcc 导致的事务隔离导致的查询错误

  1. 惊群多线程在事务里写后(非锁定)读
  2. 都读不到对方的写,导致判定错误,全部判定出错。

解决方案,加锁,加数据库乐观锁(不太好,可能因为大家都兼容读共享锁,而导致升级为写互斥锁彼此死锁),或者加流程锁。

MySQL 的时区问题

MySQL “java.lang.IllegalArgumentException: HOUR_OF_DAY: 2 -> 3” 问题解析
Retrieval of DATETIME with value in DST lost hour causes error

在数据库连接串加上 &serverTimezone=Asia/Shanghai 即可。

mysql on duplicate key update 的时候触发 mybatis 的 bug

根据官方文档

这种 bug 在自增主键上反而不容易出现,在并发插入唯一性索引的时候容易出现。

如果有多行冲突的话,每次 update 只能 update 一行而不是多行。

1
2
3
-- If a=1 OR b=2 matches several rows, only one row is updated. In general, you should try to avoid using an ON DUPLICATE KEY UPDATE clause on tables with multiple unique indexes.
- With ON DUPLICATE KEY UPDATE, the affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated
UPDATE t1 SET c=c+1 WHERE a=1 OR b=2 LIMIT 1;

参考:

  1. Error occurred when I try to “INSERT INTO ~ ON DUPLICATE KEY UPDATE” with useGeneratedKeys. #1523
  2. Statement.getGeneratedKeys() returns too many rows on INSERT .. ON DUPLICATE KEY

not eligible for auto-proxying

代理未成功,这个警告主要由 BeanPostProcessors 抛出。

MySQL order by 性能优化

MySQL ORDER BY LIMIT Performance Optimization

Spring Context 在JUnit 下的加载顺序

JUnitRunners -> spring context junit -> spring.test.context.support -> loadContext:60, AbstractGenericContextLoader -> refresh:531, AbstractApplicationContext -> invokeBeanFactoryPostProcessors:705, AbstractApplicationContext -> invokeBeanDefinitionRegistryPostProcessors:275, PostProcessorRegistrationDelegate -> postProcessBeanDefinitionRegistry:232, ConfigurationClassPostProcessor -> processConfigBeanDefinitions:327, ConfigurationClassPostProcessor -> loadBeanDefinitionsForConfigurationClass:144, ConfigurationClassBeanDefinitionReader -> loadBeanDefinitions:188, AbstractBeanDefinitionReader -> registerBeanDefinitions:96, DefaultBeanDefinitionDocumentReader -> BeanDefinitionParser

GenericApplicationContext

MergedContextConfiguration

beanDefinitions == bean
beafactory = registry

使用自定义的 PropertiesPlaceholderResolver 却导致 property 占位符填充不正常

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
194
interface PlaceholderResolver {
/**
* 解析配置信息
*
* @param props
* @return
*/
public Map<String, String> resolve(Properties props);
}

public class CustomizedPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {

private static Map<String, String> ctxPropertiesMap;
private static Properties props;

public static Object getContextProperty(String name) {
return ctxPropertiesMap.get(name);
}

public static String getPropertyString(String name) {
Object obj = ctxPropertiesMap.get(name);
if (obj == null) {
return null;
}
return obj.toString();
}

public static Properties getProps() {
return props;
}

public static String getPropertyString(String name, String def) {
Object obj = ctxPropertiesMap.get(name);
if (obj == null) {
return def;
}
return obj.toString();
}

public static Integer getPropertyInt(String name) {
Object obj = ctxPropertiesMap.get(name);
if (obj == null) {
return null;
}
return Integer.valueOf(obj.toString());
}

public static Boolean getPropertyBoolean(String name) {
Object obj = ctxPropertiesMap.get(name);
if (obj != null) {
return Boolean.parseBoolean(obj.toString());
}
return false;
}

public static Integer getPropertyInt(String name, int def) {
Object obj = ctxPropertiesMap.get(name);
if (obj == null) {
return def;
}
return Integer.valueOf(obj.toString());
}

@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {
super.processProperties(beanFactoryToProcess, props);
ctxPropertiesMap = new HashMap<String, String>();
for (Object key : props.keySet()) {
String keyStr = key.toString();
String value = props.getProperty(keyStr);
ctxPropertiesMap.put(keyStr, value);
}
new PropertiesPlaceholderResolver().parse(ctxPropertiesMap);
this.props = props;
}
}

class PropertiesPlaceholderResolver implements PlaceholderResolver {

private static final String DEFAULT_PLACEHOLDER_PREFIX = "${";// 默认占位符前缀
private static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";// 默认占位符后缀

private String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;// 占位符前缀
private String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX;// 占位符后缀
private Properties config;
private Set<String> visitedPlaceholders = new HashSet<String>();// 存放已访问的占位符,用于判断是否循环调用

@Override
public Map<String, String> resolve(Properties props) {
Map<String, String> configure = readConfigure(props);
this.parse(configure);
return configure;
}

/**
* 读取配置
**/
private Map<String, String> readConfigure(Properties properties) {
if (null == properties) {
throw new IllegalArgumentException("configure file is Null!");
}
Map<String, String> prop = new HashMap<String, String>();
if (properties.isEmpty()) {
return prop;
}
Set<Object> keySet = properties.keySet();
Iterator<Object> keys = keySet.iterator();
while (keys.hasNext()) {
String key = String.valueOf(keys.next());
String value = properties.getProperty(key);
if (null != value) {
prop.put(key, value);
}
}
return prop;
}

/**
* 解析配置
**/
public void parse(Map<String, String> config) {
for (Map.Entry<String, String> entry : config.entrySet()) {
String val = parseValue(entry.getKey(), entry.getValue(), config);
entry.setValue(val);
}
}

public String parseValue(String key, String val, Map<String, String> config) {
String value = val;
int beginIndex = value.indexOf(placeholderPrefix);
int endIndex = value.indexOf(placeholderSuffix);
if (beginIndex != -1 && endIndex != -1) {
final String placeHolder = value.substring(beginIndex, endIndex + placeholderSuffix.length());
final String placeHolderName = value.substring(beginIndex + placeholderPrefix.length(), endIndex);
if (isCircleReferece(new StringBuilder().append(key).append(placeHolderName))) {
throw new RuntimeException("Circular placeholder reference '" + placeHolder + "' in property definitions");
}
String placeHolderReplace = "";
if (config.get(placeHolderName) == null) {
if (System.getProperty(placeHolderName) != null) {
placeHolderReplace = System.getProperty(placeHolderName);
}
} else {
placeHolderReplace = config.get(placeHolderName);
}
value = value.replace(placeHolder, placeHolderReplace);
value = parseValue(key, value, config);
}
return value;
}

/**
* 判断占位符是否循环引用
**/
private boolean isCircleReferece(StringBuilder placeholder) {
int count = 0;
while (count < 2) {
count++;
if (!visitedPlaceholders.add(placeholder.reverse().toString())) {
// 循环引用
break;
}
}
if (count != 2) {
return true;
}
return false;
}

public String getPlaceholderPrefix() {
return placeholderPrefix;
}

public void setPlaceholderPrefix(String placeholderPrefix) {
this.placeholderPrefix = placeholderPrefix;
}

public String getPlaceholderSuffix() {
return placeholderSuffix;
}

public void setPlaceholderSuffix(String placeholderSuffix) {
this.placeholderSuffix = placeholderSuffix;
}

public Properties getConfig() {
return config;
}

public void setConfig(Properties config) {
this.config = config;
}

}

trade 重复交易问题

一个业务事务没有校验在途的支付交易功能,导致事件驱动后重复执行交易。

创建不了 appender,所以导致appenderref 失败

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
2020-07-28 14:05:10,732 main ERROR Cannot access RandomAccessFile java.io.FileNotFoundException: /opt/logs/mobile/xxx.log

java.io.FileNotFoundException: /opt/logs/mobile/xxx.log(No such file or directory)

at java.io.RandomAccessFile.open0(Native Method)

at java.io.RandomAccessFile.open(RandomAccessFile.java:316)

at java.io.RandomAccessFile.<init>(RandomAccessFile.java:243)

at java.io.RandomAccessFile.<init>(RandomAccessFile.java:124)

at org.apache.logging.log4j.core.appender.rolling.RollingRandomAccessFileManager$RollingRandomAccessFileManagerFactory.createManager(RollingRandomAccessFileManager.java:182)

2020-07-28 14:05:10,741 main ERROR Unable to invoke factory method in class class org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender for element RollingRandomAccessFile. java.lang.reflect.InvocationTargetException

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:498)

at org.apache.logging.log4j.core.config.plugins.util.PluginBuilder.build(PluginBuilder.java:132)

at org.apache.logging.log4j.core.config.AbstractConfiguration.createPluginObject(AbstractConfiguration.java:942)

at org.apache.logging.log4j.core.config.AbstractConfiguration.createConfiguration(AbstractConfiguration.java:882)

at org.apache.logging.log4j.core.config.AbstractConfiguration.createConfiguration(AbstractConfiguration.java:874)

at org.apache.logging.log4j.core.config.AbstractConfiguration.doConfigure(AbstractConfiguration.java:498)

Caused by: java.lang.IllegalStateException: ManagerFactory [org.apache.logging.log4j.core.appender.rolling.RollingRandomAccessFileManager$RollingRandomAccessFileManagerFactory@196a42c3] unable to create manager for

日志性能问题

如果只是寻求日志异步化,log4j2 提供几个方案

  • 全局异步化 :使用 Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  • 局部异步化:
    • 使用异步的 logger asyncAppender
    • 使用异步的 appender asyncLogger

为什么不推荐使用全局异步化?

全局异步化依赖于 Disruptor。

Disruptor 有两个缺点:

  • 它的 ring buffer 可能导致 OOM,而大部分的人都不熟悉它的性能调优(包括我在内)。
  • 有些公司的中间件和日志组件没有打通和继承好 Disruptor,所以异步化可能会丢的东西都可能会丢(包括但不限于压测标记、mtraceid)。

不要使用 console

console里大量使用 synchronize,高并发时可能导致线程阻塞、请求堆积,进而产生其他雪崩问题。

没有事务

transactionManager 和 sessionFactory 没有使用同样抽象层次的 datasource。

批量插入数据,导致 mybatis 报错

参考: 批量insertOrUpdate或replace-into使用useGeneratedKeys报错简单分析

归档的几种策略

  1. 先执行单次任务:一次性先归 30 天:
    1. 策略静态化:写死时间
    2. 用写死时间的 sql 查出最后一个 id。
    3. 用写死时间的 sql + and id <= “最后一个 id” 生成一个一次性运行的任务。跑几天。
  2. 建立周期性任务。

大数据 sql 里的 bigint 与 string

已知:bigint 可以转化为 string 而不丢失信息,反过来则会丢失信息。

教训:

  • 不要用隐式类型转换,要显式地这样做:
    • bigint > bigint
    • string(bigint) > string

运营配置错误

缺乏内控流程。
文案是错的,理赔是对的,证明元数据的配置和表达是割裂的。

连接 KeepAlive 配置有误

上游不优雅退出,下游大量报 Java IOException。

因为 war 部署触发 log4j2 加载 jar包全部信息引发线程阻塞

  1. JVM对反射的优化,导致该接口调用超过15次之后,异常栈中会有GeneratedMethodAccessor类。
  2. log4j2打印异常栈时,需要额外获取类的所属jar息,版本等额外信息,需要进行类加载。
  3. 在war包模式部署下,加载GeneratedMethodAccessor类时,会同步线性扫描所有jar包,在此过程中会将该jar包中的所有文件构建成一个缓存(最耗时的部分),而jar包模式部署下则只需要一次文件读取,无此问题!
  4. 该缓存构建好之后,默认30s后就会被清理,导致之后的异常请求需要重新构建缓存,继续变慢。

机器内部对环境判定错误

导致mock程序在生产环境生效,产生不当调用甚至资损(未支付成功却误认为支付成功,这可能要求 mock 平台有留痕能力)。

  1. 机器的环境判定一定要看容器的环境配置,去 agent 问。
  2. 通过编译脚本,在编译的时候把环境变量写到发布包里。
  3. 压测使用 mock 一定要把数据做好严格的隔离。

退款和关单乱序到达

  1. 没有加锁,没有并发控制,又退又关,造成资损。

把天转成秒

100年的天数转成秒,会导致 Integer 溢出。
针对异常状况的兜底应当有斜率告警。
要有调账机制。

涉及外网的网关也可以连到外网的生产环境

如果测试用例使用了真实的用户信息,测试订单发到了测试的gateway,测试的gateway发到外网的生产接口(比如外网的接口不分环境,或者误以为查询接口无需区分线上线下,或者误将交易当作查询接口)。

数据隔离和环境隔离要一起做。

st 和 prod

  1. 代码版本不对。
  2. 交叉调用有可能导致逻辑不一致。
  3. 秘钥设计成一致的,很难通过验签拦截。特别要指出的是:线上线下的秘钥要专门隔离。

参考:《一次log4j2的慢日志问题排查》

ThreadLocal Context里携带过多的子上下文,子线程一直持有这些上下文,导致 fgc

  1. 上下文的主clear方法要完全清除对entry的引用。
  2. 子对象对context的引用要栈封闭。
  3. 子对象要慎用 InheritableThreadLocal,因为它会无意之中引用父线程的 InheritableThreadLocal 的 value。防止 ThreadLocal 内存泄漏的主要问题是,防止对 Value 的悬垂引用。

Spring 启动中数据源关闭的问题

  1. Spring 中间件乱序启动,导致 Hystrix ConcurrencyStrategy自 重复注册
  2. Hystrix ConcurrencyStrategy 在 ApplicationContextAware 里被手工注册,触发重复注册
  3. Spring Context 启动失败
  4. 数据源关闭
  5. 刷数任务未关闭,导致 jdbc 异常。

没有开启 NettyIO 和平滑启动,导致 thrift client 启动大量时间消耗在 getConn 上

突然打开灰度开关,导致 thrift client 所有连接都在 getConn,然后触发大量超时。

无法弹性扩容,导致需要强依赖熔断降级

zk 连接 sgagent 故障,导致 zk 无法更新,所有的云调度系统无法批量刷新配置信息,所有的拓扑变更(节点上线注册和离线)都无法执行,导致流量到来的时候无法处置。

这时候所有的对上接口的熔断和限流能力就尤为重要了,每个接口设计之初就要考虑好熔断和限流问题。

Spring 在锁定 WebClassLoader 做字节码增强的时候,正好遇到 Web中间件自己也在锁定 WebClassLoader

导致死锁。

使用双查询条件导致 es build_scorer 耗时偏长

keyword 使用跳表加速,但 integer 必须使用 bkdtree 排序。进行双条件查询的时候,integer 的查询会导致无序的 docId 大量进入内存,查询变得非常长。

ES 5.4 以后优化:

  • 结果集小:PointRangeQuery
  • 结果集大:SortedSetDocValuesRangeQuery

es 带有 explain 功能

异步日志没有写 blocking = false

大量打日志导致 long-mq。

Netty 处理速度过慢

导致输入编解码的缓冲区堆积过多数据,多次 Major GC 也无法回收内存,而吞吐会进一步变慢。

Netty 的 inflate 缓冲区泄漏,导致 gc 异常

https://github.com/eclipse/jetty.project/issues/575

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;

import java.nio.ByteBuffer;
import java.lang.Thread;

@BTrace public class BtracerInflater{
@OnMethod(
clazz="java.util.zip.Inflater",
method="/.*/"
)
public static void traceCacheBlock(){
println("Who call java.util.zip.Inflater's methods :");
jstack();
}
}

jdk8 会默认打开一些 internal cache,但 java9 修复了这个问题。

JarFile -> ZipFile,ZipFile 会持有 Inflater,Inflater 会申请和持有堆外内存。在依赖的 jar包非常多的时候,会发生堆外内存泄漏。

使用 stream 来优化 for 循环,但 Long 的 value 设值没有捕获异常

对于原有的大 try catch 的分批重构需要仔细考察各个步骤的异常点,特别是更内部的异常要仔细看。

在构造器里启动了线程,使用 run 而不是 start

导致 context refresh 卡在倒数第二步,无法释放 startShutDownMonitor。

此时再启动了 Spring 自己的 shutdownhook,而 AbstractApplicationContext 的 close 也需要求 startShutDownMonitor。

这给我们一个启示,关闭线程可能从另一个视角访问启动的资源。

我们平时不注意在这两种看起来毫无关系的操作之间加锁,是因为 Spring 自身已经意识到了这一点,帮我们在外部加了锁。

先 mkdir 生成一个目录,再试图用 ln -s 创建同一个目标目录

第二个命令失败,软链接没有创建成功。日志挂载可以这样设计:

  1. 设计 /data/log/${APP_NAME}。
  2. 从 1 生成 ${APP_PATH}/log 的软链接。
  3. 从 2 生成 ${APP_PATH}/logs 的软链接。
  4. 日志写入 2。
  5. 日志挂载的 volume 监控 1,log agent 读出日志。
  6. 挂载点必须是原始的目录,而不能是符号链接。

Play 里没有对 @ImplementedBy 加上 @Singleton

导致配置对象实例在 Action 中被注入,进而导致内存溢出。

Cos 没有加上内网地址

内网访问和外网访问的逻辑是不一样的

全部的拦截器拦截了不需要拦截的接口

需要注意全拦截的表达式的逻辑

header 不为空导致 Spring cors filter 工作不正常

关键代码见 CorsUtils。

搜索引擎同步延迟导致规则应用失败

需要引入旁路主副本。

ebean 为 @dbArray 的 null field 提供 emptyList

导致无法判断未初始化,判空都失败。

logfilter 过滤时没有处理好

导致无法处理异步的 response body(只有 servlet 异步事件api 可以处理好它)和处理流式返回body(录制流出会导致空 body,而文件名可以被header 的指定)。

没有准备 @RequestBody 和 @Valid

注入出错,解析出错

轮询算法使用 indexof 来判定位置

nextPos = indexOf(lastUid) + 1

1
1 2 3 a b c a b c 4 5 6 

第二个 a 造成死循环:a b c a b c……

ThreadPoolTaskExecutor 忘记加上 @Bean

导致每个请求都带来一个 ThreadPoolTaskExecutor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean(name = "executor")
public ThreadPoolTaskExecutor asyncServiceExecutor() {
logger.info("start asyncServiceExecutor");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(4);
//配置最大线程数
executor.setMaxPoolSize(8);
//配置队列大小
executor.setQueueCapacity(MAX_REQUEST_IMPORT_PER_SEC);
//存活时长
executor.setKeepAliveSeconds(10);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("thread-pool-");

// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}

Cookie 没有加上 Path

Play 的 Cookie 默认为 /,Spring 没有默认值。这导致 Spring 下 Cookie 不能重放。
HttpOnly 同理。

Spring MVC 不支持 application/javascript

浏览器在 strict mime 检查的时候失败。
解法:

1
2
3
4
5
6
7
8
String jsonpResult =
ServiceTag.SERVICE_TAG_LEADS.name() + "(" + JacksonUtils.toJson(res) + ")";
final MediaType mediaType = new MediaType("application", "javascript");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
headers.addAll(HttpHeaders.SET_COOKIE, Lists.newArrayList(mockLoginUserIdCookie.toString(),
noLoginCookie.toString()));
return new ResponseEntity<>(jsonpResult, headers, HttpStatus.OK);

父子进程无法关闭

/bin/sh 作为 shebang 无法理解 kill sigterm。
子进程没有 trap,收集不到父进程传过来的 kill
父进程没有 wait,会导致僵尸。

正确的做法:

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
# 启动业务进程
echo "start biz process"
${install_path}/bin/start.sh & PID=$!
echo "biz process started"

# 等待程序启动
sleep 15
echo "Start application $PID finished..."

# 优化终止
# 处理信号
handle_sig() {
echo "Received SIGNAL $1 $PID" >> ${APP_PATH}/log/handleSig.log
kill -s $1 $PID
wait $PID
}

# trap 监听传递信号包装函数
trap_sig() {
for sig in "$@" ; do
echo "handle_sig $sig"
trap "handle_sig $sig" $sig
done
}
# trap的信号
trap_sig SIGINT SIGTERM SIGSEGV SIGUSR2

# 等待主进程回调执行完再退出
wait $PID

echo "Leads application $PID ended..."
sleep 30

bash 不识别 python 的 alias

因为非交互式 shell 不支持 alias。

解决方法:使用一个 alias path,把它作为 path 的一部分。然后让 alias 作为这个 path 的软连接里出现。
《Why doesn’t my Bash script recognize aliases?》

双配置难题 2 Configurations puzzle

Spring的几个基础假设:

  • 任意@Bean 开头的方法,是一个工厂 bean 方法,它的调用流程是:
    • 在调用任意的 @Bean 开头的方法之前,它依赖的 Configuration bean必须先初始化。
    • 解析方法的参数bean:
      • 调用构造器:适用于@Component类的Bean。
      • 调用其他 @Bean方法。
    • 用参数 bean 调用方法。
    • 把返回值注册为一个 bean。
  • 任意一个平凡 Bean springConfiguration 的初始化流程是:

    • 尝试通过 postProcessBeforeInstantiation 生成一个前代理:

      • ask should skip(这是 ask advisor 1):生成前代理以前要先列出当前 BeanFactory 里的 advisors(也就意味着所有的 MethodInterceptor 要先被找出来,装配出相应的 advisor),确认 springConfiguration 是否是一个 Aspect扩展点,是就skip。
      • 对于大部分平凡 bean - 如 springConfiguration,不需要生成前代理-逻辑太复杂,先不解释。
    • 生成一个 bean 实例-但此时 bean 的构造没有 complete。

    • populateBean:注入所有的成员变量。
    • initialization:
      • 调用 postConstruct。
      • 调用 afterPropertiesSet
      • 调用 init 方法。
    • 尝试 postProcessAfterInitialization:
      • 再问一遍 should skip(这是 ask advisor 2)。
      • 如果不shouldSkip,尝试 wrapIfNecessary 把它包装进一个 proxy(这是 ask advisor 3)。
  • 任意一个 advisor 没有“尝试生成 postProcessBeforeInstantiation 一个前代理”这一过程。
  • 任意一个 ask advisor 都是for循环,检索所有的 advisor 的构造。如果 advisor 构造不出来,则吞掉构造异常,把这个 advisor 相关的对象图涉及的 bean 都 destruct 掉,但这时候这些bean的构造器已经调用过了,而且因为这批对象图没有构造完成,所以下次需要从头开始通过构造器再调用一遍。
  • create Bean springConfiguration 会在一开始的时候就把 springConfiguration 设为 inCreation 状态,如果 springConfiguration 依赖于 Bar,而Bar 需要 ask advisor,advisor 又是 springConfiguration 里的 @Bean 开头的方法,又会尝试初始化 springConfiguration(即 1.a 提到的设定),而第二次尝试 create Bean springConfiguration 检查到 springConfiguration 的状态为 inCreation,就会抛出一个异常,毁掉这个初始化。
  • 一个 @Configuration bean 继承另一个 @Configuration bean 以后,实际上 Spring 会初始化2个bean,但只会通过子类 bean 实例调用工厂方法,不会产生穿梭问题。

双配置1
双配置2

结论:

  • 任意的 MethodInterceptor 的成员变量,可能被初始化一次,也可能被初始化无数次。因此:
    • 所有的成员都应该是 @Lazy 的(最推荐的方案)。String 之类的基础类型是例外,首先它们是 final 的,不可用被 subClass,所以无法出现 lazyBean。其次是,它们也无法作为对象图的根。
    • 所有依赖的成员的构造器都是幂等的,能抗无数次初始化。
  • 如果两个Configuration都有 @Bean 注解,那么 @Bean 注解带有的工厂方法可能会相互调用,产生奇怪的问题。如果条件允许,让 Configuration 类型互相继承也是一个好主意。
  • 细心的读者已经发现了,任意一个 @Configuration bean 是无法被自己生产的任意 advisor 环绕的,相当于一个裸 proxied instance(其他 advisor 仍然可以环绕这个 bean)。

枚举互相引用导致成员为空

https://brickydev.com/posts/enum-circular-dependency-in-java/

Dozer 导致线上卡顿

ReflectionUtils.findPropertyDescriptor 依赖于

1
2
3
4
5
6
7
public class PropertyDescriptor {

public synchronized Class<?> getPropertyType() {

}
}

es 查询被拒绝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"error": {
"root_cause": [],
"type": "search_phase_execution_exception",
"reason": "",
"phase": "fetch",
"grouped": true,
"failed_shards": [],
"caused_by": {
"type": "es_rejected_execution_exception",
"reason": "rejected execution of org.elasticsearch.common.util.concurrent.TimedRunnable@204d3d67 on QueueResizingEsThreadPoolExecutor[name = 1693273402005365932/search, queue capacity = 1000, min queue capacity = 1000, max queue capacity = 1000, frame size = 2000, targeted response rate = 1s, task execution EWMA = 335.7ms, adjustment amount = 50, org.elasticsearch.common.util.concurrent.QueueResizingEsThreadPoolExecutor@ae1b393[Running, pool size = 25, active threads = 25, queued tasks = 1195, completed tasks = 50356750]]"
}
},
"status": 503
}

这种毛刺可能和瞬时io毛刺有关。有几种解决思路:

  1. 调高线程数:在 cpu有富余的时候最简单。
  2. 调整 thread_pool.bulk.queue_size(文档写入队列大小,适用于5.6.4版本)、thread_pool.write.queue_size(文档写入队列大小,适用于6.4.3及以上版本)、thread_pool.search.queue_size(文档搜索队列大小)。让读写互相让步。
  3. 纵向扩展:在单一节点上加硬件。
  4. 横向扩展:加节点。

在腾讯云上,1不被允许使用,而2被允许使用。
3 和4 可以间接达成1,但如果只是瞬时毛刺,cpu/mem/io util 比较低的话,2也可以临时顶一下。queue的存在就是为了这种临时并发而设计出来的,使用旁路带宽也可以,并不一定要把总带宽加上去

便宜的带宽量通常大,如果便宜的带宽用尽,则意味着系统必须 scale up/scale out了。

left join 的时候没有提前做聚合

a 表有多行,b表也有多行。

先把b表做聚合,但没有对a表做聚合。

对两个结果做 join,导致b表的结果被放大,放大以后再sum,导致数据大量增多。

解法:每张表把 join key 数据 aggregate 成一行,再join。

做这种设计的时候,要仔细思考:主表是一定有数据的,从表不一定有。

要join一起join。

ws socket 双联

ws是有状态连接。

微信sdk有bug,产生了两条物理连接。c持有物理连接1和2,而s1持有物理连接1,s2持有物理连接2。c往1推事件,1调用下游,下游回调到2,2往c回推事件,被c忽略。因为c的逻辑连接只有1。

解法:

  1. 每次生成新的连接,要先断掉所有可能的连接,让最后的连接成为唯一的连接。
  2. 阻塞式重连,真正断联才重连。
  3. 在前端的日志里打出sessionid。后端不同服务器打出不同的服务器标识,让前端知道有状态连接的状态是什么。

同一台机器,持有两条连接,也会有这样的问题-缩容不解决问题。

过快过期的缓存,与过期的load函数

缓存过期太快,导致load函数频繁穿透读数据库。load函数执行比较慢,导致 cpu 消耗在最底层的时候特别少(因为慢sql在等待io),但系统的输出时延很高。

嵌套缓存死锁

缓存的load方法里又调缓存。导致缓存自己的父子操作死锁。

跑任务的时候没有处理好异步化问题

  1. 线程池太小,导致任务被主线程运行,这时候一个线程的异常就会导致主任务失败。所以主任务自己做好 catch 是重要的。
  2. 要让 context 不自动终止并退出,要让任务的执行全同步化并 catch 好。所以主动执行的任务要清楚context下什么地方是异步化的,context什么时候终止。

锁与标志位

  1. 在很多公司redis集群是有兜底超时时间的。
  2. 但在很多公司没有。
  3. 加标志位的超时可以很长,如果标志位支持直接覆写,没有 set nx 的语义会比较简单,这样标志位会成为不断增长延长的位。
  4. 锁的超时时间不能设计太长,因为发布一定会导致解锁丢失。这样产生的【黑窗口是需要补偿机制的】。
  5. 换锁一定会导致并发控制失效,新机器用新锁而老机器用老锁。这就是自发多线程调度的缺陷。如果使用集中式调度中间件,可以把这个问题交给他们处理。
  6. 标志位嵌套增加,是给系统增加更细的颗粒度细节,新发布的机器会受控制,而老机器不受控制,所以会有两种不同的行为,这里存在兼容问题。
  7. 嵌套标志位减少,则细颗粒度的控制行为会丢失,老机器更细而新机器更粗。如果可以丢弃更细的行为则系统设计无问题。

不被捕获的异常

有时候诸如“bound must be greater than origin”的异常只有控制台才能看得到,在日志里看不到。如果流程意外中断而无日志,则可以考虑是不是发生了这类异常-特别是在线程池内发生的异常,退出更无声无息,所以线程池内的异常要注意捕获和记录

服务重启时大量空指针错误

1
2
3
java.util.concurrent.TimeoutException: null
at java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1784)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1928)

这意味着线程刚启动的时候内部的forkjoin线程池冷启动容易阻塞超时。

使用静态变量来调整 feature toggle

基于时间开始的特性开关是不适合用来赋值 static 变量的。因为类加载器只加载一次。
正确的做法是使用toggle的时候实时计算。

left join 的时候对left join的表的条件写在join之后

1
a left join b on a.id=b.id where a.dt=20240801 AND b.dt=20240801

最后的条件是错的。

如果要选择分区做左连接,需要

1
a left join (select * from b where dt=20240801)t on a.id=t.id where a.dt=20240801 

a b 表在内连接里做分区

在优化前,内连接是直接求等来查,而且分区查询语句是并列在一起的,看起来一个查询很难同时用到ab两张物理表的索引。

1
2
select *  from tbl_a a , tbl_b b 
where a.id=b.id and a.con = 1 and a.date=20240728 and b.date=20240728 and b.greeting like '%head_content%'

但优化的过程是把分区查询先查完,哪怕使用一个临时表来承接查询结果,也好过报错:

1
2
3
 select *  from (select * from tbl_a  where date=20240728) a, 
(select * from tbl_b where date=20240728) b
where a.id=b.id and a.con = 1 and b.greeting like '%head_content%'

错误为:

Query threw SQLException:Code: 241, e.displayText() = DB::Exception:
Memory limit (for query) exceeded: would use 9.32 GiB (attempt to
allocate chunk of 4718848 bytes), maximum: 9.31 GiB:
(avg_value_size_hint = 173.29541015625, avg_chars_size =
198.3544921875, limit = 8192): (while reading column greeting): (while reading from part
/data/clickhouse/clickhouse-server/store/0aa/0aaf2939-9927-4c36-8aaf-293999277c36/20240603_1_23_2/
from mark 24 with max_rows_to_read = 8192): While executing
MergeTreeThread (version 21.8.12.1)

2024.08.19 09:53:50.059401 [ 355361 ] {3b2967b5-bf01-4c61-bbe3-1b1966124220} executeQuery: Code:
241, e.displayText() = DB::Exception: Memory limit (for query)
exceeded: would use 9.32 GiB (attempt to allocate chunk of 6029696
bytes), maximum: 9.31 GiB: (avg_value_size_hint = 130.3759994506836,
avg_chars_size = 146.8511993408203, limit = 8192): (while reading
column greeting): (while reading from part
/data/clickhouse/clickhouse-server/store/0aa/0aaf2939-9927-4c36-8aaf-293999277c36/20240603_1_23_2/
from mark 24 with max_rows_to_read = 8192): While executing
MergeTreeThread (version 21.8.12.1) (from 11.163.8.64:15770) (in
query: select distinct a.script_id, b.greeting from
leads_db.chatbot_fmc_chat_day a,
leads_db.chatbot_fmc_chat_groupmessage_day b where a.chat_id =
b.chat_id and a.u_id = 45428689 and a.date between 20240813 and
20240819 order by a.created_at desc), Stack trace (when copying this
message, always include the lines below):

注意看 in query。

parallelStream 带来的异步化问题

1
2
3
4
5
6
 executor.execute(() -> {
// 这个线程执行会导致丢失消息
}
);

consumer.ack();

解法:

  1. 使用终端操作
    parallelStream()的大多数终端操作(如forEach, collect, reduce等)都是阻塞的。这意味着,当你调用这些操作时,它们会处理完所有元素后才返回。例如:
1
2
3
4
5
6
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubled = list.parallelStream()
.map(i -> i * 2)
.collect(Collectors.toList());
System.out.println("Completed: " + doubled);
在这个例子中,collect()是一个阻塞操作,它会等待所有的映射操作(map())完成后才继续执行System.out.println()。
  1. 使用CountDownLatch
    如果你的parallelStream()操作是非终端操作,或者你需要在多个并行流之后执行一些操作,你可以使用CountDownLatch来同步:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int size = list.size();
CountDownLatch latch = new CountDownLatch(size);

list.parallelStream().forEach(i -> {
// 执行一些操作
System.out.println(i * 2);
latch.countDown(); // 每完成一个元素,计数减一
});

try {
latch.await(); // 等待直到所有元素处理完成
System.out.println("All tasks completed.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Interrupted.");
}

在这个例子中,每处理完一个元素,CountDownLatch的计数就减一。latch.await()会阻塞当前线程,直到计数器减到零。

  1. 使用CompletableFuture
    如果你想要更多的灵活性和控制,你可以使用CompletableFuture来处理并行流中的每个元素,并在所有的Future完成后继续执行:
1
2
3
4
5
6
7
8
9
10
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<CompletableFuture<Void>> futures = list.parallelStream()
.map(i -> CompletableFuture.runAsync(() -> {
System.out.println(i * 2);
}))
.collect(Collectors.toList());

CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println("All tasks completed.");
这里,每个元素的处理被封装在一个CompletableFuture中,CompletableFuture.allOf()等待所有的Future完成。

单例构造里抛出异常,导致单例始终不生成成功

在 static 或者其他单例的流程里,带有下游依赖的构造器的调用,都是可能抛出异常的。如果不catch住异常很可能没有日志,也会频繁进入单例构造体。

wait 被伪唤醒

解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public synchronized String receive() {
// 大部分的例子没有讲过为什么这个地方是必须的,实际上就算用来防止伪唤醒的。
while (transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Thread Interrupted");
}
}

// 先修改条件变量
transfer = true;

String returnPacket = packet;

// 再唤醒
notifyAll();
return returnPacket;
}

已经有了一个清洗状态,仍然触发清洗

缺乏终态设计思维,应该先问清楚是否允许从终态回撤回来。
如果不允许,那么哪些是不允许的,如果有必要,引入一个任务系统,围绕这个任务系统的状态来跟踪是否允许重新发起