1. 醫療保健領域的表現挑戰
醫療FHIR Server系統必須滿足高要求:正常運作時間≥99.9%,回應時間<500ms,百萬級資源。
| 因素 | 挑戰 | 目標 |
|---|---|---|
| 回應時間 | 大型資料集的複雜搜索 | < 500 毫秒 (P95) |
| 吞吐量 | 多家醫院同時查詢 | ≥ 1000 請求/秒 |
| 數據量 | 數百萬患者,數十億觀察結果 | 結核病規模 |
| 可用性 | 24/7 緊急護理 | ≥99.9% |
| 一致性 | 醫療數據必須準確 | 一致性強 |
2. 資料庫優化
HAPI FHIR 的 PostgreSQL 索引
-- Index cho search by identifier (CCCD, BHYT)
CREATE INDEX idx_spidx_token_hash
ON hfj_spidx_token (hash_identity, sp_value_normalized)
WHERE sp_missing = false;
-- Index cho search by name
CREATE INDEX idx_spidx_string_normalized
ON hfj_spidx_string (hash_identity, sp_value_normalized)
WHERE sp_missing = false;
-- Index cho search by date (Observation.date)
CREATE INDEX idx_spidx_date_range
ON hfj_spidx_date (hash_identity, sp_value_low, sp_value_high)
WHERE sp_missing = false;
-- Index cho _lastUpdated sorting
CREATE INDEX idx_res_updated
ON hfj_resource (res_updated DESC, res_id)
WHERE res_deleted_at IS NULL;
-- Partitioning cho Observation (table lớn nhất)
CREATE TABLE hfj_res_observation PARTITION OF hfj_resource
FOR VALUES IN ('Observation')
PARTITION BY RANGE (res_updated);
CREATE TABLE hfj_res_observation_2025_q1
PARTITION OF hfj_res_observation
FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
查詢最佳化
# application.yml - HAPI FHIR tuning
hapi:
fhir:
# Search caching
reuse_cached_search_results_millis: 60000
# Pagination
default_page_size: 20
max_page_size: 200
# Prefetch
search_prefetch_thresholds:
- 13 # offset 0
- 50 # offset 1
- 200 # offset 2+
# Inline resource storage (tránh JOIN)
inline_resource_storage_below_size: 4096
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 5000
idle-timeout: 300000
3. 快取策略
┌────────┐ ┌───────┐ ┌──────────┐ ┌──────┐
│ Client │──▶│ CDN/ │──▶│ Redis │──▶│ FHIR │
│ │ │ Nginx │ │ Cache │ │Server│
└────────┘ └───────┘ └──────────┘ └──────┘
// Redis cache cho CapabilityStatement và ValueSet
@Configuration
public class FhirCacheConfig {
@Bean
public CacheManager cacheManager(
RedisConnectionFactory redisFactory) {
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(
SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisFactory)
.cacheDefaults(config)
.withCacheConfiguration("metadata",
config.entryTtl(Duration.ofHours(1)))
.withCacheConfiguration("valueset",
config.entryTtl(Duration.ofHours(24)))
.build();
}
}
// Interceptor: cache CapabilityStatement
@Component
public class MetadataCacheInterceptor {
@Autowired
private CacheManager cacheManager;
@Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED)
public void cacheCapabilityStatement(
IBaseConformance cs) {
cacheManager.getCache("metadata")
.put("capability", cs);
}
}
HTTP 快取標頭
# nginx.conf
location /fhir/metadata {
proxy_pass http://fhir-server:8080;
proxy_cache fhir_cache;
proxy_cache_valid 200 1h;
add_header X-Cache-Status $upstream_cache_status;
}
location /fhir/ValueSet {
proxy_pass http://fhir-server:8080;
proxy_cache fhir_cache;
proxy_cache_valid 200 24h;
proxy_cache_key "$request_uri";
}
# Dynamic resources — no cache
location /fhir/Patient {
proxy_pass http://fhir-server:8080;
add_header Cache-Control "no-store";
}
4. 水平縮放
# docker-compose-ha.yml
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- fhir-1
- fhir-2
- fhir-3
fhir-1:
image: hapiproject/hapi:latest
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://pgpool:5432/hapi_fhir
- JAVA_OPTS=-Xmx2g
fhir-2:
image: hapiproject/hapi:latest
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://pgpool:5432/hapi_fhir
- JAVA_OPTS=-Xmx2g
fhir-3:
image: hapiproject/hapi:latest
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://pgpool:5432/hapi_fhir
- JAVA_OPTS=-Xmx2g
pgpool:
image: bitnami/pgpool:latest
environment:
- PGPOOL_BACKEND_NODES=0:pg-primary:5432,1:pg-replica:5432
- PGPOOL_ENABLE_LOAD_BALANCING=yes
- PGPOOL_SR_CHECK_USER=repmgr
pg-primary:
image: bitnami/postgresql-repmgr:16
environment:
- POSTGRESQL_DATABASE=hapi_fhir
- REPMGR_NODE_TYPE=primary
pg-replica:
image: bitnami/postgresql-repmgr:16
environment:
- REPMGR_NODE_TYPE=standby
- REPMGR_PRIMARY_HOST=pg-primary
redis:
image: redis:7-alpine
command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
5. 使用Prometheus + Grafana進行監控
公開指標
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: fhir-server
普羅米修斯配置
# prometheus.yml
scrape_configs:
- job_name: 'fhir-server'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets:
- 'fhir-1:8080'
- 'fhir-2:8080'
- 'fhir-3:8080'
關鍵指標需要追蹤
| 公制 | 描述 | 警報閾值 |
|---|---|---|
| http_server_requests_seconds_p95 | P95響應時間 | > 500 毫秒 |
| http_server_requests_total | 請求率 | 異常檢測 |
| jvm_內存_已使用_位元組 | JVM 堆使用情況 | > 80% |
| hikaricp_connections_active | 活動資料庫連接 | > 80% 池 |
| fhir_search_duration_秒 | 搜尋查詢時間 | > 1秒 |
| fhir_資源_計數 | 總資源 | 成長率 |
| pg_stat_activity_count | PostgreSQL 連接 | > 最大 90% |
| 磁碟使用百分比 | 磁碟空間 | > 85% |
健康檢查端點
@Component
public class FhirHealthIndicator implements HealthIndicator {
@Autowired
private IGenericClient fhirClient;
@Override
public Health health() {
try {
CapabilityStatement cs = fhirClient
.capabilities()
.ofType(CapabilityStatement.class)
.execute();
return Health.up()
.withDetail("fhirVersion", cs.getFhirVersion().toCode())
.withDetail("resourceCount",
cs.getRest().get(0).getResource().size())
.build();
} catch (Exception e) {
return Health.down()
.withException(e)
.build();
}
}
}
6. 警報規則
# alerting-rules.yml
groups:
- name: fhir-server
rules:
- alert: FHIRHighResponseTime
expr: |
histogram_quantile(0.95,
rate(http_server_requests_seconds_bucket{uri=~"/fhir/.*"}[5m])
) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "FHIR Server P95 response time > 500ms"
- alert: FHIRServerDown
expr: up{job="fhir-server"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "FHIR Server instance down"
- alert: FHIRDatabaseConnectionExhausted
expr: |
hikaricp_connections_active /
hikaricp_connections_max > 0.8
for: 5m
labels:
severity: warning
七、總結
資料庫最佳化 — PostgreSQL 的索引、分區、連接池
快取 — Redis 用於元資料/ValueSet、Nginx 反向代理緩存
水平縮放 — 多個 FHIR 實例 + 負載平衡器 + PgPool
監控 — Prometheus 指標、Grafana 儀表板、警報規則
健康檢查 — 自訂 HealthIndicator、就緒性/活性探針
醫療 SLA — 正常運作時間 ≥ 99.9%,P95 < 500ms,稽核追蹤 7 年以上