搜索​​​​

清除过滤器
文章
姚 鑫 · 四月 18, 2021

第三章 优化表(一)

# 第三章 优化表(一) 要确保InterSystems IRIS®Data Platform上的InterSystems SQL表的最高性能,可以执行多种操作。优化可以对针对该表运行的任何查询产生重大影响。本章讨论以下性能优化注意事项: - `ExtentSize`、`Selective`和`BlockCount`用于在用数据填充表之前指定表数据估计;此元数据用于优化未来的查询。 - 运行tune Table来分析填充表中的代表表数据;生成的元数据用于优化未来的查询。 - 优化表计算的值包括扩展大小、选择性、异常值选择性、平均字段大小和块计数 - 导出和重新导入优选表统计数据 # 扩展大小、选择性和块数(ExtentSize, Selectivity, and BlockCount) 当查询优化器决定执行特定SQL查询的最有效方式时,它会考虑以下三种情况: - 查询中使用的每个表的`ExtentSize`行计数。 - S`electivity`为查询使用的每列计算的DISTINCT值的百分比。 - 查询使用的每个SQL映射的块计数。 为了确保查询优化器能够做出正确的决策,正确设置这些值非常重要。 - 在用数据填充表之前,可以在类(表)定义期间显式设置这些统计信息中的任何一个。 - 在用代表性数据填充表之后,可以运行tune Table来计算这些统计数据。 - 运行TuneTable之后,可以通过指定显式值来覆盖计算的统计信息。 可以将显式设置的统计信息与优化表生成的结果进行比较。如果优化表所做的假设导致查询优化器的结果不是最优的,则可以使用显式设置的统计信息,而不是优化表生成的统计信息。 在Studio中,类编辑器窗口显示类源代码。在源代码的底部,它显示了Storage定义,其中包括类`ExtentSize`和每个属性的选择性(如果合适,还包括`OutlierSelectivity`)。 ## ExtentSize 表的`ExtentSize`值就是表中存储的行数(大致)。 **在开发时,可以提供初始`ExtentSize`值。如果未指定`ExtentSize`,则默认值为100,000**。 通常,会提供一个粗略的估计,即在填充数据时该表的大小是多少。 有一个确切的数字并不重要。 此值用于比较扫描不同表的相对成本; 最重要的是确保关联表之间的`ExtentSize`的相对值代表一个准确的比例(也就是说,小表的值应该小,大表的值应该大)。 - `CREATE TABLE`提供了一个`%EXTENTSIZE`参数关键字来指定表中的预期行数,示例如下: ```sql CREATE TABLE Sample.DaysInAYear (%EXTENTSIZE 366, MonthName VARCHAR(24),Day INTEGER, Holiday VARCHAR(24),ZodiacSign VARCHAR(24)) ``` 表的持久类定义可以在存储定义中指定`ExtentSize`参数: ```xml ... 200 ... ``` 在本例中,片段是`MyClass`类的存储定义,它为`ExtentSize`指定了200的值。 如果表有真实的(或真实的)数据,可以使用管理门户中的调优表功能自动计算和设置它的区段大小值; ## Selectivity 在InterSystems SQL表(类)中,每个列(属性)都有一个与之相关联的选择性值。 列的选择性值是在查询该列的典型值时返回的表中的行的百分比。 选择性为`1/D`,其中D是字段不同值的数目,除非检测到异常值。 选择性基于大致相等的不同值的数量。例如,假设一个表包含一个性别列,其值大致均匀分布在`“M”`和`“F”`之间。性别栏的选择值将为50%。更具区分性的特性(例如街道名称`Street Name`)的选择性值通常只有很小的百分比。 所有值都相同的字段的选择性为`100%`。为了确定这一点,优化器首先测试一小部分或几条记录,如果这些记录都具有相同的字段值,它将测试多达`100,000`条随机选择的记录,以支持非索引字段的所有值都相同的假设。如果在对`100,000`条随机选择的记录进行的测试中可能未检测到某个字段的其他值,则应手动设置选择性。 **定义为唯一(所有值都不同)的字段的选择性为1(不应与`1.0000%`的选择性混淆)。例如,`RowID`的选择性为1。** 在开发时,可以通过在存储定义中定义一个选择性参数来提供此值,该参数是表的类定义的一部分: ```xml ... 50% ... ``` 若要查看类的存储定义,请在Studio中,从“视图”菜单中选择“查看存储”;Studio在类的源代码底部包含存储。 ![image](/sites/default/files/inline/images/1_37.png) 通常,需要提供在应用程序中使用时预期的选择性的估计值。与`ExtentSize`一样,拥有确切的数字并不重要。InterSystems IRIS提供的许多数据类型类将为选择性提供合理的默认值。 还可以使用`SetFieldSelectivity()`方法设置特定字段(属性)的选择值。 如果表中有真实的(或真实的)数据,则可以使用管理门户中的Tune table工具自动计算和设置其选择性值。 调优表确定一个字段是否有一个离群值,这个值比任何其他值都常见得多。 如果是这样,Tune Table将计算一个单独的离群值选择性百分比,并根据这个离群值的存在来计算选择性。 异常值的存在可能会极大地改变选择性值。 选择性用于查询优化。 在`SELECT`查询中指定的字段和在视图的`SELECT`子句中指定的字段使用相同的选择性值。 请注意,视图的行分布可能与源表不同。 这可能会影响视场选择性的精度。 ## BlockCount 当编译一个持久化类时,类编译器会根据区段大小和属性定义计算每个SQL映射使用的映射块的大致数量。 可以在调优表工具的Map `BlockCount`选项卡中查看这些`BlockCount`值。 块计数在调优表中由类编译器估计。 注意,如果更改了区段大小,则必须关闭并重新打开SQL Tune Table窗口,以查看该更改反映在`BlockCount`值中。 当运行Tune Table时,它会测量每个SQL映射的实际块计数。 除非另有指定,调优表测量值将替换类编译器的近近值。 这些调优表测量值在类定义中表示为负整数,以区别于指定的`BlockCount`值。 如下面的例子所示: ```xml -4 ``` 调优表测量值在调优表中表示为正整数,标识为由调优表测量。 可以在类定义中定义显式的块计数值。 可以显式地指定块计数为正整数,如下面的示例所示: ```xml 12 ``` 当定义一个类时,可以省略为`map`定义`BlockCount`,显式地指定一个`BlockCount`为正整数,或显式地定义`BlockCount`为`NULL`。 - 如果不指定块计数,或指定块计数为0,则类编译器估计块计数。 运行Tune Table将替换类编译器的估计值。 - 如果指定一个显式的正整数`BlockCount`,运行Tune Table不会替换此显式的`BlockCount`值。 在调优表中,显式的类定义块计数值表示为正整数,标识为在类定义中定义的。 这些块计数值不会通过随后运行Tune Table而更改。 - 如果将显式`BlockCount`指定为`NULL`,则SQL Map将使用类编译器估计的`BlockCount`值。因为`BlockCount`在类定义中是“定义的”,所以运行Tune Table不会替换这个估计的`BlockCount`值。 所有InterSystems SQL映射块的大小为2048字节(2K字节)。 在以下情况下,优化表不测量块计数: - 如果表是由数组或列表集合投影的子表。这些类型的子表的`BlockCount`值与父表数据映射的`BlockCount`值相同。 - 如果全局映射是远程全局(不同名称空间中的全局)。取而代之的是使用在类编译期间使用的估计的`BlockCount`。 # Tune Table Tune Table是一个实用程序,它检查表中的数据,并返回关于区段大小(表中的行数)、每个字段中不同值的相对分布以及平均字段大小(每个字段中值的平均长度)的统计信息。 它还为每个SQL映射生成块计数。 可以指定该调优表,使用此信息更新与表及其每个字段相关联的元数据。 查询优化器随后可以使用这些统计信息来确定最有效的查询执行计划。 在外部表上使用Tune Table将只计算区段大小。 调优表无法计算外部表的字段选择性值、平均字段大小或映射块计数值。 ## 何时运行调优表 **应该在每个表填充了具有代表性的实际数据之后,在该表上运行tune Table。通常,在数据“激活”之前,只需要运行一次tune Table,这是应用程序开发的最后一步。Tune Table不是维护实用程序;它不应对实时数据定期运行。** **注:在极少数情况下,运行调优表会降低SQL性能。虽然TuneTable可以在实时数据上运行,但建议在具有实际数据的测试系统上运行TuneTable,而不是在生产系统上运行。可以使用可选的系统模式配置参数来指示当前系统是测试系统还是活动系统。设置后,系统模式将显示在管理门户页面的顶部,并可由`$SYSTEM.Version.SystemMode()`方法返回。** 通常,在添加、修改或删除表数据时不应重新运行Tune Table,除非当前数据的特征发生了数量级的更改,如下所示: - 相对表大小:Tune Table假设它正在分析具有代表性的数据子集。如果该子集是代表性子集,则该子集只能是整个数据集的一小部分。如果联接或其他关系中涉及的表的`ExtentSize`保持大致相同的相对大小,则当表中的行数发生变化时,Tune Table结果仍然是相关的。如果连接表之间的比率更改了一个数量级,则需要更新`ExtentSize`。这对于`JOIN`语句很重要,因为SQL优化器在优化表连接顺序时使用`ExtentSize`。一般来说,无论查询中指定的联接顺序如何,都会先联接较小的表,然后再联接较大的表。因此,如果`tableA`和`tableB`中的行比从`1000:2000`更改为`10000:2000`,可能在一个或多个表上重新运行tune Table,但如果更改为`2100:4000`,则不需要重新运行tune Table。 - 均匀值分布:优化表假设每个数据值的可能性都是相等的。如果它检测到离群值,它会假定除离群值之外的每个数据值的可能性都是相等的。调谐表通过分析每个字段的当前数据值来建立选择性。真实数据的可能性相等始终是一个粗略的近似值;不同数据值的数量及其相对分布的正态变化不应保证重新运行调优表。但是,字段可能值的数量(不同值与记录的比率)的数量级变化或单个字段值的总体可能性可能会导致不准确的选择性。大幅更改具有单个字段值的记录的百分比可能会导致TuneTable指定一个离群值或删除指定的离群值,从而显著改变计算的选择性。如果字段的选择性不再反映数据值的实际分布,则应重新运行调优表。 - 重大升级或新的站点安装可能需要重新运行Tune Table。 ## 运行 Tune Table 运行调优表有三个接口: - 使用Management Portal SQL interface Actions下拉列表,它允许在单个表或多个表上运行Tune Table。 - 为单个表或当前命名空间中的所有表调用`$SYSTEM.SQL.Stats.Table.GatherTableStats()`方法。 - 对单个表发出SQL命令调优表。 Tune Table清除引用正在调优的表的缓存查询。 调优表命令提供了一个recompile缓存查询选项,以使用新的调优表计算值重新生成缓存的查询。 如果表映射到只读数据库,则无法执行调优表,并生成错误消息。 在运行了调优表工具之后,生成的区段大小和选择性值将保存在类的存储定义中。 要查看存储定义,在Studio中,从“视图”菜单中选择“视图存储”; Studio在类源代码的底部包含存储。 ### 从管理门户调优表 要从管理门户运行Tune Table: 1. 选择System Explorer,然后选择SQL。 通过单击页面顶部的Switch选项选择一个名称空间,然后从显示的列表中选择一个名称空间。 (可以为每个用户设置管理门户的默认名称空间。) 2. 从屏幕左侧的下拉列表中选择模式,或者使用筛选器。 3. 执行下列操作之一: - 优化单个表:展开表类别,然后从列表中选择一个表。选择表格后,单击操作下拉列表,然后选择调整表格信息。这将显示表的当前`ExtentSize`和选择性信息。如果从未运行过调谐表,`ExtentSize=100000`,则不会显示任何选择性、异常值选择性、异常值或平均字段大小信息(除了选择性为1的行ID),并且会按照类编译器的估计列出映射块计数信息。 从选择性选项卡中,选择调谐表按钮。这将在表上运行tune Table,根据表中的数据计算ExtentSize、选择性、异常值选择性、异常值和Average Field Size值。Map BlockCount(地图块计数)信息按Tune Table(调谐表)测量列出。 单个表上的Tune Table始终作为后台进程运行,并在完成后刷新该表。这可以防止超时问题。当此后台进程正在运行时,将显示一条正在进行的消息。在后台进程执行时,关闭按钮可用于关闭调谐表窗口。 - 优化方案中的所有表:单击操作下拉列表,然后选择优化方案中的所有表。这将显示调谐表方框。选择Finish按钮在方案中的所有表上运行Tune Table。调谐表完成后,此框显示完成按钮。选择Done(完成)退出Tune Table(调谐表)框。 SQL优化表窗口有两个选项卡:选择性和映射块计数。这些选项卡显示由调谐表生成的当前值。它们还允许手动设置与Tune Table生成的值不同的值。 选择性选项卡包含以下字段: - 当前表扩展大小。此字段有一个编辑按钮,允许输入不同的表格扩展大小。 - “使类保持最新”复选框。对Tune Table生成的统计数据的任何更改,或由Tune Table界面或Tune Table方法中的用户输入值生成的任何更改,都会立即表示在类定义中: - 如果未选中此框(否),则不会设置修改后的类别定义上的最新标志。这表明类定义已过期,应该重新编译。这是默认设置。 - 如果选中此框(是),类定义将保持标记为最新。在活动系统上更改统计信息时,这是首选选项,因为它降低了重新编译表类定义的可能性。 - 字段表,其中包含字段名称、选择性、备注、异常值选择性、异常值和平均字段大小等列。通过单击`Fields`表格标题,可以按该列的值进行排序。通过单击`Fields`表行,您可以手动设置该字段的选择性、异常值选择性、异常值和平均字段大小的值。 Map BlockCount选项卡包含以下字段: - 包含SQL Map Name、BlockCount和Source of BlockCount列的映射名称表。索引的SQL映射名称是SQL索引名;这可能不同于持久类索引属性名。 - 通过单击单个map名称,可以手动设置该地图名称的`BlockCount`值。 在选择性选项卡中,可以单击优化表按钮在此表上运行优化表。 ### 使用方法调整表 可以使用`$SYSTEM.SQL.Stats.Table.GatherTableStats()`方法在当前名称空间中运行Tune Table工具。 - `GatherTableStats(“Sample.MyTable”)`在单个表上运行TuneTable。 - `GatherSchemaStats(“Sample”)`在指定模式中的所有表上运行tune Table。 - `GatherTableStats(“*”)`在当前命名空间中的所有表上运行TuneTable。 使用`GatherTableStats()`方法时,可能会生成以下错误消息: - 不存在的表: ```java DO $SYSTEM.SQL.Stats.Table.GatherTableStats("NoSuchTable") ``` ```java No such table 'SQLUser.NoSuchTable' ``` - `View`视图: ```java DO $SYSTEM.SQL.Stats.Table.GatherTableStats("Sample.MyView") ``` ```java 'Sample.MyView' is a view, not a table. No tuning will be performed. ``` 当运行`GatherTableStats(“*”)`或`GatherSchemaStats(“SchemaName”)`时,如果系统支持并行处理,系统将使用多个进程并行调优多个表。 ## 在分片表上运行Tune table 如果在一个分片表上运行调优表,那么调优表操作将被转发到每个碎片,并针对该表的那个碎片运行。 调优表不会在调用它的主名称空间中执行。 如果在导出到碎片的类定义的非分片表上运行调优表,因为该表已连接到一个分片表,调优表操作将转发到每个碎片,并且它也在主名称空间中执行。 在分片表上运行Tune Table时,应该遵循以下准则: - 优化分片主表,而不是分片本地表。 - 区段大小和块计数值是每个分片的值,而不是所有分片的总和。 - 如果使用`$SYSTEM.SQL.Stats.Table.Export()`和`$SYSTEM.SQL.Stats.Table.Import()`,则导出/导入分片主表的调优统计,而不是分片本地表。 - 调优切分表将在切分主类和切分本地类/表定义中定义调优统计。 如果手动编辑类定义中的调优表元数据,建议的过程是修改碎片主类的定义,然后重新编译碎片主类。 在编译碎片主类时,碎片主调优统计信息将被复制到类的碎片本地版本。 如果`GatherTableStats()`或`GatherSchemaStats()`指定了一个`logFile`参数,shard master实例中的日志文件有一个针对指定表的条目,例如: - Sharded table: `TABLE: Invoking TuneTable on shards for sharded table ` - Non-sharded table: `TABLE: Invoking TuneTable on shards for mapped non-sharded table ` 在每个分片实例上,在`mgr/`目录中创建一个同名的日志文件,记录这个分片上这个表的调优表信息。 如果为日志文件指定了目录路径,那么分片将忽略该路径,并且该文件始终存储在`mgr/`中。
文章
Nicky Zhu · 一月 8, 2021

DeepSee:数据库、命名空间和映射 - 第1部分,共 5 部分

我打算基于实例中的数据实现业务智能。 怎样才是设置数据库和环境来使用 DeepSee 的最佳方法呢?   ![](/sites/default/files/inline/images/graphic-3a-final.png) 本教程通过 3 个 DeepSee 架构示例来解决此问题。 首先,我们从基本架构模型开始,并重点说明其局限性。 对于复杂程度中等的业务智能应用,建议使用下一个模型,对于大多数用例而言,该模型应该足矣。 在本教程的最后,我们将说明如何增强架构的灵活性以管理高级实现。 本教程中的每个示例都介绍了新的数据库和全局映射,并讨论了为何以及何时设置它们。 在构建架构时,则重点说明更灵活的示例提供的好处。 开始前 主服务器和分析服务器 为了使数据高度可用,InterSystems 通常建议使用镜像或映射,并将 DeepSee 实现基于镜像/映射服务器。 承载数据原始副本的机器称为“主服务器”,而承载数据副本和业务智能应用程序的计算机通常称为“分析服务器”(有时称为“报告服务器”)。 拥有主服务器和分析服务器至关重要,主要原因是避免任一台服务器出现性能问题。 请查阅有关推荐架构的文档。 数据和应用程序代码 通常,将源数据和代码存储在同一数据库中仅对小型应用程序有效。 对于更大型的应用程序,建议将源数据和代码存储在两个专用数据库中,这样您就可以与运行 DeepSee 的所有命名空间共享代码,同时保持数据分离。 源数据的数据库应从生产服务器镜像。 该数据库可以为只读,也可为读写。 建议为此数据库持续启用日志功能。 源类和自定义应用程序应存储在生产和分析服务器上的专用数据库中。 请注意,这两个用于源代码的数据库不需要同步,甚至不需要运行相同的 Caché 版本。 只要定期将代码备份到其他地方,通常就不需要日志。 在本教程中,我们采用以下配置。 分析服务器上的 APP 命名空间有 APP-DATA 和 APP-CODE 作为默认数据库。 APP-DATA 数据库可以访问主服务器上的源数据数据库中的数据(源表类及其事实数据)。 APP-CODE 数据库存储 Caché 代码(.cls 和 .INT 文件)以及其他自定义代码。 数据和代码的这种分离是一种典型的架构,这允许用户,例如,有效地部署 DeepSee 代码和自定义应用程序。   在不同的命名空间上运行 DeepSee 使用 DeepSee 的业务智能实现通常在不同的命名空间中运行。 在本文中,我们将说明如何设置单个的 APP 命名空间,但是相同的过程适用于运行业务智能应用程序的所有名称空间。 文档 建议熟悉文档页面执行初始设置。 该页面的内容包括:设置 Web 应用程序,如何将 DeepSee 全局变量放置在单独的数据库中,以及 Deepeep 全局变量的替代映射列表。   * * * 在本系列的第二部分中,我们将阐述基本架构模型的实现
文章
姚 鑫 · 七月 23, 2021

类关键字ClientDataType,ClientName,CompileAfter,DdlAllowed

# <center> 第十三章 类关键字 - ClientDataType 指定将此数据类型投影到客户端技术时使用的客户端数据类型。仅适用于数据类型类。 # 用法 要指定将此数据类型投影到客户端技术时要使用的客户端数据类型,请使用以下语法:```javaClass MyApp.MyString [ ClientDataType = clienttype ] { //class members }``` 其中clienttype是下列之一: - `BIGINT`- `BINARY`- `BINARYSTREAM`- `BOOLEAN`- `CHARACTERSTREAM`- `CURRENCY`- `DATE`- `DECIMAL`- `DOUBLE`- `FDATE`- `FTIMESTAMP`- `HANDLE`- `INTEGER`- `LIST`- `LONGVARCHAR`- `NUMERIC`- `STATUS`- `TIME`- `TIMESTAMP`- `VARCHAR` (默认) # 详解 此关键字指定将此类投影到客户端技术时使用的客户端数据类型。每个数据类型类都必须指定一个客户端数据类型。 # 对子类的影响 这个关键字是从主超类继承的。子类可以覆盖关键字的值。 # 默认 默认的客户端数据类型是`VARCHAR`。 # <center> 第十四章 类关键字 - ClientName 能够重写此类的客户端投影中使用的默认类名。 # 用法 要在将类投影到客户端时覆盖类的默认名称,请使用以下语法: ```javaClass MyApp.MyClass [ ClientName = clientclassname ] { //class members }``` 其中clientclassname是用作客户端名称的不带引号的字符串,而不是类名。 # 详解 该关键字允许在类被投影到客户端时为其定义一个替代名称(例如当使用InterSystems IRIS Java绑定时) # 对子类的影响 此关键字不是继承的。 # 默认 如果省略此关键字,实际的类名将在客户端上使用。 # <center> 第十五章 类关键字 - CompileAfter 指定此类应在其他(指定的)类之后编译。 # 用法 要指示类编译器应该在其他类之后编译此类,请使用以下语法: ```javaClass MyApp.MyClass [ CompileAfter = classlist ] { //class members }``` 其中`classlist`是下列之一: - 类名。例如:- ```java[ CompileAfter = MyApp.Class1 ]```- 用逗号分隔的类名列表,用括号括起来。例如: ```java[ CompileAfter = (MyApp.Class1,MyApp.Class2,MyApp.Class3) ]``` # 详解 此关键字指定类编译器应该在编译指定的类后编译此类。 通常,当类之间存在编译器无法检测到的依赖关系,以致必须一个接一个地编译时,会使用此关键字。 此关键字仅影响编译顺序,不影响运行时行为。 **注意:`CompileAfter`关键字不能确保在编译这个类之前指定的类是可运行的。** 此外,`CompileAfter`关键字只影响与`System`关键字具有公共值的类。 # 对子类的影响 这个关键字继承自所有超类。如果子类为关键字指定了一个值,该值指定了在子类可以被编译之前必须被编译的附加类。 # 默认 默认情况下,不指定该关键字。 # <center> 第十六章 类关键字 - DdlAllowed 指定`DDL`语句是否可用于更改或删除类定义。仅适用于持久类。 # 用法 要通过`DDL`修改类,请使用以下语法: ```Class MyApp.Person Extends %Persistent [ DdlAllowed ] { //class members }```否则,省略此关键字或使用以下语法: ```Class MyApp.Person Extends %Persistent [ Not DdlAllowed ] { //class members }``` # 详情 此关键字指定是否可以使用`DDL`语句(如删除表、更改表、删除索引等)来更改或删除类定义。 通常,不希望让SQL用户使用`DDL`语句修改类。 # 对子类的影响 此关键字不是继承的。 # 默认 如果省略这个关键字,`DDL`语句就不能用来影响类定义。 # 注意 如果通过执行`DDL CREATE TABLE`语句来创建一个类,那么对于该类,`DdlAllowed`关键字最初将被设置为`true`。
文章
Michael Lei · 八月 20, 2021

IRIS ObjectScript 原生API Demo

这是一个IRIS 2020.2上的代码示例,并非InterSystems 官方支持! 本demo基于原始类描述 is based on the raw class descriptions.使用的数据类是Address, Person, Employee, Company如果要做更有吸引力的 demo, 可以添加 JSONtoString by ID的方法 用ZPM安装后从终端启动:After installation with ZPM just run from Terminal USER>do ##class(rcc.ONAPI.demo).Run() Adjust Parameters host[127.0.0.1]: port[51773]: namespace[USER]: user[_SYSTEM]: pwd[SYS]: timeout[5]: ****** connected ******** 下一步, 你会得到一系列可能的Demo动作。you get a list of possible demo actions.没有输入就意味着没有动作No input means no action.菜单会一直循环直到退出The menu loops until you exit. Populate Person by:100 100 Populate Company by:10 10 Populate Employee by:50 50 Show Person by ID:3 {"Name":"Rogers,Norbert V.","SSN":"990-11-9806","DOB":"1962-04-23","Home":{"Street":"867 Oak Street","City":"Denver","State":"NH","Zip":"64647"},"Office":{"Street":"3309 Oak Court","City":"Denver","State":"NY","Zip":"76436"},"FavoriteColors":["Green","Green"],"Age":58} Show Company by ID:3 {"Name":"CompuComp Corp.","Mission":"Specializing in the development and manufacturing of open-source object-oriented models for additive manufacturing.","TaxID":"Y8155","Revenue":819493934,"Employees":[{"Name":"Ahmed,Sophia P.","SSN":"936-73-5161","DOB":"1933-08-08","Home":{"Street":"8758 Elm Street","City":"Fargo","State":"OH","Zip":"40652"},"Office":{"Street":"1578 Maple Street","City":"Larchmont","State":"IA","Zip":"89021"},"Spouse":{"Name":"Olsen,William A.","SSN":"912-52-4809","DOB":"2010-01-26","Home":{"Street":"2933 Main Street","City":"Bensonhurst","State":"WA","Zip":"51960"},"Office":{"Street":"4994 Ash Street","City":"Gansevoort","State":"OR","Zip":"89750"},"Spouse":{"Name":"Adam,Brian D.","SSN":"799-82-3083","DOB":"2005-11-04","Home":{"Street":"1264 Oak Avenue","City":"Chicago","State":"WV","Zip":"34943"},"Office":{"Street":"7443 Second Avenue","City":"Zanesville","State":"CO","Zip":"30478"},"Spouse":{"Name":"Cooke,Diane C.","SSN":"754-49-5729","DOB":"1984-01-12","Home":{"Street":"9624 Maple Place","City":"Albany","State":"MS","Zip":"60948"},"Office":{"Street":"4711 Second Place","City":"Youngstown","State":"VA","Zip":"31250"},"Age":36},"FavoriteColors":["Orange"],"Age":14},"FavoriteColors":["Green","Green"],"Age":10,"Title":"Senior Systems Engineer","Salary":61511},"Age":87,"Title":"Product Manager","Salary":59127},{"Name":"Ng,Elmo S.","SSN":"201-15-3259","DOB":"1954-10-14","Home":{"Street":"2437 Main Avenue","City":"Xavier","State":"KY","Zip":"10156"},"Office":{"Street":"2770 Oak Drive","City":"Tampa","State":"OH","Zip":"18459"},"Spouse":{"Name":"Eastman,Linda M.","SSN":"110-41-1818","DOB":"1980-03-19","Home":{"Street":"5309 Ash Drive","City":"Xavier","State":"RI","Zip":"36964"},"Office":{"Street":"4288 Washington Place","City":"Xavier","State":"HI","Zip":"41889"},"Spouse":{"Name":"Huff,Dave C.","SSN":"559-32-3838","DOB":"1973-05-05","Home":{"Street":"6216 First Avenue","City":"Tampa","State":"WA","Zip":"68628"},"Office":{"Street":"2896 Clinton Drive","City":"Elmhurst","State":"UT","Zip":"97796"},"FavoriteColors":["Purple"],"Age":47},"FavoriteColors":["Purple","Blue"],"Age":40},"FavoriteColors":["Red","Purple"],"Age":65,"Title":"Laboratory Marketing Manager","Salary":35888}]} Show Employee by ID:103 {"Name":"Faust,Buzz H.","SSN":"979-41-6347","DOB":"1938-02-07","Home":{"Street":"6231 Madison Avenue","City":"Gansevoort","State":"TX","Zip":"49085"},"Office":{"Street":"6402 Main Street","City":"Elmhurst","State":"RI","Zip":"82976"},"Spouse":{"Name":"Joyce,Chad C.","SSN":"199-86-8085","DOB":"1974-11-17","Home":{"Street":"6229 Main Street","City":"Reston","State":"FL","Zip":"16922"},"Office":{"Street":"8509 Elm Blvd","City":"Bensonhurst","State":"HI","Zip":"90665"},"Spouse":{"Name":"Cooke,Diane C.","SSN":"754-49-5729","DOB":"1984-01-12","Home":{"Street":"9624 Maple Place","City":"Albany","State":"MS","Zip":"60948"},"Office":{"Street":"4711 Second Place","City":"Youngstown","State":"VA","Zip":"31250"},"Age":36},"FavoriteColors":["Purple"],"Age":45},"Age":82,"Title":"Global Administrator","Salary":13813} Show Global PersonD by ID:4 $Data()=1 Value=$lb("","Eastman,Mary C.","887-18-3730",44711,$lb("3889 Ash Blvd","Washington","TX",67862),$lb("5709 Oak Blvd","Chicago","IL",30845),"","") Index list for Person & Employee (n,y):y $Employee $Person NameIDX SSNKey ZipCode Exit Demo (n,y,*): Populate Person by: Populate Company by: Populate Employee by: Show Person by ID: Show Company by ID: Show Employee by ID:104 {"Name":"Novello,Emily I.","SSN":"411-35-4234","DOB":"1943-01-07","Home":{"Street":"3353 Washington Court","City":"Hialeah","State":"MT","Zip":"22403"},"Office":{"Street":"9743 Clinton Blvd","City":"Xavier","State":"OH","Zip":"89038"},"Spouse":{"Name":"Goldman,Usha T.","SSN":"465-59-4053","DOB":"1987-07-16","Home":{"Street":"2578 Second Blvd","City":"Gansevoort","State":"FL","Zip":"77552"},"Office":{"Street":"6986 Main Street","City":"Elmhurst","State":"VT","Zip":"48713"},"Spouse":{"Name":"Rogers,Norbert V.","SSN":"990-11-9806","DOB":"1962-04-23","Home":{"Street":"867 Oak Street","City":"Denver","State":"NH","Zip":"64647"},"Office":{"Street":"3309 Oak Court","City":"Denver","State":"NY","Zip":"76436"},"FavoriteColors":["Green","Green"],"Age":58},"Age":33},"FavoriteColors":["Green","White"],"Age":77,"Title":"Senior Product Specialist","Salary":96469} Show Global PersonD by ID: Index list for Person & Employee (n,y): Exit Demo (n,y,*):y ****** done ******** USER>
文章
Johnny Wang · 四月 25, 2022

Angular 中 Ensemble 工作流的 UI

大家应该都已经很熟悉 InterSystems Ensemble(一个集成和应用程序开发平台),每个人都知道 Ensemble Workflow 子系统是什么以及它对于自动化人类交互的作用。 对于那些不了解 Ensemble Workflow 的人,我将简要介绍它的功能(已经熟悉的朋友可以直接跳过这一部分并学习如何使用 Angular.js 中的 Workflow 接口)。 InterSystems Ensemble InterSystems Ensemble 是一个集成和应用程序开发平台,旨在集成异构系统、自动化业务流程和创建新的复杂应用程序,这些应用程序通过新的业务逻辑或新的用户界面增强集成应用程序的功能:EAI、SOA、BPM、BAM 甚至 BI (感谢 InterSystems DeepSee:一种用于开发分析应用程序的内置技术)。 Ensemble 具有以下关键功能: 适配器:与应用程序、技术和数据源交互的组件。 Ensemble 提供技术和应用程序集成适配器(Web 和 REST 服务、文件、FTP、电子邮件、SQL、EDI、HL7、SAP、Siebel、1S Enterprise 等)。 您可以使用适配器 SDK 创建自己的适配器。 业务服务:将来自外部系统的数据转换为 Ensemble 消息并启动业务流程和/或业务运营的组件。 业务流程:用于编排服务和操作的可执行流程,以自动化系统和/或人员之间的交互(通过工作流子系统)。 流程要么用声明性业务流程语言描述,要么用 Caché 对象脚本实现。 通过服务和操作将与外界交互的逻辑与这种交互的具体实现分开。 业务运营:负责向外部系统发送/接收消息并将 Ensemble 消息转换为与此类系统兼容的格式的组件。 消息转换:使用声明性数据转换语言将消息从一种格式转换为另一种格式的集成组件。 业务规则:允许集成解决方案的管理员在特定决策点更改 Ensemble 业务流程的行为,而无需编写代码。 工作流管理:Ensemble Workflow 子系统提供任务分配的自动化。 业务指标:允许您收集和计算 KPI。 结合仪表板,它们用于实施业务活动监控 (BAM) 解决方案。 OK,让我们回到工作流管理,仔细看看 Ensemble Workflow 子系统的功能。 工作流管理和 Ensemble 工作流子系统 根据工作流管理联盟 (www.WfMC.org) 的定义,“工作流”是完全或部分自动化的业务流程,其中文档、信息或任务根据既定规则和程序从一个参与者传递给另一个参与者。” 工作流程的关键方面: 工作流的目的是涵盖工作的“片段” 工作流是一组程序性任务执行规则 工作流用户是在工作流管理系统中处理任务的人 工作流中的角色是一组从事特定类型任务的用户。 Ensemble 中的工作流管理子系统使您能够执行以下操作: 使用 Ensemble 业务流程自动化工作流程管理 灵活配置任务分配流程 通过 Ensemble 提供的特殊工作流门户使用工作流管理系统 组织工作流管理子系统与 Ensemble 的集成业务流程的交互 使用业务活动监控子系统,Ensemble 的管理和监控工具 轻松配置和扩展工作流子系统的功能 工作流管理自动化的最简单示例是 Ensemble HelpDesk 应用程序(下图为HelpDesk 业务流程算法的片段),它可以自动化支持人员的交互,并且是标准的 Ensemble 示例集(在 Ensdemo 空间中)的一部分。 Ensemble 接收问题报告并启动 HelpDesk 业务流程。 业务流程使用 EnsLib.Workflow.TaskRequest 类的消息向具有 Demo-Development 角色的用户发送任务,该类定义了所有可能的操作(“Fixed”或“Ignored”)以及“Comment”字段。 消息的正文还包含有关错误和报告错误的用户的信息。 在此之后,相应的任务会出现在每个具有演示开发角色的用户的工作流门户中。 最初(如果未在 TaskRequest 消息中定义),任务不与任何特定用户关联(仅与角色关联),因此用户必须通过单击相应按钮来接受它。 您也可以通过单击“推迟”按钮来拒绝任务。 完成后,您可以执行此任务允许的任何操作。 在我们的例子中,我们可以在相应字段中提供评论后单击“已修复”按钮。 HelpTask 业务流程将处理此事件并向具有 Demo-Testing 角色的用户发送一条新消息,从而表明需要测试更改。 如果单击“忽略”按钮,该任务将被标记为“不是问题”,并且其处理将停止。 从这个例子可以看出,Ensemble Workflow 是一个简单直观的系统,用于组织用户的工作流。 关于 Ensemble Workflow 子系统的更多详细信息可以在 Ensemble 手册的定义工作流部分找到。 Ensemble Workflow 子系统的功能可以轻松扩展并集成到基于 InterSystems Ensemble 的外部复合应用程序中。 作为一个例子,让我们看一下在使用 Angular.js + REST API(由 Eduard Lebedyuk 编写)开发的外部复合应用程序中实现 Ensemble Workflow 的用户界面。 Angular.js 中的Ensemble工作流接口 要使 Workflow 界面与 Angular.js 一起使用,您需要在服务器上安装以下 Ensemble 应用程序: UI in Angular.js REST API 安装过程在指定存储库的自述文件中进行了描述。 目前(原帖里说),该应用程序具有 Ensemble Workflow 的所有必要功能:显示任务列表、附加字段和操作、排序、任务中的全文搜索。 用户可以接受/拒绝任务。 有关任务的详细信息显示在模式窗口中。 (实现只是概念证明,它还有很大的改进空间。它还以一种不得在生产中使用的方式使用 BasicAuth。目前我们已经有一个更复杂的例子)。 应用程序如下所示: UI 使用以下库和框架:Angular.js、Twitter Bootstrap 以及 FontAwesome 图标字体。 您可以查看我们的测试服务器上运行的 HelpDesk 应用程序的用户界面。 账号:dev,密码:123 对于那些对源代码感兴趣的朋友们 以下这个小应用程序的结构: 该应用程序有 4 个 Angular 服务(RESTSrvc、SessionSrvc、UtilSrvc 和 WorklistSrvc)、3 个控制器(MainCtrl、TaskCtrl、TasksGridCtrl)、一个主页(index.csp)和 2 个模板(task.csp 和 tasks.csp)。 RESTSrvc 服务只有一个方法 getPromise,它是 $http Angular.js 服务的包装器。 RESTSrvc 的唯一目的是向服务器发送 HTTP 请求并返回这些请求的 Promise 对象。 其他服务使用 RESTSrvc 来发出请求,它们的分离本质上是一种功能性的(它其实可以写得更好)。点击下栏查看代码: RESTSrvc 'use strict'; function RESTSrvc($http, $q) { return { getPromise: function(config) { var deferred = $q.defer(); $http(config) .success(function(data, status, headers, config) { deferred.resolve(data); }) .error(function(data, status, headers, config) { deferred.reject(data, status, headers, config); }); return deferred.promise; } }}; RESTSrvc.$inject = ['$http', '$q']; servicesModule.factory('RESTSrvc', RESTSrvc); SessionSrvc :包含一个负责关闭会话的方法。 此应用程序中的身份验证是使用基本访问身份验证 (http://en.wikipedia.org/wiki/Basic_access_authentication) 实现的,因此不需要单独的身份验证方法,因为每个请求的标头中都有一个授权令牌。点击下栏查看代码: SessionSrvc 'use strict'; // Session servicefunction SessionSrvc(RESTSrvc) { return { // save worklist object logout: function (baseAuthToken) { return RESTSrvc.getPromise({ method: 'GET', url: RESTWebApp.appName + '/logout', headers: { 'Authorization': baseAuthToken } }); } }}; SessionSrvc.$inject = ['RESTSrvc'];servicesModule.factory('SessionSrvc', SessionSrvc); UtilSrvc :包含辅助方法,例如按名称获取 cookie 值、按名称获取对象属性。点击下栏查看代码: UtilSrvc // Utils servicefunction UtilSrvc($cookies) { return { // get cookie by name readCookie: function (name) { return $cookies[name]; }, // Function to get value of property of the object by name // Example: // var obj = {car: {body: {company: {name: 'Mazda'}}}}; // getPropertyValue(obj, 'car.body.company.name') getPropertyValue: function (item, propertyStr) { var value = item; try { var properties = propertyStr.split('.'); for (var i = 0; i < properties.length; i++) { value = value[properties[i]]; if (value !== Object(value)) break; } } catch (ex) { console.log('Something goes wrong :/'); } return value == undefined ? '' : value; } }}; UtilSrvc.$inject = ['$cookies'];servicesModule.factory('UtilSrvc', UtilSrvc); WorklistSrvc :负责与任务列表数据相关的请求。点击下栏查看代码: WorklistSrvc 'use strict'; // Worklist servicefunction WorklistSrvc(RESTSrvc) { return { // save worklist object save: function (worklist, baseAuthToken) { return RESTSrvc.getPromise({ method: 'POST', url: RESTWebApp.appName + '/tasks/' + worklist._id, data: worklist, headers: { 'Authorization': baseAuthToken } }); }, // get worklist by id get: function (id, baseAuthToken) { return RESTSrvc.getPromise({ method: 'GET', url: RESTWebApp.appName + '/tasks/' + id, headers: { 'Authorization': baseAuthToken } }); }, // get all worklists for current user getAll: function (baseAuthToken) { return RESTSrvc.getPromise({ method: 'GET', url: RESTWebApp.appName + '/tasks', headers: { 'Authorization': baseAuthToken } }); } }}; WorklistSrvc.$inject = ['RESTSrvc'];servicesModule.factory('WorklistSrvc', WorklistSrvc); MainCtrl :负责用户身份验证的应用程序的主控制器。点击下栏查看代码: MainCtrl 'use strict'; // Main controller// Controls the authentication. Loads all the worklists for user.function MainCtrl($scope, $location, $cookies, WorklistSrvc, SessionSrvc, UtilSrvc) { $scope.page = {}; $scope.page.alerts = []; $scope.utils = UtilSrvc; $scope.page.loading = false; $scope.page.loginState = $cookies['Token'] ? 1 : 0; $scope.page.authToken = $cookies['Token']; $scope.page.closeAlert = function (index) { if ($scope.page.alerts.length) { $('.alert:nth-child(' + (index + 1) + ')').animate({ opacity: 0, top: "-=150" }, 400, function () { $scope.page.alerts.splice(index, 1); $scope.$apply(); }); } }; $scope.page.addAlert = function (alert) { $scope.page.alerts.push(alert); if ($scope.page.alerts.length > 5) { $scope.page.closeAlert(0); } }; /* Authentication section */ $scope.page.makeBaseAuth = function (user, password) { var token = user + ':' + password; var hash = Base64.encode(token); return "Basic " + hash; } // login $scope.page.doLogin = function (login, password) { var authToken = $scope.page.makeBaseAuth(login, password); $scope.page.loading = true; WorklistSrvc.getAll(authToken).then( function (data) { $scope.page.alerts = []; $scope.page.loginState = 1; $scope.page.authToken = authToken; // set cookie to restore loginState after page reload $cookies['User'] = login.toLowerCase(); $cookies['Token'] = $scope.page.authToken; // refresh the data on page $scope.page.loadSuccess(data); }, function (data, status, headers, config) { if (data.Error) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); } else { $scope.page.addAlert({ type: 'danger', msg: "Login unsuccessful" }); } }) .then(function () { $scope.page.loading = false; }) }; // logout $scope.page.doExit = function () { SessionSrvc.logout($scope.page.authToken).then( function (data) { $scope.page.loginState = 0; $scope.page.grid.items = null; $scope.page.loading = false; // clear cookies delete $cookies['User']; delete $cookies['Token']; document.cookie = "CacheBrowserId" + "=; Path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; document.cookie = "CSPSESSIONID" + "=; Path=" + RESTWebApp.appName + "; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; document.cookie = "CSPWSERVERID" + "=; Path=" + RESTWebApp.appName + "; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); }; } MainCtrl.$inject = ['$scope', '$location', '$cookies', 'WorklistSrvc', 'SessionSrvc', 'UtilSrvc'];controllersModule.controller('MainCtrl', MainCtrl); TasksGridCtrl :一个控制器,负责任务列表和与之关联的操作。 它初始化任务列表表,包含加载任务列表和具体任务的方法,以及处理用户动作的方法(按键、表格排序、行选择、过滤)。点击下栏查看代码: TasksGridCtrl 'use strict'; // TasksGrid controller// dependency injectionfunction TasksGridCtrl($scope, $window, $modal, $cookies, WorklistSrvc) { // Initialize grid. // grid data: // grid title, css grid class, column names $scope.page.grid = { caption: 'Inbox Tasks', cssClass: 'table table-condensed table-bordered table-hover', columns: [{ name: '', property: 'New', align: 'center' }, { name: 'Priority', property: 'Priority' }, { name: 'Subject', property: 'Subject' }, { name: 'Message', property: 'Message' }, { name: 'Role', property: 'RoleName' }, { name: 'Assigned To', property: 'AssignedTo' }, { name: 'Time Created', property: 'TimeCreated' }, { name: 'Age', property: 'Age' }] }; // data initialization for Worklist $scope.page.dataInit = function () { if ($scope.page.loginState) { $scope.page.loadTasks(); } }; $scope.page.loadSuccess = function (data) { $scope.page.grid.items = data.children; // if we get data for other user - logout if (!$scope.page.checkUserValidity()) { $scope.page.doExit(); } var date = new Date(); var hours = (date.getHours() > 9) ? date.getHours() : '0' + date.getHours(); var minutes = (date.getMinutes() > 9) ? date.getMinutes() : '0' + date.getMinutes(); var secs = (date.getSeconds() > 9) ? date.getSeconds() : '0' + date.getSeconds(); $('#updateTime').animate({ opacity: 0 }, 100, function () { $('#updateTime').animate({ opacity: 1 }, 1000); }); $scope.page.grid.updateTime = ' [Last Update: ' + hours; $scope.page.grid.updateTime += ':' + minutes + ':' + secs + ']'; }; // all user's tasks loading $scope.page.loadTasks = function () { $scope.page.loading = true; WorklistSrvc.getAll($scope.page.authToken).then( function (data) { $scope.page.loadSuccess(data); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }) .then(function () { $scope.page.loading = false; }) }; // load task (worklist) by id $scope.page.loadTask = function (id) { WorklistSrvc.get(id, $scope.page.authToken).then( function (data) { $scope.page.task = data; }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); }; // 'Accept' button handler. // Send worklist object with '$Accept' action to server. $scope.page.accept = function (id) { // nothing to do, if no id if (!id) return; // get full worklist, set action and submit worklist. WorklistSrvc.get(id).then( function (data) { data.Task["%Action"] = "$Accept"; $scope.page.submit(data); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); }; // 'Yield' button handler. // Send worklist object with '$Relinquish' action to server. $scope.page.yield = function (id) { // nothing to do, if no id if (!id) return; // get full worklist, set action and submit worklist. WorklistSrvc.get(id).then( function (data) { data.Task["%Action"] = "$Relinquish"; $scope.page.submit(data); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); }; // submit the worklist object $scope.page.submit = function (worklist) { // send object to server. If ok, refresh data on page. WorklistSrvc.save(worklist, $scope.page.authToken).then( function (data) { $scope.page.dataInit(); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); } ); }; /* table section */ // sorting table $scope.page.sort = function (property, isUp) { $scope.page.predicate = property; $scope.page.isUp = !isUp; // change sorting icon $scope.page.sortIcon = 'fa fa-sort-' + ($scope.page.isUp ? 'up' : 'down') + ' pull-right'; }; // selecting row in table $scope.page.select = function (item) { if ($scope.page.grid.selected) { $scope.page.grid.selected.rowCss = ''; if ($scope.page.grid.selected == item) { $scope.page.grid.selected = null; return; } } $scope.page.grid.selected = item; // change css class to highlight the row $scope.page.grid.selected.rowCss = 'info'; }; // count currently displayed tasks $scope.page.totalCnt = function () { return $window.document.getElementById('tasksTable').getElementsByTagName('TR').length - 2; }; // if AssignedTo matches with current user - return 'true' $scope.page.isAssigned = function (selected) { if (selected) { if (selected.AssignedTo.toLowerCase() === $cookies['User'].toLowerCase()) return true; } return false; }; // watching for changes in 'Search' input // if there is change, reset the selection. $scope.$watch('query', function () { if ($scope.page.grid.selected) { $scope.page.select($scope.page.grid.selected); } }); /* modal window open */ $scope.page.modalOpen = function (size, id) { // if no id - nothing to do if (!id) return; // obtainig the full object by id. If ok - open modal. WorklistSrvc.get(id).then( function (data) { // see http://angular-ui.github.io/bootstrap/ for more options var modalInstance = $modal.open({ templateUrl: 'partials/task.csp', controller: 'TaskCtrl', size: size, backdrop: true, resolve: { task: function () { return data; }, submit: function () { return $scope.page.submit } } }); // onResult modalInstance.result.then( function (reason) { if (reason === 'save') { $scope.page.addAlert({ type: 'success', msg: 'Task saved' }); } }, function () { }); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); }; /* User's validity checking. */ // If we get the data for other user, logout immediately $scope.page.checkUserValidity = function () { var user = $cookies['User']; for (var i = 0; i < $scope.page.grid.items.length; i++) { if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() !== $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return false; } else if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() == $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return true; } } return true; }; // Check user's validity every 10 minutes. setInterval(function () { $scope.page.dataInit() }, 600000); /* Initialize */ // sort table (by Age, asc) // to change sorting column change 'columns[<index>]' $scope.page.sort($scope.page.grid.columns[7].property, true); $scope.page.dataInit();} TasksGridCtrl.$inject = ['$scope', '$window', '$modal', '$cookies', 'WorklistSrvc'];controllersModule.controller('TasksGridCtrl', TasksGridCtrl); TaskCtrl :模式窗口的控制器,包含有关任务的详细信息。 形成字段和用户操作的列表,还处理模态窗口中的按钮单击。点击下栏查看代码: TaskCtrl 'use strict'; function TaskCtrl($scope, $routeParams, $location, $modalInstance, WorklistSrvc, task, submit) { $scope.page = { task: {} }; $scope.page.task = task; $scope.page.actions = ""; $scope.page.formFields = ""; $scope.page.formValues = task.Task['%FormValues']; if (task.Task['%TaskStatus'].Request['%Actions']) { $scope.page.actions = task.Task['%TaskStatus'].Request['%Actions'].split(','); } if (task.Task['%TaskStatus'].Request['%FormFields']) { $scope.page.formFields = task.Task['%TaskStatus'].Request['%FormFields'].split(','); } // dismiss modal $scope.page.cancel = function () { $modalInstance.dismiss('cancel'); }; // perform a specified action $scope.page.doAction = function (action) { $scope.page.task.Task["%Action"] = action; $scope.page.task.Task['%FormValues'] = $scope.page.formValues; submit($scope.page.task); $modalInstance.close(action); } } // resolving minification problemsTaskCtrl.$inject = ['$scope', '$routeParams', '$location', '$modalInstance', 'WorklistSrvc', 'task', 'submit'];controllersModule.controller('TaskCtrl', TaskCtrl); app.js :包含所有应用程序模块的文件。点击下栏查看代码: app.js 'use strict';/*Adding routes(when).[route], {[template path for ng-view], [controller for this template]} otherwiseSet default route. $routeParams.id - :id parameter.*/ var servicesModule = angular.module('servicesModule', []);var controllersModule = angular.module('controllersModule', []);var app = angular.module('app', ['ngRoute', 'ngCookies', 'ui.bootstrap', 'servicesModule', 'controllersModule']); app.config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/tasks', { templateUrl: 'partials/tasks.csp' }); $routeProvider.when('/tasks/:id', { templateUrl: 'partials/task.csp', controller: 'TaskCtrl' }); $routeProvider.otherwise({ redirectTo: '/tasks' });}]); index.csp :应用程序的主页。点击下栏查看代码: index.csp <!doctype html> <html> <head> <title>Ensemble Workflow</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <!-- CSS Initialization --> <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="css/font-awesome.min.css"> <link rel="stylesheet" type="text/css" href="css/bootstrap-theme.min.css"> <link rel="stylesheet" type="text/css" href="css/custom.css"> <script language="javascript"> // REST web-app name, global variable var RESTWebApp = {appName: '#($GET(^Settings("WF", "WebAppName")))#'}; </script> </head> <body ng-app="app" ng-controller="MainCtrl"> <nav class="navbar navbar-default navbar-fixed-top"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="#">Ensemble Workflow</a> </div> <div class="navbar-left"> <button ng-cloak ng-disabled="page.loginState != 1 || page.loading" type="button" class="btn btn-default navbar-btn" ng-click="page.dataInit();">Refresh Worklist</button> </div> <div class="navbar-left"> <form role="search" class="navbar-form"> <div class="form-group form-inline"> <label for="search" class="sr-only">Search</label> <input ng-cloak ng-disabled="page.loginState != 1" type="text" class="form-control" placeholder="Search" id="search" ng-model="query"> </div> </form> </div> <div class="navbar-right"> <form role="form" class="navbar-form form-inline" ng-show="page.loginState != 1" ng-model="user" ng-submit="page.doLogin(user.Login, user.PasswordSetter); user='';" ng-cloak> <div class="form-group"> <input class="form-control uc-inline" ng-model="user.Login" placeholder="Username" ng-disabled="page.loading"> <input type="password" class="form-control uc-inline" ng-model="user.PasswordSetter" placeholder="Password" ng-disabled="page.loading"> <button type="submit" class="btn btn-default" ng-disabled="page.loading">Sign In</button> </div> </form> </div> <button ng-show="page.loginState == 1" type="button" ng-click="page.doExit();" class="btn navbar-btn btn-default pull-right" ng-cloak>Logout, <span class="label label-info" ng-bind="utils.readCookie('User')"></span> </button> </div> </nav> <div class="container-fluid"> <div style="height: 20px;"> <div ng-show="page.loading" class="progress-bar progress-bar-striped progress-condensed active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%" ng-cloak> Loading </div> </div> <!-- Alerts --> <div ng-controller="AlertController" ng-cloak> <alert title="Click to dismiss" ng-repeat="alert in page.alerts" type="{{alert.type}}" ng-click="page.closeAlert($index, alert)">{{alert.msg}}</alert> </div> <div ng-show="page.loginState != 1" class="attention" ng-cloak> <p>Please, Log In first.</p> </div> <!-- Loading template --> <div ng-view> </div> </div> </div> <!-- Hooking scripts --> <script language="javascript" src="libs/angular.min.js"></script> <script language="javascript" src="libs/angular-route.min.js"></script> <script language="javascript" src="libs/angular-cookies.min.js"></script> <script language="javascript" src="libs/ui-bootstrap-custom-tpls-0.12.0.min.js"></script> <script language="javascript" src="libs/base64.js"></script> <script language="javascript" src="js/app.js"></script> <script language="javascript" src="js/services/RESTSrvc.js"></script> <script language="javascript" src="js/services/WorklistSrvc.js"></script> <script language="javascript" src="js/services/SessionSrvc.js"></script> <script language="javascript" src="js/services/UtilSrvc.js"></script> <script language="javascript" src="js/controllers/MainCtrl.js"></script> <script language="javascript" src="js/controllers/TaskCtrl.js"></script> <script language="javascript" src="js/controllers/TasksGridCtrl.js"></script> <script language="javascript" src="libs/jquery-1.11.2.min.js"></script> <script language="javascript" src="libs/bootstrap.min.js"></script> </body></html> tasks.csp :任务列表模板。点击下栏查看代码: tasks.csp <div class="row-fluid"> <div class="span1"> </div> <div ng-hide="page.loginState != 1 || (page.loading && !page.totalCnt())" ng-controller="TasksGridCtrl"> <div class="panel panel-default top-buffer"> <table class="table-tasks" ng-class="page.grid.cssClass" id="tasksTable"> <caption class="text-left"> <b ng-bind="page.grid.caption"></b><b id="updateTime" ng-bind="page.grid.updateTime"></b> </caption> <thead style="cursor: pointer; vertical-align: middle;"> <tr> <th class="text-center">#</th> <!-- In the cycle prints the name of the column, specify for each column click handler and the icon (sorting) --> <th ng-repeat="column in page.grid.columns" class="text-center" ng-click="page.sort(column.property, page.isUp)"> <span ng-bind="column.name" style="padding-right: 4px;"></span> <i style="margin-top: 3px;" ng-class="page.sortIcon" ng-show="column.property == page.predicate"></i> <i style="color: #ccc; margin-top: 3px;" class="fa fa-sort pull-right" ng-show="column.property != page.predicate"></i> </th> <th class="text-center">Action</th> </tr> </thead> <tfoot> <tr> <!-- Control buttons and messages --> <td colspan="{{page.grid.columns.length + 2}}"> <p ng-hide="page.grid.items.length">There is no task(s) for current user.</p> <span ng-show="page.grid.items.length"> Showing {{page.totalCnt()}} of {{page.grid.items.length}} task(s). </span> </td> </tr> </tfoot> <tbody style="cursor: default;"> <!-- In the cycle prints the table rows (sort by specified column) --> <tr ng-repeat="item in page.grid.items | orderBy:page.predicate:page.isUp | filter:query" ng-class="item.rowCss" > <td ng-bind="$index + 1" class="text-right"></td> <!-- In the cycle prints the table cells to each row --> <td ng-repeat="column in page.grid.columns" style="text-align: {{column.align}};" ng-click="page.select(item)"> <span class="label label-info" ng-show="$first && item.New">New</span> <span ng-hide="$first" ng-bind="utils.getPropertyValue(item, column.property)"></span> </td> <td class="text-center"> <div title="Accept task" class="button button-success fa fa-plus-circle" ng-click="page.accept(item.ID)" ng-show="!page.isAssigned(item)"></div> <div title="Details" class="button button-info fa fa-search" ng-click="page.modalOpen('lg', item.ID)" ng-show="page.isAssigned(item)"></div> <div title="Yield task" class="button button-danger fa fa-minus-circle" ng-click="page.yield(item.ID)" ng-show="page.isAssigned(item)"></div> </td> </tr> </tbody> </table> </div> </div> <div class="span1"> </div></div><br> task.csp — 模态窗口模板。点击下栏查看代码: task.csp <div class="modal-header"> <h3 class="modal-title">Task description</h3> </div> <div class="modal-body"> <div class="container-fluid"> <div class="row top-buffer"> <div class="col-xs-12 col-md-6"> <div class="form-group"> <label for="subject">Subject</label> <input id="subject" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Subject'];" readonly> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for="timeCreated">Time created</label> <input id="timeCreated" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].TimeCreated;" readonly> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="message">Message</label> <textarea id="message" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Message'];" rows="3" readonly></textarea> </div> </div> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for="role">Role</label> <input id="role" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Role.Name;" readonly> </div> </div> <div class="col-md-3"> <div class="form-group"> <label for="assignedTo">Assigned to</label> <input id="assignedTo" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].AssignedTo;" readonly> </div> </div> <div class="col-md-3"> <div class="form-group"> <label for="priority">Priority</label> <input id="priority" type="text" class="form-control task-info-input" ng-model="page.task.Task['%Priority'];" readonly> </div> </div> </div> <div class="row" ng-show="page.formFields"> <div class="delimeter col-md-6 el-centered"> </div> </div> <div class="row" ng-repeat="formField in page.formFields"> <div class="col-md-12"> <div class="form-group"> <label for="form{{$index}}" ng-bind="formField"></label> <input id="form{{$index}}" type="text" class="form-control task-info-input" ng-model="page.formValues[formField]"> </div> </div> </div> </div> </div> <div class="modal-footer"> <button ng-repeat="action in page.actions" class="btn btn-primary top-buffer" ng-click="page.doAction(action)" ng-bind="action"></button> <button class="btn btn-success top-buffer" ng-click="page.doAction('$Save')">Save</button> <button class="btn btn-warning top-buffer" ng-click="page.cancel()">Cancel</button> </div> 此外,您可以自由地将我们的 REST API 用于您的 UI,尤其是考虑到它非常简单。点击下栏查看代码: The URL map of our REST API <Routes> <Route Url="/logout" Method="GET" Call="Logout"/> <Route Url="/tasks" Method="GET" Call="GetTasks"/> <Route Url="/tasks/:id" Method="GET" Call="GetTask"/> <Route Url="/tasks/:id" Method="POST" Call="PostTask"/> <Route Url="/test" Method="GET" Call="Test"/></Routes> 本文翻译自 Habrahabr InterSystems 博客(俄语) <Routes> <Route Url="/logout" Method="GET" Call="Logout"/> <Route Url="/tasks" Method="GET" Call="GetTasks"/> <Route Url="/tasks/:id" Method="GET" Call="GetTask"/> <Route Url="/tasks/:id" Method="POST" Call="PostTask"/> <Route Url="/test" Method="GET" Call="Test"/></Routes> <div class="modal-header"> <h3 class="modal-title">Task description</h3> </div> <div class="modal-body"> <div class="container-fluid"> <div class="row top-buffer"> <div class="col-xs-12 col-md-6"> <div class="form-group"> <label for="subject">Subject</label> <input id="subject" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Subject'];" readonly> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for="timeCreated">Time created</label> <input id="timeCreated" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].TimeCreated;" readonly> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="message">Message</label> <textarea id="message" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Message'];" rows="3" readonly></textarea> </div> </div> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for="role">Role</label> <input id="role" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Role.Name;" readonly> </div> </div> <div class="col-md-3"> <div class="form-group"> <label for="assignedTo">Assigned to</label> <input id="assignedTo" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].AssignedTo;" readonly> </div> </div> <div class="col-md-3"> <div class="form-group"> <label for="priority">Priority</label> <input id="priority" type="text" class="form-control task-info-input" ng-model="page.task.Task['%Priority'];" readonly> </div> </div> </div> <div class="row" ng-show="page.formFields"> <div class="delimeter col-md-6 el-centered"> </div> </div> <div class="row" ng-repeat="formField in page.formFields"> <div class="col-md-12"> <div class="form-group"> <label for="form{{$index}}" ng-bind="formField"></label> <input id="form{{$index}}" type="text" class="form-control task-info-input" ng-model="page.formValues[formField]"> </div> </div> </div> </div> </div> <div class="modal-footer"> <button ng-repeat="action in page.actions" class="btn btn-primary top-buffer" ng-click="page.doAction(action)" ng-bind="action"></button> <button class="btn btn-success top-buffer" ng-click="page.doAction('$Save')">Save</button> <button class="btn btn-warning top-buffer" ng-click="page.cancel()">Cancel</button> </div> 'use strict';/*Adding routes(when).[route], {[template path for ng-view], [controller for this template]} otherwiseSet default route. $routeParams.id - :id parameter.*/ var servicesModule = angular.module('servicesModule', []);var controllersModule = angular.module('controllersModule', []);var app = angular.module('app', ['ngRoute', 'ngCookies', 'ui.bootstrap', 'servicesModule', 'controllersModule']); app.config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/tasks', { templateUrl: 'partials/tasks.csp' }); $routeProvider.when('/tasks/:id', { templateUrl: 'partials/task.csp', controller: 'TaskCtrl' }); $routeProvider.otherwise({ redirectTo: '/tasks' });}]); 欢迎下载Ensembleworkflow 小程序
文章
Nicky Zhu · 二月 3, 2021

IRIS中的权限管理

下一篇: [案例: 建立只能使用SQL的用户](https://cn.community.intersystems.com/post/%E6%A1%88%E4%BE%8B-%E5%BB%BA%E7%AB%8B%E5%8F%AA%E8%83%BD%E4%BD%BF%E7%94%A8sql%E7%9A%84%E7%94%A8%E6%88%B7) IRIS通过认证(Authentication)与授权(Authorization)两项机制控制外部用户对系统及应用、数据资源的可访问性。因此。如需要进行权限控制,则需要通过配置认证和授权进行。 ## IRIS中的认证 {#2} 认证可以验证任何试图连接到InterSystems IRIS®的用户的身份。一旦通过认证,用户就与IRIS建立了通信,从而可以使用其数据和工具。有许多不同的方法可以验证用户的身份;每种方法都称为验证机制。IRIS 通常被配置为只使用其中一种方式。 支持的认证方式 * 实例认证:通过用户名/密码对登录平台,即密码认证 * LDAP:通过第三方LDAP服务器(如Windows Active Directory )完成认证 * 操作系统认证:建立操作系统用户-平台用户映射,使用操作系统用户登录平台 * Kerberos:使用Kerberos协议进行认证 * 代理认证:使用自定义的代码实现认证过程 ### 系统服务与认证 {#2.1} 在安装时,IRIS会启动一系列系统级的服务用与控制与外部用户或系统的交互,这些服务都绑定了默认的认证机制 ![image](/sites/default/files/inline/images/2.1.png) 图中红框标出的即为系统安装后会自动启用并需经认证才可使用的系统服务,认证手段可配置。 例如,如果变更%Service_Console的身份验证方法,取消密码方法,用户就不能通过输入用户名密码登入Terminal。 通过Portal的菜单 系统管理 > 安全 > 服务 可访问该设置。 ### 账户控制参数 {#2.2} 通过系统管理 > 安全 > 系统安全 > 系统范围的安全参数中的选项可对于用户名/密码认证手段的行为进行更多的约束。 ![image](/sites/default/files/inline/images/2.2.png) * 非活动限制 - 指定用户账户不活跃的最大天数,它被定义为成功登录之间的时间。当达到此限制时,该帐户将被禁用。值为0(0)表示对登录之间的天数没有限制。[对于最低安全级别的安装,默认为0,对于正常和锁定的安装,默认为90]。 * 无效登录限制 (0-64) - 指定连续不成功的登录尝试的最大次数。在达到此限制后,要么禁用账户,要么对每次尝试进行递增的时间延迟;行动取决于如果达到登录限制字段则禁用账户的值。值为0(零)表示对无效登录的次数没有限制。[默认为5] * 如果达到登录限制,则禁用账户 - 如果选中,则指定达到无效登录次数(在前一字段中指定)将导致用户账户被禁用。 * 密码有效期天数(0-99999) - 指定密码过期的频率以及用户更改密码的频率(天数)。当初始设置时,指定密码过期的天数。0(0)表示密码永远不会过期。不会影响已设置了下次登录时更改密码字段的用户。[默认为0] 需要特别注意的是,密码有效性、过期和禁用账户等设置会影响IRIS实例的所有账户,包括IRIS超级管理员账户。如触发了控制策略,则在更新这些帐户的信息之前,可能无法进行各种操作,这可能导致意外的结果。如超级管理员账户被锁定,则需要通过紧急模式启动实例再进行修改。 对于系统可用的认证手段的配置和其他可用的安全配置,请参见[Security Administration Guide](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS "安全管理向导") ## IRIS中的授权 {#3} ### 授权模型 {#3.1} InterSystems公司的授权模式采用基于角色的访问控制。 * Users – 用户 * Roles – 角色 * Privileges – 权限 * Resources – 资源 | Permissions – 许可 在这种模式下,用户拥有与分配给各自用户身份的角色相关的权限。 ![image](/sites/default/files/inline/images/3.1.png) * 一个角色是一个命名的特权集合 * 一个用户可以拥有一个以上的角色 * 权限分配给角色,角色分配给用户 其中,Roles就是权限的集合,而权限提供对资源的特定类型的访问的许可。 * 可控资源: 数据库,服务,应用(包括Web应用 )和其他 * 可选用的许可: Read, Write or Use,其中执行代码需要数据库的读权限 ### 资源的定义 {#3.2} 资源是一项相对抽象的概念,用来指代IRIS中的数据库,服务,应用等可被访问的对象。例如,对于数据库,在建立时默认采用%DB_%DEFAULT指代,也可自定义资源(数据库资源必须以%DB_开头): ![image](/sites/default/files/inline/images/3.2.png) 对于Web应用,默认不需要通过资源控制,即所有可登录用户都可访问(但该用户进程不一定能访问到数据,还需参照是否具有对数据库的访问权限)。如通过分配资源进行控制,则登录用户还需具有资源才能访问这个Web应用: ![image](/sites/default/files/inline/images/3.3.png) 因此,一项权限实际上是指对某个资源的一些特定操作的集合。 例如,对于数据库UserDB具有读写操作许可的权限A,对于Web应用/csp/sys具有使用操作许可的权限B。如果我们将这两项权限都赋给角色RoleA,那么这个角色就同时拥有A权限和B权限,从而能够访问数据库UserDB和访问Web应用/csp/sys。 ### SQL授权 {#3.3} 除了对数据库进行授权外,IRIS作为一个数据平台,需要对外提供数据访问。因此,IRIS也提供了SQL授权对用户可执行的SQL进行细粒度的权限控制。 SQL的授权可以分配给角色或用户。但通常在企业环境中,用户数量会很多,仍然需要对SQL用户进行分组,根据分组规划角色,通过角色进行授权的控制,才能有效降低维护授权所需的工作量。 SQL的授权针对SQL类型,可分为库、表级授权。 对于Create table、drop view、truncate table这一类的DDL,使用库级授权,即用户可在特定的库中执行建表、删除视图等经过授权的操作。如下: ![image](/sites/default/files/inline/images/3.4.png) 对于Select、update等DML,则使用表级授权,使用户能够通过DML访问特定的表中的数据。如下: ![image](/sites/default/files/inline/images/3.5.png) 除通过Portal操作之外,对于SQL的授权,还可使用IRIS SQL中的额GRANT语句,例如: `GRANT * ON Schema Test TO TestRole` 这个SQL即可以将当前操作数据库下Schema Test中的所有表的所有权限都赋给TestRole这个角色。 关于GRANT语句的用法,可参见[GRANT指令](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_grant) 以上即为IRIS中进行权限控制所需掌握的概念和内容,在后续文章中,我们会结合实例向大家介绍其使用。 下一篇: [案例: 建立只能使用SQL的用户](https://cn.community.intersystems.com/post/%E6%A1%88%E4%BE%8B-%E5%BB%BA%E7%AB%8B%E5%8F%AA%E8%83%BD%E4%BD%BF%E7%94%A8sql%E7%9A%84%E7%94%A8%E6%88%B7) 推荐阅读 [Security Administration Guide](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS "安全管理向导") - https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS
文章
姚 鑫 · 六月 13, 2021

第六章 控制名称空间的使用

# 第六章 控制名称空间的使用 # 控制名称空间的使用 如将对象投射到`XML`中所述,可以将类分配给名称空间,以便相应的XML元素属于该名称空间,还可以控制类的属性是否也属于该名称空间。 将类中的对象导出为XML时,`%XML.Write`提供其他选项,例如指定元素是否为其父级的本地元素。本节包括以下主题: - 默认情况下,`%XML.Writer`如何处理命名空间 - 如何指定本地元素是否合格 - 如何指定元素是否为其父元素的本地元素 - 如何指定属性是否合格 - 命名空间分配方式的摘要 注意:在InterSystems IRIS XML支持中,可以按类指定名称空间。通常,每个类都有自己的命名空间声明;但是,通常只需要一个或少量的命名空间。还可以在逐个类的基础上指定相关信息(而不是以某种全局方式)。这包括控制元素是否为其父元素的本地元素以及子元素是否合格的设置。为简单起见,建议使用一致的方法。 ## 名称空间的默认处理 若要将启用XML的类分配给命名空间,请设置该类的`Namespace`参数,如将对象投影到XML中所述。在`%XML.Writer`会自动插入命名空间声明,生成命名空间前缀,并在适当的地方应用前缀。例如,以下类定义: ```java Class GXML.Objects.WithNamespaces.Person Extends (%Persistent, %Populate, %XML.Adaptor) { Parameter NAMESPACE = "http://www.person.com"; Property Name As %Name [ Required ]; Property DOB As %Date(FORMAT = 5, MAXVAL = "+$h") [ Required ]; Property GroupID As %Integer(MAXVAL=10,MINVAL=1,XMLPROJECTION="ATTRIBUTE"); } ``` ```java Uberoth,Amanda Q. 1952-01-13 ``` 请注意以下事项: - 名称空间声明被添加到每``元素。 - 默认情况下,``元素的局部元素(``和``)是限定的。 该名称空间被添加为默认名称空间,因此应用于这些元素。 - ``元素的属性(`GroupID`)默认是不限定的。 这个属性没有前缀,因此被认为是未限定的。 - 这里显示的前缀是自动生成的。 (请记住,当对象分配给名称空间时,只指定名称空间,而不是前缀。) - 此输出不会在写入器中设置任何与名称空间相关的属性,也不会在写入器中使用任何与名称空间相关的方法。 ### 命名空间分配的上下文效应 为支持xml的对象分配的名称空间取决于该对象是在顶层导出还是作为另一个对象的属性导出。 一个名为`Address`的类。 假设使用`NAMESPACE`参数将`Address`类分配给名称空间`“http://www.address.org”`。 如果你直接导出`Address`类的一个对象,你可能会收到如下输出: ```java 8280 Main Avenue Washington VT 15355 ``` 注意, `` 元素及其所有元素都在同一个名称空间(`“http://www.address.org”`)中。 相反,假设`Person`类的属性是`Address`对象。 使用`NAMESPACE`参数将`Person`类分配给名称空间`“http://www.person.org”`。 如果导出`Person`类的一个对象,将收到如下输出: ```java Zevon,Samantha H. 1964-05-24 8280 Main Avenue Washington VT 15355 ``` 注意,``元素位于其父对象(`“http://www.person.org”`)的名称空间中。 但是,``的元素位于名称空间`“http://www.address.org”`中。 ## 控制局部元素是否限定 在顶层导出对象时,通常将其视为全局元素。然后根据启用XML的对象的`ELEMENTQUALIFIED`参数的设置处理其本地元素。如果未设置此类参数,则改用编写器属性`ElementQualified`的值;默认情况下,文本格式为1,编码格式为0。 下面的示例显示一个默认设置为`ElementQualified`的对象,即1: ```java Pybus,Gertrude X. 1986-10-19 ``` 该名称空间被添加到``元素中作为默认名称空间,因此应用于元素``和``子元素。 我们修改了写入器定义并将`ElementQualified`属性设置为0。 在本例中,相同的对象如下所示: ```java Pybus,Gertrude X. 1986-10-19 ``` 在本例中,名称空间被添加到带有前缀的``元素中,该前缀用于``元素,但不用于其子元素。 ## 控制一个元素是否局部于它的父元素 默认情况下,当使用`object()`方法生成一个元素并且该元素具有命名空间时,该元素不是其父元素的本地元素。相反,可以强制元素属于其父元素的命名空间。为此,可以使用`object()`方法的可选本地参数;这是第四个参数。 ### 本地参数为0(默认值) 在这里的示例中虑将`NAMESPACE`类参数指定为`“http://www.person.com”`的`Person`类。 如果打开根元素,然后使用`Object()`生成`Person`,则``元素位于`“http://www.person.com”`名称空间中。 以下例子: ```java Adam,George L. 1947-06-29 ``` 如果我们将``元素更深地嵌套到其他元素中,也会出现类似的结果。 ```java Adam,George L. 1947-06-29 ``` ### 局部参数设置为1 为了强制``元素成为其父元素的本地元素,我们将`local`参数设置为1。 如果我们这样做并再次生成前面的输出,我们将收到以下少嵌套版本的输出: ```java Adam,George L. 1947-06-29 ``` 注意,现在``元素在`“http://www.rootns.org”`名称空间中,这是它的父元素的名称空间。 类似地,嵌套程度更高的版本应该是这样的: ```java Adam,George L. 1947-06-29 ``` ## 控制属性是否限定 导出对象时,默认情况下其属性不合格。要使它们合格,请将编写器属性`AttributeQualified`设置为1。下面的示例显示`AttributeQualified`等于0(或尚未设置)的编写器生成的输出: ```java Leiberman,Amanda E. 1988-10-28 ``` 相反,下面的示例显示使用`AttributeQualified`等于1的编写器生成的同一对象: ```java Leiberman,Amanda E. 1988-10-28 ``` 在这两种情况下,元素都是不合格的。 ## 命名空间分配摘要 本节介绍如何为`XML`输出中的任何给定元素确定命名空间。 ### 顶级元素 对于与在顶级导出的InterSystems IRIS类相对应的元素,适用以下规则: 1. 如果为类指定了`Namespace`参数,则元素位于该命名空间中。 2. 如果未指定该参数,则元素位于在生成元素的输出方法(`RootObject()`、`RootElement()`、`Object()`或`Element()`)中指定的命名空间中。 3. 如果未在输出方法中指定命名空间,则元素位于编写器的`DefaultNamespace`属性指定的命名空间中。 4. 如果`DefaultNamespace`属性为空,则元素不在任何命名空间中。 ### 低层元素 要导出的类的子元素受该类的`ELEMENTQUALIFIED`参数影响。如果未设置`ELEMENTQUALIFIED`,则改用编写器属性`ElementQualified`的值;默认情况下,文本格式为1,编码格式为0。 如果元素符合给定类的条件,则该类的子元素将按如下方式分配给命名空间: 1. 如果为父对象指定了`Namespace`参数,则子元素将显式分配给该命名空间。 2. 如果未指定该参数,子元素将显式分配给在生成元素的输出方法(`RootObject()`、`RootElement()`、`Object()`或`Element()`)中指定的命名空间。 3. 如果未在输出方法中指定命名空间,则子元素将显式分配给由编写器的`DefaultNamespace`属性指定的命名空间。 4. 如果`DefaultNamespace`属性为空,则子元素不会显式分配给任何命名空间。
文章
Hao Ma · 十一月 20, 2022

ZPM介绍(2)

## 发布您自己的软件 首先:要发布您的软件,您要支持这个”[命名规范](https://community.intersystems.com/post/objectscript-package-manager-naming-convention)。其中和zmp最相关的是包名和l类名的设计,你要定义成这样: **company.project.subpackage.TheClass.cls** 如果您的Package Name定义是: Company.Project, 有大写字母,对不起,是无法用zpm打包的。 [这个链接](https://community.intersystems.com/post/zpm-simple-implementation-cookbook)给了最简单的例子,但还不详细,我来总结一下: 发布您的软件前,有几件事情要了解: 1. zpm的注册中心并不存代码,存的只是一个到您代码的链接。因此,您得找地方放您的代码。当前最常用的是github。 2. 文件目录的结构 举例:有一个class定义是 `com.tony.Test1.cls`, 你的目录应该这么组织,假设您要放在 `/myDemo`, 那么class应该在`/myDemo/src/com/tony/Test1.cls`。这是使用VSCode组织代码的默认方式,**只有保证这样的目录结构,您才可能用zpm加载代码到iris**. 让我来做个简单的例子。 首先,有这样的class: ```java Class com.tony.Test1 { Property p1; } ``` 我的文件目录设置 ```sh $ ls -l /external/myDemo/src total 4 -rw-r--r-- 1 irisowner irisowner 40 Nov 12 10:00 Test1.cls $ ``` 这时候我来使用zpm打包测试 ### module.xml的生成和加载 第一步,生成module.xml 我们看看最简单的用zpm generate命令生成module的例子: ```sh zpm:USER>generate /external/myDemo/project1 Enter module name: project1 Enter module version: 1.0.0 => Enter module description: Enter module keywords: Enter module source folder: src => Existing Web Applications: /csp/user /terminal /terminalsocket Enter a comma separated list of web applications or * for all: Dependencies: Enter module:version or empty string to continue: zpm:USER> ``` `zpm generate`会把/external/myDemo/project1目录下的文件打包,在这个目录下创建一个module.xml文件, 是这样的: ```sh project1 1.0.0 module src ``` 先等等解释这个xml, 让我们先执行第2步。 第2步: 把文件load到iris ```sh zpm:USER>load /external/myDemo/ [USER|firstdemo] Reload START (/external/myDemo/) [USER|firstdemo] Reload SUCCESS [firstdemo] Module object refreshed. [USER|firstdemo] Validate START [USER|firstdemo] Validate SUCCESS [USER|firstdemo] Compile START [USER|firstdemo] Compile SUCCESS [USER|firstdemo] Activate START [USER|firstdemo] Configure START [USER|firstdemo] Configure SUCCESS [USER|firstdemo] Activate SUCCESS zpm:USER> ``` 您去iris里看看, 确认class已经被loaded 第3步: 删除(optional, 但你可能会用到这个命令) ```sh zpm:USER>uninstall firstdemo [USER|firstdemo] Clean START [USER|firstdemo] Unconfigure START [USER|firstdemo] Unconfigure SUCCESS Deleting class com.tony.Test1 [USER|firstdemo] Clean SUCCESS zpm:USER> ``` 这时候您应该可以发现com.tong.Test1类已经从Iris删除了。 对上面的例子总结一下: 1. 打包是对一个文件夹打包 2. 使用zpm把软件包加载进iris是先找包里面的module.xml文件。通过module.xml里定义的信息来知道包的名字,版本,打包的内容等等。 这个module.xml是在打包的时候用`zpm generate`创建的, 但您也可以自己手工创建,比如copy其他包的module.xml改改, 有时候会更快捷,尤其是您对zpm命令不是很熟悉的时候, 对很多打包的需求,比如后面会提到的定义依赖等等,直接改module.xml比`zpm generate`容易多了。 3. 让我来说说可能的问题: module.xml中``定义了包名*com.PKG*, 加载数据包到iris很成功。但是,如果您还记得,刚刚例子里打包的是class是`com.tony.Test1`,那么如果你再定义一个新的类,叫`com.tiedan.Test1`, 用`zpm generate`一个新module, 名字叫project2(注意第一个打包的module名字是project1), project2的"Resource Name"也还是"com.PKG, 它能正确加载吗? 不会, 你会被告知:"ERROR! Resource 'com.PKG' is already defined as part of module 'project1'; cannot also be listed in module 'project2'" ​ 解决方法:手工将module.xml里面的Resource Name改成“com.tony.PKG"和"com.tiedan.PKG", 这样两个包都能成功加载了。 还有个小问题,Mac用户可能会看到这样的提示: >zpm:USER>load /external/myDemo/project1 > >[USER|project1] Reload START (/external/myDemo/project1/) >[project1] Reload FAILURE >ERROR! Unable to import file '/external/myDemo/project1/src/com/.DS_Store' as this is not a supported type. >zpm:USER> 这是说打包的文件夹下面有.DS_Store文件,而zpm不认识。zpm会把里面认识的文件, 比如.cls文件成功加载, 然后告诉你"Roload FAILURE" 好吧, 到这里我们知道怎么打包和把包加载到iris里, 接着看看什么文件可以被打包。 ### Package可以包含的文件类型 这时候要好好了解module.xml的内容的细节了, 请阅读[技术文档的module.xml部分](https://github.com/intersystems/ipm/wiki/03.-Module.xml#elements)。 其中的resource部分,阐明了您可以打包的内容: 第一部分:可以被加载到iris的文件类型。 > Use the following suffixes for different types of resources: > > .PKG - Package > .CLS - Class > .INC - Include > .MAC - Routine > .LOC - LocalizedErrorMessages > .GBL - Global > .DFI - DeepSee Item 第二部分:jar包 ```xml Copies content of lib folder to Target Copies just desired file to Target ``` 第三部分:UnitTest module.xml的例子里给出的UnitTest部分是这个样子 ```xml ``` 说实话,我还没研究怎么使用`zpm generate`可以做到这一点,能想到的就是手工去修改module.xml文件。 第四部分:Web Application 执行`zpm generate`的时候, 会列出当前命名空间可以使用的Web Application列表。让我重新执行一下打包的第一步,看看结果是什么样子 ```sh zpm:USER>generate /external/myDemo/project1 Enter module name: project11 Enter module version: 1.0.0 => Enter module description: Enter module keywords: Enter module source folder: src => Existing Web Applications: /csp/user /terminal /terminalsocket Enter a comma separated list of web applications or * for all: /csp/user Enter path to csp files for /csp/user: Dependencies: Enter module:version or empty string to continue: zpm:USER> ``` 得出的module.xml里多了如下内容: ```xml src ``` 其中,打包时提问`Enter path to csp files for /csp/user:。 这里,您需要填入的是当前要load的csp文件。比如:您有一个tony.csp要加载,那么您可以在要打包的目录下创建一个子目录“cspfiles", 把tony.csp放在cspfiles目录里, 回答提问的使用这样 `Enter path to csp files for /csp/user:/cspfiles` > 这里用的是相对路径,但格式是绝对路径的格式,我把它看成一个bug。 module.xml中还有其他很多配置的内容,我在后面会介绍包的依赖的部分。 ### 软件包的Package 用package命令,在iris里将软件打包。打包的结果是得倒一个project1-1.0.0.tgz的文件。 `package -v`显示verbose信息,您可以清楚的看到project1-1.0.0.tgz和module.xml的存放位置。 ```sh zpm:USER>project1 package -v [USER|project1] Reload START (/external/myDemo/project1/) Skipping preload - directory does not exist. Load of directory started on 11/20/2022 13:15:17 '*' Loading file /external/myDemo/project1/src/com/tony/Test1.cls as udl Load finished successfully. [USER|project1] Reload SUCCESS [project1] Module object refreshed. [USER|project1] Validate START [USER|project1] Validate SUCCESS [USER|project1] Compile START Compilation started on 11/20/2022 13:15:17 with qualifiers 'd-lck' Compiling class com.tony.Test1 Compiling routine com.tony.Test1.1 Compilation finished successfully in 0.008s. [USER|project1] Compile SUCCESS [USER|project1] Activate START [USER|project1] Configure START [USER|project1] Configure SUCCESS Studio project created/updated: project1.PRJ [USER|project1] Activate SUCCESS [USER|project1] Package START Exporting 'com.tony.Test1.cls' to '/usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0/src/com/tony/Test1.cls' Exported to /usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0/module.xml Module exported to: /usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0/ Module package generated: /usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0.tgz [USER|project1] Package SUCCESS zpm:USER> ``` ### 软件包的Publish 我们并没有权限把软件包直接发布到官方的registry去。您需要去InterSystems的[OpenExchange页面](https://openexchange.intersystems.com),提交您的软件包。如下图填入软件包的信息,Github URL, 注意勾选右下角的"Publish in Package Manager" 。 ![image](/sites/default/files/inline/images/image-20221120215709459.png) 后面, 我会介绍怎么创建自己team的私服, 用zpm publish可以简单的把iris的软件包发布到私服上去,这对一个开发团队共享软件包并方便部署应该是更有吸引力些。 马老师,zpm可以打包出任务计划里的指定任务吗? 正常导出用sql, 执行`call %sys.task_tasklist()`。内部并没有一个表,而是直接存成global形式,而且结构还挺复杂真找还是能找到,那么你可以把这个global打包到zpm。 个人以为这是个错误的方法。导入导出global就不是正常该干的事,尤其是对于%SYS库。 如果我做, 我会写个定义task的程序,打在安装包里,到处都能用。
文章
姚 鑫 · 二月 26, 2021

第四十八章 Caché 变量大全 ^$LOCK 变量

# 第四十八章 Caché 变量大全 ^$LOCK 变量 提供锁名信息。 # 大纲 ``` ^$|nspace|LOCK(lock_name,info_type,pid) ^$|nspace|L(lock_name,info_type,pid) ``` # 参数 - `|nspace|`或`[nspace]` [nspace]可选-扩展SSVN引用,显式名称空间名称或隐含名称空间。必须计算为带引号的字符串,该字符串括在方括号(`[“nspace”]`)或竖线(`|“nspace”|`)中。命名空间名称不区分大小写;它们以大写字母存储和显示。 - lock_name 计算结果为包含锁定变量名称(带下标或无下标)的字符串的表达式。如果是文字,则必须指定为带引号的字符串。 - info_type 可选-解析为所有大写字母指定为带引号字符串的关键字的表达式。info_type指定返回关于`lock_name`的哪种类型的信息。可用选项有“所有者”、“标志”、“模式”和“计数”。作为独立函数调用`^$LOCK`时需要。 - pid 可选-用于“计数”关键字。一个整数,指定锁所有者的进程标识。如果指定,最多为“计数”返回一个列表元素。如果省略(或指定为0),将为持有指定锁的每个所有者返回一个列表元素。`pid`对其他info_type关键字没有影响。 # 描述 `^$LOCK`结构化系统变量返回有关当前命名空间或本地系统上指定命名空间中的锁的信息。可以通过两种方式使用`^$LOCK`: - info_type作为独立函数返回指定锁的信息。 - `$DATA`、`$ORDER`或`$QUERY`函数没有info_type作为参数。 注意:`^$LOCK`从本地系统的锁表中检索锁表信息。它不会从远程服务器上的锁表中返回信息。 ## ECP环境中的`^$LOCK。` - 本地系统:为本地系统持有的锁调用`^$LOCK`时,`^$LOCK`的行为与没有ECP时相同,只有一个例外:info_type的`“FLAGS”`返回一个星号(*),表示锁处于ECP环境中。这意味着一旦锁被释放,远程系统间IRIS实例就能够持有锁。 - 应用服务器:当通过ECP(数据服务器或另一个应用服务器)在一个应用服务器上为另一个服务器上持有的锁调用`^$LOCK`时,`^$LOCK`不返回任何信息。请注意,这是与锁不存在时相同的行为。 - 数据服务器:当在数据服务器上为应用服务器持有的锁调用`^$LOCK`时,`^$LOCK`的行为与本地系统略有不同,如下所示: - “Owner”:如果锁由连接到调用`^$Lock`的数据服务器的应用程序服务器持有,则`^$Lock(LOCKNAME,“OWNER”)`为持有该锁的实例返回`“ECPConfigurationName:MachineName:InstanceName”`,但不标识持有该锁的特定进程。 - `“FLAGS”`:如果锁由连接到调用`^$LOCK`的数据服务器的应用程序服务器持有,则独占锁的`^$LOCK(lockname,“FLAGS”)`将返回`“Z”`标志。这个`“Z”`表示除了ECP环境之外,在InterSystems IRIS中不再使用的一种遗留锁。 - `“mode”`:如果锁由连接到调用`^$lock`的数据服务器的应用程序服务器持有,则独占锁的`^$lock(lockname,“mode”)`返回`“ZAX”`而不是`“X”`。`ZA`是一种遗留锁,除ECP环境外,在系统间IRIS中不再使用。对于共享锁,`^$lock(lockname,“mode”)`返回`“S”`,与本地锁相同。 # 参数 ## nspace 此可选参数允许您使用扩展的SSVN引用在另一个名称空间中指定全局变量。可以显式指定名称空间名称,将其命名为带引号的字符串文字或变量,或者通过指定隐式名称空间。命名空间名称不区分大小写。可以使用方括号语法`[“ USER”]`或环境语法`|“ USER” |`。 nspace分隔符前后不允许有空格。 可以使用以下方法测试是否定义了名称空间: ```java WRITE ##class(%SYS.Namespace).Exists("USER"),! WRITE ##class(%SYS.Namespace).Exists("LOSER") ``` 可以使用`$NAMESPACE`特殊变量来确定当前的名称空间。更改当前名称空间的首选方法是`NEW $NAMESPACE`,然后`SET $NAMESPACE =“nspacename”`。 ## lock_name 该表达式的计算结果为包含锁定变量名称(带下标或未下标)的字符串。使用`LOCK`命令定义一个锁变量(通常是全局变量)。 ## info_type 当将`^$LOCK`用作独立函数时,需要一个`info_type`关键字;当将`^$LOCK`用作另一个函数的参数时,则是一个可选参数。 info_type必须以大写字母指定为带引号的字符串。 - `“OWNER”`返回锁所有者的进程ID(pid)。如果该锁是共享锁,则以逗号分隔列表的形式返回该锁的所有所有者的进程ID。如果指定的锁不存在,则`^$LOCK`返回空字符串。 - `“FLAGS”`返回锁的状态。它可以返回以下值:`“D”`-处于挂起挂起状态; `“P”`-处于锁定挂起状态; `“N”`-这是一个节点锁定,后代未锁定; `“Z”`-此锁处于ZAX模式; `“L”`-锁丢失,服务器不再具有此锁; `“*”`-这是一个远程锁定。如果指定的锁处于正常锁状态或不存在,则`^$LOCK`返回空字符串。 - `“MODE”`返回当前节点的锁定模式。对于排他锁定模式,它返回`“X”`,对于共享锁定模式,它返回`“S”`,对于`ZALLOCATE`锁定模式,它返回`“ZAX”`。如果指定的锁不存在,则`^$LOCK`返回空字符串。 - `“COUNTS”`返回锁的锁计数,指定为二进制列表结构。对于排他锁,列表包含一个元素;对于共享锁,列表包含每个锁所有者的元素。可以使用pid参数仅返回指定锁定所有者的list元素。每个元素都包含所有者的pid,独占模式增量计数和共享模式增量计数。如果独占模式和共享模式的增量计数均为0(或`“”`),则锁定处于`“ZAX”`模式。增量计数后可以跟一个`“D”`,以指示该锁已在当前事务中解锁,但是其释放被延迟(`“D”`),直到事务被提交或回滚为止。如果指定的锁不存在,则`^$LOCK`返回空字符串。 必须使用所有大写字母指定`info_type`关键字。指定无效的`info_type`关键字会生成``错误。 ## pid 锁所有者的进程ID。仅在使用`“COUNTS”`关键字时有意义。用于将“ ”返回值限制为(最多)一个列表元素。 pid在所有平台上均指定为整数。如果pid与`lock_name ^$LOCK`的所有者的进程ID匹配,则返回该所有者的`“COUNTS”`列表元素;如果pid与`lock_name ^$LOCK`的所有者的进程ID不匹配,则返回空字符串。将pid指定为0表示与省略pid相同; `^$LOCK`返回所有`“COUNTS”`列表元素。pid参数与`“OWNER”`,`“FLAGS”`或`“MODE”`关键字一起使用,但被忽略。 # 示例 下面的示例显示由info_type关键字返回的排他锁的值: ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK() ClassMethod LOCK() { LOCK ^B(1,1) ; define lock WRITE !,"lock owner: ",^$LOCK("^B(1,1)","OWNER") WRITE !,"lock flags: ",^$LOCK("^B(1,1)","FLAGS") WRITE !,"lock mode: ",^$LOCK("^B(1,1)","MODE") WRITE !,"lock counts: " ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; delete lock } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK() lock owner: 17824 lock flags: lock mode: X lock counts: 0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E..... ``` 下面的示例显示在递增和递减独占锁时,info_type `“COUNTS”`返回的值如何变化: ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK1() ClassMethod LOCK1() { LOCK ^B(1,1) ; define exclusive lock ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK +^B(1,1) ; increment lock ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK +^B(1,1) ; increment lock again ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; decrement lock ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; decrement lock again ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; delete exclusive lock } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK1() 0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 02 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 03 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 02 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E..... ``` 下面的示例显示在递增和递减共享锁时,info_type`“COUNTS”`返回的值如何变化 ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK2() ClassMethod LOCK2() { LOCK ^S(1,1)#"S" ; define shared lock ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK +^S(1,1)#"S" ; increment lock ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK +^S(1,1)#"S" ; increment lock again ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK -^S(1,1)#"S" ; decrement lock ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK -^S(1,1)#"S" ; decrement lock again ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK -^S(1,1)#"S" ; delete shared lock } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK2() 0000: 0B 01 04 04 A0 45 02 01 03 04 01 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 02 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 03 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 02 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 01 ....??E..... ``` 以下示例显示如何将`^$lock`用作`$DATA`、`$ORDER`和`$QUERY`函数的参数。 ## 作为$DATA的参数 `$DATA(^$|nspace|LOCK(lock_name))` `^$lock`作为`$DATA`的参数返回一个整数值,该值指定锁定名称是否作为节点存在于`^$lock`中。下表显示了`$DATA`可以返回的整数值。 Value| Meaning ---|--- 0 |锁信息不存在 10 |锁信息存在 请注意,在此上下文中使用的`$DATA`只能返回0或10,其中10表示指定的锁存在。它不能确定锁是否有后代,也不能返回1或11。 下面的示例测试当前命名空间中是否存在锁名。第一次写入返回10(锁名存在),第二次写入返回0(锁名不存在): ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK3() ClassMethod LOCK3() { LOCK ^B(1,2) ; define lock WRITE !,$DATA(^$LOCK("^B(1,2)")) LOCK -^B(1,2) ; delete lock WRITE !,$DATA(^$LOCK("^B(1,2)")) } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK3() 10 0 ``` ## 作为`$ORDER`的参数 `$ORDER(^$|nspace|LOCK(lock_name),direction)` `^$lock`作为`$ORDER`的参数,按排序顺序将下一个或上一个`^$lock`锁名节点返回到指定的锁名。如果不存在这样的锁名作为`^$lock`节点,`$ORDER`将返回空字符串。 锁以区分大小写的字符串排序顺序返回。使用数字排序规则以下标树顺序返回命名锁的下标。 Direction参数指定是返回下一个锁名称还是返回上一个锁名称。如果不提供方向参数,InterSystems IRIS会将排序序列中的下一个锁名返回到您指定的锁名。 以下子例程在Samples名称空间中搜索锁,并将锁名称存储在名为locket的本地数组中。 ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK4() ClassMethod LOCK4() { LOCKARRAY SET lname="" FOR I=1:1 { SET lname=$ORDER(^$|"SAMPLES"|LOCK(lname)) QUIT:lname="" SET LOCKET(I)=lname WRITE !,"the lock name is: ",lname } WRITE !,"All lock names listed" QUIT } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK4() the lock name is: ^%SYS("CSP","Daemon") All lock names listed ``` ## 作为`$QUERY`的参数 `$QUERY(^$|nspace|LOCK(lock_name))` `^$lock`作为`$query`的参数,将排序序列中的下一个锁名返回到您指定的锁名。如果没有将下一个锁名定义为`^$lock`中的节点,则`$query`将返回空字符串。 锁以区分大小写的字符串排序顺序返回。使用数字排序规则以下标树顺序返回命名锁的下标。 在下面的示例中,在当前命名空间中(按随机顺序)创建了五个全局锁名称。 ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK5() ClassMethod LOCK5() { LOCK (^B(1),^A,^D,^A(1,2,3),^A(1,2)) WRITE !,"lock name: ",$QUERY(^$LOCK("")) WRITE !,"lock name: ",$QUERY(^$LOCK("^C")) WRITE !,"lock name: ",$QUERY(^$LOCK("^A(1,2)")) } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK5() lock name: ^$LOCK("^%SYS(""CSP"",""Daemon"")") lock name: ^$LOCK("^D") lock name: ^$LOCK("^A(1,2,3)") ``` `$QUERY`将所有全局锁变量名(带下标或无下标)视为字符串,并按字符串排序顺序检索它们。因此,`$QUERY(^$LOCK(“”))`按排序顺序检索第一个锁名:`^$LOCK(“^A”)`或排序序列中位置较高的InterSystems IRIS定义的锁。`$QUERY(^$LOCK(“^C”))`检索排序序列中不存在的`^C`:`^$LOCK(“^D”)`之后的下一个锁名。`$QUERY(^$LOCK(“^A(1,2)”))`检索排序规则序列中它后面的`^$LOCK(“^A(1,2,3)”)`。 想知道锁产生的原因大概有哪些?应该怎么避免呢? 我有个想法,希望结合应用场景来介绍,这样能够明白使用场景。
文章
姚 鑫 · 七月 27, 2022

第九章 REST 服务安全

# 第九章 REST 服务安全 如果 `REST` 服务正在访问机密数据,应该对服务使用身份验证。如果需要为不同的用户提供不同级别的访问权限,还要指定端点所需的权限。 # 为 `REST` 服务设置身份验证 可以对 `IRIS REST` 服务使用以下任何形式的身份验证: - `HTTP` 身份验证标头 — 这是 `REST` 服务的推荐身份验证形式。 - `Web` 会话身份验证 — 其中用户名和密码在 URL 中的问号后面指定。 - `OAuth 2.0` 身份验证 - 请参阅以下小节。 ## `REST` 应用程序和 `OAuth 2.0` 要通过 `OAuth 2.0` 对 `REST` 应用程序进行身份验证,请执行以下所有操作: - 将包含 `REST` 应用程序的资源服务器配置为 `OAuth 2.0` 资源服务器。 - 允许对 `%Service.CSP` 进行委派身份验证。 - 确保将 `Web` 应用程序(用于 `REST` 应用程序)配置为使用委托身份验证。 - 在 `%SYS` 命名空间中创建一个名为 `ZAUTHENTICATE` 的例程。 提供了一个示例例程 `REST.ZAUTHENTICATE.mac`,可以复制和修改它。此例程是 GitHub (https://github.com/intersystems/Samples-Security) 上 Samples-Security 示例的一部分。可以按照“下载用于 IRIS 的示例”中的说明下载整个示例,但在 `GitHub` 上打开例程并复制其内容可能更方便。 在例程中,修改 `applicationName` 的值并根据需要进行其他更改。 ## 指定使用 `REST` 服务所需的权限 为了指定执行代码或访问数据所需的权限, 技术使用基于角色的访问控制 (`RBAC`)。 如果需要为不同的用户提供不同级别的访问权限,请执行以下操作来指定权限: - 修改规范类以指定使用 `REST` 服务或 `REST` 服务中的特定端点所需的权限;然后重新编译。权限是与资源名称组合的权限(例如读取或写入)。 - 使用管理门户: - 定义在规范类中引用的资源。 - 定义提供权限集的角色。例如,角色可以提供对端点的读取访问权限或对不同端点的写入访问权限。一个角色可以包含多组权限。 - 将用户置于其任务所需的所有角色中。 此外,可以使用 `%CSP.REST` 类的 `SECURITYRESOURCE` 参数来执行授权。 # 指定权限 可以为整个 `REST` 服务指定权限列表,也可以为每个端点指定权限列表。为此: 1. 要指定访问服务所需的权限,请编辑规范类中的 `OpenAPI XData` 块。对于 `info` 对象,添加一个名为 `x-ISC_RequiredResource` 的新属性,其值是以逗号分隔的已定义资源列表及其访问模式 (`resource:mode`),这是访问 `REST` 服务的任何端点所必需的。 下面显示了一个示例: ```java "swagger":"2.0", "info":{ "version":"1.0.0", "title":"Swagger Petstore", "description":"A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", "termsOfService":"http://swagger.io/terms/", "x-ISC_RequiredResource":["resource1:read","resource2:read","resource3:read"], "contact":{ "name":"Swagger API Team" }, ... ``` 2. 要指定访问特定端点所需的权限,请将 `x-ISC_RequiredResource` 属性添加到定义该端点的操作对象,如下例所示: ```java "post":{ "description":"Creates a new pet in the store. Duplicates are allowed", "operationId":"addPet", "x-ISC_RequiredResource":["resource1:read","resource2:read","resource3:read"], "produces":[ "application/json" ], ... ``` 3. 编译规范类。此操作重新生成调度类。 ## 使用 `SECURITYRESOURC` 参数 作为附加的授权工具,分派 `%CSP.REST` 子类的类具有 `SECURITYRESOURCE` 参数。 `SECURITYRESOURCE` 的值要么是资源及其权限,要么只是资源(在这种情况下,相关权限是使用)。系统检查用户是否对与 `SECURITYRESOURCE` 关联的资源具有所需的权限。 注意:如果调度类为 `SECURITYRESOURCE` 指定了一个值,并且 `CSPSystem` 用户没有足够的权限,那么这可能会导致登录尝试失败时出现意外的 `HTTP` 错误代码。为防止这种情况发生, 建议您将指定资源的权限授予 `CSPSystem` 用户。
文章
姚 鑫 · 七月 11, 2022

第二章 嵌入式Python概述(二)

# 第二章 嵌入式Python概述(二) # 从 Python 调用 IRIS API 如果使用的是嵌入式 `Python` 并且需要与 `IRIS` 交互,可以使用 `Python shell` 中的 `iris` 模块,或者使用 `Python` 编写的 `IRIS` 类中的方法。要遵循本节中的示例,可以使用 `ObjectScript` 命令 `do ##class(%SYS.Python).Shell()` 从终端会话启动 `Python shell`。 当启动终端会话时,将被放置在 `IRIS` 的 `USER` 命名空间中,将看到提示 `USER>`。但是,如果从 `GitHub` 加载了示例类,则需要在 `SAMPLES` 命名空间中才能访问它们。 在终端中,更改为 `SAMPLES` 命名空间,然后启动 `Python shell`,如下所示: ```java USER>set $namespace = "SAMPLES" SAMPLES>do ##class(%SYS.Python).Shell() Python 3.9.5 (default, Jul 19 2021, 17:50:44) [MSC v.1927 64 bit (AMD64)] on win32 Type quit() or Ctrl-D to exit this shell. >>> ``` 当从终端会话启动 `Python shell` 时,`Python shell` 继承与终端相同的上下文,例如,当前命名空间和用户。局部变量不被继承。 ## 使用类 要从 `Python` 访问 `IRIS` 类,请使用 `iris` 模块来实例化要使用的类。然后,可以像访问 `Python` 类一样使用访问它的属性和方法。 注意:可能习惯于在 `Python` 中导入模块,然后再使用它,例如: ```java >>> import iris ``` 但是,在使用 `%SYS.Python` 类的 `Shell()` 方法运行 `Python shell `时,不需要显式导入 `iris` 模块。继续使用该模块。 以下示例使用系统类 `%Library.File` 的 `ManagerDirectory()` 方法打印 `IRIS` 管理器目录的路径: ```java >>> lf = iris.cls('%Library.File') >>> print(lf.ManagerDirectory()) C:\InterSystems\IRIS\mgr\ ``` 此示例使用系统类 `%SYSTEM.CPU` 的 `Dump()` 方法来显示有关正在运行 `IRIS` 实例的服务器的信息: ```java >>> cpu = iris.cls('%SYSTEM.CPU') >>> cpu.Dump() -- CPU Info for node MYSERVER ---------------------------------------------- Architecture: x86_64 Model: Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz Vendor: Intel # of threads: 4 # of cores: 2 # of chips: 1 # of threads per core: 2 # of cores per chip: 2 MT supported: 1 MT enabled: 1 MHz: 2904 ------------------------------------------------------------------------------ ``` 此示例使用 `GitHub` 上 `Samples-Data` 存储库中的 `Sample.Company` 类。虽然可以使用任何命名空间中以百分号 (`%`) 开头的类(如 `%SYS.Python` 或 `%Library.File`)来访问 `Sample.Company` 类,但如前所述,必须位于 `SAMPLES` 命名空间中。 `Sample.Company` 的类定义如下: ```java Class Sample.Company Extends (%Persistent, %Populate, %XML.Adaptor) { /// The company's name. Property Name As %String(MAXLEN = 80, POPSPEC = "Company()") [ Required ]; /// The company's mission statement. Property Mission As %String(MAXLEN = 200, POPSPEC = "Mission()"); /// The unique Tax ID number for the company. Property TaxID As %String [ Required ]; /// The last reported revenue for the company. Property Revenue As %Integer; /// The Employee objects associated with this Company. Relationship Employees As Employee [ Cardinality = many, Inverse = Company ]; } ``` 此类扩展 `%Library.Persistent`(通常缩写为 `%Persistent`),这意味着此类的对象可以持久保存在 `IRIS` 数据库中。该类还具有多个属性,包括 `Name` 和 `TaxID`,这两个属性都是保存对象所必需的。 尽管不会在类定义中看到它们,但持久类带有许多用于操作此类对象的方法,例如 `%New()`、`%Save()`、`%Id()` 和 `%OpenId()`。但是,`Python` 方法名称中不允许使用百分号 (`%`),因此请改用下划线 (`_`)。 下面的代码创建一个新的 `Company` 对象,设置所需的 `Name` 和 `TaxID` 属性,然后将公司保存在数据库中: ```java >>> myCompany = iris.cls('Sample.Company')._New() >>> myCompany.Name = 'Acme Widgets, Inc.' >>> myCompany.TaxID = '123456789' >>> status = myCompany._Save() >>> print(status) 1 >>> print(myCompany._Id()) 22 ``` 上面的代码使用 `_New()` 方法创建类的实例,并使用 `_Save()` 将实例保存在数据库中。 `_Save()` 方法返回一个状态码。在这种情况下,`1` 表示保存成功。当保存一个对象时, `IRIS` 会为其分配一个唯一 `ID`,可以在以后使用该 `ID` 从存储中检索该对象。 `_Id()` 方法返回对象的 `ID`。 使用类的 `_OpenId()` 方法将对象从持久存储中检索到内存中进行处理: ```java >>> yourCompany = iris.cls("Sample.Company")._OpenId(22) >>> print(yourCompany.Name) Acme Widgets, Inc. ``` 将以下代码添加到类定义中会创建一个 `Print()` 方法,该方法打印当前公司的 `Name` 和 `TaxID`。将 `Language` 关键字设置为 `python` 会告诉类编译器该方法是用 `Python` 编写的。 ```java Method Print() [ Language = python ] { print ('\nName: ' + self.Name + ' TaxID: ' + self.TaxID) } ``` 给定一个 `Company` 对象,可以调用它的 `Print()` 方法,如下所示: ```java >>> yourCompany.Print() Name: Acme Widgets, Inc. TaxID: 123456789 ```
文章
Nicky Zhu · 五月 7, 2022

一个新库引发的血案

我们的一位客户五一期间向使用IRIS搭建的数据流推送一家三甲医院数年的历史数据,导致实施的同事们经历了一系列噩梦,包括但不限与: 1. 由于未通知实施团队有这样规模的数据推送,数据推送过程与全库备份任务重叠。尽管实例和数据流正常运行,但备份任务与数据流争抢IO,导致备份任务不能在预期时间内完成,实施童鞋五一加班处理问题。 2. 为了节省磁盘空间,服务器上部署了定期删除IRIS备份文件的任务,原本能够保持一周的全备+增量备份,但在本次数据暴增的情况下,新的备份尚未完成而旧的全备已被删除,导致问题发生时没有可用于恢复的备份。 3. 由于这次数据推送前未进行数据质量校验,推送的数据全部不合规,但已经历了较长的数据流进行处理全部入库;同时由于备份文件已被删除,无法通过恢复数据库的方法回滚,导致实施童鞋不得不逐条从生产环境三个库的数百张表中挑出问题数据逐一删除,从五一放假结束至今还未完成善后工作。大家可以设想一下,如果备份还在,那么恢复备份就可以了。 因此,我们希望再次提醒各位在前线奋斗的亲们: 1. 善待你的备份。尽管对于大型医院或医疗集团来说,两周的全备+增量备份策略下,备份文件会占据数个TB的存储空间。但在需要回滚时,这几个T的空间能救命。 2. 保持可用的测试环境。尤其是对于可能出现随机数据需求的客户,随机产生数据需求意味着随机出现测试需求。 3. 验证新数据的合规性,永远不要假设新数据一定合规。未经测试的新数据必然毫无悬念地导致新问题。 4. 对于任何批量数据处理任务,请务必提前规划,错开资源(CPU、内存、IO)的抢占,避免抢不到资源的任务饿饭。 5. 保持与最终客户的频繁沟通,所有对于生产环境进行的改动都应该经过项目组评估。虽然客户是上帝,但命运有时很顽皮,生产环境的安全保障也需要客户的合作。 最后,大家都知道InterSystems的IRIS在多数客户的场景下都不需要搭建负载均衡集群,这家客户也不例外,数据流中的数层结点上部署的都是单实例IRIS,通过Mirror实现高可用。在这次新数据的上传过程中,IRIS的数据流自然经历了突如其来的爆发式数据压力,以其中一个实例的消息量为例: 该用户在实例上保存30天的数据,可见在经历了五一的消息暴增之后,该客户的每日平均消息量已超过3300万条每天(实际上我们已经查到其中数天单日消息增量已超过5000万条),而该客户平时的消息量不过数十万条每天。 这次IRIS经洪峰而不倒固然可喜可贺,但相信在需要在客户面前经历各种千夫所指的PM、实施、开发与测试同事一定不希望经历这种惊喜。 Good luck. 值得一篇专门的文章介绍客户应该怎么使用备份以及数据推流
文章
Michael Lei · 六月 23, 2021

Yape - 另一个 pButtons 提取程序(自动创建图表)

> 注(2019 年 6 月):许多内容发生了变化,[最新的详细信息请参见此处](https://community.intersystems.com/post/unpacking-pbuttons-yape-update-notes-and-quick-guides) > 注(2018 年 9 月):自本帖首次发布以来,内容已经有了很大改动,我建议使用 Docker 容器版本,以容器形式运行的项目以及详细信息仍然在 [GitHub 的同一个地址发布](https://github.com/murrayo/yape),您可以下载、运行并根据需要进行修改。 与客户合作进行性能评估、容量规划和故障排除时,我经常解包和查看来自 pButtons 的 Caché 和操作系统指标。 [我不久前发布了一个帖子,介绍了一个用来解包 pButtons 指标的实用工具](https://community.intersystems.com/post/extracting-pbuttons-data-csv-file-easy-charting)(该实用工具使用 unix shell、perl 和 awk 脚本编写),而不是费力地浏览 html 文件,再将需要绘制的部分剪切并粘贴到 excel 中。 虽然这是一个*有用的省时工具*,但还不够完善... 我还使用脚本自动绘制指标图表,以便快速查看并包含在报告中。 但是,这些绘图脚本不容易维护,并且当需要站点特定的配置(例如 iostat 或 Windows perfmon 的磁盘列表)时会变得特别混乱,所以我从未公开发布过绘图实用工具。 不过我现在可以很高兴地说,已经有了简单得多的解决方案。 当我与 Fabian 一起在客户站点查看系统性能时,有了意外发现,[他向我展示了使用实用的 Python 绘图模块所做的工作](https://community.intersystems.com/post/visualizing-data-jungle-part-i-lets-make-graph)。 这是一个比我使用的脚本更灵活、更容易维护的解决方案。 集成 Python 模块进行文件管理和绘制图表的简便性,包括可以分享的交互式 html,意味着输出可以有更大用处。 以 Fabian 的帖子为基础,我编写了 __Yape__,旨在快速简单地提取客户的多种格式的 pButtons 文件,然后绘制图表。 该项目已[在 GitHub 上发布](https://github.com/murrayo/yape),您可以下载、运行并根据需要进行修改。 ## 概述 目前,此过程有_两个_步骤。 ### 步骤 1. `extract_pButtons.py` 从 pButtons 提取感兴趣的部分并写入到 .csv 文件,以便使用 Excel 打开或使用 `graph_pButtons.py` 进行绘图处理。 ### 步骤 2. `graph_pButtons.py` 绘制步骤 1 中创建的文件的图表。 目前,输出可以是 `.png` 形式的线形图或点阵图,也可以是带有平移、缩放、打印等选项的`交互式 .html`。 GitHub 上的 _Readme.md_ 详细介绍了如何设置和运行这两个 python 脚本,并且将是最新的参考。 ## 其他说明 例如:使用向输出和输入目录添加前缀的选项,可以轻松遍历包含一组(例如一个星期)pButtons html 文件的目录,并针对每个 pButtons 文件都输出到一个单独目录。 for i in `ls *.html`; do ./extract_pButtons.py $i -p ${i}_; done for i in `ls *.html`; do ./graph_pButtons.py ./${i}_metrics -p ${i}_; done 在短期内,当我继续撰写有关 [Caché 容量规划和性能](https://community.intersystems.com/post/intersystems-data-platforms-capacity-planning-and-performance-series-index)的系列文章时,我将使用由这些实用工具创建的图表。 我已经在 OSX 上进行了测试,但没有在 Windows 上测试。 您应该能够在 Windows 上安装和运行 Python,请留下您在 Windows 下的经验反馈。 例如,我猜想必须对文件路径斜杠进行更改。 > 注:直到几周前,我都没有用 Python 编写过任何东西,所以如果您是 Python 专家,那么代码中可能会有一些内容并不是最佳做法。 但是,我几乎每天都使用这些脚本,因此我将继续进行改进。 我希望我的 Python 技能会有所提高 — 但是如果您看到一些应该纠正的地方,请随意“教导”我! 如果您发现这些脚本有用,请告诉我,并不时回来看看以获取新功能和更新。
文章
Michael Lei · 十二月 7, 2022

创建基于 FHIR 的表单

Intersystems IRIS for Health 对 FHIR 行业标准提供了出色的支持。主要特点是:1.FHIR 服务器2. FHIR数据库3. REST 和 ObjectScript API 用于 FHIR 资源(患者、问卷、疫苗等)的 CRUD 操作 本文演示了如何使用这些功能,并展示了用于创建和查看表单类型的 FHIR 资源的Angula前端。 第 1 步 - 使用 InterSystems IRIS for Health 部署您的 FHIR 服务器 要创建 FHIR 服务器,您必须将以下说明添加到 iris.script 文件中(来自:https://openexchange.intersystems.com/package/iris-fhir-template) zn "HSLIB" set namespace= "FHIRSERVER" Set appKey = "/fhir/r4" Set strategyClass = "HS.FHIRServer.Storage.Json.InteractionsStrategy" set metadataPackages = $lb ( "hl7.fhir.r4.core@4.0.1" ) set importdir= "/opt/irisapp/src" //Install a Foundation namespace and change to it Do ##class (HS.HC.Util.Installer).InstallFoundation(namespace) zn namespace // Install elements that are required for a FHIR-enabled namespace Do ##class (HS.FHIRServer.Installer).InstallNamespace() // Install an instance of a FHIR Service into the current namespace Do ##class (HS.FHIRServer.Installer).InstallInstance(appKey, strategyClass, metadataPackages) // Configure FHIR Service instance to accept unauthenticated requests set strategy = ##class (HS.FHIRServer.API.InteractionsStrategy).GetStrategyForEndpoint(appKey) set config = strategy.GetServiceConfigData() set config.DebugMode = 4 do strategy.SaveServiceConfigData(config) zw ##class (HS.FHIRServer.Tools.DataLoader).SubmitResourceFiles( "/opt/irisapp/fhirdata/" , "FHIRServer" , appKey) do $System .OBJ.LoadDir( "/opt/irisapp/src" , "ck" ,, 1 ) zn "%SYS" Do ##class (Security.Users).UnExpireUserPasswords( "*" ) zn "FHIRSERVER" zpm "load /opt/irisapp/ -v" : 1 : 1 //zpm "install fhir-portal" halt 使用实用程序类 HS.FHIRServer.Installer,您可以创建 FHIR 服务器。 第 2 步 - 使用 FHIR REST 或 ObjectScript API 读取、更新、删除和查找 FHIR 数据 我喜欢使用 ObjectScript 类 HS.FHIRServer.Service 来执行所有 CRUD 操作。 要从资源类型(表单)中获取所有 FHIR 数据: /// Retreive all the records of questionnaire ClassMethod GetAllQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) set json = pResponse.Json set resp = [] set iter = json.entry. %GetIterator () while iter. %GetNext (.key, .value) { do resp. %Push (value.resource) } write resp. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get all questionnairies" } Quit tSC } 要从 FHIR 数据存储库中获取特定数据项(调查表): /// Retreive a questionnaire by id ClassMethod GetQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) write pResponse.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get the questionnaire" } Quit tSC } 要创建新的 FHIR 资源事件(新的调查表): /// Create questionnaire ClassMethod CreateQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "POST" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on create questionnaire" } Return tSC } 要更新 FHIR 资源(表单): /// Update a questionnaire ClassMethod UpdateQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "PUT" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on update questionnaire" } Return tSC } 要删除 FHIR 资源事件(表单): /// Delete a questionnaire by id ClassMethod DeleteQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "DELETE" do fhirService.DispatchRequest(request, .pResponse) } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on delete the questionnaire" } Quit tSC } 如您所见,您想要创建使用 POST,更新使用 PUT,删除使用 DELETE,查询使用 GET 动词。 第 3 步 - 创建 Angular 客户端以使用您的 FHIR 服务器应用程序 我使用 PrimeNG 创建了一个角度应用程序并安装了包 npm install --save @types/fhir。此包具有映射到 TypeScript 的所有 FHIR 类型。 Angular控制器类: import { Component, OnInit, ViewEncapsulation } from '@angular/core' ; import { ActivatedRoute, Router } from '@angular/router' ; import { Period, Questionnaire } from 'fhir/r4' ; import { ConfirmationService, MessageService, SelectItem } from 'primeng/api' ; import { QuestionnaireService } from './questionnaireservice' ; const QUESTIONNAIREID = 'questionnaireId' ; @Component({ selector: 'app-questionnaire', templateUrl: './questionnaire.component.html', providers: [MessageService, ConfirmationService], styleUrls: ['./questionnaire.component.css'], encapsulation: ViewEncapsulation.None }) export class QuestionnaireComponent implements OnInit { public questionnaire: Questionnaire ; public questionnairies: Questionnaire[] ; public selectedQuestionnaire: Questionnaire ; public questionnaireId: string ; public sub: any ; public publicationStatusList: SelectItem[] ; constructor( private questionnaireService: QuestionnaireService, private router: Router, private route: ActivatedRoute, private confirmationService: ConfirmationService, private messageService: MessageService){ this.publicationStatusList = [ {label: 'Draft', value: 'draft'}, {label: 'Active', value: 'active'}, {label: 'Retired', value: 'retired'}, {label: 'Unknown', value: 'unknown'} ] } ngOnInit() { this.reset() ; this.listQuestionnaires() ; this.sub = this.route.params.subscribe(params => { this.questionnaireId = String(+params[QUESTIONNAIREID]) ; if (!Number.isNaN(this.questionnaireId)) { this.loadQuestionnaire(this.questionnaireId) ; } }) ; } private loadQuestionnaire(questionnaireId) { this.questionnaireService.load(questionnaireId).subscribe(response => { this.questionnaire = response ; this.selectedQuestionnaire = this.questionnaire ; if (!response.effectivePeriod) { this.questionnaire.effectivePeriod = <Period>{} ; } }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load questionnaire.' }) ; }) ; } public loadQuestions() { if (this.questionnaire && this.questionnaire.id) { this.router.navigate(['/question', this.questionnaire.id]) ; } else { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Choose a questionnaire.' }) ; } } private listQuestionnaires() { this.questionnaireService.list().subscribe(response => { this.questionnairies = response ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load the questionnaries.' }) ; }) ; } public onChangeQuestionnaire() { if (this.selectedQuestionnaire && !this.selectedQuestionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { if (this.selectedQuestionnaire && this.selectedQuestionnaire.id) { this.loadQuestionnaire(this.selectedQuestionnaire.id) ; } } } public reset() { this.questionnaire = <Questionnaire>{} ; this.questionnaire.effectivePeriod = <Period>{} ; } public save() { if (this.questionnaire.id && this.questionnaire.id != "" ) { this.questionnaireService.update(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(this.questionnaire.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } else { this.questionnaireService.save(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(resp.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } } public delete(id: string) { if (!this.questionnaire || !this.questionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { this.confirmationService.confirm({ message: ' Do you confirm?', accept: () => { this.questionnaireService.delete(id).subscribe( () => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire deleted.' }) ; this.listQuestionnaires() ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on delete questionnaire.' }) ; } ) ; } }) ; } } } Angular HTML 文件 <p-toast [style]= "{marginTop: '80px', width: '320px'}" ></p-toast> <p-card> <div class = "p-fluid p-formgrid grid" > <div class = "field col-12 lg:col-12 md:col-12" > <p-dropdown id= "dropquestions1" [options]= "questionnairies" [(ngModel)]= "selectedQuestionnaire" (onChange)= "onChangeQuestionnaire()" placeholder= "Select a Questionnaire" optionLabel= "title" [filter]= "true" [showClear]= "true" ></p-dropdown> </div> </div> <p-tabView> <p-tabPanel leftIcon= "fa fa-question" header= "Basic Data" > <div class = "p-fluid p-formgrid grid" > <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtname" >Name</label> <input class = "inputfield w-full" id= "txtname" required type= "text" [(ngModel)]= "questionnaire.name" pInputText placeholder= "Name" > </div> <div class = "field col-7 lg:col-7 md:col-12" > <label for = "txttitle" >Title</label> <input class = "inputfield w-full" id= "txttitle" required type= "text" [(ngModel)]= "questionnaire.title" pInputText placeholder= "Title" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtdate" >Date</label> <p-inputMask id= "txtdate" mask= "9999-99-99" [(ngModel)]= "questionnaire.date" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstatus" >Status</label> <p-dropdown [options]= "publicationStatusList" [(ngModel)]= "questionnaire.status" ></p-dropdown> </div> <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtpublisher" >Publisher</label> <input class = "inputfield w-full" id= "txtpublisher" required type= "text" [(ngModel)]= "questionnaire.publisher" pInputText placeholder= "Publisher" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstartperiod" >Start Period</label> <p-inputMask id= "txtstartperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.start" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtendperiod" >End Period</label> <p-inputMask id= "txtendperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.end" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-12 lg:col-12 md:col-12" > <label for = "txtcontent" >Description</label> <p-editor [(ngModel)]= "questionnaire.description" [style]= "{'height':'100px'}" ></p-editor> </div> </div> <div class = "grid justify-content-end" > <button pButton pRipple type= "button" label= "New Record" (click)= "reset()" class = "p-button-rounded p-button-success mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Save" (click)= "save()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Delete" (click)= "delete(questionnaire.id)" class = "p-button-rounded p-button-danger mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Questions" (click)= "loadQuestions()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> </div> </p-tabPanel> </p-tabView> </p-card> <p-confirmDialog #cd header= "Atenção" icon= "pi pi-exclamation-triangle" > <p-footer> <button type= "button" pButton icon= "pi pi-times" label= "Não" (click)= "cd.reject()" ></button> <button type= "button" pButton icon= "pi pi-check" label= "Sim" (click)= "cd.accept()" ></button> </p-footer> </p-confirmDialog> 角度服务类 import { Injectable } from '@angular/core' ; import { HttpClient, HttpHeaders } from '@angular/common/http' ; import { Observable } from 'rxjs' ; import { environment } from 'src/environments/environment' ; import { take } from 'rxjs/operators' ; import { Questionnaire } from 'fhir/r4' ; @Injectable({ providedIn: 'root' }) export class QuestionnaireService { private url = environment.host2 + 'questionnaire' ; constructor(private http: HttpClient) { } public save(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.post<Questionnaire>(this.url, Questionnaire).pipe(take( 1 )) ; } public update(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.put<Questionnaire>(`${this.url}/${Questionnaire.id}`, Questionnaire).pipe(take( 1 )) ; } public load(id: string): Observable<Questionnaire> { return this.http.get<Questionnaire>(`${this.url}/${id}`).pipe(take( 1 )) ; } public delete(id: string): Observable<any> { return this.http.delete(`${this.url}/${id}`).pipe(take( 1 )) ; } public list(): Observable<Questionnaire[]> { return this.http.get<Questionnaire[]>(this.url).pipe(take( 1 )) ; } } 第 4 步 - 实际应用 1. 转到 https://openexchange.intersystems.com/package/FHIR-Questionnaires 应用程序。 2. clone/git pull repo 到任何本地目录 $ git clone https://github.com/yurimarx/fhir-questions.git 3、在该目录下打开终端,运行: $ docker-compose up -d 4.打开网络应用程序: http://localhost:52773/fhirquestions/index.html 截图:
文章
Michael Lei · 七月 6, 2021

精华文章--虚拟化大型数据库 - VMware CPU 容量规划

供应商或内部团队要求说明如何为 VMware vSphere 上运行的_大型生产数据库_进行 CPU 容量规划。 总的来说,在调整大型生产数据库的 CPU 规模时,有几个简单的最佳做法可以遵循: - 为每个物理 CPU 核心规划一个 vCPU。 - 考虑 NUMA 并按理想情况调整虚拟机规模,以使 CPU 和内存对于 NUMA 节点是本地的。 - 合理调整虚拟机规模。 仅在需要时才添加 vCPU。 通常,这会引出几个常见问题: - 由于使用超线程技术,VMware 创建的虚拟机的 CPU 数量可以是物理 CPU 数量的两倍。 那不就是双倍容量吗? 创建的虚拟机不应该有尽可能多的 CPU 吗? - 什么是 NUMA 节点? 我应该在意 NUMA 吗? - 虚拟机应该合理调整规模,但我如何知道什么时候合理? 我以下面的示例回答这些问题。 但也要记住,最佳做法并不是一成不变的。 有时需要做出妥协。 例如,大型生产数据库虚拟机很可能不适合 NUMA 节点,但我们会看到,其实是没问题的。 最佳做法是指必须针对应用程序和环境进行评估和验证的准则。 虽然本文中的示例是在 InterSystems 数据平台上运行的数据库,但概念和规则通常适用于任何大型(怪兽)虚拟机的容量和性能规划。 有关虚拟化最佳做法以及有关性能和容量规划的更多帖子,请参见 [InterSystems 数据平台和性能系列的其他帖子列表](https://cn.community.intersystems.com/post/intersystems-数据平台的容量规划和性能系列文章)。 # 怪兽虚拟机 本帖主要是关于部署_怪兽虚拟机_,有时也称为 _Wide 虚拟机_。 高事务数据库的 CPU 资源要求意味着它们通常部署在怪兽虚拟机上。 > 怪兽虚拟机是指虚拟 CPU 或内存多于物理 NUMA 节点的虚拟机。 # CPU 架构和 NUMA 当前的英特尔处理器架构采用非统一内存架构 (NUMA)。 例如,本帖中用来运行测试的服务器有: - 两个 CPU 插槽,每个插槽一个 12 核处理器(英特尔 E5-2680 v3)。 - 256 GB 内存(16 条 16GB RDIMM) 每个 12 核处理器都有自己的本地内存(128GB RDIMM 及本地高速缓存),还可以访问同一主机中其他处理器上的内存。 每个由 CPU、CPU 高速缓存和 128GB RDIMM 内存组成的 12 核套装都是一个 NUMA 节点。 为了访问其他处理器上的内存,NUMA 节点通过快速互连来连接。 处理器上运行的进程访问本地 RDIMM 和缓存内存的延迟比跨互连访问其他处理器上的远程内存的延迟要低。 跨互连访问会增加延迟,因此性能不一致。 同样的设计也适用于具有两个以上插槽的服务器。 一台四插槽英特尔服务器有四个 NUMA 节点。 ESXi 了解物理 NUMA,ESXi CPU 调度器设计为优化 NUMA 系统的性能。 ESXi 使性能最大化的方法之一是在物理 NUMA 节点上创建数据本地性。 在我们的示例中,如果虚拟机有 12 个 vCPU,并且内存不到 128GB,ESXi 将分配该虚拟机在一个物理 NUMA 节点上运行。 这就形成了规则: > 如果可能,将虚拟机规模调整为使 CPU 和内存对于 NUMA 节点是本地的。 如果需要比 NUMA 节点规模大的怪兽虚拟机也没有问题,ESXi 可以很好地自动计算和管理要求。 例如,ESXi 将创建能够智能调度到物理 NUMA 节点上的虚拟 NUMA 节点 (vNUMA),以获得最佳性能。 vNUMA 结构对操作系统公开。 例如,如果您有一台具有两个 12 核处理器的主机服务器和一个具有 16 个 vCPU 的虚拟机,ESXi 可能会使用每个处理器上的 8 个物理核心来调度虚拟机 vCPU,操作系统(Linux 或 Windows)将看到两个 NUMA 节点。 同样重要的是,应合理调整虚拟机的规模,并且分配的资源不要超过所需的资源,否则会导致资源浪费和性能损失。 除了有助于调整 NUMA 的规模,具有高(但安全的)CPU 利用率的 12 vCPU 虚拟机比具有中低 CPU 利用率的 24 vCPU 虚拟机更高效、性能更好,特别是该主机上还有其他虚拟机需要调度并且争用资源时。 这也再次强化了该规则: > 合理调整虚拟机规模。 __注意:__英特尔和 AMD 的 NUMA 实现有区别。 AMD 每个处理器有多个 NUMA 节点。 我已经有一段时间没有在客户服务器中看到 AMD 处理器了,但是如果你有这些处理器,请检查 NUMA 布局,作为规划的一部分。 ## Wide 虚拟机和授权 为实现最佳 NUMA 调度,请配置 Wide 虚拟机; 2017 年 6 月更正:按每个插槽 1 个 vCPU 配置虚拟机。 例如,默认情况下,一个具有 24 个 vCPU 的虚拟机应配置为 24 个 CPU 插槽,每个插槽一个核心。 > 遵守 VMware 最佳做法规则。 请参见 [VMware 博客上的这篇文章以查看示例。 ](https://blogs.vmware.com/performance/2017/03/virtual-machine-vcpu-and-vnuma-rightsizing-rules-of-thumb.html) 该 VMware 博客文章进行了详细介绍,但是作者 Mark Achtemichuk 建议遵循以下经验法则: - 虽然有许多高级 vNUMA 设置,但只有极少数情况下需要更改其默认值。 - 总是将虚拟机 vCPU 数配置为反映每插槽核心数,直到超过单个物理 NUMA 节点的物理核心数。 - 当需要配置的 vCPU 数量超过 NUMA 节点中的物理核心数量时,将 vCPU 均匀分配到最少数量的 NUMA 节点上。 - 当虚拟机规模超过物理 NUMA 节点时,不要分配奇数数量的 vCPU。 - 不要启用 vCPU 热添加,除非您不介意禁用 vNUMA。 - 不要创建规模大于主机物理核心总数的虚拟机。 Caché 授权以核心数为准,因此这不是问题,但是对于除 Caché 以外的软件或数据库,指定虚拟机有 24 个插槽可能会对软件授权产生影响,因此必须与供应商核实。 # 超线程和 CPU 调度器 超线程 (HT) 经常在讨论中出现,我听过“超线程使 CPU 核心数量翻倍”。 这在物理层面上显然是不可能的,物理核心有多少就是多少。 超线程应该被启用,并会提高系统性能。 预计应用程序性能可能会提高 20% 或更多,但实际数字取决于应用程序和工作负载。 但肯定不会翻倍。 正如我在 [VMware 最佳实践](https://cn.community.intersystems.com/post/intersystems-数据平台和性能-–-第-9-篇-intersystems-iris-vmware-最佳实践指南)中所述,_调整大型生产数据库虚拟机规模_的一个很好的起点是假定 vCPU 拥有服务器上完整的物理核心专用资源 — 在进行容量规划时基本忽略超线程。 例如: > 对于一台 24 核主机服务器,可规划总共多达 24 个 vCPU 的生产数据库虚拟机,且可能还有余量。 在您花时间监测应用程序、操作系统和 VMware 在峰值处理期间的性能后,您可以决定是否进行更高度的虚拟机整合。 在最佳做法帖子中,我将规则表述为: > 一个物理 CPU(包括超线程)= 一个 vCPU(包括超线程)。 ## 为什么超线程不会使 CPU 翻倍 英特尔至强处理器上的超线程是在一个物理核心上创建两个_逻辑_ CPU 的方法。 操作系统可以有效地针对两个逻辑处理器进行调度 — 如果一个逻辑处理器上的进程或线程正在等待,例如等待 IO,则物理 CPU 资源可以被另一个逻辑处理器使用。 在任何时间点都只能有一个逻辑处理器运行,因此虽然物理核心得到了更有效的利用,但_性能并没有翻倍_。 在主机 BIOS 中启用超线程后,当创建虚拟机时,可以为每个超线程逻辑处理器配置一个 vCPU。 例如,在一台启用了超线程的物理 24 核服务器上,可以创建具有多达 48 个 vCPU 的虚拟机。 ESXi CPU 调度器将通过首先在独立的物理核心上运行虚拟机进程来优化处理(同时仍然考虑 NUMA)。 在以后的帖子中,我将探讨在怪兽数据库虚拟机上分配比物理核心数更多的 vCPU 是否有助于扩展。 ### 协同停止和 CPU 调度 在监测主机和应用程序性能后,您可以决定是否让主机 CPU 资源过载。 这是否是一个好主意在很大程度上取决于应用程序和工作负载。 了解调度器和要监测的关键指标有助于确保没有使主机资源过载。 我有时听说,要让虚拟机正常运行,空闲逻辑 CPU 的数量必须与虚拟机中的 vCPU 数量相同。 例如,一个 12 vCPU 虚拟机必须“等待”12 个逻辑 CPU“可用”,才能继续执行。 不过应该注意,ESXi 在版本 3 之后就不是这样了。 ESXi 对 CPU 使用宽松的协同调度,以提高应用程序性能。 由于多个协作线程或进程经常相互同步,不一起调度它们可能会增加操作的延迟。 例如,在自旋循环中,一个线程等待被另一个线程调度。 为了获得最佳性能,ESXi 尝试将尽可能多的同级 vCPU 一起调度。 但是,当有多个虚拟机在整合环境中争用 CPU 资源时,CPU 调度器可以灵活地调度 vCPU。 如果一些 vCPU 的进展比同级 vCPU 领先太多(这个时间差称为偏移),领先的 vCPU 将决定是否停止自身(协同停止)。 请注意,协同停止(或协同启动)的是 vCPU,不是整个虚拟机。 这种机制即使在资源有些过载的情况下也非常有效,但正如您所预期,CPU 资源过载太多将不可避免地影响性能。 我在后面的示例 2 中展示了一个过载和协同停止的例子。 记住,这不是虚拟机之间全力争夺 CPU 资源的竞赛;ESXi CPU 调度器的工作是确保 CPU 共享、保留和限制等策略被遵守,同时最大限度地提高 CPU 利用率,并确保公平性、吞吐量、响应速度和可伸缩性。 关于使用保留和共享来确定生产工作负载优先级的讨论不在本帖范围之内,而且取决于应用程序和工作负载组合。 如果我以后发现任何特定于 Caché 的建议,我可能会重新讨论这个话题。 有许多因素会影响到 CPU 调度器,本节只是简单提一下。 要深入了解,请参见帖子末尾的参考资料中的 VMware 白皮书及其他链接。 # 示例 为了说明不同的 vCPU 配置,我使用一个基于浏览器的高事务速率医院信息系统应用程序运行了一系列基准测试。 与 VMware 开发的 DVD 商店数据库基准测试的概念类似。 基准测试的脚本是根据现场医院实施的观测值和指标创建的,包括高使用率的工作流程、事务和使用最多系统资源的组件。 其他主机上的驱动虚拟机以设置的工作流程事务速率执行具有随机输入数据的脚本,来模拟 Web 会话(用户)。 1 倍速率的基准为基线。 速率可以按比例递增和递减。 除了数据库和操作系统指标外,一个很好的用来衡量基准数据库虚拟机性能的指标是在服务器上测量的组件(也可以是事务)响应时间。 一个组件示例是一部分最终用户屏幕。 组件响应时间增加意味着用户将开始看到应用程序响应时间变差。 性能良好的数据库系统必须为最终用户提供_一致的_高性能。 在下面的图表中,我针对一致的测试性能进行测量,并通过对 10 个最慢的高使用率组件的响应时间取平均值来表示最终用户体验。 预计平均组件响应时间为亚秒级,用户屏幕可能由一个组件组成,或者复杂的屏幕可能有多个组件。 > 请记住,您始终针对峰值工作负载进行规模调整,并且为意外的活动峰值留出缓冲区。 我通常以平均 80% 的峰值 CPU 利用率为目标。 基准测试硬件和软件的完整列表在帖子末尾。 ## 示例 1. 合理调整规模 - 每个主机一个怪兽虚拟机 可以创建一个可以使用主机服务器所有物理核心的数据库虚拟机,例如 24 物理核心主机上的 24 vCPU 虚拟机。 数据库虚拟机不会在 Caché 数据库镜像中“裸机”运行服务器以实现 HA,也不会引入操作系统故障转移集群的复杂性,而是包含在 vSphere 集群中实现管理和 HA,例如 DRS 和 VMware HA。 我见过有客户遵循老派的思维,根据五年硬件寿命结束时的预期容量来确定主数据库虚拟机的规模,但从上文可知,最好合理调整规模;如果虚拟机没有过度调整,性能和整合度会更好,并且管理 HA 将更容易;如果需要维护或主机出现故障,并且数据库怪兽虚拟机必须迁移或在其他主机上重启,想想俄罗斯方块的玩法就知道了。 如果预计事务速率显著增加,可以在计划维护期间提前增加 vCPU。 > 注意,“热添加”CPU 选项会禁用 vNUMA,因此不要将其用于怪兽虚拟机。 考虑下图显示的在 24 核主机上进行的一系列测试。 对于这个 24 核系统,3 倍事务速率是甜蜜点和容量规划目标。 - 主机上运行一个虚拟机。 - 使用了四种虚拟机规模来展示 12、24、36 和 48 vCPU 的性能。 - 尽可能对每种虚拟机规模都运行一系列事务速率(1 倍、2倍、3 倍、4 倍、5 倍)。 - 性能/用户体验以组件响应时间(条形图)的形式显示。 - 客户机虚拟机的 CPU 利用率百分比为平均值(线条)。 - 所有虚拟机规模中,主机 CPU 利用率都在 4 倍速率时达到 100%(红色虚线)。 ![24 物理核心主机 单个客户机虚拟机平均 CPU 百分比和组件响应时间 ](https://community.intersystems.com/sites/default/files/inline/images/single_guest_vm.png "单个客户机虚拟机") 这个图表中有许多信息,但我们可以关注几个有趣的事情。 - 24 vCPU 虚拟机(橙色)平稳地增加到目标 3 倍事务速率。 在 3 倍速率时,客户机内虚拟机的平均 CPU 利用率为 76%(峰值为 91% 左右)。 主机 CPU 利用率并不比客户机虚拟机高多少。 在 3 倍速率之前,组件响应时间非常稳定,因此用户很满意。 就我们的目标事务速率而言 — _这个虚拟机已合理调整规模_。 关于合理规模调整先说这么多,那么增加 vCPU 也就是使用超线程又会如何。 性能和可伸缩性有可能翻倍吗? 简短回答是_不可能!_ 在这种情况下,可以通过查看 4 倍以上速率的组件响应时间来了解答案。 虽然在分配了更多逻辑核心 (vCPU) 后性能“更好”,但仍然不平稳,不像 3 倍速率之前那样一致。 4 倍速率时,用户将报告响应时间变慢,无论分配多少个 vCPU。 请记住,在 4 倍速率时,_主机_曲线已经持平于 100% CPU 利用率,如 vSphere 所报告。 在 vCPU 数量较多的情况下,即使客户机内 CPU 指标 (vmstat) 报告低于 100% 利用率,对于物理资源来说情况也并非如此。 请记住,客户机操作系统不知道它是虚拟化的,它只是报告它所看到的资源。 另外,客户机操作系统也看不到超线程,所有 vCPU 都表现为物理核心。 关键是,数据库进程(在 3 倍事务速率时有 200 多个 Caché 进程)非常繁忙,并且非常高效地使用处理器,逻辑处理器没有很多空闲资源来调度更多工作,或将更多虚拟机整合到该主机。 例如,很大一部分 Caché 处理是在内存中进行的,因此没有很多 IO 等待。 所以,虽然可以分配比物理核心更多的 vCPU,但由于主机已经被 100% 利用,并不会获益许多。 Caché 非常擅长处理高工作负载。 即使主机和虚拟机的 CPU 利用率达到 100%,应用程序仍在运行,并且事务速率仍在提高 — 扩展不是线性的,如我们所见,响应时间越来越长,用户体验将受到影响 — 但应用程序不会“一落千丈”,尽管情况不是很好,但用户仍可以工作。 如果您的应用程序对响应时间不是那么敏感,那么很高兴地告诉您,您可以将其推向边缘甚至更远,并且 Caché 仍然可以安全地工作。 > 请记住,您不会想要以 100% CPU 运行数据库虚拟机或主机。 您需要容量来应对虚拟机的意外峰值和增长,而 ESXi 虚拟机监控程序需要资源来进行所有网络、存储和其他活动。 我总是针对 80% CPU 利用率的峰值进行规划。 即便如此,vCPU 的规模最多也只调整到物理核心数,这样即使在极端情况下,仍然有余量让 ESXi 虚拟机监控程序处理逻辑线程。 > 如果您运行超融合 (HCI) 解决方案,还必须考虑主机级别的 HCI CPU 要求。 有关详细信息,请参见我[先前关于 HCI](https://community.intersystems.com/post/intersystems-data-platforms-and-performance-%E2%80%93-part-8-hyper-converged-infrastructure-capacity "previous post on HCI") 的帖子。 部署在 HCI 上的虚拟机的基本 CPU 规模调整与其他虚拟机相同。 请记住,您必须在您自己的环境中使用您的应用程序验证和测试所有内容。 ## 示例 2. 资源过载 我看到过客户站点报告应用程序性能“慢”,而客户机操作系统却报告有空闲的 CPU 资源。 记住,客户机操作系统并不知道它是虚拟化的。 不幸的是,客户机内指标(例如 vmstat 在 pButtons 中报告的指标)可能具有欺骗性,您还必须获得主机级指标和 ESXi 指标(例如 `esxtop`)才能真正了解系统运行状况和容量。 如上面的图表所示,当主机报告 100% 利用率时,客户机虚拟机可能报告较低的利用率。 36 vCPU 虚拟机(红色)在 4 倍速率时报告 80% 平均 CPU 利用率,而主机报告 100%。 即使规模调整合理的虚拟机也可能出现资源短缺的情况,例如,如果在启动后有其他虚拟机迁移到主机上,或者由于 DRS 规则配置不当而导致资源过载。 为了显示关键指标,在下面的一系列测试中,我进行了以下配置: - 主机上运行两个数据库虚拟机。 - - 一个 24 vCPU 虚拟机以恒定的 2 倍事务速率运行(图表上未显示)。 - - 一个 24 vCPU 虚拟机以 1 倍、2 倍、3 倍事务速率运行(图表上显示这些指标)。 在另一个数据库使用资源的情况下;在 3 倍速率时,客户机操作系统 (RHEL 7) vmstat 只报告 86% 平均 CPU 利用率,运行队列大小平均只有 25。 然而,该系统的用户将大声抱怨,因为组件响应时间随着进程变慢而迅速增加。 如下图所示,协同停止和就绪时间说明了为什么用户性能如此糟糕。 就绪时间 (`%RDY`) 和协同停止 (`%CoStop`) 指标显示 CPU 资源在目标 3 倍速率下大幅过载。 这实际并不奇怪,因为_主机_以 2 倍速率运行(其他虚拟机),_而_该数据库虚拟机以 3 倍速率运行。 ![](https://community.intersystems.com/sites/default/files/inline/images/overcommit_3.png "过载的主机") 该图表明,当主机上的 总 CPU 负载增加时,就绪时间也会增加。 > 就绪时间是指虚拟机已准备好运行,但由于 CPU 资源不可用而无法运行的时间。 协同停止也会增加。 没有足够的空闲逻辑 CPU 来允许数据库虚拟机运行(正如我在上面的超线程部分详细说明的那样)。 最终结果是由于对物理 CPU 资源的争用而导致处理延迟。 我曾在一个客户站点看到过这种情况,当时通过 pButtons 和 vmstat 获取的支持视图只显示了虚拟化的操作系统。 虽然 vmstat 报告还有 CPU 余量,但用户的性能体验非常糟糕。 这里的教训是,直到 ESXi 指标和主机级视图可用,才能诊断出真正的问题;一般的集群 CPU 资源短缺导致的 CPU 资源过载,以及使情况变得更糟的不良 DRS 规则,会使高事务数据库虚拟机一起迁移并使主机资源不堪重负。 ## 示例 3. 资源过载 在此示例中,我使用了一个以 3 倍事务速率运行的基准 24 vCPU 数据库虚拟机,然后使用两个以恒定 3 倍事务速率运行的 24 vCPU 数据库虚拟机。 虚拟机的平均基准 CPU 利用率(见上面的示例 1)为 76%,主机则为 85%。 单个 24 vCPU 数据库虚拟机会使用全部 24 个物理处理器。 运行两个 24 vCPU 虚拟机意味着这两个虚拟机将争用资源,并使用服务器上的全部 48 个逻辑执行线程。 ![](https://community.intersystems.com/sites/default/files/inline/images/overcommit_2vm.png "过载的主机") 请记住,在运行单个虚拟机时,主机并没有被 100% 利用,我们仍然可以看到,当两个非常繁忙的 24 vCPU 虚拟机试图使用主机上的 24 个物理核心(即使开启了超线程)时,吞吐量和性能显著下降。 尽管 Caché 非常有效地使用了可用的 CPU 资源,但每个虚拟机的数据库吞吐量仍然下降了 16%,更重要的是,组件(用户)响应时间增加了 50% 以上。 ## 总结 本帖的目的是回答几个常见问题。 要深入了解 CPU 主机资源和 VMware CPU 调度器,请参见下面的参考部分。 虽然有许多专业级的调整,并且要深入研究 ESXi 才能榨干系统的最后一点性能,但基本规则非常简单。 对于_大型生产数据库_: - 为每个物理 CPU 核心规划一个 vCPU。 - 考虑 NUMA 并按理想情况调整虚拟机规模,以使 CPU 和内存对于 NUMA 节点是本地的。 - 合理调整虚拟机规模。 仅在需要时才添加 vCPU。 如果您想要整合虚拟机,请记住,大型数据库非常繁忙,在高峰期会大量使用 CPU(物理和逻辑)。 在您的监视系统告诉您安全之前,不要超额预定 CPU。 ## 参考 - [VMware 博客 - 怪兽虚拟机何时过载 vCPU:pCPU](https://blogs.vmware.com/vsphere/2014/02/overcommit-vcpupcpu-monster-vms.html) - [2016 NUMA 深入研究系列介绍](http://frankdenneman.nl/2016/07/06/introduction-2016-numa-deep-dive-series) - [VMware vSphere 5.1 中的 CPU 调度器](http://www.vmware.com/content/dam/digitalmarketing/vmware/en/pdf/techpaper/vmware-vsphere-cpu-sched-performance-white-paper.pdf) ## 测试 我在一个 vSphere 集群上运行了本帖中的示例,该集群包括连接到一个全闪存阵列的双处理器 Dell R730。 在示例运行期间,网络或存储没有出现瓶颈。 - Caché 2016.2.1.803.0 PowerEdge R730 - 2 个 Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz - 16 条 16GB RDIMM,2133 MT/s,双列,x4 数据宽度 - SAS 12Gbps HBA 外部控制器 - 超线程 (HT) 开启 PowerVault MD3420,12G SAS,2U-24 驱动器 - 24 个 960GB 固态硬盘 SAS 读取密集型 MLC 12Gbps 2.5 英寸热拔插驱动器,PX04SR - 2 个控制器,12G SAS,2U MD34xx,8G 缓存 VMware ESXi 6.0.0 build-2494585 - 按照最佳实践配置虚拟机;VMXNET3、PVSCSI 等 RHEL 7 - 大页面 基准 1 倍速率下平均每秒 700,000 gloref(每秒数据库访问次数)。 24 vCPU 在 5 倍速率下平均每秒超过 3,000,000 gloref。 测试以老化方式进行,直到达到稳定的性能,然后进行 15 分钟采样并取平均值。 > 这些示例只是为了说明理论,您必须使用自己的应用程序进行验证!