2023年3月

Antlr4文件解析流程

image
该图展示了一个语言应用程序中的基本流动过程

  • 输入一个字符流,首先经过词法分析,获取各个Token
  • 然后经过语法分析,组成语法分析树

Antlr4语法书写规范

语法关键字和使用

符号 作用
表达式可选
* 表达式出现0此或多次
+ 表达式至少一次
EOF 语法结尾
expr expr1 expr2 序列模式,由多个表达式或Token组成一个语法规则
expr|expr1|expr2 选择模式,指定一个语法规则可以选择多个匹配模式
expr|expr1|expr* 嵌套模式,自身引用自身

处理优先级、左递归和结合性

Antlr4默认使用自上而下,默认左递归的方式识别语法, 使用下面一个例子说明左递归的方式
expr:expr '*' expr
|expr '+' expr
|INT
;
输入1+2*3;识别的树为
image

这是因为首先定义了乘法语法规则,然后定义了加法语法规则。

更改左递归的方式

expr:<assoc=right> expr '^' expr
|INT
;
指定第一个expr接受
expr的结果作为结合一个语法规则,输入1
2^3,识别的树为
image

Antlr4的基本命令我们就了解到这,有兴趣研究的小伙伴,可以查看《Antlr4权威指南》这本书。

第一个脚本

避坑指南

使用go mode模式引用antlr4默认是1.X版本,这个是低版本antlr,无法识别最新的antlr语法,使用
go get -u github.com/antlr/antlr4/runtime/Go/antlr/v4
获取antlr4最新版本的golang包

语法文件

我这里使用的IDE是goland,这个大家根据自己的爱好自选就行。
新建文件夹:parser
在parser文件夹下新建文件:Calc.g4
输入一下内容:

grammar Calc;
//Token
MUL: '*';
DIV: '/';
ADD:+;
SUB-';
NUMBER' | [1-9] ('_'? [0-9])*);
WS_NLSEMI:[ \r\n\t]+ -> skip;
//Rule
expression:
expression op = (MUL | DIV) expression #MulDiv
| expression op =  (ADD | SUB) expression #AddSub
| NUMBER
start:expression EOF
  • g4文件是Antlr4的文件类型
  • Token代表定制的语法中的关键字,Token都是全部大写
  • Rule是语法规则,由驼峰样式书写,首字母小写,后续每个单词首字母大写
  • EOF代表结尾

脚本命令

我们需要使用Antlr4命令生成Go语言的语法树和Listen方式的go文件

$ java -jar 'C:\Program Files\Java\antlr\antlr-4.12.0-complete.jar' -Dlanguage=Go -no-visitor -package parser *.g4

上述命令就是指定使用antlr4将文件目录下所有的.g4文件生成目标语言为Go的语言文件。

执行CMD命令:
cd .\parser\
执行上述命令,我们会在parser文件夹中看到生成了很多文件:
Calc.interp
Calc.tokens
calc_base_listener.go //监听模式基类的文件
calc_lexer.go //文法文件
calc_listener.go //监听模式的文件(定义多少个语法或者自定义类型就会有多少对Enter、Exit方法)
calc_parser.go //识别语法的文件

验证语法

func main(){
  is := antlr.NewInputStream("1+1*2")`
  lexer := parser.NewCalcLexer(is)
  // Read all tokens
  for {
	t := lexer.NextToken()
	if t.GetTokenType() == antlr.TokenEOF {
		break
	}
	fmt.Printf("%s (%q)\n",lexer.SymbolicNames[t.GetTokenType()], t.GetText())
      }

}

输入内容:

NUMBER ("1")
ADD ("+")
NUMBER ("1")
MUL ("*")
NUMBER ("2")

证明我们的每个Token都被识别

建立一个四则运算法则

type calcListener struct {
	*parser.BaseCalcListener

	stack []int
}

func (l *calcListener) push(i int) {
	l.stack = append(l.stack, i)
}

func (l *calcListener) pop() int {
	if len(l.stack) < 1 {
		panic("stack is empty unable to pop")
	}

	// Get the last value from the stack.
	result := l.stack[len(l.stack)-1]

	// Remove the last element from the stack.
	l.stack = l.stack[:len(l.stack)-1]

	return result
}

func (l *calcListener) ExitMulDiv(c *parser.MulDivContext) {
	right, left := l.pop(), l.pop()

	switch c.GetOp().GetTokenType() {
	case parser.CalcParserMUL:
		l.push(left * right)
	case parser.CalcParserDIV:
		l.push(left / right)
	default:
		panic(fmt.Sprintf("unexpected op: %s", c.GetOp().GetText()))
	}
}

func (l *calcListener) ExitAddSub(c *parser.AddSubContext) {
	right, left := l.pop(), l.pop()

	switch c.GetOp().GetTokenType() {
	case parser.CalcParserADD:
		l.push(left + right)
	case parser.CalcParserSUB:
		l.push(left - right)
	default:
		panic(fmt.Sprintf("unexpected op: %s", c.GetOp().GetText()))
	}
}

func (l *calcListener) ExitNumber(c *parser.NumberContext) {
	i, err := strconv.Atoi(c.GetText())
	if err != nil {
		panic(err.Error())
	}

	l.push(i)
}

func calc(input string) int {
	// Setup the input
	is := antlr.NewInputStream(input)

	// Create the Lexer
	lexer := parser.NewCalcLexer(is)
	stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)

	// Create the Parser
	p := parser.NewCalcParser(stream)

	// Finally parse the expression (by walking the tree)
	var listener calcListener
	antlr.ParseTreeWalkerDefault.Walk(&listener, p.Start())

	return listener.pop()
}

func main(){
	fmt.Println(calc(1+1*2))
}

至此,我们已经使用antlr4+golang开始自己第一个语法文件使用,接下来就是如何实现我们自定的语法了!!!

温故知新----线程之Runnable与Callable接口的本质区别

预备知识:Java中的线程对象是Thread,新建线程也只有通过创建Thread对象的实例来创建。

先说结论

1 Runnable没有返回值的FunctionalInterface(jdk 1.8概念)接口,相反Callable是有返回值的FunctionalInterface接口

2 Runnable + Thread 创建一个无返回结果的任务线程

3 Runnable + Callable +Thread 创建一个有返回结果的任务线程

一:Runnable 示例

​ 创建一个无返回结果的任务线程,so eazy !!!!

public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "我是一个无返回结果的任务线程");
            }
        },"线程一:").start(); //start()开启任务
    }

二:Callable示例

2.1
JAVA异步编程之Callbacks与Futures模型
我是用Executors线程池的方式来创建的。

2.2 用最原始的方法

public class PrimitiveCallable implements Callable, Runnable {
    
    public static void main(String[] args) {
        //实现
        new Thread(new PrimitiveCallable(), "线程二:").start();
    }
    @Override
    public void run() {//Runnable实现
        try {
            Object call = this.call();
            System.out.println(call);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
    @Override
    public Object call() throws Exception { //Callable 实现
        return "异步任务返回的结果!!!";
    }
}

2.3 FutureTask 一种jdk的实现方式

public class FutureTask<V> implements RunnableFuture<V> { //RunnableFuture 实现了以上2个接口
    ........省略.........
	public void run() { //重写run实现
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable; //构建对象是传入的参数
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call(); //调用任务并返回结果
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex); //设置异常时的结果
                }
                if (ran)
                    set(result); //设置结果
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
     ........省略.........
}

小结

1 不要纠结有几种创建线程的方法了,就一种即通过Thread,但Thread有9个构造函数方法

2 9个构造函数方法

1 public Thread()
2 public Thread(Runnable target)
3 Thread(Runnable target, AccessControlContext acc)
4 public Thread(ThreadGroup group, Runnable target)
5 public Thread(String name)
6 public Thread(ThreadGroup group, String name)
7 public Thread(Runnable target, String name)
8 public Thread(ThreadGroup group, Runnable target, String name)
9 public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize)

浏览器读写文件?

有一份老旧而精巧的代码(2006或更早),带js的html,可以只用浏览器来处理一些二进制存档数据。

文件的读写怎么办?通过变动的方法来完成。

利用十六进制编辑软件如WinHEX,直接复制十六进制数值为字符串,贴到一个TextArea以输入;

同样处理过的数据也是生成十六进制字符串,用WinHEX以ASCII Hex的格式粘贴到新文件中。

很巧妙,也有点繁琐。

FileReader:

最近找到了该程序的汉化版,也是好多年前的了,发现里面设计了直接用<input>来加载文件的功能,用的vbs调用Msxml2.XMLHTTP对象来处理。

可是现在的浏览器基本不支持vbs了,就想改一下,用js来完成。

上网搜索了一堆,
XMLHTTP
/
XMLHttpRequest
的代码,有些需要
服务器端支持
,结果都不理想,或浏览器显示无法创建对象之类的。

刚好
new bing
的申请也通过了,一番对话式交流,新搜索引擎初试身手,效果不错,直接给出了示例代码。

只要是支持HTML5的现代浏览器,调用FileReader,那叫一个驾轻就熟!

依葫芦画瓢,花了一点功夫也就改造完成了。

这时又回想起一个问题,旧浏览器(IE10以下,原网银的最爱)怎么办。

如何兼容IE8?

有这个必要吗?

都3202年了,不支持HTML5、不支持ES6的浏览器就该回垃圾堆或封存至博物馆!

网银都终于要求兼容Edge啦!

但无聊也是无聊,找了台老电脑远程其桌面,继续随便搜搜“JS读取本地文本文件 IE8兼容”,结果是有一大堆,可大多都是标题党。

在有些
参考资料
中,提到使用“Microsoft.XMLDOM”对象,试了好久,更适用于文本数据;

FSO对象貌似也只能OpenTextFile或OpenAsTextStream。

以上方法,读取到的是文本数据,都
少了很多
非可打印
/显示字符,当然不行。

还是要用ADODB.Stream,因为它有个方法Read(),可读取二进制数据流。

ADODB.Stream:

碰到两个问题:

  1. 浏览器无法创建对象。安全问题,容易给恶意程序随意读写客户的数据,一般不开放这个功能,
    改改注册表
    ,强行开通吧。
  2. 读不到二进制数据。在IE8中调试程序,一到adodb.Read()后面,产生的东西却不是个object,typeof测试一下,unknown...

正想放弃adodb.stream,又搜到一些资料,既然.Read()不行,严肃地建议我用.ReadText()。

读取
文本
,用来处理
二进制
数据,这也行?

adodb.stream支持对字符集的转换处理,刚好有个神奇的字符集系列:

ISO-8859-1~15

ISO-8859-1
(Latin1/Windows-1252),MySQL人士比较熟悉,软件默认的拉丁西欧字符集,它的特点在于对单字节
完全编码
,1Byte=8bit, 它把所有256个码位全部都排满了。

所以,可用它存储任意二进制数据而不会丢失。

至于编码解码的处理是另外一回事,后续折腾也是由此……

程序
好像
改造成功,一个数据也没少,不管是否为可打印/显示字符,或者对拉丁来说全是合法字符!

然~~而,读取的数据好像有几个地方有
差异
!不多、但是不同!

换成变体标准
ISO-8859-15

差异少了
许多,但还是有差异。

十万个为什么?

差异原因

为什么网上的示例代码对图像、声音文件进行复制都不出错,我加载数据到程序中就有差异?

继续追踪调试,找到了端倪,有几个特定的数据,读取出来就变了。

比如
84
就固定转化为
1E

80

AC
……

只是文件复制的话,持续沿用ISO-8859 Latin字符集,同一个管道,怎么In/Out、Read/Write,数据都不会变。

但是一旦用js将数据读取为数值,在转化为16进制字符串时,问题出现了。

因JavaScript引擎内部,所有字符都用
Unicode
表示。而Latin1字符集中的某些符号,在Unicode中是多字节编码。


ISO-8859-15
中,这样的字符比较少而已,当然,所处码位也不同。

比如拉丁字符中用1字节0x
80
表示了“

”,而在Unicode中编码是0x20
AC
;而“

”则0x
84
变成了0x20
1E
,等等……

刚好对上!

原二进制数据就这样被转换了,编码冲突,怎么还原?

再问
new bing
,多次给出的代码也存在这个问题,继续追问Unicode的编码问题,认错态度非常好,但是没有解决方案。

那只能这样了:

手工转换

懒得写代码,new bing也不理我。

刚好又搜索到一位
国外友人的代码
,问题解决,但是TA用的是437字符集,直接把256个字符的编码做了个对照查询表,转换函数非常之长,我也不想改用437了。

我直接输出了ISO-8859-15字符集所有00~FF用js转换为数值的结果,和真实二进制数据相比,也就存在8处不同的Unicode字符嘛,处理过程中查询修正一下就行了。

搞定!

还是要自己写……其实也不费力。

示例,读取并显示为HEX

<input type="file" id="fileInput" title="Choose a file" onchange="dispHex(this);"/>
<br />
<h1 id='hexh1'>16进制</h1>
<textarea id='display' title='16进制内容显示' style='width:600px;height:400px;'></textarea>
<script language='javascript' type='text/javascript'>
	function dispHex(f){
		var hexString = "";
		//获取显示框
		var disp = document.getElementById("display");
		if (window.FileReader){
			alert("H5");
			var file = f.files[0];
			var reader = new FileReader();
			//添加读取完成事件
			reader.onload = function(e) {
				var buffer = e.target.result;
				var array = new Uint8Array(buffer);
				for (var i = 0; i < array.length; i++) {
					//将每个元素转化为16进制字符串,并补齐两位,用空格分隔
					var s= '0' + array[i].toString(16);
					s = s.substr(s.length - 2, 2);
					hexString += s;
				}
				//将字符串显示在页面上
				disp.textContent = hexString;
			};
			reader.readAsArrayBuffer(file);
		}
		else if (typeof window.ActiveXObject != 'undefined') {
			alert("IE8及以下,请启用ActiveX控件交互!\n如不能打开文件,请将网页下载至本地运行。\n如何开启Adodb.Stream请自行搜索……");
			var file = f.value;	//网页不在本地,则为fakepath
			//f.select();
			//file = document.selection.createRange().text
			hexString = AdodbReadHexFromFile(file);
			disp.value = hexString;
		}
		else alert("Other!");
	}

	//使用ActiveX,要求浏览器开启相应功能和安全设置
	function AdodbReadHexFromFile(fileURL){
		var binStr, hexStr="";
		//ADODB方式,需启用:https://www.cnblogs.com/weiweictgu/archive/2007/03/02/661940.html
		var inStream = new ActiveXObject("ADODB.Stream");
			inStream.Type = 2;	//adTypeBinary = 1,2为Text
			inStream.Open();
			//Latin1(Windows-1252):ISO-8859-1)单字节完全编码
			inStream.CharSet = "iso-8859-15";
			//转换结果iso-8859-15比iso-8859-1更接近原数据?更少收录Unicode中的多字节符号。
			inStream.LoadFromFile(fileURL);
			//Read()为二进制数组,可结果typeof=unknown.WHY.?.所以用ReadText变通...
			binStr = inStream.ReadText();
			inStream.Close();
			inStream = null;

		//binStr2HEX
		//https://www.codeproject.com/articles/17825/reading-and-writing-binary-files-using-jscript
		var ISO885915=[8364,352,353,381,382,338,339,376];	//单字节集中包含的Unicode字符编码
		var HexOrg=['A4','A6','A8','B4','B8','BC','BD','BE'];	//上述字符Latin真实16进制值,由比较而来,437/Latin1同理可得
		var isoCheckNum=ISO885915.length;
		for (var i=0 ; i<binStr.length ; i++) {
			var curCode=binStr.charCodeAt(i);
			//charCodeAt可能会解释成多字节,有字符会被错误转换,如'€',无视CharSet LatinX
			//因JavaScript引擎内部,所有字符都用 Unicode 表示
			var s,isUnicode=false;
			if(curCode>=256){
				var j;		//IE8不支持indexOf()
				for(j=0;j<isoCheckNum;j++) if(curCode==ISO885915[j]) break;
				if(j<isoCheckNum) isUnicode=true;
			}
			if(isUnicode) s=HexOrg[j];	//查表将Unicode字符转换回正确的单字节16进制
			else{
				s= '0' + curCode.toString(16).toUpperCase();
				//确保是两位数的HEX
				var bytes = 2;
				//var bytes=Math.floor(s.length/2)*2;			//多字节内容处理
				s = s.substr(s.length - bytes, bytes);
			}
			hexStr += s;
		}
		return hexStr;
	}
</script>

后记

ISO-8859之外,其它字符集也一般可使用,只要找到冲突编码并转换处理即可;

js生成文件供浏览器下载,现代浏览器用blob加createObjectURL轻松搞定;

在分离FileReader一段代码至子函数时,对异步操作的处理还闹过小问题,Promise的逻辑还真是有些不适合本中老年;

IE8?不想玩了。

奇怪地是,再过了几天,问
new bing
本文类似的问题,它拒绝给出示例代码,说它不会,说它没有记忆,说IE8没法处理这个读取本地二进制文件的问题……

摘要:
本文主要讲解如何在内核保证操作不能中断采取的特殊处理,理论上用户执行的sql使用的内存(dynamic_used_memory) 是不会大范围的超过max_dynamic_memory的内存的

本文分享自华为云社区《
Gaussdb(DWS)内存报错排查方法
》,作者: fighttingman。

Gaussdb内存布局

内存上下文memoryContext内存结构

一、内存问题定位方法

分析场景1:数据库日志出现ERROR:memory is temporarily unavailable

从报错信息可以找到是哪个节点的内存不足,例如 dn_6003_6004: memory is temporarily unavailable , 这个就是dn_6003的内存不足了

1.从日志分析

观察对应dn的日志,是否为“reaching the database memory limitation”,表示为数据库的逻辑内存管理机制保护引起,需要进一步分析数据库的视图;若为“reaching the OS memory limitation”,表示为操作系统内存分配失败引起,需要查看操作系统参数配置及内存硬件情况等。

1)reaching the database memory limitation情况实例

----debug_query_id=76279718689098154, memory allocation failed due to reaching the database memory limitation. Current thread is consuming about 10 MB, allocating 240064bytes.----debug_query_id=76279718689098154, Memory information of whole process in MB:max_dynamic_memory: 18770, dynamic_used_memory: 18770, dynamic_peak_memory: 18770, dynamic_used_shrctx: 1804, dynamic_peak_shrctx: 1826, max_sctpcomm_memory: 4000, sctpcomm_used_memory: 1786, sctpcomm_peak_memory: 1786, comm_global_memctx: 0, gpu_max_dynamic_memory: 0, gpu_dynamic_used_memory: 0, gpu_dynamic_peak_memory: 0, large_storage_memory: 0, process_used_memory: 22105, cstore_used_memory: 1022, shared_used_memory: 2605, other_used_memory: 0, os_totalmem: 257906, os_freemem: 16762.

此时,作业76279718689098154准备申请240064 bytes内存,dynamic_used_memory内存值为18770MB,二者之和大于max_dynamic_memory(18770MB),超出数据库限制,内存申请失败。

在811之后的版本还会打印top3的memoryContext内存占用,示例如下

----debug_query_id=72339069014641088, sessId: 1670914731.140604465997568.coordinator1, sessType: postgres, contextName: ExprContext, level: 5, parent: FunctionScan_140604465997568, totalSize: 950010640, freeSize: 0, usedSize: 950010640
----debug_query_id=72339069014641053, pid=140604465997568, application_name=gsql, query=select * from pv_total_memory_detail, state=retrying, query_start=2022-12-13 14:59:22.059805+08, enqueue=no waiting queue, connection_info={"driver_name":"gsql","driver_version":"(GaussDB 8.2.0 build bc4cec20) compiled at 2022-12-13 14:45:14 commit 3629 last mr 5138 debug","driver_path":"/data3/x00574567/self/gaussdb/mppdb_temp_install/bin/gsql","os_user":"x00574567"}----debug_query_id=72339069014641088, sessId: 1670914731.140604738627328.coordinator1, sessType: postgres, contextName: ExprContext, level: 5, parent: FunctionScan_140604738627328, totalSize: 900010080, freeSize: 0, usedSize: 900010080
----debug_query_id=72339069014641057, pid=140604738627328, application_name=gsql, query=select * from pv_total_memory_detail, state=retrying, query_start=2022-12-13 14:59:22.098775+08, enqueue=no waiting queue, connection_info={"driver_name":"gsql","driver_version":"(GaussDB 8.2.0 build bc4cec20) compiled at 2022-12-13 14:45:14 commit 3629 last mr 5138 debug","driver_path":"/data3/x00574567/self/gaussdb/mppdb_temp_install/bin/gsql","os_user":"x00574567"}----debug_query_id=72339069014641088, sessId: 1670914731.140603779163904.coordinator1, sessType: postgres, contextName: ExprContext, level: 5, parent: FunctionScan_140603779163904, totalSize: 890009968, freeSize: 0, usedSize: 890009968
----debug_query_id=72339069014641058, pid=140603779163904, application_name=gsql, query=select * from pv_total_memory_detail, state=retrying, query_start=2022-12-13 14:59:22.117463+08, enqueue=no waiting queue, connection_info={"driver_name":"gsql","driver_version":"(GaussDB 8.2.0 build bc4cec20) compiled at 2022-12-13 14:45:14 commit 3629 last mr 5138 debug","driver_path":"/data3/x00574567/self/gaussdb/mppdb_temp_install/bin/gsql","os_user":"x00574567"}----allBackendSize=34, idleSize=7, runningSize=7, retryingSize=20

重要字段解释:

sessId:线程启动时间+线程标识(字符串信息为timestamp.threadid)

sessType:线程名称

contextName:memoryContext名字

totalSize:内存占用大小,单位Byte

freeSize:当前memoryContext释放内存总数,单位Byte

usedSize:当前memoryContext已使用的内存总数,单位Byte

application_name:连接到该后端的应用名

query:查询语句

enqueue:排队情况

allBackendSize:总线程个数,idleSize:idle线程个数,runningSize:活跃的线程个数,retryingSize:重试的线程个数

数据库还会在复杂作业中进行检查,查看复杂作业预估内存是否超过实际使用内存,如果存在,则打印下列信息,供分析。

----debug_query_id=76279718689098154, Total estimated Memory is 15196 MB, total current cost Memory is 16454 MB, the difference is 1258 MB.The count of complicated queries is 17 and the count of uncontrolled queries is 1.

上述信息表示全部复杂作业预计使用内存15196 MB,实际使用16454 MB,超出1258 MB。

复杂作业共17个,其中有1个作业实际使用内存超过预计内存。

----debug_query_id=76279718689098154, The abnormal query thread id 140664667547392.It current used memory is 13618 MB and estimated memory is 1102 MB.It also is the query which costs the maximum memory.

上述信息表示,异常线程id为140664667547392,该线程预估消耗内存1102MB,实际消耗13618MB。

----debug_query_id=76279718689098154, It is not the current session and beentry info : datid<16389>, app_name<cn_5001>, query_id<76279718688746485>, tid<140664667547392>, lwtid<173496>, parent_tid<0>, thread_level<0>, query_string<explainperformance with ws as (select d_year AS ws_sold_year, ws_item_sk, ws_bill_customer_sk ws_customer_sk, sum(ws_quantity) ws_qty, sum(ws_wholesale_cost) ws_wc, sum(ws_sales_price) ws_sp from web_sales left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk join date_dim on ws_sold_date_sk = d_date_sk where wr_order_number is null group by d_year, ws_item_sk, ws_bill_customer_sk ), cs as (select d_year AS cs_sold_year, cs_item_sk, cs_bill_customer_sk cs_customer_sk, sum(cs_quantity) cs_qty, sum(cs_wholesale_cost) cs_wc, sum(cs_sales_price) cs_sp from catalog_sales left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk join date_dim on cs_sold_date_sk =d_date_sk where cr_order_number is null group by d_year, cs_item_sk, cs_bill_customer_sk ), ss as (select d_year AS ss_sold_year, ss_item_sk, ss_customer_sk, sum(ss_quantity) ss_qty, sum(ss_wholesale_cost) ss_wc, sum(ss_sales_price) ss_spfrom store_sales left join store_returns on sr_ticket_numbe>.
上述信息进一步显示内存使用超过预估内存的作业信息的sql信息,其中datid表示数据库的OID,app_name表示application name,query_string表示查询sql。
----debug_query_id=76279718689098154, WARNING: the common memory context 'HashContext' is using 1059 MB size larger than 989 MB.----debug_query_id=76279718689098154, WARNING: the common memory context 'VecHashJoin_76279718688746485_6' is using 12359 MB size larger than 10 MB.
上述信息表示超限的memcontext,76279718689098154号查询中,memory context预设值的最大值为989MB,实际使用了1059 MB。

2)reaching the OS memory limitation

当GaussDB内存使用符合GUC中相关参数限制,但操作系统可用内存不足时,会出现与1.1中类似的日志信息,格式如下

----debug_query_id=%lu, FATAL: memory allocation failed due to reaching the OS memory limitation. Current thread is consuming about %d MB, allocating %ld bytes.----debug_query_id=%lu, Please check the sysctl configuration and GUC variable max_process_memory.----debug_query_id=%lu, Memory information of whole process in MB:"                            "max_dynamic_memory: %d, dynamic_used_memory: %d,
dynamic_peak_memory: %d, dynamic_used_shrctx: %d,
dynamic_peak_shrctx:
%d, max_sctpcomm_memory: %d,
sctpcomm_used_memory:
%d, sctpcomm_peak_memory: %d,
comm_global_memctx:
%d, gpu_max_dynamic_memory: %d,
gpu_dynamic_used_memory:
%d,
gpu_dynamic_peak_memory:
%d, large_storage_memory: %d,
process_used_memory:
%d, cstore_used_memory: %d,
shared_used_memory:
%d, other_used_memory: %d,
os_totalmem:
%d, os_freemem: %d

其中,os_totalmem是当前OS中的总内存,即“free”命令中的total信息。os_freemem是当前OS中的可用内存,即“free”命令中的free信息。

第一条日志中“allocating %ld bytes”中的待申请的内存大于第三条日志中“os_freemem”项,且数据库可运行,无其他异常,则符合预期,说明OS内存不足。

2. 实例每个实例的内存使用情况,查询pgxc_total_memory_detail

内存报错后,语句使用的内存就会释放,当时占用内存高的语句可能会因为报错,导致现场没有了,查询内存视图查询不到的情况

with a as (select *from pgxc_total_memory_detail where memorytype='dynamic_used_memory'), b as(select * from pgxc_total_memory_detail wherememorytype='dynamic_peak_memory'), c as (select * from pgxc_total_memory_detailwhere memorytype='max_dynamic_memory'), d as (select * frompgxc_total_memory_detail where memorytype='process_used_memory'), e as (select* from pgxc_total_memory_detail where memorytype='other_used_memory'), f as(select * from pgxc_total_memory_detail where memorytype='max_process_memory')select a.nodename,a.memorymbytes as dynamic_used_memory,b.memorymbytes asdynamic_peak_memory,c.memorymbytes as max_dynamic_memory,d.memorymbytes asprocess_used_memory,e.memorymbytes as other_used_memory,f.memorymbytes asmax_process_memory from a,b,c,d,e,f where a.nodename=b.nodename andb.nodename=c.nodename and c.nodename=d.nodename and d.nodename=e.nodename ande.nodename=f.nodename order by a.nodename;

在查询这个视图也有可能会因为内存不足报memory is temporarily unavailable,导致视图查不出来,此时需要将disable_memory_protect设置为off。

set disable_memory_protect=off; 之后在查询视图就不会报错。

通过上边这视图可以找到集群中哪个节点的内存使用有异常,之后连接那个节点通过pv_session_memory_detail视图找到有问题的memorycontext。

SELECT * FROM pv_session_memory_detail ORDER BY totalsize desc LIMIT 100;

结合pg_stat_activity视图可以找到哪个语句使用的memcontext最多。

select sessid, contextname, level,parent, pg_size_pretty(totalsize) as total ,pg_size_pretty(freesize) as freesize, pg_size_pretty(usedsize) as usedsize, datname,query_id, query from pv_session_memory_detail a , pg_stat_activity b where split_part(a.sessid,'.',2) = b.pid order by totalsize desc limit 100;

紧急恢复

EXECUTE DIRECT ON(cn_5001) 'SELECT pg_terminate_backend(139780156290816)';

二、内存占用高的场景分析

1.空闲连接过多导致内存占用

先确认是哪个实例的内存占用高,确认方法如上查询pgxc_total_memory_detail,之后连上那个cn或者dn查询如下sql

select b.state, sum(totalsize) as totalsize, sum(freesize) as freesize, sum(usedsize) as usedsize from pv_session_memory_detail a , pg_stat_activity b where split_part(a.sessid,'.',2) = b.pid group by b.state order by totalsize desc limit 100;

如果是上图的idle状态的totalsize占用很多内存,可以尝试清理idle状态的空闲连接释放内存

解决措施:清理idle状态的空闲连接

CLEAN CONNECTION TO ALL FORCE FOR DATABASE xxxx;

clean connection 只能清理pg_pooler_status中 in_used是f状态的空闲连接,不能清理in_used状态为t的连接,in_used为t 一般是执行了pbe语句导致cn和dn的空闲连接不能释放导致

如果上边方法清理不掉,只能尝试清理cn和客户端的连接,之后在执行clean connection清理cn和dn之间的连接,可以尝试在cn上找到是idle状态的空闲连接,此操作会断掉cn和客户端的连接,需要和客户确认是否可以执行

select 'execute direct on ('||coorname||')''select pg_terminate_backend('||pid||')'';' from pgxc_stat_activity where usename not in ('Ruby', 'omm') and state='idle';
将select的结果依次执行。

2.语句占用内存过多,如果第一个步骤中的第一个语句查询的是active状态的语句内存占用多,说明是正在执行语句占用的内存多导致的

查询下边的语句找到内存占用多的语句

select b.state as state, a.sessid as sessid, b.query_id as query_id, substr(b.query,1,100) as query, sum(totalsize) as totalsize, sum(freesize) as freesize, sum(usedsize) as usedsize from pv_session_memory_detail a , pg_stat_activity b where split_part(a.sessid,'.',2) = b.pid and usename not in ('Ruby', 'omm') group by state,sessid,query_id,query order by totalsize desc limit 100;
找到语句后,根据query_id去对应的cn上进行查杀这个异常sql

3.dynamic_used_shrctx内存使用较多

dynamic_used_shrctx是共享内存上下文使用的内存,也是通过MemoryContext分的,在线程之间共享。通过pg_shared_memory_detail视图查看

select * from pg_shared_memory_detail order by totalsize desc limit 10;

一般共享内存上下文分配和语句有关的, contextname都会带有线程号或者query_id,根据query_id或者线程号进行查杀异常sql,除此之外共享内存上下文一般是内核中各个模块使用的内存,比如topsql,需要排查内存使用是否合理,以及释放机制。

4. 内存视图pv_total_memory_detail 中,dynamic_used_memory > max_dynamic_memory的情况

1)GUC参数disable_memory_protect为on的时候

2)分配内存的时候,debug_query_id为0

3)内核在执行关键代码段的时候

4)内核Postmaster线程内的内存分配

5)在事务回滚阶段

以上情况都是内核保证操作不能中断采取的特殊处理,理论上用户执行的sql使用的内存(dynamic_used_memory) 是不会大范围的超过max_dynamic_memory的内存的

点击关注,第一时间了解华为云新鲜技术~

一直搞不清传输层和网络层的校验和为什么校验内容不一样,最近问了一些前辈,找寻了一些答案,总结一下自己的思考。

先说一下传输层(TCP)和网络层(IP)的校验和:

  • TCP校验和有伪首部、TCP herder、数据段。而IP的校验和只覆盖IP header,不覆盖IP数据报中的任何数据。TCP校验和、IP校验和的计算方法是一致。
  • TCP的校验和是必需的,而UDP的校验和是可选的。
  • 12字节伪首部:源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP是6)、TCP报文长度(报头+数据)。

先提出三个问题:

  1. 为什么IP算的cksum只有头部呢?TCP却包含数据?
  2. 这个伪首部啥意思,为什么叫'伪'?
  3. 为何TCP算的cksum又加上伪头部呢?

为什么IP算的只有ip header呢?TCP却包含数据?

让我们先回顾一下计算机网络里说到的TCP五层模型,现实中是把物理层和数据链路层放一起了,都叫链路层。

  • 应用层 (application layer)
    :应用进程提供服务
  • 传输层 (transport layer)
    :为进程提供通信服务,为上层提供端到端(最终用户到最终用户)的透明的、可靠的数据传输服务。拥塞控制,流控都在这。
  • 网络层 (internet layer)
    :为主机提供通信服务
  • 数据链路层 (data link layer)
    :为同一链路主机提供数据传输服务
  • 物理层 (physical Layer)
    :确保数据可以在各种物理媒介上进行传输,为数据的传输提供可靠的环境。

首先,其实每一层都有自己的校验方法?为什么?校验算法没法百分百保证正确,越早发现越早丢弃。

我自己的理解:每个层都是有自己负责的内容,按照这个思路去看。数据先加端口号封装传输层,传输层负责的是为进程通信确保了送到主机上正确的port(进程绑定的port)。加ip封装网络层,ip层只是负责送到正确的机器上。加mac封装物理层,控制网卡需要网卡驱动程序,网卡驱动从ip模块获得包后将其复制到网卡的缓存中,然后
在头部加报头和起始帧分界符,末尾加上帧校验序列
(循环冗余算法(CRC)生成的fcs校验序列)。经过网卡将数字信号转化为电信号,通过网线发送出去。这里负责的是真正传输内容。首先链路层这里是简单确保整个报文没有被改变,交换机会解析MAC然后校验这个fcs,这里链路层保证数据可靠专属。路由器收到之后会解析到ip header,计算fcs和ip校验和,也就是路由器里面相当于也有fcs保证数据的正确,尽管很羸弱,ip层的任务就是只确保ip是对的,就像贴在箱子上的快递单做的事情,不关心箱子里的东西对不对,当然路由器会查表封装新的MAC头从计算fcs。真正到了主机,也会校验fcs,ip校验和,到了传输层,需要向上交付数据,所以传输层这里校验和会包含数据段。

TCP/IP地分层,让每一层注重自己的事情,IP不会过问数据,TCP层需要保证数据,但是
为什么链路层有了校验,还需要其它层保证呢?
举个简单的例子,路由器拆了以太帧头之后放到自己的缓冲区,在缓存去里这个时候报文出错了,路由器吧错误的报文计算一下fcs,封装好,继续发,这样之后链路层再校验,也没问题,还是需要更上层的校验。

刚才说的这些校验都是低级的校验,TCP/IP提出的时代,设备能力还是很差的,只能用这种简单的CRC去校验,但比如一个"10 20"变成的"20 10",对于CRC是校验不出来的,因为CRC只管累加没问题。

这个伪首部啥意思,为什么叫'伪'?

12字节伪首部:源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP是6)、TCP报文长度(报头+数据)。

计算checksum时,其实整个包的信息都可以获取,传输层直接从ip header拿出源ip、目的ip、传输协议号用来计算checksum,这些字段并不是在tcp报文里,所以是伪,只是计算时带上而已。

收端也是,收到之后连带这几个字段校验一下,实际上tcp报文并没有这几个字段的内容。

为何TCP算的cksum又加上伪头部呢?

这更算一个
历史遗留问题
,过去还没有光纤,没有现在这些可靠性高的传输技术,之前线路误码率非常高,为了确保数据包到收端的正确性,这又相当于多加了一点保证,并且几乎没有损失性能。

IPV6和IPV4

IPV6省略了IP层的校验和,引入了更加可靠的链路层错误检测和纠正机制。因为当前的设备不再像之前错误那么多,付出校验所有包的代价去转发,并不值得,有其他更好的错误检测,这样像路由器这样的网络设备,就不需要再校验了,提高了性能。

延伸

为什么应用层还要做数据完整性校验?

推荐读一下这个论文:https://zhuanlan.zhihu.com/p/55311553