- 工程实践 - 《QPS百万级的无状态服务实践》01 - 从单机到集群

         本文属于专栏《构建工业级QPS百万级服务》


        继续上篇《工业级QPS百万级服务从0到1概述》 ,需求是“给系统传入两个日期,计算间隔有多少天”。我们逐步实现,看看我们会遇到哪些问题,以及业界的一般解决方案是什么。

        我们很快能写出服务代码:

from flask import Flask, request, jsonify
from datetime import datetime

app = Flask(__name__)

@app.route('/days_between', methods=['GET'])
def days_between():
    date_format = "%Y%m%d"  # 定义日期格式
    start_date_str = request.args.get('start_date')  # 从请求中获取开始日期
    end_date_str = request.args.get('end_date')  # 从请求中获取结束日期

    # 尝试将字符串日期转换为datetime对象
    try:
        start_date = datetime.strptime(start_date_str, date_format)
        end_date = datetime.strptime(end_date_str, date_format)
    except ValueError:
        # 如果有错误,返回错误信息
        response = {"error": "Invalid date format. Please use YYYYMMDD."}
        return jsonify(response), 400

    # 计算日期差异
    delta = end_date - start_date
    days_between = delta.days

    # 返回结果
    response = {"days_between": days_between}
    return jsonify(response)

if __name__ == '__main__':
    app.run(debug=True)  # 启用多线程

        以及简单的压测工具代码

import requests
import threading
import time

# 测试的目标URL
url = "http://127.0.0.1:5000/days_between?start_date=20240103&end_date=20240108"

# 并发数和请求总数
concurrent = 10000
total_requests = concurrent * 10

# 请求函数
def send_request():
    response = requests.get(url)

# 创建并启动线程
threads = []
start_time = time.time()
for i in range(total_requests):
    t = threading.Thread(target=send_request)
    threads.append(t)
    t.start()

# 等待所有线程完成
for t in threads:
    t.join()
end_time = time.time()

duration_time = end_time - start_time

print(f"Total Duration: {duration_time:.2f} seconds")
print(f"avg qps: {total_requests/duration_time}")

        经过本地测试,我们发现,运行在单核CPU上,服务能处理的最大QPS只有1000。那么要接受百万QPS的请求,我们需要1000CPU核。目前没有单机可以拥有这么多CPU核,所以我们需要多台机器来处理这些请求。

        增加机器的第一个问题来了,我们希望每台机器收到的请求量基本一致,但是每台机器ip不一样,单个用户并不感知其他用户的存在,那用户怎么知道发送给哪个ip,才能保证服务端每个机器负载是近似的呢。所以我们需要一个专门帮助我们分发请求的机器。于是架构很自然的由图1演变为图2。

- 工程实践 - 《QPS百万级的无状态服务实践》01 - 从单机到集群_第1张图片

图1 

- 工程实践 - 《QPS百万级的无状态服务实践》01 - 从单机到集群_第2张图片

图2

        那么为什么负载均衡服务器能扛住更大的请求量,因为一般来说其机器配置更好,相比业务逻辑计算量更小,甚至可以是专门用于做负载均衡的硬件。但是再好的单机,能力也是有限的,如果流量再扩张,一台做负载均衡的服务器不够用了怎么办。这个时候通常的做法,是做一个服务A,管理所有负载均衡服务的ip列表,用户在发送请求时,先根据业务名称,去A查询下面有哪些负载均衡服务ip,然后服务A根据用户的信息,提供部分ip,这样,不同用户拿到不同的ip,保证负载均衡服务的请求量级是大体一致的。而业务名称,就是所谓的域名,服务A,就是DNS(域名系统)。于是架构又从图2演变为图3。

- 工程实践 - 《QPS百万级的无状态服务实践》01 - 从单机到集群_第3张图片

图3        

          对于小流量,负载均衡使用一台机器,安装nginx足够,但是流量增大之后,可扩展性、高可用性、安全性等会是一个问题,使用亚马逊的ELB,或者阿里云的SLB等云厂商的负载均衡解决方案,会是更省心的选择。当然如果体量更大,那就需要自建团队,构建最适合自己业务的负载均衡服务。

        图3的架构中,每个服务的压力都是有限的,在一定程度上,该系统已经能够很方便地给用户提供服务了。如果现在你是一个负责服务系统维护和迭代的同学,相比图1的系统,在现实中你还会遇到新的问题:

  • 业务代码更新
    • 新的业务代码如何部署到每台服务器上正确运行的
    • 如果机器之间有环境导致运行行为不一致,怎么办
    • 业务代码,是很多人一起开发的,版本会不会乱
    • 代码更新了,我怎么测试,才能保证上线后符合预期,总有用户发送奇怪的请求,有什么好办法提前检测呢
  • 线上问题
    • 突然一台行为异常怎么办,比如请求跌0了,或者CPU 100%了,或者延迟特别高
    • 需要实时监控服务的日志,比如整个集群今天一共接受了多少查询。每天机器都手动登陆统计,然后汇总太费劲了
    • 如果服务因为任何原因,不能给用户提供正确的服务了,怎么能第一时间发现,而不是在用户抱怨之后才知道发生问题了
    • 一台机器突然断电重启了怎么办,所在机房光纤被挖断了怎么办(真实发生过)
  • 成本
    • 白天用户qps能到百万,但是凌晨3点,用户请求qps只有5千,凌晨的机器有些浪费怎么办

        这些问题我会在后续《QPS百万级的无状态服务实践》的部分,给出我的建议。


下一篇:《QPS百万级的无状态服务实践》02

你可能感兴趣的:(构建工业级QPS百万级服务,系统架构,python,c++)