系统设计练习 - 分布式黑名单服务(design a distributed denylist service)

背景

设计一个分布式服务,根据一个集中管理的黑名单决定一个请求是否要被block还是allow。例如一个请求:

复制代码
Request:
    user_id = 12345
    ip = 1.2.3.4
    device_id = abc123

调用者需要知道

复制代码
response:
    allowed or denied

需求

功能需求

  1. 用户发送请求,得到该请求allowed或者denied的response
  2. 黑名单包含不同类型的标识符
    1. user ID
    2. IP address
    3. device ID
    4. API key
    5. service ID
  3. 管理员可以从黑名单里删除或者增加条目

非功能需求

  1. high QPS
  2. low latency
  3. high availability
  4. updates to the denylist should propagate quickly
  5. millions or billions of denylist entries

API

  1. 发送request:identifier可能从多个地方获取,例如request body,http header,或者url parameter,或者security token。这里我们简化为从request body里获取。

    POST /denylist/check
    {
    "identifiers": [
    {
    "type": "IP",
    "value": "1.2.3.4"
    },
    {
    "type": "USER_ID",
    "value": "user1234"
    }
    ]
    }

    Response:
    {
    "result": "deny",
    "matched": [
    {
    "type": "IP",
    "value": "1.2.3.4"
    }
    ]
    }

  2. 查看黑名单

    GET /denylist?offsize=100;size=10

    response:
    {
    "denylist": [
    {
    "userId": "id1",
    "ipAddress": "180.9.0.1",
    "deviceId": "u12345",
    "apiKey": "2xdrfe2ddf",
    "serviceId": "service1"
    }
    ],
    "createtime": 123456789,
    "updatetime": 123456789
    }

  3. 删除黑名单

    DELETE /denylist?userId={userId}&ipaddress={1.2.3.4}&deviceId=device1&apiKey=1234&servcieId="serviceId1"

  4. 增加黑名单

    POST /denylist
    BODY:
    [
    {
    "userId": "id1",
    "ipAddress": "180.9.0.1",
    "deviceId": "u12345",
    "apiKey": "2xdrfe2ddf",
    "serviceId": "service1"
    }
    ]

架构

我们的系统架构如下:

说明如下:

  1. user发送的request,通过API Gateway被转送到deny-check-service。这个service本身是stateless的,所以可以做scale out来应对不同的traffic。deny-check-service首先去cache查找对应的identifier是否在deny list里。这个cache可以用redis。本质上仍然是key-value-pair查询。redis的记录设置TTL来防止某条记录没有被即使更新而造成的判断错误。如果cache没有命中,deny-check-service查找deny-list-DB。这里我们仍然仅仅需要key-value-pair查询,因此推荐使用DDB。

  2. user发送的deny list的更改请求,例如增加,删除,被转发给deny-list-service。这个service更改deny-list-DB里的记录。在个别情况下更新可能会有conflict,例如两个请求几乎同时到达,向deny-list-DB对同一个key做不同的update。根据不同的业务需求,我们会有不同的设计。如果业务角度看更改的order不是非常严格,比如几乎同时到达的request,先处理谁都是可以的。那我们不需要特殊逻辑,直接update就可以。如果order的要求很严格,我们可以考虑用消息到达的时间作为记录的update-time,然后使用optimistic lock,也就是DDB的optional update,来对记录进行更新,也就是只能是update-time大的请求更改DDB里的记录;update-time已经过时的请求不被处理。

  3. deny-list-DB上enable data stream,也就是CDC。deny-cache-update-worker处理data stream里的message,同步cache。这里注意,对于我们来说deny-list-DB是source of truth。

假如我们的deny-list-DB不可用了怎么办?这里又涉及到业务要求。普遍的做法可能是暂时允许所有的request通过。或者因为访问的信息比较敏感,则暂时不允许任何request通过。

讨论

现在我们进一步考虑这个问题。假如请求deny判断的request非常多,我们应该如何优化我们的设计?优化之一,我们可以考虑使用bloom filter。这样我们的存储空间会被大大减少。同时带来的问题是我们有一定概率得到false positive case,也就是说某个请求的identifier不在deny list,但是它恰好和一个在deny list里的identifier算出的hash key都一样。所以这是一个需要从业务角度回答的问题,少量的false positive case行不行?另一个问题是在deny list被更新时,bloom filter如何被update。

我们先回答第二个问题。当deny list被更新时,一种更新bloom filter的方法是重建bloom filter list。这会引起bloom filter的暂时不可用。我们也可以设置warm standby的bloom filter,这样可以先重建,再switch,来避免bloom filter的暂时不可用。另一种处理bloom filter更新的方法是使用counting bloom filter。具体的含义可以搜索外部材料。

现在回答第一个问题。首先我们在bloom filter之前设置一个deny list更新的log。这个log是deny list的south of truth。如果bloom filter crash了,我们可以利用log的信息重新建立bloom filter。但是如果我们不允许false positive case,我们可以考虑将bloom filter作为第一层check。考虑到大多数request应该不会被deny,bloom filter将为我们处理大多数的请求。对于positive case,我们再通过deny-check-service查看cache或者DB。系统此时的架构如下:

相关推荐
江畔柳前堤8 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
霸道流氓气质13 小时前
RabbitMQ 延迟队列与死信队列:原理、实现与实战
分布式·rabbitmq·ruby
爱吃面的猫14 小时前
大数据Kafka3.x之——Kafka3.3.0安装与使用(详细)
大数据·分布式·zookeeper
火云牌神20 小时前
如何用项目规则给 AI 划定编码边界
人工智能·系统架构·ai编程·vibecoding
小张同学a.1 天前
zabbix企业级监控平台4——分布式监控与grafana数据可视化
linux·运维·数据库·分布式·信息可视化·zabbix·grafana
@insist1231 天前
系统集成项目管理工程师-架构基础与系统架构
架构·系统架构·软考·系统集成项目管理工程师·软考中项·软件水平考试
科技之门1 天前
精密制造设备国产替代提速:机器视觉系统架构解析与选型评估指南
系统架构·制造
番茄炒鸡蛋加糖1 天前
分布式 CAP、BASE 理论 & 中间件 CAP 取舍
分布式·中间件
海上小飞龙2 天前
Redis 分布式锁原理:从 SET NX EX 到 Redisson 看门狗
数据库·redis·分布式