0%

离职系列 第九篇
离职系列,想想这几年在公司的成长,在这做个记录。这篇是遇到的一个比较典型的线上问题。

问题现象

写了一个每天执行两次的定时任务,该任务会分批对线上所有几百个租户生成《平安通告》,上线1个多月后突然手机收到告警,某几个用户生成失败。

  
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
2024-10-17 17:00:10,125 [safetyNoticeGenerator8] ERROR [com.alibaba.druid.pool.DruidDataSource] DruidDataSource.java:1988 - {conn-110021} discard
org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend.
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:395)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:498)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:415)
at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:190)
at org.postgresql.jdbc.PgPreparedStatement.execute(PgPreparedStatement.java:177)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:483)
at org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement$2.executeSQL(ShardingSpherePreparedStatement.java:439)
at org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement$2.executeSQL(ShardingSpherePreparedStatement.java:435)
at org.apache.shardingsphere.infra.executor.sql.execute.engine.driver.jdbc.JDBCExecutorCallback.execute(JDBCExecutorCallback.java:95)
at org.apache.shardingsphere.infra.executor.sql.execute.engine.driver.jdbc.JDBCExecutorCallback.execute(JDBCExecutorCallback.java:75)
at org.apache.shardingsphere.infra.executor.kernel.ExecutorEngine.syncExecute(ExecutorEngine.java:135)
at org.apache.shardingsphere.infra.executor.kernel.ExecutorEngine.serialExecute(ExecutorEngine.java:121)
at org.apache.shardingsphere.infra.executor.kernel.ExecutorEngine.execute(ExecutorEngine.java:115)
at org.apache.shardingsphere.infra.executor.sql.execute.engine.driver.jdbc.JDBCExecutor.execute(JDBCExecutor.java:65)
at org.apache.shardingsphere.infra.executor.sql.execute.engine.driver.jdbc.JDBCExecutor.execute(JDBCExecutor.java:49)
at org.apache.shardingsphere.driver.executor.DriverJDBCExecutor.doExecute(DriverJDBCExecutor.java:156)
at org.apache.shardingsphere.driver.executor.DriverJDBCExecutor.execute(DriverJDBCExecutor.java:145)
at org.apache.shardingsphere.driver.jdbc.core.statement.ShardingSpherePreparedStatement.execute(ShardingSpherePreparedStatement.java:402)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.execute(ProxyPreparedStatement.java:44)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.execute(HikariProxyPreparedStatement.java)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:80)
at jdk.internal.reflect.GeneratedMethodAccessor49.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at jdk.proxy2/jdk.proxy2.$Proxy207.query(Unknown Source)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:65)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:333)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:158)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:110)
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:81)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59)
at jdk.proxy2/jdk.proxy2.$Proxy206.query(Unknown Source)
at jdk.internal.reflect.GeneratedMethodAccessor44.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49)
at com.github.yulichang.interceptor.MPJInterceptor.intercept(MPJInterceptor.java:76)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59)
at jdk.proxy2/jdk.proxy2.$Proxy206.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:154)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:142)
at jdk.internal.reflect.GeneratedMethodAccessor109.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:425)
at jdk.proxy2/jdk.proxy2.$Proxy189.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:224)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:166)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:77)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:152)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89)
at jdk.proxy2/jdk.proxy2.$Proxy197.findDistinctCiIdsByIdentityId(Unknown Source)
at com.xxx.xxxcloud.manage.service.safety.notice.impl.xxServiceImpl.addAlertData(xxServiceImpl.java:612)
at com.xxx.xxxcloud.manage.service.safety.notice.impl.xxServiceImpl.lambda$setJsonField$5(xxServiceImpl.java:368)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at com.xxx.xxxcloud.manage.service.safety.notice.impl.xxServiceImpl.setJsonField(xxServiceImpl.java:346)
at com.xxx.xxxcloud.manage.service.safety.notice.impl.xxServiceImpl.batchCreate(xxServiceImpl.java:201)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:751)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:117)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:391)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:751)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:703)
at com.xxx.xxxcloud.manage.service.safety.notice.impl.xxServiceImpl$$SpringCGLIB$$0.batchCreate(<generated>)
at com.xxx.xxxcloud.manage.service.safety.notice.impl.SafetyNoticeScheduleServiceImpl.processBatch(SafetyNoticeScheduleServiceImpl.java:194)
at com.xxx.xxxcloud.manage.service.safety.notice.impl.SafetyNoticeScheduleServiceImpl.lambda$processBatchesInThreadPool$0(SafetyNoticeScheduleServiceImpl.java:112)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: java.net.SocketTimeoutException: Read timed out
at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:288)
at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:314)
at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:355)
at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:808)
at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966)
at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:484)
at java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:478)
at java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:70)
at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1465)
at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1069)
at org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:161)
at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:128)
at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:113)
at org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:73)
at org.postgresql.core.PGStream.receiveChar(PGStream.java:465)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2155)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:368)
... 82 common frames omitted
org.springframework.jdbc.UncategorizedSQLException:
### Error querying database. Cause: java.sql.SQLException: Connection is closed
### The error may exist in class path resource [mapper/TbxxHealthCheckResultMapper.xml]
### The error may involve com.xxx.xxxcloud.manage.mapper.xx.TbxxHealthCheckResultMapper.selectExecuteLatest
### The error occurred while executing a query
### Cause: java.sql.SQLException: Connection is closed
; uncategorized SQLException; SQL state [null]; error code [0]; Connection is closed
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:93)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:439)
at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: java.sql.SQLException: Connection is closed
at com.zaxxer.hikari.pool.ProxyConnection$ClosedConnection.lambda$getClosedConnection$0(ProxyConnection.java:502)
at jdk.proxy3/jdk.proxy3.$Proxy173.prepareStatement(Unknown Source)
at com.zaxxer.hikari.pool.ProxyConnection.prepareStatement(ProxyConnection.java:327)
at com.zaxxer.hikari.pool.HikariProxyConnection.prepareStatement(HikariProxyConnection.java)

从上面有几个关键信息梳理下异常调用链:

得出几个重要信息:

  1. 异常发生的业务代码是在批量处理”xx报告”时,查询告警处
  2. ShardingSphere下的连接池hikari抛出了异常Connection is closed
  3. Druid连接池也抛出了异常{conn-xxx} discard
  4. 底层SocketTimeoutException,表明是客户端等待数据库服务器的响应超时(初步判断为慢sql)

所以我开始从以下几个方面排查:

  1. 查业务代码的变更,为什么之前好好的跑了一个多月,突然出问题。
  2. 检查数据库连接参数设置
  3. 评估查询的数据量是否过大
  4. 看下HikariCP和Druid是否有啥联系以及为啥会有两个连接池?
  5. 查看数据库负载情况

处理过程

第一步:摸索

怀疑一切,切忌先入为主。

  1. 想办法重现问题
    1. 提前发通知,下午7点以后会操作线上系统。通常6点半以后几乎就没人用系统了。
    2. 因为设计该功能时,留了补偿手动,可手动重新触发报告生成。
    3. 反复重试了几次,问题未复现。
  2. 查看业务代码变更。
    1. 报错处业务代码owner是我,没做任何更改,所以变成重点关注addAlertData方法,也就是与告警相关(重点在数据量)
    2. 业务没变更但是加入了ShardingSphere(异常中也有这块的信息,先存疑)
  3. 检查系统连接池参数
    1. druid,几乎都没有做定制,都是使用的默认值。
      1. 使用默认值其实存在风险,应该根据业务调整一些参数,因为买的阿里的pg所以咨询了他们拿到了一份他们暴露的参数调优参考,后续可针对性修改)
    2. 新增的ShardingSphere的HikariCP也是使用的默认值。(异常中也有这块的信息,先存疑)
  4. 观察显示的数据库负载情况(阿里云的监控看板、pg的pg_stat_activity等视图、数据库日志等)
    1. 从视图发现确实存在执行时间较长的几条sqlsql,虽然有些慢但是不足以触发异常。慢的原因初步判断为数据量过大(报错的租户行数都在50w左右)
    2. 记录下这个sql,拿去控制台执行,EXPLAIN ANALYZE该sql,发现Seq Scan除了时间较长以外还存在索引问题,几乎每次查询都要用到的“告警状态”字段之前没加索引。(可能是原因之一)
    3. 查看数据库日志如下,这表明数据库和客户端的连接中断确实是问题的根源,可能是因为网络问题、数据库负载过高、或者连接超时等因素导致的。

第二步:验证

验证怀疑的所有点,通常控制变量法进行验证。

因为暂时没有复现,不着急先按兵不动。

  1. 前提是要有补偿方案,不能阻断线上使用,特别是这种核心业务。因为当时留了后门可以手动触发某个租户所以没问题。
  2. 虽然是线上故障同时也算严重bug,但是也不要着急,胡乱改一通,可能按下葫芦又起瓢,尽可能的找到根因,哪怕不能一次性修复。

等待了两天发现同样的问题又发生了。

  1. 但是这次发现了上一次遗漏的一个信息,报错的租户从日志看时间,异常都是在10s以后抛出的。(初步判断是SocketTimeout的超时时间,可能是10s)
  2. 拿到同样报错的sql去执行发现虽然跟之前没啥差别,问题还是那些问题,也没超过10s。(怀疑是因为报错时候执行了sql,触发了pg的缓存,所以再去查缓存生效,导致执行时间变短)

验证第一个问题:

因为当前看存在有两个连接池,查HikariCP与Druid的源码:

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
// HikariCP只有connectionTimeout为30s
static {
CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(30L);
VALIDATION_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
SOFT_TIMEOUT_FLOOR = Long.getLong("com.zaxxer.hikari.timeoutMs.floor", 250L);
IDLE_TIMEOUT = TimeUnit.MINUTES.toMillis(10L);
MAX_LIFETIME = TimeUnit.MINUTES.toMillis(30L);
unitTest = false;
}

// DruidDataSource初始化,socketTimeout是10s
public void init() throws SQLException {
if (!this.inited) {
DruidDriver.getInstance();
ReentrantLock lock = this.lock;

try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new SQLException("interrupt", e);
}

boolean init = false;

if (this.connectTimeout == 0) {
this.connectTimeout = 10000;
}

if (this.socketTimeout == 0) {
this.socketTimeout = 10000;
}

先不管HikariCP,至少能确定Druid的10s超时确实存在,因为阿里云上我开起了pg的慢sql监控,根据时间刚好查到了确实存在一条告警sql查询执行时间为10s+。

  1. 所以初步判断SocketTimeoutException是因为sql执行时间超过了连接池的默认超时。
  2. 去查了下该租户下告警的数据总数已经超过了50W,在活5000左右。

第三步:修复方案

方案如下:

  1. 加索引,对常用的字段“告警状态”添加索引。
  2. 对在活告警查询的地方分批
  3. 对在活主告警限制查询的条数而不是查所有。

第四步:方案执行

  1. 对所有配置分表的表,检查索引,添加必要的索引。

    1.   -- ========================================
        -- 描述: 创建告警表的“status”字段索引
        -- 文件名: 001_create_alert_status_index.sql
        -- 作者: hht
        -- 创建日期: 2024-10-31
        -- ========================================
      
        DO $$
        DECLARE
            i INTEGER;
        BEGIN
            -- 遍历 tb_xx_alert_0 ~ tb_xx_alert_15
            FOR i IN 0..15 LOOP
                -- 动态生成 ALTER TABLE 语句,添加字段
                EXECUTE FORMAT('
                    ALTER TABLE public.tb_xx_alert_%s 
                    CREATE INDEX tb_xx_alert_%s_status_index ON tb_xx_alert_%s  (status);
                ', i);
            END LOOP;
        END $$;
      
  2. 告警分批且限制条数

    1. 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
      private List<AlertVO> findSubscribedAlerts(AlertSubscribedParam param) {
      try {
      List<List<String>> parts = PartsListUtil.getParts(param.getSubscribedCiIds(), BATCH_SIZE);
      List<AlertVO> all = new ArrayList<>();
      for (List<String> part : parts) {
      // 分批查询
      param.setSubscribedCiIds(part);
      // 主告警不超过100条
      Page<AlertVO> page = new Page<>(1, 100 - all.size());
      IPage<AlertVO> result = tbXxAlertMapper.findSubscribedAlerts(page, param);
      if (result.getTotal() == 0) {
      continue;
      }
      // 聚合告警的子告警数据补充
      getAlertAggChildren(param.getIdentityId(), result.getRecords());
      all.addAll(result.getRecords());
      // 如果已经达到100,退出循环
      if (all.size() == 100) {
      break;
      }
      }
      // 最后统一按createTime降序排序
      all.sort((a1, a2) -> Long.compare(a2.getCreateTime(), a1.getCreateTime()));
      return all;
      } catch (Exception e) {
      String errorMessage =
      String.format("查询关注告警失败: identityId=%s, deal=%s", param.getIdentityId(), param.isDeal());
      log.error(errorMessage, param.getIdentityId(), e);
      throw new SafetyNoticeException(errorMessage, e);
      }
      }

第五步:监控效果

  1. 上线后,连续一周到点蹲守
    1. SELECT pg_stat_reset(); – 重置所有统计信息
    2. 数据库负载看板以及pg的pg_stat_activity。
      1. 之前的sql执行时间没有再超过2s
      2. 看板上慢sql也没有在发现
      3. 数据库日志也没有再出现异常
    3. pg_stat_user_indexes+EXPLAIN ANALYZE 对应sql,查看索引使用情况。
  2. 业务功能正常。

看上去目前超时的问题暂时解决,但是要想更彻底的解,还需要后续对遗留项逐个解决。

遗留项/改进项

  1. 连接池的参数调优,虽然默认的看上去没啥问题,但是迟早肯定会出问题,记一个DFX。
  2. 多了一个HikariCP连接池,而且通过jconsole看了下,两个连接池都会初始化,这块是否有必要有两个连接池,这儿存在隐患,对ShardingSphere需要深入了解下,记一个DFX。
  3. 历史数据的清理,跟PO提出,需要加一个需求不仅仅是告警,可能还有其他数据。
  4. 对于核心且经常更新的表是否需要定时REINDEX

离职系列 第八篇
离职系列,想想这几年在公司的成长,在这做个记录。

此篇是因为遇到了太多环境类问题,从LMT建立的数据统计,这类问题占比已经超过了我处理问题的60%左右,占所有现场问题的20%左右以上,所以抽了个时间把可以在前置检查中避免的问题都梳理出来,试着写了个脚本,并一次次到现场验证,最终有了以下版本。该脚本在产品没有出根解之前,直接交给了TAC,让其与一线一起前置处理,避免过多的流转到LMT。

检查脚本,

   
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
#!/bin/bash

# 环境前置检查脚本(用于提前感知问题,减少安装失败、升级失败等问题出现)
# by hht
# 上传后,chmod +x 授予执行权限,然后./pre_check.sh 直接执行

# 输出错误到文件
exec 2>/tmp/pre_test_error.log
# ANSI颜色代码
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # 恢复默认颜色
YELLOW='\033[0;33m'
YELLOW_BG='\033[43m'
BLACK='\033[30m' # 黑色字体
none='\e[0m'
BLUE="\e[0;94m"
_red_bg() { echo -e "\e[41m$@${none}"; }
is_err=$(_red_bg 异常!)

warn() {
echo -e "\n${YELLOW_BG}${BLACK}警告!${NC} $@\n"
}
err() {
echo -e "\n$@ $is_err\n"
}

# 函数来检验IP地址的有效性
is_valid_ip() {
local ip="$1"
local ip_regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"

if [[ $ip =~ $ip_regex ]]; then
return 0
else
return 1
fi
}

# 声明账号
server1_user="root"
server2_user="root"

echo -e "\n${GREEN}------------------------------------前置检查------------------------------------${NC}"
echo
# 输入服务器IP地址,进行校验
while true; do
echo "请输入master服务器的IP地址:"
read server1_ip

if is_valid_ip "$server1_ip"; then
break
else
warn "无效的IP地址,请重新输入。"
fi
done

while true; do
echo "请输入worker服务器的IP地址:"
read server2_ip
if is_valid_ip "$server2_ip"; then
break
else
warn "无效的IP地址,请重新输入。"
fi
done
# 测试Ping
echo -e "\n${BLUE}ping测试:${NC}\n"
ping_check(){
local server1_ip=$1
local server2_ip=$2
ping -c 3 $server1_ip > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "$server1_ip 可以Ping通。${GREEN}正常${NC}"
else
err "$server1_ip 无法Ping通。"
fi

ping -c 3 $server2_ip > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "$server2_ip 可以Ping通。${GREEN}正常${NC}"
else
err "$server2_ip 无法Ping通。" && exit 1
fi

}
ping_check $server1_ip $server2_ip
echo -e "\n------------------------------------------------------------------------------"
# SSH测试
echo -e "\n${BLUE}SSH测试:${NC}\n"
ssh_check(){
local server1_ip=$1
local server2_ip=$2
if sshpass timeout 10s ssh -o StrictHostKeyChecking=no root@$server1_ip echo "SSH test" 2>/dev/null; then
echo -e "Success: SSH from $server1_ip to $server2_ip connected.${GREEN}正常${NC}"
else
err "SSH from $server1_ip to $server2_ip failed"
fi
}

ssh_check $server1_ip $server2_ip
echo -e "\n------------------------------------------------------------------------------"
# 时间一致性检查
echo -e "\n${BLUE}时间一致性检查:${NC}\n"
date_check(){
local server1_ip=$1
local server2_ip=$2
# 时间一致性检查
server1_time=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$server1_ip date +%s)
server2_time=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$server2_ip date +%s)

if [ $server1_time -eq $server2_time ]; then
echo -e "两台服务器的时间完全一致。${GREEN}正常${NC}"
else
# 使用 date 命令将时间戳转换为日期和时间
formatted1_time=$(date -d "@$server1_time")
formatted2_time=$(date -d "@$server2_time")
err "两台服务器的时间不一致。"
echo -e "${server1_ip}时间为:${formatted1_time}"
echo -e "${server2_ip}时间为:${formatted2_time}"
fi
}

date_check $server1_ip $server2_ip
echo -e "\n------------------------------------------------------------------------------"

# 添加检查防火墙是否开启
echo -e "\n${BLUE}检查防火墙:${NC}\n"
check_firewall_status() {
local server_ip=$1

# 使用 ssh 连接到服务器并查看 firewalld 服务的状态
firewall_status=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "root@$server_ip" "systemctl is-active firewalld")

if [ "$firewall_status" = "active" ]; then
warn "$server_ip 防火墙已开启。请根据http://172.17.160.32:18090/x/cYA0CQ检查端口"
else
echo "$server_ip 防火墙未开启。"
fi
}

# 调用这个函数并传入服务器 IP 和用户名
check_firewall_status $server1_ip
check_firewall_status $server2_ip
echo -e "\n------------------------------------------------------------------------------"

# DNS配置检查
echo -e "\n${BLUE}DNS检查:${NC}\n"
dns_check(){
local server1_user=$1
local server2_user=$1
local server1_ip=$2
local server2_ip=$3
# DNS配置检查 - 验证是否一致
server1_dns=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server1_user@$server1_ip cat /etc/resolv.conf)
server2_dns=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server2_user@$server2_ip cat /etc/resolv.conf)

if [ "$server1_dns" == "$server2_dns" ]; then
echo -e "两台服务器的DNS配置一致。${GREEN}正常${NC}"
else
warn "两台服务器的DNS配置不一致。请判断是否影响集群。"
fi


# DNS配置检查 - 验证是否存在多行nameserver记录
server1_nameserver_count=$(echo "$server1_dns" | grep -c '^nameserver')
server2_nameserver_count=$(echo "$server2_dns" | grep -c '^nameserver')

if [ $server1_nameserver_count -eq 1 ] && [ $server2_nameserver_count -eq 1 ]; then
echo -e "两台服务器的DNS配置中只存在一行nameserver记录。${GREEN}正常${NC}"
else
if [ $server1_nameserver_count -ne 1 ]; then
warn "第一台服务器($server1_ip)的DNS配置存在多行nameserver记录。请判断是否影响集群。"
fi

if [ $server2_nameserver_count -ne 1 ]; then
warn "第二台服务器($server2_ip)的DNS配置存在多行nameserver记录。请判断是否影响集群。"
fi
fi
}

dns_check $server1_user $server1_ip $server2_ip
echo -e "\n------------------------------------------------------------------------------"
# 挂载检查
echo -e "\n${BLUE}数据盘挂载检查:${NC}\n"
mount_check(){
local server1_user=$1
local server2_user=$1
local server1_ip=$2
local server2_ip=$3
server1_mount=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server1_user@$server1_ip mount | grep /opt/local-path-provisioner)
server2_mount=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server2_user@$server2_ip mount | grep /opt/local-path-provisioner)

if [ -n "$server1_mount" ] && [ -n "$server2_mount" ]; then
echo -e "两台服务器都正确挂载了/opt/local-path-provisioner目录。${GREEN}正常${NC}"
else
# echo -e "两台服务器中有一台或两台未正确挂载/opt/local-path-provisioner目录。${RED}异常${NC}"
if [ -z "$server1_mount" ]; then
err "第一台服务器($server1_ip)未正确挂载/opt/local-path-provisioner目录。"
fi

if [ -z "$server2_mount" ]; then
err "第二台服务器($server2_ip)未正确挂载/opt/local-path-provisioner目录。"
fi
fi
}

mount_check $server1_user $server1_ip $server2_ip
echo -e "\n------------------------------------------------------------------------------"
# 使用Telnet检查SFTP服务是否联通
# check_sftp() {
# local server_ip=$1
# echo -e "\n-----------使用Telnet检查SFTP服务是否联通-------------"
# # # 使用Telnet连接到SSH端口(默认是22)
# # # 尝试连接SFTP服务器
# # sftp -oPort=22 $server_ip <<EOF 2>/dev/null
# # quit
# # EOF

# # # Check the exit status of the SFTP command
# # if [ $? -eq 0 ]; then
# # echo "SFTP on $IP is working normally"
# # else
# # echo "SFTP on $IP is not working!"
# # fi

# # 使用SSH命令测试SFTP服务
# ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null sftpuser@$server_ip sftp exit > /dev/null 2>&1

# # 检查SSH命令的退出状态码
# if [ $? -eq 0 ]; then
# echo "SFTP服务正常,可以连接到主机 $server_ip 的SFTP服务。"
# else
# err "SFTP服务异常,无法连接到主机 $server_ip 的SFTP服务。"
# fi
# }
# # 调用函数来进行Telnet检查
# check_sftp $server_ip

# 检查master SSH服务状态和配置
echo -e "\n${BLUE}master SSH服务状态和配置检查:${NC}\n"
check_sshd_config() {

local server_ip=$1
local server_user=$2

# 使用 ssh 连接到服务器并执行命令检查SSH服务状态
ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "systemctl is-active sshd" > /dev/null 2>&1

if [ $? -eq 0 ]; then
# 获取SSH配置文件内容
sshd_config_content=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "cat /etc/ssh/sshd_config")

# 检查配置文件内容是否包含指定的三行数据
if echo "$sshd_config_content" | grep -q -E "Subsystem\s+sftp\s+internal-sftp" && echo "$sshd_config_content" | grep -q "Match User sftpuser" && echo "$sshd_config_content" | grep -q "ChrootDirectory /opt/ftpfile/sftp/sftpuser/"; then
echo -e "SSH配置正常。${GREEN}正常${NC}"
else
warn "请检查sshd_config文件,是否正确配置sftp。"
fi
else
warn "无法连接服务器或检查SSH服务状态。"
fi
}

# 调用函数执行检查
check_sshd_config $server1_ip $server1_user
echo -e "\n------------------------------------------------------------------------------"
# sftp_with_password() {
# echo -e "\n-----------检查master的sftp连通性-------------"
# local server_ip=$1
# local server_password=$2

# # 使用 expect 来自动输入密码
# expect -c "
# spawn sftp -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null sftpuser@$server_ip
# expect {
# \"password:\" {
# send \"$server_password\n\"
# exp_continue
# }
# eof
# }
# "

# if [ $? -eq 0 ]; then
# echo -e "$server_ip SFTP连接成功。${GREEN}正常${NC}"
# else
# warn "无法连接$server_ip 的SFTP服务。"
# fi
# }

# # 调用函数来进行SFTP连接检查
# sftp_with_password $server1_ip "FYktvR1w2upoOb"

# 检查目录权限是否为777
# check_directory_permission() {
# local server_ip=$1
# local directory_path=$2
# echo -e "\n-----------检查master的目录${directory_path}权限-------------"

# # 使用 ssh 连接到服务器并执行 stat 命令获取目录权限信息
# ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null sftpuser@$server_ip "stat -c %a $directory_path" > /dev/null 2>&1

# if [ $? -eq 0 ]; then
# # 获取目录权限
# directory_permission=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null sftpuser@$server_ip "stat -c %a $directory_path")

# if [ "$directory_permission" -eq 777 ]; then
# echo -e "$directory_path 目录权限为777。${GREEN}正常${NC}"
# else
# warn "$directory_path 目录权限不是777。"
# fi
# else
# warn "无法连接$server_ip 服务器或获取目录权限。"
# fi
# }
echo -e "\n${BLUE}检查master的sftp目录权限:${NC}\n"
check_user() {
ssh $1 "id $2 >/dev/null 2>&1"
if [ $? -eq 0 ]; then
echo -e "$2用户在$1上存在。${GREEN}正常${NC}"
else
err "$2用户在$1上不存在"
fi
}

check_dir_perm() {
ssh $1 "if [ \`stat -c %a $2\` -eq $3 ]; then echo -e '$2目录为$3权限。${GREEN}正常${NC}'; else err '$2目录不为$3权限'; fi"
}

# 调用函数来检查服务器的目录权限
check_user $server1_ip "sftpuser"

check_dir_perm $server1_ip "/opt" 755

check_dir_perm $server1_ip "/opt/ftpfile/sftp/sftpuser" 755

echo -e "\n------------------------------------------------------------------------------"
# check_directory_permission $server1_ip "/opt"
# check_directory_permission $server1_ip "/opt/ftpfile/sftp/sftpuser"

# 检查主机名与/etc/hosts文件的一致性
echo -e "\n${BLUE}检查主机名的一致性:${NC}\n"
check_hostname_and_hosts() {

local server_ip=$1
local server_user=$2
# 使用 ssh 连接到服务器并获取主机名
server_hostname=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "hostname")

if [ $? -eq 0 ]; then
# 获取 /etc/hosts 文件内容
hosts_file_content=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "cat /etc/hosts")

# 检查主机名与 /etc/hosts 内是否一致
if echo "$hosts_file_content" | grep -q -E "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s+$server_hostname\s*$"; then
echo -e "$server_ip 主机名与/etc/hosts文件一致。${GREEN}正常${NC}"
else
warn "$server_ip 主机名与/etc/hosts文件不一致。"
fi
else
warn "无法连接$server_ip 服务器或获取主机名。"
fi
}

# 检查主机名与Kubernetes节点名是否一致
check_hostname_and_k8s_node() {

local server_ip=$1
local server_user=$2
local num=$3
# 使用 ssh 连接到服务器并获取Kubernetes节点名
k8s_node_name=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "kubectl get node -o jsonpath='{.items[$num].metadata.name}'")

if [ $? -eq 0 ]; then
# 获取主机名
server_hostname=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "hostname")

# 检查主机名与Kubernetes节点名是否一致
if [ "$server_hostname" = "$k8s_node_name" ]; then
echo -e "$server_ip 主机名与Kubernetes节点名一致。${GREEN}正常${NC}"
else
warn "$server_ip 主机名与Kubernetes节点名不一致。"
fi
else
warn "无法连接$server_ip 服务器或获取Kubernetes节点名。"
fi
}

# 调用函数来检查服务器的主机名与/etc/hosts文件一致性
check_hostname_and_hosts $server1_ip $server1_user
check_hostname_and_hosts $server2_ip $server2_user

# 调用函数来检查服务器的主机名与Kubernetes节点名一致性
check_hostname_and_k8s_node $server1_ip $server1_user 0
check_hostname_and_k8s_node $server2_ip $server2_user 1
echo -e "\n------------------------------------------------------------------------------"

# 磁盘写入速度测试(带缓存)
# disk_speed_test() {
# # local server_ip=$1
# # local server_user=$2
# # local test_file="/tmp/disk_speed_test_file"
# # # 进行写入测试
# # write_speed=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "dd if=/dev/zero of=${test_file} bs=8k count=128 conv=fsync oflag=direct" 2>&1 | tail -n 1)
# # echo -e "${server_ip} 写入速度: $write_speed"
# # # 进行读取测试
# # read_speed=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "sudo dd if=${test_file} of=/dev/null bs=8k count=128 conv=fsync iflag=direct" 2>&1 | tail -n 1)
# # echo -e "${server_ip} 读取速度: $read_speed"

# # # 删除测试文件
# # ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "rm $test_file"
# }

# 测试磁盘读速度的函数
disk_speed_test() {
local server_ip=$1
local test_duration=30 # 测试持续时间(秒)

echo "开始测试 $server_ip 过程会持续30s..."

local time0=$(date "+%s")
cat /dev/null > disk_res

while ((($(date "+%s") - time0) <= test_duration)); do
disk_info=$(ssh "$server_ip" 'dd if=/dev/zero of=output.file bs=8k count=128 conv=fsync 2>&1 1>/dev/null')
io_res=$(echo "$disk_info" | grep --only-matching -E '[0-9.]+ ([MGk]?B|bytes)/[s(ec)?|秒]')
echo "$io_res" >> disk_res
done

local count=$(cat disk_res | wc -l)
local sum=$(cat disk_res | xargs -n2 | awk '{ if ($2 == "kB/秒" || $2 == "kB/s") a+=($1/1024); else a+=$1 } END{printf("%.2f", a)}')
local average_speed=$(awk 'BEGIN{printf "%.2f\n", '$sum'/'$count'}')

echo -e "平均速度 $server_ip: ${RED}$average_speed MB/s${NC}。推荐值:${GREEN}>=200m/s${NC}"
}



# 磁盘写入速度测试(不带缓存)

# disk_speed_test_no_cache() {
# local server_ip=$1
# local server_user=$2
# local test_file="/tmp/disk_speed_test_file"
# # 禁用磁盘缓存
# ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "hdparm -W0 /dev/mapper/centos-root" 2>/dev/null
# # 进行写入测试
# write_speed=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "dd if=/dev/zero of=${test_file} bs=8k count=128 conv=fsync oflag=direct" 2>&1 | tail -n 1)
# echo -e "${server_ip} 写入速度: $write_speed"
# # 进行读取测试
# read_speed=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "sudo dd if=${test_file} of=/dev/null bs=8k count=128 conv=fsync iflag=direct" 2>&1 | tail -n 1)
# echo -e "${server_ip} 读取速度: $read_speed"

# # 删除测试文件
# ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "rm $test_file"
# # 启用磁盘缓存
# ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $server_user@$server_ip "hdparm -W1 /dev/mapper/centos-root" 2>/dev/null
# }

# 测试磁盘写入速度的函数
disk_speed_test_no_cache() {
local server_ip="$1"
local test_duration=30 # 测试持续时间(秒)

echo "开始测试 $server_ip 过程会持续30s..."

local time0=$(date "+%s")
cat /dev/null > disk_res

while ((($(date "+%s") - time0) <= test_duration)); do
disk_info=$(ssh "$server_ip" 'dd if=/dev/zero of=output.file bs=8k count=128 oflag=direct,nonblock conv=fsync 2>&1 1>/dev/null')
io_res=$(echo "$disk_info" | grep --only-matching -E '[0-9.]+ ([MGk]?B|bytes)/[s(ec)?|秒]')
echo "$io_res" >> disk_res
done

local count=$(cat disk_res | wc -l)
local sum=$(cat disk_res | xargs -n2 | awk '{ if ($2 == "kB/秒" || $2 == "kB/s") a+=($1/1024); else a+=$1 } END{printf("%.2f", a)}')
local average_speed=$(awk 'BEGIN{printf "%.2f\n", '$sum'/'$count'}')

echo -e "平均速度 $server_ip: ${RED} $average_speed MB/s ${NC}。推荐值:${GREEN}>=50m/s${NC}"
}


# 检查 CPU 是否支持 AVX
echo -e "\n${BLUE}检查 CPU 是否支持 AVX:${NC}\n"
check_avx_support() {
local server_ip=$1
local avx_check=$(ssh -q -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$server_ip "grep -o 'avx' /proc/cpuinfo")

if [ -n "$avx_check" ]; then
echo -e "$server_ip CPU 支持 AVX (Advanced Vector Extensions)。${GREEN}正常${NC}"
else
warn "$server_ip CPU 不支持 AVX (Advanced Vector Extensions)。"
fi
}

# 调用函数来检查 AVX 支持
check_avx_support $server1_ip
check_avx_support $server2_ip

# 定义要加载镜像的目录列表
load_images() {
image_directories=("/opt/xx/images/product/insight" "/opt/xx/images/product/insight/patch")

# 遍历目录并加载镜像
for directory in "${image_directories[@]}"; do
if [ -d "$directory" ]; then
cd "$directory"
echo "进入目录: $directory"
for file in $(ls . | grep .tgz); do
echo "加载镜像: $file"
docker load < "$file"
done
else
echo "目录 $directory 不存在。"
fi
done
}


# 检查磁盘性能
echo -e "\n${BLUE}磁盘测试:${NC}\n"
# read -p "是否要检查磁盘? (y/n): " confirm
echo "是否要检查磁盘? (y/n):"
read confirm
if [ "$confirm" = "y" ]; then
echo -e "\n${BLUE}磁盘写入速度测试(不带缓存):${NC}\n"
disk_speed_test_no_cache $server1_ip
disk_speed_test_no_cache $server2_ip
echo -e "\n${BLUE}磁盘写入速度测试(带缓存):${NC}\n"
disk_speed_test $server1_ip
disk_speed_test $server2_ip
else
echo "取消磁盘检查。"
fi

# 镜像丢的时候使用
echo -e "\n${BLUE}镜像加载:${NC}\n"
echo "是否要加载 Docker 镜像? (y/n):"
read confirm
# read -p "是否要加载 Docker 镜像? (y/n): " confirm
if [ "$confirm" = "y" ]; then
load_images
else
echo "取消加载 Docker 镜像。"
fi



离职系列 第七篇
离职系列,想想这几年在公司的成长,在这做个记录。此篇主要谈谈LMT时,处理的两次OOM问题。

为啥是两次?因为都很有代表性,一次可以算是三方库使用不当,一次是程序自身的问题。

第一次

JPA使用不当,….待续

第二次

应用频繁重启,….待续

离职系列 第七篇
离职系列,想想这几年在公司的成长,在这做个记录。上一篇服务可用性定位问题常用命令,针对服务可用性常用命令进行了说明,这一篇主要是讲实践案例。

因为我在LMT除了日常管理工作外,额外向领导请求了承担部分现场问题处理,挑的比较陌生且有挑战性的“服务可用性的问题”,想着在实践中学习,并且这部分问题是最麻烦的没有什么标准,往往又是最紧急的,我担起来一是减少了组员上报成本,另一个我更方便直接找到对应的负责人进行沟通,提高效率。所以这块积累的经验更多。

案例分享

简单分享几个案例,一个主要对外(TAC),一个对内(组内研发)。两个文档因为对象不同,使用场景不同,稍有差异。

整理案例,一是为了组内共同学习,二是提供给TAC团队,让其能更多的拦截一些一线提出的较简单的现场问题。

案例1(TAC)

案例2(TAC)

案例3(研发)

离职系列 第六篇
离职系列,想想这几年在公司的成长,在这做个记录。此篇主要谈谈LMT时期。

因为现场故障直接影响客户口碑、人力成本等事业部的重要指标,所以事业部领导要求针对现场故障处理需要有单独的奖惩制度,让整个事业部重视起来,所以我整理了对应的内容用于事业部邮件同步。

事故的定义

故障处理或产品自身服务中断造成客户损失、恶劣影响。(一线、客户投诉且影响产品口碑的)。

事故处理

奖惩制度

这块就比较个性化了,主要看重视程度,需要根据公司规章制度和领导的要求做调整。

离职系列 第五篇
离职系列,想想这几年在公司的成长,在这做个记录。上一篇现场故障定位指南,主要讲的方法论,这篇主要对服务可用性的几个场景总结下相应的命令。

以下命令主要针对现场经常出现的安装失败、升级失败、补丁失败、服务不断重启、服务不可用几个场景:

  1. 安装失败,通常就是现场环境问题,比如服务器的磁盘性能不达标、网络通信问题、服务器DNS配置错误、集群IP段不可用
  2. 升级失败,通常和服务器的资源紧张有关(内存、磁盘、CPU等)
  3. 服务不断重启,通常是基础组件问题如redis异常、应用pod自身程序的bug如OOM、k8s组件问题如etcd重启
  4. 服务不可用,通常就是集群出了问题,比如磁盘满了导致镜像丢失

命令

  1. 确认环境信息
    • 环境信息
      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
      # 操作系统版本
      cat /etc/redhat-release # CentOS版本
      cat /etc/openEuler-release # 欧拉版本
      uname -r # 内核版本
      cat /proc/version # 内核编译信息
      hostnamectl # 查看完整的系统信息

      # 系统基础信息
      df -h # 磁盘空间
      free -h # 内存使用
      top # CPU和进程状态
      netstat -ant # 网络连接
      uptime # 系统负载
      iostat -x 1 10 # 磁盘状态

      # 进程分析
      ps -ef | grep 进程名
      pstree -p 进程ID
      lsof -p 进程ID

      # 分区及挂载
      lsblk # 查看块设备
      df -Th # 查看文件系统类型和空间
      mount | grep -E "^/dev" # 查看挂载参数

      # 磁盘空间
      du -sh /* | sort -hr # 大文件目录排序
      # 时间同步状态
      chronyc sources -v
    • K8s集群状态
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      # K8s集群状态
      systemctl status kubelet # kubelet是否正常
      systemctl status docker # docker是否正常
      systemctl status NetworkManager # 网络连接工具是否正常
      kubectl cluster-info #查看集群信息
      kubectl get nodes # kubelet集群节点
      kubectl get po -A # 查看所以pod状态
      kubectl get po -A -owide # 查看所以pod的ip和所在的node
      kubectl describe node <node-name>
      kubectl get events -n <namespace> #Kubernetes 事件日志
      journalctl -u kubelet -f # 日志查看
      cat /var/log/messages | grep xx # 日志查看

      # 应用Pod状态
      kubectl get pods -n <namespace> -o wide
      ping pod_ip # 判断容器之间的联通性
      kubectl describe <pod-name> -n <namespace>
      kubectl exec -it <pod-name> -n <namespace> /bin/sh # 进入容器内部
      kubectl logs <pod-name> -n <namespace>

      # 集群资源状态
      kubectl top nodes
      kubectl top pods -n <namespace>
  • 客户网络环境限制(可用端口、防火墙策略)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 网络组件
    ip link show
    iptables -L

    # DNS配置
    cat /etc/resolv.conf
    # 网络分析
    curl url # 应用连通性
    fping -c xx -p xx 目标IP或域名 # 基础连通性
    ping <目标IP> # 基础连通性
    telnet <IP> <端口> # 端口连通性
    traceroute # 路由跟踪
    tcpdump -i any port <端口> -w dump.pcap # 抓包分析

参考

https://jimmysong.io/kubernetes-hndbaook/guide/using-kubectl.html
https://kubernetes.io/zh-cn/docs/tasks/debug/_print/
https://cheat.sh/
https://kubernetes.io/docs/reference/kubectl/cheatsheet/
https://kubernetes.io/zh/docs/reference/kubectl/
https://docs.docker.com/engine/reference/run/


离职系列 第四篇
离职系列,想想这几年在公司的成长,在这做个记录。此篇主要谈谈LMT时,简单总结了一套针对问题现场定位方法论。

前言

在客户现场环境中,我们往往面临网络隔离、工具受限、信息不完整等挑战。并且由于LMT(9人)资源有限,但是试用+付费的现场却有500左右,且数字还在不断增加,因此需要科学的方法论和充分的实践经验,下面是我针对服务异常问题整理的,该问题出现频次最高且专业性强涉及多种操作系统(欧拉、centos、麒麟),整理文档方便其它成员学习实践。成员可以结合实际情况灵活调整。

定位指南

第一阶段:问题收集与初步分析(原则上TAC或一线提供)

  1. 确认问题的基本信息

    • 问题的具体表现(错误信息、异常行为等)
    • 问题的影响范围
    • 问题的发生时间和频率
    • 问题是否可复现
  2. 建立问题基线

    • 首次发现问题的时间点
    • 相关变更的时间点(补丁、升级、断电等)
    • 现场采取的临时措施

第二阶段:快速诊断(LMT)

  1. 检查环境

    • k8s集群、组件状态、应用pod状态
    • 检查系统资源(磁盘、内存等)
  2. 检查日志信息

    • 查看集群日志、组件日志
    • 查看应用pod日志
  3. 进行初步故障假设

    • 根据已收集的信息提出可能的故障原因
    • 按照影响范围和可能性排序
    • 可通过经验+知识库等制定快速验证方案

第三阶段:深入分析(LMT+后端研发接口人)

  1. 验证假设

    • 复现问题场景
    • 收集更多证据支持或否定假设
  2. 确定初步根因

    • 总结所有收集到的证据
    • 确认问题的触发条件
    • 建立问题发生的完整链路
  3. 是否升级问题

    • 如果验证有出入或者没有更好的办法则转交问题到我
    • 我来决定是否升级问题(申请后端研发介入)

第四阶段:解决方案(LMT+后端研发接口人+TAC+一线)

  1. 制定修复方案

    • 提出短期解决方案(快速修复)
    • 设计长期解决方案(根本解决)
    • 评估方案的风险和影响并告知一线,让其与客户沟通确认
  2. 实施修复

    • 客户确认后,在测试环境验证解决方案
    • 准备回滚方案(备份数据、备份镜像等)
    • 实施修复并验证效果

注意事项

  1. 所有重要操作前先备份
  2. 收集足够的证据再行动
  3. 重要变更需要得到一线授权
  4. 保持操作记录的完整性
  5. 及时同步问题处理进展
  6. 警惕处理过程中的连锁反应

附一张简单的问题记录卡模板

离职系列 第三篇
离职系列,想想这几年在公司的成长,在这做个记录。此篇主要谈谈LMT时,整理的现场故障处理流程。

LMT团队对外最大的价值就是及时响应现场故障,有点Google SRE On-Call Engineer的感觉,但是现场问题往往较复杂或处理链条很长,所以必须要有一个相对标准且高效的流程,让各角色团队达成一致,从而能快速推进。

LMT主要职责

  1. 快速响应告警,处理生产环境中的故障,对故障分级,同时保证SLA时效。

  2. 故障排查

    1. 使用日志、指标和工具(如 fping、netdata、arthas 等)定位问题的根本原因。
    2. 执行临时修复措施(如回滚、重启服务)以恢复服务。
    3. 问题升级
  3. 事后故障复盘

    1. 在故障解决后,组织处理人撰写事后分析报告,记录问题的根本原因、影响和改进措施。
    2. 预防,推动改进措施的实施,防止类似问题再次发生。
  4. 协作与沟通

    1. 跟进每一个现场故障,与开发团队、产品团队和其他相关方协作,确保问题得到彻底解决。
    2. 在故障期间向利益相关者提供状态更新和预计故障关闭时间。
  5. 知识库

    1. 和研发团队一起沉淀文档建立知识库,提升效率的同时培养人员。

现场故障处理流程

基于上面的职责,我梳理两个版本,0.5和1.0版本现场故障处理流程,主要在于先分清每个团队的职责,然后把现场故障能快速的流转起来,不管是否为疑难杂症,都做到万事有回响。

0.5版本

0.5版本用于建队初期,时间紧任务重,人员还未完全到位的情况。彼时LMT更多的是解决简单的问题以及跟进问题,大多问题的处理还是需要寻求原研发团队的支持故称接口人模式
(故障组就是LMT)

1.0版本

1.0版本是各核心模块人员配置到位,且各团队磨合期过了后,整理的,1.0版本流程重点主要在两方面:

  1. LMT能独立解决大部分现场故障
  2. LMT没法及时解决的需要有故障的升级路径,升级后LMT转为跟进故障处理情况并反馈一线。

后续还有2.0版本,但是因为我已经不在LMT,且职责已经跟当初我建立时大相径庭所以我就不做梳理了。

最后

一定不要忘了还有 复盘与改进

  1. 复盘

    • 使用标准的文档模板,由实际故障处理人记录问题的完整过程,总结根因
    • 复盘时共创改进措施,并每项都建立跟进人和时间
  2. 预防措施

    • 举一反三,自查类似问题
    • 更新知识库
    • 加强相关人员培训
  3. 填单
    整个流程,为了各团队统一语言,所有过程记录我们要求都基于JIRA单,每个团队对应其流程节点,对JIRA单进行扭转和补充。

离职系列 第二篇
离职系列,想想这几年在公司的成长,在这做个记录。此为第二篇,我在LMT时,整理的一个现场缺陷处理流程。

LMT的诞生背景

随着客户规模的扩大和系统复杂性的提升,产品在客户现场面临的问题越来越多,如果没有专门的团队负责快速处理这些问题,而是像之前一样流到PDT团队,事业部会面临以下困境:

  1. 响应延迟:
    1. 故障发生后,没有明确的责任人或团队,响应时间过长。
    2. 多次传递信息易导致关键信息丢失或误解。
  2. 修复效率低下:
    1. 不同团队各自为战,缺乏统一协调,资源浪费严重。
    2. 处理人员对现场环境了解不足,容易导致误判或误操作。
  3. 业务影响扩大:
    1. 紧急问题未能及时解决,可能对用户体验和企业声誉造成负面影响,影响口碑。
    2. 业务中断时间延长,如升级为事故,则事业部需要追责且需要出具报告跟客户道歉。
  4. 缺乏经验积累:
    1. 故障处理缺乏记录与总结,类似问题反复发生却未能彻底解决。

LMT 的价值与必要性

LMT(Line Maintenance Team)是一支专注于现场故障处理的专业团队,其成立能够有效应对上述问题,带来以下价值:

  1. 快速响应,缩短问题解决时间
    • 通过明确的责任划分和快速响应机制,LMT会确保问题第一时间得到处理。
    • 使用标准化工具和诊断方法,快速定位问题。
    • 如遇到必须要对应的研发才能解决的问题时,LMT会对前期现场问题进行基本的定位处理,为后续研发提供输入,大大缩减研发修复问题的时间。
  2. 专业处理,提升修复效率
    • LMT团队成员筛选的都是技术经验丰富,对功能和业务场景有深入理解的研发,能对一线服务和TAC团队提供更专业的支持,减少对PDT团队的打断。
    • 对于无法远程或远程困难的现场,可提供在线技术支持。
  3. 降低业务中断风险
  • 优先处理对业务影响重大的问题,将损失降至最低。
  • 与其他团队协作,LMT可作为沟通的桥梁,推动重大问题尽快解决。
  1. 经验积累与优化
  • 故障处理经验录入知识库,形成可复用的解决方案。
  • 通过定期复盘优化流程,不断提升处理效率和稳定性。
  1. 统一管理与高效协作
  • 明确的组织架构和职责分工,解决了多部门协作中的沟通问题。
  • 借助Jira系统和实时监控平台,实现高效管理。
  • 产品补丁包打包由LMT接手,可减少Devops团队工作的同时,更贴近一线的需求。

正因如此,LMT 的成立成为企业提升运维效率、保障业务稳定性的必然选择。

最后

给你们贴一张LMT成立初期拦截现场故障的占比,你就知道单独抽出几个研发成立LMT有没有必要了。

AI 第一篇

背景

这篇文章拖了很久了。因为我之前带LMT团队,团队的主要工作内容就是处理客户现场问题,因为我要求过程留痕并且达到知识沉淀的效果,所以处理过程中产生了大量的文档,也就是很多经验都落到了文档上,比如linux系统(硬盘、系统版本、dns等)、docker、k8s、现场网络问题(防火墙、网闸等安全设备),也就是运维经常面临的问题,也有产品配置、bug等文档。

文档越来越多,但是却越来越难利用起来,我就想着把这些经验积累起来弄一个知识库,就像gpt一样,只要我输入有关问题他就能根据这些内容生成回答的内容,当然里面不仅仅是经验问答还需要有linux系统(硬盘、系统版本、dns等)、docker、k8s、现场网络问题(防火墙、网闸等安全设备)这些原始(原理)知识,还得有我们的产品知识。

这个知识库一是为了方便使用且能让我练手,二是当时公司推行创新活动,AI相关的案例也能为事业部加分。

开始

知识库的定位:小而美,因为是个人发起的前期申请到的服务器资源有限,而且功能非常单一明确,只需要满足知识库就行,不需要全能。

选型

通过一通查找对比,综合考虑,选择了Anything LLM或者Dify.AI+Ollama+Llama2小模型。

Anything LLM

alt text

考虑的其中一点

是用Anything LLM这种开箱即用的还是用LangChain这种需要自己上手写代码的
Anything LLM和Dify.AI在其架构中广泛使用了LangChain组件,尤其是:

  • 文档加载器(Document Loaders)
  • 文本分割器(Text Splitters)
  • 向量存储(Vector Stores)
  • 检索器(Retrievers)
  1. 直接使用Anything LLM这种系统的好处
    1. 意味着即使不直接编写LangChain代码,您也在间接使用LangChain的强大功能。这有几个好处:
      模块化设计 - 可以灵活替换组件(如切换向量数据库)
      经过验证的架构 - 使用业界已验证的RAG实现方式
      未来升级路径 - 如果您将来想更深入定制,可以直接使用LangChain API
  2. 坏处
    1. 没有深入的了解和实践经验

我当时其实想用LangChain的,环境都搭好了,但是因为没有系统的学习过,进度很慢,没法赶上评审节点,所以最终选择了使用Anything LLM这种简单的方式。

验证

Anything LLM+Ollama试了几种模型和Embedder,最终勉强得出一个组合Llama2:7B-chinese+bge-m3
还需要调整文档本身的内容,以及一些参数再多次尝试。

待续…

参考

https://docs.useanything.com/setup/llm-configuration/overview
https://github.com/Mintplex-Labs/anything-llm/blob/master/docker/HOW_TO_USE_DOCKER.md
https://adasci.org/anythingllm-for-local-execution-and-inferencing-of-llms-a-deep-dive/
https://itnext.io/deploy-flexible-and-custom-setups-with-anything-llm-on-kubernetes-a2b5687f2bcc
https://www.youtube.com/watch?v=4UFrVvy7VlA