搜索​​​​

清除过滤器
文章
姚 鑫 · 二月 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)”)`。 想知道锁产生的原因大概有哪些?应该怎么避免呢? 我有个想法,希望结合应用场景来介绍,这样能够明白使用场景。
文章
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. 值得一篇专门的文章介绍客户应该怎么使用备份以及数据推流
文章
姚 鑫 · 七月 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` 用户。
文章
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 · 九月 20, 2023

基于IRISI对您的敏感数据进行令牌化/代币化

根据剑桥词典的解释,令牌化数据是“用令牌(=代表第一个数据的不同数据)替换隐私数据,以防止隐私信息被不被允许做的人看到”(https://dictionary.cambridge.org/pt/dicionario/ingles/tokenize)。如今,一些公司,尤其是金融和医疗保健领域的公司,正在将其数据令牌/代币化作为满足网络安全和数据隐私(GDPR、CCPA、HIPAA 和 LGPD)要求的重要策略。但是,为什么不使用加密呢?保护敏感数据的令牌化过程比数据加密更常用,原因如下: 更好的性能:在密集的操作处理中动态加密和解密数据会提高性能并需要更多的处理器能力。 测试:可以标记生产数据库并复制到测试数据库,并维护适合更真实的单元和功能测试的测试数据。 更好的安全性:如果黑客破解或获得密钥,所有加密数据都将可用,因为加密是一个可逆过程。令牌化过程是不可逆的。如果您需要从令牌化数据中获取原始数据,则需要维护一个安全且独立的数据库来链接到原始数据和令牌化数据。 令牌化架构 令牌化架构需要两个数据库:应用程序数据库(App DB)用于存储令牌化数据和来自业务的其他数据;令牌数据库(Token Database)用于存储原始值和令牌化值,因此当您需要时,您的应用程序可以获得原始值以向用户显示。还有一个 令牌生成器 REST API 用于标记敏感数据、存储到令牌数据库中并返回票证。应用程序将票据、令牌化数据和其他数据存储在应用程序数据库中。参见架构图: 分词器应用程序 了解它在令牌化应用程序中的工作原理: https://openexchange.intersystems.com/package/Tokenizator。 该应用程序是一个用于标记化的 REST API: 对任何数值显示为“*”号。示例:信用卡 4450 3456 1212 0050 至 4450 **** **** 0050。 任何真实的IP地址都是假值。示例:192.168.0.1 到 168.1.1.1。 任何人的数据都是假的。示例:地址为北京海淀的张三到地址为西藏拉萨的仓央嘉措。 任何数值为假数值。示例:300.00 到 320.00。 任何信用卡数据都是虚假的信用卡号码。示例:4450 3456 1212 0050 至 4250 2256 4512 5050。 任何值到哈希值。示例:dfgdgasdrrrdd123 的系统架构师。 正则表达式的任何值。示例:使用正则表达式规则 [AZ]{2}-\\d{5}-[az]{5} 将 EI-54105-tjfdk 转换为 AI-44102-ghdfg。 如果您需要任何其他选项,请在 github 项目上提交问题。 要对值进行标记并在之后获取原始值,请按照以下步骤操作: 打开您的 Postman 或从您的应用程序使用此 API。 使用 STARS、PERSON、NUMBER、CREDITCARD、HASH、IPADDRESS 和 REGEX 方法对此敏感数据样本创建 令牌化请求: 方法:POST 网址: http://localhost:8080/token/tokenize 正文(JSON): [ { "tokenType":"STARS", "originalValueString":"545049405679", "settings": { "starsPosition":"1", "starsQuantity":"4" } }, { "tokenType":"IPADDRESS", "originalValueString":"192.168.0.1", "settings": { "classType":"CLASS_B", "ipSize":"4" } }, { "tokenType":"PERSON", "originalValueString":"Yuri Marx Pereira Gomes", "settings": { "localeLanguage":"en", "localeCountry":"US", "withAddress":"true", "withEmail":"true" } }, { "tokenType":"NUMBER", "originalValueNumber":300.0, "settings": { "minRange":"100.0", "maxRange":"400.0" } }, { "tokenType":"CREDITCARD", "originalValueString":"4892879268763190", "settings": { "type":"VISA" } }, { "tokenType":"HASH", "originalValueString":"System Architect" }, { "tokenType":"REGEX", "originalValueString":"EI-54105-tjfdk", "settings": { "regex":"[A-Z]{2}-\\d{5}-[a-z]{5}" } } ] 查看结果。您将获得一个令牌化的数值 (tokenizedValueString) 并将其存储到本地数据库中。 从响应中复制票证(将其与令牌化化值一起存储到本地数据库中) 现在凭票证即可获得原数值。创建一个请求以使用票证获取原始值: 方法:获取 网址: http://localhost:8080/token/query-ticket/TICKET-VALUE-HERE 查看您的原始值 生成的所有令牌都存储到 InterSystems IRIS Cloud SQL 中,以便您能够可靠地获得原始值。 欢迎体验!
文章
姚 鑫 · 七月 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 ```
文章
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 截图:
文章
Lilian Huang · 九月 7, 2023

Docker 简介 - 第 2 部分(Docker Compose、Docker File、Docker Volume)

您好!社区的各位老师, 在我的上一篇文章中,我们学习了以下主题: 什么是 Docker? Docker 的一些好处 Docker 是如何工作的? Docker 镜像 Docker容器 Docker 镜像存储库 InterSystems 的 Docker 镜像存储库 Docker安装 Docker 基本命令 使用 Docker 运行 IRIS 社区版 Docker 桌面图形用户界面 在本文中,我们将讨论以下主题: 使用 Docker Compose 文件( YAML 文件) Docker 文件的使用(用于构建 Docker 镜像) Docker 卷的使用 那么让我们开始吧。 1.使用Docker Compose文件( YAML文件) Docker Compose 是一款旨在帮助定义和共享多容器应用程序的工具。使用 Compose,我们可以创建一个 YAML 文件来定义服务,并且通过单个命令,我们可以启动或拆除所有内容。 使用 Compose 的一大优势是能够在文件中定义应用程序堆栈并将其保存在项目存储库的根目录中。您还可以让其他人轻松地为您的项目做出贡献。 他们只需要克隆您的存储库并启动撰写应用程序。 在我的上一篇文章中,我们使用下面提到的命令来创建并启动带有 InterSystems 社区镜像的容器: docker run -d -p 52773:52773 intersystemsdc/iris-community 此时,让我们修改此命令并添加容器名称、映射更多端口并设置重新启动选项: docker run -d -p 52773:52773 -p 53773:53773 -p 1972:1972 --name iris --restart=always intersystemsdc/iris-community 让我为您分解一下上述命令: # docker run command to create and start the container docker run # -d -an option used to start container in deattach mode -d # -p -an option is used to map the ports -p 52773:52773 -p 53773:53773 -p 1972:1972 # name of the container --name iris # set the restart option to always --restart=always # base image intersystemsdc/iris-community 创建撰写文件 在根文件夹中,创建一个名为-----100-----的文件,并写入上述命令,如下所示: #specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036 :1972 - 55037 :52773 - 53773 :53773 docker run 命令和 docker-compose 文件的映射如下所示:Docker-compose 文件快照如下所示: 为了运行 docker-compose 文件代码,我们将使用 docker-compose up 命令: docker-compose up -d -----101----- 或 -----102-----:在后台运行命令并将控制返回到终端的选项。 容器已启动。让我们运行“docker ps”命令来列出正在运行的容器 正如您所看到的,我们使用 docker-compose 文件得到了相同的结果。 创建并启动多个容器 在 docker-compose 的帮助下,我们不仅可以运行多个容器,还可以组织并向其添加更多命令。 例如,在下面的 docker-compose 文件中,我们运行 MongoDB 容器和 iris 容器: #specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036:1972 - 55037:52773 - 53773:53773 #start MongoDB container mongodb: image: mongo ports: - 27017:27017 让我们运行 docker-compose up 命令 MongoDB 和 iris 容器现已创建并启动。 2.Docker 文件 Docker可以通过读取-----103-----的指令来自动构建镜像。 -----103----- 是一个文本文档,其中包含用户可以在命令行上调用来组装图像的所有命令。 所以,我们的第一个问题很简单。什么是 Dockerfile? Docker 使用它来构建镜像本身。 Dockerfile 本质上是如何创建镜像的构建指令。 与简单存储二进制映像相比,Dockerfile 的优点是自动构建将确保您拥有可用的最新版本。从安全角度来看这是一件好事,因为您希望确保没有安装任何易受攻击的软件。 常用 Docker 文件命令 您可以在下面找到一些最常用的 docker 命令。请注意,所有 docker 命令都必须大写字母。 从 第一个是 FROM 命令,它告诉您映像基于什么。正是这种多层方法使得 Docker 如此高效和强大。在本例中,使用了 iris-community Docker 映像,它再次引用 Dockerfile 来自动化构建过程。 FROM intersystemsdc/iris-community 工作目录 该命令用于设置一个工作目录,我们将在其中复制文件。例如,下面提到的命令将设置 /opt/irisbuild 作为工作目录: WORKDIR /opt/irisbuild 复制 COPY 命令就像听起来一样简单。它可以将文件复制到容器中。通常,我们复制自定义配置文件、应用程序源文件、数据文件等。 #coping Installer.cls to a root of workdir. Don't miss the dot, here. COPY Installer.cls . #copy source files from src folder to src folder in workdir in the docker container. COPY src src #copy source files from data/fhir folder to fhirdata folder in the docker container. COPY data/fhir fhirdata 环境电压 这会设置环境变量,这些变量可以在 Dockerfile 及其调用的任何脚本中使用。-----105-----指令将环境变量-----106-----定义为值-----107----- ENV USER_ID "SYSTEM" ENV USER_PASSWORD "MANAGER" 跑步 -----108-----命令用于在镜像构建过程中执行命令。 #here we give the rights to irisowner user and group which are run IRIS. RUN chown ${ISC_PACKAGE_MGRUSER} : ${ISC_PACKAGE_IRISGROUP} /opt/irisapp #start IRIS and run script in iris.script file RUN iris start IRIS \ && iris session IRIS < /tmp/iris.script 用户 默认情况下,容器以 root 身份运行,这使它们能够完全控制主机系统。随着容器技术的成熟,可能会出现更安全的默认选项。目前,要求 root 对其他人来说是危险的,并且可能不适用于所有环境。您的映像应使用 -----109----- 指令来指定容器运行的非 root用户。如果您的软件没有创建自己的用户,您可以在 Dockerfile 中创建用户和组。 #here we switch user to a root to create a folder and copy files in docker. USER root WORKDIR /opt/irisapp #switching user from root to irisowner, to copy files USER irisowner COPY src src 更多细节请阅读Docker官方文档 3.Docker卷 Docker卷是一个完全由Docker管理的独立文件系统。它作为标准文件或目录存在于数据保存的主机上。 使用 Docker 卷的目的是将数据保留在容器外部,以便可用于备份或共享。 Docker 卷依赖于 Docker 的文件系统,是 Docker 容器和服务保存数据的首选方法。当容器启动时,Docker 会加载只读镜像层,在镜像堆栈顶部添加读写层,并将卷挂载到容器文件系统上。 我们使用 -----110----- 或 -----111----- 标志来允许我们将本地文件挂载到容器中。 卷是保存 Docker 容器生成和使用的数据的首选机制。虽然绑定挂载取决于主机的目录结构和操作系统,但卷完全由 Docker 管理。与绑定挂载相比,卷有几个优点: 卷比绑定安装更容易备份或迁移。 您可以使用 Docker CLI 命令或 Docker API 管理卷。 卷适用于 Linux 和 Windows 容器。 卷可以在多个容器之间更安全地共享。 卷驱动程序允许您将卷存储在远程主机或云提供商上,以加密卷的内容或添加其他功能。 新卷的内容可以由容器预先填充。 Docker Desktop 上的卷比 Mac 和 Windows 主机上的绑定挂载具有更高的性能。 此外,卷通常是比将数据保留在容器的可写层中更好的选择,因为卷在使用时不会增加容器的大小。此外,卷的内容存在于给定容器的生命周期之外。 我们可以在 docker-compose 文件的 services 部分下提及卷。 #specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036 :1972 - 55037 :52773 - 53773 :53773 #create a volume volumes: - ./:/irisdev/app 我们已经创建了 irisdev/app 卷。 概括 Docker 是一个功能强大的工具,允许开发人员和 IT 团队在容器化环境中创建、部署和运行应用程序。通过提供可移植性、一致性、可扩展性、资源效率和安全性,Docker 可以轻松地跨不同环境和基础设施部署应用程序。随着容器化的日益普及,Docker正在成为现代软件开发和部署的重要工具。在本文中,我们学习了如何使用 Docker compose(一个 YAML 文件,指定应用程序中每个容器的配置选项)、Docker 文件(用于构建 Docker 镜像)和 Docker 卷(一种用于共享数据的持久数据存储机制) Docker 容器和主机之间的数据。) 感谢您的阅读!
文章
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 分钟采样并取平均值。 > 这些示例只是为了说明理论,您必须使用自己的应用程序进行验证!
文章
Hao Ma · 一月 15, 2021

IAM实践指南——OAuth 2.0下的API保卫战(第二部分)

在这个由三部分组成的系列文章中,我们将展示如何在OAuth 2.0标准下使用IAM简单地为IRIS中的未经验证的服务添加安全性。 在第一部分中,我们介绍了一些OAuth 2.0背景知识,以及IRIS和IAM的初始定义和配置,以帮助读者理解确保服务安全的整个过程。 现在,本文将详细讨论和演示配置IAM所需的步骤——验证传入请求中的访问令牌,并在验证成功时将请求转发到后端。 本系列的最后一部分将讨论和演示IAM生成访问令牌(充当授权服务器)并对其进行验证时所需的配置,以及一些重要的最终考虑事项。 如果您想试用IAM,请联系InterSystems销售代表。 场景1:IAM作为访问令牌验证器 在该场景中,需要使用一个外部授权服务器生成JWT(JSON Web Token)格式的访问令牌。该JWT使用了RS256算法和私钥签名。为了验证JWT签名,另一方(本例中是IAM)需要拥有授权服务器提供的公钥。 由外部授权服务器生成的JWT主体中还包括一个名为“exp”的声明(包含该令牌过期的时间戳),以及另一个名为“iss”的声明(包含授权服务器的地址)。 因此,IAM需要先使用授权服务器的公钥和JWT内部“exp”声明中包含的过期时间戳对JWT签名进行验证,然后再将请求转发给IRIS。 对IAM进行相应配置时,首先要向IAM中的“SampleIRISService”添加一个名为“JWT”的插件。为此,请转到IAM中的Services页面并复制“SampleIRISService”的ID,稍后会用到。 之后,打开插件,点击“New Plugin”按钮,找到“JWT”插件,点击启用。 在下个页面中,将“SampleIRISService”ID粘贴在“service_id”字段中,然后在“config.claims_to_verify”参数中选中“exp”框。 注意,“config.key_claim_name”参数的值是“iss”。后面会用到。 然后,点击“Create”按钮。 完成操作后,找到左侧菜单中的“Consumers”部分,然后单击先前创建的“ClientApp”。点击“Credentials”标签,然后单击按钮“New JWT Credential”。 在下一页中,选择JWT签名算法(本例中为RS256),并将公钥(这是授权服务器提供的PEM格式的公钥)粘贴到“rsa_public_key”字段中。 在“key”字段中,在添加JWT插件时需要用到之前在“config.key_claim_name”字段中输入的JWT声明内容。所以在本例中,需要插入的是JWT的iss声明内容(本例中是授权服务器的地址)。 之后,单击“Create”按钮。 提示:出于调试目的,可以使用一个在线工具对JWT进行解码,将公钥粘贴进去就可以检查声明内容及其值,并且验证签名。该在线工具的链接如下:https://jwt.io/#debugger 现在,添加了JWT插件后,就不能发送未经身份验证的请求了。如下所示,对URL的一个简单GET请求(未经身份验证): http://iamhost:8000/event/1 返回一个未经授权的信息,以及状态码“401未经授权”。 为了从IRIS获得结果,需要将JWT添加到请求中。 首先,需要向授权服务器请求JWT。如果POST请求与主体中的一些键值对(包括用户和客户端信息)一起发出,那么在这里使用的自定义授权服务器将向以下URL返回一个JWT: https://authorizationserver:5001/auth 该请求及其响应如下所示: 然后,可以将响应中获得的JWT添加到授权标头中作为Bearer令牌使用,并将GET请求发送到和之前相同的URL: http://iamhost:8000/event/1 或者将它作为querystring参数添加进去。当添加JWT插件(本例中是“jwt”)时,querystring关键字是在“config.uri_param_names”字段中指定的值 最后,如果在“config.cookie_names”字段中输入任意名称,选择将JWT作为cookie包含在请求中。 请继续阅读本系列的第三部分也即最后一部分,了解IAM生成和验证访问令牌所需的配置,以及一些重要的最终考虑因素。
文章
姚 鑫 · 五月 31, 2021

第十二章 IBM WebSphere MQ检索邮件

# 第十二章 IBM WebSphere MQ检索邮件 # 检索邮件 要检索邮件,请执行以下操作: 1. 按照“创建连接对象”中的说明创建连接对象。在这种情况下,请创建`%Net.MQRecv`的实例。`Connection`对象有一个消息队列,可以从中检索消息。 2. 根据需要调用以下方法: - `%Get()`-通过引用返回字符串消息作为第一个参数。 - `%GetStream()`-给定初始化的文件字符流,此方法从队列中检索消息,并将其放入与该流关联的文件中。请注意,必须设置流的`Filename`属性才能对其进行初始化。不支持二进制流。 3. 检查调用的方法返回的值。请参阅“获取错误代码”。请记住,当队列为空时,IBM `WebSphere MQ`返回`2033`。 4. 检索完消息后,调用`Connection`对象的`%Close()`方法以释放动态链接库的句柄。 示例1:`ReceiveString()` 下面的类方法从`mqtest`队列检索消息。 ```java ///Method returns string or null or error message ClassMethod ReceiveString() As %String { Set recv=##class(%Net.MQRecv).%New() Set queue="mqtest" Set qm="QM_antigua" Set chan="S_antigua/TCP/antigua(1414)" Set logfile="c:\mq-recv-log.txt" Set check=recv.%Init(queue,qm,chan,logfile) If 'check Quit recv.%GetLastError() Set check=recv.%Get(.msg) If 'check { Set reasoncode=recv.%GetLastError() If reasoncode=2033 Quit "" Quit "ERROR: "_reasoncode } Quit msg } ``` 示例2:`ReceiveCharacterStream()` 以下方法可以检索更长的消息,因为它使用`%GetStream()`: ```java /// Method returns reason code from IBM WebSphere MQ ClassMethod ReceiveCharacterStream() As %Integer { Set recv=##class(%Net.MQRecv).%New() Set queue="mqtest" Set qm="QM_antigua" Set chan="S_antigua/TCP/antigua(1414)" Set logfile="c:\mq-recv-log.txt" Set check=recv.%Init(queue,qm,chan,logfile) If 'check Quit recv.%GetLastError() //initialize the stream and tell it what file to use //make sure filename is unique we can tell what we received Set longmsg=##class(%FileCharacterStream).%New() Set longmsg.Filename="c:\mq-received"_$h_".txt" Set check=recv.%GetStream(longmsg) If 'check Quit recv.%GetLastError() Quit check } ``` # 更新消息信息 `%Net.MQSend`和`%Net.MQRecv`类还提供以下方法: ### %CorId() (通过引用)更新上次读取的邮件的关联ID。 ### %ReplyQMgrName() (通过引用)更新上次读取的消息的回复队列管理器名称。 ### %ReplyQName() (通过引用)更新上次读取的消息的回复队列名称。 # Troubleshooting 如果在使用IBM `WebSphere MQ`的InterSystems IRIS接口时遇到问题,应该首先确定客户端是否安装正确并且可以与服务器通信。要执行这样的测试,可以使用IBM `WebSphere MQ`提供的示例程序。可执行文件位于IBM `WebSphere MQ`客户端的bin目录中。 以下步骤介绍如何在`Windows`上使用这些示例程序。在其他操作系统上,细节可能会有所不同;请参考IBM文档并检查您的客户端中存在的文件的名称。 1. 创建一个名为`MQSERVER`的环境变量。它的值的格式应该是`channel_name/Transport/server`,其中`channel_name`是要使用的通道的名称,`Transport`是指示要使用的传输的字符串,而`server`是服务器的名称。例如:`S_Antigua/TCP/Antigua` 2. 在命令行中,输入以下命令: ```java amqsputc queue_name queue_manager_name ``` 其中,`QUEUE_NAME`是要使用的队列的名称,`QUEUE_MANAGER_NAME`是队列管理器的名称。例如: ```java amqsputc mqtest QM_antigua ``` 如果`amqsputc`命令无法识别,请确保已更新`PATH`环境变量以包括IBM `WebSphere MQ`客户端的bin目录。 3. 应该会看到几行代码,如下所示: ```java Sample AMQSPUT0 start target queue is mqtest ``` 4. 现在可以发送消息了。只需键入每条消息,然后在每条消息后按Enter键即可。例如: ```java sample message 1 sample message 2 ``` 5. 发送完邮件后,按两次Enter键。然后,将看到如下所示的行: ```java Sample AMQSPUT0 end ``` 6. 要完成此测试,我们将检索发送到队列的消息。在命令行中键入以下命令: ```java amqsgetc queue_name queue_manager_name ``` 其中,`QUEUE_NAME`是要使用的队列的名称,`QUEUE_MANAGER_NAME`是队列管理器的名称。例如: 7. 然后,应该看到一个起始行,后跟之前发送的消息,如下所示: ```java Sample AMQSGET0 start message message ``` 8. 此示例程序短暂等待接收任何其他消息,然后显示以下内容: ```java no more messages Sample AMQSGET0 end ``` 如果测试失败,请参考IBM文档。问题的可能原因包括以下几个方面: - 安全问题 - 队列定义不正确 - 队列管理器未启动
文章
Hao Ma · 十一月 17, 2021

开发Ensemble REST服务

REF: https://docs.intersystems.com/healthconnectlatest/csp/docbook/Doc.View.cls?KEY=GREST REF: https://docs.intersystems.com/healthconnectlatest/csp/docbook/DocBook.UI.Page.cls?KEY=AFL_rest#AFL_C4838 开发REST服务有两个方式, 一个是生生的写代码, 定义接口的标准,被称为"Manually Coding"。第2个方式是目前越来越流行的"Sepcification-first",也就是使用描述性的语言定义接口规范,然后通过这个规范生成接口代码。第2种方式更快捷,但这里我还是从第一种介绍起,对理解里面的代码层次更容易一些,而这是调试一个接口必须的。 从代码开发REST服务 不同于HTTP和SOAP, Ensemble里面没有REST的inbound Adaptor,也没有可用的BS组件。在Production里开发一个REST服务的步骤是: 1. 开发一个REST Service, 这个Service是一个CSP Page, 是一个网页服务,和Ensemble没关系。要在Production中使用这个服务,您需要在这个服务里调用一个Production的业务服务BS。 2. 要访问这个REST页面服务, 您需要配置一个Web Application。Web Application的配置项上有一个选项: "REST 分派类"。这样配置好之后, Web Application收到相应的URL后就会调用这个REST页面,页面再去调用Production的BS。 3. 最后,您需要在BS中处理收到的JSON, 发送给其他组件,以传递给接收方系统。 如果您看的了代码包里的EnsLib.REST.Service类, 它继承了%CSP.REST页面, 也继承了BusinessService,非常符合Ensemble的结构设计。But, 别用。在线文档中有专门的说明。 Although InterSystems IRIS defines a class EnsLib.REST.ServiceOpens in a new window, that is a subclass of %CSP.RESTOpens in a new window, we recommend that you not use this class because it provides an incomplete implementation of %CSP.REST Opens in a new window. 让我们开始开发一个简单的REST服务并加入Production: Step 1: 创建以下代码,解释一下: - 继承%CSP.REST,这是个专用于REST的CSP页面 - UrlMap是一个XData, 在COS语言里用于在代码里放置固定的xml数据结构。UrlMap定义从收到的URL到本类里不同的方法之间的映射。 - 方法中入参可以是任意的数据结构和用户定义的类结构,不需要出参。如果直接返回消息给调用者,直接"write"一个流或者字符串 Class SEDemo.IO.REST.SampleService Extends %CSP.REST { XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ] { } ClassMethod Test(pInput As %String) As %Status { write "Received: "_pInput,! Quit 1 } ClassMethod GetPatientById(pID As %String) As %Status { Try{ Set tObj=##class(SEDemo.Common.Patient).%OpenId(pID),tStream = "" d ##class(%ZEN.Auxiliary.jsonProvider).%WriteJSONStreamFromObject(.tStream,tObj) w tStream.Read() } Catch (e) {Set tSC=e.AsStatus()} Quit tSC } } Step 2: 创建Web Application 在管理界面System Administration > Security > Applications > Web Applications,创建一个用于接收此REST服务的Web APPlication, 设置"Dispatch Class"为当前类。 假设创建的Web Applicaiton为"/CSP/myrest",注意: - 选中“Enable Application" - 权限: 分配本命名空间数据库的资源,默认是%DB_%Default。 后面会详细介绍权限和用户管理的细节。 Step 3: 测试你的REST service 你可以选择自己喜欢的测试方式,比如用浏览器,POSTMAN, SoapUI..., 下面是我测试的记录: CNMBPHMA:~ hma$ curl -v http://172.16.58.200:52773/csp/myrest/Test/333 * Trying 172.16.58.200... * TCP_NODELAY set * Connected to 172.16.58.200 (172.16.58.200) port 52773 (#0) > GET /csp/myrest/Test/333 HTTP/1.1 > Host: 172.16.58.200:52773 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 14 Jul 2021 06:47:26 GMT < Server: Apache < CACHE-CONTROL: no-cache < EXPIRES: Thu, 29 Oct 1998 17:04:19 GMT < PRAGMA: no-cache < CONTENT-LENGTH: 15 < Content-Type: text/html; charset=utf-8 < Received: 333 * Connection #0 to host 172.16.58.200 left intact CNMBPHMA:~ hma$ 这里是一个匿名访问,如果需要用户认证,修改一下重发: CNMBPHMA:~ hma$ curl -u 'superuser:SYS' http://172.16.58.200:52773/csp/myrest/Test/333 Received: 333 CNMBPHMA:~ hma$ 注意两点: 1. 到目前为止我们测试的其实是一个HTTP请求和响应,虽然内部用了%CSP.REST的类, 但响应中'Content-Type'还是'text/html' 2. 代码中没有处理出错和查不到结果的情况 3. 到目前为止和Ensemble Production没有任何关系。 Step 4: 将服务加入Ensemble Production 加入Production的意思实际上时调用一个Production的BusinessService。 让我们先创建一个简单的Service. ///不使用Adapter, 收到任何请求,同步发送给目标组件 Class Test.BS.GeneralService Extends Ens.BusinessService { Method OnProcessInput(pInput As %RegisteredObject, Output pOutput As %RegisteredObject) As %Status { set tRequest=##class(Ens.StringRequest).%New() Set tStatus = ..SendRequestSync("Test.BO.dummyOperation", tRequest, .tResponse) set pOutput = tResponse Quit tStatus } } 当需要前面的REST服务来调用这个BusinessService的时候, 需要在method里面加入直接调用的语句,比如上面的GetPatientById() ClassMethod GetPatientById(pID As %String) As %Status { set status = ##class(Ens.Director).CreateBusinessService("Test.BS.GeneralService", .tService) if $$$ISOK(status) { set status = service.OnProcessInput(pID, .tResponse) } w tResponse,! }
文章
姚 鑫 · 六月 10, 2021

第三章 指定输出的字符集

# 第三章 指定输出的字符集 # 指定输出的字符集 若要指定要在输出文档中使用的字符集,可以设置Writer实例的Charset属性。选项包括`“UTF-8”`、`“UTF-16”`以及InterSystems IRIS支持的其他字符集。 # Writing the Prolog XML文件的序言(根元素之前的部分)可以包含文档类型声明、处理指令和注释。 ## 影响Prolog的属性 在`writer`实例中,以下属性会影响`prolog`: ### Charset 控制两件事`:XML`声明中的字符集声明和(相应的)输出中使用的字符集编码。 ### NoXmlDeclaration 控制输出是否包含XML声明。在大多数情况下,默认值是0,这意味着已经编写了声明。如果没有指定字符集,并且输出定向到字符串或字符流,则默认为1,并且不写入任何声明。 ## 生成文档类型声明 在根元素之前,可以包含文档类型声明,该声明声明了文档中使用的模式。 要生成文档类型声明,需要使用`WriteDocType()`方法,该方法有一个必选参数和三个可选参数。 就本文档而言,文档类型声明包括以下可能的部分: ```java ``` 如这里所示,文档类型有一个名称,根据XML规则,该名称必须是根元素的名称。 声明可以包含外部子集、内部子集或两者。 external_subset 部分指向其他地方的DTD文件。 本节的结构是以下任何一种: ```java PUBLIC public_literal_identifier PUBLIC public_literal_identifier system_literal_identifier SYSTEM system_literal_identifier ``` 这里`public_literal_identifier`和`system_literal_identifier`是包含DTD uri的字符串。 注意,DTD可以同时具有公共标识符和系统标识符。 下面是一个文档类型声明示例,它包含一个同时使用公共标识符和系统标识符的外部子集: ```java ``` internal_subset部分是一组实体声明。 下面是一个文档类型声明的示例,它只包含一组内部声明: ```java !ENTITY city (#PCDATA)> !ENTITY player (#PCDATA)> ] > ``` `WriteDocType()`方法有四个参数: - 第一个参数指定文档类型的名称,用于在这个XML文档中使用。 这是必需的,而且必须是有效的XML标识符。 还必须将此名称用作本文档中根级别元素的名称。 - 可选的第二个和第三个参数指定声明的外部部分,如下所示: WriteDocType参数 第二个参数 | 第三个参数| 其他部分 ---|---|--- “publicURI” |null| PUBLIC “publicURI” “publicURI” |“systemURI”| PUBLIC “publicURI” “systemURI” null |“systemURI”| SYSTEM “systemURI” - 可选的第四个参数指定声明的内部部分。如果此参数非空,则将其括在方括号`[]`中,并适当地放在声明的末尾。没有添加其他字符。 ## 编写处理指令 要将处理指令写入`XML`,请使用`WriteProcessingInstruction()`方法,该方法有两个参数: 1. 处理指令(也称为目标)的名称。 2. 指令本身是一个字符串。 该方法将以下内容写入XML: ```java ``` 例如,要编写以下处理指令: ```java ``` 为此,可以按如下方式调用`WriteProcessingInstruction()`方法: ```java set instructions="type=""text/css"" href=""mystyles.css""" set status=writer.WriteProcessingInstruction("xml-stylesheet", instructions) ``` ## 指定默认命名空间 在编写器实例中,可以指定默认命名空间,该命名空间仅应用于没有`Namespace`参数设置的类。有几个选项: - 可以在输出方法中指定默认命名空间。四个主要的输出方法(`RootObject()`、`RootElement()`、`Object()`或`Element()`)都接受名称空间作为参数。只有在类定义中未设置`Namespace`参数时,才会将相关元素分配给`Namespace`。 - 可以为编写器实例指定总体默认命名空间。为此,请为编写器实例的`DefaultNamespace`属性指定值。 ```java Class Writers.BasicDemoPerson Extends (%RegisteredObject, %XML.Adaptor) { Parameter XMLNAME = "Person"; Property Name As %Name; Property DOB As %Date; } ``` 默认情况下,如果我们只是导出此类的对象,我们会看到如下所示的输出: ```java Persephone MacMillan 1976-02-20 ``` 相反,如果我们在编写器实例中`将DefaultNamespace`设置为`"http://www.person.org",`然后导出一个对象,则会收到如下所示的输出: ```java Persephone MacMillan 1976-02-20 ``` 在本例中, `` 元素使用默认名称空间,否则不会分配给名称空间。
文章
Frank Ma · 六月 13, 2022

使用IRIS IntegratedML(一体化机器学习)预测孕产妇风险的Web应用

孕产妇风险可以通过一些医学界众所周知的参数来测量。这样,为了帮助医学界和计算机系统,特别是人工智能,科学家Yasir Hussein Shakir发布了一个非常有用的数据集,用于训练检测/预测孕产妇风险的机器学习(ML)算法。这份出版物可以在最大和最知名的ML数据库Kaggle上找到,网址是 https://www.kaggle.com/code/yasserhessein/classification-maternal-health-5-algorithms-ml. 关于数据集 由于缺乏怀孕期间和怀孕后的孕产妇保健信息,许多孕妇死于怀孕问题。这在农村地区和新兴国家的中下层家庭中更为常见。在怀孕期间,应时刻注意观察,以确保婴儿的正常成长和安全分娩 (来源: https://www.kaggle.com/code/yasserhessein/classification-maternal-health-5-algorithms-ml). 数据是通过基于物联网的风险监测系统,从不同的医院、社区诊所、孕产妇保健机构收集而来。 Age(年龄): 妇女怀孕时的年龄,以岁为单位。 SystolicBP (收缩压): 血压的最高值(mmHg),这是怀孕期间的另一个重要属性。 DiastolicBP(舒张压): 血压的较低值(mmHg),这是怀孕期间的另一个重要属性。 BS(血糖): 血糖水平是以摩尔浓度为单位,即mmol/L。 HeartRate(心率): 正常的静息心率,单位是每分钟的心跳次数。 Risk Level(风险等级): 基于前边的属性所预测的孕期风险强度水平。 从Kaggle获取孕产妇的风险数据 来自Kaggle的孕产妇风险数据可以通过Health-Dataset(健康数据集)应用程序加载到IRIS表中: https://openexchange.intersystems.com/package/Health-Dataset. 要做到这一点,在你的module.xml项目中,设置依赖关系(Health Dataset的ModuleReference): Module.xml with Health Dataset application reference <?xml version="1.0" encoding="UTF-8"?> <Export generator="Cache" version="25"> <Document name="predict-diseases.ZPM"> <Module> <Name>predict-diseases</Name> <Version>1.0.0</Version> <Packaging>module</Packaging> <SourcesRoot>src/iris</SourcesRoot> <Resource Name="dc.predict.disease.PKG"/> <Dependencies> <ModuleReference> <Name>swagger-ui</Name> <Version>1.*.*</Version> </ModuleReference> <ModuleReference> <Name>dataset-health</Name> <Version>*</Version> </ModuleReference> </Dependencies> <CSPApplication Url="/predict-diseases" DispatchClass="dc.predict.disease.PredictDiseaseRESTApp" MatchRoles=":{$dbrole}" PasswordAuthEnabled="1" UnauthenticatedEnabled="1" Recurse="1" UseCookies="2" CookiePath="/predict-diseases" /> <CSPApplication CookiePath="/disease-predictor/" DefaultTimeout="900" SourcePath="/src/csp" DeployPath="${cspdir}/csp/${namespace}/" MatchRoles=":{$dbrole}" PasswordAuthEnabled="0" Recurse="1" ServeFiles="1" ServeFilesTimeout="3600" UnauthenticatedEnabled="1" Url="/disease-predictor" UseSessionCookie="2" /> </Module> </Document> </Export> Web Frontend and Backend Application to Predict Maternal Risk Go to Open Exchange app link (https://openexchange.intersystems.com/package/Disease-Predictor) and follow these steps: 使用Clone/git 把repo拉到任一本地目录中: $ git clone https://github.com/yurimarx/predict-diseases.git 在该文件夹中打开Docker 终端并运行: $ docker-compose build 运行IRIS容器: $ docker-compose up -d 进入管理门户执行查询,训练AI模型: http://localhost:52773/csp/sys/exp/%25CSP.UI.Portal.SQL.Home.zen?$NAMESPACE=USER 创建用于训练的VIEW(视图): CREATE VIEW MaternalRiskTrain AS SELECT BS, BodyTemp, DiastolicBP, HeartRate, RiskLevel, SystolicBP, age FROM dc_data_health.MaternalHealthRisk 使用视图创建AI模型: CREATE MODEL MaternalRiskModel PREDICTING (RiskLevel) FROM MaternalRiskTrain 训练模型: TRAIN MODEL MaternalRiskModel 访问 http://localhost:52773/disease-predictor/index.html ,使用 Disease Predictor(疾病预测器)前端进行疾病预测,如下: 幕后工作 预测孕产妇风险疾病的后端类方法 InterSystems IRIS允许你执行SELECT,使用之前创建的模型进行预测。 Backend ClassMethod to predict Maternal Risk /// Predict Maternal Risk ClassMethod PredictMaternalRisk() As %Status { Try { Set data = {}.%FromJSON(%request.Content) Set %response.Status = 200 Set %response.Headers("Access-Control-Allow-Origin")="*" Set qry = "SELECT PREDICT(MaternalRiskModel) As PredictedMaternalRisk, " _"age, BS, BodyTemp, DiastolicBP, HeartRate, SystolicBP " _"FROM (SELECT "_data.BS_" AS BS, " _data.BodyTemp_" As BodyTemp, " _data.DiastolicBP_" AS DiastolicBP, " _data.HeartRate_" AS HeartRate, " _data.SystolicBP_" As SystolicBP, " _data.Age_" AS age)" Set tStatement = ##class(%SQL.Statement).%New() Set qStatus = tStatement.%Prepare(qry) If qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT} Set rset = tStatement.%Execute() Do rset.%Next() Set Response = {} Set Response.PredictedMaternalRisk = rset.PredictedMaternalRisk Set Response.Age = rset.Age Set Response.SystolicBP = rset.SystolicBP Set Response.DiastolicBP = rset.DiastolicBP Set Response.BS = rset.BS Set Response.BodyTemp = rset.BodyTemp Set Response.HeartRate = rset.HeartRate Write Response.%ToJSON() Return 1 } Catch err { write !, "Error name: ", ?20, err.Name, !, "Error code: ", ?20, err.Code, !, "Error location: ", ?20, err.Location, !, "Additional data: ", ?20, err.Data, ! Return 0 } } 现在,任何web应用都可以进行预测并显示结果。请在预测疾病应用程序的前端文件夹中查看源代码。
文章
姚 鑫 · 六月 22, 2021

第十五章 XML检查属性

# 第十五章 XML检查属性 # 检查属性的基本方法 可以使用`%XML.Node`的以下方法。以检查当前节点的属性。 - `AttributeDefined()` 如果当前元素具有具有给定名称的属性,则返回非零(TRUE)。 - `FirstAttributeName()` 返回当前元素的第一个属性的属性名称。 - `GetAttributeValue()` 返回给定属性的值。如果元素没有该属性,则该方法返回NULL。 - `GetNumberAttributes()` 返回当前元素的属性数。 - `LastAttributeName()` 返回当前元素的最后一个属性的属性名称。 - `NextAttributeName()` 在给定属性名称的情况下,无论指定的属性是否有效,此方法都会按排序顺序返回下一个属性的名称。 - `PreviousAttributeName()` 在给定属性名称的情况下,无论指定的属性是否有效,此方法都会按排序顺序返回上一个属性的名称。 下面的示例遍历给定节点中的属性并编写一个简单报表: ```java /// d ##class(Demo.XmlDemo).ShowAttributes("David Marston") /// David Marston ClassMethod ShowAttributes(string) { set reader=##class(%XML.Reader).%New() set status=reader.OpenString(string) if $$$ISERR(status) {do $System.Status.DisplayError(status)} s node = reader.Document.GetDocumentElement() b s count = node.GetNumberAttributes() w !, "属性数量: ", count s first = node.FirstAttributeName() w !, "第一个属性是: ", first w !, " 值是: ",node.GetAttributeValue(first) s next = node.NextAttributeName(first) for i = 1 : 1 : count - 2 { w !, "下一个属性是: ", next w !, " 值是: ",node.GetAttributeValue(next) s next = node.NextAttributeName(next) } s last = node.LastAttributeName() w !, "最后一个属性是: ", last w !, " 值是: ",node.GetAttributeValue(last) } ``` 示例XML文档: ```xml David Marston ``` 如果将此文档的第一个节点传递给示例方法,则会看到以下输出: ```java Number of attributes: 5 First attribute is: attr1 Its value is: first Next attribute is: attr2 Its value is: second Next attribute is: attr3 Its value is: third Next attribute is: attr4 Its value is: fourth Last attribute is: attr5 Its value is: fifth ``` # 检查属性的其他方法 本节讨论可用于获取任何属性的名称、值、命名空间、`QName`和值命名空间的方法。这些方法分为以下几组: - 仅使用属性名称的方法 - 使用属性名称和命名空间的方法 注意:在XML标准中,一个元素可以包含多个同名的属性,每个属性位于不同的名称空间中。但是,在InterSystems IRIS XML中,这是不受支持的。 ## 仅使用属性名称的方法 使用以下方法获取有关属性的信息。 ### GetAttribute() ```java method GetAttribute(attributeName As %String, ByRef namespace As %String, ByRef value As %String, ByRef valueNamespace As %String) ``` 返回给定属性的数据。此方法通过引用返回下列值: - `Namespace`是来自属性QName的命名空间URI - `value` 是属性值。 - `valueNamespace` 值所属的命名空间URI。例如,以下属性: ``` xsi:type="s:string" ``` 此属性的值为字符串,并且此值位于使用前缀s在其他位置声明的命名空间中。假设本文档的较早部分包含以下命名空间声明: ```java xmlns:s="http://www.w3.org/2001/XMLSchema" ``` 在本例中,`valueNamespace`将为`“http://www.w3.org/2001/XMLSchema”`. ### GetAttributeNamespace() ```java method GetAttributeNamespace(attributeName As %String) as %String ``` 从当前元素的名为`AttributeName`的属性的`QName`返回命名空间URI。 ### GetAttributeQName() ```java method GetAttributeQName(attributeName As %String) as %String ``` 返回给定属性的`QName`。 ### GetAttributeValue() ```java method GetAttributeValue(attributeName As %String) as %String ``` 返回给定属性的值。 ### GetAttributeValueNamespace() ```java method GetAttributeValueNamespace(attributeName As %String) as %String ``` 返回给定属性的值的命名空间。 ## 使用属性名和命名空间的方法 要同时使用属性名称及其命名空间来获取有关属性的信息,请使用以下方法: ### GetAttributeNS() ```java method GetAttributeNS(attributeName As %String, namespace As %String, ByRef value As %String, ByRef valueNamespace As %String) ``` 返回给定属性的数据,其中`AttributeName`和`Namespace`指定感兴趣的属性。此方法通过引用返回以下数据: - `value` 是属性值。 - `valueNamespace` 值所属的命名空间URI。例如,以下属性: ```java xsi:type="s:string" ``` 此属性的值为字符串,并且此值位于使用前缀s在其他位置声明的命名空间中。假设本文档的较早部分包含以下命名空间声明: ```java xmlns:s="http://www.w3.org/2001/XMLSchema" ``` ### GetAttributeQNameNS() ```java method GetAttributeQNameNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的`QName`,其中`AttributeName`和`Namespace`指定感兴趣的属性。 ### GetAttributeValueNS() ```java method GetAttributeValueNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的值,其中`AttributeName`和`Namespace`指定感兴趣的属性。 ### GetAttributeValueNamespaceNS ```java method GetAttributeValueNamespaceNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的值的命名空间,其中`AttributeName`和`Namespace`指定感兴趣的属性。