playbook/docs/tsl/syntax_book/07_debug_and_profiler.md

19 KiB
Raw Blame History

07 运行时与性能工具

本章收录网格计算与全局缓存等运行时与性能相关机制。

目录

#网格计算操作符

#网格计算操作符简介

什么是网格计算?

网格计算用于并行执行函数调用,把任务分发到可用计算资源并异步返回句柄。适用于大量独立任务的批量计算;对强顺序依赖的流程不适用。

网格使用格式:

R[i] := #函数名(参数…) with array(系统参数列表…) timeout N;

其中with 语句可指定网格运算子程中的系统参数,此输入可省

timeout N 为指定网格运算子程序运行的超时时间,若运行超过设定时间,则程序报错,可省,默认为一直等待,单位为:毫秒。

网格计算案例

如果用户拥有网格计算的权限,就可以进行网格计算了。

在 TSL 中使用网格计算非常简单,仅仅只需要在网格调用的函数前加上#即可。

例如:

A := array();
B := array('SZ000001', 'SZ000002', 'SH600000');
for i := 0 to length(B) - 1 do
begin
    A[i] := #CalcStock(B[i]);
end;
// 对结果进行访问,用来等待所有网格运行完成并取得结果
r := array();
for j := 0 to length(A) - 1 do r[j] := dupvalue(A[j]); // 复制网格子程序结果
return r;

需要被网格调用的函数 CalcStock

function CalcStock(StockId);
begin
    // ...
end;

网格计算代入系统参数的案例

在有些函数调用的时候,需要设置系统参数,由于网格计算是一个重新开始的计算,并不会主动将系统参数带给新的网格计算函数,这个时候可以使用 with 后缀。

例如:

A[i] := #Close() with array(pn_stock():StockId, pn_date():Today() - 1);

就可以获得指定股票昨日的收盘价。实际使用时不建议用网格计算调用收盘价函数,这里仅作示例。

网格计算设置任务超时时间

网格调用时可通过设置 timeout N 对该子进程进行设置超时时间,若网格运行的程序运行时间超过该设置时间(单位:毫秒),则程序进行报错。

如有网格运行目标程序:

function TestDo();
begin
    sleep(10 * 1000);
    return getsysparam(pn_stock());
end;

在网格中设置超时间为 3 秒,调用如下:

r := #TestDo() timeout 3000;
t := dupvalue(r);
return t;

在网格中通过 with 传入系统参数的同时设置超时间为 3 秒,调用如下:

r := #TestDo() with array(pn_stock():"SZ000002") timeout 3000;
t := dupvalue(r);
return t;

超时报错 Grid timeout,示例如下:

TSL 全局缓存的应用说明

全局缓存管理

TSL 设计的全局缓存是一套极为高效的内存缓存机制,其以 COPYONWRITE 的模式实现了设置和写入完全分离,允许存在多份使用中的版本,使得更新数据和读数据无冲突。

TSL 的全局缓存主要是为了对公共类的数据进行全局优化,尤其是对那些准备效率低下的数据,例如存贮在数据库内的数据,又或者是需要经过大的计算的数据。这些数据的全局缓存化可以使得应用不再关注于数据准备的开销上。

TSL 的全局缓存对于用户而言是一种新的数据类型,但这种数据结构和 TSL 原生数据结构完全相同,是在 TSL 的原生数据类型上扩展而成的,我们的开发使其不仅仅支持 TSL 的标准算符,例如四则运算,同时也支持矩阵计算,还支持子矩阵等算符,不仅仅如此,全局缓存还支持 SELECT绝大多数 TSL 的函数对于全局缓存也是透明的,在计算的使用上,用户完全不需要理会一个数据到底是全局缓存还是其他的类型,除非真的需要(例如缓存是否过期等)。

TSL 的全局缓存对于用户的透明还有一个特性,我们对全局缓存的数据类型进行更改的时候(不是设置),系统会自动将用户使用的全局数据的引用实例化,也就是会将全局数据的相关内容复制到用户的运行环境中,然后进行修改的操作。

TSL 的全局缓存主要应用于数组和矩阵两种类型,也支持其他简单类型,但仅仅对数组和矩阵两种类型采用引用的方式,而其余数据类型取出的时候就进行了实例化。

由于我们对 TSL 全局缓存的透明处理,因为普通函数无法分辨全局缓存还是标准数据类型,除非采用特殊的函数。

全局缓存管理的函数

SetGlobalCache

范例

a := rand(1000, 100);
return SetGlobalCache("LLL", a, now() + 1); // 设置一个名为”LLL”的全局缓存生存周期为1天
GetGlobalCache

范例

mtic;
for i := 0 to 999999 do getglobalCache("LLL", V);
return array(V, mtoc);

这个例子告诉我们,对于全局缓存数据,无论全局缓存数据本身多大,例如这个是 1000*1000 的矩阵,获取 100 万次花费的时间也是微乎其微的。这样我们的数据准备工作所耗费的时间几乎就不存在了。

CheckGlobalCacheExpired

范例

Setglobalcache("VVV", rand(1000, 100));
Getglobalcache("VVV", V);
expired1 := CheckGlobalCacheExpired(V);
setglobalcache("VVV", rand(1000, 100)); // 重置V过期
return array("重置前":expired1, "重置后":CheckGlobalCacheExpired(V));
GetGlobalCacheInfo

范例

if GetGlobalCache("LLL", V) then return GetGlobalCacheInfo(V);
ListGlobalCache

范例

getglobalcache("LLL", v);
return listglobalcache();

Owners 列里是缓存使用中的用户列表

ListGlobalCacheRemoved

范例

范例 1

SetGlobalCache("CCC", array(1, 2, 3));
Getglobalcache("CCC", V); // v指向全局缓存
setglobalcache("CCC", array(1, 2, 3, 4)); // v的版本已经过期
return listglobalcacheremoved();

存在一份 CCC 的过期版本。

范例 2

SetGlobalCache("CCC", array(1, 2, 3));
Getglobalcache("CCC", V); // v指向全局缓存
setglobalcache("CCC", array(1, 2, 3, 4)); // V已经过期
// return listglobalcacheremoved();
V := nil; // V释放了没有过期版本
return listglobalcacheremoved();

返回结果:空数组。

IfCache

全局缓存的使用

全局缓存的基础函数和子矩阵的支持

绝大多数的函数已经可以完全支持全局缓存,当成和原始数据类型对待,而子矩阵这类的操作也毫无问题。

a := array();
for i := 1 to 9 do for j := 1 to 9 do
a[i - 1, j - 1] := i * j;
setglobalcache("99MT", a);
Getglobalcache("99MT", V);
return array(sum(sum(V)), length(V), mcols(V), ifarray(V), V[8, 8], V[0:3, 0:3]);
全局缓存的算符支持

对于四则运算等算符,以及矩阵运算符,还有集合运算符号等等,全局缓存和原始类型一致。

a := array();
for i := 1 to 9 do for j := 1 to 9 do
a[i - 1, j - 1] := i * j;
setglobalcache("99MT", a);
Getglobalcache("99MT", V);
return V + 100;
支持 SELECT

对于 SELECT 而言,全局缓存的表现和其原始数据没有任何差异。

a := array();
for i := 1 to 9 do for j := 1 to 9 do
a[i - 1, j - 1] := i * j;
setglobalcache("99MT", a);
Getglobalcache("99MT", V);
return select * from V order by [0] desc end;
写入实例化

当对全局缓存进行各式写入操作时,无论是数据设置,还是 UPDATE,INSERT 等 SQL 操作,我们均会将全局缓存实例化,在用户使用的时候和传统数据复制后进行写入操作毫无差异,这样最大化地保证了易用性。

a := array();
for i := 1 to 9 do for j := 1 to 9 do
a[i - 1, j - 1] := i * j;
setglobalcache("99MT", a);
Getglobalcache("99MT", V);
b := ifcache(V); // 是缓存 ,b为真
V[0, 0] := 100; // 设置完成后ifcache就为假了
return array(b, ifcache(V), V);

使用 SQL 的 Update 更新全局缓存也引发数据的实例化

a := array();
for i := 1 to 9 do for j := 1 to 9 do
a[i - 1, j - 1] := i * j;
setglobalcache("99MT", a);
Getglobalcache("99MT", V);
b := ifcache(V); // 是缓存 ,b为真
update v set [0] = 1 end; // update后V也不再是globalcache
return array(b, ifcache(V), V);

全局缓存的过期与回收策略

用户一旦使用过期的全局缓存,会导致内存的占用,因而系统必需建立回收过期缓存的机制,否则可能会危害到系统的正常运行的安全。

一旦系统进行过期的内存的回收,会导致使用这些过期的全局缓存的模型被终止,并产生不可恢复的错误,这又会对一些特殊的应用造成,因而内存的回收是必要又是需要谨慎的。

TSL 为全局缓存建立了一套回收规则,结合了系统的剩余内存比例,剩余内存的物理大小,以及占用的过期内存总和大小以及占用的时长等等,基于极为审慎的原则对违反规则的模型进行终止,保障其他正常模型的运行。

一旦遇到这类的问题用户应该检查模型使用这些全局缓存是否存在问题。由于全局缓存的获取效率以及使用效率均极高用户不应该将全局缓存放在系统变量TSL 的 GLOBAL 存贮等等中,用户应尽量直接使用缓存,并审慎长期占用缓存的模式。

配置

配置在 plugin\FileMgr.ini 中

一个典型的设置如下:

[Global Cache]

MemoryLoadLimit=90 //在物理内存使用达到 90%的时候才检查

MemoryAvailLimit=26214400 //在内存剩余大小不到 25G 时才检查,单位 KB

ExpiredSecondsCheck=900 //允许过期后使用的秒数

ExpiredLoadLimit=5 //允许过期的全局缓存占用的物理内存百分比

ExpiredAvailLimit=16777216 //允许过期的全局缓存占用的物理内存大小,单位 KB

MemoryLoadLimit

单位:百分数

描述进行全局缓存回收的物理内存占用比例阈值,不超过该阈值不回收

MemoryAvailLimit

单位KB

描述在内存剩余大小低于的阈值才回收,剩余内存超过不回收。

ExpiredSecondsCheck

单位:秒

描述安全使用的过期后的时长

ExpiredLoadLimit

单位:百分数

描述允许超过安全使用时的全局内存占用的物理内存比例

ExpiredAvailLimit

单位:KB

描述允许超过安全使用时的全局内存占用的物理内存大小。

全局缓存管理的初始化和监控

有瑕疵的全局缓存管理方式

全局缓存的生成,一种模式是在由应用模型内来设置:

例如: if not GetGlobalCache(CacheName,V) then

begin
    V := CalcDataCall();
    SetGlobalCache(Cache, V);
end;

但这样存在几个问题:

一是全局缓存的设置是需要权限的,一旦采用这样的模式,代码只能运行在高权限下。

二是用户模型的性能是不稳定的,当第一次运行的时候会很缓慢,这样有时候会造成不可靠的用户体验。

三是我们很难知道何时适合于进行缓存的准备工作以及缓存的更新工作,因为缓存总有失效的时候,如果要解决失效问题,我们还得将代码变成如下:

if not GetGlobalCache(CacheName, V) or IsDataNeedReCalc(V) then
begin
    V := CalcDataCall();
    SetGlobalCache(Cache, V);
end;

我们需要在 IsDataNeedReCalc 里来检查诸如外部数据的版本是否发生了变更等工作,往往这种检查对事件的耗费远远大于全局缓存的获取,这样又会影响到用户模型的效率。

期望的方式

如果全局缓存的生成和更新,交由系统,那么我们期望的应用开发是如下模式:

if not GetGlobalCache(CacheName, V) then V := CalcDataCall();

由于全局缓存系统管了生成,所以我们只需要取即可,如果系统未生成,我们直接进入计算模式。

不推荐的模式
GetGlobalCache(CacheName, V);

有的开发者会在升级应用后,将取数程序变成了最简单的模式,假设缓存的获得会成功,这样在项目实施中并无不可,假设数据的初始化和数据变更确定性由系统其他部分完成了,这样也带来了数据底层来源和上层应用分离的优势。

但在产品开发中,我们并不推荐如此模式,因为这样会带来了新的问题。

一是采用缓存本身是对旧有模式的升级,一旦采用了这样的方式,缓存就成为了必需而非选项,这样对程序的兼容产生不利影响。

二是可能产生内存依赖,缓存本身是依赖数据在内存里的常驻来达到性能的飞跃,如果一些系统的内存本身就不足够,实施缓存模式本身就不现实。

三是不利于灵活化实施,我们可能会根据内存的大小进行灵活化实施,例如某些数据进入到缓存管理里,某些数据则采用旧有模式,在这种情况下,保留缓存的检查的模式是最佳的选择。

全局缓存的初始化

采用【新开发的初始化 TSL 和监控管理的 TSL】功能将所需的全局缓存的初始化工作交由 InitRun.TSL 来完成。如果开发采用了强依赖模式,那么,我们在初始化中必需保障全部用到的全局缓存的准备工作。

系统将在这个初始化 TSL 运行完成后才会接收应用,无论是 WEB 还是平台。

全局缓存的更新监控

采用【新开发的初始化 TSL 和监控管理的 TSL】功能将所需的全局缓存的更新监控工作交由 AutoMon.TSL 来完成。这个 TSL 会在独立的线程里运行,和用户模型的运行平行独立。而 AutoMon.TSL 的工作是监视数据的变动,当数据发生了变动就立刻重置全局缓存。

如果开发采用了对全局缓存的弱依赖模式,例如初始化全部全局缓存的时间代价太高,而全局缓存的击中概率也不高,那我们也可以将一些本身应由初始化完成的全局缓存准备工作交由监控来完成。这样可以逐步将全局缓存准备出来,而不会因为初始化速度很低影响到用户的使用。

未升级的系统对新代码的使用

由于 TSL 的设计特性,底层系统函数的优先级高于公共和用户函数,我们建议在未升级全局缓存管理功能的用户处,按照规范新增两个函数 SetGlobalCache 以及 GetGlobalCache返回的结果为假即可。

这样,老的 TSL 版本也可以正确地运行使用了新特性的模型。

初始化 TSL 和监控管理的 TSL

某些特殊情况下,平台或者 WEB 都需要一些初始化工作,例如数据初始化或者缓存准备等工作,这时候平台管理者会希望在启动的时候运行一个 TSL 代码。当没有这种支撑的时候,往往管理者会采用调度一个 TSL 代码来执行的模式。

在另外一些情况下,平台或者 WEB 可能需要一些非用户任务,不需要调度而是不断在后台监控运行。例如,用于数据变动检查,进行一些资源回收等等,这些工作有时候被开发者放入了一些用户模型中,这样既不及时,也会影响用户模型的效率,而且还存在一些权限性问题。

为了这些应用的需求,因而我们在天软的平台以及 WEB 模块里设计了初始化 TSL 以及监控 TSL 的功能。

初始化是指平台或者 WEB 在启动的时候先允许执行一个初始化的 TSL。

而监控 TSL则是允许在后台启动数个线程一般只需要 1 个),这个线程可以运行监控的 TSL用于从事缓存更新以及其他所需要的工作。

注:初始化 TSL 只运行一次,而监控的线程不会退出,当 TSL 运行完毕后会重新调用运行。

功能设计

WEB 模块的初始化和监控

Apache 的模块,设置在 TSL.INI 中。

[WebApp]

automonthreads=1 //监控线程的个数

initrun=1 //是否运行初始化 TSL

初始化

初始化 InitRun.TSL 位于进程或者模块所在目录,进程所在目录优先。

当 initrun=1 时候apache 的模块将会在启动后运行 InitRun.TSL运行完成后才接收请求否则返回 406 错误HTTP 406 错误指无法接受 (Not acceptable)错误

监控及管理线程

Automonthreads 设置的是启动的监控管理线程的数量。

这些线程执行 WEB 服务器进程或者模块所在目录的文件(进程目录优先):

文件名的规则为:

第一个线程执行 AutoMon0.TSL第二个线程执行 AutoMon1.TSL依此类推。

如果每个线程没有特殊的 TSL则执行缺省的 AutoMon.TSL

执行 TSL 可以使用 AutoMonIndex 系统参数来获得自己是第几个线程,并可以 AutoFileName 系统参数获得运行的文件名

平台的初始化和监控

初始化是在执行接收分发任务前,设计上初始化无法也不允许调用网格计算等功能。

监控进程目前并未支持调用网格计算。

初始化

初始化的启动是通过参数-i 来设定的,例如 Exec64.exe -i 则表明需要启动初始化。

初始化程序为 InitRun.TSL 位于执行进程所在目录。

当初始化参数指定的时候,执行将在数据同步准备完成后启动 InitRun.TSL运行完成后才接收请求。

监控及管理线程

监控线程的启动是通过参数-M 来设置,例如 Exec64.exe -M 1 则表明采用了一个监控管理线程。

这些线程执行进程所在目录的文件:

文件名的规则为:

第一个线程执行 AutoMon0.TSL第二个线程执行 AutoMon1.TSL一次类推。

如果每个线程没有特殊的 TSL则执行缺省的 AutoMon.TSL

执行 TSL 可以使用 AutoMonIndex 系统参数来获得自己是第几个线程,并可以 AutoFileName 系统参数获得运行的文件名

平台的初始化和监控 TSL 的管理

应用执行服务启动的时候,会尝试同步下载 InitRun.TSL 以及 AutoMon.TSL 和相应的 AutoMon0….TSL 等 TSL 文件。这些文件平台管理员可以按照更新 ini 配置的方式通过事件服务器进行统一管理。

平台的初始化和监控的权限管控
// 如果初始化和监控 TSL 需要调用内外部的 TSL 函数来进行数据准备,设计者推荐用户采用 data:=sudo("modeluser",getcalcdata())的模式来进行数据的一些准备工作,因为 GetCalcData()这类的函数往往不需要任何特殊权限,这样可以最大限度地防止非授权代码的运行。

如果我们仅仅只是利用初始化和监控进行一些系统性操作,设计者强烈建议不需要使用一些中间函数,将除了二进制函数外的实现直接在.TSL 里完成,这样做可以让这些代码可以独立运行。如果无法保障这一点,强烈建议将无需权限运行的内容以 sudo 模式来运行。