精华内容
下载资源
问答
  • 使用nginx软件负载结构图使用阿里云硬件负载均衡服务结构图因为flask app需要在内存中保存ip树以及国家、省份、城市相关的字典,因此占用内存较。gunicorn的1个worker需要占用300M内存,nginx的4个worker...

    结构设计

    基础架构为flask+gunicorn+负载均衡,负载均衡分为阿里云硬件负载均衡服务和软负载nginx。gunicorn使用supervisor进行管理。

    使用nginx软件负载结构图

    使用阿里云硬件负载均衡服务结构图

    因为flask app需要在内存中保存ip树以及国家、省份、城市相关的字典,因此占用内存较高。gunicorn的1个worker需要占用300M内存,nginx的4个worker内存占用较小(不到100M),因此占用1.3G的内存(即需要一个2G内存的服务器)。当gunicorn任意一个节点挂断或者升级时,另外一个节点仍然在使用,不会影响整体服务

    ip数据库

    IP库(也叫IP地址数据库),是由专业技术人员经过长时间通过多种技术手段收集而来的,并且长期有专业人员进行更新、维护、补充。

    ip数据库解析查询代码

    基于二叉查找树实现

    import struct

    from socket import inet_aton, inet_ntoa

    import os

    import sys

    sys.setrecursionlimit(1000000)

    _unpack_V = lambda b: struct.unpack("

    _unpack_N = lambda b: struct.unpack(">L", b)

    _unpack_C = lambda b: struct.unpack("B", b)

    class IpTree:

    def __init__(self):

    self.ip_dict = {}

    self.country_codes = {}

    self.china_province_codes = {}

    self.china_city_codes = {}

    def load_country_codes(self, file_name):

    try:

    path = os.path.abspath(file_name)

    with open(path, "rb") as f:

    for line in f.readlines():

    data = line.split('\t')

    self.country_codes[data[0]] = data[1]

    # print self.country_codes

    except Exception as ex:

    print "cannot open file %s: %s" % (file, ex)

    print ex.message

    exit(0)

    def load_china_province_codes(self, file_name):

    try:

    path = os.path.abspath(file_name)

    with open(path, "rb") as f:

    for line in f.readlines():

    data = line.split('\t')

    provinces = data[2].split('\r')

    self.china_province_codes[provinces[0]] = data[0]

    # print self.china_province_codes

    except Exception as ex:

    print "cannot open file %s: %s" % (file, ex)

    print ex.message

    exit(0)

    def load_china_city_codes(self, file_name):

    try:

    path = os.path.abspath(file_name)

    with open(path, "rb") as f:

    for line in f.readlines():

    data = line.split('\t')

    cities = data[3].split('\r')

    self.china_city_codes[cities[0]] = data[0]

    except Exception as ex:

    print "cannot open file %s: %s" % (file, ex)

    print ex.message

    exit(0)

    def loadfile(self, file_name):

    try:

    ipdot0 = 254

    path = os.path.abspath(file_name)

    with open(path, "rb") as f:

    local_binary0 = f.read()

    local_offset, = _unpack_N(local_binary0[:4])

    local_binary = local_binary0[4:local_offset]

    # 256 nodes

    while ipdot0 >= 0:

    middle_ip = None

    middle_content = None

    lis = []

    # offset

    begin_offset = ipdot0 * 4

    end_offset = (ipdot0 + 1) * 4

    # index

    start_index, = _unpack_V(local_binary[begin_offset:begin_offset + 4])

    start_index = start_index * 8 + 1024

    end_index, = _unpack_V(local_binary[end_offset:end_offset + 4])

    end_index = end_index * 8 + 1024

    while start_index < end_index:

    content_offset, = _unpack_V(local_binary[start_index + 4: start_index + 7] +

    chr(0).encode('utf-8'))

    content_length, = _unpack_C(local_binary[start_index + 7])

    content_offset = local_offset + content_offset - 1024

    content = local_binary0[content_offset:content_offset + content_length]

    if middle_content != content and middle_content is not None:

    contents = middle_content.split('\t')

    lis.append((middle_ip, (contents[0], self.lookup_country_code(contents[0]),

    contents[1], self.lookup_china_province_code(contents[1]),

    contents[2], self.lookup_china_city_code(contents[2]),

    contents[3], contents[4])))

    middle_content, = content,

    middle_ip = inet_ntoa(local_binary[start_index:start_index + 4])

    start_index += 8

    self.ip_dict[ipdot0] = self.generate_tree(lis)

    ipdot0 -= 1

    except Exception as ex:

    print "cannot open file %s: %s" % (file, ex)

    print ex.message

    exit(0)

    def lookup_country(self, country_code):

    try:

    for item_country, item_country_code in self.country_codes.items():

    if country_code == item_country_code:

    return item_country, item_country_code

    return 'None', 'None'

    except KeyError:

    return 'None', 'None'

    def lookup_country_code(self, country):

    try:

    return self.country_codes[country]

    except KeyError:

    return 'None'

    def lookup_china_province(self, province_code):

    try:

    for item_province, item_province_code, in self.china_province_codes.items():

    if province_code == item_province_code:

    return item_province, item_province_code

    return 'None', 'None'

    except KeyError:

    return 'None', 'None'

    def lookup_china_province_code(self, province):

    try:

    return self.china_province_codes[province.encode('utf-8')]

    except KeyError:

    return 'None'

    def lookup_china_city(self, city_code):

    try:

    for item_city, item_city_code in self.china_city_codes.items():

    if city_code == item_city_code:

    return item_city, item_city_code

    return 'None', 'None'

    except KeyError:

    return 'None', 'None'

    def lookup_china_city_code(self, city):

    try:

    return self.china_city_codes[city]

    except KeyError:

    return 'None'

    def lookup(self, ip):

    ipdot = ip.split('.')

    ipdot0 = int(ipdot[0])

    if ipdot0 < 0 or ipdot0 > 255 or len(ipdot) != 4:

    return None

    try:

    d = self.ip_dict[int(ipdot[0])]

    except KeyError:

    return None

    展开全文
  • 榜单查询由于榜单数据范围不好确定,范围查询 有可能查询数据太多,所以在查询排行榜时,先取离线数据 2 倍的数据量,然后根据离线数据 返回业务主键,查询当前的实时数据,将实时数据覆盖到离线数据后,进行内存...

    精选30+云产品,助力企业轻松上云!>>> hot3.png

    点击蓝色“大数据每日哔哔”关注我

    加个“星标”,第一时间获取大数据架构,实战经验





    摘要:由于影院单日售票在 3000W 张,预售将近 1 亿张票,计算量大,写入频 次高,从感知影院售票到客户端呈现数据,采用什么样的方案,什么样的技术,能够通过最小 的改动让数据最快的呈现出来,成了技术考虑的难点。主要内容包括:


    1. 技术挑战

    2. 技术策略

    3. 落地方法

    4. 总结沉淀

    5. 专业名词解释

    作者: 阿里大文娱 | 奋氛



    背景


    灯塔是阿里大文娱旗下一站式宣发平台,同时也是阿里巴巴为数不多对外提供数据的数据平台。作为数据平台,数据的时效性和准确性一直技术需要突破的重点和难点。

    一、技术挑战

    灯塔数据系统(前身淘票票专业版)从 2017 年开始建设,最开始采用 MYSQL 作为数据存储,基础数据定时计算写入数据库,经过 2 年多的建设,产品已经基本成形,但对于数据的实效性有了更高的要求,由于影院单日售票在 3000W 张,预售将近 1 亿张票,计算量大,写入频 次高,从感知影院售票到客户端呈现数据,采用什么样的方案,什么样的技术,能够通过最小 的改动让数据最快的呈现出来,成了技术考虑的难点。

    二、技术策略


    首先是缩小数据量,找出数据规律,实现数据的实时计算,各维度数据汇总如图:


    232bc4d26f69a41965c682eff5e1aa21b16.jpg

    (图片:灯塔专业版数据汇总关系图)

    各维度数据在业务应用的场景中,均可以按照时间、地区、业务主键进行检索,根据这个 特征,我们生成了天然的 Key 组合,时间、地区、业务主键,并排列组合出三种 Key:时间_地区_业务主键,时间_业务主键_地区,地区_业务主键_时间。按照以上三种 Key 组合,在已知 任何两个条件的情况下,均能实现对业务数据的检索。此时我们已经锁定了数据的存储平台HBase。剩下的就是如何改造系统实现实时化。

    三、落地方法

    数据源有了,MYSQL 和 HBase,HBase 是实时数据,MYSQL 是离线数据,为了让上层业务无感知,特在底层数据做处理,实现离线数据和实时数据的结合,数据处理流程如图:


    e9d2d626e5f0a9149887f03db79806903da.jpg

    (图片:灯塔专业版数据处理流程图)


    用户在请求票房数据的时候,先根据业务开关,决定请求实时数据还是离线数据,离线数据直接请求 MYSQL时数据,优先查询缓存,若缓存存在且不过期,直接返回缓存数据。缓存数据失效的情况下,查询 HBase,重新写入缓存。

    系统日常还是有上千的 QPS,为了防止缓存击穿,对数据源造成压力,需要对热数据进行缓存预热。由于数据的特殊性,T 日为最热数据,占到总流量的 80%以上,这时候,缓存预热 成了承受高并发访问的关键。定时任务每秒将 T 日数据整体刷入缓存,防止缓存失效击穿(因 为都是 key-valu 存储,后续考虑热数据直接写入缓存,直接替代预热方案)。其实为了防止击穿, 这部分数据是 24 小时不过期的,数据的更新是依赖定时任务的,一但数据链路故障、HBase 故 障或算法异常,只需要停止定时任务,就能暂时止血,给技术留出处理时间不至于故障升级。同时 HBase 做了主备链路,而且主链路和备用链路的的算法略有不同,保证主备链路不会同时 出问题。这样的架构,对于应用而言,就有了 4 套不同的数据源做保证的。架构上线至今,数 据未曾出现一次问题。而且无形中解决了高 QPS 的问题,数据的提供主要依靠 TAIR,而缓存 应对 QPS 就显得简单的多了。

    367dfce8e53a5ce89290ad8902ed11aae3e.png(图片:灯塔专业版数据源关系图)

    系统的难点在于实时数据和离线数据的结合。数据结合共分为以下几类:

    1. T日查询,非实时即离线,如查询今日大盘票房;系统首先定义了一个方法,根据日期判定数据应该查询实时还是查询离线,由于行业数据是按照 6 点到 6 点,即 T 日数据,在第二 日 6 点后才变为离线数据,且由于专资办数据回刷的问题,防止数据回跳,会在数据回刷后才切换为离线数据。当查询单日数据时,针对查询日期,判定数据源,进行数据转换;

    2. 日期范围查询,即有实时又有离线,如查询影片每日票房情况;当查询日期范围时,由于日期范围时连续的,特将范围日期拆散成每一天,按照方法 1 中的判定规则,切分日期范围 为离线日期和实时日期,然后数据源根究日期范围取最小日期和最大日期进行范围查询,查询 后进行数据组合后返回数据;

    3. 范围统计,离线+实时,如本周票房;当进行范围统计查询时,首先去离线数据,然后根据日期判定,如果 T-1 数据还未回刷,则去 T-1 T 日数据,否则只去 T 日数据即可,将实 时数据和离线数据进行加和,返回查询数据结果;

    4. 榜单查询,离线补实时,如影片榜单。榜单查询由于榜单数据范围不好确定,范围查询 有可能查询数据太多,所以在查询排行榜时,先取离线数据 2 倍的数据量,然后根据离线数据 返回业务主键,查询当前的实时数据,将实时数据覆盖到离线数据后,进行内存排序和截取, 最终返回榜单数据。榜单数据略有不同,比如院线影片,由于全国院线一共 49 家,此时不做离 线查询,直接查询所有实时数据进行排序。


    针对以上数据整合的各种可能,参照以往出现的各种问题,封装代码如下:


    bffbffe81771361aebd52fc529e23311db9.jpg

    (图片:示例代码 1)


    62810ac6465e7004be54544c10f533b4f30.jpg

    (图片:示例代码 2)


    通过以上处理,在上层业务无感知的情况下,下游数据实现了整体实时化的切换。而且通 过 switch 开关控制针对数据源能够实现单业务主备切流,实时离线数据转换,使得数据的稳定性更可靠,为底层数据改造和升级留下了充足的扩展空间。

    有了架构还不够,还需要感知能力,业务异常感知还是比较简单的,但是对于灯塔来说, 有数不代表正常,数据到底对不对,这是问题感知的关键,这时候需要一个智能化的监控系统。针对票房数据,在不改造代码的情况下,我们设计了一个切面,引入脚本代码,针对特定数据 来源做数据动态处理,将返回数据整合在一起,并提取出来,通过算法识别票房数据行为趋势, 如图:


    a3e2f26fae37dd66f0d4f2efa7832359ec7.jpg


    (图片:灯塔专业版票房监测趋势图)

    针对数据的趋势做智能化监控,当数据异常变化或者超过业务限定范围,就会通知告警, 以此来有效的规避数据异常的情况,并能及时感知问题。

    四、总结沉淀

    随着 B 端业务的发展,数据的作用越来越大,在海量数据存储和更新的需要下,关系型数据库已经越来越无力,各种类型的数据存储起到了不同的作用,多数据源的整合也越来越重要。本文介绍了灯塔为了做到实时的数据系统,是如何组合 Mysql、Hbase、Tair 三个数据源来实现 高写入,高并发、高可靠的数据系统,希望能给后续更多的业务系统提供参考和指导。


    专业名词解释:


    Tair 是一个高性能、分布式、可扩展、高可靠的Key-Value nosql结构存储系统,专注于高速缓存场景。


    HBase 是一个高可靠、高性能、面向列、可伸缩的分布式数据库,是谷歌BigTable的开源实现,主要用来存储非结构化和半结构化的松散数据。HBase的目标是处理非常庞大的表,可以通过水平扩展的方式,利用廉价计算机集群处理由超过10亿行数据和数百万列元素组成的数据表。


    缓存击穿:通常我们在使用缓存时都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。

    4b4811abf431ab94ad6a50f279e7e8dc133.png

    查询一个数据库中不存在的数据,比如商品详情,查询一个不存在的ID,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成过大的压力。


    当通过某一个key去查询数据的时候,如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值,比如“NULL”,并设置一个缓存的失效时间,这时在缓存失效之前,所有通过此key的访问都被缓存挡住了。后面如果此key对应的数据在DB中存在时,缓存失效之后,通过此key再去访问数据,就能拿到新的value了。


    ▼ 更多实战技术文章 


    日均万亿条数据如何处理?爱奇艺实时计算平台这样做


    58全站用户行为数据仓库建设及实践


    kafka 在 360 商业化的实践


    干货 | 携程机票数据仓库建设之路


    干货 | 携程Hadoop跨机房架构实践


    (完)




    专注大数据技术、架构、实战

    关注我,带你不同角度看数据架构


    b747ce8da2ed8b9769b5c1ef4d7ce00230b.jpg

    本文分享自微信公众号 - 大数据每日哔哔(bb-bigdata)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    展开全文
  • 因为flask app需要在内存中保存ip树以及国家、省份、城市相关的字典,因此占用内存较。gunicorn的1个worker需要占用300M内存,nginx的4个worker内存占用较小(不到100M),因此占用1.3G的内存(即需要一个2G内存的...

    结构设计

    基础架构为flask+gunicorn+负载均衡,负载均衡分为阿里云硬件负载均衡服务和软负载nginx。gunicorn使用supervisor进行管理。

    使用nginx软件负载结构图

    使用阿里云硬件负载均衡服务结构图

    因为flask app需要在内存中保存ip树以及国家、省份、城市相关的字典,因此占用内存较高。gunicorn的1个worker需要占用300M内存,nginx的4个worker内存占用较小(不到100M),因此占用1.3G的内存(即需要一个2G内存的服务器)。当gunicorn任意一个节点挂断或者升级时,另外一个节点仍然在使用,不会影响整体服务

    ip数据库

    IP库(也叫IP地址数据库),是由专业技术人员经过长时间通过多种技术手段收集而来的,并且长期有专业人员进行更新、维护、补充。

    ip数据库解析查询代码

    基于二叉查找树实现

    import struct
    from socket import inet_aton, inet_ntoa
    import os
    import sys
    
    sys.setrecursionlimit(1000000)
    
    _unpack_V = lambda b: struct.unpack("<L", b)
    _unpack_N = lambda b: struct.unpack(">L", b)
    _unpack_C = lambda b: struct.unpack("B", b)
    
    
    class IpTree:
        def __init__(self):
            self.ip_dict = {}
            self.country_codes = {}
            self.china_province_codes = {}
            self.china_city_codes = {}
    
        def load_country_codes(self, file_name):
            try:
                path = os.path.abspath(file_name)
                with open(path, "rb") as f:
                    for line in f.readlines():
                        data = line.split('\t')
                        self.country_codes[data[0]] = data[1]
                        # print self.country_codes
            except Exception as ex:
                print "cannot open file %s: %s" % (file, ex)
                print ex.message
                exit(0)
    
        def load_china_province_codes(self, file_name):
            try:
                path = os.path.abspath(file_name)
                with open(path, "rb") as f:
                    for line in f.readlines():
                        data = line.split('\t')
                        provinces = data[2].split('\r')
                        self.china_province_codes[provinces[0]] = data[0]
                        # print self.china_province_codes
            except Exception as ex:
                print "cannot open file %s: %s" % (file, ex)
                print ex.message
                exit(0)
    
        def load_china_city_codes(self, file_name):
            try:
                path = os.path.abspath(file_name)
                with open(path, "rb") as f:
                    for line in f.readlines():
                        data = line.split('\t')
                        cities = data[3].split('\r')
                        self.china_city_codes[cities[0]] = data[0]
            except Exception as ex:
                print "cannot open file %s: %s" % (file, ex)
                print ex.message
                exit(0)
    
        def loadfile(self, file_name):
            try:
                ipdot0 = 254
                path = os.path.abspath(file_name)
                with open(path, "rb") as f:
                    local_binary0 = f.read()
                    local_offset, = _unpack_N(local_binary0[:4])
                    local_binary = local_binary0[4:local_offset]
                    # 256 nodes
                    while ipdot0 >= 0:
                        middle_ip = None
                        middle_content = None
                        lis = []
                        # offset
                        begin_offset = ipdot0 * 4
                        end_offset = (ipdot0 + 1) * 4
                        # index
                        start_index, = _unpack_V(local_binary[begin_offset:begin_offset + 4])
                        start_index = start_index * 8 + 1024
                        end_index, = _unpack_V(local_binary[end_offset:end_offset + 4])
                        end_index = end_index * 8 + 1024
                        while start_index < end_index:
                            content_offset, = _unpack_V(local_binary[start_index + 4: start_index + 7] +
                                                        chr(0).encode('utf-8'))
                            content_length, = _unpack_C(local_binary[start_index + 7])
                            content_offset = local_offset + content_offset - 1024
                            content = local_binary0[content_offset:content_offset + content_length]
                            if middle_content != content and middle_content is not None:
                                contents = middle_content.split('\t')
                                lis.append((middle_ip, (contents[0], self.lookup_country_code(contents[0]),
                                                        contents[1], self.lookup_china_province_code(contents[1]),
                                                        contents[2], self.lookup_china_city_code(contents[2]),
                                                        contents[3], contents[4])))
                            middle_content, = content,
                            middle_ip = inet_ntoa(local_binary[start_index:start_index + 4])
                            start_index += 8
                        self.ip_dict[ipdot0] = self.generate_tree(lis)
                        ipdot0 -= 1
            except Exception as ex:
                print "cannot open file %s: %s" % (file, ex)
                print ex.message
                exit(0)
    
        def lookup_country(self, country_code):
            try:
                for item_country, item_country_code in self.country_codes.items():
                    if country_code == item_country_code:
                        return item_country, item_country_code
                return 'None', 'None'
            except KeyError:
                return 'None', 'None'
    
        def lookup_country_code(self, country):
            try:
                return self.country_codes[country]
            except KeyError:
                return 'None'
    
        def lookup_china_province(self, province_code):
            try:
                for item_province, item_province_code, in self.china_province_codes.items():
                    if province_code == item_province_code:
                        return item_province, item_province_code
                return 'None', 'None'
            except KeyError:
                return 'None', 'None'
    
        def lookup_china_province_code(self, province):
            try:
                return self.china_province_codes[province.encode('utf-8')]
            except KeyError:
                return 'None'
    
        def lookup_china_city(self, city_code):
            try:
                for item_city, item_city_code in self.china_city_codes.items():
                    if city_code == item_city_code:
                        return item_city, item_city_code
                return 'None', 'None'
            except KeyError:
                return 'None', 'None'
    
        def lookup_china_city_code(self, city):
            try:
                return self.china_city_codes[city]
            except KeyError:
                return 'None'
    
        def lookup(self, ip):
            ipdot = ip.split('.')
            ipdot0 = int(ipdot[0])
            if ipdot0 < 0 or ipdot0 > 255 or len(ipdot) != 4:
                return None
            try:
                d = self.ip_dict[int(ipdot[0])]
            except KeyError:
                return None
            if d is not None:
                return self.lookup1(inet_aton(ip), d)
            else:
                return None
    
        def lookup1(self, net_ip, (net_ip1, content, lefts, rights)):
            if net_ip < net_ip1:
                if lefts is None:
                    return content
                else:
                    return self.lookup1(net_ip, lefts)
            elif net_ip > net_ip1:
                if rights is None:
                    return content
                else:
                    return self.lookup1(net_ip, rights)
            else:
                return content
    
        def generate_tree(self, ip_list):
            length = len(ip_list)
            if length > 1:
                lefts = ip_list[:length / 2]
                rights = ip_list[length / 2:]
                (ip, content) = lefts[length / 2 - 1]
                return inet_aton(ip), content, self.generate_tree(lefts), self.generate_tree(rights)
            elif length == 1:
                (ip, content) = ip_list[0]
                return inet_aton(ip), content, None, None
            else:
                return
    
    if __name__ == "__main__":
        import sys
    
        reload(sys)
        sys.setdefaultencoding('utf-8')
        ip_tree = IpTree()
        ip_tree.load_country_codes("doc/country_list.txt")
        ip_tree.load_china_province_codes("doc/china_province_code.txt")
        ip_tree.load_china_city_codes("doc/china_city_code.txt")
        ip_tree.loadfile("doc/mydata4vipday2.dat")
        print ip_tree.lookup('123.12.23.45')

    http请求

    提供ip查询服务的GET请求和POST请求

    @ip_app.route('/api/ip_query', methods=['POST'])
    def ip_query():
        try:
            ip = request.json['ip']
        except KeyError as e:
            raise InvalidUsage('bad request: no key ip in your request json body. {}'.format(e), status_code=400)
        if not is_ip(ip):
            raise InvalidUsage('{} is not a ip'.format(ip), status_code=400)
        try:
            res = ip_tree.lookup(ip)
        except Exception as e:
            raise InvalidUsage('internal error: {}'.format(e), status_code=500)
        if res is not None:
            return jsonify(res)
        else:
            raise InvalidUsage('no ip info in ip db for ip: {}'.format(ip), status_code=501)
    
    
    @ip_app.route('/api/ip_query', methods=['GET'])
    def ip_query_get():
        try:
            ip = request.values.get('ip')
        except ValueError as e:
            raise InvalidUsage('bad request: no param ip in your request. {}'.format(e), status_code=400)
        if not is_ip(ip):
            raise InvalidUsage('{} is not a ip'.format(ip), status_code=400)
        try:
            res = ip_tree.lookup(ip)
        except Exception as e:
            raise InvalidUsage('internal error: {}'.format(e), status_code=500)
        if res is not None:
            return jsonify(res)
        else:
            raise InvalidUsage('no ip info in ip db for ip: {}'.format(ip), status_code=501)

    POST请求需要在请求体中包含类似下面的json字段

    {
        "ip": "165.118.213.9"
    }

    GET请求的形式如:http://127.0.0.1:5000/api/ip_query?ip=165.118.213.9

    服务部署

    安装依赖库

    依赖的库requirements.txt如下:

    certifi==2017.7.27.1
    chardet==3.0.4
    click==6.7
    Flask==0.12.2
    gevent==1.1.1
    greenlet==0.4.12
    gunicorn==19.7.1
    idna==2.5
    itsdangerous==0.24
    Jinja2==2.9.6
    locustio==0.7.5
    MarkupSafe==1.0
    meld3==1.0.2
    msgpack-python==0.4.8
    requests==2.18.3
    supervisor==3.3.3
    urllib3==1.22
    Werkzeug==0.12.2

    安装方法:pip install -r requirements.txt

    配置supervisor

    vim /etc/supervisor/conf.d/ip_query_http_service.conf,内容如下

    [program:ip_query_http_service]
    directory = /root/qk_python/ip_query
    command = gunicorn -w10 -b0.0.0.0:8080 ip_query_app:ip_app --worker-class gevent
    autostart = true
    startsecs = 5
    autorestart = true
    startretries = 3
    user = root
    stdout_logfile=/root/qk_python/ip_query/log/gunicorn.log
    stderr_logfile=/root/qk_python/ip_query/log/gunicorn.err

    内容添加完成之后,需要创建stdout_logfile和stderr_logfile这两个目录,否则supervisor启动会报错。然后更新supervisor启动ip_query_http_service进程。

    # 启动supervisor
    supervisord -c /etc/supervisor/supervisord.conf    
    
    # 更新supervisor服务
    supervisorctl update

    关于supervisor的常用操作参见最后面的参考资料。

    安装nginx

    如果是软负载的形式需要安装nginx,编译安装nginx的方法参见最后面的参考资料。

    配置nginx

    vim /usr/local/nginx/nginx.conf,修改配置文件内容如下:

    #user  nobody;
    #nginx进程数,建议设置为等于CPU总核心数。
    worker_processes  4;
    #error_log  logs/error.log;
    #error_log  logs/error.log  notice;
    #全局错误日志定义类型,[ debug | info | notice | warn | error | crit ]
    error_log  logs/error.log  info;
    #进程文件
    pid        logs/nginx.pid;
    #一个nginx进程打开的最多文件描述符数目,理论值应该是最多打开文件数(系统的值ulimit -n)与nginx进程数相除,但是nginx分配请求并不均匀,所以建议与ulimit -n的值保持一致。
    worker_rlimit_nofile 65535;
    events {
        #参考事件模型 linux 下使用epoll
        use epoll;
        #单个进程最大连接数(最大连接数=连接数*进程数)
        worker_connections  65535;
    }
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
        '$status $body_bytes_sent "$http_referer" '
        '"$http_user_agent" "$http_x_forwarded_for"';
        access_log  logs/access.log  main;
        sendfile        on;
        #keepalive_timeout  0;
        keepalive_timeout  65;
        tcp_nopush on; #防止网络阻塞
        tcp_nodelay on; #防止网络阻塞
        #gzip  on;
        server {
            #这里配置衔接服务提供的代理端口.
            listen       9000;
            server_name  localhost;
            #charset koi8-r;
            #access_log  logs/host.access.log  main;
            location / {
                #            root   html;
                #            index  index.html index.htm;
                proxy_pass http://127.0.0.1:8000;
                proxy_redirect off;
                proxy_set_header X-Real-IP $remote_addr;
                #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $host;
                client_max_body_size 10m; #允许客户端请求的最大单文件字节数
                client_body_buffer_size 128k; #缓冲区代理缓冲用户端请求的最大字节数,
                proxy_buffer_size 4k; #设置代理服务器(nginx)保存用户头信息的缓冲区大小
                proxy_temp_file_write_size 64k;       #设定缓存文件夹大小,大于这个值,将从upstream服务器传
            }
    
            #error_page  404              /404.html;
            # redirect server error pages to the static page /50x.html
            #
            error_page   500 502 503 504  /50x.html;
            location = /50x.html {
                root   html;
            }       
        }
    }

    压力测试

    做压力测试,选择正确的工具是前提。以下工具中,jmeter运行在windows机器较多,其他工具建议都运行在*nix机器上。

    压力测试工具选择

    工具名称 优缺点 建议
    ApacheBench(ab) 命令使用简单,效率高,统计信息完善,施压机器内存压力小 推荐
    locust python编写,效率低,受限于GIL,需要编写python测试脚本 不推荐
    wrk 命令使用简单,效率高,统计信息精炼,坑少,少报错 最推荐
    jmeter 基于java,Apache开源,图形化界面,操作简便 推荐
    webbench 使用简单,但是不支持POST请求 一般
    tsung erlang编写,配置模板较多,较复杂 不推荐

    上述六种工具全部亲身使用过,下面选择ab、wrk、jmeter三种工具简单说明安装使用方法,其他工具的使用方法如有需要,自行google

    ab

    安装

    apt-get install apache2-utils 

    常见options

    option 含义
    -r 当接收到socket错误的时候ab不退出
    -t 发送请求的最长时间
    -c 并发数,一次构造的请求数量
    -n 发送的请求数量
    -p postfile,指定包含post数据的文件
    -T content-type,指定post和put发送请求时请求体的类型

    使用

    测试GET请求

    ab -r -t 120 -c 5000 http://127.0.0.1:8080/api/ip_query?ip=165.118.213.9

    测试POST请求

    ab -r -t 120 -c 5000 -p /tmp/post_data.txt -T 'application/json' http://127.0.0.1:8080/api/ip_query

    其中/tmp/post_data.txt文件的内容为待发送的-T指定格式的数据,在此处为json格式

    {"ip": "125.118.213.9"}

    wrk

    http://www.restran.net/2016/09/27/wrk-http-benchmark/

    安装

    apt-get install libssl-dev
    git clone https://github.com/wg/wrk.git
    cd wrk
    make
    cp wrk /usr/sbin

    常见options

    option 含义
    -c 打开的连接数,即并发数
    -d 压力测试时间:发送请求的最长时间
    -t 施压机器使用的线程数量
    -s 指定要加载的lua脚本
    --latency 打印延迟统计信息

    使用

    测试GET请求

    wrk -t10 -c5000 -d120s --latency http://127.0.0.1:8080/api/ip_query?ip=165.118.213.9

    测试POST请求

    wrk -t50 -c5000 -d120s --latency -s /tmp/wrk_post.lua http://127.0.0.1:8080

    其中/tmp/wrk_post.lua文件的内容为待加载的lua脚本,指定post的path,header,body

    request = function()
      path = "/api/ip_query"
      wrk.headers["Content-Type"] = "application/json"
      wrk.body = "{\"ip\":\"125.118.213.9\"}"
      return wrk.format("POST", path)
    end

    jmeter

    安装

    安装jmeter前需要先安装jdk1.8。然后在Apache官网可以下载jmeter,点此下载

    使用

    xmind-jmeter

    以上图片来自一个测试大牛,非常详细,完整的xmind文件下载见:jmeter-张蓓.xmind

    jmeter的入门级使用也可以参考最后面的参考资料部分:使用Apache Jmeter进行并发压力测试

    压力测试结果分析

    wrk GET请求压测结果

    root@ubuntu:/tmp# wrk -t10 -c5000 -d60s --latency http://127.0.0.1:8080/api/ip_query?ip=165.118.213.9
    Running 1m test @ http://127.0.0.1:8080/api/ip_query?ip=165.118.213.9
      10 threads and 5000 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   897.19ms  322.83ms   1.99s    70.52%
        Req/Sec   318.80    206.03     2.14k    68.84%
      Latency Distribution
         50%  915.29ms
         75%    1.11s 
         90%    1.29s 
         99%    1.57s 
      187029 requests in 1.00m, 51.01MB read
      Socket errors: connect 0, read 0, write 0, timeout 38
    Requests/sec:   3113.27
    Transfer/sec:    869.53KB

    ab GET请求压测结果

    root@ubuntu:/tmp# ab -r -t 60 -c 5000 http://127.0.0.1:8080/api/ip_query?ip=165.118.213.9
    This is ApacheBench, Version 2.3 <$Revision: 1796539 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, https://www.apache.org/
    
    Benchmarking 127.0.0.1 (be patient)
    Completed 5000 requests
    Completed 10000 requests
    Completed 15000 requests
    Completed 20000 requests
    Completed 25000 requests
    Completed 30000 requests
    Completed 35000 requests
    Completed 40000 requests
    Completed 45000 requests
    Completed 50000 requests
    Finished 50000 requests
    
    
    Server Software:        gunicorn/19.7.1
    Server Hostname:        127.0.0.1
    Server Port:            8080
    
    Document Path:          /api/ip_query?ip=165.118.213.9
    Document Length:        128 bytes
    
    Concurrency Level:      5000
    Time taken for tests:   19.617 seconds
    Complete requests:      50000
    Failed requests:        2
       (Connect: 0, Receive: 0, Length: 1, Exceptions: 1)
    Total transferred:      14050000 bytes
    HTML transferred:       6400000 bytes
    Requests per second:    2548.85 [#/sec] (mean)
    Time per request:       1961.668 [ms] (mean)
    Time per request:       0.392 [ms] (mean, across all concurrent requests)
    Transfer rate:          699.44 [Kbytes/sec] received
    
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0  597 1671.8      4   15500
    Processing:     4  224 201.4    173    3013
    Waiting:        4  223 200.1    172    2873
    Total:          7  821 1694.4    236   15914
    
    Percentage of the requests served within a certain time (ms)
      50%    236
      66%    383
      75%   1049
      80%   1155
      90%   1476
      95%   3295
      98%   7347
      99%   7551
     100%  15914 (longest request)

    jmeter GET请求压测结果

    结果分析

    以上三个工具的压测结果大体相同,RPS(Requests per second)大致在3000左右,此时机器配置为4核4G内存,并且gunicorn开了10个worker,内存占用3.2G。单台机器只有3000并发,对于此配置的机器来说,需要进一步分析原因。后续再弄一台机器,负载均衡后能达到5000以上才能满足使用要求。

    压力测试注意事项

    文件打开数

    压力测试时对施压机器的文件打开数一般有要求,远不止1024个open files,需要增加linux系统的文件打开数,增加方法:

    # 文件打开数
    ulimit -a
    # 修改文件打开数
    ulimit -n 500000

    SYN洪水攻击保护

    linux系统中有一个参数:/etc/sysctl.conf配置文件中的net.ipv4.tcp_syncookies字段。这个字段值默认为1,表示系统会检测SYN洪水攻击,并开启保护。因此压测时,如果发送大量重复性数据的请求,受压机器SYN队列溢出之后启用SYN cookie,导致会有大量请求超时失败。阿里云的负载均衡是有SYN洪水攻击检测和DDos攻击检测功能的,因此在做压力测试时需要注意两点:

    • 测试时适当关闭负载均衡机器的 net.ipv4.tcp_syncookies 字段
    • 造数据时应该尽量避免大量重复性数据,以免被识别为攻击。

    gunicorn简介及调优

    关于gunicorn的选择可以参考测试报告:Python WSGI Server 性能分析

    在选定gunicorn作为WSGI server之后,需要根据机器选择相应的worker数量以及每个worker的worker-class。

    worker数量选择

    每一个worker都是作为一个单独的子进程来运行,都持有一份独立的内存数据,每增加或减少一个worker,系统内存明显的成倍数的改变。最初单台机器gunicorn开启3个worker,系统只支持1000RPS的并发。当把worker扩展为9个之后,系统支持3000RPS的并发。因此在内存足够的时候,可以适当增加worker数量。

    worker-class选择

    可以参考尾部的参考资料中的gunicorn常用settingsGunicorn 几种 Worker class 性能测试比较这两篇文章。

    将gunicorn启动时的worker-class从默认的sync改成gevent之后,系统RPS直接翻倍。

    worker-class worker数量 ab测试的RPS
    sync 3 573.90
    gevent 3 1011.84

    gevent依赖:gevent >= 0.13。因此需要先使用pip安装。对应的gunicorn启动flask应用的命令需要修改为:

    gunicorn -w10 -b0.0.0.0:8080 ip_query_app:ip_app --worker-class gevent

    改进点

    改进ip数据库准确性

    损失效率换取准确性:使用单一ip数据库会存在一些ip无法查询出结果的情况,并且国外ip一般只能精确到国家。可以平衡几家ip数据库的准确度和覆盖率,当无法查询出准确的地址信息时去查询另外几个ip数据库。

    提高单台机器并发量

    从发起请求,到WSGI服务器处理,到应用接口,到ip查询每个过程都需要单独分析每秒可执行量,进而分析系统瓶颈,从根本上提高单机并发量。


    参考资料


    记得帮我点赞哦!

    精心整理了计算机各个方向的从入门、进阶、实战的视频课程和电子书,按照目录合理分类,总能找到你需要的学习资料,还在等什么?快去关注下载吧!!!

    resource-introduce

    念念不忘,必有回响,小伙伴们帮我点个赞吧,非常感谢。

    我是职场亮哥,YY高级软件工程师、四年工作经验,拒绝咸鱼争当龙头的斜杠程序员。

    听我说,进步多,程序人生一把梭

    如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个鼓励,将不胜感激。

    职场亮哥文章列表:更多文章

    wechat-platform-guide-attention

    本人所有文章、回答都与版权保护平台有合作,著作权归职场亮哥所有,未经授权,转载必究!

    展开全文
  • 高可用 Redis 服务架构方案 1 基于内存的 Redis 应该是目前各种 web 开发业务中最为常用的 key-value 数据库了我们经常在业 务中用其存储用户登陆态 Session 存储加速一些热数据的查询相比较 mysql 而言速度有数 ...
  • 基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在业务中用其存储用户登陆态(Session存储),加速一些热数据的查询(相比较mysql而言,速度有数量级的提升),做简单的消息队列...

    基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在业务中用其存储用户登陆态(Session存储),加速一些热数据的查询(相比较mysql而言,速度有数量级的提升),做简单的消息队列(LPUSH和BRPOP)、订阅发布(PUB/SUB)系统等等。规模比较大的互联网公司,一般都会有专门的团队,将Redis存储以基础服务的形式提供给各个业务调用。

    不过任何一个基础服务的提供方,都会被调用方问起的一个问题是:你的服务是否具有高可用性?最好不要因为你的服务经常出问题,导致我这边的业务跟着遭殃。最近我所在的项目中也自己搭了一套小型的“高可用”Redis服务,在此做一下自己的总结和思考。

    首先我们要定义一下对于Redis服务来说怎样才算是高可用,即在各种出现异常的情况下,依然可以正常提供服务。或者宽松一些,出现异常的情况下,只经过很短暂的时间即可恢复正常服务。所谓异常,应该至少包含了以下几种可能性:

    【异常1】某个节点服务器的某个进程突然down掉(例如某开发手残,把一台服务器的redis-server进程kill了)

    【异常2】某台节点服务器down掉,相当于这个节点上所有进程都停了(例如某运维手残,把一个服务器的电源拔了;例如一些老旧机器出现硬件故障)

    【异常3】任意两个节点服务器之间的通信中断了(例如某临时工手残,把用于两个机房通信的光缆挖断了)

    其实以上任意一种异常都是小概率事件,而做到高可用性的基本指导思想就是:多个小概率事件同时发生的概率可以忽略不计。只要我们设计的系统可以容忍短时间内的单点故障,即可实现高可用性。

    对于搭建高可用Redis服务,网上已有了很多方案,例如Keepalived,Codis,Twemproxy,Redis Sentinel。其中Codis和Twemproxy主要是用于大规模的Redis集群中,也是在Redis官方发布Redis Sentinel之前twitter和豌豆荚提供的开源解决方案。我的业务中数据量并不大,所以搞集群服务反而是浪费机器了。最终在Keepalived和Redis Sentinel之间做了个选择,选择了官方的解决方案Redis Sentinel。

    Redis Sentinel可以理解为一个监控Redis Server服务是否正常的进程,并且一旦检测到不正常,可以自动地将备份(slave)Redis Server启用,使得外部用户对Redis服务内部出现的异常无感知。我们按照由简至繁的步骤,搭建一个最小型的高可用的Redis服务。

    方案1:单机版Redis Server,无Sentinel

    57f73daa1236f3254192cf84c2bb32e1.png

    一般情况下,我们搭的个人网站,或者平时做开发时,会起一个单实例的Redis Server。调用方直接连接Redis服务即可,甚至Client和Redis本身就处于同一台服务器上。这种搭配仅适合个人学习娱乐,毕竟这种配置总会有单点故障的问题无法解决。一旦Redis服务进程挂了,或者服务器1停机了,那么服务就不可用了。并且如果没有配置Redis数据持久化的话,Redis内部已经存储的数据也会丢失。

    方案2:主从同步Redis Server,单实例Sentinel

    8b3dfb06b41dd8a35ab8f2bdbaac852d.png

    为了实现高可用,解决方案1中所述的单点故障问题,我们必须增加一个备份服务,即在两台服务器上分别各启动一个Redis Server进程,一般情况下由master提供服务,slave只负责同步和备份。与此同时,在额外启动一个Sentinel进程,监控两个Redis Server实例的可用性,以便在master挂掉的时候,及时把slave提升到master的角色继续提供服务,这样就实现了Redis Server的高可用。这基于一个高可用服务设计的依据,即单点故障本身就是个小概率事件,而多个单点同时故障(即master和slave同时挂掉),可以认为是(基本)不可能发生的事件。

    对于Redis服务的调用方来说,现在要连接的是Redis Sentinel服务,而不是Redis Server了。常见的调用过程是,client先连接Redis Sentinel并询问目前Redis Server中哪个服务是master,哪些是slave,然后再去连接相应的Redis Server进行操作。当然目前的第三方库一般都已经实现了这一调用过程,不再需要我们手动去实现(例如Nodejs的ioredis,PHP的predis,Golang的go-redis/redis,JAVA的jedis等)。

    然而,我们实现了Redis Server服务的主从切换之后,又引入了一个新的问题,即Redis Sentinel本身也是个单点服务,一旦Sentinel进程挂了,那么客户端就没办法链接Sentinel了。所以说,方案2的配置并无法实现高可用性。

    方案3:主从同步Redis Server,双实例Sentinel

    73dec8a743f7580029cc70015791b73a.png

    为了解决方案2的问题,我们把Redis Sentinel进程也额外启动一份,两个Sentinel进程同时为客户端提供服务发现的功能。对于客户端来说,它可以连接任何一个Redis Sentinel服务,来获取当前Redis Server实例的基本信息。通常情况下,我们会在Client端配置多个Redis Sentinel的链接地址,Client一旦发现某个地址连接不上,会去试图连接其他的Sentinel实例,这当然也不需要我们手动实现,各个开发语言中比较热门的redis连接库都帮我们实现了这个功能。我们预期是:即使其中一个Redis Sentinel挂掉了,还有另外一个Sentinel可以提供服务。

    然而,愿景是美好的,现实却是很残酷的。如此架构下,依然无法实现Redis服务的高可用。方案3示意图中,红线部分是两台服务器之间的通信,而我们所设想的异常场景(【异常2】)是,某台服务器整体down机,不妨假设服务器1停机,此时,只剩下服务器2上面的Redis Sentinel和slave Redis Server进程。这时,Sentinel其实是不会将仅剩的slave切换成master继续服务的,也就导致Redis服务不可用,因为Redis的设定是只有当超过50%的Sentinel进程可以连通并投票选取新的master时,才会真正发生主从切换。本例中两个Sentinel只有一个可以连通,等于50%并不在可以主从切换的场景中。

    你可能会问,为什么Redis要有这个50%的设定?假设我们允许小于等于50%的Sentinel连通的场景下也可以进行主从切换。试想一下【异常3】,即服务器1和服务器2之间的网络中断,但是服务器本身是可以运行的。如下图所示:

    b46ae6e2d772d164b503e9870cbfdbdd.png

    实际上对于服务器2来说,服务器1直接down掉和服务器1网络连不通是一样的效果,反正都是突然就无法进行任何通信了。假设网络中断时我们允许服务器2的Sentinel把slave切换为master,结果就是你现在拥有了两个可以对外提供服务的Redis Server。Client做任何的增删改操作,有可能落在服务器1的Redis上,也有可能落在服务器2的Redis上(取决于Client到底连通的是哪个Sentinel),造成数据混乱。即使后面服务器1和服务器2之间的网络又恢复了,那我们也无法把数据统一了(两份不一样的数据,到底该信任谁呢?),数据一致性完全被破坏。

    方案4:主从同步Redis Server,三实例Sentinel

    d8a48f8f5c981b0f08d2541d35e3a83a.png

    鉴于方案3并没有办法做到高可用,我们最终的版本就是上图所示的方案4了。实际上这就是我们最终搭建的架构。我们引入了服务器3,并且在3上面又搭建起一个Redis Sentinel进程,现在由三个Sentinel进程来管理两个Redis Server实例。这种场景下,不管是单一进程故障、还是单个机器故障、还是某两个机器网络通信故障,都可以继续对外提供Redis服务。

    实际上,如果你的机器比较空闲,当然也可以把服务器3上面也开启一个Redis Server,形成1 master + 2 slave的架构,每个数据都有两个备份,可用性会提升一些。当然也并不是slave越多越好,毕竟主从同步也是需要时间成本的。

    在方案4中,一旦服务器1和其他服务器的通信完全中断,那么服务器2和3会将slave切换为master。对于客户端来说,在这么一瞬间会有2个master提供服务,并且一旦网络恢复了,那么所有在中断期间落在服务器1上的新数据都会丢失。如果想要部分解决这个问题,可以配置Redis Server进程,让其在检测到自己网络有问题的时候,立即停止服务,避免在网络故障期间还有新数据进来(可以参考Redis的min-slaves-to-write和min-slaves-max-lag这两个配置项)。

    至此,我们就用3台机器搭建了一个高可用的Redis服务。其实网上还有更加节省机器的办法,就是把一个Sentinel进程放在Client机器上,而不是服务提供方的机器上。只不过在公司里面,一般服务的提供方和调用方并不来自同一个团队。两个团队共同操作同一个机器,很容易因为沟通问题导致一些误操作,所以出于这种人为因素的考虑,我们还是采用了方案4的架构。并且由于服务器3上面只跑了一个Sentinel进程,对服务器资源消耗并不多,还可以用服务器3来跑一些其他的服务。

    易用性:像使用单机版Redis一样使用Redis Sentinel

    作为服务的提供方,我们总是会讲到用户体验问题。在上述方案当中始终有一个让Client端用的不是那么舒服的地方。对于单机版Redis,Client端直接连接Redis Server,我们只需要给一个ip和port,Client就可以使用我们的服务了。而改造成Sentinel模式之后,Client不得不采用一些支持Sentinel模式的外部依赖包,并且还要修改自己的Redis连接配置,这对于“矫情”的用户来讲显然是不能接收的。有没有办法还是像在使用单机版的Redis那样,只给Client一个固定的ip和port就可以提供服务呢?

    7a9e1648030f1b4ae230640477a68bc0.png

    答案当然是肯定的。这可能就要引入虚拟IP(Virtual IP,VIP),如上图所示。我们可以把虚拟IP指向Redis Server master所在的服务器,在发生Redis主从切换的时候,会触发一个回调脚本,回调脚本中将VIP切换至slave所在的服务器。这样对于Client端来说,他仿佛在使用的依然是一个单机版的高可用Redis服务。

    结语

    搭建任何一个服务,做到“能用”其实是非常简单的,就像我们运行一个单机版的Redis。不过一旦要做到“高可用”,事情就会变得复杂起来。业务中使用了额外的两台服务器,3个Sentinel进程+1个Slave进程,只是为了保证在那小概率的事故中依然做到服务可用。在实际业务中我们还启用了supervisor做进程监控,一旦进程意外退出,会自动尝试重新启动。

    展开全文
  • 不过一旦要做到“高可用”,事情就会变得复杂起来。。基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在业务中用其存储用户登陆态(Session存储),加速一些热数据的查询(相比较...
  • 使用nginx软件负载结构图使用阿里云硬件负载均衡服务结构图因为flask app需要在内存中保存ip树以及国家、省份、城市相关的字典,因此占用内存较。gunicorn的1个worker需要占用300M内存,nginx的4个worker...
  • 也就是解决当一个服务注册中心当机后,我们可以提供多个服务注册副本来解决服务查询不到的问题,从达到在微服开发中实现高可用注册中心的功能。 开发环境:IDEA集成工具,JDK 1.8 使用步骤:下载资源后解压项目,...
  • 基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在业务中用其存储用户登陆态(Session存储),加速一些热数据的查询(相比较mysql而言,速度有数量级的提升),做简单的消息队列...
  • 3 限流在开发并发系统时,有很多手段...而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页)等。因此,需要有一种手段来限制这些场景下的
  • 基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在业务中用其存储用户登录态(Session存储),加速一些热数据的查询(相比较mysql而言,速度有数量级的提升),做简单的消息队列...
  • 高可用服务设计概述[1] 1. 负载均衡与反向代理 当我们的应用单实例不能支撑用户请求时,就需要扩容,从一天服务器扩容到两台、几十台、几百台。然而用户访问时是通过如http://www.jd.com的方式访问,在请求时...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,466
精华内容 586
关键字:

高可用查询服务