线上出现问题,就像墨菲定律。它一定会出现,没听说过哪个团队零失误。如果有我想也是我们的航天事业吧,点赞。
本来这没什么好说的,但是想着还有其他朋友可能需要,就简单记录一下。不要说什么规范那规则,实际工作中无论白猫黑猫,抓到老鼠就是好猫。
案发现场
每一个周期都有要对线上的系统进行发版,恰巧同事失误将我的一个测试还未通过的接口给推到了线上。因为该接口是一个数据接收的接口,具体是处理APP和web异步推送过来的数据。这样一搞我第二天下午两点多APP端才发现数据推送失败,至此数据一条未存入库中。控制台日志收集整理已经有三四百兆大小,唉又是一笔不小的额外活。
我是如何处理的
- 立马向领导报告当前实际情况。
- 按照应急流程与其他方协商在时间节点停掉该接口的业务。
- 收集停掉后的服务日志。
- 确定数据缺口时间区间,确定数据缺少数量。
- 报告大致数据缺少量。
- 协商如何将数据重新入库,是APP端和web端重新发送还是在日志中提取。
- 最终日志中提取(此处可见日志中简洁且关键信息的输出对以后解决问题的帮助有多大)
- 以下就是对日志的提取
- 将数据的模拟发送
日志特征分析
我出现问题的接口假设是2926,日志中还有其他接口的日志信息,我需要将自己相关接口的信息给提炼处理。但是日志中换行字符 “\r\n” 和 “\r” 混用,这给后来提取造成了一定的困扰。这暂且不提,具体看日志提取部分。
部分和谐修改后的日志如下:
2021-01-05 14:02:03 -接口入===>
2021-01-05 14:02:04 -接口返回===>WebServiceData(afferent=null, param={AAE135=123421342314, AAC003=李卓琦}, result=0, message=null, efferent={"HDJG":"00"})
2021-01-05 14:05:10 -接口入===>
2021-01-05 14:05:11 -接口返回===>WebServiceData(afferent=null, param={AAE135=123123123438, AAC003=揣震}, result=0, message=null, efferent={"HDJG":"00"})
2021-01-05 14:05:12 -接口入===>
2021-01-05 14:05:13 -接口返回===>WebServiceData(afferent=null, param={AAE135=14523412341234, AAC003=宋岩}, result=0, message=null, efferent={"HDJG":"00"})
2021-01-05 14:08:09 -接口入===>2200^23452435245
2021-01-05 14:08:09 -接口返回===>WebServiceData(afferent=2200^223452523450020, param=null, result=1, message=com.neusoft.unieap.service.exception.AppException: 未查询到该人的相关信息!, efferent=null)
2021-01-05 14:09:46 -接口入===>2200^234523235452
2021-01-05 14:09:51 -接口返回===>WebServiceData(afferent=2200^223452345020, param=null, result=1, message=com.neusoft.unieap.service.exception.AppException: 未查询到该人的相关信息!, efferent=null)
2021-01-05 14:09:57 -接口入===>2200^2345223452
2021-01-05 14:09:57 -接口返回===>WebServiceData(afferent=2200^23452345243523, param=null, result=0, message=null, efferent=XXXX
)
2021-01-05 14:09:57 -新接口入参===>{
"targetAttributeObject":{
"business":"0179",
"createTime":1609826997500,
"fromUser":"232442",
"msgType":"json",
"toUser":"433434",
"token":"ErMWt9A"
},
"targetContent":{
"CHRIDDATE":"20201013",
"MATEMEC001":"23232323234",
"MATENAME":"李邱双子"
}
}
021-01-05 14:11:20 -接口入===>1500^232323
2021-01-05 14:11:20 -接口返回===>WebServiceData(afferent=1500^12, param=null, result=0, message=null, efferent=132123123
)
2021-01-05 14:11:20 -新接口入参===>{
"targetAttributeObject":{
"business":"2926",
"createTime":1609827080501,
"fromUser":"21213443",
"msgType":"json",
"toUser":"21323",
"token":"ErMWt9A"
},
"targetContent":{
"AAB034":"234234",
"AAE036":"20210105",
.......
"PIC":"/9j"
"PICNUM":"1",
"PICTYPE":"2"
}
}
2021-01-05 14:11:21 -新接口返回===><200,{"APPCODE":0,"ERRMSG":""},[Server:"aaa", Date:"Tue, 05 Jan 2021 06:10:27 GMT", Content-Type:"text/html; charset=UTF-8", Content-Length:"25", Connection:"keep-alive", Error-Code:"0", P3P:"CP=CAO PSA OUR", Error:"", X-Powered-By:"Servlet/2.5 JSP/2.1", X-Frame-Options:"SAMEORIGIN"]>
2021-01-05 14:11:23 -接口入===>
写个方法提取日志
代码写的比较匆忙,大致就是用了 RandomAccessFile 来进行大文件的读取,其实我是看上了它的指针,同时因为换行字符 “\r\n” 和 “\r” 都有,RandomAccessFile 在(Java8)读取一行的时候遇到"\r\n" 会出现再读下一行的时候指针就会卡在"\r\n"之间的位置,所以下面代码中读取一行用了自己的方法。网上也有倒着读取的方式巧妙的避免这个问题。但是我还是习惯正序读取。
另外说的就是,因为读取一行是不包含"\r\n" 和 “\r” 这样的字符的,所以我对提取的数据在完成一个完整的数据后手动加上一个 “\r\n” 这样写入文件中就是一行一个完整的json数据,方便后续的发送
具体提取多个日志代码如下:
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
/**
* <p>Title: GetDataFromLog</p>
* <p>Description: 从log里面提取2926数据</p>
* @author houzw
* @date 2021年1月6日
*/
public class GetDataFromLog {
/**
* 执行一个log文件组,从中提取数据并输出到一个文本文件中
* @param args
* @throws IOException
*/
public static void main(String args[]) throws IOException {
// 创建一个list 存储文件
List<String> fileList = Arrays.asList(
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-05.14.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-05.15.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-05.16.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-05.17.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.08.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.09.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.10.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.11.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.13.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.14.log",
"D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.15.log"
// "D:/nowWork/7001XXX/2101一月份打版需求管理备注/logback.2021-01-06.16.log"
);
String outFile = "D:/jsonData.txt";
final File tmpLogFile = new File(outFile);
if(!tmpLogFile.exists()) { // 文件不存在就创建
tmpLogFile.createNewFile();
} else {
// tmpLogFile.delete();
// tmpLogFile.createNewFile();
}
for (String str : fileList) {
readlogWrit2File(str, tmpLogFile);
}
}
/**
* 读取一行文本
* @param raf
* @return
* @throws IOException
*/
public static final String readLine(RandomAccessFile raf) throws IOException {
StringBuffer input = new StringBuffer();
int c = -1;
boolean eol = false;
while (!eol) {
switch (c = raf.read()) {
case -1:
case '\n':
eol = true;
break;
case '\r':
eol = true;
long cur = raf.getFilePointer();
if ((raf.read()) != '\n') { // 此处判断并未处理
raf.seek(cur);
} else { // 此处源码可能是有bug,这儿增加 \r情况如果后一位为 \n 则指针指向下一个
raf.seek(cur + 1);
}
break;
default:
input.append((char)c);
break;
}
}
if ((c == -1) && (input.length() == 0)) {
return null;
}
return input.toString();
}
/**
* 快速匹配文本中是否有无目标字符串
* @param source
* @param matchStr
* @return
*/
private static boolean matchStr(String source, String... matchStr) {
for (int i = 0; i < matchStr.length; i++) {
if(!source.contains(matchStr[i])) {
return false;
}
}
return true;
}
/**
* 将文本数据写入到新文件中
* @param filename
* @param tmpLogFile
*/
private static void readlogWrit2File(String filename, File tmpLogFile) {
// 读取2926的参数信息输出到新文件中
try (RandomAccessFile raf = new RandomAccessFile(filename, "r");
Writer txtWriter = new FileWriter(tmpLogFile,true)){
// String charset = "utf-8";
String charset = "gb2312";
long len = raf.length();
System.out.println(new BigDecimal(len).divide(new BigDecimal(1<<20), 3, BigDecimal.ROUND_UP) + "Mb");
long start = raf.getFilePointer();
long nextend = start;
long preLen = -1;
String line;
// 文本有 \r\n 混用情况,需要额外判断,此处用自定义的readLine
while (nextend < len) {
raf.seek(nextend);
line = readLine(raf);
if(line == null) {// 说明读到文件的结尾
break;
}
// RandomAccessFile 每次读取无论什么格式都转为 ISO-8859-1, 所以charset才是文档编码的格式
String data = new String(line.getBytes("ISO-8859-1"), charset);
if(matchStr(data, "\"business\":\"2926\"")) { // 发现2926的交易
nextend = raf.getFilePointer() - data.length() - preLen - 2 - 1; // 上一行的指针位置,也即指针回退两行另外加两个换行符
StringBuilder sb = new StringBuilder("{"); // 根据日志可知 ,补全json串的 左大括号 {
String str;
int i = 0;
do {
raf.seek(nextend);
line = readLine(raf);
str = new String(line.getBytes("ISO-8859-1"), charset);
sb.append(str);
nextend = raf.getFilePointer();
} while (!"}".equals(str)); // 根据日志发现 最后一个右大括号为单独一行,即发现json的结尾
// 将数据记录到文件
txtWriter.write(sb.toString().replaceAll("\t", "") + "\r\n");
txtWriter.flush();
}
preLen = line.length(); // 记录当前行指针的偏移量
nextend = raf.getFilePointer();
}
} catch (FileNotFoundException e) {
// 创建文件流失败
e.printStackTrace();
} catch (IOException e) {
// io错误
e.printStackTrace();
}
}
}
将数据发送至接口,由接口完成业务逻辑处理
将数据提取完成后进行校验,确保完全符合,数据数量是否吻合。提取部分进行测试库预发送,然后进行正式服务的接口调用。
提取了 79多MB的数据,将它们进行组织成一行,确保读取一行为一条完整的数据。
然后对每一行做一次发送请求
具体代码如下
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
public class HttpPost2926 {
public static void main(String[] args) {
// 发送请求
String url = "http://测试地址?method=siInterface";
// String url = "http://正式地址?method=siInterface";
// 数据所在文件,一行为一条完整的json数据
String filename = "D:/jsonData.txt";
try(InputStream inputStream = new FileInputStream(filename)) {
Scanner sc = new Scanner(inputStream, "utf-8");
while (sc.hasNextLine()) {
String line = sc.nextLine();
System.out.println(line);
String doPostJson = doPostJson(url, line);
System.out.println(doPostJson);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
public static String doPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
Map<String, String> headParams = new HashMap<String, String>();
headParams.put("accept", "text/plain;charset=utf-8");
headParams.put("Content-Type", "application/json;charset=UTF-8");
headParams.put("connection", "Keep-Alive");
headParams.put("msgtype", "json");
headParams.put("token", "ErM9A");
headParams.put("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
entity.setContentEncoding("UTF-8");
packageHeader(headParams, httpPost);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 封装请求头
*
* @param params
* @param httpRequestBase
*/
private static void packageHeader(Map<String, String> params, HttpRequestBase httpRequestBase) {
//封装请求头
if (params != null) {
for (Map.Entry<String, String> entry : params.entrySet()) {
httpRequestBase.setHeader(entry.getKey(), entry.getValue());
}
}
}
}
终于将数据完整的通过接口落到库中,比自己根据业务逻辑重新整理成sql数据容易的多。至此线上问题解决,修改一下数据库中该时间段的上传的数据说明字段信息进行记录一下。后面对该处理数据进行监控,如果没问题就万事大吉,有问题也能再能把这些数据给揪出来。
完结
好啦,上面就是我在遇到该事件的处理步骤,往往说者无意,听者有心。本文未像某些博客一样渲染事情的严重性和解决该问题的成就,这些只是繁杂问题中普通的一个,不知坚持读到此处的你有没有一点帮助呢?解决bug道路千万条,自己拿手第一条。
祝你玩的开心!!