Algorithm
本周选择的算法题是:Combination Sum
规则如下:
Given a set of candidate numbers (candidates
) (without duplicates) and a target number (target
), find all unique combinations in candidates
where the candidate numbers sums to target
.
The same repeated number may be chosen from candidates
unlimited number of times.
Note:
- All numbers (including
target
) will be positive integers. - The solution set must not contain duplicate combinations.
Example 1:
Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
[7],
[2,2,3]
]
Example 2:
Input: candidates = [2,3,5], target = 8,
A solution set is:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
Solution
我实现的方案:
Runtime:48 ms,快过 96.74%。
Memory:12.9 MB,低于 100%。
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
dp = [[[]]] + [[] for length in range(target)]
for candidate in candidates:
for index in range(candidate, target + 1):
position = index - candidate
dp[index] += [combination + [candidate] for combination in dp[position]]
return dp[target]
这是在 IDE 里反复调试并优化后的版本,最初的版本如下:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
result = []
self.dp(candidates, 0, [], target, result)
return result
def dp(self,
nums: List[int],
sum: int,
list_to_sum: List[int],
target: int,
result: List[List[int]]):
if len(nums) == 0: return
num = nums[0]
if sum + num == target:
result.append(list_to_sum + [num])
return
elif sum + num > target:
return
self.dp(nums, sum + num, list_to_sum + [num], target, result)
self.dp(nums[1:], sum + num, list_to_sum + [num], target, result)
self.dp(nums[1:], sum, list_to_sum, target, result)
性能上大打折扣:
- 过多的重复计算
- 需要对结果去重
Review
These four “clean code” tips will dramatically improve your engineering team’s productivity
作者在文章提到了一些想法:
- “如果它没有经过测试,那它就是坏的” - 做大量测试,特别是单元测试,否则你会后悔
- 选择有意义的名称 - 使用简短、精确的变量名、类名和方法名
- 类和方法要足够小到符合单一职责 - 它们应该只做一件事
- 方法应该没有副作用 - 不能修改入参、满足幂等设计
基本的概念就跳过了,看看这段代码:
function getUserByEmailAndPassword(email, password) {
let user = UserService.getByEmailAndPassword(email, password);
if (user) {
LoginService.loginUser(user); // Log user in, add cookie (Side effect!!!!)
}
return user;
}
有几个问题:
- 有副作用。通过方法名能猜到这是一个查询用户的接口,除此之外这个方法还做了登录并写入 cookie,这个逻辑有可能会写在文档里,但是对现代的 IDE 来说,代码提示已经很智能了,程序员并不一定会先读文档再写代码,这是一个潜在的风险
- 不容易测试。在单元测试中你需要构建依赖,我们希望依赖越少越好,这样测试的效率才高,由于该方法内部需要走网络请求,你不得不在测试“根据邮箱和密码返回用户”时构建一个 HTTP 请求或者 mocking。
- 耦合。第2点谈到了可测试性,依赖多了耦合就多了,查找用户和登录用户应该是两个功能,这是没必要的耦合,而且这个耦合很可能会在未来被解掉,所以这个方法并不安全 - it’s not “future proof”。
Tip
本周正式接入了自动化测试。为了提高测试效率,我们引入了自动化更新、部署,主要是采用 git + watchdog 的方式实现。
为了改善 UAT(User Acceptance Test) 效率,我们在平台接入了自研的极速 UAT 方案,后续的性能瓶颈可能会集中到服务器。
Share
一个完整的 PaaS 平台包括:
- 调度层 - PaaS 的自动化和分布式对高可用、高性能的管理
- 能力服务层 - PaaS 对外提供的服务和能力
- 流量调度 - 流量调度相关的东西,含对高并发的管理
- 运营管理 - 软件资源库、软件接入、认证和开放平台门户
- 运维管理 - DevOps 相关的东西
弹力设计
要提高系统的可用性,需要在系统中引入弹力设计。弹力设计又叫容错设计,容错能力包括:
- 服务治理 - 服务隔离、异步调用、请求幂等性
- 可伸缩性 - 有/无 状态的服务
- 一致性 - 补偿事务、重试
- 应对大流量的能力 - 熔断、降级
弹力设计以提高系统的可用性为保障的重点。
可用性计算公式为:
Availability = MTTF / (MTTF + MTTR)
MTTF - Mean Time To Failure,平均故障前的时间
MTTR - Mean Time To Recovery,平均修复故障的时间
要提高可用性,要么提高 MTTF,要么降低 MTTR。
分布式系统故障发生是常态,一般有以下几种原因的故障:
- 网络问题。带宽拥塞、链接不稳定等
- 性能问题。数据库慢 SQL,Java Full GC,硬盘 IO 过大,CPU 使用率过高,内存不足等
- 安全问题。被网络攻击,如 DDoS 等
- 运维问题。系统总是在被更新和修改,架构也在不断地被调整,监控问题等
- 管理问题。没有梳理出关键服务以及服务依赖、调用的“地图”,运行信息没有和控制系统同步等
- 硬件问题。硬盘损坏,网卡出问题,交换机出问题,机房问题等
分布式下不要尝试去避免故障,要将故障的处理逻辑当成正常的功能做在架构里:
- 故障是正常的,而且是常见的
- 故障是不可预测突发的,而且相当难缠
好的弹力设计就是想尽一切办法降低 MTTR。
服务隔离
服务隔离就是将系统分离开,有两种分离方式:
- 按服务分离 - 每个服务的域名、服务器、数据库独有,服务间完全隔离
- 按用户分离 - 将用户分成不同的组,组与组隔离,即多租户模式
多租户按隔离程度有三种方式:
- 完全独立 - 每个租户都有自己完全独立的服务和数据
- 独立的数据分区,共享的服务 - 服务间共享,数据分开隔离
- 共享的数据,共享的服务 - 每个租户的数据和服务都是共享的
隔离设计的重点和考量:
- 需要清楚地定义隔离业务的大小和粒度
- 需要结合系统的复杂度、成本、性能、资源使用的情况,找出一个合适的均衡方案,不存在什么都满足的系统
- 需要搭配一些高可用、重试、异步、消息中间件,流控、熔断等设计模式
- 需要结合自动化运维的管理工具,驾驭复杂度
- 需要一个服务“地图”,能监控完整的服务状态、依赖
异步调用
异步调用可以让服务间的解耦更彻底,同时可以起到削峰的作用。
同步调用会带来的问题:
- 整个调用链的性能由最慢的那个服务决定
- 调用链的参与方会有相同的等待时间,消耗资源
- 同步调用很难做到一对多
- 同步调用容易产生多米诺骨牌效应
异步通讯相对于同步来说,除了可以提高系统的吞吐量,最大的一个好处是可以让服务间的解耦更为彻底,调用方和被调用方可以按照自己的速率而不是步调一致,让系统更有弹力。
异步通讯的三种方式:
- 请求响应式:
- 发送方时不时轮询一下
- 调用多将回调地址给被调用方
- 订阅的方式 - 观察者的形式
- 通过 Broker 的方式 - 由 Broker 中间件解耦订阅的双方,事件驱动的最佳实践
事件驱动的好处:
- 服务间的依赖没有了
- 开发、测试、运维、故障处理都是高度隔离的
- 服务间通过事件关联,不会相互 block
- 服务间容易增加一些 Adapter(如日志、认证、版本、限流、降级、熔断等)
- 各服务可以按照自己的速率来处理事件
异步通讯的开发注意事项:
- Broker 成了关键,需要设计成高可用不丢消息的
- 流程不直观,需要有相应的跟踪机制
- 需要有一个总控方管理状态
- 需要考虑像 TCP 那样的 send 和 ACK 机制,服务的幂等性
请求幂等性
幂等性的定义是一次或多次调用具有相同的副作用。以 HTTP 请求 Method 来说:
- GET - 虽然请求的返回数据可能不一样,但没有产生副作用,所以是幂等的
- HEAD - 和 GET 一样,但是仅返回头信息,所以也是幂等的
- OPTIONS - 获取请求 URL 支持的全部 Method,幂等
- DELETE - 有副作用,但是是幂等的
- POST - 每次调用会创建资源,所以不是幂等的
- PUT - 每次更新同一个资源,有副作用,但是是幂等的
除此之外要做到幂等性,还需要有全局唯一的 ID,特别是在分布式的场景下,可以参考 Twitter 的 Snowflake 算法:
- 它是一个分布式 ID 的生成算法
- 产生的是一个 Long 型的 ID
- 1bit,总是为 0
- 41bits 作为毫秒数,大概可以用 69.7 年
- 10bits 作为机器编号(5bits 是数据中心,5bits 是机器 ID),支持 1024 个实例
- 12bits 作为毫秒内的序列号,一毫秒可以生成 4096 个序号
服务的状态
可伸缩的系统对服务的状态管理要求比较高,无状态的服务特点是没有副作用,可轻松扩展和运维。因此无状态的服务被当作分布式服务设计的最佳实践。
为了做出无状态的服务,我们需要将服务的状态存储到其他地方,比如 Redis、MySQL、ZooKeeper/Etcd
等第三方存储服务中,这些服务必须也做成高可用和高拓展,同时为了减少网络开销,无状态的服务需要增加缓存机制。
无论服务是否带有状态,底层的分布式数据库或分布式文件系统都是系统的关键组成部分,它可以:
- 减少服务启动或恢复的时间,变相提高系统的可用性
- 减少节点间的通讯开销
- 数据持久化,避免状态丢失