外观
yidun-mplatform-event — 中台事件数据服务(事件写入 + 增量同步下发)
分层:业务支撑层 | 部署单元:2 个可构建(另 3 个为类库)| 数据库:MySQL / OceanBase(
event_data_info、event_data_operate_record)| base image:private-registry.nis.netease.com/library/base-image:jdk17(tagbase-250409)
一、模块定位与架构
位于各中台(业务/元数据/结算/SAAS/反垃圾结算/统一运营后台)与各下游订阅方之间的事件汇聚分发中枢:上游按“新增/修改/删除”写入业务事件数据并落库,下游经客户端 SDK 以“操作流水 + 增量拉取”的方式把变更同步到本地缓存。它解决的是跨服务之间如何可靠传播配置/状态类事件数据的问题——字段契约或同步协议一改,所有订阅方都会受影响。它不做事件内容的语义校验,只保证变更“有序、不丢、可重放”,并把传播过程暴露为可回放的流水。
架构分层
- 写入侧:
service/business(EventDataInfoServiceImpl.save:增删改 + 乐观锁 + 操作流水) - 读侧:
facade/http-api(EventCacheManager缓存与增量引擎 +EventController) - 门面侧:
facade/dubbo-provider(EventDataInfoProvider提供EventDataInfoFacade) - 客户端:
client/core(EventClient/EventHttpClient/EventBus)、client/plugin(EventListenerPluginSPI)、client/spring-boot-starter(@EnableEventClient)
关键数据流
- 事件源 →
EventDataInfoService.save落库event_data_info(带version乐观锁version+1)并写event_data_operate_record流水 EventCacheManager.refresh周期扫描流水(startUpdateTime=lastRefreshTime,endUpdateTime=now-refreshDelayTime)刷新缓存- 客户端
init(/api/event/init,按 id 分批全量)或fetch(/api/event/fetch,按lastOperateTimestamp拉增量) - 拉到的增量经
EventBus分发给各EventListenerPlugin监听器 - 服务端另有一条
deleteEventDataOperateRecord()周期删除operateRecordSaveDay天前的流水
读写协议与并发控制
- 两种读协议:
init(全量,按 id 分批)与fetch(增量,按lastOperateTimestamp);服务端用EventCacheProperties.refreshInterval(默认 10s)、loadBatchSize(默认 1000)、refreshDelayTime(默认 10s)、historicalDataSize(默认 4)、lastOperateTimeProtection(默认 2h)控制节奏与保护 - 每条
event_data_info带version,更新时乐观锁version+1,并发冲突直接失败(不做重试合并) EventCacheManager内部用ReentrantLock串行化refresh与“往前补数据”逻辑,并用hasRefreshedData标志避免空转
二、可部署服务清单
| 部署单元目录 | artifactId | 镜像名 | 端口 | 服务类型 | 独立 Dockerfile | 是否进 kubernetes.yml |
|---|---|---|---|---|---|---|
facade/http-api | mplatform-event-facade-http | private-registry.nis.netease.com/yidun/mplatform-event-facade-http:base-250409 | 仓库内无 server.port(由 Apollo 下发) | HTTP 读接口 + 本地缓存 | 有 | 否(无 kubernetes.yml) |
facade/dubbo-provider | mplatform-event-facade-dubbo-provider | private-registry.nis.netease.com/yidun/mplatform-event-facade-dubbo-provider:base-250409 | Dubbo port=-1 | Dubbo Provider | 有 | 否 |
client/spring-boot-starter | mplatform-event-client-spring-boot-starter | — 类库,不部署 | — | 客户端订阅 starter | 无 | 否 |
client/core | mplatform-event-client-core | — 类库,不部署 | — | 客户端核心(拉取循环/EventBus) | 无 | 否 |
client/plugin | mplatform-event-client-plugin | — 类库,不部署 | — | 监听器插件 SPI | 无 | 否 |
本仓库无
kubernetes.yml;buildAll.sh用find . -name build.sh只会构建facade/http-api与facade/dubbo-provider;三个client/*是纯库。
三、同模块启动顺序
facade/dubbo-provider—— 事件写入入口(EventDataInfoFacade.save),需先可用,否则上游事件无法落库、下游无增量可取。facade/http-api—— 事件读取入口(/api/event/init、/api/event/fetch),启动时会从 DB(或 RocksDB)做一次全量初始化,应在写入端可用之后再起,避免初始化阶段拉到不完整数据。
理由:两侧都强依赖 Apollo 下发的 DB/ZK 配置,Apollo 不可达时二者都起不来;写入与读取虽可并行,但读取侧冷启动会全量扫库,晚起更稳妥。
四、逐服务启动逻辑
facade/http-api
- 启动类:
com.netease.yidun.mplatform.event.facade.http.HttpApplication(facade/http-api/src/main/java/com/netease/yidun/mplatform/event/facade/http/HttpApplication.java) @SpringBootApplication:无 exclude;@EnableConfigurationProperties({EventCacheProperties.class})main():有自定义——SpringContextKits.setContext(SpringApplication.run(...))(把ApplicationContext写入静态工具类)- 配置类:
FacadeHttpConfiguration(@Configuration+@EnableApolloConfig+@ComponentScan({"com.netease.yidun.mplatform.event.service","...manager"})+@MapperScan("com.netease.yidun.mplatform.event.dao")) - 控制器:
EventController(init/fetch)、EventDataInfoController、HealthCheckApi - MyBatis:
mybatis.mapper-locations(classpath*:mybatis/mappers/*.xml)由FacadeHttpConfiguration的@MapperScan配合service/base的 Dao 生效 - 打包:
pom.xml开启spring-boot-maven-plugin的layers(<layers><enabled>true</enabled><includeLayerTools>true</includeLayerTools></layers>),并用maven-dependency-plugin把 sentry 相关依赖复制到target/lib - 启动钩子:
EventCacheManager.init()(@PostConstruct):判定event.cache.loadFromStorage或 DB 连接失败时从 RocksDB 载入,否则清 RocksDB 并从 DB 全量初始化;随后用ScheduledThreadPoolExecutor(1)调scheduleAtFixedRate(refresh, refreshInterval)(默认 10s)与scheduleAtFixedRate(deleteEventDataOperateRecord, 0, deletePeriod)(默认 1 天);@PreDestroy destroy()关线程池与缓存RocksdbStorage.@PostConstruct init():仅当event.cache.storage.enable=true时生效(@ConditionalOnProperty),RocksDB.loadLibrary()+RocksDB.open(path)- 就绪门槛:
initialized=false期间initFetch/getCache由AssertKits.isTrue(initialized, ResultStatus.EVENT_CACHE_NOT_READY)拦截
facade/dubbo-provider
- 启动类:
com.netease.yidun.mplatform.event.facade.dubbo.DubboApplication @SpringBootApplication(无 exclude)+@EnableAspectJAutoProxymain():有自定义——SpringContextKits.setContext(SpringApplication.run(...))- 配置类:
FacadeDubboConfiguration(@Configuration+@EnableApolloConfig+@ComponentScan({"...service","...manager"})+@MapperScan("...dao")+@Bean Validator,hibernate.validator.fail_fast=true) - Provider:
com.netease.yidun.mplatform.event.facade.dubbo.provider.EventDataInfoProvider;GlobalExceptionHandler统一处理 Dubbo 异常 - 启动钩子:未发现
client/*(类库)
@EnableEventClient = @EnableConfigurationProperties({EventClientProperties.class}) + @Import(EventClientConfiguration.class)EventClientConfiguration.eventHttpClientWrapper():@Bean(destroyMethod="destroy")+@ConditionalOnClass(EventHttpClient.class)+@ConditionalOnProperty(prefix="event.client.options.protocol.http", name="address");enableCustomEventListener为真是注册所有EventListenerPlugin;autoStart默认 true 时立即start()拉事件
五、启动前置依赖
| 依赖 | 配置键 / 地址 | 阻塞 or 弱依赖 | 配置文件 |
|---|---|---|---|
| Apollo(配置源) | http-api app.id=yd-mp-event_http、namespaces yd-mp-event_http,yd-bsd.yd-mp-event,yidun.common;dubbo app.id=yd-mp-event_dubbo、namespaces yd-mp-event_dubbo,yd-bsd.yd-mp-event,yidun.common;apollo.bootstrap.enabled=true;online apollo.meta=http://yd-apollo-meta.service.163.org;dev http://yd-apollo-config.nistest.netease.com | 阻塞(DB/ZK/端口等关键配置均由 Apollo 下发) | application.properties + application-<profile>.properties |
| MySQL / OceanBase | 依赖 oceanbase-client;DB 连接信息来自 Apollo bootstrap | 阻塞 | Apollo |
| ZK / Dubbo | Dubbo 注册与 dubbo.scan.basePackages 来自 Apollo | 阻塞 | Apollo |
| RocksDB(本地) | event.cache.storage.path、event.cache.storage.enable(默认 false) | 弱(默认不启用) | application*.properties 或 Apollo |
| log4j2 | logging.config=classpath:log4j2/log4j2-<profile>.xml | 弱(缺失则回落默认) | application-<profile>.properties |
apollo.cache-dir=/home/appops/approot/data/apollo/(online)/./apollo/cache(dev):Apollo 不可达时用本地缓存兜底,但会存在配置陈旧风险。
六、启动参数与 Profile
spring.profiles.active:application.properties根默认dev;http-api 含dev/test/online/on-prem/frankfurt-online/guizhou-online/tianjin-online/tokyo-online/virginia-online/xjp-online;dubbo-provider 含dev/test/online/on-prem- Dockerfile:
ENV JAVA_OPTS=-Xmx1024m -Xms1024m -XX:+UseG1GC -Dspring.profiles.active=private;构建时写入/app/version.txt(GIT_COMMIT/BUILD_TIME/GIT_BRANCH) - base image 与仓库:
private-registry.nis.netease.com/library/base-image:jdk17(JDK17,尽管pom.xml里java.version=1.8);DOCKER_REPOSITORY=private-registry.nis.netease.com/yidun,tagbase-250409 - maven profile:
facade/http-api/pom.xml另有private-bes(排除spring-boot-starter-tomcat,引入bes-lite-spring-boot-2.x-starter) build.sh:MODEL_NAME=facade/http-api(dubbo-provider 为facade/dubbo-provider)、cd ../..→mvn ... -Pprivate→docker buildx build --platform linux/amd64,linux/arm64 --push→source ~/.zsh_private && bash $k8s_script_path/podupdate.sh <image> <tag>buildAll.sh固定GLOBAL_ENV=private并逐个执行bash build.sh -c false -p false -e private(-c false跳过子模块内再次编译,-p false跳过git pull)- 多地域 profile 的
apollo.meta不同(如 online 指向yd-apollo-meta.service.163.org)
七、启动期踩坑
- Dockerfile 指定
-Dspring.profiles.active=private,但仓库内没有application-private.properties→ 实际落到application.properties(spring.profiles.active=dev,apollo.meta指向 test 环境)。 @EnableApolloConfig依赖apollo.bootstrap.enabled=true生效顺序:bootstrap 早于ApplicationContext,若 namespace(yd-mp-event_http/yd-bsd.yd-mp-event/yidun.common)拼写错或缺失,DB/ZK 配置拉不到,启动直接失败。EventCacheManager冷启动可能全量扫库:event.cache.loadFromStorage=false且 DB 可连时走 DB 全量初始化(eventCache.init(true)),DB 慢会显著拖长就绪;只有loadFromStorage=true或 DB 连不上时才走 RocksDB。- 就绪窗口内请求被拒:
initialized=false期间initFetch/getCache抛EVENT_CACHE_NOT_READY,客户端需容忍该错误码并重试。 - RocksDB 是“保留但不推荐”的旁路:
rocksdbjni版本 6.27.3(根 pom 注释“已不需要但保留”),且event.cache.storage.enable默认 false,RocksDB 组件不会创建,EventCacheManager用@Autowired(required=false)兼容其缺席。 SpringContextKits.setContext在run之后执行:启动期若已有组件通过静态上下文取 bean,会拿到 null 触发 NPE。- 多地域 profile 各有不同
apollo.meta:选错 profile 会连到别的地域 Apollo,导致读到错误的 DB/ZK。 private-bes切换需同步依赖:切到 BES 会 excludespring-boot-starter-tomcat,忘记引入bes-lite将无内嵌 Web 容器。- 两服务共用一个
app.id之外的 namespace 容易串:yd-bsd.yd-mp-event与yidun.common被 http-api 与 dubbo-provider 同时引用,改这两个 namespace 会同时影响读侧与写侧。 deleteEventDataOperateRecord的deletePeriod以“天”为单位:EventCacheProperties.deletePeriod=1L,把它误当毫秒会变成“1ms 一次删流水”,会直接掏空event_data_operate_record导致增量同步丢数据。EventDataInfoProvider的写入语义是硬契约:EventSourceService枚举限定了允许的事件源,新增事件源必须同步改枚举并协调上游,否则写入被拒。
八、跨模块前置
- 上游(写入方):
mplatform_business/mplatform_metadata/mplatform_package/antispam_cms/antispam_bill/union_admin,事件源范围由EventSourceService枚举限定,经 DubboEventDataInfoFacade.save或 HTTP/event/data/v1/save写入。 - 下游(订阅方):引入
mplatform-event-clientSDK 的业务服务(如antispam-business各子域、antispam-bill的套餐变更事件PACKAGE_EVENT),通过/api/event/init+/api/event/fetch拉取增量。 - 基础设施前置(阻塞):Apollo 是硬前置——DB、ZK、端口等关键配置都由 Apollo bootstrap 提供;Apollo 不可达时只能靠
apollo.cache-dir兜底缓存的陈旧配置启动。 - 运行期弱依赖:RocksDB 本地目录(默认不启用)、log4j2 配置文件(按 profile 挑
log4j2-<profile>.xml)。 - 协议版本前置:
client/core的拉取协议(/api/event/init的按 id 分批语义、/api/event/fetch的lastOperateTimestamp语义)必须与服务端EventCacheManager一致,SDK 升级需与服务端同步发布。 - 无业务模块强依赖:除 Apollo / DB / ZK 外,不阻塞其它易盾模块启动;但当上游未接入时事件表为空、下游拿到空事件集属正常,不应据此判定故障。