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--协议模式(httphttps 等)
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 = METADATA_PREFIX + key

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

Redis 实现

RedisServiceRegistry

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

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

关键设计要点:

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->>EMap: addEphemeralInstance(namespace, instance)
    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 (成功)
    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、ttl_at、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

实例 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 许可发布。