使用Redis配置分布式服务

Published on 2017 - 06 - 12

随着我们越来越多地使用Redis以及其他服务,如何存储各项服务的配置信息将变成一个棘手的问题:对于一个Redis服务器、一个数据库服务器以及一个Web服务器来说,存储它们的配置信息并不困难;但如果我们使用了一个拥有好几个从服务器的Redis主服务器,或者为不同的应用程序设置了不同的Redis服务器,甚至为数据库也设置了主服务器和从服务器的话,那么存储这些服务器的配置信息将变成一件让人头疼的事情。

用于连接其他服务以及服务器的配置信息一般都是以配置文件的形式存储在硬盘里面,每当机器下线、网络连接断开或者某些需要连接其他服务器的情况出现时,程序通常需要一次性地对不同服务器中的多个配置文件进行更新。而这一节要介绍的就是如何将大部分配置信息从文件转移到Redis里面,使得应用程序可以自己完成绝大部分配置工作。

现在,让我们先来看一个简单的在线配置(live configuration)示例,了解一下如何使用Redis来存储配置信息。

使用Redis存储配置信息

为了展示配置管理方面的难题是多么的常见,来看一个非常简单的配置例子——假设现在我们要用一个标志(flag)来表示Web服务器是否正在进行维护:如果服务器正在进行维护,那么它就不应该发送数据库请求,而是应该向访客们返回一条简短的“抱歉,我们正在进行维护,请稍候再试”的信息;与此相反,如果服务器并没有在进行维护,那么它就应该按照既定的程序来运行。

在通常情况下,即使只更新配置中的一个标志,也会导致更新后的配置文件被强制推送至所有Web服务器,收到更新的服务器可能需要重新载入配置,甚至可能还要重启应用程序服务器。

与其尝试为不断增多的服务写入和维护配置文件,不如让我们直接将配置写入Redis里面。只要将配置信息存储在Redis里面,并编写应用程序来获取这些信息,我们就不用再编写工具来向服务器推送配置信息了,服务器和服务也不用再通过重新载入配置文件的方式来更新配置信息了。

为了实现这个简单的功能,让我们假设自己已经构建了一个中间层或者插件,这个中间层的作用在于:当is_under_maintenance()函数返回True时,它将向用户显示维护页面;与此相反,如果is_under_maintenance()函数返回False,它将如常地处理用户的访问请求。其中,is_under_maintenance()函数通过检查一个名为is-under-maintenance的键来判断服务器是否正在进行维护:如果is-under-maintenance键非空,那么函数返回True;否则返回False。另外,因为访客在看见维护页面的时候通常都会不耐烦地频繁刷新页面,所以为了尽量降低Redis在处理高访问量Web服务器时的负载,is_under_maintenance()函数最多只会每秒更新一次服务器维护信息。代码清单13展示了is_under_maintenance()函数的具体定义。

LAST_CHECKED = None
IS_UNDER_MAINTENANCE = False

def is_under_maintenance(conn):
    global LAST_CHECKED, IS_UNDER_MAINTENANCE   #A

    if LAST_CHECKED < time.time() - 1:          #B
        LAST_CHECKED = time.time()              #C
        IS_UNDER_MAINTENANCE = bool(            #D
            conn.get('is-under-maintenance'))   #D

    return IS_UNDER_MAINTENANCE                 #E
#A Set the two variables as globals so we can write to them later
#B Check to see if it has been at least 1 second since we last checked
#C Update the last checked time
#D Find out whether the system is under maintenance
#E Return whether the system is under maintenance

通过将is_under_maintenance()函数插入(plug into)应用程序的正确位置上,我们可以在1秒内改变数以千计Web服务器的行为。为了降低Redis在处理高访问量Web服务器时的负载,is_under_maintenance()函数将服务器维护状态信息的更新频率限制为最多每秒一次,但如果有需要的话,我们也可以加快信息的更新频率,甚至直接移除函数里面限制更新速度的那些代码。虽然is_under_maintenance()函数看上去似乎并不实用,但它的确展示了将配置信息存储在一个普遍可访问位置(commonly accessible location)的威力。接下来我们要考虑的是,怎样才能将更复杂的配置选项存储到Redis里面呢?

为每个应用程序组件分别配置一个Redis服务器

在我们越来越多地使用Redis的过程中,无数的开发者已经发现,最终在某个时间点上,只使用一台Redis服务器将不能满足我们的需求。因为我们可能需要记录更多信息,可能需要更多用于缓存的空间。

为了平滑地从单台服务器过渡到多台服务器,用户最好还是为应用程序中的每个独立部分都分别运行一个Redis服务器,比如说,一个专门负责记录日志、一个专门负责记录统计数据、一个专门负责进行缓存、一个专门负责存储cookies等。别忘了,一台机器上是可以运行多个Redis服务器的,只要这些服务器使用的端口号各不同就可以了。除此之外,在一个Redis服务器里面使用多个“数据库”,也可以减少系统管理的工作量。以上提到的两种方法,都是通过将不同数据划分至不同键空间(key space)的方式,来或多或少地简化迁移至更大或者更多服务器时所需的工作。但遗憾的是,随着Redis服务器的数量或者Redis数据库的数量不断增多,为所有Redis服务器管理和分发配置信息的工作将变得越来越烦琐和无趣。

在上一节中,我们使用了Redis来存储表示服务器是否正在进行维护的标志,并通过这个标志来决定是否需要向访客显示维护页面。而这一次,我们同样可以使用Redis来存储与其他Redis服务器有关的信息。说得更详细一点,我们可以把一个已知的Redis服务器用作配置信息字典,然后通过这个字典存储的配置信息来连接为不同应用或服务组件提供数据的其他Redis服务器。此外,这个字典还会在配置出现变更时,帮助客户端连接至正确的服务器。字典的具体实现比这个例子所要求的更为通用一些,因为我敢肯定,当你开始使用这个字典来获取配置信息的时候,你很快就会把它应用到其他服务器以及其他服务上面,而不仅仅用于获取Redis服务器的配置信息。

我们将构建一个函数,该函数可以从一个键里面取出一个JSON编码的配置值,其中,存储配置值的键由服务的类型以及使用该服务的应用程序命名。举个例子,如果我们想要获取连接存储统计数据的Redis服务器所需的信息,那么就需要获取config:redis:statistics键的值。代码清单14的set_config()函数展示了设置配置值的具体方法。

def set_config(conn, type, component, config):
    conn.set(
        'config:%s:%s'%(type, component),
        json.dumps(config))

通过这个set_config()函数,我们可以随心所欲地设置任何JSON编码的配置信息。因为get_config()函数和前面介绍过的is_under_maintenance()函数具有相似的结构,所以我们只要在语义上稍作修改,就可以使用get_config()函数来代替is_under_maintenance()函数。代码清单15列出了与set_config()相对应的get_config()函数,这个函数可以按照用户的需要,对配置信息进行0秒、1秒或者10秒的局部缓存。

CONFIGS = {}
CHECKED = {}

def get_config(conn, type, component, wait=1):
    key = 'config:%s:%s'%(type, component)

    if CHECKED.get(key) < time.time() - wait:           #A
        CHECKED[key] = time.time()                      #B
        config = json.loads(conn.get(key) or '{}')      #C
        config = dict((str(k), config[k]) for k in config)#G
        old_config = CONFIGS.get(key)                   #D

        if config != old_config:                        #E
            CONFIGS[key] = config                       #F

    return CONFIGS.get(key)
#A Check to see if we should update the configuration information about this component
#B We can, so update the last time we checked this connection
#C Fetch the configuration for this component
#G Convert potentially unicode keyword arguments into string keyword arguments
#D Get the old configuration for this component
#E If the configurations are different
#F Update the configuration

在拥有了设置配置信息和获取配置信息的两个函数之后,我们还可以在此之上更进一步。我们在前面一直考虑的都是怎样存储和获取配置信息以便连接各个不同的Redis服务器,但直到目前为止,我们编写的绝大多数函数的第一个参数都是一个连接参数。因此,为了不再需要手动获取我们正在使用的各项服务的连接,下面让我们来构建一个能够帮助我们自动连接这些服务的方法。

自动Redis连接管理

手动创建和传递Redis连接并不是一件容易的事情,这不仅仅是因为我们需要重复查阅配置信息,还有一个原因就是,即使使用了上一节介绍的配置管理函数,我们还是需要获取配置、连接Redis,并在使用完连接之后关闭连接。为了简化连接的管理操作,我们将编写一个装饰器(decorator),让它负责连接除配置服务器之外的所有其他Redis服务器。

装饰器Python提供了一种语法,用于将函数X传入至另一个函数Y的内部,其中函数Y就被称为装饰器。装饰器给用户提供了一个修改函数X行为的机会。有些装饰器可以用于校验参数,而有些装饰器则可以用于注册回调函数,甚至还有一些装饰器可以用于管理连接——就像我们接下来要做的那样。

代码清单16展示了我们定义的装饰器,它接受一个指定的配置作为参数并生成一个包装器(wrapper),这个包装器可以包裹起一个函数,使得之后对被包裹函数的调用可以自动连接至正确的Redis服务器,并且连接Redis服务器所使用的那个连接会和用户之后提供的其他参数一同传递至被包裹的函数。

REDIS_CONNECTIONS = {}

def redis_connection(component, wait=1):                        #A
    key = 'config:redis:' + component                           #B
    def wrapper(function):                                      #C
        @functools.wraps(function)                              #D
        def call(*args, **kwargs):                              #E
            old_config = CONFIGS.get(key, object())             #F
            _config = get_config(                               #G
                config_connection, 'redis', component, wait)    #G

            config = {}
            for k, v in _config.iteritems():                    #L
                config[k.encode('utf-8')] = v                   #L

            if config != old_config:                            #H
                REDIS_CONNECTIONS[key] = redis.Redis(**config)  #H

            return function(                                    #I
                REDIS_CONNECTIONS.get(key), *args, **kwargs)    #I
        return call                                             #J
    return wrapper                                              #K
#A We pass the name of the application component to the decorator
#B We cache the configuration key because we will be fetching it every time the function is called
#C Our wrapper takes a function that it wraps with another function
#D Copy some useful metadata from the original function to the configuration handler
#E Create the actual function that will be managing connection information
#F Fetch the old configuration, if any
#G Get the new configuration, if any
#L Make the configuration usable for creating a Redis connection
#H If the new and old configuration do not match, create a new connection
#I Call and return the result of our wrapped function, remembering to pass the connection and the other matched arguments
#J Return the fully wrapped function
#K Return a function that can wrap our Redis function

同时使用*args**kwargs函数定义中的args变量用于获取所有位置参数(positional argument),而kwargs变量则用于获取所有命名参数(named argument),这两种参数传递方式都可以将给定的参数传入被调用的函数里面。

代码清单16展示的一系列嵌套函数初看上去可能会让人感到头昏目眩,但它们实际上并没有想象中的那么复杂。redis_connection()装饰器接受一个应用组件的名字作为参数并返回一个包装器。这个包装器接受一个我们想要将连接传递给它的函数为参数,然后对函数进行包裹并返回被包裹函数的调用器(caller)。这个调用器负责处理所有获取配置信息的工作,除此之外,它还负责连接Redis服务器并调用被包裹的函数。尽管redis_connection()函数描述起来相当复杂,但实际使用起来却是非常方便的,代码清单17就展示了怎样将redis_connection()函数应用到之前介绍的log_recent()函数上面。

@redis_connection('logs')                   #A
def log_recent(conn, app, message):         #B
    'the old log_recent() code'

log_recent('main', 'User 235 logged in')    #C
#A The redis_connection() decorator is very easy to use
#B The function definition doesn't change
#C You no longer need to worry about passing the log server connection when calling log_recent()

装饰器代码清单17使用了特殊的语法来“装饰”log_recent()函数。这里的装饰指的是“将一个函数传递给装饰器,让装饰器在返回被传入的函数之前,对被传入的函数执行一些操作”。

参考文档