搜索​​​​

687 个结果
文章
Hao Ma · 一月 10, 2021

完整性检查_ 加速还是减速

虽然 Caché 和 InterSystems IRIS 数据库的[完整性](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCDI_integrity)完全不会受到系统故障的影响,但物理存储设备故障确实会损坏其存储的数据。 因此,许多站点选择运行定期数据库完整性检查,尤其要与备份配合,以验证在发生灾难时是否可以依赖给定的备份。 系统管理员在应对涉及存储损坏的灾难时,也可能强烈需要完整性检查。 完整性检查必须读取所检查的 global 的每个块(如果尚未在缓冲区中),并且按照 global 结构指示的顺序读取。 这会花费大量时间,**但完整性检查能够以存储子系统可以承受的最快速度进行读取**。 在某些情况下,需要以这种方式运行以尽快获得结果。 在其他情况下,完整性检查需要更加保守,以避免消耗过多的存储子系统带宽。 ## 行动计划 以下概述适合大多数情况。 本文其余部分中的详细讨论提供了采取其中任一行动或得出其他行动方案所需的信息。 1. 如果使用 Linux 并且完整性检查很慢,请参阅下面有关启用异步 I/O 的信息。 2. 如果完整性检查必须尽快完成,则在隔离的环境中运行;或者如果迫切需要结果,则使用多进程完整性检查来并行检查多个 global 或数据库。 进程数乘以每个进程将执行的并发异步读取数(默认为 8,如果使用 Linux 并且禁用异步 I/O 则为 1)是实时并发读取数的限制。 假定平均数是限制数量的一半,然后与存储子系统的能力进行比较。 例如,存储由 20 个驱动器条带化,每个进程的默认并发读取数为 8,则可能需要 5 个或更多进程才能利用存储子系统的全部能力 (5*8/2=20)。 3. 在平衡完整性检查速度与对生产的影响时,首先调整多进程完整性检查的进程数,然后如果需要的话,查看可调参数 SetAsyncReadBuffers。 对于长期解决方案(以及为消除误报),请参见下面的隔离完整性检查。 4. 如果已经被限制为一个进程(例如有一个极大的 global 或存在其他外部约束),并且完整性检查的速度需要上下调整,则查看下面的可调参数 SetAsyncReadBuffers。 ## 多进程完整性检查 让完整性检查更快完成(以更高的速度使用系统资源)的一般解决方案是将工作分给多个并行进程。 一些完整性检查用户界面和 API 会这样做,而其他一些则使用单个进程。 对进程的分配按 global 进行,因此对单个 global 的检查始终由一个进程执行(Caché 2018.1 之前的版本按数据库而不是按 global 分配工作)。 多进程完整性检查的主要 API 是 **CheckLIst^Integrity**(有关详细信息,请参阅[文档](https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GCDI_integrity#GCDI_integrity_verify_utility))。 它将结果收集在一个临时的 global 中,通过 Display^Integrity 来显示。 以下是使用 5 个进程检查 3 个数据库的示例。 这里如省略数据库列表参数,将检查所有数据库。 set dblist=$listbuild(“/data/db1/”,”/data/db2/”,”/data/db3/”) set sc=$$CheckList^Integrity(,dblist,,,5) do Display^Integrity() kill ^IRIS.TempIntegrityOutput(+$job) /* Note: evaluating ‘sc’ above isn’t needed just to display the results, but...    $system.Status.IsOK(sc) - ran successfully and found no errors    $system.Status.GetErrorCodes(sc)=$$$ERRORCODE($$$IntegrityCheckErrors) // 267                            - ran successfully, but found errors.    Else - a problem may have prevented some portion from running, ‘sc’ may have            multiple error codes, one of which may be $$$IntegrityCheckErrors. */ 像这样使用 CheckLIst^Integrity 是实现我们感兴趣的控制水平的最直接方法。 管理门户接口和完整性检查任务(内置但未安排)使用多个进程,但可能无法为我们的用途提供足够的控制。* 其他完整性检查接口,尤其是终端用户接口 ^INTEGRIT 或 ^Integrity 以及 Silent^Integrity,在单个进程中执行完整性检查。 因此,这些接口不能以最快的速度完成检查,并且它们使用的资源也较少。 但一个优点是,它们的结果是可见的,可以记录到文件或输出到终端,因为每个 global 都会被检查,而且顺序明确。 ## 异步 I/O 完整性检查进程会排查 global 的每个指针块,一次检查一个,根据它指向的数据块的内容来进行验证。 数据块以异步 I/O 的方式读取,以确保每时每刻都有一定数量的读取请求供存储子系统处理,并且每次读取完成后都进行验证。 在 Linux 上,异步 I/O 只有与直接 I/O 结合时才有效,而 InterSystems IRIS 2020.3 之前的版本默认不启用直接 I/O。 这解释了大量 Linux 上完整性检查时间过长的情况。 幸运的是,可以在 Cache 2018.1、IRIS 2019.1 及以后的版本上启用直接 I/O,方法是在 .cpf 文件的 [config] 部分中设置 **wduseasyncio=1**,然后重新启动。 通常建议设置此参数,以在繁忙系统上实现 I/O 可伸缩性,并且自 Caché 2015.2 起,在非 Linux 平台上默认设置此参数。 在启用之前,确保已经为数据库缓存(global 缓冲区)配置了足够的内存,因为启用直接 I/O 后,数据库将不再被 Linux(冗余)缓存。 未启用时,完整性检查执行的读取会同步完成,不能有效利用存储。 在所有平台上,完整性检查进程一次执行的读取数默认设置为 8。 如果必须更改单个完整性检查进程从磁盘读取的速率,可以调整此参数 – 向上调会使单个进程更快完成,向下调则使用更少的存储带宽。 请记住: * 此参数应用于每个完整性检查进程。 当使用多个进程时,进程数会使实时读取数增加。更改并行完整性检查进程数会产生较大影响,因此这通常是先做的事情。 每个进程还受到计算时间的限制(除其他限制外),因此增加此参数的值所获得的收益也有限。 * 这只在存储子系统处理并发读取的能力范围内有效。 如果数据库存储在单个本地驱动器上,再高的数值也没有用处,而在几十个驱动器上条带化的存储阵列可以并发处理几十个读取。 要从 %SYS 命名空间调整此参数,则 **do SetAsyncReadBuffers^Integrity(**value**)**。 要查看当前值,则 **write $$GetAsyncReadBuffers^Integrity()**。 更改在检查下一个 global 时生效。 目前,该设置在系统重启后不会保持,虽然可以将其添加到 SYSTEM^%ZSTART 中。 有一个相似的参数用于在磁盘上的块是连续(或接近连续)分布时控制每次读取的最大大小。 此参数很少需要用到,尽管具有高存储延迟的系统或具有较大块大小的数据库可能会从微调中受益。 该值的单位为 64KB,因此值 1 表示 64KB,4 表示 256KB 等等。0(默认值)表示让系统选择,当前选择 1 (64KB)。 此参数的 ^Integrity 函数(类似于上面提及的函数)为 **SetAsyncReadBufferSize** 和 **GetAsyncReadBufferSize**。 ## 隔离完整性检查 许多站点直接在生产系统上运行定期完整性检查。 这当然是最简单的配置,但并不理想。 除了完整性检查对存储带宽的影响,并发数据库更新活动有时还可能导致误报错误(尽管检查算法内置了缓解措施)。 因此,在生产系统上运行的完整性检查所报告的错误,需要由管理员进行评估和/或重新检查。 很多时候,存在更好的选择。 可以将存储快照或备份映像挂载到另一台主机上,在那里由隔离的 Caché 或 IRIS 实例运行完整性检查。 这样不仅可以防止任何误报,而且如果存储也与生产隔离,运行完整性检查可以充分利用存储带宽并更快完成。 这种方法非常适合使用完整性检查来验证备份的模型;经过验证的备份可以有效验证截至生成备份前的生产情况。 还可以通过云和虚拟化平台更容易地从快照建立可用的隔离环境。   * * * * 管理门户接口、完整性检查任务和 SYS.Database 的 IntegrityCheck 方法会选择相当多的进程(等于 CPU 内核数),在很多情况下缺少所需的控制。 管理门户和任务还会对任何报告错误的 global 执行完整的重新检查,以识别可能因并发更新而出现的误报。 除了完整性检查算法内置的误报缓解措施,也可能进行这种重新检查;在某些情况下,由于会花费额外的时间(重新检查在单个进程中运行,并检查整个 global),可能并不需要重新检查。 此行为将来可能会更改。
文章
Nicky Zhu · 五月 20, 2021

互操作消息统一管理系列:MessageBank

## 一. 企业信息库简介 企业信息库(MessageBank)是一个可选的远程归档设施,可以从多个来自不同实例的互操作性Production中收集信息、事件日志项目和搜索表项。如下图所示: ![image](/sites/default/files/inline/images/1_1_0.png) 这套环境由两种角色的实例构成: 企业信息库服务器,它本身也是一个Production,完全由Message Bank服务组成,接收来自任何数量的客户Production提交的消息、日志等。 客户端Operation(Message Bank Operation),将其添加到一个正在运行的Production中,并用企业信息库服务器的地址进行配置。如连接通畅,消息和日志即可自动转发到Message Bank并在其中存储。 为了使你能方便地看到信息库中的信息,InterSystems IRIS®提供了以下附加选项。 对于企业信息库实例,管理门户自动包括企业监控器页面,在那里你可以监控客户端Production的状态,浏览消息库,并对被监控客户的消息进行检索。 对于每个客户端实例,你在消息库实例中配置一个到企业监控器的链接。 如下所示: ![image](/sites/default/files/inline/images/1_2_0.png) ## 二. 常见应用场景 ### 消息归档 在使用IRIS互操作性时,对于生产环境,为保障其有充足的磁盘空间和即时查询的效率,通常会采用消息和日志过期策略。在生产环境中只保留近期(如一个月)的信息以备回溯,过期数据将定期被清除。因此,如果有长期保留消息(如在生产环境清除周期之外还需要更长时间的回溯)的需求,则可以通过Message Bank对消息和日志进行长期保存。 ### 企业消息仓库 对于集成规模较大,集成业务较多的大型企业和集团(如大型医院、医联体、医共体),往往会采用多套互操作性实例支撑数据交换和集成业务。在这种环境下,可以通过Message Bank汇聚和存储整个企业环境下的所有互操作消息和日志,为业务集中监控、跨实例业务故障分析等工作创造条件。 ### 消息和日志再利用 理想条件下,实施互操作性项目之后,消息和日志中就会包括大量的业务数据,典型的包括下达的医嘱、患者信息、医疗记录等。通过对Message Bank中的数据进行分析和挖掘,能够获得有价值的业务信息。 接下来我们会为大家介绍Message Bank的搭建过程。 ## 三. 搭建Message Bank ### 创建Message Bank 命名空间 在生产所用的实例之外,我们需要使用一台独立的实例用于安装和配置Message Bank(实例安装过程和License激活过程从略,请查看安装文档或联系您的支持工程师)。 在该实例上,创建一个命名空间安装Message Bank,如下所示: ![image](/sites/default/files/inline/images/1_3_0.png) 由于Message Bank本质上由Production实现,因此创建命名空间时要选上对互操作Producation的支持。 InterSystems为大家提供了可以套用的Production模版。因此,请按照以下步骤创建Production: 在刚才创建的命名空间MessageStore下创建类MessageBank.BankProduction,继承Ens.Enterprise.MsgBank.Production并将Ens.Enterprise.MsgBank.Production中的XData代码块拷贝到新建的类中,如下所示: ![image](/sites/default/files/inline/images/1_4_0.png) 保存和编译该类,并在Interoperability菜单中加载该Production。 ![image](/sites/default/files/inline/images/1_5_0.png) 其中已部署了两个服务: MsgBankService:该服务通过TCP连接从其他Production接收消息 注意该Service默认使用9192端口与其他客户端通信。 ![image](/sites/default/files/inline/images/1_6_0.png) MonitorService:该服务收集其他实例的其他Production的运行状态 此时,这个Production已经具备了从其他实例的Production收集消息和事件信息的能力,可直接启动。当然,我们还需要配置与客户端的连接。 ### 为客户端Production添加消息转发Operation 假设我们已经有一个可运行的的如下所示的Production ![image](/sites/default/files/inline/images/1_7_0.png) 注意这个Production与Message Bank不在同一个实例上。 ![image](/sites/default/files/inline/images/1_8_0.png) 这个Production接收XML格式的报文并根据报文类型转发到不同的BO。 要将这个Production加入Message Bank,则需要对该客户端Production添加Business Operation Ens.Enterprise.MsgBankOperation。 ![image](/sites/default/files/inline/images/1_9_0.png) 对于该Operation,需要指定要连接的Message Bank的IP地址和端口。 ![image](/sites/default/files/inline/images/1_10_0.png) 同时,建议开启这个Operation的“启用存档”开关,保证在Message Bank临时故障时挂起消息,在故障恢复后还能捕捉到故障期间的消息和日志。 配置完成后启用该Operation。 ### 在Message Bank中加入客户端信息 上述连接建立后,客户端和Message Bank间的连接已建立,还需要配置Message Bank和客户端Production之间的程序信息(相当于注册)才能正常工作。 #### 添加客户端连接凭据 ![image](/sites/default/files/inline/images/1_11_0.png) Message Bank需要通过Web请求访问客户端信息,因此,需要配置客户端凭据,即可通过管理门户访问客户端Production的用户名和密码(对访问权限的设计和配置,可参见我们之前的文章:IRIS中的权限管理) #### 在Message Bank上配置客户端信息 在Message Bank中的Interoperability菜单中找到“企业系统”项 ![image](/sites/default/files/inline/images/1_12_0.png) 在操作页面上通过“新建连接” ![image](/sites/default/files/inline/images/1_13_0.png) 新建连接添加客户端信息。 ![image](/sites/default/files/inline/images/1_14_0.png) 注意其中的服务Web应用路径为该客户端实例上Production所在的命名空间的Web Application根路径,并引用之前填写的凭据。 如配置正确,可通过企业监视器查看连接状态 ![image](/sites/default/files/inline/images/1_15_0.png) 连接成功的状态如下: ![image](/sites/default/files/inline/images/1_16_0.png) #### 在客户端上添加Message Bank连接信息(可选步骤) 如果需要在客户端上通过链接查看消息仓库的信息,则可以配置链接。 在客户端上,在被采集的Production所在的命名空间的Interoerability菜单中“消息仓库链接”配置 ![image](/sites/default/files/inline/images/1_17_0.png) 输入Message Bank所在的IP、端口和Production所在的命名空间,保存并“开始”即可跳转到Messsage Bank的企业监视器。 ![image](/sites/default/files/inline/images/1_18_0.png) 需要注意的是,该配置固定采用了/csp/[namespace]为Message Bank的Web Application路径,而在Message Bank实例上,这个Web Application默认的路径是/csp/healthshare/messagestorage。可通过在Message Bank上添加一个Web Application,拷贝/csp/healthshare/messagestorage的配置。 ![image](/sites/default/files/inline/images/1_19_0.png) ## 四. Message Bank的实施效果 ### 测试消息 在客户端的Production中触发任意流程产生消息,如下所示: ![image](/sites/default/files/inline/images/1_20_0.png) 此时通过Message Bank中的“消息仓库查看器”即可查询存储在消息仓库中的消息 ![image](/sites/default/files/inline/images/1_21_0.png) 如下: ![image](/sites/default/files/inline/images/1_22_0.png) 可以注意到该消息已被同步到消息仓库。 需要注意,使用“消息仓库查看器”时,查询的是在Message Bank中存储的消息数据,使用在Message Bank上定义的Search Table或索引进行查询;如果通过“企业消息查看器”查询,则是链接到客户端的消息查看器查询,应用的是在客户端上定义的索引。 ### 消息的存储 根据在源系统的消息类型的不同,传递到Message Bank后会以不同的形式保存消息。 #### 虚拟文档 对于HL7 V2等标准消息或基于XML虚拟文档的消息,在Message Bank这一侧也同样以虚拟文档的形式保存。 ![image](/sites/default/files/inline/images/1_23_0.png) 特别注意其中的如下属性: MessageBodyClassName:该类型为消息在Message Bank侧持久化的类型。 ClientBodyClassName:该类型为消息在客户端侧持久化的类型。 在本例中可以看到,客户端通过EnsLib.EDI.XML.Document类型传递的消息,在Message Bank中也是通过EnsLib.EDI.XML.Document保存。 MessageBodyId:消息在Message Bank中的物理主键 ClientBodyId:客户端侧持久化消息的物理主键 ClientSessionId:客户端会话Id #### 结构化消息 对于基于Ens.Request等持久化类型的消息,在Message Bank这一侧则默认使用字符流来保存。 例如,对于如下的客户端结构化消息传输 ![image](/sites/default/files/inline/images/1_24_0.png) 在Message Bank中的保存形式为: ![image](/sites/default/files/inline/images/1_25_0.png) 可见: MessageBodyClassName:消息在Message Bank中以%Stream.GlobalCharacter即字符流进行保存 因此,无论是保存为EnsLib.EDI.XML.Document或是%Stream.GlobalCharacter,在Message Bank中保存的消息本身都缺乏足够的结构化特征和索引以支持对消息体的检索,我们会在下一篇教程《[互操作消息统一管理系列:SearchTable加速检索](https://cn.community.intersystems.com/post/%E4%BA%92%E6%93%8D%E4%BD%9C%E6%B6%88%E6%81%AF%E7%BB%9F%E4%B8%80%E7%AE%A1%E7%90%86%E7%B3%BB%E5%88%97%EF%BC%9Asearchtable%E5%8A%A0%E9%80%9F%E6%A3%80%E7%B4%A2)》中介绍如何通过构建Search Table来检索这些消息。 对于Message Bank相关的内容,可参见: https://docs.intersystems.com/healthconnect20201/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_message_bank 也欢迎与我们联系获得更详细的信息。
文章
Weiwei Gu · 六月 27

Globals 是管理数据的魔剑 : 第一部分

Globals,这些存储数据的魔剑,已经存在了一段时间,但是没有多少人能够有效地使用它们,也没有多少人知道这个超级武器。 如果你把Globals的东西用在它们真正能发挥作用的地方,其结果可能是惊人的,要么是性能的提高,要么是整体解决方案的大幅简化 (1, 2). Globals提供了一种特殊的存储和处理数据的方式,它与SQL表完全不同。它们在1966年首次出现在 M(UMPS)编程语言中, 该语言最初用于医学数据库。现在它仍然以同样的方式被使用,但也被其他一些以可靠性和高性能为首要任务的行业所采用:金融、交易等。 后来M(UMPS)演变为 Caché ObjectScript (COS). COS是由InterSystems公司开发的,作为M的一个超集. 其原始语言仍然被开发者社区所接受,并在一些实现中保持活力。在网络上有几个活跃的网址,比如:MUMPS Google group, Mumps User's group), effective ISO Standard等等 现代基于Globals的数据库支持交易、日志、复制、分区等。这意味着它们可以被用来构建现代的、可靠的、快速的分布式系统。 Gloabls并不将你限制于关系模型的范围内。它们让你可以自由地创建为特定任务优化的数据结构。对于许多应用来说,合理地使用好的Globals就如一颗真正的银子弹头,它所提供的速度是传统关系型应用的开发者所梦寐以求的。 作为一种存储数据的方法,globals可以在许多现代编程语言中使用,包括高级和低级语言。因此,本文将特别关注Globals本身,而不是它们曾经来自的语言。 Globals 是如何工作的 让我们先了解一下globals是如何工作的,它们有哪些优点。 我们可以从不同的角度来看待globals。在文章的这一部分,我们可将把它们看成是树形状结构或分层的数据存储空间。 简单地说,Globals是一个持久化的数组。一个自动保存在磁盘上的数组。 很难想象有什么比这更简单的方法来存储数据。在程序代码中(用COS/M语言编写),与普通关联数组的唯一区别是站在它们名字前面的^符号。 若将数据保存为Globals, 你可以不需要知道SQL,因为所有必要的命令都非常简单,在一个小时内就可以学会。 让我们从最简单的例子开始,一个有两个分支的单层的树形结构。例子是用COS(Caché ObjectScript) 写的。 Set ^a("+7926X") = "John Sidorov" Set ^a("+7916Y") = "Sergey Smith" 当数据被插入一个Global(Set命令)时,有3件事情会自动发生: 1.将数据保存到磁盘。2.编制索引。括号里的是下标,等号右边的是а节点值。3. 排序。数据是按一个键来排序的。下一次的遍历会把 "Sergey Smith "放到第一个位置,然后是 "John Sidorov"。当从global获得一个用户列表时,数据库不会在排序上花费时间。实际上你可以请求一个从任何键开始的排序列表,甚至是一个不存在的键(输出将从这个键之后的第一个真正的键开始)。所有这些操作都以惊人的速度进行。在我的个人系统(i5-3340,16GB,HDD WD 1TB Blue)上,我设法在一个进程中达到105万次插入/秒。在多核系统上,速度可以达到几千万次/秒 的插入。 当然,记录插入速度本身并不能说明什么。例如,我们可以将数据写入文本文件--根据传言,这就是Visa的处理方式。然而,通过globals,我们得到了一个结构化和索引化的存储,你可以在工作中享受其高速和易用性。 globals最大的优势是在其中插入新节点的速度。 数据在全局中总是有索引的。单层和深入的树形遍历总是非常快的。 让我们在global中添加一些二级和三级的分支看看: Set ^a("+7926X", "city") = "Moscow" Set ^a("+7926X", "city", "street") = "Req Square" Set ^a("+7926X", "age") = 25 Set ^a("+7916Y", "city") = "London" Set ^a("+7916Y", "city", "street") = "Baker Street" Set ^a("+7916Y", "age") = 36 显然,你可以使用globals建立多层级的“树”。由于每次插入后的自动索引,因而其对任何节点的访问几乎都是即时的。任何一级的树枝都可以按一个键进行排序。 正如你所看到的,数据可以被存储在键和值中。一个键的综合长度(所有索引的长度之和)可以达到511字节,而Caché中的值可以达到3.6MB的大小。树中的层数(维数)上限为31。 还有一件很酷的事情:你可以在不定义顶级节点的值的情况下建立一棵“树”。 Set ^b("a", "b", "c", "d") = 1 Set ^b("a", "b", "c", "e") = 2 Set ^b("a", "b", "f", "g") = 3 空的圆圈是没有值的节点。 为了更好地理解globals,让我们把它们与其他树进行比较:所谓“花园树”和“文件系统名称树”。 让我们把globals与最熟悉的层次结构进行比较:如下的Orchard tree-“生长在花园和田野中的普通树”,以及File system-文件系统。 我们可以看到,叶子和果实只生长在普通树木的枝干末端。文件系统--信息也被存储在树枝的末端,也被称为全文件名。 而下面这里是一个Global的数据结构: 不同: 1.内部节点:Global中的信息可以存储在每个节点中,而不是只存储在分支末端。2.外部节点:globals必须有定义的分支末端(有值的末端),这对文件系统和"花园树"来说不是强制性的。 关于内部节点,我们可以把global的结构看作是文件系统的名字树和花园树结构的超集。所以global的结构是一个更灵活的结构。 一般来说,global是一个结构化的树,支持在每个节点中保存数据。 为了更好地理解globals是如何工作的,让我们想象一下,如果文件系统的创建者使用与globals相同的方法来存储信息,会发生什么? 1. 如果一个文件夹中的最后一个文件被删除了,那么这个文件夹本身以及所有只包含这个被删除的文件夹的高层文件夹也会被删除。 2. 这样就根本不需要文件夹了。会有带子文件的文件和不带子文件的文件。如果你把它与普通的树作比较,每个分支都会变成一个果实。 3. 像README.txt这样的东西可能就不再需要了。所有你需要说的关于文件夹的内容都可以写在文件夹文件本身。一般来说,文件名和文件夹名是没有区别的(例如,/etc/readme可以是文件夹,也可以是文件),这意味着我们只需要操作文件就可以了。 4. 带有子文件夹和文件的文件夹可以更快地被删除。网络上有一些文章讲述了删除数百万个小文件是多么的耗时和困难(1, 2, 3). 然而,如果你创建一个基于Global的假的文件系统,它将只需要几秒钟甚至几分之一秒。当我在家里的电脑上测试删除子树时,我成功地从HDD(不是SDD)上的两级树上删除了96-341万个节点。值得一提的是,我们讨论的是删除Global树的一部分,而不是删除包含Global的整个文件。 子树的移除是globals的另一个优势:你不需要递归来做这个。它的速度快得令人难以置信。 在我们的树中,这可以通过一个Kill的命令来完成。 Kill ^a("+7926X") 下面是一个小表格,它可以让你更好地了解你可以在Global上执行的操作 Cache object script中与Globals有关的关键命令和功能 Set设置 设置(初始化)分支到一个节点(如果未定义)和节点值 Merge合并 复制一棵子树 Kill 删除一棵字树 ZKill 删除一个特定节点的值。源自该节点的子树不受影响。 $Query 对树进行全面深入的遍历 $Order 返回同一级别的下一个下标 $Data 检查一个节点是否被定义 $Increment 节点值的原子递增,以避免ACID的读和写。最新的建议是使用 $Sequence 来代替 感谢你的关注,我很乐意回答你的任何问题。 免责声明:本文反映了作者的个人观点,与InterSystems的官方立场无关。 让我们期待下一篇继续 "Globals 是存储数据的魔剑-树 :第二部分 (待翻译) 你将了解到哪些类型的数据可以显示在globals中,以及它们在哪些地方效果最好。
文章
姚 鑫 · 八月 22

第九章 配置数据库(一)

# 第九章 配置数据库(一) 数据库是使用数据库向导创建的 `IRIS.DAT` 文件。 `IRIS`数据库保存称为全局变量的多维数组中的数据和称为例程的可执行内容,以及类和表定义。 全局变量和例程包括方法、类、网页、SQL、BASIC和JavaScript文件 **注意:在 `Windows` 系统上,不要对 `IRIS.DAT` 数据库文件使用文件压缩。 (通过右键单击 `Windows` 资源管理器中的文件或文件夹并选择属性,然后选择高级,然后压缩内容以节省磁盘空间来压缩文件;压缩后,文件夹名称或文件名在 `Windows` 资源管理器中呈现为蓝色。)如果压缩`IRIS.DAT` 文件,它所属的实例将无法启动,并出现误导性错误。** `IRIS` 数据库根据需要动态扩展(假设有可用空间),但可以指定最大大小。如果使用默认的 `8KB` 块大小,数据库可以增长到 `32 TB`。 可以动态更改大多数数据库配置;可以在系统运行时创建和删除数据库以及修改数据库属性。 **注意:这些主题描述了使用管理门户手动配置数据库的过程。 `IRIS` 还包含可用于自动化数据库配置的编程工具。可以使用新选项卡类中的 `Config.Databases` 来创建和配置数据库;还可以使用 `^DATABASE` 命令行实用程序配置数据库。** **配置数据库的另一种方法是将 `CreateDatabase`、`ModifyDatabase` 或 `DeleteDatabase` 操作与配置合并结合使用。配置合并允许通过应用声明性合并文件来自定义 `IRIS` 实例,该文件指定要应用于该实例的设置和操作。** # Background `IRIS` 将数据——持久多维数组(`globals`)以及可执行代码(例程)——存储在一个或多个称为数据库的物理结构中。数据库由存储在本地操作系统中的一个或多个物理文件组成。一个 `IRIS` 系统可能(并且通常确实)有多个数据库。 每个 `IRIS` 系统都维护一个数据库缓存——一个本地共享内存缓冲区,用于缓存从物理数据库中检索到的数据。这种高速缓存大大减少了访问数据所需的昂贵 `I/O` 操作的数量,并提供了 `IRIS` 的许多性能优势。 `IRIS` 应用程序通过命名空间访问数据。命名空间提供存储在一个或多个物理数据库中的数据(全局变量和例程)的逻辑视图。一个 `IRIS` 系统可能(并且通常确实)有多个命名空间。 `IRIS` 将逻辑命名空间中可见的数据映射到一个或多个物理数据库。这种映射为应用程序提供了一种强大的机制,可以在不更改应用程序逻辑的情况下更改应用程序的物理部署。 在最简单的情况下,命名空间和数据库之间存在一一对应关系,但许多系统利用定义命名空间的能力来提供对多个数据库中数据的访问。例如,一个系统可以有多个命名空间,每个命名空间提供存储在一个或多个物理数据库中的数据的不同逻辑视图。 # 数据库注意事项 ## 数据库总限制 可以在单个 `IRIS` 实例中配置的数据库数量的绝对限制(如果有足够的存储空间)是 `15,998`。其他限制如下: - 数据库的目录信息不能超过 `256 KB`。这意味着,如果数据库目录名称的平均长度较长,则实例可以拥有较少的数据库总数。以下公式描述了这种关系: ```math maximum_DBs = 258048/ (avg_DB_path_length + 3) ``` 例如,如果所有数据库目录路径的格式为 `c:\InterSystems\IRIS\mgr\DBNNNN\`,则平均长度为 `33` 个字节。因此,最大数据库数为 `7,168`,计算如下:`258048/ (33 + 3) = 7168`。 - 镜像数据库在 `15,998` 的绝对限制中计数两次。如果实例上的所有数据库都进行了镜像,则有效限制为 `7,499` 个数据库。这是因为 `IRIS` 为镜像数据库创建了两个数据库定义;一个用于目录路径 (`c:\InterSystems\IRIS\mgr\DBNNNN\`),另一个用于镜像定义 (`:mirror:MIRRORNAME:MirrorDBName`)。 - 可以同时使用的数据库数量受操作系统对打开文件数量(每个进程或系统范围)的限制的限制。 `IRIS` 将大约一半的操作系统打开文件分配留给自己和设备使用。 ## 数据库配置注意事项 以下是配置数据库时要考虑的提示: - `IRIS` 提供了一个无缝选项,可以在多个物理数据库 (`IRIS.DAT`) 文件中传播数据。因此,可以根据需要构建具有多个数据库的应用程序或通过全局或下标级映射拆分数据。 - 根据可用于管理任务(如备份、恢复、完整性检查等)的基础设施,将数据库大小保持在可管理的范围内。 - 建议将流全局变量(如果将流存储在 `IRIS.DAT` 数据库文件中)全局映射到单独的数据库,并且将流数据库配置为大 (`64 KB`) 块大小。 - 根据工作负载,考虑替代(更大)块大小可能比默认的 `8 KB` 数据库块大小更有利。 ## 大数据块大小注意事项 除了 `IRIS` 支持的 `8 KB`(默认)块大小(始终启用)之外,还可以启用以下块大小: - `16 KB (16384)` - `32 KB (32768)` - `64 KB (65536)` 但是,在创建使用大块的数据库时应该谨慎,因为使用它们会影响系统的性能。 在启用和使用大的块大小之前,请考虑以下几点: - 如果应用程序工作负载主要由顺序插入或顺序读取/查询组成,那么大的块大小可以提高性能。 - 如果应用程序工作负载主要由随机插入或随机读取/查询组成,那么大的块大小可能会降低性能。 由于对于给定的数据库缓存总大小,较大的块大小会导致缓存更少的块,为了减少对随机数据库访问的影响,还应该考虑将更多的总内存用作数据库缓存。 - 对于索引类型的数据库,默认的块大小(`8 KB`)确保最佳性能; 较大的块大小可能会降低性能。 如果正在考虑为数据设置更大的块大小,那么应该考虑将索引全局变量映射到一个单独的`8 KB`块大小的数据库。 要创建一个使用不支持的块大小的数据库,请执行以下操作: 1. 使用启动设置页面(系统管理>附加设置>启动)的设置启用块大小,在配置参数文件引用的`DBSizesAllowed`条目中描述。 2. 在启动设置页面(系统管理>附加设置>启动),按照内存和启动设置中的描述,为启用的块大小配置数据库缓存。 3. 重新启动 4. 按照创建本地数据库中的说明创建数据库。 # 数据库兼容性注意事项 如创建本地数据库过程中所述,可以通过复制或移动 `IRIS.DAT` 文件将 `IRIS` 数据库复制或移动到创建它的实例之外的实例,或临时装载在另一个实例中创建的数据库在同一个系统上。还可以将数据库的备份(请参阅数据完整性指南的“备份和恢复”一章)恢复到其原始实例以外的实例。但是,为避免数据不兼容,必须满足以下要求: - 目标(新)实例必须使用相同的字符宽度(`8`位或`Unicode`; 请参阅安装指南中的新选项卡中的字符宽度设置),并使用相同的区域设置(请参阅使用管理门户的NLS设置页面)作为创建数据库的实例。 此要求的一个例外是使用基于 `ISO 8859 Latin-1` 字符集的区域设置的 `8` 位实例与使用相应宽字符区域设置的 `Unicode` 实例兼容。例如,使用 `enu8` 语言环境在 `8` 位实例中创建的数据库可以在使用 `enuw` 语言环境的 `Unicode` 实例中使用。 - 如果源实例和目标实例位于不同字节序的系统上,则数据库必须转换为目标实例的字节序后才能使用。 根据平台的不同,多字节数据存储在最低内存地址(即首先)中的最高有效字节或最低有效字节:当最高有效字节首先存储时,称为“大端;”当首先存储最低有效字节时,它被称为“小端”。 当使用在不同端序的系统上创建的现有`IRIS.DAT`定义数据库时,请在使用数据库之前使用`cvendian`实用程。
文章
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 技能会有所提高 — 但是如果您看到一些应该纠正的地方,请随意“教导”我! 如果您发现这些脚本有用,请告诉我,并不时回来看看以获取新功能和更新。
文章
姚 鑫 · 七月 11

第二章 嵌入式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 ```
文章
姚 鑫 · 七月 27

第九章 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` 用户。
文章
姚 鑫 · 三月 29, 2021

第十三章 使用动态SQL(七)

# 第十三章 使用动态SQL(七) # SQL元数据 动态SQL提供以下类型的元数据: - 在“准备”之后,描述查询类型的元数据。 - 在“准备”之后,描述查询中选择项的元数据(“列”和“扩展列信息”)。 - 在准备之后,描述查询参数的元数据:参数,`:var`参数和常量。 (语句参数,形式参数和对象) - 执行之后,描述查询结果集的元数据。在执行Prepare操作(`%Prepare()`,`%PrepareClassQuery()`或`%ExecDirect()`)之后,可以使用`%SQL.StatementMetadata`属性值。 - 可以直接为最新的`%Prepare()`返回`%SQL.Statement`元数据属性。 - 可以返回包含`%SQL.StatementMetadata`属性的oref的`%SQL.Statement%Metadata`属性。这使可以返回多个准备操作的元数据。 `SELECT`或`CALL`语句返回所有这些元数据。 `INSERT`,`UPDATE`或`DELETE`返回语句类型元数据和形式参数。 ## 语句类型元数据 使用`%SQL.Statement`类进行`Prepare`之后,可以使用`%SQL.StatementMetadata statementType`属性来确定准备哪种类型的SQL语句,如以下示例所示。本示例使用`%SQL.Statement%Metadata`属性来保存和比较两`个Prepare`操作的元数据: ```java /// d ##class(PHA.TEST.SQL).MetaData() ClassMethod MetaData() { SET tStatement = ##class(%SQL.Statement).%New() SET myquery1 = "SELECT TOP ? Name,Age,AVG(Age),CURRENT_DATE FROM Sample.Person" SET myquery2 = "CALL Sample.SP_Sample_By_Name(?)" SET qStatus = tStatement.%Prepare(myquery1) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET meta1 = tStatement.%Metadata SET qStatus = tStatement.%Prepare(myquery2) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET meta2 = tStatement.%Metadata WRITE "语句类型query 1: ",meta1.statementType,! WRITE "语句类型query 2: ",meta2.statementType,! WRITE "End of metadata" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).MetaData() 语句类型query 1: 1 语句类型query 2: 45 End of metadata ``` `statementType`属性的“类引用”条目列出了语句类型整数代码。最常见的代码是1(`SELECT`查询)和45(`CALL`到存储的查询)。 可以使用`%GetImplementationDetails()`实例方法返回相同的信息,如成功准备的结果中所述。 执行查询后,可以从结果集中返回语句类型名称(例如`SELECT`)。 ## 选择项目Select-item元数据 使用`%SQL.Statement`类准备`SELECT`或`CALL`语句之后,可以通过显示所有元数据或指定各个元数据项来返回有关查询中指定的每个选择项列的元数据。此列元数据包括ODBC数据类型信息,以及客户端类型和InterSystems Objects属性的起源以及类类型信息。 以下示例返回最近准备的查询中指定的列数: ```java /// d ##class(PHA.TEST.SQL).MetaData1() ClassMethod MetaData1() { SET myquery = "SELECT %ID AS id,Name,DOB,Age,AVG(Age),CURRENT_DATE,Home_State FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT} WRITE "Number of columns=",tStatement.%Metadata.columnCount,! WRITE "End of metadata" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).MetaData1() Number of columns=7 End of metadata ``` 以下示例返回列名称(或列别名),ODBC数据类型,最大数据长度(精度),以及每个`SELECT`项目字段的比例: ```java /// d ##class(PHA.TEST.SQL).MetaData2() ClassMethod MetaData2() { SET $NAMESPACE="SAMPLES" SET myquery=2 SET myquery(1)="SELECT Name AS VendorName,LastPayDate,MinPayment,NetDays," SET myquery(2)="AVG(MinPayment),$HOROLOG,%TABLENAME FROM Sample.Vendor" SET rset = ##class(%SQL.Statement).%New() SET qStatus = rset.%Prepare(.myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET x=rset.%Metadata.columns.Count() SET x=1 WHILE rset.%Metadata.columns.GetAt(x) { SET column=rset.%Metadata.columns.GetAt(x) WRITE !,x," ",column.colName," 是数据类型 ",column.ODBCType WRITE " 大小为 ",column.precision," 规模 = ",column.scale SET x=x+1 } WRITE !,"End of metadata" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).MetaData2() 1 VendorName 是数据类型 12 大小为 50 规模 = 0 2 LastPayDate 是数据类型 9 大小为 10 规模 = 0 3 MinPayment 是数据类型 8 大小为 6 规模 = 0 4 NetDays 是数据类型 4 大小为 3 规模 = 0 5 Aggregate_5 是数据类型 8 大小为 20 规模 = 0 6 Expression_6 是数据类型 12 大小为 255 规模 = 0 7 Literal_7 是数据类型 12 大小为 13 规模 = 0 End of metadata ``` 下面的示例使用`%SQL.StatementMetadata%Display()`实例方法显示所有列元数据: ```java /// d ##class(PHA.TEST.SQL).MetaData3() ClassMethod MetaData3() { SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare("SELECT %ID AS id,Name,DOB,Age,AVG(Age),CURRENT_DATE,Home_State FROM Sample.Person") IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT} DO tStatement.%Metadata.%Display() WRITE !,"End of metadata" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).MetaData3() Columns (SQLRESULTCOL, property 'columns'): Column Name Type Prec Scale Null Label Table Schema CType ----------- ---- ---- ----- ---- ------------ ------------ ------------ ----- id 4 10 0 0 id Person Sample 5 Name 12 50 0 0 Name Person Sample 10 DOB 9 10 0 1 DOB Person Sample 2 Age 4 10 0 1 Age Person Sample 5 Aggregate_5 2 20 8 1 Aggregate_5 14 Expression_6 9 11 0 2 Expression_6 2 Home_State 12 2 0 1 Home_State Person Sample 10 Extended Column Info (SQLRESULTCOL) Flags: 1:AutoIncrement,2:CaseSensitive,3:Currency,4:ReadOnly,5:RowVersion, 6:Unique,7:Aliased,8:Expression,9:Hidden,10:Identity,11:KeyColumn, 12:RowId Column Name Linked Prop Type Class Flags ------------ --------------------- --------------------- ----------------------- id Sample.Person Y,N,N,Y,N,Y,Y,N,N,Y,Y,Y Name Sample.Person.Name %Library.String N,N,N,N,N,N,N,N,N,N,N,N DOB Sample.Person.DOB %Library.Date N,N,N,N,N,N,N,N,N,N,N,N Age Sample.Person.Age %Library.Integer N,N,N,N,N,N,N,N,N,N,N,N Aggregate_5 %Library.Numeric N,N,N,Y,N,N,Y,N,N,N,N,N Expression_6 %Library.Date N,N,N,Y,N,N,Y,Y,N,N,N,N Home_State Sample.Address.State %Library.String N,N,N,N,N,N,N,N,N,N,N,N Statement Parameters (property 'parameters'): Nbr. Type precision scale nullable colName columntype ---- ---- --------- ----- -------- ------------ ---------- Formal Parameters (property 'formalParameters'): Nbr. Type precision scale nullable colName columntype ---- ---- --------- ----- -------- ------------ ---------- Objects: Col Column Name Extent ExportCall --- ----------- ----------------- ----------------------------- 1 id Sample.Person ##class(Sample.Person).%SQLQuickLoad ``` 这将返回所选字段的两个表列表。第一列元数据表列出了列定义信息: 显示标题 | `%SQL.StatementColumn`属性 | 描述 ---|---|--- Column Name | colName |列的SQL名称。如果为该列提供了别名,则会在此处列出该列的别名,而不是字段名称。名称和别名将被截断为12个字符。对于表达式,聚合,文字,主机变量或子查询,列出了分配的`“ Expression_n”`,`“ Aggregate_n”`,`“ Literal_n”`,`“ HostVar_n”`或`“ Subquery_n”`标签(`n`为`SELECT`项序列号)。如果为表达式,聚合,文字,主机变量或子查询分配了别名,则在此处列出该别名。 Type| ODBCType |ODBC数据类型的整数代码。请注意,这些ODBC数据类型代码与CType数据类型代码不同。 Prec| precision|精度或最大长度(以字符为单位)。日期,时间,PosixTime和TimeStamp数据类型中描述了TIME数据类型的精度和小数位元数据。 Scale| scale| 小数位数的最大数目。对于整数或非数值返回0。日期,时间,PosixTime和TimeStamp数据类型中描述了`TIME`数据类型的精度和小数位元数据。 Null| isNullable| 一个整数值,指示是否将列定义为`Non-NULL(0)`,或者是否允许`NULL(1)`。 RowID返回0。如果`SELECT`项是可能导致`NULL`的聚合或子查询,或者如果它指定`NULL`文字,则该项设置为1。如果`SELECT`项是表达式或主机变量,则设置此项到2(无法确定)。 Label| label| 列名或列别名(与列名相同)。 Table| tableName| SQL表名称。即使为表指定了别名,也始终在此处列出实际的表名。如果`SELECT`项是表达式或聚合,则不会列出任何表名。如果`SELECT`项是子查询,则列出子查询表名称。 Schema| schemaName|表的架构名称。如果未指定架构名称,则返回系统范围的默认架构。如果`SELECT`项是表达式或聚合,则不会列出任何模式名称。如果SELECT项是子查询,则不会列出任何架构名称。 CType| clientType| 客户端数据类型的整数代码。 第二列元数据表列出了扩展列信息。扩展列信息表列出了具有十二个布尔标志(SQLRESULTCOL)的每一列,这些标志被指定为Y(是)或N(否): 显示标题 | `%SQL.StatementColumn`属性 | 描述 ---|---|--- 1: AutoIncrement| isAutoIncrement| TRowID和IDENTITY字段返回Y。 2: CaseSensitive| isCaseSensitive |具有`%EXACT`归类的字符串数据类型字段返回Y。引用`%SerialObject`嵌入式对象的属性返回Y。 3: Currency| isCurrency| 使用%Library.Currency数据类型定义的字段,例如`MONEY`数据类型。 4: ReadOnly |isReadOnly|表达式,聚合,文字,`HostVar`或子查询返回Y。RowID,IDENTITY和RowVersion字段返回Y。 5: RowVersion| isRowVersion|RowVersion字段返回Y。 6: Unique| isUnique| 定义为具有唯一值约束的字段。 RowID和IDENTITY字段返回Y。 7: Aliased| isAliased| 系统为非字段选择项提供别名。因此,无论用户是否通过指定列别名替换了系统别名,表达式,聚合,文字,HostVar或子查询都将返回Y。此标志不受用户指定的列别名的影响。 8: Expression| isExpression| 表达式返回Y。 9: Hidden| isHidden| 如果使用`%PUBLICROWID`或`SqlRowIdPrivate = 0`(默认值)定义表,则RowID字段返回N。否则,RowID字段返回Y。引用`%SerialObject`嵌入式对象的属性返回Y。 10: Identity| isIdentity| 定义为IDENTITY字段的字段返回Y。如果未隐藏RowID,则RowID字段返回Y。 11: KeyColumn| isKeyColumn| 定义为主键字段或外键约束目标的字段。 RowID字段返回Y。 12: RowID| isRowId |ROWID和Identity字段返回Y. 扩展列信息元数据表列出了每个选定字段的列名称(SQL名称或列别名),链接属性(链接的持久性类属性)和类型类(数据类型类)。请注意,链接属性列出了持久性类名(不是SQL表名)和属性名(不是列别名)。 - 对于普通表字段(`SELECT Name FROM Sample.Person`): `Linked Prop=Sample.Person.Name, Type Class=%Library.String`. - 对于表格的RowID (`SELECT %ID FROM Sample.Person`): `Linked Prop= [none], Type Class=Sample.Person`. - 对于表达式,聚合,文字,`HostVar`或子查询 (`SELECT COUNT(Name) FROM Sample.Person`): `Linked Prop= [none], Type Class=%Library.BigInt`. - 供参考`%Serial Object`嵌入式对象属性 (`SELECT Home_State FROM Sample.Person`). `Linked Prop=Sample.Address.State, Type Class=%Library.String.` - 对于引用`%SerialObject`嵌入式对象的字段(`SELECT Home FROM Sample.Person`). `Linked Prop=Sample.Person.Home, Type Class=Sample.Address`. 在此示例中,`Sample.Person`中的`Home_State`字段引用`%SerialObject`类`Sample.Address`的`State`属性。 下面的示例返回带有一个形式参数(也就是语句参数)的被调用存储过程的元数据: ```java /// d ##class(PHA.TEST.SQL).MetaData4() ClassMethod MetaData4() { SET $NAMESPACE="SAMPLES" SET mysql = "CALL Sample.SP_Sample_By_Name(?)" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(.mysql) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } DO tStatement.%Metadata.%Display() WRITE !,"End of metadata" } ``` 它不仅返回列(字段)信息,还返回语句参数,形式参数和对象的值。 以下示例返回具有三个形式参数的的元数据。这三个参数之一用问号(`?`)指定,使其成为语句参数: ```java /// d ##class(PHA.TEST.SQL).MetaData5() ClassMethod MetaData5() { SET $NAMESPACE="SAMPLES" SET mycall = "CALL personsets(?,'MA')" SET tStatement = ##class(%SQL.Statement).%New(0,"sample") SET qStatus = tStatement.%Prepare(mycall) IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT} DO tStatement.%Metadata.%Display() WRITE !,"End of metadata" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).MetaData5() Columns (SQLRESULTCOL, property 'columns'): Column Name Type Prec Scale Null Label Table Schema CType ----------- ---- ---- ----- ---- ------------ ------------ ------------ ----- Extended Column Info (SQLRESULTCOL) Flags: 1:AutoIncrement,2:CaseSensitive,3:Currency,4:ReadOnly,5:RowVersion, 6:Unique,7:Aliased,8:Expression,9:Hidden,10:Identity,11:KeyColumn, 12:RowId Column Name Linked Prop Type Class Flags ------------ --------------------- --------------------- ----------------------- Statement Parameters (property 'parameters'): Nbr. Type precision scale nullable colName columntype ---- ---- --------- ----- -------- ------------ ---------- 1 12 50 0 2 name 1 Formal Parameters (property 'formalParameters'): Nbr. Type precision scale nullable colName columntype ---- ---- --------- ----- -------- ------------ ---------- 1 4 4 0 2 _isc_sp_ret_val 5 2 12 50 0 2 name 1 3 12 50 0 2 state 1 Objects: Col Column Name Extent ExportCall --- ----------- ----------------- ----------------------------- End of metadata ``` 请注意,此元数据不返回任何列信息,但是“语句参数”,“形式参数”列表包含列名称和数据类型。 ## Query参数元数据 使用`%SQL.Statement`类进行`Prepare`之后,您可以返回有关查询参数的元数据:输入参数(指定为问号(`?`)),输入主机变量(指定为`:varname`)和常量(文字值)。可以返回以下元数据: - `?`参数`:parameterCount`属性 - ODBC数据类型为`?`参数`:%SQL.StatementMetadata%Display()`实例方法“语句参数”列表。 - ?,v(:var)和c(常量)参数的列表:`%GetImplementationDetails()`实例方法,如成功准备的结果中所述。 - ?,v(:var)和c(常量)参数的ODBC数据类型:`formalParameters`属性。 `%SQL.StatementMetadata%Display()`实例方法“形式参数”列表。 - 查询文本,其中显示以下参数:`%GetImplementationDetails()`实例方法,如成功准备结果中所述。 语句元数据`%Display()`方法列出了“语句参数”和“形式参数”。对于每个参数,它列出了顺序参数号,ODBC数据类型,精度,小数位数,该参数是否可为空(2表示始终提供一个值)及其对应的属性名称(colName)和列类型。 请注意,某些ODBC数据类型以负整数形式返回。 下面的示例按顺序返回每个查询参数(`?`,`:var`和常量)的ODBC数据类型。请注意,`TOP`参数以数据类型12(`VARCHAR`)而不是数据类型4(`INTEGER`)返回,因为可以指定`TOP ALL`: ```java /// d ##class(PHA.TEST.SQL).MetaData6() ClassMethod MetaData6() { SET myquery = 4 SET myquery(1) = "SELECT TOP ? Name,DOB,Age+10 " SET myquery(2) = "FROM Sample.Person" SET myquery(3) = "WHERE %ID BETWEEN :startid :endid AND DOB=?" SET myquery(4) = "ORDER BY $PIECE(Name,',',?)" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(.myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET prepmeta = tStatement.%Metadata WRITE "Number of ? parameters=",prepmeta.parameterCount,! SET formalobj = prepmeta.formalParameters SET i=1 WHILE formalobj.GetAt(i) { SET prop=formalobj.GetAt(i) WRITE prop.colName," type= ",prop.ODBCType,! SET i=i+1 } WRITE "End of metadata" } ``` 执行`Execute`之后,无法从查询结果集元数据中获取参数元数据。在结果集中,所有参数均已解析。因此`parameterCount = 0`,`formalParameters`不包含任何数据。 ## Query结果集元数据 使用`%SQL.Statemen`t类执行`Execute`之后,可以通过调用返回结果集元数据: - `%SQL.StatementResult`类的属性。 - `%SQL.StatementResult%GetMetadata()`方法,访问`%SQL.StatementMetadata`类属性。 ### %SQL.StatementResult属性 执行查询操作后,`%SQL.StatementResult`返回: - `%StatementType`属性返回与最近执行的SQL语句相对应的整数代码。以下是这些整数代码的部分列表:`1 = SELECT; 2 = INSERT; 3 = UPDATE; 4 = DELETE or TRUNCATE TABLE; 9 = CREATE TABLE; 15 = CREATE INDEX; 45 = CALL`. - `%StatementTypeName`计算的属性基于`%StatementType`返回最近执行的SQL语句的命令名称。此名称以大写字母返回。请注意,`TRUNCATE TABLE`操作将作为`DELETE`返回。即使执行了更新操作,`INSERT OR UPDATE`也将作为`INSERT`返回。 - `%ResultColumnCount`属性返回结果集行中的列数。 下面的示例显示这些属性: ```java /// d ##class(PHA.TEST.SQL).MetaData7() ClassMethod MetaData7() { SET myquery = "SELECT TOP ? Name,DOB,Age FROM Sample.Person WHERE Age > ?" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute(10,55) IF rset.%SQLCODE=0 { WRITE "Statement type=",rset.%StatementType,! WRITE "Statement name=",rset.%StatementTypeName,! WRITE "Column count=",rset.%ResultColumnCount,! WRITE "End of metadata" } ELSE { WRITE !,"SQLCODE=",rset.%SQLCODE," ",rset.%Message } } ``` ### %SQL.StatementResult %GetMetadata() 执行之后,可以使用`%SQL.StatementResult %GetMetadata()`方法访问`%SQL.StatementMetadata`类属性。这些是在Prepare之后由`%SQL.Statement%Metadata`属性访问的相同属性。 以下示例显示了属性: ```java /// d ##class(PHA.TEST.SQL).MetaData8() ClassMethod MetaData8() { SET myquery=2 SET myquery(1)="SELECT Name AS VendorName,LastPayDate,MinPayment,NetDays," SET myquery(2)="AVG(MinPayment),$HOROLOG,%TABLENAME FROM Sample.Vendor" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(.myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() IF rset.%SQLCODE=0 { SET rsmeta=rset.%GetMetadata() SET x=rsmeta.columns.Count() SET x=1 WHILE rsmeta.columns.GetAt(x) { SET column=rsmeta.columns.GetAt(x) WRITE !,x," ",column.colName," is data type ",column.ODBCType WRITE " with a size of ",column.precision," and scale = ",column.scale SET x=x+1 } } ELSE { WRITE !,"SQLCODE=",rset.%SQLCODE," ",rset.%Message } WRITE !,"End of metadata" } ``` 请注意,结果集元数据不提供参数元数据。这是因为`Execute`操作会解析所有参数。因此,在结果集中,`parameterCount = 0`,而`formalParameters`不包含任何数据。 # 审核动态SQL InterSystems IRIS支持动态SQL语句的可选审核。启用%System /%SQL / DynamicStatement系统审核事件时,将执行动态SQL审核。默认情况下,未启用此系统审核事件。 如果启用%System /%SQL / DynamicStatement,则系统将自动审核在系统范围内执行的每个`%SQL.Statement`动态语句。审核将信息记录在审核数据库中。 要查看审核数据库,请依次转到管理门户,系统管理,选择安全性,审核,然后查看审核数据库。可以将“事件名称”过滤器设置为DynamicStatement,以将View Audit Database限制为Dynamic SQL语句。审核数据库列出了时间(本地时间戳),用户,PID(进程ID)和事件的描述。说明指定动态SQL语句的类型。例如,SQL SELECT语句(`%SQL.Statement`)或SQL CREATE VIEW语句(`%SQL.Statement`)。 通过选择事件的详细信息链接,可以列出其他信息,包括事件数据。事件数据包括执行的SQL语句和该语句的任何参数的值。例如: ```java SELECT TOP ? Name , Age FROM Sample . MyTest WHERE Name %STARTSWITH ? /*#OPTIONS {"DynamicSQLTypeList":",1"} */ Parameter values: %CallArgs(1)=5 %CallArgs(2)="Fred" ``` 事件数据的总长度(包括语句和参数)为3,632,952个字符。如果该语句和参数长于3632952,则事件数据将被截断。 InterSystems IRIS还支持ODBC和JDBC语句的审核(事件名称= XDBCStatement),以及嵌入式SQL语句的审核(事件名称= EmbeddedStatement)。
文章
Michael Lei · 七月 4, 2021

数据平台和性能 - 第 7 部分 用于确保性能、可伸缩性和可用性的企业缓存协议ECP

(ECP) Caché 出色的可用性和扩展特性之一是企业缓存协议 (ECP)。 在应用程序开发过程中,如对使用 ECP 的分布式处理加以考虑,可以横向扩展 Caché 应用程序的架构。 应用程序处理可以调整为非常高的速率,处理能力从单个应用程序服务器扩展到最多 255 个应用程序服务器,并且不需要任何应用程序更改。 在我参与的 TrakCare 部署中,ECP 已广泛使用多年。 十年前,主要供应商之一的一台“大型”x86 服务器可能总共只有八个核心。 对于大型部署来说,ECP 是横向扩展商业服务器处理能力的方式,不适合单台昂贵的大型企业服务器。 即使是高核心数的企业服务器也有限制,因此 ECP 也用于扩展这些服务器上的部署。 如今,大多数的新 TrakCare 部署或升级到当前硬件_不需要 ECP_ 即可扩展。 目前的双插槽 x86 生产服务器可以拥有数十个核心和巨大容量的内存。 我们看到,在最近的 Caché 版本中,TrakCare 以及许多其他 Caché 应用程序具有可预测的线性扩展能力,能够随着单台服务器中 CPU 核心数量和内存的增加而支持逐渐增多的用户和事务。 在现场,我看到大多数的新部署都是虚拟化的,即使如此,虚拟机也可以根据需要扩展到主机服务器的规模。 如果资源需求超过单个物理主机可以提供的资源,则使用 ECP 进行横向扩展。 - ___提示:___ _为了简化管理和部署规模,在部署 ECP 之前,先在单台服务器内扩展。_ 在本帖中,我将展示一个示例架构以及 ECP 工作原理的基础知识,然后评论性能注意事项,重点是存储。 有关配置 ECP 和应用程序开发的具体信息,请参见在线的 [Caché 分布式数据管理指南](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GDDM),并且[社区上有一个 ECP 学习轨迹](https://community.intersystems.com/learning-track/enterprise-cache-protocol-ecp-videos)。 ECP 的其他关键特性之一是提高了应用程序可用性,有关详细信息,请参见 [Caché 高可用性指南](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GHA_ecp)中的 ECP 部分。 [本系列其他帖子的列表](https://cn.community.intersystems.com/post/intersystems-数据平台的容量规划和性能系列文章) # ECP 架构基础知识 ECP 的架构和运行在概念上很简单,ECP 提供了在多个服务器系统之间有效共享数据、锁定和可执行代码的方法。 从应用程序服务器角度看,数据和代码远程存储在_数据服务器_上,但缓存在_应用程序服务器_的本地内存中,以提供对活动数据的有效访问,同时尽可能减少网络流量。 数据服务器管理对磁盘上持久性存储的数据库读写,而多个应用程序服务器是解决方案的主力,执行大多数应用程序处理。 ## 多层架构 ECP 采用多层架构。 描述处理层和它们扮演的角色有多种不同的方式,以下是我在描述基于 Web 浏览器的 Caché 应用程序时发现很有用的方式,也是我的帖子的模型和术语。 我知道可能有不同的方法来细分层级,但现在先使用我的方法 :) 基于浏览器的应用程序(例如 Caché Server Pages (CSP))使用多层架构,其中表示、应用程序处理和数据管理功能在逻辑上是分开的。 具有不同角色的__逻辑__“服务器”填充各层。 逻辑服务器不必保留在单独的物理主机或虚拟服务器上,出于成本效益和可管理性的考虑,部分甚至全部逻辑服务器可能位于单个主机或操作系统实例上。 随着部署规模的扩展,服务器可以通过 ECP 划分到多个物理或虚拟主机上,从而可根据需要分散处理工作负载,而无需更改应用程序。 主机系统可以是物理的或虚拟化的,具体取决于容量和可用性要求。 以下层和逻辑服务器构成了一个部署: - _表示层:_包括在基于浏览器的客户端和应用程序层之间充当网关的 Web 服务器。 - _应用程序层:_这是 ECP 应用程序服务器所在的位置。 如上文所述,这是一个逻辑模型,其中应用程序服务器不必与数据服务器分开,而且除了最大型的站点外,所有情况下通常都不需要分开。 该层还可能包括进行专门处理的其他服务器,如报告服务器。 - _数据层:_这是数据服务器所在的位置。 数据服务器执行事务处理,是存储在 Caché 数据库中的应用程序代码和数据存储库。 数据服务器负责读写持久性磁盘存储。 ## 逻辑架构 下图是一个基于浏览器的应用程序在部署为三层架构时的逻辑视图: 尽管初看之下该架构可能很复杂,但构成它的组件仍然与安装在单台服务器上的 Caché 系统的组件相同,只是逻辑组件安装在多个物理或虚拟服务器上。 服务器之间的所有通信都通过 TCP/IP 进行。 ### 逻辑视图中的 ECP 操作 上图从顶部开始,显示用户安全地连接到多个已进行负载平衡的 Web 服务器。 这些 Web 服务器在客户端和应用程序层(应用程序服务器)之间传递 CSP 网页请求,应用程序层进行所有处理,允许动态创建内容,并通过 Web 服务器将完成的页面返回给客户端。 在这个三层模型中,应用程序处理通过 ECP 分散到多个应用程序服务器上。 应用程序只将数据(您的应用程序数据库)视为应用程序服务器的本地数据。 当应用程序服务器发出数据请求时,它将尝试从本地缓存满足请求,如果不能满足,ECP 将向数据服务器请求必要的数据,数据服务器自己的缓存可能会满足请求,否则将从磁盘获取数据。 数据服务器对应用程序服务器的回复包括存储该数据的数据库块。 这些块将被使用,并且此时将缓存到应用程序服务器上。 ECP 自动负责管理整个网络中的缓存一致性,并将变化传播回数据服务器。 客户端会体验到快速响应,因为它们经常使用本地缓存的数据。 默认情况下,Web 服务器与首选的应用程序服务器通信,确保同一应用程序服务器满足相关数据的后续请求,因为这些数据可能已经在本地缓存中。 - ___提示:___ _如 [Caché 文档](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GDDM_develop#GDDM_develop_considerations)中详述,在循环或负载平衡方案中,应避免用户连接到应用程序服务器,因为这会影响应用程序服务器上缓存的优势。 理想情况下,相同的用户或用户组保持连接到同一应用程序服务器。_ 该解决方案通过在表示层添加 Web 服务器和在应用程序层添加其他应用程序服务器来进行扩展,无需用户停机。 数据层通过增加数据服务器的 CPU 和内存来进行扩展。 ## 物理架构 下图显示了与三层逻辑架构示例相同的三层部署中使用的物理主机示例: 请注意,在每一层部署物理或虚拟主机时均采用 n+1 或 n+2 模式,以确保在主机故障或计划维护时保持 100% 能力。 由于用户分布在多个 Web 和应用程序服务器上,单个服务器故障只会影响少量用户,他们会自动重新连接到其余服务器之一。 数据管理层具有高度可用性,例如,位于连接到一个或多个存储阵列的故障转移集群上(例如,虚拟化 HA、InterSystems 数据库镜像或传统的故障转移集群)。 如果硬件或服务出现故障,集群将在其中一个幸存节点上重启服务。 ECP 的一个附加好处是内置弹性,并且在数据库节点集群发生故障转移时能保持事务完整性,应用程序用户将观察到处理暂停,直到故障转移和自动恢复完成,随后用户将无缝继续,不会断开连接。 同样的架构也可以映射到虚拟化服务器,例如,VMware vSphere 可用于虚拟化应用程序服务器。 # ECP 容量规划 如上文所述,数据服务器管理对持久性磁盘的数据库读写,而多个应用程序服务器是解决方案的主力,执行大多数应用程序处理。 这是考虑系统资源容量规划时的一个关键概念,总结来说: - ___数据服务器___(有时称为数据库服务器)通常执行很少的应用程序处理,因此_对 CPU 要求低_,但该服务器执行大部分存储 IO,因此可能有_非常高的存储 IOPS_,即数据库读写以及日志写入(稍后将详细介绍日志 IO)。 - ___应用程序服务器___执行大多数应用程序处理,因此_对 CPU 要求高_,但存储 IO 非常少。 通常,调整 ECP 服务器 CPU、内存和 IO 要求的规则与调整非常大的单服务器解决方案的规则相同,同时考虑 N+1 或 N+2 台服务器以确保高可用性。 ## 基本 CPU 和存储规模调整: 假设 My_Application 需要最多 72 个 CPU 核心进行应用程序处理(记得还要考虑余量),并且预计在写入守护进程周期期间需要 20,000 次写入,以及 10,000 次随机数据库读取的持续峰值。 一个简单的虚拟或物理服务器规模调整方案为: - 4 台 32 CPU 应用程序服务器(3 台服务器 + 1 台服务器用于确保 HA)。 低 IOPS 要求。 - 2 台 10 CPU 数据服务器(镜像或集群以确保 HA)。 [低延迟 IOPS 要求](https://cn.community.intersystems.com/post/数据平台和性能-第-6-部分-caché-存储-io-配置文件)为 20K 写入、10K 读取,加上 WIJ 和日志。 虽然数据服务器只执行非常少的处理,但考虑到系统和 Caché 进程,将其规模调整为 8-10 个 CPU。 应用程序服务器的规模可以根据每台物理主机的最佳性价比和/或可用性来进行调整。 横向扩展时会有一些效率损失,但通常可以在服务器块中增加处理能力,并预计吞吐量有近乎线性的增长。 限制更有可能首先在存储 IO 中出现。 - ___提示:____与确保 HA 一样,要考虑主机、机箱或机架故障的影响。 在 VMWare 上虚拟化应用程序和数据服务器时,确保应用 vSphere DRS 和相关性规则以分散处理负载并确保可用性。_ ## 日志同步 IO 要求 ECP 部署的另一个容量规划注意事项是,由于日志同步,它们需要较高 IO,并且对存储响应时间的要求非常严格,以保持数据服务器上日志记录的可伸缩性 。 同步请求可以触发对日志中最后一个块的写入,以确保数据耐久性。 不过您的情况可能有所不同;在一个典型的以高事务处理速率运行的客户站点上,我经常看到非 ECP 配置上的日志写入 IOPS 为每秒十几次。 在繁忙的系统上使用 ECP 时,由于 ECP 强制日志同步,可以在日志磁盘上看到 100 到 1000 的写入 IOPS。 - ___提示:____如果在繁忙的系统上显示 mgstat 或查看 [pButtons](https://cn.community.intersystems.com/post/intersystems-数据平台和性能-–-第-1-篇) 中的 mgstat,您将看到 Jrnwrts(日志写入次数),您将在存储 IO 资源规划中对其加以考虑。 在 ECP 数据服务器上,还有未显示在 mgstat 中的对日志磁盘的日志同步写入,要了解这些信息,您需要查看日志磁盘的操作系统指标,例如使用 iostat 查看_。 ### 什么是日志同步? 需要日志同步的原因: - 确保在数据服务器发生故障时数据的耐久性和可恢复性。 - 它们也是确保应用程序服务器之间的缓存一致性的触发器。 在非 ECP 配置中,对 Caché 数据库的修改将写入日志缓冲区(128 x 64K 缓冲区),当日志缓冲区满时或每两秒由日志守护程序写入磁盘上的日志文件。 Caché 为整个缓冲区分配 64k,并且这些缓冲区总是被重复使用,而不是被销毁和重新创建,Caché 只是跟踪末尾偏移量。 在大多数情况下(除非一次进行大量更新),日志写入次数非常小。 ECP 系统中也有日志同步。 日志同步可以定义为将当前日志缓冲区的相关部分重新写入磁盘,以确保磁盘上的日志始终是最新的。 因此,日志同步会请求多次重新写入同一日志块的某个部分(大小在 2k 到 64k 之间)。 ECP 客户端上可以触发日志同步请求的事件为更新(SET 或 KILL)或 LOCK。 例如,对于每个 SET 或 KILL,都会将当前日志缓冲区写入(或重新写入)磁盘。 在非常繁忙的系统中,单次同步操作中的日志同步可能被捆绑或延迟为多个同步请求。 ### 日志同步的容量规划 为确保持续的吞吐量,日志同步的平均写入响应时间必须: - _
文章
姚 鑫 · 六月 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 ``` 在本例中, `` 元素使用默认名称空间,否则不会分配给名称空间。
文章
姚 鑫 · 六月 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`指定感兴趣的属性。
文章
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,! }
文章
Frank Ma · 六月 13

使用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应用都可以进行预测并显示结果。请在预测疾病应用程序的前端文件夹中查看源代码。