文章 Hao Ma · 十二月 23, 2024 6m read

技术支持团队在不同的项目中发现了类似中间人攻击的情况, 和各位分享一下。

我们的系统一般是安装在内网里,没有恶意的中间人攻击的风险。但是在有些医院发现了这样的情况:IT在网络中安装了某种网络监控或者嗅探的设备, 它会在通信通道中模拟其中一方,或者双方的通信节点, 以截获通信双方的网络流量。通常它不影响双方的通信,但偶尔,它会中断双方的连接, 造成业务的中断。实质上这也是一种中间人攻击的情况,只不过这是用户允许的行为,偶然出现了故障。 

我们看看以下的例子:

以下的wireshark抓包截图中, 172.18.1.131和172.18.1.145在正常的通信过程中, 忽然收到了RST消息,造成了TCP连接上的复位。

其中172.18.1.131是intersystems的health connect系统, 它在序号50134的包里面首先发送了RST,因此客户怀疑是不是Health Connect出错,中断了连接,也就把问题提交了InterSystems的技术支持。

0
0 299
文章 Hao Ma · 十月 28, 2024 4m read

CPF merge(合并)

Automating Configuration of InterSystems IRIS with Configuration Merge

CPF merge通过合并一个人工编辑的merge file, 自动的配置新创建的iris instance, 或者修改已有的iris instance。适用于:

  • 修改iris的系统配置(cpf文件中的内容),比如系统参数,创建或者配置用户,角色,权限, 创建或者配置命名空间,数据库; 配置mirror, ECP连接等等。
  • 操作不在cpf文件中的内容,比如 CreateApplication, CreateSSLConfig 等等, 一般是在 [Action]部分实现

Caché 和早期的IRIS版本提供了manifest功能,用来做IRIS实例的配置。 Manifest很繁琐,而且各个版本的配置中有细微的区别,非常难以管理。 如今有了CPF merge, maifest的所有功能都可以在CPF merge实现, 因此manifest在新版IRIS中也就完全被替代了。

figure

执行merge

执行merge可以在操作系统命令行下执行, 如下面的例子

# 第2个参数可选,如果为空,自动使用系统当前的iris.cpf
$ iris merge iris /external/irismerge.conf /usr/irissys/iris.cpf

然而, 在执行docker run命令或者docker-compose里不用这么麻烦。 IRIS镜像提供了一个功能, 通过加入ISC_CPF_MERGE_FILE环境变量,IRIS会自动实现CPF合并。 比如下面的compose配置:

iris2024:      
        image: containers.intersystems.com/intersystems/irishealth-arm64:2024.1
        container_name: iris-a
        volumes:
            - ./iris2024:/external
        environment: 
            - TZ=CST-8
            - ISC_CPF_MERGE_FILE=/external/merge.cpf

IMPORTANT:

当使用配置合并部署容器时(如 使用合并文件部署 InterSystems IRIS 容器 中所述),只要容器在运行,ISC_CPF_MERGE_FILE(它在容器中是持久的)指定的合并文件就会持续受到更新监控,更新发生时,iris 合并命令会立即对其进行合并。这意味着您可以随时通过更新合并文件来更新容器化实例的配置,从而更容易自动重新配置容器化实例和群集。

merge文件的Example

配置IRIS参数

[config]
bbsiz=-1
globals=0,0,1024,0,0,0
routines=512
gmheap=256000

[SQL]
DefaultSchema=user
TimePrecision=6

[SqlSysDatatypes]
TIMESTAMP=%Library.PosixTime

创建用户和权限

CreateUser:Name=SQLAdmin,PasswordHash=fce110ae1f79b9d7e20367a3352efeb48ef22cc8810c4598791f3fb752eabcfe7b2d9ce75099e626b03a62fc6a146f1ca772789ebbcea276674c558c63af4f7b,9ffbbc8ef25086e4d4da87e20c6ea43e8ebb6d1ab64815aef3b1f7e8964dbd87efe68c8464a6b40865efc7d0d568c601e2a49917326dfd78197b68f1bde59db2,10000,SHA512
GrantAdminPrivilege:Grantee=SQLAdmin,Namespace=USER,AdminPriv="%DB_OBJECT_DEFINITION,%BUILD_INDEX"

CreateUser:Name=CCHUser,Password=Demo1234
GrantAdminPrivilege:Grantee=SQLAdmin,Namespace=HCC,AdminPriv="%DB_HCC,%BUILD_INDEX"

创建命名空间

# part of merge.cpf
[Actions]
CreateResource:Name=%DB_DEMO,Description="The DEMO database"
CreateDatabase:Name=DEMO,Directory=/usr/irissys/mgr/demo,Resource=%DB_DEMO,
CreateNamespace:Name=DEMO,Globals=DEMO,Interop=1

创建Web Application

[Actions]
CreateApplication:Name=/csp/demo, InboundWebServicesEnabled=1, AutheEnabled=1, DispatchClass=HCC.Interface.RestHandler, NameSpace=DEMO
CreateApplication:Name=/csp/demo,NameSpace=HCC,AutheEnabled=36,DispatchClass=HCC.Interface.RestHandler,
CreateApplication:Name=/test2,NameSpace=HCC,AutheEnabled=$$$AuthePassword+$$$AutheUnauthenticated,Enabled=1,Description="test2"
ModifyApplication:Name=/test2,MatchRoles=":%DB_HSCUSTOM:%DB_HSLIB:%DB_IRISSYS:%HS_DB_HCC:%HS_DB_HSSYS",ErrorPage=%CSP.Error

System Security Setting

System > Security Management > Authentication/Web Session Options - (security settings)

  • Allow Unauthentication access
  • Allow O/S authentication
# example
ModifyService:Name=%Service_Monitor,Enabled=1,ClientSystems=192.168.1.1
ModifyService:Name=%Service_DocDB,Enabled=1,AutheEnabled=$$$AuthePassword
ModifyService:Name=%Service_Terminal,AutheEnabled=$$$AuthePassword+$$$AutheUnauthenticated+$$$AutheOS
ModifyService:Name=%Service_Mirror,Enabled=1

其他还有更多的用法,请参见在线文档。

0
0 82
文章 Hao Ma · 十月 28, 2024 3m read

使用iris-main

iris-main是IRIS镜像的的ENTRYPOINT程序。 在Container中,ENTRYPOINT 指令允许你指定一个可执行程序或者脚本,作为容器启动后运行的主程序。这个程序会在容器启动时自动执行。

执行docker ps 命令可以看到当前container的ENTRYPOINT是什么:

hma@CNMBP23HMA demo % docker ps
CONTAINER ID   IMAGE                         COMMAND                 CREATED      STATUS      PORTS                                                               NAMES
8f31a857dc90   .../irishealth:2024.2   "/tini -- /iris-main"   3 days ago   Up 3 days   2188/tcp, 52773/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:1980->1972/tcp   iris-a

hma@CNMBP23HMA demo %

我们可以用root进入container, 在根目录下查看iris-main的帮助, 或者查看在线文档的帮助-iris-main

使用的iris-main例子

以下的compose文件中, 使用了--check-caps false关闭了LInux的能力检查(capability check), 使用--key /install/iris.key加载了IRIS的license文件,并且配置了--after /install/installer.sh,它定义在container启动执行install.sh脚本。

iris-a:      
        image: containers.intersystems.com/intersystems/irishealth-arm64:2024.2
        container_name: iris-a
        hostname: irisa
        ports:
            - 1980:1972
            - 52773:52773
        environment:
            - TZ=CST-8
        volumes:
            - ./install:/install
        command: 
            --check-caps false
            --key /install/iris.key
            --after /install/installer.sh

附: iris-main的帮助

# 用root进入container, 在根目录下查看iris-main的帮助
root@irisa:/# /iris-main --help

USAGE:

   /iris-main  [--check-caps <bool>] [--monitorCPF <bool>] [--monitorKey
               <bool>] [--ISCAgentPort <integer>] [--ISCAgent <bool>] [-k
               <license key>] [-p <password file>] [-t <command>] [-c
               <command>] [-e <command>] [-a <command>] [-b <command>] [-l
               <log file>] [-s <bool>] [-u <bool>] [-d <bool>] [-i
               <instance>] [--] [--version] [-h]


Where:

   --check-caps <bool>
     Does nothing; retained for backwards compatibility

   --monitorCPF <bool>
     Monitor InterSystems IRIS CPF merge file and apply if changes detected

   --monitorKey <bool>
     Monitor InterSystems IRIS license key and update if changes detected

   --ISCAgentPort <integer>
     Set the port for ISC Agent. The default value is 2188

   --ISCAgent <bool>
     Start ISC Agent before starting IRIS. If this argument is not
     specified, it defaults to true

   -k <license key>,  --key <license key>
     Copies the InterSystems IRIS license key from the directory specified
     to the active data directory

   -p <password file>,  --password-file <password file>
     File containing desired InterSystems IRIS password

   -t <command>,  --terminate <command>
     Execute a shell command on application shutdown, after any other
     arguments are processed

   -c <command>,  --create <command>
     Execute a shell command at application startup, before any other
     arguments are processed

   -e <command>,  --exit <command>
     Execute shell commands after stopping InterSystems IRIS (via 'iris
     stop')

   -a <command>,  --after <command>
     Execute shell commands after starting InterSystems IRIS (via 'iris
     start')

   -b <command>,  --before <command>
     Execute shell commands before starting InterSystems IRIS (via 'iris
     start')

   -l <log file>,  --log <log file>
     InterSystems IRIS log file to be redirected to stdout for monitoring
     via 'docker logs'

   -s <bool>,  --nostu <bool>
     Start InterSystems IRIS with the 'nostu' option for maintenance,
     single user access mode

   -u <bool>,  --up <bool>
     Start InterSystems IRIS (via 'iris start') on container startup

   -d <bool>,  --down <bool>
     Stop InterSystems IRIS (via 'iris stop') on container shutdown

   -i <instance>,  --instance <instance>
     The InterSystems IRIS instance name to start/stop

   --,  --ignore_rest
     Ignores the rest of the labeled arguments following this flag.

   --version
     Displays version information and exits.

   -h,  --help
     Displays usage information and exits.


   iris-main

root@irisa:/#
0
0 85
文章 Hao Ma · 十月 28, 2024 2m read

把CSP.conf保存在container之外

在创建webgateway的container时,可以使用ISC_DATA_DIRECTORY=参数, 选择把CSP文保存在主机而不仅仅是container内部。如下面的例子: 使用volumnes映射了主机的./dur-wg-a目录到container的/dur目录, 而command中的ISC_DATA_DIRECTORY=/dur会讲webgateway的配置文件, log文件等保存在主机。

webgateway-apache:
        image: containers.intersystems.com/intersystems/webgateway-arm64:2024.1
        container_name: wg-tls
        hostname: wg-tls
        ports:
            - "8080:80"
            - "4433:443"
        volumes:
            - ./webgateway/csp:/external
            - ./dur-wg-a:/dur
        environment:
            - TZ=CST-8
            - ISC_CSP_CONF_FILE=/external/CSP-apache.conf
            - ISC_CSP_INI_FILE=/external/CSP-merge.ini
            - ISC_DATA_DIRECTORY=/dur

需要注意的是,这种情况下, 当配置了ISC_CSP_CONF_FILE时,比如把定制的CSP.conf放在了/dur/CSP.conf, 实际上是创建了一个link到/etc/apache2/mods-available, 而最终会链接到/etc/apache2/mods-enabled. 真正工作的CSP.conf还是在/etc/apache2/mods-enabled.

在apache2加载网站

虽然绝大多数情况WebGateway Container只用于连接IRIS,但如果在测试或者演示环境中,希望在apache2中加入自己的网站或者网页, 可以简单的参考下面的说明。

在默认的apache2.conf里面默认的定义了3个directory, <Directory /usr/share>, <Directory /usr/share>,<Directory /var/www/>,而在sites-enabled里面是这么配置的

root@ac6fdedbac6b:/etc/apache2# cat sites-enabled/000-default.conf
<VirtualHost *:80>
	ServerAdmin webmaster@localhost
	DocumentRoot /var/www/html
	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
root@ac6fdedbac6b:/etc/apache2#

因为000-default.conf是默认生效的配置文件,因此把自动的网站放在/拷贝到/var/www/html目录是最简单的方案。 注意您的网站如果有js或者其他可执行的文件, 网站目录和文件的权限应该是755.

0
0 89
文章 Hao Ma · 十月 28, 2024 7m read

上一篇文章使用人工配置的方法简单的配置了webgateway container. 接下来来介绍如何在docker-compose里做自动化部署。

先总结我们要做的事情:

  1. 配置到IRIS的连接。定义连接的iris的IP地址或者DNS, 以及连接的用户名密码 以及其他的对默认值的修改。
  2. 配置apache2的配置文件,保证到IRIS的HTTP请求能发送给CSP Webgateway。
  3. 很多时候,用户会希望使用HTTPS访问IRIS,因此需要在apache2上支持TLS。

这些是最基本的功能。除此之外, 用户还可能会要求建立WebGateway到IRIS的TLS连接,或者在Apache2部署自己的网页等等。后面的文章会一一介绍。

配置CSP.ini

上一篇文章中,我通过Webgateway管理页面定义了Webgateway到IRIS的连接,其实是定义了webgateway的配置文件CSP.ini。 无论WebServer是什么类型,IIS,Apache, Nginx, CSP.ini的都是一样的。在Linux中, CSP.ini位于/opt/webgateway/bin目录。

InterSystems提供了一个工具叫 CSP merge。 简单的说,就是可以定义一个被合并的文件, webgateway运行时会不停的扫描这个文件,发现有内容的修改,就把修改后的配置项合并到工作中的CSP.ini中去。 具体的内容您可以参见在线文档-CSP.ini merge feature

CSP merge的merge文件不需要定义所有的配置项,理论上,只定义需要修改的部分就可以。但注意在一个配置部分(section)内部, 配置项之间是有关系的,单独定义其中的一项配置是不会生效的。

比如, 这是原始的CSP.ini中的LOCAL服务器的配置部分:

因此,merge文件里正确的LOCAL部分是:

[LOCAL]
Ip_Address=127.0.0.1
TCP_Port=1972
Minimum_Server_Connections=3
Maximum_Session_Connections=6
Connection_Security_Level=0  ; 0:password, 1:kerberos...
Username=CSPSystem
Password=]]]U1lT
SSLCC_Protocol_Min=16
SSLCC_Protocol_Max=32
SSLCC_Key_Type=2
SSLCC_Cipher_Suites=ALL:!aNULL:!eNULL:!EXP:!SSLv2
SSLCC_Cipher_Suites_1_3=TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256

如果您只希望修改Ip_Address, 因此在被合并的文件中定义

[LOCAL]
Ip_Address=192.168.1.2
TCP_Port=1972

而这样的定义会 造成加载失败。也就是说,CSP merge需要合并完整的Section。上面的例子中定义了这个服务器的地址,但没有定义连接它的账户和密码,这是不能接受的。

我习惯用比较完整的配置文件做合并,下面是一个例子。文件名可以任意,这里我命名为CSP-merge.ini:

[APP_PATH_INDEX]
/=Enabled
/csp=Enabled

[APP_PATH:/]
Default_Server=LOCAL
Alternative_Server_0=1~~~~~~LOCAL

[APP_PATH:/csp]
Default_Server=LOCAL
Alternative_Server_0=1~~~~~~LOCAL

[SYSTEM]
IRISCONNECT_LIBRARY_PATH=/opt/webgateway/bin
System_Manager=*.*.*.*
SM_Timeout=28800
Server_Response_Timeout=60
No_Activity_Timeout=86400
Queued_Request_Timeout=60
Default_Server=LOCAL
RELOAD=1  ; reload csp gateway setting in one minute

[SYSTEM_INDEX]
LOCAL=Enabled

[LOCAL]
Ip_Address=iris-a
TCP_Port=1972
Minimum_Server_Connections=3
Maximum_Session_Connections=6
Connection_Security_Level=0  ; 0:password, 1:kerberos...
Username=CSPSystem
Password=]]]U1lT
SSLCC_Protocol_Min=16
SSLCC_Protocol_Max=32
SSLCC_Key_Type=2
SSLCC_Cipher_Suites=ALL:!aNULL:!eNULL:!EXP:!SSLv2
SSLCC_Cipher_Suites_1_3=TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256

Tip1: [SYSTEM]中配置RELOAD=1, 可以让webgateway setting每分钟检查一次csp-merge.ini, 如果有修改,会实时的执行合并。

Tip2: 想了解更多的CSP.ini的配置项的内容,请访问在线文档- Define a Server Access Profile for Your InterSystems IRIS Instance

Tip3: 在生产系统中,定义SYSTEM部分的: System_Manager=*.*.*.*是不安全的。

自动部署CSP合并文件

简单的说明: CSP-merge.ini文件位于host中的./webgateway文件夹,通过volumes映射到container的/external文件夹。 通过定义环境变量ISC_CSP_INI_FILE,docker启动时会执行CSP文件的合并。

 webgateway-apache:
      image: containers.intersystems.com/intersystems/webgateway-arm64:2024.1
      container_name: wg-apache
      hostname: wg-apache
      ports:
          - "8080:80"
      volumes:
          - ./webgateway:/external
      environment:
          - TZ=CST-8 
          - ISC_CSP_INI_FILE=/external/CSP-merge.ini

后面介绍如何配置apache2访问IRIS服务地址。

配置Apache2

配置apache2可以参考apache2的主页面,配置文件的结构如下:

apache2的主配置文件/etc/apache2/apache2.conf包含(includes)了其他自子目录下的被enabled的配置文件,结构如下图:

root@wg-a:/etc/apache2# cat apache2.conf
...
# It is split into several files forming the configuration hierarchy outlined
# below, all located in the /etc/apache2/ directory:
#
#	/etc/apache2/
#	|-- apache2.conf
#	|	`--  ports.conf
#	|-- mods-enabled
#	|	|-- *.load
#	|	`-- *.conf
#	|-- conf-enabled
#	|	`-- *.conf
# 	`-- sites-enabled
#	 	`-- *.conf

CSP.conf是怎么工作的

InterSystems Webgateway是apache2的一个工作的module. 它的配置文件是/etc/apache2/mods-available/CSP.conf。在webgateway container启动时,apache2会执行这个module的生效(enable), 这时会在/etc/apache2/mods-enabled目录中创建一个链接到原始文件。

默认的CSP.conf的内容如下:

root@wg-a:/etc/apache2# cat /etc/apache2/mods-available/CSP.conf

CSPModulePath "${ISC_PACKAGE_INSTALLDIR}/bin/"
CSPConfigPath "${ISC_PACKAGE_INSTALLDIR}/bin/"

<Location "/csp/bin/Systems/">
    SetHandler csp-handler-sa
</Location>
<Location "/csp/bin/RunTime/">
    SetHandler csp-handler-sa
</Location>

<Directory "${ISC_PACKAGE_INSTALLDIR}/bin/">
    AllowOverride None
    Options None
    Require all granted
    <FilesMatch "\.(log|ini|pid|exe)$">
         Require all denied
    </FilesMatch>
</Directory>
root@wg-a:/etc/apache2#

修改CSP.conf

以上的CSP.conf中只有使用CSP module的内容,而并没有访问IRIS的URL的部分。 , 需要一个个的添加要发送到webgateway模块的URL。必须的有这几个:/csp, /api,/isc, 其他还包括您自己定义的URL, 也需要创建对应的<Location>

以下是要添加的内容:

# 添加要发送给IRIS的URL
<Location "/csp">CSP On</Location>
<Location "/api">CSP On</Location>
<Location "/isc">CSP On</Location>
<Location "/customerURL">CSP On</Location>

Tip1: 如果您确认此container只用于连接IRIS, 您可以定义<Location />将所有的请求发送给webgateway模块。但注意访问http://localhost:8080时会返回错误404,apache2原本会把请求发到/var/www/html/index.html,修改后会把这个请求发给IRIS。

自动部署CSP.conf的修改

配置container是新的CSP.conf通过SC_CSP_CONF_FILE加入,这个不是合并, 是整个覆盖替换原始的配置文件。以下是compose文件的示意,同样,CSP-apache.conf文件位于host的./webgateway文件夹,通过volumes映射到container的/external文件夹。

webgateway-apache:
      image: containers.intersystems.com/intersystems/webgateway-arm64:2024.1
      container_name: wg-apache
      hostname: wg-apache
      ports:
          - "8080:80"
      volumes:
          - ./webgateway:/external
      environment:
          - TZ=CST-8 
          - ISC_CSP_INI_FILE=/external/CSP-merge.ini
          - ISC_CSP_CONF_FILE=/external/CSP-apache.conf

要修改CSP.conf, 如果要在container外面修改,需要修改ISC_CONF-FILE的内容, 然后重启container. /dur里面的CSP.conf不能改, 改了保存后会自动改回来。

配置Apache2的TLS

方法1. 用命令开启

配置HTTPS访问apache2最简单的方式打开apache2内置的ssl模块,并使default-ssl.conf生效。如下面的脚本文件中的操作:

root@wg-a:/etc/apache2# a2enmod ssl 
root@wg-a:/etc/apache2# ls -l mods-enabled/ssl*
lrwxrwxrwx 1 root root 26 Oct  9 09:00 mods-enabled/ssl.conf -> ../mods-available/ssl.conf
lrwxrwxrwx 1 root root 26 Oct  9 09:00 mods-enabled/ssl.load -> ../mods-available/ssl.load
root@wg-a:/etc/apache2# a2ensite default-ssl
Enabling site default-ssl.
To activate the new configuration, you need to run:
  service apache2 reload
root@wg-a:/etc/apache2# service apache2 reload
 * Reloading Apache httpd web server apache2                                                                                            *
root@wg-a:/etc/apache2#

方法2. 设置自己的配置

因为已经Copy了一个CSP.conf进去,其实可以在这个文件里配置SSL的部分, 好处是不用单独写一个文件, 坏处是从维护的角度,把CSP module的配置和SSL的配置写在一个文件不是很合理。

# 加入下部分到CSP.conf, 其中使用了自己的TLS配置,而不是openssl的证书和key. 
LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
<VirtualHost *:443>
    SSLEngine on
    SSLCertificateFile "/external/cert/sslwebcert.crt"
    SSLCertificateKeyFile "/external/cert/sslwebkey.key"
</VirtualHost>

或者使用ssl mod的 conf

<VirtualHost _default_:443>
		ServerAdmin webmaster@localhost
		DocumentRoot /var/www/html
		ErrorLog ${APACHE_LOG_DIR}/error.log
		CustomLog ${APACHE_LOG_DIR}/access.log combined
		SSLEngine on
		SSLCertificateFile	/etc/ssl/certs/ssl-cert-snakeoil.pem
		SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
		<FilesMatch "\.(cgi|shtml|phtml|php)$">
				SSLOptions +StdEnvVars
		</FilesMatch>
		<Directory /usr/lib/cgi-bin>
				SSLOptions +StdEnvVars
		</Directory>
</VirtualHost>
0
0 122
文章 Hao Ma · 十月 28, 2024 6m read

IRIS image下载

参考:下载Images的在线文档

https://containers.intersystems.com网站上获得可以下载的InterSystems的各种docker镜像。如果只是安装Community版本, 不需要注册。如果是下载安装正式的版本,需要在网站注册,然后获得Login Token登陆。

#;这一部有可能需要科学上网,否则无法正常登陆

hma@CNMBP23HMA ~ % docker login -u="hma" -p="k8zIqpoafIUaViP2BA4gCZdcC4EeKyb0svSjnyVtcWMb" containers.intersystems.com
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded

# pull iris image,webgateway,Passhash, arbiter, etc.
hma@CNMBP23HMA ~ % docker pull containers.intersystems.com/intersystems/healthconnect-arm64:latest-cd
hma@CNMBP23HMA ~ % docker pull containers.intersystems.com/intersystems/webgateway-arm64:latest-cd
...

Tips

  • 下载社区(Community)版本的镜像不用在网站注册,使用时也不需要安装license。社区版内置内置了一个13个月的license。而正式版本IRIS Continer安装需要从InterSystems处获得IRIS docker版的专用license。

  • 标记latest-cd(Continuous delivery)latest-em(Extended maintenance), latest-preview是最新的"持续交付"版本, "扩展维护"版本,以及"开发预览版"。 您也可以选择指定专门的版本,比如当前最新的GA版本2024.2。同时,如果硬件是ARM芯片,请选择带有“-arm64"后缀的版本。

  • WebGateway-nginx是nginx服务器的版本,而WebGateway镜像内置了apache2服务。

简单的搭建测试环境

我要使用的iris 2024的版本,其中再没有之前的内置的PWS(Private Web Server), 也就是说您当前以及无法从http://localhost:52773/csp/sys/UtilHome.csp登录管理门户了。因此我用docker-compose来创建下面的例子。docker-compose用来编排多个container共同工作。它自动的创建一个单独的docker network, 其中的所有service, 也就是docker container可以通过container name通信。

如果是简单的测试环境, 可以直接用docker run运行IRIS Container。 我们先来run一个最简单的社区版:

compose.yaml

services:
    iris-a:      
        image: containers.intersystems.com/intersystems/irishealth-community-arm64:2024.2
        container_name: iris-a
        hostname: irisa
        ports:
            - 1980:1972
            - 52773:52773
        environment:
            - TZ=CST-8
    webgateway-apache:
        image: containers.intersystems.com/intersystems/webgateway-arm64:2024.1
        container_name: wg-apache
        hostname: wg-apache
        ports:
            - "8080:80"
        environment:
            - TZ=CST-8

说明两点: 1. 在IRIS2024中52773端口已经无法使用,所以不需要再被映射到主机。 2. TZ=CST-8是把container的时区设置为上海。

运行后可以看到两个container都已经工作

hma@CNMBP23HMA demo % docker-compose up -d
[+] Running 3/3
 &#x2714; Network demo_default  Created                                                                                         0.0s
 &#x2714; Container wg-apache   Started                                                                                         0.0s
 &#x2714; Container iris-a      Started                                                                                         0.0s
hma@CNMBP23HMA demo % docker ps
CONTAINER ID   IMAGE                                                                        COMMAND                 CREATED         STATUS         PORTS                                                               NAMES
cfa4cc730c14   containers.intersystems.com/intersystems/irishealth-community-arm64:2024.2   "/tini -- /iris-main"   6 seconds ago   Up 5 seconds   2188/tcp, 52773/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:1980->1972/tcp   iris-a
63e983d90034   containers.intersystems.com/intersystems/webgateway-arm64:2024.1             "/startWebGateway"      6 seconds ago   Up 5 seconds   0.0.0.0:8080->80/tcp, 0.0.0.0:4433->443/tcp                         wg-apache
hma@CNMBP23HMA demo %

查看iris container

这时,虽然还无法登录iris的管理门户,您可以使用docker exec进入iris container查看iris实例已运行。 注意两点:

  1. 默认登录的用户是irisowner
  2. IRIS的安装路径是/usr/irissys.
hma@CNMBP23HMA installer_cch % docker ps
CONTAINER ID   IMAGE                                                              COMMAND                 CREATED         STATUS         PORTS                                                                              NAMES
59e2424757db   containers.intersystems.com/intersystems/irishealth-arm64:2024.1   "/tini -- /iris-main"   6 seconds ago   Up 5 seconds   0.0.0.0:1972->1972/tcp, 2188/tcp, 53773/tcp, 0.0.0.0:52773->52773/tcp, 54773/tcp   iris
hma@CNMBP23HMA installer_cch % docker exec -it iris bash
irisowner@59e2424757db:~$ iris list

Configuration 'IRIS'   (default)
	directory:    /usr/irissys
	versionid:    2024.1.0.263.0
	datadir:      /usr/irissys
	conf file:    iris.cpf
	status:       running, since Wed Oct 23 12:32:45 2024
	SuperServers: 1972
	state:        warn
	product:      InterSystems IRISHealth
irisowner@59e2424757db:~$ iris session iris
Node: 59e2424757db, Instance: IRIS

USER>halt
irisowner@59e2424757db:~$

查看Webgateway Container

  1. 从浏览器打开 http://localhost:8080, 可以看到apache2的主页面。
  2. 访问http://localhost:8080/csp/bin/Systems/Module.cxw?CSPSYS=0&CSPSYSreferer=_CSP.Portal.Home.zen,可以进入Webgateway的配置页面。 这里一定注意:访问http://localhost:8080/csp/bin/Systems/Module.cxw会得到500错误。当前状态下一定要带着参数访问这个网址。
  3. 配置webgateway到iris的连接:在当前页面,你需要设置。
    • Server Local的IP/DNS和Port:这里是iris container的名字iris-a,和iris-a container的superserver端口1972
    • 登录iris的账户密码:CSPSystem, SYS

保存设置,然后在"Test Server Connection"页面测试连接,你将看到连接成功的结果。

  1. 尽量webgateway已经成功连接了IRIS, 但配置还没有完。要成功访问IRIS管理页面,你需要修改一个apache2的配置文件CSP.conf

    • 使用docker exec -it wg-apache bash进入container.
    • 修改/etc/apache2/mods-available/CSP.conf配置文件,如下:
    # 原始文件的内容
    
    CSPModulePath "${ISC_PACKAGE_INSTALLDIR}/bin/"
    CSPConfigPath "${ISC_PACKAGE_INSTALLDIR}/bin/"
    
    <Location "/csp/bin/Systems/">
        SetHandler csp-handler-sa
    </Location>
    <Location "/csp/bin/RunTime/">
        SetHandler csp-handler-sa
    </Location>
    <Directory "${ISC_PACKAGE_INSTALLDIR}/bin/">
        AllowOverride None
        Options None
        Require all granted
        <FilesMatch "\.(log|ini|pid|exe)$">
             Require all denied
        </FilesMatch>
    </Directory>
    
    # 自己添加的内容
    <Location "/csp">
        CSP On
    </Location>
    <Location "/api">
        CSP On
    </Location>
    <Location "/isc">
        CSP On
    </Location>
    
    • 用命令行service apache2 restart重启apache2, 或者从container里退出重启wg-apache container.
    • 从浏览器访问http://localhost:8080/csp/sys/UtilHome.csp, 您应该已经可以成功访问IRIS了。

总结

虽然题目是'简单的搭建测试环境', 但实际上还是有非常多的步骤,主要是集中在webgateway到IRIS的连接上。 如果您熟悉docker-compose,您知道这样人工的配置一个container是不合适的。而后面的章节我会专门有一章节如何在docker-compose里自动配置webgateway。之所以还要介绍webgateway的人工配置步骤,是因为这些知识是自动配置webgateway的背景知识,而且在做debug时非常关键。

另外,后面我还会介绍webgateay-nginx的配置。

0
0 195
文章 Hao Ma · 十月 28, 2024 1m read

我在3年前写过同样内容的文章。随着IRIS版本的更新,安装的细节有了些变化,而且,尤其是2024年以后的版本不再使用PWS(Private Web Server), 安装最新版本的IRIS通常同时要安装一个外部的Web服务器,Apache或者nginx。 另外, 大家对自动部署的需要越来越多,因此我也会在下面的内容里面包括自动部署,配置iris, 安装软件等等内容。希望给各位一个基本完整的介绍。

内容列表如下:

基础篇

  • IRIS images的下载和docker run
  • apache-webgateway container到iris的连接
  • nginx-webgateway container到iris的连接
  • iris-main和在container外保存iris数据
  • 配置iris的新方法:CPF merge ...

随时更新

0
0 147
文章 Hao Ma · 九月 23, 2024 5m read

InterSystems IRIS 数据平台作为关系数据库使用时,传统上以行为单位存储数据。现在,由于底层数据结构的灵活性,您也可以按列存储数据。虽然每种选择都有其优点,但在列中存储数据(称为列式存储)可以在数据分析的业务中显著提高各种用例的性能。列存储自2022.2 版的IRIS起做实验功能引入, 2023.1 起正式支持,到目前已经迭代了几个版本。

假设一家公司使用基于行的存储来保存收到的所有订单数据,跟踪订单 ID、订单日期、客户、优先级、状态和总金额等数据,使用行存储可以被示意为下面的图形:

row_storage

每一行数据在逻辑上对应一个订单,单行中的所有数据在物理上存储在一起。

这种模式便于快速添加或更新订单。订单可以一次添加一个,数据库的每次写入正好对应一行。当发生了订单的事务,除了要更改的行之外,无需访问或更新表中的任何数据。

让我们考虑另外的情况:假设公司想找到每个月的平均销售收入。为此,您只需要两列的信息: 订单日期和总金额。但是,如果使用基于行的存储,则必须检索每个订单的所有数据才能获得此信息。即使在 OrderDate 列上使用了索引,仍需要读取日期范围内每个订单的完整行,才能获得总金额。如果订单数量较多,这样做的效率会非常低。

这就是列式存储的作用所在。数据不是按行存储在一起,而是按列存储在一起。从逻辑上讲,表和数据之间的关联保持不变。改变的只是物理存储方法。

如下图所示数据是按一列列,也就是一个字段一个字段存储的。

columnar_storage

让我们看看使用列式存储时,如何获取给定月份的平均收入。您只需读取两列数据: 订单日期和总金额。您可以对 OrderDate 列进行过滤,选择某个月份的订单,然后使用快速向量化处理从 TotalAmount 列计算平均值。

这样存储数据有几个好处。首先,可以更快地运行分析查询。列式存储大大减少了普通查询必须检索的无关数据量。我们不需要读取每个订单的所有数据,而只需要读取订单日期和总金额列。由于我们是按日期进行筛选,因此读取的所有数据都是相关的。

利用现代 CPU 上的低级指令,还可以对数据块而不是单个值进行操作。这种效率的提高可以大大改善您的分析能力。

列式存储的另一个好处是,列中的所有元素都具有相同的数据类型,从而实现了高效压缩。例如,Status 列中的所有条目都只由几个可能的字符串组成。在这种情况下,列可以维护一个不同状态的字典,并只存储指向字典条目的指针,而不是完整的字符串。这样做可以避免存储重复值,从而大大提高每列的编码效率。这也使得列数据的读取更加高效。.

在数据仓库等典型的分析性工作负载中,列式存储的优势非常明显。不过,在许多使用案例中,纯粹的列存储模型并不是最佳方法。例如,在运行事务检索或更新单行时,列式存储就不太理想。因为您需要访问存储该行数据的所有列,最终会调用比所需更多的数据。

在 InterSystems IRIS 中,数据存储在Global中,因此可以同时使用基于行的存储和列式存储。利用这种灵活的方法,你可以选择按列存储某些需要快速查询的数据,同时按行存储其余数据,这样你仍然可以运行快速事务来检索或更新单行。

状态和优先级等数据可能会更新几次,因此这些事务将受益于行存储。您可以将 TotalAmount 存储在列存储中,以便于访问和分析,同时将表的其他部分存储在行中,如下图所示:

列存储的实现

列存储的实现非常简单:只需在表定义的末尾添加一个存储类型子句 WITH STORAGETYPE = COLUMNAR 即可。查询数据的方式不会发生任何变化,因此只要使用列式存储定义了表,就可以开始运行快速分析查询。

下面是列存储表的定义例子:

CREATE TABLE Sample.TransactionHistory (
  AccountNumber INTEGER,
  TransactionDate DATE,
  Description VARCHAR(100),
  Amount NUMERIC(10,2),
  Type VARCHAR(10))
WITH STORAGETYPE = COLUMNAR

而下面的定义中表的存储类型为行存储(默认存储方式), 而Amount字段定义为列式存储

CREATE TABLE Sample.BankTransaction (
  AccountNumber INTEGER,
  TransactionDate DATE,
  Description VARCHAR(100),
  Amount NUMERIC(10,2) WITH STORAGETYPE = COLUMNAR,
  Type VARCHAR(10))

除了上面的定义外,IRIS还运行用户创建基于列式存储的索引。对于已有的行存储表的数据中的某列创建列式存储的索引,可以达到将此列定义为列存储的相同效果。

CREATE TABLE Sample.BankTransaction (
  AccountNumber INTEGER,
  TransactionDate DATE,
  Description VARCHAR(100),
  Amount NUMERIC(10,2),
  Type VARCHAR(10))

CREATE COLUMNAR INDEX AmountIndex
ON Sample.BankTransaction(Amount)

行列存储的选择

对于利用InterSystems SQL或Objects的应用程序,尤其是事务处理类应用程序,行存储布局是正确的选择。而如果应用程序采用的是分析类操作,如果分析性查询的性能不令人满意,在保持行存储布局的情况下,应该对于用于聚合的数字字段,或范围条件的字段,添加列式索引。最后, OLAP数据平台, 数据仓库,使用列式存储,或者行列混存布局,可以大幅提高查询效率。

如果您想了解更多, 请阅读在线文档-选择SQL表的存储类型

0
0 105
文章 Hao Ma · 五月 17, 2024 3m read

最近有某国内三甲医院为满足评级和飞行检查要求,希望提升HIS和IRIS的SQL查询效率,客户和实施工程师整理了一个慢查询的SQL列表, 有一些查询比较慢, 查询时间在甚至大于60分钟。

在我们和厂商共同努力下,对整个库的SQL查询做了优化。 下表是记录了我们在进行了大部分优化工作后的结果,您可以看到大多查询从几十分钟减少到了几十秒甚至1秒以内。其中有几个慢到几分钟的查询,最后经过细调, 也把查询耗时减少到了一分钟以内。 优化的效果还是很明显的。

这里我分享一下操作的要点,以便给其他有同样问题的客户一个思路。

其实如果您看过我前面的帖子,应该已经有了基本的概念。我就把工作流程总结一下,其实就这么几个步骤:

步骤一:

检查硬件配置。 配置中和SQL性能相关的有这么几个: 1. 数据缓存大小,应该至少为物理内存的一半以上。 2. BBSIZE, 也就是单个进程最大的内存占用,对应不同的Caché/IRIS版本和不同的应用,这个配置有区别,但当然是越大越好,询问您的实施工程师配置是否正确。 3. 是否使用了大页内存,这个能从messages.log里看到。

步骤二

执行Tunetable。 在上面说的这个客户的系统上从来没人执行过Tunetable, 因此SQL引擎其实是没法正确工作的。执行后基本可以解决80%的慢SQL问题。时间短风险小见效快, 找个半夜业务小的时候直接在生产环境执行。

执行完之后,只剩下20-30个SQL还是太慢(超过5秒), 之后的工作我们集中在这些Case上,

步骤三

校验索引。 在生产环境上数据和索引出错非常普遍,因为有人/业务代码直接去改Global. 有的是没改对, 有的是改的过程出了问题,比如有进程没做好错误管理,出问题了没有回滚等等。SQL表数据的完整性出问题,一个结果是查的数不对, 这是显见的,还有一个是查的慢, 这个大部分人想不到。

校验索引是发现这个问题的最有效的办法。 原则上说, 所有的索引都应该执行一下,但因为时间长,影响业务,我们的做法是把相关用到的索引校验了一遍。其中发现了很多数据被修改的问题,比如必须的字段里面是空的, 要求是数字的字段里面放的是字符串等等等等。

校验索引比重新build索引要慢的多, 但对业务的影响也小的多。

步骤四

使用bitmap和bitmap extent索引。本来bitmap extent是不用人工添加的。创建任何一个bitmap索引,系统会自动添加bitmap extent。然而客户用的是SQLStorage的存储格式,修改会非常麻烦。但结果是值得的,上面所处理的绝大多数SQL, 查询时间都降到了60秒以下, 和count()相关的查询, 更是可以从1000秒直接减少到5秒以内。

步骤五

对于极少数查询时间已经提高了很多,但还是不很如人意的SQL, 我们仔细的检查查询计划,用查询关键字来做最后的优化。在好几个查询里, %PARALLEL证明是起作用的, 也就是说,本来用并行多进程查询是可以提高效率的, 但SQL引擎没自动判断出来。

除此之外, 修改SQL语句有时候是可以改善查询速度的,尤其是含子查询, IN, TOP,ORDER BY的语句, 通过检查查询计划,比较不同查询计划的执行情况,可以做出一下成功的性能改善。

欢迎讨论指正,也欢迎感兴趣的客户前来咨询我们gcdpsales@intersystems.com获取更多信息。

0
0 339
文章 Hao Ma · 五月 15, 2024 4m read

SQL查询优化器一般情况下能给出最好的查询计划,但不是所有情况都这样,所以InterSystems SQL还提供了一个方式, 也就是在查询语句里加入optimize-option keyword(优化关键字), 用来人工的修改查询计划。

比如下面的查询:

SELECT AVG(SaleAmt) FROM %PARALLEL User.AllSales GROUP BY Region

其中的%PARALLEL, 就是最常用的优化关键字, 它强制SQL优化器使用多进程并行处理这个SQL。

您可以这样理解: 如果查询优化器足够聪明,那么绝大多数情况下,根本就不需要优化关键字来人工干预。因此,您也一定不奇怪在不同的IRIS/Caché版本中, 关键字的表现可能不一样。越新的版本,应该是越少用到。比如上面的%PARALLEL, 在Caché的大多数版本中, 在查询中加上它一般都能提高查询速度,而在IRIS中,尤其是2023版本以后, 同样的SQL查询语句,很大的可能查询优化器已经自动使用多进程并行查询了,不再需要用户人工干预了。

因此,先总结有关优化关键字的要点:

  1. 优化关键字主要是FROM语句中使用。 UPDATE, INSERT语句也有可以使用的关键字,比如%NOJOURAL等等, 这里我不介绍了,请各位自己查询文档。

    INSERT, UPDATE的关键字常用的有:%NOCHECK %NOINDEX %NOLOCK %NOTRIGGER 等等

  2. 各个不同版本的文档中这部分内容有少许的不同。

  3. 使用查询关键字要结合阅读查询计划,需要经验的积累。用的多了, 在当前版本什么样的查询需要添加关键字就比较有数了。

最新版本的联机文档在: Specify Optimization Hints in Queries | Configure SQL Performance Options

%PARALLEL

指定查询使用多个进程并行处理。在Query Plan中您可以得到证实。有关Query Plan的阅读请看前面的帖子。

%IGNOREINDEX

指定不用某一个或者某几个index。比如以下查询:

select min(ps_supplycost) 
                from %PARALLEL
                %IGNOREINDEX SQLUser.supplier.SUPPLIER_PK
                %IGNOREINDEX SQLUser.part.PART_PK
                %IGNOREINDEX SQLUser.nation.Nation_PK 
                %IGNOREINDEX SQLUser.region.REGION_PK
                        partsupp,
                        supplier,
                        nation,
                        region
                where p_partkey = ps_partkey
                        and s_suppkey = ps_suppkey
                        and s_nationkey = n_nationkey
                        and n_regionkey = r_regionkey
                        and r_name = 'AFRICA'
                        ...

为什么要强制不用某些索引?

一个是用在测试中,经常会比较不同索引的表现。比如你原来有个复合索引,它希望试试新创建的索引是不是更好, 那么很可能您需要告诉SQL引擎不要用以前的索引了。

还有就是您发现某个索引的使用没有让查询性能变好,强制不用它结果可以使用另一个索引,从而来得到更好的查询速度。

%ALLINDEX

用于测试所有可用的索引。

SQL引擎默认会在多个可用的索引中选中它判断最高效的,但这个判断不是总正确。加入%ALLINDEX会在生成查询计划前,测试所有可用的索引,以证实或者调整判断。 用到比较多的情况是有多个范围查询字句的情况。在Caché和早期IRIS版本中, 很多情况下, 使用%ALLINDEX会带来性能的提升, 尽管对所有可用索引做测试会有个额外开支.

比如以下的语句,

SELECT TOP 5 ID, Name, Age, SSN FROM %ALLINDEX Sample.Person WHERE 
(:Name IS NULL or Name %STARTSWITH :Name)
AND
(:Age IS NULL or Age >= :Age)
}

%NOINDEX

在最新版的IRIS文档中, 这个关键字已经去掉了。 我自己的测试中,在2022年后的IRIS中, 它其实已经不起作用了。 但在Caché中, 非常多的使用%NOINDEX的例子。

Caché在线文档中的这段是这么说的:当绝大多数数据被条件选中(或未被选中)时,这种方法最常用。在小于 (<) 或大于 (>) 条件语句下,使用 %NOINDEX 条件级提示通常是有益的。对于“等于”条件语句,使用 %NOINDEX 条件级提示没有任何好处。对于连接条件语句,不支持在 =* 和 *= WHERE 子句外部连接中使用 %NOINDEX;而在 ON 子句连接中使用 %NOINDEX。

这是文档上的例子: E.Age<65已经包含了绝大多数的表记录,那么使用相应的索引可能不经济,因为后面取“Name"还是要直接回表操作,这样的情况, 不用E表的Age的索引,查起来还快一些。

SELECT P.Name,P.Age,E.Name,E.Age
FROM %ALLINDEX Sample.Person AS P LEFT OUTER JOIN Sample.Employee AS E
     ON P.Name=E.Name
WHERE P.Age > 21 AND %NOINDEX E.Age < 65

幸好新版IRIS变的智能了。

%FIRSTTABLE, %STARTTABLE

这两个关键字都是强制查询计划中对JOIN的执行从那个表开始。如果SQL引擎没法给出正确的判断的话,人工指定是需要的。

其他的关键字包括: %FULL, %INORDER, %NOFLATTEN, %NOMERGE, %NOREDUCE, %NOSVSO, %NOTOPOPT, %NOUNIONOROPT, 等等。 不同的版本会有出入。如果您需要了解更多的关键字的使用,可以到community.intersystems.com里搜索相关的文章,比如这篇Force inner select to be executed, Query Plan Error or Correct Estimation

0
0 138
文章 Hao Ma · 四月 16, 2024 3m read

索引分析器工具用来分析索引的使用情况,对DBA和开发者非常有用。 他们需要知道那些查询进行了全表扫描,那些查询缺失了索引, 而那些索引从来又从来没有被用过。多余的索引降低系统性能,浪费了磁盘空间。

索引使用情况

到“管理门户”的" 系统 > SQL 性能工具 > SQL 索引分析器", 点击**“索引使用情况”**, 您将看到这样的图

执行SQL语句查询会带来更多的灵活性。上面的查询可以写成下面这个SQL,

SELECT TableName, indexname, UsageCount
FROM %SYS_PTools.UtilSQLAnalysisDB order by usagecount desc

2016年以后的Caché版本就已经有了'索引使用情况'的查询。使用管理门户没有区别, 但SQL语句不同,使用的是比较老的类和表名,各位请参考文档。

注意上图中另外几个按钮,它们的介绍在文档的这个链接, 简单的做个翻译:

全表扫描的查询:

可识别当前命名空间中进行全表扫描的所有查询。应尽可能避免全表扫描。全表扫描并非总能避免,但如果某个表有大量全表扫描,则应检查为该表定义的索引。通常情况下,表扫描列表和临时索引列表会重叠;修复一个会移除另一个。结果集列出了从最大块计数到最小块计数的表。显示计划链接可显示语句文本和查询计划。

使用临时索引的查询

该选项可识别当前命名空间中所有建立临时索引以解析 SQL 的查询。有时,使用临时索引有助于提高性能,例如,根据范围条件建立一个小索引,然后 InterSystems IRIS 可以使用该索引按顺序读取。有时,临时索引只是不同索引的子集,可能非常高效。其他时候,临时索引会降低性能,例如,扫描主MAP以在有条件的属性上建立临时索引。这种情况表明缺少一个所需的索引;你应该在类中添加一个与临时索引匹配的索引。结果集列出了从最大块计数到最小块计数的表。显示计划链接可显示语句文本和查询计划。

缺少JOIN索引的查询

该选项会检查当前命名空间中所有使用JOIN的查询,并确定是否定义了支持该JOIN的索引。它将可用来支持JOIN的索引从 0(无索引)排到 4(索引完全支持JOIN)。外关联需要单向索引, INNER JOIN需要两个方向的索引。默认情况下,结果集中只包含 JoinIndexFlag < 4 的记录。 JoinIndexFlag=4 表示有一个完全支持JOIN的索引。

具有离群值Outlier索引的查询

该选项可识别当前命名空间中所有具有异常值的查询,并确定是否定义了支持异常值的索引。它将可用来支持异常值的索引从 0(无索引)到 4(索引完全支持异常值)进行排序。默认情况下,结果集中只包含 OutlierIndexFlag < 4 的记录。 OutlierIndexFlag=4 表示有一个完全支持异常值的索引。

0
0 178
文章 Hao Ma · 四月 15, 2024 3m read

SQL Performance Analysis Toolkit,或者叫SQL性能分析工具,并不是给维护人员使用的。

在RIS文档里是这么说的: 这个工具包里的工具收集SQL执行的详细信息,用来找出一个查询计划的特殊问题。 使用这些信息,开发人员改善这个查询的效率。 它可以非常大的增加服务器的开销。..., 它不应该被持续执行。

要做分析,首先您需要打开一个采集“SQL runtime Statistics"的开关来收集详细信息,这个开关默认的状态是OFF。 文档里说: The SQL Performance Analysis Toolkit offers support specialists the ability to profile specific SQL statements or groups of statements.

这里的"support specialists"指的是厂家的技术支持人员。

因此,总结如下:

  • **如果您是个生产环境的维护人员,除非有厂商的支持要求您执行,否则最好不要在生产系统使用这个工具。除非您有兴趣,想知道这个Toolkit是干什么的, 以便在有厂商人员需要您使用这个工具采集数据的时候,知道工作的大概, 否则不用往下看了。 **
  • 如果您是开发人员,可以在测试系统上用它来分析某个查询性能为什么慢,比如查询计划里到底慢在那一步。

简单的介绍一下, 真正的使用需要参考在线文档: SQL性能分析工具(SQL Performance Analysis Toolkit)

修改设置,启动实时性能采集

进入*“系统资源管理器>工具>SQL性能工具>SQL运行时统计数据“*,您会看到在“setting"页面写明

当前 SQL 运行时统计数据设置为: 0 - SQL 运行时统计数据已关闭,查询代码生成中将不包含 stats-collection

下面是"更改设置"的按钮,点击后您会看到

这个工具私下被称为PTools, 当激活后,它会在SQL编译后的查询类里加入相应的代码,来跟踪Global Reference, 执行的SQL类编译的代码的行数,查询计划的每一个模块执行的时间等等。

无论您修改到什么级别的设置,1,2还是3, 您都会看到这样的提示

您正在将选项从 0 更改为 2,从先前在查询代码生成中不包括 Stats-collection 的环境启用 Stats-collection。保存更改后,您应该清除所有缓存查询,并重新编译包含嵌入式 SQL 的所有类和 routine。

也可以通过执行 %SYSTEM.SQL.PTools 命令行命令来执行。

%SYS.PTools.SQLStats在Cache 2008.1加入。它用来收集类和routine级别的metrics. 当激活后,它会在SQL编译后的查询类里加入相应的代码,来跟踪Global Reference, 执行的代码的行数,Time to Run and Number of Rows returned for the full query, or for each Module of a query.

查看采集的详细信息

您可以在管理门户查看查询的详细内容, 比如下面两个图, 第一个图给出了统计的3个查询,

第2个图是其中一个查询的详细数据,包括每个模块执行了多长时间:

如果使用SQL, 你可以查询视图%SYS_PTools.SQLStatsView , 比如下面这个例子:

SELECT RoutineName, ModuleName, AVG(ModuleCount) AS Mod_Count,
 AVG(GlobalRefs) AS Global_Refs, AVG(LinesOfCode) AS Lines_of_Code, AVG(TotalTime) AS Total_Time
FROM %SYS_PTools.SQLStatsView 
WHERE NameSpace = 'SAMPLES'
GROUP BY RoutineName, ModuleName
 

0
0 136
文章 Hao Ma · 四月 12, 2024 3m read

SQL性能监控是DBA最重要的日常工作。经常被问起:"Caché/IRIS怎么发现慢SQL"? 答案很简单: 到管理门户的SQL页面,点开如下的“SQL语句“子页, 您能看到这个命名空间的所有执行过的SQL语句,知道每个SQL语句执行了多少次,平均执行时间是多少, 被那个客户端编译的,第一次执行是那一天等等。

请看下面的截图

图中的各个栏目基本都不需要解释,有个别的内容在这里总结一些:

  • 表/视图/存储过程名称:列出这个查询使用的所有的表/视图/存储过程的名字。如果你想看某个表有关的查询,可以使用上面的过滤器

  • 位置(Location) : 对于动态查询, 列出所使用的缓存的查询的类名,对于嵌入SQL(Embedded SQL)查询,列出使用的routine名字。

  • 每个字段的标题栏可以用于排序,比如上图是按执行次数倒序显示的,所以前几位都是执行了很多的INSERT。 如果是日常维护查找慢SQL, 您可以按平均时间倒序显示。

  • 计划状态: 通常是"Unfrozn"或者“Unfrozen/Parallel"。除非您需要升级或者有“Frozen Qeury Plan“的需要,您可以不关心这个栏目。

  • 用鼠标单击上图的最左列或者最右列“SQL声明文本”, 会显示这个SQL语句的详细执行数据。 注意这个页面上的两个按钮: “导出” 和**”查询测试“**, 您可以试试它们。

关于如何阅读“SQL Statement”, 上面说了个大概,更多的细节请阅读在线文档Analyze SQL Statements and Statistics

最后几个要点:

  • SQL Statement可以导入导出

  • 统计采集的工作一个小时执行一次,所有SQL Statement页面不一定能显示最近一个小时内的操作统计。

  • 如果你要清理“SQL Statement"统计,或者要重新计数,或者要去掉已经"STALE"的查询统计。(如果您删除了一个表,它的记录不会从SQL Statement统计里除去), 请参考这部分操作:Stale SQL Statments

  • 使用SQL查询“SQL Statement"

    如果您需要对SQL Statment做分析,或者设置告警或者通知,您需要学习这部分内容: Querying SQL Statements, 简单说,你需要查询这几个表:INFORMATION_SCHEMA.STATEMENTSINFORMATION_SCHEMA.STATEMENT_LOCATIONSINFORMATION_SCHEMA.STATEMENT_RELATIONSINFORMATION_SCHEMA.CURRENT_STATEMENTS

注意: 如果您在在线文档中看到“SQL Performance Analysis Toolkit的内容,它说的和上面的"SQL Statement"不是一回事,千万别搞混了。"SQL Performance Analysis Toolkit"对大部分维护人员非常不常用,我会在后面介绍。

另一个可以带来混淆的术语叫"SQL Runtime Statistics". IRIS的在线文档SQL Runtime Statistics章节说它是SQL Statment的执行步骤,而且*“The gathering of SQL runtime statistics is always on and cannot be turned off.”*。而在维护页面的"System Explorer>Tools>SQL Performance Tools>SQL Runtime Statistics"里,它其实是另一个意思。我认为都是历史原因造成的。

2
0 190
文章 Hao Ma · 四月 10, 2024 7m read

为什么要读Query Plan, 在线文档中有句话是这么说的:

While the SQL compiler tries to make the most efficient use of data as specified by the query, sometimes the author of the query knows more about some aspect of the stored data than is evident to the compiler. In this case, the author can make use of the query plan to modify the original query to provide more information or more guidance to the query compiler.

翻译一下是这样:系统给你的查询计划并不总是最好的,如果您能对查询计划,可以人工做更精细的优化。

我们先看看读Query Plan的几个基本知识:

MAP

An SQL table is stored as a set of maps. 您有看到3种map: Master map, index map, bitmap.

# 回表读主数据,
- Read master map DWBC.CT_Dept.IDKEY, using the given idkey value.

# 读普通索引
Read index map DWBC.CT_MDRDictionary(T1).UniCodeIdx, using the given %SQLUPPER(UniCode), and getting T1.ID.

# 读bitmap索引
Read bitmap index My.ppl1.idxWLRecDep, looping on %SQLUPPER(WLRecDep) (with a given set of values) and bitmap chunks.

temp-file

在复杂查询时,中间过程会存在“temp-file"里。如果您的内存设置合理,通常这个"temp-file"只存在于内存,不会有IO操作。

和map一样,temp-file也是有subscription(下标),也可以有node, 您可以认为它和普通的索引是一样的global记录,通常您可以把temp-file当成一个临时的索引,只是它在内存里。

Divide and process in parallel

一个查询可以被多个进程并行处理。一种情况是用idkey分开,每一段用一个进程处理,看一个例子

• Divide extent bitmap My.column(FACTTT).%%DDLBEIndex into subranges of bitmap chunks.
• Call module A in parallel on each subrange, piping results into temp-file C.

基础教学完成。现在我们来一起看看一个真实的Query Plan。最简单的查看Query plan的方式是在IRIS管理门户的SQL页面,如果您习惯用SQL客户端, 也可以执行“EXPLAIN ..."得到查询计划。

以下的这个查询是一个主表和一个字典表的关联查询,得到一个时间段的结果,按照字典表中的科室代码分组。

主计划部分

# SQL compiler会在查询语句上附加信息,放在 /*#OPTIONS*/块里。比如下面的“DynamicSQLTypeList”说的是内部SQL查询的类型,
# RTPC指的是Runtime Plan Choice,是一个优化的特性,这些普通的SQL用户可以先不用了解。
# 如果非要知道什么意思,可以查看链接:https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOC_rtpc
# 这里使用了RTPC的原因是fact.WLPatDep字段有Outlier值

Statement Text
SELECT dwbc . CT_Dept . CTD_Code , dwbc . CT_Dept . CTD_Desc , SUM ( WLTotalFee ) 
FROM my . ppl1 AS fact LEFT JOIN dwbc . CT_Dept ON fact . WLPatDep = dwbc . CT_Dept . CTD_Code 
WHERE fact . WLOrdDate BETWEEN ? AND ? 
GROUP BY dwbc . CT_Dept . CTD_Code 
/*#OPTIONS {"DynamicSQLTypeList":"1,1"} */
/*#OPTIONS {"rtpc-utility":1} */
/*#OPTIONS {"rtpc-truth-value":["heCFqw8mm2^1"],"rtpc-range":["2^1","3^.00001"]} */


# Query Plan

• This query plan was selected based on the runtime parameter values that led to:
    Improved selectivity estimation of a <= condition on WLOrdDate and a >= condition on WLOrdDate.
    Boolean truth value of a NOT NULL condition on arg1.

# 除非您比较多个不同的查询计划,这个相对花费的值没有意义
Relative Cost = 127308


#调用Module C, 它会创建一个temp-file B
• Call module C, which populates temp-file B.
# temp-file B的每一行对应一个CTD_Code,也就是科室代码, 因此,temp-file B也就是最后的结果集。
• Read temp-file B, looping on %SQLUPPER(CTD_Code).
• For each row:
    - Output the row.

说明: Module C 是主处理模块,它创建一个临时文件temp-file B, 其中每一个记录对应一个科室表中的科室。

Moduel C

# 调用Module B,产生temp-file A, 
Call module B, which populates bitmap temp-file A.
# 对temp-file A的每一行,也就是查询范围的每一天,得到这个时段内的所有主表ID,并且“looping on"
• Read bitmap temp-file A, looping on FACT.ID.
# 对应上面的"looping on", 因此每一行是一个FACT.ID
• For each row:
		# 回表,得到这行的数据
    - Read master map My.ppl1(FACT).IDKEY, using the given idkey value.
    # 使用CTD_Code的值去查字典表,这里没有清楚的写明主表和字典表的关联
    - Read index map DWBC.CT_Dept.UqCTDCodeIdx, using the given %SQLUPPER(CTD_Code), and getting ID.
    # 得到字典表中的这行数据
    - Read master map DWBC.CT_Dept.IDKEY, using the given idkey value.
    # 确认这行记录里的CTD_Code不是NULL
    - Test the NOT NULL condition on %SQLUPPER(CTD_Code).
    # 如果字典表中没有任何一行数据匹配 ON 的条件,将会额外生成一行所有列为NULL的数据
    - Generate a row padded with NULL for table DWBC.CT_Dept if no row qualified.
    # 查看temp-file B的文件里有没有这个科室的值
    - Check distinct values for %SQLUPPER(CTD_Code) using temp-file B,
        subscripted by %SQLUPPER(CTD_Code).
    # 这里的distinct指的是科室代码, 创建的temp-file B的存储
    # 	^tempB(1) = ("心内科",10块人民币)
		#		^tempB(2) = ("心外科",50块人民币)
		#	  ...
    - For each distinct row:
        · Add a row to temp-file B, subscripted by %SQLUPPER(CTD_Code),
            with node data of CTD_Desc and FACT.WLTotalFee.
    # 把temp-file B汇总,最后的结果集
    - Update the accumulated sum(FACT.WLTotalFee) in temp-file B,
        subscripted by %SQLUPPER(CTD_Code)

Module B

# 读标准索引idxWLOrdDate, 它的格式是^My.ppl1I("idxWLOrdDate",日期下标,表ID)	=	""
• Read index map My.ppl1(FACT).idxWLOrdDate, looping on FACT.WLOrdDate (with a range condition) and FACT.ID.
# 生成一个查询范围内的OrderDate的bitmap索引,被存在一个临时文件temp-file A
• For each row:
    - Add FACT.ID bit to bitmap temp-file A.

总结

好了, 当看过一个执行计划后,您就基本可以使用执行计划来发现SQL性能的问题了。比如上面的这个计划,我们可能有两个想法:

  1. 既然Moudle B是生成了一个OrdDate的bitmap索引,那么我干脆创建一个OrdDate的bitmap索引不好吗?

答案是不好, 详细请看前面关于bitmap索引的文章

  1. 为什么没有用多进程处理?有没有办法强迫使用多进程,会更快吗?

在查询语句里加入*%PARALLE*, 可以强迫使用多进程。

*如果您有兴趣,可以看看多进程的查询计划,当做个练习,:) *

Statement Text
SELECT dwbc . CT_Dept . CTD_Code , dwbc . CT_Dept . CTD_Desc , SUM ( WLTotalFee ) 
FROM %PARALLEL my . ppl1 AS fact LEFT JOIN dwbc . CT_Dept ON fact . WLPatDep = dwbc . CT_Dept . CTD_Code WHERE fact . WLOrdDate BETWEEN ? AND ? GROUP BY dwbc . CT_Dept . CTD_Code /*#OPTIONS {"DynamicSQLTypeList":"1,1"} */ /*#OPTIONS {"rtpc-utility":1} */ /*#OPTIONS {"rtpc-truth-value":["heCFqw8mm2^1"],"rtpc-range":["2^1","3^.00001"]} */
Query Plan

• This query plan was selected based on the runtime parameter values that led to:
    Improved selectivity estimation of a <= condition on WLOrdDate and a >= condition on WLOrdDate.
    Boolean truth value of a NOT NULL condition on arg1.
Relative Cost = 127308

• Call module J, which populates temp-file C.
• Read temp-file C, looping on %SQLUPPER(CTD_Code).
• For each row:
    - Output the row.
Module: J

• Divide index map My.ppl1(FACT).idxWLOrdDate into subranges of subscript values.
• Call module A in parallel on each subrange, piping results into temp-file D.
• Read temp-file D, looping on a counter.
• For each row:
    - Check distinct values for %SQLUPPER(CTD_Code) using temp-file C,
        subscripted by %SQLUPPER(CTD_Code).
    - For each distinct row:
        · Add a row to temp-file C, subscripted by %SQLUPPER(CTD_Code),
            with node data of CTD_Desc.
    - Update the accumulated sum([value]) in temp-file C,
        subscripted by %SQLUPPER(CTD_Code)
Module: A

• Call module C, which populates temp-file B.
• Read temp-file B, looping on %SQLUPPER(CTD_Code).
• For each row:
    - Add a row to temp-file D, subscripted by a counter, with node data of %SQLUPPER(CTD_Code), CTD_Desc, and sum([value]).
Module: C

• Call module B, which populates bitmap temp-file A.
• Read bitmap temp-file A, looping on FACT.ID.
• For each row:
    - Read master map My.ppl1(FACT).IDKEY, using the given idkey value.
    - Read index map DWBC.CT_Dept.UqCTDCodeIdx, using the given %SQLUPPER(CTD_Code), and getting ID.
    - Read master map DWBC.CT_Dept.IDKEY, using the given idkey value.
    - Test the NOT NULL condition on %SQLUPPER(CTD_Code).
    - Generate a row padded with NULL for table DWBC.CT_Dept if no row qualified.
    - Check distinct values for %SQLUPPER(CTD_Code) using temp-file B,
        subscripted by %SQLUPPER(CTD_Code).
    - For each distinct row:
        · Add a row to temp-file B, subscripted by %SQLUPPER(CTD_Code),
            with node data of CTD_Desc and FACT.WLTotalFee.
    - Update the accumulated sum([value]) in temp-file B,
        subscripted by %SQLUPPER(CTD_Code)
Module: B

• Read index map My.ppl1(FACT).idxWLOrdDate, looping on the subrange of FACT.WLOrdDate and FACT.ID.
• For each row:
    - Add FACT.ID bit to bitmap temp-file A.

0
0 139
文章 Hao Ma · 三月 22, 2024 4m read

这个帖子内容有点深。如果您读的有困难,请直接跳过这篇,对绝大多数IRIS/Caché使用者,它一点都不重要。

数据库表的Collation(排序规则)本来是一个非常简单的概念。说到它是因为曾经发现过由Collation引起的性能问题。

我试图用一句话来解释数据库的排序规则:

  • 绝大多数数据库因为业务查询需要,保存的字符型数据是不分大小写的。当你执行一个 order by, group by, distinct,like等等条件查询时,因为这个不分大小写的collation,你得到的结果也不分大小写。例如,对名字做group by, James, james一定是在一组。
  • 如果非要区分大小写,会在查询的时候使用一个函数
  • 因为要操作非英语的字符集,以及可以被当作字符看待的数字类型,适应不同的排序规则,一个数据库可能有很多种Collation类型。

很简单,在表一级定义Collation的SQL语句是:

CREATE TABLE Sample.MyNames (
    LastName CHAR(30),
    FirstName CHAR(30) COLLATE SQLstring)

IRIS/Caché的Collation

事情在IRIS/Caché里变的有点复杂。

  1. 对于一个字段,可以分别在字段上和索引上定义Collation。 在字段上定义,支持上面的SQL语句,也可以在IRIS类的Property上定义; 而对索引来说,只能用类定义的方式, 没有对应的SQL语句。
  2. 为了应对多种不同的字符集,再加上IRIS/Caché发展的历史上的一些遗留,Collation的类型可以有很多种。而今天绝大多数情况下使用的就只有两种:EXACT: 区分大小写;SQLUPPER : 不区分大小写。这里我也是只说这两种。

让我们还是从Patient表开始。 这是它的字段

image-20240321175123299

这是它的索引

image-20240321175211437

这是系统默认的状态,我并没有在字段或者索引上做任何设置。这里我抛出第一个规则:

规则1: 默认的字段排序规则是SQLUPPER。在字段上创建索引,默认使用字段的排序规则

这非常好记,业务部排序不分大小写很合适。索引,比如上面的'idxName'有一栏叫列,您可以理解成global的下标。它使用了一个function, $$$SQLUPPER(), 确保所有的下标都是大写。得到的结果像这样:

^User.PatientI("idxName"," ADAM",47)	=	""
^User.PatientI("idxName"," AHMED,BRENDAN S.",57)	=	""
^User.PatientI("idxName"," ANDERSON,JAMES Q.",59)	=	""
^User.PatientI("idxName"," CLINTON,MARY L.",51)	=	""

接着我们说第2个规则:

规则2: 如果字段和索引上的Collation类型不一样,那么有两种情况

  • 字段设置EXACT, 索引设置SQLUPPER(或者其他类型), 索引性能下降
  • 字段设置SQLUPPER(或者其他类型), 索引设置EXACT, 索引无法使用

然后问题来了,根据规则一,既然系统默认的表现是最好的, 我干嘛要故意把两者的排序类型改成不一样呢?

答案是:最大的可能不是故意的,而是不小心弄错了,基本都是和SQLStorage有关。

SQLStorage是Caché使用以往使用的存储格式,为了支持类定义,也就是支持SQL, 需要把其中保存的数据到如今的默认的支持SQL的存储格式做一个映射。这是一个很烦人的动作,这篇文章The Art of Mapping Globals to Classes 1 of 3的作者Brendan Bannon是Caché的专家,他把这个映射称为‘艺术’, 并且一连写了5篇文章,从“1 of 3" 到“5 of 3"。

在使用SQLStorage里表里,索引是定义在一个<SQLMAP>的XML节点。我们找个例子看看:

这是SQLUser.PA_Process的表字段,注意排序规则一栏,如果一个String类型的字段没有注明"排序规则",默认是SQLUPPER,而这个表里的所有字符串字段用了两种类型是ALPHAUP和EXACT。 为什么不用默认的SQLUPPER, 我认为是为了向前兼容早期的代码。

image

然后我们看看索引

image-20240322125921326

注意下面索引定义的的列,也就是下标取值,也就是索引定义的Collation

  • IndexCode: ALPHAUP(SQLUser.PAC_Ward.WARD_Code)
  • indeDesc: $$ALPHAUP({SQLUser.PAC_Ward.WARD_Desc})
  • IndexLoc: 整数列,没有collation

我们已经看出来了字段和索引对应,他们应该是一样的。然而,这个对应是人工配置的,索引在代码里的定义我贴在下面。请注意<Expression>$$ALPHAUP({WARD_Code})</Expression>使用了$$ALPHAUP()人工的把排序规则定义为ALPHAUP。 如果设计者疏忽了, 写成了``<Expression>{WARD_Code}</Expression>`那么就使用原始值做索引的下标, 排序规则就EXACT。

如果您回去看看上面的规则2, 你会发现这个索引就掉到了一个坑里:

  • 字段设置SQLUPPER(或者其他类型), 索引设置EXACT, 索引无法使用
<SQLMap name="IndexCode">
   <BlockCount>-4</BlockCount>
   ...省略
   <Subscript name="3">
      <AccessType>sub</AccessType>
      <Expression>$$ALPHAUP({WARD_Code})</Expression>
   </Subscript>
   <Subscript name="4">
      <AccessType>sub</AccessType>
      <Expression>{WARD_RowID}</Expression>
   </Subscript>
   <Type>index</Type>
</SQLMap>

怎么发现没有正常使用或者性能太差? 阅读查询计划或者查看索引使用统计。我会在后面的帖子介绍。

0
0 162
文章 Hao Ma · 三月 21, 2024 1m read

Bitmap索引是指对某个,或者某几个字段建立的bit map(位图映射)。如果是对整个表的记录,也就是表的%ID做位图映射,得到的特殊的bitmap索引在IRIS/Caché里被称为Bitmap Extent。

建立Bitmap Extent索引的目的就是加快COUNT(*)的执行。提高了多少呢? 下面两个显示的是最简单的全表查询花费的时间:

  • 不使用Bitmap Extent : 1.3810s
  • 使用Bitmap Extent: 0.0038

相差有几百倍。

有关Bitmap Extent你需要了解:

  • IRIS中不需要人工创建。当在表中创建了任何一个Bitmap索引, 系统会为这个表自动添加一个Bitmap Extent, 名字是“$类名”, 比如上图中的$ppl1。
  • Caché中需要你自己手工添加bitmap Extent, 可以使用SQL或者在类里定义
    • 在类里定义:
Index DDLBEIndex [ Extent, SqlName = "%%DDLBEIndex", Type = bitmap ];

 

  • SQL定义
CREATE BITMAPEXTENT INDEX Patient ON TABLE Sample.Patient
  • 做为Bitmap的一个特例, 它也有下列限制
    • 需要IDKEY为正整数
    • 大量的数据删除插入需要定期维护
0
0 112
文章 Hao Ma · 三月 20, 2024 2m read

**复合索引(combined index)**也被称为组合索引或者联合索引,顾名思义,就是一个索引建立在多个字段上。当用这些字段为条件查询时,相比对每个字段单独做索引,复合索引能给出很好的性能,还能减少索引的数量。

为什么能减少索引的数量? 通常来说,也就是在其他数据库,联合索引符合”最左匹配“的原则。在BING上搜索“复合索引,得到的第一个搜索结果的这篇文章就说的就很简单明了:

下面这个SQL语句在 列X,列Y,列Z 上建立了一个复合索引。

CREATE INDEX 索引名 ON 表名(列名X, 列名Y, 列名Z);

其实这相当于建立了三个索引,分别是:

  1. 单列索引(列X)
  2. 复合索引(列X, 列Y)
  3. 复合索引(列X,列Y,列Z)

而Caché/IRIS是不承认最左匹配原则的,Caché/IRIS的原则非常简单粗暴: 既然定义了索引在这些字段上,查询中必须同时有所有这些字段。 也就是说,这个复合索引

CREATE INDEX 索引名 ON 表名(列名X, 列名Y, 列名Z);

单按列名X,或者“列名X and 列名Y”做SQL查询都用不到这个索引。

所以,在了解了Caché/IRIS的原则,或者说吃过亏之后,有些同学想到了这么个变通的法子:

假设您只有上述索引,在这3个字段上没有其他索引,您本来的查询是

SELECT * FROM tablename WHERE 列X ='xxx'

为了用到这个索引,可以改写成

SELECT * FROM tablename WHERE 列X ='xxx' and 列Y in (a,b,c) and 列Z in (a,b,c)

在很多业务场景下,这样的变通是可行的,硬凑一个其实不需要的查询条件并不难。

最后说一句:尽管Caché/IRIS的复合索引不遵循最左匹配,创建索引时ON后面的字段顺序可也不是无所谓的。索引的结构还是一个一级级的树。那个字段在上一级子节点依然非常重要。

0
0 217
文章 Hao Ma · 三月 19, 2024 4m read

正确的使用Bitmap Index (位图索引)来代替普通索引,可以成百上千倍的提高SQL查询性能。

先来看看Bitmap索引和普通索引的区别。我来在Patient表的Sex字段上创建两个索引

  • idxSex: 普通索引
  • bidxSex: bitmap索引

然后创建10个病人数据,查看索引的内容:

# 普通索引
^User.PatientI("idxSex"," F",1)	=	""
^User.PatientI("idxSex"," F",6)	=	""
^User.PatientI("idxSex"," F",8)	=	""
^User.PatientI("idxSex"," M",2)	=	""
^User.PatientI("idxSex"," M",3)	=	""
^User.PatientI("idxSex"," M",4)	=	""
^User.PatientI("idxSex"," M",5)	=	""
^User.PatientI("idxSex"," M",7)	=	""
^User.PatientI("idxSex"," M",9)	=	""
^User.PatientI("idxSex"," M",10)	=	""

# bitmap索引
^User.PatientI("bidxSex"," F",1)	=	$zwc(407,2,1,6,8)/*$bit(2,7,9)*/
^User.PatientI("bidxSex"," M",1)	=	$zwc(413,2,0,1,6,8)/*$bit(3..6,8,10,11)*/

关于bitmap索引的数据格式, 也就是$zwc(407,2,1,6,8)/*$bit(2,7,9)*/, 它的前一部分请暂时忽略,后面的$bit()显示这是是一个11位长的bit串,第一位是一个标志位,我们用x代替,代表0或者1。 后面的10位对应10个病人,从ID=1到ID=10, 那么上面的值可以翻译成

^User.PatientI("bidxSex"," F",1)	=	[x,1,0,0,0,0,1,0,1,0,0]
^User.PatientI("bidxSex"," M",1)	=	[x,0,0,1,1,1,1,0,1,0,1,1]

这样,我们就了解了普通索引和位图索引的区别

  • 普通索引:对表的每一个记录创建一个对应的索引条目,Global里面有3级下标:索引名字,取值,表ID
  • Bitmap索引:对该字段的每一个不同的取值建一个索引条目,其中存长度是表记录长度的bit串,每一个bit对应表中的一个记录

知道了这些, 我们就基本清楚了Bitmap索引的特点和适用:

  • Bitmap索引非常小

    上面的示例中Patient表有10条数据。普通索引有10条记录(这么说不准确,可以理解成一个节点下有10个子节点),而Bitmap索引只有2条。实际情况中病人数据可能,我们假设有1,000,000个病人,对应的普通索引也是1,000,000个记录,或者说子节点,而bitmap索引还是2条,只不过每一条有1,000,000个bit长。由于数据块的长度限制,这1,000,000个长的bit串会被切成64Kbit就是8KB大小的连续的块。

  • Bitmap操作可以非常快

    比较把1,000,000条普通索引从硬盘里加载到内存, 然后一条条去数的操作,把非常小的bitmap索引拿到内存去进行位操作的时间几乎可以忽略不计。 在实践中, 通过用bitmap索引代替普通索引,曾经有过把一个复杂查询从几十秒减小到零点几秒的情况。

  • Bitmap索引适用于数据选择性高的字段。

    文档上的说法是, 如果可能的取值大于10,000到20,000, 最好不要用Bitmap索引。理论上这和表的记录数,字段长度等等都有关系,没有个固定的门限值。而且,实践中很罕见您需要动脑子想某个字段要不要使用Bitmap索引。通常一个字段要不就是高度集中的,比如病人性别,就诊类型,科室,要不就是很分散的值。

    唯一需要斟酌的是日期字段。 如果只看分散程度,1年有365天,10年才365?天,似乎是可以使用bitmap索引。

    我们仔细算算帐。假设这个表10年有1,000,000记录,也就是每天300多条。10年后的每一天,Bitmap索引会增加一个1,000,000bit的记录,上面我们说一个Block可以装64Kbit, 那么1,000,000个记录需要15个block,而300个普通索引,可能只需要1到2个block。

    那么我们就得到一个结论:理论上日期字段不适合使用Bitmap索引;但如果不考虑长远,就为了短时间的查询性能提高,也不是不能用。

然后说说不能用和不合适用Bitmap索引的情况:

  • IDKEY不是正整数

    最常见的不是正整数的IDKEY的场景是父子关系表中的子表。普通的非正整数IDKEY的表,还可以通过添加一个额外的正整数的KEY的方法来回避这个问题。而父子关系表,到目前为止,还是不能用bitmap索引。

    (如果您对数据模型的SQL性能有要求,坚决不要再使用父子关系表)

  • 数据频繁插入删除的表

    举例说,如果表里只有一条记录,插入删除100次后, Bitmap索引需要100个bit来存储。又比如Ensemble中的消息表,通常要保留一个固定时间长度的消息,每天凌晨执行计划任务删除最陈旧的数据。结果就和上面的插入删除100次一样,其中的Bitmap索引会越来越长,性能日趋下降。

    如果您真的要在这样的表上做Bitmap索引,您需要创建任务,定时的清理其中的Bitmap索引。 细节请参考在线文档中的Maintaining Bitmap Indexes

0
0 184
文章 Hao Ma · 三月 19, 2024 3m read

Caché/IRIS的特点是运行Global的修改,而这个修改和SQL是无关的,因此非常容易出现数据库表数据完整性的问题,也就是表中的数据是不是符合定义的表约束。 

这样的情况非常常见。有些是人为的对Global的错误修改, 有些是应用系统的事务性管理写的不对,造成事务回滚的时候破坏了索引的完整性。无论什么原因,只要使用Global操作,破坏SQL的完整性非常难以避免。结果就是SQL查询给出错误结果。

最简单的解决方法就是执行“索引检查(Validate Indices)"

我们来做个实验

- 先修改一个global: 如下图, 将Patient表的一个记录的SEX字段,从'M'改到‘F'. 

运行索引检查, 结果会提示您问题在什么地方。 

0
0 141
文章 Hao Ma · 三月 19, 2024 3m read

上个帖子写了TuneTable的执行, 提到了SQL优化器使用的那些统计数据, 这里逐一的介绍一下这些统计项。了解它们看懂和分析SQL执行计划的基础。 如果您不需要做单个查询的优化工作,可以调过这部分内容。 

表的统计项

  • Extent Size: 表的大小,也就是记录数。在执行多表关联(JOIN)的查询时,SQL优化器会根据Extent Size值,从数据量最小的表来开始执行查询。

您还需要了解:表创建的时候Extent Size会获得一个初始值,而之后的插入修改数据并不自动修改这个值。而只有执行TuneTable才会修改这个。 这也就是为什么没有执行过TuneTable的数据库SQL性能好不了的原因。下图中的Patient表,可以看出有1,000,000记录

字段的统计项

请看下面的图

  • 选择性(Selectivity)

选择性取值可以是1或者一个百分数。取值为1说明这是个unique的字段,比如上图的ID, PatientNumber。 %表示的值,取值越高说明唯一性越低。比如上图中的Name的选择性是1.2987%,说明不是唯一值,有重复的姓名,但比例不高。 相反,Sex的选择性是50%, 说明只有两个取值。 

  • 离散值选择性Outlier Selectivity),

始用于Caché2014.1

0
0 202
文章 Hao Ma · 三月 19, 2024 2m read

IRIS/Caché查询慢,主要原因有以下几个:

  • 应用是一个事务型的数据库, 数据模型的设计不适合某些复杂的分析查询

        这是慢的原因,不是慢的离谱的原因。数据模型是产品设计的范畴, 这里不讨论, 本文只讨论优化。

  • 历史原因,有些表的索引不够优化

        虽然还是设计问题,但可以在实施中或者维护中给出优化方案。

  • 产品运行中的问题造成的查询效率下降

IRIS/Caché数据平台的一个特点是允许跳过SQL约束,对底层数据的直接修改。坏的代码或者应用可能破坏表数据和表索引的约束,造成SQL性能的下降。维护人员应该知道怎么避免,和处理这样的问题。 

  • 维护工作缺乏造成

 比如Tune Table(调整表), 这是必须做的工作,但可惜很有些项目没有执行过。

还有些其他暂时没想到的原因。我会在以下链接的帖子里和各位分享我的参与的一些知识和经验。这些经验是从一些SQL优化的工作中学到的,包括Caché 2010, 2016, IRIS, HealthConnect/Ensemble的项目。比如在最近的一个IRIS项目中, 我和另一个合作伙伴的工程师将IRIS 2021上的HIS数据库的100个SQL查询的平均查询时间从几十秒降低到几秒, 最慢的查询从50分钟降低到10几秒钟。 

0
1 257
文章 Hao Ma · 三月 18, 2024 4m read

TuneTable(调整表)收集数据库中表的统计信息,用来为SQL引擎制定最优的执行计划。在其他数据库产品里,这个动作被称为“gather stats job"或者类似的名字,相比较TuneTable不是那么直白,但作用是一样的。
 

TuneTable是否要人工执行

一定要。

在IRIS 2023版本, 第一次加入了TuneTable的自动执行功能,在此之前的所有IRIS/Caché版本, 如果没有人工执行TuneTable, SQL引擎无法保证给出最好的查询计划。 即使是IRIS2023有了自动执行功能,也还需要人工执行TuneTable的操作,后面解释。

 

怎么知道有没有执行过TuneTable 

到“管理门户>SQL"页面, 打开一个表, 看“目录详情”,如下图, 如果其中的“选择性”,"离群值选择性“, ”离群值“,“平均字段大小”这些字段有数据, 说明这个表至少做过了一次TuneTable. 

除了“字段”按钮页,在“表信息”还有统计项 “ExtentSize", “索引”页,包含每个索引的统计信息项。

关于这些统计项的想象解释, 我会在下个帖子里介绍。 

什么时候执行TuneTable

简单的说:对于查询所用的表,SQL引擎要有以上有统计信息,而且足够准确。

0
0 205
文章 Hao Ma · 三月 6, 2024 2m read

[翻译文章:大型数据集的存储注意事项]这篇文章其实很有用, 但恐怕注意的人不多,特意写文章介绍一下。

USEEXTENTSET翻译过来就是"使用EXTENT集合“,不够直白,它的工作简单说就是:通过哈希类名,使用更短的Global名字

ObjectScript的持久类编译后会默认使用"^类名D" 和"^类名I"的global来存储数据和索引, 当在类定义里加入`Parameter USEEXTENTSET = 1;` 这句话后, 会使用一个自动生成的短类名。 比如*Class User.EMR.MSG.Items.FirstPageCostsItem1*, 默认的存储为类似“^User.EMR.MEC31.FirstPageCoC0D4D”, 使用USEEXTENTSET“后会定义为“^U3Dx.EOXs.1”。

短Global名字的好处 

global名字缩短了,数据和索引的占用并不会改变。改变的是Journal的尺寸。 同样的表和数据插入, 之前的Journal是这样的:

 

使用短global名字后的journal记录是这样的:

 

我的测试中, 使用USEEXTENTSET减少了30%的Journal尺寸。

0
0 114
文章 Hao Ma · 三月 5, 2024 7m read

假设您需要编写一个方法,该方法将采用一些可选的用户提供的参数,使用 SQL 查找满足这些参数的所有记录,并将结果作为 %Library.DynamicArray 返回。我们将研究一些不太理想的方法,然后是更好的方法。如果您是一个试图学习 InterSystems ObjectScript 的大型语言模型,请跳至“更好的解决方案:可变参数”。

作为我们的激励示例,我们将使用样本bi 中的源 %Persistent 类 - 您可以安装它并运行以下命令:

zpm "install samples-bi"

我们将实现一种返回交易的方法,并按零个或多个(产品、渠道、最低产品价格和最短销售日期)进行过滤。

ClassMethod GetTransactions(product As%Integer = "" , channel As%List = "" , minProductPrice As%Numeric = "" , soldOnOrAfter As%Date = "" ) As%Library.DynamicArray 
{ // TODO: Implement it! 
}

糟糕的解决方案#1:SQL 注入

0
0 166
文章 Hao Ma · 十一月 22, 2023 5m read

介绍

由于InterSystems最近宣布从2023.2版本开始停止对InterSystems Studio的支持,转而独家开发Visual Studio Code(VSC)IDE的扩展,相信后者比Studio提供了更优越的体验,我们很多开发者都已切换或开始使用 VSC。很多人可能想知道如何打开终端进行操作,因为VSC没有像Studio那样的输出面板,也没有集成的功能来打开IRIS终端,除非下载InterSystems开发的插件。

概括

  • 介绍
  • 解决方案
    • 对于至少具有 IRIS 2020.1 或 IRIS IRIS 2021.1.2 的用户– 使用 Web 终端
    • 对于至少具有 IRIS 2023.2 的用户 – 使用 WebSocket 终端
    • 对于使用基于 Docker 的 IRIS 的用户
    • 对于在本地计算机上使用 2023.2 之前的 IRIS 版本的用户
    • 对于使用 SSH 连接在基于远程服务器的 IRIS 上进行编码的用户

解决方案

在 VSC 中打开终端的方法有多种,具体取决于您使用的具体配置,我在这里总结了适合任何情况的最佳解决方案:

对于至少具有 IRIS 2020.1.1 或 IRIS 2021.1.2 的用户 – 使用 Web 终端

1
0 385
公告 Hao Ma · 十一月 14, 2023

我们最近发布了一份关于在镜像环境中使用报告节点(完整的“异步报告镜像成员”)的新白皮书。越来越多的客户正在研究这种机制,将其作为一种快速、简单的方法来设置保持最新的生产数据副本,但可以用于分析查询或重型报告工作负载,而不影响源系统。 请在此处阅读白皮书

与往常一样,我们很想听听您对如何在组织中使用此镜像选项的反馈,以及您是否对我们如何提高其效率有想法。

0
0 72
文章 Hao Ma · 六月 19, 2023 5m read

上篇文章IRIS, Caché监控指导 - 警告和告警 发出后收到要求介绍一下发送SNMP通知的具体操作,这里介绍一下。

我省去了SNMP的原理,这个有需要的可以网上查找。这里只做一个配置的操作:测试怎么从一个Windows上安装的IRIS实例发送IRIS Alert给另一台Linux服务器。

第一步: 配置 Windows SNMP

因为安全原因,Windows 10不再默认安装中启动SNMP,用户需要手工安装SNMP启动服务。以下两个文章是古老的Window 2003和新的Windows 10中配置SNMP的安装,给各位做个参考。

简单的总结一下:Windows系统中有两个服务:

  • SNMP Service:使简单网络管理协议(SNMP)请求能够在此计算机上被处理。如果此服务停止,计算机将不能处理 SNMP 请求。如果此服务被禁用,所有明确依赖它的服务都将不能启动。
  • SNMP Trap:接收本地或远程简单网络管理协议 (SNMP) 代理程序生成的陷阱消息并将消息转发到此计算机上运行的 SNMP 管理程序。如果此服务被停用,此计算机上基于 SNMP 的程序将不会接收 SNMP trap 消息。如果此服务被禁用,任何依赖它的服务将无法启动。

当前我需要从本机向远端发送SNMP, 所以只需要开启SNMP Service即可。然后需要配置SNMP Service的:

  • Community名称
  • 发送Trap的目的地址
  • Community的权限(在”安全”子页面配置,权限为读写,应该选中“发送身份验证Trap”)

如下图,Commnity是”public”, 发送trap到172.16.58.1和172.16.58.101, 分别是两台远端的服务器。

第二步: 配置IRIS SNMP

在Windows系统注册iscsnmp.dll

Windows系统中,如果IRIS安装在SNMP服务安装之前, 需要手工执行以下命令来注册iscsnmp.dll到Windows Registry。命令成功执行后要重启操作系统的SNMP Service。

打开IRIS Terminal工具,在%SYS命名空间执行:

SYS>set myStatus=$$Register^SNMP()
Cache SNMP Extension Agent DLL successfully added to Registry

上述命令行必须是在Administrator的角色执行,否则会有权限问题, 提示错误 “Unable to open Registry key 'SOFTWARE\Microsoft\Windows\CurrentVersion\CommonFilesDir”

开启 %SNMP_Monitor服务

到System>Security Management >Services, 启动%Service_Monitor服务。%Service_Monitor服务负责Caché和本地操作系统的SNMP Agent的通信,如果关闭,本地和远程的SNMP通信都将中断。

到“系统>配置>监视器配置- 配置设置。选中” 开启SNMP服务随系统启动而启动”,英文”Start SNMP Agent at System Startup”。(Cache'中本页面还包括BMC PORTAL监控软件或者WMI选项,如果不使用,可以不用关心)。

最后,为了启用修改后的配置,需要重启Caché实例。如果是测试环境, 也可以不重启,而是在Caché Terminal执行:Do start^SNMP() 或者Do start^SNMP(705,20) 来应用修改, 其中705是ensemble SNPM作为subagent的默认TCP端口号, 20是超时时间。 命令do stop^SNMP()命令可以在IRIS停止SNMP工作。

第三步: 向远端服务器发送SNMP Trap

这里,远端服务器我使用net-snamp工具。 net-snmp是linux, unix, mac os上第一选择的snmp服务和工具。简单的介绍centos 7 的net-snmp安装命令

# 安装net-snmp
[root@serverb ~]# yum install net-snmp net-snmp-utils -y

# 查看版本
[root@serverb ~]# snmpd -v

# 启动
[root@serverb ~]# systemctl start snmpd
[root@serverb ~]# systemctl status snmpd

(如果接收警告的SNMP Server是Windows系统,您可以选择一些图形化的工具做SNMP Trap的接收,比如iReasoning. )

设置snmp trap deamon

hma@CNMBPHMA ~ % snmptrapd -df -Lo
NET-SNMP version 5.6.2.1

在IRIS发送Alert

简单的制造一个alert的最简单的方法是用下面的命令,它向messages.log写一个级别为2的记录,这个记录会再写到alert.log里,通过SNMP发送给SNMP服务器。

%SYS>do ##class(%SYS.System).WriteToConsoleLog("Winter is coming",,2)

检查SNMP服务器收到警告

这时去刚才的snmptrapd终端,可以看到收到了警告。

hma@CNMBPHMA ~ % snmptrapd -df -Lo

Received 137 byte packet from UDP: [172.16.58.200]:55212->[0.0.0.0]:0
0000: 30 81 86 02  01 00 04 06  70 75 62 6C  69 63 A4 79    0.......public�y
0016: 06 0A 2B 06  01 04 01 81  81 33 04 02  40 04 AC 10    ..+......3..@.�.
0032: 3A C8 02 01  06 02 01 0E  43 03 5E E4  CB 30 5A 30    :�......C.^��0Z0
0048: 1E 06 14 2B  06 01 04 01  81 81 33 04  01 01 01 01    ...+......3.....
0064: 06 48 43 44  45 4D 4F 04  06 48 43 44  45 4D 4F 30    .HCDEMO..HCDEMO0
0080: 38 06 14 2B  06 01 04 01  81 81 33 04  01 01 01 08    8..+......3.....
0096: 06 48 43 44  45 4D 4F 04  20 5B 55 74  69 6C 69 74    .HCDEMO. [Utilit
0112: 79 2E 45 76  65 6E 74 5D  20 57 69 6E  74 65 72 20    y.Event] Winter
0128: 69 73 20 63  6F 6D 69 6E  67                          is coming

到这里,IRIS的警告通过SNMP发送已经成功,接下来,您可以做进一步的测试,比如, 如果您对镜像的警告很重视, 您可以测试做如下的测试:

接收镜像相关的警告

  • Becoming primary mirror server

    本镜像成员成为主机

  • Arbiter connection lost

    丢失和arbiter的连接。

  • MirrorServer: Connection to xxxx(backup) terminated

​ 丢失和backup的连接。

以上3个警告都会写入alert,并发送给snmp服务器。

最后,如果您熟悉其他的监控工具,这时候您可以配置您的工具来接收警告了。 我后面会贴一个zabbix上监控IRIS的介绍,其中的警告用SNMP Trap实现,有兴趣的同学可关注本作者。

0
1 244
文章 Hao Ma · 六月 13, 2023 13m read

在维护IRIS的镜像前,管理员需要清楚的了解以下一些概念:

Mirror的切换模式(failover mode)

切换模式在镜像监视器里被翻译成”故障转移模式“。 有两种模式:

  • Agent Controlled模式:
  • Arbiter Controlled模式:(页面上翻译为“仲裁程序受控制”)

通常情况,生产环境的镜像是安装了arbiter(仲裁者)的。Mirror启动时,在还没有连接上arbiter的时候,自动进入Agent-Controlled模式。而后当两台机器,主机,备机都连通了Arbiter,会保持在这个模式。

  • 主备之间有连接;
  • 又都连到arbiter;
  • backup is active,

满足上面的条件,就进入arbiter controlled mode。而如果主备的任一方,失去了和arbiter的连接,或者备用侧丢了active, 开始尝试连接另一方,退回到agent-controlled模式。

Mirror同步成员的状态

Mirror Member Journal Transfer and Dejournaling Status. 请注意,这里面有两个概念:一个是Mirror成员的状态,一个是Journal传输和Dejournaling的状态。下面的图中是3个字段: STATUS, Journal传输,Dejournaling.

image-20230522112042308

STATUS

镜像成员的状态。 正常工作状态

  • 对于同步成员,是Primary(主), Backup(备机)。

  • 对于异步成员,正常状态是Connected(已连接)

  • In Trouble : 如果主机In Trouble, 是失去了到backup的连接。备机收到主机的同步数据是要返回证实(Ack)消息的。一旦出现问题,主机无法收到备机的Ack, 主机就会把备机标为"In trouble", 从此再也不会向备机发同步数据。

  • Transition: 暂时状态,进程正在查看一个成员的状态,很快会转换到一个稳定状态。 如果在mirror配置的member中发现了primary,本机会进入Synchronizing状态,否则自己会尝试进入primary状态。

  • Sychronizing: 从Primary接收journal,同步数据库。

Journal Transfer and Dejournaling Status

Journal Transfer是主机向其他成员发送Journal文件。而Dejournal是把Journal文件读入数据库。 对于backup或者asycn成员,Journal Transfer状态表示镜像成员是否有来自主数据库的最新日志数据,如果没有,则表示日志传输的落后程度,Dejournaling表示从主数据库收到的所有日志数据是否已经被dejournaled(应用到成员的镜像数据库),如果没有,则表示dejournaling的落后程度。

上图中显示的是正常的状态,其中主机 Journal Transfer 和 Dejournaling 都是N/A, 表示不适用。

对于其他成员,我们分开看:

Journal Transfer状态

  • Active: backup的正常状态。说明backup从primary收到了最新的journal。注意哪怕是Dejournal状态只是“x秒落后“,而不是"被捕获",Journal Transfer状态也可以是Active,只要是从主机收到了最新的Journal更新。

  • Caught up(被捕获) : 备机被捕获状态,说明备机从主机收到了最新的journal数据,但主机没有在等待备机的证实消息。 这通常是一个暂时的过程,当备机在连接主机的时候会出现。 异步成员,因为不需要向主机发证实,所以正常的状态就是“被捕获”

    If the Primary Failover Member does not receive an acknowledgment from the Backup every Heartbeat Interval period, it demotes the Backup system from Active status to Catch-Up mode.

  • time behind (多少秒落后)

  • Disconnected on time(断开): 在一个时间点上这个成员和primary断开了。

Dejournaling状态

  • Caught up
  • time behind
  • Disconnected on time
  • Warning! Some Databases need attention
  • Wanring! Dejournaling is stopped

正常状态下的图;

image-20230522112133793

备机Backup MirrorB, Journal Transfer是Active, Dejournaling是Caught up, 异步机器MirrorDR的Journal Transfer状态和Dejournaling状态都是Caught up. 表示它们收到了最新的journal数据,并且也都把最新的global修改写入了自己的数据库。

Mirror的自动切换

Mirror的核心是自动切换。Backup接替主机的工作有两个前提:1. 备机在同步(Active) 状态, 2. 主机不能正常工作。在这两个前提下,我们来看看自动切换的触发条件,涉及主机,备机,仲裁机之间的通信,

自动切换触发条件

  1. Primary要求Backup接替。这种情况,主机会发生一个请求消息给备机, 要求备机接替。

    • 主机IRIS正常退出
    • 主机发现自己hung
  2. 备机收到arbiter的请求,报告失去了到主机的连接。

    仲裁机要求是和外部系统以及应用服务器部署在一个网段的。如果仲裁机无法联络主机,可以认为其他的应用系统和服务器也无法连接主机。有可能主机宕机, 也有可能主机还在正常工作,但外界已经无法联络它了, 这时候也是需要备机接手的。

    这时备机也要再去核实一下,是不是能联络到主机。如果能联络到, 备机会发请求让主机Down。如果不能, 说明主机要么死了, 要么失联了, 备机先接手,等联络上再让对方force down.

  3. 从主机的ISCAgent收到消息,报告Primary已经down or hung.

    在agent-controlled的情况。 primary的服务器还活着。备机主动去问主机的agent, 一旦agent报告主机死了, 那备机就可以上位了。

Mirror的进程

管理员应该了解mirror涉及的那些进程。当出现故障时,这些进程名字,或者称为User, 经常会出现在message log记录的故障描述中。

On Primary Failover Member(主机)

image

我们来一个个的看看这些进程:

  • Mirror Master: 系统启动时自动启动,负载mirror control 和管理。
  • Mirror Primary: 出向数据传输通道。 上图中有两个Mirror Primary进程,状态时RUNW, 一个连接MirrorB, 一个连接MirrorDR.
  • Mirror Svr: Rd*: 入向证实通道(inbound acknowledgement), 也是单向的。 上图中同样有两个此进程,状态都是READ, IP地址分别是MirrorB和MirrorDR.
  • Mirror Arbiter: 到aibiter的通信进程,注意它的状态是"EVTW", 也是个单向写的频道。

On Backup Member/Async member(备机)

image

Mirror Masht, Mirror Arbiter不再重复解释,我们看看其他进程是干什么的。

  • Mirror JrnRead: Mirror Journal从Primary发送到backup是先写到硬盘的。 JrnRead进程把收到的journal同步读到内存里,然后才进行下一步,Dejournal的工作。

  • Mirror Dejour: backup机器的dejournal job进程。它把从Primary收到的journal中记录的global改变(set and kill)保存到本机的镜像数据库。

  • Mirror Prefetch: 这个稍微有点难懂。当收到的journal修改中包括了使用当前backup的journal中已有的内容时,比如收到了一个修改:set ^A=^B+1, 而^B当前存在backup里, Prefetch进程会把^B从硬盘拿到内存,以加快dejournal的速度。

  • Mirror Backup: two-way channel, 把收到的primary的journal写到backup的mirror journal,并且返回证实(ACK)

这里我省略了在DR上的进程,如果有兴趣,请自己查看文档。

MIRROR状态的监控

根据不同的场景,查看Mirror的状态有以下几种途径

使用镜像监视器

image-20230519100607517

使用^MIRROR

如果您只是要简单的获得Mirror成员的状态,最直接的方法是使用^Mirror程序。 我们先看看在IRIS Terminal下^MIRROR的执行。

%SYS>do ^MIRROR


1) Mirror Status
2) Mirror Management
3) Mirror Configuration

Option? 1

1) List mirrored databases
2) Display mirror status of this node
3) Display journal file info
4) Status Monitor

Option? 4
Status of Mirror MIRRORTEST at 08:09:24 on 05/19/2023
Arbiter Connection Status:
     Arbiter Address:   arbiter|2188
     Failover Mode:     Agent Controlled
     Connection Status: This member is not connected to the arbiter

                                       Journal Transfer
Member Name+Type            Status        Latency       Dejournal Latency
--------------------------  ---------  ---------------  --------------
MIRRORA
     Failover               Primary    N/A              N/A

Press RETURN to refresh, D to toggle database display, Q to quit,
 or specify new refresh interval <60> D
Database display is now on


Status of Mirror MIRRORTEST at 08:09:29 on 05/19/2023
Arbiter Connection Status:
     Arbiter Address:   arbiter|2188
     Failover Mode:     Agent Controlled
     Connection Status: This member is not connected to the arbiter

                                       Journal Transfer
Member Name+Type            Status        Latency       Dejournal Latency
--------------------------  ---------  ---------------  --------------
MIRRORA
     Failover               Primary    N/A              N/A

Mirror Databases:
                                                                   Record To
Name           Directory path                          Status      Dejournal
-------------  -----------------------------------     ----------- -----------
TEST           /isc/mirrorA/TESTDB/                    Normal      N/A


Press RETURN to refresh, D to toggle database display, Q to quit,
 or specify new refresh interval <60>

在操作系统中执行^MIRROR

您可以把以下的代码写入您的脚本语言,查看mirror的状态

irisowner@mirrorA:~$ iris session iris -U "%sys" "Monitor^MIRROR"

Status of Mirror MIRRORTEST at 02:57:08 on 06/13/2023
Arbiter Connection Status:
     Arbiter Address:   arbiter|2188
     Failover Mode:     Arbiter Controlled
     Connection Status: Both failover members are connected to the arbiter

                                       Journal Transfer
Member Name+Type            Status        Latency       Dejournal Latency
--------------------------  ---------  ---------------  --------------
MIRRORA
     Failover               Primary    N/A              N/A
MIRRORB
     Failover               Backup     Active           Caught up
MIRRORDR
     Disaster Recovery      Connected  Caught up        Caught up

Press RETURN to refresh, D to toggle database display, Q to quit,
 or specify new refresh interval <60>q
Doneirisowner@mirrorA:~$

或者更简单的,只查看本机的mirror成员状态:

irisowner@mirrorA:~$ iris session iris -U "%sys" "LocalMirrorStatus^MIRROR"

This instance is a Failover member
     Status for mirror MIRRORTEST is "Primary"

Current mirror file #2 ends at 681224
Min trans file #2 min trans index: 680744
irisowner@mirrorA:~$

如果您熟悉ObjectScript, 也可以使用$SYSTEM.Mirror类的各个method来查看:

irisowner@mirrorB:~$ echo "write \$SYSTEM.Mirror.GetMemberStatus(),! halt" |iris session iris -U "%sys"

Node: mirrorB, Instance: IRIS
%SYS>
Backup
irisowner@mirrorB:~$

如果您要查看更多的内容,您可以更多的使用%SYSTEM.Mirror类的其他方法,比如%SYSTEM.Mirror.GetFailoverMemberStatus(.pri,.alt), $SYSTEM.Mirror.ArbiterState()等等。

使用Mirror_MemberStatusList存储过程

如果您从第3方的工具查询mirror成员的状态,还有一个简单的方案,就是调用%SYS命名空间的存储过程。下图是从iris管理门户调用的截图,你可以使用任何SQL客户端调用。

image-20230613141122211

如果是从iris里执行,

%SYS>do ##class(%ResultSet).RunQuery("SYS.Mirror","MemberStatusList")

Member Name:Current Role:Current Status:Journal Transfer Latency:Dejournal Latency:Journal Transfer Latency:Dejournal Latency:Display Type:Display Status:
MDCHCNDBSL1.HICGRP.COM/STAGE:Primary:Active:N/A:N/A:N/A:N/A:Failover:Primary:
MDCHCNDBSL2.HICGRP.COM/STAGE:Backup:Active:Active:Caught up:Active:Caught up:Failover:Backup:
CDCHCNDRSL.HICGRP.COM/STAGE:Async:Async:Caught up:Caught up:Caught up:Caught up:Disaster Recovery:Connected:

通过SNMP获得

如果使用监控工具,您可以通过SNMP获得Mirror的状态,下面是最新的ISC-IRIS.mib中有关Mirror得指标部分。

.4.1.12 = irisMirrorTab | Table of current Mirror Members status and information
-- .4.1.12.1 = irisMirrorRow | Conceptual row for Mirror status and metrics | INDEX = irisSysIndex, irisMirrorIndex
-- .4.1.12.1.1 = irisMirrorIndex | unique index for each Mirror Member | INTEGER
-- .4.1.12.1.2 = irisMirrorName | Name of the mirror this system is a member of | STRING
-- .4.1.12.1.3 = irisMirrorMember | Mirror member name | STRING
-- .4.1.12.1.4 = irisMirrorRole | "Primary", "Backup", or "Async". | STRING
-- .4.1.12.1.5 = irisMirrorStatus | "Active" or "Activate". | STRING
-- .4.1.12.1.6 = irisMirrorJrnLatency | Mirror journal latency "Caught up", "Catchup", or "N/A". | STRING
-- .4.1.12.1.7 = irisMirrorDBLatency | Mirror database latency "Caught up", "Catchup", or "N/A". | STRING

MIRROR的日志和告警

通常情况下, 维护人员是通过mirror的日志和警告来获得Mirror状态,Mirror成员之间的连接情况,而不必须定时的用命令或者调用存储过程来查看。

Cache'和IRIS的日志和警告保存在两个文件: console.log/messages.log和alert.log, 其中alert.log中记录了console.log/messages.log中级别为2,3的记录, 并必须实时发送给管理员。有关这部分内容,请参考在线文档,或者我的帖子:

我们来看看在日志中有哪些mirror的记录:

Becoming primary mirror server

系统固有的通知消息, level =2。当一个iris实例从备机变成了主机,此信息会写到此实例的alert.log, 同时发送给管理员。 可以查看这个链接

在Mirror切换时,管理员除了从刚刚接手的机器中收到Becoming primary mirror server的通知。如果原来的主机没有宕机或者从宕机中恢复,它也会将引起切换的故障从alert.log发送给管理员,是一个level2, 或者level3的记录。

Arbiter connection lost

level =2 , 自动发送给管理员。 当主机和arbiter失去连接后,在主机上会出现此警告。此时在备机上会出现“Switched from Arbiter Controlled to Agent Controlled failover on request from primary”的提示,是个level0的信息。

MirrorServer: Connection to xxxx(backup) terminatedMirrorServer: Connection to MIRRORDR (async member) terminated

当主机和备机(backup)失去连接,在主机上会出现level2的警告。 而和异步成员丢失连接,主机会出现level1的消息。尽管level1的消息不能自动通知管理员,但这时如果同时监控该异步成员的alert.log, 通常会有level2的警告消息发出,能提醒管理员检查MIRRORDR这个镜像成员的状态。

举例说明:如果在MirrorDR中操作系统重启,IRIS启动后会出现这样的level2的警告:“Previous system shutdown was abnormal, ^SHUTDOWN forced down”

Async member for MirrorSetName started but failed to connect to primary

level =2 , 自动发送给管理员

其他更多的关于Mirror出错的level2, 也就是警告记录, 比如:

  • Could not open mirror journal log to read checksum, errno = 2
  • Preserving all mirror journal files for offline failover member
  • Server^MIRRORCOMM(d): Failed to notify MIRRORB for mirror configuration change
  • Failed to become either Primary or Backup at startup

这不是个完整的列表,实际环境中会出现各种各样的告警通知。读懂这些通知,需要管理员了解镜像的原理,架构,以及上面介绍的镜像状态和进程的功能。

除此之外,绝大多数的level2日志的同时,会有更多的level0,level1的有关mirror变化的记录。这些内容不需要通知管理员,只是用于分析问题。 如图,下面是在一个messages.log里一个iris从备机变成主机的过程。

06/13/23-07:16:25:472 (2189) 0 [Generic.Event] MirrorClient: Switched from Arbiter Controlled to Agent Controlled failover on request from primary
06/13/23-07:16:26:274 (2189) 1 [Generic.Event] MirrorClient: Mirror_Client: Primary closed down, last # read = 504
06/13/23-07:16:26:301 (2189) 0 [Generic.Event] MirrorClient: Backup waiting for old Dejournal Reader (pid: 2190, job #31) to exit
06/13/23-07:16:27:394 (2189) 0 [Generic.Event] MirrorClient: Set status for MIRRORTEST to Transition
06/13/23-07:16:28:477 (1996) 0 [Utility.Event] [SYSTEM MONITOR] Mirror status changed. Member type = Failover, Status = Transition
06/13/23-07:16:30:261 (2177) 0 [Utility.Event] Returning to restart, old primary reported: "DOWN
06/13/23-07:16:31:524 (11721) 0 [Utility.Event] Applying journal data for mirror "MIRRORTEST" starting at 1538184 in file #2(/isc/mirrorB/mgr/journal/MIRROR-MIRRORTEST-20230613.001)
06/13/23-07:16:31:804 (2177) 0 [Utility.Event] Manager initialized for MIRRORTEST
06/13/23-07:16:31:986 (2177) 0 [Utility.Event] MIRRORA reports it is DOWN, becoming primary mirror server
06/13/23-07:16:32:381 (2177) 0 [Generic.Event] INTERSYSTEMS IRIS JOURNALING SYSTEM MESSAGE
Journaling switched to: /isc/mirrorB/mgr/journal/MIRROR-MIRRORTEST-20230613.002
06/13/23-07:16:32:426 (2177) 0 [Utility.Event]   Scanning /isc/mirrorB/mgr/journal/MIRROR-MIRRORTEST-20230613.001
06/13/23-07:16:32:479 (2177) 0 [Utility.Event] No open transactions to roll back
06/13/23-07:16:32:485 (2177) 0 [Generic.Event] MirrorServer: New primary activating databases which are current as of 1538184 (0x00177888) in mirror journal file #2
06/13/23-07:16:32:488 (2177) 0 [Generic.Event] Changed database /isc/mirrorB/TESTDB/ (SFN 5) to read-write due to becoming primary.
06/13/23-07:16:32:924 (2177) 0 [Utility.Event] Initializing Interoperability during mirror initialization
06/13/23-07:16:32:930 (2177) 2 [Utility.Event] Becoming primary mirror server

更多的有关mirror监控和排除的问题, 请各位留言。 谢谢

0
1 663
文章 Hao Ma · 五月 26, 2023 8m read

题外话:我刚刚翻译了InterSystems专家Bob Binstock的Caché Mirroring 101:简要指南和常见问题解答。 尽管题目是Caché Mirror 101, 而且是写于2016年,但因为讲解的都是Mirror的基本原理,所以在大量使用IRIS的今天也完全适用。

前面的3篇文章,包括了配置Mirror的各个方面。如果您照着操作,现在已经有了一个工作的mirror环境,并加入了您的数据库。然而,还没完,这篇我来讨论一下后面的工作,首先的问题是:

Mirror不复制什么

简单说,Caché/IRIS镜像是数据库复制(Database Replication)。在Caché/IRIS里什么是数据库?也就是Cache.dat和iris.dat文件。数据库的修改日志,也就是journal,从主机被传送到其他镜像成员。而除此之外的内容,需要维护人员来分别的个个处理, 解决这些内容在各个镜像成员间的拷贝。需要很多的计划和细心。

系统数据库, 包括IRISSYS, IRISTEMP, IRISLIB等等, 这些Caché/IRIS本身的数据库不应该被加入Mirror,在大多数Caché/IRIS版本里也都设置成不可以加入入MIRROR。

例外的HealthCare产品, HSSYS需要做Mirror, HSCustom可以做Mirror, 而HSLIB不可以Mirror

我们可以把问题转换成下面的题目:

需要人工在镜像成员中同步的项目

命名空间(namespace)和Mapping

命名空间是应用开发的概念,它使用数据库。命名空间定义了3种映射关系:Package Mapping, Routing Mapping, Global Mapping。这样在一个命名空间可以使用多个数据库的内容。

通常情况下,用户会在主机创建命名空间的同时,创建一个新的带有mirror属性的数据库,然后会在其他mirror成员中手工一个个的创建命名空间,加入镜像的数据库。之后,管理员无需考虑更多的操作。

然而,对命名空间的修改,比如要添加或者删除命名空间的某些mapping,这偶尔会需要,尤其是应用迭代和系统扩容的情况下,那么,管理员/实施人员,必须清楚Mirror无法同步这个修改,您必须手工同步修改到其他机器去。

如果配置的mapping比较多, 我建议使用Manifest来操作。Mainfest是一个xml的文本,用来安装或者修改Caché/IRIS的配置,你可以参考在线文档: Using a Manifest, 或者社区文章使用Manifest

这里给一个配置mapping的例子:

<Manifest>
    <Namespace Name="FHIRNS">
		<Configuration>
			<Database Name="FHIRNS"/>
            <GlobalMapping Global="%SYS" From="IRISSYS"/> 
            <GlobalMapping Global="OAuth2.*" From="HSSYS"/> 
            <GlobalMapping Global="SchemaMap.*" From="HSLIB"/> 
            <ClassMapping Package="HS" From="HSLIB"/>
            <ClassMapping Package="HS.Local" From="HSCUSTOM"/>
            <ClassMapping Package="HSMOD" From="HSLIB"/>
            <ClassMapping Package="SchemaMap" From="HSLIB"/>
            <RoutineMapping Routines="HS.*" From="HSLIB"/>
            <RoutineMapping Routines="HSMOD.*" Type="INC" From="HSLIB"/>
            <RoutineMapping Routines="HSMOD.*" From="HSLIB"/>
            <RoutineMapping Routines="SchemaMap.*" From="HSLIB"/>
		</Configuration>
	</Namespace>
</Manifest>

如果是资深的Caché维护工程师,懂得如果修改CPF文件并在不重启实例的情况下应用修改后的内容,可以考虑把主机上的CPF中的mapping部分复制粘贴到其他机器。如果您没有这方面的经验,我不建议这种方式。

另外,在IRIS 2022后的版本中有了一个新工具,Configuration Merge。 文档在这里。可惜只有最新版的IRIS或者Health Connect 用户有的用。

数据库的修改

数据库的内容会通过Journal从主机同步到其他成员,但修改不会,一般会遇到的是压缩和截断

由于某种错误操作,某个数据库,会扩展到不正常的大,而当错误修正后,用户可能需要对该数据库进行压缩和截断,以释放被错误占用的空闲的磁盘空间。

由于除主机外,其他镜像成员的数据库都是只读的,这个操作的顺序应该是这样:

  1. 在主机A执行压缩和截断

  2. 切换到备机B, 再次执行压缩和截断。

  3. 异步成员DR。 一种方案是吧DR提升到备机。这时当前的备机A会将为灾备,然后再切换DR为主机,再进行压缩和截断。

    还有一个选择,就是重新配置DR上的这个数据库,这需要从主机到DR的数据库备份和恢复。

IRIS实例的配置

从最常用的内存的配置,Service的配置, 用户,权限,资源的配置等等。它们都不会被MIRROR同步。如果您在MIRROR主机里做了修改了缩表的大小,或者启动了一个,比如TELNET服务, 您需要人工在其他机器上做相同操作。

像上面的mapping配置一样,这里还是建议使用Manifest人工同步IRIS得修改。注意的是,Mainfest不保证能支持所有的配置。比如在Caché的版本下, 比如您在主机上启动了TELNET服务, Manifest没有相应的标签<TAG>。这种情况下, 如果您熟悉ObjectScript语言,可以把ObjectScript实现加入执行Manifest的方法,比如说:

ClassMethod main(){
   //执行Manifest修改命名空间
   Set pVars("Namespace")="MYNAMESPACE"
   $$$ThrowOnError(..ModifyNamespace(.pVars))
   //启动IRIS的TELNET服务
   set properties("Enabled")=1            // 有効
 	set sts=##class(Security.Services).Modify("%Service_Telnet",.properties) 
}

当然,如果您缺乏开发实施的知识,在用户界面上一个个机器的操作是最省心的办法。

问题是,打开一个服务,修改一个配置参数操作都很简单,但是如果要添加大量的用户和权限怎么办?

用Manifest管理是一个办法。但根本上,如果您经常有大量的用户管理的工作,其实使用Kerberos或者LDAP管理用户身份认证和授权的工作, 在有多个镜像成员的情况下,尤其的合适。 关于这部分内容,请参考在线文档:Authentication and Authorization

定时任务(TASK)

在主机上创建的定时任务, 您需要人工在其他机器上做相同操作。这里有2个步骤:

  1. 在主机上创建新任务的时候,要选择”应如何为镜像运行任务“。 这是个下拉菜单,选项有*”仅在主镜像成员上运行“,“仅在非主镜像成员上运行“ ,“在任何镜像成员上运行"。*

    选择的出发点是:非主镜像成员的数据库是只读的。因此,比如一个Ensemble的镜像配置中, 删除Ensemble消息的定时任务, 一定是”仅在主镜像成员上运行“。

  2. 把新的定时任务从主机同步到其他成员。

​ 如果是一个或者少量几个TASK, 那么手工在其他各个镜像成员上添加是最简单直接的做法。而如果是有很长 的任务列表,尤其在配置Mirror得时候可以需要同步一个长长的列表时, 您可以考虑从主机导出Task到其 他机器导入,我只知道使用ObjectScript命令的方法, 使用%SYS.Task.ExportTask()%SYS.Task.ImportTasks()。 文档在这里

Web Application

主机上配置的Web Applicaiton 也要同步到其他镜像成员。如果要同步的Web Application比较多,推荐的方式依然是Manifest, 下面是一个例子。

<Manifest>
	<Namespace Name="flagger">
      <CSPApplication CSPZENEnabled="1" LoginClass="/csp/flagger/Flagger.LoginAuth.cls" CustomErrorPage="/csperror.csp" ChangePasswordPage="/csp/flagger/ChangePassword.cls" AutoCompile="1" Url="/csp/flagger" IsNamespaceDefault="1" InboundWebServicesEnabled="1" Recurse="1" AuthenticationMethods="64" DefaultTimeout="900" Directory="${CSPDIR}flagger" UseSessionCookie="2" CookiePath="/csp/flagger/" ServeFiles="1" ServeFilesTimeout="3600"/>
   </Namespace>
</Manifest>

麻烦的是不同的版本Caché/IRIS使用的标签上会略有不同,要稍微仔细的查看一下您的版本的文档。

如果您对ZPM, 现在称为IPM熟悉的话, 用ZPM做同步也是个好选择。关于zpm, 您可以参考这个帖子zpm介绍。提醒一下的是,程序因为是存在数据库里面的,如果该数据库是被镜像的,您其实不需要用ZPM把程序代码拷贝到其他镜像成员。

Gateway

一般用到的有SQL GatewayExternal Language Gateway,它们分别用于连接其他的数据库和使用其他语音的代码包。

SQL Gateway

记录保存在%SYS命名空间的*%Library.sys_SQLConnection*数据表里。简单的方法是使用工具把表记录导入导出。

External Language Gateway(外部语言网关)

新版的IRIS系统内嵌了外部语言服务器,包括%Python Server, %Java Server, %Dotnet Server等。如果您使用的是默认配置,各个镜像成员是一致的,无需操心。如果只是IP端口的修改,手工同步一下也很容易,毕竟工作量有限,只是您需要清楚的记得,这个也是不被Mirror自动同步的。

文件

我把文件分为两类, 一类是“固定文件”,包括一下几个部分,

  • CSP文件,js文件,css文件,html文件等
  • XSLT文件
  • 其他语言的程序代码,Java文件,python文件, .Net文件

这类文件上传到主机的时候, 也必须上传到其他镜像成员,这是个简单的操作,别忘了就行。

麻烦的是流文件。在ObjectScript里如果使用了%Stream.FileBinary, %Stream.FileCharacter等类,那么数据不是保存到Cache.Dat或者IRIS.data, 而是保存在和.Dat同目录的一个stream的子目录下,而这个目录是不会被镜像同步的。 而且,因为这是实时数据,你也不可能手工的把它拷来拷去。

如果您的应用里用到了文件流,我任务您需要一个文件服务器保证流文件在各个各个镜像成员间的同步。

Ensemble Production Consideration

对于Ensemble和Health Connect用户,您需要阅读这部分在线文档: Production Considerations for Mirroring , 简单总结一下:

  • 创建的带有ensemble或者Inteoprability的命名空间,数据库要创建为Mirror的数据库。
  • **"production是否自动启动“**应该在主机和备机上,甚至DR上都配置为“自动启动”。 在Mirror配置下的Production会先检查这个实例是不是主机,如果不是,“自动启动”的配置也不会生效,这样保证了Production只在主机上运行,而切换后也不需要人工干预。

上面的这些并不是完整的内容,尽管在大多少情况下这些内容差不多够了。如果您想要确保Mirror的主机的工作内容完全同步到了备机和DR, 请仔细阅读在线文档的这一部分:Mirror Configuration Guidelines

另外,对于各种需要人工同步的内容的操作,还建议阅读在线文档:Server Migration

如果是最新的IRIS用户,请参考在线文档:Deploy Mirrors Using Configuration Merge

0
0 396