Skip to content

Commit d0d8a4b

Browse files
committed
feat(people): 身份画像后台构建改为有界并发协调器
新增 identityProfileCoordinator,将原先每轮串行构建单个人物的 buildOneDirtyPerson 升级为“有界并发构建 + 串行写入提交 + 批末 ANN 合并调度”,提升 dirty 画像吞吐: - coordinator(identity_profile_coordinator.go):worker 并发执行只读+纯计算 (ListProfileFaces + builder.Build),写入仍经 WriteQueue 串行提交,单 person 失败只 MarkFailed 不阻断同批;批末统一决定 ANN activate 还是 full rebuild。 - 前台让路:service.go 注入 SetForegroundBusyFn,复用 clusteringCoordinator 的 foregroundWaiterCount,前台有 merge/split/move 写操作时跳过新批次,已开始 的小批次允许完成,避免与前台争抢资源。 - 高/低优先级 dirty 分批:新增 repo.ListDirtyByReasons,前台操作产生的 dirty_reason 优先构建,避免被低优先级按 person_id 挤出。 - 时间预算:slice budget(默认 5s)软上限,超时不再派发新 worker。 - 新增配置项 build_workers(1-4)/dirty_batch_size/slice_budget_ms/ ann_rebuild_delta_threshold,含默认值与 Validate 校验。 - NewBackgroundDB 连接池 1→8,WAL 模式支持并发只读 worker 不互锁。 - 运行状态接口新增 IdentityCoordinatorStats(脱敏),定位瓶颈在 build/write/ANN rebuild 哪一阶段。 - 测试桩补齐 ListDirtyByReasons,SliceBuildsAtMostOnePerson 改为 SliceBuildsBoundedDirtyBatch。
1 parent 21aaaa6 commit d0d8a4b

12 files changed

Lines changed: 1247 additions & 82 deletions

backend/config.dev.yaml.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,7 @@ people:
5858
identity_profile_rescue_threshold: 0.65
5959
identity_profile_batch_size: 25
6060
identity_profile_cooldown_ms: 500
61+
identity_profile_build_workers: 2 # 后台画像构建并发 worker 数(1-4),写入仍串行
62+
identity_profile_dirty_batch_size: 10 # 单轮调度最多构建的 dirty 人物数
63+
identity_profile_slice_budget_ms: 5000 # 单轮调度时间预算(软上限)
64+
identity_profile_ann_rebuild_delta_threshold: 0.75 # delta 占用比例达阈值时批末触发 ANN full rebuild

backend/config.prod.yaml.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ people:
9393
identity_profile_rescue_threshold: 0.65
9494
identity_profile_batch_size: 25
9595
identity_profile_cooldown_ms: 500
96+
identity_profile_build_workers: 2
97+
identity_profile_dirty_batch_size: 10
98+
identity_profile_slice_budget_ms: 5000
99+
identity_profile_ann_rebuild_delta_threshold: 0.75
96100

97101
# 地理编码配置(启动默认值,运行时通过管理后台配置后以数据库为准)
98102
geocode:

backend/internal/model/dto.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,14 @@ type BatchRotateRequest struct {
718718
// IdentityProfileOperationalStatsResponse 是身份画像运行状态的顶层响应。
719719
// legacy 模式仅返回 mode 与零值运行状态,不查询 profile/center/member/ANN/backfill/decision。
720720
type IdentityProfileOperationalStatsResponse struct {
721-
Mode string `json:"mode"`
722-
Profiles IdentityProfileCountStats `json:"profiles"`
723-
Centers IdentityCenterStats `json:"centers"`
724-
Members IdentityMemberStats `json:"members"`
725-
Backfill IdentityBackfillStats `json:"backfill"`
726-
ANN IdentityANNStats `json:"ann"`
727-
Decisions IdentityDecisionStats `json:"decisions"`
721+
Mode string `json:"mode"`
722+
Profiles IdentityProfileCountStats `json:"profiles"`
723+
Centers IdentityCenterStats `json:"centers"`
724+
Members IdentityMemberStats `json:"members"`
725+
Backfill IdentityBackfillStats `json:"backfill"`
726+
ANN IdentityANNStats `json:"ann"`
727+
Coordinator IdentityCoordinatorStats `json:"coordinator"`
728+
Decisions IdentityDecisionStats `json:"decisions"`
728729
}
729730

730731
// IdentityProfileCountStats 汇总 profile 各 status 计数。
@@ -799,6 +800,31 @@ type IdentityDecisionStats struct {
799800
RescueApplied int64 `json:"rescue_applied"`
800801
}
801802

803+
// IdentityCoordinatorStats 汇总身份画像后台构建协调器的运行状态。
804+
// 用于判断瓶颈在 backfill / build / write / ANN rebuild 哪一阶段。
805+
// 不含 person ID、embedding、路径或原始 SQL 错误。
806+
type IdentityCoordinatorStats struct {
807+
Running bool `json:"running"`
808+
809+
LastSliceStartedAt *time.Time `json:"last_slice_started_at,omitempty"`
810+
LastSliceEndedAt *time.Time `json:"last_slice_ended_at,omitempty"`
811+
812+
LastDirtySelected int `json:"last_dirty_selected"`
813+
LastBuiltSuccess int `json:"last_built_success"`
814+
LastBuiltFailed int `json:"last_built_failed"`
815+
LastSkipped int `json:"last_skipped"`
816+
817+
Workers int `json:"workers"`
818+
819+
LastBuildDurationMs int64 `json:"last_build_duration_ms"`
820+
MaxBuildDurationMs int64 `json:"max_build_duration_ms"`
821+
LastWriteDurationMs int64 `json:"last_write_duration_ms"`
822+
823+
LastAnnActivated int `json:"last_ann_activated"`
824+
LastAnnRebuild bool `json:"last_ann_rebuild"`
825+
LastAnnRebuildReason string `json:"last_ann_rebuild_reason,omitempty"`
826+
}
827+
802828
// IdentityDecisionResponse 是单条决策遥测的只读响应。
803829
// 明确不返回 ComponentFaceIDs、ComponentHash、DecisionKey、embedding、路径、人物名称。
804830
// CenterIDs 从数据库逗号字符串安全解析(过滤非法/0/重复、升序、最多 32 个)。

backend/internal/repository/person_identity_profile_repo.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ type PersonIdentityProfileRepository interface {
7171
// ListDirty 返回 status=dirty 且 person_id > cursor 的 profile,按 person_id ASC,
7272
// 严格应用 limit,游标分页避免全量加载。
7373
ListDirty(cursor uint, limit int) ([]*model.PersonIdentityProfile, error)
74+
// ListDirtyByReasons 返回 status=dirty、person_id > cursor 且 dirty_reason IN reasons
75+
// 的 profile,按 person_id ASC,严格应用 limit。reasons 为空时不按 reason 过滤
76+
// (等价于 ListDirty)。用于协调器高/低优先级分批拉取,避免全量加载后内存排序。
77+
ListDirtyByReasons(reasons []string, cursor uint, limit int) ([]*model.PersonIdentityProfile, error)
7478
// GetActive 加载人物活动 generation 的完整构建(profile + centers + members)。
7579
// profile 不存在或 active_generation=0 时返回 (nil, nil)。
7680
GetActive(personID uint) (*model.PersonIdentityProfileBuild, error)
@@ -189,6 +193,25 @@ func (r *personIdentityProfileRepository) ListDirty(cursor uint, limit int) ([]*
189193
return profiles, nil
190194
}
191195

196+
// ListDirtyByReasons 返回 status=dirty、person_id > cursor 且 dirty_reason IN reasons 的 profile。
197+
// reasons 为空时不按 reason 过滤(等价于 ListDirty),避免空 IN 子句语义歧义。
198+
// 按 person_id ASC,严格应用 limit,游标分页避免全量加载。
199+
func (r *personIdentityProfileRepository) ListDirtyByReasons(reasons []string, cursor uint, limit int) ([]*model.PersonIdentityProfile, error) {
200+
var profiles []*model.PersonIdentityProfile
201+
q := r.db.Where("status = ? AND person_id > ?", model.PersonIdentityProfileStatusDirty, cursor)
202+
if len(reasons) > 0 {
203+
q = q.Where("dirty_reason IN ?", reasons)
204+
}
205+
q = q.Order("person_id ASC")
206+
if limit > 0 {
207+
q = q.Limit(limit)
208+
}
209+
if err := q.Find(&profiles).Error; err != nil {
210+
return nil, err
211+
}
212+
return profiles, nil
213+
}
214+
192215
func (r *personIdentityProfileRepository) GetActive(personID uint) (*model.PersonIdentityProfileBuild, error) {
193216
var profile model.PersonIdentityProfile
194217
err := r.db.Where("person_id = ?", personID).First(&profile).Error

0 commit comments

Comments
 (0)