第一章:Zinc Search Engine核心架构与Golang生态适配全景图
Zinc Search Engine 是一个轻量级、高性能的全文搜索引擎,专为现代云原生场景设计,其核心定位是替代 Elasticsearch 的简化子集,同时深度拥抱 Go 语言生态。它完全使用 Go 编写,不依赖 JVM 或外部服务,二进制单文件分发,天然契合 Go 的构建链(go build)、模块管理(go mod)和可观测性标准(expvar、pprof、OpenTelemetry SDK 原生集成)。
架构分层概览
Zinc 的运行时由四大逻辑层构成:
- HTTP 网关层:基于
net/http构建,支持 HTTP/1.1 与 HTTP/2,内置 CORS、JWT 认证中间件(通过github.com/gogf/gf/v2/net/ghttp扩展可插拔); - 查询协调层:解析 Lucene 风格查询(如
title:"Go generics" AND status:published),转换为内部 AST,并分发至索引层; - 存储引擎层:采用内存映射(
mmap)+ WAL 日志的 LSM-Tree 变体,索引数据序列化为gob格式并辅以zstd压缩,所有 I/O 操作经sync.Pool复用缓冲区; - 集群管理层:基于 Raft 协议(
github.com/hashicorp/raft)实现元数据强一致同步,数据分片(shard)默认本地化,暂不支持跨节点数据分发(设计上保留扩展接口)。
Golang 生态关键适配点
Zinc 主动对齐 Go 社区最佳实践:
- 使用
context.Context贯穿全链路超时与取消; - 日志统一接入
slog(Go 1.21+ 标准库),支持 JSON/Text 输出及结构化字段; - 配置管理兼容
viper和原生flag,默认读取config.yaml或环境变量(如ZINC_ADDR=:4080); - 测试套件 100% 覆盖核心路径,
go test -race启用竞态检测,CI 中强制gofmt+golint+staticcheck。
快速验证示例
启动 Zinc 并索引一条文档,仅需三步:
# 1. 下载预编译二进制(Linux x86_64)
curl -L https://github.com/zincsearch/zinc/releases/download/v0.5.0/zinc-0.5.0-linux-x86_64.tar.gz | tar xz
./zinc & # 默认监听 :4080
# 2. 创建索引(自动推导 schema)
curl -XPUT 'http://localhost:4080/api/indexes/blog' -H 'Content-Type: application/json' -d '{}'
# 3. 写入文档(触发实时刷新)
curl -XPOST 'http://localhost:4080/api/indexes/blog/documents' \
-H 'Content-Type: application/json' \
-d '{"title":"Zinc in Go Ecosystem","tags":["search","golang"]}'
该流程体现 Zinc 对 Go 工具链的零耦合依赖——无需 Docker、无 CGO、无外部配置中心,纯 Go 运行时即开即用。
第二章:Zinc服务端深度集成与Golang客户端工程化实践
2.1 Zinc REST API协议解析与Golang HTTP客户端最佳实践
Zinc 是轻量级全文搜索引擎,其 REST API 遵循标准 HTTP 语义,支持 JSON 请求/响应,Content-Type: application/json 为强制要求,所有端点均以 /api/ 为前缀,错误统一返回 4xx/5xx + {"error": "message"} 结构。
请求构造要点
- 必须设置
Authorization: Basic <base64(user:pass)>或Bearer <token> - 批量写入推荐使用
POST /api/index/_bulk,单文档操作走PUT /api/index/doc/<id> - 超时控制建议:连接 ≤3s,读取 ≤10s(防慢查询阻塞)
Golang 客户端核心配置
client := &http.Client{
Timeout: 12 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}
此配置避免 TLS 验证绕过风险,同时通过连接复用提升吞吐;
MaxIdleConnsPerHost与 Zinc 集群节点数对齐可减少握手开销。
| 参数 | 推荐值 | 说明 |
|---|---|---|
Timeout |
12s | 总超时,覆盖连接+请求+响应全过程 |
MaxIdleConnsPerHost |
≥节点数×2 | 防止单节点连接耗尽 |
IdleConnTimeout |
30s | 匹配 Zinc 默认 keep-alive 时长 |
错误重试策略
- 幂等性操作(GET/DELETE)可无条件重试
- 非幂等操作(POST/PUT)需结合
X-Zinc-Request-ID去重 - 推荐指数退避:
time.Sleep(time.Second << uint(retry))
graph TD
A[发起请求] --> B{HTTP 状态码}
B -->|2xx| C[解析JSON响应]
B -->|429/503| D[指数退避后重试]
B -->|4xx| E[校验参数并终止]
B -->|5xx| F[记录日志并重试≤3次]
2.2 基于go-zinc-client的索引生命周期管理(创建/更新/删除/别名)
ZincSearch 作为轻量级全文检索引擎,其索引生命周期需通过 go-zinc-client 精确控制。
创建索引
index := &zinc.Index{
Name: "logs-2024",
Settings: &zinc.IndexSettings{
NumShards: 1,
NumReplicas: 0,
},
}
_, err := client.CreateIndex(context.Background(), index)
// 参数说明:Name 必填且唯一;NumShards 影响写入并发能力;NumReplicas=0 适用于单节点开发环境
别名绑定与切换
| 操作 | 命令示例 |
|---|---|
| 添加别名 | POST /_aliases → "actions": [{"add": {"index": "logs-2024", "alias": "logs-active"}}] |
| 原子切换 | 先移除旧别名,再添加新索引别名 |
生命周期演进流程
graph TD
A[创建索引] --> B[写入数据]
B --> C[按时间滚动创建新索引]
C --> D[别名指向最新索引]
D --> E[归档旧索引并删除]
2.3 Golang批量写入与流式索引优化:Bulk API封装与内存/并发调优
数据同步机制
Elasticsearch 的 Bulk API 是高吞吐写入的核心。Golang 客户端需避免逐条提交,转而构建批次缓冲区,按大小(如 10MB)或数量(如 500 docs)触发提交。
内存与并发协同调优
- 使用
sync.Pool复用[]byte缓冲区,降低 GC 压力 - 并发 worker 数建议设为
CPU 核心数 × 2,配合semaphore控制并发请求数
批量写入封装示例
type BulkWriter struct {
client *elastic.Client
pool sync.Pool
sem *semaphore.Weighted
}
func (w *BulkWriter) Write(docs []interface{}) error {
buf := w.pool.Get().([]byte)
defer func() { w.pool.Put(buf) }()
// 构建 bulk JSON: action line + doc line
for _, doc := range docs {
// ... 序列化逻辑
}
return w.client.Bulk().Index("logs").BodyBytes(buf).Do(context.TODO())
}
sync.Pool 显著减少内存分配;BodyBytes 避免重复序列化;sem 防止连接风暴。
| 参数 | 推荐值 | 影响 |
|---|---|---|
bulk_size |
5–10 MB | 平衡网络开销与延迟 |
workers |
4–16 | 依 I/O 能力动态调整 |
refresh |
false |
关闭实时刷新提升吞吐 |
graph TD
A[文档流] --> B{缓冲区满?}
B -->|否| C[追加到 batch]
B -->|是| D[异步提交 Bulk]
D --> E[复用 buffer]
E --> C
2.4 Zinc文档映射建模与Golang struct tag驱动的Schema自动同步机制
Zinc 通过解析 Go struct 的 zinc tag,将结构体字段语义自动映射为 Elasticsearch 兼容的文档 Schema。
数据同步机制
Zinc 利用 reflect 遍历结构体字段,提取 zinc:"name,type=text,store=true" 等元信息,生成动态 mapping JSON。
type Product struct {
ID string `zinc:"name=id,type=keyword,store=true"`
Name string `zinc:"name=name,type=text,analyzer=ik_smart"`
Price float64 `zinc:"name=price,type=double"`
Active bool `zinc:"name=active,type=boolean"`
}
字段
zinctag 支持name(目标字段名)、type(ES 类型)、analyzer(分词器)、store(是否独立存储)等参数,缺失时按默认策略推导(如string→text)。
映射能力对照表
| Tag 参数 | 取值示例 | 对应 Zinc Schema 属性 |
|---|---|---|
type |
keyword, date |
"type": "keyword" |
analyzer |
ik_max_word |
"analyzer": "ik_max_word" |
store |
true / false |
"store": true |
自动同步流程
graph TD
A[Struct 定义] --> B[Tag 解析]
B --> C[字段类型校验]
C --> D[生成 mapping JSON]
D --> E[PUT /_mapping API 同步]
2.5 Zinc搜索DSL在Golang中的类型安全封装:Query Builder设计与错误恢复策略
Zinc 的 RESTful 搜索 DSL 原生为 JSON,直接拼接易出错且缺乏编译期校验。我们通过 Go 泛型构建类型安全的 QueryBuilder,将 term、bool、range 等查询节点建模为强类型结构体。
核心设计原则
- 所有查询构造器方法返回
*QueryBuilder,支持链式调用 Build()方法执行结构校验并序列化为合法 Zinc DSL JSON- 错误恢复采用两阶段策略:语法预检(如空字段名) + 运行时 HTTP 响应解析重试
示例:构建带容错的 term 查询
q := NewQueryBuilder().
Term("title", "Go").
MustNot().Term("status", "").
Build() // 返回 (json.RawMessage, error)
Term(field, value)内部自动跳过空值value,避免 Zinc 400 错误;MustNot()在bool上下文中生成must_not子句,若前置未初始化bool则惰性创建。
| 组件 | 职责 | 错误恢复机制 |
|---|---|---|
Validator |
字段名合法性、嵌套深度限制 | 提前 panic → 日志+降级默认值 |
Serializer |
JSON 序列化 | json.Marshal 失败时返回 ErrInvalidQuery |
graph TD
A[Start Query Construction] --> B{Field Valid?}
B -->|Yes| C[Append to AST]
B -->|No| D[Log & Return ErrFieldInvalid]
C --> E[Build JSON]
E --> F{Marshal Success?}
F -->|Yes| G[Return RawMessage]
F -->|No| H[Wrap as ErrSerialization]
第三章:高可用场景下的Zinc+Golang协同治理模式
3.1 单节点Zinc服务的Golang健康探针与自动故障转移实现
健康探针设计原则
采用 HTTP + TCP 双层探测:HTTP 探针验证 /health 端点返回 200 OK 且 status: "healthy";TCP 探针确保 Zinc 默认端口 4080 可建连,规避应用层卡死但进程存活的场景。
自动故障转移逻辑
func probeZinc() (bool, error) {
resp, err := http.DefaultClient.Post(
"http://localhost:4080/health",
"application/json",
bytes.NewReader([]byte{}),
)
if err != nil {
return false, fmt.Errorf("TCP unreachable: %w", err) // 底层连接失败
}
defer resp.Body.Close()
var health struct{ Status string }
if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
return false, fmt.Errorf("invalid JSON response: %w", err)
}
return health.Status == "healthy", nil
}
该函数执行轻量级同步探测,超时设为 3s(通过 http.Client.Timeout 控制),失败连续 3 次触发本地服务重启流程。
故障响应策略对比
| 触发条件 | 动作 | 恢复时间 |
|---|---|---|
| 单次探针失败 | 记录告警日志 | — |
| 连续3次失败 | systemctl restart zinc |
|
| 进程不存在 | 启动守护进程 |
graph TD
A[启动探针定时器] --> B{HTTP/TCP双探针}
B --> C[成功?]
C -->|是| D[维持运行状态]
C -->|否| E[计数器+1]
E --> F{≥3次?}
F -->|是| G[执行systemctl restart]
F -->|否| A
3.2 多实例Zinc集群状态同步与Golang协调器(Coordinator)轻量级实现
数据同步机制
Zinc 多实例间通过基于 Raft 的轻量共识层保障元数据一致性,但索引分片状态需异步广播。Coordinator 采用心跳+版本向量(Vector Clock)混合机制检测脑裂并触发重平衡。
Coordinator 核心逻辑
type Coordinator struct {
peers map[string]*PeerState // 节点ID → 最新心跳时间戳+版本号
clock *vector.Clock // 全局逻辑时钟,每状态变更自增
sync.RWMutex
}
func (c *Coordinator) UpdateState(nodeID string, state IndexState, version uint64) {
c.Lock()
defer c.Unlock()
c.peers[nodeID] = &PeerState{State: state, Version: version, LastSeen: time.Now()}
c.clock.Increment(nodeID) // 绑定节点逻辑时钟
}
UpdateState 确保每个节点状态带唯一时序标记;vector.Clock 避免全局锁,支持并发更新;LastSeen 用于超时剔除离线节点。
同步策略对比
| 策略 | 延迟 | 一致性强度 | 实现复杂度 |
|---|---|---|---|
| HTTP轮询 | 高 | 弱 | 低 |
| WebSocket推送 | 中 | 中 | 中 |
| Coordinator事件总线 | 低 | 强(CAS+向量时钟) | 低 |
状态收敛流程
graph TD
A[节点上报分片状态] --> B{Coordinator校验版本向量}
B -->|冲突| C[触发协商协议]
B -->|一致| D[广播最终状态到所有在线Peer]
C --> E[选取最高向量节点为权威源]
E --> D
3.3 日志驱动的检索可观测性:Zinc指标采集 + Golang Prometheus Exporter定制
Zinc 作为轻量级全文检索引擎,原生不暴露结构化指标。为实现日志驱动的可观测性闭环,需构建专用 exporter。
数据同步机制
通过 Zinc Admin API(/api/_stats)周期性拉取节点级指标(如 docs_count、indexing_rate、search_latency_p95),并映射为 Prometheus 标准格式。
自定义 Exporter 核心逻辑
func (e *ZincExporter) Collect(ch chan<- prometheus.Metric) {
stats, _ := e.client.GetStats() // 调用 Zinc v0.5+ /api/_stats REST 接口
ch <- prometheus.MustNewConstMetric(
zincIndexDocsTotal,
prometheus.GaugeValue,
float64(stats.Store.DocCount),
stats.NodeName, // label: instance
)
}
stats.NodeName 提供多节点区分能力;DocCount 直接反映索引健康度,避免日志解析开销。
关键指标映射表
| Zinc 原始字段 | Prometheus 指标名 | 类型 | 用途 |
|---|---|---|---|
store.doc_count |
zinc_index_docs_total |
Gauge | 文档总量监控 |
search.latency.p95 |
zinc_search_latency_seconds |
Summary | 检索延迟 P95 |
graph TD
A[Zinc HTTP Server] -->|GET /api/_stats| B[Golang Exporter]
B -->|expose /metrics| C[Prometheus Scraping]
C --> D[Grafana 日志-指标联动看板]
第四章:生产级Zinc检索能力增强与Golang扩展开发实战
4.1 自定义Analyzer插件开发:Golang编写的分词器嵌入Zinc原生Pipeline
Zinc v0.5+ 支持通过 plugin.Analyzer 接口动态注册 Golang 实现的分词器,无缝集成至其默认分析管道。
实现核心接口
type ChineseTokenizer struct{}
func (t *ChineseTokenizer) Tokenize(text string) []string {
// 使用 gojieba 进行中文分词(需提前 vendoring)
segs := jieba.Cut(text)
var tokens []string
for _, s := range segs {
if len(strings.TrimSpace(s)) > 0 {
tokens = append(tokens, strings.ToLower(s))
}
}
return tokens
}
该实现将原始文本交由 gojieba.Cut() 切分,过滤空格后统一小写,符合 Zinc token normalization 要求;Tokenize() 方法签名与 Zinc Analyzer 接口严格一致。
注册流程
- 编译为
.so插件(CGO enabled) - 放入
plugins/analyzer/目录 - 在
zinc.json中声明:"analyzers": { "chinese_custom": { "type": "plugin", "library": "chinese_tokenizer.so" } }
支持能力对比
| 特性 | 内置 Standard | Golang 插件 |
|---|---|---|
| 扩展性 | ❌ 静态编译 | ✅ 动态加载 |
| 语言支持 | 仅基础 Unicode | 可集成 jieba、pkuseg 等 |
| 热更新 | 不支持 | 支持替换 .so 文件 |
graph TD
A[HTTP Index Request] --> B{Analyzer Config}
B -->|chinese_custom| C[Load chinese_tokenizer.so]
C --> D[Call Tokenize()]
D --> E[Build Inverted Index]
4.2 基于Zinc Webhook + Golang微服务的实时数据同步架构设计
数据同步机制
ZincSearch 支持 Webhook 事件通知(如文档创建/更新),Golang 微服务作为接收端,实现低延迟、幂等的数据同步。
架构核心组件
- Zinc 实例:启用
webhook插件,配置目标 URL(如/sync) - Golang 微服务:HTTP 服务 + 幂等校验 + 异步写入下游存储(如 PostgreSQL)
- 消息队列(可选):Kafka 缓冲高峰流量,解耦 Zinc 与业务逻辑
Webhook 处理示例(Golang)
func syncHandler(w http.ResponseWriter, r *http.Request) {
var event zinc.WebhookEvent
json.NewDecoder(r.Body).Decode(&event)
// event.ID 是 Zinc 文档唯一标识,用于幂等键
if !isProcessed(event.ID) { // 基于 Redis SETNX 实现去重
go persistToDB(event.Data) // 异步落库,避免阻塞响应
}
w.WriteHeader(http.StatusOK)
}
逻辑分析:
event.ID作为全局唯一幂等键;isProcessed()底层调用SETNX zinc:sync:<id> 1 EX 3600,确保 1 小时内重复事件仅处理一次;异步persistToDB()避免 Webhook 超时失败。
同步状态对比表
| 状态类型 | 触发条件 | 延迟 | 可靠性 |
|---|---|---|---|
| 直连同步 | Zinc → Golang HTTP | 中 | |
| Kafka 中转 | Zinc → Kafka → Golang | ~1s | 高(支持重试/回溯) |
graph TD
A[Zinc Document Update] --> B[Webhook POST /sync]
B --> C{Golang Service}
C --> D[Redis 幂等校验]
D -->|未处理| E[Async DB Persist]
D -->|已存在| F[Return 200]
E --> G[PostgreSQL]
4.3 Zinc权限控制增强:Golang中间件实现RBAC细粒度API访问控制
Zinc通过轻量级Golang中间件将RBAC模型深度嵌入HTTP生命周期,避免框架耦合,支持动态策略加载。
中间件核心逻辑
func RBACMiddleware(permRepo PermissionRepository) gin.HandlerFunc {
return func(c *gin.Context) {
user := c.MustGet("user").(*User)
path := c.Request.URL.Path
method := c.Request.Method
if !permRepo.HasPermission(user.Role, path, method) {
c.AbortWithStatusJSON(http.StatusForbidden, "access denied")
return
}
c.Next()
}
}
该中间件从context提取用户角色,结合当前path+method三元组查询权限仓库。HasPermission内部执行SQL或缓存查表,支持通配符路径(如 /api/v1/users/*)。
权限决策依据
| 角色 | /api/v1/logs | /api/v1/config | /api/v1/users/delete |
|---|---|---|---|
| admin | ✅ | ✅ | ✅ |
| operator | ✅ | ❌ | ❌ |
| viewer | ✅ | ❌ | ❌ |
策略加载流程
graph TD
A[启动时加载角色-权限映射] --> B[Redis缓存策略快照]
B --> C[API请求触发中间件]
C --> D{查缓存命中?}
D -->|是| E[快速鉴权]
D -->|否| F[回源DB加载并刷新缓存]
4.4 检索结果后处理流水线:Golang Filter Chain对Zinc原始响应进行语义增强
Zinc 返回的原始 JSON 响应仅含基础字段(_id, score, highlight),缺乏业务语义。我们通过可插拔的 Go Filter Chain 实现动态增强:
Filter Chain 架构设计
type Filter interface {
Apply(ctx context.Context, doc *SearchResult) error
}
var filters = []Filter{
NewHighlightNormalizer(),
NewEntityLinker(), // 基于NER识别公司/产品名并注入知识图谱ID
NewScoreCalibrator(0.7), // 融合BM25与领域关键词权重
}
该链式结构支持运行时热插拔,每个 Filter 接收上下文与结果对象,原地修改或扩展字段。
核心增强能力对比
| Filter | 输入字段 | 输出新增字段 | 依赖服务 |
|---|---|---|---|
| HighlightNormalizer | highlight |
clean_snippet |
— |
| EntityLinker | content |
linked_entities |
spaCy + Wikidata |
执行流程
graph TD
A[Zinc Raw Response] --> B[Filter Chain Entry]
B --> C[HighlightNormalizer]
C --> D[EntityLinker]
D --> E[ScoreCalibrator]
E --> F[Enriched SearchResult]
第五章:Zinc Search Engine在Golang微服务架构中的演进边界与未来思考
Zinc嵌入式部署在订单搜索服务中的真实瓶颈
某电商中台团队将Zinc 0.5.2作为轻量级全文检索引擎嵌入Golang订单微服务(Go 1.21,Gin框架),初期QPS达1200+,但当订单索引量突破800万、日均写入峰值达4.2万文档时,出现显著延迟毛刺:P99查询响应从120ms跃升至850ms。根因分析发现Zinc默认内存映射(mmap)策略在高并发写入下触发频繁GC停顿,且其单实例无分片能力导致CPU利用率长期维持在92%以上。
服务网格化改造后的资源隔离实践
团队采用Istio Service Mesh对Zinc进行独立Pod封装,并通过Sidecar注入定制限流策略:
apiVersion: networking.istio.io/v1beta1
kind: EnvoyFilter
metadata:
name: zinc-rate-limit
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: local_rate_limiter
token_bucket:
max_tokens: 200
tokens_per_fill: 200
fill_interval: 1s
该配置使Zinc服务在突发流量下保持稳定,错误率从17%降至0.3%。
多租户索引隔离的落地挑战
为支持SaaS多租户场景,团队基于Zinc的index_name前缀机制构建租户路由层。但实测发现当租户数超200时,GET /api/index/_search请求因路径解析开销激增,平均延迟上升3.8倍。最终通过Golang中间件预编译正则路由(^/api/([a-z0-9]+-[a-z0-9]+)/_search$)并缓存租户元数据,将路由耗时从42ms压降至1.3ms。
Zinc与Elasticsearch混合架构的灰度迁移路径
| 阶段 | 流量比例 | 数据同步方式 | 关键指标 |
|---|---|---|---|
| Phase 1 | Zinc 100% | 手动双写 | 查询延迟 ≤150ms |
| Phase 2 | Zinc 70% + ES 30% | Kafka Binlog订阅 | 数据一致性 ≥99.999% |
| Phase 3 | Zinc 0% + ES 100% | Logstash全量迁移 | 索引重建耗时 |
当前处于Phase 2,Zinc承担实时订单搜索,ES承载历史报表分析,通过Kafka Topic zinc-to-es-sync实现异步数据投递,消息积压控制在200条以内。
内存泄漏的深度修复过程
Zinc 0.6.0版本在高负载下出现持续内存增长,pprof分析显示github.com/zincsearch/zinc/pkg/core.(*Index).Search中未释放bytes.Buffer对象。团队提交PR#1289,引入sync.Pool复用缓冲区:
var searchBufferPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 1024))
},
}
// 使用时:
buf := searchBufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer searchBufferPool.Put(buf)
该修复使内存占用下降62%,GC周期从8s延长至47s。
边界认知:何时必须放弃Zinc
当业务出现以下组合特征时,Zinc已触及能力天花板:
- 需要跨字段聚合分析(如按地域+时间+商品类目三维统计)
- 要求近实时(
- 日均增量文档超50万且存在复杂嵌套结构(如订单含动态SKU数组)
- 必须支持跨集群灾备与自动分片再平衡
此时应启动架构升级评估,而非强行扩展Zinc配置参数。
生态协同的可行性验证
团队将Zinc与OpenTelemetry Collector集成,通过OTLP Exporter采集查询延迟、索引大小、内存使用三类指标,接入Grafana看板。当zinc_index_size_bytes{index="orders"}连续3分钟超过12GB阈值时,自动触发告警并调用Golang健康检查接口执行索引优化(POST /api/index/orders/optimize)。该闭环机制已在生产环境稳定运行147天,成功预防3次潜在OOM事件。
