Skip to content

服务注册 ​

CoSky 的服务注册管理微服务集群中服务实例的生命周期。基于 Redis 和 Lua 脚本,它提供原子性、无竞态条件的注册、注销、心跳续约和元数据管理——所有操作都在多租户命名空间模型下运行。

方面详情
接口ServiceRegistry
Redis 实现RedisServiceRegistry
存储引擎Redis Hash + Set + Lua 脚本
并发模型响应式(Mono<Boolean>)
心跳RenewInstanceService(定时保活)
序列化ServiceInstanceCodec

ServiceRegistry 接口 ​

ServiceRegistry 接口定义了所有注册操作的契约。

方法返回类型描述源码
registerMono<Boolean>注册服务实例,可选 TTLServiceRegistry.kt:33
deregisterMono<Boolean>从注册中心移除服务实例ServiceRegistry.kt:43
renewMono<Boolean>续约/延长临时实例的 TTLServiceRegistry.kt:41
setServiceMono<Boolean>在命名空间索引中创建命名服务条目ServiceRegistry.kt:24
removeServiceMono<Boolean>从命名空间索引中移除命名服务ServiceRegistry.kt:25
setMetadataMono<Boolean>设置服务实例的元数据键值对ServiceRegistry.kt:53

ServiceInstance 数据模型 ​

ServiceInstance 继承基础 Instance 接口,包含以下字段:

字段类型默认值描述
instanceIdString--唯一标识符:{serviceId}@{schema}#{host}#{port}
serviceIdString--逻辑服务名称
schemaString--协议模式(http、https 等)
hostString--主机地址
portInt--端口号
weightInt1负载均衡权重
isEphemeralBooleantrue临时实例会过期;持久实例不会
ttlAtLongTTL_AT_FOREVER (-1)绝对 TTL 过期时间戳(纪元秒)
metadataMap<String, String>emptyMap()用户自定义键值元数据;拒绝以 _ 开头的键

ServiceInstance 上的 isExpired 属性将 ttlAt 与当前系统时间比较,以确定临时实例是否已过期 (ServiceInstance.kt:34)。

ServiceInstanceCodec 序列化 ​

ServiceInstanceCodec 负责实例数据与 Redis 哈希字段之间的编码和解码。它使用 _ 前缀表示元数据键,保留 __ 用于系统元数据:

kotlin
// 编码:元数据键 "version" 在 Redis 中变为 "_version"
fun encodeMetadataKey(key: String): String {
    require(!key.startsWith(METADATA_PREFIX))
    return METADATA_PREFIX + key
}

元数据键不得以 _ 开头。该前缀保留给 __last_renew_pub_ttl_at 等内部字段;拒绝此类键可防止用户元数据与注册中心状态冲突。

decode 函数 (ServiceInstanceCodec.kt:57) 将 Redis HGETALL 返回的扁平键值列表解析为 ServiceInstance 对象。

Redis 实现 ​

RedisServiceRegistry ​

RedisServiceRegistry 是 ServiceRegistry 的 Redis 实现。它使用 Lua 脚本进行原子操作,并维护已注册临时实例的内存映射:

kotlin
class RedisServiceRegistry(
    private val registryProperties: RegistryProperties,
    private val redisTemplate: ReactiveStringRedisTemplate
) : ServiceRegistry

关键设计要点:

  • 临时实例被跟踪在 ConcurrentHashMap<NamespacedInstanceId, ServiceInstance> 中,用于心跳续约 (RedisServiceRegistry.kt:40)。
  • register 方法只在 Lua 脚本执行成功后才将实例添加到临时映射——在 doOnNext 中,且仅当脚本返回 true 时 (RedisServiceRegistry.kt:97)。
  • 如果续约失败(实例键不存在),会自动重新注册实例 (RedisServiceRegistry.kt:184)。

DiscoveryRedisScripts ​

DiscoveryRedisScripts 从类路径加载所有注册相关的 Lua 脚本:

脚本资源文件用途
SCRIPT_REGISTRY_REGISTERregistry_register.lua原子实例注册
SCRIPT_REGISTRY_DEREGISTERregistry_deregister.lua原子实例移除
SCRIPT_REGISTRY_RENEWregistry_renew.lua带发布节流的 TTL 续约
SCRIPT_REGISTRY_SET_METADATAregistry_set_metadata.lua设置实例元数据字段
SCRIPT_REGISTRY_SET_SERVICEregistry_set_service.lua在命名空间索引中创建服务
SCRIPT_REGISTRY_REMOVE_SERVICEregistry_remove_service.lua从命名空间索引中移除服务

RegistryProperties ​

RegistryProperties 配置默认实例 TTL:

属性类型默认值描述
instanceTtlDuration1 minute临时实例的生存时间

RenewInstanceService(心跳) ​

RenewInstanceService 为临时实例提供保活机制。它在专用调度器(CoSky-Renew)上运行,定期续约所有已注册的临时实例:

属性默认值描述
initialDelay1 second首次续约周期前的延迟
period10 seconds续约周期间隔

续约周期必须小于 RegistryProperties.instanceTtl,以防止过早过期。该服务遍历所有 registeredEphemeralInstances 并通过 ServiceRegistry 对每个实例调用 renew (RenewInstanceService.kt:70)。

时序图 ​

注册流程 ​

mermaid
sequenceDiagram
    autonumber
    participant App as 应用程序
    participant SR as RedisServiceRegistry
    participant EMap as EphemeralInstances 映射
    participant Redis as Redis (Lua 脚本)
    participant Sub as PubSub 订阅者

    App->>SR: register(namespace, serviceInstance)
    SR->>SR: 构建 ARGV(ttl、serviceId、instanceId、schema、host、port、weight、metadata)
    SR->>Redis: EVAL registry_register.lua KEYS=[namespace] ARGV=[...]
    Redis->>Redis: SADD svc_itc_idx:{serviceId} {instanceId}
    Redis->>Redis: SADD svc_idx {serviceId}
    Redis->>Redis: HMSET svc_itc:{instanceId} {fields}
    Redis->>Sub: PUBLISH svc_itc:{instanceId} "register"
    Redis->>Redis: EXPIRE svc_itc:{instanceId} {ttl}
    Redis-->>SR: Boolean (成功)
    alt registered == true
        SR->>EMap: addEphemeralInstance(namespace, instance)
    end
    SR-->>App: Mono<Boolean>

续约 / 心跳流程 ​

mermaid
sequenceDiagram
    autonumber
    participant RS as RenewInstanceService
    participant SR as RedisServiceRegistry
    participant Redis as Redis (Lua 脚本)
    participant Sub as PubSub 订阅者

    RS->>RS: 调度器触发(每 period 秒)
    RS->>RS: 遍历 registeredEphemeralInstances
    RS->>SR: renew(namespace, instance)
    SR->>Redis: EVAL registry_renew.lua KEYS=[namespace] ARGV=[instanceId, ttl]
    Redis->>Redis: TTL svc_itc:{instanceId}(检查是否存在)
    alt 实例存在
        Redis->>Redis: EXPIRE svc_itc:{instanceId} {ttl}
        Redis->>Redis: 检查发布节流窗口
        alt 超过节流窗口
            Redis->>Redis: HSET __last_renew_pub_ttl_at {currentTtlAt}
            Redis->>Sub: PUBLISH svc_itc:{instanceId} "renew"
        end
        Redis-->>SR: 1 (成功)
    else 实例缺失(TTL <= 0)
        Redis-->>SR: -1 或 -2 (失败)
        SR->>SR: 自动重新注册实例
    end

注销流程 ​

mermaid
sequenceDiagram
    autonumber
    participant App as 应用程序
    participant SR as RedisServiceRegistry
    participant EMap as EphemeralInstances 映射
    participant Redis as Redis (Lua 脚本)
    participant Sub as PubSub 订阅者

    App->>SR: deregister(namespace, instance)
    SR->>EMap: removeEphemeralInstance(namespace, instance)
    SR->>Redis: EVAL registry_deregister.lua KEYS=[namespace] ARGV=[serviceId, instanceId]
    Redis->>Redis: SREM svc_itc_idx:{serviceId} {instanceId}
    alt removed == 1
        Redis->>Sub: PUBLISH svc_itc:{instanceId} "deregister"
        Redis->>Redis: DEL svc_itc:{instanceId}
        Redis-->>SR: true
    else 已被移除
        Redis-->>SR: false
    end
    SR-->>App: Mono<Boolean>

Redis 键结构 ​

注册中心使用结构化的键模式,在每个命名空间内组织服务和实例数据:

Redis 键类型用途示例
{namespace}:svc_idxSET命名空间中所有服务 ID 的集合production:svc_idx
{namespace}:svc_statHASH服务 ID 到实例数统计production:svc_stat
{namespace}:svc_itc_idx:{serviceId}SET指定服务的实例 ID 集合production:svc_itc_idx:order-service
{namespace}:svc_itc:{instanceId}HASH实例数据(字段:instanceId、serviceId、schema、host、port、weight、ephemeral、metadata)production:svc_itc:order-service@http#10.0.1.5#8080
{namespace}:topology_idxHASH拓扑索引:消费者名称到时间戳production:topology_idx
{namespace}:topology:{consumer}HASH拓扑依赖:生产者名称到时间戳production:topology:gateway-service

注意:ttl_at 并不存储在实例哈希中。它由发现脚本在读取时按 now + ttl 计算得出(discovery_get_instances.lua:33、discovery_get_instance.lua:30)。续约脚本还可能向哈希中写入系统字段 __last_renew_pub_ttl_at,用于发布节流(registry_renew.lua:9)。

实例 ID 格式为 {serviceId}@{schema}#{host}#{port}(例如 order-service@http#10.0.1.5#8080),由 Instance.asInstanceId 定义。

类图 ​

mermaid
classDiagram
    class ServiceRegistry {
        <<interface>>
        +register(namespace, serviceInstance) Mono~Boolean~
        +deregister(namespace, serviceInstance) Mono~Boolean~
        +renew(namespace, serviceInstance) Mono~Boolean~
        +setService(namespace, serviceId) Mono~Boolean~
        +removeService(namespace, serviceId) Mono~Boolean~
        +setMetadata(namespace, serviceId, instanceId, key, value) Mono~Boolean~
        +registeredEphemeralInstances Map
    }
    class RedisServiceRegistry {
        -registryProperties: RegistryProperties
        -redisTemplate: ReactiveStringRedisTemplate
        +registeredEphemeralInstances: ConcurrentHashMap
        -registerInternal(namespace, instance) Mono~Boolean~
        -addEphemeralInstance(namespace, instance)
        -removeEphemeralInstance(namespace, instance)
    }
    class RegistryProperties {
        +instanceTtl: Duration
    }
    class RenewProperties {
        +initialDelay: Duration
        +period: Duration
    }
    class RenewInstanceService {
        -renewProperties: RenewProperties
        -serviceRegistry: ServiceRegistry
        -scheduler: Scheduler
        +start()
        +stop()
        -renew()
    }
    class DiscoveryRedisScripts {
        +SCRIPT_REGISTRY_REGISTER: RedisScript~Boolean~
        +SCRIPT_REGISTRY_DEREGISTER: RedisScript~Boolean~
        +SCRIPT_REGISTRY_RENEW: RedisScript~Long~
        +SCRIPT_REGISTRY_SET_METADATA: RedisScript~Boolean~
        +SCRIPT_REGISTRY_SET_SERVICE: RedisScript~Boolean~
        +SCRIPT_REGISTRY_REMOVE_SERVICE: RedisScript~Boolean~
    }
    class ServiceInstanceCodec {
        +encodeMetadataKey(key) String
        +encodeMetadata(preArgs, metadata) List~String~
        +decode(instanceData) ServiceInstance
    }

    ServiceRegistry <|.. RedisServiceRegistry : 实现
    RedisServiceRegistry --> RegistryProperties : 配置
    RedisServiceRegistry --> DiscoveryRedisScripts : 使用 Lua 脚本
    RedisServiceRegistry --> ServiceInstanceCodec : 编码使用
    RenewInstanceService --> ServiceRegistry : 续约
    RenewInstanceService --> RenewProperties : 配置

相关页面 ​

参考文献 ​

基于 Apache License 2.0 许可发布。