文章 Hao Ma · 五月 31, 2021 6m read

HealthConnect中创建HTTP服务端

这里我说说怎么在HealthConnect上开发HTTP服务。

作为消息引擎,HealthConnect会需要从一个接口接收HTTP请求发送到另一个接口,中间做消息转换,路由等等,目的的接口可能是HTTP,或者SOAP,REST等等。这里只介绍HTTP服务的内容,也就是最简单的两种实现:

第一种:实现客户定制的HTTP服务业务服务组件(Business Servie)

创建Business Service类,继承EnsLib.HTTP.Service, 如下面的示例:

Class SEDemo.IO.HTTP.ServiceExample1 Extends EnsLib.HTTP.Service
{
    Parameter ADAPTER;
    Method OnProcessInput(pInput As %Stream.Object, Output pOutput As %Stream.Object) As %Status
    {
        //创建Ensemble消息发送给其他组件
        set pRequest=##class(Ens.StreamContainer).%New()
        Set tSC=pRequest.StreamSet(pInput)
        set tSC= ..SendRequestAsync("Dummy1",pRequest,.pResponse)
        //创建返回Stream,发送给调用方
        set pOutput=##class(%Stream.GlobalCharacter).%New()
        do pOutput.Write("yes, I recieved request")
        Quit tSC
    }
}

详细说明:

使用CSP机制接收请求,不要用HTTP的Inbound Adpater,这样能得到

如果您学习过在Ensemble上开发SOAP接口,一定对代码里的***"Parameter ADAPTER;"***不陌生,它的作用是确定不要使用父类里的Adapter。

EnsLib.HTTP.Service有两个工种模式,一个是使用内置适配器是Ens.HTTP.InboundAdapter,还有一个是使用CSP机制,从CSP Gateway接收请求。下面的图中黄色的箭头是用适配器接收消息,这时业务服务可以定义工种的URL,端口,SSL等等;下面红色的箭头是所谓的”CSP请求“,也就是HTTP请求经过Web服务器,CSP Gateway, 到Web Application, 再被EnsLib.HTTP.Service收到。

使用CSP机制有更好的安全性和性能,所以在HealthConnect中任何HTTP的服务端接口我们都推荐CSP机制,包括HTTP接口,SOAP接口, REST接口。这些接口的开发都不要使用对应的InboundAdapter。

有关CSP Gateway的原理,还可以参见在线文档或者我的另一技术文章:Web Gateway介绍

注意的是:当不使用Adapter时,Production页面的组件配置中很多项目会消失,这些是Adapter的属性,比如允许的IP, 端口,包括编码等等。因为不用Adapter,您也不用定义IP,端口号;只有编码,可以在BS的代码里实现。实现的操作可以参考Adapter的设置: https://docs.intersystems.com/healthconnectlatest/csp/docbook/Doc.View.cls?KEY=EHTTP_settings_inbound

OnProcessInput()的入参pInput

业务组件收到的HTTP请求由pInput传入,真正的类型是%CSP.GlobalBinaryStream,它的父类是已经不推荐使用的流类型%GlobalBinaryStream。用%Stream.Object作为pInput的对象类型是合适的,这是一个新版本的Stream对象的超类,可以是任意类型的流。上面代码里业务服务组件发出的Ensemble消息的类型是StreamContainer,如果你看看消息跟踪的类型,你会发现里面流的类型是”GB",也就是一个%GlobalBinaryStream类型的流。

pInput对象的属性Stream里存放的是HTTP Body,而HTTP头放在Attributes属性里,如下图所示:

如何获得请求里的消息头

以下是用pInput.GetAttributeList()得到的Attributes的内容:

 <![CDATA[*CSPApplication/csp/healthshare/demo/CharEncoding1EnsConfigName SEDemo.IO.HTTP.ServiceExample1
        HTTPVersion1.1
        HttpRequestGET
            IParamsParamsRawParamsTranslationTableRAWAURL:/csp/healthshare/demo/SEDemo.IO.HTTP.ServiceExample1.clsaccept*/*"accept-encodinggzip, deflatecache-control
        no-cacheconnectionkeep-alivecontent-length0content-type£cookieCSPSESSIONID-SP-80-UP-csp-healthshare-=0000010100002ROlNxwgxwUQuPHKQogHGNWfADLfF2xiPwce2s; CacheBrowserId=ui$4lIZ_rJsPD_xUTPG$Rw; CSPWSERVERID=B33PnJAehost172.16.58.200mykeyimess7postman-token&83e942ce-ae73-4d98-b7ab-1e2803c95794%user-agentPostmanRuntime/7.18.0]]>

而获取消息头的某些值, 可以用 pInput.Attributes(http_header_name)得到, 比如下面这些:

    set MethodTypeName=pInput.Attributes("HttpRequest")
	set HTTPVersion = pInput.Attributes("HTTPVersion")
    set ContentType = pInput.Attributes("content-type")
    set ContentLength = pInput.Attributes("content-length")
    set URL = pInput.Attributes("URL")
    //获得GET请求,得到URL中的form参数(注意,POST请求的表单数据在消息体里面)
    set pList = Attributes("Params",form_variable_name,n), 
    //得到自定义的消息头值,比如上面的消息里面有个自定义的头字段“mykey"
    set Mykey = pInput.Attributes("mykey")

更多的细节,请参考在线文档: Using the HTTP Inbound Adap: About the Attributes Array

中文编码转码

如果请求的流里面有中文字符,需要在代码里执行转码,比如下面这个例子,把pInput的流,转码成UTF8,放到Ens.StringRequest里传输。

    set pRequest=##class(Ens.StringRequest).%New()
    set pRequest.StringValue=$ZCVT(pInput.Read(),"I","UTF8")
    set sc= ..SendRequestAsync("Dummy1",pRequest,.pResponse)

有关组件名称和访问的URL

请求的URL有两个通常的选择:

  1. Production里面的业务组件名字和类名称一样

比如上面代码的类是"SEDemo.IO.HTTP.ServiceExample1",如果production的业务服务也是同样的名字,那么调用它的URL就是

    http://hostip:port/csp/healthshare/demo/SEDemo.IO.HTTP.ServiceExample1.cls

其中"/csp/healthshare/demo/"是Web Application的名字。

  • 如果上一条不成立。比如写了一个类,用于多个业务服务组件,那么需要组件的名字可以用自己的名字,调用的URL要包含?CfgItem=xxx表示寻找不同的业务组件服务。举例说,还是上面的类,用于添加了两个业务服务组件"httpservice2"和"httpservice3",访问它们的URL就是:
    http://hostip:port/csp/healthshare/demo/SEDemo.IO.HTTP.ServiceExample1.cls?CfgItem=httpservice2
    http://hostip:port/csp/healthshare/demo/SEDemo.IO.HTTP.ServiceExample1.cls?CfgItem=httpservice3

还有这么个操作,就是单独创建一个Web Application, 配置DispatchClass,来接入一个Web服务。我觉得完全没有必要,而且新版本中Web Application的菜单里只剩下了为REST分配分派类的选择,因此这里就不说这个了。

第二种: 使用EnsLib.HTTP.GenericService预置业务服务组件

使用预置的,开发好的组件意味着不用写代码,配置一下就可以使用。相应的,灵活性上就差了,大概只适合简单的透传,转发,路由。如果要修改数据包内容,http头内容,编码转换等等,需要要在其他的组件上,比如production中消息经过的BP, BO中实现。

EnsLib.HTTP.GenericService的父类是EnsLib.HTTP.Service,因此它也是可以使用Adapter机制或者CSP标准机制。如前面所述,我们需要用CSP机制,调用的URL必须包含**?CfgItem=组件配置名**,比如我在Production里面配置了组件”GenericService1", 那么我访问的请求就应该是:

    http://localhost/csp/healthshare/demo/EnsLib.HTTP.GenericService.cls?CfgItem=GenericHTTPService1

还有,因为我们不修改代码,所以组件配置项中会有Adapter的配置,比如端口号,SSL配置,直接忽略它们,不用理睬。 。如果您看到配置项上有”字符集",显示的是UTF-8,可是没起作用,请不要奇怪,这个是Adapter的配置,您用CSP机制时它是不起作用的。

EnsLib.HTTP.GenericService向其他业务组件发出的Ensemble消息的类型是EnsLib.HTTP.GenericMessage。以下是一个POST请求被业务服务组件发送给BO的消息样例。

        <!-- type: EnsLib.HTTP.GenericMessage  id: 531 -->
        <HTTPMessage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:s="http://www.w3.org/2001/XMLSchema">
        <Stream>{
        âserial_idâ: âC20114062017025â,
        âtake_timeâ: â2019-01-02 15:04:05â,
        âpos_timeâ: â2019-01-02 15:04:05â,
        âtypeâ: â1â,
        âwarnâ: â0â,
        âfile_idâ: â9cd586eef356c71f64b82a190b469e69â,
        âfile_nameâ: âA1012014400596520160714190338.hlyâ,
        âfile_pathâ: â/service/data/TE9100Yâ,
        âbegin_timeâ: â2016-07-04 20:17:21â,
        âend_timeâ: â2016-07-04 20:18:21â,
        âlengthâ: â60â,
        âsizeâ: â9650â,
        âresultâ: â窦æ§å¿ç, æ¬æ¬¡å¿çµçæµæªè§å¼å¸¸â
        }</Stream>
        <Type>GB</Type>
        <HTTPHeaders>
        <HTTPHeadersItem HTTPHeadersKey="CSPApplication">/csp/healthshare/demo/</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="CharEncoding" xsi:nil="true"></HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="EnsConfigName">GenericHTTPService1</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="HTTPVersion">1.1</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="HttpRequest">POST</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="IParams">1</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="IParams_1">CfgItem=GenericHTTPService1</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="RawParams">CfgItem=GenericHTTPService1</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="TranslationTable">RAW</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="URL">/csp/healthshare/demo/EnsLib.HTTP.GenericService.cls</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="accept">*/*</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="accept-encoding">gzip, deflate, br</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="authorization">Basic X3N5c3RlbTpTWVM=</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="cache-control">no-cache</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="connection">keep-alive</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="content-length">532</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="content-type">text/plain</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="cookie">CSPSESSIONID-SP-52773-UP-csp-healthshare-=0000000100006fSAbXykPFTd0NFNEoaUH94y07Phmq0V92aeDg; CSPBrowserId=F4nsRY6yDGVipvQ84sDN3w; CSPWSERVERID=I33xYkI3</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="host">172.16.58.200:52773</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="postman-token">0e0bf4a7-868b-4e69-bbac-bfd89909fe99</HTTPHeadersItem>
        <HTTPHeadersItem HTTPHeadersKey="user-agent">PostmanRuntime/7.28.0</HTTPHeadersItem>
        </HTTPHeaders>
        </HTTPMessage>

使用这个组件要注意的几点:

  • 上面的数据包里有中文乱码,我是故意这么做的,就是提醒您:强调一下选用这个组件时,如果要修改数据,可以在其他的组件处理。如果我做一个简单的HTTP转发,我可以选用EnsLib.HTTP.GenericService和EnsLib.HTTP.GenericOperation这一对预置组件,这样BS,BO就免开发了。而如果中间要做中文编码的转换,我会插入一个BP, 专门处理EnsLib.HTTP.GenericMessage里的中文转码。

  • 组件的"启动标准请求"配置项:要使用CSP Gateway请求机制时应该勾选。

  • "持久消息已发送INPROC"配置项( PersistInProcData) 这个选项是专用于以InProc模式同步调用时是否要持久化Ensemble消息的选项,默认是保存。如果设置成off,那么HealthConnect即不保留消息头,也不保留消息体,在消息查看器里无法查看,也不能重传。

  • “保持标准请求分区”配置项 也是用于InProc模式调用,是否保留到BO定义的外部系统的连接。和上面的"持久消息已发送INPROC"配置项一样,很少被用到,保留默认的选中就可以。

  • "没有字符集转换”配置项 控制CSP Gateway是否对%reponse消息里的文本按content-type的类型转码,默认是不勾选,也就是保留CSP Gateway的转码功能。

  • ”One-Way"配置项 如果客户端不期待响应消息,那么选中这个选项后,EnsLib.HTTP.GenericService收到请求转发的同时会送一个Http状态码202,意思是”服务器已接受请求,但尚未处理“. 默认是不选中这个配置。

0
1 417
公告 Hao Ma · 五月 7, 2021

Hi Developers!

看看2021年4月的开发者社区新版本有些什么改善, 主要的新特性包括:

This is a release of how did we improve the Developer Community in April 2021. The key features:

  • 代码块里的‘拷贝到剪贴板’按钮
  • 帖子翻译页
  • 私信帖子作者
  • InterSystems官方版本和安全警示

以下是详细介绍。

0
0 129
文章 Hao Ma · 四月 29, 2021 3m read

经常被问到有关IRIS如何支持SSL,HTTPS的问题,有必要写个东西介绍一下。

##HTTPS的原理 简单的说,https实现两个目的:一是访问网站加密,2是确认被访问的网站是真的。

首先,被访问的网站要申请一个证书,这个证书必须是权威机构发放的,比如google, VeriSign等等,所有的浏览器里有预装了这些组织的公钥(Public Key),因此能确认你提供的证书真是这些组织给出的,而这个证书可以证明你的网站的身份。注意证书证明的是提供服务的组织和服务的真实性,和用什么设备没关系,也就是说,IRIS不管证书的事儿。

接下去,被访问的服务器可以生成公钥和私钥,和客户端交换key,生成整个世界只有两者知道的security code,用来两者之间数据的交换。详细的过程和消息交互可以在网上找到很多很好的文章和视频,比如这个: How does HTTPS work? What's a CA? What's a self-signed Certificate?

如果是测试环境或者使用者可以控制的内部网络,self-signed证书非常常用。self-signed证书就是不去花钱找人认证,而是告诉客户端,我这个证书是自己认证的,你知道我这台机器试内网的一个机器,不用权威机构证明我服务器的身份,咱们交换一下钥匙把通信加密了吧。操作系统,各种Web服务器都提供这样的假证书,可以用于测试。浏览器访问这样的网站时会提醒用户这个网站不安全,客户需要确认继续访问。

IRIS的https访问

如果要访问的是IRIS上的http服务或者页面,需要做的是在连接IRIS的Web服务器配置SSL/TLS。有关IRIS和IRIS WebGateway的介绍,请查看这个系列前面的文章

不需要在IRIS或者IRIS Gateway做任何配置。在IRIS文档里有各种有关SSL/TLS的内容,除非你要开发一个TCP层的使用SSL/TLS的应用或者IRIS作为客户端访问其他HTTPS的服务,你根本不用阅读。

下面简单介绍配置Apache Web服务器简单实现IRIS管理页面的HTTPS访问的步骤。

1.apache Web服务器安装SSL.

如果你的Apache没有安装过SSL组件,运行下面命令安装

yum -y install mod_ssl 

命令执行结束安装完成后,在/etc/httpd/modules目录会添加了mod_ssl.so,并且在/etc/httpd/conf.d 目录下会出现一个ssl.conf文件。

如果是Windows, 您需要下载使用Windows的Apache服务器,比如从这个页面:Apach2.4.46。 按照说明,您需要将软件解压缩到一个目录,比如c:/Apache24,然后执行 "httpd -k install"安装。

并且, 你要确保httpd.conf文件中下面两行没有被注释

LoadModule ssl_module modules/mod_ssl.so
Include conf/extra/httpd-ssl.conf

访问https://WebServerIP,你会被浏览器提醒这不是个可信任的网站,是不是还要继续访问,确认后会看到Apache的测试页,访问是成功的。

2.到IRIS的WebGateway的连接。

我一般放在一个单独的配置文件里,在linux下是在./conf.d/isc.conf, 在Windows系统是在./extra/httpd-isc.conf。这个配置文件是要被include在httpd.conf里面。配置https并不需要修改这个配置文件。下面是在Windows下的httpd-isc.conf的配置示意。

  LoadModule csp_module_sa C:/InterSystems/WebGateway/CSPa24.dll 
    <Location "/csp/bin/Systems/"> 
    SetHandler csp-handler-sa 
    </Location> 
    <Location "/csp/bin/RunTime/"> 
    SetHandler csp-handler-sa 
    </Location> 
    CSPFileTypes * 
    Alias /csp/ c:/InterSystems/WebGateway/csp/ 
    <Directory "c:/InterSystems/WebGateway/csp"> 
        AllowOverride None 
        Options MultiViews FollowSymLinks ExecCGI 
        Require all granted
        <FilesMatch "\.(log|ini|pid|exe)$"> 
        Require all denied 
        </FilesMatch>
    </Directory> 

这时您应该可以测试到IRIS管理页面的HTTPS访问了。

3. 获得证书并添加到Web服务器。

这步是可选的。面向公众服务的Web服务通常会购物证书, 而内部服务个个客户的网络中会有相关的CA的处理方式,相应的如何修改Apache服务器的配置请自行查看文档。

五一节快乐

0
0 424
文章 Hao Ma · 四月 18, 2021 5m read

IRIS相比Caché在部署上的一个进步是支持docker。即便不是云部署, 使用docker也带来非常多的便利。 尤其是在开发测试环节,由于docker的使用更便捷,除非要模拟客户的环境或者做规定的性能测试,我在测试中基本已经不再使用本机的实例或者虚机。IRIS的联机文档有详细的IRIS docker安装使用指导,本文只是一个简单的,快速上手的在测试环境安装IRIS docker的简单步骤,尤其适合初学者。

注意Windows上docker可能会遇到这样那样的问题,因此通常还是推荐在Linux或者Mac OS上使用。正式的生产环境的IRIS docker container也是不支持Windows系统的。

Referrence

##1. 在操作系统上安装Docker环境

Docker官方文档的安装步骤非常清晰,我按照上面的步骤在MAC和CentOS上安装docker从来没有出现过问题。恰恰是这个文档中没有的Redhat上的安装,过程中曾碰到过几个问题,在网上搜索了答案,也不算困难。

简单的复述一下步骤:root用户的最简单安装,没有权限问题, 没有docker网络修改等等:

  1. 安装yum-util, 用于添加yum源,如果您的系统中已经有了yum-utils包这步可以省略

     sudo yum install -y yum-utils
    
  2. 添加docker的Repo到yum源并确认

     sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
     sudo yum repolist
    
  3. 安装3个docker组件

     sudo yum install docker-ce docker-ce-cli containerd.io
    
  4. 启动Docker

     sudo systemctl start docker
    
  5. 确认安装成功。'docker run'命令会去本地的库里找'hello-world',没有找到就去网上下一个"images",然后创建一个docker容器给你使用。

     sudo docker run hello-world
    

安装结束您想要到https://hub.docker.com/注册一个账户,用来下载上传docker image。 下载命令使用'docker pull', 比如

docker pull nginx

2. 下载IRIS Docker image

在线文档Container Images Available from InterSystems中介绍了https://containers.intersystems.com网站上可以下载的IRIS images的列表,其中community版本不需要license,其他的版本需要从InterSystems处获得IRIS docker版的专用license.

下面是登录并下载iris docker image的记录。从网页登录https://containers.intersystems.com,您会得到docker的登录密码"使用的密码"jaRWSBJjcUcNprCKTuMX10PYHNq2IYPrAQoYdp6Siokb"。

[root@centos7 ~]# docker login -u="hma" -p="jaRWSBJjcUcNprCKTuMX10PYHNq2IYPrAQoYdp6Siokb" containers.intersystems.com
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
[root@centos7 ~]# docker pull containers.intersystems.com/intersystems/iris-ml:2020.3.0.304.0
2020.3.0.304.0: Pulling from intersystems/iris-ml
5c939e3a4d10: Pull complete
c63719cdbe7a: Pull complete
19a861ea6baf: Pull complete
651c9d2d6c4f: Pull complete
d21839215a64: Pull complete
7995f836674b: Pull complete
841ee3aaa7aa: Pull complete
739c318c2223: Pull complete
d76886412dda: Pull complete
Digest: sha256:4c62690f4d0391d801d3ac157dc4abbf47ab3d8f6b46144288e0234e68f8f10e
Status: Downloaded newer image for containers.intersystems.com/intersystems/iris-ml:2020.3.0.304.0
containers.intersystems.com/intersystems/iris-ml:2020.3.0.304.0
[root@centos7 ~]#

另外,您也可以从InterSystems的技术支持网站下载iris docker的压缩包"iris*.tar.gz",然后使用"docker load"安装, 比如:

CNMBPHMA:~ hma$ docker load -i iris-2020.4.0.524.0-docker.tar.gz

3. 创建并运行IRIS Container

###简单的办法

如果是只是简单的测试,可以直接运行,但如果不是community版本的docker, 您还需要把license拷贝到container内部。 比如下面的步骤

CNMBPHMA:~ hma$ docker run -d -i -p 52773:52773 -p 51773:51773 --name iris20204 intersystems/irishealth:2020.2.0.211.0
CNMBPHMA:~ hma$ docker cp iris.key iris20204:/usr/irissys/mgr/iris.key

需要注意的是,不同版本的超级端口可能不同,有些是1972,有些是51773。为了省事,您也许可以使用"-P"把所有container内部的端口都映射出来。

除了要拷贝license到container内部,在做测试的时候,您可能还要经常的使用"docker cp"把各种测试文件拷入container。而且,当查看和修改container内部文件的使用,还需要使用下面的命令进入container内部:

CNMBPHMA:~ hma$ docker exec -it iris20204 /bin/sh

因此,如果是您需要一个经常使用的iris container环境,我还是建议您使用下面的方法。

IRIS Container使用外部存储

简单的说,下面的命令

  • 使用"--volume"参数为container创建外部存储,将host上的"/root/data/dur"文件夹和container内部的"/dur"文件夹链接起来。(/root/data/dur文件夹要在host上手工创建,这只是一个示意,您可以使用任何可用的目录)

  • ISC_DATA_DIRECTORY环境设置会在创建iris container时在/dur文件夹下创建一个子目录”/dur/irisepy",所有IRIS运行的日志,客户配置,mgr目录下的用户数据都会保存在这个目录。因为"/dur"是外部存储,即使docker container被删除数据也不会丢失。这对container的重新创建或者iris升级都带来很大的方便。在线文档上对如果覆盖或者升级带有外部存储的iris container有详细的介绍。

  • “key"参数定义了在IRIS container创建时会把"/dur/license"目录下的iris.key拷贝到iris安装目录的mgr下并激活。也就是说,在运行命令前, 您需要在host上在"/root/data/dur"目录下创建"license"子目录并将iris.key拷贝进去。

      docker run --name irisepy --init -d\
           -p 9091:1972 -p 9092:52773\
           -v /root/data/dur:/dur\ 
           --env ISC_DATA_DIRECTORY=/dur/irisepy \
           containers.intersystems.com/intersystems/iris-ml:2020.3.0.304.0 \
           --key /dur/license/iris.key	
    

希望您喜欢使用docker

0
1 834
文章 Hao Ma · 三月 26, 2021 2m read

目前,机器学习的使用过程仍然有些困难,需要消耗大量的数据科学家服务。 AutoML 技术的诞生便是为了帮助组织降低这种复杂性和对专业 ML 人员的依赖。

AutoML 允许用户指向一个数据集,选择感兴趣的主题(特征)并设置影响主题的变量(标签)。 接着,用户告知模型名称,然后基于机器学习创建预测或数据分类模型。

这不需要了解认知算法,因为 AutoML 会在内部执行市面上的主要算法,并指出预测或分类数据的最佳算法。

用户现在可以从经过训练的模型中指向新数据,轻松进行预测和分类。

新的 InterSystems IRIS Advanced Analytics 除了与非常著名的 H2O 或 DataRobot(作为附加选项)搭配使用之外,还具有 AutoML 引擎 IntegratedML。

AutoML 运算通过 SQL 命令执行,详见:https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GIML

这就是 Sapphire 的用武之地。 它是 Open Exchange上的一个 Web 应用 (https://openexchange.intersystems.com/package/SAPPHIRE),允许连接到 IRIS Advanced Analytics,以可视方式创建和训练 IntegratedML 模型,抽象化在数据库命令行上交互的需求。

示例如下:

  1. 按照 https://openexchange.intersystems.com/package/integratedml-demo-template 的说明运行 AutoML 环境。

  2. 按照 https://openexchange.intersystems.com/package/SAPPHIRE 的说明运行 Sapphire 实例。

  3. 访问 localhost: 8080(或在 docker 上配置的端口),输入问题数据,如下图所示:

  1. 按 Save 按钮保存模型。 按 Test 按钮测试连接。

  2. 在 AutoML 菜单顶部选择 Model Definition:

  1. 在模型定义中,选择第 4 步中创建的问题,并按示例填写字段:

提示:要加载 IRIS 表,应填写 IRIS Schema 并点击 Get tables。

  1. 点击 Save 按钮保存数据。 保存后,点击 Create Model 以在 IntegratedML 中创建 ML 模型。

  2. 训练模型。 在 AutoML 菜单中选择 Train Model:

  1. 选择模型,点击 Train Model,将模型训练到 IntegratedML。 示例:

  1. 您的模型已经准备好预测新数据了!
0
0 212
文章 Hao Ma · 三月 26, 2021 15m read

关键字:IRIS,IntegratedML,Flask,FastAPI,TensorFlow Serving,HAProxy,Docker,Covid-19

目的:

过去几个月里,我们提到了一些深度学习和机器学习的快速演示,包括一个简单的 Covid-19 X 射线图像分类器和一个用于可能的 ICU 入院的 Covid-19 实验室结果分类器。  我们还介绍了 ICU 分类器的 IntegratedML 演示实现。  虽然“数据科学”远足仍在继续,但从“数据工程”的角度来看,或许也是尝试一些 AI 服务部署的好时机 - 我们能否将目前所接触到的一切都封装成一套服务 API?  我们可以利用哪些常用的工具、组件和基础架构,以最简单的方式实现这样的服务堆栈?

0
0 1420
文章 Hao Ma · 三月 26, 2021 1m read

Intersystems IRIS 是开发、运行和消耗数据科学服务的绝佳平台。 IRIS 可以使用适配器从任何类型、格式、协议和时间提取数据。 这些数据集可以通过 BPL、DTL 和 Object Script 准备,并存储为 SQL 或 NoSQL 数据。 最后,它可以被 IRIS 内部的开放 ML 算法所消耗,并在 IRIS 仪表板中可视化。 了解详情:https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=PAGE_data_science

0
0 159
文章 Hao Ma · 三月 26, 2021 4m read

图片

使用 IRIS 和 Python 创建聊天机器人

本文将展示如何把 InterSystems IRIS 数据库与 Python 集成,以服务于自然语言处理 (NLP) 的机器学习模型。

为什么选择 Python?

随着在世界范围内的广泛采用和使用,Python 拥有了出色的社区,以及许多加速器 | 库用于部署任何类型的应用。 如果您感兴趣,请访问 https://www.python.org/about/apps/

Iris Globals

我接触到 ^globals 后很快就熟悉了,它们可以用作快速获取现成数据模型中数据的方法。 因此,首先,我将使用 ^globals 存储训练数据和对话以记录聊天机器人的行为。

自然语言处理

自然语言处理或 NLP 是 AI 的一个主题,它创造了机器从我们的语言阅读、理解含义的能力。 显然,这并不简单,但是我将展示如何在这个广阔而美丽的领域中迈出您的第一步。

演示 - 来试试吧

我在这里部署了 Chatbot 应用作为演示: http://iris-python-suite.eastus.cloudapp.azure.com:8080

工作原理

机器学习

首先要知道与普通软件开发相比,机器学习具有不同的范式。 很难理解的要点是机器学习模型的开发周期。

浅显解释预警

一个标准的应用开发周期大概是这样:

开发代码 -> 测试(使用开发数据)-> 部署(真实用户数据)

机器学习代码本身不具有相同的价值。 它会与数据分担责任! 而且不是任意数据,是真实数据! 因为待执行的最终代码是由开发概念和所用数据合并生成。 所以机器学习应用周期类似于:

开发(训练)模型 + 真实数据 -> 验证 -> 部署此模型的结果

如何训练模型?

训练模型的技术有很多,每种情况和目标都需要很大的学习曲线。 在本例中,我使用的是 ChatterBot 库,该库封装了一些技术,并提供了训练方法和经过预处理的训练数据,有助于我们关注结果。

预训练的模型语言和自定义模型

您可以由此开始拥有一个基本的会话聊天机器人。 您还可以创建所有数据来训练您的聊天机器人,全面满足您的需求,但这在短时间内很难完成。 在这个项目中,我使用 en_core_web_sm 作为对话的基础,并与可以通过表单创建的自定义训练数据合并

基础架构

图片

在 Python 中使用了什么

在这个应用环境中,我使用了 Python 3.7 和这些模块:

  • PyYAML<=5.0.0
  • dash==1.12.0
  • dash-bootstrap-components==0.10.1
  • dash-core-components==1.10.0
  • dash-html-components==1.0.3
  • dash-renderer==1.4.1
  • dash-table==4.7.0
  • plotly==4.7.1
  • numpy==1.18.4
  • networkx==2.4
  • Flask>=1.0.0
  • chatterbot>=1.0.0
  • chatterbot-corpus>=1.2.0
  • SQLAlchemy>=1.2
  • ./nativeAPI_wheel/irisnative-1.0.0-cp34-abi3-linux_x86_64.whl

项目结构

本项目具有简单易懂的结构。 在主文件夹上,有 3 个最重要的子文件夹:

  • ./app:具有全部应用代码和安装配置。
  • ./iris:具有 InterSystems IRIS dockerfile,准备服务于应用。
  • ./data:通过一个将主机链接到容器环境

应用结构

现在,可以在 ./app 目录下看到一些文件:

  • chatbot.py:具有 Web 应用实现
  • iris_python_suite.py:具有一些加速器的类,通过 IRIS Native API 与 IRIS 数据库和 Python 搭配使用。

数据库结构

此应用使用 Intersystems IRIS 作为存储库,使用的 globals 包括:

  • ^chatbot.training.data:以问题和答案的形式存储所有自定义训练数据。
  • ^chatbot.conversation:存储所有对话有效负载。
  • ^chatbot.training.isupdated:控制训练管道。

其他解决方案的产品

我没有为所有对话创建报告,但这不是什么问题,使用全局图形查看器即可跟踪对话。

图片

亲自运行应用

先决条件

  • git
  • docker 和 docker-compose(以及 docker 中更多的内存设置,至少 4GB)
  • 在您的环境中访问终端

步骤

使用 docker-compose,您可以在一个环境中将所有组件和配置轻松转到 iris-python-covid19 文件夹,键入:

$ docker compose build
$ docker compose up

估计转入容器的时间

第一次运行将通过网络链接下载图像和依赖项。 如果持续时间超过 15 分钟,那可能是出现了问题,请随时来这里留言。 第一次运行后,下一次运行会好很多,只需要不到 2 分钟。

如果一切正常

一段时间后,您可以打开浏览器并转到地址:

训练数据表单

http://localhost:8050/chatbot-training-data

聊天机器人

http://localhost:8080

您应该查看 IRIS 管理门户

我暂时使用的是 USER 命名空间

http://localhost:9092
user: _SYSTEM
pass: theansweris42

如果本文对您有帮助或者有您喜欢的内容,请投票:

此应用目前正在参与 Open Exchange 竞赛,您可以在这里给我的应用 iris-python-suite 投票 (https://openexchange.intersystems.com/contest/current)

0
0 408
文章 Hao Ma · 三月 25, 2021 2m read

在Caché时代, 比较受欢迎的IRIS数据库客户端是Sqldbx和Winsql, 这两者的共同点是提供ODBC兼容的连接,而且免费。限制也差不多:只能用于Windows环境,只能用ODBC连接。 

DBeaver是我最近使用的免费SQL客户端, 推荐给各位。它有几个好处:

1
1 610
文章 Hao Ma · 三月 25, 2021 7m read

上一部分,现在要利用 IntegratedML VALIDATION MODEL 语句提供信息以监视您的 ML 模型。 您可以在此处观看实际运作。

此处所示代码衍生自 InterSystems IntegragedML 模板IRIS 文档提供的示例,我主要是把代码混合了起来。 这是一个简单的示例,目的是为进一步讨论和未来工作提供一个起点。

注:此处提供的代码仅作说明之用。 如果您想尝试,我开发了一个 Open Exchange 技术示例应用 (iris-integratedml-monitor-example),并将其提交到 InterSystems IRIS AI Contest。 读完这篇文章后您可以去看看,如果喜欢,就请投我一票吧! :)

目录

第一部分:

第二部分:

监视 ML 性能

要监视 ML 模型,至少需要两个功能:

  1. 性能指标提供程序 2) 监视和通知服务

幸运的是,IRIS 为我们提供了这两个必要的功能。

获取 ML 模型性能指标

上一部分所示,IntegratedML 提供了 VALIDATE MODEL 语句来计算以下性能参数:

  • 准确率:模型的好坏(值接近 1 表示正确答案率高)
  • 精度:模型处理误报的能力如何(值接近 1 表示误报率高)
  • 召回率:模型处理漏报的能力如何(值接近 1 表示漏报率高)
  • F 度量:另一种测量准确率的方法,用于准确率表现不佳的情况(值接近 1 表示正确答案率高)

注:这些定义并不是正式的,而且非常浅显! 我推荐您花些时间了解它们

最妙的是,每次调用 VALIDATE MODEL 时,IntegrateML 都会存储它的性能指标,这样的功能可以很好地用于监视。

监视引擎

InterSystems IRIS 提供 System Monitor 框架用于处理监视任务。 它还允许您定义自定义规则,以根据这些指标上应用的谓词触发通知。

默认提供磁盘、内存、进程、网络等一系列指标。 此外,System Monitor 还可以让您扩展监视器,覆盖无限的可能性。 这样的自定义监视器在系统监视器术语中称为应用监视器。

您可以在此处了解有关 System Monitor 的更多信息。

整合

现在,有了一种获取各模型验证性能指标值的方法,还有一个可以根据应用于自定义指标源的自定义规则触发警报的工具.……那么,是时候把它们结合起来了。

首先,我们需要通过扩展 %Monitor.Abstract 类创建自定义应用监视器类,并实现 InitializeGetSample 方法。

Class MyMetric.IntegratedMLModelsValidation Extends %Monitor.Adaptor
{

/// Initialize the list of models validation metrics.
Method Initialize() As %Status
{
    Return $$$OK
}

/// Get routine metric sample. 
/// A return code of $$$OK indicates there is a new sample instance. 
/// Any other return code indicates there is no sample instance. 
Method GetSample() As %Status
{
    Return $$$OK
}

}

系统监视器会定期发出调用以监视类,获取一组称为样本的指标。 这样的样本可以仅用于监视,也可用于检查是否必须提高警报规则。 您可以通过在监视器类中定义标准的非内部属性来定义此类样本的结构。 需要注意的是,必须在参数 INDEX 中指定其中一个属性作为每个样本的主键,否则将抛出键重复错误。

Class MyMetric.IntegratedMLModelsValidation1 Extends %Monitor.Adaptor
{

Parameter INDEX = "ModelTrainedName";

/// Name of the model definition
Property ModelName As %Monitor.String;

/// Name of the trained model being validated
Property ModelTrainedName As %Monitor.String;

/// Validation error (if encountered)
Property StatusCode As %Monitor.String;

/// Precision
Property ModelMetricPrecision As %Monitor.Numeric;

/// Recall
Property ModelMetricRecall As %Monitor.Numeric;

/// F-Measure
Property ModelMetricFMeasure As %Monitor.Numeric;

/// Accuracy
Property ModelMetricAccuracy As %Monitor.Numeric;

...

}

Initialize 方法在每次监视器调用时被调用一次,GetSample 方法则被调用到返回 0 为止。

因此,我们可以在 IntegrateML 验证历史上设置 SQL,向监视器提供指标信息,实现 InitializeGetSample 方法:

/// Initialize the list of models validation metrics.
Method Initialize() As %Status
{
    // Get the latest validation for each model validated by VALIDATION MODEL statement
    Set sql = 
    "SELECT MODEL_NAME, TRAINED_MODEL_NAME, STATUS_CODE, %DLIST(pair) AS METRICS_LIST FROM ("_
        "SELECT m.*, $LISTBUILD(m.METRIC_NAME, m.METRIC_VALUE) pair, r.STATUS_CODE "_
        "FROM INFORMATION_SCHEMA.ML_VALIDATION_RUNS r "_
        "JOIN INFORMATION_SCHEMA.ML_VALIDATION_METRICS m "_
        "ON m.MODEL_NAME = r.MODEL_NAME "_
            "AND m.TRAINED_MODEL_NAME = r.TRAINED_MODEL_NAME "_
            "AND m.VALIDATION_RUN_NAME = r.VALIDATION_RUN_NAME "_
        "GROUP BY m.MODEL_NAME, m.METRIC_NAME "_
        "HAVING r.COMPLETED_TIMESTAMP = MAX(r.COMPLETED_TIMESTAMP)"_
    ") "_
    "GROUP BY MODEL_NAME"
    Set stmt = ##class(%SQL.Statement).%New()
    $$$THROWONERROR(status, stmt.%Prepare(sql))
    Set ..Rspec = stmt.%Execute()
    Return $$$OK
}

/// Get routine metric sample. 
/// A return code of $$$OK indicates there is a new sample instance. 
/// Any other return code indicates there is no sample instance. 
Method GetSample() As %Status
{
    Set stat = ..Rspec.%Next(.sc)
    $$$THROWONERROR(sc, sc)

    // Quit if we have done all the datasets
    If 'stat {
        Quit 0
    }

    // populate this instance
    Set ..ModelName = ..Rspec.%Get("MODEL_NAME")
    Set ..ModelTrainedName = ..Rspec.%Get("TRAINED_MODEL_NAME")_" ["_$zdt($zts,3)_"]"
    Set ..StatusCode = ..Rspec.%Get("STATUS_CODE")
    Set metricsList = ..Rspec.%Get("METRICS_LIST")
    Set len = $LL(metricsList)
    For iMetric = 1:1:len {
        Set metric = $LG(metricsList, iMetric)
        Set metricName = $LG(metric, 1)
        Set metricValue = $LG(metric, 2)
        Set:(metricName = "PRECISION") ..ModelMetricPrecision = metricValue
        Set:(metricName = "RECALL") ..ModelMetricRecall = metricValue
        Set:(metricName = "F-MEASURE") ..ModelMetricFMeasure = metricValue
        Set:(metricName = "ACCURACY") ..ModelMetricAccuracy = metricValue
    }

    // quit with return value indicating the sample data is ready
    Return $$$OK
}

编译监视器类后,您需要重新启动系统监视器,使系统意识到一个新的监视器已经创建并可以使用。 您可以使用 ^%SYSMONMGR 例程或 %SYS.Monitor 类来完成这一步。

简单用例

这样就有了所需的工具来收集、监视和发布 ML 性能指标的警报。 接下来要做的是定义自定义警报规则,模拟已部署的 ML 模型开始对性能造成负面影响的场景。

首先,我们必须配置电子邮件警报及其触发规则。 这可以使用 ^%SYSMONMGR 例程完成。 不过,为了方便,我创建了一个设置方法,它可以设置所有电子邮件配置和警报规则。 您需要将 <> 之间的值替换为您的电子邮件服务器和帐户参数。

ClassMethod NotificationSetup()
{
    // Set E-mail parameters
    Set sender = "<your e-mail address>"
    Set password = "<your e-mail password>"
    Set server = "<SMTP server>"
    Set port = "<SMTP server port>"
    Set sslConfig = "default"
    Set useTLS = 1
    Set recipients = $LB("<comma-separated receivers for alerts>")
    Do ##class(%Monitor.Manager).AppEmailSender(sender)
    Do ##class(%Monitor.Manager).AppSmtpServer(server, port, sslConfig, useTLS)
    Do ##class(%Monitor.Manager).AppSmtpUserName(sender)
    Do ##class(%Monitor.Manager).AppSmtpPassword(password)
    Do ##class(%Monitor.Manager).AppRecipients(recipients)

    // E-mail as default notification method
    Do ##class(%Monitor.Manager).AppNotify(1)

    // Enable e-mail notifications
    Do ##class(%Monitor.Manager).AppEnableEmail(1)

    Set name  = "perf-model-appointments-prediction"
    Set appname = $namespace
    Set action = 1
    Set nmethod = ""
    Set nclass = ""
    Set mclass = "MyMetric.IntegratedMLModelsValidation"
    Set prop = "ModelMetricAccuracy"
    Set expr = "%1 < .8"
    Set once = 0
    Set evalmethod = ""
    // Create an alert
    Set st = ##class(%Monitor.Alert).Create(name, appname, action, nmethod, nclass, mclass, prop, expr, once, evalmethod)
    $$$THROWONERROR(st, st)

    // Restart monitor
    Do ##class(MyMetric.Install).RestartMonitor()
}

在以前的方法中,警报在监视器获得的准确率值小于 90% 时发出。

现在,设置警报规则后,让我们用前 500 条记录创建、训练和验证履约/失约预测模型,并通过前 600 条记录进行验证。

注:种子参数只是为了保证可重复性(即没有随机值),通常在生产中必须避免。

-- Creates the model
CREATE MODEL AppointmentsPredection PREDICTING (Show) FROM MedicalAppointments USING {\"seed\": 3}
-- Train it using first 500 records from dataset
TRAIN MODEL AppointmentsPredection FROM MedicalAppointments WHERE ID <= 500 USING {\"seed\": 3}
-- Show model information
SELECT * FROM INFORMATION_SCHEMA.ML_TRAINED_MODELS
|   | MODEL_NAME             | TRAINED_MODEL_NAME      | PROVIDER | TRAINED_TIMESTAMP       | MODEL_TYPE     | MODEL_INFO                                        |
|---|------------------------|-------------------------|----------|-------------------------|----------------|---------------------------------------------------|
| 0 | AppointmentsPredection | AppointmentsPredection2 | AutoML   | 2020-07-12 04:46:00.615 | classification | ModelType:Logistic Regression, Package:sklearn... |

需要注意的是,使用 AutoML 作为提供程序(PROVIDER 列),IntegrateML 从提供的数据集中采用逻辑回归算法从 scikit-learn 库(MODEL_INFO 列)推断出分类模型(MODEL_TYPE 列)。 这里必须强调“垃圾进,垃圾出”规则,即模型质量与数据质量直接相关。

接下来继续进行模型验证。

-- Calculate performace metrics of model using first 600 records (500 from trainning set + 100 for test)
VALIDATE MODEL AppointmentsPredection FROM MedicalAppointments WHERE ID < 600 USING {\"seed\": 3}
-- Show validation metrics
SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS WHERE MODEL_NAME = '%s'
| METRIC_NAME              | Accuracy | F-Measure | Precision | Recall |
|--------------------------|----------|-----------|-----------|--------|
| AppointmentsPredection21 | 0.9      | 0.94      | 0.98      | 0.91   |

模型可用于通过 PREDICT 语句执行预测:

SELECT PREDICT(AppointmentsPredection) As Predicted, Show FROM MedicalAppointments  WHERE ID <= 500
|     | Predicted | Show  |
|-----|-----------|-------|
| 0   | 0         | False |
| 1   | 0         | False |
| 2   | 0         | False |
| 3   | 0         | False |
| 4   | 0         | False |
| ... | ...       | ...   |
| 495 | 1         | True  |
| 496 | 0         | True  |
| 497 | 1         | True  |
| 498 | 1         | True  |
| 499 | 1         | True  |

然后,我们来模拟在模型中添加 200 条新记录(共 800 条记录),使模型的准确率降低到 87%。

-- Calculate performace metrics of model using first 800 records
VALIDATE MODEL AppointmentsPredection FROM MedicalAppointments WHERE ID < **800** USING {\"seed\": 3}
-- Show validation metrics
SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS WHERE MODEL_NAME = '%s'
| METRIC_NAME              | Accuracy | F-Measure | Precision | Recall |
|--------------------------|----------|-----------|-----------|--------|
| AppointmentsPredection21 | 0.9      | 0.94      | 0.98      | 0.91   |
| AppointmentsPredection22 | 0.87     | 0.93      | 0.98      | 0.88   |

由于我们早些时候设置了当准确率低于 90% 时发出电子邮件通知的规则,系统监视器意识到是时候向相关电子邮件帐户发出警报了。

在电子邮件正文中,您可以找到有关警报的信息,例如它的名称、应用监视器和触发警报的指标值。

这样一来,人们可以在收到通知后及时采取应对措施。 例如,一个操作可能只是简单的重新训练模型,但在某些情况下可能需要更详细的方法。

当然,您可以详细说明监视指标并创建更好的警报。 例如,假设您有多个 ML 模型,每个模型由不同的人员负责运行。 您可以使用模型名称指标,并为特定的电子邮件接收者设置特定的警报规则。

系统监视器还允许您用 ClassMethod 代替电子邮件。 也就是说您可以在引发警报时执行复杂的逻辑,例如自动重新训练模型。

需要注意的是,由于系统监视器定期运行 Initialize 和 GetSample 方法,所以这些方法需要仔细设计,以免占用太多系统资源。

未来工作

正如 Benjamin De Boe 注意到的,IRIS 引入了一种自定义监视任务的新方法,即 SAM 工具。 我的第一印象非常积极,因为 SAM 与 Grafana 和 Prometheus 等市面上的标准监视技术相集成。 那么,为什么不去测试如何用这样的新功能来改进这项工作呢? 因为这是留给未来工作的材料…… :)

那么就到这里了! 希望本文对您有一定的帮助。 再见!

0
0 135
文章 Hao Ma · 三月 25, 2021 4m read

几个月前,我在 MIT Technology Review 读到一篇很有意思的文章,作者解释了新冠疫情如何给全球 IT 团队带来关乎机器学习 (ML) 系统的难题。

这篇文章引起我对 ML 模型部署后如何处理性能问题的思考。

我在一个 Open Exchange 技术示例应用 (iris-integratedml-monitor-example) 中模拟了一个简单的性能问题场景,并提交到 InterSystems IRIS AI Contest。 读完这篇文章后您可以去看看,如果喜欢,就请投我一票吧! :)

目录

第一部分:

第二部分:

IRIS IntegratedML 和 ML 系统

讨论 COVID-19 以及它对全球 ML 系统的影响之前,我们先来简单谈谈 InterSystems IRIS IntegratedML。

通过将特征选择之类的任务及其与标准 SQL 数据操作语言的集成自动化,IntegratedML 可以协助开发和部署 ML 解决方案。

例如,对医疗预约的数据进行适当的操作和分析后,可以使用以下 SQL 语句设置 ML 模型,预测患者的履约/失约情况:

CREATE MODEL AppointmentsPredection PREDICTING (Show) FROM MedicalAppointments
TRAIN MODEL AppointmentsPredection FROM MedicalAppointments
VALIDATE MODEL AppointmentsPredection FROM MedicalAppointments

AutoML 提供程序将选择性能最好的特征集和 ML 算法。 这里,AutoML 提供程序使用 scikit-learn 库选择了逻辑回归模型,获得 90% 的准确率。

|   | MODEL_NAME             | TRAINED_MODEL_NAME      | PROVIDER | TRAINED_TIMESTAMP       | MODEL_TYPE     | MODEL_INFO                                        |
|---|------------------------|-------------------------|----------|-------------------------|----------------|---------------------------------------------------|
| 0 | AppointmentsPredection | AppointmentsPredection2 | AutoML   | 2020-07-12 04:46:00.615 | classification | ModelType:Logistic Regression, Package:sklearn... |
| METRIC_NAME              | Accuracy | F-Measure | Precision | Recall |
|--------------------------|----------|-----------|-----------|--------|
| AppointmentsPredection21 | 0.9      | 0.94      | 0.98      | 0.91   |

集成到 SQL 后,您可以通过估计履约和失约的患者,将 ML 模型无缝集成到现的预约系统中以提高其性能:

SELECT PREDICT(AppointmentsPredection) As Predicted FROM MedicalAppointments WHERE ID = ?

您可以在此处详细了解 IntegrateML。 有关这个简单的预测模型的更多详细信息,可以参考此处

然而,由于 AI/ML 模型在设计上是为了直接或间接地适应社会行为,因此当相关行为快速变化时,这些模型可能会受到很大影响。 最近,由于新冠疫情,我们(很遗憾地)得以实验这种场景。

新旧常态之间

MIT Technology Review 的文章所解释,新冠疫情一直在显著且迅速地改变着社会行为。 我在 Google Trends 中查询了一些文章中引用的词语,如 N95 口罩、卫生纸和消毒洗手液,确认在全球大流行中这些词语的热度有所提高:

文章中提到:

“但是它们(指由 COVID-19 引起的变化)也影响了人工智能,给库存管理、欺诈检测、营销等幕后运行的算法造成干扰。 根据正常人类行为进行训练的机器学习模型现在发现,所谓的‘正常’已经发生变化,有些模型因而不再能发挥应有的作用。”

即,在“旧常态”和“新常态”之间,我们正在经历一种“新异常”。
文章中还有这样一段话:

“机器学习模型虽然是为了应对变化而设计的, 但大多数也很脆弱。当输入数据与训练的数据相差太大时,它们的表现就会很糟糕。 (...) AI 是一种活着的引擎。”

本文继续列出一些 AI/ML 模型的示例,这些示例有的是性能突然开始受到负面影响,有的需要立即进行更改。 一些示例:

  • 零售公司的非常规产品在批量订购后缺货;
  • 由于媒体文章内容过于悲观,投资推荐服务根据情绪分析提出的建议失准;
  • 自动短语生成器由于新的语境而开始生成不合适的内容;
  • Amazon 更改了卖家推荐系统,选择自己送货的卖家,避免对其仓库物流的过度需求。

因此,我们要监控我们的 AI/ML 模型,确保模型能可靠地持续帮助客户。

到这里,希望您已经明白,对 ML 模型的创建、训练和部署并不是全部,跟踪过程也是必不可少的。 在下一篇文章中,我将展示如何使用 IRIS %Monitor.Abstract 框架来监视 ML 系统的性能,以及如何根据监视器的指标设置警报触发器。

同时,我很想知道您是否遇到过疫情导致的问题,以及您又是如何应对的。请在评论区留言吧!

敬请关注!保重身体 😊!

0
0 286
文章 Hao Ma · 三月 25, 2021 1m read

对于有海量数据运算场景的机器学习项目来说,IRIS 是一个很好的选择,原因包括:

  1. 与 MongoDB 一样,支持使用分片扩展数据存储库。
  2. 支持创建分析型多维数据集,与分片关联可提高性能。
  3. 支持使用各种数据适配器选项按计划或实时收集数据。
  4. 允许使用 Python 或 ObjectScript 中的逻辑自动化整个重复数据删除过程。
  5. 允许使用可视流 (BPL) 和数据转换器 (DTL) 协调并自动化到存储库的数据流。
  6. 通过 docker (IaC) 和 Cloud Manager 脚本提供高级自动扩展支持。
  7. 支持通过 ZPM 在配置中加载 ObjectScript 库。
  8. 与 Python 和 R 的互操作性支持实时执行机器学习。
  9. 允许使用 AutoML 引擎、IntegratedML 对所指向的数据集执行最佳算法。
  10. 允许创建执行后分析,例如 AutoML 预测和分类、Python 和 R 认知处理的输出、BI 数据透视表,并且都带有自己的视图或第三方视图。
  11. 允许使用 JReport 创建高级视图和报告。
  12. 可以通过 API 管理实现最大限度的重用和获利能力。 
0
0 477
文章 Hao Ma · 三月 25, 2021 8m read

关键字:PyODBC,unixODBC,IRIS,IntegratedML,Jupyter Notebook,Python 3

目的

几个月前,我简单谈到了关于“将 Python JDBC 连接到 IRIS”的话题。我后来频繁提起它, 因此决定再写一篇 5 分钟的笔记,说明如何“将 Python ODBC 连接到 IRIS”。

在 Windows 客户端中通常很容易设置 ODBC 和 PyODBC,不过我每次在 Linux/Unix 风格的服务器中设置 unixODBC 和 PyODBC 客户端时,都会遇到一些麻烦。

有没有一种简单连贯的方法,可以不安装任何 IRIS,在原版 Linux 客户端中让 PyODBC/unixODBC 针对远程 IRIS 服务器运行?

范围

最近,我花了点时间研究如何在 Linux Docker 环境的 Jupyter Notebook 中从头开始让一个 PyODBC 演示运行起来, 记录下这篇稍微有些繁琐的笔记,以供日后快速参考。  

范围内: 

这篇笔记将涉及以下组件:

  • PyODBC over unixODBC 
  • 安装了 TensorFlow 2.2 和 Python 3 的 Jupyter Notebook 服务器
  • 带有 IntegratedML 的 IRIS2020.3 CE 服务器,包括示例测试数据。
  • 在此环境中

  • 安装了 Docker-compose over AWS Ubuntu 16.04 的 Docker Engine 
  • Docker Desktop for MacOSDocker Toolbox for Windows 10 也经过了测试
  • 范围外

    同样,在此演示环境中不评估非功能性方面。 它们很重要,并且可以针对特定站点,如:

  • 端到端安全和审核
  • 性能和可扩展性
  • 许可和可支持性等
  • 环境

    任何原版 Linux Docker 镜像都可以用于以下配置和测试步骤,但有一个简单的方法可以在 5 分钟内设置这样的环境:

    1.  Git 克隆演示模板
    2.  在包含 docker-compose.yml 文件的克隆目录中运行“docker-compose up -d”。

    这将创建一个演示环境,如下面的拓扑所示,其中包含 2 个容器。 一个用于 Jupyter Notebook 服务器作为 PyODBC 客户端,另一个用于 IRIS2020.3 CE 服务器。

    在上面的环境中,tf2jupyter 仅包含“Python over JDBC”客户端配置;它尚不包含任何 ODBC 或 PyODBC 客户端配置。

    因此,我们将直接在 Jupyter Notebook 内部运行以下设置步骤,以使其易于说明。  

    步骤

    以下配置和测试由我在 AWS Ubuntu 16.04 服务器中运行, 由我的同事 @Thomas.Dyar 在 MacOS 中运行。 另外在 Docker Toolbox for Windows 中也进行了简单的测试。 不过,如果您遇到任何问题,还是请告诉我们。

    以下步骤可以自动化到其 Dockerfile。 我在这里特别记录一下,以防几个月后忘记。

    1. 官方文档:

  • IRIS 的 ODBC 支持
  • 在 Unix 上定义 ODBC 数据源 
  • IRIS 的 PyODBC 支持 
  • 2. 连接到 Jupyter 服务器

    我用本地 Putty 的 SSH 隧道连接到远程 AWS Ubuntu 端口 22,然后按照上述拓扑结构映射到端口 8896。

    (举个例子,在本地 Docker 环境中,也可以直接直接 http 到 Docker 机器的 IP:8896。)

    3. 从 Jupyter Notebook 中运行 ODBC 安装

    直接在 Jupyter 单元格中运行以下代码: 

    !apt-get update&lt;br>!apt-get install gcc&lt;br>!apt-get install -y tdsodbc unixodbc-dev&lt;br>!apt install unixodbc-bin -y&lt;br>!apt-get clean -y

    它将安装 gcc(包括 g++)编译器、FreeTDS、unixODBC 和 unixodbc-dev,以在下一步重新编译 PyODBC 驱动程序。

    在原生 Windows 服务器或 PC 上安装 PyODBC 不需要这一步。 

    4. 从 Jupyter 中运行 PyODBC 安装

    !pip install pyodbc
    Collecting pyodbc
      Downloading pyodbc-4.0.30.tar.gz (266 kB)
         |████████████████████████████████| 266 kB 11.3 MB/s eta 0:00:01
    Building wheels for collected packages: pyodbc
      Building wheel for pyodbc (setup.py) ... done
      Created wheel for pyodbc: filename=pyodbc-4.0.30-cp36-cp36m-linux_x86_64.whl size=273453 sha256=b794c35f41e440441f2e79a95fead36d3aebfa74c0832a92647bb90c934688b3
      Stored in directory: /root/.cache/pip/wheels/e3/3f/16/e11367542166d4f8a252c031ac3a4163d3b901b251ec71e905
    Successfully built pyodbc
    Installing collected packages: pyodbc
    Successfully installed pyodbc-4.0.30

    以上是这个 Docker 演示的最简化 pip 安装。 在官方文档中,为“MacOS X 安装”提供了更详细的 pip 安装。

    5 在 Linux 中重新配置 ODBC INI 文件和链接:

    运行以下命令重新创建 odbcinst.iniodbc.ini 链接

    !rm /etc/odbcinst.ini
    !rm /etc/odbc.ini
    !ln -s /tf/odbcinst.ini /etc/odbcinst.ini
    !ln -s /tf/odbc.ini /etc/odbc.ini

    注:这样的原因是,**第 3 步和第 4 步通常会在 \etc\ directory 下创建 2 个空白(因此无效)的 ODBC 文件。**与 Windows 安装不同,这里的空白 ini 文件会导致问题。因此需要先将其删除,然后重新创建一个链接来指向映射的 Docker 卷中提供的真实 ini 文件:/tf/odbcinst.ini 和 /tf/odbc.ini

    看一看这两个 ini 文件。在这种情况下,它们是 Linux ODBC 配置的最简形式:

    !cat /tf/odbcinst.ini
    [InterSystems ODBC35]
    UsageCount=1
    Driver=/tf/libirisodbcu35.so
    Setup=/tf/libirisodbcu35.so
    SQLLevel=1
    FileUsage=0
    DriverODBCVer=02.10
    ConnectFunctions=YYN
    APILevel=1
    DEBUG=1
    CPTimeout=&lt;not pooled>
    !cat /tf/odbc.ini
    [IRIS PyODBC Demo]
    Driver=InterSystems ODBC35
    Protocol=TCP
    Host=irisimlsvr
    Port=51773
    Namespace=USER
    UID=SUPERUSER
    Password=SYS
    Description=Sample namespace
    Query Timeout=0
    Static Cursors=0

    以上文件都已预先配置,位于映射的驱动器中。 引用的是驱动程序文件 libirisodbcu35.so,可以从 IRIS 服务器的容器实例中获取该文件(在其 {iris-installation}/bin 目录下)。

    要使上述 ODBC 安装正常运行,这 3 个文件必须存在于具有正确文件权限的映射驱动器(或任何 Linux 驱动器)中:

  • libirisodbcu35.so
  • odbcinst.ini
  • odbc.ini
  • **6. 验证 PyODBC 安装 **

    !odbcinst -j
    unixODBC 2.3.4
    DRIVERS............: /etc/odbcinst.ini
    SYSTEM DATA SOURCES: /etc/odbc.ini
    FILE DATA SOURCES..: /etc/ODBCDataSources
    USER DATA SOURCES..: /root/.odbc.ini
    SQLULEN Size.......: 8
    SQLLEN Size........: 8
    SQLSETPOSIROW Size.: 8
    import pyodbc
    print(pyodbc.drivers())
    ['InterSystems ODBC35']

    以上输出将表明 ODBC 驱动程序目前具有有效链接。

    我们应该能够在 Jupyter Notebook 中运行一些 Python ODBC 测试

    7. 运行将 Python ODBC 连接到 IRIS 的示例:

    import pyodbc 
    import time
    ### 1. Get an ODBC connection 
    #input("Hit any key to start")
    dsn = 'IRIS PyODBC Demo'
    server = 'irisimlsvr'   # IRIS server container or the docker machine's IP 
    port = '51773'   # or 8091 if docker machine IP is used
    database = 'USER' 
    username = 'SUPERUSER' 
    password = 'SYS' 
    #cnxn = pyodbc.connect('DSN='+dsn+';')   # use the user DSN defined in odbc.ini, or use the connection string below
    cnxn = pyodbc.connect('DRIVER={InterSystems ODBC35};SERVER='+server+';PORT='+port+';DATABASE='+database+';UID='+username+';PWD='+ password)
    ###ensure it reads strings correctly.
    cnxn.setdecoding(pyodbc.SQL_CHAR, encoding='utf8')
    cnxn.setdecoding(pyodbc.SQL_WCHAR, encoding='utf8')
    cnxn.setencoding(encoding='utf8')
    ### 2. Get a cursor; start the timer
    cursor = cnxn.cursor()
    start= time.clock()
    ### 3. specify the training data, and give a model name
    dataTable = 'DataMining.IrisDataset'
    dataTablePredict = 'Result12'
    dataColumn =  'Species'
    dataColumnPredict = "PredictedSpecies"
    modelName = "Flower12" #chose a name - must be unique in server end
    ### 4. Train and predict
    #cursor.execute("CREATE MODEL %s PREDICTING (%s)  FROM %s" % (modelName, dataColumn, dataTable))
    #cursor.execute("TRAIN MODEL %s FROM %s" % (modelName, dataTable))
    #cursor.execute("Create Table %s (%s VARCHAR(100), %s VARCHAR(100))" % (dataTablePredict, dataColumnPredict, dataColumn))
    #cursor.execute("INSERT INTO %s  SELECT TOP 20 PREDICT(%s) AS %s, %s FROM %s" % (dataTablePredict, modelName, dataColumnPredict, dataColumn, dataTable)) 
    #cnxn.commit()
    ### 5. show the predict result
    cursor.execute("SELECT * from %s ORDER BY ID" % dataTable)   #or use dataTablePredict result by IntegratedML if you run step 4 above
    row = cursor.fetchone() 
    while row: 
        print(row) 
        row = cursor.fetchone()
    ### 6. CLose and clean     
    cnxn.close()
    end= time.clock()
    print ("Total elapsed time: ")
    print (end-start)
    (1, 1.4, 0.2, 5.1, 3.5, 'Iris-setosa')
    (2, 1.4, 0.2, 4.9, 3.0, 'Iris-setosa')
    (3, 1.3, 0.2, 4.7, 3.2, 'Iris-setosa')
    (4, 1.5, 0.2, 4.6, 3.1, 'Iris-setosa')
    (5, 1.4, 0.2, 5.0, 3.6, 'Iris-setosa')
    ... ...
    ... ...
    ... ...
    (146, 5.2, 2.3, 6.7, 3.0, 'Iris-virginica')
    (147, 5.0, 1.9, 6.3, 2.5, 'Iris-virginica')
    (148, 5.2, 2.0, 6.5, 3.0, 'Iris-virginica')
    (149, 5.4, 2.3, 6.2, 3.4, 'Iris-virginica')
    (150, 5.1, 1.8, 5.9, 3.0, 'Iris-virginica')
    Total elapsed time: 
    0.023873000000000033

    这里有一些陷阱:

    1. **cnxn = pyodbc.connect() **- 在 Linux 环境下,此调用中传递的连接字符串必须正确无误,不能有任何空格。  
    2. 正确设置连接编码,例如使用 utf8。  在这里默认值对字符串不起作用。
    3. libirisodbcu35.so - 理想情况下,此驱动程序文件应与远程 IRIS 服务器的版本保持一致。  

    **未来计划 **

    这样就得到一个带有 Jupyter Notebook 的 Docker 环境,包括 Python3 和 TensorFlow 2.2(无 GPU),通过 PyODBC(以及 JDBC)连接到远程 IRIS 服务器。 所有定制的 SQL 语法应该都可以适用,比如 IRIS Integrated ML 专有的 SQL 语法。那么何不多研究一下 IntegratedML 的功能,用它驱动 ML 生命周期的 SQL 方法以进行一些创新? 

    另外,我希望接下来能介绍或总结出在 IRIS Native 甚至是 Python 环境中的魔法 SQL 上最简单的 IRIS 服务器挂接方法。 而且,现在有出色的 Python Gateway,我们甚至可以直接从 IRIS 服务器内部调用外部 Python ML 应用和服务。我希望我们也能在这方面多做些尝试。

    附录

    上面的笔记本文件也将被迁入此 Github 存储库以及 Open Exchange 中。

    1
    0 469
    文章 Hao Ma · 三月 25, 2021 12m read

    Covid-19 肺部 X 射线分类和 CT 检测演示 关键字:COVID-19,医学影像,深度学习,PACS Viewer 和 HealthShare。

    目的

    在这场史无前例的新冠疫情笼罩之下, 我们竭尽所能为客户提供支援,同时利用先进的 AI 技术观察着不同的疫情战线。 

    去年,我简单提及了一个深度学习演示环境。 在这个漫长的复活节周末,我们就来看一看现实世界的图像,在 Covid-19 肺部 X 射线数据集上测试运行一些深度学习模型以进行快速分类,并见证这类用于 X 射线甚至 CT 的工具如何通过 docker 等方式快速部署到云端,实现及时的“AI 分诊”并协助放射科医生。     

    这只是一个 10 分钟的快速笔记,希望通过简单的方法帮助各位上手实践。   

    0
    0 527
    文章 Hao Ma · 一月 30, 2021 4m read

    IIS在Windows Server里是默认安装,在Windows7和Windows10里面需要用户到"控制面板>程序"里面找到"Turn Windows features on or off"的设置来启动。 本文假设IIS已经在用户的服务器上启动,并且正常工作。

    通常情况下,当IRIS安装在Windows系统时,用户会在同一台机器上使用IIS,很少有单独安装一台IIS服务器给远程IRIS提供Web服务器的,当然这样也绝无问题。

    有两个软件包可以安装CSP Gateway。一个是IRIS的安装包。在IRIS的安装过程中, 如果有IIS正在工作, 那么安装程序会自动的安装CSP Gateway。 2018年以前的Ensemble或者Cache'的版本的安装过程中会跳出一个询问框,让用户选择是否"安装CSP网关。。。”。而后来的版本大多不做询问而自动为用户做了选择。 如果需要强制安装或者不安装CSP Gateway,那么需要在安装中选"Customer Component"设置。

    另一个软件包是单独的CSP Gateway安装包, 可以在InterSystems的WRC或者工程师处得到。这是一个只有10多兆的很小的安装包,它只安装CSP Gateway, 并不安装IRIS实例,适合在单独的IIS硬件服务器上安装CSP Gateway; 或者, 当一个服务器上已有了IRIS, 但后来想添加IIS服务和CSP Gateway,用这个单独的安装包也很方便。

    单独的CSP Gateway的安装会在IIS的工作目录“C:\Inetpub"下添加子目录CSPGateway,或者WebGateway, 然后在IIS的default Website上添加CSP Gateway的配置。整个配置相当简单。下面的步骤是在Windows10上单独安装Web Gateway安装包2020.1连接同一台服务器上的IRIS的的过程。

    其中后面配置CSP Gateway访问IRIS应用的部分可以参考Apache上配置CSP Gateway的文档。

    CSP Gateway安装

    1. 使用浏览器访问 http://localhost, 显示IIS的欢迎主页, 确认IIS已工作。
    2. 打开WebGateway-20201.1.xxxx-win_x64应用程序开始安装。安装时会提示用户输入连接的IRIS Server的IP地址和端口。默认的Application Name用CSP,IP地址端口使用localhost, 51773。安装过程会重启IIS服务,安装结束后用户不用手工重启。
    1. 安装后的检查IIS
    • 检查Web Gateway安装后的文件。 确认在IIS的安装目录(默认为C:\inetput)下安装了CSPGateway子目录, 其中包含若干dll文件。它们是IIS中用到的CSP Gateway的模块,在不同的CSP Gateway版本中这些Dll文件的数量和名字稍有不同。
    • 创建了c:/intersystems/WebGateway的文件夹,早些的版本中并不创建这个目录。
    • 打开IIS配置界面, 确认在Default Site下安装了CSP application. 在某些版本的Web Gateway安装后, /csp被安装为Virtual Directory, 关于IIS中application和virtual diectory的区别请自行查询, 无论安装成那种类型, 对csp的使用和配置没有区别。使用HealthConnect 2018以前版本的安装包安装的CSP Gateway生成CSP和ensemble两个Application。
    1. 查看CSP Gateway登录页面。

    登录http://localhost/csp/bin/Systems/Module.cxw。 在主页上会显示Web Gateway的版本, Web Server Type是"Microseof-IIS"。配置文件和日志文件在c:\Inetpub\CSPGateway目录下。 (如果是打开时出现HTTP错误500.19, 你需要重启IIS)

    1. 查看连接的IRIS Server。

    从左边菜单栏进入Server Access配置界面。其中csp是默认安装的IRIS连接。2018以前的版本可能用的是LOCAL。不管怎么说,使用"Edit Server"打开,可以看到连接的IRIS的端口和设置。

    配置UserName"CSPSystem"和Password "SYS",并保存。

    在左边菜单栏,使用"Test Server Connection"测试到“csp"的连接,测试成功会显示IRIS的版本。

    Test Server Connection
    Server connection test was successful: csp (localhost:51773)
    $ZVersion: IRIS for Windows (x86-64) 2020.2 (Build 199U) Tue Apr 28 2020 17:17:56 EDT
    
    1. 访问IRIS维护主页 (可选)

    从链接 http://WebServer/csp/sys/Utilhome.csp 访问IRIS维护主页System Management Portal应该可以成功了,但您会发现有部分网页内容(element)无法加载。这是因为在默认的安装中,只将"csp cls zen cxw"这4种类型的请求发送给CSP Gateway, 而被称为Static file的文件,比如.js, .css, .png等等类型的文件并没有被发送给CSP Gateway. 这是另外的一个安全机制,强制客户人工的配置是否需要从Web服务器访问IRIS维护主页。如果答案是NO, 那么访问IRIS维护页面就只能通过PWS,用IRIS服务器的52773的接口。 如果用户认为从Web服务器访问IRIS维护页面是必要的, 需要修改CSPFileTypes配置,把任意类型的请求发送给IRIS。

    1. 访问IRIS上的其他Web Application

    IRIS上其他的Web Application, 比如”/api", ”/test"等等,通常情况下当Web Application被添加后,配置会自动同步到Web Gateway, 用户不用去Web Gateway的页面配置路由。但IIS上必须填写响应的配置,才能把请求从IIS发送到CSP Gateway. 这样操作:

    • 添加applications。如下图所示, 在IIS的default site下添加了两个新application, test和api.
    • 为每个applcation配置"Handler Mappings"。使用右边动作栏中的”Add Module Mapping",而不是另3种动作。 还要注意不要勾选"Request Restrictions"的"invoke handler only if request is mapped to"选择框,这样在"Handler Mappings"页面看到的Path Type一栏中显示的是"Unspecified", 否则会显示"File"或者其他内容。
    • 测试访问一些应用, 比如

    http://172.16.58.200/api/mgmnt/v2/
    http://172.16.58.200/test/test.webservice1.cls

    一般情况下,到目前为止IIS已经能够正常工作,将需要的请求发送给IRIS。如果出现故障或者需要调整CSP gateway的配置,请参考[WebGateway系列_配置Apache连接IRIS]

    3
    0 484
    文章 Hao Ma · 一月 30, 2021 13m read

    检查Apache工作状态

    确认Apache正常工作, apache的版本已经安装路径。

    [root@centos7 ~]# httpd -v
    Server version: Apache/2.4.6 (CentOS)
    Server built:   Apr 24 2019 13:45:48
    [root@centos7 ~]# systemctl status httpd
    ● httpd.service - The Apache HTTP Server
      Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
      Active: active (running) since Mon 2020-06-15 16:46:36 CST; 5min ago
        Docs: man:httpd(8)
              man:apachectl(8)
    Main PID: 6506 (httpd)
      Status: "Total requests: 0; Current requests/sec: 0; Current traffic:   0 B/sec"
        Tasks: 272
      Memory: 31.3M
      CGroup: /system.slice/httpd.service
              ├─6506 /usr/sbin/httpd -DFOREGROUND
              ├─6592 /usr/sbin/httpd -DFOREGROUND
              ├─6607 /usr/sbin/httpd -DFOREGROUND
              ├─6608 /usr/sbin/httpd -DFOREGROUND
              ├─6609 /usr/sbin/httpd -DFOREGROUND
              ├─6610 /usr/sbin/httpd -DFOREGROUND
              ├─6611 /usr/sbin/httpd -DFOREGROUND
              ├─6612 /usr/sbin/httpd -DFOREGROUND
              ├─6613 /usr/sbin/httpd -DFOREGROUND
              ├─6622 /usr/sbin/httpd -DFOREGROUND
              ├─6623 /usr/sbin/httpd -DFOREGROUND
              └─6633 /usr/sbin/httpd -DFOREGROUND
    
    Jun 15 16:46:36 centos7 systemd[1]: Starting The Apache HTTP Server...
    Jun 15 16:46:36 centos7 systemd[1]: Started The Apache HTTP Server.
    [root@centos7 ~]#
    

    确认httpd.conf的位置。 在CentOS7中此位置为/etc/httpd/conf, 其他linux系统可能有其他位置, 如果不确认,可以使用 find命令寻找.

    [root@centos7 ~]# ll /etc/httpd/conf
    total 56
    -rw-r--r-- 1 root root   890 Jun 26  2019 healthshare.conf
    -rw-r--r-- 1 root root     0 Jun 26  2019 healthshare.conf_save
    -rw-r--r-- 1 root root 11786 Jun 30  2019 httpd.conf
    -rw-r--r-- 1 root root 11753 Jun 26  2019 httpd.conf.bak
    -rw-r--r-- 1 root root 11746 Jun 30  2019 httpd.conf2
    -rw-r--r-- 1 root root 13077 Apr 24  2019 magic
    [root@centos7 ~]#
    

    从Caché所在服务器用浏览器检查Apache测试页面可以访问。如果在Apache本地服务器访问, 网址为127.0.0.1(如果远端无法访问,请首先检查防火墙,后面步骤中有介绍)

    picture testing123

    关闭SELinux配置

    查询确认SELinux状态为disabled

    [root@centos7 ~]# getenforce
    Disabled
    

    如果非disabled状态,需要修改配置文件实现, 下图为修改后的文件内容,修改后重启电脑生效。

    [root@centos7 ~]# cat /etc/selinux/config
    # This file controls the state of SELinux on the system.
    # SELINUX= can take one of these three values:
    #     enforcing - SELinux security policy is enforced.
    #     permissive - SELinux prints warnings instead of enforcing.
    #     disabled - No SELinux policy is loaded.
    SELINUX=disabled
    # SELINUXTYPE= can take one of three two values:
    #     targeted - Targeted processes are protected,
    #     minimum - Modification of targeted policy. Only selected processes are protected.
    #     mls - Multi Level Security protection.
    SELINUXTYPE=targeted
    
    [root@centos7 ~]#
    

    检查防火墙

    确认apache所在服务器的防火墙打开了80端口。(为简化步骤, 这里不讨论Web Server的SSL接入)

    [root@centos7 ~]# systemctl status firewalld
    ● firewalld.service - firewalld - dynamic firewall daemon
      Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled)
      Active: active (running) since Mon 2020-06-15 17:24:15 CST; 2s ago
        Docs: man:firewalld(1)
    Main PID: 27433 (firewalld)
        Tasks: 2
      Memory: 25.1M
      CGroup: /system.slice/firewalld.service
              └─27433 /usr/bin/python -Es /usr/sbin/firewalld --nofork --nopid
    
    Jun 15 17:24:15 centos7 systemd[1]: Starting firewalld - dynamic firewall daemon...
    Jun 15 17:24:15 centos7 systemd[1]: Started firewalld - dynamic firewall daemon.
    [root@centos7 ~]# firewall-cmd --state
    running
    [root@centos7 ~]# firewall-cmd --zone=public --list-ports
    
    [root@centos7 ~]# firewall-cmd --zone=public --add-port=80/tcp --permanent
    Success
    [root@centos7 ~]# firewall-cmd --reload
    success
    [root@centos7 ~]# firewall-cmd --zone=public --list-ports
    80/tcp 
    [root@centos7 ~]
    

    如果Caché安装在另一台服务器, Web gateway和Caché间的通信通过Caché的superserver端口(默认1972), 因此Caché所在服务器的防火墙必须运行此端口访问。

    对Apache调优

    如果apache的工作模式为Prefork, 通过修改配置文件后重启服务,将工作模式改成“worker”(下面cat命令显示修改后的配置文件)

    [root@centos7 ~]# apachectl -V | grep MPM
    Server MPM:     prefork
    [root@centos7 ~]# vim /etc/httpd/conf.modules.d/
    [root@centos7 ~]# cat /etc/httpd/conf.modules.d/00-mpm.conf
    # Select the MPM module which should be used by uncommenting exactly
    # one of the following LoadModule lines:
    
    # prefork MPM: Implements a non-threaded, pre-forking web server
    # See: http://httpd.apache.org/docs/2.4/mod/prefork.html
    #LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
    
    # worker MPM: Multi-Processing Module implementing a hybrid
    # multi-threaded multi-process web server
    # See: http://httpd.apache.org/docs/2.4/mod/worker.html
    #
    LoadModule mpm_worker_module modules/mod_mpm_worker.so
    
    # Worker MPM parameters
    ServerLimit	40
    StartServers 	10
    MaxRequestWorkers	1000
    MinSpareThreads	75
    MaxSpareThreads	250
    ThreadsPerChild	25
    
    # event MPM: A variant of the worker MPM with the goal of consuming
    # threads only for connections with active processing
    # See: http://httpd.apache.org/docs/2.4/mod/event.html
    #
    #LoadModule mpm_event_module modules/mod_mpm_event.so
    
    [root@centos7 ~]# systemctl restart httpd
    [root@centos7 ~]# apachectl -V | grep MPM
    Server MPM:     worker
    [root@centos7 ~]#
    

    安装Web Gateway

    最新的IRIS或者HealthConnect安装包可能不包含让用户选择是否安装csp/web gateway的选项, 因此大多数情况, 用户更多的是使用专门的Web/CSP gateway的安装包来安装, 无论Apach Server 和Caché/IRIS Server是否在同一台服务器上。

    以下的介绍是用WebGateway 2020.1版本安装的过程。

    1. 解压缩安装包到一个临时文件夹 [root@centos7 ~]# tar -xzf WebGateway-2020.1.0.197.0-lnxrhx64.tar.gz

    2. 使用touch命令在/etc/httpd/conf.d目录下创建空配置文件isc.conf

    Apaceh启动时会调用主配置文件/etc/httpd/conf/httpd.conf。 该文件的默认配置项中会自动include目录/etc/httpd/conf.d下的*.conf文件, 因此不同的应用创建不同的conf文件放在conf.d目录下是方便管理的通常做法。 这个.conf的文件名可以任意名字,isc.conf只是示意。(下一步安装Web Gateway时需要输入此名字)。

    [root@centos7 httpd]# touch /etc/httpd/conf.d/isc.conf
    

    同理手动 在 /opt/webgateway/bin 下面建立CSP.ini文件,并且赋予读写权限

    1. 到解压后的安装包目录下的install子目录, 执行以下命令:

      [root@centos7 ~]# cd WebGateway-2020.1.0.197.0-lnxrhx64/ [root@centos7 WebGateway-2020.1.0.197.0-lnxrhx64]# ls install lnxrhx64 [root@centos7 WebGateway-2020.1.0.197.0-lnxrhx64]# cd install/ [root@centos7 install]# ./GatewayInstall Starting Web Gateway installation procedure.

      Please select WebServer type. Choose "None" if you want to configure your WebServer manually.

      1. Apache
      2. None WebServer type? [2] 1

      Please enter location of Apache configuration file [/etc/httpd/conf/httpd.conf]: /etc/httpd/conf.d/isc.conf

      Enter user name used by Apache server to run its worker processes :

      Please enter location of Apache executable file </usr/sbin/httpd>: Apache version 2.4 is detected.

      Please enter destination directory for Web Gateway files [/opt/webgateway]:

      Do you want to create directory /opt/webgateway [Y]:

      Please enter hostname of your InterSystems IRIS server [localhost]: HCDEMO

      Please enter superserver port number for your InterSystems IRIS server [51773]:

      Please enter InterSystems IRIS configuration name [IRIS]: HCDEMO (注 这里的 configuration name 其实是csp.ini 中服务器的配置代称,可以任意起,不一定必填为服务器本身的hostname)

      Please enter directory for static CSP content [/opt/webgateway/hcdemo]:

      Do you want to create directory /opt/webgateway/hcdemo [Y]:

      Installing InterSystems IRIS Web Gateway for Apache:

      Apache configuration file: /etc/httpd/conf.d/isc.conf InterSystems IRIS configuration name: HCDEMO InterSystems IRIS server address: HCDEMO InterSystems IRIS server port number: 51773 Web Gateway installation directory: /opt/webgateway

      Do you want to continue and perform the installation [Y]:

      Updating Apache configuration file ...
      - /etc/httpd/conf.d/isc.conf
      
      * You need to restart your Apache server before any
        configuration changes will take effect.
      

      Web Gateway configuration completed! [root@centos7 install]#

    安装结束后

    如果不是在Apache服务器访问而是远程登录该页面,此时会出现错误提示,显示CSP Gateway的版本信息和“You are not authorized to use this facility”的提醒。这是CSP Gateway的安全策略。默认不允许远程的访问,对于需要远程访问的源IP地址或者源网段,用户必须手工在CSP.ini配置文件的[SYSTEM]块里添加,比如添加 ”System_Manager=172.16.58.100",或者"System_Manger=172.16.."。虽然不推荐,但"System_Manager=...”允许任意地址远程访问的远程访问。如果CSP.ini没有自动生成,那需要手动 在 /opt/webgateway/bin 下面建立此文件,并且赋予读写权限。 下面是添加System_Manager后的CSP.ini例子:

    [root@centos7 bin]# cat /opt/webgateway/bin/CSP.ini
    [SYSTEM_INDEX]
    HCDEMO=Enabled
    LOCAL=Enabled
    
    [HCDEMO]
    Ip_Address=HCDEMO
    TCP_Port=51773
    Minimum_Server_Connections=3
    Maximum_Session_Connections=6
    
    [APP_PATH_INDEX]
    /=Enabled
    /csp=Enabled
    /hcdemo=Enabled
    
    [APP_PATH:/]
    Default_Server=HCDEMO
    Alternative_Server_0=1~~~~~~HCDEMO
    
    [APP_PATH:/csp]
    Default_Server=HCDEMO
    Alternative_Server_0=1~~~~~~HCDEMO
    
    [APP_PATH:/hcdemo]
    Default_Server=HCDEMO
    Alternative_Server_0=1~~~~~~HCDEMO
    
    [SYSTEM]
    SM_Timeout=28800
    Server_Response_Timeout=60
    No_Activity_Timeout=86400
    Queued_Request_Timeout=60
    Configuration_Initialized=Tue Nov 17 07:58:29 2020
    Configuration_Initialized_Build=2001.1740
    System_Manager=*.*.*.*
    
    [LOCAL]
    Ip_Address=127.0.0.1
    TCP_Port=1972
    Minimum_Server_Connections=3
    Maximum_Session_Connections=6
    [root@centos7 bin]#
    

    登录Web Gateway管理页面的抓图

    检查Web Gateway的配置文件位置,版本,log位置

    配置CSP Gateway到IRIS的连接,并测试从Apache登录IRIS维护界面

    在CSP Gateway配置页面,查看Server Access。

    Server Access中会列出这本CSP Gateway连接的IRIS实例的列表。在上面的安装步骤中,当问到了“ Please enter hostname of your InterSystems IRIS server [localhost]: HCDEMO ”选择了HCDEMO, 这时这个列表中会显示有两个Server, localhost和HCDEMO. (localhost无法被删除,遗憾)

    下面检查HCDEMO Server的配置

    • 检查服务器地址为127.0.0.1
    • 添加到Caché服务器的账号密码,默认为CSPSystem, SYS

    如果IIS服务器+Web Gateway和Caché位于两个不同的服务器, 或者需要添加到另一Caché Server的连接, 需要添加Server, 如下图, 需要的配置: Caché服务器的IP,superserver端口号, CSPSystem用于的密码,服务器的类型(可选)

    测试Caché Server连接成功

    双击左边菜单栏的"Test Server Connection", 确认结果中收到"Server connection test was successful: ...."的结果。

    访问IRIS维护主页 (可选)

    从链接 http://WebServer/csp/sys/Utilhome.csp 访问IRIS维护主页System Management Portal应该可以成功了,但您会发现有部分网页内容(element)无法加载。这是因为在默认的安装中,isc.conf中CSP Gateway路径的<Directory>配置的"CSPFileTypes csp cls zen cxw"中只将这4种类型的请求发送给CSP Gateway, 而被称为Static file的文件,比如.js, .css, .png等等类型的文件并没有被发送给CSP Gateway. 这是另外的一个安全机制,强制客户人工的配置是否需要从Web服务器访问IRIS维护主页。如果答案是NO, 那么访问IRIS维护页面就只能通过PWS,用IRIS服务器的52773的接口。 如果用户认为从Web服务器访问IRIS维护页面是必要的, 需要修改CSPFileTypes配置,比如修改成"CSPFileTypes *",作用是把任意类型的请求发送给IRIS。以下是安装并修改后的isc.conf文件示例。

    [root@centos7 conf.d]# cat isc.conf
    #### BEGIN-ApacheCSP-SECTION ####
    LoadModule csp_module_sa "/opt/webgateway/bin/CSPa24.so"
    CSPModulePath "/opt/webgateway/bin/"
    CSPConfigPath "/opt/webgateway/bin/"
    Alias /csp/ "/opt/webgateway/hcdemo/csp/"
    <Location "/csp/bin/Systems/">
      SetHandler csp-handler-sa
    </Location>
    <Location "/csp/bin/RunTime/">
      SetHandler csp-handler-sa
    </Location>
    
    <Directory "/opt/webgateway/hcdemo/csp">
      CSPFileTypes *
      AllowOverride None
      Options MultiViews FollowSymLinks ExecCGI
      Require all granted
      <FilesMatch "\.(log|ini|pid|exe)$">
        Require all denied
      </FilesMatch>
    </Directory>
    <Directory "/opt/webgateway/bin/">
      AllowOverride None
      Options None
      Require all granted
      <FilesMatch "\.(log|ini|pid|exe)$">
        Require all denied
      </FilesMatch>
    </Directory>
    #### END-ApacheCSP-SECTION ####
    #### BEGIN-ApacheCSP-SECTION-HCDEMO ####
    Alias /hcdemo/csp/ "/opt/webgateway/hcdemo/csp/"
    #### END-ApacheCSP-SECTION-HCDEMO ####
    [root@centos7 conf.d]#
    

    注意isc.conf修改后需要重启apache server [root@centos7 conf.d]# systemctl restart httpd [root@centos7 conf.d]#

    访问IRIS上的其他Web Application

    IRIS上其他的Web Application需要经过配置才可以发送到IRIS Server。这些Web Application可能是一个访问HTTP, REST的URL, 或者是一个用户自己定义的SOAP,甚至可能是一个简单的CSP文件。要确保他们被发送给IRIS Server, 用户需要:

    1. 配置Apache配置文件isc.conf, 保证请求被发送给了CSP Gateway。 可以通过CSP Gateway管理页面的HTTP Trace来确认。
    2. 如果需要,配置CSP Gateway, 将请求发送给IRIS.

    访问带文件后缀的应用

    在isc.conf中的<Directory>中定义的是Web Server中文件对象的地址,比如"/opt/webgateway/bin/"是CSP Gateway的.so文件的存放位置。 Alias是URL中资源地址,比如"/csp/"到<Directory>定义的映射。他们在apache中注册一个有后缀的文件的发送路径, 这个配置使得访问"http://WebServer/csp/sys/Utilhome.csp"可以成功发送给CSP Gateway。

    Alias /csp/ "/opt/webgateway/hcdemo/csp/"
    <Directory "/opt/webgateway/hcdemo/csp">
          CSPFileTypes *
          AllowOverride None
          Options MultiViews FollowSymLinks ExecCGI
          Require all granted
          <FilesMatch "\.(log|ini|pid|exe)$">
            Require all denied
          </FilesMatch>
    </Directory>
    

    对于其他的Web Application, 比如如果需要将"http://WebServer/test/Hello.csp"成功发送给CSP Gateway, 需要添加以下配置,它把路径为”/test/"的URL发送给CSP Gateway处理。

    Alias /test/ "/opt/webgateway/hcdemo/csp/"
    

    测试连接一个SOAP服务,注意这个服务要在IRIS的Web Applicatin里配置正确,它至少可以从PWS用匿名用户访问。(关于Web Application的配置另行文档, 简单说, 要匿名访问, 要使用%Security_WebGateway的资源).测试结果:

    [root@centos7 conf.d]# curl http://172.16.58.100/test/test.webservice1.cls?soap_method=winter
    <?xml version="1.0" encoding="UTF-8" ?>
    <SOAP-ENV:Envelope xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:s='http://www.w3.org/2001/XMLSchema'>
      <SOAP-ENV:Body><winterResponse xmlns="http://tempuri.org"><winterResult>Winter is Coming...</winterResult></winterResponse></SOAP-ENV:Body>
    </SOAP-ENV:Envelope>
    [root@centos7 conf.d]#
    

    访问其他URL应用

    对于 “http://172.16.58.100/api/mgmnt/v2/”这样的URL地址, 映射到CSP Gateway处理需要的配置是<Location>。下面的配置保证对"/api/"开头的,没有文件地址的URL的处理:

    <Location "/api/">
          SetHandler csp-handler-sa
          CSP on
    </Location>
    

    请求的结果如下:

    [root@centos7 ~]# curl -X GET "http://172.16.58.100/api/mgmnt/v2/" [{"name":"%Api.IAM.v1","webApplications":"/api/iam","dispatchClass":"%Api.IAM.v1.disp","namespace":"%SYS","swaggerSpec":"/api/mgmnt/v2/%25SYS/%25Api.IAM.v1"},{"name":"%Api.Mgmnt.v2","webApplications":"/api/mgmnt","dispatchClass":"%Api.Mgmnt.v2.disp","namespace":"%SYS","swaggerSpec":"/api/mgmnt/v2/%25SYS/%25Api.Mgmnt.v2"},{"name":"PetStore","dispatchClass":"PetStore.disp","namespace":"DEMO","swaggerSpec":"/api/mgmnt/v2/DEMO/PetStore"}][root@centos7 ~]#

    备注: 如果得到了{"msg":"错误 #8754: Unable to use namespace: USER."},或者403 forbidden, 需要在IRIS上给Web Application "/api/mgmnt"添加”%DB_USER"的权限;或者,也可以将应用的“安全设置”设成"密码",然后使用下面的命令查看:

    [root@centos7 conf.d]# curl -i -X GET http://172.16.58.100/api/mgmnt/v2/ -u "_system:SYS"
    HTTP/1.1 200 OK。。。(后面省略)
    [{"name":"%Api.IAM.v1","webApplications":"/api/iam","dispatchClass":"%Api.IAM.v1.disp","namespace":"%SYS","swaggerSpec":"/api/mgmnt/v2/%25SYS/%25Api.IAM.v1"},{"name":"%Api.Mgmnt.v2","webApplications":"/api/mgmnt","dispatchClass":"%Api.Mgmnt.v2.disp","namespace":"%SYS","swaggerSpec":"/api/mgmnt/v2/%25SYS/%25Api.Mgmnt.v2"},{"name":"PetStore","dispatchClass":"PetStore.disp","namespace":"DEMO","swaggerSpec":"/api/mgmnt/v2/DEMO/PetStore"}][root@centos7 conf.d]#
    

    CSP Gateway配置 "Application Access"

    通常情况下,CSP Gateway测试成功连接IRIS Server后,会发现IRIS上的Web Application列表,并添加到自己的”APPlicaiton Access"列表里。如下图所示。每次用户在IRIS添加一个新的Web应用, 只需要在isc.conf上做相应的配置,无需人工去更新CSP Gateway的配置。

    特殊的情况,如果发现某个URL无法发送到IRIS。先打开了CSP Gateway的HTTP Trace,确认CSP Gateway可以收到请求消息但无法发送到IRIS, 这是需要人工检查并且配置"Application Access".

    0
    0 599
    文章 Hao Ma · 一月 30, 2021 6m read

    本文介绍InterSystems Web Gateway的安装和配置。

    在2018以前的ISC产品中, InterSystems Web Gateway被称为CSP Gateway。, CSP是Cache'的页面技术。InterSystems的产品页面,Web服务等大多是CSP写成的。IRIS发布后CSP Gateway改名成Web Gateway, 但内部的配置文件,说明等等还到处可见CSP Gateway的叫法。在本文里不同的地方有这两个说法别奇怪,他们是一个东西。 IRIS通过它和外部Web服务器连接。 本文的内容适用任何ISC产品的部署,包括页面的选项Cache'. IRIS,HealthConnect, Ensemble等等。它的作用和表现是一样的。 无论您使用的是Cache',IRIS , HealthConnect还是HealthShare, 只有在生产环境中使用HTTP请求,基本上都需要使用Web Gateway。

    如果需要更详细的内容,请参考在线文档:InterSystems Web Gateway

    什么是Web Gateway

    CSP是Cache' Server Page的缩写,如同JSP(Java Server Page)是Java的前端技术, CSP是InterSystems的前端技术。要在IRIS或者HealthConnect上提供一个HTTP服务,唯一安全可靠的技术是CSP. 创建HTTP,REST服务直接创建CSP页面, 创建SOAP服务使用%SOAP.WebService或者EnsLib.SOAP.Service, 它们都是%CSP.Page的子类,因此在IRIS的在线文档中有CSP Server的称法,指的就是IRIS中负责CSP处理的那部分功能。

    CSP Server并不监听TCP端口得到HTTP消息,它只能通过CSP Gateway从Web服务器接收请求。用户的请求要先发给IIS/Apach/Nginx等Web服务器,转发给IRIS, 而Web Gateway就是Web服务器发请求给IRIS所使用的网关。

    或者说, 它是InterSystems提供的给第三方Web服务器的一个组件,或者称为模块。在Windows系统中是若干DLL库文件,在LINUX环境是SO动态链接库。安装CSP Gateway就是诸如”CSPa24.so"等文件拷贝到Web服务器的目录,将这些模块配置到Web服务器,并将以.csp,.cls,.zen结尾的HTTP请求发送给IRIS。如果Web服务器和IRIS独立安装在不同的硬件服务器上(更安全的方式),发送的是TCP消息,到IRIS的superserver端口,默认是51773(Cache'是1972)。

    CSP Gateway支持3种Web服务器:IIS, Apache Web Server, Nginx。 后面的链接提供了完整的在各种操作系统中ISC产品支持的Web Server的版本: IRIS支持的第三方Web Server列表

    听上去是不是挺简单?那用户还有什么可糊涂的?

    Private Web Server(PWS)带来的混乱

    混乱来自IRIS的安装过程会安装一个私有的Apache Web服务器,被称作PWS。它的作用有两个:支持访问维护页面;给一个测试环境提供测试Web服务的能力。在线文档是这么描述PWS的: >> The PWS is not supported for any other purpose. For deployments of http-based applications, including REST, CSP, Zen, and SOAP over http or https, you should not use the private web server for any application other than the Management Portal; instead, you must install and deploy one of the supported web servers. For information, see the section “Supported Web Servers” in the online InterSystems Supported Platforms document for this release.(如果要部署http应用, 包括在http或者https上层的REST, CSP, Zen, SOAP,你绝不能让除Management Portal以外的任何应用使用PWS. 你必须安装一个IRIS兼容的Web服务器。了解这部分内容, 请查看InterSystems在线文档的"Supported Web Servers"部分)

    然后很多用户没有意识到这个提醒。当安装IRIS时被问到”你想要安装CSP网关并未CSP网关配置外部Web服务器(IIS和Apache)吗?"时,他们选择了“不要安装CSP网关",然后浏览器接入维护界面,开发了若干Web服务,一直没有意识使用PWS访问IRIS上的Web服务在生产环境是不可接受的。PWS是一个非常轻量级的Apache Web服务器。它的程序包在IRIS安装目录下的httpd子目录里。IRIS启动后, 它开始工作,监听IRIS上配置的Web端口,默认是57772,或者52773.它的工作机制决定了它无法承受大的负载,因此不能用于生产环境的http应用。

    它和CSP/IRIS Server的连接用的是与上面讲的CSP Gateway完全相同的方式,也就是说,这里有一个PWS专用的Gateway, 我们可以称它为Private CSP Gateway。为了写的更清楚,总结了下面几点:

    CSP Gateway

    • 安装IRIS实例时用户可以选择是否安装CSP Gateway. 如果这时没选择安装,后面可以用单独的安装包安装。
    • 安装的程序可以放在任何位置。比如在Linux默认放在"/opt/webgateway"目录,配置文件在Web Gateway的配置文件目录。
    • 访问CSP Gateway的管理页面是 http://WebServer:80/csp/bin/Systems/Module.cxw 。 (这里的WebServer是Web服务器的地址,➕它的端口是默认的80)

    PWS

    • 安装时自动安装
    • 程序和配置都在IRIS的安装目录,比如"C:/InterSystems/HealthConnect/CSP/bin/"
    • 访问管理页面的地址是 http://IRIS:57772/csp/bin/Systems/Module.cxw,这里的IRIS是IRIS服务器的地址,如果是本机登录,也就是localhost.

    注意一点:从PWS访问IRIS管理页面, 比如 http://localhost/csp/sys/UtilHome.csp, 选择其中的 “系统管理 > 配置 > Web网关管理"进入的是PWS的配置。如果是从 http://WebServer/csp/sys/UtilHome.csp进入的IRIS管理页面, 那么同样的操作进入的是CSP Gateway的管理页面。 这很容易从页面显示的Web Server的类型和版本发现区别。

    其他关于部署CSP Gateway的疑问

    • 一个Web服务器可以连接多个CSP Gateway吗? 如果你真正理解了CSP Gateway, 你就明白它是Web服务器工作的一部分,比如在IIS里面它就是配置的一个虚拟路径。技术上你可以多配一个,但完全没有必要。 如果要把HTTP从一个Web服务器发到多个IRIS, 可以在一个Web Gateway里配置多个"Server Access"连接。

    • 一个CSP Gateway是怎么连接多个IRIS Server的?

    CSP Gateway可以配置多个"Server Access”, 只是要区分出收到的请求应该发给那个IRIS Server.如果分发给不同的IRIS的URL是不同的,比如CSP Gateway可以路由"/csp/demo1"到第一个IRIS, "/csp/demo2"到第2个IRIS。

    • Web Server要和IRIS部署在一台服务器吗?

    生产环境中, 部署单独的Web Server通常是好选择。为了安全起见,很多用户会部署Web Server的高可用。

    如果Web Server和Caché/IRIS分别装在两台服务器,IRIS安装时选择“不要安装CSP网关”,在Web Server的服务器上安装单独的Web Gateway软件包,测试和Caché/IRIS的连接。

    如果是Web Server和Caché/IRIS装在同一台服务器, 那么应该先安装Web Server, 然后使用Caché/IRIS安装包安装Caché, 选择 “安装CSP网关”, 这样CSP网关会被安装在Web Server的目录下, 相关的模块和Web Server配置也会自动完成。 如果顺序反过来, 那么需要手工配置Web Server, 增加的不必要的复杂步骤。

    • 安装外部Web Server能使用私有Web Gateway吗?

    对Web服务器有了解的用户更会有这样的疑问。既然Web Gateway只是给Web Gateway工作的程序组件,那么是否从外部服务器就可以直接使用私有的Web Gateway了,何必再多安装一个。 是的,技术上这样是可行的。前提是,1. 外部Web服务器和IRIS在一台硬件服务器上。2. 客户要对外部服务器的配置非常熟悉,可以手工配置外部Web服务器对私有Web Gateway的访问, 包括路径或者虚假路径,文件夹的访问权限,用户或者用户组的权限等等。总的说, 这样既麻烦,又不便于后期的管理,因此我推荐还是重新装一个Web Gateway。只是要分清它和私有的连接PWS的Web Gateway的区别,而永远不要让他们混在一起。

    安装CSP Gateway的具体步骤请参考下面的文章:

    WebGateway系列(2): 配置Apache连接IRIS WebGateway系列(3): 配置IIS连接IRIS

    0
    3 923
    文章 Hao Ma · 一月 15, 2021 5m read

    在这个由三个部分组成的系列文章中,介绍了如何在OAuth 2.0标准下使用IAM简单地为IRIS中的未经验证的服务添加安全性。 

    第一部分介绍了一些OAuth 2.0背景知识,以及IRIS和IAM的一些初始定义和配置,以帮助读者理解确保服务安全的整个过程。 

    第二部分详细讨论和演示了配置IAM所需的步骤——验证传入请求中的访问令牌,并在验证成功时将请求转发到后端。 

    本系列的最后一部分将讨论和演示IAM生成访问令牌(充当授权服务器)并对其进行验证时所需的配置,以及一些重要的最终考虑事项。 

    如果您想试用IAM,请联系InterSystems销售代表。 

    场景2:IAM作为授权服务器和访问令牌验证器  

    与上个场景不同的是,该场景中将使用一个名为“OAuth 2.0 Authentication”的插件。 

    如果要在资源所有者密码凭证流中将IAM作为授权服务器使用,客户端应用程序必须对用户名和密码进行身份验证。只有在身份验证成功时,才能发出获取IAM访问令牌的请求。 

    首先,将其添加到“SampleIRISService”中。正如下面截屏所示,需要填充一些不同的字段来配置此插件。 

    首先,将“SampleIRISService”的ID粘贴到“service_id”字段中,这样就可以在服务中启用该插件。 

    0
    0 409
    文章 Hao Ma · 一月 15, 2021 3m read

    在这个由三部分组成的系列文章中,我们将展示如何在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。 

    0
    0 326
    文章 Hao Ma · 一月 15, 2021 3m read

    介绍 

    目前,诸多应用程序通过开放授权框架(OAuth)来安全、可靠、高效地访问各种服务中的资源。InterSystems IRIS目前已兼容OAuth 2.0框架。事实上社区有一篇关于OAuth 2.0和InterSystems IRIS的精彩文章,链接如下。 

    然而,随着API管理工具的出现,一些组织开始将其用作单点身份验证,从而防止未经授权的请求到达下游服务,并将授权/身份验证复杂性从服务本身分离出来。 

    您可能知道,InterSystems已经推出了自己的API管理工具,即InterSystems API Management (IAM),以IRIS Enterprise license(IRIS Community版本不含此功能)的形式提供。这里是社区另一篇介绍InterSystems AIM的精华帖。 

    这是三篇系列文章中的第一篇,该系列文章将展示如何在OAuth 2.0标准下使用IAM简单地为IRIS中的未经验证的服务添加安全性。 

    第一部分将介绍OAuth 2.0相关背景,以及IRIS和IAM的初始定义和配置,以帮助读者理解确保服务安全的整个过程。 

    本系列文章的后续部分还将介绍两种使用IAM保护服务的可能的场景。在第一种场景中,IAM只验证传入请求中的访问令牌,如果验证成功,则将请求转发到后端。在第二种场景中,IAM将生成一个访问令牌(充当授权服务器)并对其进行验证。 

    0
    0 539
    文章 Hao Ma · 一月 15, 2021 5m read

    InterSystems编程语言的错误管理技术一直在发展。接下来,我们将展示几种不同的错误管理实现方式,以及为什么要使用TRY/THROW/CATCH机制。  

    您可以点击这里阅读官方的错误处理建议。  

    为了支持遗留应用程序,InterSystems不会废弃非推荐的错误管理方法。我们建议使用objectscriptQuality等工具来检测遗留的非推荐用例以及其他可能的问题和错误。  

    $ZERROR

    $ZERROR是一种较老的错误管理机制,支持与标准“M”不同的实现。虽然$ZERROR现在仍然有效,但我们非常不推荐使用。  

    如果您已经使用了$ZERROR,那么很容易对该变量进行错误的设计使用。$ZERROR是一个全局公共变量,可以被当前进程中正在执行的所有routine(宏)(来自InterSystems或自定义的)进行访问和修改。因此,它的值仅在产生错误的时候是可靠的。InterSystems不保证$ZERROR在调用系统库时会保留旧值。  

    我们在这里对一些案例展开分析。  

    案例1:自定义代码中的错误代码  

    0
    0 212
    文章 Hao Ma · 一月 15, 2021 2m read

    什么是npm-iris? 

    NPM是“No Project Mess(项目不乱)”的缩写。 

    NPM是使用Intersystems IRIS和Bootstrp 4建成的项目和任务管理应用程序。 

    NPM的创建初衷是通过一个简单直观的项目和任务管理软件,帮助开发者和小型商业公司降低日常问题的复杂度。 

    它能提供不同的任务视图,包括电子表格、看板、日历,甚至甘特图! 

    为什么? 

    在不同的团队中工作,您会发现不同的人喜欢不同的工具。 

    所以,有时您会在一个项目中使用甘特图,在另一个项目中使用看板,在其他项目中使用纸上的列表…… 

    NPM专注于任务。无论您和您的团队喜欢以哪种方式查看。只需单击并更改您的视图。 

    功能 

    • 初始安装 
    • 项目 
    • 用户 
    • 任务 - 创建和管理任务 
    • 调度程序 - 任务的日历视图 
    • 看板 - 用看板风格管理您的任务 
    • 甘特图 - 使用甘特图查看截止日期、里程碑和进度 

    新特性/改进的路线图 

    • OAuth2身份验证 
    • 项目/团队/用户的安全性 
    • 时间跟踪 
    • 自定义日历(假期) 
    • 支持附件 
    • 利用AppS.REST框架 
    • Vue.js版本 
    • Home面板,可以查看活动的概况 

     

    试一下这款应用程序! 
    http://npm-iris.eastus.cloudapp.azure.com:52773/npm/home.csp  

    0
    0 148
    文章 Hao Ma · 一月 15, 2021 6m read

    假设您想编写一些真正的web应用程序,例如medium.com网站的简单克隆。这类应用程序可以在后端使用任何不同的语言编写,也可以使用前端的任何框架编写。编写这样一个应用程序有很多方法,你也可以看看这个项目。它为完全相同的应用程序提供了一堆前端和后端实现。您可以轻松组合它们,任何所选前端应该与任何后端搭配。

    我来介绍一下这个使用后端InterSystems IRIS来实现后端的相同的应用程序。  

    0
    0 259
    文章 Hao Ma · 一月 10, 2021 15m read

    自 Caché 2017 以后,SQL 引擎包含了一些新的统计信息。 这些统计信息记录了执行查询的次数以及运行查询所花费的时间。

    对于想要对包含许多 SQL 语句的应用程序的性能进行监控和尝试优化的人来说,这是一座宝库,但访问数据并不像一些人希望的那么容易。

    本文和相关的示例代码说明了如何使用这些信息,以及如何例行提取每日统计信息的摘要,并保存应用程序的 SQL 性能的历史记录。

    记录了什么?

    每次执行 SQL 语句时,都记录花费的时间。 这是非常轻量的操作,无法关闭。 为了最大程度地降低开销,统计信息保留在内存中并定期写入磁盘。 数据包括一天中执行查询的次数以及所花费的平均时间和总时间。

    数据不会立即写入磁盘,并且在写入之后,统计信息将由“更新 SQL 查询统计信息”任务更新,该任务通常计划为每小时运行一次。 该任务可以手动触发,但是如果你希望在测试查询时实时查看统计信息,则整个过程需要一点耐心。

    警告:在 InterSystems IRIS 2019 及更早版本中,不会针对已使用 %Studio.Project:Deploy 机制部署类或例程中的嵌入式 SQL 收集这些统计信息。 示例代码不会有任何中断,但这可能会使你产生误导(我被误导过),让你以为一切正常,因为没有查询显示为高开销。

    如何查看信息?

    你可以在管理门户中查看查询列表。 转到 SQL 页面,点击“SQL 语句”选项卡。 对于你正在运行并查看的新查询,这种方式很好;但是如果有数千条查询正在运行,则可能变得难以管理。

    另一种方法是使用 SQL 搜索查询。 信息存储在 INFORMATION_SCHEMA 模式的表中。 该模式含有大量表,我在本文的最后附上了一些 SQL 查询示例。

    何时删除统计信息?

    每次重新编辑查询时会删除其数据。 因此对于动态查询,这可能意味着清除缓存的查询时。 对于嵌入式 SQL,则意味着重新编译在其中嵌入 SQL 的类或例程时。

    在活跃的站点上,可以合理预期统计信息将保存超过一天,但是存放统计信息的表不能用作运行报告或长期分析的长期参考源。

    如何汇总信息?

    我建议每天晚上将数据提取到永久表中,这些表在生成性能报告时更易于使用。 如果在白天编译类,可能会丢失一些信息,但这不太可能对慢速查询的分析产生任何实际影响。

    下面的代码示例说明了如何将每个查询的统计信息提取到每日汇总中。 它包括三个简短的类:

    • 一个应在每晚运行的任务。
    • DRL.MonitorSQL 是主类,用于从 INFORMATION_SCHEMA 表提取数据并存储。
  • 第三个类 DRL.MonitorSQLText 是一个优化类,它存储一次(可能很长的)查询文本,并且只将查询的哈希存储在每天的统计信息中。
  • 示例说明

    该任务提取前一天的信息,因此应安排在午夜后不久执行。

    你可以导出更多历史数据,只要其存在。 要提取过去 120 天的数据

    Do ##class(DRL.MonitorSQL).Capture($h-120,$h-1)

    该示例代码直接读取全局 ^rIndex,因为最早版本的统计信息未将日期公开给 SQL。

    我所包括的变体将循环实例中的所有命名空间,但这并不总是合适的。

    如何查询已提取的数据

    提取数据后,您可以通过运行以下语句查找最繁重的查询

    SELECT top 20S.RunDate,S.RoutineName,S.TotalHits,S.SumpTIme,S.Hash,t.QueryTextfrom DRL.MonitorSQL Sleft join DRL.MonitorSQLText T on S.Hash=T.Hashwhere RunDate='08/25/2019'order by SumpTime desc

     

    此外,如果选择了开销大的查询的哈希,可以通过以下语句查看该查询的历史记录

    SELECT S.RunDate,S.RoutineName,S.TotalHits,S.SumpTIme,S.Hash,t.QueryTextfrom DRL.MonitorSQL Sleft join DRL.MonitorSQLText T on S.Hash=T.Hashwhere S.Hash='CgOlfRw7pGL4tYbiijYznQ84kmQ='order by RunDate

     

    今年早些时候,我获取了一个活跃站点的数据,然后查看了开销最大的查询。 有一个查询的平均时间不到 6 秒,但每天被调用 14000 次,加起来每天消耗的时间将近 24 小时。 实际上,一个核心完全被这个查询占用。 更糟糕的是,第二个查询要花一个小时,它是第一个查询的变体。

    <td>
      例程名称
    </td>
    
    <td>
      总命中次数
    </td>
    
    <td>
      总时间
    </td>
    
    <td>
      哈希
    </td>
    
    <td>
      查询文本(有节略)
    </td>
    
    <td>
       
    </td>
    
    <td>
      14,576
    </td>
    
    <td>
      85,094
    </td>
    
    <td>
      5xDSguu4PvK04se2pPiOexeh6aE=
    </td>
    
    <td>
      DECLARE C CURSOR FOR SELECT * INTO :%col(1) , :%col(2) , :%col(3) , :%col(4)  …
    </td>
    
    <td>
       
    </td>
    
    <td>
      15,552
    </td>
    
    <td>
      3,326
    </td>
    
    <td>
      rCQX+CKPwFR9zOplmtMhxVnQxyw=
    </td>
    
    <td>
      DECLARE C CURSOR FOR SELECT * INTO :%col(1) , :%col(2) , :%col(3) , :%col(4) , …
    </td>
    
    <td>
       
    </td>
    
    <td>
      16,892
    </td>
    
    <td>
      597
    </td>
    
    <td>
      yW3catzQzC0KE9euvIJ+o4mDwKc=
    </td>
    
    <td>
      DECLARE C CURSOR FOR SELECT * INTO :%col(1) , :%col(2) , :%col(3) , :%col(4) , :%col(5) , :%col(6) , :%col(7) ,
    </td>
    
    <td>
       
    </td>
    
    <td>
      16,664
    </td>
    
    <td>
      436
    </td>
    
    <td>
      giShyiqNR3K6pZEt7RWAcen55rs=
    </td>
    
    <td>
      DECLARE C CURSOR FOR SELECT * , TKGROUP INTO :%col(1) , :%col(2) , :%col(3) , ..
    </td>
    
    <td>
       
    </td>
    
    <td>
      74,550
    </td>
    
    <td>
      342
    </td>
    
    <td>
      4ZClMPqMfyje4m9Wed0NJzxz9qw=
    </td>
    
    <td>
      DECLARE C CURSOR FOR SELECT …
    </td>
    
    运行日期
    03/16/2019
    03/16/2019
    03/16/2019
    03/16/2019
    03/16/2019

    1:客户站点的实际结果

    INFORMATION_SCHEMA 模式中的表

    除了统计信息外,此模式中的表还会跟踪查询、列、索引等的使用位置。 通常,SQL 语句是起始表,它的连接方式类似于“Statements.Hash=OtherTable.Statement”。

    直接访问这些表以查找一天中开销最大的查询,这一操作的等效查询是...

    SELECT DS.Day,Loc.Location,DS.StatCount,DS.StatTotal,S.Statement,S.HashFROM INFORMATION_SCHEMA.STATEMENT_DAILY_STATS DSleft join INFORMATION_SCHEMA.STATEMENTS  Son S.Hash=DS.Statementleft join INFORMATION_SCHEMA.STATEMENT_LOCATIONS  Locon S.Hash=Loc.Statementwhere Day='08/26/2019'order by DS.stattotal desc

     

    无论你是否考虑建立一个更系统的过程,我都建议每个使用 SQL 处理大型应用程序的人今天都运行这个查询。

    如果某个特定查询显示为高开销,则可以通过运行以下语句获取历史记录

    SELECT DS.Day,Loc.Location,DS.StatCount,DS.StatTotal,S.Statement,S.HashFROM INFORMATION_SCHEMA.STATEMENT_DAILY_STATS DSleft join INFORMATION_SCHEMA.STATEMENTS  Son S.Hash=DS.Statementleft join INFORMATION_SCHEMA.STATEMENT_LOCATIONS  Locon S.Hash=Loc.Statementwhere S.Hash='jDqCKaksff/4up7Ob0UXlkT2xKY='order by DS.Day

     

     
    每日提取统计信息的代码示例
    标准免责声明 - 此示例仅用于说明。 不对其提供支持,也不保证其有效。
    <p>
    </p>
    
    <p>
      <span style="font-size:11pt"><span style="line-height:107%"><span style=""><span style="color:navy">Class DRL.MonitorSQLTask Extends %SYS.Task.Definition</span><br /><span style="color:black">{</span><br /><span style="color:navy">Parameter </span><span style="color:black">TaskName = </span><span style="color:purple">"SQL Statistics Summary"</span><span style="color:black">;</span><br /><span style="color:navy">Method </span><span style="color:black">OnTask() </span><span style="color:navy">As %Status</span><br /><span style="color:black">{</span><br />              <span style="color:blue">set </span><span style="color:olive">tSC</span><span style="color:black">=</span><span style="color:blue">$$$OK</span><br />              <span style="color:blue">TRY </span><span style="color:purple">{</span><br />                             <span style="color:blue">do </span><span style="color:navy">##class</span><span style="color:black">(</span><span style="color:teal">DRL.MonitorSQL</span><span style="color:black">).</span><span style="color:blue">Run</span><span style="color:black">()</span><br />              <span style="color:purple">}</span><br />              <span style="color:blue">CATCH </span><span style="color:olive">exp </span><span style="color:purple">{</span><br />                            <span style="color:blue">set </span><span style="color:olive">tSC</span><span style="color:black">=</span><span style="color:blue">$SYSTEM</span><span style="color:teal">.Status</span><span style="color:black">.</span><span style="color:blue">Error</span><span style="color:black">(</span><span style="color:green">"Error in SQL Monitor Summary Task"</span><span style="color:black">)</span><br />              <span style="color:purple">}</span><br />              <span style="color:blue">quit </span><span style="color:olive">tSC</span><br /><span style="color:black"> }</span><br /><span style="color:black">}</span></span></span></span>
    </p>
    
    <p>
       
    </p>
    
    <p>
      <span style="font-size:11pt"><span style="line-height:107%"><span style=""><span style="color:navy">Class DRL.MonitorSQLText Extends %Persistent</span><br /><span style="color:black">{</span><br /><span style="color:navy">/// Hash of query text</span><br /><span style="color:navy">Property </span><span style="color:black">Hash </span><span style="color:navy">As %String</span><span style="color:black">;</span></span></span></span><br /> 
    </p>
    
    <p>
      <span style="font-size:11pt"><span style="line-height:107%"><span style=""><span style="color:navy">/// query text for hash</span><br /><span style="color:navy">Property </span><span style="color:black">QueryText </span><span style="color:navy">As %String</span><span style="color:black">(</span><span style="color:navy">MAXLEN </span><span style="color:black">= </span><span style="color:navy">9999</span><span style="color:black">);</span><br /><span style="color:navy">Index </span><span style="color:black">IndHash On Hash [ </span><span style="color:navy">IdKey</span><span style="color:black">, </span><span style="color:navy">Unique </span><span style="color:black">];</span><br /><span style="color:black">}</span></span></span></span>
    </p>
    
    <p>
      <span style="font-size:11pt"><span style="line-height:107%"><span style=""><span style="color:navy">/// Summary of very low cost SQL query statistics collected in Cache 2017.1 and later. <br /></span><br /><span style="color:navy">/// Refer to documentation on "SQL Statement Details" for information on the source data. <br /></span><br /><span style="color:navy">/// Data is stored by date and time to support queries over time. <br /></span><br /><span style="color:navy">/// Typically run to summarise the SQL query data from the previous day.</span><br /><span style="color:navy">Class DRL.MonitorSQL Extends %Persistent</span><br /><span style="color:black">{</span><br /><span style="color:navy">/// RunDate and RunTime uniquely identify a run</span><br /><span style="color:navy">Property </span><span style="color:black">RunDate </span><span style="color:navy">As %Date</span><span style="color:black">;</span><br /><span style="color:navy">/// Time the capture was started</span><br /><span style="color:navy">/// RunDate and RunTime uniquely identify a run</span><br /><span style="color:navy">Property </span><span style="color:black">RunTime </span><span style="color:navy">As %Time</span><span style="color:black">;</span><br /><br /><span style="color:navy">/// Count of total hits for the time period for </span><br /><span style="color:navy">Property </span><span style="color:black">TotalHits </span><span style="color:navy">As %Integer</span><span style="color:black">;</span><br /><br /><span style="color:navy">/// Sum of pTime</span><br /><span style="color:navy">Property </span><span style="color:black">SumPTime </span><span style="color:navy">As %Numeric</span><span style="color:black">(</span><span style="color:navy">SCALE </span><span style="color:black">= </span><span style="color:navy">4</span><span style="color:black">);</span><br /><br /><span style="color:navy">/// Routine where SQL is found</span><br /><span style="color:navy">Property </span><span style="color:black">RoutineName </span><span style="color:navy">As %String</span><span style="color:black">(</span><span style="color:navy">MAXLEN </span><span style="color:black">= </span><span style="color:navy">1024</span><span style="color:black">);</span><br /><br /><span style="color:navy">/// Hash of query text</span><br /><span style="color:navy">Property </span><span style="color:black">Hash </span><span style="color:navy">As %String</span><span style="color:black">;</span><br /><br /><span style="color:navy">Property </span><span style="color:black">Variance </span><span style="color:navy">As %Numeric</span><span style="color:black">(</span><span style="color:navy">SCALE </span><span style="color:black">= </span><span style="color:navy">4</span><span style="color:black">);</span><br /><br /><span style="color:navy">/// Namespace where queries are run</span><br /><span style="color:navy">Property </span><span style="color:black">Namespace </span><span style="color:navy">As %String</span><span style="color:black">;</span><br /><br /><span style="color:navy">/// Default run will process the previous days data for a single day.</span><br /><span style="color:navy">/// Other date range combinations can be achieved using the Capture method.</span><br /><span style="color:navy">ClassMethod </span><span style="color:black">Run()</span><br /><span style="color:black">{</span><br />              <span style="color:green">//Each run is identified by the start date / time to keep related items together</span><br />             <br />              <span style="color:blue">set </span><span style="color:olive">h</span><span style="color:black">=</span><span style="color:blue">$h</span><span style="color:black">-1</span><br />              <span style="color:blue">do </span><span style="color:black">..</span><span style="color:blue">Capture</span><span style="color:black">(+</span><span style="color:olive">h</span><span style="color:black">,+</span><span style="color:olive">h</span><span style="color:black">)</span><br /><span style="color:black">}</span><br /><br /><span style="color:navy">/// Captures historic statistics for a range of dates</span><br /><span style="color:navy">ClassMethod </span><span style="color:black">Capture(</span><span style="color:fuchsia">dfrom</span><span style="color:black">, </span><span style="color:fuchsia">dto</span><span style="color:black">)</span><br /><span style="color:black">{</span><br />              <span style="color:blue">set </span><span style="color:olive">oldstatsvalue</span><span style="color:black">=</span><span style="color:blue">$system</span><span style="color:teal">.SQL</span><span style="color:black">.</span><span style="color:blue">SetSQLStatsJob</span><span style="color:black">(-1)</span><br />             <br />                <span style="color:blue">set </span><span style="color:olive">currNS</span><span style="color:black">=</span><span style="color:blue">$znspace</span><br />                <span style="color:blue">set </span><span style="color:olive">tSC</span><span style="color:black">=</span><span style="color:navy">##class</span><span style="color:black">(</span><span style="color:teal">%SYS.Namespace</span><span style="color:black">).</span><span style="color:blue">ListAll</span><span style="color:black">(.</span><span style="color:olive">nsArray</span><span style="color:black">)</span><br />                <span style="color:blue">set </span><span style="color:olive">ns</span><span style="color:black">=</span><span style="color:green">""</span><br />      <span style="color:blue">set </span><span style="color:olive">time</span><span style="color:black">=</span><span style="color:blue">$piece</span><span style="color:black">(</span><span style="color:blue">$h</span><span style="color:black">,</span><span style="color:green">","</span><span style="color:black">,2)</span><br />      <span style="color:blue">kill </span><span style="color:black">^||TMP.MonitorSQL</span><br />                <span style="color:blue">do </span><span style="color:purple">{</span><br />                               <span style="color:blue">set </span><span style="color:olive">ns</span><span style="color:black">=</span><span style="color:blue">$o</span><span style="color:black">(</span><span style="color:olive">nsArray</span><span style="color:black">(</span><span style="color:olive">ns</span><span style="color:black">))</span><br />                               <span style="color:blue">quit</span><span style="color:black">:</span><span style="color:olive">ns</span><span style="color:black">=</span><span style="color:green">""</span><br />                               <span style="color:blue">use </span><span style="color:black">0 </span><span style="color:blue">write </span><span style="color:black">!,</span><span style="color:green">"processing namespace "</span><span style="color:black">,</span><span style="color:olive">ns</span><br />                               <span style="color:blue">zn </span><span style="color:olive">ns</span><br />                                           <span style="color:blue">for </span><span style="color:maroon">dateh</span><span style="color:black">=</span><span style="color:fuchsia">dfrom</span><span style="color:black">:1:</span><span style="color:fuchsia">dto </span><span style="color:purple">{</span><br />                                                          <span style="color:blue">set </span><span style="color:maroon">hash</span><span style="color:black">=</span><span style="color:green">""</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">purgedun</span><span style="color:black">=0</span><br />                                                          <span style="color:blue">do </span><span style="color:purple">{</span><br />                                                                        <span style="color:blue">set </span><span style="color:maroon">hash</span><span style="color:black">=</span><span style="color:blue">$order</span><span style="color:black">(^rINDEXSQL(</span><span style="color:green">"sqlidx"</span><span style="color:black">,1,</span><span style="color:maroon">hash</span><span style="color:black">))</span><br />                                                                        <span style="color:blue">continue</span><span style="color:black">:</span><span style="color:maroon">hash</span><span style="color:black">=</span><span style="color:green">""</span><br />                                                                        <span style="color:blue">set </span><span style="color:olive">stats</span><span style="color:black">=</span><span style="color:blue">$get</span><span style="color:black">(^rINDEXSQL(</span><span style="color:green">"sqlidx"</span><span style="color:black">,1,</span><span style="color:maroon">hash</span><span style="color:black">,</span><span style="color:green">"stat"</span><span style="color:black">,</span><span style="color:maroon">dateh</span><span style="color:black">))</span><br />                                                                        <span style="color:blue">continue</span><span style="color:black">:</span><span style="color:olive">stats</span><span style="color:black">=</span><span style="color:green">""</span><br />                                                                        <span style="color:blue">set </span><span style="color:black">^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">,</span><span style="color:maroon">hash</span><span style="color:black">)=</span><span style="color:olive">stats</span><br />                                                                       <br />                                                          <span style="color:purple">&SQL(</span><span style="color:blue">SELECT  </span><span style="color:green">Location </span><span style="color:navy">into </span><span style="color:maroon">:tLocation </span><span style="color:navy">FROM </span><span style="color:green">INFORMATION_SCHEMA</span><span style="color:black">.</span><span style="color:green">STATEMENT_LOCATIONS </span><span style="color:navy">WHERE </span><span style="color:green">Statement</span><span style="color:black">=</span><span style="color:maroon">:hash</span><span style="color:purple">)</span><br />                                                                        <span style="color:blue">if </span><span style="color:maroon">SQLCODE</span><span style="color:black">'=0 </span><span style="color:blue">set </span><span style="color:olive">Location</span><span style="color:black">=</span><span style="color:green">""</span><br />                                                                        <span style="color:blue">set </span><span style="color:black">^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">,</span><span style="color:maroon">hash</span><span style="color:black">,</span><span style="color:green">"Location"</span><span style="color:black">)=</span><span style="color:maroon">tLocation</span><br />                                                                       <br />                                                          <span style="color:purple">&SQL(</span><span style="color:blue">SELECT  </span><span style="color:green">Statement </span><span style="color:navy">INTO </span><span style="color:maroon">:Statement </span><span style="color:navy">FROM </span><span style="color:green">INFORMATION_SCHEMA</span><span style="color:black">.</span><span style="color:green">STATEMENTS </span><span style="color:navy">WHERE </span><span style="color:green">Hash</span><span style="color:black">=</span><span style="color:maroon">:hash</span><span style="color:purple">)</span><br />                                                                        <span style="color:blue">if </span><span style="color:maroon">SQLCODE</span><span style="color:black">'=0 </span><span style="color:blue">set </span><span style="color:maroon">Statement</span><span style="color:black">=</span><span style="color:green">""</span><br />                                                                        <span style="color:blue">set </span><span style="color:black">^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">,</span><span style="color:maroon">hash</span><span style="color:black">,</span><span style="color:green">"QueryText"</span><span style="color:black">)=</span><span style="color:maroon">Statement</span><br />                                                          <span style="color:purple">} </span><span style="color:blue">while </span><span style="color:maroon">hash</span><span style="color:black">'=</span><span style="color:green">""</span><br />                                                         <br />                                           <span style="color:purple">}</span><br />                <span style="color:purple">} </span><span style="color:blue">while </span><span style="color:olive">ns</span><span style="color:black">'=</span><span style="color:green">""</span><br />                <span style="color:blue">zn </span><span style="color:olive">currNS</span><br />                <span style="color:blue">set </span><span style="color:maroon">dateh</span><span style="color:black">=</span><span style="color:green">""</span><br />                <span style="color:blue">do </span><span style="color:purple">{</span><br />                               <span style="color:blue">set </span><span style="color:maroon">dateh</span><span style="color:black">=</span><span style="color:blue">$o</span><span style="color:black">(^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">))</span><br />                               <span style="color:blue">quit</span><span style="color:black">:</span><span style="color:maroon">dateh</span><span style="color:black">=</span><span style="color:green">""</span><br />                               <span style="color:blue">set </span><span style="color:olive">ns</span><span style="color:black">=</span><span style="color:green">""</span><br />                               <span style="color:blue">do </span><span style="color:purple">{</span><br />                                             <span style="color:blue">set </span><span style="color:olive">ns</span><span style="color:black">=</span><span style="color:blue">$o</span><span style="color:black">(^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">))</span><br />                                             <span style="color:blue">quit</span><span style="color:black">:</span><span style="color:olive">ns</span><span style="color:black">=</span><span style="color:green">""</span><br />                                             <span style="color:blue">set </span><span style="color:maroon">hash</span><span style="color:black">=</span><span style="color:green">""</span><br />                                             <span style="color:blue">do </span><span style="color:purple">{</span><br />                                                         <span style="color:blue">set </span><span style="color:maroon">hash</span><span style="color:black">=</span><span style="color:blue">$o</span><span style="color:black">(^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">,</span><span style="color:maroon">hash</span><span style="color:black">))</span><br />                                                          <span style="color:blue">quit</span><span style="color:black">:</span><span style="color:maroon">hash</span><span style="color:black">=</span><span style="color:green">""</span><br />                                                         <span style="color:blue">set </span><span style="color:olive">stats</span><span style="color:black">=</span><span style="color:blue">$g</span><span style="color:black">(^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">,</span><span style="color:maroon">hash</span><span style="color:black">))</span><br />                                                          <span style="color:blue">continue</span><span style="color:black">:</span><span style="color:olive">stats</span><span style="color:black">=</span><span style="color:green">""</span><br />                                                          <span style="color:green">// The first time through the loop delete all statistics for the day so it is re-runnable</span><br />                                                          <span style="color:green">// But if we run for a day after the raw data has been purged, it will wreck eveything</span><br />                                                          <span style="color:green">// so do it here, where we already know there are results to insert in their place.</span><br />                                                          <span style="color:blue">if </span><span style="color:olive">purgedun</span><span style="color:black">=0 </span><span style="color:purple">{</span><br />                                                                        <span style="color:purple">&SQL(</span><span style="color:blue">DELETE </span><span style="color:navy">FROM </span><span style="color:green">websys</span><span style="color:black">.</span><span style="color:green">MonitorSQL </span><span style="color:navy">WHERE </span><span style="color:green">RunDate</span><span style="color:black">=</span><span style="color:maroon">:dateh </span><span style="color:purple">)</span><br />                                                                        <span style="color:blue">set </span><span style="color:olive">purgedun</span><span style="color:black">=1</span><br />                                                          <span style="color:purple">}</span><br />                                                         <br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">=</span><span style="color:navy">##class</span><span style="color:black">(</span><span style="color:teal">DRL.MonitorSQL</span><span style="color:black">).</span><span style="color:blue">%New</span><span style="color:black">()</span><br /><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">Namespace</span><span style="color:black">=</span><span style="color:olive">ns</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">RunDate</span><span style="color:black">=</span><span style="color:maroon">dateh</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">RunTime</span><span style="color:black">=</span><span style="color:olive">time</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">Hash</span><span style="color:black">=</span><span style="color:maroon">hash</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">TotalHits</span><span style="color:black">=</span><span style="color:blue">$listget</span><span style="color:black">(</span><span style="color:olive">stats</span><span style="color:black">,1)</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">SumPTime</span><span style="color:black">=</span><span style="color:blue">$listget</span><span style="color:black">(</span><span style="color:olive">stats</span><span style="color:black">,2)</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">Variance</span><span style="color:black">=</span><span style="color:blue">$listget</span><span style="color:black">(</span><span style="color:olive">stats</span><span style="color:black">,3)</span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">Variance</span><span style="color:black">=</span><span style="color:blue">$listget</span><span style="color:black">(</span><span style="color:olive">stats</span><span style="color:black">,3)</span><br />                                                         <br />                                                         <span style="color:blue">set </span><span style="color:olive">queryText</span><span style="color:black">=^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">,</span><span style="color:maroon">hash</span><span style="color:black">,</span><span style="color:green">"QueryText"</span><span style="color:black">)</span><br />                                                         <span style="color:blue">set </span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">RoutineName</span><span style="color:black">=^||TMP.MonitorSQL(</span><span style="color:maroon">dateh</span><span style="color:black">,</span><span style="color:olive">ns</span><span style="color:black">,</span><span style="color:maroon">hash</span><span style="color:black">,</span><span style="color:green">"Location"</span><span style="color:black">)</span><br />                                                         <br />                                                    <span style="color:purple">&SQL(</span><span style="color:blue">Select </span><span style="color:green">ID </span><span style="color:navy">into </span><span style="color:maroon">:TextID </span><span style="color:navy">from </span><span style="color:green">DRL</span><span style="color:black">.</span><span style="color:green">MonitorSQLText </span><span style="color:navy">where </span><span style="color:green">Hash</span><span style="color:black">=</span><span style="color:maroon">:hash</span><span style="color:purple">)</span><br />                                                          <span style="color:blue">if </span><span style="color:maroon">SQLCODE</span><span style="color:black">'=0 </span><span style="color:purple">{</span><br />                                                                        <span style="color:blue">set </span><span style="color:olive">textref</span><span style="color:black">=</span><span style="color:navy">##class</span><span style="color:black">(</span><span style="color:teal">DRL.MonitorSQLText</span><span style="color:black">).</span><span style="color:blue">%New</span><span style="color:black">()</span><br />                                                                        <span style="color:blue">set </span><span style="color:olive">textref</span><span style="color:black">.</span><span style="color:blue">Hash</span><span style="color:black">=</span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">Hash</span><br />                                                                        <span style="color:blue">set </span><span style="color:olive">textref</span><span style="color:black">.</span><span style="color:blue">QueryText</span><span style="color:black">=</span><span style="color:olive">queryText</span><br />                                                                        <span style="color:blue">set </span><span style="color:olive">sc</span><span style="color:black">=</span><span style="color:olive">textref</span><span style="color:black">.</span><span style="color:blue">%Save</span><span style="color:black">()</span><br />                                                          <span style="color:purple">}                                                        </span><br />                                                          <span style="color:blue">set </span><span style="color:olive">tSc</span><span style="color:black">=</span><span style="color:olive">tObj</span><span style="color:black">.</span><span style="color:blue">%Save</span><span style="color:black">()</span><br />                                                         <br />                                                          <span style="color:green">//avoid dupicating the query text in each record because it can be very long. Use a lookup</span><br />                                                          <span style="color:green">//table keyed on the hash. If it doesn't exist add it.</span><br />                                                          <span style="color:blue">if $$$ISERR</span><span style="color:black">(</span><span style="color:olive">tSc</span><span style="color:black">) </span><span style="color:blue">do $system</span><span style="color:teal">.OBJ</span><span style="color:black">.</span><span style="color:blue">DisplayError</span><span style="color:black">(</span><span style="color:olive">tSc</span><span style="color:black">)</span><br />                                                         <br /><br />                                                          <span style="color:blue">if $$$ISERR</span><span style="color:black">(</span><span style="color:olive">tSc</span><span style="color:black">) </span><span style="color:blue">do $system</span><span style="color:teal">.OBJ</span><span style="color:black">.</span><span style="color:blue">DisplayError</span><span style="color:black">(</span><span style="color:olive">tSc</span><span style="color:black">)</span><br />                                             <span style="color:purple">} </span><span style="color:blue">while </span><span style="color:maroon">hash</span><span style="color:black">'=</span><span style="color:green">""</span><br />                               <span style="color:purple">} </span><span style="color:blue">while </span><span style="color:olive">ns</span><span style="color:black">'=</span><span style="color:green">""</span><br />                                                         <br />                <span style="color:purple">} </span><span style="color:blue">while </span><span style="color:maroon">dateh</span><span style="color:black">'=</span><span style="color:green">""</span><br />                <br />                <br />             <br />              <span style="color:blue">do $system</span><span style="color:teal">.SQL</span><span style="color:black">.</span><span style="color:blue">SetSQLStatsJob</span><span style="color:black">(0)</span><br /><span style="color:black">}</span><br /><br /><span style="color:navy">Query </span><span style="color:black">Export(</span><span style="color:fuchsia">RunDateH1 </span><span style="color:navy">As %Date</span><span style="color:black">, </span><span style="color:fuchsia">RunDateH2 </span><span style="color:navy">As %Date</span><span style="color:black">) </span><span style="color:navy">As %SQLQuery</span><br /><span style="color:black">{</span><br /><span style="color:blue">SELECT </span><span style="color:green">S</span><span style="color:black">.</span><span style="color:green">Hash</span><span style="color:black">,</span><span style="color:green">RoutineName</span><span style="color:black">,</span><span style="color:green">RunDate</span><span style="color:black">,</span><span style="color:green">RunTime</span><span style="color:black">,</span><span style="color:green">SumPTime</span><span style="color:black">,</span><span style="color:green">TotalHits</span><span style="color:black">,</span><span style="color:green">Variance</span><span style="color:black">,</span><span style="color:green">RoutineName</span><span style="color:black">,</span><span style="color:green">T</span><span style="color:black">.</span><span style="color:green">QueryText</span><br />              <span style="color:navy">FROM DRL.</span><span style="color:green">MonitorSQL S </span><span style="color:navy">LEFT JOIN DRL.</span><span style="color:green">MonitorSQLText T </span><span style="color:navy">on </span><span style="color:green">S</span><span style="color:black">.</span><span style="color:green">Hash</span><span style="color:black">=</span><span style="color:green">T</span><span style="color:black">.</span><span style="color:green">Hash</span><br />              <span style="color:navy">WHERE </span><span style="color:green">RunDate</span><span style="color:black">>=</span><span style="color:maroon">:RunDateH1 </span><span style="color:black">AND </span><span style="color:green">RunDate</span><span style="color:black"><=</span><span style="color:maroon">:RunDateH2</span><br /><span style="color:black">}</span><br /><span style="color:black">}</span></span></span></span>
    </p>   
    
    <p>
       
    </p>
    
    0
    0 615
    文章 Hao Ma · 一月 10, 2021 11m read

    在本文中,我想谈一谈规范优先的 REST API 开发方式。

    传统的代码优先 REST API 开发是这样的:

    • 编写代码
    • 使其支持 REST
    • 形成文档(成为 REST API)

    规范优先遵循同样的步骤,不过是反过来的。 我们先制定规范(同时兼做文档),然后根据它生成一个样板 REST 应用,最后编写一些业务逻辑。

    这是有好处的,因为:

    • 对于想要使用你的 REST API 的外部或前端开发者,你总是有相关且有用的文档
    • 使用 OAS (Swagger) 创建的规范可以导入各种工具,从而进行编辑、客户端生成、API 管理、单元测试和自动化,或者许多其他任务的简化
    • 改进了 API 架构。 在代码优先的方式中,API 是逐个方法开发的,因此开发者很容易失去对整体 API 架构的跟踪,但在规范优先的方式中,开发者被强制从 API 使用者的角度与 API 进行交互,这通常有助于设计出更简洁的 API 架构
    • 更快的开发速度 - 由于所有样板代码都是自动生成的,你无需编写代码,只需开发业务逻辑。
    • 更快的反馈循环 - 使用者可以立即查看 API,并且只需修改规范即可轻松提供建议 让我们以规范优先的方式开发 API 吧!
    0
    0 357
    文章 Hao Ma · 一月 10, 2021 3m read

    本文讨论 Windows 写入缓存设置,该设置会使系统在断电或操作系统崩溃的情况下容易发生数据丢失或损坏。 该设置在某些 Windows 配置中默认开启。

    为磁盘启用 Windows 写入缓存意味着 Caché(或任何程序)写入该磁盘的某些内容不一定会立即提交到持久性存储(即使 Caché 在其写入阶段的特定关键点刷新从操作系统缓存到磁盘的写入也是如此)。 如果计算机断电,为该设备缓存的任何内容都会丢失,除非该设备的缓存是非易失性的或者由电池供电。 Caché 依靠操作系统来保证数据的持久性。 在这种情况下,保证是无效的。 对于 Caché 来说,这可能会导致数据库损坏或者数据库或日志文件中的数据缺失。

    InterSystems 的文档显示,使“写入映像日志”提供的保证失效的一种情况是回写缓存内容丢失(请参见 http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCDI_wij#GCDI/wij_limits)。 InterSystems 全球响应中心的数据完整性团队研究了许多 Windows 平台上的数据丢失或损坏案例,这些案例中有证据表明,回写缓存内容丢失是由该设置的值导致的。

    值得一提的是,磁盘的缓存可能会有效防止发生此类问题。 如果相关磁盘的缓存是非易失性的或由电池供电,则即使开启该设置,写入磁盘也应该是安全的。 如果相关存储比直接连接的磁盘更复杂,您需要了解在该存储基础架构的何处对写入进行缓存,以及这些缓存是否是易失性的或者是否由电池供电,以评估风险。

    您可以转到“设备管理器”,展开“磁盘驱动器”部分,然后查看给定磁盘的属性来查看设置。 我们感兴趣的设置在“策略”选项卡上。

    界面上的用词并不总是与您在这里看到的相同,可能因设备类型的不同而有所不同。 不过,这是常见的用词表述,并且 Windows 明确指出,开启该设置后,如果机器断电或崩溃,系统可能会发生数据丢失。

    接下来是同一机器上另一个磁盘的示例,其中的影响没有那么明显。在这里选择“更好的性能”将与在另一个示例中选择“启用写入缓存”带来相同问题。

    在这两个示例中,您看到的选定设置都是该设备的默认设置,我没有更改过。 您可以看到,在第一个示例中,默认设置使设备处于风险之中,而第二个示例则没有。 据我所知,并没有通用于所有设备类型或 Windows 版本的默认设置。 换句话说,需要在每台设备上检查此设置,以了解设备是否存在此风险。

    作为系统管理员,处理这种情况有三种基本方法。 禁用该设置是最简单的方法,可确保不会面临此风险。 但是,禁用该设置可能会对性能产生不可接受的影响。 如果是这种情况,您可能更愿意开启该设置,并将计算机连接到不间断电源。 这样做可以防止断电导致的数据丢失或损坏,因为 UPS 应该可以在断电时提供足够的时间让您从容地关机。 最后一个选择是简单地接受服务器断电或崩溃时数据丢失的风险。 InterSystems 建议不要采用此方式。 消费级 UPS 已相当便宜,而且检测完整性问题并从中恢复可能非常耗时又会产生问题。

    InterSystems 建议您在未确保计算机连接到不间断电源的情况下不要开启此设置。 如果存储是外部设备,则该设备也需要连接到 UPS。

    0
    0 270
    文章 Hao Ma · 一月 10, 2021 7m read

    虽然 Caché 和 InterSystems IRIS 数据库的完整性完全不会受到系统故障的影响,但物理存储设备故障确实会损坏其存储的数据。 因此,许多站点选择运行定期数据库完整性检查,尤其要与备份配合,以验证在发生灾难时是否可以依赖给定的备份。 系统管理员在应对涉及存储损坏的灾难时,也可能强烈需要完整性检查。 完整性检查必须读取所检查的 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(有关详细信息,请参阅文档)。 它将结果收集在一个临时的 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 函数(类似于上面提及的函数)为 SetAsyncReadBufferSizeGetAsyncReadBufferSize

    隔离完整性检查

    许多站点直接在生产系统上运行定期完整性检查。 这当然是最简单的配置,但并不理想。 除了完整性检查对存储带宽的影响,并发数据库更新活动有时还可能导致误报错误(尽管检查算法内置了缓解措施)。 因此,在生产系统上运行的完整性检查所报告的错误,需要由管理员进行评估和/或重新检查。

    很多时候,存在更好的选择。 可以将存储快照或备份映像挂载到另一台主机上,在那里由隔离的 Caché 或 IRIS 实例运行完整性检查。 这样不仅可以防止任何误报,而且如果存储也与生产隔离,运行完整性检查可以充分利用存储带宽并更快完成。 这种方法非常适合使用完整性检查来验证备份的模型;经过验证的备份可以有效验证截至生成备份前的生产情况。 还可以通过云和虚拟化平台更容易地从快照建立可用的隔离环境。


    * 管理门户接口、完整性检查任务和 SYS.Database 的 IntegrityCheck 方法会选择相当多的进程(等于 CPU 内核数),在很多情况下缺少所需的控制。 管理门户和任务还会对任何报告错误的 global 执行完整的重新检查,以识别可能因并发更新而出现的误报。 除了完整性检查算法内置的误报缓解措施,也可能进行这种重新检查;在某些情况下,由于会花费额外的时间(重新检查在单个进程中运行,并检查整个 global),可能并不需要重新检查。 此行为将来可能会更改。

    0
    0 207
    文章 Hao Ma · 一月 10, 2021 17m read

    RESTful 应用程序编程接口 (API) 设计和文档编制初学者指南。 通过示例,您将学习一些常见的 RESTful API 模式。

    在阅读之前

    您需要知道

    • 如何在 Ensemble 中创建 RESTful Web 服务
    • 如何在 Ensemble 中使用 RESTful Web 服务
    • 如何传递服务参数
    • 如何返回服务结果

    什么是服务 API?

    什么是应用程序编程接口? 是具体化的东西吗? 是单一编程单元吗? API 的作用是什么? 在我看来,API 是由程序代码以间接方式决定的。 但完全定义的 API 是由运行可执行程序的容器(由部署设置控制)提供的。 因此,我宁愿将 API 定义为服务的公共描述。 该描述可以是人类可读的,也可以仅机器可读, 或者两者均可。 API 用于与将要使用服务的人员共享有关服务的基本信息。 API 说明了服务的作用、使用环境、功能以及管理的数据结构等。

    在过去的好时光,“编制程序文档”或多或少是一种“必要之恶”。 现代编程语言通过在程序源码中引入声明来强制编制文档。 虽然声明是“机器人”可读的文档,但通过使用工具(runoff、Java doc...),可以提取信息并将其格式化成人类可读的格式。 即使没有在源码中添加任何一行真正的文档,这些工具仍然能够生成少量文本。

    现在有什么不同吗? 并没有。 服务 API 仍然是一个抽象的概念,表示正常使用一个功能性计算机软件所需的信息集合。 有一些语言可以将 API 定义形式化,例如 Web 服务描述语言 (WSDL)。 遗憾的是,这些语言的使用受限。 原因并不在于,例如,WSDL 的能力不足以表达 RESTful API,而是因为非技术上的不匹配。 (用 XML 表达 JSON 结构会是怎样的?) 最终,没有像适用于 SOAP Web 服务的 WSDL 那样的适用于 REST 的实际标准语言。 很可惜。 是不是?

    没关系。 无论如何,我们首先需要了解 API 编制的内容。

    API 由什么组成?

    服务的核心属性是什么?

    • 服务位置。 服务的 URL 根路径。 例如 http://localhost:57774/csp/msa/person
    • 服务方法。 即服务的功能。 方法由 HTTP 标头中的动词(GET、POST、PUT…)与其他路径类型参数的组合定义。
    • 接受的方法参数。 参数及其类型的列表。 类型可以是路径,表示 URL 路径中包含的参数;查询,表示编码的 URL 查询;表单,表示表单数据;内容,表示 HTTP 消息正文。
    • 返回状态。 HTTP 响应标头中的状态字段。 每个服务方法可能有多个返回状态码。 数字取决于服务方法和异常处理的粒度。
    • 响应内容。 每个状态码的预期内容。 格式可能因状态码而异。 例如,成功完成某个请求后,预期获得一个 JSON 序列化对象。 如果服务器出错 (500),将发送纯文本说明。

    示例 API

    首先,让我们试着描述一下我们要实现的目标。 我们要构建一个非常简单的服务。 一个注册表服务。 它将管理相同类型的资源。 例如人员。

    结构非常简单:姓名、出生日期和地点、母亲的娘家姓和生成的内部唯一注册表 ID。 地点的结构如下:国家/地区、城市。

    我们要向注册表插入一条新记录(更新完整的注册表条目),更新条目的各个属性(更新属性),删除条目,按注册表 ID 获取单个条目,根据属性匹配查询注册表 ID 列表。

    我们还需要一些服务功能:初始化注册表,填充一些记录以进行测试。

    从外部看,我们希望 http://localhost:57774/csp/msa/person 为服务位置。

    在服务位置通过 PUT 添加新条目。 将注册表记录作为内容发送。 预期返回带注册表 ID 的完整条目。

    通过 POST 更新。 URL 包含要更新的条目的注册表 ID。 要更新的属性以表单数据的形式发送。

    GET 用于检索数据。 如果 URL 路径以 ID 结尾,则返回由 ID 标识的注册表条目。 如果找不到 ID,但 URL 中有查询,则返回 ID 列表。 例如 http://localhost:57774/csp/msa/person/12A33 返回条目 12A33。 查询键值对是内部用于选择条目的属性匹配子句。 例如,http://localhost:57774/csp/msa/person?name=Hahn%20Istvan&dob=1961 返回出生于 1961 年且姓名为 Hahn Istvan 的人员名单。

    DELETE 执行删除。

    POSThttp://localhost:57774/csp/msa/person/_init 将初始化注册表。

    POSThttp://localhost:57774/csp/msa/person/_populate/100 将加载 100 条测试条目。

    API 文档

    下面一节给出了一个如何对服务 API 编制文档的示例。 请记住,结构和内容均未标准化。 这只是一个示例。

    我尽量让文档编制变得“工具不可知”。 市场上已有文档编制工具。 其中一些的表现已几乎达到应有的效果。 本节的目的是让您感受一下,使用文本编辑器来编制 API 文档有多复杂。

    资源:            人员

    一个用于管理人员类型资源的通用服务。 人员具有最少的一组属性。 基本上是人口统计特征和注册表 ID。

    位置:    http://localhost:57774/csp/msa/person

    方法:

    根据资源的唯一 ID 获取单个资源。

    动词:GET

    参数:

    <td>
      类型
    </td>
    
    <td>
      数据类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      路径
    </td>
    
    <td>
      资源 ID
    </td>
    
    <td>
      要检索的资源的唯一 ID。
    </td>
    
    名称
    1

    响应:

    <td>
      返回类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      人员
    </td>
    
    <td>
      找到记录。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      不存在该资源 ID 的记录。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      未经授权的访问。 该资源需要在标头中提供用户凭据。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      被禁止。 用户无权访问该资源。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      内部服务器错误。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      请求的方法未实现。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      服务暂时不可用。
    </td>
    
    状态
    200
    204
    401
    403
    500
    501
    503

     

    方法:

    根据非唯一查询获取匹配的资源 ID 列表。 该方法使用查询部分来构建查询字符串。 查询键/值对会转换为列名/值对。

    动词:GET

    参数:

    <td>
      类型
    </td>
    
    <td>
      数据类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      查询
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
      搜索条件。
    </td>
    
    <td>
      查询
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
       
    </td>
    
    <td>
      查询
    </td>
    
    <td>
      日期
    </td>
    
    <td>
       
    </td>
    
    <td>
      查询
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
       
    </td>
    
    <td>
      查询
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
       
    </td>
    
    名称
    name
    motherMaidenName
    dob
    birthPlaceCounty
    birthPlaceCity

    响应:

    <td>
      返回类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      人员
    </td>
    
    <td>
      找到记录。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      无匹配记录。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      未经授权的访问。 该资源需要在标头中提供用户凭据。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      被禁止。 用户无权访问该资源。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      内部服务器错误。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      请求的方法未实现。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      服务暂时不可用。
    </td>
    
    状态
    200
    204
    401
    403
    500
    501
    503

     

    方法:

    从注册表中删除条目。

    动词:DELETE

    参数:

    <td>
      类型
    </td>
    
    <td>
      数据类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      路径
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
      唯一注册表 ID。
    </td>
    
    名称
    1

    响应:

    <td>
      返回类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      人员
    </td>
    
    <td>
      记录已删除。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      未经授权的访问。 该资源需要在标头中提供用户凭据。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      被禁止。 用户无权访问该资源。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      内部服务器错误。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      请求的方法未实现。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      服务暂时不可用。
    </td>
    
    状态
    200
    401
    403
    500
    501
    503

     

    方法:

    向注册表添加或更新条目。

    动词:PUT

    参数:

    <td>
      类型
    </td>
    
    <td>
      数据类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      内容
    </td>
    
    <td>
      JSON
    </td>
    
    <td>
      序列化为 JSON 格式的对象。
    </td>
    
    名称

    响应:

    <td>
      返回类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      人员
    </td>
    
    <td>
      注册表服务已新增或更新具有生成的资源 ID 的条目。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      未经授权的访问。 该资源需要在标头中提供用户凭据。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      被禁止。 用户无权访问该资源。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      内部服务器错误。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      请求的方法未实现。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      服务暂时不可用。
    </td>
    
    状态
    200
    401
    403
    500
    501
    503

     

    方法:

    更新注册表条目的单个属性。

    动词:POST

    参数:

    <td>
      类型
    </td>
    
    <td>
      数据类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
       
    </td>
    
    <td>
      路径
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
      要更新的条目的资源 ID。
    </td>
    
    <td>
       
    </td>
    
    <td>
      表单
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
      属性的新值
    </td>
    
    <td>
      表单
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
       
    </td>
    
    <td>
      表单
    </td>
    
    <td>
      日期
    </td>
    
    <td>
       
    </td>
    
    <td>
      表单
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
       
    </td>
    
    <td>
      表单
    </td>
    
    <td>
      字符串
    </td>
    
    <td>
       
    </td>
    
    名称
    1
    name
    motherMaidenName
    dob
    birthPlaceCounty
    birthPlaceCity

    响应:

    <td>
      返回类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      人员
    </td>
    
    <td>
      记录已更新。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      不存在该资源 ID 的记录。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      未经授权的访问。 该资源需要在标头中提供用户凭据。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      被禁止。 用户无权访问该资源。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      内部服务器错误。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      请求的方法未实现。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      服务暂时不可用。
    </td>
    
    状态
    200
    204
    401
    403
    500
    501
    503

     

    方法:

    初始化注册表。

    动词:POST

    参数:

    <td>
      类型
    </td>
    
    <td>
      数据类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      路径
    </td>
    
    <td>
       
    </td>
    
    <td>
       
    </td>
    
    名称
    _init

    响应:

    <td>
      返回类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      无
    </td>
    
    <td>
      已初始化。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      未经授权的访问。 该资源需要在标头中提供用户凭据。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      被禁止。 用户无权访问该资源。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      内部服务器错误。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      请求的方法未实现。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      服务暂时不可用。
    </td>
    
    状态
    200
    401
    403
    500
    501
    503

     

    方法:

    填充测试数据。

    动词:POST

    参数:

    <td>
      类型
    </td>
    
    <td>
      数据类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      路径
    </td>
    
    <td>
       
    </td>
    
    <td>
       
    </td>
    
    <td>
      路径
    </td>
    
    <td>
      数值
    </td>
    
    <td>
      要填充的条目数。
    </td>
    
    名称
    _populate
    2

    响应:

    <td>
      返回类型
    </td>
    
    <td>
      注释
    </td>
    
    <td>
      无
    </td>
    
    <td>
      已初始化。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      未经授权的访问。 该资源需要在标头中提供用户凭据。
    </td>
    
    <td>
      无
    </td>
    
    <td>
      被禁止。 用户无权访问该资源。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      内部服务器错误。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      请求的方法未实现。
    </td>
    
    <td>
      错误
    </td>
    
    <td>
      服务暂时不可用。
    </td>
    
    状态
    200
    401
    403
    500
    501
    503

     

    数据结构:

    人员

    名称类型标记注释
    ID注册表 IDR生成的注册表 ID。
    Name字符串R该人员的母语形式的姓名。
    DOB日期R出生日期。
    BirthPlace出生地O出生地。
    MotherMaidenName字符串O母亲的娘家姓。

    出生地

    名称类型标记注释
    Country字符串O国家/地区代码。
    City字符串R城市名称

    错误

    名称类型标记注释
    Code字符串R错误代码
    Text字符串O错误文本
    InnerError错误O报告组件的子组件报告内部错误。

    实现

    以下部分给出了我们前面讨论的资源注册表的示例。 这同样只是一个示例。

    为了让您更容易理解,我人为地将源码进行了分组。

    n  属于 API 的所有内容都放到资源映射类中。

    n  完整的 UrlMap XData 块分成单个 Route 条目。

    n  每个条目都粘附到实际实现功能的静态方法。

    所以,要恢复真实的类,需要进行一些(重新)设计。 请愉快地(重新)设计!

    第一个服务方法是查询...

    
    
    <
    
    Route Url="/:service" Method="GET" Call="QueryRegistry"/>
    
    classmethod QueryRegistry(service) as %Status {     
    
            try {
    
                   set serviceInstance = ..getServiceInstance(service)
    
                   do ..dumpResponse(serviceInstance.runQuery(..getQueryParameters($listbuild("name","dob","motherMaidenName","birthPlaceCountry","birthPlaceCity"))))
    
            }
    
            catch ex {
    
                   do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())
    
            }
    
            quit $$$OK
    
    }

     

    
    
    <
    
    Route Url="/:service/:registryID" Method="GET" Call="GetEntry"/>
    
    classmethod GetEntry(service,registryID) as %Status {
    
            try {
    
                   set serviceInstance = ..getServiceInstance(service)
    
                   do ..dumpResponse(serviceInstance.get(registryID))
    
            }
    
            catch ex {
    
                   do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())
    
            }
    
            quit $$$OK
    
    }

     

    
    
    <
    
    Route Url="/:service/:registryID" Method="DELETE" Call="DeleteEntry"/>
    
    classmethod DeleteEntry(service,registryID) as %Status {
    
            try {
    
                   set serviceInstance = ..getServiceInstance(service)
    
                   do ..dumpResponse(serviceInstance.delete(registryID))
    
            }
    
            catch ex {
    
                   do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())
    
            }
    
            quit $$$OK
    
    }

     

    
    
    <
    
    Route Url="/:service/_init" Method="POST" Call="InitializeRegistry"/>
    
    classmethod InitializeRegistry(service) as %Status {
    
            try {
    
                   set serviceInstance = ..getServiceInstance(service)
    
                   do ..dumpResponse(serviceInstance.init())
    
            }
    
            catch ex {
    
                   do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())
    
            }
    
            quit $$$OK
    
    }

     

    
    
    <
    
    Route Url="/:service/_populate/:numberOfRecords" Method="POST" Call="Populate"/>
    
    classmethod Populate(service,numberOfRecords) as %Status {
    
            try {
    
                   set serviceInstance = ..getServiceInstance(service)
    
                   do ..dumpResponse(serviceInstance.populate(numberOfRecords))
    
            }
    
            catch ex {
    
                   do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())
    
            }
    
            quit $$$OK
    
    }

     

    
    
    <
    
    Route Url="/:service/:registryID" Method="POST" Call="UpdateAttribute"/>
    
    classmethod UpdateAttribute(service,registryID) as %Status {
    
            try {
    
                   set serviceInstance = ..getServiceInstance(service)
    
                   do ..dumpResponse(serviceInstance.updateAttribute(registryID, ..getFormParameters($listbuild("name","dob","motherMaidenName","birthPlaceCountry","birthPlaceCity"))))
    
            }
    
            catch ex {
    
                   do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())
    
            }
    
            quit $$$OK
    
    }

     

    
    
    <
    
    Route Url="/:service" Method="PUT" Call="AddOrUpdate"/>
    
    classmethod AddOrUpdate(service) as %Status {
    
            try {
    
                   set serviceInstance = ..getServiceInstance(service)
    
                   do ..dumpResponse(serviceInstance.addOrUpdate(..getContentParameter()))
    
            }
    
            catch ex {
    
                   do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())
    
            }
    
            quit $$$OK
    
    }

     

    现在是时候分享您的实用方法了。

    classmethod getServiceInstance(serviceName) as Ens.BusinessService {
    
            set status = ##class(Ens.Director).CreateBusinessService(serviceName, .instance)
    
            throw:$$$ISERR(status) ##class(NoProduction).%New(status)
    
            quit instance
    
    }

     

    classmethod getHTTPStatusCode(ex) {
    
            quit $case(ex.%ClassName(1),
    
                                  ##class(NoProduction).%ClassName(1)                  :503,
    
                                  ##class(NotImplemented).%ClassName(1)        :501,
    
                                                                                                                     :500)
    
    }

     

    classmethod dumpResponse(responseObject) {
    
            if $isObject(responseObject) {
    
                   if responseObject.%Extends(##class(%DynamicObject).%ClassName(1)) { write responseObject.%ToJSON() }
    
                   elseif responseObject.%Extends(##class(%ZEN.proxyObject).%ClassName(1)) {
    
                           do ##class(%ZEN.Auxiliary.jsonProvider).%ObjectToJSON(responseObject)
    
                   }
    
                   elseif responseObject.%Extends(##class(%XML.Adaptor).%ClassName(1)) {
    
                           do responseObject.XMLExportToString(.ret)
    
                           write ret
    
                   }
    
                   else { throw ##class(Serialization).%New() }
    
            }
    
            else {
    
                   write responseObject
    
            }      
    
    }

     

    classmethod getQueryParameters(parameterList) as %DynamicObject {
    
            set parameterObject = {}
    
            for i=1:1:$listlength(parameterList) {
    
                   set parameterName=$listget(parameterList,i)
    
                   set $property(parameterObject, parameterName) = %request.Get(parameterName)
    
            }
    
            quit parameterObject
    
    }

     

    classmethod getFormParameters(parameterList,queryObject) as %DynamicObject {
    
            if $data(queryObject) { set parameterObject = queryObject }
    
            else { set parameterObject = {} }
    
            for i=1:1:$listlength(parameterList) {
    
                   set parameterName=$listget(parameterList,i)
    
                   set $property(parameterObject, parameterName) = %request.Get(parameterName)
    
            }
    
            quit parameterObject
    
    }

     

    classmethod getContentParameter() as %DynamicObject {
    
            quit {}.%FromJSON(%request.Content)
    
    }

     

    到此为止。 我们完成了一个 RESTful Web 服务 API 的设计(?)、实现和文档编制。

    敬请关注,我很快会回来进一步解读 Ensemble RESTful Web 服务。 下一个主题是“使用 RESTful Web 服务创建 Ensemble 微服务”。

    0
    0 184
    文章 Hao Ma · 一月 10, 2021 6m read

    在我发了前一个帖子后,有人催促我说重点 - 好吧,于是我找到了我的“明星”日志 global,也就是那些最占空间的 global - 但如何避免这种情况呢? 如何最大程度地减小日志?

    [免责声明:有些人在看到本帖后可能仍然会失望 frown,那就等下个帖子吧... blush]

    不过很遗憾,你还得再多失望一会儿,我需要推迟讨论(不会很久...),因为我要先提出两个我认为很重要的问题,之后再讨论如何避免记录那些我们不需要的日志,这两个问题是:

    1. 到底有哪些内容会被写入日志?
    2. 为什么?

    我认为在讨论不记录日志之前回答这两个问题非常重要的原因是,从根本上来说,日志记录是有好处的,所以数据首先会放在日志中,如果你决定不记录日志,必须有非常充分的理由。

    让我们尝试回答这两个问题 -

    通常每个 SET 和 KILL 命令(以及 TSTART 和 TCOMMIT/TROLLBACK)都会记录在日志文件中。

    例如,如果我运行以下命令

    我会在日志中看到:

    日志文件有几个用途:

    1. 为了防止在系统崩溃后的启动恢复中发生数据丢失 - 当系统启动时,如果日志中的操作尚未记入数据库,系统会前滚这些操作(假设它们不是未提交事务的一部分,否则它们会被回滚)。
    2. 作为灾难恢复计划的补充 - 允许恢复自上一个可用备份以来更改的数据。 例如,如果在昨天夜间运行了一次备份,由于某种原因,数据库(即 CACHE.DAT)文件在中午时受损(磁盘损坏或其他原因),那么可以从备份中恢复,让状态回到早上的样子,然后从日志恢复到中午发生系统故障之前的点,以将数据损失降到最低。
    3. 事务回滚支持 - 无论出于何种原因,都能回滚事务。

    更多信息,请参见此处

    (写一个单独的帖子可以更详细地介绍每个用途,并说明其用法。 还可以介绍日志中的回滚是什么样子等等。 这不是我目前正在计划的帖子,但欢迎其他人阐述这个主题。

    所以记录数据日志是有充分理由的,它非常有用,甚至至关重要(重要到甚至有一个开关的说明是,如果日志记录因某种原因失败,我们应该冻结整个系统)。

    (希望此时不会让你因为考虑不记录日志而感到内疚...wink

    Caché 中的每个数据库都有一个指示数据是否被记录日志的标志。 默认和推荐的设置是记录日志。 即使你决定不记录,你也应该意识到,事务中的任何 SET 或 KILL 仍然会被记入日志,即使数据所在的数据库被标记为不记录日志(为了允许上文所述的回滚)。 [一个重要的例外是位于 CACHETEMP 数据库中的 global,我们稍后会用到它]

    例如,SAMPLES 数据库未记录日志,因此简单的 SET 不会被记录,但事务中的 SET 会被记录:

    日志不显示第一个 SET(在事务外部),只显示第二个:

    对于事务内自动保存的对象同样如此。

    再以 SAMPLES 为例:

    我们得到这样的结果(尽管我们没有明确发出 TSTART 命令,但对象归档器会在后台执行此操作):

    这样,虽然简短,但我们的 2 个问题(日志记录什么和为什么记录)都得到了回答。

    这将引导我们进入下一步 – 如何避免某些数据被记入日志。

    你会意识到的第一个问题是决定是否可以真正“承担得起”不记录那些你想要避免填满日志的数据。

    不过让我先说一个非常简单的案例 -“哇! 我不知道我们在设置这个 global?!”

    例如, 查看日志可以发现,也许在代码中的某个地方,我们设置了一个临时开关,可能是设置某个时间,或者是为了调试目的而需要写入一些日志,或者是具有临时调查性质的某种机制,而我们完全不需要这个开关,我们可以删除该代码(或者注释掉,或者不调用,或者关闭开关等等)。

    这是一个相对容易的案例,我们不需要继续讨论是否以及如何不记录日志。

    现在,关于此案例,你可能会想 - 你需要看日志才能发现这个“失控”的 global 设置吗,你看不到数据库实际在增长吗?

    好吧,也许的确如此,数据库也确实在增长,但你忽略了,或者没有给予过多关注并且/或者认为这只是自然增长。 但也可能是另一种情况,这是我要强调的一点,就是日志增长不一定与数据库增长相关联。

    举一个极端的例子:

    你可以设置一个 global 的值,让其在两个值(比如 0 和 1)之间交替,每秒多次。 数据库完全不会增长,因为数据的大小没有变化,但日志中会有成千上万的 SET 条目。

    或者你可以设置一个临时的大型 global 树结构,数秒钟后将其终止,那么树结构占用的数据库大小差不多是恒定的,但是日志会因为记录所有 set 和 kill 操作而填满。

    假设情况并非如此,你对设置此数据并不感到惊讶,实际上你很清楚这一点,并且你的应用程序需要设置此数据。 那样的话,我们又回到了前面的问题 - 你是否真正承担得起不记录这些数据的日志?

    为了回答这个问题,我们试着把它分成几个小一点的问题:

    1. 你能承受在崩溃前输入(或删除)的数据在崩溃后不存在(或重新出现)吗?
    2. 你能承受所输入(或删除)的数据在系统恢复过程中无法恢复(或重新出现)吗?
    3. 你不需要这些数据作为事务的一部分被回滚吗?

    如果你对全部 3 个问题的回答都是“是”,那么你确实可以考虑不记录这些数据的日志了。

    但是如果你需要此数据在崩溃后恢复,或者作为备份的一部分恢复,或者需要作为事务的一部分回滚。 你就需要记录日志。

    在这种情况下,你只需要计算并确定真正的日志增长率,并确保有足够的磁盘空间 [你可以看我的另一个帖子,一些相关代码在 Git 上,可以帮助估计 Ensemble 接口所需的日志空间。 当然你也可以修改这些代码来用于其他用例]。

    以下是我将在下一个帖子介绍的选项,先让你们预览一下:

    • 关闭整个系统的日志记录
    • 将 global 映射到 CACHETEMP
    • 将 global 映射到不记录日志的数据库
    • 关闭对进程的日志记录
    • 关闭用于对象归档的事务

    下次再见...(我保证到时候说重点...angel

    这是下一个帖子

    0
    0 178