上一篇【第38篇】OAP Server新增模块实战从零打造健康检查模块下一篇【第40篇】手把手开发OAP Server扩展打造企业微信告警通知一、开篇自助餐里的选菜逻辑回到第一篇的开场比喻——OAP是一家自助餐厅。现在的问题是谁来决定今天用哪个厨师模块实现答案藏在一个神奇的字段里——selector。这个selector就像餐厅门口的电子屏根据当天的环境季节、食材供应、客人偏好自动选择开哪个档口。今天环境变量SW_STORAGEelasticsearch那就开ES档口明天换成SW_STORAGEmysqlMySQL档口无缝接手。但selector的魔力远不止一个字符串匹配那么简单。让我们掀开它的面纱。环境变量/配置文件 OAP框架运行时 SW_STORAGEelasticsearch ---- --------------------- | SelectorBased | application.yml: | ModuleDefine | storage: | | selector: ${SW_STORAGE:es} | 根据selector值 | | 路由到正确的 | | ModuleProvider | -------------------- | ---------------------------------------------------- | | | v v v --------------- --------------- --------------- | ES Provider | | MySQL Provider | | H2 Provider | | elasticsearch | | mysql | | h2 | ----------------- ----------------- ----------------- 图1Selector的选择逻辑——一条配置决定千行代码的命运二、SelectorBasedModuleDefine模块的智能身份证普通的ModuleDefine只定义模块身份而SelectorBasedModuleDefine在它的基础上增加了一个维度——“我可以在多种实现中选一种”。/** * 带选择器的模块定义 * * 当模块有多个可选实现时继承此类而不是ModuleDefine */publicabstractclassSelectorBasedModuleDefineextendsModuleDefine{/** * 返回selector的默认值 * 例如当用户没有配置时存储模块默认用h2 */protectedStringdefaultSelector(){returnnull;}/** * 是否必须有selector配置 * 如果为true用户必须显式配置selector */protectedbooleanrequireSelector(){returnfalse;}/** * 尝试匹配selector值 * 框架会遍历所有已注册的Provider找到name与selector匹配的那个 */publicModuleProviderselectProvider(StringselectorValue)throwsProviderNotFoundException{for(ModuleProviderprovider:loadedProviders()){if(provider.name().equals(selectorValue)){logger.info(模块 [{}] 选择了Provider: {},name(),provider.name());returnprovider;}}thrownewProviderNotFoundException(String.format(模块 [%s] 找不到匹配的Providerselector%s可用Provider: %s,name(),selectorValue,getAvailableProviderNames()));}}SelectorBasedModuleDefine的本质是给ModuleDefine增加了一个路由表——当多个Provider争当同一个模块的实现时由selector来裁决。三、以存储模块为例拆解selector的运作机制存储模块是最经典的选择器案例。它有三种实现Elasticsearch、MySQLJDBC、H2。我们来看看它如何运作。3.1 存储模块的定义// 注意它继承的是ModuleDefine但通过另一种方式实现了选择器支持publicclassStorageModuleextendsModuleDefine{publicstaticfinalStringNAMEstorage;publicStorageModule(){super(NAME);}OverridepublicClass?extendsService[]services(){returnnewClass[]{IStorageDAO.class,IModelInstaller.class,IBatchDAO.class,IHistoryDeleteDAO.class,IRegisterDAO.class,IMetricsDAO.class,IRecordDAO.class,IAlarmQueryDAO.class,IProfileThreadSnapshotQueryDAO.class,// ... 更多DAO接口};}}3.2 选择器配置的完整语法# application.yml - 存储模块的选择器配置storage:# 选择器语法${环境变量:默认值}# 这是SkyWalking最巧妙的设计之一——环境变量驱动配置selector:${SW_STORAGE:elasticsearch}# ES实现的配置 elasticsearch:nameSpace:${SW_NAMESPACE:}clusterNodes:${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200}protocol:${SW_STORAGE_ES_HTTP_PROTOCOL:http}user:${SW_ES_USER:}password:${SW_ES_PASSWORD:}trustStorePath:${SW_STORAGE_ES_SSL_JKS_PATH:}trustStorePass:${SW_STORAGE_ES_SSL_JKS_PASS:}# 索引配置indexShardsNumber:${SW_STORAGE_ES_INDEX_SHARDS:2}indexReplicasNumber:${SW_STORAGE_ES_INDEX_REPLICAS:0}# 批量写入bulkActions:${SW_STORAGE_ES_BULK_ACTIONS:2000}bulkSize:${SW_STORAGE_ES_BULK_SIZE:20}flushInterval:${SW_STORAGE_ES_FLUSH_INTERVAL:10}concurrentRequests:${SW_STORAGE_ES_CONCURRENT_REQUESTS:2}# 高级设置resultWindowMaxSize:${SW_STORAGE_ES_QUERY_MAX_WINDOW_SIZE:10000}metadataQueryMaxSize:${SW_STORAGE_ES_QUERY_MAX_SIZE:5000}segmentQueryMaxSize:${SW_STORAGE_ES_QUERY_SEGMENT_SIZE:200}advanced:${SW_STORAGE_ES_ADVANCED:}# MySQL实现的配置 mysql:properties:jdbcUrl:${SW_JDBC_URL:jdbc:mysql://localhost:3306/swtest}dataSource.user:${SW_DATA_SOURCE_USER:root}dataSource.password:${SW_DATA_SOURCE_PASSWORD:root1234}dataSource.cachePrepStmts:truedataSource.prepStmtCacheSize:250dataSource.prepStmtCacheSqlLimit:2048dataSource.useServerPrepStmts:truemetadataQueryMaxSize:${SW_STORAGE_MYSQL_QUERY_MAX_SIZE:5000}maxSizeOfBatchSql:${SW_STORAGE_MAX_BATCH_SQL_SIZE:2000}asyncBatchPoolSize:${SW_STORAGE_ASYNC_BATCH_POOL_SIZE:4}asyncBatchPoolType:${SW_STORAGE_ASYNC_BATCH_POOL_TYPE:OFF}# H2内存实现的配置 h2:driver:${SW_STORAGE_H2_DRIVER:org.h2.jdbcx.JdbcDataSource}url:${SW_STORAGE_H2_URL:jdbc:h2:mem:skywalking-oap-db;DB_CLOSE_DELAY-1}user:${SW_STORAGE_H2_USER:sa}password:${SW_STORAGE_H2_PASSWORD:}metadataQueryMaxSize:${SW_STORAGE_H2_QUERY_MAX_SIZE:5000}maxSizeOfBatchSql:${SW_STORAGE_MAX_BATCH_SQL_SIZE:2000}3.3 选择器的解析过程// ModuleManager中的配置加载代码简化版voidloadProvider(ModuleDefinemodule,MapString,ObjectmoduleConfig){// 1. 读取selector值ObjectselectorObjmoduleConfig.get(selector);StringselectorValueselectorObj!null?selectorObj.toString():default;// 2. 环境变量替换处理${SW_STORAGE:elasticsearch}selectorValuePlaceholderResolver.resolve(selectorValue);// 3. 根据selector值选择ProviderModuleProviderselectedProvidermodule.loadedProviders().stream().filter(p-p.name().equals(selectorValue)).findFirst().orElseThrow(()-newProviderNotFoundException(找不到Provider: selectorValue));// 4. 绑定对应Provider的配置MapString,ObjectproviderConfig(MapString,Object)moduleConfig.get(selectorValue);if(providerConfig!null){ModuleConfigconfigBeanselectedProvider.createConfigBeanIfAbsent();bindYamlToConfig(configBean,providerConfig);}// 5. 设置选中的Providermodule.setSelectedProvider(selectedProvider);}四、环境变量驱动配置容器化的最佳拍档SkyWalking的配置体系中环境变量占位符${VAR:default}是最精妙的设计。它把配置决策从文件层面提升到了部署层面。工作原理图解Kubernetes ConfigMap 环境变量替换引擎 Java配置对象 SW_STORAGE: mysql -- 扫描配置中的 -- StorageModule ${SW_STORAGE:xxx} uses SW_JDBC_URL: jdbc: -- 替换为环境变量值 -- StorageConfig mysql://prod:3306 如果未设置则用默认值 .jdbcUrl jdbc:mysql://prod:3306 图2环境变量如何穿透到Java配置对象实战技巧# 技巧1分层配置——开发/测试/生产用不同的环境变量文件# dev.envexportSW_STORAGEh2exportSW_CLUSTERstandalone# test.envexportSW_STORAGEelasticsearchexportSW_STORAGE_ES_CLUSTER_NODESes-test:9200exportSW_CLUSTERnacosexportSW_CLUSTER_NACOS_HOST_PORTnacos-test:8848# prod.envexportSW_STORAGEelasticsearchexportSW_STORAGE_ES_CLUSTER_NODESes1:9200,es2:9200,es3:9200exportSW_STORAGE_ES_USERadminexportSW_STORAGE_ES_PASSWORDsecret123exportSW_CLUSTERnacosexportSW_CLUSTER_NACOS_HOST_PORTnacos1:8848,nacos2:8848# 切换环境sourcedev.env./bin/oapService.sh# 开发sourcetest.env./bin/oapService.sh# 测试sourceprod.env./bin/oapService.sh# 生产五、集群模块的选择器对比集群模块同样使用选择器模式而且它的选择更丰富cluster:selector:${SW_CLUSTER:standalone}# 四种选择standalone:# 单机模式 - 零配置zookeeper:hostPort:${SW_CLUSTER_ZK_HOST_PORT:localhost:2181}sessionTimeout:${SW_CLUSTER_ZK_SESSION_TIMEOUT:40000}namespace:${SW_NAMESPACE:}baseSleepTimeMs:${SW_CLUSTER_ZK_SLEEP_TIME:1000}maxRetries:${SW_CLUSTER_ZK_MAX_RETRIES:3}enableACL:${SW_CLUSTER_ZK_ENABLE_ACL:false}nacos:serviceName:${SW_SERVICE_NAME:SkyWalking_OAP_Cluster}hostPort:${SW_CLUSTER_NACOS_HOST_PORT:localhost:8848}namespace:${SW_CLUSTER_NACOS_NAMESPACE:}kubernetes:namespace:${SW_CLUSTER_K8S_NAMESPACE:default}labelSelector:${SW_CLUSTER_K8S_LABEL:appskywalking-oap}uidEnvName:${SW_CLUSTER_K8S_UID:SKYWALKING_OAP_INSTANCE_UID}六、深入源码环境变量替换引擎这部分是SkyWalking配置系统的精髓publicclassPlaceholderResolver{// 占位符正则${VAR_NAME:default_value}privatestaticfinalPatternPLACEHOLDER_PATTERNPattern.compile(\\$\\{([^:}]):?([^}]*)\\});publicstaticStringresolve(Stringvalue){if(valuenull||!value.contains(${)){returnvalue;}MatchermatcherPLACEHOLDER_PATTERN.matcher(value);StringBufferresultnewStringBuffer();while(matcher.find()){StringenvKeymatcher.group(1);// 环境变量名StringdefaultValuematcher.group(2);// 默认值// 先从系统属性中找StringresolvedSystem.getProperty(envKey);// 再从环境变量中找if(resolvednull){resolvedSystem.getenv(envKey);}// 都没有就用默认值if(resolvednull){resolveddefaultValue;}// 递归解析支持嵌套占位符resolvedresolve(resolved);matcher.appendReplacement(result,Matcher.quoteReplacement(resolved));}matcher.appendTail(result);returnresult.toString();}}七、自定义选择器模块的实战模板假设你要创建一个通知渠道模块支持钉钉、企业微信、飞书三种选择/** * 通知模块 - 使用选择器模式 */publicclassNotificationModuleextendsModuleDefine{publicstaticfinalStringNAMEnotification;publicNotificationModule(){super(NAME);}OverridepublicClass?extendsService[]services(){returnnewClass[]{INotificationSender.class};}}/** * 通知服务接口 */publicinterfaceINotificationSenderextendsService{voidsend(Stringtitle,Stringcontent,ListStringrecipients);}/** * 钉钉Provider */publicclassDingTalkNotificationProviderextendsModuleProvider{OverridepublicStringname(){returndingtalk;}OverridepublicClass?extendsModuleDefinemodule(){returnNotificationModule.class;}// ...实现dingtalk的send逻辑}/** * 企业微信Provider */publicclassWechatNotificationProviderextendsModuleProvider{OverridepublicStringname(){returnwechat;}OverridepublicClass?extendsModuleDefinemodule(){returnNotificationModule.class;}// ...实现wechat的send逻辑}/** * 飞书Provider */publicclassLarkNotificationProviderextendsModuleProvider{OverridepublicStringname(){returnlark;}OverridepublicClass?extendsModuleDefinemodule(){returnNotificationModule.class;}// ...实现lark的send逻辑}配置示例# application.ymlnotification:selector:${SW_NOTIFICATION:dingtalk}# 默认钉钉dingtalk:webhook:${SW_DINGTALK_WEBHOOK:}secret:${SW_DINGTALK_SECRET:}wechat:corpId:${SW_WECHAT_CORP_ID:}corpSecret:${SW_WECHAT_CORP_SECRET:}agentId:${SW_WECHAT_AGENT_ID:0}lark:webhook:${SW_LARK_WEBHOOK:}secret:${SW_LARK_SECRET:}八、选择器设计的最佳实践------------------------------------------------------------------- | 选择器模式使用最佳实践 | ------------------------------------------------------------------- | | | [1] 环境变量命名规范 | | 前缀: SW_{模块名}_{配置项} | | 示例: SW_STORAGE_ES_CLUSTER_NODES | | | | [2] 默认值策略 | | - 开发友好型默认值如H2存储、Standalone集群 | | - 生产环境必须在文档中说明如何覆盖 | | | | [3] 配置隔离 | | - 每个Provider的配置独立成块 | | - 切换selector后不需要的配置默默忽略 | | | | [4] 启动校验 | | - selector指向的Provider不存在时给出明确的错误信息 | | - 列出所有可用的Provider名称供运维参考 | | | | [5] 文档完善 | | - 每个selector的可选值在配置文件中注释 | | - 提供docker-compose和k8s deployment模板 | | | ------------------------------------------------------------------- 图3选择器设计最佳实践一览九、常见疑难解答Q1多个模块都要用selector吗不需要。只有那些在设计上确实存在多种实现的模块才需要selector。比如core模块通常只有一个实现就不需要selector。Q2selector的值一定要和Provider的name()严格一致吗是的。框架是通过字符串匹配来选择Provider的selector: elasticsearch→ 匹配provider.name() elasticsearchselector: nacos→ 匹配provider.name() nacosQ3环境变量没设置配置注释里的默认值也没被解析请检查YAML语法。${SW_STORAGE:elasticsearch}是标准语法冒号前面是环境变量名冒号后面是默认值如果环境变量存在但为空字符串框架会用空字符串而不是默认值十、总结选择器Selector是OAP模块化框架的柔性关节。它让运行时配置可以决定代码路径让DevOps团队在不改代码的情况下适配不同基础设施。核心要点SelectorBasedModuleDefine为模块增加多选一的实现路由能力环境变量占位符${VAR:default}让配置文件容器化友好每个Provider独立配置块保证切换时互不干扰启动时报错友好找不到Provider时会列出所有可用选项掌握了选择器你就掌握了OAP模块体系的遥控器——按一个环境变量就能切换底层实现。上一篇【第38篇】OAP Server新增模块实战从零打造健康检查模块下一篇【第40篇】手把手开发OAP Server扩展打造企业微信告警通知