CodingTour
ARTS #32

Algorithm

本周选择的算法题是:Combination Sum II

规则如下:

Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.

Each number in candidates may only be used once in the combination.

Note:

  • All numbers (including target) will be positive integers.
  • The solution set must not contain duplicate combinations.

Example 1:

Input: candidates = [10,1,2,7,6,1,5], target = 8,
A solution set is:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

Example 2:

Input: candidates = [2,5,2,1,2], target = 5,
A solution set is:
[
  [1,2,2],
  [5]
]

Solution

我实现的方案:

Runtime:36 ms,快过 97.99%。

Memory:12.7 MB,低于 100%。

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        result = []
        self.recursive(0, candidates, target, result, [])
        return result
    
    def recursive(self, start: int, candidates: List[int], target: int, result: List[List[int]], path: List[int]):
        if target == 0:
            result += [path]
            return

        for index in range(start, len(candidates)):
            candidate = candidates[index]

            if index > start and candidate == candidates[index - 1]:
                continue

            if candidate > target:
                return
                
            self.recursive(index + 1, candidates, target - candidate, result, path + [candidate])

参考了评论区的做法。

Review

Basic Performance Optimization in Django

如果你无法衡量它,那么也无法改善它。“

– Lord Kelvin

作者分享了一些 Django 性能优化的技巧,常见的技巧略过,因为 Django 的文档已经很全面了,我从该文中发现了几个我不曾用过的:

line_profiler

一个 Python 库,需要通过 pip 安装,使用方式:

def books_by_library_id_view(request):
    from IPython import embed; embed()
    books_by_library_id = get_books_by_library_id()
    ...
    return HttpResponse(response)

生成结果:

SQL Logging

只需配置一下就能打印每一条 SQL 语句执行的日志:

# settings.py
LOGGING = {
    'version': 1,
    'filters': {
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        }
     },
     'handlers': {
        'console': {
            'level': 'DEBUG',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
         }
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        }
    }
}

timeit

Python 内置的模块:

In [12]: timeit(get_books_by_library_id, number=10)
Out[12]: 6.598360636999132
In [13]: timeit(get_books_by_library_id_one_query, number=10)
Out[13]: 0.677092163998168

Tip

这是一个好工具,让我帮你百度一下:)

Share

降级设计

降级设计的目的:解决资源不足和访问量过大的问题。

降级的 trade-off:

  • 降低一致性 - 从强一致性变成最终一致性
  • 停止次要功能 - 停止访问不重要的功能,从而释放出更多的资源
  • 简化功能 - 把一些功能简化掉,简化业务流程,或者只返回部分数据

降级设计的重点:

  • 梳理业务功能,哪些是 must-have,哪些是 nice-to-have
  • 需要牺牲一致性,读操作可以考虑缓存来解决,写操作需要异步调用,需要有完整的日志记录
  • 提供一个系统的配置开关
  • 数据降级需要前端配合
  • 数据传输需要有一个字段区分当前是否在降级中

弹力设计总结

为了向用户承诺 SLA,服务不能是单体的,需要在架构中冗余服务,需要用到的技术:

  • 负载均衡 + 服务健康检查 - 可以使用像 Nginx 或 HAProxy 这样的技术
  • 服务发现 + 动态路由 + 服务健康检查 - 比如 Consul 或 ZooKeeper
  • 自动化运维 - Kubernetes 服务调度、伸缩和故障迁移

接着是服务隔离,需要对服务进行拆分和解耦:

  • bulkheads 模式 - 业务分片、用户分片、数据库分片
  • 自包含系统 - 单体应用到微服务的中间状态,把一组密切相关的微服务给拆分出来,没有外部依赖即可
  • 异步通讯 - 服务发现、事件驱动、消息队列、业务工作流
  • 自动化运维 - 需要一个服务调用链和性能监控的监控系统

接着是容错设计:

  • 错误处理 - 调用重试 + 熔断 + 服务幂等性设计
  • 一致性 - 强一致性使用两阶段提交、最终一致性采用异步通讯
  • 流控 - 使用限流 + 降级
  • 自动化运维 - 网关流量调度,服务监控

分布式锁

分布式系统下需要有一个分布式锁,这个锁的特点是:

  • 安全性 - 任意时刻具有排他性
  • 避免死锁 - 客户端最终一定可以获取到锁
  • 容错性 - 只要锁服务的大部分节点存活,客户端就可以执行加解锁操作

常见的锁服务设计:

  • 需要一个过期时间
  • 客户端获取锁时需要生成一个唯一值
  • 客户端需要获取、比较数据的版本号
  • 使用数据库作为锁服务

锁服务设计的重点:

  • 锁还不回来怎么办?需要有锁一定被释放的方式
  • 如果出现了两个进程拿到了同一个锁:
    • 用 CAS(Compare And Swap)保证不出问题
    • 用数据库的乐观锁保证不出问题
  • 高可用
  • 提供非阻塞方式的锁服务
  • 考虑锁的可重入性

配置中心

动态配置的区分维度:

  • 按运行环境分 - 开发、测试、预发、生产等
  • 按依赖分 - 内部配置和外部配置,如 MySQL 的连接配置
  • 按层次分 - 基础层、中间平台层、应用层等

配置中心的设计重点:

  • 配置项应该由专人(运维或者架构师)维护:
    • key 应该有命名规范,有名字空间
    • value 应该是选项
  • 外部配置放在服务发现系统中
  • 需要考虑不同的运行环境下使用不同的配置,如开发环境和测试环境
  • 需要有一个整体的版本管理,最好能与软件的版本号关联
  • 需要有一个配置管理工具,可能是命令行的,也可以是 web 的
  • 配置更新要作为一个事务处理
  • 需要有一个配置更新的控制器

边车模式

边车模式的核心作用:控制和逻辑分离,从而提高系统整体的稳定性和可用性。

对于标准化的组件和模块,一般有两种设计方式:

  • 通过 SDK、Lib 或 Framework - 与应用密切集成,有利于资源的利用和应用的性能,但是对应用有侵入性,容易受应用的编程语言和技术限制;需要与应用同步更新与编译
  • Sidecar(边车模式) - 对应用没有侵入性,并且不受应用语言和技术的限制;但是 RPC 增加了应用的延迟与服务的依赖性,大大增加管理、托管、部署的复杂度

Sidecar 设计重点:

  • 进程间采用网络调用的方式来通信
  • 两层服务协议 - 对内贴近本地服务;对外尽量开发、标准化
  • 不要把业务逻辑设计放到 Sidecar 中
  • 考虑上下文的传递,如 HTTP 请求头

Sidecar 适用的场景:

  • 老应用的改造
  • 对对语言的分布式服务系统进行管理和扩展
  • 应用服务由不同的供应商提供

Sidecar 不适用的场景:

  • 架构并不复杂的时候,API Gateway 或者 Nginx 和 HAProxy 就能搞定
  • 服务间的协议不标准且无法转换
  • 不需要分布式架构

服务网格

什么是 Service Mesh:专注于处理服务和服务间的通讯,负责构造一个稳定可靠的服务通讯基础设施,并让整个架构更为先进和 Cloud Native:

  • 一个基础设施
  • 一个轻量的服务通讯的网络代理
  • 对于应用服务来说是无侵入的
  • 用于解耦和分离分布式系统架构中控制层面上的东西

设计重点:

  • Service Mesh 需要调度流量,可能导致服务的异常运行
  • Service Mesh 一定要是高可靠或者出现了故障有 workround 的方式:
    • 可以为集群部署一个集中式的 Sidecar,为本机的 Sidecar 兜底
  • 作为基础设施独立部署,需要和 K18S 密切结合

网关模式

网关需要的功能:

  • 请求路由 - 调用端不需要知道自己需要用到的其他服务的地方,全部统一地交给 Gateway 来处理
  • 服务注册 - 后端的服务实例可以把其提供服务的地址注册、取消注册,对 restful 来说,注册是针对 URI、method、HTTP 头
  • 负载均衡 - 网关需要在各个对等的服务实例上做负载均衡策略
  • 弹力设计 - 网关可以集成弹力设计中的那些异步、重试、幂等、流控、熔断、监控等,让应用服务只关心自己的业务逻辑(数据面)而不是控制逻辑(控制面)
  • 安全方面 - SSL 加密及证书管理、Session 验证、授权、数据校验,以及对请求源进行恶意攻击的防范

网关还可以做一些有趣的事:

  • 灰度发布 - 可以对相同服务不同版本的实例进行导游,还可以收集相关的数据,对软件质量的提升和产品试错都有非常积极的意义
  • API 聚合 - 可以帮助用户端将多个单独请求聚合成一个请求
  • API 编排

和网关相似的设计模式:

  • Sidecar - 主要用来改造已有的服务,避免“政治复杂度”太高的问题
  • Service Mesh - Sidecar 越来越多,需要统一管理的控制器,在这个控制器中,将非业务功能的东西全部实现在 Sidecar 和 Controller 中,形成一个网格,业务方只需要把服务放进这个网格即可
  • Gateway - Service Mesh 的架构和部署太过于复杂,可以将 Sidecar 的粒度变为可粗可细

网关设计的重点:

  • 高性能
  • 高可用
    • 集群化 - 组成一个集群,并可以自己同步集群数据,而不依赖第三方系统
    • 服务化 - 不间断的情况下修改配置
    • 持续化 - 需要重启时,新的请求被分配到新的进程中,老的进程处理完后退出
  • 高扩展 - 由于需要承接流量和请求,或多或少会有一些业务上的东西,一个好的 Gateway 应该是可扩展和能二次开发的,像 Serverless 和 FaaS 那样

运维方面的设计原则:

  • 业务松耦合,协议紧耦合
  • 应用监控,提供分析数据
  • 用弹力设计保护后端系统
  • DevOps

架构方面的设计原则:

  • 不要直接在网关的代码里内置聚合后端服务的功能,考虑做成插件或形成 Serverless 服务
  • 网关应该靠近后端服务,并和后端服务在同一个内网中
  • 需要成为一个集群来分担前端的流量
  • 对于服务发现可以做一个时间不长的缓存,避免每次请求都去查一个相关服务的地址
  • 为网关考虑 bulkhead 设计,用不同的网关服务不同的后端服务

安全方面的考虑:

  • 加密数据
  • 校验用户请求
  • 检测异常访问