背景&问题
对于一个系统来说, 配置文件是必不可少的, 有的系统是把配置文件放到conf文件夹下面, 系统启动的时候加载进来, 有的是统一放到外部的某个地方, 方便统一管理. 这样设计配置文件的问题显而易见, 一旦需要修改配置文件, 需要重启或重新部署该应用. 重启你懂的, 用户们在使用呢, 怎么能突然no service呢. 金主爸爸们不能得罪啊, 难道要半夜起来重启吗? 有没有什么好的方法可以无需重启就直接无缝衔接到新的配置项呢?
最近在看Microsoft的CloudDesignPattern, 写的非常详细, 各种云计算中的软件开发最佳实践都提到了, 推荐大家下载PDF查阅. 这本手册中就有提到Runtime Reconfiguration Pattern, 结合工作中的实践来做一个小小的总结.
解决方法
step0: 前提
首先需要一套全局配置中心, 专门用于存储公司内部各个应用的配置项, 解决配置混乱分散的问题. 用户可以在配置中心创建和修改配置, 修改配置有相应的版本控制和容灾措施. 有了这套系统之后, 其他应用就可以通过API获取配置项. 具体的配置中心搭建可以通过淘宝开源的Diamond来实现, 这个人写的XDiamond也很清晰(http://blog.csdn.net/hengyunabc/article/details/47777807)
有了这套中心化的配置管理服务, 就可以开始安全, 高效, 实时地获取配置了. 通过调用全局配置的API, 获取该项目的配置项, 这个过程中也有许多注意事项.
step1: 可靠地获取配置
有了上述的配置系统, 剩下的事情就是怎么call API的问题了. 通过HTTP请求获取配置信息, 难免遇到网络拥塞, 请求失败, 或者server一时间忙碌, 无响应等. 怎么避免这些并非client side的问题导致配置项获取不到呢? 配置项对程序运行非常重要, 怎么保证能够更加可靠地获取呢?
- 首先想到的就是Retry. 如果这个请求不是我的问题, 是服务器的问题, 或者网络问题, 重新请求一次会成功的话, 那么可以选择一定的backoff时间后重试.
- 重试不能一直无限重试下去. 需要设定一个timeout的时间, 如果多次尝试都没有结果, 说明服务挂了, 或者系统出现其他的故障. 必要的添加一些logging, 比如请求失败的原因, 方便后续发现问题.
- 并非所有的请求失败都要重试. 针对特定的HTTP code来重发请求, 比如请求返回401(unauthorized), 重试多少次都是没有用的, 因为你的认证没有通过. 一般来说, 500, 502, 504这些server端的问题, 可以进行重试.
通过下面的retry session, 一股脑解决上述问题!
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
def requests_retry_session(
retries=3,
backoff_factor=0.3,
status_forcelist=(500, 502, 504),
session=None,
):
session = session or requests.Session()
retry = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
这里有个高大上的python retry best practice. 覆盖了重试过程中需要考虑的各种问题, 有空可以研读一下https://www.peterbe.com/plog/best-practice-with-retries-with-requests
step2: 有效地更新配置
获取应用的配置信息, 可以通过主动pull的方式, 也可以通过被动push的方式, push方式需要配置管理中心有消息通知的服务.
简单说说主动去pull配置的一些注意事项吧.
- 间隔一定的时间去更新缓存的配置. HTTP请求难免会花费一定的时间, 遇到网络不好的时候, 请求配置时间会更加长, 因此会给你的应用程序带来一定的延迟. 我们可以把获取到的配置缓存到内存中, 超过一定的时间后可以再重新获取并更新.
import time
class ConfigReader(object):
# 初始化一个配置更新时间
last_update_time = None
def load_config(self):
if self.last_update_time is None or time.time() - self.last_update_time > 60:
# initialization and load config every 1 minutes
config_values = requests_retry_session().get('your request url').json()
app_env = 'production or whatever'
self.do_update(app_env, config_values)
@classmethod
def do_update(cls, app_env, config_values):
# 记得要更新配置的时间
cls.last_update_time = time.time()
cls.db_config = config_values.get('db_config')
...
如上所示, @classmethod
声明了函数do_update()
为类函数, cls
指向该类, 在该函数内设置的变量为类变量, 类变量(attribute)的好处是绑定在class上的, 即使你在程序的各个地方都创建了ConfigReader
对象, 只要有一个对象在调用的时候发现超时了需要再次调用do_update
, 并更新了诸如db_config
等attribute的值, 那么所有ConfigReader
对象的attribute db_config
都是最新的.
下面提供一段代码仅供理解:
# -*- coding: utf-8 -*-
import time
class ConfigReader(object):
# 初始化一个配置更新时间
last_update_time = None
def load_config(self):
if self.last_update_time is None or time.time() - self.last_update_time > 6:
# initialization and load config every 1 minutes
config_values = dict(db_config="localhost:3306")
app_env = 'production or whatever'
print "do load config"
self.do_update(app_env, config_values)
else:
print "not yet to load config"
@classmethod
def do_update(cls, app_env, config_values):
# 记得要更新配置的时间
cls.last_update_time = time.time()
cls.db_config = config_values.get('db_config')
config_reader1 = ConfigReader()
config_reader1.load_config()
print "reader1 update time: %s" % config_reader1.last_update_time
config_reader2 = ConfigReader()
config_reader2.load_config()
print "reader2 update time: %s" % config_reader2.last_update_time
print "sleep 6 second, see if it load again..."
time.sleep(6)
config_reader2.load_config()
print "reader1 update time: %s" % config_reader1.last_update_time
print "reader2 update time: %s" % config_reader2.last_update_time
输出结果:
do load config
reader1 update time: 1519571163.71
not yet to load config
reader2 update time: 1519571163.71
sleep 6 second, see if it load again...
do load config
reader1 update time: 1519571169.71
reader2 update time: 1519571169.71
从上述代码的输出结果可见, attribute是跟类绑定的, 即使我创建了config_reader1
和config_reader2
, 不管中间谁执行了load_config()
方法, 它们的last_update_time
变量是保持一致的.
当然缓存有很多方法, 比如可以写到本地文件, 看文件的最后更新时间来决定是否更新缓存. 像上述的方法, 简单的保存在内存中, 适合数据量不大的情景. 通过以上的可靠获取配置, 有效更新配置的方法, 就能够实现实时地去修改配置项, 无需重启应用啦.