ReZero's Utopia.

Think More

Word count: 4.5kReading time: 17 min
2020/09/23 Share

Think More

https://time.geekbang.org/column/intro/48

错误码的处理

常见设计

  1. Cerrno 是记录系统的最后一次错误代码

    • 错误具有歧义性,比如 0 不能区分是不是真的出错还是的确返回值就是 0
    • 错误不够显式,常常忘记检查
  2. Win 的 HRESULT

    • 错误变成了出参,导致接口变得不够纯净
    • 仍然错误不够显式
  3. Go 的处理:函数的返回值附带一个 error 作为异常返回,当出现异常时其不为 nil

    • 是接口类型,只包含了一个字符串做描述,因此可以自己做实现
    • Go 的设计者觉得 try/catch 机制的使用太泛滥了,而且从底层向更高的层级抛异常太耗费资源。他们给 Go 设计的机制也可以 “捕捉” 异常,但是更轻量,并且只应该作为(处理错误的)最后的手段
    • 这样带来的问题就是无穷多的 if err.(switchError)

资源清理

  1. 传统清理使用 goto
    • 弊端就是万一中间不小心 return 了就凉凉
      1
      2
      3
      4
      5
      6
      7
      8
      if ( fname == NULL ){    
      goto fail;
      }
      if ( lname == NULL ){
      goto fail;
      }
      ......
      fail: FREE(fname); FREE(lname);
  1. C++ 的构造申请资源析构释放资源(相当舒服)

  2. Go 的 defer:defer后面的表达式会被放入一个列表中,在当前方法返回的时候,列表中的表达式就会被执行。

感觉 defer 更灵活些,C++ 还需要额外的创建封装

异常捕获处理

try-catch-finally 是最好的处理模式,个人感觉这像是一种 switch-goto,之前在做导入的时候需要做大量的校验。
但最终其实都要返回统一的异常,在遇到那种严重影响到后续校验的逻辑时个人感觉应该抛出而不是做进一步的猜测空指针的空指针。

优点

  • 首先异常可以自由扩展借助多态
  • 其次必须Get到所有的异常,即便是Catch后不处理,那也是显式Get到了异常
  • 与返回码相比,有着嵌套调用与链式调用的优势:耗子叔这里的意思好像是因为返回码需要接到之后处理再返回
  • 如果是上面说的这个意思,那错误码的一般解决方式就是前面提到的连接中使用 wrapper 作为入参来处理的

问题

  • 异常抛出函数直接return需要清理上下文(栈跟踪,记录哪行抛的异常,调用信息等),开销大
  • 异步问题,线程抛出异常无法被调用线程捕获到

小结

对于我们并不期望会发生的事,我们可以使用异常捕捉:比如系统调用出错切换调用方式时判定

对于我们觉得可能会发生的事,使用返回码:比如状态码 404

异步处理

  1. 回调函数: 回调地狱

  2. Promise 模式

    1
    2
    3
    4
    5
    doSomething()
    .then(result => doSomethingElse(result))
    .then(finalResult => {
    console.log(`Got the final result: ${finalResult}`);
    }).catch(failureCallback);
    • java 中的 CompletableFuture
  3. async 和 await

    • async 变成异步,不会阻塞主线程,await变同步,等待返回结果

    • 这样就可以继续使用 try catch了

      1
      2
      3
      4
      5
      6
      7
      8
      9
      async function foo() {
      try {
      let result = await doSomething();
      let finalResult = await doThirdThing(newResult);
      console.log(`Got the final result: ${finalResult}`);
      } catch(error) {
      failureCallback(error);
      }
      }

大结

  1. 统一分类的错误字典 404 502
  2. 同类错误的定义最好是可以扩展的 接口 多态
  3. 定义错误的严重程度 debug info
  4. 错误日志的输出最好使用错误码,而不是错误信息 PageNotFound Instead Of 404
  5. 处理错误时,总是要清理已分配的资源
  6. 向上尽可能地返回原始的错误
  7. 忽略错误最好有日志
  8. 对于同一个地方不停的报错,最好不要都打到日志里
  9. 不要用错误处理逻辑来处理业务逻辑 业务逻辑应该用 if else
  10. 对于同类的错误处理,用一样的模式
  11. 尽可能在错误发生的地方处理错误
  12. 向上尽可能地返回原始的错误
  13. 处理错误时,总是要清理已分配的资源
  14. 不推荐在循环体里处理错误
  15. 不要把大量的代码都放在一个 try 语句块内
  16. 为你的错误定义提供清楚的文档以及每种错误的代码示例
  17. 对于异步的方式,推荐使用 Promise 模式处理错误
  18. 对于分布式的系统,推荐使用 APM 相关的软件 尤其是使用 Zipkin 这样的服务调用跟踪的分析来关联错误。

浮点数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;

y = number;
这行代码就是把一个浮点数的 32bits 的二进制转成整型。
也就是,前面我们例子里说过的,3.1432bits 的二进制是:01000000010010001111010111000011
整型是:1078523331。即 y = 3.14,i = 1078523331
i = * ( long * ) &y; // evil floating point bit level hacking
Y 约等于 R - X/2
R = 0x5f3759df 代入
// 猜出的近似值
i = 0x5f3759df - ( i >> 1 ); // what the fuck?

// 牛顿平方根逼近
x2 = number * 0.5F;
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// 2nd iteration, this can be removed
// y = y * ( threehalfs - ( x2 * y * y ) );

return y;
}
  • 第一段占 1bit,表示符号位。 [0, 1] 代称为 S(sign)。
  • 第二段占 8bits,表示指数。 [-127, 128] 代称为 E(Exponent)。
  • 第三段占 23bits,表示尾数。 [0, 2^(23) - 1] 代称为 M(Mantissa)。
    • 也就是说,把 2n 到 2n+1 分成了 8388608 个线段。而存储的 M 值,就是从 2n 到 x 要经过多少个段。这要计算一下,2n 到 x 的长度占 2n 到 2n+1 长度的比例是多少。

举例:3.14

  • S = 0
    1. 2^1 < 3.14 <2^2。所以,n=1, n+127 = 128。所以,E=128。
  • (3.14 - 2) / (4 - 2) = 0.57, 而 0.57∗223=4781506.56,四舍五入,得到 M = 4781507。因为有四舍五入,所以,产生了浮点数据的精度问题。

回算:

整个代码是,之前生成的整数操作产生首次近似值后,将首次近似值作为参数送入函数最后两句进行精化处理。
代码中的两次迭代正是为了进一步提高结果的精度。

Git

常用命令

git add -p可以让你挑选改动提交

git grep $regexp $(git rev-list –all)可以用来在所有的提交中找代码

常见协同流

Git flow 工作流(产品现在使用的工作流)

  1. 合并分支巨多

    • 产生了恶心的 merge 节点,使得 git log 看起来十分混乱

      建议:只有 feature 合并到 developer 分支时,使用–no-ff 参数,其他的合并都不使用–no-ff参数来做合并。

    • 分支多可能忘了切

      看起来是个人问题,其实在这种模式下真的很常见,好的模式应该使得用户在无意中做出正确的操作,使看起来一切都是自然而然的

  2. dev-latest 和 master 大多数的情况下相似,但为此付出的额外维护成本并不低

  3. 不好回滚,revert 基本不能用

https://www.endoflineblog.com/gitflow-considered-harmful

http://luci.criosweb.ro/a-real-life-git-workflow-why-git-flow-does-not-work-for-us/

GitHub Flow

环境概念应用的不是很好,但是基本是参与开源的标配了

GitLab Flow

https://about.gitlab.com/blog/2014/09/29/gitlab-flow/

小结

协同工作流的本质,并不是怎么玩好代码仓库的分支策略,而是玩好我们的软件架构和软件开发流程

疑惑点:现有的 CI/CD 只是针对发版使用,不清楚在处理分支上能否有所应用

扩展阅读

https://gist.github.com/djspiewak/9f2f91085607a4859a66

小憩

说下为什么API都返回200,在Body里写错误信息:因为有的运营商会拦截非200请求,然后返回广告
200 是为了seo,如果搜索引擎发现异常状态码,会对你网站进行十分明显的排名降级

https://coolshell.cn/articles/17459.html

分布式

基本概念

CAP 定理表明,在存在网络分区的情况下,一致性和可用性必须二选一。而在没有发生网络故障时,即分布式系统正常运行时,一致性和可用性是可以同时被满足的

  • CA (consistency + availability),这样的系统关注一致性和可用性,它需要非常严格的全体一致的协议,比如“两阶段提交”(2PC)。CA 系统不能容忍网络错误或节点错误,一旦出现这样的问题,整个系统就会拒绝写请求,因为它并不知道对面的那个结点是否挂掉了,还是只是网络问题。唯一安全的做法就是把自己变成只读的。

  • CP (consistency + partition tolerance),这样的系统关注一致性和分区容忍性。它关注的是系统里大多数人的一致性协议,比如:Paxos 算法(Quorum 类的算法)。这样的系统只需要保证大多数结点数据一致,而少数的结点会在没有同步到最新版本的数据时变成不可用的状态。这样能够提供一部分的可用性。

  • AP (availability + partition tolerance),这样的系统关心可用性和分区容忍性。因此,这样的系统不能达成一致性,需要给出数据冲突,给出数据冲突就需要维护数据版本。Dynamo 就是这样的系统。

https://time.geekbang.org/column/article/2080

https://time.geekbang.org/column/article/2421

全栈监控

APM 监控,字节码插桩,无侵入实现

ELK, ZIPKIN

  • 容量管理。提供一个全局的系统运行时数据的展示,可以让工程师团队知道是否需要增加机器或者其它资源。

  • 性能管理。可以通过查看大盘,找到系统瓶颈,并有针对性地优化系统和相应代码。

  • “急诊”定位问题。可以快速地暴露并找到问题的发生点,帮助技术人员诊断问题。

  • 性能分析。当出现非预期的流量提升时,可以快速地找到系统的瓶颈,并帮助开发人员深入代码。

服务调度

微服务是服务依赖最优解的上限,而服务依赖的下限是千万不要有依赖环。

https://time.geekbang.org/column/article/1604

流量与数据调度

主要功能

  1. 服务流控。服务发现、服务路由、服务降级、服务熔断、服务保护等。
  2. 流量控制。负载均衡、流量分配、流量控制、异地灾备(多活)等。
  3. 流量管理。协议转换、请求校验、数据缓存、数据计算等。

关键技术

https://megaease.com/zh/

  • 高性能。API Gateway 必须使用高性能的技术,所以,也就需要使用高性能的语言。
  • 扛流量。要能扛流量,就需要使用集群技术。集群技术的关键点是在集群内的各个结点中共享数据。这就需要使用像 Paxos、Raft、Gossip 这样的通讯协议。因为 Gateway 需要部署在广域网上,所以还需要集群的分组技术。
  • 业务逻辑。API Gateway 需要有简单的业务逻辑,所以,最好是像 AWS 的 Lambda 服务一样,可以让人注入不同语言的简单业务逻辑。
  • 服务化。一个好的 API Gateway 需要能够通过 Admin API 来不停机地管理配置变更,而不是通过一个.conf 文件来人肉地修改配置。

小结

态数据调度应该是在 IaaS 层的数据存储解决的问题,而不是在 PaaS 层或者 SaaS 层来解决的。

  1. 对于应用层上的分布式事务一致性,只有两阶段提交这样的方式。
  2. 而底层存储可以解决这个问题的方式是通过一些像 Paxos、Raft 或是 NWR 这样的算法和模型来解决。
  3. 状态数据调度应该是由分布式存储系统来解决的,这样会更为完美。但是因为数据存储的 Scheme 太多,所以,导致我们有各式各样的分布式存储系统,有文件对象的,有关系型数据库的,有 NoSQL 的,有时序数据的,有搜索数据的,有队列的……

编程范式

泛型编程

  1. C 只能通过 void* 或者 宏替换 的形式来对数据类型适配。
  2. C 泛型需要引入额外的 size,这意味着要把泛型算法的信任交付给程序员。
  3. C 强大之处在于 使用 C 语言的程序员在高级语言的特性之上还能简单地做任何底层上的微观控制

https://time.geekbang.org/column/article/2017

函数式

参考 sicp https://time.geekbang.org/column/article/2711

  1. 惰性求值

  2. 确定性

惯用技术

  • first class function(头等函数) :这个技术可以让你的函数就像变量一样来使用。也就是说,你的函数可以像变量一样被创建、修改,并当成变量一样传递、返回,或是在函数中嵌套函数。
  • tail recursion optimization(尾递归优化) : 我们知道递归的害处,那就是如果递归很深的话,stack 受不了,并会导致性能大幅度下降。因此,我们使用尾递归优化技术——每次递归时都会重用 stack,这样能够提升性能。当然,这需要语言或编译器的支持。Python 就不支持。
  • map & reduce :这个技术不用多说了,函数式编程最常见的技术就是对一个集合做 Map 和 Reduce 操作。这比起过程式的语言来说,在代码上要更容易阅读。(传统过程式的语言需要使用 for/while 循环,然后在各种变量中把数据倒过来倒过去的)这个很像 C++ STL 中 foreach、find_if、count_if 等函数的玩法。
  • pipeline(管道):这个技术的意思是,将函数实例成一个一个的 action,然后将一组 action 放到一个数组或是列表中,再把数据传给这个 action list,数据就像一个 pipeline 一样顺序地被各个函数所操作,最终得到我们想要的结果。
  • recursing(递归) :递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。
  • currying(柯里化) :将一个函数的多个参数分解成多个函数, 然后将函数多层封装起来,每层函数都返回一个函数去接收下一个参数,这可以简化函数的多个参数。在 C++ 中,这很像 STL 中的 bind1st 或是 bind2nd。
  • higher order function(高阶函数):所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。这个技术用来做 Decorator 很不错。

修饰器

https://time.geekbang.org/column/article/2723

OOP

https://time.geekbang.org/column/article/2729

基于原型

https://time.geekbang.org/column/article/2741

Go 的委托模式

https://time.geekbang.org/column/article/2748

总结

https://time.geekbang.org/column/article/2751

https://time.geekbang.org/column/article/2752

https://time.geekbang.org/column/article/2754

弹力设计

隔离设计 Bulkheads

https://time.geekbang.org/column/article/3917

异步通讯

https://time.geekbang.org/column/article/3926

幂等性

服务状态

补偿事务

重试设计

熔断设计

限流设计

降级设计

总结

管理设计

分布式锁

1
2
3
4
5
6
7
SET resource_name my_random_value NX PX 30000

SET NX 命令只会在 key 不存在的时候给 key 赋值,PX 命令通知 Redis 保存这个 key 30000ms。
my_random_value 必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性。
PX 操作后面的参数代表的是这个 key 的存活时间,称作锁过期时间。
当资源被锁定超过这个时间时,锁将自动释放。
获得锁的客户端如果没有在这个时间窗口内完成操作,就可能会有其他客户端获得锁,引起争用问题。

配置中心

https://time.geekbang.org/column/article/5819

边车模式

https://time.geekbang.org/column/article/5909

服务网格

https://time.geekbang.org/column/article/5920

Gateway

https://time.geekbang.org/column/article/6086

部署升级策略

https://time.geekbang.org/column/article/6283

性能设计

Cache 内存和 IO 密集型的应用

cache aside pattern

所以,这也就是 Quora 上的那个答案里说的,要么通过 2PC 或是 Paxos 协议保证一致性,要么就是拼命地降低并发时脏数据的概率。而 Facebook 使用了这个降低概率的玩法,因为 2PC 太慢,而 Paxos 太复杂。当然,最好还是为缓存设置好过期时间

Read/Write Through 更新模式

  1. Cache Aside 是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载,从而对应用方是透明的

  2. 当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由 Cache 自己更新数据库(这是一个同步操作)。

Write Behind Caching 更新模式

异步处理

https://time.geekbang.org/column/article/7036

数据库扩展

业务层上,只有两阶段提交,数据层上,只有Paxos

https://time.geekbang.org/column/article/7045

秒杀

https://time.geekbang.org/column/article/7047

边缘计算

https://time.geekbang.org/column/article/7086

练级

入门篇

编码

Git

https://www.runoob.com/git/git-workspace-index-repo.html

https://github.com/git/git/blob/master/Documentation/CodingGuidelines

编程规范

https://google.github.io/styleguide/javaguide.html

理论学科

算法

算法列表

算法可视化

系统知识

https://time.geekbang.org/column/article/8888

TCP/IP

http://www.tcpipguide.com/index.htm

http://www.saminiir.com/lets-code-tcp-ip-stack-1-ethernet-arp/

CATALOG
  1. 1. Think More
    1. 1.1. 错误码的处理
      1. 1.1.1. 常见设计
      2. 1.1.2. 资源清理
      3. 1.1.3. 异常捕获处理
        1. 1.1.3.1. 优点
        2. 1.1.3.2. 问题
      4. 1.1.4. 小结
      5. 1.1.5. 异步处理
      6. 1.1.6. 大结
    2. 1.2. 浮点数
    3. 1.3. Git
      1. 1.3.1. 常用命令
      2. 1.3.2. 常见协同流
        1. 1.3.2.1. Git flow 工作流(产品现在使用的工作流)
        2. 1.3.2.2. GitHub Flow
        3. 1.3.2.3. GitLab Flow
        4. 1.3.2.4. 小结
        5. 1.3.2.5. 扩展阅读
        6. 1.3.2.6. 小憩
    4. 1.4. 分布式
      1. 1.4.1. 基本概念
      2. 1.4.2. 全栈监控
      3. 1.4.3. 服务调度
      4. 1.4.4. 流量与数据调度
        1. 1.4.4.1. 主要功能
        2. 1.4.4.2. 关键技术
        3. 1.4.4.3. 小结
    5. 1.5. 编程范式
      1. 1.5.1. 泛型编程
      2. 1.5.2. 函数式
        1. 1.5.2.1. 惯用技术
      3. 1.5.3. 修饰器
      4. 1.5.4. OOP
      5. 1.5.5. 基于原型
      6. 1.5.6. Go 的委托模式
      7. 1.5.7. 总结
    6. 1.6. 弹力设计
      1. 1.6.1. 隔离设计 Bulkheads
      2. 1.6.2. 异步通讯
      3. 1.6.3. 幂等性
      4. 1.6.4. 服务状态
      5. 1.6.5. 补偿事务
      6. 1.6.6. 重试设计
      7. 1.6.7. 熔断设计
      8. 1.6.8. 限流设计
      9. 1.6.9. 降级设计
      10. 1.6.10. 总结
    7. 1.7. 管理设计
      1. 1.7.1. 分布式锁
      2. 1.7.2. 配置中心
      3. 1.7.3. 边车模式
      4. 1.7.4. 服务网格
      5. 1.7.5. Gateway
      6. 1.7.6. 部署升级策略
    8. 1.8. 性能设计
      1. 1.8.1. Cache 内存和 IO 密集型的应用
        1. 1.8.1.1. cache aside pattern
        2. 1.8.1.2. Read/Write Through 更新模式
        3. 1.8.1.3. Write Behind Caching 更新模式
      2. 1.8.2. 异步处理
      3. 1.8.3. 数据库扩展
      4. 1.8.4. 秒杀
      5. 1.8.5. 边缘计算
    9. 1.9. 练级
      1. 1.9.1. 入门篇
        1. 1.9.1.1. 编码
        2. 1.9.1.2. Git
        3. 1.9.1.3. 编程规范
      2. 1.9.2. 理论学科
        1. 1.9.2.1. 算法
      3. 1.9.3. 系统知识
        1. 1.9.3.1. TCP/IP